aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.codeclimate.yml36
-rw-r--r--.gitattributes2
-rw-r--r--.github/autolabeler.yml24
-rw-r--r--.github/issue_template.md14
-rw-r--r--.github/no-response.yml12
-rw-r--r--.github/pull_request_template.md21
-rw-r--r--.github/stale.yml28
-rw-r--r--.gitignore32
-rw-r--r--.rubocop.yml218
-rw-r--r--.travis.yml215
-rw-r--r--.yarnrc2
-rw-r--r--Brewfile16
-rw-r--r--CODE_OF_CONDUCT.md12
-rw-r--r--CONTRIBUTING.md46
-rw-r--r--Gemfile186
-rw-r--r--Gemfile.lock735
-rw-r--r--MIT-LICENSE20
-rw-r--r--RAILS_VERSION2
-rw-r--r--README.md83
-rw-r--r--RELEASING_RAILS.md225
-rw-r--r--RELEASING_RAILS.rdoc205
-rw-r--r--Rakefile53
-rw-r--r--actioncable/.babelrc8
-rw-r--r--actioncable/.eslintrc19
-rw-r--r--actioncable/.gitignore3
-rw-r--r--actioncable/CHANGELOG.md24
-rw-r--r--actioncable/MIT-LICENSE20
-rw-r--r--actioncable/README.md567
-rw-r--r--actioncable/Rakefile47
-rw-r--r--actioncable/actioncable.gemspec35
-rw-r--r--actioncable/app/assets/javascripts/action_cable.js476
-rw-r--r--actioncable/app/javascript/action_cable/connection.js156
-rw-r--r--actioncable/app/javascript/action_cable/connection_monitor.js126
-rw-r--r--actioncable/app/javascript/action_cable/consumer.js53
-rw-r--r--actioncable/app/javascript/action_cable/index.js58
-rw-r--r--actioncable/app/javascript/action_cable/subscription.js89
-rw-r--r--actioncable/app/javascript/action_cable/subscriptions.js86
-rwxr-xr-xactioncable/bin/test5
-rw-r--r--actioncable/blade.yml28
-rw-r--r--actioncable/lib/action_cable.rb56
-rw-r--r--actioncable/lib/action_cable/channel.rb17
-rw-r--r--actioncable/lib/action_cable/channel/base.rb305
-rw-r--r--actioncable/lib/action_cable/channel/broadcasting.rb31
-rw-r--r--actioncable/lib/action_cable/channel/callbacks.rb37
-rw-r--r--actioncable/lib/action_cable/channel/naming.rb25
-rw-r--r--actioncable/lib/action_cable/channel/periodic_timers.rb78
-rw-r--r--actioncable/lib/action_cable/channel/streams.rb176
-rw-r--r--actioncable/lib/action_cable/channel/test_case.rb271
-rw-r--r--actioncable/lib/action_cable/connection.rb21
-rw-r--r--actioncable/lib/action_cable/connection/authorization.rb15
-rw-r--r--actioncable/lib/action_cable/connection/base.rb257
-rw-r--r--actioncable/lib/action_cable/connection/client_socket.rb157
-rw-r--r--actioncable/lib/action_cable/connection/identification.rb47
-rw-r--r--actioncable/lib/action_cable/connection/internal_channel.rb45
-rw-r--r--actioncable/lib/action_cable/connection/message_buffer.rb54
-rw-r--r--actioncable/lib/action_cable/connection/stream.rb117
-rw-r--r--actioncable/lib/action_cable/connection/stream_event_loop.rb136
-rw-r--r--actioncable/lib/action_cable/connection/subscriptions.rb79
-rw-r--r--actioncable/lib/action_cable/connection/tagged_logger_proxy.rb42
-rw-r--r--actioncable/lib/action_cable/connection/web_socket.rb41
-rw-r--r--actioncable/lib/action_cable/engine.rb79
-rw-r--r--actioncable/lib/action_cable/gem_version.rb17
-rw-r--r--actioncable/lib/action_cable/helpers/action_cable_helper.rb42
-rw-r--r--actioncable/lib/action_cable/remote_connections.rb71
-rw-r--r--actioncable/lib/action_cable/server.rb17
-rw-r--r--actioncable/lib/action_cable/server/base.rb89
-rw-r--r--actioncable/lib/action_cable/server/broadcasting.rb54
-rw-r--r--actioncable/lib/action_cable/server/configuration.rb56
-rw-r--r--actioncable/lib/action_cable/server/connections.rb36
-rw-r--r--actioncable/lib/action_cable/server/worker.rb77
-rw-r--r--actioncable/lib/action_cable/server/worker/active_record_connection_management.rb21
-rw-r--r--actioncable/lib/action_cable/subscription_adapter.rb12
-rw-r--r--actioncable/lib/action_cable/subscription_adapter/async.rb29
-rw-r--r--actioncable/lib/action_cable/subscription_adapter/base.rb30
-rw-r--r--actioncable/lib/action_cable/subscription_adapter/channel_prefix.rb28
-rw-r--r--actioncable/lib/action_cable/subscription_adapter/inline.rb37
-rw-r--r--actioncable/lib/action_cable/subscription_adapter/postgresql.rb130
-rw-r--r--actioncable/lib/action_cable/subscription_adapter/redis.rb179
-rw-r--r--actioncable/lib/action_cable/subscription_adapter/subscriber_map.rb59
-rw-r--r--actioncable/lib/action_cable/subscription_adapter/test.rb40
-rw-r--r--actioncable/lib/action_cable/test_case.rb11
-rw-r--r--actioncable/lib/action_cable/test_helper.rb133
-rw-r--r--actioncable/lib/action_cable/version.rb10
-rw-r--r--actioncable/lib/rails/generators/channel/USAGE12
-rw-r--r--actioncable/lib/rails/generators/channel/channel_generator.rb50
-rw-r--r--actioncable/lib/rails/generators/channel/templates/application_cable/channel.rb.tt4
-rw-r--r--actioncable/lib/rails/generators/channel/templates/application_cable/connection.rb.tt4
-rw-r--r--actioncable/lib/rails/generators/channel/templates/channel.rb.tt16
-rw-r--r--actioncable/lib/rails/generators/channel/templates/javascript/channel.js.tt20
-rw-r--r--actioncable/lib/rails/generators/channel/templates/javascript/consumer.js.tt6
-rw-r--r--actioncable/lib/rails/generators/channel/templates/javascript/index.js.tt5
-rw-r--r--actioncable/package.json41
-rw-r--r--actioncable/rollup.config.js28
-rw-r--r--actioncable/test/channel/base_test.rb274
-rw-r--r--actioncable/test/channel/broadcasting_test.rb39
-rw-r--r--actioncable/test/channel/naming_test.rb12
-rw-r--r--actioncable/test/channel/periodic_timers_test.rb85
-rw-r--r--actioncable/test/channel/rejection_test.rb56
-rw-r--r--actioncable/test/channel/stream_test.rb230
-rw-r--r--actioncable/test/channel/test_case_test.rb188
-rw-r--r--actioncable/test/client_test.rb314
-rw-r--r--actioncable/test/connection/authorization_test.rb33
-rw-r--r--actioncable/test/connection/base_test.rb143
-rw-r--r--actioncable/test/connection/client_socket_test.rb93
-rw-r--r--actioncable/test/connection/cross_site_forgery_test.rb92
-rw-r--r--actioncable/test/connection/identifier_test.rb77
-rw-r--r--actioncable/test/connection/multiple_identifiers_test.rb34
-rw-r--r--actioncable/test/connection/stream_test.rb69
-rw-r--r--actioncable/test/connection/string_identifier_test.rb36
-rw-r--r--actioncable/test/connection/subscriptions_test.rb119
-rw-r--r--actioncable/test/javascript/src/test.coffee3
-rw-r--r--actioncable/test/javascript/src/test_helpers/consumer_test_helper.coffee47
-rw-r--r--actioncable/test/javascript/src/test_helpers/index.coffee11
-rw-r--r--actioncable/test/javascript/src/unit/action_cable_test.coffee41
-rw-r--r--actioncable/test/javascript/src/unit/consumer_test.coffee14
-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/javascript/vendor/mock-socket.js4532
-rw-r--r--actioncable/test/server/base_test.rb38
-rw-r--r--actioncable/test/server/broadcasting_test.rb62
-rw-r--r--actioncable/test/stubs/global_id.rb10
-rw-r--r--actioncable/test/stubs/room.rb18
-rw-r--r--actioncable/test/stubs/test_adapter.rb14
-rw-r--r--actioncable/test/stubs/test_connection.rb35
-rw-r--r--actioncable/test/stubs/test_server.rb32
-rw-r--r--actioncable/test/stubs/user.rb17
-rw-r--r--actioncable/test/subscription_adapter/async_test.rb19
-rw-r--r--actioncable/test/subscription_adapter/base_test.rb65
-rw-r--r--actioncable/test/subscription_adapter/channel_prefix.rb38
-rw-r--r--actioncable/test/subscription_adapter/common.rb131
-rw-r--r--actioncable/test/subscription_adapter/inline_test.rb19
-rw-r--r--actioncable/test/subscription_adapter/postgresql_test.rb65
-rw-r--r--actioncable/test/subscription_adapter/redis_test.rb52
-rw-r--r--actioncable/test/subscription_adapter/subscriber_map_test.rb19
-rw-r--r--actioncable/test/subscription_adapter/test_adapter_test.rb47
-rw-r--r--actioncable/test/test_helper.rb43
-rw-r--r--actioncable/test/test_helper_test.rb116
-rw-r--r--actioncable/test/worker_test.rb46
-rw-r--r--actioncable/yarn.lock1928
-rw-r--r--actionmailer/CHANGELOG.md105
-rw-r--r--actionmailer/MIT-LICENSE2
-rw-r--r--actionmailer/README.rdoc9
-rw-r--r--actionmailer/Rakefile12
-rw-r--r--actionmailer/actionmailer.gemspec44
-rwxr-xr-xactionmailer/bin/test7
-rw-r--r--actionmailer/lib/action_mailer.rb36
-rw-r--r--actionmailer/lib/action_mailer/base.rb430
-rw-r--r--actionmailer/lib/action_mailer/collector.rb8
-rw-r--r--actionmailer/lib/action_mailer/delivery_job.rb25
-rw-r--r--actionmailer/lib/action_mailer/delivery_methods.rb35
-rw-r--r--actionmailer/lib/action_mailer/gem_version.rb6
-rw-r--r--actionmailer/lib/action_mailer/inline_preview_interceptor.rb22
-rw-r--r--actionmailer/lib/action_mailer/log_subscriber.rb19
-rw-r--r--actionmailer/lib/action_mailer/mail_helper.rb4
-rw-r--r--actionmailer/lib/action_mailer/message_delivery.rb92
-rw-r--r--actionmailer/lib/action_mailer/parameterized.rb155
-rw-r--r--actionmailer/lib/action_mailer/preview.rb79
-rw-r--r--actionmailer/lib/action_mailer/railtie.rb45
-rw-r--r--actionmailer/lib/action_mailer/rescuable.rb29
-rw-r--r--actionmailer/lib/action_mailer/test_case.rb41
-rw-r--r--actionmailer/lib/action_mailer/test_helper.rb62
-rw-r--r--actionmailer/lib/action_mailer/version.rb4
-rw-r--r--actionmailer/lib/rails/generators/mailer/mailer_generator.rb27
-rw-r--r--actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb4
-rw-r--r--actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb.tt6
-rw-r--r--actionmailer/lib/rails/generators/mailer/templates/mailer.rb.tt (renamed from actionmailer/lib/rails/generators/mailer/templates/mailer.rb)0
-rw-r--r--actionmailer/test/abstract_unit.rb52
-rw-r--r--actionmailer/test/assert_select_email_test.rb12
-rw-r--r--actionmailer/test/asset_host_test.rb20
-rw-r--r--actionmailer/test/base_test.rb491
-rw-r--r--actionmailer/test/caching_test.rb271
-rw-r--r--actionmailer/test/delivery_methods_test.rb62
-rw-r--r--actionmailer/test/fixtures/async_mailer/welcome.erb1
-rw-r--r--actionmailer/test/fixtures/caching_mailer/_partial.html.erb3
-rw-r--r--actionmailer/test/fixtures/caching_mailer/fragment_cache.html.erb3
-rw-r--r--actionmailer/test/fixtures/caching_mailer/fragment_cache_in_partials.html.erb1
-rw-r--r--actionmailer/test/fixtures/caching_mailer/fragment_caching_options.html.erb3
-rw-r--r--actionmailer/test/fixtures/caching_mailer/multipart_cache.html.erb3
-rw-r--r--actionmailer/test/fixtures/caching_mailer/multipart_cache.text.erb3
-rw-r--r--actionmailer/test/fixtures/caching_mailer/skip_fragment_cache_digesting.html.erb3
-rw-r--r--actionmailer/test/i18n_with_controller_test.rb41
-rw-r--r--actionmailer/test/log_subscriber_test.rb28
-rw-r--r--actionmailer/test/mail_helper_test.rb24
-rw-r--r--actionmailer/test/mail_layout_test.rb20
-rw-r--r--actionmailer/test/mailers/asset_mailer.rb4
-rw-r--r--actionmailer/test/mailers/base_mailer.rb53
-rw-r--r--actionmailer/test/mailers/caching_mailer.rb25
-rw-r--r--actionmailer/test/mailers/delayed_mailer.rb24
-rw-r--r--actionmailer/test/mailers/params_mailer.rb13
-rw-r--r--actionmailer/test/mailers/proc_mailer.rb22
-rw-r--r--actionmailer/test/message_delivery_test.rb128
-rw-r--r--actionmailer/test/parameterized_test.rb69
-rw-r--r--actionmailer/test/test_case_test.rb44
-rw-r--r--actionmailer/test/test_helper_test.rb121
-rw-r--r--actionmailer/test/url_test.rb66
-rw-r--r--actionpack/CHANGELOG.md432
-rw-r--r--actionpack/MIT-LICENSE2
-rw-r--r--actionpack/README.rdoc11
-rw-r--r--actionpack/Rakefile22
-rw-r--r--actionpack/actionpack.gemspec48
-rwxr-xr-xactionpack/bin/test7
-rw-r--r--actionpack/lib/abstract_controller.rb16
-rw-r--r--actionpack/lib/abstract_controller/asset_paths.rb2
-rw-r--r--actionpack/lib/abstract_controller/base.rb55
-rw-r--r--actionpack/lib/abstract_controller/caching.rb66
-rw-r--r--actionpack/lib/abstract_controller/caching/fragments.rb170
-rw-r--r--actionpack/lib/abstract_controller/callbacks.rb92
-rw-r--r--actionpack/lib/abstract_controller/collector.rb21
-rw-r--r--actionpack/lib/abstract_controller/error.rb6
-rw-r--r--actionpack/lib/abstract_controller/helpers.rb52
-rw-r--r--actionpack/lib/abstract_controller/logger.rb2
-rw-r--r--actionpack/lib/abstract_controller/railties/routes_helpers.rb4
-rw-r--r--actionpack/lib/abstract_controller/rendering.rb76
-rw-r--r--actionpack/lib/abstract_controller/translation.rb8
-rw-r--r--actionpack/lib/abstract_controller/url_for.rb2
-rw-r--r--actionpack/lib/action_controller.rb45
-rw-r--r--actionpack/lib/action_controller/api.rb55
-rw-r--r--actionpack/lib/action_controller/api/api_rendering.rb16
-rw-r--r--actionpack/lib/action_controller/base.rb45
-rw-r--r--actionpack/lib/action_controller/caching.rb71
-rw-r--r--actionpack/lib/action_controller/caching/fragments.rb103
-rw-r--r--actionpack/lib/action_controller/form_builder.rb2
-rw-r--r--actionpack/lib/action_controller/log_subscriber.rb23
-rw-r--r--actionpack/lib/action_controller/metal.rb171
-rw-r--r--actionpack/lib/action_controller/metal/basic_implicit_render.rb4
-rw-r--r--actionpack/lib/action_controller/metal/conditional_get.rb108
-rw-r--r--actionpack/lib/action_controller/metal/content_security_policy.rb52
-rw-r--r--actionpack/lib/action_controller/metal/cookies.rb6
-rw-r--r--actionpack/lib/action_controller/metal/data_streaming.rb78
-rw-r--r--actionpack/lib/action_controller/metal/default_headers.rb17
-rw-r--r--actionpack/lib/action_controller/metal/etag_with_flash.rb18
-rw-r--r--actionpack/lib/action_controller/metal/etag_with_template_digest.rb33
-rw-r--r--actionpack/lib/action_controller/metal/exceptions.rb45
-rw-r--r--actionpack/lib/action_controller/metal/flash.rb9
-rw-r--r--actionpack/lib/action_controller/metal/force_ssl.rb85
-rw-r--r--actionpack/lib/action_controller/metal/head.rb44
-rw-r--r--actionpack/lib/action_controller/metal/helpers.rb33
-rw-r--r--actionpack/lib/action_controller/metal/http_authentication.rb100
-rw-r--r--actionpack/lib/action_controller/metal/implicit_render.rb67
-rw-r--r--actionpack/lib/action_controller/metal/instrumentation.rb40
-rw-r--r--actionpack/lib/action_controller/metal/live.rb214
-rw-r--r--actionpack/lib/action_controller/metal/mime_responds.rb47
-rw-r--r--actionpack/lib/action_controller/metal/parameter_encoding.rb51
-rw-r--r--actionpack/lib/action_controller/metal/params_wrapper.rb113
-rw-r--r--actionpack/lib/action_controller/metal/rack_delegation.rb38
-rw-r--r--actionpack/lib/action_controller/metal/redirecting.rb77
-rw-r--r--actionpack/lib/action_controller/metal/renderers.rb131
-rw-r--r--actionpack/lib/action_controller/metal/rendering.rb118
-rw-r--r--actionpack/lib/action_controller/metal/request_forgery_protection.rb286
-rw-r--r--actionpack/lib/action_controller/metal/rescue.rb25
-rw-r--r--actionpack/lib/action_controller/metal/streaming.rb16
-rw-r--r--actionpack/lib/action_controller/metal/strong_parameters.rb789
-rw-r--r--actionpack/lib/action_controller/metal/testing.rb19
-rw-r--r--actionpack/lib/action_controller/metal/url_for.rb18
-rw-r--r--actionpack/lib/action_controller/middleware.rb39
-rw-r--r--actionpack/lib/action_controller/railtie.rb38
-rw-r--r--actionpack/lib/action_controller/railties/helpers.rb4
-rw-r--r--actionpack/lib/action_controller/renderer.rb130
-rw-r--r--actionpack/lib/action_controller/template_assertions.rb2
-rw-r--r--actionpack/lib/action_controller/test_case.rb403
-rw-r--r--actionpack/lib/action_dispatch.rb46
-rw-r--r--actionpack/lib/action_dispatch/http/cache.rb146
-rw-r--r--actionpack/lib/action_dispatch/http/content_disposition.rb45
-rw-r--r--actionpack/lib/action_dispatch/http/content_security_policy.rb273
-rw-r--r--actionpack/lib/action_dispatch/http/filter_parameters.rb52
-rw-r--r--actionpack/lib/action_dispatch/http/filter_redirect.rb19
-rw-r--r--actionpack/lib/action_dispatch/http/headers.rb77
-rw-r--r--actionpack/lib/action_dispatch/http/mime_negotiation.rb92
-rw-r--r--actionpack/lib/action_dispatch/http/mime_type.rb265
-rw-r--r--actionpack/lib/action_dispatch/http/mime_types.rb28
-rw-r--r--actionpack/lib/action_dispatch/http/parameter_filter.rb86
-rw-r--r--actionpack/lib/action_dispatch/http/parameters.rb141
-rw-r--r--actionpack/lib/action_dispatch/http/rack_cache.rb2
-rw-r--r--actionpack/lib/action_dispatch/http/request.rb273
-rw-r--r--actionpack/lib/action_dispatch/http/response.rb307
-rw-r--r--actionpack/lib/action_dispatch/http/upload.rb29
-rw-r--r--actionpack/lib/action_dispatch/http/url.rb143
-rw-r--r--actionpack/lib/action_dispatch/journey.rb12
-rw-r--r--actionpack/lib/action_dispatch/journey/backwards.rb5
-rw-r--r--actionpack/lib/action_dispatch/journey/formatter.rb72
-rw-r--r--actionpack/lib/action_dispatch/journey/gtg/builder.rb12
-rw-r--r--actionpack/lib/action_dispatch/journey/gtg/simulator.rb12
-rw-r--r--actionpack/lib/action_dispatch/journey/gtg/transition_table.rb33
-rw-r--r--actionpack/lib/action_dispatch/journey/nfa/builder.rb8
-rw-r--r--actionpack/lib/action_dispatch/journey/nfa/dot.rb26
-rw-r--r--actionpack/lib/action_dispatch/journey/nfa/simulator.rb6
-rw-r--r--actionpack/lib/action_dispatch/journey/nfa/transition_table.rb6
-rw-r--r--actionpack/lib/action_dispatch/journey/nodes/node.rb24
-rw-r--r--actionpack/lib/action_dispatch/journey/parser.rb45
-rw-r--r--actionpack/lib/action_dispatch/journey/parser.y5
-rw-r--r--actionpack/lib/action_dispatch/journey/parser_extras.rb16
-rw-r--r--actionpack/lib/action_dispatch/journey/path/pattern.rb86
-rw-r--r--actionpack/lib/action_dispatch/journey/route.rb123
-rw-r--r--actionpack/lib/action_dispatch/journey/router.rb55
-rw-r--r--actionpack/lib/action_dispatch/journey/router/strexp.rb27
-rw-r--r--actionpack/lib/action_dispatch/journey/router/utils.rb41
-rw-r--r--actionpack/lib/action_dispatch/journey/routes.rb17
-rw-r--r--actionpack/lib/action_dispatch/journey/scanner.rb44
-rw-r--r--actionpack/lib/action_dispatch/journey/visitors.rb151
-rw-r--r--actionpack/lib/action_dispatch/middleware/callbacks.rb3
-rw-r--r--actionpack/lib/action_dispatch/middleware/cookies.rb526
-rw-r--r--actionpack/lib/action_dispatch/middleware/debug_exceptions.rb215
-rw-r--r--actionpack/lib/action_dispatch/middleware/debug_locks.rb124
-rw-r--r--actionpack/lib/action_dispatch/middleware/exception_wrapper.rb169
-rw-r--r--actionpack/lib/action_dispatch/middleware/executor.rb21
-rw-r--r--actionpack/lib/action_dispatch/middleware/flash.rb100
-rw-r--r--actionpack/lib/action_dispatch/middleware/load_interlock.rb21
-rw-r--r--actionpack/lib/action_dispatch/middleware/params_parser.rb55
-rw-r--r--actionpack/lib/action_dispatch/middleware/public_exceptions.rb52
-rw-r--r--actionpack/lib/action_dispatch/middleware/reloader.rb96
-rw-r--r--actionpack/lib/action_dispatch/middleware/remote_ip.rb68
-rw-r--r--actionpack/lib/action_dispatch/middleware/request_id.rb15
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/abstract_store.rb54
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/cache_store.rb14
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/cookie_store.rb124
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb6
-rw-r--r--actionpack/lib/action_dispatch/middleware/show_exceptions.rb48
-rw-r--r--actionpack/lib/action_dispatch/middleware/ssl.rb148
-rw-r--r--actionpack/lib/action_dispatch/middleware/stack.rb75
-rw-r--r--actionpack/lib/action_dispatch/middleware/static.rb94
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb27
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb29
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_source.text.erb8
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb80
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb22
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb21
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb13
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb1
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb19
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb3
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb4
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb6
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb11
-rw-r--r--actionpack/lib/action_dispatch/railtie.rb29
-rw-r--r--actionpack/lib/action_dispatch/request/session.rb125
-rw-r--r--actionpack/lib/action_dispatch/request/utils.rb87
-rw-r--r--actionpack/lib/action_dispatch/routing.rb37
-rw-r--r--actionpack/lib/action_dispatch/routing/endpoint.rb11
-rw-r--r--actionpack/lib/action_dispatch/routing/inspector.rb224
-rw-r--r--actionpack/lib/action_dispatch/routing/mapper.rb1169
-rw-r--r--actionpack/lib/action_dispatch/routing/polymorphic_routes.rb301
-rw-r--r--actionpack/lib/action_dispatch/routing/redirection.rb62
-rw-r--r--actionpack/lib/action_dispatch/routing/route_set.rb509
-rw-r--r--actionpack/lib/action_dispatch/routing/routes_proxy.rb37
-rw-r--r--actionpack/lib/action_dispatch/routing/url_for.rb73
-rw-r--r--actionpack/lib/action_dispatch/system_test_case.rb147
-rw-r--r--actionpack/lib/action_dispatch/system_testing/browser.rb49
-rw-r--r--actionpack/lib/action_dispatch/system_testing/driver.rb59
-rw-r--r--actionpack/lib/action_dispatch/system_testing/server.rb31
-rw-r--r--actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb96
-rw-r--r--actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb31
-rw-r--r--actionpack/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb26
-rw-r--r--actionpack/lib/action_dispatch/testing/assertion_response.rb47
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions.rb10
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/response.rb64
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/routing.rb65
-rw-r--r--actionpack/lib/action_dispatch/testing/integration.rb499
-rw-r--r--actionpack/lib/action_dispatch/testing/request_encoder.rb55
-rw-r--r--actionpack/lib/action_dispatch/testing/test_process.rb44
-rw-r--r--actionpack/lib/action_dispatch/testing/test_request.rb38
-rw-r--r--actionpack/lib/action_dispatch/testing/test_response.rb42
-rw-r--r--actionpack/lib/action_pack.rb6
-rw-r--r--actionpack/lib/action_pack/gem_version.rb4
-rw-r--r--actionpack/lib/action_pack/version.rb4
-rw-r--r--actionpack/test/abstract/callbacks_test.rb97
-rw-r--r--actionpack/test/abstract/collector_test.rb10
-rw-r--r--actionpack/test/abstract/translation_test.rb40
-rw-r--r--actionpack/test/abstract_unit.rb343
-rw-r--r--actionpack/test/assertions/response_assertions_test.rb90
-rw-r--r--actionpack/test/controller/action_pack_assertions_test.rb220
-rw-r--r--actionpack/test/controller/api/conditional_get_test.rb24
-rw-r--r--actionpack/test/controller/api/data_streaming_test.rb10
-rw-r--r--actionpack/test/controller/api/force_ssl_test.rb8
-rw-r--r--actionpack/test/controller/api/implicit_render_test.rb4
-rw-r--r--actionpack/test/controller/api/params_wrapper_test.rb12
-rw-r--r--actionpack/test/controller/api/redirect_to_test.rb4
-rw-r--r--actionpack/test/controller/api/renderers_test.rb24
-rw-r--r--actionpack/test/controller/api/url_for_test.rb6
-rw-r--r--actionpack/test/controller/api/with_cookies_test.rb23
-rw-r--r--actionpack/test/controller/api/with_helpers_test.rb44
-rw-r--r--actionpack/test/controller/base_test.rb149
-rw-r--r--actionpack/test/controller/caching_test.rb278
-rw-r--r--actionpack/test/controller/content_type_test.rb78
-rw-r--r--actionpack/test/controller/default_url_options_with_before_action_test.rb12
-rw-r--r--actionpack/test/controller/filters_test.rb326
-rw-r--r--actionpack/test/controller/flash_hash_test.rb164
-rw-r--r--actionpack/test/controller/flash_test.rb130
-rw-r--r--actionpack/test/controller/force_ssl_test.rb113
-rw-r--r--actionpack/test/controller/form_builder_test.rb4
-rw-r--r--actionpack/test/controller/helper_test.rb101
-rw-r--r--actionpack/test/controller/http_basic_authentication_test.rb111
-rw-r--r--actionpack/test/controller/http_digest_authentication_test.rb189
-rw-r--r--actionpack/test/controller/http_token_authentication_test.rb83
-rw-r--r--actionpack/test/controller/integration_test.rb893
-rw-r--r--actionpack/test/controller/live_stream_test.rb292
-rw-r--r--actionpack/test/controller/localized_templates_test.rb4
-rw-r--r--actionpack/test/controller/log_subscriber_test.rb66
-rw-r--r--actionpack/test/controller/metal/renderers_test.rb50
-rw-r--r--actionpack/test/controller/metal_test.rb32
-rw-r--r--actionpack/test/controller/mime/accept_format_test.rb12
-rw-r--r--actionpack/test/controller/mime/respond_to_test.rb366
-rw-r--r--actionpack/test/controller/new_base/bare_metal_test.rb68
-rw-r--r--actionpack/test/controller/new_base/base_test.rb39
-rw-r--r--actionpack/test/controller/new_base/content_negotiation_test.rb7
-rw-r--r--actionpack/test/controller/new_base/content_type_test.rb28
-rw-r--r--actionpack/test/controller/new_base/metal_test.rb43
-rw-r--r--actionpack/test/controller/new_base/middleware_test.rb12
-rw-r--r--actionpack/test/controller/new_base/render_action_test.rb57
-rw-r--r--actionpack/test/controller/new_base/render_body_test.rb12
-rw-r--r--actionpack/test/controller/new_base/render_context_test.rb23
-rw-r--r--actionpack/test/controller/new_base/render_file_test.rb28
-rw-r--r--actionpack/test/controller/new_base/render_html_test.rb10
-rw-r--r--actionpack/test/controller/new_base/render_implicit_action_test.rb6
-rw-r--r--actionpack/test/controller/new_base/render_layout_test.rb15
-rw-r--r--actionpack/test/controller/new_base/render_partial_test.rb15
-rw-r--r--actionpack/test/controller/new_base/render_plain_test.rb10
-rw-r--r--actionpack/test/controller/new_base/render_streaming_test.rb24
-rw-r--r--actionpack/test/controller/new_base/render_template_test.rb49
-rw-r--r--actionpack/test/controller/new_base/render_test.rb26
-rw-r--r--actionpack/test/controller/new_base/render_text_test.rb158
-rw-r--r--actionpack/test/controller/new_base/render_xml_test.rb5
-rw-r--r--actionpack/test/controller/output_escaping_test.rb8
-rw-r--r--actionpack/test/controller/parameter_encoding_test.rb52
-rw-r--r--actionpack/test/controller/parameters/accessors_test.rb279
-rw-r--r--actionpack/test/controller/parameters/always_permitted_parameters_test.rb22
-rw-r--r--actionpack/test/controller/parameters/dup_test.rb67
-rw-r--r--actionpack/test/controller/parameters/log_on_unpermitted_params_test.rb56
-rw-r--r--actionpack/test/controller/parameters/multi_parameter_attributes_test.rb13
-rw-r--r--actionpack/test/controller/parameters/mutators_test.rb72
-rw-r--r--actionpack/test/controller/parameters/nested_parameters_permit_test.rb184
-rw-r--r--actionpack/test/controller/parameters/nested_parameters_test.rb187
-rw-r--r--actionpack/test/controller/parameters/parameters_permit_test.rb393
-rw-r--r--actionpack/test/controller/parameters/raise_on_unpermitted_params_test.rb16
-rw-r--r--actionpack/test/controller/parameters/serialization_test.rb54
-rw-r--r--actionpack/test/controller/params_parse_test.rb34
-rw-r--r--actionpack/test/controller/params_wrapper_test.rb264
-rw-r--r--actionpack/test/controller/permitted_params_test.rb8
-rw-r--r--actionpack/test/controller/redirect_test.rb158
-rw-r--r--actionpack/test/controller/render_js_test.rb18
-rw-r--r--actionpack/test/controller/render_json_test.rb57
-rw-r--r--actionpack/test/controller/render_other_test.rb24
-rw-r--r--actionpack/test/controller/render_test.rb495
-rw-r--r--actionpack/test/controller/render_xml_test.rb31
-rw-r--r--actionpack/test/controller/renderer_test.rb123
-rw-r--r--actionpack/test/controller/renderers_test.rb91
-rw-r--r--actionpack/test/controller/request/test_request_test.rb43
-rw-r--r--actionpack/test/controller/request_forgery_protection_test.rb686
-rw-r--r--actionpack/test/controller/required_params_test.rb53
-rw-r--r--actionpack/test/controller/rescue_test.rb160
-rw-r--r--actionpack/test/controller/resources_test.rb826
-rw-r--r--actionpack/test/controller/routing_test.rb1512
-rw-r--r--actionpack/test/controller/runner_test.rb10
-rw-r--r--actionpack/test/controller/send_file_test.rb209
-rw-r--r--actionpack/test/controller/show_exceptions_test.rb60
-rw-r--r--actionpack/test/controller/streaming_test.rb6
-rw-r--r--actionpack/test/controller/test_case_test.rb715
-rw-r--r--actionpack/test/controller/url_for_integration_test.rb267
-rw-r--r--actionpack/test/controller/url_for_test.rb353
-rw-r--r--actionpack/test/controller/url_rewriter_test.rb63
-rw-r--r--actionpack/test/controller/webservice_test.rb78
-rw-r--r--actionpack/test/dispatch/callbacks_test.rb23
-rw-r--r--actionpack/test/dispatch/content_disposition_test.rb37
-rw-r--r--actionpack/test/dispatch/content_security_policy_test.rb546
-rw-r--r--actionpack/test/dispatch/cookies_test.rb980
-rw-r--r--actionpack/test/dispatch/debug_exceptions_test.rb444
-rw-r--r--actionpack/test/dispatch/debug_locks_test.rb38
-rw-r--r--actionpack/test/dispatch/exception_wrapper_test.rb109
-rw-r--r--actionpack/test/dispatch/executor_test.rb136
-rw-r--r--actionpack/test/dispatch/header_test.rb78
-rw-r--r--actionpack/test/dispatch/live_response_test.rb58
-rw-r--r--actionpack/test/dispatch/mapper_test.rb160
-rw-r--r--actionpack/test/dispatch/middleware_stack/middleware_test.rb77
-rw-r--r--actionpack/test/dispatch/middleware_stack_test.rb58
-rw-r--r--actionpack/test/dispatch/mime_type_test.rb136
-rw-r--r--actionpack/test/dispatch/mount_test.rb26
-rw-r--r--actionpack/test/dispatch/prefix_generation_test.rb252
-rw-r--r--actionpack/test/dispatch/rack_cache_test.rb8
-rw-r--r--actionpack/test/dispatch/reloader_test.rb117
-rw-r--r--actionpack/test/dispatch/request/json_params_parsing_test.rb103
-rw-r--r--actionpack/test/dispatch/request/multipart_params_parsing_test.rb123
-rw-r--r--actionpack/test/dispatch/request/query_string_parsing_test.rb72
-rw-r--r--actionpack/test/dispatch/request/session_test.rb159
-rw-r--r--actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb16
-rw-r--r--actionpack/test/dispatch/request_id_test.rb50
-rw-r--r--actionpack/test/dispatch/request_test.rb990
-rw-r--r--actionpack/test/dispatch/response_test.rb380
-rw-r--r--actionpack/test/dispatch/routing/concerns_test.rb6
-rw-r--r--actionpack/test/dispatch/routing/custom_url_helpers_test.rb333
-rw-r--r--actionpack/test/dispatch/routing/inspector_test.rb277
-rw-r--r--actionpack/test/dispatch/routing/ipv6_redirect_test.rb46
-rw-r--r--actionpack/test/dispatch/routing/route_set_test.rb83
-rw-r--r--actionpack/test/dispatch/routing_assertions_test.rb150
-rw-r--r--actionpack/test/dispatch/routing_test.rb4086
-rw-r--r--actionpack/test/dispatch/runner_test.rb19
-rw-r--r--actionpack/test/dispatch/session/abstract_store_test.rb26
-rw-r--r--actionpack/test/dispatch/session/cache_store_test.rb100
-rw-r--r--actionpack/test/dispatch/session/cookie_store_test.rb277
-rw-r--r--actionpack/test/dispatch/session/mem_cache_store_test.rb102
-rw-r--r--actionpack/test/dispatch/session/test_session_test.rb50
-rw-r--r--actionpack/test/dispatch/show_exceptions_test.rb63
-rw-r--r--actionpack/test/dispatch/ssl_test.rb328
-rw-r--r--actionpack/test/dispatch/static_test.rb131
-rw-r--r--actionpack/test/dispatch/system_testing/driver_test.rb54
-rw-r--r--actionpack/test/dispatch/system_testing/screenshot_helper_test.rb81
-rw-r--r--actionpack/test/dispatch/system_testing/server_test.rb32
-rw-r--r--actionpack/test/dispatch/system_testing/system_test_case_test.rb84
-rw-r--r--actionpack/test/dispatch/test_request_test.rb74
-rw-r--r--actionpack/test/dispatch/test_response_test.rb26
-rw-r--r--actionpack/test/dispatch/uploaded_file_test.rb86
-rw-r--r--actionpack/test/dispatch/url_generation_test.rb31
-rw-r--r--actionpack/test/fixtures/alternate_helpers/foo_helper.rb4
-rw-r--r--actionpack/test/fixtures/collection_cache/index.html.erb2
-rw-r--r--actionpack/test/fixtures/company.rb4
-rw-r--r--actionpack/test/fixtures/functional_caching/_formatted_partial.html.erb1
-rw-r--r--actionpack/test/fixtures/functional_caching/formatted_fragment_cached.html.erb2
-rw-r--r--actionpack/test/fixtures/functional_caching/formatted_fragment_cached.xml.builder2
-rw-r--r--actionpack/test/fixtures/functional_caching/formatted_fragment_cached_with_variant.html+phone.erb2
-rw-r--r--actionpack/test/fixtures/functional_caching/fragment_cached.html.erb2
-rw-r--r--actionpack/test/fixtures/functional_caching/fragment_cached_with_options.html.erb3
-rw-r--r--actionpack/test/fixtures/functional_caching/xml_fragment_cached_with_html_partial.xml.builder5
-rw-r--r--actionpack/test/fixtures/helpers/abc_helper.rb2
-rw-r--r--actionpack/test/fixtures/helpers/fun/games_helper.rb4
-rw-r--r--actionpack/test/fixtures/helpers/fun/pdf_helper.rb4
-rw-r--r--actionpack/test/fixtures/helpers/just_me_helper.rb4
-rw-r--r--actionpack/test/fixtures/helpers/me_too_helper.rb4
-rw-r--r--actionpack/test/fixtures/helpers1_pack/pack1_helper.rb2
-rw-r--r--actionpack/test/fixtures/helpers2_pack/pack2_helper.rb2
-rw-r--r--actionpack/test/fixtures/helpers_typo/admin/users_helper.rb3
-rw-r--r--actionpack/test/fixtures/implicit_render_test/empty_action_with_mobile_variant.html+mobile.erb1
-rw-r--r--actionpack/test/fixtures/implicit_render_test/empty_action_with_template.html.erb1
-rw-r--r--actionpack/test/fixtures/layouts/builder.builder2
-rw-r--r--actionpack/test/fixtures/load_me.rb4
-rw-r--r--actionpack/test/fixtures/multipart/mona_lisa.jpgbin159528 -> 0 bytes
-rw-r--r--actionpack/test/fixtures/multipart/ruby_on_rails.jpgbin0 -> 45142 bytes
-rw-r--r--actionpack/test/fixtures/multipart/utf8_filename10
-rw-r--r--actionpack/test/fixtures/namespaced/implicit_render_test/hello_world.erb1
-rw-r--r--actionpack/test/fixtures/old_content_type/render_default_for_builder.builder2
-rw-r--r--actionpack/test/fixtures/public/foo/さようなら.html1
-rw-r--r--actionpack/test/fixtures/public/foo/さようなら.html.gzbin0 -> 67 bytes
-rw-r--r--actionpack/test/fixtures/respond_to/using_defaults.xml.builder2
-rw-r--r--actionpack/test/fixtures/respond_to/using_defaults_with_type_list.xml.builder2
-rw-r--r--actionpack/test/fixtures/respond_to/variant_with_implicit_template_rendering.html+mobile.erb (renamed from actionpack/test/fixtures/respond_to/variant_with_implicit_rendering.html+mobile.erb)0
-rw-r--r--actionpack/test/fixtures/session_autoload_test/session_autoload_test/foo.rb4
-rw-r--r--actionpack/test/fixtures/test/formatted_xml_erb.builder2
-rw-r--r--actionpack/test/fixtures/test/hello_xml_world.builder2
-rw-r--r--actionpack/test/fixtures/test/with_implicit_template.erb1
-rw-r--r--actionpack/test/fixtures/公共/foo/さようなら.html1
-rw-r--r--actionpack/test/fixtures/公共/foo/さようなら.html.gzbin0 -> 67 bytes
-rw-r--r--actionpack/test/journey/gtg/builder_test.rb58
-rw-r--r--actionpack/test/journey/gtg/transition_table_test.rb88
-rw-r--r--actionpack/test/journey/nfa/simulator_test.rb60
-rw-r--r--actionpack/test/journey/nfa/transition_table_test.rb52
-rw-r--r--actionpack/test/journey/nodes/symbol_test.rb10
-rw-r--r--actionpack/test/journey/path/pattern_test.rb264
-rw-r--r--actionpack/test/journey/route/definition/parser_test.rb62
-rw-r--r--actionpack/test/journey/route/definition/scanner_test.rb113
-rw-r--r--actionpack/test/journey/route_test.rb87
-rw-r--r--actionpack/test/journey/router/utils_test.rb15
-rw-r--r--actionpack/test/journey/router_test.rb571
-rw-r--r--actionpack/test/journey/routes_test.rb69
-rw-r--r--actionpack/test/lib/controller/fake_controllers.rb2
-rw-r--r--actionpack/test/lib/controller/fake_models.rb14
-rw-r--r--actionpack/test/routing/helper_test.rb4
-rw-r--r--actionview/.gitignore5
-rw-r--r--actionview/CHANGELOG.md202
-rw-r--r--actionview/MIT-LICENSE2
-rw-r--r--actionview/README.rdoc9
-rw-r--r--actionview/RUNNING_UJS_TESTS.rdoc8
-rw-r--r--actionview/RUNNING_UNIT_TESTS.rdoc15
-rw-r--r--actionview/Rakefile110
-rw-r--r--actionview/actionview.gemspec48
-rw-r--r--actionview/app/assets/javascripts/MIT-LICENSE20
-rw-r--r--actionview/app/assets/javascripts/README.md57
-rw-r--r--actionview/app/assets/javascripts/rails-ujs.coffee39
-rw-r--r--actionview/app/assets/javascripts/rails-ujs/BANNER.js5
-rw-r--r--actionview/app/assets/javascripts/rails-ujs/features/confirm.coffee30
-rw-r--r--actionview/app/assets/javascripts/rails-ujs/features/disable.coffee91
-rw-r--r--actionview/app/assets/javascripts/rails-ujs/features/method.coffee34
-rw-r--r--actionview/app/assets/javascripts/rails-ujs/features/remote.coffee90
-rw-r--r--actionview/app/assets/javascripts/rails-ujs/start.coffee71
-rw-r--r--actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee97
-rw-r--r--actionview/app/assets/javascripts/rails-ujs/utils/csp.coffee4
-rw-r--r--actionview/app/assets/javascripts/rails-ujs/utils/csrf.coffee25
-rw-r--r--actionview/app/assets/javascripts/rails-ujs/utils/dom.coffee35
-rw-r--r--actionview/app/assets/javascripts/rails-ujs/utils/event.coffee68
-rw-r--r--actionview/app/assets/javascripts/rails-ujs/utils/form.coffee36
-rwxr-xr-xactionview/bin/test7
-rw-r--r--actionview/blade.yml11
-rw-r--r--actionview/coffeelint.json135
-rw-r--r--actionview/lib/action_view.rb15
-rw-r--r--actionview/lib/action_view/base.rb56
-rw-r--r--actionview/lib/action_view/buffers.rb19
-rw-r--r--actionview/lib/action_view/context.rb15
-rw-r--r--actionview/lib/action_view/dependency_tracker.rb55
-rw-r--r--actionview/lib/action_view/digestor.rb176
-rw-r--r--actionview/lib/action_view/flows.rb24
-rw-r--r--actionview/lib/action_view/gem_version.rb4
-rw-r--r--actionview/lib/action_view/helpers.rb8
-rw-r--r--actionview/lib/action_view/helpers/active_model_helper.rb28
-rw-r--r--actionview/lib/action_view/helpers/asset_tag_helper.rb298
-rw-r--r--actionview/lib/action_view/helpers/asset_url_helper.rb218
-rw-r--r--actionview/lib/action_view/helpers/atom_feed_helper.rb33
-rw-r--r--actionview/lib/action_view/helpers/cache_helper.rb151
-rw-r--r--actionview/lib/action_view/helpers/capture_helper.rb30
-rw-r--r--actionview/lib/action_view/helpers/controller_helper.rb18
-rw-r--r--actionview/lib/action_view/helpers/csp_helper.rb24
-rw-r--r--actionview/lib/action_view/helpers/csrf_helper.rb12
-rw-r--r--actionview/lib/action_view/helpers/date_helper.rb331
-rw-r--r--actionview/lib/action_view/helpers/debug_helper.rb11
-rw-r--r--actionview/lib/action_view/helpers/form_helper.rb604
-rw-r--r--actionview/lib/action_view/helpers/form_options_helper.rb121
-rw-r--r--actionview/lib/action_view/helpers/form_tag_helper.rb162
-rw-r--r--actionview/lib/action_view/helpers/javascript_helper.rb38
-rw-r--r--actionview/lib/action_view/helpers/number_helper.rb121
-rw-r--r--actionview/lib/action_view/helpers/output_safety_helper.rb40
-rw-r--r--actionview/lib/action_view/helpers/record_tag_helper.rb21
-rw-r--r--actionview/lib/action_view/helpers/rendering_helper.rb11
-rw-r--r--actionview/lib/action_view/helpers/sanitize_helper.rb38
-rw-r--r--actionview/lib/action_view/helpers/tag_helper.rb274
-rw-r--r--actionview/lib/action_view/helpers/tags.rb4
-rw-r--r--actionview/lib/action_view/helpers/tags/base.rb236
-rw-r--r--actionview/lib/action_view/helpers/tags/check_box.rb38
-rw-r--r--actionview/lib/action_view/helpers/tags/checkable.rb6
-rw-r--r--actionview/lib/action_view/helpers/tags/collection_check_boxes.rb40
-rw-r--r--actionview/lib/action_view/helpers/tags/collection_helpers.rb106
-rw-r--r--actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb17
-rw-r--r--actionview/lib/action_view/helpers/tags/collection_select.rb6
-rw-r--r--actionview/lib/action_view/helpers/tags/color_field.rb6
-rw-r--r--actionview/lib/action_view/helpers/tags/date_field.rb2
-rw-r--r--actionview/lib/action_view/helpers/tags/date_select.rb74
-rw-r--r--actionview/lib/action_view/helpers/tags/datetime_field.rb6
-rw-r--r--actionview/lib/action_view/helpers/tags/datetime_local_field.rb2
-rw-r--r--actionview/lib/action_view/helpers/tags/datetime_select.rb2
-rw-r--r--actionview/lib/action_view/helpers/tags/email_field.rb2
-rw-r--r--actionview/lib/action_view/helpers/tags/file_field.rb17
-rw-r--r--actionview/lib/action_view/helpers/tags/grouped_collection_select.rb6
-rw-r--r--actionview/lib/action_view/helpers/tags/hidden_field.rb2
-rw-r--r--actionview/lib/action_view/helpers/tags/label.rb2
-rw-r--r--actionview/lib/action_view/helpers/tags/month_field.rb2
-rw-r--r--actionview/lib/action_view/helpers/tags/number_field.rb2
-rw-r--r--actionview/lib/action_view/helpers/tags/password_field.rb4
-rw-r--r--actionview/lib/action_view/helpers/tags/placeholderable.rb2
-rw-r--r--actionview/lib/action_view/helpers/tags/radio_button.rb12
-rw-r--r--actionview/lib/action_view/helpers/tags/range_field.rb2
-rw-r--r--actionview/lib/action_view/helpers/tags/search_field.rb2
-rw-r--r--actionview/lib/action_view/helpers/tags/select.rb20
-rw-r--r--actionview/lib/action_view/helpers/tags/tel_field.rb2
-rw-r--r--actionview/lib/action_view/helpers/tags/text_area.rb6
-rw-r--r--actionview/lib/action_view/helpers/tags/text_field.rb14
-rw-r--r--actionview/lib/action_view/helpers/tags/time_field.rb2
-rw-r--r--actionview/lib/action_view/helpers/tags/time_select.rb2
-rw-r--r--actionview/lib/action_view/helpers/tags/time_zone_select.rb4
-rw-r--r--actionview/lib/action_view/helpers/tags/translator.rb29
-rw-r--r--actionview/lib/action_view/helpers/tags/url_field.rb2
-rw-r--r--actionview/lib/action_view/helpers/tags/week_field.rb4
-rw-r--r--actionview/lib/action_view/helpers/text_helper.rb84
-rw-r--r--actionview/lib/action_view/helpers/translation_helper.rb52
-rw-r--r--actionview/lib/action_view/helpers/url_helper.rb181
-rw-r--r--actionview/lib/action_view/layouts.rb123
-rw-r--r--actionview/lib/action_view/log_subscriber.rb70
-rw-r--r--actionview/lib/action_view/lookup_context.rb136
-rw-r--r--actionview/lib/action_view/model_naming.rb2
-rw-r--r--actionview/lib/action_view/path_set.rb55
-rw-r--r--actionview/lib/action_view/railtie.rb57
-rw-r--r--actionview/lib/action_view/record_identifier.rb33
-rw-r--r--actionview/lib/action_view/renderer/abstract_renderer.rb36
-rw-r--r--actionview/lib/action_view/renderer/partial_renderer.rb403
-rw-r--r--actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb84
-rw-r--r--actionview/lib/action_view/renderer/renderer.rb8
-rw-r--r--actionview/lib/action_view/renderer/streaming_template_renderer.rb98
-rw-r--r--actionview/lib/action_view/renderer/template_renderer.rb133
-rw-r--r--actionview/lib/action_view/rendering.rb24
-rw-r--r--actionview/lib/action_view/routing_url_for.rb54
-rw-r--r--actionview/lib/action_view/tasks/cache_digests.rake25
-rw-r--r--actionview/lib/action_view/tasks/dependencies.rake23
-rw-r--r--actionview/lib/action_view/template.rb106
-rw-r--r--actionview/lib/action_view/template/error.rb30
-rw-r--r--actionview/lib/action_view/template/handlers.rb12
-rw-r--r--actionview/lib/action_view/template/handlers/builder.rb21
-rw-r--r--actionview/lib/action_view/template/handlers/erb.rb124
-rw-r--r--actionview/lib/action_view/template/handlers/erb/erubi.rb83
-rw-r--r--actionview/lib/action_view/template/handlers/html.rb11
-rw-r--r--actionview/lib/action_view/template/handlers/raw.rb6
-rw-r--r--actionview/lib/action_view/template/html.rb10
-rw-r--r--actionview/lib/action_view/template/resolver.rb289
-rw-r--r--actionview/lib/action_view/template/text.rb17
-rw-r--r--actionview/lib/action_view/template/types.rb36
-rw-r--r--actionview/lib/action_view/test_case.rb74
-rw-r--r--actionview/lib/action_view/testing/resolvers.rb62
-rw-r--r--actionview/lib/action_view/version.rb4
-rw-r--r--actionview/lib/action_view/view_paths.rb40
-rw-r--r--actionview/package.json36
-rw-r--r--actionview/test/abstract_unit.rb216
-rw-r--r--actionview/test/actionpack/abstract/abstract_controller_test.rb58
-rw-r--r--actionview/test/actionpack/abstract/helper_test.rb17
-rw-r--r--actionview/test/actionpack/abstract/layouts_test.rb256
-rw-r--r--actionview/test/actionpack/abstract/render_test.rb14
-rw-r--r--actionview/test/actionpack/controller/capture_test.rb27
-rw-r--r--actionview/test/actionpack/controller/layout_test.rb91
-rw-r--r--actionview/test/actionpack/controller/render_test.rb483
-rw-r--r--actionview/test/actionpack/controller/view_paths_test.rb44
-rw-r--r--actionview/test/active_record_unit.rb39
-rw-r--r--actionview/test/activerecord/controller_runtime_test.rb40
-rw-r--r--actionview/test/activerecord/debug_helper_test.rb11
-rw-r--r--actionview/test/activerecord/form_helper_activerecord_test.rb67
-rw-r--r--actionview/test/activerecord/multifetch_cache_test.rb35
-rw-r--r--actionview/test/activerecord/polymorphic_routes_test.rb133
-rw-r--r--actionview/test/activerecord/relation_cache_test.rb24
-rw-r--r--actionview/test/activerecord/render_partial_with_record_identification_test.rb68
-rw-r--r--actionview/test/fixtures/actionpack/layouts/builder.builder2
-rw-r--r--actionview/test/fixtures/actionpack/layouts/standard.text.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_hello.builder2
-rw-r--r--actionview/test/fixtures/actionpack/test/formatted_xml_erb.builder2
-rw-r--r--actionview/test/fixtures/actionpack/test/hello.builder4
-rw-r--r--actionview/test/fixtures/actionpack/test/hello_world_container.builder4
-rw-r--r--actionview/test/fixtures/actionpack/test/hello_xml_world.builder2
-rw-r--r--actionview/test/fixtures/actionpack/test/non_erb_block_content_for.builder2
-rw-r--r--actionview/test/fixtures/comments/empty.html+grid.erb1
-rw-r--r--actionview/test/fixtures/comments/empty.html.builder2
-rw-r--r--actionview/test/fixtures/company.rb4
-rw-r--r--actionview/test/fixtures/developer.rb4
-rw-r--r--actionview/test/fixtures/digestor/api/comments/_comment.json.erb1
-rw-r--r--actionview/test/fixtures/digestor/api/comments/_comments.json.erb1
-rw-r--r--actionview/test/fixtures/digestor/comments/_comment.html.erb2
-rw-r--r--actionview/test/fixtures/digestor/comments/show.js.erb1
-rw-r--r--actionview/test/fixtures/digestor/events/_completed.html.erb (renamed from actionpack/test/tmp/.gitignore)0
-rw-r--r--actionview/test/fixtures/digestor/events/index.html.erb1
-rw-r--r--actionview/test/fixtures/digestor/messages/peek.html.erb2
-rw-r--r--actionview/test/fixtures/digestor/messages/thread.json.erb1
-rw-r--r--actionview/test/fixtures/helpers/abc_helper.rb2
-rw-r--r--actionview/test/fixtures/helpers/helpery_test_helper.rb2
-rw-r--r--actionview/test/fixtures/helpers_missing/invalid_require_helper.rb5
-rw-r--r--actionview/test/fixtures/layouts/render_partial_html.erb2
-rw-r--r--actionview/test/fixtures/layouts/streaming_with_locale.erb2
-rw-r--r--actionview/test/fixtures/mascot.rb4
-rw-r--r--actionview/test/fixtures/project.rb6
-rw-r--r--actionview/test/fixtures/public/.gitignore1
-rw-r--r--actionview/test/fixtures/reply.rb2
-rw-r--r--actionview/test/fixtures/ruby_template.ruby2
-rw-r--r--actionview/test/fixtures/test/_builder_tag_nested_in_content_tag.erb3
-rw-r--r--actionview/test/fixtures/test/_cached_nested_cached_customer.erb3
-rw-r--r--actionview/test/fixtures/test/_customer.mobile.erb1
-rw-r--r--actionview/test/fixtures/test/_klass.erb1
-rw-r--r--actionview/test/fixtures/test/_nested_cached_customer.erb1
-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/_partial_with_variants.html+grid.erb1
-rw-r--r--actionview/test/fixtures/test/_partialhtml.html1
-rw-r--r--actionview/test/fixtures/test/_🍣.erb1
-rw-r--r--actionview/test/fixtures/test/hello.builder4
-rw-r--r--actionview/test/fixtures/test/render_file_inspect_local_assigns.erb1
-rw-r--r--actionview/test/fixtures/test/render_file_instance_variable.erb1
-rw-r--r--actionview/test/fixtures/test/render_file_unicode_local.erb1
-rw-r--r--actionview/test/fixtures/test/render_file_with_ruby_keyword_locals.erb1
-rw-r--r--actionview/test/fixtures/test/streaming_with_locale.erb1
-rw-r--r--actionview/test/fixtures/test/test_template_with_delegation_reserved_keywords.erb1
-rw-r--r--actionview/test/fixtures/topic.rb4
-rw-r--r--actionview/test/lib/controller/fake_models.rb34
-rw-r--r--actionview/test/template/active_model_helper_test.rb91
-rw-r--r--actionview/test/template/asset_tag_helper_test.rb275
-rw-r--r--actionview/test/template/atom_feed_helper_test.rb64
-rw-r--r--actionview/test/template/capture_helper_test.rb147
-rw-r--r--actionview/test/template/compiled_templates_test.rb55
-rw-r--r--actionview/test/template/controller_helper_test.rb19
-rw-r--r--actionview/test/template/date_helper_i18n_test.rb144
-rw-r--r--actionview/test/template/date_helper_test.rb1501
-rw-r--r--actionview/test/template/dependency_tracker_test.rb9
-rw-r--r--actionview/test/template/digestor_test.rb197
-rw-r--r--actionview/test/template/erb/form_for_test.rb8
-rw-r--r--actionview/test/template/erb/helper.rb13
-rw-r--r--actionview/test/template/erb/tag_helper_test.rb6
-rw-r--r--actionview/test/template/erb_util_test.rb45
-rw-r--r--actionview/test/template/form_collections_helper_test.rb450
-rw-r--r--actionview/test/template/form_helper/form_with_test.rb2367
-rw-r--r--actionview/test/template/form_helper_test.rb1226
-rw-r--r--actionview/test/template/form_options_helper_i18n_test.rb15
-rw-r--r--actionview/test/template/form_options_helper_test.rb744
-rw-r--r--actionview/test/template/form_tag_helper_test.rb335
-rw-r--r--actionview/test/template/html_test.rb16
-rw-r--r--actionview/test/template/javascript_helper_test.rb34
-rw-r--r--actionview/test/template/log_subscriber_test.rb213
-rw-r--r--actionview/test/template/lookup_context_test.rb83
-rw-r--r--actionview/test/template/number_helper_test.rb139
-rw-r--r--actionview/test/template/output_safety_helper_test.rb100
-rw-r--r--actionview/test/template/partial_iteration_test.rb10
-rw-r--r--actionview/test/template/record_identifier_test.rb14
-rw-r--r--actionview/test/template/record_tag_helper_test.rb32
-rw-r--r--actionview/test/template/render_test.rb372
-rw-r--r--actionview/test/template/resolver_cache_test.rb9
-rw-r--r--actionview/test/template/resolver_patterns_test.rb25
-rw-r--r--actionview/test/template/sanitize_helper_test.rb16
-rw-r--r--actionview/test/template/streaming_render_test.rb61
-rw-r--r--actionview/test/template/tag_helper_test.rb249
-rw-r--r--actionview/test/template/template_error_test.rb27
-rw-r--r--actionview/test/template/template_test.rb77
-rw-r--r--actionview/test/template/test_case_test.rb148
-rw-r--r--actionview/test/template/test_test.rb12
-rw-r--r--actionview/test/template/testing/fixture_resolver_test.rb8
-rw-r--r--actionview/test/template/testing/null_resolver_test.rb6
-rw-r--r--actionview/test/template/text_helper_test.rb272
-rw-r--r--actionview/test/template/text_test.rb26
-rw-r--r--actionview/test/template/translation_helper_test.rb138
-rw-r--r--actionview/test/template/url_helper_test.rb407
-rw-r--r--actionview/test/ujs/config.ru6
-rw-r--r--actionview/test/ujs/public/test/.eslintrc.yml21
-rw-r--r--actionview/test/ujs/public/test/call-ajax.js26
-rw-r--r--actionview/test/ujs/public/test/call-remote-callbacks.js239
-rw-r--r--actionview/test/ujs/public/test/call-remote.js286
-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.js342
-rw-r--r--actionview/test/ujs/public/test/data-disable-with.js394
-rw-r--r--actionview/test/ujs/public/test/data-disable.js339
-rw-r--r--actionview/test/ujs/public/test/data-method.js85
-rw-r--r--actionview/test/ujs/public/test/data-remote.js441
-rw-r--r--actionview/test/ujs/public/test/override.js56
-rw-r--r--actionview/test/ujs/public/test/settings.js122
-rw-r--r--actionview/test/ujs/public/vendor/jquery-2.2.0.js9831
-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.rb91
-rw-r--r--actionview/test/ujs/views/layouts/application.html.erb26
-rw-r--r--actionview/test/ujs/views/tests/index.html.erb11
-rw-r--r--activejob/.gitignore1
-rw-r--r--activejob/CHANGELOG.md133
-rw-r--r--activejob/MIT-LICENSE2
-rw-r--r--activejob/README.md29
-rw-r--r--activejob/Rakefile47
-rw-r--r--activejob/activejob.gemspec36
-rwxr-xr-xactivejob/bin/test5
-rw-r--r--activejob/lib/active_job.rb13
-rw-r--r--activejob/lib/active_job/arguments.rb107
-rw-r--r--activejob/lib/active_job/base.rb26
-rw-r--r--activejob/lib/active_job/callbacks.rb17
-rw-r--r--activejob/lib/active_job/configured_job.rb4
-rw-r--r--activejob/lib/active_job/core.rb104
-rw-r--r--activejob/lib/active_job/enqueuing.rb53
-rw-r--r--activejob/lib/active_job/exceptions.rb143
-rw-r--r--activejob/lib/active_job/execution.rb19
-rw-r--r--activejob/lib/active_job/gem_version.rb4
-rw-r--r--activejob/lib/active_job/logging.rb150
-rw-r--r--activejob/lib/active_job/queue_adapter.rb61
-rw-r--r--activejob/lib/active_job/queue_adapters.rb55
-rw-r--r--activejob/lib/active_job/queue_adapters/async_adapter.rb116
-rw-r--r--activejob/lib/active_job/queue_adapters/backburner_adapter.rb8
-rw-r--r--activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb12
-rw-r--r--activejob/lib/active_job/queue_adapters/inline_adapter.rb6
-rw-r--r--activejob/lib/active_job/queue_adapters/qu_adapter.rb44
-rw-r--r--activejob/lib/active_job/queue_adapters/que_adapter.rb8
-rw-r--r--activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb18
-rw-r--r--activejob/lib/active_job/queue_adapters/resque_adapter.rb13
-rw-r--r--activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb26
-rw-r--r--activejob/lib/active_job/queue_adapters/sneakers_adapter.rb8
-rw-r--r--activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb29
-rw-r--r--activejob/lib/active_job/queue_adapters/test_adapter.rb47
-rw-r--r--activejob/lib/active_job/queue_name.rb16
-rw-r--r--activejob/lib/active_job/queue_priority.rb43
-rw-r--r--activejob/lib/active_job/railtie.rb36
-rw-r--r--activejob/lib/active_job/serializers.rb63
-rw-r--r--activejob/lib/active_job/serializers/date_serializer.rb21
-rw-r--r--activejob/lib/active_job/serializers/date_time_serializer.rb21
-rw-r--r--activejob/lib/active_job/serializers/duration_serializer.rb24
-rw-r--r--activejob/lib/active_job/serializers/object_serializer.rb54
-rw-r--r--activejob/lib/active_job/serializers/symbol_serializer.rb21
-rw-r--r--activejob/lib/active_job/serializers/time_serializer.rb21
-rw-r--r--activejob/lib/active_job/serializers/time_with_zone_serializer.rb21
-rw-r--r--activejob/lib/active_job/test_case.rb6
-rw-r--r--activejob/lib/active_job/test_helper.rb865
-rw-r--r--activejob/lib/active_job/timezones.rb13
-rw-r--r--activejob/lib/active_job/translation.rb13
-rw-r--r--activejob/lib/active_job/version.rb4
-rw-r--r--activejob/lib/rails/generators/job/job_generator.rb33
-rw-r--r--activejob/lib/rails/generators/job/templates/application_job.rb.tt9
-rw-r--r--activejob/lib/rails/generators/job/templates/job.rb.tt (renamed from activejob/lib/rails/generators/job/templates/job.rb)0
-rw-r--r--activejob/test/adapters/async.rb4
-rw-r--r--activejob/test/adapters/backburner.rb6
-rw-r--r--activejob/test/adapters/delayed_job.rb5
-rw-r--r--activejob/test/adapters/inline.rb4
-rw-r--r--activejob/test/adapters/qu.rb3
-rw-r--r--activejob/test/adapters/que.rb4
-rw-r--r--activejob/test/adapters/queue_classic.rb4
-rw-r--r--activejob/test/adapters/resque.rb2
-rw-r--r--activejob/test/adapters/sidekiq.rb4
-rw-r--r--activejob/test/adapters/sneakers.rb4
-rw-r--r--activejob/test/adapters/sucker_punch.rb4
-rw-r--r--activejob/test/adapters/test.rb2
-rw-r--r--activejob/test/cases/adapter_test.rb4
-rw-r--r--activejob/test/cases/argument_serialization_test.rb130
-rw-r--r--activejob/test/cases/callbacks_test.rb12
-rw-r--r--activejob/test/cases/exceptions_test.rb142
-rw-r--r--activejob/test/cases/job_serialization_test.rb57
-rw-r--r--activejob/test/cases/logging_test.rb152
-rw-r--r--activejob/test/cases/queue_adapter_test.rb27
-rw-r--r--activejob/test/cases/queue_naming_test.rb44
-rw-r--r--activejob/test/cases/queue_priority_test.rb49
-rw-r--r--activejob/test/cases/queuing_test.rb18
-rw-r--r--activejob/test/cases/rescue_test.rb22
-rw-r--r--activejob/test/cases/serializers_test.rb98
-rw-r--r--activejob/test/cases/test_case_test.rb14
-rw-r--r--activejob/test/cases/test_helper_test.rb1422
-rw-r--r--activejob/test/cases/timezones_test.rb24
-rw-r--r--activejob/test/cases/translation_test.rb22
-rw-r--r--activejob/test/helper.rb18
-rw-r--r--activejob/test/integration/queuing_test.rb119
-rw-r--r--activejob/test/jobs/application_job.rb4
-rw-r--r--activejob/test/jobs/callback_job.rb4
-rw-r--r--activejob/test/jobs/gid_job.rb5
-rw-r--r--activejob/test/jobs/hello_job.rb4
-rw-r--r--activejob/test/jobs/inherited_job.rb7
-rw-r--r--activejob/test/jobs/kwargs_job.rb4
-rw-r--r--activejob/test/jobs/logging_job.rb3
-rw-r--r--activejob/test/jobs/multiple_kwargs_job.rb9
-rw-r--r--activejob/test/jobs/nested_job.rb3
-rw-r--r--activejob/test/jobs/overridden_logging_job.rb11
-rw-r--r--activejob/test/jobs/provider_jid_job.rb9
-rw-r--r--activejob/test/jobs/queue_adapter_job.rb5
-rw-r--r--activejob/test/jobs/queue_as_job.rb12
-rw-r--r--activejob/test/jobs/rescue_job.rb14
-rw-r--r--activejob/test/jobs/retry_job.rb41
-rw-r--r--activejob/test/jobs/timezone_dependent_job.rb22
-rw-r--r--activejob/test/jobs/translated_hello_job.rb12
-rw-r--r--activejob/test/models/person.rb4
-rw-r--r--activejob/test/support/backburner/inline.rb8
-rw-r--r--activejob/test/support/delayed_job/delayed/backend/test.rb25
-rw-r--r--activejob/test/support/integration/adapters/async.rb12
-rw-r--r--activejob/test/support/integration/adapters/backburner.rb10
-rw-r--r--activejob/test/support/integration/adapters/delayed_job.rb7
-rw-r--r--activejob/test/support/integration/adapters/inline.rb3
-rw-r--r--activejob/test/support/integration/adapters/qu.rb38
-rw-r--r--activejob/test/support/integration/adapters/que.rb15
-rw-r--r--activejob/test/support/integration/adapters/queue_classic.rb23
-rw-r--r--activejob/test/support/integration/adapters/resque.rb16
-rw-r--r--activejob/test/support/integration/adapters/sidekiq.rb30
-rw-r--r--activejob/test/support/integration/adapters/sneakers.rb50
-rw-r--r--activejob/test/support/integration/adapters/sucker_punch.rb2
-rw-r--r--activejob/test/support/integration/dummy_app_template.rb24
-rw-r--r--activejob/test/support/integration/helper.rb16
-rw-r--r--activejob/test/support/integration/jobs_manager.rb4
-rw-r--r--activejob/test/support/integration/test_case_helpers.rb35
-rw-r--r--activejob/test/support/job_buffer.rb2
-rw-r--r--activejob/test/support/que/inline.rb7
-rw-r--r--activejob/test/support/queue_classic/inline.rb17
-rw-r--r--activejob/test/support/sneakers/inline.rb9
-rw-r--r--activejob/test/support/stubs/strong_parameters.rb15
-rw-r--r--activemodel/CHANGELOG.md114
-rw-r--r--activemodel/MIT-LICENSE2
-rw-r--r--activemodel/README.rdoc18
-rw-r--r--activemodel/Rakefile16
-rw-r--r--activemodel/activemodel.gemspec34
-rwxr-xr-xactivemodel/bin/test7
-rw-r--r--activemodel/lib/active_model.rb28
-rw-r--r--activemodel/lib/active_model/attribute.rb247
-rw-r--r--activemodel/lib/active_model/attribute/user_provided_default.rb51
-rw-r--r--activemodel/lib/active_model/attribute_assignment.rb35
-rw-r--r--activemodel/lib/active_model/attribute_methods.rb94
-rw-r--r--activemodel/lib/active_model/attribute_mutation_tracker.rb119
-rw-r--r--activemodel/lib/active_model/attribute_set.rb114
-rw-r--r--activemodel/lib/active_model/attribute_set/builder.rb124
-rw-r--r--activemodel/lib/active_model/attribute_set/yaml_encoder.rb40
-rw-r--r--activemodel/lib/active_model/attributes.rb99
-rw-r--r--activemodel/lib/active_model/callbacks.rb49
-rw-r--r--activemodel/lib/active_model/conversion.rb23
-rw-r--r--activemodel/lib/active_model/dirty.rb221
-rw-r--r--activemodel/lib/active_model/errors.rb297
-rw-r--r--activemodel/lib/active_model/forbidden_attributes_protection.rb9
-rw-r--r--activemodel/lib/active_model/gem_version.rb4
-rw-r--r--activemodel/lib/active_model/lint.rb27
-rw-r--r--activemodel/lib/active_model/model.rb9
-rw-r--r--activemodel/lib/active_model/naming.rb47
-rw-r--r--activemodel/lib/active_model/railtie.rb8
-rw-r--r--activemodel/lib/active_model/secure_password.rb113
-rw-r--r--activemodel/lib/active_model/serialization.rb20
-rw-r--r--activemodel/lib/active_model/serializers/json.rb28
-rw-r--r--activemodel/lib/active_model/serializers/xml.rb238
-rw-r--r--activemodel/lib/active_model/test_case.rb4
-rw-r--r--activemodel/lib/active_model/translation.rb7
-rw-r--r--activemodel/lib/active_model/type.rb53
-rw-r--r--activemodel/lib/active_model/type/big_integer.rb15
-rw-r--r--activemodel/lib/active_model/type/binary.rb52
-rw-r--r--activemodel/lib/active_model/type/boolean.rb38
-rw-r--r--activemodel/lib/active_model/type/date.rb56
-rw-r--r--activemodel/lib/active_model/type/date_time.rb50
-rw-r--r--activemodel/lib/active_model/type/decimal.rb74
-rw-r--r--activemodel/lib/active_model/type/float.rb36
-rw-r--r--activemodel/lib/active_model/type/helpers.rb6
-rw-r--r--activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb41
-rw-r--r--activemodel/lib/active_model/type/helpers/mutable.rb20
-rw-r--r--activemodel/lib/active_model/type/helpers/numeric.rb37
-rw-r--r--activemodel/lib/active_model/type/helpers/time_value.rb86
-rw-r--r--activemodel/lib/active_model/type/immutable_string.rb32
-rw-r--r--activemodel/lib/active_model/type/integer.rb65
-rw-r--r--activemodel/lib/active_model/type/registry.rb62
-rw-r--r--activemodel/lib/active_model/type/string.rb26
-rw-r--r--activemodel/lib/active_model/type/time.rb46
-rw-r--r--activemodel/lib/active_model/type/value.rb126
-rw-r--r--activemodel/lib/active_model/validations.rb38
-rw-r--r--activemodel/lib/active_model/validations/absence.rb4
-rw-r--r--activemodel/lib/active_model/validations/acceptance.rb63
-rw-r--r--activemodel/lib/active_model/validations/callbacks.rb38
-rw-r--r--activemodel/lib/active_model/validations/clusivity.rb23
-rw-r--r--activemodel/lib/active_model/validations/confirmation.rb41
-rw-r--r--activemodel/lib/active_model/validations/exclusion.rb5
-rw-r--r--activemodel/lib/active_model/validations/format.rb49
-rw-r--r--activemodel/lib/active_model/validations/helper_methods.rb2
-rw-r--r--activemodel/lib/active_model/validations/inclusion.rb7
-rw-r--r--activemodel/lib/active_model/validations/length.rb60
-rw-r--r--activemodel/lib/active_model/validations/numericality.rb59
-rw-r--r--activemodel/lib/active_model/validations/presence.rb4
-rw-r--r--activemodel/lib/active_model/validations/validates.rb23
-rw-r--r--activemodel/lib/active_model/validations/with.rb8
-rw-r--r--activemodel/lib/active_model/validator.rb29
-rw-r--r--activemodel/lib/active_model/version.rb4
-rw-r--r--activemodel/test/cases/attribute_assignment_test.rb47
-rw-r--r--activemodel/test/cases/attribute_methods_test.rb92
-rw-r--r--activemodel/test/cases/attribute_set_test.rb274
-rw-r--r--activemodel/test/cases/attribute_test.rb255
-rw-r--r--activemodel/test/cases/attributes_dirty_test.rb205
-rw-r--r--activemodel/test/cases/attributes_test.rb97
-rw-r--r--activemodel/test/cases/callbacks_test.rb33
-rw-r--r--activemodel/test/cases/conversion_test.rb8
-rw-r--r--activemodel/test/cases/dirty_test.rb129
-rw-r--r--activemodel/test/cases/errors_test.rb259
-rw-r--r--activemodel/test/cases/forbidden_attributes_protection_test.rb24
-rw-r--r--activemodel/test/cases/helper.rb32
-rw-r--r--activemodel/test/cases/lint_test.rb4
-rw-r--r--activemodel/test/cases/model_test.rb16
-rw-r--r--activemodel/test/cases/naming_test.rb98
-rw-r--r--activemodel/test/cases/railtie_test.rb36
-rw-r--r--activemodel/test/cases/secure_password_test.rb133
-rw-r--r--activemodel/test/cases/serialization_test.rb89
-rw-r--r--activemodel/test/cases/serializers/json_serialization_test.rb34
-rw-r--r--activemodel/test/cases/serializers/xml_serialization_test.rb251
-rw-r--r--activemodel/test/cases/translation_test.rb87
-rw-r--r--activemodel/test/cases/type/big_integer_test.rb25
-rw-r--r--activemodel/test/cases/type/binary_test.rb16
-rw-r--r--activemodel/test/cases/type/boolean_test.rb40
-rw-r--r--activemodel/test/cases/type/date_test.rb20
-rw-r--r--activemodel/test/cases/type/date_time_test.rb50
-rw-r--r--activemodel/test/cases/type/decimal_test.rb76
-rw-r--r--activemodel/test/cases/type/float_test.rb34
-rw-r--r--activemodel/test/cases/type/immutable_string_test.rb22
-rw-r--r--activemodel/test/cases/type/integer_test.rb121
-rw-r--r--activemodel/test/cases/type/registry_test.rb42
-rw-r--r--activemodel/test/cases/type/string_test.rb38
-rw-r--r--activemodel/test/cases/type/time_test.rb37
-rw-r--r--activemodel/test/cases/type/value_test.rb15
-rw-r--r--activemodel/test/cases/validations/absence_validation_test.rb30
-rw-r--r--activemodel/test/cases/validations/acceptance_validation_test.rb41
-rw-r--r--activemodel/test/cases/validations/callbacks_test.rb106
-rw-r--r--activemodel/test/cases/validations/conditional_validation_test.rb124
-rw-r--r--activemodel/test/cases/validations/confirmation_validation_test.rb63
-rw-r--r--activemodel/test/cases/validations/exclusion_validation_test.rb47
-rw-r--r--activemodel/test/cases/validations/format_validation_test.rb53
-rw-r--r--activemodel/test/cases/validations/i18n_generate_message_validation_test.rb48
-rw-r--r--activemodel/test/cases/validations/i18n_validation_test.rb209
-rw-r--r--activemodel/test/cases/validations/inclusion_validation_test.rb113
-rw-r--r--activemodel/test/cases/validations/length_validation_test.rb296
-rw-r--r--activemodel/test/cases/validations/numericality_validation_test.rb185
-rw-r--r--activemodel/test/cases/validations/presence_validation_test.rb33
-rw-r--r--activemodel/test/cases/validations/validates_test.rb84
-rw-r--r--activemodel/test/cases/validations/validations_context_test.rb31
-rw-r--r--activemodel/test/cases/validations/with_validation_test.rb78
-rw-r--r--activemodel/test/cases/validations_test.rb128
-rw-r--r--activemodel/test/config.rb3
-rw-r--r--activemodel/test/models/account.rb2
-rw-r--r--activemodel/test/models/blog_post.rb2
-rw-r--r--activemodel/test/models/contact.rb3
-rw-r--r--activemodel/test/models/custom_reader.rb4
-rw-r--r--activemodel/test/models/helicopter.rb2
-rw-r--r--activemodel/test/models/person.rb6
-rw-r--r--activemodel/test/models/person_with_validator.rb2
-rw-r--r--activemodel/test/models/project.rb3
-rw-r--r--activemodel/test/models/reply.rb4
-rw-r--r--activemodel/test/models/sheep.rb2
-rw-r--r--activemodel/test/models/topic.rb17
-rw-r--r--activemodel/test/models/track_back.rb4
-rw-r--r--activemodel/test/models/user.rb7
-rw-r--r--activemodel/test/models/visitor.rb5
-rw-r--r--activemodel/test/validators/email_validator.rb6
-rw-r--r--activemodel/test/validators/namespace/email_validator.rb4
-rw-r--r--activerecord/.gitignore3
-rw-r--r--activerecord/CHANGELOG.md1069
-rw-r--r--activerecord/MIT-LICENSE6
-rw-r--r--activerecord/README.rdoc15
-rw-r--r--activerecord/RUNNING_UNIT_TESTS.rdoc11
-rw-r--r--activerecord/Rakefile120
-rw-r--r--activerecord/activerecord.gemspec38
-rwxr-xr-xactiverecord/bin/test20
-rw-r--r--activerecord/examples/.gitignore1
-rw-r--r--activerecord/examples/performance.rb61
-rw-r--r--activerecord/examples/simple.rb9
-rw-r--r--activerecord/lib/active_record.rb58
-rw-r--r--activerecord/lib/active_record/aggregations.rb498
-rw-r--r--activerecord/lib/active_record/association_relation.rb17
-rw-r--r--activerecord/lib/active_record/associations.rb3288
-rw-r--r--activerecord/lib/active_record/associations/alias_tracker.rb60
-rw-r--r--activerecord/lib/active_record/associations/association.rb149
-rw-r--r--activerecord/lib/active_record/associations/association_scope.rb188
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb117
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb20
-rw-r--r--activerecord/lib/active_record/associations/builder/association.rb13
-rw-r--r--activerecord/lib/active_record/associations/builder/belongs_to.rb88
-rw-r--r--activerecord/lib/active_record/associations/builder/collection_association.rb15
-rw-r--r--activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb118
-rw-r--r--activerecord/lib/active_record/associations/builder/has_many.rb6
-rw-r--r--activerecord/lib/active_record/associations/builder/has_one.rb6
-rw-r--r--activerecord/lib/active_record/associations/builder/singular_association.rb17
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb392
-rw-r--r--activerecord/lib/active_record/associations/collection_proxy.rb327
-rw-r--r--activerecord/lib/active_record/associations/foreign_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb104
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb76
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb108
-rw-r--r--activerecord/lib/active_record/associations/has_one_through_association.rb31
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb306
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_association.rb115
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_base.rb19
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_part.rb28
-rw-r--r--activerecord/lib/active_record/associations/preloader.rb173
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb201
-rw-r--r--activerecord/lib/active_record/associations/preloader/belongs_to.rb17
-rw-r--r--activerecord/lib/active_record/associations/preloader/collection_association.rb24
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_many.rb17
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_many_through.rb19
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_one.rb23
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_one_through.rb9
-rw-r--r--activerecord/lib/active_record/associations/preloader/singular_association.rb21
-rw-r--r--activerecord/lib/active_record/associations/preloader/through_association.rb156
-rw-r--r--activerecord/lib/active_record/associations/singular_association.rb66
-rw-r--r--activerecord/lib/active_record/associations/through_association.rb54
-rw-r--r--activerecord/lib/active_record/attribute.rb174
-rw-r--r--activerecord/lib/active_record/attribute/user_provided_default.rb32
-rw-r--r--activerecord/lib/active_record/attribute_assignment.rb138
-rw-r--r--activerecord/lib/active_record/attribute_decorators.rb53
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb292
-rw-r--r--activerecord/lib/active_record/attribute_methods/before_type_cast.rb18
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb302
-rw-r--r--activerecord/lib/active_record/attribute_methods/primary_key.rb157
-rw-r--r--activerecord/lib/active_record/attribute_methods/query.rb4
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb78
-rw-r--r--activerecord/lib/active_record/attribute_methods/serialization.rb43
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb109
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb85
-rw-r--r--activerecord/lib/active_record/attribute_set.rb93
-rw-r--r--activerecord/lib/active_record/attribute_set/builder.rb92
-rw-r--r--activerecord/lib/active_record/attributes.rb77
-rw-r--r--activerecord/lib/active_record/autosave_association.rb112
-rw-r--r--activerecord/lib/active_record/base.rb100
-rw-r--r--activerecord/lib/active_record/callbacks.rb179
-rw-r--r--activerecord/lib/active_record/coders/json.rb4
-rw-r--r--activerecord/lib/active_record/coders/yaml_column.rb31
-rw-r--r--activerecord/lib/active_record/collection_cache_key.rb53
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb899
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb22
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb311
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb95
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb162
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb99
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb427
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb134
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb755
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/transaction.rb250
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb527
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb1254
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb50
-rw-r--r--activerecord/lib/active_record/connection_adapters/connection_specification.rb292
-rw-r--r--activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb29
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/column.rb27
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb141
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb72
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/quoting.rb44
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb72
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb87
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb80
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb177
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb35
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb228
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql_adapter.rb468
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/column.rb31
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb163
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/explain_pretty_printer.rb44
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid.rb45
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb48
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb15
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb23
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb20
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb44
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb35
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb14
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb45
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb10
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/oid.rb15
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb38
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/rails_5_1_point.rb50
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb73
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb5
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb112
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb132
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb44
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb40
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb103
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb50
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb707
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb13
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/utils.rb18
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb729
-rw-r--r--activerecord/lib/active_record/connection_adapters/schema_cache.rb64
-rw-r--r--activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/explain_pretty_printer.rb21
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb63
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb19
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb18
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb111
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb540
-rw-r--r--activerecord/lib/active_record/connection_adapters/statement_pool.rb27
-rw-r--r--activerecord/lib/active_record/connection_handling.rb176
-rw-r--r--activerecord/lib/active_record/core.rb362
-rw-r--r--activerecord/lib/active_record/counter_cache.rb105
-rw-r--r--activerecord/lib/active_record/database_configurations.rb186
-rw-r--r--activerecord/lib/active_record/database_configurations/database_config.rb37
-rw-r--r--activerecord/lib/active_record/database_configurations/hash_config.rb50
-rw-r--r--activerecord/lib/active_record/database_configurations/url_config.rb74
-rw-r--r--activerecord/lib/active_record/define_callbacks.rb22
-rw-r--r--activerecord/lib/active_record/dynamic_matchers.rb173
-rw-r--r--activerecord/lib/active_record/enum.rb149
-rw-r--r--activerecord/lib/active_record/errors.rb215
-rw-r--r--activerecord/lib/active_record/explain.rb34
-rw-r--r--activerecord/lib/active_record/explain_registry.rb6
-rw-r--r--activerecord/lib/active_record/explain_subscriber.rb15
-rw-r--r--activerecord/lib/active_record/fixture_set/file.rb44
-rw-r--r--activerecord/lib/active_record/fixture_set/model_metadata.rb33
-rw-r--r--activerecord/lib/active_record/fixture_set/render_context.rb17
-rw-r--r--activerecord/lib/active_record/fixture_set/table_row.rb153
-rw-r--r--activerecord/lib/active_record/fixture_set/table_rows.rb47
-rw-r--r--activerecord/lib/active_record/fixtures.rb723
-rw-r--r--activerecord/lib/active_record/gem_version.rb4
-rw-r--r--activerecord/lib/active_record/inheritance.rb260
-rw-r--r--activerecord/lib/active_record/integration.rb98
-rw-r--r--activerecord/lib/active_record/internal_metadata.rb45
-rw-r--r--activerecord/lib/active_record/legacy_yaml_adapter.rb6
-rw-r--r--activerecord/lib/active_record/locale/en.yml4
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb174
-rw-r--r--activerecord/lib/active_record/locking/pessimistic.rb24
-rw-r--r--activerecord/lib/active_record/log_subscriber.rb107
-rw-r--r--activerecord/lib/active_record/migration.rb849
-rw-r--r--activerecord/lib/active_record/migration/command_recorder.rb265
-rw-r--r--activerecord/lib/active_record/migration/compatibility.rb235
-rw-r--r--activerecord/lib/active_record/migration/join_table.rb14
-rw-r--r--activerecord/lib/active_record/model_schema.rb402
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb428
-rw-r--r--activerecord/lib/active_record/no_touching.rb15
-rw-r--r--activerecord/lib/active_record/null_relation.rb47
-rw-r--r--activerecord/lib/active_record/persistence.rb424
-rw-r--r--activerecord/lib/active_record/query_cache.rb37
-rw-r--r--activerecord/lib/active_record/querying.rb36
-rw-r--r--activerecord/lib/active_record/railtie.rb187
-rw-r--r--activerecord/lib/active_record/railties/collection_cache_association_loading.rb34
-rw-r--r--activerecord/lib/active_record/railties/console_sandbox.rb2
-rw-r--r--activerecord/lib/active_record/railties/controller_runtime.rb67
-rw-r--r--activerecord/lib/active_record/railties/databases.rake401
-rw-r--r--activerecord/lib/active_record/railties/jdbcmysql_error.rb16
-rw-r--r--activerecord/lib/active_record/readonly_attributes.rb9
-rw-r--r--activerecord/lib/active_record/reflection.rb750
-rw-r--r--activerecord/lib/active_record/relation.rb691
-rw-r--r--activerecord/lib/active_record/relation/batches.rb268
-rw-r--r--activerecord/lib/active_record/relation/batches/batch_enumerator.rb69
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb468
-rw-r--r--activerecord/lib/active_record/relation/delegation.rb126
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb496
-rw-r--r--activerecord/lib/active_record/relation/from_clause.rb14
-rw-r--r--activerecord/lib/active_record/relation/merger.rb137
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb172
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/array_handler.rb34
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb78
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb43
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/base_handler.rb7
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb10
-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_value.rb53
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/range_handler.rb32
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb8
-rw-r--r--activerecord/lib/active_record/relation/query_attribute.rb28
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb729
-rw-r--r--activerecord/lib/active_record/relation/record_fetch_warning.rb20
-rw-r--r--activerecord/lib/active_record/relation/spawn_methods.rb27
-rw-r--r--activerecord/lib/active_record/relation/where_clause.rb201
-rw-r--r--activerecord/lib/active_record/relation/where_clause_factory.rb23
-rw-r--r--activerecord/lib/active_record/result.rb121
-rw-r--r--activerecord/lib/active_record/runtime_registry.rb10
-rw-r--r--activerecord/lib/active_record/sanitization.rb239
-rw-r--r--activerecord/lib/active_record/schema.rb54
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb196
-rw-r--r--activerecord/lib/active_record/schema_migration.rb37
-rw-r--r--activerecord/lib/active_record/scoping.rb73
-rw-r--r--activerecord/lib/active_record/scoping/default.rb190
-rw-r--r--activerecord/lib/active_record/scoping/named.rb101
-rw-r--r--activerecord/lib/active_record/secure_token.rb14
-rw-r--r--activerecord/lib/active_record/serialization.rb8
-rw-r--r--activerecord/lib/active_record/serializers/xml_serializer.rb193
-rw-r--r--activerecord/lib/active_record/statement_cache.rb93
-rw-r--r--activerecord/lib/active_record/store.rb117
-rw-r--r--activerecord/lib/active_record/suppressor.rb11
-rw-r--r--activerecord/lib/active_record/table_metadata.rb47
-rw-r--r--activerecord/lib/active_record/tasks/database_tasks.rb294
-rw-r--r--activerecord/lib/active_record/tasks/mysql_database_tasks.rb153
-rw-r--r--activerecord/lib/active_record/tasks/postgresql_database_tasks.rb119
-rw-r--r--activerecord/lib/active_record/tasks/sqlite_database_tasks.rb54
-rw-r--r--activerecord/lib/active_record/test_databases.rb38
-rw-r--r--activerecord/lib/active_record/test_fixtures.rb201
-rw-r--r--activerecord/lib/active_record/timestamp.rb101
-rw-r--r--activerecord/lib/active_record/touch_later.rb24
-rw-r--r--activerecord/lib/active_record/transactions.rb323
-rw-r--r--activerecord/lib/active_record/translation.rb4
-rw-r--r--activerecord/lib/active_record/type.rb65
-rw-r--r--activerecord/lib/active_record/type/adapter_specific_registry.rb109
-rw-r--r--activerecord/lib/active_record/type/big_integer.rb13
-rw-r--r--activerecord/lib/active_record/type/binary.rb50
-rw-r--r--activerecord/lib/active_record/type/boolean.rb19
-rw-r--r--activerecord/lib/active_record/type/date.rb48
-rw-r--r--activerecord/lib/active_record/type/date_time.rb43
-rw-r--r--activerecord/lib/active_record/type/decimal.rb48
-rw-r--r--activerecord/lib/active_record/type/decimal_without_scale.rb8
-rw-r--r--activerecord/lib/active_record/type/float.rb25
-rw-r--r--activerecord/lib/active_record/type/hash_lookup_type_map.rb8
-rw-r--r--activerecord/lib/active_record/type/helpers.rb4
-rw-r--r--activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb30
-rw-r--r--activerecord/lib/active_record/type/helpers/mutable.rb18
-rw-r--r--activerecord/lib/active_record/type/helpers/numeric.rb34
-rw-r--r--activerecord/lib/active_record/type/helpers/time_value.rb58
-rw-r--r--activerecord/lib/active_record/type/integer.rb66
-rw-r--r--activerecord/lib/active_record/type/internal/timezone.rb17
-rw-r--r--activerecord/lib/active_record/type/json.rb30
-rw-r--r--activerecord/lib/active_record/type/serialized.rb32
-rw-r--r--activerecord/lib/active_record/type/string.rb36
-rw-r--r--activerecord/lib/active_record/type/text.rb4
-rw-r--r--activerecord/lib/active_record/type/time.rb41
-rw-r--r--activerecord/lib/active_record/type/type_map.rb32
-rw-r--r--activerecord/lib/active_record/type/unsigned_integer.rb16
-rw-r--r--activerecord/lib/active_record/type/value.rb104
-rw-r--r--activerecord/lib/active_record/type_caster.rb8
-rw-r--r--activerecord/lib/active_record/type_caster/connection.rb19
-rw-r--r--activerecord/lib/active_record/type_caster/map.rb11
-rw-r--r--activerecord/lib/active_record/validations.rb57
-rw-r--r--activerecord/lib/active_record/validations/absence.rb3
-rw-r--r--activerecord/lib/active_record/validations/associated.rb17
-rw-r--r--activerecord/lib/active_record/validations/length.rb14
-rw-r--r--activerecord/lib/active_record/validations/presence.rb12
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb78
-rw-r--r--activerecord/lib/active_record/version.rb4
-rw-r--r--activerecord/lib/arel.rb45
-rw-r--r--activerecord/lib/arel/alias_predication.rb9
-rw-r--r--activerecord/lib/arel/attributes.rb22
-rw-r--r--activerecord/lib/arel/attributes/attribute.rb37
-rw-r--r--activerecord/lib/arel/collectors/bind.rb24
-rw-r--r--activerecord/lib/arel/collectors/composite.rb31
-rw-r--r--activerecord/lib/arel/collectors/plain_string.rb20
-rw-r--r--activerecord/lib/arel/collectors/sql_string.rb20
-rw-r--r--activerecord/lib/arel/collectors/substitute_binds.rb28
-rw-r--r--activerecord/lib/arel/compatibility/wheres.rb35
-rw-r--r--activerecord/lib/arel/crud.rb42
-rw-r--r--activerecord/lib/arel/delete_manager.rb18
-rw-r--r--activerecord/lib/arel/errors.rb9
-rw-r--r--activerecord/lib/arel/expressions.rb29
-rw-r--r--activerecord/lib/arel/factory_methods.rb49
-rw-r--r--activerecord/lib/arel/insert_manager.rb49
-rw-r--r--activerecord/lib/arel/math.rb45
-rw-r--r--activerecord/lib/arel/nodes.rb67
-rw-r--r--activerecord/lib/arel/nodes/and.rb32
-rw-r--r--activerecord/lib/arel/nodes/ascending.rb23
-rw-r--r--activerecord/lib/arel/nodes/binary.rb52
-rw-r--r--activerecord/lib/arel/nodes/bind_param.rb32
-rw-r--r--activerecord/lib/arel/nodes/case.rb55
-rw-r--r--activerecord/lib/arel/nodes/casted.rb46
-rw-r--r--activerecord/lib/arel/nodes/count.rb12
-rw-r--r--activerecord/lib/arel/nodes/delete_statement.rb45
-rw-r--r--activerecord/lib/arel/nodes/descending.rb23
-rw-r--r--activerecord/lib/arel/nodes/equality.rb18
-rw-r--r--activerecord/lib/arel/nodes/extract.rb24
-rw-r--r--activerecord/lib/arel/nodes/false.rb16
-rw-r--r--activerecord/lib/arel/nodes/full_outer_join.rb8
-rw-r--r--activerecord/lib/arel/nodes/function.rb44
-rw-r--r--activerecord/lib/arel/nodes/grouping.rb8
-rw-r--r--activerecord/lib/arel/nodes/in.rb8
-rw-r--r--activerecord/lib/arel/nodes/infix_operation.rb80
-rw-r--r--activerecord/lib/arel/nodes/inner_join.rb8
-rw-r--r--activerecord/lib/arel/nodes/insert_statement.rb37
-rw-r--r--activerecord/lib/arel/nodes/join_source.rb20
-rw-r--r--activerecord/lib/arel/nodes/matches.rb18
-rw-r--r--activerecord/lib/arel/nodes/named_function.rb23
-rw-r--r--activerecord/lib/arel/nodes/node.rb50
-rw-r--r--activerecord/lib/arel/nodes/node_expression.rb13
-rw-r--r--activerecord/lib/arel/nodes/outer_join.rb8
-rw-r--r--activerecord/lib/arel/nodes/over.rb15
-rw-r--r--activerecord/lib/arel/nodes/regexp.rb16
-rw-r--r--activerecord/lib/arel/nodes/right_outer_join.rb8
-rw-r--r--activerecord/lib/arel/nodes/select_core.rb63
-rw-r--r--activerecord/lib/arel/nodes/select_statement.rb41
-rw-r--r--activerecord/lib/arel/nodes/sql_literal.rb16
-rw-r--r--activerecord/lib/arel/nodes/string_join.rb11
-rw-r--r--activerecord/lib/arel/nodes/table_alias.rb27
-rw-r--r--activerecord/lib/arel/nodes/terminal.rb16
-rw-r--r--activerecord/lib/arel/nodes/true.rb16
-rw-r--r--activerecord/lib/arel/nodes/unary.rb44
-rw-r--r--activerecord/lib/arel/nodes/unary_operation.rb20
-rw-r--r--activerecord/lib/arel/nodes/unqualified_column.rb22
-rw-r--r--activerecord/lib/arel/nodes/update_statement.rb41
-rw-r--r--activerecord/lib/arel/nodes/values.rb16
-rw-r--r--activerecord/lib/arel/nodes/values_list.rb24
-rw-r--r--activerecord/lib/arel/nodes/window.rb126
-rw-r--r--activerecord/lib/arel/nodes/with.rb11
-rw-r--r--activerecord/lib/arel/order_predications.rb13
-rw-r--r--activerecord/lib/arel/predications.rb249
-rw-r--r--activerecord/lib/arel/select_manager.rb271
-rw-r--r--activerecord/lib/arel/table.rb110
-rw-r--r--activerecord/lib/arel/tree_manager.rb72
-rw-r--r--activerecord/lib/arel/update_manager.rb34
-rw-r--r--activerecord/lib/arel/visitors.rb20
-rw-r--r--activerecord/lib/arel/visitors/depth_first.rb199
-rw-r--r--activerecord/lib/arel/visitors/dot.rb292
-rw-r--r--activerecord/lib/arel/visitors/ibm_db.rb21
-rw-r--r--activerecord/lib/arel/visitors/informix.rb55
-rw-r--r--activerecord/lib/arel/visitors/mssql.rb143
-rw-r--r--activerecord/lib/arel/visitors/mysql.rb83
-rw-r--r--activerecord/lib/arel/visitors/oracle.rb159
-rw-r--r--activerecord/lib/arel/visitors/oracle12.rb67
-rw-r--r--activerecord/lib/arel/visitors/postgresql.rb116
-rw-r--r--activerecord/lib/arel/visitors/sqlite.rb39
-rw-r--r--activerecord/lib/arel/visitors/to_sql.rb911
-rw-r--r--activerecord/lib/arel/visitors/visitor.rb42
-rw-r--r--activerecord/lib/arel/visitors/where_sql.rb23
-rw-r--r--activerecord/lib/arel/window_predications.rb9
-rw-r--r--activerecord/lib/rails/generators/active_record.rb12
-rw-r--r--activerecord/lib/rails/generators/active_record/application_record/application_record_generator.rb27
-rw-r--r--activerecord/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt5
-rw-r--r--activerecord/lib/rails/generators/active_record/migration.rb32
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/migration_generator.rb76
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb24
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt24
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/templates/migration.rb42
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/templates/migration.rb.tt46
-rw-r--r--activerecord/lib/rails/generators/active_record/model/model_generator.rb41
-rw-r--r--activerecord/lib/rails/generators/active_record/model/templates/model.rb.tt (renamed from activerecord/lib/rails/generators/active_record/model/templates/model.rb)0
-rw-r--r--activerecord/lib/rails/generators/active_record/model/templates/module.rb.tt (renamed from activerecord/lib/rails/generators/active_record/model/templates/module.rb)0
-rw-r--r--activerecord/test/.gitignore1
-rw-r--r--activerecord/test/active_record/connection_adapters/fake_adapter.rb10
-rw-r--r--activerecord/test/assets/schema_dump_5_1.yml345
-rw-r--r--activerecord/test/cases/adapter_test.rb412
-rw-r--r--activerecord/test/cases/adapters/mysql/active_schema_test.rb192
-rw-r--r--activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb54
-rw-r--r--activerecord/test/cases/adapters/mysql/charset_collation_test.rb54
-rw-r--r--activerecord/test/cases/adapters/mysql/connection_test.rb191
-rw-r--r--activerecord/test/cases/adapters/mysql/consistency_test.rb49
-rw-r--r--activerecord/test/cases/adapters/mysql/enum_test.rb10
-rw-r--r--activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb152
-rw-r--r--activerecord/test/cases/adapters/mysql/quoting_test.rb15
-rw-r--r--activerecord/test/cases/adapters/mysql/reserved_word_test.rb153
-rw-r--r--activerecord/test/cases/adapters/mysql/schema_test.rb100
-rw-r--r--activerecord/test/cases/adapters/mysql/sp_test.rb15
-rw-r--r--activerecord/test/cases/adapters/mysql/sql_types_test.rb14
-rw-r--r--activerecord/test/cases/adapters/mysql/statement_pool_test.rb19
-rw-r--r--activerecord/test/cases/adapters/mysql/table_options_test.rb42
-rw-r--r--activerecord/test/cases/adapters/mysql/unsigned_type_test.rb30
-rw-r--r--activerecord/test/cases/adapters/mysql2/active_schema_test.rb97
-rw-r--r--activerecord/test/cases/adapters/mysql2/auto_increment_test.rb34
-rw-r--r--activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb8
-rw-r--r--activerecord/test/cases/adapters/mysql2/boolean_test.rb24
-rw-r--r--activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb45
-rw-r--r--activerecord/test/cases/adapters/mysql2/charset_collation_test.rb34
-rw-r--r--activerecord/test/cases/adapters/mysql2/connection_test.rb161
-rw-r--r--activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb55
-rw-r--r--activerecord/test/cases/adapters/mysql2/enum_test.rb15
-rw-r--r--activerecord/test/cases/adapters/mysql2/explain_test.rb38
-rw-r--r--activerecord/test/cases/adapters/mysql2/json_test.rb24
-rw-r--r--activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb77
-rw-r--r--activerecord/test/cases/adapters/mysql2/reserved_word_test.rb152
-rw-r--r--activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb57
-rw-r--r--activerecord/test/cases/adapters/mysql2/schema_test.rb95
-rw-r--r--activerecord/test/cases/adapters/mysql2/sp_test.rb38
-rw-r--r--activerecord/test/cases/adapters/mysql2/sql_types_test.rb14
-rw-r--r--activerecord/test/cases/adapters/mysql2/table_options_test.rb87
-rw-r--r--activerecord/test/cases/adapters/mysql2/transaction_test.rb149
-rw-r--r--activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb44
-rw-r--r--activerecord/test/cases/adapters/mysql2/virtual_column_test.rb63
-rw-r--r--activerecord/test/cases/adapters/postgresql/active_schema_test.rb73
-rw-r--r--activerecord/test/cases/adapters/postgresql/array_test.rb203
-rw-r--r--activerecord/test/cases/adapters/postgresql/bit_string_test.rb35
-rw-r--r--activerecord/test/cases/adapters/postgresql/bytea_test.rb52
-rw-r--r--activerecord/test/cases/adapters/postgresql/case_insensitive_test.rb28
-rw-r--r--activerecord/test/cases/adapters/postgresql/change_schema_test.rb12
-rw-r--r--activerecord/test/cases/adapters/postgresql/cidr_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/citext_test.rb116
-rw-r--r--activerecord/test/cases/adapters/postgresql/collation_test.rb32
-rw-r--r--activerecord/test/cases/adapters/postgresql/composite_test.rb34
-rw-r--r--activerecord/test/cases/adapters/postgresql/connection_test.rb168
-rw-r--r--activerecord/test/cases/adapters/postgresql/create_unlogged_tables_test.rb74
-rw-r--r--activerecord/test/cases/adapters/postgresql/datatype_test.rb31
-rw-r--r--activerecord/test/cases/adapters/postgresql/date_test.rb42
-rw-r--r--activerecord/test/cases/adapters/postgresql/domain_test.rb16
-rw-r--r--activerecord/test/cases/adapters/postgresql/enum_test.rb20
-rw-r--r--activerecord/test/cases/adapters/postgresql/explain_test.rb18
-rw-r--r--activerecord/test/cases/adapters/postgresql/extension_migration_test.rb22
-rw-r--r--activerecord/test/cases/adapters/postgresql/foreign_table_test.rb109
-rw-r--r--activerecord/test/cases/adapters/postgresql/full_text_test.rb14
-rw-r--r--activerecord/test/cases/adapters/postgresql/geometric_test.rb253
-rw-r--r--activerecord/test/cases/adapters/postgresql/hstore_test.rb535
-rw-r--r--activerecord/test/cases/adapters/postgresql/infinity_test.rb51
-rw-r--r--activerecord/test/cases/adapters/postgresql/integer_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/json_test.rb195
-rw-r--r--activerecord/test/cases/adapters/postgresql/ltree_test.rb26
-rw-r--r--activerecord/test/cases/adapters/postgresql/money_test.rb37
-rw-r--r--activerecord/test/cases/adapters/postgresql/network_test.rb58
-rw-r--r--activerecord/test/cases/adapters/postgresql/numbers_test.rb12
-rw-r--r--activerecord/test/cases/adapters/postgresql/partitions_test.rb22
-rw-r--r--activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb353
-rw-r--r--activerecord/test/cases/adapters/postgresql/prepared_statements_disabled_test.rb27
-rw-r--r--activerecord/test/cases/adapters/postgresql/quoting_test.rb13
-rw-r--r--activerecord/test/cases/adapters/postgresql/range_test.rb155
-rw-r--r--activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb26
-rw-r--r--activerecord/test/cases/adapters/postgresql/rename_table_test.rb16
-rw-r--r--activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb55
-rw-r--r--activerecord/test/cases/adapters/postgresql/schema_test.rb375
-rw-r--r--activerecord/test/cases/adapters/postgresql/serial_test.rb106
-rw-r--r--activerecord/test/cases/adapters/postgresql/statement_pool_test.rb22
-rw-r--r--activerecord/test/cases/adapters/postgresql/timestamp_test.rb28
-rw-r--r--activerecord/test/cases/adapters/postgresql/transaction_test.rb190
-rw-r--r--activerecord/test/cases/adapters/postgresql/type_lookup_test.rb28
-rw-r--r--activerecord/test/cases/adapters/postgresql/utils_test.rb20
-rw-r--r--activerecord/test/cases/adapters/postgresql/uuid_test.rb308
-rw-r--r--activerecord/test/cases/adapters/postgresql/view_test.rb64
-rw-r--r--activerecord/test/cases/adapters/postgresql/xml_test.rb18
-rw-r--r--activerecord/test/cases/adapters/sqlite3/bind_parameter_test.rb20
-rw-r--r--activerecord/test/cases/adapters/sqlite3/collation_test.rb32
-rw-r--r--activerecord/test/cases/adapters/sqlite3/copy_table_test.rb59
-rw-r--r--activerecord/test/cases/adapters/sqlite3/explain_test.rb38
-rw-r--r--activerecord/test/cases/adapters/sqlite3/json_test.rb29
-rw-r--r--activerecord/test/cases/adapters/sqlite3/quoting_test.rb176
-rw-r--r--activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb436
-rw-r--r--activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb20
-rw-r--r--activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb31
-rw-r--r--activerecord/test/cases/aggregations_test.rb36
-rw-r--r--activerecord/test/cases/ar_schema_test.rb207
-rw-r--r--activerecord/test/cases/arel/attributes/attribute_test.rb1015
-rw-r--r--activerecord/test/cases/arel/attributes/math_test.rb83
-rw-r--r--activerecord/test/cases/arel/attributes_test.rb68
-rw-r--r--activerecord/test/cases/arel/collectors/bind_test.rb40
-rw-r--r--activerecord/test/cases/arel/collectors/composite_test.rb47
-rw-r--r--activerecord/test/cases/arel/collectors/sql_string_test.rb41
-rw-r--r--activerecord/test/cases/arel/collectors/substitute_bind_collector_test.rb48
-rw-r--r--activerecord/test/cases/arel/crud_test.rb65
-rw-r--r--activerecord/test/cases/arel/delete_manager_test.rb53
-rw-r--r--activerecord/test/cases/arel/factory_methods_test.rb46
-rw-r--r--activerecord/test/cases/arel/helper.rb45
-rw-r--r--activerecord/test/cases/arel/insert_manager_test.rb242
-rw-r--r--activerecord/test/cases/arel/nodes/and_test.rb21
-rw-r--r--activerecord/test/cases/arel/nodes/as_test.rb36
-rw-r--r--activerecord/test/cases/arel/nodes/ascending_test.rb46
-rw-r--r--activerecord/test/cases/arel/nodes/bin_test.rb35
-rw-r--r--activerecord/test/cases/arel/nodes/binary_test.rb29
-rw-r--r--activerecord/test/cases/arel/nodes/bind_param_test.rb22
-rw-r--r--activerecord/test/cases/arel/nodes/case_test.rb86
-rw-r--r--activerecord/test/cases/arel/nodes/casted_test.rb18
-rw-r--r--activerecord/test/cases/arel/nodes/count_test.rb35
-rw-r--r--activerecord/test/cases/arel/nodes/delete_statement_test.rb36
-rw-r--r--activerecord/test/cases/arel/nodes/descending_test.rb46
-rw-r--r--activerecord/test/cases/arel/nodes/distinct_test.rb21
-rw-r--r--activerecord/test/cases/arel/nodes/equality_test.rb86
-rw-r--r--activerecord/test/cases/arel/nodes/extract_test.rb43
-rw-r--r--activerecord/test/cases/arel/nodes/false_test.rb21
-rw-r--r--activerecord/test/cases/arel/nodes/grouping_test.rb26
-rw-r--r--activerecord/test/cases/arel/nodes/infix_operation_test.rb42
-rw-r--r--activerecord/test/cases/arel/nodes/insert_statement_test.rb44
-rw-r--r--activerecord/test/cases/arel/nodes/named_function_test.rb48
-rw-r--r--activerecord/test/cases/arel/nodes/node_test.rb41
-rw-r--r--activerecord/test/cases/arel/nodes/not_test.rb31
-rw-r--r--activerecord/test/cases/arel/nodes/or_test.rb36
-rw-r--r--activerecord/test/cases/arel/nodes/over_test.rb69
-rw-r--r--activerecord/test/cases/arel/nodes/select_core_test.rb71
-rw-r--r--activerecord/test/cases/arel/nodes/select_statement_test.rb51
-rw-r--r--activerecord/test/cases/arel/nodes/sql_literal_test.rb75
-rw-r--r--activerecord/test/cases/arel/nodes/sum_test.rb35
-rw-r--r--activerecord/test/cases/arel/nodes/table_alias_test.rb29
-rw-r--r--activerecord/test/cases/arel/nodes/true_test.rb21
-rw-r--r--activerecord/test/cases/arel/nodes/unary_operation_test.rb41
-rw-r--r--activerecord/test/cases/arel/nodes/update_statement_test.rb60
-rw-r--r--activerecord/test/cases/arel/nodes/window_test.rb81
-rw-r--r--activerecord/test/cases/arel/nodes_test.rb34
-rw-r--r--activerecord/test/cases/arel/select_manager_test.rb1225
-rw-r--r--activerecord/test/cases/arel/support/fake_record.rb129
-rw-r--r--activerecord/test/cases/arel/table_test.rb216
-rw-r--r--activerecord/test/cases/arel/update_manager_test.rb126
-rw-r--r--activerecord/test/cases/arel/visitors/depth_first_test.rb270
-rw-r--r--activerecord/test/cases/arel/visitors/dispatch_contamination_test.rb72
-rw-r--r--activerecord/test/cases/arel/visitors/dot_test.rb83
-rw-r--r--activerecord/test/cases/arel/visitors/ibm_db_test.rb73
-rw-r--r--activerecord/test/cases/arel/visitors/informix_test.rb98
-rw-r--r--activerecord/test/cases/arel/visitors/mssql_test.rb138
-rw-r--r--activerecord/test/cases/arel/visitors/mysql_test.rb109
-rw-r--r--activerecord/test/cases/arel/visitors/oracle12_test.rb100
-rw-r--r--activerecord/test/cases/arel/visitors/oracle_test.rb236
-rw-r--r--activerecord/test/cases/arel/visitors/postgres_test.rb320
-rw-r--r--activerecord/test/cases/arel/visitors/sqlite_test.rb75
-rw-r--r--activerecord/test/cases/arel/visitors/to_sql_test.rb713
-rw-r--r--activerecord/test/cases/associations/association_scope_test.rb16
-rw-r--r--activerecord/test/cases/associations/belongs_to_associations_test.rb579
-rw-r--r--activerecord/test/cases/associations/bidirectional_destroy_dependencies_test.rb43
-rw-r--r--activerecord/test/cases/associations/callbacks_test.rb77
-rw-r--r--activerecord/test/cases/associations/cascaded_eager_loading_test.rb101
-rw-r--r--activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb90
-rw-r--r--activerecord/test/cases/associations/eager_load_nested_include_test.rb66
-rw-r--r--activerecord/test/cases/associations/eager_singularization_test.rb42
-rw-r--r--activerecord/test/cases/associations/eager_test.rb856
-rw-r--r--activerecord/test/cases/associations/extension_test.rb33
-rw-r--r--activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb550
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb1451
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb757
-rw-r--r--activerecord/test/cases/associations/has_one_associations_test.rb350
-rw-r--r--activerecord/test/cases/associations/has_one_through_associations_test.rb221
-rw-r--r--activerecord/test/cases/associations/inner_join_association_test.rb92
-rw-r--r--activerecord/test/cases/associations/inverse_associations_test.rb405
-rw-r--r--activerecord/test/cases/associations/join_model_test.rb339
-rw-r--r--activerecord/test/cases/associations/left_outer_join_association_test.rb91
-rw-r--r--activerecord/test/cases/associations/nested_through_associations_test.rb195
-rw-r--r--activerecord/test/cases/associations/required_test.rb60
-rw-r--r--activerecord/test/cases/associations_test.rb206
-rw-r--r--activerecord/test/cases/attribute_decorators_test.rb52
-rw-r--r--activerecord/test/cases/attribute_methods/read_test.rb15
-rw-r--r--activerecord/test/cases/attribute_methods_test.rb632
-rw-r--r--activerecord/test/cases/attribute_set_test.rb211
-rw-r--r--activerecord/test/cases/attribute_test.rb192
-rw-r--r--activerecord/test/cases/attributes_test.rb145
-rw-r--r--activerecord/test/cases/autosave_association_test.rb1014
-rw-r--r--activerecord/test/cases/base_test.rb764
-rw-r--r--activerecord/test/cases/batches_test.rb544
-rw-r--r--activerecord/test/cases/binary_test.rb26
-rw-r--r--activerecord/test/cases/bind_parameter_test.rb144
-rw-r--r--activerecord/test/cases/boolean_test.rb43
-rw-r--r--activerecord/test/cases/cache_key_test.rb53
-rw-r--r--activerecord/test/cases/calculations_test.rb539
-rw-r--r--activerecord/test/cases/callbacks_test.rb274
-rw-r--r--activerecord/test/cases/clone_test.rb22
-rw-r--r--activerecord/test/cases/coders/json_test.rb17
-rw-r--r--activerecord/test/cases/coders/yaml_column_test.rb29
-rw-r--r--activerecord/test/cases/collection_cache_key_test.rb175
-rw-r--r--activerecord/test/cases/column_alias_test.rb16
-rw-r--r--activerecord/test/cases/column_definition_test.rb68
-rw-r--r--activerecord/test/cases/comment_test.rb168
-rw-r--r--activerecord/test/cases/connection_adapters/adapter_leasing_test.rb22
-rw-r--r--activerecord/test/cases/connection_adapters/connection_handler_test.rb341
-rw-r--r--activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb285
-rw-r--r--activerecord/test/cases/connection_adapters/connection_specification_test.rb4
-rw-r--r--activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb119
-rw-r--r--activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb109
-rw-r--r--activerecord/test/cases/connection_adapters/quoting_test.rb13
-rw-r--r--activerecord/test/cases/connection_adapters/schema_cache_test.rb81
-rw-r--r--activerecord/test/cases/connection_adapters/type_lookup_test.rb186
-rw-r--r--activerecord/test/cases/connection_management_test.rb91
-rw-r--r--activerecord/test/cases/connection_pool_test.rb361
-rw-r--r--activerecord/test/cases/connection_specification/resolver_test.rb79
-rw-r--r--activerecord/test/cases/core_test.rb127
-rw-r--r--activerecord/test/cases/counter_cache_test.rb260
-rw-r--r--activerecord/test/cases/custom_locking_test.rb10
-rw-r--r--activerecord/test/cases/database_statements_test.rb36
-rw-r--r--activerecord/test/cases/date_test.rb46
-rw-r--r--activerecord/test/cases/date_time_precision_test.rb162
-rw-r--r--activerecord/test/cases/date_time_test.rb31
-rw-r--r--activerecord/test/cases/defaults_test.rb174
-rw-r--r--activerecord/test/cases/dirty_test.rb627
-rw-r--r--activerecord/test/cases/disconnected_test.rb2
-rw-r--r--activerecord/test/cases/dup_test.rb74
-rw-r--r--activerecord/test/cases/enum_test.rb276
-rw-r--r--activerecord/test/cases/errors_test.rb18
-rw-r--r--activerecord/test/cases/explain_subscriber_test.rb32
-rw-r--r--activerecord/test/cases/explain_test.rb64
-rw-r--r--activerecord/test/cases/filter_attributes_test.rb136
-rw-r--r--activerecord/test/cases/finder_respond_to_test.rb23
-rw-r--r--activerecord/test/cases/finder_test.rb793
-rw-r--r--activerecord/test/cases/fixture_set/file_test.rb76
-rw-r--r--activerecord/test/cases/fixtures_test.rb799
-rw-r--r--activerecord/test/cases/forbidden_attributes_protection_test.rb109
-rw-r--r--activerecord/test/cases/habtm_destroy_order_test.rb28
-rw-r--r--activerecord/test/cases/helper.rb87
-rw-r--r--activerecord/test/cases/hot_compatibility_test.rb106
-rw-r--r--activerecord/test/cases/i18n_test.rb35
-rw-r--r--activerecord/test/cases/inheritance_test.rb382
-rw-r--r--activerecord/test/cases/instrumentation_test.rb72
-rw-r--r--activerecord/test/cases/integration_test.rb178
-rw-r--r--activerecord/test/cases/invalid_connection_test.rb32
-rw-r--r--activerecord/test/cases/invalid_date_test.rb32
-rw-r--r--activerecord/test/cases/invertible_migration_test.rb168
-rw-r--r--activerecord/test/cases/json_attribute_test.rb35
-rw-r--r--activerecord/test/cases/json_serialization_test.rb123
-rw-r--r--activerecord/test/cases/json_shared_test_cases.rb269
-rw-r--r--activerecord/test/cases/legacy_configurations_test.rb43
-rw-r--r--activerecord/test/cases/locking_test.rb475
-rw-r--r--activerecord/test/cases/log_subscriber_test.rb152
-rw-r--r--activerecord/test/cases/migration/change_schema_test.rb166
-rw-r--r--activerecord/test/cases/migration/change_table_test.rb33
-rw-r--r--activerecord/test/cases/migration/column_attributes_test.rb93
-rw-r--r--activerecord/test/cases/migration/column_positioning_test.rb26
-rw-r--r--activerecord/test/cases/migration/columns_test.rb157
-rw-r--r--activerecord/test/cases/migration/command_recorder_test.rb76
-rw-r--r--activerecord/test/cases/migration/compatibility_test.rb383
-rw-r--r--activerecord/test/cases/migration/create_join_table_test.rb68
-rw-r--r--activerecord/test/cases/migration/foreign_key_test.rb617
-rw-r--r--activerecord/test/cases/migration/helper.rb6
-rw-r--r--activerecord/test/cases/migration/index_test.rb117
-rw-r--r--activerecord/test/cases/migration/logger_test.rb6
-rw-r--r--activerecord/test/cases/migration/pending_migrations_test.rb65
-rw-r--r--activerecord/test/cases/migration/references_foreign_key_test.rb312
-rw-r--r--activerecord/test/cases/migration/references_index_test.rb40
-rw-r--r--activerecord/test/cases/migration/references_statements_test.rb50
-rw-r--r--activerecord/test/cases/migration/rename_table_test.rb51
-rw-r--r--activerecord/test/cases/migration/table_and_index_test.rb24
-rw-r--r--activerecord/test/cases/migration_test.rb647
-rw-r--r--activerecord/test/cases/migrator_test.rb316
-rw-r--r--activerecord/test/cases/mixin_test.rb18
-rw-r--r--activerecord/test/cases/modules_test.rb72
-rw-r--r--activerecord/test/cases/multiparameter_attributes_test.rb107
-rw-r--r--activerecord/test/cases/multiple_db_test.rb28
-rw-r--r--activerecord/test/cases/nested_attributes_test.rb661
-rw-r--r--activerecord/test/cases/nested_attributes_with_callbacks_test.rb48
-rw-r--r--activerecord/test/cases/null_relation_test.rb85
-rw-r--r--activerecord/test/cases/numeric_data_test.rb93
-rw-r--r--activerecord/test/cases/persistence_test.rb644
-rw-r--r--activerecord/test/cases/pooled_connections_test.rb16
-rw-r--r--activerecord/test/cases/primary_keys_test.rb322
-rw-r--r--activerecord/test/cases/query_cache_test.rb549
-rw-r--r--activerecord/test/cases/quoting_test.rb212
-rw-r--r--activerecord/test/cases/readonly_test.rb96
-rw-r--r--activerecord/test/cases/reaper_test.rb24
-rw-r--r--activerecord/test/cases/reflection_test.rb326
-rw-r--r--activerecord/test/cases/relation/delegation_test.rb97
-rw-r--r--activerecord/test/cases/relation/delete_all_test.rb104
-rw-r--r--activerecord/test/cases/relation/merging_test.rb89
-rw-r--r--activerecord/test/cases/relation/mutation_test.rb140
-rw-r--r--activerecord/test/cases/relation/or_test.rb107
-rw-r--r--activerecord/test/cases/relation/predicate_builder_test.rb6
-rw-r--r--activerecord/test/cases/relation/record_fetch_warning_test.rb40
-rw-r--r--activerecord/test/cases/relation/select_test.rb15
-rw-r--r--activerecord/test/cases/relation/update_all_test.rb239
-rw-r--r--activerecord/test/cases/relation/where_chain_test.rb46
-rw-r--r--activerecord/test/cases/relation/where_clause_test.rb169
-rw-r--r--activerecord/test/cases/relation/where_test.rb146
-rw-r--r--activerecord/test/cases/relation_test.rb296
-rw-r--r--activerecord/test/cases/relations_test.rb1328
-rw-r--r--activerecord/test/cases/reload_models_test.rb22
-rw-r--r--activerecord/test/cases/reserved_word_test.rb141
-rw-r--r--activerecord/test/cases/result_test.rb55
-rw-r--r--activerecord/test/cases/sanitize_test.rb173
-rw-r--r--activerecord/test/cases/schema_dumper_test.rb329
-rw-r--r--activerecord/test/cases/schema_loading_test.rb54
-rw-r--r--activerecord/test/cases/scoping/default_scoping_test.rb369
-rw-r--r--activerecord/test/cases/scoping/named_scoping_test.rb239
-rw-r--r--activerecord/test/cases/scoping/relation_scoping_test.rb196
-rw-r--r--activerecord/test/cases/secure_token_test.rb8
-rw-r--r--activerecord/test/cases/serialization_test.rb52
-rw-r--r--activerecord/test/cases/serialized_attribute_test.rb158
-rw-r--r--activerecord/test/cases/statement_cache_test.rb80
-rw-r--r--activerecord/test/cases/store_test.rb165
-rw-r--r--activerecord/test/cases/suppressor_test.rb51
-rw-r--r--activerecord/test/cases/tasks/database_tasks_test.rb1075
-rw-r--r--activerecord/test/cases/tasks/mysql_rake_test.rb587
-rw-r--r--activerecord/test/cases/tasks/postgresql_rake_test.rb691
-rw-r--r--activerecord/test/cases/tasks/sqlite_rake_test.rb375
-rw-r--r--activerecord/test/cases/test_case.rb63
-rw-r--r--activerecord/test/cases/test_fixtures_test.rb26
-rw-r--r--activerecord/test/cases/time_precision_test.rb161
-rw-r--r--activerecord/test/cases/timestamp_test.rb192
-rw-r--r--activerecord/test/cases/touch_later_test.rb39
-rw-r--r--activerecord/test/cases/transaction_callbacks_test.rb283
-rw-r--r--activerecord/test/cases/transaction_isolation_test.rb22
-rw-r--r--activerecord/test/cases/transactions_test.rb626
-rw-r--r--activerecord/test/cases/type/adapter_specific_registry_test.rb2
-rw-r--r--activerecord/test/cases/type/date_time_test.rb16
-rw-r--r--activerecord/test/cases/type/decimal_test.rb51
-rw-r--r--activerecord/test/cases/type/integer_test.rb106
-rw-r--r--activerecord/test/cases/type/string_test.rb32
-rw-r--r--activerecord/test/cases/type/type_map_test.rb57
-rw-r--r--activerecord/test/cases/type/unsigned_integer_test.rb4
-rw-r--r--activerecord/test/cases/type_test.rb2
-rw-r--r--activerecord/test/cases/types_test.rb109
-rw-r--r--activerecord/test/cases/unconnected_test.rb4
-rw-r--r--activerecord/test/cases/unsafe_raw_sql_test.rb319
-rw-r--r--activerecord/test/cases/validations/absence_validation_test.rb36
-rw-r--r--activerecord/test/cases/validations/association_validation_test.rb58
-rw-r--r--activerecord/test/cases/validations/i18n_generate_message_validation_test.rb30
-rw-r--r--activerecord/test/cases/validations/i18n_validation_test.rb53
-rw-r--r--activerecord/test/cases/validations/length_validation_test.rb80
-rw-r--r--activerecord/test/cases/validations/presence_validation_test.rb64
-rw-r--r--activerecord/test/cases/validations/uniqueness_validation_test.rb319
-rw-r--r--activerecord/test/cases/validations_repair_helper.rb2
-rw-r--r--activerecord/test/cases/validations_test.rb92
-rw-r--r--activerecord/test/cases/view_test.rb201
-rw-r--r--activerecord/test/cases/xml_serialization_test.rb447
-rw-r--r--activerecord/test/cases/yaml_serialization_test.rb66
-rw-r--r--activerecord/test/config.example.yml19
-rw-r--r--activerecord/test/config.rb4
-rw-r--r--activerecord/test/fixtures/.gitignore1
-rw-r--r--activerecord/test/fixtures/all/namespaced/accounts.yml2
-rw-r--r--activerecord/test/fixtures/author_addresses.yml6
-rw-r--r--activerecord/test/fixtures/authors.yml2
-rw-r--r--activerecord/test/fixtures/bad_posts.yml9
-rw-r--r--activerecord/test/fixtures/binaries.yml4
-rw-r--r--activerecord/test/fixtures/books.yml2
-rw-r--r--activerecord/test/fixtures/citations.yml5
-rw-r--r--activerecord/test/fixtures/content.yml3
-rw-r--r--activerecord/test/fixtures/content_positions.yml3
-rw-r--r--activerecord/test/fixtures/customers.yml11
-rw-r--r--activerecord/test/fixtures/memberships.yml7
-rw-r--r--activerecord/test/fixtures/minimalistics.yml3
-rw-r--r--activerecord/test/fixtures/naked/yml/courses_with_invalid_key.yml3
-rw-r--r--activerecord/test/fixtures/naked/yml/parrots.yml3
-rw-r--r--activerecord/test/fixtures/naked/yml/trees.yml3
-rw-r--r--activerecord/test/fixtures/other_comments.yml6
-rw-r--r--activerecord/test/fixtures/other_dogs.yml2
-rw-r--r--activerecord/test/fixtures/other_posts.yml8
-rw-r--r--activerecord/test/fixtures/posts.yml8
-rw-r--r--activerecord/test/fixtures/price_estimates.yml11
-rw-r--r--activerecord/test/fixtures/reserved_words/values.yml4
-rw-r--r--activerecord/test/fixtures/sponsors.yml3
-rw-r--r--activerecord/test/fixtures/subscribers.yml2
-rw-r--r--activerecord/test/fixtures/teapots.yml3
-rw-r--r--activerecord/test/migrations/10_urban/9_add_expressions.rb4
-rw-r--r--activerecord/test/migrations/decimal/1_give_me_big_numbers.rb12
-rw-r--r--activerecord/test/migrations/empty/.keep (renamed from actionview/test/tmp/.gitkeep)0
-rw-r--r--activerecord/test/migrations/magic/1_currencies_have_symbols.rb7
-rw-r--r--activerecord/test/migrations/missing/1000_people_have_middle_names.rb4
-rw-r--r--activerecord/test/migrations/missing/1_people_have_last_names.rb4
-rw-r--r--activerecord/test/migrations/missing/3_we_need_reminders.rb4
-rw-r--r--activerecord/test/migrations/missing/4_innocent_jointable.rb6
-rw-r--r--activerecord/test/migrations/rename/1_we_need_things.rb4
-rw-r--r--activerecord/test/migrations/rename/2_rename_things.rb4
-rw-r--r--activerecord/test/migrations/to_copy/1_people_have_hobbies.rb4
-rw-r--r--activerecord/test/migrations/to_copy/2_people_have_descriptions.rb4
-rw-r--r--activerecord/test/migrations/to_copy2/1_create_articles.rb4
-rw-r--r--activerecord/test/migrations/to_copy2/2_create_comments.rb4
-rw-r--r--activerecord/test/migrations/to_copy_with_name_collision/1_people_have_hobbies.rb4
-rw-r--r--activerecord/test/migrations/to_copy_with_timestamps/20090101010101_people_have_hobbies.rb4
-rw-r--r--activerecord/test/migrations/to_copy_with_timestamps/20090101010202_people_have_descriptions.rb4
-rw-r--r--activerecord/test/migrations/to_copy_with_timestamps2/20090101010101_create_articles.rb4
-rw-r--r--activerecord/test/migrations/to_copy_with_timestamps2/20090101010202_create_comments.rb4
-rw-r--r--activerecord/test/migrations/valid/1_valid_people_have_last_names.rb4
-rw-r--r--activerecord/test/migrations/valid/2_we_need_reminders.rb4
-rw-r--r--activerecord/test/migrations/valid/3_innocent_jointable.rb6
-rw-r--r--activerecord/test/migrations/valid_with_subdirectories/1_valid_people_have_last_names.rb4
-rw-r--r--activerecord/test/migrations/valid_with_subdirectories/sub/2_we_need_reminders.rb4
-rw-r--r--activerecord/test/migrations/valid_with_subdirectories/sub1/3_innocent_jointable.rb6
-rw-r--r--activerecord/test/migrations/valid_with_timestamps/20100101010101_valid_with_timestamps_people_have_last_names.rb4
-rw-r--r--activerecord/test/migrations/valid_with_timestamps/20100201010101_valid_with_timestamps_we_need_reminders.rb4
-rw-r--r--activerecord/test/migrations/valid_with_timestamps/20100301010101_valid_with_timestamps_innocent_jointable.rb6
-rw-r--r--activerecord/test/migrations/version_check/20131219224947_migration_version_check.rb4
-rw-r--r--activerecord/test/models/account.rb41
-rw-r--r--activerecord/test/models/admin.rb4
-rw-r--r--activerecord/test/models/admin/account.rb2
-rw-r--r--activerecord/test/models/admin/randomly_named_c1.rb16
-rw-r--r--activerecord/test/models/admin/user.rb24
-rw-r--r--activerecord/test/models/aircraft.rb5
-rw-r--r--activerecord/test/models/arunit2_model.rb2
-rw-r--r--activerecord/test/models/author.rb226
-rw-r--r--activerecord/test/models/auto_id.rb2
-rw-r--r--activerecord/test/models/autoloadable/extra_firm.rb2
-rw-r--r--activerecord/test/models/binary.rb2
-rw-r--r--activerecord/test/models/bird.rb4
-rw-r--r--activerecord/test/models/book.rb18
-rw-r--r--activerecord/test/models/boolean.rb5
-rw-r--r--activerecord/test/models/bulb.rb9
-rw-r--r--activerecord/test/models/cake_designer.rb2
-rw-r--r--activerecord/test/models/car.rb23
-rw-r--r--activerecord/test/models/carrier.rb4
-rw-r--r--activerecord/test/models/cat.rb12
-rw-r--r--activerecord/test/models/categorization.rb16
-rw-r--r--activerecord/test/models/category.rb43
-rw-r--r--activerecord/test/models/chef.rb6
-rw-r--r--activerecord/test/models/citation.rb5
-rw-r--r--activerecord/test/models/club.rb18
-rw-r--r--activerecord/test/models/college.rb6
-rw-r--r--activerecord/test/models/column.rb2
-rw-r--r--activerecord/test/models/column_name.rb2
-rw-r--r--activerecord/test/models/comment.rb53
-rw-r--r--activerecord/test/models/company.rb205
-rw-r--r--activerecord/test/models/company_in_module.rb52
-rw-r--r--activerecord/test/models/computer.rb4
-rw-r--r--activerecord/test/models/contact.rb12
-rw-r--r--activerecord/test/models/content.rb42
-rw-r--r--activerecord/test/models/contract.rb10
-rw-r--r--activerecord/test/models/country.rb6
-rw-r--r--activerecord/test/models/course.rb4
-rw-r--r--activerecord/test/models/customer.rb24
-rw-r--r--activerecord/test/models/customer_carrier.rb16
-rw-r--r--activerecord/test/models/dashboard.rb2
-rw-r--r--activerecord/test/models/default.rb2
-rw-r--r--activerecord/test/models/department.rb2
-rw-r--r--activerecord/test/models/developer.rb173
-rw-r--r--activerecord/test/models/dog.rb2
-rw-r--r--activerecord/test/models/dog_lover.rb2
-rw-r--r--activerecord/test/models/doubloon.rb4
-rw-r--r--activerecord/test/models/drink_designer.rb5
-rw-r--r--activerecord/test/models/edge.rb6
-rw-r--r--activerecord/test/models/electron.rb2
-rw-r--r--activerecord/test/models/engine.rb5
-rw-r--r--activerecord/test/models/entrant.rb2
-rw-r--r--activerecord/test/models/essay.rb9
-rw-r--r--activerecord/test/models/event.rb2
-rw-r--r--activerecord/test/models/eye.rb8
-rw-r--r--activerecord/test/models/face.rb19
-rw-r--r--activerecord/test/models/family.rb6
-rw-r--r--activerecord/test/models/family_tree.rb6
-rw-r--r--activerecord/test/models/friendship.rb8
-rw-r--r--activerecord/test/models/frog.rb8
-rw-r--r--activerecord/test/models/guid.rb2
-rw-r--r--activerecord/test/models/guitar.rb6
-rw-r--r--activerecord/test/models/hotel.rb10
-rw-r--r--activerecord/test/models/image.rb2
-rw-r--r--activerecord/test/models/interest.rb8
-rw-r--r--activerecord/test/models/invoice.rb6
-rw-r--r--activerecord/test/models/item.rb4
-rw-r--r--activerecord/test/models/job.rb8
-rw-r--r--activerecord/test/models/joke.rb6
-rw-r--r--activerecord/test/models/keyboard.rb4
-rw-r--r--activerecord/test/models/legacy_thing.rb2
-rw-r--r--activerecord/test/models/lesson.rb2
-rw-r--r--activerecord/test/models/line_item.rb4
-rw-r--r--activerecord/test/models/liquid.rb2
-rw-r--r--activerecord/test/models/man.rb19
-rw-r--r--activerecord/test/models/matey.rb4
-rw-r--r--activerecord/test/models/member.rb45
-rw-r--r--activerecord/test/models/member_detail.rb10
-rw-r--r--activerecord/test/models/member_type.rb2
-rw-r--r--activerecord/test/models/membership.rb20
-rw-r--r--activerecord/test/models/mentor.rb5
-rw-r--r--activerecord/test/models/minimalistic.rb2
-rw-r--r--activerecord/test/models/minivan.rb5
-rw-r--r--activerecord/test/models/mixed_case_monkey.rb2
-rw-r--r--activerecord/test/models/molecule.rb2
-rw-r--r--activerecord/test/models/movie.rb2
-rw-r--r--activerecord/test/models/node.rb6
-rw-r--r--activerecord/test/models/non_primary_key.rb4
-rw-r--r--activerecord/test/models/notification.rb3
-rw-r--r--activerecord/test/models/numeric_data.rb10
-rw-r--r--activerecord/test/models/order.rb6
-rw-r--r--activerecord/test/models/organization.rb16
-rw-r--r--activerecord/test/models/other_dog.rb7
-rw-r--r--activerecord/test/models/owner.rb13
-rw-r--r--activerecord/test/models/parrot.rb27
-rw-r--r--activerecord/test/models/person.rb110
-rw-r--r--activerecord/test/models/personal_legacy_thing.rb4
-rw-r--r--activerecord/test/models/pet.rb7
-rw-r--r--activerecord/test/models/pet_treasure.rb8
-rw-r--r--activerecord/test/models/pirate.rb92
-rw-r--r--activerecord/test/models/possession.rb4
-rw-r--r--activerecord/test/models/post.rb257
-rw-r--r--activerecord/test/models/price_estimate.rb12
-rw-r--r--activerecord/test/models/professor.rb7
-rw-r--r--activerecord/test/models/project.rb37
-rw-r--r--activerecord/test/models/publisher.rb2
-rw-r--r--activerecord/test/models/publisher/article.rb2
-rw-r--r--activerecord/test/models/publisher/magazine.rb2
-rw-r--r--activerecord/test/models/randomly_named_c1.rb8
-rw-r--r--activerecord/test/models/rating.rb4
-rw-r--r--activerecord/test/models/reader.rb12
-rw-r--r--activerecord/test/models/recipe.rb2
-rw-r--r--activerecord/test/models/record.rb2
-rw-r--r--activerecord/test/models/reference.rb8
-rw-r--r--activerecord/test/models/reply.rb34
-rw-r--r--activerecord/test/models/ship.rb29
-rw-r--r--activerecord/test/models/ship_part.rb6
-rw-r--r--activerecord/test/models/shop.rb6
-rw-r--r--activerecord/test/models/shop_account.rb8
-rw-r--r--activerecord/test/models/speedometer.rb2
-rw-r--r--activerecord/test/models/sponsor.rb13
-rw-r--r--activerecord/test/models/string_key_object.rb2
-rw-r--r--activerecord/test/models/student.rb2
-rw-r--r--activerecord/test/models/subject.rb16
-rw-r--r--activerecord/test/models/subscriber.rb6
-rw-r--r--activerecord/test/models/subscription.rb4
-rw-r--r--activerecord/test/models/tag.rb13
-rw-r--r--activerecord/test/models/tagging.rb19
-rw-r--r--activerecord/test/models/task.rb2
-rw-r--r--activerecord/test/models/topic.rb71
-rw-r--r--activerecord/test/models/toy.rb2
-rw-r--r--activerecord/test/models/traffic_light.rb2
-rw-r--r--activerecord/test/models/treasure.rb9
-rw-r--r--activerecord/test/models/treaty.rb6
-rw-r--r--activerecord/test/models/tree.rb2
-rw-r--r--activerecord/test/models/tuning_peg.rb6
-rw-r--r--activerecord/test/models/tyre.rb2
-rw-r--r--activerecord/test/models/user.rb12
-rw-r--r--activerecord/test/models/uuid_child.rb2
-rw-r--r--activerecord/test/models/uuid_item.rb8
-rw-r--r--activerecord/test/models/uuid_parent.rb2
-rw-r--r--activerecord/test/models/vegetables.rb7
-rw-r--r--activerecord/test/models/vehicle.rb9
-rw-r--r--activerecord/test/models/vertex.rb10
-rw-r--r--activerecord/test/models/warehouse_thing.rb2
-rw-r--r--activerecord/test/models/wheel.rb4
-rw-r--r--activerecord/test/models/without_table.rb4
-rw-r--r--activerecord/test/models/zine.rb4
-rw-r--r--activerecord/test/schema/mysql2_specific_schema.rb72
-rw-r--r--activerecord/test/schema/mysql_specific_schema.rb62
-rw-r--r--activerecord/test/schema/oracle_specific_schema.rb4
-rw-r--r--activerecord/test/schema/postgresql_specific_schema.rb89
-rw-r--r--activerecord/test/schema/schema.rb383
-rw-r--r--activerecord/test/schema/sqlite_specific_schema.rb27
-rw-r--r--activerecord/test/support/config.rb27
-rw-r--r--activerecord/test/support/connection.rb17
-rw-r--r--activerecord/test/support/connection_helper.rb2
-rw-r--r--activerecord/test/support/ddl_helper.rb2
-rw-r--r--activerecord/test/support/schema_dumping_helper.rb4
-rw-r--r--activestorage/.babelrc8
-rw-r--r--activestorage/.eslintrc19
-rw-r--r--activestorage/.gitignore6
-rw-r--r--activestorage/CHANGELOG.md123
-rw-r--r--activestorage/MIT-LICENSE20
-rw-r--r--activestorage/README.md161
-rw-r--r--activestorage/Rakefile17
-rw-r--r--activestorage/activestorage.gemspec35
-rw-r--r--activestorage/app/assets/javascripts/activestorage.js939
-rw-r--r--activestorage/app/controllers/active_storage/base_controller.rb8
-rw-r--r--activestorage/app/controllers/active_storage/blobs_controller.rb14
-rw-r--r--activestorage/app/controllers/active_storage/direct_uploads_controller.rb23
-rw-r--r--activestorage/app/controllers/active_storage/disk_controller.rb66
-rw-r--r--activestorage/app/controllers/active_storage/representations_controller.rb14
-rw-r--r--activestorage/app/controllers/concerns/active_storage/set_blob.rb16
-rw-r--r--activestorage/app/controllers/concerns/active_storage/set_current.rb15
-rw-r--r--activestorage/app/javascript/activestorage/blob_record.js68
-rw-r--r--activestorage/app/javascript/activestorage/blob_upload.js35
-rw-r--r--activestorage/app/javascript/activestorage/direct_upload.js48
-rw-r--r--activestorage/app/javascript/activestorage/direct_upload_controller.js67
-rw-r--r--activestorage/app/javascript/activestorage/direct_uploads_controller.js50
-rw-r--r--activestorage/app/javascript/activestorage/file_checksum.js53
-rw-r--r--activestorage/app/javascript/activestorage/helpers.js51
-rw-r--r--activestorage/app/javascript/activestorage/index.js11
-rw-r--r--activestorage/app/javascript/activestorage/ujs.js86
-rw-r--r--activestorage/app/jobs/active_storage/analyze_job.rb10
-rw-r--r--activestorage/app/jobs/active_storage/base_job.rb5
-rw-r--r--activestorage/app/jobs/active_storage/purge_job.rb11
-rw-r--r--activestorage/app/models/active_storage/attachment.rb48
-rw-r--r--activestorage/app/models/active_storage/blob.rb243
-rw-r--r--activestorage/app/models/active_storage/blob/analyzable.rb57
-rw-r--r--activestorage/app/models/active_storage/blob/identifiable.rb24
-rw-r--r--activestorage/app/models/active_storage/blob/representable.rb93
-rw-r--r--activestorage/app/models/active_storage/current.rb5
-rw-r--r--activestorage/app/models/active_storage/filename.rb77
-rw-r--r--activestorage/app/models/active_storage/preview.rb89
-rw-r--r--activestorage/app/models/active_storage/variant.rb131
-rw-r--r--activestorage/app/models/active_storage/variation.rb80
-rwxr-xr-xactivestorage/bin/test5
-rw-r--r--activestorage/config/routes.rb32
-rw-r--r--activestorage/db/migrate/20170806125915_create_active_storage_tables.rb26
-rw-r--r--activestorage/lib/active_storage.rb62
-rw-r--r--activestorage/lib/active_storage/analyzer.rb38
-rw-r--r--activestorage/lib/active_storage/analyzer/image_analyzer.rb45
-rw-r--r--activestorage/lib/active_storage/analyzer/null_analyzer.rb13
-rw-r--r--activestorage/lib/active_storage/analyzer/video_analyzer.rb118
-rw-r--r--activestorage/lib/active_storage/attached.rb25
-rw-r--r--activestorage/lib/active_storage/attached/changes.rb16
-rw-r--r--activestorage/lib/active_storage/attached/changes/create_many.rb46
-rw-r--r--activestorage/lib/active_storage/attached/changes/create_one.rb68
-rw-r--r--activestorage/lib/active_storage/attached/changes/create_one_of_many.rb10
-rw-r--r--activestorage/lib/active_storage/attached/changes/delete_many.rb23
-rw-r--r--activestorage/lib/active_storage/attached/changes/delete_one.rb19
-rw-r--r--activestorage/lib/active_storage/attached/many.rb65
-rw-r--r--activestorage/lib/active_storage/attached/model.rb140
-rw-r--r--activestorage/lib/active_storage/attached/one.rb79
-rw-r--r--activestorage/lib/active_storage/downloader.rb44
-rw-r--r--activestorage/lib/active_storage/downloading.rb47
-rw-r--r--activestorage/lib/active_storage/engine.rb110
-rw-r--r--activestorage/lib/active_storage/errors.rb26
-rw-r--r--activestorage/lib/active_storage/gem_version.rb17
-rw-r--r--activestorage/lib/active_storage/log_subscriber.rb58
-rw-r--r--activestorage/lib/active_storage/previewer.rb84
-rw-r--r--activestorage/lib/active_storage/previewer/mupdf_previewer.rb36
-rw-r--r--activestorage/lib/active_storage/previewer/poppler_pdf_previewer.rb35
-rw-r--r--activestorage/lib/active_storage/previewer/video_previewer.rb26
-rw-r--r--activestorage/lib/active_storage/reflection.rb64
-rw-r--r--activestorage/lib/active_storage/service.rb131
-rw-r--r--activestorage/lib/active_storage/service/azure_storage_service.rb167
-rw-r--r--activestorage/lib/active_storage/service/configurator.rb34
-rw-r--r--activestorage/lib/active_storage/service/disk_service.rb162
-rw-r--r--activestorage/lib/active_storage/service/gcs_service.rb143
-rw-r--r--activestorage/lib/active_storage/service/mirror_service.rb55
-rw-r--r--activestorage/lib/active_storage/service/s3_service.rb122
-rw-r--r--activestorage/lib/active_storage/transformers/image_processing_transformer.rb39
-rw-r--r--activestorage/lib/active_storage/transformers/mini_magick_transformer.rb38
-rw-r--r--activestorage/lib/active_storage/transformers/transformer.rb42
-rw-r--r--activestorage/lib/active_storage/version.rb10
-rw-r--r--activestorage/lib/tasks/activestorage.rake15
-rw-r--r--activestorage/package.json41
-rw-r--r--activestorage/rollup.config.js28
-rw-r--r--activestorage/test/analyzer/image_analyzer_test.rb32
-rw-r--r--activestorage/test/analyzer/video_analyzer_test.rb54
-rw-r--r--activestorage/test/controllers/blobs_controller_test.rb22
-rw-r--r--activestorage/test/controllers/direct_uploads_controller_test.rb147
-rw-r--r--activestorage/test/controllers/disk_controller_test.rb94
-rw-r--r--activestorage/test/controllers/representations_controller_test.rb61
-rw-r--r--activestorage/test/database/create_users_migration.rb9
-rw-r--r--activestorage/test/database/setup.rb7
-rw-r--r--activestorage/test/dummy/Rakefile5
-rw-r--r--activestorage/test/dummy/app/assets/config/manifest.js3
-rw-r--r--activestorage/test/dummy/app/assets/images/.keep (renamed from activerecord/test/migrations/empty/.gitkeep)0
-rw-r--r--activestorage/test/dummy/app/assets/javascripts/application.js13
-rw-r--r--activestorage/test/dummy/app/assets/stylesheets/application.css15
-rw-r--r--activestorage/test/dummy/app/controllers/application_controller.rb5
-rw-r--r--activestorage/test/dummy/app/controllers/concerns/.keep (renamed from railties/lib/rails/generators/rails/plugin/templates/app/mailers/.empty_directory)0
-rw-r--r--activestorage/test/dummy/app/helpers/application_helper.rb4
-rw-r--r--activestorage/test/dummy/app/jobs/application_job.rb4
-rw-r--r--activestorage/test/dummy/app/models/application_record.rb5
-rw-r--r--activestorage/test/dummy/app/models/concerns/.keep (renamed from railties/lib/rails/generators/rails/plugin/templates/app/models/.empty_directory)0
-rw-r--r--activestorage/test/dummy/app/views/layouts/application.html.erb14
-rwxr-xr-xactivestorage/test/dummy/bin/bundle5
-rwxr-xr-xactivestorage/test/dummy/bin/rails6
-rwxr-xr-xactivestorage/test/dummy/bin/rake6
-rwxr-xr-xactivestorage/test/dummy/bin/yarn13
-rw-r--r--activestorage/test/dummy/config.ru7
-rw-r--r--activestorage/test/dummy/config/application.rb22
-rw-r--r--activestorage/test/dummy/config/boot.rb7
-rw-r--r--activestorage/test/dummy/config/database.yml25
-rw-r--r--activestorage/test/dummy/config/environment.rb7
-rw-r--r--activestorage/test/dummy/config/environments/development.rb51
-rw-r--r--activestorage/test/dummy/config/environments/production.rb83
-rw-r--r--activestorage/test/dummy/config/environments/test.rb38
-rw-r--r--activestorage/test/dummy/config/initializers/application_controller_renderer.rb7
-rw-r--r--activestorage/test/dummy/config/initializers/assets.rb16
-rw-r--r--activestorage/test/dummy/config/initializers/backtrace_silencers.rb8
-rw-r--r--activestorage/test/dummy/config/initializers/cookies_serializer.rb7
-rw-r--r--activestorage/test/dummy/config/initializers/filter_parameter_logging.rb6
-rw-r--r--activestorage/test/dummy/config/initializers/inflections.rb17
-rw-r--r--activestorage/test/dummy/config/initializers/mime_types.rb5
-rw-r--r--activestorage/test/dummy/config/initializers/wrap_parameters.rb16
-rw-r--r--activestorage/test/dummy/config/routes.rb4
-rw-r--r--activestorage/test/dummy/config/secrets.yml32
-rw-r--r--activestorage/test/dummy/config/spring.rb8
-rw-r--r--activestorage/test/dummy/config/storage.yml3
-rw-r--r--activestorage/test/dummy/config/webpacker.yml72
-rw-r--r--activestorage/test/dummy/lib/assets/.keep0
-rw-r--r--activestorage/test/dummy/log/.keep0
-rw-r--r--activestorage/test/dummy/package.json5
-rw-r--r--activestorage/test/dummy/public/404.html67
-rw-r--r--activestorage/test/dummy/public/422.html67
-rw-r--r--activestorage/test/dummy/public/500.html66
-rw-r--r--activestorage/test/dummy/public/apple-touch-icon-precomposed.png0
-rw-r--r--activestorage/test/dummy/public/apple-touch-icon.png0
-rw-r--r--activestorage/test/dummy/public/favicon.ico0
-rw-r--r--activestorage/test/fixtures/files/empty_file.txt0
-rw-r--r--activestorage/test/fixtures/files/favicon.icobin0 -> 16958 bytes
-rw-r--r--activestorage/test/fixtures/files/icon.psdbin0 -> 37441 bytes
-rw-r--r--activestorage/test/fixtures/files/icon.svg13
-rw-r--r--activestorage/test/fixtures/files/image.gifbin0 -> 2032 bytes
-rw-r--r--activestorage/test/fixtures/files/racecar.jpgbin0 -> 1124062 bytes
-rw-r--r--activestorage/test/fixtures/files/racecar_rotated.jpgbin0 -> 1124060 bytes
-rw-r--r--activestorage/test/fixtures/files/report.pdfbin0 -> 13469 bytes
-rw-r--r--activestorage/test/fixtures/files/rotated_video.mp4bin0 -> 275090 bytes
-rw-r--r--activestorage/test/fixtures/files/video.mp4bin0 -> 275433 bytes
-rw-r--r--activestorage/test/fixtures/files/video_with_rectangular_samples.mp4bin0 -> 361535 bytes
-rw-r--r--activestorage/test/fixtures/files/video_with_undefined_display_aspect_ratio.mp4bin0 -> 128737 bytes
-rw-r--r--activestorage/test/fixtures/files/video_without_video_stream.mp4bin0 -> 16252 bytes
-rw-r--r--activestorage/test/jobs/purge_job_test.rb27
-rw-r--r--activestorage/test/models/attached/many_test.rb596
-rw-r--r--activestorage/test/models/attached/one_test.rb513
-rw-r--r--activestorage/test/models/blob_test.rb191
-rw-r--r--activestorage/test/models/filename_test.rb56
-rw-r--r--activestorage/test/models/presence_validation_test.rb30
-rw-r--r--activestorage/test/models/preview_test.rb40
-rw-r--r--activestorage/test/models/reflection_test.rb34
-rw-r--r--activestorage/test/models/representation_test.rb41
-rw-r--r--activestorage/test/models/variant_test.rb177
-rw-r--r--activestorage/test/previewer/mupdf_previewer_test.rb23
-rw-r--r--activestorage/test/previewer/poppler_pdf_previewer_test.rb23
-rw-r--r--activestorage/test/previewer/video_previewer_test.rb24
-rw-r--r--activestorage/test/service/azure_storage_service_test.rb39
-rw-r--r--activestorage/test/service/configurations.example.yml29
-rw-r--r--activestorage/test/service/configurations.yml.encbin0 -> 2848 bytes
-rw-r--r--activestorage/test/service/configurator_test.rb23
-rw-r--r--activestorage/test/service/disk_service_test.rb18
-rw-r--r--activestorage/test/service/gcs_service_test.rb55
-rw-r--r--activestorage/test/service/mirror_service_test.rb66
-rw-r--r--activestorage/test/service/s3_service_test.rb66
-rw-r--r--activestorage/test/service/shared_service_tests.rb130
-rw-r--r--activestorage/test/template/image_tag_test.rb44
-rw-r--r--activestorage/test/test_helper.rb103
-rw-r--r--activestorage/yarn.lock1923
-rw-r--r--activesupport/.gitignore1
-rw-r--r--activesupport/CHANGELOG.md380
-rw-r--r--activesupport/MIT-LICENSE4
-rw-r--r--activesupport/README.rdoc7
-rw-r--r--activesupport/Rakefile37
-rw-r--r--activesupport/activesupport.gemspec43
-rwxr-xr-xactivesupport/bin/generate_tables144
-rwxr-xr-xactivesupport/bin/test7
-rw-r--r--activesupport/lib/active_support.rb21
-rw-r--r--activesupport/lib/active_support/all.rb8
-rw-r--r--activesupport/lib/active_support/array_inquirer.rb16
-rw-r--r--activesupport/lib/active_support/backtrace_cleaner.rb33
-rw-r--r--activesupport/lib/active_support/benchmarkable.rb10
-rw-r--r--activesupport/lib/active_support/builder.rb4
-rw-r--r--activesupport/lib/active_support/cache.rb477
-rw-r--r--activesupport/lib/active_support/cache/file_store.rb96
-rw-r--r--activesupport/lib/active_support/cache/mem_cache_store.rb184
-rw-r--r--activesupport/lib/active_support/cache/memory_store.rb64
-rw-r--r--activesupport/lib/active_support/cache/null_store.rb18
-rw-r--r--activesupport/lib/active_support/cache/redis_cache_store.rb485
-rw-r--r--activesupport/lib/active_support/cache/strategy/local_cache.rb99
-rw-r--r--activesupport/lib/active_support/cache/strategy/local_cache_middleware.rb19
-rw-r--r--activesupport/lib/active_support/callbacks.rb1259
-rw-r--r--activesupport/lib/active_support/concern.rb4
-rw-r--r--activesupport/lib/active_support/concurrency/latch.rb27
-rw-r--r--activesupport/lib/active_support/concurrency/load_interlock_aware_monitor.rb17
-rw-r--r--activesupport/lib/active_support/concurrency/share_lock.rb175
-rw-r--r--activesupport/lib/active_support/configurable.rb20
-rw-r--r--activesupport/lib/active_support/core_ext.rb5
-rw-r--r--activesupport/lib/active_support/core_ext/array.rb17
-rw-r--r--activesupport/lib/active_support/core_ext/array/access.rb22
-rw-r--r--activesupport/lib/active_support/core_ext/array/conversions.rb38
-rw-r--r--activesupport/lib/active_support/core_ext/array/extract.rb21
-rw-r--r--activesupport/lib/active_support/core_ext/array/extract_options.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/array/grouping.rb29
-rw-r--r--activesupport/lib/active_support/core_ext/array/inquiry.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/array/prepend_and_append.rb8
-rw-r--r--activesupport/lib/active_support/core_ext/array/wrap.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/benchmark.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/big_decimal.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/big_decimal/conversions.rb19
-rw-r--r--activesupport/lib/active_support/core_ext/class.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/class/attribute.rb59
-rw-r--r--activesupport/lib/active_support/core_ext/class/attribute_accessors.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/class/subclasses.rb28
-rw-r--r--activesupport/lib/active_support/core_ext/date.rb10
-rw-r--r--activesupport/lib/active_support/core_ext/date/acts_like.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/date/blank.rb14
-rw-r--r--activesupport/lib/active_support/core_ext/date/calculations.rb20
-rw-r--r--activesupport/lib/active_support/core_ext/date/conversions.rb48
-rw-r--r--activesupport/lib/active_support/core_ext/date/zones.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/date_and_time/calculations.rb182
-rw-r--r--activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb16
-rw-r--r--activesupport/lib/active_support/core_ext/date_and_time/zones.rb24
-rw-r--r--activesupport/lib/active_support/core_ext/date_time.rb11
-rw-r--r--activesupport/lib/active_support/core_ext/date_time/acts_like.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/date_time/blank.rb14
-rw-r--r--activesupport/lib/active_support/core_ext/date_time/calculations.rb72
-rw-r--r--activesupport/lib/active_support/core_ext/date_time/compatibility.rb18
-rw-r--r--activesupport/lib/active_support/core_ext/date_time/conversions.rb28
-rw-r--r--activesupport/lib/active_support/core_ext/date_time/zones.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/digest/uuid.rb12
-rw-r--r--activesupport/lib/active_support/core_ext/enumerable.rb92
-rw-r--r--activesupport/lib/active_support/core_ext/file.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/file/atomic.rb66
-rw-r--r--activesupport/lib/active_support/core_ext/hash.rb18
-rw-r--r--activesupport/lib/active_support/core_ext/hash/compact.rb23
-rw-r--r--activesupport/lib/active_support/core_ext/hash/conversions.rb86
-rw-r--r--activesupport/lib/active_support/core_ext/hash/deep_merge.rb20
-rw-r--r--activesupport/lib/active_support/core_ext/hash/except.rb19
-rw-r--r--activesupport/lib/active_support/core_ext/hash/indifferent_access.rb7
-rw-r--r--activesupport/lib/active_support/core_ext/hash/keys.rb40
-rw-r--r--activesupport/lib/active_support/core_ext/hash/reverse_merge.rb7
-rw-r--r--activesupport/lib/active_support/core_ext/hash/slice.rb16
-rw-r--r--activesupport/lib/active_support/core_ext/hash/transform_values.rb26
-rw-r--r--activesupport/lib/active_support/core_ext/integer.rb8
-rw-r--r--activesupport/lib/active_support/core_ext/integer/inflections.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/integer/multiple.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/integer/time.rb29
-rw-r--r--activesupport/lib/active_support/core_ext/kernel.rb10
-rw-r--r--activesupport/lib/active_support/core_ext/kernel/agnostics.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/kernel/concern.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/kernel/debugger.rb3
-rw-r--r--activesupport/lib/active_support/core_ext/kernel/reporting.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/kernel/singleton_class.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/load_error.rb25
-rw-r--r--activesupport/lib/active_support/core_ext/marshal.rb15
-rw-r--r--activesupport/lib/active_support/core_ext/module.rb25
-rw-r--r--activesupport/lib/active_support/core_ext/module/aliasing.rb53
-rw-r--r--activesupport/lib/active_support/core_ext/module/anonymous.rb8
-rw-r--r--activesupport/lib/active_support/core_ext/module/attr_internal.rb10
-rw-r--r--activesupport/lib/active_support/core_ext/module/attribute_accessors.rb80
-rw-r--r--activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb144
-rw-r--r--activesupport/lib/active_support/core_ext/module/concerning.rb17
-rw-r--r--activesupport/lib/active_support/core_ext/module/delegation.rb148
-rw-r--r--activesupport/lib/active_support/core_ext/module/deprecation.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/module/introspection.rb62
-rw-r--r--activesupport/lib/active_support/core_ext/module/method_transplanting.rb3
-rw-r--r--activesupport/lib/active_support/core_ext/module/qualified_const.rb52
-rw-r--r--activesupport/lib/active_support/core_ext/module/reachable.rb7
-rw-r--r--activesupport/lib/active_support/core_ext/module/redefine_method.rb40
-rw-r--r--activesupport/lib/active_support/core_ext/module/remove_method.rb12
-rw-r--r--activesupport/lib/active_support/core_ext/name_error.rb7
-rw-r--r--activesupport/lib/active_support/core_ext/numeric.rb9
-rw-r--r--activesupport/lib/active_support/core_ext/numeric/bytes.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/numeric/conversions.rb267
-rw-r--r--activesupport/lib/active_support/core_ext/numeric/inquiry.rb27
-rw-r--r--activesupport/lib/active_support/core_ext/numeric/time.rb52
-rw-r--r--activesupport/lib/active_support/core_ext/object.rb26
-rw-r--r--activesupport/lib/active_support/core_ext/object/acts_like.rb13
-rw-r--r--activesupport/lib/active_support/core_ext/object/blank.rb32
-rw-r--r--activesupport/lib/active_support/core_ext/object/conversions.rb10
-rw-r--r--activesupport/lib/active_support/core_ext/object/deep_dup.rb16
-rw-r--r--activesupport/lib/active_support/core_ext/object/duplicable.rb133
-rw-r--r--activesupport/lib/active_support/core_ext/object/inclusion.rb8
-rw-r--r--activesupport/lib/active_support/core_ext/object/instance_variables.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/object/json.rb55
-rw-r--r--activesupport/lib/active_support/core_ext/object/to_param.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/object/to_query.rb15
-rw-r--r--activesupport/lib/active_support/core_ext/object/try.rb32
-rw-r--r--activesupport/lib/active_support/core_ext/object/with_options.rb17
-rw-r--r--activesupport/lib/active_support/core_ext/range.rb11
-rw-r--r--activesupport/lib/active_support/core_ext/range/compare_range.rb61
-rw-r--r--activesupport/lib/active_support/core_ext/range/conversions.rb67
-rw-r--r--activesupport/lib/active_support/core_ext/range/each.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/range/include_range.rb30
-rw-r--r--activesupport/lib/active_support/core_ext/range/include_time_with_zone.rb23
-rw-r--r--activesupport/lib/active_support/core_ext/range/overlaps.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/regexp.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/securerandom.rb12
-rw-r--r--activesupport/lib/active_support/core_ext/string.rb28
-rw-r--r--activesupport/lib/active_support/core_ext/string/access.rb24
-rw-r--r--activesupport/lib/active_support/core_ext/string/behavior.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/string/conversions.rb13
-rw-r--r--activesupport/lib/active_support/core_ext/string/exclude.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/string/filters.rb49
-rw-r--r--activesupport/lib/active_support/core_ext/string/indent.rb10
-rw-r--r--activesupport/lib/active_support/core_ext/string/inflections.rb83
-rw-r--r--activesupport/lib/active_support/core_ext/string/inquiry.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/string/multibyte.rb15
-rw-r--r--activesupport/lib/active_support/core_ext/string/output_safety.rb82
-rw-r--r--activesupport/lib/active_support/core_ext/string/starts_ends_with.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/string/strip.rb11
-rw-r--r--activesupport/lib/active_support/core_ext/string/zones.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/struct.rb3
-rw-r--r--activesupport/lib/active_support/core_ext/time.rb11
-rw-r--r--activesupport/lib/active_support/core_ext/time/acts_like.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/time/calculations.rb140
-rw-r--r--activesupport/lib/active_support/core_ext/time/compatibility.rb16
-rw-r--r--activesupport/lib/active_support/core_ext/time/conversions.rb33
-rw-r--r--activesupport/lib/active_support/core_ext/time/marshal.rb3
-rw-r--r--activesupport/lib/active_support/core_ext/time/zones.rb31
-rw-r--r--activesupport/lib/active_support/core_ext/uri.rb13
-rw-r--r--activesupport/lib/active_support/current_attributes.rb197
-rw-r--r--activesupport/lib/active_support/dependencies.rb264
-rw-r--r--activesupport/lib/active_support/dependencies/autoload.rb2
-rw-r--r--activesupport/lib/active_support/dependencies/interlock.rb34
-rw-r--r--activesupport/lib/active_support/deprecation.rb21
-rw-r--r--activesupport/lib/active_support/deprecation/behaviors.rb43
-rw-r--r--activesupport/lib/active_support/deprecation/constant_accessor.rb52
-rw-r--r--activesupport/lib/active_support/deprecation/instance_delegator.rb19
-rw-r--r--activesupport/lib/active_support/deprecation/method_wrappers.rb79
-rw-r--r--activesupport/lib/active_support/deprecation/proxy_wrappers.rb19
-rw-r--r--activesupport/lib/active_support/deprecation/reporting.rb44
-rw-r--r--activesupport/lib/active_support/descendants_tracker.rb2
-rw-r--r--activesupport/lib/active_support/digest.rb20
-rw-r--r--activesupport/lib/active_support/duration.rb335
-rw-r--r--activesupport/lib/active_support/duration/iso8601_parser.rb124
-rw-r--r--activesupport/lib/active_support/duration/iso8601_serializer.rb54
-rw-r--r--activesupport/lib/active_support/encrypted_configuration.rb45
-rw-r--r--activesupport/lib/active_support/encrypted_file.rb99
-rw-r--r--activesupport/lib/active_support/evented_file_update_checker.rb205
-rw-r--r--activesupport/lib/active_support/execution_wrapper.rb129
-rw-r--r--activesupport/lib/active_support/executor.rb8
-rw-r--r--activesupport/lib/active_support/file_update_checker.rb100
-rw-r--r--activesupport/lib/active_support/gem_version.rb6
-rw-r--r--activesupport/lib/active_support/gzip.rb12
-rw-r--r--activesupport/lib/active_support/hash_with_indifferent_access.rb148
-rw-r--r--activesupport/lib/active_support/i18n.rb15
-rw-r--r--activesupport/lib/active_support/i18n_railtie.rb61
-rw-r--r--activesupport/lib/active_support/inflections.rb24
-rw-r--r--activesupport/lib/active_support/inflector.rb12
-rw-r--r--activesupport/lib/active_support/inflector/inflections.rb72
-rw-r--r--activesupport/lib/active_support/inflector/methods.rb183
-rw-r--r--activesupport/lib/active_support/inflector/transliterate.rb65
-rw-r--r--activesupport/lib/active_support/json.rb6
-rw-r--r--activesupport/lib/active_support/json/decoding.rb19
-rw-r--r--activesupport/lib/active_support/json/encoding.rb27
-rw-r--r--activesupport/lib/active_support/key_generator.rb50
-rw-r--r--activesupport/lib/active_support/lazy_load_hooks.rb74
-rw-r--r--activesupport/lib/active_support/locale/en.rb31
-rw-r--r--activesupport/lib/active_support/locale/en.yml2
-rw-r--r--activesupport/lib/active_support/log_subscriber.rb23
-rw-r--r--activesupport/lib/active_support/log_subscriber/test_helper.rb26
-rw-r--r--activesupport/lib/active_support/logger.rb42
-rw-r--r--activesupport/lib/active_support/logger_silence.rb49
-rw-r--r--activesupport/lib/active_support/logger_thread_safe_level.rb55
-rw-r--r--activesupport/lib/active_support/message_encryptor.rb222
-rw-r--r--activesupport/lib/active_support/message_verifier.rb105
-rw-r--r--activesupport/lib/active_support/messages/metadata.rb71
-rw-r--r--activesupport/lib/active_support/messages/rotation_configuration.rb22
-rw-r--r--activesupport/lib/active_support/messages/rotator.rb56
-rw-r--r--activesupport/lib/active_support/multibyte.rb6
-rw-r--r--activesupport/lib/active_support/multibyte/chars.rb112
-rw-r--r--activesupport/lib/active_support/multibyte/unicode.rb342
-rw-r--r--activesupport/lib/active_support/notifications.rb40
-rw-r--r--activesupport/lib/active_support/notifications/fanout.rb60
-rw-r--r--activesupport/lib/active_support/notifications/instrumenter.rb88
-rw-r--r--activesupport/lib/active_support/number_helper.rb155
-rw-r--r--activesupport/lib/active_support/number_helper/number_converter.rb24
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_currency_converter.rb14
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb14
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_human_converter.rb20
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb19
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb6
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_phone_converter.rb19
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb66
-rw-r--r--activesupport/lib/active_support/number_helper/rounding_helper.rb66
-rw-r--r--activesupport/lib/active_support/option_merger.rb4
-rw-r--r--activesupport/lib/active_support/ordered_hash.rb10
-rw-r--r--activesupport/lib/active_support/ordered_options.rb16
-rw-r--r--activesupport/lib/active_support/parameter_filter.rb124
-rw-r--r--activesupport/lib/active_support/per_thread_registry.rb19
-rw-r--r--activesupport/lib/active_support/proxy_object.rb2
-rw-r--r--activesupport/lib/active_support/rails.rb14
-rw-r--r--activesupport/lib/active_support/railtie.rb49
-rw-r--r--activesupport/lib/active_support/reloader.rb132
-rw-r--r--activesupport/lib/active_support/rescuable.rb157
-rw-r--r--activesupport/lib/active_support/security_utils.rb23
-rw-r--r--activesupport/lib/active_support/string_inquirer.rb12
-rw-r--r--activesupport/lib/active_support/subscriber.rb44
-rw-r--r--activesupport/lib/active_support/tagged_logging.rb36
-rw-r--r--activesupport/lib/active_support/test_case.rb124
-rw-r--r--activesupport/lib/active_support/testing/assertions.rb149
-rw-r--r--activesupport/lib/active_support/testing/autorun.rb15
-rw-r--r--activesupport/lib/active_support/testing/composite_filter.rb54
-rw-r--r--activesupport/lib/active_support/testing/constant_lookup.rb3
-rw-r--r--activesupport/lib/active_support/testing/declarative.rb4
-rw-r--r--activesupport/lib/active_support/testing/deprecation.rb23
-rw-r--r--activesupport/lib/active_support/testing/file_fixtures.rb6
-rw-r--r--activesupport/lib/active_support/testing/isolation.rb59
-rw-r--r--activesupport/lib/active_support/testing/method_call_assertions.rb45
-rw-r--r--activesupport/lib/active_support/testing/parallelization.rb110
-rw-r--r--activesupport/lib/active_support/testing/setup_and_teardown.rb21
-rw-r--r--activesupport/lib/active_support/testing/stream.rb58
-rw-r--r--activesupport/lib/active_support/testing/tagged_logging.rb4
-rw-r--r--activesupport/lib/active_support/testing/time_helpers.rb104
-rw-r--r--activesupport/lib/active_support/time.rb26
-rw-r--r--activesupport/lib/active_support/time_with_zone.rb192
-rw-r--r--activesupport/lib/active_support/values/time_zone.rb219
-rw-r--r--activesupport/lib/active_support/values/unicode_tables.datbin1001806 -> 0 bytes
-rw-r--r--activesupport/lib/active_support/version.rb4
-rw-r--r--activesupport/lib/active_support/xml_mini.rb113
-rw-r--r--activesupport/lib/active_support/xml_mini/jdom.rb228
-rw-r--r--activesupport/lib/active_support/xml_mini/libxml.rb29
-rw-r--r--activesupport/lib/active_support/xml_mini/libxmlsax.rb34
-rw-r--r--activesupport/lib/active_support/xml_mini/nokogiri.rb26
-rw-r--r--activesupport/lib/active_support/xml_mini/nokogirisax.rb31
-rw-r--r--activesupport/lib/active_support/xml_mini/rexml.rb24
-rw-r--r--activesupport/test/abstract_unit.rb51
-rw-r--r--activesupport/test/array_inquirer_test.rb41
-rw-r--r--activesupport/test/autoload_test.rb18
-rw-r--r--activesupport/test/autoloading_fixtures/a/b.rb4
-rw-r--r--activesupport/test/autoloading_fixtures/a/c/d.rb4
-rw-r--r--activesupport/test/autoloading_fixtures/a/c/em/f.rb4
-rw-r--r--activesupport/test/autoloading_fixtures/application.rb2
-rw-r--r--activesupport/test/autoloading_fixtures/circular1.rb2
-rw-r--r--activesupport/test/autoloading_fixtures/circular2.rb2
-rw-r--r--activesupport/test/autoloading_fixtures/class_folder.rb4
-rw-r--r--activesupport/test/autoloading_fixtures/class_folder/class_folder_subclass.rb4
-rw-r--r--activesupport/test/autoloading_fixtures/class_folder/inline_class.rb2
-rw-r--r--activesupport/test/autoloading_fixtures/class_folder/nested_class.rb2
-rw-r--r--activesupport/test/autoloading_fixtures/conflict.rb4
-rw-r--r--activesupport/test/autoloading_fixtures/counting_loader.rb2
-rw-r--r--activesupport/test/autoloading_fixtures/cross_site_dependency.rb4
-rw-r--r--activesupport/test/autoloading_fixtures/d.rb4
-rw-r--r--activesupport/test/autoloading_fixtures/em.rb4
-rw-r--r--activesupport/test/autoloading_fixtures/html/some_class.rb2
-rw-r--r--activesupport/test/autoloading_fixtures/load_path/loaded_constant.rb3
-rw-r--r--activesupport/test/autoloading_fixtures/loads_constant.rb2
-rw-r--r--activesupport/test/autoloading_fixtures/module_folder/inline_class.rb2
-rw-r--r--activesupport/test/autoloading_fixtures/module_folder/nested_class.rb2
-rw-r--r--activesupport/test/autoloading_fixtures/module_folder/nested_sibling.rb4
-rw-r--r--activesupport/test/autoloading_fixtures/module_folder/nested_with_require.rb8
-rw-r--r--activesupport/test/autoloading_fixtures/module_with_custom_const_missing/a/b.rb4
-rw-r--r--activesupport/test/autoloading_fixtures/multiple_constant_file.rb2
-rw-r--r--activesupport/test/autoloading_fixtures/nested_with_require_parent.rb5
-rw-r--r--activesupport/test/autoloading_fixtures/prepend.rb10
-rw-r--r--activesupport/test/autoloading_fixtures/prepend/sub_class_conflict.rb4
-rw-r--r--activesupport/test/autoloading_fixtures/raises_arbitrary_exception.rb6
-rw-r--r--activesupport/test/autoloading_fixtures/raises_name_error.rb2
-rw-r--r--activesupport/test/autoloading_fixtures/raises_no_method_error.rb2
-rw-r--r--activesupport/test/autoloading_fixtures/requires_constant.rb3
-rw-r--r--activesupport/test/autoloading_fixtures/should_not_be_required.rb2
-rw-r--r--activesupport/test/autoloading_fixtures/throws.rb6
-rw-r--r--activesupport/test/autoloading_fixtures/typo.rb3
-rw-r--r--activesupport/test/benchmarkable_test.rb32
-rw-r--r--activesupport/test/broadcast_logger_test.rb171
-rw-r--r--activesupport/test/cache/behaviors.rb12
-rw-r--r--activesupport/test/cache/behaviors/autoloading_cache_behavior.rb43
-rw-r--r--activesupport/test/cache/behaviors/cache_delete_matched_behavior.rb15
-rw-r--r--activesupport/test/cache/behaviors/cache_increment_decrement_behavior.rb27
-rw-r--r--activesupport/test/cache/behaviors/cache_instrumentation_behavior.rb60
-rw-r--r--activesupport/test/cache/behaviors/cache_store_behavior.rb537
-rw-r--r--activesupport/test/cache/behaviors/cache_store_version_behavior.rb88
-rw-r--r--activesupport/test/cache/behaviors/connection_pool_behavior.rb60
-rw-r--r--activesupport/test/cache/behaviors/encoded_key_cache_behavior.rb36
-rw-r--r--activesupport/test/cache/behaviors/failure_safety_behavior.rb91
-rw-r--r--activesupport/test/cache/behaviors/local_cache_behavior.rb154
-rw-r--r--activesupport/test/cache/cache_entry_test.rb16
-rw-r--r--activesupport/test/cache/cache_key_test.rb90
-rw-r--r--activesupport/test/cache/cache_store_logger_test.rb36
-rw-r--r--activesupport/test/cache/cache_store_namespace_test.rb40
-rw-r--r--activesupport/test/cache/cache_store_setting_test.rb68
-rw-r--r--activesupport/test/cache/local_cache_middleware_test.rb63
-rw-r--r--activesupport/test/cache/stores/file_store_test.rb134
-rw-r--r--activesupport/test/cache/stores/mem_cache_store_test.rb139
-rw-r--r--activesupport/test/cache/stores/memory_store_test.rb116
-rw-r--r--activesupport/test/cache/stores/null_store_test.rb59
-rw-r--r--activesupport/test/cache/stores/redis_cache_store_test.rb280
-rw-r--r--activesupport/test/caching_test.rb1095
-rw-r--r--activesupport/test/callback_inheritance_test.rb28
-rw-r--r--activesupport/test/callbacks_test.rb340
-rw-r--r--activesupport/test/class_cache_test.rb30
-rw-r--r--activesupport/test/clean_backtrace_test.rb59
-rw-r--r--activesupport/test/clean_logger_test.rb12
-rw-r--r--activesupport/test/concern_test.rb16
-rw-r--r--activesupport/test/concurrency/load_interlock_aware_monitor_test.rb55
-rw-r--r--activesupport/test/configurable_test.rb20
-rw-r--r--activesupport/test/constantize_test_cases.rb13
-rw-r--r--activesupport/test/core_ext/array/access_test.rb8
-rw-r--r--activesupport/test/core_ext/array/conversions_test.rb116
-rw-r--r--activesupport/test/core_ext/array/extract_options_test.rb8
-rw-r--r--activesupport/test/core_ext/array/extract_test.rb44
-rw-r--r--activesupport/test/core_ext/array/grouping_test.rb40
-rw-r--r--activesupport/test/core_ext/array/prepend_append_test.rb6
-rw-r--r--activesupport/test/core_ext/array/wrap_test.rb6
-rw-r--r--activesupport/test/core_ext/bigdecimal_test.rb12
-rw-r--r--activesupport/test/core_ext/class/attribute_test.rb56
-rw-r--r--activesupport/test/core_ext/class_test.rb18
-rw-r--r--activesupport/test/core_ext/date_and_time_behavior.rb382
-rw-r--r--activesupport/test/core_ext/date_and_time_compatibility_test.rb275
-rw-r--r--activesupport/test/core_ext/date_ext_test.rb239
-rw-r--r--activesupport/test/core_ext/date_time_ext_test.rb328
-rw-r--r--activesupport/test/core_ext/digest/uuid_test.rb8
-rw-r--r--activesupport/test/core_ext/duration_test.rb543
-rw-r--r--activesupport/test/core_ext/enumerable_test.rb159
-rw-r--r--activesupport/test/core_ext/file_test.rb12
-rw-r--r--activesupport/test/core_ext/hash/transform_keys_test.rb54
-rw-r--r--activesupport/test/core_ext/hash/transform_values_test.rb67
-rw-r--r--activesupport/test/core_ext/hash_ext_test.rb1197
-rw-r--r--activesupport/test/core_ext/integer_ext_test.rb20
-rw-r--r--activesupport/test/core_ext/kernel/concern_test.rb8
-rw-r--r--activesupport/test/core_ext/kernel_test.rb22
-rw-r--r--activesupport/test/core_ext/load_error_test.rb28
-rw-r--r--activesupport/test/core_ext/marshal_test.rb57
-rw-r--r--activesupport/test/core_ext/module/anonymous_test.rb16
-rw-r--r--activesupport/test/core_ext/module/attr_internal_test.rb32
-rw-r--r--activesupport/test/core_ext/module/attribute_accessor_per_thread_test.rb133
-rw-r--r--activesupport/test/core_ext/module/attribute_accessor_test.rb88
-rw-r--r--activesupport/test/core_ext/module/attribute_aliasing_test.rb24
-rw-r--r--activesupport/test/core_ext/module/concerning_test.rb24
-rw-r--r--activesupport/test/core_ext/module/introspection_test.rb57
-rw-r--r--activesupport/test/core_ext/module/qualified_const_test.rb108
-rw-r--r--activesupport/test/core_ext/module/reachable_test.rb36
-rw-r--r--activesupport/test/core_ext/module/remove_method_test.rb56
-rw-r--r--activesupport/test/core_ext/module_test.rb451
-rw-r--r--activesupport/test/core_ext/name_error_test.rb8
-rw-r--r--activesupport/test/core_ext/numeric_ext_test.rb522
-rw-r--r--activesupport/test/core_ext/object/acts_like_test.rb16
-rw-r--r--activesupport/test/core_ext/object/blank_test.rb11
-rw-r--r--activesupport/test/core_ext/object/deep_dup_test.rb36
-rw-r--r--activesupport/test/core_ext/object/duplicable_test.rb22
-rw-r--r--activesupport/test/core_ext/object/inclusion_test.rb28
-rw-r--r--activesupport/test/core_ext/object/instance_variables_test.rb20
-rw-r--r--activesupport/test/core_ext/object/json_cherry_pick_test.rb10
-rw-r--r--activesupport/test/core_ext/object/json_gem_encoding_test.rb14
-rw-r--r--activesupport/test/core_ext/object/to_param_test.rb14
-rw-r--r--activesupport/test/core_ext/object/to_query_test.rb82
-rw-r--r--activesupport/test/core_ext/object/try_test.rb51
-rw-r--r--activesupport/test/core_ext/range_ext_test.rb54
-rw-r--r--activesupport/test/core_ext/regexp_ext_test.rb6
-rw-r--r--activesupport/test/core_ext/secure_random_test.rb6
-rw-r--r--activesupport/test/core_ext/string_ext_test.rb448
-rw-r--r--activesupport/test/core_ext/time_ext_test.rb917
-rw-r--r--activesupport/test/core_ext/time_with_zone_test.rb761
-rw-r--r--activesupport/test/core_ext/uri_ext_test.rb10
-rw-r--r--activesupport/test/current_attributes_test.rb105
-rw-r--r--activesupport/test/dependencies/check_warnings.rb2
-rw-r--r--activesupport/test/dependencies/conflict.rb4
-rw-r--r--activesupport/test/dependencies/cross_site_depender.rb4
-rw-r--r--activesupport/test/dependencies/module_folder/lib_class.rb8
-rw-r--r--activesupport/test/dependencies/mutual_one.rb8
-rw-r--r--activesupport/test/dependencies/mutual_two.rb8
-rw-r--r--activesupport/test/dependencies/raises_exception.rb4
-rw-r--r--activesupport/test/dependencies/raises_exception_without_blame_file.rb4
-rw-r--r--activesupport/test/dependencies/requires_nonexistent0.rb4
-rw-r--r--activesupport/test/dependencies/requires_nonexistent1.rb4
-rw-r--r--activesupport/test/dependencies/service_one.rb4
-rw-r--r--activesupport/test/dependencies/service_two.rb4
-rw-r--r--activesupport/test/dependencies_test.rb413
-rw-r--r--activesupport/test/dependencies_test_helpers.rb8
-rw-r--r--activesupport/test/deprecation/method_wrappers_test.rb93
-rw-r--r--activesupport/test/deprecation/proxy_wrappers_test.rb12
-rw-r--r--activesupport/test/deprecation_test.rb148
-rw-r--r--activesupport/test/descendants_tracker_test_cases.rb42
-rw-r--r--activesupport/test/descendants_tracker_with_autoloading_test.rb12
-rw-r--r--activesupport/test/descendants_tracker_without_autoloading_test.rb10
-rw-r--r--activesupport/test/digest_test.rb27
-rw-r--r--activesupport/test/encrypted_configuration_test.rb73
-rw-r--r--activesupport/test/encrypted_file_test.rb59
-rw-r--r--activesupport/test/evented_file_update_checker_test.rb197
-rw-r--r--activesupport/test/executor_test.rb242
-rw-r--r--activesupport/test/file_update_checker_shared_tests.rb284
-rw-r--r--activesupport/test/file_update_checker_test.rb115
-rw-r--r--activesupport/test/fixtures/autoload/another_class.rb4
-rw-r--r--activesupport/test/fixtures/autoload/some_class.rb4
-rw-r--r--activesupport/test/gzip_test.rb24
-rw-r--r--activesupport/test/hash_with_indifferent_access_test.rb831
-rw-r--r--activesupport/test/i18n_test.rb46
-rw-r--r--activesupport/test/inflector_test.rb158
-rw-r--r--activesupport/test/inflector_test_cases.rb106
-rw-r--r--activesupport/test/json/decoding_test.rb130
-rw-r--r--activesupport/test/json/encoding_test.rb225
-rw-r--r--activesupport/test/json/encoding_test_cases.rb56
-rw-r--r--activesupport/test/key_generator_test.rb96
-rw-r--r--activesupport/test/lazy_load_hooks_test.rb101
-rw-r--r--activesupport/test/load_paths_test.rb16
-rw-r--r--activesupport/test/log_subscriber_test.rb24
-rw-r--r--activesupport/test/logger_test.rb196
-rw-r--r--activesupport/test/message_encryptor_test.rb198
-rw-r--r--activesupport/test/message_verifier_test.rb150
-rw-r--r--activesupport/test/messages/rotation_configuration_test.rb25
-rw-r--r--activesupport/test/metadata/shared_metadata_tests.rb88
-rw-r--r--activesupport/test/multibyte_chars_test.rb550
-rw-r--r--activesupport/test/multibyte_conformance_test.rb147
-rw-r--r--activesupport/test/multibyte_grapheme_break_conformance_test.rb60
-rw-r--r--activesupport/test/multibyte_normalization_conformance_test.rb116
-rw-r--r--activesupport/test/multibyte_proxy_test.rb5
-rw-r--r--activesupport/test/multibyte_test_helpers.rb37
-rw-r--r--activesupport/test/multibyte_unicode_database_test.rb26
-rw-r--r--activesupport/test/notifications/evented_notification_test.rb54
-rw-r--r--activesupport/test/notifications/instrumenter_test.rb16
-rw-r--r--activesupport/test/notifications_test.rb115
-rw-r--r--activesupport/test/number_helper_i18n_test.rb155
-rw-r--r--activesupport/test/number_helper_test.rb415
-rw-r--r--activesupport/test/option_merger_test.rb34
-rw-r--r--activesupport/test/ordered_hash_test.rb98
-rw-r--r--activesupport/test/ordered_options_test.rb29
-rw-r--r--activesupport/test/parameter_filter_test.rb105
-rw-r--r--activesupport/test/reloader_test.rb99
-rw-r--r--activesupport/test/rescuable_test.rb86
-rw-r--r--activesupport/test/safe_buffer_test.rb147
-rw-r--r--activesupport/test/security_utils_test.rb21
-rw-r--r--activesupport/test/share_lock_test.rb580
-rw-r--r--activesupport/test/silence_logger_test.rb35
-rw-r--r--activesupport/test/string_inquirer_test.rb31
-rw-r--r--activesupport/test/subscriber_test.rb12
-rw-r--r--activesupport/test/tagged_logging_test.rb62
-rw-r--r--activesupport/test/test_case_test.rb233
-rw-r--r--activesupport/test/testing/after_teardown_test.rb36
-rw-r--r--activesupport/test/testing/constant_lookup_test.rb8
-rw-r--r--activesupport/test/testing/file_fixtures_test.rb14
-rw-r--r--activesupport/test/testing/method_call_assertions_test.rb137
-rw-r--r--activesupport/test/time_travel_test.rb153
-rw-r--r--activesupport/test/time_zone_test.rb616
-rw-r--r--activesupport/test/time_zone_test_helpers.rb29
-rw-r--r--activesupport/test/transliterate_test.rb28
-rw-r--r--activesupport/test/xml_mini/jdom_engine_test.rb167
-rw-r--r--activesupport/test/xml_mini/libxml_engine_test.rb211
-rw-r--r--activesupport/test/xml_mini/libxmlsax_engine_test.rb201
-rw-r--r--activesupport/test/xml_mini/nokogiri_engine_test.rb221
-rw-r--r--activesupport/test/xml_mini/nokogirisax_engine_test.rb222
-rw-r--r--activesupport/test/xml_mini/rexml_engine_test.rb38
-rw-r--r--activesupport/test/xml_mini/xml_mini_engine_test.rb264
-rw-r--r--activesupport/test/xml_mini_test.rb116
-rw-r--r--ci/qunit-selenium-runner.rb16
-rwxr-xr-xci/travis.rb132
-rw-r--r--guides/CHANGELOG.md17
-rw-r--r--guides/Rakefile60
-rw-r--r--guides/assets/images/4_0_release_notes/rails4_features.pngbin0 -> 65840 bytes
-rw-r--r--guides/assets/images/akshaysurve.jpgbin3444 -> 0 bytes
-rw-r--r--guides/assets/images/association_basics/belongs_to.pngbin0 -> 35041 bytes
-rw-r--r--guides/assets/images/association_basics/habtm.pngbin0 -> 61435 bytes
-rw-r--r--guides/assets/images/association_basics/has_many.pngbin0 -> 36233 bytes
-rw-r--r--guides/assets/images/association_basics/has_many_through.pngbin0 -> 98834 bytes
-rw-r--r--guides/assets/images/association_basics/has_one.pngbin0 -> 38222 bytes
-rw-r--r--guides/assets/images/association_basics/has_one_through.pngbin0 -> 92535 bytes
-rw-r--r--guides/assets/images/association_basics/polymorphic.pngbin0 -> 84739 bytes
-rw-r--r--guides/assets/images/belongs_to.pngbin26076 -> 0 bytes
-rw-r--r--guides/assets/images/credits_pic_blank.gifbin597 -> 0 bytes
-rw-r--r--guides/assets/images/favicon.icobin5430 -> 16958 bytes
-rw-r--r--guides/assets/images/fxn.pngbin15436 -> 0 bytes
-rw-r--r--guides/assets/images/getting_started/article_with_comments.pngbin22560 -> 13884 bytes
-rw-r--r--guides/assets/images/getting_started/challenge.pngbin21690 -> 20347 bytes
-rw-r--r--guides/assets/images/getting_started/confirm_dialog.pngbin18809 -> 17507 bytes
-rw-r--r--guides/assets/images/getting_started/forbidden_attributes_for_new_article.pngbin10783 -> 9851 bytes
-rw-r--r--guides/assets/images/getting_started/form_with_errors.pngbin12447 -> 11665 bytes
-rw-r--r--guides/assets/images/getting_started/index_action_with_edit_link.pngbin10209 -> 9703 bytes
-rw-r--r--guides/assets/images/getting_started/new_article.pngbin3579 -> 3193 bytes
-rw-r--r--guides/assets/images/getting_started/rails_welcome.pngbin142320 -> 282547 bytes
-rw-r--r--guides/assets/images/getting_started/routing_error_no_controller.pngbin4186 -> 3869 bytes
-rw-r--r--guides/assets/images/getting_started/routing_error_no_route_matches.pngbin5913 -> 0 bytes
-rw-r--r--guides/assets/images/getting_started/show_action_for_articles.pngbin2965 -> 2901 bytes
-rw-r--r--guides/assets/images/getting_started/template_is_missing_articles_new.pngbin6174 -> 472167 bytes
-rw-r--r--guides/assets/images/getting_started/unknown_action_create_for_articles.pngbin5327 -> 4808 bytes
-rw-r--r--guides/assets/images/getting_started/unknown_action_new_for_articles.pngbin5481 -> 4933 bytes
-rw-r--r--guides/assets/images/habtm.pngbin49332 -> 0 bytes
-rw-r--r--guides/assets/images/has_many.pngbin28988 -> 0 bytes
-rw-r--r--guides/assets/images/has_many_through.pngbin79428 -> 0 bytes
-rw-r--r--guides/assets/images/has_one.pngbin29072 -> 0 bytes
-rw-r--r--guides/assets/images/has_one_through.pngbin72434 -> 0 bytes
-rw-r--r--guides/assets/images/header_backdrop.pngbin224 -> 0 bytes
-rw-r--r--guides/assets/images/i18n/demo_html_safe.pngbin10073 -> 9860 bytes
-rw-r--r--guides/assets/images/i18n/demo_localized_pirate.pngbin11485 -> 11214 bytes
-rw-r--r--guides/assets/images/i18n/demo_translated_en.pngbin9325 -> 9069 bytes
-rw-r--r--guides/assets/images/i18n/demo_translated_pirate.pngbin10202 -> 9974 bytes
-rw-r--r--guides/assets/images/i18n/demo_translation_missing.pngbin10260 -> 9984 bytes
-rw-r--r--guides/assets/images/i18n/demo_untranslated.pngbin9224 -> 8985 bytes
-rw-r--r--guides/assets/images/icons/README5
-rw-r--r--guides/assets/images/icons/callouts/1.pngbin147 -> 0 bytes
-rw-r--r--guides/assets/images/icons/callouts/10.pngbin183 -> 0 bytes
-rw-r--r--guides/assets/images/icons/callouts/11.pngbin176 -> 0 bytes
-rw-r--r--guides/assets/images/icons/callouts/12.pngbin186 -> 0 bytes
-rw-r--r--guides/assets/images/icons/callouts/13.pngbin188 -> 0 bytes
-rw-r--r--guides/assets/images/icons/callouts/14.pngbin246 -> 0 bytes
-rw-r--r--guides/assets/images/icons/callouts/15.pngbin191 -> 0 bytes
-rw-r--r--guides/assets/images/icons/callouts/2.pngbin168 -> 0 bytes
-rw-r--r--guides/assets/images/icons/callouts/3.pngbin170 -> 0 bytes
-rw-r--r--guides/assets/images/icons/callouts/4.pngbin165 -> 0 bytes
-rw-r--r--guides/assets/images/icons/callouts/5.pngbin169 -> 0 bytes
-rw-r--r--guides/assets/images/icons/callouts/6.pngbin176 -> 0 bytes
-rw-r--r--guides/assets/images/icons/callouts/7.pngbin160 -> 0 bytes
-rw-r--r--guides/assets/images/icons/callouts/8.pngbin176 -> 0 bytes
-rw-r--r--guides/assets/images/icons/callouts/9.pngbin177 -> 0 bytes
-rw-r--r--guides/assets/images/icons/caution.pngbin2295 -> 0 bytes
-rw-r--r--guides/assets/images/icons/example.pngbin2078 -> 0 bytes
-rw-r--r--guides/assets/images/icons/home.pngbin1163 -> 0 bytes
-rw-r--r--guides/assets/images/icons/important.pngbin2451 -> 0 bytes
-rw-r--r--guides/assets/images/icons/next.pngbin1146 -> 0 bytes
-rw-r--r--guides/assets/images/icons/note.pngbin2155 -> 0 bytes
-rw-r--r--guides/assets/images/icons/prev.pngbin1126 -> 0 bytes
-rw-r--r--guides/assets/images/icons/tip.pngbin2248 -> 0 bytes
-rw-r--r--guides/assets/images/icons/up.pngbin1133 -> 0 bytes
-rw-r--r--guides/assets/images/icons/warning.pngbin2616 -> 0 bytes
-rw-r--r--guides/assets/images/oscardelben.jpgbin6299 -> 0 bytes
-rw-r--r--guides/assets/images/polymorphic.pngbin66415 -> 0 bytes
-rw-r--r--guides/assets/images/radar.pngbin17095 -> 0 bytes
-rw-r--r--guides/assets/images/rails4_features.pngbin67766 -> 0 bytes
-rw-r--r--guides/assets/images/rails_guides_logo.gifbin5106 -> 3770 bytes
-rw-r--r--guides/assets/images/rails_guides_logo_1x.pngbin0 -> 2340 bytes
-rw-r--r--guides/assets/images/rails_guides_logo_2x.pngbin0 -> 3107 bytes
-rw-r--r--guides/assets/images/rails_logo_remix.gifbin8533 -> 0 bytes
-rw-r--r--guides/assets/images/security/csrf.png (renamed from guides/assets/images/csrf.png)bin32179 -> 32179 bytes
-rw-r--r--guides/assets/images/security/session_fixation.pngbin0 -> 38296 bytes
-rw-r--r--guides/assets/images/session_fixation.pngbin38451 -> 0 bytes
-rw-r--r--guides/assets/images/tab_yellow.pngbin1441 -> 0 bytes
-rw-r--r--guides/assets/images/vijaydev.jpgbin2897 -> 0 bytes
-rw-r--r--guides/assets/javascripts/guides.js116
-rw-r--r--guides/assets/javascripts/jquery.min.js4
-rw-r--r--[-rwxr-xr-x]guides/assets/javascripts/responsive-tables.js79
-rw-r--r--guides/assets/javascripts/syntaxhighlighter.js20
-rw-r--r--guides/assets/javascripts/syntaxhighlighter/shBrushAS3.js59
-rw-r--r--guides/assets/javascripts/syntaxhighlighter/shBrushAppleScript.js75
-rw-r--r--guides/assets/javascripts/syntaxhighlighter/shBrushBash.js59
-rw-r--r--guides/assets/javascripts/syntaxhighlighter/shBrushCSharp.js65
-rw-r--r--guides/assets/javascripts/syntaxhighlighter/shBrushColdFusion.js100
-rw-r--r--guides/assets/javascripts/syntaxhighlighter/shBrushCpp.js97
-rw-r--r--guides/assets/javascripts/syntaxhighlighter/shBrushCss.js91
-rw-r--r--guides/assets/javascripts/syntaxhighlighter/shBrushDelphi.js55
-rw-r--r--guides/assets/javascripts/syntaxhighlighter/shBrushDiff.js41
-rw-r--r--guides/assets/javascripts/syntaxhighlighter/shBrushErlang.js52
-rw-r--r--guides/assets/javascripts/syntaxhighlighter/shBrushGroovy.js67
-rw-r--r--guides/assets/javascripts/syntaxhighlighter/shBrushJScript.js52
-rw-r--r--guides/assets/javascripts/syntaxhighlighter/shBrushJava.js57
-rw-r--r--guides/assets/javascripts/syntaxhighlighter/shBrushJavaFX.js58
-rw-r--r--guides/assets/javascripts/syntaxhighlighter/shBrushPerl.js72
-rw-r--r--guides/assets/javascripts/syntaxhighlighter/shBrushPhp.js88
-rw-r--r--guides/assets/javascripts/syntaxhighlighter/shBrushPlain.js33
-rw-r--r--guides/assets/javascripts/syntaxhighlighter/shBrushPowerShell.js74
-rw-r--r--guides/assets/javascripts/syntaxhighlighter/shBrushPython.js64
-rw-r--r--guides/assets/javascripts/syntaxhighlighter/shBrushRuby.js55
-rw-r--r--guides/assets/javascripts/syntaxhighlighter/shBrushSass.js94
-rw-r--r--guides/assets/javascripts/syntaxhighlighter/shBrushScala.js51
-rw-r--r--guides/assets/javascripts/syntaxhighlighter/shBrushSql.js66
-rw-r--r--guides/assets/javascripts/syntaxhighlighter/shBrushVb.js56
-rw-r--r--guides/assets/javascripts/syntaxhighlighter/shBrushXml.js69
-rw-r--r--guides/assets/javascripts/syntaxhighlighter/shCore.js17
-rw-r--r--guides/assets/javascripts/turbolinks.js6
-rw-r--r--guides/assets/stylesheets/main.css100
-rw-r--r--guides/assets/stylesheets/main.rtl.css762
-rw-r--r--guides/assets/stylesheets/print.css2
-rwxr-xr-xguides/assets/stylesheets/responsive-tables.css50
-rw-r--r--guides/assets/stylesheets/style.css1
-rw-r--r--guides/assets/stylesheets/syntaxhighlighter/shCore.css2
-rw-r--r--guides/assets/stylesheets/syntaxhighlighter/shCoreDefault.css328
-rw-r--r--guides/assets/stylesheets/syntaxhighlighter/shCoreDjango.css331
-rw-r--r--guides/assets/stylesheets/syntaxhighlighter/shCoreEclipse.css339
-rw-r--r--guides/assets/stylesheets/syntaxhighlighter/shCoreEmacs.css324
-rw-r--r--guides/assets/stylesheets/syntaxhighlighter/shCoreFadeToGrey.css328
-rw-r--r--guides/assets/stylesheets/syntaxhighlighter/shCoreMDUltra.css324
-rw-r--r--guides/assets/stylesheets/syntaxhighlighter/shCoreMidnight.css324
-rw-r--r--guides/assets/stylesheets/syntaxhighlighter/shCoreRDark.css324
-rw-r--r--guides/assets/stylesheets/syntaxhighlighter/shThemeDefault.css117
-rw-r--r--guides/assets/stylesheets/syntaxhighlighter/shThemeDjango.css120
-rw-r--r--guides/assets/stylesheets/syntaxhighlighter/shThemeEclipse.css128
-rw-r--r--guides/assets/stylesheets/syntaxhighlighter/shThemeEmacs.css113
-rw-r--r--guides/assets/stylesheets/syntaxhighlighter/shThemeFadeToGrey.css117
-rwxr-xr-xguides/assets/stylesheets/syntaxhighlighter/shThemeMDUltra.css113
-rw-r--r--guides/assets/stylesheets/syntaxhighlighter/shThemeMidnight.css113
-rw-r--r--guides/assets/stylesheets/syntaxhighlighter/shThemeRDark.css113
-rw-r--r--guides/assets/stylesheets/syntaxhighlighter/shThemeRailsGuides.css2
-rw-r--r--guides/assets/stylesheets/turbolinks.css3
-rw-r--r--guides/bug_report_templates/action_controller_gem.rb38
-rw-r--r--guides/bug_report_templates/action_controller_master.rb35
-rw-r--r--guides/bug_report_templates/active_job_gem.rb29
-rw-r--r--guides/bug_report_templates/active_job_master.rb28
-rw-r--r--guides/bug_report_templates/active_record_gem.rb29
-rw-r--r--guides/bug_report_templates/active_record_master.rb31
-rw-r--r--guides/bug_report_templates/active_record_migrations_gem.rb60
-rw-r--r--guides/bug_report_templates/active_record_migrations_master.rb59
-rw-r--r--guides/bug_report_templates/benchmark.rb49
-rw-r--r--guides/bug_report_templates/generic_gem.rb24
-rw-r--r--guides/bug_report_templates/generic_master.rb26
-rw-r--r--guides/rails_guides.rb39
-rw-r--r--guides/rails_guides/generator.rb345
-rw-r--r--guides/rails_guides/helpers.rb27
-rw-r--r--guides/rails_guides/indexer.rb78
-rw-r--r--guides/rails_guides/kindle.rb97
-rw-r--r--guides/rails_guides/levenshtein.rb18
-rw-r--r--guides/rails_guides/markdown.rb56
-rw-r--r--guides/rails_guides/markdown/renderer.rb93
-rw-r--r--guides/source/2_2_release_notes.md42
-rw-r--r--guides/source/2_3_release_notes.md64
-rw-r--r--guides/source/3_0_release_notes.md37
-rw-r--r--guides/source/3_1_release_notes.md10
-rw-r--r--guides/source/3_2_release_notes.md16
-rw-r--r--guides/source/4_0_release_notes.md16
-rw-r--r--guides/source/4_1_release_notes.md6
-rw-r--r--guides/source/4_2_release_notes.md27
-rw-r--r--guides/source/5_0_release_notes.md1096
-rw-r--r--guides/source/5_1_release_notes.md659
-rw-r--r--guides/source/5_2_release_notes.md861
-rw-r--r--guides/source/6_0_release_notes.md175
-rw-r--r--guides/source/_welcome.html.erb23
-rw-r--r--guides/source/action_cable_overview.md692
-rw-r--r--guides/source/action_controller_overview.md190
-rw-r--r--guides/source/action_mailer_basics.md240
-rw-r--r--guides/source/action_view_overview.md167
-rw-r--r--guides/source/active_job_basics.md238
-rw-r--r--guides/source/active_model_basics.md169
-rw-r--r--guides/source/active_record_basics.md73
-rw-r--r--guides/source/active_record_callbacks.md136
-rw-r--r--guides/source/active_record_migrations.md324
-rw-r--r--guides/source/active_record_postgresql.md149
-rw-r--r--guides/source/active_record_querying.md455
-rw-r--r--guides/source/active_record_validations.md311
-rw-r--r--guides/source/active_storage_overview.md765
-rw-r--r--guides/source/active_support_core_extensions.md795
-rw-r--r--guides/source/active_support_instrumentation.md257
-rw-r--r--guides/source/api_app.md181
-rw-r--r--guides/source/api_documentation_guidelines.md30
-rw-r--r--guides/source/asset_pipeline.md336
-rw-r--r--guides/source/association_basics.md925
-rw-r--r--guides/source/autoloading_and_reloading_constants.md157
-rw-r--r--guides/source/caching_with_rails.md384
-rw-r--r--guides/source/command_line.md325
-rw-r--r--guides/source/configuring.md695
-rw-r--r--guides/source/contributing_to_ruby_on_rails.md296
-rw-r--r--guides/source/credits.html.erb80
-rw-r--r--guides/source/debugging_rails_applications.md471
-rw-r--r--guides/source/development_dependencies_install.md294
-rw-r--r--guides/source/documents.yaml90
-rw-r--r--guides/source/engines.md284
-rw-r--r--guides/source/form_helpers.md422
-rw-r--r--guides/source/generators.md99
-rw-r--r--guides/source/getting_started.md483
-rw-r--r--guides/source/i18n.md237
-rw-r--r--guides/source/index.html.erb4
-rw-r--r--guides/source/initialization.md216
-rw-r--r--guides/source/kindle/layout.html.erb4
-rw-r--r--guides/source/kindle/rails_guides.opf.erb5
-rw-r--r--guides/source/kindle/toc.html.erb3
-rw-r--r--guides/source/kindle/toc.ncx.erb10
-rw-r--r--guides/source/layout.html.erb84
-rw-r--r--guides/source/layouts_and_rendering.md125
-rw-r--r--guides/source/maintenance_policy.md10
-rw-r--r--guides/source/nested_model_forms.md230
-rw-r--r--guides/source/plugins.md228
-rw-r--r--guides/source/profiling.md16
-rw-r--r--guides/source/rails_application_templates.md42
-rw-r--r--guides/source/rails_on_rack.md99
-rw-r--r--guides/source/routing.md233
-rw-r--r--guides/source/ruby_on_rails_guides_guidelines.md50
-rw-r--r--guides/source/security.md491
-rw-r--r--guides/source/testing.md1360
-rw-r--r--guides/source/threading_and_code_execution.md324
-rw-r--r--guides/source/upgrading_ruby_on_rails.md493
-rw-r--r--guides/source/working_with_javascript_in_rails.md253
-rw-r--r--guides/w3c_validator.rb71
-rw-r--r--install.rb16
-rw-r--r--load_paths.rb3
-rw-r--r--package.json14
-rw-r--r--rails.gemspec46
-rw-r--r--railties/.gitignore6
-rw-r--r--railties/CHANGELOG.md258
-rw-r--r--railties/MIT-LICENSE4
-rw-r--r--railties/RDOC_MAIN.rdoc119
-rw-r--r--railties/README.rdoc3
-rw-r--r--railties/Rakefile82
-rwxr-xr-xrailties/bin/test5
-rwxr-xr-xrailties/exe/rails5
-rw-r--r--railties/lib/minitest/rails_plugin.rb59
-rw-r--r--railties/lib/rails.rb43
-rw-r--r--railties/lib/rails/all.rb22
-rw-r--r--railties/lib/rails/api/generator.rb38
-rw-r--r--railties/lib/rails/api/task.rb109
-rw-r--r--railties/lib/rails/app_loader.rb43
-rw-r--r--railties/lib/rails/app_updater.rb37
-rw-r--r--railties/lib/rails/application.rb220
-rw-r--r--railties/lib/rails/application/bootstrap.rb16
-rw-r--r--railties/lib/rails/application/configuration.rb276
-rw-r--r--railties/lib/rails/application/default_middleware_stack.rb50
-rw-r--r--railties/lib/rails/application/finisher.rb118
-rw-r--r--railties/lib/rails/application/routes_reloader.rb11
-rw-r--r--railties/lib/rails/application_controller.rb33
-rw-r--r--railties/lib/rails/backtrace_cleaner.rb30
-rw-r--r--railties/lib/rails/cli.rb14
-rw-r--r--railties/lib/rails/code_statistics.rb55
-rw-r--r--railties/lib/rails/code_statistics_calculator.rb9
-rw-r--r--railties/lib/rails/command.rb114
-rw-r--r--railties/lib/rails/command/actions.rb44
-rw-r--r--railties/lib/rails/command/base.rb157
-rw-r--r--railties/lib/rails/command/behavior.rb85
-rw-r--r--railties/lib/rails/command/environment_argument.rb47
-rw-r--r--railties/lib/rails/command/helpers/editor.rb35
-rw-r--r--railties/lib/rails/command/spellchecker.rb58
-rw-r--r--railties/lib/rails/commands.rb10
-rw-r--r--railties/lib/rails/commands/application.rb17
-rw-r--r--railties/lib/rails/commands/application/application_command.rb31
-rw-r--r--railties/lib/rails/commands/commands_tasks.rb185
-rw-r--r--railties/lib/rails/commands/console.rb68
-rw-r--r--railties/lib/rails/commands/console/console_command.rb100
-rw-r--r--railties/lib/rails/commands/console_helper.rb34
-rw-r--r--railties/lib/rails/commands/credentials/USAGE49
-rw-r--r--railties/lib/rails/commands/credentials/credentials_command.rb98
-rw-r--r--railties/lib/rails/commands/dbconsole.rb173
-rw-r--r--railties/lib/rails/commands/dbconsole/dbconsole_command.rb170
-rw-r--r--railties/lib/rails/commands/destroy.rb11
-rw-r--r--railties/lib/rails/commands/destroy/destroy_command.rb28
-rw-r--r--railties/lib/rails/commands/dev/dev_command.rb17
-rw-r--r--railties/lib/rails/commands/encrypted/encrypted_command.rb86
-rw-r--r--railties/lib/rails/commands/generate.rb13
-rw-r--r--railties/lib/rails/commands/generate/generate_command.rb30
-rw-r--r--railties/lib/rails/commands/help/USAGE16
-rw-r--r--railties/lib/rails/commands/help/help_command.rb15
-rw-r--r--railties/lib/rails/commands/initializers/initializers_command.rb16
-rw-r--r--railties/lib/rails/commands/new/new_command.rb19
-rw-r--r--railties/lib/rails/commands/notes/notes_command.rb39
-rw-r--r--railties/lib/rails/commands/plugin.rb23
-rw-r--r--railties/lib/rails/commands/plugin/plugin_command.rb45
-rw-r--r--railties/lib/rails/commands/rake/rake_command.rb51
-rw-r--r--railties/lib/rails/commands/routes/routes_command.rb37
-rw-r--r--railties/lib/rails/commands/runner.rb62
-rw-r--r--railties/lib/rails/commands/runner/USAGE20
-rw-r--r--railties/lib/rails/commands/runner/runner_command.rb53
-rw-r--r--railties/lib/rails/commands/secrets/USAGE60
-rw-r--r--railties/lib/rails/commands/secrets/secrets_command.rb65
-rw-r--r--railties/lib/rails/commands/server.rb120
-rw-r--r--railties/lib/rails/commands/server/server_command.rb322
-rw-r--r--railties/lib/rails/commands/test.rb5
-rw-r--r--railties/lib/rails/commands/test/test_command.rb37
-rw-r--r--railties/lib/rails/commands/version/version_command.rb11
-rw-r--r--railties/lib/rails/configuration.rb29
-rw-r--r--railties/lib/rails/console/app.rb13
-rw-r--r--railties/lib/rails/console/helpers.rb4
-rw-r--r--railties/lib/rails/dev_caching.rb44
-rw-r--r--railties/lib/rails/engine.rb152
-rw-r--r--railties/lib/rails/engine/commands.rb46
-rw-r--r--railties/lib/rails/engine/configuration.rb22
-rw-r--r--railties/lib/rails/engine/railties.rb2
-rw-r--r--railties/lib/rails/engine/updater.rb21
-rw-r--r--railties/lib/rails/gem_version.rb4
-rw-r--r--railties/lib/rails/generators.rb545
-rw-r--r--railties/lib/rails/generators/actions.rb180
-rw-r--r--railties/lib/rails/generators/actions/create_migration.rb52
-rw-r--r--railties/lib/rails/generators/active_model.rb8
-rw-r--r--railties/lib/rails/generators/app_base.rb356
-rw-r--r--railties/lib/rails/generators/base.rb116
-rw-r--r--railties/lib/rails/generators/css/assets/assets_generator.rb6
-rw-r--r--railties/lib/rails/generators/css/scaffold/scaffold_generator.rb8
-rw-r--r--railties/lib/rails/generators/erb.rb30
-rw-r--r--railties/lib/rails/generators/erb/controller/controller_generator.rb4
-rw-r--r--railties/lib/rails/generators/erb/controller/templates/view.html.erb.tt (renamed from railties/lib/rails/generators/erb/controller/templates/view.html.erb)0
-rw-r--r--railties/lib/rails/generators/erb/mailer/mailer_generator.rb26
-rw-r--r--railties/lib/rails/generators/erb/mailer/templates/layout.html.erb5
-rw-r--r--railties/lib/rails/generators/erb/mailer/templates/layout.html.erb.tt13
-rw-r--r--railties/lib/rails/generators/erb/mailer/templates/layout.text.erb.tt (renamed from railties/lib/rails/generators/erb/mailer/templates/layout.text.erb)0
-rw-r--r--railties/lib/rails/generators/erb/mailer/templates/view.html.erb.tt (renamed from railties/lib/rails/generators/erb/mailer/templates/view.html.erb)0
-rw-r--r--railties/lib/rails/generators/erb/mailer/templates/view.text.erb.tt (renamed from railties/lib/rails/generators/erb/mailer/templates/view.text.erb)0
-rw-r--r--railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb8
-rw-r--r--railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb34
-rw-r--r--railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb.tt34
-rw-r--r--railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb.tt (renamed from railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb)0
-rw-r--r--railties/lib/rails/generators/erb/scaffold/templates/index.html.erb31
-rw-r--r--railties/lib/rails/generators/erb/scaffold/templates/index.html.erb.tt31
-rw-r--r--railties/lib/rails/generators/erb/scaffold/templates/new.html.erb.tt (renamed from railties/lib/rails/generators/erb/scaffold/templates/new.html.erb)0
-rw-r--r--railties/lib/rails/generators/erb/scaffold/templates/show.html.erb11
-rw-r--r--railties/lib/rails/generators/erb/scaffold/templates/show.html.erb.tt11
-rw-r--r--railties/lib/rails/generators/generated_attribute.rb65
-rw-r--r--railties/lib/rails/generators/js/assets/assets_generator.rb13
-rw-r--r--railties/lib/rails/generators/js/assets/templates/javascript.js2
-rw-r--r--railties/lib/rails/generators/migration.rb33
-rw-r--r--railties/lib/rails/generators/model_helpers.rb15
-rw-r--r--railties/lib/rails/generators/named_base.rb170
-rw-r--r--railties/lib/rails/generators/rails/app/USAGE1
-rw-r--r--railties/lib/rails/generators/rails/app/app_generator.rb343
-rw-r--r--railties/lib/rails/generators/rails/app/templates/Gemfile55
-rw-r--r--railties/lib/rails/generators/rails/app/templates/Gemfile.tt81
-rw-r--r--railties/lib/rails/generators/rails/app/templates/README.md24
-rw-r--r--railties/lib/rails/generators/rails/app/templates/README.md.tt24
-rw-r--r--railties/lib/rails/generators/rails/app/templates/Rakefile6
-rw-r--r--railties/lib/rails/generators/rails/app/templates/Rakefile.tt6
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/assets/config/manifest.js.tt2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt20
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css14
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css.tt15
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/channel.rb.tt4
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/connection.rb.tt4
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/controllers/application_controller.rb.tt7
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/helpers/application_helper.rb.tt (renamed from railties/lib/rails/generators/rails/app/templates/app/helpers/application_helper.rb)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/javascript/channels/consumer.js6
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/javascript/channels/index.js5
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/javascript/packs/application.js.tt21
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb.tt7
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/mailers/application_mailer.rb.tt4
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/models/application_record.rb.tt3
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt35
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/views/layouts/mailer.html.erb.tt13
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/views/layouts/mailer.text.erb.tt1
-rw-r--r--railties/lib/rails/generators/rails/app/templates/bin/bundle2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/bin/rails3
-rw-r--r--railties/lib/rails/generators/rails/app/templates/bin/rails.tt3
-rw-r--r--railties/lib/rails/generators/rails/app/templates/bin/rake.tt (renamed from railties/lib/rails/generators/rails/app/templates/bin/rake)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/bin/setup29
-rw-r--r--railties/lib/rails/generators/rails/app/templates/bin/setup.tt38
-rw-r--r--railties/lib/rails/generators/rails/app/templates/bin/update.tt33
-rw-r--r--railties/lib/rails/generators/rails/app/templates/bin/yarn.tt10
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config.ru4
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config.ru.tt5
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/application.rb43
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/application.rb.tt45
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/boot.rb3
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/boot.rb.tt6
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/cable.yml.tt10
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml49
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml.tt50
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml85
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml.tt86
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml68
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml.tt69
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml52
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml.tt53
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml68
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml.tt69
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml23
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml.tt24
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml58
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml.tt58
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml58
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml.tt61
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml85
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml.tt85
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml25
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml.tt25
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml68
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml.tt52
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/environment.rb5
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/environment.rb.tt5
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt46
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt50
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt26
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/active_record_belongs_to_required_by_default.rb4
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/application_controller_renderer.rb6
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/application_controller_renderer.rb.tt8
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt7
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/backtrace_silencers.rb.tt (renamed from railties/lib/rails/generators/rails/app/templates/config/initializers/backtrace_silencers.rb)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/callback_terminator.rb4
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/content_security_policy.rb.tt29
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb3
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb.tt5
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/cors.rb14
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/cors.rb.tt16
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb.tt (renamed from railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb.tt (renamed from railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb.tt (renamed from railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt17
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt3
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/locales/en.yml12
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt35
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/routes.rb56
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/routes.rb.tt3
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/secrets.yml22
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/spring.rb.tt6
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/storage.yml.tt34
-rw-r--r--railties/lib/rails/generators/rails/app/templates/db/seeds.rb.tt6
-rw-r--r--railties/lib/rails/generators/rails/app/templates/gitignore22
-rw-r--r--railties/lib/rails/generators/rails/app/templates/gitignore.tt35
-rw-r--r--railties/lib/rails/generators/rails/app/templates/package.json.tt11
-rw-r--r--railties/lib/rails/generators/rails/app/templates/public/404.html12
-rw-r--r--railties/lib/rails/generators/rails/app/templates/public/422.html12
-rw-r--r--railties/lib/rails/generators/rails/app/templates/public/500.html12
-rw-r--r--railties/lib/rails/generators/rails/app/templates/public/apple-touch-icon-precomposed.png0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/public/apple-touch-icon.png0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/public/robots.txt4
-rw-r--r--railties/lib/rails/generators/rails/app/templates/ruby-version.tt1
-rw-r--r--railties/lib/rails/generators/rails/app/templates/test/application_system_test_case.rb.tt5
-rw-r--r--railties/lib/rails/generators/rails/app/templates/test/test_helper.rb12
-rw-r--r--railties/lib/rails/generators/rails/app/templates/test/test_helper.rb.tt19
-rw-r--r--railties/lib/rails/generators/rails/application_record/application_record_generator.rb9
-rw-r--r--railties/lib/rails/generators/rails/assets/USAGE5
-rw-r--r--railties/lib/rails/generators/rails/assets/assets_generator.rb24
-rw-r--r--railties/lib/rails/generators/rails/assets/templates/javascript.js2
-rw-r--r--railties/lib/rails/generators/rails/assets/templates/stylesheet.css2
-rw-r--r--railties/lib/rails/generators/rails/controller/controller_generator.rb59
-rw-r--r--railties/lib/rails/generators/rails/controller/templates/controller.rb.tt (renamed from railties/lib/rails/generators/rails/controller/templates/controller.rb)0
-rw-r--r--railties/lib/rails/generators/rails/credentials/credentials_generator.rb56
-rw-r--r--railties/lib/rails/generators/rails/encrypted_file/encrypted_file_generator.rb27
-rw-r--r--railties/lib/rails/generators/rails/encryption_key_file/encryption_key_file_generator.rb58
-rw-r--r--railties/lib/rails/generators/rails/generator/generator_generator.rb7
-rw-r--r--railties/lib/rails/generators/rails/generator/templates/%file_name%_generator.rb.tt2
-rw-r--r--railties/lib/rails/generators/rails/helper/helper_generator.rb4
-rw-r--r--railties/lib/rails/generators/rails/helper/templates/helper.rb.tt (renamed from railties/lib/rails/generators/rails/helper/templates/helper.rb)0
-rw-r--r--railties/lib/rails/generators/rails/integration_test/integration_test_generator.rb2
-rw-r--r--railties/lib/rails/generators/rails/master_key/master_key_generator.rb53
-rw-r--r--railties/lib/rails/generators/rails/migration/migration_generator.rb2
-rw-r--r--railties/lib/rails/generators/rails/model/USAGE8
-rw-r--r--railties/lib/rails/generators/rails/model/model_generator.rb4
-rw-r--r--railties/lib/rails/generators/rails/plugin/plugin_generator.rb140
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec27
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec.tt33
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/Gemfile47
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/Gemfile.tt48
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE.tt (renamed from railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/README.md.tt28
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/README.rdoc3
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/Rakefile29
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/Rakefile.tt28
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt5
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/app/helpers/%namespaced_name%/application_helper.rb.tt2
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/app/jobs/%namespaced_name%/application_job.rb.tt5
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/app/mailers/%namespaced_name%/application_mailer.rb.tt7
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/app/models/%namespaced_name%/application_record.rb.tt6
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/app/views/layouts/%namespaced_name%/application.html.erb.tt6
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt27
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/bin/test.tt4
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/config/routes.rb.tt (renamed from railties/lib/rails/generators/rails/plugin/templates/config/routes.rb)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/gitignore10
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/gitignore.tt18
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%.rb5
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%.rb.tt7
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb6
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb.tt7
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/railtie.rb.tt5
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb1
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb.tt1
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/lib/tasks/%namespaced_name%_tasks.rake.tt (renamed from railties/lib/rails/generators/rails/plugin/templates/lib/tasks/%namespaced_name%_tasks.rake)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/application.rb18
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/application.rb.tt23
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb5
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb.tt5
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/dummy_manifest.js.tt10
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/engine_manifest.js.tt6
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js13
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js.tt17
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb4
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb.tt3
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/stylesheets.css3
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/test/%namespaced_name%_test.rb.tt (renamed from railties/lib/rails/generators/rails/plugin/templates/test/%namespaced_name%_test.rb)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/test/application_system_test_case.rb.tt5
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb8
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb.tt7
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb25
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb.tt29
-rw-r--r--railties/lib/rails/generators/rails/resource/USAGE2
-rw-r--r--railties/lib/rails/generators/rails/resource/resource_generator.rb6
-rw-r--r--railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb45
-rw-r--r--railties/lib/rails/generators/rails/scaffold/USAGE2
-rw-r--r--railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb5
-rw-r--r--railties/lib/rails/generators/rails/scaffold/templates/scaffold.css16
-rw-r--r--railties/lib/rails/generators/rails/scaffold_controller/USAGE2
-rw-r--r--railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb12
-rw-r--r--railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb61
-rw-r--r--railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb.tt61
-rw-r--r--railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb68
-rw-r--r--railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb.tt68
-rw-r--r--railties/lib/rails/generators/rails/system_test/USAGE10
-rw-r--r--railties/lib/rails/generators/rails/system_test/system_test_generator.rb9
-rw-r--r--railties/lib/rails/generators/rails/task/task_generator.rb5
-rw-r--r--railties/lib/rails/generators/rails/task/templates/task.rb.tt (renamed from railties/lib/rails/generators/rails/task/templates/task.rb)0
-rw-r--r--railties/lib/rails/generators/resource_helpers.rb24
-rw-r--r--railties/lib/rails/generators/test_case.rb17
-rw-r--r--railties/lib/rails/generators/test_unit.rb4
-rw-r--r--railties/lib/rails/generators/test_unit/controller/controller_generator.rb8
-rw-r--r--railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb25
-rw-r--r--railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb.tt23
-rw-r--r--railties/lib/rails/generators/test_unit/generator/generator_generator.rb8
-rw-r--r--railties/lib/rails/generators/test_unit/generator/templates/generator_test.rb.tt (renamed from railties/lib/rails/generators/test_unit/generator/templates/generator_test.rb)0
-rw-r--r--railties/lib/rails/generators/test_unit/helper/helper_generator.rb4
-rw-r--r--railties/lib/rails/generators/test_unit/integration/integration_generator.rb6
-rw-r--r--railties/lib/rails/generators/test_unit/integration/templates/integration_test.rb7
-rw-r--r--railties/lib/rails/generators/test_unit/integration/templates/integration_test.rb.tt9
-rw-r--r--railties/lib/rails/generators/test_unit/job/job_generator.rb13
-rw-r--r--railties/lib/rails/generators/test_unit/job/templates/unit_test.rb.tt (renamed from railties/lib/rails/generators/test_unit/job/templates/unit_test.rb.erb)0
-rw-r--r--railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb12
-rw-r--r--railties/lib/rails/generators/test_unit/mailer/templates/functional_test.rb.tt (renamed from railties/lib/rails/generators/test_unit/mailer/templates/functional_test.rb)0
-rw-r--r--railties/lib/rails/generators/test_unit/mailer/templates/preview.rb.tt (renamed from railties/lib/rails/generators/test_unit/mailer/templates/preview.rb)0
-rw-r--r--railties/lib/rails/generators/test_unit/model/model_generator.rb9
-rw-r--r--railties/lib/rails/generators/test_unit/model/templates/fixtures.yml29
-rw-r--r--railties/lib/rails/generators/test_unit/model/templates/fixtures.yml.tt29
-rw-r--r--railties/lib/rails/generators/test_unit/model/templates/unit_test.rb.tt (renamed from railties/lib/rails/generators/test_unit/model/templates/unit_test.rb)0
-rw-r--r--railties/lib/rails/generators/test_unit/plugin/plugin_generator.rb6
-rw-r--r--railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb27
-rw-r--r--railties/lib/rails/generators/test_unit/scaffold/templates/api_functional_test.rb40
-rw-r--r--railties/lib/rails/generators/test_unit/scaffold/templates/api_functional_test.rb.tt44
-rw-r--r--railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb53
-rw-r--r--railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb.tt54
-rw-r--r--railties/lib/rails/generators/test_unit/scaffold/templates/system_test.rb.tt49
-rw-r--r--railties/lib/rails/generators/test_unit/system/system_generator.rb19
-rw-r--r--railties/lib/rails/generators/test_unit/system/templates/application_system_test_case.rb.tt5
-rw-r--r--railties/lib/rails/generators/test_unit/system/templates/system_test.rb.tt9
-rw-r--r--railties/lib/rails/generators/testing/assertions.rb16
-rw-r--r--railties/lib/rails/generators/testing/behaviour.rb59
-rw-r--r--railties/lib/rails/generators/testing/setup_and_teardown.rb2
-rw-r--r--railties/lib/rails/info.rb53
-rw-r--r--railties/lib/rails/info_controller.rb42
-rw-r--r--railties/lib/rails/initializable.rb10
-rw-r--r--railties/lib/rails/mailers_controller.rb52
-rw-r--r--railties/lib/rails/paths.rb54
-rw-r--r--railties/lib/rails/plugin/test.rb9
-rw-r--r--railties/lib/rails/rack.rb2
-rw-r--r--railties/lib/rails/rack/debugger.rb3
-rw-r--r--railties/lib/rails/rack/logger.rb100
-rw-r--r--railties/lib/rails/railtie.rb172
-rw-r--r--railties/lib/rails/railtie/configurable.rb14
-rw-r--r--railties/lib/rails/railtie/configuration.rb6
-rw-r--r--railties/lib/rails/ruby_version_check.rb8
-rw-r--r--railties/lib/rails/secrets.rb105
-rw-r--r--railties/lib/rails/source_annotation_extractor.rb236
-rw-r--r--railties/lib/rails/tasks.rb11
-rw-r--r--railties/lib/rails/tasks/annotations.rake24
-rw-r--r--railties/lib/rails/tasks/dev.rake11
-rw-r--r--railties/lib/rails/tasks/engine.rake32
-rw-r--r--railties/lib/rails/tasks/framework.rake48
-rw-r--r--railties/lib/rails/tasks/initializers.rake11
-rw-r--r--railties/lib/rails/tasks/log.rake30
-rw-r--r--railties/lib/rails/tasks/middleware.rake4
-rw-r--r--railties/lib/rails/tasks/misc.rake47
-rw-r--r--railties/lib/rails/tasks/restart.rake8
-rw-r--r--railties/lib/rails/tasks/routes.rake12
-rw-r--r--railties/lib/rails/tasks/statistics.rake9
-rw-r--r--railties/lib/rails/tasks/tmp.rake29
-rw-r--r--railties/lib/rails/tasks/yarn.rake19
-rw-r--r--railties/lib/rails/templates/rails/mailers/email.html.erb56
-rw-r--r--railties/lib/rails/templates/rails/welcome/index.html.erb336
-rw-r--r--railties/lib/rails/test_help.rb44
-rw-r--r--railties/lib/rails/test_unit/line_filtering.rb13
-rw-r--r--railties/lib/rails/test_unit/minitest_plugin.rb51
-rw-r--r--railties/lib/rails/test_unit/railtie.rb13
-rw-r--r--railties/lib/rails/test_unit/reporter.rb90
-rw-r--r--railties/lib/rails/test_unit/runner.rb143
-rw-r--r--railties/lib/rails/test_unit/test_requirer.rb28
-rw-r--r--railties/lib/rails/test_unit/testing.rake41
-rw-r--r--railties/lib/rails/version.rb4
-rw-r--r--railties/lib/rails/welcome_controller.rb4
-rw-r--r--railties/railties.gemspec50
-rw-r--r--railties/test/abstract_unit.rb43
-rw-r--r--railties/test/app_loader_test.rb36
-rw-r--r--railties/test/application/asset_debugging_test.rb121
-rw-r--r--railties/test/application/assets_test.rb217
-rw-r--r--railties/test/application/bin_setup_test.rb42
-rw-r--r--railties/test/application/configuration/custom_test.rb27
-rw-r--r--railties/test/application/configuration_test.rb1350
-rw-r--r--railties/test/application/console_test.rb89
-rw-r--r--railties/test/application/content_security_policy_test.rb223
-rw-r--r--railties/test/application/current_attributes_integration_test.rb88
-rw-r--r--railties/test/application/dbconsole_test.rb74
-rw-r--r--railties/test/application/generators_test.rb52
-rw-r--r--railties/test/application/help_test.rb25
-rw-r--r--railties/test/application/initializers/frameworks_test.rb63
-rw-r--r--railties/test/application/initializers/hooks_test.rb9
-rw-r--r--railties/test/application/initializers/i18n_test.rb110
-rw-r--r--railties/test/application/initializers/load_path_test.rb7
-rw-r--r--railties/test/application/initializers/notifications_test.rb12
-rw-r--r--railties/test/application/integration_test_case_test.rb76
-rw-r--r--railties/test/application/loading_test.rb175
-rw-r--r--railties/test/application/mailer_previews_test.rb314
-rw-r--r--railties/test/application/middleware/cache_test.rb41
-rw-r--r--railties/test/application/middleware/cookies_test.rb168
-rw-r--r--railties/test/application/middleware/exceptions_test.rb43
-rw-r--r--railties/test/application/middleware/remote_ip_test.rb20
-rw-r--r--railties/test/application/middleware/sendfile_test.rb17
-rw-r--r--railties/test/application/middleware/session_test.rb299
-rw-r--r--railties/test/application/middleware/static_test.rb45
-rw-r--r--railties/test/application/middleware_test.rb143
-rw-r--r--railties/test/application/multiple_applications_test.rb13
-rw-r--r--railties/test/application/paths_test.rb11
-rw-r--r--railties/test/application/per_request_digest_cache_test.rb70
-rw-r--r--railties/test/application/rack/logger_test.rb12
-rw-r--r--railties/test/application/rackup_test.rb7
-rw-r--r--railties/test/application/rake/dbs_test.rb341
-rw-r--r--railties/test/application/rake/dev_test.rb63
-rw-r--r--railties/test/application/rake/framework_test.rb14
-rw-r--r--railties/test/application/rake/initializers_test.rb44
-rw-r--r--railties/test/application/rake/log_test.rb35
-rw-r--r--railties/test/application/rake/migrations_test.rb483
-rw-r--r--railties/test/application/rake/multi_dbs_test.rb230
-rw-r--r--railties/test/application/rake/notes_test.rb152
-rw-r--r--railties/test/application/rake/restart_test.rb17
-rw-r--r--railties/test/application/rake/routes_test.rb43
-rw-r--r--railties/test/application/rake/tmp_test.rb43
-rw-r--r--railties/test/application/rake_test.rb221
-rw-r--r--railties/test/application/rendering_test.rb19
-rw-r--r--railties/test/application/routing_test.rb428
-rw-r--r--railties/test/application/runner_test.rb81
-rw-r--r--railties/test/application/server_test.rb66
-rw-r--r--railties/test/application/test_runner_test.rb758
-rw-r--r--railties/test/application/test_test.rb128
-rw-r--r--railties/test/application/url_generation_test.rb13
-rw-r--r--railties/test/application/version_test.rb26
-rw-r--r--railties/test/backtrace_cleaner_test.rb32
-rw-r--r--railties/test/code_statistics_calculator_test.rb60
-rw-r--r--railties/test/code_statistics_test.rb26
-rw-r--r--railties/test/command/base_test.rb13
-rw-r--r--railties/test/command/spellchecker_test.rb10
-rw-r--r--railties/test/commands/console_test.rb124
-rw-r--r--railties/test/commands/credentials_test.rb102
-rw-r--r--railties/test/commands/dbconsole_test.rb307
-rw-r--r--railties/test/commands/dev_test.rb65
-rw-r--r--railties/test/commands/encrypted_test.rb106
-rw-r--r--railties/test/commands/initializers_test.rb32
-rw-r--r--railties/test/commands/notes_test.rb128
-rw-r--r--railties/test/commands/routes_test.rb217
-rw-r--r--railties/test/commands/secrets_test.rb77
-rw-r--r--railties/test/commands/server_test.rb260
-rw-r--r--railties/test/configuration/middleware_stack_proxy_test.rb24
-rw-r--r--railties/test/console_helpers.rb25
-rw-r--r--railties/test/credentials_test.rb49
-rw-r--r--railties/test/engine/commands_test.rb81
-rw-r--r--railties/test/engine/test_test.rb31
-rw-r--r--railties/test/engine_test.rb6
-rw-r--r--railties/test/env_helpers.rb40
-rw-r--r--railties/test/fixtures/about_yml_plugins/bad_about_yml/about.yml1
-rw-r--r--railties/test/fixtures/about_yml_plugins/bad_about_yml/init.rb1
-rw-r--r--railties/test/fixtures/about_yml_plugins/plugin_without_about_yml/init.rb1
-rw-r--r--railties/test/fixtures/lib/create_test_dummy_template.rb2
-rw-r--r--railties/test/fixtures/lib/generators/active_record/fixjour_generator.rb4
-rw-r--r--railties/test/fixtures/lib/generators/fixjour_generator.rb2
-rw-r--r--railties/test/fixtures/lib/generators/model_generator.rb4
-rw-r--r--railties/test/fixtures/lib/generators/usage_template/usage_template_generator.rb6
-rw-r--r--railties/test/fixtures/lib/rails/generators/foobar/foobar_generator.rb2
-rw-r--r--railties/test/fixtures/lib/template.rb2
-rw-r--r--railties/test/generators/actions_test.rb381
-rw-r--r--railties/test/generators/api_app_generator_test.rb187
-rw-r--r--railties/test/generators/app_generator_test.rb935
-rw-r--r--railties/test/generators/application_record_generator_test.rb16
-rw-r--r--railties/test/generators/argv_scrubber_test.rb69
-rw-r--r--railties/test/generators/assets_generator_test.rb10
-rw-r--r--railties/test/generators/channel_generator_test.rb92
-rw-r--r--railties/test/generators/controller_generator_test.rb45
-rw-r--r--railties/test/generators/create_migration_test.rb32
-rw-r--r--railties/test/generators/generated_attribute_test.rb48
-rw-r--r--railties/test/generators/generator_generator_test.rb14
-rw-r--r--railties/test/generators/generator_test.rb55
-rw-r--r--railties/test/generators/generators_test_helper.rb44
-rw-r--r--railties/test/generators/helper_generator_test.rb8
-rw-r--r--railties/test/generators/integration_test_generator_test.rb14
-rw-r--r--railties/test/generators/job_generator_test.rb23
-rw-r--r--railties/test/generators/mailer_generator_test.rb35
-rw-r--r--railties/test/generators/migration_generator_test.rb87
-rw-r--r--railties/test/generators/model_generator_test.rb159
-rw-r--r--railties/test/generators/named_base_test.rb188
-rw-r--r--railties/test/generators/namespaced_generators_test.rb74
-rw-r--r--railties/test/generators/orm_test.rb2
-rw-r--r--railties/test/generators/plugin_generator_test.rb504
-rw-r--r--railties/test/generators/plugin_test_helper.rb26
-rw-r--r--railties/test/generators/plugin_test_runner_test.rb122
-rw-r--r--railties/test/generators/resource_generator_test.rb22
-rw-r--r--railties/test/generators/scaffold_controller_generator_test.rb61
-rw-r--r--railties/test/generators/scaffold_generator_test.rb264
-rw-r--r--railties/test/generators/shared_generator_tests.rb311
-rw-r--r--railties/test/generators/system_test_generator_test.rb19
-rw-r--r--railties/test/generators/task_generator_test.rb10
-rw-r--r--railties/test/generators/test_runner_in_engine_test.rb34
-rw-r--r--railties/test/generators_test.rb87
-rw-r--r--railties/test/initializable_test.rb16
-rw-r--r--railties/test/isolation/abstract_unit.rb395
-rw-r--r--railties/test/json_params_parsing_test.rb51
-rw-r--r--railties/test/minitest/rails_plugin_test.rb42
-rw-r--r--railties/test/path_generation_test.rb46
-rw-r--r--railties/test/paths_test.rb97
-rw-r--r--railties/test/rack_logger_test.rb50
-rw-r--r--railties/test/rails_info_controller_test.rb51
-rw-r--r--railties/test/rails_info_test.rb53
-rw-r--r--railties/test/railties/engine_test.rb371
-rw-r--r--railties/test/railties/generators_test.rb18
-rw-r--r--railties/test/railties/mounted_engine_test.rb67
-rw-r--r--railties/test/railties/railtie_test.rb42
-rw-r--r--railties/test/secrets_test.rb178
-rw-r--r--railties/test/test_unit/reporter_test.rb164
-rw-r--r--railties/test/version_test.rb4
-rw-r--r--tasks/release.rb202
-rw-r--r--tasks/release_announcement_draft.erb38
-rw-r--r--tools/README.md5
-rwxr-xr-xtools/console14
-rwxr-xr-xtools/profile78
-rw-r--r--tools/test.rb24
-rw-r--r--version.rb4
3414 files changed, 214533 insertions, 92056 deletions
diff --git a/.codeclimate.yml b/.codeclimate.yml
new file mode 100644
index 0000000000..139ee8013b
--- /dev/null
+++ b/.codeclimate.yml
@@ -0,0 +1,36 @@
+checks:
+ argument-count:
+ enabled: false
+ complex-logic:
+ enabled: false
+ file-lines:
+ enabled: false
+ method-complexity:
+ enabled: false
+ method-count:
+ enabled: false
+ method-lines:
+ enabled: false
+ nested-control-flow:
+ enabled: false
+ return-statements:
+ enabled: false
+ similar-code:
+ enabled: false
+ identical-code:
+ enabled: false
+
+engines:
+ rubocop:
+ enabled: true
+ channel: rubocop-0-60
+
+ratings:
+ paths:
+ - "**.rb"
+
+exclude_paths:
+ - ci/
+ - guides/
+ - tasks/
+ - tools/
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000000..f6276855ce
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+*.rb diff=ruby
+*.gemspec diff=ruby
diff --git a/.github/autolabeler.yml b/.github/autolabeler.yml
new file mode 100644
index 0000000000..d73b2e3362
--- /dev/null
+++ b/.github/autolabeler.yml
@@ -0,0 +1,24 @@
+actioncable:
+ - "actioncable/**/*"
+actionmailer:
+ - "actionmailer/**/*"
+actionpack:
+ - "actionpack/**/*"
+actionview:
+ - "actionview/**/*"
+activejob:
+ - "activejob/**/*"
+activemodel:
+ - "activemodel/**/*"
+activerecord:
+ - "activerecord/**/*"
+activestorage:
+ - "activestorage/**/*"
+activesupport:
+ - "activesupport/**/*"
+rails-ujs:
+ - "actionview/app/assets/javascripts/rails-ujs*/*"
+railties:
+ - "railties/**/*"
+docs:
+ - "guides/**/*"
diff --git a/.github/issue_template.md b/.github/issue_template.md
new file mode 100644
index 0000000000..87e666a7dd
--- /dev/null
+++ b/.github/issue_template.md
@@ -0,0 +1,14 @@
+### Steps to reproduce
+<!-- (Guidelines for creating a bug report are [available
+here](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#creating-a-bug-report)) -->
+
+### Expected behavior
+<!-- Tell us what should happen -->
+
+### Actual behavior
+<!-- Tell us what happens instead -->
+
+### System configuration
+**Rails version**:
+
+**Ruby version**:
diff --git a/.github/no-response.yml b/.github/no-response.yml
new file mode 100644
index 0000000000..326fa84b7e
--- /dev/null
+++ b/.github/no-response.yml
@@ -0,0 +1,12 @@
+# Configuration for probot-no-response - https://github.com/probot/no-response
+
+# Number of days of inactivity before an Issue is closed for lack of response
+daysUntilClose: 14
+# Label requiring a response
+responseRequiredLabel: more-information-needed
+# Comment to post when closing an Issue for lack of response. Set to `false` to disable
+closeComment: >
+ This issue has been automatically closed because there has been no follow-up
+ response from the original author. We currently don't have enough
+ information in order to take action. Please reach out if you have any additional
+ information that will help us move this issue forward.
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000000..85c0e29ff0
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,21 @@
+### Summary
+
+<!-- Provide a general description of the code changes in your pull
+request... were there any bugs you had fixed? If so, mention them. If
+these bugs have open GitHub issues, be sure to tag them here as well,
+to keep the conversation linked together. -->
+
+### Other Information
+
+<!-- If there's anything else that's important and relevant to your pull
+request, mention that information here. This could include
+benchmarks, or other information.
+
+If you are updating any of the CHANGELOG files or are asked to update the
+CHANGELOG files by reviewers, please add the CHANGELOG entry at the top of the file.
+
+Finally, if your pull request affects documentation or any non-code
+changes, guidelines for those changes are [available
+here](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation)
+
+Thanks for contributing to Rails! -->
diff --git a/.github/stale.yml b/.github/stale.yml
new file mode 100644
index 0000000000..21d9d792b0
--- /dev/null
+++ b/.github/stale.yml
@@ -0,0 +1,28 @@
+# Number of days of inactivity before an issue becomes stale
+daysUntilStale: 90
+# Number of days of inactivity before a stale issue is closed
+daysUntilClose: 7
+# 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-2-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
+# Limit to only `issues` or `pulls`
+only: issues
diff --git a/.gitignore b/.gitignore
index c3cb009140..35440cb54f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,21 +1,19 @@
-# Don't put *.swp, *.bak, etc here; those belong in a global ~/.gitignore.
+# Don't put *.swp, *.bak, etc here; those belong in a global .gitignore.
# Check out https://help.github.com/articles/ignoring-files for how to set that up.
-debug.log
.Gemfile
+.byebug_history
+.ruby-version
+/*/doc/
+/*/test/tmp/
/.bundle
-/.ruby-version
-pkg
-/dist
-/doc/rdoc
-/*/doc
-/*/test/tmp
-/activerecord/sqlnet.log
-/activemodel/test/fixtures/fixture_database.sqlite3
-/activesupport/test/fixtures/isolation_test
-/railties/test/500.html
-/railties/test/fixtures/tmp
-/railties/test/initializer/root/log
-/railties/doc
-/railties/tmp
-/guides/output
+/dist/
+/doc/
+/guides/output/
+debug.log
+node_modules/
+package-lock.json
+pkg/
+/tmp/
+/yarn-error.log
+/yarn.lock
diff --git a/.rubocop.yml b/.rubocop.yml
new file mode 100644
index 0000000000..033f7adf49
--- /dev/null
+++ b/.rubocop.yml
@@ -0,0 +1,218 @@
+AllCops:
+ TargetRubyVersion: 2.4
+ # RuboCop has a bunch of cops enabled by default. This setting tells RuboCop
+ # to ignore them, so only the ones explicitly set in this file are enabled.
+ DisabledByDefault: true
+ Exclude:
+ - '**/templates/**/*'
+ - '**/vendor/**/*'
+ - 'actionpack/lib/action_dispatch/journey/parser.rb'
+ - 'railties/test/fixtures/tmp/**/*'
+ - 'node_modules/**/*'
+
+Performance:
+ Exclude:
+ - '**/test/**/*'
+
+Rails:
+ Enabled: true
+
+# Prefer assert_not over assert !
+Rails/AssertNot:
+ Include:
+ - '**/test/**/*'
+
+# Prefer assert_not_x over refute_x
+Rails/RefuteMethods:
+ Include:
+ - '**/test/**/*'
+
+# Prefer &&/|| over and/or.
+Style/AndOr:
+ Enabled: true
+
+# Do not use braces for hash literals when they are the last argument of a
+# method call.
+Style/BracesAroundHashParameters:
+ Enabled: true
+ EnforcedStyle: context_dependent
+
+# Align `when` with `case`.
+Layout/CaseIndentation:
+ Enabled: true
+
+# Align comments with method definitions.
+Layout/CommentIndentation:
+ Enabled: true
+
+Layout/ElseAlignment:
+ Enabled: true
+
+# Align `end` with the matching keyword or starting expression except for
+# assignments, where it should be aligned with the LHS.
+Layout/EndAlignment:
+ Enabled: true
+ EnforcedStyleAlignWith: variable
+ AutoCorrect: true
+
+Layout/EmptyLineAfterMagicComment:
+ Enabled: true
+
+Layout/EmptyLinesAroundBlockBody:
+ Enabled: true
+
+# In a regular class definition, no empty lines around the body.
+Layout/EmptyLinesAroundClassBody:
+ Enabled: true
+
+# In a regular method definition, no empty lines around the body.
+Layout/EmptyLinesAroundMethodBody:
+ Enabled: true
+
+# In a regular module definition, no empty lines around the body.
+Layout/EmptyLinesAroundModuleBody:
+ Enabled: true
+
+Layout/FirstParameterIndentation:
+ Enabled: true
+
+# Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }.
+Style/HashSyntax:
+ Enabled: true
+
+# Method definitions after `private` or `protected` isolated calls need one
+# extra level of indentation.
+Layout/IndentationConsistency:
+ Enabled: true
+ EnforcedStyle: rails
+
+# Two spaces, no tabs (for indentation).
+Layout/IndentationWidth:
+ Enabled: true
+
+Layout/LeadingCommentSpace:
+ Enabled: true
+
+Layout/SpaceAfterColon:
+ Enabled: true
+
+Layout/SpaceAfterComma:
+ Enabled: true
+
+Layout/SpaceAroundEqualsInParameterDefault:
+ Enabled: true
+
+Layout/SpaceAroundKeyword:
+ Enabled: true
+
+Layout/SpaceAroundOperators:
+ Enabled: true
+
+Layout/SpaceBeforeComma:
+ Enabled: true
+
+Layout/SpaceBeforeFirstArg:
+ Enabled: true
+
+Style/DefWithParentheses:
+ Enabled: true
+
+# Defining a method with parameters needs parentheses.
+Style/MethodDefParentheses:
+ Enabled: true
+
+Style/FrozenStringLiteralComment:
+ Enabled: true
+ EnforcedStyle: always
+ Exclude:
+ - 'actionview/test/**/*.builder'
+ - 'actionview/test/**/*.ruby'
+ - 'actionpack/test/**/*.builder'
+ - 'actionpack/test/**/*.ruby'
+ - 'activestorage/db/migrate/**/*.rb'
+
+Style/RedundantFreeze:
+ Enabled: true
+
+# Use `foo {}` not `foo{}`.
+Layout/SpaceBeforeBlockBraces:
+ Enabled: true
+
+# Use `foo { bar }` not `foo {bar}`.
+Layout/SpaceInsideBlockBraces:
+ Enabled: true
+ EnforcedStyleForEmptyBraces: space
+
+# Use `{ a: 1 }` not `{a:1}`.
+Layout/SpaceInsideHashLiteralBraces:
+ Enabled: true
+
+Layout/SpaceInsideParens:
+ Enabled: true
+
+# Check quotes usage according to lint rule below.
+Style/StringLiterals:
+ Enabled: true
+ EnforcedStyle: double_quotes
+
+# Detect hard tabs, no hard tabs.
+Layout/Tab:
+ Enabled: true
+
+# Blank lines should not have any spaces.
+Layout/TrailingBlankLines:
+ Enabled: true
+
+# No trailing whitespace.
+Layout/TrailingWhitespace:
+ Enabled: true
+
+# Use quotes for string literals when they are enough.
+Style/UnneededPercentQ:
+ Enabled: true
+
+# Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg.
+Lint/RequireParentheses:
+ Enabled: true
+
+Lint/StringConversionInInterpolation:
+ Enabled: true
+
+Lint/UriEscapeUnescape:
+ Enabled: true
+
+Style/ParenthesesAroundCondition:
+ Enabled: true
+
+Style/RedundantReturn:
+ Enabled: true
+ AllowMultipleReturnValues: true
+
+Style/Semicolon:
+ Enabled: true
+ AllowAsExpressionSeparator: true
+
+# Prefer Foo.method over Foo::method
+Style/ColonMethodCall:
+ Enabled: true
+
+Style/TrivialAccessors:
+ Enabled: true
+
+Performance/FlatMap:
+ Enabled: true
+
+Performance/RedundantMerge:
+ Enabled: true
+
+Performance/StartWith:
+ Enabled: true
+
+Performance/EndWith:
+ Enabled: true
+
+Performance/RegexpMatch:
+ Enabled: true
+
+Performance/UnfreezeString:
+ Enabled: true
diff --git a/.travis.yml b/.travis.yml
index 2237ca85d9..3db4e9cd94 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,53 +1,218 @@
language: ruby
sudo: false
-script: 'ci/travis.rb'
+
+cache:
+ directories:
+ - vendor/bundle
+ - /tmp/cache/unicode_conformance
+ - /tmp/beanstalkd-1.10
+ - node_modules
+ - $HOME/.nvm
+
+services:
+ - memcached
+ - redis-server
+
+addons:
+ postgresql: 10
+ chrome: stable
+ apt:
+ sources:
+ - sourceline: "ppa:mc3man/trusty-media"
+ - sourceline: "ppa:ubuntuhandbook1/apps"
+ - mysql-5.7-trusty
+ packages:
+ - ffmpeg
+ - mupdf
+ - mupdf-tools
+ - poppler-utils
+ - mysql-server
+ - mysql-client
+ - postgresql-10
+ - postgresql-client-10
+
+bundler_args: --jobs 3 --retry 3
before_install:
- - gem install bundler
- "rm ${BUNDLE_GEMFILE}.lock"
+ - "travis_retry gem update --system"
+ - "travis_retry gem install bundler"
+ - "[[ -z $encrypted_0fb9444d0374_key && -z $encrypted_0fb9444d0374_iv ]] || openssl aes-256-cbc -K $encrypted_0fb9444d0374_key -iv $encrypted_0fb9444d0374_iv -in activestorage/test/service/configurations.yml.enc -out activestorage/test/service/configurations.yml -d"
+ - "[[ $GEM != 'av:ujs' ]] || nvm install node"
+ - "[[ $GEM != 'av:ujs' ]] || node --version"
+ - "[[ $GEM != 'av:ujs' ]] || (cd actionview && npm install)"
+ - "[[ $GEM != 'railties' ]] || (curl -o- -L https://yarnpkg.com/install.sh | bash)"
+ - "[[ $GEM != 'railties' ]] || export PATH=$HOME/.yarn/bin:$PATH"
+
before_script:
- - bundle update
-cache: bundler
+ # Set Sauce Labs username and access key. Obfuscated, purposefully not encrypted.
+ # Decodes to e.g. `export VARIABLE=VALUE`
+ - $(base64 --decode <<< "ZXhwb3J0IFNBVUNFX0FDQ0VTU19LRVk9YTAzNTM0M2YtZTkyMi00MGIzLWFhM2MtMDZiM2VhNjM1YzQ4")
+ - $(base64 --decode <<< "ZXhwb3J0IFNBVUNFX1VTRVJOQU1FPXJ1YnlvbnJhaWxz")
+
+script: 'ci/travis.rb'
+
env:
global:
- - JRUBY_OPTS='-J-Xmx1024M'
+ - "JRUBY_OPTS='--dev -J-Xmx1024M'"
matrix:
- - "GEM=railties"
- - "GEM=ap"
- - "GEM=am,amo,as,av,aj"
- - "GEM=ar:mysql"
- - "GEM=ar:mysql2"
+ - "GEM=ap,ac"
+ - "GEM=am,amo,as,av,aj,ast"
+ - "GEM=as PRESERVE_TIMEZONES=1"
- "GEM=ar:sqlite3"
- - "GEM=ar:postgresql"
- - "GEM=aj:integration"
- "GEM=guides"
+ - "GEM=ac:integration"
+
rvm:
- - 2.2.2
+ - 2.4.4
+ - 2.5.1
- ruby-head
- - rbx-2
- - jruby-head
+
matrix:
+ include:
+ - rvm: 2.4.4
+ env: "GEM=railties"
+ sudo: required
+ before_install:
+ - "rm ${BUNDLE_GEMFILE}.lock"
+ - "travis_retry gem update --system"
+ - "travis_retry gem install bundler"
+ - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf"
+ - "sudo service postgresql restart 10"
+ - rvm: 2.5.1
+ env: "GEM=railties"
+ sudo: required
+ before_install:
+ - "rm ${BUNDLE_GEMFILE}.lock"
+ - "travis_retry gem update --system"
+ - "travis_retry gem install bundler"
+ - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf"
+ - "sudo service postgresql restart 10"
+ - rvm: ruby-head
+ env: "GEM=railties"
+ sudo: required
+ before_install:
+ - "rm ${BUNDLE_GEMFILE}.lock"
+ - "travis_retry gem update --system"
+ - "travis_retry gem install bundler"
+ - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf"
+ - "sudo service postgresql restart 10"
+ - rvm: 2.5.1
+ env: "GEM=av:ujs"
+ - rvm: 2.4.4
+ sudo: required
+ env: "GEM=aj:integration"
+ services:
+ - memcached
+ - redis-server
+ - rabbitmq
+ before_install:
+ - sudo sed -i -e '/local.*peer/s/postgres/all/' -e 's/peer\|md5/trust/g' /etc/postgresql/*/main/pg_hba.conf
+ - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf"
+ - "sudo service postgresql restart 10"
+ - "[ -f /tmp/beanstalkd-1.10/Makefile ] || (curl -L https://github.com/kr/beanstalkd/archive/v1.10.tar.gz | tar xz -C /tmp)"
+ - "pushd /tmp/beanstalkd-1.10 && make && (./beanstalkd &); popd"
+ - rvm: 2.5.1
+ sudo: required
+ env: "GEM=aj:integration"
+ services:
+ - memcached
+ - redis-server
+ - rabbitmq
+ before_install:
+ - sudo sed -i -e '/local.*peer/s/postgres/all/' -e 's/peer\|md5/trust/g' /etc/postgresql/*/main/pg_hba.conf
+ - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf"
+ - "sudo service postgresql restart 10"
+ - "[ -f /tmp/beanstalkd-1.10/Makefile ] || (curl -L https://github.com/kr/beanstalkd/archive/v1.10.tar.gz | tar xz -C /tmp)"
+ - "pushd /tmp/beanstalkd-1.10 && make && (./beanstalkd &); popd"
+ - rvm: ruby-head
+ sudo: required
+ env: "GEM=aj:integration"
+ services:
+ - memcached
+ - redis-server
+ - rabbitmq
+ before_install:
+ - sudo sed -i -e '/local.*peer/s/postgres/all/' -e 's/peer\|md5/trust/g' /etc/postgresql/*/main/pg_hba.conf
+ - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf"
+ - "sudo service postgresql restart 10"
+ - "[ -f /tmp/beanstalkd-1.10/Makefile ] || (curl -L https://github.com/kr/beanstalkd/archive/v1.10.tar.gz | tar xz -C /tmp)"
+ - "pushd /tmp/beanstalkd-1.10 && make && (./beanstalkd &); popd"
+ - rvm: 2.4.4
+ env: "GEM=ar:mysql2"
+ sudo: required
+ before_install:
+ - "sudo mysql -e \"use mysql; update user set authentication_string='' where User='root'; update user set plugin='mysql_native_password';FLUSH PRIVILEGES;\""
+ - "sudo mysql_upgrade"
+ - "sudo service mysql restart"
+ - rvm: 2.5.1
+ env: "GEM=ar:mysql2"
+ sudo: required
+ before_install:
+ - "sudo mysql -e \"use mysql; update user set authentication_string='' where User='root'; update user set plugin='mysql_native_password';FLUSH PRIVILEGES;\""
+ - "sudo mysql_upgrade"
+ - "sudo service mysql restart"
+ - rvm: ruby-head
+ env: "GEM=ar:mysql2"
+ sudo: required
+ before_install:
+ - "sudo mysql -e \"use mysql; update user set authentication_string='' where User='root'; update user set plugin='mysql_native_password';FLUSH PRIVILEGES;\""
+ - "sudo mysql_upgrade"
+ - "sudo service mysql restart"
+ - rvm: 2.5.1
+ env:
+ - "GEM=ar:mysql2 MYSQL=mariadb"
+ addons:
+ mariadb: 10.3
+ - rvm: 2.5.1
+ env:
+ - "GEM=ar:sqlite3_mem"
+ - rvm: 2.4.4
+ env: "GEM=ar:postgresql"
+ sudo: required
+ before_install:
+ - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf"
+ - "sudo service postgresql restart 10"
+ - rvm: 2.5.1
+ env: "GEM=ar:postgresql"
+ sudo: required
+ before_install:
+ - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf"
+ - "sudo service postgresql restart 10"
+ - rvm: ruby-head
+ env: "GEM=ar:postgresql"
+ sudo: required
+ before_install:
+ - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf"
+ - "sudo service postgresql restart 10"
+ - rvm: 2.5.1
+ env:
+ - "GEM=ar:postgresql POSTGRES=9.2"
+ addons:
+ postgresql: "9.2"
+ - rvm: jruby-head
+ jdk: oraclejdk8
+ env:
+ - "GEM=ap"
+ - rvm: jruby-head
+ jdk: oraclejdk8
+ env:
+ - "GEM=am,amo,aj"
allow_failures:
- - env: "GEM=ar:mysql"
- rvm: ruby-head
- - rvm: rbx-2
- rvm: jruby-head
+ - env: "GEM=ac:integration"
fast_finish: true
+
notifications:
email: false
irc:
on_success: change
on_failure: always
channels:
- - "irc.freenode.org#rails-contrib"
+ # "irc.freenode.org#rails-contrib"
+ - secure: "QFKSOK7xQiWWqTzYfYm0XWoW7idzuxT57MBW9i9EASyRLEPuDwZEubKRP40Y7wPx7ylQd9lp6kJheeLnrDvvTjFbW3sWv9GDRl4WlOU8sG/Kv7MXAASXlDqzyJxxXTtzLeXz2iwY296kOBuKxKxl923eTvEGeocwH02QGo14LpQ="
campfire:
on_success: change
on_failure: always
rooms:
- secure: "YA1alef1ESHWGFNVwvmVGCkMe4cUy4j+UcNvMUESraceiAfVyRMAovlQBGs6\n9kBRm7DHYBUXYC2ABQoJbQRLDr/1B5JPf/M8+Qd7BKu8tcDC03U01SMHFLpO\naOs/HLXcDxtnnpL07tGVsm0zhMc5N8tq4/L3SHxK7Vi+TacwQzI="
-bundler_args: --without test --jobs 3 --retry 3
-services:
- - memcached
- - redis
- - rabbitmq
-addons:
- postgresql: "9.3"
diff --git a/.yarnrc b/.yarnrc
new file mode 100644
index 0000000000..6be74e145d
--- /dev/null
+++ b/.yarnrc
@@ -0,0 +1,2 @@
+workspaces-experimental true
+--add.prefer-offline true \ No newline at end of file
diff --git a/Brewfile b/Brewfile
new file mode 100644
index 0000000000..8a11a8be83
--- /dev/null
+++ b/Brewfile
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+tap "homebrew/core"
+tap "homebrew/bundle"
+tap "homebrew/services"
+tap "caskroom/cask"
+brew "ffmpeg"
+brew "memcached"
+brew "mysql"
+brew "postgresql"
+brew "redis"
+brew "yarn"
+cask "xquartz"
+brew "mupdf"
+brew "poppler"
+brew "imagemagick"
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000000..ecd56b87d6
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,12 @@
+# Contributor Code of Conduct
+
+The Rails team is committed to fostering a welcoming community.
+
+**Our Code of Conduct can be found here**:
+
+http://rubyonrails.org/conduct/
+
+For a history of updates, see the page history here:
+
+https://github.com/rails/homepage/commits/master/conduct.html
+
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 699b6fd2d1..097e2f2f49 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,16 +1,48 @@
-Ruby on Rails is a volunteer effort. We encourage you to pitch in. [Join the team](http://contributors.rubyonrails.org)!
+## How to contribute to Ruby on Rails
-* If you want to submit a bug report please make sure to follow our [reporting guidelines](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#reporting-an-issue).
+#### **Did you find a bug?**
-* If you want to submit a patch, please read the [Contributing to Ruby on Rails](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html) guide.
+* **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/).
-* If you want to contribute to Rails documentation, please read the [Contributing to the Rails Documentation](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation) section of the aforementioned guide.
+* **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/rails/rails/issues).
-*We only accept bug reports and pull requests on GitHub*.
+* 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.
-* If you have a question about how to use Ruby on Rails, please [ask it on the rubyonrails-talk mailing list](https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-talk).
+* If possible, use the relevant bug report templates to create the issue. Simply copy the content of the appropriate template into a .rb file, make the necessary changes to demonstrate the issue, and **paste the content into the issue description**:
+ * [**Active Record** (models, database) issues](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_master.rb)
+ * [**Action Pack** (controllers, routing) issues](https://github.com/rails/rails/blob/master/guides/bug_report_templates/action_controller_master.rb)
+ * [**Generic template** for other issues](https://github.com/rails/rails/blob/master/guides/bug_report_templates/generic_master.rb)
-* If you have a change or new feature in mind, please [suggest it on the rubyonrails-core mailing list](https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core) and start writing code.
+* For more detailed information on submitting a bug report and creating an issue, visit our [reporting guidelines](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#reporting-an-issue).
+
+#### **Did you write a patch that fixes a bug?**
+
+* Open a new GitHub pull request with the patch.
+
+* Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable.
+
+* Before submitting, please read the [Contributing to Ruby on Rails](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html) guide to know more about coding conventions and benchmarks.
+
+#### **Did you fix whitespace, format code, or make a purely cosmetic patch?**
+
+Changes that are cosmetic in nature and do not add anything substantial to the stability, functionality, or testability of Rails will generally not be accepted (read more about [our rationales behind this decision](https://github.com/rails/rails/pull/13771#issuecomment-32746700)).
+
+#### **Do you intend to add a new feature or change an existing one?**
+
+* Suggest your change in the [rubyonrails-core mailing list](https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core) and start writing code.
+
+* Do not open an issue on GitHub until you have collected positive feedback about the change. GitHub issues are primarily intended for bug reports and fixes.
+
+#### **Do you have questions about the source code?**
+
+* Ask any question about how to use Ruby on Rails in the [rubyonrails-talk mailing list](https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-talk).
+
+#### **Do you want to contribute to the Rails documentation?**
+
+* Please read [Contributing to the Rails Documentation](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation).
+
+Ruby on Rails is a volunteer effort. We encourage you to pitch in and [join the team](http://contributors.rubyonrails.org)!
Thanks! :heart: :heart: :heart:
diff --git a/Gemfile b/Gemfile
index de4159d254..9df706f9c2 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,126 +1,160 @@
-source 'https://rubygems.org'
+# frozen_string_literal: true
+
+source "https://rubygems.org"
+
+git_source(:github) { |repo| "https://github.com/#{repo}.git" }
gemspec
# We need a newish Rake since Active Job sets its test tasks' descriptions.
-gem 'rake', '>= 10.3'
+gem "rake", ">= 11.1"
-# Active Job depends on the URI::GID::MissingModelIDError, which isn't released yet.
-gem 'globalid', github: 'rails/globalid'
+gem "capybara", ">= 2.15"
-# This needs to be with require false as it is
-# loaded after loading the test library to
-# ensure correct loading order
-gem 'mocha', '~> 0.14', require: false
+gem "rack-cache", "~> 1.2"
+gem "sass-rails"
+gem "turbolinks", "~> 5"
+gem "webpacker", github: "rails/webpacker", require: ENV["SKIP_REQUIRE_WEBPACKER"] != "true"
+# require: false so bcrypt is loaded only when has_secure_password is used.
+# This is to avoid Active Model (and by extension the entire framework)
+# being dependent on a binary library.
+gem "bcrypt", "~> 3.1.11", require: false
-gem 'rack-cache', '~> 1.2'
-gem 'jquery-rails', github: 'rails/jquery-rails', branch: 'master'
-gem 'coffee-rails', '~> 4.1.0'
-gem 'turbolinks'
-gem 'arel', github: 'rails/arel', branch: 'master'
-gem 'mail', github: 'mikel/mail'
+# This needs to be with require false to avoid it being automatically loaded by
+# sprockets.
+gem "uglifier", ">= 1.3.0", require: false
-gem 'sprockets', '~> 3.0.0.rc.1'
-gem 'sprockets-rails', github: 'rails/sprockets-rails', branch: 'master'
+# Explicitly avoid 1.x that doesn't support Ruby 2.4+
+gem "json", ">= 2.0.0"
-# require: false so bcrypt is loaded only when has_secure_password is used.
-# This is to avoid ActiveModel (and by extension the entire framework)
-# being dependent on a binary library.
-gem 'bcrypt', '~> 3.1.10', require: false
+gem "rubocop", ">= 0.47", require: false
-# This needs to be with require false to avoid
-# it being automatically loaded by sprockets
-gem 'uglifier', '>= 1.3.0', require: false
+# https://github.com/guard/rb-inotify/pull/79
+gem "rb-inotify", github: "matthewd/rb-inotify", branch: "close-handling", require: false
group :doc do
- gem 'sdoc', '~> 0.4.0'
- gem 'redcarpet', '~> 3.2.3', platforms: :ruby
- gem 'w3c_validators'
- gem 'kindlerb', '0.1.1'
+ gem "sdoc", "~> 1.0"
+ gem "redcarpet", "~> 3.2.3", platforms: :ruby
+ gem "w3c_validators"
+ gem "kindlerb", "~> 1.2.0"
end
-# ActiveSupport
-gem 'dalli', '>= 2.2.1'
+# Active Support.
+gem "dalli"
+gem "listen", ">= 3.0.5", "< 3.2", require: false
+gem "libxml-ruby", platforms: :ruby
+gem "connection_pool", require: false
+
+# for railties app_generator_test
+gem "bootsnap", ">= 1.1.0", require: false
-# ActiveJob
+# Active Job.
group :job do
- gem 'resque', require: false
- gem 'resque-scheduler', require: false
- gem 'sidekiq', require: false
- gem 'sucker_punch', require: false
- gem 'delayed_job', require: false
- gem 'queue_classic', require: false, platforms: :ruby
- gem 'sneakers', require: false
- gem 'que', require: false
- gem 'backburner', require: false
- gem 'qu-rails', github: "bkeepers/qu", branch: "master", require: false
- gem 'qu-redis', require: false
- gem 'delayed_job_active_record', require: false
- gem 'sequel', require: false
+ gem "resque", require: false
+ gem "resque-scheduler", require: false
+ gem "sidekiq", require: false
+ gem "sucker_punch", require: false
+ gem "delayed_job", require: false
+ gem "queue_classic", github: "rafaelfranca/queue_classic", branch: "update-pg", require: false, platforms: :ruby
+ gem "sneakers", require: false
+ gem "que", require: false
+ gem "backburner", require: false
+ gem "delayed_job_active_record", require: false
+ gem "sequel", require: false
+end
+
+# Action Cable
+group :cable do
+ gem "puma", require: false
+
+ gem "hiredis", require: false
+ gem "redis", "~> 4.0", require: false
+
+ gem "redis-namespace"
+
+ gem "websocket-client-simple", github: "matthewd/websocket-client-simple", branch: "close-race", require: false
+
+ gem "blade", require: false, platforms: [:ruby]
+ gem "blade-sauce_labs_plugin", require: false, platforms: [:ruby]
+ gem "sprockets-export", require: false
+end
+
+# Active Storage
+group :storage do
+ gem "aws-sdk-s3", require: false
+ gem "google-cloud-storage", "~> 1.11", require: false
+ gem "azure-storage", require: false
+
+ gem "image_processing", "~> 1.2"
+end
+
+group :ujs do
+ gem "qunit-selenium"
+ gem "chromedriver-helper"
end
-# Add your own local bundler stuff
-local_gemfile = File.dirname(__FILE__) + "/.Gemfile"
+# Add your own local bundler stuff.
+local_gemfile = File.expand_path(".Gemfile", __dir__)
instance_eval File.read local_gemfile if File.exist? local_gemfile
group :test do
- # FIX: Our test suite isn't ready to run in random order yet
- gem 'minitest', '< 5.3.4'
+ gem "minitest-bisect"
+ gem "minitest-retry"
platforms :mri do
- gem 'stackprof'
- gem 'byebug'
+ gem "stackprof"
+ gem "byebug"
end
- gem 'benchmark-ips'
+ gem "benchmark-ips"
end
-platforms :ruby do
- gem 'nokogiri', '>= 1.4.5'
+platforms :ruby, :mswin, :mswin64, :mingw, :x64_mingw do
+ gem "nokogiri", ">= 1.8.1"
- # Needed for compiling the ActionDispatch::Journey parser
- gem 'racc', '>=1.4.6', require: false
+ # Needed for compiling the ActionDispatch::Journey parser.
+ gem "racc", ">=1.4.6", require: false
- # ActiveRecord
- gem 'sqlite3', '~> 1.3.6'
+ # Active Record.
+ gem "sqlite3", "~> 1.3.6"
group :db do
- gem 'pg', '>= 0.18.0'
- gem 'mysql', '>= 2.9.0'
- gem 'mysql2', '>= 0.3.18'
+ gem "pg", ">= 0.18.0"
+ gem "mysql2", ">= 0.4.10"
end
end
platforms :jruby do
- gem 'json'
- if ENV['AR_JDBC']
- gem 'activerecord-jdbcsqlite3-adapter', github: 'jruby/activerecord-jdbc-adapter', branch: 'master'
+ if ENV["AR_JDBC"]
+ gem "activerecord-jdbcsqlite3-adapter", github: "jruby/activerecord-jdbc-adapter", branch: "master"
group :db do
- gem 'activerecord-jdbcmysql-adapter', github: 'jruby/activerecord-jdbc-adapter', branch: 'master'
- gem 'activerecord-jdbcpostgresql-adapter', github: 'jruby/activerecord-jdbc-adapter', branch: 'master'
+ gem "activerecord-jdbcmysql-adapter", github: "jruby/activerecord-jdbc-adapter", branch: "master"
+ gem "activerecord-jdbcpostgresql-adapter", github: "jruby/activerecord-jdbc-adapter", branch: "master"
end
else
- gem 'activerecord-jdbcsqlite3-adapter', '>= 1.3.0'
+ gem "activerecord-jdbcsqlite3-adapter", ">= 1.3.0"
group :db do
- gem 'activerecord-jdbcmysql-adapter', '>= 1.3.0'
- gem 'activerecord-jdbcpostgresql-adapter', '>= 1.3.0'
+ gem "activerecord-jdbcmysql-adapter", ">= 1.3.0"
+ gem "activerecord-jdbcpostgresql-adapter", ">= 1.3.0"
end
end
end
platforms :rbx do
- # The rubysl-yaml gem doesn't ship with Psych by default
- # as it needs libyaml that isn't always available.
- gem 'psych', '~> 2.0'
+ # The rubysl-yaml gem doesn't ship with Psych by default as it needs
+ # libyaml that isn't always available.
+ gem "psych", "~> 3.0"
end
-# gems that are necessary for ActiveRecord tests with Oracle database
-if ENV['ORACLE_ENHANCED']
+# Gems that are necessary for Active Record tests with Oracle.
+if ENV["ORACLE_ENHANCED"]
platforms :ruby do
- gem 'ruby-oci8', '~> 2.1'
+ gem "ruby-oci8", "~> 2.2"
end
- gem 'activerecord-oracle_enhanced-adapter', github: 'rsim/oracle-enhanced', branch: 'master'
+ gem "activerecord-oracle_enhanced-adapter", github: "rsim/oracle-enhanced", branch: "master"
end
-# A gem necessary for ActiveRecord tests with IBM DB
-gem 'ibm_db' if ENV['IBM_DB']
+# A gem necessary for Active Record tests with IBM DB.
+gem "ibm_db" if ENV["IBM_DB"]
+gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby]
+gem "wdm", ">= 0.1.0", platforms: [:mingw, :mswin, :x64_mingw, :mswin64]
diff --git a/Gemfile.lock b/Gemfile.lock
index 960a77af92..f4f3e430ba 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,265 +1,509 @@
GIT
- remote: git://github.com/bkeepers/qu.git
- revision: d098e2657c92e89a6413bebd9c033930759c061f
- branch: master
+ remote: https://github.com/matthewd/rb-inotify.git
+ revision: 856730aad4b285969e8dd621e44808a7c5af4242
+ branch: close-handling
specs:
- qu (0.2.0)
- qu-rails (0.2.0)
- qu (= 0.2.0)
- railties (>= 3.2, < 5)
- qu-redis (0.2.0)
- qu (= 0.2.0)
- redis-namespace
+ rb-inotify (0.9.9)
+ ffi (~> 1.0)
GIT
- remote: git://github.com/mikel/mail.git
- revision: b159e0a542962fdd5e292a48cfffa560d7cf412e
+ remote: https://github.com/matthewd/websocket-client-simple.git
+ revision: e161305f1a466b9398d86df3b1731b03362da91b
+ branch: close-race
specs:
- mail (2.6.3.edge)
- mime-types (>= 1.16, < 3)
+ websocket-client-simple (0.3.0)
+ event_emitter
+ websocket
GIT
- remote: git://github.com/rails/arel.git
- revision: aac9da257f291ad8d2d4f914528881c240848bb2
- branch: master
+ remote: https://github.com/rafaelfranca/queue_classic.git
+ revision: dee64b361355d56700ad7aa3b151bf653a617526
+ branch: update-pg
specs:
- arel (7.0.0.alpha)
+ queue_classic (3.2.0.RC1)
+ pg (>= 0.17, < 2.0)
GIT
- remote: git://github.com/rails/globalid.git
- revision: 4df66fb9e9f0c832d29119aa8bc30be55a614b71
+ remote: https://github.com/rails/webpacker.git
+ revision: bb132d591da35095e3246082cba3d693f847e0b5
specs:
- globalid (0.3.5)
- activesupport (>= 4.1.0)
-
-GIT
- remote: git://github.com/rails/jquery-rails.git
- revision: 272abdd319bb3381b23182b928b25320590096b0
- branch: master
- specs:
- jquery-rails (4.0.3)
- rails-dom-testing (~> 1.0)
- railties (>= 4.2.0)
- thor (>= 0.14, < 2.0)
-
-GIT
- remote: git://github.com/rails/sprockets-rails.git
- revision: 85b89c44ad40af3056899808475e6e4bf65c1f5a
- branch: master
- specs:
- sprockets-rails (3.0.0.beta1)
- actionpack (>= 4.0)
- activesupport (>= 4.0)
- sprockets (>= 3.0.0, < 4.0)
+ webpacker (4.0.0.pre.3)
+ activesupport (>= 4.2)
+ rack-proxy (>= 0.6.1)
+ railties (>= 4.2)
PATH
remote: .
specs:
- actionmailer (5.0.0.alpha)
- actionpack (= 5.0.0.alpha)
- actionview (= 5.0.0.alpha)
- activejob (= 5.0.0.alpha)
+ actioncable (6.0.0.alpha)
+ actionpack (= 6.0.0.alpha)
+ nio4r (~> 2.0)
+ websocket-driver (>= 0.6.1)
+ actionmailer (6.0.0.alpha)
+ actionpack (= 6.0.0.alpha)
+ actionview (= 6.0.0.alpha)
+ activejob (= 6.0.0.alpha)
mail (~> 2.5, >= 2.5.4)
- rails-dom-testing (~> 1.0, >= 1.0.5)
- actionpack (5.0.0.alpha)
- actionview (= 5.0.0.alpha)
- activesupport (= 5.0.0.alpha)
- rack (~> 1.6)
- rack-test (~> 0.6.3)
- rails-dom-testing (~> 1.0, >= 1.0.5)
- rails-html-sanitizer (~> 1.0, >= 1.0.2)
- actionview (5.0.0.alpha)
- activesupport (= 5.0.0.alpha)
- builder (~> 3.1)
- erubis (~> 2.7.0)
- rails-dom-testing (~> 1.0, >= 1.0.5)
+ rails-dom-testing (~> 2.0)
+ actionpack (6.0.0.alpha)
+ actionview (= 6.0.0.alpha)
+ activesupport (= 6.0.0.alpha)
+ rack (~> 2.0)
+ rack-test (>= 0.6.3)
+ rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
- activejob (5.0.0.alpha)
- activesupport (= 5.0.0.alpha)
- globalid (>= 0.3.0)
- activemodel (5.0.0.alpha)
- activesupport (= 5.0.0.alpha)
+ actionview (6.0.0.alpha)
+ activesupport (= 6.0.0.alpha)
builder (~> 3.1)
- activerecord (5.0.0.alpha)
- activemodel (= 5.0.0.alpha)
- activesupport (= 5.0.0.alpha)
- arel (= 7.0.0.alpha)
- activesupport (5.0.0.alpha)
- i18n (~> 0.7)
- json (~> 1.7, >= 1.7.7)
+ erubi (~> 1.4)
+ rails-dom-testing (~> 2.0)
+ rails-html-sanitizer (~> 1.0, >= 1.0.3)
+ activejob (6.0.0.alpha)
+ activesupport (= 6.0.0.alpha)
+ globalid (>= 0.3.6)
+ activemodel (6.0.0.alpha)
+ activesupport (= 6.0.0.alpha)
+ activerecord (6.0.0.alpha)
+ activemodel (= 6.0.0.alpha)
+ activesupport (= 6.0.0.alpha)
+ activestorage (6.0.0.alpha)
+ actionpack (= 6.0.0.alpha)
+ activerecord (= 6.0.0.alpha)
+ marcel (~> 0.3.1)
+ activesupport (6.0.0.alpha)
+ concurrent-ruby (~> 1.0, >= 1.0.2)
+ i18n (>= 0.7, < 2)
minitest (~> 5.1)
- thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
- rails (5.0.0.alpha)
- actionmailer (= 5.0.0.alpha)
- actionpack (= 5.0.0.alpha)
- actionview (= 5.0.0.alpha)
- activejob (= 5.0.0.alpha)
- activemodel (= 5.0.0.alpha)
- activerecord (= 5.0.0.alpha)
- activesupport (= 5.0.0.alpha)
- bundler (>= 1.3.0, < 2.0)
- railties (= 5.0.0.alpha)
- sprockets-rails
- railties (5.0.0.alpha)
- actionpack (= 5.0.0.alpha)
- activesupport (= 5.0.0.alpha)
+ rails (6.0.0.alpha)
+ actioncable (= 6.0.0.alpha)
+ actionmailer (= 6.0.0.alpha)
+ actionpack (= 6.0.0.alpha)
+ actionview (= 6.0.0.alpha)
+ activejob (= 6.0.0.alpha)
+ activemodel (= 6.0.0.alpha)
+ activerecord (= 6.0.0.alpha)
+ activestorage (= 6.0.0.alpha)
+ activesupport (= 6.0.0.alpha)
+ bundler (>= 1.3.0)
+ railties (= 6.0.0.alpha)
+ sprockets-rails (>= 2.0.0)
+ railties (6.0.0.alpha)
+ actionpack (= 6.0.0.alpha)
+ activesupport (= 6.0.0.alpha)
method_source
rake (>= 0.8.7)
- thor (>= 0.18.1, < 2.0)
+ thor (>= 0.19.0, < 2.0)
GEM
remote: https://rubygems.org/
specs:
- amq-protocol (1.9.2)
- backburner (0.4.6)
- beaneater (~> 0.3.1)
- dante (~> 0.1.5)
- bcrypt (3.1.10)
- bcrypt (3.1.10-x64-mingw32)
- bcrypt (3.1.10-x86-mingw32)
- beaneater (0.3.3)
- benchmark-ips (2.1.1)
- builder (3.2.2)
- bunny (1.7.0)
- amq-protocol (>= 1.9.2)
- byebug (4.0.5)
- columnize (= 0.9.0)
- celluloid (0.16.0)
- timers (~> 4.0.0)
- coffee-rails (4.1.0)
- coffee-script (>= 2.2.0)
- railties (>= 4.0.0, < 5.0)
- coffee-script (2.3.0)
+ activerecord-jdbc-adapter (52.1-java)
+ activerecord (~> 5.2.0)
+ activerecord-jdbcmysql-adapter (52.1-java)
+ activerecord-jdbc-adapter (= 52.1)
+ jdbc-mysql (~> 5.1.36)
+ activerecord-jdbcpostgresql-adapter (52.1-java)
+ activerecord-jdbc-adapter (= 52.1)
+ jdbc-postgres (>= 9.4, < 43)
+ activerecord-jdbcsqlite3-adapter (52.1-java)
+ activerecord-jdbc-adapter (= 52.1)
+ jdbc-sqlite3 (~> 3.8, < 3.30)
+ addressable (2.5.2)
+ public_suffix (>= 2.0.2, < 4.0)
+ amq-protocol (2.3.0)
+ archive-zip (0.11.0)
+ io-like (~> 0.3.0)
+ ast (2.4.0)
+ aws-eventstream (1.0.1)
+ aws-partitions (1.111.0)
+ aws-sdk-core (3.37.0)
+ aws-eventstream (~> 1.0)
+ aws-partitions (~> 1.0)
+ aws-sigv4 (~> 1.0)
+ jmespath (~> 1.0)
+ aws-sdk-kms (1.11.0)
+ aws-sdk-core (~> 3, >= 3.26.0)
+ aws-sigv4 (~> 1.0)
+ aws-sdk-s3 (1.23.1)
+ aws-sdk-core (~> 3, >= 3.26.0)
+ aws-sdk-kms (~> 1)
+ aws-sigv4 (~> 1.0)
+ aws-sigv4 (1.0.3)
+ azure-core (0.1.14)
+ faraday (~> 0.9)
+ faraday_middleware (~> 0.10)
+ nokogiri (~> 1.6)
+ azure-storage (0.15.0.preview)
+ azure-core (~> 0.1)
+ faraday (~> 0.9)
+ faraday_middleware (~> 0.10)
+ nokogiri (~> 1.6, >= 1.6.8)
+ backburner (1.5.0)
+ beaneater (~> 1.0)
+ concurrent-ruby (~> 1.0, >= 1.0.1)
+ dante (> 0.1.5)
+ bcrypt (3.1.12)
+ bcrypt (3.1.12-java)
+ bcrypt (3.1.12-x64-mingw32)
+ bcrypt (3.1.12-x86-mingw32)
+ beaneater (1.0.0)
+ benchmark-ips (2.7.2)
+ blade (0.7.1)
+ activesupport (>= 3.0.0)
+ blade-qunit_adapter (~> 2.0.1)
+ coffee-script
+ coffee-script-source
+ curses (~> 1.0.0)
+ eventmachine
+ faye
+ sprockets (>= 3.0)
+ thin (>= 1.6.0)
+ thor (>= 0.19.1)
+ useragent (~> 0.16.7)
+ blade-qunit_adapter (2.0.1)
+ blade-sauce_labs_plugin (0.7.3)
+ childprocess
+ faraday
+ selenium-webdriver
+ bootsnap (1.3.2)
+ msgpack (~> 1.0)
+ bootsnap (1.3.2-java)
+ msgpack (~> 1.0)
+ builder (3.2.3)
+ bunny (2.9.2)
+ amq-protocol (~> 2.3.0)
+ byebug (10.0.2)
+ capybara (3.10.1)
+ addressable
+ mini_mime (>= 0.1.3)
+ nokogiri (~> 1.8)
+ rack (>= 1.6.0)
+ rack-test (>= 0.6.3)
+ regexp_parser (~> 1.2)
+ xpath (~> 3.2)
+ childprocess (0.9.0)
+ ffi (~> 1.0, >= 1.0.11)
+ chromedriver-helper (2.1.0)
+ archive-zip (~> 0.10)
+ nokogiri (~> 1.8)
+ coffee-script (2.4.1)
coffee-script-source
execjs
- coffee-script-source (1.9.0)
- columnize (0.9.0)
- connection_pool (2.1.1)
- dalli (2.7.2)
- dante (0.1.5)
- delayed_job (4.0.6)
- activesupport (>= 3.0, < 5.0)
- delayed_job_active_record (4.0.3)
- activerecord (>= 3.0, < 5.0)
- delayed_job (>= 3.0, < 4.1)
- erubis (2.7.0)
- execjs (2.3.0)
- hitimes (1.2.2)
- hitimes (1.2.2-x86-mingw32)
- i18n (0.7.0)
- json (1.8.2)
- kindlerb (0.1.1)
+ coffee-script-source (1.12.2)
+ concurrent-ruby (1.1.3)
+ connection_pool (2.2.2)
+ cookiejar (0.3.3)
+ crass (1.0.4)
+ curses (1.0.2)
+ daemons (1.2.6)
+ dalli (2.7.9)
+ dante (0.2.0)
+ declarative (0.0.10)
+ declarative-option (0.1.0)
+ delayed_job (4.1.5)
+ activesupport (>= 3.0, < 5.3)
+ delayed_job_active_record (4.1.3)
+ activerecord (>= 3.0, < 5.3)
+ delayed_job (>= 3.0, < 5)
+ digest-crc (0.4.1)
+ em-http-request (1.1.5)
+ addressable (>= 2.3.4)
+ cookiejar (!= 0.3.1)
+ em-socksify (>= 0.3)
+ eventmachine (>= 1.0.3)
+ http_parser.rb (>= 0.6.0)
+ em-socksify (0.3.2)
+ eventmachine (>= 1.0.0.beta.4)
+ erubi (1.7.1)
+ et-orbi (1.1.6)
+ tzinfo
+ event_emitter (0.2.6)
+ eventmachine (1.2.7)
+ execjs (2.7.0)
+ faraday (0.15.3)
+ multipart-post (>= 1.2, < 3)
+ faraday_middleware (0.12.2)
+ faraday (>= 0.7.4, < 1.0)
+ faye (1.2.4)
+ cookiejar (>= 0.3.0)
+ em-http-request (>= 0.3.0)
+ eventmachine (>= 0.12.0)
+ faye-websocket (>= 0.9.1)
+ multi_json (>= 1.0.0)
+ rack (>= 1.0.0)
+ websocket-driver (>= 0.5.1)
+ faye-websocket (0.10.7)
+ eventmachine (>= 0.12.0)
+ websocket-driver (>= 0.5.1)
+ ffi (1.9.25)
+ ffi (1.9.25-java)
+ ffi (1.9.25-x64-mingw32)
+ ffi (1.9.25-x86-mingw32)
+ fugit (1.1.6)
+ et-orbi (~> 1.1, >= 1.1.6)
+ raabro (~> 1.1)
+ globalid (0.4.1)
+ activesupport (>= 4.2.0)
+ google-api-client (0.25.0)
+ addressable (~> 2.5, >= 2.5.1)
+ googleauth (>= 0.5, < 0.7.0)
+ httpclient (>= 2.8.1, < 3.0)
+ mime-types (~> 3.0)
+ representable (~> 3.0)
+ retriable (>= 2.0, < 4.0)
+ signet (~> 0.10)
+ google-cloud-core (1.2.7)
+ google-cloud-env (~> 1.0)
+ google-cloud-env (1.0.5)
+ faraday (~> 0.11)
+ google-cloud-storage (1.15.0)
+ digest-crc (~> 0.4)
+ google-api-client (~> 0.23)
+ google-cloud-core (~> 1.2)
+ googleauth (~> 0.6.2)
+ googleauth (0.6.7)
+ faraday (~> 0.12)
+ jwt (>= 1.4, < 3.0)
+ memoist (~> 0.16)
+ multi_json (~> 1.11)
+ os (>= 0.9, < 2.0)
+ signet (~> 0.7)
+ hiredis (0.6.3)
+ hiredis (0.6.3-java)
+ http_parser.rb (0.6.0)
+ httpclient (2.8.3)
+ i18n (1.1.1)
+ concurrent-ruby (~> 1.0)
+ image_processing (1.7.1)
+ mini_magick (~> 4.0)
+ ruby-vips (>= 2.0.13, < 3)
+ io-like (0.3.0)
+ jaro_winkler (1.5.1)
+ jaro_winkler (1.5.1-java)
+ jdbc-mysql (5.1.46)
+ jdbc-postgres (42.1.4)
+ jdbc-sqlite3 (3.20.1)
+ jmespath (1.4.0)
+ json (2.1.0)
+ json (2.1.0-java)
+ jwt (2.1.0)
+ kindlerb (1.2.0)
mustache
nokogiri
- loofah (2.0.1)
+ libxml-ruby (3.1.0)
+ listen (3.1.5)
+ rb-fsevent (~> 0.9, >= 0.9.4)
+ rb-inotify (~> 0.9, >= 0.9.7)
+ ruby_dep (~> 1.2)
+ loofah (2.2.3)
+ crass (~> 1.0.2)
nokogiri (>= 1.5.9)
- metaclass (0.0.4)
- method_source (0.8.2)
- mime-types (2.4.3)
- mini_portile (0.6.2)
- minitest (5.3.3)
- mocha (0.14.0)
- metaclass (~> 0.0.1)
+ mail (2.7.1)
+ mini_mime (>= 0.1.1)
+ marcel (0.3.3)
+ mimemagic (~> 0.3.2)
+ memoist (0.16.0)
+ method_source (0.9.1)
+ mime-types (3.2.2)
+ mime-types-data (~> 3.2015)
+ mime-types-data (3.2018.0812)
+ mimemagic (0.3.2)
+ mini_magick (4.9.2)
+ mini_mime (1.0.1)
+ mini_portile2 (2.3.0)
+ minitest (5.11.3)
+ minitest-bisect (1.4.0)
+ minitest-server (~> 1.0)
+ path_expander (~> 1.0)
+ minitest-retry (0.1.9)
+ minitest (>= 5.0)
+ minitest-server (1.0.5)
+ minitest (~> 5.0)
mono_logger (1.1.0)
- multi_json (1.11.0)
- mustache (1.0.0)
- mysql (2.9.1)
- mysql2 (0.3.18)
- nokogiri (1.6.6.2)
- mini_portile (~> 0.6.0)
- nokogiri (1.6.6.2-x64-mingw32)
- mini_portile (~> 0.6.0)
- nokogiri (1.6.6.2-x86-mingw32)
- mini_portile (~> 0.6.0)
- pg (0.18.1)
- psych (2.0.13)
- que (0.9.2)
- queue_classic (3.1.0)
- pg (>= 0.17, < 0.19)
- racc (1.4.12)
- rack (1.6.0)
- rack-cache (1.2)
+ msgpack (1.2.4)
+ msgpack (1.2.4-java)
+ msgpack (1.2.4-x64-mingw32)
+ msgpack (1.2.4-x86-mingw32)
+ multi_json (1.13.1)
+ multipart-post (2.0.0)
+ mustache (1.1.0)
+ mustermann (1.0.3)
+ mysql2 (0.5.2)
+ mysql2 (0.5.2-x64-mingw32)
+ mysql2 (0.5.2-x86-mingw32)
+ nio4r (2.3.1)
+ nio4r (2.3.1-java)
+ nokogiri (1.8.5)
+ mini_portile2 (~> 2.3.0)
+ nokogiri (1.8.5-java)
+ nokogiri (1.8.5-x64-mingw32)
+ mini_portile2 (~> 2.3.0)
+ nokogiri (1.8.5-x86-mingw32)
+ mini_portile2 (~> 2.3.0)
+ os (1.0.0)
+ parallel (1.12.1)
+ parser (2.5.3.0)
+ ast (~> 2.4.0)
+ path_expander (1.0.3)
+ pg (1.1.3)
+ pg (1.1.3-x64-mingw32)
+ pg (1.1.3-x86-mingw32)
+ powerpack (0.1.2)
+ psych (3.0.3)
+ public_suffix (3.0.3)
+ puma (3.12.0)
+ puma (3.12.0-java)
+ que (0.14.3)
+ qunit-selenium (0.0.4)
+ selenium-webdriver
+ thor
+ raabro (1.1.6)
+ racc (1.4.14)
+ rack (2.0.6)
+ rack-cache (1.8.0)
rack (>= 0.4)
- rack-protection (1.5.3)
+ rack-protection (2.0.4)
+ rack
+ rack-proxy (0.6.5)
rack
- rack-test (0.6.3)
- rack (>= 1.0)
- rails-deprecated_sanitizer (1.0.3)
- activesupport (>= 4.2.0.alpha)
- rails-dom-testing (1.0.6)
- activesupport (>= 4.2.0.beta, < 5.0)
- nokogiri (~> 1.6.0)
- rails-deprecated_sanitizer (>= 1.0.1)
- rails-html-sanitizer (1.0.2)
- loofah (~> 2.0)
- rake (10.4.2)
- rdoc (4.2.0)
+ rack-test (1.1.0)
+ rack (>= 1.0, < 3)
+ rails-dom-testing (2.0.3)
+ activesupport (>= 4.2.0)
+ nokogiri (>= 1.6)
+ rails-html-sanitizer (1.0.4)
+ loofah (~> 2.2, >= 2.2.2)
+ rainbow (3.0.0)
+ rake (12.3.1)
+ rb-fsevent (0.10.3)
+ rdoc (6.0.4)
redcarpet (3.2.3)
- redis (3.2.1)
- redis-namespace (1.5.1)
- redis (~> 3.0, >= 3.0.4)
- resque (1.25.2)
+ redis (4.0.3)
+ redis-namespace (1.6.0)
+ redis (>= 3.0.4)
+ regexp_parser (1.2.0)
+ representable (3.0.4)
+ declarative (< 0.1.0)
+ declarative-option (< 0.2.0)
+ uber (< 0.2.0)
+ resque (1.27.4)
mono_logger (~> 1.0)
multi_json (~> 1.0)
redis-namespace (~> 1.3)
sinatra (>= 0.9.2)
vegas (~> 0.1.2)
- resque-scheduler (4.0.0)
+ resque-scheduler (4.3.1)
mono_logger (~> 1.0)
- redis (~> 3.0)
- resque (~> 1.25)
- rufus-scheduler (~> 3.0)
- rufus-scheduler (3.0.9)
- tzinfo
- sdoc (0.4.1)
- json (~> 1.7, >= 1.7.7)
- rdoc (~> 4.0)
- sequel (4.19.0)
- serverengine (1.5.10)
+ redis (>= 3.3, < 5)
+ resque (~> 1.26)
+ rufus-scheduler (~> 3.2)
+ retriable (3.1.2)
+ rubocop (0.60.0)
+ jaro_winkler (~> 1.5.1)
+ parallel (~> 1.10)
+ parser (>= 2.5, != 2.5.1.1)
+ powerpack (~> 0.1)
+ rainbow (>= 2.2.2, < 4.0)
+ ruby-progressbar (~> 1.7)
+ unicode-display_width (~> 1.4.0)
+ ruby-progressbar (1.10.0)
+ ruby-vips (2.0.13)
+ ffi (~> 1.9)
+ ruby_dep (1.5.0)
+ rubyzip (1.2.2)
+ rufus-scheduler (3.5.2)
+ fugit (~> 1.1, >= 1.1.5)
+ sass (3.7.2)
+ sass-listen (~> 4.0.0)
+ sass-listen (4.0.0)
+ rb-fsevent (~> 0.9, >= 0.9.4)
+ rb-inotify (~> 0.9, >= 0.9.7)
+ sass-rails (5.0.7)
+ railties (>= 4.0.0, < 6)
+ sass (~> 3.1)
+ sprockets (>= 2.8, < 4.0)
+ sprockets-rails (>= 2.0, < 4.0)
+ tilt (>= 1.1, < 3)
+ sdoc (1.0.0)
+ rdoc (>= 5.0)
+ selenium-webdriver (3.141.0)
+ childprocess (~> 0.5)
+ rubyzip (~> 1.2, >= 1.2.2)
+ sequel (5.14.0)
+ serverengine (2.0.7)
sigdump (~> 0.2.2)
- sidekiq (3.3.2)
- celluloid (>= 0.16.0)
- connection_pool (>= 2.1.1)
- json
- redis (>= 3.0.6)
- redis-namespace (>= 1.3.1)
- sigdump (0.2.2)
- sinatra (1.4.5)
- rack (~> 1.4)
- rack-protection (~> 1.4)
- tilt (~> 1.3, >= 1.3.4)
- sneakers (1.0.4)
- bunny (~> 1.7.0)
- serverengine (~> 1.5.5)
+ sidekiq (5.2.3)
+ connection_pool (~> 2.2, >= 2.2.2)
+ rack-protection (>= 1.5.0)
+ redis (>= 3.3.5, < 5)
+ sigdump (0.2.4)
+ signet (0.11.0)
+ addressable (~> 2.3)
+ faraday (~> 0.9)
+ jwt (>= 1.5, < 3.0)
+ multi_json (~> 1.10)
+ sinatra (2.0.4)
+ mustermann (~> 1.0)
+ rack (~> 2.0)
+ rack-protection (= 2.0.4)
+ tilt (~> 2.0)
+ sneakers (2.7.0)
+ bunny (~> 2.9.2)
+ concurrent-ruby (~> 1.0)
+ serverengine (~> 2.0.5)
thor
- thread (~> 0.1.7)
- sprockets (3.0.2)
- rack (~> 1.0)
- sqlite3 (1.3.10)
- stackprof (0.2.7)
- sucker_punch (1.3.2)
- celluloid (~> 0.16.0)
- thor (0.19.1)
- thread (0.1.7)
- thread_safe (0.3.5)
- tilt (1.4.1)
- timers (4.0.1)
- hitimes
- turbolinks (2.5.3)
- coffee-rails
- tzinfo (1.2.2)
+ sprockets (3.7.2)
+ concurrent-ruby (~> 1.0)
+ rack (> 1, < 3)
+ sprockets-export (1.0.0)
+ sprockets-rails (3.2.1)
+ actionpack (>= 4.0)
+ activesupport (>= 4.0)
+ sprockets (>= 3.0.0)
+ sqlite3 (1.3.13)
+ sqlite3 (1.3.13-x64-mingw32)
+ sqlite3 (1.3.13-x86-mingw32)
+ stackprof (0.2.12)
+ sucker_punch (2.1.1)
+ concurrent-ruby (~> 1.0)
+ thin (1.7.2)
+ daemons (~> 1.0, >= 1.0.9)
+ eventmachine (~> 1.0, >= 1.0.4)
+ rack (>= 1, < 3)
+ thor (0.20.3)
+ thread_safe (0.3.6)
+ thread_safe (0.3.6-java)
+ tilt (2.0.8)
+ turbolinks (5.2.0)
+ turbolinks-source (~> 5.2)
+ turbolinks-source (5.2.0)
+ tzinfo (1.2.5)
thread_safe (~> 0.1)
- uglifier (2.7.0)
- execjs (>= 0.3.0)
- json (>= 1.8.0)
+ tzinfo-data (1.2018.7)
+ tzinfo (>= 1.0.0)
+ uber (0.1.0)
+ uglifier (4.1.19)
+ execjs (>= 0.3.0, < 3)
+ unicode-display_width (1.4.0)
+ useragent (0.16.10)
vegas (0.1.11)
rack (>= 1.0.0)
- w3c_validators (1.2)
- json
- nokogiri
+ w3c_validators (1.3.4)
+ json (>= 1.8)
+ nokogiri (~> 1.6)
+ wdm (0.1.1)
+ websocket (1.2.8)
+ websocket-driver (0.7.0)
+ websocket-extensions (>= 0.1.0)
+ websocket-driver (0.7.0-java)
+ websocket-extensions (>= 0.1.0)
+ websocket-extensions (0.1.3)
+ xpath (3.2.0)
+ nokogiri (~> 1.8)
PLATFORMS
+ java
ruby
x64-mingw32
x86-mingw32
@@ -268,50 +512,65 @@ DEPENDENCIES
activerecord-jdbcmysql-adapter (>= 1.3.0)
activerecord-jdbcpostgresql-adapter (>= 1.3.0)
activerecord-jdbcsqlite3-adapter (>= 1.3.0)
- arel!
+ aws-sdk-s3
+ azure-storage
backburner
- bcrypt (~> 3.1.10)
+ bcrypt (~> 3.1.11)
benchmark-ips
+ blade
+ blade-sauce_labs_plugin
+ bootsnap (>= 1.1.0)
byebug
- coffee-rails (~> 4.1.0)
- dalli (>= 2.2.1)
+ capybara (>= 2.15)
+ chromedriver-helper
+ connection_pool
+ dalli
delayed_job
delayed_job_active_record
- globalid!
- jquery-rails!
- json
- kindlerb (= 0.1.1)
- mail!
- minitest (< 5.3.4)
- mocha (~> 0.14)
- mysql (>= 2.9.0)
- mysql2 (>= 0.3.18)
- nokogiri (>= 1.4.5)
+ google-cloud-storage (~> 1.11)
+ hiredis
+ image_processing (~> 1.2)
+ json (>= 2.0.0)
+ kindlerb (~> 1.2.0)
+ libxml-ruby
+ listen (>= 3.0.5, < 3.2)
+ minitest-bisect
+ minitest-retry
+ mysql2 (>= 0.4.10)
+ nokogiri (>= 1.8.1)
pg (>= 0.18.0)
- psych (~> 2.0)
- qu-rails!
- qu-redis
+ psych (~> 3.0)
+ puma
que
- queue_classic
+ queue_classic!
+ qunit-selenium
racc (>= 1.4.6)
rack-cache (~> 1.2)
rails!
- rake (>= 10.3)
+ rake (>= 11.1)
+ rb-inotify!
redcarpet (~> 3.2.3)
+ redis (~> 4.0)
+ redis-namespace
resque
resque-scheduler
- sdoc (~> 0.4.0)
+ rubocop (>= 0.47)
+ sass-rails
+ sdoc (~> 1.0)
sequel
sidekiq
sneakers
- sprockets (~> 3.0.0.rc.1)
- sprockets-rails!
+ sprockets-export
sqlite3 (~> 1.3.6)
stackprof
sucker_punch
- turbolinks
+ turbolinks (~> 5)
+ tzinfo-data
uglifier (>= 1.3.0)
w3c_validators
+ wdm (>= 0.1.0)
+ webpacker!
+ websocket-client-simple!
BUNDLED WITH
- 1.10.5
+ 1.17.1
diff --git a/MIT-LICENSE b/MIT-LICENSE
new file mode 100644
index 0000000000..8f769c0767
--- /dev/null
+++ b/MIT-LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2005-2018 David Heinemeier Hansson
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"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/RAILS_VERSION b/RAILS_VERSION
index 2b915d7d5c..747766c587 100644
--- a/RAILS_VERSION
+++ b/RAILS_VERSION
@@ -1 +1 @@
-5.0.0.alpha
+6.0.0.alpha
diff --git a/README.md b/README.md
index bbe3a6731d..461e8cea78 100644
--- a/README.md
+++ b/README.md
@@ -1,44 +1,55 @@
-## Welcome to Rails
+# Welcome to Rails
+
+## What's Rails
Rails is a web-application framework that includes everything needed to
create database-backed web applications according to the
-[Model-View-Controller (MVC)](http://en.wikipedia.org/wiki/Model-view-controller)
+[Model-View-Controller (MVC)](https://en.wikipedia.org/wiki/Model-view-controller)
pattern.
Understanding the MVC pattern is key to understanding Rails. MVC divides your
-application into three layers, each with a specific responsibility.
+application into three layers: Model, View, and Controller, each with a specific responsibility.
+
+## Model layer
-The _Model layer_ represents your domain model (such as Account, Product,
-Person, Post, etc.) and encapsulates the business logic that is specific to
+The _**Model layer**_ represents the domain model (such as Account, Product,
+Person, Post, etc.) and encapsulates the business logic specific to
your application. In Rails, database-backed model classes are derived from
-`ActiveRecord::Base`. Active Record allows you to present the data from
+`ActiveRecord::Base`. [Active Record](activerecord/README.rdoc) allows you to present the data from
database rows as objects and embellish these data objects with business logic
-methods. You can read more about Active Record in its [README](activerecord/README.rdoc).
+methods.
Although most Rails models are backed by a database, models can also be ordinary
Ruby classes, or Ruby classes that implement a set of interfaces as provided by
-the Active Model module. You can read more about Active Model in its [README](activemodel/README.rdoc).
+the [Active Model](activemodel/README.rdoc) module.
-The _Controller layer_ is responsible for handling incoming HTTP requests and
+## Controller layer
+
+The _**Controller layer**_ is responsible for handling incoming HTTP requests and
providing a suitable response. Usually this means returning HTML, but Rails controllers
can also generate XML, JSON, PDFs, mobile-specific views, and more. Controllers load and
manipulate models, and render view templates in order to generate the appropriate HTTP response.
In Rails, incoming requests are routed by Action Dispatch to an appropriate controller, and
controller classes are derived from `ActionController::Base`. Action Dispatch and Action Controller
-are bundled together in Action Pack. You can read more about Action Pack in its
-[README](actionpack/README.rdoc).
+are bundled together in [Action Pack](actionpack/README.rdoc).
+
+## View layer
-The _View layer_ is composed of "templates" that are responsible for providing
+The _**View layer**_ is composed of "templates" that are responsible for providing
appropriate representations of your application's resources. Templates can
come in a variety of formats, but most view templates are HTML with embedded
Ruby code (ERB files). Views are typically rendered to generate a controller response,
-or to generate the body of an email. In Rails, View generation is handled by Action View.
-You can read more about Action View in its [README](actionview/README.rdoc).
-
-Active Record, Action Pack, and Action View can each be used independently outside Rails.
-In addition to them, Rails also comes with Action Mailer ([README](actionmailer/README.rdoc)), a library
-to generate and send emails; Active Job ([README](activejob/README.md)), a
-framework for declaring jobs and making them run on a variety of queueing
-backends; and Active Support ([README](activesupport/README.rdoc)), a collection
+or to generate the body of an email. In Rails, View generation is handled by [Action View](actionview/README.rdoc).
+
+## Frameworks and libraries
+
+[Active Record](activerecord/README.rdoc), [Active Model](activemodel/README.rdoc), [Action Pack](actionpack/README.rdoc), and [Action View](actionview/README.rdoc) can each be used independently outside Rails.
+In addition to that, Rails also comes with [Action Mailer](actionmailer/README.rdoc), a library
+to generate and send emails; [Active Job](activejob/README.md), a
+framework for declaring jobs and making them run on a variety of queuing
+backends; [Action Cable](actioncable/README.md), a framework to
+integrate WebSockets with a Rails application; [Active Storage](activestorage/README.md), a library to attach cloud
+and local files to Rails applications;
+and [Active Support](activesupport/README.rdoc), a collection
of utility classes and standard library extensions that are useful for Rails,
and may also be used independently outside Rails.
@@ -46,35 +57,43 @@ and may also be used independently outside Rails.
1. Install Rails at the command prompt if you haven't yet:
- gem install rails
+ $ gem install rails
2. At the command prompt, create a new Rails application:
- rails new myapp
+ $ rails new myapp
where "myapp" is the application name.
3. Change directory to `myapp` and start the web server:
- cd myapp
- rails server
+ $ cd myapp
+ $ rails server
Run with `--help` or `-h` for options.
-4. Using a browser, go to `http://localhost:3000` and you'll see:
-"Welcome aboard: You're riding Ruby on Rails!"
+4. Go to `http://localhost:3000` and you'll see:
+"Yay! You’re on Rails!"
5. Follow the guidelines to start developing your application. You may find
the following resources handy:
- * [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)
+ * [Getting Started with Rails](https://guides.rubyonrails.org/getting_started.html)
+ * [Ruby on Rails Guides](https://guides.rubyonrails.org)
+ * [The API Documentation](https://api.rubyonrails.org)
+ * [Ruby on Rails Tutorial](https://www.railstutorial.org/book)
## Contributing
+[![Code Triage Badge](https://www.codetriage.com/rails/rails/badges/users.svg)](https://www.codetriage.com/rails/rails)
+
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)
+[Contributing to Ruby on Rails guide](https://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html) for guidelines about how to proceed. [Join us!](https://contributors.rubyonrails.org)
+
+Trying to report a possible security vulnerability in Rails? Please
+check out our [security policy](https://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](https://rubyonrails.org/conduct/).
## Code Status
@@ -82,4 +101,4 @@ We encourage you to contribute to Ruby on Rails! Please check out the
## License
-Ruby on Rails is released under the [MIT License](http://www.opensource.org/licenses/MIT).
+Ruby on Rails is released under the [MIT License](https://opensource.org/licenses/MIT).
diff --git a/RELEASING_RAILS.md b/RELEASING_RAILS.md
new file mode 100644
index 0000000000..287dd4fa12
--- /dev/null
+++ b/RELEASING_RAILS.md
@@ -0,0 +1,225 @@
+# Releasing Rails
+
+In this document, we'll cover the steps necessary to release Rails. Each
+section contains steps to take during that time before the release. The times
+suggested in each header are just that: suggestions. However, they should
+really be considered as minimums.
+
+## 10 Days before release
+
+Today is mostly coordination tasks. Here are the things you must do today:
+
+### Is the CI green? If not, make it green. (See "Fixing the CI")
+
+Do not release with a Red CI. You can find the CI status here:
+
+```
+https://travis-ci.org/rails/rails
+```
+
+### Is Sam Ruby happy? If not, make him happy.
+
+Sam Ruby keeps a [test suite](https://github.com/rubys/awdwr) that makes
+sure the code samples in his book
+([Agile Web Development with Rails](https://pragprog.com/book/rails51/agile-web-development-with-rails-51))
+all work. These are valuable system tests
+for Rails. You can check the status of these tests here:
+
+[http://intertwingly.net/projects/dashboard.html](http://intertwingly.net/projects/dashboard.html)
+
+Do not release with Red AWDwR tests.
+
+### Do we have any Git dependencies? If so, contact those authors.
+
+Having Git dependencies indicates that we depend on unreleased code.
+Obviously Rails cannot be released when it depends on unreleased code.
+Contact the authors of those particular gems and work out a release date that
+suits them.
+
+### Contact the security team (either tenderlove or rafaelfranca)
+
+Let them know of your plans to release. There may be security issues to be
+addressed, and that can impact your release date.
+
+### Notify implementors.
+
+Ruby implementors have high stakes in making sure Rails works. Be kind and
+give them a heads up that Rails will be released soonish.
+
+This is only required for major and minor releases, bugfix releases aren't a
+big enough deal, and are supposed to be backward compatible.
+
+Send an email just giving a heads up about the upcoming release to these
+lists:
+
+* team@jruby.org
+* community@rubini.us
+* rubyonrails-core@googlegroups.com
+
+Implementors will love you and help you.
+
+## 3 Days before release
+
+This is when you should release the release candidate. Here are your tasks
+for today:
+
+### Is the CI green? If not, make it green.
+
+### Is Sam Ruby happy? If not, make him happy.
+
+### Contact the security team. CVE emails must be sent on this day.
+
+### Create a release branch.
+
+From the stable branch, create a release branch. For example, if you're
+releasing Rails 3.0.10, do this:
+
+```
+[aaron@higgins rails (3-0-stable)]$ git checkout -b 3-0-10
+Switched to a new branch '3-0-10'
+[aaron@higgins rails (3-0-10)]$
+```
+
+### Update each CHANGELOG.
+
+Many times commits are made without the CHANGELOG being updated. You should
+review the commits since the last release, and fill in any missing information
+for each CHANGELOG.
+
+You can review the commits for the 3.0.10 release like this:
+
+```
+[aaron@higgins rails (3-0-10)]$ git log v3.0.9..
+```
+
+If you're doing a stable branch release, you should also ensure that all of
+the CHANGELOG entries in the stable branch are also synced to the master
+branch.
+
+### Put the new version in the RAILS_VERSION file.
+
+Include an RC number if appropriate, e.g. `6.0.0.rc1`.
+
+### Build and test the gem.
+
+Run `rake verify` to generate the gems and install them locally. `verify` also
+generates a Rails app with a migration and boots it to smoke test with in your
+browser.
+
+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 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!
+
+The release task will sign the release tag. If you haven't got commit signing
+set up, use https://git-scm.com/book/tr/v2/Git-Tools-Signing-Your-Work as a
+guide. You can generate keys with the GPG suite from here: https://gpgtools.org.
+
+Run `rake changelog:header` to put a header with the new version in every
+CHANGELOG. Don't commit this, the release task handles it.
+
+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
+rubygems.org.
+
+### Send Rails release announcements
+
+Write a release announcement that includes the version, changes, and links to
+GitHub where people can find the specific commit list. Here are the mailing
+lists where you should announce:
+
+* rubyonrails-core@googlegroups.com
+* rubyonrails-talk@googlegroups.com
+* ruby-talk@ruby-lang.org
+
+Use Markdown format for your announcement. Remember to ask people to report
+issues with the release candidate to the rails-core mailing list.
+
+NOTE: For patch releases, there's a `rake announce` task to generate the release
+post. It supports multiple patch releases too:
+
+```
+VERSIONS="5.0.5.rc1,5.1.3.rc1" rake announce
+```
+
+IMPORTANT: If any users experience regressions when using the release
+candidate, you *must* postpone the release. Bugfix releases *should not*
+break existing applications.
+
+### Post the announcement to the Rails blog.
+
+If you used Markdown format for your email, you can just paste it into the
+blog.
+
+* https://weblog.rubyonrails.org
+
+### Post the announcement to the Rails Twitter account.
+
+## Time between release candidate and actual release
+
+Check the rails-core mailing list and the GitHub issue list for regressions in
+the RC.
+
+If any regressions are found, fix the regressions and repeat the release
+candidate process. We will not release the final until 72 hours after the
+last release candidate has been pushed. This means that if users find
+regressions, the scheduled release date must be postponed.
+
+When you fix the regressions, do not create a new branch. Fix them on the
+stable branch, then cherry pick the commit to your release branch. No other
+commits should be added to the release branch besides regression fixing commits.
+
+## Day of release
+
+Many of these steps are the same as for the release candidate, so if you need
+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 RAILS_VERSION to remove the rc
+* Build and test the gem
+* Release the gems
+* If releasing a new stable version:
+ - Trigger stable docs generation (see below)
+ - Update the version in the home page
+* Email security lists
+* Email general announcement lists
+
+### Emailing the Rails security announce list
+
+Email the security announce list once for each vulnerability fixed.
+
+You can do this, or ask the security team to do it.
+
+Email the security reports to:
+
+* rubyonrails-security@googlegroups.com
+* oss-security@lists.openwall.com
+
+Be sure to note the security fixes in your announcement along with CVE numbers
+and links to each patch. Some people may not be able to upgrade right away,
+so we need to give them the security fixes in patch form.
+
+* Blog announcements
+* Twitter announcements
+* Merge the release branch to the stable branch
+* Drink beer (or other cocktail)
+
+## Misc
+
+### Fixing the CI
+
+There are two simple steps for fixing the CI:
+
+1. Identify the problem
+2. Fix it
+
+Repeat these steps until the CI is green.
diff --git a/RELEASING_RAILS.rdoc b/RELEASING_RAILS.rdoc
deleted file mode 100644
index a020b958e1..0000000000
--- a/RELEASING_RAILS.rdoc
+++ /dev/null
@@ -1,205 +0,0 @@
-= Releasing Rails
-
-In this document, we'll cover the steps necessary to release Rails. Each
-section contains steps to take during that time before the release. The times
-suggested in each header are just that: suggestions. However, they should
-really be considered as minimums.
-
-== 10 Days before release
-
-Today is mostly coordination tasks. Here are the things you must do today:
-
-=== Is the CI green? If not, make it green. (See "Fixing the CI")
-
-Do not release with a Red CI. You can find the CI status here:
-
- http://travis-ci.org/rails/rails
-
-=== Is Sam Ruby happy? If not, make him happy.
-
-Sam Ruby keeps a test suite that makes sure the code samples in his book (Agile
-Web Development with Rails) all work. These are valuable integration tests
-for Rails. You can check the status of his tests here:
-
- http://intertwingly.net/projects/dashboard.html
-
-Do not release with Red AWDwR tests.
-
-=== Do we have any Git dependencies? If so, contact those authors.
-
-Having Git dependencies indicates that we depend on unreleased code.
-Obviously Rails cannot be released when it depends on unreleased code.
-Contact the authors of those particular gems and work out a release date that
-suits them.
-
-=== Contact the security team (either Koz or tenderlove)
-
-Let them know of your plans to release. There may be security issues to be
-addressed, and that can impact your release date.
-
-=== Notify implementors.
-
-Ruby implementors have high stakes in making sure Rails works. Be kind and
-give them a heads up that Rails will be released soonish.
-
-This is only required for major and minor releases, bugfix releases aren't a
-big enough deal, and are supposed to be backwards compatible.
-
-Send an email just giving a heads up about the upcoming release to these
-lists:
-
-* team@jruby.org
-* community@rubini.us
-* rubyonrails-core@googlegroups.com
-
-Implementors will love you and help you.
-
-== 3 Days before release
-
-This is when you should release the release candidate. Here are your tasks
-for today:
-
-=== Is the CI green? If not, make it green.
-
-=== Is Sam Ruby happy? If not, make him happy.
-
-=== Contact the security team. CVE emails must be sent on this day.
-
-=== Create a release branch.
-
-From the stable branch, create a release branch. For example, if you're
-releasing Rails 3.0.10, do this:
-
- [aaron@higgins rails (3-0-stable)]$ git checkout -b 3-0-10
- Switched to a new branch '3-0-10'
- [aaron@higgins rails (3-0-10)]$
-
-=== Update each CHANGELOG.
-
-Many times commits are made without the CHANGELOG being updated. You should
-review the commits since the last release, and fill in any missing information
-for each CHANGELOG.
-
-You can review the commits for the 3.0.10 release like this:
-
- [aaron@higgins rails (3-0-10)]$ git log v3.0.9..
-
-If you're doing a stable branch release, you should also ensure that all of
-the CHANGELOG entries in the stable branch are also synced to the master
-branch.
-
-=== Update the RAILS_VERSION file to include the RC.
-
-=== Build and test the gem.
-
-Run `rake install` to generate the gems and install them locally. Then try
-generating a new app and ensure that nothing explodes.
-
-This will stop you from looking silly when you push an RC to rubygems.org and
-then realize it is broken.
-
-=== Release the gem.
-
-IMPORTANT: Due to YAML parse problems on the rubygems.org server, it is safest
-to use Ruby 1.8 when releasing.
-
-Run `rake release`. This will populate the gemspecs with data from
-RAILS_VERSION, commit the changes, tag it, and push the gems to rubygems.org.
-Here are the commands that `rake release` should use, so you can understand
-what to do in case anything goes wrong:
-
- $ rake all:build
- $ git commit -am'updating RAILS_VERSION'
- $ git tag -m 'v3.0.10.rc1 release' v3.0.10.rc1
- $ git push
- $ git push --tags
- $ for i in $(ls pkg); do gem push $i; done
-
-=== Send Rails release announcements
-
-Write a release announcement that includes the version, changes, and links to
-GitHub where people can find the specific commit list. Here are the mailing
-lists where you should announce:
-
-* rubyonrails-core@googlegroups.com
-* rubyonrails-talk@googlegroups.com
-* ruby-talk@ruby-lang.org
-
-Use Markdown format for your announcement. Remember to ask people to report
-issues with the release candidate to the rails-core mailing list.
-
-IMPORTANT: If any users experience regressions when using the release
-candidate, you *must* postpone the release. Bugfix releases *should not*
-break existing applications.
-
-=== Post the announcement to the Rails blog.
-
-If you used Markdown format for your email, you can just paste it in to the
-blog.
-
-* http://weblog.rubyonrails.org
-
-=== Post the announcement to the Rails Twitter account.
-
-== Time between release candidate and actual release
-
-Check the rails-core mailing list and the GitHub issue list for regressions in
-the RC.
-
-If any regressions are found, fix the regressions and repeat the release
-candidate process. We will not release the final until 72 hours after the
-last release candidate has been pushed. This means that if users find
-regressions, the scheduled release date must be postponed.
-
-When you fix the regressions, do not create a new branch. Fix them on the
-stable branch, then cherry pick the commit to your release branch. No other
-commits should be added to the release branch besides regression fixing commits.
-
-== Day of release
-
-Many of these steps are the same as for the release candidate, so if you need
-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 RAILS_VERSION to remove the rc
-* Build and test the gem
-* Release the gems
-* If releasing a new stable version:
- - Trigger stable docs generation (see below)
- - Update the version in the home page
-* Email security lists
-* Email general announcement lists
-
-=== Emailing the Rails security announce list
-
-Email the security announce list once for each vulnerability fixed.
-
-You can do this, or ask the security team to do it.
-
-Email the security reports to:
-
-* rubyonrails-security@googlegroups.com
-* oss-security@lists.openwall.com
-
-Be sure to note the security fixes in your announcement along with CVE numbers
-and links to each patch. Some people may not be able to upgrade right away,
-so we need to give them the security fixes in patch form.
-
-* Blog announcements
-* Twitter announcements
-* Merge the release branch to the stable branch.
-* Drink beer (or other cocktail)
-
-== Misc
-
-=== Fixing the CI
-
-There are two simple steps for fixing the CI:
-
-1. Identify the problem
-2. Fix it
-
-Repeat these steps until the CI is green.
diff --git a/Rakefile b/Rakefile
index db27105cc3..a67f8fd028 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,27 +1,32 @@
-require 'sdoc'
-require 'net/http'
+# frozen_string_literal: true
-$:.unshift File.expand_path('..', __FILE__)
+require "net/http"
+
+$:.unshift __dir__
require "tasks/release"
-require 'railties/lib/rails/api/task'
+require "railties/lib/rails/api/task"
desc "Build gem files for all projects"
-task :build => "all:build"
+task build: "all:build"
-desc "Release all gems to rubygems and create a tag"
-task :release => "all:release"
+desc "Build, install and verify the gem files in a generated Rails app."
+task verify: "all:verify"
-PROJECTS = %w(activesupport activemodel actionpack actionview actionmailer activerecord railties activejob)
+desc "Prepare the release"
+task prep_release: "all:prep_release"
+
+desc "Release all gems to rubygems and create a tag"
+task release: "all:release"
-desc 'Run all tests by default'
-task :default => %w(test test:isolated)
+desc "Run all tests by default"
+task default: %w(test test:isolated)
%w(test test:isolated package gem).each do |task_name|
desc "Run #{task_name} task for all projects"
task task_name do
errors = []
- PROJECTS.each do |project|
- system(%(cd #{project} && #{$0} #{task_name})) || errors << project
+ FRAMEWORKS.each do |project|
+ system(%(cd #{project} && #{$0} #{task_name} --trace)) || errors << project
end
fail("Errors in #{errors.join(', ')}") unless errors.empty?
end
@@ -29,24 +34,24 @@ end
desc "Smoke-test all projects"
task :smoke do
- (PROJECTS - %w(activerecord)).each do |project|
- system %(cd #{project} && #{$0} test:isolated)
+ (FRAMEWORKS - %w(activerecord)).each do |project|
+ system %(cd #{project} && #{$0} test:isolated --trace)
end
- system %(cd activerecord && #{$0} sqlite3:isolated_test)
+ system %(cd activerecord && #{$0} sqlite3:isolated_test --trace)
end
desc "Install gems for all projects."
-task :install => "all:install"
+task install: "all:install"
desc "Generate documentation for the Rails framework"
-if ENV['EDGE']
- Rails::API::EdgeTask.new('rdoc')
+if ENV["EDGE"]
+ Rails::API::EdgeTask.new("rdoc")
else
- Rails::API::StableTask.new('rdoc')
+ Rails::API::StableTask.new("rdoc")
end
-desc 'Bump all versions to match RAILS_VERSION'
-task :update_versions => "all:update_versions"
+desc "Bump all versions to match RAILS_VERSION"
+task update_versions: "all:update_versions"
# We have a webhook configured in GitHub that gets invoked after pushes.
# This hook triggers the following tasks:
@@ -57,10 +62,10 @@ task :update_versions => "all:update_versions"
# * if there's a new stable tag, generates and publishes stable docs
#
# Everything is automated and you do NOT need to run this task normally.
-desc 'Publishes docs, run this AFTER a new stable tag has been pushed'
+desc "Publishes docs, run this AFTER a new stable tag has been pushed"
task :publish_docs do
- Net::HTTP.new('api.rubyonrails.org', 8080).start do |http|
- request = Net::HTTP::Post.new('/rails-master-hook')
+ Net::HTTP.new("api.rubyonrails.org", 8080).start do |http|
+ request = Net::HTTP::Post.new("/rails-master-hook")
response = http.request(request)
puts response.body
end
diff --git a/actioncable/.babelrc b/actioncable/.babelrc
new file mode 100644
index 0000000000..4f0c469c60
--- /dev/null
+++ b/actioncable/.babelrc
@@ -0,0 +1,8 @@
+{
+ "presets": [
+ ["env", { "modules": false, "loose": true } ]
+ ],
+ "plugins": [
+ "external-helpers"
+ ]
+}
diff --git a/actioncable/.eslintrc b/actioncable/.eslintrc
new file mode 100644
index 0000000000..3d9ecd4bce
--- /dev/null
+++ b/actioncable/.eslintrc
@@ -0,0 +1,19 @@
+{
+ "extends": "eslint:recommended",
+ "rules": {
+ "semi": ["error", "never"],
+ "quotes": ["error", "double"],
+ "no-unused-vars": ["error", { "vars": "all", "args": "none" }]
+ },
+ "plugins": [
+ "import"
+ ],
+ "env": {
+ "browser": true,
+ "es6": true
+ },
+ "parserOptions": {
+ "ecmaVersion": 6,
+ "sourceType": "module"
+ }
+}
diff --git a/actioncable/.gitignore b/actioncable/.gitignore
new file mode 100644
index 0000000000..5d5c867f2d
--- /dev/null
+++ b/actioncable/.gitignore
@@ -0,0 +1,3 @@
+/app/javascript/action_cable/internal.js
+/lib/assets/compiled/
+/tmp/
diff --git a/actioncable/CHANGELOG.md b/actioncable/CHANGELOG.md
new file mode 100644
index 0000000000..17c7e9a3b7
--- /dev/null
+++ b/actioncable/CHANGELOG.md
@@ -0,0 +1,24 @@
+* Add `id` option to redis adapter so now you can distinguish
+ ActionCable's redis connections among others. Also, you can set
+ custom id in options.
+
+ Before:
+ ```
+ $ redis-cli client list
+ id=669 addr=127.0.0.1:46442 fd=8 name= age=18 ...
+ ```
+
+ After:
+ ```
+ $ redis-cli client list
+ id=673 addr=127.0.0.1:46516 fd=8 name=ActionCable-PID-19413 age=2 ...
+ ```
+
+ *Ilia Kasianenko*
+
+* Rails 6 requires Ruby 2.4.1 or newer.
+
+ *Jeremy Daer*
+
+
+Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/actioncable/CHANGELOG.md) for previous changes.
diff --git a/actioncable/MIT-LICENSE b/actioncable/MIT-LICENSE
new file mode 100644
index 0000000000..a42759f024
--- /dev/null
+++ b/actioncable/MIT-LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2015-2018 Basecamp, LLC
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"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/actioncable/README.md b/actioncable/README.md
new file mode 100644
index 0000000000..a2783d6f45
--- /dev/null
+++ b/actioncable/README.md
@@ -0,0 +1,567 @@
+# Action Cable – Integrated WebSockets for Rails
+
+Action Cable seamlessly integrates WebSockets with the rest of your Rails application.
+It allows for real-time features to be written in Ruby in the same style
+and form as the rest of your Rails application, while still being performant
+and scalable. It's a full-stack offering that provides both a client-side
+JavaScript framework and a server-side Ruby framework. You have access to your full
+domain model written with Active Record or your ORM of choice.
+
+## Terminology
+
+A single Action Cable server can handle multiple connection instances. It has one
+connection instance per WebSocket connection. A single user may have multiple
+WebSockets open to your application if they use multiple browser tabs or devices.
+The client of a WebSocket connection is called the consumer.
+
+Each consumer can in turn subscribe to multiple cable channels. Each channel encapsulates
+a logical unit of work, similar to what a controller does in a regular MVC setup. For example,
+you could have a `ChatChannel` and an `AppearancesChannel`, and a consumer could be subscribed to either
+or to both of these channels. At the very least, a consumer should be subscribed to one channel.
+
+When the consumer is subscribed to a channel, they act as a subscriber. The connection between
+the subscriber and the channel is, surprise-surprise, called a subscription. A consumer
+can act as a subscriber to a given channel any number of times. For example, a consumer
+could subscribe to multiple chat rooms at the same time. (And remember that a physical user may
+have multiple consumers, one per tab/device open to your connection).
+
+Each channel can then again be streaming zero or more broadcastings. A broadcasting is a
+pubsub link where anything transmitted by the broadcaster is sent directly to the channel
+subscribers who are streaming that named broadcasting.
+
+As you can see, this is a fairly deep architectural stack. There's a lot of new terminology
+to identify the new pieces, and on top of that, you're dealing with both client and server side
+reflections of each unit.
+
+## Examples
+
+### A full-stack example
+
+The first thing you must do is define your `ApplicationCable::Connection` class in Ruby. This
+is the place where you authorize the incoming connection, and proceed to establish it,
+if all is well. Here's the simplest example starting with the server-side connection class:
+
+```ruby
+# app/channels/application_cable/connection.rb
+module ApplicationCable
+ class Connection < ActionCable::Connection::Base
+ identified_by :current_user
+
+ def connect
+ self.current_user = find_verified_user
+ end
+
+ private
+ def find_verified_user
+ if verified_user = User.find_by(id: cookies.encrypted[:user_id])
+ verified_user
+ else
+ reject_unauthorized_connection
+ end
+ end
+ end
+end
+```
+Here `identified_by` is a connection identifier that can be used to find the specific connection again or later.
+Note that anything marked as an identifier will automatically create a delegate by the same name on any channel instances created off the connection.
+
+This relies on the fact that you will already have handled authentication of the user, and
+that a successful authentication sets a signed cookie with the `user_id`. This cookie is then
+automatically sent to the connection instance when a new connection is attempted, and you
+use that to set the `current_user`. By identifying the connection by this same current_user,
+you're also ensuring that you can later retrieve all open connections by a given user (and
+potentially disconnect them all if the user is deleted or deauthorized).
+
+Next, you should define your `ApplicationCable::Channel` class in Ruby. This is the place where you put
+shared logic between your channels.
+
+```ruby
+# app/channels/application_cable/channel.rb
+module ApplicationCable
+ class Channel < ActionCable::Channel::Base
+ end
+end
+```
+
+The client-side needs to setup a consumer instance of this connection. That's done like so:
+
+```js
+// app/assets/javascripts/cable.js
+//= require action_cable
+//= require_self
+//= require_tree ./channels
+
+(function() {
+ this.App || (this.App = {});
+
+ App.cable = ActionCable.createConsumer("ws://cable.example.com");
+}).call(this);
+```
+
+The `ws://cable.example.com` address must point to your Action Cable server(s), and it
+must share a cookie namespace with the rest of the application (which may live under http://example.com).
+This ensures that the signed cookie will be correctly sent.
+
+That's all you need to establish the connection! But of course, this isn't very useful in
+itself. This just gives you the plumbing. To make stuff happen, you need content. That content
+is defined by declaring channels on the server and allowing the consumer to subscribe to them.
+
+
+### Channel example 1: User appearances
+
+Here's a simple example of a channel that tracks whether a user is online or not, and also what page they are currently on.
+(This is useful for creating presence features like showing a green dot next to a user's name if they're online).
+
+First you declare the server-side channel:
+
+```ruby
+# app/channels/appearance_channel.rb
+class AppearanceChannel < ApplicationCable::Channel
+ def subscribed
+ current_user.appear
+ end
+
+ def unsubscribed
+ current_user.disappear
+ end
+
+ def appear(data)
+ current_user.appear on: data['appearing_on']
+ end
+
+ def away
+ current_user.away
+ end
+end
+```
+
+The `#subscribed` callback is invoked when, as we'll show below, a client-side subscription is initiated. In this case,
+we take that opportunity to say "the current user has indeed appeared". That appear/disappear API could be backed by
+Redis or a database or whatever else. Here's what the client-side of that looks like:
+
+```coffeescript
+# app/assets/javascripts/cable/subscriptions/appearance.coffee
+App.cable.subscriptions.create "AppearanceChannel",
+ # Called when the subscription is ready for use on the server
+ connected: ->
+ @install()
+ @appear()
+
+ # Called when the WebSocket connection is closed
+ disconnected: ->
+ @uninstall()
+
+ # Called when the subscription is rejected by the server
+ rejected: ->
+ @uninstall()
+
+ appear: ->
+ # Calls `AppearanceChannel#appear(data)` on the server
+ @perform("appear", appearing_on: $("main").data("appearing-on"))
+
+ away: ->
+ # Calls `AppearanceChannel#away` on the server
+ @perform("away")
+
+
+ buttonSelector = "[data-behavior~=appear_away]"
+
+ install: ->
+ $(document).on "turbolinks:load.appearance", =>
+ @appear()
+
+ $(document).on "click.appearance", buttonSelector, =>
+ @away()
+ false
+
+ $(buttonSelector).show()
+
+ uninstall: ->
+ $(document).off(".appearance")
+ $(buttonSelector).hide()
+```
+
+Simply calling `App.cable.subscriptions.create` will setup the subscription, which will call `AppearanceChannel#subscribed`,
+which in turn is linked to the original `App.cable` -> `ApplicationCable::Connection` instances.
+
+Next, we link the client-side `appear` method to `AppearanceChannel#appear(data)`. This is possible because the server-side
+channel instance will automatically expose the public methods declared on the class (minus the callbacks), so that these
+can be reached as remote procedure calls via a subscription's `perform` method.
+
+### Channel example 2: Receiving new web notifications
+
+The appearance example was all about exposing server functionality to client-side invocation over the WebSocket connection.
+But the great thing about WebSockets is that it's a two-way street. So now let's show an example where the server invokes
+an action on the client.
+
+This is a web notification channel that allows you to trigger client-side web notifications when you broadcast to the right
+streams:
+
+```ruby
+# app/channels/web_notifications_channel.rb
+class WebNotificationsChannel < ApplicationCable::Channel
+ def subscribed
+ stream_from "web_notifications_#{current_user.id}"
+ end
+end
+```
+
+```coffeescript
+# Client-side, which assumes you've already requested the right to send web notifications
+App.cable.subscriptions.create "WebNotificationsChannel",
+ received: (data) ->
+ new Notification data["title"], body: data["body"]
+```
+
+```ruby
+# Somewhere in your app this is called, perhaps from a NewCommentJob
+ActionCable.server.broadcast \
+ "web_notifications_#{current_user.id}", { title: 'New things!', body: 'All the news that is fit to print' }
+```
+
+The `ActionCable.server.broadcast` call places a message in the Action Cable pubsub queue under a separate broadcasting name for each user. For a user with an ID of 1, the broadcasting name would be `web_notifications_1`.
+The channel has been instructed to stream everything that arrives at `web_notifications_1` directly to the client by invoking the
+`#received(data)` callback. The data is the hash sent as the second parameter to the server-side broadcast call, JSON encoded for the trip
+across the wire, and unpacked for the data argument arriving to `#received`.
+
+
+### Passing Parameters to Channel
+
+You can pass parameters from the client side to the server side when creating a subscription. For example:
+
+```ruby
+# app/channels/chat_channel.rb
+class ChatChannel < ApplicationCable::Channel
+ def subscribed
+ stream_from "chat_#{params[:room]}"
+ end
+end
+```
+
+If you pass an object as the first argument to `subscriptions.create`, that object will become the params hash in your cable channel. The keyword `channel` is required.
+
+```coffeescript
+# Client-side, which assumes you've already requested the right to send web notifications
+App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" },
+ received: (data) ->
+ @appendLine(data)
+
+ appendLine: (data) ->
+ html = @createLine(data)
+ $("[data-chat-room='Best Room']").append(html)
+
+ createLine: (data) ->
+ """
+ <article class="chat-line">
+ <span class="speaker">#{data["sent_by"]}</span>
+ <span class="body">#{data["body"]}</span>
+ </article>
+ """
+```
+
+```ruby
+# Somewhere in your app this is called, perhaps from a NewCommentJob
+ActionCable.server.broadcast \
+ "chat_#{room}", { sent_by: 'Paul', body: 'This is a cool chat app.' }
+```
+
+
+### Rebroadcasting message
+
+A common use case is to rebroadcast a message sent by one client to any other connected clients.
+
+```ruby
+# app/channels/chat_channel.rb
+class ChatChannel < ApplicationCable::Channel
+ def subscribed
+ stream_from "chat_#{params[:room]}"
+ end
+
+ def receive(data)
+ ActionCable.server.broadcast "chat_#{params[:room]}", data
+ end
+end
+```
+
+```coffeescript
+# Client-side, which assumes you've already requested the right to send web notifications
+App.chatChannel = App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" },
+ received: (data) ->
+ # data => { sent_by: "Paul", body: "This is a cool chat app." }
+
+App.chatChannel.send({ sent_by: "Paul", body: "This is a cool chat app." })
+```
+
+The rebroadcast will be received by all connected clients, _including_ the client that sent the message. Note that params are the same as they were when you subscribed to the channel.
+
+
+### More complete examples
+
+See the [rails/actioncable-examples](https://github.com/rails/actioncable-examples) repository for a full example of how to setup Action Cable in a Rails app, and how to add channels.
+
+## Configuration
+
+Action Cable has three required configurations: a subscription adapter, allowed request origins, and the cable server URL (which can optionally be set on the client side).
+
+### Redis
+
+By default, `ActionCable::Server::Base` will look for a configuration file in `Rails.root.join('config/cable.yml')`.
+This file must specify an adapter and a URL for each Rails environment. It may use the following format:
+
+```yaml
+production: &production
+ adapter: redis
+ url: redis://10.10.3.153:6381
+development: &development
+ adapter: redis
+ url: redis://localhost:6379
+test: *development
+```
+
+You can also change the location of the Action Cable config file in a Rails initializer with something like:
+
+```ruby
+Rails.application.paths.add "config/cable", with: "somewhere/else/cable.yml"
+```
+
+### Allowed Request Origins
+
+Action Cable will only accept requests from specific origins.
+
+By default, only an origin matching the cable server itself will be permitted.
+Additional origins can be specified using strings or regular expressions, provided in an array.
+
+```ruby
+Rails.application.config.action_cable.allowed_request_origins = ['http://rubyonrails.com', /http:\/\/ruby.*/]
+```
+
+When running in the development environment, this defaults to "http://localhost:3000".
+
+To disable protection and allow requests from any origin:
+
+```ruby
+Rails.application.config.action_cable.disable_request_forgery_protection = true
+```
+
+To disable automatic access for same-origin requests, and strictly allow
+only the configured origins:
+
+```ruby
+Rails.application.config.action_cable.allow_same_origin_as_host = false
+```
+
+### Consumer Configuration
+
+Once you have decided how to run your cable server (see below), you must provide the server URL (or path) to your client-side setup.
+There are two ways you can do this.
+
+The first is to simply pass it in when creating your consumer. For a standalone server,
+this would be something like: `App.cable = ActionCable.createConsumer("ws://example.com:28080")`, and for an in-app server,
+something like: `App.cable = ActionCable.createConsumer("/cable")`.
+
+The second option is to pass the server URL through the `action_cable_meta_tag` in your layout.
+This uses a URL or path typically set via `config.action_cable.url` in the environment configuration files, or defaults to "/cable".
+
+This method is especially useful if your WebSocket URL might change between environments. If you host your production server via https, you will need to use the wss scheme
+for your Action Cable server, but development might remain http and use the ws scheme. You might use localhost in development and your
+domain in production.
+
+In any case, to vary the WebSocket URL between environments, add the following configuration to each environment:
+
+```ruby
+config.action_cable.url = "ws://example.com:28080"
+```
+
+Then add the following line to your layout before your JavaScript tag:
+
+```erb
+<%= action_cable_meta_tag %>
+```
+
+And finally, create your consumer like so:
+
+```coffeescript
+App.cable = ActionCable.createConsumer()
+```
+
+### Other Configurations
+
+The other common option to configure is the log tags applied to the per-connection logger. Here's an example that uses the user account id if available, else "no-account" while tagging:
+
+```ruby
+config.action_cable.log_tags = [
+ -> request { request.env['user_account_id'] || "no-account" },
+ :action_cable,
+ -> request { request.uuid }
+]
+```
+
+For a full list of all configuration options, see the `ActionCable::Server::Configuration` class.
+
+Also note that your server must provide at least the same number of database connections as you have workers. The default worker pool is set to 4, so that means you have to make at least that available. You can change that in `config/database.yml` through the `pool` attribute.
+
+
+## Running the cable server
+
+### Standalone
+The cable server(s) is separated from your normal application server. It's still a Rack application, but it is its own Rack
+application. The recommended basic setup is as follows:
+
+```ruby
+# cable/config.ru
+require_relative '../config/environment'
+Rails.application.eager_load!
+
+run ActionCable.server
+```
+
+Then you start the server using a binstub in bin/cable ala:
+```sh
+#!/bin/bash
+bundle exec puma -p 28080 cable/config.ru
+```
+
+The above will start a cable server on port 28080.
+
+### In app
+
+If you are using a server that supports the [Rack socket hijacking API](https://www.rubydoc.info/github/rack/rack/file/SPEC#label-Hijacking), Action Cable can run alongside your Rails application. For example, to listen for WebSocket requests on `/websocket`, specify that path to `config.action_cable.mount_path`:
+
+```ruby
+# config/application.rb
+class Application < Rails::Application
+ config.action_cable.mount_path = '/websocket'
+end
+```
+
+For every instance of your server you create and for every worker your server spawns, you will also have a new instance of Action Cable, but the use of Redis keeps messages synced across connections.
+
+### Notes
+
+Beware that currently, the cable server will _not_ auto-reload any changes in the framework. As we've discussed, long-running cable connections mean long-running objects. We don't yet have a way of reloading the classes of those objects in a safe manner. So when you change your channels, or the model your channels use, you must restart the cable server.
+
+We'll get all this abstracted properly when the framework is integrated into Rails.
+
+The WebSocket server doesn't have access to the session, but it has access to the cookies. This can be used when you need to handle authentication. You can see one way of doing that with Devise in this [article](https://greg.molnar.io/blog/actioncable-devise-authentication/).
+
+## Dependencies
+
+Action Cable provides a subscription adapter interface to process its pubsub internals. By default, asynchronous, inline, PostgreSQL, and Redis adapters are included. The default adapter in new Rails applications is the asynchronous (`async`) adapter. To create your own adapter, you can look at `ActionCable::SubscriptionAdapter::Base` for all methods that must be implemented, and any of the adapters included within Action Cable as example implementations.
+
+The Ruby side of things is built on top of [websocket-driver](https://github.com/faye/websocket-driver-ruby), [nio4r](https://github.com/celluloid/nio4r), and [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby).
+
+
+## Deployment
+
+Action Cable is powered by a combination of WebSockets and threads. All of the
+connection management is handled internally by utilizing Ruby's native thread
+support, which means you can use all your regular Rails models with no problems
+as long as you haven't committed any thread-safety sins.
+
+The Action Cable server does _not_ need to be a multi-threaded application server.
+This is because Action Cable uses the [Rack socket hijacking API](https://www.rubydoc.info/github/rack/rack/file/SPEC#label-Hijacking)
+to take over control of connections from the application server. Action Cable
+then manages connections internally, in a multithreaded manner, regardless of
+whether the application server is multi-threaded or not. So Action Cable works
+with all the popular application servers -- Unicorn, Puma and Passenger.
+
+Action Cable does not work with WEBrick, because WEBrick does not support the
+Rack socket hijacking API.
+
+## Frontend assets
+
+Action Cable's frontend assets are distributed through two channels: the
+official gem and npm package, both titled `actioncable`.
+
+### Gem usage
+
+Through the `actioncable` gem, Action Cable's frontend assets are
+available through the Rails Asset Pipeline. Create a `cable.js` or
+`cable.coffee` file (this is automatically done for you with Rails
+generators), and then simply require the assets:
+
+In JavaScript...
+
+```javascript
+//= require action_cable
+```
+
+... and in CoffeeScript:
+
+```coffeescript
+#= require action_cable
+```
+
+### npm usage
+
+In addition to being available through the `actioncable` gem, Action Cable's
+frontend JS assets are also bundled in an officially supported npm module,
+intended for usage in standalone frontend applications that communicate with a
+Rails application. A common use case for this could be if you have a decoupled
+frontend application written in React, Ember.js, etc. and want to add real-time
+WebSocket functionality.
+
+### Installation
+
+```
+npm install actioncable --save
+```
+
+### Usage
+
+The `ActionCable` constant is available as a `require`-able module, so
+you only have to require the package to gain access to the API that is
+provided.
+
+In JavaScript...
+
+```javascript
+ActionCable = require('actioncable')
+
+var cable = ActionCable.createConsumer('wss://RAILS-API-PATH.com/cable')
+
+cable.subscriptions.create('AppearanceChannel', {
+ // normal channel code goes here...
+});
+```
+
+and in CoffeeScript...
+
+```coffeescript
+ActionCable = require('actioncable')
+
+cable = ActionCable.createConsumer('wss://RAILS-API-PATH.com/cable')
+
+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:
+
+* https://opensource.org/licenses/MIT
+
+
+## Support
+
+API documentation is at:
+
+* http://api.rubyonrails.org
+
+Bug reports for the Ruby on Rails project can be filed here:
+
+* https://github.com/rails/rails/issues
+
+Feature requests should be discussed on the rails-core mailing list here:
+
+* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core
diff --git a/actioncable/Rakefile b/actioncable/Rakefile
new file mode 100644
index 0000000000..52c86eb5d7
--- /dev/null
+++ b/actioncable/Rakefile
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require "rake/testtask"
+require "pathname"
+require "open3"
+require "action_cable"
+
+task default: :test
+
+task :package
+
+Rake::TestTask.new do |t|
+ t.libs << "test"
+ t.test_files = Dir.glob("#{__dir__}/test/**/*_test.rb")
+ t.warning = true
+ t.verbose = true
+ t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
+end
+
+namespace :test do
+ task :isolated do
+ Dir.glob("test/**/*_test.rb").all? do |file|
+ sh(Gem.ruby, "-w", "-Ilib:test", file)
+ end || raise("Failures")
+ end
+
+ task :integration do
+ require "blade"
+ if ENV["CI"]
+ Blade.start(interface: :ci)
+ else
+ Blade.start(interface: :runner)
+ end
+ end
+end
+
+namespace :assets do
+ desc "Generate ActionCable::INTERNAL JS module"
+ task :codegen do
+ require "json"
+ require "action_cable"
+
+ File.open(File.join(__dir__, "app/javascript/action_cable/internal.js").to_s, "w+") do |file|
+ file.write("export default #{JSON.generate(ActionCable::INTERNAL)}")
+ end
+ end
+end
diff --git a/actioncable/actioncable.gemspec b/actioncable/actioncable.gemspec
new file mode 100644
index 0000000000..31c6fb41f2
--- /dev/null
+++ b/actioncable/actioncable.gemspec
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip
+
+Gem::Specification.new do |s|
+ s.platform = Gem::Platform::RUBY
+ s.name = "actioncable"
+ s.version = version
+ s.summary = "WebSocket framework for Rails."
+ s.description = "Structure many real-time application concerns into channels over a single WebSocket connection."
+
+ s.required_ruby_version = ">= 2.4.1"
+
+ s.license = "MIT"
+
+ s.author = ["Pratik Naik", "David Heinemeier Hansson"]
+ s.email = ["pratiknaik@gmail.com", "david@loudthinking.com"]
+ s.homepage = "http://rubyonrails.org"
+
+ s.files = Dir["CHANGELOG.md", "MIT-LICENSE", "README.md", "lib/**/*", "app/assets/javascripts/action_cable.js"]
+ s.require_path = "lib"
+
+ s.metadata = {
+ "source_code_uri" => "https://github.com/rails/rails/tree/v#{version}/actioncable",
+ "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/actioncable/CHANGELOG.md"
+ }
+
+ # NOTE: Please read our dependency guidelines before updating versions:
+ # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves
+
+ s.add_dependency "actionpack", version
+
+ s.add_dependency "nio4r", "~> 2.0"
+ s.add_dependency "websocket-driver", ">= 0.6.1"
+end
diff --git a/actioncable/app/assets/javascripts/action_cable.js b/actioncable/app/assets/javascripts/action_cable.js
new file mode 100644
index 0000000000..07151b9d25
--- /dev/null
+++ b/actioncable/app/assets/javascripts/action_cable.js
@@ -0,0 +1,476 @@
+(function(global, factory) {
+ typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define(factory) : global.ActionCable = factory();
+})(this, function() {
+ "use strict";
+ var INTERNAL = {
+ message_types: {
+ welcome: "welcome",
+ ping: "ping",
+ confirmation: "confirm_subscription",
+ rejection: "reject_subscription"
+ },
+ default_mount_path: "/cable",
+ protocols: [ "actioncable-v1-json", "actioncable-unsupported" ]
+ };
+ var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function(obj) {
+ return typeof obj;
+ } : function(obj) {
+ return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
+ };
+ var classCallCheck = function(instance, Constructor) {
+ if (!(instance instanceof Constructor)) {
+ throw new TypeError("Cannot call a class as a function");
+ }
+ };
+ var message_types = INTERNAL.message_types, protocols = INTERNAL.protocols;
+ var supportedProtocols = protocols.slice(0, protocols.length - 1);
+ var indexOf = [].indexOf;
+ var Connection = function() {
+ function Connection(consumer) {
+ classCallCheck(this, Connection);
+ this.open = this.open.bind(this);
+ this.consumer = consumer;
+ this.subscriptions = this.consumer.subscriptions;
+ this.monitor = new ActionCable.ConnectionMonitor(this);
+ this.disconnected = true;
+ }
+ Connection.prototype.send = function send(data) {
+ if (this.isOpen()) {
+ this.webSocket.send(JSON.stringify(data));
+ return true;
+ } else {
+ return false;
+ }
+ };
+ Connection.prototype.open = function open() {
+ if (this.isActive()) {
+ ActionCable.log("Attempted to open WebSocket, but existing socket is " + this.getState());
+ return false;
+ } else {
+ ActionCable.log("Opening WebSocket, current state is " + this.getState() + ", subprotocols: " + protocols);
+ if (this.webSocket) {
+ this.uninstallEventHandlers();
+ }
+ this.webSocket = new ActionCable.WebSocket(this.consumer.url, protocols);
+ this.installEventHandlers();
+ this.monitor.start();
+ return true;
+ }
+ };
+ Connection.prototype.close = function close() {
+ var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {
+ allowReconnect: true
+ }, allowReconnect = _ref.allowReconnect;
+ if (!allowReconnect) {
+ this.monitor.stop();
+ }
+ if (this.isActive()) {
+ return this.webSocket ? this.webSocket.close() : undefined;
+ }
+ };
+ Connection.prototype.reopen = function reopen() {
+ ActionCable.log("Reopening WebSocket, current state is " + this.getState());
+ if (this.isActive()) {
+ try {
+ return this.close();
+ } catch (error) {
+ ActionCable.log("Failed to reopen WebSocket", error);
+ } finally {
+ ActionCable.log("Reopening WebSocket in " + this.constructor.reopenDelay + "ms");
+ setTimeout(this.open, this.constructor.reopenDelay);
+ }
+ } else {
+ return this.open();
+ }
+ };
+ Connection.prototype.getProtocol = function getProtocol() {
+ return this.webSocket ? this.webSocket.protocol : undefined;
+ };
+ Connection.prototype.isOpen = function isOpen() {
+ return this.isState("open");
+ };
+ Connection.prototype.isActive = function isActive() {
+ return this.isState("open", "connecting");
+ };
+ Connection.prototype.isProtocolSupported = function isProtocolSupported() {
+ return indexOf.call(supportedProtocols, this.getProtocol()) >= 0;
+ };
+ Connection.prototype.isState = function isState() {
+ for (var _len = arguments.length, states = Array(_len), _key = 0; _key < _len; _key++) {
+ states[_key] = arguments[_key];
+ }
+ return indexOf.call(states, this.getState()) >= 0;
+ };
+ Connection.prototype.getState = function getState() {
+ if (this.webSocket) {
+ for (var state in WebSocket) {
+ if (WebSocket[state] === this.webSocket.readyState) {
+ return state.toLowerCase();
+ }
+ }
+ }
+ return null;
+ };
+ Connection.prototype.installEventHandlers = function installEventHandlers() {
+ for (var eventName in this.events) {
+ var handler = this.events[eventName].bind(this);
+ this.webSocket["on" + eventName] = handler;
+ }
+ };
+ Connection.prototype.uninstallEventHandlers = function uninstallEventHandlers() {
+ for (var eventName in this.events) {
+ this.webSocket["on" + eventName] = function() {};
+ }
+ };
+ return Connection;
+ }();
+ Connection.reopenDelay = 500;
+ Connection.prototype.events = {
+ message: function message(event) {
+ if (!this.isProtocolSupported()) {
+ return;
+ }
+ var _JSON$parse = JSON.parse(event.data), identifier = _JSON$parse.identifier, message = _JSON$parse.message, type = _JSON$parse.type;
+ switch (type) {
+ case message_types.welcome:
+ this.monitor.recordConnect();
+ return this.subscriptions.reload();
+
+ case message_types.ping:
+ return this.monitor.recordPing();
+
+ case message_types.confirmation:
+ return this.subscriptions.notify(identifier, "connected");
+
+ case message_types.rejection:
+ return this.subscriptions.reject(identifier);
+
+ default:
+ return this.subscriptions.notify(identifier, "received", message);
+ }
+ },
+ open: function open() {
+ ActionCable.log("WebSocket onopen event, using '" + this.getProtocol() + "' subprotocol");
+ this.disconnected = false;
+ if (!this.isProtocolSupported()) {
+ ActionCable.log("Protocol is unsupported. Stopping monitor and disconnecting.");
+ return this.close({
+ allowReconnect: false
+ });
+ }
+ },
+ close: function close(event) {
+ ActionCable.log("WebSocket onclose event");
+ if (this.disconnected) {
+ return;
+ }
+ this.disconnected = true;
+ this.monitor.recordDisconnect();
+ return this.subscriptions.notifyAll("disconnected", {
+ willAttemptReconnect: this.monitor.isRunning()
+ });
+ },
+ error: function error() {
+ ActionCable.log("WebSocket onerror event");
+ }
+ };
+ var now = function now() {
+ return new Date().getTime();
+ };
+ var secondsSince = function secondsSince(time) {
+ return (now() - time) / 1e3;
+ };
+ var clamp = function clamp(number, min, max) {
+ return Math.max(min, Math.min(max, number));
+ };
+ var ConnectionMonitor = function() {
+ function ConnectionMonitor(connection) {
+ classCallCheck(this, ConnectionMonitor);
+ this.visibilityDidChange = this.visibilityDidChange.bind(this);
+ this.connection = connection;
+ this.reconnectAttempts = 0;
+ }
+ ConnectionMonitor.prototype.start = function start() {
+ if (!this.isRunning()) {
+ this.startedAt = now();
+ delete this.stoppedAt;
+ this.startPolling();
+ document.addEventListener("visibilitychange", this.visibilityDidChange);
+ ActionCable.log("ConnectionMonitor started. pollInterval = " + this.getPollInterval() + " ms");
+ }
+ };
+ ConnectionMonitor.prototype.stop = function stop() {
+ if (this.isRunning()) {
+ this.stoppedAt = now();
+ this.stopPolling();
+ document.removeEventListener("visibilitychange", this.visibilityDidChange);
+ ActionCable.log("ConnectionMonitor stopped");
+ }
+ };
+ ConnectionMonitor.prototype.isRunning = function isRunning() {
+ return this.startedAt && !this.stoppedAt;
+ };
+ ConnectionMonitor.prototype.recordPing = function recordPing() {
+ this.pingedAt = now();
+ };
+ ConnectionMonitor.prototype.recordConnect = function recordConnect() {
+ this.reconnectAttempts = 0;
+ this.recordPing();
+ delete this.disconnectedAt;
+ ActionCable.log("ConnectionMonitor recorded connect");
+ };
+ ConnectionMonitor.prototype.recordDisconnect = function recordDisconnect() {
+ this.disconnectedAt = now();
+ ActionCable.log("ConnectionMonitor recorded disconnect");
+ };
+ ConnectionMonitor.prototype.startPolling = function startPolling() {
+ this.stopPolling();
+ this.poll();
+ };
+ ConnectionMonitor.prototype.stopPolling = function stopPolling() {
+ clearTimeout(this.pollTimeout);
+ };
+ ConnectionMonitor.prototype.poll = function poll() {
+ var _this = this;
+ this.pollTimeout = setTimeout(function() {
+ _this.reconnectIfStale();
+ _this.poll();
+ }, this.getPollInterval());
+ };
+ ConnectionMonitor.prototype.getPollInterval = function getPollInterval() {
+ var _constructor$pollInte = this.constructor.pollInterval, min = _constructor$pollInte.min, max = _constructor$pollInte.max, multiplier = _constructor$pollInte.multiplier;
+ var interval = multiplier * Math.log(this.reconnectAttempts + 1);
+ return Math.round(clamp(interval, min, max) * 1e3);
+ };
+ ConnectionMonitor.prototype.reconnectIfStale = function reconnectIfStale() {
+ if (this.connectionIsStale()) {
+ ActionCable.log("ConnectionMonitor detected stale connection. reconnectAttempts = " + this.reconnectAttempts + ", pollInterval = " + this.getPollInterval() + " ms, time disconnected = " + secondsSince(this.disconnectedAt) + " s, stale threshold = " + this.constructor.staleThreshold + " s");
+ this.reconnectAttempts++;
+ if (this.disconnectedRecently()) {
+ ActionCable.log("ConnectionMonitor skipping reopening recent disconnect");
+ } else {
+ ActionCable.log("ConnectionMonitor reopening");
+ this.connection.reopen();
+ }
+ }
+ };
+ ConnectionMonitor.prototype.connectionIsStale = function connectionIsStale() {
+ return secondsSince(this.pingedAt ? this.pingedAt : this.startedAt) > this.constructor.staleThreshold;
+ };
+ ConnectionMonitor.prototype.disconnectedRecently = function disconnectedRecently() {
+ return this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold;
+ };
+ ConnectionMonitor.prototype.visibilityDidChange = function visibilityDidChange() {
+ var _this2 = this;
+ if (document.visibilityState === "visible") {
+ setTimeout(function() {
+ if (_this2.connectionIsStale() || !_this2.connection.isOpen()) {
+ ActionCable.log("ConnectionMonitor reopening stale connection on visibilitychange. visbilityState = " + document.visibilityState);
+ _this2.connection.reopen();
+ }
+ }, 200);
+ }
+ };
+ return ConnectionMonitor;
+ }();
+ ConnectionMonitor.pollInterval = {
+ min: 3,
+ max: 30,
+ multiplier: 5
+ };
+ ConnectionMonitor.staleThreshold = 6;
+ var Consumer = function() {
+ function Consumer(url) {
+ classCallCheck(this, Consumer);
+ this.url = url;
+ this.subscriptions = new ActionCable.Subscriptions(this);
+ this.connection = new ActionCable.Connection(this);
+ }
+ Consumer.prototype.send = function send(data) {
+ return this.connection.send(data);
+ };
+ Consumer.prototype.connect = function connect() {
+ return this.connection.open();
+ };
+ Consumer.prototype.disconnect = function disconnect() {
+ return this.connection.close({
+ allowReconnect: false
+ });
+ };
+ Consumer.prototype.ensureActiveConnection = function ensureActiveConnection() {
+ if (!this.connection.isActive()) {
+ return this.connection.open();
+ }
+ };
+ return Consumer;
+ }();
+ var extend = function extend(object, properties) {
+ if (properties != null) {
+ for (var key in properties) {
+ var value = properties[key];
+ object[key] = value;
+ }
+ }
+ return object;
+ };
+ var Subscription = function() {
+ function Subscription(consumer) {
+ var params = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
+ var mixin = arguments[2];
+ classCallCheck(this, Subscription);
+ this.consumer = consumer;
+ this.identifier = JSON.stringify(params);
+ extend(this, mixin);
+ }
+ Subscription.prototype.perform = function perform(action) {
+ var data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
+ data.action = action;
+ return this.send(data);
+ };
+ Subscription.prototype.send = function send(data) {
+ return this.consumer.send({
+ command: "message",
+ identifier: this.identifier,
+ data: JSON.stringify(data)
+ });
+ };
+ Subscription.prototype.unsubscribe = function unsubscribe() {
+ return this.consumer.subscriptions.remove(this);
+ };
+ return Subscription;
+ }();
+ var Subscriptions = function() {
+ function Subscriptions(consumer) {
+ classCallCheck(this, Subscriptions);
+ this.consumer = consumer;
+ this.subscriptions = [];
+ }
+ Subscriptions.prototype.create = function create(channelName, mixin) {
+ var channel = channelName;
+ var params = (typeof channel === "undefined" ? "undefined" : _typeof(channel)) === "object" ? channel : {
+ channel: channel
+ };
+ var subscription = new ActionCable.Subscription(this.consumer, params, mixin);
+ return this.add(subscription);
+ };
+ Subscriptions.prototype.add = function add(subscription) {
+ this.subscriptions.push(subscription);
+ this.consumer.ensureActiveConnection();
+ this.notify(subscription, "initialized");
+ this.sendCommand(subscription, "subscribe");
+ return subscription;
+ };
+ Subscriptions.prototype.remove = function remove(subscription) {
+ this.forget(subscription);
+ if (!this.findAll(subscription.identifier).length) {
+ this.sendCommand(subscription, "unsubscribe");
+ }
+ return subscription;
+ };
+ Subscriptions.prototype.reject = function reject(identifier) {
+ var _this = this;
+ return this.findAll(identifier).map(function(subscription) {
+ _this.forget(subscription);
+ _this.notify(subscription, "rejected");
+ return subscription;
+ });
+ };
+ Subscriptions.prototype.forget = function forget(subscription) {
+ this.subscriptions = this.subscriptions.filter(function(s) {
+ return s !== subscription;
+ });
+ return subscription;
+ };
+ Subscriptions.prototype.findAll = function findAll(identifier) {
+ return this.subscriptions.filter(function(s) {
+ return s.identifier === identifier;
+ });
+ };
+ Subscriptions.prototype.reload = function reload() {
+ var _this2 = this;
+ return this.subscriptions.map(function(subscription) {
+ return _this2.sendCommand(subscription, "subscribe");
+ });
+ };
+ Subscriptions.prototype.notifyAll = function notifyAll(callbackName) {
+ var _this3 = this;
+ for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
+ args[_key - 1] = arguments[_key];
+ }
+ return this.subscriptions.map(function(subscription) {
+ return _this3.notify.apply(_this3, [ subscription, callbackName ].concat(args));
+ });
+ };
+ Subscriptions.prototype.notify = function notify(subscription, callbackName) {
+ for (var _len2 = arguments.length, args = Array(_len2 > 2 ? _len2 - 2 : 0), _key2 = 2; _key2 < _len2; _key2++) {
+ args[_key2 - 2] = arguments[_key2];
+ }
+ var subscriptions = void 0;
+ if (typeof subscription === "string") {
+ subscriptions = this.findAll(subscription);
+ } else {
+ subscriptions = [ subscription ];
+ }
+ return subscriptions.map(function(subscription) {
+ return typeof subscription[callbackName] === "function" ? subscription[callbackName].apply(subscription, args) : undefined;
+ });
+ };
+ Subscriptions.prototype.sendCommand = function sendCommand(subscription, command) {
+ var identifier = subscription.identifier;
+ return this.consumer.send({
+ command: command,
+ identifier: identifier
+ });
+ };
+ return Subscriptions;
+ }();
+ var ActionCable = {
+ Connection: Connection,
+ ConnectionMonitor: ConnectionMonitor,
+ Consumer: Consumer,
+ INTERNAL: INTERNAL,
+ Subscription: Subscription,
+ Subscriptions: Subscriptions,
+ WebSocket: window.WebSocket,
+ logger: window.console,
+ createConsumer: function createConsumer(url) {
+ if (url == null) {
+ var urlConfig = this.getConfig("url");
+ url = urlConfig ? urlConfig : this.INTERNAL.default_mount_path;
+ }
+ return new Consumer(this.createWebSocketURL(url));
+ },
+ getConfig: function getConfig(name) {
+ var element = document.head.querySelector("meta[name='action-cable-" + name + "']");
+ return element ? element.getAttribute("content") : undefined;
+ },
+ createWebSocketURL: function createWebSocketURL(url) {
+ if (url && !/^wss?:/i.test(url)) {
+ var a = document.createElement("a");
+ a.href = url;
+ a.href = a.href;
+ a.protocol = a.protocol.replace("http", "ws");
+ return a.href;
+ } else {
+ return url;
+ }
+ },
+ startDebugging: function startDebugging() {
+ this.debugging = true;
+ },
+ stopDebugging: function stopDebugging() {
+ this.debugging = null;
+ },
+ log: function log() {
+ if (this.debugging) {
+ var _logger;
+ for (var _len = arguments.length, messages = Array(_len), _key = 0; _key < _len; _key++) {
+ messages[_key] = arguments[_key];
+ }
+ messages.push(Date.now());
+ (_logger = this.logger).log.apply(_logger, [ "[ActionCable]" ].concat(messages));
+ }
+ }
+ };
+ return ActionCable;
+});
diff --git a/actioncable/app/javascript/action_cable/connection.js b/actioncable/app/javascript/action_cable/connection.js
new file mode 100644
index 0000000000..4ad436f2c0
--- /dev/null
+++ b/actioncable/app/javascript/action_cable/connection.js
@@ -0,0 +1,156 @@
+import ActionCable from "./index"
+import INTERNAL from "./internal"
+
+// Encapsulate the cable connection held by the consumer. This is an internal class not intended for direct user manipulation.
+
+const {message_types, protocols} = INTERNAL
+const supportedProtocols = protocols.slice(0, protocols.length - 1)
+
+const indexOf = [].indexOf
+
+class Connection {
+ constructor(consumer) {
+ this.open = this.open.bind(this)
+ this.consumer = consumer
+ this.subscriptions = this.consumer.subscriptions
+ this.monitor = new ActionCable.ConnectionMonitor(this)
+ this.disconnected = true
+ }
+
+ send(data) {
+ if (this.isOpen()) {
+ this.webSocket.send(JSON.stringify(data))
+ return true
+ } else {
+ return false
+ }
+ }
+
+ open() {
+ if (this.isActive()) {
+ ActionCable.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`)
+ return false
+ } else {
+ ActionCable.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${protocols}`)
+ if (this.webSocket) { this.uninstallEventHandlers() }
+ this.webSocket = new ActionCable.WebSocket(this.consumer.url, protocols)
+ this.installEventHandlers()
+ this.monitor.start()
+ return true
+ }
+ }
+
+ close({allowReconnect} = {allowReconnect: true}) {
+ if (!allowReconnect) { this.monitor.stop() }
+ if (this.isActive()) { return (this.webSocket ? this.webSocket.close() : undefined) }
+ }
+
+ reopen() {
+ ActionCable.log(`Reopening WebSocket, current state is ${this.getState()}`)
+ if (this.isActive()) {
+ try {
+ return this.close()
+ } catch (error) {
+ ActionCable.log("Failed to reopen WebSocket", error)
+ }
+ finally {
+ ActionCable.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`)
+ setTimeout(this.open, this.constructor.reopenDelay)
+ }
+ } else {
+ return this.open()
+ }
+ }
+
+ getProtocol() {
+ return (this.webSocket ? this.webSocket.protocol : undefined)
+ }
+
+ isOpen() {
+ return this.isState("open")
+ }
+
+ isActive() {
+ return this.isState("open", "connecting")
+ }
+
+ // Private
+
+ isProtocolSupported() {
+ return indexOf.call(supportedProtocols, this.getProtocol()) >= 0
+ }
+
+ isState(...states) {
+ return indexOf.call(states, this.getState()) >= 0
+ }
+
+ getState() {
+ if (this.webSocket) {
+ for (let state in WebSocket) {
+ if (WebSocket[state] === this.webSocket.readyState) {
+ return state.toLowerCase()
+ }
+ }
+ }
+ return null
+ }
+
+ installEventHandlers() {
+ for (let eventName in this.events) {
+ const handler = this.events[eventName].bind(this)
+ this.webSocket[`on${eventName}`] = handler
+ }
+ }
+
+ uninstallEventHandlers() {
+ for (let eventName in this.events) {
+ this.webSocket[`on${eventName}`] = function() {}
+ }
+ }
+
+}
+
+Connection.reopenDelay = 500
+
+Connection.prototype.events = {
+ message(event) {
+ if (!this.isProtocolSupported()) { return }
+ const {identifier, message, type} = JSON.parse(event.data)
+ switch (type) {
+ case message_types.welcome:
+ this.monitor.recordConnect()
+ return this.subscriptions.reload()
+ case message_types.ping:
+ return this.monitor.recordPing()
+ case message_types.confirmation:
+ return this.subscriptions.notify(identifier, "connected")
+ case message_types.rejection:
+ return this.subscriptions.reject(identifier)
+ default:
+ return this.subscriptions.notify(identifier, "received", message)
+ }
+ },
+
+ open() {
+ ActionCable.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`)
+ this.disconnected = false
+ if (!this.isProtocolSupported()) {
+ ActionCable.log("Protocol is unsupported. Stopping monitor and disconnecting.")
+ return this.close({allowReconnect: false})
+ }
+ },
+
+ close(event) {
+ ActionCable.log("WebSocket onclose event")
+ if (this.disconnected) { return }
+ this.disconnected = true
+ this.monitor.recordDisconnect()
+ return this.subscriptions.notifyAll("disconnected", {willAttemptReconnect: this.monitor.isRunning()})
+ },
+
+ error() {
+ ActionCable.log("WebSocket onerror event")
+ }
+}
+
+export default Connection
diff --git a/actioncable/app/javascript/action_cable/connection_monitor.js b/actioncable/app/javascript/action_cable/connection_monitor.js
new file mode 100644
index 0000000000..cd1e4602d8
--- /dev/null
+++ b/actioncable/app/javascript/action_cable/connection_monitor.js
@@ -0,0 +1,126 @@
+import ActionCable from "./index"
+
+// Responsible for ensuring the cable connection is in good health by validating the heartbeat pings sent from the server, and attempting
+// revival reconnections if things go astray. Internal class, not intended for direct user manipulation.
+
+const now = () => new Date().getTime()
+
+const secondsSince = time => (now() - time) / 1000
+
+const clamp = (number, min, max) => Math.max(min, Math.min(max, number))
+
+class ConnectionMonitor {
+ constructor(connection) {
+ this.visibilityDidChange = this.visibilityDidChange.bind(this)
+ this.connection = connection
+ this.reconnectAttempts = 0
+ }
+
+ start() {
+ if (!this.isRunning()) {
+ this.startedAt = now()
+ delete this.stoppedAt
+ this.startPolling()
+ document.addEventListener("visibilitychange", this.visibilityDidChange)
+ ActionCable.log(`ConnectionMonitor started. pollInterval = ${this.getPollInterval()} ms`)
+ }
+ }
+
+ stop() {
+ if (this.isRunning()) {
+ this.stoppedAt = now()
+ this.stopPolling()
+ document.removeEventListener("visibilitychange", this.visibilityDidChange)
+ ActionCable.log("ConnectionMonitor stopped")
+ }
+ }
+
+ isRunning() {
+ return this.startedAt && !this.stoppedAt
+ }
+
+ recordPing() {
+ this.pingedAt = now()
+ }
+
+ recordConnect() {
+ this.reconnectAttempts = 0
+ this.recordPing()
+ delete this.disconnectedAt
+ ActionCable.log("ConnectionMonitor recorded connect")
+ }
+
+ recordDisconnect() {
+ this.disconnectedAt = now()
+ ActionCable.log("ConnectionMonitor recorded disconnect")
+ }
+
+ // Private
+
+ startPolling() {
+ this.stopPolling()
+ this.poll()
+ }
+
+ stopPolling() {
+ clearTimeout(this.pollTimeout)
+ }
+
+ poll() {
+ this.pollTimeout = setTimeout(() => {
+ this.reconnectIfStale()
+ this.poll()
+ }
+ , this.getPollInterval())
+ }
+
+ getPollInterval() {
+ const {min, max, multiplier} = this.constructor.pollInterval
+ const interval = multiplier * Math.log(this.reconnectAttempts + 1)
+ return Math.round(clamp(interval, min, max) * 1000)
+ }
+
+ reconnectIfStale() {
+ if (this.connectionIsStale()) {
+ ActionCable.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, pollInterval = ${this.getPollInterval()} ms, time disconnected = ${secondsSince(this.disconnectedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`)
+ this.reconnectAttempts++
+ if (this.disconnectedRecently()) {
+ ActionCable.log("ConnectionMonitor skipping reopening recent disconnect")
+ } else {
+ ActionCable.log("ConnectionMonitor reopening")
+ this.connection.reopen()
+ }
+ }
+ }
+
+ connectionIsStale() {
+ return secondsSince(this.pingedAt ? this.pingedAt : this.startedAt) > this.constructor.staleThreshold
+ }
+
+ disconnectedRecently() {
+ return this.disconnectedAt && (secondsSince(this.disconnectedAt) < this.constructor.staleThreshold)
+ }
+
+ visibilityDidChange() {
+ if (document.visibilityState === "visible") {
+ setTimeout(() => {
+ if (this.connectionIsStale() || !this.connection.isOpen()) {
+ ActionCable.log(`ConnectionMonitor reopening stale connection on visibilitychange. visbilityState = ${document.visibilityState}`)
+ this.connection.reopen()
+ }
+ }
+ , 200)
+ }
+ }
+
+}
+
+ConnectionMonitor.pollInterval = {
+ min: 3,
+ max: 30,
+ multiplier: 5
+}
+
+ConnectionMonitor.staleThreshold = 6 // Server::Connections::BEAT_INTERVAL * 2 (missed two pings)
+
+export default ConnectionMonitor
diff --git a/actioncable/app/javascript/action_cable/consumer.js b/actioncable/app/javascript/action_cable/consumer.js
new file mode 100644
index 0000000000..c484ceebbd
--- /dev/null
+++ b/actioncable/app/javascript/action_cable/consumer.js
@@ -0,0 +1,53 @@
+import ActionCable from "./index"
+
+// The ActionCable.Consumer establishes the connection to a server-side Ruby Connection object. Once established,
+// the ActionCable.ConnectionMonitor will ensure that its properly maintained through heartbeats and checking for stale updates.
+// The Consumer instance is also the gateway to establishing subscriptions to desired channels through the #createSubscription
+// method.
+//
+// The following example shows how this can be setup:
+//
+// App = {}
+// App.cable = ActionCable.createConsumer("ws://example.com/accounts/1")
+// App.appearance = App.cable.subscriptions.create("AppearanceChannel")
+//
+// For more details on how you'd configure an actual channel subscription, see ActionCable.Subscription.
+//
+// When a consumer is created, it automatically connects with the server.
+//
+// To disconnect from the server, call
+//
+// App.cable.disconnect()
+//
+// and to restart the connection:
+//
+// App.cable.connect()
+//
+// Any channel subscriptions which existed prior to disconnecting will
+// automatically resubscribe.
+
+export default class Consumer {
+ constructor(url) {
+ this.url = url
+ this.subscriptions = new ActionCable.Subscriptions(this)
+ this.connection = new ActionCable.Connection(this)
+ }
+
+ send(data) {
+ return this.connection.send(data)
+ }
+
+ connect() {
+ return this.connection.open()
+ }
+
+ disconnect() {
+ return this.connection.close({allowReconnect: false})
+ }
+
+ ensureActiveConnection() {
+ if (!this.connection.isActive()) {
+ return this.connection.open()
+ }
+ }
+}
diff --git a/actioncable/app/javascript/action_cable/index.js b/actioncable/app/javascript/action_cable/index.js
new file mode 100644
index 0000000000..eb0c4844df
--- /dev/null
+++ b/actioncable/app/javascript/action_cable/index.js
@@ -0,0 +1,58 @@
+import Connection from "./connection"
+import ConnectionMonitor from "./connection_monitor"
+import Consumer from "./consumer"
+import INTERNAL from "./internal"
+import Subscription from "./subscription"
+import Subscriptions from "./subscriptions"
+
+export default {
+ Connection,
+ ConnectionMonitor,
+ Consumer,
+ INTERNAL,
+ Subscription,
+ Subscriptions,
+ WebSocket: window.WebSocket,
+ logger: window.console,
+
+ createConsumer(url) {
+ if (url == null) {
+ const urlConfig = this.getConfig("url")
+ url = (urlConfig ? urlConfig : this.INTERNAL.default_mount_path)
+ }
+ return new Consumer(this.createWebSocketURL(url))
+ },
+
+ getConfig(name) {
+ const element = document.head.querySelector(`meta[name='action-cable-${name}']`)
+ return (element ? element.getAttribute("content") : undefined)
+ },
+
+ createWebSocketURL(url) {
+ if (url && !/^wss?:/i.test(url)) {
+ const a = document.createElement("a")
+ a.href = url
+ // Fix populating Location properties in IE. Otherwise, protocol will be blank.
+ a.href = a.href
+ a.protocol = a.protocol.replace("http", "ws")
+ return a.href
+ } else {
+ return url
+ }
+ },
+
+ startDebugging() {
+ this.debugging = true
+ },
+
+ stopDebugging() {
+ this.debugging = null
+ },
+
+ log(...messages) {
+ if (this.debugging) {
+ messages.push(Date.now())
+ this.logger.log("[ActionCable]", ...messages)
+ }
+ }
+}
diff --git a/actioncable/app/javascript/action_cable/subscription.js b/actioncable/app/javascript/action_cable/subscription.js
new file mode 100644
index 0000000000..7de08f93b3
--- /dev/null
+++ b/actioncable/app/javascript/action_cable/subscription.js
@@ -0,0 +1,89 @@
+// A new subscription is created through the ActionCable.Subscriptions instance available on the consumer.
+// It provides a number of callbacks and a method for calling remote procedure calls on the corresponding
+// Channel instance on the server side.
+//
+// An example demonstrates the basic functionality:
+//
+// App.appearance = App.cable.subscriptions.create("AppearanceChannel", {
+// connected() {
+// // Called once the subscription has been successfully completed
+// },
+//
+// disconnected({ willAttemptReconnect: boolean }) {
+// // Called when the client has disconnected with the server.
+// // The object will have an `willAttemptReconnect` property which
+// // says whether the client has the intention of attempting
+// // to reconnect.
+// },
+//
+// appear() {
+// this.perform('appear', {appearing_on: this.appearingOn()})
+// },
+//
+// away() {
+// this.perform('away')
+// },
+//
+// appearingOn() {
+// $('main').data('appearing-on')
+// }
+// })
+//
+// The methods #appear and #away forward their intent to the remote AppearanceChannel instance on the server
+// by calling the `perform` method with the first parameter being the action (which maps to AppearanceChannel#appear/away).
+// The second parameter is a hash that'll get JSON encoded and made available on the server in the data parameter.
+//
+// This is how the server component would look:
+//
+// class AppearanceChannel < ApplicationActionCable::Channel
+// def subscribed
+// current_user.appear
+// end
+//
+// def unsubscribed
+// current_user.disappear
+// end
+//
+// def appear(data)
+// current_user.appear on: data['appearing_on']
+// end
+//
+// def away
+// current_user.away
+// end
+// end
+//
+// The "AppearanceChannel" name is automatically mapped between the client-side subscription creation and the server-side Ruby class name.
+// The AppearanceChannel#appear/away public methods are exposed automatically to client-side invocation through the perform method.
+
+const extend = function(object, properties) {
+ if (properties != null) {
+ for (let key in properties) {
+ const value = properties[key]
+ object[key] = value
+ }
+ }
+ return object
+}
+
+export default class Subscription {
+ constructor(consumer, params = {}, mixin) {
+ this.consumer = consumer
+ this.identifier = JSON.stringify(params)
+ extend(this, mixin)
+ }
+
+ // Perform a channel action with the optional data passed as an attribute
+ perform(action, data = {}) {
+ data.action = action
+ return this.send(data)
+ }
+
+ send(data) {
+ return this.consumer.send({command: "message", identifier: this.identifier, data: JSON.stringify(data)})
+ }
+
+ unsubscribe() {
+ return this.consumer.subscriptions.remove(this)
+ }
+}
diff --git a/actioncable/app/javascript/action_cable/subscriptions.js b/actioncable/app/javascript/action_cable/subscriptions.js
new file mode 100644
index 0000000000..712ff50d28
--- /dev/null
+++ b/actioncable/app/javascript/action_cable/subscriptions.js
@@ -0,0 +1,86 @@
+import ActionCable from "./index"
+
+// Collection class for creating (and internally managing) channel subscriptions. The only method intended to be triggered by the user
+// us ActionCable.Subscriptions#create, and it should be called through the consumer like so:
+//
+// App = {}
+// App.cable = ActionCable.createConsumer("ws://example.com/accounts/1")
+// App.appearance = App.cable.subscriptions.create("AppearanceChannel")
+//
+// For more details on how you'd configure an actual channel subscription, see ActionCable.Subscription.
+
+export default class Subscriptions {
+ constructor(consumer) {
+ this.consumer = consumer
+ this.subscriptions = []
+ }
+
+ create(channelName, mixin) {
+ const channel = channelName
+ const params = typeof channel === "object" ? channel : {channel}
+ const subscription = new ActionCable.Subscription(this.consumer, params, mixin)
+ return this.add(subscription)
+ }
+
+ // Private
+
+ add(subscription) {
+ this.subscriptions.push(subscription)
+ this.consumer.ensureActiveConnection()
+ this.notify(subscription, "initialized")
+ this.sendCommand(subscription, "subscribe")
+ return subscription
+ }
+
+ remove(subscription) {
+ this.forget(subscription)
+ if (!this.findAll(subscription.identifier).length) {
+ this.sendCommand(subscription, "unsubscribe")
+ }
+ return subscription
+ }
+
+ reject(identifier) {
+ return this.findAll(identifier).map((subscription) => {
+ this.forget(subscription)
+ this.notify(subscription, "rejected")
+ return subscription
+ })
+ }
+
+ forget(subscription) {
+ this.subscriptions = (this.subscriptions.filter((s) => s !== subscription))
+ return subscription
+ }
+
+ findAll(identifier) {
+ return this.subscriptions.filter((s) => s.identifier === identifier)
+ }
+
+ reload() {
+ return this.subscriptions.map((subscription) =>
+ this.sendCommand(subscription, "subscribe"))
+ }
+
+ notifyAll(callbackName, ...args) {
+ return this.subscriptions.map((subscription) =>
+ this.notify(subscription, callbackName, ...args))
+ }
+
+ notify(subscription, callbackName, ...args) {
+ let subscriptions
+ if (typeof subscription === "string") {
+ subscriptions = this.findAll(subscription)
+ } else {
+ subscriptions = [subscription]
+ }
+
+ return subscriptions.map((subscription) =>
+ (typeof subscription[callbackName] === "function" ? subscription[callbackName](...args) : undefined))
+ }
+
+ sendCommand(subscription, command) {
+ const {identifier} = subscription
+ return this.consumer.send({command, identifier})
+ }
+}
diff --git a/actioncable/bin/test b/actioncable/bin/test
new file mode 100755
index 0000000000..c53377cc97
--- /dev/null
+++ b/actioncable/bin/test
@@ -0,0 +1,5 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+COMPONENT_ROOT = File.expand_path("..", __dir__)
+require_relative "../../tools/test"
diff --git a/actioncable/blade.yml b/actioncable/blade.yml
new file mode 100644
index 0000000000..54d13a2ba2
--- /dev/null
+++ b/actioncable/blade.yml
@@ -0,0 +1,28 @@
+load_paths:
+ - app/assets/javascripts
+ - test/javascript/src
+ - test/javascript/vendor
+
+logical_paths:
+ - test.js
+
+plugins:
+ sauce_labs:
+ browsers:
+ Google Chrome:
+ os: Mac, Windows
+ version: -1
+ Firefox:
+ os: Mac, Windows
+ version: -1
+ Safari:
+ platform: Mac
+ version: -1
+ Microsoft Edge:
+ version: -1
+ Internet Explorer:
+ version: 11
+ iPhone:
+ version: -1
+ Motorola Droid 4 Emulator:
+ version: -1
diff --git a/actioncable/lib/action_cable.rb b/actioncable/lib/action_cable.rb
new file mode 100644
index 0000000000..d261d4112e
--- /dev/null
+++ b/actioncable/lib/action_cable.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+#--
+# Copyright (c) 2015-2018 Basecamp, LLC
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "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.
+#++
+
+require "active_support"
+require "active_support/rails"
+require "action_cable/version"
+
+module ActionCable
+ extend ActiveSupport::Autoload
+
+ INTERNAL = {
+ message_types: {
+ welcome: "welcome",
+ ping: "ping",
+ confirmation: "confirm_subscription",
+ rejection: "reject_subscription"
+ },
+ default_mount_path: "/cable",
+ protocols: ["actioncable-v1-json", "actioncable-unsupported"].freeze
+ }
+
+ # Singleton instance of the server
+ module_function def server
+ @server ||= ActionCable::Server::Base.new
+ end
+
+ autoload :Server
+ autoload :Connection
+ autoload :Channel
+ autoload :RemoteConnections
+ autoload :SubscriptionAdapter
+ autoload :TestHelper
+ autoload :TestCase
+end
diff --git a/actioncable/lib/action_cable/channel.rb b/actioncable/lib/action_cable/channel.rb
new file mode 100644
index 0000000000..d5118b9dc9
--- /dev/null
+++ b/actioncable/lib/action_cable/channel.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module ActionCable
+ module Channel
+ extend ActiveSupport::Autoload
+
+ eager_autoload do
+ autoload :Base
+ autoload :Broadcasting
+ autoload :Callbacks
+ autoload :Naming
+ autoload :PeriodicTimers
+ autoload :Streams
+ autoload :TestCase
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/channel/base.rb b/actioncable/lib/action_cable/channel/base.rb
new file mode 100644
index 0000000000..70c93ec0f3
--- /dev/null
+++ b/actioncable/lib/action_cable/channel/base.rb
@@ -0,0 +1,305 @@
+# frozen_string_literal: true
+
+require "set"
+
+module ActionCable
+ module Channel
+ # The channel provides the basic structure of grouping behavior into logical units when communicating over the WebSocket connection.
+ # You can think of a channel like a form of controller, but one that's capable of pushing content to the subscriber in addition to simply
+ # responding to the subscriber's direct requests.
+ #
+ # Channel instances are long-lived. A channel object will be instantiated when the cable consumer becomes a subscriber, and then
+ # lives until the consumer disconnects. This may be seconds, minutes, hours, or even days. That means you have to take special care
+ # not to do anything silly in a channel that would balloon its memory footprint or whatever. The references are forever, so they won't be released
+ # as is normally the case with a controller instance that gets thrown away after every request.
+ #
+ # Long-lived channels (and connections) also mean you're responsible for ensuring that the data is fresh. If you hold a reference to a user
+ # record, but the name is changed while that reference is held, you may be sending stale data if you don't take precautions to avoid it.
+ #
+ # The upside of long-lived channel instances is that you can use instance variables to keep reference to objects that future subscriber requests
+ # can interact with. Here's a quick example:
+ #
+ # class ChatChannel < ApplicationCable::Channel
+ # def subscribed
+ # @room = Chat::Room[params[:room_number]]
+ # end
+ #
+ # def speak(data)
+ # @room.speak data, user: current_user
+ # end
+ # end
+ #
+ # The #speak action simply uses the Chat::Room object that was created when the channel was first subscribed to by the consumer when that
+ # subscriber wants to say something in the room.
+ #
+ # == Action processing
+ #
+ # Unlike subclasses of ActionController::Base, channels do not follow a RESTful
+ # constraint form for their actions. Instead, Action Cable operates through a
+ # remote-procedure call model. You can declare any public method on the
+ # channel (optionally taking a <tt>data</tt> argument), and this method is
+ # automatically exposed as callable to the client.
+ #
+ # Example:
+ #
+ # class AppearanceChannel < ApplicationCable::Channel
+ # def subscribed
+ # @connection_token = generate_connection_token
+ # end
+ #
+ # def unsubscribed
+ # current_user.disappear @connection_token
+ # end
+ #
+ # def appear(data)
+ # current_user.appear @connection_token, on: data['appearing_on']
+ # end
+ #
+ # def away
+ # current_user.away @connection_token
+ # end
+ #
+ # private
+ # def generate_connection_token
+ # SecureRandom.hex(36)
+ # end
+ # end
+ #
+ # In this example, the subscribed and unsubscribed methods are not callable methods, as they
+ # were already declared in ActionCable::Channel::Base, but <tt>#appear</tt>
+ # and <tt>#away</tt> are. <tt>#generate_connection_token</tt> is also not
+ # callable, since it's a private method. You'll see that appear accepts a data
+ # parameter, which it then uses as part of its model call. <tt>#away</tt>
+ # does not, since it's simply a trigger action.
+ #
+ # Also note that in this example, <tt>current_user</tt> is available because
+ # it was marked as an identifying attribute on the connection. All such
+ # identifiers will automatically create a delegation method of the same name
+ # on the channel instance.
+ #
+ # == Rejecting subscription requests
+ #
+ # A channel can reject a subscription request in the #subscribed callback by
+ # invoking the #reject method:
+ #
+ # class ChatChannel < ApplicationCable::Channel
+ # def subscribed
+ # @room = Chat::Room[params[:room_number]]
+ # reject unless current_user.can_access?(@room)
+ # end
+ # end
+ #
+ # In this example, the subscription will be rejected if the
+ # <tt>current_user</tt> does not have access to the chat room. On the
+ # client-side, the <tt>Channel#rejected</tt> callback will get invoked when
+ # the server rejects the subscription request.
+ class Base
+ include Callbacks
+ include PeriodicTimers
+ include Streams
+ include Naming
+ include Broadcasting
+
+ attr_reader :params, :connection, :identifier
+ delegate :logger, to: :connection
+
+ class << self
+ # A list of method names that should be considered actions. This
+ # includes all public instance methods on a channel, less
+ # any internal methods (defined on Base), adding back in
+ # any methods that are internal, but still exist on the class
+ # itself.
+ #
+ # ==== Returns
+ # * <tt>Set</tt> - A set of all methods that should be considered actions.
+ def action_methods
+ @action_methods ||= begin
+ # All public instance methods of this class, including ancestors
+ methods = (public_instance_methods(true) -
+ # Except for public instance methods of Base and its ancestors
+ ActionCable::Channel::Base.public_instance_methods(true) +
+ # Be sure to include shadowed public instance methods of this class
+ public_instance_methods(false)).uniq.map(&:to_s)
+ methods.to_set
+ end
+ end
+
+ 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! # :doc:
+ @action_methods = nil
+ end
+
+ # Refresh the cached action_methods when a new action_method is added.
+ def method_added(name) # :doc:
+ super
+ clear_action_methods!
+ end
+ end
+
+ def initialize(connection, identifier, params = {})
+ @connection = connection
+ @identifier = identifier
+ @params = params
+
+ # When a channel is streaming via pubsub, we want to delay the confirmation
+ # transmission until pubsub subscription is confirmed.
+ #
+ # The counter starts at 1 because it's awaiting a call to #subscribe_to_channel
+ @defer_subscription_confirmation_counter = Concurrent::AtomicFixnum.new(1)
+
+ @reject_subscription = nil
+ @subscription_confirmation_sent = nil
+
+ delegate_connection_identifiers
+ end
+
+ # Extract the action name from the passed data and process it via the channel. The process will ensure
+ # that the action requested is a public method on the channel declared by the user (so not one of the callbacks
+ # like #subscribed).
+ def perform_action(data)
+ action = extract_action(data)
+
+ if processable_action?(action)
+ payload = { channel_class: self.class.name, action: action, data: data }
+ ActiveSupport::Notifications.instrument("perform_action.action_cable", payload) do
+ dispatch_action(action, data)
+ end
+ else
+ logger.error "Unable to process #{action_signature(action, data)}"
+ end
+ end
+
+ # This method is called after subscription has been added to the connection
+ # and confirms or rejects the subscription.
+ def subscribe_to_channel
+ run_callbacks :subscribe do
+ subscribed
+ end
+
+ reject_subscription if subscription_rejected?
+ ensure_confirmation_sent
+ end
+
+ # Called by the cable connection when it's cut, so the channel has a chance to cleanup with callbacks.
+ # This method is not intended to be called directly by the user. Instead, overwrite the #unsubscribed callback.
+ def unsubscribe_from_channel # :nodoc:
+ run_callbacks :unsubscribe do
+ unsubscribed
+ end
+ end
+
+ 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 # :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 # :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) # :doc:
+ status = "#{self.class.name} transmitting #{data.inspect.truncate(300)}"
+ status += " (via #{via})" if via
+ logger.debug(status)
+
+ payload = { channel_class: self.class.name, data: data, via: via }
+ ActiveSupport::Notifications.instrument("transmit.action_cable", payload) do
+ connection.transmit identifier: @identifier, message: data
+ end
+ end
+
+ def 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! # :doc:
+ @defer_subscription_confirmation_counter.increment
+ end
+
+ def defer_subscription_confirmation? # :doc:
+ @defer_subscription_confirmation_counter.value > 0
+ end
+
+ def subscription_confirmation_sent? # :doc:
+ @subscription_confirmation_sent
+ end
+
+ def reject # :doc:
+ @reject_subscription = true
+ end
+
+ def subscription_rejected? # :doc:
+ @reject_subscription
+ end
+
+ def delegate_connection_identifiers
+ connection.identifiers.each do |identifier|
+ define_singleton_method(identifier) do
+ connection.send(identifier)
+ end
+ end
+ end
+
+ def extract_action(data)
+ (data["action"].presence || :receive).to_sym
+ end
+
+ def processable_action?(action)
+ self.class.action_methods.include?(action.to_s) unless subscription_rejected?
+ end
+
+ def dispatch_action(action, data)
+ logger.info action_signature(action, data)
+
+ if method(action).arity == 1
+ public_send action, data
+ else
+ public_send action
+ end
+ end
+
+ def action_signature(action, data)
+ (+"#{self.class.name}##{action}").tap do |signature|
+ if (arguments = data.except("action")).any?
+ signature << "(#{arguments.inspect})"
+ end
+ end
+ end
+
+ def transmit_subscription_confirmation
+ unless subscription_confirmation_sent?
+ logger.info "#{self.class.name} is transmitting the subscription confirmation"
+
+ ActiveSupport::Notifications.instrument("transmit_subscription_confirmation.action_cable", channel_class: self.class.name) do
+ connection.transmit identifier: @identifier, type: ActionCable::INTERNAL[:message_types][:confirmation]
+ @subscription_confirmation_sent = true
+ end
+ end
+ end
+
+ def reject_subscription
+ connection.subscriptions.remove_subscription self
+ transmit_subscription_rejection
+ end
+
+ def transmit_subscription_rejection
+ logger.info "#{self.class.name} is transmitting the subscription rejection"
+
+ ActiveSupport::Notifications.instrument("transmit_subscription_rejection.action_cable", channel_class: self.class.name) do
+ connection.transmit identifier: @identifier, type: ActionCable::INTERNAL[:message_types][:rejection]
+ end
+ end
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/channel/broadcasting.rb b/actioncable/lib/action_cable/channel/broadcasting.rb
new file mode 100644
index 0000000000..9a96720f4a
--- /dev/null
+++ b/actioncable/lib/action_cable/channel/broadcasting.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/object/to_param"
+
+module ActionCable
+ module Channel
+ module Broadcasting
+ extend ActiveSupport::Concern
+
+ delegate :broadcasting_for, to: :class
+
+ module ClassMethods
+ # Broadcast a hash to a unique broadcasting for this <tt>model</tt> in this channel.
+ def broadcast_to(model, message)
+ ActionCable.server.broadcast(broadcasting_for([ channel_name, model ]), message)
+ end
+
+ def broadcasting_for(model) #:nodoc:
+ case
+ when model.is_a?(Array)
+ model.map { |m| broadcasting_for(m) }.join(":")
+ when model.respond_to?(:to_gid_param)
+ model.to_gid_param
+ else
+ model.to_param
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/channel/callbacks.rb b/actioncable/lib/action_cable/channel/callbacks.rb
new file mode 100644
index 0000000000..e4cb19b26a
--- /dev/null
+++ b/actioncable/lib/action_cable/channel/callbacks.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require "active_support/callbacks"
+
+module ActionCable
+ module Channel
+ module Callbacks
+ extend ActiveSupport::Concern
+ include ActiveSupport::Callbacks
+
+ included do
+ define_callbacks :subscribe
+ define_callbacks :unsubscribe
+ end
+
+ module ClassMethods
+ def before_subscribe(*methods, &block)
+ set_callback(:subscribe, :before, *methods, &block)
+ end
+
+ def after_subscribe(*methods, &block)
+ set_callback(:subscribe, :after, *methods, &block)
+ end
+ alias_method :on_subscribe, :after_subscribe
+
+ def before_unsubscribe(*methods, &block)
+ set_callback(:unsubscribe, :before, *methods, &block)
+ end
+
+ def after_unsubscribe(*methods, &block)
+ set_callback(:unsubscribe, :after, *methods, &block)
+ end
+ alias_method :on_unsubscribe, :after_unsubscribe
+ end
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/channel/naming.rb b/actioncable/lib/action_cable/channel/naming.rb
new file mode 100644
index 0000000000..9c324a2a53
--- /dev/null
+++ b/actioncable/lib/action_cable/channel/naming.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module ActionCable
+ module Channel
+ module Naming
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ # Returns the name of the channel, underscored, without the <tt>Channel</tt> ending.
+ # If the channel is in a namespace, then the namespaces are represented by single
+ # colon separators in the channel name.
+ #
+ # ChatChannel.channel_name # => 'chat'
+ # Chats::AppearancesChannel.channel_name # => 'chats:appearances'
+ # FooChats::BarAppearancesChannel.channel_name # => 'foo_chats:bar_appearances'
+ def channel_name
+ @channel_name ||= name.sub(/Channel$/, "").gsub("::", ":").underscore
+ end
+ end
+
+ # Delegates to the class' <tt>channel_name</tt>
+ delegate :channel_name, to: :class
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/channel/periodic_timers.rb b/actioncable/lib/action_cable/channel/periodic_timers.rb
new file mode 100644
index 0000000000..830b3efa3c
--- /dev/null
+++ b/actioncable/lib/action_cable/channel/periodic_timers.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+module ActionCable
+ module Channel
+ module PeriodicTimers
+ extend ActiveSupport::Concern
+
+ included do
+ class_attribute :periodic_timers, instance_reader: false, default: []
+
+ after_subscribe :start_periodic_timers
+ after_unsubscribe :stop_periodic_timers
+ end
+
+ module ClassMethods
+ # Periodically performs a task on the channel, like updating an online
+ # user counter, polling a backend for new status messages, sending
+ # regular "heartbeat" messages, or doing some internal work and giving
+ # progress updates.
+ #
+ # Pass a method name or lambda argument or provide a block to call.
+ # Specify the calling period in seconds using the <tt>every:</tt>
+ # keyword argument.
+ #
+ # periodically :transmit_progress, every: 5.seconds
+ #
+ # periodically every: 3.minutes do
+ # transmit action: :update_count, count: current_count
+ # end
+ #
+ def periodically(callback_or_method_name = nil, every:, &block)
+ callback =
+ if block_given?
+ raise ArgumentError, "Pass a block or provide a callback arg, not both" if callback_or_method_name
+ block
+ else
+ case callback_or_method_name
+ when Proc
+ callback_or_method_name
+ when Symbol
+ -> { __send__ callback_or_method_name }
+ else
+ raise ArgumentError, "Expected a Symbol method name or a Proc, got #{callback_or_method_name.inspect}"
+ end
+ end
+
+ unless every.kind_of?(Numeric) && every > 0
+ raise ArgumentError, "Expected every: to be a positive number of seconds, got #{every.inspect}"
+ end
+
+ self.periodic_timers += [[ callback, every: every ]]
+ end
+ end
+
+ private
+ def active_periodic_timers
+ @active_periodic_timers ||= []
+ end
+
+ def start_periodic_timers
+ self.class.periodic_timers.each do |callback, options|
+ active_periodic_timers << start_periodic_timer(callback, every: options.fetch(:every))
+ end
+ end
+
+ def start_periodic_timer(callback, every:)
+ connection.server.event_loop.timer every do
+ connection.worker_pool.async_exec self, connection: connection, &callback
+ end
+ end
+
+ def stop_periodic_timers
+ active_periodic_timers.each { |timer| timer.shutdown }
+ active_periodic_timers.clear
+ end
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/channel/streams.rb b/actioncable/lib/action_cable/channel/streams.rb
new file mode 100644
index 0000000000..81c2c38064
--- /dev/null
+++ b/actioncable/lib/action_cable/channel/streams.rb
@@ -0,0 +1,176 @@
+# frozen_string_literal: true
+
+module ActionCable
+ module Channel
+ # Streams allow channels to route broadcastings to the subscriber. A broadcasting is, as discussed elsewhere, a pubsub queue where any data
+ # placed into it is automatically sent to the clients that are connected at that time. It's purely an online queue, though. If you're not
+ # streaming a broadcasting at the very moment it sends out an update, you will not get that update, even if you connect after it has been sent.
+ #
+ # Most commonly, the streamed broadcast is sent straight to the subscriber on the client-side. The channel just acts as a connector between
+ # the two parties (the broadcaster and the channel subscriber). Here's an example of a channel that allows subscribers to get all new
+ # comments on a given page:
+ #
+ # class CommentsChannel < ApplicationCable::Channel
+ # def follow(data)
+ # stream_from "comments_for_#{data['recording_id']}"
+ # end
+ #
+ # def unfollow
+ # stop_all_streams
+ # end
+ # end
+ #
+ # Based on the above example, the subscribers of this channel will get whatever data is put into the,
+ # let's say, <tt>comments_for_45</tt> broadcasting as soon as it's put there.
+ #
+ # An example broadcasting for this channel looks like so:
+ #
+ # ActionCable.server.broadcast "comments_for_45", author: 'DHH', content: 'Rails is just swell'
+ #
+ # If you have a stream that is related to a model, then the broadcasting used can be generated from the model and channel.
+ # The following example would subscribe to a broadcasting like <tt>comments:Z2lkOi8vVGVzdEFwcC9Qb3N0LzE</tt>.
+ #
+ # class CommentsChannel < ApplicationCable::Channel
+ # def subscribed
+ # post = Post.find(params[:id])
+ # stream_for post
+ # end
+ # end
+ #
+ # You can then broadcast to this channel using:
+ #
+ # CommentsChannel.broadcast_to(@post, @comment)
+ #
+ # If you don't just want to parlay the broadcast unfiltered to the subscriber, you can also supply a callback that lets you alter what is sent out.
+ # The below example shows how you can use this to provide performance introspection in the process:
+ #
+ # class ChatChannel < ApplicationCable::Channel
+ # def subscribed
+ # @room = Chat::Room[params[:room_number]]
+ #
+ # stream_for @room, coder: ActiveSupport::JSON do |message|
+ # if message['originated_at'].present?
+ # elapsed_time = (Time.now.to_f - message['originated_at']).round(2)
+ #
+ # ActiveSupport::Notifications.instrument :performance, measurement: 'Chat.message_delay', value: elapsed_time, action: :timing
+ # logger.info "Message took #{elapsed_time}s to arrive"
+ # end
+ #
+ # transmit message
+ # end
+ # end
+ # end
+ #
+ # You can stop streaming from all broadcasts by calling #stop_all_streams.
+ module Streams
+ extend ActiveSupport::Concern
+
+ included do
+ on_unsubscribe :stop_all_streams
+ end
+
+ # Start streaming from the named <tt>broadcasting</tt> pubsub queue. Optionally, you can pass a <tt>callback</tt> that'll be used
+ # instead of the default of just transmitting the updates straight to the subscriber.
+ # Pass <tt>coder: ActiveSupport::JSON</tt> to decode messages as JSON before passing to the callback.
+ # Defaults to <tt>coder: nil</tt> which does no decoding, passes raw messages.
+ def stream_from(broadcasting, callback = nil, coder: nil, &block)
+ broadcasting = String(broadcasting)
+
+ # Don't send the confirmation until pubsub#subscribe is successful
+ defer_subscription_confirmation!
+
+ # Build a stream handler by wrapping the user-provided callback with
+ # a decoder or defaulting to a JSON-decoding retransmitter.
+ handler = worker_pool_stream_handler(broadcasting, callback || block, coder: coder)
+ streams << [ broadcasting, handler ]
+
+ connection.server.event_loop.post do
+ pubsub.subscribe(broadcasting, handler, lambda do
+ ensure_confirmation_sent
+ logger.info "#{self.class.name} is streaming from #{broadcasting}"
+ end)
+ end
+ end
+
+ # Start streaming the pubsub queue for the <tt>model</tt> in this channel. Optionally, you can pass a
+ # <tt>callback</tt> that'll be used instead of the default of just transmitting the updates straight
+ # to the subscriber.
+ #
+ # Pass <tt>coder: ActiveSupport::JSON</tt> to decode messages as JSON before passing to the callback.
+ # Defaults to <tt>coder: nil</tt> which does no decoding, passes raw messages.
+ def stream_for(model, callback = nil, coder: nil, &block)
+ stream_from(broadcasting_for([ channel_name, model ]), callback || block, coder: coder)
+ end
+
+ # Unsubscribes all streams associated with this channel from the pubsub queue.
+ def stop_all_streams
+ streams.each do |broadcasting, callback|
+ pubsub.unsubscribe broadcasting, callback
+ logger.info "#{self.class.name} stopped streaming from #{broadcasting}"
+ end.clear
+ end
+
+ private
+ delegate :pubsub, to: :connection
+
+ def streams
+ @_streams ||= []
+ end
+
+ # Always wrap the outermost handler to invoke the user handler on the
+ # worker pool rather than blocking the event loop.
+ def worker_pool_stream_handler(broadcasting, user_handler, coder: nil)
+ handler = stream_handler(broadcasting, user_handler, coder: coder)
+
+ -> message do
+ connection.worker_pool.async_invoke handler, :call, message, connection: connection
+ end
+ end
+
+ # May be overridden to add instrumentation, logging, specialized error
+ # handling, or other forms of handler decoration.
+ #
+ # TODO: Tests demonstrating this.
+ def stream_handler(broadcasting, user_handler, coder: nil)
+ if user_handler
+ stream_decoder user_handler, coder: coder
+ else
+ default_stream_handler broadcasting, coder: coder
+ end
+ end
+
+ # May be overridden to change the default stream handling behavior
+ # which decodes JSON and transmits to the client.
+ #
+ # TODO: Tests demonstrating this.
+ #
+ # TODO: Room for optimization. Update transmit API to be coder-aware
+ # so we can no-op when pubsub and connection are both JSON-encoded.
+ # Then we can skip decode+encode if we're just proxying messages.
+ def default_stream_handler(broadcasting, coder:)
+ coder ||= ActiveSupport::JSON
+ stream_transmitter stream_decoder(coder: coder), broadcasting: broadcasting
+ end
+
+ def stream_decoder(handler = identity_handler, coder:)
+ if coder
+ -> message { handler.(coder.decode(message)) }
+ else
+ handler
+ end
+ end
+
+ def stream_transmitter(handler = identity_handler, broadcasting:)
+ via = "streamed from #{broadcasting}"
+
+ -> (message) do
+ transmit handler.(message), via: via
+ end
+ end
+
+ def identity_handler
+ -> message { message }
+ end
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/channel/test_case.rb b/actioncable/lib/action_cable/channel/test_case.rb
new file mode 100644
index 0000000000..dd2762ccd0
--- /dev/null
+++ b/actioncable/lib/action_cable/channel/test_case.rb
@@ -0,0 +1,271 @@
+# frozen_string_literal: true
+
+require "active_support"
+require "active_support/test_case"
+require "active_support/core_ext/hash/indifferent_access"
+require "json"
+
+module ActionCable
+ module Channel
+ class NonInferrableChannelError < ::StandardError
+ def initialize(name)
+ super "Unable to determine the channel to test from #{name}. " +
+ "You'll need to specify it using `tests YourChannel` in your " +
+ "test case definition."
+ end
+ end
+
+ # Stub `stream_from` to track streams for the channel.
+ # Add public aliases for `subscription_confirmation_sent?` and
+ # `subscription_rejected?`.
+ module ChannelStub
+ def confirmed?
+ subscription_confirmation_sent?
+ end
+
+ def rejected?
+ subscription_rejected?
+ end
+
+ def stream_from(broadcasting, *)
+ streams << broadcasting
+ end
+
+ def stop_all_streams
+ @_streams = []
+ end
+
+ def streams
+ @_streams ||= []
+ end
+
+ # Make periodic timers no-op
+ def start_periodic_timers; end
+ alias stop_periodic_timers start_periodic_timers
+ end
+
+ class ConnectionStub
+ attr_reader :transmissions, :identifiers, :subscriptions, :logger
+
+ def initialize(identifiers = {})
+ @transmissions = []
+
+ identifiers.each do |identifier, val|
+ define_singleton_method(identifier) { val }
+ end
+
+ @subscriptions = ActionCable::Connection::Subscriptions.new(self)
+ @identifiers = identifiers.keys
+ @logger = ActiveSupport::TaggedLogging.new ActiveSupport::Logger.new(StringIO.new)
+ end
+
+ def transmit(cable_message)
+ transmissions << cable_message.with_indifferent_access
+ end
+ end
+
+ # Superclass for Action Cable channel functional tests.
+ #
+ # == Basic example
+ #
+ # Functional tests are written as follows:
+ # 1. First, one uses the +subscribe+ method to simulate subscription creation.
+ # 2. Then, one asserts whether the current state is as expected. "State" can be anything:
+ # transmitted messages, subscribed streams, etc.
+ #
+ # For example:
+ #
+ # class ChatChannelTest < ActionCable::Channel::TestCase
+ # def test_subscribed_with_room_number
+ # # Simulate a subscription creation
+ # subscribe room_number: 1
+ #
+ # # Asserts that the subscription was successfully created
+ # assert subscription.confirmed?
+ #
+ # # Asserts that the channel subscribes connection to a stream
+ # assert_equal "chat_1", streams.last
+ # end
+ #
+ # def test_does_not_subscribe_without_room_number
+ # subscribe
+ #
+ # # Asserts that the subscription was rejected
+ # assert subscription.rejected?
+ # end
+ # end
+ #
+ # You can also perform actions:
+ # def test_perform_speak
+ # subscribe room_number: 1
+ #
+ # perform :speak, message: "Hello, Rails!"
+ #
+ # assert_equal "Hello, Rails!", transmissions.last["text"]
+ # end
+ #
+ # == Special methods
+ #
+ # ActionCable::Channel::TestCase will also automatically provide the following instance
+ # methods for use in the tests:
+ #
+ # <b>connection</b>::
+ # An ActionCable::Channel::ConnectionStub, representing the current HTTP connection.
+ # <b>subscription</b>::
+ # An instance of the current channel, created when you call `subscribe`.
+ # <b>transmissions</b>::
+ # A list of all messages that have been transmitted into the channel.
+ # <b>streams</b>::
+ # A list of all created streams subscriptions (as identifiers) for the subscription.
+ #
+ #
+ # == Channel is automatically inferred
+ #
+ # ActionCable::Channel::TestCase will automatically infer the channel under test
+ # from the test class name. If the channel cannot be inferred from the test
+ # class name, you can explicitly set it with +tests+.
+ #
+ # class SpecialEdgeCaseChannelTest < ActionCable::Channel::TestCase
+ # tests SpecialChannel
+ # end
+ #
+ # == Specifying connection identifiers
+ #
+ # You need to set up your connection manually to provide values for the identifiers.
+ # To do this just use:
+ #
+ # stub_connection(user: users[:john])
+ #
+ # == Testing broadcasting
+ #
+ # ActionCable::Channel::TestCase enhances ActionCable::TestHelper assertions (e.g.
+ # +assert_broadcasts+) to handle broadcasting to models:
+ #
+ #
+ # # in your channel
+ # def speak(data)
+ # broadcast_to room, text: data["message"]
+ # end
+ #
+ # def test_speak
+ # subscribe room_id: rooms[:chat].id
+ #
+ # assert_broadcasts_on(rooms[:chat], text: "Hello, Rails!") do
+ # perform :speak, message: "Hello, Rails!"
+ # end
+ # end
+ class TestCase < ActiveSupport::TestCase
+ module Behavior
+ extend ActiveSupport::Concern
+
+ include ActiveSupport::Testing::ConstantLookup
+ include ActionCable::TestHelper
+
+ CHANNEL_IDENTIFIER = "test_stub"
+
+ included do
+ class_attribute :_channel_class
+
+ attr_reader :connection, :subscription
+ delegate :streams, to: :subscription
+
+ ActiveSupport.run_load_hooks(:action_cable_channel_test_case, self)
+ end
+
+ module ClassMethods
+ def tests(channel)
+ case channel
+ when String, Symbol
+ self._channel_class = channel.to_s.camelize.constantize
+ when Module
+ self._channel_class = channel
+ else
+ raise NonInferrableChannelError.new(channel)
+ end
+ end
+
+ def channel_class
+ if channel = self._channel_class
+ channel
+ else
+ tests determine_default_channel(name)
+ end
+ end
+
+ def determine_default_channel(name)
+ channel = determine_constant_from_test_name(name) do |constant|
+ Class === constant && constant < ActionCable::Channel::Base
+ end
+ raise NonInferrableChannelError.new(name) if channel.nil?
+ channel
+ end
+ end
+
+ # Setup test connection with the specified identifiers:
+ #
+ # class ApplicationCable < ActionCable::Connection::Base
+ # identified_by :user, :token
+ # end
+ #
+ # stub_connection(user: users[:john], token: 'my-secret-token')
+ def stub_connection(identifiers = {})
+ @connection = ConnectionStub.new(identifiers)
+ end
+
+ # Subscribe to the channel under test. Optionally pass subscription parameters as a Hash.
+ def subscribe(params = {})
+ @connection ||= stub_connection
+ @subscription = self.class.channel_class.new(connection, CHANNEL_IDENTIFIER, params.with_indifferent_access)
+ @subscription.singleton_class.include(ChannelStub)
+ @subscription.subscribe_to_channel
+ @subscription
+ end
+
+ # Unsubscribe the subscription under test.
+ def unsubscribe
+ check_subscribed!
+ subscription.unsubscribe_from_channel
+ end
+
+ # Perform action on a channel.
+ #
+ # NOTE: Must be subscribed.
+ def perform(action, data = {})
+ check_subscribed!
+ subscription.perform_action(data.stringify_keys.merge("action" => action.to_s))
+ end
+
+ # Returns messages transmitted into channel
+ def transmissions
+ # Return only directly sent message (via #transmit)
+ connection.transmissions.map { |data| data["message"] }.compact
+ end
+
+ # Enhance TestHelper assertions to handle non-String
+ # broadcastings
+ def assert_broadcasts(stream_or_object, *args)
+ super(broadcasting_for(stream_or_object), *args)
+ end
+
+ def assert_broadcast_on(stream_or_object, *args)
+ super(broadcasting_for(stream_or_object), *args)
+ end
+
+ private
+ def check_subscribed!
+ raise "Must be subscribed!" if subscription.nil? || subscription.rejected?
+ end
+
+ def broadcasting_for(stream_or_object)
+ return stream_or_object if stream_or_object.is_a?(String)
+
+ self.class.channel_class.broadcasting_for(
+ [self.class.channel_class.channel_name, stream_or_object]
+ )
+ end
+ end
+
+ include Behavior
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/connection.rb b/actioncable/lib/action_cable/connection.rb
new file mode 100644
index 0000000000..804b89a707
--- /dev/null
+++ b/actioncable/lib/action_cable/connection.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module ActionCable
+ module Connection
+ extend ActiveSupport::Autoload
+
+ eager_autoload do
+ autoload :Authorization
+ autoload :Base
+ autoload :ClientSocket
+ autoload :Identification
+ autoload :InternalChannel
+ autoload :MessageBuffer
+ autoload :Stream
+ autoload :StreamEventLoop
+ autoload :Subscriptions
+ autoload :TaggedLoggerProxy
+ autoload :WebSocket
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/connection/authorization.rb b/actioncable/lib/action_cable/connection/authorization.rb
new file mode 100644
index 0000000000..aef3386f71
--- /dev/null
+++ b/actioncable/lib/action_cable/connection/authorization.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module ActionCable
+ module Connection
+ module Authorization
+ class UnauthorizedError < StandardError; 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
new file mode 100644
index 0000000000..11a1f1a5e8
--- /dev/null
+++ b/actioncable/lib/action_cable/connection/base.rb
@@ -0,0 +1,257 @@
+# frozen_string_literal: true
+
+require "action_dispatch"
+
+module ActionCable
+ module Connection
+ # For every WebSocket connection the Action Cable server accepts, a Connection object will be instantiated. This instance becomes the parent
+ # of all of the channel subscriptions that are created from there on. Incoming messages are then routed to these channel subscriptions
+ # based on an identifier sent by the Action Cable consumer. The Connection itself does not deal with any specific application logic beyond
+ # authentication and authorization.
+ #
+ # Here's a basic example:
+ #
+ # module ApplicationCable
+ # class Connection < ActionCable::Connection::Base
+ # identified_by :current_user
+ #
+ # def connect
+ # self.current_user = find_verified_user
+ # logger.add_tags current_user.name
+ # end
+ #
+ # def disconnect
+ # # Any cleanup work needed when the cable connection is cut.
+ # end
+ #
+ # private
+ # def find_verified_user
+ # User.find_by_identity(cookies.encrypted[:identity_id]) ||
+ # reject_unauthorized_connection
+ # end
+ # end
+ # end
+ #
+ # First, we declare that this connection can be identified by its current_user. This allows us to later be able to find all connections
+ # established for that current_user (and potentially disconnect them). You can declare as many
+ # identification indexes as you like. Declaring an identification means that an attr_accessor is automatically set for that key.
+ #
+ # Second, we rely on the fact that the WebSocket connection is established with the cookies from the domain being sent along. This makes
+ # it easy to use signed cookies that were set when logging in via a web interface to authorize the WebSocket connection.
+ #
+ # Finally, we add a tag to the connection-specific logger with the name of the current user to easily distinguish their messages in the log.
+ #
+ # Pretty simple, eh?
+ class Base
+ include Identification
+ include InternalChannel
+ include Authorization
+
+ attr_reader :server, :env, :subscriptions, :logger, :worker_pool, :protocol
+ delegate :event_loop, :pubsub, to: :server
+
+ def initialize(server, env, coder: ActiveSupport::JSON)
+ @server, @env, @coder = server, env, coder
+
+ @worker_pool = server.worker_pool
+ @logger = new_tagged_logger
+
+ @websocket = ActionCable::Connection::WebSocket.new(env, self, event_loop)
+ @subscriptions = ActionCable::Connection::Subscriptions.new(self)
+ @message_buffer = ActionCable::Connection::MessageBuffer.new(self)
+
+ @_internal_subscriptions = nil
+ @started_at = Time.now
+ end
+
+ # Called by the server when a new WebSocket connection is established. This configures the callbacks intended for overwriting by the user.
+ # This method should not be called directly -- instead rely upon on the #connect (and #disconnect) callbacks.
+ def process #:nodoc:
+ logger.info started_request_message
+
+ if websocket.possible? && allow_request_origin?
+ respond_to_successful_request
+ else
+ respond_to_invalid_request
+ end
+ end
+
+ # Decodes WebSocket messages and dispatches them to subscribed channels.
+ # WebSocket message transfer encoding is always JSON.
+ def receive(websocket_message) #:nodoc:
+ send_async :dispatch_websocket_message, websocket_message
+ end
+
+ def dispatch_websocket_message(websocket_message) #:nodoc:
+ if websocket.alive?
+ subscriptions.execute_command decode(websocket_message)
+ else
+ logger.error "Ignoring message processed after the WebSocket was closed: #{websocket_message.inspect})"
+ end
+ end
+
+ def transmit(cable_message) # :nodoc:
+ websocket.transmit encode(cable_message)
+ end
+
+ # Close the WebSocket connection.
+ def close
+ websocket.close
+ end
+
+ # Invoke a method on the connection asynchronously through the pool of thread workers.
+ def send_async(method, *arguments)
+ worker_pool.async_invoke(self, method, *arguments)
+ end
+
+ # Return a basic hash of statistics for the connection keyed with <tt>identifier</tt>, <tt>started_at</tt>, <tt>subscriptions</tt>, and <tt>request_id</tt>.
+ # This can be returned by a health check against the connection.
+ def statistics
+ {
+ identifier: connection_identifier,
+ started_at: @started_at,
+ subscriptions: subscriptions.identifiers,
+ request_id: @env["action_dispatch.request_id"]
+ }
+ end
+
+ def beat
+ transmit type: ActionCable::INTERNAL[:message_types][:ping], message: Time.now.to_i
+ end
+
+ def on_open # :nodoc:
+ send_async :handle_open
+ end
+
+ def on_message(message) # :nodoc:
+ message_buffer.append message
+ end
+
+ def on_error(message) # :nodoc:
+ # log errors to make diagnosing socket errors easier
+ logger.error "WebSocket error occurred: #{message}"
+ end
+
+ def on_close(reason, code) # :nodoc:
+ send_async :handle_close
+ end
+
+ private
+ attr_reader :websocket
+ attr_reader :message_buffer
+
+ # The request that initiated the WebSocket connection is available here. This gives access to the environment, cookies, etc.
+ def request # :doc:
+ @request ||= begin
+ environment = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application
+ ActionDispatch::Request.new(environment || env)
+ end
+ end
+
+ # The cookies of the request that initiated the WebSocket connection. Useful for performing authorization checks.
+ def cookies # :doc:
+ request.cookie_jar
+ end
+
+ def encode(cable_message)
+ @coder.encode cable_message
+ end
+
+ def decode(websocket_message)
+ @coder.decode websocket_message
+ end
+
+ def handle_open
+ @protocol = websocket.protocol
+ connect if respond_to?(:connect)
+ subscribe_to_internal_channel
+ send_welcome_message
+
+ message_buffer.process!
+ server.add_connection(self)
+ rescue ActionCable::Connection::Authorization::UnauthorizedError
+ respond_to_invalid_request
+ end
+
+ def handle_close
+ logger.info finished_request_message
+
+ server.remove_connection(self)
+
+ subscriptions.unsubscribe_from_all
+ unsubscribe_from_internal_channel
+
+ disconnect if respond_to?(:disconnect)
+ end
+
+ def send_welcome_message
+ # Send welcome message to the internal connection monitor channel.
+ # This ensures the connection monitor state is reset after a successful
+ # websocket connection.
+ transmit type: ActionCable::INTERNAL[:message_types][:welcome]
+ end
+
+ def allow_request_origin?
+ return true if server.config.disable_request_forgery_protection
+
+ proto = Rack::Request.new(env).ssl? ? "https" : "http"
+ if server.config.allow_same_origin_as_host && env["HTTP_ORIGIN"] == "#{proto}://#{env['HTTP_HOST']}"
+ true
+ elsif Array(server.config.allowed_request_origins).any? { |allowed_origin| allowed_origin === env["HTTP_ORIGIN"] }
+ true
+ else
+ logger.error("Request origin not allowed: #{env['HTTP_ORIGIN']}")
+ false
+ end
+ end
+
+ def respond_to_successful_request
+ logger.info successful_request_message
+ websocket.rack_response
+ end
+
+ def respond_to_invalid_request
+ close if websocket.alive?
+
+ logger.error invalid_request_message
+ logger.info finished_request_message
+ [ 404, { "Content-Type" => "text/plain" }, [ "Page not found" ] ]
+ end
+
+ # Tags are declared in the server but computed in the connection. This allows us per-connection tailored tags.
+ def new_tagged_logger
+ TaggedLoggerProxy.new server.logger,
+ tags: server.config.log_tags.map { |tag| tag.respond_to?(:call) ? tag.call(request) : tag.to_s.camelize }
+ end
+
+ def started_request_message
+ 'Started %s "%s"%s for %s at %s' % [
+ request.request_method,
+ request.filtered_path,
+ websocket.possible? ? " [WebSocket]" : "[non-WebSocket]",
+ request.ip,
+ Time.now.to_s ]
+ end
+
+ def finished_request_message
+ 'Finished "%s"%s for %s at %s' % [
+ request.filtered_path,
+ websocket.possible? ? " [WebSocket]" : "[non-WebSocket]",
+ request.ip,
+ Time.now.to_s ]
+ end
+
+ def invalid_request_message
+ "Failed to upgrade to WebSocket (REQUEST_METHOD: %s, HTTP_CONNECTION: %s, HTTP_UPGRADE: %s)" % [
+ env["REQUEST_METHOD"], env["HTTP_CONNECTION"], env["HTTP_UPGRADE"]
+ ]
+ end
+
+ def successful_request_message
+ "Successfully upgraded to WebSocket (REQUEST_METHOD: %s, HTTP_CONNECTION: %s, HTTP_UPGRADE: %s)" % [
+ env["REQUEST_METHOD"], env["HTTP_CONNECTION"], env["HTTP_UPGRADE"]
+ ]
+ end
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/connection/client_socket.rb b/actioncable/lib/action_cable/connection/client_socket.rb
new file mode 100644
index 0000000000..4b1964c4ae
--- /dev/null
+++ b/actioncable/lib/action_cable/connection/client_socket.rb
@@ -0,0 +1,157 @@
+# frozen_string_literal: true
+
+require "websocket/driver"
+
+module ActionCable
+ module Connection
+ #--
+ # This class is heavily based on faye-websocket-ruby
+ #
+ # Copyright (c) 2010-2015 James Coglan
+ class ClientSocket # :nodoc:
+ def self.determine_url(env)
+ scheme = secure_request?(env) ? "wss:" : "ws:"
+ "#{ scheme }//#{ env['HTTP_HOST'] }#{ env['REQUEST_URI'] }"
+ end
+
+ def self.secure_request?(env)
+ return true if env["HTTPS"] == "on"
+ return true if env["HTTP_X_FORWARDED_SSL"] == "on"
+ return true if env["HTTP_X_FORWARDED_SCHEME"] == "https"
+ return true if env["HTTP_X_FORWARDED_PROTO"] == "https"
+ return true if env["rack.url_scheme"] == "https"
+
+ false
+ end
+
+ CONNECTING = 0
+ OPEN = 1
+ CLOSING = 2
+ CLOSED = 3
+
+ attr_reader :env, :url
+
+ def initialize(env, event_target, event_loop, protocols)
+ @env = env
+ @event_target = event_target
+ @event_loop = event_loop
+
+ @url = ClientSocket.determine_url(@env)
+
+ @driver = @driver_started = nil
+ @close_params = ["", 1006]
+
+ @ready_state = CONNECTING
+
+ # The driver calls +env+, +url+, and +write+
+ @driver = ::WebSocket::Driver.rack(self, protocols: protocols)
+
+ @driver.on(:open) { |e| open }
+ @driver.on(:message) { |e| receive_message(e.data) }
+ @driver.on(:close) { |e| begin_close(e.reason, e.code) }
+ @driver.on(:error) { |e| emit_error(e.message) }
+
+ @stream = ActionCable::Connection::Stream.new(@event_loop, self)
+ end
+
+ def start_driver
+ return if @driver.nil? || @driver_started
+ @stream.hijack_rack_socket
+
+ if callback = @env["async.callback"]
+ callback.call([101, {}, @stream])
+ end
+
+ @driver_started = true
+ @driver.start
+ end
+
+ def rack_response
+ start_driver
+ [ -1, {}, [] ]
+ end
+
+ def write(data)
+ @stream.write(data)
+ rescue => e
+ emit_error e.message
+ end
+
+ def transmit(message)
+ return false if @ready_state > OPEN
+ case message
+ when Numeric then @driver.text(message.to_s)
+ when String then @driver.text(message)
+ when Array then @driver.binary(message)
+ else false
+ end
+ end
+
+ def close(code = nil, reason = nil)
+ code ||= 1000
+ 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. " \
+ "#{code} is neither."
+ end
+
+ @ready_state = CLOSING unless @ready_state == CLOSED
+ @driver.close(reason, code)
+ end
+
+ def parse(data)
+ @driver.parse(data)
+ end
+
+ def client_gone
+ finalize_close
+ end
+
+ def alive?
+ @ready_state == OPEN
+ end
+
+ def protocol
+ @driver.protocol
+ end
+
+ private
+ def open
+ return unless @ready_state == CONNECTING
+ @ready_state = OPEN
+
+ @event_target.on_open
+ end
+
+ def receive_message(data)
+ return unless @ready_state == OPEN
+
+ @event_target.on_message(data)
+ end
+
+ def emit_error(message)
+ return if @ready_state >= CLOSING
+
+ @event_target.on_error(message)
+ end
+
+ def begin_close(reason, code)
+ return if @ready_state == CLOSED
+ @ready_state = CLOSING
+ @close_params = [reason, code]
+
+ @stream.shutdown if @stream
+ finalize_close
+ end
+
+ def finalize_close
+ return if @ready_state == CLOSED
+ @ready_state = CLOSED
+
+ @event_target.on_close(*@close_params)
+ end
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/connection/identification.rb b/actioncable/lib/action_cable/connection/identification.rb
new file mode 100644
index 0000000000..cc544685dd
--- /dev/null
+++ b/actioncable/lib/action_cable/connection/identification.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require "set"
+
+module ActionCable
+ module Connection
+ module Identification
+ extend ActiveSupport::Concern
+
+ included do
+ class_attribute :identifiers, default: Set.new
+ end
+
+ module ClassMethods
+ # Mark a key as being a connection identifier index that can then be used to find the specific connection again later.
+ # Common identifiers are current_user and current_account, but could be anything, really.
+ #
+ # Note that anything marked as an identifier will automatically create a delegate by the same name on any
+ # channel instances created off the connection.
+ def identified_by(*identifiers)
+ Array(identifiers).each { |identifier| attr_accessor identifier }
+ self.identifiers += identifiers
+ end
+ end
+
+ # Return a single connection identifier that combines the value of all the registered identifiers into a single gid.
+ def connection_identifier
+ unless defined? @connection_identifier
+ @connection_identifier = connection_gid identifiers.map { |id| instance_variable_get("@#{id}") }.compact
+ end
+
+ @connection_identifier
+ end
+
+ private
+ def connection_gid(ids)
+ ids.map do |o|
+ if o.respond_to? :to_gid_param
+ o.to_gid_param
+ else
+ o.to_s
+ end
+ end.sort.join(":")
+ end
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/connection/internal_channel.rb b/actioncable/lib/action_cable/connection/internal_channel.rb
new file mode 100644
index 0000000000..f03904137b
--- /dev/null
+++ b/actioncable/lib/action_cable/connection/internal_channel.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module ActionCable
+ module Connection
+ # Makes it possible for the RemoteConnection to disconnect a specific connection.
+ module InternalChannel
+ extend ActiveSupport::Concern
+
+ private
+ def internal_channel
+ "action_cable/#{connection_identifier}"
+ end
+
+ def subscribe_to_internal_channel
+ if connection_identifier.present?
+ callback = -> (message) { process_internal_message decode(message) }
+ @_internal_subscriptions ||= []
+ @_internal_subscriptions << [ internal_channel, callback ]
+
+ server.event_loop.post { pubsub.subscribe(internal_channel, callback) }
+ logger.info "Registered connection (#{connection_identifier})"
+ end
+ end
+
+ def unsubscribe_from_internal_channel
+ if @_internal_subscriptions.present?
+ @_internal_subscriptions.each { |channel, callback| server.event_loop.post { pubsub.unsubscribe(channel, callback) } }
+ end
+ end
+
+ def process_internal_message(message)
+ case message["type"]
+ when "disconnect"
+ logger.info "Removing connection (#{connection_identifier})"
+ websocket.close
+ end
+ rescue Exception => e
+ logger.error "There was an exception - #{e.class}(#{e.message})"
+ logger.error e.backtrace.join("\n")
+
+ close
+ end
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/connection/message_buffer.rb b/actioncable/lib/action_cable/connection/message_buffer.rb
new file mode 100644
index 0000000000..965841b67e
--- /dev/null
+++ b/actioncable/lib/action_cable/connection/message_buffer.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module ActionCable
+ module Connection
+ # Allows us to buffer messages received from the WebSocket before the Connection has been fully initialized, and is ready to receive them.
+ class MessageBuffer # :nodoc:
+ def initialize(connection)
+ @connection = connection
+ @buffered_messages = []
+ end
+
+ def append(message)
+ if valid? message
+ if processing?
+ receive message
+ else
+ buffer message
+ end
+ else
+ connection.logger.error "Couldn't handle non-string message: #{message.class}"
+ end
+ end
+
+ def processing?
+ @processing
+ end
+
+ def process!
+ @processing = true
+ receive_buffered_messages
+ end
+
+ private
+ attr_reader :connection
+ attr_reader :buffered_messages
+
+ def valid?(message)
+ message.is_a?(String)
+ end
+
+ def receive(message)
+ connection.receive message
+ end
+
+ def buffer(message)
+ buffered_messages << message
+ end
+
+ def receive_buffered_messages
+ receive buffered_messages.shift until buffered_messages.empty?
+ end
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/connection/stream.rb b/actioncable/lib/action_cable/connection/stream.rb
new file mode 100644
index 0000000000..e658948a55
--- /dev/null
+++ b/actioncable/lib/action_cable/connection/stream.rb
@@ -0,0 +1,117 @@
+# frozen_string_literal: true
+
+require "thread"
+
+module ActionCable
+ module Connection
+ #--
+ # This class is heavily based on faye-websocket-ruby
+ #
+ # Copyright (c) 2010-2015 James Coglan
+ class Stream # :nodoc:
+ def initialize(event_loop, socket)
+ @event_loop = event_loop
+ @socket_object = socket
+ @stream_send = socket.env["stream.send"]
+
+ @rack_hijack_io = nil
+ @write_lock = Mutex.new
+
+ @write_head = nil
+ @write_buffer = Queue.new
+ end
+
+ def each(&callback)
+ @stream_send ||= callback
+ end
+
+ def close
+ shutdown
+ @socket_object.client_gone
+ end
+
+ def shutdown
+ clean_rack_hijack
+ end
+
+ def write(data)
+ if @stream_send
+ return @stream_send.call(data)
+ end
+
+ if @write_lock.try_lock
+ begin
+ if @write_head.nil? && @write_buffer.empty?
+ written = @rack_hijack_io.write_nonblock(data, exception: false)
+
+ case written
+ when :wait_writable
+ # proceed below
+ when data.bytesize
+ return data.bytesize
+ else
+ @write_head = data.byteslice(written, data.bytesize)
+ @event_loop.writes_pending @rack_hijack_io
+
+ return data.bytesize
+ end
+ end
+ ensure
+ @write_lock.unlock
+ end
+ end
+
+ @write_buffer << data
+ @event_loop.writes_pending @rack_hijack_io
+
+ data.bytesize
+ rescue EOFError, Errno::ECONNRESET
+ @socket_object.client_gone
+ end
+
+ def flush_write_buffer
+ @write_lock.synchronize do
+ loop do
+ if @write_head.nil?
+ return true if @write_buffer.empty?
+ @write_head = @write_buffer.pop
+ end
+
+ written = @rack_hijack_io.write_nonblock(@write_head, exception: false)
+ case written
+ when :wait_writable
+ return false
+ when @write_head.bytesize
+ @write_head = nil
+ else
+ @write_head = @write_head.byteslice(written, @write_head.bytesize)
+ return false
+ end
+ end
+ end
+ end
+
+ def receive(data)
+ @socket_object.parse(data)
+ end
+
+ def hijack_rack_socket
+ return unless @socket_object.env["rack.hijack"]
+
+ # This should return the underlying io according to the SPEC:
+ @rack_hijack_io = @socket_object.env["rack.hijack"].call
+ # Retain existing behaviour if required:
+ @rack_hijack_io ||= @socket_object.env["rack.hijack_io"]
+
+ @event_loop.attach(@rack_hijack_io, self)
+ end
+
+ private
+ def clean_rack_hijack
+ return unless @rack_hijack_io
+ @event_loop.detach(@rack_hijack_io, self)
+ @rack_hijack_io = nil
+ end
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/connection/stream_event_loop.rb b/actioncable/lib/action_cable/connection/stream_event_loop.rb
new file mode 100644
index 0000000000..d95afc50ba
--- /dev/null
+++ b/actioncable/lib/action_cable/connection/stream_event_loop.rb
@@ -0,0 +1,136 @@
+# frozen_string_literal: true
+
+require "nio"
+require "thread"
+
+module ActionCable
+ module Connection
+ class StreamEventLoop
+ def initialize
+ @nio = @executor = @thread = nil
+ @map = {}
+ @stopping = false
+ @todo = Queue.new
+
+ @spawn_mutex = Mutex.new
+ end
+
+ def timer(interval, &block)
+ Concurrent::TimerTask.new(execution_interval: interval, &block).tap(&:execute)
+ end
+
+ def post(task = nil, &block)
+ task ||= block
+
+ spawn
+ @executor << task
+ end
+
+ def attach(io, stream)
+ @todo << lambda do
+ @map[io] = @nio.register(io, :r)
+ @map[io].value = stream
+ end
+ wakeup
+ end
+
+ def detach(io, stream)
+ @todo << lambda do
+ @nio.deregister io
+ @map.delete io
+ io.close
+ end
+ wakeup
+ end
+
+ def writes_pending(io)
+ @todo << lambda do
+ if monitor = @map[io]
+ monitor.interests = :rw
+ end
+ end
+ wakeup
+ end
+
+ def stop
+ @stopping = true
+ wakeup if @nio
+ end
+
+ private
+ def spawn
+ return if @thread && @thread.status
+
+ @spawn_mutex.synchronize do
+ return if @thread && @thread.status
+
+ @nio ||= NIO::Selector.new
+
+ @executor ||= Concurrent::ThreadPoolExecutor.new(
+ min_threads: 1,
+ max_threads: 10,
+ max_queue: 0,
+ )
+
+ @thread = Thread.new { run }
+
+ return true
+ end
+ end
+
+ def wakeup
+ spawn || @nio.wakeup
+ end
+
+ def run
+ loop do
+ if @stopping
+ @nio.close
+ break
+ end
+
+ until @todo.empty?
+ @todo.pop(true).call
+ end
+
+ next unless monitors = @nio.select
+
+ monitors.each do |monitor|
+ io = monitor.io
+ stream = monitor.value
+
+ begin
+ if monitor.writable?
+ if stream.flush_write_buffer
+ monitor.interests = :r
+ end
+ next unless monitor.readable?
+ end
+
+ incoming = io.read_nonblock(4096, exception: false)
+ case incoming
+ when :wait_readable
+ next
+ when nil
+ stream.close
+ else
+ stream.receive incoming
+ end
+ rescue
+ # We expect one of EOFError or Errno::ECONNRESET in
+ # normal operation (when the client goes away). But if
+ # anything else goes wrong, this is still the best way
+ # to handle it.
+ begin
+ stream.close
+ rescue
+ @nio.deregister io
+ @map.delete io
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/connection/subscriptions.rb b/actioncable/lib/action_cable/connection/subscriptions.rb
new file mode 100644
index 0000000000..1ad8d05107
--- /dev/null
+++ b/actioncable/lib/action_cable/connection/subscriptions.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/indifferent_access"
+
+module ActionCable
+ module Connection
+ # Collection class for all the channel subscriptions established on a given connection. Responsible for routing incoming commands that arrive on
+ # the connection to the proper channel.
+ class Subscriptions # :nodoc:
+ def initialize(connection)
+ @connection = connection
+ @subscriptions = {}
+ end
+
+ def execute_command(data)
+ case data["command"]
+ when "subscribe" then add data
+ when "unsubscribe" then remove data
+ when "message" then perform_action data
+ else
+ 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(" | ")}"
+ end
+
+ def add(data)
+ id_key = data["identifier"]
+ id_options = ActiveSupport::JSON.decode(id_key).with_indifferent_access
+
+ return if subscriptions.key?(id_key)
+
+ subscription_klass = id_options[:channel].safe_constantize
+
+ if subscription_klass && ActionCable::Channel::Base >= subscription_klass
+ subscription = subscription_klass.new(connection, id_key, id_options)
+ subscriptions[id_key] = subscription
+ subscription.subscribe_to_channel
+ else
+ logger.error "Subscription class not found: #{id_options[:channel].inspect}"
+ end
+ end
+
+ def remove(data)
+ logger.info "Unsubscribing from channel: #{data['identifier']}"
+ remove_subscription find(data)
+ end
+
+ def remove_subscription(subscription)
+ subscription.unsubscribe_from_channel
+ subscriptions.delete(subscription.identifier)
+ end
+
+ def perform_action(data)
+ find(data).perform_action ActiveSupport::JSON.decode(data["data"])
+ end
+
+ def identifiers
+ subscriptions.keys
+ end
+
+ def unsubscribe_from_all
+ subscriptions.each { |id, channel| remove_subscription(channel) }
+ end
+
+ private
+ attr_reader :connection, :subscriptions
+ delegate :logger, to: :connection
+
+ def find(data)
+ if subscription = subscriptions[data["identifier"]]
+ subscription
+ else
+ raise "Unable to find subscription with identifier: #{data['identifier']}"
+ end
+ end
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/connection/tagged_logger_proxy.rb b/actioncable/lib/action_cable/connection/tagged_logger_proxy.rb
new file mode 100644
index 0000000000..85831806a9
--- /dev/null
+++ b/actioncable/lib/action_cable/connection/tagged_logger_proxy.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module ActionCable
+ module Connection
+ # Allows the use of per-connection tags against the server logger. This wouldn't work using the traditional
+ # <tt>ActiveSupport::TaggedLogging</tt> enhanced Rails.logger, as that logger will reset the tags between requests.
+ # The connection is long-lived, so it needs its own set of tags for its independent duration.
+ class TaggedLoggerProxy
+ attr_reader :tags
+
+ def initialize(logger, tags:)
+ @logger = logger
+ @tags = tags.flatten
+ end
+
+ def add_tags(*tags)
+ @tags += tags.flatten
+ @tags = @tags.uniq
+ end
+
+ def tag(logger)
+ if logger.respond_to?(:tagged)
+ current_tags = tags - logger.formatter.current_tags
+ logger.tagged(*current_tags) { yield }
+ else
+ yield
+ end
+ end
+
+ %i( debug info warn error fatal unknown ).each do |severity|
+ define_method(severity) do |message|
+ log severity, message
+ end
+ end
+
+ private
+ def log(type, message) # :doc:
+ tag(@logger) { @logger.send type, message }
+ end
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/connection/web_socket.rb b/actioncable/lib/action_cable/connection/web_socket.rb
new file mode 100644
index 0000000000..31f29fdd2f
--- /dev/null
+++ b/actioncable/lib/action_cable/connection/web_socket.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require "websocket/driver"
+
+module ActionCable
+ module Connection
+ # Wrap the real socket to minimize the externally-presented API
+ class WebSocket # :nodoc:
+ def initialize(env, event_target, event_loop, protocols: ActionCable::INTERNAL[:protocols])
+ @websocket = ::WebSocket::Driver.websocket?(env) ? ClientSocket.new(env, event_target, event_loop, protocols) : nil
+ end
+
+ def possible?
+ websocket
+ end
+
+ def alive?
+ websocket && websocket.alive?
+ end
+
+ def transmit(data)
+ websocket.transmit data
+ end
+
+ def close
+ websocket.close
+ end
+
+ def protocol
+ websocket.protocol
+ end
+
+ def rack_response
+ websocket.rack_response
+ end
+
+ private
+ attr_reader :websocket
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/engine.rb b/actioncable/lib/action_cable/engine.rb
new file mode 100644
index 0000000000..53cbb597cd
--- /dev/null
+++ b/actioncable/lib/action_cable/engine.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require "rails"
+require "action_cable"
+require "action_cable/helpers/action_cable_helper"
+require "active_support/core_ext/hash/indifferent_access"
+
+module ActionCable
+ class Engine < Rails::Engine # :nodoc:
+ config.action_cable = ActiveSupport::OrderedOptions.new
+ config.action_cable.mount_path = ActionCable::INTERNAL[:default_mount_path]
+
+ config.eager_load_namespaces << ActionCable
+
+ initializer "action_cable.helpers" do
+ ActiveSupport.on_load(:action_view) do
+ include ActionCable::Helpers::ActionCableHelper
+ end
+ end
+
+ initializer "action_cable.logger" do
+ ActiveSupport.on_load(:action_cable) { self.logger ||= ::Rails.logger }
+ end
+
+ initializer "action_cable.set_configs" do |app|
+ options = app.config.action_cable
+ options.allowed_request_origins ||= /https?:\/\/localhost:\d+/ if ::Rails.env.development?
+
+ app.paths.add "config/cable", with: "config/cable.yml"
+
+ ActiveSupport.on_load(:action_cable) do
+ if (config_path = Pathname.new(app.config.paths["config/cable"].first)).exist?
+ self.cable = Rails.application.config_for(config_path).with_indifferent_access
+ end
+
+ previous_connection_class = connection_class
+ self.connection_class = -> { "ApplicationCable::Connection".safe_constantize || previous_connection_class.call }
+
+ options.each { |k, v| send("#{k}=", v) }
+ end
+ end
+
+ initializer "action_cable.routes" do
+ config.after_initialize do |app|
+ config = app.config
+ unless config.action_cable.mount_path.nil?
+ app.routes.prepend do
+ mount ActionCable.server => config.action_cable.mount_path, internal: true
+ end
+ end
+ end
+ end
+
+ initializer "action_cable.set_work_hooks" do |app|
+ ActiveSupport.on_load(:action_cable) do
+ ActionCable::Server::Worker.set_callback :work, :around, prepend: true do |_, inner|
+ app.executor.wrap do
+ # If we took a while to get the lock, we may have been halted
+ # in the meantime. As we haven't started doing any real work
+ # yet, we should pretend that we never made it off the queue.
+ unless stopping?
+ inner.call
+ end
+ end
+ end
+
+ wrap = lambda do |_, inner|
+ app.executor.wrap(&inner)
+ end
+ ActionCable::Channel::Base.set_callback :subscribe, :around, prepend: true, &wrap
+ ActionCable::Channel::Base.set_callback :unsubscribe, :around, prepend: true, &wrap
+
+ app.reloader.before_class_unload do
+ ActionCable.server.restart
+ end
+ end
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/gem_version.rb b/actioncable/lib/action_cable/gem_version.rb
new file mode 100644
index 0000000000..cd1d9bccef
--- /dev/null
+++ b/actioncable/lib/action_cable/gem_version.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module ActionCable
+ # Returns the version of the currently loaded Action Cable as a <tt>Gem::Version</tt>.
+ def self.gem_version
+ Gem::Version.new VERSION::STRING
+ end
+
+ module VERSION
+ MAJOR = 6
+ MINOR = 0
+ TINY = 0
+ PRE = "alpha"
+
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
+ end
+end
diff --git a/actioncable/lib/action_cable/helpers/action_cable_helper.rb b/actioncable/lib/action_cable/helpers/action_cable_helper.rb
new file mode 100644
index 0000000000..df16c02e83
--- /dev/null
+++ b/actioncable/lib/action_cable/helpers/action_cable_helper.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module ActionCable
+ module Helpers
+ module ActionCableHelper
+ # Returns an "action-cable-url" meta tag with the value of the URL specified in your
+ # configuration. Ensure this is above your JavaScript tag:
+ #
+ # <head>
+ # <%= action_cable_meta_tag %>
+ # <%= javascript_include_tag 'application', 'data-turbolinks-track' => 'reload' %>
+ # </head>
+ #
+ # This is then used by Action Cable to determine the URL of your WebSocket server.
+ # Your CoffeeScript can then connect to the server without needing to specify the
+ # URL directly:
+ #
+ # #= require cable
+ # @App = {}
+ # App.cable = Cable.createConsumer()
+ #
+ # Make sure to specify the correct server location in each of your environment
+ # config files:
+ #
+ # config.action_cable.mount_path = "/cable123"
+ # <%= action_cable_meta_tag %> would render:
+ # => <meta name="action-cable-url" content="/cable123" />
+ #
+ # config.action_cable.url = "ws://actioncable.com"
+ # <%= action_cable_meta_tag %> would render:
+ # => <meta name="action-cable-url" content="ws://actioncable.com" />
+ #
+ def action_cable_meta_tag
+ tag "meta", name: "action-cable-url", content: (
+ ActionCable.server.config.url ||
+ ActionCable.server.config.mount_path ||
+ raise("No Action Cable URL configured -- please configure this at config.action_cable.url")
+ )
+ end
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/remote_connections.rb b/actioncable/lib/action_cable/remote_connections.rb
new file mode 100644
index 0000000000..283400d9e7
--- /dev/null
+++ b/actioncable/lib/action_cable/remote_connections.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/redefine_method"
+
+module ActionCable
+ # If you need to disconnect a given connection, you can go through the
+ # RemoteConnections. You can find the connections you're looking for by
+ # searching for the identifier declared on the connection. For example:
+ #
+ # module ApplicationCable
+ # class Connection < ActionCable::Connection::Base
+ # identified_by :current_user
+ # ....
+ # end
+ # end
+ #
+ # ActionCable.server.remote_connections.where(current_user: User.find(1)).disconnect
+ #
+ # This will disconnect all the connections established for
+ # <tt>User.find(1)</tt>, across all servers running on all machines, because
+ # it uses the internal channel that all of these servers are subscribed to.
+ class RemoteConnections
+ attr_reader :server
+
+ def initialize(server)
+ @server = server
+ end
+
+ def where(identifier)
+ RemoteConnection.new(server, identifier)
+ end
+
+ private
+ # Represents a single remote connection found via <tt>ActionCable.server.remote_connections.where(*)</tt>.
+ # Exists solely for the purpose of calling #disconnect on that connection.
+ class RemoteConnection
+ class InvalidIdentifiersError < StandardError; end
+
+ include Connection::Identification, Connection::InternalChannel
+
+ def initialize(server, ids)
+ @server = server
+ set_identifier_instance_vars(ids)
+ end
+
+ # Uses the internal channel to disconnect the connection.
+ def disconnect
+ server.broadcast internal_channel, type: "disconnect"
+ end
+
+ # Returns all the identifiers that were applied to this connection.
+ redefine_method :identifiers do
+ server.connection_identifiers
+ end
+
+ protected
+ attr_reader :server
+
+ private
+ def set_identifier_instance_vars(ids)
+ raise InvalidIdentifiersError unless valid_identifiers?(ids)
+ ids.each { |k, v| instance_variable_set("@#{k}", v) }
+ end
+
+ def valid_identifiers?(ids)
+ keys = ids.keys
+ identifiers.all? { |id| keys.include?(id) }
+ end
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/server.rb b/actioncable/lib/action_cable/server.rb
new file mode 100644
index 0000000000..8d485a44f6
--- /dev/null
+++ b/actioncable/lib/action_cable/server.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module ActionCable
+ module Server
+ extend ActiveSupport::Autoload
+
+ eager_autoload do
+ autoload :Base
+ autoload :Broadcasting
+ autoload :Connections
+ autoload :Configuration
+
+ autoload :Worker
+ autoload :ActiveRecordConnectionManagement, "action_cable/server/worker/active_record_connection_management"
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/server/base.rb b/actioncable/lib/action_cable/server/base.rb
new file mode 100644
index 0000000000..1ee03f6dfc
--- /dev/null
+++ b/actioncable/lib/action_cable/server/base.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+require "monitor"
+
+module ActionCable
+ module Server
+ # A singleton ActionCable::Server instance is available via ActionCable.server. It's used by the Rack process that starts the Action Cable server, but
+ # is also used by the user to reach the RemoteConnections object, which is used for finding and disconnecting connections across all servers.
+ #
+ # Also, this is the server instance used for broadcasting. See Broadcasting for more information.
+ class Base
+ include ActionCable::Server::Broadcasting
+ include ActionCable::Server::Connections
+
+ cattr_accessor :config, instance_accessor: true, default: ActionCable::Server::Configuration.new
+
+ def self.logger; config.logger; end
+ delegate :logger, to: :config
+
+ attr_reader :mutex
+
+ def initialize
+ @mutex = Monitor.new
+ @remote_connections = @event_loop = @worker_pool = @pubsub = nil
+ end
+
+ # Called by Rack to setup the server.
+ def call(env)
+ setup_heartbeat_timer
+ config.connection_class.call.new(self, env).process
+ end
+
+ # Disconnect all the connections identified by +identifiers+ on this server or any others via RemoteConnections.
+ def disconnect(identifiers)
+ remote_connections.where(identifiers).disconnect
+ end
+
+ def restart
+ connections.each(&:close)
+
+ @mutex.synchronize do
+ # Shutdown the worker pool
+ @worker_pool.halt if @worker_pool
+ @worker_pool = nil
+
+ # Shutdown the pub/sub adapter
+ @pubsub.shutdown if @pubsub
+ @pubsub = nil
+ end
+ end
+
+ # Gateway to RemoteConnections. See that class for details.
+ def remote_connections
+ @remote_connections || @mutex.synchronize { @remote_connections ||= RemoteConnections.new(self) }
+ end
+
+ def event_loop
+ @event_loop || @mutex.synchronize { @event_loop ||= ActionCable::Connection::StreamEventLoop.new }
+ end
+
+ # The worker pool is where we run connection callbacks and channel actions. We do as little as possible on the server's main thread.
+ # The worker pool is an executor service that's backed by a pool of threads working from a task queue. The thread pool size maxes out
+ # at 4 worker threads by default. Tune the size yourself with <tt>config.action_cable.worker_pool_size</tt>.
+ #
+ # Using Active Record, Redis, etc within your channel actions means you'll get a separate connection from each thread in the worker pool.
+ # Plan your deployment accordingly: 5 servers each running 5 Puma workers each running an 8-thread worker pool means at least 200 database
+ # connections.
+ #
+ # Also, ensure that your database connection pool size is as least as large as your worker pool size. Otherwise, workers may oversubscribe
+ # the database connection pool and block while they wait for other workers to release their connections. Use a smaller worker pool or a larger
+ # database connection pool instead.
+ def worker_pool
+ @worker_pool || @mutex.synchronize { @worker_pool ||= ActionCable::Server::Worker.new(max_size: config.worker_pool_size) }
+ end
+
+ # Adapter used for all streams/broadcasting.
+ def pubsub
+ @pubsub || @mutex.synchronize { @pubsub ||= config.pubsub_adapter.new(self) }
+ end
+
+ # All of the identifiers applied to the connection class associated with this server.
+ def connection_identifiers
+ config.connection_class.call.identifiers
+ end
+ end
+
+ ActiveSupport.run_load_hooks(:action_cable, Base.config)
+ end
+end
diff --git a/actioncable/lib/action_cable/server/broadcasting.rb b/actioncable/lib/action_cable/server/broadcasting.rb
new file mode 100644
index 0000000000..bc54d784b3
--- /dev/null
+++ b/actioncable/lib/action_cable/server/broadcasting.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module ActionCable
+ module Server
+ # Broadcasting is how other parts of your application can send messages to a channel's subscribers. As explained in Channel, most of the time, these
+ # broadcastings are streamed directly to the clients subscribed to the named broadcasting. Let's explain with a full-stack example:
+ #
+ # class WebNotificationsChannel < ApplicationCable::Channel
+ # def subscribed
+ # stream_from "web_notifications_#{current_user.id}"
+ # end
+ # end
+ #
+ # # Somewhere in your app this is called, perhaps from a NewCommentJob:
+ # ActionCable.server.broadcast \
+ # "web_notifications_1", { title: "New things!", body: "All that's fit for print" }
+ #
+ # # Client-side CoffeeScript, which assumes you've already requested the right to send web notifications:
+ # App.cable.subscriptions.create "WebNotificationsChannel",
+ # received: (data) ->
+ # new Notification data['title'], body: data['body']
+ module Broadcasting
+ # Broadcast a hash directly to a named <tt>broadcasting</tt>. This will later be JSON encoded.
+ def broadcast(broadcasting, message, coder: ActiveSupport::JSON)
+ broadcaster_for(broadcasting, coder: coder).broadcast(message)
+ end
+
+ # Returns a broadcaster for a named <tt>broadcasting</tt> that can be reused. Useful when you have an object that
+ # may need multiple spots to transmit to a specific broadcasting over and over.
+ def broadcaster_for(broadcasting, coder: ActiveSupport::JSON)
+ Broadcaster.new(self, String(broadcasting), coder: coder)
+ end
+
+ private
+ class Broadcaster
+ attr_reader :server, :broadcasting, :coder
+
+ def initialize(server, broadcasting, coder:)
+ @server, @broadcasting, @coder = server, broadcasting, coder
+ end
+
+ def broadcast(message)
+ server.logger.debug "[ActionCable] Broadcasting to #{broadcasting}: #{message.inspect}"
+
+ payload = { broadcasting: broadcasting, message: message, coder: coder }
+ ActiveSupport::Notifications.instrument("broadcast.action_cable", payload) do
+ encoded = coder ? coder.encode(message) : message
+ server.pubsub.broadcast broadcasting, encoded
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/server/configuration.rb b/actioncable/lib/action_cable/server/configuration.rb
new file mode 100644
index 0000000000..26209537df
--- /dev/null
+++ b/actioncable/lib/action_cable/server/configuration.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+module ActionCable
+ module Server
+ # An instance of this configuration object is available via ActionCable.server.config, which allows you to tweak Action Cable configuration
+ # in a Rails config initializer.
+ class Configuration
+ attr_accessor :logger, :log_tags
+ attr_accessor :connection_class, :worker_pool_size
+ attr_accessor :disable_request_forgery_protection, :allowed_request_origins, :allow_same_origin_as_host
+ attr_accessor :cable, :url, :mount_path
+
+ def initialize
+ @log_tags = []
+
+ @connection_class = -> { ActionCable::Connection::Base }
+ @worker_pool_size = 4
+
+ @disable_request_forgery_protection = false
+ @allow_same_origin_as_host = true
+ end
+
+ # Returns constant of subscription adapter specified in config/cable.yml.
+ # If the adapter cannot be found, this will default to the Redis adapter.
+ # Also makes sure proper dependencies are required.
+ def pubsub_adapter
+ adapter = (cable.fetch("adapter") { "redis" })
+
+ # Require the adapter itself and give useful feedback about
+ # 1. Missing adapter gems and
+ # 2. Adapter gems' missing dependencies.
+ path_to_adapter = "action_cable/subscription_adapter/#{adapter}"
+ begin
+ require path_to_adapter
+ rescue LoadError => e
+ # We couldn't require the adapter itself. Raise an exception that
+ # points out config typos and missing gems.
+ if e.path == path_to_adapter
+ # We can assume that a non-builtin adapter was specified, so it's
+ # either misspelled or missing from Gemfile.
+ raise e.class, "Could not load the '#{adapter}' Action Cable pubsub adapter. Ensure that the adapter is spelled correctly in config/cable.yml and that you've added the necessary adapter gem to your Gemfile.", e.backtrace
+
+ # Bubbled up from the adapter require. Prefix the exception message
+ # with some guidance about how to address it and reraise.
+ else
+ raise e.class, "Error loading the '#{adapter}' Action Cable pubsub adapter. Missing a gem it depends on? #{e.message}", e.backtrace
+ end
+ end
+
+ adapter = adapter.camelize
+ adapter = "PostgreSQL" if adapter == "Postgresql"
+ "ActionCable::SubscriptionAdapter::#{adapter}".constantize
+ end
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/server/connections.rb b/actioncable/lib/action_cable/server/connections.rb
new file mode 100644
index 0000000000..39557d63a7
--- /dev/null
+++ b/actioncable/lib/action_cable/server/connections.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module ActionCable
+ module Server
+ # Collection class for all the connections that have been established on this specific server. Remember, usually you'll run many Action Cable servers, so
+ # you can't use this collection as a full list of all of the connections established against your application. Instead, use RemoteConnections for that.
+ module Connections # :nodoc:
+ BEAT_INTERVAL = 3
+
+ def connections
+ @connections ||= []
+ end
+
+ def add_connection(connection)
+ connections << connection
+ end
+
+ def remove_connection(connection)
+ connections.delete connection
+ end
+
+ # WebSocket connection implementations differ on when they'll mark a connection as stale. We basically never want a connection to go stale, as you
+ # then can't rely on being able to communicate with the connection. To solve this, a 3 second heartbeat runs on all connections. If the beat fails, we automatically
+ # disconnect.
+ def setup_heartbeat_timer
+ @heartbeat_timer ||= event_loop.timer(BEAT_INTERVAL) do
+ event_loop.post { connections.map(&:beat) }
+ end
+ end
+
+ def open_connections_statistics
+ connections.map(&:statistics)
+ end
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/server/worker.rb b/actioncable/lib/action_cable/server/worker.rb
new file mode 100644
index 0000000000..c69cc4ac31
--- /dev/null
+++ b/actioncable/lib/action_cable/server/worker.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require "active_support/callbacks"
+require "active_support/core_ext/module/attribute_accessors_per_thread"
+require "concurrent"
+
+module ActionCable
+ module Server
+ # Worker used by Server.send_async to do connection work in threads.
+ class Worker # :nodoc:
+ include ActiveSupport::Callbacks
+
+ thread_mattr_accessor :connection
+ define_callbacks :work
+ include ActiveRecordConnectionManagement
+
+ attr_reader :executor
+
+ def initialize(max_size: 5)
+ @executor = Concurrent::ThreadPoolExecutor.new(
+ min_threads: 1,
+ max_threads: max_size,
+ max_queue: 0,
+ )
+ end
+
+ # Stop processing work: any work that has not already started
+ # running will be discarded from the queue
+ def halt
+ @executor.shutdown
+ end
+
+ def stopping?
+ @executor.shuttingdown?
+ end
+
+ def work(connection)
+ self.connection = connection
+
+ run_callbacks :work do
+ yield
+ end
+ ensure
+ self.connection = nil
+ end
+
+ def async_exec(receiver, *args, connection:, &block)
+ async_invoke receiver, :instance_exec, *args, connection: connection, &block
+ end
+
+ def async_invoke(receiver, method, *args, connection: receiver, &block)
+ @executor.post do
+ invoke(receiver, method, *args, connection: connection, &block)
+ end
+ end
+
+ def invoke(receiver, method, *args, connection:, &block)
+ work(connection) do
+ begin
+ receiver.send method, *args, &block
+ rescue Exception => e
+ logger.error "There was an exception - #{e.class}(#{e.message})"
+ logger.error e.backtrace.join("\n")
+
+ receiver.handle_exception if receiver.respond_to?(:handle_exception)
+ end
+ end
+ end
+
+ private
+
+ def logger
+ ActionCable.server.logger
+ end
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/server/worker/active_record_connection_management.rb b/actioncable/lib/action_cable/server/worker/active_record_connection_management.rb
new file mode 100644
index 0000000000..2e378d4bf3
--- /dev/null
+++ b/actioncable/lib/action_cable/server/worker/active_record_connection_management.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module ActionCable
+ module Server
+ class Worker
+ module ActiveRecordConnectionManagement
+ extend ActiveSupport::Concern
+
+ included do
+ if defined?(ActiveRecord::Base)
+ set_callback :work, :around, :with_database_connections
+ end
+ end
+
+ def with_database_connections
+ connection.logger.tag(ActiveRecord::Base.logger) { yield }
+ end
+ end
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/subscription_adapter.rb b/actioncable/lib/action_cable/subscription_adapter.rb
new file mode 100644
index 0000000000..6a9d5c2080
--- /dev/null
+++ b/actioncable/lib/action_cable/subscription_adapter.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module ActionCable
+ module SubscriptionAdapter
+ extend ActiveSupport::Autoload
+
+ autoload :Base
+ autoload :Test
+ autoload :SubscriberMap
+ autoload :ChannelPrefix
+ end
+end
diff --git a/actioncable/lib/action_cable/subscription_adapter/async.rb b/actioncable/lib/action_cable/subscription_adapter/async.rb
new file mode 100644
index 0000000000..c9930299c7
--- /dev/null
+++ b/actioncable/lib/action_cable/subscription_adapter/async.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require "action_cable/subscription_adapter/inline"
+
+module ActionCable
+ module SubscriptionAdapter
+ class Async < Inline # :nodoc:
+ private
+ def new_subscriber_map
+ AsyncSubscriberMap.new(server.event_loop)
+ end
+
+ class AsyncSubscriberMap < SubscriberMap
+ def initialize(event_loop)
+ @event_loop = event_loop
+ super()
+ end
+
+ def add_subscriber(*)
+ @event_loop.post { super }
+ end
+
+ def invoke_callback(*)
+ @event_loop.post { super }
+ end
+ end
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/subscription_adapter/base.rb b/actioncable/lib/action_cable/subscription_adapter/base.rb
new file mode 100644
index 0000000000..34077707fd
--- /dev/null
+++ b/actioncable/lib/action_cable/subscription_adapter/base.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module ActionCable
+ module SubscriptionAdapter
+ class Base
+ attr_reader :logger, :server
+
+ def initialize(server)
+ @server = server
+ @logger = @server.logger
+ end
+
+ def broadcast(channel, payload)
+ raise NotImplementedError
+ end
+
+ def subscribe(channel, message_callback, success_callback = nil)
+ raise NotImplementedError
+ end
+
+ def unsubscribe(channel, message_callback)
+ raise NotImplementedError
+ end
+
+ def shutdown
+ raise NotImplementedError
+ end
+ end
+ 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..df0aa040f5
--- /dev/null
+++ b/actioncable/lib/action_cable/subscription_adapter/channel_prefix.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+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/inline.rb b/actioncable/lib/action_cable/subscription_adapter/inline.rb
new file mode 100644
index 0000000000..d2c85c1c8d
--- /dev/null
+++ b/actioncable/lib/action_cable/subscription_adapter/inline.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module ActionCable
+ module SubscriptionAdapter
+ class Inline < Base # :nodoc:
+ def initialize(*)
+ super
+ @subscriber_map = nil
+ end
+
+ def broadcast(channel, payload)
+ subscriber_map.broadcast(channel, payload)
+ end
+
+ def subscribe(channel, callback, success_callback = nil)
+ subscriber_map.add_subscriber(channel, callback, success_callback)
+ end
+
+ def unsubscribe(channel, callback)
+ subscriber_map.remove_subscriber(channel, callback)
+ end
+
+ def shutdown
+ # nothing to do
+ end
+
+ private
+ def subscriber_map
+ @subscriber_map || @server.mutex.synchronize { @subscriber_map ||= new_subscriber_map }
+ end
+
+ def new_subscriber_map
+ SubscriberMap.new
+ end
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/subscription_adapter/postgresql.rb b/actioncable/lib/action_cable/subscription_adapter/postgresql.rb
new file mode 100644
index 0000000000..50ec438c3a
--- /dev/null
+++ b/actioncable/lib/action_cable/subscription_adapter/postgresql.rb
@@ -0,0 +1,130 @@
+# frozen_string_literal: true
+
+gem "pg", ">= 0.18", "< 2.0"
+require "pg"
+require "thread"
+require "digest/sha1"
+
+module ActionCable
+ module SubscriptionAdapter
+ class PostgreSQL < Base # :nodoc:
+ def initialize(*)
+ super
+ @listener = nil
+ end
+
+ def broadcast(channel, payload)
+ with_broadcast_connection do |pg_conn|
+ pg_conn.exec("NOTIFY #{pg_conn.escape_identifier(channel_identifier(channel))}, '#{pg_conn.escape_string(payload)}'")
+ end
+ end
+
+ def subscribe(channel, callback, success_callback = nil)
+ listener.add_subscriber(channel_identifier(channel), callback, success_callback)
+ end
+
+ def unsubscribe(channel, callback)
+ listener.remove_subscriber(channel_identifier(channel), callback)
+ end
+
+ def shutdown
+ listener.shutdown
+ end
+
+ def with_subscriptions_connection(&block) # :nodoc:
+ ar_conn = ActiveRecord::Base.connection_pool.checkout.tap do |conn|
+ # Action Cable is taking ownership over this database connection, and
+ # will perform the necessary cleanup tasks
+ ActiveRecord::Base.connection_pool.remove(conn)
+ end
+ pg_conn = ar_conn.raw_connection
+
+ verify!(pg_conn)
+ yield pg_conn
+ ensure
+ ar_conn.disconnect!
+ end
+
+ def with_broadcast_connection(&block) # :nodoc:
+ ActiveRecord::Base.connection_pool.with_connection do |ar_conn|
+ pg_conn = ar_conn.raw_connection
+ verify!(pg_conn)
+ yield pg_conn
+ end
+ end
+
+ private
+ def channel_identifier(channel)
+ channel.size > 63 ? Digest::SHA1.hexdigest(channel) : channel
+ end
+
+ def listener
+ @listener || @server.mutex.synchronize { @listener ||= Listener.new(self, @server.event_loop) }
+ end
+
+ def verify!(pg_conn)
+ unless pg_conn.is_a?(PG::Connection)
+ raise "The Active Record database must be PostgreSQL in order to use the PostgreSQL Action Cable storage adapter"
+ end
+ end
+
+ class Listener < SubscriberMap
+ def initialize(adapter, event_loop)
+ super()
+
+ @adapter = adapter
+ @event_loop = event_loop
+ @queue = Queue.new
+
+ @thread = Thread.new do
+ Thread.current.abort_on_exception = true
+ listen
+ end
+ end
+
+ def listen
+ @adapter.with_subscriptions_connection do |pg_conn|
+ catch :shutdown do
+ loop do
+ until @queue.empty?
+ action, channel, callback = @queue.pop(true)
+
+ case action
+ when :listen
+ pg_conn.exec("LISTEN #{pg_conn.escape_identifier channel}")
+ @event_loop.post(&callback) if callback
+ when :unlisten
+ pg_conn.exec("UNLISTEN #{pg_conn.escape_identifier channel}")
+ when :shutdown
+ throw :shutdown
+ end
+ end
+
+ pg_conn.wait_for_notify(1) do |chan, pid, message|
+ broadcast(chan, message)
+ end
+ end
+ end
+ end
+ end
+
+ def shutdown
+ @queue.push([:shutdown])
+ Thread.pass while @thread.alive?
+ end
+
+ def add_channel(channel, on_success)
+ @queue.push([:listen, channel, on_success])
+ end
+
+ def remove_channel(channel)
+ @queue.push([:unlisten, channel])
+ end
+
+ def invoke_callback(*)
+ @event_loop.post { super }
+ end
+ end
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/subscription_adapter/redis.rb b/actioncable/lib/action_cable/subscription_adapter/redis.rb
new file mode 100644
index 0000000000..ad8fa52760
--- /dev/null
+++ b/actioncable/lib/action_cable/subscription_adapter/redis.rb
@@ -0,0 +1,179 @@
+# frozen_string_literal: true
+
+require "thread"
+
+gem "redis", ">= 3", "< 5"
+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 the redis gem.
+ # This is needed, for example, when using Makara proxies for distributed Redis.
+ cattr_accessor :redis_connector, default: ->(config) do
+ config[:id] ||= "ActionCable-PID-#{$$}"
+ ::Redis.new(config.slice(:url, :host, :port, :db, :password, :id))
+ end
+
+ def initialize(*)
+ super
+ @listener = nil
+ @redis_connection_for_broadcasts = nil
+ end
+
+ def broadcast(channel, payload)
+ redis_connection_for_broadcasts.publish(channel, payload)
+ end
+
+ def subscribe(channel, callback, success_callback = nil)
+ listener.add_subscriber(channel, callback, success_callback)
+ end
+
+ def unsubscribe(channel, callback)
+ listener.remove_subscriber(channel, callback)
+ end
+
+ def shutdown
+ @listener.shutdown if @listener
+ end
+
+ def redis_connection_for_subscriptions
+ redis_connection
+ end
+
+ private
+ def listener
+ @listener || @server.mutex.synchronize { @listener ||= Listener.new(self, @server.event_loop) }
+ end
+
+ def redis_connection_for_broadcasts
+ @redis_connection_for_broadcasts || @server.mutex.synchronize do
+ @redis_connection_for_broadcasts ||= redis_connection
+ end
+ end
+
+ def redis_connection
+ self.class.redis_connector.call(@server.config.cable)
+ end
+
+ class Listener < SubscriberMap
+ def initialize(adapter, event_loop)
+ super()
+
+ @adapter = adapter
+ @event_loop = event_loop
+
+ @subscribe_callbacks = Hash.new { |h, k| h[k] = [] }
+ @subscription_lock = Mutex.new
+
+ @raw_client = nil
+
+ @when_connected = []
+
+ @thread = nil
+ end
+
+ def listen(conn)
+ conn.without_reconnect do
+ original_client = conn.respond_to?(:_client) ? conn._client : conn.client
+
+ conn.subscribe("_action_cable_internal") do |on|
+ on.subscribe do |chan, count|
+ @subscription_lock.synchronize do
+ if count == 1
+ @raw_client = original_client
+
+ until @when_connected.empty?
+ @when_connected.shift.call
+ end
+ end
+
+ if callbacks = @subscribe_callbacks[chan]
+ next_callback = callbacks.shift
+ @event_loop.post(&next_callback) if next_callback
+ @subscribe_callbacks.delete(chan) if callbacks.empty?
+ end
+ end
+ end
+
+ on.message do |chan, message|
+ broadcast(chan, message)
+ end
+
+ on.unsubscribe do |chan, count|
+ if count == 0
+ @subscription_lock.synchronize do
+ @raw_client = nil
+ end
+ end
+ end
+ end
+ end
+ end
+
+ def shutdown
+ @subscription_lock.synchronize do
+ return if @thread.nil?
+
+ when_connected do
+ send_command("unsubscribe")
+ @raw_client = nil
+ end
+ end
+
+ Thread.pass while @thread.alive?
+ end
+
+ def add_channel(channel, on_success)
+ @subscription_lock.synchronize do
+ ensure_listener_running
+ @subscribe_callbacks[channel] << on_success
+ when_connected { send_command("subscribe", channel) }
+ end
+ end
+
+ def remove_channel(channel)
+ @subscription_lock.synchronize do
+ when_connected { send_command("unsubscribe", channel) }
+ end
+ end
+
+ def invoke_callback(*)
+ @event_loop.post { super }
+ end
+
+ private
+ def ensure_listener_running
+ @thread ||= Thread.new do
+ Thread.current.abort_on_exception = true
+
+ conn = @adapter.redis_connection_for_subscriptions
+ listen conn
+ end
+ end
+
+ def when_connected(&block)
+ if @raw_client
+ block.call
+ else
+ @when_connected << block
+ end
+ end
+
+ def send_command(*command)
+ @raw_client.write(command)
+
+ very_raw_connection =
+ @raw_client.connection.instance_variable_defined?(:@connection) &&
+ @raw_client.connection.instance_variable_get(:@connection)
+
+ if very_raw_connection && very_raw_connection.respond_to?(:flush)
+ very_raw_connection.flush
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/subscription_adapter/subscriber_map.rb b/actioncable/lib/action_cable/subscription_adapter/subscriber_map.rb
new file mode 100644
index 0000000000..01cdc2dfa1
--- /dev/null
+++ b/actioncable/lib/action_cable/subscription_adapter/subscriber_map.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+module ActionCable
+ module SubscriptionAdapter
+ class SubscriberMap
+ def initialize
+ @subscribers = Hash.new { |h, k| h[k] = [] }
+ @sync = Mutex.new
+ end
+
+ def add_subscriber(channel, subscriber, on_success)
+ @sync.synchronize do
+ new_channel = !@subscribers.key?(channel)
+
+ @subscribers[channel] << subscriber
+
+ if new_channel
+ add_channel channel, on_success
+ elsif on_success
+ on_success.call
+ end
+ end
+ end
+
+ def remove_subscriber(channel, subscriber)
+ @sync.synchronize do
+ @subscribers[channel].delete(subscriber)
+
+ if @subscribers[channel].empty?
+ @subscribers.delete channel
+ remove_channel channel
+ end
+ end
+ end
+
+ def broadcast(channel, message)
+ list = @sync.synchronize do
+ return if !@subscribers.key?(channel)
+ @subscribers[channel].dup
+ end
+
+ list.each do |subscriber|
+ invoke_callback(subscriber, message)
+ end
+ end
+
+ def add_channel(channel, on_success)
+ on_success.call if on_success
+ end
+
+ def remove_channel(channel)
+ end
+
+ def invoke_callback(callback, message)
+ callback.call message
+ end
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/subscription_adapter/test.rb b/actioncable/lib/action_cable/subscription_adapter/test.rb
new file mode 100644
index 0000000000..ce604cc88e
--- /dev/null
+++ b/actioncable/lib/action_cable/subscription_adapter/test.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require_relative "async"
+
+module ActionCable
+ module SubscriptionAdapter
+ # == Test adapter for Action Cable
+ #
+ # The test adapter should be used only in testing. Along with
+ # <tt>ActionCable::TestHelper</tt> it makes a great tool to test your Rails application.
+ #
+ # To use the test adapter set +adapter+ value to +test+ in your +config/cable.yml+ file.
+ #
+ # NOTE: Test adapter extends the <tt>ActionCable::SubscriptionsAdapter::Async</tt> adapter,
+ # so it could be used in system tests too.
+ class Test < Async
+ def broadcast(channel, payload)
+ broadcasts(channel) << payload
+ super
+ end
+
+ def broadcasts(channel)
+ channels_data[channel] ||= []
+ end
+
+ def clear_messages(channel)
+ channels_data[channel] = []
+ end
+
+ def clear
+ @channels_data = nil
+ end
+
+ private
+ def channels_data
+ @channels_data ||= {}
+ end
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/test_case.rb b/actioncable/lib/action_cable/test_case.rb
new file mode 100644
index 0000000000..d153259bf6
--- /dev/null
+++ b/actioncable/lib/action_cable/test_case.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require "active_support/test_case"
+
+module ActionCable
+ class TestCase < ActiveSupport::TestCase
+ include ActionCable::TestHelper
+
+ ActiveSupport.run_load_hooks(:action_cable_test_case, self)
+ end
+end
diff --git a/actioncable/lib/action_cable/test_helper.rb b/actioncable/lib/action_cable/test_helper.rb
new file mode 100644
index 0000000000..7bc877663c
--- /dev/null
+++ b/actioncable/lib/action_cable/test_helper.rb
@@ -0,0 +1,133 @@
+# frozen_string_literal: true
+
+module ActionCable
+ # Provides helper methods for testing Action Cable broadcasting
+ module TestHelper
+ def before_setup # :nodoc:
+ server = ActionCable.server
+ test_adapter = ActionCable::SubscriptionAdapter::Test.new(server)
+
+ @old_pubsub_adapter = server.pubsub
+
+ server.instance_variable_set(:@pubsub, test_adapter)
+ super
+ end
+
+ def after_teardown # :nodoc:
+ super
+ ActionCable.server.instance_variable_set(:@pubsub, @old_pubsub_adapter)
+ end
+
+ # Asserts that the number of broadcasted messages to the stream matches the given number.
+ #
+ # def test_broadcasts
+ # assert_broadcasts 'messages', 0
+ # ActionCable.server.broadcast 'messages', { text: 'hello' }
+ # assert_broadcasts 'messages', 1
+ # ActionCable.server.broadcast 'messages', { text: 'world' }
+ # assert_broadcasts 'messages', 2
+ # end
+ #
+ # If a block is passed, that block should cause the specified number of
+ # messages to be broadcasted.
+ #
+ # def test_broadcasts_again
+ # assert_broadcasts('messages', 1) do
+ # ActionCable.server.broadcast 'messages', { text: 'hello' }
+ # end
+ #
+ # assert_broadcasts('messages', 2) do
+ # ActionCable.server.broadcast 'messages', { text: 'hi' }
+ # ActionCable.server.broadcast 'messages', { text: 'how are you?' }
+ # end
+ # end
+ #
+ def assert_broadcasts(stream, number)
+ if block_given?
+ original_count = broadcasts_size(stream)
+ yield
+ new_count = broadcasts_size(stream)
+ actual_count = new_count - original_count
+ else
+ actual_count = broadcasts_size(stream)
+ end
+
+ assert_equal number, actual_count, "#{number} broadcasts to #{stream} expected, but #{actual_count} were sent"
+ end
+
+ # Asserts that no messages have been sent to the stream.
+ #
+ # def test_no_broadcasts
+ # assert_no_broadcasts 'messages'
+ # ActionCable.server.broadcast 'messages', { text: 'hi' }
+ # assert_broadcasts 'messages', 1
+ # end
+ #
+ # If a block is passed, that block should not cause any message to be sent.
+ #
+ # def test_broadcasts_again
+ # assert_no_broadcasts 'messages' do
+ # # No job messages should be sent from this block
+ # end
+ # end
+ #
+ # Note: This assertion is simply a shortcut for:
+ #
+ # assert_broadcasts 'messages', 0, &block
+ #
+ def assert_no_broadcasts(stream, &block)
+ assert_broadcasts stream, 0, &block
+ end
+
+ # Asserts that the specified message has been sent to the stream.
+ #
+ # def test_assert_transmited_message
+ # ActionCable.server.broadcast 'messages', text: 'hello'
+ # assert_broadcast_on('messages', text: 'hello')
+ # end
+ #
+ # If a block is passed, that block should cause a message with the specified data to be sent.
+ #
+ # def test_assert_broadcast_on_again
+ # assert_broadcast_on('messages', text: 'hello') do
+ # ActionCable.server.broadcast 'messages', text: 'hello'
+ # end
+ # end
+ #
+ def assert_broadcast_on(stream, data)
+ # Encode to JSON and back–we want to use this value to compare
+ # with decoded JSON.
+ # Comparing JSON strings doesn't work due to the order if the keys.
+ serialized_msg =
+ ActiveSupport::JSON.decode(ActiveSupport::JSON.encode(data))
+
+ new_messages = broadcasts(stream)
+ if block_given?
+ old_messages = new_messages
+ clear_messages(stream)
+
+ yield
+ new_messages = broadcasts(stream)
+ clear_messages(stream)
+
+ # Restore all sent messages
+ (old_messages + new_messages).each { |m| pubsub_adapter.broadcast(stream, m) }
+ end
+
+ message = new_messages.find { |msg| ActiveSupport::JSON.decode(msg) == serialized_msg }
+
+ assert message, "No messages sent with #{data} to #{stream}"
+ end
+
+ def pubsub_adapter # :nodoc:
+ ActionCable.server.pubsub
+ end
+
+ delegate :broadcasts, :clear_messages, to: :pubsub_adapter
+
+ private
+ def broadcasts_size(channel)
+ broadcasts(channel).size
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/version.rb b/actioncable/lib/action_cable/version.rb
new file mode 100644
index 0000000000..86115c6065
--- /dev/null
+++ b/actioncable/lib/action_cable/version.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require_relative "gem_version"
+
+module ActionCable
+ # Returns the version of the currently loaded Action Cable as a <tt>Gem::Version</tt>
+ def self.version
+ gem_version
+ end
+end
diff --git a/actioncable/lib/rails/generators/channel/USAGE b/actioncable/lib/rails/generators/channel/USAGE
new file mode 100644
index 0000000000..ea9662436c
--- /dev/null
+++ b/actioncable/lib/rails/generators/channel/USAGE
@@ -0,0 +1,12 @@
+Description:
+============
+ Stubs out a new cable channel for the server (in Ruby) and client (in JavaScript).
+ Pass the channel name, either CamelCased or under_scored, and an optional list of channel actions as arguments.
+
+Example:
+========
+ rails generate channel Chat speak
+
+ creates a Chat channel class and JavaScript asset:
+ Channel: app/channels/chat_channel.rb
+ Assets: app/javascript/channels/chat_channel.js
diff --git a/actioncable/lib/rails/generators/channel/channel_generator.rb b/actioncable/lib/rails/generators/channel/channel_generator.rb
new file mode 100644
index 0000000000..ef51981e89
--- /dev/null
+++ b/actioncable/lib/rails/generators/channel/channel_generator.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module Rails
+ module Generators
+ class ChannelGenerator < NamedBase
+ source_root File.expand_path("templates", __dir__)
+
+ argument :actions, type: :array, default: [], banner: "method method"
+
+ class_option :assets, type: :boolean
+
+ check_class_collision suffix: "Channel"
+
+ def create_channel_file
+ template "channel.rb", File.join("app/channels", class_path, "#{file_name}_channel.rb")
+
+ if options[:assets]
+ if behavior == :invoke
+ template "javascript/index.js", "app/javascript/channels/index.js"
+ template "javascript/consumer.js", "app/javascript/channels/consumer.js"
+ end
+
+ js_template "javascript/channel", File.join("app/javascript/channels", class_path, "#{file_name}_channel")
+ end
+
+ generate_application_cable_files
+ end
+
+ private
+ def file_name
+ @_file_name ||= super.sub(/_channel\z/i, "")
+ end
+
+ # FIXME: Change these files to symlinks once RubyGems 2.5.0 is required.
+ def generate_application_cable_files
+ return if behavior != :invoke
+
+ files = [
+ "application_cable/channel.rb",
+ "application_cable/connection.rb"
+ ]
+
+ files.each do |name|
+ path = File.join("app/channels/", name)
+ template(name, path) if !File.exist?(path)
+ end
+ end
+ end
+ end
+end
diff --git a/actioncable/lib/rails/generators/channel/templates/application_cable/channel.rb.tt b/actioncable/lib/rails/generators/channel/templates/application_cable/channel.rb.tt
new file mode 100644
index 0000000000..d672697283
--- /dev/null
+++ b/actioncable/lib/rails/generators/channel/templates/application_cable/channel.rb.tt
@@ -0,0 +1,4 @@
+module ApplicationCable
+ class Channel < ActionCable::Channel::Base
+ end
+end
diff --git a/actioncable/lib/rails/generators/channel/templates/application_cable/connection.rb.tt b/actioncable/lib/rails/generators/channel/templates/application_cable/connection.rb.tt
new file mode 100644
index 0000000000..0ff5442f47
--- /dev/null
+++ b/actioncable/lib/rails/generators/channel/templates/application_cable/connection.rb.tt
@@ -0,0 +1,4 @@
+module ApplicationCable
+ class Connection < ActionCable::Connection::Base
+ end
+end
diff --git a/actioncable/lib/rails/generators/channel/templates/channel.rb.tt b/actioncable/lib/rails/generators/channel/templates/channel.rb.tt
new file mode 100644
index 0000000000..4bcfb2be4d
--- /dev/null
+++ b/actioncable/lib/rails/generators/channel/templates/channel.rb.tt
@@ -0,0 +1,16 @@
+<% module_namespacing do -%>
+class <%= class_name %>Channel < ApplicationCable::Channel
+ def subscribed
+ # stream_from "some_channel"
+ end
+
+ def unsubscribed
+ # Any cleanup needed when channel is unsubscribed
+ end
+<% actions.each do |action| -%>
+
+ def <%= action %>
+ end
+<% end -%>
+end
+<% end -%>
diff --git a/actioncable/lib/rails/generators/channel/templates/javascript/channel.js.tt b/actioncable/lib/rails/generators/channel/templates/javascript/channel.js.tt
new file mode 100644
index 0000000000..33baaa5a22
--- /dev/null
+++ b/actioncable/lib/rails/generators/channel/templates/javascript/channel.js.tt
@@ -0,0 +1,20 @@
+import consumer from "./consumer"
+
+consumer.subscriptions.create("<%= class_name %>Channel", {
+ connected: function() {
+ // Called when the subscription is ready for use on the server
+ },
+
+ disconnected: function() {
+ // Called when the subscription has been terminated by the server
+ },
+
+ received: function(data) {
+ // Called when there's incoming data on the websocket for this channel
+ }<%= actions.any? ? ",\n" : '' %>
+<% actions.each do |action| -%>
+ <%=action %>: function() {
+ return this.perform('<%= action %>');
+ }<%= action == actions[-1] ? '' : ",\n" %>
+<% end -%>
+});
diff --git a/actioncable/lib/rails/generators/channel/templates/javascript/consumer.js.tt b/actioncable/lib/rails/generators/channel/templates/javascript/consumer.js.tt
new file mode 100644
index 0000000000..76ca3d0f2f
--- /dev/null
+++ b/actioncable/lib/rails/generators/channel/templates/javascript/consumer.js.tt
@@ -0,0 +1,6 @@
+// Action Cable provides the framework to deal with WebSockets in Rails.
+// You can generate new channels where WebSocket features live using the `rails generate channel` command.
+
+import ActionCable from "actioncable"
+
+export default ActionCable.createConsumer()
diff --git a/actioncable/lib/rails/generators/channel/templates/javascript/index.js.tt b/actioncable/lib/rails/generators/channel/templates/javascript/index.js.tt
new file mode 100644
index 0000000000..0cfcf74919
--- /dev/null
+++ b/actioncable/lib/rails/generators/channel/templates/javascript/index.js.tt
@@ -0,0 +1,5 @@
+// Load all the channels within this directory and all subdirectories.
+// Channel files must be named *_channel.js.
+
+const channels = require.context('.', true, /_channel\.js$/)
+channels.keys().forEach(channels)
diff --git a/actioncable/package.json b/actioncable/package.json
new file mode 100644
index 0000000000..5d5105a6d5
--- /dev/null
+++ b/actioncable/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "actioncable",
+ "version": "6.0.0-alpha",
+ "description": "WebSocket framework for Ruby on Rails.",
+ "main": "app/assets/javascripts/action_cable.js",
+ "files": [
+ "app/assets/javascripts/*.js"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "rails/rails"
+ },
+ "keywords": [
+ "websockets",
+ "actioncable",
+ "rails"
+ ],
+ "author": "David Heinemeier Hansson <david@loudthinking.com>",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/rails/rails/issues"
+ },
+ "homepage": "http://rubyonrails.org/",
+ "devDependencies": {
+ "babel-core": "^6.25.0",
+ "babel-plugin-external-helpers": "^6.22.0",
+ "babel-preset-env": "^1.6.0",
+ "eslint": "^4.3.0",
+ "eslint-plugin-import": "^2.7.0",
+ "rollup": "^0.58.2",
+ "rollup-plugin-babel": "^3.0.4",
+ "rollup-plugin-commonjs": "^9.1.0",
+ "rollup-plugin-node-resolve": "^3.3.0",
+ "rollup-plugin-uglify": "^3.0.0"
+ },
+ "scripts": {
+ "prebuild": "yarn lint && bundle exec rake assets:codegen",
+ "build": "rollup --config rollup.config.js",
+ "lint": "eslint app/javascript"
+ }
+}
diff --git a/actioncable/rollup.config.js b/actioncable/rollup.config.js
new file mode 100644
index 0000000000..03046526aa
--- /dev/null
+++ b/actioncable/rollup.config.js
@@ -0,0 +1,28 @@
+import resolve from "rollup-plugin-node-resolve"
+import commonjs from "rollup-plugin-commonjs"
+import babel from "rollup-plugin-babel"
+import uglify from "rollup-plugin-uglify"
+
+const uglifyOptions = {
+ mangle: false,
+ compress: false,
+ output: {
+ beautify: true,
+ indent_level: 2
+ }
+}
+
+export default {
+ input: "app/javascript/action_cable/index.js",
+ output: {
+ file: "app/assets/javascripts/action_cable.js",
+ format: "umd",
+ name: "ActionCable"
+ },
+ plugins: [
+ resolve(),
+ commonjs(),
+ babel(),
+ uglify(uglifyOptions)
+ ]
+}
diff --git a/actioncable/test/channel/base_test.rb b/actioncable/test/channel/base_test.rb
new file mode 100644
index 0000000000..eb0e1673b0
--- /dev/null
+++ b/actioncable/test/channel/base_test.rb
@@ -0,0 +1,274 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "minitest/mock"
+require "stubs/test_connection"
+require "stubs/room"
+
+class ActionCable::Channel::BaseTest < ActionCable::TestCase
+ class ActionCable::Channel::Base
+ def kick
+ @last_action = [ :kick ]
+ end
+
+ def topic
+ end
+ end
+
+ class BasicChannel < ActionCable::Channel::Base
+ def chatters
+ @last_action = [ :chatters ]
+ end
+ end
+
+ class ChatChannel < BasicChannel
+ attr_reader :room, :last_action
+ after_subscribe :toggle_subscribed
+ after_unsubscribe :toggle_subscribed
+
+ def initialize(*)
+ @subscribed = false
+ super
+ end
+
+ def subscribed
+ @room = Room.new params[:id]
+ @actions = []
+ end
+
+ def unsubscribed
+ @room = nil
+ end
+
+ def toggle_subscribed
+ @subscribed = !@subscribed
+ end
+
+ def leave
+ @last_action = [ :leave ]
+ end
+
+ def speak(data)
+ @last_action = [ :speak, data ]
+ end
+
+ def topic(data)
+ @last_action = [ :topic, data ]
+ end
+
+ def subscribed?
+ @subscribed
+ end
+
+ def get_latest
+ transmit data: "latest"
+ end
+
+ def receive
+ @last_action = [ :receive ]
+ end
+
+ private
+ def rm_rf
+ @last_action = [ :rm_rf ]
+ end
+ end
+
+ setup do
+ @user = User.new "lifo"
+ @connection = TestConnection.new(@user)
+ @channel = ChatChannel.new @connection, "{id: 1}", id: 1
+ end
+
+ test "should subscribe to a channel" do
+ @channel.subscribe_to_channel
+ assert_equal 1, @channel.room.id
+ end
+
+ test "on subscribe callbacks" do
+ @channel.subscribe_to_channel
+ assert @channel.subscribed
+ end
+
+ test "channel params" do
+ assert_equal({ id: 1 }, @channel.params)
+ end
+
+ test "unsubscribing from a channel" do
+ @channel.subscribe_to_channel
+
+ assert @channel.room
+ assert_predicate @channel, :subscribed?
+
+ @channel.unsubscribe_from_channel
+
+ assert_not @channel.room
+ assert_not_predicate @channel, :subscribed?
+ end
+
+ test "connection identifiers" do
+ assert_equal @user.name, @channel.current_user.name
+ end
+
+ test "callable action without any argument" do
+ @channel.perform_action "action" => :leave
+ assert_equal [ :leave ], @channel.last_action
+ end
+
+ test "callable action with arguments" do
+ data = { "action" => :speak, "content" => "Hello World" }
+
+ @channel.perform_action data
+ assert_equal [ :speak, data ], @channel.last_action
+ end
+
+ test "should not dispatch a private method" do
+ @channel.perform_action "action" => :rm_rf
+ assert_nil @channel.last_action
+ end
+
+ test "should not dispatch a public method defined on Base" do
+ @channel.perform_action "action" => :kick
+ assert_nil @channel.last_action
+ end
+
+ test "should dispatch a public method defined on Base and redefined on channel" do
+ data = { "action" => :topic, "content" => "This is Sparta!" }
+
+ @channel.perform_action data
+ assert_equal [ :topic, data ], @channel.last_action
+ end
+
+ test "should dispatch calling a public method defined in an ancestor" do
+ @channel.perform_action "action" => :chatters
+ assert_equal [ :chatters ], @channel.last_action
+ end
+
+ test "should dispatch receive action when perform_action is called with empty action" do
+ data = { "content" => "hello" }
+ @channel.perform_action data
+ assert_equal [ :receive ], @channel.last_action
+ end
+
+ test "transmitting data" do
+ @channel.perform_action "action" => :get_latest
+
+ expected = { "identifier" => "{id: 1}", "message" => { "data" => "latest" } }
+ assert_equal expected, @connection.last_transmission
+ end
+
+ test "do not send subscription confirmation on initialize" do
+ assert_nil @connection.last_transmission
+ end
+
+ test "subscription confirmation on subscribe_to_channel" do
+ expected = { "identifier" => "{id: 1}", "type" => "confirm_subscription" }
+ @channel.subscribe_to_channel
+ assert_equal expected, @connection.last_transmission
+ end
+
+ test "actions available on Channel" do
+ available_actions = %w(room last_action subscribed unsubscribed toggle_subscribed leave speak subscribed? get_latest receive chatters topic).to_set
+ assert_equal available_actions, ChatChannel.action_methods
+ end
+
+ test "invalid action on Channel" do
+ assert_logged("Unable to process ActionCable::Channel::BaseTest::ChatChannel#invalid_action") do
+ @channel.perform_action "action" => :invalid_action
+ end
+ end
+
+ test "notification for perform_action" do
+ begin
+ events = []
+ ActiveSupport::Notifications.subscribe "perform_action.action_cable" do |*args|
+ events << ActiveSupport::Notifications::Event.new(*args)
+ end
+
+ data = { "action" => :speak, "content" => "hello" }
+ @channel.perform_action data
+
+ assert_equal 1, events.length
+ assert_equal "perform_action.action_cable", events[0].name
+ assert_equal "ActionCable::Channel::BaseTest::ChatChannel", events[0].payload[:channel_class]
+ assert_equal :speak, events[0].payload[:action]
+ assert_equal data, events[0].payload[:data]
+ ensure
+ ActiveSupport::Notifications.unsubscribe "perform_action.action_cable"
+ end
+ end
+
+ test "notification for transmit" do
+ begin
+ events = []
+ ActiveSupport::Notifications.subscribe "transmit.action_cable" do |*args|
+ events << ActiveSupport::Notifications::Event.new(*args)
+ end
+
+ @channel.perform_action "action" => :get_latest
+ expected_data = { data: "latest" }
+
+ assert_equal 1, events.length
+ assert_equal "transmit.action_cable", events[0].name
+ assert_equal "ActionCable::Channel::BaseTest::ChatChannel", events[0].payload[:channel_class]
+ assert_equal expected_data, events[0].payload[:data]
+ assert_nil events[0].payload[:via]
+ ensure
+ ActiveSupport::Notifications.unsubscribe "transmit.action_cable"
+ end
+ end
+
+ test "notification for transmit_subscription_confirmation" do
+ begin
+ @channel.subscribe_to_channel
+
+ events = []
+ ActiveSupport::Notifications.subscribe "transmit_subscription_confirmation.action_cable" do |*args|
+ events << ActiveSupport::Notifications::Event.new(*args)
+ end
+
+ @channel.stub(:subscription_confirmation_sent?, false) do
+ @channel.send(:transmit_subscription_confirmation)
+
+ assert_equal 1, events.length
+ assert_equal "transmit_subscription_confirmation.action_cable", events[0].name
+ assert_equal "ActionCable::Channel::BaseTest::ChatChannel", events[0].payload[:channel_class]
+ end
+ ensure
+ ActiveSupport::Notifications.unsubscribe "transmit_subscription_confirmation.action_cable"
+ end
+ end
+
+ test "notification for transmit_subscription_rejection" do
+ begin
+ events = []
+ ActiveSupport::Notifications.subscribe "transmit_subscription_rejection.action_cable" do |*args|
+ events << ActiveSupport::Notifications::Event.new(*args)
+ end
+
+ @channel.send(:transmit_subscription_rejection)
+
+ assert_equal 1, events.length
+ assert_equal "transmit_subscription_rejection.action_cable", events[0].name
+ assert_equal "ActionCable::Channel::BaseTest::ChatChannel", events[0].payload[:channel_class]
+ ensure
+ ActiveSupport::Notifications.unsubscribe "transmit_subscription_rejection.action_cable"
+ end
+ end
+
+ private
+ def assert_logged(message)
+ old_logger = @connection.logger
+ log = StringIO.new
+ @connection.instance_variable_set(:@logger, Logger.new(log))
+
+ begin
+ yield
+
+ log.rewind
+ assert_match message, log.read
+ ensure
+ @connection.instance_variable_set(:@logger, old_logger)
+ end
+ end
+end
diff --git a/actioncable/test/channel/broadcasting_test.rb b/actioncable/test/channel/broadcasting_test.rb
new file mode 100644
index 0000000000..2cbfabc1d0
--- /dev/null
+++ b/actioncable/test/channel/broadcasting_test.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "stubs/test_connection"
+require "stubs/room"
+
+class ActionCable::Channel::BroadcastingTest < ActionCable::TestCase
+ class ChatChannel < ActionCable::Channel::Base
+ end
+
+ setup do
+ @connection = TestConnection.new
+ end
+
+ test "broadcasts_to" do
+ assert_called_with(
+ ActionCable.server,
+ :broadcast,
+ [
+ "action_cable:channel:broadcasting_test:chat:Room#1-Campfire",
+ "Hello World"
+ ]
+ ) do
+ ChatChannel.broadcast_to(Room.new(1), "Hello World")
+ end
+ end
+
+ test "broadcasting_for with an object" do
+ assert_equal "Room#1-Campfire", ChatChannel.broadcasting_for(Room.new(1))
+ end
+
+ test "broadcasting_for with an array" do
+ assert_equal "Room#1-Campfire:Room#2-Campfire", ChatChannel.broadcasting_for([ Room.new(1), Room.new(2) ])
+ end
+
+ test "broadcasting_for with a string" do
+ assert_equal "hello", ChatChannel.broadcasting_for("hello")
+ end
+end
diff --git a/actioncable/test/channel/naming_test.rb b/actioncable/test/channel/naming_test.rb
new file mode 100644
index 0000000000..45652d9cc9
--- /dev/null
+++ b/actioncable/test/channel/naming_test.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class ActionCable::Channel::NamingTest < ActionCable::TestCase
+ class ChatChannel < ActionCable::Channel::Base
+ end
+
+ test "channel_name" do
+ assert_equal "action_cable:channel:naming_test:chat", ChatChannel.channel_name
+ end
+end
diff --git a/actioncable/test/channel/periodic_timers_test.rb b/actioncable/test/channel/periodic_timers_test.rb
new file mode 100644
index 0000000000..0c979f4c7c
--- /dev/null
+++ b/actioncable/test/channel/periodic_timers_test.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "stubs/test_connection"
+require "stubs/room"
+require "active_support/time"
+
+class ActionCable::Channel::PeriodicTimersTest < ActionCable::TestCase
+ class ChatChannel < ActionCable::Channel::Base
+ # Method name arg
+ periodically :send_updates, every: 1
+
+ # Proc arg
+ periodically -> { ping }, every: 2
+
+ # Block arg
+ periodically every: 3 do
+ ping
+ end
+
+ private
+ def ping
+ end
+ end
+
+ setup do
+ @connection = TestConnection.new
+ end
+
+ test "periodic timers definition" do
+ timers = ChatChannel.periodic_timers
+
+ assert_equal 3, timers.size
+
+ timers.each_with_index do |timer, i|
+ assert_kind_of Proc, timer[0]
+ assert_equal i + 1, timer[1][:every]
+ end
+ end
+
+ test "disallow negative and zero periods" do
+ [ 0, 0.0, 0.seconds, -1, -1.seconds, "foo", :foo, Object.new ].each do |invalid|
+ 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
+ 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|
+ e = assert_raise ArgumentError do
+ ChatChannel.periodically invalid, every: 1
+ end
+ assert_match(/Expected a Symbol/, e.message)
+ end
+ end
+
+ test "timer start and stop" do
+ mock = Minitest::Mock.new
+ 3.times { mock.expect(:shutdown, nil) }
+
+ assert_called(
+ @connection.server.event_loop,
+ :timer,
+ times: 3,
+ returns: mock
+ ) do
+ channel = ChatChannel.new @connection, "{id: 1}", id: 1
+
+ channel.subscribe_to_channel
+ channel.unsubscribe_from_channel
+ assert_equal [], channel.send(:active_periodic_timers)
+ end
+
+ assert mock.verify
+ end
+end
diff --git a/actioncable/test/channel/rejection_test.rb b/actioncable/test/channel/rejection_test.rb
new file mode 100644
index 0000000000..683eafcac0
--- /dev/null
+++ b/actioncable/test/channel/rejection_test.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "minitest/mock"
+require "stubs/test_connection"
+require "stubs/room"
+
+class ActionCable::Channel::RejectionTest < ActionCable::TestCase
+ class SecretChannel < ActionCable::Channel::Base
+ def subscribed
+ reject if params[:id] > 0
+ end
+
+ def secret_action
+ end
+ end
+
+ setup do
+ @user = User.new "lifo"
+ @connection = TestConnection.new(@user)
+ end
+
+ test "subscription rejection" do
+ subscriptions = Minitest::Mock.new
+ subscriptions.expect(:remove_subscription, SecretChannel, [SecretChannel])
+
+ @connection.stub(:subscriptions, subscriptions) do
+ @channel = SecretChannel.new @connection, "{id: 1}", id: 1
+ @channel.subscribe_to_channel
+
+ expected = { "identifier" => "{id: 1}", "type" => "reject_subscription" }
+ assert_equal expected, @connection.last_transmission
+ end
+
+ assert subscriptions.verify
+ end
+
+ test "does not execute action if subscription is rejected" do
+ subscriptions = Minitest::Mock.new
+ subscriptions.expect(:remove_subscription, SecretChannel, [SecretChannel])
+
+ @connection.stub(:subscriptions, subscriptions) do
+ @channel = SecretChannel.new @connection, "{id: 1}", id: 1
+ @channel.subscribe_to_channel
+
+ expected = { "identifier" => "{id: 1}", "type" => "reject_subscription" }
+ assert_equal expected, @connection.last_transmission
+ assert_equal 1, @connection.transmissions.size
+
+ @channel.perform_action("action" => :secret_action)
+ assert_equal 1, @connection.transmissions.size
+ end
+
+ assert subscriptions.verify
+ end
+end
diff --git a/actioncable/test/channel/stream_test.rb b/actioncable/test/channel/stream_test.rb
new file mode 100644
index 0000000000..9ad2213d47
--- /dev/null
+++ b/actioncable/test/channel/stream_test.rb
@@ -0,0 +1,230 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "minitest/mock"
+require "stubs/test_connection"
+require "stubs/room"
+
+module ActionCable::StreamTests
+ class Connection < ActionCable::Connection::Base
+ attr_reader :websocket
+
+ def send_async(method, *args)
+ send method, *args
+ end
+ end
+
+ class ChatChannel < ActionCable::Channel::Base
+ def subscribed
+ if params[:id]
+ @room = Room.new params[:id]
+ stream_from "test_room_#{@room.id}", coder: pick_coder(params[:coder])
+ end
+ end
+
+ def send_confirmation
+ transmit_subscription_confirmation
+ end
+
+ private
+ def pick_coder(coder)
+ case coder
+ when nil, "json"
+ ActiveSupport::JSON
+ when "custom"
+ DummyEncoder
+ when "none"
+ nil
+ end
+ end
+ end
+
+ module DummyEncoder
+ extend self
+ def encode(*) '{ "foo": "encoded" }' end
+ def decode(*) { foo: "decoded" } end
+ end
+
+ class SymbolChannel < ActionCable::Channel::Base
+ def subscribed
+ stream_from :channel
+ end
+ end
+
+ class StreamTest < ActionCable::TestCase
+ test "streaming start and stop" do
+ run_in_eventmachine do
+ connection = TestConnection.new
+ pubsub = Minitest::Mock.new connection.pubsub
+
+ pubsub.expect(:subscribe, nil, ["test_room_1", Proc, Proc])
+ pubsub.expect(:unsubscribe, nil, ["test_room_1", Proc])
+
+ connection.stub(:pubsub, pubsub) do
+ channel = ChatChannel.new connection, "{id: 1}", id: 1
+ channel.subscribe_to_channel
+
+ wait_for_async
+ channel.unsubscribe_from_channel
+ end
+
+ assert pubsub.verify
+ end
+ end
+
+ test "stream from non-string channel" do
+ run_in_eventmachine do
+ connection = TestConnection.new
+ pubsub = Minitest::Mock.new connection.pubsub
+
+ pubsub.expect(:subscribe, nil, ["channel", Proc, Proc])
+ pubsub.expect(:unsubscribe, nil, ["channel", Proc])
+
+ connection.stub(:pubsub, pubsub) do
+ channel = SymbolChannel.new connection, ""
+ channel.subscribe_to_channel
+
+ wait_for_async
+
+ channel.unsubscribe_from_channel
+ end
+
+ assert pubsub.verify
+ end
+ end
+
+ test "stream_for" do
+ run_in_eventmachine do
+ connection = TestConnection.new
+
+ channel = ChatChannel.new connection, ""
+ channel.subscribe_to_channel
+ channel.stream_for Room.new(1)
+ wait_for_async
+
+ pubsub_call = channel.pubsub.class.class_variable_get "@@subscribe_called"
+
+ assert_equal "action_cable:stream_tests:chat:Room#1-Campfire", pubsub_call[:channel]
+ assert_instance_of Proc, pubsub_call[:callback]
+ assert_instance_of Proc, pubsub_call[:success_callback]
+ end
+ end
+
+ test "stream_from subscription confirmation" do
+ run_in_eventmachine do
+ connection = TestConnection.new
+
+ channel = ChatChannel.new connection, "{id: 1}", id: 1
+ channel.subscribe_to_channel
+
+ assert_nil connection.last_transmission
+
+ wait_for_async
+
+ confirmation = { "identifier" => "{id: 1}", "type" => "confirm_subscription" }
+ connection.transmit(confirmation)
+
+ assert_equal confirmation, connection.last_transmission, "Did not receive subscription confirmation within 0.1s"
+ end
+ end
+
+ test "subscription confirmation should only be sent out once" do
+ run_in_eventmachine do
+ connection = TestConnection.new
+
+ channel = ChatChannel.new connection, "test_channel"
+ channel.send_confirmation
+ channel.send_confirmation
+
+ wait_for_async
+
+ expected = { "identifier" => "test_channel", "type" => "confirm_subscription" }
+ assert_equal expected, connection.last_transmission, "Did not receive subscription confirmation"
+
+ assert_equal 1, connection.transmissions.size
+ end
+ end
+ end
+
+ require "action_cable/subscription_adapter/async"
+
+ class UserCallbackChannel < ActionCable::Channel::Base
+ def subscribed
+ stream_from :channel do
+ Thread.current[:ran_callback] = true
+ end
+ end
+ end
+
+ class MultiChatChannel < ActionCable::Channel::Base
+ def subscribed
+ stream_from "main_room"
+ stream_from "test_all_rooms"
+ end
+ end
+
+ class StreamFromTest < ActionCable::TestCase
+ setup do
+ @server = TestServer.new(subscription_adapter: ActionCable::SubscriptionAdapter::Async)
+ @server.config.allowed_request_origins = %w( http://rubyonrails.com )
+ end
+
+ test "custom encoder" do
+ run_in_eventmachine do
+ connection = open_connection
+ subscribe_to connection, identifiers: { id: 1 }
+
+ assert_called(connection.websocket, :transmit) do
+ @server.broadcast "test_room_1", { foo: "bar" }, { coder: DummyEncoder }
+ wait_for_async
+ wait_for_executor connection.server.worker_pool.executor
+ end
+ end
+ end
+
+ test "user supplied callbacks are run through the worker pool" do
+ run_in_eventmachine do
+ connection = open_connection
+ receive(connection, command: "subscribe", channel: UserCallbackChannel.name, identifiers: { id: 1 })
+
+ @server.broadcast "channel", {}
+ wait_for_async
+ assert_not Thread.current[:ran_callback], "User callback was not run through the worker pool"
+ end
+ end
+
+ test "subscription confirmation should only be sent out once with multiple stream_from" do
+ run_in_eventmachine do
+ connection = open_connection
+ expected = { "identifier" => { "channel" => MultiChatChannel.name }.to_json, "type" => "confirm_subscription" }
+ assert_called_with(connection.websocket, :transmit, [expected.to_json]) do
+ receive(connection, command: "subscribe", channel: MultiChatChannel.name, identifiers: {})
+ wait_for_async
+ end
+ end
+ end
+
+ private
+ def subscribe_to(connection, identifiers:)
+ receive connection, command: "subscribe", identifiers: identifiers
+ end
+
+ def open_connection
+ env = Rack::MockRequest.env_for "/test", "HTTP_HOST" => "localhost", "HTTP_CONNECTION" => "upgrade", "HTTP_UPGRADE" => "websocket", "HTTP_ORIGIN" => "http://rubyonrails.com"
+
+ Connection.new(@server, env).tap do |connection|
+ connection.process
+ assert_predicate connection.websocket, :possible?
+
+ wait_for_async
+ assert_predicate connection.websocket, :alive?
+ end
+ end
+
+ def receive(connection, command:, identifiers:, channel: "ActionCable::StreamTests::ChatChannel")
+ identifier = JSON.generate(identifiers.merge(channel: channel))
+ connection.dispatch_websocket_message JSON.generate(command: command, identifier: identifier)
+ wait_for_async
+ end
+ end
+end
diff --git a/actioncable/test/channel/test_case_test.rb b/actioncable/test/channel/test_case_test.rb
new file mode 100644
index 0000000000..63d0d6207e
--- /dev/null
+++ b/actioncable/test/channel/test_case_test.rb
@@ -0,0 +1,188 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class TestTestChannel < ActionCable::Channel::Base
+end
+
+class NonInferrableExplicitClassChannelTest < ActionCable::Channel::TestCase
+ tests TestTestChannel
+
+ def test_set_channel_class_manual
+ assert_equal TestTestChannel, self.class.channel_class
+ end
+end
+
+class NonInferrableSymbolNameChannelTest < ActionCable::Channel::TestCase
+ tests :test_test_channel
+
+ def test_set_channel_class_manual_using_symbol
+ assert_equal TestTestChannel, self.class.channel_class
+ end
+end
+
+class NonInferrableStringNameChannelTest < ActionCable::Channel::TestCase
+ tests "test_test_channel"
+
+ def test_set_channel_class_manual_using_string
+ assert_equal TestTestChannel, self.class.channel_class
+ end
+end
+
+class SubscriptionsTestChannel < ActionCable::Channel::Base
+end
+
+class SubscriptionsTestChannelTest < ActionCable::Channel::TestCase
+ def setup
+ stub_connection
+ end
+
+ def test_no_subscribe
+ assert_nil subscription
+ end
+
+ def test_subscribe
+ subscribe
+
+ assert subscription.confirmed?
+ assert_not subscription.rejected?
+ assert_equal 1, connection.transmissions.size
+ assert_equal ActionCable::INTERNAL[:message_types][:confirmation],
+ connection.transmissions.last["type"]
+ end
+end
+
+class StubConnectionTest < ActionCable::Channel::TestCase
+ tests SubscriptionsTestChannel
+
+ def test_connection_identifiers
+ stub_connection username: "John", admin: true
+
+ subscribe
+
+ assert_equal "John", subscription.username
+ assert subscription.admin
+ end
+end
+
+class RejectionTestChannel < ActionCable::Channel::Base
+ def subscribed
+ reject
+ end
+end
+
+class RejectionTestChannelTest < ActionCable::Channel::TestCase
+ def test_rejection
+ subscribe
+
+ assert_not subscription.confirmed?
+ assert subscription.rejected?
+ assert_equal 1, connection.transmissions.size
+ assert_equal ActionCable::INTERNAL[:message_types][:rejection],
+ connection.transmissions.last["type"]
+ end
+end
+
+class StreamsTestChannel < ActionCable::Channel::Base
+ def subscribed
+ stream_from "test_#{params[:id] || 0}"
+ end
+end
+
+class StreamsTestChannelTest < ActionCable::Channel::TestCase
+ def test_stream_without_params
+ subscribe
+
+ assert_equal "test_0", streams.last
+ end
+
+ def test_stream_with_params
+ subscribe id: 42
+
+ assert_equal "test_42", streams.last
+ end
+end
+
+class PerformTestChannel < ActionCable::Channel::Base
+ def echo(data)
+ data.delete("action")
+ transmit data
+ end
+
+ def ping
+ transmit type: "pong"
+ end
+end
+
+class PerformTestChannelTest < ActionCable::Channel::TestCase
+ def setup
+ stub_connection user_id: 2016
+ subscribe id: 5
+ end
+
+ def test_perform_with_params
+ perform :echo, text: "You are man!"
+
+ assert_equal({ "text" => "You are man!" }, transmissions.last)
+ end
+
+ def test_perform_and_transmit
+ perform :ping
+
+ assert_equal "pong", transmissions.last["type"]
+ end
+end
+
+class PerformUnsubscribedTestChannelTest < ActionCable::Channel::TestCase
+ tests PerformTestChannel
+
+ def test_perform_when_unsubscribed
+ assert_raises do
+ perform :echo
+ end
+ end
+end
+
+class BroadcastsTestChannel < ActionCable::Channel::Base
+ def broadcast(data)
+ ActionCable.server.broadcast(
+ "broadcast_#{params[:id]}",
+ text: data["message"], user_id: user_id
+ )
+ end
+
+ def broadcast_to_user(data)
+ user = User.new user_id
+
+ self.class.broadcast_to user, text: data["message"]
+ end
+end
+
+class BroadcastsTestChannelTest < ActionCable::Channel::TestCase
+ def setup
+ stub_connection user_id: 2017
+ subscribe id: 5
+ end
+
+ def test_broadcast_matchers_included
+ assert_broadcast_on("broadcast_5", user_id: 2017, text: "SOS") do
+ perform :broadcast, message: "SOS"
+ end
+ end
+
+ def test_broadcast_to_object
+ user = User.new(2017)
+
+ assert_broadcasts(user, 1) do
+ perform :broadcast_to_user, text: "SOS"
+ end
+ end
+
+ def test_broadcast_to_object_with_data
+ user = User.new(2017)
+
+ assert_broadcast_on(user, text: "SOS") do
+ perform :broadcast_to_user, message: "SOS"
+ end
+ end
+end
diff --git a/actioncable/test/client_test.rb b/actioncable/test/client_test.rb
new file mode 100644
index 0000000000..e5f43488c4
--- /dev/null
+++ b/actioncable/test/client_test.rb
@@ -0,0 +1,314 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "concurrent"
+
+require "websocket-client-simple"
+require "json"
+
+require "active_support/hash_with_indifferent_access"
+
+####
+# 😷 Warning suppression 😷
+WebSocket::Frame::Handler::Handler03.prepend Module.new {
+ def initialize(*)
+ @application_data_buffer = nil
+ super
+ end
+}
+
+WebSocket::Frame::Data.prepend Module.new {
+ def initialize(*)
+ @masking_key = nil
+ super
+ end
+}
+#
+####
+
+class ClientTest < ActionCable::TestCase
+ WAIT_WHEN_EXPECTING_EVENT = 2
+ WAIT_WHEN_NOT_EXPECTING_EVENT = 0.5
+
+ class EchoChannel < ActionCable::Channel::Base
+ def subscribed
+ stream_from "global"
+ end
+
+ def unsubscribed
+ "Goodbye from EchoChannel!"
+ end
+
+ def ding(data)
+ transmit(dong: data["message"])
+ end
+
+ def delay(data)
+ sleep 1
+ transmit(dong: data["message"])
+ end
+
+ def bulk(data)
+ ActionCable.server.broadcast "global", wide: data["message"]
+ end
+ end
+
+ def setup
+ ActionCable.instance_variable_set(:@server, nil)
+ server = ActionCable.server
+ server.config.logger = Logger.new(StringIO.new).tap { |l| l.level = Logger::UNKNOWN }
+
+ server.config.cable = ActiveSupport::HashWithIndifferentAccess.new(adapter: "async")
+
+ # and now the "real" setup for our test:
+ server.config.disable_request_forgery_protection = true
+ end
+
+ def with_puma_server(rack_app = ActionCable.server, port = 3099)
+ server = ::Puma::Server.new(rack_app, ::Puma::Events.strings)
+ server.add_tcp_listener "127.0.0.1", port
+ server.min_threads = 1
+ server.max_threads = 4
+
+ thread = server.run
+
+ begin
+ yield port
+
+ ensure
+ server.stop
+
+ begin
+ thread.join
+
+ rescue IOError
+ # Work around https://bugs.ruby-lang.org/issues/13405
+ #
+ # Puma's sometimes raising while shutting down, when it closes
+ # its internal pipe. We can safely ignore that, but we do need
+ # to do the step skipped by the exception:
+ server.binder.close
+
+ rescue RuntimeError => ex
+ # Work around https://bugs.ruby-lang.org/issues/13239
+ raise unless ex.message =~ /can't modify frozen IOError/
+
+ # Handle this as if it were the IOError: do the same as above.
+ server.binder.close
+ end
+ end
+ end
+
+ class SyncClient
+ attr_reader :pings
+
+ def initialize(port)
+ messages = @messages = Queue.new
+ closed = @closed = Concurrent::Event.new
+ has_messages = @has_messages = Concurrent::Semaphore.new(0)
+ pings = @pings = Concurrent::AtomicFixnum.new(0)
+
+ open = Concurrent::Promise.new
+
+ @ws = WebSocket::Client::Simple.connect("ws://127.0.0.1:#{port}/") do |ws|
+ ws.on(:error) do |event|
+ event = RuntimeError.new(event.message) unless event.is_a?(Exception)
+
+ if open.pending?
+ open.fail(event)
+ else
+ messages << event
+ has_messages.release
+ end
+ end
+
+ ws.on(:open) do |event|
+ open.set(true)
+ end
+
+ ws.on(:message) do |event|
+ if event.type == :close
+ closed.set
+ else
+ message = JSON.parse(event.data)
+ if message["type"] == "ping"
+ pings.increment
+ else
+ messages << message
+ has_messages.release
+ end
+ end
+ end
+
+ ws.on(:close) do |event|
+ closed.set
+ end
+ end
+
+ open.wait!(WAIT_WHEN_EXPECTING_EVENT)
+ end
+
+ def read_message
+ @has_messages.try_acquire(1, WAIT_WHEN_EXPECTING_EVENT)
+
+ msg = @messages.pop(true)
+ raise msg if msg.is_a?(Exception)
+
+ msg
+ end
+
+ def read_messages(expected_size = 0)
+ list = []
+ loop do
+ if @has_messages.try_acquire(1, list.size < expected_size ? WAIT_WHEN_EXPECTING_EVENT : WAIT_WHEN_NOT_EXPECTING_EVENT)
+ msg = @messages.pop(true)
+ raise msg if msg.is_a?(Exception)
+
+ list << msg
+ else
+ break
+ end
+ end
+ list
+ end
+
+ def send_message(message)
+ @ws.send(JSON.generate(message))
+ end
+
+ def close
+ sleep WAIT_WHEN_NOT_EXPECTING_EVENT
+
+ unless @messages.empty?
+ raise "#{@messages.size} messages unprocessed"
+ end
+
+ @ws.close
+ wait_for_close
+ end
+
+ def wait_for_close
+ @closed.wait(WAIT_WHEN_EXPECTING_EVENT)
+ end
+
+ def closed?
+ @closed.set?
+ end
+ end
+
+ def websocket_client(port)
+ SyncClient.new(port)
+ end
+
+ def concurrently(enum)
+ enum.map { |*x| Concurrent::Future.execute { yield(*x) } }.map(&:value!)
+ end
+
+ def test_single_client
+ with_puma_server do |port|
+ c = websocket_client(port)
+ assert_equal({ "type" => "welcome" }, c.read_message) # pop the first welcome message off the stack
+ c.send_message command: "subscribe", identifier: JSON.generate(channel: "ClientTest::EchoChannel")
+ assert_equal({ "identifier" => "{\"channel\":\"ClientTest::EchoChannel\"}", "type" => "confirm_subscription" }, c.read_message)
+ c.send_message command: "message", identifier: JSON.generate(channel: "ClientTest::EchoChannel"), data: JSON.generate(action: "ding", message: "hello")
+ assert_equal({ "identifier" => "{\"channel\":\"ClientTest::EchoChannel\"}", "message" => { "dong" => "hello" } }, c.read_message)
+ c.close
+ end
+ end
+
+ def test_interacting_clients
+ with_puma_server do |port|
+ clients = concurrently(10.times) { websocket_client(port) }
+
+ barrier_1 = Concurrent::CyclicBarrier.new(clients.size)
+ barrier_2 = Concurrent::CyclicBarrier.new(clients.size)
+
+ concurrently(clients) do |c|
+ assert_equal({ "type" => "welcome" }, c.read_message) # pop the first welcome message off the stack
+ c.send_message command: "subscribe", identifier: JSON.generate(channel: "ClientTest::EchoChannel")
+ assert_equal({ "identifier" => '{"channel":"ClientTest::EchoChannel"}', "type" => "confirm_subscription" }, c.read_message)
+ c.send_message command: "message", identifier: JSON.generate(channel: "ClientTest::EchoChannel"), data: JSON.generate(action: "ding", message: "hello")
+ assert_equal({ "identifier" => '{"channel":"ClientTest::EchoChannel"}', "message" => { "dong" => "hello" } }, c.read_message)
+ barrier_1.wait WAIT_WHEN_EXPECTING_EVENT
+ c.send_message command: "message", identifier: JSON.generate(channel: "ClientTest::EchoChannel"), data: JSON.generate(action: "bulk", message: "hello")
+ barrier_2.wait WAIT_WHEN_EXPECTING_EVENT
+ assert_equal clients.size, c.read_messages(clients.size).size
+ end
+
+ concurrently(clients, &:close)
+ end
+ end
+
+ def test_many_clients
+ with_puma_server do |port|
+ clients = concurrently(100.times) { websocket_client(port) }
+
+ concurrently(clients) do |c|
+ assert_equal({ "type" => "welcome" }, c.read_message) # pop the first welcome message off the stack
+ c.send_message command: "subscribe", identifier: JSON.generate(channel: "ClientTest::EchoChannel")
+ assert_equal({ "identifier" => '{"channel":"ClientTest::EchoChannel"}', "type" => "confirm_subscription" }, c.read_message)
+ c.send_message command: "message", identifier: JSON.generate(channel: "ClientTest::EchoChannel"), data: JSON.generate(action: "ding", message: "hello")
+ assert_equal({ "identifier" => '{"channel":"ClientTest::EchoChannel"}', "message" => { "dong" => "hello" } }, c.read_message)
+ end
+
+ concurrently(clients, &:close)
+ end
+ end
+
+ def test_disappearing_client
+ with_puma_server do |port|
+ c = websocket_client(port)
+ assert_equal({ "type" => "welcome" }, c.read_message) # pop the first welcome message off the stack
+ c.send_message command: "subscribe", identifier: JSON.generate(channel: "ClientTest::EchoChannel")
+ assert_equal({ "identifier" => "{\"channel\":\"ClientTest::EchoChannel\"}", "type" => "confirm_subscription" }, c.read_message)
+ c.send_message command: "message", identifier: JSON.generate(channel: "ClientTest::EchoChannel"), data: JSON.generate(action: "delay", message: "hello")
+ c.close # disappear before write
+
+ c = websocket_client(port)
+ assert_equal({ "type" => "welcome" }, c.read_message) # pop the first welcome message off the stack
+ c.send_message command: "subscribe", identifier: JSON.generate(channel: "ClientTest::EchoChannel")
+ assert_equal({ "identifier" => "{\"channel\":\"ClientTest::EchoChannel\"}", "type" => "confirm_subscription" }, c.read_message)
+ c.send_message command: "message", identifier: JSON.generate(channel: "ClientTest::EchoChannel"), data: JSON.generate(action: "ding", message: "hello")
+ assert_equal({ "identifier" => '{"channel":"ClientTest::EchoChannel"}', "message" => { "dong" => "hello" } }, c.read_message)
+ c.close # disappear before read
+ end
+ end
+
+ def test_unsubscribe_client
+ with_puma_server do |port|
+ app = ActionCable.server
+ identifier = JSON.generate(channel: "ClientTest::EchoChannel")
+
+ c = websocket_client(port)
+ assert_equal({ "type" => "welcome" }, c.read_message)
+ c.send_message command: "subscribe", identifier: identifier
+ assert_equal({ "identifier" => "{\"channel\":\"ClientTest::EchoChannel\"}", "type" => "confirm_subscription" }, c.read_message)
+ assert_equal(1, app.connections.count)
+ assert(app.remote_connections.where(identifier: identifier))
+
+ subscriptions = app.connections.first.subscriptions.send(:subscriptions)
+ assert_not_equal 0, subscriptions.size, "Missing EchoChannel subscription"
+ channel = subscriptions.first[1]
+ assert_called(channel, :unsubscribed) do
+ c.close
+ sleep 0.1 # Data takes a moment to process
+ end
+
+ # All data is removed: No more connection or subscription information!
+ assert_equal(0, app.connections.count)
+ end
+ end
+
+ def test_server_restart
+ with_puma_server do |port|
+ c = websocket_client(port)
+ assert_equal({ "type" => "welcome" }, c.read_message)
+ c.send_message command: "subscribe", identifier: JSON.generate(channel: "ClientTest::EchoChannel")
+ assert_equal({ "identifier" => "{\"channel\":\"ClientTest::EchoChannel\"}", "type" => "confirm_subscription" }, c.read_message)
+
+ ActionCable.server.restart
+ c.wait_for_close
+ assert_predicate c, :closed?
+ end
+ end
+end
diff --git a/actioncable/test/connection/authorization_test.rb b/actioncable/test/connection/authorization_test.rb
new file mode 100644
index 0000000000..f77e543435
--- /dev/null
+++ b/actioncable/test/connection/authorization_test.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "stubs/test_server"
+
+class ActionCable::Connection::AuthorizationTest < ActionCable::TestCase
+ class Connection < ActionCable::Connection::Base
+ attr_reader :websocket
+
+ def connect
+ reject_unauthorized_connection
+ end
+
+ def send_async(method, *args)
+ send method, *args
+ end
+ end
+
+ test "unauthorized connection" do
+ run_in_eventmachine do
+ server = TestServer.new
+ server.config.allowed_request_origins = %w( http://rubyonrails.com )
+
+ env = Rack::MockRequest.env_for "/test", "HTTP_CONNECTION" => "upgrade", "HTTP_UPGRADE" => "websocket",
+ "HTTP_HOST" => "localhost", "HTTP_ORIGIN" => "http://rubyonrails.com"
+
+ connection = Connection.new(server, env)
+ assert_called(connection.websocket, :close) do
+ connection.process
+ end
+ end
+ end
+end
diff --git a/actioncable/test/connection/base_test.rb b/actioncable/test/connection/base_test.rb
new file mode 100644
index 0000000000..6ffa0961bc
--- /dev/null
+++ b/actioncable/test/connection/base_test.rb
@@ -0,0 +1,143 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "stubs/test_server"
+require "active_support/core_ext/object/json"
+
+class ActionCable::Connection::BaseTest < ActionCable::TestCase
+ class Connection < ActionCable::Connection::Base
+ attr_reader :websocket, :subscriptions, :message_buffer, :connected
+
+ def connect
+ @connected = true
+ end
+
+ def disconnect
+ @connected = false
+ end
+
+ def send_async(method, *args)
+ send method, *args
+ end
+ end
+
+ setup do
+ @server = TestServer.new
+ @server.config.allowed_request_origins = %w( http://rubyonrails.com )
+ end
+
+ test "making a connection with invalid headers" do
+ run_in_eventmachine do
+ connection = ActionCable::Connection::Base.new(@server, Rack::MockRequest.env_for("/test"))
+ response = connection.process
+ assert_equal 404, response[0]
+ end
+ end
+
+ test "websocket connection" do
+ run_in_eventmachine do
+ connection = open_connection
+ connection.process
+
+ assert_predicate connection.websocket, :possible?
+
+ wait_for_async
+ assert_predicate connection.websocket, :alive?
+ end
+ end
+
+ test "rack response" do
+ run_in_eventmachine do
+ connection = open_connection
+ response = connection.process
+
+ assert_equal [ -1, {}, [] ], response
+ end
+ end
+
+ test "on connection open" do
+ run_in_eventmachine do
+ connection = open_connection
+
+ assert_called_with(connection.websocket, :transmit, [{ type: "welcome" }.to_json]) do
+ assert_called(connection.message_buffer, :process!) do
+ connection.process
+ wait_for_async
+ end
+ end
+
+ assert_equal [ connection ], @server.connections
+ assert connection.connected
+ end
+ end
+
+ test "on connection close" do
+ run_in_eventmachine do
+ connection = open_connection
+ connection.process
+
+ # Setup the connection
+ connection.send :handle_open
+ assert connection.connected
+
+ assert_called(connection.subscriptions, :unsubscribe_from_all) do
+ connection.send :handle_close
+ end
+
+ assert_not connection.connected
+ assert_equal [], @server.connections
+ end
+ end
+
+ test "connection statistics" do
+ run_in_eventmachine do
+ connection = open_connection
+ connection.process
+
+ statistics = connection.statistics
+
+ assert_predicate statistics[:identifier], :blank?
+ assert_kind_of Time, statistics[:started_at]
+ assert_equal [], statistics[:subscriptions]
+ end
+ end
+
+ test "explicitly closing a connection" do
+ run_in_eventmachine do
+ connection = open_connection
+ connection.process
+
+ assert_called(connection.websocket, :close) do
+ connection.close
+ end
+ end
+ end
+
+ test "rejecting a connection causes a 404" do
+ run_in_eventmachine do
+ class CallMeMaybe
+ def call(*)
+ raise "Do not call me!"
+ end
+ end
+
+ env = Rack::MockRequest.env_for(
+ "/test",
+ "HTTP_CONNECTION" => "upgrade", "HTTP_UPGRADE" => "websocket",
+ "HTTP_HOST" => "localhost", "HTTP_ORIGIN" => "http://rubyonrails.org", "rack.hijack" => CallMeMaybe.new
+ )
+
+ connection = ActionCable::Connection::Base.new(@server, env)
+ response = connection.process
+ assert_equal 404, response[0]
+ end
+ end
+
+ private
+ def open_connection
+ env = Rack::MockRequest.env_for "/test", "HTTP_CONNECTION" => "upgrade", "HTTP_UPGRADE" => "websocket",
+ "HTTP_HOST" => "localhost", "HTTP_ORIGIN" => "http://rubyonrails.com"
+
+ Connection.new(@server, env)
+ end
+end
diff --git a/actioncable/test/connection/client_socket_test.rb b/actioncable/test/connection/client_socket_test.rb
new file mode 100644
index 0000000000..a7db32c3e4
--- /dev/null
+++ b/actioncable/test/connection/client_socket_test.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "stubs/test_server"
+
+class ActionCable::Connection::ClientSocketTest < ActionCable::TestCase
+ class Connection < ActionCable::Connection::Base
+ attr_reader :connected, :websocket, :errors
+
+ def initialize(*)
+ super
+ @errors = []
+ end
+
+ def connect
+ @connected = true
+ end
+
+ def disconnect
+ @connected = false
+ end
+
+ def send_async(method, *args)
+ send method, *args
+ end
+
+ def on_error(message)
+ @errors << message
+ end
+ end
+
+ setup do
+ @server = TestServer.new
+ @server.config.allowed_request_origins = %w( http://rubyonrails.com )
+ end
+
+ test "delegate socket errors to on_error handler" do
+ run_in_eventmachine do
+ connection = open_connection
+
+ # Internal hax = :(
+ client = connection.websocket.send(:websocket)
+ client.instance_variable_get("@stream").stub(:write, proc { raise "foo" }) do
+ assert_not_called(client, :client_gone) do
+ client.write("boo")
+ end
+ end
+ assert_equal %w[ foo ], connection.errors
+ end
+ end
+
+ test "closes hijacked i/o socket at shutdown" do
+ run_in_eventmachine do
+ connection = open_connection
+
+ client = connection.websocket.send(:websocket)
+ event = Concurrent::Event.new
+ client.instance_variable_get("@stream")
+ .instance_variable_get("@rack_hijack_io")
+ .define_singleton_method(:close) { event.set }
+ connection.close
+ event.wait
+ end
+ end
+
+ private
+ def open_connection
+ env = Rack::MockRequest.env_for "/test",
+ "HTTP_CONNECTION" => "upgrade", "HTTP_UPGRADE" => "websocket",
+ "HTTP_HOST" => "localhost", "HTTP_ORIGIN" => "http://rubyonrails.com"
+ io, client_io = \
+ begin
+ Socket.pair(Socket::AF_UNIX, Socket::SOCK_STREAM, 0)
+ rescue
+ StringIO.new
+ end
+ env["rack.hijack"] = -> { env["rack.hijack_io"] = io }
+
+ Connection.new(@server, env).tap do |connection|
+ connection.process
+ if client_io
+ # Make sure server returns handshake response
+ Timeout.timeout(1) do
+ loop do
+ break if client_io.readline == "\r\n"
+ end
+ end
+ end
+ connection.send :handle_open
+ assert connection.connected
+ end
+ end
+end
diff --git a/actioncable/test/connection/cross_site_forgery_test.rb b/actioncable/test/connection/cross_site_forgery_test.rb
new file mode 100644
index 0000000000..3e21138ffc
--- /dev/null
+++ b/actioncable/test/connection/cross_site_forgery_test.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "stubs/test_server"
+
+class ActionCable::Connection::CrossSiteForgeryTest < ActionCable::TestCase
+ HOST = "rubyonrails.com"
+
+ class Connection < ActionCable::Connection::Base
+ def send_async(method, *args)
+ send method, *args
+ end
+ end
+
+ setup do
+ @server = TestServer.new
+ @server.config.allowed_request_origins = %w( http://rubyonrails.com )
+ @server.config.allow_same_origin_as_host = false
+ end
+
+ teardown do
+ @server.config.disable_request_forgery_protection = false
+ @server.config.allowed_request_origins = []
+ @server.config.allow_same_origin_as_host = true
+ end
+
+ test "disable forgery protection" do
+ @server.config.disable_request_forgery_protection = true
+ assert_origin_allowed "http://rubyonrails.com"
+ assert_origin_allowed "http://hax.com"
+ end
+
+ test "explicitly specified a single allowed origin" do
+ @server.config.allowed_request_origins = "http://hax.com"
+ assert_origin_not_allowed "http://rubyonrails.com"
+ assert_origin_allowed "http://hax.com"
+ end
+
+ test "explicitly specified multiple allowed origins" do
+ @server.config.allowed_request_origins = %w( http://rubyonrails.com http://www.rubyonrails.com )
+ assert_origin_allowed "http://rubyonrails.com"
+ assert_origin_allowed "http://www.rubyonrails.com"
+ assert_origin_not_allowed "http://hax.com"
+ end
+
+ test "explicitly specified a single regexp allowed origin" do
+ @server.config.allowed_request_origins = /.*ha.*/
+ assert_origin_not_allowed "http://rubyonrails.com"
+ assert_origin_allowed "http://hax.com"
+ end
+
+ test "explicitly specified multiple regexp allowed origins" do
+ @server.config.allowed_request_origins = [/http:\/\/ruby.*/, /.*rai.s.*com/, "string" ]
+ assert_origin_allowed "http://rubyonrails.com"
+ assert_origin_allowed "http://www.rubyonrails.com"
+ assert_origin_not_allowed "http://hax.com"
+ assert_origin_not_allowed "http://rails.co.uk"
+ end
+
+ test "allow same origin as host" do
+ @server.config.allow_same_origin_as_host = true
+ assert_origin_allowed "http://#{HOST}"
+ assert_origin_not_allowed "http://hax.com"
+ assert_origin_not_allowed "http://rails.co.uk"
+ end
+
+ private
+ def assert_origin_allowed(origin)
+ response = connect_with_origin origin
+ assert_equal(-1, response[0])
+ end
+
+ def assert_origin_not_allowed(origin)
+ response = connect_with_origin origin
+ assert_equal 404, response[0]
+ end
+
+ def connect_with_origin(origin)
+ response = nil
+
+ run_in_eventmachine do
+ response = Connection.new(@server, env_for_origin(origin)).process
+ end
+
+ response
+ end
+
+ def env_for_origin(origin)
+ Rack::MockRequest.env_for "/test", "HTTP_CONNECTION" => "upgrade", "HTTP_UPGRADE" => "websocket", "SERVER_NAME" => HOST,
+ "HTTP_HOST" => HOST, "HTTP_ORIGIN" => origin
+ end
+end
diff --git a/actioncable/test/connection/identifier_test.rb b/actioncable/test/connection/identifier_test.rb
new file mode 100644
index 0000000000..707f4bab72
--- /dev/null
+++ b/actioncable/test/connection/identifier_test.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "stubs/test_server"
+require "stubs/user"
+
+class ActionCable::Connection::IdentifierTest < ActionCable::TestCase
+ class Connection < ActionCable::Connection::Base
+ identified_by :current_user
+ attr_reader :websocket
+
+ public :process_internal_message
+
+ def connect
+ self.current_user = User.new "lifo"
+ end
+ end
+
+ test "connection identifier" do
+ run_in_eventmachine do
+ open_connection
+ assert_equal "User#lifo", @connection.connection_identifier
+ end
+ end
+
+ test "should subscribe to internal channel on open and unsubscribe on close" do
+ run_in_eventmachine do
+ server = TestServer.new
+
+ open_connection(server)
+ close_connection
+ wait_for_async
+
+ %w[subscribe unsubscribe].each do |method|
+ pubsub_call = server.pubsub.class.class_variable_get "@@#{method}_called"
+
+ assert_equal "action_cable/User#lifo", pubsub_call[:channel]
+ assert_instance_of Proc, pubsub_call[:callback]
+ end
+ end
+ end
+
+ test "processing disconnect message" do
+ run_in_eventmachine do
+ open_connection
+
+ assert_called(@connection.websocket, :close) do
+ @connection.process_internal_message "type" => "disconnect"
+ end
+ end
+ end
+
+ test "processing invalid message" do
+ run_in_eventmachine do
+ open_connection
+
+ assert_not_called(@connection.websocket, :close) do
+ @connection.process_internal_message "type" => "unknown"
+ end
+ end
+ end
+
+ private
+ def open_connection(server = nil)
+ server ||= TestServer.new
+
+ env = Rack::MockRequest.env_for "/test", "HTTP_HOST" => "localhost", "HTTP_CONNECTION" => "upgrade", "HTTP_UPGRADE" => "websocket"
+ @connection = Connection.new(server, env)
+
+ @connection.process
+ @connection.send :handle_open
+ end
+
+ def close_connection
+ @connection.send :handle_close
+ end
+end
diff --git a/actioncable/test/connection/multiple_identifiers_test.rb b/actioncable/test/connection/multiple_identifiers_test.rb
new file mode 100644
index 0000000000..51716410b2
--- /dev/null
+++ b/actioncable/test/connection/multiple_identifiers_test.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "stubs/test_server"
+require "stubs/user"
+
+class ActionCable::Connection::MultipleIdentifiersTest < ActionCable::TestCase
+ class Connection < ActionCable::Connection::Base
+ identified_by :current_user, :current_room
+
+ def connect
+ self.current_user = User.new "lifo"
+ self.current_room = Room.new "my", "room"
+ end
+ end
+
+ test "multiple connection identifiers" do
+ run_in_eventmachine do
+ open_connection
+
+ assert_equal "Room#my-room:User#lifo", @connection.connection_identifier
+ end
+ end
+
+ private
+ def open_connection
+ server = TestServer.new
+ env = Rack::MockRequest.env_for "/test", "HTTP_HOST" => "localhost", "HTTP_CONNECTION" => "upgrade", "HTTP_UPGRADE" => "websocket"
+ @connection = Connection.new(server, env)
+
+ @connection.process
+ @connection.send :handle_open
+ end
+end
diff --git a/actioncable/test/connection/stream_test.rb b/actioncable/test/connection/stream_test.rb
new file mode 100644
index 0000000000..0f4576db40
--- /dev/null
+++ b/actioncable/test/connection/stream_test.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "minitest/mock"
+require "stubs/test_server"
+
+class ActionCable::Connection::StreamTest < ActionCable::TestCase
+ class Connection < ActionCable::Connection::Base
+ attr_reader :connected, :websocket, :errors
+
+ def initialize(*)
+ super
+ @errors = []
+ end
+
+ def connect
+ @connected = true
+ end
+
+ def disconnect
+ @connected = false
+ end
+
+ def send_async(method, *args)
+ send method, *args
+ end
+
+ def on_error(message)
+ @errors << message
+ end
+ end
+
+ setup do
+ @server = TestServer.new
+ @server.config.allowed_request_origins = %w( http://rubyonrails.com )
+ end
+
+ [ EOFError, Errno::ECONNRESET ].each do |closed_exception|
+ test "closes socket on #{closed_exception}" do
+ run_in_eventmachine do
+ connection = open_connection
+
+ # Internal hax = :(
+ client = connection.websocket.send(:websocket)
+ rack_hijack_io = client.instance_variable_get("@stream").instance_variable_get("@rack_hijack_io")
+ rack_hijack_io.stub(:write, proc { raise(closed_exception, "foo") }) do
+ assert_called(client, :client_gone) do
+ client.write("boo")
+ end
+ end
+ assert_equal [], connection.errors
+ end
+ end
+ end
+
+ private
+ def open_connection
+ env = Rack::MockRequest.env_for "/test",
+ "HTTP_CONNECTION" => "upgrade", "HTTP_UPGRADE" => "websocket",
+ "HTTP_HOST" => "localhost", "HTTP_ORIGIN" => "http://rubyonrails.com"
+ env["rack.hijack"] = -> { env["rack.hijack_io"] = StringIO.new }
+
+ Connection.new(@server, env).tap do |connection|
+ connection.process
+ connection.send :handle_open
+ assert connection.connected
+ end
+ end
+end
diff --git a/actioncable/test/connection/string_identifier_test.rb b/actioncable/test/connection/string_identifier_test.rb
new file mode 100644
index 0000000000..f7019b926a
--- /dev/null
+++ b/actioncable/test/connection/string_identifier_test.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "stubs/test_server"
+
+class ActionCable::Connection::StringIdentifierTest < ActionCable::TestCase
+ class Connection < ActionCable::Connection::Base
+ identified_by :current_token
+
+ def connect
+ self.current_token = "random-string"
+ end
+
+ def send_async(method, *args)
+ send method, *args
+ end
+ end
+
+ test "connection identifier" do
+ run_in_eventmachine do
+ open_connection
+
+ assert_equal "random-string", @connection.connection_identifier
+ end
+ end
+
+ private
+ def open_connection
+ server = TestServer.new
+ env = Rack::MockRequest.env_for "/test", "HTTP_HOST" => "localhost", "HTTP_CONNECTION" => "upgrade", "HTTP_UPGRADE" => "websocket"
+ @connection = Connection.new(server, env)
+
+ @connection.process
+ @connection.send :on_open
+ end
+end
diff --git a/actioncable/test/connection/subscriptions_test.rb b/actioncable/test/connection/subscriptions_test.rb
new file mode 100644
index 0000000000..902085c5d6
--- /dev/null
+++ b/actioncable/test/connection/subscriptions_test.rb
@@ -0,0 +1,119 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class ActionCable::Connection::SubscriptionsTest < ActionCable::TestCase
+ class Connection < ActionCable::Connection::Base
+ attr_reader :websocket
+
+ def send_async(method, *args)
+ send method, *args
+ end
+ end
+
+ class ChatChannel < ActionCable::Channel::Base
+ attr_reader :room, :lines
+
+ def subscribed
+ @room = Room.new params[:id]
+ @lines = []
+ end
+
+ def speak(data)
+ @lines << data
+ end
+ end
+
+ setup do
+ @server = TestServer.new
+
+ @chat_identifier = ActiveSupport::JSON.encode(id: 1, channel: "ActionCable::Connection::SubscriptionsTest::ChatChannel")
+ end
+
+ test "subscribe command" do
+ run_in_eventmachine do
+ setup_connection
+ channel = subscribe_to_chat_channel
+
+ assert_kind_of ChatChannel, channel
+ assert_equal 1, channel.room.id
+ end
+ end
+
+ test "subscribe command without an identifier" do
+ run_in_eventmachine do
+ setup_connection
+
+ @subscriptions.execute_command "command" => "subscribe"
+ assert_empty @subscriptions.identifiers
+ end
+ end
+
+ test "unsubscribe command" do
+ run_in_eventmachine do
+ setup_connection
+ subscribe_to_chat_channel
+
+ channel = subscribe_to_chat_channel
+
+ assert_called(channel, :unsubscribe_from_channel) do
+ @subscriptions.execute_command "command" => "unsubscribe", "identifier" => @chat_identifier
+ end
+
+ assert_empty @subscriptions.identifiers
+ end
+ end
+
+ test "unsubscribe command without an identifier" do
+ run_in_eventmachine do
+ setup_connection
+
+ @subscriptions.execute_command "command" => "unsubscribe"
+ assert_empty @subscriptions.identifiers
+ end
+ end
+
+ test "message command" do
+ run_in_eventmachine do
+ setup_connection
+ channel = subscribe_to_chat_channel
+
+ data = { "content" => "Hello World!", "action" => "speak" }
+ @subscriptions.execute_command "command" => "message", "identifier" => @chat_identifier, "data" => ActiveSupport::JSON.encode(data)
+
+ assert_equal [ data ], channel.lines
+ end
+ end
+
+ test "unsubscribe from all" do
+ run_in_eventmachine do
+ setup_connection
+
+ channel1 = subscribe_to_chat_channel
+
+ channel2_id = ActiveSupport::JSON.encode(id: 2, channel: "ActionCable::Connection::SubscriptionsTest::ChatChannel")
+ channel2 = subscribe_to_chat_channel(channel2_id)
+
+ assert_called(channel1, :unsubscribe_from_channel) do
+ assert_called(channel2, :unsubscribe_from_channel) do
+ @subscriptions.unsubscribe_from_all
+ end
+ end
+ end
+ end
+
+ private
+ def subscribe_to_chat_channel(identifier = @chat_identifier)
+ @subscriptions.execute_command "command" => "subscribe", "identifier" => identifier
+ assert_equal identifier, @subscriptions.identifiers.last
+
+ @subscriptions.send :find, "identifier" => identifier
+ end
+
+ def setup_connection
+ env = Rack::MockRequest.env_for "/test", "HTTP_HOST" => "localhost", "HTTP_CONNECTION" => "upgrade", "HTTP_UPGRADE" => "websocket"
+ @connection = Connection.new(@server, env)
+
+ @subscriptions = ActionCable::Connection::Subscriptions.new(@connection)
+ end
+end
diff --git a/actioncable/test/javascript/src/test.coffee b/actioncable/test/javascript/src/test.coffee
new file mode 100644
index 0000000000..eb95fb2604
--- /dev/null
+++ b/actioncable/test/javascript/src/test.coffee
@@ -0,0 +1,3 @@
+#= require action_cable
+#= require ./test_helpers
+#= require_tree ./unit
diff --git a/actioncable/test/javascript/src/test_helpers/consumer_test_helper.coffee b/actioncable/test/javascript/src/test_helpers/consumer_test_helper.coffee
new file mode 100644
index 0000000000..a9e95c37f0
--- /dev/null
+++ b/actioncable/test/javascript/src/test_helpers/consumer_test_helper.coffee
@@ -0,0 +1,47 @@
+#= require mock-socket
+
+{TestHelpers} = ActionCable
+
+TestHelpers.consumerTest = (name, options = {}, callback) ->
+ unless callback?
+ callback = options
+ options = {}
+
+ options.url ?= TestHelpers.testURL
+
+ QUnit.test name, (assert) ->
+ doneAsync = assert.async()
+
+ ActionCable.WebSocket = MockWebSocket
+ server = new MockServer options.url
+ consumer = ActionCable.createConsumer(options.url)
+
+ server.on "connection", ->
+ clients = server.clients()
+ 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()
+ doneAsync()
+
+ testData = {assert, consumer, server, done}
+
+ if options.connect is false
+ callback(testData)
+ else
+ server.on "connection", ->
+ testData.client = server.clients()[0]
+ callback(testData)
+ consumer.connect()
diff --git a/actioncable/test/javascript/src/test_helpers/index.coffee b/actioncable/test/javascript/src/test_helpers/index.coffee
new file mode 100644
index 0000000000..c84cbbcb2c
--- /dev/null
+++ b/actioncable/test/javascript/src/test_helpers/index.coffee
@@ -0,0 +1,11 @@
+#= require_self
+#= require_tree .
+
+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/action_cable_test.coffee b/actioncable/test/javascript/src/unit/action_cable_test.coffee
new file mode 100644
index 0000000000..3944f3a7f6
--- /dev/null
+++ b/actioncable/test/javascript/src/unit/action_cable_test.coffee
@@ -0,0 +1,41 @@
+{module, test} = QUnit
+{testURL} = ActionCable.TestHelpers
+
+module "ActionCable", ->
+ module "Adapters", ->
+ module "WebSocket", ->
+ test "default is window.WebSocket", (assert) ->
+ assert.equal ActionCable.WebSocket, window.WebSocket
+
+ test "configurable", (assert) ->
+ ActionCable.WebSocket = ""
+ assert.equal ActionCable.WebSocket, ""
+
+ module "logger", ->
+ test "default is window.console", (assert) ->
+ assert.equal ActionCable.logger, window.console
+
+ test "configurable", (assert) ->
+ ActionCable.logger = ""
+ assert.equal ActionCable.logger, ""
+
+ module "#createConsumer", ->
+ test "uses specified URL", (assert) ->
+ consumer = ActionCable.createConsumer(testURL)
+ assert.equal consumer.url, testURL
+
+ test "uses default URL", (assert) ->
+ pattern = ///#{ActionCable.INTERNAL.default_mount_path}$///
+ consumer = ActionCable.createConsumer()
+ assert.ok pattern.test(consumer.url), "Expected #{consumer.url} to match #{pattern}"
+
+ test "uses URL from meta tag", (assert) ->
+ element = document.createElement("meta")
+ element.setAttribute("name", "action-cable-url")
+ element.setAttribute("content", testURL)
+
+ document.head.appendChild(element)
+ consumer = ActionCable.createConsumer()
+ document.head.removeChild(element)
+
+ assert.equal consumer.url, testURL
diff --git a/actioncable/test/javascript/src/unit/consumer_test.coffee b/actioncable/test/javascript/src/unit/consumer_test.coffee
new file mode 100644
index 0000000000..41445274eb
--- /dev/null
+++ b/actioncable/test/javascript/src/unit/consumer_test.coffee
@@ -0,0 +1,14 @@
+{module, test} = QUnit
+{consumerTest} = ActionCable.TestHelpers
+
+module "ActionCable.Consumer", ->
+ consumerTest "#connect", connect: false, ({consumer, server, assert, done}) ->
+ server.on "connection", ->
+ assert.equal consumer.connect(), false
+ done()
+
+ consumer.connect()
+
+ consumerTest "#disconnect", ({consumer, client, done}) ->
+ client.addEventListener("close", done)
+ consumer.disconnect()
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/javascript/vendor/mock-socket.js b/actioncable/test/javascript/vendor/mock-socket.js
new file mode 100644
index 0000000000..4564cebc40
--- /dev/null
+++ b/actioncable/test/javascript/vendor/mock-socket.js
@@ -0,0 +1,4532 @@
+(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+/*!
+ * URI.js - Mutating URLs
+ * IPv6 Support
+ *
+ * Version: 1.17.0
+ *
+ * Author: Rodney Rehm
+ * Web: http://medialize.github.io/URI.js/
+ *
+ * Licensed under
+ * MIT License http://www.opensource.org/licenses/mit-license
+ * GPL v3 http://opensource.org/licenses/GPL-3.0
+ *
+ */
+
+(function (root, factory) {
+ 'use strict';
+ // https://github.com/umdjs/umd/blob/master/returnExports.js
+ if (typeof exports === 'object') {
+ // Node
+ module.exports = factory();
+ } else if (typeof define === 'function' && define.amd) {
+ // AMD. Register as an anonymous module.
+ define(factory);
+ } else {
+ // Browser globals (root is window)
+ root.IPv6 = factory(root);
+ }
+}(this, function (root) {
+ 'use strict';
+
+ /*
+ var _in = "fe80:0000:0000:0000:0204:61ff:fe9d:f156";
+ var _out = IPv6.best(_in);
+ var _expected = "fe80::204:61ff:fe9d:f156";
+
+ console.log(_in, _out, _expected, _out === _expected);
+ */
+
+ // save current IPv6 variable, if any
+ var _IPv6 = root && root.IPv6;
+
+ function bestPresentation(address) {
+ // based on:
+ // Javascript to test an IPv6 address for proper format, and to
+ // present the "best text representation" according to IETF Draft RFC at
+ // http://tools.ietf.org/html/draft-ietf-6man-text-addr-representation-04
+ // 8 Feb 2010 Rich Brown, Dartware, LLC
+ // Please feel free to use this code as long as you provide a link to
+ // http://www.intermapper.com
+ // http://intermapper.com/support/tools/IPV6-Validator.aspx
+ // http://download.dartware.com/thirdparty/ipv6validator.js
+
+ var _address = address.toLowerCase();
+ var segments = _address.split(':');
+ var length = segments.length;
+ var total = 8;
+
+ // trim colons (:: or ::a:b:c… or …a:b:c::)
+ if (segments[0] === '' && segments[1] === '' && segments[2] === '') {
+ // must have been ::
+ // remove first two items
+ segments.shift();
+ segments.shift();
+ } else if (segments[0] === '' && segments[1] === '') {
+ // must have been ::xxxx
+ // remove the first item
+ segments.shift();
+ } else if (segments[length - 1] === '' && segments[length - 2] === '') {
+ // must have been xxxx::
+ segments.pop();
+ }
+
+ length = segments.length;
+
+ // adjust total segments for IPv4 trailer
+ if (segments[length - 1].indexOf('.') !== -1) {
+ // found a "." which means IPv4
+ total = 7;
+ }
+
+ // fill empty segments them with "0000"
+ var pos;
+ for (pos = 0; pos < length; pos++) {
+ if (segments[pos] === '') {
+ break;
+ }
+ }
+
+ if (pos < total) {
+ segments.splice(pos, 1, '0000');
+ while (segments.length < total) {
+ segments.splice(pos, 0, '0000');
+ }
+
+ length = segments.length;
+ }
+
+ // strip leading zeros
+ var _segments;
+ for (var i = 0; i < total; i++) {
+ _segments = segments[i].split('');
+ for (var j = 0; j < 3 ; j++) {
+ if (_segments[0] === '0' && _segments.length > 1) {
+ _segments.splice(0,1);
+ } else {
+ break;
+ }
+ }
+
+ segments[i] = _segments.join('');
+ }
+
+ // find longest sequence of zeroes and coalesce them into one segment
+ var best = -1;
+ var _best = 0;
+ var _current = 0;
+ var current = -1;
+ var inzeroes = false;
+ // i; already declared
+
+ for (i = 0; i < total; i++) {
+ if (inzeroes) {
+ if (segments[i] === '0') {
+ _current += 1;
+ } else {
+ inzeroes = false;
+ if (_current > _best) {
+ best = current;
+ _best = _current;
+ }
+ }
+ } else {
+ if (segments[i] === '0') {
+ inzeroes = true;
+ current = i;
+ _current = 1;
+ }
+ }
+ }
+
+ if (_current > _best) {
+ best = current;
+ _best = _current;
+ }
+
+ if (_best > 1) {
+ segments.splice(best, _best, '');
+ }
+
+ length = segments.length;
+
+ // assemble remaining segments
+ var result = '';
+ if (segments[0] === '') {
+ result = ':';
+ }
+
+ for (i = 0; i < length; i++) {
+ result += segments[i];
+ if (i === length - 1) {
+ break;
+ }
+
+ result += ':';
+ }
+
+ if (segments[length - 1] === '') {
+ result += ':';
+ }
+
+ return result;
+ }
+
+ function noConflict() {
+ /*jshint validthis: true */
+ if (root.IPv6 === this) {
+ root.IPv6 = _IPv6;
+ }
+
+ return this;
+ }
+
+ return {
+ best: bestPresentation,
+ noConflict: noConflict
+ };
+}));
+
+},{}],2:[function(require,module,exports){
+/*!
+ * URI.js - Mutating URLs
+ * Second Level Domain (SLD) Support
+ *
+ * Version: 1.17.0
+ *
+ * Author: Rodney Rehm
+ * Web: http://medialize.github.io/URI.js/
+ *
+ * Licensed under
+ * MIT License http://www.opensource.org/licenses/mit-license
+ * GPL v3 http://opensource.org/licenses/GPL-3.0
+ *
+ */
+
+(function (root, factory) {
+ 'use strict';
+ // https://github.com/umdjs/umd/blob/master/returnExports.js
+ if (typeof exports === 'object') {
+ // Node
+ module.exports = factory();
+ } else if (typeof define === 'function' && define.amd) {
+ // AMD. Register as an anonymous module.
+ define(factory);
+ } else {
+ // Browser globals (root is window)
+ root.SecondLevelDomains = factory(root);
+ }
+}(this, function (root) {
+ 'use strict';
+
+ // save current SecondLevelDomains variable, if any
+ var _SecondLevelDomains = root && root.SecondLevelDomains;
+
+ var SLD = {
+ // list of known Second Level Domains
+ // converted list of SLDs from https://github.com/gavingmiller/second-level-domains
+ // ----
+ // publicsuffix.org is more current and actually used by a couple of browsers internally.
+ // downside is it also contains domains like "dyndns.org" - which is fine for the security
+ // issues browser have to deal with (SOP for cookies, etc) - but is way overboard for URI.js
+ // ----
+ list: {
+ 'ac':' com gov mil net org ',
+ 'ae':' ac co gov mil name net org pro sch ',
+ 'af':' com edu gov net org ',
+ 'al':' com edu gov mil net org ',
+ 'ao':' co ed gv it og pb ',
+ 'ar':' com edu gob gov int mil net org tur ',
+ 'at':' ac co gv or ',
+ 'au':' asn com csiro edu gov id net org ',
+ 'ba':' co com edu gov mil net org rs unbi unmo unsa untz unze ',
+ 'bb':' biz co com edu gov info net org store tv ',
+ 'bh':' biz cc com edu gov info net org ',
+ 'bn':' com edu gov net org ',
+ 'bo':' com edu gob gov int mil net org tv ',
+ 'br':' adm adv agr am arq art ato b bio blog bmd cim cng cnt com coop ecn edu eng esp etc eti far flog fm fnd fot fst g12 ggf gov imb ind inf jor jus lel mat med mil mus net nom not ntr odo org ppg pro psc psi qsl rec slg srv tmp trd tur tv vet vlog wiki zlg ',
+ 'bs':' com edu gov net org ',
+ 'bz':' du et om ov rg ',
+ 'ca':' ab bc mb nb nf nl ns nt nu on pe qc sk yk ',
+ 'ck':' biz co edu gen gov info net org ',
+ 'cn':' ac ah bj com cq edu fj gd gov gs gx gz ha hb he hi hl hn jl js jx ln mil net nm nx org qh sc sd sh sn sx tj tw xj xz yn zj ',
+ 'co':' com edu gov mil net nom org ',
+ 'cr':' ac c co ed fi go or sa ',
+ 'cy':' ac biz com ekloges gov ltd name net org parliament press pro tm ',
+ 'do':' art com edu gob gov mil net org sld web ',
+ 'dz':' art asso com edu gov net org pol ',
+ 'ec':' com edu fin gov info med mil net org pro ',
+ 'eg':' com edu eun gov mil name net org sci ',
+ 'er':' com edu gov ind mil net org rochest w ',
+ 'es':' com edu gob nom org ',
+ 'et':' biz com edu gov info name net org ',
+ 'fj':' ac biz com info mil name net org pro ',
+ 'fk':' ac co gov net nom org ',
+ 'fr':' asso com f gouv nom prd presse tm ',
+ 'gg':' co net org ',
+ 'gh':' com edu gov mil org ',
+ 'gn':' ac com gov net org ',
+ 'gr':' com edu gov mil net org ',
+ 'gt':' com edu gob ind mil net org ',
+ 'gu':' com edu gov net org ',
+ 'hk':' com edu gov idv net org ',
+ 'hu':' 2000 agrar bolt casino city co erotica erotika film forum games hotel info ingatlan jogasz konyvelo lakas media news org priv reklam sex shop sport suli szex tm tozsde utazas video ',
+ 'id':' ac co go mil net or sch web ',
+ 'il':' ac co gov idf k12 muni net org ',
+ 'in':' ac co edu ernet firm gen gov i ind mil net nic org res ',
+ 'iq':' com edu gov i mil net org ',
+ 'ir':' ac co dnssec gov i id net org sch ',
+ 'it':' edu gov ',
+ 'je':' co net org ',
+ 'jo':' com edu gov mil name net org sch ',
+ 'jp':' ac ad co ed go gr lg ne or ',
+ 'ke':' ac co go info me mobi ne or sc ',
+ 'kh':' com edu gov mil net org per ',
+ 'ki':' biz com de edu gov info mob net org tel ',
+ 'km':' asso com coop edu gouv k medecin mil nom notaires pharmaciens presse tm veterinaire ',
+ 'kn':' edu gov net org ',
+ 'kr':' ac busan chungbuk chungnam co daegu daejeon es gangwon go gwangju gyeongbuk gyeonggi gyeongnam hs incheon jeju jeonbuk jeonnam k kg mil ms ne or pe re sc seoul ulsan ',
+ 'kw':' com edu gov net org ',
+ 'ky':' com edu gov net org ',
+ 'kz':' com edu gov mil net org ',
+ 'lb':' com edu gov net org ',
+ 'lk':' assn com edu gov grp hotel int ltd net ngo org sch soc web ',
+ 'lr':' com edu gov net org ',
+ 'lv':' asn com conf edu gov id mil net org ',
+ 'ly':' com edu gov id med net org plc sch ',
+ 'ma':' ac co gov m net org press ',
+ 'mc':' asso tm ',
+ 'me':' ac co edu gov its net org priv ',
+ 'mg':' com edu gov mil nom org prd tm ',
+ 'mk':' com edu gov inf name net org pro ',
+ 'ml':' com edu gov net org presse ',
+ 'mn':' edu gov org ',
+ 'mo':' com edu gov net org ',
+ 'mt':' com edu gov net org ',
+ 'mv':' aero biz com coop edu gov info int mil museum name net org pro ',
+ 'mw':' ac co com coop edu gov int museum net org ',
+ 'mx':' com edu gob net org ',
+ 'my':' com edu gov mil name net org sch ',
+ 'nf':' arts com firm info net other per rec store web ',
+ 'ng':' biz com edu gov mil mobi name net org sch ',
+ 'ni':' ac co com edu gob mil net nom org ',
+ 'np':' com edu gov mil net org ',
+ 'nr':' biz com edu gov info net org ',
+ 'om':' ac biz co com edu gov med mil museum net org pro sch ',
+ 'pe':' com edu gob mil net nom org sld ',
+ 'ph':' com edu gov i mil net ngo org ',
+ 'pk':' biz com edu fam gob gok gon gop gos gov net org web ',
+ 'pl':' art bialystok biz com edu gda gdansk gorzow gov info katowice krakow lodz lublin mil net ngo olsztyn org poznan pwr radom slupsk szczecin torun warszawa waw wroc wroclaw zgora ',
+ 'pr':' ac biz com edu est gov info isla name net org pro prof ',
+ 'ps':' com edu gov net org plo sec ',
+ 'pw':' belau co ed go ne or ',
+ 'ro':' arts com firm info nom nt org rec store tm www ',
+ 'rs':' ac co edu gov in org ',
+ 'sb':' com edu gov net org ',
+ 'sc':' com edu gov net org ',
+ 'sh':' co com edu gov net nom org ',
+ 'sl':' com edu gov net org ',
+ 'st':' co com consulado edu embaixada gov mil net org principe saotome store ',
+ 'sv':' com edu gob org red ',
+ 'sz':' ac co org ',
+ 'tr':' av bbs bel biz com dr edu gen gov info k12 name net org pol tel tsk tv web ',
+ 'tt':' aero biz cat co com coop edu gov info int jobs mil mobi museum name net org pro tel travel ',
+ 'tw':' club com ebiz edu game gov idv mil net org ',
+ 'mu':' ac co com gov net or org ',
+ 'mz':' ac co edu gov org ',
+ 'na':' co com ',
+ 'nz':' ac co cri geek gen govt health iwi maori mil net org parliament school ',
+ 'pa':' abo ac com edu gob ing med net nom org sld ',
+ 'pt':' com edu gov int net nome org publ ',
+ 'py':' com edu gov mil net org ',
+ 'qa':' com edu gov mil net org ',
+ 're':' asso com nom ',
+ 'ru':' ac adygeya altai amur arkhangelsk astrakhan bashkiria belgorod bir bryansk buryatia cbg chel chelyabinsk chita chukotka chuvashia com dagestan e-burg edu gov grozny int irkutsk ivanovo izhevsk jar joshkar-ola kalmykia kaluga kamchatka karelia kazan kchr kemerovo khabarovsk khakassia khv kirov koenig komi kostroma kranoyarsk kuban kurgan kursk lipetsk magadan mari mari-el marine mil mordovia mosreg msk murmansk nalchik net nnov nov novosibirsk nsk omsk orenburg org oryol penza perm pp pskov ptz rnd ryazan sakhalin samara saratov simbirsk smolensk spb stavropol stv surgut tambov tatarstan tom tomsk tsaritsyn tsk tula tuva tver tyumen udm udmurtia ulan-ude vladikavkaz vladimir vladivostok volgograd vologda voronezh vrn vyatka yakutia yamal yekaterinburg yuzhno-sakhalinsk ',
+ 'rw':' ac co com edu gouv gov int mil net ',
+ 'sa':' com edu gov med net org pub sch ',
+ 'sd':' com edu gov info med net org tv ',
+ 'se':' a ac b bd c d e f g h i k l m n o org p parti pp press r s t tm u w x y z ',
+ 'sg':' com edu gov idn net org per ',
+ 'sn':' art com edu gouv org perso univ ',
+ 'sy':' com edu gov mil net news org ',
+ 'th':' ac co go in mi net or ',
+ 'tj':' ac biz co com edu go gov info int mil name net nic org test web ',
+ 'tn':' agrinet com defense edunet ens fin gov ind info intl mincom nat net org perso rnrt rns rnu tourism ',
+ 'tz':' ac co go ne or ',
+ 'ua':' biz cherkassy chernigov chernovtsy ck cn co com crimea cv dn dnepropetrovsk donetsk dp edu gov if in ivano-frankivsk kh kharkov kherson khmelnitskiy kiev kirovograd km kr ks kv lg lugansk lutsk lviv me mk net nikolaev od odessa org pl poltava pp rovno rv sebastopol sumy te ternopil uzhgorod vinnica vn zaporizhzhe zhitomir zp zt ',
+ 'ug':' ac co go ne or org sc ',
+ 'uk':' ac bl british-library co cym gov govt icnet jet lea ltd me mil mod national-library-scotland nel net nhs nic nls org orgn parliament plc police sch scot soc ',
+ 'us':' dni fed isa kids nsn ',
+ 'uy':' com edu gub mil net org ',
+ 've':' co com edu gob info mil net org web ',
+ 'vi':' co com k12 net org ',
+ 'vn':' ac biz com edu gov health info int name net org pro ',
+ 'ye':' co com gov ltd me net org plc ',
+ 'yu':' ac co edu gov org ',
+ 'za':' ac agric alt bourse city co cybernet db edu gov grondar iaccess imt inca landesign law mil net ngo nis nom olivetti org pix school tm web ',
+ 'zm':' ac co com edu gov net org sch '
+ },
+ // gorhill 2013-10-25: Using indexOf() instead Regexp(). Significant boost
+ // in both performance and memory footprint. No initialization required.
+ // http://jsperf.com/uri-js-sld-regex-vs-binary-search/4
+ // Following methods use lastIndexOf() rather than array.split() in order
+ // to avoid any memory allocations.
+ has: function(domain) {
+ var tldOffset = domain.lastIndexOf('.');
+ if (tldOffset <= 0 || tldOffset >= (domain.length-1)) {
+ return false;
+ }
+ var sldOffset = domain.lastIndexOf('.', tldOffset-1);
+ if (sldOffset <= 0 || sldOffset >= (tldOffset-1)) {
+ return false;
+ }
+ var sldList = SLD.list[domain.slice(tldOffset+1)];
+ if (!sldList) {
+ return false;
+ }
+ return sldList.indexOf(' ' + domain.slice(sldOffset+1, tldOffset) + ' ') >= 0;
+ },
+ is: function(domain) {
+ var tldOffset = domain.lastIndexOf('.');
+ if (tldOffset <= 0 || tldOffset >= (domain.length-1)) {
+ return false;
+ }
+ var sldOffset = domain.lastIndexOf('.', tldOffset-1);
+ if (sldOffset >= 0) {
+ return false;
+ }
+ var sldList = SLD.list[domain.slice(tldOffset+1)];
+ if (!sldList) {
+ return false;
+ }
+ return sldList.indexOf(' ' + domain.slice(0, tldOffset) + ' ') >= 0;
+ },
+ get: function(domain) {
+ var tldOffset = domain.lastIndexOf('.');
+ if (tldOffset <= 0 || tldOffset >= (domain.length-1)) {
+ return null;
+ }
+ var sldOffset = domain.lastIndexOf('.', tldOffset-1);
+ if (sldOffset <= 0 || sldOffset >= (tldOffset-1)) {
+ return null;
+ }
+ var sldList = SLD.list[domain.slice(tldOffset+1)];
+ if (!sldList) {
+ return null;
+ }
+ if (sldList.indexOf(' ' + domain.slice(sldOffset+1, tldOffset) + ' ') < 0) {
+ return null;
+ }
+ return domain.slice(sldOffset+1);
+ },
+ noConflict: function(){
+ if (root.SecondLevelDomains === this) {
+ root.SecondLevelDomains = _SecondLevelDomains;
+ }
+ return this;
+ }
+ };
+
+ return SLD;
+}));
+
+},{}],3:[function(require,module,exports){
+/*!
+ * URI.js - Mutating URLs
+ *
+ * Version: 1.17.0
+ *
+ * Author: Rodney Rehm
+ * Web: http://medialize.github.io/URI.js/
+ *
+ * Licensed under
+ * MIT License http://www.opensource.org/licenses/mit-license
+ * GPL v3 http://opensource.org/licenses/GPL-3.0
+ *
+ */
+(function (root, factory) {
+ 'use strict';
+ // https://github.com/umdjs/umd/blob/master/returnExports.js
+ if (typeof exports === 'object') {
+ // Node
+ module.exports = factory(require('./punycode'), require('./IPv6'), require('./SecondLevelDomains'));
+ } else if (typeof define === 'function' && define.amd) {
+ // AMD. Register as an anonymous module.
+ define(['./punycode', './IPv6', './SecondLevelDomains'], factory);
+ } else {
+ // Browser globals (root is window)
+ root.URI = factory(root.punycode, root.IPv6, root.SecondLevelDomains, root);
+ }
+}(this, function (punycode, IPv6, SLD, root) {
+ 'use strict';
+ /*global location, escape, unescape */
+ /*jshint camelcase: false */
+
+ // save current URI variable, if any
+ var _URI = root && root.URI;
+
+ function URI(url, base) {
+ var _urlSupplied = arguments.length >= 1;
+ var _baseSupplied = arguments.length >= 2;
+
+ // Allow instantiation without the 'new' keyword
+ if (!(this instanceof URI)) {
+ if (_urlSupplied) {
+ if (_baseSupplied) {
+ return new URI(url, base);
+ }
+
+ return new URI(url);
+ }
+
+ return new URI();
+ }
+
+ if (url === undefined) {
+ if (_urlSupplied) {
+ throw new TypeError('undefined is not a valid argument for URI');
+ }
+
+ if (typeof location !== 'undefined') {
+ url = location.href + '';
+ } else {
+ url = '';
+ }
+ }
+
+ this.href(url);
+
+ // resolve to base according to http://dvcs.w3.org/hg/url/raw-file/tip/Overview.html#constructor
+ if (base !== undefined) {
+ return this.absoluteTo(base);
+ }
+
+ return this;
+ }
+
+ URI.version = '1.17.0';
+
+ var p = URI.prototype;
+ var hasOwn = Object.prototype.hasOwnProperty;
+
+ function escapeRegEx(string) {
+ // https://github.com/medialize/URI.js/commit/85ac21783c11f8ccab06106dba9735a31a86924d#commitcomment-821963
+ return string.replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
+ }
+
+ function getType(value) {
+ // IE8 doesn't return [Object Undefined] but [Object Object] for undefined value
+ if (value === undefined) {
+ return 'Undefined';
+ }
+
+ return String(Object.prototype.toString.call(value)).slice(8, -1);
+ }
+
+ function isArray(obj) {
+ return getType(obj) === 'Array';
+ }
+
+ function filterArrayValues(data, value) {
+ var lookup = {};
+ var i, length;
+
+ if (getType(value) === 'RegExp') {
+ lookup = null;
+ } else if (isArray(value)) {
+ for (i = 0, length = value.length; i < length; i++) {
+ lookup[value[i]] = true;
+ }
+ } else {
+ lookup[value] = true;
+ }
+
+ for (i = 0, length = data.length; i < length; i++) {
+ /*jshint laxbreak: true */
+ var _match = lookup && lookup[data[i]] !== undefined
+ || !lookup && value.test(data[i]);
+ /*jshint laxbreak: false */
+ if (_match) {
+ data.splice(i, 1);
+ length--;
+ i--;
+ }
+ }
+
+ return data;
+ }
+
+ function arrayContains(list, value) {
+ var i, length;
+
+ // value may be string, number, array, regexp
+ if (isArray(value)) {
+ // Note: this can be optimized to O(n) (instead of current O(m * n))
+ for (i = 0, length = value.length; i < length; i++) {
+ if (!arrayContains(list, value[i])) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ var _type = getType(value);
+ for (i = 0, length = list.length; i < length; i++) {
+ if (_type === 'RegExp') {
+ if (typeof list[i] === 'string' && list[i].match(value)) {
+ return true;
+ }
+ } else if (list[i] === value) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ function arraysEqual(one, two) {
+ if (!isArray(one) || !isArray(two)) {
+ return false;
+ }
+
+ // arrays can't be equal if they have different amount of content
+ if (one.length !== two.length) {
+ return false;
+ }
+
+ one.sort();
+ two.sort();
+
+ for (var i = 0, l = one.length; i < l; i++) {
+ if (one[i] !== two[i]) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ function trimSlashes(text) {
+ var trim_expression = /^\/+|\/+$/g;
+ return text.replace(trim_expression, '');
+ }
+
+ URI._parts = function() {
+ return {
+ protocol: null,
+ username: null,
+ password: null,
+ hostname: null,
+ urn: null,
+ port: null,
+ path: null,
+ query: null,
+ fragment: null,
+ // state
+ duplicateQueryParameters: URI.duplicateQueryParameters,
+ escapeQuerySpace: URI.escapeQuerySpace
+ };
+ };
+ // state: allow duplicate query parameters (a=1&a=1)
+ URI.duplicateQueryParameters = false;
+ // state: replaces + with %20 (space in query strings)
+ URI.escapeQuerySpace = true;
+ // static properties
+ URI.protocol_expression = /^[a-z][a-z0-9.+-]*$/i;
+ URI.idn_expression = /[^a-z0-9\.-]/i;
+ URI.punycode_expression = /(xn--)/i;
+ // well, 333.444.555.666 matches, but it sure ain't no IPv4 - do we care?
+ URI.ip4_expression = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
+ // credits to Rich Brown
+ // source: http://forums.intermapper.com/viewtopic.php?p=1096#1096
+ // specification: http://www.ietf.org/rfc/rfc4291.txt
+ URI.ip6_expression = /^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/;
+ // expression used is "gruber revised" (@gruber v2) determined to be the
+ // best solution in a regex-golf we did a couple of ages ago at
+ // * http://mathiasbynens.be/demo/url-regex
+ // * http://rodneyrehm.de/t/url-regex.html
+ URI.find_uri_expression = /\b((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/ig;
+ URI.findUri = {
+ // valid "scheme://" or "www."
+ start: /\b(?:([a-z][a-z0-9.+-]*:\/\/)|www\.)/gi,
+ // everything up to the next whitespace
+ end: /[\s\r\n]|$/,
+ // trim trailing punctuation captured by end RegExp
+ trim: /[`!()\[\]{};:'".,<>?«»“”„‘’]+$/
+ };
+ // http://www.iana.org/assignments/uri-schemes.html
+ // http://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers#Well-known_ports
+ URI.defaultPorts = {
+ http: '80',
+ https: '443',
+ ftp: '21',
+ gopher: '70',
+ ws: '80',
+ wss: '443'
+ };
+ // allowed hostname characters according to RFC 3986
+ // ALPHA DIGIT "-" "." "_" "~" "!" "$" "&" "'" "(" ")" "*" "+" "," ";" "=" %encoded
+ // I've never seen a (non-IDN) hostname other than: ALPHA DIGIT . -
+ URI.invalid_hostname_characters = /[^a-zA-Z0-9\.-]/;
+ // map DOM Elements to their URI attribute
+ URI.domAttributes = {
+ 'a': 'href',
+ 'blockquote': 'cite',
+ 'link': 'href',
+ 'base': 'href',
+ 'script': 'src',
+ 'form': 'action',
+ 'img': 'src',
+ 'area': 'href',
+ 'iframe': 'src',
+ 'embed': 'src',
+ 'source': 'src',
+ 'track': 'src',
+ 'input': 'src', // but only if type="image"
+ 'audio': 'src',
+ 'video': 'src'
+ };
+ URI.getDomAttribute = function(node) {
+ if (!node || !node.nodeName) {
+ return undefined;
+ }
+
+ var nodeName = node.nodeName.toLowerCase();
+ // <input> should only expose src for type="image"
+ if (nodeName === 'input' && node.type !== 'image') {
+ return undefined;
+ }
+
+ return URI.domAttributes[nodeName];
+ };
+
+ function escapeForDumbFirefox36(value) {
+ // https://github.com/medialize/URI.js/issues/91
+ return escape(value);
+ }
+
+ // encoding / decoding according to RFC3986
+ function strictEncodeURIComponent(string) {
+ // see https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/encodeURIComponent
+ return encodeURIComponent(string)
+ .replace(/[!'()*]/g, escapeForDumbFirefox36)
+ .replace(/\*/g, '%2A');
+ }
+ URI.encode = strictEncodeURIComponent;
+ URI.decode = decodeURIComponent;
+ URI.iso8859 = function() {
+ URI.encode = escape;
+ URI.decode = unescape;
+ };
+ URI.unicode = function() {
+ URI.encode = strictEncodeURIComponent;
+ URI.decode = decodeURIComponent;
+ };
+ URI.characters = {
+ pathname: {
+ encode: {
+ // RFC3986 2.1: For consistency, URI producers and normalizers should
+ // use uppercase hexadecimal digits for all percent-encodings.
+ expression: /%(24|26|2B|2C|3B|3D|3A|40)/ig,
+ map: {
+ // -._~!'()*
+ '%24': '$',
+ '%26': '&',
+ '%2B': '+',
+ '%2C': ',',
+ '%3B': ';',
+ '%3D': '=',
+ '%3A': ':',
+ '%40': '@'
+ }
+ },
+ decode: {
+ expression: /[\/\?#]/g,
+ map: {
+ '/': '%2F',
+ '?': '%3F',
+ '#': '%23'
+ }
+ }
+ },
+ reserved: {
+ encode: {
+ // RFC3986 2.1: For consistency, URI producers and normalizers should
+ // use uppercase hexadecimal digits for all percent-encodings.
+ expression: /%(21|23|24|26|27|28|29|2A|2B|2C|2F|3A|3B|3D|3F|40|5B|5D)/ig,
+ map: {
+ // gen-delims
+ '%3A': ':',
+ '%2F': '/',
+ '%3F': '?',
+ '%23': '#',
+ '%5B': '[',
+ '%5D': ']',
+ '%40': '@',
+ // sub-delims
+ '%21': '!',
+ '%24': '$',
+ '%26': '&',
+ '%27': '\'',
+ '%28': '(',
+ '%29': ')',
+ '%2A': '*',
+ '%2B': '+',
+ '%2C': ',',
+ '%3B': ';',
+ '%3D': '='
+ }
+ }
+ },
+ urnpath: {
+ // The characters under `encode` are the characters called out by RFC 2141 as being acceptable
+ // for usage in a URN. RFC2141 also calls out "-", ".", and "_" as acceptable characters, but
+ // these aren't encoded by encodeURIComponent, so we don't have to call them out here. Also
+ // note that the colon character is not featured in the encoding map; this is because URI.js
+ // gives the colons in URNs semantic meaning as the delimiters of path segements, and so it
+ // should not appear unencoded in a segment itself.
+ // See also the note above about RFC3986 and capitalalized hex digits.
+ encode: {
+ expression: /%(21|24|27|28|29|2A|2B|2C|3B|3D|40)/ig,
+ map: {
+ '%21': '!',
+ '%24': '$',
+ '%27': '\'',
+ '%28': '(',
+ '%29': ')',
+ '%2A': '*',
+ '%2B': '+',
+ '%2C': ',',
+ '%3B': ';',
+ '%3D': '=',
+ '%40': '@'
+ }
+ },
+ // These characters are the characters called out by RFC2141 as "reserved" characters that
+ // should never appear in a URN, plus the colon character (see note above).
+ decode: {
+ expression: /[\/\?#:]/g,
+ map: {
+ '/': '%2F',
+ '?': '%3F',
+ '#': '%23',
+ ':': '%3A'
+ }
+ }
+ }
+ };
+ URI.encodeQuery = function(string, escapeQuerySpace) {
+ var escaped = URI.encode(string + '');
+ if (escapeQuerySpace === undefined) {
+ escapeQuerySpace = URI.escapeQuerySpace;
+ }
+
+ return escapeQuerySpace ? escaped.replace(/%20/g, '+') : escaped;
+ };
+ URI.decodeQuery = function(string, escapeQuerySpace) {
+ string += '';
+ if (escapeQuerySpace === undefined) {
+ escapeQuerySpace = URI.escapeQuerySpace;
+ }
+
+ try {
+ return URI.decode(escapeQuerySpace ? string.replace(/\+/g, '%20') : string);
+ } catch(e) {
+ // we're not going to mess with weird encodings,
+ // give up and return the undecoded original string
+ // see https://github.com/medialize/URI.js/issues/87
+ // see https://github.com/medialize/URI.js/issues/92
+ return string;
+ }
+ };
+ // generate encode/decode path functions
+ var _parts = {'encode':'encode', 'decode':'decode'};
+ var _part;
+ var generateAccessor = function(_group, _part) {
+ return function(string) {
+ try {
+ return URI[_part](string + '').replace(URI.characters[_group][_part].expression, function(c) {
+ return URI.characters[_group][_part].map[c];
+ });
+ } catch (e) {
+ // we're not going to mess with weird encodings,
+ // give up and return the undecoded original string
+ // see https://github.com/medialize/URI.js/issues/87
+ // see https://github.com/medialize/URI.js/issues/92
+ return string;
+ }
+ };
+ };
+
+ for (_part in _parts) {
+ URI[_part + 'PathSegment'] = generateAccessor('pathname', _parts[_part]);
+ URI[_part + 'UrnPathSegment'] = generateAccessor('urnpath', _parts[_part]);
+ }
+
+ var generateSegmentedPathFunction = function(_sep, _codingFuncName, _innerCodingFuncName) {
+ return function(string) {
+ // Why pass in names of functions, rather than the function objects themselves? The
+ // definitions of some functions (but in particular, URI.decode) will occasionally change due
+ // to URI.js having ISO8859 and Unicode modes. Passing in the name and getting it will ensure
+ // that the functions we use here are "fresh".
+ var actualCodingFunc;
+ if (!_innerCodingFuncName) {
+ actualCodingFunc = URI[_codingFuncName];
+ } else {
+ actualCodingFunc = function(string) {
+ return URI[_codingFuncName](URI[_innerCodingFuncName](string));
+ };
+ }
+
+ var segments = (string + '').split(_sep);
+
+ for (var i = 0, length = segments.length; i < length; i++) {
+ segments[i] = actualCodingFunc(segments[i]);
+ }
+
+ return segments.join(_sep);
+ };
+ };
+
+ // This takes place outside the above loop because we don't want, e.g., encodeUrnPath functions.
+ URI.decodePath = generateSegmentedPathFunction('/', 'decodePathSegment');
+ URI.decodeUrnPath = generateSegmentedPathFunction(':', 'decodeUrnPathSegment');
+ URI.recodePath = generateSegmentedPathFunction('/', 'encodePathSegment', 'decode');
+ URI.recodeUrnPath = generateSegmentedPathFunction(':', 'encodeUrnPathSegment', 'decode');
+
+ URI.encodeReserved = generateAccessor('reserved', 'encode');
+
+ URI.parse = function(string, parts) {
+ var pos;
+ if (!parts) {
+ parts = {};
+ }
+ // [protocol"://"[username[":"password]"@"]hostname[":"port]"/"?][path]["?"querystring]["#"fragment]
+
+ // extract fragment
+ pos = string.indexOf('#');
+ if (pos > -1) {
+ // escaping?
+ parts.fragment = string.substring(pos + 1) || null;
+ string = string.substring(0, pos);
+ }
+
+ // extract query
+ pos = string.indexOf('?');
+ if (pos > -1) {
+ // escaping?
+ parts.query = string.substring(pos + 1) || null;
+ string = string.substring(0, pos);
+ }
+
+ // extract protocol
+ if (string.substring(0, 2) === '//') {
+ // relative-scheme
+ parts.protocol = null;
+ string = string.substring(2);
+ // extract "user:pass@host:port"
+ string = URI.parseAuthority(string, parts);
+ } else {
+ pos = string.indexOf(':');
+ if (pos > -1) {
+ parts.protocol = string.substring(0, pos) || null;
+ if (parts.protocol && !parts.protocol.match(URI.protocol_expression)) {
+ // : may be within the path
+ parts.protocol = undefined;
+ } else if (string.substring(pos + 1, pos + 3) === '//') {
+ string = string.substring(pos + 3);
+
+ // extract "user:pass@host:port"
+ string = URI.parseAuthority(string, parts);
+ } else {
+ string = string.substring(pos + 1);
+ parts.urn = true;
+ }
+ }
+ }
+
+ // what's left must be the path
+ parts.path = string;
+
+ // and we're done
+ return parts;
+ };
+ URI.parseHost = function(string, parts) {
+ // Copy chrome, IE, opera backslash-handling behavior.
+ // Back slashes before the query string get converted to forward slashes
+ // See: https://github.com/joyent/node/blob/386fd24f49b0e9d1a8a076592a404168faeecc34/lib/url.js#L115-L124
+ // See: https://code.google.com/p/chromium/issues/detail?id=25916
+ // https://github.com/medialize/URI.js/pull/233
+ string = string.replace(/\\/g, '/');
+
+ // extract host:port
+ var pos = string.indexOf('/');
+ var bracketPos;
+ var t;
+
+ if (pos === -1) {
+ pos = string.length;
+ }
+
+ if (string.charAt(0) === '[') {
+ // IPv6 host - http://tools.ietf.org/html/draft-ietf-6man-text-addr-representation-04#section-6
+ // I claim most client software breaks on IPv6 anyways. To simplify things, URI only accepts
+ // IPv6+port in the format [2001:db8::1]:80 (for the time being)
+ bracketPos = string.indexOf(']');
+ parts.hostname = string.substring(1, bracketPos) || null;
+ parts.port = string.substring(bracketPos + 2, pos) || null;
+ if (parts.port === '/') {
+ parts.port = null;
+ }
+ } else {
+ var firstColon = string.indexOf(':');
+ var firstSlash = string.indexOf('/');
+ var nextColon = string.indexOf(':', firstColon + 1);
+ if (nextColon !== -1 && (firstSlash === -1 || nextColon < firstSlash)) {
+ // IPv6 host contains multiple colons - but no port
+ // this notation is actually not allowed by RFC 3986, but we're a liberal parser
+ parts.hostname = string.substring(0, pos) || null;
+ parts.port = null;
+ } else {
+ t = string.substring(0, pos).split(':');
+ parts.hostname = t[0] || null;
+ parts.port = t[1] || null;
+ }
+ }
+
+ if (parts.hostname && string.substring(pos).charAt(0) !== '/') {
+ pos++;
+ string = '/' + string;
+ }
+
+ return string.substring(pos) || '/';
+ };
+ URI.parseAuthority = function(string, parts) {
+ string = URI.parseUserinfo(string, parts);
+ return URI.parseHost(string, parts);
+ };
+ URI.parseUserinfo = function(string, parts) {
+ // extract username:password
+ var firstSlash = string.indexOf('/');
+ var pos = string.lastIndexOf('@', firstSlash > -1 ? firstSlash : string.length - 1);
+ var t;
+
+ // authority@ must come before /path
+ if (pos > -1 && (firstSlash === -1 || pos < firstSlash)) {
+ t = string.substring(0, pos).split(':');
+ parts.username = t[0] ? URI.decode(t[0]) : null;
+ t.shift();
+ parts.password = t[0] ? URI.decode(t.join(':')) : null;
+ string = string.substring(pos + 1);
+ } else {
+ parts.username = null;
+ parts.password = null;
+ }
+
+ return string;
+ };
+ URI.parseQuery = function(string, escapeQuerySpace) {
+ if (!string) {
+ return {};
+ }
+
+ // throw out the funky business - "?"[name"="value"&"]+
+ string = string.replace(/&+/g, '&').replace(/^\?*&*|&+$/g, '');
+
+ if (!string) {
+ return {};
+ }
+
+ var items = {};
+ var splits = string.split('&');
+ var length = splits.length;
+ var v, name, value;
+
+ for (var i = 0; i < length; i++) {
+ v = splits[i].split('=');
+ name = URI.decodeQuery(v.shift(), escapeQuerySpace);
+ // no "=" is null according to http://dvcs.w3.org/hg/url/raw-file/tip/Overview.html#collect-url-parameters
+ value = v.length ? URI.decodeQuery(v.join('='), escapeQuerySpace) : null;
+
+ if (hasOwn.call(items, name)) {
+ if (typeof items[name] === 'string' || items[name] === null) {
+ items[name] = [items[name]];
+ }
+
+ items[name].push(value);
+ } else {
+ items[name] = value;
+ }
+ }
+
+ return items;
+ };
+
+ URI.build = function(parts) {
+ var t = '';
+
+ if (parts.protocol) {
+ t += parts.protocol + ':';
+ }
+
+ if (!parts.urn && (t || parts.hostname)) {
+ t += '//';
+ }
+
+ t += (URI.buildAuthority(parts) || '');
+
+ if (typeof parts.path === 'string') {
+ if (parts.path.charAt(0) !== '/' && typeof parts.hostname === 'string') {
+ t += '/';
+ }
+
+ t += parts.path;
+ }
+
+ if (typeof parts.query === 'string' && parts.query) {
+ t += '?' + parts.query;
+ }
+
+ if (typeof parts.fragment === 'string' && parts.fragment) {
+ t += '#' + parts.fragment;
+ }
+ return t;
+ };
+ URI.buildHost = function(parts) {
+ var t = '';
+
+ if (!parts.hostname) {
+ return '';
+ } else if (URI.ip6_expression.test(parts.hostname)) {
+ t += '[' + parts.hostname + ']';
+ } else {
+ t += parts.hostname;
+ }
+
+ if (parts.port) {
+ t += ':' + parts.port;
+ }
+
+ return t;
+ };
+ URI.buildAuthority = function(parts) {
+ return URI.buildUserinfo(parts) + URI.buildHost(parts);
+ };
+ URI.buildUserinfo = function(parts) {
+ var t = '';
+
+ if (parts.username) {
+ t += URI.encode(parts.username);
+
+ if (parts.password) {
+ t += ':' + URI.encode(parts.password);
+ }
+
+ t += '@';
+ }
+
+ return t;
+ };
+ URI.buildQuery = function(data, duplicateQueryParameters, escapeQuerySpace) {
+ // according to http://tools.ietf.org/html/rfc3986 or http://labs.apache.org/webarch/uri/rfc/rfc3986.html
+ // being »-._~!$&'()*+,;=:@/?« %HEX and alnum are allowed
+ // the RFC explicitly states ?/foo being a valid use case, no mention of parameter syntax!
+ // URI.js treats the query string as being application/x-www-form-urlencoded
+ // see http://www.w3.org/TR/REC-html40/interact/forms.html#form-content-type
+
+ var t = '';
+ var unique, key, i, length;
+ for (key in data) {
+ if (hasOwn.call(data, key) && key) {
+ if (isArray(data[key])) {
+ unique = {};
+ for (i = 0, length = data[key].length; i < length; i++) {
+ if (data[key][i] !== undefined && unique[data[key][i] + ''] === undefined) {
+ t += '&' + URI.buildQueryParameter(key, data[key][i], escapeQuerySpace);
+ if (duplicateQueryParameters !== true) {
+ unique[data[key][i] + ''] = true;
+ }
+ }
+ }
+ } else if (data[key] !== undefined) {
+ t += '&' + URI.buildQueryParameter(key, data[key], escapeQuerySpace);
+ }
+ }
+ }
+
+ return t.substring(1);
+ };
+ URI.buildQueryParameter = function(name, value, escapeQuerySpace) {
+ // http://www.w3.org/TR/REC-html40/interact/forms.html#form-content-type -- application/x-www-form-urlencoded
+ // don't append "=" for null values, according to http://dvcs.w3.org/hg/url/raw-file/tip/Overview.html#url-parameter-serialization
+ return URI.encodeQuery(name, escapeQuerySpace) + (value !== null ? '=' + URI.encodeQuery(value, escapeQuerySpace) : '');
+ };
+
+ URI.addQuery = function(data, name, value) {
+ if (typeof name === 'object') {
+ for (var key in name) {
+ if (hasOwn.call(name, key)) {
+ URI.addQuery(data, key, name[key]);
+ }
+ }
+ } else if (typeof name === 'string') {
+ if (data[name] === undefined) {
+ data[name] = value;
+ return;
+ } else if (typeof data[name] === 'string') {
+ data[name] = [data[name]];
+ }
+
+ if (!isArray(value)) {
+ value = [value];
+ }
+
+ data[name] = (data[name] || []).concat(value);
+ } else {
+ throw new TypeError('URI.addQuery() accepts an object, string as the name parameter');
+ }
+ };
+ URI.removeQuery = function(data, name, value) {
+ var i, length, key;
+
+ if (isArray(name)) {
+ for (i = 0, length = name.length; i < length; i++) {
+ data[name[i]] = undefined;
+ }
+ } else if (getType(name) === 'RegExp') {
+ for (key in data) {
+ if (name.test(key)) {
+ data[key] = undefined;
+ }
+ }
+ } else if (typeof name === 'object') {
+ for (key in name) {
+ if (hasOwn.call(name, key)) {
+ URI.removeQuery(data, key, name[key]);
+ }
+ }
+ } else if (typeof name === 'string') {
+ if (value !== undefined) {
+ if (getType(value) === 'RegExp') {
+ if (!isArray(data[name]) && value.test(data[name])) {
+ data[name] = undefined;
+ } else {
+ data[name] = filterArrayValues(data[name], value);
+ }
+ } else if (data[name] === String(value) && (!isArray(value) || value.length === 1)) {
+ data[name] = undefined;
+ } else if (isArray(data[name])) {
+ data[name] = filterArrayValues(data[name], value);
+ }
+ } else {
+ data[name] = undefined;
+ }
+ } else {
+ throw new TypeError('URI.removeQuery() accepts an object, string, RegExp as the first parameter');
+ }
+ };
+ URI.hasQuery = function(data, name, value, withinArray) {
+ if (typeof name === 'object') {
+ for (var key in name) {
+ if (hasOwn.call(name, key)) {
+ if (!URI.hasQuery(data, key, name[key])) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ } else if (typeof name !== 'string') {
+ throw new TypeError('URI.hasQuery() accepts an object, string as the name parameter');
+ }
+
+ switch (getType(value)) {
+ case 'Undefined':
+ // true if exists (but may be empty)
+ return name in data; // data[name] !== undefined;
+
+ case 'Boolean':
+ // true if exists and non-empty
+ var _booly = Boolean(isArray(data[name]) ? data[name].length : data[name]);
+ return value === _booly;
+
+ case 'Function':
+ // allow complex comparison
+ return !!value(data[name], name, data);
+
+ case 'Array':
+ if (!isArray(data[name])) {
+ return false;
+ }
+
+ var op = withinArray ? arrayContains : arraysEqual;
+ return op(data[name], value);
+
+ case 'RegExp':
+ if (!isArray(data[name])) {
+ return Boolean(data[name] && data[name].match(value));
+ }
+
+ if (!withinArray) {
+ return false;
+ }
+
+ return arrayContains(data[name], value);
+
+ case 'Number':
+ value = String(value);
+ /* falls through */
+ case 'String':
+ if (!isArray(data[name])) {
+ return data[name] === value;
+ }
+
+ if (!withinArray) {
+ return false;
+ }
+
+ return arrayContains(data[name], value);
+
+ default:
+ throw new TypeError('URI.hasQuery() accepts undefined, boolean, string, number, RegExp, Function as the value parameter');
+ }
+ };
+
+
+ URI.commonPath = function(one, two) {
+ var length = Math.min(one.length, two.length);
+ var pos;
+
+ // find first non-matching character
+ for (pos = 0; pos < length; pos++) {
+ if (one.charAt(pos) !== two.charAt(pos)) {
+ pos--;
+ break;
+ }
+ }
+
+ if (pos < 1) {
+ return one.charAt(0) === two.charAt(0) && one.charAt(0) === '/' ? '/' : '';
+ }
+
+ // revert to last /
+ if (one.charAt(pos) !== '/' || two.charAt(pos) !== '/') {
+ pos = one.substring(0, pos).lastIndexOf('/');
+ }
+
+ return one.substring(0, pos + 1);
+ };
+
+ URI.withinString = function(string, callback, options) {
+ options || (options = {});
+ var _start = options.start || URI.findUri.start;
+ var _end = options.end || URI.findUri.end;
+ var _trim = options.trim || URI.findUri.trim;
+ var _attributeOpen = /[a-z0-9-]=["']?$/i;
+
+ _start.lastIndex = 0;
+ while (true) {
+ var match = _start.exec(string);
+ if (!match) {
+ break;
+ }
+
+ var start = match.index;
+ if (options.ignoreHtml) {
+ // attribut(e=["']?$)
+ var attributeOpen = string.slice(Math.max(start - 3, 0), start);
+ if (attributeOpen && _attributeOpen.test(attributeOpen)) {
+ continue;
+ }
+ }
+
+ var end = start + string.slice(start).search(_end);
+ var slice = string.slice(start, end).replace(_trim, '');
+ if (options.ignore && options.ignore.test(slice)) {
+ continue;
+ }
+
+ end = start + slice.length;
+ var result = callback(slice, start, end, string);
+ string = string.slice(0, start) + result + string.slice(end);
+ _start.lastIndex = start + result.length;
+ }
+
+ _start.lastIndex = 0;
+ return string;
+ };
+
+ URI.ensureValidHostname = function(v) {
+ // Theoretically URIs allow percent-encoding in Hostnames (according to RFC 3986)
+ // they are not part of DNS and therefore ignored by URI.js
+
+ if (v.match(URI.invalid_hostname_characters)) {
+ // test punycode
+ if (!punycode) {
+ throw new TypeError('Hostname "' + v + '" contains characters other than [A-Z0-9.-] and Punycode.js is not available');
+ }
+
+ if (punycode.toASCII(v).match(URI.invalid_hostname_characters)) {
+ throw new TypeError('Hostname "' + v + '" contains characters other than [A-Z0-9.-]');
+ }
+ }
+ };
+
+ // noConflict
+ URI.noConflict = function(removeAll) {
+ if (removeAll) {
+ var unconflicted = {
+ URI: this.noConflict()
+ };
+
+ if (root.URITemplate && typeof root.URITemplate.noConflict === 'function') {
+ unconflicted.URITemplate = root.URITemplate.noConflict();
+ }
+
+ if (root.IPv6 && typeof root.IPv6.noConflict === 'function') {
+ unconflicted.IPv6 = root.IPv6.noConflict();
+ }
+
+ if (root.SecondLevelDomains && typeof root.SecondLevelDomains.noConflict === 'function') {
+ unconflicted.SecondLevelDomains = root.SecondLevelDomains.noConflict();
+ }
+
+ return unconflicted;
+ } else if (root.URI === this) {
+ root.URI = _URI;
+ }
+
+ return this;
+ };
+
+ p.build = function(deferBuild) {
+ if (deferBuild === true) {
+ this._deferred_build = true;
+ } else if (deferBuild === undefined || this._deferred_build) {
+ this._string = URI.build(this._parts);
+ this._deferred_build = false;
+ }
+
+ return this;
+ };
+
+ p.clone = function() {
+ return new URI(this);
+ };
+
+ p.valueOf = p.toString = function() {
+ return this.build(false)._string;
+ };
+
+
+ function generateSimpleAccessor(_part){
+ return function(v, build) {
+ if (v === undefined) {
+ return this._parts[_part] || '';
+ } else {
+ this._parts[_part] = v || null;
+ this.build(!build);
+ return this;
+ }
+ };
+ }
+
+ function generatePrefixAccessor(_part, _key){
+ return function(v, build) {
+ if (v === undefined) {
+ return this._parts[_part] || '';
+ } else {
+ if (v !== null) {
+ v = v + '';
+ if (v.charAt(0) === _key) {
+ v = v.substring(1);
+ }
+ }
+
+ this._parts[_part] = v;
+ this.build(!build);
+ return this;
+ }
+ };
+ }
+
+ p.protocol = generateSimpleAccessor('protocol');
+ p.username = generateSimpleAccessor('username');
+ p.password = generateSimpleAccessor('password');
+ p.hostname = generateSimpleAccessor('hostname');
+ p.port = generateSimpleAccessor('port');
+ p.query = generatePrefixAccessor('query', '?');
+ p.fragment = generatePrefixAccessor('fragment', '#');
+
+ p.search = function(v, build) {
+ var t = this.query(v, build);
+ return typeof t === 'string' && t.length ? ('?' + t) : t;
+ };
+ p.hash = function(v, build) {
+ var t = this.fragment(v, build);
+ return typeof t === 'string' && t.length ? ('#' + t) : t;
+ };
+
+ p.pathname = function(v, build) {
+ if (v === undefined || v === true) {
+ var res = this._parts.path || (this._parts.hostname ? '/' : '');
+ return v ? (this._parts.urn ? URI.decodeUrnPath : URI.decodePath)(res) : res;
+ } else {
+ if (this._parts.urn) {
+ this._parts.path = v ? URI.recodeUrnPath(v) : '';
+ } else {
+ this._parts.path = v ? URI.recodePath(v) : '/';
+ }
+ this.build(!build);
+ return this;
+ }
+ };
+ p.path = p.pathname;
+ p.href = function(href, build) {
+ var key;
+
+ if (href === undefined) {
+ return this.toString();
+ }
+
+ this._string = '';
+ this._parts = URI._parts();
+
+ var _URI = href instanceof URI;
+ var _object = typeof href === 'object' && (href.hostname || href.path || href.pathname);
+ if (href.nodeName) {
+ var attribute = URI.getDomAttribute(href);
+ href = href[attribute] || '';
+ _object = false;
+ }
+
+ // window.location is reported to be an object, but it's not the sort
+ // of object we're looking for:
+ // * location.protocol ends with a colon
+ // * location.query != object.search
+ // * location.hash != object.fragment
+ // simply serializing the unknown object should do the trick
+ // (for location, not for everything...)
+ if (!_URI && _object && href.pathname !== undefined) {
+ href = href.toString();
+ }
+
+ if (typeof href === 'string' || href instanceof String) {
+ this._parts = URI.parse(String(href), this._parts);
+ } else if (_URI || _object) {
+ var src = _URI ? href._parts : href;
+ for (key in src) {
+ if (hasOwn.call(this._parts, key)) {
+ this._parts[key] = src[key];
+ }
+ }
+ } else {
+ throw new TypeError('invalid input');
+ }
+
+ this.build(!build);
+ return this;
+ };
+
+ // identification accessors
+ p.is = function(what) {
+ var ip = false;
+ var ip4 = false;
+ var ip6 = false;
+ var name = false;
+ var sld = false;
+ var idn = false;
+ var punycode = false;
+ var relative = !this._parts.urn;
+
+ if (this._parts.hostname) {
+ relative = false;
+ ip4 = URI.ip4_expression.test(this._parts.hostname);
+ ip6 = URI.ip6_expression.test(this._parts.hostname);
+ ip = ip4 || ip6;
+ name = !ip;
+ sld = name && SLD && SLD.has(this._parts.hostname);
+ idn = name && URI.idn_expression.test(this._parts.hostname);
+ punycode = name && URI.punycode_expression.test(this._parts.hostname);
+ }
+
+ switch (what.toLowerCase()) {
+ case 'relative':
+ return relative;
+
+ case 'absolute':
+ return !relative;
+
+ // hostname identification
+ case 'domain':
+ case 'name':
+ return name;
+
+ case 'sld':
+ return sld;
+
+ case 'ip':
+ return ip;
+
+ case 'ip4':
+ case 'ipv4':
+ case 'inet4':
+ return ip4;
+
+ case 'ip6':
+ case 'ipv6':
+ case 'inet6':
+ return ip6;
+
+ case 'idn':
+ return idn;
+
+ case 'url':
+ return !this._parts.urn;
+
+ case 'urn':
+ return !!this._parts.urn;
+
+ case 'punycode':
+ return punycode;
+ }
+
+ return null;
+ };
+
+ // component specific input validation
+ var _protocol = p.protocol;
+ var _port = p.port;
+ var _hostname = p.hostname;
+
+ p.protocol = function(v, build) {
+ if (v !== undefined) {
+ if (v) {
+ // accept trailing ://
+ v = v.replace(/:(\/\/)?$/, '');
+
+ if (!v.match(URI.protocol_expression)) {
+ throw new TypeError('Protocol "' + v + '" contains characters other than [A-Z0-9.+-] or doesn\'t start with [A-Z]');
+ }
+ }
+ }
+ return _protocol.call(this, v, build);
+ };
+ p.scheme = p.protocol;
+ p.port = function(v, build) {
+ if (this._parts.urn) {
+ return v === undefined ? '' : this;
+ }
+
+ if (v !== undefined) {
+ if (v === 0) {
+ v = null;
+ }
+
+ if (v) {
+ v += '';
+ if (v.charAt(0) === ':') {
+ v = v.substring(1);
+ }
+
+ if (v.match(/[^0-9]/)) {
+ throw new TypeError('Port "' + v + '" contains characters other than [0-9]');
+ }
+ }
+ }
+ return _port.call(this, v, build);
+ };
+ p.hostname = function(v, build) {
+ if (this._parts.urn) {
+ return v === undefined ? '' : this;
+ }
+
+ if (v !== undefined) {
+ var x = {};
+ var res = URI.parseHost(v, x);
+ if (res !== '/') {
+ throw new TypeError('Hostname "' + v + '" contains characters other than [A-Z0-9.-]');
+ }
+
+ v = x.hostname;
+ }
+ return _hostname.call(this, v, build);
+ };
+
+ // compound accessors
+ p.origin = function(v, build) {
+ var parts;
+
+ if (this._parts.urn) {
+ return v === undefined ? '' : this;
+ }
+
+ if (v === undefined) {
+ var protocol = this.protocol();
+ var authority = this.authority();
+ if (!authority) return '';
+ return (protocol ? protocol + '://' : '') + this.authority();
+ } else {
+ var origin = URI(v);
+ this
+ .protocol(origin.protocol())
+ .authority(origin.authority())
+ .build(!build);
+ return this;
+ }
+ };
+ p.host = function(v, build) {
+ if (this._parts.urn) {
+ return v === undefined ? '' : this;
+ }
+
+ if (v === undefined) {
+ return this._parts.hostname ? URI.buildHost(this._parts) : '';
+ } else {
+ var res = URI.parseHost(v, this._parts);
+ if (res !== '/') {
+ throw new TypeError('Hostname "' + v + '" contains characters other than [A-Z0-9.-]');
+ }
+
+ this.build(!build);
+ return this;
+ }
+ };
+ p.authority = function(v, build) {
+ if (this._parts.urn) {
+ return v === undefined ? '' : this;
+ }
+
+ if (v === undefined) {
+ return this._parts.hostname ? URI.buildAuthority(this._parts) : '';
+ } else {
+ var res = URI.parseAuthority(v, this._parts);
+ if (res !== '/') {
+ throw new TypeError('Hostname "' + v + '" contains characters other than [A-Z0-9.-]');
+ }
+
+ this.build(!build);
+ return this;
+ }
+ };
+ p.userinfo = function(v, build) {
+ if (this._parts.urn) {
+ return v === undefined ? '' : this;
+ }
+
+ if (v === undefined) {
+ if (!this._parts.username) {
+ return '';
+ }
+
+ var t = URI.buildUserinfo(this._parts);
+ return t.substring(0, t.length -1);
+ } else {
+ if (v[v.length-1] !== '@') {
+ v += '@';
+ }
+
+ URI.parseUserinfo(v, this._parts);
+ this.build(!build);
+ return this;
+ }
+ };
+ p.resource = function(v, build) {
+ var parts;
+
+ if (v === undefined) {
+ return this.path() + this.search() + this.hash();
+ }
+
+ parts = URI.parse(v);
+ this._parts.path = parts.path;
+ this._parts.query = parts.query;
+ this._parts.fragment = parts.fragment;
+ this.build(!build);
+ return this;
+ };
+
+ // fraction accessors
+ p.subdomain = function(v, build) {
+ if (this._parts.urn) {
+ return v === undefined ? '' : this;
+ }
+
+ // convenience, return "www" from "www.example.org"
+ if (v === undefined) {
+ if (!this._parts.hostname || this.is('IP')) {
+ return '';
+ }
+
+ // grab domain and add another segment
+ var end = this._parts.hostname.length - this.domain().length - 1;
+ return this._parts.hostname.substring(0, end) || '';
+ } else {
+ var e = this._parts.hostname.length - this.domain().length;
+ var sub = this._parts.hostname.substring(0, e);
+ var replace = new RegExp('^' + escapeRegEx(sub));
+
+ if (v && v.charAt(v.length - 1) !== '.') {
+ v += '.';
+ }
+
+ if (v) {
+ URI.ensureValidHostname(v);
+ }
+
+ this._parts.hostname = this._parts.hostname.replace(replace, v);
+ this.build(!build);
+ return this;
+ }
+ };
+ p.domain = function(v, build) {
+ if (this._parts.urn) {
+ return v === undefined ? '' : this;
+ }
+
+ if (typeof v === 'boolean') {
+ build = v;
+ v = undefined;
+ }
+
+ // convenience, return "example.org" from "www.example.org"
+ if (v === undefined) {
+ if (!this._parts.hostname || this.is('IP')) {
+ return '';
+ }
+
+ // if hostname consists of 1 or 2 segments, it must be the domain
+ var t = this._parts.hostname.match(/\./g);
+ if (t && t.length < 2) {
+ return this._parts.hostname;
+ }
+
+ // grab tld and add another segment
+ var end = this._parts.hostname.length - this.tld(build).length - 1;
+ end = this._parts.hostname.lastIndexOf('.', end -1) + 1;
+ return this._parts.hostname.substring(end) || '';
+ } else {
+ if (!v) {
+ throw new TypeError('cannot set domain empty');
+ }
+
+ URI.ensureValidHostname(v);
+
+ if (!this._parts.hostname || this.is('IP')) {
+ this._parts.hostname = v;
+ } else {
+ var replace = new RegExp(escapeRegEx(this.domain()) + '$');
+ this._parts.hostname = this._parts.hostname.replace(replace, v);
+ }
+
+ this.build(!build);
+ return this;
+ }
+ };
+ p.tld = function(v, build) {
+ if (this._parts.urn) {
+ return v === undefined ? '' : this;
+ }
+
+ if (typeof v === 'boolean') {
+ build = v;
+ v = undefined;
+ }
+
+ // return "org" from "www.example.org"
+ if (v === undefined) {
+ if (!this._parts.hostname || this.is('IP')) {
+ return '';
+ }
+
+ var pos = this._parts.hostname.lastIndexOf('.');
+ var tld = this._parts.hostname.substring(pos + 1);
+
+ if (build !== true && SLD && SLD.list[tld.toLowerCase()]) {
+ return SLD.get(this._parts.hostname) || tld;
+ }
+
+ return tld;
+ } else {
+ var replace;
+
+ if (!v) {
+ throw new TypeError('cannot set TLD empty');
+ } else if (v.match(/[^a-zA-Z0-9-]/)) {
+ if (SLD && SLD.is(v)) {
+ replace = new RegExp(escapeRegEx(this.tld()) + '$');
+ this._parts.hostname = this._parts.hostname.replace(replace, v);
+ } else {
+ throw new TypeError('TLD "' + v + '" contains characters other than [A-Z0-9]');
+ }
+ } else if (!this._parts.hostname || this.is('IP')) {
+ throw new ReferenceError('cannot set TLD on non-domain host');
+ } else {
+ replace = new RegExp(escapeRegEx(this.tld()) + '$');
+ this._parts.hostname = this._parts.hostname.replace(replace, v);
+ }
+
+ this.build(!build);
+ return this;
+ }
+ };
+ p.directory = function(v, build) {
+ if (this._parts.urn) {
+ return v === undefined ? '' : this;
+ }
+
+ if (v === undefined || v === true) {
+ if (!this._parts.path && !this._parts.hostname) {
+ return '';
+ }
+
+ if (this._parts.path === '/') {
+ return '/';
+ }
+
+ var end = this._parts.path.length - this.filename().length - 1;
+ var res = this._parts.path.substring(0, end) || (this._parts.hostname ? '/' : '');
+
+ return v ? URI.decodePath(res) : res;
+
+ } else {
+ var e = this._parts.path.length - this.filename().length;
+ var directory = this._parts.path.substring(0, e);
+ var replace = new RegExp('^' + escapeRegEx(directory));
+
+ // fully qualifier directories begin with a slash
+ if (!this.is('relative')) {
+ if (!v) {
+ v = '/';
+ }
+
+ if (v.charAt(0) !== '/') {
+ v = '/' + v;
+ }
+ }
+
+ // directories always end with a slash
+ if (v && v.charAt(v.length - 1) !== '/') {
+ v += '/';
+ }
+
+ v = URI.recodePath(v);
+ this._parts.path = this._parts.path.replace(replace, v);
+ this.build(!build);
+ return this;
+ }
+ };
+ p.filename = function(v, build) {
+ if (this._parts.urn) {
+ return v === undefined ? '' : this;
+ }
+
+ if (v === undefined || v === true) {
+ if (!this._parts.path || this._parts.path === '/') {
+ return '';
+ }
+
+ var pos = this._parts.path.lastIndexOf('/');
+ var res = this._parts.path.substring(pos+1);
+
+ return v ? URI.decodePathSegment(res) : res;
+ } else {
+ var mutatedDirectory = false;
+
+ if (v.charAt(0) === '/') {
+ v = v.substring(1);
+ }
+
+ if (v.match(/\.?\//)) {
+ mutatedDirectory = true;
+ }
+
+ var replace = new RegExp(escapeRegEx(this.filename()) + '$');
+ v = URI.recodePath(v);
+ this._parts.path = this._parts.path.replace(replace, v);
+
+ if (mutatedDirectory) {
+ this.normalizePath(build);
+ } else {
+ this.build(!build);
+ }
+
+ return this;
+ }
+ };
+ p.suffix = function(v, build) {
+ if (this._parts.urn) {
+ return v === undefined ? '' : this;
+ }
+
+ if (v === undefined || v === true) {
+ if (!this._parts.path || this._parts.path === '/') {
+ return '';
+ }
+
+ var filename = this.filename();
+ var pos = filename.lastIndexOf('.');
+ var s, res;
+
+ if (pos === -1) {
+ return '';
+ }
+
+ // suffix may only contain alnum characters (yup, I made this up.)
+ s = filename.substring(pos+1);
+ res = (/^[a-z0-9%]+$/i).test(s) ? s : '';
+ return v ? URI.decodePathSegment(res) : res;
+ } else {
+ if (v.charAt(0) === '.') {
+ v = v.substring(1);
+ }
+
+ var suffix = this.suffix();
+ var replace;
+
+ if (!suffix) {
+ if (!v) {
+ return this;
+ }
+
+ this._parts.path += '.' + URI.recodePath(v);
+ } else if (!v) {
+ replace = new RegExp(escapeRegEx('.' + suffix) + '$');
+ } else {
+ replace = new RegExp(escapeRegEx(suffix) + '$');
+ }
+
+ if (replace) {
+ v = URI.recodePath(v);
+ this._parts.path = this._parts.path.replace(replace, v);
+ }
+
+ this.build(!build);
+ return this;
+ }
+ };
+ p.segment = function(segment, v, build) {
+ var separator = this._parts.urn ? ':' : '/';
+ var path = this.path();
+ var absolute = path.substring(0, 1) === '/';
+ var segments = path.split(separator);
+
+ if (segment !== undefined && typeof segment !== 'number') {
+ build = v;
+ v = segment;
+ segment = undefined;
+ }
+
+ if (segment !== undefined && typeof segment !== 'number') {
+ throw new Error('Bad segment "' + segment + '", must be 0-based integer');
+ }
+
+ if (absolute) {
+ segments.shift();
+ }
+
+ if (segment < 0) {
+ // allow negative indexes to address from the end
+ segment = Math.max(segments.length + segment, 0);
+ }
+
+ if (v === undefined) {
+ /*jshint laxbreak: true */
+ return segment === undefined
+ ? segments
+ : segments[segment];
+ /*jshint laxbreak: false */
+ } else if (segment === null || segments[segment] === undefined) {
+ if (isArray(v)) {
+ segments = [];
+ // collapse empty elements within array
+ for (var i=0, l=v.length; i < l; i++) {
+ if (!v[i].length && (!segments.length || !segments[segments.length -1].length)) {
+ continue;
+ }
+
+ if (segments.length && !segments[segments.length -1].length) {
+ segments.pop();
+ }
+
+ segments.push(trimSlashes(v[i]));
+ }
+ } else if (v || typeof v === 'string') {
+ v = trimSlashes(v);
+ if (segments[segments.length -1] === '') {
+ // empty trailing elements have to be overwritten
+ // to prevent results such as /foo//bar
+ segments[segments.length -1] = v;
+ } else {
+ segments.push(v);
+ }
+ }
+ } else {
+ if (v) {
+ segments[segment] = trimSlashes(v);
+ } else {
+ segments.splice(segment, 1);
+ }
+ }
+
+ if (absolute) {
+ segments.unshift('');
+ }
+
+ return this.path(segments.join(separator), build);
+ };
+ p.segmentCoded = function(segment, v, build) {
+ var segments, i, l;
+
+ if (typeof segment !== 'number') {
+ build = v;
+ v = segment;
+ segment = undefined;
+ }
+
+ if (v === undefined) {
+ segments = this.segment(segment, v, build);
+ if (!isArray(segments)) {
+ segments = segments !== undefined ? URI.decode(segments) : undefined;
+ } else {
+ for (i = 0, l = segments.length; i < l; i++) {
+ segments[i] = URI.decode(segments[i]);
+ }
+ }
+
+ return segments;
+ }
+
+ if (!isArray(v)) {
+ v = (typeof v === 'string' || v instanceof String) ? URI.encode(v) : v;
+ } else {
+ for (i = 0, l = v.length; i < l; i++) {
+ v[i] = URI.encode(v[i]);
+ }
+ }
+
+ return this.segment(segment, v, build);
+ };
+
+ // mutating query string
+ var q = p.query;
+ p.query = function(v, build) {
+ if (v === true) {
+ return URI.parseQuery(this._parts.query, this._parts.escapeQuerySpace);
+ } else if (typeof v === 'function') {
+ var data = URI.parseQuery(this._parts.query, this._parts.escapeQuerySpace);
+ var result = v.call(this, data);
+ this._parts.query = URI.buildQuery(result || data, this._parts.duplicateQueryParameters, this._parts.escapeQuerySpace);
+ this.build(!build);
+ return this;
+ } else if (v !== undefined && typeof v !== 'string') {
+ this._parts.query = URI.buildQuery(v, this._parts.duplicateQueryParameters, this._parts.escapeQuerySpace);
+ this.build(!build);
+ return this;
+ } else {
+ return q.call(this, v, build);
+ }
+ };
+ p.setQuery = function(name, value, build) {
+ var data = URI.parseQuery(this._parts.query, this._parts.escapeQuerySpace);
+
+ if (typeof name === 'string' || name instanceof String) {
+ data[name] = value !== undefined ? value : null;
+ } else if (typeof name === 'object') {
+ for (var key in name) {
+ if (hasOwn.call(name, key)) {
+ data[key] = name[key];
+ }
+ }
+ } else {
+ throw new TypeError('URI.addQuery() accepts an object, string as the name parameter');
+ }
+
+ this._parts.query = URI.buildQuery(data, this._parts.duplicateQueryParameters, this._parts.escapeQuerySpace);
+ if (typeof name !== 'string') {
+ build = value;
+ }
+
+ this.build(!build);
+ return this;
+ };
+ p.addQuery = function(name, value, build) {
+ var data = URI.parseQuery(this._parts.query, this._parts.escapeQuerySpace);
+ URI.addQuery(data, name, value === undefined ? null : value);
+ this._parts.query = URI.buildQuery(data, this._parts.duplicateQueryParameters, this._parts.escapeQuerySpace);
+ if (typeof name !== 'string') {
+ build = value;
+ }
+
+ this.build(!build);
+ return this;
+ };
+ p.removeQuery = function(name, value, build) {
+ var data = URI.parseQuery(this._parts.query, this._parts.escapeQuerySpace);
+ URI.removeQuery(data, name, value);
+ this._parts.query = URI.buildQuery(data, this._parts.duplicateQueryParameters, this._parts.escapeQuerySpace);
+ if (typeof name !== 'string') {
+ build = value;
+ }
+
+ this.build(!build);
+ return this;
+ };
+ p.hasQuery = function(name, value, withinArray) {
+ var data = URI.parseQuery(this._parts.query, this._parts.escapeQuerySpace);
+ return URI.hasQuery(data, name, value, withinArray);
+ };
+ p.setSearch = p.setQuery;
+ p.addSearch = p.addQuery;
+ p.removeSearch = p.removeQuery;
+ p.hasSearch = p.hasQuery;
+
+ // sanitizing URLs
+ p.normalize = function() {
+ if (this._parts.urn) {
+ return this
+ .normalizeProtocol(false)
+ .normalizePath(false)
+ .normalizeQuery(false)
+ .normalizeFragment(false)
+ .build();
+ }
+
+ return this
+ .normalizeProtocol(false)
+ .normalizeHostname(false)
+ .normalizePort(false)
+ .normalizePath(false)
+ .normalizeQuery(false)
+ .normalizeFragment(false)
+ .build();
+ };
+ p.normalizeProtocol = function(build) {
+ if (typeof this._parts.protocol === 'string') {
+ this._parts.protocol = this._parts.protocol.toLowerCase();
+ this.build(!build);
+ }
+
+ return this;
+ };
+ p.normalizeHostname = function(build) {
+ if (this._parts.hostname) {
+ if (this.is('IDN') && punycode) {
+ this._parts.hostname = punycode.toASCII(this._parts.hostname);
+ } else if (this.is('IPv6') && IPv6) {
+ this._parts.hostname = IPv6.best(this._parts.hostname);
+ }
+
+ this._parts.hostname = this._parts.hostname.toLowerCase();
+ this.build(!build);
+ }
+
+ return this;
+ };
+ p.normalizePort = function(build) {
+ // remove port of it's the protocol's default
+ if (typeof this._parts.protocol === 'string' && this._parts.port === URI.defaultPorts[this._parts.protocol]) {
+ this._parts.port = null;
+ this.build(!build);
+ }
+
+ return this;
+ };
+ p.normalizePath = function(build) {
+ var _path = this._parts.path;
+ if (!_path) {
+ return this;
+ }
+
+ if (this._parts.urn) {
+ this._parts.path = URI.recodeUrnPath(this._parts.path);
+ this.build(!build);
+ return this;
+ }
+
+ if (this._parts.path === '/') {
+ return this;
+ }
+
+ var _was_relative;
+ var _leadingParents = '';
+ var _parent, _pos;
+
+ // handle relative paths
+ if (_path.charAt(0) !== '/') {
+ _was_relative = true;
+ _path = '/' + _path;
+ }
+
+ // handle relative files (as opposed to directories)
+ if (_path.slice(-3) === '/..' || _path.slice(-2) === '/.') {
+ _path += '/';
+ }
+
+ // resolve simples
+ _path = _path
+ .replace(/(\/(\.\/)+)|(\/\.$)/g, '/')
+ .replace(/\/{2,}/g, '/');
+
+ // remember leading parents
+ if (_was_relative) {
+ _leadingParents = _path.substring(1).match(/^(\.\.\/)+/) || '';
+ if (_leadingParents) {
+ _leadingParents = _leadingParents[0];
+ }
+ }
+
+ // resolve parents
+ while (true) {
+ _parent = _path.indexOf('/..');
+ if (_parent === -1) {
+ // no more ../ to resolve
+ break;
+ } else if (_parent === 0) {
+ // top level cannot be relative, skip it
+ _path = _path.substring(3);
+ continue;
+ }
+
+ _pos = _path.substring(0, _parent).lastIndexOf('/');
+ if (_pos === -1) {
+ _pos = _parent;
+ }
+ _path = _path.substring(0, _pos) + _path.substring(_parent + 3);
+ }
+
+ // revert to relative
+ if (_was_relative && this.is('relative')) {
+ _path = _leadingParents + _path.substring(1);
+ }
+
+ _path = URI.recodePath(_path);
+ this._parts.path = _path;
+ this.build(!build);
+ return this;
+ };
+ p.normalizePathname = p.normalizePath;
+ p.normalizeQuery = function(build) {
+ if (typeof this._parts.query === 'string') {
+ if (!this._parts.query.length) {
+ this._parts.query = null;
+ } else {
+ this.query(URI.parseQuery(this._parts.query, this._parts.escapeQuerySpace));
+ }
+
+ this.build(!build);
+ }
+
+ return this;
+ };
+ p.normalizeFragment = function(build) {
+ if (!this._parts.fragment) {
+ this._parts.fragment = null;
+ this.build(!build);
+ }
+
+ return this;
+ };
+ p.normalizeSearch = p.normalizeQuery;
+ p.normalizeHash = p.normalizeFragment;
+
+ p.iso8859 = function() {
+ // expect unicode input, iso8859 output
+ var e = URI.encode;
+ var d = URI.decode;
+
+ URI.encode = escape;
+ URI.decode = decodeURIComponent;
+ try {
+ this.normalize();
+ } finally {
+ URI.encode = e;
+ URI.decode = d;
+ }
+ return this;
+ };
+
+ p.unicode = function() {
+ // expect iso8859 input, unicode output
+ var e = URI.encode;
+ var d = URI.decode;
+
+ URI.encode = strictEncodeURIComponent;
+ URI.decode = unescape;
+ try {
+ this.normalize();
+ } finally {
+ URI.encode = e;
+ URI.decode = d;
+ }
+ return this;
+ };
+
+ p.readable = function() {
+ var uri = this.clone();
+ // removing username, password, because they shouldn't be displayed according to RFC 3986
+ uri.username('').password('').normalize();
+ var t = '';
+ if (uri._parts.protocol) {
+ t += uri._parts.protocol + '://';
+ }
+
+ if (uri._parts.hostname) {
+ if (uri.is('punycode') && punycode) {
+ t += punycode.toUnicode(uri._parts.hostname);
+ if (uri._parts.port) {
+ t += ':' + uri._parts.port;
+ }
+ } else {
+ t += uri.host();
+ }
+ }
+
+ if (uri._parts.hostname && uri._parts.path && uri._parts.path.charAt(0) !== '/') {
+ t += '/';
+ }
+
+ t += uri.path(true);
+ if (uri._parts.query) {
+ var q = '';
+ for (var i = 0, qp = uri._parts.query.split('&'), l = qp.length; i < l; i++) {
+ var kv = (qp[i] || '').split('=');
+ q += '&' + URI.decodeQuery(kv[0], this._parts.escapeQuerySpace)
+ .replace(/&/g, '%26');
+
+ if (kv[1] !== undefined) {
+ q += '=' + URI.decodeQuery(kv[1], this._parts.escapeQuerySpace)
+ .replace(/&/g, '%26');
+ }
+ }
+ t += '?' + q.substring(1);
+ }
+
+ t += URI.decodeQuery(uri.hash(), true);
+ return t;
+ };
+
+ // resolving relative and absolute URLs
+ p.absoluteTo = function(base) {
+ var resolved = this.clone();
+ var properties = ['protocol', 'username', 'password', 'hostname', 'port'];
+ var basedir, i, p;
+
+ if (this._parts.urn) {
+ throw new Error('URNs do not have any generally defined hierarchical components');
+ }
+
+ if (!(base instanceof URI)) {
+ base = new URI(base);
+ }
+
+ if (!resolved._parts.protocol) {
+ resolved._parts.protocol = base._parts.protocol;
+ }
+
+ if (this._parts.hostname) {
+ return resolved;
+ }
+
+ for (i = 0; (p = properties[i]); i++) {
+ resolved._parts[p] = base._parts[p];
+ }
+
+ if (!resolved._parts.path) {
+ resolved._parts.path = base._parts.path;
+ if (!resolved._parts.query) {
+ resolved._parts.query = base._parts.query;
+ }
+ } else if (resolved._parts.path.substring(-2) === '..') {
+ resolved._parts.path += '/';
+ }
+
+ if (resolved.path().charAt(0) !== '/') {
+ basedir = base.directory();
+ basedir = basedir ? basedir : base.path().indexOf('/') === 0 ? '/' : '';
+ resolved._parts.path = (basedir ? (basedir + '/') : '') + resolved._parts.path;
+ resolved.normalizePath();
+ }
+
+ resolved.build();
+ return resolved;
+ };
+ p.relativeTo = function(base) {
+ var relative = this.clone().normalize();
+ var relativeParts, baseParts, common, relativePath, basePath;
+
+ if (relative._parts.urn) {
+ throw new Error('URNs do not have any generally defined hierarchical components');
+ }
+
+ base = new URI(base).normalize();
+ relativeParts = relative._parts;
+ baseParts = base._parts;
+ relativePath = relative.path();
+ basePath = base.path();
+
+ if (relativePath.charAt(0) !== '/') {
+ throw new Error('URI is already relative');
+ }
+
+ if (basePath.charAt(0) !== '/') {
+ throw new Error('Cannot calculate a URI relative to another relative URI');
+ }
+
+ if (relativeParts.protocol === baseParts.protocol) {
+ relativeParts.protocol = null;
+ }
+
+ if (relativeParts.username !== baseParts.username || relativeParts.password !== baseParts.password) {
+ return relative.build();
+ }
+
+ if (relativeParts.protocol !== null || relativeParts.username !== null || relativeParts.password !== null) {
+ return relative.build();
+ }
+
+ if (relativeParts.hostname === baseParts.hostname && relativeParts.port === baseParts.port) {
+ relativeParts.hostname = null;
+ relativeParts.port = null;
+ } else {
+ return relative.build();
+ }
+
+ if (relativePath === basePath) {
+ relativeParts.path = '';
+ return relative.build();
+ }
+
+ // determine common sub path
+ common = URI.commonPath(relativePath, basePath);
+
+ // If the paths have nothing in common, return a relative URL with the absolute path.
+ if (!common) {
+ return relative.build();
+ }
+
+ var parents = baseParts.path
+ .substring(common.length)
+ .replace(/[^\/]*$/, '')
+ .replace(/.*?\//g, '../');
+
+ relativeParts.path = (parents + relativeParts.path.substring(common.length)) || './';
+
+ return relative.build();
+ };
+
+ // comparing URIs
+ p.equals = function(uri) {
+ var one = this.clone();
+ var two = new URI(uri);
+ var one_map = {};
+ var two_map = {};
+ var checked = {};
+ var one_query, two_query, key;
+
+ one.normalize();
+ two.normalize();
+
+ // exact match
+ if (one.toString() === two.toString()) {
+ return true;
+ }
+
+ // extract query string
+ one_query = one.query();
+ two_query = two.query();
+ one.query('');
+ two.query('');
+
+ // definitely not equal if not even non-query parts match
+ if (one.toString() !== two.toString()) {
+ return false;
+ }
+
+ // query parameters have the same length, even if they're permuted
+ if (one_query.length !== two_query.length) {
+ return false;
+ }
+
+ one_map = URI.parseQuery(one_query, this._parts.escapeQuerySpace);
+ two_map = URI.parseQuery(two_query, this._parts.escapeQuerySpace);
+
+ for (key in one_map) {
+ if (hasOwn.call(one_map, key)) {
+ if (!isArray(one_map[key])) {
+ if (one_map[key] !== two_map[key]) {
+ return false;
+ }
+ } else if (!arraysEqual(one_map[key], two_map[key])) {
+ return false;
+ }
+
+ checked[key] = true;
+ }
+ }
+
+ for (key in two_map) {
+ if (hasOwn.call(two_map, key)) {
+ if (!checked[key]) {
+ // two contains a parameter not present in one
+ return false;
+ }
+ }
+ }
+
+ return true;
+ };
+
+ // state
+ p.duplicateQueryParameters = function(v) {
+ this._parts.duplicateQueryParameters = !!v;
+ return this;
+ };
+
+ p.escapeQuerySpace = function(v) {
+ this._parts.escapeQuerySpace = !!v;
+ return this;
+ };
+
+ return URI;
+}));
+
+},{"./IPv6":1,"./SecondLevelDomains":2,"./punycode":4}],4:[function(require,module,exports){
+(function (global){
+/*! http://mths.be/punycode v1.2.3 by @mathias */
+;(function(root) {
+
+ /** Detect free variables */
+ var freeExports = typeof exports == 'object' && exports;
+ var freeModule = typeof module == 'object' && module &&
+ module.exports == freeExports && module;
+ var freeGlobal = typeof global == 'object' && global;
+ if (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal) {
+ root = freeGlobal;
+ }
+
+ /**
+ * The `punycode` object.
+ * @name punycode
+ * @type Object
+ */
+ var punycode,
+
+ /** Highest positive signed 32-bit float value */
+ maxInt = 2147483647, // aka. 0x7FFFFFFF or 2^31-1
+
+ /** Bootstring parameters */
+ base = 36,
+ tMin = 1,
+ tMax = 26,
+ skew = 38,
+ damp = 700,
+ initialBias = 72,
+ initialN = 128, // 0x80
+ delimiter = '-', // '\x2D'
+
+ /** Regular expressions */
+ regexPunycode = /^xn--/,
+ regexNonASCII = /[^ -~]/, // unprintable ASCII chars + non-ASCII chars
+ regexSeparators = /\x2E|\u3002|\uFF0E|\uFF61/g, // RFC 3490 separators
+
+ /** Error messages */
+ errors = {
+ 'overflow': 'Overflow: input needs wider integers to process',
+ 'not-basic': 'Illegal input >= 0x80 (not a basic code point)',
+ 'invalid-input': 'Invalid input'
+ },
+
+ /** Convenience shortcuts */
+ baseMinusTMin = base - tMin,
+ floor = Math.floor,
+ stringFromCharCode = String.fromCharCode,
+
+ /** Temporary variable */
+ key;
+
+ /*--------------------------------------------------------------------------*/
+
+ /**
+ * A generic error utility function.
+ * @private
+ * @param {String} type The error type.
+ * @returns {Error} Throws a `RangeError` with the applicable error message.
+ */
+ function error(type) {
+ throw RangeError(errors[type]);
+ }
+
+ /**
+ * A generic `Array#map` utility function.
+ * @private
+ * @param {Array} array The array to iterate over.
+ * @param {Function} callback The function that gets called for every array
+ * item.
+ * @returns {Array} A new array of values returned by the callback function.
+ */
+ function map(array, fn) {
+ var length = array.length;
+ while (length--) {
+ array[length] = fn(array[length]);
+ }
+ return array;
+ }
+
+ /**
+ * A simple `Array#map`-like wrapper to work with domain name strings.
+ * @private
+ * @param {String} domain The domain name.
+ * @param {Function} callback The function that gets called for every
+ * character.
+ * @returns {Array} A new string of characters returned by the callback
+ * function.
+ */
+ function mapDomain(string, fn) {
+ return map(string.split(regexSeparators), fn).join('.');
+ }
+
+ /**
+ * Creates an array containing the numeric code points of each Unicode
+ * character in the string. While JavaScript uses UCS-2 internally,
+ * this function will convert a pair of surrogate halves (each of which
+ * UCS-2 exposes as separate characters) into a single code point,
+ * matching UTF-16.
+ * @see `punycode.ucs2.encode`
+ * @see <http://mathiasbynens.be/notes/javascript-encoding>
+ * @memberOf punycode.ucs2
+ * @name decode
+ * @param {String} string The Unicode input string (UCS-2).
+ * @returns {Array} The new array of code points.
+ */
+ function ucs2decode(string) {
+ var output = [],
+ counter = 0,
+ length = string.length,
+ value,
+ extra;
+ while (counter < length) {
+ value = string.charCodeAt(counter++);
+ if (value >= 0xD800 && value <= 0xDBFF && counter < length) {
+ // high surrogate, and there is a next character
+ extra = string.charCodeAt(counter++);
+ if ((extra & 0xFC00) == 0xDC00) { // low surrogate
+ output.push(((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000);
+ } else {
+ // unmatched surrogate; only append this code unit, in case the next
+ // code unit is the high surrogate of a surrogate pair
+ output.push(value);
+ counter--;
+ }
+ } else {
+ output.push(value);
+ }
+ }
+ return output;
+ }
+
+ /**
+ * Creates a string based on an array of numeric code points.
+ * @see `punycode.ucs2.decode`
+ * @memberOf punycode.ucs2
+ * @name encode
+ * @param {Array} codePoints The array of numeric code points.
+ * @returns {String} The new Unicode string (UCS-2).
+ */
+ function ucs2encode(array) {
+ return map(array, function(value) {
+ var output = '';
+ if (value > 0xFFFF) {
+ value -= 0x10000;
+ output += stringFromCharCode(value >>> 10 & 0x3FF | 0xD800);
+ value = 0xDC00 | value & 0x3FF;
+ }
+ output += stringFromCharCode(value);
+ return output;
+ }).join('');
+ }
+
+ /**
+ * Converts a basic code point into a digit/integer.
+ * @see `digitToBasic()`
+ * @private
+ * @param {Number} codePoint The basic numeric code point value.
+ * @returns {Number} The numeric value of a basic code point (for use in
+ * representing integers) in the range `0` to `base - 1`, or `base` if
+ * the code point does not represent a value.
+ */
+ function basicToDigit(codePoint) {
+ if (codePoint - 48 < 10) {
+ return codePoint - 22;
+ }
+ if (codePoint - 65 < 26) {
+ return codePoint - 65;
+ }
+ if (codePoint - 97 < 26) {
+ return codePoint - 97;
+ }
+ return base;
+ }
+
+ /**
+ * Converts a digit/integer into a basic code point.
+ * @see `basicToDigit()`
+ * @private
+ * @param {Number} digit The numeric value of a basic code point.
+ * @returns {Number} The basic code point whose value (when used for
+ * representing integers) is `digit`, which needs to be in the range
+ * `0` to `base - 1`. If `flag` is non-zero, the uppercase form is
+ * used; else, the lowercase form is used. The behavior is undefined
+ * if `flag` is non-zero and `digit` has no uppercase form.
+ */
+ function digitToBasic(digit, flag) {
+ // 0..25 map to ASCII a..z or A..Z
+ // 26..35 map to ASCII 0..9
+ return digit + 22 + 75 * (digit < 26) - ((flag != 0) << 5);
+ }
+
+ /**
+ * Bias adaptation function as per section 3.4 of RFC 3492.
+ * http://tools.ietf.org/html/rfc3492#section-3.4
+ * @private
+ */
+ function adapt(delta, numPoints, firstTime) {
+ var k = 0;
+ delta = firstTime ? floor(delta / damp) : delta >> 1;
+ delta += floor(delta / numPoints);
+ for (/* no initialization */; delta > baseMinusTMin * tMax >> 1; k += base) {
+ delta = floor(delta / baseMinusTMin);
+ }
+ return floor(k + (baseMinusTMin + 1) * delta / (delta + skew));
+ }
+
+ /**
+ * Converts a Punycode string of ASCII-only symbols to a string of Unicode
+ * symbols.
+ * @memberOf punycode
+ * @param {String} input The Punycode string of ASCII-only symbols.
+ * @returns {String} The resulting string of Unicode symbols.
+ */
+ function decode(input) {
+ // Don't use UCS-2
+ var output = [],
+ inputLength = input.length,
+ out,
+ i = 0,
+ n = initialN,
+ bias = initialBias,
+ basic,
+ j,
+ index,
+ oldi,
+ w,
+ k,
+ digit,
+ t,
+ length,
+ /** Cached calculation results */
+ baseMinusT;
+
+ // Handle the basic code points: let `basic` be the number of input code
+ // points before the last delimiter, or `0` if there is none, then copy
+ // the first basic code points to the output.
+
+ basic = input.lastIndexOf(delimiter);
+ if (basic < 0) {
+ basic = 0;
+ }
+
+ for (j = 0; j < basic; ++j) {
+ // if it's not a basic code point
+ if (input.charCodeAt(j) >= 0x80) {
+ error('not-basic');
+ }
+ output.push(input.charCodeAt(j));
+ }
+
+ // Main decoding loop: start just after the last delimiter if any basic code
+ // points were copied; start at the beginning otherwise.
+
+ for (index = basic > 0 ? basic + 1 : 0; index < inputLength; /* no final expression */) {
+
+ // `index` is the index of the next character to be consumed.
+ // Decode a generalized variable-length integer into `delta`,
+ // which gets added to `i`. The overflow checking is easier
+ // if we increase `i` as we go, then subtract off its starting
+ // value at the end to obtain `delta`.
+ for (oldi = i, w = 1, k = base; /* no condition */; k += base) {
+
+ if (index >= inputLength) {
+ error('invalid-input');
+ }
+
+ digit = basicToDigit(input.charCodeAt(index++));
+
+ if (digit >= base || digit > floor((maxInt - i) / w)) {
+ error('overflow');
+ }
+
+ i += digit * w;
+ t = k <= bias ? tMin : (k >= bias + tMax ? tMax : k - bias);
+
+ if (digit < t) {
+ break;
+ }
+
+ baseMinusT = base - t;
+ if (w > floor(maxInt / baseMinusT)) {
+ error('overflow');
+ }
+
+ w *= baseMinusT;
+
+ }
+
+ out = output.length + 1;
+ bias = adapt(i - oldi, out, oldi == 0);
+
+ // `i` was supposed to wrap around from `out` to `0`,
+ // incrementing `n` each time, so we'll fix that now:
+ if (floor(i / out) > maxInt - n) {
+ error('overflow');
+ }
+
+ n += floor(i / out);
+ i %= out;
+
+ // Insert `n` at position `i` of the output
+ output.splice(i++, 0, n);
+
+ }
+
+ return ucs2encode(output);
+ }
+
+ /**
+ * Converts a string of Unicode symbols to a Punycode string of ASCII-only
+ * symbols.
+ * @memberOf punycode
+ * @param {String} input The string of Unicode symbols.
+ * @returns {String} The resulting Punycode string of ASCII-only symbols.
+ */
+ function encode(input) {
+ var n,
+ delta,
+ handledCPCount,
+ basicLength,
+ bias,
+ j,
+ m,
+ q,
+ k,
+ t,
+ currentValue,
+ output = [],
+ /** `inputLength` will hold the number of code points in `input`. */
+ inputLength,
+ /** Cached calculation results */
+ handledCPCountPlusOne,
+ baseMinusT,
+ qMinusT;
+
+ // Convert the input in UCS-2 to Unicode
+ input = ucs2decode(input);
+
+ // Cache the length
+ inputLength = input.length;
+
+ // Initialize the state
+ n = initialN;
+ delta = 0;
+ bias = initialBias;
+
+ // Handle the basic code points
+ for (j = 0; j < inputLength; ++j) {
+ currentValue = input[j];
+ if (currentValue < 0x80) {
+ output.push(stringFromCharCode(currentValue));
+ }
+ }
+
+ handledCPCount = basicLength = output.length;
+
+ // `handledCPCount` is the number of code points that have been handled;
+ // `basicLength` is the number of basic code points.
+
+ // Finish the basic string - if it is not empty - with a delimiter
+ if (basicLength) {
+ output.push(delimiter);
+ }
+
+ // Main encoding loop:
+ while (handledCPCount < inputLength) {
+
+ // All non-basic code points < n have been handled already. Find the next
+ // larger one:
+ for (m = maxInt, j = 0; j < inputLength; ++j) {
+ currentValue = input[j];
+ if (currentValue >= n && currentValue < m) {
+ m = currentValue;
+ }
+ }
+
+ // Increase `delta` enough to advance the decoder's <n,i> state to <m,0>,
+ // but guard against overflow
+ handledCPCountPlusOne = handledCPCount + 1;
+ if (m - n > floor((maxInt - delta) / handledCPCountPlusOne)) {
+ error('overflow');
+ }
+
+ delta += (m - n) * handledCPCountPlusOne;
+ n = m;
+
+ for (j = 0; j < inputLength; ++j) {
+ currentValue = input[j];
+
+ if (currentValue < n && ++delta > maxInt) {
+ error('overflow');
+ }
+
+ if (currentValue == n) {
+ // Represent delta as a generalized variable-length integer
+ for (q = delta, k = base; /* no condition */; k += base) {
+ t = k <= bias ? tMin : (k >= bias + tMax ? tMax : k - bias);
+ if (q < t) {
+ break;
+ }
+ qMinusT = q - t;
+ baseMinusT = base - t;
+ output.push(
+ stringFromCharCode(digitToBasic(t + qMinusT % baseMinusT, 0))
+ );
+ q = floor(qMinusT / baseMinusT);
+ }
+
+ output.push(stringFromCharCode(digitToBasic(q, 0)));
+ bias = adapt(delta, handledCPCountPlusOne, handledCPCount == basicLength);
+ delta = 0;
+ ++handledCPCount;
+ }
+ }
+
+ ++delta;
+ ++n;
+
+ }
+ return output.join('');
+ }
+
+ /**
+ * Converts a Punycode string representing a domain name to Unicode. Only the
+ * Punycoded parts of the domain name will be converted, i.e. it doesn't
+ * matter if you call it on a string that has already been converted to
+ * Unicode.
+ * @memberOf punycode
+ * @param {String} domain The Punycode domain name to convert to Unicode.
+ * @returns {String} The Unicode representation of the given Punycode
+ * string.
+ */
+ function toUnicode(domain) {
+ return mapDomain(domain, function(string) {
+ return regexPunycode.test(string)
+ ? decode(string.slice(4).toLowerCase())
+ : string;
+ });
+ }
+
+ /**
+ * Converts a Unicode string representing a domain name to Punycode. Only the
+ * non-ASCII parts of the domain name will be converted, i.e. it doesn't
+ * matter if you call it with a domain that's already in ASCII.
+ * @memberOf punycode
+ * @param {String} domain The domain name to convert, as a Unicode string.
+ * @returns {String} The Punycode representation of the given domain name.
+ */
+ function toASCII(domain) {
+ return mapDomain(domain, function(string) {
+ return regexNonASCII.test(string)
+ ? 'xn--' + encode(string)
+ : string;
+ });
+ }
+
+ /*--------------------------------------------------------------------------*/
+
+ /** Define the public API */
+ punycode = {
+ /**
+ * A string representing the current Punycode.js version number.
+ * @memberOf punycode
+ * @type String
+ */
+ 'version': '1.2.3',
+ /**
+ * An object of methods to convert from JavaScript's internal character
+ * representation (UCS-2) to Unicode code points, and back.
+ * @see <http://mathiasbynens.be/notes/javascript-encoding>
+ * @memberOf punycode
+ * @type Object
+ */
+ 'ucs2': {
+ 'decode': ucs2decode,
+ 'encode': ucs2encode
+ },
+ 'decode': decode,
+ 'encode': encode,
+ 'toASCII': toASCII,
+ 'toUnicode': toUnicode
+ };
+
+ /** Expose `punycode` */
+ // Some AMD build optimizers, like r.js, check for specific condition patterns
+ // like the following:
+ if (
+ typeof define == 'function' &&
+ typeof define.amd == 'object' &&
+ define.amd
+ ) {
+ define(function() {
+ return punycode;
+ });
+ } else if (freeExports && !freeExports.nodeType) {
+ if (freeModule) { // in Node.js or RingoJS v0.8.0+
+ freeModule.exports = punycode;
+ } else { // in Narwhal or RingoJS v0.7.0-
+ for (key in punycode) {
+ punycode.hasOwnProperty(key) && (freeExports[key] = punycode[key]);
+ }
+ }
+ } else { // in Rhino or a web browser
+ root.punycode = punycode;
+ }
+
+}(this));
+
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+
+},{}],5:[function(require,module,exports){
+'use strict';
+
+Object.defineProperty(exports, '__esModule', {
+ value: true
+});
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+var _helpersEvent = require('./helpers/event');
+
+var _helpersEvent2 = _interopRequireDefault(_helpersEvent);
+
+var _helpersMessageEvent = require('./helpers/message-event');
+
+var _helpersMessageEvent2 = _interopRequireDefault(_helpersMessageEvent);
+
+var _helpersCloseEvent = require('./helpers/close-event');
+
+var _helpersCloseEvent2 = _interopRequireDefault(_helpersCloseEvent);
+
+/*
+* Creates an Event object and extends it to allow full modification of
+* its properties.
+*
+* @param {object} config - within config you will need to pass type and optionally target
+*/
+function createEvent(config) {
+ var type = config.type;
+ var target = config.target;
+
+ var eventObject = new _helpersEvent2['default'](type);
+
+ if (target) {
+ eventObject.target = target;
+ eventObject.srcElement = target;
+ eventObject.currentTarget = target;
+ }
+
+ return eventObject;
+}
+
+/*
+* Creates a MessageEvent object and extends it to allow full modification of
+* its properties.
+*
+* @param {object} config - within config you will need to pass type, origin, data and optionally target
+*/
+function createMessageEvent(config) {
+ var type = config.type;
+ var origin = config.origin;
+ var data = config.data;
+ var target = config.target;
+
+ var messageEvent = new _helpersMessageEvent2['default'](type, {
+ data: data,
+ origin: origin
+ });
+
+ if (target) {
+ messageEvent.target = target;
+ messageEvent.srcElement = target;
+ messageEvent.currentTarget = target;
+ }
+
+ return messageEvent;
+}
+
+/*
+* Creates a CloseEvent object and extends it to allow full modification of
+* its properties.
+*
+* @param {object} config - within config you will need to pass type and optionally target, code, and reason
+*/
+function createCloseEvent(config) {
+ var code = config.code;
+ var reason = config.reason;
+ var type = config.type;
+ var target = config.target;
+ var wasClean = config.wasClean;
+
+ if (!wasClean) {
+ wasClean = code === 1000;
+ }
+
+ var closeEvent = new _helpersCloseEvent2['default'](type, {
+ code: code,
+ reason: reason,
+ wasClean: wasClean
+ });
+
+ if (target) {
+ closeEvent.target = target;
+ closeEvent.srcElement = target;
+ closeEvent.currentTarget = target;
+ }
+
+ return closeEvent;
+}
+
+exports.createEvent = createEvent;
+exports.createMessageEvent = createMessageEvent;
+exports.createCloseEvent = createCloseEvent;
+},{"./helpers/close-event":9,"./helpers/event":12,"./helpers/message-event":13}],6:[function(require,module,exports){
+'use strict';
+
+Object.defineProperty(exports, '__esModule', {
+ value: true
+});
+
+var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
+
+var _helpersArrayHelpers = require('./helpers/array-helpers');
+
+/*
+* EventTarget is an interface implemented by objects that can
+* receive events and may have listeners for them.
+*
+* https://developer.mozilla.org/en-US/docs/Web/API/EventTarget
+*/
+
+var EventTarget = (function () {
+ function EventTarget() {
+ _classCallCheck(this, EventTarget);
+
+ this.listeners = {};
+ }
+
+ /*
+ * Ties a listener function to a event type which can later be invoked via the
+ * dispatchEvent method.
+ *
+ * @param {string} type - the type of event (ie: 'open', 'message', etc.)
+ * @param {function} listener - the callback function to invoke whenever a event is dispatched matching the given type
+ * @param {boolean} useCapture - N/A TODO: implement useCapture functionality
+ */
+
+ _createClass(EventTarget, [{
+ key: 'addEventListener',
+ value: function addEventListener(type, listener /* , useCapture */) {
+ if (typeof listener === 'function') {
+ if (!Array.isArray(this.listeners[type])) {
+ this.listeners[type] = [];
+ }
+
+ // Only add the same function once
+ if ((0, _helpersArrayHelpers.filter)(this.listeners[type], function (item) {
+ return item === listener;
+ }).length === 0) {
+ this.listeners[type].push(listener);
+ }
+ }
+ }
+
+ /*
+ * Removes the listener so it will no longer be invoked via the dispatchEvent method.
+ *
+ * @param {string} type - the type of event (ie: 'open', 'message', etc.)
+ * @param {function} listener - the callback function to invoke whenever a event is dispatched matching the given type
+ * @param {boolean} useCapture - N/A TODO: implement useCapture functionality
+ */
+ }, {
+ key: 'removeEventListener',
+ value: function removeEventListener(type, removingListener /* , useCapture */) {
+ var arrayOfListeners = this.listeners[type];
+ this.listeners[type] = (0, _helpersArrayHelpers.reject)(arrayOfListeners, function (listener) {
+ return listener === removingListener;
+ });
+ }
+
+ /*
+ * Invokes all listener functions that are listening to the given event.type property. Each
+ * listener will be passed the event as the first argument.
+ *
+ * @param {object} event - event object which will be passed to all listeners of the event.type property
+ */
+ }, {
+ key: 'dispatchEvent',
+ value: function dispatchEvent(event) {
+ var _this = this;
+
+ for (var _len = arguments.length, customArguments = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
+ customArguments[_key - 1] = arguments[_key];
+ }
+
+ var eventName = event.type;
+ var listeners = this.listeners[eventName];
+
+ if (!Array.isArray(listeners)) {
+ return false;
+ }
+
+ listeners.forEach(function (listener) {
+ if (customArguments.length > 0) {
+ listener.apply(_this, customArguments);
+ } else {
+ listener.call(_this, event);
+ }
+ });
+ }
+ }]);
+
+ return EventTarget;
+})();
+
+exports['default'] = EventTarget;
+module.exports = exports['default'];
+},{"./helpers/array-helpers":7}],7:[function(require,module,exports){
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.reject = reject;
+exports.filter = filter;
+
+function reject(array, callback) {
+ var results = [];
+ array.forEach(function (itemInArray) {
+ if (!callback(itemInArray)) {
+ results.push(itemInArray);
+ }
+ });
+
+ return results;
+}
+
+function filter(array, callback) {
+ var results = [];
+ array.forEach(function (itemInArray) {
+ if (callback(itemInArray)) {
+ results.push(itemInArray);
+ }
+ });
+
+ return results;
+}
+},{}],8:[function(require,module,exports){
+/*
+* https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
+*/
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+var codes = {
+ CLOSE_NORMAL: 1000,
+ CLOSE_GOING_AWAY: 1001,
+ CLOSE_PROTOCOL_ERROR: 1002,
+ CLOSE_UNSUPPORTED: 1003,
+ CLOSE_NO_STATUS: 1005,
+ CLOSE_ABNORMAL: 1006,
+ CLOSE_TOO_LARGE: 1009
+};
+
+exports["default"] = codes;
+module.exports = exports["default"];
+},{}],9:[function(require,module,exports){
+'use strict';
+
+Object.defineProperty(exports, '__esModule', {
+ value: true
+});
+
+var _get = function get(_x2, _x3, _x4) { var _again = true; _function: while (_again) { var object = _x2, property = _x3, receiver = _x4; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x2 = parent; _x3 = property; _x4 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } };
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
+
+function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
+
+var _eventPrototype = require('./event-prototype');
+
+var _eventPrototype2 = _interopRequireDefault(_eventPrototype);
+
+var CloseEvent = (function (_EventPrototype) {
+ _inherits(CloseEvent, _EventPrototype);
+
+ function CloseEvent(type) {
+ var eventInitConfig = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];
+
+ _classCallCheck(this, CloseEvent);
+
+ _get(Object.getPrototypeOf(CloseEvent.prototype), 'constructor', this).call(this);
+
+ if (!type) {
+ throw new TypeError('Failed to construct \'CloseEvent\': 1 argument required, but only 0 present.');
+ }
+
+ if (typeof eventInitConfig !== 'object') {
+ throw new TypeError('Failed to construct \'CloseEvent\': parameter 2 (\'eventInitDict\') is not an object');
+ }
+
+ var bubbles = eventInitConfig.bubbles;
+ var cancelable = eventInitConfig.cancelable;
+ var code = eventInitConfig.code;
+ var reason = eventInitConfig.reason;
+ var wasClean = eventInitConfig.wasClean;
+
+ this.type = String(type);
+ this.timeStamp = Date.now();
+ this.target = null;
+ this.srcElement = null;
+ this.returnValue = true;
+ this.isTrusted = false;
+ this.eventPhase = 0;
+ this.defaultPrevented = false;
+ this.currentTarget = null;
+ this.cancelable = cancelable ? Boolean(cancelable) : false;
+ this.canncelBubble = false;
+ this.bubbles = bubbles ? Boolean(bubbles) : false;
+ this.code = typeof code === 'number' ? Number(code) : 0;
+ this.reason = reason ? String(reason) : '';
+ this.wasClean = wasClean ? Boolean(wasClean) : false;
+ }
+
+ return CloseEvent;
+})(_eventPrototype2['default']);
+
+exports['default'] = CloseEvent;
+module.exports = exports['default'];
+},{"./event-prototype":11}],10:[function(require,module,exports){
+/*
+* This delay allows the thread to finish assigning its on* methods
+* before invoking the delay callback. This is purely a timing hack.
+* http://geekabyte.blogspot.com/2014/01/javascript-effect-of-setting-settimeout.html
+*
+* @param {callback: function} the callback which will be invoked after the timeout
+* @parma {context: object} the context in which to invoke the function
+*/
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+function delay(callback, context) {
+ setTimeout(function timeout(timeoutContext) {
+ callback.call(timeoutContext);
+ }, 4, context);
+}
+
+exports["default"] = delay;
+module.exports = exports["default"];
+},{}],11:[function(require,module,exports){
+'use strict';
+
+Object.defineProperty(exports, '__esModule', {
+ value: true
+});
+
+var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
+
+var EventPrototype = (function () {
+ function EventPrototype() {
+ _classCallCheck(this, EventPrototype);
+ }
+
+ _createClass(EventPrototype, [{
+ key: 'stopPropagation',
+
+ // Noops
+ value: function stopPropagation() {}
+ }, {
+ key: 'stopImmediatePropagation',
+ value: function stopImmediatePropagation() {}
+
+ // if no arguments are passed then the type is set to "undefined" on
+ // chrome and safari.
+ }, {
+ key: 'initEvent',
+ value: function initEvent() {
+ var type = arguments.length <= 0 || arguments[0] === undefined ? 'undefined' : arguments[0];
+ var bubbles = arguments.length <= 1 || arguments[1] === undefined ? false : arguments[1];
+ var cancelable = arguments.length <= 2 || arguments[2] === undefined ? false : arguments[2];
+
+ Object.assign(this, {
+ type: String(type),
+ bubbles: Boolean(bubbles),
+ cancelable: Boolean(cancelable)
+ });
+ }
+ }]);
+
+ return EventPrototype;
+})();
+
+exports['default'] = EventPrototype;
+module.exports = exports['default'];
+},{}],12:[function(require,module,exports){
+'use strict';
+
+Object.defineProperty(exports, '__esModule', {
+ value: true
+});
+
+var _get = function get(_x2, _x3, _x4) { var _again = true; _function: while (_again) { var object = _x2, property = _x3, receiver = _x4; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x2 = parent; _x3 = property; _x4 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } };
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
+
+function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
+
+var _eventPrototype = require('./event-prototype');
+
+var _eventPrototype2 = _interopRequireDefault(_eventPrototype);
+
+var Event = (function (_EventPrototype) {
+ _inherits(Event, _EventPrototype);
+
+ function Event(type) {
+ var eventInitConfig = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];
+
+ _classCallCheck(this, Event);
+
+ _get(Object.getPrototypeOf(Event.prototype), 'constructor', this).call(this);
+
+ if (!type) {
+ throw new TypeError('Failed to construct \'Event\': 1 argument required, but only 0 present.');
+ }
+
+ if (typeof eventInitConfig !== 'object') {
+ throw new TypeError('Failed to construct \'Event\': parameter 2 (\'eventInitDict\') is not an object');
+ }
+
+ var bubbles = eventInitConfig.bubbles;
+ var cancelable = eventInitConfig.cancelable;
+
+ this.type = String(type);
+ this.timeStamp = Date.now();
+ this.target = null;
+ this.srcElement = null;
+ this.returnValue = true;
+ this.isTrusted = false;
+ this.eventPhase = 0;
+ this.defaultPrevented = false;
+ this.currentTarget = null;
+ this.cancelable = cancelable ? Boolean(cancelable) : false;
+ this.canncelBubble = false;
+ this.bubbles = bubbles ? Boolean(bubbles) : false;
+ }
+
+ return Event;
+})(_eventPrototype2['default']);
+
+exports['default'] = Event;
+module.exports = exports['default'];
+},{"./event-prototype":11}],13:[function(require,module,exports){
+'use strict';
+
+Object.defineProperty(exports, '__esModule', {
+ value: true
+});
+
+var _get = function get(_x2, _x3, _x4) { var _again = true; _function: while (_again) { var object = _x2, property = _x3, receiver = _x4; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x2 = parent; _x3 = property; _x4 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } };
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
+
+function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
+
+var _eventPrototype = require('./event-prototype');
+
+var _eventPrototype2 = _interopRequireDefault(_eventPrototype);
+
+var MessageEvent = (function (_EventPrototype) {
+ _inherits(MessageEvent, _EventPrototype);
+
+ function MessageEvent(type) {
+ var eventInitConfig = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];
+
+ _classCallCheck(this, MessageEvent);
+
+ _get(Object.getPrototypeOf(MessageEvent.prototype), 'constructor', this).call(this);
+
+ if (!type) {
+ throw new TypeError('Failed to construct \'MessageEvent\': 1 argument required, but only 0 present.');
+ }
+
+ if (typeof eventInitConfig !== 'object') {
+ throw new TypeError('Failed to construct \'MessageEvent\': parameter 2 (\'eventInitDict\') is not an object');
+ }
+
+ var bubbles = eventInitConfig.bubbles;
+ var cancelable = eventInitConfig.cancelable;
+ var data = eventInitConfig.data;
+ var origin = eventInitConfig.origin;
+ var lastEventId = eventInitConfig.lastEventId;
+ var ports = eventInitConfig.ports;
+
+ this.type = String(type);
+ this.timeStamp = Date.now();
+ this.target = null;
+ this.srcElement = null;
+ this.returnValue = true;
+ this.isTrusted = false;
+ this.eventPhase = 0;
+ this.defaultPrevented = false;
+ this.currentTarget = null;
+ this.cancelable = cancelable ? Boolean(cancelable) : false;
+ this.canncelBubble = false;
+ this.bubbles = bubbles ? Boolean(bubbles) : false;
+ this.origin = origin ? String(origin) : '';
+ this.ports = typeof ports === 'undefined' ? null : ports;
+ this.data = typeof data === 'undefined' ? null : data;
+ this.lastEventId = lastEventId ? String(lastEventId) : '';
+ }
+
+ return MessageEvent;
+})(_eventPrototype2['default']);
+
+exports['default'] = MessageEvent;
+module.exports = exports['default'];
+},{"./event-prototype":11}],14:[function(require,module,exports){
+'use strict';
+
+Object.defineProperty(exports, '__esModule', {
+ value: true
+});
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+var _server = require('./server');
+
+var _server2 = _interopRequireDefault(_server);
+
+var _socketIo = require('./socket-io');
+
+var _socketIo2 = _interopRequireDefault(_socketIo);
+
+var _websocket = require('./websocket');
+
+var _websocket2 = _interopRequireDefault(_websocket);
+
+if (typeof window !== 'undefined') {
+ window.MockServer = _server2['default'];
+ window.MockWebSocket = _websocket2['default'];
+ window.MockSocketIO = _socketIo2['default'];
+}
+
+var Server = _server2['default'];
+exports.Server = Server;
+var WebSocket = _websocket2['default'];
+exports.WebSocket = WebSocket;
+var SocketIO = _socketIo2['default'];
+exports.SocketIO = SocketIO;
+},{"./server":16,"./socket-io":17,"./websocket":18}],15:[function(require,module,exports){
+'use strict';
+
+Object.defineProperty(exports, '__esModule', {
+ value: true
+});
+
+var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
+
+var _helpersArrayHelpers = require('./helpers/array-helpers');
+
+/*
+* The network bridge is a way for the mock websocket object to 'communicate' with
+* all avalible servers. This is a singleton object so it is important that you
+* clean up urlMap whenever you are finished.
+*/
+
+var NetworkBridge = (function () {
+ function NetworkBridge() {
+ _classCallCheck(this, NetworkBridge);
+
+ this.urlMap = {};
+ }
+
+ /*
+ * Attaches a websocket object to the urlMap hash so that it can find the server
+ * it is connected to and the server in turn can find it.
+ *
+ * @param {object} websocket - websocket object to add to the urlMap hash
+ * @param {string} url
+ */
+
+ _createClass(NetworkBridge, [{
+ key: 'attachWebSocket',
+ value: function attachWebSocket(websocket, url) {
+ var connectionLookup = this.urlMap[url];
+
+ if (connectionLookup && connectionLookup.server && connectionLookup.websockets.indexOf(websocket) === -1) {
+ connectionLookup.websockets.push(websocket);
+ return connectionLookup.server;
+ }
+ }
+
+ /*
+ * Attaches a websocket to a room
+ */
+ }, {
+ key: 'addMembershipToRoom',
+ value: function addMembershipToRoom(websocket, room) {
+ var connectionLookup = this.urlMap[websocket.url];
+
+ if (connectionLookup && connectionLookup.server && connectionLookup.websockets.indexOf(websocket) !== -1) {
+ if (!connectionLookup.roomMemberships[room]) {
+ connectionLookup.roomMemberships[room] = [];
+ }
+
+ connectionLookup.roomMemberships[room].push(websocket);
+ }
+ }
+
+ /*
+ * Attaches a server object to the urlMap hash so that it can find a websockets
+ * which are connected to it and so that websockets can in turn can find it.
+ *
+ * @param {object} server - server object to add to the urlMap hash
+ * @param {string} url
+ */
+ }, {
+ key: 'attachServer',
+ value: function attachServer(server, url) {
+ var connectionLookup = this.urlMap[url];
+
+ if (!connectionLookup) {
+ this.urlMap[url] = {
+ server: server,
+ websockets: [],
+ roomMemberships: {}
+ };
+
+ return server;
+ }
+ }
+
+ /*
+ * Finds the server which is 'running' on the given url.
+ *
+ * @param {string} url - the url to use to find which server is running on it
+ */
+ }, {
+ key: 'serverLookup',
+ value: function serverLookup(url) {
+ var connectionLookup = this.urlMap[url];
+
+ if (connectionLookup) {
+ return connectionLookup.server;
+ }
+ }
+
+ /*
+ * Finds all websockets which is 'listening' on the given url.
+ *
+ * @param {string} url - the url to use to find all websockets which are associated with it
+ * @param {string} room - if a room is provided, will only return sockets in this room
+ */
+ }, {
+ key: 'websocketsLookup',
+ value: function websocketsLookup(url, room) {
+ var connectionLookup = this.urlMap[url];
+
+ if (!connectionLookup) {
+ return [];
+ }
+
+ if (room) {
+ var members = connectionLookup.roomMemberships[room];
+ return members ? members : [];
+ }
+
+ return connectionLookup.websockets;
+ }
+
+ /*
+ * Removes the entry associated with the url.
+ *
+ * @param {string} url
+ */
+ }, {
+ key: 'removeServer',
+ value: function removeServer(url) {
+ delete this.urlMap[url];
+ }
+
+ /*
+ * Removes the individual websocket from the map of associated websockets.
+ *
+ * @param {object} websocket - websocket object to remove from the url map
+ * @param {string} url
+ */
+ }, {
+ key: 'removeWebSocket',
+ value: function removeWebSocket(websocket, url) {
+ var connectionLookup = this.urlMap[url];
+
+ if (connectionLookup) {
+ connectionLookup.websockets = (0, _helpersArrayHelpers.reject)(connectionLookup.websockets, function (socket) {
+ return socket === websocket;
+ });
+ }
+ }
+
+ /*
+ * Removes a websocket from a room
+ */
+ }, {
+ key: 'removeMembershipFromRoom',
+ value: function removeMembershipFromRoom(websocket, room) {
+ var connectionLookup = this.urlMap[websocket.url];
+ var memberships = connectionLookup.roomMemberships[room];
+
+ if (connectionLookup && memberships !== null) {
+ connectionLookup.roomMemberships[room] = (0, _helpersArrayHelpers.reject)(memberships, function (socket) {
+ return socket === websocket;
+ });
+ }
+ }
+ }]);
+
+ return NetworkBridge;
+})();
+
+exports['default'] = new NetworkBridge();
+// Note: this is a singleton
+module.exports = exports['default'];
+},{"./helpers/array-helpers":7}],16:[function(require,module,exports){
+'use strict';
+
+Object.defineProperty(exports, '__esModule', {
+ value: true
+});
+
+var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
+
+var _get = function get(_x4, _x5, _x6) { var _again = true; _function: while (_again) { var object = _x4, property = _x5, receiver = _x6; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x4 = parent; _x5 = property; _x6 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } };
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
+
+function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
+
+var _urijs = require('urijs');
+
+var _urijs2 = _interopRequireDefault(_urijs);
+
+var _websocket = require('./websocket');
+
+var _websocket2 = _interopRequireDefault(_websocket);
+
+var _eventTarget = require('./event-target');
+
+var _eventTarget2 = _interopRequireDefault(_eventTarget);
+
+var _networkBridge = require('./network-bridge');
+
+var _networkBridge2 = _interopRequireDefault(_networkBridge);
+
+var _helpersCloseCodes = require('./helpers/close-codes');
+
+var _helpersCloseCodes2 = _interopRequireDefault(_helpersCloseCodes);
+
+var _eventFactory = require('./event-factory');
+
+/*
+* https://github.com/websockets/ws#server-example
+*/
+
+var Server = (function (_EventTarget) {
+ _inherits(Server, _EventTarget);
+
+ /*
+ * @param {string} url
+ */
+
+ function Server(url) {
+ _classCallCheck(this, Server);
+
+ _get(Object.getPrototypeOf(Server.prototype), 'constructor', this).call(this);
+ this.url = (0, _urijs2['default'])(url).toString();
+ var server = _networkBridge2['default'].attachServer(this, this.url);
+
+ if (!server) {
+ this.dispatchEvent((0, _eventFactory.createEvent)({ type: 'error' }));
+ throw new Error('A mock server is already listening on this url');
+ }
+ }
+
+ /*
+ * Alternative constructor to support namespaces in socket.io
+ *
+ * http://socket.io/docs/rooms-and-namespaces/#custom-namespaces
+ */
+
+ /*
+ * This is the main function for the mock server to subscribe to the on events.
+ *
+ * ie: mockServer.on('connection', function() { console.log('a mock client connected'); });
+ *
+ * @param {string} type - The event key to subscribe to. Valid keys are: connection, message, and close.
+ * @param {function} callback - The callback which should be called when a certain event is fired.
+ */
+
+ _createClass(Server, [{
+ key: 'on',
+ value: function on(type, callback) {
+ this.addEventListener(type, callback);
+ }
+
+ /*
+ * This send function will notify all mock clients via their onmessage callbacks that the server
+ * has a message for them.
+ *
+ * @param {*} data - Any javascript object which will be crafted into a MessageObject.
+ */
+ }, {
+ key: 'send',
+ value: function send(data) {
+ var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];
+
+ this.emit('message', data, options);
+ }
+
+ /*
+ * Sends a generic message event to all mock clients.
+ */
+ }, {
+ key: 'emit',
+ value: function emit(event, data) {
+ var _this2 = this;
+
+ var options = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2];
+ var websockets = options.websockets;
+
+ if (!websockets) {
+ websockets = _networkBridge2['default'].websocketsLookup(this.url);
+ }
+
+ websockets.forEach(function (socket) {
+ socket.dispatchEvent((0, _eventFactory.createMessageEvent)({
+ type: event,
+ data: data,
+ origin: _this2.url,
+ target: socket
+ }));
+ });
+ }
+
+ /*
+ * Closes the connection and triggers the onclose method of all listening
+ * websockets. After that it removes itself from the urlMap so another server
+ * could add itself to the url.
+ *
+ * @param {object} options
+ */
+ }, {
+ key: 'close',
+ value: function close() {
+ var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
+ var code = options.code;
+ var reason = options.reason;
+ var wasClean = options.wasClean;
+
+ var listeners = _networkBridge2['default'].websocketsLookup(this.url);
+
+ listeners.forEach(function (socket) {
+ socket.readyState = _websocket2['default'].CLOSE;
+ socket.dispatchEvent((0, _eventFactory.createCloseEvent)({
+ type: 'close',
+ target: socket,
+ code: code || _helpersCloseCodes2['default'].CLOSE_NORMAL,
+ reason: reason || '',
+ wasClean: wasClean
+ }));
+ });
+
+ this.dispatchEvent((0, _eventFactory.createCloseEvent)({ type: 'close' }), this);
+ _networkBridge2['default'].removeServer(this.url);
+ }
+
+ /*
+ * Returns an array of websockets which are listening to this server
+ */
+ }, {
+ key: 'clients',
+ value: function clients() {
+ return _networkBridge2['default'].websocketsLookup(this.url);
+ }
+
+ /*
+ * Prepares a method to submit an event to members of the room
+ *
+ * e.g. server.to('my-room').emit('hi!');
+ */
+ }, {
+ key: 'to',
+ value: function to(room) {
+ var _this = this;
+ var websockets = _networkBridge2['default'].websocketsLookup(this.url, room);
+ return {
+ emit: function emit(event, data) {
+ _this.emit(event, data, { websockets: websockets });
+ }
+ };
+ }
+ }]);
+
+ return Server;
+})(_eventTarget2['default']);
+
+Server.of = function of(url) {
+ return new Server(url);
+};
+
+exports['default'] = Server;
+module.exports = exports['default'];
+},{"./event-factory":5,"./event-target":6,"./helpers/close-codes":8,"./network-bridge":15,"./websocket":18,"urijs":3}],17:[function(require,module,exports){
+'use strict';
+
+Object.defineProperty(exports, '__esModule', {
+ value: true
+});
+
+var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
+
+var _get = function get(_x3, _x4, _x5) { var _again = true; _function: while (_again) { var object = _x3, property = _x4, receiver = _x5; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x3 = parent; _x4 = property; _x5 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } };
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
+
+function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
+
+var _urijs = require('urijs');
+
+var _urijs2 = _interopRequireDefault(_urijs);
+
+var _helpersDelay = require('./helpers/delay');
+
+var _helpersDelay2 = _interopRequireDefault(_helpersDelay);
+
+var _eventTarget = require('./event-target');
+
+var _eventTarget2 = _interopRequireDefault(_eventTarget);
+
+var _networkBridge = require('./network-bridge');
+
+var _networkBridge2 = _interopRequireDefault(_networkBridge);
+
+var _helpersCloseCodes = require('./helpers/close-codes');
+
+var _helpersCloseCodes2 = _interopRequireDefault(_helpersCloseCodes);
+
+var _eventFactory = require('./event-factory');
+
+/*
+* The socket-io class is designed to mimick the real API as closely as possible.
+*
+* http://socket.io/docs/
+*/
+
+var SocketIO = (function (_EventTarget) {
+ _inherits(SocketIO, _EventTarget);
+
+ /*
+ * @param {string} url
+ */
+
+ function SocketIO() {
+ var _this = this;
+
+ var url = arguments.length <= 0 || arguments[0] === undefined ? 'socket.io' : arguments[0];
+ var protocol = arguments.length <= 1 || arguments[1] === undefined ? '' : arguments[1];
+
+ _classCallCheck(this, SocketIO);
+
+ _get(Object.getPrototypeOf(SocketIO.prototype), 'constructor', this).call(this);
+
+ this.binaryType = 'blob';
+ this.url = (0, _urijs2['default'])(url).toString();
+ this.readyState = SocketIO.CONNECTING;
+ this.protocol = '';
+
+ if (typeof protocol === 'string') {
+ this.protocol = protocol;
+ } else if (Array.isArray(protocol) && protocol.length > 0) {
+ this.protocol = protocol[0];
+ }
+
+ var server = _networkBridge2['default'].attachWebSocket(this, this.url);
+
+ /*
+ * Delay triggering the connection events so they can be defined in time.
+ */
+ (0, _helpersDelay2['default'])(function delayCallback() {
+ if (server) {
+ this.readyState = SocketIO.OPEN;
+ server.dispatchEvent((0, _eventFactory.createEvent)({ type: 'connection' }), server, this);
+ server.dispatchEvent((0, _eventFactory.createEvent)({ type: 'connect' }), server, this); // alias
+ this.dispatchEvent((0, _eventFactory.createEvent)({ type: 'connect', target: this }));
+ } else {
+ this.readyState = SocketIO.CLOSED;
+ this.dispatchEvent((0, _eventFactory.createEvent)({ type: 'error', target: this }));
+ this.dispatchEvent((0, _eventFactory.createCloseEvent)({
+ type: 'close',
+ target: this,
+ code: _helpersCloseCodes2['default'].CLOSE_NORMAL
+ }));
+
+ console.error('Socket.io connection to \'' + this.url + '\' failed');
+ }
+ }, this);
+
+ /**
+ Add an aliased event listener for close / disconnect
+ */
+ this.addEventListener('close', function (event) {
+ _this.dispatchEvent((0, _eventFactory.createCloseEvent)({
+ type: 'disconnect',
+ target: event.target,
+ code: event.code
+ }));
+ });
+ }
+
+ /*
+ * Closes the SocketIO connection or connection attempt, if any.
+ * If the connection is already CLOSED, this method does nothing.
+ */
+
+ _createClass(SocketIO, [{
+ key: 'close',
+ value: function close() {
+ if (this.readyState !== SocketIO.OPEN) {
+ return undefined;
+ }
+
+ var server = _networkBridge2['default'].serverLookup(this.url);
+ _networkBridge2['default'].removeWebSocket(this, this.url);
+
+ this.readyState = SocketIO.CLOSED;
+ this.dispatchEvent((0, _eventFactory.createCloseEvent)({
+ type: 'close',
+ target: this,
+ code: _helpersCloseCodes2['default'].CLOSE_NORMAL
+ }));
+
+ if (server) {
+ server.dispatchEvent((0, _eventFactory.createCloseEvent)({
+ type: 'disconnect',
+ target: this,
+ code: _helpersCloseCodes2['default'].CLOSE_NORMAL
+ }), server);
+ }
+ }
+
+ /*
+ * Alias for Socket#close
+ *
+ * https://github.com/socketio/socket.io-client/blob/master/lib/socket.js#L383
+ */
+ }, {
+ key: 'disconnect',
+ value: function disconnect() {
+ this.close();
+ }
+
+ /*
+ * Submits an event to the server with a payload
+ */
+ }, {
+ key: 'emit',
+ value: function emit(event, data) {
+ if (this.readyState !== SocketIO.OPEN) {
+ throw new Error('SocketIO is already in CLOSING or CLOSED state');
+ }
+
+ var messageEvent = (0, _eventFactory.createMessageEvent)({
+ type: event,
+ origin: this.url,
+ data: data
+ });
+
+ var server = _networkBridge2['default'].serverLookup(this.url);
+
+ if (server) {
+ server.dispatchEvent(messageEvent, data);
+ }
+ }
+
+ /*
+ * Submits a 'message' event to the server.
+ *
+ * Should behave exactly like WebSocket#send
+ *
+ * https://github.com/socketio/socket.io-client/blob/master/lib/socket.js#L113
+ */
+ }, {
+ key: 'send',
+ value: function send(data) {
+ this.emit('message', data);
+ }
+
+ /*
+ * For registering events to be received from the server
+ */
+ }, {
+ key: 'on',
+ value: function on(type, callback) {
+ this.addEventListener(type, callback);
+ }
+
+ /*
+ * Join a room on a server
+ *
+ * http://socket.io/docs/rooms-and-namespaces/#joining-and-leaving
+ */
+ }, {
+ key: 'join',
+ value: function join(room) {
+ _networkBridge2['default'].addMembershipToRoom(this, room);
+ }
+
+ /*
+ * Get the websocket to leave the room
+ *
+ * http://socket.io/docs/rooms-and-namespaces/#joining-and-leaving
+ */
+ }, {
+ key: 'leave',
+ value: function leave(room) {
+ _networkBridge2['default'].removeMembershipFromRoom(this, room);
+ }
+
+ /*
+ * Invokes all listener functions that are listening to the given event.type property. Each
+ * listener will be passed the event as the first argument.
+ *
+ * @param {object} event - event object which will be passed to all listeners of the event.type property
+ */
+ }, {
+ key: 'dispatchEvent',
+ value: function dispatchEvent(event) {
+ var _this2 = this;
+
+ for (var _len = arguments.length, customArguments = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
+ customArguments[_key - 1] = arguments[_key];
+ }
+
+ var eventName = event.type;
+ var listeners = this.listeners[eventName];
+
+ if (!Array.isArray(listeners)) {
+ return false;
+ }
+
+ listeners.forEach(function (listener) {
+ if (customArguments.length > 0) {
+ listener.apply(_this2, customArguments);
+ } else {
+ // Regular WebSockets expect a MessageEvent but Socketio.io just wants raw data
+ // payload instanceof MessageEvent works, but you can't isntance of NodeEvent
+ // for now we detect if the output has data defined on it
+ listener.call(_this2, event.data ? event.data : event);
+ }
+ });
+ }
+ }]);
+
+ return SocketIO;
+})(_eventTarget2['default']);
+
+SocketIO.CONNECTING = 0;
+SocketIO.OPEN = 1;
+SocketIO.CLOSING = 2;
+SocketIO.CLOSED = 3;
+
+/*
+* Static constructor methods for the IO Socket
+*/
+var IO = function ioConstructor(url) {
+ return new SocketIO(url);
+};
+
+/*
+* Alias the raw IO() constructor
+*/
+IO.connect = function ioConnect(url) {
+ /* eslint-disable new-cap */
+ return IO(url);
+ /* eslint-enable new-cap */
+};
+
+exports['default'] = IO;
+module.exports = exports['default'];
+},{"./event-factory":5,"./event-target":6,"./helpers/close-codes":8,"./helpers/delay":10,"./network-bridge":15,"urijs":3}],18:[function(require,module,exports){
+'use strict';
+
+Object.defineProperty(exports, '__esModule', {
+ value: true
+});
+
+var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
+
+var _get = function get(_x2, _x3, _x4) { var _again = true; _function: while (_again) { var object = _x2, property = _x3, receiver = _x4; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x2 = parent; _x3 = property; _x4 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } };
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
+
+function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
+
+var _urijs = require('urijs');
+
+var _urijs2 = _interopRequireDefault(_urijs);
+
+var _helpersDelay = require('./helpers/delay');
+
+var _helpersDelay2 = _interopRequireDefault(_helpersDelay);
+
+var _eventTarget = require('./event-target');
+
+var _eventTarget2 = _interopRequireDefault(_eventTarget);
+
+var _networkBridge = require('./network-bridge');
+
+var _networkBridge2 = _interopRequireDefault(_networkBridge);
+
+var _helpersCloseCodes = require('./helpers/close-codes');
+
+var _helpersCloseCodes2 = _interopRequireDefault(_helpersCloseCodes);
+
+var _eventFactory = require('./event-factory');
+
+/*
+* The main websocket class which is designed to mimick the native WebSocket class as close
+* as possible.
+*
+* https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
+*/
+
+var WebSocket = (function (_EventTarget) {
+ _inherits(WebSocket, _EventTarget);
+
+ /*
+ * @param {string} url
+ */
+
+ function WebSocket(url) {
+ var protocol = arguments.length <= 1 || arguments[1] === undefined ? '' : arguments[1];
+
+ _classCallCheck(this, WebSocket);
+
+ _get(Object.getPrototypeOf(WebSocket.prototype), 'constructor', this).call(this);
+
+ if (!url) {
+ throw new TypeError('Failed to construct \'WebSocket\': 1 argument required, but only 0 present.');
+ }
+
+ this.binaryType = 'blob';
+ this.url = (0, _urijs2['default'])(url).toString();
+ this.readyState = WebSocket.CONNECTING;
+ this.protocol = '';
+
+ if (typeof protocol === 'string') {
+ this.protocol = protocol;
+ } else if (Array.isArray(protocol) && protocol.length > 0) {
+ this.protocol = protocol[0];
+ }
+
+ /*
+ * In order to capture the callback function we need to define custom setters.
+ * To illustrate:
+ * mySocket.onopen = function() { alert(true) };
+ *
+ * The only way to capture that function and hold onto it for later is with the
+ * below code:
+ */
+ Object.defineProperties(this, {
+ onopen: {
+ configurable: true,
+ enumerable: true,
+ get: function get() {
+ return this.listeners.open;
+ },
+ set: function set(listener) {
+ this.addEventListener('open', listener);
+ }
+ },
+ onmessage: {
+ configurable: true,
+ enumerable: true,
+ get: function get() {
+ return this.listeners.message;
+ },
+ set: function set(listener) {
+ this.addEventListener('message', listener);
+ }
+ },
+ onclose: {
+ configurable: true,
+ enumerable: true,
+ get: function get() {
+ return this.listeners.close;
+ },
+ set: function set(listener) {
+ this.addEventListener('close', listener);
+ }
+ },
+ onerror: {
+ configurable: true,
+ enumerable: true,
+ get: function get() {
+ return this.listeners.error;
+ },
+ set: function set(listener) {
+ this.addEventListener('error', listener);
+ }
+ }
+ });
+
+ var server = _networkBridge2['default'].attachWebSocket(this, this.url);
+
+ /*
+ * This delay is needed so that we dont trigger an event before the callbacks have been
+ * setup. For example:
+ *
+ * var socket = new WebSocket('ws://localhost');
+ *
+ * // If we dont have the delay then the event would be triggered right here and this is
+ * // before the onopen had a chance to register itself.
+ *
+ * socket.onopen = () => { // this would never be called };
+ *
+ * // and with the delay the event gets triggered here after all of the callbacks have been
+ * // registered :-)
+ */
+ (0, _helpersDelay2['default'])(function delayCallback() {
+ if (server) {
+ this.readyState = WebSocket.OPEN;
+ server.dispatchEvent((0, _eventFactory.createEvent)({ type: 'connection' }), server, this);
+ this.dispatchEvent((0, _eventFactory.createEvent)({ type: 'open', target: this }));
+ } else {
+ this.readyState = WebSocket.CLOSED;
+ this.dispatchEvent((0, _eventFactory.createEvent)({ type: 'error', target: this }));
+ this.dispatchEvent((0, _eventFactory.createCloseEvent)({ type: 'close', target: this, code: _helpersCloseCodes2['default'].CLOSE_NORMAL }));
+
+ console.error('WebSocket connection to \'' + this.url + '\' failed');
+ }
+ }, this);
+ }
+
+ /*
+ * Transmits data to the server over the WebSocket connection.
+ *
+ * https://developer.mozilla.org/en-US/docs/Web/API/WebSocket#send()
+ */
+
+ _createClass(WebSocket, [{
+ key: 'send',
+ value: function send(data) {
+ if (this.readyState === WebSocket.CLOSING || this.readyState === WebSocket.CLOSED) {
+ throw new Error('WebSocket is already in CLOSING or CLOSED state');
+ }
+
+ var messageEvent = (0, _eventFactory.createMessageEvent)({
+ type: 'message',
+ origin: this.url,
+ data: data
+ });
+
+ var server = _networkBridge2['default'].serverLookup(this.url);
+
+ if (server) {
+ server.dispatchEvent(messageEvent, data);
+ }
+ }
+
+ /*
+ * Closes the WebSocket connection or connection attempt, if any.
+ * If the connection is already CLOSED, this method does nothing.
+ *
+ * https://developer.mozilla.org/en-US/docs/Web/API/WebSocket#close()
+ */
+ }, {
+ key: 'close',
+ value: function close() {
+ if (this.readyState !== WebSocket.OPEN) {
+ return undefined;
+ }
+
+ var server = _networkBridge2['default'].serverLookup(this.url);
+ var closeEvent = (0, _eventFactory.createCloseEvent)({
+ type: 'close',
+ target: this,
+ code: _helpersCloseCodes2['default'].CLOSE_NORMAL
+ });
+
+ _networkBridge2['default'].removeWebSocket(this, this.url);
+
+ this.readyState = WebSocket.CLOSED;
+ this.dispatchEvent(closeEvent);
+
+ if (server) {
+ server.dispatchEvent(closeEvent, server);
+ }
+ }
+ }]);
+
+ return WebSocket;
+})(_eventTarget2['default']);
+
+WebSocket.CONNECTING = 0;
+WebSocket.OPEN = 1;
+WebSocket.CLOSING = 2;
+WebSocket.CLOSED = 3;
+
+exports['default'] = WebSocket;
+module.exports = exports['default'];
+},{"./event-factory":5,"./event-target":6,"./helpers/close-codes":8,"./helpers/delay":10,"./network-bridge":15,"urijs":3}]},{},[14])
+//# sourceMappingURL=data:application/json;charset:utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL25vZGVfbW9kdWxlcy9icm93c2VyLXBhY2svX3ByZWx1ZGUuanMiLCIuLi8uLi9ub2RlX21vZHVsZXMvdXJpanMvc3JjL0lQdjYuanMiLCIuLi8uLi9ub2RlX21vZHVsZXMvdXJpanMvc3JjL1NlY29uZExldmVsRG9tYWlucy5qcyIsIi4uLy4uL25vZGVfbW9kdWxlcy91cmlqcy9zcmMvVVJJLmpzIiwiLi4vLi4vbm9kZV9tb2R1bGVzL3VyaWpzL3NyYy9wdW55Y29kZS5qcyIsImV2ZW50LWZhY3RvcnkuanMiLCJldmVudC10YXJnZXQuanMiLCJoZWxwZXJzL2FycmF5LWhlbHBlcnMuanMiLCJoZWxwZXJzL2Nsb3NlLWNvZGVzLmpzIiwiaGVscGVycy9jbG9zZS1ldmVudC5qcyIsImhlbHBlcnMvZGVsYXkuanMiLCJoZWxwZXJzL2V2ZW50LXByb3RvdHlwZS5qcyIsImhlbHBlcnMvZXZlbnQuanMiLCJoZWxwZXJzL21lc3NhZ2UtZXZlbnQuanMiLCJtYWluLmpzIiwibmV0d29yay1icmlkZ2UuanMiLCJzZXJ2ZXIuanMiLCJzb2NrZXQtaW8uanMiLCJ3ZWJzb2NrZXQuanMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7QUNBQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FDNUxBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FDalBBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7OztBQ2puRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7OztBQzVmQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FDckdBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUN4R0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUM1QkE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUNuQkE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FDL0RBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUNwQkE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FDN0NBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQ3pEQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FDakVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FDL0JBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUM3S0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FDN0xBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FDclJBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBIiwiZmlsZSI6ImdlbmVyYXRlZC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzQ29udGVudCI6WyIoZnVuY3Rpb24gZSh0LG4scil7ZnVuY3Rpb24gcyhvLHUpe2lmKCFuW29dKXtpZighdFtvXSl7dmFyIGE9dHlwZW9mIHJlcXVpcmU9PVwiZnVuY3Rpb25cIiYmcmVxdWlyZTtpZighdSYmYSlyZXR1cm4gYShvLCEwKTtpZihpKXJldHVybiBpKG8sITApO3ZhciBmPW5ldyBFcnJvcihcIkNhbm5vdCBmaW5kIG1vZHVsZSAnXCIrbytcIidcIik7dGhyb3cgZi5jb2RlPVwiTU9EVUxFX05PVF9GT1VORFwiLGZ9dmFyIGw9bltvXT17ZXhwb3J0czp7fX07dFtvXVswXS5jYWxsKGwuZXhwb3J0cyxmdW5jdGlvbihlKXt2YXIgbj10W29dWzFdW2VdO3JldHVybiBzKG4/bjplKX0sbCxsLmV4cG9ydHMsZSx0LG4scil9cmV0dXJuIG5bb10uZXhwb3J0c312YXIgaT10eXBlb2YgcmVxdWlyZT09XCJmdW5jdGlvblwiJiZyZXF1aXJlO2Zvcih2YXIgbz0wO288ci5sZW5ndGg7bysrKXMocltvXSk7cmV0dXJuIHN9KSIsIi8qIVxuICogVVJJLmpzIC0gTXV0YXRpbmcgVVJMc1xuICogSVB2NiBTdXBwb3J0XG4gKlxuICogVmVyc2lvbjogMS4xNy4wXG4gKlxuICogQXV0aG9yOiBSb2RuZXkgUmVobVxuICogV2ViOiBodHRwOi8vbWVkaWFsaXplLmdpdGh1Yi5pby9VUkkuanMvXG4gKlxuICogTGljZW5zZWQgdW5kZXJcbiAqICAgTUlUIExpY2Vuc2UgaHR0cDovL3d3dy5vcGVuc291cmNlLm9yZy9saWNlbnNlcy9taXQtbGljZW5zZVxuICogICBHUEwgdjMgaHR0cDovL29wZW5zb3VyY2Uub3JnL2xpY2Vuc2VzL0dQTC0zLjBcbiAqXG4gKi9cblxuKGZ1bmN0aW9uIChyb290LCBmYWN0b3J5KSB7XG4gICd1c2Ugc3RyaWN0JztcbiAgLy8gaHR0cHM6Ly9naXRodWIuY29tL3VtZGpzL3VtZC9ibG9iL21hc3Rlci9yZXR1cm5FeHBvcnRzLmpzXG4gIGlmICh0eXBlb2YgZXhwb3J0cyA9PT0gJ29iamVjdCcpIHtcbiAgICAvLyBOb2RlXG4gICAgbW9kdWxlLmV4cG9ydHMgPSBmYWN0b3J5KCk7XG4gIH0gZWxzZSBpZiAodHlwZW9mIGRlZmluZSA9PT0gJ2Z1bmN0aW9uJyAmJiBkZWZpbmUuYW1kKSB7XG4gICAgLy8gQU1ELiBSZWdpc3RlciBhcyBhbiBhbm9ueW1vdXMgbW9kdWxlLlxuICAgIGRlZmluZShmYWN0b3J5KTtcbiAgfSBlbHNlIHtcbiAgICAvLyBCcm93c2VyIGdsb2JhbHMgKHJvb3QgaXMgd2luZG93KVxuICAgIHJvb3QuSVB2NiA9IGZhY3Rvcnkocm9vdCk7XG4gIH1cbn0odGhpcywgZnVuY3Rpb24gKHJvb3QpIHtcbiAgJ3VzZSBzdHJpY3QnO1xuXG4gIC8qXG4gIHZhciBfaW4gPSBcImZlODA6MDAwMDowMDAwOjAwMDA6MDIwNDo2MWZmOmZlOWQ6ZjE1NlwiO1xuICB2YXIgX291dCA9IElQdjYuYmVzdChfaW4pO1xuICB2YXIgX2V4cGVjdGVkID0gXCJmZTgwOjoyMDQ6NjFmZjpmZTlkOmYxNTZcIjtcblxuICBjb25zb2xlLmxvZyhfaW4sIF9vdXQsIF9leHBlY3RlZCwgX291dCA9PT0gX2V4cGVjdGVkKTtcbiAgKi9cblxuICAvLyBzYXZlIGN1cnJlbnQgSVB2NiB2YXJpYWJsZSwgaWYgYW55XG4gIHZhciBfSVB2NiA9IHJvb3QgJiYgcm9vdC5JUHY2O1xuXG4gIGZ1bmN0aW9uIGJlc3RQcmVzZW50YXRpb24oYWRkcmVzcykge1xuICAgIC8vIGJhc2VkIG9uOlxuICAgIC8vIEphdmFzY3JpcHQgdG8gdGVzdCBhbiBJUHY2IGFkZHJlc3MgZm9yIHByb3BlciBmb3JtYXQsIGFuZCB0b1xuICAgIC8vIHByZXNlbnQgdGhlIFwiYmVzdCB0ZXh0IHJlcHJlc2VudGF0aW9uXCIgYWNjb3JkaW5nIHRvIElFVEYgRHJhZnQgUkZDIGF0XG4gICAgLy8gaHR0cDovL3Rvb2xzLmlldGYub3JnL2h0bWwvZHJhZnQtaWV0Zi02bWFuLXRleHQtYWRkci1yZXByZXNlbnRhdGlvbi0wNFxuICAgIC8vIDggRmViIDIwMTAgUmljaCBCcm93biwgRGFydHdhcmUsIExMQ1xuICAgIC8vIFBsZWFzZSBmZWVsIGZyZWUgdG8gdXNlIHRoaXMgY29kZSBhcyBsb25nIGFzIHlvdSBwcm92aWRlIGEgbGluayB0b1xuICAgIC8vIGh0dHA6Ly93d3cuaW50ZXJtYXBwZXIuY29tXG4gICAgLy8gaHR0cDovL2ludGVybWFwcGVyLmNvbS9zdXBwb3J0L3Rvb2xzL0lQVjYtVmFsaWRhdG9yLmFzcHhcbiAgICAvLyBodHRwOi8vZG93bmxvYWQuZGFydHdhcmUuY29tL3RoaXJkcGFydHkvaXB2NnZhbGlkYXRvci5qc1xuXG4gICAgdmFyIF9hZGRyZXNzID0gYWRkcmVzcy50b0xvd2VyQ2FzZSgpO1xuICAgIHZhciBzZWdtZW50cyA9IF9hZGRyZXNzLnNwbGl0KCc6Jyk7XG4gICAgdmFyIGxlbmd0aCA9IHNlZ21lbnRzLmxlbmd0aDtcbiAgICB2YXIgdG90YWwgPSA4O1xuXG4gICAgLy8gdHJpbSBjb2xvbnMgKDo6IG9yIDo6YTpiOmPigKYgb3Ig4oCmYTpiOmM6OilcbiAgICBpZiAoc2VnbWVudHNbMF0gPT09ICcnICYmIHNlZ21lbnRzWzFdID09PSAnJyAmJiBzZWdtZW50c1syXSA9PT0gJycpIHtcbiAgICAgIC8vIG11c3QgaGF2ZSBiZWVuIDo6XG4gICAgICAvLyByZW1vdmUgZmlyc3QgdHdvIGl0ZW1zXG4gICAgICBzZWdtZW50cy5zaGlmdCgpO1xuICAgICAgc2VnbWVudHMuc2hpZnQoKTtcbiAgICB9IGVsc2UgaWYgKHNlZ21lbnRzWzBdID09PSAnJyAmJiBzZWdtZW50c1sxXSA9PT0gJycpIHtcbiAgICAgIC8vIG11c3QgaGF2ZSBiZWVuIDo6eHh4eFxuICAgICAgLy8gcmVtb3ZlIHRoZSBmaXJzdCBpdGVtXG4gICAgICBzZWdtZW50cy5zaGlmdCgpO1xuICAgIH0gZWxzZSBpZiAoc2VnbWVudHNbbGVuZ3RoIC0gMV0gPT09ICcnICYmIHNlZ21lbnRzW2xlbmd0aCAtIDJdID09PSAnJykge1xuICAgICAgLy8gbXVzdCBoYXZlIGJlZW4geHh4eDo6XG4gICAgICBzZWdtZW50cy5wb3AoKTtcbiAgICB9XG5cbiAgICBsZW5ndGggPSBzZWdtZW50cy5sZW5ndGg7XG5cbiAgICAvLyBhZGp1c3QgdG90YWwgc2VnbWVudHMgZm9yIElQdjQgdHJhaWxlclxuICAgIGlmIChzZWdtZW50c1tsZW5ndGggLSAxXS5pbmRleE9mKCcuJykgIT09IC0xKSB7XG4gICAgICAvLyBmb3VuZCBhIFwiLlwiIHdoaWNoIG1lYW5zIElQdjRcbiAgICAgIHRvdGFsID0gNztcbiAgICB9XG5cbiAgICAvLyBmaWxsIGVtcHR5IHNlZ21lbnRzIHRoZW0gd2l0aCBcIjAwMDBcIlxuICAgIHZhciBwb3M7XG4gICAgZm9yIChwb3MgPSAwOyBwb3MgPCBsZW5ndGg7IHBvcysrKSB7XG4gICAgICBpZiAoc2VnbWVudHNbcG9zXSA9PT0gJycpIHtcbiAgICAgICAgYnJlYWs7XG4gICAgICB9XG4gICAgfVxuXG4gICAgaWYgKHBvcyA8IHRvdGFsKSB7XG4gICAgICBzZWdtZW50cy5zcGxpY2UocG9zLCAxLCAnMDAwMCcpO1xuICAgICAgd2hpbGUgKHNlZ21lbnRzLmxlbmd0aCA8IHRvdGFsKSB7XG4gICAgICAgIHNlZ21lbnRzLnNwbGljZShwb3MsIDAsICcwMDAwJyk7XG4gICAgICB9XG5cbiAgICAgIGxlbmd0aCA9IHNlZ21lbnRzLmxlbmd0aDtcbiAgICB9XG5cbiAgICAvLyBzdHJpcCBsZWFkaW5nIHplcm9zXG4gICAgdmFyIF9zZWdtZW50cztcbiAgICBmb3IgKHZhciBpID0gMDsgaSA8IHRvdGFsOyBpKyspIHtcbiAgICAgIF9zZWdtZW50cyA9IHNlZ21lbnRzW2ldLnNwbGl0KCcnKTtcbiAgICAgIGZvciAodmFyIGogPSAwOyBqIDwgMyA7IGorKykge1xuICAgICAgICBpZiAoX3NlZ21lbnRzWzBdID09PSAnMCcgJiYgX3NlZ21lbnRzLmxlbmd0aCA+IDEpIHtcbiAgICAgICAgICBfc2VnbWVudHMuc3BsaWNlKDAsMSk7XG4gICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgYnJlYWs7XG4gICAgICAgIH1cbiAgICAgIH1cblxuICAgICAgc2VnbWVudHNbaV0gPSBfc2VnbWVudHMuam9pbignJyk7XG4gICAgfVxuXG4gICAgLy8gZmluZCBsb25nZXN0IHNlcXVlbmNlIG9mIHplcm9lcyBhbmQgY29hbGVzY2UgdGhlbSBpbnRvIG9uZSBzZWdtZW50XG4gICAgdmFyIGJlc3QgPSAtMTtcbiAgICB2YXIgX2Jlc3QgPSAwO1xuICAgIHZhciBfY3VycmVudCA9IDA7XG4gICAgdmFyIGN1cnJlbnQgPSAtMTtcbiAgICB2YXIgaW56ZXJvZXMgPSBmYWxzZTtcbiAgICAvLyBpOyBhbHJlYWR5IGRlY2xhcmVkXG5cbiAgICBmb3IgKGkgPSAwOyBpIDwgdG90YWw7IGkrKykge1xuICAgICAgaWYgKGluemVyb2VzKSB7XG4gICAgICAgIGlmIChzZWdtZW50c1tpXSA9PT0gJzAnKSB7XG4gICAgICAgICAgX2N1cnJlbnQgKz0gMTtcbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICBpbnplcm9lcyA9IGZhbHNlO1xuICAgICAgICAgIGlmIChfY3VycmVudCA+IF9iZXN0KSB7XG4gICAgICAgICAgICBiZXN0ID0gY3VycmVudDtcbiAgICAgICAgICAgIF9iZXN0ID0gX2N1cnJlbnQ7XG4gICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICB9IGVsc2Uge1xuICAgICAgICBpZiAoc2VnbWVudHNbaV0gPT09ICcwJykge1xuICAgICAgICAgIGluemVyb2VzID0gdHJ1ZTtcbiAgICAgICAgICBjdXJyZW50ID0gaTtcbiAgICAgICAgICBfY3VycmVudCA9IDE7XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9XG5cbiAgICBpZiAoX2N1cnJlbnQgPiBfYmVzdCkge1xuICAgICAgYmVzdCA9IGN1cnJlbnQ7XG4gICAgICBfYmVzdCA9IF9jdXJyZW50O1xuICAgIH1cblxuICAgIGlmIChfYmVzdCA+IDEpIHtcbiAgICAgIHNlZ21lbnRzLnNwbGljZShiZXN0LCBfYmVzdCwgJycpO1xuICAgIH1cblxuICAgIGxlbmd0aCA9IHNlZ21lbnRzLmxlbmd0aDtcblxuICAgIC8vIGFzc2VtYmxlIHJlbWFpbmluZyBzZWdtZW50c1xuICAgIHZhciByZXN1bHQgPSAnJztcbiAgICBpZiAoc2VnbWVudHNbMF0gPT09ICcnKSAge1xuICAgICAgcmVzdWx0ID0gJzonO1xuICAgIH1cblxuICAgIGZvciAoaSA9IDA7IGkgPCBsZW5ndGg7IGkrKykge1xuICAgICAgcmVzdWx0ICs9IHNlZ21lbnRzW2ldO1xuICAgICAgaWYgKGkgPT09IGxlbmd0aCAtIDEpIHtcbiAgICAgICAgYnJlYWs7XG4gICAgICB9XG5cbiAgICAgIHJlc3VsdCArPSAnOic7XG4gICAgfVxuXG4gICAgaWYgKHNlZ21lbnRzW2xlbmd0aCAtIDFdID09PSAnJykge1xuICAgICAgcmVzdWx0ICs9ICc6JztcbiAgICB9XG5cbiAgICByZXR1cm4gcmVzdWx0O1xuICB9XG5cbiAgZnVuY3Rpb24gbm9Db25mbGljdCgpIHtcbiAgICAvKmpzaGludCB2YWxpZHRoaXM6IHRydWUgKi9cbiAgICBpZiAocm9vdC5JUHY2ID09PSB0aGlzKSB7XG4gICAgICByb290LklQdjYgPSBfSVB2NjtcbiAgICB9XG4gIFxuICAgIHJldHVybiB0aGlzO1xuICB9XG5cbiAgcmV0dXJuIHtcbiAgICBiZXN0OiBiZXN0UHJlc2VudGF0aW9uLFxuICAgIG5vQ29uZmxpY3Q6IG5vQ29uZmxpY3RcbiAgfTtcbn0pKTtcbiIsIi8qIVxuICogVVJJLmpzIC0gTXV0YXRpbmcgVVJMc1xuICogU2Vjb25kIExldmVsIERvbWFpbiAoU0xEKSBTdXBwb3J0XG4gKlxuICogVmVyc2lvbjogMS4xNy4wXG4gKlxuICogQXV0aG9yOiBSb2RuZXkgUmVobVxuICogV2ViOiBodHRwOi8vbWVkaWFsaXplLmdpdGh1Yi5pby9VUkkuanMvXG4gKlxuICogTGljZW5zZWQgdW5kZXJcbiAqICAgTUlUIExpY2Vuc2UgaHR0cDovL3d3dy5vcGVuc291cmNlLm9yZy9saWNlbnNlcy9taXQtbGljZW5zZVxuICogICBHUEwgdjMgaHR0cDovL29wZW5zb3VyY2Uub3JnL2xpY2Vuc2VzL0dQTC0zLjBcbiAqXG4gKi9cblxuKGZ1bmN0aW9uIChyb290LCBmYWN0b3J5KSB7XG4gICd1c2Ugc3RyaWN0JztcbiAgLy8gaHR0cHM6Ly9naXRodWIuY29tL3VtZGpzL3VtZC9ibG9iL21hc3Rlci9yZXR1cm5FeHBvcnRzLmpzXG4gIGlmICh0eXBlb2YgZXhwb3J0cyA9PT0gJ29iamVjdCcpIHtcbiAgICAvLyBOb2RlXG4gICAgbW9kdWxlLmV4cG9ydHMgPSBmYWN0b3J5KCk7XG4gIH0gZWxzZSBpZiAodHlwZW9mIGRlZmluZSA9PT0gJ2Z1bmN0aW9uJyAmJiBkZWZpbmUuYW1kKSB7XG4gICAgLy8gQU1ELiBSZWdpc3RlciBhcyBhbiBhbm9ueW1vdXMgbW9kdWxlLlxuICAgIGRlZmluZShmYWN0b3J5KTtcbiAgfSBlbHNlIHtcbiAgICAvLyBCcm93c2VyIGdsb2JhbHMgKHJvb3QgaXMgd2luZG93KVxuICAgIHJvb3QuU2Vjb25kTGV2ZWxEb21haW5zID0gZmFjdG9yeShyb290KTtcbiAgfVxufSh0aGlzLCBmdW5jdGlvbiAocm9vdCkge1xuICAndXNlIHN0cmljdCc7XG5cbiAgLy8gc2F2ZSBjdXJyZW50IFNlY29uZExldmVsRG9tYWlucyB2YXJpYWJsZSwgaWYgYW55XG4gIHZhciBfU2Vjb25kTGV2ZWxEb21haW5zID0gcm9vdCAmJiByb290LlNlY29uZExldmVsRG9tYWlucztcblxuICB2YXIgU0xEID0ge1xuICAgIC8vIGxpc3Qgb2Yga25vd24gU2Vjb25kIExldmVsIERvbWFpbnNcbiAgICAvLyBjb252ZXJ0ZWQgbGlzdCBvZiBTTERzIGZyb20gaHR0cHM6Ly9naXRodWIuY29tL2dhdmluZ21pbGxlci9zZWNvbmQtbGV2ZWwtZG9tYWluc1xuICAgIC8vIC0tLS1cbiAgICAvLyBwdWJsaWNzdWZmaXgub3JnIGlzIG1vcmUgY3VycmVudCBhbmQgYWN0dWFsbHkgdXNlZCBieSBhIGNvdXBsZSBvZiBicm93c2VycyBpbnRlcm5hbGx5LlxuICAgIC8vIGRvd25zaWRlIGlzIGl0IGFsc28gY29udGFpbnMgZG9tYWlucyBsaWtlIFwiZHluZG5zLm9yZ1wiIC0gd2hpY2ggaXMgZmluZSBmb3IgdGhlIHNlY3VyaXR5XG4gICAgLy8gaXNzdWVzIGJyb3dzZXIgaGF2ZSB0byBkZWFsIHdpdGggKFNPUCBmb3IgY29va2llcywgZXRjKSAtIGJ1dCBpcyB3YXkgb3ZlcmJvYXJkIGZvciBVUkkuanNcbiAgICAvLyAtLS0tXG4gICAgbGlzdDoge1xuICAgICAgJ2FjJzonIGNvbSBnb3YgbWlsIG5ldCBvcmcgJyxcbiAgICAgICdhZSc6JyBhYyBjbyBnb3YgbWlsIG5hbWUgbmV0IG9yZyBwcm8gc2NoICcsXG4gICAgICAnYWYnOicgY29tIGVkdSBnb3YgbmV0IG9yZyAnLFxuICAgICAgJ2FsJzonIGNvbSBlZHUgZ292IG1pbCBuZXQgb3JnICcsXG4gICAgICAnYW8nOicgY28gZWQgZ3YgaXQgb2cgcGIgJyxcbiAgICAgICdhcic6JyBjb20gZWR1IGdvYiBnb3YgaW50IG1pbCBuZXQgb3JnIHR1ciAnLFxuICAgICAgJ2F0JzonIGFjIGNvIGd2IG9yICcsXG4gICAgICAnYXUnOicgYXNuIGNvbSBjc2lybyBlZHUgZ292IGlkIG5ldCBvcmcgJyxcbiAgICAgICdiYSc6JyBjbyBjb20gZWR1IGdvdiBtaWwgbmV0IG9yZyBycyB1bmJpIHVubW8gdW5zYSB1bnR6IHVuemUgJyxcbiAgICAgICdiYic6JyBiaXogY28gY29tIGVkdSBnb3YgaW5mbyBuZXQgb3JnIHN0b3JlIHR2ICcsXG4gICAgICAnYmgnOicgYml6IGNjIGNvbSBlZHUgZ292IGluZm8gbmV0IG9yZyAnLFxuICAgICAgJ2JuJzonIGNvbSBlZHUgZ292IG5ldCBvcmcgJyxcbiAgICAgICdibyc6JyBjb20gZWR1IGdvYiBnb3YgaW50IG1pbCBuZXQgb3JnIHR2ICcsXG4gICAgICAnYnInOicgYWRtIGFkdiBhZ3IgYW0gYXJxIGFydCBhdG8gYiBiaW8gYmxvZyBibWQgY2ltIGNuZyBjbnQgY29tIGNvb3AgZWNuIGVkdSBlbmcgZXNwIGV0YyBldGkgZmFyIGZsb2cgZm0gZm5kIGZvdCBmc3QgZzEyIGdnZiBnb3YgaW1iIGluZCBpbmYgam9yIGp1cyBsZWwgbWF0IG1lZCBtaWwgbXVzIG5ldCBub20gbm90IG50ciBvZG8gb3JnIHBwZyBwcm8gcHNjIHBzaSBxc2wgcmVjIHNsZyBzcnYgdG1wIHRyZCB0dXIgdHYgdmV0IHZsb2cgd2lraSB6bGcgJyxcbiAgICAgICdicyc6JyBjb20gZWR1IGdvdiBuZXQgb3JnICcsXG4gICAgICAnYnonOicgZHUgZXQgb20gb3YgcmcgJyxcbiAgICAgICdjYSc6JyBhYiBiYyBtYiBuYiBuZiBubCBucyBudCBudSBvbiBwZSBxYyBzayB5ayAnLFxuICAgICAgJ2NrJzonIGJpeiBjbyBlZHUgZ2VuIGdvdiBpbmZvIG5ldCBvcmcgJyxcbiAgICAgICdjbic6JyBhYyBhaCBiaiBjb20gY3EgZWR1IGZqIGdkIGdvdiBncyBneCBneiBoYSBoYiBoZSBoaSBobCBobiBqbCBqcyBqeCBsbiBtaWwgbmV0IG5tIG54IG9yZyBxaCBzYyBzZCBzaCBzbiBzeCB0aiB0dyB4aiB4eiB5biB6aiAnLFxuICAgICAgJ2NvJzonIGNvbSBlZHUgZ292IG1pbCBuZXQgbm9tIG9yZyAnLFxuICAgICAgJ2NyJzonIGFjIGMgY28gZWQgZmkgZ28gb3Igc2EgJyxcbiAgICAgICdjeSc6JyBhYyBiaXogY29tIGVrbG9nZXMgZ292IGx0ZCBuYW1lIG5ldCBvcmcgcGFybGlhbWVudCBwcmVzcyBwcm8gdG0gJyxcbiAgICAgICdkbyc6JyBhcnQgY29tIGVkdSBnb2IgZ292IG1pbCBuZXQgb3JnIHNsZCB3ZWIgJyxcbiAgICAgICdkeic6JyBhcnQgYXNzbyBjb20gZWR1IGdvdiBuZXQgb3JnIHBvbCAnLFxuICAgICAgJ2VjJzonIGNvbSBlZHUgZmluIGdvdiBpbmZvIG1lZCBtaWwgbmV0IG9yZyBwcm8gJyxcbiAgICAgICdlZyc6JyBjb20gZWR1IGV1biBnb3YgbWlsIG5hbWUgbmV0IG9yZyBzY2kgJyxcbiAgICAgICdlcic6JyBjb20gZWR1IGdvdiBpbmQgbWlsIG5ldCBvcmcgcm9jaGVzdCB3ICcsXG4gICAgICAnZXMnOicgY29tIGVkdSBnb2Igbm9tIG9yZyAnLFxuICAgICAgJ2V0JzonIGJpeiBjb20gZWR1IGdvdiBpbmZvIG5hbWUgbmV0IG9yZyAnLFxuICAgICAgJ2ZqJzonIGFjIGJpeiBjb20gaW5mbyBtaWwgbmFtZSBuZXQgb3JnIHBybyAnLFxuICAgICAgJ2ZrJzonIGFjIGNvIGdvdiBuZXQgbm9tIG9yZyAnLFxuICAgICAgJ2ZyJzonIGFzc28gY29tIGYgZ291diBub20gcHJkIHByZXNzZSB0bSAnLFxuICAgICAgJ2dnJzonIGNvIG5ldCBvcmcgJyxcbiAgICAgICdnaCc6JyBjb20gZWR1IGdvdiBtaWwgb3JnICcsXG4gICAgICAnZ24nOicgYWMgY29tIGdvdiBuZXQgb3JnICcsXG4gICAgICAnZ3InOicgY29tIGVkdSBnb3YgbWlsIG5ldCBvcmcgJyxcbiAgICAgICdndCc6JyBjb20gZWR1IGdvYiBpbmQgbWlsIG5ldCBvcmcgJyxcbiAgICAgICdndSc6JyBjb20gZWR1IGdvdiBuZXQgb3JnICcsXG4gICAgICAnaGsnOicgY29tIGVkdSBnb3YgaWR2IG5ldCBvcmcgJyxcbiAgICAgICdodSc6JyAyMDAwIGFncmFyIGJvbHQgY2FzaW5vIGNpdHkgY28gZXJvdGljYSBlcm90aWthIGZpbG0gZm9ydW0gZ2FtZXMgaG90ZWwgaW5mbyBpbmdhdGxhbiBqb2dhc3oga29ueXZlbG8gbGFrYXMgbWVkaWEgbmV3cyBvcmcgcHJpdiByZWtsYW0gc2V4IHNob3Agc3BvcnQgc3VsaSBzemV4IHRtIHRvenNkZSB1dGF6YXMgdmlkZW8gJyxcbiAgICAgICdpZCc6JyBhYyBjbyBnbyBtaWwgbmV0IG9yIHNjaCB3ZWIgJyxcbiAgICAgICdpbCc6JyBhYyBjbyBnb3YgaWRmIGsxMiBtdW5pIG5ldCBvcmcgJyxcbiAgICAgICdpbic6JyBhYyBjbyBlZHUgZXJuZXQgZmlybSBnZW4gZ292IGkgaW5kIG1pbCBuZXQgbmljIG9yZyByZXMgJyxcbiAgICAgICdpcSc6JyBjb20gZWR1IGdvdiBpIG1pbCBuZXQgb3JnICcsXG4gICAgICAnaXInOicgYWMgY28gZG5zc2VjIGdvdiBpIGlkIG5ldCBvcmcgc2NoICcsXG4gICAgICAnaXQnOicgZWR1IGdvdiAnLFxuICAgICAgJ2plJzonIGNvIG5ldCBvcmcgJyxcbiAgICAgICdqbyc6JyBjb20gZWR1IGdvdiBtaWwgbmFtZSBuZXQgb3JnIHNjaCAnLFxuICAgICAgJ2pwJzonIGFjIGFkIGNvIGVkIGdvIGdyIGxnIG5lIG9yICcsXG4gICAgICAna2UnOicgYWMgY28gZ28gaW5mbyBtZSBtb2JpIG5lIG9yIHNjICcsXG4gICAgICAna2gnOicgY29tIGVkdSBnb3YgbWlsIG5ldCBvcmcgcGVyICcsXG4gICAgICAna2knOicgYml6IGNvbSBkZSBlZHUgZ292IGluZm8gbW9iIG5ldCBvcmcgdGVsICcsXG4gICAgICAna20nOicgYXNzbyBjb20gY29vcCBlZHUgZ291diBrIG1lZGVjaW4gbWlsIG5vbSBub3RhaXJlcyBwaGFybWFjaWVucyBwcmVzc2UgdG0gdmV0ZXJpbmFpcmUgJyxcbiAgICAgICdrbic6JyBlZHUgZ292IG5ldCBvcmcgJyxcbiAgICAgICdrcic6JyBhYyBidXNhbiBjaHVuZ2J1ayBjaHVuZ25hbSBjbyBkYWVndSBkYWVqZW9uIGVzIGdhbmd3b24gZ28gZ3dhbmdqdSBneWVvbmdidWsgZ3llb25nZ2kgZ3llb25nbmFtIGhzIGluY2hlb24gamVqdSBqZW9uYnVrIGplb25uYW0gayBrZyBtaWwgbXMgbmUgb3IgcGUgcmUgc2Mgc2VvdWwgdWxzYW4gJyxcbiAgICAgICdrdyc6JyBjb20gZWR1IGdvdiBuZXQgb3JnICcsXG4gICAgICAna3knOicgY29tIGVkdSBnb3YgbmV0IG9yZyAnLFxuICAgICAgJ2t6JzonIGNvbSBlZHUgZ292IG1pbCBuZXQgb3JnICcsXG4gICAgICAnbGInOicgY29tIGVkdSBnb3YgbmV0IG9yZyAnLFxuICAgICAgJ2xrJzonIGFzc24gY29tIGVkdSBnb3YgZ3JwIGhvdGVsIGludCBsdGQgbmV0IG5nbyBvcmcgc2NoIHNvYyB3ZWIgJyxcbiAgICAgICdscic6JyBjb20gZWR1IGdvdiBuZXQgb3JnICcsXG4gICAgICAnbHYnOicgYXNuIGNvbSBjb25mIGVkdSBnb3YgaWQgbWlsIG5ldCBvcmcgJyxcbiAgICAgICdseSc6JyBjb20gZWR1IGdvdiBpZCBtZWQgbmV0IG9yZyBwbGMgc2NoICcsXG4gICAgICAnbWEnOicgYWMgY28gZ292IG0gbmV0IG9yZyBwcmVzcyAnLFxuICAgICAgJ21jJzonIGFzc28gdG0gJyxcbiAgICAgICdtZSc6JyBhYyBjbyBlZHUgZ292IGl0cyBuZXQgb3JnIHByaXYgJyxcbiAgICAgICdtZyc6JyBjb20gZWR1IGdvdiBtaWwgbm9tIG9yZyBwcmQgdG0gJyxcbiAgICAgICdtayc6JyBjb20gZWR1IGdvdiBpbmYgbmFtZSBuZXQgb3JnIHBybyAnLFxuICAgICAgJ21sJzonIGNvbSBlZHUgZ292IG5ldCBvcmcgcHJlc3NlICcsXG4gICAgICAnbW4nOicgZWR1IGdvdiBvcmcgJyxcbiAgICAgICdtbyc6JyBjb20gZWR1IGdvdiBuZXQgb3JnICcsXG4gICAgICAnbXQnOicgY29tIGVkdSBnb3YgbmV0IG9yZyAnLFxuICAgICAgJ212JzonIGFlcm8gYml6IGNvbSBjb29wIGVkdSBnb3YgaW5mbyBpbnQgbWlsIG11c2V1bSBuYW1lIG5ldCBvcmcgcHJvICcsXG4gICAgICAnbXcnOicgYWMgY28gY29tIGNvb3AgZWR1IGdvdiBpbnQgbXVzZXVtIG5ldCBvcmcgJyxcbiAgICAgICdteCc6JyBjb20gZWR1IGdvYiBuZXQgb3JnICcsXG4gICAgICAnbXknOicgY29tIGVkdSBnb3YgbWlsIG5hbWUgbmV0IG9yZyBzY2ggJyxcbiAgICAgICduZic6JyBhcnRzIGNvbSBmaXJtIGluZm8gbmV0IG90aGVyIHBlciByZWMgc3RvcmUgd2ViICcsXG4gICAgICAnbmcnOicgYml6IGNvbSBlZHUgZ292IG1pbCBtb2JpIG5hbWUgbmV0IG9yZyBzY2ggJyxcbiAgICAgICduaSc6JyBhYyBjbyBjb20gZWR1IGdvYiBtaWwgbmV0IG5vbSBvcmcgJyxcbiAgICAgICducCc6JyBjb20gZWR1IGdvdiBtaWwgbmV0IG9yZyAnLFxuICAgICAgJ25yJzonIGJpeiBjb20gZWR1IGdvdiBpbmZvIG5ldCBvcmcgJyxcbiAgICAgICdvbSc6JyBhYyBiaXogY28gY29tIGVkdSBnb3YgbWVkIG1pbCBtdXNldW0gbmV0IG9yZyBwcm8gc2NoICcsXG4gICAgICAncGUnOicgY29tIGVkdSBnb2IgbWlsIG5ldCBub20gb3JnIHNsZCAnLFxuICAgICAgJ3BoJzonIGNvbSBlZHUgZ292IGkgbWlsIG5ldCBuZ28gb3JnICcsXG4gICAgICAncGsnOicgYml6IGNvbSBlZHUgZmFtIGdvYiBnb2sgZ29uIGdvcCBnb3MgZ292IG5ldCBvcmcgd2ViICcsXG4gICAgICAncGwnOicgYXJ0IGJpYWx5c3RvayBiaXogY29tIGVkdSBnZGEgZ2RhbnNrIGdvcnpvdyBnb3YgaW5mbyBrYXRvd2ljZSBrcmFrb3cgbG9keiBsdWJsaW4gbWlsIG5ldCBuZ28gb2xzenR5biBvcmcgcG96bmFuIHB3ciByYWRvbSBzbHVwc2sgc3pjemVjaW4gdG9ydW4gd2Fyc3phd2Egd2F3IHdyb2Mgd3JvY2xhdyB6Z29yYSAnLFxuICAgICAgJ3ByJzonIGFjIGJpeiBjb20gZWR1IGVzdCBnb3YgaW5mbyBpc2xhIG5hbWUgbmV0IG9yZyBwcm8gcHJvZiAnLFxuICAgICAgJ3BzJzonIGNvbSBlZHUgZ292IG5ldCBvcmcgcGxvIHNlYyAnLFxuICAgICAgJ3B3JzonIGJlbGF1IGNvIGVkIGdvIG5lIG9yICcsXG4gICAgICAncm8nOicgYXJ0cyBjb20gZmlybSBpbmZvIG5vbSBudCBvcmcgcmVjIHN0b3JlIHRtIHd3dyAnLFxuICAgICAgJ3JzJzonIGFjIGNvIGVkdSBnb3YgaW4gb3JnICcsXG4gICAgICAnc2InOicgY29tIGVkdSBnb3YgbmV0IG9yZyAnLFxuICAgICAgJ3NjJzonIGNvbSBlZHUgZ292IG5ldCBvcmcgJyxcbiAgICAgICdzaCc6JyBjbyBjb20gZWR1IGdvdiBuZXQgbm9tIG9yZyAnLFxuICAgICAgJ3NsJzonIGNvbSBlZHUgZ292IG5ldCBvcmcgJyxcbiAgICAgICdzdCc6JyBjbyBjb20gY29uc3VsYWRvIGVkdSBlbWJhaXhhZGEgZ292IG1pbCBuZXQgb3JnIHByaW5jaXBlIHNhb3RvbWUgc3RvcmUgJyxcbiAgICAgICdzdic6JyBjb20gZWR1IGdvYiBvcmcgcmVkICcsXG4gICAgICAnc3onOicgYWMgY28gb3JnICcsXG4gICAgICAndHInOicgYXYgYmJzIGJlbCBiaXogY29tIGRyIGVkdSBnZW4gZ292IGluZm8gazEyIG5hbWUgbmV0IG9yZyBwb2wgdGVsIHRzayB0diB3ZWIgJyxcbiAgICAgICd0dCc6JyBhZXJvIGJpeiBjYXQgY28gY29tIGNvb3AgZWR1IGdvdiBpbmZvIGludCBqb2JzIG1pbCBtb2JpIG11c2V1bSBuYW1lIG5ldCBvcmcgcHJvIHRlbCB0cmF2ZWwgJyxcbiAgICAgICd0dyc6JyBjbHViIGNvbSBlYml6IGVkdSBnYW1lIGdvdiBpZHYgbWlsIG5ldCBvcmcgJyxcbiAgICAgICdtdSc6JyBhYyBjbyBjb20gZ292IG5ldCBvciBvcmcgJyxcbiAgICAgICdteic6JyBhYyBjbyBlZHUgZ292IG9yZyAnLFxuICAgICAgJ25hJzonIGNvIGNvbSAnLFxuICAgICAgJ256JzonIGFjIGNvIGNyaSBnZWVrIGdlbiBnb3Z0IGhlYWx0aCBpd2kgbWFvcmkgbWlsIG5ldCBvcmcgcGFybGlhbWVudCBzY2hvb2wgJyxcbiAgICAgICdwYSc6JyBhYm8gYWMgY29tIGVkdSBnb2IgaW5nIG1lZCBuZXQgbm9tIG9yZyBzbGQgJyxcbiAgICAgICdwdCc6JyBjb20gZWR1IGdvdiBpbnQgbmV0IG5vbWUgb3JnIHB1YmwgJyxcbiAgICAgICdweSc6JyBjb20gZWR1IGdvdiBtaWwgbmV0IG9yZyAnLFxuICAgICAgJ3FhJzonIGNvbSBlZHUgZ292IG1pbCBuZXQgb3JnICcsXG4gICAgICAncmUnOicgYXNzbyBjb20gbm9tICcsXG4gICAgICAncnUnOicgYWMgYWR5Z2V5YSBhbHRhaSBhbXVyIGFya2hhbmdlbHNrIGFzdHJha2hhbiBiYXNoa2lyaWEgYmVsZ29yb2QgYmlyIGJyeWFuc2sgYnVyeWF0aWEgY2JnIGNoZWwgY2hlbHlhYmluc2sgY2hpdGEgY2h1a290a2EgY2h1dmFzaGlhIGNvbSBkYWdlc3RhbiBlLWJ1cmcgZWR1IGdvdiBncm96bnkgaW50IGlya3V0c2sgaXZhbm92byBpemhldnNrIGphciBqb3Noa2FyLW9sYSBrYWxteWtpYSBrYWx1Z2Ega2FtY2hhdGthIGthcmVsaWEga2F6YW4ga2NociBrZW1lcm92byBraGFiYXJvdnNrIGtoYWthc3NpYSBraHYga2lyb3Yga29lbmlnIGtvbWkga29zdHJvbWEga3Jhbm95YXJzayBrdWJhbiBrdXJnYW4ga3Vyc2sgbGlwZXRzayBtYWdhZGFuIG1hcmkgbWFyaS1lbCBtYXJpbmUgbWlsIG1vcmRvdmlhIG1vc3JlZyBtc2sgbXVybWFuc2sgbmFsY2hpayBuZXQgbm5vdiBub3Ygbm92b3NpYmlyc2sgbnNrIG9tc2sgb3JlbmJ1cmcgb3JnIG9yeW9sIHBlbnphIHBlcm0gcHAgcHNrb3YgcHR6IHJuZCByeWF6YW4gc2FraGFsaW4gc2FtYXJhIHNhcmF0b3Ygc2ltYmlyc2sgc21vbGVuc2sgc3BiIHN0YXZyb3BvbCBzdHYgc3VyZ3V0IHRhbWJvdiB0YXRhcnN0YW4gdG9tIHRvbXNrIHRzYXJpdHN5biB0c2sgdHVsYSB0dXZhIHR2ZXIgdHl1bWVuIHVkbSB1ZG11cnRpYSB1bGFuLXVkZSB2bGFkaWthdmtheiB2bGFkaW1pciB2bGFkaXZvc3RvayB2b2xnb2dyYWQgdm9sb2dkYSB2b3JvbmV6aCB2cm4gdnlhdGthIHlha3V0aWEgeWFtYWwgeWVrYXRlcmluYnVyZyB5dXpobm8tc2FraGFsaW5zayAnLFxuICAgICAgJ3J3JzonIGFjIGNvIGNvbSBlZHUgZ291diBnb3YgaW50IG1pbCBuZXQgJyxcbiAgICAgICdzYSc6JyBjb20gZWR1IGdvdiBtZWQgbmV0IG9yZyBwdWIgc2NoICcsXG4gICAgICAnc2QnOicgY29tIGVkdSBnb3YgaW5mbyBtZWQgbmV0IG9yZyB0diAnLFxuICAgICAgJ3NlJzonIGEgYWMgYiBiZCBjIGQgZSBmIGcgaCBpIGsgbCBtIG4gbyBvcmcgcCBwYXJ0aSBwcCBwcmVzcyByIHMgdCB0bSB1IHcgeCB5IHogJyxcbiAgICAgICdzZyc6JyBjb20gZWR1IGdvdiBpZG4gbmV0IG9yZyBwZXIgJyxcbiAgICAgICdzbic6JyBhcnQgY29tIGVkdSBnb3V2IG9yZyBwZXJzbyB1bml2ICcsXG4gICAgICAnc3knOicgY29tIGVkdSBnb3YgbWlsIG5ldCBuZXdzIG9yZyAnLFxuICAgICAgJ3RoJzonIGFjIGNvIGdvIGluIG1pIG5ldCBvciAnLFxuICAgICAgJ3RqJzonIGFjIGJpeiBjbyBjb20gZWR1IGdvIGdvdiBpbmZvIGludCBtaWwgbmFtZSBuZXQgbmljIG9yZyB0ZXN0IHdlYiAnLFxuICAgICAgJ3RuJzonIGFncmluZXQgY29tIGRlZmVuc2UgZWR1bmV0IGVucyBmaW4gZ292IGluZCBpbmZvIGludGwgbWluY29tIG5hdCBuZXQgb3JnIHBlcnNvIHJucnQgcm5zIHJudSB0b3VyaXNtICcsXG4gICAgICAndHonOicgYWMgY28gZ28gbmUgb3IgJyxcbiAgICAgICd1YSc6JyBiaXogY2hlcmthc3N5IGNoZXJuaWdvdiBjaGVybm92dHN5IGNrIGNuIGNvIGNvbSBjcmltZWEgY3YgZG4gZG5lcHJvcGV0cm92c2sgZG9uZXRzayBkcCBlZHUgZ292IGlmIGluIGl2YW5vLWZyYW5raXZzayBraCBraGFya292IGtoZXJzb24ga2htZWxuaXRza2l5IGtpZXYga2lyb3ZvZ3JhZCBrbSBrciBrcyBrdiBsZyBsdWdhbnNrIGx1dHNrIGx2aXYgbWUgbWsgbmV0IG5pa29sYWV2IG9kIG9kZXNzYSBvcmcgcGwgcG9sdGF2YSBwcCByb3ZubyBydiBzZWJhc3RvcG9sIHN1bXkgdGUgdGVybm9waWwgdXpoZ29yb2QgdmlubmljYSB2biB6YXBvcml6aHpoZSB6aGl0b21pciB6cCB6dCAnLFxuICAgICAgJ3VnJzonIGFjIGNvIGdvIG5lIG9yIG9yZyBzYyAnLFxuICAgICAgJ3VrJzonIGFjIGJsIGJyaXRpc2gtbGlicmFyeSBjbyBjeW0gZ292IGdvdnQgaWNuZXQgamV0IGxlYSBsdGQgbWUgbWlsIG1vZCBuYXRpb25hbC1saWJyYXJ5LXNjb3RsYW5kIG5lbCBuZXQgbmhzIG5pYyBubHMgb3JnIG9yZ24gcGFybGlhbWVudCBwbGMgcG9saWNlIHNjaCBzY290IHNvYyAnLFxuICAgICAgJ3VzJzonIGRuaSBmZWQgaXNhIGtpZHMgbnNuICcsXG4gICAgICAndXknOicgY29tIGVkdSBndWIgbWlsIG5ldCBvcmcgJyxcbiAgICAgICd2ZSc6JyBjbyBjb20gZWR1IGdvYiBpbmZvIG1pbCBuZXQgb3JnIHdlYiAnLFxuICAgICAgJ3ZpJzonIGNvIGNvbSBrMTIgbmV0IG9yZyAnLFxuICAgICAgJ3ZuJzonIGFjIGJpeiBjb20gZWR1IGdvdiBoZWFsdGggaW5mbyBpbnQgbmFtZSBuZXQgb3JnIHBybyAnLFxuICAgICAgJ3llJzonIGNvIGNvbSBnb3YgbHRkIG1lIG5ldCBvcmcgcGxjICcsXG4gICAgICAneXUnOicgYWMgY28gZWR1IGdvdiBvcmcgJyxcbiAgICAgICd6YSc6JyBhYyBhZ3JpYyBhbHQgYm91cnNlIGNpdHkgY28gY3liZXJuZXQgZGIgZWR1IGdvdiBncm9uZGFyIGlhY2Nlc3MgaW10IGluY2EgbGFuZGVzaWduIGxhdyBtaWwgbmV0IG5nbyBuaXMgbm9tIG9saXZldHRpIG9yZyBwaXggc2Nob29sIHRtIHdlYiAnLFxuICAgICAgJ3ptJzonIGFjIGNvIGNvbSBlZHUgZ292IG5ldCBvcmcgc2NoICdcbiAgICB9LFxuICAgIC8vIGdvcmhpbGwgMjAxMy0xMC0yNTogVXNpbmcgaW5kZXhPZigpIGluc3RlYWQgUmVnZXhwKCkuIFNpZ25pZmljYW50IGJvb3N0XG4gICAgLy8gaW4gYm90aCBwZXJmb3JtYW5jZSBhbmQgbWVtb3J5IGZvb3RwcmludC4gTm8gaW5pdGlhbGl6YXRpb24gcmVxdWlyZWQuXG4gICAgLy8gaHR0cDovL2pzcGVyZi5jb20vdXJpLWpzLXNsZC1yZWdleC12cy1iaW5hcnktc2VhcmNoLzRcbiAgICAvLyBGb2xsb3dpbmcgbWV0aG9kcyB1c2UgbGFzdEluZGV4T2YoKSByYXRoZXIgdGhhbiBhcnJheS5zcGxpdCgpIGluIG9yZGVyXG4gICAgLy8gdG8gYXZvaWQgYW55IG1lbW9yeSBhbGxvY2F0aW9ucy5cbiAgICBoYXM6IGZ1bmN0aW9uKGRvbWFpbikge1xuICAgICAgdmFyIHRsZE9mZnNldCA9IGRvbWFpbi5sYXN0SW5kZXhPZignLicpO1xuICAgICAgaWYgKHRsZE9mZnNldCA8PSAwIHx8IHRsZE9mZnNldCA+PSAoZG9tYWluLmxlbmd0aC0xKSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgICB9XG4gICAgICB2YXIgc2xkT2Zmc2V0ID0gZG9tYWluLmxhc3RJbmRleE9mKCcuJywgdGxkT2Zmc2V0LTEpO1xuICAgICAgaWYgKHNsZE9mZnNldCA8PSAwIHx8IHNsZE9mZnNldCA+PSAodGxkT2Zmc2V0LTEpKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICAgIH1cbiAgICAgIHZhciBzbGRMaXN0ID0gU0xELmxpc3RbZG9tYWluLnNsaWNlKHRsZE9mZnNldCsxKV07XG4gICAgICBpZiAoIXNsZExpc3QpIHtcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgICAgfVxuICAgICAgcmV0dXJuIHNsZExpc3QuaW5kZXhPZignICcgKyBkb21haW4uc2xpY2Uoc2xkT2Zmc2V0KzEsIHRsZE9mZnNldCkgKyAnICcpID49IDA7XG4gICAgfSxcbiAgICBpczogZnVuY3Rpb24oZG9tYWluKSB7XG4gICAgICB2YXIgdGxkT2Zmc2V0ID0gZG9tYWluLmxhc3RJbmRleE9mKCcuJyk7XG4gICAgICBpZiAodGxkT2Zmc2V0IDw9IDAgfHwgdGxkT2Zmc2V0ID49IChkb21haW4ubGVuZ3RoLTEpKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICAgIH1cbiAgICAgIHZhciBzbGRPZmZzZXQgPSBkb21haW4ubGFzdEluZGV4T2YoJy4nLCB0bGRPZmZzZXQtMSk7XG4gICAgICBpZiAoc2xkT2Zmc2V0ID49IDApIHtcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgICAgfVxuICAgICAgdmFyIHNsZExpc3QgPSBTTEQubGlzdFtkb21haW4uc2xpY2UodGxkT2Zmc2V0KzEpXTtcbiAgICAgIGlmICghc2xkTGlzdCkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgICB9XG4gICAgICByZXR1cm4gc2xkTGlzdC5pbmRleE9mKCcgJyArIGRvbWFpbi5zbGljZSgwLCB0bGRPZmZzZXQpICsgJyAnKSA+PSAwO1xuICAgIH0sXG4gICAgZ2V0OiBmdW5jdGlvbihkb21haW4pIHtcbiAgICAgIHZhciB0bGRPZmZzZXQgPSBkb21haW4ubGFzdEluZGV4T2YoJy4nKTtcbiAgICAgIGlmICh0bGRPZmZzZXQgPD0gMCB8fCB0bGRPZmZzZXQgPj0gKGRvbWFpbi5sZW5ndGgtMSkpIHtcbiAgICAgICAgcmV0dXJuIG51bGw7XG4gICAgICB9XG4gICAgICB2YXIgc2xkT2Zmc2V0ID0gZG9tYWluLmxhc3RJbmRleE9mKCcuJywgdGxkT2Zmc2V0LTEpO1xuICAgICAgaWYgKHNsZE9mZnNldCA8PSAwIHx8IHNsZE9mZnNldCA+PSAodGxkT2Zmc2V0LTEpKSB7XG4gICAgICAgIHJldHVybiBudWxsO1xuICAgICAgfVxuICAgICAgdmFyIHNsZExpc3QgPSBTTEQubGlzdFtkb21haW4uc2xpY2UodGxkT2Zmc2V0KzEpXTtcbiAgICAgIGlmICghc2xkTGlzdCkge1xuICAgICAgICByZXR1cm4gbnVsbDtcbiAgICAgIH1cbiAgICAgIGlmIChzbGRMaXN0LmluZGV4T2YoJyAnICsgZG9tYWluLnNsaWNlKHNsZE9mZnNldCsxLCB0bGRPZmZzZXQpICsgJyAnKSA8IDApIHtcbiAgICAgICAgcmV0dXJuIG51bGw7XG4gICAgICB9XG4gICAgICByZXR1cm4gZG9tYWluLnNsaWNlKHNsZE9mZnNldCsxKTtcbiAgICB9LFxuICAgIG5vQ29uZmxpY3Q6IGZ1bmN0aW9uKCl7XG4gICAgICBpZiAocm9vdC5TZWNvbmRMZXZlbERvbWFpbnMgPT09IHRoaXMpIHtcbiAgICAgICAgcm9vdC5TZWNvbmRMZXZlbERvbWFpbnMgPSBfU2Vjb25kTGV2ZWxEb21haW5zO1xuICAgICAgfVxuICAgICAgcmV0dXJuIHRoaXM7XG4gICAgfVxuICB9O1xuXG4gIHJldHVybiBTTEQ7XG59KSk7XG4iLCIvKiFcbiAqIFVSSS5qcyAtIE11dGF0aW5nIFVSTHNcbiAqXG4gKiBWZXJzaW9uOiAxLjE3LjBcbiAqXG4gKiBBdXRob3I6IFJvZG5leSBSZWhtXG4gKiBXZWI6IGh0dHA6Ly9tZWRpYWxpemUuZ2l0aHViLmlvL1VSSS5qcy9cbiAqXG4gKiBMaWNlbnNlZCB1bmRlclxuICogICBNSVQgTGljZW5zZSBodHRwOi8vd3d3Lm9wZW5zb3VyY2Uub3JnL2xpY2Vuc2VzL21pdC1saWNlbnNlXG4gKiAgIEdQTCB2MyBodHRwOi8vb3BlbnNvdXJjZS5vcmcvbGljZW5zZXMvR1BMLTMuMFxuICpcbiAqL1xuKGZ1bmN0aW9uIChyb290LCBmYWN0b3J5KSB7XG4gICd1c2Ugc3RyaWN0JztcbiAgLy8gaHR0cHM6Ly9naXRodWIuY29tL3VtZGpzL3VtZC9ibG9iL21hc3Rlci9yZXR1cm5FeHBvcnRzLmpzXG4gIGlmICh0eXBlb2YgZXhwb3J0cyA9PT0gJ29iamVjdCcpIHtcbiAgICAvLyBOb2RlXG4gICAgbW9kdWxlLmV4cG9ydHMgPSBmYWN0b3J5KHJlcXVpcmUoJy4vcHVueWNvZGUnKSwgcmVxdWlyZSgnLi9JUHY2JyksIHJlcXVpcmUoJy4vU2Vjb25kTGV2ZWxEb21haW5zJykpO1xuICB9IGVsc2UgaWYgKHR5cGVvZiBkZWZpbmUgPT09ICdmdW5jdGlvbicgJiYgZGVmaW5lLmFtZCkge1xuICAgIC8vIEFNRC4gUmVnaXN0ZXIgYXMgYW4gYW5vbnltb3VzIG1vZHVsZS5cbiAgICBkZWZpbmUoWycuL3B1bnljb2RlJywgJy4vSVB2NicsICcuL1NlY29uZExldmVsRG9tYWlucyddLCBmYWN0b3J5KTtcbiAgfSBlbHNlIHtcbiAgICAvLyBCcm93c2VyIGdsb2JhbHMgKHJvb3QgaXMgd2luZG93KVxuICAgIHJvb3QuVVJJID0gZmFjdG9yeShyb290LnB1bnljb2RlLCByb290LklQdjYsIHJvb3QuU2Vjb25kTGV2ZWxEb21haW5zLCByb290KTtcbiAgfVxufSh0aGlzLCBmdW5jdGlvbiAocHVueWNvZGUsIElQdjYsIFNMRCwgcm9vdCkge1xuICAndXNlIHN0cmljdCc7XG4gIC8qZ2xvYmFsIGxvY2F0aW9uLCBlc2NhcGUsIHVuZXNjYXBlICovXG4gIC8vIEZJWE1FOiB2Mi4wLjAgcmVuYW1jZSBub24tY2FtZWxDYXNlIHByb3BlcnRpZXMgdG8gdXBwZXJjYXNlXG4gIC8qanNoaW50IGNhbWVsY2FzZTogZmFsc2UgKi9cblxuICAvLyBzYXZlIGN1cnJlbnQgVVJJIHZhcmlhYmxlLCBpZiBhbnlcbiAgdmFyIF9VUkkgPSByb290ICYmIHJvb3QuVVJJO1xuXG4gIGZ1bmN0aW9uIFVSSSh1cmwsIGJhc2UpIHtcbiAgICB2YXIgX3VybFN1cHBsaWVkID0gYXJndW1lbnRzLmxlbmd0aCA+PSAxO1xuICAgIHZhciBfYmFzZVN1cHBsaWVkID0gYXJndW1lbnRzLmxlbmd0aCA+PSAyO1xuXG4gICAgLy8gQWxsb3cgaW5zdGFudGlhdGlvbiB3aXRob3V0IHRoZSAnbmV3JyBrZXl3b3JkXG4gICAgaWYgKCEodGhpcyBpbnN0YW5jZW9mIFVSSSkpIHtcbiAgICAgIGlmIChfdXJsU3VwcGxpZWQpIHtcbiAgICAgICAgaWYgKF9iYXNlU3VwcGxpZWQpIHtcbiAgICAgICAgICByZXR1cm4gbmV3IFVSSSh1cmwsIGJhc2UpO1xuICAgICAgICB9XG5cbiAgICAgICAgcmV0dXJuIG5ldyBVUkkodXJsKTtcbiAgICAgIH1cblxuICAgICAgcmV0dXJuIG5ldyBVUkkoKTtcbiAgICB9XG5cbiAgICBpZiAodXJsID09PSB1bmRlZmluZWQpIHtcbiAgICAgIGlmIChfdXJsU3VwcGxpZWQpIHtcbiAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcigndW5kZWZpbmVkIGlzIG5vdCBhIHZhbGlkIGFyZ3VtZW50IGZvciBVUkknKTtcbiAgICAgIH1cblxuICAgICAgaWYgKHR5cGVvZiBsb2NhdGlvbiAhPT0gJ3VuZGVmaW5lZCcpIHtcbiAgICAgICAgdXJsID0gbG9jYXRpb24uaHJlZiArICcnO1xuICAgICAgfSBlbHNlIHtcbiAgICAgICAgdXJsID0gJyc7XG4gICAgICB9XG4gICAgfVxuXG4gICAgdGhpcy5ocmVmKHVybCk7XG5cbiAgICAvLyByZXNvbHZlIHRvIGJhc2UgYWNjb3JkaW5nIHRvIGh0dHA6Ly9kdmNzLnczLm9yZy9oZy91cmwvcmF3LWZpbGUvdGlwL092ZXJ2aWV3Lmh0bWwjY29uc3RydWN0b3JcbiAgICBpZiAoYmFzZSAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICByZXR1cm4gdGhpcy5hYnNvbHV0ZVRvKGJhc2UpO1xuICAgIH1cblxuICAgIHJldHVybiB0aGlzO1xuICB9XG5cbiAgVVJJLnZlcnNpb24gPSAnMS4xNy4wJztcblxuICB2YXIgcCA9IFVSSS5wcm90b3R5cGU7XG4gIHZhciBoYXNPd24gPSBPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5O1xuXG4gIGZ1bmN0aW9uIGVzY2FwZVJlZ0V4KHN0cmluZykge1xuICAgIC8vIGh0dHBzOi8vZ2l0aHViLmNvbS9tZWRpYWxpemUvVVJJLmpzL2NvbW1pdC84NWFjMjE3ODNjMTFmOGNjYWIwNjEwNmRiYTk3MzVhMzFhODY5MjRkI2NvbW1pdGNvbW1lbnQtODIxOTYzXG4gICAgcmV0dXJuIHN0cmluZy5yZXBsYWNlKC8oWy4qKz9ePSE6JHt9KCl8W1xcXVxcL1xcXFxdKS9nLCAnXFxcXCQxJyk7XG4gIH1cblxuICBmdW5jdGlvbiBnZXRUeXBlKHZhbHVlKSB7XG4gICAgLy8gSUU4IGRvZXNuJ3QgcmV0dXJuIFtPYmplY3QgVW5kZWZpbmVkXSBidXQgW09iamVjdCBPYmplY3RdIGZvciB1bmRlZmluZWQgdmFsdWVcbiAgICBpZiAodmFsdWUgPT09IHVuZGVmaW5lZCkge1xuICAgICAgcmV0dXJuICdVbmRlZmluZWQnO1xuICAgIH1cblxuICAgIHJldHVybiBTdHJpbmcoT2JqZWN0LnByb3RvdHlwZS50b1N0cmluZy5jYWxsKHZhbHVlKSkuc2xpY2UoOCwgLTEpO1xuICB9XG5cbiAgZnVuY3Rpb24gaXNBcnJheShvYmopIHtcbiAgICByZXR1cm4gZ2V0VHlwZShvYmopID09PSAnQXJyYXknO1xuICB9XG5cbiAgZnVuY3Rpb24gZmlsdGVyQXJyYXlWYWx1ZXMoZGF0YSwgdmFsdWUpIHtcbiAgICB2YXIgbG9va3VwID0ge307XG4gICAgdmFyIGksIGxlbmd0aDtcblxuICAgIGlmIChnZXRUeXBlKHZhbHVlKSA9PT0gJ1JlZ0V4cCcpIHtcbiAgICAgIGxvb2t1cCA9IG51bGw7XG4gICAgfSBlbHNlIGlmIChpc0FycmF5KHZhbHVlKSkge1xuICAgICAgZm9yIChpID0gMCwgbGVuZ3RoID0gdmFsdWUubGVuZ3RoOyBpIDwgbGVuZ3RoOyBpKyspIHtcbiAgICAgICAgbG9va3VwW3ZhbHVlW2ldXSA9IHRydWU7XG4gICAgICB9XG4gICAgfSBlbHNlIHtcbiAgICAgIGxvb2t1cFt2YWx1ZV0gPSB0cnVlO1xuICAgIH1cblxuICAgIGZvciAoaSA9IDAsIGxlbmd0aCA9IGRhdGEubGVuZ3RoOyBpIDwgbGVuZ3RoOyBpKyspIHtcbiAgICAgIC8qanNoaW50IGxheGJyZWFrOiB0cnVlICovXG4gICAgICB2YXIgX21hdGNoID0gbG9va3VwICYmIGxvb2t1cFtkYXRhW2ldXSAhPT0gdW5kZWZpbmVkXG4gICAgICAgIHx8ICFsb29rdXAgJiYgdmFsdWUudGVzdChkYXRhW2ldKTtcbiAgICAgIC8qanNoaW50IGxheGJyZWFrOiBmYWxzZSAqL1xuICAgICAgaWYgKF9tYXRjaCkge1xuICAgICAgICBkYXRhLnNwbGljZShpLCAxKTtcbiAgICAgICAgbGVuZ3RoLS07XG4gICAgICAgIGktLTtcbiAgICAgIH1cbiAgICB9XG5cbiAgICByZXR1cm4gZGF0YTtcbiAgfVxuXG4gIGZ1bmN0aW9uIGFycmF5Q29udGFpbnMobGlzdCwgdmFsdWUpIHtcbiAgICB2YXIgaSwgbGVuZ3RoO1xuXG4gICAgLy8gdmFsdWUgbWF5IGJlIHN0cmluZywgbnVtYmVyLCBhcnJheSwgcmVnZXhwXG4gICAgaWYgKGlzQXJyYXkodmFsdWUpKSB7XG4gICAgICAvLyBOb3RlOiB0aGlzIGNhbiBiZSBvcHRpbWl6ZWQgdG8gTyhuKSAoaW5zdGVhZCBvZiBjdXJyZW50IE8obSAqIG4pKVxuICAgICAgZm9yIChpID0gMCwgbGVuZ3RoID0gdmFsdWUubGVuZ3RoOyBpIDwgbGVuZ3RoOyBpKyspIHtcbiAgICAgICAgaWYgKCFhcnJheUNvbnRhaW5zKGxpc3QsIHZhbHVlW2ldKSkge1xuICAgICAgICAgIHJldHVybiBmYWxzZTtcbiAgICAgICAgfVxuICAgICAgfVxuXG4gICAgICByZXR1cm4gdHJ1ZTtcbiAgICB9XG5cbiAgICB2YXIgX3R5cGUgPSBnZXRUeXBlKHZhbHVlKTtcbiAgICBmb3IgKGkgPSAwLCBsZW5ndGggPSBsaXN0Lmxlbmd0aDsgaSA8IGxlbmd0aDsgaSsrKSB7XG4gICAgICBpZiAoX3R5cGUgPT09ICdSZWdFeHAnKSB7XG4gICAgICAgIGlmICh0eXBlb2YgbGlzdFtpXSA9PT0gJ3N0cmluZycgJiYgbGlzdFtpXS5tYXRjaCh2YWx1ZSkpIHtcbiAgICAgICAgICByZXR1cm4gdHJ1ZTtcbiAgICAgICAgfVxuICAgICAgfSBlbHNlIGlmIChsaXN0W2ldID09PSB2YWx1ZSkge1xuICAgICAgICByZXR1cm4gdHJ1ZTtcbiAgICAgIH1cbiAgICB9XG5cbiAgICByZXR1cm4gZmFsc2U7XG4gIH1cblxuICBmdW5jdGlvbiBhcnJheXNFcXVhbChvbmUsIHR3bykge1xuICAgIGlmICghaXNBcnJheShvbmUpIHx8ICFpc0FycmF5KHR3bykpIHtcbiAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG5cbiAgICAvLyBhcnJheXMgY2FuJ3QgYmUgZXF1YWwgaWYgdGhleSBoYXZlIGRpZmZlcmVudCBhbW91bnQgb2YgY29udGVudFxuICAgIGlmIChvbmUubGVuZ3RoICE9PSB0d28ubGVuZ3RoKSB7XG4gICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuXG4gICAgb25lLnNvcnQoKTtcbiAgICB0d28uc29ydCgpO1xuXG4gICAgZm9yICh2YXIgaSA9IDAsIGwgPSBvbmUubGVuZ3RoOyBpIDwgbDsgaSsrKSB7XG4gICAgICBpZiAob25lW2ldICE9PSB0d29baV0pIHtcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgICAgfVxuICAgIH1cblxuICAgIHJldHVybiB0cnVlO1xuICB9XG5cbiAgZnVuY3Rpb24gdHJpbVNsYXNoZXModGV4dCkge1xuICAgIHZhciB0cmltX2V4cHJlc3Npb24gPSAvXlxcLyt8XFwvKyQvZztcbiAgICByZXR1cm4gdGV4dC5yZXBsYWNlKHRyaW1fZXhwcmVzc2lvbiwgJycpO1xuICB9XG5cbiAgVVJJLl9wYXJ0cyA9IGZ1bmN0aW9uKCkge1xuICAgIHJldHVybiB7XG4gICAgICBwcm90b2NvbDogbnVsbCxcbiAgICAgIHVzZXJuYW1lOiBudWxsLFxuICAgICAgcGFzc3dvcmQ6IG51bGwsXG4gICAgICBob3N0bmFtZTogbnVsbCxcbiAgICAgIHVybjogbnVsbCxcbiAgICAgIHBvcnQ6IG51bGwsXG4gICAgICBwYXRoOiBudWxsLFxuICAgICAgcXVlcnk6IG51bGwsXG4gICAgICBmcmFnbWVudDogbnVsbCxcbiAgICAgIC8vIHN0YXRlXG4gICAgICBkdXBsaWNhdGVRdWVyeVBhcmFtZXRlcnM6IFVSSS5kdXBsaWNhdGVRdWVyeVBhcmFtZXRlcnMsXG4gICAgICBlc2NhcGVRdWVyeVNwYWNlOiBVUkkuZXNjYXBlUXVlcnlTcGFjZVxuICAgIH07XG4gIH07XG4gIC8vIHN0YXRlOiBhbGxvdyBkdXBsaWNhdGUgcXVlcnkgcGFyYW1ldGVycyAoYT0xJmE9MSlcbiAgVVJJLmR1cGxpY2F0ZVF1ZXJ5UGFyYW1ldGVycyA9IGZhbHNlO1xuICAvLyBzdGF0ZTogcmVwbGFjZXMgKyB3aXRoICUyMCAoc3BhY2UgaW4gcXVlcnkgc3RyaW5ncylcbiAgVVJJLmVzY2FwZVF1ZXJ5U3BhY2UgPSB0cnVlO1xuICAvLyBzdGF0aWMgcHJvcGVydGllc1xuICBVUkkucHJvdG9jb2xfZXhwcmVzc2lvbiA9IC9eW2Etel1bYS16MC05ListXSokL2k7XG4gIFVSSS5pZG5fZXhwcmVzc2lvbiA9IC9bXmEtejAtOVxcLi1dL2k7XG4gIFVSSS5wdW55Y29kZV9leHByZXNzaW9uID0gLyh4bi0tKS9pO1xuICAvLyB3ZWxsLCAzMzMuNDQ0LjU1NS42NjYgbWF0Y2hlcywgYnV0IGl0IHN1cmUgYWluJ3Qgbm8gSVB2NCAtIGRvIHdlIGNhcmU/XG4gIFVSSS5pcDRfZXhwcmVzc2lvbiA9IC9eXFxkezEsM31cXC5cXGR7MSwzfVxcLlxcZHsxLDN9XFwuXFxkezEsM30kLztcbiAgLy8gY3JlZGl0cyB0byBSaWNoIEJyb3duXG4gIC8vIHNvdXJjZTogaHR0cDovL2ZvcnVtcy5pbnRlcm1hcHBlci5jb20vdmlld3RvcGljLnBocD9wPTEwOTYjMTA5NlxuICAvLyBzcGVjaWZpY2F0aW9uOiBodHRwOi8vd3d3LmlldGYub3JnL3JmYy9yZmM0MjkxLnR4dFxuICBVUkkuaXA2X2V4cHJlc3Npb24gPSAvXlxccyooKChbMC05QS1GYS1mXXsxLDR9Oil7N30oWzAtOUEtRmEtZl17MSw0fXw6KSl8KChbMC05QS1GYS1mXXsxLDR9Oil7Nn0oOlswLTlBLUZhLWZdezEsNH18KCgyNVswLTVdfDJbMC00XVxcZHwxXFxkXFxkfFsxLTldP1xcZCkoXFwuKDI1WzAtNV18MlswLTRdXFxkfDFcXGRcXGR8WzEtOV0/XFxkKSl7M30pfDopKXwoKFswLTlBLUZhLWZdezEsNH06KXs1fSgoKDpbMC05QS1GYS1mXXsxLDR9KXsxLDJ9KXw6KCgyNVswLTVdfDJbMC00XVxcZHwxXFxkXFxkfFsxLTldP1xcZCkoXFwuKDI1WzAtNV18MlswLTRdXFxkfDFcXGRcXGR8WzEtOV0/XFxkKSl7M30pfDopKXwoKFswLTlBLUZhLWZdezEsNH06KXs0fSgoKDpbMC05QS1GYS1mXXsxLDR9KXsxLDN9KXwoKDpbMC05QS1GYS1mXXsxLDR9KT86KCgyNVswLTVdfDJbMC00XVxcZHwxXFxkXFxkfFsxLTldP1xcZCkoXFwuKDI1WzAtNV18MlswLTRdXFxkfDFcXGRcXGR8WzEtOV0/XFxkKSl7M30pKXw6KSl8KChbMC05QS1GYS1mXXsxLDR9Oil7M30oKCg6WzAtOUEtRmEtZl17MSw0fSl7MSw0fSl8KCg6WzAtOUEtRmEtZl17MSw0fSl7MCwyfTooKDI1WzAtNV18MlswLTRdXFxkfDFcXGRcXGR8WzEtOV0/XFxkKShcXC4oMjVbMC01XXwyWzAtNF1cXGR8MVxcZFxcZHxbMS05XT9cXGQpKXszfSkpfDopKXwoKFswLTlBLUZhLWZdezEsNH06KXsyfSgoKDpbMC05QS1GYS1mXXsxLDR9KXsxLDV9KXwoKDpbMC05QS1GYS1mXXsxLDR9KXswLDN9OigoMjVbMC01XXwyWzAtNF1cXGR8MVxcZFxcZHxbMS05XT9cXGQpKFxcLigyNVswLTVdfDJbMC00XVxcZHwxXFxkXFxkfFsxLTldP1xcZCkpezN9KSl8OikpfCgoWzAtOUEtRmEtZl17MSw0fTopezF9KCgoOlswLTlBLUZhLWZdezEsNH0pezEsNn0pfCgoOlswLTlBLUZhLWZdezEsNH0pezAsNH06KCgyNVswLTVdfDJbMC00XVxcZHwxXFxkXFxkfFsxLTldP1xcZCkoXFwuKDI1WzAtNV18MlswLTRdXFxkfDFcXGRcXGR8WzEtOV0/XFxkKSl7M30pKXw6KSl8KDooKCg6WzAtOUEtRmEtZl17MSw0fSl7MSw3fSl8KCg6WzAtOUEtRmEtZl17MSw0fSl7MCw1fTooKDI1WzAtNV18MlswLTRdXFxkfDFcXGRcXGR8WzEtOV0/XFxkKShcXC4oMjVbMC01XXwyWzAtNF1cXGR8MVxcZFxcZHxbMS05XT9cXGQpKXszfSkpfDopKSkoJS4rKT9cXHMqJC87XG4gIC8vIGV4cHJlc3Npb24gdXNlZCBpcyBcImdydWJlciByZXZpc2VkXCIgKEBncnViZXIgdjIpIGRldGVybWluZWQgdG8gYmUgdGhlXG4gIC8vIGJlc3Qgc29sdXRpb24gaW4gYSByZWdleC1nb2xmIHdlIGRpZCBhIGNvdXBsZSBvZiBhZ2VzIGFnbyBhdFxuICAvLyAqIGh0dHA6Ly9tYXRoaWFzYnluZW5zLmJlL2RlbW8vdXJsLXJlZ2V4XG4gIC8vICogaHR0cDovL3JvZG5leXJlaG0uZGUvdC91cmwtcmVnZXguaHRtbFxuICBVUkkuZmluZF91cmlfZXhwcmVzc2lvbiA9IC9cXGIoKD86W2Etel1bXFx3LV0rOig/OlxcL3sxLDN9fFthLXowLTklXSl8d3d3XFxkezAsM31bLl18W2EtejAtOS5cXC1dK1suXVthLXpdezIsNH1cXC8pKD86W15cXHMoKTw+XSt8XFwoKFteXFxzKCk8Pl0rfChcXChbXlxccygpPD5dK1xcKSkpKlxcKSkrKD86XFwoKFteXFxzKCk8Pl0rfChcXChbXlxccygpPD5dK1xcKSkpKlxcKXxbXlxcc2AhKClcXFtcXF17fTs6J1wiLiw8Pj/Cq8K74oCc4oCd4oCY4oCZXSkpL2lnO1xuICBVUkkuZmluZFVyaSA9IHtcbiAgICAvLyB2YWxpZCBcInNjaGVtZTovL1wiIG9yIFwid3d3LlwiXG4gICAgc3RhcnQ6IC9cXGIoPzooW2Etel1bYS16MC05ListXSo6XFwvXFwvKXx3d3dcXC4pL2dpLFxuICAgIC8vIGV2ZXJ5dGhpbmcgdXAgdG8gdGhlIG5leHQgd2hpdGVzcGFjZVxuICAgIGVuZDogL1tcXHNcXHJcXG5dfCQvLFxuICAgIC8vIHRyaW0gdHJhaWxpbmcgcHVuY3R1YXRpb24gY2FwdHVyZWQgYnkgZW5kIFJlZ0V4cFxuICAgIHRyaW06IC9bYCEoKVxcW1xcXXt9OzonXCIuLDw+P8KrwrvigJzigJ3igJ7igJjigJldKyQvXG4gIH07XG4gIC8vIGh0dHA6Ly93d3cuaWFuYS5vcmcvYXNzaWdubWVudHMvdXJpLXNjaGVtZXMuaHRtbFxuICAvLyBodHRwOi8vZW4ud2lraXBlZGlhLm9yZy93aWtpL0xpc3Rfb2ZfVENQX2FuZF9VRFBfcG9ydF9udW1iZXJzI1dlbGwta25vd25fcG9ydHNcbiAgVVJJLmRlZmF1bHRQb3J0cyA9IHtcbiAgICBodHRwOiAnODAnLFxuICAgIGh0dHBzOiAnNDQzJyxcbiAgICBmdHA6ICcyMScsXG4gICAgZ29waGVyOiAnNzAnLFxuICAgIHdzOiAnODAnLFxuICAgIHdzczogJzQ0MydcbiAgfTtcbiAgLy8gYWxsb3dlZCBob3N0bmFtZSBjaGFyYWN0ZXJzIGFjY29yZGluZyB0byBSRkMgMzk4NlxuICAvLyBBTFBIQSBESUdJVCBcIi1cIiBcIi5cIiBcIl9cIiBcIn5cIiBcIiFcIiBcIiRcIiBcIiZcIiBcIidcIiBcIihcIiBcIilcIiBcIipcIiBcIitcIiBcIixcIiBcIjtcIiBcIj1cIiAlZW5jb2RlZFxuICAvLyBJJ3ZlIG5ldmVyIHNlZW4gYSAobm9uLUlETikgaG9zdG5hbWUgb3RoZXIgdGhhbjogQUxQSEEgRElHSVQgLiAtXG4gIFVSSS5pbnZhbGlkX2hvc3RuYW1lX2NoYXJhY3RlcnMgPSAvW15hLXpBLVowLTlcXC4tXS87XG4gIC8vIG1hcCBET00gRWxlbWVudHMgdG8gdGhlaXIgVVJJIGF0dHJpYnV0ZVxuICBVUkkuZG9tQXR0cmlidXRlcyA9IHtcbiAgICAnYSc6ICdocmVmJyxcbiAgICAnYmxvY2txdW90ZSc6ICdjaXRlJyxcbiAgICAnbGluayc6ICdocmVmJyxcbiAgICAnYmFzZSc6ICdocmVmJyxcbiAgICAnc2NyaXB0JzogJ3NyYycsXG4gICAgJ2Zvcm0nOiAnYWN0aW9uJyxcbiAgICAnaW1nJzogJ3NyYycsXG4gICAgJ2FyZWEnOiAnaHJlZicsXG4gICAgJ2lmcmFtZSc6ICdzcmMnLFxuICAgICdlbWJlZCc6ICdzcmMnLFxuICAgICdzb3VyY2UnOiAnc3JjJyxcbiAgICAndHJhY2snOiAnc3JjJyxcbiAgICAnaW5wdXQnOiAnc3JjJywgLy8gYnV0IG9ubHkgaWYgdHlwZT1cImltYWdlXCJcbiAgICAnYXVkaW8nOiAnc3JjJyxcbiAgICAndmlkZW8nOiAnc3JjJ1xuICB9O1xuICBVUkkuZ2V0RG9tQXR0cmlidXRlID0gZnVuY3Rpb24obm9kZSkge1xuICAgIGlmICghbm9kZSB8fCAhbm9kZS5ub2RlTmFtZSkge1xuICAgICAgcmV0dXJuIHVuZGVmaW5lZDtcbiAgICB9XG5cbiAgICB2YXIgbm9kZU5hbWUgPSBub2RlLm5vZGVOYW1lLnRvTG93ZXJDYXNlKCk7XG4gICAgLy8gPGlucHV0PiBzaG91bGQgb25seSBleHBvc2Ugc3JjIGZvciB0eXBlPVwiaW1hZ2VcIlxuICAgIGlmIChub2RlTmFtZSA9PT0gJ2lucHV0JyAmJiBub2RlLnR5cGUgIT09ICdpbWFnZScpIHtcbiAgICAgIHJldHVybiB1bmRlZmluZWQ7XG4gICAgfVxuXG4gICAgcmV0dXJuIFVSSS5kb21BdHRyaWJ1dGVzW25vZGVOYW1lXTtcbiAgfTtcblxuICBmdW5jdGlvbiBlc2NhcGVGb3JEdW1iRmlyZWZveDM2KHZhbHVlKSB7XG4gICAgLy8gaHR0cHM6Ly9naXRodWIuY29tL21lZGlhbGl6ZS9VUkkuanMvaXNzdWVzLzkxXG4gICAgcmV0dXJuIGVzY2FwZSh2YWx1ZSk7XG4gIH1cblxuICAvLyBlbmNvZGluZyAvIGRlY29kaW5nIGFjY29yZGluZyB0byBSRkMzOTg2XG4gIGZ1bmN0aW9uIHN0cmljdEVuY29kZVVSSUNvbXBvbmVudChzdHJpbmcpIHtcbiAgICAvLyBzZWUgaHR0cHM6Ly9kZXZlbG9wZXIubW96aWxsYS5vcmcvZW4tVVMvZG9jcy9KYXZhU2NyaXB0L1JlZmVyZW5jZS9HbG9iYWxfT2JqZWN0cy9lbmNvZGVVUklDb21wb25lbnRcbiAgICByZXR1cm4gZW5jb2RlVVJJQ29tcG9uZW50KHN0cmluZylcbiAgICAgIC5yZXBsYWNlKC9bIScoKSpdL2csIGVzY2FwZUZvckR1bWJGaXJlZm94MzYpXG4gICAgICAucmVwbGFjZSgvXFwqL2csICclMkEnKTtcbiAgfVxuICBVUkkuZW5jb2RlID0gc3RyaWN0RW5jb2RlVVJJQ29tcG9uZW50O1xuICBVUkkuZGVjb2RlID0gZGVjb2RlVVJJQ29tcG9uZW50O1xuICBVUkkuaXNvODg1OSA9IGZ1bmN0aW9uKCkge1xuICAgIFVSSS5lbmNvZGUgPSBlc2NhcGU7XG4gICAgVVJJLmRlY29kZSA9IHVuZXNjYXBlO1xuICB9O1xuICBVUkkudW5pY29kZSA9IGZ1bmN0aW9uKCkge1xuICAgIFVSSS5lbmNvZGUgPSBzdHJpY3RFbmNvZGVVUklDb21wb25lbnQ7XG4gICAgVVJJLmRlY29kZSA9IGRlY29kZVVSSUNvbXBvbmVudDtcbiAgfTtcbiAgVVJJLmNoYXJhY3RlcnMgPSB7XG4gICAgcGF0aG5hbWU6IHtcbiAgICAgIGVuY29kZToge1xuICAgICAgICAvLyBSRkMzOTg2IDIuMTogRm9yIGNvbnNpc3RlbmN5LCBVUkkgcHJvZHVjZXJzIGFuZCBub3JtYWxpemVycyBzaG91bGRcbiAgICAgICAgLy8gdXNlIHVwcGVyY2FzZSBoZXhhZGVjaW1hbCBkaWdpdHMgZm9yIGFsbCBwZXJjZW50LWVuY29kaW5ncy5cbiAgICAgICAgZXhwcmVzc2lvbjogLyUoMjR8MjZ8MkJ8MkN8M0J8M0R8M0F8NDApL2lnLFxuICAgICAgICBtYXA6IHtcbiAgICAgICAgICAvLyAtLl9+IScoKSpcbiAgICAgICAgICAnJTI0JzogJyQnLFxuICAgICAgICAgICclMjYnOiAnJicsXG4gICAgICAgICAgJyUyQic6ICcrJyxcbiAgICAgICAgICAnJTJDJzogJywnLFxuICAgICAgICAgICclM0InOiAnOycsXG4gICAgICAgICAgJyUzRCc6ICc9JyxcbiAgICAgICAgICAnJTNBJzogJzonLFxuICAgICAgICAgICclNDAnOiAnQCdcbiAgICAgICAgfVxuICAgICAgfSxcbiAgICAgIGRlY29kZToge1xuICAgICAgICBleHByZXNzaW9uOiAvW1xcL1xcPyNdL2csXG4gICAgICAgIG1hcDoge1xuICAgICAgICAgICcvJzogJyUyRicsXG4gICAgICAgICAgJz8nOiAnJTNGJyxcbiAgICAgICAgICAnIyc6ICclMjMnXG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9LFxuICAgIHJlc2VydmVkOiB7XG4gICAgICBlbmNvZGU6IHtcbiAgICAgICAgLy8gUkZDMzk4NiAyLjE6IEZvciBjb25zaXN0ZW5jeSwgVVJJIHByb2R1Y2VycyBhbmQgbm9ybWFsaXplcnMgc2hvdWxkXG4gICAgICAgIC8vIHVzZSB1cHBlcmNhc2UgaGV4YWRlY2ltYWwgZGlnaXRzIGZvciBhbGwgcGVyY2VudC1lbmNvZGluZ3MuXG4gICAgICAgIGV4cHJlc3Npb246IC8lKDIxfDIzfDI0fDI2fDI3fDI4fDI5fDJBfDJCfDJDfDJGfDNBfDNCfDNEfDNGfDQwfDVCfDVEKS9pZyxcbiAgICAgICAgbWFwOiB7XG4gICAgICAgICAgLy8gZ2VuLWRlbGltc1xuICAgICAgICAgICclM0EnOiAnOicsXG4gICAgICAgICAgJyUyRic6ICcvJyxcbiAgICAgICAgICAnJTNGJzogJz8nLFxuICAgICAgICAgICclMjMnOiAnIycsXG4gICAgICAgICAgJyU1Qic6ICdbJyxcbiAgICAgICAgICAnJTVEJzogJ10nLFxuICAgICAgICAgICclNDAnOiAnQCcsXG4gICAgICAgICAgLy8gc3ViLWRlbGltc1xuICAgICAgICAgICclMjEnOiAnIScsXG4gICAgICAgICAgJyUyNCc6ICckJyxcbiAgICAgICAgICAnJTI2JzogJyYnLFxuICAgICAgICAgICclMjcnOiAnXFwnJyxcbiAgICAgICAgICAnJTI4JzogJygnLFxuICAgICAgICAgICclMjknOiAnKScsXG4gICAgICAgICAgJyUyQSc6ICcqJyxcbiAgICAgICAgICAnJTJCJzogJysnLFxuICAgICAgICAgICclMkMnOiAnLCcsXG4gICAgICAgICAgJyUzQic6ICc7JyxcbiAgICAgICAgICAnJTNEJzogJz0nXG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9LFxuICAgIHVybnBhdGg6IHtcbiAgICAgIC8vIFRoZSBjaGFyYWN0ZXJzIHVuZGVyIGBlbmNvZGVgIGFyZSB0aGUgY2hhcmFjdGVycyBjYWxsZWQgb3V0IGJ5IFJGQyAyMTQxIGFzIGJlaW5nIGFjY2VwdGFibGVcbiAgICAgIC8vIGZvciB1c2FnZSBpbiBhIFVSTi4gUkZDMjE0MSBhbHNvIGNhbGxzIG91dCBcIi1cIiwgXCIuXCIsIGFuZCBcIl9cIiBhcyBhY2NlcHRhYmxlIGNoYXJhY3RlcnMsIGJ1dFxuICAgICAgLy8gdGhlc2UgYXJlbid0IGVuY29kZWQgYnkgZW5jb2RlVVJJQ29tcG9uZW50LCBzbyB3ZSBkb24ndCBoYXZlIHRvIGNhbGwgdGhlbSBvdXQgaGVyZS4gQWxzb1xuICAgICAgLy8gbm90ZSB0aGF0IHRoZSBjb2xvbiBjaGFyYWN0ZXIgaXMgbm90IGZlYXR1cmVkIGluIHRoZSBlbmNvZGluZyBtYXA7IHRoaXMgaXMgYmVjYXVzZSBVUkkuanNcbiAgICAgIC8vIGdpdmVzIHRoZSBjb2xvbnMgaW4gVVJOcyBzZW1hbnRpYyBtZWFuaW5nIGFzIHRoZSBkZWxpbWl0ZXJzIG9mIHBhdGggc2VnZW1lbnRzLCBhbmQgc28gaXRcbiAgICAgIC8vIHNob3VsZCBub3QgYXBwZWFyIHVuZW5jb2RlZCBpbiBhIHNlZ21lbnQgaXRzZWxmLlxuICAgICAgLy8gU2VlIGFsc28gdGhlIG5vdGUgYWJvdmUgYWJvdXQgUkZDMzk4NiBhbmQgY2FwaXRhbGFsaXplZCBoZXggZGlnaXRzLlxuICAgICAgZW5jb2RlOiB7XG4gICAgICAgIGV4cHJlc3Npb246IC8lKDIxfDI0fDI3fDI4fDI5fDJBfDJCfDJDfDNCfDNEfDQwKS9pZyxcbiAgICAgICAgbWFwOiB7XG4gICAgICAgICAgJyUyMSc6ICchJyxcbiAgICAgICAgICAnJTI0JzogJyQnLFxuICAgICAgICAgICclMjcnOiAnXFwnJyxcbiAgICAgICAgICAnJTI4JzogJygnLFxuICAgICAgICAgICclMjknOiAnKScsXG4gICAgICAgICAgJyUyQSc6ICcqJyxcbiAgICAgICAgICAnJTJCJzogJysnLFxuICAgICAgICAgICclMkMnOiAnLCcsXG4gICAgICAgICAgJyUzQic6ICc7JyxcbiAgICAgICAgICAnJTNEJzogJz0nLFxuICAgICAgICAgICclNDAnOiAnQCdcbiAgICAgICAgfVxuICAgICAgfSxcbiAgICAgIC8vIFRoZXNlIGNoYXJhY3RlcnMgYXJlIHRoZSBjaGFyYWN0ZXJzIGNhbGxlZCBvdXQgYnkgUkZDMjE0MSBhcyBcInJlc2VydmVkXCIgY2hhcmFjdGVycyB0aGF0XG4gICAgICAvLyBzaG91bGQgbmV2ZXIgYXBwZWFyIGluIGEgVVJOLCBwbHVzIHRoZSBjb2xvbiBjaGFyYWN0ZXIgKHNlZSBub3RlIGFib3ZlKS5cbiAgICAgIGRlY29kZToge1xuICAgICAgICBleHByZXNzaW9uOiAvW1xcL1xcPyM6XS9nLFxuICAgICAgICBtYXA6IHtcbiAgICAgICAgICAnLyc6ICclMkYnLFxuICAgICAgICAgICc/JzogJyUzRicsXG4gICAgICAgICAgJyMnOiAnJTIzJyxcbiAgICAgICAgICAnOic6ICclM0EnXG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9XG4gIH07XG4gIFVSSS5lbmNvZGVRdWVyeSA9IGZ1bmN0aW9uKHN0cmluZywgZXNjYXBlUXVlcnlTcGFjZSkge1xuICAgIHZhciBlc2NhcGVkID0gVVJJLmVuY29kZShzdHJpbmcgKyAnJyk7XG4gICAgaWYgKGVzY2FwZVF1ZXJ5U3BhY2UgPT09IHVuZGVmaW5lZCkge1xuICAgICAgZXNjYXBlUXVlcnlTcGFjZSA9IFVSSS5lc2NhcGVRdWVyeVNwYWNlO1xuICAgIH1cblxuICAgIHJldHVybiBlc2NhcGVRdWVyeVNwYWNlID8gZXNjYXBlZC5yZXBsYWNlKC8lMjAvZywgJysnKSA6IGVzY2FwZWQ7XG4gIH07XG4gIFVSSS5kZWNvZGVRdWVyeSA9IGZ1bmN0aW9uKHN0cmluZywgZXNjYXBlUXVlcnlTcGFjZSkge1xuICAgIHN0cmluZyArPSAnJztcbiAgICBpZiAoZXNjYXBlUXVlcnlTcGFjZSA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICBlc2NhcGVRdWVyeVNwYWNlID0gVVJJLmVzY2FwZVF1ZXJ5U3BhY2U7XG4gICAgfVxuXG4gICAgdHJ5IHtcbiAgICAgIHJldHVybiBVUkkuZGVjb2RlKGVzY2FwZVF1ZXJ5U3BhY2UgPyBzdHJpbmcucmVwbGFjZSgvXFwrL2csICclMjAnKSA6IHN0cmluZyk7XG4gICAgfSBjYXRjaChlKSB7XG4gICAgICAvLyB3ZSdyZSBub3QgZ29pbmcgdG8gbWVzcyB3aXRoIHdlaXJkIGVuY29kaW5ncyxcbiAgICAgIC8vIGdpdmUgdXAgYW5kIHJldHVybiB0aGUgdW5kZWNvZGVkIG9yaWdpbmFsIHN0cmluZ1xuICAgICAgLy8gc2VlIGh0dHBzOi8vZ2l0aHViLmNvbS9tZWRpYWxpemUvVVJJLmpzL2lzc3Vlcy84N1xuICAgICAgLy8gc2VlIGh0dHBzOi8vZ2l0aHViLmNvbS9tZWRpYWxpemUvVVJJLmpzL2lzc3Vlcy85MlxuICAgICAgcmV0dXJuIHN0cmluZztcbiAgICB9XG4gIH07XG4gIC8vIGdlbmVyYXRlIGVuY29kZS9kZWNvZGUgcGF0aCBmdW5jdGlvbnNcbiAgdmFyIF9wYXJ0cyA9IHsnZW5jb2RlJzonZW5jb2RlJywgJ2RlY29kZSc6J2RlY29kZSd9O1xuICB2YXIgX3BhcnQ7XG4gIHZhciBnZW5lcmF0ZUFjY2Vzc29yID0gZnVuY3Rpb24oX2dyb3VwLCBfcGFydCkge1xuICAgIHJldHVybiBmdW5jdGlvbihzdHJpbmcpIHtcbiAgICAgIHRyeSB7XG4gICAgICAgIHJldHVybiBVUklbX3BhcnRdKHN0cmluZyArICcnKS5yZXBsYWNlKFVSSS5jaGFyYWN0ZXJzW19ncm91cF1bX3BhcnRdLmV4cHJlc3Npb24sIGZ1bmN0aW9uKGMpIHtcbiAgICAgICAgICByZXR1cm4gVVJJLmNoYXJhY3RlcnNbX2dyb3VwXVtfcGFydF0ubWFwW2NdO1xuICAgICAgICB9KTtcbiAgICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgICAgLy8gd2UncmUgbm90IGdvaW5nIHRvIG1lc3Mgd2l0aCB3ZWlyZCBlbmNvZGluZ3MsXG4gICAgICAgIC8vIGdpdmUgdXAgYW5kIHJldHVybiB0aGUgdW5kZWNvZGVkIG9yaWdpbmFsIHN0cmluZ1xuICAgICAgICAvLyBzZWUgaHR0cHM6Ly9naXRodWIuY29tL21lZGlhbGl6ZS9VUkkuanMvaXNzdWVzLzg3XG4gICAgICAgIC8vIHNlZSBodHRwczovL2dpdGh1Yi5jb20vbWVkaWFsaXplL1VSSS5qcy9pc3N1ZXMvOTJcbiAgICAgICAgcmV0dXJuIHN0cmluZztcbiAgICAgIH1cbiAgICB9O1xuICB9O1xuXG4gIGZvciAoX3BhcnQgaW4gX3BhcnRzKSB7XG4gICAgVVJJW19wYXJ0ICsgJ1BhdGhTZWdtZW50J10gPSBnZW5lcmF0ZUFjY2Vzc29yKCdwYXRobmFtZScsIF9wYXJ0c1tfcGFydF0pO1xuICAgIFVSSVtfcGFydCArICdVcm5QYXRoU2VnbWVudCddID0gZ2VuZXJhdGVBY2Nlc3NvcigndXJucGF0aCcsIF9wYXJ0c1tfcGFydF0pO1xuICB9XG5cbiAgdmFyIGdlbmVyYXRlU2VnbWVudGVkUGF0aEZ1bmN0aW9uID0gZnVuY3Rpb24oX3NlcCwgX2NvZGluZ0Z1bmNOYW1lLCBfaW5uZXJDb2RpbmdGdW5jTmFtZSkge1xuICAgIHJldHVybiBmdW5jdGlvbihzdHJpbmcpIHtcbiAgICAgIC8vIFdoeSBwYXNzIGluIG5hbWVzIG9mIGZ1bmN0aW9ucywgcmF0aGVyIHRoYW4gdGhlIGZ1bmN0aW9uIG9iamVjdHMgdGhlbXNlbHZlcz8gVGhlXG4gICAgICAvLyBkZWZpbml0aW9ucyBvZiBzb21lIGZ1bmN0aW9ucyAoYnV0IGluIHBhcnRpY3VsYXIsIFVSSS5kZWNvZGUpIHdpbGwgb2NjYXNpb25hbGx5IGNoYW5nZSBkdWVcbiAgICAgIC8vIHRvIFVSSS5qcyBoYXZpbmcgSVNPODg1OSBhbmQgVW5pY29kZSBtb2Rlcy4gUGFzc2luZyBpbiB0aGUgbmFtZSBhbmQgZ2V0dGluZyBpdCB3aWxsIGVuc3VyZVxuICAgICAgLy8gdGhhdCB0aGUgZnVuY3Rpb25zIHdlIHVzZSBoZXJlIGFyZSBcImZyZXNoXCIuXG4gICAgICB2YXIgYWN0dWFsQ29kaW5nRnVuYztcbiAgICAgIGlmICghX2lubmVyQ29kaW5nRnVuY05hbWUpIHtcbiAgICAgICAgYWN0dWFsQ29kaW5nRnVuYyA9IFVSSVtfY29kaW5nRnVuY05hbWVdO1xuICAgICAgfSBlbHNlIHtcbiAgICAgICAgYWN0dWFsQ29kaW5nRnVuYyA9IGZ1bmN0aW9uKHN0cmluZykge1xuICAgICAgICAgIHJldHVybiBVUklbX2NvZGluZ0Z1bmNOYW1lXShVUklbX2lubmVyQ29kaW5nRnVuY05hbWVdKHN0cmluZykpO1xuICAgICAgICB9O1xuICAgICAgfVxuXG4gICAgICB2YXIgc2VnbWVudHMgPSAoc3RyaW5nICsgJycpLnNwbGl0KF9zZXApO1xuXG4gICAgICBmb3IgKHZhciBpID0gMCwgbGVuZ3RoID0gc2VnbWVudHMubGVuZ3RoOyBpIDwgbGVuZ3RoOyBpKyspIHtcbiAgICAgICAgc2VnbWVudHNbaV0gPSBhY3R1YWxDb2RpbmdGdW5jKHNlZ21lbnRzW2ldKTtcbiAgICAgIH1cblxuICAgICAgcmV0dXJuIHNlZ21lbnRzLmpvaW4oX3NlcCk7XG4gICAgfTtcbiAgfTtcblxuICAvLyBUaGlzIHRha2VzIHBsYWNlIG91dHNpZGUgdGhlIGFib3ZlIGxvb3AgYmVjYXVzZSB3ZSBkb24ndCB3YW50LCBlLmcuLCBlbmNvZGVVcm5QYXRoIGZ1bmN0aW9ucy5cbiAgVVJJLmRlY29kZVBhdGggPSBnZW5lcmF0ZVNlZ21lbnRlZFBhdGhGdW5jdGlvbignLycsICdkZWNvZGVQYXRoU2VnbWVudCcpO1xuICBVUkkuZGVjb2RlVXJuUGF0aCA9IGdlbmVyYXRlU2VnbWVudGVkUGF0aEZ1bmN0aW9uKCc6JywgJ2RlY29kZVVyblBhdGhTZWdtZW50Jyk7XG4gIFVSSS5yZWNvZGVQYXRoID0gZ2VuZXJhdGVTZWdtZW50ZWRQYXRoRnVuY3Rpb24oJy8nLCAnZW5jb2RlUGF0aFNlZ21lbnQnLCAnZGVjb2RlJyk7XG4gIFVSSS5yZWNvZGVVcm5QYXRoID0gZ2VuZXJhdGVTZWdtZW50ZWRQYXRoRnVuY3Rpb24oJzonLCAnZW5jb2RlVXJuUGF0aFNlZ21lbnQnLCAnZGVjb2RlJyk7XG5cbiAgVVJJLmVuY29kZVJlc2VydmVkID0gZ2VuZXJhdGVBY2Nlc3NvcigncmVzZXJ2ZWQnLCAnZW5jb2RlJyk7XG5cbiAgVVJJLnBhcnNlID0gZnVuY3Rpb24oc3RyaW5nLCBwYXJ0cykge1xuICAgIHZhciBwb3M7XG4gICAgaWYgKCFwYXJ0cykge1xuICAgICAgcGFydHMgPSB7fTtcbiAgICB9XG4gICAgLy8gW3Byb3RvY29sXCI6Ly9cIlt1c2VybmFtZVtcIjpcInBhc3N3b3JkXVwiQFwiXWhvc3RuYW1lW1wiOlwicG9ydF1cIi9cIj9dW3BhdGhdW1wiP1wicXVlcnlzdHJpbmddW1wiI1wiZnJhZ21lbnRdXG5cbiAgICAvLyBleHRyYWN0IGZyYWdtZW50XG4gICAgcG9zID0gc3RyaW5nLmluZGV4T2YoJyMnKTtcbiAgICBpZiAocG9zID4gLTEpIHtcbiAgICAgIC8vIGVzY2FwaW5nP1xuICAgICAgcGFydHMuZnJhZ21lbnQgPSBzdHJpbmcuc3Vic3RyaW5nKHBvcyArIDEpIHx8IG51bGw7XG4gICAgICBzdHJpbmcgPSBzdHJpbmcuc3Vic3RyaW5nKDAsIHBvcyk7XG4gICAgfVxuXG4gICAgLy8gZXh0cmFjdCBxdWVyeVxuICAgIHBvcyA9IHN0cmluZy5pbmRleE9mKCc/Jyk7XG4gICAgaWYgKHBvcyA+IC0xKSB7XG4gICAgICAvLyBlc2NhcGluZz9cbiAgICAgIHBhcnRzLnF1ZXJ5ID0gc3RyaW5nLnN1YnN0cmluZyhwb3MgKyAxKSB8fCBudWxsO1xuICAgICAgc3RyaW5nID0gc3RyaW5nLnN1YnN0cmluZygwLCBwb3MpO1xuICAgIH1cblxuICAgIC8vIGV4dHJhY3QgcHJvdG9jb2xcbiAgICBpZiAoc3RyaW5nLnN1YnN0cmluZygwLCAyKSA9PT0gJy8vJykge1xuICAgICAgLy8gcmVsYXRpdmUtc2NoZW1lXG4gICAgICBwYXJ0cy5wcm90b2NvbCA9IG51bGw7XG4gICAgICBzdHJpbmcgPSBzdHJpbmcuc3Vic3RyaW5nKDIpO1xuICAgICAgLy8gZXh0cmFjdCBcInVzZXI6cGFzc0Bob3N0OnBvcnRcIlxuICAgICAgc3RyaW5nID0gVVJJLnBhcnNlQXV0aG9yaXR5KHN0cmluZywgcGFydHMpO1xuICAgIH0gZWxzZSB7XG4gICAgICBwb3MgPSBzdHJpbmcuaW5kZXhPZignOicpO1xuICAgICAgaWYgKHBvcyA+IC0xKSB7XG4gICAgICAgIHBhcnRzLnByb3RvY29sID0gc3RyaW5nLnN1YnN0cmluZygwLCBwb3MpIHx8IG51bGw7XG4gICAgICAgIGlmIChwYXJ0cy5wcm90b2NvbCAmJiAhcGFydHMucHJvdG9jb2wubWF0Y2goVVJJLnByb3RvY29sX2V4cHJlc3Npb24pKSB7XG4gICAgICAgICAgLy8gOiBtYXkgYmUgd2l0aGluIHRoZSBwYXRoXG4gICAgICAgICAgcGFydHMucHJvdG9jb2wgPSB1bmRlZmluZWQ7XG4gICAgICAgIH0gZWxzZSBpZiAoc3RyaW5nLnN1YnN0cmluZyhwb3MgKyAxLCBwb3MgKyAzKSA9PT0gJy8vJykge1xuICAgICAgICAgIHN0cmluZyA9IHN0cmluZy5zdWJzdHJpbmcocG9zICsgMyk7XG5cbiAgICAgICAgICAvLyBleHRyYWN0IFwidXNlcjpwYXNzQGhvc3Q6cG9ydFwiXG4gICAgICAgICAgc3RyaW5nID0gVVJJLnBhcnNlQXV0aG9yaXR5KHN0cmluZywgcGFydHMpO1xuICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgIHN0cmluZyA9IHN0cmluZy5zdWJzdHJpbmcocG9zICsgMSk7XG4gICAgICAgICAgcGFydHMudXJuID0gdHJ1ZTtcbiAgICAgICAgfVxuICAgICAgfVxuICAgIH1cblxuICAgIC8vIHdoYXQncyBsZWZ0IG11c3QgYmUgdGhlIHBhdGhcbiAgICBwYXJ0cy5wYXRoID0gc3RyaW5nO1xuXG4gICAgLy8gYW5kIHdlJ3JlIGRvbmVcbiAgICByZXR1cm4gcGFydHM7XG4gIH07XG4gIFVSSS5wYXJzZUhvc3QgPSBmdW5jdGlvbihzdHJpbmcsIHBhcnRzKSB7XG4gICAgLy8gQ29weSBjaHJvbWUsIElFLCBvcGVyYSBiYWNrc2xhc2gtaGFuZGxpbmcgYmVoYXZpb3IuXG4gICAgLy8gQmFjayBzbGFzaGVzIGJlZm9yZSB0aGUgcXVlcnkgc3RyaW5nIGdldCBjb252ZXJ0ZWQgdG8gZm9yd2FyZCBzbGFzaGVzXG4gICAgLy8gU2VlOiBodHRwczovL2dpdGh1Yi5jb20vam95ZW50L25vZGUvYmxvYi8zODZmZDI0ZjQ5YjBlOWQxYThhMDc2NTkyYTQwNDE2OGZhZWVjYzM0L2xpYi91cmwuanMjTDExNS1MMTI0XG4gICAgLy8gU2VlOiBodHRwczovL2NvZGUuZ29vZ2xlLmNvbS9wL2Nocm9taXVtL2lzc3Vlcy9kZXRhaWw/aWQ9MjU5MTZcbiAgICAvLyBodHRwczovL2dpdGh1Yi5jb20vbWVkaWFsaXplL1VSSS5qcy9wdWxsLzIzM1xuICAgIHN0cmluZyA9IHN0cmluZy5yZXBsYWNlKC9cXFxcL2csICcvJyk7XG5cbiAgICAvLyBleHRyYWN0IGhvc3Q6cG9ydFxuICAgIHZhciBwb3MgPSBzdHJpbmcuaW5kZXhPZignLycpO1xuICAgIHZhciBicmFja2V0UG9zO1xuICAgIHZhciB0O1xuXG4gICAgaWYgKHBvcyA9PT0gLTEpIHtcbiAgICAgIHBvcyA9IHN0cmluZy5sZW5ndGg7XG4gICAgfVxuXG4gICAgaWYgKHN0cmluZy5jaGFyQXQoMCkgPT09ICdbJykge1xuICAgICAgLy8gSVB2NiBob3N0IC0gaHR0cDovL3Rvb2xzLmlldGYub3JnL2h0bWwvZHJhZnQtaWV0Zi02bWFuLXRleHQtYWRkci1yZXByZXNlbnRhdGlvbi0wNCNzZWN0aW9uLTZcbiAgICAgIC8vIEkgY2xhaW0gbW9zdCBjbGllbnQgc29mdHdhcmUgYnJlYWtzIG9uIElQdjYgYW55d2F5cy4gVG8gc2ltcGxpZnkgdGhpbmdzLCBVUkkgb25seSBhY2NlcHRzXG4gICAgICAvLyBJUHY2K3BvcnQgaW4gdGhlIGZvcm1hdCBbMjAwMTpkYjg6OjFdOjgwIChmb3IgdGhlIHRpbWUgYmVpbmcpXG4gICAgICBicmFja2V0UG9zID0gc3RyaW5nLmluZGV4T2YoJ10nKTtcbiAgICAgIHBhcnRzLmhvc3RuYW1lID0gc3RyaW5nLnN1YnN0cmluZygxLCBicmFja2V0UG9zKSB8fCBudWxsO1xuICAgICAgcGFydHMucG9ydCA9IHN0cmluZy5zdWJzdHJpbmcoYnJhY2tldFBvcyArIDIsIHBvcykgfHwgbnVsbDtcbiAgICAgIGlmIChwYXJ0cy5wb3J0ID09PSAnLycpIHtcbiAgICAgICAgcGFydHMucG9ydCA9IG51bGw7XG4gICAgICB9XG4gICAgfSBlbHNlIHtcbiAgICAgIHZhciBmaXJzdENvbG9uID0gc3RyaW5nLmluZGV4T2YoJzonKTtcbiAgICAgIHZhciBmaXJzdFNsYXNoID0gc3RyaW5nLmluZGV4T2YoJy8nKTtcbiAgICAgIHZhciBuZXh0Q29sb24gPSBzdHJpbmcuaW5kZXhPZignOicsIGZpcnN0Q29sb24gKyAxKTtcbiAgICAgIGlmIChuZXh0Q29sb24gIT09IC0xICYmIChmaXJzdFNsYXNoID09PSAtMSB8fCBuZXh0Q29sb24gPCBmaXJzdFNsYXNoKSkge1xuICAgICAgICAvLyBJUHY2IGhvc3QgY29udGFpbnMgbXVsdGlwbGUgY29sb25zIC0gYnV0IG5vIHBvcnRcbiAgICAgICAgLy8gdGhpcyBub3RhdGlvbiBpcyBhY3R1YWxseSBub3QgYWxsb3dlZCBieSBSRkMgMzk4NiwgYnV0IHdlJ3JlIGEgbGliZXJhbCBwYXJzZXJcbiAgICAgICAgcGFydHMuaG9zdG5hbWUgPSBzdHJpbmcuc3Vic3RyaW5nKDAsIHBvcykgfHwgbnVsbDtcbiAgICAgICAgcGFydHMucG9ydCA9IG51bGw7XG4gICAgICB9IGVsc2Uge1xuICAgICAgICB0ID0gc3RyaW5nLnN1YnN0cmluZygwLCBwb3MpLnNwbGl0KCc6Jyk7XG4gICAgICAgIHBhcnRzLmhvc3RuYW1lID0gdFswXSB8fCBudWxsO1xuICAgICAgICBwYXJ0cy5wb3J0ID0gdFsxXSB8fCBudWxsO1xuICAgICAgfVxuICAgIH1cblxuICAgIGlmIChwYXJ0cy5ob3N0bmFtZSAmJiBzdHJpbmcuc3Vic3RyaW5nKHBvcykuY2hhckF0KDApICE9PSAnLycpIHtcbiAgICAgIHBvcysrO1xuICAgICAgc3RyaW5nID0gJy8nICsgc3RyaW5nO1xuICAgIH1cblxuICAgIHJldHVybiBzdHJpbmcuc3Vic3RyaW5nKHBvcykgfHwgJy8nO1xuICB9O1xuICBVUkkucGFyc2VBdXRob3JpdHkgPSBmdW5jdGlvbihzdHJpbmcsIHBhcnRzKSB7XG4gICAgc3RyaW5nID0gVVJJLnBhcnNlVXNlcmluZm8oc3RyaW5nLCBwYXJ0cyk7XG4gICAgcmV0dXJuIFVSSS5wYXJzZUhvc3Qoc3RyaW5nLCBwYXJ0cyk7XG4gIH07XG4gIFVSSS5wYXJzZVVzZXJpbmZvID0gZnVuY3Rpb24oc3RyaW5nLCBwYXJ0cykge1xuICAgIC8vIGV4dHJhY3QgdXNlcm5hbWU6cGFzc3dvcmRcbiAgICB2YXIgZmlyc3RTbGFzaCA9IHN0cmluZy5pbmRleE9mKCcvJyk7XG4gICAgdmFyIHBvcyA9IHN0cmluZy5sYXN0SW5kZXhPZignQCcsIGZpcnN0U2xhc2ggPiAtMSA/IGZpcnN0U2xhc2ggOiBzdHJpbmcubGVuZ3RoIC0gMSk7XG4gICAgdmFyIHQ7XG5cbiAgICAvLyBhdXRob3JpdHlAIG11c3QgY29tZSBiZWZvcmUgL3BhdGhcbiAgICBpZiAocG9zID4gLTEgJiYgKGZpcnN0U2xhc2ggPT09IC0xIHx8IHBvcyA8IGZpcnN0U2xhc2gpKSB7XG4gICAgICB0ID0gc3RyaW5nLnN1YnN0cmluZygwLCBwb3MpLnNwbGl0KCc6Jyk7XG4gICAgICBwYXJ0cy51c2VybmFtZSA9IHRbMF0gPyBVUkkuZGVjb2RlKHRbMF0pIDogbnVsbDtcbiAgICAgIHQuc2hpZnQoKTtcbiAgICAgIHBhcnRzLnBhc3N3b3JkID0gdFswXSA/IFVSSS5kZWNvZGUodC5qb2luKCc6JykpIDogbnVsbDtcbiAgICAgIHN0cmluZyA9IHN0cmluZy5zdWJzdHJpbmcocG9zICsgMSk7XG4gICAgfSBlbHNlIHtcbiAgICAgIHBhcnRzLnVzZXJuYW1lID0gbnVsbDtcbiAgICAgIHBhcnRzLnBhc3N3b3JkID0gbnVsbDtcbiAgICB9XG5cbiAgICByZXR1cm4gc3RyaW5nO1xuICB9O1xuICBVUkkucGFyc2VRdWVyeSA9IGZ1bmN0aW9uKHN0cmluZywgZXNjYXBlUXVlcnlTcGFjZSkge1xuICAgIGlmICghc3RyaW5nKSB7XG4gICAgICByZXR1cm4ge307XG4gICAgfVxuXG4gICAgLy8gdGhyb3cgb3V0IHRoZSBmdW5reSBidXNpbmVzcyAtIFwiP1wiW25hbWVcIj1cInZhbHVlXCImXCJdK1xuICAgIHN0cmluZyA9IHN0cmluZy5yZXBsYWNlKC8mKy9nLCAnJicpLnJlcGxhY2UoL15cXD8qJip8JiskL2csICcnKTtcblxuICAgIGlmICghc3RyaW5nKSB7XG4gICAgICByZXR1cm4ge307XG4gICAgfVxuXG4gICAgdmFyIGl0ZW1zID0ge307XG4gICAgdmFyIHNwbGl0cyA9IHN0cmluZy5zcGxpdCgnJicpO1xuICAgIHZhciBsZW5ndGggPSBzcGxpdHMubGVuZ3RoO1xuICAgIHZhciB2LCBuYW1lLCB2YWx1ZTtcblxuICAgIGZvciAodmFyIGkgPSAwOyBpIDwgbGVuZ3RoOyBpKyspIHtcbiAgICAgIHYgPSBzcGxpdHNbaV0uc3BsaXQoJz0nKTtcbiAgICAgIG5hbWUgPSBVUkkuZGVjb2RlUXVlcnkodi5zaGlmdCgpLCBlc2NhcGVRdWVyeVNwYWNlKTtcbiAgICAgIC8vIG5vIFwiPVwiIGlzIG51bGwgYWNjb3JkaW5nIHRvIGh0dHA6Ly9kdmNzLnczLm9yZy9oZy91cmwvcmF3LWZpbGUvdGlwL092ZXJ2aWV3Lmh0bWwjY29sbGVjdC11cmwtcGFyYW1ldGVyc1xuICAgICAgdmFsdWUgPSB2Lmxlbmd0aCA/IFVSSS5kZWNvZGVRdWVyeSh2LmpvaW4oJz0nKSwgZXNjYXBlUXVlcnlTcGFjZSkgOiBudWxsO1xuXG4gICAgICBpZiAoaGFzT3duLmNhbGwoaXRlbXMsIG5hbWUpKSB7XG4gICAgICAgIGlmICh0eXBlb2YgaXRlbXNbbmFtZV0gPT09ICdzdHJpbmcnIHx8IGl0ZW1zW25hbWVdID09PSBudWxsKSB7XG4gICAgICAgICAgaXRlbXNbbmFtZV0gPSBbaXRlbXNbbmFtZV1dO1xuICAgICAgICB9XG5cbiAgICAgICAgaXRlbXNbbmFtZV0ucHVzaCh2YWx1ZSk7XG4gICAgICB9IGVsc2Uge1xuICAgICAgICBpdGVtc1tuYW1lXSA9IHZhbHVlO1xuICAgICAgfVxuICAgIH1cblxuICAgIHJldHVybiBpdGVtcztcbiAgfTtcblxuICBVUkkuYnVpbGQgPSBmdW5jdGlvbihwYXJ0cykge1xuICAgIHZhciB0ID0gJyc7XG5cbiAgICBpZiAocGFydHMucHJvdG9jb2wpIHtcbiAgICAgIHQgKz0gcGFydHMucHJvdG9jb2wgKyAnOic7XG4gICAgfVxuXG4gICAgaWYgKCFwYXJ0cy51cm4gJiYgKHQgfHwgcGFydHMuaG9zdG5hbWUpKSB7XG4gICAgICB0ICs9ICcvLyc7XG4gICAgfVxuXG4gICAgdCArPSAoVVJJLmJ1aWxkQXV0aG9yaXR5KHBhcnRzKSB8fCAnJyk7XG5cbiAgICBpZiAodHlwZW9mIHBhcnRzLnBhdGggPT09ICdzdHJpbmcnKSB7XG4gICAgICBpZiAocGFydHMucGF0aC5jaGFyQXQoMCkgIT09ICcvJyAmJiB0eXBlb2YgcGFydHMuaG9zdG5hbWUgPT09ICdzdHJpbmcnKSB7XG4gICAgICAgIHQgKz0gJy8nO1xuICAgICAgfVxuXG4gICAgICB0ICs9IHBhcnRzLnBhdGg7XG4gICAgfVxuXG4gICAgaWYgKHR5cGVvZiBwYXJ0cy5xdWVyeSA9PT0gJ3N0cmluZycgJiYgcGFydHMucXVlcnkpIHtcbiAgICAgIHQgKz0gJz8nICsgcGFydHMucXVlcnk7XG4gICAgfVxuXG4gICAgaWYgKHR5cGVvZiBwYXJ0cy5mcmFnbWVudCA9PT0gJ3N0cmluZycgJiYgcGFydHMuZnJhZ21lbnQpIHtcbiAgICAgIHQgKz0gJyMnICsgcGFydHMuZnJhZ21lbnQ7XG4gICAgfVxuICAgIHJldHVybiB0O1xuICB9O1xuICBVUkkuYnVpbGRIb3N0ID0gZnVuY3Rpb24ocGFydHMpIHtcbiAgICB2YXIgdCA9ICcnO1xuXG4gICAgaWYgKCFwYXJ0cy5ob3N0bmFtZSkge1xuICAgICAgcmV0dXJuICcnO1xuICAgIH0gZWxzZSBpZiAoVVJJLmlwNl9leHByZXNzaW9uLnRlc3QocGFydHMuaG9zdG5hbWUpKSB7XG4gICAgICB0ICs9ICdbJyArIHBhcnRzLmhvc3RuYW1lICsgJ10nO1xuICAgIH0gZWxzZSB7XG4gICAgICB0ICs9IHBhcnRzLmhvc3RuYW1lO1xuICAgIH1cblxuICAgIGlmIChwYXJ0cy5wb3J0KSB7XG4gICAgICB0ICs9ICc6JyArIHBhcnRzLnBvcnQ7XG4gICAgfVxuXG4gICAgcmV0dXJuIHQ7XG4gIH07XG4gIFVSSS5idWlsZEF1dGhvcml0eSA9IGZ1bmN0aW9uKHBhcnRzKSB7XG4gICAgcmV0dXJuIFVSSS5idWlsZFVzZXJpbmZvKHBhcnRzKSArIFVSSS5idWlsZEhvc3QocGFydHMpO1xuICB9O1xuICBVUkkuYnVpbGRVc2VyaW5mbyA9IGZ1bmN0aW9uKHBhcnRzKSB7XG4gICAgdmFyIHQgPSAnJztcblxuICAgIGlmIChwYXJ0cy51c2VybmFtZSkge1xuICAgICAgdCArPSBVUkkuZW5jb2RlKHBhcnRzLnVzZXJuYW1lKTtcblxuICAgICAgaWYgKHBhcnRzLnBhc3N3b3JkKSB7XG4gICAgICAgIHQgKz0gJzonICsgVVJJLmVuY29kZShwYXJ0cy5wYXNzd29yZCk7XG4gICAgICB9XG5cbiAgICAgIHQgKz0gJ0AnO1xuICAgIH1cblxuICAgIHJldHVybiB0O1xuICB9O1xuICBVUkkuYnVpbGRRdWVyeSA9IGZ1bmN0aW9uKGRhdGEsIGR1cGxpY2F0ZVF1ZXJ5UGFyYW1ldGVycywgZXNjYXBlUXVlcnlTcGFjZSkge1xuICAgIC8vIGFjY29yZGluZyB0byBodHRwOi8vdG9vbHMuaWV0Zi5vcmcvaHRtbC9yZmMzOTg2IG9yIGh0dHA6Ly9sYWJzLmFwYWNoZS5vcmcvd2ViYXJjaC91cmkvcmZjL3JmYzM5ODYuaHRtbFxuICAgIC8vIGJlaW5nIMK7LS5ffiEkJicoKSorLDs9OkAvP8KrICVIRVggYW5kIGFsbnVtIGFyZSBhbGxvd2VkXG4gICAgLy8gdGhlIFJGQyBleHBsaWNpdGx5IHN0YXRlcyA/L2ZvbyBiZWluZyBhIHZhbGlkIHVzZSBjYXNlLCBubyBtZW50aW9uIG9mIHBhcmFtZXRlciBzeW50YXghXG4gICAgLy8gVVJJLmpzIHRyZWF0cyB0aGUgcXVlcnkgc3RyaW5nIGFzIGJlaW5nIGFwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZFxuICAgIC8vIHNlZSBodHRwOi8vd3d3LnczLm9yZy9UUi9SRUMtaHRtbDQwL2ludGVyYWN0L2Zvcm1zLmh0bWwjZm9ybS1jb250ZW50LXR5cGVcblxuICAgIHZhciB0ID0gJyc7XG4gICAgdmFyIHVuaXF1ZSwga2V5LCBpLCBsZW5ndGg7XG4gICAgZm9yIChrZXkgaW4gZGF0YSkge1xuICAgICAgaWYgKGhhc093bi5jYWxsKGRhdGEsIGtleSkgJiYga2V5KSB7XG4gICAgICAgIGlmIChpc0FycmF5KGRhdGFba2V5XSkpIHtcbiAgICAgICAgICB1bmlxdWUgPSB7fTtcbiAgICAgICAgICBmb3IgKGkgPSAwLCBsZW5ndGggPSBkYXRhW2tleV0ubGVuZ3RoOyBpIDwgbGVuZ3RoOyBpKyspIHtcbiAgICAgICAgICAgIGlmIChkYXRhW2tleV1baV0gIT09IHVuZGVmaW5lZCAmJiB1bmlxdWVbZGF0YVtrZXldW2ldICsgJyddID09PSB1bmRlZmluZWQpIHtcbiAgICAgICAgICAgICAgdCArPSAnJicgKyBVUkkuYnVpbGRRdWVyeVBhcmFtZXRlcihrZXksIGRhdGFba2V5XVtpXSwgZXNjYXBlUXVlcnlTcGFjZSk7XG4gICAgICAgICAgICAgIGlmIChkdXBsaWNhdGVRdWVyeVBhcmFtZXRlcnMgIT09IHRydWUpIHtcbiAgICAgICAgICAgICAgICB1bmlxdWVbZGF0YVtrZXldW2ldICsgJyddID0gdHJ1ZTtcbiAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgfSBlbHNlIGlmIChkYXRhW2tleV0gIT09IHVuZGVmaW5lZCkge1xuICAgICAgICAgIHQgKz0gJyYnICsgVVJJLmJ1aWxkUXVlcnlQYXJhbWV0ZXIoa2V5LCBkYXRhW2tleV0sIGVzY2FwZVF1ZXJ5U3BhY2UpO1xuICAgICAgICB9XG4gICAgICB9XG4gICAgfVxuXG4gICAgcmV0dXJuIHQuc3Vic3RyaW5nKDEpO1xuICB9O1xuICBVUkkuYnVpbGRRdWVyeVBhcmFtZXRlciA9IGZ1bmN0aW9uKG5hbWUsIHZhbHVlLCBlc2NhcGVRdWVyeVNwYWNlKSB7XG4gICAgLy8gaHR0cDovL3d3dy53My5vcmcvVFIvUkVDLWh0bWw0MC9pbnRlcmFjdC9mb3Jtcy5odG1sI2Zvcm0tY29udGVudC10eXBlIC0tIGFwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZFxuICAgIC8vIGRvbid0IGFwcGVuZCBcIj1cIiBmb3IgbnVsbCB2YWx1ZXMsIGFjY29yZGluZyB0byBodHRwOi8vZHZjcy53My5vcmcvaGcvdXJsL3Jhdy1maWxlL3RpcC9PdmVydmlldy5odG1sI3VybC1wYXJhbWV0ZXItc2VyaWFsaXphdGlvblxuICAgIHJldHVybiBVUkkuZW5jb2RlUXVlcnkobmFtZSwgZXNjYXBlUXVlcnlTcGFjZSkgKyAodmFsdWUgIT09IG51bGwgPyAnPScgKyBVUkkuZW5jb2RlUXVlcnkodmFsdWUsIGVzY2FwZVF1ZXJ5U3BhY2UpIDogJycpO1xuICB9O1xuXG4gIFVSSS5hZGRRdWVyeSA9IGZ1bmN0aW9uKGRhdGEsIG5hbWUsIHZhbHVlKSB7XG4gICAgaWYgKHR5cGVvZiBuYW1lID09PSAnb2JqZWN0Jykge1xuICAgICAgZm9yICh2YXIga2V5IGluIG5hbWUpIHtcbiAgICAgICAgaWYgKGhhc093bi5jYWxsKG5hbWUsIGtleSkpIHtcbiAgICAgICAgICBVUkkuYWRkUXVlcnkoZGF0YSwga2V5LCBuYW1lW2tleV0pO1xuICAgICAgICB9XG4gICAgICB9XG4gICAgfSBlbHNlIGlmICh0eXBlb2YgbmFtZSA9PT0gJ3N0cmluZycpIHtcbiAgICAgIGlmIChkYXRhW25hbWVdID09PSB1bmRlZmluZWQpIHtcbiAgICAgICAgZGF0YVtuYW1lXSA9IHZhbHVlO1xuICAgICAgICByZXR1cm47XG4gICAgICB9IGVsc2UgaWYgKHR5cGVvZiBkYXRhW25hbWVdID09PSAnc3RyaW5nJykge1xuICAgICAgICBkYXRhW25hbWVdID0gW2RhdGFbbmFtZV1dO1xuICAgICAgfVxuXG4gICAgICBpZiAoIWlzQXJyYXkodmFsdWUpKSB7XG4gICAgICAgIHZhbHVlID0gW3ZhbHVlXTtcbiAgICAgIH1cblxuICAgICAgZGF0YVtuYW1lXSA9IChkYXRhW25hbWVdIHx8IFtdKS5jb25jYXQodmFsdWUpO1xuICAgIH0gZWxzZSB7XG4gICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdVUkkuYWRkUXVlcnkoKSBhY2NlcHRzIGFuIG9iamVjdCwgc3RyaW5nIGFzIHRoZSBuYW1lIHBhcmFtZXRlcicpO1xuICAgIH1cbiAgfTtcbiAgVVJJLnJlbW92ZVF1ZXJ5ID0gZnVuY3Rpb24oZGF0YSwgbmFtZSwgdmFsdWUpIHtcbiAgICB2YXIgaSwgbGVuZ3RoLCBrZXk7XG5cbiAgICBpZiAoaXNBcnJheShuYW1lKSkge1xuICAgICAgZm9yIChpID0gMCwgbGVuZ3RoID0gbmFtZS5sZW5ndGg7IGkgPCBsZW5ndGg7IGkrKykge1xuICAgICAgICBkYXRhW25hbWVbaV1dID0gdW5kZWZpbmVkO1xuICAgICAgfVxuICAgIH0gZWxzZSBpZiAoZ2V0VHlwZShuYW1lKSA9PT0gJ1JlZ0V4cCcpIHtcbiAgICAgIGZvciAoa2V5IGluIGRhdGEpIHtcbiAgICAgICAgaWYgKG5hbWUudGVzdChrZXkpKSB7XG4gICAgICAgICAgZGF0YVtrZXldID0gdW5kZWZpbmVkO1xuICAgICAgICB9XG4gICAgICB9XG4gICAgfSBlbHNlIGlmICh0eXBlb2YgbmFtZSA9PT0gJ29iamVjdCcpIHtcbiAgICAgIGZvciAoa2V5IGluIG5hbWUpIHtcbiAgICAgICAgaWYgKGhhc093bi5jYWxsKG5hbWUsIGtleSkpIHtcbiAgICAgICAgICBVUkkucmVtb3ZlUXVlcnkoZGF0YSwga2V5LCBuYW1lW2tleV0pO1xuICAgICAgICB9XG4gICAgICB9XG4gICAgfSBlbHNlIGlmICh0eXBlb2YgbmFtZSA9PT0gJ3N0cmluZycpIHtcbiAgICAgIGlmICh2YWx1ZSAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIGlmIChnZXRUeXBlKHZhbHVlKSA9PT0gJ1JlZ0V4cCcpIHtcbiAgICAgICAgICBpZiAoIWlzQXJyYXkoZGF0YVtuYW1lXSkgJiYgdmFsdWUudGVzdChkYXRhW25hbWVdKSkge1xuICAgICAgICAgICAgZGF0YVtuYW1lXSA9IHVuZGVmaW5lZDtcbiAgICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgICAgZGF0YVtuYW1lXSA9IGZpbHRlckFycmF5VmFsdWVzKGRhdGFbbmFtZV0sIHZhbHVlKTtcbiAgICAgICAgICB9XG4gICAgICAgIH0gZWxzZSBpZiAoZGF0YVtuYW1lXSA9PT0gU3RyaW5nKHZhbHVlKSAmJiAoIWlzQXJyYXkodmFsdWUpIHx8IHZhbHVlLmxlbmd0aCA9PT0gMSkpIHtcbiAgICAgICAgICBkYXRhW25hbWVdID0gdW5kZWZpbmVkO1xuICAgICAgICB9IGVsc2UgaWYgKGlzQXJyYXkoZGF0YVtuYW1lXSkpIHtcbiAgICAgICAgICBkYXRhW25hbWVdID0gZmlsdGVyQXJyYXlWYWx1ZXMoZGF0YVtuYW1lXSwgdmFsdWUpO1xuICAgICAgICB9XG4gICAgICB9IGVsc2Uge1xuICAgICAgICBkYXRhW25hbWVdID0gdW5kZWZpbmVkO1xuICAgICAgfVxuICAgIH0gZWxzZSB7XG4gICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdVUkkucmVtb3ZlUXVlcnkoKSBhY2NlcHRzIGFuIG9iamVjdCwgc3RyaW5nLCBSZWdFeHAgYXMgdGhlIGZpcnN0IHBhcmFtZXRlcicpO1xuICAgIH1cbiAgfTtcbiAgVVJJLmhhc1F1ZXJ5ID0gZnVuY3Rpb24oZGF0YSwgbmFtZSwgdmFsdWUsIHdpdGhpbkFycmF5KSB7XG4gICAgaWYgKHR5cGVvZiBuYW1lID09PSAnb2JqZWN0Jykge1xuICAgICAgZm9yICh2YXIga2V5IGluIG5hbWUpIHtcbiAgICAgICAgaWYgKGhhc093bi5jYWxsKG5hbWUsIGtleSkpIHtcbiAgICAgICAgICBpZiAoIVVSSS5oYXNRdWVyeShkYXRhLCBrZXksIG5hbWVba2V5XSkpIHtcbiAgICAgICAgICAgIHJldHVybiBmYWxzZTtcbiAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgIH1cblxuICAgICAgcmV0dXJuIHRydWU7XG4gICAgfSBlbHNlIGlmICh0eXBlb2YgbmFtZSAhPT0gJ3N0cmluZycpIHtcbiAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ1VSSS5oYXNRdWVyeSgpIGFjY2VwdHMgYW4gb2JqZWN0LCBzdHJpbmcgYXMgdGhlIG5hbWUgcGFyYW1ldGVyJyk7XG4gICAgfVxuXG4gICAgc3dpdGNoIChnZXRUeXBlKHZhbHVlKSkge1xuICAgICAgY2FzZSAnVW5kZWZpbmVkJzpcbiAgICAgICAgLy8gdHJ1ZSBpZiBleGlzdHMgKGJ1dCBtYXkgYmUgZW1wdHkpXG4gICAgICAgIHJldHVybiBuYW1lIGluIGRhdGE7IC8vIGRhdGFbbmFtZV0gIT09IHVuZGVmaW5lZDtcblxuICAgICAgY2FzZSAnQm9vbGVhbic6XG4gICAgICAgIC8vIHRydWUgaWYgZXhpc3RzIGFuZCBub24tZW1wdHlcbiAgICAgICAgdmFyIF9ib29seSA9IEJvb2xlYW4oaXNBcnJheShkYXRhW25hbWVdKSA/IGRhdGFbbmFtZV0ubGVuZ3RoIDogZGF0YVtuYW1lXSk7XG4gICAgICAgIHJldHVybiB2YWx1ZSA9PT0gX2Jvb2x5O1xuXG4gICAgICBjYXNlICdGdW5jdGlvbic6XG4gICAgICAgIC8vIGFsbG93IGNvbXBsZXggY29tcGFyaXNvblxuICAgICAgICByZXR1cm4gISF2YWx1ZShkYXRhW25hbWVdLCBuYW1lLCBkYXRhKTtcblxuICAgICAgY2FzZSAnQXJyYXknOlxuICAgICAgICBpZiAoIWlzQXJyYXkoZGF0YVtuYW1lXSkpIHtcbiAgICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgICAgIH1cblxuICAgICAgICB2YXIgb3AgPSB3aXRoaW5BcnJheSA/IGFycmF5Q29udGFpbnMgOiBhcnJheXNFcXVhbDtcbiAgICAgICAgcmV0dXJuIG9wKGRhdGFbbmFtZV0sIHZhbHVlKTtcblxuICAgICAgY2FzZSAnUmVnRXhwJzpcbiAgICAgICAgaWYgKCFpc0FycmF5KGRhdGFbbmFtZV0pKSB7XG4gICAgICAgICAgcmV0dXJuIEJvb2xlYW4oZGF0YVtuYW1lXSAmJiBkYXRhW25hbWVdLm1hdGNoKHZhbHVlKSk7XG4gICAgICAgIH1cblxuICAgICAgICBpZiAoIXdpdGhpbkFycmF5KSB7XG4gICAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgICAgICB9XG5cbiAgICAgICAgcmV0dXJuIGFycmF5Q29udGFpbnMoZGF0YVtuYW1lXSwgdmFsdWUpO1xuXG4gICAgICBjYXNlICdOdW1iZXInOlxuICAgICAgICB2YWx1ZSA9IFN0cmluZyh2YWx1ZSk7XG4gICAgICAgIC8qIGZhbGxzIHRocm91Z2ggKi9cbiAgICAgIGNhc2UgJ1N0cmluZyc6XG4gICAgICAgIGlmICghaXNBcnJheShkYXRhW25hbWVdKSkge1xuICAgICAgICAgIHJldHVybiBkYXRhW25hbWVdID09PSB2YWx1ZTtcbiAgICAgICAgfVxuXG4gICAgICAgIGlmICghd2l0aGluQXJyYXkpIHtcbiAgICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgICAgIH1cblxuICAgICAgICByZXR1cm4gYXJyYXlDb250YWlucyhkYXRhW25hbWVdLCB2YWx1ZSk7XG5cbiAgICAgIGRlZmF1bHQ6XG4gICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ1VSSS5oYXNRdWVyeSgpIGFjY2VwdHMgdW5kZWZpbmVkLCBib29sZWFuLCBzdHJpbmcsIG51bWJlciwgUmVnRXhwLCBGdW5jdGlvbiBhcyB0aGUgdmFsdWUgcGFyYW1ldGVyJyk7XG4gICAgfVxuICB9O1xuXG5cbiAgVVJJLmNvbW1vblBhdGggPSBmdW5jdGlvbihvbmUsIHR3bykge1xuICAgIHZhciBsZW5ndGggPSBNYXRoLm1pbihvbmUubGVuZ3RoLCB0d28ubGVuZ3RoKTtcbiAgICB2YXIgcG9zO1xuXG4gICAgLy8gZmluZCBmaXJzdCBub24tbWF0Y2hpbmcgY2hhcmFjdGVyXG4gICAgZm9yIChwb3MgPSAwOyBwb3MgPCBsZW5ndGg7IHBvcysrKSB7XG4gICAgICBpZiAob25lLmNoYXJBdChwb3MpICE9PSB0d28uY2hhckF0KHBvcykpIHtcbiAgICAgICAgcG9zLS07XG4gICAgICAgIGJyZWFrO1xuICAgICAgfVxuICAgIH1cblxuICAgIGlmIChwb3MgPCAxKSB7XG4gICAgICByZXR1cm4gb25lLmNoYXJBdCgwKSA9PT0gdHdvLmNoYXJBdCgwKSAmJiBvbmUuY2hhckF0KDApID09PSAnLycgPyAnLycgOiAnJztcbiAgICB9XG5cbiAgICAvLyByZXZlcnQgdG8gbGFzdCAvXG4gICAgaWYgKG9uZS5jaGFyQXQocG9zKSAhPT0gJy8nIHx8IHR3by5jaGFyQXQocG9zKSAhPT0gJy8nKSB7XG4gICAgICBwb3MgPSBvbmUuc3Vic3RyaW5nKDAsIHBvcykubGFzdEluZGV4T2YoJy8nKTtcbiAgICB9XG5cbiAgICByZXR1cm4gb25lLnN1YnN0cmluZygwLCBwb3MgKyAxKTtcbiAgfTtcblxuICBVUkkud2l0aGluU3RyaW5nID0gZnVuY3Rpb24oc3RyaW5nLCBjYWxsYmFjaywgb3B0aW9ucykge1xuICAgIG9wdGlvbnMgfHwgKG9wdGlvbnMgPSB7fSk7XG4gICAgdmFyIF9zdGFydCA9IG9wdGlvbnMuc3RhcnQgfHwgVVJJLmZpbmRVcmkuc3RhcnQ7XG4gICAgdmFyIF9lbmQgPSBvcHRpb25zLmVuZCB8fCBVUkkuZmluZFVyaS5lbmQ7XG4gICAgdmFyIF90cmltID0gb3B0aW9ucy50cmltIHx8IFVSSS5maW5kVXJpLnRyaW07XG4gICAgdmFyIF9hdHRyaWJ1dGVPcGVuID0gL1thLXowLTktXT1bXCInXT8kL2k7XG5cbiAgICBfc3RhcnQubGFzdEluZGV4ID0gMDtcbiAgICB3aGlsZSAodHJ1ZSkge1xuICAgICAgdmFyIG1hdGNoID0gX3N0YXJ0LmV4ZWMoc3RyaW5nKTtcbiAgICAgIGlmICghbWF0Y2gpIHtcbiAgICAgICAgYnJlYWs7XG4gICAgICB9XG5cbiAgICAgIHZhciBzdGFydCA9IG1hdGNoLmluZGV4O1xuICAgICAgaWYgKG9wdGlvbnMuaWdub3JlSHRtbCkge1xuICAgICAgICAvLyBhdHRyaWJ1dChlPVtcIiddPyQpXG4gICAgICAgIHZhciBhdHRyaWJ1dGVPcGVuID0gc3RyaW5nLnNsaWNlKE1hdGgubWF4KHN0YXJ0IC0gMywgMCksIHN0YXJ0KTtcbiAgICAgICAgaWYgKGF0dHJpYnV0ZU9wZW4gJiYgX2F0dHJpYnV0ZU9wZW4udGVzdChhdHRyaWJ1dGVPcGVuKSkge1xuICAgICAgICAgIGNvbnRpbnVlO1xuICAgICAgICB9XG4gICAgICB9XG5cbiAgICAgIHZhciBlbmQgPSBzdGFydCArIHN0cmluZy5zbGljZShzdGFydCkuc2VhcmNoKF9lbmQpO1xuICAgICAgdmFyIHNsaWNlID0gc3RyaW5nLnNsaWNlKHN0YXJ0LCBlbmQpLnJlcGxhY2UoX3RyaW0sICcnKTtcbiAgICAgIGlmIChvcHRpb25zLmlnbm9yZSAmJiBvcHRpb25zLmlnbm9yZS50ZXN0KHNsaWNlKSkge1xuICAgICAgICBjb250aW51ZTtcbiAgICAgIH1cblxuICAgICAgZW5kID0gc3RhcnQgKyBzbGljZS5sZW5ndGg7XG4gICAgICB2YXIgcmVzdWx0ID0gY2FsbGJhY2soc2xpY2UsIHN0YXJ0LCBlbmQsIHN0cmluZyk7XG4gICAgICBzdHJpbmcgPSBzdHJpbmcuc2xpY2UoMCwgc3RhcnQpICsgcmVzdWx0ICsgc3RyaW5nLnNsaWNlKGVuZCk7XG4gICAgICBfc3RhcnQubGFzdEluZGV4ID0gc3RhcnQgKyByZXN1bHQubGVuZ3RoO1xuICAgIH1cblxuICAgIF9zdGFydC5sYXN0SW5kZXggPSAwO1xuICAgIHJldHVybiBzdHJpbmc7XG4gIH07XG5cbiAgVVJJLmVuc3VyZVZhbGlkSG9zdG5hbWUgPSBmdW5jdGlvbih2KSB7XG4gICAgLy8gVGhlb3JldGljYWxseSBVUklzIGFsbG93IHBlcmNlbnQtZW5jb2RpbmcgaW4gSG9zdG5hbWVzIChhY2NvcmRpbmcgdG8gUkZDIDM5ODYpXG4gICAgLy8gdGhleSBhcmUgbm90IHBhcnQgb2YgRE5TIGFuZCB0aGVyZWZvcmUgaWdub3JlZCBieSBVUkkuanNcblxuICAgIGlmICh2Lm1hdGNoKFVSSS5pbnZhbGlkX2hvc3RuYW1lX2NoYXJhY3RlcnMpKSB7XG4gICAgICAvLyB0ZXN0IHB1bnljb2RlXG4gICAgICBpZiAoIXB1bnljb2RlKSB7XG4gICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ0hvc3RuYW1lIFwiJyArIHYgKyAnXCIgY29udGFpbnMgY2hhcmFjdGVycyBvdGhlciB0aGFuIFtBLVowLTkuLV0gYW5kIFB1bnljb2RlLmpzIGlzIG5vdCBhdmFpbGFibGUnKTtcbiAgICAgIH1cblxuICAgICAgaWYgKHB1bnljb2RlLnRvQVNDSUkodikubWF0Y2goVVJJLmludmFsaWRfaG9zdG5hbWVfY2hhcmFjdGVycykpIHtcbiAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcignSG9zdG5hbWUgXCInICsgdiArICdcIiBjb250YWlucyBjaGFyYWN0ZXJzIG90aGVyIHRoYW4gW0EtWjAtOS4tXScpO1xuICAgICAgfVxuICAgIH1cbiAgfTtcblxuICAvLyBub0NvbmZsaWN0XG4gIFVSSS5ub0NvbmZsaWN0ID0gZnVuY3Rpb24ocmVtb3ZlQWxsKSB7XG4gICAgaWYgKHJlbW92ZUFsbCkge1xuICAgICAgdmFyIHVuY29uZmxpY3RlZCA9IHtcbiAgICAgICAgVVJJOiB0aGlzLm5vQ29uZmxpY3QoKVxuICAgICAgfTtcblxuICAgICAgaWYgKHJvb3QuVVJJVGVtcGxhdGUgJiYgdHlwZW9mIHJvb3QuVVJJVGVtcGxhdGUubm9Db25mbGljdCA9PT0gJ2Z1bmN0aW9uJykge1xuICAgICAgICB1bmNvbmZsaWN0ZWQuVVJJVGVtcGxhdGUgPSByb290LlVSSVRlbXBsYXRlLm5vQ29uZmxpY3QoKTtcbiAgICAgIH1cblxuICAgICAgaWYgKHJvb3QuSVB2NiAmJiB0eXBlb2Ygcm9vdC5JUHY2Lm5vQ29uZmxpY3QgPT09ICdmdW5jdGlvbicpIHtcbiAgICAgICAgdW5jb25mbGljdGVkLklQdjYgPSByb290LklQdjYubm9Db25mbGljdCgpO1xuICAgICAgfVxuXG4gICAgICBpZiAocm9vdC5TZWNvbmRMZXZlbERvbWFpbnMgJiYgdHlwZW9mIHJvb3QuU2Vjb25kTGV2ZWxEb21haW5zLm5vQ29uZmxpY3QgPT09ICdmdW5jdGlvbicpIHtcbiAgICAgICAgdW5jb25mbGljdGVkLlNlY29uZExldmVsRG9tYWlucyA9IHJvb3QuU2Vjb25kTGV2ZWxEb21haW5zLm5vQ29uZmxpY3QoKTtcbiAgICAgIH1cblxuICAgICAgcmV0dXJuIHVuY29uZmxpY3RlZDtcbiAgICB9IGVsc2UgaWYgKHJvb3QuVVJJID09PSB0aGlzKSB7XG4gICAgICByb290LlVSSSA9IF9VUkk7XG4gICAgfVxuXG4gICAgcmV0dXJuIHRoaXM7XG4gIH07XG5cbiAgcC5idWlsZCA9IGZ1bmN0aW9uKGRlZmVyQnVpbGQpIHtcbiAgICBpZiAoZGVmZXJCdWlsZCA9PT0gdHJ1ZSkge1xuICAgICAgdGhpcy5fZGVmZXJyZWRfYnVpbGQgPSB0cnVlO1xuICAgIH0gZWxzZSBpZiAoZGVmZXJCdWlsZCA9PT0gdW5kZWZpbmVkIHx8IHRoaXMuX2RlZmVycmVkX2J1aWxkKSB7XG4gICAgICB0aGlzLl9zdHJpbmcgPSBVUkkuYnVpbGQodGhpcy5fcGFydHMpO1xuICAgICAgdGhpcy5fZGVmZXJyZWRfYnVpbGQgPSBmYWxzZTtcbiAgICB9XG5cbiAgICByZXR1cm4gdGhpcztcbiAgfTtcblxuICBwLmNsb25lID0gZnVuY3Rpb24oKSB7XG4gICAgcmV0dXJuIG5ldyBVUkkodGhpcyk7XG4gIH07XG5cbiAgcC52YWx1ZU9mID0gcC50b1N0cmluZyA9IGZ1bmN0aW9uKCkge1xuICAgIHJldHVybiB0aGlzLmJ1aWxkKGZhbHNlKS5fc3RyaW5nO1xuICB9O1xuXG5cbiAgZnVuY3Rpb24gZ2VuZXJhdGVTaW1wbGVBY2Nlc3NvcihfcGFydCl7XG4gICAgcmV0dXJuIGZ1bmN0aW9uKHYsIGJ1aWxkKSB7XG4gICAgICBpZiAodiA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIHJldHVybiB0aGlzLl9wYXJ0c1tfcGFydF0gfHwgJyc7XG4gICAgICB9IGVsc2Uge1xuICAgICAgICB0aGlzLl9wYXJ0c1tfcGFydF0gPSB2IHx8IG51bGw7XG4gICAgICAgIHRoaXMuYnVpbGQoIWJ1aWxkKTtcbiAgICAgICAgcmV0dXJuIHRoaXM7XG4gICAgICB9XG4gICAgfTtcbiAgfVxuXG4gIGZ1bmN0aW9uIGdlbmVyYXRlUHJlZml4QWNjZXNzb3IoX3BhcnQsIF9rZXkpe1xuICAgIHJldHVybiBmdW5jdGlvbih2LCBidWlsZCkge1xuICAgICAgaWYgKHYgPT09IHVuZGVmaW5lZCkge1xuICAgICAgICByZXR1cm4gdGhpcy5fcGFydHNbX3BhcnRdIHx8ICcnO1xuICAgICAgfSBlbHNlIHtcbiAgICAgICAgaWYgKHYgIT09IG51bGwpIHtcbiAgICAgICAgICB2ID0gdiArICcnO1xuICAgICAgICAgIGlmICh2LmNoYXJBdCgwKSA9PT0gX2tleSkge1xuICAgICAgICAgICAgdiA9IHYuc3Vic3RyaW5nKDEpO1xuICAgICAgICAgIH1cbiAgICAgICAgfVxuXG4gICAgICAgIHRoaXMuX3BhcnRzW19wYXJ0XSA9IHY7XG4gICAgICAgIHRoaXMuYnVpbGQoIWJ1aWxkKTtcbiAgICAgICAgcmV0dXJuIHRoaXM7XG4gICAgICB9XG4gICAgfTtcbiAgfVxuXG4gIHAucHJvdG9jb2wgPSBnZW5lcmF0ZVNpbXBsZUFjY2Vzc29yKCdwcm90b2NvbCcpO1xuICBwLnVzZXJuYW1lID0gZ2VuZXJhdGVTaW1wbGVBY2Nlc3NvcigndXNlcm5hbWUnKTtcbiAgcC5wYXNzd29yZCA9IGdlbmVyYXRlU2ltcGxlQWNjZXNzb3IoJ3Bhc3N3b3JkJyk7XG4gIHAuaG9zdG5hbWUgPSBnZW5lcmF0ZVNpbXBsZUFjY2Vzc29yKCdob3N0bmFtZScpO1xuICBwLnBvcnQgPSBnZW5lcmF0ZVNpbXBsZUFjY2Vzc29yKCdwb3J0Jyk7XG4gIHAucXVlcnkgPSBnZW5lcmF0ZVByZWZpeEFjY2Vzc29yKCdxdWVyeScsICc/Jyk7XG4gIHAuZnJhZ21lbnQgPSBnZW5lcmF0ZVByZWZpeEFjY2Vzc29yKCdmcmFnbWVudCcsICcjJyk7XG5cbiAgcC5zZWFyY2ggPSBmdW5jdGlvbih2LCBidWlsZCkge1xuICAgIHZhciB0ID0gdGhpcy5xdWVyeSh2LCBidWlsZCk7XG4gICAgcmV0dXJuIHR5cGVvZiB0ID09PSAnc3RyaW5nJyAmJiB0Lmxlbmd0aCA/ICgnPycgKyB0KSA6IHQ7XG4gIH07XG4gIHAuaGFzaCA9IGZ1bmN0aW9uKHYsIGJ1aWxkKSB7XG4gICAgdmFyIHQgPSB0aGlzLmZyYWdtZW50KHYsIGJ1aWxkKTtcbiAgICByZXR1cm4gdHlwZW9mIHQgPT09ICdzdHJpbmcnICYmIHQubGVuZ3RoID8gKCcjJyArIHQpIDogdDtcbiAgfTtcblxuICBwLnBhdGhuYW1lID0gZnVuY3Rpb24odiwgYnVpbGQpIHtcbiAgICBpZiAodiA9PT0gdW5kZWZpbmVkIHx8IHYgPT09IHRydWUpIHtcbiAgICAgIHZhciByZXMgPSB0aGlzLl9wYXJ0cy5wYXRoIHx8ICh0aGlzLl9wYXJ0cy5ob3N0bmFtZSA/ICcvJyA6ICcnKTtcbiAgICAgIHJldHVybiB2ID8gKHRoaXMuX3BhcnRzLnVybiA/IFVSSS5kZWNvZGVVcm5QYXRoIDogVVJJLmRlY29kZVBhdGgpKHJlcykgOiByZXM7XG4gICAgfSBlbHNlIHtcbiAgICAgIGlmICh0aGlzLl9wYXJ0cy51cm4pIHtcbiAgICAgICAgdGhpcy5fcGFydHMucGF0aCA9IHYgPyBVUkkucmVjb2RlVXJuUGF0aCh2KSA6ICcnO1xuICAgICAgfSBlbHNlIHtcbiAgICAgICAgdGhpcy5fcGFydHMucGF0aCA9IHYgPyBVUkkucmVjb2RlUGF0aCh2KSA6ICcvJztcbiAgICAgIH1cbiAgICAgIHRoaXMuYnVpbGQoIWJ1aWxkKTtcbiAgICAgIHJldHVybiB0aGlzO1xuICAgIH1cbiAgfTtcbiAgcC5wYXRoID0gcC5wYXRobmFtZTtcbiAgcC5ocmVmID0gZnVuY3Rpb24oaHJlZiwgYnVpbGQpIHtcbiAgICB2YXIga2V5O1xuXG4gICAgaWYgKGhyZWYgPT09IHVuZGVmaW5lZCkge1xuICAgICAgcmV0dXJuIHRoaXMudG9TdHJpbmcoKTtcbiAgICB9XG5cbiAgICB0aGlzLl9zdHJpbmcgPSAnJztcbiAgICB0aGlzLl9wYXJ0cyA9IFVSSS5fcGFydHMoKTtcblxuICAgIHZhciBfVVJJID0gaHJlZiBpbnN0YW5jZW9mIFVSSTtcbiAgICB2YXIgX29iamVjdCA9IHR5cGVvZiBocmVmID09PSAnb2JqZWN0JyAmJiAoaHJlZi5ob3N0bmFtZSB8fCBocmVmLnBhdGggfHwgaHJlZi5wYXRobmFtZSk7XG4gICAgaWYgKGhyZWYubm9kZU5hbWUpIHtcbiAgICAgIHZhciBhdHRyaWJ1dGUgPSBVUkkuZ2V0RG9tQXR0cmlidXRlKGhyZWYpO1xuICAgICAgaHJlZiA9IGhyZWZbYXR0cmlidXRlXSB8fCAnJztcbiAgICAgIF9vYmplY3QgPSBmYWxzZTtcbiAgICB9XG5cbiAgICAvLyB3aW5kb3cubG9jYXRpb24gaXMgcmVwb3J0ZWQgdG8gYmUgYW4gb2JqZWN0LCBidXQgaXQncyBub3QgdGhlIHNvcnRcbiAgICAvLyBvZiBvYmplY3Qgd2UncmUgbG9va2luZyBmb3I6XG4gICAgLy8gKiBsb2NhdGlvbi5wcm90b2NvbCBlbmRzIHdpdGggYSBjb2xvblxuICAgIC8vICogbG9jYXRpb24ucXVlcnkgIT0gb2JqZWN0LnNlYXJjaFxuICAgIC8vICogbG9jYXRpb24uaGFzaCAhPSBvYmplY3QuZnJhZ21lbnRcbiAgICAvLyBzaW1wbHkgc2VyaWFsaXppbmcgdGhlIHVua25vd24gb2JqZWN0IHNob3VsZCBkbyB0aGUgdHJpY2tcbiAgICAvLyAoZm9yIGxvY2F0aW9uLCBub3QgZm9yIGV2ZXJ5dGhpbmcuLi4pXG4gICAgaWYgKCFfVVJJICYmIF9vYmplY3QgJiYgaHJlZi5wYXRobmFtZSAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICBocmVmID0gaHJlZi50b1N0cmluZygpO1xuICAgIH1cblxuICAgIGlmICh0eXBlb2YgaHJlZiA9PT0gJ3N0cmluZycgfHwgaHJlZiBpbnN0YW5jZW9mIFN0cmluZykge1xuICAgICAgdGhpcy5fcGFydHMgPSBVUkkucGFyc2UoU3RyaW5nKGhyZWYpLCB0aGlzLl9wYXJ0cyk7XG4gICAgfSBlbHNlIGlmIChfVVJJIHx8IF9vYmplY3QpIHtcbiAgICAgIHZhciBzcmMgPSBfVVJJID8gaHJlZi5fcGFydHMgOiBocmVmO1xuICAgICAgZm9yIChrZXkgaW4gc3JjKSB7XG4gICAgICAgIGlmIChoYXNPd24uY2FsbCh0aGlzLl9wYXJ0cywga2V5KSkge1xuICAgICAgICAgIHRoaXMuX3BhcnRzW2tleV0gPSBzcmNba2V5XTtcbiAgICAgICAgfVxuICAgICAgfVxuICAgIH0gZWxzZSB7XG4gICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdpbnZhbGlkIGlucHV0Jyk7XG4gICAgfVxuXG4gICAgdGhpcy5idWlsZCghYnVpbGQpO1xuICAgIHJldHVybiB0aGlzO1xuICB9O1xuXG4gIC8vIGlkZW50aWZpY2F0aW9uIGFjY2Vzc29yc1xuICBwLmlzID0gZnVuY3Rpb24od2hhdCkge1xuICAgIHZhciBpcCA9IGZhbHNlO1xuICAgIHZhciBpcDQgPSBmYWxzZTtcbiAgICB2YXIgaXA2ID0gZmFsc2U7XG4gICAgdmFyIG5hbWUgPSBmYWxzZTtcbiAgICB2YXIgc2xkID0gZmFsc2U7XG4gICAgdmFyIGlkbiA9IGZhbHNlO1xuICAgIHZhciBwdW55Y29kZSA9IGZhbHNlO1xuICAgIHZhciByZWxhdGl2ZSA9ICF0aGlzLl9wYXJ0cy51cm47XG5cbiAgICBpZiAodGhpcy5fcGFydHMuaG9zdG5hbWUpIHtcbiAgICAgIHJlbGF0aXZlID0gZmFsc2U7XG4gICAgICBpcDQgPSBVUkkuaXA0X2V4cHJlc3Npb24udGVzdCh0aGlzLl9wYXJ0cy5ob3N0bmFtZSk7XG4gICAgICBpcDYgPSBVUkkuaXA2X2V4cHJlc3Npb24udGVzdCh0aGlzLl9wYXJ0cy5ob3N0bmFtZSk7XG4gICAgICBpcCA9IGlwNCB8fCBpcDY7XG4gICAgICBuYW1lID0gIWlwO1xuICAgICAgc2xkID0gbmFtZSAmJiBTTEQgJiYgU0xELmhhcyh0aGlzLl9wYXJ0cy5ob3N0bmFtZSk7XG4gICAgICBpZG4gPSBuYW1lICYmIFVSSS5pZG5fZXhwcmVzc2lvbi50ZXN0KHRoaXMuX3BhcnRzLmhvc3RuYW1lKTtcbiAgICAgIHB1bnljb2RlID0gbmFtZSAmJiBVUkkucHVueWNvZGVfZXhwcmVzc2lvbi50ZXN0KHRoaXMuX3BhcnRzLmhvc3RuYW1lKTtcbiAgICB9XG5cbiAgICBzd2l0Y2ggKHdoYXQudG9Mb3dlckNhc2UoKSkge1xuICAgICAgY2FzZSAncmVsYXRpdmUnOlxuICAgICAgICByZXR1cm4gcmVsYXRpdmU7XG5cbiAgICAgIGNhc2UgJ2Fic29sdXRlJzpcbiAgICAgICAgcmV0dXJuICFyZWxhdGl2ZTtcblxuICAgICAgLy8gaG9zdG5hbWUgaWRlbnRpZmljYXRpb25cbiAgICAgIGNhc2UgJ2RvbWFpbic6XG4gICAgICBjYXNlICduYW1lJzpcbiAgICAgICAgcmV0dXJuIG5hbWU7XG5cbiAgICAgIGNhc2UgJ3NsZCc6XG4gICAgICAgIHJldHVybiBzbGQ7XG5cbiAgICAgIGNhc2UgJ2lwJzpcbiAgICAgICAgcmV0dXJuIGlwO1xuXG4gICAgICBjYXNlICdpcDQnOlxuICAgICAgY2FzZSAnaXB2NCc6XG4gICAgICBjYXNlICdpbmV0NCc6XG4gICAgICAgIHJldHVybiBpcDQ7XG5cbiAgICAgIGNhc2UgJ2lwNic6XG4gICAgICBjYXNlICdpcHY2JzpcbiAgICAgIGNhc2UgJ2luZXQ2JzpcbiAgICAgICAgcmV0dXJuIGlwNjtcblxuICAgICAgY2FzZSAnaWRuJzpcbiAgICAgICAgcmV0dXJuIGlkbjtcblxuICAgICAgY2FzZSAndXJsJzpcbiAgICAgICAgcmV0dXJuICF0aGlzLl9wYXJ0cy51cm47XG5cbiAgICAgIGNhc2UgJ3Vybic6XG4gICAgICAgIHJldHVybiAhIXRoaXMuX3BhcnRzLnVybjtcblxuICAgICAgY2FzZSAncHVueWNvZGUnOlxuICAgICAgICByZXR1cm4gcHVueWNvZGU7XG4gICAgfVxuXG4gICAgcmV0dXJuIG51bGw7XG4gIH07XG5cbiAgLy8gY29tcG9uZW50IHNwZWNpZmljIGlucHV0IHZhbGlkYXRpb25cbiAgdmFyIF9wcm90b2NvbCA9IHAucHJvdG9jb2w7XG4gIHZhciBfcG9ydCA9IHAucG9ydDtcbiAgdmFyIF9ob3N0bmFtZSA9IHAuaG9zdG5hbWU7XG5cbiAgcC5wcm90b2NvbCA9IGZ1bmN0aW9uKHYsIGJ1aWxkKSB7XG4gICAgaWYgKHYgIT09IHVuZGVmaW5lZCkge1xuICAgICAgaWYgKHYpIHtcbiAgICAgICAgLy8gYWNjZXB0IHRyYWlsaW5nIDovL1xuICAgICAgICB2ID0gdi5yZXBsYWNlKC86KFxcL1xcLyk/JC8sICcnKTtcblxuICAgICAgICBpZiAoIXYubWF0Y2goVVJJLnByb3RvY29sX2V4cHJlc3Npb24pKSB7XG4gICAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcignUHJvdG9jb2wgXCInICsgdiArICdcIiBjb250YWlucyBjaGFyYWN0ZXJzIG90aGVyIHRoYW4gW0EtWjAtOS4rLV0gb3IgZG9lc25cXCd0IHN0YXJ0IHdpdGggW0EtWl0nKTtcbiAgICAgICAgfVxuICAgICAgfVxuICAgIH1cbiAgICByZXR1cm4gX3Byb3RvY29sLmNhbGwodGhpcywgdiwgYnVpbGQpO1xuICB9O1xuICBwLnNjaGVtZSA9IHAucHJvdG9jb2w7XG4gIHAucG9ydCA9IGZ1bmN0aW9uKHYsIGJ1aWxkKSB7XG4gICAgaWYgKHRoaXMuX3BhcnRzLnVybikge1xuICAgICAgcmV0dXJuIHYgPT09IHVuZGVmaW5lZCA/ICcnIDogdGhpcztcbiAgICB9XG5cbiAgICBpZiAodiAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICBpZiAodiA9PT0gMCkge1xuICAgICAgICB2ID0gbnVsbDtcbiAgICAgIH1cblxuICAgICAgaWYgKHYpIHtcbiAgICAgICAgdiArPSAnJztcbiAgICAgICAgaWYgKHYuY2hhckF0KDApID09PSAnOicpIHtcbiAgICAgICAgICB2ID0gdi5zdWJzdHJpbmcoMSk7XG4gICAgICAgIH1cblxuICAgICAgICBpZiAodi5tYXRjaCgvW14wLTldLykpIHtcbiAgICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdQb3J0IFwiJyArIHYgKyAnXCIgY29udGFpbnMgY2hhcmFjdGVycyBvdGhlciB0aGFuIFswLTldJyk7XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9XG4gICAgcmV0dXJuIF9wb3J0LmNhbGwodGhpcywgdiwgYnVpbGQpO1xuICB9O1xuICBwLmhvc3RuYW1lID0gZnVuY3Rpb24odiwgYnVpbGQpIHtcbiAgICBpZiAodGhpcy5fcGFydHMudXJuKSB7XG4gICAgICByZXR1cm4gdiA9PT0gdW5kZWZpbmVkID8gJycgOiB0aGlzO1xuICAgIH1cblxuICAgIGlmICh2ICE9PSB1bmRlZmluZWQpIHtcbiAgICAgIHZhciB4ID0ge307XG4gICAgICB2YXIgcmVzID0gVVJJLnBhcnNlSG9zdCh2LCB4KTtcbiAgICAgIGlmIChyZXMgIT09ICcvJykge1xuICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdIb3N0bmFtZSBcIicgKyB2ICsgJ1wiIGNvbnRhaW5zIGNoYXJhY3RlcnMgb3RoZXIgdGhhbiBbQS1aMC05Li1dJyk7XG4gICAgICB9XG5cbiAgICAgIHYgPSB4Lmhvc3RuYW1lO1xuICAgIH1cbiAgICByZXR1cm4gX2hvc3RuYW1lLmNhbGwodGhpcywgdiwgYnVpbGQpO1xuICB9O1xuXG4gIC8vIGNvbXBvdW5kIGFjY2Vzc29yc1xuICBwLm9yaWdpbiA9IGZ1bmN0aW9uKHYsIGJ1aWxkKSB7XG4gICAgdmFyIHBhcnRzO1xuXG4gICAgaWYgKHRoaXMuX3BhcnRzLnVybikge1xuICAgICAgcmV0dXJuIHYgPT09IHVuZGVmaW5lZCA/ICcnIDogdGhpcztcbiAgICB9XG5cbiAgICBpZiAodiA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICB2YXIgcHJvdG9jb2wgPSB0aGlzLnByb3RvY29sKCk7XG4gICAgICB2YXIgYXV0aG9yaXR5ID0gdGhpcy5hdXRob3JpdHkoKTtcbiAgICAgIGlmICghYXV0aG9yaXR5KSByZXR1cm4gJyc7XG4gICAgICByZXR1cm4gKHByb3RvY29sID8gcHJvdG9jb2wgKyAnOi8vJyA6ICcnKSArIHRoaXMuYXV0aG9yaXR5KCk7XG4gICAgfSBlbHNlIHtcbiAgICAgIHZhciBvcmlnaW4gPSBVUkkodik7XG4gICAgICB0aGlzXG4gICAgICAgIC5wcm90b2NvbChvcmlnaW4ucHJvdG9jb2woKSlcbiAgICAgICAgLmF1dGhvcml0eShvcmlnaW4uYXV0aG9yaXR5KCkpXG4gICAgICAgIC5idWlsZCghYnVpbGQpO1xuICAgICAgcmV0dXJuIHRoaXM7XG4gICAgfVxuICB9O1xuICBwLmhvc3QgPSBmdW5jdGlvbih2LCBidWlsZCkge1xuICAgIGlmICh0aGlzLl9wYXJ0cy51cm4pIHtcbiAgICAgIHJldHVybiB2ID09PSB1bmRlZmluZWQgPyAnJyA6IHRoaXM7XG4gICAgfVxuXG4gICAgaWYgKHYgPT09IHVuZGVmaW5lZCkge1xuICAgICAgcmV0dXJuIHRoaXMuX3BhcnRzLmhvc3RuYW1lID8gVVJJLmJ1aWxkSG9zdCh0aGlzLl9wYXJ0cykgOiAnJztcbiAgICB9IGVsc2Uge1xuICAgICAgdmFyIHJlcyA9IFVSSS5wYXJzZUhvc3QodiwgdGhpcy5fcGFydHMpO1xuICAgICAgaWYgKHJlcyAhPT0gJy8nKSB7XG4gICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ0hvc3RuYW1lIFwiJyArIHYgKyAnXCIgY29udGFpbnMgY2hhcmFjdGVycyBvdGhlciB0aGFuIFtBLVowLTkuLV0nKTtcbiAgICAgIH1cblxuICAgICAgdGhpcy5idWlsZCghYnVpbGQpO1xuICAgICAgcmV0dXJuIHRoaXM7XG4gICAgfVxuICB9O1xuICBwLmF1dGhvcml0eSA9IGZ1bmN0aW9uKHYsIGJ1aWxkKSB7XG4gICAgaWYgKHRoaXMuX3BhcnRzLnVybikge1xuICAgICAgcmV0dXJuIHYgPT09IHVuZGVmaW5lZCA/ICcnIDogdGhpcztcbiAgICB9XG5cbiAgICBpZiAodiA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICByZXR1cm4gdGhpcy5fcGFydHMuaG9zdG5hbWUgPyBVUkkuYnVpbGRBdXRob3JpdHkodGhpcy5fcGFydHMpIDogJyc7XG4gICAgfSBlbHNlIHtcbiAgICAgIHZhciByZXMgPSBVUkkucGFyc2VBdXRob3JpdHkodiwgdGhpcy5fcGFydHMpO1xuICAgICAgaWYgKHJlcyAhPT0gJy8nKSB7XG4gICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ0hvc3RuYW1lIFwiJyArIHYgKyAnXCIgY29udGFpbnMgY2hhcmFjdGVycyBvdGhlciB0aGFuIFtBLVowLTkuLV0nKTtcbiAgICAgIH1cblxuICAgICAgdGhpcy5idWlsZCghYnVpbGQpO1xuICAgICAgcmV0dXJuIHRoaXM7XG4gICAgfVxuICB9O1xuICBwLnVzZXJpbmZvID0gZnVuY3Rpb24odiwgYnVpbGQpIHtcbiAgICBpZiAodGhpcy5fcGFydHMudXJuKSB7XG4gICAgICByZXR1cm4gdiA9PT0gdW5kZWZpbmVkID8gJycgOiB0aGlzO1xuICAgIH1cblxuICAgIGlmICh2ID09PSB1bmRlZmluZWQpIHtcbiAgICAgIGlmICghdGhpcy5fcGFydHMudXNlcm5hbWUpIHtcbiAgICAgICAgcmV0dXJuICcnO1xuICAgICAgfVxuXG4gICAgICB2YXIgdCA9IFVSSS5idWlsZFVzZXJpbmZvKHRoaXMuX3BhcnRzKTtcbiAgICAgIHJldHVybiB0LnN1YnN0cmluZygwLCB0Lmxlbmd0aCAtMSk7XG4gICAgfSBlbHNlIHtcbiAgICAgIGlmICh2W3YubGVuZ3RoLTFdICE9PSAnQCcpIHtcbiAgICAgICAgdiArPSAnQCc7XG4gICAgICB9XG5cbiAgICAgIFVSSS5wYXJzZVVzZXJpbmZvKHYsIHRoaXMuX3BhcnRzKTtcbiAgICAgIHRoaXMuYnVpbGQoIWJ1aWxkKTtcbiAgICAgIHJldHVybiB0aGlzO1xuICAgIH1cbiAgfTtcbiAgcC5yZXNvdXJjZSA9IGZ1bmN0aW9uKHYsIGJ1aWxkKSB7XG4gICAgdmFyIHBhcnRzO1xuXG4gICAgaWYgKHYgPT09IHVuZGVmaW5lZCkge1xuICAgICAgcmV0dXJuIHRoaXMucGF0aCgpICsgdGhpcy5zZWFyY2goKSArIHRoaXMuaGFzaCgpO1xuICAgIH1cblxuICAgIHBhcnRzID0gVVJJLnBhcnNlKHYpO1xuICAgIHRoaXMuX3BhcnRzLnBhdGggPSBwYXJ0cy5wYXRoO1xuICAgIHRoaXMuX3BhcnRzLnF1ZXJ5ID0gcGFydHMucXVlcnk7XG4gICAgdGhpcy5fcGFydHMuZnJhZ21lbnQgPSBwYXJ0cy5mcmFnbWVudDtcbiAgICB0aGlzLmJ1aWxkKCFidWlsZCk7XG4gICAgcmV0dXJuIHRoaXM7XG4gIH07XG5cbiAgLy8gZnJhY3Rpb24gYWNjZXNzb3JzXG4gIHAuc3ViZG9tYWluID0gZnVuY3Rpb24odiwgYnVpbGQpIHtcbiAgICBpZiAodGhpcy5fcGFydHMudXJuKSB7XG4gICAgICByZXR1cm4gdiA9PT0gdW5kZWZpbmVkID8gJycgOiB0aGlzO1xuICAgIH1cblxuICAgIC8vIGNvbnZlbmllbmNlLCByZXR1cm4gXCJ3d3dcIiBmcm9tIFwid3d3LmV4YW1wbGUub3JnXCJcbiAgICBpZiAodiA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICBpZiAoIXRoaXMuX3BhcnRzLmhvc3RuYW1lIHx8IHRoaXMuaXMoJ0lQJykpIHtcbiAgICAgICAgcmV0dXJuICcnO1xuICAgICAgfVxuXG4gICAgICAvLyBncmFiIGRvbWFpbiBhbmQgYWRkIGFub3RoZXIgc2VnbWVudFxuICAgICAgdmFyIGVuZCA9IHRoaXMuX3BhcnRzLmhvc3RuYW1lLmxlbmd0aCAtIHRoaXMuZG9tYWluKCkubGVuZ3RoIC0gMTtcbiAgICAgIHJldHVybiB0aGlzLl9wYXJ0cy5ob3N0bmFtZS5zdWJzdHJpbmcoMCwgZW5kKSB8fCAnJztcbiAgICB9IGVsc2Uge1xuICAgICAgdmFyIGUgPSB0aGlzLl9wYXJ0cy5ob3N0bmFtZS5sZW5ndGggLSB0aGlzLmRvbWFpbigpLmxlbmd0aDtcbiAgICAgIHZhciBzdWIgPSB0aGlzLl9wYXJ0cy5ob3N0bmFtZS5zdWJzdHJpbmcoMCwgZSk7XG4gICAgICB2YXIgcmVwbGFjZSA9IG5ldyBSZWdFeHAoJ14nICsgZXNjYXBlUmVnRXgoc3ViKSk7XG5cbiAgICAgIGlmICh2ICYmIHYuY2hhckF0KHYubGVuZ3RoIC0gMSkgIT09ICcuJykge1xuICAgICAgICB2ICs9ICcuJztcbiAgICAgIH1cblxuICAgICAgaWYgKHYpIHtcbiAgICAgICAgVVJJLmVuc3VyZVZhbGlkSG9zdG5hbWUodik7XG4gICAgICB9XG5cbiAgICAgIHRoaXMuX3BhcnRzLmhvc3RuYW1lID0gdGhpcy5fcGFydHMuaG9zdG5hbWUucmVwbGFjZShyZXBsYWNlLCB2KTtcbiAgICAgIHRoaXMuYnVpbGQoIWJ1aWxkKTtcbiAgICAgIHJldHVybiB0aGlzO1xuICAgIH1cbiAgfTtcbiAgcC5kb21haW4gPSBmdW5jdGlvbih2LCBidWlsZCkge1xuICAgIGlmICh0aGlzLl9wYXJ0cy51cm4pIHtcbiAgICAgIHJldHVybiB2ID09PSB1bmRlZmluZWQgPyAnJyA6IHRoaXM7XG4gICAgfVxuXG4gICAgaWYgKHR5cGVvZiB2ID09PSAnYm9vbGVhbicpIHtcbiAgICAgIGJ1aWxkID0gdjtcbiAgICAgIHYgPSB1bmRlZmluZWQ7XG4gICAgfVxuXG4gICAgLy8gY29udmVuaWVuY2UsIHJldHVybiBcImV4YW1wbGUub3JnXCIgZnJvbSBcInd3dy5leGFtcGxlLm9yZ1wiXG4gICAgaWYgKHYgPT09IHVuZGVmaW5lZCkge1xuICAgICAgaWYgKCF0aGlzLl9wYXJ0cy5ob3N0bmFtZSB8fCB0aGlzLmlzKCdJUCcpKSB7XG4gICAgICAgIHJldHVybiAnJztcbiAgICAgIH1cblxuICAgICAgLy8gaWYgaG9zdG5hbWUgY29uc2lzdHMgb2YgMSBvciAyIHNlZ21lbnRzLCBpdCBtdXN0IGJlIHRoZSBkb21haW5cbiAgICAgIHZhciB0ID0gdGhpcy5fcGFydHMuaG9zdG5hbWUubWF0Y2goL1xcLi9nKTtcbiAgICAgIGlmICh0ICYmIHQubGVuZ3RoIDwgMikge1xuICAgICAgICByZXR1cm4gdGhpcy5fcGFydHMuaG9zdG5hbWU7XG4gICAgICB9XG5cbiAgICAgIC8vIGdyYWIgdGxkIGFuZCBhZGQgYW5vdGhlciBzZWdtZW50XG4gICAgICB2YXIgZW5kID0gdGhpcy5fcGFydHMuaG9zdG5hbWUubGVuZ3RoIC0gdGhpcy50bGQoYnVpbGQpLmxlbmd0aCAtIDE7XG4gICAgICBlbmQgPSB0aGlzLl9wYXJ0cy5ob3N0bmFtZS5sYXN0SW5kZXhPZignLicsIGVuZCAtMSkgKyAxO1xuICAgICAgcmV0dXJuIHRoaXMuX3BhcnRzLmhvc3RuYW1lLnN1YnN0cmluZyhlbmQpIHx8ICcnO1xuICAgIH0gZWxzZSB7XG4gICAgICBpZiAoIXYpIHtcbiAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcignY2Fubm90IHNldCBkb21haW4gZW1wdHknKTtcbiAgICAgIH1cblxuICAgICAgVVJJLmVuc3VyZVZhbGlkSG9zdG5hbWUodik7XG5cbiAgICAgIGlmICghdGhpcy5fcGFydHMuaG9zdG5hbWUgfHwgdGhpcy5pcygnSVAnKSkge1xuICAgICAgICB0aGlzLl9wYXJ0cy5ob3N0bmFtZSA9IHY7XG4gICAgICB9IGVsc2Uge1xuICAgICAgICB2YXIgcmVwbGFjZSA9IG5ldyBSZWdFeHAoZXNjYXBlUmVnRXgodGhpcy5kb21haW4oKSkgKyAnJCcpO1xuICAgICAgICB0aGlzLl9wYXJ0cy5ob3N0bmFtZSA9IHRoaXMuX3BhcnRzLmhvc3RuYW1lLnJlcGxhY2UocmVwbGFjZSwgdik7XG4gICAgICB9XG5cbiAgICAgIHRoaXMuYnVpbGQoIWJ1aWxkKTtcbiAgICAgIHJldHVybiB0aGlzO1xuICAgIH1cbiAgfTtcbiAgcC50bGQgPSBmdW5jdGlvbih2LCBidWlsZCkge1xuICAgIGlmICh0aGlzLl9wYXJ0cy51cm4pIHtcbiAgICAgIHJldHVybiB2ID09PSB1bmRlZmluZWQgPyAnJyA6IHRoaXM7XG4gICAgfVxuXG4gICAgaWYgKHR5cGVvZiB2ID09PSAnYm9vbGVhbicpIHtcbiAgICAgIGJ1aWxkID0gdjtcbiAgICAgIHYgPSB1bmRlZmluZWQ7XG4gICAgfVxuXG4gICAgLy8gcmV0dXJuIFwib3JnXCIgZnJvbSBcInd3dy5leGFtcGxlLm9yZ1wiXG4gICAgaWYgKHYgPT09IHVuZGVmaW5lZCkge1xuICAgICAgaWYgKCF0aGlzLl9wYXJ0cy5ob3N0bmFtZSB8fCB0aGlzLmlzKCdJUCcpKSB7XG4gICAgICAgIHJldHVybiAnJztcbiAgICAgIH1cblxuICAgICAgdmFyIHBvcyA9IHRoaXMuX3BhcnRzLmhvc3RuYW1lLmxhc3RJbmRleE9mKCcuJyk7XG4gICAgICB2YXIgdGxkID0gdGhpcy5fcGFydHMuaG9zdG5hbWUuc3Vic3RyaW5nKHBvcyArIDEpO1xuXG4gICAgICBpZiAoYnVpbGQgIT09IHRydWUgJiYgU0xEICYmIFNMRC5saXN0W3RsZC50b0xvd2VyQ2FzZSgpXSkge1xuICAgICAgICByZXR1cm4gU0xELmdldCh0aGlzLl9wYXJ0cy5ob3N0bmFtZSkgfHwgdGxkO1xuICAgICAgfVxuXG4gICAgICByZXR1cm4gdGxkO1xuICAgIH0gZWxzZSB7XG4gICAgICB2YXIgcmVwbGFjZTtcblxuICAgICAgaWYgKCF2KSB7XG4gICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ2Nhbm5vdCBzZXQgVExEIGVtcHR5Jyk7XG4gICAgICB9IGVsc2UgaWYgKHYubWF0Y2goL1teYS16QS1aMC05LV0vKSkge1xuICAgICAgICBpZiAoU0xEICYmIFNMRC5pcyh2KSkge1xuICAgICAgICAgIHJlcGxhY2UgPSBuZXcgUmVnRXhwKGVzY2FwZVJlZ0V4KHRoaXMudGxkKCkpICsgJyQnKTtcbiAgICAgICAgICB0aGlzLl9wYXJ0cy5ob3N0bmFtZSA9IHRoaXMuX3BhcnRzLmhvc3RuYW1lLnJlcGxhY2UocmVwbGFjZSwgdik7XG4gICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcignVExEIFwiJyArIHYgKyAnXCIgY29udGFpbnMgY2hhcmFjdGVycyBvdGhlciB0aGFuIFtBLVowLTldJyk7XG4gICAgICAgIH1cbiAgICAgIH0gZWxzZSBpZiAoIXRoaXMuX3BhcnRzLmhvc3RuYW1lIHx8IHRoaXMuaXMoJ0lQJykpIHtcbiAgICAgICAgdGhyb3cgbmV3IFJlZmVyZW5jZUVycm9yKCdjYW5ub3Qgc2V0IFRMRCBvbiBub24tZG9tYWluIGhvc3QnKTtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIHJlcGxhY2UgPSBuZXcgUmVnRXhwKGVzY2FwZVJlZ0V4KHRoaXMudGxkKCkpICsgJyQnKTtcbiAgICAgICAgdGhpcy5fcGFydHMuaG9zdG5hbWUgPSB0aGlzLl9wYXJ0cy5ob3N0bmFtZS5yZXBsYWNlKHJlcGxhY2UsIHYpO1xuICAgICAgfVxuXG4gICAgICB0aGlzLmJ1aWxkKCFidWlsZCk7XG4gICAgICByZXR1cm4gdGhpcztcbiAgICB9XG4gIH07XG4gIHAuZGlyZWN0b3J5ID0gZnVuY3Rpb24odiwgYnVpbGQpIHtcbiAgICBpZiAodGhpcy5fcGFydHMudXJuKSB7XG4gICAgICByZXR1cm4gdiA9PT0gdW5kZWZpbmVkID8gJycgOiB0aGlzO1xuICAgIH1cblxuICAgIGlmICh2ID09PSB1bmRlZmluZWQgfHwgdiA9PT0gdHJ1ZSkge1xuICAgICAgaWYgKCF0aGlzLl9wYXJ0cy5wYXRoICYmICF0aGlzLl9wYXJ0cy5ob3N0bmFtZSkge1xuICAgICAgICByZXR1cm4gJyc7XG4gICAgICB9XG5cbiAgICAgIGlmICh0aGlzLl9wYXJ0cy5wYXRoID09PSAnLycpIHtcbiAgICAgICAgcmV0dXJuICcvJztcbiAgICAgIH1cblxuICAgICAgdmFyIGVuZCA9IHRoaXMuX3BhcnRzLnBhdGgubGVuZ3RoIC0gdGhpcy5maWxlbmFtZSgpLmxlbmd0aCAtIDE7XG4gICAgICB2YXIgcmVzID0gdGhpcy5fcGFydHMucGF0aC5zdWJzdHJpbmcoMCwgZW5kKSB8fCAodGhpcy5fcGFydHMuaG9zdG5hbWUgPyAnLycgOiAnJyk7XG5cbiAgICAgIHJldHVybiB2ID8gVVJJLmRlY29kZVBhdGgocmVzKSA6IHJlcztcblxuICAgIH0gZWxzZSB7XG4gICAgICB2YXIgZSA9IHRoaXMuX3BhcnRzLnBhdGgubGVuZ3RoIC0gdGhpcy5maWxlbmFtZSgpLmxlbmd0aDtcbiAgICAgIHZhciBkaXJlY3RvcnkgPSB0aGlzLl9wYXJ0cy5wYXRoLnN1YnN0cmluZygwLCBlKTtcbiAgICAgIHZhciByZXBsYWNlID0gbmV3IFJlZ0V4cCgnXicgKyBlc2NhcGVSZWdFeChkaXJlY3RvcnkpKTtcblxuICAgICAgLy8gZnVsbHkgcXVhbGlmaWVyIGRpcmVjdG9yaWVzIGJlZ2luIHdpdGggYSBzbGFzaFxuICAgICAgaWYgKCF0aGlzLmlzKCdyZWxhdGl2ZScpKSB7XG4gICAgICAgIGlmICghdikge1xuICAgICAgICAgIHYgPSAnLyc7XG4gICAgICAgIH1cblxuICAgICAgICBpZiAodi5jaGFyQXQoMCkgIT09ICcvJykge1xuICAgICAgICAgIHYgPSAnLycgKyB2O1xuICAgICAgICB9XG4gICAgICB9XG5cbiAgICAgIC8vIGRpcmVjdG9yaWVzIGFsd2F5cyBlbmQgd2l0aCBhIHNsYXNoXG4gICAgICBpZiAodiAmJiB2LmNoYXJBdCh2Lmxlbmd0aCAtIDEpICE9PSAnLycpIHtcbiAgICAgICAgdiArPSAnLyc7XG4gICAgICB9XG5cbiAgICAgIHYgPSBVUkkucmVjb2RlUGF0aCh2KTtcbiAgICAgIHRoaXMuX3BhcnRzLnBhdGggPSB0aGlzLl9wYXJ0cy5wYXRoLnJlcGxhY2UocmVwbGFjZSwgdik7XG4gICAgICB0aGlzLmJ1aWxkKCFidWlsZCk7XG4gICAgICByZXR1cm4gdGhpcztcbiAgICB9XG4gIH07XG4gIHAuZmlsZW5hbWUgPSBmdW5jdGlvbih2LCBidWlsZCkge1xuICAgIGlmICh0aGlzLl9wYXJ0cy51cm4pIHtcbiAgICAgIHJldHVybiB2ID09PSB1bmRlZmluZWQgPyAnJyA6IHRoaXM7XG4gICAgfVxuXG4gICAgaWYgKHYgPT09IHVuZGVmaW5lZCB8fCB2ID09PSB0cnVlKSB7XG4gICAgICBpZiAoIXRoaXMuX3BhcnRzLnBhdGggfHwgdGhpcy5fcGFydHMucGF0aCA9PT0gJy8nKSB7XG4gICAgICAgIHJldHVybiAnJztcbiAgICAgIH1cblxuICAgICAgdmFyIHBvcyA9IHRoaXMuX3BhcnRzLnBhdGgubGFzdEluZGV4T2YoJy8nKTtcbiAgICAgIHZhciByZXMgPSB0aGlzLl9wYXJ0cy5wYXRoLnN1YnN0cmluZyhwb3MrMSk7XG5cbiAgICAgIHJldHVybiB2ID8gVVJJLmRlY29kZVBhdGhTZWdtZW50KHJlcykgOiByZXM7XG4gICAgfSBlbHNlIHtcbiAgICAgIHZhciBtdXRhdGVkRGlyZWN0b3J5ID0gZmFsc2U7XG5cbiAgICAgIGlmICh2LmNoYXJBdCgwKSA9PT0gJy8nKSB7XG4gICAgICAgIHYgPSB2LnN1YnN0cmluZygxKTtcbiAgICAgIH1cblxuICAgICAgaWYgKHYubWF0Y2goL1xcLj9cXC8vKSkge1xuICAgICAgICBtdXRhdGVkRGlyZWN0b3J5ID0gdHJ1ZTtcbiAgICAgIH1cblxuICAgICAgdmFyIHJlcGxhY2UgPSBuZXcgUmVnRXhwKGVzY2FwZVJlZ0V4KHRoaXMuZmlsZW5hbWUoKSkgKyAnJCcpO1xuICAgICAgdiA9IFVSSS5yZWNvZGVQYXRoKHYpO1xuICAgICAgdGhpcy5fcGFydHMucGF0aCA9IHRoaXMuX3BhcnRzLnBhdGgucmVwbGFjZShyZXBsYWNlLCB2KTtcblxuICAgICAgaWYgKG11dGF0ZWREaXJlY3RvcnkpIHtcbiAgICAgICAgdGhpcy5ub3JtYWxpemVQYXRoKGJ1aWxkKTtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIHRoaXMuYnVpbGQoIWJ1aWxkKTtcbiAgICAgIH1cblxuICAgICAgcmV0dXJuIHRoaXM7XG4gICAgfVxuICB9O1xuICBwLnN1ZmZpeCA9IGZ1bmN0aW9uKHYsIGJ1aWxkKSB7XG4gICAgaWYgKHRoaXMuX3BhcnRzLnVybikge1xuICAgICAgcmV0dXJuIHYgPT09IHVuZGVmaW5lZCA/ICcnIDogdGhpcztcbiAgICB9XG5cbiAgICBpZiAodiA9PT0gdW5kZWZpbmVkIHx8IHYgPT09IHRydWUpIHtcbiAgICAgIGlmICghdGhpcy5fcGFydHMucGF0aCB8fCB0aGlzLl9wYXJ0cy5wYXRoID09PSAnLycpIHtcbiAgICAgICAgcmV0dXJuICcnO1xuICAgICAgfVxuXG4gICAgICB2YXIgZmlsZW5hbWUgPSB0aGlzLmZpbGVuYW1lKCk7XG4gICAgICB2YXIgcG9zID0gZmlsZW5hbWUubGFzdEluZGV4T2YoJy4nKTtcbiAgICAgIHZhciBzLCByZXM7XG5cbiAgICAgIGlmIChwb3MgPT09IC0xKSB7XG4gICAgICAgIHJldHVybiAnJztcbiAgICAgIH1cblxuICAgICAgLy8gc3VmZml4IG1heSBvbmx5IGNvbnRhaW4gYWxudW0gY2hhcmFjdGVycyAoeXVwLCBJIG1hZGUgdGhpcyB1cC4pXG4gICAgICBzID0gZmlsZW5hbWUuc3Vic3RyaW5nKHBvcysxKTtcbiAgICAgIHJlcyA9ICgvXlthLXowLTklXSskL2kpLnRlc3QocykgPyBzIDogJyc7XG4gICAgICByZXR1cm4gdiA/IFVSSS5kZWNvZGVQYXRoU2VnbWVudChyZXMpIDogcmVzO1xuICAgIH0gZWxzZSB7XG4gICAgICBpZiAodi5jaGFyQXQoMCkgPT09ICcuJykge1xuICAgICAgICB2ID0gdi5zdWJzdHJpbmcoMSk7XG4gICAgICB9XG5cbiAgICAgIHZhciBzdWZmaXggPSB0aGlzLnN1ZmZpeCgpO1xuICAgICAgdmFyIHJlcGxhY2U7XG5cbiAgICAgIGlmICghc3VmZml4KSB7XG4gICAgICAgIGlmICghdikge1xuICAgICAgICAgIHJldHVybiB0aGlzO1xuICAgICAgICB9XG5cbiAgICAgICAgdGhpcy5fcGFydHMucGF0aCArPSAnLicgKyBVUkkucmVjb2RlUGF0aCh2KTtcbiAgICAgIH0gZWxzZSBpZiAoIXYpIHtcbiAgICAgICAgcmVwbGFjZSA9IG5ldyBSZWdFeHAoZXNjYXBlUmVnRXgoJy4nICsgc3VmZml4KSArICckJyk7XG4gICAgICB9IGVsc2Uge1xuICAgICAgICByZXBsYWNlID0gbmV3IFJlZ0V4cChlc2NhcGVSZWdFeChzdWZmaXgpICsgJyQnKTtcbiAgICAgIH1cblxuICAgICAgaWYgKHJlcGxhY2UpIHtcbiAgICAgICAgdiA9IFVSSS5yZWNvZGVQYXRoKHYpO1xuICAgICAgICB0aGlzLl9wYXJ0cy5wYXRoID0gdGhpcy5fcGFydHMucGF0aC5yZXBsYWNlKHJlcGxhY2UsIHYpO1xuICAgICAgfVxuXG4gICAgICB0aGlzLmJ1aWxkKCFidWlsZCk7XG4gICAgICByZXR1cm4gdGhpcztcbiAgICB9XG4gIH07XG4gIHAuc2VnbWVudCA9IGZ1bmN0aW9uKHNlZ21lbnQsIHYsIGJ1aWxkKSB7XG4gICAgdmFyIHNlcGFyYXRvciA9IHRoaXMuX3BhcnRzLnVybiA/ICc6JyA6ICcvJztcbiAgICB2YXIgcGF0aCA9IHRoaXMucGF0aCgpO1xuICAgIHZhciBhYnNvbHV0ZSA9IHBhdGguc3Vic3RyaW5nKDAsIDEpID09PSAnLyc7XG4gICAgdmFyIHNlZ21lbnRzID0gcGF0aC5zcGxpdChzZXBhcmF0b3IpO1xuXG4gICAgaWYgKHNlZ21lbnQgIT09IHVuZGVmaW5lZCAmJiB0eXBlb2Ygc2VnbWVudCAhPT0gJ251bWJlcicpIHtcbiAgICAgIGJ1aWxkID0gdjtcbiAgICAgIHYgPSBzZWdtZW50O1xuICAgICAgc2VnbWVudCA9IHVuZGVmaW5lZDtcbiAgICB9XG5cbiAgICBpZiAoc2VnbWVudCAhPT0gdW5kZWZpbmVkICYmIHR5cGVvZiBzZWdtZW50ICE9PSAnbnVtYmVyJykge1xuICAgICAgdGhyb3cgbmV3IEVycm9yKCdCYWQgc2VnbWVudCBcIicgKyBzZWdtZW50ICsgJ1wiLCBtdXN0IGJlIDAtYmFzZWQgaW50ZWdlcicpO1xuICAgIH1cblxuICAgIGlmIChhYnNvbHV0ZSkge1xuICAgICAgc2VnbWVudHMuc2hpZnQoKTtcbiAgICB9XG5cbiAgICBpZiAoc2VnbWVudCA8IDApIHtcbiAgICAgIC8vIGFsbG93IG5lZ2F0aXZlIGluZGV4ZXMgdG8gYWRkcmVzcyBmcm9tIHRoZSBlbmRcbiAgICAgIHNlZ21lbnQgPSBNYXRoLm1heChzZWdtZW50cy5sZW5ndGggKyBzZWdtZW50LCAwKTtcbiAgICB9XG5cbiAgICBpZiAodiA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICAvKmpzaGludCBsYXhicmVhazogdHJ1ZSAqL1xuICAgICAgcmV0dXJuIHNlZ21lbnQgPT09IHVuZGVmaW5lZFxuICAgICAgICA/IHNlZ21lbnRzXG4gICAgICAgIDogc2VnbWVudHNbc2VnbWVudF07XG4gICAgICAvKmpzaGludCBsYXhicmVhazogZmFsc2UgKi9cbiAgICB9IGVsc2UgaWYgKHNlZ21lbnQgPT09IG51bGwgfHwgc2VnbWVudHNbc2VnbWVudF0gPT09IHVuZGVmaW5lZCkge1xuICAgICAgaWYgKGlzQXJyYXkodikpIHtcbiAgICAgICAgc2VnbWVudHMgPSBbXTtcbiAgICAgICAgLy8gY29sbGFwc2UgZW1wdHkgZWxlbWVudHMgd2l0aGluIGFycmF5XG4gICAgICAgIGZvciAodmFyIGk9MCwgbD12Lmxlbmd0aDsgaSA8IGw7IGkrKykge1xuICAgICAgICAgIGlmICghdltpXS5sZW5ndGggJiYgKCFzZWdtZW50cy5sZW5ndGggfHwgIXNlZ21lbnRzW3NlZ21lbnRzLmxlbmd0aCAtMV0ubGVuZ3RoKSkge1xuICAgICAgICAgICAgY29udGludWU7XG4gICAgICAgICAgfVxuXG4gICAgICAgICAgaWYgKHNlZ21lbnRzLmxlbmd0aCAmJiAhc2VnbWVudHNbc2VnbWVudHMubGVuZ3RoIC0xXS5sZW5ndGgpIHtcbiAgICAgICAgICAgIHNlZ21lbnRzLnBvcCgpO1xuICAgICAgICAgIH1cblxuICAgICAgICAgIHNlZ21lbnRzLnB1c2godHJpbVNsYXNoZXModltpXSkpO1xuICAgICAgICB9XG4gICAgICB9IGVsc2UgaWYgKHYgfHwgdHlwZW9mIHYgPT09ICdzdHJpbmcnKSB7XG4gICAgICAgIHYgPSB0cmltU2xhc2hlcyh2KTtcbiAgICAgICAgaWYgKHNlZ21lbnRzW3NlZ21lbnRzLmxlbmd0aCAtMV0gPT09ICcnKSB7XG4gICAgICAgICAgLy8gZW1wdHkgdHJhaWxpbmcgZWxlbWVudHMgaGF2ZSB0byBiZSBvdmVyd3JpdHRlblxuICAgICAgICAgIC8vIHRvIHByZXZlbnQgcmVzdWx0cyBzdWNoIGFzIC9mb28vL2JhclxuICAgICAgICAgIHNlZ21lbnRzW3NlZ21lbnRzLmxlbmd0aCAtMV0gPSB2O1xuICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgIHNlZ21lbnRzLnB1c2godik7XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9IGVsc2Uge1xuICAgICAgaWYgKHYpIHtcbiAgICAgICAgc2VnbWVudHNbc2VnbWVudF0gPSB0cmltU2xhc2hlcyh2KTtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIHNlZ21lbnRzLnNwbGljZShzZWdtZW50LCAxKTtcbiAgICAgIH1cbiAgICB9XG5cbiAgICBpZiAoYWJzb2x1dGUpIHtcbiAgICAgIHNlZ21lbnRzLnVuc2hpZnQoJycpO1xuICAgIH1cblxuICAgIHJldHVybiB0aGlzLnBhdGgoc2VnbWVudHMuam9pbihzZXBhcmF0b3IpLCBidWlsZCk7XG4gIH07XG4gIHAuc2VnbWVudENvZGVkID0gZnVuY3Rpb24oc2VnbWVudCwgdiwgYnVpbGQpIHtcbiAgICB2YXIgc2VnbWVudHMsIGksIGw7XG5cbiAgICBpZiAodHlwZW9mIHNlZ21lbnQgIT09ICdudW1iZXInKSB7XG4gICAgICBidWlsZCA9IHY7XG4gICAgICB2ID0gc2VnbWVudDtcbiAgICAgIHNlZ21lbnQgPSB1bmRlZmluZWQ7XG4gICAgfVxuXG4gICAgaWYgKHYgPT09IHVuZGVmaW5lZCkge1xuICAgICAgc2VnbWVudHMgPSB0aGlzLnNlZ21lbnQoc2VnbWVudCwgdiwgYnVpbGQpO1xuICAgICAgaWYgKCFpc0FycmF5KHNlZ21lbnRzKSkge1xuICAgICAgICBzZWdtZW50cyA9IHNlZ21lbnRzICE9PSB1bmRlZmluZWQgPyBVUkkuZGVjb2RlKHNlZ21lbnRzKSA6IHVuZGVmaW5lZDtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIGZvciAoaSA9IDAsIGwgPSBzZWdtZW50cy5sZW5ndGg7IGkgPCBsOyBpKyspIHtcbiAgICAgICAgICBzZWdtZW50c1tpXSA9IFVSSS5kZWNvZGUoc2VnbWVudHNbaV0pO1xuICAgICAgICB9XG4gICAgICB9XG5cbiAgICAgIHJldHVybiBzZWdtZW50cztcbiAgICB9XG5cbiAgICBpZiAoIWlzQXJyYXkodikpIHtcbiAgICAgIHYgPSAodHlwZW9mIHYgPT09ICdzdHJpbmcnIHx8IHYgaW5zdGFuY2VvZiBTdHJpbmcpID8gVVJJLmVuY29kZSh2KSA6IHY7XG4gICAgfSBlbHNlIHtcbiAgICAgIGZvciAoaSA9IDAsIGwgPSB2Lmxlbmd0aDsgaSA8IGw7IGkrKykge1xuICAgICAgICB2W2ldID0gVVJJLmVuY29kZSh2W2ldKTtcbiAgICAgIH1cbiAgICB9XG5cbiAgICByZXR1cm4gdGhpcy5zZWdtZW50KHNlZ21lbnQsIHYsIGJ1aWxkKTtcbiAgfTtcblxuICAvLyBtdXRhdGluZyBxdWVyeSBzdHJpbmdcbiAgdmFyIHEgPSBwLnF1ZXJ5O1xuICBwLnF1ZXJ5ID0gZnVuY3Rpb24odiwgYnVpbGQpIHtcbiAgICBpZiAodiA9PT0gdHJ1ZSkge1xuICAgICAgcmV0dXJuIFVSSS5wYXJzZVF1ZXJ5KHRoaXMuX3BhcnRzLnF1ZXJ5LCB0aGlzLl9wYXJ0cy5lc2NhcGVRdWVyeVNwYWNlKTtcbiAgICB9IGVsc2UgaWYgKHR5cGVvZiB2ID09PSAnZnVuY3Rpb24nKSB7XG4gICAgICB2YXIgZGF0YSA9IFVSSS5wYXJzZVF1ZXJ5KHRoaXMuX3BhcnRzLnF1ZXJ5LCB0aGlzLl9wYXJ0cy5lc2NhcGVRdWVyeVNwYWNlKTtcbiAgICAgIHZhciByZXN1bHQgPSB2LmNhbGwodGhpcywgZGF0YSk7XG4gICAgICB0aGlzLl9wYXJ0cy5xdWVyeSA9IFVSSS5idWlsZFF1ZXJ5KHJlc3VsdCB8fCBkYXRhLCB0aGlzLl9wYXJ0cy5kdXBsaWNhdGVRdWVyeVBhcmFtZXRlcnMsIHRoaXMuX3BhcnRzLmVzY2FwZVF1ZXJ5U3BhY2UpO1xuICAgICAgdGhpcy5idWlsZCghYnVpbGQpO1xuICAgICAgcmV0dXJuIHRoaXM7XG4gICAgfSBlbHNlIGlmICh2ICE9PSB1bmRlZmluZWQgJiYgdHlwZW9mIHYgIT09ICdzdHJpbmcnKSB7XG4gICAgICB0aGlzLl9wYXJ0cy5xdWVyeSA9IFVSSS5idWlsZFF1ZXJ5KHYsIHRoaXMuX3BhcnRzLmR1cGxpY2F0ZVF1ZXJ5UGFyYW1ldGVycywgdGhpcy5fcGFydHMuZXNjYXBlUXVlcnlTcGFjZSk7XG4gICAgICB0aGlzLmJ1aWxkKCFidWlsZCk7XG4gICAgICByZXR1cm4gdGhpcztcbiAgICB9IGVsc2Uge1xuICAgICAgcmV0dXJuIHEuY2FsbCh0aGlzLCB2LCBidWlsZCk7XG4gICAgfVxuICB9O1xuICBwLnNldFF1ZXJ5ID0gZnVuY3Rpb24obmFtZSwgdmFsdWUsIGJ1aWxkKSB7XG4gICAgdmFyIGRhdGEgPSBVUkkucGFyc2VRdWVyeSh0aGlzLl9wYXJ0cy5xdWVyeSwgdGhpcy5fcGFydHMuZXNjYXBlUXVlcnlTcGFjZSk7XG5cbiAgICBpZiAodHlwZW9mIG5hbWUgPT09ICdzdHJpbmcnIHx8IG5hbWUgaW5zdGFuY2VvZiBTdHJpbmcpIHtcbiAgICAgIGRhdGFbbmFtZV0gPSB2YWx1ZSAhPT0gdW5kZWZpbmVkID8gdmFsdWUgOiBudWxsO1xuICAgIH0gZWxzZSBpZiAodHlwZW9mIG5hbWUgPT09ICdvYmplY3QnKSB7XG4gICAgICBmb3IgKHZhciBrZXkgaW4gbmFtZSkge1xuICAgICAgICBpZiAoaGFzT3duLmNhbGwobmFtZSwga2V5KSkge1xuICAgICAgICAgIGRhdGFba2V5XSA9IG5hbWVba2V5XTtcbiAgICAgICAgfVxuICAgICAgfVxuICAgIH0gZWxzZSB7XG4gICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdVUkkuYWRkUXVlcnkoKSBhY2NlcHRzIGFuIG9iamVjdCwgc3RyaW5nIGFzIHRoZSBuYW1lIHBhcmFtZXRlcicpO1xuICAgIH1cblxuICAgIHRoaXMuX3BhcnRzLnF1ZXJ5ID0gVVJJLmJ1aWxkUXVlcnkoZGF0YSwgdGhpcy5fcGFydHMuZHVwbGljYXRlUXVlcnlQYXJhbWV0ZXJzLCB0aGlzLl9wYXJ0cy5lc2NhcGVRdWVyeVNwYWNlKTtcbiAgICBpZiAodHlwZW9mIG5hbWUgIT09ICdzdHJpbmcnKSB7XG4gICAgICBidWlsZCA9IHZhbHVlO1xuICAgIH1cblxuICAgIHRoaXMuYnVpbGQoIWJ1aWxkKTtcbiAgICByZXR1cm4gdGhpcztcbiAgfTtcbiAgcC5hZGRRdWVyeSA9IGZ1bmN0aW9uKG5hbWUsIHZhbHVlLCBidWlsZCkge1xuICAgIHZhciBkYXRhID0gVVJJLnBhcnNlUXVlcnkodGhpcy5fcGFydHMucXVlcnksIHRoaXMuX3BhcnRzLmVzY2FwZVF1ZXJ5U3BhY2UpO1xuICAgIFVSSS5hZGRRdWVyeShkYXRhLCBuYW1lLCB2YWx1ZSA9PT0gdW5kZWZpbmVkID8gbnVsbCA6IHZhbHVlKTtcbiAgICB0aGlzLl9wYXJ0cy5xdWVyeSA9IFVSSS5idWlsZFF1ZXJ5KGRhdGEsIHRoaXMuX3BhcnRzLmR1cGxpY2F0ZVF1ZXJ5UGFyYW1ldGVycywgdGhpcy5fcGFydHMuZXNjYXBlUXVlcnlTcGFjZSk7XG4gICAgaWYgKHR5cGVvZiBuYW1lICE9PSAnc3RyaW5nJykge1xuICAgICAgYnVpbGQgPSB2YWx1ZTtcbiAgICB9XG5cbiAgICB0aGlzLmJ1aWxkKCFidWlsZCk7XG4gICAgcmV0dXJuIHRoaXM7XG4gIH07XG4gIHAucmVtb3ZlUXVlcnkgPSBmdW5jdGlvbihuYW1lLCB2YWx1ZSwgYnVpbGQpIHtcbiAgICB2YXIgZGF0YSA9IFVSSS5wYXJzZVF1ZXJ5KHRoaXMuX3BhcnRzLnF1ZXJ5LCB0aGlzLl9wYXJ0cy5lc2NhcGVRdWVyeVNwYWNlKTtcbiAgICBVUkkucmVtb3ZlUXVlcnkoZGF0YSwgbmFtZSwgdmFsdWUpO1xuICAgIHRoaXMuX3BhcnRzLnF1ZXJ5ID0gVVJJLmJ1aWxkUXVlcnkoZGF0YSwgdGhpcy5fcGFydHMuZHVwbGljYXRlUXVlcnlQYXJhbWV0ZXJzLCB0aGlzLl9wYXJ0cy5lc2NhcGVRdWVyeVNwYWNlKTtcbiAgICBpZiAodHlwZW9mIG5hbWUgIT09ICdzdHJpbmcnKSB7XG4gICAgICBidWlsZCA9IHZhbHVlO1xuICAgIH1cblxuICAgIHRoaXMuYnVpbGQoIWJ1aWxkKTtcbiAgICByZXR1cm4gdGhpcztcbiAgfTtcbiAgcC5oYXNRdWVyeSA9IGZ1bmN0aW9uKG5hbWUsIHZhbHVlLCB3aXRoaW5BcnJheSkge1xuICAgIHZhciBkYXRhID0gVVJJLnBhcnNlUXVlcnkodGhpcy5fcGFydHMucXVlcnksIHRoaXMuX3BhcnRzLmVzY2FwZVF1ZXJ5U3BhY2UpO1xuICAgIHJldHVybiBVUkkuaGFzUXVlcnkoZGF0YSwgbmFtZSwgdmFsdWUsIHdpdGhpbkFycmF5KTtcbiAgfTtcbiAgcC5zZXRTZWFyY2ggPSBwLnNldFF1ZXJ5O1xuICBwLmFkZFNlYXJjaCA9IHAuYWRkUXVlcnk7XG4gIHAucmVtb3ZlU2VhcmNoID0gcC5yZW1vdmVRdWVyeTtcbiAgcC5oYXNTZWFyY2ggPSBwLmhhc1F1ZXJ5O1xuXG4gIC8vIHNhbml0aXppbmcgVVJMc1xuICBwLm5vcm1hbGl6ZSA9IGZ1bmN0aW9uKCkge1xuICAgIGlmICh0aGlzLl9wYXJ0cy51cm4pIHtcbiAgICAgIHJldHVybiB0aGlzXG4gICAgICAgIC5ub3JtYWxpemVQcm90b2NvbChmYWxzZSlcbiAgICAgICAgLm5vcm1hbGl6ZVBhdGgoZmFsc2UpXG4gICAgICAgIC5ub3JtYWxpemVRdWVyeShmYWxzZSlcbiAgICAgICAgLm5vcm1hbGl6ZUZyYWdtZW50KGZhbHNlKVxuICAgICAgICAuYnVpbGQoKTtcbiAgICB9XG5cbiAgICByZXR1cm4gdGhpc1xuICAgICAgLm5vcm1hbGl6ZVByb3RvY29sKGZhbHNlKVxuICAgICAgLm5vcm1hbGl6ZUhvc3RuYW1lKGZhbHNlKVxuICAgICAgLm5vcm1hbGl6ZVBvcnQoZmFsc2UpXG4gICAgICAubm9ybWFsaXplUGF0aChmYWxzZSlcbiAgICAgIC5ub3JtYWxpemVRdWVyeShmYWxzZSlcbiAgICAgIC5ub3JtYWxpemVGcmFnbWVudChmYWxzZSlcbiAgICAgIC5idWlsZCgpO1xuICB9O1xuICBwLm5vcm1hbGl6ZVByb3RvY29sID0gZnVuY3Rpb24oYnVpbGQpIHtcbiAgICBpZiAodHlwZW9mIHRoaXMuX3BhcnRzLnByb3RvY29sID09PSAnc3RyaW5nJykge1xuICAgICAgdGhpcy5fcGFydHMucHJvdG9jb2wgPSB0aGlzLl9wYXJ0cy5wcm90b2NvbC50b0xvd2VyQ2FzZSgpO1xuICAgICAgdGhpcy5idWlsZCghYnVpbGQpO1xuICAgIH1cblxuICAgIHJldHVybiB0aGlzO1xuICB9O1xuICBwLm5vcm1hbGl6ZUhvc3RuYW1lID0gZnVuY3Rpb24oYnVpbGQpIHtcbiAgICBpZiAodGhpcy5fcGFydHMuaG9zdG5hbWUpIHtcbiAgICAgIGlmICh0aGlzLmlzKCdJRE4nKSAmJiBwdW55Y29kZSkge1xuICAgICAgICB0aGlzLl9wYXJ0cy5ob3N0bmFtZSA9IHB1bnljb2RlLnRvQVNDSUkodGhpcy5fcGFydHMuaG9zdG5hbWUpO1xuICAgICAgfSBlbHNlIGlmICh0aGlzLmlzKCdJUHY2JykgJiYgSVB2Nikge1xuICAgICAgICB0aGlzLl9wYXJ0cy5ob3N0bmFtZSA9IElQdjYuYmVzdCh0aGlzLl9wYXJ0cy5ob3N0bmFtZSk7XG4gICAgICB9XG5cbiAgICAgIHRoaXMuX3BhcnRzLmhvc3RuYW1lID0gdGhpcy5fcGFydHMuaG9zdG5hbWUudG9Mb3dlckNhc2UoKTtcbiAgICAgIHRoaXMuYnVpbGQoIWJ1aWxkKTtcbiAgICB9XG5cbiAgICByZXR1cm4gdGhpcztcbiAgfTtcbiAgcC5ub3JtYWxpemVQb3J0ID0gZnVuY3Rpb24oYnVpbGQpIHtcbiAgICAvLyByZW1vdmUgcG9ydCBvZiBpdCdzIHRoZSBwcm90b2NvbCdzIGRlZmF1bHRcbiAgICBpZiAodHlwZW9mIHRoaXMuX3BhcnRzLnByb3RvY29sID09PSAnc3RyaW5nJyAmJiB0aGlzLl9wYXJ0cy5wb3J0ID09PSBVUkkuZGVmYXVsdFBvcnRzW3RoaXMuX3BhcnRzLnByb3RvY29sXSkge1xuICAgICAgdGhpcy5fcGFydHMucG9ydCA9IG51bGw7XG4gICAgICB0aGlzLmJ1aWxkKCFidWlsZCk7XG4gICAgfVxuXG4gICAgcmV0dXJuIHRoaXM7XG4gIH07XG4gIHAubm9ybWFsaXplUGF0aCA9IGZ1bmN0aW9uKGJ1aWxkKSB7XG4gICAgdmFyIF9wYXRoID0gdGhpcy5fcGFydHMucGF0aDtcbiAgICBpZiAoIV9wYXRoKSB7XG4gICAgICByZXR1cm4gdGhpcztcbiAgICB9XG5cbiAgICBpZiAodGhpcy5fcGFydHMudXJuKSB7XG4gICAgICB0aGlzLl9wYXJ0cy5wYXRoID0gVVJJLnJlY29kZVVyblBhdGgodGhpcy5fcGFydHMucGF0aCk7XG4gICAgICB0aGlzLmJ1aWxkKCFidWlsZCk7XG4gICAgICByZXR1cm4gdGhpcztcbiAgICB9XG5cbiAgICBpZiAodGhpcy5fcGFydHMucGF0aCA9PT0gJy8nKSB7XG4gICAgICByZXR1cm4gdGhpcztcbiAgICB9XG5cbiAgICB2YXIgX3dhc19yZWxhdGl2ZTtcbiAgICB2YXIgX2xlYWRpbmdQYXJlbnRzID0gJyc7XG4gICAgdmFyIF9wYXJlbnQsIF9wb3M7XG5cbiAgICAvLyBoYW5kbGUgcmVsYXRpdmUgcGF0aHNcbiAgICBpZiAoX3BhdGguY2hhckF0KDApICE9PSAnLycpIHtcbiAgICAgIF93YXNfcmVsYXRpdmUgPSB0cnVlO1xuICAgICAgX3BhdGggPSAnLycgKyBfcGF0aDtcbiAgICB9XG5cbiAgICAvLyBoYW5kbGUgcmVsYXRpdmUgZmlsZXMgKGFzIG9wcG9zZWQgdG8gZGlyZWN0b3JpZXMpXG4gICAgaWYgKF9wYXRoLnNsaWNlKC0zKSA9PT0gJy8uLicgfHwgX3BhdGguc2xpY2UoLTIpID09PSAnLy4nKSB7XG4gICAgICBfcGF0aCArPSAnLyc7XG4gICAgfVxuXG4gICAgLy8gcmVzb2x2ZSBzaW1wbGVzXG4gICAgX3BhdGggPSBfcGF0aFxuICAgICAgLnJlcGxhY2UoLyhcXC8oXFwuXFwvKSspfChcXC9cXC4kKS9nLCAnLycpXG4gICAgICAucmVwbGFjZSgvXFwvezIsfS9nLCAnLycpO1xuXG4gICAgLy8gcmVtZW1iZXIgbGVhZGluZyBwYXJlbnRzXG4gICAgaWYgKF93YXNfcmVsYXRpdmUpIHtcbiAgICAgIF9sZWFkaW5nUGFyZW50cyA9IF9wYXRoLnN1YnN0cmluZygxKS5tYXRjaCgvXihcXC5cXC5cXC8pKy8pIHx8ICcnO1xuICAgICAgaWYgKF9sZWFkaW5nUGFyZW50cykge1xuICAgICAgICBfbGVhZGluZ1BhcmVudHMgPSBfbGVhZGluZ1BhcmVudHNbMF07XG4gICAgICB9XG4gICAgfVxuXG4gICAgLy8gcmVzb2x2ZSBwYXJlbnRzXG4gICAgd2hpbGUgKHRydWUpIHtcbiAgICAgIF9wYXJlbnQgPSBfcGF0aC5pbmRleE9mKCcvLi4nKTtcbiAgICAgIGlmIChfcGFyZW50ID09PSAtMSkge1xuICAgICAgICAvLyBubyBtb3JlIC4uLyB0byByZXNvbHZlXG4gICAgICAgIGJyZWFrO1xuICAgICAgfSBlbHNlIGlmIChfcGFyZW50ID09PSAwKSB7XG4gICAgICAgIC8vIHRvcCBsZXZlbCBjYW5ub3QgYmUgcmVsYXRpdmUsIHNraXAgaXRcbiAgICAgICAgX3BhdGggPSBfcGF0aC5zdWJzdHJpbmcoMyk7XG4gICAgICAgIGNvbnRpbnVlO1xuICAgICAgfVxuXG4gICAgICBfcG9zID0gX3BhdGguc3Vic3RyaW5nKDAsIF9wYXJlbnQpLmxhc3RJbmRleE9mKCcvJyk7XG4gICAgICBpZiAoX3BvcyA9PT0gLTEpIHtcbiAgICAgICAgX3BvcyA9IF9wYXJlbnQ7XG4gICAgICB9XG4gICAgICBfcGF0aCA9IF9wYXRoLnN1YnN0cmluZygwLCBfcG9zKSArIF9wYXRoLnN1YnN0cmluZyhfcGFyZW50ICsgMyk7XG4gICAgfVxuXG4gICAgLy8gcmV2ZXJ0IHRvIHJlbGF0aXZlXG4gICAgaWYgKF93YXNfcmVsYXRpdmUgJiYgdGhpcy5pcygncmVsYXRpdmUnKSkge1xuICAgICAgX3BhdGggPSBfbGVhZGluZ1BhcmVudHMgKyBfcGF0aC5zdWJzdHJpbmcoMSk7XG4gICAgfVxuXG4gICAgX3BhdGggPSBVUkkucmVjb2RlUGF0aChfcGF0aCk7XG4gICAgdGhpcy5fcGFydHMucGF0aCA9IF9wYXRoO1xuICAgIHRoaXMuYnVpbGQoIWJ1aWxkKTtcbiAgICByZXR1cm4gdGhpcztcbiAgfTtcbiAgcC5ub3JtYWxpemVQYXRobmFtZSA9IHAubm9ybWFsaXplUGF0aDtcbiAgcC5ub3JtYWxpemVRdWVyeSA9IGZ1bmN0aW9uKGJ1aWxkKSB7XG4gICAgaWYgKHR5cGVvZiB0aGlzLl9wYXJ0cy5xdWVyeSA9PT0gJ3N0cmluZycpIHtcbiAgICAgIGlmICghdGhpcy5fcGFydHMucXVlcnkubGVuZ3RoKSB7XG4gICAgICAgIHRoaXMuX3BhcnRzLnF1ZXJ5ID0gbnVsbDtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIHRoaXMucXVlcnkoVVJJLnBhcnNlUXVlcnkodGhpcy5fcGFydHMucXVlcnksIHRoaXMuX3BhcnRzLmVzY2FwZVF1ZXJ5U3BhY2UpKTtcbiAgICAgIH1cblxuICAgICAgdGhpcy5idWlsZCghYnVpbGQpO1xuICAgIH1cblxuICAgIHJldHVybiB0aGlzO1xuICB9O1xuICBwLm5vcm1hbGl6ZUZyYWdtZW50ID0gZnVuY3Rpb24oYnVpbGQpIHtcbiAgICBpZiAoIXRoaXMuX3BhcnRzLmZyYWdtZW50KSB7XG4gICAgICB0aGlzLl9wYXJ0cy5mcmFnbWVudCA9IG51bGw7XG4gICAgICB0aGlzLmJ1aWxkKCFidWlsZCk7XG4gICAgfVxuXG4gICAgcmV0dXJuIHRoaXM7XG4gIH07XG4gIHAubm9ybWFsaXplU2VhcmNoID0gcC5ub3JtYWxpemVRdWVyeTtcbiAgcC5ub3JtYWxpemVIYXNoID0gcC5ub3JtYWxpemVGcmFnbWVudDtcblxuICBwLmlzbzg4NTkgPSBmdW5jdGlvbigpIHtcbiAgICAvLyBleHBlY3QgdW5pY29kZSBpbnB1dCwgaXNvODg1OSBvdXRwdXRcbiAgICB2YXIgZSA9IFVSSS5lbmNvZGU7XG4gICAgdmFyIGQgPSBVUkkuZGVjb2RlO1xuXG4gICAgVVJJLmVuY29kZSA9IGVzY2FwZTtcbiAgICBVUkkuZGVjb2RlID0gZGVjb2RlVVJJQ29tcG9uZW50O1xuICAgIHRyeSB7XG4gICAgICB0aGlzLm5vcm1hbGl6ZSgpO1xuICAgIH0gZmluYWxseSB7XG4gICAgICBVUkkuZW5jb2RlID0gZTtcbiAgICAgIFVSSS5kZWNvZGUgPSBkO1xuICAgIH1cbiAgICByZXR1cm4gdGhpcztcbiAgfTtcblxuICBwLnVuaWNvZGUgPSBmdW5jdGlvbigpIHtcbiAgICAvLyBleHBlY3QgaXNvODg1OSBpbnB1dCwgdW5pY29kZSBvdXRwdXRcbiAgICB2YXIgZSA9IFVSSS5lbmNvZGU7XG4gICAgdmFyIGQgPSBVUkkuZGVjb2RlO1xuXG4gICAgVVJJLmVuY29kZSA9IHN0cmljdEVuY29kZVVSSUNvbXBvbmVudDtcbiAgICBVUkkuZGVjb2RlID0gdW5lc2NhcGU7XG4gICAgdHJ5IHtcbiAgICAgIHRoaXMubm9ybWFsaXplKCk7XG4gICAgfSBmaW5hbGx5IHtcbiAgICAgIFVSSS5lbmNvZGUgPSBlO1xuICAgICAgVVJJLmRlY29kZSA9IGQ7XG4gICAgfVxuICAgIHJldHVybiB0aGlzO1xuICB9O1xuXG4gIHAucmVhZGFibGUgPSBmdW5jdGlvbigpIHtcbiAgICB2YXIgdXJpID0gdGhpcy5jbG9uZSgpO1xuICAgIC8vIHJlbW92aW5nIHVzZXJuYW1lLCBwYXNzd29yZCwgYmVjYXVzZSB0aGV5IHNob3VsZG4ndCBiZSBkaXNwbGF5ZWQgYWNjb3JkaW5nIHRvIFJGQyAzOTg2XG4gICAgdXJpLnVzZXJuYW1lKCcnKS5wYXNzd29yZCgnJykubm9ybWFsaXplKCk7XG4gICAgdmFyIHQgPSAnJztcbiAgICBpZiAodXJpLl9wYXJ0cy5wcm90b2NvbCkge1xuICAgICAgdCArPSB1cmkuX3BhcnRzLnByb3RvY29sICsgJzovLyc7XG4gICAgfVxuXG4gICAgaWYgKHVyaS5fcGFydHMuaG9zdG5hbWUpIHtcbiAgICAgIGlmICh1cmkuaXMoJ3B1bnljb2RlJykgJiYgcHVueWNvZGUpIHtcbiAgICAgICAgdCArPSBwdW55Y29kZS50b1VuaWNvZGUodXJpLl9wYXJ0cy5ob3N0bmFtZSk7XG4gICAgICAgIGlmICh1cmkuX3BhcnRzLnBvcnQpIHtcbiAgICAgICAgICB0ICs9ICc6JyArIHVyaS5fcGFydHMucG9ydDtcbiAgICAgICAgfVxuICAgICAgfSBlbHNlIHtcbiAgICAgICAgdCArPSB1cmkuaG9zdCgpO1xuICAgICAgfVxuICAgIH1cblxuICAgIGlmICh1cmkuX3BhcnRzLmhvc3RuYW1lICYmIHVyaS5fcGFydHMucGF0aCAmJiB1cmkuX3BhcnRzLnBhdGguY2hhckF0KDApICE9PSAnLycpIHtcbiAgICAgIHQgKz0gJy8nO1xuICAgIH1cblxuICAgIHQgKz0gdXJpLnBhdGgodHJ1ZSk7XG4gICAgaWYgKHVyaS5fcGFydHMucXVlcnkpIHtcbiAgICAgIHZhciBxID0gJyc7XG4gICAgICBmb3IgKHZhciBpID0gMCwgcXAgPSB1cmkuX3BhcnRzLnF1ZXJ5LnNwbGl0KCcmJyksIGwgPSBxcC5sZW5ndGg7IGkgPCBsOyBpKyspIHtcbiAgICAgICAgdmFyIGt2ID0gKHFwW2ldIHx8ICcnKS5zcGxpdCgnPScpO1xuICAgICAgICBxICs9ICcmJyArIFVSSS5kZWNvZGVRdWVyeShrdlswXSwgdGhpcy5fcGFydHMuZXNjYXBlUXVlcnlTcGFjZSlcbiAgICAgICAgICAucmVwbGFjZSgvJi9nLCAnJTI2Jyk7XG5cbiAgICAgICAgaWYgKGt2WzFdICE9PSB1bmRlZmluZWQpIHtcbiAgICAgICAgICBxICs9ICc9JyArIFVSSS5kZWNvZGVRdWVyeShrdlsxXSwgdGhpcy5fcGFydHMuZXNjYXBlUXVlcnlTcGFjZSlcbiAgICAgICAgICAgIC5yZXBsYWNlKC8mL2csICclMjYnKTtcbiAgICAgICAgfVxuICAgICAgfVxuICAgICAgdCArPSAnPycgKyBxLnN1YnN0cmluZygxKTtcbiAgICB9XG5cbiAgICB0ICs9IFVSSS5kZWNvZGVRdWVyeSh1cmkuaGFzaCgpLCB0cnVlKTtcbiAgICByZXR1cm4gdDtcbiAgfTtcblxuICAvLyByZXNvbHZpbmcgcmVsYXRpdmUgYW5kIGFic29sdXRlIFVSTHNcbiAgcC5hYnNvbHV0ZVRvID0gZnVuY3Rpb24oYmFzZSkge1xuICAgIHZhciByZXNvbHZlZCA9IHRoaXMuY2xvbmUoKTtcbiAgICB2YXIgcHJvcGVydGllcyA9IFsncHJvdG9jb2wnLCAndXNlcm5hbWUnLCAncGFzc3dvcmQnLCAnaG9zdG5hbWUnLCAncG9ydCddO1xuICAgIHZhciBiYXNlZGlyLCBpLCBwO1xuXG4gICAgaWYgKHRoaXMuX3BhcnRzLnVybikge1xuICAgICAgdGhyb3cgbmV3IEVycm9yKCdVUk5zIGRvIG5vdCBoYXZlIGFueSBnZW5lcmFsbHkgZGVmaW5lZCBoaWVyYXJjaGljYWwgY29tcG9uZW50cycpO1xuICAgIH1cblxuICAgIGlmICghKGJhc2UgaW5zdGFuY2VvZiBVUkkpKSB7XG4gICAgICBiYXNlID0gbmV3IFVSSShiYXNlKTtcbiAgICB9XG5cbiAgICBpZiAoIXJlc29sdmVkLl9wYXJ0cy5wcm90b2NvbCkge1xuICAgICAgcmVzb2x2ZWQuX3BhcnRzLnByb3RvY29sID0gYmFzZS5fcGFydHMucHJvdG9jb2w7XG4gICAgfVxuXG4gICAgaWYgKHRoaXMuX3BhcnRzLmhvc3RuYW1lKSB7XG4gICAgICByZXR1cm4gcmVzb2x2ZWQ7XG4gICAgfVxuXG4gICAgZm9yIChpID0gMDsgKHAgPSBwcm9wZXJ0aWVzW2ldKTsgaSsrKSB7XG4gICAgICByZXNvbHZlZC5fcGFydHNbcF0gPSBiYXNlLl9wYXJ0c1twXTtcbiAgICB9XG5cbiAgICBpZiAoIXJlc29sdmVkLl9wYXJ0cy5wYXRoKSB7XG4gICAgICByZXNvbHZlZC5fcGFydHMucGF0aCA9IGJhc2UuX3BhcnRzLnBhdGg7XG4gICAgICBpZiAoIXJlc29sdmVkLl9wYXJ0cy5xdWVyeSkge1xuICAgICAgICByZXNvbHZlZC5fcGFydHMucXVlcnkgPSBiYXNlLl9wYXJ0cy5xdWVyeTtcbiAgICAgIH1cbiAgICB9IGVsc2UgaWYgKHJlc29sdmVkLl9wYXJ0cy5wYXRoLnN1YnN0cmluZygtMikgPT09ICcuLicpIHtcbiAgICAgIHJlc29sdmVkLl9wYXJ0cy5wYXRoICs9ICcvJztcbiAgICB9XG5cbiAgICBpZiAocmVzb2x2ZWQucGF0aCgpLmNoYXJBdCgwKSAhPT0gJy8nKSB7XG4gICAgICBiYXNlZGlyID0gYmFzZS5kaXJlY3RvcnkoKTtcbiAgICAgIGJhc2VkaXIgPSBiYXNlZGlyID8gYmFzZWRpciA6IGJhc2UucGF0aCgpLmluZGV4T2YoJy8nKSA9PT0gMCA/ICcvJyA6ICcnO1xuICAgICAgcmVzb2x2ZWQuX3BhcnRzLnBhdGggPSAoYmFzZWRpciA/IChiYXNlZGlyICsgJy8nKSA6ICcnKSArIHJlc29sdmVkLl9wYXJ0cy5wYXRoO1xuICAgICAgcmVzb2x2ZWQubm9ybWFsaXplUGF0aCgpO1xuICAgIH1cblxuICAgIHJlc29sdmVkLmJ1aWxkKCk7XG4gICAgcmV0dXJuIHJlc29sdmVkO1xuICB9O1xuICBwLnJlbGF0aXZlVG8gPSBmdW5jdGlvbihiYXNlKSB7XG4gICAgdmFyIHJlbGF0aXZlID0gdGhpcy5jbG9uZSgpLm5vcm1hbGl6ZSgpO1xuICAgIHZhciByZWxhdGl2ZVBhcnRzLCBiYXNlUGFydHMsIGNvbW1vbiwgcmVsYXRpdmVQYXRoLCBiYXNlUGF0aDtcblxuICAgIGlmIChyZWxhdGl2ZS5fcGFydHMudXJuKSB7XG4gICAgICB0aHJvdyBuZXcgRXJyb3IoJ1VSTnMgZG8gbm90IGhhdmUgYW55IGdlbmVyYWxseSBkZWZpbmVkIGhpZXJhcmNoaWNhbCBjb21wb25lbnRzJyk7XG4gICAgfVxuXG4gICAgYmFzZSA9IG5ldyBVUkkoYmFzZSkubm9ybWFsaXplKCk7XG4gICAgcmVsYXRpdmVQYXJ0cyA9IHJlbGF0aXZlLl9wYXJ0cztcbiAgICBiYXNlUGFydHMgPSBiYXNlLl9wYXJ0cztcbiAgICByZWxhdGl2ZVBhdGggPSByZWxhdGl2ZS5wYXRoKCk7XG4gICAgYmFzZVBhdGggPSBiYXNlLnBhdGgoKTtcblxuICAgIGlmIChyZWxhdGl2ZVBhdGguY2hhckF0KDApICE9PSAnLycpIHtcbiAgICAgIHRocm93IG5ldyBFcnJvcignVVJJIGlzIGFscmVhZHkgcmVsYXRpdmUnKTtcbiAgICB9XG5cbiAgICBpZiAoYmFzZVBhdGguY2hhckF0KDApICE9PSAnLycpIHtcbiAgICAgIHRocm93IG5ldyBFcnJvcignQ2Fubm90IGNhbGN1bGF0ZSBhIFVSSSByZWxhdGl2ZSB0byBhbm90aGVyIHJlbGF0aXZlIFVSSScpO1xuICAgIH1cblxuICAgIGlmIChyZWxhdGl2ZVBhcnRzLnByb3RvY29sID09PSBiYXNlUGFydHMucHJvdG9jb2wpIHtcbiAgICAgIHJlbGF0aXZlUGFydHMucHJvdG9jb2wgPSBudWxsO1xuICAgIH1cblxuICAgIGlmIChyZWxhdGl2ZVBhcnRzLnVzZXJuYW1lICE9PSBiYXNlUGFydHMudXNlcm5hbWUgfHwgcmVsYXRpdmVQYXJ0cy5wYXNzd29yZCAhPT0gYmFzZVBhcnRzLnBhc3N3b3JkKSB7XG4gICAgICByZXR1cm4gcmVsYXRpdmUuYnVpbGQoKTtcbiAgICB9XG5cbiAgICBpZiAocmVsYXRpdmVQYXJ0cy5wcm90b2NvbCAhPT0gbnVsbCB8fCByZWxhdGl2ZVBhcnRzLnVzZXJuYW1lICE9PSBudWxsIHx8IHJlbGF0aXZlUGFydHMucGFzc3dvcmQgIT09IG51bGwpIHtcbiAgICAgIHJldHVybiByZWxhdGl2ZS5idWlsZCgpO1xuICAgIH1cblxuICAgIGlmIChyZWxhdGl2ZVBhcnRzLmhvc3RuYW1lID09PSBiYXNlUGFydHMuaG9zdG5hbWUgJiYgcmVsYXRpdmVQYXJ0cy5wb3J0ID09PSBiYXNlUGFydHMucG9ydCkge1xuICAgICAgcmVsYXRpdmVQYXJ0cy5ob3N0bmFtZSA9IG51bGw7XG4gICAgICByZWxhdGl2ZVBhcnRzLnBvcnQgPSBudWxsO1xuICAgIH0gZWxzZSB7XG4gICAgICByZXR1cm4gcmVsYXRpdmUuYnVpbGQoKTtcbiAgICB9XG5cbiAgICBpZiAocmVsYXRpdmVQYXRoID09PSBiYXNlUGF0aCkge1xuICAgICAgcmVsYXRpdmVQYXJ0cy5wYXRoID0gJyc7XG4gICAgICByZXR1cm4gcmVsYXRpdmUuYnVpbGQoKTtcbiAgICB9XG5cbiAgICAvLyBkZXRlcm1pbmUgY29tbW9uIHN1YiBwYXRoXG4gICAgY29tbW9uID0gVVJJLmNvbW1vblBhdGgocmVsYXRpdmVQYXRoLCBiYXNlUGF0aCk7XG5cbiAgICAvLyBJZiB0aGUgcGF0aHMgaGF2ZSBub3RoaW5nIGluIGNvbW1vbiwgcmV0dXJuIGEgcmVsYXRpdmUgVVJMIHdpdGggdGhlIGFic29sdXRlIHBhdGguXG4gICAgaWYgKCFjb21tb24pIHtcbiAgICAgIHJldHVybiByZWxhdGl2ZS5idWlsZCgpO1xuICAgIH1cblxuICAgIHZhciBwYXJlbnRzID0gYmFzZVBhcnRzLnBhdGhcbiAgICAgIC5zdWJzdHJpbmcoY29tbW9uLmxlbmd0aClcbiAgICAgIC5yZXBsYWNlKC9bXlxcL10qJC8sICcnKVxuICAgICAgLnJlcGxhY2UoLy4qP1xcLy9nLCAnLi4vJyk7XG5cbiAgICByZWxhdGl2ZVBhcnRzLnBhdGggPSAocGFyZW50cyArIHJlbGF0aXZlUGFydHMucGF0aC5zdWJzdHJpbmcoY29tbW9uLmxlbmd0aCkpIHx8ICcuLyc7XG5cbiAgICByZXR1cm4gcmVsYXRpdmUuYnVpbGQoKTtcbiAgfTtcblxuICAvLyBjb21wYXJpbmcgVVJJc1xuICBwLmVxdWFscyA9IGZ1bmN0aW9uKHVyaSkge1xuICAgIHZhciBvbmUgPSB0aGlzLmNsb25lKCk7XG4gICAgdmFyIHR3byA9IG5ldyBVUkkodXJpKTtcbiAgICB2YXIgb25lX21hcCA9IHt9O1xuICAgIHZhciB0d29fbWFwID0ge307XG4gICAgdmFyIGNoZWNrZWQgPSB7fTtcbiAgICB2YXIgb25lX3F1ZXJ5LCB0d29fcXVlcnksIGtleTtcblxuICAgIG9uZS5ub3JtYWxpemUoKTtcbiAgICB0d28ubm9ybWFsaXplKCk7XG5cbiAgICAvLyBleGFjdCBtYXRjaFxuICAgIGlmIChvbmUudG9TdHJpbmcoKSA9PT0gdHdvLnRvU3RyaW5nKCkpIHtcbiAgICAgIHJldHVybiB0cnVlO1xuICAgIH1cblxuICAgIC8vIGV4dHJhY3QgcXVlcnkgc3RyaW5nXG4gICAgb25lX3F1ZXJ5ID0gb25lLnF1ZXJ5KCk7XG4gICAgdHdvX3F1ZXJ5ID0gdHdvLnF1ZXJ5KCk7XG4gICAgb25lLnF1ZXJ5KCcnKTtcbiAgICB0d28ucXVlcnkoJycpO1xuXG4gICAgLy8gZGVmaW5pdGVseSBub3QgZXF1YWwgaWYgbm90IGV2ZW4gbm9uLXF1ZXJ5IHBhcnRzIG1hdGNoXG4gICAgaWYgKG9uZS50b1N0cmluZygpICE9PSB0d28udG9TdHJpbmcoKSkge1xuICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cblxuICAgIC8vIHF1ZXJ5IHBhcmFtZXRlcnMgaGF2ZSB0aGUgc2FtZSBsZW5ndGgsIGV2ZW4gaWYgdGhleSdyZSBwZXJtdXRlZFxuICAgIGlmIChvbmVfcXVlcnkubGVuZ3RoICE9PSB0d29fcXVlcnkubGVuZ3RoKSB7XG4gICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuXG4gICAgb25lX21hcCA9IFVSSS5wYXJzZVF1ZXJ5KG9uZV9xdWVyeSwgdGhpcy5fcGFydHMuZXNjYXBlUXVlcnlTcGFjZSk7XG4gICAgdHdvX21hcCA9IFVSSS5wYXJzZVF1ZXJ5KHR3b19xdWVyeSwgdGhpcy5fcGFydHMuZXNjYXBlUXVlcnlTcGFjZSk7XG5cbiAgICBmb3IgKGtleSBpbiBvbmVfbWFwKSB7XG4gICAgICBpZiAoaGFzT3duLmNhbGwob25lX21hcCwga2V5KSkge1xuICAgICAgICBpZiAoIWlzQXJyYXkob25lX21hcFtrZXldKSkge1xuICAgICAgICAgIGlmIChvbmVfbWFwW2tleV0gIT09IHR3b19tYXBba2V5XSkge1xuICAgICAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgICAgICAgIH1cbiAgICAgICAgfSBlbHNlIGlmICghYXJyYXlzRXF1YWwob25lX21hcFtrZXldLCB0d29fbWFwW2tleV0pKSB7XG4gICAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgICAgICB9XG5cbiAgICAgICAgY2hlY2tlZFtrZXldID0gdHJ1ZTtcbiAgICAgIH1cbiAgICB9XG5cbiAgICBmb3IgKGtleSBpbiB0d29fbWFwKSB7XG4gICAgICBpZiAoaGFzT3duLmNhbGwodHdvX21hcCwga2V5KSkge1xuICAgICAgICBpZiAoIWNoZWNrZWRba2V5XSkge1xuICAgICAgICAgIC8vIHR3byBjb250YWlucyBhIHBhcmFtZXRlciBub3QgcHJlc2VudCBpbiBvbmVcbiAgICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9XG5cbiAgICByZXR1cm4gdHJ1ZTtcbiAgfTtcblxuICAvLyBzdGF0ZVxuICBwLmR1cGxpY2F0ZVF1ZXJ5UGFyYW1ldGVycyA9IGZ1bmN0aW9uKHYpIHtcbiAgICB0aGlzLl9wYXJ0cy5kdXBsaWNhdGVRdWVyeVBhcmFtZXRlcnMgPSAhIXY7XG4gICAgcmV0dXJuIHRoaXM7XG4gIH07XG5cbiAgcC5lc2NhcGVRdWVyeVNwYWNlID0gZnVuY3Rpb24odikge1xuICAgIHRoaXMuX3BhcnRzLmVzY2FwZVF1ZXJ5U3BhY2UgPSAhIXY7XG4gICAgcmV0dXJuIHRoaXM7XG4gIH07XG5cbiAgcmV0dXJuIFVSSTtcbn0pKTtcbiIsIi8qISBodHRwOi8vbXRocy5iZS9wdW55Y29kZSB2MS4yLjMgYnkgQG1hdGhpYXMgKi9cbjsoZnVuY3Rpb24ocm9vdCkge1xuXG5cdC8qKiBEZXRlY3QgZnJlZSB2YXJpYWJsZXMgKi9cblx0dmFyIGZyZWVFeHBvcnRzID0gdHlwZW9mIGV4cG9ydHMgPT0gJ29iamVjdCcgJiYgZXhwb3J0cztcblx0dmFyIGZyZWVNb2R1bGUgPSB0eXBlb2YgbW9kdWxlID09ICdvYmplY3QnICYmIG1vZHVsZSAmJlxuXHRcdG1vZHVsZS5leHBvcnRzID09IGZyZWVFeHBvcnRzICYmIG1vZHVsZTtcblx0dmFyIGZyZWVHbG9iYWwgPSB0eXBlb2YgZ2xvYmFsID09ICdvYmplY3QnICYmIGdsb2JhbDtcblx0aWYgKGZyZWVHbG9iYWwuZ2xvYmFsID09PSBmcmVlR2xvYmFsIHx8IGZyZWVHbG9iYWwud2luZG93ID09PSBmcmVlR2xvYmFsKSB7XG5cdFx0cm9vdCA9IGZyZWVHbG9iYWw7XG5cdH1cblxuXHQvKipcblx0ICogVGhlIGBwdW55Y29kZWAgb2JqZWN0LlxuXHQgKiBAbmFtZSBwdW55Y29kZVxuXHQgKiBAdHlwZSBPYmplY3Rcblx0ICovXG5cdHZhciBwdW55Y29kZSxcblxuXHQvKiogSGlnaGVzdCBwb3NpdGl2ZSBzaWduZWQgMzItYml0IGZsb2F0IHZhbHVlICovXG5cdG1heEludCA9IDIxNDc0ODM2NDcsIC8vIGFrYS4gMHg3RkZGRkZGRiBvciAyXjMxLTFcblxuXHQvKiogQm9vdHN0cmluZyBwYXJhbWV0ZXJzICovXG5cdGJhc2UgPSAzNixcblx0dE1pbiA9IDEsXG5cdHRNYXggPSAyNixcblx0c2tldyA9IDM4LFxuXHRkYW1wID0gNzAwLFxuXHRpbml0aWFsQmlhcyA9IDcyLFxuXHRpbml0aWFsTiA9IDEyOCwgLy8gMHg4MFxuXHRkZWxpbWl0ZXIgPSAnLScsIC8vICdcXHgyRCdcblxuXHQvKiogUmVndWxhciBleHByZXNzaW9ucyAqL1xuXHRyZWdleFB1bnljb2RlID0gL154bi0tLyxcblx0cmVnZXhOb25BU0NJSSA9IC9bXiAtfl0vLCAvLyB1bnByaW50YWJsZSBBU0NJSSBjaGFycyArIG5vbi1BU0NJSSBjaGFyc1xuXHRyZWdleFNlcGFyYXRvcnMgPSAvXFx4MkV8XFx1MzAwMnxcXHVGRjBFfFxcdUZGNjEvZywgLy8gUkZDIDM0OTAgc2VwYXJhdG9yc1xuXG5cdC8qKiBFcnJvciBtZXNzYWdlcyAqL1xuXHRlcnJvcnMgPSB7XG5cdFx0J292ZXJmbG93JzogJ092ZXJmbG93OiBpbnB1dCBuZWVkcyB3aWRlciBpbnRlZ2VycyB0byBwcm9jZXNzJyxcblx0XHQnbm90LWJhc2ljJzogJ0lsbGVnYWwgaW5wdXQgPj0gMHg4MCAobm90IGEgYmFzaWMgY29kZSBwb2ludCknLFxuXHRcdCdpbnZhbGlkLWlucHV0JzogJ0ludmFsaWQgaW5wdXQnXG5cdH0sXG5cblx0LyoqIENvbnZlbmllbmNlIHNob3J0Y3V0cyAqL1xuXHRiYXNlTWludXNUTWluID0gYmFzZSAtIHRNaW4sXG5cdGZsb29yID0gTWF0aC5mbG9vcixcblx0c3RyaW5nRnJvbUNoYXJDb2RlID0gU3RyaW5nLmZyb21DaGFyQ29kZSxcblxuXHQvKiogVGVtcG9yYXJ5IHZhcmlhYmxlICovXG5cdGtleTtcblxuXHQvKi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tKi9cblxuXHQvKipcblx0ICogQSBnZW5lcmljIGVycm9yIHV0aWxpdHkgZnVuY3Rpb24uXG5cdCAqIEBwcml2YXRlXG5cdCAqIEBwYXJhbSB7U3RyaW5nfSB0eXBlIFRoZSBlcnJvciB0eXBlLlxuXHQgKiBAcmV0dXJucyB7RXJyb3J9IFRocm93cyBhIGBSYW5nZUVycm9yYCB3aXRoIHRoZSBhcHBsaWNhYmxlIGVycm9yIG1lc3NhZ2UuXG5cdCAqL1xuXHRmdW5jdGlvbiBlcnJvcih0eXBlKSB7XG5cdFx0dGhyb3cgUmFuZ2VFcnJvcihlcnJvcnNbdHlwZV0pO1xuXHR9XG5cblx0LyoqXG5cdCAqIEEgZ2VuZXJpYyBgQXJyYXkjbWFwYCB1dGlsaXR5IGZ1bmN0aW9uLlxuXHQgKiBAcHJpdmF0ZVxuXHQgKiBAcGFyYW0ge0FycmF5fSBhcnJheSBUaGUgYXJyYXkgdG8gaXRlcmF0ZSBvdmVyLlxuXHQgKiBAcGFyYW0ge0Z1bmN0aW9ufSBjYWxsYmFjayBUaGUgZnVuY3Rpb24gdGhhdCBnZXRzIGNhbGxlZCBmb3IgZXZlcnkgYXJyYXlcblx0ICogaXRlbS5cblx0ICogQHJldHVybnMge0FycmF5fSBBIG5ldyBhcnJheSBvZiB2YWx1ZXMgcmV0dXJuZWQgYnkgdGhlIGNhbGxiYWNrIGZ1bmN0aW9uLlxuXHQgKi9cblx0ZnVuY3Rpb24gbWFwKGFycmF5LCBmbikge1xuXHRcdHZhciBsZW5ndGggPSBhcnJheS5sZW5ndGg7XG5cdFx0d2hpbGUgKGxlbmd0aC0tKSB7XG5cdFx0XHRhcnJheVtsZW5ndGhdID0gZm4oYXJyYXlbbGVuZ3RoXSk7XG5cdFx0fVxuXHRcdHJldHVybiBhcnJheTtcblx0fVxuXG5cdC8qKlxuXHQgKiBBIHNpbXBsZSBgQXJyYXkjbWFwYC1saWtlIHdyYXBwZXIgdG8gd29yayB3aXRoIGRvbWFpbiBuYW1lIHN0cmluZ3MuXG5cdCAqIEBwcml2YXRlXG5cdCAqIEBwYXJhbSB7U3RyaW5nfSBkb21haW4gVGhlIGRvbWFpbiBuYW1lLlxuXHQgKiBAcGFyYW0ge0Z1bmN0aW9ufSBjYWxsYmFjayBUaGUgZnVuY3Rpb24gdGhhdCBnZXRzIGNhbGxlZCBmb3IgZXZlcnlcblx0ICogY2hhcmFjdGVyLlxuXHQgKiBAcmV0dXJucyB7QXJyYXl9IEEgbmV3IHN0cmluZyBvZiBjaGFyYWN0ZXJzIHJldHVybmVkIGJ5IHRoZSBjYWxsYmFja1xuXHQgKiBmdW5jdGlvbi5cblx0ICovXG5cdGZ1bmN0aW9uIG1hcERvbWFpbihzdHJpbmcsIGZuKSB7XG5cdFx0cmV0dXJuIG1hcChzdHJpbmcuc3BsaXQocmVnZXhTZXBhcmF0b3JzKSwgZm4pLmpvaW4oJy4nKTtcblx0fVxuXG5cdC8qKlxuXHQgKiBDcmVhdGVzIGFuIGFycmF5IGNvbnRhaW5pbmcgdGhlIG51bWVyaWMgY29kZSBwb2ludHMgb2YgZWFjaCBVbmljb2RlXG5cdCAqIGNoYXJhY3RlciBpbiB0aGUgc3RyaW5nLiBXaGlsZSBKYXZhU2NyaXB0IHVzZXMgVUNTLTIgaW50ZXJuYWxseSxcblx0ICogdGhpcyBmdW5jdGlvbiB3aWxsIGNvbnZlcnQgYSBwYWlyIG9mIHN1cnJvZ2F0ZSBoYWx2ZXMgKGVhY2ggb2Ygd2hpY2hcblx0ICogVUNTLTIgZXhwb3NlcyBhcyBzZXBhcmF0ZSBjaGFyYWN0ZXJzKSBpbnRvIGEgc2luZ2xlIGNvZGUgcG9pbnQsXG5cdCAqIG1hdGNoaW5nIFVURi0xNi5cblx0ICogQHNlZSBgcHVueWNvZGUudWNzMi5lbmNvZGVgXG5cdCAqIEBzZWUgPGh0dHA6Ly9tYXRoaWFzYnluZW5zLmJlL25vdGVzL2phdmFzY3JpcHQtZW5jb2Rpbmc+XG5cdCAqIEBtZW1iZXJPZiBwdW55Y29kZS51Y3MyXG5cdCAqIEBuYW1lIGRlY29kZVxuXHQgKiBAcGFyYW0ge1N0cmluZ30gc3RyaW5nIFRoZSBVbmljb2RlIGlucHV0IHN0cmluZyAoVUNTLTIpLlxuXHQgKiBAcmV0dXJucyB7QXJyYXl9IFRoZSBuZXcgYXJyYXkgb2YgY29kZSBwb2ludHMuXG5cdCAqL1xuXHRmdW5jdGlvbiB1Y3MyZGVjb2RlKHN0cmluZykge1xuXHRcdHZhciBvdXRwdXQgPSBbXSxcblx0XHQgICAgY291bnRlciA9IDAsXG5cdFx0ICAgIGxlbmd0aCA9IHN0cmluZy5sZW5ndGgsXG5cdFx0ICAgIHZhbHVlLFxuXHRcdCAgICBleHRyYTtcblx0XHR3aGlsZSAoY291bnRlciA8IGxlbmd0aCkge1xuXHRcdFx0dmFsdWUgPSBzdHJpbmcuY2hhckNvZGVBdChjb3VudGVyKyspO1xuXHRcdFx0aWYgKHZhbHVlID49IDB4RDgwMCAmJiB2YWx1ZSA8PSAweERCRkYgJiYgY291bnRlciA8IGxlbmd0aCkge1xuXHRcdFx0XHQvLyBoaWdoIHN1cnJvZ2F0ZSwgYW5kIHRoZXJlIGlzIGEgbmV4dCBjaGFyYWN0ZXJcblx0XHRcdFx0ZXh0cmEgPSBzdHJpbmcuY2hhckNvZGVBdChjb3VudGVyKyspO1xuXHRcdFx0XHRpZiAoKGV4dHJhICYgMHhGQzAwKSA9PSAweERDMDApIHsgLy8gbG93IHN1cnJvZ2F0ZVxuXHRcdFx0XHRcdG91dHB1dC5wdXNoKCgodmFsdWUgJiAweDNGRikgPDwgMTApICsgKGV4dHJhICYgMHgzRkYpICsgMHgxMDAwMCk7XG5cdFx0XHRcdH0gZWxzZSB7XG5cdFx0XHRcdFx0Ly8gdW5tYXRjaGVkIHN1cnJvZ2F0ZTsgb25seSBhcHBlbmQgdGhpcyBjb2RlIHVuaXQsIGluIGNhc2UgdGhlIG5leHRcblx0XHRcdFx0XHQvLyBjb2RlIHVuaXQgaXMgdGhlIGhpZ2ggc3Vycm9nYXRlIG9mIGEgc3Vycm9nYXRlIHBhaXJcblx0XHRcdFx0XHRvdXRwdXQucHVzaCh2YWx1ZSk7XG5cdFx0XHRcdFx0Y291bnRlci0tO1xuXHRcdFx0XHR9XG5cdFx0XHR9IGVsc2Uge1xuXHRcdFx0XHRvdXRwdXQucHVzaCh2YWx1ZSk7XG5cdFx0XHR9XG5cdFx0fVxuXHRcdHJldHVybiBvdXRwdXQ7XG5cdH1cblxuXHQvKipcblx0ICogQ3JlYXRlcyBhIHN0cmluZyBiYXNlZCBvbiBhbiBhcnJheSBvZiBudW1lcmljIGNvZGUgcG9pbnRzLlxuXHQgKiBAc2VlIGBwdW55Y29kZS51Y3MyLmRlY29kZWBcblx0ICogQG1lbWJlck9mIHB1bnljb2RlLnVjczJcblx0ICogQG5hbWUgZW5jb2RlXG5cdCAqIEBwYXJhbSB7QXJyYXl9IGNvZGVQb2ludHMgVGhlIGFycmF5IG9mIG51bWVyaWMgY29kZSBwb2ludHMuXG5cdCAqIEByZXR1cm5zIHtTdHJpbmd9IFRoZSBuZXcgVW5pY29kZSBzdHJpbmcgKFVDUy0yKS5cblx0ICovXG5cdGZ1bmN0aW9uIHVjczJlbmNvZGUoYXJyYXkpIHtcblx0XHRyZXR1cm4gbWFwKGFycmF5LCBmdW5jdGlvbih2YWx1ZSkge1xuXHRcdFx0dmFyIG91dHB1dCA9ICcnO1xuXHRcdFx0aWYgKHZhbHVlID4gMHhGRkZGKSB7XG5cdFx0XHRcdHZhbHVlIC09IDB4MTAwMDA7XG5cdFx0XHRcdG91dHB1dCArPSBzdHJpbmdGcm9tQ2hhckNvZGUodmFsdWUgPj4+IDEwICYgMHgzRkYgfCAweEQ4MDApO1xuXHRcdFx0XHR2YWx1ZSA9IDB4REMwMCB8IHZhbHVlICYgMHgzRkY7XG5cdFx0XHR9XG5cdFx0XHRvdXRwdXQgKz0gc3RyaW5nRnJvbUNoYXJDb2RlKHZhbHVlKTtcblx0XHRcdHJldHVybiBvdXRwdXQ7XG5cdFx0fSkuam9pbignJyk7XG5cdH1cblxuXHQvKipcblx0ICogQ29udmVydHMgYSBiYXNpYyBjb2RlIHBvaW50IGludG8gYSBkaWdpdC9pbnRlZ2VyLlxuXHQgKiBAc2VlIGBkaWdpdFRvQmFzaWMoKWBcblx0ICogQHByaXZhdGVcblx0ICogQHBhcmFtIHtOdW1iZXJ9IGNvZGVQb2ludCBUaGUgYmFzaWMgbnVtZXJpYyBjb2RlIHBvaW50IHZhbHVlLlxuXHQgKiBAcmV0dXJucyB7TnVtYmVyfSBUaGUgbnVtZXJpYyB2YWx1ZSBvZiBhIGJhc2ljIGNvZGUgcG9pbnQgKGZvciB1c2UgaW5cblx0ICogcmVwcmVzZW50aW5nIGludGVnZXJzKSBpbiB0aGUgcmFuZ2UgYDBgIHRvIGBiYXNlIC0gMWAsIG9yIGBiYXNlYCBpZlxuXHQgKiB0aGUgY29kZSBwb2ludCBkb2VzIG5vdCByZXByZXNlbnQgYSB2YWx1ZS5cblx0ICovXG5cdGZ1bmN0aW9uIGJhc2ljVG9EaWdpdChjb2RlUG9pbnQpIHtcblx0XHRpZiAoY29kZVBvaW50IC0gNDggPCAxMCkge1xuXHRcdFx0cmV0dXJuIGNvZGVQb2ludCAtIDIyO1xuXHRcdH1cblx0XHRpZiAoY29kZVBvaW50IC0gNjUgPCAyNikge1xuXHRcdFx0cmV0dXJuIGNvZGVQb2ludCAtIDY1O1xuXHRcdH1cblx0XHRpZiAoY29kZVBvaW50IC0gOTcgPCAyNikge1xuXHRcdFx0cmV0dXJuIGNvZGVQb2ludCAtIDk3O1xuXHRcdH1cblx0XHRyZXR1cm4gYmFzZTtcblx0fVxuXG5cdC8qKlxuXHQgKiBDb252ZXJ0cyBhIGRpZ2l0L2ludGVnZXIgaW50byBhIGJhc2ljIGNvZGUgcG9pbnQuXG5cdCAqIEBzZWUgYGJhc2ljVG9EaWdpdCgpYFxuXHQgKiBAcHJpdmF0ZVxuXHQgKiBAcGFyYW0ge051bWJlcn0gZGlnaXQgVGhlIG51bWVyaWMgdmFsdWUgb2YgYSBiYXNpYyBjb2RlIHBvaW50LlxuXHQgKiBAcmV0dXJucyB7TnVtYmVyfSBUaGUgYmFzaWMgY29kZSBwb2ludCB3aG9zZSB2YWx1ZSAod2hlbiB1c2VkIGZvclxuXHQgKiByZXByZXNlbnRpbmcgaW50ZWdlcnMpIGlzIGBkaWdpdGAsIHdoaWNoIG5lZWRzIHRvIGJlIGluIHRoZSByYW5nZVxuXHQgKiBgMGAgdG8gYGJhc2UgLSAxYC4gSWYgYGZsYWdgIGlzIG5vbi16ZXJvLCB0aGUgdXBwZXJjYXNlIGZvcm0gaXNcblx0ICogdXNlZDsgZWxzZSwgdGhlIGxvd2VyY2FzZSBmb3JtIGlzIHVzZWQuIFRoZSBiZWhhdmlvciBpcyB1bmRlZmluZWRcblx0ICogaWYgYGZsYWdgIGlzIG5vbi16ZXJvIGFuZCBgZGlnaXRgIGhhcyBubyB1cHBlcmNhc2UgZm9ybS5cblx0ICovXG5cdGZ1bmN0aW9uIGRpZ2l0VG9CYXNpYyhkaWdpdCwgZmxhZykge1xuXHRcdC8vICAwLi4yNSBtYXAgdG8gQVNDSUkgYS4ueiBvciBBLi5aXG5cdFx0Ly8gMjYuLjM1IG1hcCB0byBBU0NJSSAwLi45XG5cdFx0cmV0dXJuIGRpZ2l0ICsgMjIgKyA3NSAqIChkaWdpdCA8IDI2KSAtICgoZmxhZyAhPSAwKSA8PCA1KTtcblx0fVxuXG5cdC8qKlxuXHQgKiBCaWFzIGFkYXB0YXRpb24gZnVuY3Rpb24gYXMgcGVyIHNlY3Rpb24gMy40IG9mIFJGQyAzNDkyLlxuXHQgKiBodHRwOi8vdG9vbHMuaWV0Zi5vcmcvaHRtbC9yZmMzNDkyI3NlY3Rpb24tMy40XG5cdCAqIEBwcml2YXRlXG5cdCAqL1xuXHRmdW5jdGlvbiBhZGFwdChkZWx0YSwgbnVtUG9pbnRzLCBmaXJzdFRpbWUpIHtcblx0XHR2YXIgayA9IDA7XG5cdFx0ZGVsdGEgPSBmaXJzdFRpbWUgPyBmbG9vcihkZWx0YSAvIGRhbXApIDogZGVsdGEgPj4gMTtcblx0XHRkZWx0YSArPSBmbG9vcihkZWx0YSAvIG51bVBvaW50cyk7XG5cdFx0Zm9yICgvKiBubyBpbml0aWFsaXphdGlvbiAqLzsgZGVsdGEgPiBiYXNlTWludXNUTWluICogdE1heCA+PiAxOyBrICs9IGJhc2UpIHtcblx0XHRcdGRlbHRhID0gZmxvb3IoZGVsdGEgLyBiYXNlTWludXNUTWluKTtcblx0XHR9XG5cdFx0cmV0dXJuIGZsb29yKGsgKyAoYmFzZU1pbnVzVE1pbiArIDEpICogZGVsdGEgLyAoZGVsdGEgKyBza2V3KSk7XG5cdH1cblxuXHQvKipcblx0ICogQ29udmVydHMgYSBQdW55Y29kZSBzdHJpbmcgb2YgQVNDSUktb25seSBzeW1ib2xzIHRvIGEgc3RyaW5nIG9mIFVuaWNvZGVcblx0ICogc3ltYm9scy5cblx0ICogQG1lbWJlck9mIHB1bnljb2RlXG5cdCAqIEBwYXJhbSB7U3RyaW5nfSBpbnB1dCBUaGUgUHVueWNvZGUgc3RyaW5nIG9mIEFTQ0lJLW9ubHkgc3ltYm9scy5cblx0ICogQHJldHVybnMge1N0cmluZ30gVGhlIHJlc3VsdGluZyBzdHJpbmcgb2YgVW5pY29kZSBzeW1ib2xzLlxuXHQgKi9cblx0ZnVuY3Rpb24gZGVjb2RlKGlucHV0KSB7XG5cdFx0Ly8gRG9uJ3QgdXNlIFVDUy0yXG5cdFx0dmFyIG91dHB1dCA9IFtdLFxuXHRcdCAgICBpbnB1dExlbmd0aCA9IGlucHV0Lmxlbmd0aCxcblx0XHQgICAgb3V0LFxuXHRcdCAgICBpID0gMCxcblx0XHQgICAgbiA9IGluaXRpYWxOLFxuXHRcdCAgICBiaWFzID0gaW5pdGlhbEJpYXMsXG5cdFx0ICAgIGJhc2ljLFxuXHRcdCAgICBqLFxuXHRcdCAgICBpbmRleCxcblx0XHQgICAgb2xkaSxcblx0XHQgICAgdyxcblx0XHQgICAgayxcblx0XHQgICAgZGlnaXQsXG5cdFx0ICAgIHQsXG5cdFx0ICAgIGxlbmd0aCxcblx0XHQgICAgLyoqIENhY2hlZCBjYWxjdWxhdGlvbiByZXN1bHRzICovXG5cdFx0ICAgIGJhc2VNaW51c1Q7XG5cblx0XHQvLyBIYW5kbGUgdGhlIGJhc2ljIGNvZGUgcG9pbnRzOiBsZXQgYGJhc2ljYCBiZSB0aGUgbnVtYmVyIG9mIGlucHV0IGNvZGVcblx0XHQvLyBwb2ludHMgYmVmb3JlIHRoZSBsYXN0IGRlbGltaXRlciwgb3IgYDBgIGlmIHRoZXJlIGlzIG5vbmUsIHRoZW4gY29weVxuXHRcdC8vIHRoZSBmaXJzdCBiYXNpYyBjb2RlIHBvaW50cyB0byB0aGUgb3V0cHV0LlxuXG5cdFx0YmFzaWMgPSBpbnB1dC5sYXN0SW5kZXhPZihkZWxpbWl0ZXIpO1xuXHRcdGlmIChiYXNpYyA8IDApIHtcblx0XHRcdGJhc2ljID0gMDtcblx0XHR9XG5cblx0XHRmb3IgKGogPSAwOyBqIDwgYmFzaWM7ICsraikge1xuXHRcdFx0Ly8gaWYgaXQncyBub3QgYSBiYXNpYyBjb2RlIHBvaW50XG5cdFx0XHRpZiAoaW5wdXQuY2hhckNvZGVBdChqKSA+PSAweDgwKSB7XG5cdFx0XHRcdGVycm9yKCdub3QtYmFzaWMnKTtcblx0XHRcdH1cblx0XHRcdG91dHB1dC5wdXNoKGlucHV0LmNoYXJDb2RlQXQoaikpO1xuXHRcdH1cblxuXHRcdC8vIE1haW4gZGVjb2RpbmcgbG9vcDogc3RhcnQganVzdCBhZnRlciB0aGUgbGFzdCBkZWxpbWl0ZXIgaWYgYW55IGJhc2ljIGNvZGVcblx0XHQvLyBwb2ludHMgd2VyZSBjb3BpZWQ7IHN0YXJ0IGF0IHRoZSBiZWdpbm5pbmcgb3RoZXJ3aXNlLlxuXG5cdFx0Zm9yIChpbmRleCA9IGJhc2ljID4gMCA/IGJhc2ljICsgMSA6IDA7IGluZGV4IDwgaW5wdXRMZW5ndGg7IC8qIG5vIGZpbmFsIGV4cHJlc3Npb24gKi8pIHtcblxuXHRcdFx0Ly8gYGluZGV4YCBpcyB0aGUgaW5kZXggb2YgdGhlIG5leHQgY2hhcmFjdGVyIHRvIGJlIGNvbnN1bWVkLlxuXHRcdFx0Ly8gRGVjb2RlIGEgZ2VuZXJhbGl6ZWQgdmFyaWFibGUtbGVuZ3RoIGludGVnZXIgaW50byBgZGVsdGFgLFxuXHRcdFx0Ly8gd2hpY2ggZ2V0cyBhZGRlZCB0byBgaWAuIFRoZSBvdmVyZmxvdyBjaGVja2luZyBpcyBlYXNpZXJcblx0XHRcdC8vIGlmIHdlIGluY3JlYXNlIGBpYCBhcyB3ZSBnbywgdGhlbiBzdWJ0cmFjdCBvZmYgaXRzIHN0YXJ0aW5nXG5cdFx0XHQvLyB2YWx1ZSBhdCB0aGUgZW5kIHRvIG9idGFpbiBgZGVsdGFgLlxuXHRcdFx0Zm9yIChvbGRpID0gaSwgdyA9IDEsIGsgPSBiYXNlOyAvKiBubyBjb25kaXRpb24gKi87IGsgKz0gYmFzZSkge1xuXG5cdFx0XHRcdGlmIChpbmRleCA+PSBpbnB1dExlbmd0aCkge1xuXHRcdFx0XHRcdGVycm9yKCdpbnZhbGlkLWlucHV0Jyk7XG5cdFx0XHRcdH1cblxuXHRcdFx0XHRkaWdpdCA9IGJhc2ljVG9EaWdpdChpbnB1dC5jaGFyQ29kZUF0KGluZGV4KyspKTtcblxuXHRcdFx0XHRpZiAoZGlnaXQgPj0gYmFzZSB8fCBkaWdpdCA+IGZsb29yKChtYXhJbnQgLSBpKSAvIHcpKSB7XG5cdFx0XHRcdFx0ZXJyb3IoJ292ZXJmbG93Jyk7XG5cdFx0XHRcdH1cblxuXHRcdFx0XHRpICs9IGRpZ2l0ICogdztcblx0XHRcdFx0dCA9IGsgPD0gYmlhcyA/IHRNaW4gOiAoayA+PSBiaWFzICsgdE1heCA/IHRNYXggOiBrIC0gYmlhcyk7XG5cblx0XHRcdFx0aWYgKGRpZ2l0IDwgdCkge1xuXHRcdFx0XHRcdGJyZWFrO1xuXHRcdFx0XHR9XG5cblx0XHRcdFx0YmFzZU1pbnVzVCA9IGJhc2UgLSB0O1xuXHRcdFx0XHRpZiAodyA+IGZsb29yKG1heEludCAvIGJhc2VNaW51c1QpKSB7XG5cdFx0XHRcdFx0ZXJyb3IoJ292ZXJmbG93Jyk7XG5cdFx0XHRcdH1cblxuXHRcdFx0XHR3ICo9IGJhc2VNaW51c1Q7XG5cblx0XHRcdH1cblxuXHRcdFx0b3V0ID0gb3V0cHV0Lmxlbmd0aCArIDE7XG5cdFx0XHRiaWFzID0gYWRhcHQoaSAtIG9sZGksIG91dCwgb2xkaSA9PSAwKTtcblxuXHRcdFx0Ly8gYGlgIHdhcyBzdXBwb3NlZCB0byB3cmFwIGFyb3VuZCBmcm9tIGBvdXRgIHRvIGAwYCxcblx0XHRcdC8vIGluY3JlbWVudGluZyBgbmAgZWFjaCB0aW1lLCBzbyB3ZSdsbCBmaXggdGhhdCBub3c6XG5cdFx0XHRpZiAoZmxvb3IoaSAvIG91dCkgPiBtYXhJbnQgLSBuKSB7XG5cdFx0XHRcdGVycm9yKCdvdmVyZmxvdycpO1xuXHRcdFx0fVxuXG5cdFx0XHRuICs9IGZsb29yKGkgLyBvdXQpO1xuXHRcdFx0aSAlPSBvdXQ7XG5cblx0XHRcdC8vIEluc2VydCBgbmAgYXQgcG9zaXRpb24gYGlgIG9mIHRoZSBvdXRwdXRcblx0XHRcdG91dHB1dC5zcGxpY2UoaSsrLCAwLCBuKTtcblxuXHRcdH1cblxuXHRcdHJldHVybiB1Y3MyZW5jb2RlKG91dHB1dCk7XG5cdH1cblxuXHQvKipcblx0ICogQ29udmVydHMgYSBzdHJpbmcgb2YgVW5pY29kZSBzeW1ib2xzIHRvIGEgUHVueWNvZGUgc3RyaW5nIG9mIEFTQ0lJLW9ubHlcblx0ICogc3ltYm9scy5cblx0ICogQG1lbWJlck9mIHB1bnljb2RlXG5cdCAqIEBwYXJhbSB7U3RyaW5nfSBpbnB1dCBUaGUgc3RyaW5nIG9mIFVuaWNvZGUgc3ltYm9scy5cblx0ICogQHJldHVybnMge1N0cmluZ30gVGhlIHJlc3VsdGluZyBQdW55Y29kZSBzdHJpbmcgb2YgQVNDSUktb25seSBzeW1ib2xzLlxuXHQgKi9cblx0ZnVuY3Rpb24gZW5jb2RlKGlucHV0KSB7XG5cdFx0dmFyIG4sXG5cdFx0ICAgIGRlbHRhLFxuXHRcdCAgICBoYW5kbGVkQ1BDb3VudCxcblx0XHQgICAgYmFzaWNMZW5ndGgsXG5cdFx0ICAgIGJpYXMsXG5cdFx0ICAgIGosXG5cdFx0ICAgIG0sXG5cdFx0ICAgIHEsXG5cdFx0ICAgIGssXG5cdFx0ICAgIHQsXG5cdFx0ICAgIGN1cnJlbnRWYWx1ZSxcblx0XHQgICAgb3V0cHV0ID0gW10sXG5cdFx0ICAgIC8qKiBgaW5wdXRMZW5ndGhgIHdpbGwgaG9sZCB0aGUgbnVtYmVyIG9mIGNvZGUgcG9pbnRzIGluIGBpbnB1dGAuICovXG5cdFx0ICAgIGlucHV0TGVuZ3RoLFxuXHRcdCAgICAvKiogQ2FjaGVkIGNhbGN1bGF0aW9uIHJlc3VsdHMgKi9cblx0XHQgICAgaGFuZGxlZENQQ291bnRQbHVzT25lLFxuXHRcdCAgICBiYXNlTWludXNULFxuXHRcdCAgICBxTWludXNUO1xuXG5cdFx0Ly8gQ29udmVydCB0aGUgaW5wdXQgaW4gVUNTLTIgdG8gVW5pY29kZVxuXHRcdGlucHV0ID0gdWNzMmRlY29kZShpbnB1dCk7XG5cblx0XHQvLyBDYWNoZSB0aGUgbGVuZ3RoXG5cdFx0aW5wdXRMZW5ndGggPSBpbnB1dC5sZW5ndGg7XG5cblx0XHQvLyBJbml0aWFsaXplIHRoZSBzdGF0ZVxuXHRcdG4gPSBpbml0aWFsTjtcblx0XHRkZWx0YSA9IDA7XG5cdFx0YmlhcyA9IGluaXRpYWxCaWFzO1xuXG5cdFx0Ly8gSGFuZGxlIHRoZSBiYXNpYyBjb2RlIHBvaW50c1xuXHRcdGZvciAoaiA9IDA7IGogPCBpbnB1dExlbmd0aDsgKytqKSB7XG5cdFx0XHRjdXJyZW50VmFsdWUgPSBpbnB1dFtqXTtcblx0XHRcdGlmIChjdXJyZW50VmFsdWUgPCAweDgwKSB7XG5cdFx0XHRcdG91dHB1dC5wdXNoKHN0cmluZ0Zyb21DaGFyQ29kZShjdXJyZW50VmFsdWUpKTtcblx0XHRcdH1cblx0XHR9XG5cblx0XHRoYW5kbGVkQ1BDb3VudCA9IGJhc2ljTGVuZ3RoID0gb3V0cHV0Lmxlbmd0aDtcblxuXHRcdC8vIGBoYW5kbGVkQ1BDb3VudGAgaXMgdGhlIG51bWJlciBvZiBjb2RlIHBvaW50cyB0aGF0IGhhdmUgYmVlbiBoYW5kbGVkO1xuXHRcdC8vIGBiYXNpY0xlbmd0aGAgaXMgdGhlIG51bWJlciBvZiBiYXNpYyBjb2RlIHBvaW50cy5cblxuXHRcdC8vIEZpbmlzaCB0aGUgYmFzaWMgc3RyaW5nIC0gaWYgaXQgaXMgbm90IGVtcHR5IC0gd2l0aCBhIGRlbGltaXRlclxuXHRcdGlmIChiYXNpY0xlbmd0aCkge1xuXHRcdFx0b3V0cHV0LnB1c2goZGVsaW1pdGVyKTtcblx0XHR9XG5cblx0XHQvLyBNYWluIGVuY29kaW5nIGxvb3A6XG5cdFx0d2hpbGUgKGhhbmRsZWRDUENvdW50IDwgaW5wdXRMZW5ndGgpIHtcblxuXHRcdFx0Ly8gQWxsIG5vbi1iYXNpYyBjb2RlIHBvaW50cyA8IG4gaGF2ZSBiZWVuIGhhbmRsZWQgYWxyZWFkeS4gRmluZCB0aGUgbmV4dFxuXHRcdFx0Ly8gbGFyZ2VyIG9uZTpcblx0XHRcdGZvciAobSA9IG1heEludCwgaiA9IDA7IGogPCBpbnB1dExlbmd0aDsgKytqKSB7XG5cdFx0XHRcdGN1cnJlbnRWYWx1ZSA9IGlucHV0W2pdO1xuXHRcdFx0XHRpZiAoY3VycmVudFZhbHVlID49IG4gJiYgY3VycmVudFZhbHVlIDwgbSkge1xuXHRcdFx0XHRcdG0gPSBjdXJyZW50VmFsdWU7XG5cdFx0XHRcdH1cblx0XHRcdH1cblxuXHRcdFx0Ly8gSW5jcmVhc2UgYGRlbHRhYCBlbm91Z2ggdG8gYWR2YW5jZSB0aGUgZGVjb2RlcidzIDxuLGk+IHN0YXRlIHRvIDxtLDA+LFxuXHRcdFx0Ly8gYnV0IGd1YXJkIGFnYWluc3Qgb3ZlcmZsb3dcblx0XHRcdGhhbmRsZWRDUENvdW50UGx1c09uZSA9IGhhbmRsZWRDUENvdW50ICsgMTtcblx0XHRcdGlmIChtIC0gbiA+IGZsb29yKChtYXhJbnQgLSBkZWx0YSkgLyBoYW5kbGVkQ1BDb3VudFBsdXNPbmUpKSB7XG5cdFx0XHRcdGVycm9yKCdvdmVyZmxvdycpO1xuXHRcdFx0fVxuXG5cdFx0XHRkZWx0YSArPSAobSAtIG4pICogaGFuZGxlZENQQ291bnRQbHVzT25lO1xuXHRcdFx0biA9IG07XG5cblx0XHRcdGZvciAoaiA9IDA7IGogPCBpbnB1dExlbmd0aDsgKytqKSB7XG5cdFx0XHRcdGN1cnJlbnRWYWx1ZSA9IGlucHV0W2pdO1xuXG5cdFx0XHRcdGlmIChjdXJyZW50VmFsdWUgPCBuICYmICsrZGVsdGEgPiBtYXhJbnQpIHtcblx0XHRcdFx0XHRlcnJvcignb3ZlcmZsb3cnKTtcblx0XHRcdFx0fVxuXG5cdFx0XHRcdGlmIChjdXJyZW50VmFsdWUgPT0gbikge1xuXHRcdFx0XHRcdC8vIFJlcHJlc2VudCBkZWx0YSBhcyBhIGdlbmVyYWxpemVkIHZhcmlhYmxlLWxlbmd0aCBpbnRlZ2VyXG5cdFx0XHRcdFx0Zm9yIChxID0gZGVsdGEsIGsgPSBiYXNlOyAvKiBubyBjb25kaXRpb24gKi87IGsgKz0gYmFzZSkge1xuXHRcdFx0XHRcdFx0dCA9IGsgPD0gYmlhcyA/IHRNaW4gOiAoayA+PSBiaWFzICsgdE1heCA/IHRNYXggOiBrIC0gYmlhcyk7XG5cdFx0XHRcdFx0XHRpZiAocSA8IHQpIHtcblx0XHRcdFx0XHRcdFx0YnJlYWs7XG5cdFx0XHRcdFx0XHR9XG5cdFx0XHRcdFx0XHRxTWludXNUID0gcSAtIHQ7XG5cdFx0XHRcdFx0XHRiYXNlTWludXNUID0gYmFzZSAtIHQ7XG5cdFx0XHRcdFx0XHRvdXRwdXQucHVzaChcblx0XHRcdFx0XHRcdFx0c3RyaW5nRnJvbUNoYXJDb2RlKGRpZ2l0VG9CYXNpYyh0ICsgcU1pbnVzVCAlIGJhc2VNaW51c1QsIDApKVxuXHRcdFx0XHRcdFx0KTtcblx0XHRcdFx0XHRcdHEgPSBmbG9vcihxTWludXNUIC8gYmFzZU1pbnVzVCk7XG5cdFx0XHRcdFx0fVxuXG5cdFx0XHRcdFx0b3V0cHV0LnB1c2goc3RyaW5nRnJvbUNoYXJDb2RlKGRpZ2l0VG9CYXNpYyhxLCAwKSkpO1xuXHRcdFx0XHRcdGJpYXMgPSBhZGFwdChkZWx0YSwgaGFuZGxlZENQQ291bnRQbHVzT25lLCBoYW5kbGVkQ1BDb3VudCA9PSBiYXNpY0xlbmd0aCk7XG5cdFx0XHRcdFx0ZGVsdGEgPSAwO1xuXHRcdFx0XHRcdCsraGFuZGxlZENQQ291bnQ7XG5cdFx0XHRcdH1cblx0XHRcdH1cblxuXHRcdFx0KytkZWx0YTtcblx0XHRcdCsrbjtcblxuXHRcdH1cblx0XHRyZXR1cm4gb3V0cHV0LmpvaW4oJycpO1xuXHR9XG5cblx0LyoqXG5cdCAqIENvbnZlcnRzIGEgUHVueWNvZGUgc3RyaW5nIHJlcHJlc2VudGluZyBhIGRvbWFpbiBuYW1lIHRvIFVuaWNvZGUuIE9ubHkgdGhlXG5cdCAqIFB1bnljb2RlZCBwYXJ0cyBvZiB0aGUgZG9tYWluIG5hbWUgd2lsbCBiZSBjb252ZXJ0ZWQsIGkuZS4gaXQgZG9lc24ndFxuXHQgKiBtYXR0ZXIgaWYgeW91IGNhbGwgaXQgb24gYSBzdHJpbmcgdGhhdCBoYXMgYWxyZWFkeSBiZWVuIGNvbnZlcnRlZCB0b1xuXHQgKiBVbmljb2RlLlxuXHQgKiBAbWVtYmVyT2YgcHVueWNvZGVcblx0ICogQHBhcmFtIHtTdHJpbmd9IGRvbWFpbiBUaGUgUHVueWNvZGUgZG9tYWluIG5hbWUgdG8gY29udmVydCB0byBVbmljb2RlLlxuXHQgKiBAcmV0dXJucyB7U3RyaW5nfSBUaGUgVW5pY29kZSByZXByZXNlbnRhdGlvbiBvZiB0aGUgZ2l2ZW4gUHVueWNvZGVcblx0ICogc3RyaW5nLlxuXHQgKi9cblx0ZnVuY3Rpb24gdG9Vbmljb2RlKGRvbWFpbikge1xuXHRcdHJldHVybiBtYXBEb21haW4oZG9tYWluLCBmdW5jdGlvbihzdHJpbmcpIHtcblx0XHRcdHJldHVybiByZWdleFB1bnljb2RlLnRlc3Qoc3RyaW5nKVxuXHRcdFx0XHQ/IGRlY29kZShzdHJpbmcuc2xpY2UoNCkudG9Mb3dlckNhc2UoKSlcblx0XHRcdFx0OiBzdHJpbmc7XG5cdFx0fSk7XG5cdH1cblxuXHQvKipcblx0ICogQ29udmVydHMgYSBVbmljb2RlIHN0cmluZyByZXByZXNlbnRpbmcgYSBkb21haW4gbmFtZSB0byBQdW55Y29kZS4gT25seSB0aGVcblx0ICogbm9uLUFTQ0lJIHBhcnRzIG9mIHRoZSBkb21haW4gbmFtZSB3aWxsIGJlIGNvbnZlcnRlZCwgaS5lLiBpdCBkb2Vzbid0XG5cdCAqIG1hdHRlciBpZiB5b3UgY2FsbCBpdCB3aXRoIGEgZG9tYWluIHRoYXQncyBhbHJlYWR5IGluIEFTQ0lJLlxuXHQgKiBAbWVtYmVyT2YgcHVueWNvZGVcblx0ICogQHBhcmFtIHtTdHJpbmd9IGRvbWFpbiBUaGUgZG9tYWluIG5hbWUgdG8gY29udmVydCwgYXMgYSBVbmljb2RlIHN0cmluZy5cblx0ICogQHJldHVybnMge1N0cmluZ30gVGhlIFB1bnljb2RlIHJlcHJlc2VudGF0aW9uIG9mIHRoZSBnaXZlbiBkb21haW4gbmFtZS5cblx0ICovXG5cdGZ1bmN0aW9uIHRvQVNDSUkoZG9tYWluKSB7XG5cdFx0cmV0dXJuIG1hcERvbWFpbihkb21haW4sIGZ1bmN0aW9uKHN0cmluZykge1xuXHRcdFx0cmV0dXJuIHJlZ2V4Tm9uQVNDSUkudGVzdChzdHJpbmcpXG5cdFx0XHRcdD8gJ3huLS0nICsgZW5jb2RlKHN0cmluZylcblx0XHRcdFx0OiBzdHJpbmc7XG5cdFx0fSk7XG5cdH1cblxuXHQvKi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tKi9cblxuXHQvKiogRGVmaW5lIHRoZSBwdWJsaWMgQVBJICovXG5cdHB1bnljb2RlID0ge1xuXHRcdC8qKlxuXHRcdCAqIEEgc3RyaW5nIHJlcHJlc2VudGluZyB0aGUgY3VycmVudCBQdW55Y29kZS5qcyB2ZXJzaW9uIG51bWJlci5cblx0XHQgKiBAbWVtYmVyT2YgcHVueWNvZGVcblx0XHQgKiBAdHlwZSBTdHJpbmdcblx0XHQgKi9cblx0XHQndmVyc2lvbic6ICcxLjIuMycsXG5cdFx0LyoqXG5cdFx0ICogQW4gb2JqZWN0IG9mIG1ldGhvZHMgdG8gY29udmVydCBmcm9tIEphdmFTY3JpcHQncyBpbnRlcm5hbCBjaGFyYWN0ZXJcblx0XHQgKiByZXByZXNlbnRhdGlvbiAoVUNTLTIpIHRvIFVuaWNvZGUgY29kZSBwb2ludHMsIGFuZCBiYWNrLlxuXHRcdCAqIEBzZWUgPGh0dHA6Ly9tYXRoaWFzYnluZW5zLmJlL25vdGVzL2phdmFzY3JpcHQtZW5jb2Rpbmc+XG5cdFx0ICogQG1lbWJlck9mIHB1bnljb2RlXG5cdFx0ICogQHR5cGUgT2JqZWN0XG5cdFx0ICovXG5cdFx0J3VjczInOiB7XG5cdFx0XHQnZGVjb2RlJzogdWNzMmRlY29kZSxcblx0XHRcdCdlbmNvZGUnOiB1Y3MyZW5jb2RlXG5cdFx0fSxcblx0XHQnZGVjb2RlJzogZGVjb2RlLFxuXHRcdCdlbmNvZGUnOiBlbmNvZGUsXG5cdFx0J3RvQVNDSUknOiB0b0FTQ0lJLFxuXHRcdCd0b1VuaWNvZGUnOiB0b1VuaWNvZGVcblx0fTtcblxuXHQvKiogRXhwb3NlIGBwdW55Y29kZWAgKi9cblx0Ly8gU29tZSBBTUQgYnVpbGQgb3B0aW1pemVycywgbGlrZSByLmpzLCBjaGVjayBmb3Igc3BlY2lmaWMgY29uZGl0aW9uIHBhdHRlcm5zXG5cdC8vIGxpa2UgdGhlIGZvbGxvd2luZzpcblx0aWYgKFxuXHRcdHR5cGVvZiBkZWZpbmUgPT0gJ2Z1bmN0aW9uJyAmJlxuXHRcdHR5cGVvZiBkZWZpbmUuYW1kID09ICdvYmplY3QnICYmXG5cdFx0ZGVmaW5lLmFtZFxuXHQpIHtcblx0XHRkZWZpbmUoZnVuY3Rpb24oKSB7XG5cdFx0XHRyZXR1cm4gcHVueWNvZGU7XG5cdFx0fSk7XG5cdH1cdGVsc2UgaWYgKGZyZWVFeHBvcnRzICYmICFmcmVlRXhwb3J0cy5ub2RlVHlwZSkge1xuXHRcdGlmIChmcmVlTW9kdWxlKSB7IC8vIGluIE5vZGUuanMgb3IgUmluZ29KUyB2MC44LjArXG5cdFx0XHRmcmVlTW9kdWxlLmV4cG9ydHMgPSBwdW55Y29kZTtcblx0XHR9IGVsc2UgeyAvLyBpbiBOYXJ3aGFsIG9yIFJpbmdvSlMgdjAuNy4wLVxuXHRcdFx0Zm9yIChrZXkgaW4gcHVueWNvZGUpIHtcblx0XHRcdFx0cHVueWNvZGUuaGFzT3duUHJvcGVydHkoa2V5KSAmJiAoZnJlZUV4cG9ydHNba2V5XSA9IHB1bnljb2RlW2tleV0pO1xuXHRcdFx0fVxuXHRcdH1cblx0fSBlbHNlIHsgLy8gaW4gUmhpbm8gb3IgYSB3ZWIgYnJvd3NlclxuXHRcdHJvb3QucHVueWNvZGUgPSBwdW55Y29kZTtcblx0fVxuXG59KHRoaXMpKTtcbiIsIid1c2Ugc3RyaWN0JztcblxuT2JqZWN0LmRlZmluZVByb3BlcnR5KGV4cG9ydHMsICdfX2VzTW9kdWxlJywge1xuICB2YWx1ZTogdHJ1ZVxufSk7XG5cbmZ1bmN0aW9uIF9pbnRlcm9wUmVxdWlyZURlZmF1bHQob2JqKSB7IHJldHVybiBvYmogJiYgb2JqLl9fZXNNb2R1bGUgPyBvYmogOiB7ICdkZWZhdWx0Jzogb2JqIH07IH1cblxudmFyIF9oZWxwZXJzRXZlbnQgPSByZXF1aXJlKCcuL2hlbHBlcnMvZXZlbnQnKTtcblxudmFyIF9oZWxwZXJzRXZlbnQyID0gX2ludGVyb3BSZXF1aXJlRGVmYXVsdChfaGVscGVyc0V2ZW50KTtcblxudmFyIF9oZWxwZXJzTWVzc2FnZUV2ZW50ID0gcmVxdWlyZSgnLi9oZWxwZXJzL21lc3NhZ2UtZXZlbnQnKTtcblxudmFyIF9oZWxwZXJzTWVzc2FnZUV2ZW50MiA9IF9pbnRlcm9wUmVxdWlyZURlZmF1bHQoX2hlbHBlcnNNZXNzYWdlRXZlbnQpO1xuXG52YXIgX2hlbHBlcnNDbG9zZUV2ZW50ID0gcmVxdWlyZSgnLi9oZWxwZXJzL2Nsb3NlLWV2ZW50Jyk7XG5cbnZhciBfaGVscGVyc0Nsb3NlRXZlbnQyID0gX2ludGVyb3BSZXF1aXJlRGVmYXVsdChfaGVscGVyc0Nsb3NlRXZlbnQpO1xuXG4vKlxuKiBDcmVhdGVzIGFuIEV2ZW50IG9iamVjdCBhbmQgZXh0ZW5kcyBpdCB0byBhbGxvdyBmdWxsIG1vZGlmaWNhdGlvbiBvZlxuKiBpdHMgcHJvcGVydGllcy5cbipcbiogQHBhcmFtIHtvYmplY3R9IGNvbmZpZyAtIHdpdGhpbiBjb25maWcgeW91IHdpbGwgbmVlZCB0byBwYXNzIHR5cGUgYW5kIG9wdGlvbmFsbHkgdGFyZ2V0XG4qL1xuZnVuY3Rpb24gY3JlYXRlRXZlbnQoY29uZmlnKSB7XG4gIHZhciB0eXBlID0gY29uZmlnLnR5cGU7XG4gIHZhciB0YXJnZXQgPSBjb25maWcudGFyZ2V0O1xuXG4gIHZhciBldmVudE9iamVjdCA9IG5ldyBfaGVscGVyc0V2ZW50MlsnZGVmYXVsdCddKHR5cGUpO1xuXG4gIGlmICh0YXJnZXQpIHtcbiAgICBldmVudE9iamVjdC50YXJnZXQgPSB0YXJnZXQ7XG4gICAgZXZlbnRPYmplY3Quc3JjRWxlbWVudCA9IHRhcmdldDtcbiAgICBldmVudE9iamVjdC5jdXJyZW50VGFyZ2V0ID0gdGFyZ2V0O1xuICB9XG5cbiAgcmV0dXJuIGV2ZW50T2JqZWN0O1xufVxuXG4vKlxuKiBDcmVhdGVzIGEgTWVzc2FnZUV2ZW50IG9iamVjdCBhbmQgZXh0ZW5kcyBpdCB0byBhbGxvdyBmdWxsIG1vZGlmaWNhdGlvbiBvZlxuKiBpdHMgcHJvcGVydGllcy5cbipcbiogQHBhcmFtIHtvYmplY3R9IGNvbmZpZyAtIHdpdGhpbiBjb25maWcgeW91IHdpbGwgbmVlZCB0byBwYXNzIHR5cGUsIG9yaWdpbiwgZGF0YSBhbmQgb3B0aW9uYWxseSB0YXJnZXRcbiovXG5mdW5jdGlvbiBjcmVhdGVNZXNzYWdlRXZlbnQoY29uZmlnKSB7XG4gIHZhciB0eXBlID0gY29uZmlnLnR5cGU7XG4gIHZhciBvcmlnaW4gPSBjb25maWcub3JpZ2luO1xuICB2YXIgZGF0YSA9IGNvbmZpZy5kYXRhO1xuICB2YXIgdGFyZ2V0ID0gY29uZmlnLnRhcmdldDtcblxuICB2YXIgbWVzc2FnZUV2ZW50ID0gbmV3IF9oZWxwZXJzTWVzc2FnZUV2ZW50MlsnZGVmYXVsdCddKHR5cGUsIHtcbiAgICBkYXRhOiBkYXRhLFxuICAgIG9yaWdpbjogb3JpZ2luXG4gIH0pO1xuXG4gIGlmICh0YXJnZXQpIHtcbiAgICBtZXNzYWdlRXZlbnQudGFyZ2V0ID0gdGFyZ2V0O1xuICAgIG1lc3NhZ2VFdmVudC5zcmNFbGVtZW50ID0gdGFyZ2V0O1xuICAgIG1lc3NhZ2VFdmVudC5jdXJyZW50VGFyZ2V0ID0gdGFyZ2V0O1xuICB9XG5cbiAgcmV0dXJuIG1lc3NhZ2VFdmVudDtcbn1cblxuLypcbiogQ3JlYXRlcyBhIENsb3NlRXZlbnQgb2JqZWN0IGFuZCBleHRlbmRzIGl0IHRvIGFsbG93IGZ1bGwgbW9kaWZpY2F0aW9uIG9mXG4qIGl0cyBwcm9wZXJ0aWVzLlxuKlxuKiBAcGFyYW0ge29iamVjdH0gY29uZmlnIC0gd2l0aGluIGNvbmZpZyB5b3Ugd2lsbCBuZWVkIHRvIHBhc3MgdHlwZSBhbmQgb3B0aW9uYWxseSB0YXJnZXQsIGNvZGUsIGFuZCByZWFzb25cbiovXG5mdW5jdGlvbiBjcmVhdGVDbG9zZUV2ZW50KGNvbmZpZykge1xuICB2YXIgY29kZSA9IGNvbmZpZy5jb2RlO1xuICB2YXIgcmVhc29uID0gY29uZmlnLnJlYXNvbjtcbiAgdmFyIHR5cGUgPSBjb25maWcudHlwZTtcbiAgdmFyIHRhcmdldCA9IGNvbmZpZy50YXJnZXQ7XG4gIHZhciB3YXNDbGVhbiA9IGNvbmZpZy53YXNDbGVhbjtcblxuICBpZiAoIXdhc0NsZWFuKSB7XG4gICAgd2FzQ2xlYW4gPSBjb2RlID09PSAxMDAwO1xuICB9XG5cbiAgdmFyIGNsb3NlRXZlbnQgPSBuZXcgX2hlbHBlcnNDbG9zZUV2ZW50MlsnZGVmYXVsdCddKHR5cGUsIHtcbiAgICBjb2RlOiBjb2RlLFxuICAgIHJlYXNvbjogcmVhc29uLFxuICAgIHdhc0NsZWFuOiB3YXNDbGVhblxuICB9KTtcblxuICBpZiAodGFyZ2V0KSB7XG4gICAgY2xvc2VFdmVudC50YXJnZXQgPSB0YXJnZXQ7XG4gICAgY2xvc2VFdmVudC5zcmNFbGVtZW50ID0gdGFyZ2V0O1xuICAgIGNsb3NlRXZlbnQuY3VycmVudFRhcmdldCA9IHRhcmdldDtcbiAgfVxuXG4gIHJldHVybiBjbG9zZUV2ZW50O1xufVxuXG5leHBvcnRzLmNyZWF0ZUV2ZW50ID0gY3JlYXRlRXZlbnQ7XG5leHBvcnRzLmNyZWF0ZU1lc3NhZ2VFdmVudCA9IGNyZWF0ZU1lc3NhZ2VFdmVudDtcbmV4cG9ydHMuY3JlYXRlQ2xvc2VFdmVudCA9IGNyZWF0ZUNsb3NlRXZlbnQ7IiwiJ3VzZSBzdHJpY3QnO1xuXG5PYmplY3QuZGVmaW5lUHJvcGVydHkoZXhwb3J0cywgJ19fZXNNb2R1bGUnLCB7XG4gIHZhbHVlOiB0cnVlXG59KTtcblxudmFyIF9jcmVhdGVDbGFzcyA9IChmdW5jdGlvbiAoKSB7IGZ1bmN0aW9uIGRlZmluZVByb3BlcnRpZXModGFyZ2V0LCBwcm9wcykgeyBmb3IgKHZhciBpID0gMDsgaSA8IHByb3BzLmxlbmd0aDsgaSsrKSB7IHZhciBkZXNjcmlwdG9yID0gcHJvcHNbaV07IGRlc2NyaXB0b3IuZW51bWVyYWJsZSA9IGRlc2NyaXB0b3IuZW51bWVyYWJsZSB8fCBmYWxzZTsgZGVzY3JpcHRvci5jb25maWd1cmFibGUgPSB0cnVlOyBpZiAoJ3ZhbHVlJyBpbiBkZXNjcmlwdG9yKSBkZXNjcmlwdG9yLndyaXRhYmxlID0gdHJ1ZTsgT2JqZWN0LmRlZmluZVByb3BlcnR5KHRhcmdldCwgZGVzY3JpcHRvci5rZXksIGRlc2NyaXB0b3IpOyB9IH0gcmV0dXJuIGZ1bmN0aW9uIChDb25zdHJ1Y3RvciwgcHJvdG9Qcm9wcywgc3RhdGljUHJvcHMpIHsgaWYgKHByb3RvUHJvcHMpIGRlZmluZVByb3BlcnRpZXMoQ29uc3RydWN0b3IucHJvdG90eXBlLCBwcm90b1Byb3BzKTsgaWYgKHN0YXRpY1Byb3BzKSBkZWZpbmVQcm9wZXJ0aWVzKENvbnN0cnVjdG9yLCBzdGF0aWNQcm9wcyk7IHJldHVybiBDb25zdHJ1Y3RvcjsgfTsgfSkoKTtcblxuZnVuY3Rpb24gX2NsYXNzQ2FsbENoZWNrKGluc3RhbmNlLCBDb25zdHJ1Y3RvcikgeyBpZiAoIShpbnN0YW5jZSBpbnN0YW5jZW9mIENvbnN0cnVjdG9yKSkgeyB0aHJvdyBuZXcgVHlwZUVycm9yKCdDYW5ub3QgY2FsbCBhIGNsYXNzIGFzIGEgZnVuY3Rpb24nKTsgfSB9XG5cbnZhciBfaGVscGVyc0FycmF5SGVscGVycyA9IHJlcXVpcmUoJy4vaGVscGVycy9hcnJheS1oZWxwZXJzJyk7XG5cbi8qXG4qIEV2ZW50VGFyZ2V0IGlzIGFuIGludGVyZmFjZSBpbXBsZW1lbnRlZCBieSBvYmplY3RzIHRoYXQgY2FuXG4qIHJlY2VpdmUgZXZlbnRzIGFuZCBtYXkgaGF2ZSBsaXN0ZW5lcnMgZm9yIHRoZW0uXG4qXG4qIGh0dHBzOi8vZGV2ZWxvcGVyLm1vemlsbGEub3JnL2VuLVVTL2RvY3MvV2ViL0FQSS9FdmVudFRhcmdldFxuKi9cblxudmFyIEV2ZW50VGFyZ2V0ID0gKGZ1bmN0aW9uICgpIHtcbiAgZnVuY3Rpb24gRXZlbnRUYXJnZXQoKSB7XG4gICAgX2NsYXNzQ2FsbENoZWNrKHRoaXMsIEV2ZW50VGFyZ2V0KTtcblxuICAgIHRoaXMubGlzdGVuZXJzID0ge307XG4gIH1cblxuICAvKlxuICAqIFRpZXMgYSBsaXN0ZW5lciBmdW5jdGlvbiB0byBhIGV2ZW50IHR5cGUgd2hpY2ggY2FuIGxhdGVyIGJlIGludm9rZWQgdmlhIHRoZVxuICAqIGRpc3BhdGNoRXZlbnQgbWV0aG9kLlxuICAqXG4gICogQHBhcmFtIHtzdHJpbmd9IHR5cGUgLSB0aGUgdHlwZSBvZiBldmVudCAoaWU6ICdvcGVuJywgJ21lc3NhZ2UnLCBldGMuKVxuICAqIEBwYXJhbSB7ZnVuY3Rpb259IGxpc3RlbmVyIC0gdGhlIGNhbGxiYWNrIGZ1bmN0aW9uIHRvIGludm9rZSB3aGVuZXZlciBhIGV2ZW50IGlzIGRpc3BhdGNoZWQgbWF0Y2hpbmcgdGhlIGdpdmVuIHR5cGVcbiAgKiBAcGFyYW0ge2Jvb2xlYW59IHVzZUNhcHR1cmUgLSBOL0EgVE9ETzogaW1wbGVtZW50IHVzZUNhcHR1cmUgZnVuY3Rpb25hbGl0eVxuICAqL1xuXG4gIF9jcmVhdGVDbGFzcyhFdmVudFRhcmdldCwgW3tcbiAgICBrZXk6ICdhZGRFdmVudExpc3RlbmVyJyxcbiAgICB2YWx1ZTogZnVuY3Rpb24gYWRkRXZlbnRMaXN0ZW5lcih0eXBlLCBsaXN0ZW5lciAvKiAsIHVzZUNhcHR1cmUgKi8pIHtcbiAgICAgIGlmICh0eXBlb2YgbGlzdGVuZXIgPT09ICdmdW5jdGlvbicpIHtcbiAgICAgICAgaWYgKCFBcnJheS5pc0FycmF5KHRoaXMubGlzdGVuZXJzW3R5cGVdKSkge1xuICAgICAgICAgIHRoaXMubGlzdGVuZXJzW3R5cGVdID0gW107XG4gICAgICAgIH1cblxuICAgICAgICAvLyBPbmx5IGFkZCB0aGUgc2FtZSBmdW5jdGlvbiBvbmNlXG4gICAgICAgIGlmICgoMCwgX2hlbHBlcnNBcnJheUhlbHBlcnMuZmlsdGVyKSh0aGlzLmxpc3RlbmVyc1t0eXBlXSwgZnVuY3Rpb24gKGl0ZW0pIHtcbiAgICAgICAgICByZXR1cm4gaXRlbSA9PT0gbGlzdGVuZXI7XG4gICAgICAgIH0pLmxlbmd0aCA9PT0gMCkge1xuICAgICAgICAgIHRoaXMubGlzdGVuZXJzW3R5cGVdLnB1c2gobGlzdGVuZXIpO1xuICAgICAgICB9XG4gICAgICB9XG4gICAgfVxuXG4gICAgLypcbiAgICAqIFJlbW92ZXMgdGhlIGxpc3RlbmVyIHNvIGl0IHdpbGwgbm8gbG9uZ2VyIGJlIGludm9rZWQgdmlhIHRoZSBkaXNwYXRjaEV2ZW50IG1ldGhvZC5cbiAgICAqXG4gICAgKiBAcGFyYW0ge3N0cmluZ30gdHlwZSAtIHRoZSB0eXBlIG9mIGV2ZW50IChpZTogJ29wZW4nLCAnbWVzc2FnZScsIGV0Yy4pXG4gICAgKiBAcGFyYW0ge2Z1bmN0aW9ufSBsaXN0ZW5lciAtIHRoZSBjYWxsYmFjayBmdW5jdGlvbiB0byBpbnZva2Ugd2hlbmV2ZXIgYSBldmVudCBpcyBkaXNwYXRjaGVkIG1hdGNoaW5nIHRoZSBnaXZlbiB0eXBlXG4gICAgKiBAcGFyYW0ge2Jvb2xlYW59IHVzZUNhcHR1cmUgLSBOL0EgVE9ETzogaW1wbGVtZW50IHVzZUNhcHR1cmUgZnVuY3Rpb25hbGl0eVxuICAgICovXG4gIH0sIHtcbiAgICBrZXk6ICdyZW1vdmVFdmVudExpc3RlbmVyJyxcbiAgICB2YWx1ZTogZnVuY3Rpb24gcmVtb3ZlRXZlbnRMaXN0ZW5lcih0eXBlLCByZW1vdmluZ0xpc3RlbmVyIC8qICwgdXNlQ2FwdHVyZSAqLykge1xuICAgICAgdmFyIGFycmF5T2ZMaXN0ZW5lcnMgPSB0aGlzLmxpc3RlbmVyc1t0eXBlXTtcbiAgICAgIHRoaXMubGlzdGVuZXJzW3R5cGVdID0gKDAsIF9oZWxwZXJzQXJyYXlIZWxwZXJzLnJlamVjdCkoYXJyYXlPZkxpc3RlbmVycywgZnVuY3Rpb24gKGxpc3RlbmVyKSB7XG4gICAgICAgIHJldHVybiBsaXN0ZW5lciA9PT0gcmVtb3ZpbmdMaXN0ZW5lcjtcbiAgICAgIH0pO1xuICAgIH1cblxuICAgIC8qXG4gICAgKiBJbnZva2VzIGFsbCBsaXN0ZW5lciBmdW5jdGlvbnMgdGhhdCBhcmUgbGlzdGVuaW5nIHRvIHRoZSBnaXZlbiBldmVudC50eXBlIHByb3BlcnR5LiBFYWNoXG4gICAgKiBsaXN0ZW5lciB3aWxsIGJlIHBhc3NlZCB0aGUgZXZlbnQgYXMgdGhlIGZpcnN0IGFyZ3VtZW50LlxuICAgICpcbiAgICAqIEBwYXJhbSB7b2JqZWN0fSBldmVudCAtIGV2ZW50IG9iamVjdCB3aGljaCB3aWxsIGJlIHBhc3NlZCB0byBhbGwgbGlzdGVuZXJzIG9mIHRoZSBldmVudC50eXBlIHByb3BlcnR5XG4gICAgKi9cbiAgfSwge1xuICAgIGtleTogJ2Rpc3BhdGNoRXZlbnQnLFxuICAgIHZhbHVlOiBmdW5jdGlvbiBkaXNwYXRjaEV2ZW50KGV2ZW50KSB7XG4gICAgICB2YXIgX3RoaXMgPSB0aGlzO1xuXG4gICAgICBmb3IgKHZhciBfbGVuID0gYXJndW1lbnRzLmxlbmd0aCwgY3VzdG9tQXJndW1lbnRzID0gQXJyYXkoX2xlbiA+IDEgPyBfbGVuIC0gMSA6IDApLCBfa2V5ID0gMTsgX2tleSA8IF9sZW47IF9rZXkrKykge1xuICAgICAgICBjdXN0b21Bcmd1bWVudHNbX2tleSAtIDFdID0gYXJndW1lbnRzW19rZXldO1xuICAgICAgfVxuXG4gICAgICB2YXIgZXZlbnROYW1lID0gZXZlbnQudHlwZTtcbiAgICAgIHZhciBsaXN0ZW5lcnMgPSB0aGlzLmxpc3RlbmVyc1tldmVudE5hbWVdO1xuXG4gICAgICBpZiAoIUFycmF5LmlzQXJyYXkobGlzdGVuZXJzKSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgICB9XG5cbiAgICAgIGxpc3RlbmVycy5mb3JFYWNoKGZ1bmN0aW9uIChsaXN0ZW5lcikge1xuICAgICAgICBpZiAoY3VzdG9tQXJndW1lbnRzLmxlbmd0aCA+IDApIHtcbiAgICAgICAgICBsaXN0ZW5lci5hcHBseShfdGhpcywgY3VzdG9tQXJndW1lbnRzKTtcbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICBsaXN0ZW5lci5jYWxsKF90aGlzLCBldmVudCk7XG4gICAgICAgIH1cbiAgICAgIH0pO1xuICAgIH1cbiAgfV0pO1xuXG4gIHJldHVybiBFdmVudFRhcmdldDtcbn0pKCk7XG5cbmV4cG9ydHNbJ2RlZmF1bHQnXSA9IEV2ZW50VGFyZ2V0O1xubW9kdWxlLmV4cG9ydHMgPSBleHBvcnRzWydkZWZhdWx0J107IiwiXCJ1c2Ugc3RyaWN0XCI7XG5cbk9iamVjdC5kZWZpbmVQcm9wZXJ0eShleHBvcnRzLCBcIl9fZXNNb2R1bGVcIiwge1xuICB2YWx1ZTogdHJ1ZVxufSk7XG5leHBvcnRzLnJlamVjdCA9IHJlamVjdDtcbmV4cG9ydHMuZmlsdGVyID0gZmlsdGVyO1xuXG5mdW5jdGlvbiByZWplY3QoYXJyYXksIGNhbGxiYWNrKSB7XG4gIHZhciByZXN1bHRzID0gW107XG4gIGFycmF5LmZvckVhY2goZnVuY3Rpb24gKGl0ZW1JbkFycmF5KSB7XG4gICAgaWYgKCFjYWxsYmFjayhpdGVtSW5BcnJheSkpIHtcbiAgICAgIHJlc3VsdHMucHVzaChpdGVtSW5BcnJheSk7XG4gICAgfVxuICB9KTtcblxuICByZXR1cm4gcmVzdWx0cztcbn1cblxuZnVuY3Rpb24gZmlsdGVyKGFycmF5LCBjYWxsYmFjaykge1xuICB2YXIgcmVzdWx0cyA9IFtdO1xuICBhcnJheS5mb3JFYWNoKGZ1bmN0aW9uIChpdGVtSW5BcnJheSkge1xuICAgIGlmIChjYWxsYmFjayhpdGVtSW5BcnJheSkpIHtcbiAgICAgIHJlc3VsdHMucHVzaChpdGVtSW5BcnJheSk7XG4gICAgfVxuICB9KTtcblxuICByZXR1cm4gcmVzdWx0cztcbn0iLCIvKlxuKiBodHRwczovL2RldmVsb3Blci5tb3ppbGxhLm9yZy9lbi1VUy9kb2NzL1dlYi9BUEkvQ2xvc2VFdmVudFxuKi9cblwidXNlIHN0cmljdFwiO1xuXG5PYmplY3QuZGVmaW5lUHJvcGVydHkoZXhwb3J0cywgXCJfX2VzTW9kdWxlXCIsIHtcbiAgdmFsdWU6IHRydWVcbn0pO1xudmFyIGNvZGVzID0ge1xuICBDTE9TRV9OT1JNQUw6IDEwMDAsXG4gIENMT1NFX0dPSU5HX0FXQVk6IDEwMDEsXG4gIENMT1NFX1BST1RPQ09MX0VSUk9SOiAxMDAyLFxuICBDTE9TRV9VTlNVUFBPUlRFRDogMTAwMyxcbiAgQ0xPU0VfTk9fU1RBVFVTOiAxMDA1LFxuICBDTE9TRV9BQk5PUk1BTDogMTAwNixcbiAgQ0xPU0VfVE9PX0xBUkdFOiAxMDA5XG59O1xuXG5leHBvcnRzW1wiZGVmYXVsdFwiXSA9IGNvZGVzO1xubW9kdWxlLmV4cG9ydHMgPSBleHBvcnRzW1wiZGVmYXVsdFwiXTsiLCIndXNlIHN0cmljdCc7XG5cbk9iamVjdC5kZWZpbmVQcm9wZXJ0eShleHBvcnRzLCAnX19lc01vZHVsZScsIHtcbiAgdmFsdWU6IHRydWVcbn0pO1xuXG52YXIgX2dldCA9IGZ1bmN0aW9uIGdldChfeDIsIF94MywgX3g0KSB7IHZhciBfYWdhaW4gPSB0cnVlOyBfZnVuY3Rpb246IHdoaWxlIChfYWdhaW4pIHsgdmFyIG9iamVjdCA9IF94MiwgcHJvcGVydHkgPSBfeDMsIHJlY2VpdmVyID0gX3g0OyBfYWdhaW4gPSBmYWxzZTsgaWYgKG9iamVjdCA9PT0gbnVsbCkgb2JqZWN0ID0gRnVuY3Rpb24ucHJvdG90eXBlOyB2YXIgZGVzYyA9IE9iamVjdC5nZXRPd25Qcm9wZXJ0eURlc2NyaXB0b3Iob2JqZWN0LCBwcm9wZXJ0eSk7IGlmIChkZXNjID09PSB1bmRlZmluZWQpIHsgdmFyIHBhcmVudCA9IE9iamVjdC5nZXRQcm90b3R5cGVPZihvYmplY3QpOyBpZiAocGFyZW50ID09PSBudWxsKSB7IHJldHVybiB1bmRlZmluZWQ7IH0gZWxzZSB7IF94MiA9IHBhcmVudDsgX3gzID0gcHJvcGVydHk7IF94NCA9IHJlY2VpdmVyOyBfYWdhaW4gPSB0cnVlOyBkZXNjID0gcGFyZW50ID0gdW5kZWZpbmVkOyBjb250aW51ZSBfZnVuY3Rpb247IH0gfSBlbHNlIGlmICgndmFsdWUnIGluIGRlc2MpIHsgcmV0dXJuIGRlc2MudmFsdWU7IH0gZWxzZSB7IHZhciBnZXR0ZXIgPSBkZXNjLmdldDsgaWYgKGdldHRlciA9PT0gdW5kZWZpbmVkKSB7IHJldHVybiB1bmRlZmluZWQ7IH0gcmV0dXJuIGdldHRlci5jYWxsKHJlY2VpdmVyKTsgfSB9IH07XG5cbmZ1bmN0aW9uIF9pbnRlcm9wUmVxdWlyZURlZmF1bHQob2JqKSB7IHJldHVybiBvYmogJiYgb2JqLl9fZXNNb2R1bGUgPyBvYmogOiB7ICdkZWZhdWx0Jzogb2JqIH07IH1cblxuZnVuY3Rpb24gX2NsYXNzQ2FsbENoZWNrKGluc3RhbmNlLCBDb25zdHJ1Y3RvcikgeyBpZiAoIShpbnN0YW5jZSBpbnN0YW5jZW9mIENvbnN0cnVjdG9yKSkgeyB0aHJvdyBuZXcgVHlwZUVycm9yKCdDYW5ub3QgY2FsbCBhIGNsYXNzIGFzIGEgZnVuY3Rpb24nKTsgfSB9XG5cbmZ1bmN0aW9uIF9pbmhlcml0cyhzdWJDbGFzcywgc3VwZXJDbGFzcykgeyBpZiAodHlwZW9mIHN1cGVyQ2xhc3MgIT09ICdmdW5jdGlvbicgJiYgc3VwZXJDbGFzcyAhPT0gbnVsbCkgeyB0aHJvdyBuZXcgVHlwZUVycm9yKCdTdXBlciBleHByZXNzaW9uIG11c3QgZWl0aGVyIGJlIG51bGwgb3IgYSBmdW5jdGlvbiwgbm90ICcgKyB0eXBlb2Ygc3VwZXJDbGFzcyk7IH0gc3ViQ2xhc3MucHJvdG90eXBlID0gT2JqZWN0LmNyZWF0ZShzdXBlckNsYXNzICYmIHN1cGVyQ2xhc3MucHJvdG90eXBlLCB7IGNvbnN0cnVjdG9yOiB7IHZhbHVlOiBzdWJDbGFzcywgZW51bWVyYWJsZTogZmFsc2UsIHdyaXRhYmxlOiB0cnVlLCBjb25maWd1cmFibGU6IHRydWUgfSB9KTsgaWYgKHN1cGVyQ2xhc3MpIE9iamVjdC5zZXRQcm90b3R5cGVPZiA/IE9iamVjdC5zZXRQcm90b3R5cGVPZihzdWJDbGFzcywgc3VwZXJDbGFzcykgOiBzdWJDbGFzcy5fX3Byb3RvX18gPSBzdXBlckNsYXNzOyB9XG5cbnZhciBfZXZlbnRQcm90b3R5cGUgPSByZXF1aXJlKCcuL2V2ZW50LXByb3RvdHlwZScpO1xuXG52YXIgX2V2ZW50UHJvdG90eXBlMiA9IF9pbnRlcm9wUmVxdWlyZURlZmF1bHQoX2V2ZW50UHJvdG90eXBlKTtcblxudmFyIENsb3NlRXZlbnQgPSAoZnVuY3Rpb24gKF9FdmVudFByb3RvdHlwZSkge1xuICBfaW5oZXJpdHMoQ2xvc2VFdmVudCwgX0V2ZW50UHJvdG90eXBlKTtcblxuICBmdW5jdGlvbiBDbG9zZUV2ZW50KHR5cGUpIHtcbiAgICB2YXIgZXZlbnRJbml0Q29uZmlnID0gYXJndW1lbnRzLmxlbmd0aCA8PSAxIHx8IGFyZ3VtZW50c1sxXSA9PT0gdW5kZWZpbmVkID8ge30gOiBhcmd1bWVudHNbMV07XG5cbiAgICBfY2xhc3NDYWxsQ2hlY2sodGhpcywgQ2xvc2VFdmVudCk7XG5cbiAgICBfZ2V0KE9iamVjdC5nZXRQcm90b3R5cGVPZihDbG9zZUV2ZW50LnByb3RvdHlwZSksICdjb25zdHJ1Y3RvcicsIHRoaXMpLmNhbGwodGhpcyk7XG5cbiAgICBpZiAoIXR5cGUpIHtcbiAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ0ZhaWxlZCB0byBjb25zdHJ1Y3QgXFwnQ2xvc2VFdmVudFxcJzogMSBhcmd1bWVudCByZXF1aXJlZCwgYnV0IG9ubHkgMCBwcmVzZW50LicpO1xuICAgIH1cblxuICAgIGlmICh0eXBlb2YgZXZlbnRJbml0Q29uZmlnICE9PSAnb2JqZWN0Jykge1xuICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcignRmFpbGVkIHRvIGNvbnN0cnVjdCBcXCdDbG9zZUV2ZW50XFwnOiBwYXJhbWV0ZXIgMiAoXFwnZXZlbnRJbml0RGljdFxcJykgaXMgbm90IGFuIG9iamVjdCcpO1xuICAgIH1cblxuICAgIHZhciBidWJibGVzID0gZXZlbnRJbml0Q29uZmlnLmJ1YmJsZXM7XG4gICAgdmFyIGNhbmNlbGFibGUgPSBldmVudEluaXRDb25maWcuY2FuY2VsYWJsZTtcbiAgICB2YXIgY29kZSA9IGV2ZW50SW5pdENvbmZpZy5jb2RlO1xuICAgIHZhciByZWFzb24gPSBldmVudEluaXRDb25maWcucmVhc29uO1xuICAgIHZhciB3YXNDbGVhbiA9IGV2ZW50SW5pdENvbmZpZy53YXNDbGVhbjtcblxuICAgIHRoaXMudHlwZSA9IFN0cmluZyh0eXBlKTtcbiAgICB0aGlzLnRpbWVTdGFtcCA9IERhdGUubm93KCk7XG4gICAgdGhpcy50YXJnZXQgPSBudWxsO1xuICAgIHRoaXMuc3JjRWxlbWVudCA9IG51bGw7XG4gICAgdGhpcy5yZXR1cm5WYWx1ZSA9IHRydWU7XG4gICAgdGhpcy5pc1RydXN0ZWQgPSBmYWxzZTtcbiAgICB0aGlzLmV2ZW50UGhhc2UgPSAwO1xuICAgIHRoaXMuZGVmYXVsdFByZXZlbnRlZCA9IGZhbHNlO1xuICAgIHRoaXMuY3VycmVudFRhcmdldCA9IG51bGw7XG4gICAgdGhpcy5jYW5jZWxhYmxlID0gY2FuY2VsYWJsZSA/IEJvb2xlYW4oY2FuY2VsYWJsZSkgOiBmYWxzZTtcbiAgICB0aGlzLmNhbm5jZWxCdWJibGUgPSBmYWxzZTtcbiAgICB0aGlzLmJ1YmJsZXMgPSBidWJibGVzID8gQm9vbGVhbihidWJibGVzKSA6IGZhbHNlO1xuICAgIHRoaXMuY29kZSA9IHR5cGVvZiBjb2RlID09PSAnbnVtYmVyJyA/IE51bWJlcihjb2RlKSA6IDA7XG4gICAgdGhpcy5yZWFzb24gPSByZWFzb24gPyBTdHJpbmcocmVhc29uKSA6ICcnO1xuICAgIHRoaXMud2FzQ2xlYW4gPSB3YXNDbGVhbiA/IEJvb2xlYW4od2FzQ2xlYW4pIDogZmFsc2U7XG4gIH1cblxuICByZXR1cm4gQ2xvc2VFdmVudDtcbn0pKF9ldmVudFByb3RvdHlwZTJbJ2RlZmF1bHQnXSk7XG5cbmV4cG9ydHNbJ2RlZmF1bHQnXSA9IENsb3NlRXZlbnQ7XG5tb2R1bGUuZXhwb3J0cyA9IGV4cG9ydHNbJ2RlZmF1bHQnXTsiLCIvKlxuKiBUaGlzIGRlbGF5IGFsbG93cyB0aGUgdGhyZWFkIHRvIGZpbmlzaCBhc3NpZ25pbmcgaXRzIG9uKiBtZXRob2RzXG4qIGJlZm9yZSBpbnZva2luZyB0aGUgZGVsYXkgY2FsbGJhY2suIFRoaXMgaXMgcHVyZWx5IGEgdGltaW5nIGhhY2suXG4qIGh0dHA6Ly9nZWVrYWJ5dGUuYmxvZ3Nwb3QuY29tLzIwMTQvMDEvamF2YXNjcmlwdC1lZmZlY3Qtb2Ytc2V0dGluZy1zZXR0aW1lb3V0Lmh0bWxcbipcbiogQHBhcmFtIHtjYWxsYmFjazogZnVuY3Rpb259IHRoZSBjYWxsYmFjayB3aGljaCB3aWxsIGJlIGludm9rZWQgYWZ0ZXIgdGhlIHRpbWVvdXRcbiogQHBhcm1hIHtjb250ZXh0OiBvYmplY3R9IHRoZSBjb250ZXh0IGluIHdoaWNoIHRvIGludm9rZSB0aGUgZnVuY3Rpb25cbiovXG5cInVzZSBzdHJpY3RcIjtcblxuT2JqZWN0LmRlZmluZVByb3BlcnR5KGV4cG9ydHMsIFwiX19lc01vZHVsZVwiLCB7XG4gIHZhbHVlOiB0cnVlXG59KTtcbmZ1bmN0aW9uIGRlbGF5KGNhbGxiYWNrLCBjb250ZXh0KSB7XG4gIHNldFRpbWVvdXQoZnVuY3Rpb24gdGltZW91dCh0aW1lb3V0Q29udGV4dCkge1xuICAgIGNhbGxiYWNrLmNhbGwodGltZW91dENvbnRleHQpO1xuICB9LCA0LCBjb250ZXh0KTtcbn1cblxuZXhwb3J0c1tcImRlZmF1bHRcIl0gPSBkZWxheTtcbm1vZHVsZS5leHBvcnRzID0gZXhwb3J0c1tcImRlZmF1bHRcIl07IiwiJ3VzZSBzdHJpY3QnO1xuXG5PYmplY3QuZGVmaW5lUHJvcGVydHkoZXhwb3J0cywgJ19fZXNNb2R1bGUnLCB7XG4gIHZhbHVlOiB0cnVlXG59KTtcblxudmFyIF9jcmVhdGVDbGFzcyA9IChmdW5jdGlvbiAoKSB7IGZ1bmN0aW9uIGRlZmluZVByb3BlcnRpZXModGFyZ2V0LCBwcm9wcykgeyBmb3IgKHZhciBpID0gMDsgaSA8IHByb3BzLmxlbmd0aDsgaSsrKSB7IHZhciBkZXNjcmlwdG9yID0gcHJvcHNbaV07IGRlc2NyaXB0b3IuZW51bWVyYWJsZSA9IGRlc2NyaXB0b3IuZW51bWVyYWJsZSB8fCBmYWxzZTsgZGVzY3JpcHRvci5jb25maWd1cmFibGUgPSB0cnVlOyBpZiAoJ3ZhbHVlJyBpbiBkZXNjcmlwdG9yKSBkZXNjcmlwdG9yLndyaXRhYmxlID0gdHJ1ZTsgT2JqZWN0LmRlZmluZVByb3BlcnR5KHRhcmdldCwgZGVzY3JpcHRvci5rZXksIGRlc2NyaXB0b3IpOyB9IH0gcmV0dXJuIGZ1bmN0aW9uIChDb25zdHJ1Y3RvciwgcHJvdG9Qcm9wcywgc3RhdGljUHJvcHMpIHsgaWYgKHByb3RvUHJvcHMpIGRlZmluZVByb3BlcnRpZXMoQ29uc3RydWN0b3IucHJvdG90eXBlLCBwcm90b1Byb3BzKTsgaWYgKHN0YXRpY1Byb3BzKSBkZWZpbmVQcm9wZXJ0aWVzKENvbnN0cnVjdG9yLCBzdGF0aWNQcm9wcyk7IHJldHVybiBDb25zdHJ1Y3RvcjsgfTsgfSkoKTtcblxuZnVuY3Rpb24gX2NsYXNzQ2FsbENoZWNrKGluc3RhbmNlLCBDb25zdHJ1Y3RvcikgeyBpZiAoIShpbnN0YW5jZSBpbnN0YW5jZW9mIENvbnN0cnVjdG9yKSkgeyB0aHJvdyBuZXcgVHlwZUVycm9yKCdDYW5ub3QgY2FsbCBhIGNsYXNzIGFzIGEgZnVuY3Rpb24nKTsgfSB9XG5cbnZhciBFdmVudFByb3RvdHlwZSA9IChmdW5jdGlvbiAoKSB7XG4gIGZ1bmN0aW9uIEV2ZW50UHJvdG90eXBlKCkge1xuICAgIF9jbGFzc0NhbGxDaGVjayh0aGlzLCBFdmVudFByb3RvdHlwZSk7XG4gIH1cblxuICBfY3JlYXRlQ2xhc3MoRXZlbnRQcm90b3R5cGUsIFt7XG4gICAga2V5OiAnc3RvcFByb3BhZ2F0aW9uJyxcblxuICAgIC8vIE5vb3BzXG4gICAgdmFsdWU6IGZ1bmN0aW9uIHN0b3BQcm9wYWdhdGlvbigpIHt9XG4gIH0sIHtcbiAgICBrZXk6ICdzdG9wSW1tZWRpYXRlUHJvcGFnYXRpb24nLFxuICAgIHZhbHVlOiBmdW5jdGlvbiBzdG9wSW1tZWRpYXRlUHJvcGFnYXRpb24oKSB7fVxuXG4gICAgLy8gaWYgbm8gYXJndW1lbnRzIGFyZSBwYXNzZWQgdGhlbiB0aGUgdHlwZSBpcyBzZXQgdG8gXCJ1bmRlZmluZWRcIiBvblxuICAgIC8vIGNocm9tZSBhbmQgc2FmYXJpLlxuICB9LCB7XG4gICAga2V5OiAnaW5pdEV2ZW50JyxcbiAgICB2YWx1ZTogZnVuY3Rpb24gaW5pdEV2ZW50KCkge1xuICAgICAgdmFyIHR5cGUgPSBhcmd1bWVudHMubGVuZ3RoIDw9IDAgfHwgYXJndW1lbnRzWzBdID09PSB1bmRlZmluZWQgPyAndW5kZWZpbmVkJyA6IGFyZ3VtZW50c1swXTtcbiAgICAgIHZhciBidWJibGVzID0gYXJndW1lbnRzLmxlbmd0aCA8PSAxIHx8IGFyZ3VtZW50c1sxXSA9PT0gdW5kZWZpbmVkID8gZmFsc2UgOiBhcmd1bWVudHNbMV07XG4gICAgICB2YXIgY2FuY2VsYWJsZSA9IGFyZ3VtZW50cy5sZW5ndGggPD0gMiB8fCBhcmd1bWVudHNbMl0gPT09IHVuZGVmaW5lZCA/IGZhbHNlIDogYXJndW1lbnRzWzJdO1xuXG4gICAgICBPYmplY3QuYXNzaWduKHRoaXMsIHtcbiAgICAgICAgdHlwZTogU3RyaW5nKHR5cGUpLFxuICAgICAgICBidWJibGVzOiBCb29sZWFuKGJ1YmJsZXMpLFxuICAgICAgICBjYW5jZWxhYmxlOiBCb29sZWFuKGNhbmNlbGFibGUpXG4gICAgICB9KTtcbiAgICB9XG4gIH1dKTtcblxuICByZXR1cm4gRXZlbnRQcm90b3R5cGU7XG59KSgpO1xuXG5leHBvcnRzWydkZWZhdWx0J10gPSBFdmVudFByb3RvdHlwZTtcbm1vZHVsZS5leHBvcnRzID0gZXhwb3J0c1snZGVmYXVsdCddOyIsIid1c2Ugc3RyaWN0JztcblxuT2JqZWN0LmRlZmluZVByb3BlcnR5KGV4cG9ydHMsICdfX2VzTW9kdWxlJywge1xuICB2YWx1ZTogdHJ1ZVxufSk7XG5cbnZhciBfZ2V0ID0gZnVuY3Rpb24gZ2V0KF94MiwgX3gzLCBfeDQpIHsgdmFyIF9hZ2FpbiA9IHRydWU7IF9mdW5jdGlvbjogd2hpbGUgKF9hZ2FpbikgeyB2YXIgb2JqZWN0ID0gX3gyLCBwcm9wZXJ0eSA9IF94MywgcmVjZWl2ZXIgPSBfeDQ7IF9hZ2FpbiA9IGZhbHNlOyBpZiAob2JqZWN0ID09PSBudWxsKSBvYmplY3QgPSBGdW5jdGlvbi5wcm90b3R5cGU7IHZhciBkZXNjID0gT2JqZWN0LmdldE93blByb3BlcnR5RGVzY3JpcHRvcihvYmplY3QsIHByb3BlcnR5KTsgaWYgKGRlc2MgPT09IHVuZGVmaW5lZCkgeyB2YXIgcGFyZW50ID0gT2JqZWN0LmdldFByb3RvdHlwZU9mKG9iamVjdCk7IGlmIChwYXJlbnQgPT09IG51bGwpIHsgcmV0dXJuIHVuZGVmaW5lZDsgfSBlbHNlIHsgX3gyID0gcGFyZW50OyBfeDMgPSBwcm9wZXJ0eTsgX3g0ID0gcmVjZWl2ZXI7IF9hZ2FpbiA9IHRydWU7IGRlc2MgPSBwYXJlbnQgPSB1bmRlZmluZWQ7IGNvbnRpbnVlIF9mdW5jdGlvbjsgfSB9IGVsc2UgaWYgKCd2YWx1ZScgaW4gZGVzYykgeyByZXR1cm4gZGVzYy52YWx1ZTsgfSBlbHNlIHsgdmFyIGdldHRlciA9IGRlc2MuZ2V0OyBpZiAoZ2V0dGVyID09PSB1bmRlZmluZWQpIHsgcmV0dXJuIHVuZGVmaW5lZDsgfSByZXR1cm4gZ2V0dGVyLmNhbGwocmVjZWl2ZXIpOyB9IH0gfTtcblxuZnVuY3Rpb24gX2ludGVyb3BSZXF1aXJlRGVmYXVsdChvYmopIHsgcmV0dXJuIG9iaiAmJiBvYmouX19lc01vZHVsZSA/IG9iaiA6IHsgJ2RlZmF1bHQnOiBvYmogfTsgfVxuXG5mdW5jdGlvbiBfY2xhc3NDYWxsQ2hlY2soaW5zdGFuY2UsIENvbnN0cnVjdG9yKSB7IGlmICghKGluc3RhbmNlIGluc3RhbmNlb2YgQ29uc3RydWN0b3IpKSB7IHRocm93IG5ldyBUeXBlRXJyb3IoJ0Nhbm5vdCBjYWxsIGEgY2xhc3MgYXMgYSBmdW5jdGlvbicpOyB9IH1cblxuZnVuY3Rpb24gX2luaGVyaXRzKHN1YkNsYXNzLCBzdXBlckNsYXNzKSB7IGlmICh0eXBlb2Ygc3VwZXJDbGFzcyAhPT0gJ2Z1bmN0aW9uJyAmJiBzdXBlckNsYXNzICE9PSBudWxsKSB7IHRocm93IG5ldyBUeXBlRXJyb3IoJ1N1cGVyIGV4cHJlc3Npb24gbXVzdCBlaXRoZXIgYmUgbnVsbCBvciBhIGZ1bmN0aW9uLCBub3QgJyArIHR5cGVvZiBzdXBlckNsYXNzKTsgfSBzdWJDbGFzcy5wcm90b3R5cGUgPSBPYmplY3QuY3JlYXRlKHN1cGVyQ2xhc3MgJiYgc3VwZXJDbGFzcy5wcm90b3R5cGUsIHsgY29uc3RydWN0b3I6IHsgdmFsdWU6IHN1YkNsYXNzLCBlbnVtZXJhYmxlOiBmYWxzZSwgd3JpdGFibGU6IHRydWUsIGNvbmZpZ3VyYWJsZTogdHJ1ZSB9IH0pOyBpZiAoc3VwZXJDbGFzcykgT2JqZWN0LnNldFByb3RvdHlwZU9mID8gT2JqZWN0LnNldFByb3RvdHlwZU9mKHN1YkNsYXNzLCBzdXBlckNsYXNzKSA6IHN1YkNsYXNzLl9fcHJvdG9fXyA9IHN1cGVyQ2xhc3M7IH1cblxudmFyIF9ldmVudFByb3RvdHlwZSA9IHJlcXVpcmUoJy4vZXZlbnQtcHJvdG90eXBlJyk7XG5cbnZhciBfZXZlbnRQcm90b3R5cGUyID0gX2ludGVyb3BSZXF1aXJlRGVmYXVsdChfZXZlbnRQcm90b3R5cGUpO1xuXG52YXIgRXZlbnQgPSAoZnVuY3Rpb24gKF9FdmVudFByb3RvdHlwZSkge1xuICBfaW5oZXJpdHMoRXZlbnQsIF9FdmVudFByb3RvdHlwZSk7XG5cbiAgZnVuY3Rpb24gRXZlbnQodHlwZSkge1xuICAgIHZhciBldmVudEluaXRDb25maWcgPSBhcmd1bWVudHMubGVuZ3RoIDw9IDEgfHwgYXJndW1lbnRzWzFdID09PSB1bmRlZmluZWQgPyB7fSA6IGFyZ3VtZW50c1sxXTtcblxuICAgIF9jbGFzc0NhbGxDaGVjayh0aGlzLCBFdmVudCk7XG5cbiAgICBfZ2V0KE9iamVjdC5nZXRQcm90b3R5cGVPZihFdmVudC5wcm90b3R5cGUpLCAnY29uc3RydWN0b3InLCB0aGlzKS5jYWxsKHRoaXMpO1xuXG4gICAgaWYgKCF0eXBlKSB7XG4gICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdGYWlsZWQgdG8gY29uc3RydWN0IFxcJ0V2ZW50XFwnOiAxIGFyZ3VtZW50IHJlcXVpcmVkLCBidXQgb25seSAwIHByZXNlbnQuJyk7XG4gICAgfVxuXG4gICAgaWYgKHR5cGVvZiBldmVudEluaXRDb25maWcgIT09ICdvYmplY3QnKSB7XG4gICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdGYWlsZWQgdG8gY29uc3RydWN0IFxcJ0V2ZW50XFwnOiBwYXJhbWV0ZXIgMiAoXFwnZXZlbnRJbml0RGljdFxcJykgaXMgbm90IGFuIG9iamVjdCcpO1xuICAgIH1cblxuICAgIHZhciBidWJibGVzID0gZXZlbnRJbml0Q29uZmlnLmJ1YmJsZXM7XG4gICAgdmFyIGNhbmNlbGFibGUgPSBldmVudEluaXRDb25maWcuY2FuY2VsYWJsZTtcblxuICAgIHRoaXMudHlwZSA9IFN0cmluZyh0eXBlKTtcbiAgICB0aGlzLnRpbWVTdGFtcCA9IERhdGUubm93KCk7XG4gICAgdGhpcy50YXJnZXQgPSBudWxsO1xuICAgIHRoaXMuc3JjRWxlbWVudCA9IG51bGw7XG4gICAgdGhpcy5yZXR1cm5WYWx1ZSA9IHRydWU7XG4gICAgdGhpcy5pc1RydXN0ZWQgPSBmYWxzZTtcbiAgICB0aGlzLmV2ZW50UGhhc2UgPSAwO1xuICAgIHRoaXMuZGVmYXVsdFByZXZlbnRlZCA9IGZhbHNlO1xuICAgIHRoaXMuY3VycmVudFRhcmdldCA9IG51bGw7XG4gICAgdGhpcy5jYW5jZWxhYmxlID0gY2FuY2VsYWJsZSA/IEJvb2xlYW4oY2FuY2VsYWJsZSkgOiBmYWxzZTtcbiAgICB0aGlzLmNhbm5jZWxCdWJibGUgPSBmYWxzZTtcbiAgICB0aGlzLmJ1YmJsZXMgPSBidWJibGVzID8gQm9vbGVhbihidWJibGVzKSA6IGZhbHNlO1xuICB9XG5cbiAgcmV0dXJuIEV2ZW50O1xufSkoX2V2ZW50UHJvdG90eXBlMlsnZGVmYXVsdCddKTtcblxuZXhwb3J0c1snZGVmYXVsdCddID0gRXZlbnQ7XG5tb2R1bGUuZXhwb3J0cyA9IGV4cG9ydHNbJ2RlZmF1bHQnXTsiLCIndXNlIHN0cmljdCc7XG5cbk9iamVjdC5kZWZpbmVQcm9wZXJ0eShleHBvcnRzLCAnX19lc01vZHVsZScsIHtcbiAgdmFsdWU6IHRydWVcbn0pO1xuXG52YXIgX2dldCA9IGZ1bmN0aW9uIGdldChfeDIsIF94MywgX3g0KSB7IHZhciBfYWdhaW4gPSB0cnVlOyBfZnVuY3Rpb246IHdoaWxlIChfYWdhaW4pIHsgdmFyIG9iamVjdCA9IF94MiwgcHJvcGVydHkgPSBfeDMsIHJlY2VpdmVyID0gX3g0OyBfYWdhaW4gPSBmYWxzZTsgaWYgKG9iamVjdCA9PT0gbnVsbCkgb2JqZWN0ID0gRnVuY3Rpb24ucHJvdG90eXBlOyB2YXIgZGVzYyA9IE9iamVjdC5nZXRPd25Qcm9wZXJ0eURlc2NyaXB0b3Iob2JqZWN0LCBwcm9wZXJ0eSk7IGlmIChkZXNjID09PSB1bmRlZmluZWQpIHsgdmFyIHBhcmVudCA9IE9iamVjdC5nZXRQcm90b3R5cGVPZihvYmplY3QpOyBpZiAocGFyZW50ID09PSBudWxsKSB7IHJldHVybiB1bmRlZmluZWQ7IH0gZWxzZSB7IF94MiA9IHBhcmVudDsgX3gzID0gcHJvcGVydHk7IF94NCA9IHJlY2VpdmVyOyBfYWdhaW4gPSB0cnVlOyBkZXNjID0gcGFyZW50ID0gdW5kZWZpbmVkOyBjb250aW51ZSBfZnVuY3Rpb247IH0gfSBlbHNlIGlmICgndmFsdWUnIGluIGRlc2MpIHsgcmV0dXJuIGRlc2MudmFsdWU7IH0gZWxzZSB7IHZhciBnZXR0ZXIgPSBkZXNjLmdldDsgaWYgKGdldHRlciA9PT0gdW5kZWZpbmVkKSB7IHJldHVybiB1bmRlZmluZWQ7IH0gcmV0dXJuIGdldHRlci5jYWxsKHJlY2VpdmVyKTsgfSB9IH07XG5cbmZ1bmN0aW9uIF9pbnRlcm9wUmVxdWlyZURlZmF1bHQob2JqKSB7IHJldHVybiBvYmogJiYgb2JqLl9fZXNNb2R1bGUgPyBvYmogOiB7ICdkZWZhdWx0Jzogb2JqIH07IH1cblxuZnVuY3Rpb24gX2NsYXNzQ2FsbENoZWNrKGluc3RhbmNlLCBDb25zdHJ1Y3RvcikgeyBpZiAoIShpbnN0YW5jZSBpbnN0YW5jZW9mIENvbnN0cnVjdG9yKSkgeyB0aHJvdyBuZXcgVHlwZUVycm9yKCdDYW5ub3QgY2FsbCBhIGNsYXNzIGFzIGEgZnVuY3Rpb24nKTsgfSB9XG5cbmZ1bmN0aW9uIF9pbmhlcml0cyhzdWJDbGFzcywgc3VwZXJDbGFzcykgeyBpZiAodHlwZW9mIHN1cGVyQ2xhc3MgIT09ICdmdW5jdGlvbicgJiYgc3VwZXJDbGFzcyAhPT0gbnVsbCkgeyB0aHJvdyBuZXcgVHlwZUVycm9yKCdTdXBlciBleHByZXNzaW9uIG11c3QgZWl0aGVyIGJlIG51bGwgb3IgYSBmdW5jdGlvbiwgbm90ICcgKyB0eXBlb2Ygc3VwZXJDbGFzcyk7IH0gc3ViQ2xhc3MucHJvdG90eXBlID0gT2JqZWN0LmNyZWF0ZShzdXBlckNsYXNzICYmIHN1cGVyQ2xhc3MucHJvdG90eXBlLCB7IGNvbnN0cnVjdG9yOiB7IHZhbHVlOiBzdWJDbGFzcywgZW51bWVyYWJsZTogZmFsc2UsIHdyaXRhYmxlOiB0cnVlLCBjb25maWd1cmFibGU6IHRydWUgfSB9KTsgaWYgKHN1cGVyQ2xhc3MpIE9iamVjdC5zZXRQcm90b3R5cGVPZiA/IE9iamVjdC5zZXRQcm90b3R5cGVPZihzdWJDbGFzcywgc3VwZXJDbGFzcykgOiBzdWJDbGFzcy5fX3Byb3RvX18gPSBzdXBlckNsYXNzOyB9XG5cbnZhciBfZXZlbnRQcm90b3R5cGUgPSByZXF1aXJlKCcuL2V2ZW50LXByb3RvdHlwZScpO1xuXG52YXIgX2V2ZW50UHJvdG90eXBlMiA9IF9pbnRlcm9wUmVxdWlyZURlZmF1bHQoX2V2ZW50UHJvdG90eXBlKTtcblxudmFyIE1lc3NhZ2VFdmVudCA9IChmdW5jdGlvbiAoX0V2ZW50UHJvdG90eXBlKSB7XG4gIF9pbmhlcml0cyhNZXNzYWdlRXZlbnQsIF9FdmVudFByb3RvdHlwZSk7XG5cbiAgZnVuY3Rpb24gTWVzc2FnZUV2ZW50KHR5cGUpIHtcbiAgICB2YXIgZXZlbnRJbml0Q29uZmlnID0gYXJndW1lbnRzLmxlbmd0aCA8PSAxIHx8IGFyZ3VtZW50c1sxXSA9PT0gdW5kZWZpbmVkID8ge30gOiBhcmd1bWVudHNbMV07XG5cbiAgICBfY2xhc3NDYWxsQ2hlY2sodGhpcywgTWVzc2FnZUV2ZW50KTtcblxuICAgIF9nZXQoT2JqZWN0LmdldFByb3RvdHlwZU9mKE1lc3NhZ2VFdmVudC5wcm90b3R5cGUpLCAnY29uc3RydWN0b3InLCB0aGlzKS5jYWxsKHRoaXMpO1xuXG4gICAgaWYgKCF0eXBlKSB7XG4gICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdGYWlsZWQgdG8gY29uc3RydWN0IFxcJ01lc3NhZ2VFdmVudFxcJzogMSBhcmd1bWVudCByZXF1aXJlZCwgYnV0IG9ubHkgMCBwcmVzZW50LicpO1xuICAgIH1cblxuICAgIGlmICh0eXBlb2YgZXZlbnRJbml0Q29uZmlnICE9PSAnb2JqZWN0Jykge1xuICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcignRmFpbGVkIHRvIGNvbnN0cnVjdCBcXCdNZXNzYWdlRXZlbnRcXCc6IHBhcmFtZXRlciAyIChcXCdldmVudEluaXREaWN0XFwnKSBpcyBub3QgYW4gb2JqZWN0Jyk7XG4gICAgfVxuXG4gICAgdmFyIGJ1YmJsZXMgPSBldmVudEluaXRDb25maWcuYnViYmxlcztcbiAgICB2YXIgY2FuY2VsYWJsZSA9IGV2ZW50SW5pdENvbmZpZy5jYW5jZWxhYmxlO1xuICAgIHZhciBkYXRhID0gZXZlbnRJbml0Q29uZmlnLmRhdGE7XG4gICAgdmFyIG9yaWdpbiA9IGV2ZW50SW5pdENvbmZpZy5vcmlnaW47XG4gICAgdmFyIGxhc3RFdmVudElkID0gZXZlbnRJbml0Q29uZmlnLmxhc3RFdmVudElkO1xuICAgIHZhciBwb3J0cyA9IGV2ZW50SW5pdENvbmZpZy5wb3J0cztcblxuICAgIHRoaXMudHlwZSA9IFN0cmluZyh0eXBlKTtcbiAgICB0aGlzLnRpbWVTdGFtcCA9IERhdGUubm93KCk7XG4gICAgdGhpcy50YXJnZXQgPSBudWxsO1xuICAgIHRoaXMuc3JjRWxlbWVudCA9IG51bGw7XG4gICAgdGhpcy5yZXR1cm5WYWx1ZSA9IHRydWU7XG4gICAgdGhpcy5pc1RydXN0ZWQgPSBmYWxzZTtcbiAgICB0aGlzLmV2ZW50UGhhc2UgPSAwO1xuICAgIHRoaXMuZGVmYXVsdFByZXZlbnRlZCA9IGZhbHNlO1xuICAgIHRoaXMuY3VycmVudFRhcmdldCA9IG51bGw7XG4gICAgdGhpcy5jYW5jZWxhYmxlID0gY2FuY2VsYWJsZSA/IEJvb2xlYW4oY2FuY2VsYWJsZSkgOiBmYWxzZTtcbiAgICB0aGlzLmNhbm5jZWxCdWJibGUgPSBmYWxzZTtcbiAgICB0aGlzLmJ1YmJsZXMgPSBidWJibGVzID8gQm9vbGVhbihidWJibGVzKSA6IGZhbHNlO1xuICAgIHRoaXMub3JpZ2luID0gb3JpZ2luID8gU3RyaW5nKG9yaWdpbikgOiAnJztcbiAgICB0aGlzLnBvcnRzID0gdHlwZW9mIHBvcnRzID09PSAndW5kZWZpbmVkJyA/IG51bGwgOiBwb3J0cztcbiAgICB0aGlzLmRhdGEgPSB0eXBlb2YgZGF0YSA9PT0gJ3VuZGVmaW5lZCcgPyBudWxsIDogZGF0YTtcbiAgICB0aGlzLmxhc3RFdmVudElkID0gbGFzdEV2ZW50SWQgPyBTdHJpbmcobGFzdEV2ZW50SWQpIDogJyc7XG4gIH1cblxuICByZXR1cm4gTWVzc2FnZUV2ZW50O1xufSkoX2V2ZW50UHJvdG90eXBlMlsnZGVmYXVsdCddKTtcblxuZXhwb3J0c1snZGVmYXVsdCddID0gTWVzc2FnZUV2ZW50O1xubW9kdWxlLmV4cG9ydHMgPSBleHBvcnRzWydkZWZhdWx0J107IiwiJ3VzZSBzdHJpY3QnO1xuXG5PYmplY3QuZGVmaW5lUHJvcGVydHkoZXhwb3J0cywgJ19fZXNNb2R1bGUnLCB7XG4gIHZhbHVlOiB0cnVlXG59KTtcblxuZnVuY3Rpb24gX2ludGVyb3BSZXF1aXJlRGVmYXVsdChvYmopIHsgcmV0dXJuIG9iaiAmJiBvYmouX19lc01vZHVsZSA/IG9iaiA6IHsgJ2RlZmF1bHQnOiBvYmogfTsgfVxuXG52YXIgX3NlcnZlciA9IHJlcXVpcmUoJy4vc2VydmVyJyk7XG5cbnZhciBfc2VydmVyMiA9IF9pbnRlcm9wUmVxdWlyZURlZmF1bHQoX3NlcnZlcik7XG5cbnZhciBfc29ja2V0SW8gPSByZXF1aXJlKCcuL3NvY2tldC1pbycpO1xuXG52YXIgX3NvY2tldElvMiA9IF9pbnRlcm9wUmVxdWlyZURlZmF1bHQoX3NvY2tldElvKTtcblxudmFyIF93ZWJzb2NrZXQgPSByZXF1aXJlKCcuL3dlYnNvY2tldCcpO1xuXG52YXIgX3dlYnNvY2tldDIgPSBfaW50ZXJvcFJlcXVpcmVEZWZhdWx0KF93ZWJzb2NrZXQpO1xuXG5pZiAodHlwZW9mIHdpbmRvdyAhPT0gJ3VuZGVmaW5lZCcpIHtcbiAgd2luZG93Lk1vY2tTZXJ2ZXIgPSBfc2VydmVyMlsnZGVmYXVsdCddO1xuICB3aW5kb3cuTW9ja1dlYlNvY2tldCA9IF93ZWJzb2NrZXQyWydkZWZhdWx0J107XG4gIHdpbmRvdy5Nb2NrU29ja2V0SU8gPSBfc29ja2V0SW8yWydkZWZhdWx0J107XG59XG5cbnZhciBTZXJ2ZXIgPSBfc2VydmVyMlsnZGVmYXVsdCddO1xuZXhwb3J0cy5TZXJ2ZXIgPSBTZXJ2ZXI7XG52YXIgV2ViU29ja2V0ID0gX3dlYnNvY2tldDJbJ2RlZmF1bHQnXTtcbmV4cG9ydHMuV2ViU29ja2V0ID0gV2ViU29ja2V0O1xudmFyIFNvY2tldElPID0gX3NvY2tldElvMlsnZGVmYXVsdCddO1xuZXhwb3J0cy5Tb2NrZXRJTyA9IFNvY2tldElPOyIsIid1c2Ugc3RyaWN0JztcblxuT2JqZWN0LmRlZmluZVByb3BlcnR5KGV4cG9ydHMsICdfX2VzTW9kdWxlJywge1xuICB2YWx1ZTogdHJ1ZVxufSk7XG5cbnZhciBfY3JlYXRlQ2xhc3MgPSAoZnVuY3Rpb24gKCkgeyBmdW5jdGlvbiBkZWZpbmVQcm9wZXJ0aWVzKHRhcmdldCwgcHJvcHMpIHsgZm9yICh2YXIgaSA9IDA7IGkgPCBwcm9wcy5sZW5ndGg7IGkrKykgeyB2YXIgZGVzY3JpcHRvciA9IHByb3BzW2ldOyBkZXNjcmlwdG9yLmVudW1lcmFibGUgPSBkZXNjcmlwdG9yLmVudW1lcmFibGUgfHwgZmFsc2U7IGRlc2NyaXB0b3IuY29uZmlndXJhYmxlID0gdHJ1ZTsgaWYgKCd2YWx1ZScgaW4gZGVzY3JpcHRvcikgZGVzY3JpcHRvci53cml0YWJsZSA9IHRydWU7IE9iamVjdC5kZWZpbmVQcm9wZXJ0eSh0YXJnZXQsIGRlc2NyaXB0b3Iua2V5LCBkZXNjcmlwdG9yKTsgfSB9IHJldHVybiBmdW5jdGlvbiAoQ29uc3RydWN0b3IsIHByb3RvUHJvcHMsIHN0YXRpY1Byb3BzKSB7IGlmIChwcm90b1Byb3BzKSBkZWZpbmVQcm9wZXJ0aWVzKENvbnN0cnVjdG9yLnByb3RvdHlwZSwgcHJvdG9Qcm9wcyk7IGlmIChzdGF0aWNQcm9wcykgZGVmaW5lUHJvcGVydGllcyhDb25zdHJ1Y3Rvciwgc3RhdGljUHJvcHMpOyByZXR1cm4gQ29uc3RydWN0b3I7IH07IH0pKCk7XG5cbmZ1bmN0aW9uIF9jbGFzc0NhbGxDaGVjayhpbnN0YW5jZSwgQ29uc3RydWN0b3IpIHsgaWYgKCEoaW5zdGFuY2UgaW5zdGFuY2VvZiBDb25zdHJ1Y3RvcikpIHsgdGhyb3cgbmV3IFR5cGVFcnJvcignQ2Fubm90IGNhbGwgYSBjbGFzcyBhcyBhIGZ1bmN0aW9uJyk7IH0gfVxuXG52YXIgX2hlbHBlcnNBcnJheUhlbHBlcnMgPSByZXF1aXJlKCcuL2hlbHBlcnMvYXJyYXktaGVscGVycycpO1xuXG4vKlxuKiBUaGUgbmV0d29yayBicmlkZ2UgaXMgYSB3YXkgZm9yIHRoZSBtb2NrIHdlYnNvY2tldCBvYmplY3QgdG8gJ2NvbW11bmljYXRlJyB3aXRoXG4qIGFsbCBhdmFsaWJsZSBzZXJ2ZXJzLiBUaGlzIGlzIGEgc2luZ2xldG9uIG9iamVjdCBzbyBpdCBpcyBpbXBvcnRhbnQgdGhhdCB5b3VcbiogY2xlYW4gdXAgdXJsTWFwIHdoZW5ldmVyIHlvdSBhcmUgZmluaXNoZWQuXG4qL1xuXG52YXIgTmV0d29ya0JyaWRnZSA9IChmdW5jdGlvbiAoKSB7XG4gIGZ1bmN0aW9uIE5ldHdvcmtCcmlkZ2UoKSB7XG4gICAgX2NsYXNzQ2FsbENoZWNrKHRoaXMsIE5ldHdvcmtCcmlkZ2UpO1xuXG4gICAgdGhpcy51cmxNYXAgPSB7fTtcbiAgfVxuXG4gIC8qXG4gICogQXR0YWNoZXMgYSB3ZWJzb2NrZXQgb2JqZWN0IHRvIHRoZSB1cmxNYXAgaGFzaCBzbyB0aGF0IGl0IGNhbiBmaW5kIHRoZSBzZXJ2ZXJcbiAgKiBpdCBpcyBjb25uZWN0ZWQgdG8gYW5kIHRoZSBzZXJ2ZXIgaW4gdHVybiBjYW4gZmluZCBpdC5cbiAgKlxuICAqIEBwYXJhbSB7b2JqZWN0fSB3ZWJzb2NrZXQgLSB3ZWJzb2NrZXQgb2JqZWN0IHRvIGFkZCB0byB0aGUgdXJsTWFwIGhhc2hcbiAgKiBAcGFyYW0ge3N0cmluZ30gdXJsXG4gICovXG5cbiAgX2NyZWF0ZUNsYXNzKE5ldHdvcmtCcmlkZ2UsIFt7XG4gICAga2V5OiAnYXR0YWNoV2ViU29ja2V0JyxcbiAgICB2YWx1ZTogZnVuY3Rpb24gYXR0YWNoV2ViU29ja2V0KHdlYnNvY2tldCwgdXJsKSB7XG4gICAgICB2YXIgY29ubmVjdGlvbkxvb2t1cCA9IHRoaXMudXJsTWFwW3VybF07XG5cbiAgICAgIGlmIChjb25uZWN0aW9uTG9va3VwICYmIGNvbm5lY3Rpb25Mb29rdXAuc2VydmVyICYmIGNvbm5lY3Rpb25Mb29rdXAud2Vic29ja2V0cy5pbmRleE9mKHdlYnNvY2tldCkgPT09IC0xKSB7XG4gICAgICAgIGNvbm5lY3Rpb25Mb29rdXAud2Vic29ja2V0cy5wdXNoKHdlYnNvY2tldCk7XG4gICAgICAgIHJldHVybiBjb25uZWN0aW9uTG9va3VwLnNlcnZlcjtcbiAgICAgIH1cbiAgICB9XG5cbiAgICAvKlxuICAgICogQXR0YWNoZXMgYSB3ZWJzb2NrZXQgdG8gYSByb29tXG4gICAgKi9cbiAgfSwge1xuICAgIGtleTogJ2FkZE1lbWJlcnNoaXBUb1Jvb20nLFxuICAgIHZhbHVlOiBmdW5jdGlvbiBhZGRNZW1iZXJzaGlwVG9Sb29tKHdlYnNvY2tldCwgcm9vbSkge1xuICAgICAgdmFyIGNvbm5lY3Rpb25Mb29rdXAgPSB0aGlzLnVybE1hcFt3ZWJzb2NrZXQudXJsXTtcblxuICAgICAgaWYgKGNvbm5lY3Rpb25Mb29rdXAgJiYgY29ubmVjdGlvbkxvb2t1cC5zZXJ2ZXIgJiYgY29ubmVjdGlvbkxvb2t1cC53ZWJzb2NrZXRzLmluZGV4T2Yod2Vic29ja2V0KSAhPT0gLTEpIHtcbiAgICAgICAgaWYgKCFjb25uZWN0aW9uTG9va3VwLnJvb21NZW1iZXJzaGlwc1tyb29tXSkge1xuICAgICAgICAgIGNvbm5lY3Rpb25Mb29rdXAucm9vbU1lbWJlcnNoaXBzW3Jvb21dID0gW107XG4gICAgICAgIH1cblxuICAgICAgICBjb25uZWN0aW9uTG9va3VwLnJvb21NZW1iZXJzaGlwc1tyb29tXS5wdXNoKHdlYnNvY2tldCk7XG4gICAgICB9XG4gICAgfVxuXG4gICAgLypcbiAgICAqIEF0dGFjaGVzIGEgc2VydmVyIG9iamVjdCB0byB0aGUgdXJsTWFwIGhhc2ggc28gdGhhdCBpdCBjYW4gZmluZCBhIHdlYnNvY2tldHNcbiAgICAqIHdoaWNoIGFyZSBjb25uZWN0ZWQgdG8gaXQgYW5kIHNvIHRoYXQgd2Vic29ja2V0cyBjYW4gaW4gdHVybiBjYW4gZmluZCBpdC5cbiAgICAqXG4gICAgKiBAcGFyYW0ge29iamVjdH0gc2VydmVyIC0gc2VydmVyIG9iamVjdCB0byBhZGQgdG8gdGhlIHVybE1hcCBoYXNoXG4gICAgKiBAcGFyYW0ge3N0cmluZ30gdXJsXG4gICAgKi9cbiAgfSwge1xuICAgIGtleTogJ2F0dGFjaFNlcnZlcicsXG4gICAgdmFsdWU6IGZ1bmN0aW9uIGF0dGFjaFNlcnZlcihzZXJ2ZXIsIHVybCkge1xuICAgICAgdmFyIGNvbm5lY3Rpb25Mb29rdXAgPSB0aGlzLnVybE1hcFt1cmxdO1xuXG4gICAgICBpZiAoIWNvbm5lY3Rpb25Mb29rdXApIHtcbiAgICAgICAgdGhpcy51cmxNYXBbdXJsXSA9IHtcbiAgICAgICAgICBzZXJ2ZXI6IHNlcnZlcixcbiAgICAgICAgICB3ZWJzb2NrZXRzOiBbXSxcbiAgICAgICAgICByb29tTWVtYmVyc2hpcHM6IHt9XG4gICAgICAgIH07XG5cbiAgICAgICAgcmV0dXJuIHNlcnZlcjtcbiAgICAgIH1cbiAgICB9XG5cbiAgICAvKlxuICAgICogRmluZHMgdGhlIHNlcnZlciB3aGljaCBpcyAncnVubmluZycgb24gdGhlIGdpdmVuIHVybC5cbiAgICAqXG4gICAgKiBAcGFyYW0ge3N0cmluZ30gdXJsIC0gdGhlIHVybCB0byB1c2UgdG8gZmluZCB3aGljaCBzZXJ2ZXIgaXMgcnVubmluZyBvbiBpdFxuICAgICovXG4gIH0sIHtcbiAgICBrZXk6ICdzZXJ2ZXJMb29rdXAnLFxuICAgIHZhbHVlOiBmdW5jdGlvbiBzZXJ2ZXJMb29rdXAodXJsKSB7XG4gICAgICB2YXIgY29ubmVjdGlvbkxvb2t1cCA9IHRoaXMudXJsTWFwW3VybF07XG5cbiAgICAgIGlmIChjb25uZWN0aW9uTG9va3VwKSB7XG4gICAgICAgIHJldHVybiBjb25uZWN0aW9uTG9va3VwLnNlcnZlcjtcbiAgICAgIH1cbiAgICB9XG5cbiAgICAvKlxuICAgICogRmluZHMgYWxsIHdlYnNvY2tldHMgd2hpY2ggaXMgJ2xpc3RlbmluZycgb24gdGhlIGdpdmVuIHVybC5cbiAgICAqXG4gICAgKiBAcGFyYW0ge3N0cmluZ30gdXJsIC0gdGhlIHVybCB0byB1c2UgdG8gZmluZCBhbGwgd2Vic29ja2V0cyB3aGljaCBhcmUgYXNzb2NpYXRlZCB3aXRoIGl0XG4gICAgKiBAcGFyYW0ge3N0cmluZ30gcm9vbSAtIGlmIGEgcm9vbSBpcyBwcm92aWRlZCwgd2lsbCBvbmx5IHJldHVybiBzb2NrZXRzIGluIHRoaXMgcm9vbVxuICAgICovXG4gIH0sIHtcbiAgICBrZXk6ICd3ZWJzb2NrZXRzTG9va3VwJyxcbiAgICB2YWx1ZTogZnVuY3Rpb24gd2Vic29ja2V0c0xvb2t1cCh1cmwsIHJvb20pIHtcbiAgICAgIHZhciBjb25uZWN0aW9uTG9va3VwID0gdGhpcy51cmxNYXBbdXJsXTtcblxuICAgICAgaWYgKCFjb25uZWN0aW9uTG9va3VwKSB7XG4gICAgICAgIHJldHVybiBbXTtcbiAgICAgIH1cblxuICAgICAgaWYgKHJvb20pIHtcbiAgICAgICAgdmFyIG1lbWJlcnMgPSBjb25uZWN0aW9uTG9va3VwLnJvb21NZW1iZXJzaGlwc1tyb29tXTtcbiAgICAgICAgcmV0dXJuIG1lbWJlcnMgPyBtZW1iZXJzIDogW107XG4gICAgICB9XG5cbiAgICAgIHJldHVybiBjb25uZWN0aW9uTG9va3VwLndlYnNvY2tldHM7XG4gICAgfVxuXG4gICAgLypcbiAgICAqIFJlbW92ZXMgdGhlIGVudHJ5IGFzc29jaWF0ZWQgd2l0aCB0aGUgdXJsLlxuICAgICpcbiAgICAqIEBwYXJhbSB7c3RyaW5nfSB1cmxcbiAgICAqL1xuICB9LCB7XG4gICAga2V5OiAncmVtb3ZlU2VydmVyJyxcbiAgICB2YWx1ZTogZnVuY3Rpb24gcmVtb3ZlU2VydmVyKHVybCkge1xuICAgICAgZGVsZXRlIHRoaXMudXJsTWFwW3VybF07XG4gICAgfVxuXG4gICAgLypcbiAgICAqIFJlbW92ZXMgdGhlIGluZGl2aWR1YWwgd2Vic29ja2V0IGZyb20gdGhlIG1hcCBvZiBhc3NvY2lhdGVkIHdlYnNvY2tldHMuXG4gICAgKlxuICAgICogQHBhcmFtIHtvYmplY3R9IHdlYnNvY2tldCAtIHdlYnNvY2tldCBvYmplY3QgdG8gcmVtb3ZlIGZyb20gdGhlIHVybCBtYXBcbiAgICAqIEBwYXJhbSB7c3RyaW5nfSB1cmxcbiAgICAqL1xuICB9LCB7XG4gICAga2V5OiAncmVtb3ZlV2ViU29ja2V0JyxcbiAgICB2YWx1ZTogZnVuY3Rpb24gcmVtb3ZlV2ViU29ja2V0KHdlYnNvY2tldCwgdXJsKSB7XG4gICAgICB2YXIgY29ubmVjdGlvbkxvb2t1cCA9IHRoaXMudXJsTWFwW3VybF07XG5cbiAgICAgIGlmIChjb25uZWN0aW9uTG9va3VwKSB7XG4gICAgICAgIGNvbm5lY3Rpb25Mb29rdXAud2Vic29ja2V0cyA9ICgwLCBfaGVscGVyc0FycmF5SGVscGVycy5yZWplY3QpKGNvbm5lY3Rpb25Mb29rdXAud2Vic29ja2V0cywgZnVuY3Rpb24gKHNvY2tldCkge1xuICAgICAgICAgIHJldHVybiBzb2NrZXQgPT09IHdlYnNvY2tldDtcbiAgICAgICAgfSk7XG4gICAgICB9XG4gICAgfVxuXG4gICAgLypcbiAgICAqIFJlbW92ZXMgYSB3ZWJzb2NrZXQgZnJvbSBhIHJvb21cbiAgICAqL1xuICB9LCB7XG4gICAga2V5OiAncmVtb3ZlTWVtYmVyc2hpcEZyb21Sb29tJyxcbiAgICB2YWx1ZTogZnVuY3Rpb24gcmVtb3ZlTWVtYmVyc2hpcEZyb21Sb29tKHdlYnNvY2tldCwgcm9vbSkge1xuICAgICAgdmFyIGNvbm5lY3Rpb25Mb29rdXAgPSB0aGlzLnVybE1hcFt3ZWJzb2NrZXQudXJsXTtcbiAgICAgIHZhciBtZW1iZXJzaGlwcyA9IGNvbm5lY3Rpb25Mb29rdXAucm9vbU1lbWJlcnNoaXBzW3Jvb21dO1xuXG4gICAgICBpZiAoY29ubmVjdGlvbkxvb2t1cCAmJiBtZW1iZXJzaGlwcyAhPT0gbnVsbCkge1xuICAgICAgICBjb25uZWN0aW9uTG9va3VwLnJvb21NZW1iZXJzaGlwc1tyb29tXSA9ICgwLCBfaGVscGVyc0FycmF5SGVscGVycy5yZWplY3QpKG1lbWJlcnNoaXBzLCBmdW5jdGlvbiAoc29ja2V0KSB7XG4gICAgICAgICAgcmV0dXJuIHNvY2tldCA9PT0gd2Vic29ja2V0O1xuICAgICAgICB9KTtcbiAgICAgIH1cbiAgICB9XG4gIH1dKTtcblxuICByZXR1cm4gTmV0d29ya0JyaWRnZTtcbn0pKCk7XG5cbmV4cG9ydHNbJ2RlZmF1bHQnXSA9IG5ldyBOZXR3b3JrQnJpZGdlKCk7XG4vLyBOb3RlOiB0aGlzIGlzIGEgc2luZ2xldG9uXG5tb2R1bGUuZXhwb3J0cyA9IGV4cG9ydHNbJ2RlZmF1bHQnXTsiLCIndXNlIHN0cmljdCc7XG5cbk9iamVjdC5kZWZpbmVQcm9wZXJ0eShleHBvcnRzLCAnX19lc01vZHVsZScsIHtcbiAgdmFsdWU6IHRydWVcbn0pO1xuXG52YXIgX2NyZWF0ZUNsYXNzID0gKGZ1bmN0aW9uICgpIHsgZnVuY3Rpb24gZGVmaW5lUHJvcGVydGllcyh0YXJnZXQsIHByb3BzKSB7IGZvciAodmFyIGkgPSAwOyBpIDwgcHJvcHMubGVuZ3RoOyBpKyspIHsgdmFyIGRlc2NyaXB0b3IgPSBwcm9wc1tpXTsgZGVzY3JpcHRvci5lbnVtZXJhYmxlID0gZGVzY3JpcHRvci5lbnVtZXJhYmxlIHx8IGZhbHNlOyBkZXNjcmlwdG9yLmNvbmZpZ3VyYWJsZSA9IHRydWU7IGlmICgndmFsdWUnIGluIGRlc2NyaXB0b3IpIGRlc2NyaXB0b3Iud3JpdGFibGUgPSB0cnVlOyBPYmplY3QuZGVmaW5lUHJvcGVydHkodGFyZ2V0LCBkZXNjcmlwdG9yLmtleSwgZGVzY3JpcHRvcik7IH0gfSByZXR1cm4gZnVuY3Rpb24gKENvbnN0cnVjdG9yLCBwcm90b1Byb3BzLCBzdGF0aWNQcm9wcykgeyBpZiAocHJvdG9Qcm9wcykgZGVmaW5lUHJvcGVydGllcyhDb25zdHJ1Y3Rvci5wcm90b3R5cGUsIHByb3RvUHJvcHMpOyBpZiAoc3RhdGljUHJvcHMpIGRlZmluZVByb3BlcnRpZXMoQ29uc3RydWN0b3IsIHN0YXRpY1Byb3BzKTsgcmV0dXJuIENvbnN0cnVjdG9yOyB9OyB9KSgpO1xuXG52YXIgX2dldCA9IGZ1bmN0aW9uIGdldChfeDQsIF94NSwgX3g2KSB7IHZhciBfYWdhaW4gPSB0cnVlOyBfZnVuY3Rpb246IHdoaWxlIChfYWdhaW4pIHsgdmFyIG9iamVjdCA9IF94NCwgcHJvcGVydHkgPSBfeDUsIHJlY2VpdmVyID0gX3g2OyBfYWdhaW4gPSBmYWxzZTsgaWYgKG9iamVjdCA9PT0gbnVsbCkgb2JqZWN0ID0gRnVuY3Rpb24ucHJvdG90eXBlOyB2YXIgZGVzYyA9IE9iamVjdC5nZXRPd25Qcm9wZXJ0eURlc2NyaXB0b3Iob2JqZWN0LCBwcm9wZXJ0eSk7IGlmIChkZXNjID09PSB1bmRlZmluZWQpIHsgdmFyIHBhcmVudCA9IE9iamVjdC5nZXRQcm90b3R5cGVPZihvYmplY3QpOyBpZiAocGFyZW50ID09PSBudWxsKSB7IHJldHVybiB1bmRlZmluZWQ7IH0gZWxzZSB7IF94NCA9IHBhcmVudDsgX3g1ID0gcHJvcGVydHk7IF94NiA9IHJlY2VpdmVyOyBfYWdhaW4gPSB0cnVlOyBkZXNjID0gcGFyZW50ID0gdW5kZWZpbmVkOyBjb250aW51ZSBfZnVuY3Rpb247IH0gfSBlbHNlIGlmICgndmFsdWUnIGluIGRlc2MpIHsgcmV0dXJuIGRlc2MudmFsdWU7IH0gZWxzZSB7IHZhciBnZXR0ZXIgPSBkZXNjLmdldDsgaWYgKGdldHRlciA9PT0gdW5kZWZpbmVkKSB7IHJldHVybiB1bmRlZmluZWQ7IH0gcmV0dXJuIGdldHRlci5jYWxsKHJlY2VpdmVyKTsgfSB9IH07XG5cbmZ1bmN0aW9uIF9pbnRlcm9wUmVxdWlyZURlZmF1bHQob2JqKSB7IHJldHVybiBvYmogJiYgb2JqLl9fZXNNb2R1bGUgPyBvYmogOiB7ICdkZWZhdWx0Jzogb2JqIH07IH1cblxuZnVuY3Rpb24gX2NsYXNzQ2FsbENoZWNrKGluc3RhbmNlLCBDb25zdHJ1Y3RvcikgeyBpZiAoIShpbnN0YW5jZSBpbnN0YW5jZW9mIENvbnN0cnVjdG9yKSkgeyB0aHJvdyBuZXcgVHlwZUVycm9yKCdDYW5ub3QgY2FsbCBhIGNsYXNzIGFzIGEgZnVuY3Rpb24nKTsgfSB9XG5cbmZ1bmN0aW9uIF9pbmhlcml0cyhzdWJDbGFzcywgc3VwZXJDbGFzcykgeyBpZiAodHlwZW9mIHN1cGVyQ2xhc3MgIT09ICdmdW5jdGlvbicgJiYgc3VwZXJDbGFzcyAhPT0gbnVsbCkgeyB0aHJvdyBuZXcgVHlwZUVycm9yKCdTdXBlciBleHByZXNzaW9uIG11c3QgZWl0aGVyIGJlIG51bGwgb3IgYSBmdW5jdGlvbiwgbm90ICcgKyB0eXBlb2Ygc3VwZXJDbGFzcyk7IH0gc3ViQ2xhc3MucHJvdG90eXBlID0gT2JqZWN0LmNyZWF0ZShzdXBlckNsYXNzICYmIHN1cGVyQ2xhc3MucHJvdG90eXBlLCB7IGNvbnN0cnVjdG9yOiB7IHZhbHVlOiBzdWJDbGFzcywgZW51bWVyYWJsZTogZmFsc2UsIHdyaXRhYmxlOiB0cnVlLCBjb25maWd1cmFibGU6IHRydWUgfSB9KTsgaWYgKHN1cGVyQ2xhc3MpIE9iamVjdC5zZXRQcm90b3R5cGVPZiA/IE9iamVjdC5zZXRQcm90b3R5cGVPZihzdWJDbGFzcywgc3VwZXJDbGFzcykgOiBzdWJDbGFzcy5fX3Byb3RvX18gPSBzdXBlckNsYXNzOyB9XG5cbnZhciBfdXJpanMgPSByZXF1aXJlKCd1cmlqcycpO1xuXG52YXIgX3VyaWpzMiA9IF9pbnRlcm9wUmVxdWlyZURlZmF1bHQoX3VyaWpzKTtcblxudmFyIF93ZWJzb2NrZXQgPSByZXF1aXJlKCcuL3dlYnNvY2tldCcpO1xuXG52YXIgX3dlYnNvY2tldDIgPSBfaW50ZXJvcFJlcXVpcmVEZWZhdWx0KF93ZWJzb2NrZXQpO1xuXG52YXIgX2V2ZW50VGFyZ2V0ID0gcmVxdWlyZSgnLi9ldmVudC10YXJnZXQnKTtcblxudmFyIF9ldmVudFRhcmdldDIgPSBfaW50ZXJvcFJlcXVpcmVEZWZhdWx0KF9ldmVudFRhcmdldCk7XG5cbnZhciBfbmV0d29ya0JyaWRnZSA9IHJlcXVpcmUoJy4vbmV0d29yay1icmlkZ2UnKTtcblxudmFyIF9uZXR3b3JrQnJpZGdlMiA9IF9pbnRlcm9wUmVxdWlyZURlZmF1bHQoX25ldHdvcmtCcmlkZ2UpO1xuXG52YXIgX2hlbHBlcnNDbG9zZUNvZGVzID0gcmVxdWlyZSgnLi9oZWxwZXJzL2Nsb3NlLWNvZGVzJyk7XG5cbnZhciBfaGVscGVyc0Nsb3NlQ29kZXMyID0gX2ludGVyb3BSZXF1aXJlRGVmYXVsdChfaGVscGVyc0Nsb3NlQ29kZXMpO1xuXG52YXIgX2V2ZW50RmFjdG9yeSA9IHJlcXVpcmUoJy4vZXZlbnQtZmFjdG9yeScpO1xuXG4vKlxuKiBodHRwczovL2dpdGh1Yi5jb20vd2Vic29ja2V0cy93cyNzZXJ2ZXItZXhhbXBsZVxuKi9cblxudmFyIFNlcnZlciA9IChmdW5jdGlvbiAoX0V2ZW50VGFyZ2V0KSB7XG4gIF9pbmhlcml0cyhTZXJ2ZXIsIF9FdmVudFRhcmdldCk7XG5cbiAgLypcbiAgKiBAcGFyYW0ge3N0cmluZ30gdXJsXG4gICovXG5cbiAgZnVuY3Rpb24gU2VydmVyKHVybCkge1xuICAgIF9jbGFzc0NhbGxDaGVjayh0aGlzLCBTZXJ2ZXIpO1xuXG4gICAgX2dldChPYmplY3QuZ2V0UHJvdG90eXBlT2YoU2VydmVyLnByb3RvdHlwZSksICdjb25zdHJ1Y3RvcicsIHRoaXMpLmNhbGwodGhpcyk7XG4gICAgdGhpcy51cmwgPSAoMCwgX3VyaWpzMlsnZGVmYXVsdCddKSh1cmwpLnRvU3RyaW5nKCk7XG4gICAgdmFyIHNlcnZlciA9IF9uZXR3b3JrQnJpZGdlMlsnZGVmYXVsdCddLmF0dGFjaFNlcnZlcih0aGlzLCB0aGlzLnVybCk7XG5cbiAgICBpZiAoIXNlcnZlcikge1xuICAgICAgdGhpcy5kaXNwYXRjaEV2ZW50KCgwLCBfZXZlbnRGYWN0b3J5LmNyZWF0ZUV2ZW50KSh7IHR5cGU6ICdlcnJvcicgfSkpO1xuICAgICAgdGhyb3cgbmV3IEVycm9yKCdBIG1vY2sgc2VydmVyIGlzIGFscmVhZHkgbGlzdGVuaW5nIG9uIHRoaXMgdXJsJyk7XG4gICAgfVxuICB9XG5cbiAgLypcbiAgICogQWx0ZXJuYXRpdmUgY29uc3RydWN0b3IgdG8gc3VwcG9ydCBuYW1lc3BhY2VzIGluIHNvY2tldC5pb1xuICAgKlxuICAgKiBodHRwOi8vc29ja2V0LmlvL2RvY3Mvcm9vbXMtYW5kLW5hbWVzcGFjZXMvI2N1c3RvbS1uYW1lc3BhY2VzXG4gICAqL1xuXG4gIC8qXG4gICogVGhpcyBpcyB0aGUgbWFpbiBmdW5jdGlvbiBmb3IgdGhlIG1vY2sgc2VydmVyIHRvIHN1YnNjcmliZSB0byB0aGUgb24gZXZlbnRzLlxuICAqXG4gICogaWU6IG1vY2tTZXJ2ZXIub24oJ2Nvbm5lY3Rpb24nLCBmdW5jdGlvbigpIHsgY29uc29sZS5sb2coJ2EgbW9jayBjbGllbnQgY29ubmVjdGVkJyk7IH0pO1xuICAqXG4gICogQHBhcmFtIHtzdHJpbmd9IHR5cGUgLSBUaGUgZXZlbnQga2V5IHRvIHN1YnNjcmliZSB0by4gVmFsaWQga2V5cyBhcmU6IGNvbm5lY3Rpb24sIG1lc3NhZ2UsIGFuZCBjbG9zZS5cbiAgKiBAcGFyYW0ge2Z1bmN0aW9ufSBjYWxsYmFjayAtIFRoZSBjYWxsYmFjayB3aGljaCBzaG91bGQgYmUgY2FsbGVkIHdoZW4gYSBjZXJ0YWluIGV2ZW50IGlzIGZpcmVkLlxuICAqL1xuXG4gIF9jcmVhdGVDbGFzcyhTZXJ2ZXIsIFt7XG4gICAga2V5OiAnb24nLFxuICAgIHZhbHVlOiBmdW5jdGlvbiBvbih0eXBlLCBjYWxsYmFjaykge1xuICAgICAgdGhpcy5hZGRFdmVudExpc3RlbmVyKHR5cGUsIGNhbGxiYWNrKTtcbiAgICB9XG5cbiAgICAvKlxuICAgICogVGhpcyBzZW5kIGZ1bmN0aW9uIHdpbGwgbm90aWZ5IGFsbCBtb2NrIGNsaWVudHMgdmlhIHRoZWlyIG9ubWVzc2FnZSBjYWxsYmFja3MgdGhhdCB0aGUgc2VydmVyXG4gICAgKiBoYXMgYSBtZXNzYWdlIGZvciB0aGVtLlxuICAgICpcbiAgICAqIEBwYXJhbSB7Kn0gZGF0YSAtIEFueSBqYXZhc2NyaXB0IG9iamVjdCB3aGljaCB3aWxsIGJlIGNyYWZ0ZWQgaW50byBhIE1lc3NhZ2VPYmplY3QuXG4gICAgKi9cbiAgfSwge1xuICAgIGtleTogJ3NlbmQnLFxuICAgIHZhbHVlOiBmdW5jdGlvbiBzZW5kKGRhdGEpIHtcbiAgICAgIHZhciBvcHRpb25zID0gYXJndW1lbnRzLmxlbmd0aCA8PSAxIHx8IGFyZ3VtZW50c1sxXSA9PT0gdW5kZWZpbmVkID8ge30gOiBhcmd1bWVudHNbMV07XG5cbiAgICAgIHRoaXMuZW1pdCgnbWVzc2FnZScsIGRhdGEsIG9wdGlvbnMpO1xuICAgIH1cblxuICAgIC8qXG4gICAgKiBTZW5kcyBhIGdlbmVyaWMgbWVzc2FnZSBldmVudCB0byBhbGwgbW9jayBjbGllbnRzLlxuICAgICovXG4gIH0sIHtcbiAgICBrZXk6ICdlbWl0JyxcbiAgICB2YWx1ZTogZnVuY3Rpb24gZW1pdChldmVudCwgZGF0YSkge1xuICAgICAgdmFyIF90aGlzMiA9IHRoaXM7XG5cbiAgICAgIHZhciBvcHRpb25zID0gYXJndW1lbnRzLmxlbmd0aCA8PSAyIHx8IGFyZ3VtZW50c1syXSA9PT0gdW5kZWZpbmVkID8ge30gOiBhcmd1bWVudHNbMl07XG4gICAgICB2YXIgd2Vic29ja2V0cyA9IG9wdGlvbnMud2Vic29ja2V0cztcblxuICAgICAgaWYgKCF3ZWJzb2NrZXRzKSB7XG4gICAgICAgIHdlYnNvY2tldHMgPSBfbmV0d29ya0JyaWRnZTJbJ2RlZmF1bHQnXS53ZWJzb2NrZXRzTG9va3VwKHRoaXMudXJsKTtcbiAgICAgIH1cblxuICAgICAgd2Vic29ja2V0cy5mb3JFYWNoKGZ1bmN0aW9uIChzb2NrZXQpIHtcbiAgICAgICAgc29ja2V0LmRpc3BhdGNoRXZlbnQoKDAsIF9ldmVudEZhY3RvcnkuY3JlYXRlTWVzc2FnZUV2ZW50KSh7XG4gICAgICAgICAgdHlwZTogZXZlbnQsXG4gICAgICAgICAgZGF0YTogZGF0YSxcbiAgICAgICAgICBvcmlnaW46IF90aGlzMi51cmwsXG4gICAgICAgICAgdGFyZ2V0OiBzb2NrZXRcbiAgICAgICAgfSkpO1xuICAgICAgfSk7XG4gICAgfVxuXG4gICAgLypcbiAgICAqIENsb3NlcyB0aGUgY29ubmVjdGlvbiBhbmQgdHJpZ2dlcnMgdGhlIG9uY2xvc2UgbWV0aG9kIG9mIGFsbCBsaXN0ZW5pbmdcbiAgICAqIHdlYnNvY2tldHMuIEFmdGVyIHRoYXQgaXQgcmVtb3ZlcyBpdHNlbGYgZnJvbSB0aGUgdXJsTWFwIHNvIGFub3RoZXIgc2VydmVyXG4gICAgKiBjb3VsZCBhZGQgaXRzZWxmIHRvIHRoZSB1cmwuXG4gICAgKlxuICAgICogQHBhcmFtIHtvYmplY3R9IG9wdGlvbnNcbiAgICAqL1xuICB9LCB7XG4gICAga2V5OiAnY2xvc2UnLFxuICAgIHZhbHVlOiBmdW5jdGlvbiBjbG9zZSgpIHtcbiAgICAgIHZhciBvcHRpb25zID0gYXJndW1lbnRzLmxlbmd0aCA8PSAwIHx8IGFyZ3VtZW50c1swXSA9PT0gdW5kZWZpbmVkID8ge30gOiBhcmd1bWVudHNbMF07XG4gICAgICB2YXIgY29kZSA9IG9wdGlvbnMuY29kZTtcbiAgICAgIHZhciByZWFzb24gPSBvcHRpb25zLnJlYXNvbjtcbiAgICAgIHZhciB3YXNDbGVhbiA9IG9wdGlvbnMud2FzQ2xlYW47XG5cbiAgICAgIHZhciBsaXN0ZW5lcnMgPSBfbmV0d29ya0JyaWRnZTJbJ2RlZmF1bHQnXS53ZWJzb2NrZXRzTG9va3VwKHRoaXMudXJsKTtcblxuICAgICAgbGlzdGVuZXJzLmZvckVhY2goZnVuY3Rpb24gKHNvY2tldCkge1xuICAgICAgICBzb2NrZXQucmVhZHlTdGF0ZSA9IF93ZWJzb2NrZXQyWydkZWZhdWx0J10uQ0xPU0U7XG4gICAgICAgIHNvY2tldC5kaXNwYXRjaEV2ZW50KCgwLCBfZXZlbnRGYWN0b3J5LmNyZWF0ZUNsb3NlRXZlbnQpKHtcbiAgICAgICAgICB0eXBlOiAnY2xvc2UnLFxuICAgICAgICAgIHRhcmdldDogc29ja2V0LFxuICAgICAgICAgIGNvZGU6IGNvZGUgfHwgX2hlbHBlcnNDbG9zZUNvZGVzMlsnZGVmYXVsdCddLkNMT1NFX05PUk1BTCxcbiAgICAgICAgICByZWFzb246IHJlYXNvbiB8fCAnJyxcbiAgICAgICAgICB3YXNDbGVhbjogd2FzQ2xlYW5cbiAgICAgICAgfSkpO1xuICAgICAgfSk7XG5cbiAgICAgIHRoaXMuZGlzcGF0Y2hFdmVudCgoMCwgX2V2ZW50RmFjdG9yeS5jcmVhdGVDbG9zZUV2ZW50KSh7IHR5cGU6ICdjbG9zZScgfSksIHRoaXMpO1xuICAgICAgX25ldHdvcmtCcmlkZ2UyWydkZWZhdWx0J10ucmVtb3ZlU2VydmVyKHRoaXMudXJsKTtcbiAgICB9XG5cbiAgICAvKlxuICAgICogUmV0dXJucyBhbiBhcnJheSBvZiB3ZWJzb2NrZXRzIHdoaWNoIGFyZSBsaXN0ZW5pbmcgdG8gdGhpcyBzZXJ2ZXJcbiAgICAqL1xuICB9LCB7XG4gICAga2V5OiAnY2xpZW50cycsXG4gICAgdmFsdWU6IGZ1bmN0aW9uIGNsaWVudHMoKSB7XG4gICAgICByZXR1cm4gX25ldHdvcmtCcmlkZ2UyWydkZWZhdWx0J10ud2Vic29ja2V0c0xvb2t1cCh0aGlzLnVybCk7XG4gICAgfVxuXG4gICAgLypcbiAgICAqIFByZXBhcmVzIGEgbWV0aG9kIHRvIHN1Ym1pdCBhbiBldmVudCB0byBtZW1iZXJzIG9mIHRoZSByb29tXG4gICAgKlxuICAgICogZS5nLiBzZXJ2ZXIudG8oJ215LXJvb20nKS5lbWl0KCdoaSEnKTtcbiAgICAqL1xuICB9LCB7XG4gICAga2V5OiAndG8nLFxuICAgIHZhbHVlOiBmdW5jdGlvbiB0byhyb29tKSB7XG4gICAgICB2YXIgX3RoaXMgPSB0aGlzO1xuICAgICAgdmFyIHdlYnNvY2tldHMgPSBfbmV0d29ya0JyaWRnZTJbJ2RlZmF1bHQnXS53ZWJzb2NrZXRzTG9va3VwKHRoaXMudXJsLCByb29tKTtcbiAgICAgIHJldHVybiB7XG4gICAgICAgIGVtaXQ6IGZ1bmN0aW9uIGVtaXQoZXZlbnQsIGRhdGEpIHtcbiAgICAgICAgICBfdGhpcy5lbWl0KGV2ZW50LCBkYXRhLCB7IHdlYnNvY2tldHM6IHdlYnNvY2tldHMgfSk7XG4gICAgICAgIH1cbiAgICAgIH07XG4gICAgfVxuICB9XSk7XG5cbiAgcmV0dXJuIFNlcnZlcjtcbn0pKF9ldmVudFRhcmdldDJbJ2RlZmF1bHQnXSk7XG5cblNlcnZlci5vZiA9IGZ1bmN0aW9uIG9mKHVybCkge1xuICByZXR1cm4gbmV3IFNlcnZlcih1cmwpO1xufTtcblxuZXhwb3J0c1snZGVmYXVsdCddID0gU2VydmVyO1xubW9kdWxlLmV4cG9ydHMgPSBleHBvcnRzWydkZWZhdWx0J107IiwiJ3VzZSBzdHJpY3QnO1xuXG5PYmplY3QuZGVmaW5lUHJvcGVydHkoZXhwb3J0cywgJ19fZXNNb2R1bGUnLCB7XG4gIHZhbHVlOiB0cnVlXG59KTtcblxudmFyIF9jcmVhdGVDbGFzcyA9IChmdW5jdGlvbiAoKSB7IGZ1bmN0aW9uIGRlZmluZVByb3BlcnRpZXModGFyZ2V0LCBwcm9wcykgeyBmb3IgKHZhciBpID0gMDsgaSA8IHByb3BzLmxlbmd0aDsgaSsrKSB7IHZhciBkZXNjcmlwdG9yID0gcHJvcHNbaV07IGRlc2NyaXB0b3IuZW51bWVyYWJsZSA9IGRlc2NyaXB0b3IuZW51bWVyYWJsZSB8fCBmYWxzZTsgZGVzY3JpcHRvci5jb25maWd1cmFibGUgPSB0cnVlOyBpZiAoJ3ZhbHVlJyBpbiBkZXNjcmlwdG9yKSBkZXNjcmlwdG9yLndyaXRhYmxlID0gdHJ1ZTsgT2JqZWN0LmRlZmluZVByb3BlcnR5KHRhcmdldCwgZGVzY3JpcHRvci5rZXksIGRlc2NyaXB0b3IpOyB9IH0gcmV0dXJuIGZ1bmN0aW9uIChDb25zdHJ1Y3RvciwgcHJvdG9Qcm9wcywgc3RhdGljUHJvcHMpIHsgaWYgKHByb3RvUHJvcHMpIGRlZmluZVByb3BlcnRpZXMoQ29uc3RydWN0b3IucHJvdG90eXBlLCBwcm90b1Byb3BzKTsgaWYgKHN0YXRpY1Byb3BzKSBkZWZpbmVQcm9wZXJ0aWVzKENvbnN0cnVjdG9yLCBzdGF0aWNQcm9wcyk7IHJldHVybiBDb25zdHJ1Y3RvcjsgfTsgfSkoKTtcblxudmFyIF9nZXQgPSBmdW5jdGlvbiBnZXQoX3gzLCBfeDQsIF94NSkgeyB2YXIgX2FnYWluID0gdHJ1ZTsgX2Z1bmN0aW9uOiB3aGlsZSAoX2FnYWluKSB7IHZhciBvYmplY3QgPSBfeDMsIHByb3BlcnR5ID0gX3g0LCByZWNlaXZlciA9IF94NTsgX2FnYWluID0gZmFsc2U7IGlmIChvYmplY3QgPT09IG51bGwpIG9iamVjdCA9IEZ1bmN0aW9uLnByb3RvdHlwZTsgdmFyIGRlc2MgPSBPYmplY3QuZ2V0T3duUHJvcGVydHlEZXNjcmlwdG9yKG9iamVjdCwgcHJvcGVydHkpOyBpZiAoZGVzYyA9PT0gdW5kZWZpbmVkKSB7IHZhciBwYXJlbnQgPSBPYmplY3QuZ2V0UHJvdG90eXBlT2Yob2JqZWN0KTsgaWYgKHBhcmVudCA9PT0gbnVsbCkgeyByZXR1cm4gdW5kZWZpbmVkOyB9IGVsc2UgeyBfeDMgPSBwYXJlbnQ7IF94NCA9IHByb3BlcnR5OyBfeDUgPSByZWNlaXZlcjsgX2FnYWluID0gdHJ1ZTsgZGVzYyA9IHBhcmVudCA9IHVuZGVmaW5lZDsgY29udGludWUgX2Z1bmN0aW9uOyB9IH0gZWxzZSBpZiAoJ3ZhbHVlJyBpbiBkZXNjKSB7IHJldHVybiBkZXNjLnZhbHVlOyB9IGVsc2UgeyB2YXIgZ2V0dGVyID0gZGVzYy5nZXQ7IGlmIChnZXR0ZXIgPT09IHVuZGVmaW5lZCkgeyByZXR1cm4gdW5kZWZpbmVkOyB9IHJldHVybiBnZXR0ZXIuY2FsbChyZWNlaXZlcik7IH0gfSB9O1xuXG5mdW5jdGlvbiBfaW50ZXJvcFJlcXVpcmVEZWZhdWx0KG9iaikgeyByZXR1cm4gb2JqICYmIG9iai5fX2VzTW9kdWxlID8gb2JqIDogeyAnZGVmYXVsdCc6IG9iaiB9OyB9XG5cbmZ1bmN0aW9uIF9jbGFzc0NhbGxDaGVjayhpbnN0YW5jZSwgQ29uc3RydWN0b3IpIHsgaWYgKCEoaW5zdGFuY2UgaW5zdGFuY2VvZiBDb25zdHJ1Y3RvcikpIHsgdGhyb3cgbmV3IFR5cGVFcnJvcignQ2Fubm90IGNhbGwgYSBjbGFzcyBhcyBhIGZ1bmN0aW9uJyk7IH0gfVxuXG5mdW5jdGlvbiBfaW5oZXJpdHMoc3ViQ2xhc3MsIHN1cGVyQ2xhc3MpIHsgaWYgKHR5cGVvZiBzdXBlckNsYXNzICE9PSAnZnVuY3Rpb24nICYmIHN1cGVyQ2xhc3MgIT09IG51bGwpIHsgdGhyb3cgbmV3IFR5cGVFcnJvcignU3VwZXIgZXhwcmVzc2lvbiBtdXN0IGVpdGhlciBiZSBudWxsIG9yIGEgZnVuY3Rpb24sIG5vdCAnICsgdHlwZW9mIHN1cGVyQ2xhc3MpOyB9IHN1YkNsYXNzLnByb3RvdHlwZSA9IE9iamVjdC5jcmVhdGUoc3VwZXJDbGFzcyAmJiBzdXBlckNsYXNzLnByb3RvdHlwZSwgeyBjb25zdHJ1Y3RvcjogeyB2YWx1ZTogc3ViQ2xhc3MsIGVudW1lcmFibGU6IGZhbHNlLCB3cml0YWJsZTogdHJ1ZSwgY29uZmlndXJhYmxlOiB0cnVlIH0gfSk7IGlmIChzdXBlckNsYXNzKSBPYmplY3Quc2V0UHJvdG90eXBlT2YgPyBPYmplY3Quc2V0UHJvdG90eXBlT2Yoc3ViQ2xhc3MsIHN1cGVyQ2xhc3MpIDogc3ViQ2xhc3MuX19wcm90b19fID0gc3VwZXJDbGFzczsgfVxuXG52YXIgX3VyaWpzID0gcmVxdWlyZSgndXJpanMnKTtcblxudmFyIF91cmlqczIgPSBfaW50ZXJvcFJlcXVpcmVEZWZhdWx0KF91cmlqcyk7XG5cbnZhciBfaGVscGVyc0RlbGF5ID0gcmVxdWlyZSgnLi9oZWxwZXJzL2RlbGF5Jyk7XG5cbnZhciBfaGVscGVyc0RlbGF5MiA9IF9pbnRlcm9wUmVxdWlyZURlZmF1bHQoX2hlbHBlcnNEZWxheSk7XG5cbnZhciBfZXZlbnRUYXJnZXQgPSByZXF1aXJlKCcuL2V2ZW50LXRhcmdldCcpO1xuXG52YXIgX2V2ZW50VGFyZ2V0MiA9IF9pbnRlcm9wUmVxdWlyZURlZmF1bHQoX2V2ZW50VGFyZ2V0KTtcblxudmFyIF9uZXR3b3JrQnJpZGdlID0gcmVxdWlyZSgnLi9uZXR3b3JrLWJyaWRnZScpO1xuXG52YXIgX25ldHdvcmtCcmlkZ2UyID0gX2ludGVyb3BSZXF1aXJlRGVmYXVsdChfbmV0d29ya0JyaWRnZSk7XG5cbnZhciBfaGVscGVyc0Nsb3NlQ29kZXMgPSByZXF1aXJlKCcuL2hlbHBlcnMvY2xvc2UtY29kZXMnKTtcblxudmFyIF9oZWxwZXJzQ2xvc2VDb2RlczIgPSBfaW50ZXJvcFJlcXVpcmVEZWZhdWx0KF9oZWxwZXJzQ2xvc2VDb2Rlcyk7XG5cbnZhciBfZXZlbnRGYWN0b3J5ID0gcmVxdWlyZSgnLi9ldmVudC1mYWN0b3J5Jyk7XG5cbi8qXG4qIFRoZSBzb2NrZXQtaW8gY2xhc3MgaXMgZGVzaWduZWQgdG8gbWltaWNrIHRoZSByZWFsIEFQSSBhcyBjbG9zZWx5IGFzIHBvc3NpYmxlLlxuKlxuKiBodHRwOi8vc29ja2V0LmlvL2RvY3MvXG4qL1xuXG52YXIgU29ja2V0SU8gPSAoZnVuY3Rpb24gKF9FdmVudFRhcmdldCkge1xuICBfaW5oZXJpdHMoU29ja2V0SU8sIF9FdmVudFRhcmdldCk7XG5cbiAgLypcbiAgKiBAcGFyYW0ge3N0cmluZ30gdXJsXG4gICovXG5cbiAgZnVuY3Rpb24gU29ja2V0SU8oKSB7XG4gICAgdmFyIF90aGlzID0gdGhpcztcblxuICAgIHZhciB1cmwgPSBhcmd1bWVudHMubGVuZ3RoIDw9IDAgfHwgYXJndW1lbnRzWzBdID09PSB1bmRlZmluZWQgPyAnc29ja2V0LmlvJyA6IGFyZ3VtZW50c1swXTtcbiAgICB2YXIgcHJvdG9jb2wgPSBhcmd1bWVudHMubGVuZ3RoIDw9IDEgfHwgYXJndW1lbnRzWzFdID09PSB1bmRlZmluZWQgPyAnJyA6IGFyZ3VtZW50c1sxXTtcblxuICAgIF9jbGFzc0NhbGxDaGVjayh0aGlzLCBTb2NrZXRJTyk7XG5cbiAgICBfZ2V0KE9iamVjdC5nZXRQcm90b3R5cGVPZihTb2NrZXRJTy5wcm90b3R5cGUpLCAnY29uc3RydWN0b3InLCB0aGlzKS5jYWxsKHRoaXMpO1xuXG4gICAgdGhpcy5iaW5hcnlUeXBlID0gJ2Jsb2InO1xuICAgIHRoaXMudXJsID0gKDAsIF91cmlqczJbJ2RlZmF1bHQnXSkodXJsKS50b1N0cmluZygpO1xuICAgIHRoaXMucmVhZHlTdGF0ZSA9IFNvY2tldElPLkNPTk5FQ1RJTkc7XG4gICAgdGhpcy5wcm90b2NvbCA9ICcnO1xuXG4gICAgaWYgKHR5cGVvZiBwcm90b2NvbCA9PT0gJ3N0cmluZycpIHtcbiAgICAgIHRoaXMucHJvdG9jb2wgPSBwcm90b2NvbDtcbiAgICB9IGVsc2UgaWYgKEFycmF5LmlzQXJyYXkocHJvdG9jb2wpICYmIHByb3RvY29sLmxlbmd0aCA+IDApIHtcbiAgICAgIHRoaXMucHJvdG9jb2wgPSBwcm90b2NvbFswXTtcbiAgICB9XG5cbiAgICB2YXIgc2VydmVyID0gX25ldHdvcmtCcmlkZ2UyWydkZWZhdWx0J10uYXR0YWNoV2ViU29ja2V0KHRoaXMsIHRoaXMudXJsKTtcblxuICAgIC8qXG4gICAgKiBEZWxheSB0cmlnZ2VyaW5nIHRoZSBjb25uZWN0aW9uIGV2ZW50cyBzbyB0aGV5IGNhbiBiZSBkZWZpbmVkIGluIHRpbWUuXG4gICAgKi9cbiAgICAoMCwgX2hlbHBlcnNEZWxheTJbJ2RlZmF1bHQnXSkoZnVuY3Rpb24gZGVsYXlDYWxsYmFjaygpIHtcbiAgICAgIGlmIChzZXJ2ZXIpIHtcbiAgICAgICAgdGhpcy5yZWFkeVN0YXRlID0gU29ja2V0SU8uT1BFTjtcbiAgICAgICAgc2VydmVyLmRpc3BhdGNoRXZlbnQoKDAsIF9ldmVudEZhY3RvcnkuY3JlYXRlRXZlbnQpKHsgdHlwZTogJ2Nvbm5lY3Rpb24nIH0pLCBzZXJ2ZXIsIHRoaXMpO1xuICAgICAgICBzZXJ2ZXIuZGlzcGF0Y2hFdmVudCgoMCwgX2V2ZW50RmFjdG9yeS5jcmVhdGVFdmVudCkoeyB0eXBlOiAnY29ubmVjdCcgfSksIHNlcnZlciwgdGhpcyk7IC8vIGFsaWFzXG4gICAgICAgIHRoaXMuZGlzcGF0Y2hFdmVudCgoMCwgX2V2ZW50RmFjdG9yeS5jcmVhdGVFdmVudCkoeyB0eXBlOiAnY29ubmVjdCcsIHRhcmdldDogdGhpcyB9KSk7XG4gICAgICB9IGVsc2Uge1xuICAgICAgICB0aGlzLnJlYWR5U3RhdGUgPSBTb2NrZXRJTy5DTE9TRUQ7XG4gICAgICAgIHRoaXMuZGlzcGF0Y2hFdmVudCgoMCwgX2V2ZW50RmFjdG9yeS5jcmVhdGVFdmVudCkoeyB0eXBlOiAnZXJyb3InLCB0YXJnZXQ6IHRoaXMgfSkpO1xuICAgICAgICB0aGlzLmRpc3BhdGNoRXZlbnQoKDAsIF9ldmVudEZhY3RvcnkuY3JlYXRlQ2xvc2VFdmVudCkoe1xuICAgICAgICAgIHR5cGU6ICdjbG9zZScsXG4gICAgICAgICAgdGFyZ2V0OiB0aGlzLFxuICAgICAgICAgIGNvZGU6IF9oZWxwZXJzQ2xvc2VDb2RlczJbJ2RlZmF1bHQnXS5DTE9TRV9OT1JNQUxcbiAgICAgICAgfSkpO1xuXG4gICAgICAgIGNvbnNvbGUuZXJyb3IoJ1NvY2tldC5pbyBjb25uZWN0aW9uIHRvIFxcJycgKyB0aGlzLnVybCArICdcXCcgZmFpbGVkJyk7XG4gICAgICB9XG4gICAgfSwgdGhpcyk7XG5cbiAgICAvKipcbiAgICAgIEFkZCBhbiBhbGlhc2VkIGV2ZW50IGxpc3RlbmVyIGZvciBjbG9zZSAvIGRpc2Nvbm5lY3RcbiAgICAgKi9cbiAgICB0aGlzLmFkZEV2ZW50TGlzdGVuZXIoJ2Nsb3NlJywgZnVuY3Rpb24gKGV2ZW50KSB7XG4gICAgICBfdGhpcy5kaXNwYXRjaEV2ZW50KCgwLCBfZXZlbnRGYWN0b3J5LmNyZWF0ZUNsb3NlRXZlbnQpKHtcbiAgICAgICAgdHlwZTogJ2Rpc2Nvbm5lY3QnLFxuICAgICAgICB0YXJnZXQ6IGV2ZW50LnRhcmdldCxcbiAgICAgICAgY29kZTogZXZlbnQuY29kZVxuICAgICAgfSkpO1xuICAgIH0pO1xuICB9XG5cbiAgLypcbiAgKiBDbG9zZXMgdGhlIFNvY2tldElPIGNvbm5lY3Rpb24gb3IgY29ubmVjdGlvbiBhdHRlbXB0LCBpZiBhbnkuXG4gICogSWYgdGhlIGNvbm5lY3Rpb24gaXMgYWxyZWFkeSBDTE9TRUQsIHRoaXMgbWV0aG9kIGRvZXMgbm90aGluZy5cbiAgKi9cblxuICBfY3JlYXRlQ2xhc3MoU29ja2V0SU8sIFt7XG4gICAga2V5OiAnY2xvc2UnLFxuICAgIHZhbHVlOiBmdW5jdGlvbiBjbG9zZSgpIHtcbiAgICAgIGlmICh0aGlzLnJlYWR5U3RhdGUgIT09IFNvY2tldElPLk9QRU4pIHtcbiAgICAgICAgcmV0dXJuIHVuZGVmaW5lZDtcbiAgICAgIH1cblxuICAgICAgdmFyIHNlcnZlciA9IF9uZXR3b3JrQnJpZGdlMlsnZGVmYXVsdCddLnNlcnZlckxvb2t1cCh0aGlzLnVybCk7XG4gICAgICBfbmV0d29ya0JyaWRnZTJbJ2RlZmF1bHQnXS5yZW1vdmVXZWJTb2NrZXQodGhpcywgdGhpcy51cmwpO1xuXG4gICAgICB0aGlzLnJlYWR5U3RhdGUgPSBTb2NrZXRJTy5DTE9TRUQ7XG4gICAgICB0aGlzLmRpc3BhdGNoRXZlbnQoKDAsIF9ldmVudEZhY3RvcnkuY3JlYXRlQ2xvc2VFdmVudCkoe1xuICAgICAgICB0eXBlOiAnY2xvc2UnLFxuICAgICAgICB0YXJnZXQ6IHRoaXMsXG4gICAgICAgIGNvZGU6IF9oZWxwZXJzQ2xvc2VDb2RlczJbJ2RlZmF1bHQnXS5DTE9TRV9OT1JNQUxcbiAgICAgIH0pKTtcblxuICAgICAgaWYgKHNlcnZlcikge1xuICAgICAgICBzZXJ2ZXIuZGlzcGF0Y2hFdmVudCgoMCwgX2V2ZW50RmFjdG9yeS5jcmVhdGVDbG9zZUV2ZW50KSh7XG4gICAgICAgICAgdHlwZTogJ2Rpc2Nvbm5lY3QnLFxuICAgICAgICAgIHRhcmdldDogdGhpcyxcbiAgICAgICAgICBjb2RlOiBfaGVscGVyc0Nsb3NlQ29kZXMyWydkZWZhdWx0J10uQ0xPU0VfTk9STUFMXG4gICAgICAgIH0pLCBzZXJ2ZXIpO1xuICAgICAgfVxuICAgIH1cblxuICAgIC8qXG4gICAgKiBBbGlhcyBmb3IgU29ja2V0I2Nsb3NlXG4gICAgKlxuICAgICogaHR0cHM6Ly9naXRodWIuY29tL3NvY2tldGlvL3NvY2tldC5pby1jbGllbnQvYmxvYi9tYXN0ZXIvbGliL3NvY2tldC5qcyNMMzgzXG4gICAgKi9cbiAgfSwge1xuICAgIGtleTogJ2Rpc2Nvbm5lY3QnLFxuICAgIHZhbHVlOiBmdW5jdGlvbiBkaXNjb25uZWN0KCkge1xuICAgICAgdGhpcy5jbG9zZSgpO1xuICAgIH1cblxuICAgIC8qXG4gICAgKiBTdWJtaXRzIGFuIGV2ZW50IHRvIHRoZSBzZXJ2ZXIgd2l0aCBhIHBheWxvYWRcbiAgICAqL1xuICB9LCB7XG4gICAga2V5OiAnZW1pdCcsXG4gICAgdmFsdWU6IGZ1bmN0aW9uIGVtaXQoZXZlbnQsIGRhdGEpIHtcbiAgICAgIGlmICh0aGlzLnJlYWR5U3RhdGUgIT09IFNvY2tldElPLk9QRU4pIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKCdTb2NrZXRJTyBpcyBhbHJlYWR5IGluIENMT1NJTkcgb3IgQ0xPU0VEIHN0YXRlJyk7XG4gICAgICB9XG5cbiAgICAgIHZhciBtZXNzYWdlRXZlbnQgPSAoMCwgX2V2ZW50RmFjdG9yeS5jcmVhdGVNZXNzYWdlRXZlbnQpKHtcbiAgICAgICAgdHlwZTogZXZlbnQsXG4gICAgICAgIG9yaWdpbjogdGhpcy51cmwsXG4gICAgICAgIGRhdGE6IGRhdGFcbiAgICAgIH0pO1xuXG4gICAgICB2YXIgc2VydmVyID0gX25ldHdvcmtCcmlkZ2UyWydkZWZhdWx0J10uc2VydmVyTG9va3VwKHRoaXMudXJsKTtcblxuICAgICAgaWYgKHNlcnZlcikge1xuICAgICAgICBzZXJ2ZXIuZGlzcGF0Y2hFdmVudChtZXNzYWdlRXZlbnQsIGRhdGEpO1xuICAgICAgfVxuICAgIH1cblxuICAgIC8qXG4gICAgKiBTdWJtaXRzIGEgJ21lc3NhZ2UnIGV2ZW50IHRvIHRoZSBzZXJ2ZXIuXG4gICAgKlxuICAgICogU2hvdWxkIGJlaGF2ZSBleGFjdGx5IGxpa2UgV2ViU29ja2V0I3NlbmRcbiAgICAqXG4gICAgKiBodHRwczovL2dpdGh1Yi5jb20vc29ja2V0aW8vc29ja2V0LmlvLWNsaWVudC9ibG9iL21hc3Rlci9saWIvc29ja2V0LmpzI0wxMTNcbiAgICAqL1xuICB9LCB7XG4gICAga2V5OiAnc2VuZCcsXG4gICAgdmFsdWU6IGZ1bmN0aW9uIHNlbmQoZGF0YSkge1xuICAgICAgdGhpcy5lbWl0KCdtZXNzYWdlJywgZGF0YSk7XG4gICAgfVxuXG4gICAgLypcbiAgICAqIEZvciByZWdpc3RlcmluZyBldmVudHMgdG8gYmUgcmVjZWl2ZWQgZnJvbSB0aGUgc2VydmVyXG4gICAgKi9cbiAgfSwge1xuICAgIGtleTogJ29uJyxcbiAgICB2YWx1ZTogZnVuY3Rpb24gb24odHlwZSwgY2FsbGJhY2spIHtcbiAgICAgIHRoaXMuYWRkRXZlbnRMaXN0ZW5lcih0eXBlLCBjYWxsYmFjayk7XG4gICAgfVxuXG4gICAgLypcbiAgICAgKiBKb2luIGEgcm9vbSBvbiBhIHNlcnZlclxuICAgICAqXG4gICAgICogaHR0cDovL3NvY2tldC5pby9kb2NzL3Jvb21zLWFuZC1uYW1lc3BhY2VzLyNqb2luaW5nLWFuZC1sZWF2aW5nXG4gICAgICovXG4gIH0sIHtcbiAgICBrZXk6ICdqb2luJyxcbiAgICB2YWx1ZTogZnVuY3Rpb24gam9pbihyb29tKSB7XG4gICAgICBfbmV0d29ya0JyaWRnZTJbJ2RlZmF1bHQnXS5hZGRNZW1iZXJzaGlwVG9Sb29tKHRoaXMsIHJvb20pO1xuICAgIH1cblxuICAgIC8qXG4gICAgICogR2V0IHRoZSB3ZWJzb2NrZXQgdG8gbGVhdmUgdGhlIHJvb21cbiAgICAgKlxuICAgICAqIGh0dHA6Ly9zb2NrZXQuaW8vZG9jcy9yb29tcy1hbmQtbmFtZXNwYWNlcy8jam9pbmluZy1hbmQtbGVhdmluZ1xuICAgICAqL1xuICB9LCB7XG4gICAga2V5OiAnbGVhdmUnLFxuICAgIHZhbHVlOiBmdW5jdGlvbiBsZWF2ZShyb29tKSB7XG4gICAgICBfbmV0d29ya0JyaWRnZTJbJ2RlZmF1bHQnXS5yZW1vdmVNZW1iZXJzaGlwRnJvbVJvb20odGhpcywgcm9vbSk7XG4gICAgfVxuXG4gICAgLypcbiAgICAgKiBJbnZva2VzIGFsbCBsaXN0ZW5lciBmdW5jdGlvbnMgdGhhdCBhcmUgbGlzdGVuaW5nIHRvIHRoZSBnaXZlbiBldmVudC50eXBlIHByb3BlcnR5LiBFYWNoXG4gICAgICogbGlzdGVuZXIgd2lsbCBiZSBwYXNzZWQgdGhlIGV2ZW50IGFzIHRoZSBmaXJzdCBhcmd1bWVudC5cbiAgICAgKlxuICAgICAqIEBwYXJhbSB7b2JqZWN0fSBldmVudCAtIGV2ZW50IG9iamVjdCB3aGljaCB3aWxsIGJlIHBhc3NlZCB0byBhbGwgbGlzdGVuZXJzIG9mIHRoZSBldmVudC50eXBlIHByb3BlcnR5XG4gICAgICovXG4gIH0sIHtcbiAgICBrZXk6ICdkaXNwYXRjaEV2ZW50JyxcbiAgICB2YWx1ZTogZnVuY3Rpb24gZGlzcGF0Y2hFdmVudChldmVudCkge1xuICAgICAgdmFyIF90aGlzMiA9IHRoaXM7XG5cbiAgICAgIGZvciAodmFyIF9sZW4gPSBhcmd1bWVudHMubGVuZ3RoLCBjdXN0b21Bcmd1bWVudHMgPSBBcnJheShfbGVuID4gMSA/IF9sZW4gLSAxIDogMCksIF9rZXkgPSAxOyBfa2V5IDwgX2xlbjsgX2tleSsrKSB7XG4gICAgICAgIGN1c3RvbUFyZ3VtZW50c1tfa2V5IC0gMV0gPSBhcmd1bWVudHNbX2tleV07XG4gICAgICB9XG5cbiAgICAgIHZhciBldmVudE5hbWUgPSBldmVudC50eXBlO1xuICAgICAgdmFyIGxpc3RlbmVycyA9IHRoaXMubGlzdGVuZXJzW2V2ZW50TmFtZV07XG5cbiAgICAgIGlmICghQXJyYXkuaXNBcnJheShsaXN0ZW5lcnMpKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICAgIH1cblxuICAgICAgbGlzdGVuZXJzLmZvckVhY2goZnVuY3Rpb24gKGxpc3RlbmVyKSB7XG4gICAgICAgIGlmIChjdXN0b21Bcmd1bWVudHMubGVuZ3RoID4gMCkge1xuICAgICAgICAgIGxpc3RlbmVyLmFwcGx5KF90aGlzMiwgY3VzdG9tQXJndW1lbnRzKTtcbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICAvLyBSZWd1bGFyIFdlYlNvY2tldHMgZXhwZWN0IGEgTWVzc2FnZUV2ZW50IGJ1dCBTb2NrZXRpby5pbyBqdXN0IHdhbnRzIHJhdyBkYXRhXG4gICAgICAgICAgLy8gIHBheWxvYWQgaW5zdGFuY2VvZiBNZXNzYWdlRXZlbnQgd29ya3MsIGJ1dCB5b3UgY2FuJ3QgaXNudGFuY2Ugb2YgTm9kZUV2ZW50XG4gICAgICAgICAgLy8gIGZvciBub3cgd2UgZGV0ZWN0IGlmIHRoZSBvdXRwdXQgaGFzIGRhdGEgZGVmaW5lZCBvbiBpdFxuICAgICAgICAgIGxpc3RlbmVyLmNhbGwoX3RoaXMyLCBldmVudC5kYXRhID8gZXZlbnQuZGF0YSA6IGV2ZW50KTtcbiAgICAgICAgfVxuICAgICAgfSk7XG4gICAgfVxuICB9XSk7XG5cbiAgcmV0dXJuIFNvY2tldElPO1xufSkoX2V2ZW50VGFyZ2V0MlsnZGVmYXVsdCddKTtcblxuU29ja2V0SU8uQ09OTkVDVElORyA9IDA7XG5Tb2NrZXRJTy5PUEVOID0gMTtcblNvY2tldElPLkNMT1NJTkcgPSAyO1xuU29ja2V0SU8uQ0xPU0VEID0gMztcblxuLypcbiogU3RhdGljIGNvbnN0cnVjdG9yIG1ldGhvZHMgZm9yIHRoZSBJTyBTb2NrZXRcbiovXG52YXIgSU8gPSBmdW5jdGlvbiBpb0NvbnN0cnVjdG9yKHVybCkge1xuICByZXR1cm4gbmV3IFNvY2tldElPKHVybCk7XG59O1xuXG4vKlxuKiBBbGlhcyB0aGUgcmF3IElPKCkgY29uc3RydWN0b3JcbiovXG5JTy5jb25uZWN0ID0gZnVuY3Rpb24gaW9Db25uZWN0KHVybCkge1xuICAvKiBlc2xpbnQtZGlzYWJsZSBuZXctY2FwICovXG4gIHJldHVybiBJTyh1cmwpO1xuICAvKiBlc2xpbnQtZW5hYmxlIG5ldy1jYXAgKi9cbn07XG5cbmV4cG9ydHNbJ2RlZmF1bHQnXSA9IElPO1xubW9kdWxlLmV4cG9ydHMgPSBleHBvcnRzWydkZWZhdWx0J107IiwiJ3VzZSBzdHJpY3QnO1xuXG5PYmplY3QuZGVmaW5lUHJvcGVydHkoZXhwb3J0cywgJ19fZXNNb2R1bGUnLCB7XG4gIHZhbHVlOiB0cnVlXG59KTtcblxudmFyIF9jcmVhdGVDbGFzcyA9IChmdW5jdGlvbiAoKSB7IGZ1bmN0aW9uIGRlZmluZVByb3BlcnRpZXModGFyZ2V0LCBwcm9wcykgeyBmb3IgKHZhciBpID0gMDsgaSA8IHByb3BzLmxlbmd0aDsgaSsrKSB7IHZhciBkZXNjcmlwdG9yID0gcHJvcHNbaV07IGRlc2NyaXB0b3IuZW51bWVyYWJsZSA9IGRlc2NyaXB0b3IuZW51bWVyYWJsZSB8fCBmYWxzZTsgZGVzY3JpcHRvci5jb25maWd1cmFibGUgPSB0cnVlOyBpZiAoJ3ZhbHVlJyBpbiBkZXNjcmlwdG9yKSBkZXNjcmlwdG9yLndyaXRhYmxlID0gdHJ1ZTsgT2JqZWN0LmRlZmluZVByb3BlcnR5KHRhcmdldCwgZGVzY3JpcHRvci5rZXksIGRlc2NyaXB0b3IpOyB9IH0gcmV0dXJuIGZ1bmN0aW9uIChDb25zdHJ1Y3RvciwgcHJvdG9Qcm9wcywgc3RhdGljUHJvcHMpIHsgaWYgKHByb3RvUHJvcHMpIGRlZmluZVByb3BlcnRpZXMoQ29uc3RydWN0b3IucHJvdG90eXBlLCBwcm90b1Byb3BzKTsgaWYgKHN0YXRpY1Byb3BzKSBkZWZpbmVQcm9wZXJ0aWVzKENvbnN0cnVjdG9yLCBzdGF0aWNQcm9wcyk7IHJldHVybiBDb25zdHJ1Y3RvcjsgfTsgfSkoKTtcblxudmFyIF9nZXQgPSBmdW5jdGlvbiBnZXQoX3gyLCBfeDMsIF94NCkgeyB2YXIgX2FnYWluID0gdHJ1ZTsgX2Z1bmN0aW9uOiB3aGlsZSAoX2FnYWluKSB7IHZhciBvYmplY3QgPSBfeDIsIHByb3BlcnR5ID0gX3gzLCByZWNlaXZlciA9IF94NDsgX2FnYWluID0gZmFsc2U7IGlmIChvYmplY3QgPT09IG51bGwpIG9iamVjdCA9IEZ1bmN0aW9uLnByb3RvdHlwZTsgdmFyIGRlc2MgPSBPYmplY3QuZ2V0T3duUHJvcGVydHlEZXNjcmlwdG9yKG9iamVjdCwgcHJvcGVydHkpOyBpZiAoZGVzYyA9PT0gdW5kZWZpbmVkKSB7IHZhciBwYXJlbnQgPSBPYmplY3QuZ2V0UHJvdG90eXBlT2Yob2JqZWN0KTsgaWYgKHBhcmVudCA9PT0gbnVsbCkgeyByZXR1cm4gdW5kZWZpbmVkOyB9IGVsc2UgeyBfeDIgPSBwYXJlbnQ7IF94MyA9IHByb3BlcnR5OyBfeDQgPSByZWNlaXZlcjsgX2FnYWluID0gdHJ1ZTsgZGVzYyA9IHBhcmVudCA9IHVuZGVmaW5lZDsgY29udGludWUgX2Z1bmN0aW9uOyB9IH0gZWxzZSBpZiAoJ3ZhbHVlJyBpbiBkZXNjKSB7IHJldHVybiBkZXNjLnZhbHVlOyB9IGVsc2UgeyB2YXIgZ2V0dGVyID0gZGVzYy5nZXQ7IGlmIChnZXR0ZXIgPT09IHVuZGVmaW5lZCkgeyByZXR1cm4gdW5kZWZpbmVkOyB9IHJldHVybiBnZXR0ZXIuY2FsbChyZWNlaXZlcik7IH0gfSB9O1xuXG5mdW5jdGlvbiBfaW50ZXJvcFJlcXVpcmVEZWZhdWx0KG9iaikgeyByZXR1cm4gb2JqICYmIG9iai5fX2VzTW9kdWxlID8gb2JqIDogeyAnZGVmYXVsdCc6IG9iaiB9OyB9XG5cbmZ1bmN0aW9uIF9jbGFzc0NhbGxDaGVjayhpbnN0YW5jZSwgQ29uc3RydWN0b3IpIHsgaWYgKCEoaW5zdGFuY2UgaW5zdGFuY2VvZiBDb25zdHJ1Y3RvcikpIHsgdGhyb3cgbmV3IFR5cGVFcnJvcignQ2Fubm90IGNhbGwgYSBjbGFzcyBhcyBhIGZ1bmN0aW9uJyk7IH0gfVxuXG5mdW5jdGlvbiBfaW5oZXJpdHMoc3ViQ2xhc3MsIHN1cGVyQ2xhc3MpIHsgaWYgKHR5cGVvZiBzdXBlckNsYXNzICE9PSAnZnVuY3Rpb24nICYmIHN1cGVyQ2xhc3MgIT09IG51bGwpIHsgdGhyb3cgbmV3IFR5cGVFcnJvcignU3VwZXIgZXhwcmVzc2lvbiBtdXN0IGVpdGhlciBiZSBudWxsIG9yIGEgZnVuY3Rpb24sIG5vdCAnICsgdHlwZW9mIHN1cGVyQ2xhc3MpOyB9IHN1YkNsYXNzLnByb3RvdHlwZSA9IE9iamVjdC5jcmVhdGUoc3VwZXJDbGFzcyAmJiBzdXBlckNsYXNzLnByb3RvdHlwZSwgeyBjb25zdHJ1Y3RvcjogeyB2YWx1ZTogc3ViQ2xhc3MsIGVudW1lcmFibGU6IGZhbHNlLCB3cml0YWJsZTogdHJ1ZSwgY29uZmlndXJhYmxlOiB0cnVlIH0gfSk7IGlmIChzdXBlckNsYXNzKSBPYmplY3Quc2V0UHJvdG90eXBlT2YgPyBPYmplY3Quc2V0UHJvdG90eXBlT2Yoc3ViQ2xhc3MsIHN1cGVyQ2xhc3MpIDogc3ViQ2xhc3MuX19wcm90b19fID0gc3VwZXJDbGFzczsgfVxuXG52YXIgX3VyaWpzID0gcmVxdWlyZSgndXJpanMnKTtcblxudmFyIF91cmlqczIgPSBfaW50ZXJvcFJlcXVpcmVEZWZhdWx0KF91cmlqcyk7XG5cbnZhciBfaGVscGVyc0RlbGF5ID0gcmVxdWlyZSgnLi9oZWxwZXJzL2RlbGF5Jyk7XG5cbnZhciBfaGVscGVyc0RlbGF5MiA9IF9pbnRlcm9wUmVxdWlyZURlZmF1bHQoX2hlbHBlcnNEZWxheSk7XG5cbnZhciBfZXZlbnRUYXJnZXQgPSByZXF1aXJlKCcuL2V2ZW50LXRhcmdldCcpO1xuXG52YXIgX2V2ZW50VGFyZ2V0MiA9IF9pbnRlcm9wUmVxdWlyZURlZmF1bHQoX2V2ZW50VGFyZ2V0KTtcblxudmFyIF9uZXR3b3JrQnJpZGdlID0gcmVxdWlyZSgnLi9uZXR3b3JrLWJyaWRnZScpO1xuXG52YXIgX25ldHdvcmtCcmlkZ2UyID0gX2ludGVyb3BSZXF1aXJlRGVmYXVsdChfbmV0d29ya0JyaWRnZSk7XG5cbnZhciBfaGVscGVyc0Nsb3NlQ29kZXMgPSByZXF1aXJlKCcuL2hlbHBlcnMvY2xvc2UtY29kZXMnKTtcblxudmFyIF9oZWxwZXJzQ2xvc2VDb2RlczIgPSBfaW50ZXJvcFJlcXVpcmVEZWZhdWx0KF9oZWxwZXJzQ2xvc2VDb2Rlcyk7XG5cbnZhciBfZXZlbnRGYWN0b3J5ID0gcmVxdWlyZSgnLi9ldmVudC1mYWN0b3J5Jyk7XG5cbi8qXG4qIFRoZSBtYWluIHdlYnNvY2tldCBjbGFzcyB3aGljaCBpcyBkZXNpZ25lZCB0byBtaW1pY2sgdGhlIG5hdGl2ZSBXZWJTb2NrZXQgY2xhc3MgYXMgY2xvc2VcbiogYXMgcG9zc2libGUuXG4qXG4qIGh0dHBzOi8vZGV2ZWxvcGVyLm1vemlsbGEub3JnL2VuLVVTL2RvY3MvV2ViL0FQSS9XZWJTb2NrZXRcbiovXG5cbnZhciBXZWJTb2NrZXQgPSAoZnVuY3Rpb24gKF9FdmVudFRhcmdldCkge1xuICBfaW5oZXJpdHMoV2ViU29ja2V0LCBfRXZlbnRUYXJnZXQpO1xuXG4gIC8qXG4gICogQHBhcmFtIHtzdHJpbmd9IHVybFxuICAqL1xuXG4gIGZ1bmN0aW9uIFdlYlNvY2tldCh1cmwpIHtcbiAgICB2YXIgcHJvdG9jb2wgPSBhcmd1bWVudHMubGVuZ3RoIDw9IDEgfHwgYXJndW1lbnRzWzFdID09PSB1bmRlZmluZWQgPyAnJyA6IGFyZ3VtZW50c1sxXTtcblxuICAgIF9jbGFzc0NhbGxDaGVjayh0aGlzLCBXZWJTb2NrZXQpO1xuXG4gICAgX2dldChPYmplY3QuZ2V0UHJvdG90eXBlT2YoV2ViU29ja2V0LnByb3RvdHlwZSksICdjb25zdHJ1Y3RvcicsIHRoaXMpLmNhbGwodGhpcyk7XG5cbiAgICBpZiAoIXVybCkge1xuICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcignRmFpbGVkIHRvIGNvbnN0cnVjdCBcXCdXZWJTb2NrZXRcXCc6IDEgYXJndW1lbnQgcmVxdWlyZWQsIGJ1dCBvbmx5IDAgcHJlc2VudC4nKTtcbiAgICB9XG5cbiAgICB0aGlzLmJpbmFyeVR5cGUgPSAnYmxvYic7XG4gICAgdGhpcy51cmwgPSAoMCwgX3VyaWpzMlsnZGVmYXVsdCddKSh1cmwpLnRvU3RyaW5nKCk7XG4gICAgdGhpcy5yZWFkeVN0YXRlID0gV2ViU29ja2V0LkNPTk5FQ1RJTkc7XG4gICAgdGhpcy5wcm90b2NvbCA9ICcnO1xuXG4gICAgaWYgKHR5cGVvZiBwcm90b2NvbCA9PT0gJ3N0cmluZycpIHtcbiAgICAgIHRoaXMucHJvdG9jb2wgPSBwcm90b2NvbDtcbiAgICB9IGVsc2UgaWYgKEFycmF5LmlzQXJyYXkocHJvdG9jb2wpICYmIHByb3RvY29sLmxlbmd0aCA+IDApIHtcbiAgICAgIHRoaXMucHJvdG9jb2wgPSBwcm90b2NvbFswXTtcbiAgICB9XG5cbiAgICAvKlxuICAgICogSW4gb3JkZXIgdG8gY2FwdHVyZSB0aGUgY2FsbGJhY2sgZnVuY3Rpb24gd2UgbmVlZCB0byBkZWZpbmUgY3VzdG9tIHNldHRlcnMuXG4gICAgKiBUbyBpbGx1c3RyYXRlOlxuICAgICogICBteVNvY2tldC5vbm9wZW4gPSBmdW5jdGlvbigpIHsgYWxlcnQodHJ1ZSkgfTtcbiAgICAqXG4gICAgKiBUaGUgb25seSB3YXkgdG8gY2FwdHVyZSB0aGF0IGZ1bmN0aW9uIGFuZCBob2xkIG9udG8gaXQgZm9yIGxhdGVyIGlzIHdpdGggdGhlXG4gICAgKiBiZWxvdyBjb2RlOlxuICAgICovXG4gICAgT2JqZWN0LmRlZmluZVByb3BlcnRpZXModGhpcywge1xuICAgICAgb25vcGVuOiB7XG4gICAgICAgIGNvbmZpZ3VyYWJsZTogdHJ1ZSxcbiAgICAgICAgZW51bWVyYWJsZTogdHJ1ZSxcbiAgICAgICAgZ2V0OiBmdW5jdGlvbiBnZXQoKSB7XG4gICAgICAgICAgcmV0dXJuIHRoaXMubGlzdGVuZXJzLm9wZW47XG4gICAgICAgIH0sXG4gICAgICAgIHNldDogZnVuY3Rpb24gc2V0KGxpc3RlbmVyKSB7XG4gICAgICAgICAgdGhpcy5hZGRFdmVudExpc3RlbmVyKCdvcGVuJywgbGlzdGVuZXIpO1xuICAgICAgICB9XG4gICAgICB9LFxuICAgICAgb25tZXNzYWdlOiB7XG4gICAgICAgIGNvbmZpZ3VyYWJsZTogdHJ1ZSxcbiAgICAgICAgZW51bWVyYWJsZTogdHJ1ZSxcbiAgICAgICAgZ2V0OiBmdW5jdGlvbiBnZXQoKSB7XG4gICAgICAgICAgcmV0dXJuIHRoaXMubGlzdGVuZXJzLm1lc3NhZ2U7XG4gICAgICAgIH0sXG4gICAgICAgIHNldDogZnVuY3Rpb24gc2V0KGxpc3RlbmVyKSB7XG4gICAgICAgICAgdGhpcy5hZGRFdmVudExpc3RlbmVyKCdtZXNzYWdlJywgbGlzdGVuZXIpO1xuICAgICAgICB9XG4gICAgICB9LFxuICAgICAgb25jbG9zZToge1xuICAgICAgICBjb25maWd1cmFibGU6IHRydWUsXG4gICAgICAgIGVudW1lcmFibGU6IHRydWUsXG4gICAgICAgIGdldDogZnVuY3Rpb24gZ2V0KCkge1xuICAgICAgICAgIHJldHVybiB0aGlzLmxpc3RlbmVycy5jbG9zZTtcbiAgICAgICAgfSxcbiAgICAgICAgc2V0OiBmdW5jdGlvbiBzZXQobGlzdGVuZXIpIHtcbiAgICAgICAgICB0aGlzLmFkZEV2ZW50TGlzdGVuZXIoJ2Nsb3NlJywgbGlzdGVuZXIpO1xuICAgICAgICB9XG4gICAgICB9LFxuICAgICAgb25lcnJvcjoge1xuICAgICAgICBjb25maWd1cmFibGU6IHRydWUsXG4gICAgICAgIGVudW1lcmFibGU6IHRydWUsXG4gICAgICAgIGdldDogZnVuY3Rpb24gZ2V0KCkge1xuICAgICAgICAgIHJldHVybiB0aGlzLmxpc3RlbmVycy5lcnJvcjtcbiAgICAgICAgfSxcbiAgICAgICAgc2V0OiBmdW5jdGlvbiBzZXQobGlzdGVuZXIpIHtcbiAgICAgICAgICB0aGlzLmFkZEV2ZW50TGlzdGVuZXIoJ2Vycm9yJywgbGlzdGVuZXIpO1xuICAgICAgICB9XG4gICAgICB9XG4gICAgfSk7XG5cbiAgICB2YXIgc2VydmVyID0gX25ldHdvcmtCcmlkZ2UyWydkZWZhdWx0J10uYXR0YWNoV2ViU29ja2V0KHRoaXMsIHRoaXMudXJsKTtcblxuICAgIC8qXG4gICAgKiBUaGlzIGRlbGF5IGlzIG5lZWRlZCBzbyB0aGF0IHdlIGRvbnQgdHJpZ2dlciBhbiBldmVudCBiZWZvcmUgdGhlIGNhbGxiYWNrcyBoYXZlIGJlZW5cbiAgICAqIHNldHVwLiBGb3IgZXhhbXBsZTpcbiAgICAqXG4gICAgKiB2YXIgc29ja2V0ID0gbmV3IFdlYlNvY2tldCgnd3M6Ly9sb2NhbGhvc3QnKTtcbiAgICAqXG4gICAgKiAvLyBJZiB3ZSBkb250IGhhdmUgdGhlIGRlbGF5IHRoZW4gdGhlIGV2ZW50IHdvdWxkIGJlIHRyaWdnZXJlZCByaWdodCBoZXJlIGFuZCB0aGlzIGlzXG4gICAgKiAvLyBiZWZvcmUgdGhlIG9ub3BlbiBoYWQgYSBjaGFuY2UgdG8gcmVnaXN0ZXIgaXRzZWxmLlxuICAgICpcbiAgICAqIHNvY2tldC5vbm9wZW4gPSAoKSA9PiB7IC8vIHRoaXMgd291bGQgbmV2ZXIgYmUgY2FsbGVkIH07XG4gICAgKlxuICAgICogLy8gYW5kIHdpdGggdGhlIGRlbGF5IHRoZSBldmVudCBnZXRzIHRyaWdnZXJlZCBoZXJlIGFmdGVyIGFsbCBvZiB0aGUgY2FsbGJhY2tzIGhhdmUgYmVlblxuICAgICogLy8gcmVnaXN0ZXJlZCA6LSlcbiAgICAqL1xuICAgICgwLCBfaGVscGVyc0RlbGF5MlsnZGVmYXVsdCddKShmdW5jdGlvbiBkZWxheUNhbGxiYWNrKCkge1xuICAgICAgaWYgKHNlcnZlcikge1xuICAgICAgICB0aGlzLnJlYWR5U3RhdGUgPSBXZWJTb2NrZXQuT1BFTjtcbiAgICAgICAgc2VydmVyLmRpc3BhdGNoRXZlbnQoKDAsIF9ldmVudEZhY3RvcnkuY3JlYXRlRXZlbnQpKHsgdHlwZTogJ2Nvbm5lY3Rpb24nIH0pLCBzZXJ2ZXIsIHRoaXMpO1xuICAgICAgICB0aGlzLmRpc3BhdGNoRXZlbnQoKDAsIF9ldmVudEZhY3RvcnkuY3JlYXRlRXZlbnQpKHsgdHlwZTogJ29wZW4nLCB0YXJnZXQ6IHRoaXMgfSkpO1xuICAgICAgfSBlbHNlIHtcbiAgICAgICAgdGhpcy5yZWFkeVN0YXRlID0gV2ViU29ja2V0LkNMT1NFRDtcbiAgICAgICAgdGhpcy5kaXNwYXRjaEV2ZW50KCgwLCBfZXZlbnRGYWN0b3J5LmNyZWF0ZUV2ZW50KSh7IHR5cGU6ICdlcnJvcicsIHRhcmdldDogdGhpcyB9KSk7XG4gICAgICAgIHRoaXMuZGlzcGF0Y2hFdmVudCgoMCwgX2V2ZW50RmFjdG9yeS5jcmVhdGVDbG9zZUV2ZW50KSh7IHR5cGU6ICdjbG9zZScsIHRhcmdldDogdGhpcywgY29kZTogX2hlbHBlcnNDbG9zZUNvZGVzMlsnZGVmYXVsdCddLkNMT1NFX05PUk1BTCB9KSk7XG5cbiAgICAgICAgY29uc29sZS5lcnJvcignV2ViU29ja2V0IGNvbm5lY3Rpb24gdG8gXFwnJyArIHRoaXMudXJsICsgJ1xcJyBmYWlsZWQnKTtcbiAgICAgIH1cbiAgICB9LCB0aGlzKTtcbiAgfVxuXG4gIC8qXG4gICogVHJhbnNtaXRzIGRhdGEgdG8gdGhlIHNlcnZlciBvdmVyIHRoZSBXZWJTb2NrZXQgY29ubmVjdGlvbi5cbiAgKlxuICAqIGh0dHBzOi8vZGV2ZWxvcGVyLm1vemlsbGEub3JnL2VuLVVTL2RvY3MvV2ViL0FQSS9XZWJTb2NrZXQjc2VuZCgpXG4gICovXG5cbiAgX2NyZWF0ZUNsYXNzKFdlYlNvY2tldCwgW3tcbiAgICBrZXk6ICdzZW5kJyxcbiAgICB2YWx1ZTogZnVuY3Rpb24gc2VuZChkYXRhKSB7XG4gICAgICBpZiAodGhpcy5yZWFkeVN0YXRlID09PSBXZWJTb2NrZXQuQ0xPU0lORyB8fCB0aGlzLnJlYWR5U3RhdGUgPT09IFdlYlNvY2tldC5DTE9TRUQpIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKCdXZWJTb2NrZXQgaXMgYWxyZWFkeSBpbiBDTE9TSU5HIG9yIENMT1NFRCBzdGF0ZScpO1xuICAgICAgfVxuXG4gICAgICB2YXIgbWVzc2FnZUV2ZW50ID0gKDAsIF9ldmVudEZhY3RvcnkuY3JlYXRlTWVzc2FnZUV2ZW50KSh7XG4gICAgICAgIHR5cGU6ICdtZXNzYWdlJyxcbiAgICAgICAgb3JpZ2luOiB0aGlzLnVybCxcbiAgICAgICAgZGF0YTogZGF0YVxuICAgICAgfSk7XG5cbiAgICAgIHZhciBzZXJ2ZXIgPSBfbmV0d29ya0JyaWRnZTJbJ2RlZmF1bHQnXS5zZXJ2ZXJMb29rdXAodGhpcy51cmwpO1xuXG4gICAgICBpZiAoc2VydmVyKSB7XG4gICAgICAgIHNlcnZlci5kaXNwYXRjaEV2ZW50KG1lc3NhZ2VFdmVudCwgZGF0YSk7XG4gICAgICB9XG4gICAgfVxuXG4gICAgLypcbiAgICAqIENsb3NlcyB0aGUgV2ViU29ja2V0IGNvbm5lY3Rpb24gb3IgY29ubmVjdGlvbiBhdHRlbXB0LCBpZiBhbnkuXG4gICAgKiBJZiB0aGUgY29ubmVjdGlvbiBpcyBhbHJlYWR5IENMT1NFRCwgdGhpcyBtZXRob2QgZG9lcyBub3RoaW5nLlxuICAgICpcbiAgICAqIGh0dHBzOi8vZGV2ZWxvcGVyLm1vemlsbGEub3JnL2VuLVVTL2RvY3MvV2ViL0FQSS9XZWJTb2NrZXQjY2xvc2UoKVxuICAgICovXG4gIH0sIHtcbiAgICBrZXk6ICdjbG9zZScsXG4gICAgdmFsdWU6IGZ1bmN0aW9uIGNsb3NlKCkge1xuICAgICAgaWYgKHRoaXMucmVhZHlTdGF0ZSAhPT0gV2ViU29ja2V0Lk9QRU4pIHtcbiAgICAgICAgcmV0dXJuIHVuZGVmaW5lZDtcbiAgICAgIH1cblxuICAgICAgdmFyIHNlcnZlciA9IF9uZXR3b3JrQnJpZGdlMlsnZGVmYXVsdCddLnNlcnZlckxvb2t1cCh0aGlzLnVybCk7XG4gICAgICB2YXIgY2xvc2VFdmVudCA9ICgwLCBfZXZlbnRGYWN0b3J5LmNyZWF0ZUNsb3NlRXZlbnQpKHtcbiAgICAgICAgdHlwZTogJ2Nsb3NlJyxcbiAgICAgICAgdGFyZ2V0OiB0aGlzLFxuICAgICAgICBjb2RlOiBfaGVscGVyc0Nsb3NlQ29kZXMyWydkZWZhdWx0J10uQ0xPU0VfTk9STUFMXG4gICAgICB9KTtcblxuICAgICAgX25ldHdvcmtCcmlkZ2UyWydkZWZhdWx0J10ucmVtb3ZlV2ViU29ja2V0KHRoaXMsIHRoaXMudXJsKTtcblxuICAgICAgdGhpcy5yZWFkeVN0YXRlID0gV2ViU29ja2V0LkNMT1NFRDtcbiAgICAgIHRoaXMuZGlzcGF0Y2hFdmVudChjbG9zZUV2ZW50KTtcblxuICAgICAgaWYgKHNlcnZlcikge1xuICAgICAgICBzZXJ2ZXIuZGlzcGF0Y2hFdmVudChjbG9zZUV2ZW50LCBzZXJ2ZXIpO1xuICAgICAgfVxuICAgIH1cbiAgfV0pO1xuXG4gIHJldHVybiBXZWJTb2NrZXQ7XG59KShfZXZlbnRUYXJnZXQyWydkZWZhdWx0J10pO1xuXG5XZWJTb2NrZXQuQ09OTkVDVElORyA9IDA7XG5XZWJTb2NrZXQuT1BFTiA9IDE7XG5XZWJTb2NrZXQuQ0xPU0lORyA9IDI7XG5XZWJTb2NrZXQuQ0xPU0VEID0gMztcblxuZXhwb3J0c1snZGVmYXVsdCddID0gV2ViU29ja2V0O1xubW9kdWxlLmV4cG9ydHMgPSBleHBvcnRzWydkZWZhdWx0J107Il19
diff --git a/actioncable/test/server/base_test.rb b/actioncable/test/server/base_test.rb
new file mode 100644
index 0000000000..d46debea45
--- /dev/null
+++ b/actioncable/test/server/base_test.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "stubs/test_server"
+require "active_support/core_ext/hash/indifferent_access"
+
+class BaseTest < ActionCable::TestCase
+ def setup
+ @server = ActionCable::Server::Base.new
+ @server.config.cable = { adapter: "async" }.with_indifferent_access
+ end
+
+ class FakeConnection
+ def close
+ end
+ end
+
+ test "#restart closes all open connections" do
+ conn = FakeConnection.new
+ @server.add_connection(conn)
+
+ assert_called(conn, :close) do
+ @server.restart
+ end
+ end
+
+ test "#restart shuts down worker pool" do
+ assert_called(@server.worker_pool, :halt) do
+ @server.restart
+ end
+ end
+
+ test "#restart shuts down pub/sub adapter" do
+ assert_called(@server.pubsub, :shutdown) do
+ @server.restart
+ end
+ end
+end
diff --git a/actioncable/test/server/broadcasting_test.rb b/actioncable/test/server/broadcasting_test.rb
new file mode 100644
index 0000000000..03c900182a
--- /dev/null
+++ b/actioncable/test/server/broadcasting_test.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "stubs/test_server"
+
+class BroadcastingTest < ActionCable::TestCase
+ test "fetching a broadcaster converts the broadcasting queue to a string" do
+ broadcasting = :test_queue
+ server = TestServer.new
+ broadcaster = server.broadcaster_for(broadcasting)
+
+ assert_equal "test_queue", broadcaster.broadcasting
+ end
+
+ test "broadcast generates notification" do
+ begin
+ server = TestServer.new
+
+ events = []
+ ActiveSupport::Notifications.subscribe "broadcast.action_cable" do |*args|
+ events << ActiveSupport::Notifications::Event.new(*args)
+ end
+
+ broadcasting = "test_queue"
+ message = { body: "test message" }
+ server.broadcast(broadcasting, message)
+
+ assert_equal 1, events.length
+ assert_equal "broadcast.action_cable", events[0].name
+ assert_equal broadcasting, events[0].payload[:broadcasting]
+ assert_equal message, events[0].payload[:message]
+ assert_equal ActiveSupport::JSON, events[0].payload[:coder]
+ ensure
+ ActiveSupport::Notifications.unsubscribe "broadcast.action_cable"
+ end
+ end
+
+ test "broadcaster from broadcaster_for generates notification" do
+ begin
+ server = TestServer.new
+
+ events = []
+ ActiveSupport::Notifications.subscribe "broadcast.action_cable" do |*args|
+ events << ActiveSupport::Notifications::Event.new(*args)
+ end
+
+ broadcasting = "test_queue"
+ message = { body: "test message" }
+
+ broadcaster = server.broadcaster_for(broadcasting)
+ broadcaster.broadcast(message)
+
+ assert_equal 1, events.length
+ assert_equal "broadcast.action_cable", events[0].name
+ assert_equal broadcasting, events[0].payload[:broadcasting]
+ assert_equal message, events[0].payload[:message]
+ assert_equal ActiveSupport::JSON, events[0].payload[:coder]
+ ensure
+ ActiveSupport::Notifications.unsubscribe "broadcast.action_cable"
+ end
+ end
+end
diff --git a/actioncable/test/stubs/global_id.rb b/actioncable/test/stubs/global_id.rb
new file mode 100644
index 0000000000..15fab6b8a7
--- /dev/null
+++ b/actioncable/test/stubs/global_id.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class GlobalID
+ attr_reader :uri
+ delegate :to_param, :to_s, to: :uri
+
+ def initialize(gid, options = {})
+ @uri = gid
+ end
+end
diff --git a/actioncable/test/stubs/room.rb b/actioncable/test/stubs/room.rb
new file mode 100644
index 0000000000..df7236f408
--- /dev/null
+++ b/actioncable/test/stubs/room.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class Room
+ attr_reader :id, :name
+
+ def initialize(id, name = "Campfire")
+ @id = id
+ @name = name
+ end
+
+ def to_global_id
+ GlobalID.new("Room##{id}-#{name}")
+ end
+
+ def to_gid_param
+ to_global_id.to_param
+ end
+end
diff --git a/actioncable/test/stubs/test_adapter.rb b/actioncable/test/stubs/test_adapter.rb
new file mode 100644
index 0000000000..3b25c9168f
--- /dev/null
+++ b/actioncable/test/stubs/test_adapter.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class SuccessAdapter < ActionCable::SubscriptionAdapter::Base
+ def broadcast(channel, payload)
+ end
+
+ def subscribe(channel, callback, success_callback = nil)
+ @@subscribe_called = { channel: channel, callback: callback, success_callback: success_callback }
+ end
+
+ def unsubscribe(channel, callback)
+ @@unsubscribe_called = { channel: channel, callback: callback }
+ end
+end
diff --git a/actioncable/test/stubs/test_connection.rb b/actioncable/test/stubs/test_connection.rb
new file mode 100644
index 0000000000..155c68e38c
--- /dev/null
+++ b/actioncable/test/stubs/test_connection.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require "stubs/user"
+
+class TestConnection
+ attr_reader :identifiers, :logger, :current_user, :server, :subscriptions, :transmissions
+
+ delegate :pubsub, to: :server
+
+ def initialize(user = User.new("lifo"), coder: ActiveSupport::JSON, subscription_adapter: SuccessAdapter)
+ @coder = coder
+ @identifiers = [ :current_user ]
+
+ @current_user = user
+ @logger = ActiveSupport::TaggedLogging.new ActiveSupport::Logger.new(StringIO.new)
+ @server = TestServer.new(subscription_adapter: subscription_adapter)
+ @transmissions = []
+ end
+
+ def transmit(cable_message)
+ @transmissions << encode(cable_message)
+ end
+
+ def last_transmission
+ decode @transmissions.last if @transmissions.any?
+ end
+
+ def decode(websocket_message)
+ @coder.decode websocket_message
+ end
+
+ def encode(cable_message)
+ @coder.encode cable_message
+ end
+end
diff --git a/actioncable/test/stubs/test_server.rb b/actioncable/test/stubs/test_server.rb
new file mode 100644
index 0000000000..0bc4625e28
--- /dev/null
+++ b/actioncable/test/stubs/test_server.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require "ostruct"
+
+class TestServer
+ include ActionCable::Server::Connections
+ include ActionCable::Server::Broadcasting
+
+ attr_reader :logger, :config, :mutex
+
+ def initialize(subscription_adapter: SuccessAdapter)
+ @logger = ActiveSupport::TaggedLogging.new ActiveSupport::Logger.new(StringIO.new)
+
+ @config = OpenStruct.new(log_tags: [], subscription_adapter: subscription_adapter)
+
+ @mutex = Monitor.new
+ end
+
+ def pubsub
+ @pubsub ||= @config.subscription_adapter.new(self)
+ end
+
+ def event_loop
+ @event_loop ||= ActionCable::Connection::StreamEventLoop.new.tap do |loop|
+ loop.instance_variable_set(:@executor, Concurrent.global_io_executor)
+ end
+ end
+
+ def worker_pool
+ @worker_pool ||= ActionCable::Server::Worker.new(max_size: 5)
+ end
+end
diff --git a/actioncable/test/stubs/user.rb b/actioncable/test/stubs/user.rb
new file mode 100644
index 0000000000..7894d1d9ae
--- /dev/null
+++ b/actioncable/test/stubs/user.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class User
+ attr_reader :name
+
+ def initialize(name)
+ @name = name
+ end
+
+ def to_global_id
+ GlobalID.new("User##{name}")
+ end
+
+ def to_gid_param
+ to_global_id.to_param
+ end
+end
diff --git a/actioncable/test/subscription_adapter/async_test.rb b/actioncable/test/subscription_adapter/async_test.rb
new file mode 100644
index 0000000000..6e038259b5
--- /dev/null
+++ b/actioncable/test/subscription_adapter/async_test.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require_relative "common"
+
+class AsyncAdapterTest < ActionCable::TestCase
+ include CommonSubscriptionAdapterTest
+
+ def setup
+ super
+
+ @tx_adapter.shutdown
+ @tx_adapter = @rx_adapter
+ end
+
+ def cable_config
+ { adapter: "async" }
+ end
+end
diff --git a/actioncable/test/subscription_adapter/base_test.rb b/actioncable/test/subscription_adapter/base_test.rb
new file mode 100644
index 0000000000..999dc0cba1
--- /dev/null
+++ b/actioncable/test/subscription_adapter/base_test.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "stubs/test_server"
+
+class ActionCable::SubscriptionAdapter::BaseTest < ActionCable::TestCase
+ ## TEST THAT ERRORS ARE RETURNED FOR INHERITORS THAT DON'T OVERRIDE METHODS
+
+ class BrokenAdapter < ActionCable::SubscriptionAdapter::Base
+ end
+
+ setup do
+ @server = TestServer.new
+ @server.config.subscription_adapter = BrokenAdapter
+ @server.config.allowed_request_origins = %w( http://rubyonrails.com )
+ end
+
+ test "#broadcast returns NotImplementedError by default" do
+ assert_raises NotImplementedError do
+ BrokenAdapter.new(@server).broadcast("channel", "payload")
+ end
+ end
+
+ test "#subscribe returns NotImplementedError by default" do
+ callback = lambda { puts "callback" }
+ success_callback = lambda { puts "success" }
+
+ assert_raises NotImplementedError do
+ BrokenAdapter.new(@server).subscribe("channel", callback, success_callback)
+ end
+ end
+
+ test "#unsubscribe returns NotImplementedError by default" do
+ callback = lambda { puts "callback" }
+
+ assert_raises NotImplementedError do
+ BrokenAdapter.new(@server).unsubscribe("channel", callback)
+ end
+ end
+
+ # TEST METHODS THAT ARE REQUIRED OF THE ADAPTER'S BACKEND STORAGE OBJECT
+
+ test "#broadcast is implemented" do
+ assert_nothing_raised do
+ SuccessAdapter.new(@server).broadcast("channel", "payload")
+ end
+ end
+
+ test "#subscribe is implemented" do
+ callback = lambda { puts "callback" }
+ success_callback = lambda { puts "success" }
+
+ assert_nothing_raised do
+ SuccessAdapter.new(@server).subscribe("channel", callback, success_callback)
+ end
+ end
+
+ test "#unsubscribe is implemented" do
+ callback = lambda { puts "callback" }
+
+ assert_nothing_raised do
+ SuccessAdapter.new(@server).unsubscribe("channel", callback)
+ end
+ end
+end
diff --git a/actioncable/test/subscription_adapter/channel_prefix.rb b/actioncable/test/subscription_adapter/channel_prefix.rb
new file mode 100644
index 0000000000..3071facd9d
--- /dev/null
+++ b/actioncable/test/subscription_adapter/channel_prefix.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+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/common.rb b/actioncable/test/subscription_adapter/common.rb
new file mode 100644
index 0000000000..b3e9ae9d5c
--- /dev/null
+++ b/actioncable/test/subscription_adapter/common.rb
@@ -0,0 +1,131 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "concurrent"
+
+require "active_support/core_ext/hash/indifferent_access"
+require "pathname"
+
+module CommonSubscriptionAdapterTest
+ WAIT_WHEN_EXPECTING_EVENT = 3
+ WAIT_WHEN_NOT_EXPECTING_EVENT = 0.2
+
+ def setup
+ server = ActionCable::Server::Base.new
+ server.config.cable = cable_config.with_indifferent_access
+ server.config.logger = Logger.new(StringIO.new).tap { |l| l.level = Logger::UNKNOWN }
+
+ adapter_klass = server.config.pubsub_adapter
+
+ @rx_adapter = adapter_klass.new(server)
+ @tx_adapter = adapter_klass.new(server)
+ end
+
+ def teardown
+ [@rx_adapter, @tx_adapter].uniq.compact.each(&:shutdown)
+ end
+
+ def subscribe_as_queue(channel, adapter = @rx_adapter)
+ queue = Queue.new
+
+ callback = -> data { queue << data }
+ subscribed = Concurrent::Event.new
+ adapter.subscribe(channel, callback, Proc.new { subscribed.set })
+ subscribed.wait(WAIT_WHEN_EXPECTING_EVENT)
+ assert_predicate subscribed, :set?
+
+ yield queue
+
+ sleep WAIT_WHEN_NOT_EXPECTING_EVENT
+ assert_empty queue
+ ensure
+ adapter.unsubscribe(channel, callback) if subscribed.set?
+ end
+
+ def test_subscribe_and_unsubscribe
+ subscribe_as_queue("channel") do |queue|
+ end
+ end
+
+ def test_basic_broadcast
+ subscribe_as_queue("channel") do |queue|
+ @tx_adapter.broadcast("channel", "hello world")
+
+ assert_equal "hello world", queue.pop
+ end
+ end
+
+ def test_broadcast_after_unsubscribe
+ keep_queue = nil
+ subscribe_as_queue("channel") do |queue|
+ keep_queue = queue
+
+ @tx_adapter.broadcast("channel", "hello world")
+
+ assert_equal "hello world", queue.pop
+ end
+
+ @tx_adapter.broadcast("channel", "hello void")
+
+ sleep WAIT_WHEN_NOT_EXPECTING_EVENT
+ assert_empty keep_queue
+ end
+
+ def test_multiple_broadcast
+ subscribe_as_queue("channel") do |queue|
+ @tx_adapter.broadcast("channel", "bananas")
+ @tx_adapter.broadcast("channel", "apples")
+
+ received = []
+ 2.times { received << queue.pop }
+ assert_equal ["apples", "bananas"], received.sort
+ end
+ end
+
+ def test_identical_subscriptions
+ subscribe_as_queue("channel") do |queue|
+ subscribe_as_queue("channel") do |queue_2|
+ @tx_adapter.broadcast("channel", "hello")
+
+ assert_equal "hello", queue_2.pop
+ end
+
+ assert_equal "hello", queue.pop
+ end
+ end
+
+ def test_simultaneous_subscriptions
+ subscribe_as_queue("channel") do |queue|
+ subscribe_as_queue("other channel") do |queue_2|
+ @tx_adapter.broadcast("channel", "apples")
+ @tx_adapter.broadcast("other channel", "oranges")
+
+ assert_equal "apples", queue.pop
+ assert_equal "oranges", queue_2.pop
+ end
+ end
+ end
+
+ def test_channel_filtered_broadcast
+ subscribe_as_queue("channel") do |queue|
+ @tx_adapter.broadcast("other channel", "one")
+ @tx_adapter.broadcast("channel", "two")
+
+ assert_equal "two", queue.pop
+ end
+ end
+
+ def test_long_identifiers
+ channel_1 = "a" * 100 + "1"
+ channel_2 = "a" * 100 + "2"
+ subscribe_as_queue(channel_1) do |queue|
+ subscribe_as_queue(channel_2) do |queue_2|
+ @tx_adapter.broadcast(channel_1, "apples")
+ @tx_adapter.broadcast(channel_2, "oranges")
+
+ assert_equal "apples", queue.pop
+ assert_equal "oranges", queue_2.pop
+ end
+ end
+ end
+end
diff --git a/actioncable/test/subscription_adapter/inline_test.rb b/actioncable/test/subscription_adapter/inline_test.rb
new file mode 100644
index 0000000000..6305626b2b
--- /dev/null
+++ b/actioncable/test/subscription_adapter/inline_test.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require_relative "common"
+
+class InlineAdapterTest < ActionCable::TestCase
+ include CommonSubscriptionAdapterTest
+
+ def setup
+ super
+
+ @tx_adapter.shutdown
+ @tx_adapter = @rx_adapter
+ end
+
+ def cable_config
+ { adapter: "inline" }
+ end
+end
diff --git a/actioncable/test/subscription_adapter/postgresql_test.rb b/actioncable/test/subscription_adapter/postgresql_test.rb
new file mode 100644
index 0000000000..5fb26a8896
--- /dev/null
+++ b/actioncable/test/subscription_adapter/postgresql_test.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require_relative "common"
+
+require "active_record"
+
+class PostgresqlAdapterTest < ActionCable::TestCase
+ include CommonSubscriptionAdapterTest
+
+ def setup
+ database_config = { "adapter" => "postgresql", "database" => "activerecord_unittest" }
+ ar_tests = File.expand_path("../../../activerecord/test", __dir__)
+ if Dir.exist?(ar_tests)
+ require File.join(ar_tests, "config")
+ require File.join(ar_tests, "support/config")
+ local_config = ARTest.config["connections"]["postgresql"]["arunit"]
+ database_config.update local_config if local_config
+ end
+
+ ActiveRecord::Base.establish_connection database_config
+
+ begin
+ ActiveRecord::Base.connection
+ rescue
+ @rx_adapter = @tx_adapter = nil
+ skip "Couldn't connect to PostgreSQL: #{database_config.inspect}"
+ end
+
+ super
+ end
+
+ def teardown
+ super
+
+ ActiveRecord::Base.clear_all_connections!
+ end
+
+ def cable_config
+ { adapter: "postgresql" }
+ end
+
+ def test_clear_active_record_connections_adapter_still_works
+ server = ActionCable::Server::Base.new
+ server.config.cable = cable_config.with_indifferent_access
+ server.config.logger = Logger.new(StringIO.new).tap { |l| l.level = Logger::UNKNOWN }
+
+ adapter_klass = Class.new(server.config.pubsub_adapter) do
+ def active?
+ !@listener.nil?
+ end
+ end
+
+ adapter = adapter_klass.new(server)
+
+ subscribe_as_queue("channel", adapter) do |queue|
+ adapter.broadcast("channel", "hello world")
+ assert_equal "hello world", queue.pop
+ end
+
+ ActiveRecord::Base.clear_reloadable_connections!
+
+ assert adapter.active?
+ end
+end
diff --git a/actioncable/test/subscription_adapter/redis_test.rb b/actioncable/test/subscription_adapter/redis_test.rb
new file mode 100644
index 0000000000..ac2d8ef724
--- /dev/null
+++ b/actioncable/test/subscription_adapter/redis_test.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require_relative "common"
+require_relative "channel_prefix"
+
+require "action_cable/subscription_adapter/redis"
+
+class RedisAdapterTest < ActionCable::TestCase
+ include CommonSubscriptionAdapterTest
+ include ChannelPrefixTest
+
+ def cable_config
+ { adapter: "redis", driver: "ruby" }
+ end
+end
+
+class RedisAdapterTest::Hiredis < RedisAdapterTest
+ def cable_config
+ super.merge(driver: "hiredis")
+ end
+end
+
+class RedisAdapterTest::AlternateConfiguration < RedisAdapterTest
+ def cable_config
+ alt_cable_config = super.dup
+ alt_cable_config.delete(:url)
+ alt_cable_config.merge(host: "127.0.0.1", port: 6379, db: 12)
+ end
+end
+
+class RedisAdapterTest::Connector < ActionCable::TestCase
+ test "slices url, host, port, db, password and id from config" do
+ config = { url: 1, host: 2, port: 3, db: 4, password: 5, id: "Some custom ID" }
+
+ assert_called_with ::Redis, :new, [ config ] do
+ connect config.merge(other: "unrelated", stuff: "here")
+ end
+ end
+
+ test "adds default id if it is not specified" do
+ config = { url: 1, host: 2, port: 3, db: 4, password: 5, id: "ActionCable-PID-#{$$}" }
+
+ assert_called_with ::Redis, :new, [ config ] do
+ connect config.except(:id)
+ end
+ end
+
+ def connect(config)
+ ActionCable::SubscriptionAdapter::Redis.redis_connector.call(config)
+ end
+end
diff --git a/actioncable/test/subscription_adapter/subscriber_map_test.rb b/actioncable/test/subscription_adapter/subscriber_map_test.rb
new file mode 100644
index 0000000000..ed81099cbc
--- /dev/null
+++ b/actioncable/test/subscription_adapter/subscriber_map_test.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class SubscriberMapTest < ActionCable::TestCase
+ test "broadcast should not change subscribers" do
+ setup_subscription_map
+ origin = @subscription_map.instance_variable_get(:@subscribers).dup
+
+ @subscription_map.broadcast("not_exist_channel", "")
+
+ assert_equal origin, @subscription_map.instance_variable_get(:@subscribers)
+ end
+
+ private
+ def setup_subscription_map
+ @subscription_map = ActionCable::SubscriptionAdapter::SubscriberMap.new
+ end
+end
diff --git a/actioncable/test/subscription_adapter/test_adapter_test.rb b/actioncable/test/subscription_adapter/test_adapter_test.rb
new file mode 100644
index 0000000000..3fe07adb4a
--- /dev/null
+++ b/actioncable/test/subscription_adapter/test_adapter_test.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require_relative "common"
+
+class ActionCable::SubscriptionAdapter::TestTest < ActionCable::TestCase
+ include CommonSubscriptionAdapterTest
+
+ def setup
+ super
+
+ @tx_adapter.shutdown
+ @tx_adapter = @rx_adapter
+ end
+
+ def cable_config
+ { adapter: "test" }
+ end
+
+ test "#broadcast stores messages for streams" do
+ @tx_adapter.broadcast("channel", "payload")
+ @tx_adapter.broadcast("channel2", "payload2")
+
+ assert_equal ["payload"], @tx_adapter.broadcasts("channel")
+ assert_equal ["payload2"], @tx_adapter.broadcasts("channel2")
+ end
+
+ test "#clear_messages deletes recorded broadcasts for the channel" do
+ @tx_adapter.broadcast("channel", "payload")
+ @tx_adapter.broadcast("channel2", "payload2")
+
+ @tx_adapter.clear_messages("channel")
+
+ assert_equal [], @tx_adapter.broadcasts("channel")
+ assert_equal ["payload2"], @tx_adapter.broadcasts("channel2")
+ end
+
+ test "#clear deletes all recorded broadcasts" do
+ @tx_adapter.broadcast("channel", "payload")
+ @tx_adapter.broadcast("channel2", "payload2")
+
+ @tx_adapter.clear
+
+ assert_equal [], @tx_adapter.broadcasts("channel")
+ assert_equal [], @tx_adapter.broadcasts("channel2")
+ end
+end
diff --git a/actioncable/test/test_helper.rb b/actioncable/test/test_helper.rb
new file mode 100644
index 0000000000..c924f1e475
--- /dev/null
+++ b/actioncable/test/test_helper.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require "action_cable"
+require "active_support/testing/autorun"
+require "active_support/testing/method_call_assertions"
+
+require "puma"
+require "rack/mock"
+
+begin
+ require "byebug"
+rescue LoadError
+end
+
+# Require all the stubs and models
+Dir[File.expand_path("stubs/*.rb", __dir__)].each { |file| require file }
+
+# Set test adapter and logger
+ActionCable.server.config.cable = { "adapter" => "test" }
+ActionCable.server.config.logger = Logger.new(nil)
+
+class ActionCable::TestCase < ActiveSupport::TestCase
+ include ActiveSupport::Testing::MethodCallAssertions
+
+ def wait_for_async
+ wait_for_executor Concurrent.global_io_executor
+ end
+
+ def run_in_eventmachine
+ yield
+ wait_for_async
+ end
+
+ def wait_for_executor(executor)
+ # do not wait forever, wait 2s
+ timeout = 2
+ until executor.completed_task_count == executor.scheduled_task_count
+ sleep 0.1
+ timeout -= 0.1
+ raise "Executor could not complete all tasks in 2 seconds" unless timeout > 0
+ end
+ end
+end
diff --git a/actioncable/test/test_helper_test.rb b/actioncable/test/test_helper_test.rb
new file mode 100644
index 0000000000..90e3dbf01f
--- /dev/null
+++ b/actioncable/test/test_helper_test.rb
@@ -0,0 +1,116 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class BroadcastChannel < ActionCable::Channel::Base
+end
+
+class TransmissionsTest < ActionCable::TestCase
+ def test_assert_broadcasts
+ assert_nothing_raised do
+ assert_broadcasts("test", 1) do
+ ActionCable.server.broadcast "test", "message"
+ end
+ end
+ end
+
+ def test_assert_broadcasts_with_no_block
+ assert_nothing_raised do
+ ActionCable.server.broadcast "test", "message"
+ assert_broadcasts "test", 1
+ end
+
+ assert_nothing_raised do
+ ActionCable.server.broadcast "test", "message 2"
+ ActionCable.server.broadcast "test", "message 3"
+ assert_broadcasts "test", 3
+ end
+ end
+
+ def test_assert_no_broadcasts_with_no_block
+ assert_nothing_raised do
+ assert_no_broadcasts "test"
+ end
+ end
+
+ def test_assert_no_broadcasts
+ assert_nothing_raised do
+ assert_no_broadcasts("test") do
+ ActionCable.server.broadcast "test2", "message"
+ end
+ end
+ end
+
+ def test_assert_broadcasts_message_too_few_sent
+ ActionCable.server.broadcast "test", "hello"
+ error = assert_raises Minitest::Assertion do
+ assert_broadcasts("test", 2) do
+ ActionCable.server.broadcast "test", "world"
+ end
+ end
+
+ assert_match(/2 .* but 1/, error.message)
+ end
+
+ def test_assert_broadcasts_message_too_many_sent
+ error = assert_raises Minitest::Assertion do
+ assert_broadcasts("test", 1) do
+ ActionCable.server.broadcast "test", "hello"
+ ActionCable.server.broadcast "test", "world"
+ end
+ end
+
+ assert_match(/1 .* but 2/, error.message)
+ end
+
+ def test_assert_no_broadcasts_failure
+ error = assert_raises Minitest::Assertion do
+ assert_no_broadcasts "test" do
+ ActionCable.server.broadcast "test", "hello"
+ end
+ end
+
+ assert_match(/0 .* but 1/, error.message)
+ end
+end
+
+class TransmitedDataTest < ActionCable::TestCase
+ include ActionCable::TestHelper
+
+ def test_assert_broadcast_on
+ assert_nothing_raised do
+ assert_broadcast_on("test", "message") do
+ ActionCable.server.broadcast "test", "message"
+ end
+ end
+ end
+
+ def test_assert_broadcast_on_with_hash
+ assert_nothing_raised do
+ assert_broadcast_on("test", text: "hello") do
+ ActionCable.server.broadcast "test", text: "hello"
+ end
+ end
+ end
+
+ def test_assert_broadcast_on_with_no_block
+ assert_nothing_raised do
+ ActionCable.server.broadcast "test", "hello"
+ assert_broadcast_on "test", "hello"
+ end
+
+ assert_nothing_raised do
+ ActionCable.server.broadcast "test", "world"
+ assert_broadcast_on "test", "world"
+ end
+ end
+
+ def test_assert_broadcast_on_message
+ ActionCable.server.broadcast "test", "hello"
+ error = assert_raises Minitest::Assertion do
+ assert_broadcast_on("test", "world")
+ end
+
+ assert_match(/No messages sent/, error.message)
+ end
+end
diff --git a/actioncable/test/worker_test.rb b/actioncable/test/worker_test.rb
new file mode 100644
index 0000000000..f7dc428441
--- /dev/null
+++ b/actioncable/test/worker_test.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class WorkerTest < ActionCable::TestCase
+ class Receiver
+ attr_accessor :last_action
+
+ def run
+ @last_action = :run
+ end
+
+ def process(message)
+ @last_action = [ :process, message ]
+ end
+
+ def connection
+ self
+ end
+
+ def logger
+ # Impersonating a connection requires a TaggedLoggerProxy'ied logger.
+ inner_logger = Logger.new(StringIO.new).tap { |l| l.level = Logger::UNKNOWN }
+ ActionCable::Connection::TaggedLoggerProxy.new(inner_logger, tags: [])
+ end
+ end
+
+ setup do
+ @worker = ActionCable::Server::Worker.new
+ @receiver = Receiver.new
+ end
+
+ teardown do
+ @receiver.last_action = nil
+ end
+
+ test "invoke" do
+ @worker.invoke @receiver, :run, connection: @receiver.connection
+ assert_equal :run, @receiver.last_action
+ end
+
+ test "invoke with arguments" do
+ @worker.invoke @receiver, :process, "Hello", connection: @receiver.connection
+ assert_equal [ :process, "Hello" ], @receiver.last_action
+ end
+end
diff --git a/actioncable/yarn.lock b/actioncable/yarn.lock
new file mode 100644
index 0000000000..8b4fcaebd6
--- /dev/null
+++ b/actioncable/yarn.lock
@@ -0,0 +1,1928 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@types/estree@0.0.38":
+ version "0.0.38"
+ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.38.tgz#c1be40aa933723c608820a99a373a16d215a1ca2"
+
+"@types/node@*":
+ version "10.11.6"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.11.6.tgz#ce5690df6cd917a9178439a1013e39a7e565c46e"
+
+acorn-jsx@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b"
+ dependencies:
+ acorn "^3.0.4"
+
+acorn@^3.0.4:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
+
+acorn@^5.2.1:
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.3.0.tgz#7446d39459c54fb49a80e6ee6478149b940ec822"
+
+ajv-keywords@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762"
+
+ajv@^5.2.3, ajv@^5.3.0:
+ version "5.5.2"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
+ dependencies:
+ co "^4.6.0"
+ fast-deep-equal "^1.0.0"
+ fast-json-stable-stringify "^2.0.0"
+ json-schema-traverse "^0.3.0"
+
+ansi-escapes@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.0.0.tgz#ec3e8b4e9f8064fc02c3ac9b65f1c275bda8ef92"
+
+ansi-regex@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
+
+ansi-regex@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
+
+ansi-styles@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
+
+ansi-styles@^3.1.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88"
+ dependencies:
+ color-convert "^1.9.0"
+
+argparse@^1.0.7:
+ version "1.0.9"
+ resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86"
+ dependencies:
+ sprintf-js "~1.0.2"
+
+arr-diff@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf"
+ dependencies:
+ arr-flatten "^1.0.1"
+
+arr-flatten@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1"
+
+array-union@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39"
+ dependencies:
+ array-uniq "^1.0.1"
+
+array-uniq@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
+
+array-unique@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53"
+
+arrify@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
+
+babel-code-frame@^6.22.0, babel-code-frame@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
+ dependencies:
+ chalk "^1.1.3"
+ esutils "^2.0.2"
+ js-tokens "^3.0.2"
+
+babel-core@^6.25.0, babel-core@^6.26.0:
+ version "6.26.3"
+ resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.3.tgz#b2e2f09e342d0f0c88e2f02e067794125e75c207"
+ dependencies:
+ babel-code-frame "^6.26.0"
+ babel-generator "^6.26.0"
+ babel-helpers "^6.24.1"
+ babel-messages "^6.23.0"
+ babel-register "^6.26.0"
+ babel-runtime "^6.26.0"
+ babel-template "^6.26.0"
+ babel-traverse "^6.26.0"
+ babel-types "^6.26.0"
+ babylon "^6.18.0"
+ convert-source-map "^1.5.1"
+ debug "^2.6.9"
+ json5 "^0.5.1"
+ lodash "^4.17.4"
+ minimatch "^3.0.4"
+ path-is-absolute "^1.0.1"
+ private "^0.1.8"
+ slash "^1.0.0"
+ source-map "^0.5.7"
+
+babel-generator@^6.26.0:
+ version "6.26.1"
+ resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90"
+ dependencies:
+ babel-messages "^6.23.0"
+ babel-runtime "^6.26.0"
+ babel-types "^6.26.0"
+ detect-indent "^4.0.0"
+ jsesc "^1.3.0"
+ lodash "^4.17.4"
+ source-map "^0.5.7"
+ trim-right "^1.0.1"
+
+babel-helper-builder-binary-assignment-operator-visitor@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz#cce4517ada356f4220bcae8a02c2b346f9a56664"
+ dependencies:
+ babel-helper-explode-assignable-expression "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-helper-call-delegate@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz#ece6aacddc76e41c3461f88bfc575bd0daa2df8d"
+ dependencies:
+ babel-helper-hoist-variables "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helper-define-map@^6.24.1:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz#a5f56dab41a25f97ecb498c7ebaca9819f95be5f"
+ dependencies:
+ babel-helper-function-name "^6.24.1"
+ babel-runtime "^6.26.0"
+ babel-types "^6.26.0"
+ lodash "^4.17.4"
+
+babel-helper-explode-assignable-expression@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz#f25b82cf7dc10433c55f70592d5746400ac22caa"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helper-function-name@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9"
+ dependencies:
+ babel-helper-get-function-arity "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helper-get-function-arity@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz#8f7782aa93407c41d3aa50908f89b031b1b6853d"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-helper-hoist-variables@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz#1ecb27689c9d25513eadbc9914a73f5408be7a76"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-helper-optimise-call-expression@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz#f7a13427ba9f73f8f4fa993c54a97882d1244257"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-helper-regex@^6.24.1:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz#325c59f902f82f24b74faceed0363954f6495e72"
+ dependencies:
+ babel-runtime "^6.26.0"
+ babel-types "^6.26.0"
+ lodash "^4.17.4"
+
+babel-helper-remap-async-to-generator@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz#5ec581827ad723fecdd381f1c928390676e4551b"
+ dependencies:
+ babel-helper-function-name "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helper-replace-supers@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz#bf6dbfe43938d17369a213ca8a8bf74b6a90ab1a"
+ dependencies:
+ babel-helper-optimise-call-expression "^6.24.1"
+ babel-messages "^6.23.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helpers@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-messages@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-check-es2015-constants@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz#35157b101426fd2ffd3da3f75c7d1e91835bbf8a"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-external-helpers@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-external-helpers/-/babel-plugin-external-helpers-6.22.0.tgz#2285f48b02bd5dede85175caf8c62e86adccefa1"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-syntax-async-functions@^6.8.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95"
+
+babel-plugin-syntax-exponentiation-operator@^6.8.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de"
+
+babel-plugin-syntax-trailing-function-commas@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3"
+
+babel-plugin-transform-async-to-generator@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz#6536e378aff6cb1d5517ac0e40eb3e9fc8d08761"
+ dependencies:
+ babel-helper-remap-async-to-generator "^6.24.1"
+ babel-plugin-syntax-async-functions "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-arrow-functions@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-block-scoped-functions@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz#bbc51b49f964d70cb8d8e0b94e820246ce3a6141"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-block-scoping@^6.23.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz#d70f5299c1308d05c12f463813b0a09e73b1895f"
+ dependencies:
+ babel-runtime "^6.26.0"
+ babel-template "^6.26.0"
+ babel-traverse "^6.26.0"
+ babel-types "^6.26.0"
+ lodash "^4.17.4"
+
+babel-plugin-transform-es2015-classes@^6.23.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz#5a4c58a50c9c9461e564b4b2a3bfabc97a2584db"
+ dependencies:
+ babel-helper-define-map "^6.24.1"
+ babel-helper-function-name "^6.24.1"
+ babel-helper-optimise-call-expression "^6.24.1"
+ babel-helper-replace-supers "^6.24.1"
+ babel-messages "^6.23.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-computed-properties@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz#6fe2a8d16895d5634f4cd999b6d3480a308159b3"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-es2015-destructuring@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-duplicate-keys@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz#73eb3d310ca969e3ef9ec91c53741a6f1576423e"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-for-of@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz#f47c95b2b613df1d3ecc2fdb7573623c75248691"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-function-name@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz#834c89853bc36b1af0f3a4c5dbaa94fd8eacaa8b"
+ dependencies:
+ babel-helper-function-name "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-literals@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz#4f54a02d6cd66cf915280019a31d31925377ca2e"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-modules-amd@^6.22.0, babel-plugin-transform-es2015-modules-amd@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz#3b3e54017239842d6d19c3011c4bd2f00a00d154"
+ dependencies:
+ babel-plugin-transform-es2015-modules-commonjs "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-es2015-modules-commonjs@^6.23.0, babel-plugin-transform-es2015-modules-commonjs@^6.24.1:
+ version "6.26.2"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz#58a793863a9e7ca870bdc5a881117ffac27db6f3"
+ dependencies:
+ babel-plugin-transform-strict-mode "^6.24.1"
+ babel-runtime "^6.26.0"
+ babel-template "^6.26.0"
+ babel-types "^6.26.0"
+
+babel-plugin-transform-es2015-modules-systemjs@^6.23.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz#ff89a142b9119a906195f5f106ecf305d9407d23"
+ dependencies:
+ babel-helper-hoist-variables "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-es2015-modules-umd@^6.23.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz#ac997e6285cd18ed6176adb607d602344ad38468"
+ dependencies:
+ babel-plugin-transform-es2015-modules-amd "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-es2015-object-super@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz#24cef69ae21cb83a7f8603dad021f572eb278f8d"
+ dependencies:
+ babel-helper-replace-supers "^6.24.1"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-parameters@^6.23.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz#57ac351ab49caf14a97cd13b09f66fdf0a625f2b"
+ dependencies:
+ babel-helper-call-delegate "^6.24.1"
+ babel-helper-get-function-arity "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-shorthand-properties@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz#24f875d6721c87661bbd99a4622e51f14de38aa0"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-spread@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz#d6d68a99f89aedc4536c81a542e8dd9f1746f8d1"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-sticky-regex@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz#00c1cdb1aca71112cdf0cf6126c2ed6b457ccdbc"
+ dependencies:
+ babel-helper-regex "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-template-literals@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz#a84b3450f7e9f8f1f6839d6d687da84bb1236d8d"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-typeof-symbol@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz#dec09f1cddff94b52ac73d505c84df59dcceb372"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-unicode-regex@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz#d38b12f42ea7323f729387f18a7c5ae1faeb35e9"
+ dependencies:
+ babel-helper-regex "^6.24.1"
+ babel-runtime "^6.22.0"
+ regexpu-core "^2.0.0"
+
+babel-plugin-transform-exponentiation-operator@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz#2ab0c9c7f3098fa48907772bb813fe41e8de3a0e"
+ dependencies:
+ babel-helper-builder-binary-assignment-operator-visitor "^6.24.1"
+ babel-plugin-syntax-exponentiation-operator "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-regenerator@^6.22.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f"
+ dependencies:
+ regenerator-transform "^0.10.0"
+
+babel-plugin-transform-strict-mode@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-preset-env@^1.6.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.7.0.tgz#dea79fa4ebeb883cd35dab07e260c1c9c04df77a"
+ dependencies:
+ babel-plugin-check-es2015-constants "^6.22.0"
+ babel-plugin-syntax-trailing-function-commas "^6.22.0"
+ babel-plugin-transform-async-to-generator "^6.22.0"
+ babel-plugin-transform-es2015-arrow-functions "^6.22.0"
+ babel-plugin-transform-es2015-block-scoped-functions "^6.22.0"
+ babel-plugin-transform-es2015-block-scoping "^6.23.0"
+ babel-plugin-transform-es2015-classes "^6.23.0"
+ babel-plugin-transform-es2015-computed-properties "^6.22.0"
+ babel-plugin-transform-es2015-destructuring "^6.23.0"
+ babel-plugin-transform-es2015-duplicate-keys "^6.22.0"
+ babel-plugin-transform-es2015-for-of "^6.23.0"
+ babel-plugin-transform-es2015-function-name "^6.22.0"
+ babel-plugin-transform-es2015-literals "^6.22.0"
+ babel-plugin-transform-es2015-modules-amd "^6.22.0"
+ babel-plugin-transform-es2015-modules-commonjs "^6.23.0"
+ babel-plugin-transform-es2015-modules-systemjs "^6.23.0"
+ babel-plugin-transform-es2015-modules-umd "^6.23.0"
+ babel-plugin-transform-es2015-object-super "^6.22.0"
+ babel-plugin-transform-es2015-parameters "^6.23.0"
+ babel-plugin-transform-es2015-shorthand-properties "^6.22.0"
+ babel-plugin-transform-es2015-spread "^6.22.0"
+ babel-plugin-transform-es2015-sticky-regex "^6.22.0"
+ babel-plugin-transform-es2015-template-literals "^6.22.0"
+ babel-plugin-transform-es2015-typeof-symbol "^6.23.0"
+ babel-plugin-transform-es2015-unicode-regex "^6.22.0"
+ babel-plugin-transform-exponentiation-operator "^6.22.0"
+ babel-plugin-transform-regenerator "^6.22.0"
+ browserslist "^3.2.6"
+ invariant "^2.2.2"
+ semver "^5.3.0"
+
+babel-register@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071"
+ dependencies:
+ babel-core "^6.26.0"
+ babel-runtime "^6.26.0"
+ core-js "^2.5.0"
+ home-or-tmp "^2.0.0"
+ lodash "^4.17.4"
+ mkdirp "^0.5.1"
+ source-map-support "^0.4.15"
+
+babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
+ dependencies:
+ core-js "^2.4.0"
+ regenerator-runtime "^0.11.0"
+
+babel-template@^6.24.1, babel-template@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02"
+ dependencies:
+ babel-runtime "^6.26.0"
+ babel-traverse "^6.26.0"
+ babel-types "^6.26.0"
+ babylon "^6.18.0"
+ lodash "^4.17.4"
+
+babel-traverse@^6.24.1, babel-traverse@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee"
+ dependencies:
+ babel-code-frame "^6.26.0"
+ babel-messages "^6.23.0"
+ babel-runtime "^6.26.0"
+ babel-types "^6.26.0"
+ babylon "^6.18.0"
+ debug "^2.6.8"
+ globals "^9.18.0"
+ invariant "^2.2.2"
+ lodash "^4.17.4"
+
+babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497"
+ dependencies:
+ babel-runtime "^6.26.0"
+ esutils "^2.0.2"
+ lodash "^4.17.4"
+ to-fast-properties "^1.0.3"
+
+babylon@^6.18.0:
+ version "6.18.0"
+ resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
+
+balanced-match@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
+
+brace-expansion@^1.1.7:
+ version "1.1.8"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292"
+ dependencies:
+ balanced-match "^1.0.0"
+ concat-map "0.0.1"
+
+braces@^1.8.2:
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7"
+ dependencies:
+ expand-range "^1.8.1"
+ preserve "^0.2.0"
+ repeat-element "^1.1.2"
+
+browserslist@^3.2.6:
+ version "3.2.8"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-3.2.8.tgz#b0005361d6471f0f5952797a76fc985f1f978fc6"
+ dependencies:
+ caniuse-lite "^1.0.30000844"
+ electron-to-chromium "^1.3.47"
+
+builtin-modules@^1.0.0, builtin-modules@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
+
+builtin-modules@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-2.0.0.tgz#60b7ef5ae6546bd7deefa74b08b62a43a232648e"
+
+caller-path@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f"
+ dependencies:
+ callsites "^0.2.0"
+
+callsites@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca"
+
+caniuse-lite@^1.0.30000844:
+ version "1.0.30000890"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000890.tgz#86a18ffcc65d79ec6a437e985761b8bf1c4efeaf"
+
+chalk@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
+ dependencies:
+ ansi-styles "^2.2.1"
+ escape-string-regexp "^1.0.2"
+ has-ansi "^2.0.0"
+ strip-ansi "^3.0.0"
+ supports-color "^2.0.0"
+
+chalk@^2.0.0, chalk@^2.1.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba"
+ dependencies:
+ ansi-styles "^3.1.0"
+ escape-string-regexp "^1.0.5"
+ supports-color "^4.0.0"
+
+chardet@^0.4.0:
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2"
+
+circular-json@^0.3.1:
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66"
+
+cli-cursor@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5"
+ dependencies:
+ restore-cursor "^2.0.0"
+
+cli-width@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
+
+co@^4.6.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
+
+color-convert@^1.9.0:
+ version "1.9.1"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed"
+ dependencies:
+ color-name "^1.1.1"
+
+color-name@^1.1.1:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+
+commander@~2.13.0:
+ version "2.13.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c"
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+
+concat-stream@^1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7"
+ dependencies:
+ inherits "^2.0.3"
+ readable-stream "^2.2.2"
+ typedarray "^0.0.6"
+
+contains-path@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a"
+
+convert-source-map@^1.5.1:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20"
+ dependencies:
+ safe-buffer "~5.1.1"
+
+core-js@^2.4.0, core-js@^2.5.0:
+ version "2.5.7"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e"
+
+core-util-is@~1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
+
+cross-spawn@^5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
+ dependencies:
+ lru-cache "^4.0.1"
+ shebang-command "^1.2.0"
+ which "^1.2.9"
+
+debug@^2.6.8, debug@^2.6.9:
+ version "2.6.9"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+ dependencies:
+ ms "2.0.0"
+
+debug@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
+ dependencies:
+ ms "2.0.0"
+
+deep-is@~0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
+
+del@^2.0.2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8"
+ dependencies:
+ globby "^5.0.0"
+ is-path-cwd "^1.0.0"
+ is-path-in-cwd "^1.0.0"
+ object-assign "^4.0.1"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+ rimraf "^2.2.8"
+
+detect-indent@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208"
+ dependencies:
+ repeating "^2.0.0"
+
+doctrine@1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa"
+ dependencies:
+ esutils "^2.0.2"
+ isarray "^1.0.0"
+
+doctrine@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
+ dependencies:
+ esutils "^2.0.2"
+
+electron-to-chromium@^1.3.47:
+ version "1.3.75"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.75.tgz#dd04551739e7371862b0ac7f4ddaa9f3f95b7e68"
+
+error-ex@^1.2.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc"
+ dependencies:
+ is-arrayish "^0.2.1"
+
+escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+
+eslint-import-resolver-node@^0.3.1:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz#58f15fb839b8d0576ca980413476aab2472db66a"
+ dependencies:
+ debug "^2.6.9"
+ resolve "^1.5.0"
+
+eslint-module-utils@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.1.1.tgz#abaec824177613b8a95b299639e1b6facf473449"
+ dependencies:
+ debug "^2.6.8"
+ pkg-dir "^1.0.0"
+
+eslint-plugin-import@^2.7.0:
+ version "2.8.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.8.0.tgz#fa1b6ef31fcb3c501c09859c1b86f1fc5b986894"
+ dependencies:
+ builtin-modules "^1.1.1"
+ contains-path "^0.1.0"
+ debug "^2.6.8"
+ doctrine "1.5.0"
+ eslint-import-resolver-node "^0.3.1"
+ eslint-module-utils "^2.1.1"
+ has "^1.0.1"
+ lodash.cond "^4.3.0"
+ minimatch "^3.0.3"
+ read-pkg-up "^2.0.0"
+
+eslint-scope@^3.7.1:
+ version "3.7.1"
+ resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8"
+ dependencies:
+ esrecurse "^4.1.0"
+ estraverse "^4.1.1"
+
+eslint-visitor-keys@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d"
+
+eslint@^4.3.0:
+ version "4.16.0"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.16.0.tgz#934ada9e98715e1d7bbfd6f6f0519ed2fab35cc1"
+ dependencies:
+ ajv "^5.3.0"
+ babel-code-frame "^6.22.0"
+ chalk "^2.1.0"
+ concat-stream "^1.6.0"
+ cross-spawn "^5.1.0"
+ debug "^3.1.0"
+ doctrine "^2.1.0"
+ eslint-scope "^3.7.1"
+ eslint-visitor-keys "^1.0.0"
+ espree "^3.5.2"
+ esquery "^1.0.0"
+ esutils "^2.0.2"
+ file-entry-cache "^2.0.0"
+ functional-red-black-tree "^1.0.1"
+ glob "^7.1.2"
+ globals "^11.0.1"
+ ignore "^3.3.3"
+ imurmurhash "^0.1.4"
+ inquirer "^3.0.6"
+ is-resolvable "^1.0.0"
+ js-yaml "^3.9.1"
+ json-stable-stringify-without-jsonify "^1.0.1"
+ levn "^0.3.0"
+ lodash "^4.17.4"
+ minimatch "^3.0.2"
+ mkdirp "^0.5.1"
+ natural-compare "^1.4.0"
+ optionator "^0.8.2"
+ path-is-inside "^1.0.2"
+ pluralize "^7.0.0"
+ progress "^2.0.0"
+ require-uncached "^1.0.3"
+ semver "^5.3.0"
+ strip-ansi "^4.0.0"
+ strip-json-comments "~2.0.1"
+ table "^4.0.1"
+ text-table "~0.2.0"
+
+espree@^3.5.2:
+ version "3.5.2"
+ resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.2.tgz#756ada8b979e9dcfcdb30aad8d1a9304a905e1ca"
+ dependencies:
+ acorn "^5.2.1"
+ acorn-jsx "^3.0.0"
+
+esprima@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804"
+
+esquery@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.0.tgz#cfba8b57d7fba93f17298a8a006a04cda13d80fa"
+ dependencies:
+ estraverse "^4.0.0"
+
+esrecurse@^4.1.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.0.tgz#fa9568d98d3823f9a41d91e902dcab9ea6e5b163"
+ dependencies:
+ estraverse "^4.1.0"
+ object-assign "^4.0.1"
+
+estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
+
+estree-walker@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.2.1.tgz#bdafe8095383d8414d5dc2ecf4c9173b6db9412e"
+
+estree-walker@^0.5.1, estree-walker@^0.5.2:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.5.2.tgz#d3850be7529c9580d815600b53126515e146dd39"
+
+esutils@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
+
+expand-brackets@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b"
+ dependencies:
+ is-posix-bracket "^0.1.0"
+
+expand-range@^1.8.1:
+ version "1.8.2"
+ resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337"
+ dependencies:
+ fill-range "^2.1.0"
+
+external-editor@^2.0.4:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.1.0.tgz#3d026a21b7f95b5726387d4200ac160d372c3b48"
+ dependencies:
+ chardet "^0.4.0"
+ iconv-lite "^0.4.17"
+ tmp "^0.0.33"
+
+extglob@^0.3.1:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1"
+ dependencies:
+ is-extglob "^1.0.0"
+
+fast-deep-equal@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
+
+fast-json-stable-stringify@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
+
+fast-levenshtein@~2.0.4:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
+
+figures@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962"
+ dependencies:
+ escape-string-regexp "^1.0.5"
+
+file-entry-cache@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361"
+ dependencies:
+ flat-cache "^1.2.1"
+ object-assign "^4.0.1"
+
+filename-regex@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26"
+
+fill-range@^2.1.0:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.4.tgz#eb1e773abb056dcd8df2bfdf6af59b8b3a936565"
+ dependencies:
+ is-number "^2.1.0"
+ isobject "^2.0.0"
+ randomatic "^3.0.0"
+ repeat-element "^1.1.2"
+ repeat-string "^1.5.2"
+
+find-up@^1.0.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f"
+ dependencies:
+ path-exists "^2.0.0"
+ pinkie-promise "^2.0.0"
+
+find-up@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
+ dependencies:
+ locate-path "^2.0.0"
+
+flat-cache@^1.2.1:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.3.0.tgz#d3030b32b38154f4e3b7e9c709f490f7ef97c481"
+ dependencies:
+ circular-json "^0.3.1"
+ del "^2.0.2"
+ graceful-fs "^4.1.2"
+ write "^0.2.1"
+
+for-in@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
+
+for-own@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce"
+ dependencies:
+ for-in "^1.0.1"
+
+fs.realpath@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+
+function-bind@^1.0.2:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+
+functional-red-black-tree@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
+
+glob-base@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
+ dependencies:
+ glob-parent "^2.0.0"
+ is-glob "^2.0.0"
+
+glob-parent@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28"
+ dependencies:
+ is-glob "^2.0.0"
+
+glob@^7.0.3, glob@^7.0.5, glob@^7.1.2:
+ version "7.1.2"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.0.4"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+globals@^11.0.1:
+ version "11.1.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-11.1.0.tgz#632644457f5f0e3ae711807183700ebf2e4633e4"
+
+globals@^9.18.0:
+ version "9.18.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
+
+globby@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d"
+ dependencies:
+ array-union "^1.0.1"
+ arrify "^1.0.0"
+ glob "^7.0.3"
+ object-assign "^4.0.1"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+
+graceful-fs@^4.1.2:
+ version "4.1.11"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
+
+has-ansi@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
+ dependencies:
+ ansi-regex "^2.0.0"
+
+has-flag@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51"
+
+has@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28"
+ dependencies:
+ function-bind "^1.0.2"
+
+home-or-tmp@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"
+ dependencies:
+ os-homedir "^1.0.0"
+ os-tmpdir "^1.0.1"
+
+hosted-git-info@^2.1.4:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c"
+
+iconv-lite@^0.4.17:
+ version "0.4.19"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
+
+ignore@^3.3.3:
+ version "3.3.7"
+ resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021"
+
+imurmurhash@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
+
+inflight@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+ dependencies:
+ once "^1.3.0"
+ wrappy "1"
+
+inherits@2, inherits@^2.0.3, inherits@~2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
+
+inquirer@^3.0.6:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9"
+ dependencies:
+ ansi-escapes "^3.0.0"
+ chalk "^2.0.0"
+ cli-cursor "^2.1.0"
+ cli-width "^2.0.0"
+ external-editor "^2.0.4"
+ figures "^2.0.0"
+ lodash "^4.3.0"
+ mute-stream "0.0.7"
+ run-async "^2.2.0"
+ rx-lite "^4.0.8"
+ rx-lite-aggregates "^4.0.8"
+ string-width "^2.1.0"
+ strip-ansi "^4.0.0"
+ through "^2.3.6"
+
+invariant@^2.2.2:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
+ dependencies:
+ loose-envify "^1.0.0"
+
+is-arrayish@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
+
+is-buffer@^1.1.5:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
+
+is-builtin-module@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe"
+ dependencies:
+ builtin-modules "^1.0.0"
+
+is-dotfile@^1.0.0:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1"
+
+is-equal-shallow@^0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534"
+ dependencies:
+ is-primitive "^2.0.0"
+
+is-extendable@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
+
+is-extglob@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0"
+
+is-finite@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa"
+ dependencies:
+ number-is-nan "^1.0.0"
+
+is-fullwidth-code-point@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
+
+is-glob@^2.0.0, is-glob@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863"
+ dependencies:
+ is-extglob "^1.0.0"
+
+is-module@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591"
+
+is-number@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f"
+ dependencies:
+ kind-of "^3.0.2"
+
+is-number@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff"
+
+is-path-cwd@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d"
+
+is-path-in-cwd@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc"
+ dependencies:
+ is-path-inside "^1.0.0"
+
+is-path-inside@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036"
+ dependencies:
+ path-is-inside "^1.0.1"
+
+is-posix-bracket@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4"
+
+is-primitive@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575"
+
+is-promise@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa"
+
+is-resolvable@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.0.1.tgz#acca1cd36dbe44b974b924321555a70ba03b1cf4"
+
+isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+
+isexe@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+
+isobject@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
+ dependencies:
+ isarray "1.0.0"
+
+"js-tokens@^3.0.0 || ^4.0.0":
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
+
+js-tokens@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
+
+js-yaml@^3.9.1:
+ version "3.10.0"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc"
+ dependencies:
+ argparse "^1.0.7"
+ esprima "^4.0.0"
+
+jsesc@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b"
+
+jsesc@~0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
+
+json-schema-traverse@^0.3.0:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340"
+
+json-stable-stringify-without-jsonify@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
+
+json5@^0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
+
+kind-of@^3.0.2:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
+ dependencies:
+ is-buffer "^1.1.5"
+
+kind-of@^6.0.0:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
+
+levn@^0.3.0, levn@~0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
+ dependencies:
+ prelude-ls "~1.1.2"
+ type-check "~0.3.2"
+
+load-json-file@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8"
+ dependencies:
+ graceful-fs "^4.1.2"
+ parse-json "^2.2.0"
+ pify "^2.0.0"
+ strip-bom "^3.0.0"
+
+locate-path@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
+ dependencies:
+ p-locate "^2.0.0"
+ path-exists "^3.0.0"
+
+lodash.cond@^4.3.0:
+ version "4.5.2"
+ resolved "https://registry.yarnpkg.com/lodash.cond/-/lodash.cond-4.5.2.tgz#f471a1da486be60f6ab955d17115523dd1d255d5"
+
+lodash@^4.17.4, lodash@^4.3.0:
+ version "4.17.4"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
+
+loose-envify@^1.0.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
+ dependencies:
+ js-tokens "^3.0.0 || ^4.0.0"
+
+lru-cache@^4.0.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55"
+ dependencies:
+ pseudomap "^1.0.2"
+ yallist "^2.1.2"
+
+magic-string@^0.22.4:
+ version "0.22.5"
+ resolved "http://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz#8e9cf5afddf44385c1da5bc2a6a0dbd10b03657e"
+ dependencies:
+ vlq "^0.2.2"
+
+math-random@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.1.tgz#8b3aac588b8a66e4975e3cdea67f7bb329601fac"
+
+micromatch@^2.3.11:
+ version "2.3.11"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565"
+ dependencies:
+ arr-diff "^2.0.0"
+ array-unique "^0.2.1"
+ braces "^1.8.2"
+ expand-brackets "^0.1.4"
+ extglob "^0.3.1"
+ filename-regex "^2.0.0"
+ is-extglob "^1.0.0"
+ is-glob "^2.0.1"
+ kind-of "^3.0.2"
+ normalize-path "^2.0.1"
+ object.omit "^2.0.0"
+ parse-glob "^3.0.4"
+ regex-cache "^0.4.2"
+
+mimic-fn@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18"
+
+minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
+ dependencies:
+ brace-expansion "^1.1.7"
+
+minimist@0.0.8:
+ version "0.0.8"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
+
+mkdirp@^0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
+ dependencies:
+ minimist "0.0.8"
+
+ms@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+
+mute-stream@0.0.7:
+ version "0.0.7"
+ resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
+
+natural-compare@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
+
+normalize-package-data@^2.3.2:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f"
+ dependencies:
+ hosted-git-info "^2.1.4"
+ is-builtin-module "^1.0.0"
+ semver "2 || 3 || 4 || 5"
+ validate-npm-package-license "^3.0.1"
+
+normalize-path@^2.0.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
+ dependencies:
+ remove-trailing-separator "^1.0.1"
+
+number-is-nan@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
+
+object-assign@^4.0.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+
+object.omit@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa"
+ dependencies:
+ for-own "^0.1.4"
+ is-extendable "^0.1.1"
+
+once@^1.3.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+ dependencies:
+ wrappy "1"
+
+onetime@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
+ dependencies:
+ mimic-fn "^1.0.0"
+
+optionator@^0.8.2:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64"
+ dependencies:
+ deep-is "~0.1.3"
+ fast-levenshtein "~2.0.4"
+ levn "~0.3.0"
+ prelude-ls "~1.1.2"
+ type-check "~0.3.2"
+ wordwrap "~1.0.0"
+
+os-homedir@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
+
+os-tmpdir@^1.0.1, os-tmpdir@~1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
+
+p-limit@^1.1.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.2.0.tgz#0e92b6bedcb59f022c13d0f1949dc82d15909f1c"
+ dependencies:
+ p-try "^1.0.0"
+
+p-locate@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
+ dependencies:
+ p-limit "^1.1.0"
+
+p-try@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
+
+parse-glob@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c"
+ dependencies:
+ glob-base "^0.3.0"
+ is-dotfile "^1.0.0"
+ is-extglob "^1.0.0"
+ is-glob "^2.0.0"
+
+parse-json@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
+ dependencies:
+ error-ex "^1.2.0"
+
+path-exists@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b"
+ dependencies:
+ pinkie-promise "^2.0.0"
+
+path-exists@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
+
+path-is-absolute@^1.0.0, path-is-absolute@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+
+path-is-inside@^1.0.1, path-is-inside@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
+
+path-parse@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1"
+
+path-type@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73"
+ dependencies:
+ pify "^2.0.0"
+
+pify@^2.0.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
+
+pinkie-promise@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
+ dependencies:
+ pinkie "^2.0.0"
+
+pinkie@^2.0.0:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
+
+pkg-dir@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-1.0.0.tgz#7a4b508a8d5bb2d629d447056ff4e9c9314cf3d4"
+ dependencies:
+ find-up "^1.0.0"
+
+pluralize@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777"
+
+prelude-ls@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
+
+preserve@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
+
+private@^0.1.6, private@^0.1.8:
+ version "0.1.8"
+ resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
+
+process-nextick-args@~1.0.6:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3"
+
+progress@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f"
+
+pseudomap@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
+
+randomatic@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.0.tgz#36f2ca708e9e567f5ed2ec01949026d50aa10116"
+ dependencies:
+ is-number "^4.0.0"
+ kind-of "^6.0.0"
+ math-random "^1.0.1"
+
+read-pkg-up@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be"
+ dependencies:
+ find-up "^2.0.0"
+ read-pkg "^2.0.0"
+
+read-pkg@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8"
+ dependencies:
+ load-json-file "^2.0.0"
+ normalize-package-data "^2.3.2"
+ path-type "^2.0.0"
+
+readable-stream@^2.2.2:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c"
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.3"
+ isarray "~1.0.0"
+ process-nextick-args "~1.0.6"
+ safe-buffer "~5.1.1"
+ string_decoder "~1.0.3"
+ util-deprecate "~1.0.1"
+
+regenerate@^1.2.1:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11"
+
+regenerator-runtime@^0.11.0:
+ version "0.11.1"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
+
+regenerator-transform@^0.10.0:
+ version "0.10.1"
+ resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd"
+ dependencies:
+ babel-runtime "^6.18.0"
+ babel-types "^6.19.0"
+ private "^0.1.6"
+
+regex-cache@^0.4.2:
+ version "0.4.4"
+ resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd"
+ dependencies:
+ is-equal-shallow "^0.1.3"
+
+regexpu-core@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240"
+ dependencies:
+ regenerate "^1.2.1"
+ regjsgen "^0.2.0"
+ regjsparser "^0.1.4"
+
+regjsgen@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7"
+
+regjsparser@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c"
+ dependencies:
+ jsesc "~0.5.0"
+
+remove-trailing-separator@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
+
+repeat-element@^1.1.2:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce"
+
+repeat-string@^1.5.2:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
+
+repeating@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda"
+ dependencies:
+ is-finite "^1.0.0"
+
+require-uncached@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3"
+ dependencies:
+ caller-path "^0.1.0"
+ resolve-from "^1.0.0"
+
+resolve-from@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226"
+
+resolve@^1.1.6:
+ version "1.8.1"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.8.1.tgz#82f1ec19a423ac1fbd080b0bab06ba36e84a7a26"
+ dependencies:
+ path-parse "^1.0.5"
+
+resolve@^1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36"
+ dependencies:
+ path-parse "^1.0.5"
+
+restore-cursor@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"
+ dependencies:
+ onetime "^2.0.0"
+ signal-exit "^3.0.2"
+
+rimraf@^2.2.8:
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
+ dependencies:
+ glob "^7.0.5"
+
+rollup-plugin-babel@^3.0.4:
+ version "3.0.7"
+ resolved "https://registry.yarnpkg.com/rollup-plugin-babel/-/rollup-plugin-babel-3.0.7.tgz#5b13611f1ab8922497e9d15197ae5d8a23fe3b1e"
+ dependencies:
+ rollup-pluginutils "^1.5.0"
+
+rollup-plugin-commonjs@^9.1.0:
+ version "9.1.8"
+ resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-9.1.8.tgz#4113ed94e6054b5f8a3501d8811f934cadde3246"
+ dependencies:
+ estree-walker "^0.5.1"
+ magic-string "^0.22.4"
+ resolve "^1.5.0"
+ rollup-pluginutils "^2.0.1"
+
+rollup-plugin-node-resolve@^3.3.0:
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-3.4.0.tgz#908585eda12e393caac7498715a01e08606abc89"
+ dependencies:
+ builtin-modules "^2.0.0"
+ is-module "^1.0.0"
+ resolve "^1.1.6"
+
+rollup-plugin-uglify@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/rollup-plugin-uglify/-/rollup-plugin-uglify-3.0.0.tgz#a34eca24617709c6bf1778e9653baafa06099b86"
+ dependencies:
+ uglify-es "^3.3.7"
+
+rollup-pluginutils@^1.5.0:
+ version "1.5.2"
+ resolved "http://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-1.5.2.tgz#1e156e778f94b7255bfa1b3d0178be8f5c552408"
+ dependencies:
+ estree-walker "^0.2.1"
+ minimatch "^3.0.2"
+
+rollup-pluginutils@^2.0.1:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.3.3.tgz#3aad9b1eb3e7fe8262820818840bf091e5ae6794"
+ dependencies:
+ estree-walker "^0.5.2"
+ micromatch "^2.3.11"
+
+rollup@^0.58.2:
+ version "0.58.2"
+ resolved "https://registry.yarnpkg.com/rollup/-/rollup-0.58.2.tgz#2feddea8c0c022f3e74b35c48e3c21b3433803ce"
+ dependencies:
+ "@types/estree" "0.0.38"
+ "@types/node" "*"
+
+run-async@^2.2.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"
+ dependencies:
+ is-promise "^2.1.0"
+
+rx-lite-aggregates@^4.0.8:
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz#753b87a89a11c95467c4ac1626c4efc4e05c67be"
+ dependencies:
+ rx-lite "*"
+
+rx-lite@*, rx-lite@^4.0.8:
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444"
+
+safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
+
+"semver@2 || 3 || 4 || 5", semver@^5.3.0:
+ version "5.5.0"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab"
+
+shebang-command@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
+ dependencies:
+ shebang-regex "^1.0.0"
+
+shebang-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
+
+signal-exit@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
+
+slash@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
+
+slice-ansi@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-1.0.0.tgz#044f1a49d8842ff307aad6b505ed178bd950134d"
+ dependencies:
+ is-fullwidth-code-point "^2.0.0"
+
+source-map-support@^0.4.15:
+ version "0.4.18"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f"
+ dependencies:
+ source-map "^0.5.6"
+
+source-map@^0.5.6, source-map@^0.5.7:
+ version "0.5.7"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
+
+source-map@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+
+spdx-correct@~1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40"
+ dependencies:
+ spdx-license-ids "^1.0.2"
+
+spdx-expression-parse@~1.0.0:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c"
+
+spdx-license-ids@^1.0.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57"
+
+sprintf-js@~1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
+
+string-width@^2.1.0, string-width@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
+ dependencies:
+ is-fullwidth-code-point "^2.0.0"
+ strip-ansi "^4.0.0"
+
+string_decoder@~1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab"
+ dependencies:
+ safe-buffer "~5.1.0"
+
+strip-ansi@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
+ dependencies:
+ ansi-regex "^2.0.0"
+
+strip-ansi@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
+ dependencies:
+ ansi-regex "^3.0.0"
+
+strip-bom@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
+
+strip-json-comments@~2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
+
+supports-color@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
+
+supports-color@^4.0.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b"
+ dependencies:
+ has-flag "^2.0.0"
+
+table@^4.0.1:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/table/-/table-4.0.2.tgz#a33447375391e766ad34d3486e6e2aedc84d2e36"
+ dependencies:
+ ajv "^5.2.3"
+ ajv-keywords "^2.1.0"
+ chalk "^2.1.0"
+ lodash "^4.17.4"
+ slice-ansi "1.0.0"
+ string-width "^2.1.1"
+
+text-table@~0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
+
+through@^2.3.6:
+ version "2.3.8"
+ resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
+
+tmp@^0.0.33:
+ version "0.0.33"
+ resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
+ dependencies:
+ os-tmpdir "~1.0.2"
+
+to-fast-properties@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47"
+
+trim-right@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
+
+type-check@~0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
+ dependencies:
+ prelude-ls "~1.1.2"
+
+typedarray@^0.0.6:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
+
+uglify-es@^3.3.7:
+ version "3.3.9"
+ resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677"
+ dependencies:
+ commander "~2.13.0"
+ source-map "~0.6.1"
+
+util-deprecate@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+
+validate-npm-package-license@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc"
+ dependencies:
+ spdx-correct "~1.0.0"
+ spdx-expression-parse "~1.0.0"
+
+vlq@^0.2.2:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26"
+
+which@^1.2.9:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a"
+ dependencies:
+ isexe "^2.0.0"
+
+wordwrap@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
+
+wrappy@1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+
+write@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757"
+ dependencies:
+ mkdirp "^0.5.1"
+
+yallist@^2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md
index 7c1b0b215a..db6fc7ee9c 100644
--- a/actionmailer/CHANGELOG.md
+++ b/actionmailer/CHANGELOG.md
@@ -1,87 +1,62 @@
-* Add `config.action_mailer.deliver_later_queue_name` configuration to set the
- mailer queue name.
-
- *Chris McGrath*
-
-* `assert_emails` in block form use the given number as expected value.
- This makes the error message much easier to understand.
-
- *Yuji Yaginuma*
-
-* Add support for inline images in mailer previews by using an interceptor
- class to convert cid: urls in image src attributes to data urls.
-
- *Andrew White*
+* Allow ActionMailer classes to configure the parameterized delivery job
+ Example:
+ ```
+ class MyMailer < ApplicationMailer
+ self.parameterized_delivery_job = MyCustomDeliveryJob
+ ...
+ end
+ ```
-* Mailer preview now uses `url_for` to fix links to emails for apps running on
- a subdirectory.
+ *Luke Pearce*
- *Remo Mueller*
+* `ActionDispatch::IntegrationTest` includes `ActionMailer::TestHelper` module by default.
-* Mailer previews no longer crash when the `mail` method wasn't called
- (`NullMail`).
+ *Ricardo Díaz*
- Fixes #19849.
+* Add `perform_deliveries` to a payload of `deliver.action_mailer` notification.
- *Yves Senn*
+ *Yoshiyuki Kinjo*
-* Make sure labels and values line up in mailer previews.
+* Change delivery logging message when `perform_deliveries` is false.
- *Yves Senn*
+ *Yoshiyuki Kinjo*
-* Add `assert_enqueued_emails` and `assert_no_enqueued_emails`.
+* Allow call `assert_enqueued_email_with` with no block.
Example:
+ ```
+ def test_email
+ ContactMailer.welcome.deliver_later
+ assert_enqueued_email_with ContactMailer, :welcome
+ end
- def test_emails
- assert_enqueued_emails 2 do
- ContactMailer.welcome.deliver_later
- ContactMailer.welcome.deliver_later
- end
- end
-
- def test_no_emails
- assert_no_enqueued_emails do
- # No emails enqueued here
- end
- end
-
- *George Claghorn*
-
-* Add `_mailer` suffix to mailers created via generator, following the same
- naming convention used in controllers and jobs.
-
- *Carlos Souza*
-
-* Remove deprecate `*_path` helpers in email views.
-
- *Rafael Mendonça França*
-
-* Remove deprecated `deliver` and `deliver!` methods.
+ def test_email_with_arguments
+ ContactMailer.welcome("Hello", "Goodbye").deliver_later
+ assert_enqueued_email_with ContactMailer, :welcome, args: ["Hello", "Goodbye"]
+ end
+ ```
- *claudiob*
+ *bogdanvlviv*
-* Template lookup now respects default locale and I18n fallbacks.
+* Ensure mail gem is eager autoloaded when eager load is true to prevent thread deadlocks.
- Given the following templates:
+ *Samuel Cochran*
- mailer/demo.html.erb
- mailer/demo.en.html.erb
- mailer/demo.pt.html.erb
+* Perform email jobs in `assert_emails`.
- Before this change, for a locale that doesn't have its associated file, the
- `mailer/demo.html.erb` would be rendered even if `en` was the default locale.
+ *Gannon McGibbon*
- Now `mailer/demo.en.html.erb` has precedence over the file without locale.
+* Add `Base.unregister_observer`, `Base.unregister_observers`,
+ `Base.unregister_interceptor`, `Base.unregister_interceptors`,
+ `Base.unregister_preview_interceptor` and `Base.unregister_preview_interceptors`.
+ This makes it possible to dynamically add and remove email observers and
+ interceptors at runtime in the same way they're registered.
- Also, it is possible to give a fallback.
+ *Claudio Ortolina*, *Kota Miyake*
- mailer/demo.pt.html.erb
- mailer/demo.pt-BR.html.erb
+* Rails 6 requires Ruby 2.4.1 or newer.
- So if the locale is `pt-PT`, `mailer/demo.pt.html.erb` will be rendered given
- the right I18n fallback configuration.
+ *Jeremy Daer*
- *Rafael Mendonça França*
-Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/actionmailer/CHANGELOG.md) for previous changes.
+Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/actionmailer/CHANGELOG.md) for previous changes.
diff --git a/actionmailer/MIT-LICENSE b/actionmailer/MIT-LICENSE
index 3ec7a617cf..1cb3add0fc 100644
--- a/actionmailer/MIT-LICENSE
+++ b/actionmailer/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2015 David Heinemeier Hansson
+Copyright (c) 2004-2018 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/actionmailer/README.rdoc b/actionmailer/README.rdoc
index e5c2ed8c77..14dfb82234 100644
--- a/actionmailer/README.rdoc
+++ b/actionmailer/README.rdoc
@@ -146,9 +146,9 @@ The Base class has the full list of configuration options. Here's an example:
The latest version of Action Mailer can be installed with RubyGems:
- % gem install actionmailer
+ $ gem install actionmailer
-Source code can be downloaded as part of the Rails project on GitHub
+Source code can be downloaded as part of the Rails project on GitHub:
* https://github.com/rails/rails/tree/master/actionmailer
@@ -157,7 +157,7 @@ Source code can be downloaded as part of the Rails project on GitHub
Action Mailer is released under the MIT license:
-* http://www.opensource.org/licenses/MIT
+* https://opensource.org/licenses/MIT
== Support
@@ -166,11 +166,10 @@ API documentation is at
* http://api.rubyonrails.org
-Bug reports can be filed for the Ruby on Rails project here:
+Bug reports for the Ruby on Rails project can be filed here:
* https://github.com/rails/rails/issues
Feature requests should be discussed on the rails-core mailing list here:
* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core
-
diff --git a/actionmailer/Rakefile b/actionmailer/Rakefile
index 7197ea5e27..6ac408e1cb 100644
--- a/actionmailer/Rakefile
+++ b/actionmailer/Rakefile
@@ -1,12 +1,16 @@
-require 'rake/testtask'
+# frozen_string_literal: true
+
+require "rake/testtask"
desc "Default Task"
task default: [ :test ]
+task :package
+
# Run the unit tests
Rake::TestTask.new { |t|
t.libs << "test"
- t.pattern = 'test/**/*_test.rb'
+ t.pattern = "test/**/*_test.rb"
t.warning = true
t.verbose = true
t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
@@ -15,7 +19,7 @@ Rake::TestTask.new { |t|
namespace :test do
task :isolated do
Dir.glob("test/**/*_test.rb").all? do |file|
- sh(Gem.ruby, '-w', '-Ilib:test', file)
- end or raise "Failures"
+ sh(Gem.ruby, "-w", "-Ilib:test", file)
+ end || raise("Failures")
end
end
diff --git a/actionmailer/actionmailer.gemspec b/actionmailer/actionmailer.gemspec
index 782b208ef4..6f8d6d0699 100644
--- a/actionmailer/actionmailer.gemspec
+++ b/actionmailer/actionmailer.gemspec
@@ -1,28 +1,38 @@
-version = File.read(File.expand_path('../../RAILS_VERSION', __FILE__)).strip
+# frozen_string_literal: true
+
+version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip
Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
- s.name = 'actionmailer'
+ s.name = "actionmailer"
s.version = version
- s.summary = 'Email composition, delivery, and receiving framework (part of Rails).'
- s.description = 'Email on Rails. Compose, deliver, receive, and test emails using the familiar controller/view pattern. First-class support for multipart email and attachments.'
+ s.summary = "Email composition, delivery, and receiving framework (part of Rails)."
+ s.description = "Email on Rails. Compose, deliver, receive, and test emails using the familiar controller/view pattern. First-class support for multipart email and attachments."
+
+ s.required_ruby_version = ">= 2.4.1"
+
+ s.license = "MIT"
- s.required_ruby_version = '>= 2.2.2'
+ s.author = "David Heinemeier Hansson"
+ s.email = "david@loudthinking.com"
+ s.homepage = "http://rubyonrails.org"
- s.license = 'MIT'
+ s.files = Dir["CHANGELOG.md", "README.rdoc", "MIT-LICENSE", "lib/**/*"]
+ s.require_path = "lib"
+ s.requirements << "none"
- s.author = 'David Heinemeier Hansson'
- s.email = 'david@loudthinking.com'
- s.homepage = 'http://www.rubyonrails.org'
+ s.metadata = {
+ "source_code_uri" => "https://github.com/rails/rails/tree/v#{version}/actionmailer",
+ "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/actionmailer/CHANGELOG.md"
+ }
- s.files = Dir['CHANGELOG.md', 'README.rdoc', 'MIT-LICENSE', 'lib/**/*']
- s.require_path = 'lib'
- s.requirements << 'none'
+ # NOTE: Please read our dependency guidelines before updating versions:
+ # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves
- s.add_dependency 'actionpack', version
- s.add_dependency 'actionview', version
- s.add_dependency 'activejob', version
+ s.add_dependency "actionpack", version
+ s.add_dependency "actionview", version
+ s.add_dependency "activejob", version
- s.add_dependency 'mail', ['~> 2.5', '>= 2.5.4']
- s.add_dependency 'rails-dom-testing', '~> 1.0', '>= 1.0.5'
+ s.add_dependency "mail", ["~> 2.5", ">= 2.5.4"]
+ s.add_dependency "rails-dom-testing", "~> 2.0"
end
diff --git a/actionmailer/bin/test b/actionmailer/bin/test
index 404cabba51..c53377cc97 100755
--- a/actionmailer/bin/test
+++ b/actionmailer/bin/test
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
-COMPONENT_ROOT = File.expand_path("../../", __FILE__)
-require File.expand_path("../tools/test", COMPONENT_ROOT)
-exit Minitest.run(ARGV)
+# frozen_string_literal: true
+
+COMPONENT_ROOT = File.expand_path("..", __dir__)
+require_relative "../../tools/test"
diff --git a/actionmailer/lib/action_mailer.rb b/actionmailer/lib/action_mailer.rb
index 291a8c1e34..69eae65d60 100644
--- a/actionmailer/lib/action_mailer.rb
+++ b/actionmailer/lib/action_mailer.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
#--
-# Copyright (c) 2004-2015 David Heinemeier Hansson
+# Copyright (c) 2004-2018 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -21,15 +23,16 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
-require 'abstract_controller'
-require 'action_mailer/version'
+require "abstract_controller"
+require "action_mailer/version"
# Common Active Support usage in Action Mailer
-require 'active_support/rails'
-require 'active_support/core_ext/class'
-require 'active_support/core_ext/module/attr_internal'
-require 'active_support/core_ext/string/inflections'
-require 'active_support/lazy_load_hooks'
+require "active_support"
+require "active_support/rails"
+require "active_support/core_ext/class"
+require "active_support/core_ext/module/attr_internal"
+require "active_support/core_ext/string/inflections"
+require "active_support/lazy_load_hooks"
module ActionMailer
extend ::ActiveSupport::Autoload
@@ -42,10 +45,25 @@ module ActionMailer
autoload :DeliveryMethods
autoload :InlinePreviewInterceptor
autoload :MailHelper
+ autoload :Parameterized
autoload :Preview
- autoload :Previews, 'action_mailer/preview'
+ autoload :Previews, "action_mailer/preview"
autoload :TestCase
autoload :TestHelper
autoload :MessageDelivery
autoload :DeliveryJob
+
+ def self.eager_load!
+ super
+
+ require "mail"
+ Mail.eager_autoload!
+ end
+end
+
+autoload :Mime, "action_dispatch/http/mime_type"
+
+ActiveSupport.on_load(:action_view) do
+ ActionView::Base.default_formats ||= Mime::SET.symbols
+ ActionView::Template::Types.delegate_to Mime
end
diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb
index 7022c04a9a..509d859ac3 100644
--- a/actionmailer/lib/action_mailer/base.rb
+++ b/actionmailer/lib/action_mailer/base.rb
@@ -1,10 +1,13 @@
-require 'mail'
-require 'action_mailer/collector'
-require 'active_support/core_ext/string/inflections'
-require 'active_support/core_ext/hash/except'
-require 'active_support/core_ext/module/anonymous'
+# frozen_string_literal: true
-require 'action_mailer/log_subscriber'
+require "mail"
+require "action_mailer/collector"
+require "active_support/core_ext/string/inflections"
+require "active_support/core_ext/hash/except"
+require "active_support/core_ext/module/anonymous"
+
+require "action_mailer/log_subscriber"
+require "action_mailer/rescuable"
module ActionMailer
# Action Mailer allows you to send email from your application using a mailer model and views.
@@ -56,7 +59,7 @@ module ActionMailer
# The hash passed to the mail method allows you to specify any header that a <tt>Mail::Message</tt>
# will accept (any valid email header including optional fields).
#
- # The mail method, if not passed a block, will inspect your views and send all the views with
+ # The +mail+ method, if not passed a block, will inspect your views and send all the views with
# the same name as the method, so the above action would send the +welcome.text.erb+ view
# file as well as the +welcome.html.erb+ view file in a +multipart/alternative+ email.
#
@@ -86,7 +89,7 @@ module ActionMailer
# Like Action Controller, each mailer class has a corresponding view directory in which each
# method of the class looks for a template with its name.
#
- # To define a template to be used with a mailing, create an <tt>.erb</tt> file with the same
+ # To define a template to be used with a mailer, create an <tt>.erb</tt> file with the same
# name as the method in your mailer model. For example, in the mailer defined above, the template at
# <tt>app/views/notifier_mailer/welcome.text.erb</tt> would be used to generate the email.
#
@@ -132,6 +135,11 @@ 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
#
# Once a mailer action and template are defined, you can deliver your message or defer its creation and
@@ -142,7 +150,7 @@ module ActionMailer
# mail.deliver_now # generates and sends the email now
#
# The <tt>ActionMailer::MessageDelivery</tt> class is a wrapper around a delegate that will call
- # your method to generate the mail. If you want direct access to delegator, or <tt>Mail::Message</tt>,
+ # your method to generate the mail. If you want direct access to the delegator, or <tt>Mail::Message</tt>,
# you can call the <tt>message</tt> method on the <tt>ActionMailer::MessageDelivery</tt> object.
#
# NotifierMailer.welcome(User.first).message # => a Mail::Message object
@@ -161,7 +169,7 @@ module ActionMailer
#
# Multipart messages can also be used implicitly because Action Mailer will automatically detect and use
# multipart templates, where each template is named after the name of the action, followed by the content
- # type. Each such detected template will be added as a separate part to the message.
+ # type. Each such detected template will be added to the message, as a separate part.
#
# For example, if the following templates exist:
# * signup_notification.text.erb
@@ -205,6 +213,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
@@ -272,22 +293,21 @@ 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
- # set something in the default using a proc, and then set the same thing inside of your
- # mailer method, it will get over written by the mailer method.
+ # 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.
#
# It is also possible to set these default options that will be used in all mailers through
# the <tt>default_options=</tt> configuration in <tt>config/application.rb</tt>:
@@ -296,7 +316,7 @@ module ActionMailer
#
# = Callbacks
#
- # You can specify callbacks using before_action and after_action for configuring your messages.
+ # You can specify callbacks using <tt>before_action</tt> and <tt>after_action</tt> for configuring your messages.
# This may be useful, for example, when you want to add default inline attachments for all
# messages sent out by a certain mailer class:
#
@@ -308,7 +328,6 @@ module ActionMailer
# end
#
# private
- #
# def add_inline_attachment!
# attachments.inline["footer.jpg"] = File.read('/path/to/filename.jpg')
# end
@@ -387,13 +406,13 @@ module ActionMailer
# to use it. Defaults to <tt>true</tt>.
# * <tt>:openssl_verify_mode</tt> - When using TLS, you can set how OpenSSL checks the certificate. This is
# really useful if you need to validate a self-signed and/or a wildcard certificate. You can use the name
- # of an OpenSSL verify constant (<tt>'none'</tt>, <tt>'peer'</tt>, <tt>'client_once'</tt>,
- # <tt>'fail_if_no_peer_cert'</tt>) or directly the constant (<tt>OpenSSL::SSL::VERIFY_NONE</tt>,
- # <tt>OpenSSL::SSL::VERIFY_PEER</tt>, ...).
+ # of an OpenSSL verify constant (<tt>'none'</tt> or <tt>'peer'</tt>) or directly the constant
+ # (<tt>OpenSSL::SSL::VERIFY_NONE</tt> or <tt>OpenSSL::SSL::VERIFY_PEER</tt>).
+ # <tt>:ssl/:tls</tt> Enables the SMTP connection to use SMTP/TLS (SMTPS: SMTP over direct TLS connection)
#
# * <tt>sendmail_settings</tt> - Allows you to override options for the <tt>:sendmail</tt> delivery method.
# * <tt>:location</tt> - The location of the sendmail executable. Defaults to <tt>/usr/sbin/sendmail</tt>.
- # * <tt>:arguments</tt> - The command line arguments. Defaults to <tt>-i -t</tt> with <tt>-f sender@address</tt>
+ # * <tt>:arguments</tt> - The command line arguments. Defaults to <tt>-i</tt> with <tt>-f sender@address</tt>
# added automatically before the message is sent.
#
# * <tt>file_settings</tt> - Allows you to override options for the <tt>:file</tt> delivery method.
@@ -414,9 +433,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!
@@ -428,6 +449,7 @@ module ActionMailer
include AbstractController::Translation
include AbstractController::AssetPaths
include AbstractController::Callbacks
+ include AbstractController::Caching
include ActionView::Layouts
@@ -439,10 +461,9 @@ module ActionMailer
helper ActionMailer::MailHelper
- private_class_method :new #:nodoc:
-
- class_attribute :default_params
- self.default_params = {
+ class_attribute :delivery_job, default: ::ActionMailer::DeliveryJob
+ class_attribute :parameterized_delivery_job, default: ::ActionMailer::Parameterized::DeliveryJob
+ class_attribute :default_params, default: {
mime_version: "1.0",
charset: "UTF-8",
content_type: "text/plain",
@@ -455,40 +476,60 @@ module ActionMailer
observers.flatten.compact.each { |observer| register_observer(observer) }
end
+ # Unregister one or more previously registered Observers.
+ def unregister_observers(*observers)
+ observers.flatten.compact.each { |observer| unregister_observer(observer) }
+ end
+
# Register one or more Interceptors which will be called before mail is sent.
def register_interceptors(*interceptors)
interceptors.flatten.compact.each { |interceptor| register_interceptor(interceptor) }
end
+ # Unregister one or more previously registered Interceptors.
+ def unregister_interceptors(*interceptors)
+ interceptors.flatten.compact.each { |interceptor| unregister_interceptor(interceptor) }
+ end
+
# Register an Observer which will be notified when mail is delivered.
# Either a class, string or symbol can be passed in as the Observer.
# If a string or symbol is passed in it will be camelized and constantized.
def register_observer(observer)
- delivery_observer = case observer
- when String, Symbol
- observer.to_s.camelize.constantize
- else
- observer
- end
+ Mail.register_observer(observer_class_for(observer))
+ end
- Mail.register_observer(delivery_observer)
+ # Unregister a previously registered Observer.
+ # Either a class, string or symbol can be passed in as the Observer.
+ # If a string or symbol is passed in it will be camelized and constantized.
+ def unregister_observer(observer)
+ Mail.unregister_observer(observer_class_for(observer))
end
# Register an Interceptor which will be called before mail is sent.
# Either a class, string or symbol can be passed in as the Interceptor.
# If a string or symbol is passed in it will be camelized and constantized.
def register_interceptor(interceptor)
- delivery_interceptor = case interceptor
- when String, Symbol
- interceptor.to_s.camelize.constantize
- else
- interceptor
- end
+ Mail.register_interceptor(observer_class_for(interceptor))
+ end
+
+ # Unregister a previously registered Interceptor.
+ # Either a class, string or symbol can be passed in as the Interceptor.
+ # If a string or symbol is passed in it will be camelized and constantized.
+ def unregister_interceptor(interceptor)
+ Mail.unregister_interceptor(observer_class_for(interceptor))
+ end
- Mail.register_interceptor(delivery_interceptor)
+ def observer_class_for(value) # :nodoc:
+ case value
+ when String, Symbol
+ value.to_s.camelize.constantize
+ else
+ value
+ end
end
+ private :observer_class_for
- # Returns the name of current mailer. This method is also being used as a path for a view lookup.
+ # Returns the name of the current mailer. This method is also being used as a path for a view lookup.
# If this is an anonymous mailer, this method will return +anonymous+ instead.
def mailer_name
@mailer_name ||= anonymous? ? "anonymous" : name.underscore
@@ -545,50 +586,47 @@ module ActionMailer
end
end
- def respond_to?(method, include_private = false) #:nodoc:
- super || action_methods.include?(method.to_s)
- end
-
- protected
-
- def set_payload_for_mail(payload, mail) #:nodoc:
- payload[:mailer] = name
- payload[:message_id] = mail.message_id
- payload[:subject] = mail.subject
- payload[:to] = mail.to
- payload[:from] = mail.from
- payload[:bcc] = mail.bcc if mail.bcc.present?
- payload[:cc] = mail.cc if mail.cc.present?
- payload[:date] = mail.date
- payload[:mail] = mail.encoded
+ private
+
+ def set_payload_for_mail(payload, mail)
+ payload[:mailer] = name
+ payload[:message_id] = mail.message_id
+ payload[:subject] = mail.subject
+ payload[:to] = mail.to
+ payload[:from] = mail.from
+ payload[:bcc] = mail.bcc if mail.bcc.present?
+ payload[:cc] = mail.cc if mail.cc.present?
+ payload[:date] = mail.date
+ payload[:mail] = mail.encoded
+ payload[:perform_deliveries] = mail.perform_deliveries
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
super
end
end
+
+ def respond_to_missing?(method, include_all = false)
+ action_methods.include?(method.to_s) || super
+ end
end
attr_internal :message
- # Instantiate a new mailer object. If +method_name+ is not +nil+, the mailer
- # will be initialized according to the named method. If not, the mailer will
- # remain uninitialized (useful when you only need to invoke the "receive"
- # method, for instance).
- def initialize(method_name=nil, *args)
+ def initialize
super()
@_mail_was_called = false
@_message = Mail.new
- process(method_name, *args) if method_name
end
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
@@ -598,10 +636,10 @@ module ActionMailer
end
class NullMail #:nodoc:
- def body; '' end
+ def body; "" end
def header; {} end
- def respond_to?(string, include_all=false)
+ def respond_to?(string, include_all = false)
true
end
@@ -661,21 +699,21 @@ module ActionMailer
#
# mail.attachments['filename.jpg'] = File.read('/path/to/filename.jpg')
#
- # If you do this, then Mail will take the file name and work out the mime type
- # set the Content-Type, Content-Disposition, Content-Transfer-Encoding and
- # base64 encode the contents of the attachment all for you.
+ # If you do this, then Mail will take the file name and work out the mime type.
+ # It will also set the Content-Type, Content-Disposition, Content-Transfer-Encoding
+ # and encode the contents of the attachment in Base64.
#
# You can also specify overrides if you want by passing a hash instead of a string:
#
- # mail.attachments['filename.jpg'] = {mime_type: 'application/x-gzip',
+ # mail.attachments['filename.jpg'] = {mime_type: 'application/gzip',
# content: File.read('/path/to/filename.jpg')}
#
- # If you want to use a different encoding than Base64, you can pass an encoding in,
- # but then it is up to you to pass in the content pre-encoded, and don't expect
- # Mail to know how to decode this data:
+ # If you want to use encoding other than Base64 then you will need to pass encoding
+ # type along with the pre-encoded content as Mail doesn't know how to decode the
+ # data:
#
# file_content = SpecialEncode(File.read('/path/to/filename.jpg'))
- # mail.attachments['filename.jpg'] = {mime_type: 'application/x-gzip',
+ # mail.attachments['filename.jpg'] = {mime_type: 'application/gzip',
# encoding: 'SpecialEncoding',
# content: file_content }
#
@@ -797,151 +835,183 @@ module ActionMailer
# end
#
def mail(headers = {}, &block)
- return @_message if @_mail_was_called && headers.blank? && !block
-
- m = @_message
+ return message if @_mail_was_called && headers.blank? && !block
# At the beginning, do not consider class default for content_type
content_type = headers[:content_type]
- # Call all the procs (if any)
- default_values = {}
- self.class.default.each do |k,v|
- default_values[k] = v.is_a?(Proc) ? instance_eval(&v) : v
- end
-
- # Handle defaults
- headers = headers.reverse_merge(default_values)
- headers[:subject] ||= default_i18n_subject
+ headers = apply_defaults(headers)
# Apply charset at the beginning so all fields are properly quoted
- m.charset = charset = headers[:charset]
+ message.charset = charset = headers[:charset]
# Set configure delivery behavior
- wrap_delivery_behavior!(headers.delete(:delivery_method), headers.delete(:delivery_method_options))
+ wrap_delivery_behavior!(headers[:delivery_method], headers[:delivery_method_options])
- # Assign all headers except parts_order, content_type and body
- assignable = headers.except(:parts_order, :content_type, :body, :template_name, :template_path)
- assignable.each { |k, v| m[k] = v }
+ assign_headers_to_message(message, headers)
# Render the templates and blocks
responses = collect_responses(headers, &block)
@_mail_was_called = true
- create_parts_from_responses(m, responses)
+ create_parts_from_responses(message, responses)
# Setup content type, reapply charset and handle parts order
- m.content_type = set_content_type(m, content_type, headers[:content_type])
- m.charset = charset
+ message.content_type = set_content_type(message, content_type, headers[:content_type])
+ message.charset = charset
- if m.multipart?
- m.body.set_sort_order(headers[:parts_order])
- m.body.sort_parts!
+ if message.multipart?
+ message.body.set_sort_order(headers[:parts_order])
+ message.body.sort_parts!
end
- m
+ message
end
- protected
-
- # Used by #mail to set the content type of the message.
- #
- # It will use the given +user_content_type+, or multipart if the mail
- # message has any attachments. If the attachments are inline, the content
- # type will be "multipart/related", otherwise "multipart/mixed".
- #
- # 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)
- params = m.content_type_parameters || {}
- case
- when user_content_type.present?
- user_content_type
- when m.has_attachments?
- if m.attachments.detect(&:inline?)
- ["multipart", "related", params]
+ private
+
+ # Used by #mail to set the content type of the message.
+ #
+ # It will use the given +user_content_type+, or multipart if the mail
+ # message has any attachments. If the attachments are inline, the content
+ # type will be "multipart/related", otherwise "multipart/mixed".
+ #
+ # 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) # :doc:
+ params = m.content_type_parameters || {}
+ case
+ when user_content_type.present?
+ user_content_type
+ when m.has_attachments?
+ if m.attachments.detect(&:inline?)
+ ["multipart", "related", params]
+ else
+ ["multipart", "mixed", params]
+ end
+ when m.multipart?
+ ["multipart", "alternative", params]
else
- ["multipart", "mixed", params]
+ m.content_type || class_default
end
- when m.multipart?
- ["multipart", "alternative", params]
- else
- m.content_type || class_default
end
- end
- # Translates the +subject+ using Rails I18n class under <tt>[mailer_scope, action_name]</tt> scope.
- # 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 = {})
- mailer_scope = self.class.mailer_name.tr('/', '.')
- I18n.t(:subject, interpolations.merge(scope: [mailer_scope, action_name], default: action_name.humanize))
- end
+ # Translates the +subject+ using Rails I18n class under <tt>[mailer_scope, action_name]</tt> scope.
+ # 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 = {}) # :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? # :doc:
+ false
+ end
+
+ def apply_defaults(headers)
+ default_values = self.class.default.map do |key, value|
+ [
+ key,
+ compute_default(value)
+ ]
+ end.to_h
+
+ headers_with_defaults = headers.reverse_merge(default_values)
+ headers_with_defaults[:subject] ||= default_i18n_subject
+ headers_with_defaults
+ end
- def collect_responses(headers) #:nodoc:
- responses = []
+ def compute_default(value)
+ return value unless value.is_a?(Proc)
+
+ if value.arity == 1
+ instance_exec(self, &value)
+ else
+ instance_exec(&value)
+ end
+ end
+
+ def assign_headers_to_message(message, headers)
+ assignable = headers.except(:parts_order, :content_type, :body, :template_name,
+ :template_path, :delivery_method, :delivery_method_options)
+ assignable.each { |k, v| message[k] = v }
+ end
+
+ def collect_responses(headers)
+ if block_given?
+ collector = ActionMailer::Collector.new(lookup_context) { render(action_name) }
+ yield(collector)
+ collector.responses
+ elsif headers[:body]
+ collect_responses_from_text(headers)
+ else
+ collect_responses_from_templates(headers)
+ end
+ end
- if block_given?
- collector = ActionMailer::Collector.new(lookup_context) { render(action_name) }
- yield(collector)
- responses = collector.responses
- elsif headers[:body]
- responses << {
+ def collect_responses_from_text(headers)
+ [{
body: headers.delete(:body),
- content_type: self.class.default[:content_type] || "text/plain"
- }
- else
- templates_path = headers.delete(:template_path) || self.class.mailer_name
- templates_name = headers.delete(:template_name) || action_name
+ content_type: headers[:content_type] || "text/plain"
+ }]
+ end
- each_template(Array(templates_path), templates_name) do |template|
- self.formats = template.formats
+ def collect_responses_from_templates(headers)
+ templates_path = headers[:template_path] || self.class.mailer_name
+ templates_name = headers[:template_name] || action_name
- responses << {
+ each_template(Array(templates_path), templates_name).map do |template|
+ self.formats = template.formats
+ {
body: render(template: template),
content_type: template.type.to_s
}
end
end
- responses
- end
+ def each_template(paths, name, &block)
+ templates = lookup_context.find_all(name, paths)
+ if templates.empty?
+ raise ActionView::MissingTemplate.new(paths, name, paths, false, "mailer")
+ else
+ templates.uniq(&:formats).each(&block)
+ end
+ end
- def each_template(paths, name, &block) #:nodoc:
- templates = lookup_context.find_all(name, paths)
- if templates.empty?
- raise ActionView::MissingTemplate.new(paths, name, paths, false, 'mailer')
- else
- templates.uniq(&:formats).each(&block)
+ def create_parts_from_responses(m, responses)
+ if responses.size == 1 && !m.has_attachments?
+ responses[0].each { |k, v| m[k] = v }
+ elsif responses.size > 1 && m.has_attachments?
+ container = Mail::Part.new
+ container.content_type = "multipart/alternative"
+ responses.each { |r| insert_part(container, r, m.charset) }
+ m.add_part(container)
+ else
+ responses.each { |r| insert_part(m, r, m.charset) }
+ end
end
- end
- def create_parts_from_responses(m, responses) #:nodoc:
- if responses.size == 1 && !m.has_attachments?
- responses[0].each { |k,v| m[k] = v }
- elsif responses.size > 1 && m.has_attachments?
- container = Mail::Part.new
- container.content_type = "multipart/alternative"
- responses.each { |r| insert_part(container, r, m.charset) }
- m.add_part(container)
- else
- responses.each { |r| insert_part(m, r, m.charset) }
+ def insert_part(container, response, charset)
+ response[:charset] ||= charset
+ part = Mail::Part.new(response)
+ container.add_part(part)
end
- end
- def insert_part(container, response, charset) #:nodoc:
- response[:charset] ||= charset
- part = Mail::Part.new(response)
- container.add_part(part)
- end
+ # This and #instrument_name is for caching instrument
+ def instrument_payload(key)
+ {
+ mailer: mailer_name,
+ key: key
+ }
+ end
- # Emails do not support relative path links.
- def self.supports_path?
- false
- end
+ def instrument_name
+ "action_mailer"
+ end
- ActiveSupport.run_load_hooks(:action_mailer, self)
+ ActiveSupport.run_load_hooks(:action_mailer, self)
end
end
diff --git a/actionmailer/lib/action_mailer/collector.rb b/actionmailer/lib/action_mailer/collector.rb
index e8883a8235..888410fa75 100644
--- a/actionmailer/lib/action_mailer/collector.rb
+++ b/actionmailer/lib/action_mailer/collector.rb
@@ -1,6 +1,8 @@
-require 'abstract_controller/collector'
-require 'active_support/core_ext/hash/reverse_merge'
-require 'active_support/core_ext/array/extract_options'
+# frozen_string_literal: true
+
+require "abstract_controller/collector"
+require "active_support/core_ext/hash/reverse_merge"
+require "active_support/core_ext/array/extract_options"
module ActionMailer
class Collector
diff --git a/actionmailer/lib/action_mailer/delivery_job.rb b/actionmailer/lib/action_mailer/delivery_job.rb
index 52772af2d3..40f26d8ad1 100644
--- a/actionmailer/lib/action_mailer/delivery_job.rb
+++ b/actionmailer/lib/action_mailer/delivery_job.rb
@@ -1,13 +1,36 @@
-require 'active_job'
+# frozen_string_literal: true
+
+require "active_job"
module ActionMailer
# The <tt>ActionMailer::DeliveryJob</tt> class is used when you
# want to send emails outside of the request-response cycle.
+ #
+ # Exceptions are rescued and handled by the mailer class.
class DeliveryJob < ActiveJob::Base # :nodoc:
queue_as { ActionMailer::Base.deliver_later_queue_name }
+ rescue_from StandardError, with: :handle_exception_with_mailer_class
+
def perform(mailer, mail_method, delivery_method, *args) #:nodoc:
mailer.constantize.public_send(mail_method, *args).send(delivery_method)
end
+
+ private
+ # "Deserialize" the mailer class name by hand in case another argument
+ # (like a Global ID reference) raised DeserializationError.
+ def mailer_class
+ if mailer = Array(@serialized_arguments).first || Array(arguments).first
+ mailer.constantize
+ end
+ end
+
+ def handle_exception_with_mailer_class(exception)
+ if klass = mailer_class
+ klass.handle_exception exception
+ else
+ raise exception
+ end
+ end
end
end
diff --git a/actionmailer/lib/action_mailer/delivery_methods.rb b/actionmailer/lib/action_mailer/delivery_methods.rb
index 4758b55a2a..5cd62307e6 100644
--- a/actionmailer/lib/action_mailer/delivery_methods.rb
+++ b/actionmailer/lib/action_mailer/delivery_methods.rb
@@ -1,4 +1,6 @@
-require 'tmpdir'
+# frozen_string_literal: true
+
+require "tmpdir"
module ActionMailer
# This module handles everything related to mail delivery, from registering
@@ -7,25 +9,18 @@ module ActionMailer
extend ActiveSupport::Concern
included do
- class_attribute :delivery_methods, :delivery_method
-
# Do not make this inheritable, because we always want it to propagate
- cattr_accessor :raise_delivery_errors
- self.raise_delivery_errors = true
-
- cattr_accessor :perform_deliveries
- self.perform_deliveries = true
-
- cattr_accessor :deliver_later_queue_name
- self.deliver_later_queue_name = :mailers
+ cattr_accessor :raise_delivery_errors, default: true
+ cattr_accessor :perform_deliveries, default: true
+ cattr_accessor :deliver_later_queue_name, default: :mailers
- self.delivery_methods = {}.freeze
- self.delivery_method = :smtp
+ class_attribute :delivery_methods, default: {}.freeze
+ class_attribute :delivery_method, default: :smtp
add_delivery_method :smtp, Mail::SMTP,
address: "localhost",
port: 25,
- domain: 'localhost.localdomain',
+ domain: "localhost.localdomain",
user_name: nil,
password: nil,
authentication: nil,
@@ -35,8 +30,8 @@ module ActionMailer
location: defined?(Rails.root) ? "#{Rails.root}/tmp/mails" : "#{Dir.tmpdir}/mails"
add_delivery_method :sendmail, Mail::Sendmail,
- location: '/usr/sbin/sendmail',
- arguments: '-i -t'
+ location: "/usr/sbin/sendmail",
+ arguments: "-i"
add_delivery_method :test, Mail::TestMailer
end
@@ -51,15 +46,15 @@ module ActionMailer
#
# add_delivery_method :sendmail, Mail::Sendmail,
# location: '/usr/sbin/sendmail',
- # arguments: '-i -t'
- def add_delivery_method(symbol, klass, default_options={})
+ # arguments: '-i'
+ def add_delivery_method(symbol, klass, default_options = {})
class_attribute(:"#{symbol}_settings") unless respond_to?(:"#{symbol}_settings")
send(:"#{symbol}_settings=", default_options)
self.delivery_methods = delivery_methods.merge(symbol.to_sym => klass).freeze
end
- def wrap_delivery_behavior(mail, method=nil, options=nil) # :nodoc:
- method ||= self.delivery_method
+ def wrap_delivery_behavior(mail, method = nil, options = nil) # :nodoc:
+ 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 ac79788cf0..72eb5d61e8 100644
--- a/actionmailer/lib/action_mailer/gem_version.rb
+++ b/actionmailer/lib/action_mailer/gem_version.rb
@@ -1,11 +1,13 @@
+# frozen_string_literal: true
+
module ActionMailer
- # Returns the version of the currently loaded Action Mailer as a <tt>Gem::Version</tt>
+ # Returns the version of the currently loaded Action Mailer as a <tt>Gem::Version</tt>.
def self.gem_version
Gem::Version.new VERSION::STRING
end
module VERSION
- MAJOR = 5
+ MAJOR = 6
MINOR = 0
TINY = 0
PRE = "alpha"
diff --git a/actionmailer/lib/action_mailer/inline_preview_interceptor.rb b/actionmailer/lib/action_mailer/inline_preview_interceptor.rb
index 6d02b39225..2b97ac5b94 100644
--- a/actionmailer/lib/action_mailer/inline_preview_interceptor.rb
+++ b/actionmailer/lib/action_mailer/inline_preview_interceptor.rb
@@ -1,9 +1,11 @@
-require 'base64'
+# frozen_string_literal: true
+
+require "base64"
module ActionMailer
# Implements a mailer preview interceptor that converts image tag src attributes
- # that use inline cid: style urls to data: style urls so that they are visible
- # when previewing a HTML email in a web browser.
+ # that use inline cid: style URLs to data: style URLs so that they are visible
+ # when previewing an HTML email in a web browser.
#
# This interceptor is enabled by default. To disable it, delete it from the
# <tt>ActionMailer::Base.preview_interceptors</tt> array:
@@ -11,7 +13,7 @@ module ActionMailer
# ActionMailer::Base.preview_interceptors.delete(ActionMailer::InlinePreviewInterceptor)
#
class InlinePreviewInterceptor
- PATTERN = /src=(?:"cid:[^"]+"|'cid:[^']+')/i
+ PATTERN = /src=(?:"cid:[^"]+"|'cid:[^']+')/i
include Base64
@@ -26,7 +28,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
@@ -38,24 +40,18 @@ module ActionMailer
end
private
- def message
- @message
- end
+ attr_reader :message
def html_part
@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
def find_part(cid)
- message.all_parts.find{ |p| p.attachment? && p.cid == cid }
+ message.all_parts.find { |p| p.attachment? && p.cid == cid }
end
end
end
diff --git a/actionmailer/lib/action_mailer/log_subscriber.rb b/actionmailer/lib/action_mailer/log_subscriber.rb
index c2f671fdac..25c99342c2 100644
--- a/actionmailer/lib/action_mailer/log_subscriber.rb
+++ b/actionmailer/lib/action_mailer/log_subscriber.rb
@@ -1,4 +1,6 @@
-require 'active_support/log_subscriber'
+# frozen_string_literal: true
+
+require "active_support/log_subscriber"
module ActionMailer
# Implements the ActiveSupport::LogSubscriber for logging notifications when
@@ -7,8 +9,13 @@ module ActionMailer
# An email was delivered.
def deliver(event)
info do
- recipients = Array(event.payload[:to]).join(', ')
- "\nSent mail to #{recipients} (#{event.duration.round(1)}ms)"
+ perform_deliveries = event.payload[:perform_deliveries]
+ recipients = Array(event.payload[:to]).join(", ")
+ if perform_deliveries
+ "Sent mail to #{recipients} (#{event.duration.round(1)}ms)"
+ else
+ "Skipped sending mail to #{recipients} as `perform_deliveries` is false"
+ end
end
debug { event.payload[:mail] }
@@ -16,7 +23,7 @@ module ActionMailer
# An email was received.
def receive(event)
- info { "\nReceived mail (#{event.duration.round(1)}ms)" }
+ info { "Received mail (#{event.duration.round(1)}ms)" }
debug { event.payload[:mail] }
end
@@ -25,11 +32,11 @@ module ActionMailer
debug do
mailer = event.payload[:mailer]
action = event.payload[:action]
- "\n#{mailer}##{action}: processed outbound mail in #{event.duration.round(1)}ms"
+ "#{mailer}##{action}: processed outbound mail in #{event.duration.round(1)}ms"
end
end
- # Use the logger configured for ActionMailer::Base
+ # Use the logger configured for ActionMailer::Base.
def logger
ActionMailer::Base.logger
end
diff --git a/actionmailer/lib/action_mailer/mail_helper.rb b/actionmailer/lib/action_mailer/mail_helper.rb
index 239974e7b1..e7bed41f8d 100644
--- a/actionmailer/lib/action_mailer/mail_helper.rb
+++ b/actionmailer/lib/action_mailer/mail_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionMailer
# Provides helper methods for ActionMailer::Base that can be used for easily
# formatting messages, accessing mailer or message instances, and the
@@ -54,7 +56,7 @@ module ActionMailer
sentences = [[]]
text.split.each do |word|
- if sentences.first.present? && (sentences.last + [word]).join(' ').length > len
+ if sentences.first.present? && (sentences.last + [word]).join(" ").length > len
sentences << [word]
else
sentences.last << word
diff --git a/actionmailer/lib/action_mailer/message_delivery.rb b/actionmailer/lib/action_mailer/message_delivery.rb
index ff2cb0fd01..a57980c322 100644
--- a/actionmailer/lib/action_mailer/message_delivery.rb
+++ b/actionmailer/lib/action_mailer/message_delivery.rb
@@ -1,9 +1,10 @@
-require 'delegate'
+# frozen_string_literal: true
-module ActionMailer
+require "delegate"
+module ActionMailer
# The <tt>ActionMailer::MessageDelivery</tt> class is used by
- # <tt>ActionMailer::Base</tt> when creating a new mailer.
+ # ActionMailer::Base when creating a new mailer.
# <tt>MessageDelivery</tt> is a wrapper (+Delegator+ subclass) around a lazy
# created <tt>Mail::Message</tt>. You can get direct access to the
# <tt>Mail::Message</tt>, deliver the email or schedule the email to be sent
@@ -14,25 +15,35 @@ module ActionMailer
# Notifier.welcome(User.first).deliver_later # enqueue email delivery as a job through Active Job
# Notifier.welcome(User.first).message # a Mail::Message object
class MessageDelivery < Delegator
- def initialize(mailer, mail_method, *args) #:nodoc:
- @mailer = mailer
- @mail_method = mail_method
- @args = args
+ def initialize(mailer_class, action, *args) #:nodoc:
+ @mailer_class, @action, @args = mailer_class, action, args
+
+ # The mail is only processed if we try to call any methods on it.
+ # Typical usage will leave it unloaded and call deliver_later.
+ @processed_mailer = nil
+ @mail_message = nil
end
+ # Method calls are delegated to the Mail::Message that's ready to deliver.
def __getobj__ #:nodoc:
- @obj ||= @mailer.send(:new, @mail_method, *@args).message
+ @mail_message ||= processed_mailer.message
end
- def __setobj__(obj) #:nodoc:
- @obj = obj
+ # Unused except for delegator internals (dup, marshalling).
+ def __setobj__(mail_message) #:nodoc:
+ @mail_message = mail_message
end
- # Returns the Mail::Message object
+ # Returns the resulting Mail::Message
def message
__getobj__
end
+ # Was the delegate loaded, causing the mailer action to be processed?
+ def processed?
+ @processed_mailer || @mail_message
+ end
+
# Enqueues the email to be delivered through Active Job. When the
# job runs it will send the email using +deliver_now!+. That means
# that the message will be sent bypassing checking +perform_deliveries+
@@ -47,7 +58,15 @@ module ActionMailer
# * <tt>:wait</tt> - Enqueue the email to be delivered with a delay
# * <tt>:wait_until</tt> - Enqueue the email to be delivered at (after) a specific date / time
# * <tt>:queue</tt> - Enqueue the email on the specified queue
- def deliver_later!(options={})
+ #
+ # By default, the email will be enqueued using <tt>ActionMailer::DeliveryJob</tt>. Each
+ # <tt>ActionMailer::Base</tt> class can specify the job to use by setting the class variable
+ # +delivery_job+.
+ #
+ # class AccountRegistrationMailer < ApplicationMailer
+ # self.delivery_job = RegistrationDeliveryJob
+ # end
+ def deliver_later!(options = {})
enqueue_delivery :deliver_now!, options
end
@@ -60,10 +79,18 @@ module ActionMailer
#
# Options:
#
- # * <tt>:wait</tt> - Enqueue the email to be delivered with a delay
- # * <tt>:wait_until</tt> - Enqueue the email to be delivered at (after) a specific date / time
- # * <tt>:queue</tt> - Enqueue the email on the specified queue
- def deliver_later(options={})
+ # * <tt>:wait</tt> - Enqueue the email to be delivered with a delay.
+ # * <tt>:wait_until</tt> - Enqueue the email to be delivered at (after) a specific date / time.
+ # * <tt>:queue</tt> - Enqueue the email on the specified queue.
+ #
+ # By default, the email will be enqueued using <tt>ActionMailer::DeliveryJob</tt>. Each
+ # <tt>ActionMailer::Base</tt> class can specify the job to use by setting the class variable
+ # +delivery_job+.
+ #
+ # class AccountRegistrationMailer < ApplicationMailer
+ # self.delivery_job = RegistrationDeliveryJob
+ # end
+ def deliver_later(options = {})
enqueue_delivery :deliver_now, options
end
@@ -73,7 +100,9 @@ module ActionMailer
# Notifier.welcome(User.first).deliver_now!
#
def deliver_now!
- message.deliver!
+ processed_mailer.handle_exceptions do
+ message.deliver!
+ end
end
# Delivers an email:
@@ -81,14 +110,35 @@ module ActionMailer
# Notifier.welcome(User.first).deliver_now
#
def deliver_now
- message.deliver
+ processed_mailer.handle_exceptions do
+ message.deliver
+ end
end
private
+ # Returns the processed Mailer instance. We keep this instance
+ # on hand so we can delegate exception handling to it.
+ def processed_mailer
+ @processed_mailer ||= @mailer_class.new.tap do |mailer|
+ mailer.process @action, *@args
+ end
+ end
- def enqueue_delivery(delivery_method, options={})
- args = @mailer.name, @mail_method.to_s, delivery_method.to_s, *@args
- ActionMailer::DeliveryJob.set(options).perform_later(*args)
+ def enqueue_delivery(delivery_method, options = {})
+ if processed?
+ ::Kernel.raise "You've accessed the message before asking to " \
+ "deliver it later, so you may have made local changes that would " \
+ "be silently lost if we enqueued a job to deliver it. Why? Only " \
+ "the mailer method *arguments* are passed with the delivery job! " \
+ "Do not access the message in any way if you mean to deliver it " \
+ "later. Workarounds: 1. don't touch the message before calling " \
+ "#deliver_later, 2. only touch the message *within your mailer " \
+ "method*, or 3. use a custom Active Job instead of #deliver_later."
+ else
+ args = @mailer_class.name, @action.to_s, delivery_method.to_s, *@args
+ job = @mailer_class.delivery_job
+ job.set(options).perform_later(*args)
+ end
end
end
end
diff --git a/actionmailer/lib/action_mailer/parameterized.rb b/actionmailer/lib/action_mailer/parameterized.rb
new file mode 100644
index 0000000000..0fe417affe
--- /dev/null
+++ b/actionmailer/lib/action_mailer/parameterized.rb
@@ -0,0 +1,155 @@
+# frozen_string_literal: true
+
+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
+ job = @mailer_class.parameterized_delivery_job
+ job.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 25ad7ee721..500b3bede0 100644
--- a/actionmailer/lib/action_mailer/preview.rb
+++ b/actionmailer/lib/action_mailer/preview.rb
@@ -1,4 +1,6 @@
-require 'active_support/descendants_tracker'
+# frozen_string_literal: true
+
+require "active_support/descendants_tracker"
module ActionMailer
module Previews #:nodoc:
@@ -15,13 +17,12 @@ module ActionMailer
#
# config.action_mailer.show_previews = true
#
- # Defaults to true for development environment
+ # Defaults to +true+ for development environment
#
mattr_accessor :show_previews, instance_writer: false
# :nodoc:
- mattr_accessor :preview_interceptors, instance_writer: false
- self.preview_interceptors = [ActionMailer::InlinePreviewInterceptor]
+ mattr_accessor :preview_interceptors, instance_writer: false, default: [ActionMailer::InlinePreviewInterceptor]
end
module ClassMethods
@@ -30,29 +31,53 @@ module ActionMailer
interceptors.flatten.compact.each { |interceptor| register_preview_interceptor(interceptor) }
end
+ # Unregister one or more previously registered Interceptors.
+ def unregister_preview_interceptors(*interceptors)
+ interceptors.flatten.compact.each { |interceptor| unregister_preview_interceptor(interceptor) }
+ end
+
# Register an Interceptor which will be called before mail is previewed.
# Either a class or a string can be passed in as the Interceptor. If a
- # string is passed in it will be <tt>constantize</tt>d.
+ # string is passed in it will be constantized.
def register_preview_interceptor(interceptor)
- preview_interceptor = case interceptor
+ preview_interceptor = interceptor_class_for(interceptor)
+
+ unless preview_interceptors.include?(preview_interceptor)
+ preview_interceptors << preview_interceptor
+ end
+ end
+
+ # Unregister a previously registered Interceptor.
+ # Either a class or a string can be passed in as the Interceptor. If a
+ # string is passed in it will be constantized.
+ def unregister_preview_interceptor(interceptor)
+ preview_interceptors.delete(interceptor_class_for(interceptor))
+ end
+
+ private
+
+ def interceptor_class_for(interceptor)
+ case interceptor
when String, Symbol
interceptor.to_s.camelize.constantize
else
interceptor
end
-
- unless preview_interceptors.include?(preview_interceptor)
- preview_interceptors << preview_interceptor
end
- end
end
end
class Preview
extend ActiveSupport::DescendantsTracker
+ attr_reader :params
+
+ def initialize(params = {})
+ @params = params
+ end
+
class << self
- # Returns all mailer preview classes
+ # Returns all mailer preview classes.
def all
load_previews if descendants.empty?
descendants
@@ -61,54 +86,54 @@ module ActionMailer
# Returns the mail object for the given email name. The registered preview
# 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
+ def call(email, params = {})
+ preview = new(params)
message = preview.public_send(email)
inform_preview_interceptors(message)
message
end
- # Returns all of the available email previews
+ # Returns all of the available email previews.
def emails
public_instance_methods(false).map(&:to_s).sort
end
- # Returns true if the email exists
+ # Returns +true+ if the email exists.
def email_exists?(email)
emails.include?(email)
end
- # Returns true if the preview exists
+ # Returns +true+ if the preview exists.
def exists?(preview)
- all.any?{ |p| p.preview_name == preview }
+ all.any? { |p| p.preview_name == preview }
end
- # Find a mailer preview by its underscored class name
+ # Find a mailer preview by its underscored class name.
def find(preview)
- all.find{ |p| p.preview_name == preview }
+ all.find { |p| p.preview_name == preview }
end
- # Returns the underscored name of the mailer preview without the suffix
+ # Returns the underscored name of the mailer preview without the suffix.
def preview_name
- name.sub(/Preview$/, '').underscore
+ 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 }
+ Dir["#{preview_path}/**/*_preview.rb"].sort.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/railtie.rb b/actionmailer/lib/action_mailer/railtie.rb
index bebcf4de01..bb6141b406 100644
--- a/actionmailer/lib/action_mailer/railtie.rb
+++ b/actionmailer/lib/action_mailer/railtie.rb
@@ -1,4 +1,6 @@
-require 'active_job/railtie'
+# frozen_string_literal: true
+
+require "active_job/railtie"
require "action_mailer"
require "rails"
require "abstract_controller/railties/routes_helpers"
@@ -16,13 +18,19 @@ module ActionMailer
paths = app.config.paths
options = app.config.action_mailer
+ if app.config.force_ssl
+ options.default_url_options ||= {}
+ options.default_url_options[:protocol] ||= "https"
+ end
+
options.assets_dir ||= paths["public"].first
options.javascripts_dir ||= paths["public/javascripts"].first
options.stylesheets_dir ||= paths["public/stylesheets"].first
options.show_previews = Rails.env.development? if options.show_previews.nil?
+ options.cache_store ||= Rails.cache
if options.show_previews
- options.preview_path ||= defined?(Rails.root) ? "#{Rails.root}/test/mailers/previews" : nil
+ options.preview_path ||= defined?(Rails.root) ? "#{Rails.root}/test/mailers/previews" : nil
end
# make sure readers methods get compiled
@@ -38,14 +46,12 @@ module ActionMailer
register_preview_interceptors(options.delete(:preview_interceptors))
register_observers(options.delete(:observers))
- options.each { |k,v| send("#{k}=", v) }
+ options.each { |k, v| send("#{k}=", v) }
+ end
- if options.show_previews
- app.routes.prepend do
- get '/rails/mailers' => "rails/mailers#index"
- get '/rails/mailers/*path' => "rails/mailers#preview"
- end
- end
+ ActiveSupport.on_load(:action_dispatch_integration_test) do
+ include ActionMailer::TestHelper
+ include ActionMailer::TestCase::ClearTestDeliveries
end
end
@@ -55,9 +61,24 @@ module ActionMailer
end
end
- config.after_initialize do
- if ActionMailer::Base.preview_path
- ActiveSupport::Dependencies.autoload_paths << ActionMailer::Base.preview_path
+ initializer "action_mailer.eager_load_actions" do
+ ActiveSupport.on_load(:after_initialize) do
+ ActionMailer::Base.descendants.each(&:action_methods) if config.eager_load
+ end
+ end
+
+ config.after_initialize do |app|
+ options = app.config.action_mailer
+
+ if options.show_previews
+ app.routes.prepend do
+ get "/rails/mailers" => "rails/mailers#index", internal: true
+ get "/rails/mailers/*path" => "rails/mailers#preview", internal: true
+ end
+
+ if options.preview_path
+ ActiveSupport::Dependencies.autoload_paths << options.preview_path
+ end
end
end
end
diff --git a/actionmailer/lib/action_mailer/rescuable.rb b/actionmailer/lib/action_mailer/rescuable.rb
new file mode 100644
index 0000000000..5b567eb500
--- /dev/null
+++ b/actionmailer/lib/action_mailer/rescuable.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module ActionMailer #:nodoc:
+ # Provides +rescue_from+ for mailers. Wraps mailer action processing,
+ # mail job processing, and mail delivery.
+ module Rescuable
+ extend ActiveSupport::Concern
+ include ActiveSupport::Rescuable
+
+ class_methods do
+ def handle_exception(exception) #:nodoc:
+ rescue_with_handler(exception) || raise(exception)
+ end
+ end
+
+ def handle_exceptions #:nodoc:
+ yield
+ rescue => exception
+ rescue_with_handler(exception) || raise
+ end
+
+ private
+ def process(*)
+ handle_exceptions do
+ super
+ end
+ end
+ end
+end
diff --git a/actionmailer/lib/action_mailer/test_case.rb b/actionmailer/lib/action_mailer/test_case.rb
index 766215ce96..ee5a864847 100644
--- a/actionmailer/lib/action_mailer/test_case.rb
+++ b/actionmailer/lib/action_mailer/test_case.rb
@@ -1,16 +1,35 @@
-require 'active_support/test_case'
-require 'rails-dom-testing'
+# frozen_string_literal: true
+
+require "active_support/test_case"
+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
class TestCase < ActiveSupport::TestCase
+ module ClearTestDeliveries
+ extend ActiveSupport::Concern
+
+ included do
+ setup :clear_test_deliveries
+ teardown :clear_test_deliveries
+ end
+
+ private
+
+ def clear_test_deliveries
+ if ActionMailer::Base.delivery_method == :test
+ ActionMailer::Base.deliveries.clear
+ end
+ end
+ end
+
module Behavior
extend ActiveSupport::Concern
@@ -24,6 +43,7 @@ module ActionMailer
setup :initialize_test_deliveries
setup :set_expected_mail
teardown :restore_test_deliveries
+ ActiveSupport.run_load_hooks(:action_mailer_test_case, self)
end
module ClassMethods
@@ -39,7 +59,7 @@ module ActionMailer
end
def mailer_class
- if mailer = self._mailer_class
+ if mailer = _mailer_class
mailer
else
tests determine_default_mailer(name)
@@ -55,18 +75,18 @@ module ActionMailer
end
end
- protected
+ private
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
restore_delivery_method
ActionMailer::Base.perform_deliveries = @old_perform_deliveries
- ActionMailer::Base.deliveries.clear
end
def set_delivery_method(method)
@@ -75,17 +95,16 @@ module ActionMailer
end
def restore_delivery_method
+ ActionMailer::Base.deliveries.clear
ActionMailer::Base.delivery_method = @old_delivery_method
end
def set_expected_mail
@expected = Mail.new
@expected.content_type ["text", "plain", { "charset" => charset }]
- @expected.mime_version = '1.0'
+ @expected.mime_version = "1.0"
end
- private
-
def charset
"UTF-8"
end
@@ -95,7 +114,7 @@ module ActionMailer
end
def read_fixture(action)
- IO.readlines(File.join(Rails.root, 'test', 'fixtures', self.class.mailer_class.name.underscore, action))
+ IO.readlines(File.join(Rails.root, "test", "fixtures", self.class.mailer_class.name.underscore, action))
end
end
diff --git a/actionmailer/lib/action_mailer/test_helper.rb b/actionmailer/lib/action_mailer/test_helper.rb
index 4d03a616d2..a4751916af 100644
--- a/actionmailer/lib/action_mailer/test_helper.rb
+++ b/actionmailer/lib/action_mailer/test_helper.rb
@@ -1,8 +1,10 @@
-require 'active_job'
+# frozen_string_literal: true
+
+require "active_job"
module ActionMailer
# Provides helper methods for testing Action Mailer, including #assert_emails
- # and #assert_no_emails
+ # and #assert_no_emails.
module TestHelper
include ActiveJob::TestHelper
@@ -26,13 +28,13 @@ module ActionMailer
#
# assert_emails 2 do
# ContactMailer.welcome.deliver_now
- # ContactMailer.welcome.deliver_now
+ # ContactMailer.welcome.deliver_later
# end
# end
- def assert_emails(number)
+ def assert_emails(number, &block)
if block_given?
original_count = ActionMailer::Base.deliveries.size
- yield
+ perform_enqueued_jobs(only: [ActionMailer::DeliveryJob, ActionMailer::Parameterized::DeliveryJob], &block)
new_count = ActionMailer::Base.deliveries.size
assert_equal number, new_count - original_count, "#{number} emails expected, but #{new_count - original_count} were sent"
else
@@ -40,7 +42,7 @@ module ActionMailer
end
end
- # Assert that no emails have been sent.
+ # Asserts that no emails have been sent.
#
# def test_emails
# assert_no_emails
@@ -58,7 +60,7 @@ module ActionMailer
#
# Note: This assertion is simply a shortcut for:
#
- # assert_emails 0
+ # assert_emails 0, &block
def assert_no_emails(&block)
assert_emails 0, &block
end
@@ -88,7 +90,49 @@ 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 a specific email has been enqueued, optionally
+ # matching arguments.
+ #
+ # def test_email
+ # ContactMailer.welcome.deliver_later
+ # assert_enqueued_email_with ContactMailer, :welcome
+ # end
+ #
+ # def test_email_with_arguments
+ # ContactMailer.welcome("Hello", "Goodbye").deliver_later
+ # assert_enqueued_email_with ContactMailer, :welcome, args: ["Hello", "Goodbye"]
+ # end
+ #
+ # If a block is passed, that block should cause the specified email
+ # to be enqueued.
+ #
+ # def test_email_in_block
+ # assert_enqueued_email_with ContactMailer, :welcome do
+ # ContactMailer.welcome.deliver_later
+ # end
+ # end
+ #
+ # If +args+ is provided as a Hash, a parameterized email is matched.
+ #
+ # def test_parameterized_email
+ # assert_enqueued_email_with ContactMailer, :welcome,
+ # args: {email: 'user@example.com'} do
+ # ContactMailer.with(email: 'user@example.com').welcome.deliver_later
+ # end
+ # end
+ def assert_enqueued_email_with(mailer, method, args: nil, queue: "mailers", &block)
+ if args.is_a? Hash
+ job = ActionMailer::Parameterized::DeliveryJob
+ args = [mailer.to_s, method.to_s, "deliver_now", args]
+ else
+ job = ActionMailer::DeliveryJob
+ args = [mailer.to_s, method.to_s, "deliver_now", *args]
+ end
+
+ assert_enqueued_with(job: job, args: args, queue: queue, &block)
end
# Asserts that no emails are enqueued for later delivery.
@@ -107,7 +151,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/action_mailer/version.rb b/actionmailer/lib/action_mailer/version.rb
index 06f80a8fdc..4549d6eb57 100644
--- a/actionmailer/lib/action_mailer/version.rb
+++ b/actionmailer/lib/action_mailer/version.rb
@@ -1,4 +1,6 @@
-require_relative 'gem_version'
+# frozen_string_literal: true
+
+require_relative "gem_version"
module ActionMailer
# Returns the version of the currently loaded Action Mailer as a
diff --git a/actionmailer/lib/rails/generators/mailer/mailer_generator.rb b/actionmailer/lib/rails/generators/mailer/mailer_generator.rb
index 3ec7d3d896..c37a74c762 100644
--- a/actionmailer/lib/rails/generators/mailer/mailer_generator.rb
+++ b/actionmailer/lib/rails/generators/mailer/mailer_generator.rb
@@ -1,24 +1,37 @@
+# frozen_string_literal: true
+
module Rails
module Generators
class MailerGenerator < NamedBase
- source_root File.expand_path("../templates", __FILE__)
+ source_root File.expand_path("templates", __dir__)
argument :actions, type: :array, default: [], banner: "method method"
check_class_collision suffix: "Mailer"
def create_mailer_file
- template "mailer.rb", File.join('app/mailers', class_path, "#{file_name}_mailer.rb")
- if self.behavior == :invoke
- template "application_mailer.rb", 'app/mailers/application_mailer.rb'
+ template "mailer.rb", File.join("app/mailers", class_path, "#{file_name}_mailer.rb")
+
+ in_root do
+ if behavior == :invoke && !File.exist?(application_mailer_file_name)
+ template "application_mailer.rb", application_mailer_file_name
+ end
end
end
hook_for :template_engine, :test_framework
- protected
- def file_name
- @_file_name ||= super.gsub(/\_mailer/i, '')
+ private
+ def file_name # :doc:
+ @_file_name ||= super.sub(/_mailer\z/i, "")
+ end
+
+ def application_mailer_file_name
+ @_application_mailer_file_name ||= if mountable_engine?
+ "app/mailers/#{namespaced_path}/application_mailer.rb"
+ else
+ "app/mailers/application_mailer.rb"
+ end
end
end
end
diff --git a/actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb b/actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb
deleted file mode 100644
index d25d8892dd..0000000000
--- a/actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-class ApplicationMailer < ActionMailer::Base
- default from: "from@example.com"
- layout 'mailer'
-end
diff --git a/actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb.tt b/actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb.tt
new file mode 100644
index 0000000000..00fb9bd48f
--- /dev/null
+++ b/actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb.tt
@@ -0,0 +1,6 @@
+<% module_namespacing do -%>
+class ApplicationMailer < ActionMailer::Base
+ default from: 'from@example.com'
+ layout 'mailer'
+end
+<% end %>
diff --git a/actionmailer/lib/rails/generators/mailer/templates/mailer.rb b/actionmailer/lib/rails/generators/mailer/templates/mailer.rb.tt
index 348d314758..348d314758 100644
--- a/actionmailer/lib/rails/generators/mailer/templates/mailer.rb
+++ b/actionmailer/lib/rails/generators/mailer/templates/mailer.rb.tt
diff --git a/actionmailer/test/abstract_unit.rb b/actionmailer/test/abstract_unit.rb
index 706249a2f6..f647896374 100644
--- a/actionmailer/test/abstract_unit.rb
+++ b/actionmailer/test/abstract_unit.rb
@@ -1,19 +1,27 @@
-require File.expand_path('../../../load_paths', __FILE__)
-require 'active_support/core_ext/kernel/reporting'
+# frozen_string_literal: true
+
+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'
-require 'action_mailer'
-require 'action_mailer/test_case'
+module Rails
+ def self.root
+ File.expand_path("..", __dir__)
+ end
+end
+
+require "active_support/testing/autorun"
+require "active_support/testing/method_call_assertions"
+require "action_mailer"
+require "action_mailer/test_case"
# Emulate AV railtie
-require 'action_view'
+require "action_view"
ActionMailer::Base.include(ActionView::Layouts)
# Show backtraces for deprecated behavior for quicker cleanup.
@@ -22,22 +30,20 @@ ActiveSupport::Deprecation.debug = true
# Disable available locale checks to avoid warnings running the test suite.
I18n.enforce_available_locales = false
-FIXTURE_LOAD_PATH = File.expand_path('fixtures', File.dirname(__FILE__))
+FIXTURE_LOAD_PATH = File.expand_path("fixtures", __dir__)
ActionMailer::Base.view_paths = FIXTURE_LOAD_PATH
-module Rails
- def self.root
- File.expand_path('../', File.dirname(__FILE__))
- end
-end
+class ActiveSupport::TestCase
+ include ActiveSupport::Testing::MethodCallAssertions
-# 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
+ private
+ # Skips the current run on Rubinius using Minitest::Assertions#skip
+ def rubinius_skip(message = "")
+ skip message if RUBY_ENGINE == "rbx"
+ end
-require 'mocha/setup' # FIXME: stop using mocha
+ # Skips the current run on JRuby using Minitest::Assertions#skip
+ def jruby_skip(message = "")
+ skip message if defined?(JRUBY_VERSION)
+ end
+end
diff --git a/actionmailer/test/assert_select_email_test.rb b/actionmailer/test/assert_select_email_test.rb
index cae2e20abd..9699fe4000 100644
--- a/actionmailer/test/assert_select_email_test.rb
+++ b/actionmailer/test/assert_select_email_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class AssertSelectEmailTest < ActionMailer::TestCase
class AssertSelectMailer < ActionMailer::Base
@@ -11,8 +13,8 @@ class AssertSelectEmailTest < ActionMailer::TestCase
class AssertMultipartSelectMailer < ActionMailer::Base
def test(options)
mail subject: "Test e-mail", from: "test@test.host", to: "test <test@test.host>" do |format|
- format.text { render text: options[:text] }
- format.html { render text: options[:html] }
+ format.text { render plain: options[:text] }
+ format.html { render plain: options[:html] }
end
end
end
@@ -23,7 +25,7 @@ class AssertSelectEmailTest < ActionMailer::TestCase
def test_assert_select_email
assert_raise ActiveSupport::TestCase::Assertion do
- assert_select_email {}
+ assert_select_email { }
end
AssertSelectMailer.test("<div><p>foo</p><p>bar</p></div>").deliver_now
@@ -36,7 +38,7 @@ class AssertSelectEmailTest < ActionMailer::TestCase
end
def test_assert_select_email_multipart
- AssertMultipartSelectMailer.test(html: "<div><p>foo</p><p>bar</p></div>", text: 'foo bar').deliver_now
+ AssertMultipartSelectMailer.test(html: "<div><p>foo</p><p>bar</p></div>", text: "foo bar").deliver_now
assert_select_email do
assert_select "div:root" do
assert_select "p:first-child", "foo"
diff --git a/actionmailer/test/asset_host_test.rb b/actionmailer/test/asset_host_test.rb
index 10cfdcf693..9cd8cae88c 100644
--- a/actionmailer/test/asset_host_test.rb
+++ b/actionmailer/test/asset_host_test.rb
@@ -1,11 +1,13 @@
-require 'abstract_unit'
-require 'action_controller'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "action_controller"
class AssetHostMailer < ActionMailer::Base
def email_with_asset
- mail to: 'test@localhost',
- subject: 'testing email containing asset path while asset_host is set',
- from: 'tester@example.com'
+ mail to: "test@localhost",
+ subject: "testing email containing asset path while asset_host is set",
+ from: "tester@example.com"
end
end
@@ -22,16 +24,16 @@ class AssetHostTest < ActionMailer::TestCase
def test_asset_host_as_string
mail = AssetHostMailer.email_with_asset
- assert_dom_equal %Q{<img alt="Somelogo" src="http://www.example.com/images/somelogo.png" />}, mail.body.to_s.strip
+ assert_dom_equal '<img src="http://www.example.com/images/somelogo.png" />', mail.body.to_s.strip
end
def test_asset_host_as_one_argument_proc
AssetHostMailer.config.asset_host = Proc.new { |source|
- if source.starts_with?('/images')
- 'http://images.example.com'
+ if source.starts_with?("/images")
+ "http://images.example.com"
end
}
mail = AssetHostMailer.email_with_asset
- assert_dom_equal %Q{<img alt="Somelogo" src="http://images.example.com/images/somelogo.png" />}, mail.body.to_s.strip
+ assert_dom_equal '<img src="http://images.example.com/images/somelogo.png" />', mail.body.to_s.strip
end
end
diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb
index 59c5638f96..cbbee9fae8 100644
--- a/actionmailer/test/base_test.rb
+++ b/actionmailer/test/base_test.rb
@@ -1,12 +1,14 @@
-require 'abstract_unit'
-require 'set'
+# frozen_string_literal: true
-require 'action_dispatch'
-require 'active_support/time'
+require "abstract_unit"
+require "set"
-require 'mailers/base_mailer'
-require 'mailers/proc_mailer'
-require 'mailers/asset_mailer'
+require "action_dispatch"
+require "active_support/time"
+
+require "mailers/base_mailer"
+require "mailers/proc_mailer"
+require "mailers/asset_mailer"
class BaseTest < ActiveSupport::TestCase
include Rails::Dom::Testing::Assertions::DomAssertions
@@ -32,33 +34,33 @@ class BaseTest < ActiveSupport::TestCase
# Basic mail usage without block
test "mail() should set the headers of the mail message" do
email = BaseMailer.welcome
- assert_equal(['system@test.lindsaar.net'], email.to)
- assert_equal(['jose@test.plataformatec.com'], email.from)
- assert_equal('The first email on new API!', email.subject)
+ assert_equal(["system@test.lindsaar.net"], email.to)
+ assert_equal(["jose@test.plataformatec.com"], email.from)
+ assert_equal("The first email on new API!", email.subject)
end
test "mail() with from overwrites the class level default" do
- email = BaseMailer.welcome(from: 'someone@example.com',
- to: 'another@example.org')
- assert_equal(['someone@example.com'], email.from)
- assert_equal(['another@example.org'], email.to)
+ email = BaseMailer.welcome(from: "someone@example.com",
+ to: "another@example.org")
+ assert_equal(["someone@example.com"], email.from)
+ assert_equal(["another@example.org"], email.to)
end
test "mail() with bcc, cc, content_type, charset, mime_version, reply_to and date" do
time = Time.now.beginning_of_day.to_datetime
- email = BaseMailer.welcome(bcc: 'bcc@test.lindsaar.net',
- cc: 'cc@test.lindsaar.net',
- content_type: 'multipart/mixed',
- charset: 'iso-8559-1',
- mime_version: '2.0',
- reply_to: 'reply-to@test.lindsaar.net',
+ email = BaseMailer.welcome(bcc: "bcc@test.lindsaar.net",
+ cc: "cc@test.lindsaar.net",
+ content_type: "multipart/mixed",
+ charset: "iso-8559-1",
+ mime_version: "2.0",
+ reply_to: "reply-to@test.lindsaar.net",
date: time)
- assert_equal(['bcc@test.lindsaar.net'], email.bcc)
- assert_equal(['cc@test.lindsaar.net'], email.cc)
- assert_equal('multipart/mixed; charset=iso-8559-1', email.content_type)
- assert_equal('iso-8559-1', email.charset)
- assert_equal('2.0', email.mime_version)
- assert_equal(['reply-to@test.lindsaar.net'], email.reply_to)
+ assert_equal(["bcc@test.lindsaar.net"], email.bcc)
+ assert_equal(["cc@test.lindsaar.net"], email.cc)
+ assert_equal("multipart/mixed; charset=iso-8559-1", email.content_type)
+ assert_equal("iso-8559-1", email.charset)
+ assert_equal("2.0", email.mime_version)
+ assert_equal(["reply-to@test.lindsaar.net"], email.reply_to)
assert_equal(time, email.date)
end
@@ -75,63 +77,63 @@ class BaseTest < ActiveSupport::TestCase
test "should set template content type if mail has only one part" do
mail = BaseMailer.html_only
- assert_equal('text/html', mail.mime_type)
+ assert_equal("text/html", mail.mime_type)
mail = BaseMailer.plain_text_only
- assert_equal('text/plain', mail.mime_type)
+ assert_equal("text/plain", mail.mime_type)
end
# Custom headers
test "custom headers" do
email = BaseMailer.welcome
- assert_equal("Not SPAM", email['X-SPAM'].decoded)
+ assert_equal("Not SPAM", email["X-SPAM"].decoded)
end
test "can pass random headers in as a hash to mail" do
- hash = {'X-Special-Domain-Specific-Header' => "SecretValue",
- 'In-Reply-To' => '1234@mikel.me.com' }
+ hash = { "X-Special-Domain-Specific-Header" => "SecretValue",
+ "In-Reply-To" => "1234@mikel.me.com" }
mail = BaseMailer.welcome(hash)
- assert_equal('SecretValue', mail['X-Special-Domain-Specific-Header'].decoded)
- assert_equal('1234@mikel.me.com', mail['In-Reply-To'].decoded)
+ assert_equal("SecretValue", mail["X-Special-Domain-Specific-Header"].decoded)
+ assert_equal("1234@mikel.me.com", mail["In-Reply-To"].decoded)
end
test "can pass random headers in as a hash to headers" do
- hash = {'X-Special-Domain-Specific-Header' => "SecretValue",
- 'In-Reply-To' => '1234@mikel.me.com' }
+ hash = { "X-Special-Domain-Specific-Header" => "SecretValue",
+ "In-Reply-To" => "1234@mikel.me.com" }
mail = BaseMailer.welcome_with_headers(hash)
- assert_equal('SecretValue', mail['X-Special-Domain-Specific-Header'].decoded)
- assert_equal('1234@mikel.me.com', mail['In-Reply-To'].decoded)
+ assert_equal("SecretValue", mail["X-Special-Domain-Specific-Header"].decoded)
+ assert_equal("1234@mikel.me.com", mail["In-Reply-To"].decoded)
end
# Attachments
test "attachment with content" do
email = BaseMailer.attachment_with_content
assert_equal(1, email.attachments.length)
- assert_equal('invoice.pdf', email.attachments[0].filename)
- assert_equal('This is test File content', email.attachments['invoice.pdf'].decoded)
+ assert_equal("invoice.pdf", email.attachments[0].filename)
+ assert_equal("This is test File content", email.attachments["invoice.pdf"].decoded)
end
test "attachment gets content type from filename" do
email = BaseMailer.attachment_with_content
- assert_equal('invoice.pdf', email.attachments[0].filename)
- assert_equal('application/pdf', email.attachments[0].mime_type)
+ assert_equal("invoice.pdf", email.attachments[0].filename)
+ assert_equal("application/pdf", email.attachments[0].mime_type)
end
test "attachment with hash" do
email = BaseMailer.attachment_with_hash
assert_equal(1, email.attachments.length)
- assert_equal('invoice.jpg', email.attachments[0].filename)
- expected = "\312\213\254\232)b"
+ assert_equal("invoice.jpg", email.attachments[0].filename)
+ expected = +"\312\213\254\232)b"
expected.force_encoding(Encoding::BINARY)
- assert_equal expected, email.attachments['invoice.jpg'].decoded
+ assert_equal expected, email.attachments["invoice.jpg"].decoded
end
test "attachment with hash using default mail encoding" do
email = BaseMailer.attachment_with_hash_default_encoding
assert_equal(1, email.attachments.length)
- assert_equal('invoice.jpg', email.attachments[0].filename)
- expected = "\312\213\254\232)b"
+ assert_equal("invoice.jpg", email.attachments[0].filename)
+ expected = +"\312\213\254\232)b"
expected.force_encoding(Encoding::BINARY)
- assert_equal expected, email.attachments['invoice.jpg'].decoded
+ assert_equal expected, email.attachments["invoice.jpg"].decoded
end
test "sets mime type to multipart/mixed when attachment is included" do
@@ -140,14 +142,19 @@ 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)
assert_equal("multipart/mixed", email.mime_type)
assert_equal("text/html", email.parts[0].mime_type)
- assert_equal("Attachment with content", email.parts[0].body.encoded)
+ assert_equal("Attachment with content", email.parts[0].decoded)
assert_equal("application/pdf", email.parts[1].mime_type)
- assert_equal("VGhpcyBpcyB0ZXN0IEZpbGUgY29udGVudA==\r\n", email.parts[1].body.encoded)
+ assert_equal("This is test File content", email.parts[1].decoded)
end
test "adds the given :body as part" do
@@ -155,9 +162,9 @@ class BaseTest < ActiveSupport::TestCase
assert_equal(2, email.parts.length)
assert_equal("multipart/mixed", email.mime_type)
assert_equal("text/plain", email.parts[0].mime_type)
- assert_equal("I'm the eggman", email.parts[0].body.encoded)
+ assert_equal("I'm the eggman", email.parts[0].decoded)
assert_equal("application/pdf", email.parts[1].mime_type)
- assert_equal("VGhpcyBpcyB0ZXN0IEZpbGUgY29udGVudA==\r\n", email.parts[1].body.encoded)
+ assert_equal("This is test File content", email.parts[1].decoded)
end
test "can embed an inline attachment" do
@@ -215,24 +222,24 @@ class BaseTest < ActiveSupport::TestCase
email = BaseMailer.welcome(subject: nil)
assert_equal "Welcome", email.subject
- with_translation 'en', base_mailer: {welcome: {subject: "New Subject!"}} do
+ with_translation "en", base_mailer: { welcome: { subject: "New Subject!" } } do
email = BaseMailer.welcome(subject: nil)
assert_equal "New Subject!", email.subject
end
end
end
- test 'default subject can have interpolations' do
- with_translation 'en', base_mailer: {with_subject_interpolations: {subject: 'Will the real %{rapper_or_impersonator} please stand up?'}} do
+ test "default subject can have interpolations" do
+ with_translation "en", base_mailer: { with_subject_interpolations: { subject: "Will the real %{rapper_or_impersonator} please stand up?" } } do
email = BaseMailer.with_subject_interpolations
- assert_equal 'Will the real Slim Shady please stand up?', email.subject
+ assert_equal "Will the real Slim Shady please stand up?", email.subject
end
end
test "translations are scoped properly" do
- with_translation 'en', base_mailer: {email_with_translations: {greet_user: "Hello %{name}!"}} do
+ with_translation "en", base_mailer: { email_with_translations: { greet_user: "Hello %{name}!" } } do
email = BaseMailer.email_with_translations
- assert_equal 'Hello lifo!', email.body.encoded
+ assert_equal "Hello lifo!", email.body.encoded
end
end
@@ -240,7 +247,7 @@ class BaseTest < ActiveSupport::TestCase
class LateAttachmentMailer < ActionMailer::Base
def welcome
mail body: "yay", from: "welcome@example.com", to: "to@example.com"
- attachments['invoice.pdf'] = 'This is test File content'
+ attachments["invoice.pdf"] = "This is test File content"
end
end
@@ -252,7 +259,7 @@ class BaseTest < ActiveSupport::TestCase
class LateInlineAttachmentMailer < ActionMailer::Base
def welcome
mail body: "yay", from: "welcome@example.com", to: "to@example.com"
- attachments.inline['invoice.pdf'] = 'This is test File content'
+ attachments.inline["invoice.pdf"] = "This is test File content"
end
end
@@ -271,13 +278,13 @@ class BaseTest < ActiveSupport::TestCase
assert_nothing_raised { mail.message }
assert_equal ["image/jpeg; filename=controller_attachments.jpg",
- "image/jpeg; filename=attachments.jpg"], mail.attachments.inline.map {|a| a['Content-Type'].to_s }
+ "image/jpeg; filename=attachments.jpg"], mail.attachments.inline.map { |a| a["Content-Type"].to_s }
end
test "accessing attachments works after mail was called" do
class LateAttachmentAccessorMailer < ActionMailer::Base
def welcome
- attachments['invoice.pdf'] = 'This is test File content'
+ attachments["invoice.pdf"] = "This is test File content"
mail body: "yay", from: "welcome@example.com", to: "to@example.com"
unless attachments.map(&:filename) == ["invoice.pdf"]
@@ -315,22 +322,21 @@ class BaseTest < ActiveSupport::TestCase
test "implicit multipart with attachments creates nested parts" do
email = BaseMailer.implicit_multipart(attachments: true)
- assert_equal("application/pdf", email.parts[0].mime_type)
- assert_equal("multipart/alternative", email.parts[1].mime_type)
- assert_equal("text/plain", email.parts[1].parts[0].mime_type)
- assert_equal("TEXT Implicit Multipart", email.parts[1].parts[0].body.encoded)
- assert_equal("text/html", email.parts[1].parts[1].mime_type)
- assert_equal("HTML Implicit Multipart", email.parts[1].parts[1].body.encoded)
+ assert_equal(%w[ application/pdf multipart/alternative ], email.parts.map(&:mime_type).sort)
+ multipart = email.parts.detect { |p| p.mime_type == "multipart/alternative" }
+ assert_equal("text/plain", multipart.parts[0].mime_type)
+ assert_equal("TEXT Implicit Multipart", multipart.parts[0].body.encoded)
+ assert_equal("text/html", multipart.parts[1].mime_type)
+ assert_equal("HTML Implicit Multipart", multipart.parts[1].body.encoded)
end
test "implicit multipart with attachments and sort order" do
order = ["text/html", "text/plain"]
with_default BaseMailer, parts_order: order do
email = BaseMailer.implicit_multipart(attachments: true)
- assert_equal("application/pdf", email.parts[0].mime_type)
- assert_equal("multipart/alternative", email.parts[1].mime_type)
- assert_equal("text/plain", email.parts[1].parts[1].mime_type)
- assert_equal("text/html", email.parts[1].parts[0].mime_type)
+ assert_equal(%w[ application/pdf multipart/alternative ], email.parts.map(&:mime_type).sort)
+ multipart = email.parts.detect { |p| p.mime_type == "multipart/alternative" }
+ assert_equal(%w[ text/html text/plain ], multipart.parts.map(&:mime_type).sort)
end
end
@@ -366,7 +372,7 @@ class BaseTest < ActiveSupport::TestCase
I18n.backend = fallback_backend.new
I18n.fallbacks[:"de-AT"] = [:de]
- swap I18n, locale: 'de-AT' do
+ swap I18n, locale: "de-AT" do
email = BaseMailer.implicit_with_locale
assert_equal(2, email.parts.size)
assert_equal("multipart/alternative", email.mime_type)
@@ -380,7 +386,6 @@ class BaseTest < ActiveSupport::TestCase
end
end
-
test "implicit multipart with several view paths uses the first one with template" do
old = BaseMailer.view_paths
begin
@@ -421,12 +426,12 @@ class BaseTest < ActiveSupport::TestCase
test "explicit multipart with attachments creates nested parts" do
email = BaseMailer.explicit_multipart(attachments: true)
- assert_equal("application/pdf", email.parts[0].mime_type)
- assert_equal("multipart/alternative", email.parts[1].mime_type)
- assert_equal("text/plain", email.parts[1].parts[0].mime_type)
- assert_equal("TEXT Explicit Multipart", email.parts[1].parts[0].body.encoded)
- assert_equal("text/html", email.parts[1].parts[1].mime_type)
- assert_equal("HTML Explicit Multipart", email.parts[1].parts[1].body.encoded)
+ assert_equal(%w[ application/pdf multipart/alternative ], email.parts.map(&:mime_type).sort)
+ multipart = email.parts.detect { |p| p.mime_type == "multipart/alternative" }
+ assert_equal("text/plain", multipart.parts[0].mime_type)
+ assert_equal("TEXT Explicit Multipart", multipart.parts[0].body.encoded)
+ assert_equal("text/html", multipart.parts[1].mime_type)
+ assert_equal("HTML Explicit Multipart", multipart.parts[1].body.encoded)
end
test "explicit multipart with templates" do
@@ -449,6 +454,13 @@ class BaseTest < ActiveSupport::TestCase
assert_equal("Format with any!", email.parts[1].body.encoded)
end
+ test "explicit without specifying format with format.any" do
+ error = assert_raises(ArgumentError) do
+ BaseMailer.explicit_without_specifying_format_with_any.parts
+ end
+ assert_equal "You have to supply at least one format", error.message
+ end
+
test "explicit multipart with format(Hash)" do
email = BaseMailer.explicit_multipart_with_options(true)
email.ready_to_send!
@@ -494,20 +506,21 @@ class BaseTest < ActiveSupport::TestCase
test "should respond to action methods" do
assert_respond_to BaseMailer, :welcome
assert_respond_to BaseMailer, :implicit_multipart
- assert !BaseMailer.respond_to?(:mail)
- assert !BaseMailer.respond_to?(:headers)
+ assert_not_respond_to BaseMailer, :mail
+ assert_not_respond_to BaseMailer, :headers
end
test "calling just the action should return the generated mail object" do
email = BaseMailer.welcome
assert_equal(0, BaseMailer.deliveries.length)
- assert_equal('The first email on new API!', email.subject)
+ assert_equal("The first email on new API!", email.subject)
end
test "calling deliver on the action should deliver the mail object" do
- BaseMailer.expects(:deliver_mail).once
- mail = BaseMailer.welcome.deliver_now
- assert_equal 'The first email on new API!', mail.subject
+ assert_called(BaseMailer, :deliver_mail) do
+ mail = BaseMailer.welcome.deliver_now
+ assert_equal "The first email on new API!", mail.subject
+ end
end
test "calling deliver on the action should increment the deliveries collection if using the test mailer" do
@@ -517,42 +530,44 @@ class BaseTest < ActiveSupport::TestCase
test "calling deliver, ActionMailer should yield back to mail to let it call :do_delivery on itself" do
mail = Mail::Message.new
- mail.expects(:do_delivery).once
- BaseMailer.expects(:welcome).returns(mail)
- BaseMailer.welcome.deliver
+ assert_called(mail, :do_delivery) do
+ assert_called(BaseMailer, :welcome, returns: mail) do
+ BaseMailer.welcome.deliver
+ end
+ end
end
# Rendering
test "you can specify a different template for implicit render" do
- mail = BaseMailer.implicit_different_template('implicit_multipart').deliver_now
+ mail = BaseMailer.implicit_different_template("implicit_multipart").deliver_now
assert_equal("HTML Implicit Multipart", mail.html_part.body.decoded)
assert_equal("TEXT Implicit Multipart", mail.text_part.body.decoded)
end
test "should raise if missing template in implicit render" do
assert_raises ActionView::MissingTemplate do
- BaseMailer.implicit_different_template('missing_template').deliver_now
+ BaseMailer.implicit_different_template("missing_template").deliver_now
end
assert_equal(0, BaseMailer.deliveries.length)
end
test "you can specify a different template for explicit render" do
- mail = BaseMailer.explicit_different_template('explicit_multipart_templates').deliver_now
+ mail = BaseMailer.explicit_different_template("explicit_multipart_templates").deliver_now
assert_equal("HTML Explicit Multipart Templates", mail.html_part.body.decoded)
assert_equal("TEXT Explicit Multipart Templates", mail.text_part.body.decoded)
end
test "you can specify a different layout" do
- mail = BaseMailer.different_layout('different_layout').deliver_now
+ mail = BaseMailer.different_layout("different_layout").deliver_now
assert_equal("HTML -- HTML", mail.html_part.body.decoded)
assert_equal("PLAIN -- PLAIN", mail.text_part.body.decoded)
end
test "you can specify the template path for implicit lookup" do
- mail = BaseMailer.welcome_from_another_path('another.path/base_mailer').deliver_now
+ mail = BaseMailer.welcome_from_another_path("another.path/base_mailer").deliver_now
assert_equal("Welcome from another path", mail.body.encoded)
- mail = BaseMailer.welcome_from_another_path(['unknown/invalid', 'another.path/base_mailer']).deliver_now
+ mail = BaseMailer.welcome_from_another_path(["unknown/invalid", "another.path/base_mailer"]).deliver_now
assert_equal("Welcome from another path", mail.body.encoded)
end
@@ -562,7 +577,7 @@ class BaseTest < ActiveSupport::TestCase
mail = AssetMailer.welcome
- assert_dom_equal(%{<img alt="Dummy" src="http://global.com/images/dummy.png" />}, mail.body.to_s.strip)
+ assert_dom_equal(%{<img src="http://global.com/images/dummy.png" />}, mail.body.to_s.strip)
end
test "assets tags should use a Mailer's asset_host settings when available" do
@@ -576,18 +591,18 @@ class BaseTest < ActiveSupport::TestCase
mail = TempAssetMailer.welcome
- assert_dom_equal(%{<img alt="Dummy" src="http://local.com/images/dummy.png" />}, mail.body.to_s.strip)
+ assert_dom_equal(%{<img src="http://local.com/images/dummy.png" />}, mail.body.to_s.strip)
end
- test 'the view is not rendered when mail was never called' do
+ test "the view is not rendered when mail was never called" do
mail = BaseMailer.without_mail_call
- assert_equal('', mail.body.to_s.strip)
+ assert_equal("", mail.body.to_s.strip)
mail.deliver_now
end
- test 'the return value of mailer methods is not relevant' do
+ test "the return value of mailer methods is not relevant" do
mail = BaseMailer.with_nil_as_return_value
- assert_equal('Welcome', mail.body.to_s.strip)
+ assert_equal("Welcome", mail.body.to_s.strip)
mail.deliver_now
end
@@ -603,40 +618,68 @@ class BaseTest < ActiveSupport::TestCase
end
end
- test "you can register an observer to the mail object that gets informed on email delivery" do
+ test "you can register and unregister an observer to the mail object that gets informed on email delivery" do
mail_side_effects do
ActionMailer::Base.register_observer(MyObserver)
mail = BaseMailer.welcome
- MyObserver.expects(:delivered_email).with(mail)
- mail.deliver_now
+ assert_called_with(MyObserver, :delivered_email, [mail]) do
+ mail.deliver_now
+ end
+
+ ActionMailer::Base.unregister_observer(MyObserver)
+ assert_not_called(MyObserver, :delivered_email, returns: mail) do
+ mail.deliver_now
+ end
end
end
- test "you can register an observer using its stringified name to the mail object that gets informed on email delivery" do
+ test "you can register and unregister an observer using its stringified name to the mail object that gets informed on email delivery" do
mail_side_effects do
ActionMailer::Base.register_observer("BaseTest::MyObserver")
mail = BaseMailer.welcome
- MyObserver.expects(:delivered_email).with(mail)
- mail.deliver_now
+ assert_called_with(MyObserver, :delivered_email, [mail]) do
+ mail.deliver_now
+ end
+
+ ActionMailer::Base.unregister_observer("BaseTest::MyObserver")
+ assert_not_called(MyObserver, :delivered_email, returns: mail) do
+ mail.deliver_now
+ end
end
end
- test "you can register an observer using its symbolized underscored name to the mail object that gets informed on email delivery" do
+ test "you can register and unregister an observer using its symbolized underscored name to the mail object that gets informed on email delivery" do
mail_side_effects do
ActionMailer::Base.register_observer(:"base_test/my_observer")
mail = BaseMailer.welcome
- MyObserver.expects(:delivered_email).with(mail)
- mail.deliver_now
+ assert_called_with(MyObserver, :delivered_email, [mail]) do
+ mail.deliver_now
+ end
+
+ ActionMailer::Base.unregister_observer(:"base_test/my_observer")
+ assert_not_called(MyObserver, :delivered_email, returns: mail) do
+ mail.deliver_now
+ end
end
end
- test "you can register multiple observers to the mail object that both get informed on email delivery" do
+ test "you can register and unregister multiple observers to the mail object that both get informed on email delivery" do
mail_side_effects do
ActionMailer::Base.register_observers("BaseTest::MyObserver", MySecondObserver)
mail = BaseMailer.welcome
- MyObserver.expects(:delivered_email).with(mail)
- MySecondObserver.expects(:delivered_email).with(mail)
- mail.deliver_now
+ assert_called_with(MyObserver, :delivered_email, [mail]) do
+ assert_called_with(MySecondObserver, :delivered_email, [mail]) do
+ mail.deliver_now
+ end
+ end
+
+ ActionMailer::Base.unregister_observers("BaseTest::MyObserver", MySecondObserver)
+ assert_not_called(MyObserver, :delivered_email, returns: mail) do
+ mail.deliver_now
+ end
+ assert_not_called(MySecondObserver, :delivered_email, returns: mail) do
+ mail.deliver_now
+ end
end
end
@@ -650,53 +693,91 @@ class BaseTest < ActiveSupport::TestCase
def self.previewing_email(mail); end
end
- test "you can register an interceptor to the mail object that gets passed the mail object before delivery" do
+ test "you can register and unregister an interceptor to the mail object that gets passed the mail object before delivery" do
mail_side_effects do
ActionMailer::Base.register_interceptor(MyInterceptor)
mail = BaseMailer.welcome
- MyInterceptor.expects(:delivering_email).with(mail)
- mail.deliver_now
+ assert_called_with(MyInterceptor, :delivering_email, [mail]) do
+ mail.deliver_now
+ end
+
+ ActionMailer::Base.unregister_interceptor(MyInterceptor)
+ assert_not_called(MyInterceptor, :delivering_email, returns: mail) do
+ mail.deliver_now
+ end
end
end
- test "you can register an interceptor using its stringified name to the mail object that gets passed the mail object before delivery" do
+ test "you can register and unregister an interceptor using its stringified name to the mail object that gets passed the mail object before delivery" do
mail_side_effects do
ActionMailer::Base.register_interceptor("BaseTest::MyInterceptor")
mail = BaseMailer.welcome
- MyInterceptor.expects(:delivering_email).with(mail)
- mail.deliver_now
+ assert_called_with(MyInterceptor, :delivering_email, [mail]) do
+ mail.deliver_now
+ end
+
+ ActionMailer::Base.unregister_interceptor("BaseTest::MyInterceptor")
+ assert_not_called(MyInterceptor, :delivering_email, returns: mail) do
+ mail.deliver_now
+ end
end
end
- test "you can register an interceptor using its symbolized underscored name to the mail object that gets passed the mail object before delivery" do
+ test "you can register and unregister an interceptor using its symbolized underscored name to the mail object that gets passed the mail object before delivery" do
mail_side_effects do
ActionMailer::Base.register_interceptor(:"base_test/my_interceptor")
mail = BaseMailer.welcome
- MyInterceptor.expects(:delivering_email).with(mail)
- mail.deliver_now
+ assert_called_with(MyInterceptor, :delivering_email, [mail]) do
+ mail.deliver_now
+ end
+
+ ActionMailer::Base.unregister_interceptor(:"base_test/my_interceptor")
+ assert_not_called(MyInterceptor, :delivering_email, returns: mail) do
+ mail.deliver_now
+ end
end
end
- test "you can register multiple interceptors to the mail object that both get passed the mail object before delivery" do
+ test "you can register and unregister multiple interceptors to the mail object that both get passed the mail object before delivery" do
mail_side_effects do
ActionMailer::Base.register_interceptors("BaseTest::MyInterceptor", MySecondInterceptor)
mail = BaseMailer.welcome
- MyInterceptor.expects(:delivering_email).with(mail)
- MySecondInterceptor.expects(:delivering_email).with(mail)
- mail.deliver_now
+ assert_called_with(MyInterceptor, :delivering_email, [mail]) do
+ assert_called_with(MySecondInterceptor, :delivering_email, [mail]) do
+ mail.deliver_now
+ end
+ end
+
+ ActionMailer::Base.unregister_interceptors("BaseTest::MyInterceptor", MySecondInterceptor)
+ assert_not_called(MyInterceptor, :delivering_email, returns: mail) do
+ mail.deliver_now
+ end
+ assert_not_called(MySecondInterceptor, :delivering_email, returns: mail) do
+ mail.deliver_now
+ end
end
end
test "being able to put proc's into the defaults hash and they get evaluated on mail sending" do
- mail1 = ProcMailer.welcome['X-Proc-Method']
+ mail1 = ProcMailer.welcome["X-Proc-Method"]
yesterday = 1.day.ago
- Time.stubs(:now).returns(yesterday)
- mail2 = ProcMailer.welcome['X-Proc-Method']
- assert(mail1.to_s.to_i > mail2.to_s.to_i)
+ Time.stub(:now, yesterday) do
+ mail2 = ProcMailer.welcome["X-Proc-Method"]
+ assert(mail1.to_s.to_i > mail2.to_s.to_i)
+ end
end
- test 'default values which have to_proc (e.g. symbols) should not be considered procs' do
- assert(ProcMailer.welcome['x-has-to-proc'].to_s == 'symbol')
+ test "default values which have to_proc (e.g. symbols) should not be considered procs" do
+ assert(ProcMailer.welcome["x-has-to-proc"].to_s == "symbol")
+ end
+
+ test "proc default values can have arity of 1 where arg is a mailer instance" do
+ assert_equal(ProcMailer.welcome["X-Lambda-Arity-1-arg"].to_s, "complex_value")
+ assert_equal(ProcMailer.welcome["X-Lambda-Arity-1-self"].to_s, "complex_value")
+ end
+
+ test "proc default values with fixed arity of 0 can be called" do
+ assert_equal("0", ProcMailer.welcome["X-Lambda-Arity-0"].to_s)
end
test "we can call other defined methods on the class as needed" do
@@ -711,12 +792,12 @@ class BaseTest < ActiveSupport::TestCase
def welcome ; mail ; end
private
- def add_special_header!
- headers('X-Special-Header' => 'Wow, so special')
- end
+ def add_special_header!
+ headers("X-Special-Header" => "Wow, so special")
+ end
end
- assert_equal('Wow, so special', BeforeActionMailer.welcome['X-Special-Header'].to_s)
+ assert_equal("Wow, so special", BeforeActionMailer.welcome["X-Special-Header"].to_s)
end
test "modifying the mail message with an after_action" do
@@ -726,12 +807,12 @@ class BaseTest < ActiveSupport::TestCase
def welcome ; mail ; end
private
- def add_special_header!
- headers('X-Special-Header' => 'Testing')
- end
+ def add_special_header!
+ headers("X-Special-Header" => "Testing")
+ end
end
- assert_equal('Testing', AfterActionMailer.welcome['X-Special-Header'].to_s)
+ assert_equal("Testing", AfterActionMailer.welcome["X-Special-Header"].to_s)
end
test "adding an inline attachment using a before_action" do
@@ -741,19 +822,19 @@ class BaseTest < ActiveSupport::TestCase
def welcome ; mail ; end
private
- def add_inline_attachment!
- attachments.inline["footer.jpg"] = 'hey there'
- end
+ def add_inline_attachment!
+ attachments.inline["footer.jpg"] = "hey there"
+ end
end
mail = DefaultInlineAttachmentMailer.welcome
- assert_equal('image/jpeg; filename=footer.jpg', mail.attachments.inline.first['Content-Type'].to_s)
+ assert_equal("image/jpeg; filename=footer.jpg", mail.attachments.inline.first["Content-Type"].to_s)
end
test "action methods should be refreshed after defining new method" do
class FooMailer < ActionMailer::Base
- # this triggers action_methods
- self.respond_to?(:foo)
+ # This triggers action_methods.
+ respond_to?(:foo)
def notify
end
@@ -777,8 +858,8 @@ class BaseTest < ActiveSupport::TestCase
test "default_from can be set" do
class DefaultFromMailer < ActionMailer::Base
- default to: 'system@test.lindsaar.net'
- self.default_options = {from: "robert.pankowecki@gmail.com"}
+ default to: "system@test.lindsaar.net"
+ self.default_options = { from: "robert.pankowecki@gmail.com" }
def welcome
mail(subject: "subject", body: "hello world")
@@ -793,7 +874,7 @@ class BaseTest < ActiveSupport::TestCase
after_action :a_callback
def welcome
- headers('X-Special-Header' => 'special indeed!')
+ headers("X-Special-Header" => "special indeed!")
mail subject: "subject", body: "hello world", to: ["joe@example.com"]
end
@@ -809,7 +890,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.
@@ -834,8 +934,6 @@ class BaseTest < ActiveSupport::TestCase
klass.default_params = old
end
- # A simple hack to restore the observers and interceptors for Mail, as it
- # does not have an unregister API yet.
def mail_side_effects
old_observers = Mail.class_variable_get(:@@delivery_notification_observers)
old_delivery_interceptors = Mail.class_variable_get(:@@delivery_interceptors)
@@ -874,36 +972,91 @@ class BasePreviewInterceptorsTest < ActiveSupport::TestCase
def self.previewing_email(mail); end
end
- test "you can register a preview interceptor to the mail object that gets passed the mail object before previewing" do
+ test "you can register and unregister a preview interceptor to the mail object that gets passed the mail object before previewing" do
ActionMailer::Base.register_preview_interceptor(MyInterceptor)
mail = BaseMailer.welcome
- BaseMailerPreview.any_instance.stubs(:welcome).returns(mail)
- MyInterceptor.expects(:previewing_email).with(mail)
- BaseMailerPreview.call(:welcome)
+ stub_any_instance(BaseMailerPreview) do |instance|
+ instance.stub(:welcome, mail) do
+ assert_called_with(MyInterceptor, :previewing_email, [mail]) do
+ BaseMailerPreview.call(:welcome)
+ end
+ end
+ end
+
+ ActionMailer::Base.unregister_preview_interceptor(MyInterceptor)
+ assert_not_called(MyInterceptor, :previewing_email, returns: mail) do
+ BaseMailerPreview.call(:welcome)
+ end
end
- test "you can register a preview interceptor using its stringified name to the mail object that gets passed the mail object before previewing" do
+ test "you can register and unregister a preview interceptor using its stringified name to the mail object that gets passed the mail object before previewing" do
ActionMailer::Base.register_preview_interceptor("BasePreviewInterceptorsTest::MyInterceptor")
mail = BaseMailer.welcome
- BaseMailerPreview.any_instance.stubs(:welcome).returns(mail)
- MyInterceptor.expects(:previewing_email).with(mail)
- BaseMailerPreview.call(:welcome)
+ stub_any_instance(BaseMailerPreview) do |instance|
+ instance.stub(:welcome, mail) do
+ assert_called_with(MyInterceptor, :previewing_email, [mail]) do
+ BaseMailerPreview.call(:welcome)
+ end
+ end
+ end
+
+ ActionMailer::Base.unregister_preview_interceptor("BasePreviewInterceptorsTest::MyInterceptor")
+ assert_not_called(MyInterceptor, :previewing_email, returns: mail) do
+ BaseMailerPreview.call(:welcome)
+ end
end
- test "you can register an interceptor using its symbolized underscored name to the mail object that gets passed the mail object before previewing" do
+ test "you can register and unregister a preview interceptor using its symbolized underscored name to the mail object that gets passed the mail object before previewing" do
ActionMailer::Base.register_preview_interceptor(:"base_preview_interceptors_test/my_interceptor")
mail = BaseMailer.welcome
- BaseMailerPreview.any_instance.stubs(:welcome).returns(mail)
- MyInterceptor.expects(:previewing_email).with(mail)
- BaseMailerPreview.call(:welcome)
+ stub_any_instance(BaseMailerPreview) do |instance|
+ instance.stub(:welcome, mail) do
+ assert_called_with(MyInterceptor, :previewing_email, [mail]) do
+ BaseMailerPreview.call(:welcome)
+ end
+ end
+ end
+
+ ActionMailer::Base.unregister_preview_interceptor(:"base_preview_interceptors_test/my_interceptor")
+ assert_not_called(MyInterceptor, :previewing_email, returns: mail) do
+ BaseMailerPreview.call(:welcome)
+ end
end
- test "you can register multiple preview interceptors to the mail object that both get passed the mail object before previewing" do
+ test "you can register and unregister multiple preview interceptors to the mail object that both get passed the mail object before previewing" do
ActionMailer::Base.register_preview_interceptors("BasePreviewInterceptorsTest::MyInterceptor", MySecondInterceptor)
mail = BaseMailer.welcome
- BaseMailerPreview.any_instance.stubs(:welcome).returns(mail)
- MyInterceptor.expects(:previewing_email).with(mail)
- MySecondInterceptor.expects(:previewing_email).with(mail)
- BaseMailerPreview.call(:welcome)
+ stub_any_instance(BaseMailerPreview) do |instance|
+ instance.stub(:welcome, mail) do
+ assert_called_with(MyInterceptor, :previewing_email, [mail]) do
+ assert_called_with(MySecondInterceptor, :previewing_email, [mail]) do
+ BaseMailerPreview.call(:welcome)
+ end
+ end
+ end
+ end
+
+ ActionMailer::Base.unregister_preview_interceptors("BasePreviewInterceptorsTest::MyInterceptor", MySecondInterceptor)
+ assert_not_called(MyInterceptor, :previewing_email, returns: mail) do
+ BaseMailerPreview.call(:welcome)
+ end
+ assert_not_called(MySecondInterceptor, :previewing_email, returns: mail) do
+ BaseMailerPreview.call(:welcome)
+ end
+ end
+end
+
+class BasePreviewTest < ActiveSupport::TestCase
+ class BaseMailerPreview < ActionMailer::Preview
+ def welcome
+ BaseMailer.welcome(params)
+ end
+ end
+
+ test "has access to params" do
+ params = { name: "World" }
+
+ message = BaseMailerPreview.call(:welcome, params)
+ assert_equal "World", message["name"].decoded
end
end
diff --git a/actionmailer/test/caching_test.rb b/actionmailer/test/caching_test.rb
new file mode 100644
index 0000000000..22f310f39f
--- /dev/null
+++ b/actionmailer/test/caching_test.rb
@@ -0,0 +1,271 @@
+# frozen_string_literal: true
+
+require "fileutils"
+require "abstract_unit"
+require "mailers/base_mailer"
+require "mailers/caching_mailer"
+
+CACHE_DIR = "test_cache"
+# Don't change '/../temp/' cavalierly or you might hose something you don't want hosed
+FILE_STORE_PATH = File.join(__dir__, "/../temp/", CACHE_DIR)
+
+class FragmentCachingMailer < ActionMailer::Base
+ abstract!
+
+ def some_action; end
+end
+
+class BaseCachingTest < ActiveSupport::TestCase
+ def setup
+ super
+ @store = ActiveSupport::Cache::MemoryStore.new
+ @mailer = FragmentCachingMailer.new
+ @mailer.perform_caching = true
+ @mailer.cache_store = @store
+ end
+end
+
+class FragmentCachingTest < BaseCachingTest
+ def test_read_fragment_with_caching_enabled
+ @store.write("views/name", "value")
+ assert_equal "value", @mailer.read_fragment("name")
+ end
+
+ def test_read_fragment_with_caching_disabled
+ @mailer.perform_caching = false
+ @store.write("views/name", "value")
+ assert_nil @mailer.read_fragment("name")
+ end
+
+ def test_fragment_exist_with_caching_enabled
+ @store.write("views/name", "value")
+ assert @mailer.fragment_exist?("name")
+ assert_not @mailer.fragment_exist?("other_name")
+ end
+
+ def test_fragment_exist_with_caching_disabled
+ @mailer.perform_caching = false
+ @store.write("views/name", "value")
+ assert_not @mailer.fragment_exist?("name")
+ assert_not @mailer.fragment_exist?("other_name")
+ end
+
+ def test_write_fragment_with_caching_enabled
+ assert_nil @store.read("views/name")
+ assert_equal "value", @mailer.write_fragment("name", "value")
+ assert_equal "value", @store.read("views/name")
+ end
+
+ def test_write_fragment_with_caching_disabled
+ assert_nil @store.read("views/name")
+ @mailer.perform_caching = false
+ assert_equal "value", @mailer.write_fragment("name", "value")
+ assert_nil @store.read("views/name")
+ end
+
+ def test_expire_fragment_with_simple_key
+ @store.write("views/name", "value")
+ @mailer.expire_fragment "name"
+ assert_nil @store.read("views/name")
+ end
+
+ def test_expire_fragment_with_regexp
+ @store.write("views/name", "value")
+ @store.write("views/another_name", "another_value")
+ @store.write("views/primalgrasp", "will not expire ;-)")
+
+ @mailer.expire_fragment(/name/)
+
+ assert_nil @store.read("views/name")
+ assert_nil @store.read("views/another_name")
+ assert_equal "will not expire ;-)", @store.read("views/primalgrasp")
+ end
+
+ def test_fragment_for
+ @store.write("views/expensive", "fragment content")
+ fragment_computed = false
+
+ view_context = @mailer.view_context
+
+ buffer = "generated till now -> ".html_safe
+ buffer << view_context.send(:fragment_for, "expensive") { fragment_computed = true }
+
+ assert_not fragment_computed
+ assert_equal "generated till now -> fragment content", buffer
+ end
+
+ def test_html_safety
+ assert_nil @store.read("views/name")
+ content = "value".html_safe
+ assert_equal content, @mailer.write_fragment("name", content)
+
+ cached = @store.read("views/name")
+ assert_equal content, cached
+ assert_equal String, cached.class
+
+ html_safe = @mailer.read_fragment("name")
+ assert_equal content, html_safe
+ assert_predicate html_safe, :html_safe?
+ end
+end
+
+class FunctionalFragmentCachingTest < BaseCachingTest
+ def setup
+ super
+ @store = ActiveSupport::Cache::MemoryStore.new
+ @mailer = CachingMailer.new
+ @mailer.perform_caching = true
+ @mailer.cache_store = @store
+ end
+
+ def test_fragment_caching
+ email = @mailer.fragment_cache
+ expected_body = "\"Welcome\""
+
+ assert_match expected_body, email.body.encoded
+ assert_match expected_body,
+ @store.read("views/caching_mailer/fragment_cache:#{template_digest("caching_mailer/fragment_cache")}/caching")
+ end
+
+ def test_fragment_caching_in_partials
+ email = @mailer.fragment_cache_in_partials
+ expected_body = "Old fragment caching in a partial"
+ assert_match(expected_body, email.body.encoded)
+
+ assert_match(expected_body,
+ @store.read("views/caching_mailer/_partial:#{template_digest("caching_mailer/_partial")}/caching"))
+ end
+
+ def test_skip_fragment_cache_digesting
+ email = @mailer.skip_fragment_cache_digesting
+ expected_body = "No Digest"
+
+ assert_match expected_body, email.body.encoded
+ assert_match expected_body, @store.read("views/no_digest")
+ end
+
+ def test_fragment_caching_options
+ time = Time.now
+ email = @mailer.fragment_caching_options
+ expected_body = "No Digest"
+
+ assert_match expected_body, email.body.encoded
+ Time.stub(:now, time + 11) do
+ assert_nil @store.read("views/no_digest")
+ end
+ end
+
+ def test_multipart_fragment_caching
+ email = @mailer.multipart_cache
+
+ expected_text_body = "\"Welcome text\""
+ expected_html_body = "\"Welcome html\""
+ encoded_body = email.body.encoded
+ assert_match expected_text_body, encoded_body
+ assert_match expected_html_body, encoded_body
+ assert_match expected_text_body,
+ @store.read("views/text_caching")
+ assert_match expected_html_body,
+ @store.read("views/html_caching")
+ end
+
+ def test_fragment_cache_instrumentation
+ @mailer.enable_fragment_cache_logging = true
+ payload = nil
+
+ subscriber = proc do |*args|
+ event = ActiveSupport::Notifications::Event.new(*args)
+ payload = event.payload
+ end
+
+ ActiveSupport::Notifications.subscribed(subscriber, "read_fragment.action_mailer") do
+ @mailer.fragment_cache
+ end
+
+ assert_equal "caching_mailer", payload[:mailer]
+ assert_equal [ :views, "caching_mailer/fragment_cache:#{template_digest("caching_mailer/fragment_cache")}", :caching ], payload[:key]
+ ensure
+ @mailer.enable_fragment_cache_logging = true
+ end
+
+ private
+
+ def template_digest(name)
+ ActionView::Digestor.digest(name: name, finder: @mailer.lookup_context)
+ end
+end
+
+class CacheHelperOutputBufferTest < BaseCachingTest
+ class MockController
+ def read_fragment(name, options)
+ false
+ end
+
+ def write_fragment(name, fragment, options)
+ fragment
+ end
+ end
+
+ def setup
+ super
+ end
+
+ def test_output_buffer
+ output_buffer = ActionView::OutputBuffer.new
+ controller = MockController.new
+ cache_helper = Class.new do
+ def self.controller; end
+ def self.output_buffer; end
+ def self.output_buffer=; end
+ end
+ cache_helper.extend(ActionView::Helpers::CacheHelper)
+
+ cache_helper.stub :controller, controller do
+ cache_helper.stub :output_buffer, output_buffer do
+ assert_called_with cache_helper, :output_buffer=, [output_buffer.class.new(output_buffer)] do
+ assert_nothing_raised do
+ cache_helper.send :fragment_for, "Test fragment name", "Test fragment", &Proc.new { nil }
+ end
+ end
+ end
+ end
+ end
+
+ def test_safe_buffer
+ output_buffer = ActiveSupport::SafeBuffer.new
+ controller = MockController.new
+ cache_helper = Class.new do
+ def self.controller; end
+ def self.output_buffer; end
+ def self.output_buffer=; end
+ end
+ cache_helper.extend(ActionView::Helpers::CacheHelper)
+
+ cache_helper.stub :controller, controller do
+ cache_helper.stub :output_buffer, output_buffer do
+ assert_called_with cache_helper, :output_buffer=, [output_buffer.class.new(output_buffer)] do
+ assert_nothing_raised do
+ cache_helper.send :fragment_for, "Test fragment name", "Test fragment", &Proc.new { nil }
+ end
+ end
+ end
+ end
+ end
+end
+
+class ViewCacheDependencyTest < BaseCachingTest
+ class NoDependenciesMailer < ActionMailer::Base
+ end
+ class HasDependenciesMailer < ActionMailer::Base
+ view_cache_dependency { "trombone" }
+ view_cache_dependency { "flute" }
+ end
+
+ def test_view_cache_dependencies_are_empty_by_default
+ assert_empty NoDependenciesMailer.new.view_cache_dependencies
+ end
+
+ def test_view_cache_dependencies_are_listed_in_declaration_order
+ assert_equal %w(trombone flute), HasDependenciesMailer.new.view_cache_dependencies
+ end
+end
diff --git a/actionmailer/test/delivery_methods_test.rb b/actionmailer/test/delivery_methods_test.rb
index 78507ce7dc..025f7152bb 100644
--- a/actionmailer/test/delivery_methods_test.rb
+++ b/actionmailer/test/delivery_methods_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class MyCustomDelivery
end
@@ -23,7 +25,7 @@ class DefaultsDeliveryMethodsTest < ActiveSupport::TestCase
test "default smtp settings" do
settings = { address: "localhost",
port: 25,
- domain: 'localhost.localdomain',
+ domain: "localhost.localdomain",
user_name: nil,
password: nil,
authentication: nil,
@@ -31,15 +33,15 @@ class DefaultsDeliveryMethodsTest < ActiveSupport::TestCase
assert_equal settings, ActionMailer::Base.smtp_settings
end
- test "default file delivery settings" do
- settings = {location: "#{Dir.tmpdir}/mails"}
+ test "default file delivery settings (with Rails.root)" do
+ settings = { location: "#{Rails.root}/tmp/mails" }
assert_equal settings, ActionMailer::Base.file_settings
end
test "default sendmail settings" do
settings = {
- location: '/usr/sbin/sendmail',
- arguments: '-i -t'
+ location: "/usr/sbin/sendmail",
+ arguments: "-i"
}
assert_equal settings, ActionMailer::Base.sendmail_settings
end
@@ -83,11 +85,11 @@ end
class MailDeliveryTest < ActiveSupport::TestCase
class DeliveryMailer < ActionMailer::Base
DEFAULT_HEADERS = {
- to: 'mikel@test.lindsaar.net',
- from: 'jose@test.plataformatec.com'
+ to: "mikel@test.lindsaar.net",
+ from: "jose@test.plataformatec.com"
}
- def welcome(hash={})
+ def welcome(hash = {})
mail(DEFAULT_HEADERS.merge(hash))
end
end
@@ -102,16 +104,21 @@ class MailDeliveryTest < ActiveSupport::TestCase
end
test "ActionMailer should be told when Mail gets delivered" do
- DeliveryMailer.expects(:deliver_mail).once
- DeliveryMailer.welcome.deliver_now
+ DeliveryMailer.delivery_method = :test
+ assert_called(DeliveryMailer, :deliver_mail) do
+ DeliveryMailer.welcome.deliver_now
+ end
end
test "delivery method can be customized per instance" do
- Mail::SMTP.any_instance.expects(:deliver!)
- email = DeliveryMailer.welcome.deliver_now
- assert_instance_of Mail::SMTP, email.delivery_method
- email = DeliveryMailer.welcome(delivery_method: :test).deliver_now
- assert_instance_of Mail::TestMailer, email.delivery_method
+ stub_any_instance(Mail::SMTP, instance: Mail::SMTP.new({})) do |instance|
+ assert_called(instance, :deliver!) do
+ email = DeliveryMailer.welcome.deliver_now
+ assert_instance_of Mail::SMTP, email.delivery_method
+ email = DeliveryMailer.welcome(delivery_method: :test).deliver_now
+ assert_instance_of Mail::TestMailer, email.delivery_method
+ end
+ end
end
test "delivery method can be customized in subclasses not changing the parent" do
@@ -122,16 +129,16 @@ class MailDeliveryTest < ActiveSupport::TestCase
end
test "delivery method options default to class level options" do
- default_options = {a: "b"}
+ default_options = { a: "b" }
ActionMailer::Base.add_delivery_method :optioned, MyOptionedDelivery, default_options
mail_instance = DeliveryMailer.welcome(delivery_method: :optioned)
assert_equal default_options, mail_instance.delivery_method.options
end
test "delivery method options can be overridden per mail instance" do
- default_options = {a: "b"}
+ default_options = { a: "b" }
ActionMailer::Base.add_delivery_method :optioned, MyOptionedDelivery, default_options
- overridden_options = {a: "a"}
+ overridden_options = { a: "a" }
mail_instance = DeliveryMailer.welcome(delivery_method: :optioned, delivery_method_options: overridden_options)
assert_equal overridden_options, mail_instance.delivery_method.options
end
@@ -140,14 +147,14 @@ class MailDeliveryTest < ActiveSupport::TestCase
settings = {
address: "localhost",
port: 25,
- domain: 'localhost.localdomain',
+ domain: "localhost.localdomain",
user_name: nil,
password: nil,
authentication: nil,
enable_starttls_auto: true
}
assert_equal settings, ActionMailer::Base.smtp_settings
- overridden_options = {user_name: "overridden", password: "somethingobtuse"}
+ overridden_options = { user_name: "overridden", password: "somethingobtuse" }
mail_instance = DeliveryMailer.welcome(delivery_method_options: overridden_options)
delivery_method_instance = mail_instance.delivery_method
assert_equal "overridden", delivery_method_instance.settings[:user_name]
@@ -160,24 +167,29 @@ class MailDeliveryTest < ActiveSupport::TestCase
test "non registered delivery methods raises errors" do
DeliveryMailer.delivery_method = :unknown
- assert_raise RuntimeError do
+ error = assert_raise RuntimeError do
DeliveryMailer.welcome.deliver_now
end
+ assert_equal "Invalid delivery method :unknown", error.message
end
test "undefined delivery methods raises errors" do
DeliveryMailer.delivery_method = nil
- assert_raise RuntimeError do
+ error = assert_raise RuntimeError do
DeliveryMailer.welcome.deliver_now
end
+ assert_equal "Delivery method cannot be nil", error.message
end
test "does not perform deliveries if requested" do
old_perform_deliveries = DeliveryMailer.perform_deliveries
begin
DeliveryMailer.perform_deliveries = false
- Mail::Message.any_instance.expects(:deliver!).never
- DeliveryMailer.welcome.deliver_now
+ stub_any_instance(Mail::Message) do |instance|
+ assert_not_called(instance, :deliver!) do
+ DeliveryMailer.welcome.deliver_now
+ end
+ end
ensure
DeliveryMailer.perform_deliveries = old_perform_deliveries
end
diff --git a/actionmailer/test/fixtures/async_mailer/welcome.erb b/actionmailer/test/fixtures/async_mailer/welcome.erb
deleted file mode 100644
index 01f3f00c63..0000000000
--- a/actionmailer/test/fixtures/async_mailer/welcome.erb
+++ /dev/null
@@ -1 +0,0 @@
-Welcome \ No newline at end of file
diff --git a/actionmailer/test/fixtures/caching_mailer/_partial.html.erb b/actionmailer/test/fixtures/caching_mailer/_partial.html.erb
new file mode 100644
index 0000000000..8e965f52b4
--- /dev/null
+++ b/actionmailer/test/fixtures/caching_mailer/_partial.html.erb
@@ -0,0 +1,3 @@
+<% cache :caching do %>
+ Old fragment caching in a partial
+<% end %>
diff --git a/actionmailer/test/fixtures/caching_mailer/fragment_cache.html.erb b/actionmailer/test/fixtures/caching_mailer/fragment_cache.html.erb
new file mode 100644
index 0000000000..90189627da
--- /dev/null
+++ b/actionmailer/test/fixtures/caching_mailer/fragment_cache.html.erb
@@ -0,0 +1,3 @@
+<% cache :caching do %>
+"Welcome"
+<% end %>
diff --git a/actionmailer/test/fixtures/caching_mailer/fragment_cache_in_partials.html.erb b/actionmailer/test/fixtures/caching_mailer/fragment_cache_in_partials.html.erb
new file mode 100644
index 0000000000..2957d083e8
--- /dev/null
+++ b/actionmailer/test/fixtures/caching_mailer/fragment_cache_in_partials.html.erb
@@ -0,0 +1 @@
+<%= render "partial" %>
diff --git a/actionmailer/test/fixtures/caching_mailer/fragment_caching_options.html.erb b/actionmailer/test/fixtures/caching_mailer/fragment_caching_options.html.erb
new file mode 100644
index 0000000000..0541ac321b
--- /dev/null
+++ b/actionmailer/test/fixtures/caching_mailer/fragment_caching_options.html.erb
@@ -0,0 +1,3 @@
+<%= cache :no_digest, skip_digest: true, expires_in: 0 do %>
+ No Digest
+<% end %>
diff --git a/actionmailer/test/fixtures/caching_mailer/multipart_cache.html.erb b/actionmailer/test/fixtures/caching_mailer/multipart_cache.html.erb
new file mode 100644
index 0000000000..0d26baa2d7
--- /dev/null
+++ b/actionmailer/test/fixtures/caching_mailer/multipart_cache.html.erb
@@ -0,0 +1,3 @@
+<% cache :html_caching, skip_digest: true do %>
+ "Welcome html"
+<% end %>
diff --git a/actionmailer/test/fixtures/caching_mailer/multipart_cache.text.erb b/actionmailer/test/fixtures/caching_mailer/multipart_cache.text.erb
new file mode 100644
index 0000000000..ef97326e03
--- /dev/null
+++ b/actionmailer/test/fixtures/caching_mailer/multipart_cache.text.erb
@@ -0,0 +1,3 @@
+<% cache :text_caching, skip_digest: true do %>
+ "Welcome text"
+<% end %>
diff --git a/actionmailer/test/fixtures/caching_mailer/skip_fragment_cache_digesting.html.erb b/actionmailer/test/fixtures/caching_mailer/skip_fragment_cache_digesting.html.erb
new file mode 100644
index 0000000000..0d52429a81
--- /dev/null
+++ b/actionmailer/test/fixtures/caching_mailer/skip_fragment_cache_digesting.html.erb
@@ -0,0 +1,3 @@
+<%= cache :no_digest, skip_digest: true do %>
+ No Digest
+<% end %>
diff --git a/actionmailer/test/i18n_with_controller_test.rb b/actionmailer/test/i18n_with_controller_test.rb
index 010e44d045..6e75cff347 100644
--- a/actionmailer/test/i18n_with_controller_test.rb
+++ b/actionmailer/test/i18n_with_controller_test.rb
@@ -1,10 +1,12 @@
-require 'abstract_unit'
-require 'action_view'
-require 'action_controller'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "action_view"
+require "action_controller"
class I18nTestMailer < ActionMailer::Base
configure do |c|
- c.assets_dir = ''
+ c.assets_dir = ""
end
def mail_with_i18n_subject(recipient)
@@ -18,14 +20,16 @@ end
class TestController < ActionController::Base
def send_mail
email = I18nTestMailer.mail_with_i18n_subject("test@localhost").deliver_now
- render text: "Mail sent - Subject: #{email.subject}"
+ render plain: "Mail sent - Subject: #{email.subject}"
end
end
class ActionMailerI18nWithControllerTest < ActionDispatch::IntegrationTest
Routes = ActionDispatch::Routing::RouteSet.new
Routes.draw do
- get ':controller(/:action(/:id))'
+ ActiveSupport::Deprecation.silence do
+ get ":controller(/:action(/:id))"
+ end
end
class RoutedRackApp
@@ -52,19 +56,22 @@ class ActionMailerI18nWithControllerTest < ActionDispatch::IntegrationTest
end
def test_send_mail
- Mail::SMTP.any_instance.expects(:deliver!)
- with_translation 'de', email_subject: '[Anmeldung] Willkommen' do
- get '/test/send_mail'
- assert_equal "Mail sent - Subject: [Anmeldung] Willkommen", @response.body
+ stub_any_instance(Mail::SMTP, instance: Mail::SMTP.new({})) do |instance|
+ assert_called(instance, :deliver!) do
+ with_translation "de", email_subject: "[Anmeldung] Willkommen" do
+ 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)
- yield
- ensure
- I18n.backend.reload!
- end
+ def with_translation(locale, data)
+ I18n.backend.store_translations(locale, data)
+ yield
+ ensure
+ I18n.backend.reload!
+ end
end
diff --git a/actionmailer/test/log_subscriber_test.rb b/actionmailer/test/log_subscriber_test.rb
index 3871b16840..7686fd10c9 100644
--- a/actionmailer/test/log_subscriber_test.rb
+++ b/actionmailer/test/log_subscriber_test.rb
@@ -1,7 +1,9 @@
-require 'abstract_unit'
-require 'mailers/base_mailer'
-require 'active_support/log_subscriber/test_helper'
-require 'action_mailer/log_subscriber'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "mailers/base_mailer"
+require "active_support/log_subscriber/test_helper"
+require "action_mailer/log_subscriber"
class AMLogSubscriberTest < ActionMailer::TestCase
include ActiveSupport::LogSubscriber::TestHelper
@@ -26,7 +28,7 @@ class AMLogSubscriberTest < ActionMailer::TestCase
wait
assert_equal(1, @logger.logged(:info).size)
- assert_match(/Sent mail to system@test.lindsaar.net/, @logger.logged(:info).first)
+ assert_match(/Sent mail to system@test\.lindsaar\.net/, @logger.logged(:info).first)
assert_equal(2, @logger.logged(:debug).size)
assert_match(/BaseMailer#welcome: processed outbound mail in [\d.]+ms/, @logger.logged(:debug).first)
@@ -35,8 +37,22 @@ class AMLogSubscriberTest < ActionMailer::TestCase
BaseMailer.deliveries.clear
end
+ def test_deliver_message_when_perform_deliveries_is_false
+ BaseMailer.welcome_without_deliveries.deliver_now
+ wait
+
+ assert_equal(1, @logger.logged(:info).size)
+ assert_match("Skipped sending mail to system@test.lindsaar.net as `perform_deliveries` is false", @logger.logged(:info).first)
+
+ assert_equal(2, @logger.logged(:debug).size)
+ assert_match(/BaseMailer#welcome_without_deliveries: processed outbound mail in [\d.]+ms/, @logger.logged(:debug).first)
+ assert_match("Welcome", @logger.logged(:debug).second)
+ ensure
+ BaseMailer.deliveries.clear
+ end
+
def test_receive_is_notified
- fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email")
+ fixture = File.read(File.expand_path("fixtures/raw_email", __dir__))
TestMailer.receive(fixture)
wait
assert_equal(1, @logger.logged(:info).size)
diff --git a/actionmailer/test/mail_helper_test.rb b/actionmailer/test/mail_helper_test.rb
index ff6b25b0c7..51d6ccb10f 100644
--- a/actionmailer/test/mail_helper_test.rb
+++ b/actionmailer/test/mail_helper_test.rb
@@ -1,12 +1,14 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+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,12 +67,12 @@ The second
end
end
- protected
+ private
- def mail_with_defaults(&block)
- mail(to: "test@localhost", from: "tester@example.com",
- subject: "using helpers", &block)
- end
+ def mail_with_defaults(&block)
+ mail(to: "test@localhost", from: "tester@example.com",
+ subject: "using helpers", &block)
+ end
end
class MailerHelperTest < ActionMailer::TestCase
diff --git a/actionmailer/test/mail_layout_test.rb b/actionmailer/test/mail_layout_test.rb
index 166dd096d4..16d77ed61d 100644
--- a/actionmailer/test/mail_layout_test.rb
+++ b/actionmailer/test/mail_layout_test.rb
@@ -1,7 +1,9 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class AutoLayoutMailer < ActionMailer::Base
- default to: 'test@localhost',
+ default to: "test@localhost",
subject: "You have a mail",
from: "tester@example.com"
@@ -11,7 +13,7 @@ class AutoLayoutMailer < ActionMailer::Base
def spam
@world = "Earth"
- mail(body: render(inline: "Hello, <%= @world %>", layout: 'spam'))
+ mail(body: render(inline: "Hello, <%= @world %>", layout: "spam"))
end
def nolayout
@@ -28,9 +30,9 @@ class AutoLayoutMailer < ActionMailer::Base
end
class ExplicitLayoutMailer < ActionMailer::Base
- layout 'spam', except: [:logout]
+ layout "spam", except: [:logout]
- default to: 'test@localhost',
+ default to: "test@localhost",
subject: "You have a mail",
from: "tester@example.com"
@@ -54,10 +56,10 @@ class LayoutMailerTest < ActiveSupport::TestCase
assert_equal "multipart/alternative", mail.mime_type
assert_equal 2, mail.parts.size
- assert_equal 'text/plain', mail.parts.first.mime_type
+ assert_equal "text/plain", mail.parts.first.mime_type
assert_equal "text/plain layout - text/plain multipart", mail.parts.first.body.to_s
- assert_equal 'text/html', mail.parts.last.mime_type
+ assert_equal "text/html", mail.parts.last.mime_type
assert_equal "Hello from layout text/html multipart", mail.parts.last.body.to_s
end
@@ -66,10 +68,10 @@ class LayoutMailerTest < ActiveSupport::TestCase
assert_equal "multipart/mixed", mail.mime_type
assert_equal 2, mail.parts.size
- assert_equal 'text/plain', mail.parts.first.mime_type
+ assert_equal "text/plain", mail.parts.first.mime_type
assert_equal "text/plain layout - text/plain multipart", mail.parts.first.body.to_s
- assert_equal 'text/html', mail.parts.last.mime_type
+ assert_equal "text/html", mail.parts.last.mime_type
assert_equal "Hello from layout text/html multipart", mail.parts.last.body.to_s
end
diff --git a/actionmailer/test/mailers/asset_mailer.rb b/actionmailer/test/mailers/asset_mailer.rb
index f54a50d00d..7a9aba2629 100644
--- a/actionmailer/test/mailers/asset_mailer.rb
+++ b/actionmailer/test/mailers/asset_mailer.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
class AssetMailer < ActionMailer::Base
self.mailer_name = "asset_mailer"
def welcome
- mail
+ mail
end
end
diff --git a/actionmailer/test/mailers/base_mailer.rb b/actionmailer/test/mailers/base_mailer.rb
index bd991e209e..a3101207dc 100644
--- a/actionmailer/test/mailers/base_mailer.rb
+++ b/actionmailer/test/mailers/base_mailer.rb
@@ -1,13 +1,15 @@
+# frozen_string_literal: true
+
class BaseMailer < ActionMailer::Base
self.mailer_name = "base_mailer"
- default to: 'system@test.lindsaar.net',
- from: 'jose@test.plataformatec.com',
- reply_to: 'mikel@test.lindsaar.net'
+ default to: "system@test.lindsaar.net",
+ from: "jose@test.plataformatec.com",
+ reply_to: "mikel@test.lindsaar.net"
def welcome(hash = {})
- headers['X-SPAM'] = "Not SPAM"
- mail({subject: "The first email on new API!"}.merge!(hash))
+ headers["X-SPAM"] = "Not SPAM"
+ mail({ subject: "The first email on new API!" }.merge!(hash))
end
def welcome_with_headers(hash = {})
@@ -19,6 +21,11 @@ class BaseMailer < ActionMailer::Base
mail(template_name: "welcome", template_path: path)
end
+ def welcome_without_deliveries
+ mail(template_name: "welcome")
+ mail.perform_deliveries = false
+ end
+
def html_only(hash = {})
mail(hash)
end
@@ -28,30 +35,30 @@ class BaseMailer < ActionMailer::Base
end
def inline_attachment
- attachments.inline['logo.png'] = "\312\213\254\232"
+ attachments.inline["logo.png"] = "\312\213\254\232"
mail
end
def attachment_with_content(hash = {})
- attachments['invoice.pdf'] = 'This is test File content'
+ attachments["invoice.pdf"] = "This is test File content"
mail(hash)
end
def attachment_with_hash
- attachments['invoice.jpg'] = { data: ::Base64.encode64("\312\213\254\232)b"),
+ attachments["invoice.jpg"] = { data: ::Base64.encode64("\312\213\254\232)b"),
mime_type: "image/x-jpg",
transfer_encoding: "base64" }
mail
end
def attachment_with_hash_default_encoding
- attachments['invoice.jpg'] = { data: "\312\213\254\232)b",
+ attachments["invoice.jpg"] = { data: "\312\213\254\232)b",
mime_type: "image/x-jpg" }
mail
end
def implicit_multipart(hash = {})
- attachments['invoice.pdf'] = 'This is test File content' if hash.delete(:attachments)
+ attachments["invoice.pdf"] = "This is test File content" if hash.delete(:attachments)
mail(hash)
end
@@ -60,10 +67,10 @@ class BaseMailer < ActionMailer::Base
end
def explicit_multipart(hash = {})
- attachments['invoice.pdf'] = 'This is test File content' if hash.delete(:attachments)
+ attachments["invoice.pdf"] = "This is test File content" if hash.delete(:attachments)
mail(hash) do |format|
- format.text { render text: "TEXT Explicit Multipart" }
- format.html { render text: "HTML Explicit Multipart" }
+ format.text { render plain: "TEXT Explicit Multipart" }
+ format.html { render plain: "HTML Explicit Multipart" }
end
end
@@ -76,14 +83,20 @@ class BaseMailer < ActionMailer::Base
def explicit_multipart_with_any(hash = {})
mail(hash) do |format|
- format.any(:text, :html){ render text: "Format with any!" }
+ format.any(:text, :html) { render plain: "Format with any!" }
+ end
+ end
+
+ def explicit_without_specifying_format_with_any(hash = {})
+ mail(hash) do |format|
+ format.any
end
end
def explicit_multipart_with_options(include_html = false)
mail do |format|
- format.text(content_transfer_encoding: "base64"){ render "welcome" }
- format.html{ render "welcome" } if include_html
+ format.text(content_transfer_encoding: "base64") { render "welcome" }
+ format.html { render "welcome" } if include_html
end
end
@@ -94,18 +107,18 @@ class BaseMailer < ActionMailer::Base
end
end
- def implicit_different_template(template_name='')
+ def implicit_different_template(template_name = "")
mail(template_name: template_name)
end
- def explicit_different_template(template_name='')
+ def explicit_different_template(template_name = "")
mail do |format|
format.text { render template: "#{mailer_name}/#{template_name}" }
format.html { render template: "#{mailer_name}/#{template_name}" }
end
end
- def different_layout(layout_name='')
+ def different_layout(layout_name = "")
mail do |format|
format.text { render layout: layout_name }
format.html { render layout: layout_name }
@@ -125,6 +138,6 @@ class BaseMailer < ActionMailer::Base
end
def with_subject_interpolations
- mail(subject: default_i18n_subject(rapper_or_impersonator: 'Slim Shady'), body: '')
+ mail(subject: default_i18n_subject(rapper_or_impersonator: "Slim Shady"), body: "")
end
end
diff --git a/actionmailer/test/mailers/caching_mailer.rb b/actionmailer/test/mailers/caching_mailer.rb
new file mode 100644
index 0000000000..02f0c6c103
--- /dev/null
+++ b/actionmailer/test/mailers/caching_mailer.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class CachingMailer < ActionMailer::Base
+ self.mailer_name = "caching_mailer"
+
+ def fragment_cache
+ mail(subject: "welcome", template_name: "fragment_cache")
+ end
+
+ def fragment_cache_in_partials
+ mail(subject: "welcome", template_name: "fragment_cache_in_partials")
+ end
+
+ def skip_fragment_cache_digesting
+ mail(subject: "welcome", template_name: "skip_fragment_cache_digesting")
+ end
+
+ def fragment_caching_options
+ mail(subject: "welcome", template_name: "fragment_caching_options")
+ end
+
+ def multipart_cache
+ mail(subject: "welcome", template_name: "multipart_cache")
+ end
+end
diff --git a/actionmailer/test/mailers/delayed_mailer.rb b/actionmailer/test/mailers/delayed_mailer.rb
index 62d4baa434..b0f5ecc2fb 100644
--- a/actionmailer/test/mailers/delayed_mailer.rb
+++ b/actionmailer/test/mailers/delayed_mailer.rb
@@ -1,6 +1,28 @@
+# frozen_string_literal: true
+
+require "active_job/arguments"
+
+class DelayedMailerError < StandardError; end
+
class DelayedMailer < ActionMailer::Base
+ cattr_accessor :last_error
+ cattr_accessor :last_rescue_from_instance
+
+ rescue_from DelayedMailerError do |error|
+ @@last_error = error
+ @@last_rescue_from_instance = self
+ end
+
+ rescue_from ActiveJob::DeserializationError do |error|
+ @@last_error = error
+ @@last_rescue_from_instance = self
+ end
def test_message(*)
- mail(from: 'test-sender@test.com', to: 'test-receiver@test.com', subject: 'Test Subject', body: 'Test Body')
+ mail(from: "test-sender@test.com", to: "test-receiver@test.com", subject: "Test Subject", body: "Test Body")
+ end
+
+ def test_raise(klass_name)
+ raise klass_name.constantize, "boom"
end
end
diff --git a/actionmailer/test/mailers/params_mailer.rb b/actionmailer/test/mailers/params_mailer.rb
new file mode 100644
index 0000000000..84aa336311
--- /dev/null
+++ b/actionmailer/test/mailers/params_mailer.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+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/mailers/proc_mailer.rb b/actionmailer/test/mailers/proc_mailer.rb
index 7e189d861f..76e730bb79 100644
--- a/actionmailer/test/mailers/proc_mailer.rb
+++ b/actionmailer/test/mailers/proc_mailer.rb
@@ -1,17 +1,25 @@
+# frozen_string_literal: true
+
class ProcMailer < ActionMailer::Base
- default to: 'system@test.lindsaar.net',
- 'X-Proc-Method' => Proc.new { Time.now.to_i.to_s },
+ default to: "system@test.lindsaar.net",
+ "X-Proc-Method" => Proc.new { Time.now.to_i.to_s },
subject: Proc.new { give_a_greeting },
- 'x-has-to-proc' => :symbol
+ "x-has-to-proc" => :symbol,
+ "X-Lambda-Arity-0" => ->() { "0" },
+ "X-Lambda-Arity-1-arg" => ->(arg) { arg.computed_value },
+ "X-Lambda-Arity-1-self" => ->(_) { self.computed_value }
def welcome
mail
end
- private
-
- def give_a_greeting
- "Thanks for signing up this afternoon"
+ def computed_value
+ "complex_value"
end
+ private
+
+ def give_a_greeting
+ "Thanks for signing up this afternoon"
+ end
end
diff --git a/actionmailer/test/message_delivery_test.rb b/actionmailer/test/message_delivery_test.rb
index 862ce26187..f8dcb3f4ba 100644
--- a/actionmailer/test/message_delivery_test.rb
+++ b/actionmailer/test/message_delivery_test.rb
@@ -1,7 +1,8 @@
-require 'abstract_unit'
-require 'active_job'
-require 'minitest/mock'
-require 'mailers/delayed_mailer'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_job"
+require "mailers/delayed_mailer"
class MessageDeliveryTest < ActiveSupport::TestCase
include ActiveJob::TestHelper
@@ -13,85 +14,154 @@ class MessageDeliveryTest < ActiveSupport::TestCase
ActionMailer::Base.deliver_later_queue_name = :test_queue
ActionMailer::Base.delivery_method = :test
ActiveJob::Base.logger = Logger.new(nil)
- @mail = DelayedMailer.test_message(1, 2, 3)
- ActionMailer::Base.deliveries.clear
ActiveJob::Base.queue_adapter.perform_enqueued_at_jobs = true
ActiveJob::Base.queue_adapter.perform_enqueued_jobs = true
+
+ DelayedMailer.last_error = nil
+ DelayedMailer.last_rescue_from_instance = nil
+
+ @mail = DelayedMailer.test_message(1, 2, 3)
end
teardown do
+ ActionMailer::Base.deliveries.clear
+
ActiveJob::Base.logger = @previous_logger
ActionMailer::Base.delivery_method = @previous_delivery_method
ActionMailer::Base.deliver_later_queue_name = @previous_deliver_later_queue_name
+
+ DelayedMailer.last_error = nil
+ DelayedMailer.last_rescue_from_instance = nil
end
- test 'should have a message' do
+ test "should have a message" do
assert @mail.message
end
- test 'its message should be a Mail::Message' do
- assert_equal Mail::Message , @mail.message.class
+ test "its message should be a Mail::Message" do
+ assert_equal Mail::Message, @mail.message.class
end
- test 'should respond to .deliver_later' do
+ test "should respond to .deliver_later" do
assert_respond_to @mail, :deliver_later
end
- test 'should respond to .deliver_later!' do
+ test "should respond to .deliver_later!" do
assert_respond_to @mail, :deliver_later!
end
- test 'should respond to .deliver_now' do
+ test "should respond to .deliver_now" do
assert_respond_to @mail, :deliver_now
end
- test 'should respond to .deliver_now!' do
+ test "should respond to .deliver_now!" do
assert_respond_to @mail, :deliver_now!
end
def test_should_enqueue_and_run_correctly_in_activejob
@mail.deliver_later!
assert_equal 1, ActionMailer::Base.deliveries.size
- ensure
- ActionMailer::Base.deliveries.clear
end
- test 'should enqueue the email with :deliver_now delivery method' do
- assert_performed_with(job: ActionMailer::DeliveryJob, args: ['DelayedMailer', 'test_message', 'deliver_now', 1, 2, 3]) do
+ test "should enqueue the email with :deliver_now delivery method" do
+ assert_performed_with(job: ActionMailer::DeliveryJob, args: ["DelayedMailer", "test_message", "deliver_now", 1, 2, 3]) do
@mail.deliver_later
end
end
- test 'should enqueue the email with :deliver_now! delivery method' do
- assert_performed_with(job: ActionMailer::DeliveryJob, args: ['DelayedMailer', 'test_message', 'deliver_now!', 1, 2, 3]) do
+ test "should enqueue the email with :deliver_now! delivery method" do
+ assert_performed_with(job: ActionMailer::DeliveryJob, args: ["DelayedMailer", "test_message", "deliver_now!", 1, 2, 3]) do
@mail.deliver_later!
end
end
- test 'should enqueue a delivery with a delay' do
+ 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
- assert_performed_with(job: ActionMailer::DeliveryJob, at: later_time, args: ['DelayedMailer', 'test_message', 'deliver_now', 1, 2, 3]) do
+ test "should enqueue a delivery at a specific time" do
+ 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
end
- test 'should enqueue the job on the correct queue' do
- assert_performed_with(job: ActionMailer::DeliveryJob, args: ['DelayedMailer', 'test_message', 'deliver_now', 1, 2, 3], queue: "test_queue") do
+ test "should enqueue the job on the correct queue" do
+ assert_performed_with(job: ActionMailer::DeliveryJob, args: ["DelayedMailer", "test_message", "deliver_now", 1, 2, 3], queue: "test_queue") do
@mail.deliver_later
end
end
- test 'can override the queue when enqueuing mail' do
- assert_performed_with(job: ActionMailer::DeliveryJob, args: ['DelayedMailer', 'test_message', 'deliver_now', 1, 2, 3], queue: "another_queue") do
+ test "should enqueue the job with the correct delivery job" do
+ old_delivery_job = DelayedMailer.delivery_job
+ DelayedMailer.delivery_job = DummyJob
+
+ assert_performed_with(job: DummyJob, args: ["DelayedMailer", "test_message", "deliver_now", 1, 2, 3]) do
+ @mail.deliver_later
+ end
+
+ DelayedMailer.delivery_job = old_delivery_job
+ end
+
+ class DummyJob < ActionMailer::DeliveryJob; end
+
+ test "can override the queue when enqueuing mail" do
+ assert_performed_with(job: ActionMailer::DeliveryJob, args: ["DelayedMailer", "test_message", "deliver_now", 1, 2, 3], queue: "another_queue") do
@mail.deliver_later(queue: :another_queue)
end
end
+
+ test "deliver_later after accessing the message is disallowed" do
+ @mail.message # Load the message, which calls the mailer method.
+
+ assert_raise RuntimeError do
+ @mail.deliver_later
+ end
+ end
+
+ test "job delegates error handling to mailer" do
+ # Superclass not rescued by mailer's rescue_from RuntimeError
+ message = DelayedMailer.test_raise("StandardError")
+ assert_raise(StandardError) { message.deliver_later }
+ assert_nil DelayedMailer.last_error
+ assert_nil DelayedMailer.last_rescue_from_instance
+
+ # Rescued by mailer's rescue_from RuntimeError
+ message = DelayedMailer.test_raise("DelayedMailerError")
+ assert_nothing_raised { message.deliver_later }
+ assert_equal "boom", DelayedMailer.last_error.message
+ assert_kind_of DelayedMailer, DelayedMailer.last_rescue_from_instance
+ end
+
+ class DeserializationErrorFixture
+ include GlobalID::Identification
+
+ def self.find(id)
+ raise "boom, missing find"
+ end
+
+ attr_reader :id
+ def initialize(id = 1)
+ @id = id
+ end
+
+ def to_global_id(options = {})
+ super app: "foo"
+ end
+ end
+
+ test "job delegates deserialization errors to mailer class" do
+ # Inject an argument that can't be deserialized.
+ message = DelayedMailer.test_message(DeserializationErrorFixture.new)
+
+ # DeserializationError is raised, rescued, and delegated to the handler
+ # on the mailer class.
+ assert_nothing_raised { message.deliver_later }
+ assert_equal DelayedMailer, DelayedMailer.last_rescue_from_instance
+ assert_equal "Error while trying to deserialize arguments: boom, missing find", DelayedMailer.last_error.message
+ end
end
diff --git a/actionmailer/test/parameterized_test.rb b/actionmailer/test/parameterized_test.rb
new file mode 100644
index 0000000000..d3b34fc3e3
--- /dev/null
+++ b/actionmailer/test/parameterized_test.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+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
+
+ @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
+
+ test "should enqueue a parameterized request with the correct delivery job" do
+ old_delivery_job = ParamsMailer.parameterized_delivery_job
+ ParamsMailer.parameterized_delivery_job = ParameterizedDummyJob
+
+ assert_performed_with(job: ParameterizedDummyJob, args: ["ParamsMailer", "invitation", "deliver_now", { inviter: "david@basecamp.com", invitee: "jason@basecamp.com" } ]) do
+ @mail.deliver_later
+ end
+
+ ParamsMailer.parameterized_delivery_job = old_delivery_job
+ end
+
+ class ParameterizedDummyJob < ActionMailer::Parameterized::DeliveryJob; end
+end
diff --git a/actionmailer/test/test_case_test.rb b/actionmailer/test/test_case_test.rb
index 86fd37bea6..7b9647d295 100644
--- a/actionmailer/test/test_case_test.rb
+++ b/actionmailer/test/test_case_test.rb
@@ -1,8 +1,48 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class TestTestMailer < ActionMailer::Base
end
+class ClearTestDeliveriesMixinTest < ActiveSupport::TestCase
+ include ActionMailer::TestCase::ClearTestDeliveries
+
+ def before_setup
+ ActionMailer::Base.delivery_method, @original_delivery_method = :test, ActionMailer::Base.delivery_method
+ ActionMailer::Base.deliveries << "better clear me, setup"
+ super
+ end
+
+ def after_teardown
+ super
+ assert_equal [], ActionMailer::Base.deliveries
+ ActionMailer::Base.delivery_method = @original_delivery_method
+ end
+
+ def test_deliveries_are_cleared_on_setup_and_teardown
+ assert_equal [], ActionMailer::Base.deliveries
+ ActionMailer::Base.deliveries << "better clear me, teardown"
+ end
+end
+
+class MailerDeliveriesClearingTest < ActionMailer::TestCase
+ def before_setup
+ ActionMailer::Base.deliveries << "better clear me, setup"
+ super
+ end
+
+ def after_teardown
+ super
+ assert_equal [], ActionMailer::Base.deliveries
+ end
+
+ def test_deliveries_are_cleared_on_setup_and_teardown
+ assert_equal [], ActionMailer::Base.deliveries
+ ActionMailer::Base.deliveries << "better clear me, teardown"
+ end
+end
+
class CrazyNameMailerTest < ActionMailer::TestCase
tests TestTestMailer
@@ -20,7 +60,7 @@ class CrazySymbolNameMailerTest < ActionMailer::TestCase
end
class CrazyStringNameMailerTest < ActionMailer::TestCase
- tests 'test_test_mailer'
+ tests "test_test_mailer"
def test_set_mailer_class_manual_using_string
assert_equal TestTestMailer, self.class.mailer_class
diff --git a/actionmailer/test/test_helper_test.rb b/actionmailer/test/test_helper_test.rb
index 0a4bc75d3e..d31170706b 100644
--- a/actionmailer/test/test_helper_test.rb
+++ b/actionmailer/test/test_helper_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'active_support/testing/stream'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/testing/stream"
class TestHelperMailer < ActionMailer::Base
def test
@@ -8,6 +10,18 @@ class TestHelperMailer < ActionMailer::Base
to: "test@example.com",
from: "tester@example.com"
end
+
+ def test_args(recipient, name)
+ mail body: render(inline: "Hello, #{name}"),
+ to: recipient,
+ from: "tester@example.com"
+ end
+
+ def test_parameter_args
+ mail body: render(inline: "All is #{params[:all]}"),
+ to: "test@example.com",
+ from: "tester@example.com"
+ end
end
class TestHelperMailerTest < ActionMailer::TestCase
@@ -40,11 +54,11 @@ class TestHelperMailerTest < ActionMailer::TestCase
end
def test_encode
- assert_equal '=?UTF-8?Q?This_is_=E3=81=82_string?=', encode('This is あ string')
+ assert_equal "This is あ string", Mail::Encodings.q_value_decode(encode("This is あ string"))
end
def test_read_fixture
- assert_equal ['Welcome!'], read_fixture('welcome')
+ assert_equal ["Welcome!"], read_fixture("welcome")
end
def test_assert_emails
@@ -55,6 +69,16 @@ class TestHelperMailerTest < ActionMailer::TestCase
end
end
+ def test_assert_emails_with_enqueued_emails
+ assert_nothing_raised do
+ assert_emails 1 do
+ silence_stream($stdout) do
+ TestHelperMailer.test.deliver_later
+ end
+ end
+ end
+ end
+
def test_repeated_assert_emails_calls
assert_nothing_raised do
assert_emails 1 do
@@ -91,6 +115,18 @@ class TestHelperMailerTest < ActionMailer::TestCase
end
end
+ def test_assert_no_emails_with_enqueued_emails
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_no_emails do
+ silence_stream($stdout) do
+ TestHelperMailer.test.deliver_later
+ end
+ end
+ end
+
+ assert_match(/0 .* but 1/, error.message)
+ end
+
def test_assert_emails_too_few_sent
error = assert_raise ActiveSupport::TestCase::Assertion do
assert_emails 2 do
@@ -143,6 +179,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 +222,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
@@ -187,6 +241,63 @@ class TestHelperMailerTest < ActionMailer::TestCase
assert_match(/0 .* but 1/, error.message)
end
+
+ def test_assert_enqueued_email_with
+ assert_nothing_raised do
+ assert_enqueued_email_with TestHelperMailer, :test do
+ silence_stream($stdout) do
+ TestHelperMailer.test.deliver_later
+ end
+ end
+ end
+ end
+
+ def test_assert_enqueued_email_with_with_no_block
+ assert_nothing_raised do
+ silence_stream($stdout) do
+ TestHelperMailer.test.deliver_later
+ assert_enqueued_email_with TestHelperMailer, :test
+ end
+ end
+ end
+
+ def test_assert_enqueued_email_with_with_args
+ assert_nothing_raised do
+ assert_enqueued_email_with TestHelperMailer, :test_args, args: ["some_email", "some_name"] do
+ silence_stream($stdout) do
+ TestHelperMailer.test_args("some_email", "some_name").deliver_later
+ end
+ end
+ end
+ end
+
+ def test_assert_enqueued_email_with_with_no_block_with_args
+ assert_nothing_raised do
+ silence_stream($stdout) do
+ TestHelperMailer.test_args("some_email", "some_name").deliver_later
+ assert_enqueued_email_with TestHelperMailer, :test_args, args: ["some_email", "some_name"]
+ end
+ end
+ end
+
+ def test_assert_enqueued_email_with_with_parameterized_args
+ assert_nothing_raised do
+ assert_enqueued_email_with TestHelperMailer, :test_parameter_args, args: { all: "good" } do
+ silence_stream($stdout) do
+ TestHelperMailer.with(all: "good").test_parameter_args.deliver_later
+ end
+ end
+ end
+ end
+
+ def test_assert_enqueued_email_with_with_no_block_with_parameterized_args
+ assert_nothing_raised do
+ silence_stream($stdout) do
+ TestHelperMailer.with(all: "good").test_parameter_args.deliver_later
+ assert_enqueued_email_with TestHelperMailer, :test_parameter_args, args: { all: "good" }
+ end
+ end
+ end
end
class AnotherTestHelperMailerTest < ActionMailer::TestCase
@@ -198,6 +309,6 @@ class AnotherTestHelperMailerTest < ActionMailer::TestCase
def test_setup_shouldnt_conflict_with_mailer_setup
assert_kind_of Mail::Message, @expected
- assert_equal 'a value', @test_var
+ assert_equal "a value", @test_var
end
end
diff --git a/actionmailer/test/url_test.rb b/actionmailer/test/url_test.rb
index 7928fe9542..a926663a9f 100644
--- a/actionmailer/test/url_test.rb
+++ b/actionmailer/test/url_test.rb
@@ -1,20 +1,27 @@
-require 'abstract_unit'
-require 'action_controller'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "action_controller"
class WelcomeController < ActionController::Base
end
AppRoutes = ActionDispatch::Routing::RouteSet.new
-class ActionMailer::Base
- include AppRoutes.url_helpers
+AppRoutes.draw do
+ get "/welcome" => "foo#bar", as: "welcome"
+ get "/dummy_model" => "foo#baz", as: "dummy_model"
+ get "/welcome/greeting", to: "welcome#greeting"
+ get "/a/b(/:id)", to: "a#b"
end
class UrlTestMailer < ActionMailer::Base
- default_url_options[:host] = 'www.basecamphq.com'
+ include AppRoutes.url_helpers
+
+ default_url_options[:host] = "www.basecamphq.com"
configure do |c|
- c.assets_dir = '' # To get the tests to pass
+ c.assets_dir = "" # To get the tests to pass
end
def signed_up_with_url(recipient)
@@ -27,14 +34,14 @@ class UrlTestMailer < ActionMailer::Base
def exercise_url_for(options)
@options = options
@url = url_for(@options)
- mail(from: 'from@example.com', to: 'to@example.com', subject: 'subject')
+ mail(from: "from@example.com", to: "to@example.com", subject: "subject")
end
end
class ActionMailerUrlTest < ActionMailer::TestCase
class DummyModel
def self.model_name
- OpenStruct.new(route_key: 'dummy_model')
+ OpenStruct.new(route_key: "dummy_model")
end
def persisted?
@@ -50,11 +57,11 @@ class ActionMailerUrlTest < ActionMailer::TestCase
end
end
- def encode( text, charset="UTF-8" )
- quoted_printable( text, charset )
+ def encode(text, charset = "UTF-8")
+ quoted_printable(text, charset)
end
- def new_mail( charset="UTF-8" )
+ def new_mail(charset = "UTF-8")
mail = Mail.new
mail.mime_version = "1.0"
if charset
@@ -64,7 +71,7 @@ class ActionMailerUrlTest < ActionMailer::TestCase
end
def assert_url_for(expected, options, relative = false)
- expected = "http://www.basecamphq.com#{expected}" if expected.start_with?('/') && !relative
+ expected = "http://www.basecamphq.com#{expected}" if expected.start_with?("/") && !relative
urls = UrlTestMailer.exercise_url_for(options).body.to_s.chomp.split
assert_equal expected, urls.first
@@ -72,50 +79,39 @@ class ActionMailerUrlTest < ActionMailer::TestCase
end
def setup
- @recipient = 'test@localhost'
+ @recipient = "test@localhost"
end
def test_url_for
UrlTestMailer.delivery_method = :test
- AppRoutes.draw do
- get ':controller(/:action(/:id))'
- get '/welcome' => 'foo#bar', as: 'welcome'
- get '/dummy_model' => 'foo#baz', as: 'dummy_model'
- end
-
# string
- assert_url_for 'http://foo/', 'http://foo/'
+ assert_url_for "http://foo/", "http://foo/"
# symbol
- assert_url_for '/welcome', :welcome
+ assert_url_for "/welcome", :welcome
# hash
- assert_url_for '/a/b/c', controller: 'a', action: 'b', id: 'c'
- assert_url_for '/a/b/c', {controller: 'a', action: 'b', id: 'c', only_path: true}, true
+ assert_url_for "/a/b/c", controller: "a", action: "b", id: "c"
+ assert_url_for "/a/b/c", { controller: "a", action: "b", id: "c", only_path: true }, true
# model
- assert_url_for '/dummy_model', DummyModel.new
+ assert_url_for "/dummy_model", DummyModel.new
# class
- assert_url_for '/dummy_model', DummyModel
+ assert_url_for "/dummy_model", DummyModel
# array
- assert_url_for '/dummy_model' , [DummyModel]
+ assert_url_for "/dummy_model", [DummyModel]
end
def test_signed_up_with_url
UrlTestMailer.delivery_method = :test
- AppRoutes.draw do
- get ':controller(/:action(/:id))'
- get '/welcome' => "foo#bar", as: "welcome"
- end
-
expected = new_mail
expected.to = @recipient
expected.subject = "[Signed up] Welcome #{@recipient}"
- expected.body = "Hello there,\n\nMr. #{@recipient}. Please see our greeting at http://example.com/welcome/greeting http://www.basecamphq.com/welcome\n\n<img alt=\"Somelogo\" src=\"/images/somelogo.png\" />"
+ expected.body = "Hello there,\n\nMr. #{@recipient}. Please see our greeting at http://example.com/welcome/greeting http://www.basecamphq.com/welcome\n\n<img src=\"/images/somelogo.png\" />"
expected.from = "system@loudthinking.com"
expected.date = Time.local(2004, 12, 12)
expected.content_type = "text/html"
@@ -124,15 +120,15 @@ class ActionMailerUrlTest < ActionMailer::TestCase
assert_nothing_raised { created = UrlTestMailer.signed_up_with_url(@recipient) }
assert_not_nil created
- expected.message_id = '<123@456>'
- created.message_id = '<123@456>'
+ expected.message_id = "<123@456>"
+ created.message_id = "<123@456>"
assert_dom_equal expected.encoded, created.encoded
assert_nothing_raised { UrlTestMailer.signed_up_with_url(@recipient).deliver_now }
assert_not_nil ActionMailer::Base.deliveries.first
delivered = ActionMailer::Base.deliveries.first
- delivered.message_id = '<123@456>'
+ delivered.message_id = "<123@456>"
assert_dom_equal expected.encoded, delivered.encoded
end
end
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md
index 1c0c3c4e10..90cf989100 100644
--- a/actionpack/CHANGELOG.md
+++ b/actionpack/CHANGELOG.md
@@ -1,398 +1,166 @@
-* Allow chaining the :only and :except options for routing resource(s).
+* Allow rescue from parameter parse errors:
- resource :account, :only => [:show, :destroy], :except => :destroy
+ ```
+ rescue_from ActionDispatch::Http::Parameters::ParseError do
+ head :unauthorized
+ end
+ ```
- This now yields only the show action. This chaining can be useful for passing optional :except
- options to code that makes use of the :only option.
+ *Gannon McGibbon*, *Josh Cheek*
- *Michael Colavita*
+* Reset Capybara sessions if failed system test screenshot raising an exception.
-* Add ability to filter parameters based on parent keys.
+ Reset Capybara sessions if `take_failed_screenshot` raise exception
+ in system test `after_teardown`.
- # matches {credit_card: {code: "xxxx"}}
- # doesn't match {file: { code: "xxxx"}}
- config.filter_parameters += [ "credit_card.code" ]
+ *Maxim Perepelitsa*
- See #13897.
+* Use request object for context if there's no controller
- *Guillaume Malette*
+ There is no controller instance when using a redirect route or a
+ mounted rack application so pass the request object as the context
+ when resolving dynamic CSP sources in this scenario.
-* Deprecate passing first parameter as `Hash` and default status code for `head` method.
+ Fixes #34200.
- *Mehmet Emin İNAÇ*
+ *Andrew White*
-* Adds`Rack::Utils::ParameterTypeError` and `Rack::Utils::InvalidParameterError`
- to the rescue_responses hash in `ExceptionWrapper` (Rack recommends
- integrators serve 400s for both of these).
+* Apply mapping to symbols returned from dynamic CSP sources
- *Grey Baker*
+ Previously if a dynamic source returned a symbol such as :self it
+ would be converted to a string implicity, e.g:
-* Add support for API only apps.
- ActionController::API is added as a replacement of
- ActionController::Base for this kind of applications.
+ policy.default_src -> { :self }
- *Santiago Pastorino & Jorge Bejar*
+ would generate the header:
-* Remove `assigns` and `assert_template`. Both methods have been extracted
- into a gem at https://github.com/rails/rails-controller-testing.
+ Content-Security-Policy: default-src self
- See #18950.
+ and now it generates:
- *Alan Guo Xiang Tan*
+ Content-Security-Policy: default-src 'self'
-* `FileHandler` and `Static` middleware initializers accept `index` argument
- to configure the directory index file name. Defaults to `index` (as in
- `index.html`).
+ *Andrew White*
- See #20017.
+* Add `ActionController::Parameters#each_value`.
- *Eliot Sykes*
+ *Lukáš Zapletal*
-* Deprecate `:nothing` option for `render` method.
+* Deprecate `ActionDispatch::Http::ParameterFilter` in favor of `ActiveSupport::ParameterFilter`.
- *Mehmet Emin İNAÇ*
+ *Yoshiyuki Kinjo*
-* Fix `rake routes` not showing the right format when
- nesting multiple routes.
+* Remove undocumented `params` option from `url_for` helper.
- See #18373.
+ *Ilkka Oksanen*
- *Ravil Bayramgalin*
+* Encode Content-Disposition filenames on `send_data` and `send_file`.
+ Previously, `send_data 'data', filename: "\u{3042}.txt"` sends
+ `"filename=\"\u{3042}.txt\""` as Content-Disposition and it can be
+ garbled.
+ Now it follows [RFC 2231](https://tools.ietf.org/html/rfc2231) and
+ [RFC 5987](https://tools.ietf.org/html/rfc5987) and sends
+ `"filename=\"%3F.txt\"; filename*=UTF-8''%E3%81%82.txt"`.
+ Most browsers can find filename correctly and old browsers fallback to ASCII
+ converted name.
-* Add ability to override default form builder for a controller.
+ *Fumiaki Matsushima*
- class AdminController < ApplicationController
- default_form_builder AdminFormBuilder
- end
-
- *Kevin McPhillips*
-
-* For actions with no corresponding templates, render `head :no_content`
- instead of raising an error. This allows for slimmer API controller
- methods that simply work, without needing further instructions.
-
- See #19036.
-
- *Stephen Bussey*
-
-* Provide friendlier access to request variants.
-
- request.variant = :phone
- request.variant.phone? # true
- request.variant.tablet? # false
-
- request.variant = [:phone, :tablet]
- request.variant.phone? # true
- request.variant.desktop? # false
- request.variant.any?(:phone, :desktop) # true
- request.variant.any?(:desktop, :watch) # false
-
- *George Claghorn*
-
-* Fix regression where a gzip file response would have a Content-type,
- even when it was a 304 status code.
-
- See #19271.
-
- *Kohei Suzuki*
-
-* Fix handling of empty `X_FORWARDED_HOST` header in `raw_host_with_port`.
-
- Previously, an empty `X_FORWARDED_HOST` header would cause
- `Actiondispatch::Http:URL.raw_host_with_port` to return `nil`, causing
- `Actiondispatch::Http:URL.host` to raise a `NoMethodError`.
-
- *Adam Forsyth*
-
-* Allow `Bearer` as token-keyword in `Authorization-Header`.
-
- Aditionally to `Token`, the keyword `Bearer` is acceptable as a keyword
- for the auth-token. The `Bearer` keyword is described in the original
- OAuth RFC and used in libraries like Angular-JWT.
-
- See #19094.
-
- *Peter Schröder*
-
-* Drop request class from RouteSet constructor.
-
- If you would like to use a custom request class, please subclass and implement
- the `request_class` method.
-
- *tenderlove@ruby-lang.org*
-
-* Fallback to `ENV['RAILS_RELATIVE_URL_ROOT']` in `url_for`.
-
- Fixed an issue where the `RAILS_RELATIVE_URL_ROOT` environment variable is not
- prepended to the path when `url_for` is called. If `SCRIPT_NAME` (used by Rack)
- is set, it takes precedence.
-
- Fixes #5122.
-
- *Yasyf Mohamedali*
-
-* Partitioning of routes is now done when the routes are being drawn. This
- helps to decrease the time spent filtering the routes during the first request.
-
- *Guo Xiang Tan*
-
-* Fix regression in functional tests. Responses should have default headers
- assigned.
-
- See #18423.
-
- *Jeremy Kemper*, *Yves Senn*
-
-* Deprecate AbstractController#skip_action_callback in favor of individual skip_callback methods
- (which can be made to raise an error if no callback was removed).
-
- *Iain Beeston*
-
-* Alias the `ActionDispatch::Request#uuid` method to `ActionDispatch::Request#request_id`.
- Due to implementation, `config.log_tags = [:request_id]` also works in substitute
- for `config.log_tags = [:uuid]`.
-
- *David Ilizarov*
-
-* Change filter on /rails/info/routes to use an actual path regexp from rails
- and not approximate javascript version. Oniguruma supports much more
- extensive list of features than javascript regexp engine.
-
- Fixes #18402.
-
- *Ravil Bayramgalin*
-
-* Non-string authenticity tokens do not raise NoMethodError when decoding
- the masked token.
-
- *Ville Lautanala*
-
-* Add `http_cache_forever` to Action Controller, so we can cache a response
- that never gets expired.
-
- *arthurnn*
-
-* `ActionController#translate` supports symbols as shortcuts.
- When shortcut is given it also lookups without action name.
-
- *Max Melentiev*
-
-* Expand `ActionController::ConditionalGet#fresh_when` and `stale?` to also
- accept a collection of records as the first argument, so that the
- following code can be written in a shorter form.
-
- # Before
- def index
- @articles = Article.all
- fresh_when(etag: @articles, last_modified: @articles.maximum(:updated_at))
- end
+* Expose `ActionController::Parameters#each_key` which allows iterating over
+ keys without allocating an array.
- # After
- def index
- @articles = Article.all
- fresh_when(@articles)
- end
-
- *claudiob*
-
-* Explicitly ignored wildcard verbs when searching for HEAD routes before fallback
-
- Fixes an issue where a mounted rack app at root would intercept the HEAD
- request causing an incorrect behavior during the fall back to GET requests.
-
- Example:
-
- draw do
- get '/home' => 'test#index'
- mount rack_app, at: '/'
- end
- head '/home'
- assert_response :success
-
- In this case, a HEAD request runs through the routes the first time and fails
- to match anything. Then, it runs through the list with the fallback and matches
- `get '/home'`. The original behavior would match the rack app in the first pass.
-
- *Terence Sun*
-
-* Migrating xhr methods to keyword arguments syntax
- in `ActionController::TestCase` and `ActionDispatch::Integration`
-
- Old syntax:
-
- xhr :get, :create, params: { id: 1 }
-
- New syntax example:
-
- get :create, params: { id: 1 }, xhr: true
-
- *Kir Shatrov*
-
-* Migrating to keyword arguments syntax in `ActionController::TestCase` and
- `ActionDispatch::Integration` HTTP request methods.
-
- Example:
-
- post :create, params: { y: x }, session: { a: 'b' }
- get :view, params: { id: 1 }
- get :view, params: { id: 1 }, format: :json
-
- *Kir Shatrov*
-
-* Preserve default url options when generating URLs.
-
- Fixes an issue that would cause `default_url_options` to be lost when
- generating URLs with fewer positional arguments than parameters in the
- route definition.
-
- *Tekin Suleyman*
-
-* Deprecate `*_via_redirect` integration test methods.
-
- Use `follow_redirect!` manually after the request call for the same behavior.
+ *Richard Schneeman*
- *Aditya Kapoor*
+* Purpose metadata for signed/encrypted cookies.
-* Add `ActionController::Renderer` to render arbitrary templates
- outside controller actions.
+ Rails can now thwart attacks that attempt to copy signed/encrypted value
+ of a cookie and use it as the value of another cookie.
- Its functionality is accessible through class methods `render` and
- `renderer` of `ActionController::Base`.
+ It does so by stashing the cookie-name in the purpose field which is
+ then signed/encrypted along with the cookie value. Then, on a server-side
+ read, we verify the cookie-names and discard any attacked cookies.
- *Ravil Bayramgalin*
+ Enable `action_dispatch.use_cookies_with_metadata` to use this feature, which
+ writes cookies with the new purpose and expiry metadata embedded.
-* Support `:assigns` option when rendering with controllers/mailers.
+ *Assain Jaleel*
- *Ravil Bayramgalin*
+* Raises `ActionController::RespondToMismatchError` with confliciting `respond_to` invocations.
-* Default headers, removed in controller actions, are no longer reapplied on
- the test response.
+ `respond_to` can match multiple types and lead to undefined behavior when
+ multiple invocations are made and the types do not match:
- *Jonas Baumann*
-
-* Deprecate all `*_filter` callbacks in favor of `*_action` callbacks.
-
- *Rafael Mendonça França*
-
-* Allow you to pass `prepend: false` to `protect_from_forgery` to have the
- verification callback appended instead of prepended to the chain.
- This allows you to let the verification step depend on prior callbacks.
-
- Example:
-
- class ApplicationController < ActionController::Base
- before_action :authenticate
- protect_from_forgery prepend: false, unless: -> { @authenticated_by.oauth? }
-
- private
- def authenticate
- if oauth_request?
- # authenticate with oauth
- @authenticated_by = 'oauth'.inquiry
- else
- # authenticate with cookies
- @authenticated_by = 'cookie'.inquiry
- end
+ respond_to do |outer_type|
+ outer_type.js do
+ respond_to do |inner_type|
+ inner_type.html { render body: "HTML" }
end
+ end
end
- *Josef Šimánek*
-
-* Remove `ActionController::HideActions`.
-
- *Ravil Bayramgalin*
-
-* Remove `respond_to`/`respond_with` placeholder methods, this functionality
- has been extracted to the `responders` gem.
-
- *Carlos Antonio da Silva*
-
-* Remove deprecated assertion files.
-
- *Rafael Mendonça França*
+ *Patrick Toomey*
-* Remove deprecated usage of string keys in URL helpers.
+* `ActionDispatch::Http::UploadedFile` now delegates `to_path` to its tempfile.
- *Rafael Mendonça França*
+ This allows uploaded file objects to be passed directly to `File.read`
+ without raising a `TypeError`:
-* Remove deprecated `only_path` option on `*_path` helpers.
+ uploaded_file = ActionDispatch::Http::UploadedFile.new(tempfile: tmp_file)
+ File.read(uploaded_file)
- *Rafael Mendonça França*
+ *Aaron Kromer*
-* Remove deprecated `NamedRouteCollection#helpers`.
+* Pass along arguments to underlying `get` method in `follow_redirect!`.
- *Rafael Mendonça França*
+ Now all arguments passed to `follow_redirect!` are passed to the underlying
+ `get` method. This for example allows to set custom headers for the
+ redirection request to the server.
-* Remove deprecated support to define routes with `:to` option that doesn't contain `#`.
+ follow_redirect!(params: { foo: :bar })
- *Rafael Mendonça França*
+ *Remo Fritzsche*
-* Remove deprecated `ActionDispatch::Response#to_ary`.
+* Introduce a new error page to when the implicit render page is accessed in the browser.
- *Rafael Mendonça França*
+ Now instead of showing an error page that with exception and backtraces we now show only
+ one informative page.
-* Remove deprecated `ActionDispatch::Request#deep_munge`.
+ *Vinicius Stock*
- *Rafael Mendonça França*
+* Introduce `ActionDispatch::DebugExceptions.register_interceptor`.
-* Remove deprecated `ActionDispatch::Http::Parameters#symbolized_path_parameters`.
+ Exception aware plugin authors can use the newly introduced
+ `.register_interceptor` method to get the processed exception, instead of
+ monkey patching DebugExceptions.
- *Rafael Mendonça França*
-
-* Remove deprecated option `use_route` in controller tests.
-
- *Rafael Mendonça França*
-
-* Ensure `append_info_to_payload` is called even if an exception is raised.
-
- Fixes an issue where when an exception is raised in the request the additional
- payload data is not available.
-
- See:
- * #14903
- * https://github.com/roidrage/lograge/issues/37
-
- *Dieter Komendera*, *Margus Pärt*
-
-* Correctly rely on the response's status code to handle calls to `head`.
-
- *Robin Dupret*
-
-* Using `head` method returns empty response_body instead
- of returning a single space " ".
-
- The old behavior was added as a workaround for a bug in an early
- version of Safari, where the HTTP headers are not returned correctly
- if the response body has a 0-length. This is been fixed since and
- the workaround is no longer necessary.
+ ActionDispatch::DebugExceptions.register_interceptor do |request, exception|
+ HypoteticalPlugin.capture_exception(request, exception)
+ end
- Fixes #18253.
+ *Genadi Samokovarov*
- *Prathamesh Sonpatki*
+* Output only one Content-Security-Policy nonce header value per request.
-* Fix how polymorphic routes works with objects that implement `to_model`.
+ Fixes #32597.
- *Travis Grathwell*
+ *Andrey Novikov*, *Andrew White*
-* Stop converting empty arrays in `params` to `nil`.
+* Move default headers configuration into their own module that can be included in controllers.
- This behavior was introduced in response to CVE-2012-2660, CVE-2012-2694
- and CVE-2013-0155
+ *Kevin Deisz*
- ActiveRecord now issues a safe query when passing an empty array into
- a where clause, so there is no longer a need to defend against this type
- of input (any nils are still stripped from the array).
+* Add method `dig` to `session`.
- *Chris Sinjakli*
+ *claudiob*, *Takumi Shotoku*
-* Fixed usage of optional scopes in url helpers.
+* Controller level `force_ssl` has been deprecated in favor of
+ `config.force_ssl`.
- *Alex Robbin*
+ *Derek Prior*
-* Fixed handling of positional url helper arguments when `format: false`.
+* Rails 6 requires Ruby 2.4.1 or newer.
- Fixes #17819.
+ *Jeremy Daer*
- *Andrew White*, *Tatiana Soukiassian*
-Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/actionpack/CHANGELOG.md) for previous changes.
+Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/actionpack/CHANGELOG.md) for previous changes.
diff --git a/actionpack/MIT-LICENSE b/actionpack/MIT-LICENSE
index 3ec7a617cf..1cb3add0fc 100644
--- a/actionpack/MIT-LICENSE
+++ b/actionpack/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2015 David Heinemeier Hansson
+Copyright (c) 2004-2018 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/actionpack/README.rdoc b/actionpack/README.rdoc
index 44c980b070..f56230ffa0 100644
--- a/actionpack/README.rdoc
+++ b/actionpack/README.rdoc
@@ -28,9 +28,9 @@ can be used outside of Rails.
The latest version of Action Pack can be installed with RubyGems:
- % gem install actionpack
+ $ gem install actionpack
-Source code can be downloaded as part of the Rails project on GitHub
+Source code can be downloaded as part of the Rails project on GitHub:
* https://github.com/rails/rails/tree/master/actionpack
@@ -39,20 +39,19 @@ Source code can be downloaded as part of the Rails project on GitHub
Action Pack is released under the MIT license:
-* http://www.opensource.org/licenses/MIT
+* https://opensource.org/licenses/MIT
== Support
-API documentation is at
+API documentation is at:
* http://api.rubyonrails.org
-Bug reports can be filed for the Ruby on Rails project here:
+Bug reports for the Ruby on Rails project can be filed here:
* https://github.com/rails/rails/issues
Feature requests should be discussed on the rails-core mailing list here:
* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core
-
diff --git a/actionpack/Rakefile b/actionpack/Rakefile
index 601263bfac..e99eb1723a 100644
--- a/actionpack/Rakefile
+++ b/actionpack/Rakefile
@@ -1,13 +1,17 @@
-require 'rake/testtask'
+# frozen_string_literal: true
-test_files = Dir.glob('test/**/*_test.rb')
+require "rake/testtask"
+
+test_files = Dir.glob("test/**/*_test.rb")
desc "Default Task"
-task :default => :test
+task default: :test
+
+task :package
# Run the unit tests
Rake::TestTask.new do |t|
- t.libs << 'test'
+ t.libs << "test"
t.test_files = test_files
t.warning = true
@@ -18,19 +22,19 @@ end
namespace :test do
task :isolated do
test_files.all? do |file|
- sh(Gem.ruby, '-w', '-Ilib:test', file)
- end or raise "Failures"
+ sh(Gem.ruby, "-w", "-Ilib:test", file)
+ end || raise("Failures")
end
end
task :lines do
- load File.expand_path('..', File.dirname(__FILE__)) + '/tools/line_statistics'
+ load File.expand_path("../tools/line_statistics", __dir__)
files = FileList["lib/**/*.rb"]
CodeTools::LineStatistics.new(files).print_loc
end
-rule '.rb' => '.y' do |t|
+rule ".rb" => ".y" do |t|
sh "racc -l -o #{t.name} #{t.source}"
end
-task compile: 'lib/action_dispatch/journey/parser.rb'
+task compile: "lib/action_dispatch/journey/parser.rb"
diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec
index 1bba9df969..ec56de18f1 100644
--- a/actionpack/actionpack.gemspec
+++ b/actionpack/actionpack.gemspec
@@ -1,31 +1,41 @@
-version = File.read(File.expand_path('../../RAILS_VERSION', __FILE__)).strip
+# frozen_string_literal: true
+
+version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip
Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
- s.name = 'actionpack'
+ s.name = "actionpack"
s.version = version
- s.summary = 'Web-flow and rendering framework putting the VC in MVC (part of Rails).'
- s.description = 'Web apps on Rails. Simple, battle-tested conventions for building and testing MVC web applications. Works with any Rack-compatible server.'
+ s.summary = "Web-flow and rendering framework putting the VC in MVC (part of Rails)."
+ s.description = "Web apps on Rails. Simple, battle-tested conventions for building and testing MVC web applications. Works with any Rack-compatible server."
+
+ s.required_ruby_version = ">= 2.4.1"
+
+ s.license = "MIT"
- s.required_ruby_version = '>= 2.2.2'
+ s.author = "David Heinemeier Hansson"
+ s.email = "david@loudthinking.com"
+ s.homepage = "http://rubyonrails.org"
- s.license = 'MIT'
+ s.files = Dir["CHANGELOG.md", "README.rdoc", "MIT-LICENSE", "lib/**/*"]
+ s.require_path = "lib"
+ s.requirements << "none"
- s.author = 'David Heinemeier Hansson'
- s.email = 'david@loudthinking.com'
- s.homepage = 'http://www.rubyonrails.org'
+ s.metadata = {
+ "source_code_uri" => "https://github.com/rails/rails/tree/v#{version}/actionpack",
+ "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/actionpack/CHANGELOG.md"
+ }
- s.files = Dir['CHANGELOG.md', 'README.rdoc', 'MIT-LICENSE', 'lib/**/*']
- s.require_path = 'lib'
- s.requirements << 'none'
+ # NOTE: Please read our dependency guidelines before updating versions:
+ # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves
- s.add_dependency 'activesupport', version
+ s.add_dependency "activesupport", version
- s.add_dependency 'rack', '~> 1.6'
- s.add_dependency 'rack-test', '~> 0.6.3'
- s.add_dependency 'rails-html-sanitizer', '~> 1.0', '>= 1.0.2'
- s.add_dependency 'rails-dom-testing', '~> 1.0', '>= 1.0.5'
- s.add_dependency 'actionview', version
+ s.add_dependency "rack", "~> 2.0"
+ s.add_dependency "rack-test", ">= 0.6.3"
+ s.add_dependency "rails-html-sanitizer", "~> 1.0", ">= 1.0.2"
+ s.add_dependency "rails-dom-testing", "~> 2.0"
+ s.add_dependency "actionview", version
- s.add_development_dependency 'activemodel', version
+ s.add_development_dependency "activemodel", version
end
diff --git a/actionpack/bin/test b/actionpack/bin/test
index 404cabba51..c53377cc97 100755
--- a/actionpack/bin/test
+++ b/actionpack/bin/test
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
-COMPONENT_ROOT = File.expand_path("../../", __FILE__)
-require File.expand_path("../tools/test", COMPONENT_ROOT)
-exit Minitest.run(ARGV)
+# frozen_string_literal: true
+
+COMPONENT_ROOT = File.expand_path("..", __dir__)
+require_relative "../../tools/test"
diff --git a/actionpack/lib/abstract_controller.rb b/actionpack/lib/abstract_controller.rb
index fe9802e395..0477e7f1c9 100644
--- a/actionpack/lib/abstract_controller.rb
+++ b/actionpack/lib/abstract_controller.rb
@@ -1,13 +1,14 @@
-require 'action_pack'
-require 'active_support/rails'
-require 'active_support/core_ext/module/attr_internal'
-require 'active_support/core_ext/module/anonymous'
-require 'active_support/i18n'
+# frozen_string_literal: true
+
+require "action_pack"
+require "active_support/rails"
+require "active_support/i18n"
module AbstractController
extend ActiveSupport::Autoload
autoload :Base
+ autoload :Caching
autoload :Callbacks
autoload :Collector
autoload :DoubleRenderError, "abstract_controller/rendering"
@@ -17,4 +18,9 @@ module AbstractController
autoload :Translation
autoload :AssetPaths
autoload :UrlFor
+
+ def self.eager_load!
+ super
+ AbstractController::Caching.eager_load!
+ end
end
diff --git a/actionpack/lib/abstract_controller/asset_paths.rb b/actionpack/lib/abstract_controller/asset_paths.rb
index e6170228d9..d6ee84b87b 100644
--- a/actionpack/lib/abstract_controller/asset_paths.rb
+++ b/actionpack/lib/abstract_controller/asset_paths.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module AbstractController
module AssetPaths #:nodoc:
extend ActiveSupport::Concern
diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb
index ebd02bd9a1..bb42f2e119 100644
--- a/actionpack/lib/abstract_controller/base.rb
+++ b/actionpack/lib/abstract_controller/base.rb
@@ -1,13 +1,12 @@
-require 'erubis'
-require 'set'
-require 'active_support/configurable'
-require 'active_support/descendants_tracker'
-require 'active_support/core_ext/module/anonymous'
+# frozen_string_literal: true
-module AbstractController
- class Error < StandardError #:nodoc:
- end
+require "abstract_controller/error"
+require "active_support/configurable"
+require "active_support/descendants_tracker"
+require "active_support/core_ext/module/anonymous"
+require "active_support/core_ext/module/attr_internal"
+module AbstractController
# Raised when a non-existing controller action is triggered.
class ActionNotFound < StandardError
end
@@ -17,14 +16,21 @@ module AbstractController
# expected to provide their own +render+ method, since rendering means
# different things depending on the context.
class Base
+ ##
+ # Returns the body of the HTTP response sent by the controller.
attr_internal :response_body
+
+ ##
+ # Returns the name of the action this controller is processing.
attr_internal :action_name
+
+ ##
+ # Returns the formats that can be processed by the controller.
attr_internal :formats
include ActiveSupport::Configurable
extend ActiveSupport::DescendantsTracker
- undef_method :not_implemented
class << self
attr_reader :abstract
alias_method :abstract?, :abstract
@@ -49,7 +55,7 @@ module AbstractController
# instance methods on that abstract class. Public instance methods of
# a controller would normally be considered action methods, so methods
# declared on abstract classes are being removed.
- # (ActionController::Metal and ActionController::Base are defined as abstract)
+ # (<tt>ActionController::Metal</tt> and ActionController::Base are defined as abstract)
def internal_methods
controller = self
@@ -72,15 +78,17 @@ module AbstractController
# Except for public instance methods of Base and its ancestors
internal_methods +
# Be sure to include shadowed public instance methods of this class
- public_instance_methods(false)).uniq.map(&:to_s)
+ public_instance_methods(false))
+
+ methods.map!(&:to_s)
methods.to_set
end
end
- # action_methods are cached and there is sometimes need to refresh
+ # action_methods are cached and there is sometimes a need to refresh
# them. ::clear_action_methods! allows you to do that, so next time
- # you run action_methods, they will be recalculated
+ # you run action_methods, they will be recalculated.
def clear_action_methods!
@action_methods = nil
end
@@ -96,7 +104,7 @@ module AbstractController
# ==== Returns
# * <tt>String</tt>
def controller_path
- @controller_path ||= name.sub(/Controller$/, '').underscore unless anonymous?
+ @controller_path ||= name.sub(/Controller$/, "").underscore unless anonymous?
end
# Refresh the cached action_methods when a new action_method is added.
@@ -149,7 +157,14 @@ module AbstractController
# ==== Parameters
# * <tt>action_name</tt> - The name of an action to be tested
def available_action?(action_name)
- _find_action_name(action_name).present?
+ _find_action_name(action_name)
+ end
+
+ # Tests if a response body is set. Used to determine if the
+ # +process_action+ callback needs to be terminated in
+ # +AbstractController::Callbacks+.
+ def performed?
+ response_body
end
# Returns true if the given controller is capable of rendering
@@ -167,8 +182,6 @@ module AbstractController
#
# ==== Parameters
# * <tt>name</tt> - The name of an action to be tested
- #
- # :api: private
def action_method?(name)
self.class.action_methods.include?(name)
end
@@ -210,7 +223,7 @@ module AbstractController
# ==== Returns
# * <tt>string</tt> - The name of the method that handles the action
# * false - No valid method name could be found.
- # Raise AbstractController::ActionNotFound.
+ # Raise +AbstractController::ActionNotFound+.
def _find_action_name(action_name)
_valid_action_name?(action_name) && method_for_action(action_name)
end
@@ -226,11 +239,11 @@ module AbstractController
# with a template matching the action name is considered to exist.
#
# If you override this method to handle additional cases, you may
- # also provide a method (like _handle_method_missing) to handle
+ # also provide a method (like +_handle_method_missing+) to handle
# the case.
#
- # If none of these conditions are true, and method_for_action
- # returns nil, an AbstractController::ActionNotFound exception will be raised.
+ # If none of these conditions are true, and +method_for_action+
+ # returns +nil+, an +AbstractController::ActionNotFound+ exception will be raised.
#
# ==== Parameters
# * <tt>action_name</tt> - An action name to find a method name for
diff --git a/actionpack/lib/abstract_controller/caching.rb b/actionpack/lib/abstract_controller/caching.rb
new file mode 100644
index 0000000000..ce6b757c3c
--- /dev/null
+++ b/actionpack/lib/abstract_controller/caching.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+module AbstractController
+ module Caching
+ extend ActiveSupport::Concern
+ extend ActiveSupport::Autoload
+
+ eager_autoload do
+ autoload :Fragments
+ end
+
+ module ConfigMethods
+ def cache_store
+ config.cache_store
+ end
+
+ def cache_store=(store)
+ config.cache_store = ActiveSupport::Cache.lookup_store(store)
+ end
+
+ private
+ def cache_configured?
+ perform_caching && cache_store
+ end
+ end
+
+ include ConfigMethods
+ include AbstractController::Caching::Fragments
+
+ included do
+ extend ConfigMethods
+
+ config_accessor :default_static_extension
+ self.default_static_extension ||= ".html"
+
+ config_accessor :perform_caching
+ self.perform_caching = true if perform_caching.nil?
+
+ config_accessor :enable_fragment_cache_logging
+ self.enable_fragment_cache_logging = false
+
+ class_attribute :_view_cache_dependencies, default: []
+ helper_method :view_cache_dependencies if respond_to?(:helper_method)
+ end
+
+ module ClassMethods
+ def view_cache_dependency(&dependency)
+ self._view_cache_dependencies += [dependency]
+ end
+ end
+
+ def view_cache_dependencies
+ self.class._view_cache_dependencies.map { |dep| instance_exec(&dep) }.compact
+ end
+
+ private
+ # Convenience accessor.
+ def cache(key, options = {}, &block) # :doc:
+ if cache_configured?
+ cache_store.fetch(ActiveSupport::Cache.expand_cache_key(key, :controller), options, &block)
+ else
+ yield
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/abstract_controller/caching/fragments.rb b/actionpack/lib/abstract_controller/caching/fragments.rb
new file mode 100644
index 0000000000..95078a2a28
--- /dev/null
+++ b/actionpack/lib/abstract_controller/caching/fragments.rb
@@ -0,0 +1,170 @@
+# frozen_string_literal: true
+
+module AbstractController
+ module Caching
+ # Fragment caching is used for caching various blocks within
+ # views without caching the entire action as a whole. This is
+ # useful when certain elements of an action change frequently or
+ # depend on complicated state while other parts rarely change or
+ # can be shared amongst multiple parties. The caching is done using
+ # the +cache+ helper available in the Action View. See
+ # ActionView::Helpers::CacheHelper for more information.
+ #
+ # While it's strongly recommended that you use key-based cache
+ # expiration (see links in CacheHelper for more information),
+ # it is also possible to manually expire caches. For example:
+ #
+ # expire_fragment('name_of_cache')
+ module Fragments
+ extend ActiveSupport::Concern
+
+ included do
+ if respond_to?(:class_attribute)
+ class_attribute :fragment_cache_keys
+ else
+ mattr_writer :fragment_cache_keys
+ end
+
+ self.fragment_cache_keys = []
+
+ if respond_to?(:helper_method)
+ helper_method :fragment_cache_key
+ helper_method :combined_fragment_cache_key
+ end
+ end
+
+ module ClassMethods
+ # Allows you to specify controller-wide key prefixes for
+ # cache fragments. Pass either a constant +value+, or a block
+ # which computes a value each time a cache key is generated.
+ #
+ # For example, you may want to prefix all fragment cache keys
+ # with a global version identifier, so you can easily
+ # invalidate all caches.
+ #
+ # class ApplicationController
+ # fragment_cache_key "v1"
+ # end
+ #
+ # When it's time to invalidate all fragments, simply change
+ # the string constant. Or, progressively roll out the cache
+ # invalidation using a computed value:
+ #
+ # class ApplicationController
+ # fragment_cache_key do
+ # @account.id.odd? ? "v1" : "v2"
+ # end
+ # end
+ def fragment_cache_key(value = nil, &key)
+ self.fragment_cache_keys += [key || -> { value }]
+ end
+ end
+
+ # Given a key (as described in +expire_fragment+), returns
+ # a key suitable for use in reading, writing, or expiring a
+ # cached fragment. All keys begin with <tt>views/</tt>,
+ # followed by any controller-wide key prefix values, ending
+ # with the specified +key+ value. The key is expanded using
+ # ActiveSupport::Cache.expand_cache_key.
+ def fragment_cache_key(key)
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Calling fragment_cache_key directly is deprecated and will be removed in Rails 6.0.
+ All fragment accessors now use the combined_fragment_cache_key method that retains the key as an array,
+ such that the caching stores can interrogate the parts for cache versions used in
+ recyclable cache keys.
+ MSG
+
+ head = self.class.fragment_cache_keys.map { |k| instance_exec(&k) }
+ tail = key.is_a?(Hash) ? url_for(key).split("://").last : key
+ ActiveSupport::Cache.expand_cache_key([*head, *tail], :views)
+ end
+
+ # Given a key (as described in +expire_fragment+), returns
+ # a key array suitable for use in reading, writing, or expiring a
+ # cached fragment. All keys begin with <tt>:views</tt>,
+ # followed by <tt>ENV["RAILS_CACHE_ID"]</tt> or <tt>ENV["RAILS_APP_VERSION"]</tt> if set,
+ # followed by any controller-wide key prefix values, ending
+ # with the specified +key+ value.
+ def combined_fragment_cache_key(key)
+ head = self.class.fragment_cache_keys.map { |k| instance_exec(&k) }
+ tail = key.is_a?(Hash) ? url_for(key).split("://").last : key
+
+ cache_key = [:views, ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"], head, tail]
+ cache_key.flatten!(1)
+ cache_key.compact!
+ cache_key
+ end
+
+ # Writes +content+ to the location signified by
+ # +key+ (see +expire_fragment+ for acceptable formats).
+ def write_fragment(key, content, options = nil)
+ return content unless cache_configured?
+
+ key = combined_fragment_cache_key(key)
+ instrument_fragment_cache :write_fragment, key do
+ content = content.to_str
+ cache_store.write(key, content, options)
+ end
+ content
+ end
+
+ # Reads a cached fragment from the location signified by +key+
+ # (see +expire_fragment+ for acceptable formats).
+ def read_fragment(key, options = nil)
+ return unless cache_configured?
+
+ key = combined_fragment_cache_key(key)
+ instrument_fragment_cache :read_fragment, key do
+ result = cache_store.read(key, options)
+ result.respond_to?(:html_safe) ? result.html_safe : result
+ end
+ end
+
+ # Check if a cached fragment from the location signified by
+ # +key+ exists (see +expire_fragment+ for acceptable formats).
+ def fragment_exist?(key, options = nil)
+ return unless cache_configured?
+ key = combined_fragment_cache_key(key)
+
+ instrument_fragment_cache :exist_fragment?, key do
+ cache_store.exist?(key, options)
+ end
+ end
+
+ # Removes fragments from the cache.
+ #
+ # +key+ can take one of three forms:
+ #
+ # * String - This would normally take the form of a path, like
+ # <tt>pages/45/notes</tt>.
+ # * Hash - Treated as an implicit call to +url_for+, like
+ # <tt>{ controller: 'pages', action: 'notes', id: 45}</tt>
+ # * Regexp - Will remove any fragment that matches, so
+ # <tt>%r{pages/\d*/notes}</tt> might remove all notes. Make sure you
+ # don't use anchors in the regex (<tt>^</tt> or <tt>$</tt>) because
+ # the actual filename matched looks like
+ # <tt>./cache/filename/path.cache</tt>. Note: Regexp expiration is
+ # only supported on caches that can iterate over all keys (unlike
+ # memcached).
+ #
+ # +options+ is passed through to the cache store's +delete+
+ # method (or <tt>delete_matched</tt>, for Regexp keys).
+ def expire_fragment(key, options = nil)
+ return unless cache_configured?
+ key = combined_fragment_cache_key(key) unless key.is_a?(Regexp)
+
+ instrument_fragment_cache :expire_fragment, key do
+ if key.is_a?(Regexp)
+ cache_store.delete_matched(key, options)
+ else
+ cache_store.delete(key, options)
+ end
+ end
+ end
+
+ def instrument_fragment_cache(name, key) # :nodoc:
+ ActiveSupport::Notifications.instrument("#{name}.#{instrument_name}", instrument_payload(key)) { yield }
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb
index 13795f0dd8..42bab411d2 100644
--- a/actionpack/lib/abstract_controller/callbacks.rb
+++ b/actionpack/lib/abstract_controller/callbacks.rb
@@ -1,4 +1,26 @@
+# frozen_string_literal: true
+
module AbstractController
+ # = Abstract Controller Callbacks
+ #
+ # Abstract Controller provides hooks during the life cycle of a controller action.
+ # Callbacks allow you to trigger logic during this cycle. Available callbacks are:
+ #
+ # * <tt>after_action</tt>
+ # * <tt>append_after_action</tt>
+ # * <tt>append_around_action</tt>
+ # * <tt>append_before_action</tt>
+ # * <tt>around_action</tt>
+ # * <tt>before_action</tt>
+ # * <tt>prepend_after_action</tt>
+ # * <tt>prepend_around_action</tt>
+ # * <tt>prepend_before_action</tt>
+ # * <tt>skip_after_action</tt>
+ # * <tt>skip_around_action</tt>
+ # * <tt>skip_before_action</tt>
+ #
+ # NOTE: Calling the same callback multiple times will overwrite previous callback definitions.
+ #
module Callbacks
extend ActiveSupport::Concern
@@ -9,12 +31,12 @@ module AbstractController
included do
define_callbacks :process_action,
- terminator: ->(controller, result_lambda) { result_lambda.call if result_lambda.is_a?(Proc); controller.response_body },
+ terminator: ->(controller, result_lambda) { result_lambda.call; controller.performed? },
skip_after_callbacks_if_terminated: true
end
- # Override AbstractController::Base's process_action to run the
- # process_action callbacks around the normal behavior.
+ # Override <tt>AbstractController::Base#process_action</tt> to run the
+ # <tt>process_action</tt> callbacks around the normal behavior.
def process_action(*args)
run_callbacks(:process_action) do
super
@@ -39,8 +61,8 @@ module AbstractController
# except: :index, if: -> { true } # the :except option will be ignored.
#
# ==== Options
- # * <tt>only</tt> - The callback should be run only for this action
- # * <tt>except</tt> - The callback should be run for all actions except this action
+ # * <tt>only</tt> - The callback should be run only for this action.
+ # * <tt>except</tt> - The callback should be run for all actions except this action.
def _normalize_callback_options(options)
_normalize_callback_option(options, :only, :if)
_normalize_callback_option(options, :except, :unless)
@@ -48,30 +70,12 @@ module AbstractController
def _normalize_callback_option(options, from, to) # :nodoc:
if from = options[from]
- from = Array(from).map {|o| "action_name == '#{o}'"}.join(" || ")
+ _from = Array(from).map(&:to_s).to_set
+ from = proc { |c| _from.include? c.action_name }
options[to] = Array(options[to]).unshift(from)
end
end
- # Skip before, after, and around action callbacks matching any of the names.
- #
- # ==== Parameters
- # * <tt>names</tt> - A list of valid names that could be used for
- # callbacks. Note that skipping uses Ruby equality, so it's
- # impossible to skip a callback defined using an anonymous proc
- # using #skip_action_callback
- def skip_action_callback(*names)
- ActiveSupport::Deprecation.warn('`skip_action_callback` is deprecated and will be removed in the next major version of Rails. Please use skip_before_action, skip_after_action or skip_around_action instead.')
- skip_before_action(*names, raise: false)
- skip_after_action(*names, raise: false)
- skip_around_action(*names, raise: false)
- end
-
- def skip_filter(*names)
- ActiveSupport::Deprecation.warn("`skip_filter` is deprecated and will be removed in Rails 5.1. Use skip_before_action, skip_after_action or skip_around_action instead.")
- skip_action_callback(*names)
- end
-
# Take callback names and an optional callback proc, normalize them,
# then call the block with each callback. This allows us to abstract
# the normalization across several methods that use it.
@@ -82,8 +86,8 @@ module AbstractController
# * <tt>block</tt> - A proc that should be added to the callbacks.
#
# ==== Block Parameters
- # * <tt>name</tt> - The callback to be added
- # * <tt>options</tt> - A hash of options to be used when adding the callback
+ # * <tt>name</tt> - The callback to be added.
+ # * <tt>options</tt> - A hash of options to be used when adding the callback.
def _insert_callbacks(callbacks, block = nil)
options = callbacks.extract_options!
_normalize_callback_options(options)
@@ -99,6 +103,10 @@ module AbstractController
# :call-seq: before_action(names, block)
#
# Append a callback before actions. See _insert_callbacks for parameter details.
+ #
+ # If the callback renders or redirects, the action will not run. If there
+ # are additional callbacks scheduled to run after that callback, they are
+ # also cancelled.
##
# :method: prepend_before_action
@@ -106,6 +114,10 @@ module AbstractController
# :call-seq: prepend_before_action(names, block)
#
# Prepend a callback before actions. See _insert_callbacks for parameter details.
+ #
+ # If the callback renders or redirects, the action will not run. If there
+ # are additional callbacks scheduled to run after that callback, they are
+ # also cancelled.
##
# :method: skip_before_action
@@ -120,6 +132,10 @@ module AbstractController
# :call-seq: append_before_action(names, block)
#
# Append a callback before actions. See _insert_callbacks for parameter details.
+ #
+ # If the callback renders or redirects, the action will not run. If there
+ # are additional callbacks scheduled to run after that callback, they are
+ # also cancelled.
##
# :method: after_action
@@ -186,22 +202,12 @@ module AbstractController
end
end
- define_method "#{callback}_filter" do |*names, &blk|
- ActiveSupport::Deprecation.warn("#{callback}_filter is deprecated and will be removed in Rails 5.1. Use #{callback}_action instead.")
- send("#{callback}_action", *names, &blk)
- end
-
define_method "prepend_#{callback}_action" do |*names, &blk|
_insert_callbacks(names, blk) do |name, options|
- set_callback(:process_action, callback, name, options.merge(:prepend => true))
+ set_callback(:process_action, callback, name, options.merge(prepend: true))
end
end
- define_method "prepend_#{callback}_filter" do |*names, &blk|
- ActiveSupport::Deprecation.warn("prepend_#{callback}_filter is deprecated and will be removed in Rails 5.1. Use prepend_#{callback}_action instead.")
- send("prepend_#{callback}_action", *names, &blk)
- end
-
# Skip a before, after or around callback. See _insert_callbacks
# for details on the allowed parameters.
define_method "skip_#{callback}_action" do |*names|
@@ -210,18 +216,8 @@ module AbstractController
end
end
- define_method "skip_#{callback}_filter" do |*names, &blk|
- ActiveSupport::Deprecation.warn("skip_#{callback}_filter is deprecated and will be removed in Rails 5.1. Use skip_#{callback}_action instead.")
- send("skip_#{callback}_action", *names, &blk)
- end
-
# *_action is the same as append_*_action
alias_method :"append_#{callback}_action", :"#{callback}_action"
-
- define_method "append_#{callback}_filter" do |*names, &blk|
- ActiveSupport::Deprecation.warn("append_#{callback}_filter is deprecated and will be removed in Rails 5.1. Use append_#{callback}_action instead.")
- send("append_#{callback}_action", *names, &blk)
- end
end
end
end
diff --git a/actionpack/lib/abstract_controller/collector.rb b/actionpack/lib/abstract_controller/collector.rb
index ddd56b354a..d4a078ab32 100644
--- a/actionpack/lib/abstract_controller/collector.rb
+++ b/actionpack/lib/abstract_controller/collector.rb
@@ -1,14 +1,15 @@
+# frozen_string_literal: true
+
require "action_dispatch/http/mime_type"
module AbstractController
module Collector
def self.generate_method_for_mime(mime)
sym = mime.is_a?(Symbol) ? mime : mime.to_sym
- const = sym.upcase
class_eval <<-RUBY, __FILE__, __LINE__ + 1
- def #{sym}(*args, &block) # def html(*args, &block)
- custom(Mime::#{const}, *args, &block) # custom(Mime::HTML, *args, &block)
- end # end
+ def #{sym}(*args, &block)
+ custom(Mime[:#{sym}], *args, &block)
+ end
RUBY
end
@@ -17,24 +18,20 @@ module AbstractController
end
Mime::Type.register_callback do |mime|
- generate_method_for_mime(mime) unless self.instance_methods.include?(mime.to_sym)
+ generate_method_for_mime(mime) unless instance_methods.include?(mime.to_sym)
end
- protected
+ private
def method_missing(symbol, &block)
- const_name = symbol.upcase
-
- unless Mime.const_defined?(const_name)
+ unless mime_constant = Mime[symbol]
raise NoMethodError, "To respond to a custom format, register it as a MIME type first: " \
- "http://guides.rubyonrails.org/action_controller_overview.html#restful-downloads. " \
+ "https://guides.rubyonrails.org/action_controller_overview.html#restful-downloads. " \
"If you meant to respond to a variant like :tablet or :phone, not a custom format, " \
"be sure to nest your variant response within a format response: " \
"format.html { |html| html.tablet { ... } }"
end
- mime_constant = Mime.const_get(const_name)
-
if Mime::SET.include?(mime_constant)
AbstractController::Collector.generate_method_for_mime(mime_constant)
send(symbol, &block)
diff --git a/actionpack/lib/abstract_controller/error.rb b/actionpack/lib/abstract_controller/error.rb
new file mode 100644
index 0000000000..89a54f072e
--- /dev/null
+++ b/actionpack/lib/abstract_controller/error.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+module AbstractController
+ class Error < StandardError #:nodoc:
+ end
+end
diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb
index 109eff10eb..3913259ecc 100644
--- a/actionpack/lib/abstract_controller/helpers.rb
+++ b/actionpack/lib/abstract_controller/helpers.rb
@@ -1,15 +1,14 @@
-require 'active_support/dependencies'
+# frozen_string_literal: true
+
+require "active_support/dependencies"
module AbstractController
module Helpers
extend ActiveSupport::Concern
included do
- class_attribute :_helpers
- self._helpers = Module.new
-
- class_attribute :_helper_methods
- self._helper_methods = Array.new
+ class_attribute :_helpers, default: Module.new
+ class_attribute :_helper_methods, default: Array.new
end
class MissingHelperError < LoadError
@@ -18,7 +17,7 @@ module AbstractController
@path = "helpers/#{path}.rb"
set_backtrace error.backtrace
- if error.path =~ /^#{path}(\.rb)?$/
+ if /^#{path}(\.rb)?$/.match?(error.path)
super("Missing helper file helpers/%s.rb" % path)
else
raise error
@@ -38,7 +37,8 @@ module AbstractController
end
# Declare a controller method as a helper. For example, the following
- # makes the +current_user+ controller method available to the view:
+ # makes the +current_user+ and +logged_in?+ controller methods available
+ # to the view:
# class ApplicationController < ActionController::Base
# helper_method :current_user, :logged_in?
#
@@ -170,25 +170,25 @@ module AbstractController
end
private
- # Makes all the (instance) methods in the helper module available to templates
- # rendered through this controller.
- #
- # ==== Parameters
- # * <tt>module</tt> - The module to include into the current helper module
- # for the class
- def add_template_helper(mod)
- _helpers.module_eval { include mod }
- end
+ # Makes all the (instance) methods in the helper module available to templates
+ # rendered through this controller.
+ #
+ # ==== Parameters
+ # * <tt>module</tt> - The module to include into the current helper module
+ # for the class
+ def add_template_helper(mod)
+ _helpers.module_eval { include mod }
+ end
- def default_helper_module!
- module_name = name.sub(/Controller$/, '')
- module_path = module_name.underscore
- helper module_path
- rescue LoadError => e
- raise e unless e.is_missing? "helpers/#{module_path}_helper"
- rescue NameError => e
- raise e unless e.missing_name? "#{module_name}Helper"
- end
+ def default_helper_module!
+ module_name = name.sub(/Controller$/, "")
+ module_path = module_name.underscore
+ helper module_path
+ rescue LoadError => e
+ raise e unless e.is_missing? "helpers/#{module_path}_helper"
+ rescue NameError => e
+ raise e unless e.missing_name? "#{module_name}Helper"
+ end
end
end
end
diff --git a/actionpack/lib/abstract_controller/logger.rb b/actionpack/lib/abstract_controller/logger.rb
index c31ea6c5b5..8d0acc1b5c 100644
--- a/actionpack/lib/abstract_controller/logger.rb
+++ b/actionpack/lib/abstract_controller/logger.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "active_support/benchmarkable"
module AbstractController
diff --git a/actionpack/lib/abstract_controller/railties/routes_helpers.rb b/actionpack/lib/abstract_controller/railties/routes_helpers.rb
index 14b574e322..fbd93705ed 100644
--- a/actionpack/lib/abstract_controller/railties/routes_helpers.rb
+++ b/actionpack/lib/abstract_controller/railties/routes_helpers.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module AbstractController
module Railties
module RoutesHelpers
@@ -5,7 +7,7 @@ module AbstractController
Module.new do
define_method(:inherited) do |klass|
super(klass)
- if namespace = klass.parents.detect { |m| m.respond_to?(:railtie_routes_url_helpers) }
+ if namespace = klass.module_parents.detect { |m| m.respond_to?(:railtie_routes_url_helpers) }
klass.include(namespace.railtie_routes_url_helpers(include_path_helpers))
else
klass.include(routes.url_helpers(include_path_helpers))
diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb
index 5514213ad8..8ba2b25552 100644
--- a/actionpack/lib/abstract_controller/rendering.rb
+++ b/actionpack/lib/abstract_controller/rendering.rb
@@ -1,8 +1,9 @@
-require 'active_support/concern'
-require 'active_support/core_ext/class/attribute'
-require 'action_view'
-require 'action_view/view_paths'
-require 'set'
+# frozen_string_literal: true
+
+require "abstract_controller/error"
+require "action_view"
+require "action_view/view_paths"
+require "set"
module AbstractController
class DoubleRenderError < Error
@@ -19,12 +20,15 @@ module AbstractController
# Normalizes arguments, options and then delegates render_to_body and
# sticks the result in <tt>self.response_body</tt>.
- # :api: public
def render(*args, &block)
options = _normalize_render(*args, &block)
- self.response_body = render_to_body(options)
- _process_format(rendered_format, options) if rendered_format
- self.response_body
+ rendered_body = render_to_body(options)
+ if options[:html]
+ _set_html_content_type
+ else
+ _set_rendered_content_type rendered_format
+ end
+ self.response_body = rendered_body
end
# Raw rendering of a template to a string.
@@ -37,32 +41,26 @@ module AbstractController
# (as ActionController extends it to be anything that
# responds to the method each), this method needs to be
# overridden in order to still return a string.
- # :api: plugin
def render_to_string(*args, &block)
options = _normalize_render(*args, &block)
render_to_body(options)
end
# Performs the actual template rendering.
- # :api: public
def render_to_body(options = {})
end
- # Returns Content-Type of rendered content
- # :api: public
+ # Returns Content-Type of rendered content.
def rendered_format
- Mime::TEXT
+ Mime[:text]
end
- DEFAULT_PROTECTED_INSTANCE_VARIABLES = Set.new %w(
- @_action_name @_response_body @_formats @_prefixes @_config
- @_view_context_class @_view_renderer @_lookup_context
- @_routes @_db_runtime
- ).map(&:to_sym)
+ DEFAULT_PROTECTED_INSTANCE_VARIABLES = Set.new %i(
+ @_action_name @_response_body @_formats @_prefixes
+ )
# This method should return a hash with assigns.
# You can overwrite this configuration per controller.
- # :api: public
def view_assigns
protected_vars = _protected_ivars
variables = instance_variables
@@ -73,12 +71,18 @@ module AbstractController
}
end
+ private
# Normalize args by converting <tt>render "foo"</tt> to
# <tt>render :action => "foo"</tt> and <tt>render "foo/bar"</tt> to
# <tt>render :file => "foo/bar"</tt>.
- # :api: plugin
- def _normalize_args(action=nil, options={})
- if action.is_a? Hash
+ def _normalize_args(action = nil, options = {}) # :doc:
+ if action.respond_to?(:permitted?)
+ if action.permitted?
+ action
+ else
+ raise ArgumentError, "render parameters are not permitted"
+ end
+ elsif action.is_a?(Hash)
action
else
options
@@ -86,30 +90,32 @@ module AbstractController
end
# Normalize options.
- # :api: plugin
- def _normalize_options(options)
+ def _normalize_options(options) # :doc:
options
end
# Process extra options.
- # :api: plugin
- def _process_options(options)
+ def _process_options(options) # :doc:
options
end
# Process the rendered format.
- # :api: private
- def _process_format(format, options = {})
+ def _process_format(format) # :nodoc:
+ end
+
+ def _process_variant(options)
+ end
+
+ def _set_html_content_type # :nodoc:
+ end
+
+ def _set_rendered_content_type(format) # :nodoc:
end
# Normalize args and options.
- # :api: private
- def _normalize_render(*args, &block)
+ def _normalize_render(*args, &block) # :nodoc:
options = _normalize_args(*args, &block)
- #TODO: remove defined? when we restore AP <=> AV dependency
- if defined?(request) && request && request.variant.present?
- options[:variant] = request.variant
- end
+ _process_variant(options)
_normalize_options(options)
options
end
diff --git a/actionpack/lib/abstract_controller/translation.rb b/actionpack/lib/abstract_controller/translation.rb
index 56b8ce895e..666e154e4c 100644
--- a/actionpack/lib/abstract_controller/translation.rb
+++ b/actionpack/lib/abstract_controller/translation.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module AbstractController
module Translation
# Delegates to <tt>I18n.translate</tt>. Also aliased as <tt>t</tt>.
@@ -9,11 +11,11 @@ module AbstractController
# to translate many keys within the same controller / action and gives you a
# simple framework for scoping them consistently.
def translate(key, options = {})
- if key.to_s.first == '.'
- path = controller_path.tr('/', '.')
+ if key.to_s.first == "."
+ path = controller_path.tr("/", ".")
defaults = [:"#{path}#{key}"]
defaults << options[:default] if options[:default]
- options[:default] = defaults
+ options[:default] = defaults.flatten
key = "#{path}.#{action_name}#{key}"
end
I18n.translate(key, options)
diff --git a/actionpack/lib/abstract_controller/url_for.rb b/actionpack/lib/abstract_controller/url_for.rb
index 72d07b0927..bd74c27d3b 100644
--- a/actionpack/lib/abstract_controller/url_for.rb
+++ b/actionpack/lib/abstract_controller/url_for.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module AbstractController
# Includes +url_for+ into the host class (e.g. an abstract controller or mailer). The class
# has to provide a +RouteSet+ by implementing the <tt>_routes</tt> methods. Otherwise, an
diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb
index 89fc4520d3..29d61c3ceb 100644
--- a/actionpack/lib/action_controller.rb
+++ b/actionpack/lib/action_controller.rb
@@ -1,25 +1,33 @@
-require 'active_support/rails'
-require 'abstract_controller'
-require 'action_dispatch'
-require 'action_controller/metal/live'
-require 'action_controller/metal/strong_parameters'
+# frozen_string_literal: true
+
+require "active_support/rails"
+require "abstract_controller"
+require "action_dispatch"
+require "action_controller/metal/live"
+require "action_controller/metal/strong_parameters"
module ActionController
extend ActiveSupport::Autoload
autoload :API
autoload :Base
- autoload :Caching
autoload :Metal
autoload :Middleware
autoload :Renderer
autoload :FormBuilder
+ eager_autoload do
+ autoload :Caching
+ end
+
autoload_under "metal" do
autoload :ConditionalGet
+ autoload :ContentSecurityPolicy
autoload :Cookies
autoload :DataStreaming
+ autoload :DefaultHeaders
autoload :EtagWithTemplateDigest
+ autoload :EtagWithFlash
autoload :Flash
autoload :ForceSSL
autoload :Head
@@ -30,7 +38,6 @@ module ActionController
autoload :Instrumentation
autoload :MimeResponds
autoload :ParamsWrapper
- autoload :RackDelegation
autoload :Redirecting
autoload :Renderers
autoload :Rendering
@@ -38,23 +45,23 @@ module ActionController
autoload :Rescue
autoload :Streaming
autoload :StrongParameters
+ autoload :ParameterEncoding
autoload :Testing
autoload :UrlFor
end
- autoload :TestCase, 'action_controller/test_case'
- autoload :TemplateAssertions, 'action_controller/test_case'
-
- def self.eager_load!
- super
- ActionController::Caching.eager_load!
+ autoload_under "api" do
+ autoload :ApiRendering
end
+
+ autoload :TestCase, "action_controller/test_case"
+ autoload :TemplateAssertions, "action_controller/test_case"
end
# Common Active Support usage in Action Controller
-require 'active_support/core_ext/module/attribute_accessors'
-require 'active_support/core_ext/load_error'
-require 'active_support/core_ext/module/attr_internal'
-require 'active_support/core_ext/name_error'
-require 'active_support/core_ext/uri'
-require 'active_support/inflector'
+require "active_support/core_ext/module/attribute_accessors"
+require "active_support/core_ext/load_error"
+require "active_support/core_ext/module/attr_internal"
+require "active_support/core_ext/name_error"
+require "active_support/core_ext/uri"
+require "active_support/inflector"
diff --git a/actionpack/lib/action_controller/api.rb b/actionpack/lib/action_controller/api.rb
index 3af63b8892..c276ee57c0 100644
--- a/actionpack/lib/action_controller/api.rb
+++ b/actionpack/lib/action_controller/api.rb
@@ -1,6 +1,8 @@
-require 'action_view'
-require 'action_controller'
-require 'action_controller/log_subscriber'
+# frozen_string_literal: true
+
+require "action_view"
+require "action_controller"
+require "action_controller/log_subscriber"
module ActionController
# API Controller is a lightweight version of <tt>ActionController::Base</tt>,
@@ -10,26 +12,26 @@ module ActionController
#
# An API Controller is different from a normal controller in the sense that
# by default it doesn't include a number of features that are usually required
- # by browser access only: layouts and templates rendering, cookies, sessions,
+ # by browser access only: layouts and templates rendering,
# flash, assets, and so on. This makes the entire controller stack thinner,
# suitable for API applications. It doesn't mean you won't have such
# features if you need them: they're all available for you to include in
- # your application, they're just not part of the default API Controller stack.
+ # your application, they're just not part of the default API controller stack.
#
- # By default, only the ApplicationController in a \Rails application inherits
- # from <tt>ActionController::API</tt>. All other controllers in turn inherit
- # from ApplicationController.
+ # Normally, +ApplicationController+ is the only controller that inherits from
+ # <tt>ActionController::API</tt>. All other controllers in turn inherit from
+ # +ApplicationController+.
#
# A sample controller could look like this:
#
# class PostsController < ApplicationController
# def index
- # @posts = Post.all
- # render json: @posts
+ # posts = Post.all
+ # render json: posts
# end
# end
#
- # Request, response and parameters objects all work the exact same way as
+ # Request, response, and parameters objects all work the exact same way as
# <tt>ActionController::Base</tt>.
#
# == Renders
@@ -37,18 +39,18 @@ module ActionController
# The default API Controller stack includes all renderers, which means you
# can use <tt>render :json</tt> and brothers freely in your controllers. Keep
# in mind that templates are not going to be rendered, so you need to ensure
- # your controller is calling either <tt>render</tt> or <tt>redirect</tt> in
- # all actions, otherwise it will return 204 No Content response.
+ # your controller is calling either <tt>render</tt> or <tt>redirect_to</tt> in
+ # all actions, otherwise it will return 204 No Content.
#
# def show
- # @post = Post.find(params[:id])
- # render json: @post
+ # post = Post.find(params[:id])
+ # render json: post
# end
#
# == Redirects
#
# Redirects are used to move from one action to another. You can use the
- # <tt>redirect</tt> method in your controllers in the same way as
+ # <tt>redirect_to</tt> method in your controllers in the same way as in
# <tt>ActionController::Base</tt>. For example:
#
# def create
@@ -56,7 +58,7 @@ module ActionController
# # do stuff here
# end
#
- # == Adding new behavior
+ # == Adding New Behavior
#
# In some scenarios you may want to add back some functionality provided by
# <tt>ActionController::Base</tt> that is not present by default in
@@ -72,25 +74,25 @@ module ActionController
#
# class PostsController < ApplicationController
# def index
- # @posts = Post.all
+ # posts = Post.all
#
# respond_to do |format|
- # format.json { render json: @posts }
- # format.xml { render xml: @posts }
+ # format.json { render json: posts }
+ # format.xml { render xml: posts }
# end
# end
# end
#
- # Quite straightforward. Make sure to check <tt>ActionController::Base</tt>
- # available modules if you want to include any other functionality that is
- # not provided by <tt>ActionController::API</tt> out of the box.
+ # 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!
# Shortcut helper that returns all the ActionController::API modules except
# the ones passed as arguments:
#
- # class MetalController
+ # class MyAPIBaseController < ActionController::Metal
# ActionController::API.without_modules(:ForceSSL, :UrlFor).each do |left|
# include left
# end
@@ -112,15 +114,15 @@ module ActionController
UrlFor,
Redirecting,
- Rendering,
+ ApiRendering,
Renderers::All,
ConditionalGet,
- RackDelegation,
BasicImplicitRender,
StrongParameters,
ForceSSL,
DataStreaming,
+ DefaultHeaders,
# Before callbacks should also be executed as early as possible, so
# also include them at the bottom.
@@ -142,6 +144,7 @@ module ActionController
include mod
end
+ ActiveSupport.run_load_hooks(:action_controller_api, self)
ActiveSupport.run_load_hooks(:action_controller, self)
end
end
diff --git a/actionpack/lib/action_controller/api/api_rendering.rb b/actionpack/lib/action_controller/api/api_rendering.rb
new file mode 100644
index 0000000000..aca5265313
--- /dev/null
+++ b/actionpack/lib/action_controller/api/api_rendering.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module ActionController
+ module ApiRendering
+ extend ActiveSupport::Concern
+
+ included do
+ include Rendering
+ end
+
+ def render_to_body(options = {})
+ _process_options(options)
+ super
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb
index 2c3b3f4e05..2e565d5d44 100644
--- a/actionpack/lib/action_controller/base.rb
+++ b/actionpack/lib/action_controller/base.rb
@@ -1,4 +1,6 @@
-require 'action_view'
+# frozen_string_literal: true
+
+require "action_view"
require "action_controller/log_subscriber"
require "action_controller/metal/params_wrapper"
@@ -8,7 +10,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,9 +32,9 @@ 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.
+ # 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.
#
# == Requests
@@ -51,16 +53,16 @@ module ActionController
# == Parameters
#
# All request parameters, whether they come from a query string in the URL or form data submitted through a POST request are
- # available through the params method which returns a hash. For example, an action that was performed through
- # <tt>/posts?category=All&limit=5</tt> will include <tt>{ "category" => "All", "limit" => "5" }</tt> in params.
+ # available through the <tt>params</tt> method which returns a hash. For example, an action that was performed through
+ # <tt>/posts?category=All&limit=5</tt> will include <tt>{ "category" => "All", "limit" => "5" }</tt> in <tt>params</tt>.
#
# It's also possible to construct multi-dimensional parameter hashes by specifying keys using brackets, such as:
#
# <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>.
- # If the address input had been named <tt>post[address][street]</tt>, the params would have included
+ # 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.
#
# == Sessions
@@ -74,9 +76,9 @@ 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]}
+ # "Hello #{session[:person]}"
#
# For removing objects from the session, you can either assign a single key to +nil+:
#
@@ -183,7 +185,7 @@ module ActionController
# Shortcut helper that returns all the modules included in
# ActionController::Base except the ones passed as arguments:
#
- # class MetalController
+ # class MyBaseController < ActionController::Metal
# ActionController::Base.without_modules(:ParamsWrapper, :Streaming).each do |left|
# include left
# end
@@ -213,24 +215,26 @@ module ActionController
Renderers::All,
ConditionalGet,
EtagWithTemplateDigest,
- RackDelegation,
+ EtagWithFlash,
Caching,
MimeResponds,
ImplicitRender,
StrongParameters,
-
+ ParameterEncoding,
Cookies,
Flash,
FormBuilder,
RequestForgeryProtection,
+ ContentSecurityPolicy,
ForceSSL,
Streaming,
DataStreaming,
HttpAuthentication::Basic::ControllerMethods,
HttpAuthentication::Digest::ControllerMethods,
HttpAuthentication::Token::ControllerMethods,
+ DefaultHeaders,
- # Before callbacks should also be executed the earliest as possible, so
+ # Before callbacks should also be executed as early as possible, so
# also include them at the bottom.
AbstractController::Callbacks,
@@ -249,20 +253,19 @@ module ActionController
MODULES.each do |mod|
include mod
end
+ setup_renderer!
# Define some internal variables that should not be propagated to the view.
- PROTECTED_IVARS = AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + [
- :@_status, :@_headers, :@_params, :@_env, :@_response, :@_request,
- :@_view_runtime, :@_stream, :@_url_options, :@_action_has_layout ]
+ PROTECTED_IVARS = AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + %i(
+ @_params @_response @_request @_config @_url_options @_action_has_layout @_view_context_class
+ @_view_renderer @_lookup_context @_routes @_view_runtime @_db_runtime @_helper_proxy
+ )
def _protected_ivars # :nodoc:
PROTECTED_IVARS
end
- def self.protected_instance_variables
- PROTECTED_IVARS
- end
-
+ ActiveSupport.run_load_hooks(:action_controller_base, self)
ActiveSupport.run_load_hooks(:action_controller, self)
end
end
diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb
index de85e0c1a7..bf3b00a7b7 100644
--- a/actionpack/lib/action_controller/caching.rb
+++ b/actionpack/lib/action_controller/caching.rb
@@ -1,6 +1,4 @@
-require 'fileutils'
-require 'uri'
-require 'set'
+# frozen_string_literal: true
module ActionController
# \Caching is a cheap way of speeding up slow applications by keeping the result of
@@ -8,7 +6,7 @@ module ActionController
#
# You can read more about each approach by clicking the modules below.
#
- # Note: To turn off all caching, set
+ # Note: To turn off all caching provided by Action Controller, set
# config.action_controller.perform_caching = false
#
# == \Caching stores
@@ -24,66 +22,25 @@ module ActionController
# config.action_controller.cache_store = :mem_cache_store, Memcached::Rails.new('localhost:11211')
# config.action_controller.cache_store = MyOwnStore.new('parameter')
module Caching
- extend ActiveSupport::Concern
extend ActiveSupport::Autoload
-
- eager_autoload do
- autoload :Fragments
- end
-
- module ConfigMethods
- def cache_store
- config.cache_store
- end
-
- def cache_store=(store)
- config.cache_store = ActiveSupport::Cache.lookup_store(store)
- end
-
- private
- def cache_configured?
- perform_caching && cache_store
- end
- end
-
- include RackDelegation
- include AbstractController::Callbacks
-
- include ConfigMethods
- include Fragments
+ extend ActiveSupport::Concern
included do
- extend ConfigMethods
-
- config_accessor :default_static_extension
- self.default_static_extension ||= '.html'
-
- config_accessor :perform_caching
- self.perform_caching = true if perform_caching.nil?
-
- class_attribute :_view_cache_dependencies
- self._view_cache_dependencies = []
- helper_method :view_cache_dependencies if respond_to?(:helper_method)
+ include AbstractController::Caching
end
- module ClassMethods
- def view_cache_dependency(&dependency)
- self._view_cache_dependencies += [dependency]
- end
- end
+ private
- def view_cache_dependencies
- self.class._view_cache_dependencies.map { |dep| instance_exec(&dep) }.compact
- end
+ def instrument_payload(key)
+ {
+ controller: controller_name,
+ action: action_name,
+ key: key
+ }
+ end
- protected
- # Convenience accessor.
- def cache(key, options = {}, &block)
- if cache_configured?
- cache_store.fetch(ActiveSupport::Cache.expand_cache_key(key, :controller), options, &block)
- else
- yield
- end
+ def instrument_name
+ "action_controller"
end
end
end
diff --git a/actionpack/lib/action_controller/caching/fragments.rb b/actionpack/lib/action_controller/caching/fragments.rb
deleted file mode 100644
index 2694d4c12f..0000000000
--- a/actionpack/lib/action_controller/caching/fragments.rb
+++ /dev/null
@@ -1,103 +0,0 @@
-module ActionController
- module Caching
- # Fragment caching is used for caching various blocks within
- # views without caching the entire action as a whole. This is
- # useful when certain elements of an action change frequently or
- # depend on complicated state while other parts rarely change or
- # can be shared amongst multiple parties. The caching is done using
- # the +cache+ helper available in the Action View. See
- # ActionView::Helpers::CacheHelper for more information.
- #
- # While it's strongly recommended that you use key-based cache
- # expiration (see links in CacheHelper for more information),
- # it is also possible to manually expire caches. For example:
- #
- # expire_fragment('name_of_cache')
- module Fragments
- # Given a key (as described in +expire_fragment+), returns
- # a key suitable for use in reading, writing, or expiring a
- # cached fragment. All keys are prefixed with <tt>views/</tt> and uses
- # ActiveSupport::Cache.expand_cache_key for the expansion.
- def fragment_cache_key(key)
- ActiveSupport::Cache.expand_cache_key(key.is_a?(Hash) ? url_for(key).split("://").last : key, :views)
- end
-
- # Writes +content+ to the location signified by
- # +key+ (see +expire_fragment+ for acceptable formats).
- def write_fragment(key, content, options = nil)
- return content unless cache_configured?
-
- key = fragment_cache_key(key)
- instrument_fragment_cache :write_fragment, key do
- content = content.to_str
- cache_store.write(key, content, options)
- end
- content
- end
-
- # Reads a cached fragment from the location signified by +key+
- # (see +expire_fragment+ for acceptable formats).
- def read_fragment(key, options = nil)
- return unless cache_configured?
-
- key = fragment_cache_key(key)
- instrument_fragment_cache :read_fragment, key do
- result = cache_store.read(key, options)
- result.respond_to?(:html_safe) ? result.html_safe : result
- end
- end
-
- # Check if a cached fragment from the location signified by
- # +key+ exists (see +expire_fragment+ for acceptable formats).
- def fragment_exist?(key, options = nil)
- return unless cache_configured?
- key = fragment_cache_key(key)
-
- instrument_fragment_cache :exist_fragment?, key do
- cache_store.exist?(key, options)
- end
- end
-
- # Removes fragments from the cache.
- #
- # +key+ can take one of three forms:
- #
- # * String - This would normally take the form of a path, like
- # <tt>pages/45/notes</tt>.
- # * Hash - Treated as an implicit call to +url_for+, like
- # <tt>{ controller: 'pages', action: 'notes', id: 45}</tt>
- # * Regexp - Will remove any fragment that matches, so
- # <tt>%r{pages/\d*/notes}</tt> might remove all notes. Make sure you
- # don't use anchors in the regex (<tt>^</tt> or <tt>$</tt>) because
- # the actual filename matched looks like
- # <tt>./cache/filename/path.cache</tt>. Note: Regexp expiration is
- # only supported on caches that can iterate over all keys (unlike
- # memcached).
- #
- # +options+ is passed through to the cache store's +delete+
- # method (or <tt>delete_matched</tt>, for Regexp keys).
- def expire_fragment(key, options = nil)
- return unless cache_configured?
- key = fragment_cache_key(key) unless key.is_a?(Regexp)
-
- instrument_fragment_cache :expire_fragment, key do
- if key.is_a?(Regexp)
- cache_store.delete_matched(key, options)
- else
- cache_store.delete(key, options)
- end
- end
- end
-
- def instrument_fragment_cache(name, key) # :nodoc:
- payload = {
- controller: controller_name,
- action: action_name,
- key: key
- }
-
- ActiveSupport::Notifications.instrument("#{name}.action_controller", payload) { yield }
- end
- end
- end
-end
diff --git a/actionpack/lib/action_controller/form_builder.rb b/actionpack/lib/action_controller/form_builder.rb
index f2656ca894..09d2ac1837 100644
--- a/actionpack/lib/action_controller/form_builder.rb
+++ b/actionpack/lib/action_controller/form_builder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionController
# Override the default form builder for all views rendered by this
# controller and any of its descendants. Accepts a subclass of
diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb
index 87609d8aa7..afbd38e7fe 100644
--- a/actionpack/lib/action_controller/log_subscriber.rb
+++ b/actionpack/lib/action_controller/log_subscriber.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionController
class LogSubscriber < ActiveSupport::LogSubscriber
INTERNAL_PARAMS = %w(controller action format _method only_path)
@@ -16,16 +18,21 @@ module ActionController
def process_action(event)
info do
- payload = event.payload
+ payload = event.payload
additions = ActionController::Base.log_process_action(payload)
-
status = payload[:status]
+
if status.nil? && payload[:exception].present?
exception_class_name = payload[:exception].first
status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name)
end
- message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms"
- message << " (#{additions.join(" | ")})" unless additions.blank?
+
+ additions << "Allocations: #{event.allocations}"
+
+ message = +"Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms"
+ message << " (#{additions.join(" | ")})" unless additions.empty?
+ message << "\n\n" if defined?(Rails.env) && Rails.env.development?
+
message
end
end
@@ -49,7 +56,7 @@ module ActionController
def unpermitted_parameters(event)
debug do
unpermitted_keys = event.payload[:keys]
- "Unpermitted parameter#{'s' if unpermitted_keys.size > 1}: #{unpermitted_keys.join(", ")}"
+ "Unpermitted parameter#{'s' if unpermitted_keys.size > 1}: #{unpermitted_keys.map { |e| ":#{e}" }.join(", ")}"
end
end
@@ -57,10 +64,10 @@ module ActionController
expire_fragment expire_page write_page).each do |method|
class_eval <<-METHOD, __FILE__, __LINE__ + 1
def #{method}(event)
- return unless logger.info?
- key_or_path = event.payload[:key] || event.payload[:path]
+ return unless logger.info? && ActionController::Base.enable_fragment_cache_logging
+ key = ActiveSupport::Cache.expand_cache_key(event.payload[:key] || event.payload[:path])
human_name = #{method.to_s.humanize.inspect}
- info("\#{human_name} \#{key_or_path} (\#{event.duration.round(1)}ms)")
+ info("\#{human_name} \#{key} (\#{event.duration.round(1)}ms)")
end
METHOD
end
diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb
index ae111e4951..f875aa5e6b 100644
--- a/actionpack/lib/action_controller/metal.rb
+++ b/actionpack/lib/action_controller/metal.rb
@@ -1,5 +1,9 @@
-require 'active_support/core_ext/array/extract_options'
-require 'action_dispatch/middleware/stack'
+# frozen_string_literal: true
+
+require "active_support/core_ext/array/extract_options"
+require "action_dispatch/middleware/stack"
+require "action_dispatch/http/request"
+require "action_dispatch/http/response"
module ActionController
# Extend ActionDispatch middleware stack to make it aware of options
@@ -11,22 +15,14 @@ module ActionController
#
class MiddlewareStack < ActionDispatch::MiddlewareStack #:nodoc:
class Middleware < ActionDispatch::MiddlewareStack::Middleware #:nodoc:
- def initialize(klass, *args, &block)
- options = args.extract_options!
- @only = Array(options.delete(:only)).map(&:to_s)
- @except = Array(options.delete(:except)).map(&:to_s)
- args << options unless options.empty?
- super
+ def initialize(klass, args, actions, strategy, block)
+ @actions = actions
+ @strategy = strategy
+ super(klass, args, block)
end
def valid?(action)
- if @only.present?
- @only.include?(action)
- elsif @except.present?
- !@except.include?(action)
- else
- true
- end
+ @strategy.call @actions, action
end
end
@@ -37,6 +33,32 @@ module ActionController
middleware.valid?(action) ? middleware.build(a) : a
end
end
+
+ private
+
+ INCLUDE = ->(list, action) { list.include? action }
+ EXCLUDE = ->(list, action) { !list.include? action }
+ NULL = ->(list, action) { true }
+
+ def build_middleware(klass, args, block)
+ options = args.extract_options!
+ only = Array(options.delete(:only)).map(&:to_s)
+ except = Array(options.delete(:except)).map(&:to_s)
+ args << options unless options.empty?
+
+ strategy = NULL
+ list = nil
+
+ if only.any?
+ strategy = INCLUDE
+ list = only
+ elsif except.any?
+ strategy = EXCLUDE
+ list = except
+ end
+
+ Middleware.new(klass, args, list, strategy, block)
+ end
end
# <tt>ActionController::Metal</tt> is the simplest possible controller, providing a
@@ -98,12 +120,6 @@ module ActionController
class Metal < AbstractController::Base
abstract!
- attr_internal_writer :env
-
- def env
- @_env ||= {}
- end
-
# 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.
@@ -111,26 +127,30 @@ module ActionController
# ==== Returns
# * <tt>string</tt>
def self.controller_name
- @controller_name ||= name.demodulize.sub(/Controller$/, '').underscore
+ @controller_name ||= name.demodulize.sub(/Controller$/, "").underscore
+ end
+
+ def self.make_response!(request)
+ ActionDispatch::Response.new.tap do |res|
+ res.request = request
+ end
+ end
+
+ 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
- # The details below can be overridden to support a specific
- # Request and Response object. The default ActionController::Base
- # implementation includes RackDelegation, which makes a request
- # and response object available. You might wish to control the
- # environment and response manually for performance reasons.
-
- attr_internal :headers, :response, :request
- delegate :session, :to => "@_request"
+ attr_internal :response, :request
+ delegate :session, to: "@_request"
+ delegate :headers, :status=, :location=, :content_type=,
+ :status, :location, :content_type, to: "@_response"
def initialize
- @_headers = {"Content-Type" => "text/html"}
- @_status = 200
@_request = nil
@_response = nil
@_routes = nil
@@ -145,68 +165,52 @@ module ActionController
@_params = val
end
- # Basic implementations for content_type=, location=, and headers are
- # provided to reduce the dependency on the RackDelegation module
- # in Renderer and Redirector.
-
- def content_type=(type)
- headers["Content-Type"] = type.to_s
- end
-
- def content_type
- headers["Content-Type"]
- end
-
- def location
- headers["Location"]
- end
-
- def location=(url)
- headers["Location"] = url
- end
+ alias :response_code :status # :nodoc:
- # Basic url_for that can be overridden for more robust functionality
+ # Basic url_for that can be overridden for more robust functionality.
def url_for(string)
string
end
- def status
- @_status
- end
- alias :response_code :status # :nodoc:
-
- def status=(status)
- @_status = Rack::Utils.status_code(status)
- end
-
def response_body=(body)
body = [body] unless body.nil? || body.respond_to?(:each)
+ response.reset_body!
+ return unless body
+ response.body = body
super
end
# Tests if render or redirect has already happened.
def performed?
- response_body || (response && response.committed?)
+ response_body || response.committed?
end
- def dispatch(name, request) #:nodoc:
+ def dispatch(name, request, response) #:nodoc:
set_request!(request)
+ set_response!(response)
process(name)
+ request.commit_flash
to_a
end
+ def set_response!(response) # :nodoc:
+ @_response = response
+ end
+
def set_request!(request) #:nodoc:
@_request = request
- @_env = request.env
- @_env['action_controller.instance'] = self
+ @_request.controller_instance = self
end
def to_a #:nodoc:
- response ? response.to_a : [status, headers, response_body]
+ response.to_a
end
- class_attribute :middleware_stack
- self.middleware_stack = ActionController::MiddlewareStack.new
+ def reset_session
+ @_request.reset_session
+ end
+
+ class_attribute :middleware_stack, default: ActionController::MiddlewareStack.new
def self.inherited(base) # :nodoc:
base.middleware_stack = middleware_stack.dup
@@ -224,21 +228,28 @@ 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)
+ # Returns a Rack endpoint for the given action name.
+ def self.action(name)
+ app = lambda { |env|
+ req = ActionDispatch::Request.new(env)
+ res = make_response! req
+ new.dispatch(name, req, res)
+ }
+
+ if middleware_stack.any?
+ middleware_stack.build(name, app)
+ else
+ app
+ end
end
- # Returns a Rack endpoint for the given action name.
- def self.action(name, klass = ActionDispatch::Request)
+ # Direct dispatch to the controller. Instantiates the controller, then
+ # executes the action named +name+.
+ def self.dispatch(name, req, res)
if middleware_stack.any?
- middleware_stack.build(name) do |env|
- new.dispatch(name, klass.new(env))
- end
+ middleware_stack.build(name) { |env| new.dispatch(name, req, res) }.call req.env
else
- lambda { |env| new.dispatch(name, klass.new(env)) }
+ new.dispatch(name, req, res)
end
end
end
diff --git a/actionpack/lib/action_controller/metal/basic_implicit_render.rb b/actionpack/lib/action_controller/metal/basic_implicit_render.rb
index 6c6f8381ff..2dc990f303 100644
--- a/actionpack/lib/action_controller/metal/basic_implicit_render.rb
+++ b/actionpack/lib/action_controller/metal/basic_implicit_render.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
module ActionController
- module BasicImplicitRender
+ module BasicImplicitRender # :nodoc:
def send_action(method, *args)
super.tap { default_render unless performed? }
end
diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb
index bb3ad9b850..d6911ee2b5 100644
--- a/actionpack/lib/action_controller/metal/conditional_get.rb
+++ b/actionpack/lib/action_controller/metal/conditional_get.rb
@@ -1,15 +1,15 @@
-require 'active_support/core_ext/hash/keys'
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/keys"
module ActionController
module ConditionalGet
extend ActiveSupport::Concern
- include RackDelegation
include Head
included do
- class_attribute :etaggers
- self.etaggers = []
+ class_attribute :etaggers, default: []
end
module ClassMethods
@@ -37,8 +37,23 @@ module ActionController
#
# === Parameters:
#
- # * <tt>:etag</tt>.
- # * <tt>:last_modified</tt>.
+ # * <tt>:etag</tt> Sets a "weak" ETag validator on the response. See the
+ # +:weak_etag+ option.
+ # * <tt>:weak_etag</tt> Sets a "weak" ETag validator on the response.
+ # Requests that set If-None-Match header may return a 304 Not Modified
+ # response if it matches the ETag exactly. A weak ETag indicates semantic
+ # equivalence, not byte-for-byte equality, so they're good for caching
+ # HTML pages in browser caches. They can't be used for responses that
+ # must be byte-identical, like serving Range requests within a PDF file.
+ # * <tt>:strong_etag</tt> Sets a "strong" ETag validator on the response.
+ # Requests that set If-None-Match header may return a 304 Not Modified
+ # response if it matches the ETag exactly. A strong ETag implies exact
+ # equality: the response must match byte for byte. This is necessary for
+ # doing Range requests within a large video or PDF file, for example, or
+ # for compatibility with some CDNs that don't support weak ETags.
+ # * <tt>:last_modified</tt> Sets a "weak" last-update validator on the
+ # response. Subsequent requests that set If-Modified-Since may return a
+ # 304 Not Modified response if last_modified <= If-Modified-Since.
# * <tt>:public</tt> By default the Cache-Control header is private, set this to
# +true+ if you want your application to be cacheable by other devices (proxy caches).
# * <tt>:template</tt> By default, the template digest for the current
@@ -67,7 +82,7 @@ module ActionController
#
# You can also pass an object that responds to +maximum+, such as a
# collection of active records. In this case +last_modified+ will be set by
- # calling +maximum(:updated_at)+ on the collection (the timestamp of the
+ # calling <tt>maximum(:updated_at)</tt> on the collection (the timestamp of the
# most recently updated record) and the +etag+ by passing the object itself.
#
# def index
@@ -87,12 +102,16 @@ module ActionController
#
# before_action { fresh_when @article, template: 'widgets/show' }
#
- def fresh_when(object = nil, etag: object, last_modified: nil, public: false, template: nil)
+ def fresh_when(object = nil, etag: nil, weak_etag: nil, strong_etag: nil, last_modified: nil, public: false, template: nil)
+ weak_etag ||= etag || object unless strong_etag
last_modified ||= object.try(:updated_at) || object.try(:maximum, :updated_at)
- if etag || template
- response.etag = combine_etags(etag: etag, last_modified: last_modified,
- public: public, template: template)
+ if strong_etag
+ response.strong_etag = combine_etags strong_etag,
+ last_modified: last_modified, public: public, template: template
+ elsif weak_etag || template
+ response.weak_etag = combine_etags weak_etag,
+ last_modified: last_modified, public: public, template: template
end
response.last_modified = last_modified if last_modified
@@ -108,8 +127,23 @@ module ActionController
#
# === Parameters:
#
- # * <tt>:etag</tt>.
- # * <tt>:last_modified</tt>.
+ # * <tt>:etag</tt> Sets a "weak" ETag validator on the response. See the
+ # +:weak_etag+ option.
+ # * <tt>:weak_etag</tt> Sets a "weak" ETag validator on the response.
+ # Requests that set If-None-Match header may return a 304 Not Modified
+ # response if it matches the ETag exactly. A weak ETag indicates semantic
+ # equivalence, not byte-for-byte equality, so they're good for caching
+ # HTML pages in browser caches. They can't be used for responses that
+ # must be byte-identical, like serving Range requests within a PDF file.
+ # * <tt>:strong_etag</tt> Sets a "strong" ETag validator on the response.
+ # Requests that set If-None-Match header may return a 304 Not Modified
+ # response if it matches the ETag exactly. A strong ETag implies exact
+ # equality: the response must match byte for byte. This is necessary for
+ # doing Range requests within a large video or PDF file, for example, or
+ # for compatibility with some CDNs that don't support weak ETags.
+ # * <tt>:last_modified</tt> Sets a "weak" last-update validator on the
+ # response. Subsequent requests that set If-Modified-Since may return a
+ # 304 Not Modified response if last_modified <= If-Modified-Since.
# * <tt>:public</tt> By default the Cache-Control header is private, set this to
# +true+ if you want your application to be cacheable by other devices (proxy caches).
# * <tt>:template</tt> By default, the template digest for the current
@@ -181,12 +215,12 @@ module ActionController
# super if stale? @article, template: 'widgets/show'
# end
#
- def stale?(object = nil, etag: object, last_modified: nil, public: nil, template: nil)
- fresh_when(object, etag: etag, last_modified: last_modified, public: public, template: template)
+ def stale?(object = nil, **freshness_kwargs)
+ fresh_when(object, **freshness_kwargs)
!request.fresh?(response)
end
- # Sets a HTTP 1.1 Cache-Control header. Defaults to issuing a +private+
+ # Sets an HTTP 1.1 Cache-Control header. Defaults to issuing a +private+
# instruction, so that intermediate caches must not cache the response.
#
# expires_in 20.minutes
@@ -194,49 +228,55 @@ module ActionController
# expires_in 3.hours, public: true, must_revalidate: true
#
# This method will overwrite an existing Cache-Control header.
- # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities.
+ # See https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities.
+ #
+ # HTTP Cache-Control Extensions for Stale Content. See https://tools.ietf.org/html/rfc5861
+ # It helps to cache an asset and serve it while is being revalidated and/or returning with an error.
#
- # The method will also ensure a HTTP Date header for client compatibility.
+ # expires_in 3.hours, public: true, stale_while_revalidate: 60.seconds
+ # expires_in 3.hours, public: true, stale_while_revalidate: 60.seconds, stale_if_error: 5.minutes
+ #
+ # The method will also ensure an HTTP Date header for client compatibility.
def expires_in(seconds, options = {})
response.cache_control.merge!(
- :max_age => seconds,
- :public => options.delete(:public),
- :must_revalidate => options.delete(:must_revalidate)
+ max_age: seconds,
+ public: options.delete(:public),
+ must_revalidate: options.delete(:must_revalidate),
+ stale_while_revalidate: options.delete(:stale_while_revalidate),
+ stale_if_error: options.delete(:stale_if_error),
)
options.delete(:private)
- response.cache_control[:extras] = options.map {|k,v| "#{k}=#{v}"}
+ response.cache_control[:extras] = options.map { |k, v| "#{k}=#{v}" }
response.date = Time.now unless response.date?
end
- # Sets a HTTP 1.1 Cache-Control header of <tt>no-cache</tt> so no caching should
- # occur by the browser or intermediate caches (like caching proxy servers).
+ # Sets an HTTP 1.1 Cache-Control header of <tt>no-cache</tt>. This means the
+ # resource will be marked as stale, so clients must always revalidate.
+ # Intermediate/browser caches may still store the asset.
def expires_now
- response.cache_control.replace(:no_cache => true)
+ response.cache_control.replace(no_cache: true)
end
# Cache or yield the block. The cache is supposed to never expire.
#
- # You can use this method when you have a HTTP response that never changes,
+ # You can use this method when you have an HTTP response that never changes,
# and the browser and proxies should cache it indefinitely.
#
# * +public+: By default, HTTP responses are private, cached only on the
# user's web browser. To allow proxies to cache the response, set +true+ to
# indicate that they can serve the cached response to all users.
- #
- # * +version+: the version passed as a key for the cache.
- def http_cache_forever(public: false, version: 'v1')
+ def http_cache_forever(public: false)
expires_in 100.years, public: public
- yield if stale?(etag: "#{version}-#{request.fullpath}",
- last_modified: Time.parse('2011-01-01').utc,
+ yield if stale?(etag: request.fullpath,
+ last_modified: Time.new(2011, 1, 1).utc,
public: public)
end
private
- def combine_etags(options)
- etags = etaggers.map { |etagger| instance_exec(options, &etagger) }.compact
- etags.unshift options[:etag]
+ def combine_etags(validator, options)
+ [validator, *etaggers.map { |etagger| instance_exec(options, &etagger) }].compact
end
end
end
diff --git a/actionpack/lib/action_controller/metal/content_security_policy.rb b/actionpack/lib/action_controller/metal/content_security_policy.rb
new file mode 100644
index 0000000000..b8fab4ebe3
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/content_security_policy.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module ActionController #:nodoc:
+ module ContentSecurityPolicy
+ # TODO: Documentation
+ extend ActiveSupport::Concern
+
+ include AbstractController::Helpers
+ include AbstractController::Callbacks
+
+ included do
+ helper_method :content_security_policy?
+ helper_method :content_security_policy_nonce
+ end
+
+ module ClassMethods
+ def content_security_policy(enabled = true, **options, &block)
+ before_action(options) do
+ if block_given?
+ policy = current_content_security_policy
+ yield policy
+ request.content_security_policy = policy
+ end
+
+ unless enabled
+ request.content_security_policy = nil
+ end
+ end
+ end
+
+ def content_security_policy_report_only(report_only = true, **options)
+ before_action(options) do
+ request.content_security_policy_report_only = report_only
+ end
+ end
+ end
+
+ private
+
+ def content_security_policy?
+ request.content_security_policy
+ end
+
+ def content_security_policy_nonce
+ request.content_security_policy_nonce
+ end
+
+ def current_content_security_policy
+ request.content_security_policy.try(:clone) || ActionDispatch::ContentSecurityPolicy.new
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/cookies.rb b/actionpack/lib/action_controller/metal/cookies.rb
index d787f014cd..ff46966693 100644
--- a/actionpack/lib/action_controller/metal/cookies.rb
+++ b/actionpack/lib/action_controller/metal/cookies.rb
@@ -1,11 +1,11 @@
+# frozen_string_literal: true
+
module ActionController #:nodoc:
module Cookies
extend ActiveSupport::Concern
- include RackDelegation
-
included do
- helper_method :cookies
+ helper_method :cookies if defined?(helper_method)
end
private
diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb
index 1abd8d3a33..9ef4f50df1 100644
--- a/actionpack/lib/action_controller/metal/data_streaming.rb
+++ b/actionpack/lib/action_controller/metal/data_streaming.rb
@@ -1,4 +1,7 @@
-require 'action_controller/metal/exceptions'
+# frozen_string_literal: true
+
+require "action_controller/metal/exceptions"
+require "action_dispatch/http/content_disposition"
module ActionController #:nodoc:
# Methods for sending arbitrary data and for streaming files to the browser,
@@ -8,10 +11,10 @@ module ActionController #:nodoc:
include ActionController::Rendering
- DEFAULT_SEND_FILE_TYPE = 'application/octet-stream'.freeze #:nodoc:
- DEFAULT_SEND_FILE_DISPOSITION = 'attachment'.freeze #:nodoc:
+ DEFAULT_SEND_FILE_TYPE = "application/octet-stream" #:nodoc:
+ DEFAULT_SEND_FILE_DISPOSITION = "attachment" #: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+.
@@ -25,14 +28,13 @@ module ActionController #:nodoc:
# * <tt>:filename</tt> - suggests a filename for the browser to use.
# Defaults to <tt>File.basename(path)</tt>.
# * <tt>:type</tt> - specifies an HTTP content type.
- # You can specify either a string or a symbol for a registered type register with
- # <tt>Mime::Type.register</tt>, for example :json
- # If omitted, type will be guessed from the file extension specified in <tt>:filename</tt>.
- # If no content type is registered for the extension, default type 'application/octet-stream' will be used.
+ # You can specify either a string or a symbol for a registered type with <tt>Mime::Type.register</tt>, for example :json.
+ # If omitted, the type will be inferred from the file extension specified in <tt>:filename</tt>.
+ # If no content type is registered for the extension, the default type 'application/octet-stream' will be used.
# * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
# Valid values are 'inline' and 'attachment' (default).
# * <tt>:status</tt> - specifies the status code to send with the response. Defaults to 200.
- # * <tt>:url_based_filename</tt> - set to +true+ if you want the browser guess the filename from
+ # * <tt>:url_based_filename</tt> - set to +true+ if you want the browser to guess the filename from
# the URL, which is necessary for i18n filenames on certain browsers
# (setting <tt>:filename</tt> overrides this option).
#
@@ -55,58 +57,38 @@ module ActionController #:nodoc:
#
# Read about the other Content-* HTTP headers if you'd like to
# provide the user with more information (such as Content-Description) in
- # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11.
+ # https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11.
#
# Also be aware that the document may be cached by proxies and browsers.
# The Pragma and Cache-Control headers declare how the file may be cached
# by intermediaries. They default to require clients to validate with
# the server before releasing cached responses. See
- # http://www.mnot.net/cache_docs/ for an overview of web caching and
- # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
+ # https://www.mnot.net/cache_docs/ for an overview of web caching and
+ # https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
# for the Cache-Control header spec.
def send_file(path, options = {}) #:doc:
- raise MissingFile, "Cannot read file #{path}" unless File.file?(path) and File.readable?(path)
+ raise MissingFile, "Cannot read file #{path}" unless File.file?(path) && File.readable?(path)
options[:filename] ||= File.basename(path) unless options[:url_based_filename]
send_file_headers! options
self.status = options[:status] || 200
self.content_type = options[:content_type] if options.key?(:content_type)
- self.response_body = FileBody.new(path)
- end
-
- # Avoid having to pass an open file handle as the response body.
- # Rack::Sendfile will usually intercept the response and uses
- # the path directly, so there is no reason to open the file.
- class FileBody #:nodoc:
- attr_reader :to_path
-
- def initialize(path)
- @to_path = path
- end
-
- # Stream the file's contents if Rack::Sendfile isn't present.
- def each
- File.open(to_path, 'rb') do |file|
- while chunk = file.read(16384)
- yield chunk
- end
- end
- end
+ response.send_file path
end
# Sends the given binary data to the browser. This method is similar to
# <tt>render plain: data</tt>, but also allows you to specify whether
# the browser should display the response as a file attachment (i.e. in a
# download dialog) or as inline data. You may also set the content type,
- # the apparent file name, and other things.
+ # the file name, and other things.
#
# Options:
# * <tt>:filename</tt> - suggests a filename for the browser to use.
- # * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'. You can specify
- # either a string or a symbol for a registered type register with <tt>Mime::Type.register</tt>, for example :json
- # If omitted, type will be guessed from the file extension specified in <tt>:filename</tt>.
- # If no content type is registered for the extension, default type 'application/octet-stream' will be used.
+ # * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'.
+ # You can specify either a string or a symbol for a registered type with <tt>Mime::Type.register</tt>, for example :json.
+ # If omitted, type will be inferred from the file extension specified in <tt>:filename</tt>.
+ # If no content type is registered for the extension, the default type 'application/octet-stream' will be used.
# * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
# Valid values are 'inline' and 'attachment' (default).
# * <tt>:status</tt> - specifies the status code to send with the response. Defaults to 200.
@@ -126,14 +108,16 @@ module ActionController #:nodoc:
# See +send_file+ for more information on HTTP Content-* headers and caching.
def send_data(data, options = {}) #:doc:
send_file_headers! options
- render options.slice(:status, :content_type).merge(:text => data)
+ render options.slice(:status, :content_type).merge(body: data)
end
- private
def send_file_headers!(options)
type_provided = options.has_key?(:type)
content_type = options.fetch(:type, DEFAULT_SEND_FILE_TYPE)
+ self.content_type = content_type
+ response.sending_file = true
+
raise ArgumentError, ":type option required" if content_type.nil?
if content_type.is_a?(Symbol)
@@ -143,21 +127,17 @@ module ActionController #:nodoc:
else
if !type_provided && options[:filename]
# If type wasn't provided, try guessing from file extension.
- content_type = Mime::Type.lookup_by_extension(File.extname(options[:filename]).downcase.delete('.')) || content_type
+ content_type = Mime::Type.lookup_by_extension(File.extname(options[:filename]).downcase.delete(".")) || content_type
end
self.content_type = content_type
end
disposition = options.fetch(:disposition, DEFAULT_SEND_FILE_DISPOSITION)
- unless disposition.nil?
- disposition = disposition.to_s
- disposition += %(; filename="#{options[:filename]}") if options[:filename]
- headers['Content-Disposition'] = disposition
+ if disposition
+ headers["Content-Disposition"] = ActionDispatch::Http::ContentDisposition.format(disposition: disposition, filename: options[:filename])
end
- headers['Content-Transfer-Encoding'] = 'binary'
-
- response.sending_file = true
+ headers["Content-Transfer-Encoding"] = "binary"
# Fix a problem with IE 6.0 on opening downloaded files:
# If Cache-Control: no-cache is set (which Rails does by default),
diff --git a/actionpack/lib/action_controller/metal/default_headers.rb b/actionpack/lib/action_controller/metal/default_headers.rb
new file mode 100644
index 0000000000..eef0602fcd
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/default_headers.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module ActionController
+ # Allows configuring default headers that will be automatically merged into
+ # each response.
+ module DefaultHeaders
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ def make_response!(request)
+ ActionDispatch::Response.create.tap do |res|
+ res.request = request
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/etag_with_flash.rb b/actionpack/lib/action_controller/metal/etag_with_flash.rb
new file mode 100644
index 0000000000..38899e2f16
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/etag_with_flash.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+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
+ # in mind. This does that by including the content of the flash as a component
+ # in the ETag that's generated for a response.
+ module EtagWithFlash
+ extend ActiveSupport::Concern
+
+ include ActionController::ConditionalGet
+
+ included do
+ etag { flash unless flash.empty? }
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/etag_with_template_digest.rb b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb
index f9303efe6c..640c75536e 100644
--- a/actionpack/lib/action_controller/metal/etag_with_template_digest.rb
+++ b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionController
# When our views change, they should bubble up into HTTP cache freshness
# and bust browser caches. So the template digest for the current action
@@ -22,10 +24,9 @@ module ActionController
include ActionController::ConditionalGet
included do
- class_attribute :etag_with_template_digest
- self.etag_with_template_digest = true
+ class_attribute :etag_with_template_digest, default: true
- ActiveSupport.on_load :action_view, yield: true do |action_view_base|
+ ActiveSupport.on_load :action_view, yield: true do
etag do |options|
determine_template_etag(options) if etag_with_template_digest
end
@@ -33,18 +34,24 @@ module ActionController
end
private
- def determine_template_etag(options)
- if template = pick_template_for_etag(options)
- lookup_and_digest_template(template)
+ def determine_template_etag(options)
+ if template = pick_template_for_etag(options)
+ lookup_and_digest_template(template)
+ end
end
- end
- def pick_template_for_etag(options)
- options.fetch(:template) { "#{controller_name}/#{action_name}" }
- end
+ # Pick the template digest to include in the ETag. If the +:template+ option
+ # is present, use the named template. If +:template+ is +nil+ or absent, use
+ # the default controller/action template. If +:template+ is false, omit the
+ # template digest from the ETag.
+ def pick_template_for_etag(options)
+ unless options[:template] == false
+ options[:template] || "#{controller_path}/#{action_name}"
+ end
+ end
- def lookup_and_digest_template(template)
- ActionView::Digestor.digest name: template, finder: lookup_context
- end
+ def lookup_and_digest_template(template)
+ ActionView::Digestor.digest name: template, finder: lookup_context
+ end
end
end
diff --git a/actionpack/lib/action_controller/metal/exceptions.rb b/actionpack/lib/action_controller/metal/exceptions.rb
index 18e003741d..30034be018 100644
--- a/actionpack/lib/action_controller/metal/exceptions.rb
+++ b/actionpack/lib/action_controller/metal/exceptions.rb
@@ -1,16 +1,13 @@
+# frozen_string_literal: true
+
module ActionController
class ActionControllerError < StandardError #:nodoc:
end
class BadRequest < ActionControllerError #:nodoc:
- attr_reader :original_exception
-
- def initialize(type = nil, e = nil)
- return super() unless type && e
-
- super("Invalid #{type} parameters: #{e.message}")
- @original_exception = e
- set_backtrace e.backtrace
+ def initialize(msg = nil)
+ super(msg)
+ set_backtrace $!.backtrace if $!
end
end
@@ -19,32 +16,29 @@ module ActionController
class RoutingError < ActionControllerError #:nodoc:
attr_reader :failures
- def initialize(message, failures=[])
+ def initialize(message, failures = [])
super(message)
@failures = failures
end
end
- class ActionController::UrlGenerationError < ActionControllerError #:nodoc:
+ class UrlGenerationError < ActionControllerError #:nodoc:
end
class MethodNotAllowed < ActionControllerError #:nodoc:
def initialize(*allowed_methods)
- super("Only #{allowed_methods.to_sentence(:locale => :en)} requests are allowed.")
+ super("Only #{allowed_methods.to_sentence(locale: :en)} requests are allowed.")
end
end
class NotImplemented < MethodNotAllowed #:nodoc:
end
- class UnknownController < ActionControllerError #:nodoc:
- end
-
class MissingFile < ActionControllerError #:nodoc:
end
class SessionOverflowError < ActionControllerError #:nodoc:
- DEFAULT_MESSAGE = 'Your session data is larger than the data column in which it is to be stored. You must increase the size of your data column if you intend to store large data.'
+ DEFAULT_MESSAGE = "Your session data is larger than the data column in which it is to be stored. You must increase the size of your data column if you intend to store large data."
def initialize(message = nil)
super(message || DEFAULT_MESSAGE)
@@ -56,4 +50,25 @@ module ActionController
class UnknownFormat < ActionControllerError #:nodoc:
end
+
+ # Raised when a nested respond_to is triggered and the content types of each
+ # are incompatible. For exampe:
+ #
+ # respond_to do |outer_type|
+ # outer_type.js do
+ # respond_to do |inner_type|
+ # inner_type.html { render body: "HTML" }
+ # end
+ # end
+ # end
+ class RespondToMismatchError < ActionControllerError
+ DEFAULT_MESSAGE = "respond_to was called multiple times and matched with conflicting formats in this action. Please note that you may only call respond_to and match on a single format per action."
+
+ def initialize(message = nil)
+ super(message || DEFAULT_MESSAGE)
+ end
+ end
+
+ class MissingExactTemplate < UnknownFormat #:nodoc:
+ end
end
diff --git a/actionpack/lib/action_controller/metal/flash.rb b/actionpack/lib/action_controller/metal/flash.rb
index 65351284b9..380f2e9591 100644
--- a/actionpack/lib/action_controller/metal/flash.rb
+++ b/actionpack/lib/action_controller/metal/flash.rb
@@ -1,10 +1,11 @@
+# frozen_string_literal: true
+
module ActionController #:nodoc:
module Flash
extend ActiveSupport::Concern
included do
- class_attribute :_flash_types, instance_accessor: false
- self._flash_types = []
+ class_attribute :_flash_types, instance_accessor: false, default: []
delegate :flash, to: :request
add_flash_types(:alert, :notice)
@@ -35,14 +36,14 @@ module ActionController #:nodoc:
define_method(type) do
request.flash[type]
end
- helper_method type
+ helper_method(type) if respond_to?(:helper_method)
self._flash_types += [type]
end
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 d920668184..26e6f72b66 100644
--- a/actionpack/lib/action_controller/metal/force_ssl.rb
+++ b/actionpack/lib/action_controller/metal/force_ssl.rb
@@ -1,19 +1,13 @@
-require 'active_support/core_ext/hash/except'
-require 'active_support/core_ext/hash/slice'
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/except"
+require "active_support/core_ext/hash/slice"
module ActionController
- # This module provides a method which will redirect browser to use HTTPS
- # protocol. This will ensure that user's sensitive information will be
- # transferred safely over the internet. You _should_ always force 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
- # user from getting session hijacked when accessing the site under unsecured
- # HTTP protocol.
- module ForceSSL
+ # This module is deprecated in favor of +config.force_ssl+ in your environment
+ # config file. This will ensure all endpoints not explicitly marked otherwise
+ # will have all communication served over HTTPS.
+ module ForceSSL # :nodoc:
extend ActiveSupport::Concern
include AbstractController::Callbacks
@@ -21,45 +15,17 @@ module ActionController
URL_OPTIONS = [:protocol, :host, :domain, :subdomain, :port, :path]
REDIRECT_OPTIONS = [:status, :flash, :alert, :notice]
- module ClassMethods
- # Force the request to this particular controller or specified actions to be
- # under HTTPS protocol.
- #
- # If you need to disable this for any reason (e.g. development) then you can use
- # an +:if+ or +:unless+ condition.
- #
- # class AccountsController < ApplicationController
- # force_ssl if: :ssl_configured?
- #
- # def ssl_configured?
- # !Rails.env.development?
- # end
- # end
- #
- # ==== URL Options
- # You can pass any of the following options to affect the redirect url
- # * <tt>host</tt> - Redirect to a different host name
- # * <tt>subdomain</tt> - Redirect to a different subdomain
- # * <tt>domain</tt> - Redirect to a different domain
- # * <tt>port</tt> - Redirect to a non-standard port
- # * <tt>path</tt> - Redirect to a different path
- #
- # ==== Redirect Options
- # You can pass any of the following options to affect the redirect status and response
- # * <tt>status</tt> - Redirect with a custom status (default is 301 Moved Permanently)
- # * <tt>flash</tt> - Set a flash message when redirecting
- # * <tt>alert</tt> - Set an alert message when redirecting
- # * <tt>notice</tt> - Set a notice message when redirecting
- #
- # ==== Action Options
- # You can pass any of the following options to affect the before_action callback
- # * <tt>only</tt> - The callback should be run only for this action
- # * <tt>except</tt> - The callback should be run for all actions except this action
- # * <tt>if</tt> - A symbol naming an instance method or a proc; the callback
- # will be called only when it returns a true value.
- # * <tt>unless</tt> - A symbol naming an instance method or a proc; the callback
- # will be called only when it returns a false value.
+ module ClassMethods # :nodoc:
def force_ssl(options = {})
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
+ Controller-level `force_ssl` is deprecated and will be removed from
+ Rails 6.1. Please enable `config.force_ssl` in your environment
+ configuration to enable the ActionDispatch::SSL middleware to more
+ fully enforce that your application communicate over HTTPS. If needed,
+ you can use `config.ssl_options` to exempt matching endpoints from
+ being redirected to HTTPS.
+ MESSAGE
+
action_options = options.slice(*ACTION_OPTIONS)
redirect_options = options.except(*ACTION_OPTIONS)
before_action(action_options) do
@@ -68,18 +34,13 @@ module ActionController
end
end
- # Redirect the existing request to use the HTTPS protocol.
- #
- # ==== Parameters
- # * <tt>host_or_options</tt> - Either a host name or any of the url & redirect options
- # available to the <tt>force_ssl</tt> method.
def force_ssl_redirect(host_or_options = nil)
unless request.ssl?
options = {
- :protocol => 'https://',
- :host => request.host,
- :path => request.fullpath,
- :status => :moved_permanently
+ protocol: "https://",
+ host: request.host,
+ path: request.fullpath,
+ status: :moved_permanently
}
if host_or_options.is_a?(Hash)
@@ -89,7 +50,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 f445094bdc..3c84bebb85 100644
--- a/actionpack/lib/action_controller/metal/head.rb
+++ b/actionpack/lib/action_controller/metal/head.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionController
module Head
# Returns a response that has no content (merely headers). The options
@@ -18,22 +20,16 @@ module ActionController
# See Rack::Utils::SYMBOL_TO_STATUS_CODE for a full list of valid +status+ symbols.
def head(status, options = {})
if status.is_a?(Hash)
- msg = status[:status] ? 'The :status option' : 'The implicit :ok status'
- options, status = status, status.delete(:status)
-
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- #{msg} on `head` has been deprecated and will be removed in Rails 5.1.
- Please pass the status as a separate parameter before the options, instead.
- MSG
+ raise ArgumentError, "#{status.inspect} is not a valid value for `status`."
end
status ||= :ok
-
+
location = options.delete(:location)
content_type = options.delete(:content_type)
options.each do |key, value|
- headers[key.to_s.dasherize.split('-').each { |v| v[0] = v[0].chr.upcase }.join('-')] = value.to_s
+ headers[key.to_s.dasherize.split("-").each { |v| v[0] = v[0].chr.upcase }.join("-")] = value.to_s
end
self.status = status
@@ -41,28 +37,24 @@ module ActionController
self.response_body = ""
- if include_content?(self.response_code)
- self.content_type = content_type || (Mime[formats.first] if formats)
- self.response.charset = false if self.response
- else
- headers.delete('Content-Type')
- headers.delete('Content-Length')
+ if include_content?(response_code)
+ self.content_type = content_type || (Mime[formats.first] if formats) || Mime[:html]
+ response.charset = false
end
-
+
true
end
private
- # :nodoc:
- def include_content?(status)
- case status
- when 100..199
- false
- when 204, 205, 304
- false
- else
- true
+ def include_content?(status)
+ case status
+ when 100..199
+ false
+ when 204, 205, 304
+ false
+ else
+ true
+ end
end
- end
end
end
diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb
index b4da381d26..0faaac1ce4 100644
--- a/actionpack/lib/action_controller/metal/helpers.rb
+++ b/actionpack/lib/action_controller/metal/helpers.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionController
# The \Rails framework provides a large number of helpers for working with assets, dates, forms,
# numbers and model objects, to name a few. These helpers are available to all templates
@@ -5,10 +7,10 @@ module ActionController
#
# In addition to using the standard template helpers provided, creating custom helpers to
# extract complicated logic or reusable functionality is strongly encouraged. By default, each controller
- # will include all helpers. These helpers are only accessible on the controller through <tt>.helpers</tt>
+ # will include all helpers. These helpers are only accessible on the controller through <tt>#helpers</tt>
#
- # In previous versions of \Rails the controller will include a helper whose
- # name matches that of the controller, e.g., <tt>MyController</tt> will automatically
+ # In previous versions of \Rails the controller will include a helper which
+ # matches the name of the controller, e.g., <tt>MyController</tt> will automatically
# include <tt>MyHelper</tt>. To return old behavior set +config.action_controller.include_all_helpers+ to +false+.
#
# Additional helpers can be specified using the +helper+ class method in ActionController::Base or any
@@ -53,9 +55,8 @@ module ActionController
include AbstractController::Helpers
included do
- class_attribute :helpers_path, :include_all_helpers
- self.helpers_path ||= []
- self.include_all_helpers = true
+ class_attribute :helpers_path, default: []
+ class_attribute :include_all_helpers, default: true
end
module ClassMethods
@@ -71,9 +72,9 @@ module ActionController
attrs.flatten.each { |attr| helper_method(attr, "#{attr}=") }
end
- # Provides a proxy to access helpers methods from outside the view.
+ # Provides a proxy to access helper methods from outside the view.
def helpers
- @helper_proxy ||= begin
+ @helper_proxy ||= begin
proxy = ActionView::Base.new
proxy.config = config.inheritable_copy
proxy.extend(_helpers)
@@ -99,8 +100,7 @@ module ActionController
# # => ["application", "chart", "rubygems"]
def all_helpers_from_path(path)
helpers = Array(path).flat_map do |_path|
- extract = /^#{Regexp.quote(_path.to_s)}\/?(.*)_helper.rb$/
- names = Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1') }
+ names = Dir["#{_path}/**/*_helper.rb"].map { |file| file[_path.to_s.size + 1..-"_helper.rb".size - 1] }
names.sort!
end
helpers.uniq!
@@ -108,10 +108,15 @@ module ActionController
end
private
- # Extract helper names from files in <tt>app/helpers/**/*_helper.rb</tt>
- def all_application_helpers
- all_helpers_from_path(helpers_path)
- end
+ # Extract helper names from files in <tt>app/helpers/**/*_helper.rb</tt>
+ def all_application_helpers
+ all_helpers_from_path(helpers_path)
+ end
+ end
+
+ # Provides a proxy to access helper methods from outside the view.
+ def helpers
+ @_helper_proxy ||= view_context
end
end
end
diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb
index 39bed955a4..7036123d5d 100644
--- a/actionpack/lib/action_controller/metal/http_authentication.rb
+++ b/actionpack/lib/action_controller/metal/http_authentication.rb
@@ -1,4 +1,7 @@
-require 'base64'
+# frozen_string_literal: true
+
+require "base64"
+require "active_support/security_utils"
module ActionController
# Makes it dead easy to do HTTP Basic, Digest and Token authentication.
@@ -27,14 +30,14 @@ 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
#
# def authenticate
# case request.format
- # when Mime::XML, Mime::ATOM
+ # when Mime[:xml], Mime[:atom]
# if user = authenticate_with_http_basic { |u, p| @account.users.authenticate(u, p) }
# @current_user = user
# else
@@ -53,8 +56,9 @@ module ActionController
# In your integration tests, you can do something like this:
#
# def test_access_granted_from_xml
- # @request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password)
- # get "/notes/1.xml"
+ # authorization = ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password)
+ #
+ # get "/notes/1.xml", headers: { 'HTTP_AUTHORIZATION' => authorization }
#
# assert_equal 200, status
# end
@@ -68,7 +72,11 @@ module ActionController
def http_basic_authenticate_with(options = {})
before_action(options.except(:name, :password, :realm)) do
authenticate_or_request_with_http_basic(options[:realm] || "Application") do |name, password|
- name == options[:name] && password == options[:password]
+ # This comparison uses & so that it doesn't short circuit and
+ # uses `secure_compare` so that length information
+ # isn't leaked.
+ ActiveSupport::SecurityUtils.secure_compare(name, options[:name]) &
+ ActiveSupport::SecurityUtils.secure_compare(password, options[:password])
end
end
end
@@ -94,23 +102,23 @@ module ActionController
end
def has_basic_credentials?(request)
- request.authorization.present? && (auth_scheme(request) == 'Basic')
+ request.authorization.present? && (auth_scheme(request).downcase == "basic")
end
def user_name_and_password(request)
- decode_credentials(request).split(':', 2)
+ decode_credentials(request).split(":", 2)
end
def decode_credentials(request)
- ::Base64.decode64(auth_param(request) || '')
+ ::Base64.decode64(auth_param(request) || "")
end
def auth_scheme(request)
- request.authorization.to_s.split(' ', 2).first
+ request.authorization.to_s.split(" ", 2).first
end
def auth_param(request)
- request.authorization.to_s.split(' ', 2).second
+ request.authorization.to_s.split(" ", 2).second
end
def encode_credentials(user_name, password)
@@ -119,7 +127,7 @@ module ActionController
def authentication_request(controller, realm, message)
message ||= "HTTP Basic: Access denied.\n"
- controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.tr('"'.freeze, "".freeze)}")
+ controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.tr('"', "")}")
controller.status = 401
controller.response_body = message
end
@@ -203,7 +211,7 @@ module ActionController
password = password_procedure.call(credentials[:username])
return false unless password
- method = request.env['rack.methodoverride.original_method'] || request.env['REQUEST_METHOD']
+ method = request.get_header("rack.methodoverride.original_method") || request.get_header("REQUEST_METHOD")
uri = credentials[:uri]
[true, false].any? do |trailing_question_mark|
@@ -219,19 +227,19 @@ module ActionController
# Returns the expected response for a request of +http_method+ to +uri+ with the decoded +credentials+ and the expected +password+
# Optional parameter +password_is_ha1+ is set to +true+ by default, since best practice is to store ha1 digest instead
# of a plain-text password.
- def expected_response(http_method, uri, credentials, password, password_is_ha1=true)
+ def expected_response(http_method, uri, credentials, password, password_is_ha1 = true)
ha1 = password_is_ha1 ? password : ha1(credentials, password)
- ha2 = ::Digest::MD5.hexdigest([http_method.to_s.upcase, uri].join(':'))
- ::Digest::MD5.hexdigest([ha1, credentials[:nonce], credentials[:nc], credentials[:cnonce], credentials[:qop], ha2].join(':'))
+ ha2 = ::Digest::MD5.hexdigest([http_method.to_s.upcase, uri].join(":"))
+ ::Digest::MD5.hexdigest([ha1, credentials[:nonce], credentials[:nc], credentials[:cnonce], credentials[:qop], ha2].join(":"))
end
def ha1(credentials, password)
- ::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(':'))
+ ::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(":"))
end
def encode_credentials(http_method, credentials, password, password_is_ha1)
credentials[:response] = expected_response(http_method, credentials[:uri], credentials, password, password_is_ha1)
- "Digest " + credentials.sort_by {|x| x[0].to_s }.map {|v| "#{v[0]}='#{v[1]}'" }.join(', ')
+ "Digest " + credentials.sort_by { |x| x[0].to_s }.map { |v| "#{v[0]}='#{v[1]}'" }.join(", ")
end
def decode_credentials_header(request)
@@ -239,9 +247,9 @@ module ActionController
end
def decode_credentials(header)
- ActiveSupport::HashWithIndifferentAccess[header.to_s.gsub(/^Digest\s+/, '').split(',').map do |pair|
- key, value = pair.split('=', 2)
- [key.strip, value.to_s.gsub(/^"|"$/,'').delete('\'')]
+ ActiveSupport::HashWithIndifferentAccess[header.to_s.gsub(/^Digest\s+/, "").split(",").map do |pair|
+ key, value = pair.split("=", 2)
+ [key.strip, value.to_s.gsub(/^"|"$/, "").delete("'")]
end]
end
@@ -260,8 +268,8 @@ module ActionController
end
def secret_token(request)
- key_generator = request.env["action_dispatch.key_generator"]
- http_auth_salt = request.env["action_dispatch.http_auth_salt"]
+ key_generator = request.key_generator
+ http_auth_salt = request.http_auth_salt
key_generator.generate_key(http_auth_salt)
end
@@ -305,11 +313,11 @@ module ActionController
end
# Might want a shorter timeout depending on whether the request
- # is a PATCH, PUT, or POST, and if client is browser or web service.
+ # is a PATCH, PUT, or POST, and if the client is a browser or web service.
# Can be much shorter if the Stale directive is implemented. This would
- # allow a user to use new nonce without prompting user again for their
+ # allow a user to use new nonce without prompting the user again for their
# username and password.
- def validate_nonce(secret_key, request, value, seconds_to_timeout=5*60)
+ def validate_nonce(secret_key, request, value, seconds_to_timeout = 5 * 60)
return false if value.nil?
t = ::Base64.decode64(value).split(":").first.to_i
nonce(secret_key, t) == value && (t - Time.now.to_i).abs <= seconds_to_timeout
@@ -319,7 +327,6 @@ module ActionController
def opaque(secret_key)
::Digest::MD5.hexdigest(secret_key)
end
-
end
# Makes it dead easy to do HTTP Token authentication.
@@ -342,7 +349,9 @@ module ActionController
# private
# def authenticate
# authenticate_or_request_with_http_token do |token, options|
- # token == TOKEN
+ # # Compare the tokens in a time-constant manner, to mitigate
+ # # timing attacks.
+ # ActiveSupport::SecurityUtils.secure_compare(token, TOKEN)
# end
# end
# end
@@ -354,14 +363,14 @@ 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
#
# def authenticate
# case request.format
- # when Mime::XML, Mime::ATOM
+ # when Mime[:xml], Mime[:atom]
# if user = authenticate_with_http_token { |t, o| @account.users.authenticate(t, o) }
# @current_user = user
# else
@@ -381,10 +390,9 @@ module ActionController
# In your integration tests, you can do something like this:
#
# def test_access_granted_from_xml
- # get(
- # "/notes/1.xml", nil,
- # 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Token.encode_credentials(users(:dhh).token)
- # )
+ # authorization = ActionController::HttpAuthentication::Token.encode_credentials(users(:dhh).token)
+ #
+ # get "/notes/1.xml", headers: { 'HTTP_AUTHORIZATION' => authorization }
#
# assert_equal 200, status
# end
@@ -396,8 +404,8 @@ module ActionController
#
# RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L]
module Token
- TOKEN_KEY = 'token='
- TOKEN_REGEX = /^(Token|Bearer) /
+ TOKEN_KEY = "token="
+ TOKEN_REGEX = /^(Token|Bearer)\s+/
AUTHN_PAIR_DELIMITERS = /(?:,|;|\t+)/
extend self
@@ -436,15 +444,17 @@ module ActionController
end
end
- # Parses the token and options out of the token authorization header. If
- # the header looks like this:
+ # 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"
- # Then the returned token is "abc", and the options is {nonce: "def"}
+ # Then the returned token is <tt>"abc"</tt>, and the options are
+ # <tt>{nonce: "def"}</tt>
#
# request - ActionDispatch::Request instance with the current headers.
#
- # Returns an Array of [String, Hash] if a token is present.
- # Returns nil if no token is found.
+ # Returns an +Array+ of <tt>[String, Hash]</tt> if a token is present.
+ # Returns +nil+ if no token is found.
def token_and_options(request)
authorization_request = request.authorization.to_s
if authorization_request[TOKEN_REGEX]
@@ -464,14 +474,14 @@ module ActionController
# This removes the <tt>"</tt> characters wrapping the value.
def rewrite_param_values(array_params)
- array_params.each { |param| (param[1] || "").gsub! %r/^"|"$/, '' }
+ array_params.each { |param| (param[1] || +"").gsub! %r/^"|"$/, "" }
end
# This method takes an authorization body and splits up the key-value
# pairs by the standardized <tt>:</tt>, <tt>;</tt>, or <tt>\t</tt>
# delimiters defined in +AUTHN_PAIR_DELIMITERS+.
def raw_params(auth)
- _raw_params = auth.sub(TOKEN_REGEX, '').split(/\s*#{AUTHN_PAIR_DELIMITERS}\s*/)
+ _raw_params = auth.sub(TOKEN_REGEX, "").split(/\s*#{AUTHN_PAIR_DELIMITERS}\s*/)
if !(_raw_params.first =~ %r{\A#{TOKEN_KEY}})
_raw_params[0] = "#{TOKEN_KEY}#{_raw_params.first}"
@@ -501,8 +511,8 @@ module ActionController
# Returns nothing.
def authentication_request(controller, realm, message = nil)
message ||= "HTTP Token: Access denied.\n"
- controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.tr('"'.freeze, "".freeze)}")
- controller.__send__ :render, :text => message, :status => :unauthorized
+ controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.tr('"', "")}")
+ controller.__send__ :render, plain: message, status: :unauthorized
end
end
end
diff --git a/actionpack/lib/action_controller/metal/implicit_render.rb b/actionpack/lib/action_controller/metal/implicit_render.rb
index 17fcc2fa02..d3bb58f48b 100644
--- a/actionpack/lib/action_controller/metal/implicit_render.rb
+++ b/actionpack/lib/action_controller/metal/implicit_render.rb
@@ -1,36 +1,63 @@
+# frozen_string_literal: true
+
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 <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
+ # <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
+ # (see +render+ for details).
+ #
+ # Second, if we DON'T find a template but the controller action does have
+ # templates for other formats, variants, etc., then we trust that you meant
+ # to provide a template for this response, too, and we raise
+ # <tt>ActionController::UnknownFormat</tt> with an explanation.
+ #
+ # Third, if we DON'T find a template AND the request is a page load in a web
+ # browser (technically, a non-XHR GET request for an HTML response) where
+ # you reasonably expect to have rendered a template, then we raise
+ # <tt>ActionView::UnknownFormat</tt> with an explanation.
+ #
+ # Finally, if we DON'T find a template AND the request isn't a browser page
+ # load, then we implicitly respond with <tt>204 No Content</tt>.
module ImplicitRender
-
+ # :stopdoc:
include BasicImplicitRender
- # Renders the template corresponding to the controller action, if it exists.
- # The action name, format, and variant are all taken into account.
- # For example, the "new" action with an HTML format and variant "phone"
- # would try to render the <tt>new.html+phone.erb</tt> template.
- #
- # If no template is found <tt>ActionController::BasicImplicitRender</tt>'s implementation is called, unless
- # a block is passed. In that case, it will override the super implementation.
- #
- # default_render do
- # head 404 # No template was found
- # end
def default_render(*args)
if template_exists?(action_name.to_s, _prefixes, variants: request.variant)
render(*args)
+ elsif any_templates?(action_name.to_s, _prefixes)
+ message = "#{self.class.name}\##{action_name} is missing a template " \
+ "for this request format and variant.\n" \
+ "\nrequest.formats: #{request.formats.map(&:to_s).inspect}" \
+ "\nrequest.variant: #{request.variant.inspect}"
+
+ raise ActionController::UnknownFormat, message
+ elsif interactive_browser_request?
+ message = "#{self.class.name}\##{action_name} is missing a template for request formats: #{request.formats.map(&:to_s).join(',')}"
+ raise ActionController::MissingExactTemplate, message
else
- if block_given?
- yield(*args)
- else
- logger.info "No template found for #{self.class.name}\##{action_name}, rendering head :no_content" if logger
- super
- end
+ logger.info "No template found for #{self.class.name}\##{action_name}, rendering head :no_content" if logger
+ super
end
end
def method_for_action(action_name)
super || if template_exists?(action_name.to_s, _prefixes)
- "default_render"
- end
+ "default_render"
+ end
end
+
+ private
+ def interactive_browser_request?
+ request.get? && request.format == Mime[:html] && !request.xhr?
+ end
end
end
diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb
index a3e1a71b0a..be9449629f 100644
--- a/actionpack/lib/action_controller/metal/instrumentation.rb
+++ b/actionpack/lib/action_controller/metal/instrumentation.rb
@@ -1,9 +1,11 @@
-require 'benchmark'
-require 'abstract_controller/logger'
+# frozen_string_literal: true
+
+require "benchmark"
+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.
@@ -11,18 +13,18 @@ module ActionController
extend ActiveSupport::Concern
include AbstractController::Logger
- include ActionController::RackDelegation
attr_internal :view_runtime
def process_action(*args)
raw_payload = {
- :controller => self.class.name,
- :action => self.action_name,
- :params => request.filtered_parameters,
- :format => request.format.try(:ref),
- :method => request.request_method,
- :path => (request.fullpath rescue "unknown")
+ controller: self.class.name,
+ action: action_name,
+ params: request.filtered_parameters,
+ headers: request.headers,
+ format: request.format.ref,
+ method: request.request_method,
+ path: request.fullpath
}
ActiveSupport::Notifications.instrument("start_processing.action_controller", raw_payload.dup)
@@ -46,9 +48,9 @@ module ActionController
render_output
end
- def send_file(path, options={})
+ def send_file(path, options = {})
ActiveSupport::Notifications.instrument("send_file.action_controller",
- options.merge(:path => path)) do
+ options.merge(path: path)) do
super
end
end
@@ -72,25 +74,22 @@ module ActionController
# A hook invoked every time a before callback is halted.
def halted_callback_hook(filter)
- ActiveSupport::Notifications.instrument("halted_callback.action_controller", :filter => filter)
+ ActiveSupport::Notifications.instrument("halted_callback.action_controller", filter: filter)
end
- # A hook which allows you to clean up any time taken into account in
- # views wrongly, like database querying time.
+ # A hook which allows you to clean up any time, wrongly taken into account in
+ # views, like database querying time.
#
# def cleanup_view_runtime
# super - time_taken_in_something_expensive
# end
- #
- # :api: plugin
- def cleanup_view_runtime #:nodoc:
+ def cleanup_view_runtime # :doc:
yield
end
# Every time after an action is processed, this method is invoked
# with the payload, so you can add more information.
- # :api: plugin
- def append_info_to_payload(payload) #:nodoc:
+ def append_info_to_payload(payload) # :doc:
payload[:view_runtime] = view_runtime
end
@@ -98,7 +97,6 @@ module ActionController
# A hook which allows other frameworks to log what happened during
# controller process action. This method should return an array
# with the messages to be added.
- # :api: plugin
def log_process_action(payload) #:nodoc:
messages, view_runtime = [], payload[:view_runtime]
messages << ("Views: %.1fms" % view_runtime.to_f) if view_runtime
diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb
index 58150cd9a9..083b762f5a 100644
--- a/actionpack/lib/action_controller/metal/live.rb
+++ b/actionpack/lib/action_controller/metal/live.rb
@@ -1,9 +1,11 @@
-require 'action_dispatch/http/response'
-require 'delegate'
-require 'active_support/json'
+# frozen_string_literal: true
+
+require "action_dispatch/http/response"
+require "delegate"
+require "active_support/json"
module ActionController
- # Mix this module in to your controller, and all actions in that controller
+ # Mix this module into your controller, and all actions in that controller
# will be able to stream data to the client as it's written.
#
# class MyController < ActionController::Base
@@ -20,7 +22,7 @@ module ActionController
# end
# end
#
- # There are a few caveats with this use. You *cannot* write headers after the
+ # There are a few caveats with this module. You *cannot* write headers after the
# response has been committed (Response#committed? will return truthy).
# Calling +write+ or +close+ on the response stream will cause the response
# object to be committed. Make sure all headers are set before calling write
@@ -33,6 +35,20 @@ module ActionController
# the main thread. Make sure your actions are thread safe, and this shouldn't
# be a problem (don't share state across threads, etc).
module Live
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ def make_response!(request)
+ if request.get_header("HTTP_VERSION") == "HTTP/1.0"
+ super
+ else
+ Live::Response.new.tap do |res|
+ res.request = request
+ end
+ end
+ end
+ end
+
# This class provides the ability to write an SSE (Server Sent Event)
# to an IO stream. The class is initialized with a stream and can be used
# to either write a JSON string or an object which can be converted to JSON.
@@ -70,8 +86,7 @@ module ActionController
# Note: SSEs are not currently supported by IE. However, they are supported
# by Chrome, Firefox, Opera, and Safari.
class SSE
-
- WHITELISTED_OPTIONS = %w( retry event id )
+ PERMITTED_OPTIONS = %w( retry event id )
def initialize(stream, options = {})
@stream = stream
@@ -96,13 +111,13 @@ module ActionController
def perform_write(json, options)
current_options = @options.merge(options).stringify_keys
- WHITELISTED_OPTIONS.each do |option_name|
+ PERMITTED_OPTIONS.each do |option_name|
if (option_value = current_options[option_name])
@stream.write "#{option_name}: #{option_value}\n"
end
end
- message = json.gsub("\n".freeze, "\ndata: ".freeze)
+ message = json.gsub("\n", "\ndata: ")
@stream.write "data: #{message}\n\n"
end
end
@@ -131,8 +146,8 @@ module ActionController
def write(string)
unless @response.committed?
- @response.headers["Cache-Control"] = "no-cache"
- @response.headers.delete "Content-Length"
+ @response.set_header "Cache-Control", "no-cache"
+ @response.delete_header "Content-Length"
end
super
@@ -149,14 +164,6 @@ module ActionController
end
end
- def each
- @response.sending!
- while str = @buf.pop
- yield str
- end
- @response.sent!
- end
-
# Write a 'close' event to the buffer; the producer/writing thread
# uses this to notify us that it's finished supplying content.
#
@@ -196,60 +203,36 @@ module ActionController
def call_on_error
@error_callback.call
end
- end
- class Response < ActionDispatch::Response #:nodoc: all
- class Header < DelegateClass(Hash) # :nodoc:
- def initialize(response, header)
- @response = response
- super(header)
- end
+ private
- def []=(k,v)
- if @response.committed?
- raise ActionDispatch::IllegalStateError, 'header already sent'
+ def each_chunk(&block)
+ loop do
+ str = nil
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
+ str = @buf.pop
+ end
+ break unless str
+ yield str
end
-
- super
end
+ end
- def merge(other)
- self.class.new @response, __getobj__.merge(other)
- end
-
- def to_hash
- __getobj__.dup
- end
- end
-
+ class Response < ActionDispatch::Response #:nodoc: all
private
- def before_committed
- super
- jar = request.cookie_jar
- # The response can be committed multiple times
- jar.write self unless committed?
- end
-
- def before_sending
- super
- request.cookie_jar.commit!
- headers.freeze
- end
-
- def build_buffer(response, body)
- buf = Live::Buffer.new response
- body.each { |part| buf.write part }
- buf
- end
-
- def merge_default_headers(original, default)
- Header.new self, super
- end
+ def before_committed
+ super
+ jar = request.cookie_jar
+ # The response can be committed multiple times
+ jar.write self unless committed?
+ end
- def handle_conditional_get!
- super unless committed?
- end
+ def build_buffer(response, body)
+ buf = Live::Buffer.new response
+ body.each { |part| buf.write part }
+ buf
+ end
end
def process(name)
@@ -258,51 +241,43 @@ 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
- Thread.new {
- t2 = Thread.current
- t2.abort_on_exception = true
-
- # Since we're processing the view in a different thread, copy the
- # thread locals from the main thread to the child thread. :'(
- locals.each { |k,v| t2[k] = v }
-
- begin
- super(name)
- rescue => e
- if @_response.committed?
- begin
- @_response.stream.write(ActionView::Base.streaming_completion_on_exception) if request.format == :html
- @_response.stream.call_on_error
- rescue => exception
- log_error(exception)
- ensure
- log_error(e)
- @_response.stream.close
+ # 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
+
+ # Since we're processing the view in a different thread, copy the
+ # thread locals from the main thread to the child thread. :'(
+ locals.each { |k, v| t2[k] = v }
+
+ begin
+ super(name)
+ rescue => e
+ if @_response.committed?
+ begin
+ @_response.stream.write(ActionView::Base.streaming_completion_on_exception) if request.format == :html
+ @_response.stream.call_on_error
+ rescue => exception
+ log_error(exception)
+ ensure
+ log_error(e)
+ @_response.stream.close
+ end
+ else
+ error = e
end
- else
- error = e
+ ensure
+ @_response.commit!
end
- ensure
- @_response.commit!
end
}
- @_response.await_commit
- raise error if error
- end
-
- def log_error(exception)
- logger = ActionController::Base.logger
- return unless logger
-
- logger.fatal do
- message = "\n#{exception.class} (#{exception.message}):\n"
- message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
- message << " " << exception.backtrace.join("\n ")
- "#{message}\n\n"
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
+ @_response.await_commit
end
+
+ raise error if error
end
def response_body=(body)
@@ -310,13 +285,30 @@ module ActionController
response.close if response
end
- def set_response!(request)
- if request.env["HTTP_VERSION"] == "HTTP/1.0"
- super
- else
- @_response = Live::Response.new
- @_response.request = request
+ private
+
+ # 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
+ # this method except in Rails internals. Seriously!
+ def new_controller_thread # :nodoc:
+ Thread.new {
+ t2 = Thread.current
+ t2.abort_on_exception = true
+ yield
+ }
+ end
+
+ def log_error(exception)
+ logger = ActionController::Base.logger
+ return unless logger
+
+ logger.fatal do
+ message = +"\n#{exception.class} (#{exception.message}):\n"
+ message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
+ message << " " << exception.backtrace.join("\n ")
+ "#{message}\n\n"
+ end
end
- end
end
end
diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb
index fab1be3459..118da11990 100644
--- a/actionpack/lib/action_controller/metal/mime_responds.rb
+++ b/actionpack/lib/action_controller/metal/mime_responds.rb
@@ -1,4 +1,6 @@
-require 'abstract_controller/collector'
+# frozen_string_literal: true
+
+require "abstract_controller/collector"
module ActionController #:nodoc:
module MimeResponds
@@ -9,6 +11,13 @@ module ActionController #:nodoc:
# @people = Person.all
# end
#
+ # That action implicitly responds to all formats, but formats can also be explicitly enumerated:
+ #
+ # def index
+ # @people = Person.all
+ # respond_to :html, :js
+ # end
+ #
# Here's the same action, with web-service support baked in:
#
# def index
@@ -16,11 +25,12 @@ module ActionController #:nodoc:
#
# respond_to do |format|
# format.html
+ # format.js
# format.xml { render xml: @people }
# end
# end
#
- # What that says is, "if the client wants HTML in response to this action, just respond as we
+ # What that says is, "if the client wants HTML or JS in response to this action, just respond as we
# would have before, but if the client wants XML, return them the list of people in XML format."
# (Rails determines the desired response format from the HTTP Accept header submitted by the client.)
#
@@ -91,11 +101,11 @@ module ActionController #:nodoc:
# and accept Rails' defaults, life will be much easier.
#
# If you need to use a MIME type which isn't supported by default, you can register your own handlers in
- # config/initializers/mime_types.rb as follows.
+ # +config/initializers/mime_types.rb+ as follows.
#
# Mime::Type.register "image/jpg", :jpg
#
- # Respond to also allows you to specify a common block for different formats by using any:
+ # +respond_to+ also allows you to specify a common block for different formats by using +any+:
#
# def index
# @people = Person.all
@@ -151,21 +161,21 @@ module ActionController #:nodoc:
# format.html.none { render "trash" }
# end
#
- # Variants also support common `any`/`all` block that formats have.
+ # Variants also support common +any+/+all+ block that formats have.
#
# It works for both inline:
#
# respond_to do |format|
- # format.html.any { render text: "any" }
- # format.html.phone { render text: "phone" }
+ # format.html.any { render html: "any" }
+ # format.html.phone { render html: "phone" }
# end
#
# and block syntax:
#
# respond_to do |format|
# format.html do |variant|
- # variant.any(:tablet, :phablet){ render text: "any" }
- # variant.phone { render text: "phone" }
+ # variant.any(:tablet, :phablet){ render html: "any" }
+ # variant.phone { render html: "phone" }
# end
# end
#
@@ -173,16 +183,13 @@ 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
# format.html.phone # this gets rendered
# end
- #
- # Be sure to check the documentation of <tt>ActionController::MimeResponds.respond_to</tt>
- # for more examples.
def respond_to(*mimes)
raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given?
@@ -190,9 +197,13 @@ module ActionController #:nodoc:
yield collector if block_given?
if format = collector.negotiate_format(request)
+ if content_type && content_type != format
+ raise ActionController::RespondToMismatchError
+ end
_process_format(format)
+ _set_rendered_content_type format
response = collector.response
- response ? response.call : render({})
+ response.call if response
else
raise ActionController::UnknownFormat
end
@@ -228,7 +239,7 @@ module ActionController #:nodoc:
@responses = {}
@variant = variant
- mimes.each { |mime| @responses["Mime::#{mime.upcase}".constantize] = nil }
+ mimes.each { |mime| @responses[Mime[mime]] = nil }
end
def any(*args, &block)
@@ -274,8 +285,8 @@ module ActionController #:nodoc:
def any(*args, &block)
if block_given?
- if args.any? && args.none?{ |a| a == @variant }
- args.each{ |v| @variants[v] = block }
+ if args.any? && args.none? { |a| a == @variant }
+ args.each { |v| @variants[v] = block }
else
@variants[:any] = block
end
diff --git a/actionpack/lib/action_controller/metal/parameter_encoding.rb b/actionpack/lib/action_controller/metal/parameter_encoding.rb
new file mode 100644
index 0000000000..7a45732d31
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/parameter_encoding.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module ActionController
+ # Specify binary encoding for parameters for a given action.
+ module ParameterEncoding
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ def inherited(klass) # :nodoc:
+ super
+ klass.setup_param_encode
+ end
+
+ def setup_param_encode # :nodoc:
+ @_parameter_encodings = {}
+ end
+
+ def binary_params_for?(action) # :nodoc:
+ @_parameter_encodings[action.to_s]
+ end
+
+ # 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
+end
diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb
index cdfc523bd4..7361946de5 100644
--- a/actionpack/lib/action_controller/metal/params_wrapper.rb
+++ b/actionpack/lib/action_controller/metal/params_wrapper.rb
@@ -1,11 +1,13 @@
-require 'active_support/core_ext/hash/slice'
-require 'active_support/core_ext/hash/except'
-require 'active_support/core_ext/module/anonymous'
-require 'action_dispatch/http/mime_type'
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/slice"
+require "active_support/core_ext/hash/except"
+require "active_support/core_ext/module/anonymous"
+require "action_dispatch/http/mime_type"
module ActionController
- # Wraps the parameters hash into a nested hash. This will allow clients to submit
- # POST requests without having to specify any root elements.
+ # Wraps the parameters hash into a nested hash. This will allow clients to
+ # submit requests without having to specify any root elements.
#
# This functionality is enabled in +config/initializers/wrap_parameters.rb+
# and can be customized.
@@ -14,7 +16,7 @@ module ActionController
# a non-empty array:
#
# class UsersController < ApplicationController
- # wrap_parameters format: [:json, :xml]
+ # wrap_parameters format: [:json, :xml, :url_encoded_form, :multipart_form]
# end
#
# If you enable +ParamsWrapper+ for +:json+ format, instead of having to
@@ -71,7 +73,7 @@ module ActionController
EXCLUDE_PARAMETERS = %w(authenticity_token _method utf8)
- require 'mutex_m'
+ require "mutex_m"
class Options < Struct.new(:name, :format, :include, :exclude, :klass, :model) # :nodoc:
include Mutex_m
@@ -105,7 +107,19 @@ 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
+
+ if m.respond_to?(:nested_attributes_options) && m.nested_attributes_options.keys.any?
+ self.include += m.nested_attributes_options.keys.map do |key|
+ key.to_s.concat("_attributes")
+ end
+ end
+
+ self.include
end
end
end
@@ -128,35 +142,34 @@ module ActionController
end
private
- # Determine the wrapper model from the controller's name. By convention,
- # this could be done by trying to find the defined model that has the
- # same singular name as the controller. For example, +UsersController+
- # will try to find if the +User+ model exists.
- #
- # 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:
- return nil if klass.anonymous?
- model_name = klass.name.sub(/Controller$/, '').classify
-
- begin
- if model_klass = model_name.safe_constantize
- model_klass
- else
- namespaces = model_name.split("::")
- namespaces.delete_at(-2)
- break if namespaces.last == model_name
- model_name = namespaces.join("::")
- end
- end until model_klass
+ # Determine the wrapper model from the controller's name. By convention,
+ # this could be done by trying to find the defined model that has the
+ # same singular name as the controller. For example, +UsersController+
+ # will try to find if the +User+ model exists.
+ #
+ # 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
+ return nil if klass.anonymous?
+ model_name = klass.name.sub(/Controller$/, "").classify
+
+ begin
+ if model_klass = model_name.safe_constantize
+ model_klass
+ else
+ namespaces = model_name.split("::")
+ namespaces.delete_at(-2)
+ break if namespaces.last == model_name
+ model_name = namespaces.join("::")
+ end
+ end until model_klass
- model_klass
- end
+ model_klass
+ end
end
included do
- class_attribute :_wrapper_options
- self._wrapper_options = Options.from_hash(format: [])
+ class_attribute :_wrapper_options, default: Options.from_hash(format: [])
end
module ClassMethods
@@ -198,14 +211,14 @@ module ActionController
when Hash
options = name_or_model_or_options
when false
- options = options.merge(:format => [])
+ options = options.merge(format: [])
when Symbol, String
- options = options.merge(:name => name_or_model_or_options)
+ options = options.merge(name: name_or_model_or_options)
else
model = name_or_model_or_options
end
- opts = Options.from_hash _wrapper_options.to_h.slice(:format).merge(options)
+ opts = Options.from_hash _wrapper_options.to_h.slice(:format).merge(options)
opts.model = model
opts.klass = self
@@ -213,7 +226,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,27 +238,25 @@ 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?
- if request.parameters[_wrapper_key].present?
- wrapped_hash = _extract_parameters(request.parameters)
- else
- wrapped_hash = _wrap_parameters request.request_parameters
- end
-
+ wrapped_hash = _wrap_parameters request.request_parameters
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
+ ensure
+ # NOTE: Rescues all exceptions so they
+ # may be caught in ActionController::Rescue.
+ return super
end
private
@@ -276,8 +287,10 @@ module ActionController
# Checks if we should perform parameters wrapping.
def _wrapper_enabled?
- ref = request.content_mime_type.try(:ref)
- _wrapper_formats.include?(ref) && _wrapper_key && !request.request_parameters[_wrapper_key]
+ return false unless request.has_content_type?
+
+ ref = request.content_mime_type.ref
+ _wrapper_formats.include?(ref) && _wrapper_key && !request.parameters.key?(_wrapper_key)
end
end
end
diff --git a/actionpack/lib/action_controller/metal/rack_delegation.rb b/actionpack/lib/action_controller/metal/rack_delegation.rb
deleted file mode 100644
index ae9d89cc8c..0000000000
--- a/actionpack/lib/action_controller/metal/rack_delegation.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-require 'action_dispatch/http/request'
-require 'action_dispatch/http/response'
-
-module ActionController
- module RackDelegation
- extend ActiveSupport::Concern
-
- delegate :headers, :status=, :location=, :content_type=,
- :status, :location, :content_type, :response_code, :to => "@_response"
-
- module ClassMethods
- def build_with_env(env = {}) #:nodoc:
- new.tap { |c| c.set_request! ActionDispatch::Request.new(env) }
- end
- end
-
- def set_request!(request) #:nodoc:
- super
- set_response!(request)
- end
-
- def response_body=(body)
- response.body = body if response
- super
- end
-
- def reset_session
- @_request.reset_session
- end
-
- private
-
- def set_response!(request)
- @_response = ActionDispatch::Response.new
- @_response.request = request
- end
- end
-end
diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb
index acaa8227c9..2804a06a58 100644
--- a/actionpack/lib/action_controller/metal/redirecting.rb
+++ b/actionpack/lib/action_controller/metal/redirecting.rb
@@ -1,17 +1,10 @@
-module ActionController
- class RedirectBackError < AbstractController::Error #:nodoc:
- DEFAULT_MESSAGE = 'No HTTP_REFERER was set in the request to this action, so redirect_to :back could not be called successfully. If this is a test, make sure to specify request.env["HTTP_REFERER"].'
-
- def initialize(message = nil)
- super(message || DEFAULT_MESSAGE)
- end
- end
+# frozen_string_literal: true
+module ActionController
module Redirecting
extend ActiveSupport::Concern
include AbstractController::Logger
- include ActionController::RackDelegation
include ActionController::UrlFor
# Redirects the browser to the target specified in +options+. This parameter can be any one of:
@@ -21,34 +14,31 @@ module ActionController
# * <tt>String</tt> starting with <tt>protocol://</tt> (like <tt>http://</tt>) or a protocol relative reference (like <tt>//</tt>) - Is passed straight through as the target for redirection.
# * <tt>String</tt> not containing a protocol - The current protocol and host is prepended to the string.
# * <tt>Proc</tt> - A block that will be executed in the controller's context. Should return any option accepted by +redirect_to+.
- # * <tt>:back</tt> - Back to the page that issued the request. Useful for forms that are triggered from multiple places.
- # Short-hand for <tt>redirect_to(request.env["HTTP_REFERER"])</tt>
#
# === Examples:
#
# redirect_to action: "show", id: 5
- # redirect_to post
+ # redirect_to @post
# redirect_to "http://www.rubyonrails.org"
# redirect_to "/images/screenshot.jpg"
- # redirect_to articles_url
- # redirect_to :back
+ # 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
# redirect_to post_url(@post), status: 301
# redirect_to action: 'atom', status: 302
#
- # The status code can either be a standard {HTTP Status code}[http://www.iana.org/assignments/http-status-codes] as an
+ # The status code can either be a standard {HTTP Status code}[https://www.iana.org/assignments/http-status-codes] as an
# integer, or a symbol representing the downcased, underscored and symbolized description.
# Note that the status code must be a 3xx HTTP code, or redirection will not occur.
#
# 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
@@ -62,18 +52,45 @@ module ActionController
# redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id }
# redirect_to({ action: 'atom' }, alert: "Something serious happened")
#
- # When using <tt>redirect_to :back</tt>, if there is no referrer,
- # <tt>ActionController::RedirectBackError</tt> will be raised. You
- # may specify some fallback behavior for this case by rescuing
- # <tt>ActionController::RedirectBackError</tt>.
- 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 ActionControllerError.new("Cannot redirect to a parameter hash!") if options.is_a?(ActionController::Parameters)
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)
+ # if possible, otherwise redirects to the provided default fallback
+ # location.
+ #
+ # The referrer information is pulled from the HTTP +Referer+ (sic) header on
+ # the request. This is an optional header and its presence on the request is
+ # subject to browser security settings and user preferences. If the request
+ # is missing this header, the <tt>fallback_location</tt> will be used.
+ #
+ # redirect_back fallback_location: { action: "show", id: 5 }
+ # redirect_back fallback_location: @post
+ # redirect_back fallback_location: "http://www.rubyonrails.org"
+ # redirect_back fallback_location: "/images/screenshot.jpg"
+ # redirect_back fallback_location: posts_url
+ # redirect_back fallback_location: proc { edit_post_url(@post) }
+ # redirect_back fallback_location: '/', allow_other_host: false
+ #
+ # ==== Options
+ # * <tt>:fallback_location</tt> - The default fallback location that will be used on missing +Referer+ header.
+ # * <tt>:allow_other_host</tt> - Allow or disallow redirection to the host that is different to the current host, defaults to true.
+ #
+ # All other options that can be passed to <tt>redirect_to</tt> are accepted as
+ # options and the behavior is identical.
+ def redirect_back(fallback_location:, allow_other_host: true, **args)
+ referer = request.headers["Referer"]
+ redirect_to_referer = referer && (allow_other_host || _url_host_allowed?(referer))
+ redirect_to redirect_to_referer ? referer : fallback_location, **args
end
def _compute_redirect_to_location(request, options) #:nodoc:
@@ -81,16 +98,14 @@ module ActionController
# The scheme name consist of a letter followed by any combination of
# letters, digits, and the plus ("+"), period ("."), or hyphen ("-")
# characters; and is terminated by a colon (":").
- # See http://tools.ietf.org/html/rfc3986#section-3.1
+ # See https://tools.ietf.org/html/rfc3986#section-3.1
# The protocol relative scheme starts with a double slash "//".
when /\A([a-z][a-z\d\-+\.]*:|\/\/).*/i
options
when String
request.protocol + request.host_with_port + options
- when :back
- request.headers["Referer"] or raise RedirectBackError
when Proc
- _compute_redirect_to_location request, options.call
+ _compute_redirect_to_location request, instance_eval(&options)
else
url_for(options)
end.delete("\0\r\n")
@@ -108,5 +123,11 @@ module ActionController
302
end
end
+
+ def _url_host_allowed?(url)
+ URI(url.to_s).host == request.host
+ rescue ArgumentError, URI::Error
+ false
+ end
end
end
diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb
index cb74c4f0d4..b81d3ef539 100644
--- a/actionpack/lib/action_controller/metal/renderers.rb
+++ b/actionpack/lib/action_controller/metal/renderers.rb
@@ -1,4 +1,6 @@
-require 'set'
+# frozen_string_literal: true
+
+require "set"
module ActionController
# See <tt>Renderers.add</tt>
@@ -11,6 +13,7 @@ module ActionController
Renderers.remove(key)
end
+ # See <tt>Responder#api_behavior</tt>
class MissingRenderer < LoadError
def initialize(format)
super "No renderer defined for format: #{format}"
@@ -20,40 +23,24 @@ module ActionController
module Renderers
extend ActiveSupport::Concern
- included do
- class_attribute :_renderers
- self._renderers = Set.new.freeze
- end
+ # A Set containing renderer names that correspond to available renderer procs.
+ # Default values are <tt>:json</tt>, <tt>:js</tt>, <tt>:xml</tt>.
+ RENDERERS = Set.new
- module ClassMethods
- def use_renderers(*args)
- renderers = _renderers + args
- self._renderers = renderers.freeze
- end
- alias use_renderer use_renderers
+ included do
+ class_attribute :_renderers, default: Set.new.freeze
end
- def render_to_body(options)
- _render_to_body_with_renderer(options) || super
- end
+ # Used in <tt>ActionController::Base</tt>
+ # and <tt>ActionController::API</tt> to include all
+ # renderers by default.
+ module All
+ extend ActiveSupport::Concern
+ include Renderers
- def _render_to_body_with_renderer(options)
- _renderers.each do |name|
- if options.key?(name)
- _process_options(options)
- method_name = Renderers._render_with_renderer_method_name(name)
- return send(method_name, options.delete(name), options)
- end
+ included do
+ self._renderers = RENDERERS
end
- nil
- end
-
- # A Set containing renderer names that correspond to available renderer procs.
- # Default values are <tt>:json</tt>, <tt>:js</tt>, <tt>:xml</tt>.
- RENDERERS = Set.new
-
- def self._render_with_renderer_method_name(key)
- "_render_with_renderer_#{key}"
end
# Adds a new renderer to call within controller actions.
@@ -68,11 +55,11 @@ module ActionController
# ActionController::Renderers.add :csv do |obj, options|
# filename = options[:filename] || 'data'
# str = obj.respond_to?(:to_csv) ? obj.to_csv : obj.to_s
- # send_data str, type: Mime::CSV,
+ # send_data str, type: Mime[:csv],
# disposition: "attachment; filename=#{filename}.csv"
# end
#
- # Note that we used Mime::CSV for the csv mime type as it comes with Rails.
+ # Note that we used Mime[:csv] for the csv mime type as it comes with Rails.
# For a custom renderer, you'll need to register a mime type with
# <tt>Mime::Type.register</tt>.
#
@@ -85,8 +72,6 @@ module ActionController
# format.csv { render csv: @csvable, filename: @csvable.name }
# end
# end
- # To use renderers and their mime types in more concise ways, see
- # <tt>ActionController::MimeResponds::ClassMethods.respond_to</tt>
def self.add(key, &block)
define_method(_render_with_renderer_method_name(key), &block)
RENDERERS << key.to_sym
@@ -100,40 +85,96 @@ module ActionController
def self.remove(key)
RENDERERS.delete(key.to_sym)
method_name = _render_with_renderer_method_name(key)
- remove_method(method_name) if method_defined?(method_name)
+ remove_possible_method(method_name)
end
- module All
- extend ActiveSupport::Concern
- include Renderers
+ def self._render_with_renderer_method_name(key)
+ "_render_with_renderer_#{key}"
+ end
- included do
- self._renderers = RENDERERS
+ module ClassMethods
+ # Adds, by name, a renderer or renderers to the +_renderers+ available
+ # to call within controller actions.
+ #
+ # It is useful when rendering from an <tt>ActionController::Metal</tt> controller or
+ # otherwise to add an available renderer proc to a specific controller.
+ #
+ # Both <tt>ActionController::Base</tt> and <tt>ActionController::API</tt>
+ # include <tt>ActionController::Renderers::All</tt>, making all renderers
+ # available in the controller. See <tt>Renderers::RENDERERS</tt> and <tt>Renderers.add</tt>.
+ #
+ # 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 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
+ # +use_renderers+. For example, a controller that includes only the <tt>:json</tt> renderer
+ # (+_render_with_renderer_json+) might look like:
+ #
+ # class MetalRenderingController < ActionController::Metal
+ # include AbstractController::Rendering
+ # include ActionController::Rendering
+ # include ActionController::Renderers
+ #
+ # use_renderers :json
+ #
+ # def show
+ # render json: record
+ # end
+ # end
+ #
+ # You must specify a +use_renderer+, else the +controller.renderer+ and
+ # +controller._renderers+ will be <tt>nil</tt>, and the action will fail.
+ def use_renderers(*args)
+ renderers = _renderers + args
+ self._renderers = renderers.freeze
end
+ alias use_renderer use_renderers
+ end
+
+ # Called by +render+ in <tt>AbstractController::Rendering</tt>
+ # which sets the return value as the +response_body+.
+ #
+ # If no renderer is found, +super+ returns control to
+ # <tt>ActionView::Rendering.render_to_body</tt>, if present.
+ def render_to_body(options)
+ _render_to_body_with_renderer(options) || super
+ end
+
+ def _render_to_body_with_renderer(options)
+ _renderers.each do |name|
+ if options.key?(name)
+ _process_options(options)
+ method_name = Renderers._render_with_renderer_method_name(name)
+ return send(method_name, options.delete(name), options)
+ end
+ end
+ nil
end
add :json do |json, options|
json = json.to_json(options) unless json.kind_of?(String)
if options[:callback].present?
- if content_type.nil? || content_type == Mime::JSON
- self.content_type = Mime::JS
+ if content_type.nil? || content_type == Mime[:json]
+ self.content_type = Mime[:js]
end
"/**/#{options[:callback]}(#{json})"
else
- self.content_type ||= Mime::JSON
+ self.content_type ||= Mime[:json]
json
end
end
add :js do |js, options|
- self.content_type ||= Mime::JS
+ self.content_type ||= Mime[:js]
js.respond_to?(:to_js) ? js.to_js(options) : js
end
add :xml do |xml, options|
- self.content_type ||= Mime::XML
+ self.content_type ||= Mime[:xml]
xml.respond_to?(:to_xml) ? xml.to_xml(options) : xml
end
end
diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb
index b9ae8dd5ea..7d0a944381 100644
--- a/actionpack/lib/action_controller/metal/rendering.rb
+++ b/actionpack/lib/action_controller/metal/rendering.rb
@@ -1,17 +1,26 @@
+# frozen_string_literal: true
+
module ActionController
module Rendering
extend ActiveSupport::Concern
- RENDER_FORMATS_IN_PRIORITY = [:body, :text, :plain, :html]
+ RENDER_FORMATS_IN_PRIORITY = [:body, :plain, :html]
module ClassMethods
# Documentation at ActionController::Renderer#render
delegate :render, to: :renderer
- # Returns a renderer class (inherited from ActionController::Renderer)
+ # Returns a renderer instance (inherited from ActionController::Renderer)
# for the controller.
- def renderer
- @renderer ||= Renderer.for(self)
+ attr_reader :renderer
+
+ def setup_renderer! # :nodoc:
+ @renderer = Renderer.for(self)
+ end
+
+ def inherited(klass)
+ klass.setup_renderer!
+ super
end
end
@@ -23,15 +32,15 @@ module ActionController
# Check for double render errors and set the content_type after rendering.
def render(*args) #:nodoc:
- raise ::AbstractController::DoubleRenderError if self.response_body
+ raise ::AbstractController::DoubleRenderError if response_body
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)
- string = ""
+ string = +""
result.each { |r| string << r }
string
else
@@ -40,73 +49,74 @@ module ActionController
end
def render_to_body(options = {})
- super || _render_in_priorities(options) || ' '
+ super || _render_in_priorities(options) || " "
end
private
- def _render_in_priorities(options)
- RENDER_FORMATS_IN_PRIORITY.each do |format|
- return options[format] if options.key?(format)
+ def _process_variant(options)
+ if defined?(request) && !request.nil? && request.variant.present?
+ options[:variant] = request.variant
+ end
end
- nil
- end
-
- def _process_format(format, options = {})
- super
+ def _render_in_priorities(options)
+ RENDER_FORMATS_IN_PRIORITY.each do |format|
+ return options[format] if options.key?(format)
+ end
- if options[:plain]
- self.content_type = Mime::TEXT
- else
- self.content_type ||= format.to_s
+ nil
end
- end
-
- # Normalize arguments by catching blocks and setting them on :update.
- def _normalize_args(action=nil, options={}, &blk) #:nodoc:
- options = super
- options[:update] = blk if block_given?
- options
- end
-
- # Normalize both text and status options.
- def _normalize_options(options) #:nodoc:
- _normalize_text(options)
- if options[:html]
- options[:html] = ERB::Util.html_escape(options[:html])
+ def _set_html_content_type
+ self.content_type = Mime[:html].to_s
end
- if options.delete(:nothing)
- ActiveSupport::Deprecation.warn("`:nothing` option is deprecated and will be removed in Rails 5.1. Use `head` method to respond with empty response body.")
- options[:body] = nil
+ def _set_rendered_content_type(format)
+ if format && !response.content_type
+ self.content_type = format.to_s
+ end
end
- if options[:status]
- options[:status] = Rack::Utils.status_code(options[:status])
+ # Normalize arguments by catching blocks and setting them on :update.
+ def _normalize_args(action = nil, options = {}, &blk)
+ options = super
+ options[:update] = blk if block_given?
+ options
end
- super
- end
+ # Normalize both text and status options.
+ def _normalize_options(options)
+ _normalize_text(options)
- def _normalize_text(options)
- RENDER_FORMATS_IN_PRIORITY.each do |format|
- if options.key?(format) && options[format].respond_to?(:to_text)
- options[format] = options[format].to_text
+ if options[:html]
+ options[:html] = ERB::Util.html_escape(options[:html])
end
+
+ if options[:status]
+ options[:status] = Rack::Utils.status_code(options[:status])
+ end
+
+ super
end
- end
- # Process controller specific options, as status, content-type and location.
- def _process_options(options) #:nodoc:
- status, content_type, location = options.values_at(:status, :content_type, :location)
+ def _normalize_text(options)
+ RENDER_FORMATS_IN_PRIORITY.each do |format|
+ if options.key?(format) && options[format].respond_to?(:to_text)
+ options[format] = options[format].to_text
+ end
+ end
+ end
- self.status = status if status
- self.content_type = content_type if content_type
- self.headers["Location"] = url_for(location) if location
+ # Process controller specific options, as status, content-type and location.
+ def _process_options(options)
+ status, content_type, location = options.values_at(:status, :content_type, :location)
- super
- end
+ self.status = status if status
+ self.content_type = content_type if content_type
+ headers["Location"] = url_for(location) if location
+
+ super
+ end
end
end
diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
index 4cb634477e..cb109c6ad8 100644
--- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb
+++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
@@ -1,6 +1,8 @@
-require 'rack/session/abstract/id'
-require 'action_controller/metal/exceptions'
-require 'active_support/security_utils'
+# frozen_string_literal: true
+
+require "rack/session/abstract/id"
+require "action_controller/metal/exceptions"
+require "active_support/security_utils"
module ActionController #:nodoc:
class InvalidAuthenticityToken < ActionControllerError #:nodoc:
@@ -15,12 +17,12 @@ module ActionController #:nodoc:
# access. When a request reaches your application, \Rails verifies the received
# token with the token in the session. All requests are checked except GET requests
# as these should be idempotent. Keep in mind that all session-oriented requests
- # should be CSRF protected, including JavaScript and HTML requests.
+ # are CSRF protected by default, including JavaScript and HTML requests.
#
# Since HTML and JavaScript requests are typically made from the browser, we
# need to ensure to verify request authenticity for the web browser. We can
# use session-oriented authentication for these types of requests, by using
- # the `protect_form_forgery` method in our controllers.
+ # the <tt>protect_from_forgery</tt> method in our controllers.
#
# GET requests are not protected since they don't have side effects like writing
# to the database and don't leak sensitive information. JavaScript requests are
@@ -28,16 +30,23 @@ module ActionController #:nodoc:
# URL on your site. When your JavaScript response loads on their site, it executes.
# With carefully crafted JavaScript on their end, sensitive data in your JavaScript
# response may be extracted. To prevent this, only XmlHttpRequest (known as XHR or
- # Ajax) requests are allowed to make GET requests for JavaScript responses.
+ # Ajax) requests are allowed to make requests for JavaScript responses.
#
- # It's important to remember that XML or JSON requests are also affected and if
- # you're building an API you should change forgery protection method in
+ # It's important to remember that XML or JSON requests are also checked by default. If
+ # you're building an API or an SPA you could change forgery protection method in
# <tt>ApplicationController</tt> (by default: <tt>:exception</tt>):
#
# class ApplicationController < ActionController::Base
# protect_from_forgery unless: -> { request.format.json? }
# end
#
+ # It is generally safe to exclude XHR requests from CSRF protection
+ # (like the code snippet above does), because XHR requests can only be made from
+ # the same origin. Note however that any cross-origin third party domain
+ # allowed via {CORS}[https://en.wikipedia.org/wiki/Cross-origin_resource_sharing]
+ # will also be able to create XHR requests. Be sure to check your
+ # CORS configuration before disabling forgery protection for XHR.
+ #
# CSRF protection is turned on with the <tt>protect_from_forgery</tt> method.
# By default <tt>protect_from_forgery</tt> protects your session with
# <tt>:null_session</tt> method, which provides an empty session
@@ -52,7 +61,7 @@ module ActionController #:nodoc:
# <tt>csrf_meta_tags</tt> in the HTML +head+.
#
# Learn more about CSRF attacks and securing your application in the
- # {Ruby on Rails Security Guide}[http://guides.rubyonrails.org/security.html].
+ # {Ruby on Rails Security Guide}[https://guides.rubyonrails.org/security.html].
module RequestForgeryProtection
extend ActiveSupport::Concern
@@ -77,6 +86,18 @@ module ActionController #:nodoc:
config_accessor :log_warning_on_csrf_failure
self.log_warning_on_csrf_failure = true
+ # Controls whether the Origin header is checked in addition to the CSRF token.
+ config_accessor :forgery_protection_origin_check
+ self.forgery_protection_origin_check = false
+
+ # Controls whether form-action/method specific CSRF tokens are used.
+ config_accessor :per_form_csrf_tokens
+ self.per_form_csrf_tokens = false
+
+ # Controls whether forgery protection is enabled by default.
+ config_accessor :default_protect_from_forgery
+ self.default_protect_from_forgery = false
+
helper_method :form_authenticity_token
helper_method :protect_against_forgery?
end
@@ -90,19 +111,21 @@ module ActionController #:nodoc:
#
# class FooController < ApplicationController
# protect_from_forgery except: :index
+ # end
#
# You can disable forgery protection on controller by skipping the verification before_action:
+ #
# skip_before_action :verify_authenticity_token
#
# Valid Options:
#
- # * <tt>:only/:except</tt> - Only apply forgery protection to a subset of actions. Like <tt>only: [ :create, :create_all ]</tt>.
+ # * <tt>:only/:except</tt> - Only apply forgery protection to a subset of actions. For example <tt>only: [ :create, :create_all ]</tt>.
# * <tt>:if/:unless</tt> - Turn off the forgery protection entirely depending on the passed Proc or method reference.
- # * <tt>:prepend</tt> - By default, the verification of the authentication token is added to the front of the
- # callback chain. If you need to make the verification depend on other callbacks, like authentication methods
- # (say cookies vs OAuth), this might not work for you. Pass <tt>prepend: false</tt> to just add the
- # verification callback in the position of the protect_from_forgery call. This means any callbacks added
- # before are run first.
+ # * <tt>:prepend</tt> - By default, the verification of the authentication token will be added at the position of the
+ # protect_from_forgery call in your application. This means any callbacks added before are run first. This is useful
+ # when you want your forgery protection to depend on other callbacks, like authentication methods (Oauth vs Cookie auth).
+ #
+ # If you need to add verification to the beginning of the callback chain, use <tt>prepend: true</tt>.
# * <tt>:with</tt> - Set the method to handle unverified request.
#
# Valid unverified request handling methods are:
@@ -110,7 +133,7 @@ module ActionController #:nodoc:
# * <tt>:reset_session</tt> - Resets the session.
# * <tt>:null_session</tt> - Provides an empty session during request but doesn't reset it completely. Used as default if <tt>:with</tt> option is not specified.
def protect_from_forgery(options = {})
- options = options.reverse_merge(prepend: true)
+ options = options.reverse_merge(prepend: false)
self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session)
self.request_forgery_protection_token ||= :authenticity_token
@@ -118,13 +141,22 @@ module ActionController #:nodoc:
append_after_action :verify_same_origin_request
end
+ # Turn off request forgery protection. This is a wrapper for:
+ #
+ # skip_before_action :verify_authenticity_token
+ #
+ # See +skip_before_action+ for allowed options.
+ def skip_forgery_protection(options = {})
+ skip_before_action :verify_authenticity_token, options
+ end
+
private
- def protection_method_class(name)
- ActionController::RequestForgeryProtection::ProtectionMethods.const_get(name.to_s.classify)
- rescue NameError
- raise ArgumentError, 'Invalid request forgery protection method, use :null_session, :exception, or :reset_session'
- end
+ def protection_method_class(name)
+ ActionController::RequestForgeryProtection::ProtectionMethods.const_get(name.to_s.classify)
+ rescue NameError
+ raise ArgumentError, "Invalid request forgery protection method, use :null_session, :exception, or :reset_session"
+ end
end
module ProtectionMethods
@@ -136,42 +168,34 @@ module ActionController #:nodoc:
# This is the method that defines the application behavior when a request is found to be unverified.
def handle_unverified_request
request = @controller.request
- request.session = NullSessionHash.new(request.env)
- request.env['action_dispatch.request.flash_hash'] = nil
- request.env['rack.session.options'] = { skip: true }
- request.env['action_dispatch.cookies'] = NullCookieJar.build(request)
+ request.session = NullSessionHash.new(request)
+ request.flash = nil
+ request.session_options = { skip: true }
+ request.cookie_jar = NullCookieJar.build(request, {})
end
- protected
-
- class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc:
- def initialize(env)
- super(nil, env)
- @data = {}
- @loaded = true
- end
-
- # no-op
- def destroy; end
+ private
- def exists?
- true
- end
- end
+ class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc:
+ def initialize(req)
+ super(nil, req)
+ @data = {}
+ @loaded = true
+ end
- class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc:
- def self.build(request)
- key_generator = request.env[ActionDispatch::Cookies::GENERATOR_KEY]
- host = request.host
- secure = request.ssl?
+ # no-op
+ def destroy; end
- new(key_generator, host, secure, options_for_env({}))
+ def exists?
+ true
+ end
end
- def write(*)
- # nothing
+ class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc:
+ def write(*)
+ # nothing
+ end
end
- end
end
class ResetSession
@@ -195,29 +219,33 @@ 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
- # `<script>` verification.
+ # <tt><script></tt> verification.
#
# Lean on the protect_from_forgery declaration to mark which actions are
# due for same-origin request verification. If protect_from_forgery is
# 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?
if logger && log_warning_on_csrf_failure
- logger.warn "Can't verify CSRF token authenticity"
+ if valid_request_origin?
+ logger.warn "Can't verify CSRF token authenticity."
+ else
+ logger.warn "HTTP Origin header (#{request.origin}) didn't match request.base_url (#{request.base_url})"
+ end
end
handle_unverified_request
end
end
- def handle_unverified_request
+ def handle_unverified_request # :doc:
forgery_protection_strategy.new(self).handle_unverified_request
end
@@ -227,57 +255,80 @@ module ActionController #:nodoc:
"If you know what you're doing, go ahead and disable forgery " \
"protection on this action to permit cross-origin JavaScript embedding."
private_constant :CROSS_ORIGIN_JAVASCRIPT_WARNING
+ # :startdoc:
- # If `verify_authenticity_token` was run (indicating that we have
+ # If +verify_authenticity_token+ was run (indicating that we have
# forgery protection enabled for this request) then also verify that
# we aren't serving an unauthorized cross-origin response.
- def verify_same_origin_request
+ def verify_same_origin_request # :doc:
if marked_for_same_origin_verification? && non_xhr_javascript_response?
- logger.warn CROSS_ORIGIN_JAVASCRIPT_WARNING if logger
+ if logger && log_warning_on_csrf_failure
+ logger.warn CROSS_ORIGIN_JAVASCRIPT_WARNING
+ end
raise ActionController::InvalidCrossOriginRequest, CROSS_ORIGIN_JAVASCRIPT_WARNING
end
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
+ # 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?
- content_type =~ %r(\Atext/javascript) && !request.xhr?
+ def non_xhr_javascript_response? # :doc:
+ content_type =~ %r(\A(?:text|application)/javascript) && !request.xhr?
end
AUTHENTICITY_TOKEN_LENGTH = 32
# 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_authenticity_token?(session, form_authenticity_param) ||
- valid_authenticity_token?(session, request.headers['X-CSRF-Token'])
+ (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? # :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 # :doc:
+ [form_authenticity_param, request.x_csrf_token]
end
# Sets the token value for the current session.
- def form_authenticity_token
- masked_authenticity_token(session)
+ def form_authenticity_token(form_options: {})
+ masked_authenticity_token(session, form_options: form_options)
end
# 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)
+ 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
+ action_path = normalize_action_path(action)
+ per_form_csrf_token(session, action_path, method)
+ else
+ real_csrf_token(session)
+ end
+
one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH)
- encrypted_csrf_token = xor_byte_strings(one_time_pad, real_csrf_token(session))
+ encrypted_csrf_token = xor_byte_strings(one_time_pad, raw_token)
masked_token = one_time_pad + encrypted_csrf_token
Base64.strict_encode64(masked_token)
end
@@ -285,7 +336,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
@@ -303,44 +354,103 @@ 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
- # Split the token into the one-time pad and the encrypted
- # value and decrypt it
- one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH]
- encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1]
- csrf_token = xor_byte_strings(one_time_pad, encrypted_csrf_token)
-
- compare_with_real_token csrf_token, session
+ csrf_token = unmask_token(masked_token)
+ 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 compare_with_real_token(token, session)
- ActiveSupport::SecurityUtils.secure_compare(token, real_csrf_token(session))
+ def unmask_token(masked_token) # :doc:
+ # Split the token into the one-time pad and the encrypted
+ # 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) # :doc:
+ ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, real_csrf_token(session))
+ end
+
+ def valid_per_form_csrf_token?(token, session) # :doc:
+ if per_form_csrf_tokens
+ correct_token = per_form_csrf_token(
+ session,
+ normalize_action_path(request.fullpath),
+ request.request_method
+ )
+
+ ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, correct_token)
+ else
+ false
+ 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 xor_byte_strings(s1, s2)
- s1.bytes.zip(s2.bytes).map { |(c1,c2)| c1 ^ c2 }.pack('c*')
+ def per_form_csrf_token(session, action_path, method) # :doc:
+ OpenSSL::HMAC.digest(
+ OpenSSL::Digest::SHA256.new,
+ real_csrf_token(session),
+ [action_path, method.downcase].join("#")
+ )
+ end
+
+ def xor_byte_strings(s1, s2) # :doc:
+ s2 = s2.dup
+ size = s1.bytesize
+ i = 0
+ while i < size
+ s2.setbyte(i, s1.getbyte(i) ^ s2.getbyte(i))
+ i += 1
+ end
+ s2
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
+
+ NULL_ORIGIN_MESSAGE = <<~MSG
+ The browser returned a 'null' origin for a request with origin-based forgery protection turned on. This usually
+ means you have the 'no-referrer' Referrer-Policy header enabled, or that the request came from a site that
+ refused to give its origin. This makes it impossible for Rails to verify the source of the requests. Likely the
+ best solution is to change your referrer policy to something less strict like same-origin or strict-same-origin.
+ If you cannot change the referrer policy, you can disable origin checking with the
+ Rails.application.config.action_controller.forgery_protection_origin_check setting.
+ MSG
+
+ # Checks if the request originated from the same origin by looking at the
+ # Origin header.
+ def valid_request_origin? # :doc:
+ if forgery_protection_origin_check
+ # We accept blank origin headers because some user agents don't send it.
+ raise InvalidAuthenticityToken, NULL_ORIGIN_MESSAGE if request.origin == "null"
+ request.origin.nil? || request.origin == request.base_url
+ else
+ true
+ end
+ end
+
+ def normalize_action_path(action_path) # :doc:
+ uri = URI.parse(action_path)
+ uri.path.chomp("/")
+ end
end
end
diff --git a/actionpack/lib/action_controller/metal/rescue.rb b/actionpack/lib/action_controller/metal/rescue.rb
index 68cc9a9c9b..44f7fb7a07 100644
--- a/actionpack/lib/action_controller/metal/rescue.rb
+++ b/actionpack/lib/action_controller/metal/rescue.rb
@@ -1,25 +1,18 @@
+# frozen_string_literal: true
+
module ActionController #:nodoc:
- # This module is responsible to provide `rescue_from` helpers
- # to controllers and configure when detailed exceptions must be
+ # This module is responsible for providing +rescue_from+ helpers
+ # to controllers and configuring when detailed exceptions must be
# shown.
module Rescue
extend ActiveSupport::Concern
include ActiveSupport::Rescuable
- def rescue_with_handler(exception)
- if (exception.respond_to?(:original_exception) &&
- (orig_exception = exception.original_exception) &&
- handler_for_rescue(orig_exception))
- exception = orig_exception
- end
- super(exception)
- end
-
# Override this method if you want to customize when detailed
# 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.
+ # +consider_all_requests_local+ is +false+. By default, it returns
+ # +false+, but someone may set it to <tt>request.local?</tt> so local
+ # requests in production still show the detailed exception pages.
def show_detailed_exceptions?
false
end
@@ -28,8 +21,8 @@ module ActionController #:nodoc:
def process_action(*args)
super
rescue Exception => exception
- request.env['action_dispatch.show_detailed_exceptions'] ||= show_detailed_exceptions?
- rescue_with_handler(exception) || raise(exception)
+ request.env["action_dispatch.show_detailed_exceptions"] ||= show_detailed_exceptions?
+ rescue_with_handler(exception) || raise
end
end
end
diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb
index af31de1f3a..8dc01a5eb9 100644
--- a/actionpack/lib/action_controller/metal/streaming.rb
+++ b/actionpack/lib/action_controller/metal/streaming.rb
@@ -1,9 +1,11 @@
-require 'rack/chunked'
+# frozen_string_literal: true
+
+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.
#
@@ -181,7 +183,7 @@ module ActionController #:nodoc:
# unicorn_rails --config-file unicorn.config.rb
#
# You may also want to configure other parameters like <tt>:tcp_nodelay</tt>.
- # Please check its documentation for more information: http://unicorn.bogomips.org/Unicorn/Configurator.html#method-i-listen
+ # Please check its documentation for more information: https://bogomips.org/unicorn/Unicorn/Configurator.html#method-i-listen
#
# If you are using Unicorn with NGINX, you may need to tweak NGINX.
# Streaming should work out of the box on Rainbows.
@@ -193,13 +195,13 @@ 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 env["HTTP_VERSION"] == "HTTP/1.0"
+ if request.version == "HTTP/1.0"
options.delete(:stream)
else
headers["Cache-Control"] ||= "no-cache"
@@ -210,7 +212,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 09867e2407..04922b0715 100644
--- a/actionpack/lib/action_controller/metal/strong_parameters.rb
+++ b/actionpack/lib/action_controller/metal/strong_parameters.rb
@@ -1,10 +1,15 @@
-require 'active_support/core_ext/hash/indifferent_access'
-require 'active_support/core_ext/array/wrap'
-require 'active_support/core_ext/string/filters'
-require 'active_support/rescuable'
-require 'action_dispatch/http/upload'
-require 'stringio'
-require 'set'
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/indifferent_access"
+require "active_support/core_ext/array/wrap"
+require "active_support/core_ext/string/filters"
+require "active_support/core_ext/object/to_query"
+require "active_support/rescuable"
+require "action_dispatch/http/upload"
+require "rack/test"
+require "stringio"
+require "set"
+require "yaml"
module ActionController
# Raised when a required parameter is missing.
@@ -29,19 +34,31 @@ module ActionController
#
# params = ActionController::Parameters.new(a: "123", b: "456")
# params.permit(:c)
- # # => ActionController::UnpermittedParameters: found unpermitted parameters: a, b
+ # # => ActionController::UnpermittedParameters: found unpermitted parameters: :a, :b
class UnpermittedParameters < IndexError
attr_reader :params # :nodoc:
def initialize(params) # :nodoc:
@params = params
- super("found unpermitted parameter#{'s' if params.size > 1 }: #{params.join(", ")}")
+ super("found unpermitted parameter#{'s' if params.size > 1 }: #{params.map { |e| ":#{e}" }.join(", ")}")
+ end
+ end
+
+ # Raised when a Parameters instance is not marked as permitted and
+ # an operation to transform it to hash is called.
+ #
+ # params = ActionController::Parameters.new(a: "123", b: "456")
+ # params.to_h
+ # # => ActionController::UnfilteredParameters: unable to convert unpermitted parameters to hash
+ class UnfilteredParameters < ArgumentError
+ def initialize # :nodoc:
+ super("unable to convert unpermitted parameters to hash")
end
end
# == Action Controller \Parameters
#
- # Allows to choose which attributes should be whitelisted for mass updating
+ # Allows you to choose which attributes should be permitted for mass updating
# and thus prevent accidentally exposing that which shouldn't be exposed.
# Provides two methods for this purpose: #require and #permit. The former is
# used to mark parameters as required. The latter is used to set the parameter
@@ -49,15 +66,14 @@ module ActionController
#
# params = ActionController::Parameters.new({
# person: {
- # name: 'Francesco',
+ # name: "Francesco",
# age: 22,
- # role: 'admin'
+ # role: "admin"
# }
# })
#
# permitted = params.require(:person).permit(:name, :age)
- # permitted # => {"name"=>"Francesco", "age"=>22}
- # permitted.class # => ActionController::Parameters
+ # permitted # => <ActionController::Parameters {"name"=>"Francesco", "age"=>22} permitted: true>
# permitted.permitted? # => true
#
# Person.first.update!(permitted)
@@ -68,8 +84,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.
#
@@ -85,7 +101,7 @@ module ActionController
#
# params = ActionController::Parameters.new(a: "123", b: "456")
# params.permit(:c)
- # # => {}
+ # # => <ActionController::Parameters {} permitted: true>
#
# ActionController::Parameters.action_on_unpermitted_parameters = :raise
#
@@ -97,17 +113,108 @@ module ActionController
# environment they should only be set once at boot-time and never mutated at
# runtime.
#
- # <tt>ActionController::Parameters</tt> inherits from
- # <tt>ActiveSupport::HashWithIndifferentAccess</tt>, this means
- # that you can fetch values using either <tt>:key</tt> or <tt>"key"</tt>.
+ # You can fetch values of <tt>ActionController::Parameters</tt> using either
+ # <tt>:key</tt> or <tt>"key"</tt>.
#
- # params = ActionController::Parameters.new(key: 'value')
+ # params = ActionController::Parameters.new(key: "value")
# params[:key] # => "value"
# params["key"] # => "value"
- class Parameters < ActiveSupport::HashWithIndifferentAccess
- cattr_accessor :permit_all_parameters, instance_accessor: false
+ class Parameters
+ cattr_accessor :permit_all_parameters, instance_accessor: false, default: false
+
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: each_key
+ #
+ # :call-seq:
+ # each_key()
+ #
+ # Calls block once for each key in the parameters, passing the key.
+ # If no block is given, an enumerator is returned instead.
+
+ ##
+ # :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: to_s
+ #
+ # :call-seq:
+ # to_s()
+ #
+ # Returns the content of the parameters as a string.
+
+ ##
+ # :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_s, :each_key, to: :@parameters
+
# By default, never raise an UnpermittedParameters exception if these
# params are present. The default includes both 'controller' and 'action'
# because they are added by Rails and should be of no concern. One way
@@ -115,18 +222,7 @@ module ActionController
# config. For instance:
#
# config.always_permitted_parameters = %w( controller action format )
- cattr_accessor :always_permitted_parameters
- self.always_permitted_parameters = %w( controller action )
-
- def self.const_missing(const_name)
- return super unless const_name == :NEVER_UNPERMITTED_PARAMS
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- `ActionController::Parameters::NEVER_UNPERMITTED_PARAMS` has been deprecated.
- Use `ActionController::Parameters.always_permitted_parameters` instead.
- MSG
-
- always_permitted_parameters
- end
+ cattr_accessor :always_permitted_parameters, default: %w( controller action )
# Returns a new instance of <tt>ActionController::Parameters</tt>.
# Also, sets the +permitted+ attribute to the default value of
@@ -135,57 +231,131 @@ module ActionController
# class Person < ActiveRecord::Base
# end
#
- # params = ActionController::Parameters.new(name: 'Francesco')
+ # params = ActionController::Parameters.new(name: "Francesco")
# params.permitted? # => false
# Person.new(params) # => ActiveModel::ForbiddenAttributesError
#
# ActionController::Parameters.permit_all_parameters = true
#
- # params = ActionController::Parameters.new(name: 'Francesco')
+ # params = ActionController::Parameters.new(name: "Francesco")
# params.permitted? # => true
# Person.new(params) # => #<Person id: nil, name: "Francesco">
- def initialize(attributes = nil)
- super(attributes)
+ def initialize(parameters = {})
+ @parameters = parameters.with_indifferent_access
@permitted = self.class.permit_all_parameters
end
- # Returns a safe +Hash+ representation of this parameter with all
- # unpermitted keys removed.
+ # Returns true if another +Parameters+ object contains the same content and
+ # permitted flag.
+ def ==(other)
+ if other.respond_to?(:permitted?)
+ permitted? == other.permitted? && parameters == other.parameters
+ else
+ @parameters == other
+ end
+ end
+
+ # Returns a safe <tt>ActiveSupport::HashWithIndifferentAccess</tt>
+ # representation of the parameters with all unpermitted keys removed.
#
# params = ActionController::Parameters.new({
- # name: 'Senjougahara Hitagi',
- # oddity: 'Heavy stone crab'
+ # name: "Senjougahara Hitagi",
+ # oddity: "Heavy stone crab"
# })
- # params.to_h # => {}
+ # params.to_h
+ # # => ActionController::UnfilteredParameters: unable to convert unpermitted parameters to hash
#
# safe_params = params.permit(:name)
# safe_params.to_h # => {"name"=>"Senjougahara Hitagi"}
def to_h
if permitted?
- to_hash
+ convert_parameters_to_hashes(@parameters, :to_h)
else
- slice(*self.class.always_permitted_parameters).permit!.to_h
+ raise UnfilteredParameters
end
end
- # Returns an unsafe, unfiltered +Hash+ representation of this parameter.
+ # Returns a safe <tt>Hash</tt> representation of the parameters
+ # with all unpermitted keys removed.
+ #
+ # params = ActionController::Parameters.new({
+ # name: "Senjougahara Hitagi",
+ # oddity: "Heavy stone crab"
+ # })
+ # params.to_hash
+ # # => ActionController::UnfilteredParameters: unable to convert unpermitted parameters to hash
+ #
+ # safe_params = params.permit(:name)
+ # safe_params.to_hash # => {"name"=>"Senjougahara Hitagi"}
+ def to_hash
+ to_h.to_hash
+ end
+
+ # Returns a string representation of the receiver suitable for use as a URL
+ # query string:
+ #
+ # params = ActionController::Parameters.new({
+ # name: "David",
+ # nationality: "Danish"
+ # })
+ # params.to_query
+ # # => ActionController::UnfilteredParameters: unable to convert unpermitted parameters to hash
+ #
+ # safe_params = params.permit(:name, :nationality)
+ # safe_params.to_query
+ # # => "name=David&nationality=Danish"
+ #
+ # An optional namespace can be passed to enclose key names:
+ #
+ # params = ActionController::Parameters.new({
+ # name: "David",
+ # nationality: "Danish"
+ # })
+ # safe_params = params.permit(:name, :nationality)
+ # safe_params.to_query("user")
+ # # => "user%5Bname%5D=David&user%5Bnationality%5D=Danish"
+ #
+ # The string pairs "key=value" that conform the query string
+ # are sorted lexicographically in ascending order.
+ #
+ # This method is also aliased as +to_param+.
+ def to_query(*args)
+ to_h.to_query(*args)
+ end
+ alias_method :to_param, :to_query
+
+ # Returns an unsafe, unfiltered
+ # <tt>ActiveSupport::HashWithIndifferentAccess</tt> representation of the
+ # parameters.
+ #
+ # params = ActionController::Parameters.new({
+ # name: "Senjougahara Hitagi",
+ # oddity: "Heavy stone crab"
+ # })
+ # params.to_unsafe_h
+ # # => {"name"=>"Senjougahara Hitagi", "oddity" => "Heavy stone crab"}
def to_unsafe_h
- to_hash
+ convert_parameters_to_hashes(@parameters, :to_unsafe_h)
end
alias_method :to_unsafe_hash, :to_unsafe_h
- # Convert all hashes in values into parameters, then yield each pair like
- # the same way as <tt>Hash#each_pair</tt>
+ # Convert all hashes in values into parameters, then yield each pair in
+ # the same way as <tt>Hash#each_pair</tt>.
def each_pair(&block)
- super do |key, value|
- convert_hashes_to_parameters(key, value)
+ @parameters.each_pair do |key, value|
+ yield [key, convert_hashes_to_parameters(key, value)]
end
-
- super
end
-
alias_method :each, :each_pair
+ # Convert all hashes in values into parameters, then yield each value in
+ # the same way as <tt>Hash#each_value</tt>.
+ def each_value(&block)
+ @parameters.each_pair do |key, value|
+ yield convert_hashes_to_parameters(key, value)
+ end
+ end
+
# Attribute that keeps track of converted arrays, if any, to avoid double
# looping in the common use case permit + mass-assignment. Defined in a
# method to instantiate it only if needed.
@@ -213,7 +383,7 @@ module ActionController
# class Person < ActiveRecord::Base
# end
#
- # params = ActionController::Parameters.new(name: 'Francesco')
+ # params = ActionController::Parameters.new(name: "Francesco")
# params.permitted? # => false
# Person.new(params) # => ActiveModel::ForbiddenAttributesError
# params.permit!
@@ -221,7 +391,7 @@ module ActionController
# Person.new(params) # => #<Person id: nil, name: "Francesco">
def permit!
each_pair do |key, value|
- Array.wrap(value).each do |v|
+ Array.wrap(value).flatten.each do |v|
v.permit! if v.respond_to? :permit!
end
end
@@ -230,19 +400,58 @@ module ActionController
self
end
- # Ensures that a parameter is present. If it's present, returns
- # the parameter at the given +key+, otherwise raises an
- # <tt>ActionController::ParameterMissing</tt> error.
+ # This method accepts both a single key and an array of keys.
+ #
+ # When passed a single key, if it exists and its associated value is
+ # either present or the singleton +false+, returns said value:
+ #
+ # ActionController::Parameters.new(person: { name: "Francesco" }).require(:person)
+ # # => <ActionController::Parameters {"name"=>"Francesco"} permitted: false>
+ #
+ # Otherwise raises <tt>ActionController::ParameterMissing</tt>:
#
- # ActionController::Parameters.new(person: { name: 'Francesco' }).require(:person)
- # # => {"name"=>"Francesco"}
+ # ActionController::Parameters.new.require(:person)
+ # # ActionController::ParameterMissing: param is missing or the value is empty: person
#
# ActionController::Parameters.new(person: nil).require(:person)
- # # => ActionController::ParameterMissing: param is missing or the value is empty: person
+ # # ActionController::ParameterMissing: param is missing or the value is empty: person
+ #
+ # ActionController::Parameters.new(person: "\t").require(:person)
+ # # ActionController::ParameterMissing: param is missing or the value is empty: person
#
# ActionController::Parameters.new(person: {}).require(:person)
- # # => ActionController::ParameterMissing: param is missing or the value is empty: person
+ # # ActionController::ParameterMissing: param is missing or the value is empty: person
+ #
+ # When given an array of keys, the method tries to require each one of them
+ # in order. If it succeeds, an array with the respective return values is
+ # returned:
+ #
+ # params = ActionController::Parameters.new(user: { ... }, profile: { ... })
+ # user_params, profile_params = params.require([:user, :profile])
+ #
+ # Otherwise, the method re-raises the first exception found:
+ #
+ # params = ActionController::Parameters.new(user: {}, profile: {})
+ # user_params, profile_params = params.require([:user, :profile])
+ # # ActionController::ParameterMissing: param is missing or the value is empty: user
+ #
+ # Technically this method can be used to fetch terminal values:
+ #
+ # # CAREFUL
+ # params = ActionController::Parameters.new(person: { name: "Finn" })
+ # name = params.require(:person).require(:name) # CAREFUL
+ #
+ # but take into account that at some point those ones have to be permitted:
+ #
+ # def person_params
+ # params.require(:person).permit(:name).tap do |person_params|
+ # person_params.require(:name) # SAFER
+ # end
+ # end
+ #
+ # for example.
def require(key)
+ return key.map { |k| require(k) } if key.is_a?(Array)
value = self[key]
if value.present? || value == false
value
@@ -259,7 +468,7 @@ module ActionController
# for the object to +true+. This is useful for limiting which attributes
# should be allowed for mass updating.
#
- # params = ActionController::Parameters.new(user: { name: 'Francesco', age: 22, role: 'admin' })
+ # params = ActionController::Parameters.new(user: { name: "Francesco", age: 22, role: "admin" })
# permitted = params.require(:user).permit(:name, :age)
# permitted.permitted? # => true
# permitted.has_key?(:name) # => true
@@ -279,18 +488,27 @@ module ActionController
# You may declare that the parameter should be an array of permitted scalars
# by mapping it to an empty array:
#
- # params = ActionController::Parameters.new(tags: ['rails', 'parameters'])
+ # params = ActionController::Parameters.new(tags: ["rails", "parameters"])
# params.permit(tags: [])
#
+ # Sometimes it is not possible or convenient to declare the valid keys of
+ # a hash parameter or its internal structure. Just map to an empty hash:
+ #
+ # params.permit(preferences: {})
+ #
+ # 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.
+ #
# You can also use +permit+ on nested parameters, like:
#
# params = ActionController::Parameters.new({
# person: {
- # name: 'Francesco',
+ # name: "Francesco",
# age: 22,
# pets: [{
- # name: 'Purplish',
- # category: 'dogs'
+ # name: "Purplish",
+ # category: "dogs"
# }]
# }
# })
@@ -304,25 +522,25 @@ module ActionController
#
# Note that if you use +permit+ in a key that points to a hash,
# it won't allow all the hash. You also need to specify which
- # attributes inside the hash should be whitelisted.
+ # attributes inside the hash should be permitted.
#
# params = ActionController::Parameters.new({
# person: {
# contact: {
- # email: 'none@test.com',
- # phone: '555-1234'
+ # email: "none@test.com",
+ # phone: "555-1234"
# }
# }
# })
#
# params.require(:person).permit(:contact)
- # # => {}
+ # # => <ActionController::Parameters {} permitted: true>
#
# params.require(:person).permit(contact: :phone)
- # # => {"contact"=>{"phone"=>"555-1234"}}
+ # # => <ActionController::Parameters {"contact"=><ActionController::Parameters {"phone"=>"555-1234"} permitted: true>} permitted: true>
#
# params.require(:person).permit(contact: [ :email, :phone ])
- # # => {"contact"=>{"email"=>"none@test.com", "phone"=>"555-1234"}}
+ # # => <ActionController::Parameters {"contact"=><ActionController::Parameters {"email"=>"none@test.com", "phone"=>"555-1234"} permitted: true>} permitted: true>
def permit(*filters)
params = self.class.new
@@ -330,7 +548,7 @@ module ActionController
case filter
when Symbol, String
permitted_scalar_filter(params, filter)
- when Hash then
+ when Hash
hash_filter(params, filter)
end
end
@@ -343,28 +561,56 @@ module ActionController
# Returns a parameter for the given +key+. If not found,
# returns +nil+.
#
- # params = ActionController::Parameters.new(person: { name: 'Francesco' })
- # params[:person] # => {"name"=>"Francesco"}
+ # params = ActionController::Parameters.new(person: { name: "Francesco" })
+ # params[:person] # => <ActionController::Parameters {"name"=>"Francesco"} permitted: false>
# params[:none] # => nil
def [](key)
- convert_hashes_to_parameters(key, super)
+ convert_hashes_to_parameters(key, @parameters[key])
+ end
+
+ # Assigns a value to a given +key+. The given key may still get filtered out
+ # when +permit+ is called.
+ def []=(key, value)
+ @parameters[key] = value
end
# Returns a parameter for the given +key+. If the +key+
# can't be found, there are several options: With no other arguments,
# it will raise an <tt>ActionController::ParameterMissing</tt> error;
- # if more arguments are given, then that will be returned; if a block
+ # if a second argument is given, then that is returned (converted to an
+ # instance of ActionController::Parameters if possible); if a block
# is given, then that will be run and its result returned.
#
- # params = ActionController::Parameters.new(person: { name: 'Francesco' })
- # params.fetch(:person) # => {"name"=>"Francesco"}
+ # params = ActionController::Parameters.new(person: { name: "Francesco" })
+ # params.fetch(:person) # => <ActionController::Parameters {"name"=>"Francesco"} permitted: false>
# params.fetch(:none) # => ActionController::ParameterMissing: param is missing or the value is empty: none
- # params.fetch(:none, 'Francesco') # => "Francesco"
- # params.fetch(:none) { 'Francesco' } # => "Francesco"
+ # params.fetch(:none, {}) # => <ActionController::Parameters {} permitted: false>
+ # params.fetch(:none, "Francesco") # => "Francesco"
+ # params.fetch(:none) { "Francesco" } # => "Francesco"
def fetch(key, *args)
- convert_hashes_to_parameters(key, super, false)
- rescue KeyError
- raise ActionController::ParameterMissing.new(key)
+ convert_value_to_parameters(
+ @parameters.fetch(key) {
+ if block_given?
+ yield
+ else
+ args.fetch(0) { raise ActionController::ParameterMissing.new(key) }
+ end
+ }
+ )
+ end
+
+ # Extracts the nested parameter from the given +keys+ by calling +dig+
+ # at each step. Returns +nil+ if any intermediate step is +nil+.
+ #
+ # params = ActionController::Parameters.new(foo: { bar: { baz: 1 } })
+ # params.dig(:foo, :bar, :baz) # => 1
+ # params.dig(:foo, :zot, :xyz) # => nil
+ #
+ # params2 = ActionController::Parameters.new(foo: [10, 11, 12])
+ # params2.dig(:foo, 1) # => 11
+ def dig(*keys)
+ convert_hashes_to_parameters(keys.first, @parameters[keys.first])
+ @parameters.dig(*keys)
end
# Returns a new <tt>ActionController::Parameters</tt> instance that
@@ -372,19 +618,36 @@ module ActionController
# don't exist, returns an empty hash.
#
# params = ActionController::Parameters.new(a: 1, b: 2, c: 3)
- # params.slice(:a, :b) # => {"a"=>1, "b"=>2}
- # params.slice(:d) # => {}
+ # params.slice(:a, :b) # => <ActionController::Parameters {"a"=>1, "b"=>2} permitted: false>
+ # params.slice(:d) # => <ActionController::Parameters {} permitted: false>
def slice(*keys)
- new_instance_with_inherited_permitted_status(super)
+ new_instance_with_inherited_permitted_status(@parameters.slice(*keys))
+ end
+
+ # Returns current <tt>ActionController::Parameters</tt> instance which
+ # contains only the given +keys+.
+ def slice!(*keys)
+ @parameters.slice!(*keys)
+ self
+ end
+
+ # Returns a new <tt>ActionController::Parameters</tt> instance that
+ # filters out the given +keys+.
+ #
+ # params = ActionController::Parameters.new(a: 1, b: 2, c: 3)
+ # params.except(:a, :b) # => <ActionController::Parameters {"c"=>3} permitted: false>
+ # params.except(:d) # => <ActionController::Parameters {"a"=>1, "b"=>2, "c"=>3} permitted: false>
+ def except(*keys)
+ new_instance_with_inherited_permitted_status(@parameters.except(*keys))
end
# Removes and returns the key/value pairs matching the given keys.
#
# params = ActionController::Parameters.new(a: 1, b: 2, c: 3)
- # params.extract!(:a, :b) # => {"a"=>1, "b"=>2}
- # params # => {"c"=>3}
+ # params.extract!(:a, :b) # => <ActionController::Parameters {"a"=>1, "b"=>2} permitted: false>
+ # params # => <ActionController::Parameters {"c"=>3} permitted: false>
def extract!(*keys)
- new_instance_with_inherited_permitted_status(super)
+ new_instance_with_inherited_permitted_status(@parameters.extract!(*keys))
end
# Returns a new <tt>ActionController::Parameters</tt> with the results of
@@ -392,56 +655,165 @@ module ActionController
#
# params = ActionController::Parameters.new(a: 1, b: 2, c: 3)
# params.transform_values { |x| x * 2 }
- # # => {"a"=>2, "b"=>4, "c"=>6}
+ # # => <ActionController::Parameters {"a"=>2, "b"=>4, "c"=>6} permitted: false>
def transform_values
- if block_given?
- new_instance_with_inherited_permitted_status(super)
- else
- super
- end
+ return to_enum(:transform_values) unless block_given?
+ new_instance_with_inherited_permitted_status(
+ @parameters.transform_values { |v| yield convert_value_to_parameters(v) }
+ )
+ end
+
+ # Performs values transformation and returns the altered
+ # <tt>ActionController::Parameters</tt> instance.
+ def transform_values!
+ return to_enum(:transform_values!) unless block_given?
+ @parameters.transform_values! { |v| yield convert_value_to_parameters(v) }
+ self
end
- # This method is here only to make sure that the returned object has the
- # correct +permitted+ status. It should not matter since the parent of
- # this object is +HashWithIndifferentAccess+
- def transform_keys # :nodoc:
- if block_given?
- new_instance_with_inherited_permitted_status(super)
+ # Returns a new <tt>ActionController::Parameters</tt> instance with the
+ # results of running +block+ once for every key. The values are unchanged.
+ def transform_keys(&block)
+ if block
+ new_instance_with_inherited_permitted_status(
+ @parameters.transform_keys(&block)
+ )
else
- super
+ @parameters.transform_keys
end
end
- # Deletes and returns a key-value pair from +Parameters+ whose key is equal
- # to key. If the key is not found, returns the default value. If the
- # optional code block is given and the key is not found, pass in the key
- # and return the result of block.
+ # Performs keys transformation and returns the altered
+ # <tt>ActionController::Parameters</tt> instance.
+ def transform_keys!(&block)
+ @parameters.transform_keys!(&block)
+ self
+ end
+
+ # Deletes a key-value pair from +Parameters+ and returns the value. If
+ # +key+ is not found, returns +nil+ (or, with optional code block, yields
+ # +key+ and returns the result). Cf. +#extract!+, which returns the
+ # corresponding +ActionController::Parameters+ object.
def delete(key, &block)
- convert_hashes_to_parameters(key, super, false)
+ convert_value_to_parameters(@parameters.delete(key, &block))
end
- # Equivalent to Hash#keep_if, but returns nil if no changes were made.
+ # Returns a new instance of <tt>ActionController::Parameters</tt> with only
+ # items that the block evaluates to true.
+ def select(&block)
+ new_instance_with_inherited_permitted_status(@parameters.select(&block))
+ end
+
+ # Equivalent to Hash#keep_if, but returns +nil+ if no changes were made.
def select!(&block)
- convert_value_to_parameters(super)
+ @parameters.select!(&block)
+ self
end
+ alias_method :keep_if, :select!
- # Returns an exact copy of the <tt>ActionController::Parameters</tt>
- # instance. +permitted+ state is kept on the duped object.
- #
- # params = ActionController::Parameters.new(a: 1)
- # params.permit!
- # params.permitted? # => true
- # copy_params = params.dup # => {"a"=>1}
- # copy_params.permitted? # => true
- def dup
- super.tap do |duplicate|
+ # Returns a new instance of <tt>ActionController::Parameters</tt> with items
+ # that the block evaluates to true removed.
+ def reject(&block)
+ new_instance_with_inherited_permitted_status(@parameters.reject(&block))
+ end
+
+ # Removes items that the block evaluates to true and returns self.
+ def reject!(&block)
+ @parameters.reject!(&block)
+ self
+ end
+ alias_method :delete_if, :reject!
+
+ # Returns values that were assigned to the given +keys+. Note that all the
+ # +Hash+ objects will be converted to <tt>ActionController::Parameters</tt>.
+ def values_at(*keys)
+ convert_value_to_parameters(@parameters.values_at(*keys))
+ end
+
+ # Returns a new <tt>ActionController::Parameters</tt> with all keys from
+ # +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 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
+ alias_method :with_defaults, :reverse_merge
+
+ # 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
+ alias_method :with_defaults!, :reverse_merge!
+
+ # 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.
+ def stringify_keys # :nodoc:
+ dup
+ end
+
+ def inspect
+ "<#{self.class} #{@parameters} permitted: #{@permitted}>"
+ end
+
+ def self.hook_into_yaml_loading # :nodoc:
+ # Wire up YAML format compatibility with Rails 4.2 and Psych 2.0.8 and 2.0.9+.
+ # Makes the YAML parser call `init_with` when it encounters the keys below
+ # instead of trying its own parsing routines.
+ YAML.load_tags["!ruby/hash-with-ivars:ActionController::Parameters"] = name
+ YAML.load_tags["!ruby/hash:ActionController::Parameters"] = name
+ end
+ hook_into_yaml_loading
+
+ def init_with(coder) # :nodoc:
+ case coder.tag
+ when "!ruby/hash:ActionController::Parameters"
+ # YAML 2.0.8's format where hash instance variables weren't stored.
+ @parameters = coder.map.with_indifferent_access
+ @permitted = false
+ when "!ruby/hash-with-ivars:ActionController::Parameters"
+ # YAML 2.0.9's Hash subclass format where keys and values
+ # were stored under an elements hash and `permitted` within an ivars hash.
+ @parameters = coder.map["elements"].with_indifferent_access
+ @permitted = coder.map["ivars"][:@permitted]
+ when "!ruby/object:ActionController::Parameters"
+ # YAML's Object format. Only needed because of the format
+ # backwardscompability above, otherwise equivalent to YAML's initialization.
+ @parameters, @permitted = coder.map["parameters"], coder.map["permitted"]
+ end
+ end
+
+ # Returns duplicate of object including all parameters.
+ def deep_dup
+ self.class.new(@parameters.deep_dup).tap do |duplicate|
duplicate.permitted = @permitted
end
end
protected
- def permitted=(new_permitted)
- @permitted = new_permitted
+ attr_reader :parameters
+
+ attr_writer :permitted
+
+ def fields_for_style?
+ @parameters.all? { |k, v| k =~ /\A-?\d+\z/ && (v.is_a?(Hash) || v.is_a?(Parameters)) }
end
private
@@ -451,40 +823,56 @@ module ActionController
end
end
- def convert_hashes_to_parameters(key, value, assign_if_converted=true)
+ def convert_parameters_to_hashes(value, using)
+ case value
+ when Array
+ value.map { |v| convert_parameters_to_hashes(v, using) }
+ when Hash
+ value.transform_values do |v|
+ convert_parameters_to_hashes(v, using)
+ end.with_indifferent_access
+ when Parameters
+ value.send(using)
+ else
+ value
+ end
+ end
+
+ def convert_hashes_to_parameters(key, value)
converted = convert_value_to_parameters(value)
- self[key] = converted if assign_if_converted && !converted.equal?(value)
+ @parameters[key] = converted unless converted.equal?(value)
converted
end
def convert_value_to_parameters(value)
- if value.is_a?(Array) && !converted_arrays.member?(value)
+ case value
+ when Array
+ return value if converted_arrays.member?(value)
converted = value.map { |_| convert_value_to_parameters(_) }
converted_arrays << converted
converted
- elsif value.is_a?(Parameters) || !value.is_a?(Hash)
- value
- else
+ when Hash
self.class.new(value)
+ else
+ value
end
end
def each_element(object)
- if object.is_a?(Array)
- object.map { |el| yield el }.compact
- elsif fields_for_style?(object)
- hash = object.class.new
- object.each { |k,v| hash[k] = yield v }
- hash
- else
- yield object
+ case object
+ when Array
+ object.grep(Parameters).map { |el| yield el }.compact
+ when Parameters
+ if object.fields_for_style?
+ hash = object.class.new
+ object.each { |k, v| hash[k] = yield v }
+ hash
+ else
+ yield object
+ end
end
end
- def fields_for_style?(object)
- object.is_a?(Hash) && object.all? { |k, v| k =~ /\A-?\d+\z/ && v.is_a?(Hash) }
- end
-
def unpermitted_parameters!(params)
unpermitted_keys = unpermitted_keys(params)
if unpermitted_keys.any?
@@ -499,7 +887,7 @@ module ActionController
end
def unpermitted_keys(params)
- self.keys - params.keys - self.always_permitted_parameters
+ keys - params.keys - always_permitted_parameters
end
#
@@ -530,71 +918,125 @@ module ActionController
]
def permitted_scalar?(value)
- PERMITTED_SCALAR_TYPES.any? {|type| value.is_a?(type)}
+ PERMITTED_SCALAR_TYPES.any? { |type| value.is_a?(type) }
end
- def permitted_scalar_filter(params, key)
- if has_key?(key) && permitted_scalar?(self[key])
- params[key] = self[key]
+ # Adds existing keys to the params if their values are scalar.
+ #
+ # For example:
+ #
+ # puts self.keys #=> ["zipcode(90210i)"]
+ # params = {}
+ #
+ # permitted_scalar_filter(params, "zipcode")
+ #
+ # puts params.keys # => ["zipcode"]
+ def permitted_scalar_filter(params, permitted_key)
+ permitted_key = permitted_key.to_s
+
+ if has_key?(permitted_key) && permitted_scalar?(self[permitted_key])
+ params[permitted_key] = self[permitted_key]
end
- keys.grep(/\A#{Regexp.escape(key)}\(\d+[if]?\)\z/) do |k|
- if permitted_scalar?(self[k])
- params[k] = self[k]
- end
+ each_key do |key|
+ next unless key =~ /\(\d+[if]?\)\z/
+ next unless $~.pre_match == permitted_key
+
+ params[key] = self[key] if permitted_scalar?(self[key])
end
end
def array_of_permitted_scalars?(value)
- if value.is_a?(Array)
- value.all? {|element| permitted_scalar?(element)}
+ if value.is_a?(Array) && value.all? { |element| permitted_scalar?(element) }
+ yield value
end
end
- def array_of_permitted_scalars_filter(params, key)
- if has_key?(key) && array_of_permitted_scalars?(self[key])
- params[key] = self[key]
- end
+ def non_scalar?(value)
+ value.is_a?(Array) || value.is_a?(Parameters)
end
EMPTY_ARRAY = []
+ EMPTY_HASH = {}
def hash_filter(params, filter)
filter = filter.with_indifferent_access
# Slicing filters out non-declared keys.
slice(*filter.keys).each do |key, value|
next unless value
+ next unless has_key? key
if filter[key] == EMPTY_ARRAY
# Declaration { comment_ids: [] }.
- array_of_permitted_scalars_filter(params, key)
- else
+ array_of_permitted_scalars?(self[key]) do |val|
+ params[key] = val
+ end
+ elsif filter[key] == EMPTY_HASH
+ # Declaration { preferences: {} }.
+ if value.is_a?(Parameters)
+ params[key] = permit_any_in_parameters(value)
+ end
+ elsif non_scalar?(value)
# Declaration { user: :name } or { user: [:name, :age, { address: ... }] }.
params[key] = each_element(value) do |element|
- if element.is_a?(Hash)
- element = self.class.new(element) unless element.respond_to?(:permit)
- element.permit(*Array.wrap(filter[key]))
- end
+ element.permit(*Array.wrap(filter[key]))
end
end
end
end
+
+ def permit_any_in_parameters(params)
+ self.class.new.tap do |sanitized|
+ params.each do |key, value|
+ case value
+ when ->(v) { permitted_scalar?(v) }
+ sanitized[key] = value
+ when Array
+ sanitized[key] = permit_any_in_array(value)
+ when Parameters
+ sanitized[key] = permit_any_in_parameters(value)
+ else
+ # Filter this one out.
+ end
+ end
+ end
+ end
+
+ def permit_any_in_array(array)
+ [].tap do |sanitized|
+ array.each do |element|
+ case element
+ when ->(e) { permitted_scalar?(e) }
+ sanitized << element
+ when Parameters
+ sanitized << permit_any_in_parameters(element)
+ else
+ # Filter this one out.
+ end
+ end
+ end
+ end
+
+ def initialize_copy(source)
+ super
+ @parameters = @parameters.dup
+ end
end
# == Strong \Parameters
#
# It provides an interface for protecting attributes from end-user
# assignment. This makes Action Controller parameters forbidden
- # to be used in Active Model mass assignment until they have been
- # whitelisted.
+ # to be used in Active Model mass assignment until they have been explicitly
+ # enumerated.
#
# 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
# # Using "Person.create(params[:person])" would raise an
- # # ActiveModel::ForbiddenAttributes exception because it'd
+ # # ActiveModel::ForbiddenAttributesError exception because it'd
# # be using mass assignment without an explicit permit step.
# # This is the recommended form:
# def create
@@ -602,7 +1044,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
@@ -613,7 +1055,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
@@ -622,7 +1064,8 @@ module ActionController
# end
#
# In order to use <tt>accepts_nested_attributes_for</tt> with Strong \Parameters, you
- # will need to specify which nested attributes should be whitelisted.
+ # will need to specify which nested attributes should be permitted. You might want
+ # to allow +:id+ and +:_destroy+, see ActiveRecord::NestedAttributes for more information.
#
# class Person
# has_many :pets
@@ -639,10 +1082,10 @@ module ActionController
# private
#
# def person_params
- # # It's mandatory to specify the nested attributes that should be whitelisted.
+ # # It's mandatory to specify the nested attributes that should be permitted.
# # If you use `permit` with just the key that points to the nested attributes hash,
# # it will return an empty hash.
- # params.require(:person).permit(:name, :age, pets_attributes: [ :name, :category ])
+ # params.require(:person).permit(:name, :age, pets_attributes: [ :id, :name, :category ])
# end
# end
#
diff --git a/actionpack/lib/action_controller/metal/testing.rb b/actionpack/lib/action_controller/metal/testing.rb
index d01927b7cb..6e8a95040f 100644
--- a/actionpack/lib/action_controller/metal/testing.rb
+++ b/actionpack/lib/action_controller/metal/testing.rb
@@ -1,31 +1,16 @@
+# frozen_string_literal: true
+
module ActionController
module Testing
extend ActiveSupport::Concern
- include RackDelegation
-
- # TODO : Rewrite tests using controller.headers= to use Rack env
- def headers=(new_headers)
- @_response ||= ActionDispatch::Response.new
- @_response.headers.replace(new_headers)
- end
-
# Behavior specific to functional tests
module Functional # :nodoc:
- def set_response!(request)
- end
-
def recycle!
@_url_options = nil
self.formats = nil
self.params = nil
end
end
-
- module ClassMethods
- def before_filters
- _process_action_callbacks.find_all{|x| x.kind == :before}.map(&:name)
- end
- end
end
end
diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb
index 5a0e5c62e4..f077e765ab 100644
--- a/actionpack/lib/action_controller/metal/url_for.rb
+++ b/actionpack/lib/action_controller/metal/url_for.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
module ActionController
# Includes +url_for+ into the host class. The class has to provide a +RouteSet+ by implementing
# 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
@@ -27,10 +29,10 @@ module ActionController
def url_options
@_url_options ||= {
- :host => request.host,
- :port => request.optional_port,
- :protocol => request.protocol,
- :_recall => request.path_parameters
+ host: request.host,
+ port: request.optional_port,
+ protocol: request.protocol,
+ _recall: request.path_parameters
}.merge!(super).freeze
if (same_origin = _routes.equal?(request.routes)) ||
@@ -41,7 +43,11 @@ module ActionController
if original_script_name
options[:original_script_name] = original_script_name
else
- options[:script_name] = same_origin ? request.script_name.dup : script_name
+ if same_origin
+ options[:script_name] = request.script_name.empty? ? "" : request.script_name.dup
+ else
+ options[:script_name] = script_name
+ end
end
options.freeze
else
diff --git a/actionpack/lib/action_controller/middleware.rb b/actionpack/lib/action_controller/middleware.rb
deleted file mode 100644
index 437fec3dc6..0000000000
--- a/actionpack/lib/action_controller/middleware.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-module ActionController
- class Middleware < Metal
- class ActionMiddleware
- def initialize(controller, app)
- @controller, @app = controller, app
- end
-
- def call(env)
- request = ActionDispatch::Request.new(env)
- @controller.build(@app).dispatch(:index, request)
- end
- end
-
- class << self
- alias build new
-
- def new(app)
- ActionMiddleware.new(self, app)
- end
- end
-
- attr_internal :app
-
- def process(action)
- response = super
- self.status, self.headers, self.response_body = response if response.is_a?(Array)
- response
- end
-
- def initialize(app)
- super()
- @_app = app
- end
-
- def index
- call(env)
- end
- end
-end \ No newline at end of file
diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb
index 28b20052b5..7d42f5d931 100644
--- a/actionpack/lib/action_controller/railtie.rb
+++ b/actionpack/lib/action_controller/railtie.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "rails"
require "action_controller"
require "action_dispatch/railtie"
@@ -11,7 +13,7 @@ module ActionController
config.eager_load_namespaces << ActionController
- initializer "action_controller.assets_config", :group => :all do |app|
+ initializer "action_controller.assets_config", group: :all do |app|
app.config.action_controller.assets_dir ||= app.config.paths["public"].first
end
@@ -22,13 +24,15 @@ module ActionController
initializer "action_controller.parameters_config" do |app|
options = app.config.action_controller
- ActionController::Parameters.permit_all_parameters = options.delete(:permit_all_parameters) { false }
- if app.config.action_controller[:always_permitted_parameters]
- ActionController::Parameters.always_permitted_parameters =
- app.config.action_controller.delete(:always_permitted_parameters)
- end
- ActionController::Parameters.action_on_unpermitted_parameters = options.delete(:action_on_unpermitted_parameters) do
- (Rails.env.test? || Rails.env.development?) ? :log : false
+ ActiveSupport.on_load(:action_controller, run_once: true) do
+ ActionController::Parameters.permit_all_parameters = options.delete(:permit_all_parameters) { false }
+ if app.config.action_controller[:always_permitted_parameters]
+ ActionController::Parameters.always_permitted_parameters =
+ app.config.action_controller.delete(:always_permitted_parameters)
+ end
+ ActionController::Parameters.action_on_unpermitted_parameters = options.delete(:action_on_unpermitted_parameters) do
+ (Rails.env.test? || Rails.env.development?) ? :log : false
+ end
end
end
@@ -42,7 +46,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
@@ -51,7 +55,7 @@ module ActionController
extend ::AbstractController::Railties::RoutesHelpers.with(app.routes)
extend ::ActionController::Railties::Helpers
- options.each do |k,v|
+ options.each do |k, v|
k = "#{k}="
if respond_to?(k)
send(k, v)
@@ -67,5 +71,19 @@ module ActionController
config.compile_methods! if config.respond_to?(:compile_methods!)
end
end
+
+ initializer "action_controller.request_forgery_protection" do |app|
+ ActiveSupport.on_load(:action_controller_base) do
+ if app.config.action_controller.default_protect_from_forgery
+ protect_from_forgery with: :exception
+ end
+ end
+ end
+
+ initializer "action_controller.eager_load_actions" do
+ ActiveSupport.on_load(:after_initialize) do
+ ActionController::Metal.descendants.each(&:action_methods) if config.eager_load
+ end
+ end
end
end
diff --git a/actionpack/lib/action_controller/railties/helpers.rb b/actionpack/lib/action_controller/railties/helpers.rb
index 3985c6b273..75938108d6 100644
--- a/actionpack/lib/action_controller/railties/helpers.rb
+++ b/actionpack/lib/action_controller/railties/helpers.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionController
module Railties
module Helpers
@@ -5,7 +7,7 @@ module ActionController
super
return unless klass.respond_to?(:helpers_path=)
- if namespace = klass.parents.detect { |m| m.respond_to?(:railtie_helpers_paths) }
+ if namespace = klass.module_parents.detect { |m| m.respond_to?(:railtie_helpers_paths) }
paths = namespace.railtie_helpers_paths
else
paths = ActionController::Helpers.helpers_path
diff --git a/actionpack/lib/action_controller/renderer.rb b/actionpack/lib/action_controller/renderer.rb
index e8b29c5b5e..2b4559c760 100644
--- a/actionpack/lib/action_controller/renderer.rb
+++ b/actionpack/lib/action_controller/renderer.rb
@@ -1,11 +1,13 @@
-require 'active_support/core_ext/hash/keys'
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/keys"
module ActionController
- # ActionController::Renderer allows to render arbitrary templates
+ # ActionController::Renderer allows you to render arbitrary templates
# without requirement of being in controller actions.
#
# You get a concrete renderer class by invoking ActionController::Base#renderer.
- # For example,
+ # For example:
#
# ApplicationController.renderer
#
@@ -13,12 +15,12 @@ module ActionController
#
# ApplicationController.renderer.render template: '...'
#
- # You can use a shortcut on controller to replace previous example with:
+ # You can use this shortcut in a controller, instead of the previous example:
#
# ApplicationController.render template: '...'
#
- # #render method allows you to use any options as when rendering in controller.
- # For example,
+ # #render allows you to use the same options that you can use when rendering in a controller.
+ # For example:
#
# FooController.render :action, locals: { ... }, assigns: { ... }
#
@@ -34,67 +36,97 @@ module ActionController
# ApplicationController.renderer.new(method: 'post', https: true)
#
class Renderer
- class_attribute :controller, :defaults
- # Rack environment to render templates in.
- attr_reader :env
-
- class << self
- delegate :render, to: :new
-
- # Create a new renderer class for a specific controller class.
- def for(controller)
- Class.new self do
- self.controller = controller
- self.defaults = {
- http_host: 'example.org',
- https: false,
- method: 'get',
- script_name: '',
- 'rack.input' => ''
- }
- end
- end
+ attr_reader :defaults, :controller
+
+ DEFAULTS = {
+ http_host: "example.org",
+ https: false,
+ method: "get",
+ script_name: "",
+ input: ""
+ }.freeze
+
+ # Create a new renderer instance for a specific controller class.
+ def self.for(controller, env = {}, defaults = DEFAULTS.dup)
+ new(controller, env, defaults)
+ end
+
+ # Create a new renderer for the same controller but with a new env.
+ def new(env = {})
+ self.class.new controller, env, defaults
+ end
+
+ # 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)
end
# Accepts a custom Rack environment to render templates in.
- # It will be merged with ActionController::Renderer.defaults
- def initialize(env = {})
- @env = normalize_keys(defaults).merge normalize_keys(env)
- @env['action_dispatch.routes'] = controller._routes
+ # It will be merged with the default Rack environment defined by
+ # +ActionController::Renderer::DEFAULTS+.
+ def initialize(controller, env, defaults)
+ @controller = controller
+ @defaults = defaults
+ @env = normalize_keys defaults.merge(env)
end
# Render templates with any options from ActionController::Base#render_to_string.
+ #
+ # The primary options are:
+ # * <tt>:partial</tt> - See <tt>ActionView::PartialRenderer</tt> for details.
+ # * <tt>:file</tt> - Renders an explicit template file. Add <tt>:locals</tt> to pass in, if so desired.
+ # It shouldn’t be used directly with unsanitized user input due to lack of validation.
+ # * <tt>:inline</tt> - Renders a ERB template string.
+ # * <tt>:plain</tt> - Renders provided text and sets the content type as <tt>text/plain</tt>.
+ # * <tt>:html</tt> - Renders the provided HTML safe string, otherwise
+ # performs HTML escape on the string first. Sets the content type as <tt>text/html</tt>.
+ # * <tt>:json</tt> - Renders the provided hash or object in JSON. You don't
+ # need to call <tt>.to_json</tt> on the object you want to render.
+ # * <tt>:body</tt> - Renders provided text and sets content type of <tt>text/plain</tt>.
+ #
+ # If no <tt>options</tt> hash is passed or if <tt>:update</tt> is specified, the default is
+ # to render a partial and use the second parameter as the locals hash.
def render(*args)
- raise 'missing controller' unless controller?
+ raise "missing controller" unless controller
+
+ request = ActionDispatch::Request.new @env
+ request.routes = controller._routes
- instance = controller.build_with_env(env)
+ instance = controller.new
+ instance.set_request! request
+ instance.set_response! controller.make_response!(request)
instance.render_to_string(*args)
end
private
def normalize_keys(env)
- http_header_format(env).tap do |new_env|
- handle_method_key! new_env
- handle_https_key! new_env
- end
+ 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
- def http_header_format(env)
- env.transform_keys do |key|
- key.is_a?(Symbol) ? key.to_s.upcase : key
- end
- end
+ RACK_KEY_TRANSLATION = {
+ http_host: "HTTP_HOST",
+ https: "HTTPS",
+ method: "REQUEST_METHOD",
+ script_name: "SCRIPT_NAME",
+ input: "rack.input"
+ }
+
+ IDENTITY = ->(_) { _ }
+
+ RACK_VALUE_TRANSLATION = {
+ https: ->(v) { v ? "on" : "off" },
+ method: ->(v) { v.upcase },
+ }
- def handle_method_key!(env)
- if method = env.delete('METHOD')
- env['REQUEST_METHOD'] = method.upcase
- end
+ def rack_key_for(key)
+ RACK_KEY_TRANSLATION.fetch(key, key.to_s)
end
- def handle_https_key!(env)
- if env.has_key? 'HTTPS'
- env['HTTPS'] = env['HTTPS'] ? 'on' : 'off'
- end
+ def rack_value_for(key, value)
+ RACK_VALUE_TRANSLATION.fetch(key, IDENTITY).call value
end
end
end
diff --git a/actionpack/lib/action_controller/template_assertions.rb b/actionpack/lib/action_controller/template_assertions.rb
index 0179f4afcd..dd83c1a283 100644
--- a/actionpack/lib/action_controller/template_assertions.rb
+++ b/actionpack/lib/action_controller/template_assertions.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionController
module TemplateAssertions
def assert_template(options = {}, message = nil)
diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb
index 43c481339a..5d784ceb31 100644
--- a/actionpack/lib/action_controller/test_case.rb
+++ b/actionpack/lib/action_controller/test_case.rb
@@ -1,25 +1,49 @@
-require 'rack/session/abstract/id'
-require 'active_support/core_ext/object/to_query'
-require 'active_support/core_ext/module/anonymous'
-require 'active_support/core_ext/hash/keys'
-require 'action_controller/template_assertions'
-require 'rails-dom-testing'
+# frozen_string_literal: true
+
+require "rack/session/abstract/id"
+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/module/redefine_method"
+require "active_support/core_ext/hash/keys"
+require "active_support/testing/constant_lookup"
+require "action_controller/template_assertions"
+require "rails-dom-testing"
module ActionController
+ class Metal
+ include Testing::Functional
+ end
+
+ module Live
+ # 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.
+ silence_redefinition_of_method :new_controller_thread
+ def new_controller_thread # :nodoc:
+ yield
+ end
+ end
+
+ # ActionController::TestCase will be deprecated and moved to a gem in Rails 5.1.
+ # Please use ActionDispatch::IntegrationTest going forward.
class TestRequest < ActionDispatch::TestRequest #:nodoc:
DEFAULT_ENV = ActionDispatch::TestRequest::DEFAULT_ENV.dup
- DEFAULT_ENV.delete 'PATH_INFO'
+ DEFAULT_ENV.delete "PATH_INFO"
def self.new_session
TestSession.new
end
- # Create a new test request with default `env` values
- def self.create
+ attr_reader :controller_class
+
+ # 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
env["rack.request.cookie_hash"] = {}.with_indifferent_access
- new(default_env.merge(env), new_session)
+ new(default_env.merge(env), new_session, controller_class)
end
def self.default_env
@@ -27,29 +51,31 @@ module ActionController
end
private_class_method :default_env
- def initialize(env, session)
+ def initialize(env, session, controller_class)
super(env)
self.session = session
- self.session_options = TestSession::DEFAULT_OPTIONS
+ self.session_options = TestSession::DEFAULT_OPTIONS.dup
+ @controller_class = controller_class
+ @custom_param_parsers = {
+ xml: lambda { |raw_post| Hash.from_xml(raw_post)["hash"] }
+ }
end
def query_string=(string)
- @env[Rack::QUERY_STRING] = string
+ set_header Rack::QUERY_STRING, string
end
- def request_parameters=(params)
- @env["action_dispatch.request.request_parameters"] = params
+ def content_type=(type)
+ set_header "CONTENT_TYPE", type
end
- def assign_parameters(routes, controller_path, action, parameters = {})
- parameters = parameters.symbolize_keys
- generated_path, extra_keys = routes.generate_extras(parameters.merge(:controller => controller_path, :action => action))
+ def assign_parameters(routes, controller_path, action, parameters, generated_path, query_string_keys)
non_path_parameters = {}
path_parameters = {}
parameters.each do |key, value|
- if extra_keys.include?(key) || key == :action || key == :controller
+ if query_string_keys.include?(key)
non_path_parameters[key] = value
else
if value.is_a?(Array)
@@ -63,41 +89,41 @@ module ActionController
end
if get?
- if self.query_string.blank?
+ if query_string.blank?
self.query_string = non_path_parameters.to_query
end
else
if ENCODER.should_multipart?(non_path_parameters)
- @env['CONTENT_TYPE'] = ENCODER.content_type
+ self.content_type = ENCODER.content_type
data = ENCODER.build_multipart non_path_parameters
else
- @env['CONTENT_TYPE'] ||= 'application/x-www-form-urlencoded'
-
- # FIXME: setting `request_parametes` is normally handled by the
- # params parser middleware, and we should remove this roundtripping
- # when we switch to caling `call` on the controller
+ fetch_header("CONTENT_TYPE") do |k|
+ set_header k, "application/x-www-form-urlencoded"
+ end
- case content_mime_type.ref
+ case content_mime_type.to_sym
+ when nil
+ raise "Unknown Content-Type: #{content_type}"
when :json
data = ActiveSupport::JSON.encode(non_path_parameters)
- params = ActiveSupport::JSON.decode(data).with_indifferent_access
- self.request_parameters = params
when :xml
data = non_path_parameters.to_xml
- params = Hash.from_xml(data)['hash']
- self.request_parameters = params
when :url_encoded_form
data = non_path_parameters.to_query
else
- raise "Unknown Content-Type: #{content_type}"
+ @custom_param_parsers[content_mime_type.symbol] = ->(_) { non_path_parameters }
+ data = non_path_parameters.to_query
end
end
- @env['CONTENT_LENGTH'] = data.length.to_s
- @env['rack.input'] = StringIO.new(data)
+ data_stream = StringIO.new(data)
+ set_header "CONTENT_LENGTH", data_stream.length.to_s
+ set_header "rack.input", data_stream
end
- @env["PATH_INFO"] ||= generated_path
+ fetch_header("PATH_INFO") do |k|
+ set_header k, generated_path
+ end
path_parameters[:controller] = controller_path
path_parameters[:action] = action
@@ -108,7 +134,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
@@ -130,25 +156,21 @@ module ActionController
"multipart/form-data; boundary=#{Rack::Test::MULTIPART_BOUNDARY}"
end
end.new
- end
- class TestResponse < ActionDispatch::TestResponse
+ private
+
+ def params_parsers
+ super.merge @custom_param_parsers
+ end
end
class LiveTestResponse < Live::Response
- def body
- @body ||= super
- end
-
# Was the response successful?
alias_method :success?, :successful?
# Was the URL not found?
alias_method :missing?, :not_found?
- # Were we redirected?
- alias_method :redirect?, :redirection?
-
# Was there a server-side error?
alias_method :error?, :server_error?
end
@@ -156,7 +178,7 @@ module ActionController
# Methods #destroy and #load! are overridden to avoid calling methods on the
# @store object, which does not exist for the TestSession class.
class TestSession < Rack::Session::Abstract::SessionHash #:nodoc:
- DEFAULT_OPTIONS = Rack::Session::Abstract::ID::DEFAULT_OPTIONS
+ DEFAULT_OPTIONS = Rack::Session::Abstract::Persisted::DEFAULT_OPTIONS
def initialize(session = {})
super(nil, nil)
@@ -181,6 +203,10 @@ module ActionController
clear
end
+ def fetch(key, *args, &block)
+ @data.fetch(key.to_s, *args, &block)
+ end
+
private
def load!
@@ -189,10 +215,18 @@ module ActionController
end
# Superclass for ActionController functional tests. Functional tests allow you to
- # test a single controller action per test method. This should not be confused with
- # integration tests (see ActionDispatch::IntegrationTest), which are more like
- # "stories" that can involve multiple controllers and multiple actions (i.e. multiple
- # different HTTP requests).
+ # test a single controller action per test method.
+ #
+ # == Use integration style controller tests over functional style controller tests.
+ #
+ # Rails discourages the use of functional tests in favor of integration tests
+ # (use ActionDispatch::IntegrationTest).
+ #
+ # New Rails applications no longer generate functional style controller tests and they should
+ # only be used for backward compatibility. Integration style controller tests perform actual
+ # requests, whereas functional style controller tests merely simulate a request. Besides,
+ # integration tests are as fast as functional tests and provide lot of helpers such as +as+,
+ # +parsed_body+ for effective testing of controller actions including even API endpoints.
#
# == Basic example
#
@@ -209,11 +243,11 @@ module ActionController
# # Simulate a POST response with the given HTTP parameters.
# post(:create, params: { book: { title: "Love Hina" }})
#
- # # Assert that the controller tried to redirect us to
+ # # Asserts that the controller tried to redirect us to
# # the created book's URI.
# assert_response :found
#
- # # Assert that the controller really put the book in the database.
+ # # Asserts that the controller really put the book in the database.
# assert_not_nil Book.find_by(title: "Love Hina")
# end
# end
@@ -222,7 +256,7 @@ module ActionController
#
# def test_create
# json = {book: { title: "Love Hina" }}.to_json
- # post :create, json
+ # post :create, body: json
# end
#
# == Special instance variables
@@ -237,7 +271,7 @@ module ActionController
# request. You can modify this object before sending the HTTP request. For example,
# you might want to set some session properties before sending a GET request.
# <b>@response</b>::
- # An ActionController::TestResponse object, representing the response
+ # An ActionDispatch::TestResponse object, representing the response
# of the last HTTP response. In the above example, <tt>@response</tt> becomes valid
# after calling +post+. If the various assert methods are not sufficient, then you
# may use this object to inspect the HTTP response in detail.
@@ -269,7 +303,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.
@@ -301,7 +335,6 @@ module ActionController
attr_reader :response, :request
module ClassMethods
-
# Sets the controller class name. Useful if the name can't be inferred from test class.
# Normalizes +controller_class+ before using.
#
@@ -324,7 +357,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)
@@ -358,58 +391,43 @@ module ActionController
#
# Note that the request method is not verified. The different methods are
# available to make the tests more expressive.
- def get(action, *args)
- res = process_with_kwargs("GET", action, *args)
+ def get(action, **args)
+ res = process(action, method: "GET", **args)
cookies.update res.cookies
res
end
# Simulate a POST request with the given parameters and set/volley the response.
# See +get+ for more details.
- def post(action, *args)
- process_with_kwargs("POST", action, *args)
+ def post(action, **args)
+ process(action, method: "POST", **args)
end
# Simulate a PATCH request with the given parameters and set/volley the response.
# See +get+ for more details.
- def patch(action, *args)
- process_with_kwargs("PATCH", action, *args)
+ def patch(action, **args)
+ process(action, method: "PATCH", **args)
end
# Simulate a PUT request with the given parameters and set/volley the response.
# See +get+ for more details.
- def put(action, *args)
- process_with_kwargs("PUT", action, *args)
+ def put(action, **args)
+ process(action, method: "PUT", **args)
end
# Simulate a DELETE request with the given parameters and set/volley the response.
# See +get+ for more details.
- def delete(action, *args)
- process_with_kwargs("DELETE", action, *args)
+ def delete(action, **args)
+ process(action, method: "DELETE", **args)
end
# Simulate a HEAD request with the given parameters and set/volley the response.
# See +get+ for more details.
- def head(action, *args)
- process_with_kwargs("HEAD", action, *args)
+ def head(action, **args)
+ process(action, method: "HEAD", **args)
end
- def xml_http_request(*args)
- ActiveSupport::Deprecation.warn(<<-MSG.strip_heredoc)
- xhr and xml_http_request methods are deprecated in favor of
- `get :index, xhr: true` and `post :create, xhr: true`
- MSG
-
- @request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
- @request.env['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ')
- __send__(*args).tap do
- @request.env.delete 'HTTP_X_REQUESTED_WITH'
- @request.env.delete 'HTTP_ACCEPT'
- end
- end
- alias xhr :xml_http_request
-
- # Simulate a HTTP request to +action+ by specifying request method,
+ # Simulate an HTTP request to +action+ by specifying request method,
# parameters and set/volley the response.
#
# - +action+: The controller action to call.
@@ -421,6 +439,8 @@ module ActionController
# - +session+: A hash of parameters to store in the session. This may be +nil+.
# - +flash+: A hash of parameters to store in the flash. This may be +nil+.
# - +format+: Request format. Defaults to +nil+. Can be string or symbol.
+ # - +as+: Content type. Defaults to +nil+. Must be a symbol that corresponds
+ # to a mime type.
#
# Example calling +create+ action and sending two params:
#
@@ -437,109 +457,109 @@ module ActionController
# respectively which will make tests more expressive.
#
# Note that the request method is not verified.
- def process(action, *args)
+ def process(action, method: "GET", params: {}, session: nil, body: nil, flash: {}, format: nil, xhr: false, as: nil)
check_required_ivars
- if kwarg_request?(args)
- parameters, session, body, flash, http_method, format, xhr = args[0].values_at(:params, :session, :body, :flash, :method, :format, :xhr)
- else
- http_method, parameters, session, flash = args
- format = nil
+ http_method = method.to_s.upcase
- if parameters.is_a?(String) && http_method != 'HEAD'
- body = parameters
- parameters = nil
- end
+ @html_document = nil
- if parameters.present? || session.present? || flash.present?
- non_kwarg_request_warning
- end
- end
+ cookies.update(@request.cookies)
+ cookies.update_cookies_from_jar
+ @request.set_header "HTTP_COOKIE", cookies.to_header
+ @request.delete_header "action_dispatch.cookies"
- if body.present?
- @request.env['RAW_POST_DATA'] = body
- end
+ @request = TestRequest.new scrub_env!(@request.env), @request.session, @controller.class
+ @response = build_response @response_klass
+ @response.request = @request
+ @controller.recycle!
- if http_method.present?
- http_method = http_method.to_s.upcase
- else
- http_method = "GET"
+ if body
+ @request.set_header "RAW_POST_DATA", body
end
- parameters ||= {}
+ @request.set_header "REQUEST_METHOD", http_method
- if format.present?
- parameters[:format] = format
+ if as
+ @request.content_type = Mime[as].to_s
+ format ||= as
end
- @html_document = nil
+ parameters = params.symbolize_keys
- unless @controller.respond_to?(:recycle!)
- @controller.extend(Testing::Functional)
+ if format
+ parameters[:format] = format
end
- self.cookies.update @request.cookies
- @request.env['HTTP_COOKIE'] = cookies.to_header
- @request.env['action_dispatch.cookies'] = nil
+ generated_extras = @routes.generate_extras(parameters.merge(controller: controller_class_name, action: action.to_s))
+ generated_path = generated_path(generated_extras)
+ query_string_keys = query_parameter_names(generated_extras)
- @request = TestRequest.new scrub_env!(@request.env), @request.session
- @response = build_response @response_klass
- @response.request = @request
- @controller.recycle!
-
- @request.env['REQUEST_METHOD'] = http_method
-
- controller_class_name = @controller.class.anonymous? ?
- "anonymous" :
- @controller.class.controller_path
-
- @request.assign_parameters(@routes, controller_class_name, action.to_s, parameters)
+ @request.assign_parameters(@routes, controller_class_name, action.to_s, parameters, generated_path, query_string_keys)
@request.session.update(session) if session
@request.flash.update(flash || {})
if xhr
- @request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
- @request.env['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ')
+ @request.set_header "HTTP_X_REQUESTED_WITH", "XMLHttpRequest"
+ @request.fetch_header("HTTP_ACCEPT") do |k|
+ @request.set_header k, [Mime[:js], Mime[:html], Mime[:xml], "text/xml", "*/*"].join(", ")
+ end
end
- @controller.request = @request
- @controller.response = @response
-
- @request.env["SCRIPT_NAME"] ||= @controller.config.relative_url_root
-
- @controller.recycle!
- @controller.process(action)
+ @request.fetch_header("SCRIPT_NAME") do |k|
+ @request.set_header k, @controller.config.relative_url_root
+ end
- @request.env.delete 'HTTP_COOKIE'
+ begin
+ @controller.recycle!
+ @controller.dispatch(action, @request, @response)
+ ensure
+ @request = @controller.request
+ @response = @controller.response
+
+ if @request.have_cookie_jar?
+ unless @request.cookie_jar.committed?
+ @request.cookie_jar.write(@response)
+ cookies.update(@request.cookie_jar.instance_variable_get(:@cookies))
+ end
+ end
+ @response.prepare!
- if cookies = @request.env['action_dispatch.cookies']
- unless @response.committed?
- cookies.write(@response)
- self.cookies.update(cookies.instance_variable_get(:@cookies))
+ if flash_value = @request.flash.to_session_value
+ @request.session["flash"] = flash_value
+ else
+ @request.session.delete("flash")
end
- end
- @response.prepare!
- if flash_value = @request.flash.to_session_value
- @request.session['flash'] = flash_value
- else
- @request.session.delete('flash')
- end
+ if xhr
+ @request.delete_header "HTTP_X_REQUESTED_WITH"
+ @request.delete_header "HTTP_ACCEPT"
+ end
+ @request.query_string = ""
- if xhr
- @request.env.delete 'HTTP_X_REQUESTED_WITH'
- @request.env.delete 'HTTP_ACCEPT'
+ @response.sent!
end
- @request.query_string = ''
@response
end
+ def controller_class_name
+ @controller.class.anonymous? ? "anonymous" : @controller.class.controller_path
+ end
+
+ def generated_path(generated_extras)
+ generated_extras[0]
+ end
+
+ def query_parameter_names(generated_extras)
+ generated_extras[1] + [:controller, :action]
+ end
+
def setup_controller_request_and_response
@controller = nil unless defined? @controller
- @response_klass = TestResponse
+ @response_klass = ActionDispatch::TestResponse
if klass = self.class.controller_class
if klass < ActionController::Live
@@ -554,7 +574,7 @@ module ActionController
end
end
- @request = TestRequest.create
+ @request = TestRequest.create(@controller.class)
@response = build_response @response_klass
@response.request = @request
@@ -565,7 +585,7 @@ module ActionController
end
def build_response(klass)
- klass.new
+ klass.create
end
included do
@@ -573,68 +593,35 @@ module ActionController
include ActionDispatch::Assertions
class_attribute :_controller_class
setup :setup_controller_request_and_response
+ ActiveSupport.run_load_hooks(:action_controller_test_case, self)
end
private
- def scrub_env!(env)
- env.delete_if { |k, v| k =~ /^(action_dispatch|rack)\.request/ }
- env.delete_if { |k, v| k =~ /^action_dispatch\.rescue/ }
- env.delete 'action_dispatch.request.query_parameters'
- env.delete 'action_dispatch.request.request_parameters'
- env
- end
-
- def process_with_kwargs(http_method, action, *args)
- if kwarg_request?(args)
- args.first.merge!(method: http_method)
- process(action, *args)
- else
- non_kwarg_request_warning if args.any?
-
- args = args.unshift(http_method)
- process(action, *args)
+ def scrub_env!(env)
+ env.delete_if { |k, v| k =~ /^(action_dispatch|rack)\.request/ }
+ env.delete_if { |k, v| k =~ /^action_dispatch\.rescue/ }
+ env.delete "action_dispatch.request.query_parameters"
+ env.delete "action_dispatch.request.request_parameters"
+ env["rack.input"] = StringIO.new
+ env.delete "CONTENT_LENGTH"
+ env.delete "RAW_POST_DATA"
+ env
end
- end
-
- REQUEST_KWARGS = %i(params session flash method body xhr)
- def kwarg_request?(args)
- args[0].respond_to?(:keys) && (
- (args[0].key?(:format) && args[0].keys.size == 1) ||
- args[0].keys.any? { |k| REQUEST_KWARGS.include?(k) }
- )
- end
- def non_kwarg_request_warning
- ActiveSupport::Deprecation.warn(<<-MSG.strip_heredoc)
- ActionController::TestCase HTTP request methods will accept only
- keyword arguments in future Rails versions.
-
- Examples:
-
- get :show, params: { id: 1 }, session: { user_id: 1 }
- process :update, method: :post, params: { id: 1 }
- MSG
- end
-
- def document_root_element
- html_document.root
- end
+ def document_root_element
+ html_document.root
+ end
- def check_required_ivars
- # Sanity check for required instance variables so we can give an
- # understandable error message.
- [:@routes, :@controller, :@request, :@response].each do |iv_name|
- if !instance_variable_defined?(iv_name) || instance_variable_get(iv_name).nil?
- raise "#{iv_name} is nil: make sure you set it in your test's setup method."
+ def check_required_ivars
+ # Sanity check for required instance variables so we can give an
+ # understandable error message.
+ [:@routes, :@controller, :@request, :@response].each do |iv_name|
+ if !instance_variable_defined?(iv_name) || instance_variable_get(iv_name).nil?
+ raise "#{iv_name} is nil: make sure you set it in your test's setup method."
+ end
end
end
- end
-
- def html_format?(parameters)
- return true unless parameters.key?(:format)
- Mime.fetch(parameters[:format]) { Mime['html'] }.html?
- end
end
include Behavior
diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb
index f6336c8c7a..0822cdc0a6 100644
--- a/actionpack/lib/action_dispatch.rb
+++ b/actionpack/lib/action_dispatch.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
#--
-# Copyright (c) 2004-2015 David Heinemeier Hansson
+# Copyright (c) 2004-2018 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -21,15 +23,15 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
-require 'active_support'
-require 'active_support/rails'
-require 'active_support/core_ext/module/attribute_accessors'
+require "active_support"
+require "active_support/rails"
+require "active_support/core_ext/module/attribute_accessors"
-require 'action_pack'
-require 'rack'
+require "action_pack"
+require "rack"
module Rack
- autoload :Test, 'rack/test'
+ autoload :Test, "rack/test"
end
module ActionDispatch
@@ -39,21 +41,22 @@ module ActionDispatch
end
eager_autoload do
- autoload_under 'http' do
+ autoload_under "http" do
+ autoload :ContentSecurityPolicy
autoload :Request
autoload :Response
end
end
- autoload_under 'middleware' do
+ autoload_under "middleware" do
autoload :RequestId
autoload :Callbacks
autoload :Cookies
autoload :DebugExceptions
+ autoload :DebugLocks
autoload :ExceptionWrapper
+ autoload :Executor
autoload :Flash
- autoload :LoadInterlock
- autoload :ParamsParser
autoload :PublicExceptions
autoload :Reloader
autoload :RemoteIp
@@ -63,7 +66,7 @@ module ActionDispatch
end
autoload :Journey
- autoload :MiddlewareStack, 'action_dispatch/middleware/stack'
+ autoload :MiddlewareStack, "action_dispatch/middleware/stack"
autoload :Routing
module Http
@@ -75,30 +78,33 @@ module ActionDispatch
autoload :Parameters
autoload :ParameterFilter
autoload :Upload
- autoload :UploadedFile, 'action_dispatch/http/upload'
+ autoload :UploadedFile, "action_dispatch/http/upload"
autoload :URL
end
module Session
- autoload :AbstractStore, 'action_dispatch/middleware/session/abstract_store'
- autoload :CookieStore, 'action_dispatch/middleware/session/cookie_store'
- autoload :MemCacheStore, 'action_dispatch/middleware/session/mem_cache_store'
- autoload :CacheStore, 'action_dispatch/middleware/session/cache_store'
+ autoload :AbstractStore, "action_dispatch/middleware/session/abstract_store"
+ autoload :CookieStore, "action_dispatch/middleware/session/cookie_store"
+ autoload :MemCacheStore, "action_dispatch/middleware/session/mem_cache_store"
+ autoload :CacheStore, "action_dispatch/middleware/session/cache_store"
end
mattr_accessor :test_app
- autoload_under 'testing' do
+ autoload_under "testing" do
autoload :Assertions
autoload :Integration
- autoload :IntegrationTest, 'action_dispatch/testing/integration'
+ autoload :IntegrationTest, "action_dispatch/testing/integration"
autoload :TestProcess
autoload :TestRequest
autoload :TestResponse
+ autoload :AssertionResponse
end
+
+ autoload :SystemTestCase, "action_dispatch/system_test_case"
end
-autoload :Mime, 'action_dispatch/http/mime_type'
+autoload :Mime, "action_dispatch/http/mime_type"
ActiveSupport.on_load(:action_view) do
ActionView::Base.default_formats ||= Mime::SET.symbols
diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb
index cc1cb3f0f0..f67b13f657 100644
--- a/actionpack/lib/action_dispatch/http/cache.rb
+++ b/actionpack/lib/action_dispatch/http/cache.rb
@@ -1,26 +1,24 @@
+# frozen_string_literal: true
module ActionDispatch
module Http
module Cache
module Request
-
- HTTP_IF_MODIFIED_SINCE = 'HTTP_IF_MODIFIED_SINCE'.freeze
- HTTP_IF_NONE_MATCH = 'HTTP_IF_NONE_MATCH'.freeze
+ HTTP_IF_MODIFIED_SINCE = "HTTP_IF_MODIFIED_SINCE"
+ HTTP_IF_NONE_MATCH = "HTTP_IF_NONE_MATCH"
def if_modified_since
- if since = env[HTTP_IF_MODIFIED_SINCE]
+ if since = get_header(HTTP_IF_MODIFIED_SINCE)
Time.rfc2822(since) rescue nil
end
end
def if_none_match
- env[HTTP_IF_NONE_MATCH]
+ get_header HTTP_IF_NONE_MATCH
end
def if_none_match_etags
- (if_none_match ? if_none_match.split(/\s*,\s*/) : []).collect do |etag|
- etag.gsub(/^\"|\"$/, "")
- end
+ if_none_match ? if_none_match.split(/\s*,\s*/) : []
end
def not_modified?(modified_at)
@@ -29,8 +27,8 @@ module ActionDispatch
def etag_matches?(etag)
if etag
- etag = etag.gsub(/^\"|\"$/, "")
- if_none_match_etags.include?(etag)
+ validators = if_none_match_etags
+ validators.include?(etag) || validators.include?("*")
end
end
@@ -51,53 +49,96 @@ module ActionDispatch
end
module Response
- attr_reader :cache_control, :etag
- alias :etag? :etag
+ attr_reader :cache_control
def last_modified
- if last = headers[LAST_MODIFIED]
+ if last = get_header(LAST_MODIFIED)
Time.httpdate(last)
end
end
def last_modified?
- headers.include?(LAST_MODIFIED)
+ has_header? LAST_MODIFIED
end
def last_modified=(utc_time)
- headers[LAST_MODIFIED] = utc_time.httpdate
+ set_header LAST_MODIFIED, utc_time.httpdate
end
def date
- if date_header = headers[DATE]
+ if date_header = get_header(DATE)
Time.httpdate(date_header)
end
end
def date?
- headers.include?(DATE)
+ has_header? DATE
end
def date=(utc_time)
- headers[DATE] = utc_time.httpdate
+ set_header DATE, utc_time.httpdate
end
- def etag=(etag)
- key = ActiveSupport::Cache.expand_cache_key(etag)
- @etag = self[ETAG] = %("#{Digest::MD5.hexdigest(key)}")
+ # This method sets a weak ETag validator on the response so browsers
+ # and proxies may cache the response, keyed on the ETag. On subsequent
+ # requests, the If-None-Match header is set to the cached ETag. If it
+ # matches the current ETag, we can return a 304 Not Modified response
+ # with no body, letting the browser or proxy know that their cache is
+ # current. Big savings in request time and network bandwidth.
+ #
+ # Weak ETags are considered to be semantically equivalent but not
+ # byte-for-byte identical. This is perfect for browser caching of HTML
+ # pages where we don't care about exact equality, just what the user
+ # is viewing.
+ #
+ # Strong ETags are considered byte-for-byte identical. They allow a
+ # browser or proxy cache to support Range requests, useful for paging
+ # through a PDF file or scrubbing through a video. Some CDNs only
+ # support strong ETags and will ignore weak ETags entirely.
+ #
+ # Weak ETags are what we almost always need, so they're the default.
+ # Check out #strong_etag= to provide a strong ETag validator.
+ def etag=(weak_validators)
+ self.weak_etag = weak_validators
+ end
+
+ def weak_etag=(weak_validators)
+ set_header "ETag", generate_weak_etag(weak_validators)
+ end
+
+ def strong_etag=(strong_validators)
+ set_header "ETag", generate_strong_etag(strong_validators)
+ end
+
+ def etag?; etag; end
+
+ # True if an ETag is set and it's a weak validator (preceded with W/)
+ def weak_etag?
+ etag? && etag.starts_with?('W/"')
+ end
+
+ # True if an ETag is set and it isn't a weak validator (not preceded with W/)
+ def strong_etag?
+ etag? && !weak_etag?
end
private
- DATE = 'Date'.freeze
- LAST_MODIFIED = "Last-Modified".freeze
- ETAG = "ETag".freeze
- CACHE_CONTROL = "Cache-Control".freeze
- SPECIAL_KEYS = Set.new(%w[extras no-cache max-age public must-revalidate])
+ DATE = "Date"
+ LAST_MODIFIED = "Last-Modified"
+ SPECIAL_KEYS = Set.new(%w[extras no-cache max-age public private must-revalidate])
+
+ def generate_weak_etag(validators)
+ "W/#{generate_strong_etag(validators)}"
+ end
+
+ def generate_strong_etag(validators)
+ %("#{ActiveSupport::Digest.hexdigest(ActiveSupport::Cache.expand_cache_key(validators))}")
+ end
def cache_control_segments
- if cache_control = self[CACHE_CONTROL]
- cache_control.delete(' ').split(',')
+ if cache_control = _cache_control
+ cache_control.delete(" ").split(",")
else
[]
end
@@ -107,10 +148,10 @@ module ActionDispatch
cache_control = {}
cache_control_segments.each do |segment|
- directive, argument = segment.split('=', 2)
+ directive, argument = segment.split("=", 2)
if SPECIAL_KEYS.include? directive
- key = directive.tr('-', '_')
+ key = directive.tr("-", "_")
cache_control[key.to_sym] = argument || true
else
cache_control[:extras] ||= []
@@ -123,51 +164,58 @@ module ActionDispatch
def prepare_cache_control!
@cache_control = cache_control_headers
- @etag = self[ETAG]
end
+ DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate"
+ NO_CACHE = "no-cache"
+ PUBLIC = "public"
+ PRIVATE = "private"
+ MUST_REVALIDATE = "must-revalidate"
+
def handle_conditional_get!
- if etag? || last_modified? || !@cache_control.empty?
- set_conditional_cache_control!
+ # Normally default cache control setting is handled by ETag
+ # middleware. But, if an etag is already set, the middleware
+ # defaults to `no-cache` unless a default `Cache-Control` value is
+ # previously set. So, set a default one here.
+ if (etag? || last_modified?) && !self._cache_control
+ self._cache_control = DEFAULT_CACHE_CONTROL
end
end
- DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate".freeze
- NO_CACHE = "no-cache".freeze
- PUBLIC = "public".freeze
- PRIVATE = "private".freeze
- MUST_REVALIDATE = "must-revalidate".freeze
-
- def set_conditional_cache_control!
+ def merge_and_normalize_cache_control!(cache_control)
control = {}
cc_headers = cache_control_headers
if extras = cc_headers.delete(:extras)
- @cache_control[:extras] ||= []
- @cache_control[:extras] += extras
- @cache_control[:extras].uniq!
+ cache_control[:extras] ||= []
+ cache_control[:extras] += extras
+ cache_control[:extras].uniq!
end
control.merge! cc_headers
- control.merge! @cache_control
+ control.merge! cache_control
if control.empty?
- self[CACHE_CONTROL] = DEFAULT_CACHE_CONTROL
+ # Let middleware handle default behavior
elsif control[:no_cache]
- self[CACHE_CONTROL] = NO_CACHE
+ self._cache_control = NO_CACHE
if control[:extras]
- self[CACHE_CONTROL] += ", #{control[:extras].join(', ')}"
+ self._cache_control = _cache_control + ", #{control[:extras].join(', ')}"
end
else
- extras = control[:extras]
+ extras = control[:extras]
max_age = control[:max_age]
+ stale_while_revalidate = control[:stale_while_revalidate]
+ stale_if_error = control[:stale_if_error]
options = []
options << "max-age=#{max_age.to_i}" if max_age
options << (control[:public] ? PUBLIC : PRIVATE)
options << MUST_REVALIDATE if control[:must_revalidate]
+ options << "stale-while-revalidate=#{stale_while_revalidate.to_i}" if stale_while_revalidate
+ options << "stale-if-error=#{stale_if_error.to_i}" if stale_if_error
options.concat(extras) if extras
- self[CACHE_CONTROL] = options.join(", ")
+ self._cache_control = options.join(", ")
end
end
end
diff --git a/actionpack/lib/action_dispatch/http/content_disposition.rb b/actionpack/lib/action_dispatch/http/content_disposition.rb
new file mode 100644
index 0000000000..58164c1522
--- /dev/null
+++ b/actionpack/lib/action_dispatch/http/content_disposition.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ module Http
+ class ContentDisposition # :nodoc:
+ def self.format(disposition:, filename:)
+ new(disposition: disposition, filename: filename).to_s
+ end
+
+ attr_reader :disposition, :filename
+
+ def initialize(disposition:, filename:)
+ @disposition = disposition
+ @filename = filename
+ end
+
+ TRADITIONAL_ESCAPED_CHAR = /[^ A-Za-z0-9!#$+.^_`|~-]/
+
+ def ascii_filename
+ 'filename="' + percent_escape(I18n.transliterate(filename), TRADITIONAL_ESCAPED_CHAR) + '"'
+ end
+
+ RFC_5987_ESCAPED_CHAR = /[^A-Za-z0-9!#$&+.^_`|~-]/
+
+ def utf8_filename
+ "filename*=UTF-8''" + percent_escape(filename, RFC_5987_ESCAPED_CHAR)
+ end
+
+ def to_s
+ if filename
+ "#{disposition}; #{ascii_filename}; #{utf8_filename}"
+ else
+ "#{disposition}"
+ end
+ end
+
+ private
+ def percent_escape(string, pattern)
+ string.gsub(pattern) do |char|
+ char.bytes.map { |byte| "%%%02X" % byte }.join
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/http/content_security_policy.rb b/actionpack/lib/action_dispatch/http/content_security_policy.rb
new file mode 100644
index 0000000000..b1e5a28be5
--- /dev/null
+++ b/actionpack/lib/action_dispatch/http/content_security_policy.rb
@@ -0,0 +1,273 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/object/deep_dup"
+
+module ActionDispatch #:nodoc:
+ class ContentSecurityPolicy
+ class Middleware
+ CONTENT_TYPE = "Content-Type"
+ POLICY = "Content-Security-Policy"
+ POLICY_REPORT_ONLY = "Content-Security-Policy-Report-Only"
+
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ request = ActionDispatch::Request.new env
+ _, headers, _ = response = @app.call(env)
+
+ return response unless html_response?(headers)
+ return response if policy_present?(headers)
+
+ if policy = request.content_security_policy
+ nonce = request.content_security_policy_nonce
+ context = request.controller_instance || request
+ headers[header_name(request)] = policy.build(context, nonce)
+ end
+
+ response
+ end
+
+ private
+
+ def html_response?(headers)
+ if content_type = headers[CONTENT_TYPE]
+ content_type =~ /html/
+ end
+ end
+
+ def header_name(request)
+ if request.content_security_policy_report_only
+ POLICY_REPORT_ONLY
+ else
+ POLICY
+ end
+ end
+
+ def policy_present?(headers)
+ headers[POLICY] || headers[POLICY_REPORT_ONLY]
+ end
+ end
+
+ module Request
+ POLICY = "action_dispatch.content_security_policy"
+ POLICY_REPORT_ONLY = "action_dispatch.content_security_policy_report_only"
+ NONCE_GENERATOR = "action_dispatch.content_security_policy_nonce_generator"
+ NONCE = "action_dispatch.content_security_policy_nonce"
+
+ def content_security_policy
+ get_header(POLICY)
+ end
+
+ def content_security_policy=(policy)
+ set_header(POLICY, policy)
+ end
+
+ def content_security_policy_report_only
+ get_header(POLICY_REPORT_ONLY)
+ end
+
+ def content_security_policy_report_only=(value)
+ set_header(POLICY_REPORT_ONLY, value)
+ end
+
+ def content_security_policy_nonce_generator
+ get_header(NONCE_GENERATOR)
+ end
+
+ def content_security_policy_nonce_generator=(generator)
+ set_header(NONCE_GENERATOR, generator)
+ end
+
+ def content_security_policy_nonce
+ if content_security_policy_nonce_generator
+ if nonce = get_header(NONCE)
+ nonce
+ else
+ set_header(NONCE, generate_content_security_policy_nonce)
+ end
+ end
+ end
+
+ private
+
+ def generate_content_security_policy_nonce
+ content_security_policy_nonce_generator.call(self)
+ end
+ end
+
+ MAPPINGS = {
+ self: "'self'",
+ unsafe_eval: "'unsafe-eval'",
+ unsafe_inline: "'unsafe-inline'",
+ none: "'none'",
+ http: "http:",
+ https: "https:",
+ data: "data:",
+ mediastream: "mediastream:",
+ blob: "blob:",
+ filesystem: "filesystem:",
+ report_sample: "'report-sample'",
+ strict_dynamic: "'strict-dynamic'",
+ ws: "ws:",
+ wss: "wss:"
+ }.freeze
+
+ DIRECTIVES = {
+ base_uri: "base-uri",
+ child_src: "child-src",
+ connect_src: "connect-src",
+ default_src: "default-src",
+ font_src: "font-src",
+ form_action: "form-action",
+ frame_ancestors: "frame-ancestors",
+ frame_src: "frame-src",
+ img_src: "img-src",
+ manifest_src: "manifest-src",
+ media_src: "media-src",
+ object_src: "object-src",
+ prefetch_src: "prefetch-src",
+ script_src: "script-src",
+ style_src: "style-src",
+ worker_src: "worker-src"
+ }.freeze
+
+ NONCE_DIRECTIVES = %w[script-src style-src].freeze
+
+ private_constant :MAPPINGS, :DIRECTIVES, :NONCE_DIRECTIVES
+
+ attr_reader :directives
+
+ def initialize
+ @directives = {}
+ yield self if block_given?
+ end
+
+ def initialize_copy(other)
+ @directives = other.directives.deep_dup
+ end
+
+ DIRECTIVES.each do |name, directive|
+ define_method(name) do |*sources|
+ if sources.first
+ @directives[directive] = apply_mappings(sources)
+ else
+ @directives.delete(directive)
+ end
+ end
+ end
+
+ def block_all_mixed_content(enabled = true)
+ if enabled
+ @directives["block-all-mixed-content"] = true
+ else
+ @directives.delete("block-all-mixed-content")
+ end
+ end
+
+ def plugin_types(*types)
+ if types.first
+ @directives["plugin-types"] = types
+ else
+ @directives.delete("plugin-types")
+ end
+ end
+
+ def report_uri(uri)
+ @directives["report-uri"] = [uri]
+ end
+
+ def require_sri_for(*types)
+ if types.first
+ @directives["require-sri-for"] = types
+ else
+ @directives.delete("require-sri-for")
+ end
+ end
+
+ def sandbox(*values)
+ if values.empty?
+ @directives["sandbox"] = true
+ elsif values.first
+ @directives["sandbox"] = values
+ else
+ @directives.delete("sandbox")
+ end
+ end
+
+ def upgrade_insecure_requests(enabled = true)
+ if enabled
+ @directives["upgrade-insecure-requests"] = true
+ else
+ @directives.delete("upgrade-insecure-requests")
+ end
+ end
+
+ def build(context = nil, nonce = nil)
+ build_directives(context, nonce).compact.join("; ")
+ end
+
+ private
+ def apply_mappings(sources)
+ sources.map do |source|
+ case source
+ when Symbol
+ apply_mapping(source)
+ when String, Proc
+ source
+ else
+ raise ArgumentError, "Invalid content security policy source: #{source.inspect}"
+ end
+ end
+ end
+
+ def apply_mapping(source)
+ MAPPINGS.fetch(source) do
+ raise ArgumentError, "Unknown content security policy source mapping: #{source.inspect}"
+ end
+ end
+
+ def build_directives(context, nonce)
+ @directives.map do |directive, sources|
+ if sources.is_a?(Array)
+ if nonce && nonce_directive?(directive)
+ "#{directive} #{build_directive(sources, context).join(' ')} 'nonce-#{nonce}'"
+ else
+ "#{directive} #{build_directive(sources, context).join(' ')}"
+ end
+ elsif sources
+ directive
+ else
+ nil
+ end
+ end
+ end
+
+ def build_directive(sources, context)
+ sources.map { |source| resolve_source(source, context) }
+ end
+
+ def resolve_source(source, context)
+ case source
+ when String
+ source
+ when Symbol
+ source.to_s
+ when Proc
+ if context.nil?
+ raise RuntimeError, "Missing context for the dynamic content security policy source: #{source.inspect}"
+ else
+ resolved = context.instance_exec(&source)
+ resolved.is_a?(Symbol) ? apply_mapping(resolved) : resolved
+ end
+ else
+ raise RuntimeError, "Unexpected content security policy source: #{source.inspect}"
+ end
+ end
+
+ def nonce_directive?(directive)
+ NONCE_DIRECTIVES.include?(directive)
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb
index 3170389b36..cbb772175c 100644
--- a/actionpack/lib/action_dispatch/http/filter_parameters.rb
+++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb
@@ -1,14 +1,16 @@
-require 'active_support/core_ext/hash/keys'
-require 'active_support/core_ext/object/duplicable'
-require 'action_dispatch/http/parameter_filter'
+# frozen_string_literal: true
+
+require "active_support/parameter_filter"
module ActionDispatch
module Http
# Allows you to specify sensitive parameters which will be replaced from
# the request log by looking in the query string of the request and all
- # sub-hashes of the params hash to filter. If a block is given, each key and
- # value of the params hash and all sub-hashes is passed to it, the value
- # or key can be replaced using String#replace or similar method.
+ # sub-hashes of the params hash to filter. Filtering only certain sub-keys
+ # from a hash is possible by using the dot notation: 'credit_card.number'.
+ # If a block is given, each key and value of the params hash and all
+ # sub-hashes are passed to it, where the value or the key can be replaced using
+ # String#replace or similar methods.
#
# env["action_dispatch.parameter_filter"] = [:password]
# => replaces the value to all keys matching /password/i with "[FILTERED]"
@@ -16,61 +18,67 @@ module ActionDispatch
# env["action_dispatch.parameter_filter"] = [:foo, "bar"]
# => replaces the value to all keys matching /foo|bar/i with "[FILTERED]"
#
+ # env["action_dispatch.parameter_filter"] = [ "credit_card.code" ]
+ # => replaces { credit_card: {code: "xxxx"} } with "[FILTERED]", does not
+ # change { file: { code: "xxxx"} }
+ #
# env["action_dispatch.parameter_filter"] = -> (k, v) do
# v.reverse! if k =~ /secret/i
# end
# => reverses the value to all keys matching /secret/i
module FilterParameters
ENV_MATCH = [/RAW_POST_DATA/, "rack.request.form_vars"] # :nodoc:
- NULL_PARAM_FILTER = ParameterFilter.new # :nodoc:
- NULL_ENV_FILTER = ParameterFilter.new ENV_MATCH # :nodoc:
+ NULL_PARAM_FILTER = ActiveSupport::ParameterFilter.new # :nodoc:
+ NULL_ENV_FILTER = ActiveSupport::ParameterFilter.new ENV_MATCH # :nodoc:
- def initialize(env)
+ def initialize
super
@filtered_parameters = nil
@filtered_env = nil
@filtered_path = nil
end
- # Return a hash of parameters with all sensitive data replaced.
+ # Returns a hash of parameters with all sensitive data replaced.
def filtered_parameters
@filtered_parameters ||= parameter_filter.filter(parameters)
+ rescue ActionDispatch::Http::Parameters::ParseError
+ @filtered_parameters = {}
end
- # Return a hash of request.env with all sensitive data replaced.
+ # Returns a hash of request.env with all sensitive data replaced.
def filtered_env
@filtered_env ||= env_filter.filter(@env)
end
- # Reconstructed a path with all sensitive GET parameters replaced.
+ # Reconstructs a path with all sensitive GET parameters replaced.
def filtered_path
@filtered_path ||= query_string.empty? ? path : "#{path}?#{filtered_query_string}"
end
- protected
+ private
- def parameter_filter
- parameter_filter_for @env.fetch("action_dispatch.parameter_filter") {
+ def parameter_filter # :doc:
+ parameter_filter_for fetch_header("action_dispatch.parameter_filter") {
return NULL_PARAM_FILTER
}
end
- def env_filter
- user_key = @env.fetch("action_dispatch.parameter_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)
- ParameterFilter.new(filters)
+ def parameter_filter_for(filters) # :doc:
+ ActiveSupport::ParameterFilter.new(filters)
end
- KV_RE = '[^&;=]+'
+ 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("=")
+ parameter_filter.filter($1 => $2).first.join("=")
end
end
end
diff --git a/actionpack/lib/action_dispatch/http/filter_redirect.rb b/actionpack/lib/action_dispatch/http/filter_redirect.rb
index bf79963351..8c4e852235 100644
--- a/actionpack/lib/action_dispatch/http/filter_redirect.rb
+++ b/actionpack/lib/action_dispatch/http/filter_redirect.rb
@@ -1,12 +1,12 @@
+# frozen_string_literal: true
+
module ActionDispatch
module Http
module FilterRedirect
-
- FILTERED = '[FILTERED]'.freeze # :nodoc:
+ FILTERED = "[FILTERED]" # :nodoc:
def filtered_location # :nodoc:
- filters = location_filter
- if !filters.empty? && location_filter_match?(filters)
+ if location_filter_match?
FILTERED
else
location
@@ -15,24 +15,23 @@ module ActionDispatch
private
- def location_filter
+ def location_filters
if request
- request.env['action_dispatch.redirect_filter'] || []
+ request.get_header("action_dispatch.redirect_filter") || []
else
[]
end
end
- def location_filter_match?(filters)
- filters.any? do |filter|
+ def location_filter_match?
+ location_filters.any? do |filter|
if String === filter
location.include?(filter)
elsif Regexp === filter
- location.match(filter)
+ location =~ filter
end
end
end
-
end
end
end
diff --git a/actionpack/lib/action_dispatch/http/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb
index bc5410dc38..6c7d24d2d0 100644
--- a/actionpack/lib/action_dispatch/http/headers.rb
+++ b/actionpack/lib/action_dispatch/http/headers.rb
@@ -1,10 +1,26 @@
+# frozen_string_literal: true
+
module ActionDispatch
module Http
# Provides access to the request's HTTP headers from the environment.
#
- # env = { "CONTENT_TYPE" => "text/plain" }
- # headers = ActionDispatch::Http::Headers.new(env)
+ # env = { "CONTENT_TYPE" => "text/plain", "HTTP_USER_AGENT" => "curl/7.43.0" }
+ # headers = ActionDispatch::Http::Headers.from_hash(env)
# headers["Content-Type"] # => "text/plain"
+ # headers["User-Agent"] # => "curl/7.43.0"
+ #
+ # Also note that when headers are mapped to CGI-like variables by the Rack
+ # server, both dashes and underscores are converted to underscores. This
+ # ambiguity cannot be resolved at this stage anymore. Both underscores and
+ # dashes have to be interpreted as if they were originally sent as dashes.
+ #
+ # # GET / HTTP/1.1
+ # # ...
+ # # User-Agent: curl/7.43.0
+ # # X_Custom_Header: token
+ #
+ # headers["X_Custom_Header"] # => nil
+ # headers["X-Custom-Header"] # => "token"
class Headers
CGI_VARIABLES = Set.new(%W[
AUTH_TYPE
@@ -30,27 +46,37 @@ module ActionDispatch
HTTP_HEADER = /\A[A-Za-z0-9-]+\z/
include Enumerable
- attr_reader :env
- def initialize(env = {}) # :nodoc:
- @env = env
+ def self.from_hash(hash)
+ new ActionDispatch::Request.new hash
+ end
+
+ def initialize(request) # :nodoc:
+ @req = request
end
# Returns the value for the given key mapped to @env.
def [](key)
- @env[env_name(key)]
+ @req.get_header env_name(key)
end
# Sets the given value for the key mapped to @env.
def []=(key, value)
- @env[env_name(key)] = value
+ @req.set_header env_name(key), value
+ end
+
+ # Add a value to a multivalued header like Vary or Accept-Encoding.
+ def add(key, value)
+ @req.add_header env_name(key), value
end
def key?(key)
- @env.key? env_name(key)
+ @req.has_header? env_name(key)
end
alias :include? :key?
+ DEFAULT = Object.new # :nodoc:
+
# Returns the value for the given key mapped to @env.
#
# If the key is not found and an optional code block is not provided,
@@ -58,18 +84,22 @@ module ActionDispatch
#
# If the code block is provided, then it will be run and
# its result returned.
- def fetch(key, *args, &block)
- @env.fetch env_name(key), *args, &block
+ def fetch(key, default = DEFAULT)
+ @req.fetch_header(env_name(key)) do
+ return default unless default == DEFAULT
+ return yield if block_given?
+ raise KeyError, key
+ end
end
def each(&block)
- @env.each(&block)
+ @req.each_header(&block)
end
# Returns a new Http::Headers instance containing the contents of
# <tt>headers_or_env</tt> and the original instance.
def merge(headers_or_env)
- headers = Http::Headers.new(env.dup)
+ headers = @req.dup.headers
headers.merge!(headers_or_env)
headers
end
@@ -79,21 +109,24 @@ module ActionDispatch
# <tt>headers_or_env</tt>.
def merge!(headers_or_env)
headers_or_env.each do |key, value|
- self[env_name(key)] = value
+ @req.set_header env_name(key), value
end
end
+ def env; @req.env.dup; end
+
private
- # Converts a HTTP header name to an environment variable name if it is
- # not contained within the headers hash.
- def env_name(key)
- key = key.to_s
- if key =~ HTTP_HEADER
- key = key.upcase.tr('-', '_')
- key = "HTTP_" + key unless CGI_VARIABLES.include?(key)
+
+ # Converts an HTTP header name to an environment variable name if it is
+ # not contained within the headers hash.
+ def env_name(key)
+ key = key.to_s
+ if HTTP_HEADER.match?(key)
+ key = key.upcase.tr("-", "_")
+ key = "HTTP_" + key unless CGI_VARIABLES.include?(key)
+ end
+ key
end
- key
- end
end
end
end
diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
index ff336b7354..498b1e6695 100644
--- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb
+++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
@@ -1,26 +1,30 @@
-require 'active_support/core_ext/module/attribute_accessors'
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/attribute_accessors"
module ActionDispatch
module Http
module MimeNegotiation
extend ActiveSupport::Concern
+ RESCUABLE_MIME_FORMAT_ERRORS = [
+ ActionController::BadRequest,
+ ActionDispatch::Http::Parameters::ParseError,
+ ]
+
included do
- mattr_accessor :ignore_accept_header
- self.ignore_accept_header = false
+ mattr_accessor :ignore_accept_header, default: false
end
- # The MIME type of the HTTP request, such as Mime::XML.
- #
- # For backward compatibility, the post \format is extracted from the
- # X-Post-Data-Format HTTP header if present.
+ # The MIME type of the HTTP request, such as Mime[:xml].
def content_mime_type
- @env["action_dispatch.request.content_type"] ||= begin
- if @env['CONTENT_TYPE'] =~ /^([^,\;]*)/
+ fetch_header("action_dispatch.request.content_type") do |k|
+ v = if get_header("CONTENT_TYPE") =~ /^([^,\;]*)/
Mime::Type.lookup($1.strip.downcase)
else
nil
end
+ set_header k, v
end
end
@@ -28,46 +32,54 @@ module ActionDispatch
content_mime_type && content_mime_type.to_s
end
+ def has_content_type? # :nodoc:
+ get_header "CONTENT_TYPE"
+ end
+
# Returns the accepted MIME type for the request.
def accepts
- @env["action_dispatch.request.accepts"] ||= begin
- header = @env['HTTP_ACCEPT'].to_s.strip
+ fetch_header("action_dispatch.request.accepts") do |k|
+ header = get_header("HTTP_ACCEPT").to_s.strip
- if header.empty?
+ v = if header.empty?
[content_mime_type]
else
Mime::Type.parse(header)
end
+ set_header k, v
end
end
# Returns the MIME type for the \format used in the request.
#
- # GET /posts/5.xml | request.format => Mime::XML
- # GET /posts/5.xhtml | request.format => Mime::HTML
- # GET /posts/5 | request.format => Mime::HTML or MIME::JS, or request.accepts.first
+ # GET /posts/5.xml | request.format => Mime[:xml]
+ # GET /posts/5.xhtml | request.format => Mime[:html]
+ # GET /posts/5 | request.format => Mime[:html] or Mime[:js], or request.accepts.first
#
def format(view_path = [])
formats.first || Mime::NullType.instance
end
def formats
- @env["action_dispatch.request.formats"] ||= begin
+ fetch_header("action_dispatch.request.formats") do |k|
params_readable = begin
parameters[:format]
- rescue ActionController::BadRequest
+ rescue *RESCUABLE_MIME_FORMAT_ERRORS
false
end
- if params_readable
+ v = if params_readable
Array(Mime[parameters[:format]])
elsif use_accept_header && valid_accept_header
accepts
+ elsif extension_format = format_from_path_extension
+ [extension_format]
elsif xhr?
- [Mime::JS]
+ [Mime[:js]]
else
- [Mime::HTML]
+ [Mime[:html]]
end
+ set_header k, v
end
end
@@ -78,10 +90,7 @@ module ActionDispatch
if variant.all? { |v| v.is_a?(Symbol) }
@variant = ActiveSupport::ArrayInquirer.new(variant)
else
- raise ArgumentError, "request.variant must be set to a Symbol or an Array of Symbols. " \
- "For security reasons, never directly set the variant to a user-provided value, " \
- "like params[:variant].to_sym. Check user-provided value against a whitelist first, " \
- "then set the variant: request.variant = :tablet if params[:variant] == 'tablet'"
+ raise ArgumentError, "request.variant must be set to a Symbol or an Array of Symbols."
end
end
@@ -102,7 +111,7 @@ module ActionDispatch
# end
def format=(extension)
parameters[:format] = extension.to_s
- @env["action_dispatch.request.formats"] = [Mime::Type.lookup_by_extension(parameters[:format])]
+ set_header "action_dispatch.request.formats", [Mime::Type.lookup_by_extension(parameters[:format])]
end
# Sets the \formats by string extensions. This differs from #format= by allowing you
@@ -121,14 +130,12 @@ module ActionDispatch
# end
def formats=(extensions)
parameters[:format] = extensions.first.to_s
- @env["action_dispatch.request.formats"] = extensions.collect do |extension|
+ set_header "action_dispatch.request.formats", extensions.collect { |extension|
Mime::Type.lookup_by_extension(extension)
- end
+ }
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
@@ -141,18 +148,25 @@ module ActionDispatch
order.include?(Mime::ALL) ? format : nil
end
- protected
+ private
- BROWSER_LIKE_ACCEPTS = /,\s*\*\/\*|\*\/\*\s*,/
+ BROWSER_LIKE_ACCEPTS = /,\s*\*\/\*|\*\/\*\s*,/
- def valid_accept_header
- (xhr? && (accept.present? || content_mime_type)) ||
- (accept.present? && accept !~ BROWSER_LIKE_ACCEPTS)
- end
+ def valid_accept_header # :doc:
+ (xhr? && (accept.present? || content_mime_type)) ||
+ (accept.present? && accept !~ BROWSER_LIKE_ACCEPTS)
+ end
- def use_accept_header
- !self.class.ignore_accept_header
- end
+ def use_accept_header # :doc:
+ !self.class.ignore_accept_header
+ end
+
+ 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]
+ end
+ end
end
end
end
diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb
index 6a6e23d535..c3e0ea3c89 100644
--- a/actionpack/lib/action_dispatch/http/mime_type.rb
+++ b/actionpack/lib/action_dispatch/http/mime_type.rb
@@ -1,29 +1,38 @@
-require 'set'
-require 'singleton'
-require 'active_support/core_ext/module/attribute_accessors'
-require 'active_support/core_ext/string/starts_ends_with'
+# frozen_string_literal: true
+
+require "singleton"
+require "active_support/core_ext/string/starts_ends_with"
module Mime
- class Mimes < Array
- def symbols
- @symbols ||= map(&:to_sym)
+ class Mimes
+ include Enumerable
+
+ def initialize
+ @mimes = []
+ @symbols = nil
end
- %w(<< concat shift unshift push pop []= clear compact! collect!
- delete delete_at delete_if flatten! map! insert reject! reverse!
- replace slice! sort! uniq!).each do |method|
- module_eval <<-CODE, __FILE__, __LINE__ + 1
- def #{method}(*)
- @symbols = nil
- super
- end
- CODE
+ def each
+ @mimes.each { |x| yield x }
+ end
+
+ def <<(type)
+ @mimes << type
+ @symbols = nil
+ end
+
+ def delete_if
+ @mimes.delete_if { |x| yield x }.tap { @symbols = nil }
+ end
+
+ def symbols
+ @symbols ||= map(&:to_sym)
end
end
SET = Mimes.new
EXTENSION_LOOKUP = {}
- LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) unless k.blank? }
+ LOOKUP = {}
class << self
def [](type)
@@ -37,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
@@ -45,20 +54,17 @@ module Mime
#
# respond_to do |format|
# format.html
- # format.ics { render text: @post.to_ics, mime_type: Mime::Type.lookup("text/calendar") }
+ # format.ics { render body: @post.to_ics, mime_type: Mime::Type.lookup("text/calendar") }
# format.xml { render xml: @post }
# end
# end
# end
class Type
- @@html_types = Set.new [:html, :all]
- cattr_reader :html_types
-
attr_reader :symbol
@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
@@ -66,7 +72,7 @@ module Mime
def initialize(index, name, q = nil)
@index = index
@name = name
- q ||= 0.0 if @name == Mime::ALL.to_s # default wildcard match to end of list
+ q ||= 0.0 if @name == "*/*" # Default wildcard match to end of list.
@q = ((q || 1.0).to_f * 100).to_i
end
@@ -75,70 +81,58 @@ module Mime
result = @index <=> item.index if result == 0
result
end
-
- def ==(item)
- @name == item.to_s
- end
end
- class AcceptList < Array #:nodoc:
- def assort!
- sort!
+ class AcceptList #:nodoc:
+ def self.sort!(list)
+ list.sort!
- # Take care of the broken text/xml entry by renaming or deleting it
+ 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.
if text_xml_idx && app_xml_idx
- app_xml.q = [text_xml.q, app_xml.q].max # set the q value to the max of the two
- exchange_xml_items if app_xml_idx > text_xml_idx # make sure app_xml is ahead of text_xml in the list
- delete_at(text_xml_idx) # delete text_xml from the list
+ 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.
+ 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.
elsif text_xml_idx
- text_xml.name = Mime::XML.to_s
+ 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
- while idx < length
- type = self[idx]
+ while idx < list.length
+ type = list[idx]
break if type.q < app_xml.q
- if type.name.ends_with? '+xml'
- self[app_xml_idx], self[idx] = self[idx], app_xml
- @app_xml_idx = idx
+ if type.name.ends_with? "+xml"
+ list[app_xml_idx], list[idx] = list[idx], app_xml
+ app_xml_idx = idx
end
idx += 1
end
end
- map! { |i| Mime::Type.lookup(i.name) }.uniq!
- to_a
+ list.map! { |i| Mime::Type.lookup(i.name) }.uniq!
+ list
end
- private
- def text_xml_idx
- @text_xml_idx ||= index('text/xml')
- end
-
- def app_xml_idx
- @app_xml_idx ||= index(Mime::XML.to_s)
- end
-
- def text_xml
- self[text_xml_idx]
- end
-
- def app_xml
- self[app_xml_idx]
- end
-
- def exchange_xml_items
- self[app_xml_idx], self[text_xml_idx] = text_xml, app_xml
- @app_xml_idx, @text_xml_idx = text_xml_idx, app_xml_idx
- end
+ def self.find_item_by_name(array, name)
+ array.index { |item| item.name == name }
+ end
end
class << self
- TRAILING_STAR_REGEXP = /(text|application)\/\*/
+ TRAILING_STAR_REGEXP = /^(text|application)\/\*/
PARAMETER_SEPARATOR_REGEXP = /;\s*\w+="?\w+"?/
def register_callback(&block)
@@ -146,53 +140,54 @@ module Mime
end
def lookup(string)
- LOOKUP[string]
+ LOOKUP[string] || Type.new(string)
end
def lookup_by_extension(extension)
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)
end
def register(string, symbol, mime_type_synonyms = [], extension_synonyms = [], skip_lookup = false)
- Mime.const_set(symbol.upcase, Type.new(string, symbol, mime_type_synonyms))
+ new_mime = Type.new(string, symbol, mime_type_synonyms)
- new_mime = Mime.const_get(symbol.upcase)
SET << new_mime
- ([string] + mime_type_synonyms).each { |str| LOOKUP[str] = SET.last } unless skip_lookup
- ([symbol] + extension_synonyms).each { |ext| EXTENSION_LOOKUP[ext.to_s] = SET.last }
+ ([string] + mime_type_synonyms).each { |str| LOOKUP[str] = new_mime } unless skip_lookup
+ ([symbol] + extension_synonyms).each { |ext| EXTENSION_LOOKUP[ext.to_s] = new_mime }
@register_callbacks.each do |callback|
callback.call(new_mime)
end
+ new_mime
end
def parse(accept_header)
- if !accept_header.include?(',')
+ if !accept_header.include?(",")
accept_header = accept_header.split(PARAMETER_SEPARATOR_REGEXP).first
parse_trailing_star(accept_header) || [Mime::Type.lookup(accept_header)].compact
else
- list, index = AcceptList.new, 0
- accept_header.split(',').each do |header|
+ list, index = [], 0
+ accept_header.split(",").each do |header|
params, q = header.split(PARAMETER_SEPARATOR_REGEXP)
- if params.present?
- params.strip!
- params = parse_trailing_star(params) || [params]
+ next unless params
+ params.strip!
+ next if params.empty?
- params.each do |m|
- list << AcceptItem.new(index, m.to_s, q)
- index += 1
- end
+ params = parse_trailing_star(params) || [params]
+
+ params.each do |m|
+ list << AcceptItem.new(index, m.to_s, q)
+ index += 1
end
end
- list.assort!
+ AcceptList.sort! list
end
end
@@ -200,13 +195,13 @@ module Mime
parse_data_with_trailing_star($1) if accept_header =~ TRAILING_STAR_REGEXP
end
- # For an input of <tt>'text'</tt>, returns <tt>[Mime::JSON, Mime::XML, Mime::ICS,
- # Mime::HTML, Mime::CSS, Mime::CSV, Mime::JS, Mime::YAML, Mime::TEXT]</tt>.
+ # For an input of <tt>'text'</tt>, returns <tt>[Mime[:json], Mime[:xml], Mime[:ics],
+ # Mime[:html], Mime[:css], Mime[:csv], Mime[:js], Mime[:yaml], Mime[:text]</tt>.
#
- # For an input of <tt>'application'</tt>, returns <tt>[Mime::HTML, Mime::JS,
- # Mime::XML, Mime::YAML, Mime::ATOM, Mime::JSON, Mime::RSS, Mime::URL_ENCODED_FORM]</tt>.
- def parse_data_with_trailing_star(input)
- Mime::SET.select { |m| m =~ input }
+ # For an input of <tt>'application'</tt>, returns <tt>[Mime[:html], Mime[:js],
+ # Mime[:xml], Mime[:yaml], Mime[:atom], Mime[:json], Mime[:rss], Mime[:url_encoded_form]</tt>.
+ def parse_data_with_trailing_star(type)
+ Mime::SET.select { |m| m =~ type }
end
# This method is opposite of register method.
@@ -215,19 +210,21 @@ module Mime
#
# Mime::Type.unregister(:mobile)
def unregister(symbol)
- symbol = symbol.upcase
- mime = Mime.const_get(symbol)
- Mime.instance_eval { remove_const(symbol) }
-
- SET.delete_if { |v| v.eql?(mime) }
- LOOKUP.delete_if { |_,v| v.eql?(mime) }
- EXTENSION_LOOKUP.delete_if { |_,v| v.eql?(mime) }
+ symbol = symbol.downcase
+ if mime = Mime[symbol]
+ SET.delete_if { |v| v.eql?(mime) }
+ LOOKUP.delete_if { |_, v| v.eql?(mime) }
+ EXTENSION_LOOKUP.delete_if { |_, v| v.eql?(mime) }
+ end
end
end
+ attr_reader :hash
+
def initialize(string, symbol = nil, synonyms = [])
@symbol, @synonyms = symbol, synonyms
@string = string
+ @hash = [@string, @synonyms, @symbol].hash
end
def to_s
@@ -243,7 +240,7 @@ module Mime
end
def ref
- to_sym || to_s
+ symbol || to_s
end
def ===(list)
@@ -255,43 +252,69 @@ module Mime
end
def ==(mime_type)
- return false if mime_type.blank?
+ return false unless mime_type
(@synonyms + [ self ]).any? do |synonym|
synonym.to_s == mime_type.to_s || synonym.to_sym == mime_type.to_sym
end
end
+ def eql?(other)
+ super || (self.class == other.class &&
+ @string == other.string &&
+ @synonyms == other.synonyms &&
+ @symbol == other.symbol)
+ end
+
def =~(mime_type)
- return false if mime_type.blank?
+ return false unless mime_type
regexp = Regexp.new(Regexp.quote(mime_type.to_s))
- (@synonyms + [ self ]).any? do |synonym|
- synonym.to_s =~ regexp
- end
+ @synonyms.any? { |synonym| synonym.to_s =~ regexp } || @string =~ regexp
end
def html?
- @@html_types.include?(to_sym) || @string =~ /html/
+ symbol == :html || @string =~ /html/
end
+ def all?; false; end
+
+ protected
+
+ attr_reader :string, :synonyms
private
- def to_ary; end
- def to_a; end
+ def to_ary; end
+ def to_a; end
- def method_missing(method, *args)
- if method.to_s.ends_with? '?'
- method[0..-2].downcase.to_sym == to_sym
- else
- super
+ def method_missing(method, *args)
+ if method.to_s.ends_with? "?"
+ method[0..-2].downcase.to_sym == to_sym
+ else
+ super
+ end
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
+
+ class AllType < Type
+ include Singleton
+
+ def initialize
+ super "*/*", :all
end
+
+ def all?; true; end
+ def html?; true; end
end
+ # ALL isn't a real MIME type, so we don't register it for lookup with the
+ # other concrete types. It's a wildcard match that we use for `respond_to`
+ # negotiation internals.
+ ALL = AllType.instance
+
class NullType
include Singleton
@@ -301,15 +324,15 @@ module Mime
def ref; end
- def respond_to_missing?(method, include_private = false)
- method.to_s.ends_with? '?'
- end
-
private
- def method_missing(method, *args)
- false if method.to_s.ends_with? '?'
- end
+ def respond_to_missing?(method, _)
+ method.to_s.ends_with? "?"
+ end
+
+ def method_missing(method, *args)
+ false if method.to_s.ends_with? "?"
+ end
end
end
-require 'action_dispatch/http/mime_types'
+require "action_dispatch/http/mime_types"
diff --git a/actionpack/lib/action_dispatch/http/mime_types.rb b/actionpack/lib/action_dispatch/http/mime_types.rb
index 01a10c693b..342e6de312 100644
--- a/actionpack/lib/action_dispatch/http/mime_types.rb
+++ b/actionpack/lib/action_dispatch/http/mime_types.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
# Build list of Mime types for HTTP responses
-# http://www.iana.org/assignments/media-types/
+# https://www.iana.org/assignments/media-types/
Mime::Type.register "text/html", :html, %w( application/xhtml+xml ), %w( xhtml )
Mime::Type.register "text/plain", :text, [], %w(txt)
@@ -8,29 +10,41 @@ Mime::Type.register "text/css", :css
Mime::Type.register "text/calendar", :ics
Mime::Type.register "text/csv", :csv
Mime::Type.register "text/vcard", :vcf
+Mime::Type.register "text/vtt", :vtt, %w(vtt)
Mime::Type.register "image/png", :png, [], %w(png)
Mime::Type.register "image/jpeg", :jpeg, [], %w(jpg jpeg jpe pjpeg)
Mime::Type.register "image/gif", :gif, [], %w(gif)
Mime::Type.register "image/bmp", :bmp, [], %w(bmp)
Mime::Type.register "image/tiff", :tiff, [], %w(tif tiff)
+Mime::Type.register "image/svg+xml", :svg
Mime::Type.register "video/mpeg", :mpeg, [], %w(mpg mpeg mpe)
+Mime::Type.register "audio/mpeg", :mp3, [], %w(mp1 mp2 mp3)
+Mime::Type.register "audio/ogg", :ogg, [], %w(oga ogg spx opus)
+Mime::Type.register "audio/aac", :m4a, %w( audio/mp4 ), %w(m4a mpg4 aac)
+
+Mime::Type.register "video/webm", :webm, [], %w(webm)
+Mime::Type.register "video/mp4", :mp4, [], %w(mp4 m4v)
+
+Mime::Type.register "font/otf", :otf, [], %w(otf)
+Mime::Type.register "font/ttf", :ttf, [], %w(ttf)
+Mime::Type.register "font/woff", :woff, [], %w(woff)
+Mime::Type.register "font/woff2", :woff2, [], %w(woff2)
+
Mime::Type.register "application/xml", :xml, %w( text/xml application/x-xml )
Mime::Type.register "application/rss+xml", :rss
Mime::Type.register "application/atom+xml", :atom
-Mime::Type.register "application/x-yaml", :yaml, %w( text/yaml )
+Mime::Type.register "application/x-yaml", :yaml, %w( text/yaml ), %w(yml yaml)
Mime::Type.register "multipart/form-data", :multipart_form
Mime::Type.register "application/x-www-form-urlencoded", :url_encoded_form
-# http://www.ietf.org/rfc/rfc4627.txt
+# https://www.ietf.org/rfc/rfc4627.txt
# http://www.json.org/JSONRequest.html
-Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest application/vnd.api+json )
+Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest )
Mime::Type.register "application/pdf", :pdf, [], %w(pdf)
Mime::Type.register "application/zip", :zip, [], %w(zip)
-
-# Create Mime::ALL but do not add it to the SET.
-Mime::ALL = Mime::Type.new("*/*", :all, [])
+Mime::Type.register "application/gzip", :gzip, %w(application/x-gzip), %w(gz)
diff --git a/actionpack/lib/action_dispatch/http/parameter_filter.rb b/actionpack/lib/action_dispatch/http/parameter_filter.rb
index 6e058b829e..ddeb3d81e2 100644
--- a/actionpack/lib/action_dispatch/http/parameter_filter.rb
+++ b/actionpack/lib/action_dispatch/http/parameter_filter.rb
@@ -1,82 +1,12 @@
-module ActionDispatch
- module Http
- class ParameterFilter
- FILTERED = '[FILTERED]'.freeze # :nodoc:
-
- def initialize(filters = [])
- @filters = filters
- end
-
- def filter(params)
- compiled_filter.call(params)
- end
-
- private
-
- def compiled_filter
- @compiled_filter ||= CompiledFilter.compile(@filters)
- end
-
- class CompiledFilter # :nodoc:
- def self.compile(filters)
- return lambda { |params| params.dup } if filters.empty?
-
- strings, regexps, blocks = [], [], []
-
- filters.each do |item|
- case item
- when Proc
- blocks << item
- when Regexp
- regexps << item
- else
- strings << Regexp.escape(item.to_s)
- end
- end
+# frozen_string_literal: true
- deep_regexps, regexps = regexps.partition { |r| r.to_s.include?("\\.") }
- deep_strings, strings = strings.partition { |s| s.include?("\\.") }
+require "active_support/deprecation/constant_accessor"
+require "active_support/parameter_filter"
- regexps << Regexp.new(strings.join('|'), true) unless strings.empty?
- deep_regexps << Regexp.new(deep_strings.join('|'), true) unless deep_strings.empty?
-
- new regexps, deep_regexps, blocks
- end
-
- attr_reader :regexps, :deep_regexps, :blocks
-
- def initialize(regexps, deep_regexps, blocks)
- @regexps = regexps
- @deep_regexps = deep_regexps.any? ? deep_regexps : nil
- @blocks = blocks
- end
-
- def call(original_params, parents = [])
- filtered_params = {}
-
- original_params.each do |key, value|
- parents.push(key) if deep_regexps
- if regexps.any? { |r| key =~ r }
- value = FILTERED
- elsif deep_regexps && (joined = parents.join('.')) && deep_regexps.any? { |r| joined =~ r }
- value = FILTERED
- elsif value.is_a?(Hash)
- value = call(value, parents)
- elsif value.is_a?(Array)
- value = value.map { |v| v.is_a?(Hash) ? call(v, parents) : v }
- elsif blocks.any?
- key = key.dup if key.duplicable?
- value = value.dup if value.duplicable?
- blocks.each { |b| b.call(key, value) }
- end
- parents.pop if deep_regexps
-
- filtered_params[key] = value
- end
-
- filtered_params
- end
- end
- end
+module ActionDispatch
+ module Http
+ include ActiveSupport::Deprecation::DeprecatedConstantAccessor
+ deprecate_constant "ParameterFilter", "ActiveSupport::ParameterFilter",
+ message: "ActionDispatch::Http::ParameterFilter is deprecated and will be removed from Rails 6.1. Use ActiveSupport::ParameterFilter instead."
end
end
diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb
index c2f05ecc86..13d0963a33 100644
--- a/actionpack/lib/action_dispatch/http/parameters.rb
+++ b/actionpack/lib/action_dispatch/http/parameters.rb
@@ -1,27 +1,79 @@
-require 'active_support/core_ext/hash/keys'
-require 'active_support/core_ext/hash/indifferent_access'
+# frozen_string_literal: true
module ActionDispatch
module Http
module Parameters
- PARAMETERS_KEY = 'action_dispatch.request.path_parameters'
+ extend ActiveSupport::Concern
+
+ PARAMETERS_KEY = "action_dispatch.request.path_parameters"
+
+ DEFAULT_PARSERS = {
+ Mime[:json].symbol => -> (raw_post) {
+ data = ActiveSupport::JSON.decode(raw_post)
+ data.is_a?(Hash) ? data : { _json: data }
+ }
+ }
+
+ # Raised when raw data from the request cannot be parsed by the parser
+ # defined for request's content MIME type.
+ class ParseError < StandardError
+ def initialize
+ super($!.message)
+ end
+ end
+
+ included do
+ class << self
+ # Returns the parameter parsers.
+ attr_reader :parameter_parsers
+ end
+
+ self.parameter_parsers = DEFAULT_PARSERS
+ end
+
+ module ClassMethods
+ # 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
# Returns both GET and POST \parameters in a single hash.
def parameters
- @env["action_dispatch.request.parameters"] ||= begin
- params = begin
- request_parameters.merge(query_parameters)
- rescue EOFError
- query_parameters.dup
- end
- params.merge!(path_parameters)
- end
+ params = get_header("action_dispatch.request.parameters")
+ return params if params
+
+ params = begin
+ request_parameters.merge(query_parameters)
+ rescue EOFError
+ query_parameters.dup
+ end
+ params.merge!(path_parameters)
+ params = set_binary_encoding(params, params[:controller], params[:action])
+ set_header("action_dispatch.request.parameters", params)
+ params
end
alias :params :parameters
def path_parameters=(parameters) #:nodoc:
- @env.delete('action_dispatch.request.parameters')
- @env[PARAMETERS_KEY] = parameters
+ delete_header("action_dispatch.request.parameters")
+
+ parameters = set_binary_encoding(parameters, parameters[:controller], parameters[:action])
+ # If any of the path parameters has an invalid encoding then
+ # raise since it's likely to trigger errors further on.
+ Request::Utils.check_param_encoding(parameters)
+
+ set_header PARAMETERS_KEY, parameters
+ rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e
+ raise ActionController::BadRequest.new("Invalid path parameters: #{e.message}")
end
# Returns a hash with the \parameters used to form the \path of the request.
@@ -29,31 +81,56 @@ module ActionDispatch
#
# {'action' => 'my_action', 'controller' => 'my_controller'}
def path_parameters
- @env[PARAMETERS_KEY] ||= {}
+ get_header(PARAMETERS_KEY) || set_header(PARAMETERS_KEY, {})
end
- private
+ private
- # Convert nested Hash to HashWithIndifferentAccess.
- #
- def normalize_encode_params(params)
- case params
- when Hash
- if params.has_key?(:tempfile)
- UploadedFile.new(params)
- else
- params.each_with_object({}) do |(key, val), new_hash|
- new_hash[key] = if val.is_a?(Array)
- val.map! { |el| normalize_encode_params(el) }
- else
- normalize_encode_params(val)
- end
- end.with_indifferent_access
+ def set_binary_encoding(params, controller, action)
+ return params unless controller && controller.valid_encoding?
+
+ if binary_params_for?(controller, action)
+ ActionDispatch::Request::Utils.each_param_value(params) do |param|
+ param.force_encoding ::Encoding::ASCII_8BIT
+ end
end
- else
params
end
- end
+
+ def binary_params_for?(controller, action)
+ controller_class_for(controller).binary_params_for?(action)
+ rescue NameError
+ false
+ end
+
+ def parse_formatted_parameters(parsers)
+ return yield if content_length.zero? || content_mime_type.nil?
+
+ strategy = parsers.fetch(content_mime_type.symbol) { return yield }
+
+ begin
+ strategy.call(raw_post)
+ rescue # JSON or Ruby code block errors.
+ log_parse_error_once
+ raise ParseError
+ end
+ end
+
+ def log_parse_error_once
+ @parse_error_logged ||= begin
+ parse_logger = logger || ActiveSupport::Logger.new($stderr)
+ parse_logger.debug <<~MSG.chomp
+ Error occurred while parsing request parameters.
+ Contents:
+
+ #{raw_post}
+ MSG
+ end
+ end
+
+ def params_parsers
+ ActionDispatch::Request.parameter_parsers
+ end
end
end
end
diff --git a/actionpack/lib/action_dispatch/http/rack_cache.rb b/actionpack/lib/action_dispatch/http/rack_cache.rb
index 003ae4029d..3e2d01aea3 100644
--- a/actionpack/lib/action_dispatch/http/rack_cache.rb
+++ b/actionpack/lib/action_dispatch/http/rack_cache.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "rack/cache"
require "rack/cache/context"
require "active_support/cache"
diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb
index 3c62c055e5..44f23940d3 100644
--- a/actionpack/lib/action_dispatch/http/request.rb
+++ b/actionpack/lib/action_dispatch/http/request.rb
@@ -1,29 +1,32 @@
-require 'stringio'
-
-require 'active_support/inflector'
-require 'action_dispatch/http/headers'
-require 'action_controller/metal/exceptions'
-require 'rack/request'
-require 'action_dispatch/http/cache'
-require 'action_dispatch/http/mime_negotiation'
-require 'action_dispatch/http/parameters'
-require 'action_dispatch/http/filter_parameters'
-require 'action_dispatch/http/upload'
-require 'action_dispatch/http/url'
-require 'active_support/core_ext/array/conversions'
+# frozen_string_literal: true
+
+require "stringio"
+
+require "active_support/inflector"
+require "action_dispatch/http/headers"
+require "action_controller/metal/exceptions"
+require "rack/request"
+require "action_dispatch/http/cache"
+require "action_dispatch/http/mime_negotiation"
+require "action_dispatch/http/parameters"
+require "action_dispatch/http/filter_parameters"
+require "action_dispatch/http/upload"
+require "action_dispatch/http/url"
+require "active_support/core_ext/array/conversions"
module ActionDispatch
- class Request < Rack::Request
+ class Request
+ include Rack::Request::Helpers
include ActionDispatch::Http::Cache::Request
include ActionDispatch::Http::MimeNegotiation
include ActionDispatch::Http::Parameters
include ActionDispatch::Http::FilterParameters
include ActionDispatch::Http::URL
+ include ActionDispatch::ContentSecurityPolicy::Request
+ include Rack::Request::Env
- HTTP_X_REQUEST_ID = "HTTP_X_REQUEST_ID".freeze # :nodoc:
-
- autoload :Session, 'action_dispatch/request/session'
- autoload :Utils, 'action_dispatch/request/utils'
+ autoload :Session, "action_dispatch/request/session"
+ autoload :Utils, "action_dispatch/request/utils"
LOCALHOST = Regexp.union [/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, /^::1$/, /^0:0:0:0:0:0:0:1(%.*)?$/]
@@ -31,19 +34,28 @@ module ActionDispatch
PATH_TRANSLATED REMOTE_HOST
REMOTE_IDENT REMOTE_USER REMOTE_ADDR
SERVER_NAME SERVER_PROTOCOL
+ ORIGINAL_SCRIPT_NAME
HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING
HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM
- HTTP_NEGOTIATE HTTP_PRAGMA ].freeze
+ HTTP_NEGOTIATE HTTP_PRAGMA HTTP_CLIENT_IP
+ HTTP_X_FORWARDED_FOR HTTP_ORIGIN HTTP_VERSION
+ HTTP_X_CSRF_TOKEN HTTP_X_REQUEST_ID HTTP_X_FORWARDED_HOST
+ SERVER_ADDR
+ ].freeze
ENV_METHODS.each do |env|
class_eval <<-METHOD, __FILE__, __LINE__ + 1
def #{env.sub(/^HTTP_/n, '').downcase} # def accept_charset
- @env["#{env}"] # @env["HTTP_ACCEPT_CHARSET"]
+ get_header "#{env}".freeze # get_header "HTTP_ACCEPT_CHARSET".freeze
end # end
METHOD
end
+ def self.empty
+ new({})
+ end
+
def initialize(env)
super
@method = nil
@@ -54,30 +66,47 @@ module ActionDispatch
@ip = nil
end
- def check_path_parameters!
- # If any of the path parameters has an invalid encoding then
- # raise since it's likely to trigger errors further on.
- path_parameters.each do |key, value|
- next unless value.respond_to?(:valid_encoding?)
- unless value.valid_encoding?
- raise ActionController::BadRequest, "Invalid parameter: #{key} => #{value}"
- end
+ def commit_cookie_jar! # :nodoc:
+ end
+
+ PASS_NOT_FOUND = Class.new { # :nodoc:
+ def self.action(_); self; end
+ def self.call(_); [404, { "X-Cascade" => "pass" }, []]; end
+ def self.binary_params_for?(action); false; end
+ }
+
+ def controller_class
+ params = path_parameters
+ params[:action] ||= "index"
+ controller_class_for(params[:controller])
+ end
+
+ def controller_class_for(name)
+ if name
+ controller_param = name.underscore
+ const_name = "#{controller_param.camelize}Controller"
+ ActiveSupport::Dependencies.constantize(const_name)
+ else
+ PASS_NOT_FOUND
end
end
+ # Returns true if the request has a header matching the given key parameter.
+ #
+ # request.key? :ip_spoofing_check # => true
def key?(key)
- @env.key?(key)
+ has_header? key
end
# List of HTTP request methods from the following RFCs:
- # Hypertext Transfer Protocol -- HTTP/1.1 (http://www.ietf.org/rfc/rfc2616.txt)
- # HTTP Extensions for Distributed Authoring -- WEBDAV (http://www.ietf.org/rfc/rfc2518.txt)
- # Versioning Extensions to WebDAV (http://www.ietf.org/rfc/rfc3253.txt)
- # Ordered Collections Protocol (WebDAV) (http://www.ietf.org/rfc/rfc3648.txt)
- # Web Distributed Authoring and Versioning (WebDAV) Access Control Protocol (http://www.ietf.org/rfc/rfc3744.txt)
- # Web Distributed Authoring and Versioning (WebDAV) SEARCH (http://www.ietf.org/rfc/rfc5323.txt)
- # Calendar Extensions to WebDAV (http://www.ietf.org/rfc/rfc4791.txt)
- # PATCH Method for HTTP (http://www.ietf.org/rfc/rfc5789.txt)
+ # Hypertext Transfer Protocol -- HTTP/1.1 (https://www.ietf.org/rfc/rfc2616.txt)
+ # HTTP Extensions for Distributed Authoring -- WEBDAV (https://www.ietf.org/rfc/rfc2518.txt)
+ # Versioning Extensions to WebDAV (https://www.ietf.org/rfc/rfc3253.txt)
+ # Ordered Collections Protocol (WebDAV) (https://www.ietf.org/rfc/rfc3648.txt)
+ # Web Distributed Authoring and Versioning (WebDAV) Access Control Protocol (https://www.ietf.org/rfc/rfc3744.txt)
+ # Web Distributed Authoring and Versioning (WebDAV) SEARCH (https://www.ietf.org/rfc/rfc5323.txt)
+ # Calendar Extensions to WebDAV (https://www.ietf.org/rfc/rfc4791.txt)
+ # PATCH Method for HTTP (https://www.ietf.org/rfc/rfc5789.txt)
RFC2616 = %w(OPTIONS GET HEAD POST PUT DELETE TRACE CONNECT)
RFC2518 = %w(PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK)
RFC3253 = %w(VERSION-CONTROL REPORT CHECKOUT CHECKIN UNCHECKOUT MKWORKSPACE UPDATE LABEL MERGE BASELINE-CONTROL MKACTIVITY)
@@ -91,7 +120,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
}
@@ -103,28 +132,51 @@ module ActionDispatch
# the application should use), this \method returns the overridden
# value, not the original.
def request_method
- @request_method ||= check_method(env["REQUEST_METHOD"])
+ @request_method ||= check_method(super)
end
def routes # :nodoc:
- env["action_dispatch.routes".freeze]
+ get_header("action_dispatch.routes")
end
- def original_script_name # :nodoc:
- env['ORIGINAL_SCRIPT_NAME'.freeze]
+ def routes=(routes) # :nodoc:
+ set_header("action_dispatch.routes", routes)
end
def engine_script_name(_routes) # :nodoc:
- env[_routes.env_key]
+ get_header(_routes.env_key)
+ end
+
+ def engine_script_name=(name) # :nodoc:
+ set_header(routes.env_key, name.dup)
end
def request_method=(request_method) #:nodoc:
if check_method(request_method)
- @request_method = env["REQUEST_METHOD"] = request_method
+ @request_method = set_header("REQUEST_METHOD", request_method)
end
end
- # Returns a symbol form of the #request_method
+ def controller_instance # :nodoc:
+ get_header("action_controller.instance")
+ end
+
+ def controller_instance=(controller) # :nodoc:
+ set_header("action_controller.instance", controller)
+ end
+
+ def http_auth_salt
+ get_header "action_dispatch.http_auth_salt"
+ end
+
+ 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
+ # once.
+ !(get_header("action_dispatch.show_exceptions") == false)
+ end
+
+ # Returns a symbol form of the #request_method.
def request_method_symbol
HTTP_METHOD_LOOKUP[request_method]
end
@@ -133,10 +185,10 @@ module ActionDispatch
# even if it was overridden by middleware. See #request_method for
# more information.
def method
- @method ||= check_method(env["rack.methodoverride.original_method"] || env['REQUEST_METHOD'])
+ @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
@@ -145,7 +197,24 @@ module ActionDispatch
#
# request.headers["Content-Type"] # => "text/plain"
def headers
- @headers ||= Http::Headers.new(@env)
+ @headers ||= Http::Headers.new(self)
+ end
+
+ # Early Hints is an HTTP/2 status code that indicates hints to help a client start
+ # making preparations for processing the final response.
+ #
+ # If the env contains +rack.early_hints+ then the server accepts HTTP2 push for Link headers.
+ #
+ # The +send_early_hints+ method accepts a hash of links as follows:
+ #
+ # send_early_hints("Link" => "</style.css>; rel=preload; as=style\n</script.js>; rel=preload")
+ #
+ # If you are using +javascript_include_tag+ or +stylesheet_link_tag+ the
+ # Early Hints headers are included by default if supported.
+ def send_early_hints(links)
+ return unless env["rack.early_hints"]
+
+ env["rack.early_hints"].call(links)
end
# Returns a +String+ with the last requested path including their params.
@@ -156,7 +225,7 @@ module ActionDispatch
# # get '/foo?bar'
# request.original_fullpath # => '/foo?bar'
def original_fullpath
- @original_fullpath ||= (env["ORIGINAL_FULLPATH"] || fullpath)
+ @original_fullpath ||= (get_header("ORIGINAL_FULLPATH") || fullpath)
end
# Returns the +String+ full path including params of the last URL requested.
@@ -195,7 +264,7 @@ module ActionDispatch
# (case-insensitive), which may need to be manually added depending on the
# choice of JavaScript libraries and frameworks.
def xml_http_request?
- @env['HTTP_X_REQUESTED_WITH'] =~ /XMLHttpRequest/i
+ get_header("HTTP_X_REQUESTED_WITH") =~ /XMLHttpRequest/i
end
alias :xhr? :xml_http_request?
@@ -205,68 +274,74 @@ module ActionDispatch
end
# Returns the IP address of client as a +String+,
- # usually set by the RemoteIp middleware.
+ # usually set by the RemoteIp middleware.
def remote_ip
- @remote_ip ||= (@env["action_dispatch.remote_ip"] || ip).to_s
+ @remote_ip ||= (get_header("action_dispatch.remote_ip") || ip).to_s
end
- ACTION_DISPATCH_REQUEST_ID = "action_dispatch.request_id".freeze # :nodoc:
+ def remote_ip=(remote_ip)
+ set_header "action_dispatch.remote_ip", remote_ip
+ end
+
+ ACTION_DISPATCH_REQUEST_ID = "action_dispatch.request_id" # :nodoc:
# Returns the unique request id, which is based on either the X-Request-Id header that can
# be generated by a firewall, load balancer, or web server or by the RequestId middleware
# (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
- env[ACTION_DISPATCH_REQUEST_ID]
+ get_header ACTION_DISPATCH_REQUEST_ID
end
def request_id=(id) # :nodoc:
- env[ACTION_DISPATCH_REQUEST_ID] = id
+ set_header ACTION_DISPATCH_REQUEST_ID, id
end
alias_method :uuid, :request_id
- def x_request_id # :nodoc:
- @env[HTTP_X_REQUEST_ID]
- end
-
# Returns the lowercase name of the HTTP server software.
def server_software
- (@env['SERVER_SOFTWARE'] && /^([a-zA-Z]+)/ =~ @env['SERVER_SOFTWARE']) ? $1.downcase : nil
+ (get_header("SERVER_SOFTWARE") && /^([a-zA-Z]+)/ =~ get_header("SERVER_SOFTWARE")) ? $1.downcase : nil
end
# Read the request \body. This is useful for web services that need to
# work with raw requests directly.
def raw_post
- unless @env.include? 'RAW_POST_DATA'
+ unless has_header? "RAW_POST_DATA"
raw_post_body = body
- @env['RAW_POST_DATA'] = raw_post_body.read(content_length)
+ set_header("RAW_POST_DATA", raw_post_body.read(content_length))
raw_post_body.rewind if raw_post_body.respond_to?(:rewind)
end
- @env['RAW_POST_DATA']
+ get_header "RAW_POST_DATA"
end
# The request body is an IO input stream. If the RAW_POST_DATA environment
# variable is already set, wrap it in a StringIO.
def body
- if raw_post = @env['RAW_POST_DATA']
- raw_post.force_encoding(Encoding::BINARY)
+ if raw_post = get_header("RAW_POST_DATA")
+ raw_post = raw_post.dup.force_encoding(Encoding::BINARY)
StringIO.new(raw_post)
else
- @env['rack.input']
+ body_stream
end
end
- # Returns true if the request's content MIME type is
- # +application/x-www-form-urlencoded+ or +multipart/form-data+.
+ # Determine whether the request body contains form-data by checking
+ # the request Content-Type for one of the media-types:
+ # "application/x-www-form-urlencoded" or "multipart/form-data". The
+ # list of form-data media types can be modified through the
+ # +FORM_DATA_MEDIA_TYPES+ array.
+ #
+ # A request body is not assumed to contain form-data when no
+ # Content-Type header is provided and the request_method is POST.
def form_data?
- FORM_DATA_MEDIA_TYPES.include?(content_mime_type.to_s)
+ FORM_DATA_MEDIA_TYPES.include?(media_type)
end
def body_stream #:nodoc:
- @env['rack.input']
+ get_header("rack.input")
end
# TODO This should be broken apart into AD::Request::Session and probably
@@ -277,51 +352,71 @@ module ActionDispatch
else
self.session = {}
end
- @env['action_dispatch.request.flash_hash'] = nil
end
def session=(session) #:nodoc:
- Session.set @env, session
+ Session.set self, session
end
def session_options=(options)
- Session::Options.set @env, options
+ 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
- @env["action_dispatch.request.query_parameters"] ||= Utils.deep_munge(normalize_encode_params(super || {}))
+ fetch_header("action_dispatch.request.query_parameters") do |k|
+ rack_query_params = super || {}
+ # Check for non UTF-8 parameter values, which would cause errors later
+ Request::Utils.check_param_encoding(rack_query_params)
+ set_header k, Request::Utils.normalize_encode_params(rack_query_params)
+ end
rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e
- raise ActionController::BadRequest.new(:query, e)
+ raise ActionController::BadRequest.new("Invalid query parameters: #{e.message}")
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
- @env["action_dispatch.request.request_parameters"] ||= Utils.deep_munge(normalize_encode_params(super || {}))
+ fetch_header("action_dispatch.request.request_parameters") do
+ pr = parse_formatted_parameters(params_parsers) do |params|
+ super || {}
+ end
+ self.request_parameters = Request::Utils.normalize_encode_params(pr)
+ end
rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e
- raise ActionController::BadRequest.new(:request, e)
+ raise ActionController::BadRequest.new("Invalid request parameters: #{e.message}")
end
alias :request_parameters :POST
# Returns the authorization header regardless of whether it was specified directly or through one of the
# proxy alternatives.
def authorization
- @env['HTTP_AUTHORIZATION'] ||
- @env['X-HTTP_AUTHORIZATION'] ||
- @env['X_HTTP_AUTHORIZATION'] ||
- @env['REDIRECT_X_HTTP_AUTHORIZATION']
+ get_header("HTTP_AUTHORIZATION") ||
+ get_header("X-HTTP_AUTHORIZATION") ||
+ get_header("X_HTTP_AUTHORIZATION") ||
+ get_header("REDIRECT_X_HTTP_AUTHORIZATION")
end
- # True if the request came from localhost, 127.0.0.1.
+ # True if the request came from localhost, 127.0.0.1, or ::1.
def local?
LOCALHOST =~ remote_addr && LOCALHOST =~ remote_ip
end
- protected
- def parse_query(*)
- Utils.deep_munge(super)
- end
+ def request_parameters=(params)
+ raise if params.nil?
+ set_header("action_dispatch.request.request_parameters", params)
+ end
+
+ def logger
+ get_header("action_dispatch.logger")
+ end
+
+ def commit_flash
+ end
+
+ def ssl?
+ super || scheme == "wss"
+ end
private
def check_method(name)
diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb
index c5939adb9f..1d38942a31 100644
--- a/actionpack/lib/action_dispatch/http/response.rb
+++ b/actionpack/lib/action_dispatch/http/response.rb
@@ -1,6 +1,9 @@
-require 'active_support/core_ext/module/attribute_accessors'
-require 'action_dispatch/http/filter_redirect'
-require 'monitor'
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/attribute_accessors"
+require "action_dispatch/http/filter_redirect"
+require "action_dispatch/http/cache"
+require "monitor"
module ActionDispatch # :nodoc:
# Represents an HTTP response generated by a controller action. Use it to
@@ -32,45 +35,62 @@ module ActionDispatch # :nodoc:
# end
# end
class Response
+ class Header < DelegateClass(Hash) # :nodoc:
+ def initialize(response, header)
+ @response = response
+ super(header)
+ end
+
+ def []=(k, v)
+ if @response.sending? || @response.sent?
+ raise ActionDispatch::IllegalStateError, "header already sent"
+ end
+
+ super
+ end
+
+ def merge(other)
+ self.class.new @response, __getobj__.merge(other)
+ end
+
+ def to_hash
+ __getobj__.dup
+ end
+ end
+
# The request that the response is responding to.
attr_accessor :request
# The HTTP status code.
attr_reader :status
- attr_writer :sending_file
-
# Get headers for this response.
attr_reader :header
alias_method :headers, :header
- delegate :[], :[]=, :to => :@header
- delegate :each, :to => :@stream
+ delegate :[], :[]=, to: :@header
- # Sets the HTTP response's content MIME type. For example, in the controller
- # you could write this:
- #
- # response.content_type = "text/plain"
- #
- # If a character set has been defined for this response (see charset=) then
- # the character set information will also be included in the content type
- # information.
- attr_reader :content_type
-
- # The charset of the response. HTML wants to know the encoding of the
- # content you're giving them, so we need to send that along.
- attr_reader :charset
+ def each(&block)
+ sending!
+ x = @stream.each(&block)
+ sent!
+ x
+ end
- CONTENT_TYPE = "Content-Type".freeze
- SET_COOKIE = "Set-Cookie".freeze
- LOCATION = "Location".freeze
- NO_CONTENT_CODES = [204, 304]
+ CONTENT_TYPE = "Content-Type"
+ SET_COOKIE = "Set-Cookie"
+ LOCATION = "Location"
+ NO_CONTENT_CODES = [100, 101, 102, 204, 205, 304]
- cattr_accessor(:default_charset) { "utf-8" }
- cattr_accessor(:default_headers)
+ cattr_accessor :default_charset, default: "utf-8"
+ cattr_accessor :default_headers
include Rack::Response::Helpers
+ # Aliasing these off because AD::Http::Cache::Response defines them.
+ alias :_cache_control :cache_control
+ alias :_cache_control= :cache_control=
+
include ActionDispatch::Http::FilterRedirect
include ActionDispatch::Http::Cache::Response
include MonitorMixin
@@ -80,20 +100,33 @@ module ActionDispatch # :nodoc:
@response = response
@buf = buf
@closed = false
+ @str_body = nil
+ end
+
+ def body
+ @str_body ||= begin
+ buf = +""
+ each { |chunk| buf << chunk }
+ buf
+ end
end
def write(string)
raise IOError, "closed stream" if closed?
+ @str_body = nil
@response.commit!
@buf.push string
end
def each(&block)
- @response.sending!
- x = @buf.each(&block)
- @response.sent!
- x
+ if @str_body
+ return enum_for(:each) unless block_given?
+
+ yield @str_body
+ else
+ each_chunk(&block)
+ end
end
def abort
@@ -107,39 +140,48 @@ module ActionDispatch # :nodoc:
def closed?
@closed
end
+
+ private
+
+ def each_chunk(&block)
+ @buf.each(&block)
+ end
+ end
+
+ def self.create(status = 200, header = {}, body = [], default_headers: self.default_headers)
+ header = merge_default_headers(header, default_headers)
+ new status, header, body
+ end
+
+ def self.merge_default_headers(original, default)
+ default.respond_to?(:merge) ? default.merge(original) : original
end
# The underlying body, as a streamable object.
attr_reader :stream
- def initialize(status = 200, header = {}, body = [], default_headers: self.class.default_headers)
+ def initialize(status = 200, header = {}, body = [])
super()
- header = merge_default_headers(header, default_headers)
- @header = header
+ @header = Header.new(self, header)
self.body, self.status = body, status
- @sending_file = false
- @blank = false
@cv = new_cond
@committed = false
@sending = false
@sent = false
- @content_type = nil
- @charset = self.class.default_charset
-
- if content_type = self[CONTENT_TYPE]
- type, charset = content_type.split(/;\s*charset=/)
- @content_type = Mime::Type.lookup(type)
- @charset = charset || self.class.default_charset
- end
prepare_cache_control!
yield self if block_given?
end
+ def has_header?(key); headers.key? key; end
+ def get_header(key); headers[key]; end
+ def set_header(key, v); headers[key] = v; end
+ def delete_header(key); headers.delete key; end
+
def await_commit
synchronize do
@cv.wait_until { @committed }
@@ -182,20 +224,56 @@ module ActionDispatch # :nodoc:
@status = Rack::Utils.status_code(status)
end
- # Sets the HTTP content type.
+ # Sets the HTTP response's content MIME type. For example, in the controller
+ # you could write this:
+ #
+ # response.content_type = "text/plain"
+ #
+ # If a character set has been defined for this response (see charset=) then
+ # the character set information will also be included in the content type
+ # information.
def content_type=(content_type)
- @content_type = content_type.to_s
+ return unless content_type
+ new_header_info = parse_content_type(content_type.to_s)
+ prev_header_info = parsed_content_type_header
+ 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 character set.
+ # Content type of response.
+ # It returns just MIME type and does NOT contain charset part.
+ def content_type
+ parsed_content_type_header.mime_type
+ end
+
+ def sending_file=(v)
+ if true == v
+ self.charset = false
+ end
+ end
+
+ # Sets the HTTP character set. In case of +nil+ parameter
+ # it sets the charset to +default_charset+.
+ #
+ # response.charset = 'utf-16' # => 'utf-16'
+ # response.charset = nil # => 'utf-8'
def charset=(charset)
- if nil == charset
- @charset = self.class.default_charset
+ content_type = parsed_content_type_header.mime_type
+ if false == charset
+ set_content_type content_type, nil
else
- @charset = charset
+ set_content_type content_type, charset || self.class.default_charset
end
end
+ # The charset of the response. HTML wants to know the encoding of the
+ # content you're giving them, so we need to send that along.
+ def charset
+ header_info = parsed_content_type_header
+ header_info.charset || self.class.default_charset
+ end
+
# The response code of the request.
def response_code
@status
@@ -222,17 +300,15 @@ module ActionDispatch # :nodoc:
# Returns the content of the response as a string. This contains the contents
# of any calls to <tt>render</tt>.
def body
- strings = []
- each { |part| strings << part.to_s }
- strings.join
+ @stream.body
end
- EMPTY = " "
+ def write(string)
+ @stream.write string
+ end
# Allows you to manually set or override the response body.
def body=(body)
- @blank = true if body == EMPTY
-
if body.respond_to?(:to_path)
@stream = body
else
@@ -242,31 +318,49 @@ module ActionDispatch # :nodoc:
end
end
- def body_parts
- parts = []
- @stream.each { |x| parts << x }
- parts
- end
+ # Avoid having to pass an open file handle as the response body.
+ # Rack::Sendfile will usually intercept the response and uses
+ # the path directly, so there is no reason to open the file.
+ class FileBody #:nodoc:
+ attr_reader :to_path
+
+ def initialize(path)
+ @to_path = path
+ end
+
+ def body
+ File.binread(to_path)
+ end
- def set_cookie(key, value)
- ::Rack::Utils.set_cookie_header!(header, key, value)
+ # Stream the file's contents if Rack::Sendfile isn't present.
+ def each
+ File.open(to_path, "rb") do |file|
+ while chunk = file.read(16384)
+ yield chunk
+ end
+ end
+ end
end
- def delete_cookie(key, value={})
- ::Rack::Utils.delete_cookie_header!(header, key, value)
+ # Send the file stored at +path+ as the response body.
+ def send_file(path)
+ commit!
+ @stream = FileBody.new(path)
end
- # The location header we'll be responding with.
- def location
- headers[LOCATION]
+ def reset_body!
+ @stream = build_buffer(self, [])
end
- alias_method :redirect_url, :location
- # Sets the location header we'll be responding with.
- def location=(url)
- headers[LOCATION] = url
+ def body_parts
+ parts = []
+ @stream.each { |x| parts << x }
+ parts
end
+ # The location header we'll be responding with.
+ alias_method :redirect_url, :location
+
def close
stream.close if stream.respond_to?(:close)
end
@@ -297,10 +391,10 @@ module ActionDispatch # :nodoc:
# assert_equal 'AuthorOfNewPage', r.cookies['author']
def cookies
cookies = {}
- if header = self[SET_COOKIE]
+ if header = get_header(SET_COOKIE)
header = header.split("\n") if header.respond_to?(:to_str)
header.each do |cookie|
- if pair = cookie.split(';').first
+ if pair = cookie.split(";").first
key, value = pair.split("=").map { |v| Rack::Utils.unescape(v) }
cookies[key] = value
end
@@ -311,17 +405,49 @@ module ActionDispatch # :nodoc:
private
+ ContentTypeHeader = Struct.new :mime_type, :charset
+ NullContentTypeHeader = ContentTypeHeader.new nil, nil
+
+ def parse_content_type(content_type)
+ if content_type
+ type, charset = content_type.split(/;\s*charset=/)
+ type = nil if type && type.empty?
+ ContentTypeHeader.new(type, charset)
+ else
+ NullContentTypeHeader
+ end
+ end
+
+ # Small internal convenience method to get the parsed version of the current
+ # content type header.
+ def parsed_content_type_header
+ parse_content_type(get_header(CONTENT_TYPE))
+ end
+
+ def set_content_type(content_type, charset)
+ type = (content_type || "").dup
+ type << "; charset=#{charset.to_s.downcase}" if charset
+ set_header CONTENT_TYPE, type
+ end
+
def before_committed
return if committed?
assign_default_content_type_and_charset!
+ merge_and_normalize_cache_control!(@cache_control)
handle_conditional_get!
+ handle_no_content!
end
def before_sending
- end
+ # Normally we've already committed by now, but it's possible
+ # (e.g., if the controller action tries to read back its own
+ # response) to get here before that. In that case, we must force
+ # an "early" commit: we're about to freeze the headers, so this is
+ # our last chance.
+ commit! unless committed?
- def merge_default_headers(original, default)
- default.respond_to?(:merge) ? default.merge(original) : original
+ headers.freeze
+ request.commit_cookie_jar! unless committed?
end
def build_buffer(response, body)
@@ -333,18 +459,11 @@ module ActionDispatch # :nodoc:
end
def assign_default_content_type_and_charset!
- return if self[CONTENT_TYPE].present?
-
- @content_type ||= Mime::HTML
-
- type = @content_type.to_s.dup
- type << "; charset=#{charset}" if append_charset?
-
- self[CONTENT_TYPE] = type
- end
+ return if content_type
- def append_charset?
- !@sending_file && @charset != false
+ ct = parsed_content_type_header
+ set_content_type(ct.mime_type || Mime[:html].to_s,
+ ct.charset || self.class.default_charset)
end
class RackBody
@@ -367,7 +486,7 @@ module ActionDispatch # :nodoc:
end
def respond_to?(method, include_private = false)
- if method.to_s == 'to_path'
+ if method.to_s == "to_path"
@response.stream.respond_to?(method)
else
super
@@ -383,11 +502,15 @@ module ActionDispatch # :nodoc:
end
end
- def rack_response(status, header)
- header[SET_COOKIE] = header[SET_COOKIE].join("\n") if header[SET_COOKIE].respond_to?(:join)
-
+ def handle_no_content!
if NO_CONTENT_CODES.include?(@status)
- header.delete CONTENT_TYPE
+ @header.delete CONTENT_TYPE
+ @header.delete "Content-Length"
+ end
+ end
+
+ def rack_response(status, header)
+ if NO_CONTENT_CODES.include?(status)
[status, header, []]
else
[status, header, RackBody.new(self)]
diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb
index 540e11a4a0..827f022ca2 100644
--- a/actionpack/lib/action_dispatch/http/upload.rb
+++ b/actionpack/lib/action_dispatch/http/upload.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionDispatch
module Http
# Models uploaded files.
@@ -24,17 +26,27 @@ module ActionDispatch
attr_accessor :headers
def initialize(hash) # :nodoc:
- @tempfile = hash[:tempfile]
- raise(ArgumentError, ':tempfile is required') unless @tempfile
+ @tempfile = hash[:tempfile]
+ raise(ArgumentError, ":tempfile is required") unless @tempfile
+
+ if hash[:filename]
+ @original_filename = hash[:filename].dup
+
+ begin
+ @original_filename.encode!(Encoding::UTF_8)
+ rescue EncodingError
+ @original_filename.force_encoding(Encoding::UTF_8)
+ end
+ else
+ @original_filename = nil
+ end
- @original_filename = hash[:filename]
- @original_filename &&= @original_filename.encode "UTF-8"
@content_type = hash[:type]
@headers = hash[:head]
end
# Shortcut for +tempfile.read+.
- def read(length=nil, buffer=nil)
+ def read(length = nil, buffer = nil)
@tempfile.read(length, buffer)
end
@@ -44,7 +56,7 @@ module ActionDispatch
end
# Shortcut for +tempfile.close+.
- def close(unlink_now=false)
+ def close(unlink_now = false)
@tempfile.close(unlink_now)
end
@@ -53,6 +65,11 @@ module ActionDispatch
@tempfile.path
end
+ # Shortcut for +tempfile.to_path+.
+ def to_path
+ @tempfile.to_path
+ end
+
# Shortcut for +tempfile.rewind+.
def rewind
@tempfile.rewind
diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb
index f5b709ccd6..3af4c176a7 100644
--- a/actionpack/lib/action_dispatch/http/url.rb
+++ b/actionpack/lib/action_dispatch/http/url.rb
@@ -1,15 +1,15 @@
-require 'active_support/core_ext/module/attribute_accessors'
-require 'active_support/core_ext/hash/slice'
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/attribute_accessors"
module ActionDispatch
module Http
module URL
IP_HOST_REGEXP = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
- HOST_REGEXP = /(^[^:]+:\/\/)?([^:]+)(?::(\d+$))?/
+ HOST_REGEXP = /(^[^:]+:\/\/)?(\[[^\]]+\]|[^:]+)(?::(\d+$))?/
PROTOCOL_REGEXP = /^([^:]+)(:)?(\/\/)?$/
- mattr_accessor :tld_length
- self.tld_length = 1
+ mattr_accessor :tld_length, default: 1
class << self
# Returns the domain part of a host given the domain level.
@@ -43,7 +43,7 @@ module ActionDispatch
# # Second-level domain example
# extract_subdomain('dev.www.example.co.uk', 2) # => "dev.www"
def extract_subdomain(host, tld_length)
- extract_subdomains(host, tld_length).join('.')
+ extract_subdomains(host, tld_length).join(".")
end
def url_for(options)
@@ -60,14 +60,14 @@ module ActionDispatch
port = options[:port]
unless host
- raise ArgumentError, 'Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true'
+ raise ArgumentError, "Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true"
end
build_host_url(host, port, protocol, options, path_for(options))
end
def path_for(options)
- path = options[:script_name].to_s.chomp("/".freeze)
+ path = options[:script_name].to_s.chomp("/")
path << options[:path] if options.key?(:path)
add_trailing_slash(path) if options[:trailing_slash]
@@ -81,8 +81,9 @@ module ActionDispatch
def add_params(path, params)
params = { params: params } unless params.is_a?(Hash)
- params.reject! { |_,v| v.to_param.nil? }
- path << "?#{params.to_query}" unless params.empty?
+ params.reject! { |_, v| v.to_param.nil? }
+ query = params.to_query
+ path << "?#{query}" unless query.empty?
end
def add_anchor(path, anchor)
@@ -92,19 +93,17 @@ module ActionDispatch
end
def extract_domain_from(host, tld_length)
- host.split('.').last(1 + tld_length).join('.')
+ host.split(".").last(1 + tld_length).join(".")
end
def extract_subdomains_from(host, tld_length)
- parts = host.split('.')
+ parts = host.split(".")
parts[0..-(tld_length + 2)]
end
def add_trailing_slash(path)
- # includes querysting
- if path.include?('?')
+ if path.include?("?")
path.sub!(/\?/, '/\&')
- # does not have a .format
elsif !path.include?(".")
path.sub!(/[^\/]\z|\A\z/, '\&/')
end
@@ -158,11 +157,11 @@ module ActionDispatch
subdomain = options.fetch :subdomain, true
domain = options[:domain]
- host = ""
+ host = +""
if subdomain == true
return _host if domain.nil?
- host << extract_subdomains_from(_host, tld_length).join('.')
+ host << extract_subdomains_from(_host, tld_length).join(".")
elsif subdomain
host << subdomain.to_param
end
@@ -184,7 +183,7 @@ module ActionDispatch
end
end
- def initialize(env)
+ def initialize
super
@protocol = nil
@port = nil
@@ -192,11 +191,7 @@ module ActionDispatch
# Returns the complete URL used for this request.
#
- # class Request < Rack::Request
- # include ActionDispatch::Http::URL
- # end
- #
- # req = Request.new 'HTTP_HOST' => 'example.com'
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com'
# req.url # => "http://example.com"
def url
protocol + host_with_port + fullpath
@@ -204,61 +199,52 @@ module ActionDispatch
# Returns 'https://' if this is an SSL request and 'http://' otherwise.
#
- # class Request < Rack::Request
- # include ActionDispatch::Http::URL
- # end
- #
- # req = Request.new 'HTTP_HOST' => 'example.com'
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com'
# req.protocol # => "http://"
#
- # req = Request.new 'HTTP_HOST' => 'example.com', 'HTTPS' => 'on'
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com', 'HTTPS' => 'on'
# req.protocol # => "https://"
def protocol
- @protocol ||= ssl? ? 'https://' : 'http://'
+ @protocol ||= ssl? ? "https://" : "http://"
end
- # Returns the \host for this request, such as "example.com".
- #
- # class Request < Rack::Request
- # include ActionDispatch::Http::URL
- # end
+ # Returns the \host and port for this request, such as "example.com:8080".
#
- # req = Request.new 'HTTP_HOST' => 'example.com'
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com'
# req.raw_host_with_port # => "example.com"
#
- # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80'
+ # req.raw_host_with_port # => "example.com:80"
+ #
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
# req.raw_host_with_port # => "example.com:8080"
def raw_host_with_port
- if forwarded = env["HTTP_X_FORWARDED_HOST"].presence
+ if forwarded = x_forwarded_host.presence
forwarded.split(/,\s?/).last
else
- env['HTTP_HOST'] || "#{env['SERVER_NAME'] || env['SERVER_ADDR']}:#{env['SERVER_PORT']}"
+ get_header("HTTP_HOST") || "#{server_name || server_addr}:#{get_header('SERVER_PORT')}"
end
end
- # Returns the host for this request, such as example.com.
- #
- # class Request < Rack::Request
- # include ActionDispatch::Http::URL
- # end
+ # Returns the host for this request, such as "example.com".
#
- # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
# req.host # => "example.com"
def host
- raw_host_with_port.sub(/:\d+$/, '')
+ raw_host_with_port.sub(/:\d+$/, "")
end
# Returns a \host:\port string for this request, such as "example.com" or
- # "example.com:8080".
+ # "example.com:8080". Port is only included if it is not a default port
+ # (80 or 443)
#
- # class Request < Rack::Request
- # include ActionDispatch::Http::URL
- # end
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com'
+ # req.host_with_port # => "example.com"
#
- # req = Request.new 'HTTP_HOST' => 'example.com:80'
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80'
# req.host_with_port # => "example.com"
#
- # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
# req.host_with_port # => "example.com:8080"
def host_with_port
"#{host}#{port_string}"
@@ -266,14 +252,10 @@ module ActionDispatch
# Returns the port number of this request as an integer.
#
- # class Request < Rack::Request
- # include ActionDispatch::Http::URL
- # end
- #
- # req = Request.new 'HTTP_HOST' => 'example.com'
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com'
# req.port # => 80
#
- # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
# req.port # => 8080
def port
@port ||= begin
@@ -287,29 +269,21 @@ module ActionDispatch
# Returns the standard \port number for this request's protocol.
#
- # class Request < Rack::Request
- # include ActionDispatch::Http::URL
- # end
- #
- # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
# req.standard_port # => 80
def standard_port
case protocol
- when 'https://' then 443
- else 80
+ when "https://" then 443
+ else 80
end
end
# Returns whether this request is using the standard port
#
- # class Request < Rack::Request
- # include ActionDispatch::Http::URL
- # end
- #
- # req = Request.new 'HTTP_HOST' => 'example.com:80'
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80'
# req.standard_port? # => true
#
- # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
# req.standard_port? # => false
def standard_port?
port == standard_port
@@ -318,14 +292,10 @@ module ActionDispatch
# Returns a number \port suffix like 8080 if the \port number of this request
# is not the default HTTP \port 80 or HTTPS \port 443.
#
- # class Request < Rack::Request
- # include ActionDispatch::Http::URL
- # end
- #
- # req = Request.new 'HTTP_HOST' => 'example.com:80'
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80'
# req.optional_port # => nil
#
- # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
# req.optional_port # => 8080
def optional_port
standard_port? ? nil : port
@@ -334,21 +304,24 @@ module ActionDispatch
# Returns a string \port suffix, including colon, like ":8080" if the \port
# number of this request is not the default HTTP \port 80 or HTTPS \port 443.
#
- # class Request < Rack::Request
- # include ActionDispatch::Http::URL
- # end
- #
- # req = Request.new 'HTTP_HOST' => 'example.com:80'
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80'
# req.port_string # => ""
#
- # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
# req.port_string # => ":8080"
def port_string
- standard_port? ? '' : ":#{port}"
+ standard_port? ? "" : ":#{port}"
end
+ # Returns the requested port, such as 8080, based on SERVER_PORT
+ #
+ # req = ActionDispatch::Request.new 'SERVER_PORT' => '80'
+ # req.server_port # => 80
+ #
+ # req = ActionDispatch::Request.new 'SERVER_PORT' => '8080'
+ # req.server_port # => 8080
def server_port
- @env['SERVER_PORT'].to_i
+ get_header("SERVER_PORT").to_i
end
# Returns the \domain part of a \host, such as "rubyonrails.org" in "www.rubyonrails.org". You can specify
diff --git a/actionpack/lib/action_dispatch/journey.rb b/actionpack/lib/action_dispatch/journey.rb
index ad42713482..2852efa6ae 100644
--- a/actionpack/lib/action_dispatch/journey.rb
+++ b/actionpack/lib/action_dispatch/journey.rb
@@ -1,5 +1,7 @@
-require 'action_dispatch/journey/router'
-require 'action_dispatch/journey/gtg/builder'
-require 'action_dispatch/journey/gtg/simulator'
-require 'action_dispatch/journey/nfa/builder'
-require 'action_dispatch/journey/nfa/simulator'
+# frozen_string_literal: true
+
+require "action_dispatch/journey/router"
+require "action_dispatch/journey/gtg/builder"
+require "action_dispatch/journey/gtg/simulator"
+require "action_dispatch/journey/nfa/builder"
+require "action_dispatch/journey/nfa/simulator"
diff --git a/actionpack/lib/action_dispatch/journey/backwards.rb b/actionpack/lib/action_dispatch/journey/backwards.rb
deleted file mode 100644
index 3bd20fdf81..0000000000
--- a/actionpack/lib/action_dispatch/journey/backwards.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-module Rack # :nodoc:
- Mount = ActionDispatch::Journey::Router
- Mount::RouteSet = ActionDispatch::Journey::Router
- Mount::RegexpWithNamedGroups = ActionDispatch::Journey::Path::Pattern
-end
diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb
index c0566c6fc9..52396ec901 100644
--- a/actionpack/lib/action_dispatch/journey/formatter.rb
+++ b/actionpack/lib/action_dispatch/journey/formatter.rb
@@ -1,10 +1,13 @@
-require 'action_controller/metal/exceptions'
+# frozen_string_literal: true
+
+require "action_controller/metal/exceptions"
module ActionDispatch
+ # :stopdoc:
module Journey
# The Formatter class is used for formatting URLs. For example, parameters
# passed to +url_for+ in Rails will eventually call Formatter#generate.
- class Formatter # :nodoc:
+ class Formatter
attr_reader :routes
def initialize(routes)
@@ -14,7 +17,7 @@ module ActionDispatch
def generate(name, options, path_parameters, parameterize = nil)
constraints = path_parameters.merge(options)
- missing_keys = []
+ missing_keys = nil
match_route(name, constraints) do |route|
parameterized_parts = extract_parameterized_parts(route, options, path_parameters, parameterize)
@@ -25,22 +28,31 @@ module ActionDispatch
next unless name || route.dispatcher?
missing_keys = missing_keys(route, parameterized_parts)
- next unless missing_keys.empty?
+ next if missing_keys && !missing_keys.empty?
params = options.dup.delete_if do |key, _|
parameterized_parts.key?(key) || route.defaults.key?(key)
end
defaults = route.defaults
required_parts = route.required_parts
- parameterized_parts.delete_if do |key, value|
- value.to_s == defaults[key].to_s && !required_parts.include?(key)
+
+ route.parts.reverse_each do |key|
+ break if defaults[key].nil? && parameterized_parts[key].present?
+ next if parameterized_parts[key].to_s != defaults[key].to_s
+ break if required_parts.include?(key)
+
+ parameterized_parts.delete(key)
end
return [route.format(parameterized_parts), params]
end
- message = "No route matches #{Hash[constraints.sort_by{|k,v| k.to_s}].inspect}"
- message << " missing required keys: #{missing_keys.sort.inspect}" unless missing_keys.empty?
+ unmatched_keys = (missing_keys || []) & constraints.keys
+ missing_keys = (missing_keys || []) - unmatched_keys
+
+ message = +"No route matches #{Hash[constraints.sort_by { |k, v| k.to_s }].inspect}"
+ message << ", missing required keys: #{missing_keys.sort.inspect}" if missing_keys && !missing_keys.empty?
+ message << ", possible unmatched constraints: #{unmatched_keys.sort.inspect}" if unmatched_keys && !unmatched_keys.empty?
raise ActionController::UrlGenerationError, message
end
@@ -54,12 +66,12 @@ module ActionDispatch
def extract_parameterized_parts(route, options, recall, parameterize = nil)
parameterized_parts = recall.merge(options)
- keys_to_keep = route.parts.reverse.drop_while { |part|
+ keys_to_keep = route.parts.reverse_each.drop_while { |part|
!options.key?(part) || (options[part] || recall[part]).nil?
} | route.required_parts
- (parameterized_parts.keys - keys_to_keep).each do |bad_key|
- parameterized_parts.delete(bad_key)
+ parameterized_parts.delete_if do |bad_key, _|
+ !keys_to_keep.include?(bad_key)
end
if parameterize
@@ -82,7 +94,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
@@ -110,15 +126,36 @@ module ActionDispatch
routes
end
+ module RegexCaseComparator
+ DEFAULT_INPUT = /[-_.a-zA-Z0-9]+\/[-_.a-zA-Z0-9]+/
+ DEFAULT_REGEX = /\A#{DEFAULT_INPUT}\Z/
+
+ def self.===(regex)
+ DEFAULT_INPUT == regex
+ end
+ end
+
# Returns an array populated with missing keys if any are present.
def missing_keys(route, parts)
- missing_keys = []
+ missing_keys = nil
tests = route.path.requirements
route.required_parts.each { |key|
- if tests.key?(key)
- missing_keys << key unless /\A#{tests[key]}\Z/ === parts[key]
+ case tests[key]
+ when nil
+ unless parts[key]
+ missing_keys ||= []
+ missing_keys << key
+ end
+ when RegexCaseComparator
+ unless RegexCaseComparator::DEFAULT_REGEX === parts[key]
+ missing_keys ||= []
+ missing_keys << key
+ end
else
- missing_keys << key unless parts[key]
+ unless /\A#{tests[key]}\Z/ === parts[key]
+ missing_keys ||= []
+ missing_keys << key
+ end
end
}
missing_keys
@@ -134,7 +171,7 @@ module ActionDispatch
def build_cache
root = { ___routes: [] }
- routes.each_with_index do |route, i|
+ routes.routes.each_with_index do |route, i|
leaf = route.required_defaults.inject(root) do |h, tuple|
h[tuple] ||= {}
end
@@ -148,4 +185,5 @@ module ActionDispatch
end
end
end
+ # :startdoc:
end
diff --git a/actionpack/lib/action_dispatch/journey/gtg/builder.rb b/actionpack/lib/action_dispatch/journey/gtg/builder.rb
index 450588cda6..44c31053cb 100644
--- a/actionpack/lib/action_dispatch/journey/gtg/builder.rb
+++ b/actionpack/lib/action_dispatch/journey/gtg/builder.rb
@@ -1,4 +1,6 @@
-require 'action_dispatch/journey/gtg/transition_table'
+# frozen_string_literal: true
+
+require "action_dispatch/journey/gtg/transition_table"
module ActionDispatch
module Journey # :nodoc:
@@ -17,7 +19,7 @@ module ActionDispatch
def transition_table
dtrans = TransitionTable.new
marked = {}
- state_id = Hash.new { |h,k| h[k] = h.length }
+ state_id = Hash.new { |h, k| h[k] = h.length }
start = firstpos(root)
dstates = [start]
@@ -75,7 +77,7 @@ module ActionDispatch
when Nodes::Unary
nullable?(node.left)
else
- raise ArgumentError, 'unknown nullable: %s' % node.class.name
+ raise ArgumentError, "unknown nullable: %s" % node.class.name
end
end
@@ -96,7 +98,7 @@ module ActionDispatch
when Nodes::Terminal
nullable?(node) ? [] : [node]
else
- raise ArgumentError, 'unknown firstpos: %s' % node.class.name
+ raise ArgumentError, "unknown firstpos: %s" % node.class.name
end
end
@@ -117,7 +119,7 @@ module ActionDispatch
when Nodes::Unary
lastpos(node.left)
else
- raise ArgumentError, 'unknown lastpos: %s' % node.class.name
+ raise ArgumentError, "unknown lastpos: %s" % node.class.name
end
end
diff --git a/actionpack/lib/action_dispatch/journey/gtg/simulator.rb b/actionpack/lib/action_dispatch/journey/gtg/simulator.rb
index 94b0a24344..2ee4f5c30c 100644
--- a/actionpack/lib/action_dispatch/journey/gtg/simulator.rb
+++ b/actionpack/lib/action_dispatch/journey/gtg/simulator.rb
@@ -1,4 +1,6 @@
-require 'strscan'
+# frozen_string_literal: true
+
+require "strscan"
module ActionDispatch
module Journey # :nodoc:
@@ -18,14 +20,6 @@ module ActionDispatch
@tt = transition_table
end
- def simulate(string)
- ms = memos(string) { return }
- MatchData.new(ms)
- end
-
- alias :=~ :simulate
- alias :match :simulate
-
def memos(string)
input = StringScanner.new(string)
state = [0]
diff --git a/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb
index d7ce6042c2..ea647e051a 100644
--- a/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb
+++ b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb
@@ -1,4 +1,6 @@
-require 'action_dispatch/journey/nfa/dot'
+# frozen_string_literal: true
+
+require "action_dispatch/journey/nfa/dot"
module ActionDispatch
module Journey # :nodoc:
@@ -12,7 +14,7 @@ module ActionDispatch
@regexp_states = {}
@string_states = {}
@accepting = {}
- @memos = Hash.new { |h,k| h[k] = [] }
+ @memos = Hash.new { |h, k| h[k] = [] }
end
def add_accepting(state)
@@ -56,7 +58,7 @@ module ActionDispatch
end
def as_json(options = nil)
- simple_regexp = Hash.new { |h,k| h[k] = {} }
+ simple_regexp = Hash.new { |h, k| h[k] = {} }
@regexp_states.each do |from, hash|
hash.each do |re, to|
@@ -72,20 +74,20 @@ module ActionDispatch
end
def to_svg
- svg = IO.popen('dot -Tsvg', 'w+') { |f|
+ svg = IO.popen("dot -Tsvg", "w+") { |f|
f.write(to_dot)
f.close_write
f.readlines
}
3.times { svg.shift }
- svg.join.sub(/width="[^"]*"/, '').sub(/height="[^"]*"/, '')
+ svg.join.sub(/width="[^"]*"/, "").sub(/height="[^"]*"/, "")
end
- def visualizer(paths, title = 'FSM')
- viz_dir = File.join File.dirname(__FILE__), '..', 'visualizer'
- fsm_js = File.read File.join(viz_dir, 'fsm.js')
- fsm_css = File.read File.join(viz_dir, 'fsm.css')
- erb = File.read File.join(viz_dir, 'index.html.erb')
+ def visualizer(paths, title = "FSM")
+ viz_dir = File.join __dir__, "..", "visualizer"
+ fsm_js = File.read File.join(viz_dir, "fsm.js")
+ fsm_css = File.read File.join(viz_dir, "fsm.css")
+ erb = File.read File.join(viz_dir, "index.html.erb")
states = "function tt() { return #{to_json}; }"
fun_routes = paths.sample(3).map do |ast|
@@ -93,10 +95,10 @@ module ActionDispatch
case n
when Nodes::Symbol
case n.left
- when ':id' then rand(100).to_s
- when ':format' then %w{ xml json }.sample
+ when ":id" then rand(100).to_s
+ when ":format" then %w{ xml json }.sample
else
- 'omg'
+ "omg"
end
when Nodes::Terminal then n.symbol
else
@@ -109,13 +111,12 @@ module ActionDispatch
svg = to_svg
javascripts = [states, fsm_js]
- # Annoying hack warnings
fun_routes = fun_routes
stylesheets = stylesheets
svg = svg
javascripts = javascripts
- require 'erb'
+ require "erb"
template = ERB.new erb
template.result(binding)
end
@@ -148,7 +149,7 @@ module ActionDispatch
when Regexp
@regexp_states
else
- raise ArgumentError, 'unknown symbol: %s' % sym.class
+ raise ArgumentError, "unknown symbol: %s" % sym.class
end
end
end
diff --git a/actionpack/lib/action_dispatch/journey/nfa/builder.rb b/actionpack/lib/action_dispatch/journey/nfa/builder.rb
index ee6494c3e4..d22302e101 100644
--- a/actionpack/lib/action_dispatch/journey/nfa/builder.rb
+++ b/actionpack/lib/action_dispatch/journey/nfa/builder.rb
@@ -1,5 +1,7 @@
-require 'action_dispatch/journey/nfa/transition_table'
-require 'action_dispatch/journey/gtg/transition_table'
+# frozen_string_literal: true
+
+require "action_dispatch/journey/nfa/transition_table"
+require "action_dispatch/journey/gtg/transition_table"
module ActionDispatch
module Journey # :nodoc:
@@ -36,7 +38,7 @@ module ActionDispatch
def visit_OR(node)
from = @i += 1
children = node.children.map { |c| visit(c) }
- to = @i += 1
+ to = @i += 1
children.each do |child|
@tt[from, child.first] = nil
diff --git a/actionpack/lib/action_dispatch/journey/nfa/dot.rb b/actionpack/lib/action_dispatch/journey/nfa/dot.rb
index 47bf76bdbf..56e9e3c83d 100644
--- a/actionpack/lib/action_dispatch/journey/nfa/dot.rb
+++ b/actionpack/lib/action_dispatch/journey/nfa/dot.rb
@@ -1,4 +1,4 @@
-# encoding: utf-8
+# frozen_string_literal: true
module ActionDispatch
module Journey # :nodoc:
@@ -9,18 +9,18 @@ module ActionDispatch
" #{from} -> #{to} [label=\"#{sym || 'ε'}\"];"
}
- #memo_nodes = memos.values.flatten.map { |n|
- # label = n
- # if Journey::Route === n
- # label = "#{n.verb.source} #{n.path.spec}"
- # end
- # " #{n.object_id} [label=\"#{label}\", shape=box];"
- #}
- #memo_edges = memos.flat_map { |k, memos|
- # (memos || []).map { |v| " #{k} -> #{v.object_id};" }
- #}.uniq
+ # memo_nodes = memos.values.flatten.map { |n|
+ # label = n
+ # if Journey::Route === n
+ # label = "#{n.verb.source} #{n.path.spec}"
+ # end
+ # " #{n.object_id} [label=\"#{label}\", shape=box];"
+ # }
+ # memo_edges = memos.flat_map { |k, memos|
+ # (memos || []).map { |v| " #{k} -> #{v.object_id};" }
+ # }.uniq
- <<-eodot
+ <<-eodot
digraph nfa {
rankdir=LR;
node [shape = doublecircle];
@@ -28,7 +28,7 @@ digraph nfa {
node [shape = circle];
#{edges.join "\n"}
}
- eodot
+ eodot
end
end
end
diff --git a/actionpack/lib/action_dispatch/journey/nfa/simulator.rb b/actionpack/lib/action_dispatch/journey/nfa/simulator.rb
index b23270db3c..002f6feb97 100644
--- a/actionpack/lib/action_dispatch/journey/nfa/simulator.rb
+++ b/actionpack/lib/action_dispatch/journey/nfa/simulator.rb
@@ -1,4 +1,6 @@
-require 'strscan'
+# frozen_string_literal: true
+
+require "strscan"
module ActionDispatch
module Journey # :nodoc:
@@ -23,8 +25,6 @@ module ActionDispatch
state = tt.eclosure(0)
until input.eos?
sym = input.scan(%r([/.?]|[^/.?]+))
-
- # FIXME: tt.eclosure is not needed for the GTG
state = tt.eclosure(tt.move(state, sym))
end
diff --git a/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb b/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb
index 0ccab21801..fe55861507 100644
--- a/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb
+++ b/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb
@@ -1,4 +1,6 @@
-require 'action_dispatch/journey/nfa/dot'
+# frozen_string_literal: true
+
+require "action_dispatch/journey/nfa/dot"
module ActionDispatch
module Journey # :nodoc:
@@ -10,7 +12,7 @@ module ActionDispatch
attr_reader :memos
def initialize
- @table = Hash.new { |h,f| h[f] = {} }
+ @table = Hash.new { |h, f| h[f] = {} }
@memos = {}
@accepting = nil
@inverted = nil
diff --git a/actionpack/lib/action_dispatch/journey/nodes/node.rb b/actionpack/lib/action_dispatch/journey/nodes/node.rb
index bb01c087bc..32f632800c 100644
--- a/actionpack/lib/action_dispatch/journey/nodes/node.rb
+++ b/actionpack/lib/action_dispatch/journey/nodes/node.rb
@@ -1,4 +1,6 @@
-require 'action_dispatch/journey/visitors'
+# frozen_string_literal: true
+
+require "action_dispatch/journey/visitors"
module ActionDispatch
module Journey # :nodoc:
@@ -14,15 +16,15 @@ module ActionDispatch
end
def each(&block)
- Visitors::Each.new(block).accept(self)
+ Visitors::Each::INSTANCE.accept(self, block)
end
def to_s
- Visitors::String.new.accept(self)
+ Visitors::String::INSTANCE.accept(self, "")
end
def to_dot
- Visitors::Dot.new.accept(self)
+ Visitors::Dot::INSTANCE.accept(self)
end
def to_sym
@@ -30,7 +32,7 @@ module ActionDispatch
end
def name
- left.tr '*:', ''
+ -left.tr("*:", "")
end
def type
@@ -39,10 +41,15 @@ module ActionDispatch
def symbol?; false; end
def literal?; false; end
+ def terminal?; false; end
+ def star?; false; end
+ def cat?; false; end
+ def group?; false; end
end
class Terminal < Node # :nodoc:
alias :symbol :left
+ def terminal?; true; end
end
class Literal < Terminal # :nodoc:
@@ -69,11 +76,13 @@ module ActionDispatch
class Symbol < Terminal # :nodoc:
attr_accessor :regexp
alias :symbol :regexp
+ attr_reader :name
DEFAULT_EXP = /[^\.\/\?]+/
def initialize(left)
super
@regexp = DEFAULT_EXP
+ @name = -left.tr("*:", "")
end
def default_regexp?
@@ -89,13 +98,15 @@ module ActionDispatch
class Group < Unary # :nodoc:
def type; :GROUP; end
+ def group?; true; end
end
class Star < Unary # :nodoc:
+ def star?; true; end
def type; :STAR; end
def name
- left.name.tr '*:', ''
+ left.name.tr "*:", ""
end
end
@@ -111,6 +122,7 @@ module ActionDispatch
end
class Cat < Binary # :nodoc:
+ def cat?; true; end
def type; :CAT; end
end
diff --git a/actionpack/lib/action_dispatch/journey/parser.rb b/actionpack/lib/action_dispatch/journey/parser.rb
index 9012297400..e002755bcf 100644
--- a/actionpack/lib/action_dispatch/journey/parser.rb
+++ b/actionpack/lib/action_dispatch/journey/parser.rb
@@ -1,32 +1,33 @@
#
# 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'
+# :stopdoc:
-require 'action_dispatch/journey/parser_extras'
+require "action_dispatch/journey/parser_extras"
module ActionDispatch
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 ]
+ 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, 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 ]
+ 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 = [
- 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 ]
+ 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,
@@ -134,11 +135,11 @@ Racc_debug_parser = false
# reduce 0 omitted
def _reduce_1(val, _values)
- Cat.new(val.first, val.last)
+ Cat.new(val.first, val.last)
end
def _reduce_2(val, _values)
- val.first
+ val.first
end
# reduce 3 omitted
@@ -150,19 +151,19 @@ end
# reduce 6 omitted
def _reduce_7(val, _values)
- Group.new(val[1])
+ Group.new(val[1])
end
def _reduce_8(val, _values)
- Or.new([val.first, val.last])
+ Or.new([val.first, val.last])
end
def _reduce_9(val, _values)
- Or.new([val.first, val.last])
+ Or.new([val.first, val.last])
end
def _reduce_10(val, _values)
- Star.new(Symbol.new(val.last))
+ Star.new(Symbol.new(val.last))
end
# reduce 11 omitted
@@ -174,19 +175,19 @@ end
# reduce 14 omitted
def _reduce_15(val, _values)
- Slash.new('/')
+ Slash.new(val.first)
end
def _reduce_16(val, _values)
- Symbol.new(val.first)
+ Symbol.new(val.first)
end
def _reduce_17(val, _values)
- Literal.new(val.first)
+ Literal.new(val.first)
end
def _reduce_18(val, _values)
- Dot.new(val.first)
+ Dot.new(val.first)
end
def _reduce_none(val, _values)
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/parser_extras.rb b/actionpack/lib/action_dispatch/journey/parser_extras.rb
index 14892f4321..18ec6c9b9b 100644
--- a/actionpack/lib/action_dispatch/journey/parser_extras.rb
+++ b/actionpack/lib/action_dispatch/journey/parser_extras.rb
@@ -1,11 +1,18 @@
-require 'action_dispatch/journey/scanner'
-require 'action_dispatch/journey/nodes/node'
+# frozen_string_literal: true
+
+require "action_dispatch/journey/scanner"
+require "action_dispatch/journey/nodes/node"
module ActionDispatch
- module Journey # :nodoc:
- class Parser < Racc::Parser # :nodoc:
+ # :stopdoc:
+ module Journey
+ class Parser < Racc::Parser
include Journey::Nodes
+ def self.parse(string)
+ new.parse string
+ end
+
def initialize
@scanner = Scanner.new
end
@@ -20,4 +27,5 @@ module ActionDispatch
end
end
end
+ # :startdoc:
end
diff --git a/actionpack/lib/action_dispatch/journey/path/pattern.rb b/actionpack/lib/action_dispatch/journey/path/pattern.rb
index 64b48ca45f..537f479ee5 100644
--- a/actionpack/lib/action_dispatch/journey/path/pattern.rb
+++ b/actionpack/lib/action_dispatch/journey/path/pattern.rb
@@ -1,4 +1,4 @@
-require 'action_dispatch/journey/router/strexp'
+# frozen_string_literal: true
module ActionDispatch
module Journey # :nodoc:
@@ -6,15 +6,21 @@ module ActionDispatch
class Pattern # :nodoc:
attr_reader :spec, :requirements, :anchored
- def self.from_string string
- new Journey::Router::Strexp.build(string, {}, ["/.?"], true)
+ def self.from_string(string)
+ build(string, {}, "/.?", true)
end
- def initialize(strexp)
- @spec = strexp.ast
- @requirements = strexp.requirements
- @separators = strexp.separators.join
- @anchored = strexp.anchor
+ def self.build(path, requirements, separators, anchored)
+ parser = Journey::Parser.new
+ ast = parser.parse path
+ new ast, requirements, separators, anchored
+ end
+
+ def initialize(ast, requirements, separators, anchored)
+ @spec = ast
+ @requirements = requirements
+ @separators = separators
+ @anchored = anchored
@names = nil
@optional_names = nil
@@ -27,13 +33,20 @@ module ActionDispatch
Visitors::FormatBuilder.new.accept(spec)
end
+ def eager_load!
+ required_names
+ offsets
+ to_regexp
+ nil
+ end
+
def ast
- @spec.grep(Nodes::Symbol).each do |node|
+ @spec.find_all(&:symbol?).each do |node|
re = @requirements[node.to_sym]
node.regexp = re if re
end
- @spec.grep(Nodes::Star).each do |node|
+ @spec.find_all(&:star?).each do |node|
node = node.left
node.regexp = @requirements[node.to_sym] || /(.+)/
end
@@ -42,7 +55,7 @@ module ActionDispatch
end
def names
- @names ||= spec.grep(Nodes::Symbol).map(&:name)
+ @names ||= spec.find_all(&:symbol?).map(&:name)
end
def required_names
@@ -50,36 +63,11 @@ module ActionDispatch
end
def optional_names
- @optional_names ||= spec.grep(Nodes::Group).flat_map { |group|
- group.grep(Nodes::Symbol)
+ @optional_names ||= spec.find_all(&:group?).flat_map { |group|
+ group.find_all(&:symbol?)
}.map(&:name).uniq
end
- class RegexpOffsets < Journey::Visitors::Visitor # :nodoc:
- attr_reader :offsets
-
- def initialize(matchers)
- @matchers = matchers
- @capture_count = [0]
- end
-
- def visit(node)
- super
- @capture_count
- end
-
- def visit_SYMBOL(node)
- node = node.to_sym
-
- if @matchers.key?(node)
- re = /#{@matchers[node]}|/
- @capture_count.push((re.match('').length - 1) + (@capture_count.last || 0))
- else
- @capture_count << (@capture_count.last || 0)
- end
- end
- end
-
class AnchoredRegexp < Journey::Visitors::Visitor # :nodoc:
def initialize(separator, matchers)
@separator = separator
@@ -102,7 +90,7 @@ module ActionDispatch
return @separator_re unless @matchers.key?(node)
re = @matchers[node]
- "(#{re})"
+ "(#{Regexp.union(re)})"
end
def visit_GROUP(node)
@@ -119,7 +107,7 @@ module ActionDispatch
end
def visit_STAR(node)
- re = @matchers[node.left.to_sym] || '.+'
+ re = @matchers[node.left.to_sym] || ".+"
"(#{re})"
end
@@ -145,7 +133,7 @@ module ActionDispatch
end
def captures
- (length - 1).times.map { |i| self[i + 1] }
+ Array.new(length - 1) { |i| self[i + 1] }
end
def [](x)
@@ -189,8 +177,20 @@ module ActionDispatch
def offsets
return @offsets if @offsets
- viz = RegexpOffsets.new(@requirements)
- @offsets = viz.accept(spec)
+ @offsets = [0]
+
+ spec.find_all(&:symbol?).each do |node|
+ node = node.to_sym
+
+ if @requirements.key?(node)
+ re = /#{Regexp.union(@requirements[node])}|/
+ @offsets.push((re.match("").length - 1) + @offsets.last)
+ else
+ @offsets << @offsets.last
+ end
+ end
+
+ @offsets
end
end
end
diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb
index cbc985640a..8165709a3d 100644
--- a/actionpack/lib/action_dispatch/journey/route.rb
+++ b/actionpack/lib/action_dispatch/journey/route.rb
@@ -1,43 +1,106 @@
+# frozen_string_literal: true
+
module ActionDispatch
- module Journey # :nodoc:
- class Route # :nodoc:
- attr_reader :app, :path, :defaults, :name
+ # :stopdoc:
+ module Journey
+ class Route
+ attr_reader :app, :path, :defaults, :name, :precedence
- attr_reader :constraints
+ attr_reader :constraints, :internal
alias :conditions :constraints
- attr_accessor :precedence
+ module VerbMatchers
+ VERBS = %w{ DELETE GET HEAD OPTIONS LINK PATCH POST PUT TRACE UNLINK }
+ VERBS.each do |v|
+ class_eval <<-eoc, __FILE__, __LINE__ + 1
+ class #{v}
+ def self.verb; name.split("::").last; end
+ def self.call(req); req.#{v.downcase}?; end
+ end
+ eoc
+ end
+
+ class Unknown
+ attr_reader :verb
+
+ def initialize(verb)
+ @verb = verb
+ end
+
+ def call(request); @verb === request.request_method; end
+ end
+
+ class All
+ def self.call(_); true; end
+ def self.verb; ""; end
+ end
+
+ VERB_TO_CLASS = VERBS.each_with_object(all: All) do |verb, hash|
+ klass = const_get verb
+ hash[verb] = klass
+ hash[verb.downcase] = klass
+ hash[verb.downcase.to_sym] = klass
+ end
+ end
+
+ def self.verb_matcher(verb)
+ VerbMatchers::VERB_TO_CLASS.fetch(verb) do
+ VerbMatchers::Unknown.new verb.to_s.dasherize.upcase
+ end
+ end
+
+ def self.build(name, app, path, constraints, required_defaults, defaults)
+ request_method_match = verb_matcher(constraints.delete(:request_method))
+ new name, app, path, constraints, required_defaults, defaults, request_method_match, 0
+ end
##
# +path+ is a path constraint.
# +constraints+ is a hash of constraints to be applied to this route.
- def initialize(name, app, path, constraints, required_defaults, defaults)
+ def initialize(name, app, path, constraints, required_defaults, defaults, request_method_match, precedence, internal = false)
@name = name
@app = app
@path = path
+ @request_method_match = request_method_match
@constraints = constraints
@defaults = defaults
@required_defaults = nil
- @_required_defaults = required_defaults || []
+ @_required_defaults = required_defaults
@required_parts = nil
@parts = nil
@decorated_ast = nil
- @precedence = 0
+ @precedence = precedence
@path_formatter = @path.build_formatter
+ @internal = internal
+ end
+
+ def eager_load!
+ path.eager_load!
+ ast
+ parts
+ required_defaults
+ nil
end
def ast
@decorated_ast ||= begin
decorated_ast = path.ast
- decorated_ast.grep(Nodes::Terminal).each { |n| n.memo = self }
+ decorated_ast.find_all(&:terminal?).each { |n| n.memo = self }
decorated_ast
end
end
- def requirements # :nodoc:
- # needed for rails `rake routes`
- @defaults.merge(path.requirements).delete_if { |_,v|
+ # 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
+ @defaults.merge(path.requirements).delete_if { |_, v|
/.+?/ == v
}
end
@@ -50,13 +113,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
@@ -78,7 +146,7 @@ module ActionDispatch
end
def required_defaults
- @required_defaults ||= @defaults.dup.delete_if do |k,_|
+ @required_defaults ||= @defaults.dup.delete_if do |k, _|
parts.include?(k) || !required_default?(k)
end
end
@@ -92,7 +160,8 @@ module ActionDispatch
end
def matches?(request)
- constraints.all? do |method, value|
+ match_verb(request) &&
+ constraints.all? { |method, value|
case value
when Regexp, String
value === request.send(method).to_s
@@ -105,16 +174,30 @@ module ActionDispatch
else
value === request.send(method)
end
- end
+ }
end
def ip
constraints[:ip] || //
end
+ def requires_matching_verb?
+ !@request_method_match.all? { |x| x == VerbMatchers::All }
+ end
+
def verb
- constraints[:request_method] || //
+ verbs.join("|")
end
+
+ private
+ def verbs
+ @request_method_match.map(&:verb)
+ end
+
+ def match_verb(request)
+ @request_method_match.any? { |m| m.call request }
+ end
end
end
+ # :startdoc:
end
diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb
index b84aad8eb6..89a164f968 100644
--- a/actionpack/lib/action_dispatch/journey/router.rb
+++ b/actionpack/lib/action_dispatch/journey/router.rb
@@ -1,31 +1,33 @@
-require 'action_dispatch/journey/router/utils'
-require 'action_dispatch/journey/router/strexp'
-require 'action_dispatch/journey/routes'
-require 'action_dispatch/journey/formatter'
+# frozen_string_literal: true
+
+require "action_dispatch/journey/router/utils"
+require "action_dispatch/journey/routes"
+require "action_dispatch/journey/formatter"
before = $-w
$-w = false
-require 'action_dispatch/journey/parser'
+require "action_dispatch/journey/parser"
$-w = before
-require 'action_dispatch/journey/route'
-require 'action_dispatch/journey/path/pattern'
+require "action_dispatch/journey/route"
+require "action_dispatch/journey/path/pattern"
module ActionDispatch
module Journey # :nodoc:
class Router # :nodoc:
- class RoutingError < ::StandardError # :nodoc:
- end
-
- # :nodoc:
- VERSION = '2.0.0'
-
attr_accessor :routes
def initialize(routes)
@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
@@ -33,16 +35,20 @@ module ActionDispatch
script_name = req.script_name
unless route.path.anchored
- req.script_name = (script_name.to_s + match.to_s).chomp('/')
+ req.script_name = (script_name.to_s + match.to_s).chomp("/")
req.path_info = match.post_match
req.path_info = "/" + req.path_info unless req.path_info.start_with? "/"
end
+ parameters = route.defaults.merge parameters.transform_values { |val|
+ val.dup.force_encoding(::Encoding::UTF_8)
+ }
+
req.path_parameters = set_params.merge parameters
status, headers, body = route.app.serve(req)
- if 'pass' == headers['X-Cascade']
+ if "pass" == headers["X-Cascade"]
req.script_name = script_name
req.path_info = path_info
req.path_parameters = set_params
@@ -52,7 +58,7 @@ module ActionDispatch
return [status, headers, body]
end
- return [404, {'X-Cascade' => 'pass'}, ['Not Found']]
+ [404, { "X-Cascade" => "pass" }, ["Not Found"]]
end
def recognize(rails_req)
@@ -62,6 +68,7 @@ module ActionDispatch
rails_req.path_info = match.post_match.sub(/^([^\/])/, '/\1')
end
+ parameters = route.defaults.merge parameters
yield(route, parameters)
end
end
@@ -76,7 +83,9 @@ module ActionDispatch
private
def partitioned_routes
- routes.partitioned_routes
+ routes.partition { |r|
+ r.path.anchored && r.ast.grep(Nodes::Symbol).all? { |n| n.default_regexp? }
+ }
end
def ast
@@ -96,13 +105,13 @@ module ActionDispatch
simulator.memos(path) { [] }
end
- def find_routes req
+ def find_routes(req)
routes = filter_routes(req.path_info).concat custom_routes.find_all { |r|
r.path.match(req.path_info)
}
routes =
- if req.request_method == "HEAD"
+ if req.head?
match_head_routes(routes, req)
else
match_routes(routes, req)
@@ -111,9 +120,9 @@ module ActionDispatch
routes.sort_by!(&:precedence)
routes.map! { |r|
- match_data = r.path.match(req.path_info)
- path_parameters = r.defaults.dup
- match_data.names.zip(match_data.captures) { |name,val|
+ match_data = r.path.match(req.path_info)
+ path_parameters = {}
+ match_data.names.zip(match_data.captures) { |name, val|
path_parameters[name.to_sym] = Utils.unescape_uri(val) if val
}
[match_data, path_parameters, r]
@@ -121,7 +130,7 @@ module ActionDispatch
end
def match_head_routes(routes, req)
- verb_specific_routes = routes.reject { |route| route.verb == // }
+ verb_specific_routes = routes.select(&:requires_matching_verb?)
head_routes = match_routes(verb_specific_routes, req)
if head_routes.empty?
diff --git a/actionpack/lib/action_dispatch/journey/router/strexp.rb b/actionpack/lib/action_dispatch/journey/router/strexp.rb
deleted file mode 100644
index 4b7738f335..0000000000
--- a/actionpack/lib/action_dispatch/journey/router/strexp.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-module ActionDispatch
- module Journey # :nodoc:
- class Router # :nodoc:
- class Strexp # :nodoc:
- class << self
- alias :compile :new
- end
-
- attr_reader :path, :requirements, :separators, :anchor, :ast
-
- def self.build(path, requirements, separators, anchor = true)
- parser = Journey::Parser.new
- ast = parser.parse path
- new ast, path, requirements, separators, anchor
- end
-
- def initialize(ast, path, requirements, separators, anchor = true)
- @ast = ast
- @path = path
- @requirements = requirements
- @separators = separators
- @anchor = anchor
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_dispatch/journey/router/utils.rb b/actionpack/lib/action_dispatch/journey/router/utils.rb
index d02ed96d0d..3c8b9a6eaa 100644
--- a/actionpack/lib/action_dispatch/journey/router/utils.rb
+++ b/actionpack/lib/action_dispatch/journey/router/utils.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionDispatch
module Journey # :nodoc:
class Router # :nodoc:
@@ -5,7 +7,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"
@@ -13,27 +15,30 @@ module ActionDispatch
# normalize_path("") # => "/"
# normalize_path("/%ab") # => "/%AB"
def self.normalize_path(path)
- path = "/#{path}"
- path.squeeze!('/')
- path.sub!(%r{/+\Z}, '')
+ path ||= ""
+ encoding = path.encoding
+ path = +"/#{path}"
+ path.squeeze!("/")
+ path.sub!(%r{/+\Z}, "")
path.gsub!(/(%[a-f0-9]{2})/) { $1.upcase }
- path = '/' if path == ''
+ path = +"/" if path == ""
+ path.force_encoding(encoding)
path
end
# URI path and fragment escaping
- # http://tools.ietf.org/html/rfc3986
+ # https://tools.ietf.org/html/rfc3986
class UriEncoder # :nodoc:
- ENCODE = "%%%02X".freeze
+ ENCODE = "%%%02X"
US_ASCII = Encoding::US_ASCII
UTF_8 = Encoding::UTF_8
- EMPTY = "".force_encoding(US_ASCII).freeze
- DEC2HEX = (0..255).to_a.map{ |i| ENCODE % i }.map{ |s| s.force_encoding(US_ASCII) }
+ EMPTY = (+"").force_encoding(US_ASCII).freeze
+ DEC2HEX = (0..255).to_a.map { |i| ENCODE % i }.map { |s| s.force_encoding(US_ASCII) }
- ALPHA = "a-zA-Z".freeze
- DIGIT = "0-9".freeze
- UNRESERVED = "#{ALPHA}#{DIGIT}\\-\\._~".freeze
- SUB_DELIMS = "!\\$&'\\(\\)\\*\\+,;=".freeze
+ ALPHA = "a-zA-Z"
+ DIGIT = "0-9"
+ UNRESERVED = "#{ALPHA}#{DIGIT}\\-\\._~"
+ SUB_DELIMS = "!\\$&'\\(\\)\\*\\+,;="
ESCAPED = /%[a-zA-Z0-9]{2}/.freeze
@@ -55,12 +60,12 @@ module ActionDispatch
def unescape_uri(uri)
encoding = uri.encoding == US_ASCII ? UTF_8 : uri.encoding
- uri.gsub(ESCAPED) { |match| [match[1, 2].hex].pack('C') }.force_encoding(encoding)
+ uri.gsub(ESCAPED) { |match| [match[1, 2].hex].pack("C") }.force_encoding(encoding)
end
- protected
+ private
def escape(component, pattern)
- component.gsub(pattern){ |unsafe| percent_encode(unsafe) }.force_encoding(US_ASCII)
+ component.gsub(pattern) { |unsafe| percent_encode(unsafe) }.force_encoding(US_ASCII)
end
def percent_encode(unsafe)
@@ -84,6 +89,10 @@ module ActionDispatch
ENCODER.escape_fragment(fragment.to_s)
end
+ # Replaces any escaped sequences with their unescaped representations.
+ #
+ # uri = "/topics?title=Ruby%20on%20Rails"
+ # unescape_uri(uri) #=> "/topics?title=Ruby on Rails"
def self.unescape_uri(uri)
ENCODER.unescape_uri(uri)
end
diff --git a/actionpack/lib/action_dispatch/journey/routes.rb b/actionpack/lib/action_dispatch/journey/routes.rb
index 5990964b57..c0377459d5 100644
--- a/actionpack/lib/action_dispatch/journey/routes.rb
+++ b/actionpack/lib/action_dispatch/journey/routes.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionDispatch
module Journey # :nodoc:
# The Routing table. Contains all routes for a system. Routes can be
@@ -5,11 +7,10 @@ module ActionDispatch
class Routes # :nodoc:
include Enumerable
- attr_reader :routes, :named_routes, :custom_routes, :anchored_routes
+ attr_reader :routes, :custom_routes, :anchored_routes
def initialize
@routes = []
- @named_routes = {}
@ast = nil
@anchored_routes = []
@custom_routes = []
@@ -37,7 +38,6 @@ module ActionDispatch
routes.clear
anchored_routes.clear
custom_routes.clear
- named_routes.clear
end
def partition_route(route)
@@ -51,24 +51,21 @@ module ActionDispatch
def ast
@ast ||= begin
asts = anchored_routes.map(&:ast)
- Nodes::Or.new(asts) unless asts.empty?
+ Nodes::Or.new(asts)
end
end
def simulator
+ return if ast.nil?
@simulator ||= begin
gtg = GTG::Builder.new(ast).transition_table
GTG::Simulator.new(gtg)
end
end
- # Add a route to the routing table.
- def add_route(app, path, conditions, required_defaults, defaults, name = nil)
- route = Route.new(name, app, path, conditions, required_defaults, defaults)
-
- route.precedence = routes.length
+ def add_route(name, mapping)
+ route = mapping.make_route name, routes.length
routes << route
- named_routes[name] = route if name && !named_routes[name]
partition_route(route)
clear_cache!
route
diff --git a/actionpack/lib/action_dispatch/journey/scanner.rb b/actionpack/lib/action_dispatch/journey/scanner.rb
index 19e0bc03d6..2a075862e9 100644
--- a/actionpack/lib/action_dispatch/journey/scanner.rb
+++ b/actionpack/lib/action_dispatch/journey/scanner.rb
@@ -1,4 +1,6 @@
-require 'strscan'
+# frozen_string_literal: true
+
+require "strscan"
module ActionDispatch
module Journey # :nodoc:
@@ -32,27 +34,35 @@ module ActionDispatch
private
+ # takes advantage of String @- deduping capabilities in Ruby 2.5 upwards
+ # see: https://bugs.ruby-lang.org/issues/13077
+ def dedup_scan(regex)
+ r = @ss.scan(regex)
+ r ? -r : nil
+ end
+
def scan
case
# /
- when text = @ss.scan(/\//)
- [:SLASH, 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+/)
+ when @ss.skip(/\//)
+ [:SLASH, "/"]
+ when @ss.skip(/\(/)
+ [:LPAREN, "("]
+ when @ss.skip(/\)/)
+ [:RPAREN, ")"]
+ when @ss.skip(/\|/)
+ [:OR, "|"]
+ when @ss.skip(/\./)
+ [:DOT, "."]
+ when text = dedup_scan(/:\w+/)
[:SYMBOL, text]
- when text = @ss.scan(/(?:[\w%\-~!$&'*+,;=@]|\\:|\\\(|\\\))+/)
- [:LITERAL, text.tr('\\', '')]
+ when text = dedup_scan(/\*\w+/)
+ [:STAR, text]
+ when text = @ss.scan(/(?:[\w%\-~!$&'*+,;=@]|\\[:()])+/)
+ text.tr! "\\", ""
+ [:LITERAL, -text]
# any char
- when text = @ss.scan(/./)
+ when text = dedup_scan(/./)
[:LITERAL, text]
end
end
diff --git a/actionpack/lib/action_dispatch/journey/visitors.rb b/actionpack/lib/action_dispatch/journey/visitors.rb
index 52b4c8b489..d2619cbf3a 100644
--- a/actionpack/lib/action_dispatch/journey/visitors.rb
+++ b/actionpack/lib/action_dispatch/journey/visitors.rb
@@ -1,12 +1,13 @@
-# encoding: utf-8
+# frozen_string_literal: true
module ActionDispatch
- module Journey # :nodoc:
+ # :stopdoc:
+ module Journey
class Format
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
@@ -23,7 +24,7 @@ module ActionDispatch
@children = []
@parameters = []
- parts.each_with_index do |object,i|
+ parts.each_with_index do |object, i|
case object
when Journey::Format
@children << i
@@ -39,7 +40,7 @@ module ActionDispatch
@parameters.each do |index|
param = parts[index]
value = hash[param.name]
- return ''.freeze unless value
+ return "" unless value
parts[index] = param.escape value
end
@@ -59,7 +60,7 @@ module ActionDispatch
private
- def visit node
+ def visit(node)
send(DISPATCH_CACHE[node.type], node)
end
@@ -92,6 +93,45 @@ module ActionDispatch
end
end
+ class FunctionalVisitor # :nodoc:
+ DISPATCH_CACHE = {}
+
+ def accept(node, seed)
+ visit(node, seed)
+ end
+
+ def visit(node, seed)
+ send(DISPATCH_CACHE[node.type], node, seed)
+ end
+
+ def binary(node, seed)
+ visit(node.right, visit(node.left, seed))
+ end
+ def visit_CAT(n, seed); binary(n, seed); end
+
+ def nary(node, seed)
+ node.children.inject(seed) { |s, c| visit(c, s) }
+ end
+ def visit_OR(n, seed); nary(n, seed); end
+
+ def unary(node, seed)
+ visit(node.left, seed)
+ end
+ def visit_GROUP(n, seed); unary(n, seed); end
+ def visit_STAR(n, seed); unary(n, seed); end
+
+ def terminal(node, seed); seed; end
+ def visit_LITERAL(n, seed); terminal(n, seed); end
+ def visit_SYMBOL(n, seed); terminal(n, seed); end
+ def visit_SLASH(n, seed); terminal(n, seed); end
+ def visit_DOT(n, seed); terminal(n, seed); end
+
+ instance_methods(false).each do |pim|
+ next unless pim =~ /^visit_(.*)$/
+ DISPATCH_CACHE[$1.to_sym] = pim
+ end
+ end
+
class FormatBuilder < Visitor # :nodoc:
def accept(node); Journey::Format.new(super); end
def terminal(node); [node.left]; end
@@ -116,106 +156,113 @@ module ActionDispatch
end
end
- # Loop through the requirements AST
- class Each < Visitor # :nodoc:
- attr_reader :block
-
- def initialize(block)
- @block = block
- end
-
- def visit(node)
+ # Loop through the requirements AST.
+ class Each < FunctionalVisitor # :nodoc:
+ def visit(node, block)
block.call(node)
super
end
+
+ INSTANCE = new
end
- class String < Visitor # :nodoc:
+ class String < FunctionalVisitor # :nodoc:
private
- def binary(node)
- [visit(node.left), visit(node.right)].join
- end
+ def binary(node, seed)
+ visit(node.right, visit(node.left, seed))
+ end
- def nary(node)
- node.children.map { |c| visit(c) }.join '|'
- end
+ def nary(node, seed)
+ last_child = node.children.last
+ node.children.inject(seed) { |s, c|
+ string = visit(c, s)
+ string << "|" unless last_child == c
+ string
+ }
+ end
- def terminal(node)
- node.left
- end
+ def terminal(node, seed)
+ seed + node.left
+ end
- def visit_GROUP(node)
- "(#{visit(node.left)})"
- end
+ def visit_GROUP(node, seed)
+ visit(node.left, seed.dup << "(") << ")"
+ end
+
+ INSTANCE = new
end
- class Dot < Visitor # :nodoc:
+ class Dot < FunctionalVisitor # :nodoc:
def initialize
@nodes = []
@edges = []
end
- def accept(node)
+ def accept(node, seed = [[], []])
super
+ nodes, edges = seed
<<-eodot
digraph parse_tree {
size="8,5"
node [shape = none];
edge [dir = none];
- #{@nodes.join "\n"}
- #{@edges.join("\n")}
+ #{nodes.join "\n"}
+ #{edges.join("\n")}
}
eodot
end
private
- def binary(node)
- node.children.each do |c|
- @edges << "#{node.object_id} -> #{c.object_id};"
- end
+ def binary(node, seed)
+ seed.last.concat node.children.map { |c|
+ "#{node.object_id} -> #{c.object_id};"
+ }
super
end
- def nary(node)
- node.children.each do |c|
- @edges << "#{node.object_id} -> #{c.object_id};"
- end
+ def nary(node, seed)
+ seed.last.concat node.children.map { |c|
+ "#{node.object_id} -> #{c.object_id};"
+ }
super
end
- def unary(node)
- @edges << "#{node.object_id} -> #{node.left.object_id};"
+ def unary(node, seed)
+ seed.last << "#{node.object_id} -> #{node.left.object_id};"
super
end
- def visit_GROUP(node)
- @nodes << "#{node.object_id} [label=\"()\"];"
+ def visit_GROUP(node, seed)
+ seed.first << "#{node.object_id} [label=\"()\"];"
super
end
- def visit_CAT(node)
- @nodes << "#{node.object_id} [label=\"○\"];"
+ def visit_CAT(node, seed)
+ seed.first << "#{node.object_id} [label=\"○\"];"
super
end
- def visit_STAR(node)
- @nodes << "#{node.object_id} [label=\"*\"];"
+ def visit_STAR(node, seed)
+ seed.first << "#{node.object_id} [label=\"*\"];"
super
end
- def visit_OR(node)
- @nodes << "#{node.object_id} [label=\"|\"];"
+ def visit_OR(node, seed)
+ seed.first << "#{node.object_id} [label=\"|\"];"
super
end
- def terminal(node)
+ def terminal(node, seed)
value = node.left
- @nodes << "#{node.object_id} [label=\"#{value}\"];"
+ seed.first << "#{node.object_id} [label=\"#{value}\"];"
+ seed
end
+ INSTANCE = new
end
end
end
+ # :startdoc:
end
diff --git a/actionpack/lib/action_dispatch/middleware/callbacks.rb b/actionpack/lib/action_dispatch/middleware/callbacks.rb
index f80df78582..5b2ad36dd5 100644
--- a/actionpack/lib/action_dispatch/middleware/callbacks.rb
+++ b/actionpack/lib/action_dispatch/middleware/callbacks.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
module ActionDispatch
# Provides callbacks to be executed before and after dispatching the request.
@@ -7,8 +8,6 @@ module ActionDispatch
define_callbacks :call
class << self
- delegate :to_prepare, :to_cleanup, :to => "ActionDispatch::Reloader"
-
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 07d97bd6bd..26d3fd936f 100644
--- a/actionpack/lib/action_dispatch/middleware/cookies.rb
+++ b/actionpack/lib/action_dispatch/middleware/cookies.rb
@@ -1,15 +1,91 @@
-require 'active_support/core_ext/hash/keys'
-require 'active_support/core_ext/module/attribute_accessors'
-require 'active_support/core_ext/object/blank'
-require 'active_support/key_generator'
-require 'active_support/message_verifier'
-require 'active_support/json'
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/keys"
+require "active_support/key_generator"
+require "active_support/message_verifier"
+require "active_support/json"
+require "rack/utils"
module ActionDispatch
- class Request < Rack::Request
+ class Request
def cookie_jar
- env['action_dispatch.cookies'] ||= Cookies::CookieJar.build(env, host, ssl?, cookies)
+ fetch_header("action_dispatch.cookies") do
+ self.cookie_jar = Cookies::CookieJar.build(self, cookies)
+ end
+ end
+
+ # :stopdoc:
+ prepend Module.new {
+ def commit_cookie_jar!
+ cookie_jar.commit!
+ end
+ }
+
+ def have_cookie_jar?
+ has_header? "action_dispatch.cookies"
+ end
+
+ def cookie_jar=(jar)
+ set_header "action_dispatch.cookies", jar
+ end
+
+ def key_generator
+ get_header Cookies::GENERATOR_KEY
+ end
+
+ def signed_cookie_salt
+ get_header Cookies::SIGNED_COOKIE_SALT
+ end
+
+ def encrypted_cookie_salt
+ get_header Cookies::ENCRYPTED_COOKIE_SALT
+ end
+
+ def encrypted_signed_cookie_salt
+ get_header Cookies::ENCRYPTED_SIGNED_COOKIE_SALT
+ end
+
+ def authenticated_encrypted_cookie_salt
+ get_header Cookies::AUTHENTICATED_ENCRYPTED_COOKIE_SALT
+ end
+
+ def use_authenticated_cookie_encryption
+ get_header Cookies::USE_AUTHENTICATED_COOKIE_ENCRYPTION
+ end
+
+ def encrypted_cookie_cipher
+ get_header Cookies::ENCRYPTED_COOKIE_CIPHER
+ end
+
+ def signed_cookie_digest
+ get_header Cookies::SIGNED_COOKIE_DIGEST
+ end
+
+ def secret_token
+ get_header Cookies::SECRET_TOKEN
+ end
+
+ def secret_key_base
+ get_header Cookies::SECRET_KEY_BASE
+ end
+
+ def cookies_serializer
+ get_header Cookies::COOKIES_SERIALIZER
+ end
+
+ def cookies_digest
+ get_header Cookies::COOKIES_DIGEST
+ end
+
+ def cookies_rotations
+ get_header Cookies::COOKIES_ROTATIONS
+ end
+
+ def use_cookies_with_metadata
+ get_header Cookies::USE_COOKIES_WITH_METADATA
end
+
+ # :startdoc:
end
# \Cookies are read and written through ActionController#cookies.
@@ -28,18 +104,25 @@ module ActionDispatch
# cookies[:lat_lon] = JSON.generate([47.68, -122.37])
#
# # Sets a cookie that expires in 1 hour.
- # cookies[:login] = { value: "XJ-122", expires: 1.hour.from_now }
+ # cookies[:login] = { value: "XJ-122", expires: 1.hour }
+ #
+ # # Sets a cookie that expires at a specific time.
+ # cookies[:login] = { value: "XJ-122", expires: Time.utc(2020, 10, 15, 5) }
#
# # Sets a signed cookie, which prevents users from tampering with its value.
- # # The cookie is signed by your app's `secrets.secret_key_base` value.
# # It can be read using the signed method `cookies.signed[:name]`
# cookies.signed[:user_id] = current_user.id
#
+ # # Sets an encrypted cookie value before sending it to the client which
+ # # prevent users from reading and tampering with its value.
+ # # It can be read using the encrypted method `cookies.encrypted[:name]`
+ # cookies.encrypted[:discount] = 45
+ #
# # Sets a "permanent" cookie (which expires in 20 years from now).
# cookies.permanent[:login] = "XJ-122"
#
# # You can also chain these methods:
- # cookies.permanent.signed[:login] = "XJ-122"
+ # cookies.signed.permanent[:login] = "XJ-122"
#
# Examples of reading:
#
@@ -47,6 +130,7 @@ module ActionDispatch
# cookies.size # => 2
# JSON.parse(cookies[:lat_lon]) # => [47.68, -122.37]
# cookies.signed[:login] # => "XJ-122"
+ # cookies.encrypted[:discount] # => 45
#
# Example for deleting:
#
@@ -56,7 +140,7 @@ module ActionDispatch
#
# cookies[:name] = {
# value: 'a yummy cookie',
- # expires: 1.year.from_now,
+ # expires: 1.year,
# domain: 'domain.com'
# }
#
@@ -81,22 +165,28 @@ module ActionDispatch
#
# * <tt>:tld_length</tt> - When using <tt>:domain => :all</tt>, this option can be used to explicitly
# set the TLD length when using a short (<= 3 character) domain that is being interpreted as part of a TLD.
- # For example, to share cookies between user1.lvh.me and user2.lvh.me, set <tt>:tld_length</tt> to 1.
- # * <tt>:expires</tt> - The time at which this cookie expires, as a \Time object.
+ # For example, to share cookies between user1.lvh.me and user2.lvh.me, set <tt>:tld_length</tt> to 2.
+ # * <tt>:expires</tt> - The time at which this cookie expires, as a \Time or ActiveSupport::Duration object.
# * <tt>:secure</tt> - Whether this cookie is only transmitted to HTTPS servers.
# Default is +false+.
# * <tt>:httponly</tt> - Whether this cookie is accessible via scripting or
# only HTTP. Defaults to +false+.
class Cookies
- HTTP_HEADER = "Set-Cookie".freeze
- GENERATOR_KEY = "action_dispatch.key_generator".freeze
- SIGNED_COOKIE_SALT = "action_dispatch.signed_cookie_salt".freeze
- ENCRYPTED_COOKIE_SALT = "action_dispatch.encrypted_cookie_salt".freeze
- ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt".freeze
- SECRET_TOKEN = "action_dispatch.secret_token".freeze
- SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze
- COOKIES_SERIALIZER = "action_dispatch.cookies_serializer".freeze
- COOKIES_DIGEST = "action_dispatch.cookies_digest".freeze
+ HTTP_HEADER = "Set-Cookie"
+ GENERATOR_KEY = "action_dispatch.key_generator"
+ SIGNED_COOKIE_SALT = "action_dispatch.signed_cookie_salt"
+ ENCRYPTED_COOKIE_SALT = "action_dispatch.encrypted_cookie_salt"
+ ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt"
+ AUTHENTICATED_ENCRYPTED_COOKIE_SALT = "action_dispatch.authenticated_encrypted_cookie_salt"
+ USE_AUTHENTICATED_COOKIE_ENCRYPTION = "action_dispatch.use_authenticated_cookie_encryption"
+ ENCRYPTED_COOKIE_CIPHER = "action_dispatch.encrypted_cookie_cipher"
+ SIGNED_COOKIE_DIGEST = "action_dispatch.signed_cookie_digest"
+ SECRET_TOKEN = "action_dispatch.secret_token"
+ SECRET_KEY_BASE = "action_dispatch.secret_key_base"
+ COOKIES_SERIALIZER = "action_dispatch.cookies_serializer"
+ COOKIES_DIGEST = "action_dispatch.cookies_digest"
+ COOKIES_ROTATIONS = "action_dispatch.cookies_rotations"
+ USE_COOKIES_WITH_METADATA = "action_dispatch.use_cookies_with_metadata"
# Cookies can typically store 4096 bytes.
MAX_COOKIE_SIZE = 4096
@@ -104,7 +194,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:
#
@@ -118,17 +208,17 @@ module ActionDispatch
# cookies.permanent.signed[:remember_me] = current_user.id
# # => Set-Cookie: remember_me=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
def permanent
- @permanent ||= PermanentCookieJar.new(self, @key_generator, @options)
+ @permanent ||= PermanentCookieJar.new(self)
end
# Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from
# the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed
- # cookie was tampered with by the user (or a 3rd party), nil will be returned.
+ # cookie was tampered with by the user (or a 3rd party), +nil+ will be returned.
#
- # If +secrets.secret_key_base+ and +secrets.secret_token+ (deprecated) are both set,
+ # If +secret_key_base+ and +secrets.secret_token+ (deprecated) are both set,
# legacy cookies signed with the old key generator will be transparently upgraded.
#
- # This jar requires that you set a suitable secret for the verification on your app's +secrets.secret_key_base+.
+ # This jar requires that you set a suitable secret for the verification on your app's +secret_key_base+.
#
# Example:
#
@@ -137,66 +227,61 @@ module ActionDispatch
#
# cookies.signed[:discount] # => 45
def signed
- @signed ||=
- if @options[:upgrade_legacy_signed_cookies]
- UpgradeLegacySignedCookieJar.new(self, @key_generator, @options)
- else
- SignedCookieJar.new(self, @key_generator, @options)
- end
+ @signed ||= SignedKeyRotatingCookieJar.new(self)
end
# Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read.
- # If the cookie was tampered with by the user (or a 3rd party), nil will be returned.
+ # If the cookie was tampered with by the user (or a 3rd party), +nil+ will be returned.
#
- # If +secrets.secret_key_base+ and +secrets.secret_token+ (deprecated) are both set,
+ # If +secret_key_base+ and +secrets.secret_token+ (deprecated) are both set,
# legacy cookies signed with the old key generator will be transparently upgraded.
#
- # This jar requires that you set a suitable secret for the verification on your app's +secrets.secret_key_base+.
+ # If +config.action_dispatch.encrypted_cookie_salt+ and +config.action_dispatch.encrypted_signed_cookie_salt+
+ # are both set, legacy cookies encrypted with HMAC AES-256-CBC will be transparently upgraded.
+ #
+ # This jar requires that you set a suitable secret for the verification on your app's +secret_key_base+.
#
# Example:
#
# cookies.encrypted[:discount] = 45
- # # => Set-Cookie: discount=ZS9ZZ1R4cG1pcUJ1bm80anhQang3dz09LS1mbDZDSU5scGdOT3ltQ2dTdlhSdWpRPT0%3D--ab54663c9f4e3bc340c790d6d2b71e92f5b60315; path=/
+ # # => Set-Cookie: discount=DIQ7fw==--K3n//8vvnSbGq9dA--7Xh91HfLpwzbj1czhBiwOg==; path=/
#
# cookies.encrypted[:discount] # => 45
def encrypted
- @encrypted ||=
- if @options[:upgrade_legacy_signed_cookies]
- UpgradeLegacyEncryptedCookieJar.new(self, @key_generator, @options)
- else
- EncryptedCookieJar.new(self, @key_generator, @options)
- end
+ @encrypted ||= EncryptedKeyRotatingCookieJar.new(self)
end
# Returns the +signed+ or +encrypted+ jar, preferring +encrypted+ if +secret_key_base+ is set.
# Used by ActionDispatch::Session::CookieStore to avoid the need to introduce new cookie stores.
def signed_or_encrypted
@signed_or_encrypted ||=
- if @options[:secret_key_base].present?
+ if request.secret_key_base.present?
encrypted
else
signed
end
end
- end
- # Passing the ActiveSupport::MessageEncryptor::NullSerializer downstream
- # to the Message{Encryptor,Verifier} allows us to handle the
- # (de)serialization step within the cookie jar, which gives us the
- # opportunity to detect and migrate legacy cookies.
- module VerifyAndUpgradeLegacySignedMessage # :nodoc:
- def initialize(*args)
- super
- @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token], serializer: ActiveSupport::MessageEncryptor::NullSerializer)
- end
+ private
- def verify_and_upgrade_legacy_signed_message(name, signed_message)
- deserialize(name, @legacy_verifier.verify(signed_message)).tap do |value|
- self[name] = { value: value }
+ def upgrade_legacy_signed_cookies?
+ request.secret_token.present? && request.secret_key_base.present?
+ end
+
+ def upgrade_legacy_hmac_aes_cbc_cookies?
+ request.secret_key_base.present? &&
+ request.encrypted_signed_cookie_salt.present? &&
+ request.encrypted_cookie_salt.present? &&
+ request.use_authenticated_cookie_encryption
+ end
+
+ def encrypted_cookie_cipher
+ request.encrypted_cookie_cipher || "aes-256-gcm"
+ end
+
+ def signed_cookie_digest
+ request.signed_cookie_digest || "SHA1"
end
- rescue ActiveSupport::MessageVerifier::InvalidSignature
- nil
- end
end
class CookieJar #:nodoc:
@@ -216,34 +301,18 @@ module ActionDispatch
# $& => example.local
DOMAIN_REGEXP = /[^.]*\.([^.]*|..\...|...\...)$/
- def self.options_for_env(env) #:nodoc:
- { signed_cookie_salt: env[SIGNED_COOKIE_SALT] || '',
- encrypted_cookie_salt: env[ENCRYPTED_COOKIE_SALT] || '',
- encrypted_signed_cookie_salt: env[ENCRYPTED_SIGNED_COOKIE_SALT] || '',
- secret_token: env[SECRET_TOKEN],
- secret_key_base: env[SECRET_KEY_BASE],
- upgrade_legacy_signed_cookies: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present?,
- serializer: env[COOKIES_SERIALIZER],
- digest: env[COOKIES_DIGEST]
- }
- end
-
- def self.build(env, host, secure, cookies)
- key_generator = env[GENERATOR_KEY]
- options = options_for_env env
-
- new(key_generator, host, secure, options).tap do |hash|
+ def self.build(req, cookies)
+ new(req).tap do |hash|
hash.update(cookies)
end
end
- def initialize(key_generator, host = nil, secure = false, options = {})
- @key_generator = key_generator
+ attr_reader :request
+
+ def initialize(request)
@set_cookies = {}
@delete_cookies = {}
- @host = host
- @secure = secure
- @options = options
+ @request = request
@cookies = {}
@committed = false
end
@@ -274,30 +343,44 @@ module ActionDispatch
end
alias :has_key? :key?
+ # Returns the cookies as Hash.
+ alias :to_hash :to_h
+
def update(other_hash)
@cookies.update other_hash.stringify_keys
self
end
+ def update_cookies_from_jar
+ request_jar = @request.cookie_jar.instance_variable_get(:@cookies)
+ set_cookies = request_jar.reject { |k, _| @delete_cookies.key?(k) }
+
+ @cookies.update set_cookies if set_cookies
+ end
+
def to_header
- @cookies.map { |k,v| "#{k}=#{v}" }.join ';'
+ @cookies.map { |k, v| "#{escape(k)}=#{escape(v)}" }.join "; "
end
- def handle_options(options) #:nodoc:
+ def handle_options(options) # :nodoc:
+ if options[:expires].respond_to?(:from_now)
+ options[:expires] = options[:expires].from_now
+ end
+
options[:path] ||= "/"
- if options[:domain] == :all || options[:domain] == 'all'
- # if there is a provided tld length then we use it otherwise default domain regexp
+ if options[:domain] == :all || options[:domain] == "all"
+ # If there is a provided tld length then we use it otherwise default domain regexp.
domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP
- # if host is not ip and matches domain regexp
+ # If host is not ip and matches domain regexp.
# (ip confirms to domain regexp so we explicitly check for ip)
- options[:domain] = if (@host !~ /^[\d.]+$/) && (@host =~ domain_regexp)
+ options[:domain] = if (request.host !~ /^[\d.]+$/) && (request.host =~ domain_regexp)
".#{$&}"
end
elsif options[:domain].is_a? Array
- # if host matches one of the supplied domains without a dot in front of it
- options[:domain] = options[:domain].find {|domain| @host.include? domain.sub(/^\./, '') }
+ # 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
@@ -309,12 +392,12 @@ module ActionDispatch
value = options[:value]
else
value = options
- options = { :value => value }
+ options = { value: value }
end
handle_options(options)
- if @cookies[name.to_s] != value or options[:expires]
+ if @cookies[name.to_s] != value || options[:expires]
@cookies[name.to_s] = value
@set_cookies[name.to_s] = options
@delete_cookies.delete(name.to_s)
@@ -346,53 +429,99 @@ 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) }
+ @cookies.each_key { |k| delete(k, options) }
end
def write(headers)
- @set_cookies.each { |k, v| ::Rack::Utils.set_cookie_header!(headers, k, v) if write_cookie?(v) }
- @delete_cookies.each { |k, v| ::Rack::Utils.delete_cookie_header!(headers, k, v) }
- end
-
- def recycle! #:nodoc:
- @set_cookies = {}
- @delete_cookies = {}
+ if header = make_set_cookie_header(headers[HTTP_HEADER])
+ headers[HTTP_HEADER] = header
+ end
end
- mattr_accessor :always_write_cookie
- self.always_write_cookie = false
+ mattr_accessor :always_write_cookie, default: false
private
+
+ def escape(string)
+ ::Rack::Utils.escape(string)
+ end
+
+ def make_set_cookie_header(header)
+ header = @set_cookies.inject(header) { |m, (k, v)|
+ if write_cookie?(v)
+ ::Rack::Utils.add_cookie_to_header(m, k, v)
+ else
+ m
+ end
+ }
+ @delete_cookies.inject(header) { |m, (k, v)|
+ ::Rack::Utils.add_remove_cookie_to_header(m, k, v)
+ }
+ end
+
def write_cookie?(cookie)
- @secure || !cookie[:secure] || always_write_cookie
+ request.ssl? || !cookie[:secure] || always_write_cookie
end
end
- class PermanentCookieJar #:nodoc:
+ class AbstractCookieJar # :nodoc:
include ChainedCookieJars
- def initialize(parent_jar, key_generator, options = {})
+ def initialize(parent_jar)
@parent_jar = parent_jar
- @key_generator = key_generator
- @options = options
end
def [](name)
- @parent_jar[name.to_s]
+ if data = @parent_jar[name.to_s]
+ parse(name, data, purpose: "cookie.#{name}") || parse(name, data)
+ end
end
def []=(name, options)
if options.is_a?(Hash)
options.symbolize_keys!
else
- options = { :value => options }
+ options = { value: options }
end
- options[:expires] = 20.years.from_now
+ commit(name, options)
@parent_jar[name] = options
end
+
+ protected
+ def request; @parent_jar.request; end
+
+ private
+ def expiry_options(options)
+ if options[:expires].respond_to?(:from_now)
+ { expires_in: options[:expires] }
+ else
+ { expires_at: options[:expires] }
+ end
+ end
+
+ def cookie_metadata(name, options)
+ if request.use_cookies_with_metadata
+ metadata = expiry_options(options)
+ metadata[:purpose] = "cookie.#{name}"
+
+ metadata
+ else
+ {}
+ end
+ end
+
+ def parse(name, data, purpose: nil); data; end
+ def commit(name, options); end
+ end
+
+ class PermanentCookieJar < AbstractCookieJar # :nodoc:
+ private
+ def commit(name, options)
+ options[:expires] = 20.years.from_now
+ end
end
class JsonSerializer # :nodoc:
@@ -406,23 +535,28 @@ module ActionDispatch
end
module SerializedCookieJars # :nodoc:
- MARSHAL_SIGNATURE = "\x04\x08".freeze
+ MARSHAL_SIGNATURE = "\x04\x08"
+ SERIALIZER = ActiveSupport::MessageEncryptor::NullSerializer
protected
def needs_migration?(value)
- @options[:serializer] == :hybrid && value.start_with?(MARSHAL_SIGNATURE)
+ request.cookies_serializer == :hybrid && value.start_with?(MARSHAL_SIGNATURE)
end
def serialize(value)
serializer.dump(value)
end
- def deserialize(name, value)
+ def deserialize(name)
+ rotate = false
+ value = yield -> { rotate = true }
+
if value
- if needs_migration?(value)
- Marshal.load(value).tap do |v|
- self[name] = { value: v }
- end
+ case
+ when needs_migration?(value)
+ self[name] = Marshal.load(value)
+ when rotate
+ self[name] = serializer.load(value)
else
serializer.load(value)
end
@@ -430,7 +564,7 @@ module ActionDispatch
end
def serializer
- serializer = @options[:serializer] || :marshal
+ serializer = request.cookies_serializer || :marshal
case serializer
when :marshal
Marshal
@@ -442,125 +576,100 @@ module ActionDispatch
end
def digest
- @options[:digest] || 'SHA1'
+ request.cookies_digest || "SHA1"
end
end
- class SignedCookieJar #:nodoc:
- include ChainedCookieJars
+ class SignedKeyRotatingCookieJar < AbstractCookieJar # :nodoc:
include SerializedCookieJars
- def initialize(parent_jar, key_generator, options = {})
- @parent_jar = parent_jar
- @options = options
- secret = key_generator.generate_key(@options[:signed_cookie_salt])
- @verifier = ActiveSupport::MessageVerifier.new(secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
- end
+ def initialize(parent_jar)
+ super
- # Returns the value of the cookie by +name+ if it is untampered,
- # returns +nil+ otherwise or if no such cookie exists.
- def [](name)
- if signed_message = @parent_jar[name]
- deserialize name, verify(signed_message)
- end
- end
+ secret = request.key_generator.generate_key(request.signed_cookie_salt)
+ @verifier = ActiveSupport::MessageVerifier.new(secret, digest: signed_cookie_digest, serializer: SERIALIZER)
- # Signs and sets the cookie named +name+. The second argument may be the cookie's
- # value or a hash of options as documented above.
- def []=(name, options)
- if options.is_a?(Hash)
- options.symbolize_keys!
- options[:value] = @verifier.generate(serialize(options[:value]))
- else
- options = { :value => @verifier.generate(serialize(options)) }
+ request.cookies_rotations.signed.each do |*secrets, **options|
+ @verifier.rotate(*secrets, serializer: SERIALIZER, **options)
end
- raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
- @parent_jar[name] = options
+ if upgrade_legacy_signed_cookies?
+ @verifier.rotate request.secret_token, serializer: SERIALIZER
+ end
end
private
- def verify(signed_message)
- @verifier.verify(signed_message)
- rescue ActiveSupport::MessageVerifier::InvalidSignature
- nil
+ def parse(name, signed_message, purpose: nil)
+ deserialize(name) do |rotate|
+ @verifier.verified(signed_message, on_rotation: rotate, purpose: purpose)
+ end
end
- end
- # UpgradeLegacySignedCookieJar is used instead of SignedCookieJar if
- # secrets.secret_token and secrets.secret_key_base are both set. It reads
- # legacy cookies signed with the old dummy key generator and signs and
- # re-saves them using the new key generator to provide a smooth upgrade path.
- class UpgradeLegacySignedCookieJar < SignedCookieJar #:nodoc:
- include VerifyAndUpgradeLegacySignedMessage
+ def commit(name, options)
+ options[:value] = @verifier.generate(serialize(options[:value]), cookie_metadata(name, options))
- def [](name)
- if signed_message = @parent_jar[name]
- deserialize(name, verify(signed_message)) || verify_and_upgrade_legacy_signed_message(name, signed_message)
+ raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
end
- end
end
- class EncryptedCookieJar #:nodoc:
- include ChainedCookieJars
+ class EncryptedKeyRotatingCookieJar < AbstractCookieJar # :nodoc:
include SerializedCookieJars
- def initialize(parent_jar, key_generator, options = {})
- if ActiveSupport::LegacyKeyGenerator === key_generator
- 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
-
- @parent_jar = parent_jar
- @options = options
- secret = key_generator.generate_key(@options[:encrypted_cookie_salt])
- sign_secret = key_generator.generate_key(@options[:encrypted_signed_cookie_salt])
- @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
- end
+ def initialize(parent_jar)
+ super
- # Returns the value of the cookie by +name+ if it is untampered,
- # returns +nil+ otherwise or if no such cookie exists.
- def [](name)
- if encrypted_message = @parent_jar[name]
- deserialize name, decrypt_and_verify(encrypted_message)
+ if request.use_authenticated_cookie_encryption
+ key_len = ActiveSupport::MessageEncryptor.key_len(encrypted_cookie_cipher)
+ secret = request.key_generator.generate_key(request.authenticated_encrypted_cookie_salt, key_len)
+ @encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: encrypted_cookie_cipher, serializer: SERIALIZER)
+ else
+ key_len = ActiveSupport::MessageEncryptor.key_len("aes-256-cbc")
+ secret = request.key_generator.generate_key(request.encrypted_cookie_salt, key_len)
+ sign_secret = request.key_generator.generate_key(request.encrypted_signed_cookie_salt)
+ @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, cipher: "aes-256-cbc", serializer: SERIALIZER)
end
- end
- # Encrypts and sets the cookie named +name+. The second argument may be the cookie's
- # value or a hash of options as documented above.
- def []=(name, options)
- if options.is_a?(Hash)
- options.symbolize_keys!
- else
- options = { :value => options }
+ request.cookies_rotations.encrypted.each do |*secrets, **options|
+ @encryptor.rotate(*secrets, serializer: SERIALIZER, **options)
end
- options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value]))
+ if upgrade_legacy_hmac_aes_cbc_cookies?
+ legacy_cipher = "aes-256-cbc"
+ secret = request.key_generator.generate_key(request.encrypted_cookie_salt, ActiveSupport::MessageEncryptor.key_len(legacy_cipher))
+ sign_secret = request.key_generator.generate_key(request.encrypted_signed_cookie_salt)
- raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
- @parent_jar[name] = options
+ @encryptor.rotate(secret, sign_secret, cipher: legacy_cipher, digest: digest, serializer: SERIALIZER)
+ end
+
+ if upgrade_legacy_signed_cookies?
+ @legacy_verifier = ActiveSupport::MessageVerifier.new(request.secret_token, digest: digest, serializer: SERIALIZER)
+ end
end
private
- def decrypt_and_verify(encrypted_message)
- @encryptor.decrypt_and_verify(encrypted_message)
- rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage
- nil
+ def parse(name, encrypted_message, purpose: nil)
+ deserialize(name) do |rotate|
+ @encryptor.decrypt_and_verify(encrypted_message, on_rotation: rotate, purpose: purpose)
+ end
+ rescue ActiveSupport::MessageEncryptor::InvalidMessage, ActiveSupport::MessageVerifier::InvalidSignature
+ parse_legacy_signed_message(name, encrypted_message)
end
- end
- # UpgradeLegacyEncryptedCookieJar is used by ActionDispatch::Session::CookieStore
- # instead of EncryptedCookieJar if secrets.secret_token and secrets.secret_key_base
- # are both set. It reads legacy cookies signed with the old dummy key generator and
- # encrypts and re-saves them using the new key generator to provide a smooth upgrade path.
- class UpgradeLegacyEncryptedCookieJar < EncryptedCookieJar #:nodoc:
- include VerifyAndUpgradeLegacySignedMessage
+ def commit(name, options)
+ options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value]), cookie_metadata(name, options))
- def [](name)
- if encrypted_or_signed_message = @parent_jar[name]
- deserialize(name, decrypt_and_verify(encrypted_or_signed_message)) || verify_and_upgrade_legacy_signed_message(name, encrypted_or_signed_message)
+ raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
+ end
+
+ def parse_legacy_signed_message(name, legacy_signed_message)
+ if defined?(@legacy_verifier)
+ deserialize(name) do |rotate|
+ rotate.call
+
+ @legacy_verifier.verified(legacy_signed_message)
+ end
+ end
end
- end
end
def initialize(app)
@@ -568,9 +677,12 @@ module ActionDispatch
end
def call(env)
+ request = ActionDispatch::Request.new env
+
status, headers, body = @app.call(env)
- if cookie_jar = env['action_dispatch.cookies']
+ if request.have_cookie_jar?
+ cookie_jar = request.cookie_jar
unless cookie_jar.committed?
cookie_jar.write(headers)
if headers[HTTP_HEADER].respond_to?(:join)
diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
index 9082aac271..5f5fdbc66a 100644
--- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
@@ -1,16 +1,18 @@
-require 'action_dispatch/http/request'
-require 'action_dispatch/middleware/exception_wrapper'
-require 'action_dispatch/routing/inspector'
-require 'action_view'
-require 'action_view/base'
+# frozen_string_literal: true
-require 'pp'
+require "action_dispatch/http/request"
+require "action_dispatch/middleware/exception_wrapper"
+require "action_dispatch/routing/inspector"
+require "action_view"
+require "action_view/base"
+
+require "pp"
module ActionDispatch
# This middleware is responsible for logging exceptions and
# showing a debugging page in case the request is local.
class DebugExceptions
- RESCUES_TEMPLATE_PATH = File.expand_path('../templates', __FILE__)
+ RESCUES_TEMPLATE_PATH = File.expand_path("templates", __dir__)
class DebugView < ActionView::Base
def debug_params(params)
@@ -19,122 +21,197 @@ module ActionDispatch
clean_params.delete("controller")
if clean_params.empty?
- 'None'
+ "None"
else
- PP.pp(clean_params, "", 200)
+ PP.pp(clean_params, +"", 200)
end
end
def debug_headers(headers)
if headers.present?
- headers.inspect.gsub(',', ",\n")
+ headers.inspect.gsub(",", ",\n")
else
- 'None'
+ "None"
end
end
def debug_hash(object)
object.to_hash.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n")
end
+
+ def render(*)
+ logger = ActionView::Base.logger
+
+ if logger && logger.respond_to?(:silence)
+ logger.silence { super }
+ else
+ super
+ end
+ end
end
- def initialize(app, routes_app = nil)
- @app = app
- @routes_app = routes_app
+ cattr_reader :interceptors, instance_accessor: false, default: []
+
+ def self.register_interceptor(object = nil, &block)
+ interceptor = object || block
+ interceptors << interceptor
+ end
+
+ def initialize(app, routes_app = nil, response_format = :default, interceptors = self.class.interceptors)
+ @app = app
+ @routes_app = routes_app
+ @response_format = response_format
+ @interceptors = interceptors
end
def call(env)
+ request = ActionDispatch::Request.new env
_, headers, body = response = @app.call(env)
- if headers['X-Cascade'] == 'pass'
+ if headers["X-Cascade"] == "pass"
body.close if body.respond_to?(:close)
raise ActionController::RoutingError, "No route matches [#{env['REQUEST_METHOD']}] #{env['PATH_INFO'].inspect}"
end
response
rescue Exception => exception
- raise exception if env['action_dispatch.show_exceptions'] == false
- render_exception(env, exception)
+ invoke_interceptors(request, exception)
+ raise exception unless request.show_exceptions?
+ render_exception(request, exception)
end
private
- def render_exception(env, exception)
- wrapper = ExceptionWrapper.new(env, exception)
- log_error(env, wrapper)
-
- if env['action_dispatch.show_detailed_exceptions']
- request = Request.new(env)
- traces = wrapper.traces
+ def invoke_interceptors(request, exception)
+ backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner")
+ wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
- trace_to_show = 'Application Trace'
- if traces[trace_to_show].empty? && wrapper.rescue_template != 'routing_error'
- trace_to_show = 'Full Trace'
+ @interceptors.each do |interceptor|
+ begin
+ interceptor.call(request, exception)
+ rescue Exception
+ log_error(request, wrapper)
+ end
end
+ end
+
+ def render_exception(request, exception)
+ backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner")
+ wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
+ log_error(request, wrapper)
+
+ if request.get_header("action_dispatch.show_detailed_exceptions")
+ content_type = request.formats.first
- if source_to_show = traces[trace_to_show].first
- source_to_show_id = source_to_show[:id]
+ if api_request?(content_type)
+ render_for_api_request(content_type, wrapper)
+ else
+ render_for_browser_request(request, wrapper)
+ end
+ else
+ raise exception
end
+ end
- template = DebugView.new([RESCUES_TEMPLATE_PATH],
- request: request,
- exception: wrapper.exception,
- traces: traces,
- show_source_idx: source_to_show_id,
- trace_to_show: trace_to_show,
- routes_inspector: routes_inspector(exception),
- source_extracts: wrapper.source_extracts,
- line_number: wrapper.line_number,
- file: wrapper.file
- )
+ def render_for_browser_request(request, wrapper)
+ template = create_template(request, wrapper)
file = "rescues/#{wrapper.rescue_template}"
if request.xhr?
body = template.render(template: file, layout: false, formats: [:text])
format = "text/plain"
else
- body = template.render(template: file, layout: 'rescues/layout')
+ body = template.render(template: file, layout: "rescues/layout")
format = "text/html"
end
render(wrapper.status_code, body, format)
- else
- raise exception
end
- end
- def render(status, body, format)
- [status, {'Content-Type' => "#{format}; charset=#{Response.default_charset}", 'Content-Length' => body.bytesize.to_s}, [body]]
- end
+ def render_for_api_request(content_type, wrapper)
+ body = {
+ status: wrapper.status_code,
+ error: Rack::Utils::HTTP_STATUS_CODES.fetch(
+ wrapper.status_code,
+ Rack::Utils::HTTP_STATUS_CODES[500]
+ ),
+ exception: wrapper.exception.inspect,
+ traces: wrapper.traces
+ }
+
+ to_format = "to_#{content_type.to_sym}"
+
+ if content_type && body.respond_to?(to_format)
+ formatted_body = body.public_send(to_format)
+ format = content_type
+ else
+ formatted_body = body.to_json
+ format = Mime[:json]
+ end
- def log_error(env, wrapper)
- logger = logger(env)
- return unless logger
+ render(wrapper.status_code, formatted_body, format)
+ end
- exception = wrapper.exception
+ def create_template(request, wrapper)
+ DebugView.new([RESCUES_TEMPLATE_PATH],
+ request: request,
+ exception_wrapper: wrapper,
+ exception: wrapper.exception,
+ traces: wrapper.traces,
+ show_source_idx: wrapper.source_to_show_id,
+ trace_to_show: wrapper.trace_to_show,
+ routes_inspector: routes_inspector(wrapper.exception),
+ source_extracts: wrapper.source_extracts,
+ line_number: wrapper.line_number,
+ file: wrapper.file
+ )
+ end
- trace = wrapper.application_trace
- trace = wrapper.framework_trace if trace.empty?
+ def render(status, body, format)
+ [status, { "Content-Type" => "#{format}; charset=#{Response.default_charset}", "Content-Length" => body.bytesize.to_s }, [body]]
+ end
+
+ def log_error(request, wrapper)
+ logger = logger(request)
+ return unless logger
+
+ exception = wrapper.exception
- ActiveSupport::Deprecation.silence do
- message = "\n#{exception.class} (#{exception.message}):\n"
- message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
- message << " " << trace.join("\n ")
- logger.fatal("#{message}\n\n")
+ trace = wrapper.application_trace
+ trace = wrapper.framework_trace if trace.empty?
+
+ ActiveSupport::Deprecation.silence do
+ logger.fatal " "
+ logger.fatal "#{exception.class} (#{exception.message}):"
+ log_array logger, exception.annoted_source_code if exception.respond_to?(:annoted_source_code)
+ logger.fatal " "
+ log_array logger, trace
+ end
end
- end
- def logger(env)
- env['action_dispatch.logger'] || stderr_logger
- end
+ def log_array(logger, array)
+ if logger.formatter && logger.formatter.respond_to?(:tags_text)
+ logger.fatal array.join("\n#{logger.formatter.tags_text}")
+ else
+ logger.fatal array.join("\n")
+ end
+ end
- def stderr_logger
- @stderr_logger ||= ActiveSupport::Logger.new($stderr)
- end
+ def logger(request)
+ request.logger || ActionView::Base.logger || stderr_logger
+ end
- def routes_inspector(exception)
- if @routes_app.respond_to?(:routes) && (exception.is_a?(ActionController::RoutingError) || exception.is_a?(ActionView::Template::Error))
- ActionDispatch::Routing::RoutesInspector.new(@routes_app.routes.routes)
+ def stderr_logger
+ @stderr_logger ||= ActiveSupport::Logger.new($stderr)
+ end
+
+ def routes_inspector(exception)
+ if @routes_app.respond_to?(:routes) && (exception.is_a?(ActionController::RoutingError) || exception.is_a?(ActionView::Template::Error))
+ ActionDispatch::Routing::RoutesInspector.new(@routes_app.routes.routes)
+ end
+ end
+
+ def api_request?(content_type)
+ @response_format == :api && !content_type.html?
end
- end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/debug_locks.rb b/actionpack/lib/action_dispatch/middleware/debug_locks.rb
new file mode 100644
index 0000000000..93c6c85a71
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/debug_locks.rb
@@ -0,0 +1,124 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ # This middleware can be used to diagnose deadlocks in the autoload interlock.
+ #
+ # To use it, insert it near the top of the middleware stack, using
+ # <tt>config/application.rb</tt>:
+ #
+ # config.middleware.insert_before Rack::Sendfile, ActionDispatch::DebugLocks
+ #
+ # After restarting the application and re-triggering the deadlock condition,
+ # <tt>/rails/locks</tt> will show a summary of all threads currently known to
+ # the interlock, which lock level they are holding or awaiting, and their
+ # current backtrace.
+ #
+ # Generally a deadlock will be caused by the interlock conflicting with some
+ # other external lock or blocking I/O call. These cannot be automatically
+ # identified, but should be visible in the displayed backtraces.
+ #
+ # NOTE: The formatting and content of this middleware's output is intended for
+ # human consumption, and should be expected to change between releases.
+ #
+ # This middleware exposes operational details of the server, with no access
+ # control. It should only be enabled when in use, and removed thereafter.
+ class DebugLocks
+ def initialize(app, path = "/rails/locks")
+ @app = app
+ @path = path
+ end
+
+ def call(env)
+ req = ActionDispatch::Request.new env
+
+ if req.get?
+ path = req.path_info.chomp("/")
+ if path == @path
+ return render_details(req)
+ end
+ end
+
+ @app.call(env)
+ end
+
+ private
+ def render_details(req)
+ threads = ActiveSupport::Dependencies.interlock.raw_state do |raw_threads|
+ # The Interlock itself comes to a complete halt as long as this block
+ # is executing. That gives us a more consistent picture of everything,
+ # but creates a pretty strong Observer Effect.
+ #
+ # Most directly, that means we need to do as little as possible in
+ # this block. More widely, it means this middleware should remain a
+ # strictly diagnostic tool (to be used when something has gone wrong),
+ # and not for any sort of general monitoring.
+
+ raw_threads.each.with_index do |(thread, info), idx|
+ info[:index] = idx
+ info[:backtrace] = thread.backtrace
+ end
+
+ raw_threads
+ end
+
+ str = threads.map do |thread, info|
+ if info[:exclusive]
+ lock_state = +"Exclusive"
+ elsif info[:sharing] > 0
+ lock_state = +"Sharing"
+ lock_state << " x#{info[:sharing]}" if info[:sharing] > 1
+ else
+ lock_state = +"No lock"
+ end
+
+ if info[:waiting]
+ lock_state << " (yielded share)"
+ end
+
+ msg = +"Thread #{info[:index]} [0x#{thread.__id__.to_s(16)} #{thread.status || 'dead'}] #{lock_state}\n"
+
+ if info[:sleeper]
+ msg << " Waiting in #{info[:sleeper]}"
+ msg << " to #{info[:purpose].to_s.inspect}" unless info[:purpose].nil?
+ msg << "\n"
+
+ if info[:compatible]
+ compat = info[:compatible].map { |c| c == false ? "share" : c.to_s.inspect }
+ msg << " may be pre-empted for: #{compat.join(', ')}\n"
+ end
+
+ blockers = threads.values.select { |binfo| blocked_by?(info, binfo, threads.values) }
+ msg << " blocked by: #{blockers.map { |i| i[:index] }.join(', ')}\n" if blockers.any?
+ end
+
+ blockees = threads.values.select { |binfo| blocked_by?(binfo, info, threads.values) }
+ msg << " blocking: #{blockees.map { |i| i[:index] }.join(', ')}\n" if blockees.any?
+
+ msg << "\n#{info[:backtrace].join("\n")}\n" if info[:backtrace]
+ end.join("\n\n---\n\n\n")
+
+ [200, { "Content-Type" => "text/plain", "Content-Length" => str.size }, [str]]
+ end
+
+ def blocked_by?(victim, blocker, all_threads)
+ return false if victim.equal?(blocker)
+
+ case victim[:sleeper]
+ when :start_sharing
+ blocker[:exclusive] ||
+ (!victim[:waiting] && blocker[:compatible] && !blocker[:compatible].include?(false))
+ when :start_exclusive
+ blocker[:sharing] > 0 ||
+ blocker[:exclusive] ||
+ (blocker[:compatible] && !blocker[:compatible].include?(victim[:purpose]))
+ when :yield_shares
+ blocker[:exclusive]
+ when :stop_exclusive
+ blocker[:exclusive] ||
+ victim[:compatible] &&
+ victim[:compatible].include?(blocker[:purpose]) &&
+ all_threads.all? { |other| !other[:compatible] || blocker.equal?(other) || other[:compatible].include?(blocker[:purpose]) }
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
index 8c3d45584d..fb2b2bd3b0 100644
--- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
+++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
@@ -1,43 +1,44 @@
-require 'action_controller/metal/exceptions'
-require 'active_support/core_ext/module/attribute_accessors'
-require 'rack/utils'
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/attribute_accessors"
+require "rack/utils"
module ActionDispatch
class ExceptionWrapper
- cattr_accessor :rescue_responses
- @@rescue_responses = Hash.new(:internal_server_error)
- @@rescue_responses.merge!(
- 'ActionController::RoutingError' => :not_found,
- 'AbstractController::ActionNotFound' => :not_found,
- 'ActionController::MethodNotAllowed' => :method_not_allowed,
- 'ActionController::UnknownHttpMethod' => :method_not_allowed,
- 'ActionController::NotImplemented' => :not_implemented,
- 'ActionController::UnknownFormat' => :not_acceptable,
- 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity,
- 'ActionController::InvalidCrossOriginRequest' => :unprocessable_entity,
- 'ActionDispatch::ParamsParser::ParseError' => :bad_request,
- 'ActionController::BadRequest' => :bad_request,
- 'ActionController::ParameterMissing' => :bad_request,
- 'Rack::Utils::ParameterTypeError' => :bad_request,
- 'Rack::Utils::InvalidParameterError' => :bad_request
+ cattr_accessor :rescue_responses, default: Hash.new(:internal_server_error).merge!(
+ "ActionController::RoutingError" => :not_found,
+ "AbstractController::ActionNotFound" => :not_found,
+ "ActionController::MethodNotAllowed" => :method_not_allowed,
+ "ActionController::UnknownHttpMethod" => :method_not_allowed,
+ "ActionController::NotImplemented" => :not_implemented,
+ "ActionController::UnknownFormat" => :not_acceptable,
+ "ActionController::MissingExactTemplate" => :not_acceptable,
+ "ActionController::InvalidAuthenticityToken" => :unprocessable_entity,
+ "ActionController::InvalidCrossOriginRequest" => :unprocessable_entity,
+ "ActionDispatch::Http::Parameters::ParseError" => :bad_request,
+ "ActionController::BadRequest" => :bad_request,
+ "ActionController::ParameterMissing" => :bad_request,
+ "Rack::QueryParser::ParameterTypeError" => :bad_request,
+ "Rack::QueryParser::InvalidParameterError" => :bad_request
)
- cattr_accessor :rescue_templates
- @@rescue_templates = Hash.new('diagnostics')
- @@rescue_templates.merge!(
- 'ActionView::MissingTemplate' => 'missing_template',
- 'ActionController::RoutingError' => 'routing_error',
- 'AbstractController::ActionNotFound' => 'unknown_action',
- 'ActionView::Template::Error' => 'template_error'
+ cattr_accessor :rescue_templates, default: Hash.new("diagnostics").merge!(
+ "ActionView::MissingTemplate" => "missing_template",
+ "ActionController::RoutingError" => "routing_error",
+ "AbstractController::ActionNotFound" => "unknown_action",
+ "ActiveRecord::StatementInvalid" => "invalid_statement",
+ "ActionView::Template::Error" => "template_error",
+ "ActionController::MissingExactTemplate" => "missing_exact_template",
)
- attr_reader :env, :exception, :line_number, :file
+ attr_reader :backtrace_cleaner, :exception, :wrapped_causes, :line_number, :file
- def initialize(env, exception)
- @env = env
+ def initialize(backtrace_cleaner, exception)
+ @backtrace_cleaner = backtrace_cleaner
@exception = original_exception(exception)
+ @wrapped_causes = wrapped_causes_for(exception, backtrace_cleaner)
- expand_backtrace if exception.is_a?(SyntaxError) || exception.try(:original_exception).try(:is_a?, SyntaxError)
+ expand_backtrace if exception.is_a?(SyntaxError) || exception.cause.is_a?(SyntaxError)
end
def rescue_template
@@ -61,15 +62,19 @@ module ActionDispatch
end
def traces
- appplication_trace_with_ids = []
+ application_trace_with_ids = []
framework_trace_with_ids = []
full_trace_with_ids = []
full_trace.each_with_index do |trace, idx|
- trace_with_id = { id: idx, trace: trace }
+ trace_with_id = {
+ exception_object_id: @exception.object_id,
+ id: idx,
+ trace: trace
+ }
if application_trace.include?(trace)
- appplication_trace_with_ids << trace_with_id
+ application_trace_with_ids << trace_with_id
else
framework_trace_with_ids << trace_with_id
end
@@ -78,7 +83,7 @@ module ActionDispatch
end
{
- "Application Trace" => appplication_trace_with_ids,
+ "Application Trace" => application_trace_with_ids,
"Framework Trace" => framework_trace_with_ids,
"Full Trace" => full_trace_with_ids
}
@@ -99,59 +104,73 @@ module ActionDispatch
end
end
- private
-
- def backtrace
- Array(@exception.backtrace)
- end
-
- def original_exception(exception)
- if registered_original_exception?(exception)
- exception.original_exception
+ def trace_to_show
+ if traces["Application Trace"].empty? && rescue_template != "routing_error"
+ "Full Trace"
else
- exception
+ "Application Trace"
end
end
- def registered_original_exception?(exception)
- exception.respond_to?(:original_exception) && @@rescue_responses.has_key?(exception.original_exception.class.name)
+ def source_to_show_id
+ (traces[trace_to_show].first || {})[:id]
end
- def clean_backtrace(*args)
- if backtrace_cleaner
- backtrace_cleaner.clean(backtrace, *args)
- else
- backtrace
+ private
+
+ def backtrace
+ Array(@exception.backtrace)
end
- end
- def backtrace_cleaner
- @backtrace_cleaner ||= @env['action_dispatch.backtrace_cleaner']
- end
+ def original_exception(exception)
+ if @@rescue_responses.has_key?(exception.cause.class.name)
+ exception.cause
+ else
+ exception
+ end
+ end
- def source_fragment(path, line)
- return unless Rails.respond_to?(:root) && Rails.root
- full_path = Rails.root.join(path)
- if File.exist?(full_path)
- File.open(full_path, "r") do |file|
- start = [line - 3, 0].max
- lines = file.each_line.drop(start).take(6)
- Hash[*(start+1..(lines.count+start)).zip(lines).flatten]
+ def causes_for(exception)
+ return enum_for(__method__, exception) unless block_given?
+
+ yield exception while exception = exception.cause
+ end
+
+ def wrapped_causes_for(exception, backtrace_cleaner)
+ causes_for(exception).map { |cause| self.class.new(backtrace_cleaner, cause) }
+ end
+
+ def clean_backtrace(*args)
+ if backtrace_cleaner
+ backtrace_cleaner.clean(backtrace, *args)
+ else
+ backtrace
end
end
- end
- def extract_file_and_line_number(trace)
- # Split by the first colon followed by some digits, which works for both
- # Windows and Unix path styles.
- file, line = trace.match(/^(.+?):(\d+).*$/, &:captures) || trace
- [file, line.to_i]
- end
+ def source_fragment(path, line)
+ return unless Rails.respond_to?(:root) && Rails.root
+ full_path = Rails.root.join(path)
+ if File.exist?(full_path)
+ File.open(full_path, "r") do |file|
+ start = [line - 3, 0].max
+ lines = file.each_line.drop(start).take(6)
+ Hash[*(start + 1..(lines.count + start)).zip(lines).flatten]
+ end
+ end
+ end
- def expand_backtrace
- @exception.backtrace.unshift(
- @exception.to_s.split("\n")
- ).flatten!
- end
+ def extract_file_and_line_number(trace)
+ # Split by the first colon followed by some digits, which works for both
+ # Windows and Unix path styles.
+ file, line = trace.match(/^(.+?):(\d+).*$/, &:captures) || trace
+ [file, line.to_i]
+ end
+
+ def expand_backtrace
+ @exception.backtrace.unshift(
+ @exception.to_s.split("\n")
+ ).flatten!
+ end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/executor.rb b/actionpack/lib/action_dispatch/middleware/executor.rb
new file mode 100644
index 0000000000..129b18d3d9
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/executor.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require "rack/body_proxy"
+
+module ActionDispatch
+ class Executor
+ def initialize(app, executor)
+ @app, @executor = app, executor
+ end
+
+ def call(env)
+ state = @executor.run!
+ begin
+ response = @app.call(env)
+ returned = response << ::Rack::BodyProxy.new(response.pop) { state.complete! }
+ ensure
+ state.complete! unless returned
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb
index 59639a010e..cf9165d008 100644
--- a/actionpack/lib/action_dispatch/middleware/flash.rb
+++ b/actionpack/lib/action_dispatch/middleware/flash.rb
@@ -1,15 +1,8 @@
-require 'active_support/core_ext/hash/keys'
+# frozen_string_literal: true
-module ActionDispatch
- class Request < Rack::Request
- # Access the contents of the flash. Use <tt>flash["notice"]</tt> to
- # read a notice you put there or <tt>flash["notice"] = "hello"</tt>
- # to put a new one.
- def flash
- @env[Flash::KEY] ||= Flash::FlashHash.from_session_value(session["flash"])
- end
- end
+require "active_support/core_ext/hash/keys"
+module ActionDispatch
# The flash provides a way to pass temporary primitive-types (String, Array, Hash) between actions. Anything you place in the flash will be exposed
# to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create
# action that sets <tt>flash[:notice] = "Post successfully created"</tt> before redirecting to a display action that can
@@ -45,7 +38,46 @@ module ActionDispatch
#
# See docs on the FlashHash class for more details about the flash.
class Flash
- KEY = 'action_dispatch.request.flash_hash'.freeze
+ KEY = "action_dispatch.request.flash_hash"
+
+ module RequestMethods
+ # Access the contents of the flash. Use <tt>flash["notice"]</tt> to
+ # read a notice you put there or <tt>flash["notice"] = "hello"</tt>
+ # to put a new one.
+ def flash
+ flash = flash_hash
+ return flash if flash
+ self.flash = Flash::FlashHash.from_session_value(session["flash"])
+ end
+
+ def flash=(flash)
+ set_header Flash::KEY, flash
+ end
+
+ def flash_hash # :nodoc:
+ get_header Flash::KEY
+ end
+
+ def commit_flash # :nodoc:
+ session = self.session || {}
+ flash_hash = self.flash_hash
+
+ if flash_hash && (flash_hash.present? || session.key?("flash"))
+ session["flash"] = flash_hash.to_session_value
+ self.flash = flash_hash.dup
+ end
+
+ 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
+ end
+
+ def reset_session # :nodoc:
+ super
+ self.flash = nil
+ end
+ end
class FlashNow #:nodoc:
attr_accessor :flash
@@ -88,8 +120,8 @@ module ActionDispatch
end
new(flashes, flashes.keys)
when Hash # Rails 4.0
- flashes = value['flashes']
- if discard = value['discard']
+ flashes = value["flashes"]
+ if discard = value["discard"]
flashes.except!(*discard)
end
new(flashes, flashes.keys)
@@ -99,11 +131,11 @@ module ActionDispatch
end
# Builds a hash containing the flashes to keep for the next request.
- # If there are none to keep, returns nil.
+ # If there are none to keep, returns +nil+.
def to_session_value #:nodoc:
flashes_to_keep = @flashes.except(*@discard)
return nil if flashes_to_keep.empty?
- {'flashes' => flashes_to_keep}
+ { "discard" => [], "flashes" => flashes_to_keep }
end
def initialize(flashes = {}, discard = []) #:nodoc:
@@ -247,36 +279,22 @@ module ActionDispatch
end
protected
- def now_is_loaded?
- @now
- end
-
- def stringify_array(array)
- array.map do |item|
- item.kind_of?(Symbol) ? item.to_s : item
+ def now_is_loaded?
+ @now
end
- end
- end
- def initialize(app)
- @app = app
+ private
+ def stringify_array(array) # :doc:
+ array.map do |item|
+ item.kind_of?(Symbol) ? item.to_s : item
+ end
+ end
end
- def call(env)
- @app.call(env)
- ensure
- session = Request::Session.find(env) || {}
- flash_hash = env[KEY]
-
- if flash_hash && (flash_hash.present? || session.key?('flash'))
- session["flash"] = flash_hash.to_session_value
- env[KEY] = flash_hash.dup
- end
+ def self.new(app) app; end
+ end
- 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
- end
+ class Request
+ prepend Flash::RequestMethods
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/load_interlock.rb b/actionpack/lib/action_dispatch/middleware/load_interlock.rb
deleted file mode 100644
index 07f498319c..0000000000
--- a/actionpack/lib/action_dispatch/middleware/load_interlock.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-require 'active_support/dependencies'
-require 'rack/body_proxy'
-
-module ActionDispatch
- class LoadInterlock
- def initialize(app)
- @app = app
- end
-
- def call(env)
- interlock = ActiveSupport::Dependencies.interlock
- interlock.start_running
- response = @app.call(env)
- body = Rack::BodyProxy.new(response[2]) { interlock.done_running }
- response[2] = body
- response
- ensure
- interlock.done_running unless body
- end
- end
-end
diff --git a/actionpack/lib/action_dispatch/middleware/params_parser.rb b/actionpack/lib/action_dispatch/middleware/params_parser.rb
deleted file mode 100644
index e2b3b06fd8..0000000000
--- a/actionpack/lib/action_dispatch/middleware/params_parser.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-require 'active_support/core_ext/hash/conversions'
-require 'action_dispatch/http/request'
-require 'active_support/core_ext/hash/indifferent_access'
-
-module ActionDispatch
- class ParamsParser
- class ParseError < StandardError
- attr_reader :original_exception
-
- def initialize(message, original_exception)
- super(message)
- @original_exception = original_exception
- end
- end
-
- DEFAULT_PARSERS = {
- Mime::JSON => lambda { |raw_post|
- data = ActiveSupport::JSON.decode(raw_post)
- data = {:_json => data} unless data.is_a?(Hash)
- Request::Utils.deep_munge(data).with_indifferent_access
- }
- }
-
- def initialize(app, parsers = {})
- @app, @parsers = app, DEFAULT_PARSERS.merge(parsers)
- end
-
- def call(env)
- default = env["action_dispatch.request.request_parameters"]
- env["action_dispatch.request.request_parameters"] = parse_formatted_parameters(env, @parsers, default)
-
- @app.call(env)
- end
-
- private
- def parse_formatted_parameters(env, parsers, default)
- request = Request.new(env)
-
- return default if request.content_length.zero?
-
- strategy = parsers.fetch(request.content_mime_type) { return default }
-
- strategy.call(request.raw_post)
-
- rescue => e # JSON or Ruby code block errors
- logger(env).debug "Error occurred while parsing request parameters.\nContents:\n\n#{request.raw_post}"
-
- raise ParseError.new(e.message, e)
- end
-
- def logger(env)
- env['action_dispatch.logger'] || ActiveSupport::Logger.new($stderr)
- end
- end
-end
diff --git a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb
index 7cde76b30e..3feb3a19f3 100644
--- a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb
@@ -1,11 +1,13 @@
+# frozen_string_literal: true
+
module ActionDispatch
# When called, this middleware renders an error page. By default if an HTML
- # response is expected it will render static error pages from the `/public`
+ # response is expected it will render static error pages from the <tt>/public</tt>
# directory. For example when this middleware receives a 500 response it will
- # render the template found in `/public/500.html`.
+ # render the template found in <tt>/public/500.html</tt>.
# If an internationalized locale is set, this middleware will attempt to render
- # the template in `/public/500.<locale>.html`. If an internationalized template
- # is not found it will fall back on `/public/500.html`.
+ # the template in <tt>/public/500.<locale>.html</tt>. If an internationalized template
+ # is not found it will fall back on <tt>/public/500.html</tt>.
#
# When a request with a content type other than HTML is made, this middleware
# will attempt to convert error information into the appropriate response type.
@@ -17,39 +19,39 @@ module ActionDispatch
end
def call(env)
- status = env["PATH_INFO"][1..-1].to_i
request = ActionDispatch::Request.new(env)
+ status = request.path_info[1..-1].to_i
content_type = request.formats.first
- body = { :status => status, :error => Rack::Utils::HTTP_STATUS_CODES.fetch(status, Rack::Utils::HTTP_STATUS_CODES[500]) }
+ body = { status: status, error: Rack::Utils::HTTP_STATUS_CODES.fetch(status, Rack::Utils::HTTP_STATUS_CODES[500]) }
render(status, content_type, body)
end
private
- def render(status, content_type, body)
- format = "to_#{content_type.to_sym}" if content_type
- if format && body.respond_to?(format)
- render_format(status, content_type, body.public_send(format))
- else
- render_html(status)
+ def render(status, content_type, body)
+ format = "to_#{content_type.to_sym}" if content_type
+ if format && body.respond_to?(format)
+ render_format(status, content_type, body.public_send(format))
+ else
+ render_html(status)
+ end
end
- end
- def render_format(status, content_type, body)
- [status, {'Content-Type' => "#{content_type}; charset=#{ActionDispatch::Response.default_charset}",
- 'Content-Length' => body.bytesize.to_s}, [body]]
- end
+ def render_format(status, content_type, body)
+ [status, { "Content-Type" => "#{content_type}; charset=#{ActionDispatch::Response.default_charset}",
+ "Content-Length" => body.bytesize.to_s }, [body]]
+ end
- def render_html(status)
- path = "#{public_path}/#{status}.#{I18n.locale}.html"
- path = "#{public_path}/#{status}.html" unless (found = File.exist?(path))
+ def render_html(status)
+ path = "#{public_path}/#{status}.#{I18n.locale}.html"
+ path = "#{public_path}/#{status}.html" unless (found = File.exist?(path))
- if found || File.exist?(path)
- render_format(status, 'text/html', File.read(path))
- else
- [404, { "X-Cascade" => "pass" }, []]
+ if found || File.exist?(path)
+ render_format(status, "text/html", File.read(path))
+ else
+ [404, { "X-Cascade" => "pass" }, []]
+ end
end
- end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/reloader.rb b/actionpack/lib/action_dispatch/middleware/reloader.rb
index 6c7fba00cb..8bb3ba7504 100644
--- a/actionpack/lib/action_dispatch/middleware/reloader.rb
+++ b/actionpack/lib/action_dispatch/middleware/reloader.rb
@@ -1,98 +1,12 @@
-require 'active_support/deprecation/reporting'
+# frozen_string_literal: true
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.
- #
- class Reloader
- include ActiveSupport::Callbacks
- include ActiveSupport::Deprecation::Reporting
-
- define_callbacks :prepare
- define_callbacks :cleanup
-
- # Add a prepare callback. Prepare callbacks are run before each request, prior
- # to ActionDispatch::Callback's before callbacks.
- def self.to_prepare(*args, &block)
- unless block_given?
- warn "to_prepare without a block is deprecated. Please use a block"
- end
- set_callback(:prepare, *args, &block)
- end
-
- # Add a cleanup callback. Cleanup callbacks are run after each request is
- # complete (after #close is called on the response body).
- def self.to_cleanup(*args, &block)
- unless block_given?
- warn "to_cleanup without a block is deprecated. Please use a block"
- end
- set_callback(:cleanup, *args, &block)
- end
-
- # Execute all prepare callbacks.
- def self.prepare!
- new(nil).prepare!
- end
-
- # Execute all cleanup callbacks.
- def self.cleanup!
- new(nil).cleanup!
- end
-
- def initialize(app, condition=nil)
- @app = app
- @condition = condition || lambda { true }
- @validated = true
- end
-
- def call(env)
- @validated = @condition.call
- prepare!
-
- response = @app.call(env)
- response[2] = ::Rack::BodyProxy.new(response[2]) { cleanup! }
-
- response
- rescue Exception
- cleanup!
- raise
- end
-
- def prepare! #:nodoc:
- run_callbacks :prepare if validated?
- end
-
- def cleanup! #:nodoc:
- run_callbacks :cleanup if validated?
- ensure
- @validated = true
- end
-
- private
-
- def validated? #:nodoc:
- @validated
- end
+ # is false.
+ class Reloader < Executor
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb
index 9f894e2ec6..35158f9062 100644
--- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb
+++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb
@@ -1,4 +1,6 @@
-require 'ipaddr'
+# frozen_string_literal: true
+
+require "ipaddr"
module ActionDispatch
# This middleware calculates the IP address of the remote client that is
@@ -10,7 +12,7 @@ module ActionDispatch
# by @gingerlime. A more detailed explanation of the algorithm is given
# at GetIp#calculate_ip.
#
- # Some Rack servers concatenate repeated headers, like {HTTP RFC 2616}[http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2]
+ # Some Rack servers concatenate repeated headers, like {HTTP RFC 2616}[https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2]
# requires. Some Rack servers simply drop preceding headers, and only report
# the value that was {given in the last header}[http://andre.arko.net/2011/12/26/repeated-headers-and-ruby-web-servers].
# If you are behind multiple proxy servers (like NGINX to HAProxy to Unicorn)
@@ -29,7 +31,7 @@ module ActionDispatch
# The default trusted IPs list simply includes IP addresses that are
# guaranteed by the IP specification to be private addresses. Those will
# not be the ultimate client IP in production, and so are discarded. See
- # http://en.wikipedia.org/wiki/Private_network for details.
+ # https://en.wikipedia.org/wiki/Private_network for details.
TRUSTED_PROXIES = [
"127.0.0.1", # localhost IPv4
"::1", # localhost IPv6
@@ -43,7 +45,7 @@ module ActionDispatch
# Create a new +RemoteIp+ middleware instance.
#
- # The +check_ip_spoofing+ option is on by default. When on, an exception
+ # The +ip_spoofing_check+ option is on by default. When on, an exception
# is raised if it looks like the client is trying to lie about its own IP
# address. It makes sense to turn off this check on sites aimed at non-IP
# clients (like WAP devices), or behind proxies that set headers in an
@@ -57,9 +59,9 @@ module ActionDispatch
# with your proxy servers after it. If your proxies aren't removed, pass
# them in via the +custom_proxies+ parameter. That way, the middleware will
# ignore those IP addresses, and return the one that you want.
- def initialize(app, check_ip_spoofing = true, custom_proxies = nil)
+ def initialize(app, ip_spoofing_check = true, custom_proxies = nil)
@app = app
- @check_ip = check_ip_spoofing
+ @check_ip = ip_spoofing_check
@proxies = if custom_proxies.blank?
TRUSTED_PROXIES
elsif custom_proxies.respond_to?(:any?)
@@ -74,16 +76,17 @@ module ActionDispatch
# requests. For those requests that do need to know the IP, the
# GetIp#calculate_ip method will calculate the memoized client IP address.
def call(env)
- env["action_dispatch.remote_ip"] = GetIp.new(env, check_ip, proxies)
- @app.call(env)
+ req = ActionDispatch::Request.new env
+ req.remote_ip = GetIp.new(req, check_ip, proxies)
+ @app.call(req.env)
end
# The GetIp class exists as a way to defer processing of the request data
# into an actual IP address. If the ActionDispatch::Request#remote_ip method
# is called, this class will calculate the value and then memoize it.
class GetIp
- def initialize(env, check_ip, proxies)
- @env = env
+ def initialize(req, check_ip, proxies)
+ @req = req
@check_ip = check_ip
@proxies = proxies
end
@@ -108,23 +111,31 @@ module ActionDispatch
# the last address left, which was presumably set by one of those proxies.
def calculate_ip
# Set by the Rack web server, this is a single value.
- remote_addr = ips_from('REMOTE_ADDR').last
+ remote_addr = ips_from(@req.remote_addr).last
# Could be a CSV list and/or repeated headers that were concatenated.
- client_ips = ips_from('HTTP_CLIENT_IP').reverse
- forwarded_ips = ips_from('HTTP_X_FORWARDED_FOR').reverse
+ client_ips = ips_from(@req.client_ip).reverse
+ forwarded_ips = ips_from(@req.x_forwarded_for).reverse
# +Client-Ip+ and +X-Forwarded-For+ should not, generally, both be set.
- # If they are both set, it means that this request passed through two
- # proxies with incompatible IP header conventions, and there is no way
- # for us to determine which header is the right one after the fact.
- # Since we have no idea, we give up and explode.
+ # If they are both set, it means that either:
+ #
+ # 1) This request passed through two proxies with incompatible IP header
+ # conventions.
+ # 2) The client passed one of +Client-Ip+ or +X-Forwarded-For+
+ # (whichever the proxy servers weren't using) themselves.
+ #
+ # Either way, there is no way for us to determine which header is the
+ # right one after the fact. Since we have no idea, if we are concerned
+ # about IP spoofing we need to give up and explode. (If you're not
+ # concerned about IP spoofing you can turn the +ip_spoofing_check+
+ # option off.)
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=#{@env['HTTP_CLIENT_IP'].inspect} " +
- "HTTP_X_FORWARDED_FOR=#{@env['HTTP_X_FORWARDED_FOR'].inspect}"
+ raise IpSpoofAttackError, "IP spoofing attack?! " \
+ "HTTP_CLIENT_IP=#{@req.client_ip.inspect} " \
+ "HTTP_X_FORWARDED_FOR=#{@req.x_forwarded_for.inspect}"
end
# We assume these things about the IP headers:
@@ -144,16 +155,17 @@ module ActionDispatch
@ip ||= calculate_ip
end
- protected
+ private
- def ips_from(header)
- # Split the comma-separated list into an array of strings
- ips = @env[header] ? @env[header].strip.split(/[,\s]+/) : []
+ def ips_from(header) # :doc:
+ return [] unless header
+ # 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
@@ -161,13 +173,11 @@ module ActionDispatch
end
end
- def filter_proxies(ips)
+ def filter_proxies(ips) # :doc:
ips.reject do |ip|
@proxies.any? { |proxy| proxy === ip }
end
end
-
end
-
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/request_id.rb b/actionpack/lib/action_dispatch/middleware/request_id.rb
index 1555ff72af..fcc0c72240 100644
--- a/actionpack/lib/action_dispatch/middleware/request_id.rb
+++ b/actionpack/lib/action_dispatch/middleware/request_id.rb
@@ -1,9 +1,12 @@
-require 'securerandom'
-require 'active_support/core_ext/string/access'
+# frozen_string_literal: true
+
+require "securerandom"
+require "active_support/core_ext/string/access"
module ActionDispatch
- # Makes a unique request id available to the action_dispatch.request_id env variable (which is then accessible through
- # ActionDispatch::Request#uuid or the alias ActionDispatch::Request#request_id) and sends the same id to the client via the X-Request-Id header.
+ # Makes a unique request id available to the +action_dispatch.request_id+ env variable (which is then accessible
+ # through <tt>ActionDispatch::Request#request_id</tt> or the alias <tt>ActionDispatch::Request#uuid</tt>) and sends
+ # the same id to the client via the X-Request-Id header.
#
# The unique request id is either based on the X-Request-Id header in the request, which would typically be generated
# by a firewall, load balancer, or the web server, or, if this header is not available, a random uuid. If the
@@ -12,7 +15,7 @@ module ActionDispatch
# The unique request id can be used to trace a request end-to-end and would typically end up being part of log files
# from multiple pieces of the stack.
class RequestId
- X_REQUEST_ID = "X-Request-Id".freeze # :nodoc:
+ X_REQUEST_ID = "X-Request-Id" #:nodoc:
def initialize(app)
@app = app
@@ -27,7 +30,7 @@ module ActionDispatch
private
def make_request_id(request_id)
if request_id.presence
- request_id.gsub(/[^\w\-]/, "".freeze).first(255)
+ request_id.gsub(/[^\w\-@]/, "").first(255)
else
internal_request_id
end
diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb
index 84df55fd5a..5b0be96223 100644
--- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb
@@ -1,26 +1,25 @@
-require 'rack/utils'
-require 'rack/request'
-require 'rack/session/abstract/id'
-require 'action_dispatch/middleware/cookies'
-require 'action_dispatch/request/session'
+# frozen_string_literal: true
+
+require "rack/utils"
+require "rack/request"
+require "rack/session/abstract/id"
+require "action_dispatch/middleware/cookies"
+require "action_dispatch/request/session"
module ActionDispatch
module Session
class SessionRestoreError < StandardError #:nodoc:
- attr_reader :original_exception
-
- def initialize(const_error)
- @original_exception = const_error
-
- 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: #{const_error.message} [#{const_error.class}])\n")
+ 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" \
+ "(Original exception: #{$!.message} [#{$!.class}])\n")
+ set_backtrace $!.backtrace
end
end
module Compatibility
def initialize(app, options = {})
- options[:key] ||= '_session_id'
+ options[:key] ||= "_session_id"
super
end
@@ -30,12 +29,16 @@ module ActionDispatch
sid
end
- protected
+ private
- def initialize_sid
+ def initialize_sid # :doc:
@default_options.delete(:sidbits)
@default_options.delete(:secure_random)
end
+
+ def make_request(env)
+ ActionDispatch::Request.new env
+ end
end
module StaleSessionCheck
@@ -52,10 +55,10 @@ 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 => e
- raise ActionDispatch::Session::SessionRestoreError, e, e.backtrace
+ rescue LoadError, NameError
+ raise ActionDispatch::Session::SessionRestoreError
end
retry
else
@@ -65,8 +68,8 @@ module ActionDispatch
end
module SessionObject # :nodoc:
- def prepare_session(env)
- Request::Session.create(self, env, @default_options)
+ def prepare_session(req)
+ Request::Session.create(self, req, @default_options)
end
def loaded_session?(session)
@@ -74,17 +77,16 @@ module ActionDispatch
end
end
- class AbstractStore < Rack::Session::Abstract::ID
+ class AbstractStore < Rack::Session::Abstract::Persisted
include Compatibility
include StaleSessionCheck
include SessionObject
private
- def set_cookie(env, session_id, cookie)
- request = ActionDispatch::Request.new(env)
- request.cookie_jar[key] = cookie
- end
+ def set_cookie(request, session_id, cookie)
+ request.cookie_jar[key] = cookie
+ end
end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb
index 857e49a682..a6d965a644 100644
--- a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb
@@ -1,4 +1,6 @@
-require 'action_dispatch/middleware/session/abstract_store'
+# frozen_string_literal: true
+
+require "action_dispatch/middleware/session/abstract_store"
module ActionDispatch
module Session
@@ -18,18 +20,18 @@ module ActionDispatch
end
# Get a session from the cache.
- def get_session(env, sid)
- unless sid and session = @cache.read(cache_key(sid))
+ def find_session(env, sid)
+ unless sid && (session = @cache.read(cache_key(sid)))
sid, session = generate_sid, {}
end
[sid, session]
end
# Set a session in the cache.
- def set_session(env, sid, session, options)
+ def write_session(env, sid, session, options)
key = cache_key(sid)
if session
- @cache.write(key, session, :expires_in => options[:expire_after])
+ @cache.write(key, session, expires_in: options[:expire_after])
else
@cache.delete(key)
end
@@ -37,7 +39,7 @@ module ActionDispatch
end
# Remove a session from the cache.
- def destroy_session(env, sid, options)
+ def delete_session(env, sid, options)
@cache.delete(cache_key(sid))
generate_sid
end
diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
index d8f9614904..df680c1c5f 100644
--- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
@@ -1,6 +1,8 @@
-require 'active_support/core_ext/hash/keys'
-require 'action_dispatch/middleware/session/abstract_store'
-require 'rack/session/cookie'
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/keys"
+require "action_dispatch/middleware/session/abstract_store"
+require "rack/session/cookie"
module ActionDispatch
module Session
@@ -19,41 +21,27 @@ module ActionDispatch
# knowing your app's secret key, but can easily read their +user_id+. This
# was the default for Rails 3 apps.
#
- # If you have secret_key_base set, your cookies will be encrypted. This
+ # Your cookies will be encrypted using your apps secret_key_base. This
# goes a step further than signed cookies in that encrypted cookies cannot
# be altered or read by users. This is the default starting in Rails 4.
#
- # If you have both secret_token and secret_key base set, your cookies will
- # be encrypted, and signed cookies generated by Rails 3 will be
- # transparently read and encrypted to provide a smooth upgrade path.
- #
- # Configure your session store in config/initializers/session_store.rb:
+ # Configure your session store in an initializer:
#
# Rails.application.config.session_store :cookie_store, key: '_your_app_session'
#
- # Configure your secret key in config/secrets.yml:
- #
- # development:
- # secret_key_base: 'secret key'
+ # By default, your secret key base is derived from your application name in
+ # the test and development environments. In all other environments, it is stored
+ # encrypted in the <tt>config/credentials.yml.enc</tt> file.
#
- # To generate a secret key for an existing application, run `rake secret`.
+ # If your application was not updated to Rails 5.2 defaults, the secret_key_base
+ # will be found in the old <tt>config/secrets.yml</tt> file.
#
- # If you are upgrading an existing Rails 3 app, you should leave your
- # existing secret_token in place and simply add the new secret_key_base.
- # Note that you should wait to set secret_key_base until you have 100% of
- # your userbase on Rails 4 and are reasonably sure you will not need to
- # rollback to Rails 3. This is because cookies signed based on the new
- # secret_key_base in Rails 4 are not backwards compatible with Rails 3.
- # You are free to leave your existing secret_token in place, not set the
- # new secret_key_base, and ignore the deprecation warnings until you are
- # reasonably sure that your upgrade is otherwise complete. Additionally,
- # you should take care to make sure you are not relying on the ability to
- # decode signed cookies generated by your app in external applications or
- # JavaScript before upgrading.
+ # Note that changing your secret_key_base will invalidate all existing session.
+ # Additionally, you should take care to make sure you are not relying on the
+ # ability to decode signed cookies generated by your app in external
+ # applications or JavaScript before changing it.
#
- # Note that changing the secret key will invalidate all existing sessions!
- #
- # Because CookieStore extends Rack::Session::Abstract::ID, many of the
+ # Because CookieStore extends Rack::Session::Abstract::Persisted, many of the
# options described there can be used to customize the session cookie that
# is generated. For example:
#
@@ -62,25 +50,21 @@ module ActionDispatch
# would set the session cookie to expire automatically 14 days after creation.
# Other useful options include <tt>:key</tt>, <tt>:secure</tt> and
# <tt>:httponly</tt>.
- class CookieStore < Rack::Session::Abstract::ID
- include Compatibility
- include StaleSessionCheck
- include SessionObject
-
- def initialize(app, options={})
- super(app, options.merge!(:cookie_only => true))
+ class CookieStore < AbstractStore
+ def initialize(app, options = {})
+ super(app, options.merge!(cookie_only: true))
end
- def destroy_session(env, session_id, options)
+ def delete_session(req, session_id, options)
new_sid = generate_sid unless options[:drop]
# Reset hash and Assign the new session id
- env["action_dispatch.request.unsigned_session_cookie"] = new_sid ? { "session_id" => new_sid } : {}
+ req.set_header("action_dispatch.request.unsigned_session_cookie", new_sid ? { "session_id" => new_sid } : {})
new_sid
end
- def load_session(env)
+ def load_session(req)
stale_session_check! do
- data = unpacked_cookie_data(env)
+ data = unpacked_cookie_data(req)
data = persistent_session_id!(data)
[data["session_id"], data]
end
@@ -88,46 +72,46 @@ module ActionDispatch
private
- def extract_session_id(env)
- stale_session_check! do
- unpacked_cookie_data(env)["session_id"]
+ def extract_session_id(req)
+ stale_session_check! do
+ unpacked_cookie_data(req)["session_id"]
+ end
end
- end
- def unpacked_cookie_data(env)
- env["action_dispatch.request.unsigned_session_cookie"] ||= begin
- stale_session_check! do
- if data = get_cookie(env)
- data.stringify_keys!
+ def unpacked_cookie_data(req)
+ req.fetch_header("action_dispatch.request.unsigned_session_cookie") do |k|
+ v = stale_session_check! do
+ if data = get_cookie(req)
+ data.stringify_keys!
+ end
+ data || {}
end
- data || {}
+ req.set_header k, v
end
end
- end
- def persistent_session_id!(data, sid=nil)
- data ||= {}
- data["session_id"] ||= sid || generate_sid
- data
- end
+ def persistent_session_id!(data, sid = nil)
+ data ||= {}
+ data["session_id"] ||= sid || generate_sid
+ data
+ end
- def set_session(env, sid, session_data, options)
- session_data["session_id"] = sid
- session_data
- end
+ def write_session(req, sid, session_data, options)
+ session_data["session_id"] = sid
+ session_data
+ end
- def set_cookie(env, session_id, cookie)
- cookie_jar(env)[@key] = cookie
- end
+ def set_cookie(request, session_id, cookie)
+ cookie_jar(request)[@key] = cookie
+ end
- def get_cookie(env)
- cookie_jar(env)[@key]
- end
+ def get_cookie(req)
+ cookie_jar(req)[@key]
+ end
- def cookie_jar(env)
- request = ActionDispatch::Request.new(env)
- request.cookie_jar.signed_or_encrypted
- end
+ def cookie_jar(request)
+ request.cookie_jar.signed_or_encrypted
+ end
end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb
index cb19786f0b..914df3a2b1 100644
--- a/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb
@@ -1,6 +1,8 @@
-require 'action_dispatch/middleware/session/abstract_store'
+# frozen_string_literal: true
+
+require "action_dispatch/middleware/session/abstract_store"
begin
- require 'rack/session/dalli'
+ require "rack/session/dalli"
rescue LoadError => e
$stderr.puts "You don't have dalli installed in your application. Please add it to your Gemfile and run bundle install"
raise e
diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
index f0779279c1..3c88afd4d3 100644
--- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
@@ -1,5 +1,7 @@
-require 'action_dispatch/http/request'
-require 'action_dispatch/middleware/exception_wrapper'
+# frozen_string_literal: true
+
+require "action_dispatch/http/request"
+require "action_dispatch/middleware/exception_wrapper"
module ActionDispatch
# This middleware rescues any exception returned by the application
@@ -8,14 +10,14 @@ 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.
# If any exception happens inside the exceptions app, this middleware
# catches the exceptions and returns a FAILSAFE_RESPONSE.
class ShowExceptions
- FAILSAFE_RESPONSE = [500, { 'Content-Type' => 'text/plain' },
+ FAILSAFE_RESPONSE = [500, { "Content-Type" => "text/plain" },
["500 Internal Server Error\n" \
"If you are the administrator of this website, then please read this web " \
"application's log file and/or the web server's log file to find out what " \
@@ -27,32 +29,34 @@ module ActionDispatch
end
def call(env)
+ request = ActionDispatch::Request.new env
@app.call(env)
rescue Exception => exception
- if env['action_dispatch.show_exceptions'] == false
- raise exception
+ if request.show_exceptions?
+ render_exception(request, exception)
else
- render_exception(env, exception)
+ raise exception
end
end
private
- def render_exception(env, exception)
- wrapper = ExceptionWrapper.new(env, exception)
- status = wrapper.status_code
- env["action_dispatch.exception"] = wrapper.exception
- env["action_dispatch.original_path"] = env["PATH_INFO"]
- env["PATH_INFO"] = "/#{status}"
- response = @exceptions_app.call(env)
- response[1]['X-Cascade'] == 'pass' ? pass_response(status) : response
- rescue Exception => failsafe_error
- $stderr.puts "Error during failsafe response: #{failsafe_error}\n #{failsafe_error.backtrace * "\n "}"
- FAILSAFE_RESPONSE
- end
+ def render_exception(request, exception)
+ backtrace_cleaner = request.get_header "action_dispatch.backtrace_cleaner"
+ wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
+ status = wrapper.status_code
+ request.set_header "action_dispatch.exception", wrapper.exception
+ request.set_header "action_dispatch.original_path", request.path_info
+ request.path_info = "/#{status}"
+ response = @exceptions_app.call(request.env)
+ response[1]["X-Cascade"] == "pass" ? pass_response(status) : response
+ rescue Exception => failsafe_error
+ $stderr.puts "Error during failsafe response: #{failsafe_error}\n #{failsafe_error.backtrace * "\n "}"
+ FAILSAFE_RESPONSE
+ end
- def pass_response(status)
- [status, {"Content-Type" => "text/html; charset=#{Response.default_charset}", "Content-Length" => "0"}, []]
- end
+ def pass_response(status)
+ [status, { "Content-Type" => "text/html; charset=#{Response.default_charset}", "Content-Length" => "0" }, []]
+ end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb
index 7b3d8bcc5b..00902ede21 100644
--- a/actionpack/lib/action_dispatch/middleware/ssl.rb
+++ b/actionpack/lib/action_dispatch/middleware/ssl.rb
@@ -1,66 +1,119 @@
+# frozen_string_literal: true
+
module ActionDispatch
+ # This middleware is added to the stack when <tt>config.force_ssl = true</tt>, and is passed
+ # the options set in +config.ssl_options+. It does three jobs to enforce secure HTTP
+ # requests:
+ #
+ # 1. <b>TLS redirect</b>: Permanently redirects +http://+ requests to +https://+
+ # with the same URL host, path, etc. Enabled by default. Set +config.ssl_options+
+ # to modify the destination URL
+ # (e.g. <tt>redirect: { host: "secure.widgets.com", port: 8080 }</tt>), or set
+ # <tt>redirect: false</tt> to disable this feature.
+ #
+ # Requests can opt-out of redirection with +exclude+:
+ #
+ # config.ssl_options = { redirect: { exclude: -> request { request.path =~ /healthcheck/ } } }
+ #
+ # Cookies will not be flagged as secure for excluded requests.
+ #
+ # 2. <b>Secure cookies</b>: Sets the +secure+ flag on cookies to tell browsers they
+ # must not be sent along with +http://+ requests. Enabled by default. Set
+ # +config.ssl_options+ with <tt>secure_cookies: false</tt> to disable this feature.
+ #
+ # 3. <b>HTTP Strict Transport Security (HSTS)</b>: Tells the browser to remember
+ # this site as TLS-only and automatically redirect non-TLS requests.
+ # Enabled by default. Configure +config.ssl_options+ with <tt>hsts: false</tt> to disable.
+ #
+ # Set +config.ssl_options+ with <tt>hsts: { ... }</tt> to configure HSTS:
+ #
+ # * +expires+: How long, in seconds, these settings will stick. The minimum
+ # required to qualify for browser preload lists is 1 year. Defaults to
+ # 1 year (recommended).
+ #
+ # * +subdomains+: Set to +true+ to tell the browser to apply these settings
+ # to all subdomains. This protects your cookies from interception by a
+ # vulnerable site on a subdomain. Defaults to +true+.
+ #
+ # * +preload+: Advertise that this site may be included in browsers'
+ # preloaded HSTS lists. HSTS protects your site on every visit <i>except the
+ # first visit</i> since it hasn't seen your HSTS header yet. To close this
+ # gap, browser vendors include a baked-in list of HSTS-enabled sites.
+ # Go to https://hstspreload.org to submit your site for inclusion.
+ # Defaults to +false+.
+ #
+ # To turn off HSTS, omitting the header is not enough. Browsers will remember the
+ # original HSTS directive until it expires. Instead, use the header to tell browsers to
+ # expire HSTS immediately. Setting <tt>hsts: false</tt> is a shortcut for
+ # <tt>hsts: { expires: 0 }</tt>.
class SSL
- YEAR = 31536000
+ # :stopdoc:
+
+ # Default to 1 year, the minimum for browser preload lists.
+ HSTS_EXPIRES_IN = 31536000
def self.default_hsts_options
- { :expires => YEAR, :subdomains => false }
+ { expires: HSTS_EXPIRES_IN, subdomains: true, preload: false }
end
- def initialize(app, options = {})
+ def initialize(app, redirect: {}, hsts: {}, secure_cookies: true)
@app = app
- @hsts = options.fetch(:hsts, {})
- @hsts = {} if @hsts == true
- @hsts = self.class.default_hsts_options.merge(@hsts) if @hsts
+ @redirect = redirect
+
+ @exclude = @redirect && @redirect[:exclude] || proc { !@redirect }
+ @secure_cookies = secure_cookies
- @host = options[:host]
- @port = options[:port]
+ @hsts_header = build_hsts_header(normalize_hsts_options(hsts))
end
def call(env)
- request = Request.new(env)
+ request = Request.new env
if request.ssl?
- status, headers, body = @app.call(env)
- headers.reverse_merge!(hsts_headers)
- flag_cookies_as_secure!(headers)
- [status, headers, body]
+ @app.call(env).tap do |status, headers, body|
+ set_hsts_header! headers
+ flag_cookies_as_secure! headers if @secure_cookies && !@exclude.call(request)
+ end
else
- redirect_to_https(request)
+ return redirect_to_https request unless @exclude.call(request)
+ @app.call(env)
end
end
private
- def redirect_to_https(request)
- host = @host || request.host
- port = @port || request.port
-
- location = "https://#{host}"
- location << ":#{port}" if port != 80
- location << request.fullpath
-
- headers = { 'Content-Type' => 'text/html', 'Location' => location }
-
- [301, headers, []]
+ def set_hsts_header!(headers)
+ headers["Strict-Transport-Security"] ||= @hsts_header
end
- # http://tools.ietf.org/html/draft-hodges-strict-transport-sec-02
- def hsts_headers
- if @hsts
- value = "max-age=#{@hsts[:expires].to_i}"
- value += "; includeSubDomains" if @hsts[:subdomains]
- { 'Strict-Transport-Security' => value }
+ def normalize_hsts_options(options)
+ case options
+ # Explicitly disabling HSTS clears the existing setting from browsers
+ # by setting expiry to 0.
+ when false
+ self.class.default_hsts_options.merge(expires: 0)
+ # Default to enabled, with default options.
+ when nil, true
+ self.class.default_hsts_options
else
- {}
+ self.class.default_hsts_options.merge(options)
end
end
+ # https://tools.ietf.org/html/rfc6797#section-6.1
+ def build_hsts_header(hsts)
+ value = +"max-age=#{hsts[:expires].to_i}"
+ value << "; includeSubDomains" if hsts[:subdomains]
+ value << "; preload" if hsts[:preload]
+ value
+ end
+
def flag_cookies_as_secure!(headers)
- if cookies = headers['Set-Cookie']
+ if cookies = headers["Set-Cookie"]
cookies = cookies.split("\n")
- headers['Set-Cookie'] = cookies.map { |cookie|
- if cookie !~ /;\s*secure\s*(;|$)/i
+ headers["Set-Cookie"] = cookies.map { |cookie|
+ if !/;\s*secure\s*(;|$)/i.match?(cookie)
"#{cookie}; secure"
else
cookie
@@ -68,5 +121,30 @@ module ActionDispatch
}.join("\n")
end
end
+
+ def redirect_to_https(request)
+ [ @redirect.fetch(:status, redirection_status(request)),
+ { "Content-Type" => "text/html",
+ "Location" => https_location_for(request) },
+ @redirect.fetch(:body, []) ]
+ end
+
+ def redirection_status(request)
+ if request.get? || request.head?
+ 301 # Issue a permanent redirect via a GET request.
+ else
+ 307 # Issue a fresh request redirect to preserve the HTTP method.
+ end
+ end
+
+ def https_location_for(request)
+ host = @redirect[:host] || request.host
+ port = @redirect[:port] || request.port
+
+ location = +"https://#{host}"
+ location << ":#{port}" if port != 80 && port != 443
+ location << request.fullpath
+ location
+ end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/stack.rb b/actionpack/lib/action_dispatch/middleware/stack.rb
index bbf734f103..b82f8aa3a3 100644
--- a/actionpack/lib/action_dispatch/middleware/stack.rb
+++ b/actionpack/lib/action_dispatch/middleware/stack.rb
@@ -1,28 +1,20 @@
+# frozen_string_literal: true
+
require "active_support/inflector/methods"
require "active_support/dependencies"
module ActionDispatch
class MiddlewareStack
class Middleware
- attr_reader :args, :block, :name, :classcache
-
- def initialize(klass_or_name, *args, &block)
- @klass = nil
-
- if klass_or_name.respond_to?(:name)
- @klass = klass_or_name
- @name = @klass.name
- else
- @name = klass_or_name.to_s
- end
+ attr_reader :args, :block, :klass
- @classcache = ActiveSupport::Dependencies::Reference
- @args, @block = args, block
+ def initialize(klass, args, block)
+ @klass = klass
+ @args = args
+ @block = block
end
- def klass
- @klass || classcache[@name]
- end
+ def name; klass.name; end
def ==(middleware)
case middleware
@@ -30,24 +22,20 @@ module ActionDispatch
klass == middleware.klass
when Class
klass == middleware
- else
- normalize(@name) == normalize(middleware)
end
end
def inspect
- klass.to_s
+ if klass.is_a?(Class)
+ klass.to_s
+ else
+ klass.class.to_s
+ end
end
def build(app)
klass.new(app, *args, &block)
end
-
- private
-
- def normalize(object)
- object.to_s.strip.sub(/^::/, '')
- end
end
include Enumerable
@@ -75,19 +63,17 @@ module ActionDispatch
middlewares[i]
end
- def unshift(*args, &block)
- middleware = self.class::Middleware.new(*args, &block)
- middlewares.unshift(middleware)
+ def unshift(klass, *args, &block)
+ middlewares.unshift(build_middleware(klass, args, block))
end
def initialize_copy(other)
self.middlewares = other.middlewares.dup
end
- def insert(index, *args, &block)
+ def insert(index, klass, *args, &block)
index = assert_index(index, :before)
- middleware = self.class::Middleware.new(*args, &block)
- middlewares.insert(index, middleware)
+ middlewares.insert(index, build_middleware(klass, args, block))
end
alias_method :insert_before, :insert
@@ -104,26 +90,27 @@ module ActionDispatch
end
def delete(target)
- middlewares.delete target
+ middlewares.delete_if { |m| m.klass == target }
end
- def use(*args, &block)
- middleware = self.class::Middleware.new(*args, &block)
- middlewares.push(middleware)
+ def use(klass, *args, &block)
+ middlewares.push(build_middleware(klass, args, block))
end
- def build(app = nil, &block)
- app ||= block
- raise "MiddlewareStack#build requires an app" unless app
+ def build(app = Proc.new)
middlewares.freeze.reverse.inject(app) { |a, e| e.build(a) }
end
- protected
+ private
- def assert_index(index, where)
- i = index.is_a?(Integer) ? index : middlewares.index(index)
- raise "No such middleware to insert #{where}: #{index.inspect}" unless i
- i
- end
+ def assert_index(index, where)
+ i = index.is_a?(Integer) ? index : middlewares.index { |m| m.klass == index }
+ raise "No such middleware to insert #{where}: #{index.inspect}" unless i
+ i
+ end
+
+ def build_middleware(klass, args, block)
+ Middleware.new(klass, args, block)
+ end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb
index b098ea389f..1f2f7757a3 100644
--- a/actionpack/lib/action_dispatch/middleware/static.rb
+++ b/actionpack/lib/action_dispatch/middleware/static.rb
@@ -1,73 +1,76 @@
-require 'rack/utils'
-require 'active_support/core_ext/uri'
+# frozen_string_literal: true
+
+require "rack/utils"
+require "active_support/core_ext/uri"
module ActionDispatch
# This middleware returns a file's contents from disk in the body response.
- # When initialized, it can accept an optional 'Cache-Control' header, which
- # will be set when a response containing a file's contents is delivered.
+ # When initialized, it can accept optional HTTP headers, which will be set
+ # when a response containing a file's contents is delivered.
#
- # This middleware will render the file specified in `env["PATH_INFO"]`
+ # This middleware will render the file specified in <tt>env["PATH_INFO"]</tt>
# where the base path is in the +root+ directory. For example, if the +root+
- # is set to `public/`, then a request with `env["PATH_INFO"]` of
- # `assets/application.js` will return a response with the contents of a file
- # located at `public/assets/application.js` if the file exists. If the file
+ # is set to +public/+, then a request with <tt>env["PATH_INFO"]</tt> of
+ # +assets/application.js+ will return a response with the contents of a file
+ # located at +public/assets/application.js+ if the file exists. If the file
# does not exist, a 404 "File not Found" response will be returned.
class FileHandler
- def initialize(root, cache_control, index: 'index')
- @root = root.chomp('/')
- @compiled_root = /^#{Regexp.escape(root)}/
- headers = cache_control && { 'Cache-Control' => cache_control }
- @file_server = ::Rack::File.new(@root, headers)
- @index = index
+ def initialize(root, index: "index", headers: {})
+ @root = root.chomp("/").b
+ @file_server = ::Rack::File.new(@root, headers)
+ @index = index
end
# Takes a path to a file. If the file is found, has valid encoding, and has
# correct read permissions, the return value is a URI-escaped string
# representing the filename. Otherwise, false is returned.
#
- # Used by the `Static` class to check the existence of a valid file
- # in the server's `public/` directory (see Static#call).
+ # Used by the +Static+ class to check the existence of a valid file
+ # in the server's +public/+ directory (see Static#call).
def match?(path)
- path = URI.parser.unescape(path)
- return false unless path.valid_encoding?
- path = Rack::Utils.clean_path_info path
+ path = ::Rack::Utils.unescape_path path
+ return false unless ::Rack::Utils.valid_path? path
+ path = ::Rack::Utils.clean_path_info path
paths = [path, "#{path}#{ext}", "#{path}/#{@index}#{ext}"]
if match = paths.detect { |p|
- path = File.join(@root, p.force_encoding('UTF-8'))
+ path = File.join(@root, p.b)
begin
File.file?(path) && File.readable?(path)
rescue SystemCallError
false
end
-
}
- return ::Rack::Utils.escape(match)
+ return ::Rack::Utils.escape_path(match).b
end
end
def call(env)
- path = env['PATH_INFO']
+ serve(Rack::Request.new(env))
+ end
+
+ def serve(request)
+ path = request.path_info
gzip_path = gzip_file_path(path)
- if gzip_path && gzip_encoding_accepted?(env)
- env['PATH_INFO'] = gzip_path
- status, headers, body = @file_server.call(env)
+ if gzip_path && gzip_encoding_accepted?(request)
+ request.path_info = gzip_path
+ status, headers, body = @file_server.call(request.env)
if status == 304
return [status, headers, body]
end
- headers['Content-Encoding'] = 'gzip'
- headers['Content-Type'] = content_type(path)
+ headers["Content-Encoding"] = "gzip"
+ headers["Content-Type"] = content_type(path)
else
- status, headers, body = @file_server.call(env)
+ status, headers, body = @file_server.call(request.env)
end
- headers['Vary'] = 'Accept-Encoding' if gzip_path
+ headers["Vary"] = "Accept-Encoding" if gzip_path
- return [status, headers, body]
+ [status, headers, body]
ensure
- env['PATH_INFO'] = path
+ request.path_info = path
end
private
@@ -76,17 +79,17 @@ module ActionDispatch
end
def content_type(path)
- ::Rack::Mime.mime_type(::File.extname(path), 'text/plain')
+ ::Rack::Mime.mime_type(::File.extname(path), "text/plain")
end
- def gzip_encoding_accepted?(env)
- env['HTTP_ACCEPT_ENCODING'] =~ /\bgzip\b/i
+ def gzip_encoding_accepted?(request)
+ request.accept_encoding.any? { |enc, quality| enc =~ /\bgzip\b/i }
end
def gzip_file_path(path)
can_gzip_mime = content_type(path) =~ /\A(?:text\/|application\/javascript)/
gzip_path = "#{path}.gz"
- if can_gzip_mime && File.exist?(File.join(@root, ::Rack::Utils.unescape(gzip_path)))
+ if can_gzip_mime && File.exist?(File.join(@root, ::Rack::Utils.unescape_path(gzip_path)))
gzip_path
else
false
@@ -97,29 +100,30 @@ module ActionDispatch
# This middleware will attempt to return the contents of a file's body from
# disk in the response. If a file is not found on disk, the request will be
# delegated to the application stack. This middleware is commonly initialized
- # to serve assets from a server's `public/` directory.
+ # to serve assets from a server's +public/+ directory.
#
# This middleware verifies the path to ensure that only files
# living in the root directory can be rendered. A request cannot
# produce a directory traversal using this middleware. Only 'GET' and 'HEAD'
# requests will result in a file being returned.
class Static
- def initialize(app, path, cache_control = nil, index: 'index')
+ def initialize(app, path, index: "index", headers: {})
@app = app
- @file_handler = FileHandler.new(path, cache_control, index: index)
+ @file_handler = FileHandler.new(path, index: index, headers: headers)
end
def call(env)
- case env['REQUEST_METHOD']
- when 'GET', 'HEAD'
- path = env['PATH_INFO'].chomp('/')
+ req = Rack::Request.new env
+
+ if req.get? || req.head?
+ path = req.path_info.chomp("/")
if match = @file_handler.match?(path)
- env['PATH_INFO'] = match
- return @file_handler.call(env)
+ req.path_info = match
+ return @file_handler.serve(req)
end
end
- @app.call(env)
+ @app.call(req.env)
end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb
deleted file mode 100644
index e7b913bbe4..0000000000
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb
+++ /dev/null
@@ -1,27 +0,0 @@
-<% @source_extracts.each_with_index do |source_extract, index| %>
- <% if source_extract[:code] %>
- <div class="source <%="hidden" if @show_source_idx != index%>" id="frame-source-<%=index%>">
- <div class="info">
- Extracted source (around line <strong>#<%= source_extract[:line_number] %></strong>):
- </div>
- <div class="data">
- <table cellpadding="0" cellspacing="0" class="lines">
- <tr>
- <td>
- <pre class="line_numbers">
- <% source_extract[:code].each_key do |line_number| %>
-<span><%= line_number -%></span>
- <% end %>
- </pre>
- </td>
-<td width="100%">
-<pre>
-<% source_extract[:code].each do |line, source| -%><div class="line<%= " active" if line == source_extract[:line_number] -%>"><%= source -%></div><% end -%>
-</pre>
-</td>
- </tr>
- </table>
- </div>
- </div>
- <% end %>
-<% end %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb
new file mode 100644
index 0000000000..88a8e6ad83
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb
@@ -0,0 +1,29 @@
+<% error_index = local_assigns[:error_index] || 0 %>
+
+<% source_extracts.each_with_index do |source_extract, index| %>
+ <% if source_extract[:code] %>
+ <div class="source <%= "hidden" if show_source_idx != index %>" id="frame-source-<%= error_index %>-<%= index %>">
+ <div class="info">
+ Extracted source (around line <strong>#<%= source_extract[:line_number] %></strong>):
+ </div>
+ <div class="data">
+ <table cellpadding="0" cellspacing="0" class="lines">
+ <tr>
+ <td>
+ <pre class="line_numbers">
+ <% source_extract[:code].each_key do |line_number| %>
+<span><%= line_number -%></span>
+ <% end %>
+ </pre>
+ </td>
+<td width="100%">
+<pre>
+<% source_extract[:code].each do |line, source| -%><div class="line<%= " active" if line == source_extract[:line_number] -%>"><%= source -%></div><% end -%>
+</pre>
+</td>
+ </tr>
+ </table>
+ </div>
+ </div>
+ <% end %>
+<% end %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.text.erb
new file mode 100644
index 0000000000..23a9c7ba3f
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.text.erb
@@ -0,0 +1,8 @@
+<% @source_extracts.first(3).each do |source_extract| %>
+<% if source_extract[:code] %>
+Extracted source (around line #<%= source_extract[:line_number] %>):
+
+<% source_extract[:code].each do |line, source| -%>
+<%= line == source_extract[:line_number] ? "*#{line}" : "##{line}" -%> <%= source -%><% end -%>
+<% end %>
+<% end %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb
index ab57b11c7d..835ca8d260 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb
@@ -1,52 +1,62 @@
-<% names = @traces.keys %>
+<% names = traces.keys %>
+<% error_index = local_assigns[:error_index] || 0 %>
<p><code>Rails.root: <%= defined?(Rails) && Rails.respond_to?(:root) ? Rails.root : "unset" %></code></p>
-<div id="traces">
+<div id="traces-<%= error_index %>">
<% names.each do |name| %>
<%
- show = "show('#{name.gsub(/\s/, '-')}');"
- hide = (names - [name]).collect {|hide_name| "hide('#{hide_name.gsub(/\s/, '-')}');"}
+ show = "show('#{name.gsub(/\s/, '-')}-#{error_index}');"
+ hide = (names - [name]).collect {|hide_name| "hide('#{hide_name.gsub(/\s/, '-')}-#{error_index}');"}
%>
<a href="#" onclick="<%= hide.join %><%= show %>; return false;"><%= name %></a> <%= '|' unless names.last == name %>
<% end %>
- <% @traces.each do |name, trace| %>
- <div id="<%= name.gsub(/\s/, '-') %>" style="display: <%= (name == @trace_to_show) ? 'block' : 'none' %>;">
- <pre><code><% trace.each do |frame| %><a class="trace-frames" data-frame-id="<%= frame[:id] %>" href="#"><%= frame[:trace] %></a><br><% end %></code></pre>
+ <% traces.each do |name, trace| %>
+ <div id="<%= "#{name.gsub(/\s/, '-')}-#{error_index}" %>" style="display: <%= (name == trace_to_show) ? 'block' : 'none' %>;">
+ <code style="font-size: 11px;">
+ <% trace.each do |frame| %>
+ <a class="trace-frames trace-frames-<%= error_index %>" data-exception-object-id="<%= frame[:exception_object_id] %>" data-frame-id="<%= frame[:id] %>" href="#">
+ <%= frame[:trace] %>
+ </a>
+ <br>
+ <% end %>
+ </code>
</div>
<% end %>
<script type="text/javascript">
- var traceFrames = document.getElementsByClassName('trace-frames');
- var selectedFrame, currentSource = document.getElementById('frame-source-0');
-
- // Add click listeners for all stack frames
- for (var i = 0; i < traceFrames.length; i++) {
- traceFrames[i].addEventListener('click', function(e) {
- e.preventDefault();
- var target = e.target;
- var frame_id = target.dataset.frameId;
-
- if (selectedFrame) {
- selectedFrame.className = selectedFrame.className.replace("selected", "");
- }
-
- target.className += " selected";
- selectedFrame = target;
-
- // Change the extracted source code
- changeSourceExtract(frame_id);
- });
-
- function changeSourceExtract(frame_id) {
- var el = document.getElementById('frame-source-' + frame_id);
- if (currentSource && el) {
- currentSource.className += " hidden";
- el.className = el.className.replace(" hidden", "");
- currentSource = el;
+ (function() {
+ var traceFrames = document.getElementsByClassName('trace-frames-<%= error_index %>');
+ var selectedFrame, currentSource = document.getElementById('frame-source-<%= error_index %>-0');
+
+ // Add click listeners for all stack frames
+ for (var i = 0; i < traceFrames.length; i++) {
+ traceFrames[i].addEventListener('click', function(e) {
+ e.preventDefault();
+ var target = e.target;
+ var frame_id = target.dataset.frameId;
+
+ if (selectedFrame) {
+ selectedFrame.className = selectedFrame.className.replace("selected", "");
+ }
+
+ target.className += " selected";
+ selectedFrame = target;
+
+ // Change the extracted source code
+ changeSourceExtract(frame_id);
+ });
+
+ function changeSourceExtract(frame_id) {
+ var el = document.getElementById('frame-source-<%= error_index %>-' + frame_id);
+ if (currentSource && el) {
+ currentSource.className += " hidden";
+ el.className = el.className.replace(" hidden", "");
+ currentSource = el;
+ }
}
}
- }
+ })();
</script>
</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb
index f154021ae6..bde26f46c2 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb
@@ -10,7 +10,25 @@
<div id="container">
<h2><%= h @exception.message %></h2>
- <%= render template: "rescues/_source" %>
- <%= render template: "rescues/_trace" %>
+ <%= render "rescues/source", source_extracts: @source_extracts, show_source_idx: @show_source_idx, error_index: 0 %>
+ <%= render "rescues/trace", traces: @traces, trace_to_show: @trace_to_show, error_index: 0 %>
+
+ <% if @exception.cause %>
+ <h2>Exception Causes</h2>
+ <% end %>
+
+ <% @exception_wrapper.wrapped_causes.each.with_index(1) do |wrapper, index| %>
+ <div class="details">
+ <a class="summary" href="#" style="color: #F0F0F0; text-decoration: none; background: #C52F24; border-bottom: none;" onclick="return toggle(<%= wrapper.exception.object_id %>)">
+ <%= wrapper.exception.class.name %>: <%= h wrapper.exception.message %>
+ </a>
+ </div>
+
+ <div id="<%= wrapper.exception.object_id %>" style="display: none;">
+ <%= render "rescues/source", source_extracts: wrapper.source_extracts, show_source_idx: wrapper.source_to_show_id, error_index: index %>
+ <%= render "rescues/trace", traces: wrapper.traces, trace_to_show: wrapper.trace_to_show, error_index: index %>
+ </div>
+ <% end %>
+
<%= render template: "rescues/_request_and_response" %>
</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb
new file mode 100644
index 0000000000..e8454acfad
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb
@@ -0,0 +1,21 @@
+<header>
+ <h1>
+ <%= @exception.class.to_s %>
+ <% if @request.parameters['controller'] %>
+ in <%= @request.parameters['controller'].camelize %>Controller<% if @request.parameters['action'] %>#<%= @request.parameters['action'] %><% end %>
+ <% end %>
+ </h1>
+</header>
+
+<div id="container">
+ <h2>
+ <%= h @exception.message %>
+ <% if @exception.message.match? %r{#{ActiveStorage::Blob.table_name}|#{ActiveStorage::Attachment.table_name}} %>
+ <br />To resolve this issue run: rails active_storage:install
+ <% end %>
+ </h2>
+
+ <%= render "rescues/source", source_extracts: @source_extracts, show_source_idx: @show_source_idx %>
+ <%= render "rescues/trace", traces: @traces, trace_to_show: @trace_to_show %>
+ <%= render template: "rescues/_request_and_response" %>
+</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb
new file mode 100644
index 0000000000..e5e3196710
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb
@@ -0,0 +1,13 @@
+<%= @exception.class.to_s %><%
+ if @request.parameters['controller']
+%> in <%= @request.parameters['controller'].camelize %>Controller<% if @request.parameters['action'] %>#<%= @request.parameters['action'] %><% end %>
+<% end %>
+
+<%= @exception.message %>
+<% if @exception.message.match? %r{#{ActiveStorage::Blob.table_name}|#{ActiveStorage::Attachment.table_name}} %>
+To resolve this issue run: rails active_storage:install
+<% end %>
+
+<%= render template: "rescues/_source" %>
+<%= render template: "rescues/_trace" %>
+<%= render template: "rescues/_request_and_response" %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb
index e0509f56f4..39ea25bdfc 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb
@@ -106,6 +106,7 @@
.line {
padding-left: 10px;
+ white-space: pre;
}
.line:hover {
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb
new file mode 100644
index 0000000000..76ab1691b5
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb
@@ -0,0 +1,19 @@
+<header>
+ <h1>No template for interactive request</h1>
+</header>
+
+<div id="container">
+ <h2><%= h @exception.message %></h2>
+
+ <p class="summary">
+ <strong>NOTE!</strong><br>
+ Unless told otherwise, Rails expects an action to render a template with the same name,<br>
+ contained in a folder named after its controller.
+
+ If this controller is an API responding with 204 (No Content), <br>
+ which does not require a template,
+ then this error will occur when trying to access it via browser,<br>
+ since we expect an HTML template
+ to be rendered for such requests. If that's the case, carry on.
+ </p>
+</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb
new file mode 100644
index 0000000000..fcdbe6069d
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb
@@ -0,0 +1,3 @@
+Missing exact template
+
+<%= @exception.message %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb
index 2a65fd06ad..22eb6e9b4e 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb
@@ -5,7 +5,7 @@
<div id="container">
<h2><%= h @exception.message %></h2>
- <%= render template: "rescues/_source" %>
- <%= render template: "rescues/_trace" %>
+ <%= render "rescues/source", source_extracts: @source_extracts, show_source_idx: @show_source_idx %>
+ <%= render "rescues/trace", traces: @traces, trace_to_show: @trace_to_show %>
<%= render template: "rescues/_request_and_response" %>
</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb
index 55dd5ddc7b..2b8f3f2a5e 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb
@@ -14,7 +14,7 @@
</p>
<% end %>
- <%= render template: "rescues/_trace" %>
+ <%= render "rescues/trace", traces: @traces, trace_to_show: @trace_to_show %>
<% if @routes_inspector %>
<h2>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb
index c1e8b6cae3..324ef1567a 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb
@@ -1,6 +1,6 @@
<header>
<h1>
- <%= @exception.original_exception.class.to_s %> in
+ <%= @exception.cause.class.to_s %> in
<%= @request.parameters["controller"].camelize if @request.parameters["controller"] %>#<%= @request.parameters["action"] %>
</h1>
</header>
@@ -11,10 +11,10 @@
</p>
<pre><code><%= h @exception.message %></code></pre>
- <%= render template: "rescues/_source" %>
+ <%= render "rescues/source", source_extracts: @source_extracts, show_source_idx: @show_source_idx %>
<p><%= @exception.sub_template_message %></p>
- <%= render template: "rescues/_trace" %>
+ <%= render "rescues/trace", traces: @traces, trace_to_show: @trace_to_show %>
<%= render template: "rescues/_request_and_response" %>
</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb
index 77bcd26726..78d52acd96 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb
@@ -1,4 +1,4 @@
-<%= @exception.original_exception.class.to_s %> in <%= @request.parameters["controller"].camelize if @request.parameters["controller"] %>#<%= @request.parameters["action"] %>
+<%= @exception.cause.class.to_s %> in <%= @request.parameters["controller"].camelize if @request.parameters["controller"] %>#<%= @request.parameters["action"] %>
Showing <%= @exception.file_name %> where line #<%= @exception.line_number %> raised:
<%= @exception.message %>
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..0242b706b2 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb
@@ -17,6 +17,10 @@
line-height: 15px;
}
+ #route_table thead tr.bottom th input#search {
+ -webkit-appearance: textfield;
+ }
+
#route_table tbody tr {
border-bottom: 1px solid #ddd;
}
@@ -60,7 +64,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 +97,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);
@@ -193,4 +197,7 @@
setupMatchPaths();
setupRouteToggleHelperLinks();
+
+ // Focus the search input after page has loaded
+ document.getElementById('search').focus();
</script>
diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb
index ddeea24bb3..efc3988bc3 100644
--- a/actionpack/lib/action_dispatch/railtie.rb
+++ b/actionpack/lib/action_dispatch/railtie.rb
@@ -1,4 +1,7 @@
+# frozen_string_literal: true
+
require "action_dispatch"
+require "active_support/messages/rotation_configuration"
module ActionDispatch
class Railtie < Rails::Railtie # :nodoc:
@@ -8,22 +11,30 @@ module ActionDispatch
config.action_dispatch.show_exceptions = true
config.action_dispatch.tld_length = 1
config.action_dispatch.ignore_accept_header = false
- config.action_dispatch.rescue_templates = { }
- config.action_dispatch.rescue_responses = { }
+ config.action_dispatch.rescue_templates = {}
+ config.action_dispatch.rescue_responses = {}
config.action_dispatch.default_charset = nil
config.action_dispatch.rack_cache = false
- config.action_dispatch.http_auth_salt = 'http authentication'
- config.action_dispatch.signed_cookie_salt = 'signed cookie'
- config.action_dispatch.encrypted_cookie_salt = 'encrypted cookie'
- config.action_dispatch.encrypted_signed_cookie_salt = 'signed encrypted cookie'
+ config.action_dispatch.http_auth_salt = "http authentication"
+ config.action_dispatch.signed_cookie_salt = "signed cookie"
+ config.action_dispatch.encrypted_cookie_salt = "encrypted cookie"
+ config.action_dispatch.encrypted_signed_cookie_salt = "signed encrypted cookie"
+ config.action_dispatch.authenticated_encrypted_cookie_salt = "authenticated encrypted cookie"
+ config.action_dispatch.use_authenticated_cookie_encryption = false
+ config.action_dispatch.use_cookies_with_metadata = false
config.action_dispatch.perform_deep_munge = true
config.action_dispatch.default_headers = {
- 'X-Frame-Options' => 'SAMEORIGIN',
- 'X-XSS-Protection' => '1; mode=block',
- 'X-Content-Type-Options' => 'nosniff'
+ "X-Frame-Options" => "SAMEORIGIN",
+ "X-XSS-Protection" => "1; mode=block",
+ "X-Content-Type-Options" => "nosniff",
+ "X-Download-Options" => "noopen",
+ "X-Permitted-Cross-Domain-Policies" => "none",
+ "Referrer-Policy" => "strict-origin-when-cross-origin"
}
+ config.action_dispatch.cookies_rotations = ActiveSupport::Messages::RotationConfiguration.new
+
config.eager_load_namespaces << ActionDispatch
initializer "action_dispatch.configure" do |app|
diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb
index a8a3cd20b9..bc5e0670e0 100644
--- a/actionpack/lib/action_dispatch/request/session.rb
+++ b/actionpack/lib/action_dispatch/request/session.rb
@@ -1,41 +1,43 @@
-require 'rack/session/abstract/id'
+# frozen_string_literal: true
+
+require "rack/session/abstract/id"
module ActionDispatch
- class Request < Rack::Request
+ class Request
# Session is responsible for lazily loading the session from store.
class Session # :nodoc:
- ENV_SESSION_KEY = Rack::Session::Abstract::ENV_SESSION_KEY # :nodoc:
- ENV_SESSION_OPTIONS_KEY = Rack::Session::Abstract::ENV_SESSION_OPTIONS_KEY # :nodoc:
+ 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
- def self.create(store, env, default_options)
- session_was = find env
- session = Request::Session.new(store, env)
+
+ # 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)
session.merge! session_was if session_was
- set(env, session)
- Options.set(env, Request::Session::Options.new(store, default_options))
+ set(req, session)
+ Options.set(req, Request::Session::Options.new(store, default_options))
session
end
- def self.find(env)
- env[ENV_SESSION_KEY]
+ def self.find(req)
+ req.get_header ENV_SESSION_KEY
end
- def self.set(env, session)
- env[ENV_SESSION_KEY] = session
+ def self.set(req, session)
+ req.set_header ENV_SESSION_KEY, session
end
class Options #:nodoc:
- def self.set(env, options)
- env[ENV_SESSION_OPTIONS_KEY] = options
+ def self.set(req, options)
+ req.set_header ENV_SESSION_OPTIONS_KEY, options
end
- def self.find(env)
- env[ENV_SESSION_OPTIONS_KEY]
+ def self.find(req)
+ req.get_header ENV_SESSION_OPTIONS_KEY
end
def initialize(by, default_options)
@@ -47,50 +49,58 @@ module ActionDispatch
@delegate[key]
end
- def id(env)
+ def id(req)
@delegate.fetch(:id) {
- @by.send(:extract_session_id, env)
+ @by.send(:extract_session_id, req)
}
end
- def []=(k,v); @delegate[k] = v; end
+ def []=(k, v); @delegate[k] = v; end
def to_hash; @delegate.dup; end
def values_at(*args); @delegate.values_at(*args); end
end
- def initialize(by, env)
+ def initialize(by, req)
@by = by
- @env = env
+ @req = req
@delegate = {}
@loaded = false
- @exists = nil # we haven't checked yet
+ @exists = nil # We haven't checked yet.
end
def id
- options.id(@env)
+ options.id(@req)
end
def options
- Options.find @env
+ Options.find @req
end
def destroy
clear
options = self.options || {}
- @by.send(:destroy_session, @env, options.id(@env), 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
# Returns value of the key stored in the session or
- # nil if the given key is not found in the session.
+ # +nil+ if the given key is not found in the session.
def [](key)
load_for_read!
@delegate[key.to_s]
end
+ # Returns the nested value specified by the sequence of keys, returning
+ # +nil+ if any intermediate step is +nil+.
+ def dig(*keys)
+ load_for_read!
+ keys = keys.map.with_index { |key, i| i.zero? ? key.to_s : key }
+ @delegate.dig(*keys)
+ end
+
# Returns true if the session has the given key or false.
def has_key?(key)
load_for_read!
@@ -101,15 +111,17 @@ module ActionDispatch
# Returns keys of the session as Array.
def keys
+ load_for_read!
@delegate.keys
end
# Returns values of the session as Array.
def values
+ load_for_read!
@delegate.values
end
- # Writes given value to given key of the session.
+ # Writes given value to given key of the session.
def []=(key, value)
load_for_write!
@delegate[key.to_s] = value
@@ -124,8 +136,9 @@ module ActionDispatch
# Returns the session as Hash.
def to_hash
load_for_read!
- @delegate.dup.delete_if { |_,v| v.nil? }
+ @delegate.dup.delete_if { |_, v| v.nil? }
end
+ alias :to_h :to_hash
# Updates the session with given Hash.
#
@@ -148,8 +161,8 @@ module ActionDispatch
@delegate.delete key.to_s
end
- # Returns value of given key from the session, or raises +KeyError+
- # if can't find given key in case of not setted dafault value.
+ # Returns value of the given key from the session, or raises +KeyError+
+ # if can't find the given key and no default value is set.
# Returns default value if specified.
#
# session.fetch(:foo)
@@ -162,7 +175,7 @@ module ActionDispatch
# :bar
# end
# # => :bar
- def fetch(key, default=Unspecified, &block)
+ def fetch(key, default = Unspecified, &block)
load_for_read!
if default == Unspecified
@delegate.fetch(key.to_s, &block)
@@ -181,7 +194,7 @@ module ActionDispatch
def exists?
return @exists unless @exists.nil?
- @exists = @by.send(:session_exists?, @env)
+ @exists = @by.send(:session_exists?, @req)
end
def loaded?
@@ -198,28 +211,32 @@ module ActionDispatch
@delegate.merge!(other)
end
+ def each(&block)
+ to_hash.each(&block)
+ end
+
private
- def load_for_read!
- load! if !loaded? && exists?
- end
+ def load_for_read!
+ load! if !loaded? && exists?
+ end
- def load_for_write!
- load! unless loaded?
- end
+ def load_for_write!
+ load! unless loaded?
+ end
- def load!
- id, session = @by.load_session @env
- options[:id] = id
- @delegate.replace(stringify_keys(session))
- @loaded = true
- end
+ def load!
+ id, session = @by.load_session @req
+ options[:id] = id
+ @delegate.replace(stringify_keys(session))
+ @loaded = true
+ end
- def stringify_keys(other)
- other.each_with_object({}) { |(key, value), hash|
- hash[key.to_s] = value
- }
- end
+ def stringify_keys(other)
+ other.each_with_object({}) { |(key, value), hash|
+ hash[key.to_s] = value
+ }
+ end
end
end
end
diff --git a/actionpack/lib/action_dispatch/request/utils.rb b/actionpack/lib/action_dispatch/request/utils.rb
index 1c9371d89c..fb0efb9a58 100644
--- a/actionpack/lib/action_dispatch/request/utils.rb
+++ b/actionpack/lib/action_dispatch/request/utils.rb
@@ -1,31 +1,78 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/indifferent_access"
+
module ActionDispatch
- class Request < Rack::Request
+ class Request
class Utils # :nodoc:
+ mattr_accessor :perform_deep_munge, default: 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
- mattr_accessor :perform_deep_munge
- self.perform_deep_munge = true
-
- class << self
- # Remove nils from the params hash
- def deep_munge(hash, keys = [])
- return hash unless perform_deep_munge
-
- hash.each do |k, v|
- keys << k
- case v
- when Array
- v.grep(Hash) { |x| deep_munge(x, keys) }
- v.compact!
- when Hash
- deep_munge(v, keys)
+ def self.normalize_encode_params(params)
+ if perform_deep_munge
+ NoNilParamEncoder.normalize_encode_params params
+ else
+ ParamEncoder.normalize_encode_params params
+ end
+ end
+
+ def self.check_param_encoding(params)
+ case params
+ when Array
+ params.each { |element| check_param_encoding(element) }
+ when Hash
+ params.each_value { |value| check_param_encoding(value) }
+ when String
+ unless params.valid_encoding?
+ # Raise Rack::Utils::InvalidParameterError for consistency with Rack.
+ # ActionDispatch::Request#GET will re-raise as a BadRequest error.
+ raise Rack::Utils::InvalidParameterError, "Invalid encoding for parameter: #{params.scrub}"
+ end
+ end
+ end
+
+ class ParamEncoder # :nodoc:
+ # Convert nested Hash to HashWithIndifferentAccess.
+ def self.normalize_encode_params(params)
+ case params
+ when Array
+ handle_array params
+ when Hash
+ if params.has_key?(:tempfile)
+ ActionDispatch::Http::UploadedFile.new(params)
+ else
+ params.each_with_object({}) do |(key, val), new_hash|
+ new_hash[key] = normalize_encode_params(val)
+ end.with_indifferent_access
end
- keys.pop
+ else
+ params
end
+ end
- hash
+ def self.handle_array(params)
+ params.map! { |el| normalize_encode_params(el) }
+ end
+ end
+
+ # Remove nils from the params hash.
+ class NoNilParamEncoder < ParamEncoder # :nodoc:
+ def self.handle_array(params)
+ list = super
+ list.compact!
+ list
end
end
end
end
end
-
diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb
index a42cf72f60..5cde677051 100644
--- a/actionpack/lib/action_dispatch/routing.rb
+++ b/actionpack/lib/action_dispatch/routing.rb
@@ -1,7 +1,6 @@
-# encoding: UTF-8
-require 'active_support/core_ext/object/to_param'
-require 'active_support/core_ext/regexp'
-require 'active_support/dependencies/autoload'
+# frozen_string_literal: true
+
+require "active_support/core_ext/string/filters"
module ActionDispatch
# The routing module provides URL rewriting in native Ruby. It's a way to
@@ -58,7 +57,7 @@ module ActionDispatch
# resources :posts, :comments
# end
#
- # Alternately, you can add prefixes to your path without using a separate
+ # Alternatively, you can add prefixes to your path without using a separate
# directory by using +scope+. +scope+ takes additional options which
# apply to all enclosed routes.
#
@@ -78,14 +77,14 @@ module ActionDispatch
# get 'post/:id' => 'posts#show'
# post 'post/:id' => 'posts#create_comment'
#
+ # Now, if you POST to <tt>/posts/:id</tt>, it will route to the <tt>create_comment</tt> action. A GET on the same
+ # URL will route to the <tt>show</tt> action.
+ #
# If your route needs to respond to more than one HTTP method (or all methods) then using the
# <tt>:via</tt> option on <tt>match</tt> is preferable.
#
# match 'post/:id' => 'posts#show', via: [:get, :post]
#
- # Now, if you POST to <tt>/posts/:id</tt>, it will route to the <tt>create_comment</tt> action. A GET on the same
- # URL will route to the <tt>show</tt> action.
- #
# == Named routes
#
# Routes can be named by passing an <tt>:as</tt> option,
@@ -94,7 +93,7 @@ module ActionDispatch
#
# Example:
#
- # # In routes.rb
+ # # In config/routes.rb
# get '/login' => 'accounts#login', as: 'login'
#
# # With render, redirect_to, tests, etc.
@@ -106,7 +105,7 @@ module ActionDispatch
#
# Use <tt>root</tt> as a shorthand to name a route for the root path "/".
#
- # # In routes.rb
+ # # In config/routes.rb
# root to: 'blogs#index'
#
# # would recognize http://www.example.com/ as
@@ -119,15 +118,15 @@ module ActionDispatch
# Note: when using +controller+, the route is simply named after the
# method you call on the block parameter rather than map.
#
- # # In routes.rb
+ # # In config/routes.rb
# controller :blog do
# get 'blog/show' => :list
# get 'blog/delete' => :delete
- # get 'blog/edit/:id' => :edit
+ # get 'blog/edit' => :edit
# end
#
# # provides named routes for show, delete, and edit
- # link_to @article.title, show_path(id: @article.id)
+ # link_to @article.title, blog_show_path(id: @article.id)
#
# == Pretty URLs
#
@@ -151,6 +150,7 @@ module ActionDispatch
# get 'geocode/:postalcode' => :show, constraints: {
# postalcode: /\d{5}(-\d{4})?/
# }
+ # end
#
# Constraints can include the 'ignorecase' and 'extended syntax' regular
# expression modifiers:
@@ -163,7 +163,7 @@ module ActionDispatch
#
# controller 'geocode' do
# get 'geocode/:postalcode' => :show, constraints: {
- # postalcode: /# Postcode format
+ # postalcode: /# Postalcode format
# \d{5} #Prefix
# (-\d{4})? #Suffix
# /x
@@ -200,7 +200,7 @@ module ActionDispatch
#
# Rails.application.reload_routes!
#
- # This will clear all named routes and reload routes.rb if the file has been modified from
+ # This will clear all named routes and reload config/routes.rb if the file has been modified from
# last load. To absolutely force reloading, use <tt>reload!</tt>.
#
# == Testing Routes
@@ -241,10 +241,11 @@ module ActionDispatch
#
# == View a list of all your routes
#
- # rake routes
- #
- # Target specific controllers by prefixing the command with <tt>CONTROLLER=x</tt>.
+ # rails routes
#
+ # Target a specific controller with <tt>-c</tt>, or grep routes
+ # using <tt>-g</tt>. Useful in conjunction with <tt>--expanded</tt>
+ # which displays routes vertically.
module Routing
extend ActiveSupport::Autoload
diff --git a/actionpack/lib/action_dispatch/routing/endpoint.rb b/actionpack/lib/action_dispatch/routing/endpoint.rb
index 88aa13c3e8..28bb20d688 100644
--- a/actionpack/lib/action_dispatch/routing/endpoint.rb
+++ b/actionpack/lib/action_dispatch/routing/endpoint.rb
@@ -1,10 +1,17 @@
+# frozen_string_literal: true
+
module ActionDispatch
module Routing
class Endpoint # :nodoc:
def dispatcher?; false; end
def redirect?; false; end
- def matches?(req); true; end
- def app; self; end
+ def matches?(req); true; end
+ def app; self; end
+ def rack_app; app; end
+
+ def engine?
+ rack_app.is_a?(Class) && rack_app < Rails::Engine
+ end
end
end
end
diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb
index 48c10a7d4c..413e524ef6 100644
--- a/actionpack/lib/action_dispatch/routing/inspector.rb
+++ b/actionpack/lib/action_dispatch/routing/inspector.rb
@@ -1,5 +1,7 @@
-require 'delegate'
-require 'active_support/core_ext/string/strip'
+# frozen_string_literal: true
+
+require "delegate"
+require "io/console/size"
module ActionDispatch
module Routing
@@ -13,11 +15,7 @@ module ActionDispatch
end
def rack_app
- app.app
- end
-
- def verb
- super.source.gsub(/[$^]/, '')
+ app.rack_app
end
def path
@@ -37,25 +35,25 @@ module ActionDispatch
end
def controller
- requirements[:controller] || ':controller'
+ parts.include?(:controller) ? ":controller" : requirements[:controller]
end
def action
- requirements[:action] || ':action'
+ parts.include?(:action) ? ":action" : requirements[:action]
end
def internal?
- controller.to_s =~ %r{\Arails/(info|mailers|welcome)}
+ internal
end
def engine?
- rack_app.respond_to?(:routes)
+ app.engine?
end
end
##
# This class is just used for displaying route information when someone
- # executes `rake routes` or looks at the RoutingError page.
+ # executes `rails routes` or looks at the RoutingError page.
# People should not use this class.
class RoutesInspector # :nodoc:
def initialize(routes)
@@ -63,13 +61,11 @@ module ActionDispatch
@routes = routes
end
- def format(formatter, filter = nil)
- routes_to_display = filter_routes(filter)
-
+ def format(formatter, filter = {})
+ routes_to_display = filter_routes(normalize_filter(filter))
routes = collect_routes(routes_to_display)
-
if routes.none?
- formatter.no_routes
+ formatter.no_routes(collect_routes(@routes), filter)
return formatter.result
end
@@ -85,92 +81,156 @@ module ActionDispatch
end
private
+ def normalize_filter(filter)
+ if filter[:controller]
+ { controller: /#{filter[:controller].underscore.sub(/_?controller\z/, "")}/ }
+ elsif filter[:grep]
+ { controller: /#{filter[:grep]}/, action: /#{filter[:grep]}/,
+ verb: /#{filter[:grep]}/, name: /#{filter[:grep]}/, path: /#{filter[:grep]}/ }
+ end
+ end
- def filter_routes(filter)
- if filter
- @routes.select { |route| route.defaults[:controller] == filter }
- else
- @routes
+ def filter_routes(filter)
+ if filter
+ @routes.select do |route|
+ route_wrapper = RouteWrapper.new(route)
+ filter.any? { |default, value| route_wrapper.send(default) =~ value }
+ end
+ else
+ @routes
+ end
end
- end
- def collect_routes(routes)
- routes.collect do |route|
- RouteWrapper.new(route)
- end.reject(&:internal?).collect do |route|
- collect_engine_routes(route)
+ def collect_routes(routes)
+ routes.collect do |route|
+ RouteWrapper.new(route)
+ end.reject(&:internal?).collect do |route|
+ collect_engine_routes(route)
- { name: route.name,
- verb: route.verb,
- path: route.path,
- reqs: route.reqs }
+ { name: route.name,
+ verb: route.verb,
+ path: route.path,
+ reqs: route.reqs }
+ end
end
- end
- def collect_engine_routes(route)
- name = route.endpoint
- return unless route.engine?
- return if @engines[name]
+ def collect_engine_routes(route)
+ name = route.endpoint
+ return unless route.engine?
+ return if @engines[name]
- routes = route.rack_app.routes
- if routes.is_a?(ActionDispatch::Routing::RouteSet)
- @engines[name] = collect_routes(routes.routes)
+ routes = route.rack_app.routes
+ if routes.is_a?(ActionDispatch::Routing::RouteSet)
+ @engines[name] = collect_routes(routes.routes)
+ end
end
- end
end
- class ConsoleFormatter
- def initialize
- @buffer = []
- end
+ module ConsoleFormatter
+ class Base
+ def initialize
+ @buffer = []
+ end
- def result
- @buffer.join("\n")
- end
+ def result
+ @buffer.join("\n")
+ end
- def section_title(title)
- @buffer << "\n#{title}:"
- end
+ def section_title(title)
+ end
- def section(routes)
- @buffer << draw_section(routes)
- end
+ def section(routes)
+ end
- def header(routes)
- @buffer << draw_header(routes)
+ def header(routes)
+ end
+
+ def no_routes(routes, filter)
+ @buffer <<
+ if routes.none?
+ <<~MESSAGE
+ You don't have any routes defined!
+
+ Please add some routes in config/routes.rb.
+ MESSAGE
+ elsif filter.key?(:controller)
+ "No routes were found for this controller."
+ elsif filter.key?(:grep)
+ "No routes were found for this grep pattern."
+ end
+
+ @buffer << "For more information about routes, see the Rails guide: https://guides.rubyonrails.org/routing.html."
+ end
end
- def no_routes
- @buffer << <<-MESSAGE.strip_heredoc
- You don't have any routes defined!
+ class Sheet < Base
+ def section_title(title)
+ @buffer << "\n#{title}:"
+ end
+
+ def section(routes)
+ @buffer << draw_section(routes)
+ end
- Please add some routes in config/routes.rb.
+ def header(routes)
+ @buffer << draw_header(routes)
+ end
- For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html.
- MESSAGE
- end
+ private
- private
- def draw_section(routes)
- header_lengths = ['Prefix', 'Verb', 'URI Pattern'].map(&:length)
- name_width, verb_width, path_width = widths(routes).zip(header_lengths).map(&:max)
+ def draw_section(routes)
+ header_lengths = ["Prefix", "Verb", "URI Pattern"].map(&:length)
+ name_width, verb_width, path_width = widths(routes).zip(header_lengths).map(&:max)
- routes.map do |r|
- "#{r[:name].rjust(name_width)} #{r[:verb].ljust(verb_width)} #{r[:path].ljust(path_width)} #{r[:reqs]}"
+ routes.map do |r|
+ "#{r[:name].rjust(name_width)} #{r[:verb].ljust(verb_width)} #{r[:path].ljust(path_width)} #{r[:reqs]}"
+ end
end
- end
- def draw_header(routes)
- name_width, verb_width, path_width = widths(routes)
+ def draw_header(routes)
+ name_width, verb_width, path_width = widths(routes)
- "#{"Prefix".rjust(name_width)} #{"Verb".ljust(verb_width)} #{"URI Pattern".ljust(path_width)} Controller#Action"
+ "#{"Prefix".rjust(name_width)} #{"Verb".ljust(verb_width)} #{"URI Pattern".ljust(path_width)} Controller#Action"
+ end
+
+ def widths(routes)
+ [routes.map { |r| r[:name].length }.max || 0,
+ routes.map { |r| r[:verb].length }.max || 0,
+ routes.map { |r| r[:path].length }.max || 0]
+ end
+ end
+
+ class Expanded < Base
+ def section_title(title)
+ @buffer << "\n#{"[ #{title} ]"}"
end
- def widths(routes)
- [routes.map { |r| r[:name].length }.max || 0,
- routes.map { |r| r[:verb].length }.max || 0,
- routes.map { |r| r[:path].length }.max || 0]
+ def section(routes)
+ @buffer << draw_expanded_section(routes)
end
+
+ private
+
+ def draw_expanded_section(routes)
+ routes.map.each_with_index do |r, i|
+ <<~MESSAGE.chomp
+ #{route_header(index: i + 1)}
+ Prefix | #{r[:name]}
+ Verb | #{r[:verb]}
+ URI | #{r[:path]}
+ Controller#Action | #{r[:reqs]}
+ MESSAGE
+ end
+ end
+
+ def route_header(index:)
+ console_width = IO.console_size.second
+ header_prefix = "--[ Route #{index} ]"
+ dash_remainder = [console_width - header_prefix.size, 0].max
+
+ "#{header_prefix}#{'-' * dash_remainder}"
+ end
+ end
end
class HtmlTableFormatter
@@ -187,21 +247,21 @@ 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
- def no_routes
- @buffer << <<-MESSAGE.strip_heredoc
+ def no_routes(*)
+ @buffer << <<~MESSAGE
<p>You don't have any routes defined!</p>
<ul>
<li>Please add some routes in <tt>config/routes.rb</tt>.</li>
<li>
For more information about routes, please see the Rails guide
- <a href="http://guides.rubyonrails.org/routing.html">Rails Routing from the Outside In</a>.
+ <a href="https://guides.rubyonrails.org/routing.html">Rails Routing from the Outside In</a>.
</li>
</ul>
- MESSAGE
+ MESSAGE
end
def result
diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb
index 40e00b55c2..06ce165f76 100644
--- a/actionpack/lib/action_dispatch/routing/mapper.rb
+++ b/actionpack/lib/action_dispatch/routing/mapper.rb
@@ -1,37 +1,39 @@
-require 'active_support/core_ext/hash/except'
-require 'active_support/core_ext/hash/reverse_merge'
-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/module/remove_method'
-require 'active_support/inflector'
-require 'action_dispatch/routing/redirection'
-require 'action_dispatch/routing/endpoint'
+# frozen_string_literal: true
+
+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"
module ActionDispatch
module Routing
class Mapper
URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port]
- class Constraints < Endpoint #:nodoc:
+ class Constraints < Routing::Endpoint #:nodoc:
attr_reader :app, :constraints
- def initialize(app, constraints, dispatcher_p)
- # Unwrap Constraints objects. I don't actually think it's possible
+ SERVE = ->(app, req) { app.serve req }
+ CALL = ->(app, req) { app.call req.env }
+
+ def initialize(app, constraints, strategy)
+ # 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
app = app.app
end
- @dispatcher = dispatcher_p
+ @strategy = strategy
@app, @constraints, = app, constraints
end
- def dispatcher?; @dispatcher; end
+ def dispatcher?; @strategy == SERVE; end
def matches?(req)
@constraints.all? do |constraint|
@@ -41,119 +43,207 @@ module ActionDispatch
end
def serve(req)
- return [ 404, {'X-Cascade' => 'pass'}, [] ] unless matches?(req)
+ return [ 404, { "X-Cascade" => "pass" }, [] ] unless matches?(req)
- if dispatcher?
- @app.serve req
- else
- @app.call req.env
- end
+ @strategy.call @app, req
end
private
def constraint_args(constraint, request)
- constraint.arity == 1 ? [request] : [request.path_parameters, request]
+ arity = if constraint.respond_to?(:arity)
+ constraint.arity
+ else
+ constraint.method(:call).arity
+ end
+
+ if arity < 1
+ []
+ elsif arity == 1
+ [request]
+ else
+ [request.path_parameters, request]
+ end
end
end
class Mapping #:nodoc:
ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
+ OPTIONAL_FORMAT_REGEX = %r{(?:\(\.:format\)+|\.:format|/)\Z}
- attr_reader :requirements, :conditions, :defaults
- attr_reader :to, :default_controller, :default_action, :as, :anchor
+ attr_reader :requirements, :defaults
+ attr_reader :to, :default_controller, :default_action
+ attr_reader :required_defaults, :ast
- def self.build(scope, set, path, as, options)
+ def self.build(scope, set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options)
options = scope[:options].merge(options) if scope[:options]
- options.delete :only
- options.delete :except
- options.delete :shallow_path
- options.delete :shallow_prefix
- options.delete :shallow
+ defaults = (scope[:defaults] || {}).dup
+ scope_constraints = scope[:constraints] || {}
- defaults = (scope[:defaults] || {}).merge options.delete(:defaults) || {}
+ new set, ast, defaults, controller, default_action, scope[:module], to, formatted, scope_constraints, scope[:blocks] || [], via, options_constraints, anchor, options
+ end
+
+ def self.check_via(via)
+ if via.empty?
+ msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \
+ "If you want to expose your action to both GET and POST, add `via: [:get, :post]` option.\n" \
+ "If you want to expose your action to GET, use `get` in the router:\n" \
+ " Instead of: match \"controller#action\"\n" \
+ " Do: get \"controller#action\""
+ raise ArgumentError, msg
+ end
+ via
+ end
+
+ def self.normalize_path(path, format)
+ path = Mapper.normalize_path(path)
- new scope, set, path, defaults, as, options
+ if format == true
+ "#{path}.:format"
+ elsif optional_format?(path, format)
+ "#{path}(.:format)"
+ else
+ path
+ end
end
- def initialize(scope, set, path, defaults, as, options)
- @requirements, @conditions = {}, {}
+ def self.optional_format?(path, format)
+ format != false && path !~ OPTIONAL_FORMAT_REGEX
+ end
+
+ def initialize(set, ast, defaults, controller, default_action, modyoule, to, formatted, scope_constraints, blocks, via, options_constraints, anchor, options)
@defaults = defaults
@set = set
- @to = options.delete :to
- @default_controller = options.delete(:controller) || scope[:controller]
- @default_action = options.delete(:action) || scope[:action]
- @as = as
- @anchor = options.delete :anchor
+ @to = to
+ @default_controller = controller
+ @default_action = default_action
+ @ast = ast
+ @anchor = anchor
+ @via = via
+ @internal = options.delete(:internal)
- formatted = options.delete :format
- via = Array(options.delete(:via) { [] })
- options_constraints = options.delete :constraints
+ path_params = ast.find_all(&:symbol?).map(&:to_sym)
- path = normalize_path! path, formatted
- ast = path_ast path
- path_params = path_params ast
+ options = add_wildcard_options(options, formatted, ast)
- options = normalize_options!(options, formatted, path_params, ast, scope[:module])
+ options = normalize_options!(options, path_params, modyoule)
+ split_options = constraints(options, path_params)
- split_constraints(path_params, scope[:constraints]) if scope[:constraints]
- constraints = constraints(options, path_params)
+ constraints = scope_constraints.merge Hash[split_options[:constraints] || []]
- split_constraints path_params, constraints
+ if options_constraints.is_a?(Hash)
+ @defaults = Hash[options_constraints.find_all { |key, default|
+ URL_OPTIONS.include?(key) && (String === default || Integer === default)
+ }].merge @defaults
+ @blocks = blocks
+ constraints.merge! options_constraints
+ else
+ @blocks = blocks(options_constraints)
+ end
- @blocks = blocks(options_constraints, scope[:blocks])
+ requirements, conditions = split_constraints path_params, constraints
+ verify_regexp_requirements requirements.map(&:last).grep(Regexp)
- if options_constraints.is_a?(Hash)
- split_constraints path_params, options_constraints
- options_constraints.each do |key, default|
- if URL_OPTIONS.include?(key) && (String === default || Fixnum === default)
- @defaults[key] ||= default
- end
- end
+ formats = normalize_format(formatted)
+
+ @requirements = formats[:requirements].merge Hash[requirements]
+ @conditions = Hash[conditions]
+ @defaults = formats[:defaults].merge(@defaults).merge(normalize_defaults(options))
+
+ if path_params.include?(:action) && !@requirements.key?(:action)
+ @defaults[:action] ||= "index"
end
- normalize_format!(formatted)
+ @required_defaults = (split_options[:required_defaults] || []).map(&:first)
+ end
- @conditions[:path_info] = path
- @conditions[:parsed_path_info] = ast
+ def make_route(name, precedence)
+ route = Journey::Route.new(name,
+ application,
+ path,
+ conditions,
+ required_defaults,
+ defaults,
+ request_method,
+ precedence,
+ @internal)
+
+ route
+ end
- add_request_method(via, @conditions)
- normalize_defaults!(options)
+ def application
+ app(@blocks)
end
- def to_route
- [ app(@blocks), conditions, requirements, defaults, as, anchor ]
+ def path
+ build_path @ast, requirements, @anchor
end
- private
+ def conditions
+ build_conditions @conditions, @set.request_class
+ end
- def normalize_path!(path, format)
- path = Mapper.normalize_path(path)
+ def build_conditions(current_conditions, request_class)
+ conditions = current_conditions.dup
- if format == true
- "#{path}.:format"
- elsif optional_format?(path, format)
- "#{path}(.:format)"
+ conditions.keep_if do |k, _|
+ request_class.public_method_defined?(k)
+ end
+ end
+ private :build_conditions
+
+ def request_method
+ @via.map { |x| Journey::Route.verb_matcher(x) }
+ end
+ private :request_method
+
+ JOINED_SEPARATORS = SEPARATORS.join # :nodoc:
+
+ def build_path(ast, requirements, anchor)
+ pattern = Journey::Path::Pattern.new(ast, requirements, JOINED_SEPARATORS, anchor)
+
+ # Find all the symbol nodes that are adjacent to literal nodes and alter
+ # the regexp so that Journey will partition them into custom routes.
+ ast.find_all { |node|
+ next unless node.cat?
+
+ if node.left.literal? && node.right.symbol?
+ symbol = node.right
+ elsif node.left.literal? && node.right.cat? && node.right.left.symbol?
+ symbol = node.right.left
+ elsif node.left.symbol? && node.right.literal?
+ symbol = node.left
+ elsif node.left.symbol? && node.right.cat? && node.right.left.literal?
+ symbol = node.left
else
- path
+ next
end
- end
- def optional_format?(path, format)
- format != false && !path.include?(':format') && !path.end_with?('/')
- end
+ if symbol
+ symbol.regexp = /(?:#{Regexp.union(symbol.regexp, '-')})+/
+ end
+ }
- def normalize_options!(options, formatted, path_params, path_ast, modyoule)
+ pattern
+ end
+ private :build_path
+
+ 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) do |node|
- options[node.name.to_sym] ||= /.+?/
- end
+ path_ast.grep(Journey::Nodes::Star).each_with_object({}) { |node, hash|
+ hash[node.name.to_sym] ||= /.+?/
+ }.merge options
+ else
+ options
end
+ end
+ def normalize_options!(options, path_params, modyoule)
if path_params.include?(:controller)
raise ArgumentError, ":controller segment is not allowed within a namespace block" if modyoule
@@ -164,7 +254,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
@@ -178,82 +268,60 @@ module ActionDispatch
end
def split_constraints(path_params, constraints)
- constraints.each_pair do |key, requirement|
- if path_params.include?(key) || key == :controller
- verify_regexp_requirement(requirement) if requirement.is_a?(Regexp)
- @requirements[key] = requirement
- else
- @conditions[key] = requirement
- end
+ constraints.partition do |key, requirement|
+ path_params.include?(key) || key == :controller
end
end
- def normalize_format!(formatted)
- if formatted == true
- @requirements[:format] ||= /.+/
- elsif Regexp === formatted
- @requirements[:format] = formatted
- @defaults[:format] = nil
- elsif String === formatted
- @requirements[:format] = Regexp.compile(formatted)
- @defaults[:format] = formatted
- end
- end
-
- def verify_regexp_requirement(requirement)
- if requirement.source =~ ANCHOR_CHARACTERS_REGEX
- raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
- end
-
- if requirement.multiline?
- raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{requirement.inspect}"
+ def normalize_format(formatted)
+ case formatted
+ when true
+ { requirements: { format: /.+/ },
+ defaults: {} }
+ when Regexp
+ { requirements: { format: formatted },
+ defaults: { format: nil } }
+ when String
+ { requirements: { format: Regexp.compile(formatted) },
+ defaults: { format: formatted } }
+ else
+ { requirements: {}, defaults: {} }
end
end
- def normalize_defaults!(options)
- options.each_pair do |key, default|
- unless Regexp === default
- @defaults[key] = default
+ def verify_regexp_requirements(requirements)
+ requirements.each do |requirement|
+ if ANCHOR_CHARACTERS_REGEX.match?(requirement.source)
+ raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
end
- end
- end
- def verify_callable_constraint(callable_constraint)
- unless callable_constraint.respond_to?(:call) || callable_constraint.respond_to?(:matches?)
- raise ArgumentError, "Invalid constraint: #{callable_constraint.inspect} must respond to :call or :matches?"
+ if requirement.multiline?
+ raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{requirement.inspect}"
+ end
end
end
- def add_request_method(via, conditions)
- return if via == [:all]
-
- if via.empty?
- msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \
- "If you want to expose your action to both GET and POST, add `via: [:get, :post]` option.\n" \
- "If you want to expose your action to GET, use `get` in the router:\n" \
- " Instead of: match \"controller#action\"\n" \
- " Do: get \"controller#action\""
- raise ArgumentError, msg
- end
-
- conditions[:request_method] = via.map { |m| m.to_s.dasherize.upcase }
+ def normalize_defaults(options)
+ Hash[options.reject { |_, default| Regexp === default }]
end
def app(blocks)
- if to.respond_to?(:call)
- Constraints.new(to, blocks, false)
+ 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), blocks, true)
+ Constraints.new(dispatcher(defaults.key?(:controller)), blocks, Constraints::SERVE)
else
- dispatcher(defaults)
+ dispatcher(defaults.key?(:controller))
end
end
def check_controller_and_action(path_params, controller, action)
hash = check_part(:controller, controller, path_params, {}) do |part|
translate_controller(part) {
- message = "'#{part}' is not a supported controller name. This can lead to potential routing problems."
- message << " See http://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use"
+ message = +"'#{part}' is not a supported controller name. This can lead to potential routing problems."
+ message << " See https://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use"
raise ArgumentError, message
}
@@ -277,8 +345,8 @@ module ActionDispatch
end
def split_to(to)
- if to =~ /#/
- to.split('#')
+ if /#/.match?(to)
+ to.split("#")
else
[]
end
@@ -286,7 +354,7 @@ module ActionDispatch
def add_controller_module(controller, modyoule)
if modyoule && !controller.is_a?(Regexp)
- if controller =~ %r{\A/}
+ if %r{\A/}.match?(controller)
controller[1..-1]
else
[modyoule, controller].compact.join("/")
@@ -303,40 +371,29 @@ module ActionDispatch
yield
end
- def blocks(options_constraints, scope_blocks)
- if options_constraints && !options_constraints.is_a?(Hash)
- verify_callable_constraint(options_constraints)
- [options_constraints]
- else
- scope_blocks || []
+ def blocks(callable_constraint)
+ unless callable_constraint.respond_to?(:call) || callable_constraint.respond_to?(:matches?)
+ raise ArgumentError, "Invalid constraint: #{callable_constraint.inspect} must respond to :call or :matches?"
end
+ [callable_constraint]
end
def constraints(options, path_params)
- constraints = {}
- required_defaults = []
- options.each_pair do |key, option|
+ options.group_by do |key, option|
if Regexp === option
- constraints[key] = option
+ :constraints
else
- required_defaults << key unless path_params.include?(key)
+ if path_params.include?(key)
+ :path_params
+ else
+ :required_defaults
+ end
end
end
- @conditions[:required_defaults] = required_defaults
- constraints
end
- def path_params(ast)
- ast.grep(Journey::Nodes::Symbol).map { |n| n.name.to_sym }
- end
-
- def path_ast(path)
- parser = Journey::Parser.new
- parser.parse path
- end
-
- def dispatcher(defaults)
- @set.dispatcher defaults
+ def dispatcher(raise_on_name_error)
+ Routing::RouteSet::Dispatcher.new raise_on_name_error
end
end
@@ -345,7 +402,7 @@ module ActionDispatch
# for root cases, where the latter is the correct one.
def self.normalize_path(path)
path = Journey::Router::Utils.normalize_path(path)
- path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^)]+\)$}
+ path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/(\(+[^)]+\)){1,}$}
path
end
@@ -354,24 +411,7 @@ module ActionDispatch
end
module Base
- # You can specify what Rails should route "/" to with the root method:
- #
- # root to: 'pages#main'
- #
- # For options, see +match+, as +root+ uses it internally.
- #
- # You can also pass a string which will expand
- #
- # root 'pages#main'
- #
- # You should put the root route at the top of <tt>config/routes.rb</tt>,
- # because this means it will be matched first. As this is the most popular route
- # of most Rails applications, this is beneficial.
- def root(options = {})
- match '/', { :as => :root, :via => :get }.merge!(options)
- end
-
- # 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.
@@ -381,7 +421,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:
@@ -430,7 +470,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.
@@ -443,6 +483,31 @@ module ActionDispatch
# dynamic segment used to generate the routes).
# You can access that segment from your controller using
# <tt>params[<:param>]</tt>.
+ # In your router:
+ #
+ # resources :users, param: :name
+ #
+ # The +users+ resource here will have the following routes generated for it:
+ #
+ # GET /users(.:format)
+ # POST /users(.:format)
+ # GET /users/new(.:format)
+ # GET /users/:name/edit(.:format)
+ # GET /users/:name(.:format)
+ # PATCH/PUT /users/:name(.:format)
+ # DELETE /users/:name(.:format)
+ #
+ # You can override <tt>ActiveRecord::Base#to_param</tt> of a related
+ # model to construct a URL:
+ #
+ # class User < ActiveRecord::Base
+ # def to_param
+ # name
+ # end
+ # end
+ #
+ # user = User.find_by(name: 'Phusion')
+ # user_path(user) # => "/users/Phusion"
#
# [:path]
# The path prefix for the routes.
@@ -500,10 +565,10 @@ module ActionDispatch
#
# match 'json_only', constraints: { format: 'json' }, via: :get
#
- # class Whitelist
+ # class PermitList
# def matches?(request) request.remote_ip == '1.2.3.4' end
# end
- # match 'path', to: 'c#a', constraints: Whitelist.new, via: :get
+ # match 'path', to: 'c#a', constraints: PermitList.new, via: :get
#
# See <tt>Scoping#constraints</tt> for more examples with its scope
# equivalent.
@@ -526,7 +591,7 @@ module ActionDispatch
# [:format]
# Allows you to specify the default value for optional +format+
# segment or disable it by supplying +false+.
- def match(path, options=nil)
+ def match(path, options = nil)
end
# Mount a Rack-based application to be used within the application.
@@ -551,17 +616,20 @@ module ActionDispatch
def mount(app, options = nil)
if options
path = options.delete(:at)
- else
- unless Hash === app
- raise ArgumentError, "must be called with mount point"
- end
-
+ elsif Hash === app
options = app
app, path = options.find { |k, _| k.respond_to?(:call) }
options.delete(app) if app
end
- raise "A rack application must be specified" unless path
+ raise ArgumentError, "A rack application must be specified" unless app.respond_to?(:call)
+ raise ArgumentError, <<~MSG unless path
+ Must be called with mount point
+
+ mount SomeRackApp, at: "some_route"
+ or
+ mount(SomeRackApp => "some_route")
+ MSG
rails_app = rails_app? app
options[:as] ||= app_name(app, rails_app)
@@ -569,7 +637,7 @@ module ActionDispatch
target_as = name_for_action(options[:as], path)
options[:via] ||= :all
- match(path, options.merge(:to => app, :anchor => false, :format => false))
+ match(path, options.merge(to: app, anchor: false, format: false))
define_generate_prefix(app, target_as) if rails_app
self
@@ -588,7 +656,7 @@ module ActionDispatch
# Query if the following named route was already defined.
def has_named_route?(name)
- @set.named_routes.routes[name.to_sym]
+ @set.named_routes.key? name
end
private
@@ -608,17 +676,31 @@ module ActionDispatch
def define_generate_prefix(app, name)
_route = @set.named_routes.get name
_routes = @set
- app.routes.define_mounted_helper(name)
+ _url_helpers = @set.url_helpers
+
+ script_namer = ->(options) do
+ prefix_options = options.slice(*_route.segment_keys)
+ prefix_options[:relative_url_root] = ""
+
+ if options[:_recall]
+ prefix_options.reverse_merge!(options[:_recall].slice(*_route.segment_keys))
+ end
+
+ # We must actually delete prefix segment keys to avoid passing them to next url_for.
+ _route.segment_keys.each { |k| options.delete(k) }
+ _url_helpers.send("#{name}_path", prefix_options)
+ end
+
+ app.routes.define_mounted_helper(name, script_namer)
+
app.routes.extend Module.new {
def optimize_routes_generation?; false; end
+
define_method :find_script_name do |options|
if options.key? :script_name
super(options)
else
- prefix_options = options.slice(*_route.segment_keys)
- # 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)
+ script_namer.call(options)
end
end
}
@@ -764,7 +846,7 @@ module ActionDispatch
options = args.extract_options!.dup
scope = {}
- options[:path] = args.flatten.join('/') if args.any?
+ options[:path] = args.flatten.join("/") if args.any?
options[:constraints] ||= {}
unless nested_scope?
@@ -773,25 +855,34 @@ module ActionDispatch
end
if options[:constraints].is_a?(Hash)
- defaults = options[:constraints].select do
- |k, v| URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Fixnum))
+ defaults = options[:constraints].select do |k, v|
+ URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Integer))
end
- (options[:defaults] ||= {}).reverse_merge!(defaults)
+ options[:defaults] = defaults.merge(options[:defaults] || {})
else
block, options[:constraints] = options[:constraints], {}
end
+ if options.key?(:only) || options.key?(:except)
+ scope[:action_options] = { only: options.delete(:only),
+ except: options.delete(:except) }
+ end
+
+ if options.key? :anchor
+ raise ArgumentError, "anchor is ignored unless passed to `match`"
+ end
+
@scope.options.each do |option|
if option == :blocks
value = block
elsif option == :options
value = options
else
- value = options.delete(option)
+ value = options.delete(option) { POISON }
end
- if value
+ unless POISON == value
scope[option] = send("merge_#{option}_scope", @scope[option], value)
end
end
@@ -803,14 +894,18 @@ module ActionDispatch
@scope = @scope.parent
end
+ POISON = Object.new # :nodoc:
+
# Scopes routes to a specific controller
#
# controller "food" do
# match "bacon", action: :bacon, via: :get
# end
- def controller(controller, options={})
- options[:controller] = controller
- scope(options) { yield }
+ def controller(controller)
+ @scope = @scope.new(controller: controller)
+ yield
+ ensure
+ @scope = @scope.parent
end
# Scopes routes to a specific namespace. For example:
@@ -856,13 +951,14 @@ module ActionDispatch
defaults = {
module: path,
- path: options.fetch(:path, path),
as: options.fetch(:as, path),
shallow_path: options.fetch(:path, path),
shallow_prefix: options.fetch(:as, path)
}
- scope(defaults.merge!(options)) { yield }
+ path_scope(options.delete(:path) { path }) do
+ scope(defaults.merge!(options)) { yield }
+ end
end
# === Parameter Restriction
@@ -921,7 +1017,7 @@ module ActionDispatch
# resources :iphones
# end
def constraints(constraints = {})
- scope(:constraints => constraints) { yield }
+ scope(constraints: constraints) { yield }
end
# Allows you to set default parameters for a route, such as this:
@@ -930,66 +1026,77 @@ module ActionDispatch
# end
# Using this, the +:id+ parameter here will default to 'home'.
def defaults(defaults = {})
- scope(:defaults => defaults) { yield }
+ @scope = @scope.new(defaults: merge_defaults_scope(@scope[:defaults], defaults))
+ yield
+ ensure
+ @scope = @scope.parent
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)
child
end
- def merge_action_scope(parent, child) #:nodoc:
+ def merge_via_scope(parent, child)
child
end
- def merge_path_names_scope(parent, child) #:nodoc:
+ def merge_format_scope(parent, child)
+ child
+ end
+
+ 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:
- (parent || {}).except(*override_keys(child)).merge!(child)
+ 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
- def override_keys(child) #:nodoc:
- child.key?(:only) || child.key?(:except) ? [:only, :except] : []
+ def merge_to_scope(parent, child)
+ child
end
end
@@ -1040,17 +1147,19 @@ module ActionDispatch
CANONICAL_ACTIONS = %w(index create new show update destroy)
class Resource #:nodoc:
- attr_reader :controller, :path, :options, :param
+ attr_reader :controller, :path, :param
- def initialize(entities, api_only = false, options = {})
+ def initialize(entities, api_only, shallow, options = {})
@name = entities.to_s
@path = (options[:path] || @name).to_s
@controller = (options[:controller] || @name).to_s
@as = options[:as]
@param = (options[:param] || :id).to_sym
@options = options
- @shallow = false
+ @shallow = shallow
@api_only = api_only
+ @only = options.delete :only
+ @except = options.delete :except
end
def default_actions
@@ -1061,19 +1170,19 @@ module ActionDispatch
end
end
- def available_actions
- if only = @options[:only]
- Array(only).map(&:to_sym)
+ def actions
+ if @except
+ available_actions - Array(@except).map(&:to_sym)
else
- default_actions
+ available_actions
end
end
- def actions
- if except = @options[:except]
- available_actions - Array(except).map(&:to_sym)
+ def available_actions
+ if @only
+ Array(@only).map(&:to_sym)
else
- available_actions
+ default_actions
end
end
@@ -1098,7 +1207,7 @@ module ActionDispatch
end
def resource_scope
- { :controller => controller }
+ controller
end
alias :collection_scope :path
@@ -1121,17 +1230,15 @@ module ActionDispatch
"#{path}/:#{nested_param}"
end
- def shallow=(value)
- @shallow = value
- end
-
def shallow?
@shallow
end
+
+ def singleton?; false; end
end
class SingletonResource < Resource #:nodoc:
- def initialize(entities, api_only, options)
+ def initialize(entities, api_only, shallow, options)
super
@as = nil
@controller = (options[:controller] || plural).to_s
@@ -1159,6 +1266,8 @@ module ActionDispatch
alias :member_scope :path
alias :nested_scope :path
+
+ def singleton?; true; end
end
def resources_path_names(options)
@@ -1173,19 +1282,19 @@ 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):
#
# GET /profile/new
- # POST /profile
# GET /profile
# GET /profile/edit
# PATCH/PUT /profile
# DELETE /profile
+ # POST /profile
#
# === Options
- # Takes same options as +resources+.
+ # Takes same options as resources[rdoc-ref:#resources]
def resource(*resources, &block)
options = resources.extract_options!.dup
@@ -1193,20 +1302,23 @@ module ActionDispatch
return self
end
- resource_scope(:resource, SingletonResource.new(resources.pop, api_only?, options)) do
- yield if block_given?
+ with_scope_level(:resource) do
+ options = apply_action_options options
+ resource_scope(SingletonResource.new(resources.pop, api_only?, @scope[:shallow], options)) do
+ yield if block_given?
- concerns(options[:concerns]) if options[:concerns]
+ concerns(options[:concerns]) if options[:concerns]
- collection do
- post :create
- end if parent_resource.actions.include?(:create)
+ new do
+ get :new
+ end if parent_resource.actions.include?(:new)
- new do
- get :new
- end if parent_resource.actions.include?(:new)
+ set_member_mappings_for_resource
- set_member_mappings_for_resource
+ collection do
+ post :create
+ end if parent_resource.actions.include?(:create)
+ end
end
self
@@ -1247,7 +1359,7 @@ module ActionDispatch
# DELETE /photos/:photo_id/comments/:id
#
# === Options
- # Takes same options as <tt>Base#match</tt> as well as:
+ # Takes same options as match[rdoc-ref:Base#match] as well as:
#
# [:path_names]
# Allows you to change the segment component of the +edit+ and +new+ actions.
@@ -1255,14 +1367,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.
@@ -1351,21 +1463,24 @@ module ActionDispatch
return self
end
- resource_scope(:resources, Resource.new(resources.pop, api_only?, options)) do
- yield if block_given?
+ with_scope_level(:resources) do
+ options = apply_action_options options
+ resource_scope(Resource.new(resources.pop, api_only?, @scope[:shallow], options)) do
+ yield if block_given?
- concerns(options[:concerns]) if options[:concerns]
+ concerns(options[:concerns]) if options[:concerns]
- collection do
- get :index if parent_resource.actions.include?(:index)
- post :create if parent_resource.actions.include?(:create)
- end
+ collection do
+ get :index if parent_resource.actions.include?(:index)
+ post :create if parent_resource.actions.include?(:create)
+ end
- new do
- get :new
- end if parent_resource.actions.include?(:new)
+ new do
+ get :new
+ end if parent_resource.actions.include?(:new)
- set_member_mappings_for_resource
+ set_member_mappings_for_resource
+ end
end
self
@@ -1389,7 +1504,7 @@ module ActionDispatch
end
with_scope_level(:collection) do
- scope(parent_resource.collection_scope) do
+ path_scope(parent_resource.collection_scope) do
yield
end
end
@@ -1413,9 +1528,11 @@ module ActionDispatch
with_scope_level(:member) do
if shallow?
- shallow_scope(parent_resource.member_scope) { yield }
+ shallow_scope {
+ path_scope(parent_resource.member_scope) { yield }
+ }
else
- scope(parent_resource.member_scope) { yield }
+ path_scope(parent_resource.member_scope) { yield }
end
end
end
@@ -1426,7 +1543,7 @@ module ActionDispatch
end
with_scope_level(:new) do
- scope(parent_resource.new_scope(action_path(:new))) do
+ path_scope(parent_resource.new_scope(action_path(:new))) do
yield
end
end
@@ -1439,14 +1556,20 @@ module ActionDispatch
with_scope_level(:nested) do
if shallow? && shallow_nesting_depth >= 1
- shallow_scope(parent_resource.nested_scope, nested_options) { yield }
+ shallow_scope do
+ path_scope(parent_resource.nested_scope) do
+ scope(nested_options) { yield }
+ end
+ end
else
- scope(parent_resource.nested_scope, nested_options) { yield }
+ path_scope(parent_resource.nested_scope) do
+ scope(nested_options) { yield }
+ end
end
end
end
- # See ActionDispatch::Routing::Mapper::Scoping#namespace
+ # See ActionDispatch::Routing::Mapper::Scoping#namespace.
def namespace(path, options = {})
if resource_scope?
nested { super }
@@ -1456,31 +1579,34 @@ module ActionDispatch
end
def shallow
- scope(:shallow => true) do
- yield
- end
+ @scope = @scope.new(shallow: true)
+ yield
+ ensure
+ @scope = @scope.parent
end
def shallow?
- parent_resource.instance_of?(Resource) && @scope[:shallow]
+ !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
+ # match 'path' => 'controller#action', via: :patch
# match 'path', to: 'controller#action', via: :post
# match 'path', 'otherpath', on: :member, via: :get
- def match(path, *rest)
+ def match(path, *rest, &block)
if rest.empty? && Hash === path
options = path
path, to = options.find { |name, _value| name.is_a?(String) }
+ raise ArgumentError, "Route path not specified" if path.nil?
+
case to
when Symbol
options[:action] = to
when String
- if to =~ /#/
+ if /#/.match?(to)
options[:to] = to
else
options[:controller] = to
@@ -1496,77 +1622,30 @@ module ActionDispatch
paths = [path] + rest
end
- options[:anchor] = true unless options.key?(:anchor)
-
- if options[:on] && !VALID_ON_OPTIONS.include?(options[:on])
- raise ArgumentError, "Unknown scope #{on.inspect} given to :on"
- end
-
- if @scope[:controller] && @scope[:action]
- options[:to] ||= "#{@scope[:controller]}##{@scope[:action]}"
- end
-
- paths.each do |_path|
- route_options = options.dup
- route_options[:path] ||= _path if _path.is_a?(String)
-
- path_without_format = _path.to_s.sub(/\(\.:format\)$/, '')
- if using_match_shorthand?(path_without_format, route_options)
- route_options[:to] ||= path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1')
- route_options[:to].tr!("-", "_")
- end
-
- decomposed_match(_path, route_options)
- end
- self
- end
-
- def using_match_shorthand?(path, options)
- path && (options[:to] || options[:action]).nil? && path =~ %r{^/?[-\w]+/[-\w/]+$}
- end
-
- def decomposed_match(path, options) # :nodoc:
- if on = options.delete(:on)
- send(on) { decomposed_match(path, options) }
+ if options.key?(:defaults)
+ defaults(options.delete(:defaults)) { map_match(paths, options, &block) }
else
- case @scope.scope_level
- when :resources
- nested { decomposed_match(path, options) }
- when :resource
- member { decomposed_match(path, options) }
- else
- add_route(path, options)
- end
+ map_match(paths, options, &block)
end
end
- def add_route(action, options) # :nodoc:
- path = path_for_action(action, options.delete(:path))
- raise ArgumentError, "path is required" if path.blank?
-
- action = action.to_s.dup
-
- if action =~ /^[\w\-\/]+$/
- options[:action] ||= action.tr('-', '_') unless action.include?("/")
- else
- action = nil
- end
-
- as = if !options.fetch(:as, true) # if it's set to nil or false
- options.delete(:as)
- else
- name_for_action(options.delete(:as), action)
- end
-
- mapping = Mapping.build(@scope, @set, URI.parser.escape(path), as, options)
- app, conditions, requirements, defaults, as, anchor = mapping.to_route
- @set.add_route(app, conditions, requirements, defaults, as, anchor)
- end
-
- def root(path, options={})
+ # You can specify what Rails should route "/" to with the root method:
+ #
+ # root to: 'pages#main'
+ #
+ # For options, see +match+, as +root+ uses it internally.
+ #
+ # You can also pass a string which will expand
+ #
+ # root 'pages#main'
+ #
+ # You should put the root route at the top of <tt>config/routes.rb</tt>,
+ # because this means it will be matched first. As this is the most popular route
+ # of most Rails applications, this is beneficial.
+ def root(path, options = {})
if path.is_a?(String)
options[:to] = path
- elsif path.is_a?(Hash) and options.empty?
+ elsif path.is_a?(Hash) && options.empty?
options = path
else
raise ArgumentError, "must be called with a path and/or options"
@@ -1574,22 +1653,22 @@ module ActionDispatch
if @scope.resources?
with_scope_level(:root) do
- scope(parent_resource.path) do
- super(options)
+ path_scope(parent_resource.path) do
+ match_root_route(options)
end
end
else
- super(options)
+ match_root_route(options)
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
@@ -1619,71 +1698,51 @@ module ActionDispatch
return true
end
- unless action_options?(options)
- options.merge!(scope_action_options) if scope_action_options?
- end
-
false
end
- def action_options?(options) #:nodoc:
- options[:only] || options[:except]
+ def apply_action_options(options)
+ return options if action_options? options
+ options.merge scope_action_options
end
- def scope_action_options? #:nodoc:
- @scope[:options] && (@scope[:options][:only] || @scope[:options][:except])
+ def action_options?(options)
+ options[:only] || options[:except]
end
- def scope_action_options #:nodoc:
- @scope[:options].slice(:only, :except)
+ 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_exclusive_scope
- begin
- @scope = @scope.new(:as => nil, :path => nil)
-
- with_scope_level(:exclusive) do
- yield
- end
- ensure
- @scope = @scope.parent
- end
- 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(kind, resource) #:nodoc:
- resource.shallow = @scope[:shallow]
- @scope = @scope.new(:scope_level_resource => resource)
- @nesting.push(resource)
+ def resource_scope(resource)
+ @scope = @scope.new(scope_level_resource: resource)
- with_scope_level(kind) do
- scope(parent_resource.resource_scope) { yield }
- end
+ controller(resource.resource_scope) { yield }
ensure
- @nesting.pop
@scope = @scope.parent
end
- def nested_options #:nodoc:
- options = { :as => parent_resource.member_name }
+ def nested_options
+ options = { as: parent_resource.member_name }
options[:constraints] = {
parent_resource.nested_param => param_constraint
} if param_constraint?
@@ -1691,62 +1750,61 @@ module ActionDispatch
options
end
- def nesting_depth #:nodoc:
- @nesting.size
+ def shallow_nesting_depth
+ @scope.find_all { |node|
+ node.frame[:scope_level_resource]
+ }.count { |node| node.frame[:scope_level_resource].shallow? }
end
- def shallow_nesting_depth #:nodoc:
- @nesting.count(&: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(path, options = {}) #:nodoc:
- scope = { :as => @scope[:shallow_prefix],
- :path => @scope[:shallow_path] }
+ def shallow_scope
+ scope = { as: @scope[:shallow_prefix],
+ path: @scope[:shallow_path] }
@scope = @scope.new scope
- scope(path, options) { yield }
+ yield
ensure
@scope = @scope.parent
end
- def path_for_action(action, path) #:nodoc:
- if path.blank? && canonical_action?(action)
+ def path_for_action(action, path)
+ return "#{@scope[:path]}/#{path}" if path
+
+ if canonical_action?(action)
@scope[:path].to_s
else
- "#{@scope[:path]}/#{action_path(action, path)}"
+ "#{@scope[:path]}/#{action_path(action)}"
end
end
- def action_path(name, path = nil) #:nodoc:
- name = name.to_sym if name.is_a?(String)
- path || @scope[:path_names][name] || name.to_s
+ 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)
prefix = action
end
- if prefix && prefix != '/' && !prefix.empty?
- Mapper.normalize_name prefix.to_s.tr('-', '_')
+ if prefix && prefix != "/" && !prefix.empty?
+ Mapper.normalize_name prefix.to_s.tr("-", "_")
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]
@@ -1758,21 +1816,21 @@ module ActionDispatch
end
action_name = @scope.action_name(name_prefix, prefix, collection_name, member_name)
- candidate = action_name.select(&:present?).join('_')
+ candidate = action_name.select(&:present?).join("_")
unless candidate.empty?
# If a name was not explicitly given, we check if it is valid
# and return nil in case it isn't. Otherwise, we pass the invalid name
# forward so the underlying router engine treats it and raises an exception.
if as.nil?
- candidate unless candidate !~ /\A[_a-z]/i || @set.named_routes.key?(candidate)
+ candidate unless candidate !~ /\A[_a-z]/i || has_named_route?(candidate)
else
candidate
end
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)
@@ -1784,9 +1842,121 @@ module ActionDispatch
end
end
- def api_only?
+ def api_only? # :doc:
@set.api_only?
end
+
+ def path_scope(path)
+ @scope = @scope.new(path: merge_path_scope(@scope[:path], path))
+ yield
+ ensure
+ @scope = @scope.parent
+ end
+
+ def map_match(paths, options)
+ if options[:on] && !VALID_ON_OPTIONS.include?(options[:on])
+ raise ArgumentError, "Unknown scope #{on.inspect} given to :on"
+ end
+
+ if @scope[:to]
+ options[:to] ||= @scope[:to]
+ end
+
+ if @scope[:controller] && @scope[:action]
+ options[:to] ||= "#{@scope[:controller]}##{@scope[:action]}"
+ end
+
+ controller = options.delete(:controller) || @scope[:controller]
+ option_path = options.delete :path
+ to = options.delete :to
+ via = Mapping.check_via Array(options.delete(:via) {
+ @scope[:via]
+ })
+ formatted = options.delete(:format) { @scope[:format] }
+ anchor = options.delete(:anchor) { true }
+ options_constraints = options.delete(:constraints) || {}
+
+ path_types = paths.group_by(&:class)
+ path_types.fetch(String, []).each do |_path|
+ route_options = options.dup
+ if _path && option_path
+ raise ArgumentError, "Ambiguous route definition. Both :path and the route path were specified as strings."
+ end
+ to = get_to_from_path(_path, to, route_options[:action])
+ decomposed_match(_path, controller, route_options, _path, to, via, formatted, anchor, options_constraints)
+ end
+
+ path_types.fetch(Symbol, []).each do |action|
+ route_options = options.dup
+ decomposed_match(action, controller, route_options, option_path, to, via, formatted, anchor, options_constraints)
+ end
+
+ self
+ end
+
+ def get_to_from_path(path, to, action)
+ return to if to || action
+
+ path_without_format = path.sub(/\(\.:format\)$/, "")
+ if using_match_shorthand?(path_without_format)
+ path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1').tr("-", "_")
+ else
+ nil
+ end
+ end
+
+ def using_match_shorthand?(path)
+ path =~ %r{^/?[-\w]+/[-\w/]+$}
+ end
+
+ 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
+ case @scope.scope_level
+ when :resources
+ nested { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
+ when :resource
+ member { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
+ else
+ add_route(path, controller, options, _path, to, via, formatted, anchor, options_constraints)
+ end
+ end
+ end
+
+ 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?
+
+ action = action.to_s
+
+ default_action = options.delete(:action) || @scope[:action]
+
+ if /^[\w\-\/]+$/.match?(action)
+ default_action ||= action.tr("-", "_") unless action.include?("/")
+ else
+ action = nil
+ end
+
+ as = if !options.fetch(:as, true) # if it's set to nil or false
+ options.delete(:as)
+ else
+ name_for_action(options.delete(:as), action)
+ end
+
+ path = Mapping.normalize_path URI.parser.escape(path), formatted
+ 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, as)
+ end
+
+ def match_root_route(options)
+ name = has_named_route?(name_for_action(:root, nil)) ? nil : :root
+ args = ["/", { as: name, via: :get }.merge!(options)]
+
+ match(*args)
+ end
end
# Routing Concerns allow you to declare common routes that can be reused
@@ -1877,7 +2047,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
@@ -1894,17 +2064,131 @@ 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. <tt>{ controller: "pages", action: "index" }</tt>
+ # * An array, which is passed to +polymorphic_url+
+ # * An Active Model instance
+ # * An Active Model class
+ #
+ # NOTE: Other URL helpers can be called in the block but be careful not to invoke
+ # your custom URL helper again otherwise it will result in a stack overflow error.
+ #
+ # 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
+ # 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,
- :shallow, :blocks, :defaults, :options]
+ :shallow, :blocks, :defaults, :via, :format, :options, :to]
RESOURCE_SCOPES = [:resource, :resources]
RESOURCE_METHOD_SCOPES = [:collection, :member, :new]
attr_reader :parent, :scope_level
- def initialize(hash, parent = {}, scope_level = nil)
+ def initialize(hash, parent = NULL, scope_level = nil)
@hash = hash
@parent = parent
@scope_level = scope_level
@@ -1914,6 +2198,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
@@ -1952,27 +2244,33 @@ module ActionDispatch
end
def new_level(level)
- self.class.new(self, self, level)
- end
-
- def fetch(key, &block)
- @hash.fetch(key, &block)
+ self.class.new(frame, self, level)
end
def [](key)
- @hash.fetch(key) { @parent[key] }
+ scope = find { |node| node.frame.key? key }
+ scope && scope.frame[key]
end
- def []=(k,v)
- @hash[k] = v
+ include Enumerable
+
+ def each
+ node = self
+ until node.equal? NULL
+ yield node
+ node = node.parent
+ end
end
+
+ def frame; @hash; end
+
+ NULL = Scope.new(nil, nil)
end
def initialize(set) #:nodoc:
@set = set
- @scope = Scope.new({ :path_names => @set.resources_path_names })
+ @scope = Scope.new(path_names: @set.resources_path_names)
@concerns = {}
- @nesting = []
end
include Base
@@ -1981,6 +2279,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 9934f5547a..4de5f9e2f7 100644
--- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
+++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
@@ -1,10 +1,12 @@
+# frozen_string_literal: true
+
module ActionDispatch
module Routing
# Polymorphic URL helpers are methods for smart resolution to a named route call when
# given an Active Record model instance. They are to be used in combination with
# ActionController::Resources.
#
- # These methods are useful when you want to generate correct URL or path to a RESTful
+ # These methods are useful when you want to generate the correct URL or path to a RESTful
# resource without having to know the exact type of the record in question.
#
# Nested resources and/or namespaces are also supported, as illustrated in the example:
@@ -40,7 +42,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
@@ -79,7 +81,7 @@ module ActionDispatch
# polymorphic_url([blog, post], anchor: 'my_anchor', script_name: "/my_app")
# # => "http://example.com/my_app/blogs/1/posts/1#my_anchor"
#
- # For all of these options, see the documentation for <tt>url_for</tt>.
+ # For all of these options, see the documentation for {url_for}[rdoc-ref:ActionDispatch::Routing::UrlFor].
#
# ==== Functionality
#
@@ -103,6 +105,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
@@ -114,8 +120,7 @@ module ActionDispatch
opts
end
- # Returns the path component of a URL for the given record. It uses
- # <tt>polymorphic_url</tt> with <tt>routing_type: :path</tt>.
+ # Returns the path component of a URL for the given record.
def polymorphic_path(record_or_hash_or_array, options = {})
if Hash === record_or_hash_or_array
options = record_or_hash_or_array.merge(options)
@@ -123,6 +128,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
@@ -134,7 +143,6 @@ module ActionDispatch
opts
end
-
%w(edit new).each do |action|
module_eval <<-EOT, __FILE__, __LINE__ + 1
def #{action}_polymorphic_url(record_or_hash, options = {})
@@ -149,176 +157,195 @@ module ActionDispatch
private
- def polymorphic_url_for_action(action, record_or_hash, options)
- polymorphic_url(record_or_hash, options.merge(:action => action))
- end
-
- def polymorphic_path_for_action(action, record_or_hash, options)
- polymorphic_path(record_or_hash, options.merge(:action => action))
- end
-
- class HelperMethodBuilder # :nodoc:
- CACHE = { 'path' => {}, 'url' => {} }
-
- def self.get(action, type)
- type = type.to_s
- CACHE[type].fetch(action) { build action, type }
+ def polymorphic_url_for_action(action, record_or_hash, options)
+ polymorphic_url(record_or_hash, options.merge(action: action))
end
- def self.url; CACHE['url'.freeze][nil]; end
- def self.path; CACHE['path'.freeze][nil]; end
+ def polymorphic_path_for_action(action, record_or_hash, options)
+ polymorphic_path(record_or_hash, options.merge(action: action))
+ end
- def self.build(action, type)
- prefix = action ? "#{action}_" : ""
- suffix = type
- if action.to_s == 'new'
- HelperMethodBuilder.singular prefix, suffix
+ def polymorphic_mapping(record)
+ if record.respond_to?(:to_model)
+ _routes.polymorphic_mappings[record.to_model.model_name.name]
else
- HelperMethodBuilder.plural prefix, suffix
+ _routes.polymorphic_mappings[record.class.name]
end
end
- def self.singular(prefix, suffix)
- new(->(name) { name.singular_route_key }, prefix, suffix)
- end
+ class HelperMethodBuilder # :nodoc:
+ CACHE = { "path" => {}, "url" => {} }
- def self.plural(prefix, suffix)
- new(->(name) { name.route_key }, prefix, suffix)
- end
+ def self.get(action, type)
+ type = type.to_s
+ CACHE[type].fetch(action) { build action, type }
+ end
- def self.polymorphic_method(recipient, record_or_hash_or_array, action, type, options)
- builder = get action, type
+ def self.url; CACHE["url"][nil]; end
+ def self.path; CACHE["path"][nil]; end
- case record_or_hash_or_array
- when Array
- record_or_hash_or_array = record_or_hash_or_array.compact
- if record_or_hash_or_array.empty?
- raise ArgumentError, "Nil location provided. Can't build URI."
- end
- if record_or_hash_or_array.first.is_a?(ActionDispatch::Routing::RoutesProxy)
- recipient = record_or_hash_or_array.shift
+ def self.build(action, type)
+ prefix = action ? "#{action}_" : ""
+ suffix = type
+ if action.to_s == "new"
+ HelperMethodBuilder.singular prefix, suffix
+ else
+ HelperMethodBuilder.plural prefix, suffix
end
-
- method, args = builder.handle_list record_or_hash_or_array
- when String, Symbol
- method, args = builder.handle_string record_or_hash_or_array
- when Class
- method, args = builder.handle_class record_or_hash_or_array
-
- when nil
- raise ArgumentError, "Nil location provided. Can't build URI."
- else
- method, args = builder.handle_model record_or_hash_or_array
end
+ def self.singular(prefix, suffix)
+ new(->(name) { name.singular_route_key }, prefix, suffix)
+ end
- if options.empty?
- recipient.send(method, *args)
- else
- recipient.send(method, *args, options)
+ def self.plural(prefix, suffix)
+ new(->(name) { name.route_key }, prefix, suffix)
end
- end
- attr_reader :suffix, :prefix
+ def self.polymorphic_method(recipient, record_or_hash_or_array, action, type, options)
+ builder = get action, type
+
+ case record_or_hash_or_array
+ when Array
+ record_or_hash_or_array = record_or_hash_or_array.compact
+ if record_or_hash_or_array.empty?
+ raise ArgumentError, "Nil location provided. Can't build URI."
+ end
+ if record_or_hash_or_array.first.is_a?(ActionDispatch::Routing::RoutesProxy)
+ recipient = record_or_hash_or_array.shift
+ end
+
+ method, args = builder.handle_list record_or_hash_or_array
+ when String, Symbol
+ method, args = builder.handle_string record_or_hash_or_array
+ when Class
+ method, args = builder.handle_class record_or_hash_or_array
- def initialize(key_strategy, prefix, suffix)
- @key_strategy = key_strategy
- @prefix = prefix
- @suffix = suffix
- end
+ when nil
+ raise ArgumentError, "Nil location provided. Can't build URI."
+ else
+ method, args = builder.handle_model record_or_hash_or_array
+ end
- def handle_string(record)
- [get_method_for_string(record), []]
- end
+ if options.empty?
+ recipient.send(method, *args)
+ else
+ recipient.send(method, *args, options)
+ end
+ end
- def handle_string_call(target, str)
- target.send get_method_for_string str
- end
+ attr_reader :suffix, :prefix
- def handle_class(klass)
- [get_method_for_class(klass), []]
- end
+ def initialize(key_strategy, prefix, suffix)
+ @key_strategy = key_strategy
+ @prefix = prefix
+ @suffix = suffix
+ end
- def handle_class_call(target, klass)
- target.send get_method_for_class klass
- end
+ def handle_string(record)
+ [get_method_for_string(record), []]
+ end
- def handle_model(record)
- args = []
+ def handle_string_call(target, str)
+ target.send get_method_for_string str
+ end
- model = record.to_model
- named_route = if model.persisted?
- args << model
- get_method_for_string model.model_name.singular_route_key
- else
- get_method_for_class model
- end
+ def handle_class(klass)
+ [get_method_for_class(klass), []]
+ end
- [named_route, args]
- end
+ def handle_class_call(target, klass)
+ target.send get_method_for_class klass
+ end
- def handle_model_call(target, model)
- method, args = handle_model model
- target.send(method, *args)
- end
+ def handle_model(record)
+ args = []
- def handle_list(list)
- record_list = list.dup
- record = record_list.pop
+ model = record.to_model
+ named_route = if model.persisted?
+ args << model
+ get_method_for_string model.model_name.singular_route_key
+ else
+ get_method_for_class model
+ end
- args = []
+ [named_route, args]
+ end
- route = record_list.map { |parent|
- case parent
- when Symbol, String
- parent.to_s
- when Class
- args << parent
- parent.model_name.singular_route_key
+ def handle_model_call(target, record)
+ if mapping = polymorphic_mapping(target, record)
+ mapping.call(target, [record], suffix == "path")
else
- args << parent.to_model
- parent.to_model.model_name.singular_route_key
+ method, args = handle_model(record)
+ target.send(method, *args)
end
- }
-
- route <<
- case record
- when Symbol, String
- record.to_s
- when Class
- @key_strategy.call record.model_name
- else
- model = record.to_model
- if model.persisted?
- args << model
- model.model_name.singular_route_key
+ end
+
+ def handle_list(list)
+ record_list = list.dup
+ record = record_list.pop
+
+ args = []
+
+ route = record_list.map { |parent|
+ case parent
+ when Symbol, String
+ parent.to_s
+ when Class
+ args << parent
+ parent.model_name.singular_route_key
+ else
+ args << parent.to_model
+ parent.to_model.model_name.singular_route_key
+ end
+ }
+
+ route <<
+ case record
+ when Symbol, String
+ record.to_s
+ when Class
+ @key_strategy.call record.model_name
else
- @key_strategy.call model.model_name
+ model = record.to_model
+ if model.persisted?
+ args << model
+ model.model_name.singular_route_key
+ else
+ @key_strategy.call model.model_name
+ end
end
- end
- route << suffix
+ route << suffix
- named_route = prefix + route.join("_")
- [named_route, args]
- end
+ named_route = prefix + route.join("_")
+ [named_route, args]
+ end
- private
+ private
- def get_method_for_class(klass)
- name = @key_strategy.call klass.model_name
- get_method_for_string name
- end
+ 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_string(str)
- "#{prefix}#{str}_#{suffix}"
- end
+ def get_method_for_class(klass)
+ name = @key_strategy.call klass.model_name
+ get_method_for_string name
+ end
- [nil, 'new', 'edit'].each do |action|
- CACHE['url'][action] = build action, 'url'
- CACHE['path'][action] = build action, 'path'
+ def get_method_for_string(str)
+ "#{prefix}#{str}_#{suffix}"
+ end
+
+ [nil, "new", "edit"].each do |action|
+ CACHE["url"][action] = build action, "url"
+ CACHE["path"][action] = build action, "path"
+ end
end
- end
end
end
end
diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb
index 3c1c4fadf6..143a4b3d62 100644
--- a/actionpack/lib/action_dispatch/routing/redirection.rb
+++ b/actionpack/lib/action_dispatch/routing/redirection.rb
@@ -1,9 +1,11 @@
-require 'action_dispatch/http/request'
-require 'active_support/core_ext/uri'
-require 'active_support/core_ext/array/extract_options'
-require 'rack/utils'
-require 'action_controller/metal/exceptions'
-require 'action_dispatch/routing/endpoint'
+# frozen_string_literal: true
+
+require "action_dispatch/http/request"
+require "active_support/core_ext/uri"
+require "active_support/core_ext/array/extract_options"
+require "rack/utils"
+require "action_controller/metal/exceptions"
+require "action_dispatch/routing/endpoint"
module ActionDispatch
module Routing
@@ -22,9 +24,8 @@ module ActionDispatch
end
def serve(req)
- req.check_path_parameters!
uri = URI.parse(path(req.path_parameters, req))
-
+
unless uri.host
if relative_path?(uri.path)
uri.path = "#{req.script_name}/#{uri.path}"
@@ -32,17 +33,19 @@ module ActionDispatch
uri.path = req.script_name.empty? ? "/" : req.script_name
end
end
-
+
uri.scheme ||= req.scheme
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 = {
- 'Location' => uri.to_s,
- 'Content-Type' => 'text/html',
- 'Content-Length' => body.length.to_s
+ "Location" => uri.to_s,
+ "Content-Type" => "text/html",
+ "Content-Length" => body.length.to_s
}
[ status, headers, [body] ]
@@ -58,19 +61,19 @@ module ActionDispatch
private
def relative_path?(path)
- path && !path.empty? && path[0] != '/'
+ path && !path.empty? && path[0] != "/"
end
def escape(params)
- Hash[params.map{ |k,v| [k, Rack::Utils.escape(v)] }]
+ Hash[params.map { |k, v| [k, Rack::Utils.escape(v)] }]
end
def escape_fragment(params)
- Hash[params.map{ |k,v| [k, Journey::Router::Utils.escape_fragment(v)] }]
+ Hash[params.map { |k, v| [k, Journey::Router::Utils.escape_fragment(v)] }]
end
def escape_path(params)
- Hash[params.map{ |k,v| [k, Journey::Router::Utils.escape_path(v)] }]
+ Hash[params.map { |k, v| [k, Journey::Router::Utils.escape_path(v)] }]
end
end
@@ -104,11 +107,11 @@ module ActionDispatch
def path(params, request)
url_options = {
- :protocol => request.protocol,
- :host => request.host,
- :port => request.optional_port,
- :path => request.path,
- :params => request.query_parameters
+ protocol: request.protocol,
+ host: request.host,
+ port: request.optional_port,
+ path: request.path,
+ params: request.query_parameters
}.merge! options
if !params.empty? && url_options[:path].match(/%\{\w*\}/)
@@ -124,26 +127,28 @@ module ActionDispatch
url_options[:script_name] = request.script_name
end
end
-
+
ActionDispatch::Http::URL.url_for url_options
end
def inspect
- "redirect(#{status}, #{options.map{ |k,v| "#{k}: #{v}" }.join(', ')})"
+ "redirect(#{status}, #{options.map { |k, v| "#{k}: #{v}" }.join(', ')})"
end
end
module Redirection
-
# Redirect any path to another path:
#
# get "/stories" => redirect("/posts")
#
+ # This will redirect the user, while ignoring certain parts of the request, including query string, etc.
+ # <tt>/stories</tt>, <tt>/stories?foo=bar</tt>, etc all redirect to <tt>/posts</tt>.
+ #
# 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.
#
@@ -162,11 +167,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.
+ # <tt>/stories</tt>, <tt>/stories?foo=bar</tt>, redirect to <tt>/posts</tt> and <tt>/posts?foo=bar</tt> 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 454593b59f..2ae75b0da8 100644
--- a/actionpack/lib/action_dispatch/routing/route_set.rb
+++ b/actionpack/lib/action_dispatch/routing/route_set.rb
@@ -1,14 +1,13 @@
-require 'action_dispatch/journey'
-require 'forwardable'
-require 'thread_safe'
-require 'active_support/concern'
-require 'active_support/core_ext/object/to_query'
-require 'active_support/core_ext/hash/slice'
-require 'active_support/core_ext/module/remove_method'
-require 'active_support/core_ext/array/extract_options'
-require 'action_controller/metal/exceptions'
-require 'action_dispatch/http/request'
-require 'action_dispatch/routing/endpoint'
+# frozen_string_literal: true
+
+require "action_dispatch/journey"
+require "active_support/core_ext/object/to_query"
+require "active_support/core_ext/module/redefine_method"
+require "active_support/core_ext/module/remove_method"
+require "active_support/core_ext/array/extract_options"
+require "action_controller/metal/exceptions"
+require "action_dispatch/http/request"
+require "action_dispatch/routing/endpoint"
module ActionDispatch
module Routing
@@ -21,65 +20,47 @@ module ActionDispatch
alias inspect to_s
class Dispatcher < Routing::Endpoint
- def initialize(defaults)
- @defaults = defaults
- @controller_class_names = ThreadSafe::Cache.new
+ def initialize(raise_on_name_error)
+ @raise_on_name_error = raise_on_name_error
end
def dispatcher?; true; end
def serve(req)
- req.check_path_parameters!
- params = req.path_parameters
-
- prepare_params!(params)
-
- # Just raise undefined constant errors if a controller was specified as default.
- unless controller = controller(params, @defaults.key?(:controller))
- return [404, {'X-Cascade' => 'pass'}, []]
+ params = req.path_parameters
+ controller = controller req
+ res = controller.make_response! req
+ dispatch(controller, params[:action], req, res)
+ rescue ActionController::RoutingError
+ if @raise_on_name_error
+ raise
+ else
+ [404, { "X-Cascade" => "pass" }, []]
end
-
- dispatch(controller, params[:action], req.env)
end
- def prepare_params!(params)
- normalize_controller!(params)
- merge_default_action!(params)
- end
+ private
- # If this is a default_controller (i.e. a controller specified by the user)
- # we should raise an error in case it's not found, because it usually means
- # a user error. However, if the controller was retrieved through a dynamic
- # segment, as in :controller(/:action), we should simply return nil and
- # delegate the control back to Rack cascade. Besides, if this is not a default
- # controller, it means we should respect the @scope[:module] parameter.
- def controller(params, default_controller=true)
- if params && params.key?(:controller)
- controller_param = params[:controller]
- controller_reference(controller_param)
- end
+ def controller(req)
+ req.controller_class
rescue NameError => e
- raise ActionController::RoutingError, e.message, e.backtrace if default_controller
+ raise ActionController::RoutingError, e.message, e.backtrace
end
- private
-
- def controller_reference(controller_param)
- const_name = @controller_class_names[controller_param] ||= "#{controller_param.camelize}Controller"
- ActiveSupport::Dependencies.constantize(const_name)
+ def dispatch(controller, action, req, res)
+ controller.dispatch(action, req, res)
end
+ end
- def dispatch(controller, action, env)
- controller.action(action).call(env)
+ class StaticDispatcher < Dispatcher
+ def initialize(controller_class)
+ super(false)
+ @controller_class = controller_class
end
- def normalize_controller!(params)
- params[:controller] = params[:controller].underscore if params.key?(:controller)
- end
+ private
- def merge_default_action!(params)
- params[:action] ||= 'index'
- end
+ def controller(_); @controller_class; end
end
# A NamedRouteCollection instance is a collection of named routes, and also
@@ -88,9 +69,10 @@ module ActionDispatch
class NamedRouteCollection
include Enumerable
attr_reader :routes, :url_helpers_module, :path_helpers_module
+ private :routes
def initialize
- @routes = {}
+ @routes = {}
@path_helpers = Set.new
@url_helpers = Set.new
@url_helpers_module = Module.new
@@ -108,11 +90,11 @@ 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
@routes.clear
@@ -142,6 +124,7 @@ module ActionDispatch
end
def key?(name)
+ return unless name
routes.key? name.to_sym
end
@@ -162,6 +145,31 @@ module ActionDispatch
routes.length
end
+ # Given a +name+, defines name_path and name_url helpers.
+ # Used by 'direct', 'resolve', and 'polymorphic' route helpers.
+ def add_url_helper(name, defaults, &block)
+ helper = CustomUrlHelper.new(name, defaults, &block)
+ path_name = :"#{name}_path"
+ url_name = :"#{name}_url"
+
+ @path_helpers_module.module_eval do
+ redefine_method(path_name) do |*args|
+ helper.call(self, args, true)
+ end
+ end
+
+ @url_helpers_module.module_eval do
+ redefine_method(url_name) do |*args|
+ helper.call(self, args, false)
+ end
+ end
+
+ @path_helpers << path_name
+ @url_helpers << url_name
+
+ self
+ end
+
class UrlHelper
def self.create(route, options, route_name, url_strategy)
if optimize_helper?(route)
@@ -190,6 +198,16 @@ module ActionDispatch
if args.size == arg_size && !inner_options && optimize_routes_generation?(t)
options = t.url_options.merge @options
options[:path] = optimized_helper(args)
+
+ original_script_name = options.delete(:original_script_name)
+ script_name = t._routes.find_script_name(options)
+
+ if original_script_name
+ script_name = original_script_name + script_name
+ end
+
+ options[:script_name] = script_name
+
url_strategy.call options
else
super
@@ -198,40 +216,40 @@ module ActionDispatch
private
- def optimized_helper(args)
- params = parameterize_args(args) { |k|
- raise_generation_error(args)
- }
+ def optimized_helper(args)
+ params = parameterize_args(args) do
+ raise_generation_error(args)
+ end
- @route.format params
- end
+ @route.format params
+ end
- def optimize_routes_generation?(t)
- t.send(:optimize_routes_generation?)
- end
+ def optimize_routes_generation?(t)
+ t.send(:optimize_routes_generation?)
+ end
- def parameterize_args(args)
- params = {}
- @arg_size.times { |i|
- key = @required_parts[i]
- value = args[i].to_param
- yield key if value.nil? || value.empty?
- params[key] = value
- }
- params
- end
+ def parameterize_args(args)
+ params = {}
+ @arg_size.times { |i|
+ key = @required_parts[i]
+ value = args[i].to_param
+ yield key if value.nil? || value.empty?
+ params[key] = value
+ }
+ params
+ end
- def raise_generation_error(args)
- missing_keys = []
- params = parameterize_args(args) { |missing_key|
- missing_keys << missing_key
- }
- constraints = Hash[@route.requirements.merge(params).sort_by{|k,v| k.to_s}]
- message = "No route matches #{constraints.inspect}"
- message << " missing required keys: #{missing_keys.sort.inspect}"
+ def raise_generation_error(args)
+ missing_keys = []
+ params = parameterize_args(args) { |missing_key|
+ missing_keys << missing_key
+ }
+ constraints = Hash[@route.requirements.merge(params).sort_by { |k, v| k.to_s }]
+ message = +"No route matches #{constraints.inspect}"
+ message << ", missing required keys: #{missing_keys.sort.inspect}"
- raise ActionController::UrlGenerationError, message
- end
+ raise ActionController::UrlGenerationError, message
+ end
end
def initialize(route, options, route_name, url_strategy)
@@ -266,10 +284,16 @@ module ActionDispatch
if args.size < path_params_size
path_params -= controller_options.keys
path_params -= result.keys
+ else
+ path_params = path_params.dup
+ end
+ inner_options.each_key do |key|
+ path_params.delete(key)
end
- path_params -= inner_options.keys
- path_params.take(args.size).each do |param|
- result[param] = args.shift
+
+ args.each_with_index do |arg, index|
+ param = path_params[index]
+ result[param] = arg if param
end
end
@@ -278,29 +302,35 @@ module ActionDispatch
end
private
- # Create a url helper allowing ordered parameters to be associated
- # with corresponding dynamic segments, so you can do:
- #
- # foo_url(bar, baz, bang)
- #
- # Instead of:
- #
- # foo_url(bar: bar, baz: baz, bang: bang)
- #
- # Also allow options hash, so you can do:
- #
- # foo_url(bar, baz, bang, sort_by: 'baz')
- #
- def define_url_helper(mod, route, name, opts, route_key, url_strategy)
- helper = UrlHelper.create(route, opts, route_key, url_strategy)
- mod.module_eval do
- define_method(name) do |*args|
- options = nil
- options = args.pop if args.last.is_a? Hash
- helper.call self, args, options
+ # Create a URL helper allowing ordered parameters to be associated
+ # with corresponding dynamic segments, so you can do:
+ #
+ # foo_url(bar, baz, bang)
+ #
+ # Instead of:
+ #
+ # foo_url(bar: bar, baz: baz, bang: bang)
+ #
+ # Also allow options hash, so you can do:
+ #
+ # foo_url(bar, baz, bang, sort_by: 'baz')
+ #
+ def define_url_helper(mod, route, name, opts, route_key, url_strategy)
+ helper = UrlHelper.create(route, opts, route_key, url_strategy)
+ mod.module_eval do
+ define_method(name) do |*args|
+ last = args.last
+ options = \
+ case last
+ when Hash
+ args.pop
+ when ActionController::Parameters
+ args.pop.to_h
+ end
+ helper.call self, args, options
+ end
end
end
- end
end
# strategy for building urls to send to the client
@@ -310,12 +340,12 @@ 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
def self.default_resources_path_names
- { :new => 'new', :edit => 'edit' }
+ { new: "new", edit: "edit" }
end
def self.new_with_config(config)
@@ -347,11 +377,18 @@ module ActionDispatch
@prepend = []
@disable_clear_and_finalize = false
@finalized = false
- @env_key = "ROUTES_#{object_id}_SCRIPT_NAME".freeze
+ @env_key = "ROUTES_#{object_id}_SCRIPT_NAME"
@set = Journey::Routes.new
@router = Journey::Router.new @set
- @formatter = Journey::Formatter.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
@@ -366,6 +403,11 @@ module ActionDispatch
ActionDispatch::Request
end
+ def make_request(env)
+ request_class.new env
+ end
+ private :make_request
+
def draw(&block)
clear! unless @disable_clear_and_finalize
eval_block(block)
@@ -382,10 +424,6 @@ module ActionDispatch
end
def eval_block(block)
- if block.arity == 1
- raise "You are using the old router DSL which has been removed in Rails 3.1. " <<
- "Please check how to update your routes file at: http://www.engineyard.com/blog/2010/the-lowdown-on-routes-in-rails-3/"
- end
mapper = Mapper.new(self)
if default_scope
mapper.with_default_scope(default_scope, &block)
@@ -406,13 +444,10 @@ module ActionDispatch
named_routes.clear
set.clear
formatter.clear
+ @polymorphic_mappings.clear
@prepend.each { |blk| eval_block(blk) }
end
- def dispatcher(defaults)
- Routing::RouteSet::Dispatcher.new(defaults)
- end
-
module MountedHelpers
extend ActiveSupport::Concern
include UrlFor
@@ -426,7 +461,7 @@ module ActionDispatch
MountedHelpers
end
- def define_mounted_helper(name)
+ def define_mounted_helper(name, script_namer = nil)
return if MountedHelpers.method_defined?(name)
routes = self
@@ -434,7 +469,7 @@ module ActionDispatch
MountedHelpers.class_eval do
define_method "_#{name}" do
- RoutesProxy.new(routes, _routes_context, helpers)
+ RoutesProxy.new(routes, _routes_context, helpers, script_namer)
end
end
@@ -454,17 +489,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
@@ -488,7 +556,7 @@ module ActionDispatch
# plus a singleton class method called _routes ...
included do
- singleton_class.send(:redefine_method, :_routes) { routes }
+ redefine_singleton_method(:_routes) { routes }
end
# And an instance method _routes. Note that
@@ -508,7 +576,7 @@ module ActionDispatch
routes.empty?
end
- def add_route(app, conditions = {}, requirements = {}, defaults = {}, name = nil, anchor = true)
+ 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]
@@ -516,77 +584,73 @@ module ActionDispatch
"You may have defined two routes with the same name using the `:as` option, or " \
"you may be overriding a route already defined by a resource with the same naming. " \
"For the latter, you can restrict the routes created with `resources` as explained here: \n" \
- "http://guides.rubyonrails.org/routing.html#restricting-the-routes-created"
+ "https://guides.rubyonrails.org/routing.html#restricting-the-routes-created"
end
- path = conditions.delete :path_info
- ast = conditions.delete :parsed_path_info
- required_defaults = conditions.delete :required_defaults
- path = build_path(path, ast, requirements, anchor)
- conditions = build_conditions(conditions)
-
- route = @set.add_route(app, path, conditions, required_defaults, defaults, name)
+ route = @set.add_route(name, mapping)
named_routes[name] = route if name
- route
- end
- def build_path(path, ast, requirements, anchor)
- strexp = Journey::Router::Strexp.new(
- ast,
- path,
- requirements,
- SEPARATORS,
- anchor)
-
- pattern = Journey::Path::Pattern.new(strexp)
-
- builder = Journey::GTG::Builder.new pattern.spec
+ 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 6.0.
+ MSG
+ end
- # Get all the symbol nodes followed by literals that are not the
- # dummy node.
- symbols = pattern.spec.grep(Journey::Nodes::Symbol).find_all { |n|
- builder.followpos(n).first.literal?
- }
+ 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 6.0.
+ MSG
+ end
- # Get all the symbol nodes preceded by literals.
- symbols.concat pattern.spec.find_all(&:literal?).map { |n|
- builder.followpos(n).first
- }.find_all(&:symbol?)
+ route
+ end
- symbols.each { |x|
- x.regexp = /(?:#{Regexp.union(x.regexp, '-')})+/
- }
+ def add_polymorphic_mapping(klass, options, &block)
+ @polymorphic_mappings[klass] = CustomUrlHelper.new(klass, options, &block)
+ end
- pattern
+ def add_url_helper(name, options, &block)
+ named_routes.add_url_helper(name, options, &block)
end
- private :build_path
- def build_conditions(current_conditions)
- conditions = current_conditions.dup
+ class CustomUrlHelper
+ attr_reader :name, :defaults, :block
- # Rack-Mount requires that :request_method be a regular expression.
- # :request_method represents the HTTP verb that matches this route.
- #
- # Here we munge values before they get sent on to rack-mount.
- verbs = conditions[:request_method] || []
- unless verbs.empty?
- conditions[:request_method] = %r[^#{verbs.join('|')}$]
+ def initialize(name, defaults, &block)
+ @name = name
+ @defaults = defaults
+ @block = block
end
- conditions.keep_if do |k, _|
- request_class.public_method_defined?(k)
+ 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
- private :build_conditions
class Generator
PARAMETERIZE = lambda do |name, value|
if name == :controller
value
- elsif value.is_a?(Array)
- value.map(&:to_param).join('/')
- elsif param = value.to_param
- param
+ else
+ value.to_param
end
end
@@ -594,16 +658,14 @@ module ActionDispatch
def initialize(named_route, options, recall, set)
@named_route = named_route
- @options = options.dup
- @recall = recall.dup
+ @options = options
+ @recall = recall
@set = set
- normalize_recall!
normalize_options!
normalize_controller_action_id!
use_relative_controller!
normalize_controller!
- normalize_action!
end
def controller
@@ -617,16 +679,11 @@ module ActionDispatch
def use_recall_for(key)
if @recall[key] && (!@options.key?(key) || @options[key] == @recall[key])
if !named_route_exists? || segment_keys.include?(key)
- @options[key] = @recall.delete(key)
+ @options[key] = @recall[key]
end
end
end
- # Set 'index' as default action for recall
- def normalize_recall!
- @recall[:action] ||= 'index'
- end
-
def normalize_options!
# If an explicit :controller was given, always make :action explicit
# too, so that action expiry works as expected for things like
@@ -638,12 +695,12 @@ module ActionDispatch
# be "index", not the recalled action of "show".
if options[:controller]
- options[:action] ||= 'index'
+ options[:action] ||= "index"
options[:controller] = options[:controller].to_s
end
if options.key?(:action)
- options[:action] = (options[:action] || 'index').to_s
+ options[:action] = (options[:action] || "index").to_s
end
end
@@ -653,8 +710,8 @@ module ActionDispatch
# :controller, :action or :id is not found, don't pull any
# more keys from the recall.
def normalize_controller_action_id!
- use_recall_for(:controller) or return
- use_recall_for(:action) or return
+ use_recall_for(:controller) || return
+ use_recall_for(:action) || return
use_recall_for(:id)
end
@@ -662,7 +719,7 @@ module ActionDispatch
# is specified, the controller becomes "foo/baz/bat"
def use_relative_controller!
if !named_route && different_controller? && !controller.start_with?("/")
- old_parts = current_controller.split('/')
+ old_parts = current_controller.split("/")
size = controller.count("/") + 1
parts = old_parts[0...-size] << controller
@options[:controller] = parts.join("/")
@@ -671,13 +728,12 @@ module ActionDispatch
# Remove leading slashes from controllers
def normalize_controller!
- @options[:controller] = controller.sub(%r{^/}, '') if controller
- end
-
- # Move 'index' action from options to recall
- def normalize_action!
- if @options[:action] == 'index'
- @recall[:action] = @options.delete(:action)
+ if controller
+ if controller.start_with?("/")
+ @options[:controller] = controller[1..-1]
+ else
+ @options[:controller] = controller
+ end
end
end
@@ -704,11 +760,11 @@ module ActionDispatch
# Generate the path indicated by the arguments, and return an array of
# the keys that were not used to generate it.
- def extra_keys(options, recall={})
+ def extra_keys(options, recall = {})
generate_extras(options, recall).last
end
- def generate_extras(options, recall={})
+ def generate_extras(options, recall = {})
route_key = options.delete :use_route
path, params = generate(route_key, options, recall)
return path, params.keys
@@ -721,14 +777,18 @@ module ActionDispatch
RESERVED_OPTIONS = [:host, :protocol, :port, :subdomain, :domain, :tld_length,
:trailing_slash, :anchor, :params, :only_path, :script_name,
- :original_script_name]
+ :original_script_name, :relative_url_root]
def optimize_routes_generation?
default_url_options.empty?
end
def find_script_name(options)
- options.delete(:script_name) || relative_url_root || ''
+ options.delete(:script_name) || find_relative_url_root(options) || ""
+ end
+
+ def find_relative_url_root(options)
+ options.delete(:relative_url_root) || relative_url_root
end
def path_for(options, route_name = nil)
@@ -746,7 +806,7 @@ module ActionDispatch
password = options.delete :password
end
- recall = options.delete(:_recall) { {} }
+ recall = options.delete(:_recall) { {} }
original_script_name = options.delete(:original_script_name)
script_name = find_script_name options
@@ -760,10 +820,6 @@ module ActionDispatch
path, params = generate(route_name, path_options, recall)
- if options.key? :params
- params.merge! options[:params]
- end
-
options[:path] = path
options[:script_name] = script_name
options[:params] = params
@@ -774,7 +830,7 @@ module ActionDispatch
end
def call(env)
- req = request_class.new(env)
+ req = make_request(env)
req.path_info = Journey::Router::Utils.normalize_path(req.path_info)
@router.serve(req)
end
@@ -785,12 +841,16 @@ module ActionDispatch
extras = environment[:extras] || {}
begin
- env = Rack::MockRequest.env_for(path, {:method => method})
+ env = Rack::MockRequest.env_for(path, method: method)
rescue URI::InvalidURIError => e
raise ActionController::RoutingError, e.message
end
- req = request_class.new(env)
+ req = make_request(env)
+ recognize_path_with_request(req, path, extras)
+ end
+
+ def recognize_path_with_request(req, path, extras, raise_on_missing: true)
@router.recognize(req) do |route, params|
params.merge!(extras)
params.each do |key, value|
@@ -799,22 +859,25 @@ 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?
- dispatcher = app.app
-
- if dispatcher.controller(params, false)
- dispatcher.prepare_params!(params)
- return params
- else
+ begin
+ req.controller_class
+ rescue NameError
raise ActionController::RoutingError, "A route matches #{path.inspect}, but references missing controller: #{params[:controller].camelize}Controller"
end
+
+ return req.path_parameters
+ elsif app.matches?(req) && app.engine?
+ path_parameters = app.rack_app.routes.recognize_path_with_request(req, path, extras, raise_on_missing: false)
+ return path_parameters if path_parameters
end
end
- raise ActionController::RoutingError, "No route matches #{path.inspect}"
+ if raise_on_missing
+ raise ActionController::RoutingError, "No route matches #{path.inspect}"
+ end
end
end
# :startdoc:
diff --git a/actionpack/lib/action_dispatch/routing/routes_proxy.rb b/actionpack/lib/action_dispatch/routing/routes_proxy.rb
index 040ea04046..587a72729c 100644
--- a/actionpack/lib/action_dispatch/routing/routes_proxy.rb
+++ b/actionpack/lib/action_dispatch/routing/routes_proxy.rb
@@ -1,4 +1,6 @@
-require 'active_support/core_ext/array/extract_options'
+# frozen_string_literal: true
+
+require "active_support/core_ext/array/extract_options"
module ActionDispatch
module Routing
@@ -8,9 +10,10 @@ module ActionDispatch
attr_accessor :scope, :routes
alias :_routes :routes
- def initialize(routes, scope, helpers)
+ def initialize(routes, scope, helpers, script_namer = nil)
@routes, @scope = routes, scope
@helpers = helpers
+ @script_namer = script_namer
end
def url_options
@@ -19,7 +22,8 @@ module ActionDispatch
end
end
- def respond_to?(method, include_private = false)
+ private
+ def respond_to_missing?(method, _)
super || @helpers.respond_to?(method)
end
@@ -28,15 +32,38 @@ module ActionDispatch
self.class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{method}(*args)
options = args.extract_options!
- args << url_options.merge((options || {}).symbolize_keys)
+ options = url_options.merge((options || {}).symbolize_keys)
+
+ if @script_namer
+ options[:script_name] = merge_script_names(
+ options[:script_name],
+ @script_namer.call(options)
+ )
+ end
+
+ args << options
@helpers.#{method}(*args)
end
RUBY
- send(method, *args)
+ public_send(method, *args)
else
super
end
end
+
+ # Keeps the part of the script name provided by the global
+ # context via ENV["SCRIPT_NAME"], which `mount` doesn't know
+ # about since it depends on the specific request, but use our
+ # script name resolver for the mount point dependent part.
+ def merge_script_names(previous_script_name, new_script_name)
+ return new_script_name unless previous_script_name
+
+ resolved_parts = new_script_name.count("/")
+ previous_parts = previous_script_name.count("/")
+ context_parts = previous_parts - resolved_parts + 1
+
+ (previous_script_name.split("/").slice(0, context_parts).join("/")) + new_script_name
+ end
end
end
end
diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb
index 8379d089df..1a31c7dbb8 100644
--- a/actionpack/lib/action_dispatch/routing/url_for.rb
+++ b/actionpack/lib/action_dispatch/routing/url_for.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
module ActionDispatch
module Routing
# In <tt>config/routes.rb</tt> you define URL-to-controller mappings, but the reverse
- # is also possible: an URL can be generated from one of your routing definitions.
+ # is also possible: a URL can be generated from one of your routing definitions.
# URL generation functionality is centralized in this module.
#
# See ActionDispatch::Routing for general information about routing and routes.rb.
@@ -107,16 +109,16 @@ module ActionDispatch
end
# Hook overridden in controller to add request information
- # with `default_url_options`. Application logic should not
+ # with +default_url_options+. Application logic should not
# go into url_options.
def url_options
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
@@ -153,7 +155,7 @@ module ActionDispatch
# Missing routes keys may be filled in from the current request's parameters
# (e.g. +:controller+, +:action+, +:id+ and any other parameters that are
# placed in the path). Given that the current action has been reached
- # through `GET /users/1`:
+ # through <tt>GET /users/1</tt>:
#
# url_for(only_path: true) # => '/users/1'
# url_for(only_path: true, action: 'edit') # => '/users/1/edit'
@@ -164,19 +166,24 @@ 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)
- when Hash
+ when Hash, ActionController::Parameters
route_name = options.delete :use_route
- _routes.url_for(options.symbolize_keys.reverse_merge!(url_options),
- route_name)
+ merged_url_options = options.to_h.symbolize_keys.reverse_merge!(url_options)
+ _routes.url_for(merged_url_options, route_name)
when String
options
when Symbol
HelperMethodBuilder.url.handle_string_call self, options
when Array
- polymorphic_url(options, options.extract_options!)
+ components = options.dup
+ polymorphic_url(components, components.extract_options!)
when Class
HelperMethodBuilder.url.handle_class_call self, options
else
@@ -184,22 +191,46 @@ module ActionDispatch
end
end
+ # Allows calling direct or regular named route.
+ #
+ # resources :buckets
+ #
+ # direct :recordable do |recording|
+ # route_for(:bucket, recording.bucket)
+ # end
+ #
+ # direct :threadable do |threadable|
+ # route_for(:recordable, threadable.parent)
+ # end
+ #
+ # This maintains the context of the original caller on
+ # whether to return a path or full URL, e.g:
+ #
+ # threadable_path(threadable) # => "/buckets/1"
+ # threadable_url(threadable) # => "http://example.com/buckets/1"
+ #
+ def route_for(name, *args)
+ public_send(:"#{name}_url", *args)
+ end
+
protected
- def optimize_routes_generation?
- _routes.optimize_routes_generation? && default_url_options.empty?
- end
+ def optimize_routes_generation?
+ _routes.optimize_routes_generation? && default_url_options.empty?
+ end
- def _with_routes(routes)
- old_routes, @_routes = @_routes, routes
- yield
- ensure
- @_routes = old_routes
- end
+ private
- def _routes_context
- self
- end
+ def _with_routes(routes) # :doc:
+ old_routes, @_routes = @_routes, routes
+ yield
+ ensure
+ @_routes = old_routes
+ end
+
+ def _routes_context # :doc:
+ self
+ end
end
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..c74c0ccced
--- /dev/null
+++ b/actionpack/lib/action_dispatch/system_test_case.rb
@@ -0,0 +1,147 @@
+# frozen_string_literal: true
+
+gem "capybara", ">= 2.15"
+
+require "capybara/dsl"
+require "capybara/minitest"
+require "action_controller"
+require "action_dispatch/system_testing/driver"
+require "action_dispatch/system_testing/browser"
+require "action_dispatch/system_testing/server"
+require "action_dispatch/system_testing/test_helpers/screenshot_helper"
+require "action_dispatch/system_testing/test_helpers/setup_and_teardown"
+require "action_dispatch/system_testing/test_helpers/undef_methods"
+
+module ActionDispatch
+ # = System Testing
+ #
+ # 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.
+ #
+ # Headless browsers such as headless Chrome and headless Firefox are also supported.
+ # You can use these browsers by setting the +:using+ argument to +:headless_chrome+ or +:headless_firefox+.
+ #
+ # To use a headless driver, like Poltergeist, update your Gemfile to use
+ # Poltergeist instead of Selenium and then declare the driver name in the
+ # +application_system_test_case.rb+ file. In this case, you would leave out
+ # the +:using+ option because the driver is headless, but you can still use
+ # +:screen_size+ to change the size of the browser screen, also you can use
+ # +:options+ to pass options supported by the driver. Please refer to your
+ # driver documentation to learn about supported options.
+ #
+ # require "test_helper"
+ # require "capybara/poltergeist"
+ #
+ # class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
+ # driven_by :poltergeist, screen_size: [1400, 1400], options:
+ # { js_errors: true }
+ # 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
+ include SystemTesting::TestHelpers::UndefMethods
+
+ def initialize(*) # :nodoc:
+ super
+ self.class.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
+
+ class_attribute :driver, instance_accessor: false
+
+ # System Test configuration options
+ #
+ # The default settings are Selenium, using Chrome, with a screen size
+ # of 1400x1400.
+ #
+ # Examples:
+ #
+ # driven_by :poltergeist
+ #
+ # driven_by :selenium, screen_size: [800, 800]
+ #
+ # driven_by :selenium, using: :chrome
+ #
+ # driven_by :selenium, using: :headless_chrome
+ #
+ # driven_by :selenium, using: :firefox
+ #
+ # driven_by :selenium, using: :headless_firefox
+ def self.driven_by(driver, using: :chrome, screen_size: [1400, 1400], options: {})
+ self.driver = SystemTesting::Driver.new(driver, using: using, screen_size: screen_size, options: options)
+ end
+
+ driven_by :selenium
+
+ ActiveSupport.run_load_hooks(:action_dispatch_system_test_case, self)
+ end
+
+ SystemTestCase.start_application
+end
diff --git a/actionpack/lib/action_dispatch/system_testing/browser.rb b/actionpack/lib/action_dispatch/system_testing/browser.rb
new file mode 100644
index 0000000000..1b0bce6b9e
--- /dev/null
+++ b/actionpack/lib/action_dispatch/system_testing/browser.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ module SystemTesting
+ class Browser # :nodoc:
+ attr_reader :name
+
+ def initialize(name)
+ @name = name
+ end
+
+ def type
+ case name
+ when :headless_chrome
+ :chrome
+ when :headless_firefox
+ :firefox
+ else
+ name
+ end
+ end
+
+ def options
+ case name
+ when :headless_chrome
+ headless_chrome_browser_options
+ when :headless_firefox
+ headless_firefox_browser_options
+ end
+ end
+
+ private
+ def headless_chrome_browser_options
+ options = Selenium::WebDriver::Chrome::Options.new
+ options.args << "--headless"
+ options.args << "--disable-gpu" if Gem.win_platform?
+
+ options
+ end
+
+ def headless_firefox_browser_options
+ options = Selenium::WebDriver::Firefox::Options.new
+ options.args << "-headless"
+
+ options
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/system_testing/driver.rb b/actionpack/lib/action_dispatch/system_testing/driver.rb
new file mode 100644
index 0000000000..5252ff6746
--- /dev/null
+++ b/actionpack/lib/action_dispatch/system_testing/driver.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ module SystemTesting
+ class Driver # :nodoc:
+ def initialize(name, **options)
+ @name = name
+ @browser = Browser.new(options[:using])
+ @screen_size = options[:screen_size]
+ @options = options[:options]
+ end
+
+ def use
+ register if registerable?
+
+ setup
+ end
+
+ private
+ def registerable?
+ [:selenium, :poltergeist, :webkit].include?(@name)
+ end
+
+ def register
+ Capybara.register_driver @name do |app|
+ case @name
+ when :selenium then register_selenium(app)
+ when :poltergeist then register_poltergeist(app)
+ when :webkit then register_webkit(app)
+ end
+ end
+ end
+
+ def browser_options
+ @options.merge(options: @browser.options).compact
+ end
+
+ def register_selenium(app)
+ Capybara::Selenium::Driver.new(app, { browser: @browser.type }.merge(browser_options)).tap do |driver|
+ driver.browser.manage.window.size = Selenium::WebDriver::Dimension.new(*@screen_size)
+ end
+ end
+
+ def register_poltergeist(app)
+ Capybara::Poltergeist::Driver.new(app, @options.merge(window_size: @screen_size))
+ end
+
+ def register_webkit(app)
+ Capybara::Webkit::Driver.new(app, Capybara::Webkit::Configuration.to_hash.merge(@options)).tap do |driver|
+ driver.resize_window_to(driver.current_window_handle, *@screen_size)
+ 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..4fc1f33767
--- /dev/null
+++ b/actionpack/lib/action_dispatch/system_testing/server.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ module SystemTesting
+ class Server # :nodoc:
+ class << self
+ attr_accessor :silence_puma
+ end
+
+ self.silence_puma = false
+
+ def run
+ setup
+ end
+
+ private
+ def setup
+ set_server
+ set_port
+ end
+
+ def set_server
+ Capybara.server = :puma, { Silent: self.class.silence_puma } if Capybara.server == Capybara.servers[:default]
+ 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..884fb51d18
--- /dev/null
+++ b/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+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:
+ # * [+simple+ (default)] Only displays the screenshot path.
+ # This is the default value.
+ # * [+inline+] Display the screenshot in the terminal using the
+ # iTerm image protocol (https://iterm2.com/documentation-images.html).
+ # * [+artifact+] Display the screenshot in the terminal, using the terminal
+ # artifact format (https://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
+ @image_path ||= absolute_image_path.to_s
+ end
+
+ def absolute_image_path
+ Rails.root.join("tmp/screenshots/#{image_name}.png")
+ end
+
+ def save_image
+ page.save_screenshot(absolute_image_path)
+ end
+
+ def output_type
+ # Environment variables have priority
+ output_type = ENV["RAILS_SYSTEM_TESTING_SCREENSHOT"] || ENV["CAPYBARA_INLINE_SCREENSHOT"]
+
+ # Default to outputting a path to the screenshot
+ output_type ||= "simple"
+
+ output_type
+ end
+
+ def display_image
+ message = +"[Screenshot]: #{image_path}\n"
+
+ case output_type
+ when "artifact"
+ message << "\e]1338;url=artifact://#{absolute_image_path}\a\n"
+ when "inline"
+ name = inline_base64(File.basename(absolute_image_path))
+ image = inline_base64(File.read(absolute_image_path))
+ message << "\e]1337;File=name=#{name};height=400px;inline=1:#{image}\a\n"
+ end
+
+ message
+ end
+
+ def inline_base64(path)
+ Base64.strict_encode64(path)
+ 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..600e9c733b
--- /dev/null
+++ b/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ module SystemTesting
+ module TestHelpers
+ module SetupAndTeardown # :nodoc:
+ DEFAULT_HOST = "http://127.0.0.1"
+
+ def host!(host)
+ super
+ Capybara.app_host = host
+ end
+
+ def before_setup
+ host! DEFAULT_HOST
+ super
+ end
+
+ def after_teardown
+ begin
+ take_failed_screenshot
+ ensure
+ Capybara.reset_sessions!
+ end
+ ensure
+ super
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb b/actionpack/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb
new file mode 100644
index 0000000000..d64be3b3d9
--- /dev/null
+++ b/actionpack/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ module SystemTesting
+ module TestHelpers
+ module UndefMethods # :nodoc:
+ extend ActiveSupport::Concern
+ included do
+ METHODS = %i(get post put patch delete).freeze
+
+ METHODS.each do |verb|
+ undef_method verb
+ end
+
+ def method_missing(method, *args, &block)
+ if METHODS.include?(method)
+ raise NoMethodError, "System tests cannot make direct requests via ##{method}; use #visit and #click_on instead. See http://www.rubydoc.info/github/teamcapybara/capybara/master#The_DSL for more information."
+ else
+ super
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/testing/assertion_response.rb b/actionpack/lib/action_dispatch/testing/assertion_response.rb
new file mode 100644
index 0000000000..dc019db6ac
--- /dev/null
+++ b/actionpack/lib/action_dispatch/testing/assertion_response.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ # This is a class that abstracts away an asserted response. It purposely
+ # does not inherit from Response because it doesn't need it. That means it
+ # does not have headers or a body.
+ class AssertionResponse
+ attr_reader :code, :name
+
+ GENERIC_RESPONSE_CODES = { # :nodoc:
+ success: "2XX",
+ missing: "404",
+ redirect: "3XX",
+ error: "5XX"
+ }
+
+ # Accepts a specific response status code as an Integer (404) or String
+ # ('404') or a response status range as a Symbol pseudo-code (:success,
+ # indicating any 200-299 status code).
+ def initialize(code_or_name)
+ if code_or_name.is_a?(Symbol)
+ @name = code_or_name
+ @code = code_from_name(code_or_name)
+ else
+ @name = name_from_code(code_or_name)
+ @code = code_or_name
+ end
+
+ raise ArgumentError, "Invalid response name: #{name}" if @code.nil?
+ raise ArgumentError, "Invalid response code: #{code}" if @name.nil?
+ end
+
+ def code_and_name
+ "#{code}: #{name}"
+ end
+
+ private
+
+ def code_from_name(name)
+ GENERIC_RESPONSE_CODES[name] || Rack::Utils::SYMBOL_TO_STATUS_CODE[name]
+ end
+
+ def name_from_code(code)
+ GENERIC_RESPONSE_CODES.invert[code] || Rack::Utils::HTTP_STATUS_CODES[code]
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/testing/assertions.rb b/actionpack/lib/action_dispatch/testing/assertions.rb
index 21b3b89d22..08c2969685 100644
--- a/actionpack/lib/action_dispatch/testing/assertions.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions.rb
@@ -1,9 +1,11 @@
-require 'rails-dom-testing'
+# frozen_string_literal: true
+
+require "rails-dom-testing"
module ActionDispatch
module Assertions
- autoload :ResponseAssertions, 'action_dispatch/testing/assertions/response'
- autoload :RoutingAssertions, 'action_dispatch/testing/assertions/routing'
+ autoload :ResponseAssertions, "action_dispatch/testing/assertions/response"
+ autoload :RoutingAssertions, "action_dispatch/testing/assertions/routing"
extend ActiveSupport::Concern
@@ -12,7 +14,7 @@ module ActionDispatch
include Rails::Dom::Testing::Assertions
def html_document
- @html_document ||= if @response.content_type === Mime::XML
+ @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 13a72220b3..8595ea03cf 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/response.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb
@@ -1,8 +1,16 @@
+# frozen_string_literal: true
module ActionDispatch
module Assertions
# A small suite of assertions that test responses from \Rails applications.
module ResponseAssertions
+ RESPONSE_PREDICATES = { # :nodoc:
+ success: :successful?,
+ missing: :not_found?,
+ redirect: :redirection?,
+ error: :server_error?,
+ }
+
# Asserts that the response is one of the following types:
#
# * <tt>:success</tt> - Status code was in the 200-299 range
@@ -14,45 +22,37 @@ module ActionDispatch
# or its symbolic equivalent <tt>assert_response(:not_implemented)</tt>.
# See Rack::Utils::SYMBOL_TO_STATUS_CODE for a full list.
#
- # # assert that the response was a redirection
+ # # Asserts that the response was a redirection
# assert_response :redirect
#
- # # assert that the response code was status code 401 (unauthorized)
+ # # Asserts that the response code was status code 401 (unauthorized)
# assert_response 401
def assert_response(type, message = nil)
- message ||= "Expected response to be a <#{type}>, but was <#{@response.response_code}>"
+ message ||= generate_response_message(type)
- if Symbol === type
- if [:success, :missing, :redirect, :error].include?(type)
- assert @response.send("#{type}?"), message
- else
- code = Rack::Utils::SYMBOL_TO_STATUS_CODE[type]
- if code.nil?
- raise ArgumentError, "Invalid response type :#{type}"
- end
- assert_equal code, @response.response_code, message
- end
+ if RESPONSE_PREDICATES.keys.include?(type)
+ assert @response.send(RESPONSE_PREDICATES[type]), message
else
- assert_equal type, @response.response_code, message
+ assert_equal AssertionResponse.new(type).code, @response.response_code, message
end
end
- # Assert that the redirection options passed in match those of the redirect called in the latest action.
+ # Asserts that the redirection options passed in match those of the redirect called in the latest action.
# This match can be partial, such that <tt>assert_redirected_to(controller: "weblog")</tt> will also
# match the redirection of <tt>redirect_to(controller: "weblog", action: "show")</tt> and so on.
#
- # # assert that the redirection was to the "index" action on the WeblogController
+ # # Asserts that the redirection was to the "index" action on the WeblogController
# assert_redirected_to controller: "weblog", action: "index"
#
- # # assert that the redirection was to the named route login_url
+ # # Asserts that the redirection was to the named route login_url
# assert_redirected_to login_url
#
- # # assert 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
+ # # Asserts that the redirection matches the regular expression
# assert_redirected_to %r(\Ahttp://example.org)
- def assert_redirected_to(options = {}, message=nil)
+ def assert_redirected_to(options = {}, message = nil)
assert_response(:redirect, message)
return true if options === @response.location
@@ -77,6 +77,30 @@ module ActionDispatch
handle._compute_redirect_to_location(@request, fragment)
end
end
+
+ def generate_response_message(expected, actual = @response.response_code)
+ (+"Expected response to be a <#{code_with_name(expected)}>,"\
+ " but was a <#{code_with_name(actual)}>").concat(location_if_redirected).concat(response_body_if_short)
+ end
+
+ def response_body_if_short
+ return "" if @response.body.size > 500
+ "\nResponse body: #{@response.body}"
+ end
+
+ def location_if_redirected
+ return "" unless @response.redirection? && @response.location.present?
+ location = normalize_argument_to_redirection(@response.location)
+ " redirect to <#{location}>"
+ end
+
+ def code_with_name(code_or_name)
+ if RESPONSE_PREDICATES.values.include?("#{code_or_name}?".to_sym)
+ code_or_name = RESPONSE_PREDICATES.invert["#{code_or_name}?".to_sym]
+ end
+
+ AssertionResponse.new(code_or_name).code_and_name
+ end
end
end
end
diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb
index 543c7b78a1..af41521c5c 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb
@@ -1,12 +1,19 @@
-require 'uri'
-require 'active_support/core_ext/hash/indifferent_access'
-require 'active_support/core_ext/string/access'
-require 'action_controller/metal/exceptions'
+# frozen_string_literal: true
+
+require "uri"
+require "active_support/core_ext/hash/indifferent_access"
+require "active_support/core_ext/string/access"
+require "action_controller/metal/exceptions"
module ActionDispatch
module Assertions
# Suite of assertions to test routes generated by \Rails and the handling of requests made to them.
module RoutingAssertions
+ def setup # :nodoc:
+ @routes ||= nil
+ super
+ end
+
# Asserts that the routing of the given +path+ was handled correctly and that the parsed options (given in the +expected_options+ hash)
# match +path+. Basically, it asserts that \Rails recognizes the route given by +expected_options+.
#
@@ -14,14 +21,14 @@ module ActionDispatch
# requiring a specific HTTP method. The hash should contain a :path with the incoming request path
# and a :method containing the required HTTP verb.
#
- # # assert that POSTing to /items will call the create action on ItemsController
+ # # Asserts that POSTing to /items will call the create action on ItemsController
# 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 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:
#
- # # assert that a path of '/items/list/1?view=print' returns the correct options
+ # # 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" })
#
# The +message+ parameter allows you to pass in an error message that is displayed upon failure.
@@ -37,7 +44,7 @@ module ActionDispatch
#
# # Test a custom route
# assert_recognizes({controller: 'items', action: 'show', id: '1'}, 'view/item1')
- def assert_recognizes(expected_options, path, extras={}, msg=nil)
+ def assert_recognizes(expected_options, path, extras = {}, msg = nil)
if path.is_a?(Hash) && path[:method].to_s == "all"
[:get, :post, :put, :delete].each do |method|
assert_recognizes(expected_options, path.merge(method: method), extras, msg)
@@ -75,19 +82,20 @@ module ActionDispatch
#
# # Asserts that the generated route gives us our custom route
# assert_generates "changesets/12", { controller: 'scm', action: 'show_diff', revision: "12" }
- def assert_generates(expected_path, options, defaults={}, extras={}, message=nil)
- if expected_path =~ %r{://}
+ def assert_generates(expected_path, options, defaults = {}, extras = {}, message = nil)
+ if %r{://}.match?(expected_path)
fail_on(URI::InvalidURIError, message) do
uri = URI.parse(expected_path)
expected_path = uri.path.to_s.empty? ? "/" : uri.path
end
else
- expected_path = "/#{expected_path}" unless expected_path.first == '/'
+ expected_path = "/#{expected_path}" unless expected_path.first == "/"
end
# Load routes.rb if it hasn't been loaded.
- generated_path, extra_keys = @routes.generate_extras(options, defaults)
- found_extras = options.reject { |k, _| ! extra_keys.include? k }
+ options = options.clone
+ generated_path, query_string_keys = @routes.generate_extras(options, defaults)
+ found_extras = options.reject { |k, _| ! query_string_keys.include? k }
msg = message || sprintf("found extras <%s>, not <%s>", found_extras, extras)
assert_equal(extras, found_extras, msg)
@@ -104,21 +112,21 @@ module ActionDispatch
# The +extras+ hash allows you to specify options that would normally be provided as a query string to the action. The
# +message+ parameter allows you to specify a custom error message to display upon failure.
#
- # # Assert a basic route: a controller with the default action (index)
+ # # Asserts a basic route: a controller with the default action (index)
# assert_routing '/home', controller: 'home', action: 'index'
#
# # Test a route generated with a specific controller, action, and parameter (id)
# assert_routing '/entries/show/23', controller: 'entries', action: 'show', id: 23
#
- # # Assert a basic route (controller + default action), with an error message if it fails
+ # # Asserts a basic route (controller + default action), with an error message if it fails
# assert_routing '/store', { controller: 'store', action: 'index' }, {}, {}, 'Route for store index not generated properly'
#
# # Tests a route, providing a defaults hash
# assert_routing 'controller/action/9', {id: "9", item: "square"}, {controller: "controller", action: "action"}, {}, {item: "square"}
#
- # # Tests a route with a HTTP method
+ # # Tests a route with an HTTP method
# assert_routing({ method: 'put', path: '/product/321' }, { controller: "product", action: "update", id: "321" })
- def assert_routing(path, options, defaults={}, extras={}, message=nil)
+ def assert_routing(path, options, defaults = {}, extras = {}, message = nil)
assert_recognizes(options, path, extras, message)
controller, default_controller = options[:controller], defaults[:controller]
@@ -126,13 +134,12 @@ module ActionDispatch
options[:controller] = "/#{controller}"
end
- generate_options = options.dup.delete_if{ |k, _| defaults.key?(k) }
+ generate_options = options.dup.delete_if { |k, _| defaults.key?(k) }
assert_generates(path.is_a?(Hash) ? path[:path] : path, generate_options, defaults, extras, message)
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>:
@@ -151,8 +158,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
@@ -165,7 +175,7 @@ module ActionDispatch
# ROUTES TODO: These assertions should really work in an integration context
def method_missing(selector, *args, &block)
- if defined?(@controller) && @controller && @routes && @routes.named_routes.route_defined?(selector)
+ if defined?(@controller) && @controller && defined?(@routes) && @routes && @routes.named_routes.route_defined?(selector)
@controller.send(selector, *args, &block)
else
super
@@ -182,10 +192,9 @@ module ActionDispatch
method = :get
end
- # Assume given controller
- request = ActionController::TestRequest.create
+ request = ActionController::TestRequest.create @controller.class
- if path =~ %r{://}
+ if %r{://}.match?(path)
fail_on(URI::InvalidURIError, msg) do
uri = URI.parse(path)
request.env["rack.url_scheme"] = uri.scheme || "http"
@@ -201,7 +210,7 @@ module ActionDispatch
request.request_method = method if method
params = fail_on(ActionController::RoutingError, msg) do
- @routes.recognize_path(path, { :method => method, :extras => extras })
+ @routes.recognize_path(path, method: method, extras: extras)
end
request.path_parameters = params.with_indifferent_access
diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb
index dc664d5540..45439a3bb1 100644
--- a/actionpack/lib/action_dispatch/testing/integration.rb
+++ b/actionpack/lib/action_dispatch/testing/integration.rb
@@ -1,167 +1,62 @@
-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'
+# frozen_string_literal: true
+
+require "stringio"
+require "uri"
+require "active_support/core_ext/kernel/singleton_class"
+require "active_support/core_ext/object/try"
+require "rack/test"
+require "minitest"
+
+require "action_dispatch/testing/request_encoder"
module ActionDispatch
module Integration #:nodoc:
module RequestHelpers
- # Performs a GET request with the given parameters.
- #
- # - +path+: The URI (as a String) on which you want to perform a GET
- # request.
- # - +params+: The HTTP parameters that you want to pass. This may
- # be +nil+,
- # a Hash, or a String that is appropriately encoded
- # (<tt>application/x-www-form-urlencoded</tt> or
- # <tt>multipart/form-data</tt>).
- # - +headers+: Additional headers to pass, as a Hash. The headers will be
- # merged into the Rack env hash.
- # - +env+: Additional env to pass, as a Hash. The headers will be
- # merged into the Rack env hash.
- #
- # 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.
- #
- # You can also perform POST, PATCH, PUT, DELETE, and HEAD requests with
- # +#post+, +#patch+, +#put+, +#delete+, and +#head+.
- #
- # Example:
- #
- # get '/feed', params: { since: 201501011400 }
- # post '/profile', headers: { "X-Test-Header" => "testvalue" }
- def get(path, *args)
- process_with_kwargs(:get, path, *args)
+ # Performs a GET request with the given parameters. See ActionDispatch::Integration::Session#process
+ # for more details.
+ def get(path, **args)
+ process(:get, path, **args)
end
- # Performs a POST request with the given parameters. See +#get+ for more
- # details.
- def post(path, *args)
- process_with_kwargs(:post, path, *args)
+ # Performs a POST request with the given parameters. See ActionDispatch::Integration::Session#process
+ # for more details.
+ def post(path, **args)
+ process(:post, path, **args)
end
- # Performs a PATCH request with the given parameters. See +#get+ for more
- # details.
- def patch(path, *args)
- process_with_kwargs(:patch, path, *args)
+ # Performs a PATCH request with the given parameters. See ActionDispatch::Integration::Session#process
+ # for more details.
+ def patch(path, **args)
+ process(:patch, path, **args)
end
- # Performs a PUT request with the given parameters. See +#get+ for more
- # details.
- def put(path, *args)
- process_with_kwargs(:put, path, *args)
+ # Performs a PUT request with the given parameters. See ActionDispatch::Integration::Session#process
+ # for more details.
+ def put(path, **args)
+ process(:put, path, **args)
end
- # Performs a DELETE request with the given parameters. See +#get+ for
- # more details.
- def delete(path, *args)
- process_with_kwargs(:delete, path, *args)
+ # Performs a DELETE request with the given parameters. See ActionDispatch::Integration::Session#process
+ # for more details.
+ def delete(path, **args)
+ process(:delete, path, **args)
end
- # Performs a HEAD request with the given parameters. See +#get+ for more
- # details.
+ # Performs a HEAD request with the given parameters. See ActionDispatch::Integration::Session#process
+ # for more details.
def head(path, *args)
- process_with_kwargs(:head, path, *args)
+ process(:head, path, *args)
end
- # Performs an XMLHttpRequest request with the given parameters, mirroring
- # a request from the Prototype library.
- #
- # The request_method is +:get+, +:post+, +:patch+, +:put+, +:delete+ or
- # +:head+; the parameters are +nil+, a hash, or a url-encoded or multipart
- # string; the headers are a hash.
- #
- # Example:
- #
- # xhr :get, '/feed', params: { since: 201501011400 }
- def xml_http_request(request_method, path, *args)
- if kwarg_request?(args)
- params, headers, env = args.first.values_at(:params, :headers, :env)
- else
- params = args[0]
- headers = args[1]
- env = {}
-
- if params.present? || headers.present?
- non_kwarg_request_warning
- end
- end
-
- ActiveSupport::Deprecation.warn(<<-MSG.strip_heredoc)
- xhr and xml_http_request methods are deprecated in favor of
- `get "/posts", xhr: true` and `post "/posts/1", xhr: true`
- MSG
-
- process(request_method, path, params: params, headers: headers, xhr: true)
- end
- alias xhr :xml_http_request
-
# Follow a single redirect response. If the last response was not a
# redirect, an exception will be raised. Otherwise, the redirect is
- # performed on the location header.
- def follow_redirect!
+ # performed on the location header. Any arguments are passed to the
+ # underlying call to `get`.
+ def follow_redirect!(**args)
raise "not a redirect! #{status} #{status_message}" unless redirect?
- get(response.location)
- status
- end
-
- # Performs a request using the specified method, following any subsequent
- # redirect. Note that the redirects are followed until the response is
- # not a redirect--this means you may run into an infinite loop if your
- # redirect loops back to itself.
- #
- # Example:
- #
- # request_via_redirect :post, '/welcome',
- # params: { ref_id: 14 },
- # headers: { "X-Test-Header" => "testvalue" }
- def request_via_redirect(http_method, path, *args)
- process_with_kwargs(http_method, path, *args)
-
- follow_redirect! while redirect?
+ get(response.location, **args)
status
end
-
- # Performs a GET request, following any subsequent redirect.
- # See +request_via_redirect+ for more information.
- def get_via_redirect(path, *args)
- ActiveSupport::Deprecation.warn('`get_via_redirect` is deprecated and will be removed in the next version of Rails. Please use follow_redirect! manually after the request call for the same behavior.')
- request_via_redirect(:get, path, *args)
- end
-
- # Performs a POST request, following any subsequent redirect.
- # See +request_via_redirect+ for more information.
- def post_via_redirect(path, *args)
- ActiveSupport::Deprecation.warn('`post_via_redirect` is deprecated and will be removed in the next version of Rails. Please use follow_redirect! manually after the request call for the same behavior.')
- request_via_redirect(:post, path, *args)
- end
-
- # Performs a PATCH request, following any subsequent redirect.
- # See +request_via_redirect+ for more information.
- def patch_via_redirect(path, *args)
- ActiveSupport::Deprecation.warn('`patch_via_redirect` is deprecated and will be removed in the next version of Rails. Please use follow_redirect! manually after the request call for the same behavior.')
- request_via_redirect(:patch, path, *args)
- end
-
- # Performs a PUT request, following any subsequent redirect.
- # See +request_via_redirect+ for more information.
- def put_via_redirect(path, *args)
- ActiveSupport::Deprecation.warn('`put_via_redirect` is deprecated and will be removed in the next version of Rails. Please use follow_redirect! manually after the request call for the same behavior.')
- request_via_redirect(:put, path, *args)
- end
-
- # Performs a DELETE request, following any subsequent redirect.
- # See +request_via_redirect+ for more information.
- def delete_via_redirect(path, *args)
- ActiveSupport::Deprecation.warn('`delete_via_redirect` is deprecated and will be removed in the next version of Rails. Please use follow_redirect! manually after the request call for the same behavior.')
- request_via_redirect(:delete, path, *args)
- end
end
# An instance of this class represents a set of requests and responses
@@ -179,11 +74,11 @@ module ActionDispatch
include TestProcess, RequestHelpers, Assertions
%w( status status_message headers body redirect? ).each do |method|
- delegate method, :to => :response, :allow_nil => true
+ delegate method, to: :response, allow_nil: true
end
%w( path ).each do |method|
- delegate method, :to => :request, :allow_nil => true
+ delegate method, to: :request, allow_nil: true
end
# The hostname used in the last request.
@@ -234,7 +129,7 @@ module ActionDispatch
url_options.reverse_merge!(@app.routes.default_url_options)
end
- url_options.reverse_merge!(:host => host, :protocol => https? ? "https" : "http")
+ url_options.reverse_merge!(host: host, protocol: https? ? "https" : "http")
end
end
@@ -252,8 +147,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
@@ -280,108 +175,133 @@ module ActionDispatch
@https
end
- # Set the host name to use in the next request.
+ # Performs the actual request.
#
- # session.host! "www.example.com"
- alias :host! :host=
-
- private
- def _mock_session
- @_mock_session ||= Rack::MockSession.new(@app, host)
+ # - +method+: The HTTP method (GET, POST, PATCH, PUT, DELETE, HEAD, OPTIONS)
+ # as a symbol.
+ # - +path+: The URI (as a String) on which you want to perform the
+ # request.
+ # - +params+: The HTTP parameters that you want to pass. This may
+ # be +nil+,
+ # a Hash, or a String that is appropriately encoded
+ # (<tt>application/x-www-form-urlencoded</tt> or
+ # <tt>multipart/form-data</tt>).
+ # - +headers+: Additional headers to pass, as a Hash. The headers will be
+ # merged into the Rack env hash.
+ # - +env+: Additional env to pass, as a Hash. The headers will be
+ # merged into the Rack env hash.
+ # - +xhr+: Set to `true` if you want to make and Ajax request.
+ # Adds request headers characteristic of XMLHttpRequest e.g. HTTP_X_REQUESTED_WITH.
+ # The headers will be merged into the Rack env hash.
+ # - +as+: Used for encoding the request with different content type.
+ # Supports `:json` by default and will set the approriate request headers.
+ # The headers will be merged into the Rack env hash.
+ #
+ # This method is rarely used directly. Use +#get+, +#post+, or other standard
+ # 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 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 }
+ def process(method, path, params: nil, headers: nil, env: nil, xhr: false, as: nil)
+ request_encoder = RequestEncoder.encoder(as)
+ headers ||= {}
+
+ if method == :get && as == :json && params
+ headers["X-Http-Method-Override"] = "GET"
+ method = :post
end
- def process_with_kwargs(http_method, path, *args)
- if kwarg_request?(args)
- process(http_method, path, *args)
- else
- non_kwarg_request_warning if args.any?
- process(http_method, path, { params: args[0], headers: args[1] })
- end
- end
+ if %r{://}.match?(path)
+ path = build_expanded_path(path) do |location|
+ https! URI::HTTPS === location if location.scheme
- REQUEST_KWARGS = %i(params headers env xhr)
- def kwarg_request?(args)
- args[0].respond_to?(:keys) && args[0].keys.any? { |k| REQUEST_KWARGS.include?(k) }
+ if url_host = location.host
+ default = Rack::Request::DEFAULT_PORTS[location.scheme]
+ url_host += ":#{location.port}" if default != location.port
+ host! url_host
+ end
+ end
end
- def non_kwarg_request_warning
- ActiveSupport::Deprecation.warn(<<-MSG.strip_heredoc)
- ActionDispatch::IntegrationTest HTTP request methods will accept only
- the following keyword arguments in future Rails versions:
- #{REQUEST_KWARGS.join(', ')}
+ hostname, port = host.split(":")
- Examples:
+ request_env = {
+ :method => method,
+ :params => request_encoder.encode_params(params),
- get '/profile',
- params: { id: 1 },
- headers: { 'X-Extra-Header' => '123' },
- env: { 'action_dispatch.custom' => 'custom' },
- xhr: true
- MSG
- end
-
- # Performs the actual request.
- def process(method, path, params: nil, headers: nil, env: nil, xhr: false)
- if path =~ %r{://}
- location = URI.parse(path)
- https! URI::HTTPS === location if location.scheme
- host! "#{location.host}:#{location.port}" if location.host
- path = location.query ? "#{location.path}?#{location.query}" : location.path
- end
+ "SERVER_NAME" => hostname,
+ "SERVER_PORT" => port || (https? ? "443" : "80"),
+ "HTTPS" => https? ? "on" : "off",
+ "rack.url_scheme" => https? ? "https" : "http",
- hostname, port = host.split(':')
+ "REQUEST_URI" => path,
+ "HTTP_HOST" => host,
+ "REMOTE_ADDR" => remote_addr,
+ "CONTENT_TYPE" => request_encoder.content_type,
+ "HTTP_ACCEPT" => request_encoder.accept_header || accept
+ }
- request_env = {
- :method => method,
- :params => params,
+ wrapped_headers = Http::Headers.from_hash({})
+ wrapped_headers.merge!(headers) if headers
- "SERVER_NAME" => hostname,
- "SERVER_PORT" => port || (https? ? "443" : "80"),
- "HTTPS" => https? ? "on" : "off",
- "rack.url_scheme" => https? ? "https" : "http",
+ if xhr
+ wrapped_headers["HTTP_X_REQUESTED_WITH"] = "XMLHttpRequest"
+ wrapped_headers["HTTP_ACCEPT"] ||= [Mime[:js], Mime[:html], Mime[:xml], "text/xml", "*/*"].join(", ")
+ end
- "REQUEST_URI" => path,
- "HTTP_HOST" => host,
- "REMOTE_ADDR" => remote_addr,
- "CONTENT_TYPE" => "application/x-www-form-urlencoded",
- "HTTP_ACCEPT" => accept
- }
+ # This modifies the passed request_env directly.
+ if wrapped_headers.present?
+ Http::Headers.from_hash(request_env).merge!(wrapped_headers)
+ end
+ if env.present?
+ Http::Headers.from_hash(request_env).merge!(env)
+ end
- if xhr
- headers ||= {}
- headers['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
- headers['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ')
- end
+ session = Rack::Test::Session.new(_mock_session)
- # this modifies the passed request_env directly
- if headers.present?
- Http::Headers.new(request_env).merge!(headers)
- end
- if env.present?
- Http::Headers.new(request_env).merge!(env)
- end
+ # NOTE: rack-test v0.5 doesn't build a default uri correctly
+ # Make sure requested path is always a full URI.
+ session.request(build_full_uri(path, request_env), request_env)
- session = Rack::Test::Session.new(_mock_session)
+ @request_count += 1
+ @request = ActionDispatch::Request.new(session.last_request.env)
+ response = _mock_session.last_response
+ @response = ActionDispatch::TestResponse.from_response(response)
+ @response.request = @request
+ @html_document = nil
+ @url_options = nil
- # NOTE: rack-test v0.5 doesn't build a default uri correctly
- # Make sure requested path is always a full uri
- session.request(build_full_uri(path, request_env), request_env)
+ @controller = @request.controller_instance
- @request_count += 1
- @request = ActionDispatch::Request.new(session.last_request.env)
- response = _mock_session.last_response
- @response = ActionDispatch::TestResponse.from_response(response)
- @html_document = nil
- @url_options = nil
+ response.status
+ end
- @controller = session.last_request.env['action_controller.instance']
+ # Set the host name to use in the next request.
+ #
+ # session.host! "www.example.com"
+ alias :host! :host=
- response.status
+ private
+ def _mock_session
+ @_mock_session ||= Rack::MockSession.new(@app, host)
end
def build_full_uri(path, env)
"#{env['rack.url_scheme']}://#{env['SERVER_NAME']}:#{env['SERVER_PORT']}#{path}"
end
+
+ def build_expanded_path(path)
+ location = URI.parse(path)
+ yield location if block_given?
+ path = location.path
+ location.query ? "#{path}?#{location.query}" : path
+ end
end
module Runner
@@ -391,9 +311,13 @@ module ActionDispatch
attr_reader :app
- def before_setup
- @app = nil
+ def initialize(*args, &blk)
+ super(*args, &blk)
@integration_session = nil
+ end
+
+ def before_setup # :nodoc:
+ @app = nil
super
end
@@ -409,8 +333,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
@@ -423,11 +347,10 @@ module ActionDispatch
@integration_session = nil
end
- %w(get post patch put head delete cookies assigns
- xml_http_request xhr get_via_redirect post_via_redirect).each do |method|
+ %w(get post patch put head delete cookies assigns follow_redirect!).each do |method|
define_method(method) do |*args|
# reset the html_document variable, except for cookies/assigns calls
- unless method == 'cookies' || method == 'assigns'
+ unless method == "cookies" || method == "assigns"
@html_document = nil
end
@@ -449,6 +372,7 @@ module ActionDispatch
# simultaneously.
def open_session
dup.tap do |session|
+ session.reset!
yield session if block_given?
end
end
@@ -469,14 +393,15 @@ module ActionDispatch
integration_session.default_url_options = options
end
- def respond_to?(method, include_private = false)
- integration_session.respond_to?(method, include_private) || super
+ private
+ def respond_to_missing?(method, _)
+ integration_session.respond_to?(method) || super
end
# Delegate unhandled messages to the current session instance.
- def method_missing(sym, *args, &block)
- if integration_session.respond_to?(sym)
- integration_session.__send__(sym, *args, &block).tap do
+ def method_missing(method, *args, &block)
+ if integration_session.respond_to?(method)
+ integration_session.public_send(method, *args, &block).tap do
copy_session_variables!
end
else
@@ -638,33 +563,97 @@ module ActionDispatch
# end
# end
#
+ # See the {request helpers documentation}[rdoc-ref:ActionDispatch::Integration::RequestHelpers] for help on how to
+ # use +get+, etc.
+ #
+ # === Changing the request encoding
+ #
+ # You can also test your JSON API easily by setting what the request should
+ # be encoded as:
+ #
+ # require "test_helper"
+ #
+ # class ApiTest < ActionDispatch::IntegrationTest
+ # test "creates articles" do
+ # assert_difference -> { Article.count } do
+ # post articles_path, params: { article: { title: "Ahoy!" } }, as: :json
+ # end
+ #
+ # assert_response :success
+ # assert_equal({ id: Article.last.id, title: "Ahoy!" }, response.parsed_body)
+ # end
+ # end
+ #
+ # 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.
+ #
+ # 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 },
+ # response_parser: -> body { body }
+ #
+ # Where +param_encoder+ defines how the params should be encoded and
+ # +response_parser+ defines how the response body should be parsed through
+ # +parsed_body+.
+ #
# Consult the Rails Testing Guide for more.
class IntegrationTest < ActiveSupport::TestCase
- include Integration::Runner
- include ActionController::TemplateAssertions
- include ActionDispatch::Routing::UrlFor
+ include TestProcess::FixtureFile
- @@app = nil
-
- def self.app
- @@app || ActionDispatch.test_app
+ module UrlOptions
+ extend ActiveSupport::Concern
+ def url_options
+ integration_session.url_options
+ end
end
- def self.app=(app)
- @@app = app
- end
+ module Behavior
+ extend ActiveSupport::Concern
- def app
- super || self.class.app
- end
+ include Integration::Runner
+ include ActionController::TemplateAssertions
- def url_options
- integration_session.url_options
- end
+ included do
+ include ActionDispatch::Routing::UrlFor
+ include UrlOptions # don't let UrlFor override the url_options method
+ ActiveSupport.run_load_hooks(:action_dispatch_integration_test, self)
+ @@app = nil
+ end
+
+ module ClassMethods
+ def app
+ if defined?(@@app) && @@app
+ @@app
+ else
+ ActionDispatch.test_app
+ end
+ end
+
+ def app=(app)
+ @@app = app
+ end
+
+ def register_encoder(*args)
+ RequestEncoder.register_encoder(*args)
+ end
+ end
- def document_root_element
- html_document.root
+ def app
+ super || self.class.app
+ end
+
+ def document_root_element
+ html_document.root
+ end
end
+
+ include Behavior
end
end
diff --git a/actionpack/lib/action_dispatch/testing/request_encoder.rb b/actionpack/lib/action_dispatch/testing/request_encoder.rb
new file mode 100644
index 0000000000..9889f61951
--- /dev/null
+++ b/actionpack/lib/action_dispatch/testing/request_encoder.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ class RequestEncoder # :nodoc:
+ 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)
+ @mime = Mime[mime_name]
+
+ unless @mime
+ raise ArgumentError, "Can't register a request encoder for " \
+ "unregistered MIME Type: #{mime_name}. See `Mime::Type.register`."
+ end
+
+ @response_parser = response_parser || -> body { body }
+ @param_encoder = param_encoder || :"to_#{@mime.symbol}".to_proc
+ end
+
+ def content_type
+ @mime.to_s
+ end
+
+ def accept_header
+ @mime.to_s
+ end
+
+ def encode_params(params)
+ @param_encoder.call(params) if params
+ end
+
+ def self.parser(content_type)
+ mime = Mime::Type.lookup(content_type)
+ encoder(mime ? mime.ref : nil).response_parser
+ end
+
+ def self.encoder(name)
+ @encoders[name] || @encoders[:identity]
+ end
+
+ def self.register_encoder(mime_name, param_encoder: nil, response_parser: nil)
+ @encoders[mime_name] = new(mime_name, param_encoder, response_parser)
+ end
+
+ register_encoder :json, response_parser: -> body { JSON.parse(body) }
+ end
+end
diff --git a/actionpack/lib/action_dispatch/testing/test_process.rb b/actionpack/lib/action_dispatch/testing/test_process.rb
index 494644cd46..0b98f27f11 100644
--- a/actionpack/lib/action_dispatch/testing/test_process.rb
+++ b/actionpack/lib/action_dispatch/testing/test_process.rb
@@ -1,9 +1,30 @@
-require 'action_dispatch/middleware/cookies'
-require 'action_dispatch/middleware/flash'
-require 'active_support/core_ext/hash/indifferent_access'
+# frozen_string_literal: true
+
+require "action_dispatch/middleware/cookies"
+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, params: { 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, params: { 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,
@@ -19,26 +40,11 @@ module ActionDispatch
end
def cookies
- @cookie_jar ||= Cookies::CookieJar.build(@request.env, @request.host, @request.ssl?, @request.cookies)
+ @cookie_jar ||= Cookies::CookieJar.build(@request, @request.cookies)
end
def redirect_to_url
@response.redirect_url
end
-
- # Shortcut for <tt>Rack::Test::UploadedFile.new(File.join(ActionController::TestCase.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
- 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 ad1a7f7109..6c5b7af50e 100644
--- a/actionpack/lib/action_dispatch/testing/test_request.rb
+++ b/actionpack/lib/action_dispatch/testing/test_request.rb
@@ -1,15 +1,17 @@
-require 'active_support/core_ext/hash/indifferent_access'
-require 'rack/utils'
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/indifferent_access"
+require "rack/utils"
module ActionDispatch
class TestRequest < Request
- DEFAULT_ENV = Rack::MockRequest.env_for('/',
- 'HTTP_HOST' => 'test.host',
- 'REMOTE_ADDR' => '0.0.0.0',
- 'HTTP_USER_AGENT' => 'Rails Testing',
+ DEFAULT_ENV = Rack::MockRequest.env_for("/",
+ "HTTP_HOST" => "test.host",
+ "REMOTE_ADDR" => "0.0.0.0",
+ "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
@@ -22,23 +24,23 @@ module ActionDispatch
private_class_method :default_env
def request_method=(method)
- @env['REQUEST_METHOD'] = method.to_s.upcase
+ super(method.to_s.upcase)
end
def host=(host)
- @env['HTTP_HOST'] = host
+ set_header("HTTP_HOST", host)
end
def port=(number)
- @env['SERVER_PORT'] = number.to_i
+ set_header("SERVER_PORT", number.to_i)
end
def request_uri=(uri)
- @env['REQUEST_URI'] = uri
+ set_header("REQUEST_URI", uri)
end
def path=(path)
- @env['PATH_INFO'] = path
+ set_header("PATH_INFO", path)
end
def action=(action_name)
@@ -46,24 +48,24 @@ module ActionDispatch
end
def if_modified_since=(last_modified)
- @env['HTTP_IF_MODIFIED_SINCE'] = last_modified
+ set_header("HTTP_IF_MODIFIED_SINCE", last_modified)
end
def if_none_match=(etag)
- @env['HTTP_IF_NONE_MATCH'] = etag
+ set_header("HTTP_IF_NONE_MATCH", etag)
end
def remote_addr=(addr)
- @env['REMOTE_ADDR'] = addr
+ set_header("REMOTE_ADDR", addr)
end
def user_agent=(user_agent)
- @env['HTTP_USER_AGENT'] = user_agent
+ set_header("HTTP_USER_AGENT", user_agent)
end
def accept=(mime_types)
- @env.delete('action_dispatch.request.accepts')
- @env['HTTP_ACCEPT'] = Array(mime_types).collect(&:to_s).join(",")
+ delete_header("action_dispatch.request.accepts")
+ set_header("HTTP_ACCEPT", Array(mime_types).collect(&:to_s).join(","))
end
end
end
diff --git a/actionpack/lib/action_dispatch/testing/test_response.rb b/actionpack/lib/action_dispatch/testing/test_response.rb
index a9b88ac5fd..1e6b21f235 100644
--- a/actionpack/lib/action_dispatch/testing/test_response.rb
+++ b/actionpack/lib/action_dispatch/testing/test_response.rb
@@ -1,3 +1,7 @@
+# frozen_string_literal: true
+
+require "action_dispatch/testing/request_encoder"
+
module ActionDispatch
# Integration test methods such as ActionDispatch::Integration::Session#get
# and ActionDispatch::Integration::Session#post return objects of class
@@ -7,19 +11,43 @@ module ActionDispatch
# See Response for more information on controller response objects.
class TestResponse < Response
def self.from_response(response)
- new response.status, response.headers, response.body, default_headers: nil
+ new response.status, response.headers, response.body
+ end
+
+ def initialize(*) # :nodoc:
+ super
+ @response_parser = RequestEncoder.parser(content_type)
end
# Was the response successful?
- alias_method :success?, :successful?
+ def success?
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ The success? predicate is deprecated and will be removed in Rails 6.0.
+ Please use successful? as provided by Rack::Response::Helpers.
+ MSG
+ successful?
+ end
# Was the URL not found?
- alias_method :missing?, :not_found?
-
- # Were we redirected?
- alias_method :redirect?, :redirection?
+ def missing?
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ The missing? predicate is deprecated and will be removed in Rails 6.0.
+ Please use not_found? as provided by Rack::Response::Helpers.
+ MSG
+ not_found?
+ end
# Was there a server-side error?
- alias_method :error?, :server_error?
+ def error?
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ The error? predicate is deprecated and will be removed in Rails 6.0.
+ Please use server_error? as provided by Rack::Response::Helpers.
+ MSG
+ server_error?
+ end
+
+ def parsed_body
+ @parsed_body ||= @response_parser.call(body)
+ end
end
end
diff --git a/actionpack/lib/action_pack.rb b/actionpack/lib/action_pack.rb
index f664dab620..3f69109633 100644
--- a/actionpack/lib/action_pack.rb
+++ b/actionpack/lib/action_pack.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
#--
-# Copyright (c) 2004-2015 David Heinemeier Hansson
+# Copyright (c) 2004-2018 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -21,4 +23,4 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
-require 'action_pack/version'
+require "action_pack/version"
diff --git a/actionpack/lib/action_pack/gem_version.rb b/actionpack/lib/action_pack/gem_version.rb
index 255ac9f4ed..37969fcb57 100644
--- a/actionpack/lib/action_pack/gem_version.rb
+++ b/actionpack/lib/action_pack/gem_version.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionPack
# Returns the version of the currently loaded Action Pack as a <tt>Gem::Version</tt>
def self.gem_version
@@ -5,7 +7,7 @@ module ActionPack
end
module VERSION
- MAJOR = 5
+ MAJOR = 6
MINOR = 0
TINY = 0
PRE = "alpha"
diff --git a/actionpack/lib/action_pack/version.rb b/actionpack/lib/action_pack/version.rb
index 7088cd2760..fd039fe140 100644
--- a/actionpack/lib/action_pack/version.rb
+++ b/actionpack/lib/action_pack/version.rb
@@ -1,4 +1,6 @@
-require_relative 'gem_version'
+# frozen_string_literal: true
+
+require_relative "gem_version"
module ActionPack
# Returns the version of the currently loaded ActionPack as a <tt>Gem::Version</tt>
diff --git a/actionpack/test/abstract/callbacks_test.rb b/actionpack/test/abstract/callbacks_test.rb
index 07571602e4..4512ea27b3 100644
--- a/actionpack/test/abstract/callbacks_test.rb
+++ b/actionpack/test/abstract/callbacks_test.rb
@@ -1,8 +1,9 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module AbstractController
module Testing
-
class ControllerWithCallbacks < AbstractController::Base
include AbstractController::Callbacks
end
@@ -43,7 +44,7 @@ module AbstractController
def aroundz
@aroundz = "FIRST"
yield
- @aroundz << "SECOND"
+ @aroundz += "SECOND"
end
def index
@@ -114,8 +115,8 @@ module AbstractController
end
class CallbacksWithConditions < ControllerWithCallbacks
- before_action :list, :only => :index
- before_action :authenticate, :except => :index
+ before_action :list, only: :index
+ before_action :authenticate, except: :index
def index
self.response_body = @list.join(", ")
@@ -126,14 +127,14 @@ module AbstractController
end
private
- def list
- @list = ["Hello", "World"]
- end
-
- def authenticate
- @list ||= []
- @authenticated = "true"
- end
+ def list
+ @list = ["Hello", "World"]
+ end
+
+ def authenticate
+ @list ||= []
+ @authenticated = "true"
+ end
end
class TestCallbacksWithConditions < ActiveSupport::TestCase
@@ -153,7 +154,7 @@ module AbstractController
test "when :except is specified, an after action is not triggered on that action" do
@controller.process(:index)
- assert !@controller.instance_variable_defined?("@authenticated")
+ assert_not @controller.instance_variable_defined?("@authenticated")
end
end
@@ -170,14 +171,14 @@ module AbstractController
end
private
- def list
- @list = ["Hello", "World"]
- end
-
- def authenticate
- @list = []
- @authenticated = "true"
- end
+ def list
+ @list = ["Hello", "World"]
+ end
+
+ def authenticate
+ @list = []
+ @authenticated = "true"
+ end
end
class TestCallbacksWithArrayConditions < ActiveSupport::TestCase
@@ -197,12 +198,12 @@ module AbstractController
test "when :except is specified with an array, an after action is not triggered on that action" do
@controller.process(:index)
- assert !@controller.instance_variable_defined?("@authenticated")
+ assert_not @controller.instance_variable_defined?("@authenticated")
end
end
class ChangedConditions < Callback2
- before_action :first, :only => :index
+ before_action :first, only: :index
def not_index
@text ||= nil
@@ -265,53 +266,5 @@ module AbstractController
assert_equal "Hello world Howdy!", controller.response_body
end
end
-
- class AliasedCallbacks < ControllerWithCallbacks
- ActiveSupport::Deprecation.silence do
- before_filter :first
- after_filter :second
- around_filter :aroundz
- end
-
- def first
- @text = "Hello world"
- end
-
- def second
- @second = "Goodbye"
- end
-
- def aroundz
- @aroundz = "FIRST"
- yield
- @aroundz << "SECOND"
- end
-
- def index
- @text ||= nil
- self.response_body = @text.to_s
- end
- end
-
- class TestAliasedCallbacks < ActiveSupport::TestCase
- def setup
- @controller = AliasedCallbacks.new
- end
-
- test "before_filter works" do
- @controller.process(:index)
- assert_equal "Hello world", @controller.response_body
- end
-
- test "after_filter works" do
- @controller.process(:index)
- assert_equal "Goodbye", @controller.instance_variable_get("@second")
- end
-
- test "around_filter works" do
- @controller.process(:index)
- assert_equal "FIRSTSECOND", @controller.instance_variable_get("@aroundz")
- end
- end
end
end
diff --git a/actionpack/test/abstract/collector_test.rb b/actionpack/test/abstract/collector_test.rb
index fc59bf19c4..a4770b66e1 100644
--- a/actionpack/test/abstract/collector_test.rb
+++ b/actionpack/test/abstract/collector_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module AbstractController
module Testing
@@ -53,9 +55,9 @@ module AbstractController
collector.html
collector.text(:foo)
collector.js(:bar) { :baz }
- assert_equal [Mime::HTML, [], nil], collector.responses[0]
- assert_equal [Mime::TEXT, [:foo], nil], collector.responses[1]
- assert_equal [Mime::JS, [:bar]], collector.responses[2][0,2]
+ assert_equal [Mime[:html], [], nil], collector.responses[0]
+ assert_equal [Mime[:text], [:foo], nil], collector.responses[1]
+ assert_equal [Mime[:js], [:bar]], collector.responses[2][0, 2]
assert_equal :baz, collector.responses[2][2].call
end
end
diff --git a/actionpack/test/abstract/translation_test.rb b/actionpack/test/abstract/translation_test.rb
index 8289252dfc..7138044c03 100644
--- a/actionpack/test/abstract/translation_test.rb
+++ b/actionpack/test/abstract/translation_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module AbstractController
module Testing
@@ -9,22 +11,20 @@ module AbstractController
class TranslationControllerTest < ActiveSupport::TestCase
def setup
@controller = TranslationController.new
- I18n.backend.store_translations(:en, {
+ I18n.backend.store_translations(:en,
one: {
- two: 'bar',
+ two: "bar",
},
abstract_controller: {
testing: {
translation: {
index: {
- foo: 'bar',
+ foo: "bar",
},
- no_action: 'no_action_tr',
+ no_action: "no_action_tr",
},
},
- },
- })
- @controller.stubs(action_name: :index)
+ })
end
def test_action_controller_base_responds_to_translate
@@ -44,25 +44,35 @@ module AbstractController
end
def test_lazy_lookup
- assert_equal 'bar', @controller.t('.foo')
+ @controller.stub :action_name, :index do
+ assert_equal "bar", @controller.t(".foo")
+ end
end
def test_lazy_lookup_with_symbol
- assert_equal 'bar', @controller.t(:'.foo')
+ @controller.stub :action_name, :index do
+ assert_equal "bar", @controller.t(:'.foo')
+ end
end
def test_lazy_lookup_fallback
- assert_equal 'no_action_tr', @controller.t(:'.no_action')
+ @controller.stub :action_name, :index do
+ assert_equal "no_action_tr", @controller.t(:'.no_action')
+ end
end
def test_default_translation
- assert_equal 'bar', @controller.t('one.two')
+ @controller.stub :action_name, :index do
+ assert_equal "bar", @controller.t("one.two")
+ assert_equal "baz", @controller.t(".twoz", default: ["baz", :twoz])
+ end
end
def test_localize
- time, expected = Time.gm(2000), 'Sat, 01 Jan 2000 00:00:00 +0000'
- I18n.stubs(:localize).with(time).returns(expected)
- assert_equal expected, @controller.l(time)
+ time, expected = Time.gm(2000), "Sat, 01 Jan 2000 00:00:00 +0000"
+ I18n.stub :localize, expected do
+ assert_equal expected, @controller.l(time)
+ end
end
end
end
diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb
index cc610b6d75..65dd28b3d7 100644
--- a/actionpack/test/abstract_unit.rb
+++ b/actionpack/test/abstract_unit.rb
@@ -1,47 +1,50 @@
-require File.expand_path('../../../load_paths', __FILE__)
+# frozen_string_literal: true
-$:.unshift(File.dirname(__FILE__) + '/lib')
-$:.unshift(File.dirname(__FILE__) + '/fixtures/helpers')
-$:.unshift(File.dirname(__FILE__) + '/fixtures/alternate_helpers')
+$:.unshift File.expand_path("lib", __dir__)
+$:.unshift File.expand_path("fixtures/helpers", __dir__)
+$:.unshift File.expand_path("fixtures/alternate_helpers", __dir__)
-require 'active_support/core_ext/kernel/reporting'
+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'
+require "drb"
begin
- require 'drb/unix'
+ require "drb/unix"
rescue LoadError
puts "'drb/unix' is not available"
end
-require 'tempfile'
-PROCESS_COUNT = (ENV['N'] || 4).to_i
+if ENV["TRAVIS"]
+ PROCESS_COUNT = 0
+else
+ PROCESS_COUNT = (ENV["N"] || 4).to_i
+end
-require 'active_support/testing/autorun'
-require 'abstract_controller'
-require 'abstract_controller/railties/routes_helpers'
-require 'action_controller'
-require 'action_view'
-require 'action_view/testing/resolvers'
-require 'action_dispatch'
-require 'active_support/dependencies'
-require 'active_model'
-require 'active_record'
-require 'action_controller/caching'
+require "active_support/testing/autorun"
+require "abstract_controller"
+require "abstract_controller/railties/routes_helpers"
+require "action_controller"
+require "action_view"
+require "action_view/testing/resolvers"
+require "action_dispatch"
+require "active_support/dependencies"
+require "active_model"
-require 'pp' # require 'pp' early to prevent hidden_methods from not picking up the pretty-print methods until too late
+require "pp" # require 'pp' early to prevent hidden_methods from not picking up the pretty-print methods until too late
module Rails
class << self
def env
@_env ||= ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "test")
end
+
+ def root; end
end
end
@@ -55,14 +58,16 @@ ActiveSupport::Deprecation.debug = true
# Disable available locale checks to avoid warnings running the test suite.
I18n.enforce_available_locales = false
-# Register danish language for testing
-I18n.backend.store_translations 'da', {}
-I18n.backend.store_translations 'pt-BR', {}
-
-FIXTURE_LOAD_PATH = File.join(File.dirname(__FILE__), 'fixtures')
+FIXTURE_LOAD_PATH = File.join(__dir__, "fixtures")
SharedTestRoutes = ActionDispatch::Routing::RouteSet.new
+SharedTestRoutes.draw do
+ ActiveSupport::Deprecation.silence do
+ get ":controller(/:action)"
+ end
+end
+
module ActionDispatch
module SharedRoutes
def before_setup
@@ -70,35 +75,10 @@ module ActionDispatch
super
end
end
-
- # Hold off drawing routes until all the possible controller classes
- # have been loaded.
- module DrawOnce
- class << self
- attr_accessor :drew
- end
- self.drew = false
-
- def before_setup
- super
- return if DrawOnce.drew
-
- SharedTestRoutes.draw do
- get ':controller(/:action)'
- end
-
- ActionDispatch::IntegrationTest.app.routes.draw do
- get ':controller(/:action)'
- end
-
- DrawOnce.drew = true
- end
- end
end
module ActiveSupport
class TestCase
- include ActionDispatch::DrawOnce
if RUBY_ENGINE == "ruby" && PROCESS_COUNT > 0
parallelize_me!
end
@@ -119,44 +99,49 @@ class RoutedRackApp
end
class ActionDispatch::IntegrationTest < ActiveSupport::TestCase
- include ActionDispatch::SharedRoutes
-
def self.build_app(routes = nil)
RoutedRackApp.new(routes || ActionDispatch::Routing::RouteSet.new) do |middleware|
- middleware.use "ActionDispatch::ShowExceptions", ActionDispatch::PublicExceptions.new("#{FIXTURE_LOAD_PATH}/public")
- middleware.use "ActionDispatch::DebugExceptions"
- middleware.use "ActionDispatch::Callbacks"
- middleware.use "ActionDispatch::ParamsParser"
- middleware.use "ActionDispatch::Cookies"
- middleware.use "ActionDispatch::Flash"
- middleware.use "Rack::Head"
+ middleware.use ActionDispatch::ShowExceptions, ActionDispatch::PublicExceptions.new("#{FIXTURE_LOAD_PATH}/public")
+ middleware.use ActionDispatch::DebugExceptions
+ middleware.use ActionDispatch::Callbacks
+ middleware.use ActionDispatch::Cookies
+ middleware.use ActionDispatch::Flash
+ middleware.use Rack::MethodOverride
+ middleware.use Rack::Head
yield(middleware) if block_given?
end
end
self.app = build_app
- # Stub Rails dispatcher so it does not get controller references and
- # simply return the controller#action as Rack::Body.
- class StubDispatcher < ::ActionDispatch::Routing::RouteSet::Dispatcher
- protected
- def controller_reference(controller_param)
- controller_param
+ app.routes.draw do
+ ActiveSupport::Deprecation.silence do
+ get ":controller(/:action)"
+ end
+ end
+
+ class DeadEndRoutes < ActionDispatch::Routing::RouteSet
+ # Stub Rails dispatcher so it does not get controller references and
+ # simply return the controller#action as Rack::Body.
+ class NullController < ::ActionController::Metal
+ def self.dispatch(action, req, res)
+ [200, { "Content-Type" => "text/html" }, ["#{req.params[:controller]}##{action}"]]
+ end
+ end
+
+ class NullControllerRequest < ActionDispatch::Request
+ def controller_class
+ NullController
+ end
end
- def dispatch(controller, action, env)
- [200, {'Content-Type' => 'text/html'}, ["#{controller}##{action}"]]
+ def make_request(env)
+ NullControllerRequest.new env
end
end
- def self.stub_controllers
- old_dispatcher = ActionDispatch::Routing::RouteSet::Dispatcher
- ActionDispatch::Routing::RouteSet.module_eval { remove_const :Dispatcher }
- ActionDispatch::Routing::RouteSet.module_eval { const_set :Dispatcher, StubDispatcher }
- yield ActionDispatch::Routing::RouteSet.new
- ensure
- ActionDispatch::Routing::RouteSet.module_eval { remove_const :Dispatcher }
- ActionDispatch::Routing::RouteSet.module_eval { const_set :Dispatcher, old_dispatcher }
+ def self.stub_controllers(config = ActionDispatch::Routing::RouteSet::DEFAULT_CONFIG)
+ yield DeadEndRoutes.new(config)
end
def with_routing(&block)
@@ -168,12 +153,12 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase
yield temporary_routes
ensure
self.class.app = old_app
- self.remove!
+ remove!
silence_warnings { Object.const_set(:SharedTestRoutes, old_routes) }
end
def with_autoload_path(path)
- path = File.join(File.dirname(__FILE__), "fixtures", path)
+ path = File.join(__dir__, "fixtures", path)
if ActiveSupport::Dependencies.autoload_paths.include?(path)
yield
else
@@ -181,7 +166,7 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase
ActiveSupport::Dependencies.autoload_paths << path
yield
ensure
- ActiveSupport::Dependencies.autoload_paths.reject! {|p| p == path}
+ ActiveSupport::Dependencies.autoload_paths.reject! { |p| p == path }
ActiveSupport::Dependencies.clear
end
end
@@ -192,7 +177,7 @@ end
class Rack::TestCase < ActionDispatch::IntegrationTest
def self.testing(klass = nil)
if klass
- @testing = "/#{klass.name.underscore}".sub!(/_controller$/, '')
+ @testing = "/#{klass.name.underscore}".sub(/_controller$/, "")
else
@testing
end
@@ -247,6 +232,7 @@ module ActionController
routes = ActionDispatch::Routing::RouteSet.new
routes.draw(&block)
include routes.url_helpers
+ routes
end
end
@@ -256,36 +242,17 @@ module ActionController
end
end
-
class ::ApplicationController < ActionController::Base
end
-class Workshop
- extend ActiveModel::Naming
- include ActiveModel::Conversion
- attr_accessor :id
-
- def initialize(id)
- @id = id
- end
-
- def persisted?
- id.present?
- end
-
- def to_s
- id.to_s
- end
-end
-
module ActionDispatch
class DebugExceptions
private
- remove_method :stderr_logger
- # Silence logger
- def stderr_logger
- nil
- end
+ remove_method :stderr_logger
+ # Silence logger
+ def stderr_logger
+ nil
+ end
end
end
@@ -295,16 +262,16 @@ 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
def request_path_params(path, options = {})
- method = options[:method] || 'GET'
- resp = send_request URI('http://localhost' + path), method.to_s.upcase, nil
+ method = options[:method] || "GET"
+ resp = send_request URI("http://localhost" + path), method.to_s.upcase, nil
status = resp.first
if status == 404
raise ActionController::RoutingError, "No route matches #{path.inspect}"
@@ -313,23 +280,23 @@ module ActionDispatch
end
def get(uri_or_host, path = nil)
- send_request(uri_or_host, 'GET', path)[2].join
+ send_request(uri_or_host, "GET", path)[2].join
end
def post(uri_or_host, path = nil)
- send_request(uri_or_host, 'POST', path)[2].join
+ send_request(uri_or_host, "POST", path)[2].join
end
def put(uri_or_host, path = nil)
- send_request(uri_or_host, 'PUT', path)[2].join
+ send_request(uri_or_host, "PUT", path)[2].join
end
def delete(uri_or_host, path = nil)
- send_request(uri_or_host, 'DELETE', path)[2].join
+ send_request(uri_or_host, "DELETE", path)[2].join
end
def patch(uri_or_host, path = nil)
- send_request(uri_or_host, 'PATCH', path)[2].join
+ send_request(uri_or_host, "PATCH", path)[2].join
end
end
end
@@ -337,7 +304,7 @@ end
module RoutingTestHelpers
def url_for(set, options)
route_name = options.delete :use_route
- set.url_for options.merge(:only_path => true), route_name
+ set.url_for options.merge(only_path: true), route_name
end
def make_set(strict = true)
@@ -346,40 +313,38 @@ module RoutingTestHelpers
end
class TestSet < ActionDispatch::Routing::RouteSet
- attr_reader :strict
-
- def initialize(block, strict = false)
- @block = block
- @strict = strict
- super()
- end
-
- class Dispatcher < ActionDispatch::Routing::RouteSet::Dispatcher
- def initialize(defaults, set, block)
- super(defaults)
+ class Request < DelegateClass(ActionDispatch::Request)
+ def initialize(target, helpers, block, strict)
+ super(target)
+ @helpers = helpers
@block = block
- @set = set
- end
-
- def controller(params, default_controller=true)
- super(params, @set.strict)
+ @strict = strict
end
- def controller_reference(controller_param)
+ def controller_class
+ helpers = @helpers
block = @block
- set = @set
- super if @set.strict
- Class.new(ActionController::Base) {
- include set.url_helpers
+ Class.new(@strict ? super : ActionController::Base) {
+ include helpers
define_method(:process) { |name| block.call(self) }
def to_a; [200, {}, []]; end
}
end
end
- def dispatcher defaults
- TestSet::Dispatcher.new defaults, self, @block
+ attr_reader :strict
+
+ def initialize(block, strict = false)
+ @block = block
+ @strict = strict
+ super()
end
+
+ private
+
+ def make_request(env)
+ Request.new super, url_helpers, @block, strict
+ end
end
end
@@ -388,38 +353,11 @@ class ResourcesController < ActionController::Base
alias_method :show, :index
end
-class ThreadsController < ResourcesController; end
-class MessagesController < ResourcesController; end
class CommentsController < ResourcesController; end
-class ReviewsController < ResourcesController; end
-class LogosController < ResourcesController; end
-
-class AccountsController < ResourcesController; end
-class AdminController < ResourcesController; end
-class ProductsController < ResourcesController; end
+class AccountsController < ResourcesController; end
class ImagesController < ResourcesController; end
-class PreferencesController < ResourcesController; end
-
-module Backoffice
- class ProductsController < ResourcesController; end
- class ImagesController < ResourcesController; end
-
- module Admin
- class ProductsController < ResourcesController; end
- class ImagesController < ResourcesController; end
- 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
-require 'mocha/setup' # FIXME: stop using mocha
+require "active_support/testing/method_call_assertions"
class ForkingExecutor
class Server
@@ -429,27 +367,25 @@ class ForkingExecutor
@queue = Queue.new
end
- def record reporter, result
+ def record(reporter, result)
reporter.record result
end
- def << o
+ def <<(o)
o[2] = DRbObject.new(o[2]) if o
@queue << o
end
def pop; @queue.pop; end
end
- def initialize size
+ def initialize(size)
@size = size
@queue = Server.new
- file = File.join Dir.tmpdir, Dir::Tmpname.make_tmpname('rails-tests', 'fd')
- @url = "drbunix://#{file}"
@pool = nil
- DRb.start_service @url, @queue
+ @url = DRb.start_service("drbunix:", @queue).uri
end
- def << work; @queue << work; end
+ def <<(work); @queue << work; end
def shutdown
pool = @size.times.map {
@@ -473,21 +409,52 @@ class ForkingExecutor
end
private
- def translate_exceptions(result)
- result.failures.map! { |e|
- begin
- Marshal.dump e
- e
- rescue TypeError
- ex = Exception.new e.message
- ex.set_backtrace e.backtrace
- Minitest::UnexpectedError.new ex
- end
- }
- end
+ def translate_exceptions(result)
+ result.failures.map! { |e|
+ begin
+ Marshal.dump e
+ e
+ rescue TypeError
+ ex = Exception.new e.message
+ ex.set_backtrace e.backtrace
+ Minitest::UnexpectedError.new ex
+ end
+ }
+ end
end
if RUBY_ENGINE == "ruby" && PROCESS_COUNT > 0
# Use N processes (N defaults to 4)
Minitest.parallel_executor = ForkingExecutor.new(PROCESS_COUNT)
end
+
+class ActiveSupport::TestCase
+ include ActiveSupport::Testing::MethodCallAssertions
+
+ private
+ # 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
+end
+
+class DrivenByRackTest < ActionDispatch::SystemTestCase
+ driven_by :rack_test
+end
+
+class DrivenBySeleniumWithChrome < ActionDispatch::SystemTestCase
+ driven_by :selenium, using: :chrome
+end
+
+class DrivenBySeleniumWithHeadlessChrome < ActionDispatch::SystemTestCase
+ driven_by :selenium, using: :headless_chrome
+end
+
+class DrivenBySeleniumWithHeadlessFirefox < ActionDispatch::SystemTestCase
+ driven_by :selenium, using: :headless_firefox
+end
diff --git a/actionpack/test/assertions/response_assertions_test.rb b/actionpack/test/assertions/response_assertions_test.rb
index 5e64cae7e2..261579dce5 100644
--- a/actionpack/test/assertions/response_assertions_test.rb
+++ b/actionpack/test/assertions/response_assertions_test.rb
@@ -1,22 +1,35 @@
-require 'abstract_unit'
-require 'action_dispatch/testing/assertions/response'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "action_dispatch/testing/assertions/response"
module ActionDispatch
module Assertions
class ResponseAssertionsTest < ActiveSupport::TestCase
include ResponseAssertions
- FakeResponse = Struct.new(:response_code) do
- [:success, :missing, :redirect, :error].each do |sym|
+ FakeResponse = Struct.new(:response_code, :location, :body) do
+ def initialize(*)
+ super
+ self.location ||= "http://test.example.com/posts"
+ self.body ||= ""
+ end
+
+ [:successful, :not_found, :redirection, :server_error].each do |sym|
define_method("#{sym}?") do
sym == response_code
end
end
end
+ def setup
+ @controller = nil
+ @request = nil
+ end
+
def test_assert_response_predicate_methods
[:success, :missing, :redirect, :error].each do |sym|
- @response = FakeResponse.new sym
+ @response = FakeResponse.new RESPONSE_PREDICATES[sym].to_s.sub(/\?/, "").to_sym
assert_response sym
assert_raises(Minitest::Assertion) {
@@ -25,7 +38,7 @@ module ActionDispatch
end
end
- def test_assert_response_fixnum
+ def test_assert_response_integer
@response = FakeResponse.new 400
assert_response 400
@@ -58,6 +71,71 @@ module ActionDispatch
assert_response :succezz
}
end
+
+ def test_error_message_shows_404_when_404_asserted_for_success
+ @response = ActionDispatch::Response.new
+ @response.status = 404
+
+ error = assert_raises(Minitest::Assertion) { assert_response :success }
+ expected = "Expected response to be a <2XX: success>,"\
+ " but was a <404: Not Found>"
+ assert_match expected, error.message
+ end
+
+ def test_error_message_shows_404_when_asserted_for_200
+ @response = ActionDispatch::Response.new
+ @response.status = 404
+
+ error = assert_raises(Minitest::Assertion) { assert_response 200 }
+ expected = "Expected response to be a <200: OK>,"\
+ " but was a <404: Not Found>"
+ assert_match expected, error.message
+ end
+
+ def test_error_message_shows_302_redirect_when_302_asserted_for_success
+ @response = ActionDispatch::Response.new
+ @response.status = 302
+ @response.location = "http://test.host/posts/redirect/1"
+
+ error = assert_raises(Minitest::Assertion) { assert_response :success }
+ expected = "Expected response to be a <2XX: success>,"\
+ " but was a <302: Found>" \
+ " redirect to <http://test.host/posts/redirect/1>"
+ assert_match expected, error.message
+ end
+
+ def test_error_message_shows_302_redirect_when_302_asserted_for_301
+ @response = ActionDispatch::Response.new
+ @response.status = 302
+ @response.location = "http://test.host/posts/redirect/2"
+
+ error = assert_raises(Minitest::Assertion) { assert_response 301 }
+ expected = "Expected response to be a <301: Moved Permanently>,"\
+ " but was a <302: Found>" \
+ " redirect to <http://test.host/posts/redirect/2>"
+ assert_match expected, error.message
+ end
+
+ def test_error_message_shows_short_response_body
+ @response = ActionDispatch::Response.new
+ @response.status = 400
+ @response.body = "not too long"
+ error = assert_raises(Minitest::Assertion) { assert_response 200 }
+ expected = "Expected response to be a <200: OK>,"\
+ " but was a <400: Bad Request>" \
+ "\nResponse body: not too long"
+ assert_match expected, error.message
+ end
+
+ def test_error_message_does_not_show_long_response_body
+ @response = ActionDispatch::Response.new
+ @response.status = 400
+ @response.body = "not too long" * 50
+ error = assert_raises(Minitest::Assertion) { assert_response 200 }
+ expected = "Expected response to be a <200: OK>,"\
+ " but was a <400: Bad Request>"
+ assert_match expected, error.message
+ end
end
end
end
diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb
index 5b85e83045..763df3a776 100644
--- a/actionpack/test/controller/action_pack_assertions_test.rb
+++ b/actionpack/test/controller/action_pack_assertions_test.rb
@@ -1,33 +1,34 @@
-require 'abstract_unit'
-require 'controller/fake_controllers'
+# frozen_string_literal: true
-class ActionPackAssertionsController < ActionController::Base
+require "abstract_unit"
+require "controller/fake_controllers"
+class ActionPackAssertionsController < ActionController::Base
def nothing() head :ok end
- def hello_xml_world() render :template => "test/hello_xml_world"; end
+ def hello_xml_world() render template: "test/hello_xml_world"; end
def hello_xml_world_pdf
self.content_type = "application/pdf"
- render :template => "test/hello_xml_world"
+ render template: "test/hello_xml_world"
end
def hello_xml_world_pdf_header
response.headers["Content-Type"] = "application/pdf; charset=utf-8"
- render :template => "test/hello_xml_world"
+ render template: "test/hello_xml_world"
end
def redirect_internal() redirect_to "/nothing"; end
- def redirect_to_action() redirect_to :action => "flash_me", :id => 1, :params => { "panda" => "fun" }; end
+ def redirect_to_action() redirect_to action: "flash_me", id: 1, params: { "panda" => "fun" }; end
- def redirect_to_controller() redirect_to :controller => "elsewhere", :action => "flash_me"; end
+ def redirect_to_controller() redirect_to controller: "elsewhere", action: "flash_me"; end
- def redirect_to_controller_with_symbol() redirect_to :controller => :elsewhere, :action => :flash_me; end
+ def redirect_to_controller_with_symbol() redirect_to controller: :elsewhere, action: :flash_me; end
- def redirect_to_path() redirect_to '/some/path' end
+ def redirect_to_path() redirect_to "/some/path" end
- def redirect_invalid_external_route() redirect_to 'ht_tp://www.rubyonrails.org' end
+ def redirect_invalid_external_route() redirect_to "ht_tp://www.rubyonrails.org" end
def redirect_to_named_route() redirect_to route_one_url end
@@ -35,60 +36,60 @@ class ActionPackAssertionsController < ActionController::Base
def redirect_external_protocol_relative() redirect_to "//www.rubyonrails.org"; end
- def response404() head '404 AWOL' end
+ def response404() head "404 AWOL" end
- def response500() head '500 Sorry' end
+ def response500() head "500 Sorry" end
- def response599() head '599 Whoah!' end
+ def response599() head "599 Whoah!" end
def flash_me
- flash['hello'] = 'my name is inigo montoya...'
- render :text => "Inconceivable!"
+ flash["hello"] = "my name is inigo montoya..."
+ render plain: "Inconceivable!"
end
def flash_me_naked
flash.clear
- render :text => "wow!"
+ render plain: "wow!"
end
def assign_this
@howdy = "ho"
- render :inline => "Mr. Henke"
+ render inline: "Mr. Henke"
end
def render_based_on_parameters
- render :text => "Mr. #{params[:name]}"
+ render plain: "Mr. #{params[:name]}"
end
def render_url
- render :text => "<div>#{url_for(:action => 'flash_me', :only_path => true)}</div>"
+ render html: "<div>#{url_for(action: 'flash_me', only_path: true)}</div>"
end
def render_text_with_custom_content_type
- render :text => "Hello!", :content_type => Mime::RSS
+ render body: "Hello!", content_type: Mime[:rss]
end
def session_stuffing
- session['xmas'] = 'turkey'
- render :text => "ho ho ho"
+ session["xmas"] = "turkey"
+ render plain: "ho ho ho"
end
def raise_exception_on_get
raise "get" if request.get?
- render :text => "request method: #{request.env['REQUEST_METHOD']}"
+ render plain: "request method: #{request.env['REQUEST_METHOD']}"
end
def raise_exception_on_post
raise "post" if request.post?
- render :text => "request method: #{request.env['REQUEST_METHOD']}"
+ render plain: "request method: #{request.env['REQUEST_METHOD']}"
end
def render_file_absolute_path
- render :file => File.expand_path('../../../README.rdoc', __FILE__)
+ render file: File.expand_path("../../README.rdoc", __dir__)
end
def render_file_relative_path
- render :file => 'README.rdoc'
+ render file: "README.rdoc"
end
end
@@ -97,11 +98,11 @@ end
# is expecting something other than an error.
class AssertResponseWithUnexpectedErrorController < ActionController::Base
def index
- raise 'FAIL'
+ raise "FAIL"
end
def show
- render :text => "Boom", :status => 500
+ render plain: "Boom", status: 500
end
end
@@ -116,21 +117,30 @@ module Admin
end
def redirect_to_absolute_controller
- redirect_to :controller => '/content'
+ redirect_to controller: "/content"
end
def redirect_to_fellow_controller
- redirect_to :controller => 'user'
+ redirect_to controller: "user"
end
def redirect_to_top_level_named_route
- redirect_to top_level_url(:id => "foo")
+ redirect_to top_level_url(id: "foo")
end
end
end
-class ActionPackAssertionsControllerTest < ActionController::TestCase
+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
assert_match(/\A= Action Pack/, @response.body)
@@ -144,50 +154,67 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase
def test_get_request
assert_raise(RuntimeError) { get :raise_exception_on_get }
get :raise_exception_on_post
- assert_equal 'request method: GET', @response.body
+ assert_equal "request method: GET", @response.body
end
def test_post_request
assert_raise(RuntimeError) { post :raise_exception_on_post }
post :raise_exception_on_get
- assert_equal 'request method: POST', @response.body
+ assert_equal "request method: POST", @response.body
end
def test_get_post_request_switch
post :raise_exception_on_get
- assert_equal 'request method: POST', @response.body
+ assert_equal "request method: POST", @response.body
get :raise_exception_on_post
- assert_equal 'request method: GET', @response.body
+ assert_equal "request method: GET", @response.body
post :raise_exception_on_get
- assert_equal 'request method: POST', @response.body
+ assert_equal "request method: POST", @response.body
get :raise_exception_on_post
- assert_equal 'request method: GET', @response.body
+ assert_equal "request method: GET", @response.body
end
def test_string_constraint
with_routing do |set|
set.draw do
- get "photos", :to => 'action_pack_assertions#nothing', :constraints => {:subdomain => "admin"}
+ get "photos", to: "action_pack_assertions#nothing", constraints: { subdomain: "admin" }
+ end
+ 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
- get 'route_one', :to => 'action_pack_assertions#nothing', :as => :route_one
- get 'route_two', :to => 'action_pack_assertions#nothing', :id => 'two', :as => :route_two
- get ':controller/:action'
+ get "route_one", to: "action_pack_assertions#nothing", as: :route_one
+ get "route_two", to: "action_pack_assertions#nothing", id: "two", as: :route_two
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action"
+ end
end
process :redirect_to_named_route
assert_raise(ActiveSupport::TestCase::Assertion) do
- assert_redirected_to 'http://test.host/route_two'
+ assert_redirected_to "http://test.host/route_two"
end
assert_raise(ActiveSupport::TestCase::Assertion) do
assert_redirected_to %r(^http://test.host/route_two)
end
assert_raise(ActiveSupport::TestCase::Assertion) do
- assert_redirected_to :controller => 'action_pack_assertions', :action => 'nothing', :id => 'two'
+ assert_redirected_to controller: "action_pack_assertions", action: "nothing", id: "two"
end
assert_raise(ActiveSupport::TestCase::Assertion) do
assert_redirected_to route_two_url
@@ -200,8 +227,11 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase
with_routing do |set|
set.draw do
- get 'admin/inner_module', :to => 'admin/inner_module#index', :as => :admin_inner_module
- get ':controller/:action'
+ get "admin/inner_module", to: "admin/inner_module#index", as: :admin_inner_module
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action"
+ end
end
process :redirect_to_index
# redirection is <{"action"=>"index", "controller"=>"admin/admin/inner_module"}>
@@ -214,8 +244,11 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase
with_routing do |set|
set.draw do
- get '/action_pack_assertions/:id', :to => 'action_pack_assertions#index', :as => :top_level
- get ':controller/:action'
+ get "/action_pack_assertions/:id", to: "action_pack_assertions#index", as: :top_level
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action"
+ end
end
process :redirect_to_top_level_named_route
# assert_redirected_to "http://test.host/action_pack_assertions/foo" would pass because of exact match early return
@@ -230,12 +263,15 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase
with_routing do |set|
set.draw do
# this controller exists in the admin namespace as well which is the only difference from previous test
- get '/user/:id', :to => 'user#index', :as => :top_level
- get ':controller/:action'
+ get "/user/:id", to: "user#index", as: :top_level
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action"
+ end
end
process :redirect_to_top_level_named_route
# assert_redirected_to top_level_url('foo') would pass because of exact match early return
- assert_redirected_to top_level_path('foo')
+ assert_redirected_to top_level_path("foo")
end
end
@@ -247,57 +283,57 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase
assert_no_match(
/#{request.protocol}#{request.host}\/\/www.rubyonrails.org/,
ex.message,
- 'protocol relative url was incorrectly normalized'
+ "protocol relative url was incorrectly normalized"
)
end
end
def test_template_objects_exist
process :assign_this
- assert !@controller.instance_variable_defined?(:"@hi")
+ assert_not @controller.instance_variable_defined?(:"@hi")
assert @controller.instance_variable_get(:"@howdy")
end
def test_template_objects_missing
process :nothing
- assert !@controller.instance_variable_defined?(:@howdy)
+ assert_not @controller.instance_variable_defined?(:@howdy)
end
def test_empty_flash
process :flash_me_naked
- assert flash.empty?
+ assert_empty flash
end
def test_flash_exist
process :flash_me
- assert flash.any?
- assert flash['hello'].present?
+ assert_predicate flash, :any?
+ assert_predicate flash["hello"], :present?
end
def test_flash_does_not_exist
process :nothing
- assert flash.empty?
+ assert_empty flash
end
def test_session_exist
process :session_stuffing
- assert_equal 'turkey', session['xmas']
+ assert_equal "turkey", session["xmas"]
end
def session_does_not_exist
process :nothing
- assert session.empty?
+ assert_empty session
end
def test_redirection_location
process :redirect_internal
- assert_equal 'http://test.host/nothing', @response.redirect_url
+ assert_equal "http://test.host/nothing", @response.redirect_url
process :redirect_external
- assert_equal 'http://www.rubyonrails.org', @response.redirect_url
+ assert_equal "http://www.rubyonrails.org", @response.redirect_url
process :redirect_external_protocol_relative
- assert_equal '//www.rubyonrails.org', @response.redirect_url
+ assert_equal "//www.rubyonrails.org", @response.redirect_url
end
def test_no_redirect_url
@@ -307,51 +343,51 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase
def test_server_error_response_code
process :response500
- assert @response.server_error?
+ assert_predicate @response, :server_error?
process :response599
- assert @response.server_error?
+ assert_predicate @response, :server_error?
process :response404
- assert !@response.server_error?
+ assert_not_predicate @response, :server_error?
end
def test_missing_response_code
process :response404
- assert @response.missing?
+ assert_predicate @response, :not_found?
end
def test_client_error_response_code
process :response404
- assert @response.client_error?
+ assert_predicate @response, :client_error?
end
def test_redirect_url_match
process :redirect_external
- assert @response.redirect?
+ assert_predicate @response, :redirect?
assert_match(/rubyonrails/, @response.redirect_url)
- assert !/perloffrails/.match(@response.redirect_url)
+ assert_no_match(/perloffrails/, @response.redirect_url)
end
def test_redirection
process :redirect_internal
- assert @response.redirect?
+ assert_predicate @response, :redirect?
process :redirect_external
- assert @response.redirect?
+ assert_predicate @response, :redirect?
process :nothing
- assert !@response.redirect?
+ assert_not_predicate @response, :redirect?
end
def test_successful_response_code
process :nothing
- assert @response.success?
+ assert_predicate @response, :successful?
end
def test_response_object
process :nothing
- assert_kind_of ActionController::TestResponse, @response
+ assert_kind_of ActionDispatch::TestResponse, @response
end
def test_render_based_on_parameters
@@ -364,24 +400,24 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase
def test_assert_redirection_fails_with_incorrect_controller
process :redirect_to_controller
assert_raise(ActiveSupport::TestCase::Assertion) do
- assert_redirected_to :controller => "action_pack_assertions", :action => "flash_me"
+ assert_redirected_to controller: "action_pack_assertions", action: "flash_me"
end
end
def test_assert_redirection_with_extra_controller_option
get :redirect_to_action
- assert_redirected_to :controller => 'action_pack_assertions', :action => "flash_me", :id => 1, :params => { :panda => 'fun' }
+ assert_redirected_to controller: "action_pack_assertions", action: "flash_me", id: 1, params: { panda: "fun" }
end
def test_redirected_to_url_leading_slash
process :redirect_to_path
- assert_redirected_to '/some/path'
+ assert_redirected_to "/some/path"
end
def test_redirected_to_url_no_leading_slash_fails
process :redirect_to_path
assert_raise ActiveSupport::TestCase::Assertion do
- assert_redirected_to 'some/path'
+ assert_redirected_to "some/path"
end
end
@@ -392,43 +428,43 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase
def test_redirected_to_url_full_url
process :redirect_to_path
- assert_redirected_to 'http://test.host/some/path'
+ assert_redirected_to "http://test.host/some/path"
end
def test_assert_redirection_with_symbol
process :redirect_to_controller_with_symbol
assert_nothing_raised {
- assert_redirected_to :controller => "elsewhere", :action => "flash_me"
+ assert_redirected_to controller: "elsewhere", action: "flash_me"
}
process :redirect_to_controller_with_symbol
assert_nothing_raised {
- assert_redirected_to :controller => :elsewhere, :action => :flash_me
+ assert_redirected_to controller: :elsewhere, action: :flash_me
}
end
def test_redirected_to_with_nested_controller
@controller = Admin::InnerModuleController.new
get :redirect_to_absolute_controller
- assert_redirected_to :controller => '/content'
+ assert_redirected_to controller: "/content"
get :redirect_to_fellow_controller
- assert_redirected_to :controller => 'admin/user'
+ assert_redirected_to controller: "admin/user"
end
def test_assert_response_uses_exception_message
@controller = AssertResponseWithUnexpectedErrorController.new
- e = assert_raise RuntimeError, 'Expected non-success response' do
+ e = assert_raise RuntimeError, "Expected non-success response" do
get :index
end
assert_response :success
- assert_includes 'FAIL', e.message
+ assert_includes "FAIL", e.message
end
def test_assert_response_failure_response_with_no_exception
@controller = AssertResponseWithUnexpectedErrorController.new
get :show
assert_response 500
- assert_equal 'Boom', response.body
+ assert_equal "Boom", response.body
end
end
@@ -437,21 +473,21 @@ class ActionPackHeaderTest < ActionController::TestCase
def test_rendering_xml_sets_content_type
process :hello_xml_world
- assert_equal('application/xml; charset=utf-8', @response.headers['Content-Type'])
+ assert_equal("application/xml; charset=utf-8", @response.headers["Content-Type"])
end
def test_rendering_xml_respects_content_type
process :hello_xml_world_pdf
- assert_equal('application/pdf; charset=utf-8', @response.headers['Content-Type'])
+ assert_equal("application/pdf; charset=utf-8", @response.headers["Content-Type"])
end
def test_rendering_xml_respects_content_type_when_set_in_the_header
process :hello_xml_world_pdf_header
- assert_equal('application/pdf; charset=utf-8', @response.headers['Content-Type'])
+ assert_equal("application/pdf; charset=utf-8", @response.headers["Content-Type"])
end
def test_render_text_with_custom_content_type
get :render_text_with_custom_content_type
- assert_equal 'application/rss+xml; charset=utf-8', @response.headers['Content-Type']
+ assert_equal "application/rss+xml; charset=utf-8", @response.headers["Content-Type"]
end
end
diff --git a/actionpack/test/controller/api/conditional_get_test.rb b/actionpack/test/controller/api/conditional_get_test.rb
index d1eb27bf24..e366ce9532 100644
--- a/actionpack/test/controller/api/conditional_get_test.rb
+++ b/actionpack/test/controller/api/conditional_get_test.rb
@@ -1,25 +1,27 @@
-require 'abstract_unit'
-require 'active_support/core_ext/integer/time'
-require 'active_support/core_ext/numeric/time'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/integer/time"
+require "active_support/core_ext/numeric/time"
class ConditionalGetApiController < ActionController::API
before_action :handle_last_modified_and_etags, only: :two
def one
if stale?(last_modified: Time.now.utc.beginning_of_day, etag: [:foo, 123])
- render text: "Hi!"
+ render plain: "Hi!"
end
end
def two
- render text: "Hi!"
+ render plain: "Hi!"
end
private
- def handle_last_modified_and_etags
- fresh_when(last_modified: Time.now.utc.beginning_of_day, etag: [ :foo, 123 ])
- end
+ def handle_last_modified_and_etags
+ fresh_when(last_modified: Time.now.utc.beginning_of_day, etag: [ :foo, 123 ])
+ end
end
class ConditionalGetApiTest < ActionController::TestCase
@@ -31,7 +33,7 @@ class ConditionalGetApiTest < ActionController::TestCase
def test_request_gets_last_modified
get :two
- assert_equal @last_modified, @response.headers['Last-Modified']
+ assert_equal @last_modified, @response.headers["Last-Modified"]
assert_response :success
end
@@ -51,7 +53,7 @@ class ConditionalGetApiTest < ActionController::TestCase
@request.if_modified_since = @last_modified
get :one
assert_equal 304, @response.status.to_i
- assert @response.body.blank?
- assert_equal @last_modified, @response.headers['Last-Modified']
+ assert_predicate @response.body, :blank?
+ assert_equal @last_modified, @response.headers["Last-Modified"]
end
end
diff --git a/actionpack/test/controller/api/data_streaming_test.rb b/actionpack/test/controller/api/data_streaming_test.rb
index 0e7d97d1f4..6446ff9e40 100644
--- a/actionpack/test/controller/api/data_streaming_test.rb
+++ b/actionpack/test/controller/api/data_streaming_test.rb
@@ -1,8 +1,10 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module TestApiFileUtils
- def file_path() File.expand_path(__FILE__) end
- def file_data() @data ||= File.open(file_path, 'rb') { |f| f.read } end
+ def file_path() __FILE__ end
+ def file_data() @data ||= File.open(file_path, "rb") { |f| f.read } end
end
class DataStreamingApiController < ActionController::API
@@ -19,7 +21,7 @@ class DataStreamingApiTest < ActionController::TestCase
tests DataStreamingApiController
def test_data
- response = process('two')
+ response = process("two")
assert_kind_of String, response.body
assert_equal file_data, response.body
end
diff --git a/actionpack/test/controller/api/force_ssl_test.rb b/actionpack/test/controller/api/force_ssl_test.rb
index 8578340d82..8191578eb0 100644
--- a/actionpack/test/controller/api/force_ssl_test.rb
+++ b/actionpack/test/controller/api/force_ssl_test.rb
@@ -1,7 +1,11 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class ForceSSLApiController < ActionController::API
- force_ssl
+ ActiveSupport::Deprecation.silence do
+ force_ssl
+ end
def one; end
def two
diff --git a/actionpack/test/controller/api/implicit_render_test.rb b/actionpack/test/controller/api/implicit_render_test.rb
index 26f9cd8f78..288fb333b0 100644
--- a/actionpack/test/controller/api/implicit_render_test.rb
+++ b/actionpack/test/controller/api/implicit_render_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class ImplicitRenderAPITestController < ActionController::API
def empty_action
diff --git a/actionpack/test/controller/api/params_wrapper_test.rb b/actionpack/test/controller/api/params_wrapper_test.rb
index e40a39d829..814c24bfd8 100644
--- a/actionpack/test/controller/api/params_wrapper_test.rb
+++ b/actionpack/test/controller/api/params_wrapper_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class ParamsWrapperForApiTest < ActionController::TestCase
class UsersController < ActionController::API
@@ -7,7 +9,7 @@ class ParamsWrapperForApiTest < ActionController::TestCase
wrap_parameters :person, format: [:json]
def test
- self.last_parameters = params.except(:controller, :action)
+ self.last_parameters = params.except(:controller, :action).to_unsafe_h
head :ok
end
end
@@ -17,10 +19,10 @@ class ParamsWrapperForApiTest < ActionController::TestCase
tests UsersController
def test_specify_wrapper_name
- @request.env['CONTENT_TYPE'] = 'application/json'
- post :test, params: { 'username' => 'sikachu' }
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :test, params: { "username" => "sikachu" }
- expected = { 'username' => 'sikachu', 'person' => { 'username' => 'sikachu' }}
+ expected = { "username" => "sikachu", "person" => { "username" => "sikachu" } }
assert_equal expected, @controller.last_parameters
end
end
diff --git a/actionpack/test/controller/api/redirect_to_test.rb b/actionpack/test/controller/api/redirect_to_test.rb
index 18877c4b3a..f8230dd6a9 100644
--- a/actionpack/test/controller/api/redirect_to_test.rb
+++ b/actionpack/test/controller/api/redirect_to_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class RedirectToApiController < ActionController::API
def one
diff --git a/actionpack/test/controller/api/renderers_test.rb b/actionpack/test/controller/api/renderers_test.rb
index 9405538833..e7a9a4b2da 100644
--- a/actionpack/test/controller/api/renderers_test.rb
+++ b/actionpack/test/controller/api/renderers_test.rb
@@ -1,14 +1,16 @@
-require 'abstract_unit'
-require 'active_support/core_ext/hash/conversions'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/hash/conversions"
class RenderersApiController < ActionController::API
class Model
def to_json(options = {})
- { a: 'b' }.to_json(options)
+ { a: "b" }.to_json(options)
end
def to_xml(options = {})
- { a: 'b' }.to_xml(options)
+ { a: "b" }.to_xml(options)
end
end
@@ -19,6 +21,10 @@ class RenderersApiController < ActionController::API
def two
render xml: Model.new
end
+
+ def plain
+ render plain: "Hi from plain", status: 500
+ end
end
class RenderersApiTest < ActionController::TestCase
@@ -27,12 +33,18 @@ class RenderersApiTest < ActionController::TestCase
def test_render_json
get :one
assert_response :success
- assert_equal({ a: 'b' }.to_json, @response.body)
+ assert_equal({ a: "b" }.to_json, @response.body)
end
def test_render_xml
get :two
assert_response :success
- assert_equal({ a: 'b' }.to_xml, @response.body)
+ assert_equal({ a: "b" }.to_xml, @response.body)
+ end
+
+ def test_render_plain
+ get :plain
+ assert_response :internal_server_error
+ assert_equal("Hi from plain", @response.body)
end
end
diff --git a/actionpack/test/controller/api/url_for_test.rb b/actionpack/test/controller/api/url_for_test.rb
index 0d8691a091..aa3428bc85 100644
--- a/actionpack/test/controller/api/url_for_test.rb
+++ b/actionpack/test/controller/api/url_for_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class UrlForApiController < ActionController::API
def one; end
@@ -10,7 +12,7 @@ class UrlForApiTest < ActionController::TestCase
def setup
super
- @request.host = 'www.example.com'
+ @request.host = "www.example.com"
end
def test_url_for
diff --git a/actionpack/test/controller/api/with_cookies_test.rb b/actionpack/test/controller/api/with_cookies_test.rb
new file mode 100644
index 0000000000..1a6e12a4f3
--- /dev/null
+++ b/actionpack/test/controller/api/with_cookies_test.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class WithCookiesController < ActionController::API
+ include ActionController::Cookies
+
+ def with_cookies
+ render plain: cookies[:foobar]
+ end
+end
+
+class WithCookiesTest < ActionController::TestCase
+ tests WithCookiesController
+
+ def test_with_cookies
+ request.cookies[:foobar] = "bazbang"
+
+ get :with_cookies
+
+ assert_equal "bazbang", response.body
+ end
+end
diff --git a/actionpack/test/controller/api/with_helpers_test.rb b/actionpack/test/controller/api/with_helpers_test.rb
new file mode 100644
index 0000000000..00179d3505
--- /dev/null
+++ b/actionpack/test/controller/api/with_helpers_test.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+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 SubclassWithHelpersController < WithHelpersController
+ 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
+
+class SubclassWithHelpersTest < ActionController::TestCase
+ tests WithHelpersController
+
+ def test_with_helpers
+ get :with_helpers
+
+ assert_equal "helper", response.body
+ end
+end
diff --git a/actionpack/test/controller/base_test.rb b/actionpack/test/controller/base_test.rb
index b7fe253de7..558e710df9 100644
--- a/actionpack/test/controller/base_test.rb
+++ b/actionpack/test/controller/base_test.rb
@@ -1,6 +1,8 @@
-require 'abstract_unit'
-require 'active_support/logger'
-require 'controller/fake_models'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/logger"
+require "controller/fake_models"
# Provide some controller to run the tests on.
module Submodule
@@ -11,6 +13,12 @@ end
class EmptyController < ActionController::Base
end
+class SimpleController < ActionController::Base
+ def hello
+ self.response_body = "hello"
+ end
+end
+
class NonEmptyController < ActionController::Base
def public_action
head :ok
@@ -19,11 +27,11 @@ end
class DefaultUrlOptionsController < ActionController::Base
def from_view
- render :inline => "<%= #{params[:route]} %>"
+ render inline: "<%= #{params[:route]} %>"
end
def default_url_options
- { :host => 'www.override.com', :action => 'new', :locale => 'en' }
+ { host: "www.override.com", action: "new", locale: "en" }
end
end
@@ -33,17 +41,17 @@ class OptionalDefaultUrlOptionsController < ActionController::Base
end
def default_url_options
- { format: 'atom', id: 'default-id' }
+ { format: "atom", id: "default-id" }
end
end
class UrlOptionsController < ActionController::Base
def from_view
- render :inline => "<%= #{params[:route]} %>"
+ render inline: "<%= #{params[:route]} %>"
end
def url_options
- super.merge(:host => 'www.override.com')
+ super.merge(host: "www.override.com")
end
end
@@ -53,22 +61,21 @@ end
class ActionMissingController < ActionController::Base
def action_missing(action)
- render :text => "Response for #{action}"
+ render plain: "Response for #{action}"
end
end
class ControllerClassTests < ActiveSupport::TestCase
-
def test_controller_path
- assert_equal 'empty', EmptyController.controller_path
+ assert_equal "empty", EmptyController.controller_path
assert_equal EmptyController.controller_path, EmptyController.new.controller_path
- assert_equal 'submodule/contained_empty', Submodule::ContainedEmptyController.controller_path
+ assert_equal "submodule/contained_empty", Submodule::ContainedEmptyController.controller_path
assert_equal Submodule::ContainedEmptyController.controller_path, Submodule::ContainedEmptyController.new.controller_path
end
def test_controller_name
- assert_equal 'empty', EmptyController.controller_name
- assert_equal 'contained_empty', Submodule::ContainedEmptyController.controller_name
+ assert_equal "empty", EmptyController.controller_name
+ assert_equal "contained_empty", Submodule::ContainedEmptyController.controller_name
end
def test_no_deprecation_when_action_view_record_identifier_is_included
@@ -80,27 +87,29 @@ class ControllerClassTests < ActiveSupport::TestCase
dom_id = RecordIdentifierIncludedController.new.dom_id(record)
end
- assert_equal 'comment_1', dom_id
+ assert_equal "comment_1", dom_id
dom_class = nil
assert_not_deprecated do
dom_class = RecordIdentifierIncludedController.new.dom_class(record)
end
- assert_equal 'comment', dom_class
+ assert_equal "comment", dom_class
end
end
class ControllerInstanceTests < ActiveSupport::TestCase
def setup
@empty = EmptyController.new
+ @empty.set_request!(ActionDispatch::Request.empty)
+ @empty.set_response!(EmptyController.make_response!(@empty.request))
@contained = Submodule::ContainedEmptyController.new
@empty_controllers = [@empty, @contained]
end
def test_performed?
- assert !@empty.performed?
+ assert_not_predicate @empty, :performed?
@empty.response_body = ["sweet"]
- assert @empty.performed?
+ assert_predicate @empty, :performed?
end
def test_action_methods
@@ -110,13 +119,34 @@ class ControllerInstanceTests < ActiveSupport::TestCase
end
def test_temporary_anonymous_controllers
- name = 'ExamplesController'
+ name = "ExamplesController"
klass = Class.new(ActionController::Base)
Object.const_set(name, klass)
controller = klass.new
assert_equal "examples", controller.controller_path
end
+
+ def test_response_has_default_headers
+ original_default_headers = ActionDispatch::Response.default_headers
+
+ ActionDispatch::Response.default_headers = {
+ "X-Frame-Options" => "DENY",
+ "X-Content-Type-Options" => "nosniff",
+ "X-XSS-Protection" => "1;"
+ }
+
+ response_headers = SimpleController.action("hello").call(
+ "REQUEST_METHOD" => "GET",
+ "rack.input" => -> { }
+ )[1]
+
+ assert response_headers.key?("X-Frame-Options")
+ assert response_headers.key?("X-Content-Type-Options")
+ assert response_headers.key?("X-XSS-Protection")
+ ensure
+ ActionDispatch::Response.default_headers = original_default_headers
+ end
end
class PerformActionTest < ActionController::TestCase
@@ -150,37 +180,40 @@ class UrlOptionsTest < ActionController::TestCase
def setup
super
- @request.host = 'www.example.com'
+ @request.host = "www.example.com"
end
def test_url_for_query_params_included
rs = ActionDispatch::Routing::RouteSet.new
rs.draw do
- get 'home' => 'pages#home'
+ get "home" => "pages#home"
end
options = {
- :action => "home",
- :controller => "pages",
- :only_path => true,
- :params => { "token" => "secret" }
+ action: "home",
+ controller: "pages",
+ only_path: true,
+ token: "secret"
}
- assert_equal '/home?token=secret', rs.url_for(options)
+ assert_equal "/home?token=secret", rs.url_for(options)
end
def test_url_options_override
with_routing do |set|
set.draw do
- get 'from_view', :to => 'url_options#from_view', :as => :from_view
- get ':controller/:action'
+ get "from_view", to: "url_options#from_view", as: :from_view
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action"
+ end
end
get :from_view, params: { route: "from_view_url" }
- assert_equal 'http://www.override.com/from_view', @response.body
- assert_equal 'http://www.override.com/from_view', @controller.send(:from_view_url)
- assert_equal 'http://www.override.com/default_url_options/index', @controller.url_for(:controller => 'default_url_options')
+ assert_equal "http://www.override.com/from_view", @response.body
+ assert_equal "http://www.override.com/from_view", @controller.send(:from_view_url)
+ assert_equal "http://www.override.com/default_url_options/index", @controller.url_for(controller: "default_url_options")
end
end
@@ -190,7 +223,7 @@ class UrlOptionsTest < ActionController::TestCase
get "account/overview"
end
- assert !@controller.class.action_methods.include?("account_overview_path")
+ assert_not_includes @controller.class.action_methods, "account_overview_path"
end
end
end
@@ -200,21 +233,24 @@ class DefaultUrlOptionsTest < ActionController::TestCase
def setup
super
- @request.host = 'www.example.com'
+ @request.host = "www.example.com"
end
def test_default_url_options_override
with_routing do |set|
set.draw do
- get 'from_view', :to => 'default_url_options#from_view', :as => :from_view
- get ':controller/:action'
+ get "from_view", to: "default_url_options#from_view", as: :from_view
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action"
+ end
end
get :from_view, params: { route: "from_view_url" }
- assert_equal 'http://www.override.com/from_view?locale=en', @response.body
- assert_equal 'http://www.override.com/from_view?locale=en', @controller.send(:from_view_url)
- assert_equal 'http://www.override.com/default_url_options/new?locale=en', @controller.url_for(:controller => 'default_url_options')
+ assert_equal "http://www.override.com/from_view?locale=en", @response.body
+ assert_equal "http://www.override.com/from_view?locale=en", @controller.send(:from_view_url)
+ assert_equal "http://www.override.com/default_url_options/new?locale=en", @controller.url_for(controller: "default_url_options")
end
end
@@ -224,22 +260,25 @@ class DefaultUrlOptionsTest < ActionController::TestCase
scope("/:locale") do
resources :descriptions
end
- get ':controller/:action'
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action"
+ end
end
get :from_view, params: { route: "description_path(1)" }
- assert_equal '/en/descriptions/1', @response.body
- assert_equal '/en/descriptions', @controller.send(:descriptions_path)
- assert_equal '/pl/descriptions', @controller.send(:descriptions_path, "pl")
- assert_equal '/pl/descriptions', @controller.send(:descriptions_path, :locale => "pl")
- assert_equal '/pl/descriptions.xml', @controller.send(:descriptions_path, "pl", "xml")
- assert_equal '/en/descriptions.xml', @controller.send(:descriptions_path, :format => "xml")
- assert_equal '/en/descriptions/1', @controller.send(:description_path, 1)
- assert_equal '/pl/descriptions/1', @controller.send(:description_path, "pl", 1)
- assert_equal '/pl/descriptions/1', @controller.send(:description_path, 1, :locale => "pl")
- assert_equal '/pl/descriptions/1.xml', @controller.send(:description_path, "pl", 1, "xml")
- assert_equal '/en/descriptions/1.xml', @controller.send(:description_path, 1, :format => "xml")
+ assert_equal "/en/descriptions/1", @response.body
+ assert_equal "/en/descriptions", @controller.send(:descriptions_path)
+ assert_equal "/pl/descriptions", @controller.send(:descriptions_path, "pl")
+ assert_equal "/pl/descriptions", @controller.send(:descriptions_path, locale: "pl")
+ assert_equal "/pl/descriptions.xml", @controller.send(:descriptions_path, "pl", "xml")
+ assert_equal "/en/descriptions.xml", @controller.send(:descriptions_path, format: "xml")
+ assert_equal "/en/descriptions/1", @controller.send(:description_path, 1)
+ assert_equal "/pl/descriptions/1", @controller.send(:description_path, "pl", 1)
+ assert_equal "/pl/descriptions/1", @controller.send(:description_path, 1, locale: "pl")
+ assert_equal "/pl/descriptions/1.xml", @controller.send(:description_path, "pl", 1, "xml")
+ assert_equal "/en/descriptions/1.xml", @controller.send(:description_path, 1, format: "xml")
end
end
end
@@ -248,10 +287,10 @@ class OptionalDefaultUrlOptionsControllerTest < ActionController::TestCase
def test_default_url_options_override_missing_positional_arguments
with_routing do |set|
set.draw do
- get "/things/:id(.:format)" => 'things#show', :as => :thing
+ get "/things/:id(.:format)" => "things#show", :as => :thing
end
- assert_equal '/things/1.atom', thing_path('1')
- assert_equal '/things/default-id.atom', thing_path
+ assert_equal "/things/1.atom", thing_path("1")
+ assert_equal "/things/default-id.atom", thing_path
end
end
end
@@ -261,7 +300,7 @@ class EmptyUrlOptionsTest < ActionController::TestCase
def setup
super
- @request.host = 'www.example.com'
+ @request.host = "www.example.com"
end
def test_ensure_url_for_works_as_expected_when_called_with_no_options_if_default_url_options_is_not_set
@@ -278,7 +317,7 @@ class EmptyUrlOptionsTest < ActionController::TestCase
resources :things
end
- assert_equal '/things', @controller.send(:things_path)
+ assert_equal "/things", @controller.send(:things_path)
end
end
end
diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb
index 5698159eba..6fe036dd15 100644
--- a/actionpack/test/controller/caching_test.rb
+++ b/actionpack/test/controller/caching_test.rb
@@ -1,10 +1,12 @@
-require 'fileutils'
-require 'abstract_unit'
-require 'lib/controller/fake_models'
+# frozen_string_literal: true
-CACHE_DIR = 'test_cache'
+require "fileutils"
+require "abstract_unit"
+require "lib/controller/fake_models"
+
+CACHE_DIR = "test_cache"
# Don't change '/../temp/' cavalierly or you might hose something you don't want hosed
-FILE_STORE_PATH = File.join(File.dirname(__FILE__), '/../temp/', CACHE_DIR)
+FILE_STORE_PATH = File.join(__dir__, "../temp/", CACHE_DIR)
class FragmentCachingMetalTestController < ActionController::Metal
abstract!
@@ -21,15 +23,11 @@ class FragmentCachingMetalTest < ActionController::TestCase
@controller = FragmentCachingMetalTestController.new
@controller.perform_caching = true
@controller.cache_store = @store
- @params = { controller: 'posts', action: 'index' }
+ @params = { controller: "posts", action: "index" }
@controller.params = @params
@controller.request = @request
@controller.response = @response
end
-
- def test_fragment_cache_key
- assert_equal 'views/what a key', @controller.fragment_cache_key('what a key')
- end
end
class CachingController < ActionController::Base
@@ -43,104 +41,125 @@ class FragmentCachingTestController < CachingController
end
class FragmentCachingTest < ActionController::TestCase
+ ModelWithKeyAndVersion = Struct.new(:cache_key, :cache_version)
+
def setup
super
@store = ActiveSupport::Cache::MemoryStore.new
@controller = FragmentCachingTestController.new
@controller.perform_caching = true
@controller.cache_store = @store
- @params = {:controller => 'posts', :action => 'index'}
+ @params = { controller: "posts", action: "index" }
@controller.params = @params
@controller.request = @request
@controller.response = @response
+
+ @m1v1 = ModelWithKeyAndVersion.new("model/1", "1")
+ @m1v2 = ModelWithKeyAndVersion.new("model/1", "2")
+ @m2v1 = ModelWithKeyAndVersion.new("model/2", "1")
+ @m2v2 = ModelWithKeyAndVersion.new("model/2", "2")
end
def test_fragment_cache_key
- assert_equal 'views/what a key', @controller.fragment_cache_key('what a key')
- assert_equal "views/test.host/fragment_caching_test/some_action",
- @controller.fragment_cache_key(:controller => 'fragment_caching_test',:action => 'some_action')
+ assert_deprecated do
+ assert_equal "views/what a key", @controller.fragment_cache_key("what a key")
+ assert_equal "views/test.host/fragment_caching_test/some_action",
+ @controller.fragment_cache_key(controller: "fragment_caching_test", action: "some_action")
+ end
+ end
+
+ def test_combined_fragment_cache_key
+ assert_equal [ :views, "what a key" ], @controller.combined_fragment_cache_key("what a key")
+ assert_equal [ :views, "test.host/fragment_caching_test/some_action" ],
+ @controller.combined_fragment_cache_key(controller: "fragment_caching_test", action: "some_action")
end
def test_read_fragment_with_caching_enabled
- @store.write('views/name', 'value')
- assert_equal 'value', @controller.read_fragment('name')
+ @store.write("views/name", "value")
+ assert_equal "value", @controller.read_fragment("name")
end
def test_read_fragment_with_caching_disabled
@controller.perform_caching = false
- @store.write('views/name', 'value')
- assert_nil @controller.read_fragment('name')
+ @store.write("views/name", "value")
+ assert_nil @controller.read_fragment("name")
+ end
+
+ def test_read_fragment_with_versioned_model
+ @controller.write_fragment([ "stuff", @m1v1 ], "hello")
+ assert_equal "hello", @controller.read_fragment([ "stuff", @m1v1 ])
+ assert_nil @controller.read_fragment([ "stuff", @m1v2 ])
end
def test_fragment_exist_with_caching_enabled
- @store.write('views/name', 'value')
- assert @controller.fragment_exist?('name')
- assert !@controller.fragment_exist?('other_name')
+ @store.write("views/name", "value")
+ assert @controller.fragment_exist?("name")
+ assert_not @controller.fragment_exist?("other_name")
end
def test_fragment_exist_with_caching_disabled
@controller.perform_caching = false
- @store.write('views/name', 'value')
- assert !@controller.fragment_exist?('name')
- assert !@controller.fragment_exist?('other_name')
+ @store.write("views/name", "value")
+ assert_not @controller.fragment_exist?("name")
+ assert_not @controller.fragment_exist?("other_name")
end
def test_write_fragment_with_caching_enabled
- assert_nil @store.read('views/name')
- assert_equal 'value', @controller.write_fragment('name', 'value')
- assert_equal 'value', @store.read('views/name')
+ assert_nil @store.read("views/name")
+ assert_equal "value", @controller.write_fragment("name", "value")
+ assert_equal "value", @store.read("views/name")
end
def test_write_fragment_with_caching_disabled
- assert_nil @store.read('views/name')
+ assert_nil @store.read("views/name")
@controller.perform_caching = false
- assert_equal 'value', @controller.write_fragment('name', 'value')
- assert_nil @store.read('views/name')
+ assert_equal "value", @controller.write_fragment("name", "value")
+ assert_nil @store.read("views/name")
end
def test_expire_fragment_with_simple_key
- @store.write('views/name', 'value')
- @controller.expire_fragment 'name'
- assert_nil @store.read('views/name')
+ @store.write("views/name", "value")
+ @controller.expire_fragment "name"
+ assert_nil @store.read("views/name")
end
def test_expire_fragment_with_regexp
- @store.write('views/name', 'value')
- @store.write('views/another_name', 'another_value')
- @store.write('views/primalgrasp', 'will not expire ;-)')
+ @store.write("views/name", "value")
+ @store.write("views/another_name", "another_value")
+ @store.write("views/primalgrasp", "will not expire ;-)")
@controller.expire_fragment(/name/)
- assert_nil @store.read('views/name')
- assert_nil @store.read('views/another_name')
- assert_equal 'will not expire ;-)', @store.read('views/primalgrasp')
+ assert_nil @store.read("views/name")
+ assert_nil @store.read("views/another_name")
+ assert_equal "will not expire ;-)", @store.read("views/primalgrasp")
end
def test_fragment_for
- @store.write('views/expensive', 'fragment content')
+ @store.write("views/expensive", "fragment content")
fragment_computed = false
view_context = @controller.view_context
- buffer = 'generated till now -> '.html_safe
- buffer << view_context.send(:fragment_for, 'expensive') { fragment_computed = true }
+ buffer = "generated till now -> ".html_safe
+ buffer << view_context.send(:fragment_for, "expensive") { fragment_computed = true }
- assert !fragment_computed
- assert_equal 'generated till now -> fragment content', buffer
+ assert_not fragment_computed
+ assert_equal "generated till now -> fragment content", buffer
end
def test_html_safety
- assert_nil @store.read('views/name')
- content = 'value'.html_safe
- assert_equal content, @controller.write_fragment('name', content)
+ assert_nil @store.read("views/name")
+ content = "value".html_safe
+ assert_equal content, @controller.write_fragment("name", content)
- cached = @store.read('views/name')
+ cached = @store.read("views/name")
assert_equal content, cached
assert_equal String, cached.class
- html_safe = @controller.read_fragment('name')
+ html_safe = @controller.read_fragment("name")
assert_equal content, html_safe
- assert html_safe.html_safe?
+ assert_predicate html_safe, :html_safe?
end
end
@@ -154,6 +173,9 @@ class FunctionalCachingController < CachingController
end
end
+ def xml_fragment_cached_with_html_partial
+ end
+
def formatted_fragment_cached
respond_to do |format|
format.html
@@ -172,6 +194,9 @@ class FunctionalCachingController < CachingController
def fragment_cached_without_digest
end
+
+ def fragment_cached_with_options
+ end
end
class FunctionalFragmentCachingTest < ActionController::TestCase
@@ -181,6 +206,7 @@ class FunctionalFragmentCachingTest < ActionController::TestCase
@controller = FunctionalCachingController.new
@controller.perform_caching = true
@controller.cache_store = @store
+ @controller.enable_fragment_cache_logging = true
end
def test_fragment_caching
@@ -194,7 +220,7 @@ CACHED
assert_equal expected_body, @response.body
assert_equal "This bit's fragment cached",
- @store.read("views/test.host/functional_caching/fragment_cached/#{template_digest("functional_caching/fragment_cached")}")
+ @store.read("views/functional_caching/fragment_cached:#{template_digest("functional_caching/fragment_cached")}/fragment")
end
def test_fragment_caching_in_partials
@@ -203,7 +229,7 @@ CACHED
assert_match(/Old fragment caching in a partial/, @response.body)
assert_match("Old fragment caching in a partial",
- @store.read("views/test.host/functional_caching/html_fragment_cached_with_partial/#{template_digest("functional_caching/_partial")}"))
+ @store.read("views/functional_caching/_partial:#{template_digest("functional_caching/_partial")}/test.host/functional_caching/html_fragment_cached_with_partial"))
end
def test_skipping_fragment_cache_digesting
@@ -215,13 +241,25 @@ CACHED
assert_equal "<p>ERB</p>", @store.read("views/nodigest")
end
+ def test_fragment_caching_with_options
+ time = Time.now
+ get :fragment_cached_with_options
+ assert_response :success
+ expected_body = "<body>\n<p>ERB</p>\n</body>\n"
+
+ assert_equal expected_body, @response.body
+ Time.stub(:now, time + 11) do
+ assert_nil @store.read("views/with_options")
+ end
+ end
+
def test_render_inline_before_fragment_caching
get :inline_fragment_cached
assert_response :success
assert_match(/Some inline content/, @response.body)
assert_match(/Some cached content/, @response.body)
assert_match("Some cached content",
- @store.read("views/test.host/functional_caching/inline_fragment_cached/#{template_digest("functional_caching/inline_fragment_cached")}"))
+ @store.read("views/functional_caching/inline_fragment_cached:#{template_digest("functional_caching/inline_fragment_cached")}/test.host/functional_caching/inline_fragment_cached"))
end
def test_fragment_cache_instrumentation
@@ -248,7 +286,7 @@ CACHED
assert_equal expected_body, @response.body
assert_equal "<p>ERB</p>",
- @store.read("views/test.host/functional_caching/formatted_fragment_cached/#{template_digest("functional_caching/formatted_fragment_cached")}")
+ @store.read("views/functional_caching/formatted_fragment_cached:#{template_digest("functional_caching/formatted_fragment_cached")}/fragment")
end
def test_xml_formatted_fragment_caching
@@ -259,10 +297,9 @@ CACHED
assert_equal expected_body, @response.body
assert_equal " <p>Builder</p>\n",
- @store.read("views/test.host/functional_caching/formatted_fragment_cached/#{template_digest("functional_caching/formatted_fragment_cached")}")
+ @store.read("views/functional_caching/formatted_fragment_cached:#{template_digest("functional_caching/formatted_fragment_cached")}/fragment")
end
-
def test_fragment_caching_with_variant
get :formatted_fragment_cached_with_variant, format: "html", params: { v: :phone }
assert_response :success
@@ -271,7 +308,12 @@ CACHED
assert_equal expected_body, @response.body
assert_equal "<p>PHONE</p>",
- @store.read("views/test.host/functional_caching/formatted_fragment_cached_with_variant/#{template_digest("functional_caching/formatted_fragment_cached_with_variant")}")
+ @store.read("views/functional_caching/formatted_fragment_cached_with_variant:#{template_digest("functional_caching/formatted_fragment_cached_with_variant")}/fragment")
+ end
+
+ def test_fragment_caching_with_html_partials_in_xml
+ get :xml_fragment_cached_with_html_partial, format: "*/*"
+ assert_response :success
end
private
@@ -281,10 +323,9 @@ CACHED
end
class CacheHelperOutputBufferTest < ActionController::TestCase
-
class MockController
def read_fragment(name, options)
- return false
+ false
end
def write_fragment(name, fragment, options)
@@ -299,30 +340,42 @@ class CacheHelperOutputBufferTest < ActionController::TestCase
def test_output_buffer
output_buffer = ActionView::OutputBuffer.new
controller = MockController.new
- cache_helper = Object.new
+ cache_helper = Class.new do
+ def self.controller; end
+ def self.output_buffer; end
+ def self.output_buffer=; end
+ end
cache_helper.extend(ActionView::Helpers::CacheHelper)
- cache_helper.expects(:controller).returns(controller).at_least(0)
- cache_helper.expects(:output_buffer).returns(output_buffer).at_least(0)
- # if the output_buffer is changed, the new one should be html_safe and of the same type
- cache_helper.expects(:output_buffer=).with(responds_with(:html_safe?, true)).with(instance_of(output_buffer.class)).at_least(0)
- assert_nothing_raised do
- cache_helper.send :fragment_for, 'Test fragment name', 'Test fragment', &Proc.new{ nil }
+ cache_helper.stub :controller, controller do
+ cache_helper.stub :output_buffer, output_buffer do
+ assert_called_with cache_helper, :output_buffer=, [output_buffer.class.new(output_buffer)] do
+ assert_nothing_raised do
+ cache_helper.send :fragment_for, "Test fragment name", "Test fragment", &Proc.new { nil }
+ end
+ end
+ end
end
end
def test_safe_buffer
output_buffer = ActiveSupport::SafeBuffer.new
controller = MockController.new
- cache_helper = Object.new
+ cache_helper = Class.new do
+ def self.controller; end
+ def self.output_buffer; end
+ def self.output_buffer=; end
+ end
cache_helper.extend(ActionView::Helpers::CacheHelper)
- cache_helper.expects(:controller).returns(controller).at_least(0)
- cache_helper.expects(:output_buffer).returns(output_buffer).at_least(0)
- # if the output_buffer is changed, the new one should be html_safe and of the same type
- cache_helper.expects(:output_buffer=).with(responds_with(:html_safe?, true)).with(instance_of(output_buffer.class)).at_least(0)
- assert_nothing_raised do
- cache_helper.send :fragment_for, 'Test fragment name', 'Test fragment', &Proc.new{ nil }
+ cache_helper.stub :controller, controller do
+ cache_helper.stub :output_buffer, output_buffer do
+ assert_called_with cache_helper, :output_buffer=, [output_buffer.class.new(output_buffer)] do
+ assert_nothing_raised do
+ cache_helper.send :fragment_for, "Test fragment name", "Test fragment", &Proc.new { nil }
+ end
+ end
+ end
end
end
end
@@ -337,7 +390,7 @@ class ViewCacheDependencyTest < ActionController::TestCase
end
def test_view_cache_dependencies_are_empty_by_default
- assert NoDependenciesController.new.view_cache_dependencies.empty?
+ assert_empty NoDependenciesController.new.view_cache_dependencies
end
def test_view_cache_dependencies_are_listed_in_declaration_order
@@ -349,38 +402,44 @@ class CollectionCacheController < ActionController::Base
attr_accessor :partial_rendered_times
def index
- @customers = [Customer.new('david', params[:id] || 1)]
+ @customers = [Customer.new("david", params[:id] || 1)]
end
def index_ordered
- @customers = [Customer.new('david', 1), Customer.new('david', 2), Customer.new('david', 3)]
- render 'index'
+ @customers = [Customer.new("david", 1), Customer.new("david", 2), Customer.new("david", 3)]
+ render "index"
end
- def index_explicit_render
- @customers = [Customer.new('david', 1)]
- render partial: 'customers/customer', collection: @customers
+ def index_explicit_render_in_controller
+ @customers = [Customer.new("david", 1)]
+ render partial: "customers/customer", collection: @customers, cached: true
end
def index_with_comment
- @customers = [Customer.new('david', 1)]
- render partial: 'customers/commented_customer', collection: @customers, as: :customer
+ @customers = [Customer.new("david", 1)]
+ render partial: "customers/commented_customer", collection: @customers, as: :customer, cached: true
+ end
+
+ def index_with_callable_cache_key
+ @customers = [Customer.new("david", 1)]
+ render partial: "customers/customer", collection: @customers, cached: -> customer { "cached_david" }
end
end
-class AutomaticCollectionCacheTest < ActionController::TestCase
+class CollectionCacheTest < ActionController::TestCase
def setup
super
@controller = CollectionCacheController.new
@controller.perform_caching = true
@controller.partial_rendered_times = 0
@controller.cache_store = ActiveSupport::Cache::MemoryStore.new
- ActionView::PartialRenderer.collection_cache = @controller.cache_store
+ ActionView::PartialRenderer.collection_cache = ActiveSupport::Cache::MemoryStore.new
end
def test_collection_fetches_cached_views
get :index
assert_equal 1, @controller.partial_rendered_times
+ assert_match "david, 1", ActionView::PartialRenderer.collection_cache.read("views/customers/_customer:7c228ab609f0baf0b1f2367469210937/david/1")
get :index
assert_equal 1, @controller.partial_rendered_times
@@ -388,15 +447,18 @@ class AutomaticCollectionCacheTest < ActionController::TestCase
def test_preserves_order_when_reading_from_cache_plus_rendering
get :index, params: { id: 2 }
- get :index_ordered
+ assert_equal 1, @controller.partial_rendered_times
+ assert_select ":root", "david, 2"
- assert_select ':root', "david, 1\n david, 2\n david, 3"
+ get :index_ordered
+ assert_equal 3, @controller.partial_rendered_times
+ assert_select ":root", "david, 1\n david, 2\n david, 3"
end
def test_explicit_render_call_with_options
- get :index_explicit_render
+ get :index_explicit_render_in_controller
- assert_select ':root', "david, 1"
+ assert_select ":root", "david, 1"
end
def test_caching_works_with_beginning_comment
@@ -406,4 +468,44 @@ class AutomaticCollectionCacheTest < ActionController::TestCase
get :index_with_comment
assert_equal 1, @controller.partial_rendered_times
end
+
+ def test_caching_with_callable_cache_key
+ get :index_with_callable_cache_key
+ assert_match "david, 1", ActionView::PartialRenderer.collection_cache.read("views/customers/_customer:7c228ab609f0baf0b1f2367469210937/cached_david")
+ end
+end
+
+class FragmentCacheKeyTestController < CachingController
+ attr_accessor :account_id
+
+ fragment_cache_key "v1"
+ fragment_cache_key { account_id }
+end
+
+class FragmentCacheKeyTest < ActionController::TestCase
+ def setup
+ super
+ @store = ActiveSupport::Cache::MemoryStore.new
+ @controller = FragmentCacheKeyTestController.new
+ @controller.perform_caching = true
+ @controller.cache_store = @store
+ end
+
+ def test_combined_fragment_cache_key
+ @controller.account_id = "123"
+ assert_equal [ :views, "v1", "123", "what a key" ], @controller.combined_fragment_cache_key("what a key")
+
+ @controller.account_id = nil
+ assert_equal [ :views, "v1", "what a key" ], @controller.combined_fragment_cache_key("what a key")
+ end
+
+ def test_combined_fragment_cache_key_with_envs
+ ENV["RAILS_APP_VERSION"] = "55"
+ assert_equal [ :views, "55", "v1", "what a key" ], @controller.combined_fragment_cache_key("what a key")
+
+ ENV["RAILS_CACHE_ID"] = "66"
+ assert_equal [ :views, "66", "v1", "what a key" ], @controller.combined_fragment_cache_key("what a key")
+ ensure
+ ENV["RAILS_CACHE_ID"] = ENV["RAILS_APP_VERSION"] = nil
+ end
end
diff --git a/actionpack/test/controller/content_type_test.rb b/actionpack/test/controller/content_type_test.rb
index 89667df3a4..636b025f2c 100644
--- a/actionpack/test/controller/content_type_test.rb
+++ b/actionpack/test/controller/content_type_test.rb
@@ -1,32 +1,34 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class OldContentTypeController < ActionController::Base
# :ported:
def render_content_type_from_body
- response.content_type = Mime::RSS
- render :text => "hello world!"
+ response.content_type = Mime[:rss]
+ render body: "hello world!"
end
# :ported:
def render_defaults
- render :text => "hello world!"
+ render body: "hello world!"
end
# :ported:
def render_content_type_from_render
- render :text => "hello world!", :content_type => Mime::RSS
+ render body: "hello world!", content_type: Mime[:rss]
end
# :ported:
def render_charset_from_body
response.charset = "utf-16"
- render :text => "hello world!"
+ render body: "hello world!"
end
# :ported:
def render_nil_charset_from_body
response.charset = nil
- render :text => "hello world!"
+ render body: "hello world!"
end
def render_default_for_erb
@@ -36,16 +38,16 @@ class OldContentTypeController < ActionController::Base
end
def render_change_for_builder
- response.content_type = Mime::HTML
- render :action => "render_default_for_builder"
+ response.content_type = Mime[:html]
+ render action: "render_default_for_builder"
end
def render_default_content_types_for_respond_to
respond_to do |format|
- format.html { render :text => "hello world!" }
- format.xml { render :action => "render_default_content_types_for_respond_to" }
- format.js { render :text => "hello world!" }
- format.rss { render :text => "hello world!", :content_type => Mime::XML }
+ format.html { render body: "hello world!" }
+ format.xml { render action: "render_default_content_types_for_respond_to" }
+ format.js { render body: "hello world!" }
+ format.rss { render body: "hello world!", content_type: Mime[:xml] }
end
end
end
@@ -64,104 +66,104 @@ class ContentTypeTest < ActionController::TestCase
def test_render_defaults
get :render_defaults
assert_equal "utf-8", @response.charset
- assert_equal Mime::HTML, @response.content_type
+ assert_equal Mime[:text], @response.content_type
end
def test_render_changed_charset_default
with_default_charset "utf-16" do
get :render_defaults
assert_equal "utf-16", @response.charset
- assert_equal Mime::HTML, @response.content_type
+ assert_equal Mime[:text], @response.content_type
end
end
# :ported:
def test_content_type_from_body
get :render_content_type_from_body
- assert_equal Mime::RSS, @response.content_type
+ assert_equal Mime[:rss], @response.content_type
assert_equal "utf-8", @response.charset
end
# :ported:
def test_content_type_from_render
get :render_content_type_from_render
- assert_equal Mime::RSS, @response.content_type
+ assert_equal Mime[:rss], @response.content_type
assert_equal "utf-8", @response.charset
end
# :ported:
def test_charset_from_body
get :render_charset_from_body
- assert_equal Mime::HTML, @response.content_type
+ assert_equal Mime[:text], @response.content_type
assert_equal "utf-16", @response.charset
end
# :ported:
def test_nil_charset_from_body
get :render_nil_charset_from_body
- assert_equal Mime::HTML, @response.content_type
+ assert_equal Mime[:text], @response.content_type
assert_equal "utf-8", @response.charset, @response.headers.inspect
end
def test_nil_default_for_erb
with_default_charset nil do
get :render_default_for_erb
- assert_equal Mime::HTML, @response.content_type
+ assert_equal Mime[:html], @response.content_type
assert_nil @response.charset, @response.headers.inspect
end
end
def test_default_for_erb
get :render_default_for_erb
- assert_equal Mime::HTML, @response.content_type
+ assert_equal Mime[:html], @response.content_type
assert_equal "utf-8", @response.charset
end
def test_default_for_builder
get :render_default_for_builder
- assert_equal Mime::XML, @response.content_type
+ assert_equal Mime[:xml], @response.content_type
assert_equal "utf-8", @response.charset
end
def test_change_for_builder
get :render_change_for_builder
- assert_equal Mime::HTML, @response.content_type
+ assert_equal Mime[:html], @response.content_type
assert_equal "utf-8", @response.charset
end
private
- def with_default_charset(charset)
- old_default_charset = ActionDispatch::Response.default_charset
- ActionDispatch::Response.default_charset = charset
- yield
- ensure
- ActionDispatch::Response.default_charset = old_default_charset
- end
+ def with_default_charset(charset)
+ old_default_charset = ActionDispatch::Response.default_charset
+ ActionDispatch::Response.default_charset = charset
+ yield
+ ensure
+ ActionDispatch::Response.default_charset = old_default_charset
+ end
end
class AcceptBasedContentTypeTest < ActionController::TestCase
tests OldContentTypeController
def test_render_default_content_types_for_respond_to
- @request.accept = Mime::HTML.to_s
+ @request.accept = Mime[:html].to_s
get :render_default_content_types_for_respond_to
- assert_equal Mime::HTML, @response.content_type
+ assert_equal Mime[:html], @response.content_type
- @request.accept = Mime::JS.to_s
+ @request.accept = Mime[:js].to_s
get :render_default_content_types_for_respond_to
- assert_equal Mime::JS, @response.content_type
+ assert_equal Mime[:js], @response.content_type
end
def test_render_default_content_types_for_respond_to_with_template
- @request.accept = Mime::XML.to_s
+ @request.accept = Mime[:xml].to_s
get :render_default_content_types_for_respond_to
- assert_equal Mime::XML, @response.content_type
+ assert_equal Mime[:xml], @response.content_type
end
def test_render_default_content_types_for_respond_to_with_overwrite
- @request.accept = Mime::RSS.to_s
+ @request.accept = Mime[:rss].to_s
get :render_default_content_types_for_respond_to
- assert_equal Mime::XML, @response.content_type
+ assert_equal Mime[:xml], @response.content_type
end
end
diff --git a/actionpack/test/controller/default_url_options_with_before_action_test.rb b/actionpack/test/controller/default_url_options_with_before_action_test.rb
index 230f40d7ad..fc5b8288cd 100644
--- a/actionpack/test/controller/default_url_options_with_before_action_test.rb
+++ b/actionpack/test/controller/default_url_options_with_before_action_test.rb
@@ -1,25 +1,25 @@
-require 'abstract_unit'
+# frozen_string_literal: true
-class ControllerWithBeforeActionAndDefaultUrlOptions < ActionController::Base
+require "abstract_unit"
+class ControllerWithBeforeActionAndDefaultUrlOptions < ActionController::Base
before_action { I18n.locale = params[:locale] }
after_action { I18n.locale = "en" }
def target
- render :text => "final response"
+ render plain: "final response"
end
def redirect
- redirect_to :action => "target"
+ redirect_to action: "target"
end
def default_url_options
- {:locale => "de"}
+ { locale: "de" }
end
end
class ControllerWithBeforeActionAndDefaultUrlOptionsTest < ActionController::TestCase
-
# This test has its roots in issue #1872
test "should redirect with correct locale :de" do
get :redirect, params: { locale: "de" }
diff --git a/actionpack/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb
index 8a6a51c263..425a6e25cc 100644
--- a/actionpack/test/controller/filters_test.rb
+++ b/actionpack/test/controller/filters_test.rb
@@ -1,8 +1,10 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class ActionController::Base
class << self
- %w(append_around_action prepend_after_action prepend_around_action prepend_before_action skip_after_action skip_before_action skip_action_callback).each do |pending|
+ %w(append_around_action prepend_after_action prepend_around_action prepend_before_action skip_after_action skip_before_action).each do |pending|
define_method(pending) do |*args|
$stderr.puts "#{pending} unimplemented: #{args.inspect}"
end unless method_defined?(pending)
@@ -21,7 +23,7 @@ class FilterTest < ActionController::TestCase
after_action :clean_up
def show
- render :inline => "ran action"
+ render inline: "ran action"
end
private
@@ -37,10 +39,10 @@ class FilterTest < ActionController::TestCase
end
class ChangingTheRequirementsController < TestController
- before_action :ensure_login, :except => [:go_wild]
+ before_action :ensure_login, except: [:go_wild]
def go_wild
- render :text => "gobble"
+ render plain: "gobble"
end
end
@@ -51,19 +53,19 @@ class FilterTest < ActionController::TestCase
(1..3).each do |i|
define_method "fail_#{i}" do
- render :text => i.to_s
+ render plain: i.to_s
end
end
- protected
- (1..3).each do |i|
- define_method "try_#{i}" do
- instance_variable_set :@try, i
- if action_name == "fail_#{i}"
- head(404)
+ private
+ (1..3).each do |i|
+ define_method "try_#{i}" do
+ instance_variable_set :@try, i
+ if action_name == "fail_#{i}"
+ head(404)
+ end
end
end
- end
end
class RenderingController < ActionController::Base
@@ -72,14 +74,14 @@ class FilterTest < ActionController::TestCase
def show
@ran_action = true
- render :inline => "ran action"
+ render inline: "ran action"
end
private
def before_action_rendering
@ran_filter ||= []
@ran_filter << "before_action_rendering"
- render :inline => "something else"
+ render inline: "something else"
end
def unreached_after_action
@@ -102,19 +104,19 @@ class FilterTest < ActionController::TestCase
def show
@ran_action = true
- render :inline => "ran show action"
+ render inline: "ran show action"
end
def target_of_redirection
@ran_target_of_redirection = true
- render :inline => "ran target_of_redirection action"
+ render inline: "ran target_of_redirection action"
end
private
def before_action_redirects
@ran_filter ||= []
@ran_filter << "before_action_redirects"
- redirect_to(:action => 'target_of_redirection')
+ redirect_to(action: "target_of_redirection")
end
def unreached_after_action
@@ -133,15 +135,15 @@ class FilterTest < ActionController::TestCase
class ConditionalFilterController < ActionController::Base
def show
- render :inline => "ran action"
+ render inline: "ran action"
end
def another_action
- render :inline => "ran action"
+ render inline: "ran action"
end
def show_without_action
- render :inline => "ran action without action"
+ render inline: "ran action without action"
end
private
@@ -157,28 +159,28 @@ class FilterTest < ActionController::TestCase
end
class ConditionalCollectionFilterController < ConditionalFilterController
- before_action :ensure_login, :except => [ :show_without_action, :another_action ]
+ before_action :ensure_login, except: [ :show_without_action, :another_action ]
end
class OnlyConditionSymController < ConditionalFilterController
- before_action :ensure_login, :only => :show
+ before_action :ensure_login, only: :show
end
class ExceptConditionSymController < ConditionalFilterController
- before_action :ensure_login, :except => :show_without_action
+ before_action :ensure_login, except: :show_without_action
end
class BeforeAndAfterConditionController < ConditionalFilterController
- before_action :ensure_login, :only => :show
- after_action :clean_up_tmp, :only => :show
+ before_action :ensure_login, only: :show
+ after_action :clean_up_tmp, only: :show
end
class OnlyConditionProcController < ConditionalFilterController
- before_action(:only => :show) {|c| c.instance_variable_set(:"@ran_proc_action", true) }
+ before_action(only: :show) { |c| c.instance_variable_set(:"@ran_proc_action", true) }
end
class ExceptConditionProcController < ConditionalFilterController
- before_action(:except => :show_without_action) {|c| c.instance_variable_set(:"@ran_proc_action", true) }
+ before_action(except: :show_without_action) { |c| c.instance_variable_set(:"@ran_proc_action", true) }
end
class ConditionalClassFilter
@@ -186,24 +188,24 @@ class FilterTest < ActionController::TestCase
end
class OnlyConditionClassController < ConditionalFilterController
- before_action ConditionalClassFilter, :only => :show
+ before_action ConditionalClassFilter, only: :show
end
class ExceptConditionClassController < ConditionalFilterController
- before_action ConditionalClassFilter, :except => :show_without_action
+ before_action ConditionalClassFilter, except: :show_without_action
end
class AnomolousYetValidConditionController < ConditionalFilterController
- before_action(ConditionalClassFilter, :ensure_login, Proc.new {|c| c.instance_variable_set(:"@ran_proc_action1", true)}, :except => :show_without_action) { |c| c.instance_variable_set(:"@ran_proc_action2", true)}
+ before_action(ConditionalClassFilter, :ensure_login, Proc.new { |c| c.instance_variable_set(:"@ran_proc_action1", true) }, except: :show_without_action) { |c| c.instance_variable_set(:"@ran_proc_action2", true) }
end
class OnlyConditionalOptionsFilter < ConditionalFilterController
- before_action :ensure_login, :only => :index, :if => Proc.new {|c| c.instance_variable_set(:"@ran_conditional_index_proc", true) }
+ before_action :ensure_login, only: :index, if: Proc.new { |c| c.instance_variable_set(:"@ran_conditional_index_proc", true) }
end
class ConditionalOptionsFilter < ConditionalFilterController
- before_action :ensure_login, :if => Proc.new { |c| true }
- before_action :clean_up_tmp, :if => Proc.new { |c| false }
+ before_action :ensure_login, if: Proc.new { |c| true }
+ before_action :clean_up_tmp, if: Proc.new { |c| false }
end
class ConditionalOptionsSkipFilter < ConditionalFilterController
@@ -222,7 +224,7 @@ class FilterTest < ActionController::TestCase
skip_before_action :clean_up_tmp, only: :login, if: -> { true }
def login
- render text: 'ok'
+ render plain: "ok"
end
end
@@ -234,7 +236,7 @@ class FilterTest < ActionController::TestCase
skip_before_action :clean_up_tmp, if: -> { true }, except: :login
def login
- render text: 'ok'
+ render plain: "ok"
end
end
@@ -255,14 +257,14 @@ class FilterTest < ActionController::TestCase
class SkippingAndLimitedController < TestController
skip_before_action :ensure_login
- before_action :ensure_login, :only => :index
+ before_action :ensure_login, only: :index
def index
- render :text => 'ok'
+ render plain: "ok"
end
def public
- render :text => 'ok'
+ render plain: "ok"
end
end
@@ -272,7 +274,7 @@ class FilterTest < ActionController::TestCase
before_action :ensure_login
def index
- render :text => 'ok'
+ render plain: "ok"
end
private
@@ -283,20 +285,20 @@ class FilterTest < ActionController::TestCase
end
class ConditionalSkippingController < TestController
- skip_before_action :ensure_login, :only => [ :login ]
- skip_after_action :clean_up, :only => [ :login ]
+ skip_before_action :ensure_login, only: [ :login ]
+ skip_after_action :clean_up, only: [ :login ]
- before_action :find_user, :only => [ :change_password ]
+ before_action :find_user, only: [ :change_password ]
def login
- render :inline => "ran action"
+ render inline: "ran action"
end
def change_password
- render :inline => "ran action"
+ render inline: "ran action"
end
- protected
+ private
def find_user
@ran_filter ||= []
@ran_filter << "find_user"
@@ -304,29 +306,29 @@ class FilterTest < ActionController::TestCase
end
class ConditionalParentOfConditionalSkippingController < ConditionalFilterController
- before_action :conditional_in_parent_before, :only => [:show, :another_action]
- after_action :conditional_in_parent_after, :only => [:show, :another_action]
+ before_action :conditional_in_parent_before, only: [:show, :another_action]
+ after_action :conditional_in_parent_after, only: [:show, :another_action]
private
def conditional_in_parent_before
@ran_filter ||= []
- @ran_filter << 'conditional_in_parent_before'
+ @ran_filter << "conditional_in_parent_before"
end
def conditional_in_parent_after
@ran_filter ||= []
- @ran_filter << 'conditional_in_parent_after'
+ @ran_filter << "conditional_in_parent_after"
end
end
class ChildOfConditionalParentController < ConditionalParentOfConditionalSkippingController
- skip_before_action :conditional_in_parent_before, :only => :another_action
- skip_after_action :conditional_in_parent_after, :only => :another_action
+ skip_before_action :conditional_in_parent_before, only: :another_action
+ skip_after_action :conditional_in_parent_after, only: :another_action
end
class AnotherChildOfConditionalParentController < ConditionalParentOfConditionalSkippingController
- skip_before_action :conditional_in_parent_before, :only => :show
+ skip_before_action :conditional_in_parent_before, only: :show
end
class ProcController < PrependingController
@@ -346,7 +348,7 @@ class FilterTest < ActionController::TestCase
class AroundFilter
def before(controller)
@execution_log = "before"
- controller.class.execution_log << " before aroundfilter " if controller.respond_to? :execution_log
+ controller.class.execution_log += " before aroundfilter " if controller.respond_to? :execution_log
controller.instance_variable_set(:"@before_ran", true)
end
@@ -383,7 +385,7 @@ class FilterTest < ActionController::TestCase
before_action(AuditFilter)
def show
- render :text => "hello"
+ render plain: "hello"
end
end
@@ -418,17 +420,17 @@ class FilterTest < ActionController::TestCase
class OutOfOrder < StandardError; end
before_action :first
- before_action :second, :only => :foo
+ before_action :second, only: :foo
def foo
- render :text => 'foo'
+ render plain: "foo"
end
def bar
- render :text => 'bar'
+ render plain: "bar"
end
- protected
+ private
def first
@first = true
end
@@ -442,7 +444,7 @@ class FilterTest < ActionController::TestCase
before_action :choose
%w(foo bar baz).each do |action|
- define_method(action) { render :text => action }
+ define_method(action) { render plain: action }
end
private
@@ -458,20 +460,20 @@ class FilterTest < ActionController::TestCase
def before_all
@ran_filter ||= []
- @ran_filter << 'before_all'
+ @ran_filter << "before_all"
end
def after_all
@ran_filter ||= []
- @ran_filter << 'after_all'
+ @ran_filter << "after_all"
end
def between_before_all_and_after_all
@ran_filter ||= []
- @ran_filter << 'between_before_all_and_after_all'
+ @ran_filter << "between_before_all_and_after_all"
end
def show
- render :text => 'hello'
+ render plain: "hello"
end
end
@@ -481,7 +483,7 @@ class FilterTest < ActionController::TestCase
def around(controller)
yield
rescue ErrorToRescue => ex
- controller.__send__ :render, :text => "I rescued this: #{ex.inspect}"
+ controller.__send__ :render, plain: "I rescued this: #{ex.inspect}"
end
end
@@ -494,50 +496,48 @@ class FilterTest < ActionController::TestCase
end
class NonYieldingAroundFilterController < ActionController::Base
-
before_action :filter_one
around_action :non_yielding_action
before_action :action_two
after_action :action_three
def index
- render :inline => "index"
+ render inline: "index"
end
private
def filter_one
- @filters ||= []
- @filters << "filter_one"
+ @filters ||= []
+ @filters << "filter_one"
end
def action_two
- @filters << "action_two"
+ @filters << "action_two"
end
def non_yielding_action
- @filters << "it didn't yield"
+ @filters << "it didn't yield"
end
def action_three
- @filters << "action_three"
+ @filters << "action_three"
end
-
end
class ImplicitActionsController < ActionController::Base
- before_action :find_only, :only => :edit
- before_action :find_except, :except => :edit
+ before_action :find_only, only: :edit
+ before_action :find_except, except: :edit
private
- def find_only
- @only = 'Only'
- end
+ def find_only
+ @only = "Only"
+ end
- def find_except
- @except = 'Except'
- end
+ def find_except
+ @except = "Except"
+ end
end
def test_non_yielding_around_actions_do_not_raise
@@ -586,7 +586,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)
@@ -611,12 +611,12 @@ class FilterTest < ActionController::TestCase
end
def test_if_is_ignored_when_used_with_only
- test_process(SkipFilterUsingOnlyAndIf, 'login')
+ test_process(SkipFilterUsingOnlyAndIf, "login")
assert_not @controller.instance_variable_defined?(:@ran_filter)
end
def test_except_is_ignored_when_used_with_if
- test_process(SkipFilterUsingIfAndExcept, 'login')
+ test_process(SkipFilterUsingIfAndExcept, "login")
assert_equal %w(ensure_login), @controller.instance_variable_get(:@ran_filter)
end
@@ -706,7 +706,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
@@ -745,13 +745,13 @@ class FilterTest < ActionController::TestCase
def test_actions_with_mixed_specialization_run_in_order
assert_nothing_raised do
- response = test_process(MixedSpecializationController, 'bar')
- assert_equal 'bar', response.body
+ response = test_process(MixedSpecializationController, "bar")
+ assert_equal "bar", response.body
end
assert_nothing_raised do
- response = test_process(MixedSpecializationController, 'foo')
- assert_equal 'foo', response.body
+ response = test_process(MixedSpecializationController, "foo")
+ assert_equal "foo", response.body
end
end
@@ -787,7 +787,7 @@ class FilterTest < ActionController::TestCase
assert_equal %w( ensure_login find_user ), @controller.instance_variable_get(:@ran_filter)
test_process(ConditionalSkippingController, "login")
- assert !@controller.instance_variable_defined?("@ran_after_action")
+ assert_not @controller.instance_variable_defined?("@ran_after_action")
test_process(ConditionalSkippingController, "change_password")
assert_equal %w( clean_up ), @controller.instance_variable_get("@ran_after_action")
end
@@ -795,7 +795,7 @@ class FilterTest < ActionController::TestCase
def test_conditional_skipping_of_actions_when_parent_action_is_also_conditional
test_process(ChildOfConditionalParentController)
assert_equal %w( conditional_in_parent_before conditional_in_parent_after ), @controller.instance_variable_get(:@ran_filter)
- test_process(ChildOfConditionalParentController, 'another_action')
+ test_process(ChildOfConditionalParentController, "another_action")
assert_not @controller.instance_variable_defined?(:@ran_filter)
end
@@ -819,20 +819,20 @@ class FilterTest < ActionController::TestCase
response = test_process(RescuedController)
end
- assert response.success?
+ assert_predicate response, :successful?
assert_equal("I rescued this: #<FilterTest::ErrorToRescue: Something made the bad noise.>", response.body)
end
def test_actions_obey_only_and_except_for_implicit_actions
- test_process(ImplicitActionsController, 'show')
- assert_equal 'Except', @controller.instance_variable_get(:@except)
+ test_process(ImplicitActionsController, "show")
+ assert_equal "Except", @controller.instance_variable_get(:@except)
assert_not @controller.instance_variable_defined?(:@only)
- assert_equal 'show', response.body
+ assert_equal "show", response.body
- test_process(ImplicitActionsController, 'edit')
- assert_equal 'Only', @controller.instance_variable_get(:@only)
+ test_process(ImplicitActionsController, "edit")
+ assert_equal "Only", @controller.instance_variable_get(:@only)
assert_not @controller.instance_variable_defined?(:@except)
- assert_equal 'edit', response.body
+ assert_equal "edit", response.body
end
private
@@ -859,14 +859,14 @@ class PostsController < ActionController::Base
private
def default_action
- render :inline => "#{action_name} called"
+ render inline: "#{action_name} called"
end
end
class ControllerWithSymbolAsFilter < PostsController
- around_action :raise_before, :only => :raises_before
- around_action :raise_after, :only => :raises_after
- around_action :without_exception, :only => :no_raise
+ around_action :raise_before, only: :raises_before
+ around_action :raise_after, only: :raises_after
+ around_action :without_exception, only: :no_raise
private
def raise_before
@@ -898,7 +898,7 @@ class ControllerWithFilterClass < PostsController
end
end
- around_action YieldingFilter, :only => :raises_after
+ around_action YieldingFilter, only: :raises_after
end
class ControllerWithFilterInstance < PostsController
@@ -909,11 +909,11 @@ class ControllerWithFilterInstance < PostsController
end
end
- around_action YieldingFilter.new, :only => :raises_after
+ around_action YieldingFilter.new, only: :raises_after
end
class ControllerWithProcFilter < PostsController
- around_action(:only => :no_raise) do |c,b|
+ around_action(only: :no_raise) do |c, b|
c.instance_variable_set(:"@before", true)
b.call
c.instance_variable_set(:"@after", true)
@@ -921,7 +921,7 @@ class ControllerWithProcFilter < PostsController
end
class ControllerWithNestedFilters < ControllerWithSymbolAsFilter
- around_action :raise_before, :raise_after, :without_exception, :only => :raises_both
+ around_action :raise_before, :raise_after, :without_exception, only: :raises_both
end
class ControllerWithAllTypesOfFilters < PostsController
@@ -931,26 +931,26 @@ class ControllerWithAllTypesOfFilters < PostsController
around_action :around_again
private
- def before
- @ran_filter ||= []
- @ran_filter << 'before'
- end
+ def before
+ @ran_filter ||= []
+ @ran_filter << "before"
+ end
- def around
- @ran_filter << 'around (before yield)'
- yield
- @ran_filter << 'around (after yield)'
- end
+ def around
+ @ran_filter << "around (before yield)"
+ yield
+ @ran_filter << "around (after yield)"
+ end
- def after
- @ran_filter << 'after'
- end
+ def after
+ @ran_filter << "after"
+ end
- def around_again
- @ran_filter << 'around_again (before yield)'
- yield
- @ran_filter << 'around_again (after yield)'
- end
+ def around_again
+ @ran_filter << "around_again (before yield)"
+ yield
+ @ran_filter << "around_again (after yield)"
+ end
end
class ControllerWithTwoLessFilters < ControllerWithAllTypesOfFilters
@@ -958,46 +958,39 @@ class ControllerWithTwoLessFilters < ControllerWithAllTypesOfFilters
skip_after_action :after
end
-class SkipFilterUsingSkipActionCallback < ControllerWithAllTypesOfFilters
- ActiveSupport::Deprecation.silence do
- skip_action_callback :around_again
- skip_action_callback :after
- end
-end
-
class YieldingAroundFiltersTest < ActionController::TestCase
include PostsController::AroundExceptions
def test_base
controller = PostsController
- assert_nothing_raised { test_process(controller,'no_raise') }
- assert_nothing_raised { test_process(controller,'raises_before') }
- assert_nothing_raised { test_process(controller,'raises_after') }
- assert_nothing_raised { test_process(controller,'no_action') }
+ assert_nothing_raised { test_process(controller, "no_raise") }
+ assert_nothing_raised { test_process(controller, "raises_before") }
+ assert_nothing_raised { test_process(controller, "raises_after") }
+ assert_nothing_raised { test_process(controller, "no_action") }
end
def test_with_symbol
controller = ControllerWithSymbolAsFilter
- assert_nothing_raised { test_process(controller,'no_raise') }
- assert_raise(Before) { test_process(controller,'raises_before') }
- assert_raise(After) { test_process(controller,'raises_after') }
- assert_nothing_raised { test_process(controller,'no_raise') }
+ assert_nothing_raised { test_process(controller, "no_raise") }
+ assert_raise(Before) { test_process(controller, "raises_before") }
+ assert_raise(After) { test_process(controller, "raises_after") }
+ assert_nothing_raised { test_process(controller, "no_raise") }
end
def test_with_class
controller = ControllerWithFilterClass
- assert_nothing_raised { test_process(controller,'no_raise') }
- assert_raise(After) { test_process(controller,'raises_after') }
+ assert_nothing_raised { test_process(controller, "no_raise") }
+ assert_raise(After) { test_process(controller, "raises_after") }
end
def test_with_instance
controller = ControllerWithFilterInstance
- assert_nothing_raised { test_process(controller,'no_raise') }
- assert_raise(After) { test_process(controller,'raises_after') }
+ assert_nothing_raised { test_process(controller, "no_raise") }
+ assert_raise(After) { test_process(controller, "raises_after") }
end
def test_with_proc
- test_process(ControllerWithProcFilter,'no_raise')
+ test_process(ControllerWithProcFilter, "no_raise")
assert @controller.instance_variable_get(:@before)
assert @controller.instance_variable_get(:@after)
end
@@ -1006,71 +999,50 @@ class YieldingAroundFiltersTest < ActionController::TestCase
controller = ControllerWithNestedFilters
assert_nothing_raised do
begin
- test_process(controller,'raises_both')
+ test_process(controller, "raises_both")
rescue Before, After
end
end
assert_raise Before do
begin
- test_process(controller,'raises_both')
+ test_process(controller, "raises_both")
rescue After
end
end
end
def test_action_order_with_all_action_types
- test_process(ControllerWithAllTypesOfFilters,'no_raise')
- assert_equal 'before around (before yield) around_again (before yield) around_again (after yield) after around (after yield)', @controller.instance_variable_get(:@ran_filter).join(' ')
+ test_process(ControllerWithAllTypesOfFilters, "no_raise")
+ assert_equal "before around (before yield) around_again (before yield) around_again (after yield) after around (after yield)", @controller.instance_variable_get(:@ran_filter).join(" ")
end
def test_action_order_with_skip_action_method
- test_process(ControllerWithTwoLessFilters,'no_raise')
- assert_equal 'before around (before yield) around (after yield)', @controller.instance_variable_get(:@ran_filter).join(' ')
+ test_process(ControllerWithTwoLessFilters, "no_raise")
+ assert_equal "before around (before yield) around (after yield)", @controller.instance_variable_get(:@ran_filter).join(" ")
end
def test_first_action_in_multiple_before_action_chain_halts
controller = ::FilterTest::TestMultipleFiltersController.new
- response = test_process(controller, 'fail_1')
- assert_equal '', response.body
+ response = test_process(controller, "fail_1")
+ assert_equal "", response.body
assert_equal 1, controller.instance_variable_get(:@try)
end
def test_second_action_in_multiple_before_action_chain_halts
controller = ::FilterTest::TestMultipleFiltersController.new
- response = test_process(controller, 'fail_2')
- assert_equal '', response.body
+ response = test_process(controller, "fail_2")
+ assert_equal "", response.body
assert_equal 2, controller.instance_variable_get(:@try)
end
def test_last_action_in_multiple_before_action_chain_halts
controller = ::FilterTest::TestMultipleFiltersController.new
- response = test_process(controller, 'fail_3')
- assert_equal '', response.body
+ response = test_process(controller, "fail_3")
+ assert_equal "", response.body
assert_equal 3, controller.instance_variable_get(:@try)
end
- def test_skipping_with_skip_action_callback
- test_process(SkipFilterUsingSkipActionCallback,'no_raise')
- assert_equal 'before around (before yield) around (after yield)', @controller.instance_variable_get(:@ran_filter).join(' ')
- end
-
- def test_deprecated_skip_action_callback
- assert_deprecated do
- Class.new(PostsController) do
- skip_action_callback :clean_up
- end
- end
- end
-
- def test_deprecated_skip_filter
- assert_deprecated do
- Class.new(PostsController) do
- skip_filter :clean_up
- end
- end
- 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 081288ef21..e3ec5bb7fc 100644
--- a/actionpack/test/controller/flash_hash_test.rb
+++ b/actionpack/test/controller/flash_hash_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module ActionDispatch
class FlashHashTest < ActiveSupport::TestCase
@@ -7,116 +9,116 @@ module ActionDispatch
end
def test_set_get
- @hash[:foo] = 'zomg'
- assert_equal 'zomg', @hash[:foo]
+ @hash[:foo] = "zomg"
+ assert_equal "zomg", @hash[:foo]
end
def test_keys
assert_equal [], @hash.keys
- @hash['foo'] = 'zomg'
- assert_equal ['foo'], @hash.keys
+ @hash["foo"] = "zomg"
+ assert_equal ["foo"], @hash.keys
- @hash['bar'] = 'zomg'
- assert_equal ['foo', 'bar'].sort, @hash.keys.sort
+ @hash["bar"] = "zomg"
+ assert_equal ["foo", "bar"].sort, @hash.keys.sort
end
def test_update
- @hash['foo'] = 'bar'
- @hash.update('foo' => 'baz', 'hello' => 'world')
+ @hash["foo"] = "bar"
+ @hash.update("foo" => "baz", "hello" => "world")
- assert_equal 'baz', @hash['foo']
- assert_equal 'world', @hash['hello']
+ assert_equal "baz", @hash["foo"]
+ assert_equal "world", @hash["hello"]
end
def test_key
- @hash['foo'] = 'bar'
+ @hash["foo"] = "bar"
- assert @hash.key?('foo')
+ assert @hash.key?("foo")
assert @hash.key?(:foo)
- assert_not @hash.key?('bar')
+ assert_not @hash.key?("bar")
assert_not @hash.key?(:bar)
end
def test_delete
- @hash['foo'] = 'bar'
- @hash.delete 'foo'
+ @hash["foo"] = "bar"
+ @hash.delete "foo"
- assert !@hash.key?('foo')
- assert_nil @hash['foo']
+ assert_not @hash.key?("foo")
+ assert_nil @hash["foo"]
end
def test_to_hash
- @hash['foo'] = 'bar'
- assert_equal({'foo' => 'bar'}, @hash.to_hash)
+ @hash["foo"] = "bar"
+ assert_equal({ "foo" => "bar" }, @hash.to_hash)
- @hash.to_hash['zomg'] = 'aaron'
- assert !@hash.key?('zomg')
- assert_equal({'foo' => 'bar'}, @hash.to_hash)
+ @hash.to_hash["zomg"] = "aaron"
+ assert_not @hash.key?("zomg")
+ assert_equal({ "foo" => "bar" }, @hash.to_hash)
end
def test_to_session_value
- @hash['foo'] = 'bar'
- assert_equal({'flashes' => {'foo' => 'bar'}}, @hash.to_session_value)
+ @hash["foo"] = "bar"
+ assert_equal({ "discard" => [], "flashes" => { "foo" => "bar" } }, @hash.to_session_value)
- @hash.now['qux'] = 1
- assert_equal({'flashes' => {'foo' => 'bar'}}, @hash.to_session_value)
+ @hash.now["qux"] = 1
+ assert_equal({ "flashes" => { "foo" => "bar" }, "discard" => [] }, @hash.to_session_value)
- @hash.discard('foo')
- assert_equal(nil, @hash.to_session_value)
+ @hash.discard("foo")
+ 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
# {"session_id"=>"f8e1b8152ba7609c28bbb17ec9263ba7", "flash"=>#<ActionDispatch::Flash::FlashHash:0x00000000000000 @used=#<Set: {"farewell"}>, @closed=false, @flashes={"greeting"=>"Hello", "farewell"=>"Goodbye"}, @now=nil>}
- rails_3_2_cookie = 'BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJWY4ZTFiODE1MmJhNzYwOWMyOGJiYjE3ZWM5MjYzYmE3BjsAVEkiCmZsYXNoBjsARm86JUFjdGlvbkRpc3BhdGNoOjpGbGFzaDo6Rmxhc2hIYXNoCToKQHVzZWRvOghTZXQGOgpAaGFzaHsGSSINZmFyZXdlbGwGOwBUVDoMQGNsb3NlZEY6DUBmbGFzaGVzewdJIg1ncmVldGluZwY7AFRJIgpIZWxsbwY7AFRJIg1mYXJld2VsbAY7AFRJIgxHb29kYnllBjsAVDoJQG5vdzA='
+ rails_3_2_cookie = "BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJWY4ZTFiODE1MmJhNzYwOWMyOGJiYjE3ZWM5MjYzYmE3BjsAVEkiCmZsYXNoBjsARm86JUFjdGlvbkRpc3BhdGNoOjpGbGFzaDo6Rmxhc2hIYXNoCToKQHVzZWRvOghTZXQGOgpAaGFzaHsGSSINZmFyZXdlbGwGOwBUVDoMQGNsb3NlZEY6DUBmbGFzaGVzewdJIg1ncmVldGluZwY7AFRJIgpIZWxsbwY7AFRJIg1mYXJld2VsbAY7AFRJIgxHb29kYnllBjsAVDoJQG5vdzA="
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)
+ hash = Flash::FlashHash.from_session_value(session["flash"])
+ assert_equal({ "greeting" => "Hello" }, hash.to_hash)
+ assert_nil(hash.to_session_value)
end
def test_from_session_value_on_json_serializer
decrypted_data = "{ \"session_id\":\"d98bdf6d129618fc2548c354c161cfb5\", \"flash\":{\"discard\":[\"farewell\"], \"flashes\":{\"greeting\":\"Hello\",\"farewell\":\"Goodbye\"}} }"
session = ActionDispatch::Cookies::JsonSerializer.load(decrypted_data)
- hash = Flash::FlashHash.from_session_value(session['flash'])
+ hash = Flash::FlashHash.from_session_value(session["flash"])
- assert_equal({'greeting' => 'Hello'}, hash.to_hash)
- assert_equal(nil, hash.to_session_value)
+ assert_equal({ "greeting" => "Hello" }, hash.to_hash)
+ assert_nil(hash.to_session_value)
assert_equal "Hello", hash[:greeting]
assert_equal "Hello", hash["greeting"]
end
def test_empty?
- assert @hash.empty?
- @hash['zomg'] = 'bears'
- assert !@hash.empty?
+ assert_empty @hash
+ @hash["zomg"] = "bears"
+ assert_not_empty @hash
@hash.clear
- assert @hash.empty?
+ assert_empty @hash
end
def test_each
- @hash['hello'] = 'world'
- @hash['foo'] = 'bar'
+ @hash["hello"] = "world"
+ @hash["foo"] = "bar"
things = []
- @hash.each do |k,v|
- things << [k,v]
+ @hash.each do |k, v|
+ things << [k, v]
end
assert_equal([%w{ hello world }, %w{ foo bar }].sort, things.sort)
end
def test_replace
- @hash['hello'] = 'world'
- @hash.replace('omg' => 'aaron')
- assert_equal({'omg' => 'aaron'}, @hash.to_hash)
+ @hash["hello"] = "world"
+ @hash.replace("omg" => "aaron")
+ assert_equal({ "omg" => "aaron" }, @hash.to_hash)
end
def test_discard_no_args
- @hash['hello'] = 'world'
+ @hash["hello"] = "world"
@hash.discard
@hash.sweep
@@ -124,49 +126,49 @@ module ActionDispatch
end
def test_discard_one_arg
- @hash['hello'] = 'world'
- @hash['omg'] = 'world'
- @hash.discard 'hello'
+ @hash["hello"] = "world"
+ @hash["omg"] = "world"
+ @hash.discard "hello"
@hash.sweep
- assert_equal({'omg' => 'world'}, @hash.to_hash)
+ assert_equal({ "omg" => "world" }, @hash.to_hash)
end
def test_keep_sweep
- @hash['hello'] = 'world'
+ @hash["hello"] = "world"
@hash.sweep
- assert_equal({'hello' => 'world'}, @hash.to_hash)
+ assert_equal({ "hello" => "world" }, @hash.to_hash)
end
def test_update_sweep
- @hash['hello'] = 'world'
- @hash.update({'hi' => 'mom'})
+ @hash["hello"] = "world"
+ @hash.update("hi" => "mom")
@hash.sweep
- assert_equal({'hello' => 'world', 'hi' => 'mom'}, @hash.to_hash)
+ assert_equal({ "hello" => "world", "hi" => "mom" }, @hash.to_hash)
end
def test_update_delete_sweep
- @hash['hello'] = 'world'
- @hash.delete 'hello'
- @hash.update({'hello' => 'mom'})
+ @hash["hello"] = "world"
+ @hash.delete "hello"
+ @hash.update("hello" => "mom")
@hash.sweep
- assert_equal({'hello' => 'mom'}, @hash.to_hash)
+ assert_equal({ "hello" => "mom" }, @hash.to_hash)
end
def test_delete_sweep
- @hash['hello'] = 'world'
- @hash['hi'] = 'mom'
- @hash.delete 'hi'
+ @hash["hello"] = "world"
+ @hash["hi"] = "mom"
+ @hash.delete "hi"
@hash.sweep
- assert_equal({'hello' => 'world'}, @hash.to_hash)
+ assert_equal({ "hello" => "world" }, @hash.to_hash)
end
def test_clear_sweep
- @hash['hello'] = 'world'
+ @hash["hello"] = "world"
@hash.clear
@hash.sweep
@@ -174,38 +176,38 @@ module ActionDispatch
end
def test_replace_sweep
- @hash['hello'] = 'world'
- @hash.replace({'hi' => 'mom'})
+ @hash["hello"] = "world"
+ @hash.replace("hi" => "mom")
@hash.sweep
- assert_equal({'hi' => 'mom'}, @hash.to_hash)
+ assert_equal({ "hi" => "mom" }, @hash.to_hash)
end
def test_discard_then_add
- @hash['hello'] = 'world'
- @hash['omg'] = 'world'
- @hash.discard 'hello'
- @hash['hello'] = 'world'
+ @hash["hello"] = "world"
+ @hash["omg"] = "world"
+ @hash.discard "hello"
+ @hash["hello"] = "world"
@hash.sweep
- assert_equal({'omg' => 'world', 'hello' => 'world'}, @hash.to_hash)
+ assert_equal({ "omg" => "world", "hello" => "world" }, @hash.to_hash)
end
def test_keep_all_sweep
- @hash['hello'] = 'world'
- @hash['omg'] = 'world'
- @hash.discard 'hello'
+ @hash["hello"] = "world"
+ @hash["omg"] = "world"
+ @hash.discard "hello"
@hash.keep
@hash.sweep
- assert_equal({'omg' => 'world', 'hello' => 'world'}, @hash.to_hash)
+ assert_equal({ "omg" => "world", "hello" => "world" }, @hash.to_hash)
end
def test_double_sweep
- @hash['hello'] = 'world'
+ @hash["hello"] = "world"
@hash.sweep
- assert_equal({'hello' => 'world'}, @hash.to_hash)
+ assert_equal({ "hello" => "world" }, @hash.to_hash)
@hash.sweep
assert_equal({}, @hash.to_hash)
diff --git a/actionpack/test/controller/flash_test.rb b/actionpack/test/controller/flash_test.rb
index 64543f0659..409a4ec2e6 100644
--- a/actionpack/test/controller/flash_test.rb
+++ b/actionpack/test/controller/flash_test.rb
@@ -1,11 +1,13 @@
-require 'abstract_unit'
-require 'active_support/key_generator'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/messages/rotation_configuration"
class FlashTest < ActionController::TestCase
class TestController < ActionController::Base
def set_flash
flash["that"] = "hello"
- render :inline => "hello"
+ render inline: "hello"
end
def set_flash_now
@@ -14,32 +16,32 @@ class FlashTest < ActionController::TestCase
flash.now["foo"] ||= "err"
@flashy = flash.now["that"]
@flash_copy = {}.update flash
- render :inline => "hello"
+ render inline: "hello"
end
def attempt_to_use_flash_now
@flash_copy = {}.update flash
@flashy = flash["that"]
- render :inline => "hello"
+ render inline: "hello"
end
def use_flash
@flash_copy = {}.update flash
@flashy = flash["that"]
- render :inline => "hello"
+ render inline: "hello"
end
def use_flash_and_keep_it
@flash_copy = {}.update flash
@flashy = flash["that"]
flash.keep
- render :inline => "hello"
+ render inline: "hello"
end
def use_flash_and_update_it
flash.update("this" => "hello again")
@flash_copy = {}.update flash
- render :inline => "hello"
+ render inline: "hello"
end
def use_flash_after_reset_session
@@ -49,11 +51,11 @@ class FlashTest < ActionController::TestCase
@flashy_that_reset = flash["that"]
flash["this"] = "good-bye"
@flashy_this = flash["this"]
- render :inline => "hello"
+ render inline: "hello"
end
# methods for test_sweep_after_halted_action_chain
- before_action :halt_and_redir, only: 'filter_halting_action'
+ before_action :halt_and_redir, only: "filter_halting_action"
def std_action
@flash_copy = {}.update(flash)
@@ -66,34 +68,34 @@ class FlashTest < ActionController::TestCase
def halt_and_redir
flash["foo"] = "bar"
- redirect_to :action => "std_action"
+ redirect_to action: "std_action"
@flash_copy = {}.update(flash)
end
def redirect_with_alert
- redirect_to '/nowhere', :alert => "Beware the nowheres!"
+ redirect_to "/nowhere", alert: "Beware the nowheres!"
end
def redirect_with_notice
- redirect_to '/somewhere', :notice => "Good luck in the somewheres!"
+ redirect_to "/somewhere", notice: "Good luck in the somewheres!"
end
def render_with_flash_now_alert
flash.now.alert = "Beware the nowheres now!"
- render :inline => "hello"
+ render inline: "hello"
end
def render_with_flash_now_notice
flash.now.notice = "Good luck in the somewheres now!"
- render :inline => "hello"
+ render inline: "hello"
end
def redirect_with_other_flashes
- redirect_to '/wonderland', :flash => { :joyride => "Horses!" }
+ redirect_to "/wonderland", flash: { joyride: "Horses!" }
end
def redirect_with_foo_flash
- redirect_to "/wonderland", :foo => 'for great justice'
+ redirect_to "/wonderland", foo: "for great justice"
end
end
@@ -172,17 +174,17 @@ class FlashTest < ActionController::TestCase
def test_keep_and_discard_return_values
flash = ActionDispatch::Flash::FlashHash.new
- flash.update(:foo => :foo_indeed, :bar => :bar_indeed)
+ flash.update(foo: :foo_indeed, bar: :bar_indeed)
assert_equal(:foo_indeed, flash.discard(:foo)) # valid key passed
assert_nil flash.discard(:unknown) # non existent key passed
- assert_equal({"foo" => :foo_indeed, "bar" => :bar_indeed}, flash.discard().to_hash) # nothing passed
- assert_equal({"foo" => :foo_indeed, "bar" => :bar_indeed}, flash.discard(nil).to_hash) # nothing passed
+ assert_equal({ "foo" => :foo_indeed, "bar" => :bar_indeed }, flash.discard().to_hash) # nothing passed
+ assert_equal({ "foo" => :foo_indeed, "bar" => :bar_indeed }, flash.discard(nil).to_hash) # nothing passed
assert_equal(:foo_indeed, flash.keep(:foo)) # valid key passed
assert_nil flash.keep(:unknown) # non existent key passed
- assert_equal({"foo" => :foo_indeed, "bar" => :bar_indeed}, flash.keep().to_hash) # nothing passed
- assert_equal({"foo" => :foo_indeed, "bar" => :bar_indeed}, flash.keep(nil).to_hash) # nothing passed
+ assert_equal({ "foo" => :foo_indeed, "bar" => :bar_indeed }, flash.keep().to_hash) # nothing passed
+ assert_equal({ "foo" => :foo_indeed, "bar" => :bar_indeed }, flash.keep(nil).to_hash) # nothing passed
end
def test_redirect_to_with_alert
@@ -227,7 +229,7 @@ class FlashTest < ActionController::TestCase
add_flash_types :foo
end
subclass_controller_with_no_flash_type = Class.new(test_controller_with_flash_type_foo)
- assert subclass_controller_with_no_flash_type._flash_types.include?(:foo)
+ assert_includes subclass_controller_with_no_flash_type._flash_types, :foo
end
def test_does_not_add_flash_type_to_parent_class
@@ -239,8 +241,9 @@ class FlashTest < ActionController::TestCase
end
class FlashIntegrationTest < ActionDispatch::IntegrationTest
- SessionKey = '_myapp_session'
- Generator = ActiveSupport::LegacyKeyGenerator.new('b3c631c314c0bbca50c1b2843150fe33')
+ SessionKey = "_myapp_session"
+ Generator = ActiveSupport::LegacyKeyGenerator.new("b3c631c314c0bbca50c1b2843150fe33")
+ Rotations = ActiveSupport::Messages::RotationConfiguration.new
class TestController < ActionController::Base
add_flash_types :bar
@@ -256,22 +259,29 @@ class FlashIntegrationTest < ActionDispatch::IntegrationTest
end
def use_flash
- render :inline => "flash: #{flash["that"]}"
+ render inline: "flash: #{flash["that"]}"
end
def set_bar
flash[:bar] = "for great justice"
head :ok
end
+
+ def set_flash_optionally
+ flash.now.notice = params[:flash]
+ if stale? etag: "abe"
+ render inline: "maybe flash"
+ end
+ end
end
def test_flash
with_test_route_set do
- get '/set_flash'
+ get "/set_flash"
assert_response :success
assert_equal "hello", @request.flash["that"]
- get '/use_flash'
+ get "/use_flash"
assert_response :success
assert_equal "flash: hello", @response.body
end
@@ -279,7 +289,7 @@ class FlashIntegrationTest < ActionDispatch::IntegrationTest
def test_just_using_flash_does_not_stream_a_cookie_back
with_test_route_set do
- get '/use_flash'
+ get "/use_flash"
assert_response :success
assert_nil @response.headers["Set-Cookie"]
assert_equal "flash: ", @response.body
@@ -288,28 +298,65 @@ class FlashIntegrationTest < ActionDispatch::IntegrationTest
def test_setting_flash_does_not_raise_in_following_requests
with_test_route_set do
- env = { 'action_dispatch.request.flash_hash' => ActionDispatch::Flash::FlashHash.new }
- get '/set_flash', env: env
- get '/set_flash', env: env
+ env = { "action_dispatch.request.flash_hash" => ActionDispatch::Flash::FlashHash.new }
+ get "/set_flash", env: env
+ get "/set_flash", env: env
end
end
def test_setting_flash_now_does_not_raise_in_following_requests
with_test_route_set do
- env = { 'action_dispatch.request.flash_hash' => ActionDispatch::Flash::FlashHash.new }
- get '/set_flash_now', env: env
- get '/set_flash_now', env: env
+ env = { "action_dispatch.request.flash_hash" => ActionDispatch::Flash::FlashHash.new }
+ get "/set_flash_now", env: env
+ get "/set_flash_now", env: env
end
end
def test_added_flash_types_method
with_test_route_set do
- get '/set_bar'
+ get "/set_bar"
assert_response :success
- assert_equal 'for great justice', @controller.bar
+ assert_equal "for great justice", @controller.bar
end
end
+ def test_flash_factored_into_etag
+ with_test_route_set do
+ get "/set_flash_optionally"
+ no_flash_etag = response.etag
+
+ get "/set_flash_optionally", params: { flash: "hello!" }
+ hello_flash_etag = response.etag
+
+ assert_not_equal no_flash_etag, hello_flash_etag
+
+ get "/set_flash_optionally", params: { flash: "hello!" }
+ another_hello_flash_etag = response.etag
+
+ assert_equal another_hello_flash_etag, hello_flash_etag
+
+ get "/set_flash_optionally", params: { flash: "goodbye!" }
+ goodbye_flash_etag = response.etag
+
+ assert_not_equal another_hello_flash_etag, goodbye_flash_etag
+ end
+ end
+
+ def test_flash_usable_in_metal_without_helper
+ controller_class = nil
+
+ assert_nothing_raised do
+ controller_class = Class.new(ActionController::Metal) do
+ include ActionController::Flash
+ end
+ end
+
+ controller = controller_class.new
+
+ assert_respond_to controller, :alert
+ assert_respond_to controller, :notice
+ end
+
private
# Overwrite get to send SessionSecret in env hash
@@ -317,19 +364,22 @@ class FlashIntegrationTest < ActionDispatch::IntegrationTest
args[0] ||= {}
args[0][:env] ||= {}
args[0][:env]["action_dispatch.key_generator"] ||= Generator
+ args[0][:env]["action_dispatch.cookies_rotations"] = Rotations
super(path, *args)
end
def with_test_route_set
with_routing do |set|
set.draw do
- get ':action', :to => FlashIntegrationTest::TestController
+ ActiveSupport::Deprecation.silence do
+ get ":action", to: FlashIntegrationTest::TestController
+ end
end
@app = self.class.build_app(set) do |middleware|
- middleware.use ActionDispatch::Session::CookieStore, :key => SessionKey
+ middleware.use ActionDispatch::Session::CookieStore, key: SessionKey
middleware.use ActionDispatch::Flash
- middleware.delete "ActionDispatch::ShowExceptions"
+ middleware.delete ActionDispatch::ShowExceptions
end
yield
diff --git a/actionpack/test/controller/force_ssl_test.rb b/actionpack/test/controller/force_ssl_test.rb
index 72ae30eb39..7f59f6acaf 100644
--- a/actionpack/test/controller/force_ssl_test.rb
+++ b/actionpack/test/controller/force_ssl_test.rb
@@ -1,32 +1,38 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class ForceSSLController < ActionController::Base
def banana
- render :text => "monkey"
+ render plain: "monkey"
end
def cheeseburger
- render :text => "sikachu"
+ render plain: "sikachu"
end
end
class ForceSSLControllerLevel < ForceSSLController
- force_ssl
+ ActiveSupport::Deprecation.silence do
+ force_ssl
+ end
end
class ForceSSLCustomOptions < ForceSSLController
- force_ssl :host => "secure.example.com", :only => :redirect_host
- force_ssl :port => 8443, :only => :redirect_port
- force_ssl :subdomain => 'secure', :only => :redirect_subdomain
- force_ssl :domain => 'secure.com', :only => :redirect_domain
- force_ssl :path => '/foo', :only => :redirect_path
- force_ssl :status => :found, :only => :redirect_status
- force_ssl :flash => { :message => 'Foo, Bar!' }, :only => :redirect_flash
- force_ssl :alert => 'Foo, Bar!', :only => :redirect_alert
- force_ssl :notice => 'Foo, Bar!', :only => :redirect_notice
+ ActiveSupport::Deprecation.silence do
+ force_ssl host: "secure.example.com", only: :redirect_host
+ force_ssl port: 8443, only: :redirect_port
+ force_ssl subdomain: "secure", only: :redirect_subdomain
+ force_ssl domain: "secure.com", only: :redirect_domain
+ force_ssl path: "/foo", only: :redirect_path
+ force_ssl status: :found, only: :redirect_status
+ force_ssl flash: { message: "Foo, Bar!" }, only: :redirect_flash
+ force_ssl alert: "Foo, Bar!", only: :redirect_alert
+ force_ssl notice: "Foo, Bar!", only: :redirect_notice
+ end
def force_ssl_action
- render :text => action_name
+ render plain: action_name
end
alias_method :redirect_host, :force_ssl_action
@@ -40,55 +46,63 @@ class ForceSSLCustomOptions < ForceSSLController
alias_method :redirect_notice, :force_ssl_action
def use_flash
- render :text => flash[:message]
+ render plain: flash[:message]
end
def use_alert
- render :text => flash[:alert]
+ render plain: flash[:alert]
end
def use_notice
- render :text => flash[:notice]
+ render plain: flash[:notice]
end
end
class ForceSSLOnlyAction < ForceSSLController
- force_ssl :only => :cheeseburger
+ ActiveSupport::Deprecation.silence do
+ force_ssl only: :cheeseburger
+ end
end
class ForceSSLExceptAction < ForceSSLController
- force_ssl :except => :banana
+ ActiveSupport::Deprecation.silence do
+ force_ssl except: :banana
+ end
end
class ForceSSLIfCondition < ForceSSLController
- force_ssl :if => :use_force_ssl?
+ ActiveSupport::Deprecation.silence do
+ force_ssl if: :use_force_ssl?
+ end
def use_force_ssl?
- action_name == 'cheeseburger'
+ action_name == "cheeseburger"
end
end
class ForceSSLFlash < ForceSSLController
- force_ssl :except => [:banana, :set_flash, :use_flash]
+ ActiveSupport::Deprecation.silence do
+ force_ssl except: [:banana, :set_flash, :use_flash]
+ end
def set_flash
flash["that"] = "hello"
- redirect_to '/force_ssl_flash/cheeseburger'
+ redirect_to "/force_ssl_flash/cheeseburger"
end
def use_flash
@flash_copy = {}.update flash
@flashy = flash["that"]
- render :inline => "hello"
+ render inline: "hello"
end
end
class RedirectToSSL < ForceSSLController
def banana
- force_ssl_redirect || render(:text => 'monkey')
+ force_ssl_redirect || render(plain: "monkey")
end
def cheeseburger
- force_ssl_redirect('secure.cheeseburger.host') || render(:text => 'ihaz')
+ force_ssl_redirect("secure.cheeseburger.host") || render(plain: "ihaz")
end
end
@@ -114,7 +128,7 @@ end
class ForceSSLCustomOptionsTest < ActionController::TestCase
def setup
- @request.env['HTTP_HOST'] = 'www.example.com:80'
+ @request.env["HTTP_HOST"] = "www.example.com:80"
end
def test_redirect_to_custom_host
@@ -229,15 +243,13 @@ class ForceSSLFlashTest < ActionController::TestCase
assert_response 302
assert_equal "http://test.host/force_ssl_flash/cheeseburger", redirect_to_url
- # FIXME: AC::TestCase#build_request_uri doesn't build a new uri if PATH_INFO exists
- @request.env.delete('PATH_INFO')
+ @request.env.delete("PATH_INFO")
get :cheeseburger
assert_response 301
assert_equal "https://test.host/force_ssl_flash/cheeseburger", redirect_to_url
- # FIXME: AC::TestCase#build_request_uri doesn't build a new uri if PATH_INFO exists
- @request.env.delete('PATH_INFO')
+ @request.env.delete("PATH_INFO")
get :use_flash
assert_equal "hello", @controller.instance_variable_get("@flash_copy")["that"]
@@ -251,15 +263,15 @@ class ForceSSLDuplicateRoutesTest < ActionController::TestCase
def test_force_ssl_redirects_to_same_path
with_routing do |set|
set.draw do
- get '/foo', :to => 'force_ssl_controller_level#banana'
- get '/bar', :to => 'force_ssl_controller_level#banana'
+ get "/foo", to: "force_ssl_controller_level#banana"
+ get "/bar", to: "force_ssl_controller_level#banana"
end
- @request.env['PATH_INFO'] = '/bar'
+ @request.env["PATH_INFO"] = "/bar"
get :banana
assert_response 301
- assert_equal 'https://test.host/bar', redirect_to_url
+ assert_equal "https://test.host/bar", redirect_to_url
end
end
end
@@ -270,12 +282,12 @@ class ForceSSLFormatTest < ActionController::TestCase
def test_force_ssl_redirects_to_same_format
with_routing do |set|
set.draw do
- get '/foo', :to => 'force_ssl_controller_level#banana'
+ get "/foo", to: "force_ssl_controller_level#banana"
end
get :banana, format: :json
assert_response 301
- assert_equal 'https://test.host/foo.json', redirect_to_url
+ assert_equal "https://test.host/foo.json", redirect_to_url
end
end
end
@@ -286,18 +298,18 @@ class ForceSSLOptionalSegmentsTest < ActionController::TestCase
def test_force_ssl_redirects_to_same_format
with_routing do |set|
set.draw do
- scope '(:locale)' do
- defaults :locale => 'en' do
- get '/foo', :to => 'force_ssl_controller_level#banana'
+ scope "(:locale)" do
+ defaults locale: "en" do
+ get "/foo", to: "force_ssl_controller_level#banana"
end
end
end
- @request.env['PATH_INFO'] = '/en/foo'
- get :banana, params: { locale: 'en' }
- assert_equal 'en', @controller.params[:locale]
+ @request.env["PATH_INFO"] = "/en/foo"
+ get :banana, params: { locale: "en" }
+ assert_equal "en", @controller.params[:locale]
assert_response 301
- assert_equal 'https://test.host/en/foo', redirect_to_url
+ assert_equal "https://test.host/en/foo", redirect_to_url
end
end
end
@@ -316,9 +328,18 @@ class RedirectToSSLTest < ActionController::TestCase
end
def test_cheeseburgers_does_not_redirect_if_already_https
- request.env['HTTPS'] = 'on'
+ request.env["HTTPS"] = "on"
+ get :cheeseburger
+ assert_response 200
+ assert_equal "ihaz", response.body
+ end
+end
+
+class ForceSSLControllerLevelTest < ActionController::TestCase
+ def test_no_redirect_websocket_ssl_request
+ request.env["rack.url_scheme"] = "wss"
+ request.env["Upgrade"] = "websocket"
get :cheeseburger
assert_response 200
- assert_equal 'ihaz', response.body
end
end
diff --git a/actionpack/test/controller/form_builder_test.rb b/actionpack/test/controller/form_builder_test.rb
index 99eeaf9ab6..2db0834c5e 100644
--- a/actionpack/test/controller/form_builder_test.rb
+++ b/actionpack/test/controller/form_builder_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class FormBuilderController < ActionController::Base
class SpecializedFormBuilder < ActionView::Helpers::FormBuilder ; end
diff --git a/actionpack/test/controller/helper_test.rb b/actionpack/test/controller/helper_test.rb
index a96cfa3bf4..de8072a994 100644
--- a/actionpack/test/controller/helper_test.rb
+++ b/actionpack/test/controller/helper_test.rb
@@ -1,17 +1,19 @@
-require 'abstract_unit'
+# frozen_string_literal: true
-ActionController::Base.helpers_path = File.expand_path('../../fixtures/helpers', __FILE__)
+require "abstract_unit"
+
+ActionController::Base.helpers_path = File.expand_path("../fixtures/helpers", __dir__)
module Fun
class GamesController < ActionController::Base
def render_hello_world
- render :inline => "hello: <%= stratego %>"
+ render inline: "hello: <%= stratego %>"
end
end
class PdfController < ActionController::Base
def test
- render :inline => "test: <%= foobar %>"
+ render inline: "test: <%= foobar %>"
end
end
end
@@ -35,11 +37,11 @@ class JustMeController < ActionController::Base
clear_helpers
def flash
- render :inline => "<h1><%= notice %></h1>"
+ render inline: "<h1><%= notice %></h1>"
end
def lib
- render :inline => '<%= useful_function %>'
+ render inline: "<%= useful_function %>"
end
end
@@ -48,7 +50,7 @@ end
class HelpersPathsController < ActionController::Base
paths = ["helpers2_pack", "helpers1_pack"].map do |path|
- File.join(File.expand_path('../../fixtures', __FILE__), path)
+ File.join(File.expand_path("../fixtures", __dir__), path)
end
$:.unshift(*paths)
@@ -56,12 +58,12 @@ class HelpersPathsController < ActionController::Base
helper :all
def index
- render :inline => "<%= conflicting_helper %>"
+ render inline: "<%= conflicting_helper %>"
end
end
class HelpersTypoController < ActionController::Base
- path = File.expand_path('../../fixtures/helpers_typo', __FILE__)
+ path = File.expand_path("../fixtures/helpers_typo", __dir__)
$:.unshift(path)
self.helpers_path = path
end
@@ -89,7 +91,7 @@ class HelpersTypoControllerTest < ActiveSupport::TestCase
end
def test_helper_typo_error_message
- e = assert_raise(NameError) { HelpersTypoController.helper 'admin/users' }
+ e = assert_raise(NameError) { HelpersTypoController.helper "admin/users" }
assert_equal "Couldn't find Admin::UsersHelper, expected it to be defined in helpers/admin/users_helper.rb", e.message
end
@@ -106,7 +108,7 @@ class HelperTest < ActiveSupport::TestCase
def setup
# Increment symbol counter.
- @symbol = (@@counter ||= 'A0').succ!.dup
+ @symbol = (@@counter ||= "A0").succ.dup
# Generate new controller class.
controller_class_name = "Helper#{@symbol}Controller"
@@ -125,13 +127,13 @@ class HelperTest < ActiveSupport::TestCase
def test_helper_method
assert_nothing_raised { @controller_class.helper_method :delegate_method }
- assert master_helper_methods.include?(:delegate_method)
+ assert_includes master_helper_methods, :delegate_method
end
def test_helper_attr
assert_nothing_raised { @controller_class.helper_attr :delegate_attr }
- assert master_helper_methods.include?(:delegate_attr)
- assert master_helper_methods.include?(:delegate_attr=)
+ assert_includes master_helper_methods, :delegate_attr
+ assert_includes master_helper_methods, :delegate_attr=
end
def call_controller(klass, action)
@@ -139,22 +141,12 @@ class HelperTest < ActiveSupport::TestCase
end
def test_helper_for_nested_controller
- assert_equal 'hello: Iz guuut!',
+ assert_equal "hello: Iz guuut!",
call_controller(Fun::GamesController, "render_hello_world").last.body
- # request = ActionController::TestRequest.new
- #
- # resp = Fun::GamesController.action(:render_hello_world).call(request.env)
- # assert_equal 'hello: Iz guuut!', resp.last.body
end
def test_helper_for_acronym_controller
assert_equal "test: baz", call_controller(Fun::PdfController, "test").last.body
- #
- # request = ActionController::TestRequest.new
- # response = ActionController::TestResponse.new
- # request.action = 'test'
- #
- # assert_equal 'test: baz', Fun::PdfController.process(request, response).body
end
def test_default_helpers_only
@@ -178,49 +170,65 @@ class HelperTest < ActiveSupport::TestCase
methods = AllHelpersController._helpers.instance_methods
# abc_helper.rb
- assert methods.include?(:bare_a)
+ assert_includes methods, :bare_a
# fun/games_helper.rb
- assert methods.include?(:stratego)
+ assert_includes methods, :stratego
# fun/pdf_helper.rb
- assert methods.include?(:foobar)
+ assert_includes methods, :foobar
end
def test_all_helpers_with_alternate_helper_dir
- @controller_class.helpers_path = File.expand_path('../../fixtures/alternate_helpers', __FILE__)
+ @controller_class.helpers_path = File.expand_path("../fixtures/alternate_helpers", __dir__)
# Reload helpers
@controller_class._helpers = Module.new
@controller_class.helper :all
# helpers/abc_helper.rb should not be included
- assert !master_helper_methods.include?(:bare_a)
+ assert_not_includes master_helper_methods, :bare_a
# alternate_helpers/foo_helper.rb
- assert master_helper_methods.include?(:baz)
+ assert_includes master_helper_methods, :baz
end
def test_helper_proxy
methods = AllHelpersController.helpers.methods
# Action View
- assert methods.include?(:pluralize)
+ assert_includes methods, :pluralize
+
+ # abc_helper.rb
+ assert_includes methods, :bare_a
+
+ # fun/games_helper.rb
+ assert_includes methods, :stratego
+
+ # fun/pdf_helper.rb
+ assert_includes methods, :foobar
+ end
+
+ def test_helper_proxy_in_instance
+ methods = AllHelpersController.new.helpers.methods
+
+ # Action View
+ assert_includes methods, :pluralize
# abc_helper.rb
- assert methods.include?(:bare_a)
+ assert_includes methods, :bare_a
# fun/games_helper.rb
- assert methods.include?(:stratego)
+ assert_includes methods, :stratego
# fun/pdf_helper.rb
- assert methods.include?(:foobar)
+ assert_includes methods, :foobar
end
def test_helper_proxy_config
- AllHelpersController.config.my_var = 'smth'
+ AllHelpersController.config.my_var = "smth"
- assert_equal 'smth', AllHelpersController.helpers.config.my_var
+ assert_equal "smth", AllHelpersController.helpers.config.my_var
end
private
@@ -237,31 +245,30 @@ class HelperTest < ActiveSupport::TestCase
end
def test_helper=(helper_module)
- silence_warnings { self.class.const_set('TestHelper', helper_module) }
+ silence_warnings { self.class.const_set("TestHelper", helper_module) }
end
end
-
class IsolatedHelpersTest < ActionController::TestCase
class A < ActionController::Base
def index
- render :inline => '<%= shout %>'
+ render inline: "<%= shout %>"
end
end
class B < A
- helper { def shout; 'B' end }
+ helper { def shout; "B" end }
def index
- render :inline => '<%= shout %>'
+ render inline: "<%= shout %>"
end
end
class C < A
- helper { def shout; 'C' end }
+ helper { def shout; "C" end }
def index
- render :inline => '<%= shout %>'
+ render inline: "<%= shout %>"
end
end
@@ -271,7 +278,7 @@ class IsolatedHelpersTest < ActionController::TestCase
def setup
super
- @request.action = 'index'
+ @request.action = "index"
end
def test_helper_in_a
@@ -279,10 +286,10 @@ class IsolatedHelpersTest < ActionController::TestCase
end
def test_helper_in_b
- assert_equal 'B', call_controller(B, "index").last.body
+ assert_equal "B", call_controller(B, "index").last.body
end
def test_helper_in_c
- assert_equal 'C', call_controller(C, "index").last.body
+ assert_equal "C", call_controller(C, "index").last.body
end
end
diff --git a/actionpack/test/controller/http_basic_authentication_test.rb b/actionpack/test/controller/http_basic_authentication_test.rb
index adaf19c2dc..1544a627ee 100644
--- a/actionpack/test/controller/http_basic_authentication_test.rb
+++ b/actionpack/test/controller/http_basic_authentication_test.rb
@@ -1,83 +1,96 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class HttpBasicAuthenticationTest < ActionController::TestCase
class DummyController < ActionController::Base
before_action :authenticate, only: :index
before_action :authenticate_with_request, only: :display
before_action :authenticate_long_credentials, only: :show
+ before_action :auth_with_special_chars, only: :special_creds
- http_basic_authenticate_with :name => "David", :password => "Goliath", :only => :search
+ http_basic_authenticate_with name: "David", password: "Goliath", only: :search
def index
- render :text => "Hello Secret"
+ render plain: "Hello Secret"
end
def display
- render :text => 'Definitely Maybe' if @logged_in
+ render plain: "Definitely Maybe" if @logged_in
end
def show
- render :text => 'Only for loooooong credentials'
+ render plain: "Only for loooooong credentials"
+ end
+
+ def special_creds
+ render plain: "Only for special credentials"
end
def search
- render :text => 'All inline'
+ render plain: "All inline"
end
private
- def authenticate
- authenticate_or_request_with_http_basic do |username, password|
- username == 'lifo' && password == 'world'
+ def authenticate
+ authenticate_or_request_with_http_basic do |username, password|
+ username == "lifo" && password == "world"
+ end
end
- end
- def authenticate_with_request
- if authenticate_with_http_basic { |username, password| username == 'pretty' && password == 'please' }
- @logged_in = true
- else
- request_http_basic_authentication("SuperSecret", "Authentication Failed\n")
+ def authenticate_with_request
+ if authenticate_with_http_basic { |username, password| username == "pretty" && password == "please" }
+ @logged_in = true
+ else
+ request_http_basic_authentication("SuperSecret", "Authentication Failed\n")
+ end
end
- end
- def authenticate_long_credentials
- authenticate_or_request_with_http_basic do |username, password|
- username == '1234567890123456789012345678901234567890' && password == '1234567890123456789012345678901234567890'
+ def auth_with_special_chars
+ authenticate_or_request_with_http_basic do |username, password|
+ username == 'login!@#$%^&*()_+{}[];"\',./<>?`~ \n\r\t' && password == 'pwd:!@#$%^&*()_+{}[];"\',./<>?`~ \n\r\t'
+ end
+ end
+
+ def authenticate_long_credentials
+ authenticate_or_request_with_http_basic do |username, password|
+ username == "1234567890123456789012345678901234567890" && password == "1234567890123456789012345678901234567890"
+ end
end
- end
end
- AUTH_HEADERS = ['HTTP_AUTHORIZATION', 'X-HTTP_AUTHORIZATION', 'X_HTTP_AUTHORIZATION', 'REDIRECT_X_HTTP_AUTHORIZATION']
+ AUTH_HEADERS = ["HTTP_AUTHORIZATION", "X-HTTP_AUTHORIZATION", "X_HTTP_AUTHORIZATION", "REDIRECT_X_HTTP_AUTHORIZATION"]
tests DummyController
AUTH_HEADERS.each do |header|
test "successful authentication with #{header.downcase}" do
- @request.env[header] = encode_credentials('lifo', 'world')
+ @request.env[header] = encode_credentials("lifo", "world")
get :index
assert_response :success
- assert_equal 'Hello Secret', @response.body, "Authentication failed for request header #{header}"
+ assert_equal "Hello Secret", @response.body, "Authentication failed for request header #{header}"
end
test "successful authentication with #{header.downcase} and long credentials" do
- @request.env[header] = encode_credentials('1234567890123456789012345678901234567890', '1234567890123456789012345678901234567890')
+ @request.env[header] = encode_credentials("1234567890123456789012345678901234567890", "1234567890123456789012345678901234567890")
get :show
assert_response :success
- assert_equal 'Only for loooooong credentials', @response.body, "Authentication failed for request header #{header} and long credentials"
+ assert_equal "Only for loooooong credentials", @response.body, "Authentication failed for request header #{header} and long credentials"
end
end
AUTH_HEADERS.each do |header|
test "unsuccessful authentication with #{header.downcase}" do
- @request.env[header] = encode_credentials('h4x0r', 'world')
+ @request.env[header] = encode_credentials("h4x0r", "world")
get :index
assert_response :unauthorized
assert_equal "HTTP Basic: Access denied.\n", @response.body, "Authentication didn't fail for request header #{header}"
end
test "unsuccessful authentication with #{header.downcase} and long credentials" do
- @request.env[header] = encode_credentials('h4x0rh4x0rh4x0rh4x0rh4x0rh4x0rh4x0rh4x0r', 'worldworldworldworldworldworldworldworld')
+ @request.env[header] = encode_credentials("h4x0rh4x0rh4x0rh4x0rh4x0rh4x0rh4x0rh4x0r", "worldworldworldworldworldworldworldworld")
get :show
assert_response :unauthorized
@@ -93,58 +106,74 @@ class HttpBasicAuthenticationTest < ActionController::TestCase
end
def test_encode_credentials_has_no_newline
- username = 'laskjdfhalksdjfhalkjdsfhalksdjfhklsdjhalksdjfhalksdjfhlakdsjfh'
- password = 'kjfhueyt9485osdfasdkljfh4lkjhakldjfhalkdsjf'
+ username = "laskjdfhalksdjfhalkjdsfhalksdjfhklsdjhalksdjfhalksdjfhlakdsjfh"
+ password = "kjfhueyt9485osdfasdkljfh4lkjhakldjfhalkdsjf"
result = ActionController::HttpAuthentication::Basic.encode_credentials(
username, password)
assert_no_match(/\n/, result)
end
+ test "successful authentication with uppercase authorization scheme" do
+ @request.env["HTTP_AUTHORIZATION"] = "BASIC #{::Base64.encode64("lifo:world")}"
+ get :index
+
+ assert_response :success
+ assert_equal "Hello Secret", @response.body, "Authentication failed when authorization scheme BASIC"
+ end
+
test "authentication request without credential" do
get :display
assert_response :unauthorized
assert_equal "Authentication Failed\n", @response.body
- assert_equal 'Basic realm="SuperSecret"', @response.headers['WWW-Authenticate']
+ assert_equal 'Basic realm="SuperSecret"', @response.headers["WWW-Authenticate"]
end
test "authentication request with invalid credential" do
- @request.env['HTTP_AUTHORIZATION'] = encode_credentials('pretty', 'foo')
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials("pretty", "foo")
get :display
assert_response :unauthorized
assert_equal "Authentication Failed\n", @response.body
- assert_equal 'Basic realm="SuperSecret"', @response.headers['WWW-Authenticate']
+ assert_equal 'Basic realm="SuperSecret"', @response.headers["WWW-Authenticate"]
end
test "authentication request with valid credential" do
- @request.env['HTTP_AUTHORIZATION'] = encode_credentials('pretty', 'please')
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials("pretty", "please")
get :display
assert_response :success
- assert_equal 'Definitely Maybe', @response.body
+ assert_equal "Definitely Maybe", @response.body
+ end
+
+ test "authentication request with valid credential special chars" do
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials('login!@#$%^&*()_+{}[];"\',./<>?`~ \n\r\t', 'pwd:!@#$%^&*()_+{}[];"\',./<>?`~ \n\r\t')
+ get :special_creds
+
+ assert_response :success
+ assert_equal "Only for special credentials", @response.body
end
test "authenticate with class method" do
- @request.env['HTTP_AUTHORIZATION'] = encode_credentials('David', 'Goliath')
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials("David", "Goliath")
get :search
assert_response :success
- @request.env['HTTP_AUTHORIZATION'] = encode_credentials('David', 'WRONG!')
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials("David", "WRONG!")
get :search
assert_response :unauthorized
end
test "authentication request with wrong scheme" do
- header = 'Bearer ' + encode_credentials('David', 'Goliath').split(' ', 2)[1]
- @request.env['HTTP_AUTHORIZATION'] = header
+ header = "Bearer " + encode_credentials("David", "Goliath").split(" ", 2)[1]
+ @request.env["HTTP_AUTHORIZATION"] = header
get :search
assert_response :unauthorized
end
private
- def encode_credentials(username, password)
- "Basic #{::Base64.encode64("#{username}:#{password}")}"
- end
+ def encode_credentials(username, password)
+ "Basic #{::Base64.encode64("#{username}:#{password}")}"
+ end
end
diff --git a/actionpack/test/controller/http_digest_authentication_test.rb b/actionpack/test/controller/http_digest_authentication_test.rb
index 57964769a7..b133afb343 100644
--- a/actionpack/test/controller/http_digest_authentication_test.rb
+++ b/actionpack/test/controller/http_digest_authentication_test.rb
@@ -1,41 +1,43 @@
-require 'abstract_unit'
-require 'active_support/key_generator'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/key_generator"
class HttpDigestAuthenticationTest < ActionController::TestCase
class DummyDigestController < ActionController::Base
before_action :authenticate, only: :index
before_action :authenticate_with_request, only: :display
- USERS = { 'lifo' => 'world', 'pretty' => 'please',
- 'dhh' => ::Digest::MD5::hexdigest(["dhh","SuperSecret","secret"].join(":"))}
+ USERS = { "lifo" => "world", "pretty" => "please",
+ "dhh" => ::Digest::MD5.hexdigest(["dhh", "SuperSecret", "secret"].join(":")) }
def index
- render :text => "Hello Secret"
+ render plain: "Hello Secret"
end
def display
- render :text => 'Definitely Maybe' if @logged_in
+ render plain: "Definitely Maybe" if @logged_in
end
private
- def authenticate
- authenticate_or_request_with_http_digest("SuperSecret") do |username|
- # Returns the password
- USERS[username]
+ def authenticate
+ authenticate_or_request_with_http_digest("SuperSecret") do |username|
+ # Returns the password
+ USERS[username]
+ end
end
- end
- def authenticate_with_request
- if authenticate_with_http_digest("SuperSecret") { |username| USERS[username] }
- @logged_in = true
- else
- request_http_digest_authentication("SuperSecret", "Authentication Failed")
+ def authenticate_with_request
+ if authenticate_with_http_digest("SuperSecret") { |username| USERS[username] }
+ @logged_in = true
+ else
+ request_http_digest_authentication("SuperSecret", "Authentication Failed")
+ end
end
- end
end
- AUTH_HEADERS = ['HTTP_AUTHORIZATION', 'X-HTTP_AUTHORIZATION', 'X_HTTP_AUTHORIZATION', 'REDIRECT_X_HTTP_AUTHORIZATION']
+ AUTH_HEADERS = ["HTTP_AUTHORIZATION", "X-HTTP_AUTHORIZATION", "X_HTTP_AUTHORIZATION", "REDIRECT_X_HTTP_AUTHORIZATION"]
tests DummyDigestController
@@ -51,17 +53,17 @@ class HttpDigestAuthenticationTest < ActionController::TestCase
AUTH_HEADERS.each do |header|
test "successful authentication with #{header.downcase}" do
- @request.env[header] = encode_credentials(:username => 'lifo', :password => 'world')
+ @request.env[header] = encode_credentials(username: "lifo", password: "world")
get :index
assert_response :success
- assert_equal 'Hello Secret', @response.body, "Authentication failed for request header #{header}"
+ assert_equal "Hello Secret", @response.body, "Authentication failed for request header #{header}"
end
end
AUTH_HEADERS.each do |header|
test "unsuccessful authentication with #{header.downcase}" do
- @request.env[header] = encode_credentials(:username => 'h4x0r', :password => 'world')
+ @request.env[header] = encode_credentials(username: "h4x0r", password: "world")
get :index
assert_response :unauthorized
@@ -74,21 +76,21 @@ class HttpDigestAuthenticationTest < ActionController::TestCase
assert_response :unauthorized
assert_equal "Authentication Failed", @response.body
- credentials = decode_credentials(@response.headers['WWW-Authenticate'])
- assert_equal 'SuperSecret', credentials[:realm]
+ credentials = decode_credentials(@response.headers["WWW-Authenticate"])
+ assert_equal "SuperSecret", credentials[:realm]
end
test "authentication request with nil credentials" do
- @request.env['HTTP_AUTHORIZATION'] = encode_credentials(:username => nil, :password => nil)
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: nil, password: nil)
get :index
assert_response :unauthorized
assert_equal "HTTP Digest: Access denied.\n", @response.body, "Authentication didn't fail for request"
- assert_not_equal 'Hello Secret', @response.body, "Authentication didn't fail for request"
+ assert_not_equal "Hello Secret", @response.body, "Authentication didn't fail for request"
end
test "authentication request with invalid password" do
- @request.env['HTTP_AUTHORIZATION'] = encode_credentials(:username => 'pretty', :password => 'foo')
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: "pretty", password: "foo")
get :display
assert_response :unauthorized
@@ -96,7 +98,7 @@ class HttpDigestAuthenticationTest < ActionController::TestCase
end
test "authentication request with invalid nonce" do
- @request.env['HTTP_AUTHORIZATION'] = encode_credentials(:username => 'pretty', :password => 'please', :nonce => "xxyyzz")
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: "pretty", password: "please", nonce: "xxyyzz")
get :display
assert_response :unauthorized
@@ -104,7 +106,7 @@ class HttpDigestAuthenticationTest < ActionController::TestCase
end
test "authentication request with invalid opaque" do
- @request.env['HTTP_AUTHORIZATION'] = encode_credentials(:username => 'pretty', :password => 'foo', :opaque => "xxyyzz")
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: "pretty", password: "foo", opaque: "xxyyzz")
get :display
assert_response :unauthorized
@@ -112,7 +114,7 @@ class HttpDigestAuthenticationTest < ActionController::TestCase
end
test "authentication request with invalid realm" do
- @request.env['HTTP_AUTHORIZATION'] = encode_credentials(:username => 'pretty', :password => 'foo', :realm => "NotSecret")
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: "pretty", password: "foo", realm: "NotSecret")
get :display
assert_response :unauthorized
@@ -120,127 +122,128 @@ class HttpDigestAuthenticationTest < ActionController::TestCase
end
test "authentication request with valid credential" do
- @request.env['HTTP_AUTHORIZATION'] = encode_credentials(:username => 'pretty', :password => 'please')
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: "pretty", password: "please")
get :display
assert_response :success
- assert_equal 'Definitely Maybe', @response.body
+ assert_equal "Definitely Maybe", @response.body
end
test "authentication request with valid credential and nil session" do
- @request.env['HTTP_AUTHORIZATION'] = encode_credentials(:username => 'pretty', :password => 'please')
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: "pretty", password: "please")
get :display
assert_response :success
- assert_equal 'Definitely Maybe', @response.body
+ assert_equal "Definitely Maybe", @response.body
end
test "authentication request with request-uri that doesn't match credentials digest-uri" do
- @request.env['HTTP_AUTHORIZATION'] = encode_credentials(:username => 'pretty', :password => 'please')
- @request.env['PATH_INFO'] = "/proxied/uri"
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: "pretty", password: "please")
+ @request.env["PATH_INFO"] = "/proxied/uri"
get :display
assert_response :success
- assert_equal 'Definitely Maybe', @response.body
+ assert_equal "Definitely Maybe", @response.body
end
test "authentication request with absolute request uri (as in webrick)" do
- @request.env['HTTP_AUTHORIZATION'] = encode_credentials(:username => 'pretty', :password => 'please')
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: "pretty", password: "please")
@request.env["SERVER_NAME"] = "test.host"
- @request.env['PATH_INFO'] = "/http_digest_authentication_test/dummy_digest"
+ @request.env["PATH_INFO"] = "/http_digest_authentication_test/dummy_digest"
get :display
assert_response :success
- assert_equal 'Definitely Maybe', @response.body
+ assert_equal "Definitely Maybe", @response.body
end
test "authentication request with absolute uri in credentials (as in IE)" do
- @request.env['HTTP_AUTHORIZATION'] = encode_credentials(:url => "http://test.host/http_digest_authentication_test/dummy_digest",
- :username => 'pretty', :password => 'please')
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials(url: "http://test.host/http_digest_authentication_test/dummy_digest",
+ username: "pretty", password: "please")
get :display
assert_response :success
- assert_equal 'Definitely Maybe', @response.body
+ assert_equal "Definitely Maybe", @response.body
end
test "authentication request with absolute uri in both request and credentials (as in Webrick with IE)" do
- @request.env['HTTP_AUTHORIZATION'] = encode_credentials(:url => "http://test.host/http_digest_authentication_test/dummy_digest",
- :username => 'pretty', :password => 'please')
- @request.env['SERVER_NAME'] = "test.host"
- @request.env['PATH_INFO'] = "/http_digest_authentication_test/dummy_digest"
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials(url: "http://test.host/http_digest_authentication_test/dummy_digest",
+ username: "pretty", password: "please")
+ @request.env["SERVER_NAME"] = "test.host"
+ @request.env["PATH_INFO"] = "/http_digest_authentication_test/dummy_digest"
get :display
assert_response :success
- assert_equal 'Definitely Maybe', @response.body
+ assert_equal "Definitely Maybe", @response.body
end
test "authentication request with password stored as ha1 digest hash" do
- @request.env['HTTP_AUTHORIZATION'] = encode_credentials(:username => 'dhh',
- :password => ::Digest::MD5::hexdigest(["dhh","SuperSecret","secret"].join(":")),
- :password_is_ha1 => true)
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials(
+ username: "dhh",
+ password: ::Digest::MD5.hexdigest(["dhh", "SuperSecret", "secret"].join(":")),
+ password_is_ha1: true)
get :display
assert_response :success
- assert_equal 'Definitely Maybe', @response.body
+ assert_equal "Definitely Maybe", @response.body
end
test "authentication request with _method" do
- @request.env['HTTP_AUTHORIZATION'] = encode_credentials(:username => 'pretty', :password => 'please', :method => :post)
- @request.env['rack.methodoverride.original_method'] = 'POST'
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: "pretty", password: "please", method: :post)
+ @request.env["rack.methodoverride.original_method"] = "POST"
put :display
assert_response :success
- assert_equal 'Definitely Maybe', @response.body
+ assert_equal "Definitely Maybe", @response.body
end
test "validate_digest_response should fail with nil returning password_procedure" do
- @request.env['HTTP_AUTHORIZATION'] = encode_credentials(:username => nil, :password => nil)
- assert !ActionController::HttpAuthentication::Digest.validate_digest_response(@request, "SuperSecret"){nil}
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: nil, password: nil)
+ assert_not ActionController::HttpAuthentication::Digest.validate_digest_response(@request, "SuperSecret") { nil }
end
test "authentication request with request-uri ending in '/'" do
- @request.env['PATH_INFO'] = "/http_digest_authentication_test/dummy_digest/"
- @request.env['HTTP_AUTHORIZATION'] = encode_credentials(:username => 'pretty', :password => 'please')
+ @request.env["PATH_INFO"] = "/http_digest_authentication_test/dummy_digest/"
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: "pretty", password: "please")
# simulate normalizing PATH_INFO
- @request.env['PATH_INFO'] = "/http_digest_authentication_test/dummy_digest"
+ @request.env["PATH_INFO"] = "/http_digest_authentication_test/dummy_digest"
get :display
assert_response :success
- assert_equal 'Definitely Maybe', @response.body
+ assert_equal "Definitely Maybe", @response.body
end
test "authentication request with request-uri ending in '?'" do
- @request.env['PATH_INFO'] = "/http_digest_authentication_test/dummy_digest/?"
- @request.env['HTTP_AUTHORIZATION'] = encode_credentials(:username => 'pretty', :password => 'please')
+ @request.env["PATH_INFO"] = "/http_digest_authentication_test/dummy_digest/?"
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: "pretty", password: "please")
# simulate normalizing PATH_INFO
- @request.env['PATH_INFO'] = "/http_digest_authentication_test/dummy_digest"
+ @request.env["PATH_INFO"] = "/http_digest_authentication_test/dummy_digest"
get :display
assert_response :success
- assert_equal 'Definitely Maybe', @response.body
+ assert_equal "Definitely Maybe", @response.body
end
test "authentication request with absolute uri in credentials (as in IE) ending with /" do
- @request.env['PATH_INFO'] = "/http_digest_authentication_test/dummy_digest/"
- @request.env['HTTP_AUTHORIZATION'] = encode_credentials(:uri => "http://test.host/http_digest_authentication_test/dummy_digest/",
- :username => 'pretty', :password => 'please')
+ @request.env["PATH_INFO"] = "/http_digest_authentication_test/dummy_digest/"
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials(uri: "http://test.host/http_digest_authentication_test/dummy_digest/",
+ username: "pretty", password: "please")
# simulate normalizing PATH_INFO
- @request.env['PATH_INFO'] = "/http_digest_authentication_test/dummy_digest"
+ @request.env["PATH_INFO"] = "/http_digest_authentication_test/dummy_digest"
get :display
assert_response :success
- assert_equal 'Definitely Maybe', @response.body
+ assert_equal "Definitely Maybe", @response.body
end
test "when sent a basic auth header, returns Unauthorized" do
- @request.env['HTTP_AUTHORIZATION'] = 'Basic Gwf2aXq8ZLF3Hxq='
+ @request.env["HTTP_AUTHORIZATION"] = "Basic Gwf2aXq8ZLF3Hxq="
get :display
@@ -249,32 +252,32 @@ class HttpDigestAuthenticationTest < ActionController::TestCase
private
- def encode_credentials(options)
- options.reverse_merge!(:nc => "00000001", :cnonce => "0a4f113b", :password_is_ha1 => false)
- password = options.delete(:password)
+ def encode_credentials(options)
+ options.reverse_merge!(nc: "00000001", cnonce: "0a4f113b", password_is_ha1: false)
+ password = options.delete(:password)
- # Perform unauthenticated request to retrieve digest parameters to use on subsequent request
- method = options.delete(:method) || 'GET'
+ # Perform unauthenticated request to retrieve digest parameters to use on subsequent request
+ method = options.delete(:method) || "GET"
- case method.to_s.upcase
- when 'GET'
- get :index
- when 'POST'
- post :index
- end
+ case method.to_s.upcase
+ when "GET"
+ get :index
+ when "POST"
+ post :index
+ end
- assert_response :unauthorized
+ assert_response :unauthorized
- credentials = decode_credentials(@response.headers['WWW-Authenticate'])
- credentials.merge!(options)
- path_info = @request.env['PATH_INFO'].to_s
- uri = options[:uri] || path_info
- credentials.merge!(:uri => uri)
- @request.env["ORIGINAL_FULLPATH"] = path_info
- ActionController::HttpAuthentication::Digest.encode_credentials(method, credentials, password, options[:password_is_ha1])
- end
+ credentials = decode_credentials(@response.headers["WWW-Authenticate"])
+ credentials.merge!(options)
+ path_info = @request.env["PATH_INFO"].to_s
+ uri = options[:uri] || path_info
+ credentials[:uri] = uri
+ @request.env["ORIGINAL_FULLPATH"] = path_info
+ ActionController::HttpAuthentication::Digest.encode_credentials(method, credentials, password, options[:password_is_ha1])
+ end
- def decode_credentials(header)
- ActionController::HttpAuthentication::Digest.decode_credentials(header)
- end
+ def decode_credentials(header)
+ ActionController::HttpAuthentication::Digest.decode_credentials(header)
+ end
end
diff --git a/actionpack/test/controller/http_token_authentication_test.rb b/actionpack/test/controller/http_token_authentication_test.rb
index 802c17b6bf..103123f98c 100644
--- a/actionpack/test/controller/http_token_authentication_test.rb
+++ b/actionpack/test/controller/http_token_authentication_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class HttpTokenAuthenticationTest < ActionController::TestCase
class DummyController < ActionController::Base
@@ -7,71 +9,71 @@ class HttpTokenAuthenticationTest < ActionController::TestCase
before_action :authenticate_long_credentials, only: :show
def index
- render :text => "Hello Secret"
+ render plain: "Hello Secret"
end
def display
- render :text => 'Definitely Maybe'
+ render plain: "Definitely Maybe"
end
def show
- render :text => 'Only for loooooong credentials'
+ render plain: "Only for loooooong credentials"
end
private
- def authenticate
- authenticate_or_request_with_http_token do |token, _|
- token == 'lifo'
+ def authenticate
+ authenticate_or_request_with_http_token do |token, _|
+ token == "lifo"
+ end
end
- end
- def authenticate_with_request
- if authenticate_with_http_token { |token, options| token == '"quote" pretty' && options[:algorithm] == 'test' }
- @logged_in = true
- else
- request_http_token_authentication("SuperSecret", "Authentication Failed\n")
+ def authenticate_with_request
+ if authenticate_with_http_token { |token, options| token == '"quote" pretty' && options[:algorithm] == "test" }
+ @logged_in = true
+ else
+ request_http_token_authentication("SuperSecret", "Authentication Failed\n")
+ end
end
- end
- def authenticate_long_credentials
- authenticate_or_request_with_http_token do |token, options|
- token == '1234567890123456789012345678901234567890' && options[:algorithm] == 'test'
+ def authenticate_long_credentials
+ authenticate_or_request_with_http_token do |token, options|
+ token == "1234567890123456789012345678901234567890" && options[:algorithm] == "test"
+ end
end
- end
end
- AUTH_HEADERS = ['HTTP_AUTHORIZATION', 'X-HTTP_AUTHORIZATION', 'X_HTTP_AUTHORIZATION', 'REDIRECT_X_HTTP_AUTHORIZATION']
+ AUTH_HEADERS = ["HTTP_AUTHORIZATION", "X-HTTP_AUTHORIZATION", "X_HTTP_AUTHORIZATION", "REDIRECT_X_HTTP_AUTHORIZATION"]
tests DummyController
AUTH_HEADERS.each do |header|
test "successful authentication with #{header.downcase}" do
- @request.env[header] = encode_credentials('lifo')
+ @request.env[header] = encode_credentials("lifo")
get :index
assert_response :success
- assert_equal 'Hello Secret', @response.body, "Authentication failed for request header #{header}"
+ assert_equal "Hello Secret", @response.body, "Authentication failed for request header #{header}"
end
test "successful authentication with #{header.downcase} and long credentials" do
- @request.env[header] = encode_credentials('1234567890123456789012345678901234567890', :algorithm => 'test')
+ @request.env[header] = encode_credentials("1234567890123456789012345678901234567890", algorithm: "test")
get :show
assert_response :success
- assert_equal 'Only for loooooong credentials', @response.body, "Authentication failed for request header #{header} and long credentials"
+ assert_equal "Only for loooooong credentials", @response.body, "Authentication failed for request header #{header} and long credentials"
end
end
AUTH_HEADERS.each do |header|
test "unsuccessful authentication with #{header.downcase}" do
- @request.env[header] = encode_credentials('h4x0r')
+ @request.env[header] = encode_credentials("h4x0r")
get :index
assert_response :unauthorized
assert_equal "HTTP Token: Access denied.\n", @response.body, "Authentication didn't fail for request header #{header}"
end
test "unsuccessful authentication with #{header.downcase} and long credentials" do
- @request.env[header] = encode_credentials('h4x0rh4x0rh4x0rh4x0rh4x0rh4x0rh4x0rh4x0r')
+ @request.env[header] = encode_credentials("h4x0rh4x0rh4x0rh4x0rh4x0rh4x0rh4x0rh4x0r")
get :show
assert_response :unauthorized
@@ -80,7 +82,7 @@ class HttpTokenAuthenticationTest < ActionController::TestCase
end
test "authentication request with badly formatted header" do
- @request.env['HTTP_AUTHORIZATION'] = 'Token token$"lifo"'
+ @request.env["HTTP_AUTHORIZATION"] = 'Token token$"lifo"'
get :index
assert_response :unauthorized
@@ -88,10 +90,18 @@ class HttpTokenAuthenticationTest < ActionController::TestCase
end
test "successful authentication request with Bearer instead of Token" do
- @request.env['HTTP_AUTHORIZATION'] = 'Bearer lifo'
+ @request.env["HTTP_AUTHORIZATION"] = "Bearer lifo"
+ get :index
+
+ assert_response :success
+ end
+
+ test "authentication request with tab in header" do
+ @request.env["HTTP_AUTHORIZATION"] = "Token\ttoken=\"lifo\""
get :index
assert_response :success
+ assert_equal "Hello Secret", @response.body
end
test "authentication request without credential" do
@@ -99,16 +109,16 @@ class HttpTokenAuthenticationTest < ActionController::TestCase
assert_response :unauthorized
assert_equal "Authentication Failed\n", @response.body
- assert_equal 'Token realm="SuperSecret"', @response.headers['WWW-Authenticate']
+ assert_equal 'Token realm="SuperSecret"', @response.headers["WWW-Authenticate"]
end
test "authentication request with invalid credential" do
- @request.env['HTTP_AUTHORIZATION'] = encode_credentials('"quote" pretty')
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials('"quote" pretty')
get :display
assert_response :unauthorized
assert_equal "Authentication Failed\n", @response.body
- assert_equal 'Token realm="SuperSecret"', @response.headers['WWW-Authenticate']
+ assert_equal 'Token realm="SuperSecret"', @response.headers["WWW-Authenticate"]
end
test "token_and_options returns correct token" do
@@ -119,7 +129,7 @@ class HttpTokenAuthenticationTest < ActionController::TestCase
end
test "token_and_options returns correct token with value after the equal sign" do
- token = 'rcHu+=HzSFw89Ypyhn/896A==f34'
+ token = "rcHu+=HzSFw89Ypyhn/896A==f34"
actual = ActionController::HttpAuthentication::Token.token_and_options(sample_request(token)).first
expected = token
assert_equal(expected, actual)
@@ -140,7 +150,7 @@ class HttpTokenAuthenticationTest < ActionController::TestCase
end
test "token_and_options returns empty string with empty token" do
- token = ''
+ token = +""
actual = ActionController::HttpAuthentication::Token.token_and_options(sample_request(token)).first
expected = token
assert_equal(expected, actual)
@@ -148,18 +158,17 @@ class HttpTokenAuthenticationTest < ActionController::TestCase
test "token_and_options returns correct token with nounce option" do
token = "rcHu+HzSFw89Ypyhn/896A="
- nonce_hash = {nonce: "123abc"}
+ nonce_hash = { nonce: "123abc" }
actual = ActionController::HttpAuthentication::Token.token_and_options(sample_request(token, nonce_hash))
expected_token = token
- expected_nonce = {"nonce" => nonce_hash[:nonce]}
+ expected_nonce = { "nonce" => nonce_hash[:nonce] }
assert_equal(expected_token, actual.first)
assert_equal(expected_nonce, actual.last)
end
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
@@ -182,7 +191,7 @@ class HttpTokenAuthenticationTest < ActionController::TestCase
private
- def sample_request(token, options = {nonce: "def"})
+ def sample_request(token, options = { nonce: "def" })
authorization = options.inject([%{Token token="#{token}"}]) do |arr, (k, v)|
arr << "#{k}=\"#{v}\""
end.join(", ")
diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb
index a6460168cb..39ede1442a 100644
--- a/actionpack/test/controller/integration_test.rb
+++ b/actionpack/test/controller/integration_test.rb
@@ -1,10 +1,12 @@
-require 'abstract_unit'
-require 'controller/fake_controllers'
-require 'rails/engine'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "controller/fake_controllers"
+require "rails/engine"
class SessionTest < ActiveSupport::TestCase
StubApp = lambda { |env|
- [200, {"Content-Type" => "text/html", "Content-Length" => "13"}, ["Hello, World!"]]
+ [200, { "Content-Type" => "text/html", "Content-Length" => "13" }, ["Hello, World!"]]
}
def setup
@@ -12,11 +14,11 @@ class SessionTest < ActiveSupport::TestCase
end
def test_https_bang_works_and_sets_truth_by_default
- assert !@session.https?
+ assert_not_predicate @session, :https?
@session.https!
- assert @session.https?
+ assert_predicate @session, :https?
@session.https! false
- assert !@session.https?
+ assert_not_predicate @session, :https?
end
def test_host!
@@ -26,296 +28,114 @@ class SessionTest < ActiveSupport::TestCase
end
def test_follow_redirect_raises_when_no_redirect
- @session.stubs(:redirect?).returns(false)
- assert_raise(RuntimeError) { @session.follow_redirect! }
- end
-
- def test_request_via_redirect_uses_given_method
- path = "/somepath"; args = {:id => '1'}; headers = {"X-Test-Header" => "testvalue"}
- @session.expects(:process).with(:put, path, params: args, headers: headers)
- @session.stubs(:redirect?).returns(false)
- @session.request_via_redirect(:put, path, params: args, headers: headers)
- end
-
- def test_deprecated_request_via_redirect_uses_given_method
- path = "/somepath"; args = { id: '1' }; headers = { "X-Test-Header" => "testvalue" }
- @session.expects(:process).with(:put, path, params: args, headers: headers)
- @session.stubs(:redirect?).returns(false)
- assert_deprecated { @session.request_via_redirect(:put, path, args, headers) }
- end
-
- def test_request_via_redirect_follows_redirects
- path = "/somepath"; args = {:id => '1'}; headers = {"X-Test-Header" => "testvalue"}
- @session.stubs(:redirect?).returns(true, true, false)
- @session.expects(:follow_redirect!).times(2)
- @session.request_via_redirect(:get, path, params: args, headers: headers)
- end
-
- def test_request_via_redirect_returns_status
- path = "/somepath"; args = {:id => '1'}; headers = {"X-Test-Header" => "testvalue"}
- @session.stubs(:redirect?).returns(false)
- @session.stubs(:status).returns(200)
- assert_equal 200, @session.request_via_redirect(:get, path, params: args, headers: headers)
- end
-
- def test_deprecated_get_via_redirect
- path = "/somepath"; args = { id: '1' }; headers = { "X-Test-Header" => "testvalue" }
- @session.expects(:request_via_redirect).with(:get, path, args, headers)
-
- assert_deprecated do
- @session.get_via_redirect(path, args, headers)
- end
- end
-
- def test_deprecated_post_via_redirect
- path = "/somepath"; args = { id: '1' }; headers = { "X-Test-Header" => "testvalue" }
- @session.expects(:request_via_redirect).with(:post, path, args, headers)
-
- assert_deprecated do
- @session.post_via_redirect(path, args, headers)
+ @session.stub :redirect?, false do
+ assert_raise(RuntimeError) { @session.follow_redirect! }
end
end
- def test_deprecated_patch_via_redirect
- path = "/somepath"; args = { id: '1' }; headers = { "X-Test-Header" => "testvalue" }
- @session.expects(:request_via_redirect).with(:patch, path, args, headers)
-
- assert_deprecated do
- @session.patch_via_redirect(path, args, headers)
- end
- end
-
- def test_deprecated_put_via_redirect
- path = "/somepath"; args = { id: '1' }; headers = { "X-Test-Header" => "testvalue" }
- @session.expects(:request_via_redirect).with(:put, path, args, headers)
-
- assert_deprecated do
- @session.put_via_redirect(path, args, headers)
- end
- end
-
- def test_deprecated_delete_via_redirect
- path = "/somepath"; args = { id: '1' }; headers = { "X-Test-Header" => "testvalue" }
- @session.expects(:request_via_redirect).with(:delete, path, args, headers)
+ def test_get
+ path = "/index"; params = "blah"; headers = { location: "blah" }
- assert_deprecated do
- @session.delete_via_redirect(path, args, headers)
+ assert_called_with @session, :process, [:get, path, params: params, headers: headers] do
+ @session.get(path, params: params, headers: headers)
end
end
- def test_get
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- @session.expects(:process).with(:get, path, params: params, headers: headers)
- @session.get(path, params: params, headers: headers)
- end
-
def test_get_with_env_and_headers
- path = "/index"; params = "blah"; headers = { location: 'blah' }; env = { 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest' }
- @session.expects(:process).with(:get, path, params: params, headers: headers, env: env)
- @session.get(path, params: params, headers: headers, env: env)
- end
-
- def test_deprecated_get
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- @session.expects(:process).with(:get, path, params: params, headers: headers)
- assert_deprecated {
- @session.get(path, params, headers)
- }
+ path = "/index"; params = "blah"; headers = { location: "blah" }; env = { "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest" }
+ assert_called_with @session, :process, [:get, path, params: params, headers: headers, env: env] do
+ @session.get(path, params: params, headers: headers, env: env)
+ end
end
def test_post
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- @session.expects(:process).with(:post, path, params: params, headers: headers)
- assert_deprecated {
- @session.post(path, params, headers)
- }
- end
-
- def test_deprecated_post
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- @session.expects(:process).with(:post, path, params: params, headers: headers)
- @session.post(path, params: params, headers: headers)
+ path = "/index"; params = "blah"; headers = { location: "blah" }
+ assert_called_with @session, :process, [:post, path, params: params, headers: headers] do
+ @session.post(path, params: params, headers: headers)
+ end
end
def test_patch
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- @session.expects(:process).with(:patch, path, params: params, headers: headers)
- @session.patch(path, params: params, headers: headers)
- end
-
- def test_deprecated_patch
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- @session.expects(:process).with(:patch, path, params: params, headers: headers)
- assert_deprecated {
- @session.patch(path, params, headers)
- }
+ path = "/index"; params = "blah"; headers = { location: "blah" }
+ assert_called_with @session, :process, [:patch, path, params: params, headers: headers] do
+ @session.patch(path, params: params, headers: headers)
+ end
end
def test_put
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- @session.expects(:process).with(:put, path, params: params, headers: headers)
- @session.put(path, params: params, headers: headers)
- end
-
- def test_deprecated_put
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- @session.expects(:process).with(:put, path, params: params, headers: headers)
- assert_deprecated {
- @session.put(path, params, headers)
- }
+ path = "/index"; params = "blah"; headers = { location: "blah" }
+ assert_called_with @session, :process, [:put, path, params: params, headers: headers] do
+ @session.put(path, params: params, headers: headers)
+ end
end
def test_delete
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- @session.expects(:process).with(:delete, path, params: params, headers: headers)
- assert_deprecated {
- @session.delete(path,params,headers)
- }
- end
-
- def test_deprecated_delete
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- @session.expects(:process).with(:delete, path, params: params, headers: headers)
- @session.delete(path, params: params, headers: headers)
+ path = "/index"; params = "blah"; headers = { location: "blah" }
+ assert_called_with @session, :process, [:delete, path, params: params, headers: headers] do
+ @session.delete(path, params: params, headers: headers)
+ end
end
def test_head
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- @session.expects(:process).with(:head, path, params: params, headers: headers)
- @session.head(path, params: params, headers: headers)
- end
-
- def deprecated_test_head
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- @session.expects(:process).with(:head, path, params: params, headers: headers)
- assert_deprecated {
- @session.head(path, params, headers)
- }
+ path = "/index"; params = "blah"; headers = { location: "blah" }
+ assert_called_with @session, :process, [:head, path, params: params, headers: headers] do
+ @session.head(path, params: params, headers: headers)
+ end
end
def test_xml_http_request_get
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- @session.expects(:process).with(:get, path, params: params, headers: headers, xhr: true)
- @session.get(path, params: params, headers: headers, xhr: true)
- end
-
- def test_deprecated_xml_http_request_get
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- @session.expects(:process).with(:get, path, params: params, headers: headers, xhr: true)
- @session.get(path, params: params, headers: headers, xhr: true)
- end
-
- def test_deprecated_args_xml_http_request_get
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- @session.expects(:process).with(:get, path, params: params, headers: headers, xhr: true)
- assert_deprecated(/xml_http_request/) {
- @session.xml_http_request(:get, path, params, headers)
- }
+ path = "/index"; params = "blah"; headers = { location: "blah" }
+ assert_called_with @session, :process, [:get, path, params: params, headers: headers, xhr: true] do
+ @session.get(path, params: params, headers: headers, xhr: true)
+ end
end
def test_xml_http_request_post
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- @session.expects(:process).with(:post, path, params: params, headers: headers, xhr: true)
- @session.post(path, params: params, headers: headers, xhr: true)
- end
-
- def test_deprecated_xml_http_request_post
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- @session.expects(:process).with(:post, path, params: params, headers: headers, xhr: true)
- @session.post(path, params: params, headers: headers, xhr: true)
- end
-
- def test_deprecated_args_xml_http_request_post
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- @session.expects(:process).with(:post, path, params: params, headers: headers, xhr: true)
- assert_deprecated(/xml_http_request/) { @session.xml_http_request(:post,path,params,headers) }
+ path = "/index"; params = "blah"; headers = { location: "blah" }
+ assert_called_with @session, :process, [:post, path, params: params, headers: headers, xhr: true] do
+ @session.post(path, params: params, headers: headers, xhr: true)
+ end
end
def test_xml_http_request_patch
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- @session.expects(:process).with(:patch, path, params: params, headers: headers, xhr: true)
- @session.patch(path, params: params, headers: headers, xhr: true)
- end
-
- def test_deprecated_xml_http_request_patch
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- @session.expects(:process).with(:patch, path, params: params, headers: headers, xhr: true)
- @session.patch(path, params: params, headers: headers, xhr: true)
- end
-
- def test_deprecated_args_xml_http_request_patch
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- @session.expects(:process).with(:patch, path, params: params, headers: headers, xhr: true)
- assert_deprecated(/xml_http_request/) { @session.xml_http_request(:patch,path,params,headers) }
+ path = "/index"; params = "blah"; headers = { location: "blah" }
+ assert_called_with @session, :process, [:patch, path, params: params, headers: headers, xhr: true] do
+ @session.patch(path, params: params, headers: headers, xhr: true)
+ end
end
def test_xml_http_request_put
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- @session.expects(:process).with(:put, path, params: params, headers: headers, xhr: true)
- @session.put(path, params: params, headers: headers, xhr: true)
- end
-
- def test_deprecated_xml_http_request_put
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- @session.expects(:process).with(:put, path, params: params, headers: headers, xhr: true)
- @session.put(path, params: params, headers: headers, xhr: true)
- end
-
- def test_deprecated_args_xml_http_request_put
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- @session.expects(:process).with(:put, path, params: params, headers: headers, xhr: true)
- assert_deprecated(/xml_http_request/) { @session.xml_http_request(:put, path, params, headers) }
+ path = "/index"; params = "blah"; headers = { location: "blah" }
+ assert_called_with @session, :process, [:put, path, params: params, headers: headers, xhr: true] do
+ @session.put(path, params: params, headers: headers, xhr: true)
+ end
end
def test_xml_http_request_delete
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- @session.expects(:process).with(:delete, path, params: params, headers: headers, xhr: true)
- @session.delete(path, params: params, headers: headers, xhr: true)
- end
-
- def test_deprecated_xml_http_request_delete
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- @session.expects(:process).with(:delete, path, params: params, headers: headers, xhr: true)
- assert_deprecated { @session.xml_http_request(:delete, path, params: params, headers: headers) }
- end
-
- def test_deprecated_args_xml_http_request_delete
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- @session.expects(:process).with(:delete, path, params: params, headers: headers, xhr: true)
- assert_deprecated(/xml_http_request/) { @session.xml_http_request(:delete, path, params, headers) }
+ path = "/index"; params = "blah"; headers = { location: "blah" }
+ assert_called_with @session, :process, [:delete, path, params: params, headers: headers, xhr: true] do
+ @session.delete(path, params: params, headers: headers, xhr: true)
+ end
end
def test_xml_http_request_head
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- @session.expects(:process).with(:head, path, params: params, headers: headers, xhr: true)
- @session.head(path, params: params, headers: headers, xhr: true)
- end
-
- def test_deprecated_xml_http_request_head
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- @session.expects(:process).with(:head, path, params: params, headers: headers, xhr: true)
- assert_deprecated(/xml_http_request/) { @session.xml_http_request(:head, path, params: params, headers: headers) }
- end
-
- def test_deprecated_args_xml_http_request_head
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- @session.expects(:process).with(:head, path, params: params, headers: headers, xhr: true)
- assert_deprecated { @session.xml_http_request(:head, path, params, headers) }
+ path = "/index"; params = "blah"; headers = { location: "blah" }
+ assert_called_with @session, :process, [:head, path, params: params, headers: headers, xhr: true] do
+ @session.head(path, params: params, headers: headers, xhr: true)
+ end
end
end
class IntegrationTestTest < ActiveSupport::TestCase
def setup
@test = ::ActionDispatch::IntegrationTest.new(:app)
- @test.class.stubs(:fixture_table_names).returns([])
- @session = @test.open_session
end
def test_opens_new_session
session1 = @test.open_session { |sess| }
session2 = @test.open_session # implicit session
- assert !session1.equal?(session2)
+ assert_not session1.equal?(session2)
end
# RSpec mixes Matchers (which has a #method_missing) into
@@ -324,12 +144,12 @@ class IntegrationTestTest < ActiveSupport::TestCase
def test_does_not_prevent_method_missing_passing_up_to_ancestors
mixin = Module.new do
def method_missing(name, *args)
- name.to_s == 'foo' ? 'pass' : super
+ 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
+ assert_equal "pass", @test.foo
ensure
# leave other tests as unaffected as possible
mixin.__send__(:remove_method, :method_missing)
@@ -340,17 +160,11 @@ end
# Tests that integration tests don't call Controller test methods for processing.
# Integration tests have their own setup and teardown.
class IntegrationTestUsesCorrectClass < ActionDispatch::IntegrationTest
- def self.fixture_table_names
- []
- end
-
def test_integration_methods_called
reset!
- @integration_session.stubs(:generic_url_rewriter)
- @integration_session.stubs(:process)
%w( get post head patch put delete ).each do |verb|
- assert_nothing_raised("'#{verb}' should use integration test methods") { __send__(verb, '/') }
+ assert_nothing_raised { __send__(verb, "/") }
end
end
end
@@ -359,52 +173,55 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
class IntegrationController < ActionController::Base
def get
respond_to do |format|
- format.html { render :text => "OK", :status => 200 }
- format.js { render :text => "JS OK", :status => 200 }
- format.xml { render :xml => "<root></root>", :status => 200 }
+ format.html { render plain: "OK", status: 200 }
+ format.js { render plain: "JS OK", status: 200 }
+ format.json { render json: "JSON OK", status: 200 }
+ format.xml { render xml: "<root></root>", status: 200 }
+ format.rss { render xml: "<root></root>", status: 200 }
+ format.atom { render xml: "<root></root>", status: 200 }
end
end
def get_with_params
- render :text => "foo: #{params[:foo]}", :status => 200
+ render plain: "foo: #{params[:foo]}", status: 200
end
def post
- render :text => "Created", :status => 201
+ render plain: "Created", status: 201
end
def method
- render :text => "method: #{request.method.downcase}"
+ render plain: "method: #{request.method.downcase}"
end
def cookie_monster
cookies["cookie_1"] = nil
cookies["cookie_3"] = "chocolate"
- render :text => "Gone", :status => 410
+ render plain: "Gone", status: 410
end
def set_cookie
- cookies["foo"] = 'bar'
+ cookies["foo"] = "bar"
head :ok
end
def get_cookie
- render :text => cookies["foo"]
+ render plain: cookies["foo"]
end
def redirect
- redirect_to action_url('get')
+ redirect_to action_url("get")
end
def remove_header
response.headers.delete params[:header]
- head :ok, 'c' => '3'
+ head :ok, "c" => "3"
end
end
def test_get
with_test_route_set do
- get '/get'
+ get "/get"
assert_equal 200, status
assert_equal "OK", status_message
assert_response 200
@@ -418,25 +235,27 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
end
end
- def test_get_xml
- with_test_route_set do
- get "/get", params: {}, headers: {"HTTP_ACCEPT" => "application/xml"}
- assert_equal 200, status
- assert_equal "OK", status_message
- assert_response 200
- assert_response :success
- assert_response :ok
- assert_equal({}, cookies.to_hash)
- assert_equal "<root></root>", body
- assert_equal "<root></root>", response.body
- assert_instance_of Nokogiri::XML::Document, html_document
- assert_equal 1, request_count
+ def test_get_xml_rss_atom
+ %w[ application/xml application/rss+xml application/atom+xml ].each do |mime_string|
+ with_test_route_set do
+ get "/get", headers: { "HTTP_ACCEPT" => mime_string }
+ assert_equal 200, status
+ assert_equal "OK", status_message
+ assert_response 200
+ assert_response :success
+ assert_response :ok
+ assert_equal({}, cookies.to_hash)
+ assert_equal "<root></root>", body
+ assert_equal "<root></root>", response.body
+ assert_instance_of Nokogiri::XML::Document, html_document
+ assert_equal 1, request_count
+ end
end
end
def test_post
with_test_route_set do
- post '/post'
+ post "/post"
assert_equal 201, status
assert_equal "Created", status_message
assert_response 201
@@ -450,55 +269,55 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
end
end
- test 'response cookies are added to the cookie jar for the next request' do
+ 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"
- get '/cookie_monster'
+ 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)
+ assert_equal({ "cookie_1" => "", "cookie_2" => "oatmeal", "cookie_3" => "chocolate" }, cookies.to_hash)
end
end
- test 'cookie persist to next request' do
+ test "cookie persist to next request" do
with_test_route_set do
- get '/set_cookie'
+ get "/set_cookie"
assert_response :success
assert_equal "foo=bar; path=/", headers["Set-Cookie"]
- assert_equal({"foo"=>"bar"}, cookies.to_hash)
+ assert_equal({ "foo" => "bar" }, cookies.to_hash)
- get '/get_cookie'
+ get "/get_cookie"
assert_response :success
assert_equal "bar", body
- assert_equal nil, headers["Set-Cookie"]
- assert_equal({"foo"=>"bar"}, cookies.to_hash)
+ assert_nil headers["Set-Cookie"]
+ assert_equal({ "foo" => "bar" }, cookies.to_hash)
end
end
- test 'cookie persist to next request on another domain' do
+ test "cookie persist to next request on another domain" do
with_test_route_set do
host! "37s.backpack.test"
- get '/set_cookie'
+ get "/set_cookie"
assert_response :success
assert_equal "foo=bar; path=/", headers["Set-Cookie"]
- assert_equal({"foo"=>"bar"}, cookies.to_hash)
+ assert_equal({ "foo" => "bar" }, cookies.to_hash)
- get '/get_cookie'
+ get "/get_cookie"
assert_response :success
assert_equal "bar", body
- assert_equal nil, headers["Set-Cookie"]
- assert_equal({"foo"=>"bar"}, cookies.to_hash)
+ assert_nil headers["Set-Cookie"]
+ assert_equal({ "foo" => "bar" }, cookies.to_hash)
end
end
def test_redirect
with_test_route_set do
- get '/redirect'
+ get "/redirect"
assert_equal 302, status
assert_equal "Found", status_message
assert_response 302
@@ -512,27 +331,37 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
assert_response :success
assert_equal "/get", path
- get '/moved'
+ get "/moved"
assert_response :redirect
- assert_redirected_to '/method'
+ assert_redirected_to "/method"
end
end
- def test_xml_http_request_get
+ def test_redirect_reset_html_document
with_test_route_set do
- get '/get', xhr: true
- assert_equal 200, status
- assert_equal "OK", status_message
- assert_response 200
- assert_response :success
+ get "/redirect"
+ previous_html_document = html_document
+
+ follow_redirect!
+
assert_response :ok
- assert_equal "JS OK", response.body
+ assert_not_same previous_html_document, html_document
end
end
- def test_deprecated_xml_http_request_get
+ def test_redirect_with_arguments
with_test_route_set do
- assert_deprecated { xhr :get, '/get' }
+ get "/redirect"
+ follow_redirect! params: { foo: :bar }
+
+ assert_response :ok
+ assert_equal "bar", request.parameters["foo"]
+ end
+ end
+
+ def test_xml_http_request_get
+ with_test_route_set do
+ get "/get", xhr: true
assert_equal 200, status
assert_equal "OK", status_message
assert_response 200
@@ -544,21 +373,29 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
def test_request_with_bad_format
with_test_route_set do
- get '/get.php', xhr: true
+ get "/get.php", xhr: true
assert_equal 406, status
assert_response 406
assert_response :not_acceptable
end
end
+ test "creation of multiple integration sessions" do
+ integration_session # initialize first session
+ a = open_session
+ b = open_session
+
+ assert_not_same(a.integration_session, b.integration_session)
+ end
+
def test_get_with_query_string
with_test_route_set do
- get '/get_with_params?foo=bar'
- assert_equal '/get_with_params?foo=bar', request.env["REQUEST_URI"]
- assert_equal '/get_with_params?foo=bar', request.fullpath
+ get "/get_with_params?foo=bar"
+ assert_equal "/get_with_params?foo=bar", request.env["REQUEST_URI"]
+ assert_equal "/get_with_params?foo=bar", request.fullpath
assert_equal "foo=bar", request.env["QUERY_STRING"]
- assert_equal 'foo=bar', request.query_string
- assert_equal 'bar', request.parameters['foo']
+ assert_equal "foo=bar", request.query_string
+ assert_equal "bar", request.parameters["foo"]
assert_equal 200, status
assert_equal "foo: bar", response.body
@@ -567,77 +404,91 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
def test_get_with_parameters
with_test_route_set do
- get '/get_with_params', params: { foo: "bar" }
- assert_equal '/get_with_params', request.env["PATH_INFO"]
- assert_equal '/get_with_params', request.path_info
- assert_equal 'foo=bar', request.env["QUERY_STRING"]
- assert_equal 'foo=bar', request.query_string
- assert_equal 'bar', request.parameters['foo']
+ get "/get_with_params", params: { foo: "bar" }
+ assert_equal "/get_with_params", request.env["PATH_INFO"]
+ assert_equal "/get_with_params", request.path_info
+ assert_equal "foo=bar", request.env["QUERY_STRING"]
+ assert_equal "foo=bar", request.query_string
+ assert_equal "bar", request.parameters["foo"]
assert_equal 200, status
assert_equal "foo: bar", response.body
end
end
+ def test_post_then_get_with_parameters_do_not_leak_across_requests
+ with_test_route_set do
+ post "/post", params: { leaks: "does-leak?" }
+
+ get "/get_with_params", params: { foo: "bar" }
+
+ assert_empty request.env["rack.input"].string
+ assert_equal "foo=bar", request.env["QUERY_STRING"]
+ assert_equal "foo=bar", request.query_string
+ assert_equal "bar", request.parameters["foo"]
+ assert_predicate request.parameters["leaks"], :nil?
+ end
+ end
+
def test_head
with_test_route_set do
- head '/get'
+ head "/get"
assert_equal 200, status
assert_equal "", body
- head '/post'
+ head "/post"
assert_equal 201, status
assert_equal "", body
- get '/get/method'
+ get "/get/method"
assert_equal 200, status
assert_equal "method: get", body
- head '/get/method'
+ head "/get/method"
assert_equal 200, status
assert_equal "", body
end
end
def test_generate_url_with_controller
- assert_equal 'http://www.example.com/foo', url_for(:controller => "foo")
+ assert_equal "http://www.example.com/foo", url_for(controller: "foo")
end
def test_port_via_host!
with_test_route_set do
- host! 'www.example.com:8080'
- get '/get'
+ host! "www.example.com:8080"
+ get "/get"
assert_equal 8080, request.port
end
end
def test_port_via_process
with_test_route_set do
- get 'http://www.example.com:8080/get'
+ get "http://www.example.com:8080/get"
assert_equal 8080, request.port
end
end
def test_https_and_port_via_host_and_https!
with_test_route_set do
- host! 'www.example.com'
+ host! "www.example.com"
https! true
- get '/get'
+ get "/get"
assert_equal 443, request.port
assert_equal true, request.ssl?
- host! 'www.example.com:443'
+ host! "www.example.com:443"
https! true
- get '/get'
+ get "/get"
assert_equal 443, request.port
assert_equal true, request.ssl?
- host! 'www.example.com:8443'
+ host! "www.example.com:8443"
https! true
- get '/get'
+ get "/get"
assert_equal 8443, request.port
assert_equal true, request.ssl?
end
@@ -645,11 +496,11 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
def test_https_and_port_via_process
with_test_route_set do
- get 'https://www.example.com/get'
+ get "https://www.example.com/get"
assert_equal 443, request.port
assert_equal true, request.ssl?
- get 'https://www.example.com:8443/get'
+ get "https://www.example.com:8443/get"
assert_equal 8443, request.port
assert_equal true, request.ssl?
end
@@ -657,14 +508,26 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
def test_respect_removal_of_default_headers_by_a_controller_action
with_test_route_set do
- with_default_headers 'a' => '1', 'b' => '2' do
- get '/remove_header', params: { header: 'a' }
+ with_default_headers "a" => "1", "b" => "2" do
+ get "/remove_header", params: { header: "a" }
end
end
- assert_not_includes @response.headers, 'a', 'Response should not include default header removed by the controller action'
- assert_includes @response.headers, 'b'
- assert_includes @response.headers, 'c'
+ assert_not_includes @response.headers, "a", "Response should not include default header removed by the controller action"
+ assert_includes @response.headers, "b"
+ assert_includes @response.headers, "c"
+ end
+
+ 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
+ assert_equal "application/json", response.content_type
+
+ get "/get", headers: { "HTTP_ACCEPT" => "application/json" }, xhr: true
+ assert_equal "application/json", request.accept
+ assert_equal "application/json", response.content_type
+ end
end
private
@@ -684,13 +547,15 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
end
set.draw do
- get 'moved' => redirect('/method')
+ get "moved" => redirect("/method")
- match ':action', :to => controller, :via => [:get, :post], :as => :action
- get 'get/:action', :to => controller, :as => :get_action
+ ActiveSupport::Deprecation.silence do
+ match ":action", to: controller, via: [:get, :post], as: :action
+ get "get/:action", to: controller, as: :get_action
+ end
end
- self.singleton_class.include(set.url_helpers)
+ singleton_class.include(set.url_helpers)
yield
end
@@ -703,9 +568,9 @@ class MetalIntegrationTest < ActionDispatch::IntegrationTest
class Poller
def self.call(env)
if env["PATH_INFO"] =~ /^\/success/
- [200, {"Content-Type" => "text/plain", "Content-Length" => "12"}, ["Hello World!"]]
+ [200, { "Content-Type" => "text/plain", "Content-Length" => "12" }, ["Hello World!"]]
else
- [404, {"Content-Type" => "text/plain", "Content-Length" => "0"}, []]
+ [404, { "Content-Type" => "text/plain", "Content-Length" => "0" }, []]
end
end
end
@@ -726,15 +591,15 @@ class MetalIntegrationTest < ActionDispatch::IntegrationTest
get "/failure"
assert_response 404
assert_response :not_found
- assert_equal '', response.body
+ assert_equal "", response.body
end
def test_generate_url_without_controller
- assert_equal 'http://www.example.com/foo', url_for(:controller => "foo")
+ assert_equal "http://www.example.com/foo", url_for(controller: "foo")
end
def test_pass_headers
- get "/success", headers: {"Referer" => "http://www.example.com/foo", "Host" => "http://nohost.com"}
+ get "/success", headers: { "Referer" => "http://www.example.com/foo", "Host" => "http://nohost.com" }
assert_equal "http://nohost.com", @request.env["HTTP_HOST"]
assert_equal "http://www.example.com/foo", @request.env["HTTP_REFERER"]
@@ -755,12 +620,30 @@ class MetalIntegrationTest < ActionDispatch::IntegrationTest
assert_equal "http://test.com/", @request.env["HTTP_REFERER"]
end
+ def test_ignores_common_ports_in_host
+ get "http://test.com"
+ assert_equal "test.com", @request.env["HTTP_HOST"]
+
+ get "https://test.com"
+ assert_equal "test.com", @request.env["HTTP_HOST"]
+ end
+
+ def test_keeps_uncommon_ports_in_host
+ get "http://test.com:123"
+ assert_equal "test.com:123", @request.env["HTTP_HOST"]
+
+ get "http://test.com:443"
+ assert_equal "test.com:443", @request.env["HTTP_HOST"]
+
+ get "https://test.com:80"
+ assert_equal "test.com:80", @request.env["HTTP_HOST"]
+ end
end
class ApplicationIntegrationTest < ActionDispatch::IntegrationTest
class TestController < ActionController::Base
def index
- render :text => "index"
+ render plain: "index"
end
end
@@ -778,7 +661,7 @@ class ApplicationIntegrationTest < ActionDispatch::IntegrationTest
end
routes.draw do
- get 'baz', :to => 'application_integration_test/test#index', :as => :baz
+ get "baz", to: "application_integration_test/test#index", as: :baz
end
def self.call(*)
@@ -786,14 +669,14 @@ class ApplicationIntegrationTest < ActionDispatch::IntegrationTest
end
routes.draw do
- get '', :to => 'application_integration_test/test#index', :as => :empty_string
+ get "", to: "application_integration_test/test#index", as: :empty_string
- get 'foo', :to => 'application_integration_test/test#index', :as => :foo
- get 'bar', :to => 'application_integration_test/test#index', :as => :bar
+ get "foo", to: "application_integration_test/test#index", as: :foo
+ get "bar", to: "application_integration_test/test#index", as: :bar
- mount MountedApp => '/mounted', :as => "mounted"
- get 'fooz' => proc { |env| [ 200, {'X-Cascade' => 'pass'}, [ "omg" ] ] }, :anchor => false
- get 'fooz', :to => 'application_integration_test/test#index'
+ mount MountedApp => "/mounted", :as => "mounted"
+ get "fooz" => proc { |env| [ 200, { "X-Cascade" => "pass" }, [ "omg" ] ] }, :anchor => false
+ get "fooz", to: "application_integration_test/test#index"
end
def app
@@ -801,30 +684,30 @@ class ApplicationIntegrationTest < ActionDispatch::IntegrationTest
end
test "includes route helpers" do
- assert_equal '/', empty_string_path
- assert_equal '/foo', foo_path
- assert_equal '/bar', bar_path
+ assert_equal "/", empty_string_path
+ assert_equal "/foo", foo_path
+ assert_equal "/bar", bar_path
end
test "includes mounted helpers" do
- assert_equal '/mounted/baz', mounted.baz_path
+ assert_equal "/mounted/baz", mounted.baz_path
end
test "path after cascade pass" do
- get '/fooz'
- assert_equal 'index', response.body
- assert_equal '/fooz', path
+ get "/fooz"
+ assert_equal "index", response.body
+ assert_equal "/fooz", path
end
test "route helpers after controller access" do
- get '/'
- assert_equal '/', empty_string_path
+ get "/"
+ assert_equal "/", empty_string_path
- get '/foo'
- assert_equal '/foo', foo_path
+ get "/foo"
+ assert_equal "/foo", foo_path
- get '/bar'
- assert_equal '/bar', bar_path
+ get "/bar"
+ assert_equal "/bar", bar_path
end
test "missing route helper before controller access" do
@@ -832,14 +715,14 @@ class ApplicationIntegrationTest < ActionDispatch::IntegrationTest
end
test "missing route helper after controller access" do
- get '/foo'
+ get "/foo"
assert_raise(NameError) { missing_path }
end
test "process do not modify the env passed as argument" do
- env = { :SERVER_NAME => 'server', 'action_dispatch.custom' => 'custom' }
+ env = { :SERVER_NAME => "server", "action_dispatch.custom" => "custom" }
old_env = env.dup
- get '/foo', env: env
+ get "/foo", env: env
assert_equal old_env, env
end
end
@@ -847,7 +730,7 @@ end
class EnvironmentFilterIntegrationTest < ActionDispatch::IntegrationTest
class TestController < ActionController::Base
def post
- render :text => "Created", :status => 201
+ render plain: "Created", status: 201
end
end
@@ -861,7 +744,7 @@ class EnvironmentFilterIntegrationTest < ActionDispatch::IntegrationTest
end
routes.draw do
- match '/post', :to => 'environment_filter_integration_test/test#post', :via => :post
+ match "/post", to: "environment_filter_integration_test/test#post", via: :post
end
def app
@@ -869,36 +752,36 @@ class EnvironmentFilterIntegrationTest < ActionDispatch::IntegrationTest
end
test "filters rack request form vars" do
- post "/post", params: { username: 'cjolly', password: 'secret' }
+ post "/post", params: { username: "cjolly", password: "secret" }
- assert_equal 'cjolly', request.filtered_parameters['username']
- assert_equal '[FILTERED]', request.filtered_parameters['password']
- assert_equal '[FILTERED]', request.filtered_env['rack.request.form_vars']
+ assert_equal "cjolly", request.filtered_parameters["username"]
+ assert_equal "[FILTERED]", request.filtered_parameters["password"]
+ assert_equal "[FILTERED]", request.filtered_env["rack.request.form_vars"]
end
end
class UrlOptionsIntegrationTest < ActionDispatch::IntegrationTest
class FooController < ActionController::Base
def index
- render :text => "foo#index"
+ render plain: "foo#index"
end
def show
- render :text => "foo#show"
+ render plain: "foo#show"
end
def edit
- render :text => "foo#show"
+ render plain: "foo#show"
end
end
class BarController < ActionController::Base
def default_url_options
- { :host => "bar.com" }
+ { host: "bar.com" }
end
def index
- render :text => "foo#index"
+ render plain: "foo#index"
end
end
@@ -915,9 +798,9 @@ class UrlOptionsIntegrationTest < ActionDispatch::IntegrationTest
end
routes.draw do
- default_url_options :host => "foo.com"
+ default_url_options host: "foo.com"
- scope :module => "url_options_integration_test" do
+ scope module: "url_options_integration_test" do
get "/foo" => "foo#index", :as => :foos
get "/foo/:id" => "foo#show", :as => :foo
get "/foo/:id/edit" => "foo#edit", :as => :edit_foo
@@ -957,7 +840,7 @@ class UrlOptionsIntegrationTest < ActionDispatch::IntegrationTest
test "current request path parameters are recalled" do
get "/foo/1"
assert_response :success
- assert_equal "/foo/1/edit", url_for(:action => 'edit', :only_path => true)
+ assert_equal "/foo/1/edit", url_for(action: "edit", only_path: true)
end
end
@@ -981,12 +864,12 @@ class HeadWithStatusActionIntegrationTest < ActionDispatch::IntegrationTest
end
routes.draw do
- get "/foo/status" => 'head_with_status_action_integration_test/foo#status'
+ get "/foo/status" => "head_with_status_action_integration_test/foo#status"
end
test "get /foo/status with head result does not cause stack overflow error" do
assert_nothing_raised do
- get '/foo/status'
+ get "/foo/status"
end
assert_response :ok
end
@@ -995,7 +878,7 @@ end
class IntegrationWithRoutingTest < ActionDispatch::IntegrationTest
class FooController < ActionController::Base
def index
- render plain: 'ok'
+ render plain: "ok"
end
end
@@ -1005,25 +888,25 @@ class IntegrationWithRoutingTest < ActionDispatch::IntegrationTest
with_routing do |routes|
routes.draw do
namespace klass_namespace do
- resources :foo, path: '/with'
+ resources :foo, path: "/with"
end
end
- get '/integration_with_routing_test/with'
+ get "/integration_with_routing_test/with"
assert_response 200
- assert_equal 'ok', response.body
+ assert_equal "ok", response.body
end
with_routing do |routes|
routes.draw do
namespace klass_namespace do
- resources :foo, path: '/routing'
+ resources :foo, path: "/routing"
end
end
- get '/integration_with_routing_test/routing'
+ get "/integration_with_routing_test/routing"
assert_response 200
- assert_equal 'ok', response.body
+ assert_equal "ok", response.body
end
end
end
@@ -1035,19 +918,24 @@ class IntegrationRequestsWithoutSetup < ActionDispatch::IntegrationTest
class FooController < ActionController::Base
def ok
- cookies[:key] = 'ok'
- render plain: 'ok'
+ cookies[:key] = "ok"
+ render plain: "ok"
end
end
def test_request
with_routing do |routes|
- routes.draw { get ':action' => FooController }
- get '/ok'
+ routes.draw do
+ ActiveSupport::Deprecation.silence do
+ get ":action" => FooController
+ end
+ end
+
+ get "/ok"
assert_response 200
- assert_equal 'ok', response.body
- assert_equal 'ok', cookies['key']
+ assert_equal "ok", response.body
+ assert_equal "ok", cookies["key"]
end
end
end
@@ -1055,11 +943,204 @@ end
# to ensure that session requirements in setup are persisted in the tests
class IntegrationRequestsWithSessionSetup < ActionDispatch::IntegrationTest
setup do
- cookies['user_name'] = 'david'
+ cookies["user_name"] = "david"
end
def test_cookies_set_in_setup_are_persisted_through_the_session
get "/foo"
- assert_equal({"user_name"=>"david"}, cookies.to_hash)
+ assert_equal({ "user_name" => "david" }, cookies.to_hash)
+ end
+end
+
+class IntegrationRequestEncodersTest < ActionDispatch::IntegrationTest
+ class FooController < ActionController::Base
+ def foos
+ render plain: "ok"
+ end
+
+ def foos_json
+ render json: params.permit(:foo)
+ end
+
+ def foos_wibble
+ render plain: "ok"
+ end
+ end
+
+ def test_standard_json_encoding_works
+ with_routing do |routes|
+ routes.draw do
+ ActiveSupport::Deprecation.silence do
+ post ":action" => FooController
+ end
+ end
+
+ post "/foos_json.json", params: { foo: "fighters" }.to_json,
+ headers: { "Content-Type" => "application/json" }
+
+ assert_response :success
+ assert_equal({ "foo" => "fighters" }, response.parsed_body)
+ end
+ end
+
+ def test_encoding_as_json
+ post_to_foos as: :json do
+ assert_response :success
+ 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
+ end
+ end
+
+ def test_registering_custom_encoder
+ Mime::Type.register "text/wibble", :wibble
+
+ ActionDispatch::IntegrationTest.register_encoder(:wibble,
+ param_encoder: -> params { params })
+
+ post_to_foos as: :wibble do
+ assert_response :success
+ 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
+ ensure
+ Mime::Type.unregister :wibble
+ end
+
+ def test_parsed_body_without_as_option
+ with_routing do |routes|
+ routes.draw do
+ ActiveSupport::Deprecation.silence do
+ get ":action" => FooController
+ end
+ end
+
+ get "/foos_json.json", params: { foo: "heyo" }
+
+ assert_equal({ "foo" => "heyo" }, response.parsed_body)
+ end
+ end
+
+ def test_get_parameters_with_as_option
+ with_routing do |routes|
+ routes.draw do
+ ActiveSupport::Deprecation.silence do
+ get ":action" => FooController
+ end
+ end
+
+ get "/foos_json?foo=heyo", as: :json
+
+ assert_equal({ "foo" => "heyo" }, response.parsed_body)
+ end
+ end
+
+ def test_get_request_with_json_uses_method_override_and_sends_a_post_request
+ with_routing do |routes|
+ routes.draw do
+ ActiveSupport::Deprecation.silence do
+ get ":action" => FooController
+ end
+ end
+
+ get "/foos_json", params: { foo: "heyo" }, as: :json
+
+ assert_equal "POST", request.method
+ assert_equal "GET", request.headers["X-Http-Method-Override"]
+ assert_equal({ "foo" => "heyo" }, response.parsed_body)
+ end
+ end
+
+ def test_get_request_with_json_excludes_null_query_string
+ with_routing do |routes|
+ routes.draw do
+ ActiveSupport::Deprecation.silence do
+ get ":action" => FooController
+ end
+ end
+
+ get "/foos_json", as: :json
+
+ assert_equal "http://www.example.com/foos_json", request.url
+ end
+ end
+
+ private
+ def post_to_foos(as:)
+ with_routing do |routes|
+ routes.draw do
+ ActiveSupport::Deprecation.silence do
+ post ":action" => FooController
+ end
+ end
+
+ post "/foos_#{as}", params: { foo: "fighters" }, as: as
+
+ yield
+ end
+ end
+end
+
+class IntegrationFileUploadTest < ActionDispatch::IntegrationTest
+ class IntegrationController < ActionController::Base
+ def test_file_upload
+ render plain: params[:file].size
+ end
+ end
+
+ def self.routes
+ @routes ||= ActionDispatch::Routing::RouteSet.new
+ end
+
+ def self.call(env)
+ routes.call(env)
+ end
+
+ def app
+ self.class
+ end
+
+ def self.fixture_path
+ File.expand_path("../fixtures/multipart", __dir__)
+ end
+
+ routes.draw do
+ post "test_file_upload", to: "integration_file_upload_test/integration#test_file_upload"
+ end
+
+ def test_fixture_file_upload
+ post "/test_file_upload",
+ params: {
+ file: fixture_file_upload("/ruby_on_rails.jpg", "image/jpg")
+ }
+ assert_equal "45142", @response.body
end
end
diff --git a/actionpack/test/controller/live_stream_test.rb b/actionpack/test/controller/live_stream_test.rb
index 0c65270ec1..d81c43b87d 100644
--- a/actionpack/test/controller/live_stream_test.rb
+++ b/actionpack/test/controller/live_stream_test.rb
@@ -1,5 +1,8 @@
-require 'abstract_unit'
-require 'active_support/concurrency/latch'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "timeout"
+require "concurrent/atomic/count_down_latch"
Thread.abort_on_exception = true
module ActionController
@@ -8,10 +11,10 @@ module ActionController
include ActionController::Live
def basic_sse
- response.headers['Content-Type'] = 'text/event-stream'
+ response.headers["Content-Type"] = "text/event-stream"
sse = SSE.new(response.stream)
sse.write("{\"name\":\"John\"}")
- sse.write({ name: "Ryan" })
+ sse.write(name: "Ryan")
ensure
sse.close
end
@@ -19,7 +22,7 @@ module ActionController
def sse_with_event
sse = SSE.new(response.stream, event: "send-name")
sse.write("{\"name\":\"John\"}")
- sse.write({ name: "Ryan" })
+ sse.write(name: "Ryan")
ensure
sse.close
end
@@ -27,7 +30,7 @@ module ActionController
def sse_with_retry
sse = SSE.new(response.stream, retry: 1000)
sse.write("{\"name\":\"John\"}")
- sse.write({ name: "Ryan" }, retry: 1500)
+ sse.write({ name: "Ryan" }, { retry: 1500 })
ensure
sse.close
end
@@ -35,7 +38,7 @@ module ActionController
def sse_with_id
sse = SSE.new(response.stream)
sse.write("{\"name\":\"John\"}", id: 1)
- sse.write({ name: "Ryan" }, id: 2)
+ sse.write({ name: "Ryan" }, { id: 2 })
ensure
sse.close
end
@@ -112,10 +115,10 @@ module ActionController
class TestController < ActionController::Base
include ActionController::Live
- attr_accessor :latch, :tc
+ attr_accessor :latch, :tc, :error_latch
def self.controller_path
- 'test'
+ "test"
end
def set_cookie
@@ -125,7 +128,7 @@ module ActionController
end
def render_text
- render :text => 'zomg'
+ render plain: "zomg"
end
def default_header
@@ -134,7 +137,7 @@ module ActionController
end
def basic_stream
- response.headers['Content-Type'] = 'text/event-stream'
+ response.headers["Content-Type"] = "text/event-stream"
%w{ hello world }.each do |word|
response.stream.write word
end
@@ -142,19 +145,34 @@ module ActionController
end
def blocking_stream
- response.headers['Content-Type'] = 'text/event-stream'
+ response.headers["Content-Type"] = "text/event-stream"
%w{ hello world }.each do |word|
response.stream.write word
- latch.await
+ latch.wait
end
response.stream.close
end
+ def write_sleep_autoload
+ path = File.expand_path("../fixtures", __dir__)
+ ActiveSupport::Dependencies.autoload_paths << path
+
+ response.headers["Content-Type"] = "text/event-stream"
+ response.stream.write "before load"
+ sleep 0.01
+ silence_warning do
+ ::LoadMe
+ end
+ response.stream.close
+ latch.count_down
+
+ ActiveSupport::Dependencies.autoload_paths.reject! { |p| p == path }
+ end
+
def thread_locals
- tc.assert_equal 'aaron', Thread.current[:setting]
- tc.assert_not_equal Thread.current.object_id, Thread.current[:originating_thread]
+ tc.assert_equal "aaron", Thread.current[:setting]
- response.headers['Content-Type'] = 'text/event-stream'
+ response.headers["Content-Type"] = "text/event-stream"
%w{ hello world }.each do |word|
response.stream.write word
end
@@ -162,20 +180,20 @@ module ActionController
end
def with_stale
- render text: 'stale' if stale?(etag: "123", template: false)
+ render plain: "stale" if stale?(etag: "123", template: false)
end
def exception_in_view
- render 'doesntexist'
+ render "doesntexist"
end
def exception_in_view_after_commit
response.stream.write ""
- render 'doesntexist'
+ render "doesntexist"
end
def exception_with_callback
- response.headers['Content-Type'] = 'text/event-stream'
+ response.headers["Content-Type"] = "text/event-stream"
response.stream.on_error do
response.stream.write %(data: "500 Internal Server Error"\n\n)
@@ -183,11 +201,11 @@ module ActionController
end
response.stream.write "" # make sure the response is committed
- raise 'An exception occurred...'
+ raise "An exception occurred..."
end
def exception_in_controller
- raise Exception, 'Exception in controller'
+ raise Exception, "Exception in controller"
end
def bad_request_error
@@ -195,53 +213,60 @@ module ActionController
end
def exception_in_exception_callback
- response.headers['Content-Type'] = 'text/event-stream'
+ response.headers["Content-Type"] = "text/event-stream"
response.stream.on_error do
- raise 'We need to go deeper.'
+ raise "We need to go deeper."
end
- response.stream.write ''
+ response.stream.write ""
response.stream.write params[:widget][:didnt_check_for_nil]
end
def overfill_buffer_and_die
+ logger = ActionController::Base.logger || Logger.new($stdout)
+ response.stream.on_error do
+ logger.warn "Error while streaming."
+ error_latch.count_down
+ end
+
# Write until the buffer is full. It doesn't expose that
# information directly, so we must hard-code its size:
10.times do
- response.stream.write '.'
+ response.stream.write "."
end
# .. plus one more, because the #each frees up a slot:
- response.stream.write '.'
+ response.stream.write "."
- latch.release
+ latch.count_down
# This write will block, and eventually raise
- response.stream.write 'x'
+ response.stream.write "x"
20.times do
- response.stream.write '.'
+ response.stream.write "."
end
end
def ignore_client_disconnect
response.stream.ignore_disconnect = true
- response.stream.write '' # commit
+ response.stream.write "" # commit
# These writes will be ignored
15.times do
- response.stream.write 'x'
+ response.stream.write "x"
end
- logger.info 'Work complete'
- latch.release
+ logger.info "Work complete"
+ latch.count_down
end
end
tests TestController
def assert_stream_closed
- assert response.stream.closed?, 'stream should be closed'
- assert response.sent?, 'stream should be sent'
+ assert response.stream.closed?, "stream should be closed"
+ assert response.committed?, "response should be committed"
+ assert response.sent?, "response should be sent"
end
def capture_log_output
@@ -255,125 +280,117 @@ module ActionController
end
end
+ def setup
+ super
+
+ def @controller.new_controller_thread
+ Thread.new { yield }
+ end
+ end
+
def test_set_cookie
- @controller = TestController.new
get :set_cookie
- assert_equal({'hello' => 'world'}, @response.cookies)
+ assert_equal({ "hello" => "world" }, @response.cookies)
assert_equal "hello world", @response.body
end
- def test_set_response!
- @controller.set_response!(@request)
- assert_kind_of(Live::Response, @controller.response)
- assert_equal @request, @controller.response.request
- end
-
def test_write_to_stream
- @controller = TestController.new
get :basic_stream
assert_equal "helloworld", @response.body
- assert_equal 'text/event-stream', @response.headers['Content-Type']
+ assert_equal "text/event-stream", @response.headers["Content-Type"]
+ end
+
+ def test_delayed_autoload_after_write_within_interlock_hook
+ # Simulate InterlockHook
+ ActiveSupport::Dependencies.interlock.start_running
+ res = get :write_sleep_autoload
+ res.each { }
+ ActiveSupport::Dependencies.interlock.done_running
end
def test_async_stream
rubinius_skip "https://github.com/rubinius/rubinius/issues/2934"
- @controller.latch = ActiveSupport::Concurrency::Latch.new
- parts = ['hello', 'world']
+ @controller.latch = Concurrent::CountDownLatch.new
+ parts = ["hello", "world"]
- @controller.request = @request
- @controller.response = @response
+ get :blocking_stream
- t = Thread.new(@response) { |resp|
+ t = Thread.new(response) { |resp|
resp.await_commit
resp.stream.each do |part|
assert_equal parts.shift, part
ol = @controller.latch
- @controller.latch = ActiveSupport::Concurrency::Latch.new
- ol.release
+ @controller.latch = Concurrent::CountDownLatch.new
+ ol.count_down
end
}
- @controller.process :blocking_stream
-
- assert t.join(3), 'timeout expired before the thread terminated'
+ assert t.join(3), "timeout expired before the thread terminated"
end
def test_abort_with_full_buffer
- @controller.latch = ActiveSupport::Concurrency::Latch.new
-
- @request.parameters[:format] = 'plain'
- @controller.request = @request
- @controller.response = @response
-
- got_error = ActiveSupport::Concurrency::Latch.new
- @response.stream.on_error do
- ActionController::Base.logger.warn 'Error while streaming'
- got_error.release
- end
-
- t = Thread.new(@response) { |resp|
- resp.await_commit
- _, _, body = resp.to_a
- body.each do |part|
- @controller.latch.await
- body.close
- break
- end
- }
+ @controller.latch = Concurrent::CountDownLatch.new
+ @controller.error_latch = Concurrent::CountDownLatch.new
capture_log_output do |output|
- @controller.process :overfill_buffer_and_die
+ get :overfill_buffer_and_die, format: "plain"
+
+ t = Thread.new(response) { |resp|
+ resp.await_commit
+ _, _, body = resp.to_a
+ body.each do
+ @controller.latch.wait
+ body.close
+ break
+ end
+ }
+
t.join
- got_error.await
- assert_match 'Error while streaming', output.rewind && output.read
+ @controller.error_latch.wait
+ assert_match "Error while streaming", output.rewind && output.read
end
end
def test_ignore_client_disconnect
- @controller.latch = ActiveSupport::Concurrency::Latch.new
+ @controller.latch = Concurrent::CountDownLatch.new
- @controller.request = @request
- @controller.response = @response
+ capture_log_output do |output|
+ get :ignore_client_disconnect
- t = Thread.new(@response) { |resp|
- resp.await_commit
- _, _, body = resp.to_a
- body.each do |part|
- body.close
- break
- end
- }
+ t = Thread.new(response) { |resp|
+ resp.await_commit
+ _, _, body = resp.to_a
+ body.each do
+ body.close
+ break
+ end
+ }
- capture_log_output do |output|
- @controller.process :ignore_client_disconnect
t.join
Timeout.timeout(3) do
- @controller.latch.await
+ @controller.latch.wait
end
- assert_match 'Work complete', output.rewind && output.read
+ assert_match "Work complete", output.rewind && output.read
end
end
def test_thread_locals_get_copied
@controller.tc = self
Thread.current[:originating_thread] = Thread.current.object_id
- Thread.current[:setting] = 'aaron'
+ Thread.current[:setting] = "aaron"
get :thread_locals
end
def test_live_stream_default_header
- @controller.request = @request
- @controller.response = @response
- @controller.process :default_header
- _, headers, _ = @response.prepare!
- assert headers['Content-Type']
+ get :default_header
+ assert response.headers["Content-Type"]
end
def test_render_text
get :render_text
- assert_equal 'zomg', response.body
+ assert_equal "zomg", response.body
assert_stream_closed
end
@@ -385,7 +402,7 @@ module ActionController
capture_log_output do |output|
get :exception_in_view_after_commit
assert_match %r((window\.location = "/500\.html"</script></html>)$), response.body
- assert_match 'Missing template test/doesntexist', output.rewind && output.read
+ assert_match "Missing template test/doesntexist", output.rewind && output.read
assert_stream_closed
end
assert response.body
@@ -399,51 +416,57 @@ module ActionController
capture_log_output do |output|
get :exception_in_view_after_commit, format: :json
- assert_equal '', response.body
- assert_match 'Missing template test/doesntexist', output.rewind && output.read
+ assert_equal "", response.body
+ assert_match "Missing template test/doesntexist", output.rewind && output.read
assert_stream_closed
end
end
def test_exception_callback_when_committed
+ current_threads = Thread.list
+
capture_log_output do |output|
- get :exception_with_callback, format: 'text/event-stream'
+ get :exception_with_callback, format: "text/event-stream"
+
+ # Wait on the execution of all threads
+ (Thread.list - current_threads).each(&:join)
+
assert_equal %(data: "500 Internal Server Error"\n\n), response.body
- assert_match 'An exception occurred...', output.rewind && output.read
+ assert_match "An exception occurred...", output.rewind && output.read
assert_stream_closed
end
end
def test_exception_in_controller_before_streaming
assert_raises(ActionController::LiveStreamTest::Exception) do
- get :exception_in_controller, format: 'text/event-stream'
+ get :exception_in_controller, format: "text/event-stream"
end
end
def test_bad_request_in_controller_before_streaming
assert_raises(ActionController::BadRequest) do
- get :bad_request_error, format: 'text/event-stream'
+ get :bad_request_error, format: "text/event-stream"
end
end
def test_exceptions_raised_handling_exceptions_and_committed
capture_log_output do |output|
- get :exception_in_exception_callback, format: 'text/event-stream'
- assert_equal '', response.body
- assert_match 'We need to go deeper', output.rewind && output.read
+ get :exception_in_exception_callback, format: "text/event-stream"
+ assert_equal "", response.body
+ assert_match "We need to go deeper", output.rewind && output.read
assert_stream_closed
end
end
def test_stale_without_etag
get :with_stale
- assert_equal 200, @response.status.to_i
+ assert_equal 200, response.status.to_i
end
def test_stale_with_etag
- @request.if_none_match = Digest::MD5.hexdigest("123")
+ @request.if_none_match = %(W/"#{ActiveSupport::Digest.hexdigest('123')}")
get :with_stale
- assert_equal 304, @response.status.to_i
+ assert_equal 304, response.status.to_i
end
end
@@ -454,3 +477,42 @@ module ActionController
end
end
end
+
+class LiveStreamRouterTest < ActionDispatch::IntegrationTest
+ class TestController < ActionController::Base
+ include ActionController::Live
+
+ def index
+ response.headers["Content-Type"] = "text/event-stream"
+ sse = SSE.new(response.stream)
+ sse.write("{\"name\":\"John\"}")
+ sse.write(name: "Ryan")
+ ensure
+ sse.close
+ end
+ end
+
+ def self.call(env)
+ routes.call(env)
+ end
+
+ def self.routes
+ @routes ||= ActionDispatch::Routing::RouteSet.new
+ end
+
+ routes.draw do
+ get "/test" => "live_stream_router_test/test#index"
+ end
+
+ def app
+ self.class
+ end
+
+ test "streaming served through the router" do
+ get "/test"
+
+ assert_response :ok
+ assert_match(/data: {\"name\":\"John\"}/, response.body)
+ assert_match(/data: {\"name\":\"Ryan\"}/, response.body)
+ end
+end
diff --git a/actionpack/test/controller/localized_templates_test.rb b/actionpack/test/controller/localized_templates_test.rb
index 3576015513..d84a76fb46 100644
--- a/actionpack/test/controller/localized_templates_test.rb
+++ b/actionpack/test/controller/localized_templates_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class LocalizedController < ActionController::Base
def hello_world
diff --git a/actionpack/test/controller/log_subscriber_test.rb b/actionpack/test/controller/log_subscriber_test.rb
index 7835d2768a..0562c16284 100644
--- a/actionpack/test/controller/log_subscriber_test.rb
+++ b/actionpack/test/controller/log_subscriber_test.rb
@@ -1,10 +1,12 @@
+# frozen_string_literal: true
+
require "abstract_unit"
require "active_support/log_subscriber/test_helper"
require "action_controller/log_subscriber"
module Another
class LogSubscribersController < ActionController::Base
- wrap_parameters :person, :include => :name, :format => :json
+ wrap_parameters :person, include: :name, format: :json
class SpecialException < Exception
end
@@ -31,7 +33,7 @@ module Another
end
def data_sender
- send_data "cool data", :filename => "file.txt"
+ send_data "cool data", filename: "file.txt"
end
def file_sender
@@ -39,27 +41,27 @@ module Another
end
def with_fragment_cache
- render :inline => "<%= cache('foo'){ 'bar' } %>"
+ render inline: "<%= cache('foo'){ 'bar' } %>"
end
def with_fragment_cache_and_percent_in_key
- render :inline => "<%= cache('foo%bar'){ 'Contains % sign in key' } %>"
+ render inline: "<%= cache('foo%bar'){ 'Contains % sign in key' } %>"
end
def with_fragment_cache_if_with_true_condition
- render :inline => "<%= cache_if(true, 'foo') { 'bar' } %>"
+ render inline: "<%= cache_if(true, 'foo') { 'bar' } %>"
end
def with_fragment_cache_if_with_false_condition
- render :inline => "<%= cache_if(false, 'foo') { 'bar' } %>"
+ render inline: "<%= cache_if(false, 'foo') { 'bar' } %>"
end
def with_fragment_cache_unless_with_false_condition
- render :inline => "<%= cache_unless(false, 'foo') { 'bar' } %>"
+ render inline: "<%= cache_unless(false, 'foo') { 'bar' } %>"
end
def with_fragment_cache_unless_with_true_condition
- render :inline => "<%= cache_unless(true, 'foo') { 'bar' } %>"
+ render inline: "<%= cache_unless(true, 'foo') { 'bar' } %>"
end
def with_exception
@@ -80,9 +82,7 @@ module Another
@last_payload = payload
end
- def last_payload
- @last_payload
- end
+ attr_reader :last_payload
end
end
@@ -92,10 +92,11 @@ class ACLogSubscriberTest < ActionController::TestCase
def setup
super
+ ActionController::Base.enable_fragment_cache_logging = true
@old_logger = ActionController::Base.logger
- @cache_path = File.join Dir.tmpdir, Dir::Tmpname.make_tmpname('tmp', 'cache')
+ @cache_path = Dir.mktmpdir(%w[tmp cache])
@controller.cache_store = :file_store, @cache_path
ActionController::LogSubscriber.attach_to :action_controller
end
@@ -105,6 +106,7 @@ class ACLogSubscriberTest < ActionController::TestCase
ActiveSupport::LogSubscriber.log_subscribers.clear
FileUtils.rm_rf(@cache_path)
ActionController::Base.logger = @old_logger
+ ActionController::Base.enable_fragment_cache_logging = true
end
def set_logger(logger)
@@ -136,11 +138,11 @@ class ACLogSubscriberTest < ActionController::TestCase
def test_process_action_without_parameters
get :show
wait
- assert_nil logs.detect {|l| l =~ /Parameters/ }
+ assert_nil logs.detect { |l| l =~ /Parameters/ }
end
def test_process_action_with_parameters
- get :show, params: { id: '10' }
+ get :show, params: { id: "10" }
wait
assert_equal 3, logs.size
@@ -148,8 +150,8 @@ class ACLogSubscriberTest < ActionController::TestCase
end
def test_multiple_process_with_parameters
- get :show, params: { id: '10' }
- get :show, params: { id: '20' }
+ get :show, params: { id: "10" }
+ get :show, params: { id: "20" }
wait
@@ -159,8 +161,8 @@ class ACLogSubscriberTest < ActionController::TestCase
end
def test_process_action_with_wrapped_parameters
- @request.env['CONTENT_TYPE'] = 'application/json'
- post :show, params: { id: '10', name: 'jose' }
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :show, params: { id: "10", name: "jose" }
wait
assert_equal 3, logs.size
@@ -170,7 +172,7 @@ class ACLogSubscriberTest < ActionController::TestCase
def test_process_action_with_view_runtime
get :show
wait
- assert_match(/Completed 200 OK in [\d]ms/, logs[1])
+ assert_match(/Completed 200 OK in \d+ms/, logs[1])
end
def test_append_info_to_payload_is_called_even_with_exception
@@ -183,11 +185,17 @@ class ACLogSubscriberTest < ActionController::TestCase
assert_equal "test_value", @controller.last_payload[:test_key]
end
+ def test_process_action_headers
+ get :show
+ wait
+ assert_equal "Rails Testing", @controller.last_payload[:headers]["User-Agent"]
+ end
+
def test_process_action_with_filter_parameters
@request.env["action_dispatch.parameter_filter"] = [:lifo, :amount]
get :show, params: {
- lifo: 'Pratik', amount: '420', step: '1'
+ lifo: "Pratik", amount: "420", step: "1"
}
wait
@@ -206,7 +214,7 @@ class ACLogSubscriberTest < ActionController::TestCase
end
def test_filter_redirect_url_by_string
- @request.env['action_dispatch.redirect_filter'] = ['secret']
+ @request.env["action_dispatch.redirect_filter"] = ["secret"]
get :filterable_redirector
wait
@@ -215,7 +223,7 @@ class ACLogSubscriberTest < ActionController::TestCase
end
def test_filter_redirect_url_by_regexp
- @request.env['action_dispatch.redirect_filter'] = [/secret\.foo.+/]
+ @request.env["action_dispatch.redirect_filter"] = [/secret\.foo.+/]
get :filterable_redirector
wait
@@ -252,6 +260,20 @@ class ACLogSubscriberTest < ActionController::TestCase
@controller.config.perform_caching = true
end
+ def test_with_fragment_cache_when_log_disabled
+ @controller.config.perform_caching = true
+ ActionController::Base.enable_fragment_cache_logging = false
+ get :with_fragment_cache
+ wait
+
+ assert_equal 2, logs.size
+ assert_equal "Processing by Another::LogSubscribersController#with_fragment_cache as HTML", logs[0]
+ assert_match(/Completed 200 OK in \d+ms/, logs[1])
+ ensure
+ @controller.config.perform_caching = true
+ ActionController::Base.enable_fragment_cache_logging = true
+ end
+
def test_with_fragment_cache_if_with_true
@controller.config.perform_caching = true
get :with_fragment_cache_if_with_true_condition
diff --git a/actionpack/test/controller/metal/renderers_test.rb b/actionpack/test/controller/metal/renderers_test.rb
new file mode 100644
index 0000000000..5f0d125128
--- /dev/null
+++ b/actionpack/test/controller/metal/renderers_test.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/hash/conversions"
+
+class MetalRenderingController < ActionController::Metal
+ include AbstractController::Rendering
+ include ActionController::Rendering
+ include ActionController::Renderers
+end
+
+class MetalRenderingJsonController < MetalRenderingController
+ class Model
+ def to_json(options = {})
+ { a: "b" }.to_json(options)
+ end
+
+ def to_xml(options = {})
+ { a: "b" }.to_xml(options)
+ end
+ end
+
+ use_renderers :json
+
+ def one
+ render json: Model.new
+ end
+
+ def two
+ render xml: Model.new
+ end
+end
+
+class RenderersMetalTest < ActionController::TestCase
+ tests MetalRenderingJsonController
+
+ def test_render_json
+ get :one
+ assert_response :success
+ assert_equal({ a: "b" }.to_json, @response.body)
+ assert_equal "application/json", @response.content_type
+ end
+
+ def test_render_xml
+ get :two
+ assert_response :success
+ assert_equal(" ", @response.body)
+ assert_equal "text/plain", @response.content_type
+ end
+end
diff --git a/actionpack/test/controller/metal_test.rb b/actionpack/test/controller/metal_test.rb
new file mode 100644
index 0000000000..7b53092266
--- /dev/null
+++ b/actionpack/test/controller/metal_test.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class MetalControllerInstanceTests < ActiveSupport::TestCase
+ class SimpleController < ActionController::Metal
+ def hello
+ self.response_body = "hello"
+ end
+ end
+
+ def test_response_does_not_have_default_headers
+ original_default_headers = ActionDispatch::Response.default_headers
+
+ ActionDispatch::Response.default_headers = {
+ "X-Frame-Options" => "DENY",
+ "X-Content-Type-Options" => "nosniff",
+ "X-XSS-Protection" => "1;"
+ }
+
+ response_headers = SimpleController.action("hello").call(
+ "REQUEST_METHOD" => "GET",
+ "rack.input" => -> { }
+ )[1]
+
+ assert_not response_headers.key?("X-Frame-Options")
+ assert_not response_headers.key?("X-Content-Type-Options")
+ assert_not response_headers.key?("X-XSS-Protection")
+ ensure
+ ActionDispatch::Response.default_headers = original_default_headers
+ end
+end
diff --git a/actionpack/test/controller/mime/accept_format_test.rb b/actionpack/test/controller/mime/accept_format_test.rb
index e20c08da4e..eed671d593 100644
--- a/actionpack/test/controller/mime/accept_format_test.rb
+++ b/actionpack/test/controller/mime/accept_format_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class StarStarMimeController < ActionController::Base
layout nil
@@ -11,7 +13,7 @@ end
class StarStarMimeControllerTest < ActionController::TestCase
def test_javascript_with_format
@request.accept = "text/javascript"
- get :index, format: 'js'
+ get :index, format: "js"
assert_match "function addition(a,b){ return a+b; }", @response.body
end
@@ -29,7 +31,7 @@ class StarStarMimeControllerTest < ActionController::TestCase
end
class AbstractPostController < ActionController::Base
- self.view_paths = File.dirname(__FILE__) + "/../../fixtures/post_test/"
+ self.view_paths = File.expand_path("../../fixtures/post_test", __dir__)
end
# For testing layouts which are set automatically
@@ -40,7 +42,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"
@@ -71,7 +73,7 @@ class MimeControllerLayoutsTest < ActionController::TestCase
@request.accept = "text/iphone"
get :index
- assert_equal 'Hello iPhone', @response.body
+ assert_equal "Hello iPhone", @response.body
end
def test_format_with_inherited_layouts
diff --git a/actionpack/test/controller/mime/respond_to_test.rb b/actionpack/test/controller/mime/respond_to_test.rb
index 71660c87a2..00e1d5f3b3 100644
--- a/actionpack/test/controller/mime/respond_to_test.rb
+++ b/actionpack/test/controller/mime/respond_to_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
require "active_support/log_subscriber/test_helper"
class RespondToController < ActionController::Base
@@ -13,57 +15,56 @@ class RespondToController < ActionController::Base
def html_xml_or_rss
respond_to do |type|
- type.html { render :text => "HTML" }
- type.xml { render :text => "XML" }
- type.rss { render :text => "RSS" }
- type.all { render :text => "Nothing" }
+ type.html { render body: "HTML" }
+ type.xml { render body: "XML" }
+ type.rss { render body: "RSS" }
+ type.all { render body: "Nothing" }
end
end
def js_or_html
respond_to do |type|
- type.html { render :text => "HTML" }
- type.js { render :text => "JS" }
- type.all { render :text => "Nothing" }
+ type.html { render body: "HTML" }
+ type.js { render body: "JS" }
+ type.all { render body: "Nothing" }
end
end
def json_or_yaml
respond_to do |type|
- type.json { render :text => "JSON" }
- type.yaml { render :text => "YAML" }
+ type.json { render body: "JSON" }
+ type.yaml { render body: "YAML" }
end
end
def html_or_xml
respond_to do |type|
- type.html { render :text => "HTML" }
- type.xml { render :text => "XML" }
- type.all { render :text => "Nothing" }
+ type.html { render body: "HTML" }
+ type.xml { render body: "XML" }
+ type.all { render body: "Nothing" }
end
end
def json_xml_or_html
respond_to do |type|
- type.json { render :text => 'JSON' }
- type.xml { render :xml => 'XML' }
- type.html { render :text => 'HTML' }
+ type.json { render body: "JSON" }
+ type.xml { render xml: "XML" }
+ type.html { render body: "HTML" }
end
end
-
def forced_xml
request.format = :xml
respond_to do |type|
- type.html { render :text => "HTML" }
- type.xml { render :text => "XML" }
+ type.html { render body: "HTML" }
+ type.xml { render body: "XML" }
end
end
def just_xml
respond_to do |type|
- type.xml { render :text => "XML" }
+ type.xml { render body: "XML" }
end
end
@@ -74,6 +75,14 @@ class RespondToController < ActionController::Base
end
end
+ def missing_templates
+ respond_to do |type|
+ # This test requires a block that is empty
+ type.json { }
+ type.xml
+ end
+ end
+
def using_defaults_with_type_list
respond_to(:html, :xml)
end
@@ -81,52 +90,71 @@ class RespondToController < ActionController::Base
def using_defaults_with_all
respond_to do |type|
type.html
- type.all{ render text: "ALL" }
+ type.all { render body: "ALL" }
end
end
def made_for_content_type
respond_to do |type|
- type.rss { render :text => "RSS" }
- type.atom { render :text => "ATOM" }
- type.all { render :text => "Nothing" }
+ type.rss { render body: "RSS" }
+ type.atom { render body: "ATOM" }
+ type.all { render body: "Nothing" }
+ end
+ end
+
+ def using_conflicting_nested_js_then_html
+ respond_to do |outer_type|
+ outer_type.js do
+ respond_to do |inner_type|
+ inner_type.html { render body: "HTML" }
+ end
+ end
+ end
+ end
+
+ def using_non_conflicting_nested_js_then_js
+ respond_to do |outer_type|
+ outer_type.js do
+ respond_to do |inner_type|
+ inner_type.js { render body: "JS" }
+ end
+ end
end
end
def custom_type_handling
respond_to do |type|
- type.html { render :text => "HTML" }
- type.custom("application/crazy-xml") { render :text => "Crazy XML" }
- type.all { render :text => "Nothing" }
+ type.html { render body: "HTML" }
+ type.custom("application/crazy-xml") { render body: "Crazy XML" }
+ type.all { render body: "Nothing" }
end
end
-
def custom_constant_handling
respond_to do |type|
- type.html { render :text => "HTML" }
- type.mobile { render :text => "Mobile" }
+ type.html { render body: "HTML" }
+ type.mobile { render body: "Mobile" }
end
end
def custom_constant_handling_without_block
respond_to do |type|
- type.html { render :text => "HTML" }
+ type.html { render body: "HTML" }
type.mobile
end
end
def handle_any
respond_to do |type|
- type.html { render :text => "HTML" }
- type.any(:js, :xml) { render :text => "Either JS or XML" }
+ type.html { render body: "HTML" }
+ type.any(:js, :xml) { render body: "Either JS or XML" }
end
end
def handle_any_any
respond_to do |type|
- type.html { render :text => 'HTML' }
- type.any { render :text => 'Whatever you ask for, I got it' }
+ type.html { render body: "HTML" }
+ type.any { render body: "Whatever you ask for, I got it" }
end
end
@@ -138,7 +166,7 @@ class RespondToController < ActionController::Base
def json_with_callback
respond_to do |type|
- type.json { render :json => 'JS', :callback => 'alert' }
+ type.json { render json: "JS", callback: "alert" }
end
end
@@ -155,27 +183,34 @@ class RespondToController < ActionController::Base
request.format = "iphone" if request.env["HTTP_ACCEPT"] == "text/iphone"
respond_to do |type|
- type.html { @type = "Firefox"; render :action => "iphone_with_html_response_type" }
- type.iphone { @type = "iPhone" ; render :action => "iphone_with_html_response_type" }
+ type.html { @type = "Firefox"; render action: "iphone_with_html_response_type" }
+ type.iphone { @type = "iPhone" ; render action: "iphone_with_html_response_type" }
end
end
- def variant_with_implicit_rendering
+ def variant_with_implicit_template_rendering
+ # This has exactly one variant template defined in the file system (+mobile.html.erb),
+ # which raises the regular MissingTemplate error for other variants.
+ end
+
+ def variant_without_implicit_template_rendering
+ # This differs from the above in that it does not have any templates defined in the file
+ # system, which triggers the ImplicitRender (204 No Content) behavior.
end
def variant_with_format_and_custom_render
request.variant = :mobile
respond_to do |type|
- type.html { render text: "mobile" }
+ type.html { render body: "mobile" }
end
end
def multiple_variants_for_format
respond_to do |type|
type.html do |html|
- html.tablet { render text: "tablet" }
- html.phone { render text: "phone" }
+ html.tablet { render body: "tablet" }
+ html.phone { render body: "phone" }
end
end
end
@@ -183,7 +218,7 @@ class RespondToController < ActionController::Base
def variant_plus_none_for_format
respond_to do |format|
format.html do |variant|
- variant.phone { render text: "phone" }
+ variant.phone { render body: "phone" }
variant.none
end
end
@@ -191,9 +226,9 @@ class RespondToController < ActionController::Base
def variant_inline_syntax
respond_to do |format|
- format.js { render text: "js" }
- format.html.none { render text: "none" }
- format.html.phone { render text: "phone" }
+ format.js { render body: "js" }
+ format.html.none { render body: "none" }
+ format.html.phone { render body: "phone" }
end
end
@@ -208,8 +243,8 @@ class RespondToController < ActionController::Base
def variant_any
respond_to do |format|
format.html do |variant|
- variant.any(:tablet, :phablet){ render text: "any" }
- variant.phone { render text: "phone" }
+ variant.any(:tablet, :phablet) { render body: "any" }
+ variant.phone { render body: "phone" }
end
end
end
@@ -217,23 +252,23 @@ class RespondToController < ActionController::Base
def variant_any_any
respond_to do |format|
format.html do |variant|
- variant.any { render text: "any" }
- variant.phone { render text: "phone" }
+ variant.any { render body: "any" }
+ variant.phone { render body: "phone" }
end
end
end
def variant_inline_any
respond_to do |format|
- format.html.any(:tablet, :phablet){ render text: "any" }
- format.html.phone { render text: "phone" }
+ format.html.any(:tablet, :phablet) { render body: "any" }
+ format.html.phone { render body: "phone" }
end
end
def variant_inline_any_any
respond_to do |format|
- format.html.phone { render text: "phone" }
- format.html.any { render text: "any" }
+ format.html.phone { render body: "phone" }
+ format.html.any { render body: "any" }
end
end
@@ -246,32 +281,34 @@ class RespondToController < ActionController::Base
def variant_any_with_none
respond_to do |format|
- format.html.any(:none, :phone){ render text: "none or phone" }
+ format.html.any(:none, :phone) { render body: "none or phone" }
end
end
def format_any_variant_any
respond_to do |format|
- format.html { render text: "HTML" }
+ format.html { render body: "HTML" }
format.any(:js, :xml) do |variant|
- variant.phone{ render text: "phone" }
- variant.any(:tablet, :phablet){ render text: "tablet" }
+ variant.phone { render body: "phone" }
+ variant.any(:tablet, :phablet) { render body: "tablet" }
end
end
end
- protected
+ private
def set_layout
case action_name
- when "all_types_with_layout", "iphone_with_html_response_type"
- "respond_to/layouts/standard"
- when "iphone_with_html_response_type_without_layout"
- "respond_to/layouts/missing"
+ when "all_types_with_layout", "iphone_with_html_response_type"
+ "respond_to/layouts/standard"
+ when "iphone_with_html_response_type_without_layout"
+ "respond_to/layouts/missing"
end
end
end
class RespondToControllerTest < ActionController::TestCase
+ NO_CONTENT_WARNING = "No template found for RespondToController#variant_without_implicit_template_rendering, rendering head :no_content"
+
def setup
super
@request.host = "www.example.com"
@@ -288,10 +325,10 @@ class RespondToControllerTest < ActionController::TestCase
def test_html
@request.accept = "text/html"
get :js_or_html
- assert_equal 'HTML', @response.body
+ assert_equal "HTML", @response.body
get :html_or_xml
- assert_equal 'HTML', @response.body
+ assert_equal "HTML", @response.body
assert_raises(ActionController::UnknownFormat) do
get :just_xml
@@ -301,29 +338,29 @@ class RespondToControllerTest < ActionController::TestCase
def test_all
@request.accept = "*/*"
get :js_or_html
- assert_equal 'HTML', @response.body # js is not part of all
+ assert_equal "HTML", @response.body # js is not part of all
get :html_or_xml
- assert_equal 'HTML', @response.body
+ assert_equal "HTML", @response.body
get :just_xml
- assert_equal 'XML', @response.body
+ assert_equal "XML", @response.body
end
def test_xml
@request.accept = "application/xml"
get :html_xml_or_rss
- assert_equal 'XML', @response.body
+ assert_equal "XML", @response.body
end
def test_js_or_html
@request.accept = "text/javascript, text/html"
get :js_or_html, xhr: true
- assert_equal 'JS', @response.body
+ assert_equal "JS", @response.body
@request.accept = "text/javascript, text/html"
get :html_or_xml, xhr: true
- assert_equal 'HTML', @response.body
+ assert_equal "HTML", @response.body
@request.accept = "text/javascript, text/html"
@@ -335,25 +372,25 @@ class RespondToControllerTest < ActionController::TestCase
def test_json_or_yaml_with_leading_star_star
@request.accept = "*/*, application/json"
get :json_xml_or_html
- assert_equal 'HTML', @response.body
+ assert_equal "HTML", @response.body
@request.accept = "*/* , application/json"
get :json_xml_or_html
- assert_equal 'HTML', @response.body
+ assert_equal "HTML", @response.body
end
def test_json_or_yaml
get :json_or_yaml, xhr: true
- assert_equal 'JSON', @response.body
+ assert_equal "JSON", @response.body
- get :json_or_yaml, format: 'json'
- assert_equal 'JSON', @response.body
+ get :json_or_yaml, format: "json"
+ assert_equal "JSON", @response.body
- get :json_or_yaml, format: 'yaml'
- assert_equal 'YAML', @response.body
+ get :json_or_yaml, format: "yaml"
+ assert_equal "YAML", @response.body
- { 'YAML' => %w(text/yaml),
- 'JSON' => %w(application/json text/x-json)
+ { "YAML" => %w(text/yaml),
+ "JSON" => %w(application/json text/x-json)
}.each do |body, content_types|
content_types.each do |content_type|
@request.accept = content_type
@@ -366,20 +403,20 @@ class RespondToControllerTest < ActionController::TestCase
def test_js_or_anything
@request.accept = "text/javascript, */*"
get :js_or_html, xhr: true
- assert_equal 'JS', @response.body
+ assert_equal "JS", @response.body
get :html_or_xml, xhr: true
- assert_equal 'HTML', @response.body
+ assert_equal "HTML", @response.body
get :just_xml, xhr: true
- assert_equal 'XML', @response.body
+ assert_equal "XML", @response.body
end
def test_using_defaults
@request.accept = "*/*"
get :using_defaults
assert_equal "text/html", @response.content_type
- assert_equal 'Hello world!', @response.body
+ assert_equal "Hello world!", @response.body
@request.accept = "application/xml"
get :using_defaults
@@ -405,7 +442,7 @@ class RespondToControllerTest < ActionController::TestCase
@request.accept = "*/*"
get :using_defaults_with_type_list
assert_equal "text/html", @response.content_type
- assert_equal 'Hello world!', @response.body
+ assert_equal "Hello world!", @response.body
@request.accept = "application/xml"
get :using_defaults_with_type_list
@@ -413,6 +450,20 @@ class RespondToControllerTest < ActionController::TestCase
assert_equal "<p>Hello world!</p>\n", @response.body
end
+ def test_using_conflicting_nested_js_then_html
+ @request.accept = "*/*"
+ assert_raises(ActionController::RespondToMismatchError) do
+ get :using_conflicting_nested_js_then_html
+ end
+ end
+
+ def test_using_non_conflicting_nested_js_then_js
+ @request.accept = "*/*"
+ get :using_non_conflicting_nested_js_then_js
+ assert_equal "text/javascript", @response.content_type
+ assert_equal "JS", @response.body
+ end
+
def test_with_atom_content_type
@request.accept = ""
@request.env["CONTENT_TYPE"] = "application/atom+xml"
@@ -430,7 +481,7 @@ class RespondToControllerTest < ActionController::TestCase
def test_synonyms
@request.accept = "application/javascript"
get :js_or_html
- assert_equal 'JS', @response.body
+ assert_equal "JS", @response.body
@request.accept = "application/x-xml"
get :html_xml_or_rss
@@ -441,82 +492,82 @@ class RespondToControllerTest < ActionController::TestCase
@request.accept = "application/crazy-xml"
get :custom_type_handling
assert_equal "application/crazy-xml", @response.content_type
- assert_equal 'Crazy XML', @response.body
+ assert_equal "Crazy XML", @response.body
@request.accept = "text/html"
get :custom_type_handling
assert_equal "text/html", @response.content_type
- assert_equal 'HTML', @response.body
+ assert_equal "HTML", @response.body
end
def test_xhtml_alias
@request.accept = "application/xhtml+xml,application/xml"
get :html_or_xml
- assert_equal 'HTML', @response.body
+ assert_equal "HTML", @response.body
end
def test_firefox_simulation
@request.accept = "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5"
get :html_or_xml
- assert_equal 'HTML', @response.body
+ assert_equal "HTML", @response.body
end
def test_handle_any
@request.accept = "*/*"
get :handle_any
- assert_equal 'HTML', @response.body
+ assert_equal "HTML", @response.body
@request.accept = "text/javascript"
get :handle_any
- assert_equal 'Either JS or XML', @response.body
+ assert_equal "Either JS or XML", @response.body
@request.accept = "text/xml"
get :handle_any
- assert_equal 'Either JS or XML', @response.body
+ assert_equal "Either JS or XML", @response.body
end
def test_handle_any_any
@request.accept = "*/*"
get :handle_any_any
- assert_equal 'HTML', @response.body
+ assert_equal "HTML", @response.body
end
def test_handle_any_any_parameter_format
- get :handle_any_any, format: 'html'
- assert_equal 'HTML', @response.body
+ get :handle_any_any, format: "html"
+ assert_equal "HTML", @response.body
end
def test_handle_any_any_explicit_html
@request.accept = "text/html"
get :handle_any_any
- assert_equal 'HTML', @response.body
+ assert_equal "HTML", @response.body
end
def test_handle_any_any_javascript
@request.accept = "text/javascript"
get :handle_any_any
- assert_equal 'Whatever you ask for, I got it', @response.body
+ assert_equal "Whatever you ask for, I got it", @response.body
end
def test_handle_any_any_xml
@request.accept = "text/xml"
get :handle_any_any
- assert_equal 'Whatever you ask for, I got it', @response.body
+ assert_equal "Whatever you ask for, I got it", @response.body
end
- def test_handle_any_any_unkown_format
- get :handle_any_any, format: 'php'
- assert_equal 'Whatever you ask for, I got it', @response.body
+ 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
def test_browser_check_with_any_any
@request.accept = "application/json, application/xml"
get :json_xml_or_html
- assert_equal 'JSON', @response.body
+ assert_equal "JSON", @response.body
@request.accept = "application/json, application/xml, */*"
get :json_xml_or_html
- assert_equal 'HTML', @response.body
+ assert_equal "HTML", @response.body
end
def test_html_type_with_layout
@@ -526,15 +577,15 @@ class RespondToControllerTest < ActionController::TestCase
end
def test_json_with_callback_sets_javascript_content_type
- @request.accept = 'application/json'
+ @request.accept = "application/json"
get :json_with_callback
- assert_equal '/**/alert(JS)', @response.body
- assert_equal 'text/javascript', @response.content_type
+ assert_equal "/**/alert(JS)", @response.body
+ assert_equal "text/javascript", @response.content_type
end
def test_xhr
get :js_or_html, xhr: true
- assert_equal 'JS', @response.body
+ assert_equal "JS", @response.body
end
def test_custom_constant
@@ -615,31 +666,77 @@ class RespondToControllerTest < ActionController::TestCase
end
end
+ def test_missing_templates
+ get :missing_templates, format: :json
+ assert_response :no_content
+ get :missing_templates, format: :xml
+ assert_response :no_content
+ end
+
def test_invalid_variant
+ assert_raises(ActionController::UnknownFormat) do
+ get :variant_with_implicit_template_rendering, params: { v: :invalid }
+ end
+ end
+
+ def test_variant_not_set_regular_unknown_format
+ assert_raises(ActionController::UnknownFormat) do
+ get :variant_with_implicit_template_rendering
+ end
+ end
+
+ def test_variant_with_implicit_template_rendering
+ get :variant_with_implicit_template_rendering, params: { v: :mobile }
+ assert_equal "text/html", @response.content_type
+ assert_equal "mobile", @response.body
+ end
+
+ def test_variant_without_implicit_rendering_from_browser
+ assert_raises(ActionController::MissingExactTemplate) do
+ get :variant_without_implicit_template_rendering, params: { v: :does_not_matter }
+ end
+ end
+
+ def test_variant_variant_not_set_and_without_implicit_rendering_from_browser
+ assert_raises(ActionController::MissingExactTemplate) do
+ get :variant_without_implicit_template_rendering
+ end
+ end
+
+ def test_variant_without_implicit_rendering_from_xhr
logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
old_logger, ActionController::Base.logger = ActionController::Base.logger, logger
- get :variant_with_implicit_rendering, params: { v: :invalid }
+ get :variant_without_implicit_template_rendering, xhr: true, params: { v: :does_not_matter }
assert_response :no_content
- assert_equal 1, logger.logged(:info).select{ |s| s =~ /No template found/ }.size, "Implicit head :no_content not logged"
+
+ assert_equal 1, logger.logged(:info).select { |s| s == NO_CONTENT_WARNING }.size, "Implicit head :no_content not logged"
ensure
ActionController::Base.logger = old_logger
end
- def test_variant_not_set_regular_template_missing
- get :variant_with_implicit_rendering
+ def test_variant_without_implicit_rendering_from_api
+ logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
+ old_logger, ActionController::Base.logger = ActionController::Base.logger, logger
+
+ get :variant_without_implicit_template_rendering, format: "json", params: { v: :does_not_matter }
assert_response :no_content
+
+ assert_equal 1, logger.logged(:info).select { |s| s == NO_CONTENT_WARNING }.size, "Implicit head :no_content not logged"
+ ensure
+ ActionController::Base.logger = old_logger
end
- def test_variant_with_implicit_rendering
- get :variant_with_implicit_rendering, params: { v: :implicit }
+ def test_variant_variant_not_set_and_without_implicit_rendering_from_xhr
+ logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
+ old_logger, ActionController::Base.logger = ActionController::Base.logger, logger
+
+ get :variant_without_implicit_template_rendering, xhr: true
assert_response :no_content
- end
- def test_variant_with_implicit_template_rendering
- get :variant_with_implicit_rendering, params: { v: :mobile }
- assert_equal "text/html", @response.content_type
- assert_equal "mobile", @response.body
+ assert_equal 1, logger.logged(:info).select { |s| s == NO_CONTENT_WARNING }.size, "Implicit head :no_content not logged"
+ ensure
+ ActionController::Base.logger = old_logger
end
def test_variant_with_format_and_custom_render
@@ -661,10 +758,6 @@ class RespondToControllerTest < ActionController::TestCase
end
def test_variant_inline_syntax
- get :variant_inline_syntax, format: :js
- assert_equal "text/javascript", @response.content_type
- assert_equal "js", @response.body
-
get :variant_inline_syntax
assert_equal "text/html", @response.content_type
assert_equal "none", @response.body
@@ -674,6 +767,12 @@ class RespondToControllerTest < ActionController::TestCase
assert_equal "phone", @response.body
end
+ def test_variant_inline_syntax_with_format
+ get :variant_inline_syntax, format: :js
+ assert_equal "text/javascript", @response.content_type
+ assert_equal "js", @response.body
+ end
+
def test_variant_inline_syntax_without_block
get :variant_inline_syntax_without_block, params: { v: :phone }
assert_equal "text/html", @response.content_type
@@ -776,24 +875,3 @@ class RespondToControllerTest < ActionController::TestCase
assert_equal "phone", @response.body
end
end
-
-class RespondToWithBlockOnDefaultRenderController < ActionController::Base
- def show
- default_render do
- render text: 'default_render yielded'
- end
- end
-end
-
-class RespondToWithBlockOnDefaultRenderControllerTest < ActionController::TestCase
- def setup
- super
- @request.host = "www.example.com"
- end
-
- def test_default_render_uses_block_when_no_template_exists
- get :show
- assert_equal "default_render yielded", @response.body
- assert_equal "text/html", @response.content_type
- end
-end
diff --git a/actionpack/test/controller/new_base/bare_metal_test.rb b/actionpack/test/controller/new_base/bare_metal_test.rb
index 710c428dcc..7572d514fb 100644
--- a/actionpack/test/controller/new_base/bare_metal_test.rb
+++ b/actionpack/test/controller/new_base/bare_metal_test.rb
@@ -1,9 +1,9 @@
+# frozen_string_literal: true
+
require "abstract_unit"
module BareMetalTest
class BareController < ActionController::Metal
- include ActionController::RackDelegation
-
def index
self.response_body = "Hello world"
end
@@ -13,7 +13,7 @@ module BareMetalTest
test "response body is a Rack-compatible response" do
status, headers, body = BareController.action(:index).call(Rack::MockRequest.env_for("/"))
assert_equal 200, status
- string = ""
+ string = +""
body.each do |part|
assert part.is_a?(String), "Each part of the body must be a String"
@@ -28,6 +28,8 @@ module BareMetalTest
test "response_body value is wrapped in an array when the value is a String" do
controller = BareController.new
+ controller.set_request!(ActionDispatch::Request.empty)
+ controller.set_response!(BareController.make_response!(controller.request))
controller.index
assert_equal ["Hello world"], controller.response_body
end
@@ -37,8 +39,22 @@ module BareMetalTest
controller = BareController.new
controller.set_request! ActionDispatch::Request.new(env)
assert controller.request
- assert controller.response
- assert env['action_controller.instance']
+ end
+ end
+
+ class BareEmptyController < ActionController::Metal
+ def index
+ self.response_body = nil
+ end
+ end
+
+ class BareEmptyTest < ActiveSupport::TestCase
+ test "response body is nil" do
+ controller = BareEmptyController.new
+ controller.set_request!(ActionDispatch::Request.empty)
+ controller.set_response!(BareController.make_response!(controller.request))
+ controller.index
+ assert_nil controller.response_body
end
end
@@ -88,69 +104,75 @@ module BareMetalTest
test "head :continue (100) does not return a content-type header" do
headers = HeadController.action(:continue).call(Rack::MockRequest.env_for("/")).second
- assert_nil headers['Content-Type']
- assert_nil headers['Content-Length']
+ assert_nil headers["Content-Type"]
+ assert_nil headers["Content-Length"]
end
test "head :switching_protocols (101) does not return a content-type header" do
headers = HeadController.action(:switching_protocols).call(Rack::MockRequest.env_for("/")).second
- assert_nil headers['Content-Type']
- assert_nil headers['Content-Length']
+ assert_nil headers["Content-Type"]
+ assert_nil headers["Content-Length"]
end
test "head :processing (102) does not return a content-type header" do
headers = HeadController.action(:processing).call(Rack::MockRequest.env_for("/")).second
- assert_nil headers['Content-Type']
- assert_nil headers['Content-Length']
+ assert_nil headers["Content-Type"]
+ assert_nil headers["Content-Length"]
end
test "head :no_content (204) does not return a content-type header" do
headers = HeadController.action(:no_content).call(Rack::MockRequest.env_for("/")).second
- assert_nil headers['Content-Type']
- assert_nil headers['Content-Length']
+ assert_nil headers["Content-Type"]
+ assert_nil headers["Content-Length"]
end
test "head :reset_content (205) does not return a content-type header" do
headers = HeadController.action(:reset_content).call(Rack::MockRequest.env_for("/")).second
- assert_nil headers['Content-Type']
- assert_nil headers['Content-Length']
+ assert_nil headers["Content-Type"]
+ assert_nil headers["Content-Length"]
end
test "head :not_modified (304) does not return a content-type header" do
headers = HeadController.action(:not_modified).call(Rack::MockRequest.env_for("/")).second
- assert_nil headers['Content-Type']
- assert_nil headers['Content-Length']
+ assert_nil headers["Content-Type"]
+ assert_nil headers["Content-Length"]
end
test "head :no_content (204) does not return any content" do
- content = HeadController.action(:no_content).call(Rack::MockRequest.env_for("/")).third.first
+ content = body(HeadController.action(:no_content).call(Rack::MockRequest.env_for("/")))
assert_empty content
end
test "head :reset_content (205) does not return any content" do
- content = HeadController.action(:reset_content).call(Rack::MockRequest.env_for("/")).third.first
+ content = body(HeadController.action(:reset_content).call(Rack::MockRequest.env_for("/")))
assert_empty content
end
test "head :not_modified (304) does not return any content" do
- content = HeadController.action(:not_modified).call(Rack::MockRequest.env_for("/")).third.first
+ content = body(HeadController.action(:not_modified).call(Rack::MockRequest.env_for("/")))
assert_empty content
end
test "head :continue (100) does not return any content" do
- content = HeadController.action(:continue).call(Rack::MockRequest.env_for("/")).third.first
+ content = body(HeadController.action(:continue).call(Rack::MockRequest.env_for("/")))
assert_empty content
end
test "head :switching_protocols (101) does not return any content" do
- content = HeadController.action(:switching_protocols).call(Rack::MockRequest.env_for("/")).third.first
+ content = body(HeadController.action(:switching_protocols).call(Rack::MockRequest.env_for("/")))
assert_empty content
end
test "head :processing (102) does not return any content" do
- content = HeadController.action(:processing).call(Rack::MockRequest.env_for("/")).third.first
+ content = body(HeadController.action(:processing).call(Rack::MockRequest.env_for("/")))
assert_empty content
end
+
+ def body(rack_response)
+ buf = []
+ rack_response[2].each { |x| buf << x }
+ buf.join
+ end
end
class BareControllerTest < ActionController::TestCase
diff --git a/actionpack/test/controller/new_base/base_test.rb b/actionpack/test/controller/new_base/base_test.rb
index 964f22eb03..280134f8d2 100644
--- a/actionpack/test/controller/new_base/base_test.rb
+++ b/actionpack/test/controller/new_base/base_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
# Tests the controller dispatching happy path
module Dispatching
@@ -6,7 +8,7 @@ module Dispatching
before_action :authenticate
def index
- render :text => "success"
+ render body: "success"
end
def modify_response_body
@@ -22,10 +24,10 @@ module Dispatching
end
def show_actions
- render :text => "actions: #{action_methods.to_a.sort.join(', ')}"
+ render body: "actions: #{action_methods.to_a.sort.join(', ')}"
end
- protected
+ private
def authenticate
end
end
@@ -45,23 +47,20 @@ module Dispatching
end
class BaseTest < Rack::TestCase
- # :api: plugin
test "simple dispatching" do
get "/dispatching/simple/index"
assert_body "success"
assert_status 200
- assert_content_type "text/html; charset=utf-8"
+ assert_content_type "text/plain; charset=utf-8"
end
- # :api: plugin
test "directly modifying response body" do
get "/dispatching/simple/modify_response_body"
assert_body "success"
end
- # :api: plugin
test "directly modifying response body twice" do
get "/dispatching/simple/modify_response_body_twice"
@@ -69,48 +68,48 @@ module Dispatching
end
test "controller path" do
- assert_equal 'dispatching/empty', EmptyController.controller_path
+ assert_equal "dispatching/empty", EmptyController.controller_path
assert_equal EmptyController.controller_path, EmptyController.new.controller_path
end
test "non-default controller path" do
- assert_equal 'i_am_not_default', NonDefaultPathController.controller_path
+ assert_equal "i_am_not_default", NonDefaultPathController.controller_path
assert_equal NonDefaultPathController.controller_path, NonDefaultPathController.new.controller_path
end
test "sub controller path" do
- assert_equal 'dispatching/sub_empty', SubEmptyController.controller_path
+ assert_equal "dispatching/sub_empty", SubEmptyController.controller_path
assert_equal SubEmptyController.controller_path, SubEmptyController.new.controller_path
end
test "namespaced controller path" do
- assert_equal 'dispatching/submodule/contained_empty', Submodule::ContainedEmptyController.controller_path
+ assert_equal "dispatching/submodule/contained_empty", Submodule::ContainedEmptyController.controller_path
assert_equal Submodule::ContainedEmptyController.controller_path, Submodule::ContainedEmptyController.new.controller_path
end
test "namespaced non-default controller path" do
- assert_equal 'i_am_extremely_not_default', Submodule::ContainedNonDefaultPathController.controller_path
+ assert_equal "i_am_extremely_not_default", Submodule::ContainedNonDefaultPathController.controller_path
assert_equal Submodule::ContainedNonDefaultPathController.controller_path, Submodule::ContainedNonDefaultPathController.new.controller_path
end
test "namespaced sub controller path" do
- assert_equal 'dispatching/submodule/contained_sub_empty', Submodule::ContainedSubEmptyController.controller_path
+ assert_equal "dispatching/submodule/contained_sub_empty", Submodule::ContainedSubEmptyController.controller_path
assert_equal Submodule::ContainedSubEmptyController.controller_path, Submodule::ContainedSubEmptyController.new.controller_path
end
test "controller name" do
- assert_equal 'empty', EmptyController.controller_name
- assert_equal 'contained_empty', Submodule::ContainedEmptyController.controller_name
+ assert_equal "empty", EmptyController.controller_name
+ assert_equal "contained_empty", Submodule::ContainedEmptyController.controller_name
end
test "non-default path controller name" do
- assert_equal 'non_default_path', NonDefaultPathController.controller_name
- assert_equal 'contained_non_default_path', Submodule::ContainedNonDefaultPathController.controller_name
+ assert_equal "non_default_path", NonDefaultPathController.controller_name
+ assert_equal "contained_non_default_path", Submodule::ContainedNonDefaultPathController.controller_name
end
test "sub controller name" do
- assert_equal 'sub_empty', SubEmptyController.controller_name
- assert_equal 'contained_sub_empty', Submodule::ContainedSubEmptyController.controller_name
+ assert_equal "sub_empty", SubEmptyController.controller_name
+ assert_equal "contained_sub_empty", Submodule::ContainedSubEmptyController.controller_name
end
test "action methods" do
diff --git a/actionpack/test/controller/new_base/content_negotiation_test.rb b/actionpack/test/controller/new_base/content_negotiation_test.rb
index c8166280fc..7205e90176 100644
--- a/actionpack/test/controller/new_base/content_negotiation_test.rb
+++ b/actionpack/test/controller/new_base/content_negotiation_test.rb
@@ -1,7 +1,8 @@
-require 'abstract_unit'
+# frozen_string_literal: true
-module ContentNegotiation
+require "abstract_unit"
+module ContentNegotiation
# This has no layout and it works
class BasicController < ActionController::Base
self.view_paths = [ActionView::FixtureResolver.new(
@@ -9,7 +10,7 @@ module ContentNegotiation
)]
def all
- render :text => self.formats.inspect
+ render plain: formats.inspect
end
end
diff --git a/actionpack/test/controller/new_base/content_type_test.rb b/actionpack/test/controller/new_base/content_type_test.rb
index 88453988dd..d3ee4a8a6f 100644
--- a/actionpack/test/controller/new_base/content_type_test.rb
+++ b/actionpack/test/controller/new_base/content_type_test.rb
@@ -1,18 +1,20 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module ContentType
class BaseController < ActionController::Base
def index
- render :text => "Hello world!"
+ render body: "Hello world!"
end
def set_on_response_obj
- response.content_type = Mime::RSS
- render :text => "Hello world!"
+ response.content_type = Mime[:rss]
+ render body: "Hello world!"
end
def set_on_render
- render :text => "Hello world!", :content_type => Mime::RSS
+ render body: "Hello world!", content_type: Mime[:rss]
end
end
@@ -30,26 +32,28 @@ module ContentType
class CharsetController < ActionController::Base
def set_on_response_obj
response.charset = "utf-16"
- render :text => "Hello world!"
+ render body: "Hello world!"
end
def set_as_nil_on_response_obj
response.charset = nil
- render :text => "Hello world!"
+ render body: "Hello world!"
end
end
class ExplicitContentTypeTest < Rack::TestCase
- test "default response is HTML and UTF8" do
+ test "default response is text/plain and UTF8" do
with_routing do |set|
set.draw do
- get ':controller', :action => 'index'
+ ActiveSupport::Deprecation.silence do
+ get ":controller", action: "index"
+ end
end
get "/content_type/base"
assert_body "Hello world!"
- assert_header "Content-Type", "text/html; charset=utf-8"
+ assert_header "Content-Type", "text/plain; charset=utf-8"
end
end
@@ -99,14 +103,14 @@ module ContentType
get "/content_type/charset/set_on_response_obj"
assert_body "Hello world!"
- assert_header "Content-Type", "text/html; charset=utf-16"
+ assert_header "Content-Type", "text/plain; charset=utf-16"
end
test "setting the charset of the response as nil directly on the response object" do
get "/content_type/charset/set_as_nil_on_response_obj"
assert_body "Hello world!"
- assert_header "Content-Type", "text/html; charset=utf-8"
+ assert_header "Content-Type", "text/plain; charset=utf-8"
end
end
end
diff --git a/actionpack/test/controller/new_base/metal_test.rb b/actionpack/test/controller/new_base/metal_test.rb
deleted file mode 100644
index 537b93387a..0000000000
--- a/actionpack/test/controller/new_base/metal_test.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-require 'abstract_unit'
-
-module MetalTest
- class MetalMiddleware < ActionController::Middleware
- def call(env)
- if env["PATH_INFO"] =~ /authed/
- app.call(env)
- else
- [401, headers, "Not authed!"]
- end
- end
- end
-
- class Endpoint
- def call(env)
- [200, {}, "Hello World"]
- end
- end
-
- class TestMiddleware < ActiveSupport::TestCase
- def setup
- @app = Rack::Builder.new do
- use MetalTest::MetalMiddleware
- run MetalTest::Endpoint.new
- end.to_app
- end
-
- test "it can call the next app by using @app" do
- env = Rack::MockRequest.env_for("/authed")
- response = @app.call(env)
-
- assert_equal ["Hello World"], response[2]
- end
-
- test "it can return a response using the normal AC::Metal techniques" do
- env = Rack::MockRequest.env_for("/")
- response = @app.call(env)
-
- assert_equal ["Not authed!"], response[2]
- assert_equal 401, response[0]
- end
- end
-end
diff --git a/actionpack/test/controller/new_base/middleware_test.rb b/actionpack/test/controller/new_base/middleware_test.rb
index a30e937bb3..df69650a7b 100644
--- a/actionpack/test/controller/new_base/middleware_test.rb
+++ b/actionpack/test/controller/new_base/middleware_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module MiddlewareTest
class MyMiddleware
@@ -21,7 +23,7 @@ module MiddlewareTest
def call(env)
result = @app.call(env)
- result[1]["Middleware-Order"] << "!"
+ result[1]["Middleware-Order"] += "!"
result
end
end
@@ -56,8 +58,8 @@ module MiddlewareTest
end
class ActionsController < ActionController::Metal
- use MyMiddleware, :only => :show
- middleware.insert_before MyMiddleware, ExclaimerMiddleware, :except => :index
+ use MyMiddleware, only: :show
+ middleware.insert_before MyMiddleware, ExclaimerMiddleware, except: :index
def index
self.response_body = "index"
@@ -75,7 +77,7 @@ module MiddlewareTest
test "middleware that is 'use'd is called as part of the Rack application" do
result = @app.call(env_for("/"))
- assert_equal ["Hello World"], result[2]
+ assert_equal ["Hello World"], [].tap { |a| result[2].each { |x| a << x } }
assert_equal "Success", result[1]["Middleware-Test"]
end
diff --git a/actionpack/test/controller/new_base/render_action_test.rb b/actionpack/test/controller/new_base/render_action_test.rb
index 3bf1dd0ede..33b55dc5a8 100644
--- a/actionpack/test/controller/new_base/render_action_test.rb
+++ b/actionpack/test/controller/new_base/render_action_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module RenderAction
# This has no layout and it works
@@ -8,7 +10,7 @@ module RenderAction
)]
def hello_world
- render :action => "hello_world"
+ render action: "hello_world"
end
def hello_world_as_string
@@ -16,7 +18,7 @@ module RenderAction
end
def hello_world_as_string_with_options
- render "hello_world", :status => 404
+ render "hello_world", status: 404
end
def hello_world_as_symbol
@@ -24,25 +26,24 @@ module RenderAction
end
def hello_world_with_symbol
- render :action => :hello_world
+ render action: :hello_world
end
def hello_world_with_layout
- render :action => "hello_world", :layout => true
+ render action: "hello_world", layout: true
end
def hello_world_with_layout_false
- render :action => "hello_world", :layout => false
+ render action: "hello_world", layout: false
end
def hello_world_with_layout_nil
- render :action => "hello_world", :layout => nil
+ render action: "hello_world", layout: nil
end
def hello_world_with_custom_layout
- render :action => "hello_world", :layout => "greetings"
+ render action: "hello_world", layout: "greetings"
end
-
end
class RenderActionTest < Rack::TestCase
@@ -127,27 +128,27 @@ module RenderActionWithApplicationLayout
)]
def hello_world
- render :action => "hello_world"
+ render action: "hello_world"
end
def hello_world_with_layout
- render :action => "hello_world", :layout => true
+ render action: "hello_world", layout: true
end
def hello_world_with_layout_false
- render :action => "hello_world", :layout => false
+ render action: "hello_world", layout: false
end
def hello_world_with_layout_nil
- render :action => "hello_world", :layout => nil
+ render action: "hello_world", layout: nil
end
def hello_world_with_custom_layout
- render :action => "hello_world", :layout => "greetings"
+ render action: "hello_world", layout: "greetings"
end
def with_builder_and_layout
- render :action => "hello", :layout => "builder"
+ render action: "hello", layout: "builder"
end
end
@@ -196,7 +197,6 @@ module RenderActionWithApplicationLayout
assert_response "<html>\n<p>Hello</p>\n</html>\n"
end
end
-
end
module RenderActionWithControllerLayout
@@ -207,23 +207,23 @@ module RenderActionWithControllerLayout
)]
def hello_world
- render :action => "hello_world"
+ render action: "hello_world"
end
def hello_world_with_layout
- render :action => "hello_world", :layout => true
+ render action: "hello_world", layout: true
end
def hello_world_with_layout_false
- render :action => "hello_world", :layout => false
+ render action: "hello_world", layout: false
end
def hello_world_with_layout_nil
- render :action => "hello_world", :layout => nil
+ render action: "hello_world", layout: nil
end
def hello_world_with_custom_layout
- render :action => "hello_world", :layout => "greetings"
+ render action: "hello_world", layout: "greetings"
end
end
@@ -260,26 +260,25 @@ end
module RenderActionWithBothLayouts
class BasicController < ActionController::Base
- self.view_paths = [ActionView::FixtureResolver.new({
- "render_action_with_both_layouts/basic/hello_world.html.erb" => "Hello World!",
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "render_action_with_both_layouts/basic/hello_world.html.erb" => "Hello World!",
"layouts/application.html.erb" => "Oh Hi <%= yield %> Bye",
- "layouts/render_action_with_both_layouts/basic.html.erb" => "With Controller Layout! <%= yield %> Bye"
- })]
+ "layouts/render_action_with_both_layouts/basic.html.erb" => "With Controller Layout! <%= yield %> Bye")]
def hello_world
- render :action => "hello_world"
+ render action: "hello_world"
end
def hello_world_with_layout
- render :action => "hello_world", :layout => true
+ render action: "hello_world", layout: true
end
def hello_world_with_layout_false
- render :action => "hello_world", :layout => false
+ render action: "hello_world", layout: false
end
def hello_world_with_layout_nil
- render :action => "hello_world", :layout => nil
+ render action: "hello_world", layout: nil
end
end
diff --git a/actionpack/test/controller/new_base/render_body_test.rb b/actionpack/test/controller/new_base/render_body_test.rb
index f4a3db8b41..d0b61f0665 100644
--- a/actionpack/test/controller/new_base/render_body_test.rb
+++ b/actionpack/test/controller/new_base/render_body_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module RenderBody
class MinimalController < ActionController::Metal
@@ -66,7 +68,7 @@ module RenderBody
end
def with_custom_content_type
- response.headers['Content-Type'] = 'application/json'
+ response.headers["Content-Type"] = "application/json"
render body: '["troll","face"]'
end
@@ -85,7 +87,7 @@ module RenderBody
test "rendering body from an action with default options renders the body with the layout" do
with_routing do |set|
- set.draw { get ':controller', action: 'index' }
+ set.draw { ActiveSupport::Deprecation.silence { get ":controller", action: "index" } }
get "/render_body/simple"
assert_body "hello david"
@@ -95,7 +97,7 @@ module RenderBody
test "rendering body from an action with default options renders the body without the layout" do
with_routing do |set|
- set.draw { get ':controller', action: 'index' }
+ set.draw { ActiveSupport::Deprecation.silence { get ":controller", action: "index" } }
get "/render_body/with_layout"
@@ -150,7 +152,7 @@ module RenderBody
get "/render_body/with_layout/with_custom_content_type"
assert_equal %w{ troll face }, JSON.parse(response.body)
- assert_equal 'application/json', response.headers['Content-Type']
+ assert_equal "application/json", response.headers["Content-Type"]
end
test "rendering body with layout: false" do
diff --git a/actionpack/test/controller/new_base/render_context_test.rb b/actionpack/test/controller/new_base/render_context_test.rb
index 177a1c088d..5e570a1d79 100644
--- a/actionpack/test/controller/new_base/render_context_test.rb
+++ b/actionpack/test/controller/new_base/render_context_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
# This is testing the decoupling of view renderer and view context
# by allowing the controller to be used as view context. This is
@@ -18,24 +20,23 @@ module RenderContext
def hello_world
@value = "Hello"
- render :action => "hello_world", :layout => false
+ render action: "hello_world", layout: false
end
def with_layout
@value = "Hello"
- render :action => "hello_world", :layout => "basic"
+ render action: "hello_world", layout: "basic"
end
- protected
-
- # 3) Set view_context to self
- def view_context
- self
- end
-
- def __controller_method__
+ protected def __controller_method__
"controller context!"
end
+
+ private
+ # 3) Set view_context to self
+ def view_context
+ self
+ end
end
class RenderContextTest < Rack::TestCase
diff --git a/actionpack/test/controller/new_base/render_file_test.rb b/actionpack/test/controller/new_base/render_file_test.rb
index 0c21bb0719..de8af029e0 100644
--- a/actionpack/test/controller/new_base/render_file_test.rb
+++ b/actionpack/test/controller/new_base/render_file_test.rb
@@ -1,36 +1,38 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module RenderFile
class BasicController < ActionController::Base
- self.view_paths = File.dirname(__FILE__)
+ self.view_paths = __dir__
def index
- render :file => File.join(File.dirname(__FILE__), *%w[.. .. fixtures test hello_world])
+ render file: File.expand_path("../../fixtures/test/hello_world", __dir__)
end
def with_instance_variables
- @secret = 'in the sauce'
- render :file => File.join(File.dirname(__FILE__), '../../fixtures/test/render_file_with_ivar')
+ @secret = "in the sauce"
+ render file: File.expand_path("../../fixtures/test/render_file_with_ivar", __dir__)
end
def relative_path
- @secret = 'in the sauce'
- render :file => '../../fixtures/test/render_file_with_ivar'
+ @secret = "in the sauce"
+ render file: "../../fixtures/test/render_file_with_ivar"
end
def relative_path_with_dot
- @secret = 'in the sauce'
- render :file => '../../fixtures/test/dot.directory/render_file_with_ivar'
+ @secret = "in the sauce"
+ render file: "../../fixtures/test/dot.directory/render_file_with_ivar"
end
def pathname
- @secret = 'in the sauce'
- render :file => Pathname.new(File.dirname(__FILE__)).join(*%w[.. .. fixtures test dot.directory render_file_with_ivar])
+ @secret = "in the sauce"
+ render file: Pathname.new(__dir__).join(*%w[.. .. fixtures test dot.directory render_file_with_ivar])
end
def with_locals
- path = File.join(File.dirname(__FILE__), '../../fixtures/test/render_file_with_locals')
- render :file => path, :locals => {:secret => 'in the sauce'}
+ path = File.expand_path("../../fixtures/test/render_file_with_locals", __dir__)
+ render file: path, locals: { secret: "in the sauce" }
end
end
diff --git a/actionpack/test/controller/new_base/render_html_test.rb b/actionpack/test/controller/new_base/render_html_test.rb
index fe11501eeb..4bea2ba2e9 100644
--- a/actionpack/test/controller/new_base/render_html_test.rb
+++ b/actionpack/test/controller/new_base/render_html_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module RenderHtml
class MinimalController < ActionController::Metal
@@ -88,7 +90,7 @@ module RenderHtml
test "rendering text from an action with default options renders the text with the layout" do
with_routing do |set|
- set.draw { get ':controller', action: 'index' }
+ set.draw { ActiveSupport::Deprecation.silence { get ":controller", action: "index" } }
get "/render_html/simple"
assert_body "hello david"
@@ -98,7 +100,7 @@ module RenderHtml
test "rendering text from an action with default options renders the text without the layout" do
with_routing do |set|
- set.draw { get ':controller', action: 'index' }
+ set.draw { ActiveSupport::Deprecation.silence { get ":controller", action: "index" } }
get "/render_html/with_layout"
@@ -179,7 +181,7 @@ module RenderHtml
test "rendering from minimal controller returns response with text/html content type" do
get "/render_html/minimal/index"
- assert_content_type "text/html"
+ assert_content_type "text/html; charset=utf-8"
end
test "rendering from normal controller returns response with text/html content type" do
diff --git a/actionpack/test/controller/new_base/render_implicit_action_test.rb b/actionpack/test/controller/new_base/render_implicit_action_test.rb
index 5b4885f7e0..8c26d34b00 100644
--- a/actionpack/test/controller/new_base/render_implicit_action_test.rb
+++ b/actionpack/test/controller/new_base/render_implicit_action_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module RenderImplicitAction
class SimpleController < ::ApplicationController
@@ -6,7 +8,7 @@ module RenderImplicitAction
"render_implicit_action/simple/hello_world.html.erb" => "Hello world!",
"render_implicit_action/simple/hyphen-ated.html.erb" => "Hello hyphen-ated!",
"render_implicit_action/simple/not_implemented.html.erb" => "Not Implemented"
- ), ActionView::FileSystemResolver.new(File.expand_path('../../../controller', __FILE__))]
+ ), ActionView::FileSystemResolver.new(File.expand_path("../../controller", __dir__))]
def hello_world() end
end
diff --git a/actionpack/test/controller/new_base/render_layout_test.rb b/actionpack/test/controller/new_base/render_layout_test.rb
index 7ab3777026..806c6206dc 100644
--- a/actionpack/test/controller/new_base/render_layout_test.rb
+++ b/actionpack/test/controller/new_base/render_layout_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module ControllerLayouts
class ImplicitController < ::ApplicationController
@@ -10,15 +12,15 @@ module ControllerLayouts
)]
def index
- render :template => "basic"
+ render template: "basic"
end
def override
- render :template => "basic", :layout => "override"
+ render template: "basic", layout: "override"
end
def layout_false
- render :layout => false
+ render layout: false
end
def builder_override
@@ -32,7 +34,7 @@ module ControllerLayouts
)]
def index
- render :template => "basic"
+ render template: "basic"
end
end
@@ -55,7 +57,6 @@ module ControllerLayouts
get "/controller_layouts/implicit/override"
assert_body "Override! Hello world!"
end
-
end
class LayoutOptionsTest < Rack::TestCase
@@ -76,7 +77,7 @@ module ControllerLayouts
)]
def explicit
- render :layout => "application"
+ render layout: "application"
end
end
diff --git a/actionpack/test/controller/new_base/render_partial_test.rb b/actionpack/test/controller/new_base/render_partial_test.rb
index 9e5022c9f4..a0c7cbc686 100644
--- a/actionpack/test/controller/new_base/render_partial_test.rb
+++ b/actionpack/test/controller/new_base/render_partial_test.rb
@@ -1,9 +1,9 @@
-require 'abstract_unit'
+# frozen_string_literal: true
-module RenderPartial
+require "abstract_unit"
+module RenderPartial
class BasicController < ActionController::Base
-
self.view_paths = [ActionView::FixtureResolver.new(
"render_partial/basic/_basic.html.erb" => "BasicPartial!",
"render_partial/basic/basic.html.erb" => "<%= @test_unchanged = 'goodbye' %><%= render :partial => 'basic' %><%= @test_unchanged %>",
@@ -16,16 +16,16 @@ module RenderPartial
)]
def html_with_json_inside_json
- render :action => "with_json"
+ render action: "with_json"
end
def changing
- @test_unchanged = 'hello'
- render :action => "basic"
+ @test_unchanged = "hello"
+ render action: "basic"
end
def overridden
- @test_unchanged = 'hello'
+ @test_unchanged = "hello"
end
end
@@ -59,5 +59,4 @@ module RenderPartial
assert_response("goodbyeOverriddenPartial!goodbye")
end
end
-
end
diff --git a/actionpack/test/controller/new_base/render_plain_test.rb b/actionpack/test/controller/new_base/render_plain_test.rb
index 0e36d36b50..640979e4f5 100644
--- a/actionpack/test/controller/new_base/render_plain_test.rb
+++ b/actionpack/test/controller/new_base/render_plain_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module RenderPlain
class MinimalController < ActionController::Metal
@@ -80,7 +82,7 @@ module RenderPlain
test "rendering text from an action with default options renders the text with the layout" do
with_routing do |set|
- set.draw { get ':controller', action: 'index' }
+ set.draw { ActiveSupport::Deprecation.silence { get ":controller", action: "index" } }
get "/render_plain/simple"
assert_body "hello david"
@@ -90,7 +92,7 @@ module RenderPlain
test "rendering text from an action with default options renders the text without the layout" do
with_routing do |set|
- set.draw { get ':controller', action: 'index' }
+ set.draw { ActiveSupport::Deprecation.silence { get ":controller", action: "index" } }
get "/render_plain/with_layout"
@@ -157,7 +159,7 @@ module RenderPlain
test "rendering from minimal controller returns response with text/plain content type" do
get "/render_plain/minimal/index"
- assert_content_type "text/plain"
+ assert_content_type "text/plain; charset=utf-8"
end
test "rendering from normal controller returns response with text/plain content type" do
diff --git a/actionpack/test/controller/new_base/render_streaming_test.rb b/actionpack/test/controller/new_base/render_streaming_test.rb
index 9ea056194a..23dc6bca40 100644
--- a/actionpack/test/controller/new_base/render_streaming_test.rb
+++ b/actionpack/test/controller/new_base/render_streaming_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module RenderStreaming
class BasicController < ActionController::Base
@@ -12,32 +14,32 @@ module RenderStreaming
layout "application"
def hello_world
- render :stream => true
+ render stream: true
end
def layout_exception
- render :action => "hello_world", :stream => true, :layout => "boom"
+ render action: "hello_world", stream: true, layout: "boom"
end
def template_exception
- render :action => "boom", :stream => true
+ render action: "boom", stream: true
end
def skip
- render :action => "hello_world", :stream => false
+ render action: "hello_world", stream: false
end
def explicit
- render :action => "hello_world", :stream => true
+ render action: "hello_world", stream: true
end
def no_layout
- render :action => "hello_world", :stream => true, :layout => false
+ render action: "hello_world", stream: true, layout: false
end
def explicit_cache
headers["Cache-Control"] = "private"
- render :action => "hello_world", :stream => true
+ render action: "hello_world", stream: true
end
end
@@ -101,12 +103,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")
+ 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/new_base/render_template_test.rb b/actionpack/test/controller/new_base/render_template_test.rb
index b06ce5db40..14dc958475 100644
--- a/actionpack/test/controller/new_base/render_template_test.rb
+++ b/actionpack/test/controller/new_base/render_template_test.rb
@@ -1,8 +1,9 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module RenderTemplate
class WithoutLayoutController < ActionController::Base
-
self.view_paths = [ActionView::FixtureResolver.new(
"test/basic.html.erb" => "Hello from basic.html.erb",
"shared.html.erb" => "Elastica",
@@ -18,11 +19,11 @@ module RenderTemplate
)]
def index
- render :template => "test/basic"
+ render template: "test/basic"
end
def html_with_json_inside_json
- render :template => "test/with_json"
+ render template: "test/with_json"
end
def index_without_key
@@ -30,46 +31,46 @@ module RenderTemplate
end
def in_top_directory
- render :template => 'shared'
+ render template: "shared"
end
def in_top_directory_with_slash
- render :template => '/shared'
+ render template: "/shared"
end
def in_top_directory_with_slash_without_key
- render '/shared'
+ render "/shared"
end
def with_locals
- render :template => "locals", :locals => { :secret => 'area51' }
+ render template: "locals", locals: { secret: "area51" }
end
def with_locals_without_key
- render "locals", :locals => { :secret => 'area51' }
+ render "locals", locals: { secret: "area51" }
end
def builder_template
- render :template => "xml_template"
+ render template: "xml_template"
end
def with_raw
- render :template => "with_raw"
+ render template: "with_raw"
end
def with_implicit_raw
- render :template => "with_implicit_raw"
+ render template: "with_implicit_raw"
end
def with_error
- render :template => "test/with_error"
+ render template: "test/with_error"
end
private
- def show_detailed_exceptions?
- request.local?
- end
+ def show_detailed_exceptions?
+ request.local?
+ end
end
class TestWithoutLayout < Rack::TestCase
@@ -126,7 +127,7 @@ module RenderTemplate
assert_body "Hello <strong>this is also raw</strong> in an html template"
assert_status 200
- get :with_implicit_raw, params: { format: 'text' }
+ get :with_implicit_raw, params: { format: "text" }
assert_body "Hello <strong>this is also raw</strong> in a text template"
assert_status 200
@@ -154,30 +155,30 @@ module RenderTemplate
)]
def index
- render :template => "test/basic"
+ render template: "test/basic"
end
def with_layout
- render :template => "test/basic", :layout => true
+ render template: "test/basic", layout: true
end
def with_layout_false
- render :template => "test/basic", :layout => false
+ render template: "test/basic", layout: false
end
def with_layout_nil
- render :template => "test/basic", :layout => nil
+ render template: "test/basic", layout: nil
end
def with_custom_layout
- render :template => "test/basic", :layout => "greetings"
+ render template: "test/basic", layout: "greetings"
end
end
class TestWithLayout < Rack::TestCase
test "rendering with implicit layout" do
with_routing do |set|
- set.draw { get ':controller', :action => :index }
+ set.draw { ActiveSupport::Deprecation.silence { get ":controller", action: :index } }
get "/render_template/with_layout"
@@ -223,7 +224,7 @@ module RenderTemplate
)]
def with_forward_slash
- render :template => "/test/basic"
+ render template: "/test/basic"
end
end
diff --git a/actionpack/test/controller/new_base/render_test.rb b/actionpack/test/controller/new_base/render_test.rb
index 11a19ab783..eb29203f59 100644
--- a/actionpack/test/controller/new_base/render_test.rb
+++ b/actionpack/test/controller/new_base/render_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module Render
class BlankRenderController < ActionController::Base
@@ -18,11 +20,11 @@ module Render
end
def access_request
- render :action => "access_request"
+ render action: "access_request"
end
def render_action_name
- render :action => "access_action_name"
+ render action: "access_action_name"
end
def overridden_with_own_view_paths_appended
@@ -36,15 +38,15 @@ module Render
private
- def secretz
- render :text => "FAIL WHALE!"
- end
+ def secretz
+ render plain: "FAIL WHALE!"
+ end
end
class DoubleRenderController < ActionController::Base
def index
- render :text => "hello"
- render :text => "world"
+ render plain: "hello"
+ render plain: "world"
end
end
@@ -57,7 +59,9 @@ module Render
test "render with blank" do
with_routing do |set|
set.draw do
- get ":controller", :action => 'index'
+ ActiveSupport::Deprecation.silence do
+ get ":controller", action: "index"
+ end
end
get "/render/blank_render"
@@ -70,7 +74,9 @@ module Render
test "rendering more than once raises an exception" do
with_routing do |set|
set.draw do
- get ":controller", :action => 'index'
+ ActiveSupport::Deprecation.silence do
+ get ":controller", action: "index"
+ end
end
assert_raises(AbstractController::DoubleRenderError) do
diff --git a/actionpack/test/controller/new_base/render_text_test.rb b/actionpack/test/controller/new_base/render_text_test.rb
deleted file mode 100644
index 10bad57cd6..0000000000
--- a/actionpack/test/controller/new_base/render_text_test.rb
+++ /dev/null
@@ -1,158 +0,0 @@
-require 'abstract_unit'
-
-module RenderText
- class MinimalController < ActionController::Metal
- include AbstractController::Rendering
- include ActionController::Rendering
-
- def index
- render text: "Hello World!"
- end
- end
-
- class SimpleController < ActionController::Base
- self.view_paths = [ActionView::FixtureResolver.new]
-
- def index
- render text: "hello david"
- end
- end
-
- class WithLayoutController < ::ApplicationController
- self.view_paths = [ActionView::FixtureResolver.new(
- "layouts/application.html.erb" => "<%= yield %>, I'm here!",
- "layouts/greetings.html.erb" => "<%= yield %>, I wish thee well.",
- "layouts/ivar.html.erb" => "<%= yield %>, <%= @ivar %>"
- )]
-
- def index
- render text: "hello david"
- end
-
- def custom_code
- render text: "hello world", status: 404
- end
-
- def with_custom_code_as_string
- render text: "hello world", status: "404 Not Found"
- end
-
- def with_nil
- render text: nil
- end
-
- def with_nil_and_status
- render text: nil, status: 403
- end
-
- def with_false
- render text: false
- end
-
- def with_layout_true
- render text: "hello world", layout: true
- end
-
- def with_layout_false
- render text: "hello world", layout: false
- end
-
- def with_layout_nil
- render text: "hello world", layout: nil
- end
-
- def with_custom_layout
- render text: "hello world", layout: "greetings"
- end
-
- def with_ivar_in_layout
- @ivar = "hello world"
- render text: "hello world", layout: "ivar"
- end
- end
-
- class RenderTextTest < Rack::TestCase
- test "rendering text from a minimal controller" do
- get "/render_text/minimal/index"
- assert_body "Hello World!"
- assert_status 200
- end
-
- test "rendering text from an action with default options renders the text with the layout" do
- with_routing do |set|
- set.draw { get ':controller', action: 'index' }
-
- get "/render_text/simple"
- assert_body "hello david"
- assert_status 200
- end
- end
-
- test "rendering text from an action with default options renders the text without the layout" do
- with_routing do |set|
- set.draw { get ':controller', action: 'index' }
-
- get "/render_text/with_layout"
-
- assert_body "hello david"
- assert_status 200
- end
- end
-
- test "rendering text, while also providing a custom status code" do
- get "/render_text/with_layout/custom_code"
-
- assert_body "hello world"
- assert_status 404
- end
-
- test "rendering text with nil returns an empty body" do
- get "/render_text/with_layout/with_nil"
-
- assert_body ""
- assert_status 200
- end
-
- test "Rendering text with nil and custom status code returns an empty body and the status" do
- get "/render_text/with_layout/with_nil_and_status"
-
- assert_body ""
- assert_status 403
- end
-
- test "rendering text with false returns the string 'false'" do
- get "/render_text/with_layout/with_false"
-
- assert_body "false"
- assert_status 200
- end
-
- test "rendering text with layout: true" do
- get "/render_text/with_layout/with_layout_true"
-
- assert_body "hello world, I'm here!"
- assert_status 200
- end
-
- test "rendering text with layout: 'greetings'" do
- get "/render_text/with_layout/with_custom_layout"
-
- assert_body "hello world, I wish thee well."
- assert_status 200
- end
-
- test "rendering text with layout: false" do
- get "/render_text/with_layout/with_layout_false"
-
- assert_body "hello world"
- assert_status 200
- end
-
- test "rendering text with layout: nil" do
- get "/render_text/with_layout/with_layout_nil"
-
- assert_body "hello world"
- assert_status 200
- end
- end
-end
diff --git a/actionpack/test/controller/new_base/render_xml_test.rb b/actionpack/test/controller/new_base/render_xml_test.rb
index b8527a943d..0dc16d64e2 100644
--- a/actionpack/test/controller/new_base/render_xml_test.rb
+++ b/actionpack/test/controller/new_base/render_xml_test.rb
@@ -1,7 +1,8 @@
-require 'abstract_unit'
+# frozen_string_literal: true
-module RenderXml
+require "abstract_unit"
+module RenderXml
# This has no layout and it works
class BasicController < ActionController::Base
self.view_paths = [ActionView::FixtureResolver.new(
diff --git a/actionpack/test/controller/output_escaping_test.rb b/actionpack/test/controller/output_escaping_test.rb
index c3c549fbfc..d683bc73e6 100644
--- a/actionpack/test/controller/output_escaping_test.rb
+++ b/actionpack/test/controller/output_escaping_test.rb
@@ -1,9 +1,10 @@
-require 'abstract_unit'
+# frozen_string_literal: true
-class OutputEscapingTest < ActiveSupport::TestCase
+require "abstract_unit"
+class OutputEscapingTest < ActiveSupport::TestCase
test "escape_html shouldn't die when passed nil" do
- assert ERB::Util.h(nil).blank?
+ assert_predicate ERB::Util.h(nil), :blank?
end
test "escapeHTML should escape strings" do
@@ -13,5 +14,4 @@ class OutputEscapingTest < ActiveSupport::TestCase
test "escapeHTML shouldn't touch explicitly safe strings" do
assert_equal "<", ERB::Util.h("<".html_safe)
end
-
end
diff --git a/actionpack/test/controller/parameter_encoding_test.rb b/actionpack/test/controller/parameter_encoding_test.rb
new file mode 100644
index 0000000000..e2194e8974
--- /dev/null
+++ b/actionpack/test/controller/parameter_encoding_test.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class ParameterEncodingController < ActionController::Base
+ skip_parameter_encoding :test_bar
+ skip_parameter_encoding :test_all_values_encoding
+
+ def test_foo
+ render body: params[:foo].encoding
+ end
+
+ def test_bar
+ render body: params[:bar].encoding
+ end
+
+ def test_all_values_encoding
+ render body: ::JSON.dump(params.values.map(&:encoding).map(&:name))
+ end
+end
+
+class ParameterEncodingTest < ActionController::TestCase
+ tests ParameterEncodingController
+
+ test "properly transcodes UTF8 parameters into declared encodings" do
+ post :test_foo, params: { "foo" => "foo", "bar" => "bar", "baz" => "baz" }
+
+ assert_response :success
+ assert_equal "UTF-8", @response.body
+ end
+
+ 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 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 ["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
+ get :test_bar, params: { "bar" => URI.parser.escape("bar\xE2baz".b) }
+
+ assert_response :success
+ assert_equal "ASCII-8BIT", @response.body
+ end
+end
diff --git a/actionpack/test/controller/parameters/accessors_test.rb b/actionpack/test/controller/parameters/accessors_test.rb
index 97875c3cbb..7789e654d5 100644
--- a/actionpack/test/controller/parameters/accessors_test.rb
+++ b/actionpack/test/controller/parameters/accessors_test.rb
@@ -1,30 +1,44 @@
-require 'abstract_unit'
-require 'action_controller/metal/strong_parameters'
-require 'active_support/core_ext/hash/transform_values'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "action_controller/metal/strong_parameters"
class ParametersAccessorsTest < ActiveSupport::TestCase
setup do
+ ActionController::Parameters.permit_all_parameters = false
+
@params = ActionController::Parameters.new(
person: {
- age: '32',
+ age: "32",
name: {
- first: 'David',
- last: 'Heinemeier Hansson'
+ first: "David",
+ last: "Heinemeier Hansson"
},
- addresses: [{city: 'Chicago', state: 'Illinois'}]
+ addresses: [{ city: "Chicago", state: "Illinois" }]
}
)
end
test "[] retains permitted status" do
@params.permit!
- assert @params[:person].permitted?
- assert @params[:person][:name].permitted?
+ assert_predicate @params[:person], :permitted?
+ assert_predicate @params[:person][:name], :permitted?
end
test "[] retains unpermitted status" do
- assert_not @params[:person].permitted?
- assert_not @params[:person][:name].permitted?
+ assert_not_predicate @params[:person], :permitted?
+ assert_not_predicate @params[:person][:name], :permitted?
+ end
+
+ test "as_json returns the JSON representation of the parameters hash" do
+ assert_not @params.as_json.key? "parameters"
+ assert_not @params.as_json.key? "permitted"
+ assert @params.as_json.key? "person"
+ end
+
+ test "to_s returns the string representation of the parameters hash" do
+ assert_equal '{"person"=>{"age"=>"32", "name"=>{"first"=>"David", "last"=>"Heinemeier Hansson"}, ' \
+ '"addresses"=>[{"city"=>"Chicago", "state"=>"Illinois"}]}}', @params.to_s
end
test "each carries permitted status" do
@@ -36,6 +50,14 @@ class ParametersAccessorsTest < ActiveSupport::TestCase
@params.each { |key, value| assert_not(value.permitted?) if key == "person" }
end
+ test "each returns key,value array for block with arity 1" do
+ @params.each do |arg|
+ assert_kind_of Array, arg
+ assert_equal "person", arg[0]
+ assert_kind_of ActionController::Parameters, arg[1]
+ end
+ end
+
test "each_pair carries permitted status" do
@params.permit!
@params.each_pair { |key, value| assert(value.permitted?) if key == "person" }
@@ -45,81 +67,272 @@ class ParametersAccessorsTest < ActiveSupport::TestCase
@params.each_pair { |key, value| assert_not(value.permitted?) if key == "person" }
end
+ test "each_pair returns key,value array for block with arity 1" do
+ @params.each_pair do |arg|
+ assert_kind_of Array, arg
+ assert_equal "person", arg[0]
+ assert_kind_of ActionController::Parameters, arg[1]
+ end
+ end
+
+ test "each_value carries permitted status" do
+ @params.permit!
+ @params.each_value do |value|
+ assert_predicate(value, :permitted?)
+ end
+ end
+
+ test "each_value carries unpermitted status" do
+ @params.each_value do |value|
+ assert_not_predicate(value, :permitted?)
+ end
+ end
+
+ test "each_key converts to hash for permitted" do
+ @params.permit!
+ @params.each_key { |key| assert_kind_of(String, key) if key == "person" }
+ end
+
+ test "each_key converts to hash for unpermitted" do
+ @params.each_key { |key| assert_kind_of(String, key) if key == "person" }
+ end
+
+ test "empty? returns true when params contains no key/value pairs" do
+ params = ActionController::Parameters.new
+ assert_empty params
+ end
+
+ test "empty? returns false when any params are present" do
+ assert_not_empty @params
+ end
+
test "except retains permitted status" do
@params.permit!
- assert @params.except(:person).permitted?
- assert @params[:person].except(:name).permitted?
+ assert_predicate @params.except(:person), :permitted?
+ assert_predicate @params[:person].except(:name), :permitted?
end
test "except retains unpermitted status" do
- assert_not @params.except(:person).permitted?
- assert_not @params[:person].except(:name).permitted?
+ assert_not_predicate @params.except(:person), :permitted?
+ assert_not_predicate @params[:person].except(:name), :permitted?
end
test "fetch retains permitted status" do
@params.permit!
- assert @params.fetch(:person).permitted?
- assert @params[:person].fetch(:name).permitted?
+ assert_predicate @params.fetch(:person), :permitted?
+ assert_predicate @params[:person].fetch(:name), :permitted?
end
test "fetch retains unpermitted status" do
- assert_not @params.fetch(:person).permitted?
- assert_not @params[:person].fetch(:name).permitted?
+ assert_not_predicate @params.fetch(:person), :permitted?
+ assert_not_predicate @params[:person].fetch(:name), :permitted?
+ end
+
+ test "has_key? returns true if the given key is present in the params" do
+ assert @params.has_key?(:person)
+ end
+
+ test "has_key? returns false if the given key is not present in the params" do
+ assert_not @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")
+ assert_not 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
+ assert_not @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
+ assert_not @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?
+ assert_not_predicate @params.reject { |k| k == "person" }, :permitted?
end
test "reject retains unpermitted status" do
@params.permit!
- assert @params.reject { |k| k == "person" }.permitted?
+ assert_predicate @params.reject { |k| k == "person" }, :permitted?
end
test "select retains permitted status" do
@params.permit!
- assert @params.select { |k| k == "person" }.permitted?
+ assert_predicate @params.select { |k| k == "person" }, :permitted?
end
test "select retains unpermitted status" do
- assert_not @params.select { |k| k == "person" }.permitted?
+ assert_not_predicate @params.select { |k| k == "person" }, :permitted?
end
test "slice retains permitted status" do
@params.permit!
- assert @params.slice(:person).permitted?
+ assert_predicate @params.slice(:person), :permitted?
end
test "slice retains unpermitted status" do
- assert_not @params.slice(:person).permitted?
+ assert_not_predicate @params.slice(:person), :permitted?
end
test "transform_keys retains permitted status" do
@params.permit!
- assert @params.transform_keys { |k| k }.permitted?
+ assert_predicate @params.transform_keys { |k| k }, :permitted?
end
test "transform_keys retains unpermitted status" do
- assert_not @params.transform_keys { |k| k }.permitted?
+ assert_not_predicate @params.transform_keys { |k| k }, :permitted?
end
test "transform_values retains permitted status" do
@params.permit!
- assert @params.transform_values { |v| v }.permitted?
+ assert_predicate @params.transform_values { |v| v }, :permitted?
end
test "transform_values retains unpermitted status" do
- assert_not @params.transform_values { |v| v }.permitted?
+ assert_not_predicate @params.transform_values { |v| v }, :permitted?
+ end
+
+ test "transform_values converts hashes to parameters" do
+ @params.transform_values do |value|
+ assert_kind_of ActionController::Parameters, value
+ value
+ end
+ end
+
+ test "transform_values without block yieds an enumerator" do
+ assert_kind_of Enumerator, @params.transform_values
+ end
+
+ test "transform_values! converts hashes to parameters" do
+ @params.transform_values! do |value|
+ assert_kind_of ActionController::Parameters, value
+ end
+ end
+
+ test "transform_values! without block yields an enumerator" do
+ assert_kind_of Enumerator, @params.transform_values!
+ 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")
+ assert_not 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?
- assert @params[:person].values_at(:name).first.permitted?
+ assert_predicate @params.values_at(:person).first, :permitted?
+ assert_predicate @params[:person].values_at(:name).first, :permitted?
end
test "values_at retains unpermitted status" do
- assert_not @params.values_at(:person).first.permitted?
- assert_not @params[:person].values_at(:name).first.permitted?
+ assert_not_predicate @params.values_at(:person).first, :permitted?
+ assert_not_predicate @params[:person].values_at(:name).first, :permitted?
+ end
+
+ test "is equal to Parameters instance with same params" do
+ params1 = ActionController::Parameters.new(a: 1, b: 2)
+ params2 = ActionController::Parameters.new(a: 1, b: 2)
+ assert(params1 == params2)
+ end
+
+ test "is equal to Parameters instance with same permitted params" do
+ params1 = ActionController::Parameters.new(a: 1, b: 2).permit(:a)
+ params2 = ActionController::Parameters.new(a: 1, b: 2).permit(:a)
+ assert(params1 == params2)
+ end
+
+ test "is equal to Parameters instance with same different source params, but same permitted params" do
+ params1 = ActionController::Parameters.new(a: 1, b: 2).permit(:a)
+ params2 = ActionController::Parameters.new(a: 1, c: 3).permit(:a)
+ assert(params1 == params2)
+ assert(params2 == params1)
+ end
+
+ test "is not equal to an unpermitted Parameters instance with same params" do
+ params1 = ActionController::Parameters.new(a: 1).permit(:a)
+ params2 = ActionController::Parameters.new(a: 1)
+ assert(params1 != params2)
+ assert(params2 != params1)
+ end
+
+ test "is not equal to Parameters instance with different permitted params" do
+ params1 = ActionController::Parameters.new(a: 1, b: 2).permit(:a, :b)
+ params2 = ActionController::Parameters.new(a: 1, b: 2).permit(:a)
+ assert(params1 != params2)
+ assert(params2 != params1)
+ end
+
+ test "equality with simple types works" do
+ assert(@params != "Hello")
+ assert(@params != 42)
+ assert(@params != false)
+ end
+
+ test "inspect shows both class name, parameters and permitted flag" do
+ assert_equal(
+ '<ActionController::Parameters {"person"=>{"age"=>"32", '\
+ '"name"=>{"first"=>"David", "last"=>"Heinemeier Hansson"}, ' \
+ '"addresses"=>[{"city"=>"Chicago", "state"=>"Illinois"}]}} permitted: false>',
+ @params.inspect
+ )
+ end
+
+ test "inspect prints updated permitted flag in the output" do
+ assert_match(/permitted: false/, @params.inspect)
+
+ @params.permit!
+
+ assert_match(/permitted: true/, @params.inspect)
+ end
+
+ test "#dig delegates the dig method to its values" do
+ assert_equal "David", @params.dig(:person, :name, :first)
+ assert_equal "Chicago", @params.dig(:person, :addresses, 0, :city)
+ end
+
+ test "#dig converts hashes to parameters" do
+ assert_kind_of ActionController::Parameters, @params.dig(:person)
+ assert_kind_of ActionController::Parameters, @params.dig(:person, :addresses, 0)
+ assert @params.dig(:person, :addresses).all? do |value|
+ value.is_a?(ActionController::Parameters)
+ end
+ end
+
+ test "mutating #dig return value mutates underlying parameters" do
+ @params.dig(:person, :name)[:first] = "Bill"
+ assert_equal "Bill", @params.dig(:person, :name, :first)
+
+ @params.dig(:person, :addresses)[0] = { city: "Boston", state: "Massachusetts" }
+ assert_equal "Boston", @params.dig(:person, :addresses, 0, :city)
end
end
diff --git a/actionpack/test/controller/parameters/always_permitted_parameters_test.rb b/actionpack/test/controller/parameters/always_permitted_parameters_test.rb
index 59be08db54..974612fb7b 100644
--- a/actionpack/test/controller/parameters/always_permitted_parameters_test.rb
+++ b/actionpack/test/controller/parameters/always_permitted_parameters_test.rb
@@ -1,6 +1,7 @@
-require 'abstract_unit'
-require 'action_controller/metal/strong_parameters'
-require 'minitest/mock'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "action_controller/metal/strong_parameters"
class AlwaysPermittedParametersTest < ActiveSupport::TestCase
def setup
@@ -13,24 +14,17 @@ class AlwaysPermittedParametersTest < ActiveSupport::TestCase
ActionController::Parameters.always_permitted_parameters = %w( controller action )
end
- test "shows deprecations warning on NEVER_UNPERMITTED_PARAMS" do
- assert_deprecated do
- ActionController::Parameters::NEVER_UNPERMITTED_PARAMS
- end
- end
-
test "returns super on missing constant other than NEVER_UNPERMITTED_PARAMS" do
ActionController::Parameters.superclass.stub :const_missing, "super" do
assert_equal "super", ActionController::Parameters::NON_EXISTING_CONSTANT
end
end
- test "permits parameters that are whitelisted" do
- params = ActionController::Parameters.new({
+ test "allows both explicitly listed and always-permitted parameters" do
+ params = ActionController::Parameters.new(
book: { pages: 65 },
- format: "json"
- })
+ format: "json")
permitted = params.permit book: [:pages]
- assert permitted.permitted?
+ assert_predicate permitted, :permitted?
end
end
diff --git a/actionpack/test/controller/parameters/dup_test.rb b/actionpack/test/controller/parameters/dup_test.rb
new file mode 100644
index 0000000000..5403fc6d93
--- /dev/null
+++ b/actionpack/test/controller/parameters/dup_test.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "action_controller/metal/strong_parameters"
+require "active_support/core_ext/object/deep_dup"
+
+class ParametersDupTest < ActiveSupport::TestCase
+ setup do
+ ActionController::Parameters.permit_all_parameters = false
+
+ @params = ActionController::Parameters.new(
+ person: {
+ age: "32",
+ name: {
+ first: "David",
+ last: "Heinemeier Hansson"
+ },
+ addresses: [{ city: "Chicago", state: "Illinois" }]
+ }
+ )
+ end
+
+ test "a duplicate maintains the original's permitted status" do
+ @params.permit!
+ dupped_params = @params.dup
+ assert_predicate dupped_params, :permitted?
+ end
+
+ test "a duplicate maintains the original's parameters" do
+ @params.permit!
+ dupped_params = @params.dup
+ assert_equal @params.to_h, dupped_params.to_h
+ end
+
+ test "changes to a duplicate's parameters do not affect the original" do
+ dupped_params = @params.dup
+ dupped_params.delete(:person)
+ assert_not_equal @params, dupped_params
+ end
+
+ test "changes to a duplicate's permitted status do not affect the original" do
+ dupped_params = @params.dup
+ dupped_params.permit!
+ assert_not_equal @params, dupped_params
+ end
+
+ test "deep_dup content" do
+ dupped_params = @params.deep_dup
+ dupped_params[:person][:age] = "45"
+ dupped_params[:person][:addresses].clear
+
+ assert_not_equal @params[:person][:age], dupped_params[:person][:age]
+ assert_not_equal @params[:person][:addresses], dupped_params[:person][:addresses]
+ end
+
+ test "deep_dup @permitted" do
+ dupped_params = @params.deep_dup
+ dupped_params.permit!
+
+ assert_not_predicate @params, :permitted?
+ end
+
+ test "deep_dup @permitted is being copied" do
+ @params.permit!
+ assert_predicate @params.deep_dup, :permitted?
+ end
+end
diff --git a/actionpack/test/controller/parameters/log_on_unpermitted_params_test.rb b/actionpack/test/controller/parameters/log_on_unpermitted_params_test.rb
index 9ce04b9aeb..fc9229ca1d 100644
--- a/actionpack/test/controller/parameters/log_on_unpermitted_params_test.rb
+++ b/actionpack/test/controller/parameters/log_on_unpermitted_params_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'action_controller/metal/strong_parameters'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "action_controller/metal/strong_parameters"
class LogOnUnpermittedParamsTest < ActiveSupport::TestCase
def setup
@@ -11,62 +13,58 @@ class LogOnUnpermittedParamsTest < ActiveSupport::TestCase
end
test "logs on unexpected param" do
- params = ActionController::Parameters.new({
+ params = ActionController::Parameters.new(
book: { pages: 65 },
- fishing: "Turnips"
- })
+ fishing: "Turnips")
- assert_logged("Unpermitted parameter: fishing") do
+ assert_logged("Unpermitted parameter: :fishing") do
params.permit(book: [:pages])
end
end
test "logs on unexpected params" do
- params = ActionController::Parameters.new({
+ params = ActionController::Parameters.new(
book: { pages: 65 },
fishing: "Turnips",
- car: "Mersedes"
- })
+ car: "Mersedes")
- assert_logged("Unpermitted parameters: fishing, car") do
+ assert_logged("Unpermitted parameters: :fishing, :car") do
params.permit(book: [:pages])
end
end
test "logs on unexpected nested param" do
- params = ActionController::Parameters.new({
- book: { pages: 65, title: "Green Cats and where to find then." }
- })
+ params = ActionController::Parameters.new(
+ book: { pages: 65, title: "Green Cats and where to find then." })
- assert_logged("Unpermitted parameter: title") do
+ assert_logged("Unpermitted parameter: :title") do
params.permit(book: [:pages])
end
end
test "logs on unexpected nested params" do
- params = ActionController::Parameters.new({
- book: { pages: 65, title: "Green Cats and where to find then.", author: "G. A. Dog" }
- })
+ params = ActionController::Parameters.new(
+ book: { pages: 65, title: "Green Cats and where to find then.", author: "G. A. Dog" })
- assert_logged("Unpermitted parameters: title, author") do
+ assert_logged("Unpermitted parameters: :title, :author") do
params.permit(book: [:pages])
end
end
private
- def assert_logged(message)
- old_logger = ActionController::Base.logger
- log = StringIO.new
- ActionController::Base.logger = Logger.new(log)
+ def assert_logged(message)
+ old_logger = ActionController::Base.logger
+ log = StringIO.new
+ ActionController::Base.logger = Logger.new(log)
- begin
- yield
+ begin
+ yield
- log.rewind
- assert_match message, log.read
- ensure
- ActionController::Base.logger = old_logger
+ log.rewind
+ assert_match message, log.read
+ ensure
+ ActionController::Base.logger = old_logger
+ end
end
- end
end
diff --git a/actionpack/test/controller/parameters/multi_parameter_attributes_test.rb b/actionpack/test/controller/parameters/multi_parameter_attributes_test.rb
index 15338059bc..c890839727 100644
--- a/actionpack/test/controller/parameters/multi_parameter_attributes_test.rb
+++ b/actionpack/test/controller/parameters/multi_parameter_attributes_test.rb
@@ -1,9 +1,11 @@
-require 'abstract_unit'
-require 'action_controller/metal/strong_parameters'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "action_controller/metal/strong_parameters"
class MultiParameterAttributesTest < ActiveSupport::TestCase
test "permitted multi-parameter attribute keys" do
- params = ActionController::Parameters.new({
+ params = ActionController::Parameters.new(
book: {
"shipped_at(1i)" => "2012",
"shipped_at(2i)" => "3",
@@ -15,12 +17,11 @@ class MultiParameterAttributesTest < ActiveSupport::TestCase
"published_at(3i)" => "5",
"price(1)" => "R$",
"price(2f)" => "2.02"
- }
- })
+ })
permitted = params.permit book: [ :shipped_at, :price ]
- assert permitted.permitted?
+ assert_predicate permitted, :permitted?
assert_equal "2012", permitted[:book]["shipped_at(1i)"]
assert_equal "3", permitted[:book]["shipped_at(2i)"]
diff --git a/actionpack/test/controller/parameters/mutators_test.rb b/actionpack/test/controller/parameters/mutators_test.rb
index 744d8664be..312b1e5b27 100644
--- a/actionpack/test/controller/parameters/mutators_test.rb
+++ b/actionpack/test/controller/parameters/mutators_test.rb
@@ -1,99 +1,121 @@
-require 'abstract_unit'
-require 'action_controller/metal/strong_parameters'
-require 'active_support/core_ext/hash/transform_values'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "action_controller/metal/strong_parameters"
class ParametersMutatorsTest < ActiveSupport::TestCase
setup do
@params = ActionController::Parameters.new(
person: {
- age: '32',
+ age: "32",
name: {
- first: 'David',
- last: 'Heinemeier Hansson'
+ first: "David",
+ last: "Heinemeier Hansson"
},
- addresses: [{city: 'Chicago', state: 'Illinois'}]
+ addresses: [{ city: "Chicago", state: "Illinois" }]
}
)
end
test "delete retains permitted status" do
@params.permit!
- assert @params.delete(:person).permitted?
+ assert_predicate @params.delete(:person), :permitted?
end
test "delete retains unpermitted status" do
- assert_not @params.delete(:person).permitted?
+ assert_not_predicate @params.delete(:person), :permitted?
+ end
+
+ test "delete returns the value when the key is present" do
+ assert_equal "32", @params[:person].delete(:age)
+ end
+
+ test "delete removes the entry when the key present" do
+ @params[:person].delete(:age)
+ assert_not @params[:person].key?(:age)
+ end
+
+ test "delete returns nil when the key is not present" do
+ assert_nil @params[:person].delete(:first_name)
+ end
+
+ test "delete returns the value of the given block when the key is not present" do
+ assert_equal "David", @params[:person].delete(:first_name) { "David" }
+ end
+
+ test "delete yields the key to the given block when the key is not present" do
+ assert_equal "first_name: David", @params[:person].delete(:first_name) { |k| "#{k}: David" }
end
test "delete_if retains permitted status" do
@params.permit!
- assert @params.delete_if { |k| k == "person" }.permitted?
+ assert_predicate @params.delete_if { |k| k == "person" }, :permitted?
end
test "delete_if retains unpermitted status" do
- assert_not @params.delete_if { |k| k == "person" }.permitted?
+ assert_not_predicate @params.delete_if { |k| k == "person" }, :permitted?
end
test "extract! retains permitted status" do
@params.permit!
- assert @params.extract!(:person).permitted?
+ assert_predicate @params.extract!(:person), :permitted?
end
test "extract! retains unpermitted status" do
- assert_not @params.extract!(:person).permitted?
+ assert_not_predicate @params.extract!(:person), :permitted?
end
test "keep_if retains permitted status" do
@params.permit!
- assert @params.keep_if { |k,v| k == "person" }.permitted?
+ assert_predicate @params.keep_if { |k, v| k == "person" }, :permitted?
end
test "keep_if retains unpermitted status" do
- assert_not @params.keep_if { |k,v| k == "person" }.permitted?
+ assert_not_predicate @params.keep_if { |k, v| k == "person" }, :permitted?
end
test "reject! retains permitted status" do
@params.permit!
- assert @params.reject! { |k| k == "person" }.permitted?
+ assert_predicate @params.reject! { |k| k == "person" }, :permitted?
end
test "reject! retains unpermitted status" do
- assert_not @params.reject! { |k| k == "person" }.permitted?
+ assert_not_predicate @params.reject! { |k| k == "person" }, :permitted?
end
test "select! retains permitted status" do
@params.permit!
- assert @params.select! { |k| k != "person" }.permitted?
+ assert_predicate @params.select! { |k| k != "person" }, :permitted?
end
test "select! retains unpermitted status" do
- assert_not @params.select! { |k| k != "person" }.permitted?
+ assert_not_predicate @params.select! { |k| k != "person" }, :permitted?
end
test "slice! retains permitted status" do
@params.permit!
- assert @params.slice!(:person).permitted?
+ assert_predicate @params.slice!(:person), :permitted?
end
test "slice! retains unpermitted status" do
- assert_not @params.slice!(:person).permitted?
+ assert_not_predicate @params.slice!(:person), :permitted?
end
test "transform_keys! retains permitted status" do
@params.permit!
- assert @params.transform_keys! { |k| k }.permitted?
+ assert_predicate @params.transform_keys! { |k| k }, :permitted?
end
test "transform_keys! retains unpermitted status" do
- assert_not @params.transform_keys! { |k| k }.permitted?
+ assert_not_predicate @params.transform_keys! { |k| k }, :permitted?
end
test "transform_values! retains permitted status" do
@params.permit!
- assert @params.transform_values! { |v| v }.permitted?
+ assert_predicate @params.transform_values! { |v| v }, :permitted?
end
test "transform_values! retains unpermitted status" do
- assert_not @params.transform_values! { |v| v }.permitted?
+ assert_not_predicate @params.transform_values! { |v| v }, :permitted?
end
end
diff --git a/actionpack/test/controller/parameters/nested_parameters_permit_test.rb b/actionpack/test/controller/parameters/nested_parameters_permit_test.rb
new file mode 100644
index 0000000000..1403e224c0
--- /dev/null
+++ b/actionpack/test/controller/parameters/nested_parameters_permit_test.rb
@@ -0,0 +1,184 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "action_controller/metal/strong_parameters"
+
+class NestedParametersPermitTest < ActiveSupport::TestCase
+ def assert_filtered_out(params, key)
+ assert_not params.has_key?(key), "key #{key.inspect} has not been filtered out"
+ end
+
+ test "permitted nested parameters" do
+ params = ActionController::Parameters.new(
+ book: {
+ title: "Romeo and Juliet",
+ authors: [{
+ name: "William Shakespeare",
+ born: "1564-04-26"
+ }, {
+ name: "Christopher Marlowe"
+ }, {
+ name: %w(malicious injected names)
+ }],
+ details: {
+ pages: 200,
+ genre: "Tragedy"
+ },
+ id: {
+ isbn: "x"
+ }
+ },
+ magazine: "Mjallo!")
+
+ permitted = params.permit book: [ :title, { authors: [ :name ] }, { details: :pages }, :id ]
+
+ assert_predicate permitted, :permitted?
+ assert_equal "Romeo and Juliet", permitted[:book][:title]
+ assert_equal "William Shakespeare", permitted[:book][:authors][0][:name]
+ assert_equal "Christopher Marlowe", permitted[:book][:authors][1][:name]
+ assert_equal 200, permitted[:book][:details][:pages]
+
+ assert_filtered_out permitted, :magazine
+ assert_filtered_out permitted[:book], :id
+ assert_filtered_out permitted[:book][:details], :genre
+ assert_filtered_out permitted[:book][:authors][0], :born
+ assert_filtered_out permitted[:book][:authors][2], :name
+ end
+
+ test "permitted nested parameters with a string or a symbol as a key" do
+ params = ActionController::Parameters.new(
+ book: {
+ "authors" => [
+ { name: "William Shakespeare", born: "1564-04-26" },
+ { name: "Christopher Marlowe" }
+ ]
+ })
+
+ permitted = params.permit book: [ { "authors" => [ :name ] } ]
+
+ assert_equal "William Shakespeare", permitted[:book]["authors"][0][:name]
+ assert_equal "William Shakespeare", permitted[:book][:authors][0][:name]
+ assert_equal "Christopher Marlowe", permitted[:book]["authors"][1][:name]
+ assert_equal "Christopher Marlowe", permitted[:book][:authors][1][:name]
+
+ permitted = params.permit book: [ { authors: [ :name ] } ]
+
+ assert_equal "William Shakespeare", permitted[:book]["authors"][0][:name]
+ assert_equal "William Shakespeare", permitted[:book][:authors][0][:name]
+ assert_equal "Christopher Marlowe", permitted[:book]["authors"][1][:name]
+ assert_equal "Christopher Marlowe", permitted[:book][:authors][1][:name]
+ end
+
+ test "nested arrays with strings" do
+ params = ActionController::Parameters.new(
+ book: {
+ genres: ["Tragedy"]
+ })
+
+ permitted = params.permit book: { genres: [] }
+ assert_equal ["Tragedy"], permitted[:book][:genres]
+ end
+
+ test "permit may specify symbols or strings" do
+ params = ActionController::Parameters.new(
+ book: {
+ title: "Romeo and Juliet",
+ author: "William Shakespeare"
+ },
+ magazine: "Shakespeare Today")
+
+ permitted = params.permit({ book: ["title", :author] }, "magazine")
+ assert_equal "Romeo and Juliet", permitted[:book][:title]
+ assert_equal "William Shakespeare", permitted[:book][:author]
+ assert_equal "Shakespeare Today", permitted[:magazine]
+ end
+
+ test "nested array with strings that should be hashes" do
+ params = ActionController::Parameters.new(
+ book: {
+ genres: ["Tragedy"]
+ })
+
+ permitted = params.permit book: { genres: :type }
+ assert_empty permitted[:book][:genres]
+ end
+
+ test "nested array with strings that should be hashes and additional values" do
+ params = ActionController::Parameters.new(
+ book: {
+ title: "Romeo and Juliet",
+ genres: ["Tragedy"]
+ })
+
+ permitted = params.permit book: [ :title, { genres: :type } ]
+ assert_equal "Romeo and Juliet", permitted[:book][:title]
+ assert_empty permitted[:book][:genres]
+ end
+
+ test "nested string that should be a hash" do
+ params = ActionController::Parameters.new(
+ book: {
+ genre: "Tragedy"
+ })
+
+ permitted = params.permit book: { genre: :type }
+ assert_nil permitted[:book][:genre]
+ end
+
+ test "fields_for-style nested params" do
+ params = ActionController::Parameters.new(
+ book: {
+ authors_attributes: {
+ '0': { name: "William Shakespeare", age_of_death: "52" },
+ '1': { name: "Unattributed Assistant" },
+ '2': { name: %w(injected names) }
+ }
+ })
+ permitted = params.permit book: { authors_attributes: [ :name ] }
+
+ assert_not_nil permitted[:book][:authors_attributes]["0"]
+ assert_not_nil permitted[:book][:authors_attributes]["1"]
+ assert_empty permitted[:book][:authors_attributes]["2"]
+ 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
+
+ test "fields_for-style nested params with negative numbers" do
+ params = ActionController::Parameters.new(
+ book: {
+ authors_attributes: {
+ '-1': { name: "William Shakespeare", age_of_death: "52" },
+ '-2': { name: "Unattributed Assistant" }
+ }
+ })
+ permitted = params.permit book: { authors_attributes: [:name] }
+
+ assert_not_nil permitted[:book][:authors_attributes]["-1"]
+ assert_not_nil permitted[:book][:authors_attributes]["-2"]
+ assert_equal "William Shakespeare", permitted[:book][:authors_attributes]["-1"][:name]
+ assert_equal "Unattributed Assistant", permitted[:book][:authors_attributes]["-2"][:name]
+
+ assert_filtered_out permitted[:book][:authors_attributes]["-1"], :age_of_death
+ end
+
+ test "nested number as key" do
+ params = ActionController::Parameters.new(
+ product: {
+ properties: {
+ "0" => "prop0",
+ "1" => "prop1"
+ }
+ })
+ params = params.require(:product).permit(properties: ["0"])
+ assert_not_nil params[:properties]["0"]
+ assert_nil params[:properties]["1"]
+ assert_equal "prop0", params[:properties]["0"]
+ end
+end
diff --git a/actionpack/test/controller/parameters/nested_parameters_test.rb b/actionpack/test/controller/parameters/nested_parameters_test.rb
deleted file mode 100644
index 3b1257e8d5..0000000000
--- a/actionpack/test/controller/parameters/nested_parameters_test.rb
+++ /dev/null
@@ -1,187 +0,0 @@
-require 'abstract_unit'
-require 'action_controller/metal/strong_parameters'
-
-class NestedParametersTest < ActiveSupport::TestCase
- def assert_filtered_out(params, key)
- assert !params.has_key?(key), "key #{key.inspect} has not been filtered out"
- end
-
- test "permitted nested parameters" do
- params = ActionController::Parameters.new({
- book: {
- title: "Romeo and Juliet",
- authors: [{
- name: "William Shakespeare",
- born: "1564-04-26"
- }, {
- name: "Christopher Marlowe"
- }, {
- name: %w(malicious injected names)
- }],
- details: {
- pages: 200,
- genre: "Tragedy"
- },
- id: {
- isbn: 'x'
- }
- },
- magazine: "Mjallo!"
- })
-
- permitted = params.permit book: [ :title, { authors: [ :name ] }, { details: :pages }, :id ]
-
- assert permitted.permitted?
- assert_equal "Romeo and Juliet", permitted[:book][:title]
- assert_equal "William Shakespeare", permitted[:book][:authors][0][:name]
- assert_equal "Christopher Marlowe", permitted[:book][:authors][1][:name]
- assert_equal 200, permitted[:book][:details][:pages]
-
- assert_filtered_out permitted, :magazine
- assert_filtered_out permitted[:book], :id
- assert_filtered_out permitted[:book][:details], :genre
- assert_filtered_out permitted[:book][:authors][0], :born
- assert_filtered_out permitted[:book][:authors][2], :name
- end
-
- test "permitted nested parameters with a string or a symbol as a key" do
- params = ActionController::Parameters.new({
- book: {
- 'authors' => [
- { name: 'William Shakespeare', born: '1564-04-26' },
- { name: 'Christopher Marlowe' }
- ]
- }
- })
-
- permitted = params.permit book: [ { 'authors' => [ :name ] } ]
-
- assert_equal 'William Shakespeare', permitted[:book]['authors'][0][:name]
- assert_equal 'William Shakespeare', permitted[:book][:authors][0][:name]
- assert_equal 'Christopher Marlowe', permitted[:book]['authors'][1][:name]
- assert_equal 'Christopher Marlowe', permitted[:book][:authors][1][:name]
-
- permitted = params.permit book: [ { authors: [ :name ] } ]
-
- assert_equal 'William Shakespeare', permitted[:book]['authors'][0][:name]
- assert_equal 'William Shakespeare', permitted[:book][:authors][0][:name]
- assert_equal 'Christopher Marlowe', permitted[:book]['authors'][1][:name]
- assert_equal 'Christopher Marlowe', permitted[:book][:authors][1][:name]
- end
-
- test "nested arrays with strings" do
- params = ActionController::Parameters.new({
- book: {
- genres: ["Tragedy"]
- }
- })
-
- permitted = params.permit book: {genres: []}
- assert_equal ["Tragedy"], permitted[:book][:genres]
- end
-
- test "permit may specify symbols or strings" do
- params = ActionController::Parameters.new({
- book: {
- title: "Romeo and Juliet",
- author: "William Shakespeare"
- },
- magazine: "Shakespeare Today"
- })
-
- permitted = params.permit({book: ["title", :author]}, "magazine")
- assert_equal "Romeo and Juliet", permitted[:book][:title]
- assert_equal "William Shakespeare", permitted[:book][:author]
- assert_equal "Shakespeare Today", permitted[:magazine]
- end
-
- test "nested array with strings that should be hashes" do
- params = ActionController::Parameters.new({
- book: {
- genres: ["Tragedy"]
- }
- })
-
- permitted = params.permit book: { genres: :type }
- assert_empty permitted[:book][:genres]
- end
-
- test "nested array with strings that should be hashes and additional values" do
- params = ActionController::Parameters.new({
- book: {
- title: "Romeo and Juliet",
- genres: ["Tragedy"]
- }
- })
-
- permitted = params.permit book: [ :title, { genres: :type } ]
- assert_equal "Romeo and Juliet", permitted[:book][:title]
- assert_empty permitted[:book][:genres]
- end
-
- test "nested string that should be a hash" do
- params = ActionController::Parameters.new({
- book: {
- genre: "Tragedy"
- }
- })
-
- permitted = params.permit book: { genre: :type }
- assert_nil permitted[:book][:genre]
- end
-
- test "fields_for-style nested params" do
- params = ActionController::Parameters.new({
- book: {
- authors_attributes: {
- :'0' => { name: 'William Shakespeare', age_of_death: '52' },
- :'1' => { name: 'Unattributed Assistant' },
- :'2' => { name: %w(injected names)}
- }
- }
- })
- permitted = params.permit book: { authors_attributes: [ :name ] }
-
- assert_not_nil permitted[:book][:authors_attributes]['0']
- assert_not_nil permitted[:book][:authors_attributes]['1']
- assert_empty permitted[:book][:authors_attributes]['2']
- assert_equal 'William Shakespeare', permitted[:book][:authors_attributes]['0'][:name]
- assert_equal 'Unattributed Assistant', permitted[:book][:authors_attributes]['1'][:name]
-
- assert_filtered_out permitted[:book][:authors_attributes]['0'], :age_of_death
- end
-
- test "fields_for-style nested params with negative numbers" do
- params = ActionController::Parameters.new({
- book: {
- authors_attributes: {
- :'-1' => { name: 'William Shakespeare', age_of_death: '52' },
- :'-2' => { name: 'Unattributed Assistant' }
- }
- }
- })
- permitted = params.permit book: { authors_attributes: [:name] }
-
- assert_not_nil permitted[:book][:authors_attributes]['-1']
- assert_not_nil permitted[:book][:authors_attributes]['-2']
- assert_equal 'William Shakespeare', permitted[:book][:authors_attributes]['-1'][:name]
- assert_equal 'Unattributed Assistant', permitted[:book][:authors_attributes]['-2'][:name]
-
- assert_filtered_out permitted[:book][:authors_attributes]['-1'], :age_of_death
- end
-
- test "nested number as key" do
- params = ActionController::Parameters.new({
- product: {
- properties: {
- '0' => "prop0",
- '1' => "prop1"
- }
- }
- })
- params = params.require(:product).permit(:properties => ["0"])
- assert_not_nil params[:properties]["0"]
- assert_nil params[:properties]["1"]
- assert_equal "prop0", params[:properties]["0"]
- end
-end
diff --git a/actionpack/test/controller/parameters/parameters_permit_test.rb b/actionpack/test/controller/parameters/parameters_permit_test.rb
index 2ed486516d..d2fa0aa16e 100644
--- a/actionpack/test/controller/parameters/parameters_permit_test.rb
+++ b/actionpack/test/controller/parameters/parameters_permit_test.rb
@@ -1,42 +1,65 @@
-require 'abstract_unit'
-require 'action_dispatch/http/upload'
-require 'action_controller/metal/strong_parameters'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "action_dispatch/http/upload"
+require "action_controller/metal/strong_parameters"
class ParametersPermitTest < ActiveSupport::TestCase
def assert_filtered_out(params, key)
- assert !params.has_key?(key), "key #{key.inspect} has not been filtered out"
+ assert_not params.has_key?(key), "key #{key.inspect} has not been filtered out"
end
setup do
@params = ActionController::Parameters.new(
person: {
- age: '32',
+ age: "32",
name: {
- first: 'David',
- last: 'Heinemeier Hansson'
+ first: "David",
+ last: "Heinemeier Hansson"
},
- addresses: [{city: 'Chicago', state: 'Illinois'}]
+ addresses: [{ city: "Chicago", state: "Illinois" }]
}
)
@struct_fields = []
%w(0 1 12).each do |number|
- ['', 'i', 'f'].each do |suffix|
+ ["", "i", "f"].each do |suffix|
@struct_fields << "sf(#{number}#{suffix})"
end
end
end
- test 'if nothing is permitted, the hash becomes empty' do
- params = ActionController::Parameters.new(id: '1234')
+ def walk_permitted(params)
+ params.each do |k, v|
+ case v
+ when ActionController::Parameters
+ walk_permitted v
+ when Array
+ v.each { |x| walk_permitted v }
+ end
+ end
+ end
+
+ test "iteration should not impact permit" do
+ hash = { "foo" => { "bar" => { "0" => { "baz" => "hello", "zot" => "1" } } } }
+ params = ActionController::Parameters.new(hash)
+
+ walk_permitted params
+
+ sanitized = params[:foo].permit(bar: [:baz])
+ assert_equal({ "0" => { "baz" => "hello" } }, sanitized[:bar].to_unsafe_h)
+ end
+
+ test "if nothing is permitted, the hash becomes empty" do
+ params = ActionController::Parameters.new(id: "1234")
permitted = params.permit
- assert permitted.permitted?
- assert permitted.empty?
+ assert_predicate permitted, :permitted?
+ assert_empty permitted
end
- test 'key: permitted scalar values' do
- values = ['a', :a, nil]
- values += [0, 1.0, 2**128, BigDecimal.new(1)]
+ test "key: permitted scalar values" do
+ values = ["a", :a, nil]
+ values += [0, 1.0, 2**128, BigDecimal(1)]
values += [true, false]
values += [Date.today, Time.now, DateTime.now]
values += [STDOUT, StringIO.new, ActionDispatch::Http::UploadedFile.new(tempfile: __FILE__),
@@ -45,25 +68,33 @@ 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
- test 'key: unknown keys are filtered out' do
- params = ActionController::Parameters.new(id: '1234', injected: 'injected')
+ test "key: unknown keys are filtered out" do
+ params = ActionController::Parameters.new(id: "1234", injected: "injected")
permitted = params.permit(:id)
- assert_equal '1234', permitted[:id]
+ assert_equal "1234", permitted[:id]
assert_filtered_out permitted, :injected
end
- test 'key: arrays are filtered out' do
- [[], [1], ['1']].each do |array|
+ test "key: arrays are filtered out" do
+ [[], [1], ["1"]].each do |array|
params = ActionController::Parameters.new(id: array)
permitted = params.permit(:id)
assert_filtered_out permitted, :id
@@ -76,8 +107,8 @@ class ParametersPermitTest < ActiveSupport::TestCase
end
end
- test 'key: hashes are filtered out' do
- [{}, {foo: 1}, {foo: 'bar'}].each do |hash|
+ test "key: hashes are filtered out" do
+ [{}, { foo: 1 }, { foo: "bar" }].each do |hash|
params = ActionController::Parameters.new(id: hash)
permitted = params.permit(:id)
assert_filtered_out permitted, :id
@@ -90,7 +121,7 @@ class ParametersPermitTest < ActiveSupport::TestCase
end
end
- test 'key: non-permitted scalar values are filtered out' do
+ test "key: non-permitted scalar values are filtered out" do
params = ActionController::Parameters.new(id: Object.new)
permitted = params.permit(:id)
assert_filtered_out permitted, :id
@@ -102,51 +133,84 @@ class ParametersPermitTest < ActiveSupport::TestCase
end
end
- test 'key: it is not assigned if not present in params' do
- params = ActionController::Parameters.new(name: 'Joe')
+ test "key: it is not assigned if not present in params" do
+ params = ActionController::Parameters.new(name: "Joe")
permitted = params.permit(:id)
- assert !permitted.has_key?(:id)
+ assert_not permitted.has_key?(:id)
end
- test 'key to empty array: empty arrays pass' do
+ test "key to empty array: empty arrays pass" do
params = ActionController::Parameters.new(id: [])
permitted = params.permit(id: [])
assert_equal [], permitted[:id]
end
- test 'do not break params filtering on nil values' do
+ test "do not break params filtering on nil values" do
params = ActionController::Parameters.new(a: 1, b: [1, 2, 3], c: nil)
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
- [['foo'], [1], ['foo', 'bar'], [1, 2, 3]].each do |array|
+ test "key to empty array: arrays of permitted scalars pass" do
+ [["foo"], [1], ["foo", "bar"], [1, 2, 3]].each do |array|
params = ActionController::Parameters.new(id: array)
permitted = params.permit(id: [])
assert_equal array, permitted[:id]
end
end
- test 'key to empty array: permitted scalar values do not pass' do
- ['foo', 1].each do |permitted_scalar|
+ test "key to empty array: permitted scalar values do not pass" do
+ ["foo", 1].each do |permitted_scalar|
params = ActionController::Parameters.new(id: permitted_scalar)
permitted = params.permit(id: [])
assert_filtered_out permitted, :id
end
end
- test 'key to empty array: arrays of non-permitted scalar do not pass' do
- [[Object.new], [[]], [[1]], [{}], [{id: '1'}]].each do |non_permitted_scalar|
+ test "key to empty array: arrays of non-permitted scalar do not pass" do
+ [[Object.new], [[]], [[1]], [{}], [{ id: "1" }]].each do |non_permitted_scalar|
params = ActionController::Parameters.new(id: non_permitted_scalar)
permitted = params.permit(id: [])
assert_filtered_out permitted, :id
end
end
+ test "key to empty hash: arbitrary hashes are permitted" do
+ params = ActionController::Parameters.new(
+ username: "fxn",
+ preferences: {
+ scheme: "Marazul",
+ font: {
+ name: "Source Code Pro",
+ size: 12
+ },
+ tabstops: [4, 8, 12, 16],
+ suspicious: [true, Object.new, false, /yo!/],
+ dubious: [{ a: :a, b: /wtf!/ }, { c: :c }],
+ injected: Object.new
+ },
+ hacked: 1 # not a hash
+ )
+
+ permitted = params.permit(:username, preferences: {}, hacked: {})
+
+ assert_equal "fxn", permitted[:username]
+ assert_equal "Marazul", permitted[:preferences][:scheme]
+ assert_equal "Source Code Pro", permitted[:preferences][:font][:name]
+ assert_equal 12, permitted[:preferences][:font][:size]
+ assert_equal [4, 8, 12, 16], permitted[:preferences][:tabstops]
+ assert_equal [true, false], permitted[:preferences][:suspicious]
+ assert_equal :a, permitted[:preferences][:dubious][0][:a]
+ assert_equal :c, permitted[:preferences][:dubious][1][:c]
+
+ assert_filtered_out permitted[:preferences][:dubious][0], :b
+ assert_filtered_out permitted[:preferences], :injected
+ assert_filtered_out permitted, :hacked
+ end
+
test "fetch raises ParameterMissing exception" do
e = assert_raises(ActionController::ParameterMissing) do
@params.fetch :foo
@@ -157,20 +221,20 @@ 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
+ test "hashes in array values get wrapped" do
params = ActionController::Parameters.new(foo: [{}, {}])
params[:foo].each do |hash|
- assert !hash.permitted?
+ assert_not_predicate hash, :permitted?
end
end
# Strong params has an optimization to avoid looping every time you read
# a key whose value is an array and building a new object. We check that
# optimization here.
- test 'arrays are converted at most once' do
+ test "arrays are converted at most once" do
params = ActionController::Parameters.new(foo: [{}])
assert_same params[:foo], params[:foo]
end
@@ -181,12 +245,12 @@ class ParametersPermitTest < ActiveSupport::TestCase
# This test checks that if we push a hash to an array (in-place modification)
# the cache does not get fooled, the hash is still wrapped as strong params,
# and not permitted.
- test 'mutated arrays are detected' do
- params = ActionController::Parameters.new(users: [{id: 1}])
+ test "mutated arrays are detected" do
+ params = ActionController::Parameters.new(users: [{ id: 1 }])
permitted = params.permit(users: [:id])
- permitted[:users] << {injected: 1}
- assert_not permitted[:users].last.permitted?
+ permitted[:users] << { injected: 1 }
+ assert_not_predicate permitted[:users].last, :permitted?
end
test "fetch doesnt raise ParameterMissing exception if there is a default" do
@@ -194,13 +258,91 @@ class ParametersPermitTest < ActiveSupport::TestCase
assert_equal "monkey", @params.fetch(:foo) { "monkey" }
end
+ test "fetch doesnt raise ParameterMissing exception if there is a default that is nil" do
+ assert_nil @params.fetch(:foo, nil)
+ assert_nil @params.fetch(:foo) { nil }
+ end
+
+ test "KeyError in fetch block should not be covered up" do
+ params = ActionController::Parameters.new
+ e = assert_raises(KeyError) do
+ params.fetch(:missing_key) { {}.fetch(:also_missing) }
+ end
+ assert_match(/:also_missing$/, e.message)
+ end
+
test "not permitted is sticky beyond merges" do
- assert !@params.merge(a: "b").permitted?
+ assert_not_predicate @params.merge(a: "b"), :permitted?
end
test "permitted is sticky beyond merges" do
@params.permit!
- assert @params.merge(a: "b").permitted?
+ assert_predicate @params.merge(a: "b"), :permitted?
+ end
+
+ test "merge with parameters" do
+ other_params = ActionController::Parameters.new(id: "1234").permit!
+ merged_params = @params.merge(other_params)
+
+ assert merged_params[:id]
+ end
+
+ test "not permitted is sticky beyond merge!" do
+ assert_not_predicate @params.merge!(a: "b"), :permitted?
+ end
+
+ test "permitted is sticky beyond merge!" do
+ @params.permit!
+ assert_predicate @params.merge!(a: "b"), :permitted?
+ end
+
+ test "merge! with parameters" do
+ other_params = ActionController::Parameters.new(id: "1234").permit!
+ @params.merge!(other_params)
+
+ assert_equal "1234", @params[:id]
+ 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]
+ assert_not_predicate merged_params[:person], :empty?
+ end
+
+ test "#with_defaults is an alias of reverse_merge" do
+ default_params = ActionController::Parameters.new(id: "1234", person: {}).permit!
+ merged_params = @params.with_defaults(default_params)
+
+ assert_equal "1234", merged_params[:id]
+ assert_not_predicate merged_params[:person], :empty?
+ end
+
+ test "not permitted is sticky beyond reverse_merge" do
+ assert_not_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]
+ assert_not_predicate @params[:person], :empty?
+ end
+
+ test "#with_defaults! is an alias of reverse_merge!" do
+ default_params = ActionController::Parameters.new(id: "1234", person: {}).permit!
+ @params.with_defaults!(default_params)
+
+ assert_equal "1234", @params[:id]
+ assert_not_predicate @params[:person], :empty?
end
test "modifying the parameters" do
@@ -211,28 +353,26 @@ class ParametersPermitTest < ActiveSupport::TestCase
assert_equal "Jonas", @params[:person][:family][:brother]
end
- test "permit state is kept on a dup" do
- @params.permit!
- assert_equal @params.permitted?, @params.dup.permitted?
- end
-
- test "permit is recursive" do
+ test "permit! is recursive" do
+ @params[:nested_array] = [[{ x: 2, y: 3 }, { x: 21, y: 42 }]]
@params.permit!
- assert @params.permitted?
- assert @params[:person].permitted?
- assert @params[:person][:name].permitted?
- assert @params[:person][:addresses][0].permitted?
+ assert_predicate @params, :permitted?
+ assert_predicate @params[:person], :permitted?
+ assert_predicate @params[:person][:name], :permitted?
+ assert_predicate @params[:person][:addresses][0], :permitted?
+ assert_predicate @params[:nested_array][0][0], :permitted?
+ assert_predicate @params[:nested_array][0][1], :permitted?
end
test "permitted takes a default value when Parameters.permit_all_parameters is set" do
begin
ActionController::Parameters.permit_all_parameters = true
- params = ActionController::Parameters.new({ person: {
+ params = ActionController::Parameters.new(person: {
age: "32", name: { first: "David", last: "Heinemeier Hansson" }
- }})
+ })
- assert params.slice(:person).permitted?
- assert params[:person][:name].permitted?
+ assert_predicate params.slice(:person), :permitted?
+ assert_predicate params[:person][:name], :permitted?
ensure
ActionController::Parameters.permit_all_parameters = false
end
@@ -242,18 +382,17 @@ class ParametersPermitTest < ActiveSupport::TestCase
assert_equal "32", @params[:person].permit([ :age ])[:age]
end
- test "to_h returns empty hash on unpermitted params" do
- assert @params.to_h.is_a? Hash
- assert_not @params.to_h.is_a? ActionController::Parameters
- assert @params.to_h.empty?
+ test "to_h raises UnfilteredParameters on unfiltered params" do
+ assert_raises(ActionController::UnfilteredParameters) do
+ @params.to_h
+ end
end
test "to_h returns converted hash on permitted params" do
@params.permit!
- assert @params.to_h.is_a? Hash
- assert_not @params.to_h.is_a? ActionController::Parameters
- assert_equal @params.to_hash, @params.to_h
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess, @params.to_h
+ assert_not_kind_of ActionController::Parameters, @params.to_h
end
test "to_h returns converted hash when .permit_all_parameters is set" do
@@ -261,29 +400,117 @@ class ParametersPermitTest < ActiveSupport::TestCase
ActionController::Parameters.permit_all_parameters = true
params = ActionController::Parameters.new(crab: "Senjougahara Hitagi")
- assert params.to_h.is_a? Hash
- assert_not @params.to_h.is_a? ActionController::Parameters
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess, params.to_h
+ assert_not_kind_of ActionController::Parameters, params.to_h
assert_equal({ "crab" => "Senjougahara Hitagi" }, params.to_h)
ensure
ActionController::Parameters.permit_all_parameters = false
end
end
- test "to_h returns always permitted parameter on unpermitted params" do
- params = ActionController::Parameters.new(
- controller: "users",
- action: "create",
- user: {
- name: "Sengoku Nadeko"
- }
- )
+ test "to_hash raises UnfilteredParameters on unfiltered params" do
+ assert_raises(ActionController::UnfilteredParameters) do
+ @params.to_hash
+ end
+ end
+
+ test "to_hash returns converted hash on permitted params" do
+ @params.permit!
+
+ assert_instance_of Hash, @params.to_hash
+ assert_not_kind_of ActionController::Parameters, @params.to_hash
+ end
+
+ test "parameters can be implicit converted to Hash" do
+ params = ActionController::Parameters.new
+ params.permit!
+
+ assert_equal({ a: 1 }, { a: 1 }.merge!(params))
+ end
- assert_equal({ "controller" => "users", "action" => "create" }, params.to_h)
+ test "to_hash returns converted hash when .permit_all_parameters is set" do
+ begin
+ ActionController::Parameters.permit_all_parameters = true
+ params = ActionController::Parameters.new(crab: "Senjougahara Hitagi")
+
+ assert_instance_of Hash, params.to_hash
+ assert_not_kind_of ActionController::Parameters, params.to_hash
+ assert_equal({ "crab" => "Senjougahara Hitagi" }, params.to_hash)
+ assert_equal({ "crab" => "Senjougahara Hitagi" }, params)
+ ensure
+ ActionController::Parameters.permit_all_parameters = false
+ end
end
test "to_unsafe_h returns unfiltered params" do
- assert @params.to_h.is_a? Hash
- assert_not @params.to_h.is_a? ActionController::Parameters
- assert_equal @params.to_hash, @params.to_unsafe_h
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess, @params.to_unsafe_h
+ assert_not_kind_of ActionController::Parameters, @params.to_unsafe_h
+ end
+
+ test "to_unsafe_h returns unfiltered params even after accessing few keys" do
+ params = ActionController::Parameters.new("f" => { "language_facet" => ["Tibetan"] })
+ expected = { "f" => { "language_facet" => ["Tibetan"] } }
+
+ assert_instance_of ActionController::Parameters, params["f"]
+ assert_equal expected, params.to_unsafe_h
+ end
+
+ test "to_unsafe_h does not mutate the parameters" do
+ params = ActionController::Parameters.new("f" => { "language_facet" => ["Tibetan"] })
+ params[:f]
+
+ params.to_unsafe_h
+
+ assert_not_predicate params, :permitted?
+ assert_not_predicate params[:f], :permitted?
+ end
+
+ test "to_h only deep dups Ruby collections" do
+ company = Class.new do
+ attr_reader :dupped
+ def dup; @dupped = true; end
+ end.new
+
+ params = ActionController::Parameters.new(prem: { likes: %i( dancing ) })
+ assert_equal({ "prem" => { "likes" => %i( dancing ) } }, params.permit!.to_h)
+
+ params = ActionController::Parameters.new(companies: [ company, :acme ])
+ assert_equal({ "companies" => [ company, :acme ] }, params.permit!.to_h)
+ assert_not company.dupped
+ end
+
+ test "to_unsafe_h only deep dups Ruby collections" do
+ company = Class.new do
+ attr_reader :dupped
+ def dup; @dupped = true; end
+ end.new
+
+ params = ActionController::Parameters.new(prem: { likes: %i( dancing ) })
+ assert_equal({ "prem" => { "likes" => %i( dancing ) } }, params.to_unsafe_h)
+
+ params = ActionController::Parameters.new(companies: [ company, :acme ])
+ assert_equal({ "companies" => [ company, :acme ] }, params.to_unsafe_h)
+ assert_not company.dupped
+ end
+
+ test "include? returns true when the key is present" do
+ assert @params.include? :person
+ assert @params.include? "person"
+ assert_not @params.include? :gorilla
+ end
+
+ test "scalar values should be filtered when array or hash is specified" do
+ params = ActionController::Parameters.new(foo: "bar")
+
+ assert params.permit(:foo).has_key?(:foo)
+ assert_not params.permit(foo: []).has_key?(:foo)
+ assert_not params.permit(foo: [:bar]).has_key?(:foo)
+ assert_not params.permit(foo: :bar).has_key?(:foo)
+ end
+
+ test "#permitted? is false by default" do
+ params = ActionController::Parameters.new
+
+ assert_equal false, params.permitted?
end
end
diff --git a/actionpack/test/controller/parameters/raise_on_unpermitted_params_test.rb b/actionpack/test/controller/parameters/raise_on_unpermitted_params_test.rb
index f9cc9f96f1..4afd3da593 100644
--- a/actionpack/test/controller/parameters/raise_on_unpermitted_params_test.rb
+++ b/actionpack/test/controller/parameters/raise_on_unpermitted_params_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'action_controller/metal/strong_parameters'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "action_controller/metal/strong_parameters"
class RaiseOnUnpermittedParamsTest < ActiveSupport::TestCase
def setup
@@ -11,10 +13,9 @@ class RaiseOnUnpermittedParamsTest < ActiveSupport::TestCase
end
test "raises on unexpected params" do
- params = ActionController::Parameters.new({
+ params = ActionController::Parameters.new(
book: { pages: 65 },
- fishing: "Turnips"
- })
+ fishing: "Turnips")
assert_raises(ActionController::UnpermittedParameters) do
params.permit(book: [:pages])
@@ -22,9 +23,8 @@ class RaiseOnUnpermittedParamsTest < ActiveSupport::TestCase
end
test "raises on unexpected nested params" do
- params = ActionController::Parameters.new({
- book: { pages: 65, title: "Green Cats and where to find then." }
- })
+ params = ActionController::Parameters.new(
+ book: { pages: 65, title: "Green Cats and where to find then." })
assert_raises(ActionController::UnpermittedParameters) do
params.permit(book: [:pages])
diff --git a/actionpack/test/controller/parameters/serialization_test.rb b/actionpack/test/controller/parameters/serialization_test.rb
new file mode 100644
index 0000000000..7708c8e4fe
--- /dev/null
+++ b/actionpack/test/controller/parameters/serialization_test.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "action_controller/metal/strong_parameters"
+
+class ParametersSerializationTest < ActiveSupport::TestCase
+ setup do
+ @old_permitted_parameters = ActionController::Parameters.permit_all_parameters
+ ActionController::Parameters.permit_all_parameters = false
+ end
+
+ teardown do
+ ActionController::Parameters.permit_all_parameters = @old_permitted_parameters
+ end
+
+ test "yaml serialization" do
+ params = ActionController::Parameters.new(key: :value)
+ yaml_dump = YAML.dump(params)
+ assert_match("--- !ruby/object:ActionController::Parameters", yaml_dump)
+ assert_match(/parameters: !ruby\/hash:ActiveSupport::HashWithIndifferentAccess\n\s+key: :value/, yaml_dump)
+ assert_match("permitted: false", yaml_dump)
+ end
+
+ test "yaml deserialization" do
+ params = ActionController::Parameters.new(key: :value)
+ roundtripped = YAML.load(YAML.dump(params))
+
+ assert_equal params, roundtripped
+ assert_not_predicate roundtripped, :permitted?
+ end
+
+ test "yaml backwardscompatible with psych 2.0.8 format" do
+ params = YAML.load <<~end_of_yaml
+ --- !ruby/hash:ActionController::Parameters
+ key: :value
+ end_of_yaml
+
+ assert_equal :value, params[:key]
+ assert_not_predicate params, :permitted?
+ end
+
+ test "yaml backwardscompatible with psych 2.0.9+ format" do
+ params = YAML.load(<<~end_of_yaml)
+ --- !ruby/hash-with-ivars:ActionController::Parameters
+ elements:
+ key: :value
+ ivars:
+ :@permitted: false
+ end_of_yaml
+
+ assert_equal :value, params[:key]
+ assert_not_predicate params, :permitted?
+ end
+end
diff --git a/actionpack/test/controller/params_parse_test.rb b/actionpack/test/controller/params_parse_test.rb
new file mode 100644
index 0000000000..440ab06fd7
--- /dev/null
+++ b/actionpack/test/controller/params_parse_test.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class ParamsParseTest < ActionController::TestCase
+ class UsersController < ActionController::Base
+ def create
+ head :ok
+ end
+ end
+
+ tests UsersController
+
+ def test_parse_error_logged_once
+ log_output = capture_log_output do
+ post :create, body: "{", as: :json
+ end
+ assert_equal <<~LOG, log_output
+ Error occurred while parsing request parameters.
+ Contents:
+
+ {
+ LOG
+ end
+
+ private
+
+ def capture_log_output
+ output = StringIO.new
+ request.set_header "action_dispatch.logger", ActiveSupport::Logger.new(output)
+ yield
+ output.string
+ end
+end
diff --git a/actionpack/test/controller/params_wrapper_test.rb b/actionpack/test/controller/params_wrapper_test.rb
index 8bf016d060..c4c74e8f2b 100644
--- a/actionpack/test/controller/params_wrapper_test.rb
+++ b/actionpack/test/controller/params_wrapper_test.rb
@@ -1,10 +1,12 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module Admin; class User; end; end
module ParamsWrapperTestHelp
def with_default_wrapper_options(&block)
- @controller.class._set_wrapper_options({:format => [:json]})
+ @controller.class._set_wrapper_options(format: [:json])
@controller.class.inherited(@controller.class)
yield
end
@@ -28,8 +30,21 @@ class ParamsWrapperTest < ActionController::TestCase
end
end
- class User; end
- class Person; end
+ class User
+ def self.attribute_names
+ []
+ end
+
+ def self.stored_attributes
+ { settings: [:color, :size] }
+ end
+ end
+
+ class Person
+ def self.attribute_names
+ []
+ end
+ end
tests UsersController
@@ -39,17 +54,28 @@ class ParamsWrapperTest < ActionController::TestCase
def test_filtered_parameters
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' } }
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "username" => "sikachu" }
+ assert_equal({ "controller" => "params_wrapper_test/users", "action" => "parse", "username" => "sikachu", "user" => { "username" => "sikachu" } }, @request.filtered_parameters)
end
end
def test_derived_name_from_controller
with_default_wrapper_options do
- @request.env['CONTENT_TYPE'] = 'application/json'
- post :parse, params: { 'username' => 'sikachu' }
- assert_parameters({ 'username' => 'sikachu', 'user' => { 'username' => 'sikachu' }})
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "username" => "sikachu" }
+ assert_parameters("username" => "sikachu", "user" => { "username" => "sikachu" })
+ 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
@@ -57,9 +83,9 @@ class ParamsWrapperTest < ActionController::TestCase
with_default_wrapper_options do
UsersController.wrap_parameters :person
- @request.env['CONTENT_TYPE'] = 'application/json'
- post :parse, params: { 'username' => 'sikachu' }
- assert_parameters({ 'username' => 'sikachu', 'person' => { 'username' => 'sikachu' }})
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "username" => "sikachu" }
+ assert_parameters("username" => "sikachu", "person" => { "username" => "sikachu" })
end
end
@@ -67,147 +93,182 @@ class ParamsWrapperTest < ActionController::TestCase
with_default_wrapper_options do
UsersController.wrap_parameters Person
- @request.env['CONTENT_TYPE'] = 'application/json'
- post :parse, params: { 'username' => 'sikachu' }
- assert_parameters({ 'username' => 'sikachu', 'person' => { 'username' => 'sikachu' }})
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "username" => "sikachu" }
+ assert_parameters("username" => "sikachu", "person" => { "username" => "sikachu" })
end
end
def test_specify_include_option
with_default_wrapper_options do
- UsersController.wrap_parameters :include => :username
+ UsersController.wrap_parameters include: :username
- @request.env['CONTENT_TYPE'] = 'application/json'
- post :parse, params: { 'username' => 'sikachu', 'title' => 'Developer' }
- assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'user' => { 'username' => 'sikachu' }})
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "username" => "sikachu", "title" => "Developer" }
+ assert_parameters("username" => "sikachu", "title" => "Developer", "user" => { "username" => "sikachu" })
end
end
def test_specify_exclude_option
with_default_wrapper_options do
- UsersController.wrap_parameters :exclude => :title
+ UsersController.wrap_parameters exclude: :title
- @request.env['CONTENT_TYPE'] = 'application/json'
- post :parse, params: { 'username' => 'sikachu', 'title' => 'Developer' }
- assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'user' => { 'username' => 'sikachu' }})
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "username" => "sikachu", "title" => "Developer" }
+ assert_parameters("username" => "sikachu", "title" => "Developer", "user" => { "username" => "sikachu" })
end
end
def test_specify_both_wrapper_name_and_include_option
with_default_wrapper_options do
- UsersController.wrap_parameters :person, :include => :username
+ UsersController.wrap_parameters :person, include: :username
- @request.env['CONTENT_TYPE'] = 'application/json'
- post :parse, params: { 'username' => 'sikachu', 'title' => 'Developer' }
- assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'person' => { 'username' => 'sikachu' }})
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "username" => "sikachu", "title" => "Developer" }
+ assert_parameters("username" => "sikachu", "title" => "Developer", "person" => { "username" => "sikachu" })
end
end
def test_not_enabled_format
with_default_wrapper_options do
- @request.env['CONTENT_TYPE'] = 'application/xml'
- post :parse, params: { 'username' => 'sikachu', 'title' => 'Developer' }
- assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer' })
+ @request.env["CONTENT_TYPE"] = "application/xml"
+ post :parse, params: { "username" => "sikachu", "title" => "Developer" }
+ assert_parameters("username" => "sikachu", "title" => "Developer")
end
end
def test_wrap_parameters_false
with_default_wrapper_options do
UsersController.wrap_parameters false
- @request.env['CONTENT_TYPE'] = 'application/json'
- post :parse, params: { 'username' => 'sikachu', 'title' => 'Developer' }
- assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer' })
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "username" => "sikachu", "title" => "Developer" }
+ assert_parameters("username" => "sikachu", "title" => "Developer")
end
end
def test_specify_format
with_default_wrapper_options do
- UsersController.wrap_parameters :format => :xml
+ UsersController.wrap_parameters format: :xml
- @request.env['CONTENT_TYPE'] = 'application/xml'
- post :parse, params: { 'username' => 'sikachu', 'title' => 'Developer' }
- assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'user' => { 'username' => 'sikachu', 'title' => 'Developer' }})
+ @request.env["CONTENT_TYPE"] = "application/xml"
+ post :parse, params: { "username" => "sikachu", "title" => "Developer" }
+ assert_parameters("username" => "sikachu", "title" => "Developer", "user" => { "username" => "sikachu", "title" => "Developer" })
end
end
def test_not_wrap_reserved_parameters
with_default_wrapper_options do
- @request.env['CONTENT_TYPE'] = 'application/json'
- post :parse, params: { 'authenticity_token' => 'pwned', '_method' => 'put', 'utf8' => '&#9731;', 'username' => 'sikachu' }
- assert_parameters({ 'authenticity_token' => 'pwned', '_method' => 'put', 'utf8' => '&#9731;', 'username' => 'sikachu', 'user' => { 'username' => 'sikachu' }})
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "authenticity_token" => "pwned", "_method" => "put", "utf8" => "&#9731;", "username" => "sikachu" }
+ assert_parameters("authenticity_token" => "pwned", "_method" => "put", "utf8" => "&#9731;", "username" => "sikachu", "user" => { "username" => "sikachu" })
end
end
def test_no_double_wrap_if_key_exists
with_default_wrapper_options do
- @request.env['CONTENT_TYPE'] = 'application/json'
- post :parse, params: { 'user' => { 'username' => 'sikachu' }}
- assert_parameters({ 'user' => { 'username' => 'sikachu' }})
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "user" => { "username" => "sikachu" } }
+ assert_parameters("user" => { "username" => "sikachu" })
+ end
+ end
+
+ def test_no_double_wrap_if_key_exists_and_value_is_nil
+ with_default_wrapper_options do
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "user" => nil }
+ assert_parameters("user" => nil)
end
end
def test_nested_params
with_default_wrapper_options do
- @request.env['CONTENT_TYPE'] = 'application/json'
- post :parse, params: { 'person' => { 'username' => 'sikachu' }}
- assert_parameters({ 'person' => { 'username' => 'sikachu' }, 'user' => {'person' => { 'username' => 'sikachu' }}})
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "person" => { "username" => "sikachu" } }
+ assert_parameters("person" => { "username" => "sikachu" }, "user" => { "person" => { "username" => "sikachu" } })
end
end
def test_derived_wrapped_keys_from_matching_model
- User.expects(:respond_to?).with(:attribute_names).returns(true)
- User.expects(:attribute_names).twice.returns(["username"])
-
- with_default_wrapper_options do
- @request.env['CONTENT_TYPE'] = 'application/json'
- post :parse, params: { 'username' => 'sikachu', 'title' => 'Developer' }
- assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'user' => { 'username' => 'sikachu' }})
+ 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", "title" => "Developer" }
+ assert_parameters("username" => "sikachu", "title" => "Developer", "user" => { "username" => "sikachu" })
+ end
end
end
def test_derived_wrapped_keys_from_specified_model
with_default_wrapper_options do
- Person.expects(:respond_to?).with(:attribute_names).returns(true)
- Person.expects(:attribute_names).twice.returns(["username"])
-
- UsersController.wrap_parameters Person
+ assert_called(Person, :attribute_names, times: 2, returns: ["username"]) do
+ UsersController.wrap_parameters Person
- @request.env['CONTENT_TYPE'] = 'application/json'
- post :parse, params: { 'username' => 'sikachu', 'title' => 'Developer' }
- assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'person' => { 'username' => 'sikachu' }})
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "username" => "sikachu", "title" => "Developer" }
+ assert_parameters("username" => "sikachu", "title" => "Developer", "person" => { "username" => "sikachu" })
+ end
end
end
def test_not_wrapping_abstract_model
- User.expects(:respond_to?).with(:attribute_names).returns(true)
- User.expects(:attribute_names).returns([])
-
with_default_wrapper_options do
- @request.env['CONTENT_TYPE'] = 'application/json'
- post :parse, params: { 'username' => 'sikachu', 'title' => 'Developer' }
- assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'user' => { 'username' => 'sikachu', 'title' => 'Developer' }})
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "username" => "sikachu", "title" => "Developer" }
+ assert_parameters("username" => "sikachu", "title" => "Developer", "user" => { "username" => "sikachu", "title" => "Developer" })
end
end
def test_preserves_query_string_params
with_default_wrapper_options do
- @request.env['CONTENT_TYPE'] = 'application/json'
- get :parse, params: { 'user' => { 'username' => 'nixon' } }
+ @request.env["CONTENT_TYPE"] = "application/json"
+ get :parse, params: { "user" => { "username" => "nixon" } }
assert_parameters(
- {'user' => { 'username' => 'nixon' } }
+ "user" => { "username" => "nixon" }
)
end
end
+ def test_preserves_query_string_params_in_filtered_params
+ with_default_wrapper_options do
+ @request.env["CONTENT_TYPE"] = "application/json"
+ get :parse, params: { "user" => { "username" => "nixon" } }
+ assert_equal({ "controller" => "params_wrapper_test/users", "action" => "parse", "user" => { "username" => "nixon" } }, @request.filtered_parameters)
+ end
+ end
+
def test_empty_parameter_set
with_default_wrapper_options do
- @request.env['CONTENT_TYPE'] = 'application/json'
+ @request.env["CONTENT_TYPE"] = "application/json"
post :parse, params: {}
assert_parameters(
- {'user' => { } }
+ "user" => {}
)
end
end
+
+ def test_handles_empty_content_type
+ with_default_wrapper_options do
+ @request.env["CONTENT_TYPE"] = nil
+ _controller_class.dispatch(:parse, @request, @response)
+
+ assert_equal 200, @response.status
+ assert_equal "", @response.body
+ end
+ end
+
+ def test_derived_wrapped_keys_from_nested_attributes
+ def User.nested_attributes_options
+ { person: {} }
+ end
+
+ assert_called(User, :attribute_names, times: 2, returns: ["username"]) do
+ with_default_wrapper_options do
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "username" => "sikachu", "person_attributes" => { "title" => "Developer" } }
+ assert_parameters("username" => "sikachu", "person_attributes" => { "title" => "Developer" }, "user" => { "username" => "sikachu", "person_attributes" => { "title" => "Developer" } })
+ end
+ end
+ end
end
class NamespacedParamsWrapperTest < ActionController::TestCase
@@ -215,7 +276,7 @@ class NamespacedParamsWrapperTest < ActionController::TestCase
module Admin
module Users
- class UsersController < ActionController::Base;
+ class UsersController < ActionController::Base
class << self
attr_accessor :last_parameters
end
@@ -248,9 +309,9 @@ class NamespacedParamsWrapperTest < ActionController::TestCase
def test_derived_name_from_controller
with_default_wrapper_options do
- @request.env['CONTENT_TYPE'] = 'application/json'
- post :parse, params: { 'username' => 'sikachu' }
- assert_parameters({'username' => 'sikachu', 'user' => { 'username' => 'sikachu' }})
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "username" => "sikachu" }
+ assert_parameters("username" => "sikachu", "user" => { "username" => "sikachu" })
end
end
@@ -258,9 +319,9 @@ class NamespacedParamsWrapperTest < ActionController::TestCase
Admin.const_set(:User, Class.new(SampleOne))
begin
with_default_wrapper_options do
- @request.env['CONTENT_TYPE'] = 'application/json'
- post :parse, params: { 'username' => 'sikachu', 'title' => 'Developer' }
- assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'user' => { 'username' => 'sikachu' }})
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "username" => "sikachu", "title" => "Developer" }
+ assert_parameters("username" => "sikachu", "title" => "Developer", "user" => { "username" => "sikachu" })
end
ensure
Admin.send :remove_const, :User
@@ -271,15 +332,14 @@ class NamespacedParamsWrapperTest < ActionController::TestCase
Object.const_set(:User, Class.new(SampleTwo))
begin
with_default_wrapper_options do
- @request.env['CONTENT_TYPE'] = 'application/json'
- post :parse, params: { 'username' => 'sikachu', 'title' => 'Developer' }
- assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'user' => { 'title' => 'Developer' }})
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "username" => "sikachu", "title" => "Developer" }
+ assert_parameters("username" => "sikachu", "title" => "Developer", "user" => { "title" => "Developer" })
end
ensure
Object.send :remove_const, :User
end
end
-
end
class AnonymousControllerParamsWrapperTest < ActionController::TestCase
@@ -298,18 +358,18 @@ class AnonymousControllerParamsWrapperTest < ActionController::TestCase
def test_does_not_implicitly_wrap_params
with_default_wrapper_options do
- @request.env['CONTENT_TYPE'] = 'application/json'
- post :parse, params: { 'username' => 'sikachu' }
- assert_parameters({ 'username' => 'sikachu' })
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "username" => "sikachu" }
+ assert_parameters("username" => "sikachu")
end
end
def test_does_wrap_params_if_name_provided
with_default_wrapper_options do
- @controller.class.wrap_parameters(:name => "guest")
- @request.env['CONTENT_TYPE'] = 'application/json'
- post :parse, params: { 'username' => 'sikachu' }
- assert_parameters({ 'username' => 'sikachu', 'guest' => { 'username' => 'sikachu' }})
+ @controller.class.wrap_parameters(name: "guest")
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "username" => "sikachu" }
+ assert_parameters("username" => "sikachu", "guest" => { "username" => "sikachu" })
end
end
end
@@ -319,7 +379,7 @@ class IrregularInflectionParamsWrapperTest < ActionController::TestCase
class ParamswrappernewsItem
def self.attribute_names
- ['test_attr']
+ ["test_attr"]
end
end
@@ -339,24 +399,24 @@ class IrregularInflectionParamsWrapperTest < ActionController::TestCase
def test_uses_model_attribute_names_with_irregular_inflection
with_dup do
ActiveSupport::Inflector.inflections do |inflect|
- inflect.irregular 'paramswrappernews_item', 'paramswrappernews'
+ inflect.irregular "paramswrappernews_item", "paramswrappernews"
end
with_default_wrapper_options do
- @request.env['CONTENT_TYPE'] = 'application/json'
- post :parse, params: { 'username' => 'sikachu', 'test_attr' => 'test_value' }
- assert_parameters({ 'username' => 'sikachu', 'test_attr' => 'test_value', 'paramswrappernews_item' => { 'test_attr' => 'test_value' }})
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "username" => "sikachu", "test_attr" => "test_value" }
+ assert_parameters("username" => "sikachu", "test_attr" => "test_value", "paramswrappernews_item" => { "test_attr" => "test_value" })
end
end
end
private
- def with_dup
- original = ActiveSupport::Inflector::Inflections.instance_variable_get(:@__instance__)[:en]
- ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: original.dup)
- yield
- ensure
- ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: original)
- end
+ def with_dup
+ original = ActiveSupport::Inflector::Inflections.instance_variable_get(:@__instance__)[:en]
+ ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: original.dup)
+ yield
+ ensure
+ ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: original)
+ end
end
diff --git a/actionpack/test/controller/permitted_params_test.rb b/actionpack/test/controller/permitted_params_test.rb
index 7136fafae5..caac88ffb2 100644
--- a/actionpack/test/controller/permitted_params_test.rb
+++ b/actionpack/test/controller/permitted_params_test.rb
@@ -1,12 +1,14 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class PeopleController < ActionController::Base
def create
- render text: params[:person].permitted? ? "permitted" : "forbidden"
+ render plain: params[:person].permitted? ? "permitted" : "forbidden"
end
def create_with_permit
- render text: params[:person].permit(:name).permitted? ? "permitted" : "forbidden"
+ render plain: params[:person].permit(:name).permitted? ? "permitted" : "forbidden"
end
end
diff --git a/actionpack/test/controller/redirect_test.rb b/actionpack/test/controller/redirect_test.rb
index 4f5ca46b04..998498e1b2 100644
--- a/actionpack/test/controller/redirect_test.rb
+++ b/actionpack/test/controller/redirect_test.rb
@@ -1,53 +1,83 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class Workshop
+ extend ActiveModel::Naming
+ include ActiveModel::Conversion
+
+ OUT_OF_SCOPE_BLOCK = proc do
+ raise "Not executed in controller's context" unless RedirectController === self
+ request.original_url
+ end
+
+ attr_accessor :id
+
+ def initialize(id)
+ @id = id
+ end
+
+ def persisted?
+ id.present?
+ end
+
+ def to_s
+ id.to_s
+ end
+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 :text => 'called status'; end
- def location; render :text => '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"
+ redirect_to action: "hello_world"
end
def redirect_with_status
- redirect_to({:action => "hello_world", :status => 301})
+ redirect_to(action: "hello_world", status: 301)
end
def redirect_with_status_hash
- redirect_to({:action => "hello_world"}, {:status => 301})
+ redirect_to({ action: "hello_world" }, { status: 301 })
end
def redirect_with_protocol
- redirect_to :action => "hello_world", :protocol => "https"
+ redirect_to action: "hello_world", protocol: "https"
end
def url_redirect_with_status
- redirect_to("http://www.example.com", :status => :moved_permanently)
+ redirect_to("http://www.example.com", status: :moved_permanently)
end
def url_redirect_with_status_hash
- redirect_to("http://www.example.com", {:status => 301})
+ redirect_to("http://www.example.com", status: 301)
end
def relative_url_redirect_with_status
- redirect_to("/things/stuff", :status => :found)
+ redirect_to("/things/stuff", status: :found)
end
def relative_url_redirect_with_status_hash
- redirect_to("/things/stuff", {:status => 301})
+ redirect_to("/things/stuff", status: 301)
+ end
+
+ def redirect_back_with_status
+ redirect_back(fallback_location: "/things/stuff", status: 307)
end
- def redirect_to_back_with_status
- redirect_to :back, :status => 307
+ def safe_redirect_back_with_status
+ redirect_back(fallback_location: "/things/stuff", status: 307, allow_other_host: false)
end
def host_redirect
- redirect_to :action => "other_host", :only_path => false, :host => 'other.test.host'
+ redirect_to action: "other_host", only_path: false, host: "other.test.host"
end
def module_redirect
- redirect_to :controller => 'module_test/module_redirect', :action => "hello_world"
+ redirect_to controller: "module_test/module_redirect", action: "hello_world"
end
def redirect_to_url
@@ -66,10 +96,6 @@ class RedirectController < ActionController::Base
redirect_to "//www.rubyonrails.org/"
end
- def redirect_to_back
- redirect_to :back
- end
-
def redirect_to_existing_record
redirect_to Workshop.new(5)
end
@@ -83,7 +109,7 @@ class RedirectController < ActionController::Base
end
def redirect_to_params
- redirect_to ActionController::Parameters.new(status: 200, protocol: 'javascript', f: '%0Aeval(name)')
+ redirect_to ActionController::Parameters.new(status: 200, protocol: "javascript", f: "%0Aeval(name)")
end
def redirect_to_with_block
@@ -96,7 +122,11 @@ class RedirectController < ActionController::Base
end
def redirect_to_with_block_and_options
- redirect_to proc { {:action => "hello_world"} }
+ redirect_to proc { { action: "hello_world" } }
+ end
+
+ def redirect_to_out_of_scope_block
+ redirect_to Workshop::OUT_OF_SCOPE_BLOCK
end
def redirect_with_header_break
@@ -109,9 +139,9 @@ 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 }
+ url_for action: "dashboard", params: { "id" => id, "message" => message }
end
end
@@ -172,7 +202,6 @@ class RedirectTest < ActionController::TestCase
assert_equal "http://www.example.com", redirect_to_url
end
-
def test_relative_url_redirect_with_status
get :relative_url_redirect_with_status
assert_response 302
@@ -185,17 +214,17 @@ class RedirectTest < ActionController::TestCase
assert_equal "http://test.host/things/stuff", redirect_to_url
end
- def test_redirect_to_back_with_status
- @request.env["HTTP_REFERER"] = "http://www.example.com/coming/from"
- get :redirect_to_back_with_status
- assert_response 307
- assert_equal "http://www.example.com/coming/from", redirect_to_url
+ def test_relative_url_redirect_host_with_port
+ request.host = "test.host:1234"
+ get :relative_url_redirect_with_status
+ assert_response 302
+ assert_equal "http://test.host:1234/things/stuff", redirect_to_url
end
def test_simple_redirect_using_options
get :host_redirect
assert_response :redirect
- assert_redirected_to :action => "other_host", :only_path => false, :host => 'other.test.host'
+ assert_redirected_to action: "other_host", only_path: false, host: "other.test.host"
end
def test_module_redirect
@@ -207,7 +236,7 @@ class RedirectTest < ActionController::TestCase
def test_module_redirect_using_options
get :module_redirect
assert_response :redirect
- assert_redirected_to :controller => 'module_test/module_redirect', :action => 'hello_world'
+ assert_redirected_to controller: "module_test/module_redirect", action: "hello_world"
end
def test_redirect_to_url
@@ -234,25 +263,48 @@ class RedirectTest < ActionController::TestCase
assert_equal "//www.rubyonrails.org/", redirect_to_url
end
- def test_redirect_to_back
- @request.env["HTTP_REFERER"] = "http://www.example.com/coming/from"
- get :redirect_to_back
- assert_response :redirect
- assert_equal "http://www.example.com/coming/from", redirect_to_url
+ def test_redirect_back
+ referer = "http://www.example.com/coming/from"
+ @request.env["HTTP_REFERER"] = referer
+
+ get :redirect_back_with_status
+
+ assert_response 307
+ assert_equal referer, redirect_to_url
end
- def test_redirect_to_back_with_no_referer
- assert_raise(ActionController::RedirectBackError) {
- @request.env["HTTP_REFERER"] = nil
- get :redirect_to_back
- }
+ def test_redirect_back_with_no_referer
+ get :redirect_back_with_status
+
+ assert_response 307
+ assert_equal "http://test.host/things/stuff", redirect_to_url
+ end
+
+ def test_safe_redirect_back_from_other_host
+ @request.env["HTTP_REFERER"] = "http://another.host/coming/from"
+ get :safe_redirect_back_with_status
+
+ assert_response 307
+ assert_equal "http://test.host/things/stuff", redirect_to_url
+ end
+
+ def test_safe_redirect_back_from_the_same_host
+ referer = "http://test.host/coming/from"
+ @request.env["HTTP_REFERER"] = referer
+ get :safe_redirect_back_with_status
+
+ assert_response 307
+ assert_equal referer, redirect_to_url
end
def test_redirect_to_record
with_routing do |set|
set.draw do
resources :workshops
- get ':controller/:action'
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action"
+ end
end
get :redirect_to_existing_record
@@ -266,15 +318,17 @@ class RedirectTest < ActionController::TestCase
end
def test_redirect_to_nil
- assert_raise(ActionController::ActionControllerError) do
+ error = assert_raise(ActionController::ActionControllerError) do
get :redirect_to_nil
end
+ assert_equal "Cannot redirect to nil!", error.message
end
def test_redirect_to_params
- assert_raise(ActionController::ActionControllerError) do
+ error = assert_raise(ActionController::UnfilteredParameters) do
get :redirect_to_params
end
+ assert_equal "unable to convert unpermitted parameters to hash", error.message
end
def test_redirect_to_with_block
@@ -289,10 +343,18 @@ class RedirectTest < ActionController::TestCase
assert_redirected_to "http://www.rubyonrails.org/"
end
+ def test_redirect_to_out_of_scope_block
+ get :redirect_to_out_of_scope_block
+ assert_response :redirect
+ assert_redirected_to "http://test.host/redirect/redirect_to_out_of_scope_block"
+ end
+
def test_redirect_to_with_block_and_accepted_options
with_routing do |set|
set.draw do
- get ':controller/:action'
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action"
+ end
end
get :redirect_to_with_block_and_options
@@ -306,7 +368,7 @@ end
module ModuleTest
class ModuleRedirectController < ::RedirectController
def module_redirect
- redirect_to :controller => '/redirect', :action => "hello_world"
+ redirect_to controller: "/redirect", action: "hello_world"
end
end
@@ -322,7 +384,7 @@ module ModuleTest
def test_simple_redirect_using_options
get :host_redirect
assert_response :redirect
- assert_redirected_to :action => "other_host", :only_path => false, :host => 'other.test.host'
+ assert_redirected_to action: "other_host", only_path: false, host: "other.test.host"
end
def test_module_redirect
@@ -334,7 +396,7 @@ module ModuleTest
def test_module_redirect_using_options
get :module_redirect
assert_response :redirect
- assert_redirected_to :controller => '/redirect', :action => "hello_world"
+ assert_redirected_to controller: "/redirect", action: "hello_world"
end
end
end
diff --git a/actionpack/test/controller/render_js_test.rb b/actionpack/test/controller/render_js_test.rb
index 6b661de064..1efc0b9de1 100644
--- a/actionpack/test/controller/render_js_test.rb
+++ b/actionpack/test/controller/render_js_test.rb
@@ -1,21 +1,23 @@
-require 'abstract_unit'
-require 'controller/fake_models'
-require 'pathname'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "controller/fake_models"
+require "pathname"
class RenderJSTest < ActionController::TestCase
class TestController < ActionController::Base
protect_from_forgery
def self.controller_path
- 'test'
+ "test"
end
def render_vanilla_js_hello
- render :js => "alert('hello')"
+ render js: "alert('hello')"
end
def show_partial
- render :partial => 'partial'
+ render partial: "partial"
end
end
@@ -28,7 +30,7 @@ class RenderJSTest < ActionController::TestCase
end
def test_should_render_js_partial
- get :show_partial, format: 'js', xhr: true
- assert_equal 'partial js', @response.body
+ get :show_partial, format: "js", xhr: true
+ assert_equal "partial js", @response.body
end
end
diff --git a/actionpack/test/controller/render_json_test.rb b/actionpack/test/controller/render_json_test.rb
index b1ad16bc55..82c1ba26cb 100644
--- a/actionpack/test/controller/render_json_test.rb
+++ b/actionpack/test/controller/render_json_test.rb
@@ -1,18 +1,20 @@
-require 'abstract_unit'
-require 'controller/fake_models'
-require 'active_support/logger'
-require 'pathname'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "controller/fake_models"
+require "active_support/logger"
+require "pathname"
class RenderJsonTest < ActionController::TestCase
class JsonRenderable
- def as_json(options={})
- hash = { :a => :b, :c => :d, :e => :f }
+ def as_json(options = {})
+ hash = { a: :b, c: :d, e: :f }
hash.except!(*options[:except]) if options[:except]
hash
end
def to_json(options = {})
- super :except => [:c, :e]
+ super except: [:c, :e]
end
end
@@ -20,47 +22,47 @@ class RenderJsonTest < ActionController::TestCase
protect_from_forgery
def self.controller_path
- 'test'
+ "test"
end
def render_json_nil
- render :json => nil
+ render json: nil
end
def render_json_render_to_string
- render :text => render_to_string(:json => '[]')
+ render plain: render_to_string(json: "[]")
end
def render_json_hello_world
- render :json => ActiveSupport::JSON.encode(:hello => 'world')
+ render json: ActiveSupport::JSON.encode(hello: "world")
end
def render_json_hello_world_with_status
- render :json => ActiveSupport::JSON.encode(:hello => 'world'), :status => 401
+ render json: ActiveSupport::JSON.encode(hello: "world"), status: 401
end
def render_json_hello_world_with_callback
- render :json => ActiveSupport::JSON.encode(:hello => 'world'), :callback => 'alert'
+ render json: ActiveSupport::JSON.encode(hello: "world"), callback: "alert"
end
def render_json_with_custom_content_type
- render :json => ActiveSupport::JSON.encode(:hello => 'world'), :content_type => 'text/javascript'
+ render json: ActiveSupport::JSON.encode(hello: "world"), content_type: "text/javascript"
end
def render_symbol_json
- render :json => ActiveSupport::JSON.encode(:hello => 'world')
+ render json: ActiveSupport::JSON.encode(hello: "world")
end
def render_json_with_render_to_string
- render :json => {:hello => render_to_string(:partial => 'partial')}
+ render json: { hello: render_to_string(partial: "partial") }
end
def render_json_with_extra_options
- render :json => JsonRenderable.new, :except => [:c, :e]
+ render json: JsonRenderable.new, except: [:c, :e]
end
def render_json_without_options
- render :json => JsonRenderable.new
+ render json: JsonRenderable.new
end
end
@@ -77,20 +79,19 @@ class RenderJsonTest < ActionController::TestCase
def test_render_json_nil
get :render_json_nil
- assert_equal 'null', @response.body
- assert_equal 'application/json', @response.content_type
+ assert_equal "null", @response.body
+ assert_equal "application/json", @response.content_type
end
def test_render_json_render_to_string
get :render_json_render_to_string
- assert_equal '[]', @response.body
+ assert_equal "[]", @response.body
end
-
def test_render_json
get :render_json_hello_world
assert_equal '{"hello":"world"}', @response.body
- assert_equal 'application/json', @response.content_type
+ assert_equal "application/json", @response.content_type
end
def test_render_json_with_status
@@ -102,31 +103,31 @@ class RenderJsonTest < ActionController::TestCase
def test_render_json_with_callback
get :render_json_hello_world_with_callback, xhr: true
assert_equal '/**/alert({"hello":"world"})', @response.body
- assert_equal 'text/javascript', @response.content_type
+ assert_equal "text/javascript", @response.content_type
end
def test_render_json_with_custom_content_type
get :render_json_with_custom_content_type, xhr: true
assert_equal '{"hello":"world"}', @response.body
- assert_equal 'text/javascript', @response.content_type
+ assert_equal "text/javascript", @response.content_type
end
def test_render_symbol_json
get :render_symbol_json
assert_equal '{"hello":"world"}', @response.body
- assert_equal 'application/json', @response.content_type
+ assert_equal "application/json", @response.content_type
end
def test_render_json_with_render_to_string
get :render_json_with_render_to_string
assert_equal '{"hello":"partial html"}', @response.body
- assert_equal 'application/json', @response.content_type
+ assert_equal "application/json", @response.content_type
end
def test_render_json_forwards_extra_options
get :render_json_with_extra_options
assert_equal '{"a":"b"}', @response.body
- assert_equal 'application/json', @response.content_type
+ assert_equal "application/json", @response.content_type
end
def test_render_json_calls_to_json_from_object
diff --git a/actionpack/test/controller/render_other_test.rb b/actionpack/test/controller/render_other_test.rb
deleted file mode 100644
index af50e11261..0000000000
--- a/actionpack/test/controller/render_other_test.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-require 'abstract_unit'
-
-
-class RenderOtherTest < ActionController::TestCase
- class TestController < ActionController::Base
- def render_simon_says
- render :simon => "foo"
- end
- end
-
- tests TestController
-
- def test_using_custom_render_option
- ActionController.add_renderer :simon do |says, options|
- self.content_type = Mime::TEXT
- self.response_body = "Simon says: #{says}"
- end
-
- get :render_simon_says
- assert_equal "Simon says: foo", @response.body
- ensure
- ActionController.remove_renderer :simon
- end
-end
diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb
index 43e992d432..306b245bd1 100644
--- a/actionpack/test/controller/render_test.rb
+++ b/actionpack/test/controller/render_test.rb
@@ -1,32 +1,55 @@
-require 'abstract_unit'
-require 'controller/fake_models'
-require 'pathname'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "controller/fake_models"
class TestControllerWithExtraEtags < ActionController::Base
+ def self.controller_name; "test"; end
+ def self.controller_path; "test"; end
+
etag { nil }
- etag { 'ab' }
+ etag { "ab" }
etag { :cde }
etag { [:f] }
etag { nil }
def fresh
- render text: "stale" if stale?(etag: '123', template: false)
+ render plain: "stale" if stale?(etag: "123", template: false)
end
def array
- render text: "stale" if stale?(etag: %w(1 2 3), template: false)
+ render plain: "stale" if stale?(etag: %w(1 2 3), template: false)
+ end
+
+ def strong
+ render plain: "stale" if stale?(strong_etag: "strong", template: false)
end
def with_template
- if stale? template: 'test/hello_world'
- render text: 'stale'
+ if stale? template: "test/hello_world"
+ render plain: "stale"
end
end
+
+ def with_implicit_template
+ fresh_when(etag: "123")
+ end
end
class ImplicitRenderTestController < ActionController::Base
def empty_action
end
+
+ def empty_action_with_template
+ end
+end
+
+module Namespaced
+ class ImplicitRenderTestController < ActionController::Base
+ def hello_world
+ fresh_when(etag: "abc")
+ end
+ end
end
class TestController < ActionController::Base
@@ -50,8 +73,8 @@ class TestController < ActionController::Base
end
def conditional_hello
- if stale?(:last_modified => Time.now.utc.beginning_of_day, :etag => [:foo, 123])
- render :action => 'hello_world'
+ if stale?(last_modified: Time.now.utc.beginning_of_day, etag: [:foo, 123])
+ render action: "hello_world"
end
end
@@ -59,10 +82,24 @@ class TestController < ActionController::Base
record = Struct.new(:updated_at, :cache_key).new(Time.now.utc.beginning_of_day, "foo/123")
if stale?(record)
- render :action => 'hello_world'
+ render action: "hello_world"
end
end
+ def dynamic_render
+ render params[:id] # => String, AC::Params
+ end
+
+ def dynamic_render_permit
+ render params[:id].permit(:file)
+ end
+
+ def dynamic_render_with_file
+ # This is extremely bad, but should be possible to do.
+ file = params[:id] # => String, AC::Params
+ render file: file
+ end
+
class Collection
def initialize(records)
@records = records
@@ -80,70 +117,79 @@ class TestController < ActionController::Base
old_record = Struct.new(:updated_at, :cache_key).new(ts - 1.day, "bar/123")
if stale?(Collection.new([record, old_record]))
- render action: 'hello_world'
+ render action: "hello_world"
end
end
def conditional_hello_with_expires_in
expires_in 60.1.seconds
- render :action => 'hello_world'
+ render action: "hello_world"
end
def conditional_hello_with_expires_in_with_public
- expires_in 1.minute, :public => true
- render :action => 'hello_world'
+ expires_in 1.minute, public: true
+ render action: "hello_world"
end
def conditional_hello_with_expires_in_with_must_revalidate
- expires_in 1.minute, :must_revalidate => true
- render :action => 'hello_world'
+ expires_in 1.minute, must_revalidate: true
+ render action: "hello_world"
end
def conditional_hello_with_expires_in_with_public_and_must_revalidate
- expires_in 1.minute, :public => true, :must_revalidate => true
- render :action => 'hello_world'
+ expires_in 1.minute, public: true, must_revalidate: true
+ render action: "hello_world"
+ end
+
+ def conditional_hello_with_expires_in_with_stale_while_revalidate
+ expires_in 1.minute, public: true, stale_while_revalidate: 5.minutes
+ render action: "hello_world"
+ end
+
+ def conditional_hello_with_expires_in_with_stale_if_error
+ expires_in 1.minute, public: true, stale_if_error: 5.minutes
+ render action: "hello_world"
end
def conditional_hello_with_expires_in_with_public_with_more_keys
- expires_in 1.minute, :public => true, 's-maxage' => 5.hours
- render :action => 'hello_world'
+ expires_in 1.minute, :public => true, "s-maxage" => 5.hours
+ render action: "hello_world"
end
def conditional_hello_with_expires_in_with_public_with_more_keys_old_syntax
- expires_in 1.minute, :public => true, :private => nil, 's-maxage' => 5.hours
- render :action => 'hello_world'
+ expires_in 1.minute, :public => true, :private => nil, "s-maxage" => 5.hours
+ render action: "hello_world"
end
def conditional_hello_with_expires_now
expires_now
- render :action => 'hello_world'
+ render action: "hello_world"
end
def conditional_hello_with_cache_control_headers
- response.headers['Cache-Control'] = 'no-transform'
+ response.headers["Cache-Control"] = "no-transform"
expires_now
- render :action => 'hello_world'
- end
-
- def respond_with_empty_body
- render nothing: true
+ render action: "hello_world"
end
- def conditional_hello_with_bangs
- render :action => 'hello_world'
+ def conditional_hello_with_expires_and_confliciting_cache_control_headers
+ response.headers["Cache-Control"] = "no-cache, must-revalidate"
+ expires_now
+ render action: "hello_world"
end
- before_action :handle_last_modified_and_etags, :only=>:conditional_hello_with_bangs
- def handle_last_modified_and_etags
- fresh_when(:last_modified => Time.now.utc.beginning_of_day, :etag => [ :foo, 123 ])
+ def conditional_hello_without_expires_and_confliciting_cache_control_headers
+ response.headers["Cache-Control"] = "no-cache, must-revalidate"
+ render action: "hello_world"
end
- def head_with_status_hash
- head status: :created
+ def conditional_hello_with_bangs
+ render action: "hello_world"
end
+ before_action :handle_last_modified_and_etags, only: :conditional_hello_with_bangs
- def head_with_hash_does_not_include_status
- head warning: :deprecated
+ def handle_last_modified_and_etags
+ fresh_when(last_modified: Time.now.utc.beginning_of_day, etag: [ :foo, 123 ])
end
def head_created
@@ -151,19 +197,19 @@ class TestController < ActionController::Base
end
def head_created_with_application_json_content_type
- head :created, :content_type => "application/json"
+ head :created, content_type: "application/json"
end
def head_ok_with_image_png_content_type
- head :ok, :content_type => "image/png"
+ head :ok, content_type: "image/png"
end
def head_with_location_header
- head :ok, :location => "/foo"
+ head :ok, location: "/foo"
end
def head_with_location_object
- head :ok, :location => Customer.new("david", 1)
+ head :ok, location: Customer.new("david", 1)
end
def head_with_symbolic_status
@@ -179,20 +225,20 @@ class TestController < ActionController::Base
end
def head_with_custom_header
- head :ok, :x_custom_header => "something"
+ head :ok, x_custom_header: "something"
end
def head_with_www_authenticate_header
- head :ok, 'WWW-Authenticate' => 'something'
+ head :ok, "WWW-Authenticate" => "something"
end
def head_with_status_code_first
- head :forbidden, :x_custom_header => "something"
+ head :forbidden, x_custom_header: "something"
end
def head_and_return
- head :ok and return
- raise 'should not reach this line'
+ head(:ok) && return
+ raise "should not reach this line"
end
def head_with_no_content
@@ -204,6 +250,15 @@ class TestController < ActionController::Base
head 204
end
+ def head_default_content_type
+ # simulating path like "/1.foobar"
+ request.formats = []
+
+ respond_to do |format|
+ format.any { head 200 }
+ end
+ end
+
private
def set_variable_for_layout
@@ -212,7 +267,7 @@ class TestController < ActionController::Base
def determine_layout
case action_name
- when "hello_world", "layout_test", "rendering_without_layout",
+ when "hello_world", "layout_test", "rendering_without_layout",
"rendering_nothing_on_layout", "render_text_hello_world",
"render_text_hello_world_with_layout",
"hello_world_with_layout_false",
@@ -222,30 +277,86 @@ class TestController < ActionController::Base
"render_with_explicit_string_template",
"update_page", "update_page_with_instance_variables"
- "layouts/standard"
- when "action_talk_to_layout", "layout_overriding_layout"
- "layouts/talk_from_action"
- when "render_implicit_html_template_from_xhr_request"
- (request.xhr? ? 'layouts/xhr' : 'layouts/standard')
+ "layouts/standard"
+ when "action_talk_to_layout", "layout_overriding_layout"
+ "layouts/talk_from_action"
+ when "render_implicit_html_template_from_xhr_request"
+ (request.xhr? ? "layouts/xhr" : "layouts/standard")
end
end
end
+module TemplateModificationHelper
+ private
+ def modify_template(name)
+ path = File.expand_path("../fixtures/#{name}.erb", __dir__)
+ original = File.read(path)
+ File.write(path, "#{original} Modified!")
+ ActionView::LookupContext::DetailsKey.clear
+ yield
+ ensure
+ File.write(path, original)
+ end
+end
+
class MetalTestController < ActionController::Metal
include AbstractController::Rendering
include ActionView::Rendering
include ActionController::Rendering
- include ActionController::RackDelegation
-
def accessing_logger_in_template
- render :inline => "<%= logger.class %>"
+ render inline: "<%= logger.class %>"
end
end
class ExpiresInRenderTest < ActionController::TestCase
tests TestController
+ def setup
+ super
+ ActionController::Base.view_paths.paths.each(&:clear_cache)
+ end
+
+ def test_dynamic_render_with_file
+ # This is extremely bad, but should be possible to do.
+ assert File.exist?(File.expand_path("../../test/abstract_unit.rb", __dir__))
+ response = get :dynamic_render_with_file, params: { id: '../\\../test/abstract_unit.rb' }
+ assert_equal File.read(File.expand_path("../../test/abstract_unit.rb", __dir__)),
+ response.body
+ end
+
+ def test_dynamic_render_with_absolute_path
+ file = Tempfile.new("name")
+ file.write "secrets!"
+ file.flush
+ assert_raises ActionView::MissingTemplate do
+ get :dynamic_render, params: { id: file.path }
+ end
+ ensure
+ file.close
+ file.unlink
+ end
+
+ def test_dynamic_render
+ assert File.exist?(File.expand_path("../../test/abstract_unit.rb", __dir__))
+ assert_raises ActionView::MissingTemplate do
+ get :dynamic_render, params: { id: '../\\../test/abstract_unit.rb' }
+ end
+ end
+
+ def test_permitted_dynamic_render_file_hash
+ assert File.exist?(File.expand_path("../../test/abstract_unit.rb", __dir__))
+ response = get :dynamic_render_permit, params: { id: { file: '../\\../test/abstract_unit.rb' } }
+ assert_equal File.read(File.expand_path("../../test/abstract_unit.rb", __dir__)),
+ response.body
+ end
+
+ def test_dynamic_render_file_hash
+ assert_raises ArgumentError do
+ get :dynamic_render, params: { id: { file: '../\\../test/abstract_unit.rb' } }
+ end
+ end
+
def test_expires_in_header
get :conditional_hello_with_expires_in
assert_equal "max-age=60, private", @response.headers["Cache-Control"]
@@ -266,6 +377,16 @@ class ExpiresInRenderTest < ActionController::TestCase
assert_equal "max-age=60, public, must-revalidate", @response.headers["Cache-Control"]
end
+ def test_expires_in_header_with_stale_while_revalidate
+ get :conditional_hello_with_expires_in_with_stale_while_revalidate
+ assert_equal "max-age=60, public, stale-while-revalidate=300", @response.headers["Cache-Control"]
+ end
+
+ def test_expires_in_header_with_stale_if_error
+ get :conditional_hello_with_expires_in_with_stale_if_error
+ assert_equal "max-age=60, public, stale-if-error=300", @response.headers["Cache-Control"]
+ end
+
def test_expires_in_header_with_additional_headers
get :conditional_hello_with_expires_in_with_public_with_more_keys
assert_equal "max-age=60, public, s-maxage=18000", @response.headers["Cache-Control"]
@@ -287,17 +408,22 @@ class ExpiresInRenderTest < ActionController::TestCase
assert_match(/no-transform/, @response.headers["Cache-Control"])
end
- def test_render_nothing_deprecated
- assert_deprecated do
- get :respond_with_empty_body
- end
+ def test_expires_now_with_conflicting_cache_control_headers
+ get :conditional_hello_with_expires_and_confliciting_cache_control_headers
+ assert_equal "no-cache", @response.headers["Cache-Control"]
+ end
+
+ def test_no_expires_now_with_conflicting_cache_control_headers
+ get :conditional_hello_without_expires_and_confliciting_cache_control_headers
+ assert_equal "no-cache", @response.headers["Cache-Control"]
end
def test_date_header_when_expires_in
- time = Time.mktime(2011,10,30)
- Time.stubs(:now).returns(time)
- get :conditional_hello_with_expires_in
- assert_equal Time.now.httpdate, @response.headers["Date"]
+ time = Time.mktime(2011, 10, 30)
+ Time.stub :now, time do
+ get :conditional_hello_with_expires_in
+ assert_equal Time.now.httpdate, @response.headers["Date"]
+ end
end
end
@@ -311,92 +437,92 @@ class LastModifiedRenderTest < ActionController::TestCase
def test_responds_with_last_modified
get :conditional_hello
- assert_equal @last_modified, @response.headers['Last-Modified']
+ assert_equal @last_modified, @response.headers["Last-Modified"]
end
def test_request_not_modified
@request.if_modified_since = @last_modified
get :conditional_hello
assert_equal 304, @response.status.to_i
- assert @response.body.blank?
- assert_equal @last_modified, @response.headers['Last-Modified']
+ assert_predicate @response.body, :blank?
+ assert_equal @last_modified, @response.headers["Last-Modified"]
end
def test_request_not_modified_but_etag_differs
@request.if_modified_since = @last_modified
- @request.if_none_match = "234"
+ @request.if_none_match = '"234"'
get :conditional_hello
assert_response :success
end
def test_request_modified
- @request.if_modified_since = 'Thu, 16 Jul 2008 00:00:00 GMT'
+ @request.if_modified_since = "Thu, 16 Jul 2008 00:00:00 GMT"
get :conditional_hello
assert_equal 200, @response.status.to_i
- assert @response.body.present?
- assert_equal @last_modified, @response.headers['Last-Modified']
+ assert_predicate @response.body, :present?
+ assert_equal @last_modified, @response.headers["Last-Modified"]
end
def test_responds_with_last_modified_with_record
get :conditional_hello_with_record
- assert_equal @last_modified, @response.headers['Last-Modified']
+ assert_equal @last_modified, @response.headers["Last-Modified"]
end
def test_request_not_modified_with_record
@request.if_modified_since = @last_modified
get :conditional_hello_with_record
assert_equal 304, @response.status.to_i
- assert @response.body.blank?
+ assert_predicate @response.body, :blank?
assert_not_nil @response.etag
- assert_equal @last_modified, @response.headers['Last-Modified']
+ assert_equal @last_modified, @response.headers["Last-Modified"]
end
def test_request_not_modified_but_etag_differs_with_record
@request.if_modified_since = @last_modified
- @request.if_none_match = "234"
+ @request.if_none_match = '"234"'
get :conditional_hello_with_record
assert_response :success
end
def test_request_modified_with_record
- @request.if_modified_since = 'Thu, 16 Jul 2008 00:00:00 GMT'
+ @request.if_modified_since = "Thu, 16 Jul 2008 00:00:00 GMT"
get :conditional_hello_with_record
assert_equal 200, @response.status.to_i
- assert @response.body.present?
- assert_equal @last_modified, @response.headers['Last-Modified']
+ assert_predicate @response.body, :present?
+ assert_equal @last_modified, @response.headers["Last-Modified"]
end
def test_responds_with_last_modified_with_collection_of_records
get :conditional_hello_with_collection_of_records
- assert_equal @last_modified, @response.headers['Last-Modified']
+ assert_equal @last_modified, @response.headers["Last-Modified"]
end
def test_request_not_modified_with_collection_of_records
@request.if_modified_since = @last_modified
get :conditional_hello_with_collection_of_records
assert_equal 304, @response.status.to_i
- assert @response.body.blank?
- assert_equal @last_modified, @response.headers['Last-Modified']
+ assert_predicate @response.body, :blank?
+ assert_equal @last_modified, @response.headers["Last-Modified"]
end
def test_request_not_modified_but_etag_differs_with_collection_of_records
@request.if_modified_since = @last_modified
- @request.if_none_match = "234"
+ @request.if_none_match = '"234"'
get :conditional_hello_with_collection_of_records
assert_response :success
end
def test_request_modified_with_collection_of_records
- @request.if_modified_since = 'Thu, 16 Jul 2008 00:00:00 GMT'
+ @request.if_modified_since = "Thu, 16 Jul 2008 00:00:00 GMT"
get :conditional_hello_with_collection_of_records
assert_equal 200, @response.status.to_i
- assert @response.body.present?
- assert_equal @last_modified, @response.headers['Last-Modified']
+ assert_predicate @response.body, :present?
+ assert_equal @last_modified, @response.headers["Last-Modified"]
end
def test_request_with_bang_gets_last_modified
get :conditional_hello_with_bangs
- assert_equal @last_modified, @response.headers['Last-Modified']
+ assert_equal @last_modified, @response.headers["Last-Modified"]
assert_response :success
end
@@ -415,9 +541,28 @@ end
class EtagRenderTest < ActionController::TestCase
tests TestControllerWithExtraEtags
+ include TemplateModificationHelper
+
+ def test_strong_etag
+ @request.if_none_match = strong_etag(["strong", "ab", :cde, [:f]])
+ get :strong
+ assert_response :not_modified
+
+ @request.if_none_match = "*"
+ get :strong
+ assert_response :not_modified
+
+ @request.if_none_match = '"strong"'
+ get :strong
+ assert_response :ok
+
+ @request.if_none_match = weak_etag(["strong", "ab", :cde, [:f]])
+ get :strong
+ assert_response :ok
+ end
def test_multiple_etags
- @request.if_none_match = etag(["123", 'ab', :cde, [:f]])
+ @request.if_none_match = weak_etag(["123", "ab", :cde, [:f]])
get :fresh
assert_response :not_modified
@@ -427,7 +572,7 @@ class EtagRenderTest < ActionController::TestCase
end
def test_array
- @request.if_none_match = etag([%w(1 2 3), 'ab', :cde, [:f]])
+ @request.if_none_match = weak_etag([%w(1 2 3), "ab", :cde, [:f]])
get :array
assert_response :not_modified
@@ -445,25 +590,60 @@ class EtagRenderTest < ActionController::TestCase
get :with_template
assert_response :not_modified
- # Modify the template digest
- path = File.expand_path('../../fixtures/test/hello_world.erb', __FILE__)
- old = File.read(path)
+ modify_template("test/hello_world") do
+ request.if_none_match = etag
+ get :with_template
+ assert_response :ok
+ assert_not_equal etag, @response.etag
+ end
+ end
- begin
- File.write path, 'foo'
- ActionView::Digestor.cache.clear
+ def test_etag_reflects_implicit_template_digest
+ get :with_implicit_template
+ assert_response :ok
+ assert_not_nil etag = @response.etag
+ request.if_none_match = etag
+ get :with_implicit_template
+ assert_response :not_modified
+
+ modify_template("test/with_implicit_template") do
request.if_none_match = etag
- get :with_template
+ get :with_implicit_template
assert_response :ok
assert_not_equal etag, @response.etag
- ensure
- File.write path, old
end
end
- def etag(record)
- Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(record)).inspect
+ private
+ def weak_etag(record)
+ "W/#{strong_etag record}"
+ end
+
+ def strong_etag(record)
+ %("#{ActiveSupport::Digest.hexdigest(ActiveSupport::Cache.expand_cache_key(record))}")
+ end
+end
+
+class NamespacedEtagRenderTest < ActionController::TestCase
+ tests Namespaced::ImplicitRenderTestController
+ include TemplateModificationHelper
+
+ def test_etag_reflects_template_digest
+ get :hello_world
+ assert_response :ok
+ assert_not_nil etag = @response.etag
+
+ request.if_none_match = etag
+ get :hello_world
+ assert_response :not_modified
+
+ modify_template("namespaced/implicit_render_test/hello_world") do
+ request.if_none_match = etag
+ get :hello_world
+ assert_response :ok
+ assert_not_equal etag, @response.etag
+ end
end
end
@@ -476,13 +656,50 @@ class MetalRenderTest < ActionController::TestCase
end
end
+class ActionControllerRenderTest < ActionController::TestCase
+ class MinimalController < ActionController::Metal
+ include AbstractController::Rendering
+ include ActionController::Rendering
+ end
+
+ def test_direct_render_to_string_with_body
+ mc = MinimalController.new
+ assert_equal "Hello world!", mc.render_to_string(body: ["Hello world!"])
+ end
+end
+
+class ActionControllerBaseRenderTest < ActionController::TestCase
+ def test_direct_render_to_string
+ ac = ActionController::Base.new()
+ assert_equal "Hello world!", ac.render_to_string(template: "test/hello_world")
+ end
+end
+
class ImplicitRenderTest < ActionController::TestCase
tests ImplicitRenderTestController
- def test_implicit_no_content_response
- get :empty_action
+ def test_implicit_no_content_response_as_browser
+ assert_raises(ActionController::MissingExactTemplate) do
+ get :empty_action
+ end
+ end
+
+ def test_implicit_no_content_response_as_xhr
+ get :empty_action, xhr: true
assert_response :no_content
end
+
+ def test_implicit_success_response_with_right_format
+ get :empty_action_with_template
+ assert_equal "<h1>Empty action rendered this implicitly.</h1>\n", @response.body
+ assert_response :success
+ end
+
+ def test_implicit_unknown_format_response
+ assert_raises(ActionController::UnknownFormat) do
+ get :empty_action_with_template, format: "json"
+ end
+ end
end
class HeadRenderTest < ActionController::TestCase
@@ -494,40 +711,27 @@ class HeadRenderTest < ActionController::TestCase
def test_head_created
post :head_created
- assert @response.body.blank?
+ assert_predicate @response.body, :blank?
assert_response :created
end
- def test_passing_hash_to_head_as_first_parameter_deprecated
- assert_deprecated do
- get :head_with_status_hash
- end
- end
-
- def test_head_with_default_value_is_deprecated
- assert_deprecated do
- get :head_with_hash_does_not_include_status
- assert_response :ok
- end
- end
-
def test_head_created_with_application_json_content_type
post :head_created_with_application_json_content_type
- assert @response.body.blank?
+ assert_predicate @response.body, :blank?
assert_equal "application/json", @response.header["Content-Type"]
assert_response :created
end
def test_head_ok_with_image_png_content_type
post :head_ok_with_image_png_content_type
- assert @response.body.blank?
+ assert_predicate @response.body, :blank?
assert_equal "image/png", @response.header["Content-Type"]
assert_response :ok
end
def test_head_with_location_header
get :head_with_location_header
- assert @response.body.blank?
+ assert_predicate @response.body, :blank?
assert_equal "/foo", @response.headers["Location"]
assert_response :ok
end
@@ -536,11 +740,14 @@ class HeadRenderTest < ActionController::TestCase
with_routing do |set|
set.draw do
resources :customers
- get ':controller/:action'
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action"
+ end
end
get :head_with_location_object
- assert @response.body.blank?
+ assert_predicate @response.body, :blank?
assert_equal "http://www.nextangle.com/customers/1", @response.headers["Location"]
assert_response :ok
end
@@ -548,14 +755,14 @@ class HeadRenderTest < ActionController::TestCase
def test_head_with_custom_header
get :head_with_custom_header
- assert @response.body.blank?
+ assert_predicate @response.body, :blank?
assert_equal "something", @response.headers["X-Custom-Header"]
assert_response :ok
end
def test_head_with_www_authenticate_header
get :head_with_www_authenticate_header
- assert @response.body.blank?
+ assert_predicate @response.body, :blank?
assert_equal "something", @response.headers["WWW-Authenticate"]
assert_response :ok
end
@@ -571,7 +778,7 @@ class HeadRenderTest < ActionController::TestCase
get :head_with_symbolic_status, params: { status: "no_content" }
assert_equal 204, @response.status
- assert !@response.headers.include?('Content-Length')
+ assert_not_includes @response.headers, "Content-Length"
assert_response :no_content
Rack::Utils::SYMBOL_TO_STATUS_CODE.each do |status, code|
@@ -616,13 +823,18 @@ class HeadRenderTest < ActionController::TestCase
get :head_and_return
end
end
+
+ def test_head_default_content_type
+ post :head_default_content_type
+ assert_equal "text/html", @response.header["Content-Type"]
+ end
end
class HttpCacheForeverTest < ActionController::TestCase
class HttpCacheForeverController < ActionController::Base
def cache_me_forever
- http_cache_forever(public: params[:public], version: params[:version] || 'v1') do
- render text: 'hello'
+ http_cache_forever(public: params[:public]) do
+ render plain: "hello"
end
end
end
@@ -630,43 +842,36 @@ class HttpCacheForeverTest < ActionController::TestCase
tests HttpCacheForeverController
def test_cache_with_public
- get :cache_me_forever, params: {public: true}
- assert_equal "max-age=#{100.years.to_i}, public", @response.headers["Cache-Control"]
+ get :cache_me_forever, params: { public: true }
+ assert_response :ok
+ assert_equal "max-age=#{100.years}, public", @response.headers["Cache-Control"]
assert_not_nil @response.etag
+ assert_predicate @response, :weak_etag?
end
def test_cache_with_private
get :cache_me_forever
- assert_equal "max-age=#{100.years.to_i}, private", @response.headers["Cache-Control"]
+ assert_response :ok
+ assert_equal "max-age=#{100.years}, private", @response.headers["Cache-Control"]
assert_not_nil @response.etag
- assert_response :success
+ assert_predicate @response, :weak_etag?
end
def test_cache_response_code_with_if_modified_since
get :cache_me_forever
- assert_response :success
- @request.if_modified_since = @response.headers['Last-Modified']
+ assert_response :ok
+
+ @request.if_modified_since = @response.headers["Last-Modified"]
get :cache_me_forever
assert_response :not_modified
end
def test_cache_response_code_with_etag
get :cache_me_forever
- assert_response :success
- @request.if_modified_since = @response.headers['Last-Modified']
- @request.if_none_match = @response.etag
-
- get :cache_me_forever
- assert_response :not_modified
- @request.if_modified_since = @response.headers['Last-Modified']
- @request.if_none_match = @response.etag
+ assert_response :ok
- get :cache_me_forever, params: {version: 'v2'}
- assert_response :success
- @request.if_modified_since = @response.headers['Last-Modified']
@request.if_none_match = @response.etag
-
- get :cache_me_forever, params: {version: 'v2'}
+ get :cache_me_forever
assert_response :not_modified
end
end
diff --git a/actionpack/test/controller/render_xml_test.rb b/actionpack/test/controller/render_xml_test.rb
index 7a91577b17..a72d14e4bb 100644
--- a/actionpack/test/controller/render_xml_test.rb
+++ b/actionpack/test/controller/render_xml_test.rb
@@ -1,6 +1,8 @@
-require 'abstract_unit'
-require 'controller/fake_models'
-require 'pathname'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "controller/fake_models"
+require "pathname"
class RenderXmlTest < ActionController::TestCase
class XmlRenderable
@@ -14,31 +16,31 @@ class RenderXmlTest < ActionController::TestCase
protect_from_forgery
def self.controller_path
- 'test'
+ "test"
end
def render_with_location
- render :xml => "<hello/>", :location => "http://example.com", :status => 201
+ render xml: "<hello/>", location: "http://example.com", status: 201
end
def render_with_object_location
customer = Customer.new("Some guy", 1)
- render :xml => "<customer/>", :location => customer, :status => :created
+ render xml: "<customer/>", location: customer, status: :created
end
def render_with_to_xml
- render :xml => XmlRenderable.new
+ render xml: XmlRenderable.new
end
def formatted_xml_erb
end
def render_xml_with_custom_content_type
- render :xml => "<blah/>", :content_type => "application/atomsvc+xml"
+ render xml: "<blah/>", content_type: "application/atomsvc+xml"
end
def render_xml_with_custom_options
- render :xml => XmlRenderable.new, :root => "i-am-THE-xml"
+ render xml: XmlRenderable.new, root: "i-am-THE-xml"
end
end
@@ -72,7 +74,10 @@ class RenderXmlTest < ActionController::TestCase
with_routing do |set|
set.draw do
resources :customers
- get ':controller/:action'
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action"
+ end
end
get :render_with_object_location
@@ -82,7 +87,7 @@ class RenderXmlTest < ActionController::TestCase
def test_should_render_formatted_xml_erb_template
get :formatted_xml_erb, format: :xml
- assert_equal '<test>passed formatted xml erb</test>', @response.body
+ assert_equal "<test>passed formatted xml erb</test>", @response.body
end
def test_should_render_xml_but_keep_custom_content_type
@@ -91,7 +96,7 @@ class RenderXmlTest < ActionController::TestCase
end
def test_should_use_implicit_content_type
- get :implicit_content_type, format: 'atom'
- assert_equal Mime::ATOM, @response.content_type
+ get :implicit_content_type, format: "atom"
+ assert_equal Mime[:atom], @response.content_type
end
end
diff --git a/actionpack/test/controller/renderer_test.rb b/actionpack/test/controller/renderer_test.rb
index b55a25430b..ae8330e029 100644
--- a/actionpack/test/controller/renderer_test.rb
+++ b/actionpack/test/controller/renderer_test.rb
@@ -1,99 +1,132 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class RendererTest < ActiveSupport::TestCase
- test 'creating with a controller' do
+ test "action controller base has a renderer" do
+ assert ActionController::Base.renderer
+ end
+
+ test "creating with a controller" do
controller = CommentsController
renderer = ActionController::Renderer.for controller
assert_equal controller, renderer.controller
end
- test 'creating from a controller' do
+ test "creating from a controller" do
controller = AccountsController
renderer = controller.renderer
assert_equal controller, renderer.controller
end
- test 'rendering with a class renderer' do
+ test "creating with new defaults" do
renderer = ApplicationController.renderer
- content = renderer.render template: 'ruby_template'
- assert_equal 'Hello from Ruby code', content
+ 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 an instance renderer' do
+ test "rendering with a class renderer" do
+ renderer = ApplicationController.renderer
+ content = renderer.render template: "ruby_template"
+
+ assert_equal "Hello from Ruby code", content
+ end
+
+ test "rendering with an instance renderer" do
renderer = ApplicationController.renderer.new
- content = renderer.render file: 'test/hello_world'
+ content = renderer.render file: "test/hello_world"
- assert_equal 'Hello world!', content
+ assert_equal "Hello world!", content
end
- test 'rendering with a controller class' do
- assert_equal 'Hello world!', ApplicationController.render('test/hello_world')
+ test "rendering with a controller class" do
+ assert_equal "Hello world!", ApplicationController.render("test/hello_world")
end
- test 'rendering with locals' do
+ test "rendering with locals" do
renderer = ApplicationController.renderer
- content = renderer.render template: 'test/render_file_with_locals',
- locals: { secret: 'bar' }
+ content = renderer.render template: "test/render_file_with_locals",
+ locals: { secret: "bar" }
assert_equal "The secret is bar\n", content
end
- test 'rendering with assigns' do
+ test "rendering with assigns" do
renderer = ApplicationController.renderer
- content = renderer.render template: 'test/render_file_with_ivar',
- assigns: { secret: 'foo' }
+ content = renderer.render template: "test/render_file_with_ivar",
+ assigns: { secret: "foo" }
assert_equal "The secret is foo\n", content
end
- test 'rendering with custom env' do
- renderer = ApplicationController.renderer.new method: 'post'
- content = renderer.render inline: '<%= request.post? %>'
+ test "rendering with custom env" do
+ renderer = ApplicationController.renderer.new method: "post"
+ content = renderer.render inline: "<%= request.post? %>"
- assert_equal 'true', content
+ assert_equal "true", content
end
- test 'rendering with defaults' do
- renderer = ApplicationController.renderer
- renderer.defaults[:https] = true
- content = renderer.render inline: '<%= request.ssl? %>'
+ test "rendering with custom env using a key that is not in RACK_KEY_TRANSLATION" do
+ value = "warden is here"
+ renderer = ApplicationController.renderer.new warden: value
+ content = renderer.render inline: "<%= request.env['warden'] %>"
+
+ assert_equal value, content
+ end
- assert_equal 'true', content
+ test "rendering with defaults" do
+ renderer = ApplicationController.renderer.new https: true
+ content = renderer.render inline: "<%= request.ssl? %>"
+
+ assert_equal "true", content
end
- test 'same defaults from the same controller' do
+ test "same defaults from the same controller" do
renderer_defaults = ->(controller) { controller.renderer.defaults }
- assert renderer_defaults[AccountsController].equal? renderer_defaults[AccountsController]
- assert_not renderer_defaults[AccountsController].equal? renderer_defaults[CommentsController]
+ assert_equal renderer_defaults[AccountsController], renderer_defaults[AccountsController]
+ assert_equal renderer_defaults[AccountsController], renderer_defaults[CommentsController]
end
- test 'rendering with different formats' do
- html = 'Hello world!'
+ test "rendering with different formats" do
+ html = "Hello world!"
xml = "<p>Hello world!</p>\n"
- assert_equal html, render['respond_to/using_defaults']
- assert_equal xml, render['respond_to/using_defaults.xml.builder']
- assert_equal xml, render['respond_to/using_defaults', formats: :xml]
+ assert_equal html, render["respond_to/using_defaults"]
+ assert_equal xml, render["respond_to/using_defaults.xml.builder"]
+ assert_equal xml, render["respond_to/using_defaults", formats: :xml]
end
- test 'rendering with helpers' do
+ test "rendering with helpers" do
assert_equal "<p>1\n<br />2</p>", render[inline: '<%= simple_format "1\n2" %>']
end
-
- test 'rendering from inherited renderer' do
- inherited = Class.new ApplicationController.renderer do
- defaults[:script_name] = 'script'
- def render(options)
- super options.merge(locals: { param: :value })
- end
- end
- template = '<%= url_for controller: :foo, action: :bar, param: param %>'
- assert_equal 'script/foo/bar?param=value', inherited.render(inline: template)
+ test "rendering with user specified defaults" do
+ ApplicationController.renderer.defaults.merge!(hello: "hello", https: true)
+ renderer = ApplicationController.renderer.new
+ content = renderer.render inline: "<%= request.ssl? %>"
+
+ 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
diff --git a/actionpack/test/controller/renderers_test.rb b/actionpack/test/controller/renderers_test.rb
new file mode 100644
index 0000000000..d92de6f5d5
--- /dev/null
+++ b/actionpack/test/controller/renderers_test.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "controller/fake_models"
+require "active_support/logger"
+
+class RenderersTest < ActionController::TestCase
+ class XmlRenderable
+ def to_xml(options)
+ options[:root] ||= "i-am-xml"
+ "<#{options[:root]}/>"
+ end
+ end
+ class JsonRenderable
+ def as_json(options = {})
+ hash = { a: :b, c: :d, e: :f }
+ hash.except!(*options[:except]) if options[:except]
+ hash
+ end
+
+ def to_json(options = {})
+ super except: [:c, :e]
+ end
+ end
+ class CsvRenderable
+ def to_csv
+ "c,s,v"
+ end
+ end
+ class TestController < ActionController::Base
+ def render_simon_says
+ render simon: "foo"
+ end
+
+ def respond_to_mime
+ respond_to do |type|
+ type.json do
+ render json: JsonRenderable.new
+ end
+ type.js { render json: "JS", callback: "alert" }
+ type.csv { render csv: CsvRenderable.new }
+ type.xml { render xml: XmlRenderable.new }
+ type.html { render body: "HTML" }
+ type.rss { render body: "RSS" }
+ type.all { render body: "Nothing" }
+ type.any(:js, :xml) { render body: "Either JS or XML" }
+ end
+ end
+ end
+
+ tests TestController
+
+ def setup
+ # enable a logger so that (e.g.) the benchmarking stuff runs, so we can get
+ # a more accurate simulation of what happens in "real life".
+ super
+ @controller.logger = ActiveSupport::Logger.new(nil)
+ end
+
+ def test_using_custom_render_option
+ ActionController.add_renderer :simon do |says, options|
+ self.content_type = Mime[:text]
+ self.response_body = "Simon says: #{says}"
+ end
+
+ get :render_simon_says
+ assert_equal "Simon says: foo", @response.body
+ ensure
+ ActionController.remove_renderer :simon
+ end
+
+ def test_raises_missing_template_no_renderer
+ assert_raise ActionView::MissingTemplate do
+ get :respond_to_mime, format: "csv"
+ end
+ assert_equal Mime[:csv], @response.content_type
+ assert_equal "", @response.body
+ end
+
+ def test_adding_csv_rendering_via_renderers_add
+ ActionController::Renderers.add :csv do |value, options|
+ send_data value.to_csv, type: Mime[:csv]
+ end
+ @request.accept = "text/csv"
+ get :respond_to_mime, format: "csv"
+ assert_equal Mime[:csv], @response.content_type
+ assert_equal "c,s,v", @response.body
+ ensure
+ ActionController::Renderers.remove :csv
+ end
+end
diff --git a/actionpack/test/controller/request/test_request_test.rb b/actionpack/test/controller/request/test_request_test.rb
index e5d698d5c2..b8d86696de 100644
--- a/actionpack/test/controller/request/test_request_test.rb
+++ b/actionpack/test/controller/request/test_request_test.rb
@@ -1,23 +1,42 @@
-require 'abstract_unit'
-require 'stringio'
+# frozen_string_literal: true
-class ActionController::TestRequestTest < ActionController::TestCase
+require "abstract_unit"
+require "stringio"
+class ActionController::TestRequestTest < ActionController::TestCase
def test_test_request_has_session_options_initialized
assert @request.session_options
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")
+ def test_mutating_session_options_does_not_affect_default_options
+ @request.session_options[:myparam] = 123
+ assert_nil ActionController::TestSession::DEFAULT_OPTIONS[:myparam]
+ end
+
+ def test_content_length_has_bytes_count_value
+ non_ascii_parameters = { data: { content: "Latin + Кириллица" } }
+ @request.set_header "REQUEST_METHOD", "POST"
+ @request.set_header "CONTENT_TYPE", "application/json"
+ @request.assign_parameters(@routes, "test", "create", non_ascii_parameters,
+ "/test", [:data, :controller, :action])
+ assert_equal(StringIO.new(non_ascii_parameters.to_json).length.to_s,
+ @request.get_header("CONTENT_LENGTH"))
+ end
+
+ 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 7c17aed632..ea94a3e048 100644
--- a/actionpack/test/controller/request_forgery_protection_test.rb
+++ b/actionpack/test/controller/request_forgery_protection_test.rb
@@ -1,43 +1,61 @@
-require 'abstract_unit'
-require 'digest/sha1'
+# frozen_string_literal: true
+
+require "abstract_unit"
require "active_support/log_subscriber/test_helper"
+require "active_support/messages/rotation_configuration"
# common controller actions
module RequestForgeryProtectionActions
def index
- render :inline => "<%= form_tag('/') {} %>"
+ render inline: "<%= form_tag('/') {} %>"
end
def show_button
- render :inline => "<%= button_to('New', '/') %>"
+ render inline: "<%= button_to('New', '/') %>"
end
def unsafe
- render :text => 'pwn'
+ render plain: "pwn"
end
def meta
- render :inline => "<%= csrf_meta_tags %>"
+ render inline: "<%= csrf_meta_tags %>"
end
def form_for_remote
- render :inline => "<%= form_for(:some_resource, :remote => true ) {} %>"
+ render inline: "<%= form_for(:some_resource, :remote => true ) {} %>"
end
def form_for_remote_with_token
- render :inline => "<%= form_for(:some_resource, :remote => true, :authenticity_token => true ) {} %>"
+ render inline: "<%= form_for(:some_resource, :remote => true, :authenticity_token => true ) {} %>"
end
def form_for_with_token
- render :inline => "<%= form_for(:some_resource, :authenticity_token => true ) {} %>"
+ render inline: "<%= form_for(:some_resource, :authenticity_token => true ) {} %>"
end
def form_for_remote_with_external_token
- render :inline => "<%= form_for(:some_resource, :remote => true, :authenticity_token => 'external_token') {} %>"
+ render inline: "<%= form_for(:some_resource, :remote => true, :authenticity_token => 'external_token') {} %>"
+ end
+
+ def form_with_remote
+ render inline: "<%= form_with(scope: :some_resource) {} %>"
+ end
+
+ def form_with_remote_with_token
+ render inline: "<%= form_with(scope: :some_resource, authenticity_token: true) {} %>"
+ end
+
+ def form_with_local_with_token
+ render inline: "<%= form_with(scope: :some_resource, local: true, authenticity_token: true) {} %>"
+ end
+
+ def form_with_remote_with_external_token
+ render inline: "<%= form_with(scope: :some_resource, authenticity_token: 'external_token') {} %>"
end
def same_origin_js
- render js: 'foo();'
+ render js: "foo();"
end
def negotiate_same_origin
@@ -53,30 +71,29 @@ module RequestForgeryProtectionActions
def negotiate_cross_origin
negotiate_same_origin
end
-
end
# sample controllers
class RequestForgeryProtectionControllerUsingResetSession < ActionController::Base
include RequestForgeryProtectionActions
- protect_from_forgery :only => %w(index meta same_origin_js negotiate_same_origin), :with => :reset_session
+ protect_from_forgery only: %w(index meta same_origin_js negotiate_same_origin), with: :reset_session
end
class RequestForgeryProtectionControllerUsingException < ActionController::Base
include RequestForgeryProtectionActions
- protect_from_forgery :only => %w(index meta same_origin_js negotiate_same_origin), :with => :exception
+ protect_from_forgery only: %w(index meta same_origin_js negotiate_same_origin), with: :exception
end
class RequestForgeryProtectionControllerUsingNullSession < ActionController::Base
- protect_from_forgery :with => :null_session
+ protect_from_forgery with: :null_session
def signed
- cookies.signed[:foo] = 'bar'
+ cookies.signed[:foo] = "bar"
head :ok
end
def encrypted
- cookies.encrypted[:foo] = 'bar'
+ cookies.encrypted[:foo] = "bar"
head :ok
end
@@ -91,51 +108,75 @@ class PrependProtectForgeryBaseController < ActionController::Base
attr_accessor :called_callbacks
def index
- render inline: 'OK'
+ render inline: "OK"
end
- protected
-
- def add_called_callback(name)
- @called_callbacks ||= []
- @called_callbacks << name
- end
+ private
+ def add_called_callback(name)
+ @called_callbacks ||= []
+ @called_callbacks << name
+ end
- def custom_action
- add_called_callback("custom_action")
- end
+ def custom_action
+ add_called_callback("custom_action")
+ end
- def verify_authenticity_token
- add_called_callback("verify_authenticity_token")
- end
+ def verify_authenticity_token
+ add_called_callback("verify_authenticity_token")
+ end
end
class FreeCookieController < RequestForgeryProtectionControllerUsingResetSession
self.allow_forgery_protection = false
def index
- render :inline => "<%= form_tag('/') {} %>"
+ render inline: "<%= form_tag('/') {} %>"
end
def show_button
- render :inline => "<%= button_to('New', '/') %>"
+ render inline: "<%= button_to('New', '/') %>"
end
end
class CustomAuthenticityParamController < RequestForgeryProtectionControllerUsingResetSession
def form_authenticity_param
- 'foobar'
+ "foobar"
end
end
+class PerFormTokensController < ActionController::Base
+ protect_from_forgery with: :exception
+ self.per_form_csrf_tokens = true
+
+ def index
+ render inline: "<%= form_tag (params[:form_path] || '/per_form_tokens/post_one'), method: params[:form_method] %>"
+ end
+
+ def button_to
+ render inline: "<%= button_to 'Button', (params[:form_path] || '/per_form_tokens/post_one'), method: params[:form_method] %>"
+ end
+
+ def post_one
+ render plain: ""
+ end
+
+ def post_two
+ render plain: ""
+ end
+end
+
+class SkipProtectionController < ActionController::Base
+ include RequestForgeryProtectionActions
+ protect_from_forgery with: :exception
+ skip_forgery_protection if: :skip_requested
+ attr_accessor :skip_requested
+end
+
# common test methods
module RequestForgeryProtectionTests
def setup
- @token = "cf50faa3fe97702ca1ae"
- @controller.stubs(:form_authenticity_token).returns(@token)
- @controller.stubs(:valid_authenticity_token?).with{ |_, t| t == @token }.returns(true)
- @controller.stubs(:valid_authenticity_token?).with{ |_, t| t != @token }.returns(false)
+ @token = Base64.strict_encode64("railstestrailstestrailstestrails")
@old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token
ActionController::Base.request_forgery_protection_token = :custom_authenticity_token
end
@@ -145,17 +186,21 @@ module RequestForgeryProtectionTests
end
def test_should_render_form_with_token_tag
- assert_not_blocked do
- get :index
+ @controller.stub :form_authenticity_token, @token do
+ assert_not_blocked do
+ get :index
+ end
+ assert_select "form>input[name=?][value=?]", "custom_authenticity_token", @token
end
- assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token
end
def test_should_render_button_to_with_token_tag
- assert_not_blocked do
- get :show_button
+ @controller.stub :form_authenticity_token, @token do
+ assert_not_blocked do
+ get :show_button
+ end
+ assert_select "form>input[name=?][value=?]", "custom_authenticity_token", @token
end
- assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token
end
def test_should_render_form_without_token_tag_if_remote
@@ -185,7 +230,7 @@ module RequestForgeryProtectionTests
assert_not_blocked do
get :form_for_remote_with_external_token
end
- assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', 'external_token'
+ assert_select "form>input[name=?][value=?]", "custom_authenticity_token", "external_token"
ensure
ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = original
end
@@ -195,21 +240,99 @@ module RequestForgeryProtectionTests
assert_not_blocked do
get :form_for_remote_with_external_token
end
- assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', 'external_token'
+ assert_select "form>input[name=?][value=?]", "custom_authenticity_token", "external_token"
end
def test_should_render_form_with_token_tag_if_remote_and_authenticity_token_requested
- assert_not_blocked do
- get :form_for_remote_with_token
+ @controller.stub :form_authenticity_token, @token do
+ assert_not_blocked do
+ get :form_for_remote_with_token
+ end
+ assert_select "form>input[name=?][value=?]", "custom_authenticity_token", @token
end
- assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token
end
def test_should_render_form_with_token_tag_with_authenticity_token_requested
+ @controller.stub :form_authenticity_token, @token do
+ assert_not_blocked do
+ get :form_for_with_token
+ end
+ assert_select "form>input[name=?][value=?]", "custom_authenticity_token", @token
+ end
+ end
+
+ def test_should_render_form_with_with_token_tag_if_remote
+ assert_not_blocked do
+ get :form_with_remote
+ end
+ assert_match(/authenticity_token/, response.body)
+ end
+
+ def test_should_render_form_with_without_token_tag_if_remote_and_embedding_token_is_off
+ original = ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms
+ begin
+ ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = false
+ assert_not_blocked do
+ get :form_with_remote
+ end
+ assert_no_match(/authenticity_token/, response.body)
+ ensure
+ ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = original
+ end
+ end
+
+ def test_should_render_form_with_with_token_tag_if_remote_and_external_authenticity_token_requested_and_embedding_is_on
+ original = ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms
+ begin
+ ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = true
+ assert_not_blocked do
+ get :form_with_remote_with_external_token
+ end
+ assert_select "form>input[name=?][value=?]", "custom_authenticity_token", "external_token"
+ ensure
+ ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = original
+ end
+ end
+
+ def test_should_render_form_with_with_token_tag_if_remote_and_external_authenticity_token_requested
assert_not_blocked do
- get :form_for_with_token
+ get :form_with_remote_with_external_token
+ end
+ assert_select "form>input[name=?][value=?]", "custom_authenticity_token", "external_token"
+ end
+
+ def test_should_render_form_with_with_token_tag_if_remote_and_authenticity_token_requested
+ @controller.stub :form_authenticity_token, @token do
+ assert_not_blocked do
+ get :form_with_remote_with_token
+ end
+ assert_select "form>input[name=?][value=?]", "custom_authenticity_token", @token
+ end
+ end
+
+ def test_should_render_form_with_with_token_tag_with_authenticity_token_requested
+ @controller.stub :form_authenticity_token, @token do
+ assert_not_blocked do
+ get :form_with_local_with_token
+ end
+ assert_select "form>input[name=?][value=?]", "custom_authenticity_token", @token
+ end
+ end
+
+ def test_should_render_form_with_with_token_tag_if_remote_and_embedding_token_is_on
+ original = ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms
+ begin
+ ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = true
+
+ @controller.stub :form_authenticity_token, @token do
+ assert_not_blocked do
+ get :form_with_remote
+ end
+ end
+ assert_select "form>input[name=?][value=?]", "custom_authenticity_token", @token
+ ensure
+ ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = original
end
- assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token
end
def test_should_allow_get
@@ -229,7 +352,7 @@ module RequestForgeryProtectionTests
end
def test_should_not_allow_post_without_token_irrespective_of_format
- assert_blocked { post :index, format: 'xml' }
+ assert_blocked { post :index, format: "xml" }
end
def test_should_not_allow_patch_without_token
@@ -249,41 +372,116 @@ module RequestForgeryProtectionTests
end
def test_should_allow_post_with_token
- assert_not_blocked { post :index, params: { custom_authenticity_token: @token } }
+ session[:_csrf_token] = @token
+ @controller.stub :form_authenticity_token, @token do
+ assert_not_blocked { post :index, params: { custom_authenticity_token: @token } }
+ end
end
def test_should_allow_patch_with_token
- assert_not_blocked { patch :index, params: { custom_authenticity_token: @token } }
+ session[:_csrf_token] = @token
+ @controller.stub :form_authenticity_token, @token do
+ assert_not_blocked { patch :index, params: { custom_authenticity_token: @token } }
+ end
end
def test_should_allow_put_with_token
- assert_not_blocked { put :index, params: { custom_authenticity_token: @token } }
+ session[:_csrf_token] = @token
+ @controller.stub :form_authenticity_token, @token do
+ assert_not_blocked { put :index, params: { custom_authenticity_token: @token } }
+ end
end
def test_should_allow_delete_with_token
- assert_not_blocked { delete :index, params: { custom_authenticity_token: @token } }
+ session[:_csrf_token] = @token
+ @controller.stub :form_authenticity_token, @token do
+ assert_not_blocked { delete :index, params: { custom_authenticity_token: @token } }
+ end
end
def test_should_allow_post_with_token_in_header
- @request.env['HTTP_X_CSRF_TOKEN'] = @token
+ session[:_csrf_token] = @token
+ @request.env["HTTP_X_CSRF_TOKEN"] = @token
assert_not_blocked { post :index }
end
def test_should_allow_delete_with_token_in_header
- @request.env['HTTP_X_CSRF_TOKEN'] = @token
+ session[:_csrf_token] = @token
+ @request.env["HTTP_X_CSRF_TOKEN"] = @token
assert_not_blocked { delete :index }
end
def test_should_allow_patch_with_token_in_header
- @request.env['HTTP_X_CSRF_TOKEN'] = @token
+ session[:_csrf_token] = @token
+ @request.env["HTTP_X_CSRF_TOKEN"] = @token
assert_not_blocked { patch :index }
end
def test_should_allow_put_with_token_in_header
- @request.env['HTTP_X_CSRF_TOKEN'] = @token
+ session[:_csrf_token] = @token
+ @request.env["HTTP_X_CSRF_TOKEN"] = @token
assert_not_blocked { put :index }
end
+ def test_should_allow_post_with_origin_checking_and_correct_origin
+ forgery_protection_origin_check do
+ session[:_csrf_token] = @token
+ @controller.stub :form_authenticity_token, @token do
+ assert_not_blocked do
+ @request.set_header "HTTP_ORIGIN", "http://test.host"
+ post :index, params: { custom_authenticity_token: @token }
+ end
+ end
+ end
+ end
+
+ def test_should_allow_post_with_origin_checking_and_no_origin
+ forgery_protection_origin_check do
+ session[:_csrf_token] = @token
+ @controller.stub :form_authenticity_token, @token do
+ assert_not_blocked do
+ post :index, params: { custom_authenticity_token: @token }
+ end
+ end
+ end
+ end
+
+ def test_should_raise_for_post_with_null_origin
+ forgery_protection_origin_check do
+ session[:_csrf_token] = @token
+ @controller.stub :form_authenticity_token, @token do
+ exception = assert_raises(ActionController::InvalidAuthenticityToken) do
+ @request.set_header "HTTP_ORIGIN", "null"
+ post :index, params: { custom_authenticity_token: @token }
+ end
+ assert_match "The browser returned a 'null' origin for a request", exception.message
+ end
+ end
+ end
+
+ def test_should_block_post_with_origin_checking_and_wrong_origin
+ old_logger = ActionController::Base.logger
+ logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
+ ActionController::Base.logger = logger
+
+ forgery_protection_origin_check do
+ session[:_csrf_token] = @token
+ @controller.stub :form_authenticity_token, @token do
+ assert_blocked do
+ @request.set_header "HTTP_ORIGIN", "http://bad.host"
+ post :index, params: { custom_authenticity_token: @token }
+ end
+ end
+ end
+
+ assert_match(
+ "HTTP Origin header (http://bad.host) didn't match request.base_url (http://test.host)",
+ logger.logged(:warn).last
+ )
+ ensure
+ ActionController::Base.logger = old_logger
+ end
+
def test_should_warn_on_missing_csrf_token
old_logger = ActionController::Base.logger
logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
@@ -317,50 +515,86 @@ module RequestForgeryProtectionTests
def test_should_only_allow_same_origin_js_get_with_xhr_header
assert_cross_origin_blocked { get :same_origin_js }
- assert_cross_origin_blocked { get :same_origin_js, format: 'js' }
+ assert_cross_origin_blocked { get :same_origin_js, format: "js" }
+ assert_cross_origin_blocked do
+ @request.accept = "text/javascript"
+ get :negotiate_same_origin
+ end
+
assert_cross_origin_blocked do
- @request.accept = 'text/javascript'
+ @request.accept = "application/javascript"
get :negotiate_same_origin
end
assert_cross_origin_not_blocked { get :same_origin_js, xhr: true }
- assert_cross_origin_not_blocked { get :same_origin_js, xhr: true, format: 'js'}
+ assert_cross_origin_not_blocked { get :same_origin_js, xhr: true, format: "js" }
assert_cross_origin_not_blocked do
- @request.accept = 'text/javascript'
+ @request.accept = "text/javascript"
get :negotiate_same_origin, xhr: true
end
end
+ def test_should_warn_on_not_same_origin_js
+ old_logger = ActionController::Base.logger
+ logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
+ ActionController::Base.logger = logger
+
+ begin
+ assert_cross_origin_blocked { get :same_origin_js }
+
+ assert_equal 1, logger.logged(:warn).size
+ assert_match(/<script> tag on another site requested protected JavaScript/, logger.logged(:warn).last)
+ ensure
+ ActionController::Base.logger = old_logger
+ end
+ end
+
+ def test_should_not_warn_if_csrf_logging_disabled_and_not_same_origin_js
+ old_logger = ActionController::Base.logger
+ logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
+ ActionController::Base.logger = logger
+ ActionController::Base.log_warning_on_csrf_failure = false
+
+ begin
+ assert_cross_origin_blocked { get :same_origin_js }
+
+ assert_equal 0, logger.logged(:warn).size
+ ensure
+ ActionController::Base.logger = old_logger
+ ActionController::Base.log_warning_on_csrf_failure = true
+ end
+ end
+
# Allow non-GET requests since GET is all a remote <script> tag can muster.
def test_should_allow_non_get_js_without_xhr_header
+ session[:_csrf_token] = @token
assert_cross_origin_not_blocked { post :same_origin_js, params: { custom_authenticity_token: @token } }
- assert_cross_origin_not_blocked { post :same_origin_js, params: { format: 'js', custom_authenticity_token: @token } }
+ assert_cross_origin_not_blocked { post :same_origin_js, params: { format: "js", custom_authenticity_token: @token } }
assert_cross_origin_not_blocked do
- @request.accept = 'text/javascript'
- post :negotiate_same_origin, params: { custom_authenticity_token: @token}
+ @request.accept = "text/javascript"
+ post :negotiate_same_origin, params: { custom_authenticity_token: @token }
end
end
def test_should_only_allow_cross_origin_js_get_without_xhr_header_if_protection_disabled
assert_cross_origin_not_blocked { get :cross_origin_js }
- assert_cross_origin_not_blocked { get :cross_origin_js, format: 'js' }
+ assert_cross_origin_not_blocked { get :cross_origin_js, format: "js" }
assert_cross_origin_not_blocked do
- @request.accept = 'text/javascript'
+ @request.accept = "text/javascript"
get :negotiate_cross_origin
end
assert_cross_origin_not_blocked { get :cross_origin_js, xhr: true }
- assert_cross_origin_not_blocked { get :cross_origin_js, xhr: true, format: 'js' }
+ assert_cross_origin_not_blocked { get :cross_origin_js, xhr: true, format: "js" }
assert_cross_origin_not_blocked do
- @request.accept = 'text/javascript'
+ @request.accept = "text/javascript"
get :negotiate_cross_origin, xhr: true
end
end
def test_should_not_raise_error_if_token_is_not_a_string
- @controller.unstub(:valid_authenticity_token?)
assert_blocked do
- patch :index, params: { custom_authenticity_token: { foo: 'bar' } }
+ patch :index, params: { custom_authenticity_token: { foo: "bar" } }
end
end
@@ -385,6 +619,16 @@ module RequestForgeryProtectionTests
def assert_cross_origin_not_blocked
assert_not_blocked { yield }
end
+
+ def forgery_protection_origin_check
+ old_setting = ActionController::Base.forgery_protection_origin_check
+ ActionController::Base.forgery_protection_origin_check = true
+ begin
+ yield
+ ensure
+ ActionController::Base.forgery_protection_origin_check = old_setting
+ end
+ end
end
# OK let's get our test on
@@ -392,46 +636,40 @@ end
class RequestForgeryProtectionControllerUsingResetSessionTest < ActionController::TestCase
include RequestForgeryProtectionTests
- setup do
- @old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token
- ActionController::Base.request_forgery_protection_token = :custom_authenticity_token
- end
-
- teardown do
- ActionController::Base.request_forgery_protection_token = @old_request_forgery_protection_token
- end
-
- test 'should emit a csrf-param meta tag and a csrf-token meta tag' do
- @controller.stubs(:form_authenticity_token).returns(@token + '<=?')
- get :meta
- assert_select 'meta[name=?][content=?]', 'csrf-param', 'custom_authenticity_token'
- assert_select 'meta[name=?]', 'csrf-token'
- assert_match(/cf50faa3fe97702ca1ae&lt;=\?/, @response.body)
+ test "should emit a csrf-param meta tag and a csrf-token meta tag" do
+ @controller.stub :form_authenticity_token, @token + "<=?" do
+ get :meta
+ assert_select "meta[name=?][content=?]", "csrf-param", "custom_authenticity_token"
+ assert_select "meta[name=?]", "csrf-token"
+ regexp = "#{@token}&lt;=\?"
+ assert_match(/#{regexp}/, @response.body)
+ end
end
end
class RequestForgeryProtectionControllerUsingNullSessionTest < ActionController::TestCase
class NullSessionDummyKeyGenerator
- def generate_key(secret)
- '03312270731a2ed0d11ed091c2338a06'
+ def generate_key(secret, length = nil)
+ "03312270731a2ed0d11ed091c2338a06"
end
end
def setup
@request.env[ActionDispatch::Cookies::GENERATOR_KEY] = NullSessionDummyKeyGenerator.new
+ @request.env[ActionDispatch::Cookies::COOKIES_ROTATIONS] = ActiveSupport::Messages::RotationConfiguration.new
end
- test 'should allow to set signed cookies' do
+ test "should allow to set signed cookies" do
post :signed
assert_response :ok
end
- test 'should allow to set encrypted cookies' do
+ test "should allow to set encrypted cookies" do
post :encrypted
assert_response :ok
end
- test 'should allow reset_session' do
+ test "should allow reset_session" do
post :try_to_reset_session
assert_response :ok
end
@@ -473,10 +711,10 @@ class PrependProtectForgeryBaseControllerTest < ActionController::TestCase
assert_equal(expected_callback_order, @controller.called_callbacks)
end
- def test_verify_authenticity_token_is_prepended_by_default
+ def test_verify_authenticity_token_is_not_prepended_by_default
@controller = PrependDefaultController.new
get :index
- expected_callback_order = ["verify_authenticity_token", "custom_action"]
+ expected_callback_order = ["custom_action", "verify_authenticity_token"]
assert_equal(expected_callback_order, @controller.called_callbacks)
end
end
@@ -485,30 +723,36 @@ class FreeCookieControllerTest < ActionController::TestCase
def setup
@controller = FreeCookieController.new
@token = "cf50faa3fe97702ca1ae"
-
- SecureRandom.stubs(:base64).returns(@token)
super
end
def test_should_not_render_form_with_token_tag
- get :index
- assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token, false
+ SecureRandom.stub :base64, @token do
+ get :index
+ assert_select "form>div>input[name=?][value=?]", "authenticity_token", @token, false
+ end
end
def test_should_not_render_button_to_with_token_tag
- get :show_button
- assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token, false
+ SecureRandom.stub :base64, @token do
+ get :show_button
+ assert_select "form>div>input[name=?][value=?]", "authenticity_token", @token, false
+ end
end
def test_should_allow_all_methods_without_token
- [:post, :patch, :put, :delete].each do |method|
- assert_nothing_raised { send(method, :index)}
+ SecureRandom.stub :base64, @token do
+ [:post, :patch, :put, :delete].each do |method|
+ assert_nothing_raised { send(method, :index) }
+ end
end
end
- test 'should not emit a csrf-token meta tag' do
- get :meta
- assert @response.body.blank?
+ test "should not emit a csrf-token meta tag" do
+ SecureRandom.stub :base64, @token do
+ get :meta
+ assert_predicate @response.body, :blank?
+ end
end
end
@@ -529,11 +773,11 @@ class CustomAuthenticityParamControllerTest < ActionController::TestCase
def test_should_not_warn_if_form_authenticity_param_matches_form_authenticity_token
ActionController::Base.logger = @logger
- @controller.stubs(:valid_authenticity_token?).returns(:true)
-
begin
- post :index, params: { custom_token_name: 'foobar' }
- assert_equal 0, @logger.logged(:warn).size
+ @controller.stub :valid_authenticity_token?, :true do
+ post :index, params: { custom_token_name: "foobar" }
+ assert_equal 0, @logger.logged(:warn).size
+ end
ensure
ActionController::Base.logger = @old_logger
end
@@ -543,10 +787,232 @@ class CustomAuthenticityParamControllerTest < ActionController::TestCase
ActionController::Base.logger = @logger
begin
- post :index, params: { custom_token_name: 'bazqux' }
+ post :index, params: { custom_token_name: "bazqux" }
assert_equal 1, @logger.logged(:warn).size
ensure
ActionController::Base.logger = @old_logger
end
end
end
+
+class PerFormTokensControllerTest < ActionController::TestCase
+ def setup
+ @old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token
+ ActionController::Base.request_forgery_protection_token = :custom_authenticity_token
+ end
+
+ def teardown
+ ActionController::Base.request_forgery_protection_token = @old_request_forgery_protection_token
+ end
+
+ def test_per_form_token_is_same_size_as_global_token
+ get :index
+ expected = ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH
+ actual = @controller.send(:per_form_csrf_token, session, "/path", "post").size
+ assert_equal expected, actual
+ end
+
+ def test_accepts_token_for_correct_path_and_method
+ get :index
+
+ form_token = assert_presence_and_fetch_form_csrf_token
+
+ assert_matches_session_token_on_server form_token
+
+ # This is required because PATH_INFO isn't reset between requests.
+ @request.env["PATH_INFO"] = "/per_form_tokens/post_one"
+ assert_nothing_raised do
+ post :post_one, params: { custom_authenticity_token: form_token }
+ end
+ assert_response :success
+ end
+
+ def test_rejects_token_for_incorrect_path
+ get :index
+
+ form_token = assert_presence_and_fetch_form_csrf_token
+
+ assert_matches_session_token_on_server form_token
+
+ # This is required because PATH_INFO isn't reset between requests.
+ @request.env["PATH_INFO"] = "/per_form_tokens/post_two"
+ assert_raises(ActionController::InvalidAuthenticityToken) do
+ post :post_two, params: { custom_authenticity_token: form_token }
+ end
+ end
+
+ def test_rejects_token_for_incorrect_method
+ get :index
+
+ form_token = assert_presence_and_fetch_form_csrf_token
+
+ assert_matches_session_token_on_server form_token
+
+ # This is required because PATH_INFO isn't reset between requests.
+ @request.env["PATH_INFO"] = "/per_form_tokens/post_one"
+ assert_raises(ActionController::InvalidAuthenticityToken) do
+ patch :post_one, params: { custom_authenticity_token: form_token }
+ end
+ end
+
+ def test_rejects_token_for_incorrect_method_button_to
+ get :button_to, params: { form_method: "delete" }
+
+ form_token = assert_presence_and_fetch_form_csrf_token
+
+ assert_matches_session_token_on_server form_token, "delete"
+
+ # This is required because PATH_INFO isn't reset between requests.
+ @request.env["PATH_INFO"] = "/per_form_tokens/post_one"
+ assert_raises(ActionController::InvalidAuthenticityToken) do
+ patch :post_one, params: { custom_authenticity_token: form_token }
+ end
+ end
+
+ test "Accepts proper token for implicit post method on button_to tag" do
+ get :button_to
+
+ form_token = assert_presence_and_fetch_form_csrf_token
+
+ assert_matches_session_token_on_server form_token, "post"
+
+ # This is required because PATH_INFO isn't reset between requests.
+ @request.env["PATH_INFO"] = "/per_form_tokens/post_one"
+ assert_nothing_raised do
+ post :post_one, params: { custom_authenticity_token: form_token }
+ end
+ end
+
+ %w{delete post patch}.each do |verb|
+ test "Accepts proper token for #{verb} method on button_to tag" do
+ get :button_to, params: { form_method: verb }
+
+ form_token = assert_presence_and_fetch_form_csrf_token
+
+ assert_matches_session_token_on_server form_token, verb
+
+ # This is required because PATH_INFO isn't reset between requests.
+ @request.env["PATH_INFO"] = "/per_form_tokens/post_one"
+ assert_nothing_raised do
+ send verb, :post_one, params: { custom_authenticity_token: form_token }
+ end
+ end
+ end
+
+ def test_accepts_global_csrf_token
+ get :index
+
+ token = @controller.send(:form_authenticity_token)
+
+ # This is required because PATH_INFO isn't reset between requests.
+ @request.env["PATH_INFO"] = "/per_form_tokens/post_one"
+ assert_nothing_raised do
+ post :post_one, params: { custom_authenticity_token: token }
+ end
+ assert_response :success
+ end
+
+ def test_ignores_params
+ get :index, params: { form_path: "/per_form_tokens/post_one?foo=bar" }
+
+ form_token = assert_presence_and_fetch_form_csrf_token
+
+ assert_matches_session_token_on_server form_token
+
+ # This is required because PATH_INFO isn't reset between requests.
+ @request.env["PATH_INFO"] = "/per_form_tokens/post_one?foo=baz"
+ assert_nothing_raised do
+ post :post_one, params: { custom_authenticity_token: form_token, baz: "foo" }
+ end
+ assert_response :success
+ end
+
+ def test_ignores_trailing_slash_during_generation
+ get :index, params: { form_path: "/per_form_tokens/post_one/" }
+
+ form_token = assert_presence_and_fetch_form_csrf_token
+
+ # This is required because PATH_INFO isn't reset between requests.
+ @request.env["PATH_INFO"] = "/per_form_tokens/post_one"
+ assert_nothing_raised do
+ post :post_one, params: { custom_authenticity_token: form_token }
+ end
+ assert_response :success
+ end
+
+ def test_ignores_origin_during_generation
+ get :index, params: { form_path: "https://example.com/per_form_tokens/post_one/" }
+
+ form_token = assert_presence_and_fetch_form_csrf_token
+
+ # This is required because PATH_INFO isn't reset between requests.
+ @request.env["PATH_INFO"] = "/per_form_tokens/post_one"
+ assert_nothing_raised do
+ post :post_one, params: { custom_authenticity_token: form_token }
+ end
+ assert_response :success
+ end
+
+ def test_ignores_trailing_slash_during_validation
+ get :index
+
+ form_token = assert_presence_and_fetch_form_csrf_token
+
+ # This is required because PATH_INFO isn't reset between requests.
+ @request.env["PATH_INFO"] = "/per_form_tokens/post_one/"
+ assert_nothing_raised do
+ post :post_one, params: { custom_authenticity_token: form_token }
+ end
+ assert_response :success
+ end
+
+ def test_method_is_case_insensitive
+ get :index, params: { form_method: "POST" }
+
+ form_token = assert_presence_and_fetch_form_csrf_token
+ # This is required because PATH_INFO isn't reset between requests.
+ @request.env["PATH_INFO"] = "/per_form_tokens/post_one/"
+ assert_nothing_raised do
+ post :post_one, params: { custom_authenticity_token: form_token }
+ end
+ assert_response :success
+ end
+
+ private
+ def assert_presence_and_fetch_form_csrf_token
+ assert_select 'input[name="custom_authenticity_token"]' do |input|
+ form_csrf_token = input.first["value"]
+ assert_not_nil form_csrf_token
+ return form_csrf_token
+ end
+ end
+
+ def assert_matches_session_token_on_server(form_token, method = "post")
+ actual = @controller.send(:unmask_token, Base64.strict_decode64(form_token))
+ expected = @controller.send(:per_form_csrf_token, session, "/per_form_tokens/post_one", method)
+ assert_equal expected, actual
+ end
+end
+
+class SkipProtectionControllerTest < ActionController::TestCase
+ def test_should_not_allow_post_without_token_when_not_skipping
+ @controller.skip_requested = false
+ assert_blocked { post :index }
+ end
+
+ def test_should_allow_post_without_token_when_skipping
+ @controller.skip_requested = true
+ assert_not_blocked { post :index }
+ end
+
+ def assert_blocked
+ assert_raises(ActionController::InvalidAuthenticityToken) do
+ yield
+ end
+ end
+
+ def assert_not_blocked
+ assert_nothing_raised { yield }
+ assert_response :success
+ end
+end
diff --git a/actionpack/test/controller/required_params_test.rb b/actionpack/test/controller/required_params_test.rb
index a901e56332..4a83d07e7d 100644
--- a/actionpack/test/controller/required_params_test.rb
+++ b/actionpack/test/controller/required_params_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class BooksController < ActionController::Base
def create
@@ -32,7 +34,6 @@ class ActionControllerRequiredParamsTest < ActionController::TestCase
end
class ParametersRequireTest < ActiveSupport::TestCase
-
test "required parameters should accept and return false value" do
assert_equal(false, ActionController::Parameters.new(person: false).require(:person))
end
@@ -48,4 +49,52 @@ class ParametersRequireTest < ActiveSupport::TestCase
ActionController::Parameters.new(person: {}).require(:person)
end
end
+
+ test "require array when all required params are present" do
+ safe_params = ActionController::Parameters.new(person: { first_name: "Gaurish", title: "Mjallo", city: "Barcelona" })
+ .require(:person)
+ .require([:first_name, :title])
+
+ assert_kind_of Array, safe_params
+ assert_equal ["Gaurish", "Mjallo"], safe_params
+ end
+
+ test "require array when a required param is missing" do
+ assert_raises(ActionController::ParameterMissing) do
+ ActionController::Parameters.new(person: { first_name: "Gaurish", title: nil })
+ .require(:person)
+ .require([:first_name, :title])
+ end
+ end
+
+ test "value params" do
+ params = ActionController::Parameters.new(foo: "bar", dog: "cinco")
+ assert_equal ["bar", "cinco"], params.values
+ assert params.has_value?("cinco")
+ assert params.value?("cinco")
+ end
+
+ test "to_param works like in a Hash" do
+ params = ActionController::Parameters.new(nested: { key: "value" }).permit!
+ assert_equal({ nested: { key: "value" } }.to_param, params.to_param)
+
+ params = { root: ActionController::Parameters.new(nested: { key: "value" }).permit! }
+ assert_equal({ root: { nested: { key: "value" } } }.to_param, params.to_param)
+
+ assert_raise(ActionController::UnfilteredParameters) do
+ ActionController::Parameters.new(nested: { key: "value" }).to_param
+ end
+ end
+
+ test "to_query works like in a Hash" do
+ params = ActionController::Parameters.new(nested: { key: "value" }).permit!
+ assert_equal({ nested: { key: "value" } }.to_query, params.to_query)
+
+ params = { root: ActionController::Parameters.new(nested: { key: "value" }).permit! }
+ assert_equal({ root: { nested: { key: "value" } } }.to_query, params.to_query)
+
+ assert_raise(ActionController::UnfilteredParameters) do
+ ActionController::Parameters.new(nested: { key: "value" }).to_query
+ end
+ end
end
diff --git a/actionpack/test/controller/rescue_test.rb b/actionpack/test/controller/rescue_test.rb
index 2cfd0b8023..3c39373e55 100644
--- a/actionpack/test/controller/rescue_test.rb
+++ b/actionpack/test/controller/rescue_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class RescueController < ActionController::Base
class NotAuthorized < StandardError
@@ -31,50 +33,54 @@ class RescueController < ActionController::Base
class ResourceUnavailableToRescueAsString < StandardError
end
- # We use a fully-qualified name in some strings, and a relative constant
+ # We use a fully qualified name in some strings, and a relative constant
# name in some other to test correct handling of both cases.
- rescue_from NotAuthorized, :with => :deny_access
- rescue_from 'RescueController::NotAuthorizedToRescueAsString', :with => :deny_access
+ rescue_from NotAuthorized, with: :deny_access
+ rescue_from "RescueController::NotAuthorizedToRescueAsString", with: :deny_access
- rescue_from RecordInvalid, :with => :show_errors
- rescue_from 'RescueController::RecordInvalidToRescueAsString', :with => :show_errors
+ rescue_from RecordInvalid, with: :show_errors
+ rescue_from "RescueController::RecordInvalidToRescueAsString", with: :show_errors
- rescue_from NotAllowed, :with => proc { head :forbidden }
- rescue_from 'RescueController::NotAllowedToRescueAsString', :with => proc { head :forbidden }
+ rescue_from NotAllowed, with: proc { head :forbidden }
+ rescue_from "RescueController::NotAllowedToRescueAsString", with: proc { head :forbidden }
- rescue_from InvalidRequest, :with => proc { |exception| render :text => exception.message }
- rescue_from 'InvalidRequestToRescueAsString', :with => proc { |exception| render :text => exception.message }
+ rescue_from InvalidRequest, with: proc { |exception| render plain: exception.message }
+ rescue_from "InvalidRequestToRescueAsString", with: proc { |exception| render plain: exception.message }
rescue_from BadGateway do
head 502
end
- rescue_from 'BadGatewayToRescueAsString' do
+ rescue_from "BadGatewayToRescueAsString" do
head 502
end
rescue_from ResourceUnavailable do |exception|
- render :text => exception.message
+ render plain: exception.message
end
- rescue_from 'ResourceUnavailableToRescueAsString' do |exception|
- render :text => exception.message
+ rescue_from "ResourceUnavailableToRescueAsString" do |exception|
+ render plain: exception.message
end
rescue_from ActionView::TemplateError do
- render :text => 'action_view templater error'
+ render plain: "action_view templater error"
end
rescue_from IOError do
- render :text => 'io error'
+ render plain: "io error"
+ end
+
+ rescue_from ActionDispatch::Http::Parameters::ParseError do
+ render plain: "parse error", status: :bad_request
end
- before_action(only: :before_action_raises) { raise 'umm nice' }
+ before_action(only: :before_action_raises) { raise "umm nice" }
def before_action_raises
end
def raises
- render :text => 'already rendered'
+ render plain: "already rendered"
raise "don't panic!"
end
@@ -128,18 +134,33 @@ class RescueController < ActionController::Base
raise ResourceUnavailableToRescueAsString
end
+ def arbitrary_action
+ params
+ render plain: "arbitrary action"
+ end
+
def missing_template
end
- def io_error_in_view
- raise ActionView::TemplateError.new(nil, IOError.new('this is io error'))
+ def exception_with_more_specific_handler_for_wrapper
+ raise RecordInvalid
+ rescue
+ raise NotAuthorized
+ end
+
+ def exception_with_more_specific_handler_for_cause
+ raise NotAuthorized
+ rescue
+ raise RecordInvalid
end
- def zero_division_error_in_view
- raise ActionView::TemplateError.new(nil, ZeroDivisionError.new('this is zero division error'))
+ def exception_with_no_handler_for_wrapper
+ raise RecordInvalid
+ rescue
+ raise RangeError
end
- protected
+ private
def deny_access
head :forbidden
end
@@ -150,7 +171,6 @@ class RescueController < ActionController::Base
end
class ExceptionInheritanceRescueController < ActionController::Base
-
class ParentException < StandardError
end
@@ -160,9 +180,9 @@ class ExceptionInheritanceRescueController < ActionController::Base
class GrandchildException < ChildException
end
- rescue_from ChildException, :with => lambda { head :ok }
- rescue_from ParentException, :with => lambda { head :created }
- rescue_from GrandchildException, :with => lambda { head :no_content }
+ rescue_from ChildException, with: lambda { head :ok }
+ rescue_from ParentException, with: lambda { head :created }
+ rescue_from GrandchildException, with: lambda { head :no_content }
def raise_parent_exception
raise ParentException
@@ -196,7 +216,7 @@ class ControllerInheritanceRescueController < ExceptionInheritanceRescueControll
class SecondExceptionInChildController < StandardError
end
- rescue_from FirstExceptionInChildController, 'SecondExceptionInChildController', :with => lambda { head :gone }
+ rescue_from FirstExceptionInChildController, "SecondExceptionInChildController", with: lambda { head :gone }
def raise_first_exception_in_child_controller
raise FirstExceptionInChildController
@@ -225,17 +245,6 @@ class ControllerInheritanceRescueControllerTest < ActionController::TestCase
end
class RescueControllerTest < ActionController::TestCase
-
- def test_io_error_in_view
- get :io_error_in_view
- assert_equal 'io error', @response.body
- end
-
- def test_zero_division_error_in_view
- get :zero_division_error_in_view
- assert_equal 'action_view templater error', @response.body
- end
-
def test_rescue_handler
get :not_authorized
assert_response :forbidden
@@ -246,12 +255,14 @@ class RescueControllerTest < ActionController::TestCase
end
def test_rescue_handler_with_argument
- @controller.expects(:show_errors).once.with { |e| e.is_a?(Exception) }
- get :record_invalid
+ assert_called_with @controller, :show_errors, [Exception] do
+ get :record_invalid
+ end
end
def test_rescue_handler_with_argument_as_string
- @controller.expects(:show_errors).once.with { |e| e.is_a?(Exception) }
- get :record_invalid_raise_as_string
+ assert_called_with @controller, :show_errors, [Exception] do
+ get :record_invalid_raise_as_string
+ end
end
def test_proc_rescue_handler
@@ -285,24 +296,55 @@ class RescueControllerTest < ActionController::TestCase
get :resource_unavailable
assert_equal "RescueController::ResourceUnavailable", @response.body
end
-
def test_block_rescue_handler_with_argument_as_string
get :resource_unavailable_raise_as_string
assert_equal "RescueController::ResourceUnavailableToRescueAsString", @response.body
end
+
+ test "rescue when wrapper has more specific handler than cause" do
+ get :exception_with_more_specific_handler_for_wrapper
+ assert_response :forbidden
+ end
+
+ test "rescue when cause has more specific handler than wrapper" do
+ get :exception_with_more_specific_handler_for_cause
+ assert_response :unprocessable_entity
+ end
+
+ test "rescue when cause has handler, but wrapper doesnt" do
+ get :exception_with_no_handler_for_wrapper
+ assert_response :unprocessable_entity
+ end
+
+ test "can rescue a ParseError" do
+ capture_log_output do
+ post :arbitrary_action, body: "{", as: :json
+ end
+ assert_response :bad_request
+ assert_equal "parse error", response.body
+ end
+
+ private
+
+ def capture_log_output
+ output = StringIO.new
+ request.set_header "action_dispatch.logger", ActiveSupport::Logger.new(output)
+ yield
+ output.string
+ end
end
class RescueTest < ActionDispatch::IntegrationTest
class TestController < ActionController::Base
class RecordInvalid < StandardError
def message
- 'invalid'
+ "invalid"
end
end
- rescue_from RecordInvalid, :with => :show_errors
+ rescue_from RecordInvalid, with: :show_errors
def foo
- render :text => "foo"
+ render plain: "foo"
end
def invalid
@@ -310,26 +352,26 @@ class RescueTest < ActionDispatch::IntegrationTest
end
def b00m
- raise 'b00m'
+ raise "b00m"
end
- protected
+ private
def show_errors(exception)
- render :text => exception.message
+ render plain: exception.message
end
end
- test 'normal request' do
+ test "normal request" do
with_test_routing do
- get '/foo'
- assert_equal 'foo', response.body
+ get "/foo"
+ assert_equal "foo", response.body
end
end
- test 'rescue exceptions inside controller' do
+ test "rescue exceptions inside controller" do
with_test_routing do
- get '/invalid'
- assert_equal 'invalid', response.body
+ get "/invalid"
+ assert_equal "invalid", response.body
end
end
@@ -338,9 +380,9 @@ class RescueTest < ActionDispatch::IntegrationTest
def with_test_routing
with_routing do |set|
set.draw do
- get 'foo', :to => ::RescueTest::TestController.action(:foo)
- get 'invalid', :to => ::RescueTest::TestController.action(:invalid)
- get 'b00m', :to => ::RescueTest::TestController.action(:b00m)
+ get "foo", to: ::RescueTest::TestController.action(:foo)
+ get "invalid", to: ::RescueTest::TestController.action(:invalid)
+ get "b00m", to: ::RescueTest::TestController.action(:b00m)
end
yield
end
diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb
index 603d7848f9..d2146f12a5 100644
--- a/actionpack/test/controller/resources_test.rb
+++ b/actionpack/test/controller/resources_test.rb
@@ -1,10 +1,26 @@
-require 'abstract_unit'
-require 'active_support/core_ext/object/try'
-require 'active_support/core_ext/object/with_options'
-require 'active_support/core_ext/array/extract_options'
+# frozen_string_literal: true
-class ResourcesTest < ActionController::TestCase
+require "abstract_unit"
+require "active_support/core_ext/object/try"
+require "active_support/core_ext/object/with_options"
+require "active_support/core_ext/array/extract_options"
+
+class AdminController < ResourcesController; end
+class MessagesController < ResourcesController; end
+class ProductsController < ResourcesController; end
+class ThreadsController < ResourcesController; end
+
+module Backoffice
+ class ProductsController < ResourcesController; end
+ class ImagesController < ResourcesController; end
+
+ module Admin
+ class ProductsController < ResourcesController; end
+ class ImagesController < ResourcesController; end
+ end
+end
+class ResourcesTest < ActionController::TestCase
def test_default_restful_routes
with_restful_routing :messages do
assert_simply_restful_for :messages
@@ -12,45 +28,44 @@ class ResourcesTest < ActionController::TestCase
end
def test_override_paths_for_member_and_collection_methods
- collection_methods = { :rss => :get, :reorder => :post, :csv => :post }
- member_methods = { :rss => :get, :atom => :get, :upload => :post, :fix => :post }
- path_names = {:new => 'nuevo', :rss => 'canal', :fix => 'corrigir' }
+ collection_methods = { rss: :get, reorder: :post, csv: :post }
+ member_methods = { rss: :get, atom: :get, upload: :post, fix: :post }
+ path_names = { new: "nuevo", rss: "canal", fix: "corrigir" }
with_restful_routing :messages,
- :collection => collection_methods,
- :member => member_methods,
- :path_names => path_names do
+ collection: collection_methods,
+ member: member_methods,
+ path_names: path_names do
assert_restful_routes_for :messages,
- :collection => collection_methods,
- :member => member_methods,
- :path_names => path_names do |options|
+ collection: collection_methods,
+ member: member_methods,
+ path_names: path_names do |options|
member_methods.each do |action, method|
- assert_recognizes(options.merge(:action => action.to_s, :id => '1'),
- :path => "/messages/1/#{path_names[action] || action}",
- :method => method)
+ assert_recognizes(options.merge(action: action.to_s, id: "1"),
+ path: "/messages/1/#{path_names[action] || action}",
+ method: method)
end
collection_methods.each do |action, method|
- assert_recognizes(options.merge(:action => action.to_s),
- :path => "/messages/#{path_names[action] || action}",
- :method => method)
+ assert_recognizes(options.merge(action: action.to_s),
+ path: "/messages/#{path_names[action] || action}",
+ method: method)
end
end
assert_restful_named_routes_for :messages,
- :collection => collection_methods,
- :member => member_methods,
- :path_names => path_names do |options|
+ collection: collection_methods,
+ member: member_methods,
+ path_names: path_names do |options|
collection_methods.each_key do |action|
- assert_named_route "/messages/#{path_names[action] || action}", "#{action}_messages_path", :action => action
+ assert_named_route "/messages/#{path_names[action] || action}", "#{action}_messages_path", action: action
end
member_methods.each_key do |action|
- assert_named_route "/messages/1/#{path_names[action] || action}", "#{action}_message_path", :action => action, :id => "1"
+ assert_named_route "/messages/1/#{path_names[action] || action}", "#{action}_message_path", action: action, id: "1"
end
-
end
end
end
@@ -63,182 +78,182 @@ class ResourcesTest < ActionController::TestCase
end
def test_multiple_resources_with_options
- expected_options = {:controller => 'threads', :action => 'index'}
+ expected_options = { controller: "threads", action: "index" }
with_restful_routing :messages, :comments, expected_options.slice(:controller) do
- assert_recognizes(expected_options, :path => 'comments')
- assert_recognizes(expected_options, :path => 'messages')
+ assert_recognizes(expected_options, path: "comments")
+ assert_recognizes(expected_options, path: "messages")
end
end
def test_with_custom_conditions
- with_restful_routing :messages, :conditions => { :subdomain => 'app' } do
- assert @routes.recognize_path("/messages", :method => :get, :subdomain => 'app')
+ with_restful_routing :messages, conditions: { subdomain: "app" } do
+ assert @routes.recognize_path("/messages", method: :get, subdomain: "app")
end
end
def test_irregular_id_with_no_constraints_should_raise_error
- expected_options = {:controller => 'messages', :action => 'show', :id => '1.1.1'}
+ expected_options = { controller: "messages", action: "show", id: "1.1.1" }
with_restful_routing :messages do
assert_raise(Assertion) do
- assert_recognizes(expected_options, :path => 'messages/1.1.1', :method => :get)
+ assert_recognizes(expected_options, path: "messages/1.1.1", method: :get)
end
end
end
def test_irregular_id_with_constraints_should_pass
- expected_options = {:controller => 'messages', :action => 'show', :id => '1.1.1'}
+ expected_options = { controller: "messages", action: "show", id: "1.1.1" }
- with_restful_routing(:messages, :constraints => {:id => /[0-9]\.[0-9]\.[0-9]/}) do
- assert_recognizes(expected_options, :path => 'messages/1.1.1', :method => :get)
+ with_restful_routing(:messages, constraints: { id: /[0-9]\.[0-9]\.[0-9]/ }) do
+ assert_recognizes(expected_options, path: "messages/1.1.1", method: :get)
end
end
def test_with_path_prefix_constraints
- expected_options = {:controller => 'messages', :action => 'show', :thread_id => '1.1.1', :id => '1'}
- with_restful_routing :messages, :path_prefix => '/thread/:thread_id', :constraints => {:thread_id => /[0-9]\.[0-9]\.[0-9]/} do
- assert_recognizes(expected_options, :path => 'thread/1.1.1/messages/1', :method => :get)
+ expected_options = { controller: "messages", action: "show", thread_id: "1.1.1", id: "1" }
+ with_restful_routing :messages, path_prefix: "/thread/:thread_id", constraints: { thread_id: /[0-9]\.[0-9]\.[0-9]/ } do
+ assert_recognizes(expected_options, path: "thread/1.1.1/messages/1", method: :get)
end
end
def test_irregular_id_constraints_should_get_passed_to_member_actions
- expected_options = {:controller => 'messages', :action => 'custom', :id => '1.1.1'}
+ expected_options = { controller: "messages", action: "custom", id: "1.1.1" }
- with_restful_routing(:messages, :member => {:custom => :get}, :constraints => {:id => /[0-9]\.[0-9]\.[0-9]/}) do
- assert_recognizes(expected_options, :path => 'messages/1.1.1/custom', :method => :get)
+ with_restful_routing(:messages, member: { custom: :get }, constraints: { id: /[0-9]\.[0-9]\.[0-9]/ }) do
+ assert_recognizes(expected_options, path: "messages/1.1.1/custom", method: :get)
end
end
def test_with_path_prefix
- with_restful_routing :messages, :path_prefix => '/thread/:thread_id' do
- assert_simply_restful_for :messages, :path_prefix => 'thread/5/', :options => { :thread_id => '5' }
+ with_restful_routing :messages, path_prefix: "/thread/:thread_id" do
+ assert_simply_restful_for :messages, path_prefix: "thread/5/", options: { thread_id: "5" }
end
end
def test_multiple_with_path_prefix
- with_restful_routing :messages, :comments, :path_prefix => '/thread/:thread_id' do
- assert_simply_restful_for :messages, :path_prefix => 'thread/5/', :options => { :thread_id => '5' }
- assert_simply_restful_for :comments, :path_prefix => 'thread/5/', :options => { :thread_id => '5' }
+ with_restful_routing :messages, :comments, path_prefix: "/thread/:thread_id" do
+ assert_simply_restful_for :messages, path_prefix: "thread/5/", options: { thread_id: "5" }
+ assert_simply_restful_for :comments, path_prefix: "thread/5/", options: { thread_id: "5" }
end
end
def test_with_name_prefix
- with_restful_routing :messages, :as => 'post_messages' do
- assert_simply_restful_for :messages, :name_prefix => 'post_'
+ with_restful_routing :messages, as: "post_messages" do
+ assert_simply_restful_for :messages, name_prefix: "post_"
end
end
def test_with_collection_actions
- actions = { 'a' => :get, 'b' => :put, 'c' => :post, 'd' => :delete, 'e' => :patch }
+ actions = { "a" => :get, "b" => :put, "c" => :post, "d" => :delete, "e" => :patch }
with_routing do |set|
set.draw do
resources :messages do
- get :a, :on => :collection
- put :b, :on => :collection
- post :c, :on => :collection
- delete :d, :on => :collection
- patch :e, :on => :collection
+ get :a, on: :collection
+ put :b, on: :collection
+ post :c, on: :collection
+ delete :d, on: :collection
+ patch :e, on: :collection
end
end
assert_restful_routes_for :messages do |options|
actions.each do |action, method|
- assert_recognizes(options.merge(:action => action), :path => "/messages/#{action}", :method => method)
+ assert_recognizes(options.merge(action: action), path: "/messages/#{action}", method: method)
end
end
- assert_restful_named_routes_for :messages do |options|
+ assert_restful_named_routes_for :messages do
actions.each_key do |action|
- assert_named_route "/messages/#{action}", "#{action}_messages_path", :action => action
+ assert_named_route "/messages/#{action}", "#{action}_messages_path", action: action
end
end
end
end
def test_with_collection_actions_and_name_prefix
- actions = { 'a' => :get, 'b' => :put, 'c' => :post, 'd' => :delete, 'e' => :patch }
+ actions = { "a" => :get, "b" => :put, "c" => :post, "d" => :delete, "e" => :patch }
with_routing do |set|
set.draw do
- scope '/threads/:thread_id' do
- resources :messages, :as => 'thread_messages' do
- get :a, :on => :collection
- put :b, :on => :collection
- post :c, :on => :collection
- delete :d, :on => :collection
- patch :e, :on => :collection
+ scope "/threads/:thread_id" do
+ resources :messages, as: "thread_messages" do
+ get :a, on: :collection
+ put :b, on: :collection
+ post :c, on: :collection
+ delete :d, on: :collection
+ patch :e, on: :collection
end
end
end
- assert_restful_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do |options|
+ assert_restful_routes_for :messages, path_prefix: "threads/1/", name_prefix: "thread_", options: { thread_id: "1" } do |options|
actions.each do |action, method|
- assert_recognizes(options.merge(:action => action), :path => "/threads/1/messages/#{action}", :method => method)
+ assert_recognizes(options.merge(action: action), path: "/threads/1/messages/#{action}", method: method)
end
end
- assert_restful_named_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do |options|
+ assert_restful_named_routes_for :messages, path_prefix: "threads/1/", name_prefix: "thread_", options: { thread_id: "1" } do
actions.each_key do |action|
- assert_named_route "/threads/1/messages/#{action}", "#{action}_thread_messages_path", :action => action
+ assert_named_route "/threads/1/messages/#{action}", "#{action}_thread_messages_path", action: action
end
end
end
end
def test_with_collection_actions_and_name_prefix_and_member_action_with_same_name
- actions = { 'a' => :get }
+ actions = { "a" => :get }
with_routing do |set|
set.draw do
- scope '/threads/:thread_id' do
- resources :messages, :as => 'thread_messages' do
- get :a, :on => :collection
- get :a, :on => :member
+ scope "/threads/:thread_id" do
+ resources :messages, as: "thread_messages" do
+ get :a, on: :collection
+ get :a, on: :member
end
end
end
- assert_restful_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do |options|
+ assert_restful_routes_for :messages, path_prefix: "threads/1/", name_prefix: "thread_", options: { thread_id: "1" } do |options|
actions.each do |action, method|
- assert_recognizes(options.merge(:action => action), :path => "/threads/1/messages/#{action}", :method => method)
+ assert_recognizes(options.merge(action: action), path: "/threads/1/messages/#{action}", method: method)
end
end
- assert_restful_named_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do |options|
+ assert_restful_named_routes_for :messages, path_prefix: "threads/1/", name_prefix: "thread_", options: { thread_id: "1" } do
actions.each_key do |action|
- assert_named_route "/threads/1/messages/#{action}", "#{action}_thread_messages_path", :action => action
+ assert_named_route "/threads/1/messages/#{action}", "#{action}_thread_messages_path", action: action
end
end
end
end
def test_with_collection_action_and_name_prefix_and_formatted
- actions = { 'a' => :get, 'b' => :put, 'c' => :post, 'd' => :delete, 'e' => :patch }
+ actions = { "a" => :get, "b" => :put, "c" => :post, "d" => :delete, "e" => :patch }
with_routing do |set|
set.draw do
- scope '/threads/:thread_id' do
- resources :messages, :as => 'thread_messages' do
- get :a, :on => :collection
- put :b, :on => :collection
- post :c, :on => :collection
- delete :d, :on => :collection
- patch :e, :on => :collection
+ scope "/threads/:thread_id" do
+ resources :messages, as: "thread_messages" do
+ get :a, on: :collection
+ put :b, on: :collection
+ post :c, on: :collection
+ delete :d, on: :collection
+ patch :e, on: :collection
end
end
end
- assert_restful_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do |options|
+ assert_restful_routes_for :messages, path_prefix: "threads/1/", name_prefix: "thread_", options: { thread_id: "1" } do |options|
actions.each do |action, method|
- assert_recognizes(options.merge(:action => action, :format => 'xml'), :path => "/threads/1/messages/#{action}.xml", :method => method)
+ assert_recognizes(options.merge(action: action, format: "xml"), path: "/threads/1/messages/#{action}.xml", method: method)
end
end
- assert_restful_named_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do |options|
+ assert_restful_named_routes_for :messages, path_prefix: "threads/1/", name_prefix: "thread_", options: { thread_id: "1" } do
actions.each_key do |action|
- assert_named_route "/threads/1/messages/#{action}.xml", "#{action}_thread_messages_path", :action => action, :format => 'xml'
+ assert_named_route "/threads/1/messages/#{action}.xml", "#{action}_thread_messages_path", action: action, format: "xml"
end
end
end
@@ -246,14 +261,14 @@ class ResourcesTest < ActionController::TestCase
def test_with_member_action
[:patch, :put, :post].each do |method|
- with_restful_routing :messages, :member => { :mark => method } do
- mark_options = {:action => 'mark', :id => '1'}
+ with_restful_routing :messages, member: { mark: method } do
+ mark_options = { action: "mark", id: "1" }
mark_path = "/messages/1/mark"
assert_restful_routes_for :messages do |options|
- assert_recognizes(options.merge(mark_options), :path => mark_path, :method => method)
+ assert_recognizes(options.merge(mark_options), path: mark_path, method: method)
end
- assert_restful_named_routes_for :messages do |options|
+ assert_restful_named_routes_for :messages do
assert_named_route mark_path, :mark_message_path, mark_options
end
end
@@ -261,24 +276,24 @@ class ResourcesTest < ActionController::TestCase
end
def test_with_member_action_and_requirement
- expected_options = {:controller => 'messages', :action => 'mark', :id => '1.1.1'}
+ expected_options = { controller: "messages", action: "mark", id: "1.1.1" }
- with_restful_routing(:messages, :constraints => {:id => /[0-9]\.[0-9]\.[0-9]/}, :member => { :mark => :get }) do
- assert_recognizes(expected_options, :path => 'messages/1.1.1/mark', :method => :get)
+ with_restful_routing(:messages, constraints: { id: /[0-9]\.[0-9]\.[0-9]/ }, member: { mark: :get }) do
+ assert_recognizes(expected_options, path: "messages/1.1.1/mark", method: :get)
end
end
def test_member_when_override_paths_for_default_restful_actions_with
[:patch, :put, :post].each do |method|
- with_restful_routing :messages, :member => { :mark => method }, :path_names => {:new => 'nuevo'} do
- mark_options = {:action => 'mark', :id => '1', :controller => "messages"}
+ with_restful_routing :messages, member: { mark: method }, path_names: { new: "nuevo" } do
+ mark_options = { action: "mark", id: "1", controller: "messages" }
mark_path = "/messages/1/mark"
- assert_restful_routes_for :messages, :path_names => {:new => 'nuevo'} do |options|
- assert_recognizes(options.merge(mark_options), :path => mark_path, :method => method)
+ assert_restful_routes_for :messages, path_names: { new: "nuevo" } do |options|
+ assert_recognizes(options.merge(mark_options), path: mark_path, method: method)
end
- assert_restful_named_routes_for :messages, :path_names => {:new => 'nuevo'} do |options|
+ assert_restful_named_routes_for :messages, path_names: { new: "nuevo" } do
assert_named_route mark_path, :mark_message_path, mark_options
end
end
@@ -291,20 +306,20 @@ class ResourcesTest < ActionController::TestCase
set.draw do
resources :messages do
member do
- match :mark , :via => method
- match :unmark, :via => method
+ match :mark, via: method
+ match :unmark, via: method
end
end
end
%w(mark unmark).each do |action|
- action_options = {:action => action, :id => '1'}
+ action_options = { action: action, id: "1" }
action_path = "/messages/1/#{action}"
assert_restful_routes_for :messages do |options|
- assert_recognizes(options.merge(action_options), :path => action_path, :method => method)
+ assert_recognizes(options.merge(action_options), path: action_path, method: method)
end
- assert_restful_named_routes_for :messages do |options|
+ assert_restful_named_routes_for :messages do
assert_named_route action_path, "#{action}_message_path".to_sym, action_options
end
end
@@ -317,21 +332,21 @@ class ResourcesTest < ActionController::TestCase
set.draw do
resources :messages do
collection do
- match :search, :via => [:post, :get]
+ match :search, via: [:post, :get]
end
member do
- match :toggle, :via => [:post, :get]
+ match :toggle, via: [:post, :get]
end
end
end
assert_restful_routes_for :messages do |options|
[:get, :post].each do |method|
- assert_recognizes(options.merge(:action => 'search'), :path => "/messages/search", :method => method)
+ assert_recognizes(options.merge(action: "search"), path: "/messages/search", method: method)
end
[:get, :post].each do |method|
- assert_recognizes(options.merge(:action => 'toggle', :id => '1'), :path => '/messages/1/toggle', :method => method)
+ assert_recognizes(options.merge(action: "toggle", id: "1"), path: "/messages/1/toggle", method: method)
end
end
end
@@ -341,17 +356,17 @@ class ResourcesTest < ActionController::TestCase
with_routing do |set|
set.draw do
resources :messages do
- post :preview, :on => :new
+ post :preview, on: :new
end
end
- preview_options = {:action => 'preview'}
+ preview_options = { action: "preview" }
preview_path = "/messages/new/preview"
assert_restful_routes_for :messages do |options|
- assert_recognizes(options.merge(preview_options), :path => preview_path, :method => :post)
+ assert_recognizes(options.merge(preview_options), path: preview_path, method: :post)
end
- assert_restful_named_routes_for :messages do |options|
+ assert_restful_named_routes_for :messages do
assert_named_route preview_path, :preview_new_message_path, preview_options
end
end
@@ -360,20 +375,20 @@ class ResourcesTest < ActionController::TestCase
def test_with_new_action_with_name_prefix
with_routing do |set|
set.draw do
- scope('/threads/:thread_id') do
- resources :messages, :as => "thread_messages" do
- post :preview, :on => :new
+ scope("/threads/:thread_id") do
+ resources :messages, as: "thread_messages" do
+ post :preview, on: :new
end
end
end
- preview_options = {:action => 'preview', :thread_id => '1'}
+ preview_options = { action: "preview", thread_id: "1" }
preview_path = "/threads/1/messages/new/preview"
- assert_restful_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do |options|
- assert_recognizes(options.merge(preview_options), :path => preview_path, :method => :post)
+ assert_restful_routes_for :messages, path_prefix: "threads/1/", name_prefix: "thread_", options: { thread_id: "1" } do |options|
+ assert_recognizes(options.merge(preview_options), path: preview_path, method: :post)
end
- assert_restful_named_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do |options|
+ assert_restful_named_routes_for :messages, path_prefix: "threads/1/", name_prefix: "thread_", options: { thread_id: "1" } do
assert_named_route preview_path, :preview_new_thread_message_path, preview_options
end
end
@@ -382,20 +397,20 @@ class ResourcesTest < ActionController::TestCase
def test_with_formatted_new_action_with_name_prefix
with_routing do |set|
set.draw do
- scope('/threads/:thread_id') do
- resources :messages, :as => "thread_messages" do
- post :preview, :on => :new
+ scope("/threads/:thread_id") do
+ resources :messages, as: "thread_messages" do
+ post :preview, on: :new
end
end
end
- preview_options = {:action => 'preview', :thread_id => '1', :format => 'xml'}
+ preview_options = { action: "preview", thread_id: "1", format: "xml" }
preview_path = "/threads/1/messages/new/preview.xml"
- assert_restful_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do |options|
- assert_recognizes(options.merge(preview_options), :path => preview_path, :method => :post)
+ assert_restful_routes_for :messages, path_prefix: "threads/1/", name_prefix: "thread_", options: { thread_id: "1" } do |options|
+ assert_recognizes(options.merge(preview_options), path: preview_path, method: :post)
end
- assert_restful_named_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do |options|
+ assert_restful_named_routes_for :messages, path_prefix: "threads/1/", name_prefix: "thread_", options: { thread_id: "1" } do
assert_named_route preview_path, :preview_new_thread_message_path, preview_options
end
end
@@ -404,9 +419,9 @@ class ResourcesTest < ActionController::TestCase
def test_override_new_method
with_restful_routing :messages do
assert_restful_routes_for :messages do |options|
- assert_recognizes(options.merge(:action => "new"), :path => "/messages/new", :method => :get)
+ assert_recognizes(options.merge(action: "new"), path: "/messages/new", method: :get)
assert_raise(ActionController::RoutingError) do
- @routes.recognize_path("/messages/new", :method => :post)
+ @routes.recognize_path("/messages/new", method: :post)
end
end
end
@@ -414,13 +429,13 @@ class ResourcesTest < ActionController::TestCase
with_routing do |set|
set.draw do
resources :messages do
- match :new, :via => [:post, :get], :on => :new
+ match :new, via: [:post, :get], on: :new
end
end
assert_restful_routes_for :messages do |options|
- assert_recognizes(options.merge(:action => "new"), :path => "/messages/new", :method => :post)
- assert_recognizes(options.merge(:action => "new"), :path => "/messages/new", :method => :get)
+ assert_recognizes(options.merge(action: "new"), path: "/messages/new", method: :post)
+ assert_recognizes(options.merge(action: "new"), path: "/messages/new", method: :get)
end
end
end
@@ -437,20 +452,20 @@ class ResourcesTest < ActionController::TestCase
assert_simply_restful_for :threads
assert_simply_restful_for :messages,
- :name_prefix => 'thread_',
- :path_prefix => 'threads/1/',
- :options => { :thread_id => '1' }
+ name_prefix: "thread_",
+ path_prefix: "threads/1/",
+ options: { thread_id: "1" }
assert_simply_restful_for :comments,
- :name_prefix => 'thread_message_',
- :path_prefix => 'threads/1/messages/2/',
- :options => { :thread_id => '1', :message_id => '2' }
+ name_prefix: "thread_message_",
+ path_prefix: "threads/1/messages/2/",
+ options: { thread_id: "1", message_id: "2" }
end
end
def test_shallow_nested_restful_routes
with_routing do |set|
set.draw do
- resources :threads, :shallow => true do
+ resources :threads, shallow: true do
resources :messages do
resources :comments
end
@@ -458,17 +473,17 @@ class ResourcesTest < ActionController::TestCase
end
assert_simply_restful_for :threads,
- :shallow => true
+ shallow: true
assert_simply_restful_for :messages,
- :name_prefix => 'thread_',
- :path_prefix => 'threads/1/',
- :shallow => true,
- :options => { :thread_id => '1' }
+ name_prefix: "thread_",
+ path_prefix: "threads/1/",
+ shallow: true,
+ options: { thread_id: "1" }
assert_simply_restful_for :comments,
- :name_prefix => 'message_',
- :path_prefix => 'messages/2/',
- :shallow => true,
- :options => { :message_id => '2' }
+ name_prefix: "message_",
+ path_prefix: "messages/2/",
+ shallow: true,
+ options: { message_id: "2" }
end
end
@@ -477,7 +492,7 @@ class ResourcesTest < ActionController::TestCase
set.draw do
namespace :backoffice do
namespace :admin do
- resources :products, :shallow => true do
+ resources :products, shallow: true do
resources :images
end
end
@@ -485,18 +500,18 @@ class ResourcesTest < ActionController::TestCase
end
assert_simply_restful_for :products,
- :controller => 'backoffice/admin/products',
- :namespace => 'backoffice/admin/',
- :name_prefix => 'backoffice_admin_',
- :path_prefix => 'backoffice/admin/',
- :shallow => true
+ controller: "backoffice/admin/products",
+ namespace: "backoffice/admin/",
+ name_prefix: "backoffice_admin_",
+ path_prefix: "backoffice/admin/",
+ shallow: true
assert_simply_restful_for :images,
- :controller => 'backoffice/admin/images',
- :namespace => 'backoffice/admin/',
- :name_prefix => 'backoffice_admin_product_',
- :path_prefix => 'backoffice/admin/products/1/',
- :shallow => true,
- :options => { :product_id => '1' }
+ controller: "backoffice/admin/images",
+ namespace: "backoffice/admin/",
+ name_prefix: "backoffice_admin_product_",
+ path_prefix: "backoffice/admin/products/1/",
+ shallow: true,
+ options: { product_id: "1" }
end
end
@@ -505,8 +520,8 @@ class ResourcesTest < ActionController::TestCase
routes = @routes.routes
routes.each do |route|
routes.each do |r|
- next if route === r # skip the comparison instance
- assert_not_equal [route.conditions, route.path.spec.to_s], [r.conditions, r.path.spec.to_s]
+ next if route == r # skip the comparison instance
+ assert_not_equal [route.conditions, route.path.spec.to_s, route.verb], [r.conditions, r.path.spec.to_s, r.verb]
end
end
end
@@ -519,22 +534,22 @@ class ResourcesTest < ActionController::TestCase
end
def test_should_create_multiple_singleton_resource_routes
- with_singleton_resources :account, :logo do
+ with_singleton_resources :account, :product do
assert_singleton_restful_for :account
- assert_singleton_restful_for :logo
+ assert_singleton_restful_for :product
end
end
def test_should_create_nested_singleton_resource_routes
with_routing do |set|
set.draw do
- resource :admin, :controller => 'admin' do
+ resource :admin, controller: "admin" do
resource :account
end
end
- assert_singleton_restful_for :admin, :controller => 'admin'
- assert_singleton_restful_for :account, :name_prefix => "admin_", :path_prefix => 'admin/'
+ assert_singleton_restful_for :admin, controller: "admin"
+ assert_singleton_restful_for :account, name_prefix: "admin_", path_prefix: "admin/"
end
end
@@ -543,17 +558,17 @@ class ResourcesTest < ActionController::TestCase
with_routing do |set|
set.draw do
resource :account do
- match :reset, :on => :member, :via => method
+ match :reset, on: :member, via: method
end
end
- reset_options = {:action => 'reset'}
+ reset_options = { action: "reset" }
reset_path = "/account/reset"
assert_singleton_routes_for :account do |options|
- assert_recognizes(options.merge(reset_options), :path => reset_path, :method => method)
+ assert_recognizes(options.merge(reset_options), path: reset_path, method: method)
end
- assert_singleton_named_routes_for :account do |options|
+ assert_singleton_named_routes_for :account do
assert_named_route reset_path, :reset_account_path, reset_options
end
end
@@ -565,19 +580,19 @@ class ResourcesTest < ActionController::TestCase
with_routing do |set|
set.draw do
resource :account do
- match :reset, :on => :member, :via => method
- match :disable, :on => :member, :via => method
+ match :reset, on: :member, via: method
+ match :disable, on: :member, via: method
end
end
%w(reset disable).each do |action|
- action_options = {:action => action}
+ action_options = { action: action }
action_path = "/account/#{action}"
assert_singleton_routes_for :account do |options|
- assert_recognizes(options.merge(action_options), :path => action_path, :method => method)
+ assert_recognizes(options.merge(action_options), path: action_path, method: method)
end
- assert_singleton_named_routes_for :account do |options|
+ assert_singleton_named_routes_for :account do
assert_named_route action_path, "#{action}_account_path".to_sym, action_options
end
end
@@ -594,22 +609,22 @@ class ResourcesTest < ActionController::TestCase
end
assert_singleton_restful_for :account
- assert_simply_restful_for :messages, :name_prefix => "account_", :path_prefix => 'account/'
+ assert_simply_restful_for :messages, name_prefix: "account_", path_prefix: "account/"
end
end
def test_should_nest_resources_in_singleton_resource_with_path_scope
with_routing do |set|
set.draw do
- scope ':site_id' do
+ scope ":site_id" do
resource(:account) do
resources :messages
end
end
end
- assert_singleton_restful_for :account, :path_prefix => '7/', :options => { :site_id => '7' }
- assert_simply_restful_for :messages, :name_prefix => "account_", :path_prefix => '7/account/', :options => { :site_id => '7' }
+ assert_singleton_restful_for :account, path_prefix: "7/", options: { site_id: "7" }
+ assert_simply_restful_for :messages, name_prefix: "account_", path_prefix: "7/account/", options: { site_id: "7" }
end
end
@@ -617,31 +632,31 @@ class ResourcesTest < ActionController::TestCase
with_routing do |set|
set.draw do
resources :threads do
- resource :admin, :controller => 'admin'
+ resource :admin, controller: "admin"
end
end
assert_simply_restful_for :threads
- assert_singleton_restful_for :admin, :controller => 'admin', :name_prefix => 'thread_', :path_prefix => 'threads/5/', :options => { :thread_id => '5' }
+ assert_singleton_restful_for :admin, controller: "admin", name_prefix: "thread_", path_prefix: "threads/5/", options: { thread_id: "5" }
end
end
def test_should_not_allow_delete_or_patch_or_put_on_collection_path
controller_name = :messages
with_restful_routing controller_name do
- options = { :controller => controller_name.to_s }
+ options = { controller: controller_name.to_s }
collection_path = "/#{controller_name}"
assert_raise(Assertion) do
- assert_recognizes(options.merge(:action => 'update'), :path => collection_path, :method => :patch)
+ assert_recognizes(options.merge(action: "update"), path: collection_path, method: :patch)
end
assert_raise(Assertion) do
- assert_recognizes(options.merge(:action => 'update'), :path => collection_path, :method => :put)
+ assert_recognizes(options.merge(action: "update"), path: collection_path, method: :put)
end
assert_raise(Assertion) do
- assert_recognizes(options.merge(:action => 'destroy'), :path => collection_path, :method => :delete)
+ assert_recognizes(options.merge(action: "destroy"), path: collection_path, method: :delete)
end
end
end
@@ -649,15 +664,15 @@ class ResourcesTest < ActionController::TestCase
def test_new_style_named_routes_for_resource
with_routing do |set|
set.draw do
- scope '/threads/:thread_id' do
- resources :messages, :as => 'thread_messages' do
- get :search, :on => :collection
- get :preview, :on => :new
+ scope "/threads/:thread_id" do
+ resources :messages, as: "thread_messages" do
+ get :search, on: :collection
+ get :preview, on: :new
end
end
end
- assert_simply_restful_for :messages, :name_prefix => 'thread_', :path_prefix => 'threads/1/', :options => { :thread_id => '1' }
+ assert_simply_restful_for :messages, name_prefix: "thread_", path_prefix: "threads/1/", options: { thread_id: "1" }
assert_named_route "/threads/1/messages/search", "search_thread_messages_path", {}
assert_named_route "/threads/1/messages/new", "new_thread_message_path", {}
assert_named_route "/threads/1/messages/new/preview", "preview_new_thread_message_path", {}
@@ -667,14 +682,14 @@ class ResourcesTest < ActionController::TestCase
def test_new_style_named_routes_for_singleton_resource
with_routing do |set|
set.draw do
- scope '/admin' do
- resource :account, :as => :admin_account do
- get :login, :on => :member
- get :preview, :on => :new
+ scope "/admin" do
+ resource :account, as: :admin_account do
+ get :login, on: :member
+ get :preview, on: :new
end
end
end
- assert_singleton_restful_for :account, :name_prefix => 'admin_', :path_prefix => 'admin/'
+ assert_singleton_restful_for :account, name_prefix: "admin_", path_prefix: "admin/"
assert_named_route "/admin/account/login", "login_admin_account_path", {}
assert_named_route "/admin/account/new", "new_admin_account_path", {}
assert_named_route "/admin/account/new/preview", "preview_new_admin_account_path", {}
@@ -689,7 +704,7 @@ class ResourcesTest < ActionController::TestCase
end
end
- assert_simply_restful_for :products, :controller => "backoffice/products", :name_prefix => 'backoffice_', :path_prefix => 'backoffice/'
+ assert_simply_restful_for :products, controller: "backoffice/products", name_prefix: "backoffice_", path_prefix: "backoffice/"
end
end
@@ -703,19 +718,19 @@ class ResourcesTest < ActionController::TestCase
end
end
- assert_simply_restful_for :products, :controller => "backoffice/admin/products", :name_prefix => 'backoffice_admin_', :path_prefix => 'backoffice/admin/'
+ assert_simply_restful_for :products, controller: "backoffice/admin/products", name_prefix: "backoffice_admin_", path_prefix: "backoffice/admin/"
end
end
def test_resources_using_namespace
with_routing do |set|
set.draw do
- namespace :backoffice, :path => nil, :as => nil do
+ namespace :backoffice, path: nil, as: nil do
resources :products
end
end
- assert_simply_restful_for :products, :controller => "backoffice/products"
+ assert_simply_restful_for :products, controller: "backoffice/products"
end
end
@@ -729,7 +744,7 @@ class ResourcesTest < ActionController::TestCase
end
end
- assert_simply_restful_for :images, :controller => "backoffice/images", :name_prefix => 'backoffice_product_', :path_prefix => 'backoffice/products/1/', :options => {:product_id => '1'}
+ assert_simply_restful_for :images, controller: "backoffice/images", name_prefix: "backoffice_product_", path_prefix: "backoffice/products/1/", options: { product_id: "1" }
end
end
@@ -745,129 +760,129 @@ class ResourcesTest < ActionController::TestCase
end
end
- assert_simply_restful_for :images, :controller => "backoffice/admin/images", :name_prefix => 'backoffice_admin_product_', :path_prefix => 'backoffice/admin/products/1/', :options => {:product_id => '1'}
+ assert_simply_restful_for :images, controller: "backoffice/admin/images", name_prefix: "backoffice_admin_product_", path_prefix: "backoffice/admin/products/1/", options: { product_id: "1" }
end
end
def test_with_path_segment
with_restful_routing :messages do
assert_simply_restful_for :messages
- assert_recognizes({:controller => "messages", :action => "index"}, "/messages")
- assert_recognizes({:controller => "messages", :action => "index"}, "/messages/")
+ assert_recognizes({ controller: "messages", action: "index" }, "/messages")
+ assert_recognizes({ controller: "messages", action: "index" }, "/messages/")
end
- with_routing do |set|
- set.draw do
- resources :messages, :path => 'reviews'
- end
- assert_simply_restful_for :messages, :as => 'reviews'
- assert_recognizes({:controller => "messages", :action => "index"}, "/reviews")
- assert_recognizes({:controller => "messages", :action => "index"}, "/reviews/")
- end
+ with_routing do |set|
+ set.draw do
+ resources :messages, path: "reviews"
+ end
+ assert_simply_restful_for :messages, as: "reviews"
+ assert_recognizes({ controller: "messages", action: "index" }, "/reviews")
+ assert_recognizes({ controller: "messages", action: "index" }, "/reviews/")
+ end
end
def test_multiple_with_path_segment_and_controller
with_routing do |set|
set.draw do
- resources :products do
- resources :product_reviews, :path => 'reviews', :controller => 'messages'
- end
- resources :tutors do
- resources :tutor_reviews, :path => 'reviews', :controller => 'comments'
- end
+ resources :products do
+ resources :product_reviews, path: "reviews", controller: "messages"
+ end
+ resources :tutors do
+ resources :tutor_reviews, path: "reviews", controller: "comments"
+ end
end
- assert_simply_restful_for :product_reviews, :controller=>'messages', :as => 'reviews', :name_prefix => 'product_', :path_prefix => 'products/1/', :options => {:product_id => '1'}
- assert_simply_restful_for :tutor_reviews,:controller=>'comments', :as => 'reviews', :name_prefix => 'tutor_', :path_prefix => 'tutors/1/', :options => {:tutor_id => '1'}
+ assert_simply_restful_for :product_reviews, controller: "messages", as: "reviews", name_prefix: "product_", path_prefix: "products/1/", options: { product_id: "1" }
+ assert_simply_restful_for :tutor_reviews, controller: "comments", as: "reviews", name_prefix: "tutor_", path_prefix: "tutors/1/", options: { tutor_id: "1" }
end
end
def test_with_path_segment_path_prefix_constraints
- expected_options = {:controller => 'messages', :action => 'show', :thread_id => '1.1.1', :id => '1'}
+ expected_options = { controller: "messages", action: "show", thread_id: "1.1.1", id: "1" }
with_routing do |set|
set.draw do
- scope '/thread/:thread_id', :constraints => { :thread_id => /[0-9]\.[0-9]\.[0-9]/ } do
- resources :messages, :path => 'comments'
+ scope "/thread/:thread_id", constraints: { thread_id: /[0-9]\.[0-9]\.[0-9]/ } do
+ resources :messages, path: "comments"
end
end
- assert_recognizes(expected_options, :path => 'thread/1.1.1/comments/1', :method => :get)
+ assert_recognizes(expected_options, path: "thread/1.1.1/comments/1", method: :get)
end
end
def test_resource_has_only_show_action
with_routing do |set|
set.draw do
- resources :products, :only => :show
+ resources :products, only: :show
end
- assert_resource_allowed_routes('products', {}, { :id => '1' }, :show, [:index, :new, :create, :edit, :update, :destroy])
- assert_resource_allowed_routes('products', { :format => 'xml' }, { :id => '1' }, :show, [:index, :new, :create, :edit, :update, :destroy])
+ assert_resource_allowed_routes("products", {}, { id: "1" }, :show, [:index, :new, :create, :edit, :update, :destroy])
+ assert_resource_allowed_routes("products", { format: "xml" }, { id: "1" }, :show, [:index, :new, :create, :edit, :update, :destroy])
end
end
def test_singleton_resource_has_only_show_action
with_routing do |set|
set.draw do
- resource :account, :only => :show
+ resource :account, only: :show
end
- assert_singleton_resource_allowed_routes('accounts', {}, :show, [:index, :new, :create, :edit, :update, :destroy])
- assert_singleton_resource_allowed_routes('accounts', { :format => 'xml' }, :show, [:index, :new, :create, :edit, :update, :destroy])
+ assert_singleton_resource_allowed_routes("accounts", {}, :show, [:index, :new, :create, :edit, :update, :destroy])
+ assert_singleton_resource_allowed_routes("accounts", { format: "xml" }, :show, [:index, :new, :create, :edit, :update, :destroy])
end
end
def test_resource_does_not_have_destroy_action
with_routing do |set|
set.draw do
- resources :products, :except => :destroy
+ resources :products, except: :destroy
end
- assert_resource_allowed_routes('products', {}, { :id => '1' }, [:index, :new, :create, :show, :edit, :update], :destroy)
- assert_resource_allowed_routes('products', { :format => 'xml' }, { :id => '1' }, [:index, :new, :create, :show, :edit, :update], :destroy)
+ assert_resource_allowed_routes("products", {}, { id: "1" }, [:index, :new, :create, :show, :edit, :update], :destroy)
+ assert_resource_allowed_routes("products", { format: "xml" }, { id: "1" }, [:index, :new, :create, :show, :edit, :update], :destroy)
end
end
def test_singleton_resource_does_not_have_destroy_action
with_routing do |set|
set.draw do
- resource :account, :except => :destroy
+ resource :account, except: :destroy
end
- assert_singleton_resource_allowed_routes('accounts', {}, [:new, :create, :show, :edit, :update], :destroy)
- assert_singleton_resource_allowed_routes('accounts', { :format => 'xml' }, [:new, :create, :show, :edit, :update], :destroy)
+ assert_singleton_resource_allowed_routes("accounts", {}, [:new, :create, :show, :edit, :update], :destroy)
+ assert_singleton_resource_allowed_routes("accounts", { format: "xml" }, [:new, :create, :show, :edit, :update], :destroy)
end
end
def test_resource_has_show_action_but_does_not_have_destroy_action
with_routing do |set|
set.draw do
- resources :products, :only => [:show, :destroy], :except => :destroy
+ resources :products, only: [:show, :destroy], except: :destroy
end
- assert_resource_allowed_routes('products', {}, { :id => '1' }, :show, [:index, :new, :create, :edit, :update, :destroy])
- assert_resource_allowed_routes('products', { :format => 'xml' }, { :id => '1' }, :show, [:index, :new, :create, :edit, :update, :destroy])
+ assert_resource_allowed_routes("products", {}, { id: "1" }, :show, [:index, :new, :create, :edit, :update, :destroy])
+ assert_resource_allowed_routes("products", { format: "xml" }, { id: "1" }, :show, [:index, :new, :create, :edit, :update, :destroy])
end
end
def test_singleton_resource_has_show_action_but_does_not_have_destroy_action
with_routing do |set|
set.draw do
- resource :account, :only => [:show, :destroy], :except => :destroy
+ resource :account, only: [:show, :destroy], except: :destroy
end
- assert_singleton_resource_allowed_routes('accounts', {}, :show, [:new, :create, :edit, :update, :destroy])
- assert_singleton_resource_allowed_routes('accounts', { :format => 'xml' }, :show, [:new, :create, :edit, :update, :destroy])
+ assert_singleton_resource_allowed_routes("accounts", {}, :show, [:new, :create, :edit, :update, :destroy])
+ assert_singleton_resource_allowed_routes("accounts", { format: "xml" }, :show, [:new, :create, :edit, :update, :destroy])
end
end
def test_resource_has_only_create_action_and_named_route
with_routing do |set|
set.draw do
- resources :products, :only => :create
+ resources :products, only: :create
end
- assert_resource_allowed_routes('products', {}, { :id => '1' }, :create, [:index, :new, :show, :edit, :update, :destroy])
- assert_resource_allowed_routes('products', { :format => 'xml' }, { :id => '1' }, :create, [:index, :new, :show, :edit, :update, :destroy])
+ assert_resource_allowed_routes("products", {}, { id: "1" }, :create, [:index, :new, :show, :edit, :update, :destroy])
+ assert_resource_allowed_routes("products", { format: "xml" }, { id: "1" }, :create, [:index, :new, :show, :edit, :update, :destroy])
assert_not_nil set.named_routes[:products]
end
@@ -876,11 +891,11 @@ class ResourcesTest < ActionController::TestCase
def test_resource_has_only_update_action_and_named_route
with_routing do |set|
set.draw do
- resources :products, :only => :update
+ resources :products, only: :update
end
- assert_resource_allowed_routes('products', {}, { :id => '1' }, :update, [:index, :new, :create, :show, :edit, :destroy])
- assert_resource_allowed_routes('products', { :format => 'xml' }, { :id => '1' }, :update, [:index, :new, :create, :show, :edit, :destroy])
+ assert_resource_allowed_routes("products", {}, { id: "1" }, :update, [:index, :new, :create, :show, :edit, :destroy])
+ assert_resource_allowed_routes("products", { format: "xml" }, { id: "1" }, :update, [:index, :new, :create, :show, :edit, :destroy])
assert_not_nil set.named_routes[:product]
end
@@ -889,11 +904,11 @@ class ResourcesTest < ActionController::TestCase
def test_resource_has_only_destroy_action_and_named_route
with_routing do |set|
set.draw do
- resources :products, :only => :destroy
+ resources :products, only: :destroy
end
- assert_resource_allowed_routes('products', {}, { :id => '1' }, :destroy, [:index, :new, :create, :show, :edit, :update])
- assert_resource_allowed_routes('products', { :format => 'xml' }, { :id => '1' }, :destroy, [:index, :new, :create, :show, :edit, :update])
+ assert_resource_allowed_routes("products", {}, { id: "1" }, :destroy, [:index, :new, :create, :show, :edit, :update])
+ assert_resource_allowed_routes("products", { format: "xml" }, { id: "1" }, :destroy, [:index, :new, :create, :show, :edit, :update])
assert_not_nil set.named_routes[:product]
end
@@ -902,11 +917,11 @@ class ResourcesTest < ActionController::TestCase
def test_singleton_resource_has_only_create_action_and_named_route
with_routing do |set|
set.draw do
- resource :account, :only => :create
+ resource :account, only: :create
end
- assert_singleton_resource_allowed_routes('accounts', {}, :create, [:new, :show, :edit, :update, :destroy])
- assert_singleton_resource_allowed_routes('accounts', { :format => 'xml' }, :create, [:new, :show, :edit, :update, :destroy])
+ assert_singleton_resource_allowed_routes("accounts", {}, :create, [:new, :show, :edit, :update, :destroy])
+ assert_singleton_resource_allowed_routes("accounts", { format: "xml" }, :create, [:new, :show, :edit, :update, :destroy])
assert_not_nil set.named_routes[:account]
end
@@ -915,11 +930,11 @@ class ResourcesTest < ActionController::TestCase
def test_singleton_resource_has_only_update_action_and_named_route
with_routing do |set|
set.draw do
- resource :account, :only => :update
+ resource :account, only: :update
end
- assert_singleton_resource_allowed_routes('accounts', {}, :update, [:new, :create, :show, :edit, :destroy])
- assert_singleton_resource_allowed_routes('accounts', { :format => 'xml' }, :update, [:new, :create, :show, :edit, :destroy])
+ assert_singleton_resource_allowed_routes("accounts", {}, :update, [:new, :create, :show, :edit, :destroy])
+ assert_singleton_resource_allowed_routes("accounts", { format: "xml" }, :update, [:new, :create, :show, :edit, :destroy])
assert_not_nil set.named_routes[:account]
end
@@ -928,11 +943,11 @@ class ResourcesTest < ActionController::TestCase
def test_singleton_resource_has_only_destroy_action_and_named_route
with_routing do |set|
set.draw do
- resource :account, :only => :destroy
+ resource :account, only: :destroy
end
- assert_singleton_resource_allowed_routes('accounts', {}, :destroy, [:new, :create, :show, :edit, :update])
- assert_singleton_resource_allowed_routes('accounts', { :format => 'xml' }, :destroy, [:new, :create, :show, :edit, :update])
+ assert_singleton_resource_allowed_routes("accounts", {}, :destroy, [:new, :create, :show, :edit, :update])
+ assert_singleton_resource_allowed_routes("accounts", { format: "xml" }, :destroy, [:new, :create, :show, :edit, :update])
assert_not_nil set.named_routes[:account]
end
@@ -941,120 +956,120 @@ class ResourcesTest < ActionController::TestCase
def test_resource_has_only_collection_action
with_routing do |set|
set.draw do
- resources :products, :only => [] do
- get :sale, :on => :collection
+ resources :products, only: [] do
+ get :sale, on: :collection
end
end
- assert_resource_allowed_routes('products', {}, { :id => '1' }, [], [:index, :new, :create, :show, :edit, :update, :destroy])
- assert_resource_allowed_routes('products', { :format => 'xml' }, { :id => '1' }, [], [:index, :new, :create, :show, :edit, :update, :destroy])
+ assert_resource_allowed_routes("products", {}, { id: "1" }, [], [:index, :new, :create, :show, :edit, :update, :destroy])
+ assert_resource_allowed_routes("products", { format: "xml" }, { id: "1" }, [], [:index, :new, :create, :show, :edit, :update, :destroy])
- assert_recognizes({ :controller => 'products', :action => 'sale' }, :path => 'products/sale', :method => :get)
- assert_recognizes({ :controller => 'products', :action => 'sale', :format => 'xml' }, :path => 'products/sale.xml', :method => :get)
+ assert_recognizes({ controller: "products", action: "sale" }, { path: "products/sale", method: :get })
+ assert_recognizes({ controller: "products", action: "sale", format: "xml" }, { path: "products/sale.xml", method: :get })
end
end
def test_resource_has_only_member_action
with_routing do |set|
set.draw do
- resources :products, :only => [] do
- get :preview, :on => :member
+ resources :products, only: [] do
+ get :preview, on: :member
end
end
- assert_resource_allowed_routes('products', {}, { :id => '1' }, [], [:index, :new, :create, :show, :edit, :update, :destroy])
- assert_resource_allowed_routes('products', { :format => 'xml' }, { :id => '1' }, [], [:index, :new, :create, :show, :edit, :update, :destroy])
+ assert_resource_allowed_routes("products", {}, { id: "1" }, [], [:index, :new, :create, :show, :edit, :update, :destroy])
+ assert_resource_allowed_routes("products", { format: "xml" }, { id: "1" }, [], [:index, :new, :create, :show, :edit, :update, :destroy])
- assert_recognizes({ :controller => 'products', :action => 'preview', :id => '1' }, :path => 'products/1/preview', :method => :get)
- assert_recognizes({ :controller => 'products', :action => 'preview', :id => '1', :format => 'xml' }, :path => 'products/1/preview.xml', :method => :get)
+ assert_recognizes({ controller: "products", action: "preview", id: "1" }, { path: "products/1/preview", method: :get })
+ assert_recognizes({ controller: "products", action: "preview", id: "1", format: "xml" }, { path: "products/1/preview.xml", method: :get })
end
end
def test_singleton_resource_has_only_member_action
with_routing do |set|
set.draw do
- resource :account, :only => [] do
+ resource :account, only: [] do
member do
get :signup
end
end
end
- assert_singleton_resource_allowed_routes('accounts', {}, [], [:new, :create, :show, :edit, :update, :destroy])
- assert_singleton_resource_allowed_routes('accounts', { :format => 'xml' }, [], [:new, :create, :show, :edit, :update, :destroy])
+ assert_singleton_resource_allowed_routes("accounts", {}, [], [:new, :create, :show, :edit, :update, :destroy])
+ assert_singleton_resource_allowed_routes("accounts", { format: "xml" }, [], [:new, :create, :show, :edit, :update, :destroy])
- assert_recognizes({ :controller => 'accounts', :action => 'signup' }, :path => 'account/signup', :method => :get)
- assert_recognizes({ :controller => 'accounts', :action => 'signup', :format => 'xml' }, :path => 'account/signup.xml', :method => :get)
+ assert_recognizes({ controller: "accounts", action: "signup" }, { path: "account/signup", method: :get })
+ assert_recognizes({ controller: "accounts", action: "signup", format: "xml" }, { path: "account/signup.xml", method: :get })
end
end
def test_nested_resource_has_only_show_and_member_action
with_routing do |set|
set.draw do
- resources :products, :only => [:index, :show] do
- resources :images, :only => :show do
- get :thumbnail, :on => :member
+ resources :products, only: [:index, :show] do
+ resources :images, only: :show do
+ get :thumbnail, on: :member
end
end
end
- assert_resource_allowed_routes('images', { :product_id => '1' }, { :id => '2' }, :show, [:index, :new, :create, :edit, :update, :destroy], 'products/1/images')
- assert_resource_allowed_routes('images', { :product_id => '1', :format => 'xml' }, { :id => '2' }, :show, [:index, :new, :create, :edit, :update, :destroy], 'products/1/images')
+ assert_resource_allowed_routes("images", { product_id: "1" }, { id: "2" }, :show, [:index, :new, :create, :edit, :update, :destroy], "products/1/images")
+ assert_resource_allowed_routes("images", { product_id: "1", format: "xml" }, { id: "2" }, :show, [:index, :new, :create, :edit, :update, :destroy], "products/1/images")
- assert_recognizes({ :controller => 'images', :action => 'thumbnail', :product_id => '1', :id => '2' }, :path => 'products/1/images/2/thumbnail', :method => :get)
- assert_recognizes({ :controller => 'images', :action => 'thumbnail', :product_id => '1', :id => '2', :format => 'jpg' }, :path => 'products/1/images/2/thumbnail.jpg', :method => :get)
+ assert_recognizes({ controller: "images", action: "thumbnail", product_id: "1", id: "2" }, { path: "products/1/images/2/thumbnail", method: :get })
+ assert_recognizes({ controller: "images", action: "thumbnail", product_id: "1", id: "2", format: "jpg" }, { path: "products/1/images/2/thumbnail.jpg", method: :get })
end
end
def test_nested_resource_does_not_inherit_only_option
with_routing do |set|
set.draw do
- resources :products, :only => :show do
- resources :images, :except => :destroy
+ resources :products, only: :show do
+ resources :images, except: :destroy
end
end
- assert_resource_allowed_routes('images', { :product_id => '1' }, { :id => '2' }, [:index, :new, :create, :show, :edit, :update], :destroy, 'products/1/images')
- assert_resource_allowed_routes('images', { :product_id => '1', :format => 'xml' }, { :id => '2' }, [:index, :new, :create, :show, :edit, :update], :destroy, 'products/1/images')
+ assert_resource_allowed_routes("images", { product_id: "1" }, { id: "2" }, [:index, :new, :create, :show, :edit, :update], :destroy, "products/1/images")
+ assert_resource_allowed_routes("images", { product_id: "1", format: "xml" }, { id: "2" }, [:index, :new, :create, :show, :edit, :update], :destroy, "products/1/images")
end
end
def test_nested_resource_does_not_inherit_only_option_by_default
with_routing do |set|
set.draw do
- resources :products, :only => :show do
+ resources :products, only: :show do
resources :images
end
end
- assert_resource_allowed_routes('images', { :product_id => '1' }, { :id => '2' }, [:index, :new, :create, :show, :edit, :update, :destroy], [], 'products/1/images')
- assert_resource_allowed_routes('images', { :product_id => '1', :format => 'xml' }, { :id => '2' }, [:index, :new, :create, :show, :edit, :update, :destroy], [], 'products/1/images')
+ assert_resource_allowed_routes("images", { product_id: "1" }, { id: "2" }, [:index, :new, :create, :show, :edit, :update, :destroy], [], "products/1/images")
+ assert_resource_allowed_routes("images", { product_id: "1", format: "xml" }, { id: "2" }, [:index, :new, :create, :show, :edit, :update, :destroy], [], "products/1/images")
end
end
def test_nested_resource_does_not_inherit_except_option
with_routing do |set|
set.draw do
- resources :products, :except => :show do
- resources :images, :only => :destroy
+ resources :products, except: :show do
+ resources :images, only: :destroy
end
end
- assert_resource_allowed_routes('images', { :product_id => '1' }, { :id => '2' }, :destroy, [:index, :new, :create, :show, :edit, :update], 'products/1/images')
- assert_resource_allowed_routes('images', { :product_id => '1', :format => 'xml' }, { :id => '2' }, :destroy, [:index, :new, :create, :show, :edit, :update], 'products/1/images')
+ assert_resource_allowed_routes("images", { product_id: "1" }, { id: "2" }, :destroy, [:index, :new, :create, :show, :edit, :update], "products/1/images")
+ assert_resource_allowed_routes("images", { product_id: "1", format: "xml" }, { id: "2" }, :destroy, [:index, :new, :create, :show, :edit, :update], "products/1/images")
end
end
def test_nested_resource_does_not_inherit_except_option_by_default
with_routing do |set|
set.draw do
- resources :products, :except => :show do
+ resources :products, except: :show do
resources :images
end
end
- assert_resource_allowed_routes('images', { :product_id => '1' }, { :id => '2' }, [:index, :new, :create, :show, :edit, :update, :destroy], [], 'products/1/images')
- assert_resource_allowed_routes('images', { :product_id => '1', :format => 'xml' }, { :id => '2' }, [:index, :new, :create, :show, :edit, :update, :destroy], [], 'products/1/images')
+ assert_resource_allowed_routes("images", { product_id: "1" }, { id: "2" }, [:index, :new, :create, :show, :edit, :update, :destroy], [], "products/1/images")
+ assert_resource_allowed_routes("images", { product_id: "1", format: "xml" }, { id: "2" }, [:index, :new, :create, :show, :edit, :update, :destroy], [], "products/1/images")
end
end
@@ -1064,8 +1079,8 @@ class ResourcesTest < ActionController::TestCase
resource :product
end
- assert_routing '/product', :controller => 'products', :action => 'show'
- assert set.recognize_path("/product", :method => :get)
+ assert_routing "/product", controller: "products", action: "show"
+ assert set.recognize_path("/product", method: :get)
end
end
@@ -1092,12 +1107,12 @@ class ResourcesTest < ActionController::TestCase
end
def test_singleton_resource_name_is_not_singularized
- with_singleton_resources(:preferences) do
- assert_singleton_restful_for :preferences
+ with_singleton_resources(:products) do
+ assert_singleton_restful_for :products
end
end
- protected
+ private
def with_restful_routing(*args)
options = args.extract_options!
collection_methods = options.delete(:collection)
@@ -1107,7 +1122,7 @@ class ResourcesTest < ActionController::TestCase
with_routing do |set|
set.draw do
- scope(path_prefix || '') do
+ scope(path_prefix || "") do
resources(*args) do
if collection_methods
collection do
@@ -1133,7 +1148,7 @@ class ResourcesTest < ActionController::TestCase
def with_singleton_resources(*args)
with_routing do |set|
- set.draw {resource(*args) }
+ set.draw { resource(*args) }
yield
end
end
@@ -1150,14 +1165,14 @@ class ResourcesTest < ActionController::TestCase
end
def assert_restful_routes_for(controller_name, options = {})
- options[:options] ||= {}
- options[:options][:controller] = options[:controller] || controller_name.to_s
+ route_options = (options[:options] ||= {}).dup
+ route_options[:controller] = options[:controller] || controller_name.to_s
if options[:shallow]
options[:shallow_options] ||= {}
- options[:shallow_options][:controller] = options[:options][:controller]
+ options[:shallow_options][:controller] = route_options[:controller]
else
- options[:shallow_options] = options[:options]
+ options[:shallow_options] = route_options
end
new_action = @routes.resources_path_names[:new] || "new"
@@ -1176,37 +1191,37 @@ class ResourcesTest < ActionController::TestCase
edit_member_path = "#{member_path}/#{edit_action}"
formatted_edit_member_path = "#{member_path}/#{edit_action}.xml"
- with_options(options[:options]) do |controller|
- controller.assert_routing collection_path, :action => 'index'
- controller.assert_routing new_path, :action => 'new'
- controller.assert_routing "#{collection_path}.xml", :action => 'index', :format => 'xml'
- controller.assert_routing "#{new_path}.xml", :action => 'new', :format => 'xml'
+ with_options(route_options) do |controller|
+ controller.assert_routing collection_path, action: "index"
+ controller.assert_routing new_path, action: "new"
+ controller.assert_routing "#{collection_path}.xml", action: "index", format: "xml"
+ controller.assert_routing "#{new_path}.xml", action: "new", format: "xml"
end
with_options(options[:shallow_options]) do |controller|
- controller.assert_routing member_path, :action => 'show', :id => '1'
- controller.assert_routing edit_member_path, :action => 'edit', :id => '1'
- controller.assert_routing "#{member_path}.xml", :action => 'show', :id => '1', :format => 'xml'
- controller.assert_routing formatted_edit_member_path, :action => 'edit', :id => '1', :format => 'xml'
+ controller.assert_routing member_path, action: "show", id: "1"
+ controller.assert_routing edit_member_path, action: "edit", id: "1"
+ controller.assert_routing "#{member_path}.xml", action: "show", id: "1", format: "xml"
+ controller.assert_routing formatted_edit_member_path, action: "edit", id: "1", format: "xml"
end
- assert_recognizes(options[:options].merge(:action => 'index'), :path => collection_path, :method => :get)
- assert_recognizes(options[:options].merge(:action => 'new'), :path => new_path, :method => :get)
- assert_recognizes(options[:options].merge(:action => 'create'), :path => collection_path, :method => :post)
- assert_recognizes(options[:shallow_options].merge(:action => 'show', :id => '1'), :path => member_path, :method => :get)
- assert_recognizes(options[:shallow_options].merge(:action => 'edit', :id => '1'), :path => edit_member_path, :method => :get)
- assert_recognizes(options[:shallow_options].merge(:action => 'update', :id => '1'), :path => member_path, :method => :put)
- assert_recognizes(options[:shallow_options].merge(:action => 'destroy', :id => '1'), :path => member_path, :method => :delete)
+ assert_recognizes(route_options.merge(action: "index"), path: collection_path, method: :get)
+ assert_recognizes(route_options.merge(action: "new"), path: new_path, method: :get)
+ assert_recognizes(route_options.merge(action: "create"), path: collection_path, method: :post)
+ assert_recognizes(options[:shallow_options].merge(action: "show", id: "1"), path: member_path, method: :get)
+ assert_recognizes(options[:shallow_options].merge(action: "edit", id: "1"), path: edit_member_path, method: :get)
+ assert_recognizes(options[:shallow_options].merge(action: "update", id: "1"), path: member_path, method: :put)
+ assert_recognizes(options[:shallow_options].merge(action: "destroy", id: "1"), path: member_path, method: :delete)
- assert_recognizes(options[:options].merge(:action => 'index', :format => 'xml'), :path => "#{collection_path}.xml", :method => :get)
- assert_recognizes(options[:options].merge(:action => 'new', :format => 'xml'), :path => "#{new_path}.xml", :method => :get)
- assert_recognizes(options[:options].merge(:action => 'create', :format => 'xml'), :path => "#{collection_path}.xml", :method => :post)
- assert_recognizes(options[:shallow_options].merge(:action => 'show', :id => '1', :format => 'xml'), :path => "#{member_path}.xml", :method => :get)
- assert_recognizes(options[:shallow_options].merge(:action => 'edit', :id => '1', :format => 'xml'), :path => formatted_edit_member_path, :method => :get)
- assert_recognizes(options[:shallow_options].merge(:action => 'update', :id => '1', :format => 'xml'), :path => "#{member_path}.xml", :method => :put)
- assert_recognizes(options[:shallow_options].merge(:action => 'destroy', :id => '1', :format => 'xml'), :path => "#{member_path}.xml", :method => :delete)
+ assert_recognizes(route_options.merge(action: "index", format: "xml"), path: "#{collection_path}.xml", method: :get)
+ assert_recognizes(route_options.merge(action: "new", format: "xml"), path: "#{new_path}.xml", method: :get)
+ assert_recognizes(route_options.merge(action: "create", format: "xml"), path: "#{collection_path}.xml", method: :post)
+ assert_recognizes(options[:shallow_options].merge(action: "show", id: "1", format: "xml"), path: "#{member_path}.xml", method: :get)
+ assert_recognizes(options[:shallow_options].merge(action: "edit", id: "1", format: "xml"), path: formatted_edit_member_path, method: :get)
+ assert_recognizes(options[:shallow_options].merge(action: "update", id: "1", format: "xml"), path: "#{member_path}.xml", method: :put)
+ assert_recognizes(options[:shallow_options].merge(action: "destroy", id: "1", format: "xml"), path: "#{member_path}.xml", method: :delete)
- yield options[:options] if block_given?
+ yield route_options if block_given?
end
# test named routes like foo_path and foos_path map to the correct options.
@@ -1217,26 +1232,26 @@ class ResourcesTest < ActionController::TestCase
end
singular_name ||= controller_name.to_s.singularize
- options[:options] ||= {}
- options[:options][:controller] = options[:controller] || controller_name.to_s
+ route_options = (options[:options] ||= {}).dup
+ route_options[:controller] = options[:controller] || controller_name.to_s
if options[:shallow]
options[:shallow_options] ||= {}
- options[:shallow_options][:controller] = options[:options][:controller]
+ options[:shallow_options][:controller] = route_options[:controller]
else
- options[:shallow_options] = options[:options]
+ options[:shallow_options] = route_options
end
- @controller = "#{options[:options][:controller].camelize}Controller".constantize.new
+ @controller = "#{route_options[:controller].camelize}Controller".constantize.new
@controller.singleton_class.include(@routes.url_helpers)
- get :index, params: options[:options]
- options[:options].delete :action
+ get :index, params: route_options
+ route_options.delete :action
path = "#{options[:as] || controller_name}"
shallow_path = "/#{options[:shallow] ? options[:namespace] : options[:path_prefix]}#{path}"
full_path = "/#{options[:path_prefix]}#{path}"
name_prefix = options[:name_prefix]
- shallow_prefix = options[:shallow] ? options[:namespace].try(:gsub, /\//, '_') : options[:name_prefix]
+ shallow_prefix = options[:shallow] ? options[:namespace].try(:gsub, /\//, "_") : options[:name_prefix]
new_action = "new"
edit_action = "edit"
@@ -1245,82 +1260,83 @@ class ResourcesTest < ActionController::TestCase
edit_action = options[:path_names][:edit] || "edit"
end
- assert_named_route "#{full_path}", "#{name_prefix}#{controller_name}_path", options[:options]
- assert_named_route "#{full_path}.xml", "#{name_prefix}#{controller_name}_path", options[:options].merge(:format => 'xml')
- assert_named_route "#{shallow_path}/1", "#{shallow_prefix}#{singular_name}_path", options[:shallow_options].merge(:id => '1')
- assert_named_route "#{shallow_path}/1.xml", "#{shallow_prefix}#{singular_name}_path", options[:shallow_options].merge(:id => '1', :format => 'xml')
+ assert_named_route "#{full_path}", "#{name_prefix}#{controller_name}_path", route_options
+ assert_named_route "#{full_path}.xml", "#{name_prefix}#{controller_name}_path", route_options.merge(format: "xml")
+ assert_named_route "#{shallow_path}/1", "#{shallow_prefix}#{singular_name}_path", options[:shallow_options].merge(id: "1")
+ assert_named_route "#{shallow_path}/1.xml", "#{shallow_prefix}#{singular_name}_path", options[:shallow_options].merge(id: "1", format: "xml")
- assert_named_route "#{full_path}/#{new_action}", "new_#{name_prefix}#{singular_name}_path", options[:options]
- assert_named_route "#{full_path}/#{new_action}.xml", "new_#{name_prefix}#{singular_name}_path", options[:options].merge(:format => 'xml')
- assert_named_route "#{shallow_path}/1/#{edit_action}", "edit_#{shallow_prefix}#{singular_name}_path", options[:shallow_options].merge(:id => '1')
- assert_named_route "#{shallow_path}/1/#{edit_action}.xml", "edit_#{shallow_prefix}#{singular_name}_path", options[:shallow_options].merge(:id => '1', :format => 'xml')
+ assert_named_route "#{full_path}/#{new_action}", "new_#{name_prefix}#{singular_name}_path", route_options
+ assert_named_route "#{full_path}/#{new_action}.xml", "new_#{name_prefix}#{singular_name}_path", route_options.merge(format: "xml")
+ assert_named_route "#{shallow_path}/1/#{edit_action}", "edit_#{shallow_prefix}#{singular_name}_path", options[:shallow_options].merge(id: "1")
+ assert_named_route "#{shallow_path}/1/#{edit_action}.xml", "edit_#{shallow_prefix}#{singular_name}_path", options[:shallow_options].merge(id: "1", format: "xml")
- yield options[:options] if block_given?
+ yield route_options if block_given?
end
def assert_singleton_routes_for(singleton_name, options = {})
- options[:options] ||= {}
- options[:options][:controller] = options[:controller] || singleton_name.to_s.pluralize
+ route_options = (options[:options] ||= {}).dup
+ route_options[:controller] = options[:controller] || singleton_name.to_s.pluralize
full_path = "/#{options[:path_prefix]}#{options[:as] || singleton_name}"
new_path = "#{full_path}/new"
edit_path = "#{full_path}/edit"
formatted_edit_path = "#{full_path}/edit.xml"
- with_options options[:options] do |controller|
- controller.assert_routing full_path, :action => 'show'
- controller.assert_routing new_path, :action => 'new'
- controller.assert_routing edit_path, :action => 'edit'
- controller.assert_routing "#{full_path}.xml", :action => 'show', :format => 'xml'
- controller.assert_routing "#{new_path}.xml", :action => 'new', :format => 'xml'
- controller.assert_routing formatted_edit_path, :action => 'edit', :format => 'xml'
+ with_options route_options do |controller|
+ controller.assert_routing full_path, action: "show"
+ controller.assert_routing new_path, action: "new"
+ controller.assert_routing edit_path, action: "edit"
+ controller.assert_routing "#{full_path}.xml", action: "show", format: "xml"
+ controller.assert_routing "#{new_path}.xml", action: "new", format: "xml"
+ controller.assert_routing formatted_edit_path, action: "edit", format: "xml"
end
- assert_recognizes(options[:options].merge(:action => 'show'), :path => full_path, :method => :get)
- assert_recognizes(options[:options].merge(:action => 'new'), :path => new_path, :method => :get)
- assert_recognizes(options[:options].merge(:action => 'edit'), :path => edit_path, :method => :get)
- assert_recognizes(options[:options].merge(:action => 'create'), :path => full_path, :method => :post)
- assert_recognizes(options[:options].merge(:action => 'update'), :path => full_path, :method => :put)
- assert_recognizes(options[:options].merge(:action => 'destroy'), :path => full_path, :method => :delete)
+ assert_recognizes(route_options.merge(action: "show"), path: full_path, method: :get)
+ assert_recognizes(route_options.merge(action: "new"), path: new_path, method: :get)
+ assert_recognizes(route_options.merge(action: "edit"), path: edit_path, method: :get)
+ assert_recognizes(route_options.merge(action: "create"), path: full_path, method: :post)
+ assert_recognizes(route_options.merge(action: "update"), path: full_path, method: :put)
+ assert_recognizes(route_options.merge(action: "destroy"), path: full_path, method: :delete)
- assert_recognizes(options[:options].merge(:action => 'show', :format => 'xml'), :path => "#{full_path}.xml", :method => :get)
- assert_recognizes(options[:options].merge(:action => 'new', :format => 'xml'), :path => "#{new_path}.xml", :method => :get)
- assert_recognizes(options[:options].merge(:action => 'edit', :format => 'xml'), :path => formatted_edit_path, :method => :get)
- assert_recognizes(options[:options].merge(:action => 'create', :format => 'xml'), :path => "#{full_path}.xml", :method => :post)
- assert_recognizes(options[:options].merge(:action => 'update', :format => 'xml'), :path => "#{full_path}.xml", :method => :put)
- assert_recognizes(options[:options].merge(:action => 'destroy', :format => 'xml'), :path => "#{full_path}.xml", :method => :delete)
+ assert_recognizes(route_options.merge(action: "show", format: "xml"), path: "#{full_path}.xml", method: :get)
+ assert_recognizes(route_options.merge(action: "new", format: "xml"), path: "#{new_path}.xml", method: :get)
+ assert_recognizes(route_options.merge(action: "edit", format: "xml"), path: formatted_edit_path, method: :get)
+ assert_recognizes(route_options.merge(action: "create", format: "xml"), path: "#{full_path}.xml", method: :post)
+ assert_recognizes(route_options.merge(action: "update", format: "xml"), path: "#{full_path}.xml", method: :put)
+ assert_recognizes(route_options.merge(action: "destroy", format: "xml"), path: "#{full_path}.xml", method: :delete)
- yield options[:options] if block_given?
+ yield route_options if block_given?
end
def assert_singleton_named_routes_for(singleton_name, options = {})
- (options[:options] ||= {})[:controller] ||= singleton_name.to_s.pluralize
- @controller = "#{options[:options][:controller].camelize}Controller".constantize.new
+ route_options = (options[:options] ||= {}).dup
+ controller_name = route_options[:controller] || options[:controller] || singleton_name.to_s.pluralize
+ @controller = "#{controller_name.camelize}Controller".constantize.new
@controller.singleton_class.include(@routes.url_helpers)
- get :show, params: options[:options]
- options[:options].delete :action
+ get :show, params: route_options
+ route_options.delete :action
full_path = "/#{options[:path_prefix]}#{options[:as] || singleton_name}"
name_prefix = options[:name_prefix]
- assert_named_route "#{full_path}", "#{name_prefix}#{singleton_name}_path", options[:options]
- assert_named_route "#{full_path}.xml", "#{name_prefix}#{singleton_name}_path", options[:options].merge(:format => 'xml')
+ assert_named_route "#{full_path}", "#{name_prefix}#{singleton_name}_path", route_options
+ assert_named_route "#{full_path}.xml", "#{name_prefix}#{singleton_name}_path", route_options.merge(format: "xml")
- assert_named_route "#{full_path}/new", "new_#{name_prefix}#{singleton_name}_path", options[:options]
- assert_named_route "#{full_path}/new.xml", "new_#{name_prefix}#{singleton_name}_path", options[:options].merge(:format => 'xml')
- assert_named_route "#{full_path}/edit", "edit_#{name_prefix}#{singleton_name}_path", options[:options]
- assert_named_route "#{full_path}/edit.xml", "edit_#{name_prefix}#{singleton_name}_path", options[:options].merge(:format => 'xml')
+ assert_named_route "#{full_path}/new", "new_#{name_prefix}#{singleton_name}_path", route_options
+ assert_named_route "#{full_path}/new.xml", "new_#{name_prefix}#{singleton_name}_path", route_options.merge(format: "xml")
+ assert_named_route "#{full_path}/edit", "edit_#{name_prefix}#{singleton_name}_path", route_options
+ assert_named_route "#{full_path}/edit.xml", "edit_#{name_prefix}#{singleton_name}_path", route_options.merge(format: "xml")
end
def assert_named_route(expected, route, options)
- actual = @controller.send(route, options) rescue $!.class.name
+ actual = @controller.send(route, options) rescue $!.class.name
assert_equal expected, actual, "Error on route: #{route}(#{options.inspect})"
end
def assert_resource_methods(expected, resource, action_method, method)
assert_equal expected.length, resource.send("#{action_method}_methods")[method].size, "#{resource.send("#{action_method}_methods")[method].inspect}"
expected.each do |action|
- assert resource.send("#{action_method}_methods")[method].include?(action)
+ assert_includes resource.send("#{action_method}_methods")[method], action,
"#{method} not in #{action_method} methods: #{resource.send("#{action_method}_methods")[method].inspect}"
end
end
@@ -1328,41 +1344,41 @@ class ResourcesTest < ActionController::TestCase
def assert_resource_allowed_routes(controller, options, shallow_options, allowed, not_allowed, path = controller)
shallow_path = "#{path}/#{shallow_options[:id]}"
format = options[:format] && ".#{options[:format]}"
- options.merge!(:controller => controller)
+ options[:controller] = controller
shallow_options.merge!(options)
- assert_whether_allowed(allowed, not_allowed, options, 'index', "#{path}#{format}", :get)
- assert_whether_allowed(allowed, not_allowed, options, 'new', "#{path}/new#{format}", :get)
- assert_whether_allowed(allowed, not_allowed, options, 'create', "#{path}#{format}", :post)
- assert_whether_allowed(allowed, not_allowed, shallow_options, 'show', "#{shallow_path}#{format}", :get)
- assert_whether_allowed(allowed, not_allowed, shallow_options, 'edit', "#{shallow_path}/edit#{format}", :get)
- assert_whether_allowed(allowed, not_allowed, shallow_options, 'update', "#{shallow_path}#{format}", :put)
- assert_whether_allowed(allowed, not_allowed, shallow_options, 'destroy', "#{shallow_path}#{format}", :delete)
+ assert_whether_allowed(allowed, not_allowed, options, "index", "#{path}#{format}", :get)
+ assert_whether_allowed(allowed, not_allowed, options, "new", "#{path}/new#{format}", :get)
+ assert_whether_allowed(allowed, not_allowed, options, "create", "#{path}#{format}", :post)
+ assert_whether_allowed(allowed, not_allowed, shallow_options, "show", "#{shallow_path}#{format}", :get)
+ assert_whether_allowed(allowed, not_allowed, shallow_options, "edit", "#{shallow_path}/edit#{format}", :get)
+ assert_whether_allowed(allowed, not_allowed, shallow_options, "update", "#{shallow_path}#{format}", :put)
+ assert_whether_allowed(allowed, not_allowed, shallow_options, "destroy", "#{shallow_path}#{format}", :delete)
end
def assert_singleton_resource_allowed_routes(controller, options, allowed, not_allowed, path = controller.singularize)
format = options[:format] && ".#{options[:format]}"
- options.merge!(:controller => controller)
+ options[:controller] = controller
- assert_whether_allowed(allowed, not_allowed, options, 'new', "#{path}/new#{format}", :get)
- assert_whether_allowed(allowed, not_allowed, options, 'create', "#{path}#{format}", :post)
- assert_whether_allowed(allowed, not_allowed, options, 'show', "#{path}#{format}", :get)
- assert_whether_allowed(allowed, not_allowed, options, 'edit', "#{path}/edit#{format}", :get)
- assert_whether_allowed(allowed, not_allowed, options, 'update', "#{path}#{format}", :put)
- assert_whether_allowed(allowed, not_allowed, options, 'destroy', "#{path}#{format}", :delete)
+ assert_whether_allowed(allowed, not_allowed, options, "new", "#{path}/new#{format}", :get)
+ assert_whether_allowed(allowed, not_allowed, options, "create", "#{path}#{format}", :post)
+ assert_whether_allowed(allowed, not_allowed, options, "show", "#{path}#{format}", :get)
+ assert_whether_allowed(allowed, not_allowed, options, "edit", "#{path}/edit#{format}", :get)
+ assert_whether_allowed(allowed, not_allowed, options, "update", "#{path}#{format}", :put)
+ assert_whether_allowed(allowed, not_allowed, options, "destroy", "#{path}#{format}", :delete)
end
def assert_whether_allowed(allowed, not_allowed, options, action, path, method)
action = action.to_sym
- options = options.merge(:action => action.to_s)
- path_options = { :path => path, :method => method }
+ options = options.merge(action: action.to_s)
+ path_options = { path: path, method: method }
if Array(allowed).include?(action)
assert_recognizes options, path_options
elsif Array(not_allowed).include?(action)
assert_not_recognizes options, path_options
else
- raise Assertion, 'Invalid Action has passed'
+ raise Assertion, "Invalid Action has passed"
end
end
diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb
index 9bbfb74e72..30f2a23b33 100644
--- a/actionpack/test/controller/routing_test.rb
+++ b/actionpack/test/controller/routing_test.rb
@@ -1,7 +1,9 @@
-require 'abstract_unit'
-require 'controller/fake_controllers'
-require 'active_support/core_ext/object/with_options'
-require 'active_support/core_ext/object/json'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "controller/fake_controllers"
+require "active_support/core_ext/object/with_options"
+require "active_support/core_ext/object/json"
class MilestonesController < ActionController::Base
def index() head :ok end
@@ -15,42 +17,42 @@ class UriReservedCharactersRoutingTest < ActiveSupport::TestCase
def setup
@set = ActionDispatch::Routing::RouteSet.new
@set.draw do
- get ':controller/:action/:variable/*additional'
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:variable/*additional"
+ end
end
safe, unsafe = %w(: @ & = + $ , ;), %w(^ ? # [ ])
- hex = unsafe.map { |char| '%' + char.unpack('H2').first.upcase }
+ hex = unsafe.map { |char| "%" + char.unpack1("H2").upcase }
- @segment = "#{safe.join}#{unsafe.join}".freeze
- @escaped = "#{safe.join}#{hex.join}".freeze
+ @segment = "#{safe.join}#{unsafe.join}"
+ @escaped = "#{safe.join}#{hex.join}"
end
def test_route_generation_escapes_unsafe_path_characters
assert_equal "/content/act#{@escaped}ion/var#{@escaped}iable/add#{@escaped}itional-1/add#{@escaped}itional-2",
- url_for(@set, {
- :controller => "content",
- :action => "act#{@segment}ion",
- :variable => "var#{@segment}iable",
- :additional => ["add#{@segment}itional-1", "add#{@segment}itional-2"]
- })
+ url_for(@set,
+ controller: "content",
+ action: "act#{@segment}ion",
+ variable: "var#{@segment}iable",
+ additional: ["add#{@segment}itional-1", "add#{@segment}itional-2"])
end
def test_route_recognition_unescapes_path_components
- options = { :controller => "content",
- :action => "act#{@segment}ion",
- :variable => "var#{@segment}iable",
- :additional => "add#{@segment}itional-1/add#{@segment}itional-2" }
+ options = { controller: "content",
+ action: "act#{@segment}ion",
+ variable: "var#{@segment}iable",
+ additional: "add#{@segment}itional-1/add#{@segment}itional-2" }
assert_equal options, @set.recognize_path("/content/act#{@escaped}ion/var#{@escaped}iable/add#{@escaped}itional-1/add#{@escaped}itional-2")
end
def test_route_generation_allows_passing_non_string_values_to_generated_helper
assert_equal "/content/action/variable/1/2",
- url_for(@set, {
- :controller => "content",
- :action => "action",
- :variable => "variable",
- :additional => [1, 2]
- })
+ url_for(@set,
+ controller: "content",
+ action: "action",
+ variable: "variable",
+ additional: [1, 2])
end
end
@@ -84,106 +86,122 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_symbols_with_dashes
rs.draw do
- get '/:artist/:song-omg', :to => lambda { |env|
+ get "/:artist/:song-omg", to: lambda { |env|
resp = ActiveSupport::JSON.encode ActionDispatch::Request.new(env).path_parameters
[200, {}, [resp]]
}
end
- hash = ActiveSupport::JSON.decode get(URI('http://example.org/journey/faithfully-omg'))
- assert_equal({"artist"=>"journey", "song"=>"faithfully"}, hash)
+ hash = ActiveSupport::JSON.decode get(URI("http://example.org/journey/faithfully-omg"))
+ assert_equal({ "artist" => "journey", "song" => "faithfully" }, hash)
+ end
+
+ def test_id_encoding
+ rs.draw do
+ get "/journey/:id", to: lambda { |env|
+ param = ActionDispatch::Request.new(env).path_parameters
+ resp = ActiveSupport::JSON.encode param
+ [200, {}, [resp]]
+ }
+ end
+
+ # The encoding of the URL in production is *binary*, so we add a
+ # .b here.
+ hash = ActiveSupport::JSON.decode get(URI("http://example.org/journey/%E5%A4%AA%E9%83%8E".b))
+ assert_equal({ "id" => "太郎" }, hash)
+ assert_equal ::Encoding::UTF_8, hash["id"].encoding
end
def test_id_with_dash
rs.draw do
- get '/journey/:id', :to => lambda { |env|
+ get "/journey/:id", to: lambda { |env|
resp = ActiveSupport::JSON.encode ActionDispatch::Request.new(env).path_parameters
[200, {}, [resp]]
}
end
- hash = ActiveSupport::JSON.decode get(URI('http://example.org/journey/faithfully-omg'))
- assert_equal({"id"=>"faithfully-omg"}, hash)
+ hash = ActiveSupport::JSON.decode get(URI("http://example.org/journey/faithfully-omg"))
+ assert_equal({ "id" => "faithfully-omg" }, hash)
end
def test_dash_with_custom_regexp
rs.draw do
- get '/:artist/:song-omg', :constraints => { :song => /\d+/ }, :to => lambda { |env|
+ get "/:artist/:song-omg", constraints: { song: /\d+/ }, to: lambda { |env|
resp = ActiveSupport::JSON.encode ActionDispatch::Request.new(env).path_parameters
[200, {}, [resp]]
}
end
- hash = ActiveSupport::JSON.decode get(URI('http://example.org/journey/123-omg'))
- assert_equal({"artist"=>"journey", "song"=>"123"}, hash)
- assert_equal 'Not Found', get(URI('http://example.org/journey/faithfully-omg'))
+ hash = ActiveSupport::JSON.decode get(URI("http://example.org/journey/123-omg"))
+ assert_equal({ "artist" => "journey", "song" => "123" }, hash)
+ assert_equal "Not Found", get(URI("http://example.org/journey/faithfully-omg"))
end
def test_pre_dash
rs.draw do
- get '/:artist/omg-:song', :to => lambda { |env|
+ get "/:artist/omg-:song", to: lambda { |env|
resp = ActiveSupport::JSON.encode ActionDispatch::Request.new(env).path_parameters
[200, {}, [resp]]
}
end
- hash = ActiveSupport::JSON.decode get(URI('http://example.org/journey/omg-faithfully'))
- assert_equal({"artist"=>"journey", "song"=>"faithfully"}, hash)
+ hash = ActiveSupport::JSON.decode get(URI("http://example.org/journey/omg-faithfully"))
+ assert_equal({ "artist" => "journey", "song" => "faithfully" }, hash)
end
def test_pre_dash_with_custom_regexp
rs.draw do
- get '/:artist/omg-:song', :constraints => { :song => /\d+/ }, :to => lambda { |env|
+ get "/:artist/omg-:song", constraints: { song: /\d+/ }, to: lambda { |env|
resp = ActiveSupport::JSON.encode ActionDispatch::Request.new(env).path_parameters
[200, {}, [resp]]
}
end
- hash = ActiveSupport::JSON.decode get(URI('http://example.org/journey/omg-123'))
- assert_equal({"artist"=>"journey", "song"=>"123"}, hash)
- assert_equal 'Not Found', get(URI('http://example.org/journey/omg-faithfully'))
+ hash = ActiveSupport::JSON.decode get(URI("http://example.org/journey/omg-123"))
+ assert_equal({ "artist" => "journey", "song" => "123" }, hash)
+ assert_equal "Not Found", get(URI("http://example.org/journey/omg-faithfully"))
end
def test_star_paths_are_greedy
rs.draw do
- get "/*path", :to => lambda { |env|
+ get "/*path", to: lambda { |env|
x = env["action_dispatch.request.path_parameters"][:path]
[200, {}, [x]]
- }, :format => false
+ }, format: false
end
- u = URI('http://example.org/foo/bar.html')
- assert_equal u.path.sub(/^\//, ''), get(u)
+ u = URI("http://example.org/foo/bar.html")
+ assert_equal u.path.sub(/^\//, ""), get(u)
end
def test_star_paths_are_greedy_but_not_too_much
rs.draw do
- get "/*path", :to => lambda { |env|
+ get "/*path", to: lambda { |env|
x = ActiveSupport::JSON.encode env["action_dispatch.request.path_parameters"]
[200, {}, [x]]
}
end
expected = { "path" => "foo/bar", "format" => "html" }
- u = URI('http://example.org/foo/bar.html')
+ u = URI("http://example.org/foo/bar.html")
assert_equal expected, ActiveSupport::JSON.decode(get(u))
end
def test_optional_star_paths_are_greedy
rs.draw do
- get "/(*filters)", :to => lambda { |env|
+ get "/(*filters)", to: lambda { |env|
x = env["action_dispatch.request.path_parameters"][:filters]
[200, {}, [x]]
- }, :format => false
+ }, format: false
end
- u = URI('http://example.org/ne_27.065938,-80.6092/sw_25.489856,-82.542794')
- assert_equal u.path.sub(/^\//, ''), get(u)
+ u = URI("http://example.org/ne_27.065938,-80.6092/sw_25.489856,-82.542794")
+ assert_equal u.path.sub(/^\//, ""), get(u)
end
def test_optional_star_paths_are_greedy_but_not_too_much
rs.draw do
- get "/(*filters)", :to => lambda { |env|
+ get "/(*filters)", to: lambda { |env|
x = ActiveSupport::JSON.encode env["action_dispatch.request.path_parameters"]
[200, {}, [x]]
}
@@ -191,65 +209,65 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
expected = { "filters" => "ne_27.065938,-80.6092/sw_25.489856,-82",
"format" => "542794" }
- u = URI('http://example.org/ne_27.065938,-80.6092/sw_25.489856,-82.542794')
+ u = URI("http://example.org/ne_27.065938,-80.6092/sw_25.489856,-82.542794")
assert_equal expected, ActiveSupport::JSON.decode(get(u))
end
- def test_regexp_precidence
+ def test_regexp_precedence
rs.draw do
- get '/whois/:domain', :constraints => {
- :domain => /\w+\.[\w\.]+/ },
- :to => lambda { |env| [200, {}, %w{regexp}] }
+ get "/whois/:domain", constraints: {
+ domain: /\w+\.[\w\.]+/ },
+ to: lambda { |env| [200, {}, %w{regexp}] }
- get '/whois/:id', :to => lambda { |env| [200, {}, %w{id}] }
+ get "/whois/:id", to: lambda { |env| [200, {}, %w{id}] }
end
- assert_equal 'regexp', get(URI('http://example.org/whois/example.org'))
- assert_equal 'id', get(URI('http://example.org/whois/123'))
+ assert_equal "regexp", get(URI("http://example.org/whois/example.org"))
+ assert_equal "id", get(URI("http://example.org/whois/123"))
end
def test_class_and_lambda_constraints
subdomain = Class.new {
- def matches? request
- request.subdomain.present? and request.subdomain != 'clients'
+ def matches?(request)
+ request.subdomain.present? && request.subdomain != "clients"
end
}
rs.draw do
- get '/', :constraints => subdomain.new,
- :to => lambda { |env| [200, {}, %w{default}] }
- get '/', :constraints => { :subdomain => 'clients' },
- :to => lambda { |env| [200, {}, %w{clients}] }
+ get "/", constraints: subdomain.new,
+ to: lambda { |env| [200, {}, %w{default}] }
+ get "/", constraints: { subdomain: "clients" },
+ to: lambda { |env| [200, {}, %w{clients}] }
end
- assert_equal 'default', get(URI('http://www.example.org/'))
- assert_equal 'clients', get(URI('http://clients.example.org/'))
+ assert_equal "default", get(URI("http://www.example.org/"))
+ assert_equal "clients", get(URI("http://clients.example.org/"))
end
def test_lambda_constraints
rs.draw do
- get '/', :constraints => lambda { |req|
- req.subdomain.present? and req.subdomain != "clients" },
- :to => lambda { |env| [200, {}, %w{default}] }
+ get "/", constraints: lambda { |req|
+ req.subdomain.present? && req.subdomain != "clients" },
+ to: lambda { |env| [200, {}, %w{default}] }
- get '/', :constraints => lambda { |req|
+ get "/", constraints: lambda { |req|
req.subdomain.present? && req.subdomain == "clients" },
- :to => lambda { |env| [200, {}, %w{clients}] }
+ to: lambda { |env| [200, {}, %w{clients}] }
end
- assert_equal 'default', get(URI('http://www.example.org/'))
- assert_equal 'clients', get(URI('http://clients.example.org/'))
+ assert_equal "default", get(URI("http://www.example.org/"))
+ assert_equal "clients", get(URI("http://clients.example.org/"))
end
def test_scoped_lambda
scope_called = false
rs.draw do
- scope '/foo', :constraints => lambda { |req| scope_called = true } do
- get '/', :to => lambda { |env| [200, {}, %w{default}] }
+ scope "/foo", constraints: lambda { |req| scope_called = true } do
+ get "/", to: lambda { |env| [200, {}, %w{default}] }
end
end
- assert_equal 'default', get(URI('http://www.example.org/foo/'))
+ assert_equal "default", get(URI("http://www.example.org/foo/"))
assert scope_called, "scope constraint should be called"
end
@@ -257,165 +275,174 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
inner_called = false
rs.draw do
- scope '/foo', :constraints => lambda { |req| flunk "should not be called" } do
- get '/', :constraints => lambda { |req| inner_called = true },
- :to => lambda { |env| [200, {}, %w{default}] }
+ scope "/foo", constraints: lambda { |req| flunk "should not be called" } do
+ get "/", constraints: lambda { |req| inner_called = true },
+ to: lambda { |env| [200, {}, %w{default}] }
end
end
- assert_equal 'default', get(URI('http://www.example.org/foo/'))
+ assert_equal "default", get(URI("http://www.example.org/foo/"))
assert inner_called, "inner constraint should be called"
end
def test_empty_string_match
rs.draw do
- get '/:username', :constraints => { :username => /[^\/]+/ },
- :to => lambda { |e| [200, {}, ['foo']] }
+ get "/:username", constraints: { username: /[^\/]+/ },
+ to: lambda { |e| [200, {}, ["foo"]] }
end
- assert_equal 'Not Found', get(URI('http://example.org/'))
- assert_equal 'foo', get(URI('http://example.org/hello'))
+ assert_equal "Not Found", get(URI("http://example.org/"))
+ assert_equal "foo", get(URI("http://example.org/hello"))
end
def test_non_greedy_glob_regexp
params = nil
rs.draw do
- get '/posts/:id(/*filters)', :constraints => { :filters => /.+?/ },
- :to => lambda { |e|
+ get "/posts/:id(/*filters)", constraints: { filters: /.+?/ },
+ to: lambda { |e|
params = e["action_dispatch.request.path_parameters"]
- [200, {}, ['foo']]
+ [200, {}, ["foo"]]
}
end
- assert_equal 'foo', get(URI('http://example.org/posts/1/foo.js'))
- assert_equal({:id=>"1", :filters=>"foo", :format=>"js"}, params)
- end
-
- def test_draw_with_block_arity_one_raises
- assert_raise(RuntimeError) do
- rs.draw { |map| map.match '/:controller(/:action(/:id))' }
- end
+ assert_equal "foo", get(URI("http://example.org/posts/1/foo.js"))
+ assert_equal({ id: "1", filters: "foo", format: "js" }, params)
end
def test_specific_controller_action_failure
rs.draw do
- mount lambda {} => "/foo"
+ mount lambda { } => "/foo"
end
assert_raises(ActionController::UrlGenerationError) do
- url_for(rs, :controller => "omg", :action => "lol")
+ url_for(rs, controller: "omg", action: "lol")
end
end
def test_default_setup
- rs.draw { get '/:controller(/:action(/:id))' }
- assert_equal({:controller => "content", :action => 'index'}, rs.recognize_path("/content"))
- assert_equal({:controller => "content", :action => 'list'}, rs.recognize_path("/content/list"))
- assert_equal({:controller => "content", :action => 'show', :id => '10'}, rs.recognize_path("/content/show/10"))
+ rs.draw { ActiveSupport::Deprecation.silence { get "/:controller(/:action(/:id))" } }
+ assert_equal({ controller: "content", action: "index" }, rs.recognize_path("/content"))
+ assert_equal({ controller: "content", action: "list" }, rs.recognize_path("/content/list"))
+ assert_equal({ controller: "content", action: "show", id: "10" }, rs.recognize_path("/content/show/10"))
- assert_equal({:controller => "admin/user", :action => 'show', :id => '10'}, rs.recognize_path("/admin/user/show/10"))
+ assert_equal({ controller: "admin/user", action: "show", id: "10" }, rs.recognize_path("/admin/user/show/10"))
- assert_equal '/admin/user/show/10', url_for(rs, { :controller => 'admin/user', :action => 'show', :id => 10 })
+ assert_equal "/admin/user/show/10", url_for(rs, controller: "admin/user", action: "show", id: 10)
- get URI('http://test.host/admin/user/list/10')
+ get URI("http://test.host/admin/user/list/10")
- assert_equal({ :controller => 'admin/user', :action => 'list', :id => '10' },
+ assert_equal({ controller: "admin/user", action: "list", id: "10" },
controller.request.path_parameters)
- assert_equal '/admin/user/show', controller.url_for({ :action => 'show', :only_path => true })
- assert_equal '/admin/user/list/10', controller.url_for({:only_path => true})
+ assert_equal "/admin/user/show", controller.url_for(action: "show", only_path: true)
+ assert_equal "/admin/user/list/10", controller.url_for(only_path: true)
- assert_equal '/admin/stuff', controller.url_for({ :controller => 'stuff', :only_path => true })
- assert_equal '/stuff', controller.url_for({ :controller => '/stuff', :only_path => true })
- end
-
- def test_ignores_leading_slash
- rs.clear!
- rs.draw { get '/:controller(/:action(/:id))'}
- test_default_setup
+ assert_equal "/admin/stuff", controller.url_for(controller: "stuff", only_path: true)
+ assert_equal "/stuff", controller.url_for(controller: "/stuff", only_path: true)
end
def test_route_with_colon_first
rs.draw do
- get '/:controller/:action/:id', :action => 'index', :id => nil
- get ':url', :controller => 'tiny_url', :action => 'translate'
+ ActiveSupport::Deprecation.silence do
+ get "/:controller/:action/:id", action: "index", id: nil
+ end
+
+ get ":url", controller: "content", action: "translate"
end
+
+ assert_equal({ controller: "content", action: "translate", url: "example" }, rs.recognize_path("/example"))
+ end
+
+ def test_route_with_regexp_for_action
+ rs.draw { ActiveSupport::Deprecation.silence { get "/:controller/:action", action: /auth[-|_].+/ } }
+
+ assert_equal({ action: "auth_google", controller: "content" }, rs.recognize_path("/content/auth_google"))
+ assert_equal({ action: "auth-facebook", controller: "content" }, rs.recognize_path("/content/auth-facebook"))
+
+ assert_equal "/content/auth_google", url_for(rs, controller: "content", action: "auth_google")
+ assert_equal "/content/auth-facebook", url_for(rs, controller: "content", action: "auth-facebook")
end
def test_route_with_regexp_for_controller
rs.draw do
- get ':controller/:admintoken(/:action(/:id))', :controller => /admin\/.+/
- get '/:controller(/:action(/:id))'
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:admintoken(/:action(/:id))", controller: /admin\/.+/
+ get "/:controller(/:action(/:id))"
+ end
end
- assert_equal({:controller => "admin/user", :admintoken => "foo", :action => "index"},
+ assert_equal({ controller: "admin/user", admintoken: "foo", action: "index" },
rs.recognize_path("/admin/user/foo"))
- assert_equal({:controller => "content", :action => "foo"},
+ assert_equal({ controller: "content", action: "foo" },
rs.recognize_path("/content/foo"))
- assert_equal '/admin/user/foo', url_for(rs, { :controller => "admin/user", :admintoken => "foo", :action => "index" })
- assert_equal '/content/foo', url_for(rs, { :controller => "content", :action => "foo" })
+ assert_equal "/admin/user/foo", url_for(rs, controller: "admin/user", admintoken: "foo", action: "index")
+ assert_equal "/content/foo", url_for(rs, controller: "content", action: "foo")
end
def test_route_with_regexp_and_captures_for_controller
rs.draw do
- get '/:controller(/:action(/:id))', :controller => /admin\/(accounts|users)/
+ ActiveSupport::Deprecation.silence do
+ get "/:controller(/:action(/:id))", controller: /admin\/(accounts|users)/
+ end
end
- assert_equal({:controller => "admin/accounts", :action => "index"}, rs.recognize_path("/admin/accounts"))
- assert_equal({:controller => "admin/users", :action => "index"}, rs.recognize_path("/admin/users"))
+ assert_equal({ controller: "admin/accounts", action: "index" }, rs.recognize_path("/admin/accounts"))
+ assert_equal({ controller: "admin/users", action: "index" }, rs.recognize_path("/admin/users"))
assert_raise(ActionController::RoutingError) { rs.recognize_path("/admin/products") }
end
def test_route_with_regexp_and_dot
rs.draw do
- get ':controller/:action/:file',
- :controller => /admin|user/,
- :action => /upload|download/,
- :defaults => {:file => nil},
- :constraints => {:file => %r{[^/]+(\.[^/]+)?}}
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:file",
+ controller: /admin|user/,
+ action: /upload|download/,
+ defaults: { file: nil },
+ constraints: { file: %r{[^/]+(\.[^/]+)?} }
+ end
end
# Without a file extension
- assert_equal '/user/download/file',
- url_for(rs, { :controller => "user", :action => "download", :file => "file" })
+ assert_equal "/user/download/file",
+ url_for(rs, controller: "user", action: "download", file: "file")
- assert_equal({:controller => "user", :action => "download", :file => "file"},
+ assert_equal({ controller: "user", action: "download", file: "file" },
rs.recognize_path("/user/download/file"))
# Now, let's try a file with an extension, really a dot (.)
- assert_equal '/user/download/file.jpg',
- url_for(rs, { :controller => "user", :action => "download", :file => "file.jpg" })
+ assert_equal "/user/download/file.jpg",
+ url_for(rs, controller: "user", action: "download", file: "file.jpg")
- assert_equal({:controller => "user", :action => "download", :file => "file.jpg"},
+ assert_equal({ controller: "user", action: "download", file: "file.jpg" },
rs.recognize_path("/user/download/file.jpg"))
end
def test_basic_named_route
rs.draw do
- root :to => 'content#list', :as => 'home'
+ root to: "content#list", as: "home"
end
assert_equal("http://test.host/", setup_for_named_route.send(:home_url))
end
def test_named_route_with_option
rs.draw do
- get 'page/:title' => 'content#show_page', :as => 'page'
+ get "page/:title" => "content#show_page", :as => "page"
end
assert_equal("http://test.host/page/new%20stuff",
- setup_for_named_route.send(:page_url, :title => 'new stuff'))
+ setup_for_named_route.send(:page_url, title: "new stuff"))
end
def test_named_route_with_default
rs.draw do
- get 'page/:title' => 'content#show_page', :title => 'AboutPage', :as => 'page'
+ get "page/:title" => "content#show_page", :title => "AboutPage", :as => "page"
end
assert_equal("http://test.host/page/AboutRails",
- setup_for_named_route.send(:page_url, :title => "AboutRails"))
+ setup_for_named_route.send(:page_url, title: "AboutRails"))
end
def test_named_route_with_path_prefix
rs.draw do
scope "my" do
- get 'page' => 'content#show_page', :as => 'page'
+ get "page" => "content#show_page", :as => "page"
end
end
@@ -426,7 +453,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_named_route_with_blank_path_prefix
rs.draw do
scope "" do
- get 'page' => 'content#show_page', :as => 'page'
+ get "page" => "content#show_page", :as => "page"
end
end
@@ -436,7 +463,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_named_route_with_nested_controller
rs.draw do
- get 'admin/user' => 'admin/user#index', :as => "users"
+ get "admin/user" => "admin/user#index", :as => "users"
end
assert_equal("http://test.host/admin/user",
@@ -445,7 +472,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_optimised_named_route_with_host
rs.draw do
- get 'page' => 'content#show_page', :as => 'pages', :host => 'foo.com'
+ get "page" => "content#show_page", :as => "pages", :host => "foo.com"
end
routes = setup_for_named_route
assert_equal "http://foo.com/page", routes.pages_url
@@ -457,13 +484,15 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_named_route_without_hash
rs.draw do
- get ':controller/:action/:id', :as => 'normal'
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:id", as: "normal"
+ end
end
end
def test_named_route_root
rs.draw do
- root :to => "hello#index"
+ root to: "hello#index"
end
routes = setup_for_named_route
assert_equal("http://test.host/", routes.send(:root_url))
@@ -507,224 +536,249 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_named_route_with_regexps
rs.draw do
- get 'page/:year/:month/:day/:title' => 'page#show', :as => 'article',
+ get "page/:year/:month/:day/:title" => "page#show", :as => "article",
:year => /\d+/, :month => /\d+/, :day => /\d+/
- get ':controller/:action/:id'
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:id"
+ end
end
routes = setup_for_named_route
assert_equal "http://test.host/page/2005/6/10/hi",
- routes.send(:article_url, :title => 'hi', :day => 10, :year => 2005, :month => 6)
+ routes.send(:article_url, title: "hi", day: 10, year: 2005, month: 6)
end
def test_changing_controller
- rs.draw { get ':controller/:action/:id' }
+ rs.draw { ActiveSupport::Deprecation.silence { get ":controller/:action/:id" } }
- get URI('http://test.host/admin/user/index/10')
+ get URI("http://test.host/admin/user/index/10")
- assert_equal '/admin/stuff/show/10',
- controller.url_for({:controller => 'stuff', :action => 'show', :id => 10, :only_path => true})
+ assert_equal "/admin/stuff/show/10",
+ controller.url_for(controller: "stuff", action: "show", id: 10, only_path: true)
end
def test_paths_escaped
rs.draw do
- get 'file/*path' => 'content#show_file', :as => 'path'
- get ':controller/:action/:id'
+ get "file/*path" => "content#show_file", :as => "path"
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:id"
+ end
end
# No + to space in URI escaping, only for query params.
results = rs.recognize_path "/file/hello+world/how+are+you%3F"
assert results, "Recognition should have succeeded"
- assert_equal 'hello+world/how+are+you?', results[:path]
+ assert_equal "hello+world/how+are+you?", results[:path]
# Use %20 for space instead.
results = rs.recognize_path "/file/hello%20world/how%20are%20you%3F"
assert results, "Recognition should have succeeded"
- assert_equal 'hello world/how are you?', results[:path]
+ assert_equal "hello world/how are you?", results[:path]
end
def test_paths_slashes_unescaped_with_ordered_parameters
rs.draw do
- get '/file/*path' => 'content#index', :as => 'path'
+ get "/file/*path" => "content#index", :as => "path"
end
# No / to %2F in URI, only for query params.
- assert_equal("/file/hello/world", setup_for_named_route.send(:path_path, ['hello', 'world']))
+ assert_equal("/file/hello/world", setup_for_named_route.send(:path_path, ["hello", "world"]))
end
def test_non_controllers_cannot_be_matched
rs.draw do
- get ':controller/:action/:id'
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:id"
+ end
end
assert_raise(ActionController::RoutingError) { rs.recognize_path("/not_a/show/10") }
end
def test_should_list_options_diff_when_routing_constraints_dont_match
rs.draw do
- get 'post/:id' => 'post#show', :constraints => { :id => /\d+/ }, :as => 'post'
+ get "post/:id" => "post#show", :constraints => { id: /\d+/ }, :as => "post"
end
assert_raise(ActionController::UrlGenerationError) do
- url_for(rs, { :controller => 'post', :action => 'show', :bad_param => "foo", :use_route => "post" })
+ url_for(rs, controller: "post", action: "show", bad_param: "foo", use_route: "post")
end
end
def test_dynamic_path_allowed
rs.draw do
- get '*path' => 'content#show_file'
+ get "*path" => "content#show_file"
end
- assert_equal '/pages/boo',
- url_for(rs, { :controller => 'content', :action => 'show_file', :path => %w(pages boo) })
+ assert_equal "/pages/boo",
+ url_for(rs, controller: "content", action: "show_file", path: %w(pages boo))
end
def test_dynamic_recall_paths_allowed
rs.draw do
- get '*path' => 'content#show_file'
+ get "*path" => "content#show_file"
end
- get URI('http://test.host/pages/boo')
- assert_equal({:controller=>"content", :action=>"show_file", :path=>"pages/boo"},
+ get URI("http://test.host/pages/boo")
+ assert_equal({ controller: "content", action: "show_file", path: "pages/boo" },
controller.request.path_parameters)
- assert_equal '/pages/boo',
- controller.url_for(:only_path => true)
+ assert_equal "/pages/boo",
+ controller.url_for(only_path: true)
end
def test_backwards
rs.draw do
- get 'page/:id(/:action)' => 'pages#show'
- get ':controller(/:action(/:id))'
+ ActiveSupport::Deprecation.silence do
+ get "page/:id(/:action)" => "pages#show"
+ get ":controller(/:action(/:id))"
+ end
end
- get URI('http://test.host/pages/show')
- assert_equal '/page/20', controller.url_for({ :id => 20, :only_path => true })
- assert_equal '/page/20', url_for(rs, { :controller => 'pages', :id => 20, :action => 'show' })
- assert_equal '/pages/boo', url_for(rs, { :controller => 'pages', :action => 'boo' })
+ get URI("http://test.host/pages/show")
+ assert_equal "/page/20", controller.url_for(id: 20, only_path: true)
+ assert_equal "/page/20", url_for(rs, controller: "pages", id: 20, action: "show")
+ assert_equal "/pages/boo", url_for(rs, controller: "pages", action: "boo")
end
- def test_route_with_fixnum_default
+ def test_route_with_integer_default
rs.draw do
- get 'page(/:id)' => 'content#show_page', :id => 1
- get ':controller/:action/:id'
+ get "page(/:id)" => "content#show_page", :id => 1
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:id"
+ end
end
- assert_equal '/page', url_for(rs, { :controller => 'content', :action => 'show_page' })
- assert_equal '/page', url_for(rs, { :controller => 'content', :action => 'show_page', :id => 1 })
- assert_equal '/page', url_for(rs, { :controller => 'content', :action => 'show_page', :id => '1' })
- assert_equal '/page/10', url_for(rs, { :controller => 'content', :action => 'show_page', :id => 10 })
+ assert_equal "/page", url_for(rs, controller: "content", action: "show_page")
+ assert_equal "/page", url_for(rs, controller: "content", action: "show_page", id: 1)
+ assert_equal "/page", url_for(rs, controller: "content", action: "show_page", id: "1")
+ assert_equal "/page/10", url_for(rs, controller: "content", action: "show_page", id: 10)
- assert_equal({:controller => "content", :action => 'show_page', :id => 1 }, rs.recognize_path("/page"))
- assert_equal({:controller => "content", :action => 'show_page', :id => '1'}, rs.recognize_path("/page/1"))
- assert_equal({:controller => "content", :action => 'show_page', :id => '10'}, rs.recognize_path("/page/10"))
+ assert_equal({ controller: "content", action: "show_page", id: 1 }, rs.recognize_path("/page"))
+ assert_equal({ controller: "content", action: "show_page", id: "1" }, rs.recognize_path("/page/1"))
+ assert_equal({ controller: "content", action: "show_page", id: "10" }, rs.recognize_path("/page/10"))
end
# For newer revision
def test_route_with_text_default
rs.draw do
- get 'page/:id' => 'content#show_page', :id => 1
- get ':controller/:action/:id'
+ get "page/:id" => "content#show_page", :id => 1
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:id"
+ end
end
- assert_equal '/page/foo', url_for(rs, { :controller => 'content', :action => 'show_page', :id => 'foo' })
- assert_equal({ :controller => "content", :action => 'show_page', :id => 'foo' }, rs.recognize_path("/page/foo"))
+ assert_equal "/page/foo", url_for(rs, controller: "content", action: "show_page", id: "foo")
+ assert_equal({ controller: "content", action: "show_page", id: "foo" }, rs.recognize_path("/page/foo"))
- token = "\321\202\320\265\320\272\321\201\321\202" # 'text' in Russian
+ token = +"\321\202\320\265\320\272\321\201\321\202" # 'text' in Russian
token.force_encoding(Encoding::BINARY)
- escaped_token = CGI::escape(token)
+ escaped_token = CGI.escape(token)
- assert_equal '/page/' + escaped_token, url_for(rs, { :controller => 'content', :action => 'show_page', :id => token })
- assert_equal({ :controller => "content", :action => 'show_page', :id => token }, rs.recognize_path("/page/#{escaped_token}"))
+ assert_equal "/page/" + escaped_token, url_for(rs, controller: "content", action: "show_page", id: token)
+ assert_equal({ controller: "content", action: "show_page", id: token }, rs.recognize_path("/page/#{escaped_token}"))
end
def test_action_expiry
- rs.draw { get ':controller(/:action(/:id))' }
- get URI('http://test.host/content/show')
- assert_equal '/content', controller.url_for(:controller => 'content', :only_path => true)
+ rs.draw { ActiveSupport::Deprecation.silence { get ":controller(/:action(/:id))" } }
+ get URI("http://test.host/content/show")
+ assert_equal "/content", controller.url_for(controller: "content", only_path: true)
end
def test_requirement_should_prevent_optional_id
rs.draw do
- get 'post/:id' => 'post#show', :constraints => {:id => /\d+/}, :as => 'post'
+ get "post/:id" => "post#show", :constraints => { id: /\d+/ }, :as => "post"
end
- assert_equal '/post/10', url_for(rs, { :controller => 'post', :action => 'show', :id => 10 })
+ assert_equal "/post/10", url_for(rs, controller: "post", action: "show", id: 10)
assert_raise(ActionController::UrlGenerationError) do
- url_for(rs, { :controller => 'post', :action => 'show' })
+ url_for(rs, controller: "post", action: "show")
end
end
def test_both_requirement_and_optional
rs.draw do
- get('test(/:year)' => 'post#show', :as => 'blog',
- :defaults => { :year => nil },
- :constraints => { :year => /\d{4}/ }
+ get("test(/:year)" => "post#show", :as => "blog",
+ :defaults => { year: nil },
+ :constraints => { year: /\d{4}/ }
)
- get ':controller/:action/:id'
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:id"
+ end
end
- assert_equal '/test', url_for(rs, { :controller => 'post', :action => 'show' })
- assert_equal '/test', url_for(rs, { :controller => 'post', :action => 'show', :year => nil })
+ assert_equal "/test", url_for(rs, controller: "post", action: "show")
+ assert_equal "/test", url_for(rs, controller: "post", action: "show", year: nil)
assert_equal("http://test.host/test", setup_for_named_route.send(:blog_url))
end
def test_set_to_nil_forgets
rs.draw do
- get 'pages(/:year(/:month(/:day)))' => 'content#list_pages', :month => nil, :day => nil
- get ':controller/:action/:id'
+ get "pages(/:year(/:month(/:day)))" => "content#list_pages", :month => nil, :day => nil
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:id"
+ end
end
- assert_equal '/pages/2005',
- url_for(rs, { :controller => 'content', :action => 'list_pages', :year => 2005 })
- assert_equal '/pages/2005/6',
- url_for(rs, { :controller => 'content', :action => 'list_pages', :year => 2005, :month => 6 })
- assert_equal '/pages/2005/6/12',
- url_for(rs, { :controller => 'content', :action => 'list_pages', :year => 2005, :month => 6, :day => 12 })
+ assert_equal "/pages/2005",
+ url_for(rs, controller: "content", action: "list_pages", year: 2005)
+ assert_equal "/pages/2005/6",
+ url_for(rs, controller: "content", action: "list_pages", year: 2005, month: 6)
+ assert_equal "/pages/2005/6/12",
+ url_for(rs, controller: "content", action: "list_pages", year: 2005, month: 6, day: 12)
- get URI('http://test.host/pages/2005/6/12')
- assert_equal({ :controller => 'content', :action => 'list_pages', :year => '2005', :month => '6', :day => '12' },
+ get URI("http://test.host/pages/2005/6/12")
+ assert_equal({ controller: "content", action: "list_pages", year: "2005", month: "6", day: "12" },
controller.request.path_parameters)
- assert_equal '/pages/2005/6/4',
- controller.url_for({ :day => 4, :only_path => true })
+ assert_equal "/pages/2005/6/4",
+ controller.url_for(day: 4, only_path: true)
- assert_equal '/pages/2005/6',
- controller.url_for({ :day => nil, :only_path => true })
+ assert_equal "/pages/2005/6",
+ controller.url_for(day: nil, only_path: true)
- assert_equal '/pages/2005',
- controller.url_for({ :day => nil, :month => nil, :only_path => true })
+ assert_equal "/pages/2005",
+ controller.url_for(day: nil, month: nil, only_path: true)
end
def test_root_url_generation_with_controller_and_action
rs.draw do
- root :to => "content#index"
+ root to: "content#index"
end
- assert_equal '/', url_for(rs, { :controller => 'content', :action => 'index' })
- assert_equal '/', url_for(rs, { :controller => 'content' })
+ assert_equal "/", url_for(rs, controller: "content", action: "index")
+ assert_equal "/", url_for(rs, controller: "content")
end
def test_named_root_url_generation_with_controller_and_action
rs.draw do
- root :to => "content#index", :as => 'home'
+ root to: "content#index", as: "home"
end
- assert_equal '/', url_for(rs, { :controller => 'content', :action => 'index' })
- assert_equal '/', url_for(rs, { :controller => 'content' })
+ assert_equal "/", url_for(rs, controller: "content", action: "index")
+ assert_equal "/", url_for(rs, controller: "content")
assert_equal("http://test.host/", setup_for_named_route.send(:home_url))
end
def test_named_route_method
rs.draw do
- get 'categories' => 'content#categories', :as => 'categories'
- get ':controller(/:action(/:id))'
+ get "categories" => "content#categories", :as => "categories"
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller(/:action(/:id))"
+ end
end
- assert_equal '/categories', url_for(rs, { :controller => 'content', :action => 'categories' })
- assert_equal '/content/hi', url_for(rs, { :controller => 'content', :action => 'hi' })
+ assert_equal "/categories", url_for(rs, controller: "content", action: "categories")
+ assert_equal "/content/hi", url_for(rs, controller: "content", action: "hi")
end
def test_named_routes_array
@@ -734,52 +788,56 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_nil_defaults
rs.draw do
- get 'journal' => 'content#list_journal',
+ get "journal" => "content#list_journal",
:date => nil, :user_id => nil
- get ':controller/:action/:id'
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:id"
+ end
end
- assert_equal '/journal', url_for(rs, {
- :controller => 'content',
- :action => 'list_journal',
- :date => nil,
- :user_id => nil
- })
+ assert_equal "/journal", url_for(rs,
+ controller: "content",
+ action: "list_journal",
+ date: nil,
+ user_id: nil)
end
def setup_request_method_routes_for(method)
rs.draw do
- match '/match' => "books##{method}", :via => method.to_sym
+ match "/match" => "books##{method}", :via => method.to_sym
end
end
%w(GET PATCH POST PUT DELETE).each do |request_method|
define_method("test_request_method_recognized_with_#{request_method}") do
setup_request_method_routes_for(request_method.downcase)
- params = rs.recognize_path("/match", :method => request_method)
+ params = rs.recognize_path("/match", method: request_method)
assert_equal request_method.downcase, params[:action]
end
end
def test_recognize_array_of_methods
rs.draw do
- match '/match' => 'books#get_or_post', :via => [:get, :post]
- put '/match' => 'books#not_get_or_post'
+ match "/match" => "books#get_or_post", :via => [:get, :post]
+ put "/match" => "books#not_get_or_post"
end
- params = rs.recognize_path("/match", :method => :post)
- assert_equal 'get_or_post', params[:action]
+ params = rs.recognize_path("/match", method: :post)
+ assert_equal "get_or_post", params[:action]
- params = rs.recognize_path("/match", :method => :put)
- assert_equal 'not_get_or_post', params[:action]
+ params = rs.recognize_path("/match", method: :put)
+ assert_equal "not_get_or_post", params[:action]
end
def test_subpath_recognized
rs.draw do
- get '/books/:id/edit' => 'subpath_books#edit'
- get '/items/:id/:action' => 'subpath_books'
- get '/posts/new/:action' => 'subpath_books'
- get '/posts/:id' => 'subpath_books#show'
+ ActiveSupport::Deprecation.silence do
+ get "/books/:id/edit" => "subpath_books#edit"
+ get "/items/:id/:action" => "subpath_books"
+ get "/posts/new/:action" => "subpath_books"
+ get "/posts/:id" => "subpath_books#show"
+ end
end
hash = rs.recognize_path "/books/17/edit"
@@ -801,19 +859,21 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_subpath_generated
rs.draw do
- get '/books/:id/edit' => 'subpath_books#edit'
- get '/items/:id/:action' => 'subpath_books'
- get '/posts/new/:action' => 'subpath_books'
+ ActiveSupport::Deprecation.silence do
+ get "/books/:id/edit" => "subpath_books#edit"
+ get "/items/:id/:action" => "subpath_books"
+ get "/posts/new/:action" => "subpath_books"
+ end
end
- assert_equal "/books/7/edit", url_for(rs, { :controller => "subpath_books", :id => 7, :action => "edit" })
- assert_equal "/items/15/complete", url_for(rs, { :controller => "subpath_books", :id => 15, :action => "complete" })
- assert_equal "/posts/new/preview", url_for(rs, { :controller => "subpath_books", :action => "preview" })
+ assert_equal "/books/7/edit", url_for(rs, controller: "subpath_books", id: 7, action: "edit")
+ assert_equal "/items/15/complete", url_for(rs, controller: "subpath_books", id: 15, action: "complete")
+ assert_equal "/posts/new/preview", url_for(rs, controller: "subpath_books", action: "preview")
end
def test_failed_constraints_raises_exception_with_violated_constraints
rs.draw do
- get 'foos/:id' => 'foos#show', :as => 'foo_with_requirement', :constraints => { :id => /\d+/ }
+ get "foos/:id" => "foos#show", :as => "foo_with_requirement", :constraints => { id: /\d+/ }
end
assert_raise(ActionController::UrlGenerationError) do
@@ -824,11 +884,14 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_routes_changed_correctly_after_clear
rs = ::ActionDispatch::Routing::RouteSet.new
rs.draw do
- get 'ca' => 'ca#aa'
- get 'cb' => 'cb#ab'
- get 'cc' => 'cc#ac'
- get ':controller/:action/:id'
- get ':controller/:action/:id.:format'
+ get "ca" => "ca#aa"
+ get "cb" => "cb#ab"
+ get "cc" => "cc#ac"
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:id"
+ get ":controller/:action/:id.:format"
+ end
end
hash = rs.recognize_path "/cc"
@@ -837,10 +900,13 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
assert_equal %w(cc ac), [hash[:controller], hash[:action]]
rs.draw do
- get 'cb' => 'cb#ab'
- get 'cc' => 'cc#ac'
- get ':controller/:action/:id'
- get ':controller/:action/:id.:format'
+ get "cb" => "cb#ab"
+ get "cc" => "cc#ac"
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:id"
+ get ":controller/:action/:id.:format"
+ end
end
hash = rs.recognize_path "/cc"
@@ -871,57 +937,65 @@ class RouteSetTest < ActiveSupport::TestCase
@default_route_set ||= begin
set = ActionDispatch::Routing::RouteSet.new
set.draw do
- get '/:controller(/:action(/:id))'
+ ActiveSupport::Deprecation.silence do
+ get "/:controller(/:action(/:id))"
+ end
end
set
end
end
def test_generate_extras
- set.draw { get ':controller/(:action(/:id))' }
- path, extras = set.generate_extras(:controller => "foo", :action => "bar", :id => 15, :this => "hello", :that => "world")
+ set.draw { ActiveSupport::Deprecation.silence { get ":controller/(:action(/:id))" } }
+ path, extras = set.generate_extras(controller: "foo", action: "bar", id: 15, this: "hello", that: "world")
assert_equal "/foo/bar/15", path
assert_equal %w(that this), extras.map(&:to_s).sort
end
def test_extra_keys
- set.draw { get ':controller/:action/:id' }
- extras = set.extra_keys(:controller => "foo", :action => "bar", :id => 15, :this => "hello", :that => "world")
+ set.draw { ActiveSupport::Deprecation.silence { get ":controller/:action/:id" } }
+ extras = set.extra_keys(controller: "foo", action: "bar", id: 15, this: "hello", that: "world")
assert_equal %w(that this), extras.map(&:to_s).sort
end
def test_generate_extras_not_first
set.draw do
- get ':controller/:action/:id.:format'
- get ':controller/:action/:id'
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:id.:format"
+ get ":controller/:action/:id"
+ end
end
- path, extras = set.generate_extras(:controller => "foo", :action => "bar", :id => 15, :this => "hello", :that => "world")
+ path, extras = set.generate_extras(controller: "foo", action: "bar", id: 15, this: "hello", that: "world")
assert_equal "/foo/bar/15", path
assert_equal %w(that this), extras.map(&:to_s).sort
end
def test_generate_not_first
set.draw do
- get ':controller/:action/:id.:format'
- get ':controller/:action/:id'
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:id.:format"
+ get ":controller/:action/:id"
+ end
end
assert_equal "/foo/bar/15?this=hello",
- url_for(set, { :controller => "foo", :action => "bar", :id => 15, :this => "hello" })
+ url_for(set, controller: "foo", action: "bar", id: 15, this: "hello")
end
def test_extra_keys_not_first
set.draw do
- get ':controller/:action/:id.:format'
- get ':controller/:action/:id'
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:id.:format"
+ get ":controller/:action/:id"
+ end
end
- extras = set.extra_keys(:controller => "foo", :action => "bar", :id => 15, :this => "hello", :that => "world")
+ extras = set.extra_keys(controller: "foo", action: "bar", id: 15, this: "hello", that: "world")
assert_equal %w(that this), extras.map(&:to_s).sort
end
def test_draw
assert_equal 0, set.routes.size
set.draw do
- get '/hello/world' => 'a#b'
+ get "/hello/world" => "a#b"
end
assert_equal 1, set.routes.size
end
@@ -929,16 +1003,16 @@ class RouteSetTest < ActiveSupport::TestCase
def test_draw_symbol_controller_name
assert_equal 0, set.routes.size
set.draw do
- get '/users/index' => 'users#index'
+ get "/users/index" => "users#index"
end
- set.recognize_path('/users/index', :method => :get)
+ set.recognize_path("/users/index", method: :get)
assert_equal 1, set.routes.size
end
def test_named_draw
assert_equal 0, set.routes.size
set.draw do
- get '/hello/world' => 'a#b', :as => 'hello'
+ get "/hello/world" => "a#b", :as => "hello"
end
assert_equal 1, set.routes.size
assert_equal set.routes.first, set.named_routes[:hello]
@@ -947,57 +1021,57 @@ class RouteSetTest < ActiveSupport::TestCase
def test_duplicate_named_route_raises_rather_than_pick_precedence
assert_raise ArgumentError do
set.draw do
- get '/hello/world' => 'a#b', :as => 'hello'
- get '/hello' => 'a#b', :as => 'hello'
+ get "/hello/world" => "a#b", :as => "hello"
+ get "/hello" => "a#b", :as => "hello"
end
end
end
def setup_named_route_test
set.draw do
- get '/people(/:id)' => 'people#show', :as => 'show'
- get '/people' => 'people#index', :as => 'index'
- get '/people/go/:foo/:bar/joe(/:id)' => 'people#multi', :as => 'multi'
- get '/admin/users' => 'admin/users#index', :as => "users"
+ get "/people(/:id)" => "people#show", :as => "show"
+ get "/people" => "people#index", :as => "index"
+ get "/people/go/:foo/:bar/joe(/:id)" => "people#multi", :as => "multi"
+ get "/admin/users" => "admin/users#index", :as => "users"
end
- get URI('http://test.host/people')
+ get URI("http://test.host/people")
controller
end
def test_named_route_url_method
controller = setup_named_route_test
- assert_equal "http://test.host/people/5", controller.send(:show_url, :id => 5)
- assert_equal "/people/5", controller.send(:show_path, :id => 5)
+ assert_equal "http://test.host/people/5", controller.send(:show_url, id: 5)
+ assert_equal "/people/5", controller.send(:show_path, id: 5)
assert_equal "http://test.host/people", controller.send(:index_url)
assert_equal "/people", controller.send(:index_path)
assert_equal "http://test.host/admin/users", controller.send(:users_url)
- assert_equal '/admin/users', controller.send(:users_path)
+ assert_equal "/admin/users", controller.send(:users_path)
end
def test_named_route_url_method_with_anchor
controller = setup_named_route_test
- assert_equal "http://test.host/people/5#location", controller.send(:show_url, :id => 5, :anchor => 'location')
- assert_equal "/people/5#location", controller.send(:show_path, :id => 5, :anchor => 'location')
+ assert_equal "http://test.host/people/5#location", controller.send(:show_url, id: 5, anchor: "location")
+ assert_equal "/people/5#location", controller.send(:show_path, id: 5, anchor: "location")
- assert_equal "http://test.host/people#location", controller.send(:index_url, :anchor => 'location')
- assert_equal "/people#location", controller.send(:index_path, :anchor => 'location')
+ assert_equal "http://test.host/people#location", controller.send(:index_url, anchor: "location")
+ assert_equal "/people#location", controller.send(:index_path, anchor: "location")
- assert_equal "http://test.host/admin/users#location", controller.send(:users_url, :anchor => 'location')
- assert_equal '/admin/users#location', controller.send(:users_path, :anchor => 'location')
+ assert_equal "http://test.host/admin/users#location", controller.send(:users_url, anchor: "location")
+ assert_equal "/admin/users#location", controller.send(:users_path, anchor: "location")
assert_equal "http://test.host/people/go/7/hello/joe/5#location",
- controller.send(:multi_url, 7, "hello", 5, :anchor => 'location')
+ controller.send(:multi_url, 7, "hello", 5, anchor: "location")
assert_equal "http://test.host/people/go/7/hello/joe/5?baz=bar#location",
- controller.send(:multi_url, 7, "hello", 5, :baz => "bar", :anchor => 'location')
+ controller.send(:multi_url, 7, "hello", 5, baz: "bar", anchor: "location")
assert_equal "http://test.host/people?baz=bar#location",
- controller.send(:index_url, :baz => "bar", :anchor => 'location')
+ controller.send(:index_url, baz: "bar", anchor: "location")
assert_equal "http://test.host/people", controller.send(:index_url, anchor: nil)
assert_equal "http://test.host/people", controller.send(:index_url, anchor: false)
@@ -1005,17 +1079,17 @@ class RouteSetTest < ActiveSupport::TestCase
def test_named_route_url_method_with_port
controller = setup_named_route_test
- assert_equal "http://test.host:8080/people/5", controller.send(:show_url, 5, :port=>8080)
+ assert_equal "http://test.host:8080/people/5", controller.send(:show_url, 5, port: 8080)
end
def test_named_route_url_method_with_host
controller = setup_named_route_test
- assert_equal "http://some.example.com/people/5", controller.send(:show_url, 5, :host=>"some.example.com")
+ assert_equal "http://some.example.com/people/5", controller.send(:show_url, 5, host: "some.example.com")
end
def test_named_route_url_method_with_protocol
controller = setup_named_route_test
- assert_equal "https://test.host/people/5", controller.send(:show_url, 5, :protocol => "https")
+ assert_equal "https://test.host/people/5", controller.send(:show_url, 5, protocol: "https")
end
def test_named_route_url_method_with_ordered_parameters
@@ -1027,7 +1101,7 @@ class RouteSetTest < ActiveSupport::TestCase
def test_named_route_url_method_with_ordered_parameters_and_hash
controller = setup_named_route_test
assert_equal "http://test.host/people/go/7/hello/joe/5?baz=bar",
- controller.send(:multi_url, 7, "hello", 5, :baz => "bar")
+ controller.send(:multi_url, 7, "hello", 5, baz: "bar")
end
def test_named_route_url_method_with_ordered_parameters_and_empty_hash
@@ -1039,41 +1113,46 @@ class RouteSetTest < ActiveSupport::TestCase
def test_named_route_url_method_with_no_positional_arguments
controller = setup_named_route_test
assert_equal "http://test.host/people?baz=bar",
- controller.send(:index_url, :baz => "bar")
+ controller.send(:index_url, baz: "bar")
end
def test_draw_default_route
set.draw do
- get '/:controller/:action/:id'
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:id"
+ end
end
assert_equal 1, set.routes.size
- assert_equal '/users/show/10', url_for(set, { :controller => 'users', :action => 'show', :id => 10 })
- assert_equal '/users/index/10', url_for(set, { :controller => 'users', :id => 10 })
+ assert_equal "/users/show/10", url_for(set, controller: "users", action: "show", id: 10)
+ assert_equal "/users/index/10", url_for(set, controller: "users", id: 10)
- assert_equal({:controller => 'users', :action => 'index', :id => '10'}, set.recognize_path('/users/index/10'))
- assert_equal({:controller => 'users', :action => 'index', :id => '10'}, set.recognize_path('/users/index/10/'))
+ assert_equal({ controller: "users", action: "index", id: "10" }, set.recognize_path("/users/index/10"))
+ assert_equal({ controller: "users", action: "index", id: "10" }, set.recognize_path("/users/index/10/"))
end
def test_route_with_parameter_shell
set.draw do
- get 'page/:id' => 'pages#show', :id => /\d+/
- get '/:controller(/:action(/:id))'
+ get "page/:id" => "pages#show", :id => /\d+/
+
+ ActiveSupport::Deprecation.silence do
+ get "/:controller(/:action(/:id))"
+ end
end
- assert_equal({:controller => 'pages', :action => 'index'}, request_path_params('/pages'))
- assert_equal({:controller => 'pages', :action => 'index'}, request_path_params('/pages/index'))
- assert_equal({:controller => 'pages', :action => 'list'}, request_path_params('/pages/list'))
+ assert_equal({ controller: "pages", action: "index" }, request_path_params("/pages"))
+ assert_equal({ controller: "pages", action: "index" }, request_path_params("/pages/index"))
+ assert_equal({ controller: "pages", action: "list" }, request_path_params("/pages/list"))
- assert_equal({:controller => 'pages', :action => 'show', :id => '10'}, request_path_params('/pages/show/10'))
- assert_equal({:controller => 'pages', :action => 'show', :id => '10'}, request_path_params('/page/10'))
+ assert_equal({ controller: "pages", action: "show", id: "10" }, request_path_params("/pages/show/10"))
+ assert_equal({ controller: "pages", action: "show", id: "10" }, request_path_params("/page/10"))
end
def test_route_constraints_on_request_object_with_anchors_are_valid
assert_nothing_raised do
set.draw do
- get 'page/:id' => 'pages#show', :constraints => { :host => /^foo$/ }
+ get "page/:id" => "pages#show", :constraints => { host: /^foo$/ }
end
end
end
@@ -1081,27 +1160,27 @@ class RouteSetTest < ActiveSupport::TestCase
def test_route_constraints_with_anchor_chars_are_invalid
assert_raise ArgumentError do
set.draw do
- get 'page/:id' => 'pages#show', :id => /^\d+/
+ get "page/:id" => "pages#show", :id => /^\d+/
end
end
assert_raise ArgumentError do
set.draw do
- get 'page/:id' => 'pages#show', :id => /\A\d+/
+ get "page/:id" => "pages#show", :id => /\A\d+/
end
end
assert_raise ArgumentError do
set.draw do
- get 'page/:id' => 'pages#show', :id => /\d+$/
+ get "page/:id" => "pages#show", :id => /\d+$/
end
end
assert_raise ArgumentError do
set.draw do
- get 'page/:id' => 'pages#show', :id => /\d+\Z/
+ get "page/:id" => "pages#show", :id => /\d+\Z/
end
end
assert_raise ArgumentError do
set.draw do
- get 'page/:id' => 'pages#show', :id => /\d+\z/
+ get "page/:id" => "pages#show", :id => /\d+\z/
end
end
end
@@ -1109,7 +1188,7 @@ class RouteSetTest < ActiveSupport::TestCase
def test_route_constraints_with_options_method_condition_is_valid
assert_nothing_raised do
set.draw do
- match 'valid/route' => 'pages#show', :via => :options
+ match "valid/route" => "pages#show", :via => :options
end
end
end
@@ -1119,16 +1198,16 @@ class RouteSetTest < ActiveSupport::TestCase
get "/people" => "missing#index"
end
- assert_raises(ActionController::RoutingError) { request_path_params '/people' }
+ assert_raises(ActionController::RoutingError) { request_path_params "/people" }
end
def test_recognize_with_encoded_id_and_regex
set.draw do
- get 'page/:id' => 'pages#show', :id => /[a-zA-Z0-9\+]+/
+ get "page/:id" => "pages#show", :id => /[a-zA-Z0-9\+]+/
end
- assert_equal({:controller => 'pages', :action => 'show', :id => '10'}, request_path_params('/page/10'))
- assert_equal({:controller => 'pages', :action => 'show', :id => 'hello+world'}, request_path_params('/page/hello+world'))
+ assert_equal({ controller: "pages", action: "show", id: "10" }, request_path_params("/page/10"))
+ assert_equal({ controller: "pages", action: "show", id: "hello+world" }, request_path_params("/page/hello+world"))
end
def test_recognize_with_http_methods
@@ -1141,65 +1220,65 @@ class RouteSetTest < ActiveSupport::TestCase
delete "/people/:id" => "people#destroy"
end
- params = request_path_params("/people", :method => :get)
+ params = request_path_params("/people", method: :get)
assert_equal("index", params[:action])
- params = request_path_params("/people", :method => :post)
+ params = request_path_params("/people", method: :post)
assert_equal("create", params[:action])
- params = request_path_params("/people/5", :method => :put)
+ params = request_path_params("/people/5", method: :put)
assert_equal("update", params[:action])
- params = request_path_params("/people/5", :method => :patch)
+ params = request_path_params("/people/5", method: :patch)
assert_equal("update", params[:action])
assert_raise(ActionController::UnknownHttpMethod) {
- request_path_params("/people", :method => :bacon)
+ request_path_params("/people", method: :bacon)
}
- params = request_path_params("/people/5", :method => :get)
+ params = request_path_params("/people/5", method: :get)
assert_equal("show", params[:action])
assert_equal("5", params[:id])
- params = request_path_params("/people/5", :method => :put)
+ params = request_path_params("/people/5", method: :put)
assert_equal("update", params[:action])
assert_equal("5", params[:id])
- params = request_path_params("/people/5", :method => :patch)
+ params = request_path_params("/people/5", method: :patch)
assert_equal("update", params[:action])
assert_equal("5", params[:id])
- params = request_path_params("/people/5", :method => :delete)
+ params = request_path_params("/people/5", method: :delete)
assert_equal("destroy", params[:action])
assert_equal("5", params[:id])
assert_raise(ActionController::RoutingError) {
- request_path_params("/people/5", :method => :post)
+ request_path_params("/people/5", method: :post)
}
end
def test_recognize_with_alias_in_conditions
set.draw do
- match "/people" => 'people#index', :as => 'people', :via => :get
- root :to => "people#index"
+ match "/people" => "people#index", :as => "people", :via => :get
+ root to: "people#index"
end
- params = request_path_params("/people", :method => :get)
+ params = request_path_params("/people", method: :get)
assert_equal("people", params[:controller])
assert_equal("index", params[:action])
- params = request_path_params("/", :method => :get)
+ params = request_path_params("/", method: :get)
assert_equal("people", params[:controller])
assert_equal("index", params[:action])
end
def test_typo_recognition
set.draw do
- get 'articles/:year/:month/:day/:title' => 'articles#permalink',
+ get "articles/:year/:month/:day/:title" => "articles#permalink",
:year => /\d{4}/, :day => /\d{1,2}/, :month => /\d{1,2}/
end
- params = request_path_params("/articles/2005/11/05/a-very-interesting-article", :method => :get)
+ params = request_path_params("/articles/2005/11/05/a-very-interesting-article", method: :get)
assert_equal("permalink", params[:action])
assert_equal("2005", params[:year])
assert_equal("11", params[:month])
@@ -1208,14 +1287,14 @@ class RouteSetTest < ActiveSupport::TestCase
end
def test_routing_traversal_does_not_load_extra_classes
- assert !Object.const_defined?("Profiler__"), "Profiler should not be loaded"
+ assert_not Object.const_defined?("Profiler__"), "Profiler should not be loaded"
set.draw do
- get '/profile' => 'profile#index'
+ get "/profile" => "profile#index"
end
request_path_params("/profile") rescue nil
- assert !Object.const_defined?("Profiler__"), "Profiler should not be loaded"
+ assert_not Object.const_defined?("Profiler__"), "Profiler should not be loaded"
end
def test_recognize_with_conditions_and_format
@@ -1226,17 +1305,17 @@ class RouteSetTest < ActiveSupport::TestCase
get "people/:id(.:format)" => "people#show"
end
- params = request_path_params("/people/5", :method => :get)
+ params = request_path_params("/people/5", method: :get)
assert_equal("show", params[:action])
assert_equal("5", params[:id])
- params = request_path_params("/people/5", :method => :put)
+ params = request_path_params("/people/5", method: :put)
assert_equal("update", params[:action])
- params = request_path_params("/people/5", :method => :patch)
+ params = request_path_params("/people/5", method: :patch)
assert_equal("update", params[:action])
- params = request_path_params("/people/5.png", :method => :get)
+ params = request_path_params("/people/5.png", method: :get)
assert_equal("show", params[:action])
assert_equal("5", params[:id])
assert_equal("png", params[:format])
@@ -1244,68 +1323,66 @@ class RouteSetTest < ActiveSupport::TestCase
def test_generate_with_default_action
set.draw do
- get "/people", :controller => "people", :action => "index"
- get "/people/list", :controller => "people", :action => "list"
+ get "/people", controller: "people", action: "index"
+ get "/people/list", controller: "people", action: "list"
end
- url = url_for(set, { :controller => "people", :action => "list" })
+ url = url_for(set, controller: "people", action: "list")
assert_equal "/people/list", url
end
def test_root_map
- set.draw { root :to => 'people#index' }
+ set.draw { root to: "people#index" }
- params = request_path_params("", :method => :get)
+ params = request_path_params("", method: :get)
assert_equal("people", params[:controller])
assert_equal("index", params[:action])
end
def test_namespace
set.draw do
-
- namespace 'api' do
- get 'inventory' => 'products#inventory'
+ namespace "api" do
+ get "inventory" => "products#inventory"
end
-
end
- params = request_path_params("/api/inventory", :method => :get)
+ params = request_path_params("/api/inventory", method: :get)
assert_equal("api/products", params[:controller])
assert_equal("inventory", params[:action])
end
def test_namespaced_root_map
set.draw do
- namespace 'api' do
- root :to => 'products#index'
+ namespace "api" do
+ root to: "products#index"
end
end
- params = request_path_params("/api", :method => :get)
+ params = request_path_params("/api", method: :get)
assert_equal("api/products", params[:controller])
assert_equal("index", params[:action])
end
def test_namespace_with_path_prefix
set.draw do
- scope :module => "api", :path => "prefix" do
- get 'inventory' => 'products#inventory'
+ scope module: "api", path: "prefix" do
+ get "inventory" => "products#inventory"
end
end
- params = request_path_params("/prefix/inventory", :method => :get)
+ params = request_path_params("/prefix/inventory", method: :get)
assert_equal("api/products", params[:controller])
assert_equal("inventory", params[:action])
end
def test_namespace_with_blank_path_prefix
set.draw do
- scope :module => "api", :path => "" do
- get 'inventory' => 'products#inventory'
+ scope module: "api", path: "" do
+ get "inventory" => "products#inventory"
end
end
- params = request_path_params("/inventory", :method => :get)
+ params = request_path_params("/inventory", method: :get)
assert_equal("api/products", params[:controller])
assert_equal("inventory", params[:action])
end
@@ -1314,33 +1391,38 @@ class RouteSetTest < ActiveSupport::TestCase
@set = make_set false
set.draw do
- get ':controller/:id/:action'
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:id/:action"
+ end
end
- get URI('http://test.host/people/7/show')
+ get URI("http://test.host/people/7/show")
- assert_equal "/people/7/destroy", controller.url_for(:action => 'destroy', :only_path => true)
+ assert_equal "/people/7/destroy", controller.url_for(action: "destroy", only_path: true)
end
def test_use_static_path_when_possible
@set = make_set false
set.draw do
- get 'about' => "welcome#about"
- get ':controller/:action/:id'
+ get "about" => "welcome#about"
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:id/:action"
+ end
end
- get URI('http://test.host/welcom/get/7')
+ get URI("http://test.host/welcom/get/7")
- assert_equal "/about", controller.url_for(:controller => 'welcome',
- :action => 'about',
- :only_path => true)
+ assert_equal "/about", controller.url_for(controller: "welcome",
+ action: "about",
+ only_path: true)
end
def test_generate
- set.draw { get ':controller/:action/:id' }
+ set.draw { ActiveSupport::Deprecation.silence { get ":controller/:action/:id" } }
- args = { :controller => "foo", :action => "bar", :id => "7", :x => "y" }
+ args = { controller: "foo", action: "bar", id: "7", x: "y" }
assert_equal "/foo/bar/7?x=y", url_for(set, args)
assert_equal ["/foo/bar/7", [:x]], set.generate_extras(args)
assert_equal [:x], set.extra_keys(args)
@@ -1349,22 +1431,26 @@ class RouteSetTest < ActiveSupport::TestCase
def test_generate_with_path_prefix
set.draw do
scope "my" do
- get ':controller(/:action(/:id))'
+ ActiveSupport::Deprecation.silence do
+ get ":controller(/:action(/:id))"
+ end
end
end
- args = { :controller => "foo", :action => "bar", :id => "7", :x => "y" }
+ args = { controller: "foo", action: "bar", id: "7", x: "y" }
assert_equal "/my/foo/bar/7?x=y", url_for(set, args)
end
def test_generate_with_blank_path_prefix
set.draw do
scope "" do
- get ':controller(/:action(/:id))'
+ ActiveSupport::Deprecation.silence do
+ get ":controller(/:action(/:id))"
+ end
end
end
- args = { :controller => "foo", :action => "bar", :id => "7", :x => "y" }
+ args = { controller: "foo", action: "bar", id: "7", x: "y" }
assert_equal "/foo/bar/7?x=y", url_for(set, args)
end
@@ -1372,19 +1458,21 @@ class RouteSetTest < ActiveSupport::TestCase
@set = make_set false
set.draw do
- get "/connection/manage(/:action)" => 'connection/manage#index'
- get "/connection/connection" => "connection/connection#index"
- get '/connection' => 'connection#index', :as => 'family_connection'
+ ActiveSupport::Deprecation.silence do
+ get "/connection/manage(/:action)" => "connection/manage#index"
+ get "/connection/connection" => "connection/connection#index"
+ get "/connection" => "connection#index", :as => "family_connection"
+ end
end
- assert_equal({ :controller => 'connection/manage',
- :action => 'index', }, request_path_params('/connection/manage'))
+ assert_equal({ controller: "connection/manage",
+ action: "index", }, request_path_params("/connection/manage"))
- url = controller.url_for({ :controller => "connection", :only_path => true })
+ url = controller.url_for(controller: "connection", only_path: true)
assert_equal "/connection/connection", url
- url = controller.url_for({ :use_route => "family_connection",
- :controller => "connection", :only_path => true })
+ url = controller.url_for(use_route: "family_connection",
+ controller: "connection", only_path: true)
assert_equal "/connection", url
end
@@ -1392,71 +1480,76 @@ class RouteSetTest < ActiveSupport::TestCase
@set = make_set false
set.draw do
- get ':controller(/:action(/:id))'
+ ActiveSupport::Deprecation.silence do
+ get ":controller(/:action(/:id))"
+ end
end
- get URI('http://test.host/books/show/10')
+ get URI("http://test.host/books/show/10")
- assert_equal '/books', controller.url_for(:controller => 'books',
- :only_path => true,
- :action => 'index')
+ assert_equal "/books", controller.url_for(controller: "books",
+ only_path: true,
+ action: "index")
end
def test_query_params_will_be_shown_when_recalled
@set = make_set false
set.draw do
- get 'show_weblog/:parameter' => 'weblog#show'
- get ':controller(/:action(/:id))'
+ get "show_weblog/:parameter" => "weblog#show"
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller(/:action(/:id))"
+ end
end
- get URI('http://test.host/weblog/show/1')
+ get URI("http://test.host/weblog/show/1")
- assert_equal '/weblog/edit?parameter=1', controller.url_for(
- {:action => 'edit', :parameter => 1, :only_path => true})
+ assert_equal "/weblog/edit?parameter=1", controller.url_for(
+ action: "edit", parameter: 1, only_path: true)
end
def test_format_is_not_inherit
set.draw do
- get '/posts(.:format)' => 'posts#index'
+ get "/posts(.:format)" => "posts#index"
end
- get URI('http://test.host/posts.xml')
- assert_equal({:controller => 'posts', :action => 'index', :format => 'xml'},
+ get URI("http://test.host/posts.xml")
+ assert_equal({ controller: "posts", action: "index", format: "xml" },
controller.request.path_parameters)
- assert_equal '/posts', controller.url_for(
- {:controller => 'posts', :only_path => true})
+ assert_equal "/posts", controller.url_for(
+ controller: "posts", only_path: true)
- assert_equal '/posts.xml', controller.url_for(
- {:controller => 'posts', :format => 'xml', :only_path => true})
+ assert_equal "/posts.xml", controller.url_for(
+ controller: "posts", format: "xml", only_path: true)
end
def test_expiry_determination_should_consider_values_with_to_param
@set = make_set false
- set.draw { get 'projects/:project_id/:controller/:action' }
+ set.draw { ActiveSupport::Deprecation.silence { get "projects/:project_id/:controller/:action" } }
- get URI('http://test.host/projects/1/weblog/show')
+ get URI("http://test.host/projects/1/weblog/show")
assert_equal(
- { :controller => 'weblog', :action => 'show', :project_id => '1' },
+ { controller: "weblog", action: "show", project_id: "1" },
controller.request.path_parameters)
- assert_equal '/projects/1/weblog/show',
- controller.url_for({ :action => 'show', :project_id => 1, :only_path => true })
+ assert_equal "/projects/1/weblog/show",
+ controller.url_for(action: "show", project_id: 1, only_path: true)
end
def test_named_route_in_nested_resource
set.draw do
resources :projects do
member do
- get 'milestones' => 'milestones#index', :as => 'milestones'
+ get "milestones" => "milestones#index", :as => "milestones"
end
end
end
- params = set.recognize_path("/projects/1/milestones", :method => :get)
+ params = set.recognize_path("/projects/1/milestones", method: :get)
assert_equal("milestones", params[:controller])
assert_equal("index", params[:action])
end
@@ -1465,7 +1558,7 @@ class RouteSetTest < ActiveSupport::TestCase
assert_nothing_raised do
set.draw do
namespace :admin do
- root :to => "home#index"
+ root to: "home#index"
end
end
end
@@ -1474,8 +1567,8 @@ class RouteSetTest < ActiveSupport::TestCase
def test_setting_root_in_namespace_using_string
assert_nothing_raised do
set.draw do
- namespace 'admin' do
- root :to => "home#index"
+ namespace "admin" do
+ root to: "home#index"
end
end
end
@@ -1484,8 +1577,8 @@ class RouteSetTest < ActiveSupport::TestCase
def test_route_constraints_with_unsupported_regexp_options_must_error
assert_raise ArgumentError do
set.draw do
- get 'page/:name' => 'pages#show',
- :constraints => { :name => /(david|jamis)/m }
+ get "page/:name" => "pages#show",
+ :constraints => { name: /(david|jamis)/m }
end
end
end
@@ -1493,14 +1586,14 @@ class RouteSetTest < ActiveSupport::TestCase
def test_route_constraints_with_supported_options_must_not_error
assert_nothing_raised do
set.draw do
- get 'page/:name' => 'pages#show',
- :constraints => { :name => /(david|jamis)/i }
+ get "page/:name" => "pages#show",
+ :constraints => { name: /(david|jamis)/i }
end
end
assert_nothing_raised do
set.draw do
- get 'page/:name' => 'pages#show',
- :constraints => { :name => / # Desperately overcommented regexp
+ get "page/:name" => "pages#show",
+ :constraints => { name: / # Desperately overcommented regexp
( #Either
david #The Creator
| #Or
@@ -1513,286 +1606,268 @@ class RouteSetTest < ActiveSupport::TestCase
def test_route_with_subdomain_and_constraints_must_receive_params
name_param = nil
set.draw do
- get 'page/:name' => 'pages#show', :constraints => lambda {|request|
+ get "page/:name" => "pages#show", :constraints => lambda { |request|
name_param = request.params[:name]
return true
}
end
- assert_equal({:controller => 'pages', :action => 'show', :name => 'mypage'},
- set.recognize_path('http://subdomain.example.org/page/mypage'))
- assert_equal(name_param, 'mypage')
+ assert_equal({ controller: "pages", action: "show", name: "mypage" },
+ set.recognize_path("http://subdomain.example.org/page/mypage"))
+ assert_equal(name_param, "mypage")
end
def test_route_requirement_recognize_with_ignore_case
set.draw do
- get 'page/:name' => 'pages#show',
- :constraints => {:name => /(david|jamis)/i}
+ get "page/:name" => "pages#show",
+ :constraints => { name: /(david|jamis)/i }
end
- assert_equal({:controller => 'pages', :action => 'show', :name => 'jamis'}, set.recognize_path('/page/jamis'))
+ assert_equal({ controller: "pages", action: "show", name: "jamis" }, set.recognize_path("/page/jamis"))
assert_raise ActionController::RoutingError do
- set.recognize_path('/page/davidjamis')
+ set.recognize_path("/page/davidjamis")
end
- assert_equal({:controller => 'pages', :action => 'show', :name => 'DAVID'}, set.recognize_path('/page/DAVID'))
+ assert_equal({ controller: "pages", action: "show", name: "DAVID" }, set.recognize_path("/page/DAVID"))
end
def test_route_requirement_generate_with_ignore_case
set.draw do
- get 'page/:name' => 'pages#show',
- :constraints => {:name => /(david|jamis)/i}
+ get "page/:name" => "pages#show",
+ :constraints => { name: /(david|jamis)/i }
end
- url = url_for(set, { :controller => 'pages', :action => 'show', :name => 'david' })
+ url = url_for(set, controller: "pages", action: "show", name: "david")
assert_equal "/page/david", url
assert_raise(ActionController::UrlGenerationError) do
- url_for(set, { :controller => 'pages', :action => 'show', :name => 'davidjamis' })
+ url_for(set, controller: "pages", action: "show", name: "davidjamis")
end
- url = url_for(set, { :controller => 'pages', :action => 'show', :name => 'JAMIS' })
+ url = url_for(set, controller: "pages", action: "show", name: "JAMIS")
assert_equal "/page/JAMIS", url
end
def test_route_requirement_recognize_with_extended_syntax
set.draw do
- get 'page/:name' => 'pages#show',
- :constraints => {:name => / # Desperately overcommented regexp
+ get "page/:name" => "pages#show",
+ :constraints => { name: / # Desperately overcommented regexp
( #Either
david #The Creator
| #Or
jamis #The Deployer
- )/x}
+ )/x }
end
- assert_equal({:controller => 'pages', :action => 'show', :name => 'jamis'}, set.recognize_path('/page/jamis'))
- assert_equal({:controller => 'pages', :action => 'show', :name => 'david'}, set.recognize_path('/page/david'))
+ assert_equal({ controller: "pages", action: "show", name: "jamis" }, set.recognize_path("/page/jamis"))
+ assert_equal({ controller: "pages", action: "show", name: "david" }, set.recognize_path("/page/david"))
assert_raise ActionController::RoutingError do
- set.recognize_path('/page/david #The Creator')
+ set.recognize_path("/page/david #The Creator")
end
assert_raise ActionController::RoutingError do
- set.recognize_path('/page/David')
+ set.recognize_path("/page/David")
end
end
def test_route_requirement_with_xi_modifiers
set.draw do
- get 'page/:name' => 'pages#show',
- :constraints => {:name => / # Desperately overcommented regexp
+ get "page/:name" => "pages#show",
+ :constraints => { name: / # Desperately overcommented regexp
( #Either
david #The Creator
| #Or
jamis #The Deployer
- )/xi}
+ )/xi }
end
- assert_equal({:controller => 'pages', :action => 'show', :name => 'JAMIS'},
- set.recognize_path('/page/JAMIS'))
+ assert_equal({ controller: "pages", action: "show", name: "JAMIS" },
+ set.recognize_path("/page/JAMIS"))
assert_equal "/page/JAMIS",
- url_for(set, { :controller => 'pages', :action => 'show', :name => 'JAMIS' })
+ url_for(set, controller: "pages", action: "show", name: "JAMIS")
end
def test_routes_with_symbols
set.draw do
- get 'unnamed', :controller => :pages, :action => :show, :name => :as_symbol
- get 'named' , :controller => :pages, :action => :show, :name => :as_symbol, :as => :named
+ get "unnamed", controller: :pages, action: :show, name: :as_symbol
+ get "named", controller: :pages, action: :show, name: :as_symbol, as: :named
end
- assert_equal({:controller => 'pages', :action => 'show', :name => :as_symbol}, set.recognize_path('/unnamed'))
- assert_equal({:controller => 'pages', :action => 'show', :name => :as_symbol}, set.recognize_path('/named'))
+ assert_equal({ controller: "pages", action: "show", name: :as_symbol }, set.recognize_path("/unnamed"))
+ assert_equal({ controller: "pages", action: "show", name: :as_symbol }, set.recognize_path("/named"))
end
def test_regexp_chunk_should_add_question_mark_for_optionals
set.draw do
- get '/' => 'foo#index'
- get '/hello' => 'bar#index'
+ get "/" => "foo#index"
+ get "/hello" => "bar#index"
end
- assert_equal '/', url_for(set, { :controller => 'foo' })
- assert_equal '/hello', url_for(set, { :controller => 'bar' })
+ assert_equal "/", url_for(set, controller: "foo")
+ assert_equal "/hello", url_for(set, controller: "bar")
- assert_equal({:controller => "foo", :action => "index"}, set.recognize_path('/'))
- assert_equal({:controller => "bar", :action => "index"}, set.recognize_path('/hello'))
+ assert_equal({ controller: "foo", action: "index" }, set.recognize_path("/"))
+ assert_equal({ controller: "bar", action: "index" }, set.recognize_path("/hello"))
end
def test_assign_route_options_with_anchor_chars
set.draw do
- get '/cars/:action/:person/:car/', :controller => 'cars'
+ ActiveSupport::Deprecation.silence do
+ get "/cars/:action/:person/:car/", controller: "cars"
+ end
end
- assert_equal '/cars/buy/1/2', url_for(set, { :controller => 'cars', :action => 'buy', :person => '1', :car => '2' })
+ assert_equal "/cars/buy/1/2", url_for(set, controller: "cars", action: "buy", person: "1", car: "2")
- assert_equal({:controller => "cars", :action => "buy", :person => "1", :car => "2"}, set.recognize_path('/cars/buy/1/2'))
+ assert_equal({ controller: "cars", action: "buy", person: "1", car: "2" }, set.recognize_path("/cars/buy/1/2"))
end
def test_segmentation_of_dot_path
set.draw do
- get '/books/:action.rss', :controller => 'books'
+ ActiveSupport::Deprecation.silence do
+ get "/books/:action.rss", controller: "books"
+ end
end
- assert_equal '/books/list.rss', url_for(set, { :controller => 'books', :action => 'list' })
+ assert_equal "/books/list.rss", url_for(set, controller: "books", action: "list")
- assert_equal({:controller => "books", :action => "list"}, set.recognize_path('/books/list.rss'))
+ assert_equal({ controller: "books", action: "list" }, set.recognize_path("/books/list.rss"))
end
def test_segmentation_of_dynamic_dot_path
set.draw do
- get '/books(/:action(.:format))', :controller => 'books'
+ ActiveSupport::Deprecation.silence do
+ get "/books(/:action(.:format))", controller: "books"
+ end
end
- assert_equal '/books/list.rss', url_for(set, { :controller => 'books', :action => 'list', :format => 'rss' })
- assert_equal '/books/list.xml', url_for(set, { :controller => 'books', :action => 'list', :format => 'xml' })
- assert_equal '/books/list', url_for(set, { :controller => 'books', :action => 'list' })
- assert_equal '/books', url_for(set, { :controller => 'books', :action => 'index' })
+ assert_equal "/books/list.rss", url_for(set, controller: "books", action: "list", format: "rss")
+ assert_equal "/books/list.xml", url_for(set, controller: "books", action: "list", format: "xml")
+ assert_equal "/books/list", url_for(set, controller: "books", action: "list")
+ assert_equal "/books", url_for(set, controller: "books", action: "index")
- assert_equal({:controller => "books", :action => "list", :format => "rss"}, set.recognize_path('/books/list.rss'))
- assert_equal({:controller => "books", :action => "list", :format => "xml"}, set.recognize_path('/books/list.xml'))
- assert_equal({:controller => "books", :action => "list"}, set.recognize_path('/books/list'))
- assert_equal({:controller => "books", :action => "index"}, set.recognize_path('/books'))
+ assert_equal({ controller: "books", action: "list", format: "rss" }, set.recognize_path("/books/list.rss"))
+ assert_equal({ controller: "books", action: "list", format: "xml" }, set.recognize_path("/books/list.xml"))
+ assert_equal({ controller: "books", action: "list" }, set.recognize_path("/books/list"))
+ assert_equal({ controller: "books", action: "index" }, set.recognize_path("/books"))
end
def test_slashes_are_implied
- set.draw { get("/:controller(/:action(/:id))") }
+ set.draw { ActiveSupport::Deprecation.silence { get("/:controller(/:action(/:id))") } }
- assert_equal '/content', url_for(set, { :controller => 'content', :action => 'index' })
- assert_equal '/content/list', url_for(set, { :controller => 'content', :action => 'list' })
- assert_equal '/content/show/1', url_for(set, { :controller => 'content', :action => 'show', :id => '1' })
+ assert_equal "/content", url_for(set, controller: "content", action: "index")
+ assert_equal "/content/list", url_for(set, controller: "content", action: "list")
+ assert_equal "/content/show/1", url_for(set, controller: "content", action: "show", id: "1")
- assert_equal({:controller => "content", :action => "index"}, set.recognize_path('/content'))
- assert_equal({:controller => "content", :action => "index"}, set.recognize_path('/content/index'))
- assert_equal({:controller => "content", :action => "list"}, set.recognize_path('/content/list'))
- assert_equal({:controller => "content", :action => "show", :id => "1"}, set.recognize_path('/content/show/1'))
+ assert_equal({ controller: "content", action: "index" }, set.recognize_path("/content"))
+ assert_equal({ controller: "content", action: "index" }, set.recognize_path("/content/index"))
+ assert_equal({ controller: "content", action: "list" }, set.recognize_path("/content/list"))
+ assert_equal({ controller: "content", action: "show", id: "1" }, set.recognize_path("/content/show/1"))
end
def test_default_route_recognition
- expected = {:controller => 'pages', :action => 'show', :id => '10'}
- assert_equal expected, default_route_set.recognize_path('/pages/show/10')
- assert_equal expected, default_route_set.recognize_path('/pages/show/10/')
+ expected = { controller: "pages", action: "show", id: "10" }
+ assert_equal expected, default_route_set.recognize_path("/pages/show/10")
+ assert_equal expected, default_route_set.recognize_path("/pages/show/10/")
- expected[:id] = 'jamis'
- assert_equal expected, default_route_set.recognize_path('/pages/show/jamis/')
+ expected[:id] = "jamis"
+ assert_equal expected, default_route_set.recognize_path("/pages/show/jamis/")
expected.delete :id
- assert_equal expected, default_route_set.recognize_path('/pages/show')
- assert_equal expected, default_route_set.recognize_path('/pages/show/')
+ assert_equal expected, default_route_set.recognize_path("/pages/show")
+ assert_equal expected, default_route_set.recognize_path("/pages/show/")
- expected[:action] = 'index'
- assert_equal expected, default_route_set.recognize_path('/pages/')
- assert_equal expected, default_route_set.recognize_path('/pages')
+ expected[:action] = "index"
+ assert_equal expected, default_route_set.recognize_path("/pages/")
+ assert_equal expected, default_route_set.recognize_path("/pages")
- assert_raise(ActionController::RoutingError) { default_route_set.recognize_path('/') }
- assert_raise(ActionController::RoutingError) { default_route_set.recognize_path('/pages/how/goood/it/is/to/be/free') }
+ assert_raise(ActionController::RoutingError) { default_route_set.recognize_path("/") }
+ assert_raise(ActionController::RoutingError) { default_route_set.recognize_path("/pages/how/goood/it/is/to/be/free") }
end
def test_default_route_should_omit_default_action
- assert_equal '/accounts', url_for(default_route_set, { :controller => 'accounts', :action => 'index' })
+ assert_equal "/accounts", url_for(default_route_set, controller: "accounts", action: "index")
end
def test_default_route_should_include_default_action_when_id_present
- assert_equal '/accounts/index/20', url_for(default_route_set, { :controller => 'accounts', :action => 'index', :id => '20' })
+ assert_equal "/accounts/index/20", url_for(default_route_set, controller: "accounts", action: "index", id: "20")
end
def test_default_route_should_work_with_action_but_no_id
- assert_equal '/accounts/list_all', url_for(default_route_set, { :controller => 'accounts', :action => 'list_all' })
+ assert_equal "/accounts/list_all", url_for(default_route_set, controller: "accounts", action: "list_all")
end
def test_default_route_should_uri_escape_pluses
- expected = { :controller => 'pages', :action => 'show', :id => 'hello world' }
- assert_equal expected, default_route_set.recognize_path('/pages/show/hello%20world')
- assert_equal '/pages/show/hello%20world', url_for(default_route_set, expected)
+ expected = { controller: "pages", action: "show", id: "hello world" }
+ assert_equal expected, default_route_set.recognize_path("/pages/show/hello%20world")
+ assert_equal "/pages/show/hello%20world", url_for(default_route_set, expected)
- expected[:id] = 'hello+world'
- assert_equal expected, default_route_set.recognize_path('/pages/show/hello+world')
- assert_equal expected, default_route_set.recognize_path('/pages/show/hello%2Bworld')
- assert_equal '/pages/show/hello+world', url_for(default_route_set, expected)
+ expected[:id] = "hello+world"
+ assert_equal expected, default_route_set.recognize_path("/pages/show/hello+world")
+ assert_equal expected, default_route_set.recognize_path("/pages/show/hello%2Bworld")
+ assert_equal "/pages/show/hello+world", url_for(default_route_set, expected)
end
def test_build_empty_query_string
- assert_uri_equal '/foo', url_for(default_route_set, { :controller => 'foo' })
+ assert_uri_equal "/foo", url_for(default_route_set, controller: "foo")
end
def test_build_query_string_with_nil_value
- assert_uri_equal '/foo', url_for(default_route_set, { :controller => 'foo', :x => nil })
+ assert_uri_equal "/foo", url_for(default_route_set, controller: "foo", x: nil)
end
def test_simple_build_query_string
- assert_uri_equal '/foo?x=1&y=2', url_for(default_route_set, { :controller => 'foo', :x => '1', :y => '2' })
+ assert_uri_equal "/foo?x=1&y=2", url_for(default_route_set, controller: "foo", x: "1", y: "2")
end
def test_convert_ints_build_query_string
- assert_uri_equal '/foo?x=1&y=2', url_for(default_route_set, { :controller => 'foo', :x => 1, :y => 2 })
+ assert_uri_equal "/foo?x=1&y=2", url_for(default_route_set, controller: "foo", x: 1, y: 2)
end
def test_escape_spaces_build_query_string
- assert_uri_equal '/foo?x=hello+world&y=goodbye+world', url_for(default_route_set, { :controller => 'foo', :x => 'hello world', :y => 'goodbye world' })
+ assert_uri_equal "/foo?x=hello+world&y=goodbye+world", url_for(default_route_set, controller: "foo", x: "hello world", y: "goodbye world")
end
def test_expand_array_build_query_string
- assert_uri_equal '/foo?x%5B%5D=1&x%5B%5D=2', url_for(default_route_set, { :controller => 'foo', :x => [1, 2] })
+ assert_uri_equal "/foo?x%5B%5D=1&x%5B%5D=2", url_for(default_route_set, controller: "foo", x: [1, 2])
end
def test_escape_spaces_build_query_string_selected_keys
- assert_uri_equal '/foo?x=hello+world', url_for(default_route_set, { :controller => 'foo', :x => 'hello world' })
+ assert_uri_equal "/foo?x=hello+world", url_for(default_route_set, controller: "foo", x: "hello world")
end
def test_generate_with_default_params
set.draw do
- get 'dummy/page/:page' => 'dummy#show'
- get 'dummy/dots/page.:page' => 'dummy#dots'
- get 'ibocorp(/:page)' => 'ibocorp#show',
- :constraints => { :page => /\d+/ },
- :defaults => { :page => 1 }
-
- get ':controller/:action/:id'
+ get "dummy/page/:page" => "dummy#show"
+ get "dummy/dots/page.:page" => "dummy#dots"
+ get "ibocorp(/:page)" => "ibocorp#show",
+ :constraints => { page: /\d+/ },
+ :defaults => { page: 1 }
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:id"
+ end
end
- assert_equal '/ibocorp', url_for(set, { :controller => 'ibocorp', :action => "show", :page => 1 })
+ assert_equal "/ibocorp", url_for(set, controller: "ibocorp", action: "show", page: 1)
end
include ActionDispatch::RoutingVerbs
- class TestSet < ActionDispatch::Routing::RouteSet
- def initialize(block)
- @block = block
- super()
- end
-
- class Dispatcher < ActionDispatch::Routing::RouteSet::Dispatcher
- def initialize(defaults, set, block)
- super(defaults)
- @block = block
- @set = set
- end
-
- def controller_reference(controller_param)
- block = @block
- set = @set
- Class.new(ActionController::Base) {
- include set.url_helpers
- define_method(:process) { |name| block.call(self) }
- def to_a; [200, {}, []]; end
- }
- end
- end
-
- def dispatcher defaults
- TestSet::Dispatcher.new defaults, self, @block
- end
- end
-
alias :routes :set
def test_generate_with_optional_params_recalls_last_request
- controller = nil
- @set = TestSet.new ->(c) { controller = c }
+ @set = make_set false
set.draw do
- get "blog/", :controller => "blog", :action => "index"
+ get "blog/", controller: "blog", action: "index"
get "blog(/:year(/:month(/:day)))",
- :controller => "blog",
- :action => "show_date",
- :constraints => { :year => /(19|20)\d\d/, :month => /[01]?\d/, :day => /[0-3]?\d/ },
- :day => nil, :month => nil
+ controller: "blog",
+ action: "show_date",
+ constraints: { year: /(19|20)\d\d/, month: /[01]?\d/, day: /[0-3]?\d/ },
+ day: nil, month: nil
+
+ get "blog/show/:id", controller: "blog", action: "show", id: /\d+/
+
+ ActiveSupport::Deprecation.silence do
+ get "blog/:controller/:action(/:id)"
+ end
- get "blog/show/:id", :controller => "blog", :action => "show", :id => /\d+/
- get "blog/:controller/:action(/:id)"
- get "*anything", :controller => "blog", :action => "unknown_request"
+ get "*anything", controller: "blog", action: "unknown_request"
end
recognize_path = ->(path) {
@@ -1800,24 +1875,24 @@ class RouteSetTest < ActiveSupport::TestCase
controller.request.path_parameters
}
- assert_equal({:controller => "blog", :action => "index"}, recognize_path.("/blog"))
- assert_equal({:controller => "blog", :action => "show", :id => "123"}, recognize_path.("/blog/show/123"))
- assert_equal({:controller => "blog", :action => "show_date", :year => "2004", :day => nil, :month => nil }, recognize_path.("/blog/2004"))
- assert_equal({:controller => "blog", :action => "show_date", :year => "2004", :month => "12", :day => nil }, recognize_path.("/blog/2004/12"))
- assert_equal({:controller => "blog", :action => "show_date", :year => "2004", :month => "12", :day => "25"}, recognize_path.("/blog/2004/12/25"))
- assert_equal({:controller => "articles", :action => "edit", :id => "123"}, recognize_path.("/blog/articles/edit/123"))
- assert_equal({:controller => "articles", :action => "show_stats"}, recognize_path.("/blog/articles/show_stats"))
- assert_equal({:controller => "blog", :action => "unknown_request", :anything => "blog/wibble"}, recognize_path.("/blog/wibble"))
- assert_equal({:controller => "blog", :action => "unknown_request", :anything => "junk"}, recognize_path.("/junk"))
+ assert_equal({ controller: "blog", action: "index" }, recognize_path.("/blog"))
+ assert_equal({ controller: "blog", action: "show", id: "123" }, recognize_path.("/blog/show/123"))
+ assert_equal({ controller: "blog", action: "show_date", year: "2004", day: nil, month: nil }, recognize_path.("/blog/2004"))
+ assert_equal({ controller: "blog", action: "show_date", year: "2004", month: "12", day: nil }, recognize_path.("/blog/2004/12"))
+ assert_equal({ controller: "blog", action: "show_date", year: "2004", month: "12", day: "25" }, recognize_path.("/blog/2004/12/25"))
+ assert_equal({ controller: "articles", action: "edit", id: "123" }, recognize_path.("/blog/articles/edit/123"))
+ assert_equal({ controller: "articles", action: "show_stats" }, recognize_path.("/blog/articles/show_stats"))
+ assert_equal({ controller: "blog", action: "unknown_request", anything: "blog/wibble" }, recognize_path.("/blog/wibble"))
+ assert_equal({ controller: "blog", action: "unknown_request", anything: "junk" }, recognize_path.("/junk"))
- get URI('http://example.org/blog/2006/07/28')
+ get URI("http://example.org/blog/2006/07/28")
- assert_equal({:controller => "blog", :action => "show_date", :year => "2006", :month => "07", :day => "28"}, controller.request.path_parameters)
- assert_equal("/blog/2006/07/25", controller.url_for({ :day => 25, :only_path => true }))
- assert_equal("/blog/2005", controller.url_for({ :year => 2005, :only_path => true }))
- assert_equal("/blog/show/123", controller.url_for({ :action => "show" , :id => 123, :only_path => true }))
- assert_equal("/blog/2006", controller.url_for({ :year => 2006, :only_path => true }))
- assert_equal("/blog/2006", controller.url_for({ :year => 2006, :month => nil, :only_path => true }))
+ assert_equal({ controller: "blog", action: "show_date", year: "2006", month: "07", day: "28" }, controller.request.path_parameters)
+ assert_equal("/blog/2006/07/25", controller.url_for(day: 25, only_path: true))
+ assert_equal("/blog/2005", controller.url_for(year: 2005, only_path: true))
+ assert_equal("/blog/show/123", controller.url_for(action: "show", id: 123, only_path: true))
+ assert_equal("/blog/2006", controller.url_for(year: 2006, only_path: true))
+ assert_equal("/blog/2006", controller.url_for(year: 2006, month: nil, only_path: true))
end
private
@@ -1826,8 +1901,8 @@ class RouteSetTest < ActiveSupport::TestCase
end
def sort_query_string_params(uri)
- path, qs = uri.split('?')
- qs = qs.split('&').sort.join('&') if qs
+ path, qs = uri.split("?")
+ qs = qs.split("&").sort.join("&") if qs
qs ? "#{path}?#{qs}" : path
end
end
@@ -1842,52 +1917,59 @@ class RackMountIntegrationTests < ActiveSupport::TestCase
resources :users, :posts
end
- namespace 'api' do
- root :to => 'users#index'
+ namespace "api" do
+ root to: "users#index"
end
- get '/blog(/:year(/:month(/:day)))' => 'posts#show_date',
+ get "/blog(/:year(/:month(/:day)))" => "posts#show_date",
:constraints => {
- :year => /(19|20)\d\d/,
- :month => /[01]?\d/,
- :day => /[0-3]?\d/
+ year: /(19|20)\d\d/,
+ month: /[01]?\d/,
+ day: /[0-3]?\d/
},
:day => nil,
:month => nil
- get 'archive/:year', :controller => 'archive', :action => 'index',
- :defaults => { :year => nil },
- :constraints => { :year => /\d{4}/ },
- :as => "blog"
+ get "archive/:year", controller: "archive", action: "index",
+ defaults: { year: nil },
+ constraints: { year: /\d{4}/ },
+ as: "blog"
resources :people
- get 'legacy/people' => "people#index", :legacy => "true"
-
- get 'symbols', :controller => :symbols, :action => :show, :name => :as_symbol
- get 'id_default(/:id)' => "foo#id_default", :id => 1
- match 'get_or_post' => "foo#get_or_post", :via => [:get, :post]
- get 'optional/:optional' => "posts#index"
- get 'projects/:project_id' => "project#index", :as => "project"
- get 'clients' => "projects#index"
-
- get 'ignorecase/geocode/:postalcode' => 'geocode#show', :postalcode => /hx\d\d-\d[a-z]{2}/i
- get 'extended/geocode/:postalcode' => 'geocode#show',:constraints => {
- :postalcode => /# Postcode format
+ get "legacy/people" => "people#index", :legacy => "true"
+
+ get "symbols", controller: :symbols, action: :show, name: :as_symbol
+ get "id_default(/:id)" => "foo#id_default", :id => 1
+ match "get_or_post" => "foo#get_or_post", :via => [:get, :post]
+ get "optional/:optional" => "posts#index"
+ get "projects/:project_id" => "project#index", :as => "project"
+ get "clients" => "projects#index"
+
+ get "ignorecase/geocode/:postalcode" => "geocode#show", :postalcode => /hx\d\d-\d[a-z]{2}/i
+ get "extended/geocode/:postalcode" => "geocode#show", :constraints => {
+ postalcode: /# Postcode format
\d{5} #Prefix
(-\d{4})? #Suffix
/x
}, :as => "geocode"
- get 'news(.:format)' => "news#index"
+ get "news(.:format)" => "news#index"
+
+ ActiveSupport::Deprecation.silence do
+ get "comment/:id(/:action)" => "comments#show"
+ get "ws/:controller(/:action(/:id))", ws: true
+ get "account(/:action)" => "account#subscription"
+ get "pages/:page_id/:controller(/:action(/:id))"
+ get ":controller/ping", action: "ping"
+ end
+
+ get "こんにちは/世界", controller: "news", action: "index"
+
+ ActiveSupport::Deprecation.silence do
+ match ":controller(/:action(/:id))(.:format)", via: :all
+ end
- get 'comment/:id(/:action)' => "comments#show"
- get 'ws/:controller(/:action(/:id))', :ws => true
- get 'account(/:action)' => "account#subscription"
- get 'pages/:page_id/:controller(/:action(/:id))'
- get ':controller/ping', :action => 'ping'
- get 'こんにちは/世界', :controller => 'news', :action => 'index'
- match ':controller(/:action(/:id))(.:format)', :via => :all
- root :to => "news#index"
+ root to: "news#index"
}
attr_reader :routes
@@ -1899,118 +1981,118 @@ class RackMountIntegrationTests < ActiveSupport::TestCase
end
def test_recognize_path
- assert_equal({:controller => 'admin/users', :action => 'index'}, @routes.recognize_path('/admin/users', :method => :get))
- assert_equal({:controller => 'admin/users', :action => 'create'}, @routes.recognize_path('/admin/users', :method => :post))
- assert_equal({:controller => 'admin/users', :action => 'new'}, @routes.recognize_path('/admin/users/new', :method => :get))
- assert_equal({:controller => 'admin/users', :action => 'show', :id => '1'}, @routes.recognize_path('/admin/users/1', :method => :get))
- assert_equal({:controller => 'admin/users', :action => 'update', :id => '1'}, @routes.recognize_path('/admin/users/1', :method => :put))
- assert_equal({:controller => 'admin/users', :action => 'destroy', :id => '1'}, @routes.recognize_path('/admin/users/1', :method => :delete))
- assert_equal({:controller => 'admin/users', :action => 'edit', :id => '1'}, @routes.recognize_path('/admin/users/1/edit', :method => :get))
-
- assert_equal({:controller => 'admin/posts', :action => 'index'}, @routes.recognize_path('/admin/posts', :method => :get))
- assert_equal({:controller => 'admin/posts', :action => 'new'}, @routes.recognize_path('/admin/posts/new', :method => :get))
-
- assert_equal({:controller => 'api/users', :action => 'index'}, @routes.recognize_path('/api', :method => :get))
- assert_equal({:controller => 'api/users', :action => 'index'}, @routes.recognize_path('/api/', :method => :get))
-
- assert_equal({:controller => 'posts', :action => 'show_date', :year => '2009', :month => nil, :day => nil }, @routes.recognize_path('/blog/2009', :method => :get))
- assert_equal({:controller => 'posts', :action => 'show_date', :year => '2009', :month => '01', :day => nil }, @routes.recognize_path('/blog/2009/01', :method => :get))
- assert_equal({:controller => 'posts', :action => 'show_date', :year => '2009', :month => '01', :day => '01'}, @routes.recognize_path('/blog/2009/01/01', :method => :get))
-
- assert_equal({:controller => 'archive', :action => 'index', :year => '2010'}, @routes.recognize_path('/archive/2010'))
- assert_equal({:controller => 'archive', :action => 'index'}, @routes.recognize_path('/archive'))
-
- assert_equal({:controller => 'people', :action => 'index'}, @routes.recognize_path('/people', :method => :get))
- assert_equal({:controller => 'people', :action => 'index', :format => 'xml'}, @routes.recognize_path('/people.xml', :method => :get))
- assert_equal({:controller => 'people', :action => 'create'}, @routes.recognize_path('/people', :method => :post))
- assert_equal({:controller => 'people', :action => 'new'}, @routes.recognize_path('/people/new', :method => :get))
- assert_equal({:controller => 'people', :action => 'show', :id => '1'}, @routes.recognize_path('/people/1', :method => :get))
- assert_equal({:controller => 'people', :action => 'show', :id => '1', :format => 'xml'}, @routes.recognize_path('/people/1.xml', :method => :get))
- assert_equal({:controller => 'people', :action => 'update', :id => '1'}, @routes.recognize_path('/people/1', :method => :put))
- assert_equal({:controller => 'people', :action => 'destroy', :id => '1'}, @routes.recognize_path('/people/1', :method => :delete))
- assert_equal({:controller => 'people', :action => 'edit', :id => '1'}, @routes.recognize_path('/people/1/edit', :method => :get))
- assert_equal({:controller => 'people', :action => 'edit', :id => '1', :format => 'xml'}, @routes.recognize_path('/people/1/edit.xml', :method => :get))
-
- assert_equal({:controller => 'symbols', :action => 'show', :name => :as_symbol}, @routes.recognize_path('/symbols'))
- assert_equal({:controller => 'foo', :action => 'id_default', :id => '1'}, @routes.recognize_path('/id_default/1'))
- assert_equal({:controller => 'foo', :action => 'id_default', :id => '2'}, @routes.recognize_path('/id_default/2'))
- assert_equal({:controller => 'foo', :action => 'id_default', :id => 1 }, @routes.recognize_path('/id_default'))
- assert_equal({:controller => 'foo', :action => 'get_or_post'}, @routes.recognize_path('/get_or_post', :method => :get))
- assert_equal({:controller => 'foo', :action => 'get_or_post'}, @routes.recognize_path('/get_or_post', :method => :post))
- assert_raise(ActionController::RoutingError) { @routes.recognize_path('/get_or_post', :method => :put) }
- assert_raise(ActionController::RoutingError) { @routes.recognize_path('/get_or_post', :method => :delete) }
-
- assert_equal({:controller => 'posts', :action => 'index', :optional => 'bar'}, @routes.recognize_path('/optional/bar'))
- assert_raise(ActionController::RoutingError) { @routes.recognize_path('/optional') }
-
- assert_equal({:controller => 'posts', :action => 'show', :id => '1', :ws => true}, @routes.recognize_path('/ws/posts/show/1', :method => :get))
- assert_equal({:controller => 'posts', :action => 'list', :ws => true}, @routes.recognize_path('/ws/posts/list', :method => :get))
- assert_equal({:controller => 'posts', :action => 'index', :ws => true}, @routes.recognize_path('/ws/posts', :method => :get))
-
- assert_equal({:controller => 'account', :action => 'subscription'}, @routes.recognize_path('/account', :method => :get))
- assert_equal({:controller => 'account', :action => 'subscription'}, @routes.recognize_path('/account/subscription', :method => :get))
- assert_equal({:controller => 'account', :action => 'billing'}, @routes.recognize_path('/account/billing', :method => :get))
-
- assert_equal({:page_id => '1', :controller => 'notes', :action => 'index'}, @routes.recognize_path('/pages/1/notes', :method => :get))
- assert_equal({:page_id => '1', :controller => 'notes', :action => 'list'}, @routes.recognize_path('/pages/1/notes/list', :method => :get))
- assert_equal({:page_id => '1', :controller => 'notes', :action => 'show', :id => '2'}, @routes.recognize_path('/pages/1/notes/show/2', :method => :get))
-
- assert_equal({:controller => 'posts', :action => 'ping'}, @routes.recognize_path('/posts/ping', :method => :get))
- assert_equal({:controller => 'posts', :action => 'index'}, @routes.recognize_path('/posts', :method => :get))
- assert_equal({:controller => 'posts', :action => 'index'}, @routes.recognize_path('/posts/index', :method => :get))
- assert_equal({:controller => 'posts', :action => 'show'}, @routes.recognize_path('/posts/show', :method => :get))
- assert_equal({:controller => 'posts', :action => 'show', :id => '1'}, @routes.recognize_path('/posts/show/1', :method => :get))
- assert_equal({:controller => 'posts', :action => 'create'}, @routes.recognize_path('/posts/create', :method => :post))
-
- assert_equal({:controller => 'geocode', :action => 'show', :postalcode => 'hx12-1az'}, @routes.recognize_path('/ignorecase/geocode/hx12-1az'))
- assert_equal({:controller => 'geocode', :action => 'show', :postalcode => 'hx12-1AZ'}, @routes.recognize_path('/ignorecase/geocode/hx12-1AZ'))
- assert_equal({:controller => 'geocode', :action => 'show', :postalcode => '12345-1234'}, @routes.recognize_path('/extended/geocode/12345-1234'))
- assert_equal({:controller => 'geocode', :action => 'show', :postalcode => '12345'}, @routes.recognize_path('/extended/geocode/12345'))
-
- assert_equal({:controller => 'news', :action => 'index' }, @routes.recognize_path('/', :method => :get))
- assert_equal({:controller => 'news', :action => 'index', :format => 'rss'}, @routes.recognize_path('/news.rss', :method => :get))
-
- assert_raise(ActionController::RoutingError) { @routes.recognize_path('/none', :method => :get) }
+ assert_equal({ controller: "admin/users", action: "index" }, @routes.recognize_path("/admin/users", method: :get))
+ assert_equal({ controller: "admin/users", action: "create" }, @routes.recognize_path("/admin/users", method: :post))
+ assert_equal({ controller: "admin/users", action: "new" }, @routes.recognize_path("/admin/users/new", method: :get))
+ assert_equal({ controller: "admin/users", action: "show", id: "1" }, @routes.recognize_path("/admin/users/1", method: :get))
+ assert_equal({ controller: "admin/users", action: "update", id: "1" }, @routes.recognize_path("/admin/users/1", method: :put))
+ assert_equal({ controller: "admin/users", action: "destroy", id: "1" }, @routes.recognize_path("/admin/users/1", method: :delete))
+ assert_equal({ controller: "admin/users", action: "edit", id: "1" }, @routes.recognize_path("/admin/users/1/edit", method: :get))
+
+ assert_equal({ controller: "admin/posts", action: "index" }, @routes.recognize_path("/admin/posts", method: :get))
+ assert_equal({ controller: "admin/posts", action: "new" }, @routes.recognize_path("/admin/posts/new", method: :get))
+
+ assert_equal({ controller: "api/users", action: "index" }, @routes.recognize_path("/api", method: :get))
+ assert_equal({ controller: "api/users", action: "index" }, @routes.recognize_path("/api/", method: :get))
+
+ assert_equal({ controller: "posts", action: "show_date", year: "2009", month: nil, day: nil }, @routes.recognize_path("/blog/2009", method: :get))
+ assert_equal({ controller: "posts", action: "show_date", year: "2009", month: "01", day: nil }, @routes.recognize_path("/blog/2009/01", method: :get))
+ assert_equal({ controller: "posts", action: "show_date", year: "2009", month: "01", day: "01" }, @routes.recognize_path("/blog/2009/01/01", method: :get))
+
+ assert_equal({ controller: "archive", action: "index", year: "2010" }, @routes.recognize_path("/archive/2010"))
+ assert_equal({ controller: "archive", action: "index" }, @routes.recognize_path("/archive"))
+
+ assert_equal({ controller: "people", action: "index" }, @routes.recognize_path("/people", method: :get))
+ assert_equal({ controller: "people", action: "index", format: "xml" }, @routes.recognize_path("/people.xml", method: :get))
+ assert_equal({ controller: "people", action: "create" }, @routes.recognize_path("/people", method: :post))
+ assert_equal({ controller: "people", action: "new" }, @routes.recognize_path("/people/new", method: :get))
+ assert_equal({ controller: "people", action: "show", id: "1" }, @routes.recognize_path("/people/1", method: :get))
+ assert_equal({ controller: "people", action: "show", id: "1", format: "xml" }, @routes.recognize_path("/people/1.xml", method: :get))
+ assert_equal({ controller: "people", action: "update", id: "1" }, @routes.recognize_path("/people/1", method: :put))
+ assert_equal({ controller: "people", action: "destroy", id: "1" }, @routes.recognize_path("/people/1", method: :delete))
+ assert_equal({ controller: "people", action: "edit", id: "1" }, @routes.recognize_path("/people/1/edit", method: :get))
+ assert_equal({ controller: "people", action: "edit", id: "1", format: "xml" }, @routes.recognize_path("/people/1/edit.xml", method: :get))
+
+ assert_equal({ controller: "symbols", action: "show", name: :as_symbol }, @routes.recognize_path("/symbols"))
+ assert_equal({ controller: "foo", action: "id_default", id: "1" }, @routes.recognize_path("/id_default/1"))
+ assert_equal({ controller: "foo", action: "id_default", id: "2" }, @routes.recognize_path("/id_default/2"))
+ assert_equal({ controller: "foo", action: "id_default", id: 1 }, @routes.recognize_path("/id_default"))
+ assert_equal({ controller: "foo", action: "get_or_post" }, @routes.recognize_path("/get_or_post", method: :get))
+ assert_equal({ controller: "foo", action: "get_or_post" }, @routes.recognize_path("/get_or_post", method: :post))
+ assert_raise(ActionController::RoutingError) { @routes.recognize_path("/get_or_post", method: :put) }
+ assert_raise(ActionController::RoutingError) { @routes.recognize_path("/get_or_post", method: :delete) }
+
+ assert_equal({ controller: "posts", action: "index", optional: "bar" }, @routes.recognize_path("/optional/bar"))
+ assert_raise(ActionController::RoutingError) { @routes.recognize_path("/optional") }
+
+ assert_equal({ controller: "posts", action: "show", id: "1", ws: true }, @routes.recognize_path("/ws/posts/show/1", method: :get))
+ assert_equal({ controller: "posts", action: "list", ws: true }, @routes.recognize_path("/ws/posts/list", method: :get))
+ assert_equal({ controller: "posts", action: "index", ws: true }, @routes.recognize_path("/ws/posts", method: :get))
+
+ assert_equal({ controller: "account", action: "subscription" }, @routes.recognize_path("/account", method: :get))
+ assert_equal({ controller: "account", action: "subscription" }, @routes.recognize_path("/account/subscription", method: :get))
+ assert_equal({ controller: "account", action: "billing" }, @routes.recognize_path("/account/billing", method: :get))
+
+ assert_equal({ page_id: "1", controller: "notes", action: "index" }, @routes.recognize_path("/pages/1/notes", method: :get))
+ assert_equal({ page_id: "1", controller: "notes", action: "list" }, @routes.recognize_path("/pages/1/notes/list", method: :get))
+ assert_equal({ page_id: "1", controller: "notes", action: "show", id: "2" }, @routes.recognize_path("/pages/1/notes/show/2", method: :get))
+
+ assert_equal({ controller: "posts", action: "ping" }, @routes.recognize_path("/posts/ping", method: :get))
+ assert_equal({ controller: "posts", action: "index" }, @routes.recognize_path("/posts", method: :get))
+ assert_equal({ controller: "posts", action: "index" }, @routes.recognize_path("/posts/index", method: :get))
+ assert_equal({ controller: "posts", action: "show" }, @routes.recognize_path("/posts/show", method: :get))
+ assert_equal({ controller: "posts", action: "show", id: "1" }, @routes.recognize_path("/posts/show/1", method: :get))
+ assert_equal({ controller: "posts", action: "create" }, @routes.recognize_path("/posts/create", method: :post))
+
+ assert_equal({ controller: "geocode", action: "show", postalcode: "hx12-1az" }, @routes.recognize_path("/ignorecase/geocode/hx12-1az"))
+ assert_equal({ controller: "geocode", action: "show", postalcode: "hx12-1AZ" }, @routes.recognize_path("/ignorecase/geocode/hx12-1AZ"))
+ assert_equal({ controller: "geocode", action: "show", postalcode: "12345-1234" }, @routes.recognize_path("/extended/geocode/12345-1234"))
+ assert_equal({ controller: "geocode", action: "show", postalcode: "12345" }, @routes.recognize_path("/extended/geocode/12345"))
+
+ assert_equal({ controller: "news", action: "index" }, @routes.recognize_path("/", method: :get))
+ assert_equal({ controller: "news", action: "index", format: "rss" }, @routes.recognize_path("/news.rss", method: :get))
+
+ assert_raise(ActionController::RoutingError) { @routes.recognize_path("/none", method: :get) }
end
def test_generate_extras
- assert_equal ['/people', []], @routes.generate_extras(:controller => 'people')
- assert_equal ['/people', [:foo]], @routes.generate_extras(:controller => 'people', :foo => 'bar')
- assert_equal ['/people', []], @routes.generate_extras(:controller => 'people', :action => 'index')
- assert_equal ['/people', [:foo]], @routes.generate_extras(:controller => 'people', :action => 'index', :foo => 'bar')
- assert_equal ['/people/new', []], @routes.generate_extras(:controller => 'people', :action => 'new')
- assert_equal ['/people/new', [:foo]], @routes.generate_extras(:controller => 'people', :action => 'new', :foo => 'bar')
- assert_equal ['/people/1', []], @routes.generate_extras(:controller => 'people', :action => 'show', :id => '1')
- assert_equal ['/people/1', [:bar, :foo]], sort_extras!(@routes.generate_extras(:controller => 'people', :action => 'show', :id => '1', :foo => '2', :bar => '3'))
- assert_equal ['/people', [:person]], @routes.generate_extras(:controller => 'people', :action => 'create', :person => { :first_name => 'Josh', :last_name => 'Peek' })
- assert_equal ['/people', [:people]], @routes.generate_extras(:controller => 'people', :action => 'create', :people => ['Josh', 'Dave'])
-
- assert_equal ['/posts/show/1', []], @routes.generate_extras(:controller => 'posts', :action => 'show', :id => '1')
- assert_equal ['/posts/show/1', [:bar, :foo]], sort_extras!(@routes.generate_extras(:controller => 'posts', :action => 'show', :id => '1', :foo => '2', :bar => '3'))
- assert_equal ['/posts', []], @routes.generate_extras(:controller => 'posts', :action => 'index')
- assert_equal ['/posts', [:foo]], @routes.generate_extras(:controller => 'posts', :action => 'index', :foo => 'bar')
+ assert_equal ["/people", []], @routes.generate_extras(controller: "people")
+ assert_equal ["/people", [:foo]], @routes.generate_extras(controller: "people", foo: "bar")
+ assert_equal ["/people", []], @routes.generate_extras(controller: "people", action: "index")
+ assert_equal ["/people", [:foo]], @routes.generate_extras(controller: "people", action: "index", foo: "bar")
+ assert_equal ["/people/new", []], @routes.generate_extras(controller: "people", action: "new")
+ assert_equal ["/people/new", [:foo]], @routes.generate_extras(controller: "people", action: "new", foo: "bar")
+ assert_equal ["/people/1", []], @routes.generate_extras(controller: "people", action: "show", id: "1")
+ assert_equal ["/people/1", [:bar, :foo]], sort_extras!(@routes.generate_extras(controller: "people", action: "show", id: "1", foo: "2", bar: "3"))
+ assert_equal ["/people", [:person]], @routes.generate_extras(controller: "people", action: "create", person: { first_name: "Josh", last_name: "Peek" })
+ assert_equal ["/people", [:people]], @routes.generate_extras(controller: "people", action: "create", people: ["Josh", "Dave"])
+
+ assert_equal ["/posts/show/1", []], @routes.generate_extras(controller: "posts", action: "show", id: "1")
+ assert_equal ["/posts/show/1", [:bar, :foo]], sort_extras!(@routes.generate_extras(controller: "posts", action: "show", id: "1", foo: "2", bar: "3"))
+ assert_equal ["/posts", []], @routes.generate_extras(controller: "posts", action: "index")
+ assert_equal ["/posts", [:foo]], @routes.generate_extras(controller: "posts", action: "index", foo: "bar")
end
def test_extras
- params = {:controller => 'people'}
+ params = { controller: "people" }
assert_equal [], @routes.extra_keys(params)
- assert_equal({:controller => 'people'}, params)
+ assert_equal({ controller: "people", action: "index" }, params)
- params = {:controller => 'people', :foo => 'bar'}
+ params = { controller: "people", foo: "bar" }
assert_equal [:foo], @routes.extra_keys(params)
- assert_equal({:controller => 'people', :foo => 'bar'}, params)
+ assert_equal({ controller: "people", action: "index", foo: "bar" }, params)
- params = {:controller => 'people', :action => 'create', :person => { :name => 'Josh'}}
+ params = { controller: "people", action: "create", person: { name: "Josh" } }
assert_equal [:person], @routes.extra_keys(params)
- assert_equal({:controller => 'people', :action => 'create', :person => { :name => 'Josh'}}, params)
+ assert_equal({ controller: "people", action: "create", person: { name: "Josh" } }, params)
end
def test_unicode_path
- assert_equal({:controller => 'news', :action => 'index'}, @routes.recognize_path(URI.parser.escape('こんにちは/世界'), :method => :get))
+ assert_equal({ controller: "news", action: "index" }, @routes.recognize_path(URI.parser.escape("こんにちは/世界"), method: :get))
end
def test_downcased_unicode_path
- assert_equal({:controller => 'news', :action => 'index'}, @routes.recognize_path(URI.parser.escape('こんにちは/世界').downcase, :method => :get))
+ assert_equal({ controller: "news", action: "index" }, @routes.recognize_path(URI.parser.escape("こんにちは/世界").downcase, method: :get))
end
private
diff --git a/actionpack/test/controller/runner_test.rb b/actionpack/test/controller/runner_test.rb
index 3e9383abb2..1709ab5f6d 100644
--- a/actionpack/test/controller/runner_test.rb
+++ b/actionpack/test/controller/runner_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'action_dispatch/testing/integration'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "action_dispatch/testing/integration"
module ActionDispatch
class RunnerTest < ActiveSupport::TestCase
@@ -15,8 +17,8 @@ module ActionDispatch
def test_respond_to?
runner = MyRunner.new(Class.new { def x; end }.new)
- assert runner.respond_to?(:hi)
- assert runner.respond_to?(:x)
+ assert_respond_to runner, :hi
+ assert_respond_to runner, :x
end
end
end
diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb
index c0ddcf7f50..c917cdf761 100644
--- a/actionpack/test/controller/send_file_test.rb
+++ b/actionpack/test/controller/send_file_test.rb
@@ -1,9 +1,11 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module TestFileUtils
def file_name() File.basename(__FILE__) end
- def file_path() File.expand_path(__FILE__) end
- def file_data() @data ||= File.open(file_path, 'rb') { |f| f.read } end
+ def file_path() __FILE__ end
+ def file_data() @data ||= File.open(file_path, "rb") { |f| f.read } end
end
class SendFileController < ActionController::Base
@@ -11,6 +13,8 @@ class SendFileController < ActionController::Base
include ActionController::Testing
layout "layouts/standard" # to make sure layouts don't interfere
+ before_action :file, only: :file_from_before_action
+
attr_writer :options
def options
@options ||= {}
@@ -20,6 +24,51 @@ class SendFileController < ActionController::Base
send_file(file_path, options)
end
+ def file_from_before_action
+ raise "No file sent from before action."
+ end
+
+ def test_send_file_headers_bang
+ options = {
+ type: Mime[:png],
+ disposition: "disposition",
+ filename: "filename"
+ }
+
+ send_data "foo", options
+ end
+
+ def test_send_file_headers_with_disposition_as_a_symbol
+ options = {
+ type: Mime[:png],
+ disposition: :disposition,
+ filename: "filename"
+ }
+
+ send_data "foo", options
+ end
+
+ def test_send_file_headers_with_mime_lookup_with_symbol
+ options = { type: :png }
+
+ send_data "foo", options
+ end
+
+ def test_send_file_headers_with_bad_symbol
+ options = { type: :this_type_is_not_registered }
+ send_data "foo", options
+ end
+
+ def test_send_file_headers_with_nil_content_type
+ options = { type: nil }
+ send_data "foo", options
+ end
+
+ def test_send_file_headers_guess_type_from_extension
+ options = { filename: params[:filename] }
+ send_data "foo", options
+ end
+
def data
send_data(file_data, options)
end
@@ -37,9 +86,9 @@ class SendFileTest < ActionController::TestCase
end
def test_file_nostream
- @controller.options = { :stream => false }
+ @controller.options = { stream: false }
response = nil
- assert_nothing_raised { response = process('file') }
+ assert_nothing_raised { response = process("file") }
assert_not_nil response
body = response.body
assert_kind_of String, body
@@ -48,12 +97,12 @@ class SendFileTest < ActionController::TestCase
def test_file_stream
response = nil
- assert_nothing_raised { response = process('file') }
+ assert_nothing_raised { response = process("file") }
assert_not_nil response
assert_respond_to response.stream, :each
assert_respond_to response.stream, :to_path
- require 'stringio'
+ require "stringio"
output = StringIO.new
output.binmode
output.string.force_encoding(file_data.encoding)
@@ -62,16 +111,16 @@ class SendFileTest < ActionController::TestCase
end
def test_file_url_based_filename
- @controller.options = { :url_based_filename => true }
+ @controller.options = { url_based_filename: true }
response = nil
- assert_nothing_raised { response = process('file') }
+ assert_nothing_raised { response = process("file") }
assert_not_nil response
assert_equal "attachment", response.headers["Content-Disposition"]
end
def test_data
response = nil
- assert_nothing_raised { response = process('data') }
+ assert_nothing_raised { response = process("data") }
assert_not_nil response
assert_kind_of String, response.body
@@ -79,118 +128,101 @@ class SendFileTest < ActionController::TestCase
end
def test_headers_after_send_shouldnt_include_charset
- response = process('data')
+ response = process("data")
assert_equal "application/octet-stream", response.headers["Content-Type"]
- response = process('file')
+ response = process("file")
assert_equal "application/octet-stream", response.headers["Content-Type"]
end
# Test that send_file_headers! is setting the correct HTTP headers.
def test_send_file_headers_bang
- options = {
- :type => Mime::PNG,
- :disposition => 'disposition',
- :filename => 'filename'
- }
-
# Do it a few times: the resulting headers should be identical
# no matter how many times you send with the same options.
# Test resolving Ticket #458.
- @controller.headers = {}
- @controller.send(:send_file_headers!, options)
- @controller.send(:send_file_headers!, options)
- @controller.send(:send_file_headers!, options)
-
- h = @controller.headers
- assert_equal 'image/png', @controller.content_type
- assert_equal 'disposition; filename="filename"', h['Content-Disposition']
- assert_equal 'binary', h['Content-Transfer-Encoding']
+ 5.times do
+ get :test_send_file_headers_bang
- # test overriding Cache-Control: no-cache header to fix IE open/save dialog
- @controller.send(:send_file_headers!, options)
- @controller.response.prepare!
- assert_equal 'private', h['Cache-Control']
+ assert_equal "image/png", response.content_type
+ assert_equal %(disposition; filename="filename"; filename*=UTF-8''filename), response.get_header("Content-Disposition")
+ assert_equal "binary", response.get_header("Content-Transfer-Encoding")
+ assert_equal "private", response.get_header("Cache-Control")
+ end
end
def test_send_file_headers_with_disposition_as_a_symbol
- options = {
- :type => Mime::PNG,
- :disposition => :disposition,
- :filename => 'filename'
- }
+ get :test_send_file_headers_with_disposition_as_a_symbol
- @controller.headers = {}
- @controller.send(:send_file_headers!, options)
- assert_equal 'disposition; filename="filename"', @controller.headers['Content-Disposition']
+ assert_equal %(disposition; filename="filename"; filename*=UTF-8''filename), response.get_header("Content-Disposition")
end
def test_send_file_headers_with_mime_lookup_with_symbol
- options = {
- :type => :png
- }
-
- @controller.headers = {}
- @controller.send(:send_file_headers!, options)
-
- assert_equal 'image/png', @controller.content_type
+ get __method__
+ assert_equal "image/png", response.content_type
end
-
def test_send_file_headers_with_bad_symbol
- options = {
- :type => :this_type_is_not_registered
- }
+ error = assert_raise(ArgumentError) { get __method__ }
+ assert_equal "Unknown MIME type this_type_is_not_registered", error.message
+ end
- @controller.headers = {}
- assert_raise(ArgumentError) { @controller.send(:send_file_headers!, options) }
+ def test_send_file_headers_with_nil_content_type
+ error = assert_raise(ArgumentError) { get __method__ }
+ assert_equal ":type option required", error.message
end
def test_send_file_headers_guess_type_from_extension
{
- 'image.png' => 'image/png',
- 'image.jpeg' => 'image/jpeg',
- 'image.jpg' => 'image/jpeg',
- 'image.tif' => 'image/tiff',
- 'image.gif' => 'image/gif',
- 'movie.mpg' => 'video/mpeg',
- 'file.zip' => 'application/zip',
- 'file.unk' => 'application/octet-stream',
- 'zip' => 'application/octet-stream'
- }.each do |filename,expected_type|
- options = { :filename => filename }
- @controller.headers = {}
- @controller.send(:send_file_headers!, options)
- assert_equal expected_type, @controller.content_type
+ "image.png" => "image/png",
+ "image.jpeg" => "image/jpeg",
+ "image.jpg" => "image/jpeg",
+ "image.tif" => "image/tiff",
+ "image.gif" => "image/gif",
+ "movie.mp4" => "video/mp4",
+ "file.zip" => "application/zip",
+ "file.unk" => "application/octet-stream",
+ "zip" => "application/octet-stream"
+ }.each do |filename, expected_type|
+ get __method__, params: { filename: filename }
+ assert_equal expected_type, response.content_type
end
end
def test_send_file_with_default_content_disposition_header
- process('data')
- assert_equal 'attachment', @controller.headers['Content-Disposition']
+ process("data")
+ assert_equal "attachment", @controller.headers["Content-Disposition"]
end
def test_send_file_without_content_disposition_header
- @controller.options = {:disposition => nil}
- process('data')
- assert_nil @controller.headers['Content-Disposition']
+ @controller.options = { disposition: nil }
+ process("data")
+ assert_nil @controller.headers["Content-Disposition"]
+ end
+
+ def test_send_file_from_before_action
+ response = nil
+ assert_nothing_raised { response = process("file_from_before_action") }
+ assert_not_nil response
+
+ assert_kind_of String, response.body
+ assert_equal file_data, response.body
end
%w(file data).each do |method|
define_method "test_send_#{method}_status" do
- @controller.options = { :stream => false, :status => 500 }
- assert_nothing_raised { assert_not_nil process(method) }
+ @controller.options = { stream: false, status: 500 }
+ assert_not_nil process(method)
assert_equal 500, @response.status
end
define_method "test_send_#{method}_content_type" do
- @controller.options = { :stream => false, :content_type => "application/x-ruby" }
+ @controller.options = { stream: false, content_type: "application/x-ruby" }
assert_nothing_raised { assert_not_nil process(method) }
assert_equal "application/x-ruby", @response.content_type
end
define_method "test_default_send_#{method}_status" do
- @controller.options = { :stream => false }
+ @controller.options = { stream: false }
assert_nothing_raised { assert_not_nil process(method) }
assert_equal 200, @response.status
end
@@ -198,9 +230,30 @@ class SendFileTest < ActionController::TestCase
def test_send_file_with_action_controller_live
@controller = SendFileWithActionControllerLive.new
- @controller.options = { :content_type => "application/x-ruby" }
+ @controller.options = { content_type: "application/x-ruby" }
- response = process('file')
+ response = process("file")
assert_equal 200, response.status
end
+
+ def test_send_file_charset_with_type_options_key
+ @controller = SendFileWithActionControllerLive.new
+ @controller.options = { type: "text/calendar; charset=utf-8" }
+ response = process("file")
+ 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", response.headers["Content-Type"]
+ end
end
diff --git a/actionpack/test/controller/show_exceptions_test.rb b/actionpack/test/controller/show_exceptions_test.rb
index 786dc15444..2094aa1aed 100644
--- a/actionpack/test/controller/show_exceptions_test.rb
+++ b/actionpack/test/controller/show_exceptions_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module ShowExceptions
class ShowExceptionsController < ActionController::Base
@@ -10,11 +12,11 @@ module ShowExceptions
end
def boom
- raise 'boom!'
+ raise "boom!"
end
def another_boom
- raise 'boom!'
+ raise "boom!"
end
def show_detailed_exceptions?
@@ -23,26 +25,26 @@ module ShowExceptions
end
class ShowExceptionsTest < ActionDispatch::IntegrationTest
- test 'show error page from a remote ip' do
+ test "show error page from a remote ip" do
@app = ShowExceptionsController.action(:boom)
- self.remote_addr = '208.77.188.166'
- get '/'
+ self.remote_addr = "208.77.188.166"
+ get "/"
assert_equal "500 error fixture\n", body
end
- test 'show diagnostics from a local ip if show_detailed_exceptions? is set to request.local?' do
+ test "show diagnostics from a local ip if show_detailed_exceptions? is set to request.local?" do
@app = ShowExceptionsController.action(:boom)
- ['127.0.0.1', '127.0.0.127', '127.12.1.1', '::1', '0:0:0:0:0:0:0:1', '0:0:0:0:0:0:0:1%0'].each do |ip_address|
+ ["127.0.0.1", "127.0.0.127", "127.12.1.1", "::1", "0:0:0:0:0:0:0:1", "0:0:0:0:0:0:0:1%0"].each do |ip_address|
self.remote_addr = ip_address
- get '/'
+ get "/"
assert_match(/boom/, body)
end
end
- test 'show diagnostics from a remote ip when env is already set' do
+ test "show diagnostics from a remote ip when env is already set" do
@app = ShowExceptionsController.action(:another_boom)
- self.remote_addr = '208.77.188.166'
- get '/'
+ self.remote_addr = "208.77.188.166"
+ get "/"
assert_match(/boom/, body)
end
end
@@ -50,21 +52,21 @@ module ShowExceptions
class ShowExceptionsOverriddenController < ShowExceptionsController
private
- def show_detailed_exceptions?
- params['detailed'] == '1'
- end
+ def show_detailed_exceptions?
+ params["detailed"] == "1"
+ end
end
class ShowExceptionsOverriddenTest < ActionDispatch::IntegrationTest
- test 'show error page' do
+ test "show error page" do
@app = ShowExceptionsOverriddenController.action(:boom)
- get '/', params: { 'detailed' => '0' }
+ get "/", params: { "detailed" => "0" }
assert_equal "500 error fixture\n", body
end
- test 'show diagnostics message' do
+ test "show diagnostics message" do
@app = ShowExceptionsOverriddenController.action(:boom)
- get '/', params: { 'detailed' => '1' }
+ get "/", params: { "detailed" => "1" }
assert_match(/boom/, body)
end
end
@@ -72,25 +74,25 @@ module ShowExceptions
class ShowExceptionsFormatsTest < ActionDispatch::IntegrationTest
def test_render_json_exception
@app = ShowExceptionsOverriddenController.action(:boom)
- get "/", headers: { 'HTTP_ACCEPT' => 'application/json' }
+ get "/", headers: { "HTTP_ACCEPT" => "application/json" }
assert_response :internal_server_error
- assert_equal 'application/json', response.content_type.to_s
- assert_equal({ :status => 500, :error => 'Internal Server Error' }.to_json, response.body)
+ assert_equal "application/json", response.content_type.to_s
+ assert_equal({ status: 500, error: "Internal Server Error" }.to_json, response.body)
end
def test_render_xml_exception
@app = ShowExceptionsOverriddenController.action(:boom)
- get "/", headers: { 'HTTP_ACCEPT' => 'application/xml' }
+ get "/", headers: { "HTTP_ACCEPT" => "application/xml" }
assert_response :internal_server_error
- assert_equal 'application/xml', response.content_type.to_s
- assert_equal({ :status => 500, :error => 'Internal Server Error' }.to_xml, response.body)
+ assert_equal "application/xml", response.content_type.to_s
+ assert_equal({ status: 500, error: "Internal Server Error" }.to_xml, response.body)
end
def test_render_fallback_exception
@app = ShowExceptionsOverriddenController.action(:boom)
- get "/", headers: { 'HTTP_ACCEPT' => 'text/csv' }
+ get "/", headers: { "HTTP_ACCEPT" => "text/csv" }
assert_response :internal_server_error
- assert_equal 'text/html', response.content_type.to_s
+ assert_equal "text/html", response.content_type.to_s
end
end
@@ -101,9 +103,9 @@ module ShowExceptions
@app.instance_variable_set(:@exceptions_app, nil)
$stderr = StringIO.new
- get '/', headers: { 'HTTP_ACCEPT' => 'text/json' }
+ get "/", headers: { "HTTP_ACCEPT" => "text/json" }
assert_response :internal_server_error
- assert_equal 'text/plain', response.content_type.to_s
+ assert_equal "text/plain", response.content_type.to_s
ensure
@app.instance_variable_set(:@exceptions_app, @exceptions_app)
$stderr = STDERR
diff --git a/actionpack/test/controller/streaming_test.rb b/actionpack/test/controller/streaming_test.rb
index 6ee6444065..5a42e2ae6d 100644
--- a/actionpack/test/controller/streaming_test.rb
+++ b/actionpack/test/controller/streaming_test.rb
@@ -1,10 +1,12 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module ActionController
class StreamingResponseTest < ActionController::TestCase
class TestController < ActionController::Base
def self.controller_path
- 'test'
+ "test"
end
def basic_stream
diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb
index b991232a14..6fc70d6248 100644
--- a/actionpack/test/controller/test_case_test.rb
+++ b/actionpack/test/controller/test_case_test.rb
@@ -1,83 +1,87 @@
-require 'abstract_unit'
-require 'controller/fake_controllers'
-require 'active_support/json/decoding'
-require 'rails/engine'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "controller/fake_controllers"
+require "active_support/json/decoding"
+require "rails/engine"
class TestCaseTest < ActionController::TestCase
+ def self.fixture_path; end
+
class TestController < ActionController::Base
def no_op
- render text: 'dummy'
+ render plain: "dummy"
end
def set_flash
flash["test"] = ">#{flash["test"]}<"
- render text: 'ignore me'
+ render plain: "ignore me"
end
def delete_flash
flash.delete("test")
- render :text => 'ignore me'
+ render plain: "ignore me"
end
def set_flash_now
flash.now["test_now"] = ">#{flash["test_now"]}<"
- render text: 'ignore me'
+ render plain: "ignore me"
end
def set_session
- session['string'] = 'A wonder'
- session[:symbol] = 'it works'
- render text: 'Success'
+ session["string"] = "A wonder"
+ session[:symbol] = "it works"
+ render plain: "Success"
end
def reset_the_session
reset_session
- render text: 'ignore me'
+ render plain: "ignore me"
end
def render_raw_post
raise ActiveSupport::TestCase::Assertion, "#raw_post is blank" if request.raw_post.blank?
- render text: request.raw_post
+ render plain: request.raw_post
end
def render_body
- render text: request.body.read
+ render plain: request.body.read
end
def test_params
- render text: ::JSON.dump(params)
+ render plain: ::JSON.dump(params.to_unsafe_h)
end
def test_query_parameters
- render text: ::JSON.dump(request.query_parameters)
+ render plain: ::JSON.dump(request.query_parameters)
end
def test_request_parameters
- render text: request.request_parameters.inspect
+ render plain: request.request_parameters.inspect
end
def test_uri
- render text: request.fullpath
+ render plain: request.fullpath
end
def test_format
- render text: request.format
+ render plain: request.format
end
def test_query_string
- render text: request.query_string
+ render plain: request.query_string
end
def test_protocol
- render text: request.protocol
+ render plain: request.protocol
end
def test_headers
- render text: request.headers.env.to_json
+ render plain: ::JSON.dump(request.headers.env)
end
def test_html_output
- render text: <<HTML
+ render plain: <<HTML
<html>
<body>
<a href="/"><img src="/images/button.png" /></a>
@@ -98,46 +102,50 @@ HTML
end
def test_xml_output
- response.content_type = "application/xml"
- render text: <<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
def test_only_one_param
- render text: (params[:left] && params[:right]) ? "EEP, Both here!" : "OK"
+ render plain: (params[:left] && params[:right]) ? "EEP, Both here!" : "OK"
end
def test_remote_addr
- render text: (request.remote_addr || "not specified")
+ render plain: (request.remote_addr || "not specified")
end
def test_file_upload
- render text: params[:file].size
+ render plain: params[:file].size
end
def test_send_file
- send_file(File.expand_path(__FILE__))
+ send_file(__FILE__)
end
def redirect_to_same_controller
- redirect_to controller: 'test', action: 'test_uri', id: 5
+ redirect_to controller: "test", action: "test_uri", id: 5
end
def redirect_to_different_controller
- redirect_to controller: 'fail', id: 5
+ redirect_to controller: "fail", id: 5
end
def create
- head :created, location: 'created resource'
+ head :created, location: "/resource"
+ end
+
+ def render_cookie
+ render plain: cookies["foo"]
end
def delete_cookie
cookies.delete("foo")
- head :ok
+ render plain: "ok"
end
def test_without_body
@@ -148,6 +156,10 @@ XML
render html: '<body class="foo"></body>'.html_safe
end
+ def boom
+ raise "boom!"
+ end
+
private
def generate_url(opts)
@@ -158,19 +170,21 @@ XML
def setup
super
@controller = TestController.new
- @request.env['PATH_INFO'] = nil
+ @request.delete_header "PATH_INFO"
@routes = ActionDispatch::Routing::RouteSet.new.tap do |r|
r.draw do
- get ':controller(/:action(/:id))'
+ ActiveSupport::Deprecation.silence do
+ get ":controller(/:action(/:id))"
+ end
end
end
end
class DefaultUrlOptionsCachingController < ActionController::Base
- before_action { @dynamic_opt = 'opt' }
+ before_action { @dynamic_opt = "opt" }
def test_url_options_reset
- render text: url_for(params)
+ render plain: url_for
end
def default_url_options
@@ -185,49 +199,62 @@ XML
def test_assert_select_without_body
get :test_without_body
- assert_select 'body', 0
- assert_select 'div.foo'
+ assert_select "body", 0
+ assert_select "div.foo"
end
def test_assert_select_with_body
get :test_with_body
- assert_select 'body.foo'
+ assert_select "body.foo"
end
def test_url_options_reset
@controller = DefaultUrlOptionsCachingController.new
get :test_url_options_reset
- assert_nil @request.params['dynamic_opt']
+ assert_nil @request.params["dynamic_opt"]
assert_match(/dynamic_opt=opt/, @response.body)
end
def test_raw_post_handling
- params = Hash[:page, { name: 'page name' }, 'some key', 123]
+ params = Hash[:page, { name: "page name" }, "some key", 123]
post :render_raw_post, params: params.dup
assert_equal params.to_query, @response.body
end
- def test_body_stream
- params = Hash[:page, { name: 'page name' }, 'some key', 123]
+ def test_params_round_trip
+ params = { "foo" => { "contents" => [{ "name" => "gorby", "id" => "123" }, { "name" => "puff", "d" => "true" }] } }
+ post :test_params, params: params.dup
- post :render_body, params: params.dup
+ controller_info = { "controller" => "test_case_test/test", "action" => "test_params" }
+ assert_equal params.merge(controller_info), JSON.parse(@response.body)
+ end
- assert_equal params.to_query, @response.body
+ def test_handle_to_params
+ klass = Class.new do
+ def to_param
+ "bar"
+ end
+ end
+
+ post :test_params, params: { foo: klass.new }
+
+ assert_equal JSON.parse(@response.body)["foo"], "bar"
end
- def test_deprecated_body_stream
- params = Hash[:page, { name: 'page name' }, 'some key', 123]
- assert_deprecated { post :render_body, params.dup }
+ def test_body_stream
+ params = Hash[:page, { name: "page name" }, "some key", 123]
+
+ post :render_body, params: params.dup
assert_equal params.to_query, @response.body
end
def test_document_body_and_params_with_post
post :test_params, params: { id: 1 }
- assert_equal({"id"=>"1", "controller"=>"test_case_test/test", "action"=>"test_params"}, ::JSON.parse(@response.body))
+ assert_equal({ "id" => "1", "controller" => "test_case_test/test", "action" => "test_params" }, ::JSON.parse(@response.body))
end
def test_document_body_with_post
@@ -235,21 +262,11 @@ XML
assert_equal "document body", @response.body
end
- def test_deprecated_document_body_with_post
- assert_deprecated { post :render_body, "document body" }
- assert_equal "document body", @response.body
- end
-
def test_document_body_with_put
put :render_body, body: "document body"
assert_equal "document body", @response.body
end
- def test_deprecated_document_body_with_put
- assert_deprecated { put :render_body, "document body" }
- assert_equal "document body", @response.body
- end
-
def test_head
head :test_params
assert_equal 200, @response.status
@@ -257,31 +274,21 @@ XML
def test_process_without_flash
process :set_flash
- assert_equal '><', flash['test']
- end
-
- def test_deprecated_process_with_flash
- assert_deprecated { process :set_flash, "GET", nil, nil, { "test" => "value" } }
- assert_equal '>value<', flash['test']
+ assert_equal "><", flash["test"]
end
def test_process_with_flash
process :set_flash,
method: "GET",
flash: { "test" => "value" }
- assert_equal '>value<', flash['test']
- end
-
- def test_deprecated_process_with_flash_now
- assert_deprecated { process :set_flash_now, "GET", nil, nil, { "test_now" => "value_now" } }
- assert_equal '>value_now<', flash['test_now']
+ assert_equal ">value<", flash["test"]
end
def test_process_with_flash_now
process :set_flash_now,
method: "GET",
flash: { "test_now" => "value_now" }
- assert_equal '>value_now<', flash['test_now']
+ assert_equal ">value_now<", flash["test_now"]
end
def test_process_delete_flash
@@ -293,64 +300,38 @@ XML
def test_process_with_session
process :set_session
- assert_equal 'A wonder', session['string'], "A value stored in the session should be available by string key"
- assert_equal 'A wonder', session[:string], "Test session hash should allow indifferent access"
- assert_equal 'it works', session['symbol'], "Test session hash should allow indifferent access"
- assert_equal 'it works', session[:symbol], "Test session hash should allow indifferent access"
- end
-
- def test_process_with_session_arg
- assert_deprecated { process :no_op, "GET", nil, { 'string' => 'value1', symbol: 'value2' } }
- assert_equal 'value1', session['string']
- assert_equal 'value1', session[:string]
- assert_equal 'value2', session['symbol']
- assert_equal 'value2', session[:symbol]
+ assert_equal "A wonder", session["string"], "A value stored in the session should be available by string key"
+ assert_equal "A wonder", session[:string], "Test session hash should allow indifferent access"
+ assert_equal "it works", session["symbol"], "Test session hash should allow indifferent access"
+ assert_equal "it works", session[:symbol], "Test session hash should allow indifferent access"
end
def test_process_with_session_kwarg
- process :no_op, method: "GET", session: { 'string' => 'value1', symbol: 'value2' }
- assert_equal 'value1', session['string']
- assert_equal 'value1', session[:string]
- assert_equal 'value2', session['symbol']
- assert_equal 'value2', session[:symbol]
- end
-
- def test_deprecated_process_merges_session_arg
- session[:foo] = 'bar'
- assert_deprecated {
- get :no_op, nil, { bar: 'baz' }
- }
- assert_equal 'bar', session[:foo]
- assert_equal 'baz', session[:bar]
+ process :no_op, method: "GET", session: { "string" => "value1", symbol: "value2" }
+ assert_equal "value1", session["string"]
+ assert_equal "value1", session[:string]
+ assert_equal "value2", session["symbol"]
+ assert_equal "value2", session[:symbol]
end
def test_process_merges_session_arg
- session[:foo] = 'bar'
- get :no_op, session: { bar: 'baz' }
- assert_equal 'bar', session[:foo]
- assert_equal 'baz', session[:bar]
- end
-
- def test_deprecated_merged_session_arg_is_retained_across_requests
- assert_deprecated {
- get :no_op, nil, { foo: 'bar' }
- }
- assert_equal 'bar', session[:foo]
- get :no_op
- assert_equal 'bar', session[:foo]
+ session[:foo] = "bar"
+ get :no_op, session: { bar: "baz" }
+ assert_equal "bar", session[:foo]
+ assert_equal "baz", session[:bar]
end
def test_merged_session_arg_is_retained_across_requests
- get :no_op, session: { foo: 'bar' }
- assert_equal 'bar', session[:foo]
+ get :no_op, session: { foo: "bar" }
+ assert_equal "bar", session[:foo]
get :no_op
- assert_equal 'bar', session[:foo]
+ assert_equal "bar", session[:foo]
end
def test_process_overwrites_existing_session_arg
- session[:foo] = 'bar'
- get :no_op, session: { foo: 'baz' }
- assert_equal 'baz', session[:foo]
+ session[:foo] = "bar"
+ get :no_op, session: { foo: "baz" }
+ assert_equal "baz", session[:foo]
end
def test_session_is_cleared_from_controller_after_reset_session
@@ -381,11 +362,6 @@ XML
assert_equal "/test_case_test/test/test_uri", @response.body
end
- def test_deprecated_process_with_request_uri_with_params
- assert_deprecated { process :test_uri, "GET", id: 7 }
- assert_equal "/test_case_test/test/test_uri/7", @response.body
- end
-
def test_process_with_request_uri_with_params
process :test_uri,
method: "GET",
@@ -394,14 +370,8 @@ XML
assert_equal "/test_case_test/test/test_uri/7", @response.body
end
- def test_deprecated_process_with_request_uri_with_params_with_explicit_uri
- @request.env['PATH_INFO'] = "/explicit/uri"
- assert_deprecated { process :test_uri, "GET", id: 7 }
- assert_equal "/explicit/uri", @response.body
- end
-
def test_process_with_request_uri_with_params_with_explicit_uri
- @request.env['PATH_INFO'] = "/explicit/uri"
+ @request.env["PATH_INFO"] = "/explicit/uri"
process :test_uri, method: "GET", params: { id: 7 }
assert_equal "/explicit/uri", @response.body
end
@@ -409,13 +379,13 @@ XML
def test_process_with_query_string
process :test_query_string,
method: "GET",
- params: { q: 'test' }
+ params: { q: "test" }
assert_equal "q=test", @response.body
end
def test_process_with_query_string_with_explicit_uri
- @request.env['PATH_INFO'] = '/explicit/uri'
- @request.env['QUERY_STRING'] = 'q=test?extra=question'
+ @request.env["PATH_INFO"] = "/explicit/uri"
+ @request.env["QUERY_STRING"] = "q=test?extra=question"
process :test_query_string
assert_equal "q=test?extra=question", @response.body
end
@@ -427,36 +397,42 @@ 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
+ # <area> auto-closes, so the <p> becomes a sibling
+ if defined?(JRUBY_VERSION)
+ # https://github.com/sparklemotion/nokogiri/issues/1653
+ # HTML parser "fixes" "broken" markup in slightly different ways
+ assert_select "root > map > area + p"
+ else
+ assert_select "root > area + p"
end
+ end
- assert err.empty?
+ def test_should_not_impose_childless_html_tags_in_xml
+ process :test_xml_output, params: { response_as: "application/xml" }
+
+ # <area> is not special, so the <p> is its child
+ assert_select "root > area > p"
end
def test_assert_generates
- assert_generates 'controller/action/5', controller: 'controller', action: 'action', id: '5'
- assert_generates 'controller/action/7', { id: "7" }, { controller: "controller", action: "action" }
- assert_generates 'controller/action/5', { controller: "controller", action: "action", id: "5", name: "bob" }, {}, { name: "bob" }
- assert_generates 'controller/action/7', { id: "7", name: "bob" }, { controller: "controller", action: "action" }, { name: "bob" }
- assert_generates 'controller/action/7', { id: "7" }, { controller: "controller", action: "action", name: "bob" }, {}
+ assert_generates "controller/action/5", controller: "controller", action: "action", id: "5"
+ assert_generates "controller/action/7", { id: "7" }, { controller: "controller", action: "action" }
+ assert_generates "controller/action/5", { controller: "controller", action: "action", id: "5", name: "bob" }, {}, { name: "bob" }
+ assert_generates "controller/action/7", { id: "7", name: "bob" }, { controller: "controller", action: "action" }, { name: "bob" }
+ assert_generates "controller/action/7", { id: "7" }, { controller: "controller", action: "action", name: "bob" }, {}
end
def test_assert_routing
- assert_routing 'content', controller: 'content', action: 'index'
+ assert_routing "content", controller: "content", action: "index"
end
def test_assert_routing_with_method
with_routing do |set|
set.draw { resources(:content) }
- assert_routing({ method: 'post', path: 'content' }, { controller: 'content', action: 'create' })
+ assert_routing({ method: "post", path: "content" }, { controller: "content", action: "create" })
end
end
@@ -464,125 +440,101 @@ XML
with_routing do |set|
set.draw do
namespace :admin do
- get 'user' => 'user#index'
+ get "user" => "user#index"
end
end
- assert_routing 'admin/user', controller: 'admin/user', action: 'index'
+ assert_routing "admin/user", controller: "admin/user", action: "index"
end
end
def test_assert_routing_with_glob
with_routing do |set|
- set.draw { get('*path' => "pages#show") }
- assert_routing('/company/about', { controller: 'pages', action: 'show', path: 'company/about' })
+ set.draw { get("*path" => "pages#show") }
+ assert_routing("/company/about", controller: "pages", action: "show", path: "company/about")
end
end
- def test_deprecated_params_passing
- assert_deprecated {
- get :test_params, page: { name: "Page name", month: '4', year: '2004', day: '6' }
- }
- parsed_params = ::JSON.parse(@response.body)
- assert_equal(
- {
- 'controller' => 'test_case_test/test', 'action' => 'test_params',
- 'page' => { 'name' => "Page name", 'month' => '4', 'year' => '2004', 'day' => '6' }
- },
- parsed_params
- )
- end
-
def test_params_passing
get :test_params, params: {
page: {
name: "Page name",
- month: '4',
- year: '2004',
- day: '6'
+ month: "4",
+ year: "2004",
+ day: "6"
}
}
parsed_params = ::JSON.parse(@response.body)
assert_equal(
{
- 'controller' => 'test_case_test/test', 'action' => 'test_params',
- 'page' => { 'name' => "Page name", 'month' => '4', 'year' => '2004', 'day' => '6' }
+ "controller" => "test_case_test/test", "action" => "test_params",
+ "page" => { "name" => "Page name", "month" => "4", "year" => "2004", "day" => "6" }
},
parsed_params
)
end
def test_query_param_named_action
- get :test_query_parameters, params: {action: 'foobar'}
+ get :test_query_parameters, params: { action: "foobar" }
parsed_params = JSON.parse(@response.body)
- assert_equal({'action' => 'foobar'}, parsed_params)
+ assert_equal({ "action" => "foobar" }, parsed_params)
end
def test_request_param_named_action
- post :test_request_parameters, params: {action: 'foobar'}
+ post :test_request_parameters, params: { action: "foobar" }
parsed_params = eval(@response.body)
- assert_equal({'action' => 'foobar'}, parsed_params)
+ assert_equal({ "action" => "foobar" }, parsed_params)
end
def test_kwarg_params_passing_with_session_and_flash
get :test_params, params: {
page: {
name: "Page name",
- month: '4',
- year: '2004',
- day: '6'
+ month: "4",
+ year: "2004",
+ day: "6"
}
- }, session: { 'foo' => 'bar' }, flash: { notice: 'created' }
+ }, session: { "foo" => "bar" }, flash: { notice: "created" }
parsed_params = ::JSON.parse(@response.body)
assert_equal(
- {'controller' => 'test_case_test/test', 'action' => 'test_params',
- 'page' => {'name' => "Page name", 'month' => '4', 'year' => '2004', 'day' => '6'}},
+ { "controller" => "test_case_test/test", "action" => "test_params",
+ "page" => { "name" => "Page name", "month" => "4", "year" => "2004", "day" => "6" } },
parsed_params
)
- assert_equal 'bar', session[:foo]
- assert_equal 'created', flash[:notice]
+ assert_equal "bar", session[:foo]
+ assert_equal "created", flash[:notice]
end
- def test_params_passing_with_fixnums
+ def test_params_passing_with_integer
get :test_params, params: {
page: { name: "Page name", month: 4, year: 2004, day: 6 }
}
parsed_params = ::JSON.parse(@response.body)
assert_equal(
- {'controller' => 'test_case_test/test', 'action' => 'test_params',
- 'page' => {'name' => "Page name", 'month' => '4', 'year' => '2004', 'day' => '6'}},
+ { "controller" => "test_case_test/test", "action" => "test_params",
+ "page" => { "name" => "Page name", "month" => "4", "year" => "2004", "day" => "6" } },
parsed_params
)
end
- def test_params_passing_with_fixnums_when_not_html_request
- get :test_params, params: { format: 'json', count: 999 }
+ def test_params_passing_with_integers_when_not_html_request
+ get :test_params, params: { format: "json", count: 999 }
parsed_params = ::JSON.parse(@response.body)
assert_equal(
- {'controller' => 'test_case_test/test', 'action' => 'test_params',
- 'format' => 'json', 'count' => '999' },
+ { "controller" => "test_case_test/test", "action" => "test_params",
+ "format" => "json", "count" => "999" },
parsed_params
)
end
def test_params_passing_path_parameter_is_string_when_not_html_request
- get :test_params, params: { format: 'json', id: 1 }
+ get :test_params, params: { format: "json", id: 1 }
parsed_params = ::JSON.parse(@response.body)
assert_equal(
- {'controller' => 'test_case_test/test', 'action' => 'test_params',
- 'format' => 'json', 'id' => '1' },
- parsed_params
- )
- end
-
- def test_deprecated_params_passing_path_parameter_is_string_when_not_html_request
- assert_deprecated { get :test_params, format: 'json', id: 1 }
- parsed_params = ::JSON.parse(@response.body)
- assert_equal(
- {'controller' => 'test_case_test/test', 'action' => 'test_params',
- 'format' => 'json', 'id' => '1' },
+ { "controller" => "test_case_test/test", "action" => "test_params",
+ "format" => "json", "id" => "1" },
parsed_params
)
end
@@ -590,13 +542,13 @@ XML
def test_params_passing_with_frozen_values
assert_nothing_raised do
get :test_params, params: {
- frozen: 'icy'.freeze, frozens: ['icy'.freeze].freeze, deepfreeze: { frozen: 'icy'.freeze }.freeze
+ frozen: -"icy", frozens: [-"icy"].freeze, deepfreeze: { frozen: -"icy" }.freeze
}
end
parsed_params = ::JSON.parse(@response.body)
assert_equal(
- {'controller' => 'test_case_test/test', 'action' => 'test_params',
- 'frozen' => 'icy', 'frozens' => ['icy'], 'deepfreeze' => { 'frozen' => 'icy' }},
+ { "controller" => "test_case_test/test", "action" => "test_params",
+ "frozen" => "icy", "frozens" => ["icy"], "deepfreeze" => { "frozen" => "icy" } },
parsed_params
)
end
@@ -608,8 +560,8 @@ XML
end
test "set additional HTTP headers" do
- @request.headers['Referer'] = "http://nohost.com/home"
- @request.headers['Content-Type'] = "application/rss+xml"
+ @request.headers["Referer"] = "http://nohost.com/home"
+ @request.headers["Content-Type"] = "application/rss+xml"
get :test_headers
parsed_env = ActiveSupport::JSON.decode(@response.body)
assert_equal "http://nohost.com/home", parsed_env["HTTP_REFERER"]
@@ -617,14 +569,53 @@ XML
end
test "set additional env variables" do
- @request.headers['HTTP_REFERER'] = "http://example.com/about"
- @request.headers['CONTENT_TYPE'] = "application/json"
+ @request.headers["HTTP_REFERER"] = "http://example.com/about"
+ @request.headers["CONTENT_TYPE"] = "application/json"
get :test_headers
parsed_env = ActiveSupport::JSON.decode(@response.body)
assert_equal "http://example.com/about", parsed_env["HTTP_REFERER"]
assert_equal "application/json", parsed_env["CONTENT_TYPE"]
end
+ def test_using_as_json_sets_request_content_type_to_json
+ post :render_body, params: { bool_value: true, str_value: "string", num_value: 2 }, as: :json
+
+ assert_equal "application/json", @request.headers["CONTENT_TYPE"]
+ assert_equal true, @request.request_parameters[:bool_value]
+ assert_equal "string", @request.request_parameters[:str_value]
+ assert_equal 2, @request.request_parameters[:num_value]
+ end
+
+ def test_using_as_json_sets_format_json
+ post :render_body, params: { bool_value: true, str_value: "string", num_value: 2 }, as: :json
+ assert_equal "json", @request.format
+ end
+
+ def test_mutating_content_type_headers_for_plain_text_files_sets_the_header
+ @request.headers["Content-Type"] = "text/plain"
+ post :render_body, params: { name: "foo.txt" }
+
+ assert_equal "text/plain", @request.headers["Content-type"]
+ assert_equal "foo.txt", @request.request_parameters[:name]
+ assert_equal "render_body", @request.path_parameters[:action]
+ end
+
+ def test_mutating_content_type_headers_for_html_files_sets_the_header
+ @request.headers["Content-Type"] = "text/html"
+ post :render_body, params: { name: "foo.html" }
+
+ assert_equal "text/html", @request.headers["Content-type"]
+ assert_equal "foo.html", @request.request_parameters[:name]
+ assert_equal "render_body", @request.path_parameters[:action]
+ end
+
+ def test_mutating_content_type_headers_for_non_registered_mime_type_raises_an_error
+ assert_raises(RuntimeError) do
+ @request.headers["Content-Type"] = "type/fake"
+ post :render_body, params: { name: "foo.fake" }
+ end
+ end
+
def test_id_converted_to_string
get :test_params, params: {
id: 20, foo: Object.new
@@ -632,21 +623,19 @@ XML
assert_kind_of String, @request.path_parameters[:id]
end
- def test_deprecared_id_converted_to_string
- assert_deprecated { get :test_params, id: 20, foo: Object.new}
- assert_kind_of String, @request.path_parameters[:id]
- end
-
def test_array_path_parameter_handled_properly
with_routing do |set|
set.draw do
- get 'file/*path', to: 'test_case_test/test#test_params'
- get ':controller/:action'
+ get "file/*path", to: "test_case_test/test#test_params"
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action"
+ end
end
- get :test_params, params: { path: ['hello', 'world'] }
- assert_equal ['hello', 'world'], @request.path_parameters[:path]
- assert_equal 'hello/world', @request.path_parameters[:path].to_param
+ get :test_params, params: { path: ["hello", "world"] }
+ assert_equal ["hello", "world"], @request.path_parameters[:path]
+ assert_equal "hello/world", @request.path_parameters[:path].to_param
end
end
@@ -664,8 +653,8 @@ XML
routes_id = @routes.object_id
begin
- with_routing { raise 'fail' }
- fail 'Should not be here.'
+ with_routing { raise "fail" }
+ fail "Should not be here."
rescue RuntimeError
end
@@ -684,46 +673,23 @@ XML
def test_header_properly_reset_after_remote_http_request
get :test_params, xhr: true
- assert_nil @request.env['HTTP_X_REQUESTED_WITH']
- assert_nil @request.env['HTTP_ACCEPT']
- end
-
- def test_deprecated_xhr_with_params
- assert_deprecated { xhr :get, :test_params, params: { id: 1 } }
-
- assert_equal({"id"=>"1", "controller"=>"test_case_test/test", "action"=>"test_params"}, ::JSON.parse(@response.body))
+ assert_nil @request.env["HTTP_X_REQUESTED_WITH"]
+ assert_nil @request.env["HTTP_ACCEPT"]
end
def test_xhr_with_params
get :test_params, params: { id: 1 }, xhr: true
- assert_equal({"id"=>"1", "controller"=>"test_case_test/test", "action"=>"test_params"}, ::JSON.parse(@response.body))
+ assert_equal({ "id" => "1", "controller" => "test_case_test/test", "action" => "test_params" }, ::JSON.parse(@response.body))
end
def test_xhr_with_session
get :set_session, xhr: true
- assert_equal 'A wonder', session['string'], "A value stored in the session should be available by string key"
- assert_equal 'A wonder', session[:string], "Test session hash should allow indifferent access"
- assert_equal 'it works', session['symbol'], "Test session hash should allow indifferent access"
- assert_equal 'it works', session[:symbol], "Test session hash should allow indifferent access"
- end
-
- def test_deprecated_xhr_with_session
- assert_deprecated { xhr :get, :set_session }
-
- assert_equal 'A wonder', session['string'], "A value stored in the session should be available by string key"
- assert_equal 'A wonder', session[:string], "Test session hash should allow indifferent access"
- assert_equal 'it works', session['symbol'], "Test session hash should allow indifferent access"
- assert_equal 'it works', session[:symbol], "Test session hash should allow indifferent access"
- end
-
- def test_deprecated_params_reset_between_post_requests
- assert_deprecated { post :no_op, foo: "bar" }
- assert_equal "bar", @request.params[:foo]
-
- post :no_op
- assert @request.params[:foo].blank?
+ assert_equal "A wonder", session["string"], "A value stored in the session should be available by string key"
+ assert_equal "A wonder", session[:string], "Test session hash should allow indifferent access"
+ assert_equal "it works", session["symbol"], "Test session hash should allow indifferent access"
+ assert_equal "it works", session[:symbol], "Test session hash should allow indifferent access"
end
def test_params_reset_between_post_requests
@@ -731,7 +697,7 @@ XML
assert_equal "bar", @request.params[:foo]
post :no_op
- assert @request.params[:foo].blank?
+ assert_predicate @request.params[:foo], :blank?
end
def test_filtered_parameters_reset_between_requests
@@ -742,6 +708,27 @@ XML
assert_equal "baz", @request.filtered_parameters[:foo]
end
+ def test_raw_post_reset_between_post_requests
+ post :no_op, params: { foo: "bar" }
+ assert_equal "foo=bar", @request.raw_post
+
+ post :no_op, params: { foo: "baz" }
+ assert_equal "foo=baz", @request.raw_post
+ end
+
+ def test_content_length_reset_after_post_request
+ post :no_op, params: { foo: "bar" }
+ assert_not_equal 0, @request.content_length
+
+ get :no_op
+ assert_equal 0, @request.content_length
+ 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]
@@ -764,48 +751,70 @@ XML
end
def test_request_format
- get :test_format, params: { format: 'html' }
- assert_equal 'text/html', @response.body
+ get :test_format, params: { format: "html" }
+ assert_equal "text/html", @response.body
- get :test_format, params: { format: 'json' }
- assert_equal 'application/json', @response.body
+ get :test_format, params: { format: "json" }
+ assert_equal "application/json", @response.body
- get :test_format, params: { format: 'xml' }
- assert_equal 'application/xml', @response.body
+ get :test_format, params: { format: "xml" }
+ assert_equal "application/xml", @response.body
get :test_format
- assert_equal 'text/html', @response.body
+ assert_equal "text/html", @response.body
end
def test_request_format_kwarg
- get :test_format, format: 'html'
- assert_equal 'text/html', @response.body
+ get :test_format, format: "html"
+ assert_equal "text/html", @response.body
- get :test_format, format: 'json'
- assert_equal 'application/json', @response.body
+ get :test_format, format: "json"
+ assert_equal "application/json", @response.body
- get :test_format, format: 'xml'
- assert_equal 'application/xml', @response.body
+ get :test_format, format: "xml"
+ assert_equal "application/xml", @response.body
get :test_format
- assert_equal 'text/html', @response.body
+ assert_equal "text/html", @response.body
end
def test_request_format_kwarg_overrides_params
- get :test_format, format: 'json', params: { format: 'html' }
- assert_equal 'application/json', @response.body
+ get :test_format, format: "json", params: { format: "html" }
+ assert_equal "application/json", @response.body
+ end
+
+ def test_request_format_kwarg_doesnt_mutate_params
+ params = { foo: "bar" }.freeze
+
+ assert_nothing_raised do
+ get :test_format, format: "json", params: params
+ end
end
def test_should_have_knowledge_of_client_side_cookie_state_even_if_they_are_not_set
- cookies['foo'] = 'bar'
+ cookies["foo"] = "bar"
get :no_op
- assert_equal 'bar', cookies['foo']
+ assert_equal "bar", cookies["foo"]
+ end
+
+ def test_cookies_should_be_escaped_properly
+ cookies["foo"] = "+"
+ get :render_cookie
+ assert_equal "+", @response.body
end
def test_should_detect_if_cookie_is_deleted
- cookies['foo'] = 'bar'
+ cookies["foo"] = "bar"
get :delete_cookie
- assert_nil cookies['foo']
+ assert_nil cookies["foo"]
+ end
+
+ def test_multiple_mixed_method_process_should_scrub_rack_input
+ post :test_params, params: { id: 1, foo: "an foo" }
+ assert_equal({ "id" => "1", "foo" => "an foo", "controller" => "test_case_test/test", "action" => "test_params" }, ::JSON.parse(@response.body))
+
+ get :test_params, params: { bar: "an bar" }
+ assert_equal({ "bar" => "an bar", "controller" => "test_case_test/test", "action" => "test_params" }, ::JSON.parse(@response.body))
end
%w(controller response request).each do |variable|
@@ -824,15 +833,15 @@ XML
end
end
- FILES_DIR = File.dirname(__FILE__) + '/../fixtures/multipart'
+ FILES_DIR = File.expand_path("../fixtures/multipart", __dir__)
- READ_BINARY = 'rb:binary'
- READ_PLAIN = 'r:binary'
+ READ_BINARY = "rb:binary"
+ READ_PLAIN = "r:binary"
def test_test_uploaded_file
- filename = 'mona_lisa.jpg'
+ filename = "ruby_on_rails.jpg"
path = "#{FILES_DIR}/#{filename}"
- content_type = 'image/png'
+ content_type = "image/png"
expected = File.read(path)
expected.force_encoding(Encoding::BINARY)
@@ -845,20 +854,19 @@ 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
- TestCaseTest.stubs(:fixture_path).returns(FILES_DIR)
-
- uploaded_file = fixture_file_upload('/mona_lisa.jpg', 'image/png')
- assert_equal File.open("#{FILES_DIR}/mona_lisa.jpg", READ_PLAIN).read, uploaded_file.read
+ TestCaseTest.stub :fixture_path, FILES_DIR do
+ uploaded_file = fixture_file_upload("/ruby_on_rails.jpg", "image/png")
+ assert_equal File.open("#{FILES_DIR}/ruby_on_rails.jpg", READ_PLAIN).read, uploaded_file.read
+ end
end
def test_test_uploaded_file_with_binary
- filename = 'mona_lisa.jpg'
+ filename = "ruby_on_rails.jpg"
path = "#{FILES_DIR}/#{filename}"
- content_type = 'image/png'
+ content_type = "image/png"
binary_uploaded_file = Rack::Test::UploadedFile.new(path, content_type, :binary)
assert_equal File.open(path, READ_BINARY).read, binary_uploaded_file.read
@@ -868,9 +876,9 @@ XML
end
def test_fixture_file_upload_with_binary
- filename = 'mona_lisa.jpg'
+ filename = "ruby_on_rails.jpg"
path = "#{FILES_DIR}/#{filename}"
- content_type = 'image/jpg'
+ content_type = "image/jpg"
binary_file_upload = fixture_file_upload(path, content_type, :binary)
assert_equal File.open(path, READ_BINARY).read, binary_file_upload.read
@@ -880,50 +888,48 @@ XML
end
def test_fixture_file_upload_should_be_able_access_to_tempfile
- file = fixture_file_upload(FILES_DIR + "/mona_lisa.jpg", "image/jpg")
- assert file.respond_to?(:tempfile), "expected tempfile should respond on fixture file object, got nothing"
+ file = fixture_file_upload(FILES_DIR + "/ruby_on_rails.jpg", "image/jpg")
+ assert_respond_to file, :tempfile
end
def test_fixture_file_upload
post :test_file_upload,
params: {
- file: fixture_file_upload(FILES_DIR + "/mona_lisa.jpg", "image/jpg")
+ file: fixture_file_upload(FILES_DIR + "/ruby_on_rails.jpg", "image/jpg")
}
- assert_equal '159528', @response.body
+ assert_equal "45142", @response.body
end
def test_fixture_file_upload_relative_to_fixture_path
- TestCaseTest.stubs(:fixture_path).returns(FILES_DIR)
- uploaded_file = fixture_file_upload("mona_lisa.jpg", "image/jpg")
- assert_equal File.open("#{FILES_DIR}/mona_lisa.jpg", READ_PLAIN).read, uploaded_file.read
+ TestCaseTest.stub :fixture_path, FILES_DIR do
+ uploaded_file = fixture_file_upload("ruby_on_rails.jpg", "image/jpg")
+ assert_equal File.open("#{FILES_DIR}/ruby_on_rails.jpg", READ_PLAIN).read, uploaded_file.read
+ end
end
- def test_fixture_file_upload_ignores_nil_fixture_path
- TestCaseTest.stubs(:fixture_path).returns(nil)
- uploaded_file = fixture_file_upload("#{FILES_DIR}/mona_lisa.jpg", "image/jpg")
- assert_equal File.open("#{FILES_DIR}/mona_lisa.jpg", READ_PLAIN).read, uploaded_file.read
+ def test_fixture_file_upload_ignores_fixture_path_given_full_path
+ TestCaseTest.stub :fixture_path, __dir__ do
+ uploaded_file = fixture_file_upload("#{FILES_DIR}/ruby_on_rails.jpg", "image/jpg")
+ assert_equal File.open("#{FILES_DIR}/ruby_on_rails.jpg", READ_PLAIN).read, uploaded_file.read
+ end
end
- def test_deprecated_action_dispatch_uploaded_file_upload
- filename = 'mona_lisa.jpg'
- path = "#{FILES_DIR}/#{filename}"
- assert_deprecated {
- post :test_file_upload, file: Rack::Test::UploadedFile.new(path, "image/jpg", true)
- }
- assert_equal '159528', @response.body
+ def test_fixture_file_upload_ignores_nil_fixture_path
+ uploaded_file = fixture_file_upload("#{FILES_DIR}/ruby_on_rails.jpg", "image/jpg")
+ assert_equal File.open("#{FILES_DIR}/ruby_on_rails.jpg", READ_PLAIN).read, uploaded_file.read
end
def test_action_dispatch_uploaded_file_upload
- filename = 'mona_lisa.jpg'
+ filename = "ruby_on_rails.jpg"
path = "#{FILES_DIR}/#{filename}"
post :test_file_upload, params: {
file: Rack::Test::UploadedFile.new(path, "image/jpg", true)
}
- assert_equal '159528', @response.body
+ assert_equal "45142", @response.body
end
def test_test_uploaded_file_exception_when_file_doesnt_exist
- assert_raise(RuntimeError) { Rack::Test::UploadedFile.new('non_existent_file') }
+ assert_raise(RuntimeError) { Rack::Test::UploadedFile.new("non_existent_file") }
end
def test_redirect_url_only_cares_about_location_header
@@ -931,27 +937,52 @@ 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
+
+ def test_exception_in_action_reaches_test
+ assert_raise(RuntimeError) do
+ process :boom, method: "GET"
+ end
+ end
+
+ def test_request_state_is_cleared_after_exception
+ assert_raise(RuntimeError) do
+ process :boom,
+ method: "GET",
+ params: { q: "test1" }
+ end
+
+ process :test_query_string,
+ method: "GET",
+ params: { q: "test2" }
+
+ assert_equal "q=test2", @response.body
+ end
end
class ResponseDefaultHeadersTest < ActionController::TestCase
class TestController < ActionController::Base
def remove_header
headers.delete params[:header]
- head :ok, 'C' => '3'
+ head :ok, "C" => "3"
+ end
+
+ # Render a head response, but don't touch default headers
+ def leave_alone
+ head :ok
end
end
def before_setup
@original = ActionDispatch::Response.default_headers
- @defaults = { 'A' => '1', 'B' => '2' }
+ @defaults = { "A" => "1", "B" => "2" }
ActionDispatch::Response.default_headers = @defaults
super
end
@@ -963,26 +994,32 @@ class ResponseDefaultHeadersTest < ActionController::TestCase
def setup
super
@controller = TestController.new
- @request.env['PATH_INFO'] = nil
+ @request.env["PATH_INFO"] = nil
@routes = ActionDispatch::Routing::RouteSet.new.tap do |r|
r.draw do
- get ':controller(/:action(/:id))'
+ ActiveSupport::Deprecation.silence do
+ get ":controller(/:action(/:id))"
+ end
end
end
end
test "response contains default headers" do
+ get :leave_alone
+
# Response headers start out with the defaults
- assert_equal @defaults, response.headers
+ assert_equal @defaults.merge("Content-Type" => "text/html"), response.headers
+ end
- get :remove_header, params: { header: 'A' }
+ test "response deletes a default header" do
+ get :remove_header, params: { header: "A" }
assert_response :ok
# After a request, the response in the test case doesn't have the
# defaults merged on top again.
- assert_not_includes response.headers, 'A'
- assert_includes response.headers, 'B'
- assert_includes response.headers, 'C'
+ assert_not_includes response.headers, "A"
+ assert_includes response.headers, "B"
+ assert_includes response.headers, "C"
end
end
@@ -991,13 +1028,13 @@ module EngineControllerTests
isolate_namespace EngineControllerTests
routes.draw do
- get '/' => 'bar#index'
+ get "/" => "bar#index"
end
end
class BarController < ActionController::Base
def index
- render text: 'bar'
+ render plain: "bar"
end
end
@@ -1006,7 +1043,7 @@ module EngineControllerTests
def test_engine_controller_route
get :index
- assert_equal @response.body, 'bar'
+ assert_equal @response.body, "bar"
end
end
@@ -1019,7 +1056,7 @@ module EngineControllerTests
def test_engine_controller_route
get :index
- assert_equal @response.body, 'bar'
+ assert_equal @response.body, "bar"
end
end
end
@@ -1060,7 +1097,7 @@ class CrazySymbolNameTest < ActionController::TestCase
end
class CrazyStringNameTest < ActionController::TestCase
- tests 'content'
+ tests "content"
def test_set_controller_class_using_string
assert_equal ContentController, self.class.controller_class
@@ -1073,8 +1110,8 @@ class NamedRoutesControllerTest < ActionController::TestCase
def test_should_be_able_to_use_named_routes_before_a_request_is_done
with_routing do |set|
set.draw { resources :contents }
- assert_equal 'http://test.host/contents/new', new_content_url
- assert_equal 'http://test.host/contents/1', content_url(id: 1)
+ assert_equal "http://test.host/contents/new", new_content_url
+ assert_equal "http://test.host/contents/1", content_url(id: 1)
end
end
end
@@ -1083,20 +1120,22 @@ class AnonymousControllerTest < ActionController::TestCase
def setup
@controller = Class.new(ActionController::Base) do
def index
- render text: params[:controller]
+ render plain: params[:controller]
end
end.new
@routes = ActionDispatch::Routing::RouteSet.new.tap do |r|
r.draw do
- get ':controller(/:action(/:id))'
+ ActiveSupport::Deprecation.silence do
+ get ":controller(/:action(/:id))"
+ end
end
end
end
def test_controller_name
get :index
- assert_equal 'anonymous', @response.body
+ assert_equal "anonymous", @response.body
end
end
@@ -1104,29 +1143,29 @@ class RoutingDefaultsTest < ActionController::TestCase
def setup
@controller = Class.new(ActionController::Base) do
def post
- render text: request.fullpath
+ render plain: request.fullpath
end
def project
- render text: request.fullpath
+ render plain: request.fullpath
end
end.new
@routes = ActionDispatch::Routing::RouteSet.new.tap do |r|
r.draw do
- get '/posts/:id', to: 'anonymous#post', bucket_type: 'post'
- get '/projects/:id', to: 'anonymous#project', defaults: { bucket_type: 'project' }
+ get "/posts/:id", to: "anonymous#post", bucket_type: "post"
+ get "/projects/:id", to: "anonymous#project", defaults: { bucket_type: "project" }
end
end
end
def test_route_option_can_be_passed_via_process
- get :post, params: { id: 1, bucket_type: 'post'}
- assert_equal '/posts/1', @response.body
+ get :post, params: { id: 1, bucket_type: "post" }
+ assert_equal "/posts/1", @response.body
end
def test_route_default_is_not_required_for_building_request_uri
get :project, params: { id: 2 }
- assert_equal '/projects/2', @response.body
+ assert_equal "/projects/2", @response.body
end
end
diff --git a/actionpack/test/controller/url_for_integration_test.rb b/actionpack/test/controller/url_for_integration_test.rb
index 0e4c2b7c32..a1521da702 100644
--- a/actionpack/test/controller/url_for_integration_test.rb
+++ b/actionpack/test/controller/url_for_integration_test.rb
@@ -1,6 +1,8 @@
-require 'abstract_unit'
-require 'controller/fake_controllers'
-require 'active_support/core_ext/object/with_options'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "controller/fake_controllers"
+require "active_support/core_ext/object/with_options"
module ActionPack
class URLForIntegrationTest < ActiveSupport::TestCase
@@ -14,51 +16,53 @@ module ActionPack
resources :users, :posts
end
- namespace 'api' do
- root :to => 'users#index'
+ namespace "api" do
+ root to: "users#index"
end
- get '/blog(/:year(/:month(/:day)))' => 'posts#show_date',
+ get "/blog(/:year(/:month(/:day)))" => "posts#show_date",
:constraints => {
- :year => /(19|20)\d\d/,
- :month => /[01]?\d/,
- :day => /[0-3]?\d/
+ year: /(19|20)\d\d/,
+ month: /[01]?\d/,
+ day: /[0-3]?\d/
},
:day => nil,
:month => nil
- get 'archive/:year', :controller => 'archive', :action => 'index',
- :defaults => { :year => nil },
- :constraints => { :year => /\d{4}/ },
- :as => "blog"
+ get "archive/:year", controller: "archive", action: "index",
+ defaults: { year: nil },
+ constraints: { year: /\d{4}/ },
+ as: "blog"
resources :people
- #match 'legacy/people' => "people#index", :legacy => "true"
-
- get 'symbols', :controller => :symbols, :action => :show, :name => :as_symbol
- get 'id_default(/:id)' => "foo#id_default", :id => 1
- match 'get_or_post' => "foo#get_or_post", :via => [:get, :post]
- get 'optional/:optional' => "posts#index"
- get 'projects/:project_id' => "project#index", :as => "project"
- get 'clients' => "projects#index"
-
- get 'ignorecase/geocode/:postalcode' => 'geocode#show', :postalcode => /hx\d\d-\d[a-z]{2}/i
- get 'extended/geocode/:postalcode' => 'geocode#show',:constraints => {
- :postalcode => /# Postcode format
+
+ get "symbols", controller: :symbols, action: :show, name: :as_symbol
+ get "id_default(/:id)" => "foo#id_default", :id => 1
+ match "get_or_post" => "foo#get_or_post", :via => [:get, :post]
+ get "optional/:optional" => "posts#index"
+ get "projects/:project_id" => "project#index", :as => "project"
+ get "clients" => "projects#index"
+
+ get "ignorecase/geocode/:postalcode" => "geocode#show", :postalcode => /hx\d\d-\d[a-z]{2}/i
+ get "extended/geocode/:postalcode" => "geocode#show", :constraints => {
+ postalcode: /# Postcode format
\d{5} #Prefix
(-\d{4})? #Suffix
/x
}, :as => "geocode"
- get 'news(.:format)' => "news#index"
+ get "news(.:format)" => "news#index"
+
+ ActiveSupport::Deprecation.silence {
+ get "comment/:id(/:action)" => "comments#show"
+ get "ws/:controller(/:action(/:id))", ws: true
+ get "account(/:action)" => "account#subscription"
+ get "pages/:page_id/:controller(/:action(/:id))"
+ get ":controller/ping", action: "ping"
+ get ":controller(/:action(/:id))(.:format)"
+ }
- get 'comment/:id(/:action)' => "comments#show"
- get 'ws/:controller(/:action(/:id))', :ws => true
- get 'account(/:action)' => "account#subscription"
- get 'pages/:page_id/:controller(/:action(/:id))'
- get ':controller/ping', :action => 'ping'
- get ':controller(/:action(/:id))(.:format)'
- root :to => "news#index"
+ root to: "news#index"
}
attr_reader :routes
@@ -70,110 +74,111 @@ module ActionPack
end
[
- ['/admin/users',[ { :use_route => 'admin_users' }]],
- ['/admin/users',[ { :controller => 'admin/users' }]],
- ['/admin/users',[ { :controller => 'admin/users', :action => 'index' }]],
- ['/admin/users',[ { :action => 'index' }, { :controller => 'admin/users', :action => 'index' }, '/admin/users']],
- ['/admin/users',[ { :controller => 'users', :action => 'index' }, { :controller => 'admin/accounts', :action => 'show', :id => '1' }, '/admin/accounts/show/1']],
- ['/people',[ { :controller => '/people', :action => 'index' }, {:controller=>"admin/accounts", :action=>"foo", :id=>"bar"}, '/admin/accounts/foo/bar']],
-
- ['/admin/posts',[ { :controller => 'admin/posts' }]],
- ['/admin/posts/new',[ { :controller => 'admin/posts', :action => 'new' }]],
-
- ['/blog/2009',[ { :controller => 'posts', :action => 'show_date', :year => 2009 }]],
- ['/blog/2009/1',[ { :controller => 'posts', :action => 'show_date', :year => 2009, :month => 1 }]],
- ['/blog/2009/1/1',[ { :controller => 'posts', :action => 'show_date', :year => 2009, :month => 1, :day => 1 }]],
-
- ['/archive/2010',[ { :controller => 'archive', :action => 'index', :year => '2010' }]],
- ['/archive',[ { :controller => 'archive', :action => 'index' }]],
- ['/archive?year=january',[ { :controller => 'archive', :action => 'index', :year => 'january' }]],
-
- ['/people',[ { :controller => 'people', :action => 'index' }]],
- ['/people',[ { :action => 'index' }, { :controller => 'people', :action => 'index' }, '/people']],
- ['/people',[ { :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' }, '/people/show/1']],
- ['/people',[ { :controller => 'people', :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' }, '/people/show/1']],
- ['/people',[ {}, { :controller => 'people', :action => 'index' }, '/people']],
- ['/people/1',[ { :controller => 'people', :action => 'show' }, { :controller => 'people', :action => 'show', :id => '1' }, '/people/show/1']],
- ['/people/new',[ { :use_route => 'new_person' }]],
- ['/people/new',[ { :controller => 'people', :action => 'new' }]],
- ['/people/1',[ { :use_route => 'person', :id => '1' }]],
- ['/people/1',[ { :controller => 'people', :action => 'show', :id => '1' }]],
- ['/people/1.xml',[ { :controller => 'people', :action => 'show', :id => '1', :format => 'xml' }]],
- ['/people/1',[ { :controller => 'people', :action => 'show', :id => 1 }]],
- ['/people/1',[ { :controller => 'people', :action => 'show', :id => Model.new('1') }]],
- ['/people/1',[ { :action => 'show', :id => '1' }, { :controller => 'people', :action => 'index' }, '/people']],
- ['/people/1',[ { :action => 'show', :id => 1 }, { :controller => 'people', :action => 'show', :id => '1' }, '/people/show/1']],
- ['/people',[ { :controller => 'people', :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' }, '/people/show/1']],
- ['/people/1',[ {}, { :controller => 'people', :action => 'show', :id => '1' }, '/people/show/1']],
- ['/people/1',[ { :controller => 'people', :action => 'show' }, { :controller => 'people', :action => 'index', :id => '1' }, '/people/index/1']],
- ['/people/1/edit',[ { :controller => 'people', :action => 'edit', :id => '1' }]],
- ['/people/1/edit.xml',[ { :controller => 'people', :action => 'edit', :id => '1', :format => 'xml' }]],
- ['/people/1/edit',[ { :use_route => 'edit_person', :id => '1' }]],
- ['/people/1?legacy=true',[ { :controller => 'people', :action => 'show', :id => '1', :legacy => 'true' }]],
- ['/people?legacy=true',[ { :controller => 'people', :action => 'index', :legacy => 'true' }]],
-
- ['/id_default/2',[ { :controller => 'foo', :action => 'id_default', :id => '2' }]],
- ['/id_default',[ { :controller => 'foo', :action => 'id_default', :id => '1' }]],
- ['/id_default',[ { :controller => 'foo', :action => 'id_default', :id => 1 }]],
- ['/id_default',[ { :controller => 'foo', :action => 'id_default' }]],
- ['/optional/bar',[ { :controller => 'posts', :action => 'index', :optional => 'bar' }]],
- ['/posts',[ { :controller => 'posts', :action => 'index' }]],
-
- ['/project',[ { :controller => 'project', :action => 'index' }]],
- ['/projects/1',[ { :controller => 'project', :action => 'index', :project_id => '1' }]],
- ['/projects/1',[ { :controller => 'project', :action => 'index'}, {:project_id => '1', :controller => 'project', :action => 'index' }, '/projects/1']],
- ['/projects/1',[ { :use_route => 'project', :controller => 'project', :action => 'index', :project_id => '1' }]],
- ['/projects/1',[ { :use_route => 'project', :controller => 'project', :action => 'index' }, { :controller => 'project', :action => 'index', :project_id => '1' }, '/projects/1']],
-
- ['/clients',[ { :controller => 'projects', :action => 'index' }]],
- ['/clients?project_id=1',[ { :controller => 'projects', :action => 'index', :project_id => '1' }]],
- ['/clients',[ { :controller => 'projects', :action => 'index' }, { :project_id => '1', :controller => 'project', :action => 'index' }, '/projects/1']],
-
- ['/comment/20',[ { :id => 20 }, { :controller => 'comments', :action => 'show' }, '/comments/show']],
- ['/comment/20',[ { :controller => 'comments', :id => 20, :action => 'show' }]],
- ['/comments/boo',[ { :controller => 'comments', :action => 'boo' }]],
-
- ['/ws/posts/show/1',[ { :controller => 'posts', :action => 'show', :id => '1', :ws => true }]],
- ['/ws/posts',[ { :controller => 'posts', :action => 'index', :ws => true }]],
-
- ['/account',[ { :controller => 'account', :action => 'subscription' }]],
- ['/account/billing',[ { :controller => 'account', :action => 'billing' }]],
-
- ['/pages/1/notes/show/1',[ { :page_id => '1', :controller => 'notes', :action => 'show', :id => '1' }]],
- ['/pages/1/notes/list',[ { :page_id => '1', :controller => 'notes', :action => 'list' }]],
- ['/pages/1/notes',[ { :page_id => '1', :controller => 'notes', :action => 'index' }]],
- ['/pages/1/notes',[ { :page_id => '1', :controller => 'notes' }]],
- ['/notes',[ { :page_id => nil, :controller => 'notes' }]],
- ['/notes',[ { :controller => 'notes' }]],
- ['/notes/print',[ { :controller => 'notes', :action => 'print' }]],
- ['/notes/print',[ {}, { :controller => 'notes', :action => 'print' }, '/notes/print']],
-
- ['/notes/index/1',[ { :controller => 'notes' }, { :controller => 'notes', :action => 'index', :id => '1' }, '/notes/index/1']],
- ['/notes/index/1',[ { :controller => 'notes' }, { :controller => 'notes', :id => '1', :action => 'index' }, '/notes/index/1']],
- ['/notes/index/1',[ { :action => 'index' }, { :controller => 'notes', :id => '1', :action => 'index' }, '/notes/index/1']],
- ['/notes/index/1',[ {}, { :controller => 'notes', :id => '1', :action => 'index' }, '/notes/index/1']],
- ['/notes/show/1',[ {}, { :controller => 'notes', :action => 'show', :id => '1' }, '/notes/show/1']],
- ['/posts',[ { :controller => 'posts' }, { :controller => 'notes', :action => 'show', :id => '1' }, '/notes/show/1']],
- ['/notes/list',[ { :action => 'list' }, { :controller => 'notes', :action => 'show', :id => '1' }, '/notes/show/1']],
-
- ['/posts/ping',[ { :controller => 'posts', :action => 'ping' }]],
- ['/posts/show/1',[ { :controller => 'posts', :action => 'show', :id => '1' }]],
- ['/posts',[ { :controller => 'posts' }]],
- ['/posts',[ { :controller => 'posts', :action => 'index' }]],
- ['/posts/create',[ { :action => 'create' }, {:day=>nil, :month=>nil, :controller=>"posts", :action=>"show_date"}, '/blog']],
- ['/posts?foo=bar',[ { :controller => 'posts', :foo => 'bar' }]],
- ['/posts?foo%5B%5D=bar&foo%5B%5D=baz', [{ :controller => 'posts', :foo => ['bar', 'baz'] }]],
- ['/posts?page=2', [{ :controller => 'posts', :page => 2 }]],
- ['/posts?q%5Bfoo%5D%5Ba%5D=b', [{ :controller => 'posts', :q => { :foo => { :a => 'b'}} }]],
-
- ['/news.rss', [{ :controller => 'news', :action => 'index', :format => 'rss' }]],
+ ["/admin/users", [ { use_route: "admin_users" }]],
+ ["/admin/users", [ { controller: "admin/users" }]],
+ ["/admin/users", [ { controller: "admin/users", action: "index" }]],
+ ["/admin/users", [ { action: "index" }, { controller: "admin/users", action: "index" }, "/admin/users"]],
+ ["/admin/users", [ { controller: "users", action: "index" }, { controller: "admin/accounts", action: "show", id: "1" }, "/admin/accounts/show/1"]],
+ ["/people", [ { controller: "/people", action: "index" }, { controller: "admin/accounts", action: "foo", id: "bar" }, "/admin/accounts/foo/bar"]],
+
+ ["/admin/posts", [ { controller: "admin/posts" }]],
+ ["/admin/posts/new", [ { controller: "admin/posts", action: "new" }]],
+
+ ["/blog/2009", [ { controller: "posts", action: "show_date", year: 2009 }]],
+ ["/blog/2009/1", [ { controller: "posts", action: "show_date", year: 2009, month: 1 }]],
+ ["/blog/2009/1/1", [ { controller: "posts", action: "show_date", year: 2009, month: 1, day: 1 }]],
+
+ ["/archive/2010", [ { controller: "archive", action: "index", year: "2010" }]],
+ ["/archive", [ { controller: "archive", action: "index" }]],
+ ["/archive?year=january", [ { controller: "archive", action: "index", year: "january" }]],
+
+ ["/people", [ { controller: "people", action: "index" }]],
+ ["/people", [ { action: "index" }, { controller: "people", action: "index" }, "/people"]],
+ ["/people", [ { action: "index" }, { controller: "people", action: "show", id: "1" }, "/people/show/1"]],
+ ["/people", [ { controller: "people", action: "index" }, { controller: "people", action: "show", id: "1" }, "/people/show/1"]],
+ ["/people", [ {}, { controller: "people", action: "index" }, "/people"]],
+ ["/people/1", [ { controller: "people", action: "show" }, { controller: "people", action: "show", id: "1" }, "/people/show/1"]],
+ ["/people/new", [ { use_route: "new_person" }]],
+ ["/people/new", [ { controller: "people", action: "new" }]],
+ ["/people/1", [ { use_route: "person", id: "1" }]],
+ ["/people/1", [ { controller: "people", action: "show", id: "1" }]],
+ ["/people/1.xml", [ { controller: "people", action: "show", id: "1", format: "xml" }]],
+ ["/people/1", [ { controller: "people", action: "show", id: 1 }]],
+ ["/people/1", [ { controller: "people", action: "show", id: Model.new("1") }]],
+ ["/people/1", [ { action: "show", id: "1" }, { controller: "people", action: "index" }, "/people"]],
+ ["/people/1", [ { action: "show", id: 1 }, { controller: "people", action: "show", id: "1" }, "/people/show/1"]],
+ ["/people", [ { controller: "people", action: "index" }, { controller: "people", action: "show", id: "1" }, "/people/show/1"]],
+ ["/people/1", [ {}, { controller: "people", action: "show", id: "1" }, "/people/show/1"]],
+ ["/people/1", [ { controller: "people", action: "show" }, { controller: "people", action: "index", id: "1" }, "/people/index/1"]],
+ ["/people/1/edit", [ { controller: "people", action: "edit", id: "1" }]],
+ ["/people/1/edit.xml", [ { controller: "people", action: "edit", id: "1", format: "xml" }]],
+ ["/people/1/edit", [ { use_route: "edit_person", id: "1" }]],
+ ["/people/1?legacy=true", [ { controller: "people", action: "show", id: "1", legacy: "true" }]],
+ ["/people?legacy=true", [ { controller: "people", action: "index", legacy: "true" }]],
+
+ ["/id_default/2", [ { controller: "foo", action: "id_default", id: "2" }]],
+ ["/id_default", [ { controller: "foo", action: "id_default", id: "1" }]],
+ ["/id_default", [ { controller: "foo", action: "id_default", id: 1 }]],
+ ["/id_default", [ { controller: "foo", action: "id_default" }]],
+ ["/optional/bar", [ { controller: "posts", action: "index", optional: "bar" }]],
+ ["/posts", [ { controller: "posts", action: "index" }]],
+
+ ["/project", [ { controller: "project", action: "index" }]],
+ ["/projects/1", [ { controller: "project", action: "index", project_id: "1" }]],
+ ["/projects/1", [ { controller: "project", action: "index" }, { project_id: "1", controller: "project", action: "index" }, "/projects/1"]],
+ ["/projects/1", [ { use_route: "project", controller: "project", action: "index", project_id: "1" }]],
+ ["/projects/1", [ { use_route: "project", controller: "project", action: "index" }, { controller: "project", action: "index", project_id: "1" }, "/projects/1"]],
+
+ ["/clients", [ { controller: "projects", action: "index" }]],
+ ["/clients?project_id=1", [ { controller: "projects", action: "index", project_id: "1" }]],
+ ["/clients", [ { controller: "projects", action: "index" }, { project_id: "1", controller: "project", action: "index" }, "/projects/1"]],
+
+ ["/comment/20", [ { id: 20 }, { controller: "comments", action: "show" }, "/comments/show"]],
+ ["/comment/20", [ { controller: "comments", id: 20, action: "show" }]],
+ ["/comments/boo", [ { controller: "comments", action: "boo" }]],
+
+ ["/ws/posts/show/1", [ { controller: "posts", action: "show", id: "1", ws: true }]],
+ ["/ws/posts", [ { controller: "posts", action: "index", ws: true }]],
+
+ ["/account", [ { controller: "account", action: "subscription" }]],
+ ["/account/billing", [ { controller: "account", action: "billing" }]],
+
+ ["/pages/1/notes/show/1", [ { page_id: "1", controller: "notes", action: "show", id: "1" }]],
+ ["/pages/1/notes/list", [ { page_id: "1", controller: "notes", action: "list" }]],
+ ["/pages/1/notes", [ { page_id: "1", controller: "notes", action: "index" }]],
+ ["/pages/1/notes", [ { page_id: "1", controller: "notes" }]],
+ ["/notes", [ { page_id: nil, controller: "notes" }]],
+ ["/notes", [ { controller: "notes" }]],
+ ["/notes/print", [ { controller: "notes", action: "print" }]],
+ ["/notes/print", [ {}, { controller: "notes", action: "print" }, "/notes/print"]],
+
+ ["/notes/index/1", [ { controller: "notes" }, { controller: "notes", action: "index", id: "1" }, "/notes/index/1"]],
+ ["/notes/index/1", [ { controller: "notes" }, { controller: "notes", id: "1", action: "index" }, "/notes/index/1"]],
+ ["/notes/index/1", [ { action: "index" }, { controller: "notes", id: "1", action: "index" }, "/notes/index/1"]],
+ ["/notes/index/1", [ {}, { controller: "notes", id: "1", action: "index" }, "/notes/index/1"]],
+ ["/notes/show/1", [ {}, { controller: "notes", action: "show", id: "1" }, "/notes/show/1"]],
+ ["/posts", [ { controller: "posts" }, { controller: "notes", action: "show", id: "1" }, "/notes/show/1"]],
+ ["/notes/list", [ { action: "list" }, { controller: "notes", action: "show", id: "1" }, "/notes/show/1"]],
+
+ ["/posts/ping", [ { controller: "posts", action: "ping" }]],
+ ["/posts/show/1", [ { controller: "posts", action: "show", id: "1" }]],
+ ["/posts/show/1", [ { controller: "posts", action: "show", id: "1", format: "" }]],
+ ["/posts", [ { controller: "posts" }]],
+ ["/posts", [ { controller: "posts", action: "index" }]],
+ ["/posts/create", [ { action: "create" }, { day: nil, month: nil, controller: "posts", action: "show_date" }, "/blog"]],
+ ["/posts?foo=bar", [ { controller: "posts", foo: "bar" }]],
+ ["/posts?foo%5B%5D=bar&foo%5B%5D=baz", [{ controller: "posts", foo: ["bar", "baz"] }]],
+ ["/posts?page=2", [{ controller: "posts", page: 2 }]],
+ ["/posts?q%5Bfoo%5D%5Ba%5D=b", [{ controller: "posts", q: { foo: { a: "b" } } }]],
+
+ ["/news.rss", [{ controller: "news", action: "index", format: "rss" }]],
].each_with_index do |(url, params), i|
if params.length > 1
hash, path_params, route = *params
hash[:only_path] = true
define_method("test_#{url.gsub(/\W/, '_')}_#{i}") do
- get URI('http://test.host' + route.to_s)
+ get URI("http://test.host" + route.to_s)
assert_equal path_params, controller.request.path_parameters
assert_equal url, controller.url_for(hash), params.inspect
end
diff --git a/actionpack/test/controller/url_for_test.rb b/actionpack/test/controller/url_for_test.rb
index 31677f202d..e381abee36 100644
--- a/actionpack/test/controller/url_for_test.rb
+++ b/actionpack/test/controller/url_for_test.rb
@@ -1,10 +1,18 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module AbstractController
module Testing
class UrlForTest < ActionController::TestCase
class W
- include ActionDispatch::Routing::RouteSet.new.tap { |r| r.draw { get ':controller(/:action(/:id(.:format)))' } }.url_helpers
+ include ActionDispatch::Routing::RouteSet.new.tap { |r|
+ r.draw {
+ ActiveSupport::Deprecation.silence {
+ get ":controller(/:action(/:id(.:format)))"
+ }
+ }
+ }.url_helpers
end
def teardown
@@ -15,23 +23,23 @@ module AbstractController
klass = Class.new {
include ActionDispatch::Routing::RouteSet.new.tap { |r|
r.draw {
- get "/foo/(:bar/(:baz))/:zot", :as => 'fun',
- :controller => :articles,
- :action => :index
+ get "/foo/(:bar/(:baz))/:zot", as: "fun",
+ controller: :articles,
+ action: :index
}
}.url_helpers
- self.default_url_options[:host] = 'example.com'
+ default_url_options[:host] = "example.com"
}
- path = klass.new.fun_path({:controller => :articles,
- :baz => "baz",
- :zot => "zot"})
+ path = klass.new.fun_path(controller: :articles,
+ baz: "baz",
+ zot: "zot")
# :bar key isn't provided
- assert_equal '/foo/zot', path
+ assert_equal "/foo/zot", path
end
def add_host!(app = W)
- app.default_url_options[:host] = 'www.basecamphq.com'
+ app.default_url_options[:host] = "www.basecamphq.com"
end
def add_port!
@@ -39,166 +47,166 @@ module AbstractController
end
def add_numeric_host!
- W.default_url_options[:host] = '127.0.0.1'
+ W.default_url_options[:host] = "127.0.0.1"
end
def test_exception_is_thrown_without_host
assert_raise ArgumentError do
- W.new.url_for :controller => 'c', :action => 'a', :id => 'i'
+ W.new.url_for controller: "c", action: "a", id: "i"
end
end
def test_anchor
- assert_equal('/c/a#anchor',
- W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :anchor => 'anchor')
+ assert_equal("/c/a#anchor",
+ W.new.url_for(only_path: true, controller: "c", action: "a", anchor: "anchor")
)
end
def test_nil_anchor
assert_equal(
- '/c/a',
- W.new.url_for(only_path: true, controller: 'c', action: 'a', anchor: nil)
+ "/c/a",
+ W.new.url_for(only_path: true, controller: "c", action: "a", anchor: nil)
)
end
def test_false_anchor
assert_equal(
- '/c/a',
- W.new.url_for(only_path: true, controller: 'c', action: 'a', anchor: false)
+ "/c/a",
+ W.new.url_for(only_path: true, controller: "c", action: "a", anchor: false)
)
end
def test_anchor_should_call_to_param
- assert_equal('/c/a#anchor',
- W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :anchor => Struct.new(:to_param).new('anchor'))
+ assert_equal("/c/a#anchor",
+ W.new.url_for(only_path: true, controller: "c", action: "a", anchor: Struct.new(:to_param).new("anchor"))
)
end
def test_anchor_should_escape_unsafe_pchar
- assert_equal('/c/a#%23anchor',
- W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :anchor => Struct.new(:to_param).new('#anchor'))
+ assert_equal("/c/a#%23anchor",
+ W.new.url_for(only_path: true, controller: "c", action: "a", anchor: Struct.new(:to_param).new("#anchor"))
)
end
def test_anchor_should_not_escape_safe_pchar
- assert_equal('/c/a#name=user&email=user@domain.com',
- W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :anchor => Struct.new(:to_param).new('name=user&email=user@domain.com'))
+ assert_equal("/c/a#name=user&email=user@domain.com",
+ W.new.url_for(only_path: true, controller: "c", action: "a", anchor: Struct.new(:to_param).new("name=user&email=user@domain.com"))
)
end
def test_default_host
add_host!
- assert_equal('http://www.basecamphq.com/c/a/i',
- W.new.url_for(:controller => 'c', :action => 'a', :id => 'i')
+ assert_equal("http://www.basecamphq.com/c/a/i",
+ W.new.url_for(controller: "c", action: "a", id: "i")
)
end
def test_host_may_be_overridden
add_host!
- assert_equal('http://37signals.basecamphq.com/c/a/i',
- W.new.url_for(:host => '37signals.basecamphq.com', :controller => 'c', :action => 'a', :id => 'i')
+ assert_equal("http://37signals.basecamphq.com/c/a/i",
+ W.new.url_for(host: "37signals.basecamphq.com", controller: "c", action: "a", id: "i")
)
end
def test_subdomain_may_be_changed
add_host!
- assert_equal('http://api.basecamphq.com/c/a/i',
- W.new.url_for(:subdomain => 'api', :controller => 'c', :action => 'a', :id => 'i')
+ assert_equal("http://api.basecamphq.com/c/a/i",
+ W.new.url_for(subdomain: "api", controller: "c", action: "a", id: "i")
)
end
def test_subdomain_may_be_object
- model = Class.new { def self.to_param; 'api'; end }
+ model = Class.new { def self.to_param; "api"; end }
add_host!
- assert_equal('http://api.basecamphq.com/c/a/i',
- W.new.url_for(:subdomain => model, :controller => 'c', :action => 'a', :id => 'i')
+ assert_equal("http://api.basecamphq.com/c/a/i",
+ W.new.url_for(subdomain: model, controller: "c", action: "a", id: "i")
)
end
def test_subdomain_may_be_removed
add_host!
- assert_equal('http://basecamphq.com/c/a/i',
- W.new.url_for(:subdomain => false, :controller => 'c', :action => 'a', :id => 'i')
+ assert_equal("http://basecamphq.com/c/a/i",
+ W.new.url_for(subdomain: false, controller: "c", action: "a", id: "i")
)
end
def test_subdomain_may_be_removed_with_blank_string
- W.default_url_options[:host] = 'api.basecamphq.com'
- assert_equal('http://basecamphq.com/c/a/i',
- W.new.url_for(:subdomain => '', :controller => 'c', :action => 'a', :id => 'i')
+ W.default_url_options[:host] = "api.basecamphq.com"
+ assert_equal("http://basecamphq.com/c/a/i",
+ W.new.url_for(subdomain: "", controller: "c", action: "a", id: "i")
)
end
def test_multiple_subdomains_may_be_removed
- W.default_url_options[:host] = 'mobile.www.api.basecamphq.com'
- assert_equal('http://basecamphq.com/c/a/i',
- W.new.url_for(:subdomain => false, :controller => 'c', :action => 'a', :id => 'i')
+ W.default_url_options[:host] = "mobile.www.api.basecamphq.com"
+ assert_equal("http://basecamphq.com/c/a/i",
+ W.new.url_for(subdomain: false, controller: "c", action: "a", id: "i")
)
end
def test_subdomain_may_be_accepted_with_numeric_host
add_numeric_host!
- assert_equal('http://127.0.0.1/c/a/i',
- W.new.url_for(:subdomain => 'api', :controller => 'c', :action => 'a', :id => 'i')
+ assert_equal("http://127.0.0.1/c/a/i",
+ W.new.url_for(subdomain: "api", controller: "c", action: "a", id: "i")
)
end
def test_domain_may_be_changed
add_host!
- assert_equal('http://www.37signals.com/c/a/i',
- W.new.url_for(:domain => '37signals.com', :controller => 'c', :action => 'a', :id => 'i')
+ assert_equal("http://www.37signals.com/c/a/i",
+ W.new.url_for(domain: "37signals.com", controller: "c", action: "a", id: "i")
)
end
def test_tld_length_may_be_changed
add_host!
- assert_equal('http://mobile.www.basecamphq.com/c/a/i',
- W.new.url_for(:subdomain => 'mobile', :tld_length => 2, :controller => 'c', :action => 'a', :id => 'i')
+ assert_equal("http://mobile.www.basecamphq.com/c/a/i",
+ W.new.url_for(subdomain: "mobile", tld_length: 2, controller: "c", action: "a", id: "i")
)
end
def test_port
add_host!
- assert_equal('http://www.basecamphq.com:3000/c/a/i',
- W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :port => 3000)
+ assert_equal("http://www.basecamphq.com:3000/c/a/i",
+ W.new.url_for(controller: "c", action: "a", id: "i", port: 3000)
)
end
def test_default_port
add_host!
add_port!
- assert_equal('http://www.basecamphq.com:3000/c/a/i',
- W.new.url_for(:controller => 'c', :action => 'a', :id => 'i')
+ assert_equal("http://www.basecamphq.com:3000/c/a/i",
+ W.new.url_for(controller: "c", action: "a", id: "i")
)
end
def test_protocol
add_host!
- assert_equal('https://www.basecamphq.com/c/a/i',
- W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => 'https')
+ assert_equal("https://www.basecamphq.com/c/a/i",
+ W.new.url_for(controller: "c", action: "a", id: "i", protocol: "https")
)
end
def test_protocol_with_and_without_separators
add_host!
- assert_equal('https://www.basecamphq.com/c/a/i',
- W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => 'https')
+ assert_equal("https://www.basecamphq.com/c/a/i",
+ W.new.url_for(controller: "c", action: "a", id: "i", protocol: "https")
)
- assert_equal('https://www.basecamphq.com/c/a/i',
- W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => 'https:')
+ assert_equal("https://www.basecamphq.com/c/a/i",
+ W.new.url_for(controller: "c", action: "a", id: "i", protocol: "https:")
)
- assert_equal('https://www.basecamphq.com/c/a/i',
- W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => 'https://')
+ assert_equal("https://www.basecamphq.com/c/a/i",
+ W.new.url_for(controller: "c", action: "a", id: "i", protocol: "https://")
)
end
def test_without_protocol
add_host!
- assert_equal('//www.basecamphq.com/c/a/i',
- W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => '//')
+ assert_equal("//www.basecamphq.com/c/a/i",
+ W.new.url_for(controller: "c", action: "a", id: "i", protocol: "//")
)
- assert_equal('//www.basecamphq.com/c/a/i',
- W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => false)
+ assert_equal("//www.basecamphq.com/c/a/i",
+ W.new.url_for(controller: "c", action: "a", id: "i", protocol: false)
)
end
@@ -206,192 +214,202 @@ module AbstractController
add_host!
add_port!
- assert_equal('//www.basecamphq.com:3000/c/a/i',
- W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => '//')
+ assert_equal("//www.basecamphq.com:3000/c/a/i",
+ W.new.url_for(controller: "c", action: "a", id: "i", protocol: "//")
)
- assert_equal('//www.basecamphq.com:3000/c/a/i',
- W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => false)
+ assert_equal("//www.basecamphq.com:3000/c/a/i",
+ W.new.url_for(controller: "c", action: "a", id: "i", protocol: false)
)
end
def test_trailing_slash
add_host!
- options = {:controller => 'foo', :trailing_slash => true, :action => 'bar', :id => '33'}
- assert_equal('http://www.basecamphq.com/foo/bar/33/', W.new.url_for(options) )
+ options = { controller: "foo", trailing_slash: true, action: "bar", id: "33" }
+ assert_equal("http://www.basecamphq.com/foo/bar/33/", W.new.url_for(options))
end
def test_trailing_slash_with_protocol
add_host!
- options = { :trailing_slash => true,:protocol => 'https', :controller => 'foo', :action => 'bar', :id => '33'}
- assert_equal('https://www.basecamphq.com/foo/bar/33/', W.new.url_for(options) )
- assert_equal 'https://www.basecamphq.com/foo/bar/33/?query=string', W.new.url_for(options.merge({:query => 'string'}))
+ options = { trailing_slash: true, protocol: "https", controller: "foo", action: "bar", id: "33" }
+ assert_equal("https://www.basecamphq.com/foo/bar/33/", W.new.url_for(options))
+ assert_equal "https://www.basecamphq.com/foo/bar/33/?query=string", W.new.url_for(options.merge(query: "string"))
end
def test_trailing_slash_with_only_path
- options = {:controller => 'foo', :trailing_slash => true}
- assert_equal '/foo/', W.new.url_for(options.merge({:only_path => true}))
- options.update({:action => 'bar', :id => '33'})
- assert_equal '/foo/bar/33/', W.new.url_for(options.merge({:only_path => true}))
- assert_equal '/foo/bar/33/?query=string', W.new.url_for(options.merge({:query => 'string',:only_path => true}))
+ options = { controller: "foo", trailing_slash: true }
+ assert_equal "/foo/", W.new.url_for(options.merge(only_path: true))
+ options.update(action: "bar", id: "33")
+ assert_equal "/foo/bar/33/", W.new.url_for(options.merge(only_path: true))
+ assert_equal "/foo/bar/33/?query=string", W.new.url_for(options.merge(query: "string", only_path: true))
end
def test_trailing_slash_with_anchor
- options = {:trailing_slash => true, :controller => 'foo', :action => 'bar', :id => '33', :only_path => true, :anchor=> 'chapter7'}
- assert_equal '/foo/bar/33/#chapter7', W.new.url_for(options)
- assert_equal '/foo/bar/33/?query=string#chapter7', W.new.url_for(options.merge({:query => 'string'}))
+ options = { trailing_slash: true, controller: "foo", action: "bar", id: "33", only_path: true, anchor: "chapter7" }
+ assert_equal "/foo/bar/33/#chapter7", W.new.url_for(options)
+ assert_equal "/foo/bar/33/?query=string#chapter7", W.new.url_for(options.merge(query: "string"))
end
def test_trailing_slash_with_params
- url = W.new.url_for(:trailing_slash => true, :only_path => true, :controller => 'cont', :action => 'act', :p1 => 'cafe', :p2 => 'link')
+ url = W.new.url_for(trailing_slash: true, only_path: true, controller: "cont", action: "act", p1: "cafe", p2: "link")
params = extract_params(url)
- assert_equal({p1: 'cafe'}.to_query, params[0])
- assert_equal({p2: 'link'}.to_query, params[1])
+ assert_equal({ p1: "cafe" }.to_query, params[0])
+ assert_equal({ p2: "link" }.to_query, params[1])
end
def test_relative_url_root_is_respected
add_host!
- assert_equal('https://www.basecamphq.com/subdir/c/a/i',
- W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => 'https', :script_name => '/subdir')
+ assert_equal("https://www.basecamphq.com/subdir/c/a/i",
+ W.new.url_for(controller: "c", action: "a", id: "i", protocol: "https", script_name: "/subdir")
)
end
def test_relative_url_root_is_respected_with_environment_variable
# `config.relative_url_root` is set by ENV['RAILS_RELATIVE_URL_ROOT']
w = Class.new {
- config = ActionDispatch::Routing::RouteSet::Config.new '/subdir'
+ config = ActionDispatch::Routing::RouteSet::Config.new "/subdir"
r = ActionDispatch::Routing::RouteSet.new(config)
- r.draw { get ':controller(/:action(/:id(.:format)))' }
+ r.draw { ActiveSupport::Deprecation.silence { get ":controller(/:action(/:id(.:format)))" } }
include r.url_helpers
}
add_host!(w)
- assert_equal('https://www.basecamphq.com/subdir/c/a/i',
- w.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => 'https')
+ assert_equal("https://www.basecamphq.com/subdir/c/a/i",
+ w.new.url_for(controller: "c", action: "a", id: "i", protocol: "https")
)
end
def test_named_routes
with_routing do |set|
set.draw do
- get 'this/is/verbose', :to => 'home#index', :as => :no_args
- get 'home/sweet/home/:user', :to => 'home#index', :as => :home
+ get "this/is/verbose", to: "home#index", as: :no_args
+ get "home/sweet/home/:user", to: "home#index", as: :home
end
# We need to create a new class in order to install the new named route.
kls = Class.new { include set.url_helpers }
controller = kls.new
- assert controller.respond_to?(:home_url)
- assert_equal 'http://www.basecamphq.com/home/sweet/home/again',
- controller.send(:home_url, :host => 'www.basecamphq.com', :user => 'again')
+ assert_respond_to controller, :home_url
+ assert_equal "http://www.basecamphq.com/home/sweet/home/again",
+ controller.send(:home_url, host: "www.basecamphq.com", user: "again")
- assert_equal("/home/sweet/home/alabama", controller.send(:home_path, :user => 'alabama', :host => 'unused'))
- assert_equal("http://www.basecamphq.com/home/sweet/home/alabama", controller.send(:home_url, :user => 'alabama', :host => 'www.basecamphq.com'))
- assert_equal("http://www.basecamphq.com/this/is/verbose", controller.send(:no_args_url, :host=>'www.basecamphq.com'))
+ assert_equal("/home/sweet/home/alabama", controller.send(:home_path, user: "alabama", host: "unused"))
+ assert_equal("http://www.basecamphq.com/home/sweet/home/alabama", controller.send(:home_url, user: "alabama", host: "www.basecamphq.com"))
+ assert_equal("http://www.basecamphq.com/this/is/verbose", controller.send(:no_args_url, host: "www.basecamphq.com"))
end
end
def test_relative_url_root_is_respected_for_named_routes
with_routing do |set|
set.draw do
- get '/home/sweet/home/:user', :to => 'home#index', :as => :home
+ get "/home/sweet/home/:user", to: "home#index", as: :home
end
kls = Class.new { include set.url_helpers }
controller = kls.new
- assert_equal 'http://www.basecamphq.com/subdir/home/sweet/home/again',
- controller.send(:home_url, :host => 'www.basecamphq.com', :user => 'again', :script_name => "/subdir")
+ assert_equal "http://www.basecamphq.com/subdir/home/sweet/home/again",
+ controller.send(:home_url, host: "www.basecamphq.com", user: "again", script_name: "/subdir")
end
end
def test_using_nil_script_name_properly_concats_with_original_script_name
add_host!
- assert_equal('https://www.basecamphq.com/subdir/c/a/i',
- W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => 'https', :script_name => nil, :original_script_name => '/subdir')
+ assert_equal("https://www.basecamphq.com/subdir/c/a/i",
+ W.new.url_for(controller: "c", action: "a", id: "i", protocol: "https", script_name: nil, original_script_name: "/subdir")
)
end
def test_only_path
with_routing do |set|
set.draw do
- get 'home/sweet/home/:user', :to => 'home#index', :as => :home
- get ':controller/:action/:id'
+ get "home/sweet/home/:user", to: "home#index", as: :home
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:id"
+ end
end
# We need to create a new class in order to install the new named route.
kls = Class.new { include set.url_helpers }
controller = kls.new
assert_respond_to controller, :home_url
- assert_equal '/brave/new/world',
- controller.url_for(:controller => 'brave', :action => 'new', :id => 'world', :only_path => true)
+ assert_equal "/brave/new/world",
+ controller.url_for(controller: "brave", action: "new", id: "world", only_path: true)
- assert_equal("/home/sweet/home/alabama", controller.home_path(:user => 'alabama', :host => 'unused'))
- assert_equal("/home/sweet/home/alabama", controller.home_path('alabama'))
+ assert_equal("/home/sweet/home/alabama", controller.home_path(user: "alabama", host: "unused"))
+ assert_equal("/home/sweet/home/alabama", controller.home_path("alabama"))
end
end
def test_one_parameter
- assert_equal('/c/a?param=val',
- W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :param => 'val')
+ assert_equal("/c/a?param=val",
+ W.new.url_for(only_path: true, controller: "c", action: "a", param: "val")
)
end
def test_two_parameters
- url = W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :p1 => 'X1', :p2 => 'Y2')
+ url = W.new.url_for(only_path: true, controller: "c", action: "a", p1: "X1", p2: "Y2")
params = extract_params(url)
- assert_equal({p1: 'X1'}.to_query, params[0])
- assert_equal({p2: 'Y2'}.to_query, params[1])
+ assert_equal({ p1: "X1" }.to_query, params[0])
+ assert_equal({ p2: "Y2" }.to_query, params[1])
end
def test_hash_parameter
- url = W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :query => {:name => 'Bob', :category => 'prof'})
+ url = W.new.url_for(only_path: true, controller: "c", action: "a", query: { name: "Bob", category: "prof" })
params = extract_params(url)
- assert_equal({'query[category]' => 'prof'}.to_query, params[0])
- assert_equal({'query[name]' => 'Bob'}.to_query, params[1])
+ assert_equal({ "query[category]" => "prof" }.to_query, params[0])
+ assert_equal({ "query[name]" => "Bob" }.to_query, params[1])
end
def test_array_parameter
- url = W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :query => ['Bob', 'prof'])
+ url = W.new.url_for(only_path: true, controller: "c", action: "a", query: ["Bob", "prof"])
params = extract_params(url)
- assert_equal({'query[]' => 'Bob'}.to_query, params[0])
- assert_equal({'query[]' => 'prof'}.to_query, params[1])
+ assert_equal({ "query[]" => "Bob" }.to_query, params[0])
+ assert_equal({ "query[]" => "prof" }.to_query, params[1])
end
def test_hash_recursive_parameters
- url = W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :query => {:person => {:name => 'Bob', :position => 'prof'}, :hobby => 'piercing'})
+ url = W.new.url_for(only_path: true, controller: "c", action: "a", query: { person: { name: "Bob", position: "prof" }, hobby: "piercing" })
params = extract_params(url)
- assert_equal({'query[hobby]' => 'piercing'}.to_query, params[0])
- assert_equal({'query[person][name]' => 'Bob' }.to_query, params[1])
- assert_equal({'query[person][position]' => 'prof' }.to_query, params[2])
+ assert_equal({ "query[hobby]" => "piercing" }.to_query, params[0])
+ assert_equal({ "query[person][name]" => "Bob" }.to_query, params[1])
+ assert_equal({ "query[person][position]" => "prof" }.to_query, params[2])
end
def test_hash_recursive_and_array_parameters
- url = W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :id => 101, :query => {:person => {:name => 'Bob', :position => ['prof', 'art director']}, :hobby => 'piercing'})
+ url = W.new.url_for(only_path: true, controller: "c", action: "a", id: 101, query: { person: { name: "Bob", position: ["prof", "art director"] }, hobby: "piercing" })
assert_match(%r(^/c/a/101), url)
params = extract_params(url)
- assert_equal({'query[hobby]' => 'piercing' }.to_query, params[0])
- assert_equal({'query[person][name]' => 'Bob' }.to_query, params[1])
- assert_equal({'query[person][position][]' => 'art director'}.to_query, params[2])
- assert_equal({'query[person][position][]' => 'prof' }.to_query, params[3])
+ assert_equal({ "query[hobby]" => "piercing" }.to_query, params[0])
+ assert_equal({ "query[person][name]" => "Bob" }.to_query, params[1])
+ assert_equal({ "query[person][position][]" => "art director" }.to_query, params[2])
+ assert_equal({ "query[person][position][]" => "prof" }.to_query, params[3])
+ end
+
+ def test_url_action_controller_parameters
+ add_host!
+ assert_raise(ActionController::UnfilteredParameters) do
+ W.new.url_for(ActionController::Parameters.new(controller: "c", action: "a", protocol: "javascript", f: "%0Aeval(name)"))
+ end
end
def test_path_generation_for_symbol_parameter_keys
- assert_generates("/image", :controller=> :image)
+ assert_generates("/image", controller: :image)
end
def test_named_routes_with_nil_keys
with_routing do |set|
set.draw do
- get 'posts.:format', :to => 'posts#index', :as => :posts
- get '/', :to => 'posts#index', :as => :main
+ get "posts.:format", to: "posts#index", as: :posts
+ get "/", to: "posts#index", as: :main
end
# We need to create a new class in order to install the new named route.
kls = Class.new { include set.url_helpers }
- kls.default_url_options[:host] = 'www.basecamphq.com'
+ kls.default_url_options[:host] = "www.basecamphq.com"
controller = kls.new
- params = {:action => :index, :controller => :posts, :format => :xml}
+ params = { action: :index, controller: :posts, format: :xml }
assert_equal("http://www.basecamphq.com/posts.xml", controller.send(:url_for, params))
params[:format] = nil
assert_equal("http://www.basecamphq.com/", controller.send(:url_for, params))
@@ -402,35 +420,35 @@ module AbstractController
first_class = Class.new { include ActionController::UrlFor }
second_class = Class.new { include ActionController::UrlFor }
- first_host, second_host = 'firsthost.com', 'secondhost.com'
+ first_host, second_host = "firsthost.com", "secondhost.com"
first_class.default_url_options[:host] = first_host
second_class.default_url_options[:host] = second_host
- assert_equal first_host, first_class.default_url_options[:host]
+ assert_equal first_host, first_class.default_url_options[:host]
assert_equal second_host, second_class.default_url_options[:host]
end
def test_with_stringified_keys
- assert_equal("/c", W.new.url_for('controller' => 'c', 'only_path' => true))
- assert_equal("/c/a", W.new.url_for('controller' => 'c', 'action' => 'a', 'only_path' => true))
+ assert_equal("/c", W.new.url_for("controller" => "c", "only_path" => true))
+ assert_equal("/c/a", W.new.url_for("controller" => "c", "action" => "a", "only_path" => true))
end
def test_with_hash_with_indifferent_access
- W.default_url_options[:controller] = 'd'
+ W.default_url_options[:controller] = "d"
W.default_url_options[:only_path] = false
- assert_equal("/c", W.new.url_for(ActiveSupport::HashWithIndifferentAccess.new('controller' => 'c', 'only_path' => true)))
+ assert_equal("/c", W.new.url_for(ActiveSupport::HashWithIndifferentAccess.new("controller" => "c", "only_path" => true)))
- W.default_url_options[:action] = 'b'
- assert_equal("/c/a", W.new.url_for(ActiveSupport::HashWithIndifferentAccess.new('controller' => 'c', 'action' => 'a', 'only_path' => true)))
+ W.default_url_options[:action] = "b"
+ assert_equal("/c/a", W.new.url_for(ActiveSupport::HashWithIndifferentAccess.new("controller" => "c", "action" => "a", "only_path" => true)))
end
def test_url_params_with_nil_to_param_are_not_in_url
- assert_equal("/c/a", W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :id => Struct.new(:to_param).new(nil)))
+ assert_equal("/c/a", W.new.url_for(only_path: true, controller: "c", action: "a", id: Struct.new(:to_param).new(nil)))
end
def test_false_url_params_are_included_in_query
- assert_equal("/c/a?show=false", W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :show => false))
+ assert_equal("/c/a?show=false", W.new.url_for(only_path: true, controller: "c", action: "a", show: false))
end
def test_url_generation_with_array_and_hash
@@ -442,18 +460,59 @@ module AbstractController
end
kls = Class.new { include set.url_helpers }
- kls.default_url_options[:host] = 'www.basecamphq.com'
+ kls.default_url_options[:host] = "www.basecamphq.com"
controller = kls.new
assert_equal("http://www.basecamphq.com/admin/posts/new?param=value",
- controller.send(:url_for, [:new, :admin, :post, { param: 'value' }])
+ controller.send(:url_for, [:new, :admin, :post, { param: "value" }])
)
end
end
+ def test_url_for_with_array_is_unmodified
+ with_routing do |set|
+ set.draw do
+ namespace :admin do
+ resources :posts
+ end
+ end
+
+ kls = Class.new { include set.url_helpers }
+ kls.default_url_options[:host] = "www.basecamphq.com"
+
+ original_components = [:new, :admin, :post, { param: "value" }]
+ components = original_components.dup
+
+ kls.new.url_for(components)
+
+ assert_equal(original_components, components)
+ 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
+ url.split("?", 2).last.split("&").sort
end
end
end
diff --git a/actionpack/test/controller/url_rewriter_test.rb b/actionpack/test/controller/url_rewriter_test.rb
index 5f2abc9606..ca83b850d5 100644
--- a/actionpack/test/controller/url_rewriter_test.rb
+++ b/actionpack/test/controller/url_rewriter_test.rb
@@ -1,12 +1,14 @@
-require 'abstract_unit'
-require 'controller/fake_controllers'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "controller/fake_controllers"
class UrlRewriterTests < ActionController::TestCase
class Rewriter
def initialize(request)
@options = {
- :host => request.host_with_port,
- :protocol => request.protocol
+ host: request.host_with_port,
+ protocol: request.protocol
}
end
@@ -17,73 +19,74 @@ class UrlRewriterTests < ActionController::TestCase
def setup
@params = {}
- @rewriter = Rewriter.new(@request) #.new(@request, @params)
+ @rewriter = Rewriter.new(@request)
@routes = ActionDispatch::Routing::RouteSet.new.tap do |r|
r.draw do
- get ':controller(/:action(/:id))'
+ ActiveSupport::Deprecation.silence do
+ get ":controller(/:action(/:id))"
+ end
end
end
end
def test_port
- assert_equal('http://test.host:1271/c/a/i',
- @rewriter.rewrite(@routes, :controller => 'c', :action => 'a', :id => 'i', :port => 1271)
+ assert_equal("http://test.host:1271/c/a/i",
+ @rewriter.rewrite(@routes, controller: "c", action: "a", id: "i", port: 1271)
)
end
def test_protocol_with_and_without_separator
- assert_equal('https://test.host/c/a/i',
- @rewriter.rewrite(@routes, :protocol => 'https', :controller => 'c', :action => 'a', :id => 'i')
+ assert_equal("https://test.host/c/a/i",
+ @rewriter.rewrite(@routes, protocol: "https", controller: "c", action: "a", id: "i")
)
- assert_equal('https://test.host/c/a/i',
- @rewriter.rewrite(@routes, :protocol => 'https://', :controller => 'c', :action => 'a', :id => 'i')
+ assert_equal("https://test.host/c/a/i",
+ @rewriter.rewrite(@routes, protocol: "https://", controller: "c", action: "a", id: "i")
)
end
def test_user_name_and_password
assert_equal(
- 'http://david:secret@test.host/c/a/i',
- @rewriter.rewrite(@routes, :user => "david", :password => "secret", :controller => 'c', :action => 'a', :id => 'i')
+ "http://david:secret@test.host/c/a/i",
+ @rewriter.rewrite(@routes, user: "david", password: "secret", controller: "c", action: "a", id: "i")
)
end
def test_user_name_and_password_with_escape_codes
assert_equal(
- 'http://openid.aol.com%2Fnextangler:one+two%3F@test.host/c/a/i',
- @rewriter.rewrite(@routes, :user => "openid.aol.com/nextangler", :password => "one two?", :controller => 'c', :action => 'a', :id => 'i')
+ "http://openid.aol.com%2Fnextangler:one+two%3F@test.host/c/a/i",
+ @rewriter.rewrite(@routes, user: "openid.aol.com/nextangler", password: "one two?", controller: "c", action: "a", id: "i")
)
end
def test_anchor
assert_equal(
- 'http://test.host/c/a/i#anchor',
- @rewriter.rewrite(@routes, :controller => 'c', :action => 'a', :id => 'i', :anchor => 'anchor')
+ "http://test.host/c/a/i#anchor",
+ @rewriter.rewrite(@routes, controller: "c", action: "a", id: "i", anchor: "anchor")
)
end
def test_anchor_should_call_to_param
assert_equal(
- 'http://test.host/c/a/i#anchor',
- @rewriter.rewrite(@routes, :controller => 'c', :action => 'a', :id => 'i', :anchor => Struct.new(:to_param).new('anchor'))
+ "http://test.host/c/a/i#anchor",
+ @rewriter.rewrite(@routes, controller: "c", action: "a", id: "i", anchor: Struct.new(:to_param).new("anchor"))
)
end
def test_anchor_should_be_uri_escaped
assert_equal(
- 'http://test.host/c/a/i#anc/hor',
- @rewriter.rewrite(@routes, :controller => 'c', :action => 'a', :id => 'i', :anchor => Struct.new(:to_param).new('anc/hor'))
+ "http://test.host/c/a/i#anc/hor",
+ @rewriter.rewrite(@routes, controller: "c", action: "a", id: "i", anchor: Struct.new(:to_param).new("anc/hor"))
)
end
def test_trailing_slash
- options = {:controller => 'foo', :action => 'bar', :id => '3', :only_path => true}
- assert_equal '/foo/bar/3', @rewriter.rewrite(@routes, options)
- assert_equal '/foo/bar/3?query=string', @rewriter.rewrite(@routes, options.merge({:query => 'string'}))
- options.update({:trailing_slash => true})
- assert_equal '/foo/bar/3/', @rewriter.rewrite(@routes, options)
- options.update({:query => 'string'})
- assert_equal '/foo/bar/3/?query=string', @rewriter.rewrite(@routes, options)
+ options = { controller: "foo", action: "bar", id: "3", only_path: true }
+ assert_equal "/foo/bar/3", @rewriter.rewrite(@routes, options)
+ assert_equal "/foo/bar/3?query=string", @rewriter.rewrite(@routes, options.merge(query: "string"))
+ options.update(trailing_slash: true)
+ assert_equal "/foo/bar/3/", @rewriter.rewrite(@routes, options)
+ options.update(query: "string")
+ assert_equal "/foo/bar/3/?query=string", @rewriter.rewrite(@routes, options)
end
end
-
diff --git a/actionpack/test/controller/webservice_test.rb b/actionpack/test/controller/webservice_test.rb
index 21fa670bb6..4a10637b54 100644
--- a/actionpack/test/controller/webservice_test.rb
+++ b/actionpack/test/controller/webservice_test.rb
@@ -1,22 +1,30 @@
-require 'abstract_unit'
-require 'active_support/json/decoding'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/json/decoding"
class WebServiceTest < ActionDispatch::IntegrationTest
class TestController < ActionController::Base
def assign_parameters
if params[:full]
- render :text => dump_params_keys
+ render plain: dump_params_keys
else
- render :text => (params.keys - ['controller', 'action']).sort.join(", ")
+ render plain: (params.keys - ["controller", "action"]).sort.join(", ")
end
end
def dump_params_keys(hash = params)
hash.keys.sort.inject("") do |s, k|
value = hash[k]
- value = Hash === value ? "(#{dump_params_keys(value)})" : ""
- s << ", " unless s.empty?
- s << "#{k}#{value}"
+
+ if value.is_a?(Hash) || value.is_a?(ActionController::Parameters)
+ value = "(#{dump_params_keys(value)})"
+ else
+ value = ""
+ end
+
+ s += ", " unless s.empty?
+ s += "#{k}#{value}"
end
end
end
@@ -29,7 +37,7 @@ class WebServiceTest < ActionDispatch::IntegrationTest
def test_check_parameters
with_test_route_set do
get "/"
- assert_equal '', @controller.response.body
+ assert_equal "", @controller.response.body
end
end
@@ -37,11 +45,11 @@ class WebServiceTest < ActionDispatch::IntegrationTest
with_test_route_set do
post "/",
params: '{"entry":{"summary":"content..."}}',
- headers: { 'CONTENT_TYPE' => 'application/json' }
+ headers: { "CONTENT_TYPE" => "application/json" }
- assert_equal 'entry', @controller.response.body
+ assert_equal "entry", @controller.response.body
assert @controller.params.has_key?(:entry)
- assert_equal 'content...', @controller.params["entry"]['summary']
+ assert_equal "content...", @controller.params["entry"]["summary"]
end
end
@@ -49,34 +57,34 @@ class WebServiceTest < ActionDispatch::IntegrationTest
with_test_route_set do
put "/",
params: '{"entry":{"summary":"content..."}}',
- headers: { 'CONTENT_TYPE' => 'application/json' }
+ headers: { "CONTENT_TYPE" => "application/json" }
- assert_equal 'entry', @controller.response.body
+ assert_equal "entry", @controller.response.body
assert @controller.params.has_key?(:entry)
- assert_equal 'content...', @controller.params["entry"]['summary']
+ assert_equal "content...", @controller.params["entry"]["summary"]
end
end
def test_register_and_use_json_simple
with_test_route_set do
- with_params_parsers Mime::JSON => Proc.new { |data| ActiveSupport::JSON.decode(data)['request'].with_indifferent_access } do
+ with_params_parsers Mime[:json] => Proc.new { |data| ActiveSupport::JSON.decode(data)["request"].with_indifferent_access } do
post "/",
params: '{"request":{"summary":"content...","title":"JSON"}}',
- headers: { 'CONTENT_TYPE' => 'application/json' }
+ headers: { "CONTENT_TYPE" => "application/json" }
- assert_equal 'summary, title', @controller.response.body
+ assert_equal "summary, title", @controller.response.body
assert @controller.params.has_key?(:summary)
assert @controller.params.has_key?(:title)
- assert_equal 'content...', @controller.params["summary"]
- assert_equal 'JSON', @controller.params["title"]
+ assert_equal "content...", @controller.params["summary"]
+ assert_equal "JSON", @controller.params["title"]
end
end
end
def test_use_json_with_empty_request
with_test_route_set do
- assert_nothing_raised { post "/", headers: { 'CONTENT_TYPE' => 'application/json' } }
- assert_equal '', @controller.response.body
+ assert_nothing_raised { post "/", headers: { "CONTENT_TYPE" => "application/json" } }
+ assert_equal "", @controller.response.body
end
end
@@ -84,38 +92,42 @@ class WebServiceTest < ActionDispatch::IntegrationTest
with_test_route_set do
post "/?full=1",
params: '{"first-key":{"sub-key":"..."}}',
- headers: { 'CONTENT_TYPE' => 'application/json' }
- assert_equal 'action, controller, first-key(sub-key), full', @controller.response.body
- assert_equal "...", @controller.params['first-key']['sub-key']
+ headers: { "CONTENT_TYPE" => "application/json" }
+ assert_equal "action, controller, first-key(sub-key), full", @controller.response.body
+ assert_equal "...", @controller.params["first-key"]["sub-key"]
end
end
def test_parsing_json_doesnot_rescue_exception
- with_test_route_set do
- with_params_parsers Mime::JSON => Proc.new { |data| raise Interrupt } do
- assert_raises(Interrupt) do
- post "/",
- params: '{"title":"JSON"}}',
- headers: { 'CONTENT_TYPE' => 'application/json' }
- end
+ req = Class.new(ActionDispatch::Request) do
+ def params_parsers
+ { json: Proc.new { |data| raise Interrupt } }
end
+
+ def content_length; get_header("rack.input").length; end
+ end.new("rack.input" => StringIO.new('{"title":"JSON"}}'), "CONTENT_TYPE" => "application/json")
+
+ assert_raises(Interrupt) do
+ req.request_parameters
end
end
private
def with_params_parsers(parsers = {})
old_session = @integration_session
- @app = ActionDispatch::ParamsParser.new(app.routes, parsers)
+ original_parsers = ActionDispatch::Request.parameter_parsers
+ ActionDispatch::Request.parameter_parsers = original_parsers.merge parsers
reset!
yield
ensure
+ ActionDispatch::Request.parameter_parsers = original_parsers
@integration_session = old_session
end
def with_test_route_set
with_routing do |set|
set.draw do
- match '/', :to => 'web_service_test/test#assign_parameters', :via => :all
+ match "/", to: "web_service_test/test#assign_parameters", via: :all
end
yield
end
diff --git a/actionpack/test/dispatch/callbacks_test.rb b/actionpack/test/dispatch/callbacks_test.rb
index f767b07e75..fc80191c02 100644
--- a/actionpack/test/dispatch/callbacks_test.rb
+++ b/actionpack/test/dispatch/callbacks_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class DispatcherTest < ActiveSupport::TestCase
class Foo
@@ -7,7 +9,7 @@ class DispatcherTest < ActiveSupport::TestCase
class DummyApp
def call(env)
- [200, {}, 'response']
+ [200, {}, "response"]
end
end
@@ -28,31 +30,18 @@ class DispatcherTest < ActiveSupport::TestCase
assert_equal 4, Foo.a
assert_equal 4, Foo.b
- dispatch do |env|
+ dispatch do
raise "error"
end rescue nil
assert_equal 6, Foo.a
assert_equal 6, Foo.b
end
- def test_to_prepare_and_cleanup_delegation
- prepared = cleaned = false
- ActionDispatch::Callbacks.to_prepare { prepared = true }
- ActionDispatch::Callbacks.to_prepare { cleaned = true }
-
- ActionDispatch::Reloader.prepare!
- assert prepared
-
- ActionDispatch::Reloader.cleanup!
- assert cleaned
- end
-
private
def dispatch(&block)
ActionDispatch::Callbacks.new(block || DummyApp.new).call(
- {'rack.input' => StringIO.new('')}
+ "rack.input" => StringIO.new("")
)
end
-
end
diff --git a/actionpack/test/dispatch/content_disposition_test.rb b/actionpack/test/dispatch/content_disposition_test.rb
new file mode 100644
index 0000000000..3f5959da6e
--- /dev/null
+++ b/actionpack/test/dispatch/content_disposition_test.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+module ActionDispatch
+ class ContentDispositionTest < ActiveSupport::TestCase
+ test "encoding a Latin filename" do
+ disposition = Http::ContentDisposition.new(disposition: :inline, filename: "racecar.jpg")
+
+ assert_equal %(filename="racecar.jpg"), disposition.ascii_filename
+ assert_equal "filename*=UTF-8''racecar.jpg", disposition.utf8_filename
+ assert_equal "inline; #{disposition.ascii_filename}; #{disposition.utf8_filename}", disposition.to_s
+ end
+
+ test "encoding a Latin filename with accented characters" do
+ disposition = Http::ContentDisposition.new(disposition: :inline, filename: "råcëçâr.jpg")
+
+ assert_equal %(filename="racecar.jpg"), disposition.ascii_filename
+ assert_equal "filename*=UTF-8''r%C3%A5c%C3%AB%C3%A7%C3%A2r.jpg", disposition.utf8_filename
+ assert_equal "inline; #{disposition.ascii_filename}; #{disposition.utf8_filename}", disposition.to_s
+ end
+
+ test "encoding a non-Latin filename" do
+ disposition = Http::ContentDisposition.new(disposition: :inline, filename: "автомобиль.jpg")
+
+ assert_equal %(filename="%3F%3F%3F%3F%3F%3F%3F%3F%3F%3F.jpg"), disposition.ascii_filename
+ assert_equal "filename*=UTF-8''%D0%B0%D0%B2%D1%82%D0%BE%D0%BC%D0%BE%D0%B1%D0%B8%D0%BB%D1%8C.jpg", disposition.utf8_filename
+ assert_equal "inline; #{disposition.ascii_filename}; #{disposition.utf8_filename}", disposition.to_s
+ end
+
+ test "without filename" do
+ disposition = Http::ContentDisposition.new(disposition: :inline, filename: nil)
+
+ assert_equal "inline", disposition.to_s
+ end
+ end
+end
diff --git a/actionpack/test/dispatch/content_security_policy_test.rb b/actionpack/test/dispatch/content_security_policy_test.rb
new file mode 100644
index 0000000000..c8c885f35c
--- /dev/null
+++ b/actionpack/test/dispatch/content_security_policy_test.rb
@@ -0,0 +1,546 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class ContentSecurityPolicyTest < ActiveSupport::TestCase
+ def setup
+ @policy = ActionDispatch::ContentSecurityPolicy.new
+ end
+
+ def test_build
+ assert_equal "", @policy.build
+
+ @policy.script_src :self
+ assert_equal "script-src 'self'", @policy.build
+ end
+
+ def test_dup
+ @policy.img_src :self
+ @policy.block_all_mixed_content
+ @policy.upgrade_insecure_requests
+ @policy.sandbox
+ copied = @policy.dup
+ assert_equal copied.build, @policy.build
+ end
+
+ def test_mappings
+ @policy.script_src :data
+ assert_equal "script-src data:", @policy.build
+
+ @policy.script_src :mediastream
+ assert_equal "script-src mediastream:", @policy.build
+
+ @policy.script_src :blob
+ assert_equal "script-src blob:", @policy.build
+
+ @policy.script_src :filesystem
+ assert_equal "script-src filesystem:", @policy.build
+
+ @policy.script_src :self
+ assert_equal "script-src 'self'", @policy.build
+
+ @policy.script_src :unsafe_inline
+ assert_equal "script-src 'unsafe-inline'", @policy.build
+
+ @policy.script_src :unsafe_eval
+ assert_equal "script-src 'unsafe-eval'", @policy.build
+
+ @policy.script_src :none
+ assert_equal "script-src 'none'", @policy.build
+
+ @policy.script_src :strict_dynamic
+ assert_equal "script-src 'strict-dynamic'", @policy.build
+
+ @policy.script_src :ws
+ assert_equal "script-src ws:", @policy.build
+
+ @policy.script_src :wss
+ assert_equal "script-src wss:", @policy.build
+
+ @policy.script_src :none, :report_sample
+ assert_equal "script-src 'none' 'report-sample'", @policy.build
+ end
+
+ def test_fetch_directives
+ @policy.child_src :self
+ assert_match %r{child-src 'self'}, @policy.build
+
+ @policy.child_src false
+ assert_no_match %r{child-src}, @policy.build
+
+ @policy.connect_src :self
+ assert_match %r{connect-src 'self'}, @policy.build
+
+ @policy.connect_src false
+ assert_no_match %r{connect-src}, @policy.build
+
+ @policy.default_src :self
+ assert_match %r{default-src 'self'}, @policy.build
+
+ @policy.default_src false
+ assert_no_match %r{default-src}, @policy.build
+
+ @policy.font_src :self
+ assert_match %r{font-src 'self'}, @policy.build
+
+ @policy.font_src false
+ assert_no_match %r{font-src}, @policy.build
+
+ @policy.frame_src :self
+ assert_match %r{frame-src 'self'}, @policy.build
+
+ @policy.frame_src false
+ assert_no_match %r{frame-src}, @policy.build
+
+ @policy.img_src :self
+ assert_match %r{img-src 'self'}, @policy.build
+
+ @policy.img_src false
+ assert_no_match %r{img-src}, @policy.build
+
+ @policy.manifest_src :self
+ assert_match %r{manifest-src 'self'}, @policy.build
+
+ @policy.manifest_src false
+ assert_no_match %r{manifest-src}, @policy.build
+
+ @policy.media_src :self
+ assert_match %r{media-src 'self'}, @policy.build
+
+ @policy.media_src false
+ assert_no_match %r{media-src}, @policy.build
+
+ @policy.object_src :self
+ assert_match %r{object-src 'self'}, @policy.build
+
+ @policy.object_src false
+ assert_no_match %r{object-src}, @policy.build
+
+ @policy.prefetch_src :self
+ assert_match %r{prefetch-src 'self'}, @policy.build
+
+ @policy.prefetch_src false
+ assert_no_match %r{prefetch-src}, @policy.build
+
+ @policy.script_src :self
+ assert_match %r{script-src 'self'}, @policy.build
+
+ @policy.script_src false
+ assert_no_match %r{script-src}, @policy.build
+
+ @policy.style_src :self
+ assert_match %r{style-src 'self'}, @policy.build
+
+ @policy.style_src false
+ assert_no_match %r{style-src}, @policy.build
+
+ @policy.worker_src :self
+ assert_match %r{worker-src 'self'}, @policy.build
+
+ @policy.worker_src false
+ assert_no_match %r{worker-src}, @policy.build
+ end
+
+ def test_document_directives
+ @policy.base_uri "https://example.com"
+ assert_match %r{base-uri https://example\.com}, @policy.build
+
+ @policy.plugin_types "application/x-shockwave-flash"
+ assert_match %r{plugin-types application/x-shockwave-flash}, @policy.build
+
+ @policy.sandbox
+ assert_match %r{sandbox}, @policy.build
+
+ @policy.sandbox "allow-scripts", "allow-modals"
+ assert_match %r{sandbox allow-scripts allow-modals}, @policy.build
+
+ @policy.sandbox false
+ assert_no_match %r{sandbox}, @policy.build
+ end
+
+ def test_navigation_directives
+ @policy.form_action :self
+ assert_match %r{form-action 'self'}, @policy.build
+
+ @policy.frame_ancestors :self
+ assert_match %r{frame-ancestors 'self'}, @policy.build
+ end
+
+ def test_reporting_directives
+ @policy.report_uri "/violations"
+ assert_match %r{report-uri /violations}, @policy.build
+ end
+
+ def test_other_directives
+ @policy.block_all_mixed_content
+ assert_match %r{block-all-mixed-content}, @policy.build
+
+ @policy.block_all_mixed_content false
+ assert_no_match %r{block-all-mixed-content}, @policy.build
+
+ @policy.require_sri_for :script, :style
+ assert_match %r{require-sri-for script style}, @policy.build
+
+ @policy.require_sri_for "script", "style"
+ assert_match %r{require-sri-for script style}, @policy.build
+
+ @policy.require_sri_for
+ assert_no_match %r{require-sri-for}, @policy.build
+
+ @policy.upgrade_insecure_requests
+ assert_match %r{upgrade-insecure-requests}, @policy.build
+
+ @policy.upgrade_insecure_requests false
+ assert_no_match %r{upgrade-insecure-requests}, @policy.build
+ end
+
+ def test_multiple_sources
+ @policy.script_src :self, :https
+ assert_equal "script-src 'self' https:", @policy.build
+ end
+
+ def test_multiple_directives
+ @policy.script_src :self, :https
+ @policy.style_src :self, :https
+ assert_equal "script-src 'self' https:; style-src 'self' https:", @policy.build
+ end
+
+ def test_dynamic_directives
+ request = ActionDispatch::Request.new("HTTP_HOST" => "www.example.com")
+ controller = Struct.new(:request).new(request)
+
+ @policy.script_src -> { request.host }
+ assert_equal "script-src www.example.com", @policy.build(controller)
+ end
+
+ def test_mixed_static_and_dynamic_directives
+ @policy.script_src :self, -> { "foo.com" }, "bar.com"
+ request = ActionDispatch::Request.new({})
+ controller = Struct.new(:request).new(request)
+ assert_equal "script-src 'self' foo.com bar.com", @policy.build(controller)
+ end
+
+ def test_invalid_directive_source
+ exception = assert_raises(ArgumentError) do
+ @policy.script_src [:self]
+ end
+
+ assert_equal "Invalid content security policy source: [:self]", exception.message
+ end
+
+ def test_missing_context_for_dynamic_source
+ @policy.script_src -> { request.host }
+
+ exception = assert_raises(RuntimeError) do
+ @policy.build
+ end
+
+ assert_match %r{\AMissing context for the dynamic content security policy source:}, exception.message
+ end
+
+ def test_raises_runtime_error_when_unexpected_source
+ @policy.plugin_types [:flash]
+
+ exception = assert_raises(RuntimeError) do
+ @policy.build
+ end
+
+ assert_match %r{\AUnexpected content security policy source:}, exception.message
+ end
+end
+
+class DefaultContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest
+ class PolicyController < ActionController::Base
+ def index
+ head :ok
+ end
+ end
+
+ ROUTES = ActionDispatch::Routing::RouteSet.new
+ ROUTES.draw do
+ scope module: "default_content_security_policy_integration_test" do
+ get "/", to: "policy#index"
+ get "/redirect", to: redirect("/")
+ end
+ end
+
+ POLICY = ActionDispatch::ContentSecurityPolicy.new do |p|
+ p.default_src -> { :self }
+ p.script_src -> { :https }
+ end
+
+ class PolicyConfigMiddleware
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ env["action_dispatch.content_security_policy"] = POLICY
+ env["action_dispatch.content_security_policy_nonce_generator"] = proc { "iyhD0Yc0W+c=" }
+ env["action_dispatch.content_security_policy_report_only"] = false
+ env["action_dispatch.show_exceptions"] = false
+
+ @app.call(env)
+ end
+ end
+
+ APP = build_app(ROUTES) do |middleware|
+ middleware.use PolicyConfigMiddleware
+ middleware.use ActionDispatch::ContentSecurityPolicy::Middleware
+ end
+
+ def app
+ APP
+ end
+
+ def test_adds_nonce_to_script_src_content_security_policy_only_once
+ get "/"
+ get "/"
+ assert_response :success
+ assert_policy "default-src 'self'; script-src https: 'nonce-iyhD0Yc0W+c='"
+ end
+
+ def test_redirect_works_with_dynamic_sources
+ get "/redirect"
+ assert_response :redirect
+ assert_policy "default-src 'self'; script-src https: 'nonce-iyhD0Yc0W+c='"
+ end
+
+ private
+
+ def assert_policy(expected, report_only: false)
+ if report_only
+ expected_header = "Content-Security-Policy-Report-Only"
+ unexpected_header = "Content-Security-Policy"
+ else
+ expected_header = "Content-Security-Policy"
+ unexpected_header = "Content-Security-Policy-Report-Only"
+ end
+
+ assert_nil response.headers[unexpected_header]
+ assert_equal expected, response.headers[expected_header]
+ end
+end
+
+class ContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest
+ class PolicyController < ActionController::Base
+ content_security_policy only: :inline do |p|
+ p.default_src "https://example.com"
+ end
+
+ content_security_policy only: :conditional, if: :condition? do |p|
+ p.default_src "https://true.example.com"
+ end
+
+ content_security_policy only: :conditional, unless: :condition? do |p|
+ p.default_src "https://false.example.com"
+ end
+
+ content_security_policy only: :report_only do |p|
+ p.report_uri "/violations"
+ end
+
+ content_security_policy only: :script_src do |p|
+ p.default_src false
+ p.script_src :self
+ end
+
+ content_security_policy only: :style_src do |p|
+ p.default_src false
+ p.style_src :self
+ end
+
+ content_security_policy(false, only: :no_policy)
+
+ content_security_policy_report_only only: :report_only
+
+ def index
+ head :ok
+ end
+
+ def inline
+ head :ok
+ end
+
+ def conditional
+ head :ok
+ end
+
+ def report_only
+ head :ok
+ end
+
+ def script_src
+ head :ok
+ end
+
+ def style_src
+ head :ok
+ end
+
+ def no_policy
+ head :ok
+ end
+
+ private
+ def condition?
+ params[:condition] == "true"
+ end
+ end
+
+ ROUTES = ActionDispatch::Routing::RouteSet.new
+ ROUTES.draw do
+ scope module: "content_security_policy_integration_test" do
+ get "/", to: "policy#index"
+ get "/inline", to: "policy#inline"
+ get "/conditional", to: "policy#conditional"
+ get "/report-only", to: "policy#report_only"
+ get "/script-src", to: "policy#script_src"
+ get "/style-src", to: "policy#style_src"
+ get "/no-policy", to: "policy#no_policy"
+ end
+ end
+
+ POLICY = ActionDispatch::ContentSecurityPolicy.new do |p|
+ p.default_src :self
+ end
+
+ class PolicyConfigMiddleware
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ env["action_dispatch.content_security_policy"] = POLICY
+ env["action_dispatch.content_security_policy_nonce_generator"] = proc { "iyhD0Yc0W+c=" }
+ env["action_dispatch.content_security_policy_report_only"] = false
+ env["action_dispatch.show_exceptions"] = false
+
+ @app.call(env)
+ end
+ end
+
+ APP = build_app(ROUTES) do |middleware|
+ middleware.use PolicyConfigMiddleware
+ middleware.use ActionDispatch::ContentSecurityPolicy::Middleware
+ end
+
+ def app
+ APP
+ end
+
+ def test_generates_content_security_policy_header
+ get "/"
+ assert_policy "default-src 'self'"
+ end
+
+ def test_generates_inline_content_security_policy
+ get "/inline"
+ assert_policy "default-src https://example.com"
+ end
+
+ def test_generates_conditional_content_security_policy
+ get "/conditional", params: { condition: "true" }
+ assert_policy "default-src https://true.example.com"
+
+ get "/conditional", params: { condition: "false" }
+ assert_policy "default-src https://false.example.com"
+ end
+
+ def test_generates_report_only_content_security_policy
+ get "/report-only"
+ assert_policy "default-src 'self'; report-uri /violations", report_only: true
+ end
+
+ def test_adds_nonce_to_script_src_content_security_policy
+ get "/script-src"
+ assert_policy "script-src 'self' 'nonce-iyhD0Yc0W+c='"
+ end
+
+ def test_adds_nonce_to_style_src_content_security_policy
+ get "/style-src"
+ assert_policy "style-src 'self' 'nonce-iyhD0Yc0W+c='"
+ end
+
+ def test_generates_no_content_security_policy
+ get "/no-policy"
+
+ assert_nil response.headers["Content-Security-Policy"]
+ assert_nil response.headers["Content-Security-Policy-Report-Only"]
+ end
+
+ private
+
+ def assert_policy(expected, report_only: false)
+ assert_response :success
+
+ if report_only
+ expected_header = "Content-Security-Policy-Report-Only"
+ unexpected_header = "Content-Security-Policy"
+ else
+ expected_header = "Content-Security-Policy"
+ unexpected_header = "Content-Security-Policy-Report-Only"
+ end
+
+ assert_nil response.headers[unexpected_header]
+ assert_equal expected, response.headers[expected_header]
+ end
+end
+
+class DisabledContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest
+ class PolicyController < ActionController::Base
+ content_security_policy only: :inline do |p|
+ p.default_src "https://example.com"
+ end
+
+ def index
+ head :ok
+ end
+
+ def inline
+ head :ok
+ end
+ end
+
+ ROUTES = ActionDispatch::Routing::RouteSet.new
+ ROUTES.draw do
+ scope module: "disabled_content_security_policy_integration_test" do
+ get "/", to: "policy#index"
+ get "/inline", to: "policy#inline"
+ end
+ end
+
+ class PolicyConfigMiddleware
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ env["action_dispatch.content_security_policy"] = nil
+ env["action_dispatch.content_security_policy_nonce_generator"] = nil
+ env["action_dispatch.content_security_policy_report_only"] = false
+ env["action_dispatch.show_exceptions"] = false
+
+ @app.call(env)
+ end
+ end
+
+ APP = build_app(ROUTES) do |middleware|
+ middleware.use PolicyConfigMiddleware
+ middleware.use ActionDispatch::ContentSecurityPolicy::Middleware
+ end
+
+ def app
+ APP
+ end
+
+ def test_generates_no_content_security_policy_by_default
+ get "/"
+ assert_nil response.headers["Content-Security-Policy"]
+ end
+
+ def test_generates_content_security_policy_header_when_globally_disabled
+ get "/inline"
+ assert_equal "default-src https://example.com", response.headers["Content-Security-Policy"]
+ end
+end
diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb
index aca28ae8d1..6637c2cae9 100644
--- a/actionpack/test/dispatch/cookies_test.rb
+++ b/actionpack/test/dispatch/cookies_test.rb
@@ -1,7 +1,84 @@
-require 'abstract_unit'
-require 'openssl'
-require 'active_support/key_generator'
-require 'active_support/message_verifier'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "openssl"
+require "active_support/key_generator"
+require "active_support/messages/rotation_configuration"
+
+class CookieJarTest < ActiveSupport::TestCase
+ attr_reader :request
+
+ def setup
+ @request = ActionDispatch::Request.empty
+ end
+
+ def test_fetch
+ x = Object.new
+ assert_not request.cookie_jar.key?("zzzzzz")
+ assert_equal x, request.cookie_jar.fetch("zzzzzz", x)
+ assert_not request.cookie_jar.key?("zzzzzz")
+ end
+
+ def test_fetch_exists
+ x = Object.new
+ request.cookie_jar["foo"] = "bar"
+ assert_equal "bar", request.cookie_jar.fetch("foo", x)
+ end
+
+ def test_fetch_block
+ x = Object.new
+ assert_not request.cookie_jar.key?("zzzzzz")
+ assert_equal x, request.cookie_jar.fetch("zzzzzz") { x }
+ end
+
+ def test_key_is_to_s
+ request.cookie_jar["foo"] = "bar"
+ assert_equal "bar", request.cookie_jar.fetch(:foo)
+ end
+
+ def test_to_hash
+ request.cookie_jar["foo"] = "bar"
+ assert_equal({ "foo" => "bar" }, request.cookie_jar.to_hash)
+ assert_equal({ "foo" => "bar" }, request.cookie_jar.to_h)
+ end
+
+ def test_fetch_type_error
+ assert_raises(KeyError) do
+ request.cookie_jar.fetch(:omglolwut)
+ end
+ end
+
+ def test_each
+ request.cookie_jar["foo"] = :bar
+ list = []
+ request.cookie_jar.each do |k, v|
+ list << [k, v]
+ end
+
+ assert_equal [["foo", :bar]], list
+ end
+
+ def test_enumerable
+ request.cookie_jar["foo"] = :bar
+ actual = request.cookie_jar.map { |k, v| [k.to_s, v.to_s] }
+ assert_equal [["foo", "bar"]], actual
+ end
+
+ def test_key_methods
+ assert_not request.cookie_jar.key?(:foo)
+ assert_not request.cookie_jar.has_key?("foo")
+
+ request.cookie_jar[:foo] = :bar
+ assert request.cookie_jar.key?(:foo)
+ assert request.cookie_jar.has_key?("foo")
+ end
+
+ def test_write_doesnt_set_a_nil_header
+ headers = {}
+ request.cookie_jar.write(headers)
+ assert_not_includes headers, "Set-Cookie"
+ end
+end
class CookiesTest < ActionController::TestCase
class CustomSerializer
@@ -14,16 +91,6 @@ class CookiesTest < ActionController::TestCase
end
end
- class JSONWrapper
- def initialize(obj)
- @obj = obj
- end
-
- def as_json(options = nil)
- "wrapped: #{@obj.as_json(options)}"
- end
- end
-
class TestController < ActionController::Base
def authenticate
cookies["user_name"] = "david"
@@ -36,17 +103,17 @@ class CookiesTest < ActionController::TestCase
end
def authenticate_for_fourteen_days
- cookies["user_name"] = { "value" => "david", "expires" => Time.utc(2005, 10, 10,5) }
+ cookies["user_name"] = { "value" => "david", "expires" => Time.utc(2005, 10, 10, 5) }
head :ok
end
def authenticate_for_fourteen_days_with_symbols
- cookies[:user_name] = { :value => "david", :expires => Time.utc(2005, 10, 10,5) }
+ cookies[:user_name] = { value: "david", expires: Time.utc(2005, 10, 10, 5) }
head :ok
end
def set_multiple_cookies
- cookies["user_name"] = { "value" => "david", "expires" => Time.utc(2005, 10, 10,5) }
+ cookies["user_name"] = { "value" => "david", "expires" => Time.utc(2005, 10, 10, 5) }
cookies["login"] = "XJ-122"
head :ok
end
@@ -64,17 +131,17 @@ class CookiesTest < ActionController::TestCase
alias delete_cookie logout
def delete_cookie_with_path
- cookies.delete("user_name", :path => '/beaten')
+ cookies.delete("user_name", path: "/beaten")
head :ok
end
def authenticate_with_http_only
- cookies["user_name"] = { :value => "david", :httponly => true }
+ cookies["user_name"] = { value: "david", httponly: true }
head :ok
end
def authenticate_with_secure
- cookies["user_name"] = { :value => "david", :secure => true }
+ cookies["user_name"] = { value: "david", secure: true }
head :ok
end
@@ -88,23 +155,33 @@ class CookiesTest < ActionController::TestCase
head :ok
end
- def set_wrapped_signed_cookie
- cookies.signed[:user_id] = JSONWrapper.new(45)
- head :ok
- end
-
def get_signed_cookie
cookies.signed[:user_id]
head :ok
end
def set_encrypted_cookie
- cookies.encrypted[:foo] = 'bar'
+ cookies.encrypted[:foo] = "bar"
+ head :ok
+ end
+
+ class JSONWrapper
+ def initialize(obj)
+ @obj = obj
+ end
+
+ def as_json(options = nil)
+ "wrapped: #{@obj.as_json(options)}"
+ end
+ end
+
+ def set_wrapped_signed_cookie
+ cookies.signed[:user_id] = JSONWrapper.new(45)
head :ok
end
def set_wrapped_encrypted_cookie
- cookies.encrypted[:foo] = JSONWrapper.new('bar')
+ cookies.encrypted[:foo] = JSONWrapper.new("bar")
head :ok
end
@@ -114,12 +191,12 @@ class CookiesTest < ActionController::TestCase
end
def set_invalid_encrypted_cookie
- cookies[:invalid_cookie] = 'invalid--9170e00a57cfc27083363b5c75b835e477bd90cf'
+ cookies[:invalid_cookie] = "invalid--9170e00a57cfc27083363b5c75b835e477bd90cf"
head :ok
end
def raise_data_overflow
- cookies.signed[:foo] = 'bye!' * 1024
+ cookies.signed[:foo] = "bye!" * 1024
head :ok
end
@@ -136,47 +213,47 @@ class CookiesTest < ActionController::TestCase
def delete_and_set_cookie
cookies.delete :user_name
- cookies[:user_name] = { :value => "david", :expires => Time.utc(2005, 10, 10,5) }
+ cookies[:user_name] = { value: "david", expires: Time.utc(2005, 10, 10, 5) }
head :ok
end
def set_cookie_with_domain
- cookies[:user_name] = {:value => "rizwanreza", :domain => :all}
+ cookies[:user_name] = { value: "rizwanreza", domain: :all }
head :ok
end
def set_cookie_with_domain_all_as_string
- cookies[:user_name] = {:value => "rizwanreza", :domain => 'all'}
+ cookies[:user_name] = { value: "rizwanreza", domain: "all" }
head :ok
end
def delete_cookie_with_domain
- cookies.delete(:user_name, :domain => :all)
+ cookies.delete(:user_name, domain: :all)
head :ok
end
def delete_cookie_with_domain_all_as_string
- cookies.delete(:user_name, :domain => 'all')
+ cookies.delete(:user_name, domain: "all")
head :ok
end
def set_cookie_with_domain_and_tld
- cookies[:user_name] = {:value => "rizwanreza", :domain => :all, :tld_length => 2}
+ cookies[:user_name] = { value: "rizwanreza", domain: :all, tld_length: 2 }
head :ok
end
def delete_cookie_with_domain_and_tld
- cookies.delete(:user_name, :domain => :all, :tld_length => 2)
+ cookies.delete(:user_name, domain: :all, tld_length: 2)
head :ok
end
def set_cookie_with_domains
- cookies[:user_name] = {:value => "rizwanreza", :domain => %w(example1.com example2.com .example3.com)}
+ cookies[:user_name] = { value: "rizwanreza", domain: %w(example1.com example2.com .example3.com) }
head :ok
end
def delete_cookie_with_domains
- cookies.delete(:user_name, :domain => %w(example1.com example2.com .example3.com))
+ cookies.delete(:user_name, domain: %w(example1.com example2.com .example3.com))
head :ok
end
@@ -186,7 +263,7 @@ class CookiesTest < ActionController::TestCase
end
def string_key
- cookies['user_name'] = "dhh"
+ cookies["user_name"] = "dhh"
head :ok
end
@@ -196,148 +273,158 @@ class CookiesTest < ActionController::TestCase
end
def string_key_mock
- cookies['user_name'] = "david" if cookies['user_name'] == "andrew"
+ cookies["user_name"] = "david" if cookies["user_name"] == "andrew"
head :ok
end
def noop
head :ok
end
- end
- tests TestController
+ def encrypted_cookie
+ cookies.encrypted["foo"]
+ end
- def setup
- super
- @request.env["action_dispatch.key_generator"] = ActiveSupport::KeyGenerator.new("b3c631c314c0bbca50c1b2843150fe33", iterations: 2)
- @request.env["action_dispatch.signed_cookie_salt"] = "b3c631c314c0bbca50c1b2843150fe33"
- @request.env["action_dispatch.encrypted_cookie_salt"] = "b3c631c314c0bbca50c1b2843150fe33"
- @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "b3c631c314c0bbca50c1b2843150fe33"
- @request.host = "www.nextangle.com"
- end
+ def cookie_expires_in_two_hours
+ cookies[:user_name] = { value: "assain", expires: 2.hours }
+ head :ok
+ end
- def test_fetch
- x = Object.new
- assert_not request.cookie_jar.key?('zzzzzz')
- assert_equal x, request.cookie_jar.fetch('zzzzzz', x)
- assert_not request.cookie_jar.key?('zzzzzz')
- end
+ def encrypted_discount_and_user_id_cookie
+ cookies.encrypted[:user_id] = { value: 50, expires: 1.hour }
+ cookies.encrypted[:discount_percentage] = 10
- def test_fetch_exists
- x = Object.new
- request.cookie_jar['foo'] = 'bar'
- assert_equal 'bar', request.cookie_jar.fetch('foo', x)
- end
+ head :ok
+ end
- def test_fetch_block
- x = Object.new
- assert_not request.cookie_jar.key?('zzzzzz')
- assert_equal x, request.cookie_jar.fetch('zzzzzz') { x }
- end
+ def signed_discount_and_user_id_cookie
+ cookies.signed[:user_id] = { value: 50, expires: 1.hour }
+ cookies.signed[:discount_percentage] = 10
- def test_key_is_to_s
- request.cookie_jar['foo'] = 'bar'
- assert_equal 'bar', request.cookie_jar.fetch(:foo)
- end
+ head :ok
+ end
- def test_fetch_type_error
- assert_raises(KeyError) do
- request.cookie_jar.fetch(:omglolwut)
+ def rails_5_2_stable_encrypted_cookie_with_authenticated_encryption_flag_on
+ # cookies.encrypted[:favorite] = { value: "5-2-Stable Chocolate Cookies", expires: 1000.years }
+ cookies[:favorite] = "KvH5lIHvX5vPQkLIK63r/NuIMwzWky8M0Zwk8SZ6DwUv8+srf36geR4nWq5KmhsZIYXA8NRdCZYIfxMKJsOFlz77Gf+Fq8vBBCWJTp95rx39A28TCUTJEyMhCNJO5eie7Skef76Qt5Jo/SCnIADAhzyGQkGBopKRcA==--qXZZFWGbCy6N8AGy--WswoH+xHrNh9MzSXDpB2fA=="
+
+ head :ok
end
- end
- def test_each
- request.cookie_jar['foo'] = :bar
- list = []
- request.cookie_jar.each do |k,v|
- list << [k, v]
+ def rails_5_2_stable_encrypted_cookie_with_authenticated_encryption_flag_off
+ cookies[:favorite] = "Wmg4amgvcVVvWGcwK3c4WjJEbTdRQUgrWXhBdDliUTR0cVNidXpmVTMrc2RjcitwUzVsWWEwZGtuVGtFUjJwNi0tcVhVMTFMOTQ1d0hIVE1FK0pJc05SQT09--8b2a55c375049a50f7a959b9d42b31ef0b2bb594"
+
+ head :ok
end
- assert_equal [['foo', :bar]], list
- end
+ def rails_5_2_stable_signed_cookie_with_authenticated_encryption_flag_on
+ # cookies.signed[:favorite] = { value: "5-2-Stable Choco Chip Cookie", expires: 1000.years }
+ cookies[:favorite] = "eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaEpJaUUxTFRJdFUzUmhZbXhsSUVOb2IyTnZJRU5vYVhBZ1EyOXZhMmxsQmpvR1JWUT0iLCJleHAiOiIzMDE4LTA3LTExVDE2OjExOjI2Ljc1M1oiLCJwdXIiOm51bGx9fQ==--7df5d885b78b70a501d6e82140ae91b24060ac00"
- def test_enumerable
- request.cookie_jar['foo'] = :bar
- actual = request.cookie_jar.map { |k,v| [k.to_s, v.to_s] }
- assert_equal [['foo', 'bar']], actual
+ head :ok
+ end
+
+ def rails_5_2_stable_signed_cookie_with_authenticated_encryption_flag_off
+ cookies[:favorite] = "BAhJIiE1LTItU3RhYmxlIENob2NvIENoaXAgQ29va2llBjoGRVQ=--50bbdbf8d64f5a3ec3e54878f54d4f55b6cb3aff"
+
+ head :ok
+ end
end
- def test_key_methods
- assert !request.cookie_jar.key?(:foo)
- assert !request.cookie_jar.has_key?("foo")
+ tests TestController
- request.cookie_jar[:foo] = :bar
- assert request.cookie_jar.key?(:foo)
- assert request.cookie_jar.has_key?("foo")
+ SECRET_KEY_BASE = "b3c631c314c0bbca50c1b2843150fe33"
+ SIGNED_COOKIE_SALT = "signed cookie"
+ ENCRYPTED_COOKIE_SALT = "encrypted cookie"
+ ENCRYPTED_SIGNED_COOKIE_SALT = "sigend encrypted cookie"
+ AUTHENTICATED_ENCRYPTED_COOKIE_SALT = "authenticated encrypted cookie"
+
+ def setup
+ super
+
+ @request.env["action_dispatch.key_generator"] = ActiveSupport::KeyGenerator.new(SECRET_KEY_BASE, iterations: 2)
+ @request.env["action_dispatch.cookies_rotations"] = ActiveSupport::Messages::RotationConfiguration.new
+
+ @request.env["action_dispatch.secret_key_base"] = SECRET_KEY_BASE
+ @request.env["action_dispatch.use_authenticated_cookie_encryption"] = true
+
+ @request.env["action_dispatch.signed_cookie_salt"] = SIGNED_COOKIE_SALT
+ @request.env["action_dispatch.encrypted_cookie_salt"] = ENCRYPTED_COOKIE_SALT
+ @request.env["action_dispatch.encrypted_signed_cookie_salt"] = ENCRYPTED_SIGNED_COOKIE_SALT
+ @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] = AUTHENTICATED_ENCRYPTED_COOKIE_SALT
+
+ @request.host = "www.nextangle.com"
end
def test_setting_cookie
get :authenticate
assert_cookie_header "user_name=david; path=/"
- assert_equal({"user_name" => "david"}, @response.cookies)
+ assert_equal({ "user_name" => "david" }, @response.cookies)
end
def test_setting_the_same_value_to_cookie
- request.cookies[:user_name] = 'david'
+ request.cookies[:user_name] = "david"
get :authenticate
- assert_predicate response.cookies, :empty?
+ assert_empty response.cookies
end
def test_setting_the_same_value_to_permanent_cookie
- request.cookies[:user_name] = 'Jamie'
+ request.cookies[:user_name] = "Jamie"
get :set_permanent_cookie
- assert_equal({'user_name' => 'Jamie'}, response.cookies)
+ assert_equal({ "user_name" => "Jamie" }, response.cookies)
end
def test_setting_with_escapable_characters
get :set_with_with_escapable_characters
assert_cookie_header "that+%26+guy=foo+%26+bar+%3D%3E+baz; path=/"
- assert_equal({"that & guy" => "foo & bar => baz"}, @response.cookies)
+ assert_equal({ "that & guy" => "foo & bar => baz" }, @response.cookies)
end
def test_setting_cookie_for_fourteen_days
get :authenticate_for_fourteen_days
assert_cookie_header "user_name=david; path=/; expires=Mon, 10 Oct 2005 05:00:00 -0000"
- assert_equal({"user_name" => "david"}, @response.cookies)
+ assert_equal({ "user_name" => "david" }, @response.cookies)
end
def test_setting_cookie_for_fourteen_days_with_symbols
get :authenticate_for_fourteen_days_with_symbols
assert_cookie_header "user_name=david; path=/; expires=Mon, 10 Oct 2005 05:00:00 -0000"
- assert_equal({"user_name" => "david"}, @response.cookies)
+ assert_equal({ "user_name" => "david" }, @response.cookies)
end
def test_setting_cookie_with_http_only
get :authenticate_with_http_only
assert_cookie_header "user_name=david; path=/; HttpOnly"
- assert_equal({"user_name" => "david"}, @response.cookies)
+ assert_equal({ "user_name" => "david" }, @response.cookies)
end
def test_setting_cookie_with_secure
@request.env["HTTPS"] = "on"
get :authenticate_with_secure
assert_cookie_header "user_name=david; path=/; secure"
- assert_equal({"user_name" => "david"}, @response.cookies)
+ assert_equal({ "user_name" => "david" }, @response.cookies)
end
def test_setting_cookie_with_secure_when_always_write_cookie_is_true
- ActionDispatch::Cookies::CookieJar.any_instance.stubs(:always_write_cookie).returns(true)
+ old_cookie, @request.cookie_jar.always_write_cookie = @request.cookie_jar.always_write_cookie, true
get :authenticate_with_secure
assert_cookie_header "user_name=david; path=/; secure"
- assert_equal({"user_name" => "david"}, @response.cookies)
+ assert_equal({ "user_name" => "david" }, @response.cookies)
+ ensure
+ @request.cookie_jar.always_write_cookie = old_cookie
end
def test_not_setting_cookie_with_secure
get :authenticate_with_secure
assert_not_cookie_header "user_name=david; path=/; secure"
- assert_not_equal({"user_name" => "david"}, @response.cookies)
+ assert_not_equal({ "user_name" => "david" }, @response.cookies)
end
def test_multiple_cookies
get :set_multiple_cookies
assert_equal 2, @response.cookies.size
assert_cookie_header "user_name=david; path=/; expires=Mon, 10 Oct 2005 05:00:00 -0000\nlogin=XJ-122; path=/"
- assert_equal({"login" => "XJ-122", "user_name" => "david"}, @response.cookies)
+ assert_equal({ "login" => "XJ-122", "user_name" => "david" }, @response.cookies)
end
def test_setting_test_cookie
@@ -345,14 +432,14 @@ class CookiesTest < ActionController::TestCase
end
def test_expiring_cookie
- request.cookies[:user_name] = 'Joe'
+ request.cookies[:user_name] = "Joe"
get :logout
assert_cookie_header "user_name=; path=/; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"
- assert_equal({"user_name" => nil}, @response.cookies)
+ assert_equal({ "user_name" => nil }, @response.cookies)
end
def test_delete_cookie_with_path
- request.cookies[:user_name] = 'Joe'
+ request.cookies[:user_name] = "Joe"
get :delete_cookie_with_path
assert_cookie_header "user_name=; path=/beaten; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"
end
@@ -360,20 +447,20 @@ class CookiesTest < ActionController::TestCase
def test_delete_unexisting_cookie
request.cookies.clear
get :delete_cookie
- assert_predicate @response.cookies, :empty?
+ assert_empty @response.cookies
end
def test_deleted_cookie_predicate
- cookies[:user_name] = 'Joe'
+ cookies[:user_name] = "Joe"
cookies.delete("user_name")
assert cookies.deleted?("user_name")
assert_equal false, cookies.deleted?("another")
end
def test_deleted_cookie_predicate_with_mismatching_options
- cookies[:user_name] = 'Joe'
- cookies.delete("user_name", :path => "/path")
- assert_equal false, cookies.deleted?("user_name", :path => "/different")
+ cookies[:user_name] = "Joe"
+ cookies.delete("user_name", path: "/path")
+ assert_equal false, cookies.deleted?("user_name", path: "/different")
end
def test_cookies_persist_throughout_request
@@ -389,7 +476,7 @@ class CookiesTest < ActionController::TestCase
def test_read_permanent_cookie
get :set_permanent_cookie
- assert_equal 'Jamie', @controller.send(:cookies).permanent[:user_name]
+ assert_equal "Jamie", @controller.send(:cookies).permanent[:user_name]
end
def test_signed_cookie_using_default_digest
@@ -399,28 +486,72 @@ class CookiesTest < ActionController::TestCase
assert_equal 45, cookies.signed[:user_id]
key_generator = @request.env["action_dispatch.key_generator"]
- signed_cookie_salt = @request.env["action_dispatch.signed_cookie_salt"]
- secret = key_generator.generate_key(signed_cookie_salt)
+ secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"])
- verifier = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal, digest: 'SHA1')
+ verifier = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal, digest: "SHA1")
assert_equal verifier.generate(45), cookies[:user_id]
end
def test_signed_cookie_using_custom_digest
- @request.env["action_dispatch.cookies_digest"] = 'SHA256'
+ @request.env["action_dispatch.signed_cookie_digest"] = "SHA256"
+
get :set_signed_cookie
cookies = @controller.send :cookies
assert_not_equal 45, cookies[:user_id]
assert_equal 45, cookies.signed[:user_id]
key_generator = @request.env["action_dispatch.key_generator"]
- signed_cookie_salt = @request.env["action_dispatch.signed_cookie_salt"]
- secret = key_generator.generate_key(signed_cookie_salt)
+ secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"])
- verifier = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal, digest: 'SHA256')
+ verifier = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal, digest: "SHA256")
assert_equal verifier.generate(45), cookies[:user_id]
end
+ def test_signed_cookie_rotating_secret_and_digest
+ secret = "b3c631c314c0bbca50c1b2843150fe33"
+
+ @request.env["action_dispatch.signed_cookie_digest"] = "SHA256"
+ @request.env["action_dispatch.cookies_rotations"].rotate :signed, secret, digest: "SHA1"
+
+ old_message = ActiveSupport::MessageVerifier.new(secret, digest: "SHA1", serializer: Marshal).generate(45)
+ @request.headers["Cookie"] = "user_id=#{old_message}"
+
+ get :get_signed_cookie
+ assert_equal 45, @controller.send(:cookies).signed[:user_id]
+
+ key_generator = @request.env["action_dispatch.key_generator"]
+ secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"])
+ verifier = ActiveSupport::MessageVerifier.new(secret, digest: "SHA256", serializer: Marshal)
+ assert_equal 45, verifier.verify(@response.cookies["user_id"])
+ end
+
+ def test_signed_cookie_with_legacy_secret_scheme
+ @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
+
+ old_message = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", digest: "SHA1", serializer: Marshal).generate(45)
+
+ @request.headers["Cookie"] = "user_id=#{old_message}"
+ get :get_signed_cookie
+ assert_equal 45, @controller.send(:cookies).signed[:user_id]
+
+ key_generator = @request.env["action_dispatch.key_generator"]
+ secret = key_generator.generate_key("signed cookie")
+ verifier = ActiveSupport::MessageVerifier.new(secret, digest: "SHA1", serializer: Marshal)
+ assert_equal 45, verifier.verify(@response.cookies["user_id"])
+ end
+
+ def test_tampered_with_signed_cookie
+ key_generator = @request.env["action_dispatch.key_generator"]
+ secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"])
+
+ verifier = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal, digest: "SHA1")
+ message = verifier.generate(45)
+
+ @request.headers["Cookie"] = "user_id=#{Marshal.dump 45}--#{message.split("--").last}"
+ get :get_signed_cookie
+ assert_nil @controller.send(:cookies).signed[:user_id]
+ end
+
def test_signed_cookie_using_default_serializer
get :set_signed_cookie
cookies = @controller.send :cookies
@@ -448,23 +579,22 @@ class CookiesTest < ActionController::TestCase
@request.env["action_dispatch.cookies_serializer"] = :json
get :set_wrapped_signed_cookie
cookies = @controller.send :cookies
- assert_not_equal 'wrapped: 45', cookies[:user_id]
- assert_equal 'wrapped: 45', cookies.signed[:user_id]
+ assert_not_equal "wrapped: 45", cookies[:user_id]
+ assert_equal "wrapped: 45", cookies.signed[:user_id]
end
def test_signed_cookie_using_custom_serializer
@request.env["action_dispatch.cookies_serializer"] = CustomSerializer
get :set_signed_cookie
assert_not_equal 45, cookies[:user_id]
- assert_equal '45 was dumped and loaded', cookies.signed[:user_id]
+ assert_equal "45 was dumped and loaded", cookies.signed[:user_id]
end
def test_signed_cookie_using_hybrid_serializer_can_migrate_marshal_dumped_value_to_json
@request.env["action_dispatch.cookies_serializer"] = :hybrid
key_generator = @request.env["action_dispatch.key_generator"]
- signed_cookie_salt = @request.env["action_dispatch.signed_cookie_salt"]
- secret = key_generator.generate_key(signed_cookie_salt)
+ secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"])
marshal_value = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal).generate(45)
@request.headers["Cookie"] = "user_id=#{marshal_value}"
@@ -476,15 +606,15 @@ class CookiesTest < ActionController::TestCase
assert_equal 45, cookies.signed[:user_id]
verifier = ActiveSupport::MessageVerifier.new(secret, serializer: JSON)
- assert_equal 45, verifier.verify(@response.cookies['user_id'])
+ assert_equal 45, verifier.verify(@response.cookies["user_id"])
end
def test_signed_cookie_using_hybrid_serializer_can_read_from_json_dumped_value
@request.env["action_dispatch.cookies_serializer"] = :hybrid
key_generator = @request.env["action_dispatch.key_generator"]
- signed_cookie_salt = @request.env["action_dispatch.signed_cookie_salt"]
- secret = key_generator.generate_key(signed_cookie_salt)
+ secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"])
+
json_value = ActiveSupport::MessageVerifier.new(secret, serializer: JSON).generate(45)
@request.headers["Cookie"] = "user_id=#{json_value}"
@@ -499,91 +629,60 @@ class CookiesTest < ActionController::TestCase
def test_accessing_nonexistent_signed_cookie_should_not_raise_an_invalid_signature
get :set_signed_cookie
- assert_nil @controller.send(:cookies).signed[:non_existant_attribute]
+ assert_nil @controller.send(:cookies).signed[:non_existent_attribute]
end
def test_encrypted_cookie_using_default_serializer
get :set_encrypted_cookie
cookies = @controller.send :cookies
- assert_not_equal 'bar', cookies[:foo]
- assert_raise TypeError do
- cookies.signed[:foo]
- end
- assert_equal 'bar', cookies.encrypted[:foo]
+ assert_not_equal "bar", cookies[:foo]
+ assert_nil cookies.signed[:foo]
+ assert_equal "bar", cookies.encrypted[:foo]
end
def test_encrypted_cookie_using_marshal_serializer
@request.env["action_dispatch.cookies_serializer"] = :marshal
get :set_encrypted_cookie
cookies = @controller.send :cookies
- assert_not_equal 'bar', cookies[:foo]
- assert_raises TypeError do
- cookies.signed[:foo]
- end
- assert_equal 'bar', cookies.encrypted[:foo]
+ assert_not_equal "bar", cookies[:foo]
+ assert_nil cookies.signed[:foo]
+ assert_equal "bar", cookies.encrypted[:foo]
end
def test_encrypted_cookie_using_json_serializer
@request.env["action_dispatch.cookies_serializer"] = :json
get :set_encrypted_cookie
cookies = @controller.send :cookies
- assert_not_equal 'bar', cookies[:foo]
- assert_raises ::JSON::ParserError do
- cookies.signed[:foo]
- end
- assert_equal 'bar', cookies.encrypted[:foo]
+ assert_not_equal "bar", cookies[:foo]
+ assert_nil cookies.signed[:foo]
+ assert_equal "bar", cookies.encrypted[:foo]
end
def test_wrapped_encrypted_cookie_using_json_serializer
@request.env["action_dispatch.cookies_serializer"] = :json
get :set_wrapped_encrypted_cookie
cookies = @controller.send :cookies
- assert_not_equal 'wrapped: bar', cookies[:foo]
- assert_raises ::JSON::ParserError do
- cookies.signed[:foo]
- end
- assert_equal 'wrapped: bar', cookies.encrypted[:foo]
+ assert_not_equal "wrapped: bar", cookies[:foo]
+ assert_nil cookies.signed[:foo]
+ assert_equal "wrapped: bar", cookies.encrypted[:foo]
end
def test_encrypted_cookie_using_custom_serializer
@request.env["action_dispatch.cookies_serializer"] = CustomSerializer
get :set_encrypted_cookie
- assert_not_equal 'bar', cookies.encrypted[:foo]
- assert_equal 'bar was dumped and loaded', cookies.encrypted[:foo]
- end
-
- def test_encrypted_cookie_using_custom_digest
- @request.env["action_dispatch.cookies_digest"] = 'SHA256'
- get :set_encrypted_cookie
- cookies = @controller.send :cookies
- assert_not_equal 'bar', cookies[:foo]
- assert_equal 'bar', cookies.encrypted[:foo]
-
- sign_secret = @request.env["action_dispatch.key_generator"].generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"])
-
- sha1_verifier = ActiveSupport::MessageVerifier.new(sign_secret, serializer: ActiveSupport::MessageEncryptor::NullSerializer, digest: 'SHA1')
- sha256_verifier = ActiveSupport::MessageVerifier.new(sign_secret, serializer: ActiveSupport::MessageEncryptor::NullSerializer, digest: 'SHA256')
-
- assert_raises(ActiveSupport::MessageVerifier::InvalidSignature) do
- sha1_verifier.verify(cookies[:foo])
- end
-
- assert_nothing_raised do
- sha256_verifier.verify(cookies[:foo])
- end
+ assert_not_equal "bar", cookies.encrypted[:foo]
+ assert_equal "bar was dumped and loaded", cookies.encrypted[:foo]
end
def test_encrypted_cookie_using_hybrid_serializer_can_migrate_marshal_dumped_value_to_json
@request.env["action_dispatch.cookies_serializer"] = :hybrid
key_generator = @request.env["action_dispatch.key_generator"]
- encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"]
- encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"]
- secret = key_generator.generate_key(encrypted_cookie_salt)
- sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt)
+ secret = key_generator.generate_key(@request.env["action_dispatch.authenticated_encrypted_cookie_salt"], 32)
- marshal_value = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: Marshal).encrypt_and_sign("bar")
- @request.headers["Cookie"] = "foo=#{marshal_value}"
+ encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: Marshal)
+ marshal_value = encryptor.encrypt_and_sign("bar")
+ @request.headers["Cookie"] = "foo=#{::Rack::Utils.escape marshal_value}"
get :get_encrypted_cookie
@@ -591,20 +690,20 @@ class CookiesTest < ActionController::TestCase
assert_not_equal "bar", cookies[:foo]
assert_equal "bar", cookies.encrypted[:foo]
- encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: JSON)
- assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"])
+ json_encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: JSON)
+ assert_not_nil @response.cookies["foo"]
+ assert_equal "bar", json_encryptor.decrypt_and_verify(@response.cookies["foo"])
end
def test_encrypted_cookie_using_hybrid_serializer_can_read_from_json_dumped_value
@request.env["action_dispatch.cookies_serializer"] = :hybrid
key_generator = @request.env["action_dispatch.key_generator"]
- encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"]
- encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"]
- secret = key_generator.generate_key(encrypted_cookie_salt)
- sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt)
- json_value = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: JSON).encrypt_and_sign("bar")
- @request.headers["Cookie"] = "foo=#{json_value}"
+ secret = key_generator.generate_key(@request.env["action_dispatch.authenticated_encrypted_cookie_salt"], 32)
+
+ encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: JSON)
+ json_value = encryptor.encrypt_and_sign("bar")
+ @request.headers["Cookie"] = "foo=#{::Rack::Utils.escape json_value}"
get :get_encrypted_cookie
@@ -617,7 +716,7 @@ class CookiesTest < ActionController::TestCase
def test_accessing_nonexistent_encrypted_cookie_should_not_raise_invalid_message
get :set_encrypted_cookie
- assert_nil @controller.send(:cookies).encrypted[:non_existant_attribute]
+ assert_nil @controller.send(:cookies).encrypted[:non_existent_attribute]
end
def test_setting_invalid_encrypted_cookie_should_return_nil_when_accessing_it
@@ -632,10 +731,10 @@ class CookiesTest < ActionController::TestCase
end
def test_delete_and_set_cookie
- request.cookies[:user_name] = 'Joe'
+ request.cookies[:user_name] = "Joe"
get :delete_and_set_cookie
assert_cookie_header "user_name=david; path=/; expires=Mon, 10 Oct 2005 05:00:00 -0000"
- assert_equal({"user_name" => "david"}, @response.cookies)
+ assert_equal({ "user_name" => "david" }, @response.cookies)
end
def test_raise_data_overflow
@@ -651,13 +750,22 @@ class CookiesTest < ActionController::TestCase
end
end
+ def test_cookie_jar_mutated_by_request_persists_on_future_requests
+ get :authenticate
+ cookie_jar = @request.cookie_jar
+ cookie_jar.signed[:user_id] = 123
+ assert_equal ["user_name", "user_id"], @request.cookie_jar.instance_variable_get(:@cookies).keys
+ get :get_signed_cookie
+ assert_equal ["user_name", "user_id"], @request.cookie_jar.instance_variable_get(:@cookies).keys
+ end
+
def test_raises_argument_error_if_missing_secret
assert_raise(ArgumentError, nil.inspect) {
@request.env["action_dispatch.key_generator"] = ActiveSupport::LegacyKeyGenerator.new(nil)
get :set_signed_cookie
}
- assert_raise(ArgumentError, ''.inspect) {
+ assert_raise(ArgumentError, "".inspect) {
@request.env["action_dispatch.key_generator"] = ActiveSupport::LegacyKeyGenerator.new("")
get :set_signed_cookie
}
@@ -680,65 +788,8 @@ class CookiesTest < ActionController::TestCase
}
end
- def test_signed_uses_signed_cookie_jar_if_only_secret_token_is_set
- @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
- @request.env["action_dispatch.secret_key_base"] = nil
- get :set_signed_cookie
- assert_kind_of ActionDispatch::Cookies::SignedCookieJar, cookies.signed
- end
-
- def test_signed_uses_signed_cookie_jar_if_only_secret_key_base_is_set
- @request.env["action_dispatch.secret_token"] = nil
- @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
- get :set_signed_cookie
- assert_kind_of ActionDispatch::Cookies::SignedCookieJar, cookies.signed
- end
-
- def test_signed_uses_upgrade_legacy_signed_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
- @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
- @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
- get :set_signed_cookie
- assert_kind_of ActionDispatch::Cookies::UpgradeLegacySignedCookieJar, cookies.signed
- end
-
- def test_signed_or_encrypted_uses_signed_cookie_jar_if_only_secret_token_is_set
- @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
- @request.env["action_dispatch.secret_key_base"] = nil
- get :get_encrypted_cookie
- assert_kind_of ActionDispatch::Cookies::SignedCookieJar, cookies.signed_or_encrypted
- end
-
- def test_signed_or_encrypted_uses_encrypted_cookie_jar_if_only_secret_key_base_is_set
- @request.env["action_dispatch.secret_token"] = nil
- @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
- get :get_encrypted_cookie
- assert_kind_of ActionDispatch::Cookies::EncryptedCookieJar, cookies.signed_or_encrypted
- end
-
- def test_signed_or_encrypted_uses_upgrade_legacy_encrypted_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
- @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
- @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
- get :get_encrypted_cookie
- assert_kind_of ActionDispatch::Cookies::UpgradeLegacyEncryptedCookieJar, cookies.signed_or_encrypted
- end
-
- def test_encrypted_uses_encrypted_cookie_jar_if_only_secret_key_base_is_set
- @request.env["action_dispatch.secret_token"] = nil
- @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
- get :get_encrypted_cookie
- assert_kind_of ActionDispatch::Cookies::EncryptedCookieJar, cookies.encrypted
- end
-
- def test_encrypted_uses_upgrade_legacy_encrypted_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
- @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
- @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
- get :get_encrypted_cookie
- assert_kind_of ActionDispatch::Cookies::UpgradeLegacyEncryptedCookieJar, cookies.encrypted
- end
-
def test_legacy_signed_cookie_is_read_and_transparently_upgraded_by_signed_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
@request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
- @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate(45)
@@ -755,28 +806,22 @@ class CookiesTest < ActionController::TestCase
def test_legacy_signed_cookie_is_read_and_transparently_encrypted_by_encrypted_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
@request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
- @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
- @request.env["action_dispatch.encrypted_cookie_salt"] = "4433796b79d99a7735553e316522acee"
- @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "00646eb40062e1b1deff205a27cd30f9"
- legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate('bar')
+ legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate("bar")
@request.headers["Cookie"] = "foo=#{legacy_value}"
get :get_encrypted_cookie
- assert_equal 'bar', @controller.send(:cookies).encrypted[:foo]
+ assert_equal "bar", @controller.send(:cookies).encrypted[:foo]
- key_generator = @request.env["action_dispatch.key_generator"]
- secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_cookie_salt"])
- sign_secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"])
- encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret)
- assert_equal 'bar', encryptor.decrypt_and_verify(@response.cookies["foo"])
+ secret = @request.env["action_dispatch.key_generator"].generate_key(@request.env["action_dispatch.authenticated_encrypted_cookie_salt"], 32)
+ encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: Marshal)
+ assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"])
end
def test_legacy_json_signed_cookie_is_read_and_transparently_upgraded_by_signed_json_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
@request.env["action_dispatch.cookies_serializer"] = :json
@request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
- @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate(45)
@@ -794,28 +839,24 @@ class CookiesTest < ActionController::TestCase
def test_legacy_json_signed_cookie_is_read_and_transparently_encrypted_by_encrypted_json_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
@request.env["action_dispatch.cookies_serializer"] = :json
@request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
- @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
- @request.env["action_dispatch.encrypted_cookie_salt"] = "4433796b79d99a7735553e316522acee"
- @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "00646eb40062e1b1deff205a27cd30f9"
- legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate('bar')
+ legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate("bar")
@request.headers["Cookie"] = "foo=#{legacy_value}"
get :get_encrypted_cookie
- assert_equal 'bar', @controller.send(:cookies).encrypted[:foo]
+ assert_equal "bar", @controller.send(:cookies).encrypted[:foo]
- key_generator = @request.env["action_dispatch.key_generator"]
- secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_cookie_salt"])
- sign_secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"])
- encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: JSON)
- assert_equal 'bar', encryptor.decrypt_and_verify(@response.cookies["foo"])
+ cipher = "aes-256-gcm"
+ salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"]
+ secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)]
+ encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: JSON)
+ assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"])
end
def test_legacy_json_signed_cookie_is_read_and_transparently_upgraded_by_signed_json_hybrid_jar_if_both_secret_token_and_secret_key_base_are_set
@request.env["action_dispatch.cookies_serializer"] = :hybrid
@request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
- @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate(45)
@@ -833,28 +874,23 @@ class CookiesTest < ActionController::TestCase
def test_legacy_json_signed_cookie_is_read_and_transparently_encrypted_by_encrypted_hybrid_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
@request.env["action_dispatch.cookies_serializer"] = :hybrid
@request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
- @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
- @request.env["action_dispatch.encrypted_cookie_salt"] = "4433796b79d99a7735553e316522acee"
- @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "00646eb40062e1b1deff205a27cd30f9"
- legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate('bar')
+ legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate("bar")
@request.headers["Cookie"] = "foo=#{legacy_value}"
get :get_encrypted_cookie
- assert_equal 'bar', @controller.send(:cookies).encrypted[:foo]
+ assert_equal "bar", @controller.send(:cookies).encrypted[:foo]
- key_generator = @request.env["action_dispatch.key_generator"]
- secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_cookie_salt"])
- sign_secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"])
- encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: JSON)
- assert_equal 'bar', encryptor.decrypt_and_verify(@response.cookies["foo"])
+ salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"]
+ secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len("aes-256-gcm")]
+ encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: JSON)
+ assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"])
end
def test_legacy_marshal_signed_cookie_is_read_and_transparently_upgraded_by_signed_json_hybrid_jar_if_both_secret_token_and_secret_key_base_are_set
@request.env["action_dispatch.cookies_serializer"] = :hybrid
@request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
- @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate(45)
@@ -871,45 +907,170 @@ class CookiesTest < ActionController::TestCase
def test_legacy_marshal_signed_cookie_is_read_and_transparently_encrypted_by_encrypted_hybrid_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
@request.env["action_dispatch.cookies_serializer"] = :hybrid
+
+ @request.env["action_dispatch.use_authenticated_cookie_encryption"] = true
@request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
@request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
- @request.env["action_dispatch.encrypted_cookie_salt"] = "4433796b79d99a7735553e316522acee"
- @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "00646eb40062e1b1deff205a27cd30f9"
- legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate('bar')
+ legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate("bar")
@request.headers["Cookie"] = "foo=#{legacy_value}"
get :get_encrypted_cookie
- assert_equal 'bar', @controller.send(:cookies).encrypted[:foo]
+ assert_equal "bar", @controller.send(:cookies).encrypted[:foo]
- key_generator = @request.env["action_dispatch.key_generator"]
- secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_cookie_salt"])
- sign_secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"])
- encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: JSON)
- assert_equal 'bar', encryptor.decrypt_and_verify(@response.cookies["foo"])
+ salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"]
+ secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len("aes-256-gcm")]
+ encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: JSON)
+ assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"])
end
def test_legacy_signed_cookie_is_treated_as_nil_by_signed_cookie_jar_if_tampered
@request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
- @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
@request.headers["Cookie"] = "user_id=45"
get :get_signed_cookie
- 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
@request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
- @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
@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_use_authenticated_cookie_encryption_uses_legacy_hmac_aes_cbc_encryption_when_not_enabled
+ @request.env["action_dispatch.use_authenticated_cookie_encryption"] = nil
+
+ key_generator = @request.env["action_dispatch.key_generator"]
+ encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"]
+ encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"]
+ secret = key_generator.generate_key(encrypted_cookie_salt, ActiveSupport::MessageEncryptor.key_len("aes-256-cbc"))
+ sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt)
+ encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, cipher: "aes-256-cbc", digest: "SHA1", serializer: Marshal)
+
+ get :set_encrypted_cookie
+
+ cookies = @controller.send :cookies
+ assert_not_equal "bar", cookies[:foo]
+ assert_equal "bar", cookies.encrypted[:foo]
+ assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"])
+ end
+
+ def test_rotating_signed_cookies_digest
+ @request.env["action_dispatch.signed_cookie_digest"] = "SHA256"
+ @request.env["action_dispatch.cookies_rotations"].rotate :signed, digest: "SHA1"
+
+ key_generator = @request.env["action_dispatch.key_generator"]
+
+ old_secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"])
+ old_value = ActiveSupport::MessageVerifier.new(old_secret).generate(45)
+
+ @request.headers["Cookie"] = "user_id=#{old_value}"
+ get :get_signed_cookie
+
+ assert_equal 45, @controller.send(:cookies).signed[:user_id]
+
+ secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"])
+ verifier = ActiveSupport::MessageVerifier.new(secret, digest: "SHA256")
+ assert_equal 45, verifier.verify(@response.cookies["user_id"])
+ end
+
+ def test_legacy_hmac_aes_cbc_encrypted_marshal_cookie_is_upgraded_to_authenticated_encrypted_cookie
+ key_generator = @request.env["action_dispatch.key_generator"]
+ encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"]
+ encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"]
+ secret = key_generator.generate_key(encrypted_cookie_salt, ActiveSupport::MessageEncryptor.key_len("aes-256-cbc"))
+ sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt)
+ marshal_value = ActiveSupport::MessageEncryptor.new(secret, sign_secret, cipher: "aes-256-cbc", serializer: Marshal).encrypt_and_sign("bar")
+
+ @request.headers["Cookie"] = "foo=#{marshal_value}"
+
+ get :get_encrypted_cookie
+
+ cookies = @controller.send :cookies
+ assert_not_equal "bar", cookies[:foo]
+ assert_equal "bar", cookies.encrypted[:foo]
+
+ aead_salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"]
+ aead_secret = key_generator.generate_key(aead_salt, ActiveSupport::MessageEncryptor.key_len("aes-256-gcm"))
+ aead_encryptor = ActiveSupport::MessageEncryptor.new(aead_secret, cipher: "aes-256-gcm", serializer: Marshal)
+
+ assert_equal "bar", aead_encryptor.decrypt_and_verify(@response.cookies["foo"])
+ end
+
+ def test_legacy_hmac_aes_cbc_encrypted_json_cookie_is_upgraded_to_authenticated_encrypted_cookie
+ @request.env["action_dispatch.cookies_serializer"] = :json
+
+ key_generator = @request.env["action_dispatch.key_generator"]
+ encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"]
+ encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"]
+ secret = key_generator.generate_key(encrypted_cookie_salt, ActiveSupport::MessageEncryptor.key_len("aes-256-cbc"))
+ sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt)
+ marshal_value = ActiveSupport::MessageEncryptor.new(secret, sign_secret, cipher: "aes-256-cbc", serializer: JSON).encrypt_and_sign("bar")
+
+ @request.headers["Cookie"] = "foo=#{marshal_value}"
+
+ get :get_encrypted_cookie
+
+ cookies = @controller.send :cookies
+ assert_not_equal "bar", cookies[:foo]
+ assert_equal "bar", cookies.encrypted[:foo]
+
+ aead_salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"]
+ aead_secret = key_generator.generate_key(aead_salt)[0, ActiveSupport::MessageEncryptor.key_len("aes-256-gcm")]
+ aead_encryptor = ActiveSupport::MessageEncryptor.new(aead_secret, cipher: "aes-256-gcm", serializer: JSON)
+
+ assert_equal "bar", aead_encryptor.decrypt_and_verify(@response.cookies["foo"])
+ end
+
+ def test_legacy_hmac_aes_cbc_encrypted_cookie_using_64_byte_key_is_upgraded_to_authenticated_encrypted_cookie
+ @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
+ @request.env["action_dispatch.encrypted_cookie_salt"] = "b3c631c314c0bbca50c1b2843150fe33"
+ @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "b3c631c314c0bbca50c1b2843150fe33"
+
+ # Cookie generated with 64 bytes secret
+ message = ["566d4e75536d686e633246564e6b493062557079626c566d51574d30515430394c53315665564a694e4563786555744f57537454576b396a5a31566a626e52525054303d2d2d34663234333330623130623261306163363562316266323335396164666364613564643134623131"].pack("H*")
+ @request.headers["Cookie"] = "foo=#{message}"
+
+ get :get_encrypted_cookie
+
+ cookies = @controller.send :cookies
+ assert_not_equal "bar", cookies[:foo]
+ assert_equal "bar", cookies.encrypted[:foo]
+
+ salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"]
+ secret = @request.env["action_dispatch.key_generator"].generate_key(salt, ActiveSupport::MessageEncryptor.key_len("aes-256-gcm"))
+ encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: Marshal)
+
+ assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"])
+ end
+
+ def test_encrypted_cookie_rotating_secret
+ secret = "b3c631c314c0bbca50c1b2843150fe33"
+
+ @request.env["action_dispatch.encrypted_cookie_cipher"] = "aes-256-gcm"
+ @request.env["action_dispatch.cookies_rotations"].rotate :encrypted, secret
+
+ key_len = ActiveSupport::MessageEncryptor.key_len("aes-256-gcm")
+
+ old_message = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: Marshal).encrypt_and_sign(45)
+
+ @request.headers["Cookie"] = "foo=#{::Rack::Utils.escape old_message}"
+
+ get :get_encrypted_cookie
+ assert_equal 45, @controller.send(:cookies).encrypted[:foo]
+
+ key_generator = @request.env["action_dispatch.key_generator"]
+ secret = key_generator.generate_key(@request.env["action_dispatch.authenticated_encrypted_cookie_salt"], key_len)
+ encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: Marshal)
+ assert_equal 45, encryptor.decrypt_and_verify(@response.cookies["foo"])
end
def test_cookie_with_all_domain_option
@@ -968,7 +1129,7 @@ class CookiesTest < ActionController::TestCase
end
def test_deleting_cookie_with_all_domain_option
- request.cookies[:user_name] = 'Joe'
+ request.cookies[:user_name] = "Joe"
get :delete_cookie_with_domain
assert_response :success
assert_cookie_header "user_name=; domain=.nextangle.com; path=/; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"
@@ -1002,7 +1163,7 @@ class CookiesTest < ActionController::TestCase
end
def test_deleting_cookie_with_all_domain_option_and_tld_length
- request.cookies[:user_name] = 'Joe'
+ request.cookies[:user_name] = "Joe"
get :delete_cookie_with_domain_and_tld
assert_response :success
assert_cookie_header "user_name=; domain=.nextangle.com; path=/; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"
@@ -1031,7 +1192,7 @@ class CookiesTest < ActionController::TestCase
def test_deletings_cookie_with_several_preset_domains_using_one_of_these_domains
@request.host = "example2.com"
- request.cookies[:user_name] = 'Joe'
+ request.cookies[:user_name] = "Joe"
get :delete_cookie_with_domains
assert_response :success
assert_cookie_header "user_name=; domain=example2.com; path=/; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"
@@ -1039,7 +1200,7 @@ class CookiesTest < ActionController::TestCase
def test_deletings_cookie_with_several_preset_domains_using_other_domain
@request.host = "other-domain.com"
- request.cookies[:user_name] = 'Joe'
+ request.cookies[:user_name] = "Joe"
get :delete_cookie_with_domains
assert_response :success
assert_cookie_header "user_name=; path=/; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"
@@ -1048,20 +1209,20 @@ class CookiesTest < ActionController::TestCase
def test_cookies_hash_is_indifferent_access
get :symbol_key
assert_equal "david", cookies[:user_name]
- assert_equal "david", cookies['user_name']
+ assert_equal "david", cookies["user_name"]
get :string_key
assert_equal "dhh", cookies[:user_name]
- assert_equal "dhh", cookies['user_name']
+ assert_equal "dhh", cookies["user_name"]
end
def test_setting_request_cookies_is_indifferent_access
cookies.clear
cookies[:user_name] = "andrew"
get :string_key_mock
- assert_equal "david", cookies['user_name']
+ assert_equal "david", cookies["user_name"]
cookies.clear
- cookies['user_name'] = "andrew"
+ cookies["user_name"] = "andrew"
get :symbol_key_mock
assert_equal "david", cookies[:user_name]
end
@@ -1072,11 +1233,11 @@ class CookiesTest < ActionController::TestCase
assert_equal "david", cookies[:user_name]
get :noop
- assert_nil @response.headers["Set-Cookie"]
+ assert_not_includes @response.headers, "Set-Cookie"
assert_equal "david", cookies[:user_name]
get :noop
- assert_nil @response.headers["Set-Cookie"]
+ assert_not_includes @response.headers, "Set-Cookie"
assert_equal "david", cookies[:user_name]
end
@@ -1093,57 +1254,212 @@ class CookiesTest < ActionController::TestCase
end
def test_can_set_http_cookie_header
- @request.env['HTTP_COOKIE'] = 'user_name=david'
+ @request.env["HTTP_COOKIE"] = "user_name=david"
get :noop
- assert_equal 'david', cookies['user_name']
- assert_equal 'david', cookies[:user_name]
+ assert_equal "david", cookies["user_name"]
+ assert_equal "david", cookies[:user_name]
get :noop
- assert_equal 'david', cookies['user_name']
- assert_equal 'david', cookies[:user_name]
+ assert_equal "david", cookies["user_name"]
+ assert_equal "david", cookies[:user_name]
- @request.env['HTTP_COOKIE'] = 'user_name=andrew'
+ @request.env["HTTP_COOKIE"] = "user_name=andrew"
get :noop
- assert_equal 'andrew', cookies['user_name']
- assert_equal 'andrew', cookies[:user_name]
+ assert_equal "andrew", cookies["user_name"]
+ assert_equal "andrew", cookies[:user_name]
end
def test_can_set_request_cookies
- @request.cookies['user_name'] = 'david'
+ @request.cookies["user_name"] = "david"
get :noop
- assert_equal 'david', cookies['user_name']
- assert_equal 'david', cookies[:user_name]
+ assert_equal "david", cookies["user_name"]
+ assert_equal "david", cookies[:user_name]
get :noop
- assert_equal 'david', cookies['user_name']
- assert_equal 'david', cookies[:user_name]
+ assert_equal "david", cookies["user_name"]
+ assert_equal "david", cookies[:user_name]
- @request.cookies[:user_name] = 'andrew'
+ @request.cookies[:user_name] = "andrew"
get :noop
- assert_equal 'andrew', cookies['user_name']
- assert_equal 'andrew', cookies[:user_name]
+ assert_equal "andrew", cookies["user_name"]
+ assert_equal "andrew", cookies[:user_name]
end
def test_cookies_precedence_over_http_cookie
- @request.env['HTTP_COOKIE'] = 'user_name=andrew'
+ @request.env["HTTP_COOKIE"] = "user_name=andrew"
get :authenticate
- assert_equal 'david', cookies['user_name']
- assert_equal 'david', cookies[:user_name]
+ assert_equal "david", cookies["user_name"]
+ assert_equal "david", cookies[:user_name]
get :noop
- assert_equal 'david', cookies['user_name']
- assert_equal 'david', cookies[:user_name]
+ assert_equal "david", cookies["user_name"]
+ assert_equal "david", cookies[:user_name]
end
def test_cookies_precedence_over_request_cookies
- @request.cookies['user_name'] = 'andrew'
+ @request.cookies["user_name"] = "andrew"
get :authenticate
- assert_equal 'david', cookies['user_name']
- assert_equal 'david', cookies[:user_name]
+ assert_equal "david", cookies["user_name"]
+ assert_equal "david", cookies[:user_name]
get :noop
- assert_equal 'david', cookies['user_name']
- assert_equal 'david', cookies[:user_name]
+ assert_equal "david", cookies["user_name"]
+ 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
+
+ def test_signed_cookie_with_expires_set_relatively
+ request.env["action_dispatch.use_cookies_with_metadata"] = true
+
+ cookies.signed[:user_name] = { value: "assain", expires: 2.hours }
+
+ travel 1.hour
+ assert_equal "assain", cookies.signed[:user_name]
+
+ travel 2.hours
+ assert_nil cookies.signed[:user_name]
+ end
+
+ def test_encrypted_cookie_with_expires_set_relatively
+ request.env["action_dispatch.use_cookies_with_metadata"] = true
+
+ cookies.encrypted[:user_name] = { value: "assain", expires: 2.hours }
+
+ travel 1.hour
+ assert_equal "assain", cookies.encrypted[:user_name]
+
+ travel 2.hours
+ assert_nil cookies.encrypted[:user_name]
+ end
+
+ def test_vanilla_cookie_with_expires_set_relatively
+ travel_to Time.utc(2017, 8, 15) do
+ get :cookie_expires_in_two_hours
+ assert_cookie_header "user_name=assain; path=/; expires=Tue, 15 Aug 2017 02:00:00 -0000"
+ end
+ end
+
+ def test_purpose_metadata_for_encrypted_cookies
+ get :encrypted_discount_and_user_id_cookie
+
+ cookies[:discount_percentage] = cookies[:user_id]
+ assert_equal 50, cookies.encrypted[:discount_percentage]
+
+ request.env["action_dispatch.use_cookies_with_metadata"] = true
+
+ get :encrypted_discount_and_user_id_cookie
+
+ cookies[:discount_percentage] = cookies[:user_id]
+ assert_nil cookies.encrypted[:discount_percentage]
+ end
+
+ def test_purpose_metadata_for_signed_cookies
+ get :signed_discount_and_user_id_cookie
+
+ cookies[:discount_percentage] = cookies[:user_id]
+ assert_equal 50, cookies.signed[:discount_percentage]
+
+ request.env["action_dispatch.use_cookies_with_metadata"] = true
+
+ get :signed_discount_and_user_id_cookie
+
+ cookies[:discount_percentage] = cookies[:user_id]
+ assert_nil cookies.signed[:discount_percentage]
+ end
+
+ def test_switch_off_metadata_for_encrypted_cookies_if_config_is_false
+ request.env["action_dispatch.use_cookies_with_metadata"] = false
+
+ get :encrypted_discount_and_user_id_cookie
+
+ travel 2.hours
+ assert_equal 50, cookies.encrypted[:user_id]
+
+ cookies[:discount_percentage] = cookies[:user_id]
+ assert_not_equal 10, cookies.encrypted[:discount_percentage]
+ assert_equal 50, cookies.encrypted[:discount_percentage]
+ end
+
+ def test_switch_off_metadata_for_signed_cookies_if_config_is_false
+ request.env["action_dispatch.use_cookies_with_metadata"] = false
+
+ get :signed_discount_and_user_id_cookie
+
+ travel 2.hours
+ assert_equal 50, cookies.signed[:user_id]
+
+ cookies[:discount_percentage] = cookies[:user_id]
+ assert_not_equal 10, cookies.signed[:discount_percentage]
+ assert_equal 50, cookies.signed[:discount_percentage]
+ end
+
+ def test_read_rails_5_2_stable_encrypted_cookies_if_config_is_false
+ request.env["action_dispatch.use_cookies_with_metadata"] = false
+
+ get :rails_5_2_stable_encrypted_cookie_with_authenticated_encryption_flag_on
+
+ assert_equal "5-2-Stable Chocolate Cookies", cookies.encrypted[:favorite]
+
+ travel 1001.years do
+ assert_nil cookies.encrypted[:favorite]
+ end
+
+ get :rails_5_2_stable_encrypted_cookie_with_authenticated_encryption_flag_off
+
+ assert_equal "5-2-Stable Chocolate Cookies", cookies.encrypted[:favorite]
+ end
+
+ def test_read_rails_5_2_stable_signed_cookies_if_config_is_false
+ request.env["action_dispatch.use_cookies_with_metadata"] = false
+
+ get :rails_5_2_stable_signed_cookie_with_authenticated_encryption_flag_on
+
+ assert_equal "5-2-Stable Choco Chip Cookie", cookies.signed[:favorite]
+
+ travel 1001.years do
+ assert_nil cookies.signed[:favorite]
+ end
+
+ get :rails_5_2_stable_signed_cookie_with_authenticated_encryption_flag_off
+
+ assert_equal "5-2-Stable Choco Chip Cookie", cookies.signed[:favorite]
+ end
+
+ def test_read_rails_5_2_stable_encrypted_cookies_if_use_metadata_config_is_true
+ request.env["action_dispatch.use_cookies_with_metadata"] = true
+
+ get :rails_5_2_stable_encrypted_cookie_with_authenticated_encryption_flag_on
+
+ assert_equal "5-2-Stable Chocolate Cookies", cookies.encrypted[:favorite]
+
+ travel 1001.years do
+ assert_nil cookies.encrypted[:favorite]
+ end
+
+ get :rails_5_2_stable_encrypted_cookie_with_authenticated_encryption_flag_off
+
+ assert_equal "5-2-Stable Chocolate Cookies", cookies.encrypted[:favorite]
+ end
+
+ def test_read_rails_5_2_stable_signed_cookies_if_use_metadata_config_is_true
+ request.env["action_dispatch.use_cookies_with_metadata"] = true
+
+ get :rails_5_2_stable_signed_cookie_with_authenticated_encryption_flag_on
+
+ assert_equal "5-2-Stable Choco Chip Cookie", cookies.signed[:favorite]
+
+ travel 1001.years do
+ assert_nil cookies.signed[:favorite]
+ end
+
+ get :rails_5_2_stable_signed_cookie_with_authenticated_encryption_flag_off
+
+ assert_equal "5-2-Stable Choco Chip Cookie", cookies.signed[:favorite]
end
private
diff --git a/actionpack/test/dispatch/debug_exceptions_test.rb b/actionpack/test/dispatch/debug_exceptions_test.rb
index a867aee7ec..37399cfd07 100644
--- a/actionpack/test/dispatch/debug_exceptions_test.rb
+++ b/actionpack/test/dispatch/debug_exceptions_test.rb
@@ -1,6 +1,9 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class DebugExceptionsTest < ActionDispatch::IntegrationTest
+ InterceptedErrorInstance = StandardError.new
class Boomer
attr_accessor :closed
@@ -20,148 +23,164 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
end
def method_that_raises
- raise StandardError.new 'error in framework'
+ raise StandardError.new "error in framework"
+ end
+
+ def raise_nested_exceptions
+ begin
+ raise "First error"
+ rescue
+ begin
+ raise "Second error"
+ rescue
+ raise "Third error"
+ end
+ end
end
def call(env)
- env['action_dispatch.show_detailed_exceptions'] = @detailed
+ env["action_dispatch.show_detailed_exceptions"] = @detailed
req = ActionDispatch::Request.new(env)
case req.path
- when "/pass"
+ when %r{/pass}
[404, { "X-Cascade" => "pass" }, self]
- when "/not_found"
+ when %r{/not_found}
raise AbstractController::ActionNotFound
- when "/runtime_error"
+ when %r{/runtime_error}
raise RuntimeError
- when "/method_not_allowed"
+ when %r{/method_not_allowed}
raise ActionController::MethodNotAllowed
- when "/unknown_http_method"
+ when %r{/intercepted_error}
+ raise InterceptedErrorInstance
+ when %r{/unknown_http_method}
raise ActionController::UnknownHttpMethod
- when "/not_implemented"
+ when %r{/not_implemented}
raise ActionController::NotImplemented
- when "/unprocessable_entity"
+ when %r{/unprocessable_entity}
raise ActionController::InvalidAuthenticityToken
- when "/not_found_original_exception"
- raise ActionView::Template::Error.new('template', AbstractController::ActionNotFound.new)
- when "/missing_template"
- raise ActionView::MissingTemplate.new(%w(foo), 'foo/index', %w(foo), false, 'mailer')
- when "/bad_request"
+ when %r{/not_found_original_exception}
+ begin
+ raise AbstractController::ActionNotFound.new
+ rescue
+ raise ActionView::Template::Error.new("template")
+ end
+ when %r{/missing_template}
+ raise ActionView::MissingTemplate.new(%w(foo), "foo/index", %w(foo), false, "mailer")
+ when %r{/bad_request}
raise ActionController::BadRequest
- when "/missing_keys"
+ when %r{/missing_keys}
raise ActionController::UrlGenerationError, "No route matches"
- when "/parameter_missing"
+ when %r{/parameter_missing}
raise ActionController::ParameterMissing, :missing_param_key
- when "/original_syntax_error"
- eval 'broke_syntax =' # `eval` need for raise native SyntaxError at runtime
- when "/syntax_error_into_view"
+ when %r{/original_syntax_error}
+ eval "broke_syntax =" # `eval` need for raise native SyntaxError at runtime
+ when %r{/syntax_error_into_view}
begin
- eval 'broke_syntax ='
- rescue Exception => e
+ eval "broke_syntax ="
+ rescue Exception
template = ActionView::Template.new(File.read(__FILE__),
__FILE__,
ActionView::Template::Handlers::Raw.new,
{})
- raise ActionView::Template::Error.new(template, e)
+ raise ActionView::Template::Error.new(template)
end
- when "/framework_raises"
+ when %r{/framework_raises}
method_that_raises
+ when %r{/nested_exceptions}
+ raise_nested_exceptions
else
raise "puke!"
end
end
end
- def setup
- app = ActiveSupport::OrderedOptions.new
- app.config = ActiveSupport::OrderedOptions.new
- app.config.assets = ActiveSupport::OrderedOptions.new
- app.config.assets.prefix = '/sprockets'
- Rails.stubs(:application).returns(app)
- end
-
+ Interceptor = proc { |request, exception| request.set_header("int", exception) }
+ BadInterceptor = proc { |request, exception| raise "bad" }
RoutesApp = Struct.new(:routes).new(SharedTestRoutes)
ProductionApp = ActionDispatch::DebugExceptions.new(Boomer.new(false), RoutesApp)
DevelopmentApp = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp)
+ InterceptedApp = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :default, [Interceptor])
+ BadInterceptedApp = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :default, [BadInterceptor])
- test 'skip diagnosis if not showing detailed exceptions' do
+ test "skip diagnosis if not showing detailed exceptions" do
@app = ProductionApp
assert_raise RuntimeError do
- get "/", headers: { 'action_dispatch.show_exceptions' => true }
+ get "/", headers: { "action_dispatch.show_exceptions" => true }
end
end
- test 'skip diagnosis if not showing exceptions' do
+ test "skip diagnosis if not showing exceptions" do
@app = DevelopmentApp
assert_raise RuntimeError do
- get "/", headers: { 'action_dispatch.show_exceptions' => false }
+ get "/", headers: { "action_dispatch.show_exceptions" => false }
end
end
- test 'raise an exception on cascade pass' do
+ test "raise an exception on cascade pass" do
@app = ProductionApp
assert_raise ActionController::RoutingError do
- get "/pass", headers: { 'action_dispatch.show_exceptions' => true }
+ get "/pass", headers: { "action_dispatch.show_exceptions" => true }
end
end
- test 'closes the response body on cascade pass' do
+ test "closes the response body on cascade pass" do
boomer = Boomer.new(false)
@app = ActionDispatch::DebugExceptions.new(boomer)
assert_raise ActionController::RoutingError do
- get "/pass", headers: { 'action_dispatch.show_exceptions' => true }
+ get "/pass", headers: { "action_dispatch.show_exceptions" => true }
end
assert boomer.closed, "Expected to close the response body"
end
- test 'displays routes in a table when a RoutingError occurs' do
+ test "displays routes in a table when a RoutingError occurs" do
@app = DevelopmentApp
- get "/pass", headers: { 'action_dispatch.show_exceptions' => true }
+ get "/pass", headers: { "action_dispatch.show_exceptions" => true }
routing_table = body[/route_table.*<.table>/m]
- assert_match '/:controller(/:action)(.:format)', routing_table
- assert_match ':controller#:action', routing_table
- assert_no_match '&lt;|&gt;', routing_table, "there should not be escaped html in the output"
+ assert_match "/:controller(/:action)(.:format)", routing_table
+ assert_match ":controller#:action", routing_table
+ assert_no_match "&lt;|&gt;", routing_table, "there should not be escaped html in the output"
end
- test 'displays request and response info when a RoutingError occurs' do
+ test "displays request and response info when a RoutingError occurs" do
@app = DevelopmentApp
- get "/pass", headers: { 'action_dispatch.show_exceptions' => true }
+ get "/pass", headers: { "action_dispatch.show_exceptions" => true }
- assert_select 'h2', /Request/
- assert_select 'h2', /Response/
+ assert_select "h2", /Request/
+ assert_select "h2", /Response/
end
test "rescue with diagnostics message" do
@app = DevelopmentApp
- get "/", headers: { 'action_dispatch.show_exceptions' => true }
+ get "/", headers: { "action_dispatch.show_exceptions" => true }
assert_response 500
assert_match(/puke/, body)
- get "/not_found", headers: { 'action_dispatch.show_exceptions' => true }
+ get "/not_found", headers: { "action_dispatch.show_exceptions" => true }
assert_response 404
assert_match(/#{AbstractController::ActionNotFound.name}/, body)
- get "/method_not_allowed", headers: { 'action_dispatch.show_exceptions' => true }
+ get "/method_not_allowed", headers: { "action_dispatch.show_exceptions" => true }
assert_response 405
assert_match(/ActionController::MethodNotAllowed/, body)
- get "/unknown_http_method", headers: { 'action_dispatch.show_exceptions' => true }
+ get "/unknown_http_method", headers: { "action_dispatch.show_exceptions" => true }
assert_response 405
assert_match(/ActionController::UnknownHttpMethod/, body)
- get "/bad_request", headers: { 'action_dispatch.show_exceptions' => true }
+ get "/bad_request", headers: { "action_dispatch.show_exceptions" => true }
assert_response 400
assert_match(/ActionController::BadRequest/, body)
- get "/parameter_missing", headers: { 'action_dispatch.show_exceptions' => true }
+ get "/parameter_missing", headers: { "action_dispatch.show_exceptions" => true }
assert_response 400
assert_match(/ActionController::ParameterMissing/, body)
end
test "rescue with text error for xhr request" do
@app = DevelopmentApp
- xhr_request_env = {'action_dispatch.show_exceptions' => true, 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest'}
+ xhr_request_env = { "action_dispatch.show_exceptions" => true, "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest" }
get "/", headers: xhr_request_env
assert_response 500
@@ -170,6 +189,14 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
assert_equal "text/plain", response.content_type
assert_match(/RuntimeError\npuke/, body)
+ Rails.stub :root, Pathname.new(".") do
+ get "/", headers: xhr_request_env
+
+ assert_response 500
+ assert_match "Extracted source (around line #", body
+ assert_select "pre", { count: 0 }, body
+ end
+
get "/not_found", headers: xhr_request_env
assert_response 404
assert_no_match(/<body>/, body)
@@ -201,11 +228,91 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
assert_match(/ActionController::ParameterMissing/, body)
end
+ test "rescue with JSON error for JSON API request" do
+ @app = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :api)
+
+ get "/", headers: { "action_dispatch.show_exceptions" => true }, as: :json
+ assert_response 500
+ assert_no_match(/<header>/, body)
+ assert_no_match(/<body>/, body)
+ assert_equal "application/json", response.content_type
+ assert_match(/RuntimeError: puke/, body)
+
+ get "/not_found", headers: { "action_dispatch.show_exceptions" => true }, as: :json
+ assert_response 404
+ assert_no_match(/<body>/, body)
+ assert_equal "application/json", response.content_type
+ assert_match(/#{AbstractController::ActionNotFound.name}/, body)
+
+ get "/method_not_allowed", headers: { "action_dispatch.show_exceptions" => true }, as: :json
+ assert_response 405
+ assert_no_match(/<body>/, body)
+ assert_equal "application/json", response.content_type
+ assert_match(/ActionController::MethodNotAllowed/, body)
+
+ get "/unknown_http_method", headers: { "action_dispatch.show_exceptions" => true }, as: :json
+ assert_response 405
+ assert_no_match(/<body>/, body)
+ assert_equal "application/json", response.content_type
+ assert_match(/ActionController::UnknownHttpMethod/, body)
+
+ get "/bad_request", headers: { "action_dispatch.show_exceptions" => true }, as: :json
+ assert_response 400
+ assert_no_match(/<body>/, body)
+ assert_equal "application/json", response.content_type
+ assert_match(/ActionController::BadRequest/, body)
+
+ get "/parameter_missing", headers: { "action_dispatch.show_exceptions" => true }, as: :json
+ assert_response 400
+ assert_no_match(/<body>/, body)
+ assert_equal "application/json", response.content_type
+ assert_match(/ActionController::ParameterMissing/, body)
+ end
+
+ test "rescue with HTML format for HTML API request" do
+ @app = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :api)
+
+ get "/index.html", headers: { "action_dispatch.show_exceptions" => true }
+ assert_response 500
+ assert_match(/<header>/, body)
+ assert_match(/<body>/, body)
+ assert_equal "text/html", response.content_type
+ assert_match(/puke/, body)
+ end
+
+ test "rescue with XML format for XML API requests" do
+ @app = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :api)
+
+ get "/index.xml", headers: { "action_dispatch.show_exceptions" => true }
+ assert_response 500
+ assert_equal "application/xml", response.content_type
+ assert_match(/RuntimeError: puke/, body)
+ end
+
+ test "rescue with JSON format as fallback if API request format is not supported" do
+ begin
+ Mime::Type.register "text/wibble", :wibble
+
+ ActionDispatch::IntegrationTest.register_encoder(:wibble,
+ param_encoder: -> params { params })
+
+ @app = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :api)
+
+ get "/index", headers: { "action_dispatch.show_exceptions" => true }, as: :wibble
+ assert_response 500
+ assert_equal "application/json", response.content_type
+ assert_match(/RuntimeError: puke/, body)
+
+ ensure
+ Mime::Type.unregister :wibble
+ end
+ end
+
test "does not show filtered parameters" do
@app = DevelopmentApp
- get "/", params: { "foo"=>"bar" }, headers: { 'action_dispatch.show_exceptions' => true,
- 'action_dispatch.parameter_filter' => [:foo] }
+ get "/", params: { "foo" => "bar" }, headers: { "action_dispatch.show_exceptions" => true,
+ "action_dispatch.parameter_filter" => [:foo] }
assert_response 500
assert_match("&quot;foo&quot;=&gt;&quot;[FILTERED]&quot;", body)
end
@@ -213,7 +320,7 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
test "show registered original exception for wrapped exceptions" do
@app = DevelopmentApp
- get "/not_found_original_exception", headers: { 'action_dispatch.show_exceptions' => true }
+ get "/not_found_original_exception", headers: { "action_dispatch.show_exceptions" => true }
assert_response 404
assert_match(/AbstractController::ActionNotFound/, body)
end
@@ -221,7 +328,7 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
test "named urls missing keys raise 500 level error" do
@app = DevelopmentApp
- get "/missing_keys", headers: { 'action_dispatch.show_exceptions' => true }
+ get "/missing_keys", headers: { "action_dispatch.show_exceptions" => true }
assert_response 500
assert_match(/ActionController::UrlGenerationError/, body)
@@ -230,11 +337,11 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
test "show the controller name in the diagnostics template when controller name is present" do
@app = DevelopmentApp
get("/runtime_error", headers: {
- 'action_dispatch.show_exceptions' => true,
- 'action_dispatch.request.parameters' => {
- 'action' => 'show',
- 'id' => 'unknown',
- 'controller' => 'featured_tile'
+ "action_dispatch.show_exceptions" => true,
+ "action_dispatch.request.parameters" => {
+ "action" => "show",
+ "id" => "unknown",
+ "controller" => "featured_tile"
}
})
assert_response 500
@@ -245,71 +352,114 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
@app = DevelopmentApp
params = {
- 'id' => 'unknown',
- 'someparam' => {
- 'foo' => 'bar',
- 'abc' => 'goo'
+ "id" => "unknown",
+ "someparam" => {
+ "foo" => "bar",
+ "abc" => "goo"
}
}
get("/runtime_error", headers: {
- 'action_dispatch.show_exceptions' => true,
- 'action_dispatch.request.parameters' => {
- 'action' => 'show',
- 'controller' => 'featured_tile'
+ "action_dispatch.show_exceptions" => true,
+ "action_dispatch.request.parameters" => {
+ "action" => "show",
+ "controller" => "featured_tile"
}.merge(params)
})
assert_response 500
- assert_includes(body, CGI.escapeHTML(PP.pp(params, "", 200)))
+ assert_includes(body, CGI.escapeHTML(PP.pp(params, +"", 200)))
end
test "sets the HTTP charset parameter" do
@app = DevelopmentApp
- get "/", headers: { 'action_dispatch.show_exceptions' => true }
+ get "/", headers: { "action_dispatch.show_exceptions" => true }
assert_equal "text/html; charset=utf-8", response.headers["Content-Type"]
end
- test 'uses logger from env' do
+ test "uses logger from env" do
@app = DevelopmentApp
output = StringIO.new
- get "/", headers: { 'action_dispatch.show_exceptions' => true, 'action_dispatch.logger' => Logger.new(output) }
+ get "/", headers: { "action_dispatch.show_exceptions" => true, "action_dispatch.logger" => Logger.new(output) }
assert_match(/puke/, output.rewind && output.read)
end
- test 'uses backtrace cleaner from env' do
+ test "logs only what is necessary" do
+ @app = DevelopmentApp
+ io = StringIO.new
+ logger = ActiveSupport::Logger.new(io)
+
+ _old, ActionView::Base.logger = ActionView::Base.logger, logger
+ begin
+ get "/", headers: { "action_dispatch.show_exceptions" => true, "action_dispatch.logger" => logger }
+ ensure
+ ActionView::Base.logger = _old
+ end
+
+ output = io.rewind && io.read
+ lines = output.lines
+
+ # Other than the first three...
+ assert_equal([" \n", "RuntimeError (puke!):\n", " \n"], lines.slice!(0, 3))
+ lines.each do |line|
+ # .. all the remaining lines should be from the backtrace
+ assert_match(/:\d+:in /, line)
+ end
+ end
+
+ test "logs with non active support loggers" do
+ @app = DevelopmentApp
+ io = StringIO.new
+ logger = Logger.new(io)
+
+ _old, ActionView::Base.logger = ActionView::Base.logger, logger
+ begin
+ assert_nothing_raised do
+ get "/", headers: { "action_dispatch.show_exceptions" => true, "action_dispatch.logger" => logger }
+ end
+ ensure
+ ActionView::Base.logger = _old
+ end
+
+ assert_match(/puke/, io.rewind && io.read)
+ end
+
+ test "uses backtrace cleaner from env" do
@app = DevelopmentApp
- cleaner = stub(:clean => ['passed backtrace cleaner'])
- get "/", headers: { 'action_dispatch.show_exceptions' => true, 'action_dispatch.backtrace_cleaner' => cleaner }
- assert_match(/passed backtrace cleaner/, body)
+ backtrace_cleaner = ActiveSupport::BacktraceCleaner.new
+
+ backtrace_cleaner.stub :clean, ["passed backtrace cleaner"] do
+ get "/", headers: { "action_dispatch.show_exceptions" => true, "action_dispatch.backtrace_cleaner" => backtrace_cleaner }
+ assert_match(/passed backtrace cleaner/, body)
+ end
end
- test 'logs exception backtrace when all lines silenced' do
+ test "logs exception backtrace when all lines silenced" do
output = StringIO.new
backtrace_cleaner = ActiveSupport::BacktraceCleaner.new
backtrace_cleaner.add_silencer { true }
- env = {'action_dispatch.show_exceptions' => true,
- 'action_dispatch.logger' => Logger.new(output),
- 'action_dispatch.backtrace_cleaner' => backtrace_cleaner}
+ env = { "action_dispatch.show_exceptions" => true,
+ "action_dispatch.logger" => Logger.new(output),
+ "action_dispatch.backtrace_cleaner" => backtrace_cleaner }
get "/", headers: env
assert_operator((output.rewind && output.read).lines.count, :>, 10)
end
- test 'display backtrace when error type is SyntaxError' do
+ test "display backtrace when error type is SyntaxError" do
@app = DevelopmentApp
- get '/original_syntax_error', headers: { 'action_dispatch.backtrace_cleaner' => ActiveSupport::BacktraceCleaner.new }
+ get "/original_syntax_error", headers: { "action_dispatch.backtrace_cleaner" => ActiveSupport::BacktraceCleaner.new }
assert_response 500
- assert_select '#Application-Trace' do
- assert_select 'pre code', /syntax error, unexpected/
+ assert_select "#Application-Trace-0" do
+ assert_select "code", /syntax error, unexpected/
end
end
- test 'display backtrace on template missing errors' do
+ test "display backtrace on template missing errors" do
@app = DevelopmentApp
get "/missing_template"
@@ -318,56 +468,108 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
assert_select "#container h2", /^Missing template/
- assert_select '#Application-Trace'
- assert_select '#Framework-Trace'
- assert_select '#Full-Trace'
+ assert_select "#Application-Trace-0"
+ assert_select "#Framework-Trace-0"
+ assert_select "#Full-Trace-0"
- assert_select 'h2', /Request/
+ assert_select "h2", /Request/
end
- test 'display backtrace when error type is SyntaxError wrapped by ActionView::Template::Error' do
+ test "display backtrace when error type is SyntaxError wrapped by ActionView::Template::Error" do
@app = DevelopmentApp
- get '/syntax_error_into_view', headers: { 'action_dispatch.backtrace_cleaner' => ActiveSupport::BacktraceCleaner.new }
+ get "/syntax_error_into_view", headers: { "action_dispatch.backtrace_cleaner" => ActiveSupport::BacktraceCleaner.new }
assert_response 500
- assert_select '#Application-Trace' do
- assert_select 'pre code', /syntax error, unexpected/
+ assert_select "#Application-Trace-0" do
+ assert_select "code", /syntax error, unexpected/
end
end
- test 'debug exceptions app shows user code that caused the error in source view' do
+ test "debug exceptions app shows user code that caused the error in source view" do
@app = DevelopmentApp
- Rails.stubs(:root).returns(Pathname.new('.'))
- cleaner = ActiveSupport::BacktraceCleaner.new.tap do |bc|
- bc.add_silencer { |line| line =~ /method_that_raises/ }
- bc.add_silencer { |line| line !~ %r{test/dispatch/debug_exceptions_test.rb} }
+ Rails.stub :root, Pathname.new(".") do
+ cleaner = ActiveSupport::BacktraceCleaner.new.tap do |bc|
+ bc.add_silencer { |line| line =~ /method_that_raises/ }
+ bc.add_silencer { |line| line !~ %r{test/dispatch/debug_exceptions_test.rb} }
+ end
+
+ get "/framework_raises", headers: { "action_dispatch.backtrace_cleaner" => cleaner }
+
+ # Assert correct error
+ assert_response 500
+ assert_select "h2", /error in framework/
+
+ # assert source view line is the call to method_that_raises
+ assert_select "div.source:not(.hidden)" do
+ assert_select "pre .line.active", /method_that_raises/
+ end
+
+ # assert first source view (hidden) that throws the error
+ assert_select "div.source:first" do
+ assert_select "pre .line.active", /raise StandardError\.new/
+ end
+
+ # assert application trace refers to line that calls method_that_raises is first
+ assert_select "#Application-Trace-0" do
+ assert_select "code a:first", %r{test/dispatch/debug_exceptions_test\.rb:\d+:in `call}
+ end
+
+ # assert framework trace that threw the error is first
+ assert_select "#Framework-Trace-0" do
+ assert_select "code a:first", /method_that_raises/
+ end
end
+ end
- get '/framework_raises', headers: { 'action_dispatch.backtrace_cleaner' => cleaner }
+ test "invoke interceptors before rendering" do
+ @app = InterceptedApp
+ get "/intercepted_error", headers: { "action_dispatch.show_exceptions" => true }
+
+ assert_equal InterceptedErrorInstance, request.get_header("int")
+ end
+
+ test "bad interceptors doesn't debug exceptions" do
+ @app = BadInterceptedApp
+
+ get "/puke", headers: { "action_dispatch.show_exceptions" => true }
- # Assert correct error
assert_response 500
- assert_select 'h2', /error in framework/
+ assert_match(/puke/, body)
+ end
- # assert source view line is the call to method_that_raises
- assert_select 'div.source:not(.hidden)' do
- assert_select 'pre .line.active', /method_that_raises/
- end
+ test "debug exceptions app shows all the nested exceptions in source view" do
+ @app = DevelopmentApp
+ Rails.stub :root, Pathname.new(".") do
+ cleaner = ActiveSupport::BacktraceCleaner.new.tap do |bc|
+ bc.add_silencer { |line| line !~ %r{test/dispatch/debug_exceptions_test.rb} }
+ end
- # assert first source view (hidden) that throws the error
- assert_select 'div.source:first' do
- assert_select 'pre .line.active', /raise StandardError\.new/
- end
+ get "/nested_exceptions", headers: { "action_dispatch.backtrace_cleaner" => cleaner }
- # assert application trace refers to line that calls method_that_raises is first
- assert_select '#Application-Trace' do
- assert_select 'pre code a:first', %r{test/dispatch/debug_exceptions_test\.rb:\d+:in `call}
- end
+ # Assert correct error
+ assert_response 500
+ assert_select "h2", /Third error/
+
+ # assert source view line shows the last error
+ assert_select "div.source:not(.hidden)" do
+ assert_select "pre .line.active", /raise "Third error"/
+ end
+
+ # assert application trace refers to line that raises the last exception
+ assert_select "#Application-Trace-0" do
+ assert_select "code a:first", %r{in `rescue in rescue in raise_nested_exceptions'}
+ end
- # assert framework trace that that threw the error is first
- assert_select '#Framework-Trace' do
- assert_select 'pre code a:first', /method_that_raises/
+ # assert the second application trace refers to the line that raises the second exception
+ assert_select "#Application-Trace-1" do
+ assert_select "code a:first", %r{in `rescue in raise_nested_exceptions'}
+ end
+
+ # assert the third application trace refers to the line that raises the first exception
+ assert_select "#Application-Trace-2" do
+ assert_select "code a:first", %r{in `raise_nested_exceptions'}
+ end
end
end
end
diff --git a/actionpack/test/dispatch/debug_locks_test.rb b/actionpack/test/dispatch/debug_locks_test.rb
new file mode 100644
index 0000000000..d69614bd79
--- /dev/null
+++ b/actionpack/test/dispatch/debug_locks_test.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class DebugLocksTest < ActionDispatch::IntegrationTest
+ setup do
+ build_app
+ end
+
+ def test_render_threads_status
+ thread_ready = Concurrent::CountDownLatch.new
+ test_terminated = Concurrent::CountDownLatch.new
+
+ thread = Thread.new do
+ ActiveSupport::Dependencies.interlock.running do
+ thread_ready.count_down
+ test_terminated.wait
+ end
+ end
+
+ thread_ready.wait
+
+ get "/rails/locks"
+
+ test_terminated.count_down
+
+ assert_match(/Thread.*?Sharing/, @response.body)
+ ensure
+ thread.join
+ end
+
+ private
+ def build_app
+ @app = self.class.build_app do |middleware|
+ middleware.use ActionDispatch::DebugLocks
+ end
+ end
+end
diff --git a/actionpack/test/dispatch/exception_wrapper_test.rb b/actionpack/test/dispatch/exception_wrapper_test.rb
index 7a29a7ff97..668469a01d 100644
--- a/actionpack/test/dispatch/exception_wrapper_test.rb
+++ b/actionpack/test/dispatch/exception_wrapper_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module ActionDispatch
class ExceptionWrapperTest < ActionDispatch::IntegrationTest
@@ -17,96 +19,117 @@ module ActionDispatch
end
setup do
- Rails.stubs(:root).returns(Pathname.new('.'))
-
- cleaner = ActiveSupport::BacktraceCleaner.new
- cleaner.add_silencer { |line| line !~ /^lib/ }
-
- @environment = { 'action_dispatch.backtrace_cleaner' => cleaner }
+ @cleaner = ActiveSupport::BacktraceCleaner.new
+ @cleaner.remove_filters!
+ @cleaner.add_silencer { |line| line !~ /^lib/ }
end
- test '#source_extracts fetches source fragments for every backtrace entry' do
+ test "#source_extracts fetches source fragments for every backtrace entry" do
exception = TestError.new("lib/file.rb:42:in `index'")
- wrapper = ExceptionWrapper.new({}, exception)
+ wrapper = ExceptionWrapper.new(nil, exception)
- wrapper.expects(:source_fragment).with('lib/file.rb', 42).returns('foo')
-
- assert_equal [ code: 'foo', line_number: 42 ], wrapper.source_extracts
+ assert_called_with(wrapper, :source_fragment, ["lib/file.rb", 42], returns: "foo") do
+ assert_equal [ code: "foo", line_number: 42 ], wrapper.source_extracts
+ end
end
- test '#source_extracts works with Windows paths' do
+ test "#source_extracts works with Windows paths" do
exc = TestError.new("c:/path/to/rails/app/controller.rb:27:in 'index':")
- wrapper = ExceptionWrapper.new({}, exc)
- wrapper.expects(:source_fragment).with('c:/path/to/rails/app/controller.rb', 27).returns('nothing')
+ wrapper = ExceptionWrapper.new(nil, exc)
- assert_equal [ code: 'nothing', line_number: 27 ], wrapper.source_extracts
+ assert_called_with(wrapper, :source_fragment, ["c:/path/to/rails/app/controller.rb", 27], returns: "nothing") do
+ assert_equal [ code: "nothing", line_number: 27 ], wrapper.source_extracts
+ end
end
- test '#source_extracts works with non standard backtrace' do
- exc = TestError.new('invalid')
+ test "#source_extracts works with non standard backtrace" do
+ exc = TestError.new("invalid")
- wrapper = ExceptionWrapper.new({}, exc)
- wrapper.expects(:source_fragment).with('invalid', 0).returns('nothing')
+ wrapper = ExceptionWrapper.new(nil, exc)
- assert_equal [ code: 'nothing', line_number: 0 ], wrapper.source_extracts
+ assert_called_with(wrapper, :source_fragment, ["invalid", 0], returns: "nothing") do
+ assert_equal [ code: "nothing", line_number: 0 ], wrapper.source_extracts
+ end
end
- test '#application_trace returns traces only from the application' do
+ test "#application_trace returns traces only from the application" do
exception = TestError.new(caller.prepend("lib/file.rb:42:in `index'"))
- wrapper = ExceptionWrapper.new(@environment, exception)
+ wrapper = ExceptionWrapper.new(@cleaner, exception)
assert_equal [ "lib/file.rb:42:in `index'" ], wrapper.application_trace
end
- test '#application_trace cannot be nil' do
- nil_backtrace_wrapper = ExceptionWrapper.new(@environment, BadlyDefinedError.new)
- nil_cleaner_wrapper = ExceptionWrapper.new({}, BadlyDefinedError.new)
+ test "#status_code returns 400 for Rack::Utils::ParameterTypeError" do
+ exception = Rack::Utils::ParameterTypeError.new
+ wrapper = ExceptionWrapper.new(@cleaner, exception)
+ assert_equal 400, wrapper.status_code
+ end
+
+ test "#application_trace cannot be nil" do
+ nil_backtrace_wrapper = ExceptionWrapper.new(@cleaner, BadlyDefinedError.new)
+ nil_cleaner_wrapper = ExceptionWrapper.new(nil, BadlyDefinedError.new)
assert_equal [], nil_backtrace_wrapper.application_trace
assert_equal [], nil_cleaner_wrapper.application_trace
end
- test '#framework_trace returns traces outside the application' do
+ test "#framework_trace returns traces outside the application" do
exception = TestError.new(caller.prepend("lib/file.rb:42:in `index'"))
- wrapper = ExceptionWrapper.new(@environment, exception)
+ wrapper = ExceptionWrapper.new(@cleaner, exception)
assert_equal caller, wrapper.framework_trace
end
- test '#framework_trace cannot be nil' do
- nil_backtrace_wrapper = ExceptionWrapper.new(@environment, BadlyDefinedError.new)
- nil_cleaner_wrapper = ExceptionWrapper.new({}, BadlyDefinedError.new)
+ test "#framework_trace cannot be nil" do
+ nil_backtrace_wrapper = ExceptionWrapper.new(@cleaner, BadlyDefinedError.new)
+ nil_cleaner_wrapper = ExceptionWrapper.new(nil, BadlyDefinedError.new)
assert_equal [], nil_backtrace_wrapper.framework_trace
assert_equal [], nil_cleaner_wrapper.framework_trace
end
- test '#full_trace returns application and framework traces' do
+ test "#full_trace returns application and framework traces" do
exception = TestError.new(caller.prepend("lib/file.rb:42:in `index'"))
- wrapper = ExceptionWrapper.new(@environment, exception)
+ wrapper = ExceptionWrapper.new(@cleaner, exception)
assert_equal exception.backtrace, wrapper.full_trace
end
- test '#full_trace cannot be nil' do
- nil_backtrace_wrapper = ExceptionWrapper.new(@environment, BadlyDefinedError.new)
- nil_cleaner_wrapper = ExceptionWrapper.new({}, BadlyDefinedError.new)
+ test "#full_trace cannot be nil" do
+ nil_backtrace_wrapper = ExceptionWrapper.new(@cleaner, BadlyDefinedError.new)
+ nil_cleaner_wrapper = ExceptionWrapper.new(nil, BadlyDefinedError.new)
assert_equal [], nil_backtrace_wrapper.full_trace
assert_equal [], nil_cleaner_wrapper.full_trace
end
- test '#traces returns every trace by category enumerated with an index' do
+ test "#traces returns every trace by category enumerated with an index" do
exception = TestError.new("lib/file.rb:42:in `index'", "/gems/rack.rb:43:in `index'")
- wrapper = ExceptionWrapper.new(@environment, exception)
+ wrapper = ExceptionWrapper.new(@cleaner, exception)
assert_equal({
- 'Application Trace' => [ id: 0, trace: "lib/file.rb:42:in `index'" ],
- 'Framework Trace' => [ id: 1, trace: "/gems/rack.rb:43:in `index'" ],
- 'Full Trace' => [
- { id: 0, trace: "lib/file.rb:42:in `index'" },
- { id: 1, trace: "/gems/rack.rb:43:in `index'" }
+ "Application Trace" => [
+ exception_object_id: exception.object_id,
+ id: 0,
+ trace: "lib/file.rb:42:in `index'"
+ ],
+ "Framework Trace" => [
+ exception_object_id: exception.object_id,
+ id: 1,
+ trace: "/gems/rack.rb:43:in `index'"
+ ],
+ "Full Trace" => [
+ {
+ exception_object_id: exception.object_id,
+ id: 0,
+ trace: "lib/file.rb:42:in `index'"
+ },
+ {
+ exception_object_id: exception.object_id,
+ id: 1,
+ trace: "/gems/rack.rb:43:in `index'"
+ }
]
}, wrapper.traces)
end
diff --git a/actionpack/test/dispatch/executor_test.rb b/actionpack/test/dispatch/executor_test.rb
new file mode 100644
index 0000000000..5b8be39b6d
--- /dev/null
+++ b/actionpack/test/dispatch/executor_test.rb
@@ -0,0 +1,136 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class ExecutorTest < ActiveSupport::TestCase
+ class MyBody < Array
+ def initialize(&block)
+ @on_close = block
+ end
+
+ def foo
+ "foo"
+ end
+
+ def bar
+ "bar"
+ end
+
+ def close
+ @on_close.call if @on_close
+ end
+ end
+
+ def test_returned_body_object_always_responds_to_close
+ body = call_and_return_body
+ assert_respond_to body, :close
+ end
+
+ def test_returned_body_object_always_responds_to_close_even_if_called_twice
+ body = call_and_return_body
+ assert_respond_to body, :close
+ body.close
+
+ body = call_and_return_body
+ assert_respond_to body, :close
+ body.close
+ end
+
+ def test_returned_body_object_behaves_like_underlying_object
+ body = call_and_return_body do
+ b = MyBody.new
+ b << "hello"
+ b << "world"
+ [200, { "Content-Type" => "text/html" }, b]
+ end
+ assert_equal 2, body.size
+ assert_equal "hello", body[0]
+ assert_equal "world", body[1]
+ assert_equal "foo", body.foo
+ assert_equal "bar", body.bar
+ end
+
+ def test_it_calls_close_on_underlying_object_when_close_is_called_on_body
+ close_called = false
+ body = call_and_return_body do
+ b = MyBody.new do
+ close_called = true
+ end
+ [200, { "Content-Type" => "text/html" }, b]
+ end
+ body.close
+ assert close_called
+ end
+
+ def test_returned_body_object_responds_to_all_methods_supported_by_underlying_object
+ body = call_and_return_body do
+ [200, { "Content-Type" => "text/html" }, MyBody.new]
+ end
+ assert_respond_to body, :size
+ assert_respond_to body, :each
+ assert_respond_to body, :foo
+ assert_respond_to body, :bar
+ end
+
+ def test_run_callbacks_are_called_before_close
+ running = false
+ executor.to_run { running = true }
+
+ body = call_and_return_body
+ assert running
+
+ running = false
+ body.close
+ assert_not running
+ end
+
+ def test_complete_callbacks_are_called_on_close
+ completed = false
+ executor.to_complete { completed = true }
+
+ body = call_and_return_body
+ assert_not completed
+
+ body.close
+ assert completed
+ end
+
+ def test_complete_callbacks_are_called_on_exceptions
+ completed = false
+ executor.to_complete { completed = true }
+
+ begin
+ call_and_return_body do
+ raise "error"
+ end
+ rescue
+ end
+
+ assert completed
+ end
+
+ def test_callbacks_execute_in_shared_context
+ result = false
+ executor.to_run { @in_shared_context = true }
+ executor.to_complete { result = @in_shared_context }
+
+ call_and_return_body.close
+ assert result
+ assert_not defined?(@in_shared_context) # it's not in the test itself
+ end
+
+ private
+ def call_and_return_body(&block)
+ app = middleware(block || proc { [200, {}, "response"] })
+ _, _, body = app.call("rack.input" => StringIO.new(""))
+ body
+ end
+
+ def middleware(inner_app)
+ ActionDispatch::Executor.new(inner_app, executor)
+ end
+
+ def executor
+ @executor ||= Class.new(ActiveSupport::Executor)
+ end
+end
diff --git a/actionpack/test/dispatch/header_test.rb b/actionpack/test/dispatch/header_test.rb
index e2b38c23bc..bd2a5b35fb 100644
--- a/actionpack/test/dispatch/header_test.rb
+++ b/actionpack/test/dispatch/header_test.rb
@@ -1,27 +1,33 @@
+# frozen_string_literal: true
+
require "abstract_unit"
class HeaderTest < ActiveSupport::TestCase
+ def make_headers(hash)
+ ActionDispatch::Http::Headers.new ActionDispatch::Request.new hash
+ end
+
setup do
- @headers = ActionDispatch::Http::Headers.new(
+ @headers = make_headers(
"CONTENT_TYPE" => "text/plain",
"HTTP_REFERER" => "/some/page"
)
end
test "#new does not normalize the data" do
- headers = ActionDispatch::Http::Headers.new(
+ headers = make_headers(
"Content-Type" => "application/json",
"HTTP_REFERER" => "/some/page",
"Host" => "http://test.com")
- assert_equal({"Content-Type" => "application/json",
+ assert_equal({ "Content-Type" => "application/json",
"HTTP_REFERER" => "/some/page",
- "Host" => "http://test.com"}, headers.env)
+ "Host" => "http://test.com" }, headers.env)
end
test "#env returns the headers as env variables" do
- assert_equal({"CONTENT_TYPE" => "text/plain",
- "HTTP_REFERER" => "/some/page"}, @headers.env)
+ assert_equal({ "CONTENT_TYPE" => "text/plain",
+ "HTTP_REFERER" => "/some/page" }, @headers.env)
end
test "#each iterates through the env variables" do
@@ -38,6 +44,24 @@ class HeaderTest < ActiveSupport::TestCase
assert_equal "127.0.0.1", @headers["HTTP_HOST"]
end
+ test "add to multivalued headers" do
+ # Sets header when not present
+ @headers.add "Foo", "1"
+ assert_equal "1", @headers["Foo"]
+
+ # Ignores nil values
+ @headers.add "Foo", nil
+ assert_equal "1", @headers["Foo"]
+
+ # Converts value to string
+ @headers.add "Foo", 1
+ assert_equal "1,1", @headers["Foo"]
+
+ # Case-insensitive
+ @headers.add "fOo", 2
+ assert_equal "1,1,2", @headers["foO"]
+ end
+
test "headers can contain numbers" do
@headers["Content-MD5"] = "Q2hlY2sgSW50ZWdyaXR5IQ=="
@@ -54,9 +78,9 @@ class HeaderTest < ActiveSupport::TestCase
test "key?" do
assert @headers.key?("CONTENT_TYPE")
- assert @headers.include?("CONTENT_TYPE")
+ assert_includes @headers, "CONTENT_TYPE"
assert @headers.key?("Content-Type")
- assert @headers.include?("Content-Type")
+ assert_includes @headers, "Content-Type"
end
test "fetch with block" do
@@ -83,32 +107,32 @@ class HeaderTest < ActiveSupport::TestCase
test "#merge! headers with mutation" do
@headers.merge!("Host" => "http://example.test",
"Content-Type" => "text/html")
- assert_equal({"HTTP_HOST" => "http://example.test",
+ assert_equal({ "HTTP_HOST" => "http://example.test",
"CONTENT_TYPE" => "text/html",
- "HTTP_REFERER" => "/some/page"}, @headers.env)
+ "HTTP_REFERER" => "/some/page" }, @headers.env)
end
test "#merge! env with mutation" do
@headers.merge!("HTTP_HOST" => "http://first.com",
"CONTENT_TYPE" => "text/html")
- assert_equal({"HTTP_HOST" => "http://first.com",
+ assert_equal({ "HTTP_HOST" => "http://first.com",
"CONTENT_TYPE" => "text/html",
- "HTTP_REFERER" => "/some/page"}, @headers.env)
+ "HTTP_REFERER" => "/some/page" }, @headers.env)
end
test "merge without mutation" do
combined = @headers.merge("HTTP_HOST" => "http://example.com",
"CONTENT_TYPE" => "text/html")
- assert_equal({"HTTP_HOST" => "http://example.com",
+ assert_equal({ "HTTP_HOST" => "http://example.com",
"CONTENT_TYPE" => "text/html",
- "HTTP_REFERER" => "/some/page"}, combined.env)
+ "HTTP_REFERER" => "/some/page" }, combined.env)
- assert_equal({"CONTENT_TYPE" => "text/plain",
- "HTTP_REFERER" => "/some/page"}, @headers.env)
+ assert_equal({ "CONTENT_TYPE" => "text/plain",
+ "HTTP_REFERER" => "/some/page" }, @headers.env)
end
test "env variables with . are not modified" do
- headers = ActionDispatch::Http::Headers.new
+ headers = make_headers({})
headers.merge! "rack.input" => "",
"rack.request.cookie_hash" => "",
"action_dispatch.logger" => ""
@@ -119,7 +143,7 @@ class HeaderTest < ActiveSupport::TestCase
end
test "symbols are treated as strings" do
- headers = ActionDispatch::Http::Headers.new
+ headers = make_headers({})
headers.merge!(:SERVER_NAME => "example.com",
"HTTP_REFERER" => "/",
:Host => "test.com")
@@ -129,11 +153,17 @@ class HeaderTest < ActiveSupport::TestCase
end
test "headers directly modifies the passed environment" do
- env = {"HTTP_REFERER" => "/"}
- headers = ActionDispatch::Http::Headers.new(env)
- headers['Referer'] = "http://example.com/"
- headers.merge! "CONTENT_TYPE" => "text/plain"
- assert_equal({"HTTP_REFERER"=>"http://example.com/",
- "CONTENT_TYPE"=>"text/plain"}, env)
+ env = { "HTTP_REFERER" => "/" }
+ headers = make_headers(env)
+ headers["Referer"] = "http://example.com/"
+ headers["CONTENT_TYPE"] = "text/plain"
+ assert_equal({ "HTTP_REFERER" => "http://example.com/",
+ "CONTENT_TYPE" => "text/plain" }, env)
+ end
+
+ test "fetch exception" do
+ assert_raises KeyError do
+ @headers.fetch(:some_key_that_doesnt_exist)
+ end
end
end
diff --git a/actionpack/test/dispatch/live_response_test.rb b/actionpack/test/dispatch/live_response_test.rb
index 512f3a8a7a..a9a56f205f 100644
--- a/actionpack/test/dispatch/live_response_test.rb
+++ b/actionpack/test/dispatch/live_response_test.rb
@@ -1,16 +1,18 @@
-require 'abstract_unit'
-require 'active_support/concurrency/latch'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "concurrent/atomic/count_down_latch"
module ActionController
module Live
class ResponseTest < ActiveSupport::TestCase
def setup
@response = Live::Response.new
- @response.request = ActionDispatch::Request.new({}) #yolo
+ @response.request = ActionDispatch::Request.empty
end
def test_header_merge
- header = @response.header.merge('Foo' => 'Bar')
+ header = @response.header.merge("Foo" => "Bar")
assert_kind_of(ActionController::Live::Response::Header, header)
assert_not_equal header, @response.header
end
@@ -18,7 +20,7 @@ module ActionController
def test_initialize_with_default_headers
r = Class.new(Live::Response) do
def self.default_headers
- { 'omg' => 'g' }
+ { "omg" => "g" }
end
end
@@ -27,67 +29,69 @@ module ActionController
end
def test_parallel
- latch = ActiveSupport::Concurrency::Latch.new
+ latch = Concurrent::CountDownLatch.new
t = Thread.new {
- @response.stream.write 'foo'
- latch.await
+ @response.stream.write "foo"
+ latch.wait
@response.stream.close
}
@response.await_commit
@response.each do |part|
- assert_equal 'foo', part
- latch.release
+ assert_equal "foo", part
+ latch.count_down
end
assert t.join
end
def test_setting_body_populates_buffer
- @response.body = 'omg'
+ @response.body = "omg"
@response.close
- assert_equal ['omg'], @response.body_parts
+ assert_equal ["omg"], @response.body_parts
end
def test_cache_control_is_set
- @response.stream.write 'omg'
- assert_equal 'no-cache', @response.headers['Cache-Control']
+ @response.stream.write "omg"
+ assert_equal "no-cache", @response.headers["Cache-Control"]
end
def test_content_length_is_removed
- @response.headers['Content-Length'] = "1234"
- @response.stream.write 'omg'
- assert_nil @response.headers['Content-Length']
+ @response.headers["Content-Length"] = "1234"
+ @response.stream.write "omg"
+ assert_nil @response.headers["Content-Length"]
end
def test_headers_cannot_be_written_after_webserver_reads
- @response.stream.write 'omg'
- latch = ActiveSupport::Concurrency::Latch.new
+ @response.stream.write "omg"
+ latch = Concurrent::CountDownLatch.new
t = Thread.new {
- @response.stream.each do |chunk|
- latch.release
+ @response.each do
+ latch.count_down
end
}
- latch.await
- assert @response.headers.frozen?
+ latch.wait
+ assert_predicate @response.headers, :frozen?
e = assert_raises(ActionDispatch::IllegalStateError) do
- @response.headers['Content-Length'] = "zomg"
+ @response.headers["Content-Length"] = "zomg"
end
- assert_equal 'header already sent', e.message
+ assert_equal "header already sent", e.message
@response.stream.close
t.join
end
def test_headers_cannot_be_written_after_close
@response.stream.close
+ # we can add data until it's actually written, which happens on `each`
+ @response.each { |x| }
e = assert_raises(ActionDispatch::IllegalStateError) do
- @response.headers['Content-Length'] = "zomg"
+ @response.headers["Content-Length"] = "zomg"
end
- assert_equal 'header already sent', e.message
+ assert_equal "header already sent", e.message
end
end
end
diff --git a/actionpack/test/dispatch/mapper_test.rb b/actionpack/test/dispatch/mapper_test.rb
index 889f9a4736..969a08efed 100644
--- a/actionpack/test/dispatch/mapper_test.rb
+++ b/actionpack/test/dispatch/mapper_test.rb
@@ -1,16 +1,11 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module ActionDispatch
module Routing
class MapperTest < ActiveSupport::TestCase
class FakeSet < ActionDispatch::Routing::RouteSet
- attr_reader :routes
- alias :set :routes
-
- def initialize
- @routes = []
- end
-
def resources_path_names
{}
end
@@ -19,16 +14,24 @@ module ActionDispatch
ActionDispatch::Request
end
- def add_route(*args)
- routes << args
+ def dispatcher_class
+ RouteSet::Dispatcher
+ end
+
+ def defaults
+ routes.map(&:defaults)
end
def conditions
- routes.map { |x| x[1] }
+ routes.map(&:constraints)
end
def requirements
- routes.map { |x| x[2] }
+ routes.map(&:path).map(&:requirements)
+ end
+
+ def asts
+ routes.map(&:path).map(&:spec)
end
end
@@ -36,18 +39,88 @@ module ActionDispatch
Mapper.new FakeSet.new
end
+ def test_scope_raises_on_anchor
+ fakeset = FakeSet.new
+ mapper = Mapper.new fakeset
+ assert_raises(ArgumentError) do
+ mapper.scope(anchor: false) do
+ end
+ end
+ end
+
+ def test_blows_up_without_via
+ fakeset = FakeSet.new
+ mapper = Mapper.new fakeset
+ assert_raises(ArgumentError) do
+ mapper.match "/", to: "posts#index", as: :main
+ end
+ end
+
+ def test_unscoped_formatted
+ fakeset = FakeSet.new
+ mapper = Mapper.new fakeset
+ mapper.get "/foo", to: "posts#index", as: :main, format: true
+ assert_equal({ controller: "posts", action: "index" },
+ fakeset.defaults.first)
+ assert_equal "/foo.:format", fakeset.asts.first.to_s
+ end
+
+ def test_scoped_formatted
+ fakeset = FakeSet.new
+ mapper = Mapper.new fakeset
+ mapper.scope(format: true) do
+ mapper.get "/foo", to: "posts#index", as: :main
+ end
+ assert_equal({ controller: "posts", action: "index" },
+ fakeset.defaults.first)
+ assert_equal "/foo.:format", fakeset.asts.first.to_s
+ end
+
+ def test_random_keys
+ fakeset = FakeSet.new
+ mapper = Mapper.new fakeset
+ mapper.scope(omg: :awesome) do
+ mapper.get "/", to: "posts#index", as: :main
+ end
+ assert_equal({ omg: :awesome, controller: "posts", action: "index" },
+ fakeset.defaults.first)
+ assert_equal("GET", fakeset.routes.first.verb)
+ end
+
def test_mapping_requirements
- options = { :controller => 'foo', :action => 'bar', :via => :get }
- m = Mapper::Mapping.build({}, FakeSet.new, '/store/:name(*rest)', nil, options)
- _, _, requirements, _ = m.to_route
- assert_equal(/.+?/, requirements[:rest])
+ options = {}
+ scope = Mapper::Scope.new({})
+ ast = Journey::Parser.parse "/store/:name(*rest)"
+ m = Mapper::Mapping.build(scope, FakeSet.new, ast, "foo", "bar", nil, [:get], nil, {}, true, options)
+ assert_equal(/.+?/, m.requirements[:rest])
+ end
+
+ def test_via_scope
+ fakeset = FakeSet.new
+ mapper = Mapper.new fakeset
+ mapper.scope(via: :put) do
+ mapper.match "/", to: "posts#index", as: :main
+ end
+ assert_equal("PUT", fakeset.routes.first.verb)
+ end
+
+ def test_to_scope
+ fakeset = FakeSet.new
+ mapper = Mapper.new fakeset
+ mapper.scope(to: "posts#index") do
+ mapper.get :all
+ mapper.post :most
+ end
+
+ assert_equal "posts#index", fakeset.routes.to_a[0].defaults[:to]
+ assert_equal "posts#index", fakeset.routes.to_a[1].defaults[:to]
end
def test_map_slash
fakeset = FakeSet.new
mapper = Mapper.new fakeset
- mapper.get '/', :to => 'posts#index', :as => :main
- assert_equal '/', fakeset.conditions.first[:path_info]
+ mapper.get "/", to: "posts#index", as: :main
+ assert_equal "/", fakeset.asts.first.to_s
end
def test_map_more_slashes
@@ -55,31 +128,31 @@ module ActionDispatch
mapper = Mapper.new fakeset
# FIXME: is this a desired behavior?
- mapper.get '/one/two/', :to => 'posts#index', :as => :main
- assert_equal '/one/two(.:format)', fakeset.conditions.first[:path_info]
+ mapper.get "/one/two/", to: "posts#index", as: :main
+ assert_equal "/one/two(.:format)", fakeset.asts.first.to_s
end
def test_map_wildcard
fakeset = FakeSet.new
mapper = Mapper.new fakeset
- mapper.get '/*path', :to => 'pages#show'
- assert_equal '/*path(.:format)', fakeset.conditions.first[:path_info]
+ mapper.get "/*path", to: "pages#show"
+ assert_equal "/*path(.:format)", fakeset.asts.first.to_s
assert_equal(/.+?/, fakeset.requirements.first[:path])
end
def test_map_wildcard_with_other_element
fakeset = FakeSet.new
mapper = Mapper.new fakeset
- mapper.get '/*path/foo/:bar', :to => 'pages#show'
- assert_equal '/*path/foo/:bar(.:format)', fakeset.conditions.first[:path_info]
+ mapper.get "/*path/foo/:bar", to: "pages#show"
+ assert_equal "/*path/foo/:bar(.:format)", fakeset.asts.first.to_s
assert_equal(/.+?/, fakeset.requirements.first[:path])
end
def test_map_wildcard_with_multiple_wildcard
fakeset = FakeSet.new
mapper = Mapper.new fakeset
- mapper.get '/*foo/*bar', :to => 'pages#show'
- assert_equal '/*foo/*bar(.:format)', fakeset.conditions.first[:path_info]
+ mapper.get "/*foo/*bar", to: "pages#show"
+ assert_equal "/*foo/*bar(.:format)", fakeset.asts.first.to_s
assert_equal(/.+?/, fakeset.requirements.first[:foo])
assert_equal(/.+?/, fakeset.requirements.first[:bar])
end
@@ -87,19 +160,19 @@ module ActionDispatch
def test_map_wildcard_with_format_false
fakeset = FakeSet.new
mapper = Mapper.new fakeset
- mapper.get '/*path', :to => 'pages#show', :format => false
- assert_equal '/*path', fakeset.conditions.first[:path_info]
+ mapper.get "/*path", to: "pages#show", format: false
+ assert_equal "/*path", fakeset.asts.first.to_s
assert_nil fakeset.requirements.first[:path]
end
def test_map_wildcard_with_format_true
fakeset = FakeSet.new
mapper = Mapper.new fakeset
- mapper.get '/*path', :to => 'pages#show', :format => true
- assert_equal '/*path.:format', fakeset.conditions.first[:path_info]
+ mapper.get "/*path", to: "pages#show", format: true
+ assert_equal "/*path.:format", fakeset.asts.first.to_s
end
- def test_raising_helpful_error_on_invalid_arguments
+ def test_raising_error_when_path_is_not_passed
fakeset = FakeSet.new
mapper = Mapper.new fakeset
app = lambda { |env| [200, {}, [""]] }
@@ -107,6 +180,31 @@ module ActionDispatch
mapper.mount app
end
end
+
+ def test_raising_error_when_rack_app_is_not_passed
+ fakeset = FakeSet.new
+ mapper = Mapper.new fakeset
+ assert_raises ArgumentError do
+ mapper.mount 10, as: "exciting"
+ end
+
+ assert_raises ArgumentError do
+ mapper.mount as: "exciting"
+ end
+ end
+
+ def test_scope_does_not_destructively_mutate_default_options
+ fakeset = FakeSet.new
+ mapper = Mapper.new fakeset
+
+ frozen = { foo: :bar }.freeze
+
+ assert_nothing_raised do
+ mapper.scope(defaults: frozen) do
+ # pass
+ end
+ end
+ end
end
end
end
diff --git a/actionpack/test/dispatch/middleware_stack/middleware_test.rb b/actionpack/test/dispatch/middleware_stack/middleware_test.rb
deleted file mode 100644
index 9607f026db..0000000000
--- a/actionpack/test/dispatch/middleware_stack/middleware_test.rb
+++ /dev/null
@@ -1,77 +0,0 @@
-require 'abstract_unit'
-require 'action_dispatch/middleware/stack'
-
-module ActionDispatch
- class MiddlewareStack
- class MiddlewareTest < ActiveSupport::TestCase
- class Omg; end
-
- {
- 'concrete' => Omg,
- 'anonymous' => Class.new
- }.each do |name, klass|
-
- define_method("test_#{name}_klass") do
- mw = Middleware.new klass
- assert_equal klass, mw.klass
- end
-
- define_method("test_#{name}_==") do
- mw1 = Middleware.new klass
- mw2 = Middleware.new klass
- assert_equal mw1, mw2
- end
-
- end
-
- def test_string_class
- mw = Middleware.new Omg.name
- assert_equal Omg, mw.klass
- end
-
- def test_double_equal_works_with_classes
- k = Class.new
- mw = Middleware.new k
- assert_operator mw, :==, k
-
- result = mw != Class.new
- assert result, 'middleware should not equal other anon class'
- end
-
- def test_double_equal_works_with_strings
- mw = Middleware.new Omg
- assert_operator mw, :==, Omg.name
- end
-
- def test_double_equal_normalizes_strings
- mw = Middleware.new Omg
- assert_operator mw, :==, "::#{Omg.name}"
- end
-
- def test_middleware_loads_classnames_from_cache
- mw = Class.new(Middleware) {
- attr_accessor :classcache
- }.new(Omg.name)
-
- fake_cache = { mw.name => Omg }
- mw.classcache = fake_cache
-
- assert_equal Omg, mw.klass
-
- fake_cache[mw.name] = Middleware
- assert_equal Middleware, mw.klass
- end
-
- def test_middleware_always_returns_class
- mw = Class.new(Middleware) {
- attr_accessor :classcache
- }.new(Omg)
-
- fake_cache = { mw.name => Middleware }
- mw.classcache = fake_cache
-
- assert_equal Omg, mw.klass
- end
- end
- end
-end
diff --git a/actionpack/test/dispatch/middleware_stack_test.rb b/actionpack/test/dispatch/middleware_stack_test.rb
index 948a690979..5f43e5a3c5 100644
--- a/actionpack/test/dispatch/middleware_stack_test.rb
+++ b/actionpack/test/dispatch/middleware_stack_test.rb
@@ -1,9 +1,12 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class MiddlewareStackTest < ActiveSupport::TestCase
class FooMiddleware; end
class BarMiddleware; end
class BazMiddleware; end
+ class HiyaMiddleware; end
class BlockMiddleware
attr_reader :block
def initialize(&block)
@@ -17,37 +20,29 @@ class MiddlewareStackTest < ActiveSupport::TestCase
@stack.use BarMiddleware
end
- test "use should push middleware as class onto the stack" do
- assert_difference "@stack.size" do
- @stack.use BazMiddleware
- end
- assert_equal BazMiddleware, @stack.last.klass
- end
-
- test "use should push middleware as a string onto the stack" do
- assert_difference "@stack.size" do
- @stack.use "MiddlewareStackTest::BazMiddleware"
+ def test_delete_works
+ assert_difference "@stack.size", -1 do
+ @stack.delete FooMiddleware
end
- assert_equal BazMiddleware, @stack.last.klass
end
- test "use should push middleware as a symbol onto the stack" do
+ test "use should push middleware as class onto the stack" do
assert_difference "@stack.size" do
- @stack.use :"MiddlewareStackTest::BazMiddleware"
+ @stack.use BazMiddleware
end
assert_equal BazMiddleware, @stack.last.klass
end
test "use should push middleware class with arguments onto the stack" do
assert_difference "@stack.size" do
- @stack.use BazMiddleware, true, :foo => "bar"
+ @stack.use BazMiddleware, true, foo: "bar"
end
assert_equal BazMiddleware, @stack.last.klass
- assert_equal([true, {:foo => "bar"}], @stack.last.args)
+ assert_equal([true, { foo: "bar" }], @stack.last.args)
end
test "use should push middleware class with block arguments onto the stack" do
- proc = Proc.new {}
+ proc = Proc.new { }
assert_difference "@stack.size" do
@stack.use(BlockMiddleware, &proc)
end
@@ -83,35 +78,38 @@ class MiddlewareStackTest < ActiveSupport::TestCase
test "swaps one middleware out for same middleware class" do
assert_equal FooMiddleware, @stack[0].klass
- @stack.swap(FooMiddleware, FooMiddleware, Proc.new { |env| [500, {}, ['error!']] })
+ @stack.swap(FooMiddleware, FooMiddleware, Proc.new { |env| [500, {}, ["error!"]] })
assert_equal FooMiddleware, @stack[0].klass
end
test "unshift adds a new middleware at the beginning of the stack" do
- @stack.unshift :"MiddlewareStackTest::BazMiddleware"
+ @stack.unshift MiddlewareStackTest::BazMiddleware
assert_equal BazMiddleware, @stack.first.klass
end
test "raise an error on invalid index" do
assert_raise RuntimeError do
- @stack.insert("HiyaMiddleware", BazMiddleware)
+ @stack.insert(HiyaMiddleware, BazMiddleware)
end
assert_raise RuntimeError do
- @stack.insert_after("HiyaMiddleware", BazMiddleware)
+ @stack.insert_after(HiyaMiddleware, BazMiddleware)
end
end
- test "lazy evaluates middleware class" do
- assert_difference "@stack.size" do
- @stack.use "MiddlewareStackTest::BazMiddleware"
- end
- assert_equal BazMiddleware, @stack.last.klass
+ test "can check if Middleware are equal - Class" do
+ assert_equal @stack.last, BarMiddleware
+ end
+
+ test "includes a class" do
+ assert_equal true, @stack.include?(BarMiddleware)
+ end
+
+ test "can check if Middleware are equal - Middleware" do
+ assert_equal @stack.last, @stack.last
end
- test "lazy compares so unloaded constants are not loaded" do
- @stack.use "UnknownMiddleware"
- @stack.use :"MiddlewareStackTest::BazMiddleware"
- assert @stack.include?("::MiddlewareStackTest::BazMiddleware")
+ test "includes a middleware" do
+ assert_equal true, @stack.include?(ActionDispatch::MiddlewareStack::Middleware.new(BarMiddleware, nil, nil))
end
end
diff --git a/actionpack/test/dispatch/mime_type_test.rb b/actionpack/test/dispatch/mime_type_test.rb
index 3017a9c2d6..fa264417e1 100644
--- a/actionpack/test/dispatch/mime_type_test.rb
+++ b/actionpack/test/dispatch/mime_type_test.rb
@@ -1,107 +1,106 @@
-require 'abstract_unit'
+# frozen_string_literal: true
-class MimeTypeTest < ActiveSupport::TestCase
+require "abstract_unit"
+class MimeTypeTest < ActiveSupport::TestCase
test "parse single" do
Mime::LOOKUP.each_key do |mime_type|
- unless mime_type == 'image/*'
+ unless mime_type == "image/*"
assert_equal [Mime::Type.lookup(mime_type)], Mime::Type.parse(mime_type)
end
end
end
test "unregister" do
+ assert_nil Mime[:mobile]
+
begin
- Mime::Type.register("text/x-mobile", :mobile)
- assert defined?(Mime::MOBILE)
- assert_equal Mime::MOBILE, Mime::LOOKUP['text/x-mobile']
- assert_equal Mime::MOBILE, Mime::EXTENSION_LOOKUP['mobile']
+ mime = Mime::Type.register("text/x-mobile", :mobile)
+ assert_equal mime, Mime[:mobile]
+ assert_equal mime, Mime::Type.lookup("text/x-mobile")
+ assert_equal mime, Mime::Type.lookup_by_extension(:mobile)
Mime::Type.unregister(:mobile)
- assert !defined?(Mime::MOBILE), "Mime::MOBILE should not be defined"
- assert !Mime::LOOKUP.has_key?('text/x-mobile'), "Mime::LOOKUP should not have key ['text/x-mobile]"
- assert !Mime::EXTENSION_LOOKUP.has_key?('mobile'), "Mime::EXTENSION_LOOKUP should not have key ['mobile]"
+ assert_nil Mime[:mobile], "Mime[:mobile] should be nil after unregistering :mobile"
+ assert_nil Mime::Type.lookup_by_extension(:mobile), "Should be missing MIME extension lookup for :mobile"
ensure
- Mime.module_eval { remove_const :MOBILE if const_defined?(:MOBILE) }
- Mime::LOOKUP.reject!{|key,_| key == 'text/x-mobile'}
+ Mime::Type.unregister :mobile
end
end
test "parse text with trailing star at the beginning" do
accept = "text/*, text/html, application/json, multipart/form-data"
- expect = [Mime::HTML, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::VCF, Mime::XML, Mime::YAML, Mime::JSON, Mime::MULTIPART_FORM]
+ expect = [Mime[:html], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:vtt], Mime[:xml], Mime[:yaml], Mime[:json], Mime[:multipart_form]]
parsed = Mime::Type.parse(accept)
- assert_equal expect, parsed
+ assert_equal expect.map(&:to_s), parsed.map(&:to_s)
end
test "parse text with trailing star in the end" do
accept = "text/html, application/json, multipart/form-data, text/*"
- expect = [Mime::HTML, Mime::JSON, Mime::MULTIPART_FORM, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::VCF, Mime::XML, Mime::YAML]
+ expect = [Mime[:html], Mime[:json], Mime[:multipart_form], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:vtt], Mime[:xml], Mime[:yaml]]
parsed = Mime::Type.parse(accept)
- assert_equal expect, parsed
+ assert_equal expect.map(&:to_s), parsed.map(&:to_s)
end
test "parse text with trailing star" do
accept = "text/*"
- expect = [Mime::HTML, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::VCF, Mime::XML, Mime::YAML, Mime::JSON]
+ expect = [Mime[:html], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:vtt], Mime[:xml], Mime[:yaml], Mime[:json]]
parsed = Mime::Type.parse(accept)
- assert_equal expect, parsed
+ assert_equal expect.map(&:to_s).sort!, parsed.map(&:to_s).sort!
end
test "parse application with trailing star" do
accept = "application/*"
- expect = [Mime::HTML, Mime::JS, Mime::XML, Mime::RSS, Mime::ATOM, Mime::YAML, Mime::URL_ENCODED_FORM, Mime::JSON, Mime::PDF, Mime::ZIP]
+ expect = [Mime[:html], Mime[:js], Mime[:xml], Mime[:rss], Mime[:atom], Mime[:yaml], Mime[:url_encoded_form], Mime[:json], Mime[:pdf], Mime[:zip], Mime[:gzip]]
parsed = Mime::Type.parse(accept)
- assert_equal expect, parsed
+ assert_equal expect.map(&:to_s).sort!, parsed.map(&:to_s).sort!
end
test "parse without q" do
accept = "text/xml,application/xhtml+xml,text/yaml,application/xml,text/html,image/png,text/plain,application/pdf,*/*"
- expect = [Mime::HTML, Mime::XML, Mime::YAML, Mime::PNG, Mime::TEXT, Mime::PDF, Mime::ALL]
- assert_equal expect, Mime::Type.parse(accept)
+ expect = [Mime[:html], Mime[:xml], Mime[:yaml], Mime[:png], Mime[:text], Mime[:pdf], "*/*"]
+ assert_equal expect.map(&:to_s), Mime::Type.parse(accept).map(&:to_s)
end
test "parse with q" do
accept = "text/xml,application/xhtml+xml,text/yaml; q=0.3,application/xml,text/html; q=0.8,image/png,text/plain; q=0.5,application/pdf,*/*; q=0.2"
- expect = [Mime::HTML, Mime::XML, Mime::PNG, Mime::PDF, Mime::TEXT, Mime::YAML, Mime::ALL]
- assert_equal expect, Mime::Type.parse(accept)
+ expect = [Mime[:html], Mime[:xml], Mime[:png], Mime[:pdf], Mime[:text], Mime[:yaml], "*/*"]
+ assert_equal expect.map(&:to_s), Mime::Type.parse(accept).map(&:to_s)
end
test "parse single media range with q" do
accept = "text/html;q=0.9"
- expect = [Mime::HTML]
+ expect = [Mime[:html]]
assert_equal expect, Mime::Type.parse(accept)
end
test "parse arbitrary media type parameters" do
accept = 'multipart/form-data; boundary="simple boundary"'
- expect = [Mime::MULTIPART_FORM]
+ expect = [Mime[:multipart_form]]
assert_equal expect, Mime::Type.parse(accept)
end
# Accept header send with user HTTP_USER_AGENT: Sunrise/0.42j (Windows XP)
test "parse broken acceptlines" do
accept = "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/*,,*/*;q=0.5"
- expect = [Mime::HTML, Mime::XML, "image/*", Mime::TEXT, Mime::ALL]
- assert_equal expect, Mime::Type.parse(accept).collect(&:to_s)
+ expect = [Mime[:html], Mime[:xml], "image/*", Mime[:text], "*/*"]
+ assert_equal expect.map(&:to_s), Mime::Type.parse(accept).map(&:to_s)
end
# Accept header send with user HTTP_USER_AGENT: Mozilla/4.0
# (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322; InfoPath.1)
test "parse other broken acceptlines" do
accept = "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, , pronto/1.00.00, sslvpn/1.00.00.00, */*"
- expect = ['image/gif', 'image/x-xbitmap', 'image/jpeg','image/pjpeg', 'application/x-shockwave-flash', 'application/vnd.ms-excel', 'application/vnd.ms-powerpoint', 'application/msword', 'pronto/1.00.00', 'sslvpn/1.00.00.00', Mime::ALL]
- assert_equal expect, Mime::Type.parse(accept).collect(&:to_s)
+ expect = ["image/gif", "image/x-xbitmap", "image/jpeg", "image/pjpeg", "application/x-shockwave-flash", "application/vnd.ms-excel", "application/vnd.ms-powerpoint", "application/msword", "pronto/1.00.00", "sslvpn/1.00.00.00", "*/*"]
+ assert_equal expect.map(&:to_s), Mime::Type.parse(accept).map(&:to_s)
end
test "custom type" do
begin
- Mime::Type.register("image/foo", :foo)
- assert_nothing_raised do
- assert_equal Mime::FOO, Mime::SET.last
- end
+ type = Mime::Type.register("image/foo", :foo)
+ assert_equal type, Mime[:foo]
ensure
- Mime::Type.unregister(:FOO)
+ Mime::Type.unregister(:foo)
end
end
@@ -109,10 +108,10 @@ class MimeTypeTest < ActiveSupport::TestCase
begin
Mime::Type.register "text/foobar", :foobar, ["text/foo", "text/bar"]
%w[text/foobar text/foo text/bar].each do |type|
- assert_equal Mime::FOOBAR, type
+ assert_equal Mime[:foobar], type
end
ensure
- Mime::Type.unregister(:FOOBAR)
+ Mime::Type.unregister(:foobar)
end
end
@@ -123,10 +122,10 @@ class MimeTypeTest < ActiveSupport::TestCase
registered_mimes << mime
end
- Mime::Type.register("text/foo", :foo)
- assert_equal [Mime::FOO], registered_mimes
+ mime = Mime::Type.register("text/foo", :foo)
+ assert_equal [mime], registered_mimes
ensure
- Mime::Type.unregister(:FOO)
+ Mime::Type.unregister(:foo)
end
end
@@ -134,70 +133,55 @@ class MimeTypeTest < ActiveSupport::TestCase
begin
Mime::Type.register "text/foobar", :foobar, [], [:foo, "bar"]
%w[foobar foo bar].each do |extension|
- assert_equal Mime::FOOBAR, Mime::EXTENSION_LOOKUP[extension]
+ assert_equal Mime[:foobar], Mime::EXTENSION_LOOKUP[extension]
end
ensure
- Mime::Type.unregister(:FOOBAR)
+ Mime::Type.unregister(:foobar)
end
end
test "register alias" do
begin
Mime::Type.register_alias "application/xhtml+xml", :foobar
- assert_equal Mime::HTML, Mime::EXTENSION_LOOKUP['foobar']
+ assert_equal Mime[:html], Mime::EXTENSION_LOOKUP["foobar"]
ensure
- Mime::Type.unregister(:FOOBAR)
+ Mime::Type.unregister(:foobar)
end
end
test "type should be equal to symbol" do
- assert_equal Mime::HTML, 'application/xhtml+xml'
- assert_equal Mime::HTML, :html
+ assert_equal Mime[:html], "application/xhtml+xml"
+ assert_equal Mime[:html], :html
end
test "type convenience methods" do
- # Don't test Mime::ALL, since it Mime::ALL#html? == true
- types = Mime::SET.symbols.uniq - [:all, :iphone]
-
- # Remove custom Mime::Type instances set in other tests, like Mime::GIF and Mime::IPHONE
- types.delete_if { |type| !Mime.const_defined?(type.upcase) }
-
+ types = Mime::SET.symbols.uniq - [:iphone]
types.each do |type|
- mime = Mime.const_get(type.upcase)
- assert mime.respond_to?("#{type}?"), "#{mime.inspect} does not respond to #{type}?"
- assert mime.send("#{type}?"), "#{mime.inspect} is not #{type}?"
+ mime = Mime[type]
+ assert_respond_to mime, "#{type}?"
+ assert_equal type, mime.symbol, "#{mime.inspect} is not #{type}?"
invalid_types = types - [type]
- invalid_types.delete(:html) if Mime::Type.html_types.include?(type)
- invalid_types.each { |other_type| assert !mime.send("#{other_type}?"), "#{mime.inspect} is #{other_type}?" }
+ invalid_types.delete(:html)
+ invalid_types.each { |other_type|
+ assert_not_equal mime.symbol, other_type, "#{mime.inspect} is #{other_type}?"
+ }
end
end
- test "mime all is html" do
- assert Mime::ALL.all?, "Mime::ALL is not all?"
- assert Mime::ALL.html?, "Mime::ALL is not html?"
- end
-
- test "verifiable mime types" do
- all_types = Mime::SET.symbols
- all_types.uniq!
- # Remove custom Mime::Type instances set in other tests, like Mime::GIF and Mime::IPHONE
- all_types.delete_if { |type| !Mime.const_defined?(type.upcase) }
- end
-
test "references gives preference to symbols before strings" do
- assert_equal :html, Mime::HTML.ref
+ assert_equal :html, Mime[:html].ref
another = Mime::Type.lookup("foo/bar")
assert_nil another.to_sym
assert_equal "foo/bar", another.ref
end
test "regexp matcher" do
- assert Mime::JS =~ "text/javascript"
- assert Mime::JS =~ "application/javascript"
- assert Mime::JS !~ "text/html"
- assert !(Mime::JS !~ "text/javascript")
- assert !(Mime::JS !~ "application/javascript")
- assert Mime::HTML =~ 'application/xhtml+xml'
+ assert Mime[:js] =~ "text/javascript"
+ assert Mime[:js] =~ "application/javascript"
+ assert Mime[:js] !~ "text/html"
+ assert_not (Mime[:js] !~ "text/javascript")
+ assert_not (Mime[:js] !~ "application/javascript")
+ assert Mime[:html] =~ "application/xhtml+xml"
end
end
diff --git a/actionpack/test/dispatch/mount_test.rb b/actionpack/test/dispatch/mount_test.rb
index 6a439be2b5..f6cf653980 100644
--- a/actionpack/test/dispatch/mount_test.rb
+++ b/actionpack/test/dispatch/mount_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'rails/engine'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "rails/engine"
class TestRoutingMount < ActionDispatch::IntegrationTest
Router = ActionDispatch::Routing::RouteSet.new
@@ -15,30 +17,30 @@ class TestRoutingMount < ActionDispatch::IntegrationTest
def self.routes; Object.new; end
def self.call(env)
- [200, {"Content-Type" => "text/html"}, ["OK"]]
+ [200, { "Content-Type" => "text/html" }, ["OK"]]
end
end
Router.draw do
SprocketsApp = lambda { |env|
- [200, {"Content-Type" => "text/html"}, ["#{env["SCRIPT_NAME"]} -- #{env["PATH_INFO"]}"]]
+ [200, { "Content-Type" => "text/html" }, ["#{env["SCRIPT_NAME"]} -- #{env["PATH_INFO"]}"]]
}
- mount SprocketsApp, :at => "/sprockets"
+ mount SprocketsApp, at: "/sprockets"
mount SprocketsApp => "/shorthand"
- mount SinatraLikeApp, :at => "/fakeengine", :as => :fake
- mount SinatraLikeApp, :at => "/getfake", :via => :get
+ mount SinatraLikeApp, at: "/fakeengine", as: :fake
+ mount SinatraLikeApp, at: "/getfake", via: :get
scope "/its_a" do
- mount SprocketsApp, :at => "/sprocket"
+ mount SprocketsApp, at: "/sprocket"
end
resources :users do
- mount AppWithRoutes, :at => "/fakeengine", :as => :fake_mounted_at_resource
+ mount AppWithRoutes, at: "/fakeengine", as: :fake_mounted_at_resource
end
- mount SprocketsApp, :at => "/", :via => :get
+ mount SprocketsApp, at: "/", via: :get
end
APP = RoutedRackApp.new Router
@@ -49,7 +51,7 @@ class TestRoutingMount < ActionDispatch::IntegrationTest
def test_app_name_is_properly_generated_when_engine_is_mounted_in_resources
assert Router.mounted_helpers.method_defined?(:user_fake_mounted_at_resource),
"A mounted helper should be defined with a parent's prefix"
- assert Router.named_routes.routes[:user_fake_mounted_at_resource],
+ assert Router.named_routes.key?(:user_fake_mounted_at_resource),
"A named route should be defined with a parent's prefix"
end
@@ -64,7 +66,7 @@ class TestRoutingMount < ActionDispatch::IntegrationTest
end
def test_mounting_works_with_nested_script_name
- get "/foo/sprockets/omg", headers: { 'SCRIPT_NAME' => '/foo', 'PATH_INFO' => '/sprockets/omg' }
+ get "/foo/sprockets/omg", headers: { "SCRIPT_NAME" => "/foo", "PATH_INFO" => "/sprockets/omg" }
assert_equal "/foo/sprockets -- /omg", response.body
end
diff --git a/actionpack/test/dispatch/prefix_generation_test.rb b/actionpack/test/dispatch/prefix_generation_test.rb
index f90d5499d7..7a7a201b11 100644
--- a/actionpack/test/dispatch/prefix_generation_test.rb
+++ b/actionpack/test/dispatch/prefix_generation_test.rb
@@ -1,6 +1,8 @@
-require 'abstract_unit'
-require 'rack/test'
-require 'rails/engine'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "rack/test"
+require "rails/engine"
module TestGenerationPrefix
class Post
@@ -11,7 +13,7 @@ module TestGenerationPrefix
end
def self.model_name
- klass = "Post"
+ klass = +"Post"
def klass.name; self end
ActiveModel::Name.new(klass)
@@ -22,46 +24,44 @@ 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
- get "/posts", :to => "inside_engine_generating#index", :as => :posts
- get "/url_to_application", :to => "inside_engine_generating#url_to_application"
- get "/polymorphic_path_for_engine", :to => "inside_engine_generating#polymorphic_path_for_engine"
- get "/conflicting_url", :to => "inside_engine_generating#conflicting"
- get "/foo", :to => "never#invoked", :as => :named_helper_that_should_be_invoked_only_in_respond_to_test
-
- get "/relative_path_root", :to => redirect("")
- get "/relative_path_redirect", :to => redirect("foo")
- get "/relative_option_root", :to => redirect(:path => "")
- get "/relative_option_redirect", :to => redirect(:path => "foo")
- get "/relative_custom_root", :to => redirect { |params, request| "" }
- get "/relative_custom_redirect", :to => redirect { |params, request| "foo" }
-
- get "/absolute_path_root", :to => redirect("/")
- get "/absolute_path_redirect", :to => redirect("/foo")
- get "/absolute_option_root", :to => redirect(:path => "/")
- get "/absolute_option_redirect", :to => redirect(:path => "/foo")
- get "/absolute_custom_root", :to => redirect { |params, request| "/" }
- get "/absolute_custom_redirect", :to => redirect { |params, request| "/foo" }
+ get "/posts/:id", to: "inside_engine_generating#show", as: :post
+ get "/posts", to: "inside_engine_generating#index", as: :posts
+ get "/url_to_application", to: "inside_engine_generating#url_to_application"
+ get "/polymorphic_path_for_engine", to: "inside_engine_generating#polymorphic_path_for_engine"
+ get "/conflicting_url", to: "inside_engine_generating#conflicting"
+ get "/foo", to: "never#invoked", as: :named_helper_that_should_be_invoked_only_in_respond_to_test
+
+ get "/relative_path_root", to: redirect("")
+ get "/relative_path_redirect", to: redirect("foo")
+ get "/relative_option_root", to: redirect(path: "")
+ get "/relative_option_redirect", to: redirect(path: "foo")
+ get "/relative_custom_root", to: redirect { |params, request| "" }
+ get "/relative_custom_redirect", to: redirect { |params, request| "foo" }
+
+ get "/absolute_path_root", to: redirect("/")
+ get "/absolute_path_redirect", to: redirect("/foo")
+ get "/absolute_option_root", to: redirect(path: "/")
+ get "/absolute_option_redirect", to: redirect(path: "/foo")
+ get "/absolute_custom_root", to: redirect { |params, request| "/" }
+ get "/absolute_custom_redirect", to: redirect { |params, request| "/foo" }
end
end
class RailsApplication < Rails::Engine
routes.draw do
- scope "/:omg", :omg => "awesome" do
+ scope "/:omg", omg: "awesome" do
mount BlogEngine => "/blog", :as => "blog_engine"
end
- get "/posts/:id", :to => "outside_engine_generating#post", :as => :post
- get "/generate", :to => "outside_engine_generating#index"
- get "/polymorphic_path_for_app", :to => "outside_engine_generating#polymorphic_path_for_app"
- get "/polymorphic_path_for_engine", :to => "outside_engine_generating#polymorphic_path_for_engine"
- get "/polymorphic_with_url_for", :to => "outside_engine_generating#polymorphic_with_url_for"
- get "/conflicting_url", :to => "outside_engine_generating#conflicting"
- get "/ivar_usage", :to => "outside_engine_generating#ivar_usage"
- root :to => "outside_engine_generating#index"
+ get "/posts/:id", to: "outside_engine_generating#post", as: :post
+ get "/generate", to: "outside_engine_generating#index"
+ get "/polymorphic_path_for_app", to: "outside_engine_generating#polymorphic_path_for_app"
+ get "/polymorphic_path_for_engine", to: "outside_engine_generating#polymorphic_path_for_engine"
+ get "/polymorphic_with_url_for", to: "outside_engine_generating#polymorphic_with_url_for"
+ get "/conflicting_url", to: "outside_engine_generating#conflicting"
+ get "/ivar_usage", to: "outside_engine_generating#ivar_usage"
+ root to: "outside_engine_generating#index"
end
end
@@ -73,26 +73,26 @@ module TestGenerationPrefix
include RailsApplication.routes.mounted_helpers
def index
- render :text => posts_path
+ render plain: posts_path
end
def show
- render :text => post_path(:id => params[:id])
+ render plain: post_path(id: params[:id])
end
def url_to_application
- path = main_app.url_for(:controller => "outside_engine_generating",
- :action => "index",
- :only_path => true)
- render :text => path
+ path = main_app.url_for(controller: "outside_engine_generating",
+ action: "index",
+ only_path: true)
+ render plain: path
end
def polymorphic_path_for_engine
- render :text => polymorphic_path(Post.new)
+ render plain: polymorphic_path(Post.new)
end
def conflicting
- render :text => "engine"
+ render plain: "engine"
end
end
@@ -101,28 +101,28 @@ module TestGenerationPrefix
include RailsApplication.routes.url_helpers
def index
- render :text => blog_engine.post_path(:id => 1)
+ render plain: blog_engine.post_path(id: 1)
end
def polymorphic_path_for_engine
- render :text => blog_engine.polymorphic_path(Post.new)
+ render plain: blog_engine.polymorphic_path(Post.new)
end
def polymorphic_path_for_app
- render :text => polymorphic_path(Post.new)
+ render plain: polymorphic_path(Post.new)
end
def polymorphic_with_url_for
- render :text => blog_engine.url_for(Post.new)
+ render plain: blog_engine.url_for(Post.new)
end
def conflicting
- render :text => "application"
+ render plain: "application"
end
def ivar_usage
@blog_engine = "Not the engine route helper"
- render :text => blog_engine.post_path(:id => 1)
+ render plain: blog_engine.post_path(id: 1)
end
end
@@ -153,114 +153,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 priotity than application's url_helpers" do
+ 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"}
+ 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
@@ -269,16 +269,16 @@ module TestGenerationPrefix
end
test "[OBJECT] generating engine's route includes prefix" do
- assert_equal "/awesome/blog/posts/1", engine_object.post_path(:id => 1)
+ assert_equal "/awesome/blog/posts/1", engine_object.post_path(id: 1)
end
test "[OBJECT] generating engine's route includes dynamic prefix" do
- assert_equal "/pure-awesomeness/blog/posts/3", engine_object.post_path(:id => 3, :omg => "pure-awesomeness")
+ assert_equal "/pure-awesomeness/blog/posts/3", engine_object.post_path(id: 3, omg: "pure-awesomeness")
end
test "[OBJECT] generating engine's route includes default_url_options[:script_name]" do
- RailsApplication.routes.default_url_options = {:script_name => "/something"}
- assert_equal "/something/pure-awesomeness/blog/posts/3", engine_object.post_path(:id => 3, :omg => "pure-awesomeness")
+ RailsApplication.routes.default_url_options = { script_name: "/something" }
+ assert_equal "/something/pure-awesomeness/blog/posts/3", engine_object.post_path(id: 3, omg: "pure-awesomeness")
end
test "[OBJECT] generating application's route" do
@@ -286,7 +286,7 @@ module TestGenerationPrefix
end
test "[OBJECT] generating application's route includes default_url_options[:script_name]" do
- RailsApplication.routes.default_url_options = {:script_name => "/something"}
+ RailsApplication.routes.default_url_options = { script_name: "/something" }
assert_equal "/something/", app_object.root_path
end
@@ -296,11 +296,11 @@ module TestGenerationPrefix
end
test "[OBJECT] generating engine's route with url_for" do
- path = engine_object.url_for(:controller => "inside_engine_generating",
- :action => "show",
- :only_path => true,
- :omg => "omg",
- :id => 1)
+ path = engine_object.url_for(controller: "inside_engine_generating",
+ action: "show",
+ only_path: true,
+ omg: "omg",
+ id: 1)
assert_equal "/omg/blog/posts/1", path
end
@@ -308,7 +308,7 @@ module TestGenerationPrefix
path = engine_object.posts_path
assert_equal "/awesome/blog/posts", path
- path = engine_object.posts_url(:host => "example.com")
+ path = engine_object.posts_url(host: "example.com")
assert_equal "http://example.com/awesome/blog/posts", path
end
@@ -316,45 +316,43 @@ module TestGenerationPrefix
path = engine_object.polymorphic_path(Post.new)
assert_equal "/awesome/blog/posts/1", path
- path = engine_object.polymorphic_url(Post.new, :host => "www.example.com")
+ path = engine_object.polymorphic_url(Post.new, host: "www.example.com")
assert_equal "http://www.example.com/awesome/blog/posts/1", path
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)
%(<html><body>You are being <a href="#{url}">redirected</a>.</body></html>)
end
end
class EngineMountedAtRoot < ActionDispatch::IntegrationTest
- include Rack::Test::Methods
-
class BlogEngine
def self.routes
@routes ||= begin
routes = ActionDispatch::Routing::RouteSet.new
routes.draw do
- get "/posts/:id", :to => "posts#show", :as => :post
-
- get "/relative_path_root", :to => redirect("")
- get "/relative_path_redirect", :to => redirect("foo")
- get "/relative_option_root", :to => redirect(:path => "")
- get "/relative_option_redirect", :to => redirect(:path => "foo")
- get "/relative_custom_root", :to => redirect { |params, request| "" }
- get "/relative_custom_redirect", :to => redirect { |params, request| "foo" }
-
- get "/absolute_path_root", :to => redirect("/")
- get "/absolute_path_redirect", :to => redirect("/foo")
- get "/absolute_option_root", :to => redirect(:path => "/")
- get "/absolute_option_redirect", :to => redirect(:path => "/foo")
- get "/absolute_custom_root", :to => redirect { |params, request| "/" }
- get "/absolute_custom_redirect", :to => redirect { |params, request| "/foo" }
+ get "/posts/:id", to: "posts#show", as: :post
+
+ get "/relative_path_root", to: redirect("")
+ get "/relative_path_redirect", to: redirect("foo")
+ get "/relative_option_root", to: redirect(path: "")
+ get "/relative_option_redirect", to: redirect(path: "foo")
+ get "/relative_custom_root", to: redirect { |params, request| "" }
+ get "/relative_custom_redirect", to: redirect { |params, request| "foo" }
+
+ get "/absolute_path_root", to: redirect("/")
+ get "/absolute_path_redirect", to: redirect("/foo")
+ get "/absolute_option_root", to: redirect(path: "/")
+ get "/absolute_option_redirect", to: redirect(path: "/foo")
+ get "/absolute_custom_root", to: redirect { |params, request| "/" }
+ get "/absolute_custom_redirect", to: redirect { |params, request| "/foo" }
end
routes
@@ -362,7 +360,7 @@ module TestGenerationPrefix
end
def self.call(env)
- env['action_dispatch.routes'] = routes
+ env["action_dispatch.routes"] = routes
routes.call(env)
end
end
@@ -378,7 +376,7 @@ module TestGenerationPrefix
include RailsApplication.routes.mounted_helpers
def show
- render :text => post_path(:id => params[:id])
+ render plain: post_path(id: params[:id])
end
end
@@ -388,76 +386,76 @@ 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)
%(<html><body>You are being <a href="#{url}">redirected</a>.</body></html>)
end
diff --git a/actionpack/test/dispatch/rack_cache_test.rb b/actionpack/test/dispatch/rack_cache_test.rb
index 79d8a64d29..86b375a2a8 100644
--- a/actionpack/test/dispatch/rack_cache_test.rb
+++ b/actionpack/test/dispatch/rack_cache_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'action_dispatch/http/rack_cache'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "action_dispatch/http/rack_cache"
class RackCacheMetaStoreTest < ActiveSupport::TestCase
class ReadWriteHash < ::Hash
@@ -12,7 +14,7 @@ class RackCacheMetaStoreTest < ActiveSupport::TestCase
end
test "stuff is deep duped" do
- @store.write(:foo, { :bar => :original })
+ @store.write(:foo, bar: :original)
hash = @store.read(:foo)
hash[:bar] = :changed
hash = @store.read(:foo)
diff --git a/actionpack/test/dispatch/reloader_test.rb b/actionpack/test/dispatch/reloader_test.rb
index 62e8197e20..edc4cd62a3 100644
--- a/actionpack/test/dispatch/reloader_test.rb
+++ b/actionpack/test/dispatch/reloader_test.rb
@@ -1,28 +1,11 @@
-require 'abstract_unit'
+# frozen_string_literal: true
-class ReloaderTest < ActiveSupport::TestCase
- Reloader = ActionDispatch::Reloader
+require "abstract_unit"
+class ReloaderTest < ActiveSupport::TestCase
teardown do
- Reloader.reset_callbacks :prepare
- Reloader.reset_callbacks :cleanup
- 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
+ ActiveSupport::Reloader.reset_callbacks :prepare
+ ActiveSupport::Reloader.reset_callbacks :complete
end
class MyBody < Array
@@ -43,6 +26,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
@@ -60,9 +60,12 @@ class ReloaderTest < ActiveSupport::TestCase
def test_condition_specifies_when_to_reload
i, j = 0, 0, 0, 0
- Reloader.to_prepare { |*args| i += 1 }
- Reloader.to_cleanup { |*args| j += 1 }
- app = Reloader.new(lambda { |env| [200, {}, []] }, lambda { i < 3 })
+
+ reloader = reloader(lambda { i < 3 })
+ reloader.to_prepare { |*args| i += 1 }
+ reloader.to_complete { |*args| j += 1 }
+
+ app = middleware(lambda { |env| [200, {}, []] }, reloader)
5.times do
resp = app.call({})
resp[2].close
@@ -107,55 +110,31 @@ class ReloaderTest < ActiveSupport::TestCase
assert_respond_to body, :bar
end
- def test_cleanup_callbacks_are_called_when_body_is_closed
- cleaned = false
- Reloader.to_cleanup { cleaned = true }
+ 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_not completed
body.close
- assert cleaned
+ assert completed
end
def test_prepare_callbacks_arent_called_when_body_is_closed
prepared = false
- Reloader.to_prepare { prepared = true }
+ reloader.to_prepare { prepared = true }
body = call_and_return_body
prepared = false
body.close
- assert !prepared
+ assert_not prepared
end
- def test_manual_reloading
- prepared = cleaned = false
- Reloader.to_prepare { prepared = true }
- Reloader.to_cleanup { cleaned = true }
-
- Reloader.prepare!
- assert prepared
- assert !cleaned
-
- prepared = cleaned = false
- Reloader.cleanup!
- assert !prepared
- assert cleaned
- 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_cleanup_callbacks_are_called_on_exceptions
- cleaned = false
- Reloader.to_cleanup { cleaned = true }
+ def test_complete_callbacks_are_called_on_exceptions
+ completed = false
+ reloader.to_complete { completed = true }
begin
call_and_return_body do
@@ -164,13 +143,25 @@ class ReloaderTest < ActiveSupport::TestCase
rescue
end
- assert cleaned
+ assert completed
end
private
def call_and_return_body(&block)
- @response ||= 'response'
- @reloader ||= Reloader.new(block || proc {[200, {}, @response]})
- @reloader.call({'rack.input' => StringIO.new('')})[2]
+ 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
+
+ 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/json_params_parsing_test.rb b/actionpack/test/dispatch/request/json_params_parsing_test.rb
index d77341bc64..beab8e78b5 100644
--- a/actionpack/test/dispatch/request/json_params_parsing_test.rb
+++ b/actionpack/test/dispatch/request/json_params_parsing_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class JsonParamsParsingTest < ActionDispatch::IntegrationTest
class TestController < ActionController::Base
@@ -18,37 +20,44 @@ class JsonParamsParsingTest < ActionDispatch::IntegrationTest
test "parses json params for application json" do
assert_parses(
- {"person" => {"name" => "David"}},
- "{\"person\": {\"name\": \"David\"}}", { 'CONTENT_TYPE' => 'application/json' }
+ { "person" => { "name" => "David" } },
+ "{\"person\": {\"name\": \"David\"}}", "CONTENT_TYPE" => "application/json"
)
end
test "parses boolean and number json params for application json" do
assert_parses(
- {"item" => {"enabled" => false, "count" => 10}},
- "{\"item\": {\"enabled\": false, \"count\": 10}}", { 'CONTENT_TYPE' => 'application/json' }
+ { "item" => { "enabled" => false, "count" => 10 } },
+ "{\"item\": {\"enabled\": false, \"count\": 10}}", "CONTENT_TYPE" => "application/json"
)
end
test "parses json params for application jsonrequest" do
assert_parses(
- {"person" => {"name" => "David"}},
- "{\"person\": {\"name\": \"David\"}}", { 'CONTENT_TYPE' => 'application/jsonrequest' }
+ { "person" => { "name" => "David" } },
+ "{\"person\": {\"name\": \"David\"}}", "CONTENT_TYPE" => "application/jsonrequest"
+ )
+ end
+
+ test "does not parse unregistered media types such as application/vnd.api+json" do
+ assert_parses(
+ {},
+ "{\"person\": {\"name\": \"David\"}}", "CONTENT_TYPE" => "application/vnd.api+json"
)
end
test "nils are stripped from collections" do
assert_parses(
- {"person" => []},
- "{\"person\":[null]}", { 'CONTENT_TYPE' => 'application/json' }
+ { "person" => [] },
+ "{\"person\":[null]}", "CONTENT_TYPE" => "application/json"
)
assert_parses(
- {"person" => ['foo']},
- "{\"person\":[\"foo\",null]}", { 'CONTENT_TYPE' => 'application/json' }
+ { "person" => ["foo"] },
+ "{\"person\":[\"foo\",null]}", "CONTENT_TYPE" => "application/json"
)
assert_parses(
- {"person" => []},
- "{\"person\":[null, null]}", { 'CONTENT_TYPE' => 'application/json' }
+ { "person" => [] },
+ "{\"person\":[null, null]}", "CONTENT_TYPE" => "application/json"
)
end
@@ -56,7 +65,7 @@ class JsonParamsParsingTest < ActionDispatch::IntegrationTest
with_test_routing do
output = StringIO.new
json = "[\"person]\": {\"name\": \"David\"}}"
- post "/parse", params: json, headers: { 'CONTENT_TYPE' => 'application/json', 'action_dispatch.show_exceptions' => true, 'action_dispatch.logger' => ActiveSupport::Logger.new(output) }
+ post "/parse", params: json, headers: { "CONTENT_TYPE" => "application/json", "action_dispatch.show_exceptions" => true, "action_dispatch.logger" => ActiveSupport::Logger.new(output) }
assert_response :bad_request
output.rewind && err = output.read
assert err =~ /Error occurred while parsing request parameters/
@@ -68,18 +77,20 @@ class JsonParamsParsingTest < ActionDispatch::IntegrationTest
begin
$stderr = StringIO.new # suppress the log
json = "[\"person]\": {\"name\": \"David\"}}"
- exception = assert_raise(ActionDispatch::ParamsParser::ParseError) { post "/parse", json, {'CONTENT_TYPE' => 'application/json', 'action_dispatch.show_exceptions' => false} }
- assert_equal JSON::ParserError, exception.original_exception.class
- assert_equal exception.original_exception.message, exception.message
+ exception = assert_raise(ActionDispatch::Http::Parameters::ParseError) do
+ post "/parse", params: json, headers: { "CONTENT_TYPE" => "application/json", "action_dispatch.show_exceptions" => false }
+ end
+ assert_equal JSON::ParserError, exception.cause.class
+ assert_equal exception.cause.message, exception.message
ensure
$stderr = STDERR
end
end
end
- test 'raw_post is not empty for JSON request' do
+ test "raw_post is not empty for JSON request" do
with_test_routing do
- post '/parse', params: '{"posts": [{"title": "Post Title"}]}', headers: { 'CONTENT_TYPE' => 'application/json' }
+ post "/parse", params: '{"posts": [{"title": "Post Title"}]}', headers: { "CONTENT_TYPE" => "application/json" }
assert_equal '{"posts": [{"title": "Post Title"}]}', request.raw_post
end
end
@@ -96,7 +107,9 @@ class JsonParamsParsingTest < ActionDispatch::IntegrationTest
def with_test_routing
with_routing do |set|
set.draw do
- post ':action', :to => ::JsonParamsParsingTest::TestController
+ ActiveSupport::Deprecation.silence do
+ post ":action", to: ::JsonParamsParsingTest::TestController
+ end
end
yield
end
@@ -105,7 +118,7 @@ end
class RootLessJSONParamsParsingTest < ActionDispatch::IntegrationTest
class UsersController < ActionController::Base
- wrap_parameters :format => :json
+ wrap_parameters format: :json
class << self
attr_accessor :last_request_parameters, :last_parameters
@@ -113,7 +126,7 @@ class RootLessJSONParamsParsingTest < ActionDispatch::IntegrationTest
def parse
self.class.last_request_parameters = request.request_parameters
- self.class.last_parameters = params
+ self.class.last_parameters = params.to_unsafe_h
head :ok
end
end
@@ -124,39 +137,69 @@ class RootLessJSONParamsParsingTest < ActionDispatch::IntegrationTest
test "parses json params for application json" do
assert_parses(
- {"user" => {"username" => "sikachu"}, "username" => "sikachu"},
- "{\"username\": \"sikachu\"}", { 'CONTENT_TYPE' => 'application/json' }
+ { "user" => { "username" => "sikachu" }, "username" => "sikachu" },
+ "{\"username\": \"sikachu\"}", "CONTENT_TYPE" => "application/json"
)
end
test "parses json params for application jsonrequest" do
assert_parses(
- {"user" => {"username" => "sikachu"}, "username" => "sikachu"},
- "{\"username\": \"sikachu\"}", { 'CONTENT_TYPE' => 'application/jsonrequest' }
+ { "user" => { "username" => "sikachu" }, "username" => "sikachu" },
+ "{\"username\": \"sikachu\"}", "CONTENT_TYPE" => "application/jsonrequest"
)
end
test "parses json with non-object JSON content" do
assert_parses(
- {"user" => {"_json" => "string content" }, "_json" => "string content" },
- "\"string content\"", { 'CONTENT_TYPE' => 'application/json' }
+ { "user" => { "_json" => "string content" }, "_json" => "string content" },
+ "\"string content\"", "CONTENT_TYPE" => "application/json"
)
end
+ test "parses json params after custom json mime type registered" do
+ begin
+ Mime::Type.unregister :json
+ Mime::Type.register "application/json", :json, %w(application/vnd.rails+json)
+ assert_parses(
+ { "user" => { "username" => "meinac" }, "username" => "meinac" },
+ "{\"username\": \"meinac\"}", "CONTENT_TYPE" => "application/json"
+ )
+ ensure
+ Mime::Type.unregister :json
+ Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest )
+ end
+ end
+
+ test "parses json params after custom json mime type registered with synonym" do
+ begin
+ Mime::Type.unregister :json
+ Mime::Type.register "application/json", :json, %w(application/vnd.rails+json)
+ assert_parses(
+ { "user" => { "username" => "meinac" }, "username" => "meinac" },
+ "{\"username\": \"meinac\"}", "CONTENT_TYPE" => "application/vnd.rails+json"
+ )
+ ensure
+ Mime::Type.unregister :json
+ Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest )
+ end
+ end
+
private
def assert_parses(expected, actual, headers = {})
with_test_routing(UsersController) do
post "/parse", params: actual, headers: headers
assert_response :ok
assert_equal(expected, UsersController.last_request_parameters)
- assert_equal(expected.merge({"action" => "parse"}), UsersController.last_parameters)
+ assert_equal(expected.merge("action" => "parse"), UsersController.last_parameters)
end
end
def with_test_routing(controller)
with_routing do |set|
set.draw do
- post ':action', :to => controller
+ ActiveSupport::Deprecation.silence do
+ post ":action", to: controller
+ end
end
yield
end
diff --git a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb
index 50f69c53cb..da8233c074 100644
--- a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb
+++ b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class MultipartParamsParsingTest < ActionDispatch::IntegrationTest
class TestController < ActionController::Base
@@ -17,129 +19,140 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest
end
def read
- render :text => "File: #{params[:uploaded_data].read}"
+ render plain: "File: #{params[:uploaded_data].read}"
end
end
- FIXTURE_PATH = File.dirname(__FILE__) + '/../../fixtures/multipart'
+ FIXTURE_PATH = File.expand_path("../../fixtures/multipart", __dir__)
def teardown
TestController.last_request_parameters = nil
end
test "parses single parameter" do
- assert_equal({ 'foo' => 'bar' }, parse_multipart('single_parameter'))
+ assert_equal({ "foo" => "bar" }, parse_multipart("single_parameter"))
end
test "parses bracketed parameters" do
- assert_equal({ 'foo' => { 'baz' => 'bar'}}, parse_multipart('bracketed_param'))
+ assert_equal({ "foo" => { "baz" => "bar" } }, parse_multipart("bracketed_param"))
end
test "parse single utf8 parameter" do
- assert_equal({ 'Iñtërnâtiônàlizætiøn_name' => 'Iñtërnâtiônàlizætiøn_value'},
- parse_multipart('single_utf8_param'), "request.request_parameters")
+ assert_equal({ "Iñtërnâtiônàlizætiøn_name" => "Iñtërnâtiônàlizætiøn_value" },
+ parse_multipart("single_utf8_param"), "request.request_parameters")
assert_equal(
- 'Iñtërnâtiônàlizætiøn_value',
- TestController.last_parameters['Iñtërnâtiônàlizætiøn_name'], "request.parameters")
+ "Iñtërnâtiônàlizætiøn_value",
+ TestController.last_parameters["Iñtërnâtiônàlizætiøn_name"], "request.parameters")
end
test "parse bracketed utf8 parameter" do
- assert_equal({ 'Iñtërnâtiônàlizætiøn_name' => {
- 'Iñtërnâtiônàlizætiøn_nested_name' => 'Iñtërnâtiônàlizætiøn_value'} },
- parse_multipart('bracketed_utf8_param'), "request.request_parameters")
+ assert_equal({ "Iñtërnâtiônàlizætiøn_name" => {
+ "Iñtërnâtiônàlizætiøn_nested_name" => "Iñtërnâtiônàlizætiøn_value" } },
+ parse_multipart("bracketed_utf8_param"), "request.request_parameters")
assert_equal(
- {'Iñtërnâtiônàlizætiøn_nested_name' => 'Iñtërnâtiônàlizætiøn_value'},
- TestController.last_parameters['Iñtërnâtiônàlizætiøn_name'], "request.parameters")
+ { "Iñtërnâtiônàlizætiøn_nested_name" => "Iñtërnâtiônàlizætiøn_value" },
+ TestController.last_parameters["Iñtërnâtiônàlizætiøn_name"], "request.parameters")
end
test "parses text file" do
- params = parse_multipart('text_file')
+ params = parse_multipart("text_file")
assert_equal %w(file foo), params.keys.sort
- assert_equal 'bar', params['foo']
+ assert_equal "bar", params["foo"]
- file = params['file']
- assert_equal 'file.txt', file.original_filename
+ file = params["file"]
+ assert_equal "file.txt", file.original_filename
assert_equal "text/plain", file.content_type
- assert_equal 'contents', file.read
+ assert_equal "contents", file.read
+ end
+
+ test "parses utf8 filename with percent character" do
+ params = parse_multipart("utf8_filename")
+ assert_equal %w(file foo), params.keys.sort
+ assert_equal "bar", params["foo"]
+
+ file = params["file"]
+ assert_equal "ファイル%名.txt", file.original_filename
+ assert_equal "text/plain", file.content_type
+ assert_equal "contents", file.read
end
test "parses boundary problem file" do
- params = parse_multipart('boundary_problem_file')
+ params = parse_multipart("boundary_problem_file")
assert_equal %w(file foo), params.keys.sort
- file = params['file']
- foo = params['foo']
+ file = params["file"]
+ foo = params["foo"]
- assert_equal 'file.txt', file.original_filename
+ assert_equal "file.txt", file.original_filename
assert_equal "text/plain", file.content_type
- assert_equal 'bar', foo
+ assert_equal "bar", foo
end
test "parses large text file" do
- params = parse_multipart('large_text_file')
+ params = parse_multipart("large_text_file")
assert_equal %w(file foo), params.keys.sort
- assert_equal 'bar', params['foo']
+ assert_equal "bar", params["foo"]
- file = params['file']
+ file = params["file"]
- assert_equal 'file.txt', file.original_filename
+ assert_equal "file.txt", file.original_filename
assert_equal "text/plain", file.content_type
- assert_equal(('a' * 20480), file.read)
+ assert_equal(("a" * 20480), file.read)
end
test "parses binary file" do
- params = parse_multipart('binary_file')
+ params = parse_multipart("binary_file")
assert_equal %w(file flowers foo), params.keys.sort
- assert_equal 'bar', params['foo']
+ assert_equal "bar", params["foo"]
- file = params['file']
- assert_equal 'file.csv', file.original_filename
+ file = params["file"]
+ assert_equal "file.csv", file.original_filename
assert_nil file.content_type
- assert_equal 'contents', file.read
+ assert_equal "contents", file.read
- file = params['flowers']
- assert_equal 'flowers.jpg', file.original_filename
+ file = params["flowers"]
+ assert_equal "flowers.jpg", file.original_filename
assert_equal "image/jpeg", file.content_type
assert_equal 19512, file.size
end
test "parses mixed files" do
- params = parse_multipart('mixed_files')
+ params = parse_multipart("mixed_files")
assert_equal %w(files foo), params.keys.sort
- assert_equal 'bar', params['foo']
+ assert_equal "bar", params["foo"]
# Rack doesn't handle multipart/mixed for us.
- files = params['files']
+ files = params["files"]
assert_equal 19756, files.bytesize
end
test "does not create tempfile if no file has been selected" do
- params = parse_multipart('none')
+ params = parse_multipart("none")
assert_equal %w(submit-name), params.keys.sort
- assert_equal 'Larry', params['submit-name']
- assert_equal nil, params['files']
+ assert_equal "Larry", params["submit-name"]
+ assert_nil params["files"]
end
test "parses empty upload file" do
- params = parse_multipart('empty')
+ params = parse_multipart("empty")
assert_equal %w(files submit-name), params.keys.sort
- assert_equal 'Larry', params['submit-name']
- assert params['files']
- assert_equal "", params['files'].read
+ assert_equal "Larry", params["submit-name"]
+ assert params["files"]
+ assert_equal "", params["files"].read
end
test "uploads and reads binary file" do
with_test_routing do
- fixture = FIXTURE_PATH + "/mona_lisa.jpg"
- params = { :uploaded_data => fixture_file_upload(fixture, "image/jpg") }
- post '/read', params: params
+ fixture = FIXTURE_PATH + "/ruby_on_rails.jpg"
+ params = { uploaded_data: fixture_file_upload(fixture, "image/jpg") }
+ post "/read", params: params
end
end
test "uploads and reads file" do
with_test_routing do
- post '/read', params: { uploaded_data: fixture_file_upload(FIXTURE_PATH + "/hello.txt", "text/plain") }
+ post "/read", params: { uploaded_data: fixture_file_upload(FIXTURE_PATH + "/hello.txt", "text/plain") }
assert_equal "File: Hello", response.body
end
end
@@ -148,7 +161,9 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest
test "does not raise EOFError on GET request with multipart content-type" do
with_routing do |set|
set.draw do
- get ':action', controller: 'multipart_params_parsing_test/test'
+ ActiveSupport::Deprecation.silence do
+ get ":action", controller: "multipart_params_parsing_test/test"
+ end
end
headers = { "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x" }
get "/parse", headers: headers
@@ -158,7 +173,7 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest
private
def fixture(name)
- File.open(File.join(FIXTURE_PATH, name), 'rb') do |file|
+ File.open(File.join(FIXTURE_PATH, name), "rb") do |file|
{ "rack.input" => file.read,
"CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x",
"CONTENT_LENGTH" => file.stat.size.to_s }
@@ -177,7 +192,9 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest
def with_test_routing
with_routing do |set|
set.draw do
- post ':action', :controller => 'multipart_params_parsing_test/test'
+ ActiveSupport::Deprecation.silence do
+ post ":action", controller: "multipart_params_parsing_test/test"
+ end
end
yield
end
diff --git a/actionpack/test/dispatch/request/query_string_parsing_test.rb b/actionpack/test/dispatch/request/query_string_parsing_test.rb
index bc6716525e..f9ae5ef4e8 100644
--- a/actionpack/test/dispatch/request/query_string_parsing_test.rb
+++ b/actionpack/test/dispatch/request/query_string_parsing_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class QueryStringParsingTest < ActionDispatch::IntegrationTest
class TestController < ActionController::Base
@@ -29,92 +31,92 @@ class QueryStringParsingTest < ActionDispatch::IntegrationTest
test "query string" do
assert_parses(
- {"action" => "create_customer", "full_name" => "David Heinemeier Hansson", "customerId" => "1"},
+ { "action" => "create_customer", "full_name" => "David Heinemeier Hansson", "customerId" => "1" },
"action=create_customer&full_name=David%20Heinemeier%20Hansson&customerId=1"
)
end
test "deep query string" do
assert_parses(
- {'x' => {'y' => {'z' => '10'}}},
+ { "x" => { "y" => { "z" => "10" } } },
"x[y][z]=10"
)
end
test "deep query string with array" do
- assert_parses({'x' => {'y' => {'z' => ['10']}}}, 'x[y][z][]=10')
- assert_parses({'x' => {'y' => {'z' => ['10', '5']}}}, 'x[y][z][]=10&x[y][z][]=5')
+ assert_parses({ "x" => { "y" => { "z" => ["10"] } } }, "x[y][z][]=10")
+ assert_parses({ "x" => { "y" => { "z" => ["10", "5"] } } }, "x[y][z][]=10&x[y][z][]=5")
end
test "deep query string with array of hash" do
- assert_parses({'x' => {'y' => [{'z' => '10'}]}}, 'x[y][][z]=10')
- assert_parses({'x' => {'y' => [{'z' => '10', 'w' => '10'}]}}, 'x[y][][z]=10&x[y][][w]=10')
- assert_parses({'x' => {'y' => [{'z' => '10', 'v' => {'w' => '10'}}]}}, 'x[y][][z]=10&x[y][][v][w]=10')
+ assert_parses({ "x" => { "y" => [{ "z" => "10" }] } }, "x[y][][z]=10")
+ assert_parses({ "x" => { "y" => [{ "z" => "10", "w" => "10" }] } }, "x[y][][z]=10&x[y][][w]=10")
+ assert_parses({ "x" => { "y" => [{ "z" => "10", "v" => { "w" => "10" } }] } }, "x[y][][z]=10&x[y][][v][w]=10")
end
test "deep query string with array of hashes with one pair" do
- assert_parses({'x' => {'y' => [{'z' => '10'}, {'z' => '20'}]}}, 'x[y][][z]=10&x[y][][z]=20')
+ assert_parses({ "x" => { "y" => [{ "z" => "10" }, { "z" => "20" }] } }, "x[y][][z]=10&x[y][][z]=20")
end
test "deep query string with array of hashes with multiple pairs" do
assert_parses(
- {'x' => {'y' => [{'z' => '10', 'w' => 'a'}, {'z' => '20', 'w' => 'b'}]}},
- 'x[y][][z]=10&x[y][][w]=a&x[y][][z]=20&x[y][][w]=b'
+ { "x" => { "y" => [{ "z" => "10", "w" => "a" }, { "z" => "20", "w" => "b" }] } },
+ "x[y][][z]=10&x[y][][w]=a&x[y][][z]=20&x[y][][w]=b"
)
end
test "query string with nil" do
assert_parses(
- { "action" => "create_customer", "full_name" => ''},
+ { "action" => "create_customer", "full_name" => "" },
"action=create_customer&full_name="
)
end
test "query string with array" do
assert_parses(
- { "action" => "create_customer", "selected" => ["1", "2", "3"]},
+ { "action" => "create_customer", "selected" => ["1", "2", "3"] },
"action=create_customer&selected[]=1&selected[]=2&selected[]=3"
)
end
test "query string with amps" do
assert_parses(
- { "action" => "create_customer", "name" => "Don't & Does"},
+ { "action" => "create_customer", "name" => "Don't & Does" },
"action=create_customer&name=Don%27t+%26+Does"
)
end
test "query string with many equal" do
assert_parses(
- { "action" => "create_customer", "full_name" => "abc=def=ghi"},
+ { "action" => "create_customer", "full_name" => "abc=def=ghi" },
"action=create_customer&full_name=abc=def=ghi"
)
end
test "query string without equal" do
- assert_parses({"action" => nil}, "action")
- assert_parses({"action" => {"foo" => nil}}, "action[foo]")
- assert_parses({"action" => {"foo" => { "bar" => nil }}}, "action[foo][bar]")
- assert_parses({"action" => {"foo" => { "bar" => [] }}}, "action[foo][bar][]")
- assert_parses({"action" => {"foo" => [] }}, "action[foo][]")
- assert_parses({"action"=>{"foo"=>[{"bar"=>nil}]}}, "action[foo][][bar]")
+ assert_parses({ "action" => nil }, "action")
+ assert_parses({ "action" => { "foo" => nil } }, "action[foo]")
+ assert_parses({ "action" => { "foo" => { "bar" => nil } } }, "action[foo][bar]")
+ assert_parses({ "action" => { "foo" => { "bar" => [] } } }, "action[foo][bar][]")
+ assert_parses({ "action" => { "foo" => [] } }, "action[foo][]")
+ assert_parses({ "action" => { "foo" => [{ "bar" => nil }] } }, "action[foo][][bar]")
end
def test_array_parses_without_nil
- assert_parses({"action" => ['1']}, "action[]=1&action[]")
+ assert_parses({ "action" => ["1"] }, "action[]=1&action[]")
end
test "perform_deep_munge" do
old_perform_deep_munge = ActionDispatch::Request::Utils.perform_deep_munge
ActionDispatch::Request::Utils.perform_deep_munge = false
begin
- assert_parses({"action" => nil}, "action")
- assert_parses({"action" => {"foo" => nil}}, "action[foo]")
- assert_parses({"action" => {"foo" => {"bar" => nil}}}, "action[foo][bar]")
- assert_parses({"action" => {"foo" => {"bar" => [nil]}}}, "action[foo][bar][]")
- assert_parses({"action" => {"foo" => [nil]}}, "action[foo][]")
- assert_parses({"action" => {"foo" => [{"bar" => nil}]}}, "action[foo][][bar]")
- assert_parses({"action" => ['1',nil]}, "action[]=1&action[]")
+ assert_parses({ "action" => nil }, "action")
+ assert_parses({ "action" => { "foo" => nil } }, "action[foo]")
+ assert_parses({ "action" => { "foo" => { "bar" => nil } } }, "action[foo][bar]")
+ assert_parses({ "action" => { "foo" => { "bar" => [nil] } } }, "action[foo][bar][]")
+ assert_parses({ "action" => { "foo" => [nil] } }, "action[foo][]")
+ assert_parses({ "action" => { "foo" => [{ "bar" => nil }] } }, "action[foo][][bar]")
+ assert_parses({ "action" => ["1", nil] }, "action[]=1&action[]")
ensure
ActionDispatch::Request::Utils.perform_deep_munge = old_perform_deep_munge
end
@@ -129,14 +131,14 @@ class QueryStringParsingTest < ActionDispatch::IntegrationTest
test "query string with many ampersands" do
assert_parses(
- { "action" => "create_customer", "full_name" => "David Heinemeier Hansson"},
+ { "action" => "create_customer", "full_name" => "David Heinemeier Hansson" },
"&action=create_customer&&&full_name=David%20Heinemeier%20Hansson"
)
end
test "unbalanced query string with array" do
assert_parses(
- {'location' => ["1", "2"], 'age_group' => ["2"]},
+ { "location" => ["1", "2"], "age_group" => ["2"] },
"location[]=1&location[]=2&age_group[]=2"
)
end
@@ -144,7 +146,9 @@ class QueryStringParsingTest < ActionDispatch::IntegrationTest
test "ambiguous query string returns a bad request" do
with_routing do |set|
set.draw do
- get ':action', :to => ::QueryStringParsingTest::TestController
+ ActiveSupport::Deprecation.silence do
+ get ":action", to: ::QueryStringParsingTest::TestController
+ end
end
get "/parse", headers: { "QUERY_STRING" => "foo[]=bar&foo[4]=bar" }
@@ -156,7 +160,9 @@ class QueryStringParsingTest < ActionDispatch::IntegrationTest
def assert_parses(expected, actual)
with_routing do |set|
set.draw do
- get ':action', :to => ::QueryStringParsingTest::TestController
+ ActiveSupport::Deprecation.silence do
+ get ":action", to: ::QueryStringParsingTest::TestController
+ end
end
@app = self.class.build_app(set) do |middleware|
middleware.use(EarlyParse)
diff --git a/actionpack/test/dispatch/request/session_test.rb b/actionpack/test/dispatch/request/session_test.rb
index 10fb04e230..74da2fe7d3 100644
--- a/actionpack/test/dispatch/request/session_test.rb
+++ b/actionpack/test/dispatch/request/session_test.rb
@@ -1,44 +1,49 @@
-require 'abstract_unit'
-require 'action_dispatch/middleware/session/abstract_store'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "action_dispatch/middleware/session/abstract_store"
module ActionDispatch
class Request
class SessionTest < ActiveSupport::TestCase
+ attr_reader :req
+
+ def setup
+ @req = ActionDispatch::Request.empty
+ end
+
def test_create_adds_itself_to_env
- env = {}
- s = Session.create(store, env, {})
- assert_equal s, env[Rack::Session::Abstract::ENV_SESSION_KEY]
+ s = Session.create(store, req, {})
+ assert_equal s, req.env[Rack::RACK_SESSION]
end
def test_to_hash
- env = {}
- s = Session.create(store, env, {})
- s['foo'] = 'bar'
- assert_equal 'bar', s['foo']
- assert_equal({'foo' => 'bar'}, s.to_hash)
+ s = Session.create(store, req, {})
+ s["foo"] = "bar"
+ assert_equal "bar", s["foo"]
+ assert_equal({ "foo" => "bar" }, s.to_hash)
+ assert_equal({ "foo" => "bar" }, s.to_h)
end
def test_create_merges_old
- env = {}
- s = Session.create(store, env, {})
- s['foo'] = 'bar'
+ s = Session.create(store, req, {})
+ s["foo"] = "bar"
- s1 = Session.create(store, env, {})
+ s1 = Session.create(store, req, {})
assert_not_equal s, s1
- assert_equal 'bar', s1['foo']
+ assert_equal "bar", s1["foo"]
end
def test_find
- env = {}
- assert_nil Session.find(env)
+ assert_nil Session.find(req)
- s = Session.create(store, env, {})
- assert_equal s, Session.find(env)
+ s = Session.create(store, req, {})
+ assert_equal s, Session.find(req)
end
def test_destroy
- s = Session.create(store, {}, {})
- s['rails'] = 'ftw'
+ s = Session.create(store, req, {})
+ s["rails"] = "ftw"
s.destroy
@@ -46,70 +51,126 @@ module ActionDispatch
end
def test_keys
- s = Session.create(store, {}, {})
- s['rails'] = 'ftw'
- s['adequate'] = 'awesome'
+ s = Session.create(store, req, {})
+ s["rails"] = "ftw"
+ s["adequate"] = "awesome"
assert_equal %w[rails adequate], s.keys
end
+ def test_keys_with_deferred_loading
+ s = Session.create(store_with_data, req, {})
+ assert_equal %w[sample_key], s.keys
+ end
+
def test_values
- s = Session.create(store, {}, {})
- s['rails'] = 'ftw'
- s['adequate'] = 'awesome'
+ s = Session.create(store, req, {})
+ s["rails"] = "ftw"
+ s["adequate"] = "awesome"
assert_equal %w[ftw awesome], s.values
end
+ def test_values_with_deferred_loading
+ s = Session.create(store_with_data, req, {})
+ assert_equal %w[sample_value], s.values
+ end
+
def test_clear
- s = Session.create(store, {}, {})
- s['rails'] = 'ftw'
- s['adequate'] = 'awesome'
+ s = Session.create(store, req, {})
+ s["rails"] = "ftw"
+ s["adequate"] = "awesome"
s.clear
assert_empty(s.values)
end
def test_update
- s = Session.create(store, {}, {})
- s['rails'] = 'ftw'
+ s = Session.create(store, req, {})
+ s["rails"] = "ftw"
- s.update(:rails => 'awesome')
+ s.update(rails: "awesome")
- assert_equal(['rails'], s.keys)
- assert_equal('awesome', s['rails'])
+ assert_equal(["rails"], s.keys)
+ assert_equal("awesome", s["rails"])
end
def test_delete
- s = Session.create(store, {}, {})
- s['rails'] = 'ftw'
+ s = Session.create(store, req, {})
+ s["rails"] = "ftw"
- s.delete('rails')
+ s.delete("rails")
assert_empty(s.keys)
end
def test_fetch
- session = Session.create(store, {}, {})
+ session = Session.create(store, req, {})
- session['one'] = '1'
- assert_equal '1', session.fetch(:one)
+ session["one"] = "1"
+ assert_equal "1", session.fetch(:one)
- assert_equal '2', session.fetch(:two, '2')
+ assert_equal "2", session.fetch(:two, "2")
assert_nil session.fetch(:two, nil)
- assert_equal 'three', session.fetch(:three) {|el| el.to_s }
+ assert_equal "three", session.fetch(:three) { |el| el.to_s }
assert_raise KeyError do
session.fetch(:three)
end
end
+ def test_dig
+ session = Session.create(store, req, {})
+ session["one"] = { "two" => "3" }
+
+ assert_equal "3", session.dig("one", "two")
+ assert_equal "3", session.dig(:one, "two")
+
+ assert_nil session.dig("three", "two")
+ assert_nil session.dig("one", "three")
+ assert_nil session.dig("one", :two)
+ end
+
private
- def store
- Class.new {
- def load_session(env); [1, {}]; end
- def session_exists?(env); true; end
- def destroy_session(env, id, options); 123; end
- }.new
+ def store
+ Class.new {
+ def load_session(env); [1, {}]; end
+ def session_exists?(env); true; end
+ def delete_session(env, id, options); 123; end
+ }.new
+ end
+
+ def store_with_data
+ Class.new {
+ def load_session(env); [1, { "sample_key" => "sample_value" }]; end
+ def session_exists?(env); true; end
+ def delete_session(env, id, options); 123; end
+ }.new
+ end
+ end
+
+ class SessionIntegrationTest < ActionDispatch::IntegrationTest
+ class MySessionApp
+ def call(env)
+ request = Rack::Request.new(env)
+ request.session["hello"] = "Hello from MySessionApp!"
+ [ 200, {}, ["Hello from MySessionApp!"] ]
+ end
+ end
+
+ Router = ActionDispatch::Routing::RouteSet.new
+ Router.draw do
+ get "/mysessionapp" => MySessionApp.new
+ end
+
+ def app
+ @app ||= RoutedRackApp.new(Router)
+ end
+
+ def test_session_follows_rack_api_contract_1
+ get "/mysessionapp"
+ assert_response :ok
+ assert_equal "Hello from MySessionApp!", @response.body
+ assert_equal "Hello from MySessionApp!", session["hello"]
end
end
end
diff --git a/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb b/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb
index 365edf849a..9e55a7242e 100644
--- a/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb
+++ b/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest
class TestController < ActionController::Base
@@ -18,7 +20,7 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest
test "parses unbalanced query string with array" do
query = "location[]=1&location[]=2&age_group[]=2"
- expected = { 'location' => ["1", "2"], 'age_group' => ["2"] }
+ expected = { "location" => ["1", "2"], "age_group" => ["2"] }
assert_parses expected, query
end
@@ -55,7 +57,7 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest
"products[second]=Pc",
"=Save"
].join("&")
- expected = {
+ expected = {
"customers" => {
"boston" => {
"first" => {
@@ -107,7 +109,7 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest
query = [
"customers[boston][first][name]=David",
"something_else=blah",
- "logo=#{File.expand_path(__FILE__)}"
+ "logo=#{__FILE__}"
].join("&")
expected = {
"customers" => {
@@ -118,7 +120,7 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest
}
},
"something_else" => "blah",
- "logo" => File.expand_path(__FILE__),
+ "logo" => __FILE__,
}
assert_parses expected, query
end
@@ -140,7 +142,9 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest
def with_test_routing
with_routing do |set|
set.draw do
- post ':action', to: ::UrlEncodedParamsParsingTest::TestController
+ ActiveSupport::Deprecation.silence do
+ post ":action", to: ::UrlEncodedParamsParsingTest::TestController
+ end
end
yield
end
diff --git a/actionpack/test/dispatch/request_id_test.rb b/actionpack/test/dispatch/request_id_test.rb
index 00d8caf8f4..9df4712dab 100644
--- a/actionpack/test/dispatch/request_id_test.rb
+++ b/actionpack/test/dispatch/request_id_test.rb
@@ -1,16 +1,23 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class RequestIdTest < ActiveSupport::TestCase
test "passing on the request id from the outside" do
- assert_equal "external-uu-rid", stub_request('HTTP_X_REQUEST_ID' => 'external-uu-rid').request_id
+ assert_equal "external-uu-rid", stub_request("HTTP_X_REQUEST_ID" => "external-uu-rid").request_id
end
test "ensure that only alphanumeric uurids are accepted" do
- assert_equal "X-Hacked-HeaderStuff", stub_request('HTTP_X_REQUEST_ID' => '; X-Hacked-Header: Stuff').request_id
+ assert_equal "X-Hacked-HeaderStuff", stub_request("HTTP_X_REQUEST_ID" => "; X-Hacked-Header: Stuff").request_id
+ end
+
+ test "accept Apache mod_unique_id format" do
+ mod_unique_id = "abcxyz@ABCXYZ-0123456789"
+ assert_equal mod_unique_id, stub_request("HTTP_X_REQUEST_ID" => mod_unique_id).request_id
end
test "ensure that 255 char limit on the request id is being enforced" do
- assert_equal "X" * 255, stub_request('HTTP_X_REQUEST_ID' => 'X' * 500).request_id
+ assert_equal "X" * 255, stub_request("HTTP_X_REQUEST_ID" => "X" * 500).request_id
end
test "generating a request id when none is supplied" do
@@ -18,15 +25,15 @@ class RequestIdTest < ActiveSupport::TestCase
end
test "uuid alias" do
- assert_equal "external-uu-rid", stub_request('HTTP_X_REQUEST_ID' => 'external-uu-rid').uuid
+ assert_equal "external-uu-rid", stub_request("HTTP_X_REQUEST_ID" => "external-uu-rid").uuid
end
private
- def stub_request(env = {})
- ActionDispatch::RequestId.new(lambda { |environment| [ 200, environment, [] ] }).call(env)
- ActionDispatch::Request.new(env)
- end
+ def stub_request(env = {})
+ ActionDispatch::RequestId.new(lambda { |environment| [ 200, environment, [] ] }).call(env)
+ ActionDispatch::Request.new(env)
+ end
end
class RequestIdResponseTest < ActionDispatch::IntegrationTest
@@ -38,32 +45,31 @@ class RequestIdResponseTest < ActionDispatch::IntegrationTest
test "request id is passed all the way to the response" do
with_test_route_set do
- get '/'
+ get "/"
assert_match(/\w+/, @response.headers["X-Request-Id"])
end
end
test "request id given on request is passed all the way to the response" do
with_test_route_set do
- get '/', headers: { 'HTTP_X_REQUEST_ID' => 'X' * 500 }
+ get "/", headers: { "HTTP_X_REQUEST_ID" => "X" * 500 }
assert_equal "X" * 255, @response.headers["X-Request-Id"]
end
end
-
private
- def with_test_route_set
- with_routing do |set|
- set.draw do
- get '/', :to => ::RequestIdResponseTest::TestController.action(:index)
- end
+ def with_test_route_set
+ with_routing do |set|
+ set.draw do
+ get "/", to: ::RequestIdResponseTest::TestController.action(:index)
+ end
- @app = self.class.build_app(set) do |middleware|
- middleware.use ActionDispatch::RequestId
- end
+ @app = self.class.build_app(set) do |middleware|
+ middleware.use ActionDispatch::RequestId
+ end
- yield
+ yield
+ end
end
- end
end
diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb
index ff63c10e8d..9d1246b3a4 100644
--- a/actionpack/test/dispatch/request_test.rb
+++ b/actionpack/test/dispatch/request_test.rb
@@ -1,27 +1,33 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class BaseRequestTest < ActiveSupport::TestCase
def setup
@env = {
:ip_spoofing_check => true,
- :tld_length => 1,
"rack.input" => "foo"
}
+ @original_tld_length = ActionDispatch::Http::URL.tld_length
+ end
+
+ def teardown
+ ActionDispatch::Http::URL.tld_length = @original_tld_length
end
def url_for(options = {})
- options = { host: 'www.example.com' }.merge!(options)
+ options = { host: "www.example.com" }.merge!(options)
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
ip_app = ActionDispatch::RemoteIp.new(Proc.new { }, ip_spoofing_check, @trusted_proxies)
- tld_length = env.key?(:tld_length) ? env.delete(:tld_length) : 1
+ ActionDispatch::Http::URL.tld_length = env.delete(:tld_length) if env.key?(:tld_length)
+
ip_app.call(env)
- ActionDispatch::Http::URL.tld_length = tld_length
env = @env.merge(env)
ActionDispatch::Request.new(env)
@@ -30,141 +36,141 @@ end
class RequestUrlFor < BaseRequestTest
test "url_for class method" do
- e = assert_raise(ArgumentError) { url_for(:host => nil) }
+ e = assert_raise(ArgumentError) { url_for(host: nil) }
assert_match(/Please provide the :host parameter/, e.message)
- assert_equal '/books', url_for(:only_path => true, :path => '/books')
-
- assert_equal 'http://www.example.com/books/?q=code', url_for(trailing_slash: true, path: '/books?q=code')
- assert_equal 'http://www.example.com/books/?spareslashes=////', url_for(trailing_slash: true, path: '/books?spareslashes=////')
-
- assert_equal 'http://www.example.com', url_for
- assert_equal 'http://api.example.com', url_for(:subdomain => 'api')
- assert_equal 'http://example.com', url_for(:subdomain => false)
- assert_equal 'http://www.ror.com', url_for(:domain => 'ror.com')
- assert_equal 'http://api.ror.co.uk', url_for(:host => 'www.ror.co.uk', :subdomain => 'api', :tld_length => 2)
- assert_equal 'http://www.example.com:8080', url_for(:port => 8080)
- assert_equal 'https://www.example.com', url_for(:protocol => 'https')
- assert_equal 'http://www.example.com/docs', url_for(:path => '/docs')
- assert_equal 'http://www.example.com#signup', url_for(:anchor => 'signup')
- assert_equal 'http://www.example.com/', url_for(:trailing_slash => true)
- assert_equal 'http://dhh:supersecret@www.example.com', url_for(:user => 'dhh', :password => 'supersecret')
- assert_equal 'http://www.example.com?search=books', url_for(:params => { :search => 'books' })
- assert_equal 'http://www.example.com?params=', url_for(:params => '')
- assert_equal 'http://www.example.com?params=1', url_for(:params => 1)
+ assert_equal "/books", url_for(only_path: true, path: "/books")
+
+ assert_equal "http://www.example.com/books/?q=code", url_for(trailing_slash: true, path: "/books?q=code")
+ assert_equal "http://www.example.com/books/?spareslashes=////", url_for(trailing_slash: true, path: "/books?spareslashes=////")
+
+ assert_equal "http://www.example.com", url_for
+ assert_equal "http://api.example.com", url_for(subdomain: "api")
+ assert_equal "http://example.com", url_for(subdomain: false)
+ assert_equal "http://www.ror.com", url_for(domain: "ror.com")
+ assert_equal "http://api.ror.co.uk", url_for(host: "www.ror.co.uk", subdomain: "api", tld_length: 2)
+ assert_equal "http://www.example.com:8080", url_for(port: 8080)
+ assert_equal "https://www.example.com", url_for(protocol: "https")
+ assert_equal "http://www.example.com/docs", url_for(path: "/docs")
+ assert_equal "http://www.example.com#signup", url_for(anchor: "signup")
+ assert_equal "http://www.example.com/", url_for(trailing_slash: true)
+ assert_equal "http://dhh:supersecret@www.example.com", url_for(user: "dhh", password: "supersecret")
+ assert_equal "http://www.example.com?search=books", url_for(params: { search: "books" })
+ assert_equal "http://www.example.com?params=", url_for(params: "")
+ assert_equal "http://www.example.com?params=1", url_for(params: 1)
end
end
class RequestIP < BaseRequestTest
test "remote ip" do
- request = stub_request 'REMOTE_ADDR' => '1.2.3.4'
- assert_equal '1.2.3.4', request.remote_ip
+ request = stub_request "REMOTE_ADDR" => "1.2.3.4"
+ assert_equal "1.2.3.4", request.remote_ip
- request = stub_request 'REMOTE_ADDR' => '1.2.3.4,3.4.5.6'
- assert_equal '3.4.5.6', request.remote_ip
+ request = stub_request "REMOTE_ADDR" => "1.2.3.4,3.4.5.6"
+ assert_equal "3.4.5.6", request.remote_ip
- request = stub_request 'REMOTE_ADDR' => '1.2.3.4',
- 'HTTP_X_FORWARDED_FOR' => '3.4.5.6'
- assert_equal '3.4.5.6', request.remote_ip
+ request = stub_request "REMOTE_ADDR" => "1.2.3.4",
+ "HTTP_X_FORWARDED_FOR" => "3.4.5.6"
+ assert_equal "3.4.5.6", request.remote_ip
- request = stub_request 'REMOTE_ADDR' => '127.0.0.1',
- 'HTTP_X_FORWARDED_FOR' => '3.4.5.6'
- assert_equal '3.4.5.6', request.remote_ip
+ request = stub_request "REMOTE_ADDR" => "127.0.0.1",
+ "HTTP_X_FORWARDED_FOR" => "3.4.5.6"
+ assert_equal "3.4.5.6", request.remote_ip
- request = stub_request 'HTTP_X_FORWARDED_FOR' => '3.4.5.6,unknown'
- assert_equal '3.4.5.6', request.remote_ip
+ request = stub_request "HTTP_X_FORWARDED_FOR" => "3.4.5.6,unknown"
+ assert_equal "3.4.5.6", request.remote_ip
- request = stub_request 'HTTP_X_FORWARDED_FOR' => '3.4.5.6,172.16.0.1'
- assert_equal '3.4.5.6', request.remote_ip
+ request = stub_request "HTTP_X_FORWARDED_FOR" => "3.4.5.6,172.16.0.1"
+ assert_equal "3.4.5.6", request.remote_ip
- request = stub_request 'HTTP_X_FORWARDED_FOR' => '3.4.5.6,192.168.0.1'
- assert_equal '3.4.5.6', request.remote_ip
+ request = stub_request "HTTP_X_FORWARDED_FOR" => "3.4.5.6,192.168.0.1"
+ assert_equal "3.4.5.6", request.remote_ip
- request = stub_request 'HTTP_X_FORWARDED_FOR' => '3.4.5.6,10.0.0.1'
- assert_equal '3.4.5.6', request.remote_ip
+ request = stub_request "HTTP_X_FORWARDED_FOR" => "3.4.5.6,10.0.0.1"
+ assert_equal "3.4.5.6", request.remote_ip
- request = stub_request 'HTTP_X_FORWARDED_FOR' => '3.4.5.6, 10.0.0.1, 10.0.0.1'
- assert_equal '3.4.5.6', request.remote_ip
+ request = stub_request "HTTP_X_FORWARDED_FOR" => "3.4.5.6, 10.0.0.1, 10.0.0.1"
+ assert_equal "3.4.5.6", request.remote_ip
- request = stub_request 'HTTP_X_FORWARDED_FOR' => '3.4.5.6,127.0.0.1'
- assert_equal '3.4.5.6', request.remote_ip
+ request = stub_request "HTTP_X_FORWARDED_FOR" => "3.4.5.6,127.0.0.1"
+ 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
+ request = stub_request "HTTP_X_FORWARDED_FOR" => "unknown,192.168.0.1"
+ 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" => "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
+ request = stub_request "HTTP_X_FORWARDED_FOR" => "not_ip_address"
+ assert_nil request.remote_ip
end
test "remote ip spoof detection" do
- request = stub_request 'HTTP_X_FORWARDED_FOR' => '1.1.1.1',
- 'HTTP_CLIENT_IP' => '2.2.2.2'
+ request = stub_request "HTTP_X_FORWARDED_FOR" => "1.1.1.1",
+ "HTTP_CLIENT_IP" => "2.2.2.2"
e = assert_raise(ActionDispatch::RemoteIp::IpSpoofAttackError) {
request.remote_ip
}
assert_match(/IP spoofing attack/, e.message)
- assert_match(/HTTP_X_FORWARDED_FOR="1.1.1.1"/, e.message)
- assert_match(/HTTP_CLIENT_IP="2.2.2.2"/, e.message)
+ assert_match(/HTTP_X_FORWARDED_FOR="1\.1\.1\.1"/, e.message)
+ assert_match(/HTTP_CLIENT_IP="2\.2\.2\.2"/, e.message)
end
test "remote ip with spoof detection disabled" do
- request = stub_request 'HTTP_X_FORWARDED_FOR' => '1.1.1.1',
- 'HTTP_CLIENT_IP' => '2.2.2.2',
+ request = stub_request "HTTP_X_FORWARDED_FOR" => "1.1.1.1",
+ "HTTP_CLIENT_IP" => "2.2.2.2",
:ip_spoofing_check => false
- assert_equal '1.1.1.1', request.remote_ip
+ assert_equal "1.1.1.1", request.remote_ip
end
test "remote ip spoof protection ignores private addresses" do
- request = stub_request 'HTTP_X_FORWARDED_FOR' => '172.17.19.51',
- 'HTTP_CLIENT_IP' => '172.17.19.51',
- 'REMOTE_ADDR' => '1.1.1.1',
- 'HTTP_X_BLUECOAT_VIA' => 'de462e07a2db325e'
- assert_equal '1.1.1.1', request.remote_ip
+ request = stub_request "HTTP_X_FORWARDED_FOR" => "172.17.19.51",
+ "HTTP_CLIENT_IP" => "172.17.19.51",
+ "REMOTE_ADDR" => "1.1.1.1",
+ "HTTP_X_BLUECOAT_VIA" => "de462e07a2db325e"
+ assert_equal "1.1.1.1", request.remote_ip
end
test "remote ip v6" do
- request = stub_request 'REMOTE_ADDR' => '2001:0db8:85a3:0000:0000:8a2e:0370:7334'
- assert_equal '2001:0db8:85a3:0000:0000:8a2e:0370:7334', request.remote_ip
+ request = stub_request "REMOTE_ADDR" => "2001:0db8:85a3:0000:0000:8a2e:0370:7334"
+ assert_equal "2001:0db8:85a3:0000:0000:8a2e:0370:7334", request.remote_ip
- request = stub_request 'REMOTE_ADDR' => '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
+ request = stub_request "REMOTE_ADDR" => "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
- request = stub_request 'REMOTE_ADDR' => '2001:0db8:85a3:0000:0000:8a2e:0370:7334',
- 'HTTP_X_FORWARDED_FOR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329'
- assert_equal 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329', request.remote_ip
+ request = stub_request "REMOTE_ADDR" => "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
+ "HTTP_X_FORWARDED_FOR" => "fe80:0000:0000:0000:0202:b3ff:fe1e:8329"
+ assert_equal "fe80:0000:0000:0000:0202:b3ff:fe1e:8329", request.remote_ip
- request = stub_request 'REMOTE_ADDR' => '::1',
- 'HTTP_X_FORWARDED_FOR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329'
- assert_equal 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329', request.remote_ip
+ request = stub_request "REMOTE_ADDR" => "::1",
+ "HTTP_X_FORWARDED_FOR" => "fe80:0000:0000:0000:0202:b3ff:fe1e:8329"
+ assert_equal "fe80:0000:0000:0000:0202:b3ff:fe1e:8329", request.remote_ip
- request = stub_request 'HTTP_X_FORWARDED_FOR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329,unknown'
- assert_equal 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329', request.remote_ip
+ request = stub_request "HTTP_X_FORWARDED_FOR" => "fe80:0000:0000:0000:0202:b3ff:fe1e:8329,unknown"
+ assert_equal "fe80:0000:0000:0000:0202:b3ff:fe1e:8329", request.remote_ip
- request = stub_request 'HTTP_X_FORWARDED_FOR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329,::1'
- assert_equal 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329', request.remote_ip
+ request = stub_request "HTTP_X_FORWARDED_FOR" => "fe80:0000:0000:0000:0202:b3ff:fe1e:8329,::1"
+ assert_equal "fe80:0000:0000:0000:0202:b3ff:fe1e:8329", request.remote_ip
- request = stub_request 'HTTP_X_FORWARDED_FOR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329, ::1, ::1'
- assert_equal 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329', request.remote_ip
+ request = stub_request "HTTP_X_FORWARDED_FOR" => "fe80:0000:0000:0000:0202:b3ff:fe1e:8329, ::1, ::1"
+ 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
+ request = stub_request "HTTP_X_FORWARDED_FOR" => "unknown,::1"
+ 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
+ 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
- request = stub_request 'HTTP_X_FORWARDED_FOR' => 'FE00::, FDFF::'
- assert_equal 'FE00::', request.remote_ip
+ request = stub_request "HTTP_X_FORWARDED_FOR" => "FE00::, FDFF::"
+ assert_equal "FE00::", request.remote_ip
- request = stub_request 'HTTP_X_FORWARDED_FOR' => 'not_ip_address'
- assert_equal nil, request.remote_ip
+ request = stub_request "HTTP_X_FORWARDED_FOR" => "not_ip_address"
+ assert_nil request.remote_ip
end
test "remote ip v6 spoof detection" do
- request = stub_request 'HTTP_X_FORWARDED_FOR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329',
- 'HTTP_CLIENT_IP' => '2001:0db8:85a3:0000:0000:8a2e:0370:7334'
+ request = stub_request "HTTP_X_FORWARDED_FOR" => "fe80:0000:0000:0000:0202:b3ff:fe1e:8329",
+ "HTTP_CLIENT_IP" => "2001:0db8:85a3:0000:0000:8a2e:0370:7334"
e = assert_raise(ActionDispatch::RemoteIp::IpSpoofAttackError) {
request.remote_ip
}
@@ -174,141 +180,141 @@ class RequestIP < BaseRequestTest
end
test "remote ip v6 spoof detection disabled" do
- request = stub_request 'HTTP_X_FORWARDED_FOR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329',
- 'HTTP_CLIENT_IP' => '2001:0db8:85a3:0000:0000:8a2e:0370:7334',
+ request = stub_request "HTTP_X_FORWARDED_FOR" => "fe80:0000:0000:0000:0202:b3ff:fe1e:8329",
+ "HTTP_CLIENT_IP" => "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
:ip_spoofing_check => false
- assert_equal 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329', request.remote_ip
+ assert_equal "fe80:0000:0000:0000:0202:b3ff:fe1e:8329", request.remote_ip
end
test "remote ip with user specified trusted proxies String" do
@trusted_proxies = "67.205.106.73"
- request = stub_request 'REMOTE_ADDR' => '3.4.5.6',
- 'HTTP_X_FORWARDED_FOR' => '67.205.106.73'
- assert_equal '3.4.5.6', request.remote_ip
+ request = stub_request "REMOTE_ADDR" => "3.4.5.6",
+ "HTTP_X_FORWARDED_FOR" => "67.205.106.73"
+ assert_equal "3.4.5.6", request.remote_ip
- request = stub_request 'REMOTE_ADDR' => '172.16.0.1,67.205.106.73',
- 'HTTP_X_FORWARDED_FOR' => '67.205.106.73'
- assert_equal '67.205.106.73', request.remote_ip
+ request = stub_request "REMOTE_ADDR" => "172.16.0.1,67.205.106.73",
+ "HTTP_X_FORWARDED_FOR" => "67.205.106.73"
+ assert_equal "67.205.106.73", request.remote_ip
- request = stub_request 'REMOTE_ADDR' => '67.205.106.73,3.4.5.6',
- 'HTTP_X_FORWARDED_FOR' => '67.205.106.73'
- assert_equal '3.4.5.6', request.remote_ip
+ request = stub_request "REMOTE_ADDR" => "67.205.106.73,3.4.5.6",
+ "HTTP_X_FORWARDED_FOR" => "67.205.106.73"
+ 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
+ request = stub_request "HTTP_X_FORWARDED_FOR" => "67.205.106.73,unknown"
+ 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
+ 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
end
test "remote ip v6 with user specified trusted proxies String" do
- @trusted_proxies = 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329'
+ @trusted_proxies = "fe80:0000:0000:0000:0202:b3ff:fe1e:8329"
- request = stub_request 'REMOTE_ADDR' => '2001:0db8:85a3:0000:0000:8a2e:0370:7334',
- 'HTTP_X_FORWARDED_FOR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329'
- assert_equal '2001:0db8:85a3:0000:0000:8a2e:0370:7334', request.remote_ip
+ request = stub_request "REMOTE_ADDR" => "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
+ "HTTP_X_FORWARDED_FOR" => "fe80:0000:0000:0000:0202:b3ff:fe1e:8329"
+ assert_equal "2001:0db8:85a3:0000:0000:8a2e:0370:7334", request.remote_ip
- request = stub_request 'REMOTE_ADDR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329,2001:0db8:85a3:0000:0000:8a2e:0370:7334',
- 'HTTP_X_FORWARDED_FOR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329'
- assert_equal '2001:0db8:85a3:0000:0000:8a2e:0370:7334', request.remote_ip
+ request = stub_request "REMOTE_ADDR" => "fe80:0000:0000:0000:0202:b3ff:fe1e:8329,2001:0db8:85a3:0000:0000:8a2e:0370:7334",
+ "HTTP_X_FORWARDED_FOR" => "fe80:0000:0000:0000:0202:b3ff:fe1e:8329"
+ assert_equal "2001:0db8:85a3:0000:0000:8a2e:0370:7334", request.remote_ip
- request = stub_request 'REMOTE_ADDR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329,::1',
- 'HTTP_X_FORWARDED_FOR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329'
- assert_equal '::1', request.remote_ip
+ request = stub_request "REMOTE_ADDR" => "fe80:0000:0000:0000:0202:b3ff:fe1e:8329,::1",
+ "HTTP_X_FORWARDED_FOR" => "fe80:0000:0000:0000:0202:b3ff:fe1e:8329"
+ 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
+ request = stub_request "HTTP_X_FORWARDED_FOR" => "unknown,fe80:0000:0000:0000:0202:b3ff:fe1e:8329"
+ 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'
+ 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
end
test "remote ip with user specified trusted proxies Regexp" do
@trusted_proxies = /^67\.205\.106\.73$/i
- request = stub_request 'REMOTE_ADDR' => '67.205.106.73',
- 'HTTP_X_FORWARDED_FOR' => '3.4.5.6'
- assert_equal '3.4.5.6', request.remote_ip
+ request = stub_request "REMOTE_ADDR" => "67.205.106.73",
+ "HTTP_X_FORWARDED_FOR" => "3.4.5.6"
+ assert_equal "3.4.5.6", request.remote_ip
- request = stub_request 'HTTP_X_FORWARDED_FOR' => '10.0.0.1, 9.9.9.9, 3.4.5.6, 67.205.106.73'
- assert_equal '3.4.5.6', request.remote_ip
+ request = stub_request "HTTP_X_FORWARDED_FOR" => "10.0.0.1, 9.9.9.9, 3.4.5.6, 67.205.106.73"
+ assert_equal "3.4.5.6", request.remote_ip
end
test "remote ip v6 with user specified trusted proxies Regexp" do
@trusted_proxies = /^fe80:0000:0000:0000:0202:b3ff:fe1e:8329$/i
- request = stub_request 'REMOTE_ADDR' => '2001:0db8:85a3:0000:0000:8a2e:0370:7334',
- 'HTTP_X_FORWARDED_FOR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329'
- assert_equal '2001:0db8:85a3:0000:0000:8a2e:0370:7334', request.remote_ip
+ request = stub_request "REMOTE_ADDR" => "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
+ "HTTP_X_FORWARDED_FOR" => "fe80:0000:0000:0000:0202:b3ff:fe1e:8329"
+ assert_equal "2001:0db8:85a3:0000:0000:8a2e:0370:7334", 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'
- assert_equal '2001:0db8:85a3:0000:0000:8a2e:0370:7334', 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"
+ assert_equal "2001:0db8:85a3:0000:0000:8a2e:0370:7334", request.remote_ip
end
test "remote ip middleware not present still returns an IP" do
- request = stub_request('REMOTE_ADDR' => '127.0.0.1')
- assert_equal '127.0.0.1', request.remote_ip
+ request = stub_request("REMOTE_ADDR" => "127.0.0.1")
+ assert_equal "127.0.0.1", request.remote_ip
end
end
class RequestDomain < BaseRequestTest
test "domains" do
- request = stub_request 'HTTP_HOST' => 'www.rubyonrails.org'
- assert_equal "rubyonrails.org", request.domain
-
- request = stub_request 'HTTP_HOST' => "www.rubyonrails.co.uk"
- assert_equal "rubyonrails.co.uk", request.domain(2)
-
- request = stub_request 'HTTP_HOST' => "www.rubyonrails.co.uk", :tld_length => 2
- assert_equal "rubyonrails.co.uk", request.domain
-
- request = stub_request 'HTTP_HOST' => "192.168.1.200"
+ request = stub_request "HTTP_HOST" => "192.168.1.200"
assert_nil request.domain
- request = stub_request 'HTTP_HOST' => "foo.192.168.1.200"
+ request = stub_request "HTTP_HOST" => "foo.192.168.1.200"
assert_nil request.domain
- request = stub_request 'HTTP_HOST' => "192.168.1.200.com"
+ request = stub_request "HTTP_HOST" => "192.168.1.200.com"
assert_equal "200.com", request.domain
- end
- test "subdomains" do
- request = stub_request 'HTTP_HOST' => "www.rubyonrails.org"
- assert_equal %w( www ), request.subdomains
- assert_equal "www", request.subdomain
-
- request = stub_request 'HTTP_HOST' => "www.rubyonrails.co.uk"
- assert_equal %w( www ), request.subdomains(2)
- assert_equal "www", request.subdomain(2)
+ request = stub_request "HTTP_HOST" => "www.rubyonrails.org"
+ assert_equal "rubyonrails.org", request.domain
- request = stub_request 'HTTP_HOST' => "dev.www.rubyonrails.co.uk"
- assert_equal %w( dev www ), request.subdomains(2)
- assert_equal "dev.www", request.subdomain(2)
+ request = stub_request "HTTP_HOST" => "www.rubyonrails.co.uk"
+ assert_equal "rubyonrails.co.uk", request.domain(2)
- request = stub_request 'HTTP_HOST' => "dev.www.rubyonrails.co.uk", :tld_length => 2
- assert_equal %w( dev www ), request.subdomains
- assert_equal "dev.www", request.subdomain
+ request = stub_request "HTTP_HOST" => "www.rubyonrails.co.uk", :tld_length => 2
+ assert_equal "rubyonrails.co.uk", request.domain
+ end
- request = stub_request 'HTTP_HOST' => "foobar.foobar.com"
+ test "subdomains" do
+ request = stub_request "HTTP_HOST" => "foobar.foobar.com"
assert_equal %w( foobar ), request.subdomains
assert_equal "foobar", request.subdomain
- request = stub_request 'HTTP_HOST' => "192.168.1.200"
+ request = stub_request "HTTP_HOST" => "192.168.1.200"
assert_equal [], request.subdomains
assert_equal "", request.subdomain
- request = stub_request 'HTTP_HOST' => "foo.192.168.1.200"
+ request = stub_request "HTTP_HOST" => "foo.192.168.1.200"
assert_equal [], request.subdomains
assert_equal "", request.subdomain
- request = stub_request 'HTTP_HOST' => "192.168.1.200.com"
+ request = stub_request "HTTP_HOST" => "192.168.1.200.com"
assert_equal %w( 192 168 1 ), request.subdomains
assert_equal "192.168.1", request.subdomain
- request = stub_request 'HTTP_HOST' => nil
+ request = stub_request "HTTP_HOST" => nil
assert_equal [], request.subdomains
assert_equal "", request.subdomain
+
+ request = stub_request "HTTP_HOST" => "www.rubyonrails.org"
+ assert_equal %w( www ), request.subdomains
+ assert_equal "www", request.subdomain
+
+ request = stub_request "HTTP_HOST" => "www.rubyonrails.co.uk"
+ assert_equal %w( www ), request.subdomains(2)
+ assert_equal "www", request.subdomain(2)
+
+ request = stub_request "HTTP_HOST" => "dev.www.rubyonrails.co.uk"
+ assert_equal %w( dev www ), request.subdomains(2)
+ assert_equal "dev.www", request.subdomain(2)
+
+ request = stub_request "HTTP_HOST" => "dev.www.rubyonrails.co.uk", :tld_length => 2
+ assert_equal %w( dev www ), request.subdomains
+ assert_equal "dev.www", request.subdomain
end
end
@@ -317,95 +323,106 @@ class RequestPort < BaseRequestTest
request = stub_request
assert_equal 80, request.standard_port
- request = stub_request 'HTTPS' => 'on'
+ request = stub_request "HTTPS" => "on"
assert_equal 443, request.standard_port
end
test "standard_port?" do
request = stub_request
- assert !request.ssl?
- assert request.standard_port?
+ assert_not_predicate request, :ssl?
+ assert_predicate request, :standard_port?
- request = stub_request 'HTTPS' => 'on'
- assert request.ssl?
- assert request.standard_port?
+ request = stub_request "HTTPS" => "on"
+ assert_predicate request, :ssl?
+ assert_predicate request, :standard_port?
- request = stub_request 'HTTP_HOST' => 'www.example.org:8080'
- assert !request.ssl?
- assert !request.standard_port?
+ request = stub_request "HTTP_HOST" => "www.example.org:8080"
+ assert_not_predicate request, :ssl?
+ assert_not_predicate request, :standard_port?
- request = stub_request 'HTTP_HOST' => 'www.example.org:8443', 'HTTPS' => 'on'
- assert request.ssl?
- assert !request.standard_port?
+ request = stub_request "HTTP_HOST" => "www.example.org:8443", "HTTPS" => "on"
+ assert_predicate request, :ssl?
+ assert_not_predicate request, :standard_port?
end
test "optional port" do
- request = stub_request 'HTTP_HOST' => 'www.example.org:80'
- assert_equal nil, request.optional_port
+ request = stub_request "HTTP_HOST" => "www.example.org:80"
+ assert_nil request.optional_port
- request = stub_request 'HTTP_HOST' => 'www.example.org:8080'
+ request = stub_request "HTTP_HOST" => "www.example.org:8080"
assert_equal 8080, request.optional_port
end
test "port string" do
- request = stub_request 'HTTP_HOST' => 'www.example.org:80'
- assert_equal '', request.port_string
+ request = stub_request "HTTP_HOST" => "www.example.org:80"
+ assert_equal "", request.port_string
- request = stub_request 'HTTP_HOST' => 'www.example.org:8080'
- assert_equal ':8080', request.port_string
+ request = stub_request "HTTP_HOST" => "www.example.org:8080"
+ assert_equal ":8080", request.port_string
+ end
+
+ test "server port" do
+ request = stub_request "SERVER_PORT" => "8080"
+ assert_equal 8080, request.server_port
+
+ request = stub_request "SERVER_PORT" => "80"
+ assert_equal 80, request.server_port
+
+ request = stub_request "SERVER_PORT" => ""
+ assert_equal 0, request.server_port
end
end
class RequestPath < BaseRequestTest
test "full path" do
- request = stub_request 'SCRIPT_NAME' => '', 'PATH_INFO' => '/path/of/some/uri', 'QUERY_STRING' => 'mapped=1'
+ request = stub_request "SCRIPT_NAME" => "", "PATH_INFO" => "/path/of/some/uri", "QUERY_STRING" => "mapped=1"
assert_equal "/path/of/some/uri?mapped=1", request.fullpath
assert_equal "/path/of/some/uri", request.path_info
- request = stub_request 'SCRIPT_NAME' => '', 'PATH_INFO' => '/path/of/some/uri'
+ request = stub_request "SCRIPT_NAME" => "", "PATH_INFO" => "/path/of/some/uri"
assert_equal "/path/of/some/uri", request.fullpath
assert_equal "/path/of/some/uri", request.path_info
- request = stub_request 'SCRIPT_NAME' => '', 'PATH_INFO' => '/'
+ request = stub_request "SCRIPT_NAME" => "", "PATH_INFO" => "/"
assert_equal "/", request.fullpath
assert_equal "/", request.path_info
- request = stub_request 'SCRIPT_NAME' => '', 'PATH_INFO' => '/', 'QUERY_STRING' => 'm=b'
+ request = stub_request "SCRIPT_NAME" => "", "PATH_INFO" => "/", "QUERY_STRING" => "m=b"
assert_equal "/?m=b", request.fullpath
assert_equal "/", request.path_info
- request = stub_request 'SCRIPT_NAME' => '/hieraki', 'PATH_INFO' => '/'
+ request = stub_request "SCRIPT_NAME" => "/hieraki", "PATH_INFO" => "/"
assert_equal "/hieraki/", request.fullpath
assert_equal "/", request.path_info
- request = stub_request 'SCRIPT_NAME' => '/collaboration/hieraki', 'PATH_INFO' => '/books/edit/2'
+ request = stub_request "SCRIPT_NAME" => "/collaboration/hieraki", "PATH_INFO" => "/books/edit/2"
assert_equal "/collaboration/hieraki/books/edit/2", request.fullpath
assert_equal "/books/edit/2", request.path_info
- request = stub_request 'SCRIPT_NAME' => '/path', 'PATH_INFO' => '/of/some/uri', 'QUERY_STRING' => 'mapped=1'
+ request = stub_request "SCRIPT_NAME" => "/path", "PATH_INFO" => "/of/some/uri", "QUERY_STRING" => "mapped=1"
assert_equal "/path/of/some/uri?mapped=1", request.fullpath
assert_equal "/of/some/uri", request.path_info
end
test "original_fullpath returns ORIGINAL_FULLPATH" do
- request = stub_request('ORIGINAL_FULLPATH' => "/foo?bar")
+ request = stub_request("ORIGINAL_FULLPATH" => "/foo?bar")
path = request.original_fullpath
assert_equal "/foo?bar", path
end
test "original_url returns url built using ORIGINAL_FULLPATH" do
- request = stub_request('ORIGINAL_FULLPATH' => "/foo?bar",
- 'HTTP_HOST' => "example.org",
- 'rack.url_scheme' => "http")
+ request = stub_request("ORIGINAL_FULLPATH" => "/foo?bar",
+ "HTTP_HOST" => "example.org",
+ "rack.url_scheme" => "http")
url = request.original_url
assert_equal "http://example.org/foo?bar", url
end
test "original_fullpath returns fullpath if ORIGINAL_FULLPATH is not present" do
- request = stub_request('PATH_INFO' => "/foo",
- 'QUERY_STRING' => "bar")
+ request = stub_request("PATH_INFO" => "/foo",
+ "QUERY_STRING" => "bar")
path = request.original_fullpath
assert_equal "/foo?bar", path
@@ -413,58 +430,78 @@ class RequestPath < BaseRequestTest
end
class RequestHost < BaseRequestTest
+ test "host without specifying port" do
+ request = stub_request "HTTP_HOST" => "rubyonrails.org"
+ assert_equal "rubyonrails.org", request.host_with_port
+ end
+
test "host with default port" do
- request = stub_request 'HTTP_HOST' => 'rubyonrails.org:80'
+ request = stub_request "HTTP_HOST" => "rubyonrails.org:80"
assert_equal "rubyonrails.org", request.host_with_port
end
test "host with non default port" do
- request = stub_request 'HTTP_HOST' => 'rubyonrails.org:81'
+ request = stub_request "HTTP_HOST" => "rubyonrails.org:81"
assert_equal "rubyonrails.org:81", request.host_with_port
end
+ test "raw without specifying port" do
+ request = stub_request "HTTP_HOST" => "rubyonrails.org"
+ assert_equal "rubyonrails.org", request.raw_host_with_port
+ end
+
+ test "raw host with default port" do
+ request = stub_request "HTTP_HOST" => "rubyonrails.org:80"
+ assert_equal "rubyonrails.org:80", request.raw_host_with_port
+ end
+
+ test "raw host with non default port" do
+ request = stub_request "HTTP_HOST" => "rubyonrails.org:81"
+ assert_equal "rubyonrails.org:81", request.raw_host_with_port
+ end
+
test "proxy request" do
- request = stub_request 'HTTP_HOST' => 'glu.ttono.us:80'
+ request = stub_request "HTTP_HOST" => "glu.ttono.us:80"
assert_equal "glu.ttono.us", request.host_with_port
end
test "http host" do
- request = stub_request 'HTTP_HOST' => "rubyonrails.org:8080"
+ request = stub_request "HTTP_HOST" => "rubyonrails.org:8080"
assert_equal "rubyonrails.org", request.host
assert_equal "rubyonrails.org:8080", request.host_with_port
- request = stub_request 'HTTP_X_FORWARDED_HOST' => "www.firsthost.org, www.secondhost.org"
+ request = stub_request "HTTP_X_FORWARDED_HOST" => "www.firsthost.org, www.secondhost.org"
assert_equal "www.secondhost.org", request.host
- request = stub_request 'HTTP_X_FORWARDED_HOST' => "", 'HTTP_HOST' => "rubyonrails.org"
+ request = stub_request "HTTP_X_FORWARDED_HOST" => "", "HTTP_HOST" => "rubyonrails.org"
assert_equal "rubyonrails.org", request.host
end
test "http host with default port overrides server port" do
- request = stub_request 'HTTP_HOST' => "rubyonrails.org"
+ request = stub_request "HTTP_HOST" => "rubyonrails.org"
assert_equal "rubyonrails.org", request.host_with_port
end
test "host with port if http standard port is specified" do
- request = stub_request 'HTTP_X_FORWARDED_HOST' => "glu.ttono.us:80"
+ request = stub_request "HTTP_X_FORWARDED_HOST" => "glu.ttono.us:80"
assert_equal "glu.ttono.us", request.host_with_port
end
test "host with port if https standard port is specified" do
request = stub_request(
- 'HTTP_X_FORWARDED_PROTO' => "https",
- 'HTTP_X_FORWARDED_HOST' => "glu.ttono.us:443"
+ "HTTP_X_FORWARDED_PROTO" => "https",
+ "HTTP_X_FORWARDED_HOST" => "glu.ttono.us:443"
)
assert_equal "glu.ttono.us", request.host_with_port
end
test "host if ipv6 reference" do
- request = stub_request 'HTTP_HOST' => "[2001:1234:5678:9abc:def0::dead:beef]"
+ request = stub_request "HTTP_HOST" => "[2001:1234:5678:9abc:def0::dead:beef]"
assert_equal "[2001:1234:5678:9abc:def0::dead:beef]", request.host
end
test "host if ipv6 reference with port" do
- request = stub_request 'HTTP_HOST' => "[2001:1234:5678:9abc:def0::dead:beef]:8008"
+ request = stub_request "HTTP_HOST" => "[2001:1234:5678:9abc:def0::dead:beef]:8008"
assert_equal "[2001:1234:5678:9abc:def0::dead:beef]", request.host
end
end
@@ -502,7 +539,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
@@ -534,7 +571,7 @@ end
class LocalhostTest < BaseRequestTest
test "IPs that match localhost" do
request = stub_request("REMOTE_IP" => "127.1.1.1", "REMOTE_ADDR" => "127.1.1.1")
- assert request.local?
+ assert_predicate request, :local?
end
end
@@ -546,7 +583,7 @@ class RequestCookie < BaseRequestTest
# some Nokia phone browsers omit the space after the semicolon separator.
# some developers have grown accustomed to using comma in cookie values.
- request = stub_request("HTTP_COOKIE"=>"_session_id=c84ace847,96670c052c6ceb2451fb0f2;is_admin=yes")
+ request = stub_request("HTTP_COOKIE" => "_session_id=c84ace847,96670c052c6ceb2451fb0f2;is_admin=yes")
assert_equal "c84ace847", request.cookies["_session_id"], request.cookies.inspect
assert_equal "yes", request.cookies["is_admin"], request.cookies.inspect
end
@@ -555,28 +592,28 @@ end
class RequestParamsParsing < BaseRequestTest
test "doesnt break when content type has charset" do
request = stub_request(
- 'REQUEST_METHOD' => 'POST',
- 'CONTENT_LENGTH' => "flamenco=love".length,
- 'CONTENT_TYPE' => 'application/x-www-form-urlencoded; charset=utf-8',
- 'rack.input' => StringIO.new("flamenco=love")
+ "REQUEST_METHOD" => "POST",
+ "CONTENT_LENGTH" => "flamenco=love".length,
+ "CONTENT_TYPE" => "application/x-www-form-urlencoded; charset=utf-8",
+ "rack.input" => StringIO.new("flamenco=love")
)
- assert_equal({"flamenco"=> "love"}, request.request_parameters)
+ assert_equal({ "flamenco" => "love" }, request.request_parameters)
end
test "doesnt interpret request uri as query string when missing" do
- request = stub_request('REQUEST_URI' => 'foo')
+ request = stub_request("REQUEST_URI" => "foo")
assert_equal({}, request.query_parameters)
end
end
class RequestRewind < BaseRequestTest
test "body should be rewound" do
- data = 'rewind'
+ data = "rewind"
env = {
- 'rack.input' => StringIO.new(data),
- 'CONTENT_LENGTH' => data.length,
- 'CONTENT_TYPE' => 'application/x-www-form-urlencoded; charset=utf-8'
+ "rack.input" => StringIO.new(data),
+ "CONTENT_LENGTH" => data.length,
+ "CONTENT_TYPE" => "application/x-www-form-urlencoded; charset=utf-8"
}
# Read the request body by parsing params.
@@ -589,55 +626,55 @@ class RequestRewind < BaseRequestTest
test "raw_post rewinds rack.input if RAW_POST_DATA is nil" do
request = stub_request(
- 'rack.input' => StringIO.new("raw"),
- 'CONTENT_LENGTH' => 3
+ "rack.input" => StringIO.new("raw"),
+ "CONTENT_LENGTH" => 3
)
assert_equal "raw", request.raw_post
- assert_equal "raw", request.env['rack.input'].read
+ assert_equal "raw", request.env["rack.input"].read
end
end
class RequestProtocol < BaseRequestTest
test "server software" do
- assert_equal 'lighttpd', stub_request('SERVER_SOFTWARE' => 'lighttpd/1.4.5').server_software
- assert_equal 'apache', stub_request('SERVER_SOFTWARE' => 'Apache3.422').server_software
+ assert_equal "lighttpd", stub_request("SERVER_SOFTWARE" => "lighttpd/1.4.5").server_software
+ assert_equal "apache", stub_request("SERVER_SOFTWARE" => "Apache3.422").server_software
end
test "xml http request" do
request = stub_request
- assert !request.xml_http_request?
- assert !request.xhr?
+ assert_not_predicate request, :xml_http_request?
+ assert_not_predicate request, :xhr?
- request = stub_request 'HTTP_X_REQUESTED_WITH' => 'DefinitelyNotAjax1.0'
- assert !request.xml_http_request?
- assert !request.xhr?
+ request = stub_request "HTTP_X_REQUESTED_WITH" => "DefinitelyNotAjax1.0"
+ assert_not_predicate request, :xml_http_request?
+ assert_not_predicate request, :xhr?
- request = stub_request 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest'
- assert request.xml_http_request?
- assert request.xhr?
+ request = stub_request "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest"
+ assert_predicate request, :xml_http_request?
+ assert_predicate request, :xhr?
end
test "reports ssl" do
- assert !stub_request.ssl?
- assert stub_request('HTTPS' => 'on').ssl?
+ assert_not_predicate stub_request, :ssl?
+ assert_predicate stub_request("HTTPS" => "on"), :ssl?
end
test "reports ssl when proxied via lighttpd" do
- assert stub_request('HTTP_X_FORWARDED_PROTO' => 'https').ssl?
+ assert_predicate stub_request("HTTP_X_FORWARDED_PROTO" => "https"), :ssl?
end
test "scheme returns https when proxied" do
- request = stub_request 'rack.url_scheme' => 'http'
- assert !request.ssl?
- assert_equal 'http', request.scheme
+ request = stub_request "rack.url_scheme" => "http"
+ assert_not_predicate request, :ssl?
+ assert_equal "http", request.scheme
request = stub_request(
- 'rack.url_scheme' => 'http',
- 'HTTP_X_FORWARDED_PROTO' => 'https'
+ "rack.url_scheme" => "http",
+ "HTTP_X_FORWARDED_PROTO" => "https"
)
- assert request.ssl?
- assert_equal 'https', request.scheme
+ assert_predicate request, :ssl?
+ assert_equal "https", request.scheme
end
end
@@ -646,7 +683,7 @@ class RequestMethod < BaseRequestTest
overridden by middleware".squish do
ActionDispatch::Request::HTTP_METHODS.each do |method|
- request = stub_request('REQUEST_METHOD' => method)
+ request = stub_request("REQUEST_METHOD" => method)
assert_equal method, request.method
assert_equal method.underscore.to_sym, request.method_symbol
@@ -654,36 +691,36 @@ class RequestMethod < BaseRequestTest
end
test "allow request method hacking" do
- request = stub_request('REQUEST_METHOD' => 'POST')
+ request = stub_request("REQUEST_METHOD" => "POST")
- assert_equal 'POST', request.request_method
- assert_equal 'POST', request.env["REQUEST_METHOD"]
+ assert_equal "POST", request.request_method
+ assert_equal "POST", request.env["REQUEST_METHOD"]
- request.request_method = 'GET'
+ request.request_method = "GET"
- assert_equal 'GET', request.request_method
- assert_equal 'GET', request.env["REQUEST_METHOD"]
- assert request.get?
+ assert_equal "GET", request.request_method
+ assert_equal "GET", request.env["REQUEST_METHOD"]
+ assert_predicate request, :get?
end
test "invalid http method raises exception" do
assert_raise(ActionController::UnknownHttpMethod) do
- stub_request('REQUEST_METHOD' => 'RANDOM_METHOD').request_method
+ stub_request("REQUEST_METHOD" => "RANDOM_METHOD").request_method
end
end
test "method returns original value of environment request method on POST" do
- request = stub_request('rack.methodoverride.original_method' => 'POST')
- assert_equal 'POST', request.method
+ request = stub_request("rack.methodoverride.original_method" => "POST")
+ assert_equal "POST", request.method
end
test "method raises exception on invalid HTTP method" do
assert_raise(ActionController::UnknownHttpMethod) do
- stub_request('rack.methodoverride.original_method' => '_RANDOM_METHOD').method
+ stub_request("rack.methodoverride.original_method" => "_RANDOM_METHOD").method
end
assert_raise(ActionController::UnknownHttpMethod) do
- stub_request('REQUEST_METHOD' => '_RANDOM_METHOD').method
+ stub_request("REQUEST_METHOD" => "_RANDOM_METHOD").method
end
end
@@ -695,7 +732,7 @@ class RequestMethod < BaseRequestTest
I18n.available_locales = [:nl]
I18n.config.enforce_available_locales = true
assert_raise(ActionController::UnknownHttpMethod) do
- stub_request('REQUEST_METHOD' => '_RANDOM_METHOD').method
+ stub_request("REQUEST_METHOD" => "_RANDOM_METHOD").method
end
ensure
I18n.available_locales = old_locales
@@ -705,28 +742,28 @@ class RequestMethod < BaseRequestTest
test "post masquerading as patch" do
request = stub_request(
- 'REQUEST_METHOD' => 'PATCH',
+ "REQUEST_METHOD" => "PATCH",
"rack.methodoverride.original_method" => "POST"
)
assert_equal "POST", request.method
assert_equal "PATCH", request.request_method
- assert request.patch?
+ assert_predicate request, :patch?
end
test "post masquerading as put" do
request = stub_request(
- 'REQUEST_METHOD' => 'PUT',
+ "REQUEST_METHOD" => "PUT",
"rack.methodoverride.original_method" => "POST"
)
assert_equal "POST", request.method
assert_equal "PUT", request.request_method
- assert request.put?
+ assert_predicate request, :put?
end
test "post uneffected by local inflections" do
- existing_acrnoyms = ActiveSupport::Inflector.inflections.acronyms.dup
- existing_acrnoym_regex = ActiveSupport::Inflector.inflections.acronym_regex.dup
+ existing_acronyms = ActiveSupport::Inflector.inflections.acronyms.dup
+ assert_deprecated { ActiveSupport::Inflector.inflections.acronym_regex.dup }
begin
ActiveSupport::Inflector.inflections do |inflect|
inflect.acronym "POS"
@@ -735,12 +772,12 @@ class RequestMethod < BaseRequestTest
request = stub_request "REQUEST_METHOD" => "POST"
assert_equal :post, ActionDispatch::Request::HTTP_METHOD_LOOKUP["POST"]
assert_equal :post, request.method_symbol
- assert request.post?
+ assert_predicate request, :post?
ensure
# Reset original acronym set
ActiveSupport::Inflector.inflections do |inflect|
- inflect.send(:instance_variable_set,"@acronyms",existing_acrnoyms)
- inflect.send(:instance_variable_set,"@acronym_regex",existing_acrnoym_regex)
+ inflect.send(:instance_variable_set, "@acronyms", existing_acronyms)
+ inflect.send(:define_acronym_regex_patterns)
end
end
end
@@ -748,97 +785,99 @@ end
class RequestFormat < BaseRequestTest
test "xml format" do
- request = stub_request
- request.expects(:parameters).at_least_once.returns({ :format => 'xml' })
- assert_equal Mime::XML, request.format
+ request = stub_request "QUERY_STRING" => "format=xml"
+
+ assert_equal Mime[:xml], request.format
end
test "xhtml format" do
- request = stub_request
- request.expects(:parameters).at_least_once.returns({ :format => 'xhtml' })
- assert_equal Mime::HTML, request.format
+ request = stub_request "QUERY_STRING" => "format=xhtml"
+
+ assert_equal Mime[:html], request.format
end
test "txt format" do
- request = stub_request
- request.expects(:parameters).at_least_once.returns({ :format => 'txt' })
- assert_equal Mime::TEXT, request.format
+ request = stub_request "QUERY_STRING" => "format=txt"
+
+ assert_equal Mime[:text], request.format
end
test "XMLHttpRequest" do
request = stub_request(
- 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest',
- 'HTTP_ACCEPT' => [Mime::JS, Mime::HTML, Mime::XML, "text/xml", Mime::ALL].join(",")
+ "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest",
+ "HTTP_ACCEPT" => [Mime[:js], Mime[:html], Mime[:xml], "text/xml", "*/*"].join(","),
+ "QUERY_STRING" => ""
)
- request.expects(:parameters).at_least_once.returns({})
- assert request.xhr?
- assert_equal Mime::JS, request.format
+
+ assert_predicate request, :xhr?
+ assert_equal Mime[:js], request.format
end
test "can override format with parameter negative" do
- request = stub_request
- request.expects(:parameters).at_least_once.returns({ :format => :txt })
- assert !request.format.xml?
+ request = stub_request("QUERY_STRING" => "format=txt")
+
+ assert_not_predicate request.format, :xml?
end
test "can override format with parameter positive" do
- request = stub_request
- request.expects(:parameters).at_least_once.returns({ :format => :xml })
- assert request.format.xml?
+ request = stub_request("QUERY_STRING" => "format=xml")
+
+ assert_predicate request.format, :xml?
end
test "formats text/html with accept header" do
- request = stub_request 'HTTP_ACCEPT' => 'text/html'
- assert_equal [Mime::HTML], request.formats
+ request = stub_request "HTTP_ACCEPT" => "text/html"
+ assert_equal [Mime[:html]], request.formats
end
test "formats blank with accept header" do
- request = stub_request 'HTTP_ACCEPT' => ''
- assert_equal [Mime::HTML], request.formats
+ request = stub_request "HTTP_ACCEPT" => ""
+ assert_equal [Mime[:html]], request.formats
end
test "formats XMLHttpRequest with accept header" do
- request = stub_request 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest"
- assert_equal [Mime::JS], request.formats
+ request = stub_request "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest"
+ assert_equal [Mime[:js]], request.formats
end
test "formats application/xml with accept header" do
- request = stub_request('CONTENT_TYPE' => 'application/xml; charset=UTF-8',
- 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest")
- assert_equal [Mime::XML], request.formats
+ request = stub_request("CONTENT_TYPE" => "application/xml; charset=UTF-8",
+ "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest")
+ assert_equal [Mime[:xml]], request.formats
end
test "formats format:text with accept header" do
- request = stub_request
- request.expects(:parameters).at_least_once.returns({ :format => :txt })
- assert_equal [Mime::TEXT], request.formats
+ request = stub_request("QUERY_STRING" => "format=txt")
+
+ assert_equal [Mime[:text]], request.formats
end
test "formats format:unknown with accept header" do
- request = stub_request
- request.expects(:parameters).at_least_once.returns({ :format => :unknown })
+ request = stub_request("QUERY_STRING" => "format=unknown")
+
assert_instance_of Mime::NullType, request.format
end
test "format is not nil with unknown format" do
- request = stub_request
- request.expects(:parameters).at_least_once.returns({ format: :hello })
- assert request.format.nil?
- assert_not request.format.html?
- assert_not request.format.xml?
- assert_not request.format.json?
+ request = stub_request("QUERY_STRING" => "format=hello")
+
+ assert_nil request.format
+ assert_not_predicate request.format, :html?
+ assert_not_predicate request.format, :xml?
+ assert_not_predicate request.format, :json?
end
test "format does not throw exceptions when malformed parameters" do
request = stub_request("QUERY_STRING" => "x[y]=1&x[y][][w]=2")
assert request.formats
- assert request.format.html?
+ assert_predicate request.format, :html?
end
test "formats with xhr request" do
- request = stub_request 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest"
- request.expects(:parameters).at_least_once.returns({})
- assert_equal [Mime::JS], request.formats
+ request = stub_request "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest",
+ "QUERY_STRING" => ""
+
+ assert_equal [Mime[:js]], request.formats
end
test "ignore_accept_header" do
@@ -846,88 +885,113 @@ class RequestFormat < BaseRequestTest
ActionDispatch::Request.ignore_accept_header = true
begin
- request = stub_request 'HTTP_ACCEPT' => 'application/xml'
- request.expects(:parameters).at_least_once.returns({})
- assert_equal [ Mime::HTML ], request.formats
-
- request = stub_request 'HTTP_ACCEPT' => 'koz-asked/something-crazy'
- request.expects(:parameters).at_least_once.returns({})
- assert_equal [ Mime::HTML ], request.formats
-
- request = stub_request 'HTTP_ACCEPT' => '*/*;q=0.1'
- request.expects(:parameters).at_least_once.returns({})
- assert_equal [ Mime::HTML ], request.formats
-
- request = stub_request 'HTTP_ACCEPT' => 'application/jxw'
- request.expects(:parameters).at_least_once.returns({})
- assert_equal [ Mime::HTML ], request.formats
-
- request = stub_request 'HTTP_ACCEPT' => 'application/xml',
- 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest"
- request.expects(:parameters).at_least_once.returns({})
- assert_equal [ Mime::JS ], request.formats
-
- request = stub_request 'HTTP_ACCEPT' => 'application/xml',
- 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest"
- request.expects(:parameters).at_least_once.returns({:format => :json})
- assert_equal [ Mime::JSON ], request.formats
+ request = stub_request "HTTP_ACCEPT" => "application/xml",
+ "QUERY_STRING" => ""
+
+ assert_equal [ Mime[:html] ], request.formats
+
+ request = stub_request "HTTP_ACCEPT" => "koz-asked/something-crazy",
+ "QUERY_STRING" => ""
+
+ assert_equal [ Mime[:html] ], request.formats
+
+ request = stub_request "HTTP_ACCEPT" => "*/*;q=0.1",
+ "QUERY_STRING" => ""
+
+ assert_equal [ Mime[:html] ], request.formats
+
+ request = stub_request "HTTP_ACCEPT" => "application/jxw",
+ "QUERY_STRING" => ""
+
+ assert_equal [ Mime[:html] ], request.formats
+
+ request = stub_request "HTTP_ACCEPT" => "application/xml",
+ "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest",
+ "QUERY_STRING" => ""
+
+ assert_equal [ Mime[:js] ], request.formats
+
+ request = stub_request "HTTP_ACCEPT" => "application/xml",
+ "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest",
+ "QUERY_STRING" => "format=json"
+
+ assert_equal [ Mime[:json] ], request.formats
ensure
ActionDispatch::Request.ignore_accept_header = old_ignore_accept_header
end
end
+
+ test "format taken from the path extension" do
+ request = stub_request "PATH_INFO" => "/foo.xml", "QUERY_STRING" => ""
+
+ assert_equal [Mime[:xml]], request.formats
+
+ request = stub_request "PATH_INFO" => "/foo.123", "QUERY_STRING" => ""
+
+ assert_equal [Mime[:html]], request.formats
+ end
+
+ test "formats from accept headers have higher precedence than path extension" do
+ request = stub_request "HTTP_ACCEPT" => "application/json",
+ "PATH_INFO" => "/foo.xml",
+ "QUERY_STRING" => ""
+
+ assert_equal [Mime[:json]], request.formats
+ end
end
class RequestMimeType < BaseRequestTest
test "content type" do
- assert_equal Mime::HTML, stub_request('CONTENT_TYPE' => 'text/html').content_mime_type
+ assert_equal Mime[:html], stub_request("CONTENT_TYPE" => "text/html").content_mime_type
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
- assert_equal Mime::XML, stub_request('CONTENT_TYPE' => 'application/xml').content_mime_type
+ assert_equal Mime[:xml], stub_request("CONTENT_TYPE" => "application/xml").content_mime_type
end
test "content type with charset" do
- assert_equal Mime::XML, stub_request('CONTENT_TYPE' => 'application/xml; charset=UTF-8').content_mime_type
+ assert_equal Mime[:xml], stub_request("CONTENT_TYPE" => "application/xml; charset=UTF-8").content_mime_type
end
test "user agent" do
- assert_equal 'TestAgent', stub_request('HTTP_USER_AGENT' => 'TestAgent').user_agent
+ assert_equal "TestAgent", stub_request("HTTP_USER_AGENT" => "TestAgent").user_agent
end
test "negotiate_mime" do
request = stub_request(
- 'HTTP_ACCEPT' => 'text/html',
- 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest"
+ "HTTP_ACCEPT" => "text/html",
+ "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest"
)
- assert_equal 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])
+ 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
test "negotiate_mime with content_type" do
request = stub_request(
- 'CONTENT_TYPE' => 'application/xml; charset=UTF-8',
- 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest"
+ "CONTENT_TYPE" => "application/xml; charset=UTF-8",
+ "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest"
)
- assert_equal Mime::XML, request.negotiate_mime([Mime::XML, Mime::CSV])
+ assert_equal Mime[:xml], request.negotiate_mime([Mime[:xml], Mime[:csv]])
end
end
class RequestParameters < BaseRequestTest
test "parameters" do
- request = stub_request
- request.expects(:request_parameters).at_least_once.returns({ "foo" => 1 })
- request.expects(:query_parameters).at_least_once.returns({ "bar" => 2 })
+ request = stub_request "CONTENT_TYPE" => "application/json",
+ "CONTENT_LENGTH" => 9,
+ "RAW_POST_DATA" => '{"foo":1}',
+ "QUERY_STRING" => "bar=2"
- assert_equal({"foo" => 1, "bar" => 2}, request.parameters)
- assert_equal({"foo" => 1}, request.request_parameters)
- assert_equal({"bar" => 2}, request.query_parameters)
+ assert_equal({ "foo" => 1, "bar" => "2" }, request.parameters)
+ assert_equal({ "foo" => 1 }, request.request_parameters)
+ assert_equal({ "bar" => "2" }, request.query_parameters)
end
test "parameters not accessible after rack parse error" do
@@ -941,23 +1005,38 @@ class RequestParameters < BaseRequestTest
end
end
+ test "path parameters with invalid UTF8 encoding" do
+ request = stub_request
+
+ err = assert_raises(ActionController::BadRequest) do
+ request.path_parameters = { foo: "\xBE" }
+ end
+
+ assert_predicate err.message, :valid_encoding?
+ assert_equal "Invalid path parameters: Invalid encoding for parameter: �", err.message
+ end
+
test "parameters not accessible after rack parse error of invalid UTF8 character" do
request = stub_request("QUERY_STRING" => "foo%81E=1")
+ assert_raises(ActionController::BadRequest) { request.parameters }
+ end
- 2.times do
- assert_raises(ActionController::BadRequest) do
- # rack will raise a Rack::Utils::InvalidParameterError when parsing this query string
- request.parameters
- end
- end
+ test "parameters containing an invalid UTF8 character" do
+ request = stub_request("QUERY_STRING" => "foo=%81E")
+ assert_raises(ActionController::BadRequest) { request.parameters }
+ end
+
+ test "parameters containing a deeply nested invalid UTF8 character" do
+ request = stub_request("QUERY_STRING" => "foo[bar]=%81E")
+ assert_raises(ActionController::BadRequest) { request.parameters }
end
test "parameters not accessible after rack parse error 1" do
request = stub_request(
- 'REQUEST_METHOD' => 'POST',
- 'CONTENT_LENGTH' => "a%=".length,
- 'CONTENT_TYPE' => 'application/x-www-form-urlencoded; charset=utf-8',
- 'rack.input' => StringIO.new("a%=")
+ "REQUEST_METHOD" => "POST",
+ "CONTENT_LENGTH" => "a%=".length,
+ "CONTENT_TYPE" => "application/x-www-form-urlencoded; charset=utf-8",
+ "rack.input" => StringIO.new("a%=")
)
assert_raises(ActionController::BadRequest) do
@@ -974,49 +1053,26 @@ class RequestParameters < BaseRequestTest
request.parameters
end
- assert e.original_exception
- assert_equal e.original_exception.backtrace, e.backtrace
+ assert_not_nil e.cause
+ assert_equal e.cause.backtrace, e.backtrace
end
end
-
class RequestParameterFilter < BaseRequestTest
- test "process parameter filter" do
- test_hashes = [
- [{'foo'=>'bar'},{'foo'=>'bar'},%w'food'],
- [{'foo'=>'bar'},{'foo'=>'[FILTERED]'},%w'foo'],
- [{'foo'=>'bar', 'bar'=>'foo'},{'foo'=>'[FILTERED]', 'bar'=>'foo'},%w'foo baz'],
- [{'foo'=>'bar', 'baz'=>'foo'},{'foo'=>'[FILTERED]', 'baz'=>'[FILTERED]'},%w'foo baz'],
- [{'bar'=>{'foo'=>'bar','bar'=>'foo'}},{'bar'=>{'foo'=>'[FILTERED]','bar'=>'foo'}},%w'fo'],
- [{'foo'=>{'foo'=>'bar','bar'=>'foo'}},{'foo'=>'[FILTERED]'},%w'f banana'],
- [{'deep'=>{'cc'=>{'code'=>'bar','bar'=>'foo'},'ss'=>{'code'=>'bar'}}},{'deep'=>{'cc'=>{'code'=>'[FILTERED]','bar'=>'foo'},'ss'=>{'code'=>'bar'}}},%w'deep.cc.code'],
- [{'baz'=>[{'foo'=>'baz'}, "1"]}, {'baz'=>[{'foo'=>'[FILTERED]'}, "1"]}, [/foo/]]]
-
- test_hashes.each do |before_filter, after_filter, filter_words|
- parameter_filter = ActionDispatch::Http::ParameterFilter.new(filter_words)
- assert_equal after_filter, parameter_filter.filter(before_filter)
-
- filter_words << 'blah'
- filter_words << lambda { |key, value|
- value.reverse! if key =~ /bargain/
- }
-
- parameter_filter = ActionDispatch::Http::ParameterFilter.new(filter_words)
- before_filter['barg'] = {:bargain=>'gain', 'blah'=>'bar', 'bar'=>{'bargain'=>{'blah'=>'foo'}}}
- after_filter['barg'] = {:bargain=>'niag', 'blah'=>'[FILTERED]', 'bar'=>{'bargain'=>{'blah'=>'[FILTERED]'}}}
-
- assert_equal after_filter, parameter_filter.filter(before_filter)
+ test "parameter filter is deprecated" do
+ assert_deprecated do
+ ActionDispatch::Http::ParameterFilter.new(["blah"])
end
end
test "filtered_parameters returns params filtered" do
request = stub_request(
- 'action_dispatch.request.parameters' => {
- 'lifo' => 'Pratik',
- 'amount' => '420',
- 'step' => '1'
+ "action_dispatch.request.parameters" => {
+ "lifo" => "Pratik",
+ "amount" => "420",
+ "step" => "1"
},
- 'action_dispatch.parameter_filter' => [:lifo, :amount]
+ "action_dispatch.parameter_filter" => [:lifo, :amount]
)
params = request.filtered_parameters
@@ -1027,12 +1083,12 @@ class RequestParameterFilter < BaseRequestTest
test "filtered_env filters env as a whole" do
request = stub_request(
- 'action_dispatch.request.parameters' => {
- 'amount' => '420',
- 'step' => '1'
+ "action_dispatch.request.parameters" => {
+ "amount" => "420",
+ "step" => "1"
},
"RAW_POST_DATA" => "yada yada",
- 'action_dispatch.parameter_filter' => [:lifo, :amount]
+ "action_dispatch.parameter_filter" => [:lifo, :amount]
)
request = stub_request(request.filtered_env)
@@ -1044,9 +1100,9 @@ class RequestParameterFilter < BaseRequestTest
test "filtered_path returns path with filtered query string" do
%w(; &).each do |sep|
request = stub_request(
- 'QUERY_STRING' => %w(username=sikachu secret=bd4f21f api_key=b1bc3b3cd352f68d79d7).join(sep),
- 'PATH_INFO' => '/authenticate',
- 'action_dispatch.parameter_filter' => [:secret, :api_key]
+ "QUERY_STRING" => %w(username=sikachu secret=bd4f21f api_key=b1bc3b3cd352f68d79d7).join(sep),
+ "PATH_INFO" => "/authenticate",
+ "action_dispatch.parameter_filter" => [:secret, :api_key]
)
path = request.filtered_path
@@ -1056,9 +1112,9 @@ class RequestParameterFilter < BaseRequestTest
test "filtered_path should not unescape a genuine '[FILTERED]' value" do
request = stub_request(
- 'QUERY_STRING' => "secret=bd4f21f&genuine=%5BFILTERED%5D",
- 'PATH_INFO' => '/authenticate',
- 'action_dispatch.parameter_filter' => [:secret]
+ "QUERY_STRING" => "secret=bd4f21f&genuine=%5BFILTERED%5D",
+ "PATH_INFO" => "/authenticate",
+ "action_dispatch.parameter_filter" => [:secret]
)
path = request.filtered_path
@@ -1067,9 +1123,9 @@ class RequestParameterFilter < BaseRequestTest
test "filtered_path should preserve duplication of keys in query string" do
request = stub_request(
- 'QUERY_STRING' => "username=sikachu&secret=bd4f21f&username=fxn",
- 'PATH_INFO' => '/authenticate',
- 'action_dispatch.parameter_filter' => [:secret]
+ "QUERY_STRING" => "username=sikachu&secret=bd4f21f&username=fxn",
+ "PATH_INFO" => "/authenticate",
+ "action_dispatch.parameter_filter" => [:secret]
)
path = request.filtered_path
@@ -1078,9 +1134,9 @@ class RequestParameterFilter < BaseRequestTest
test "filtered_path should ignore searchparts" do
request = stub_request(
- 'QUERY_STRING' => "secret",
- 'PATH_INFO' => '/authenticate',
- 'action_dispatch.parameter_filter' => [:secret]
+ "QUERY_STRING" => "secret",
+ "PATH_INFO" => "/authenticate",
+ "action_dispatch.parameter_filter" => [:secret]
)
path = request.filtered_path
@@ -1089,37 +1145,42 @@ class RequestParameterFilter < BaseRequestTest
end
class RequestEtag < BaseRequestTest
- test "if_none_match_etags none" do
- request = stub_request
+ test "always matches *" do
+ request = stub_request("HTTP_IF_NONE_MATCH" => "*")
- assert_equal nil, request.if_none_match
- assert_equal [], request.if_none_match_etags
- assert !request.etag_matches?("foo")
- assert !request.etag_matches?(nil)
+ assert_equal "*", request.if_none_match
+ assert_equal ["*"], request.if_none_match_etags
+
+ assert request.etag_matches?('"strong"')
+ assert request.etag_matches?('W/"weak"')
+ assert_not request.etag_matches?(nil)
end
- test "if_none_match_etags single" do
- header = 'the-etag'
- request = stub_request('HTTP_IF_NONE_MATCH' => header)
+ test "doesn't match absent If-None-Match" do
+ request = stub_request
+
+ assert_nil request.if_none_match
+ assert_equal [], request.if_none_match_etags
- assert_equal header, request.if_none_match
- assert_equal [header], request.if_none_match_etags
- assert request.etag_matches?("the-etag")
+ assert_not request.etag_matches?("foo")
+ assert_not request.etag_matches?(nil)
end
- test "if_none_match_etags quoted single" do
+ test "matches opaque ETag validators without unquoting" do
header = '"the-etag"'
- request = stub_request('HTTP_IF_NONE_MATCH' => header)
+ request = stub_request("HTTP_IF_NONE_MATCH" => header)
assert_equal header, request.if_none_match
- assert_equal ['the-etag'], request.if_none_match_etags
- assert request.etag_matches?("the-etag")
+ assert_equal ['"the-etag"'], request.if_none_match_etags
+
+ assert request.etag_matches?('"the-etag"')
+ assert_not request.etag_matches?("the-etag")
end
test "if_none_match_etags multiple" do
header = 'etag1, etag2, "third etag", "etag4"'
- expected = ['etag1', 'etag2', 'third etag', 'etag4']
- request = stub_request('HTTP_IF_NONE_MATCH' => header)
+ expected = ["etag1", "etag2", '"third etag"', '"etag4"']
+ request = stub_request("HTTP_IF_NONE_MATCH" => header)
assert_equal header, request.if_none_match
assert_equal expected, request.if_none_match_etags
@@ -1135,42 +1196,77 @@ class RequestVariant < BaseRequestTest
@request = stub_request
end
- test 'setting variant to a symbol' do
+ test "setting variant to a symbol" do
@request.variant = :phone
- assert @request.variant.phone?
- assert_not @request.variant.tablet?
+ assert_predicate @request.variant, :phone?
+ assert_not_predicate @request.variant, :tablet?
assert @request.variant.any?(:phone, :tablet)
assert_not @request.variant.any?(:tablet, :desktop)
end
- test 'setting variant to an array of symbols' do
+ test "setting variant to an array of symbols" do
@request.variant = [:phone, :tablet]
- assert @request.variant.phone?
- assert @request.variant.tablet?
- assert_not @request.variant.desktop?
+ assert_predicate @request.variant, :phone?
+ assert_predicate @request.variant, :tablet?
+ assert_not_predicate @request.variant, :desktop?
assert @request.variant.any?(:tablet, :desktop)
assert_not @request.variant.any?(:desktop, :watch)
end
- test 'clearing variant' do
+ test "clearing variant" do
@request.variant = nil
- assert @request.variant.empty?
- assert_not @request.variant.phone?
+ assert_empty @request.variant
+ assert_not_predicate @request.variant, :phone?
assert_not @request.variant.any?(:phone, :tablet)
end
- test 'setting variant to a non-symbol value' do
+ test "setting variant to a non-symbol value" do
assert_raise ArgumentError do
- @request.variant = 'phone'
+ @request.variant = "phone"
end
end
- test 'setting variant to an array containing a non-symbol value' do
+ test "setting variant to an array containing a non-symbol value" do
assert_raise ArgumentError do
- @request.variant = [:phone, 'tablet']
+ @request.variant = [:phone, "tablet"]
end
end
end
+
+class RequestFormData < BaseRequestTest
+ test "media_type is from the FORM_DATA_MEDIA_TYPES array" do
+ assert_predicate stub_request("CONTENT_TYPE" => "application/x-www-form-urlencoded"), :form_data?
+ assert_predicate stub_request("CONTENT_TYPE" => "multipart/form-data"), :form_data?
+ end
+
+ test "media_type is not from the FORM_DATA_MEDIA_TYPES array" do
+ assert_not_predicate stub_request("CONTENT_TYPE" => "application/xml"), :form_data?
+ assert_not_predicate stub_request("CONTENT_TYPE" => "multipart/related"), :form_data?
+ end
+
+ test "no Content-Type header is provided and the request_method is POST" do
+ request = stub_request("REQUEST_METHOD" => "POST")
+
+ assert_equal "", request.media_type
+ assert_equal "POST", request.request_method
+ assert_not_predicate request, :form_data?
+ end
+end
+
+class EarlyHintsRequestTest < BaseRequestTest
+ def setup
+ super
+ @env["rack.early_hints"] = lambda { |links| links }
+ @request = stub_request
+ end
+
+ test "when early hints is set in the env link headers are sent" do
+ early_hints = @request.send_early_hints("Link" => "</style.css>; rel=preload; as=style\n</script.js>; rel=preload")
+ expected_hints = { "Link" => "</style.css>; rel=preload; as=style\n</script.js>; rel=preload" }
+
+ assert_equal expected_hints, early_hints
+ end
+end
diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb
index 7aca251066..0f37d074af 100644
--- a/actionpack/test/dispatch/response_test.rb
+++ b/actionpack/test/dispatch/response_test.rb
@@ -1,10 +1,13 @@
-require 'abstract_unit'
-require 'timeout'
-require 'rack/content_length'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "timeout"
+require "rack/content_length"
class ResponseTest < ActiveSupport::TestCase
def setup
- @response = ActionDispatch::Response.new
+ @response = ActionDispatch::Response.create
+ @response.request = ActionDispatch::Request.empty
end
def test_can_wait_until_commit
@@ -12,13 +15,13 @@ class ResponseTest < ActiveSupport::TestCase
@response.await_commit
}
@response.commit!
- assert @response.committed?
+ assert_predicate @response, :committed?
assert t.join(0.5)
end
def test_stream_close
@response.stream.close
- assert @response.stream.closed?
+ assert_predicate @response.stream, :closed?
end
def test_stream_write
@@ -36,12 +39,84 @@ class ResponseTest < ActiveSupport::TestCase
assert_equal "closed stream", e.message
end
+ def test_each_isnt_called_if_str_body_is_written
+ # Controller writes and reads response body
+ each_counter = 0
+ @response.body = Object.new.tap { |o| o.singleton_class.send(:define_method, :each) { |&block| each_counter += 1; block.call "foo" } }
+ @response["X-Foo"] = @response.body
+
+ assert_equal 1, each_counter, "#each was not called once"
+
+ # Build response
+ status, headers, body = @response.to_a
+
+ assert_equal 200, status
+ assert_equal "foo", headers["X-Foo"]
+ assert_equal "foo", body.each.to_a.join
+
+ # Show that #each was not called twice
+ assert_equal 1, each_counter, "#each was not called once"
+ end
+
+ def test_set_header_after_read_body_during_action
+ @response.body
+
+ # set header after the action reads back @response.body
+ @response["x-header"] = "Best of all possible worlds."
+
+ # the response can be built.
+ status, headers, body = @response.to_a
+ assert_equal 200, status
+ assert_equal "", body.body
+
+ assert_equal "Best of all possible worlds.", headers["x-header"]
+ end
+
+ def test_read_body_during_action
+ @response.body = "Hello, World!"
+
+ # even though there's no explicitly set content-type,
+ assert_nil @response.content_type
+
+ # after the action reads back @response.body,
+ assert_equal "Hello, World!", @response.body
+
+ # the response can be built.
+ status, headers, body = @response.to_a
+ assert_equal 200, status
+ assert_equal({
+ "Content-Type" => "text/html; charset=utf-8"
+ }, headers)
+
+ parts = []
+ body.each { |part| parts << part }
+ assert_equal ["Hello, World!"], parts
+ end
+
def test_response_body_encoding
body = ["hello".encode(Encoding::UTF_8)]
response = ActionDispatch::Response.new 200, {}, body
+ response.request = ActionDispatch::Request.empty
assert_equal Encoding::UTF_8, response.body.encoding
end
+ def test_response_charset_writer
+ @response.charset = "utf-16"
+ assert_equal "utf-16", @response.charset
+ @response.charset = nil
+ assert_equal "utf-8", @response.charset
+ end
+
+ def test_setting_content_type_header_impacts_content_type_method
+ @response.headers["Content-Type"] = "application/aaron"
+ 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!"
@@ -57,7 +132,14 @@ class ResponseTest < ActiveSupport::TestCase
end
test "status handled properly in initialize" do
- assert_equal 200, ActionDispatch::Response.new('200 OK').status
+ assert_equal 200, ActionDispatch::Response.new("200 OK").status
+ end
+
+ def test_only_set_charset_still_defaults_to_text_html
+ response = ActionDispatch::Response.new
+ response.charset = "utf-16"
+ _, headers, _ = response.to_a
+ assert_equal "text/html; charset=utf-16", headers["Content-Type"]
end
test "utf8 output" do
@@ -70,12 +152,32 @@ class ResponseTest < ActiveSupport::TestCase
}, headers)
end
+ test "content length" do
+ [100, 101, 102, 204].each do |c|
+ @response = ActionDispatch::Response.new
+ @response.status = c.to_s
+ @response.set_header "Content-Length", "0"
+ _, headers, _ = @response.to_a
+ assert_not headers.has_key?("Content-Length"), "#{c} must not have a Content-Length header field"
+ end
+ end
+
+ test "does not contain a message-body" do
+ [100, 101, 102, 204, 304].each do |c|
+ @response = ActionDispatch::Response.new
+ @response.status = c.to_s
+ @response.body = "Body must not be included"
+ _, _, body = @response.to_a
+ assert_empty body, "#{c} must not have a message-body but actually contains #{body}"
+ end
+ end
+
test "content type" do
[204, 304].each do |c|
@response = ActionDispatch::Response.new
@response.status = c.to_s
_, headers, _ = @response.to_a
- assert !headers.has_key?("Content-Type"), "#{c} should not have Content-Type header"
+ assert_not headers.has_key?("Content-Type"), "#{c} should not have Content-Type header"
end
[200, 302, 404, 500].each do |c|
@@ -89,7 +191,7 @@ class ResponseTest < ActiveSupport::TestCase
test "does not include Status header" do
@response.status = "200 OK"
_, headers, _ = @response.to_a
- assert !headers.has_key?('Status')
+ assert_not headers.has_key?("Status")
end
test "response code" do
@@ -126,77 +228,111 @@ class ResponseTest < ActiveSupport::TestCase
end
test "cookies" do
- @response.set_cookie("user_name", :value => "david", :path => "/")
- status, headers, body = @response.to_a
+ @response.set_cookie("user_name", value: "david", path: "/")
+ _status, headers, _body = @response.to_a
assert_equal "user_name=david; path=/", headers["Set-Cookie"]
- assert_equal({"user_name" => "david"}, @response.cookies)
+ assert_equal({ "user_name" => "david" }, @response.cookies)
+ end
- @response.set_cookie("login", :value => "foo&bar", :path => "/", :expires => Time.utc(2005, 10, 10,5))
- status, headers, body = @response.to_a
+ test "multiple cookies" do
+ @response.set_cookie("user_name", value: "david", path: "/")
+ @response.set_cookie("login", value: "foo&bar", path: "/", expires: Time.utc(2005, 10, 10, 5))
+ _status, headers, _body = @response.to_a
assert_equal "user_name=david; path=/\nlogin=foo%26bar; path=/; expires=Mon, 10 Oct 2005 05:00:00 -0000", headers["Set-Cookie"]
- assert_equal({"login" => "foo&bar", "user_name" => "david"}, @response.cookies)
+ assert_equal({ "login" => "foo&bar", "user_name" => "david" }, @response.cookies)
+ end
+ test "delete cookies" do
+ @response.set_cookie("user_name", value: "david", path: "/")
+ @response.set_cookie("login", value: "foo&bar", path: "/", expires: Time.utc(2005, 10, 10, 5))
@response.delete_cookie("login")
- status, headers, body = @response.to_a
- assert_equal({"user_name" => "david", "login" => nil}, @response.cookies)
+ assert_equal({ "user_name" => "david", "login" => nil }, @response.cookies)
end
- test "read cache control" do
+ test "read ETag and Cache-Control" do
resp = ActionDispatch::Response.new.tap { |response|
response.cache_control[:public] = true
- response.etag = '123'
- response.body = 'Hello'
+ response.etag = "123"
+ response.body = "Hello"
}
resp.to_a
- assert_equal('"202cb962ac59075b964b07152d234b70"', resp.etag)
- assert_equal({:public => true}, resp.cache_control)
+ assert_predicate resp, :etag?
+ assert_predicate resp, :weak_etag?
+ assert_not_predicate resp, :strong_etag?
+ assert_equal('W/"202cb962ac59075b964b07152d234b70"', resp.etag)
+ assert_equal({ public: true }, resp.cache_control)
+
+ assert_equal("public", resp.headers["Cache-Control"])
+ assert_equal('W/"202cb962ac59075b964b07152d234b70"', resp.headers["ETag"])
+ end
+
+ test "read strong ETag" do
+ resp = ActionDispatch::Response.new.tap { |response|
+ response.cache_control[:public] = true
+ response.strong_etag = "123"
+ response.body = "Hello"
+ }
+ resp.to_a
- assert_equal('public', resp.headers['Cache-Control'])
- assert_equal('"202cb962ac59075b964b07152d234b70"', resp.headers['ETag'])
+ assert_predicate resp, :etag?
+ assert_not_predicate resp, :weak_etag?
+ assert_predicate resp, :strong_etag?
+ assert_equal('"202cb962ac59075b964b07152d234b70"', resp.etag)
end
test "read charset and content type" do
resp = ActionDispatch::Response.new.tap { |response|
- response.charset = 'utf-16'
- response.content_type = Mime::XML
- response.body = 'Hello'
+ response.charset = "utf-16"
+ response.content_type = Mime[:xml]
+ response.body = "Hello"
}
resp.to_a
- assert_equal('utf-16', resp.charset)
- assert_equal(Mime::XML, resp.content_type)
+ assert_equal("utf-16", resp.charset)
+ assert_equal(Mime[:xml], resp.content_type)
- assert_equal('application/xml; charset=utf-16', resp.headers['Content-Type'])
+ assert_equal("application/xml; charset=utf-16", resp.headers["Content-Type"])
end
- test "read content type without charset" do
+ test "read content type with default charset utf-8" do
+ resp = ActionDispatch::Response.new(200, "Content-Type" => "text/xml")
+ assert_equal("utf-8", resp.charset)
+ end
+
+ test "read content type with charset utf-16" do
original = ActionDispatch::Response.default_charset
begin
- ActionDispatch::Response.default_charset = 'utf-16'
- resp = ActionDispatch::Response.new(200, { "Content-Type" => "text/xml" })
- assert_equal('utf-16', resp.charset)
+ ActionDispatch::Response.default_charset = "utf-16"
+ resp = ActionDispatch::Response.new(200, "Content-Type" => "text/xml")
+ assert_equal("utf-16", resp.charset)
ensure
ActionDispatch::Response.default_charset = original
end
end
- test "read x_frame_options, x_content_type_options and x_xss_protection" do
+ test "read x_frame_options, x_content_type_options, x_xss_protection, x_download_options and x_permitted_cross_domain_policies, referrer_policy" do
original_default_headers = ActionDispatch::Response.default_headers
begin
ActionDispatch::Response.default_headers = {
- 'X-Frame-Options' => 'DENY',
- 'X-Content-Type-Options' => 'nosniff',
- 'X-XSS-Protection' => '1;'
+ "X-Frame-Options" => "DENY",
+ "X-Content-Type-Options" => "nosniff",
+ "X-XSS-Protection" => "1;",
+ "X-Download-Options" => "noopen",
+ "X-Permitted-Cross-Domain-Policies" => "none",
+ "Referrer-Policy" => "strict-origin-when-cross-origin"
}
- resp = ActionDispatch::Response.new.tap { |response|
- response.body = 'Hello'
+ resp = ActionDispatch::Response.create.tap { |response|
+ response.body = "Hello"
}
resp.to_a
- assert_equal('DENY', resp.headers['X-Frame-Options'])
- assert_equal('nosniff', resp.headers['X-Content-Type-Options'])
- assert_equal('1;', resp.headers['X-XSS-Protection'])
+ assert_equal("DENY", resp.headers["X-Frame-Options"])
+ assert_equal("nosniff", resp.headers["X-Content-Type-Options"])
+ assert_equal("1;", resp.headers["X-XSS-Protection"])
+ assert_equal("noopen", resp.headers["X-Download-Options"])
+ assert_equal("none", resp.headers["X-Permitted-Cross-Domain-Policies"])
+ assert_equal("strict-origin-when-cross-origin", resp.headers["Referrer-Policy"])
ensure
ActionDispatch::Response.default_headers = original_default_headers
end
@@ -206,31 +342,32 @@ class ResponseTest < ActiveSupport::TestCase
original_default_headers = ActionDispatch::Response.default_headers
begin
ActionDispatch::Response.default_headers = {
- 'X-XX-XXXX' => 'Here is my phone number'
+ "X-XX-XXXX" => "Here is my phone number"
}
- resp = ActionDispatch::Response.new.tap { |response|
- response.body = 'Hello'
+ resp = ActionDispatch::Response.create.tap { |response|
+ response.body = "Hello"
}
resp.to_a
- assert_equal('Here is my phone number', resp.headers['X-XX-XXXX'])
+ assert_equal("Here is my phone number", resp.headers["X-XX-XXXX"])
ensure
ActionDispatch::Response.default_headers = original_default_headers
end
end
test "respond_to? accepts include_private" do
- assert_not @response.respond_to?(:method_missing)
+ assert_not_respond_to @response, :method_missing
assert @response.respond_to?(:method_missing, true)
end
test "can be explicitly destructured into status, headers and an enumerable body" do
- response = ActionDispatch::Response.new(404, { 'Content-Type' => 'text/plain' }, ['Not Found'])
+ response = ActionDispatch::Response.new(404, { "Content-Type" => "text/plain" }, ["Not Found"])
+ response.request = ActionDispatch::Request.empty
status, headers, body = *response
assert_equal 404, status
- assert_equal({ 'Content-Type' => 'text/plain' }, headers)
- assert_equal ['Not Found'], body.each.to_a
+ assert_equal({ "Content-Type" => "text/plain" }, headers)
+ assert_equal ["Not Found"], body.each.to_a
end
test "[response.to_a].flatten does not recurse infinitely" do
@@ -243,15 +380,74 @@ class ResponseTest < ActiveSupport::TestCase
end
test "compatibility with Rack::ContentLength" do
- @response.body = 'Hello'
+ @response.body = "Hello"
app = lambda { |env| @response.to_a }
env = Rack::MockRequest.env_for("/")
- status, headers, body = app.call(env)
- assert_nil headers['Content-Length']
+ _status, headers, _body = app.call(env)
+ assert_nil headers["Content-Length"]
+
+ _status, headers, _body = Rack::ContentLength.new(app).call(env)
+ assert_equal "5", headers["Content-Length"]
+ end
+end
+
+class ResponseHeadersTest < ActiveSupport::TestCase
+ def setup
+ @response = ActionDispatch::Response.create
+ @response.set_header "Foo", "1"
+ end
+
+ test "has_header?" do
+ assert @response.has_header? "Foo"
+ assert_not @response.has_header? "foo"
+ assert_not @response.has_header? nil
+ end
+
+ test "get_header" do
+ assert_equal "1", @response.get_header("Foo")
+ assert_nil @response.get_header("foo")
+ assert_nil @response.get_header(nil)
+ end
+
+ test "set_header" do
+ assert_equal "2", @response.set_header("Foo", "2")
+ assert @response.has_header?("Foo")
+ assert_equal "2", @response.get_header("Foo")
+
+ assert_nil @response.set_header("Foo", nil)
+ assert @response.has_header?("Foo")
+ assert_nil @response.get_header("Foo")
+ end
+
+ test "delete_header" do
+ assert_nil @response.delete_header(nil)
+
+ assert_nil @response.delete_header("foo")
+ assert @response.has_header?("Foo")
+
+ assert_equal "1", @response.delete_header("Foo")
+ assert_not @response.has_header?("Foo")
+ end
+
+ test "add_header" do
+ # Add a value to an existing header
+ assert_equal "1,2", @response.add_header("Foo", "2")
+ assert_equal "1,2", @response.get_header("Foo")
+
+ # Add nil to an existing header
+ assert_equal "1,2", @response.add_header("Foo", nil)
+ assert_equal "1,2", @response.get_header("Foo")
- status, headers, body = Rack::ContentLength.new(app).call(env)
- assert_equal '5', headers['Content-Length']
+ # Add nil to a nonexistent header
+ assert_nil @response.add_header("Bar", nil)
+ assert_not @response.has_header?("Bar")
+ assert_nil @response.get_header("Bar")
+
+ # Add a value to a nonexistent header
+ assert_equal "1", @response.add_header("Bar", "1")
+ assert @response.has_header?("Bar")
+ assert_equal "1", @response.get_header("Bar")
end
end
@@ -260,69 +456,87 @@ class ResponseIntegrationTest < ActionDispatch::IntegrationTest
@app = lambda { |env|
ActionDispatch::Response.new.tap { |resp|
resp.cache_control[:public] = true
- resp.etag = '123'
- resp.body = 'Hello'
+ resp.etag = "123"
+ resp.body = "Hello"
+ resp.request = ActionDispatch::Request.empty
}.to_a
}
- get '/'
+ get "/"
assert_response :success
- assert_equal('public', @response.headers['Cache-Control'])
- assert_equal('"202cb962ac59075b964b07152d234b70"', @response.headers['ETag'])
+ assert_equal("public", @response.headers["Cache-Control"])
+ assert_equal('W/"202cb962ac59075b964b07152d234b70"', @response.headers["ETag"])
- assert_equal('"202cb962ac59075b964b07152d234b70"', @response.etag)
- assert_equal({:public => true}, @response.cache_control)
+ assert_equal('W/"202cb962ac59075b964b07152d234b70"', @response.etag)
+ assert_equal({ public: true }, @response.cache_control)
end
test "response cache control from rackish app" do
@app = lambda { |env|
[200,
- {'ETag' => '"202cb962ac59075b964b07152d234b70"',
- 'Cache-Control' => 'public'}, ['Hello']]
+ { "ETag" => 'W/"202cb962ac59075b964b07152d234b70"',
+ "Cache-Control" => "public" }, ["Hello"]]
}
- get '/'
+ get "/"
assert_response :success
- assert_equal('public', @response.headers['Cache-Control'])
- assert_equal('"202cb962ac59075b964b07152d234b70"', @response.headers['ETag'])
+ assert_equal("public", @response.headers["Cache-Control"])
+ assert_equal('W/"202cb962ac59075b964b07152d234b70"', @response.headers["ETag"])
- assert_equal('"202cb962ac59075b964b07152d234b70"', @response.etag)
- assert_equal({:public => true}, @response.cache_control)
+ assert_equal('W/"202cb962ac59075b964b07152d234b70"', @response.etag)
+ assert_equal({ public: true }, @response.cache_control)
end
test "response charset and content type from railsish app" do
@app = lambda { |env|
ActionDispatch::Response.new.tap { |resp|
- resp.charset = 'utf-16'
- resp.content_type = Mime::XML
- resp.body = 'Hello'
+ resp.charset = "utf-16"
+ resp.content_type = Mime[:xml]
+ resp.body = "Hello"
+ resp.request = ActionDispatch::Request.empty
}.to_a
}
- get '/'
+ get "/"
assert_response :success
- assert_equal('utf-16', @response.charset)
- assert_equal(Mime::XML, @response.content_type)
+ assert_equal("utf-16", @response.charset)
+ assert_equal(Mime[:xml], @response.content_type)
- assert_equal('application/xml; charset=utf-16', @response.headers['Content-Type'])
+ assert_equal("application/xml; charset=utf-16", @response.headers["Content-Type"])
end
test "response charset and content type from rackish app" do
@app = lambda { |env|
[200,
- {'Content-Type' => 'application/xml; charset=utf-16'},
- ['Hello']]
+ { "Content-Type" => "application/xml; charset=utf-16" },
+ ["Hello"]]
}
- get '/'
+ get "/"
assert_response :success
- assert_equal('utf-16', @response.charset)
- assert_equal(Mime::XML, @response.content_type)
+ assert_equal("utf-16", @response.charset)
+ assert_equal(Mime[:xml], @response.content_type)
+
+ assert_equal("application/xml; charset=utf-16", @response.headers["Content-Type"])
+ end
+
+ test "strong ETag validator" do
+ @app = lambda { |env|
+ ActionDispatch::Response.new.tap { |resp|
+ resp.strong_etag = "123"
+ resp.body = "Hello"
+ resp.request = ActionDispatch::Request.empty
+ }.to_a
+ }
+
+ get "/"
+ assert_response :ok
- assert_equal('application/xml; charset=utf-16', @response.headers['Content-Type'])
+ assert_equal('"202cb962ac59075b964b07152d234b70"', @response.headers["ETag"])
+ assert_equal('"202cb962ac59075b964b07152d234b70"', @response.etag)
end
end
diff --git a/actionpack/test/dispatch/routing/concerns_test.rb b/actionpack/test/dispatch/routing/concerns_test.rb
index 7ef513b0c8..503a7ccd56 100644
--- a/actionpack/test/dispatch/routing/concerns_test.rb
+++ b/actionpack/test/dispatch/routing/concerns_test.rb
@@ -1,4 +1,8 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class ReviewsController < ResourcesController; end
class RoutingConcernsTest < ActionDispatch::IntegrationTest
class Reviewable
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..a1a1e79884
--- /dev/null
+++ b/actionpack/test/dispatch/routing/custom_url_helpers_test.rb
@@ -0,0 +1,333 @@
+# frozen_string_literal: true
+
+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(ActionController::UnfilteredParameters) { params_path(@unsafe_params) }
+ assert_raises(ActionController::UnfilteredParameters) { 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(ActionController::UnfilteredParameters) { params_url(@unsafe_params) }
+ assert_raises(ActionController::UnfilteredParameters) { 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
+
+ def test_defining_direct_url_registers_helper_method
+ assert_equal "http://www.example.com/basket", Routes.url_helpers.symbol_url
+ assert_equal true, Routes.named_routes.route_defined?(:symbol_url), "'symbol_url' named helper not found"
+ assert_equal true, Routes.named_routes.route_defined?(:symbol_path), "'symbol_path' named helper not found"
+ end
+end
diff --git a/actionpack/test/dispatch/routing/inspector_test.rb b/actionpack/test/dispatch/routing/inspector_test.rb
index 4047214843..fe1f1995d8 100644
--- a/actionpack/test/dispatch/routing/inspector_test.rb
+++ b/actionpack/test/dispatch/routing/inspector_test.rb
@@ -1,29 +1,23 @@
-require 'abstract_unit'
-require 'rails/engine'
-require 'action_dispatch/routing/inspector'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "rails/engine"
+require "action_dispatch/routing/inspector"
+require "io/console/size"
class MountedRackApp
def self.call(env)
end
end
+class Rails::DummyController
+end
+
module ActionDispatch
module Routing
class RoutesInspectorTest < ActiveSupport::TestCase
- def setup
+ setup do
@set = ActionDispatch::Routing::RouteSet.new
- app = ActiveSupport::OrderedOptions.new
- app.config = ActiveSupport::OrderedOptions.new
- app.config.assets = ActiveSupport::OrderedOptions.new
- app.config.assets.prefix = '/sprockets'
- Rails.stubs(:application).returns(app)
- Rails.stubs(:env).returns("development")
- end
-
- def draw(options = {}, &block)
- @set.draw(&block)
- inspector = ActionDispatch::Routing::RoutesInspector.new(@set.routes)
- inspector.format(ActionDispatch::Routing::ConsoleFormatter.new, options[:filter]).split("\n")
end
def test_displaying_routes_for_engines
@@ -33,11 +27,11 @@ module ActionDispatch
end
end
engine.routes.draw do
- get '/cart', :to => 'cart#show'
+ get "/cart", to: "cart#show"
end
output = draw do
- get '/custom/assets', :to => 'custom_assets#show'
+ get "/custom/assets", to: "custom_assets#show"
mount engine => "/blog", :as => "blog"
end
@@ -74,7 +68,7 @@ module ActionDispatch
def test_cart_inspect
output = draw do
- get '/cart', :to => 'cart#show'
+ get "/cart", to: "cart#show"
end
assert_equal [
@@ -83,9 +77,20 @@ module ActionDispatch
], output
end
+ def test_articles_inspect_with_multiple_verbs
+ output = draw do
+ match "articles/:id", to: "articles#update", via: [:put, :patch]
+ end
+
+ assert_equal [
+ "Prefix Verb URI Pattern Controller#Action",
+ " PUT|PATCH /articles/:id(.:format) articles#update"
+ ], output
+ end
+
def test_inspect_shows_custom_assets
output = draw do
- get '/custom/assets', :to => 'custom_assets#show'
+ get "/custom/assets", to: "custom_assets#show"
end
assert_equal [
@@ -114,7 +119,7 @@ module ActionDispatch
def test_inspect_routes_shows_root_route
output = draw do
- root :to => 'pages#main'
+ root to: "pages#main"
end
assert_equal [
@@ -125,7 +130,9 @@ module ActionDispatch
def test_inspect_routes_shows_dynamic_action_route
output = draw do
- get 'api/:action' => 'api'
+ ActiveSupport::Deprecation.silence do
+ get "api/:action" => "api"
+ end
end
assert_equal [
@@ -136,7 +143,9 @@ module ActionDispatch
def test_inspect_routes_shows_controller_and_action_only_route
output = draw do
- get ':controller/:action'
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action"
+ end
end
assert_equal [
@@ -147,7 +156,9 @@ module ActionDispatch
def test_inspect_routes_shows_controller_and_action_route_with_constraints
output = draw do
- get ':controller(/:action(/:id))', :id => /\d+/
+ ActiveSupport::Deprecation.silence do
+ get ":controller(/:action(/:id))", id: /\d+/
+ end
end
assert_equal [
@@ -156,20 +167,20 @@ module ActionDispatch
], output
end
- def test_rake_routes_shows_route_with_defaults
+ def test_rails_routes_shows_route_with_defaults
output = draw do
- get 'photos/:id' => 'photos#show', :defaults => {:format => 'jpg'}
+ get "photos/:id" => "photos#show", :defaults => { format: "jpg" }
end
assert_equal [
"Prefix Verb URI Pattern Controller#Action",
- %Q[ GET /photos/:id(.:format) photos#show {:format=>"jpg"}]
+ ' GET /photos/:id(.:format) photos#show {:format=>"jpg"}'
], output
end
- def test_rake_routes_shows_route_with_constraints
+ def test_rails_routes_shows_route_with_constraints
output = draw do
- get 'photos/:id' => 'photos#show', :id => /[A-Z]\d{5}/
+ get "photos/:id" => "photos#show", :id => /[A-Z]\d{5}/
end
assert_equal [
@@ -178,15 +189,15 @@ module ActionDispatch
], output
end
- def test_rake_routes_shows_routes_with_dashes
+ def test_rails_routes_shows_routes_with_dashes
output = draw do
- get 'about-us' => 'pages#about_us'
- get 'our-work/latest'
+ get "about-us" => "pages#about_us"
+ get "our-work/latest"
resources :photos, only: [:show] do
- get 'user-favorites', on: :collection
- get 'preview-photo', on: :member
- get 'summary-text'
+ get "user-favorites", on: :collection
+ get "preview-photo", on: :member
+ get "summary-text"
end
end
@@ -201,9 +212,9 @@ module ActionDispatch
], output
end
- def test_rake_routes_shows_route_with_rack_app
+ def test_rails_routes_shows_route_with_rack_app
output = draw do
- get 'foo/:id' => MountedRackApp, :id => /[A-Z]\d{5}/
+ get "foo/:id" => MountedRackApp, :id => /[A-Z]\d{5}/
end
assert_equal [
@@ -212,9 +223,9 @@ module ActionDispatch
], output
end
- def test_rake_routes_shows_named_route_with_mounted_rack_app
+ def test_rails_routes_shows_named_route_with_mounted_rack_app
output = draw do
- mount MountedRackApp => '/foo'
+ mount MountedRackApp => "/foo"
end
assert_equal [
@@ -223,9 +234,9 @@ module ActionDispatch
], output
end
- def test_rake_routes_shows_overridden_named_route_with_mounted_rack_app_with_name
+ def test_rails_routes_shows_overridden_named_route_with_mounted_rack_app_with_name
output = draw do
- mount MountedRackApp => '/foo', as: 'blog'
+ mount MountedRackApp => "/foo", as: "blog"
end
assert_equal [
@@ -234,7 +245,7 @@ module ActionDispatch
], output
end
- def test_rake_routes_shows_route_with_rack_app_nested_with_dynamic_constraints
+ def test_rails_routes_shows_route_with_rack_app_nested_with_dynamic_constraints
constraint = Class.new do
def inspect
"( my custom constraint )"
@@ -242,8 +253,8 @@ module ActionDispatch
end
output = draw do
- scope :constraint => constraint.new do
- mount MountedRackApp => '/foo'
+ scope constraint: constraint.new do
+ mount MountedRackApp => "/foo"
end
end
@@ -253,18 +264,18 @@ module ActionDispatch
], output
end
- def test_rake_routes_dont_show_app_mounted_in_assets_prefix
+ def test_rails_routes_dont_show_app_mounted_in_assets_prefix
output = draw do
- get '/sprockets' => MountedRackApp
+ get "/sprockets" => MountedRackApp
end
assert_no_match(/MountedRackApp/, output.first)
assert_no_match(/\/sprockets/, output.first)
end
- def test_rake_routes_shows_route_defined_in_under_assets_prefix
+ def test_rails_routes_shows_route_defined_in_under_assets_prefix
output = draw do
- scope '/sprockets' do
- get '/foo' => 'foo#bar'
+ scope "/sprockets" do
+ get "/foo" => "foo#bar"
end
end
assert_equal [
@@ -275,9 +286,9 @@ module ActionDispatch
def test_redirect
output = draw do
- get "/foo" => redirect("/foo/bar"), :constraints => { :subdomain => "admin" }
+ get "/foo" => redirect("/foo/bar"), :constraints => { subdomain: "admin" }
get "/bar" => redirect(path: "/foo/bar", status: 307)
- get "/foobar" => redirect{ "/foo/bar" }
+ get "/foobar" => redirect { "/foo/bar" }
end
assert_equal [
@@ -289,7 +300,7 @@ module ActionDispatch
end
def test_routes_can_be_filtered
- output = draw(filter: 'posts') do
+ output = draw(grep: "posts") do
resources :articles
resources :posts
end
@@ -305,23 +316,109 @@ module ActionDispatch
" DELETE /posts/:id(.:format) posts#destroy"], output
end
+ def test_routes_when_expanded
+ previous_console_winsize = IO.console.winsize
+ IO.console.winsize = [0, 23]
+
+ engine = Class.new(Rails::Engine) do
+ def self.inspect
+ "Blog::Engine"
+ end
+ end
+ engine.routes.draw do
+ get "/cart", to: "cart#show"
+ end
+
+ output = draw(formatter: ActionDispatch::Routing::ConsoleFormatter::Expanded.new) do
+ get "/custom/assets", to: "custom_assets#show"
+ get "/custom/furnitures", to: "custom_furnitures#show"
+ mount engine => "/blog", :as => "blog"
+ end
+
+ assert_equal ["--[ Route 1 ]----------",
+ "Prefix | custom_assets",
+ "Verb | GET",
+ "URI | /custom/assets(.:format)",
+ "Controller#Action | custom_assets#show",
+ "--[ Route 2 ]----------",
+ "Prefix | custom_furnitures",
+ "Verb | GET",
+ "URI | /custom/furnitures(.:format)",
+ "Controller#Action | custom_furnitures#show",
+ "--[ Route 3 ]----------",
+ "Prefix | blog",
+ "Verb | ",
+ "URI | /blog",
+ "Controller#Action | Blog::Engine",
+ "",
+ "[ Routes for Blog::Engine ]",
+ "--[ Route 1 ]----------",
+ "Prefix | cart",
+ "Verb | GET",
+ "URI | /cart(.:format)",
+ "Controller#Action | cart#show"], output
+ ensure
+ IO.console.winsize = previous_console_winsize
+ end
+
+ def test_no_routes_matched_filter_when_expanded
+ output = draw(grep: "rails/dummy", formatter: ActionDispatch::Routing::ConsoleFormatter::Expanded.new) do
+ get "photos/:id" => "photos#show", :id => /[A-Z]\d{5}/
+ end
+
+ assert_equal [
+ "No routes were found for this grep pattern.",
+ "For more information about routes, see the Rails guide: https://guides.rubyonrails.org/routing.html."
+ ], output
+ end
+
+ def test_not_routes_when_expanded
+ output = draw(grep: "rails/dummy", formatter: ActionDispatch::Routing::ConsoleFormatter::Expanded.new) { }
+
+ assert_equal [
+ "You don't have any routes defined!",
+ "",
+ "Please add some routes in config/routes.rb.",
+ "",
+ "For more information about routes, see the Rails guide: https://guides.rubyonrails.org/routing.html."
+ ], output
+ end
+
+ def test_routes_can_be_filtered_with_namespaced_controllers
+ output = draw(grep: "admin/posts") do
+ resources :articles
+ namespace :admin do
+ resources :posts
+ end
+ end
+
+ assert_equal [" Prefix Verb URI Pattern Controller#Action",
+ " admin_posts GET /admin/posts(.:format) admin/posts#index",
+ " POST /admin/posts(.:format) admin/posts#create",
+ " new_admin_post GET /admin/posts/new(.:format) admin/posts#new",
+ "edit_admin_post GET /admin/posts/:id/edit(.:format) admin/posts#edit",
+ " admin_post GET /admin/posts/:id(.:format) admin/posts#show",
+ " PATCH /admin/posts/:id(.:format) admin/posts#update",
+ " PUT /admin/posts/:id(.:format) admin/posts#update",
+ " DELETE /admin/posts/:id(.:format) admin/posts#destroy"], output
+ end
+
def test_regression_route_with_controller_regexp
output = draw do
- get ':controller(/:action)', controller: /api\/[^\/]+/, format: false
+ ActiveSupport::Deprecation.silence do
+ get ":controller(/:action)", controller: /api\/[^\/]+/, format: false
+ end
end
assert_equal ["Prefix Verb URI Pattern Controller#Action",
- " GET /:controller(/:action) (?-mix:api\\/[^\\/]+)#:action"], output
+ " GET /:controller(/:action) :controller#:action"], output
end
def test_inspect_routes_shows_resources_route_when_assets_disabled
@set = ActionDispatch::Routing::RouteSet.new
- app = ActiveSupport::OrderedOptions.new
-
- Rails.stubs(:application).returns(app)
output = draw do
- get '/cart', to: 'cart#show'
+ get "/cart", to: "cart#show"
end
assert_equal [
@@ -329,6 +426,70 @@ module ActionDispatch
" cart GET /cart(.:format) cart#show"
], output
end
+
+ def test_routes_with_undefined_filter
+ output = draw(controller: "Rails::MissingController") do
+ get "photos/:id" => "photos#show", :id => /[A-Z]\d{5}/
+ end
+
+ assert_equal [
+ "No routes were found for this controller.",
+ "For more information about routes, see the Rails guide: https://guides.rubyonrails.org/routing.html."
+ ], output
+ end
+
+ def test_no_routes_matched_filter
+ output = draw(grep: "rails/dummy") do
+ get "photos/:id" => "photos#show", :id => /[A-Z]\d{5}/
+ end
+
+ assert_equal [
+ "No routes were found for this grep pattern.",
+ "For more information about routes, see the Rails guide: https://guides.rubyonrails.org/routing.html."
+ ], output
+ end
+
+ def test_no_routes_were_defined
+ output = draw(grep: "Rails::DummyController") { }
+
+ assert_equal [
+ "You don't have any routes defined!",
+ "",
+ "Please add some routes in config/routes.rb.",
+ "",
+ "For more information about routes, see the Rails guide: https://guides.rubyonrails.org/routing.html."
+ ], output
+ end
+
+ def test_displaying_routes_for_internal_engines
+ engine = Class.new(Rails::Engine) do
+ def self.inspect
+ "Blog::Engine"
+ end
+ end
+ engine.routes.draw do
+ get "/cart", to: "cart#show"
+ post "/cart", to: "cart#create"
+ patch "/cart", to: "cart#update"
+ end
+
+ output = draw do
+ get "/custom/assets", to: "custom_assets#show"
+ mount engine => "/blog", as: "blog", internal: true
+ end
+
+ assert_equal [
+ " Prefix Verb URI Pattern Controller#Action",
+ "custom_assets GET /custom/assets(.:format) custom_assets#show",
+ ], output
+ end
+
+ private
+ def draw(formatter: ActionDispatch::Routing::ConsoleFormatter::Sheet.new, **options, &block)
+ @set.draw(&block)
+ inspector = ActionDispatch::Routing::RoutesInspector.new(@set.routes)
+ inspector.format(formatter, options).split("\n")
+ end
end
end
end
diff --git a/actionpack/test/dispatch/routing/ipv6_redirect_test.rb b/actionpack/test/dispatch/routing/ipv6_redirect_test.rb
new file mode 100644
index 0000000000..31559bffc7
--- /dev/null
+++ b/actionpack/test/dispatch/routing/ipv6_redirect_test.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class IPv6IntegrationTest < ActionDispatch::IntegrationTest
+ Routes = ActionDispatch::Routing::RouteSet.new
+ include Routes.url_helpers
+
+ class ::BadRouteRequestController < ActionController::Base
+ include Routes.url_helpers
+ def index
+ render plain: foo_path
+ end
+
+ def foo
+ redirect_to action: :index
+ end
+ end
+
+ Routes.draw do
+ get "/", to: "bad_route_request#index", as: :index
+ get "/foo", to: "bad_route_request#foo", as: :foo
+ end
+
+ def _routes
+ Routes
+ end
+
+ APP = build_app Routes
+ def app
+ APP
+ end
+
+ test "bad IPv6 redirection" do
+ # def test_simple_redirect
+ request_env = {
+ "REMOTE_ADDR" => "fd07:2fa:6cff:2112:225:90ff:fec7:22aa",
+ "HTTP_HOST" => "[fd07:2fa:6cff:2112:225:90ff:fec7:22aa]:3000",
+ "SERVER_NAME" => "[fd07:2fa:6cff:2112:225:90ff:fec7:22aa]",
+ "SERVER_PORT" => 3000 }
+
+ get "/foo", env: request_env
+ assert_response :redirect
+ assert_equal "http://[fd07:2fa:6cff:2112:225:90ff:fec7:22aa]:3000/", redirect_to_url
+ end
+end
diff --git a/actionpack/test/dispatch/routing/route_set_test.rb b/actionpack/test/dispatch/routing/route_set_test.rb
index 9327fe12c6..e61d47b160 100644
--- a/actionpack/test/dispatch/routing/route_set_test.rb
+++ b/actionpack/test/dispatch/routing/route_set_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module ActionDispatch
module Routing
@@ -9,7 +11,7 @@ module ActionDispatch
end
def call(env)
- [ 200, { 'Content-Type' => 'text/plain' }, [response] ]
+ [ 200, { "Content-Type" => "text/plain" }, [response] ]
end
end
@@ -21,7 +23,7 @@ module ActionDispatch
assert empty?
draw do
- get 'foo', to: SimpleApp.new('foo#index')
+ get "foo", to: SimpleApp.new("foo#index")
end
assert_not empty?
@@ -29,101 +31,101 @@ module ActionDispatch
test "url helpers are added when route is added" do
draw do
- get 'foo', to: SimpleApp.new('foo#index')
+ get "foo", to: SimpleApp.new("foo#index")
end
- assert_equal '/foo', url_helpers.foo_path
+ assert_equal "/foo", url_helpers.foo_path
assert_raises NoMethodError do
- assert_equal '/bar', url_helpers.bar_path
+ assert_equal "/bar", url_helpers.bar_path
end
draw do
- get 'foo', to: SimpleApp.new('foo#index')
- get 'bar', to: SimpleApp.new('bar#index')
+ get "foo", to: SimpleApp.new("foo#index")
+ get "bar", to: SimpleApp.new("bar#index")
end
- assert_equal '/foo', url_helpers.foo_path
- assert_equal '/bar', url_helpers.bar_path
+ assert_equal "/foo", url_helpers.foo_path
+ assert_equal "/bar", url_helpers.bar_path
end
test "url helpers are updated when route is updated" do
draw do
- get 'bar', to: SimpleApp.new('bar#index'), as: :bar
+ get "bar", to: SimpleApp.new("bar#index"), as: :bar
end
- assert_equal '/bar', url_helpers.bar_path
+ assert_equal "/bar", url_helpers.bar_path
draw do
- get 'baz', to: SimpleApp.new('baz#index'), as: :bar
+ get "baz", to: SimpleApp.new("baz#index"), as: :bar
end
- assert_equal '/baz', url_helpers.bar_path
+ assert_equal "/baz", url_helpers.bar_path
end
test "url helpers are removed when route is removed" do
draw do
- get 'foo', to: SimpleApp.new('foo#index')
- get 'bar', to: SimpleApp.new('bar#index')
+ get "foo", to: SimpleApp.new("foo#index")
+ get "bar", to: SimpleApp.new("bar#index")
end
- assert_equal '/foo', url_helpers.foo_path
- assert_equal '/bar', url_helpers.bar_path
+ assert_equal "/foo", url_helpers.foo_path
+ assert_equal "/bar", url_helpers.bar_path
draw do
- get 'foo', to: SimpleApp.new('foo#index')
+ get "foo", to: SimpleApp.new("foo#index")
end
- assert_equal '/foo', url_helpers.foo_path
+ assert_equal "/foo", url_helpers.foo_path
assert_raises NoMethodError do
- assert_equal '/bar', url_helpers.bar_path
+ assert_equal "/bar", url_helpers.bar_path
end
end
test "only_path: true with *_url and no :host option" do
draw do
- get 'foo', to: SimpleApp.new('foo#index')
+ get "foo", to: SimpleApp.new("foo#index")
end
- assert_equal '/foo', url_helpers.foo_url(only_path: true)
+ assert_equal "/foo", url_helpers.foo_url(only_path: true)
end
test "only_path: false with *_url and no :host option" do
draw do
- get 'foo', to: SimpleApp.new('foo#index')
+ get "foo", to: SimpleApp.new("foo#index")
end
assert_raises ArgumentError do
- assert_equal 'http://example.com/foo', url_helpers.foo_url(only_path: false)
+ assert_equal "http://example.com/foo", url_helpers.foo_url(only_path: false)
end
end
test "only_path: false with *_url and local :host option" do
draw do
- get 'foo', to: SimpleApp.new('foo#index')
+ get "foo", to: SimpleApp.new("foo#index")
end
- assert_equal 'http://example.com/foo', url_helpers.foo_url(only_path: false, host: 'example.com')
+ assert_equal "http://example.com/foo", url_helpers.foo_url(only_path: false, host: "example.com")
end
test "only_path: false with *_url and global :host option" do
- @set.default_url_options = { host: 'example.com' }
+ @set.default_url_options = { host: "example.com" }
draw do
- get 'foo', to: SimpleApp.new('foo#index')
+ get "foo", to: SimpleApp.new("foo#index")
end
- assert_equal 'http://example.com/foo', url_helpers.foo_url(only_path: false)
+ assert_equal "http://example.com/foo", url_helpers.foo_url(only_path: false)
end
test "explicit keys win over implicit keys" do
draw do
resources :foo do
- resources :bar, to: SimpleApp.new('foo#show')
+ resources :bar, to: SimpleApp.new("foo#show")
end
end
- assert_equal '/foo/1/bar/2', url_helpers.foo_bar_path(1, 2)
- assert_equal '/foo/1/bar/2', url_helpers.foo_bar_path(2, foo_id: 1)
+ assert_equal "/foo/1/bar/2", url_helpers.foo_bar_path(1, 2)
+ assert_equal "/foo/1/bar/2", url_helpers.foo_bar_path(2, foo_id: 1)
end
test "having an optional scope with resources" do
@@ -133,9 +135,18 @@ module ActionDispatch
end
end
- assert_equal '/users/1', url_helpers.user_path(1)
- assert_equal '/users/1', url_helpers.user_path(1, foo: nil)
- assert_equal '/a/users/1', url_helpers.user_path(1, foo: 'a')
+ assert_equal "/users/1", url_helpers.user_path(1)
+ assert_equal "/users/1", url_helpers.user_path(1, foo: nil)
+ assert_equal "/a/users/1", url_helpers.user_path(1, foo: "a")
+ end
+
+ test "implicit path components consistently return the same result" do
+ draw do
+ resources :users, to: SimpleApp.new("foo#index")
+ end
+ assert_equal "/users/1.json", url_helpers.user_path(1, :json)
+ assert_equal "/users/1.json", url_helpers.user_path(1, format: :json)
+ assert_equal "/users/1.json", url_helpers.user_path(1, :json)
end
private
diff --git a/actionpack/test/dispatch/routing_assertions_test.rb b/actionpack/test/dispatch/routing_assertions_test.rb
index 56ea644f22..009b6d9bc3 100644
--- a/actionpack/test/dispatch/routing_assertions_test.rb
+++ b/actionpack/test/dispatch/routing_assertions_test.rb
@@ -1,130 +1,208 @@
-require 'abstract_unit'
-require 'controller/fake_controllers'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "rails/engine"
+require "controller/fake_controllers"
class SecureArticlesController < ArticlesController; end
class BlockArticlesController < ArticlesController; end
class QueryArticlesController < ArticlesController; end
-class RoutingAssertionsTest < ActionController::TestCase
+class SecureBooksController < BooksController; end
+class BlockBooksController < BooksController; end
+class QueryBooksController < BooksController; end
+class RoutingAssertionsTest < ActionController::TestCase
def setup
+ engine = Class.new(Rails::Engine) do
+ def self.name
+ "blog_engine"
+ end
+ end
+ engine.routes.draw do
+ resources :books
+
+ scope "secure", constraints: { protocol: "https://" } do
+ resources :books, controller: "secure_books"
+ end
+
+ scope "block", constraints: lambda { |r| r.ssl? } do
+ resources :books, controller: "block_books"
+ end
+
+ scope "query", constraints: lambda { |r| r.params[:use_query] == "true" } do
+ resources :books, controller: "query_books"
+ end
+ end
+
@routes = ActionDispatch::Routing::RouteSet.new
@routes.draw do
resources :articles
- scope 'secure', :constraints => { :protocol => 'https://' } do
- resources :articles, :controller => 'secure_articles'
+ scope "secure", constraints: { protocol: "https://" } do
+ resources :articles, controller: "secure_articles"
end
- scope 'block', :constraints => lambda { |r| r.ssl? } do
- resources :articles, :controller => 'block_articles'
+ scope "block", constraints: lambda { |r| r.ssl? } do
+ resources :articles, controller: "block_articles"
end
- scope 'query', :constraints => lambda { |r| r.params[:use_query] == 'true' } do
- resources :articles, :controller => 'query_articles'
+ scope "query", constraints: lambda { |r| r.params[:use_query] == "true" } do
+ resources :articles, controller: "query_articles"
end
+
+ mount engine => "/shelf"
+
+ get "/shelf/foo", controller: "query_articles", action: "index"
end
end
def test_assert_generates
- assert_generates('/articles', { :controller => 'articles', :action => 'index' })
- assert_generates('/articles/1', { :controller => 'articles', :action => 'show', :id => '1' })
+ assert_generates("/articles", controller: "articles", action: "index")
+ assert_generates("/articles/1", controller: "articles", action: "show", id: "1")
end
def test_assert_generates_with_defaults
- assert_generates('/articles/1/edit', { :controller => 'articles', :action => 'edit' }, { :id => '1' })
+ assert_generates("/articles/1/edit", { controller: "articles", action: "edit" }, { id: "1" })
end
def test_assert_generates_with_extras
- assert_generates('/articles', { :controller => 'articles', :action => 'index', :page => '1' }, {}, { :page => '1' })
+ assert_generates("/articles", { controller: "articles", action: "index", page: "1" }, {}, { page: "1" })
end
def test_assert_recognizes
- assert_recognizes({ :controller => 'articles', :action => 'index' }, '/articles')
- assert_recognizes({ :controller => 'articles', :action => 'show', :id => '1' }, '/articles/1')
+ assert_recognizes({ controller: "articles", action: "index" }, "/articles")
+ assert_recognizes({ controller: "articles", action: "show", id: "1" }, "/articles/1")
end
def test_assert_recognizes_with_extras
- assert_recognizes({ :controller => 'articles', :action => 'index', :page => '1' }, '/articles', { :page => '1' })
+ assert_recognizes({ controller: "articles", action: "index", page: "1" }, "/articles", page: "1")
end
def test_assert_recognizes_with_method
- assert_recognizes({ :controller => 'articles', :action => 'create' }, { :path => '/articles', :method => :post })
- assert_recognizes({ :controller => 'articles', :action => 'update', :id => '1' }, { :path => '/articles/1', :method => :put })
+ assert_recognizes({ controller: "articles", action: "create" }, { path: "/articles", method: :post })
+ assert_recognizes({ controller: "articles", action: "update", id: "1" }, { path: "/articles/1", method: :put })
end
def test_assert_recognizes_with_hash_constraint
assert_raise(Assertion) do
- assert_recognizes({ :controller => 'secure_articles', :action => 'index' }, 'http://test.host/secure/articles')
+ assert_recognizes({ controller: "secure_articles", action: "index" }, "http://test.host/secure/articles")
end
- assert_recognizes({ :controller => 'secure_articles', :action => 'index', :protocol => 'https://' }, 'https://test.host/secure/articles')
+ assert_recognizes({ controller: "secure_articles", action: "index", protocol: "https://" }, "https://test.host/secure/articles")
end
def test_assert_recognizes_with_block_constraint
assert_raise(Assertion) do
- assert_recognizes({ :controller => 'block_articles', :action => 'index' }, 'http://test.host/block/articles')
+ assert_recognizes({ controller: "block_articles", action: "index" }, "http://test.host/block/articles")
end
- assert_recognizes({ :controller => 'block_articles', :action => 'index' }, 'https://test.host/block/articles')
+ assert_recognizes({ controller: "block_articles", action: "index" }, "https://test.host/block/articles")
end
def test_assert_recognizes_with_query_constraint
assert_raise(Assertion) do
- assert_recognizes({ :controller => 'query_articles', :action => 'index', :use_query => 'false' }, '/query/articles', { :use_query => 'false' })
+ assert_recognizes({ controller: "query_articles", action: "index", use_query: "false" }, "/query/articles", use_query: "false")
end
- assert_recognizes({ :controller => 'query_articles', :action => 'index', :use_query => 'true' }, '/query/articles', { :use_query => 'true' })
+ assert_recognizes({ controller: "query_articles", action: "index", use_query: "true" }, "/query/articles", use_query: "true")
end
def test_assert_recognizes_raises_message
err = assert_raise(Assertion) do
- assert_recognizes({ :controller => 'secure_articles', :action => 'index' }, 'http://test.host/secure/articles', {}, "This is a really bad msg")
+ assert_recognizes({ controller: "secure_articles", action: "index" }, "http://test.host/secure/articles", {}, "This is a really bad msg")
end
assert_match err.message, "This is a really bad msg"
end
+ def test_assert_recognizes_with_engine
+ assert_recognizes({ controller: "books", action: "index" }, "/shelf/books")
+ assert_recognizes({ controller: "books", action: "show", id: "1" }, "/shelf/books/1")
+ end
+
+ def test_assert_recognizes_with_engine_and_extras
+ assert_recognizes({ controller: "books", action: "index", page: "1" }, "/shelf/books", page: "1")
+ end
+
+ def test_assert_recognizes_with_engine_and_method
+ assert_recognizes({ controller: "books", action: "create" }, { path: "/shelf/books", method: :post })
+ assert_recognizes({ controller: "books", action: "update", id: "1" }, { path: "/shelf/books/1", method: :put })
+ end
+
+ def test_assert_recognizes_with_engine_and_hash_constraint
+ assert_raise(Assertion) do
+ assert_recognizes({ controller: "secure_books", action: "index" }, "http://test.host/shelf/secure/books")
+ end
+ assert_recognizes({ controller: "secure_books", action: "index", protocol: "https://" }, "https://test.host/shelf/secure/books")
+ end
+
+ def test_assert_recognizes_with_engine_and_block_constraint
+ assert_raise(Assertion) do
+ assert_recognizes({ controller: "block_books", action: "index" }, "http://test.host/shelf/block/books")
+ end
+ assert_recognizes({ controller: "block_books", action: "index" }, "https://test.host/shelf/block/books")
+ end
+
+ def test_assert_recognizes_with_engine_and_query_constraint
+ assert_raise(Assertion) do
+ assert_recognizes({ controller: "query_books", action: "index", use_query: "false" }, "/shelf/query/books", use_query: "false")
+ end
+ assert_recognizes({ controller: "query_books", action: "index", use_query: "true" }, "/shelf/query/books", use_query: "true")
+ end
+
+ def test_assert_recognizes_raises_message_with_engine
+ err = assert_raise(Assertion) do
+ assert_recognizes({ controller: "secure_books", action: "index" }, "http://test.host/shelf/secure/books", {}, "This is a really bad msg")
+ end
+
+ assert_match err.message, "This is a really bad msg"
+ end
+
+ def test_assert_recognizes_continue_to_recoginize_after_it_tried_engines
+ assert_recognizes({ controller: "query_articles", action: "index" }, "/shelf/foo")
+ end
+
def test_assert_routing
- assert_routing('/articles', :controller => 'articles', :action => 'index')
+ assert_routing("/articles", controller: "articles", action: "index")
end
def test_assert_routing_raises_message
err = assert_raise(Assertion) do
- assert_routing('/thisIsNotARoute', { :controller => 'articles', :action => 'edit', :id => '1' }, { :id => '1' }, {}, "This is a really bad msg")
+ assert_routing("/thisIsNotARoute", { controller: "articles", action: "edit", id: "1" }, { id: "1" }, {}, "This is a really bad msg")
end
assert_match err.message, "This is a really bad msg"
end
def test_assert_routing_with_defaults
- assert_routing('/articles/1/edit', { :controller => 'articles', :action => 'edit', :id => '1' }, { :id => '1' })
+ assert_routing("/articles/1/edit", { controller: "articles", action: "edit", id: "1" }, { id: "1" })
end
def test_assert_routing_with_extras
- assert_routing('/articles', { :controller => 'articles', :action => 'index', :page => '1' }, { }, { :page => '1' })
+ assert_routing("/articles", { controller: "articles", action: "index", page: "1" }, {}, { page: "1" })
end
def test_assert_routing_with_hash_constraint
assert_raise(Assertion) do
- assert_routing('http://test.host/secure/articles', { :controller => 'secure_articles', :action => 'index' })
+ assert_routing("http://test.host/secure/articles", controller: "secure_articles", action: "index")
end
- assert_routing('https://test.host/secure/articles', { :controller => 'secure_articles', :action => 'index', :protocol => 'https://' })
+ assert_routing("https://test.host/secure/articles", controller: "secure_articles", action: "index", protocol: "https://")
end
def test_assert_routing_with_block_constraint
assert_raise(Assertion) do
- assert_routing('http://test.host/block/articles', { :controller => 'block_articles', :action => 'index' })
+ assert_routing("http://test.host/block/articles", controller: "block_articles", action: "index")
end
- assert_routing('https://test.host/block/articles', { :controller => 'block_articles', :action => 'index' })
+ assert_routing("https://test.host/block/articles", controller: "block_articles", action: "index")
end
def test_with_routing
with_routing do |routes|
routes.draw do
- resources :articles, :path => 'artikel'
+ resources :articles, path: "artikel"
end
- assert_routing('/artikel', :controller => 'articles', :action => 'index')
+ assert_routing("/artikel", controller: "articles", action: "index")
assert_raise(Assertion) do
- assert_routing('/articles', { :controller => 'articles', :action => 'index' })
+ assert_routing("/articles", controller: "articles", action: "index")
end
end
end
diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb
index 26b8636c2f..affc2d8497 100644
--- a/actionpack/test/dispatch/routing_test.rb
+++ b/actionpack/test/dispatch/routing_test.rb
@@ -1,11 +1,13 @@
-# encoding: UTF-8
-require 'erb'
-require 'abstract_unit'
-require 'controller/fake_controllers'
+# frozen_string_literal: true
+
+require "erb"
+require "abstract_unit"
+require "controller/fake_controllers"
+require "active_support/messages/rotation_configuration"
class TestRoutingMapper < ActionDispatch::IntegrationTest
SprocketsApp = lambda { |env|
- [200, {"Content-Type" => "text/html"}, ["javascripts"]]
+ [200, { "Content-Type" => "text/html" }, ["javascripts"]]
}
class IpRestrictor
@@ -29,95 +31,112 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
def test_logout
draw do
controller :sessions do
- delete 'logout' => :destroy
+ delete "logout" => :destroy
end
end
- delete '/logout'
- assert_equal 'sessions#destroy', @response.body
+ delete "/logout"
+ assert_equal "sessions#destroy", @response.body
- assert_equal '/logout', logout_path
- assert_equal '/logout', url_for(:controller => 'sessions', :action => 'destroy', :only_path => true)
+ assert_equal "/logout", logout_path
+ assert_equal "/logout", url_for(controller: "sessions", action: "destroy", only_path: true)
end
def test_login
draw do
- default_url_options :host => "rubyonrails.org"
+ default_url_options host: "rubyonrails.org"
controller :sessions do
- get 'login' => :new
- post 'login' => :create
+ get "login" => :new
+ post "login" => :create
end
end
- get '/login'
- assert_equal 'sessions#new', @response.body
- assert_equal '/login', login_path
+ get "/login"
+ assert_equal "sessions#new", @response.body
+ assert_equal "/login", login_path
- post '/login'
- assert_equal 'sessions#create', @response.body
+ post "/login"
+ assert_equal "sessions#create", @response.body
- assert_equal '/login', url_for(:controller => 'sessions', :action => 'create', :only_path => true)
- assert_equal '/login', url_for(:controller => 'sessions', :action => 'new', :only_path => true)
+ assert_equal "/login", url_for(controller: "sessions", action: "create", only_path: true)
+ assert_equal "/login", url_for(controller: "sessions", action: "new", only_path: true)
- assert_equal 'http://rubyonrails.org/login', url_for(:controller => 'sessions', :action => 'create')
- assert_equal 'http://rubyonrails.org/login', login_url
+ assert_equal "http://rubyonrails.org/login", url_for(controller: "sessions", action: "create")
+ assert_equal "http://rubyonrails.org/login", login_url
end
def test_login_redirect
draw do
- get 'account/login', :to => redirect("/login")
+ get "account/login", to: redirect("/login")
end
- get '/account/login'
- verify_redirect 'http://www.example.com/login'
+ get "/account/login"
+ verify_redirect "http://www.example.com/login"
end
def test_logout_redirect_without_to
draw do
- get 'account/logout' => redirect("/logout"), :as => :logout_redirect
+ get "account/logout" => redirect("/logout"), :as => :logout_redirect
end
- assert_equal '/account/logout', logout_redirect_path
- get '/account/logout'
- verify_redirect 'http://www.example.com/logout'
+ assert_equal "/account/logout", logout_redirect_path
+ get "/account/logout"
+ verify_redirect "http://www.example.com/logout"
end
def test_namespace_redirect
draw do
namespace :private do
- root :to => redirect('/private/index')
- get "index", :to => 'private#index'
+ root to: redirect("/private/index")
+ get "index", to: "private#index"
end
end
- get '/private'
- verify_redirect 'http://www.example.com/private/index'
+ get "/private"
+ verify_redirect "http://www.example.com/private/index"
end
def test_redirect_with_failing_constraint
draw do
- get 'hi', to: redirect("/foo"), constraints: ::TestRoutingMapper::GrumpyRestrictor
+ get "hi", to: redirect("/foo"), constraints: ::TestRoutingMapper::GrumpyRestrictor
end
- get '/hi'
+ get "/hi"
assert_equal 404, status
end
def test_redirect_with_passing_constraint
draw do
- get 'hi', to: redirect("/foo"), constraints: ->(req) { true }
+ get "hi", to: redirect("/foo"), constraints: ->(req) { true }
end
- get '/hi'
+ get "/hi"
assert_equal 301, status
end
+ def test_accepts_a_constraint_object_responding_to_call
+ constraint = Class.new do
+ def call(*); true; end
+ def matches?(*); false; end
+ end
+
+ draw do
+ get "/", to: "home#show", constraints: constraint.new
+ end
+
+ assert_nothing_raised do
+ get "/"
+ end
+ end
+
def test_namespace_with_controller_segment
assert_raise(ArgumentError) do
draw do
namespace :admin do
- get '/:controller(/:action(/:id(.:format)))'
+ ActiveSupport::Deprecation.silence do
+ get "/:controller(/:action(/:id(.:format)))"
+ end
end
end
end
@@ -126,11 +145,13 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
def test_namespace_without_controller_segment
draw do
namespace :admin do
- get 'hello/:controllers/:action'
+ ActiveSupport::Deprecation.silence do
+ get "hello/:controllers/:action"
+ end
end
end
- get '/admin/hello/foo/new'
- assert_equal 'foo', @request.params["controllers"]
+ get "/admin/hello/foo/new"
+ assert_equal "foo", @request.params["controllers"]
end
def test_session_singleton_resource
@@ -141,39 +162,37 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
- get '/session'
- assert_equal 'sessions#create', @response.body
- assert_equal '/session', session_path
+ get "/session"
+ assert_equal "sessions#create", @response.body
+ assert_equal "/session", session_path
- post '/session'
- assert_equal 'sessions#create', @response.body
+ post "/session"
+ assert_equal "sessions#create", @response.body
- put '/session'
- assert_equal 'sessions#update', @response.body
+ put "/session"
+ assert_equal "sessions#update", @response.body
- delete '/session'
- assert_equal 'sessions#destroy', @response.body
+ delete "/session"
+ assert_equal "sessions#destroy", @response.body
- get '/session/new'
- assert_equal 'sessions#new', @response.body
- assert_equal '/session/new', new_session_path
+ get "/session/new"
+ assert_equal "sessions#new", @response.body
+ assert_equal "/session/new", new_session_path
- get '/session/edit'
- assert_equal 'sessions#edit', @response.body
- assert_equal '/session/edit', edit_session_path
+ get "/session/edit"
+ assert_equal "sessions#edit", @response.body
+ assert_equal "/session/edit", edit_session_path
- post '/session/reset'
- assert_equal 'sessions#reset', @response.body
- assert_equal '/session/reset', reset_session_path
+ post "/session/reset"
+ assert_equal "sessions#reset", @response.body
+ assert_equal "/session/reset", reset_session_path
end
def test_session_singleton_resource_for_api_app
- self.class.stub_controllers do |_|
- config = ActionDispatch::Routing::RouteSet::Config.new
- config.api_only = true
-
- routes = ActionDispatch::Routing::RouteSet.new(config)
+ config = ActionDispatch::Routing::RouteSet::Config.new
+ config.api_only = true
+ self.class.stub_controllers(config) do |routes|
routes.draw do
resource :session do
get :create
@@ -183,28 +202,28 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
@app = RoutedRackApp.new routes
end
- get '/session'
- assert_equal 'sessions#create', @response.body
- assert_equal '/session', session_path
+ get "/session"
+ assert_equal "sessions#create", @response.body
+ assert_equal "/session", session_path
- post '/session'
- assert_equal 'sessions#create', @response.body
+ post "/session"
+ assert_equal "sessions#create", @response.body
- put '/session'
- assert_equal 'sessions#update', @response.body
+ put "/session"
+ assert_equal "sessions#update", @response.body
- delete '/session'
- assert_equal 'sessions#destroy', @response.body
+ delete "/session"
+ assert_equal "sessions#destroy", @response.body
- post '/session/reset'
- assert_equal 'sessions#reset', @response.body
- assert_equal '/session/reset', reset_session_path
+ post "/session/reset"
+ assert_equal "sessions#reset", @response.body
+ assert_equal "/session/reset", reset_session_path
- get '/session/new'
- assert_equal 'Not Found', @response.body
+ get "/session/new"
+ assert_equal "Not Found", @response.body
- get '/session/edit'
- assert_equal 'Not Found', @response.body
+ get "/session/edit"
+ assert_equal "Not Found", @response.body
end
def test_session_info_nested_singleton_resource
@@ -214,9 +233,9 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
- get '/session/info'
- assert_equal 'infos#show', @response.body
- assert_equal '/session/info', session_info_path
+ get "/session/info"
+ assert_equal "infos#show", @response.body
+ assert_equal "/session/info", session_info_path
end
def test_member_on_resource
@@ -228,233 +247,243 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
- get '/session/crush'
- assert_equal 'sessions#crush', @response.body
- assert_equal '/session/crush', crush_session_path
+ get "/session/crush"
+ assert_equal "sessions#crush", @response.body
+ assert_equal "/session/crush", crush_session_path
end
def test_redirect_modulo
draw do
- get 'account/modulo/:name', :to => redirect("/%{name}s")
+ get "account/modulo/:name", to: redirect("/%{name}s")
end
- get '/account/modulo/name'
- verify_redirect 'http://www.example.com/names'
+ get "/account/modulo/name"
+ verify_redirect "http://www.example.com/names"
end
def test_redirect_proc
draw do
- get 'account/proc/:name', :to => redirect {|params, req| "/#{params[:name].pluralize}" }
+ get "account/proc/:name", to: redirect { |params, req| "/#{params[:name].pluralize}" }
end
- get '/account/proc/person'
- verify_redirect 'http://www.example.com/people'
+ get "/account/proc/person"
+ verify_redirect "http://www.example.com/people"
end
def test_redirect_proc_with_request
draw do
- get 'account/proc_req' => redirect {|params, req| "/#{req.method}" }
+ get "account/proc_req" => redirect { |params, req| "/#{req.method}" }
end
- get '/account/proc_req'
- verify_redirect 'http://www.example.com/GET'
+ get "/account/proc_req"
+ verify_redirect "http://www.example.com/GET"
end
def test_redirect_hash_with_subdomain
draw do
- get 'mobile', :to => redirect(:subdomain => 'mobile')
+ get "mobile", to: redirect(subdomain: "mobile")
end
- get '/mobile'
- verify_redirect 'http://mobile.example.com/mobile'
+ get "/mobile"
+ verify_redirect "http://mobile.example.com/mobile"
end
def test_redirect_hash_with_domain_and_path
draw do
- get 'documentation', :to => redirect(:domain => 'example-documentation.com', :path => '')
+ get "documentation", to: redirect(domain: "example-documentation.com", path: "")
end
- get '/documentation'
- verify_redirect 'http://www.example-documentation.com'
+ get "/documentation"
+ verify_redirect "http://www.example-documentation.com"
end
def test_redirect_hash_with_path
draw do
- get 'new_documentation', :to => redirect(:path => '/documentation/new')
+ get "new_documentation", to: redirect(path: "/documentation/new")
end
- get '/new_documentation'
- verify_redirect 'http://www.example.com/documentation/new'
+ get "/new_documentation"
+ verify_redirect "http://www.example.com/documentation/new"
end
def test_redirect_hash_with_host
draw do
- get 'super_new_documentation', :to => redirect(:host => 'super-docs.com')
+ get "super_new_documentation", to: redirect(host: "super-docs.com")
end
- get '/super_new_documentation?section=top'
- verify_redirect 'http://super-docs.com/super_new_documentation?section=top'
+ get "/super_new_documentation?section=top"
+ verify_redirect "http://super-docs.com/super_new_documentation?section=top"
end
def test_redirect_hash_path_substitution
draw do
- get 'stores/:name', :to => redirect(:subdomain => 'stores', :path => '/%{name}')
+ get "stores/:name", to: redirect(subdomain: "stores", path: "/%{name}")
end
- get '/stores/iernest'
- verify_redirect 'http://stores.example.com/iernest'
+ get "/stores/iernest"
+ verify_redirect "http://stores.example.com/iernest"
end
def test_redirect_hash_path_substitution_with_catch_all
draw do
- get 'stores/:name(*rest)', :to => redirect(:subdomain => 'stores', :path => '/%{name}%{rest}')
+ get "stores/:name(*rest)", to: redirect(subdomain: "stores", path: "/%{name}%{rest}")
end
- get '/stores/iernest/products'
- verify_redirect 'http://stores.example.com/iernest/products'
+ get "/stores/iernest/products"
+ verify_redirect "http://stores.example.com/iernest/products"
end
def test_redirect_class
draw do
- get 'youtube_favorites/:youtube_id/:name', :to => redirect(YoutubeFavoritesRedirector)
+ get "youtube_favorites/:youtube_id/:name", to: redirect(YoutubeFavoritesRedirector)
end
- get '/youtube_favorites/oHg5SJYRHA0/rick-rolld'
- verify_redirect 'http://www.youtube.com/watch?v=oHg5SJYRHA0'
+ get "/youtube_favorites/oHg5SJYRHA0/rick-rolld"
+ verify_redirect "http://www.youtube.com/watch?v=oHg5SJYRHA0"
end
def test_openid
draw do
- match 'openid/login', :via => [:get, :post], :to => "openid#login"
+ match "openid/login", via: [:get, :post], to: "openid#login"
end
- get '/openid/login'
- assert_equal 'openid#login', @response.body
+ get "/openid/login"
+ assert_equal "openid#login", @response.body
- post '/openid/login'
- assert_equal 'openid#login', @response.body
+ post "/openid/login"
+ assert_equal "openid#login", @response.body
end
def test_bookmarks
draw do
- scope "bookmark", :controller => "bookmarks", :as => :bookmark do
- get :new, :path => "build"
- post :create, :path => "create", :as => ""
+ scope "bookmark", controller: "bookmarks", as: :bookmark do
+ get :new, path: "build"
+ post :create, path: "create", as: ""
put :update
- get :remove, :action => :destroy, :as => :remove
+ get :remove, action: :destroy, as: :remove
end
end
- get '/bookmark/build'
- assert_equal 'bookmarks#new', @response.body
- assert_equal '/bookmark/build', bookmark_new_path
+ get "/bookmark/build"
+ assert_equal "bookmarks#new", @response.body
+ assert_equal "/bookmark/build", bookmark_new_path
- post '/bookmark/create'
- assert_equal 'bookmarks#create', @response.body
- assert_equal '/bookmark/create', bookmark_path
+ post "/bookmark/create"
+ assert_equal "bookmarks#create", @response.body
+ assert_equal "/bookmark/create", bookmark_path
- put '/bookmark/update'
- assert_equal 'bookmarks#update', @response.body
- assert_equal '/bookmark/update', bookmark_update_path
+ put "/bookmark/update"
+ assert_equal "bookmarks#update", @response.body
+ assert_equal "/bookmark/update", bookmark_update_path
- get '/bookmark/remove'
- assert_equal 'bookmarks#destroy', @response.body
- assert_equal '/bookmark/remove', bookmark_remove_path
+ get "/bookmark/remove"
+ assert_equal "bookmarks#destroy", @response.body
+ assert_equal "/bookmark/remove", bookmark_remove_path
end
def test_pagemarks
draw do
- scope "pagemark", :controller => "pagemarks", :as => :pagemark do
- get "new", :path => "build"
- post "create", :as => ""
+ scope "pagemark", controller: "pagemarks", as: :pagemark do
+ get "build", action: "new", as: "new"
+ post "create", as: ""
put "update"
- get "remove", :action => :destroy, :as => :remove
+ get "remove", action: :destroy, as: :remove
+ get "", action: :show, as: :show
end
end
- get '/pagemark/build'
- assert_equal 'pagemarks#new', @response.body
- assert_equal '/pagemark/build', pagemark_new_path
+ get "/pagemark/build"
+ assert_equal "pagemarks#new", @response.body
+ assert_equal "/pagemark/build", pagemark_new_path
+
+ post "/pagemark/create"
+ assert_equal "pagemarks#create", @response.body
+ assert_equal "/pagemark/create", pagemark_path
- post '/pagemark/create'
- assert_equal 'pagemarks#create', @response.body
- assert_equal '/pagemark/create', pagemark_path
+ put "/pagemark/update"
+ assert_equal "pagemarks#update", @response.body
+ assert_equal "/pagemark/update", pagemark_update_path
- put '/pagemark/update'
- assert_equal 'pagemarks#update', @response.body
- assert_equal '/pagemark/update', pagemark_update_path
+ get "/pagemark/remove"
+ assert_equal "pagemarks#destroy", @response.body
+ assert_equal "/pagemark/remove", pagemark_remove_path
- get '/pagemark/remove'
- assert_equal 'pagemarks#destroy', @response.body
- assert_equal '/pagemark/remove', pagemark_remove_path
+ get "/pagemark"
+ assert_equal "pagemarks#show", @response.body
+ assert_equal "/pagemark", pagemark_show_path
end
def test_admin
draw do
- constraints(:ip => /192\.168\.1\.\d\d\d/) do
- get 'admin' => "queenbee#index"
+ constraints(ip: /192\.168\.1\.\d\d\d/) do
+ get "admin" => "queenbee#index"
end
constraints ::TestRoutingMapper::IpRestrictor do
- get 'admin/accounts' => "queenbee#accounts"
+ get "admin/accounts" => "queenbee#accounts"
end
- get 'admin/passwords' => "queenbee#passwords", :constraints => ::TestRoutingMapper::IpRestrictor
+ get "admin/passwords" => "queenbee#passwords", :constraints => ::TestRoutingMapper::IpRestrictor
end
- get '/admin', headers: { 'REMOTE_ADDR' => '192.168.1.100' }
- assert_equal 'queenbee#index', @response.body
+ get "/admin", headers: { "REMOTE_ADDR" => "192.168.1.100" }
+ assert_equal "queenbee#index", @response.body
- get '/admin', headers: { 'REMOTE_ADDR' => '10.0.0.100' }
- assert_equal 'pass', @response.headers['X-Cascade']
+ get "/admin", headers: { "REMOTE_ADDR" => "10.0.0.100" }
+ assert_equal "pass", @response.headers["X-Cascade"]
- get '/admin/accounts', headers: { 'REMOTE_ADDR' => '192.168.1.100' }
- assert_equal 'queenbee#accounts', @response.body
+ get "/admin/accounts", headers: { "REMOTE_ADDR" => "192.168.1.100" }
+ assert_equal "queenbee#accounts", @response.body
- get '/admin/accounts', headers: { 'REMOTE_ADDR' => '10.0.0.100' }
- assert_equal 'pass', @response.headers['X-Cascade']
+ get "/admin/accounts", headers: { "REMOTE_ADDR" => "10.0.0.100" }
+ assert_equal "pass", @response.headers["X-Cascade"]
- get '/admin/passwords', headers: { 'REMOTE_ADDR' => '192.168.1.100' }
- assert_equal 'queenbee#passwords', @response.body
+ get "/admin/passwords", headers: { "REMOTE_ADDR" => "192.168.1.100" }
+ assert_equal "queenbee#passwords", @response.body
- get '/admin/passwords', headers: { 'REMOTE_ADDR' => '10.0.0.100' }
- assert_equal 'pass', @response.headers['X-Cascade']
+ get "/admin/passwords", headers: { "REMOTE_ADDR" => "10.0.0.100" }
+ assert_equal "pass", @response.headers["X-Cascade"]
end
def test_global
draw do
controller(:global) do
- get 'global/hide_notice'
- get 'global/export', :action => :export, :as => :export_request
- get '/export/:id/:file', :action => :export, :as => :export_download, :constraints => { :file => /.*/ }
- get 'global/:action'
+ get "global/hide_notice"
+ get "global/export", action: :export, as: :export_request
+ get "/export/:id/:file", action: :export, as: :export_download, constraints: { file: /.*/ }
+
+ ActiveSupport::Deprecation.silence do
+ get "global/:action"
+ end
end
end
- get '/global/dashboard'
- assert_equal 'global#dashboard', @response.body
+ get "/global/dashboard"
+ assert_equal "global#dashboard", @response.body
- get '/global/export'
- assert_equal 'global#export', @response.body
+ get "/global/export"
+ assert_equal "global#export", @response.body
- get '/global/hide_notice'
- assert_equal 'global#hide_notice', @response.body
+ get "/global/hide_notice"
+ assert_equal "global#hide_notice", @response.body
- get '/export/123/foo.txt'
- assert_equal 'global#export', @response.body
+ get "/export/123/foo.txt"
+ assert_equal "global#export", @response.body
- assert_equal '/global/export', export_request_path
- assert_equal '/global/hide_notice', global_hide_notice_path
- assert_equal '/export/123/foo.txt', export_download_path(:id => 123, :file => 'foo.txt')
+ assert_equal "/global/export", export_request_path
+ assert_equal "/global/hide_notice", global_hide_notice_path
+ assert_equal "/export/123/foo.txt", export_download_path(id: 123, file: "foo.txt")
end
def test_local
draw do
- get "/local/:action", :controller => "local"
+ ActiveSupport::Deprecation.silence do
+ get "/local/:action", controller: "local"
+ end
end
- get '/local/dashboard'
- assert_equal 'local#dashboard', @response.body
+ get "/local/dashboard"
+ assert_equal "local#dashboard", @response.body
end
# tests the use of dup in url_for
@@ -464,7 +493,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
# without dup, additional (and possibly unwanted) values will be present in the options (eg. :host)
- original_options = {:controller => 'projects', :action => 'status'}
+ original_options = { controller: "projects", action: "status" }
options = original_options.dup
url_for options
@@ -478,23 +507,23 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
get "/projects/status(.:format)"
end
- controller = '/projects'
- options = {:controller => controller, :action => 'status', :only_path => true}
+ controller = "/projects"
+ options = { controller: controller, action: "status", only_path: true }
url = url_for(options)
- assert_equal '/projects/status', url
- assert_equal '/projects', controller
+ assert_equal "/projects/status", url
+ assert_equal "/projects", controller
end
# tests the arguments modification free version of define_hash_access
def test_named_route_with_no_side_effects
draw do
resources :customers do
- get "profile", :on => :member
+ get "profile", on: :member
end
end
- original_options = { :host => 'test.host' }
+ original_options = { host: "test.host" }
options = original_options.dup
profile_customer_url("customer_model", options)
@@ -508,219 +537,218 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
get "/projects/status(.:format)"
end
- assert_equal '/projects/status', url_for(:controller => 'projects', :action => 'status', :only_path => true)
- assert_equal '/projects/status.json', url_for(:controller => 'projects', :action => 'status', :format => 'json', :only_path => true)
+ assert_equal "/projects/status", url_for(controller: "projects", action: "status", only_path: true)
+ assert_equal "/projects/status.json", url_for(controller: "projects", action: "status", format: "json", only_path: true)
end
def test_projects
draw do
- resources :projects, :controller => :project
+ resources :projects, controller: :project
end
- get '/projects'
- assert_equal 'project#index', @response.body
- assert_equal '/projects', projects_path
+ get "/projects"
+ assert_equal "project#index", @response.body
+ assert_equal "/projects", projects_path
- post '/projects'
- assert_equal 'project#create', @response.body
+ post "/projects"
+ assert_equal "project#create", @response.body
- get '/projects.xml'
- assert_equal 'project#index', @response.body
- assert_equal '/projects.xml', projects_path(:format => 'xml')
+ get "/projects.xml"
+ assert_equal "project#index", @response.body
+ assert_equal "/projects.xml", projects_path(format: "xml")
- get '/projects/new'
- assert_equal 'project#new', @response.body
- assert_equal '/projects/new', new_project_path
+ get "/projects/new"
+ assert_equal "project#new", @response.body
+ assert_equal "/projects/new", new_project_path
- get '/projects/new.xml'
- assert_equal 'project#new', @response.body
- assert_equal '/projects/new.xml', new_project_path(:format => 'xml')
+ get "/projects/new.xml"
+ assert_equal "project#new", @response.body
+ assert_equal "/projects/new.xml", new_project_path(format: "xml")
- get '/projects/1'
- assert_equal 'project#show', @response.body
- assert_equal '/projects/1', project_path(:id => '1')
+ get "/projects/1"
+ assert_equal "project#show", @response.body
+ assert_equal "/projects/1", project_path(id: "1")
- get '/projects/1.xml'
- assert_equal 'project#show', @response.body
- assert_equal '/projects/1.xml', project_path(:id => '1', :format => 'xml')
+ get "/projects/1.xml"
+ assert_equal "project#show", @response.body
+ assert_equal "/projects/1.xml", project_path(id: "1", format: "xml")
- get '/projects/1/edit'
- assert_equal 'project#edit', @response.body
- assert_equal '/projects/1/edit', edit_project_path(:id => '1')
+ get "/projects/1/edit"
+ assert_equal "project#edit", @response.body
+ assert_equal "/projects/1/edit", edit_project_path(id: "1")
end
def test_projects_for_api_app
- self.class.stub_controllers do |_|
- config = ActionDispatch::Routing::RouteSet::Config.new
- config.api_only = true
+ config = ActionDispatch::Routing::RouteSet::Config.new
+ config.api_only = true
- routes = ActionDispatch::Routing::RouteSet.new(config)
+ self.class.stub_controllers(config) do |routes|
routes.draw do
resources :projects, controller: :project
end
@app = RoutedRackApp.new routes
end
- get '/projects'
- assert_equal 'project#index', @response.body
- assert_equal '/projects', projects_path
+ get "/projects"
+ assert_equal "project#index", @response.body
+ assert_equal "/projects", projects_path
- post '/projects'
- assert_equal 'project#create', @response.body
+ post "/projects"
+ assert_equal "project#create", @response.body
- get '/projects.xml'
- assert_equal 'project#index', @response.body
- assert_equal '/projects.xml', projects_path(format: 'xml')
+ get "/projects.xml"
+ assert_equal "project#index", @response.body
+ assert_equal "/projects.xml", projects_path(format: "xml")
- get '/projects/1'
- assert_equal 'project#show', @response.body
- assert_equal '/projects/1', project_path(id: '1')
+ get "/projects/1"
+ assert_equal "project#show", @response.body
+ assert_equal "/projects/1", project_path(id: "1")
- get '/projects/1.xml'
- assert_equal 'project#show', @response.body
- assert_equal '/projects/1.xml', project_path(id: '1', format: 'xml')
+ get "/projects/1.xml"
+ assert_equal "project#show", @response.body
+ assert_equal "/projects/1.xml", project_path(id: "1", format: "xml")
- get '/projects/1/edit'
- assert_equal 'Not Found', @response.body
+ get "/projects/1/edit"
+ assert_equal "Not Found", @response.body
end
def test_projects_with_post_action_and_new_path_on_collection
draw do
- resources :projects, :controller => :project do
- post 'new', :action => 'new', :on => :collection, :as => :new
+ resources :projects, controller: :project do
+ post "new", action: "new", on: :collection, as: :new
end
end
- post '/projects/new'
+ post "/projects/new"
assert_equal "project#new", @response.body
assert_equal "/projects/new", new_projects_path
end
def test_projects_involvements
draw do
- resources :projects, :controller => :project do
+ resources :projects, controller: :project do
resources :involvements, :attachments
end
end
- get '/projects/1/involvements'
- assert_equal 'involvements#index', @response.body
- assert_equal '/projects/1/involvements', project_involvements_path(:project_id => '1')
+ get "/projects/1/involvements"
+ assert_equal "involvements#index", @response.body
+ assert_equal "/projects/1/involvements", project_involvements_path(project_id: "1")
- get '/projects/1/involvements/new'
- assert_equal 'involvements#new', @response.body
- assert_equal '/projects/1/involvements/new', new_project_involvement_path(:project_id => '1')
+ get "/projects/1/involvements/new"
+ assert_equal "involvements#new", @response.body
+ assert_equal "/projects/1/involvements/new", new_project_involvement_path(project_id: "1")
- get '/projects/1/involvements/1'
- assert_equal 'involvements#show', @response.body
- assert_equal '/projects/1/involvements/1', project_involvement_path(:project_id => '1', :id => '1')
+ get "/projects/1/involvements/1"
+ assert_equal "involvements#show", @response.body
+ assert_equal "/projects/1/involvements/1", project_involvement_path(project_id: "1", id: "1")
- put '/projects/1/involvements/1'
- assert_equal 'involvements#update', @response.body
+ put "/projects/1/involvements/1"
+ assert_equal "involvements#update", @response.body
- delete '/projects/1/involvements/1'
- assert_equal 'involvements#destroy', @response.body
+ delete "/projects/1/involvements/1"
+ assert_equal "involvements#destroy", @response.body
- get '/projects/1/involvements/1/edit'
- assert_equal 'involvements#edit', @response.body
- assert_equal '/projects/1/involvements/1/edit', edit_project_involvement_path(:project_id => '1', :id => '1')
+ get "/projects/1/involvements/1/edit"
+ assert_equal "involvements#edit", @response.body
+ assert_equal "/projects/1/involvements/1/edit", edit_project_involvement_path(project_id: "1", id: "1")
end
def test_projects_attachments
draw do
- resources :projects, :controller => :project do
+ resources :projects, controller: :project do
resources :involvements, :attachments
end
end
- get '/projects/1/attachments'
- assert_equal 'attachments#index', @response.body
- assert_equal '/projects/1/attachments', project_attachments_path(:project_id => '1')
+ get "/projects/1/attachments"
+ assert_equal "attachments#index", @response.body
+ assert_equal "/projects/1/attachments", project_attachments_path(project_id: "1")
end
def test_projects_participants
draw do
- resources :projects, :controller => :project do
+ resources :projects, controller: :project do
resources :participants do
- put :update_all, :on => :collection
+ put :update_all, on: :collection
end
end
end
- get '/projects/1/participants'
- assert_equal 'participants#index', @response.body
- assert_equal '/projects/1/participants', project_participants_path(:project_id => '1')
+ get "/projects/1/participants"
+ assert_equal "participants#index", @response.body
+ assert_equal "/projects/1/participants", project_participants_path(project_id: "1")
- put '/projects/1/participants/update_all'
- assert_equal 'participants#update_all', @response.body
- assert_equal '/projects/1/participants/update_all', update_all_project_participants_path(:project_id => '1')
+ put "/projects/1/participants/update_all"
+ assert_equal "participants#update_all", @response.body
+ assert_equal "/projects/1/participants/update_all", update_all_project_participants_path(project_id: "1")
end
def test_projects_companies
draw do
- resources :projects, :controller => :project do
+ resources :projects, controller: :project do
resources :companies do
resources :people
- resource :avatar, :controller => :avatar
+ resource :avatar, controller: :avatar
end
end
end
- get '/projects/1/companies'
- assert_equal 'companies#index', @response.body
- assert_equal '/projects/1/companies', project_companies_path(:project_id => '1')
+ get "/projects/1/companies"
+ assert_equal "companies#index", @response.body
+ assert_equal "/projects/1/companies", project_companies_path(project_id: "1")
- get '/projects/1/companies/1/people'
- assert_equal 'people#index', @response.body
- assert_equal '/projects/1/companies/1/people', project_company_people_path(:project_id => '1', :company_id => '1')
+ get "/projects/1/companies/1/people"
+ assert_equal "people#index", @response.body
+ assert_equal "/projects/1/companies/1/people", project_company_people_path(project_id: "1", company_id: "1")
- get '/projects/1/companies/1/avatar'
- assert_equal 'avatar#show', @response.body
- assert_equal '/projects/1/companies/1/avatar', project_company_avatar_path(:project_id => '1', :company_id => '1')
+ get "/projects/1/companies/1/avatar"
+ assert_equal "avatar#show", @response.body
+ assert_equal "/projects/1/companies/1/avatar", project_company_avatar_path(project_id: "1", company_id: "1")
end
def test_project_manager
draw do
resources :projects do
- resource :manager, :as => :super_manager do
+ resource :manager, as: :super_manager do
post :fire
end
end
end
- get '/projects/1/manager'
- assert_equal 'managers#show', @response.body
- assert_equal '/projects/1/manager', project_super_manager_path(:project_id => '1')
+ get "/projects/1/manager"
+ assert_equal "managers#show", @response.body
+ assert_equal "/projects/1/manager", project_super_manager_path(project_id: "1")
- get '/projects/1/manager/new'
- assert_equal 'managers#new', @response.body
- assert_equal '/projects/1/manager/new', new_project_super_manager_path(:project_id => '1')
+ get "/projects/1/manager/new"
+ assert_equal "managers#new", @response.body
+ assert_equal "/projects/1/manager/new", new_project_super_manager_path(project_id: "1")
- post '/projects/1/manager/fire'
- assert_equal 'managers#fire', @response.body
- assert_equal '/projects/1/manager/fire', fire_project_super_manager_path(:project_id => '1')
+ post "/projects/1/manager/fire"
+ assert_equal "managers#fire", @response.body
+ assert_equal "/projects/1/manager/fire", fire_project_super_manager_path(project_id: "1")
end
def test_project_images
draw do
resources :projects do
- resources :images, :as => :funny_images do
- post :revise, :on => :member
+ resources :images, as: :funny_images do
+ post :revise, on: :member
end
end
end
- get '/projects/1/images'
- assert_equal 'images#index', @response.body
- assert_equal '/projects/1/images', project_funny_images_path(:project_id => '1')
+ get "/projects/1/images"
+ assert_equal "images#index", @response.body
+ assert_equal "/projects/1/images", project_funny_images_path(project_id: "1")
- get '/projects/1/images/new'
- assert_equal 'images#new', @response.body
- assert_equal '/projects/1/images/new', new_project_funny_image_path(:project_id => '1')
+ get "/projects/1/images/new"
+ assert_equal "images#new", @response.body
+ assert_equal "/projects/1/images/new", new_project_funny_image_path(project_id: "1")
- post '/projects/1/images/1/revise'
- assert_equal 'images#revise', @response.body
- assert_equal '/projects/1/images/1/revise', revise_project_funny_image_path(:project_id => '1', :id => '1')
+ post "/projects/1/images/1/revise"
+ assert_equal "images#revise", @response.body
+ assert_equal "/projects/1/images/1/revise", revise_project_funny_image_path(project_id: "1", id: "1")
end
def test_projects_people
@@ -741,181 +769,181 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
- get '/projects/1/people'
- assert_equal 'people#index', @response.body
- assert_equal '/projects/1/people', project_people_path(:project_id => '1')
+ get "/projects/1/people"
+ assert_equal "people#index", @response.body
+ assert_equal "/projects/1/people", project_people_path(project_id: "1")
- get '/projects/1/people/1'
- assert_equal 'people#show', @response.body
- assert_equal '/projects/1/people/1', project_person_path(:project_id => '1', :id => '1')
+ get "/projects/1/people/1"
+ assert_equal "people#show", @response.body
+ assert_equal "/projects/1/people/1", project_person_path(project_id: "1", id: "1")
- get '/projects/1/people/1/7a2dec8/avatar'
- assert_equal 'avatars#show', @response.body
- assert_equal '/projects/1/people/1/7a2dec8/avatar', project_person_avatar_path(:project_id => '1', :person_id => '1', :access_token => '7a2dec8')
+ get "/projects/1/people/1/7a2dec8/avatar"
+ assert_equal "avatars#show", @response.body
+ assert_equal "/projects/1/people/1/7a2dec8/avatar", project_person_avatar_path(project_id: "1", person_id: "1", access_token: "7a2dec8")
- put '/projects/1/people/1/accessible_projects'
- assert_equal 'people#accessible_projects', @response.body
- assert_equal '/projects/1/people/1/accessible_projects', accessible_projects_project_person_path(:project_id => '1', :id => '1')
+ put "/projects/1/people/1/accessible_projects"
+ assert_equal "people#accessible_projects", @response.body
+ assert_equal "/projects/1/people/1/accessible_projects", accessible_projects_project_person_path(project_id: "1", id: "1")
- post '/projects/1/people/1/resend'
- assert_equal 'people#resend', @response.body
- assert_equal '/projects/1/people/1/resend', resend_project_person_path(:project_id => '1', :id => '1')
+ post "/projects/1/people/1/resend"
+ assert_equal "people#resend", @response.body
+ assert_equal "/projects/1/people/1/resend", resend_project_person_path(project_id: "1", id: "1")
- post '/projects/1/people/1/generate_new_password'
- assert_equal 'people#generate_new_password', @response.body
- assert_equal '/projects/1/people/1/generate_new_password', generate_new_password_project_person_path(:project_id => '1', :id => '1')
+ post "/projects/1/people/1/generate_new_password"
+ assert_equal "people#generate_new_password", @response.body
+ assert_equal "/projects/1/people/1/generate_new_password", generate_new_password_project_person_path(project_id: "1", id: "1")
end
def test_projects_with_resources_path_names
draw do
- resources_path_names :correlation_indexes => "info_about_correlation_indexes"
+ resources_path_names correlation_indexes: "info_about_correlation_indexes"
resources :projects do
- get :correlation_indexes, :on => :collection
+ get :correlation_indexes, on: :collection
end
end
- get '/projects/info_about_correlation_indexes'
- assert_equal 'projects#correlation_indexes', @response.body
- assert_equal '/projects/info_about_correlation_indexes', correlation_indexes_projects_path
+ get "/projects/info_about_correlation_indexes"
+ assert_equal "projects#correlation_indexes", @response.body
+ assert_equal "/projects/info_about_correlation_indexes", correlation_indexes_projects_path
end
def test_projects_posts
draw do
resources :projects do
resources :posts do
- get :archive, :toggle_view, :on => :collection
- post :preview, :on => :member
+ get :archive, :toggle_view, on: :collection
+ post :preview, on: :member
resource :subscription
resources :comments do
- post :preview, :on => :collection
+ post :preview, on: :collection
end
end
end
end
- get '/projects/1/posts'
- assert_equal 'posts#index', @response.body
- assert_equal '/projects/1/posts', project_posts_path(:project_id => '1')
+ get "/projects/1/posts"
+ assert_equal "posts#index", @response.body
+ assert_equal "/projects/1/posts", project_posts_path(project_id: "1")
- get '/projects/1/posts/archive'
- assert_equal 'posts#archive', @response.body
- assert_equal '/projects/1/posts/archive', archive_project_posts_path(:project_id => '1')
+ get "/projects/1/posts/archive"
+ assert_equal "posts#archive", @response.body
+ assert_equal "/projects/1/posts/archive", archive_project_posts_path(project_id: "1")
- get '/projects/1/posts/toggle_view'
- assert_equal 'posts#toggle_view', @response.body
- assert_equal '/projects/1/posts/toggle_view', toggle_view_project_posts_path(:project_id => '1')
+ get "/projects/1/posts/toggle_view"
+ assert_equal "posts#toggle_view", @response.body
+ assert_equal "/projects/1/posts/toggle_view", toggle_view_project_posts_path(project_id: "1")
- post '/projects/1/posts/1/preview'
- assert_equal 'posts#preview', @response.body
- assert_equal '/projects/1/posts/1/preview', preview_project_post_path(:project_id => '1', :id => '1')
+ post "/projects/1/posts/1/preview"
+ assert_equal "posts#preview", @response.body
+ assert_equal "/projects/1/posts/1/preview", preview_project_post_path(project_id: "1", id: "1")
- get '/projects/1/posts/1/subscription'
- assert_equal 'subscriptions#show', @response.body
- assert_equal '/projects/1/posts/1/subscription', project_post_subscription_path(:project_id => '1', :post_id => '1')
+ get "/projects/1/posts/1/subscription"
+ assert_equal "subscriptions#show", @response.body
+ assert_equal "/projects/1/posts/1/subscription", project_post_subscription_path(project_id: "1", post_id: "1")
- get '/projects/1/posts/1/comments'
- assert_equal 'comments#index', @response.body
- assert_equal '/projects/1/posts/1/comments', project_post_comments_path(:project_id => '1', :post_id => '1')
+ get "/projects/1/posts/1/comments"
+ assert_equal "comments#index", @response.body
+ assert_equal "/projects/1/posts/1/comments", project_post_comments_path(project_id: "1", post_id: "1")
- post '/projects/1/posts/1/comments/preview'
- assert_equal 'comments#preview', @response.body
- assert_equal '/projects/1/posts/1/comments/preview', preview_project_post_comments_path(:project_id => '1', :post_id => '1')
+ post "/projects/1/posts/1/comments/preview"
+ assert_equal "comments#preview", @response.body
+ assert_equal "/projects/1/posts/1/comments/preview", preview_project_post_comments_path(project_id: "1", post_id: "1")
end
def test_replies
draw do
resources :replies do
member do
- put :answer, :action => :mark_as_answer
- delete :answer, :action => :unmark_as_answer
+ put :answer, action: :mark_as_answer
+ delete :answer, action: :unmark_as_answer
end
end
end
- put '/replies/1/answer'
- assert_equal 'replies#mark_as_answer', @response.body
+ put "/replies/1/answer"
+ assert_equal "replies#mark_as_answer", @response.body
- delete '/replies/1/answer'
- assert_equal 'replies#unmark_as_answer', @response.body
+ delete "/replies/1/answer"
+ assert_equal "replies#unmark_as_answer", @response.body
end
def test_resource_routes_with_only_and_except
draw do
- resources :posts, :only => [:index, :show] do
- resources :comments, :except => :destroy
+ resources :posts, only: [:index, :show] do
+ resources :comments, except: :destroy
end
end
- get '/posts'
- assert_equal 'posts#index', @response.body
- assert_equal '/posts', posts_path
+ get "/posts"
+ assert_equal "posts#index", @response.body
+ assert_equal "/posts", posts_path
- get '/posts/1'
- assert_equal 'posts#show', @response.body
- assert_equal '/posts/1', post_path(:id => 1)
+ get "/posts/1"
+ assert_equal "posts#show", @response.body
+ assert_equal "/posts/1", post_path(id: 1)
- get '/posts/1/comments'
- assert_equal 'comments#index', @response.body
- assert_equal '/posts/1/comments', post_comments_path(:post_id => 1)
+ get "/posts/1/comments"
+ assert_equal "comments#index", @response.body
+ assert_equal "/posts/1/comments", post_comments_path(post_id: 1)
- post '/posts'
- assert_equal 'pass', @response.headers['X-Cascade']
- put '/posts/1'
- assert_equal 'pass', @response.headers['X-Cascade']
- delete '/posts/1'
- assert_equal 'pass', @response.headers['X-Cascade']
- delete '/posts/1/comments'
- assert_equal 'pass', @response.headers['X-Cascade']
+ post "/posts"
+ assert_equal "pass", @response.headers["X-Cascade"]
+ put "/posts/1"
+ assert_equal "pass", @response.headers["X-Cascade"]
+ delete "/posts/1"
+ assert_equal "pass", @response.headers["X-Cascade"]
+ delete "/posts/1/comments"
+ assert_equal "pass", @response.headers["X-Cascade"]
end
def test_resource_routes_only_create_update_destroy
draw do
- resource :past, :only => :destroy
- resource :present, :only => :update
- resource :future, :only => :create
+ resource :past, only: :destroy
+ resource :present, only: :update
+ resource :future, only: :create
end
- delete '/past'
- assert_equal 'pasts#destroy', @response.body
- assert_equal '/past', past_path
+ delete "/past"
+ assert_equal "pasts#destroy", @response.body
+ assert_equal "/past", past_path
- patch '/present'
- assert_equal 'presents#update', @response.body
- assert_equal '/present', present_path
+ patch "/present"
+ assert_equal "presents#update", @response.body
+ assert_equal "/present", present_path
- put '/present'
- assert_equal 'presents#update', @response.body
- assert_equal '/present', present_path
+ put "/present"
+ assert_equal "presents#update", @response.body
+ assert_equal "/present", present_path
- post '/future'
- assert_equal 'futures#create', @response.body
- assert_equal '/future', future_path
+ post "/future"
+ assert_equal "futures#create", @response.body
+ assert_equal "/future", future_path
end
def test_resources_routes_only_create_update_destroy
draw do
- resources :relationships, :only => [:create, :destroy]
- resources :friendships, :only => [:update]
+ resources :relationships, only: [:create, :destroy]
+ resources :friendships, only: [:update]
end
- post '/relationships'
- assert_equal 'relationships#create', @response.body
- assert_equal '/relationships', relationships_path
+ post "/relationships"
+ assert_equal "relationships#create", @response.body
+ assert_equal "/relationships", relationships_path
- delete '/relationships/1'
- assert_equal 'relationships#destroy', @response.body
- assert_equal '/relationships/1', relationship_path(1)
+ delete "/relationships/1"
+ assert_equal "relationships#destroy", @response.body
+ assert_equal "/relationships/1", relationship_path(1)
- patch '/friendships/1'
- assert_equal 'friendships#update', @response.body
- assert_equal '/friendships/1', friendship_path(1)
+ patch "/friendships/1"
+ assert_equal "friendships#update", @response.body
+ assert_equal "/friendships/1", friendship_path(1)
- put '/friendships/1'
- assert_equal 'friendships#update', @response.body
- assert_equal '/friendships/1', friendship_path(1)
+ put "/friendships/1"
+ assert_equal "friendships#update", @response.body
+ assert_equal "/friendships/1", friendship_path(1)
end
def test_resource_with_slugs_in_ids
@@ -923,153 +951,153 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
resources :posts
end
- get '/posts/rails-rocks'
- assert_equal 'posts#show', @response.body
- assert_equal '/posts/rails-rocks', post_path(:id => 'rails-rocks')
+ get "/posts/rails-rocks"
+ assert_equal "posts#show", @response.body
+ assert_equal "/posts/rails-rocks", post_path(id: "rails-rocks")
end
def test_resources_for_uncountable_names
draw do
resources :sheep do
- get "_it", :on => :member
+ get "_it", on: :member
end
end
- assert_equal '/sheep', sheep_index_path
- assert_equal '/sheep/1', sheep_path(1)
- assert_equal '/sheep/new', new_sheep_path
- assert_equal '/sheep/1/edit', edit_sheep_path(1)
- assert_equal '/sheep/1/_it', _it_sheep_path(1)
+ assert_equal "/sheep", sheep_index_path
+ assert_equal "/sheep/1", sheep_path(1)
+ assert_equal "/sheep/new", new_sheep_path
+ assert_equal "/sheep/1/edit", edit_sheep_path(1)
+ assert_equal "/sheep/1/_it", _it_sheep_path(1)
end
def test_resource_does_not_modify_passed_options
- options = {:id => /.+?/, :format => /json|xml/}
+ options = { id: /.+?/, format: /json|xml/ }
draw { resource :user, options }
- assert_equal({:id => /.+?/, :format => /json|xml/}, options)
+ assert_equal({ id: /.+?/, format: /json|xml/ }, options)
end
def test_resources_does_not_modify_passed_options
- options = {:id => /.+?/, :format => /json|xml/}
+ options = { id: /.+?/, format: /json|xml/ }
draw { resources :users, options }
- assert_equal({:id => /.+?/, :format => /json|xml/}, options)
+ assert_equal({ id: /.+?/, format: /json|xml/ }, options)
end
def test_path_names
draw do
- scope 'pt', :as => 'pt' do
- resources :projects, :path_names => { :edit => 'editar', :new => 'novo' }, :path => 'projetos'
- resource :admin, :path_names => { :new => 'novo', :activate => 'ativar' }, :path => 'administrador' do
- put :activate, :on => :member
+ scope "pt", as: "pt" do
+ resources :projects, path_names: { edit: "editar", new: "novo" }, path: "projetos"
+ resource :admin, path_names: { new: "novo", activate: "ativar" }, path: "administrador" do
+ put :activate, on: :member
end
end
end
- get '/pt/projetos'
- assert_equal 'projects#index', @response.body
- assert_equal '/pt/projetos', pt_projects_path
+ get "/pt/projetos"
+ assert_equal "projects#index", @response.body
+ assert_equal "/pt/projetos", pt_projects_path
- get '/pt/projetos/1/editar'
- assert_equal 'projects#edit', @response.body
- assert_equal '/pt/projetos/1/editar', edit_pt_project_path(1)
+ get "/pt/projetos/1/editar"
+ assert_equal "projects#edit", @response.body
+ assert_equal "/pt/projetos/1/editar", edit_pt_project_path(1)
- get '/pt/administrador'
- assert_equal 'admins#show', @response.body
- assert_equal '/pt/administrador', pt_admin_path
+ get "/pt/administrador"
+ assert_equal "admins#show", @response.body
+ assert_equal "/pt/administrador", pt_admin_path
- get '/pt/administrador/novo'
- assert_equal 'admins#new', @response.body
- assert_equal '/pt/administrador/novo', new_pt_admin_path
+ get "/pt/administrador/novo"
+ assert_equal "admins#new", @response.body
+ assert_equal "/pt/administrador/novo", new_pt_admin_path
- put '/pt/administrador/ativar'
- assert_equal 'admins#activate', @response.body
- assert_equal '/pt/administrador/ativar', activate_pt_admin_path
+ put "/pt/administrador/ativar"
+ assert_equal "admins#activate", @response.body
+ assert_equal "/pt/administrador/ativar", activate_pt_admin_path
end
def test_path_option_override
draw do
- scope 'pt', :as => 'pt' do
- resources :projects, :path_names => { :new => 'novo' }, :path => 'projetos' do
- put :close, :on => :member, :path => 'fechar'
- get :open, :on => :new, :path => 'abrir'
+ scope "pt", as: "pt" do
+ resources :projects, path_names: { new: "novo" }, path: "projetos" do
+ put :close, on: :member, path: "fechar"
+ get :open, on: :new, path: "abrir"
end
end
end
- get '/pt/projetos/novo/abrir'
- assert_equal 'projects#open', @response.body
- assert_equal '/pt/projetos/novo/abrir', open_new_pt_project_path
+ get "/pt/projetos/novo/abrir"
+ assert_equal "projects#open", @response.body
+ assert_equal "/pt/projetos/novo/abrir", open_new_pt_project_path
- put '/pt/projetos/1/fechar'
- assert_equal 'projects#close', @response.body
- assert_equal '/pt/projetos/1/fechar', close_pt_project_path(1)
+ put "/pt/projetos/1/fechar"
+ assert_equal "projects#close", @response.body
+ assert_equal "/pt/projetos/1/fechar", close_pt_project_path(1)
end
def test_sprockets
draw do
- get 'sprockets.js' => ::TestRoutingMapper::SprocketsApp
+ get "sprockets.js" => ::TestRoutingMapper::SprocketsApp
end
- get '/sprockets.js'
- assert_equal 'javascripts', @response.body
+ get "/sprockets.js"
+ assert_equal "javascripts", @response.body
end
def test_update_person_route
draw do
- get 'people/:id/update', :to => 'people#update', :as => :update_person
+ get "people/:id/update", to: "people#update", as: :update_person
end
- get '/people/1/update'
- assert_equal 'people#update', @response.body
+ get "/people/1/update"
+ assert_equal "people#update", @response.body
- assert_equal '/people/1/update', update_person_path(:id => 1)
+ assert_equal "/people/1/update", update_person_path(id: 1)
end
def test_update_project_person
draw do
- get '/projects/:project_id/people/:id/update', :to => 'people#update', :as => :update_project_person
+ get "/projects/:project_id/people/:id/update", to: "people#update", as: :update_project_person
end
- get '/projects/1/people/2/update'
- assert_equal 'people#update', @response.body
+ get "/projects/1/people/2/update"
+ assert_equal "people#update", @response.body
- assert_equal '/projects/1/people/2/update', update_project_person_path(:project_id => 1, :id => 2)
+ assert_equal "/projects/1/people/2/update", update_project_person_path(project_id: 1, id: 2)
end
def test_forum_products
draw do
namespace :forum do
- resources :products, :path => '' do
+ resources :products, path: "" do
resources :questions
end
end
end
- get '/forum'
- assert_equal 'forum/products#index', @response.body
- assert_equal '/forum', forum_products_path
+ get "/forum"
+ assert_equal "forum/products#index", @response.body
+ assert_equal "/forum", forum_products_path
- get '/forum/basecamp'
- assert_equal 'forum/products#show', @response.body
- assert_equal '/forum/basecamp', forum_product_path(:id => 'basecamp')
+ get "/forum/basecamp"
+ assert_equal "forum/products#show", @response.body
+ assert_equal "/forum/basecamp", forum_product_path(id: "basecamp")
- get '/forum/basecamp/questions'
- assert_equal 'forum/questions#index', @response.body
- assert_equal '/forum/basecamp/questions', forum_product_questions_path(:product_id => 'basecamp')
+ get "/forum/basecamp/questions"
+ assert_equal "forum/questions#index", @response.body
+ assert_equal "/forum/basecamp/questions", forum_product_questions_path(product_id: "basecamp")
- get '/forum/basecamp/questions/1'
- assert_equal 'forum/questions#show', @response.body
- assert_equal '/forum/basecamp/questions/1', forum_product_question_path(:product_id => 'basecamp', :id => 1)
+ get "/forum/basecamp/questions/1"
+ assert_equal "forum/questions#show", @response.body
+ assert_equal "/forum/basecamp/questions/1", forum_product_question_path(product_id: "basecamp", id: 1)
end
def test_articles_perma
draw do
- get 'articles/:year/:month/:day/:title', :to => "articles#show", :as => :article
+ get "articles/:year/:month/:day/:title", to: "articles#show", as: :article
end
- get '/articles/2009/08/18/rails-3'
- assert_equal 'articles#show', @response.body
+ get "/articles/2009/08/18/rails-3"
+ assert_equal "articles#show", @response.body
- assert_equal '/articles/2009/8/18/rails-3', article_path(:year => 2009, :month => 8, :day => 18, :title => 'rails-3')
+ assert_equal "/articles/2009/8/18/rails-3", article_path(year: 2009, month: 8, day: 18, title: "rails-3")
end
def test_account_namespace
@@ -1079,17 +1107,17 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
- get '/account/subscription'
- assert_equal 'account/subscriptions#show', @response.body
- assert_equal '/account/subscription', account_subscription_path
+ get "/account/subscription"
+ assert_equal "account/subscriptions#show", @response.body
+ assert_equal "/account/subscription", account_subscription_path
- get '/account/credit'
- assert_equal 'account/credits#show', @response.body
- assert_equal '/account/credit', account_credit_path
+ get "/account/credit"
+ assert_equal "account/credits#show", @response.body
+ assert_equal "/account/credit", account_credit_path
- get '/account/credit_card'
- assert_equal 'account/credit_cards#show', @response.body
- assert_equal '/account/credit_card', account_credit_card_path
+ get "/account/credit_card"
+ assert_equal "account/credit_cards#show", @response.body
+ assert_equal "/account/credit_card", account_credit_card_path
end
def test_nested_namespace
@@ -1101,9 +1129,9 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
- get '/account/admin/subscription'
- assert_equal 'account/admin/subscriptions#show', @response.body
- assert_equal '/account/admin/subscription', account_admin_subscription_path
+ get "/account/admin/subscription"
+ assert_equal "account/admin/subscriptions#show", @response.body
+ assert_equal "/account/admin/subscription", account_admin_subscription_path
end
def test_namespace_nested_in_resources
@@ -1119,155 +1147,155 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
- get '/clients/1/google/account'
- assert_equal '/clients/1/google/account', client_google_account_path(1)
- assert_equal 'google/accounts#show', @response.body
+ get "/clients/1/google/account"
+ assert_equal "/clients/1/google/account", client_google_account_path(1)
+ assert_equal "google/accounts#show", @response.body
- get '/clients/1/google/account/secret/info'
- assert_equal '/clients/1/google/account/secret/info', client_google_account_secret_info_path(1)
- assert_equal 'google/secret/infos#show', @response.body
+ get "/clients/1/google/account/secret/info"
+ assert_equal "/clients/1/google/account/secret/info", client_google_account_secret_info_path(1)
+ assert_equal "google/secret/infos#show", @response.body
end
def test_namespace_with_options
draw do
- namespace :users, :path => 'usuarios' do
- root :to => 'home#index'
+ namespace :users, path: "usuarios" do
+ root to: "home#index"
end
end
- get '/usuarios'
- assert_equal '/usuarios', users_root_path
- assert_equal 'users/home#index', @response.body
+ get "/usuarios"
+ assert_equal "/usuarios", users_root_path
+ assert_equal "users/home#index", @response.body
end
def test_namespaced_shallow_routes_with_module_option
draw do
- namespace :foo, module: 'bar' do
+ namespace :foo, module: "bar" do
resources :posts, only: [:index, :show] do
resources :comments, only: [:index, :show], shallow: true
end
end
end
- get '/foo/posts'
- assert_equal '/foo/posts', foo_posts_path
- assert_equal 'bar/posts#index', @response.body
+ get "/foo/posts"
+ assert_equal "/foo/posts", foo_posts_path
+ assert_equal "bar/posts#index", @response.body
- get '/foo/posts/1'
- assert_equal '/foo/posts/1', foo_post_path('1')
- assert_equal 'bar/posts#show', @response.body
+ get "/foo/posts/1"
+ assert_equal "/foo/posts/1", foo_post_path("1")
+ assert_equal "bar/posts#show", @response.body
- get '/foo/posts/1/comments'
- assert_equal '/foo/posts/1/comments', foo_post_comments_path('1')
- assert_equal 'bar/comments#index', @response.body
+ get "/foo/posts/1/comments"
+ assert_equal "/foo/posts/1/comments", foo_post_comments_path("1")
+ assert_equal "bar/comments#index", @response.body
- get '/foo/comments/2'
- assert_equal '/foo/comments/2', foo_comment_path('2')
- assert_equal 'bar/comments#show', @response.body
+ get "/foo/comments/2"
+ assert_equal "/foo/comments/2", foo_comment_path("2")
+ assert_equal "bar/comments#show", @response.body
end
def test_namespaced_shallow_routes_with_path_option
draw do
- namespace :foo, path: 'bar' do
+ namespace :foo, path: "bar" do
resources :posts, only: [:index, :show] do
resources :comments, only: [:index, :show], shallow: true
end
end
end
- get '/bar/posts'
- assert_equal '/bar/posts', foo_posts_path
- assert_equal 'foo/posts#index', @response.body
+ get "/bar/posts"
+ assert_equal "/bar/posts", foo_posts_path
+ assert_equal "foo/posts#index", @response.body
- get '/bar/posts/1'
- assert_equal '/bar/posts/1', foo_post_path('1')
- assert_equal 'foo/posts#show', @response.body
+ get "/bar/posts/1"
+ assert_equal "/bar/posts/1", foo_post_path("1")
+ assert_equal "foo/posts#show", @response.body
- get '/bar/posts/1/comments'
- assert_equal '/bar/posts/1/comments', foo_post_comments_path('1')
- assert_equal 'foo/comments#index', @response.body
+ get "/bar/posts/1/comments"
+ assert_equal "/bar/posts/1/comments", foo_post_comments_path("1")
+ assert_equal "foo/comments#index", @response.body
- get '/bar/comments/2'
- assert_equal '/bar/comments/2', foo_comment_path('2')
- assert_equal 'foo/comments#show', @response.body
+ get "/bar/comments/2"
+ assert_equal "/bar/comments/2", foo_comment_path("2")
+ assert_equal "foo/comments#show", @response.body
end
def test_namespaced_shallow_routes_with_as_option
draw do
- namespace :foo, as: 'bar' do
+ namespace :foo, as: "bar" do
resources :posts, only: [:index, :show] do
resources :comments, only: [:index, :show], shallow: true
end
end
end
- get '/foo/posts'
- assert_equal '/foo/posts', bar_posts_path
- assert_equal 'foo/posts#index', @response.body
+ get "/foo/posts"
+ assert_equal "/foo/posts", bar_posts_path
+ assert_equal "foo/posts#index", @response.body
- get '/foo/posts/1'
- assert_equal '/foo/posts/1', bar_post_path('1')
- assert_equal 'foo/posts#show', @response.body
+ get "/foo/posts/1"
+ assert_equal "/foo/posts/1", bar_post_path("1")
+ assert_equal "foo/posts#show", @response.body
- get '/foo/posts/1/comments'
- assert_equal '/foo/posts/1/comments', bar_post_comments_path('1')
- assert_equal 'foo/comments#index', @response.body
+ get "/foo/posts/1/comments"
+ assert_equal "/foo/posts/1/comments", bar_post_comments_path("1")
+ assert_equal "foo/comments#index", @response.body
- get '/foo/comments/2'
- assert_equal '/foo/comments/2', bar_comment_path('2')
- assert_equal 'foo/comments#show', @response.body
+ get "/foo/comments/2"
+ assert_equal "/foo/comments/2", bar_comment_path("2")
+ assert_equal "foo/comments#show", @response.body
end
def test_namespaced_shallow_routes_with_shallow_path_option
draw do
- namespace :foo, shallow_path: 'bar' do
+ namespace :foo, shallow_path: "bar" do
resources :posts, only: [:index, :show] do
resources :comments, only: [:index, :show], shallow: true
end
end
end
- get '/foo/posts'
- assert_equal '/foo/posts', foo_posts_path
- assert_equal 'foo/posts#index', @response.body
+ get "/foo/posts"
+ assert_equal "/foo/posts", foo_posts_path
+ assert_equal "foo/posts#index", @response.body
- get '/foo/posts/1'
- assert_equal '/foo/posts/1', foo_post_path('1')
- assert_equal 'foo/posts#show', @response.body
+ get "/foo/posts/1"
+ assert_equal "/foo/posts/1", foo_post_path("1")
+ assert_equal "foo/posts#show", @response.body
- get '/foo/posts/1/comments'
- assert_equal '/foo/posts/1/comments', foo_post_comments_path('1')
- assert_equal 'foo/comments#index', @response.body
+ get "/foo/posts/1/comments"
+ assert_equal "/foo/posts/1/comments", foo_post_comments_path("1")
+ assert_equal "foo/comments#index", @response.body
- get '/bar/comments/2'
- assert_equal '/bar/comments/2', foo_comment_path('2')
- assert_equal 'foo/comments#show', @response.body
+ get "/bar/comments/2"
+ assert_equal "/bar/comments/2", foo_comment_path("2")
+ assert_equal "foo/comments#show", @response.body
end
def test_namespaced_shallow_routes_with_shallow_prefix_option
draw do
- namespace :foo, shallow_prefix: 'bar' do
+ namespace :foo, shallow_prefix: "bar" do
resources :posts, only: [:index, :show] do
resources :comments, only: [:index, :show], shallow: true
end
end
end
- get '/foo/posts'
- assert_equal '/foo/posts', foo_posts_path
- assert_equal 'foo/posts#index', @response.body
+ get "/foo/posts"
+ assert_equal "/foo/posts", foo_posts_path
+ assert_equal "foo/posts#index", @response.body
- get '/foo/posts/1'
- assert_equal '/foo/posts/1', foo_post_path('1')
- assert_equal 'foo/posts#show', @response.body
+ get "/foo/posts/1"
+ assert_equal "/foo/posts/1", foo_post_path("1")
+ assert_equal "foo/posts#show", @response.body
- get '/foo/posts/1/comments'
- assert_equal '/foo/posts/1/comments', foo_post_comments_path('1')
- assert_equal 'foo/comments#index', @response.body
+ get "/foo/posts/1/comments"
+ assert_equal "/foo/posts/1/comments", foo_post_comments_path("1")
+ assert_equal "foo/comments#index", @response.body
- get '/foo/comments/2'
- assert_equal '/foo/comments/2', bar_comment_path('2')
- assert_equal 'foo/comments#show', @response.body
+ get "/foo/comments/2"
+ assert_equal "/foo/comments/2", bar_comment_path("2")
+ assert_equal "foo/comments#show", @response.body
end
def test_namespace_containing_numbers
@@ -1277,81 +1305,97 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
- get '/v2/subscriptions'
- assert_equal 'v2/subscriptions#index', @response.body
- assert_equal '/v2/subscriptions', v2_subscriptions_path
+ get "/v2/subscriptions"
+ assert_equal "v2/subscriptions#index", @response.body
+ assert_equal "/v2/subscriptions", v2_subscriptions_path
end
def test_articles_with_id
draw do
controller :articles do
- scope '/articles', :as => 'article' do
- scope :path => '/:title', :title => /[a-z]+/, :as => :with_title do
- get '/:id', :action => :with_id, :as => ""
+ scope "/articles", as: "article" do
+ scope path: "/:title", title: /[a-z]+/, as: :with_title do
+ get "/:id", action: :with_id, as: ""
end
end
end
end
- get '/articles/rails/1'
- assert_equal 'articles#with_id', @response.body
+ get "/articles/rails/1"
+ assert_equal "articles#with_id", @response.body
- get '/articles/123/1'
- assert_equal 'pass', @response.headers['X-Cascade']
+ get "/articles/123/1"
+ assert_equal "pass", @response.headers["X-Cascade"]
- assert_equal '/articles/rails/1', article_with_title_path(:title => 'rails', :id => 1)
+ assert_equal "/articles/rails/1", article_with_title_path(title: "rails", id: 1)
end
def test_access_token_rooms
draw do
- scope ':access_token', :constraints => { :access_token => /\w{5,5}/ } do
+ scope ":access_token", constraints: { access_token: /\w{5,5}/ } do
resources :rooms
end
end
- get '/12345/rooms'
- assert_equal 'rooms#index', @response.body
+ get "/12345/rooms"
+ assert_equal "rooms#index", @response.body
- get '/12345/rooms/1'
- assert_equal 'rooms#show', @response.body
+ get "/12345/rooms/1"
+ assert_equal "rooms#show", @response.body
- get '/12345/rooms/1/edit'
- assert_equal 'rooms#edit', @response.body
+ get "/12345/rooms/1/edit"
+ assert_equal "rooms#edit", @response.body
end
def test_root
draw do
- root :to => 'projects#index'
+ root to: "projects#index"
end
- assert_equal '/', root_path
- get '/'
- assert_equal 'projects#index', @response.body
+ assert_equal "/", root_path
+ get "/"
+ assert_equal "projects#index", @response.body
end
def test_scoped_root
draw do
- scope '(:locale)', :locale => /en|pl/ do
- root :to => 'projects#index'
+ scope "(:locale)", locale: /en|pl/ do
+ root to: "projects#index"
end
end
- assert_equal '/en', root_path(:locale => 'en')
- get '/en'
- assert_equal 'projects#index', @response.body
+ assert_equal "/en", root_path(locale: "en")
+ get "/en"
+ assert_equal "projects#index", @response.body
end
def test_scoped_root_as_name
draw do
- scope '(:locale)', :locale => /en|pl/ do
- root :to => 'projects#index', :as => 'projects'
+ scope "(:locale)", locale: /en|pl/ do
+ root to: "projects#index", as: "projects"
+ end
+ end
+
+ assert_equal "/en", projects_path(locale: "en")
+ assert_equal "/", projects_path
+ get "/en"
+ assert_equal "projects#index", @response.body
+ end
+
+ def test_optionally_scoped_root_unscoped_access
+ draw do
+ scope "(:locale)" do
+ scope "(:platform)" do
+ scope "(:browser)" do
+ root to: "projects#index"
+ end
+ end
end
end
- assert_equal '/en', projects_path(:locale => 'en')
- assert_equal '/', projects_path
- get '/en'
- assert_equal 'projects#index', @response.body
+ assert_equal "/", root_path
+ get "/"
+ assert_equal "projects#index", @response.body
end
def test_scope_with_format_option
@@ -1369,10 +1413,10 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
assert_equal "/scoped/index", no_format_scoped_path
assert_equal "/scoped/index?format=html", no_format_scoped_path(format: "html")
- get '/scoped/index'
+ get "/scoped/index"
assert_equal "scoped#index", @response.body
- get '/scoped/index.html'
+ get "/scoped/index.html"
assert_equal "Not Found", @response.body
end
@@ -1407,63 +1451,63 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
def test_index
draw do
- get '/info' => 'projects#info', :as => 'info'
+ get "/info" => "projects#info", :as => "info"
end
- assert_equal '/info', info_path
- get '/info'
- assert_equal 'projects#info', @response.body
+ assert_equal "/info", info_path
+ get "/info"
+ assert_equal "projects#info", @response.body
end
def test_match_with_many_paths_containing_a_slash
draw do
- get 'get/first', 'get/second', 'get/third', :to => 'get#show'
+ get "get/first", "get/second", "get/third", to: "get#show"
end
- get '/get/first'
- assert_equal 'get#show', @response.body
+ get "/get/first"
+ assert_equal "get#show", @response.body
- get '/get/second'
- assert_equal 'get#show', @response.body
+ get "/get/second"
+ assert_equal "get#show", @response.body
- get '/get/third'
- assert_equal 'get#show', @response.body
+ get "/get/third"
+ assert_equal "get#show", @response.body
end
def test_match_shorthand_with_no_scope
draw do
- get 'account/overview'
+ get "account/overview"
end
- assert_equal '/account/overview', account_overview_path
- get '/account/overview'
- assert_equal 'account#overview', @response.body
+ assert_equal "/account/overview", account_overview_path
+ get "/account/overview"
+ assert_equal "account#overview", @response.body
end
def test_match_shorthand_inside_namespace
draw do
namespace :account do
- get 'shorthand'
+ get "shorthand"
end
end
- assert_equal '/account/shorthand', account_shorthand_path
- get '/account/shorthand'
- assert_equal 'account#shorthand', @response.body
+ assert_equal "/account/shorthand", account_shorthand_path
+ get "/account/shorthand"
+ assert_equal "account#shorthand", @response.body
end
def test_match_shorthand_with_multiple_paths_inside_namespace
draw do
namespace :proposals do
- put 'activate', 'inactivate'
+ put "activate", "inactivate"
end
end
- put '/proposals/activate'
- assert_equal 'proposals#activate', @response.body
+ put "/proposals/activate"
+ assert_equal "proposals#activate", @response.body
- put '/proposals/inactivate'
- assert_equal 'proposals#inactivate', @response.body
+ put "/proposals/inactivate"
+ assert_equal "proposals#inactivate", @response.body
end
def test_match_shorthand_inside_namespace_with_controller
@@ -1473,126 +1517,130 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
- assert_equal '/api/products/list', api_products_list_path
- get '/api/products/list'
- assert_equal 'api/products#list', @response.body
+ assert_equal "/api/products/list", api_products_list_path
+ get "/api/products/list"
+ assert_equal "api/products#list", @response.body
end
def test_match_shorthand_inside_scope_with_variables_with_controller
draw do
- scope ':locale' do
- match 'questions/new', via: [:get]
+ scope ":locale" do
+ match "questions/new", via: [:get]
end
end
- get '/de/questions/new'
- assert_equal 'questions#new', @response.body
- assert_equal 'de', @request.params[:locale]
+ get "/de/questions/new"
+ assert_equal "questions#new", @response.body
+ assert_equal "de", @request.params[:locale]
end
def test_match_shorthand_inside_nested_namespaces_and_scopes_with_controller
draw do
namespace :api do
namespace :v3 do
- scope ':locale' do
+ scope ":locale" do
get "products/list"
end
end
end
end
- get '/api/v3/en/products/list'
- assert_equal 'api/v3/products#list', @response.body
+ get "/api/v3/en/products/list"
+ assert_equal "api/v3/products#list", @response.body
end
def test_not_matching_shorthand_with_dynamic_parameters
draw do
- get ':controller/:action/admin'
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/admin"
+ end
end
- get '/finances/overview/admin'
- assert_equal 'finances#overview', @response.body
+ get "/finances/overview/admin"
+ assert_equal "finances#overview", @response.body
end
def test_controller_option_with_nesting_and_leading_slash
draw do
- scope '/job', controller: 'job' do
- scope ':id', action: 'manage_applicant' do
+ scope "/job", controller: "job" do
+ scope ":id", action: "manage_applicant" do
get "/active"
end
end
end
- get '/job/5/active'
- assert_equal 'job#manage_applicant', @response.body
+ get "/job/5/active"
+ assert_equal "job#manage_applicant", @response.body
end
def test_dynamically_generated_helpers_on_collection_do_not_clobber_resources_url_helper
draw do
resources :replies do
collection do
- get 'page/:page' => 'replies#index', :page => %r{\d+}
- get ':page' => 'replies#index', :page => %r{\d+}
+ get "page/:page" => "replies#index", :page => %r{\d+}
+ get ":page" => "replies#index", :page => %r{\d+}
end
end
end
- assert_equal '/replies', replies_path
+ assert_equal "/replies", replies_path
end
def test_scoped_controller_with_namespace_and_action
draw do
namespace :account do
- get ':action/callback', :action => /twitter|github/, :controller => "callbacks", :as => :callback
+ ActiveSupport::Deprecation.silence do
+ get ":action/callback", action: /twitter|github/, controller: "callbacks", as: :callback
+ end
end
end
- assert_equal '/account/twitter/callback', account_callback_path("twitter")
- get '/account/twitter/callback'
- assert_equal 'account/callbacks#twitter', @response.body
+ assert_equal "/account/twitter/callback", account_callback_path("twitter")
+ get "/account/twitter/callback"
+ assert_equal "account/callbacks#twitter", @response.body
- get '/account/whatever/callback'
- assert_equal 'Not Found', @response.body
+ get "/account/whatever/callback"
+ assert_equal "Not Found", @response.body
end
def test_convention_match_nested_and_with_leading_slash
draw do
- get '/account/nested/overview'
+ get "/account/nested/overview"
end
- assert_equal '/account/nested/overview', account_nested_overview_path
- get '/account/nested/overview'
- assert_equal 'account/nested#overview', @response.body
+ assert_equal "/account/nested/overview", account_nested_overview_path
+ get "/account/nested/overview"
+ assert_equal "account/nested#overview", @response.body
end
def test_convention_with_explicit_end
draw do
- get 'sign_in' => "sessions#new"
+ get "sign_in" => "sessions#new"
end
- get '/sign_in'
- assert_equal 'sessions#new', @response.body
- assert_equal '/sign_in', sign_in_path
+ get "/sign_in"
+ assert_equal "sessions#new", @response.body
+ assert_equal "/sign_in", sign_in_path
end
def test_redirect_with_complete_url_and_status
draw do
- get 'account/google' => redirect('http://www.google.com/', :status => 302)
+ get "account/google" => redirect("http://www.google.com/", status: 302)
end
- get '/account/google'
- verify_redirect 'http://www.google.com/', 302
+ get "/account/google"
+ verify_redirect "http://www.google.com/", 302
end
def test_redirect_with_port
draw do
- get 'account/login', :to => redirect("/login")
+ 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'
+ get "/account/login"
+ verify_redirect "http://www.example.com:3000/login"
ensure
self.host = previous_host
end
@@ -1600,270 +1648,310 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
def test_normalize_namespaced_matches
draw do
namespace :account do
- get 'description', :action => :description, :as => "description"
+ get "description", action: :description, as: "description"
end
end
- assert_equal '/account/description', account_description_path
+ assert_equal "/account/description", account_description_path
- get '/account/description'
- assert_equal 'account#description', @response.body
+ get "/account/description"
+ assert_equal "account#description", @response.body
end
def test_namespaced_roots
draw do
namespace :account do
- root :to => "account#index"
+ root to: "account#index"
end
end
- assert_equal '/account', account_root_path
- get '/account'
- assert_equal 'account/account#index', @response.body
+ assert_equal "/account", account_root_path
+ get "/account"
+ assert_equal "account/account#index", @response.body
end
def test_optional_scoped_root
draw do
- scope '(:locale)', :locale => /en|pl/ do
- root :to => 'projects#index'
+ scope "(:locale)", locale: /en|pl/ do
+ root to: "projects#index"
end
end
- assert_equal '/en', root_path("en")
- get '/en'
- assert_equal 'projects#index', @response.body
+ assert_equal "/en", root_path("en")
+ get "/en"
+ assert_equal "projects#index", @response.body
end
def test_optional_scoped_path
draw do
- scope '(:locale)', :locale => /en|pl/ do
+ scope "(:locale)", locale: /en|pl/ do
resources :descriptions
end
end
- assert_equal '/en/descriptions', descriptions_path("en")
- assert_equal '/descriptions', descriptions_path(nil)
- assert_equal '/en/descriptions/1', description_path("en", 1)
- assert_equal '/descriptions/1', description_path(nil, 1)
+ assert_equal "/en/descriptions", descriptions_path("en")
+ assert_equal "/descriptions", descriptions_path(nil)
+ assert_equal "/en/descriptions/1", description_path("en", 1)
+ assert_equal "/descriptions/1", description_path(nil, 1)
- get '/en/descriptions'
- assert_equal 'descriptions#index', @response.body
+ get "/en/descriptions"
+ assert_equal "descriptions#index", @response.body
- get '/descriptions'
- assert_equal 'descriptions#index', @response.body
+ get "/descriptions"
+ assert_equal "descriptions#index", @response.body
- get '/en/descriptions/1'
- assert_equal 'descriptions#show', @response.body
+ get "/en/descriptions/1"
+ assert_equal "descriptions#show", @response.body
- get '/descriptions/1'
- assert_equal 'descriptions#show', @response.body
+ get "/descriptions/1"
+ assert_equal "descriptions#show", @response.body
end
def test_nested_optional_scoped_path
draw do
namespace :admin do
- scope '(:locale)', :locale => /en|pl/ do
+ scope "(:locale)", locale: /en|pl/ do
resources :descriptions
end
end
end
- assert_equal '/admin/en/descriptions', admin_descriptions_path("en")
- assert_equal '/admin/descriptions', admin_descriptions_path(nil)
- assert_equal '/admin/en/descriptions/1', admin_description_path("en", 1)
- assert_equal '/admin/descriptions/1', admin_description_path(nil, 1)
+ assert_equal "/admin/en/descriptions", admin_descriptions_path("en")
+ assert_equal "/admin/descriptions", admin_descriptions_path(nil)
+ assert_equal "/admin/en/descriptions/1", admin_description_path("en", 1)
+ assert_equal "/admin/descriptions/1", admin_description_path(nil, 1)
- get '/admin/en/descriptions'
- assert_equal 'admin/descriptions#index', @response.body
+ get "/admin/en/descriptions"
+ assert_equal "admin/descriptions#index", @response.body
- get '/admin/descriptions'
- assert_equal 'admin/descriptions#index', @response.body
+ get "/admin/descriptions"
+ assert_equal "admin/descriptions#index", @response.body
- get '/admin/en/descriptions/1'
- assert_equal 'admin/descriptions#show', @response.body
+ get "/admin/en/descriptions/1"
+ assert_equal "admin/descriptions#show", @response.body
- get '/admin/descriptions/1'
- assert_equal 'admin/descriptions#show', @response.body
+ get "/admin/descriptions/1"
+ assert_equal "admin/descriptions#show", @response.body
end
def test_nested_optional_path_shorthand
draw do
- scope '(:locale)', :locale => /en|pl/ do
+ scope "(:locale)", locale: /en|pl/ do
get "registrations/new"
end
end
- get '/registrations/new'
+ get "/registrations/new"
assert_nil @request.params[:locale]
- get '/en/registrations/new'
- assert_equal 'en', @request.params[:locale]
+ get "/en/registrations/new"
+ assert_equal "en", @request.params[:locale]
end
def test_default_string_params
draw do
- get 'inline_pages/(:id)', :to => 'pages#show', :id => 'home'
- get 'default_pages/(:id)', :to => 'pages#show', :defaults => { :id => 'home' }
+ get "inline_pages/(:id)", to: "pages#show", id: "home"
+ get "default_pages/(:id)", to: "pages#show", defaults: { id: "home" }
- defaults :id => 'home' do
- get 'scoped_pages/(:id)', :to => 'pages#show'
+ defaults id: "home" do
+ get "scoped_pages/(:id)", to: "pages#show"
end
end
- get '/inline_pages'
- assert_equal 'home', @request.params[:id]
+ get "/inline_pages"
+ assert_equal "home", @request.params[:id]
- get '/default_pages'
- assert_equal 'home', @request.params[:id]
+ get "/default_pages"
+ assert_equal "home", @request.params[:id]
- get '/scoped_pages'
- assert_equal 'home', @request.params[:id]
+ get "/scoped_pages"
+ assert_equal "home", @request.params[:id]
end
def test_default_integer_params
draw do
- get 'inline_pages/(:page)', to: 'pages#show', page: 1
- get 'default_pages/(:page)', to: 'pages#show', defaults: { page: 1 }
+ get "inline_pages/(:page)", to: "pages#show", page: 1
+ get "default_pages/(:page)", to: "pages#show", defaults: { page: 1 }
defaults page: 1 do
- get 'scoped_pages/(:page)', to: 'pages#show'
+ get "scoped_pages/(:page)", to: "pages#show"
end
end
- get '/inline_pages'
+ get "/inline_pages"
assert_equal 1, @request.params[:page]
- get '/default_pages'
+ get "/default_pages"
assert_equal 1, @request.params[:page]
- get '/scoped_pages'
+ get "/scoped_pages"
assert_equal 1, @request.params[:page]
end
+ def test_keyed_default_string_params_with_match
+ draw do
+ match "/", to: "pages#show", via: :get, defaults: { id: "home" }
+ end
+
+ get "/"
+ assert_equal "home", @request.params[:id]
+ end
+
+ def test_default_string_params_with_match
+ draw do
+ match "/", to: "pages#show", via: :get, id: "home"
+ end
+
+ get "/"
+ assert_equal "home", @request.params[:id]
+ end
+
+ def test_keyed_default_string_params_with_root
+ draw do
+ root to: "pages#show", defaults: { id: "home" }
+ end
+
+ get "/"
+ assert_equal "home", @request.params[:id]
+ end
+
+ def test_default_string_params_with_root
+ draw do
+ root to: "pages#show", id: "home"
+ end
+
+ get "/"
+ assert_equal "home", @request.params[:id]
+ end
+
def test_resource_constraints
draw do
- resources :products, :constraints => { :id => /\d{4}/ } do
- root :to => "products#root"
- get :favorite, :on => :collection
+ resources :products, constraints: { id: /\d{4}/ } do
+ root to: "products#root"
+ get :favorite, on: :collection
resources :images
end
- resource :dashboard, :constraints => { :ip => /192\.168\.1\.\d{1,3}/ }
+ resource :dashboard, constraints: { ip: /192\.168\.1\.\d{1,3}/ }
end
- get '/products/1'
- assert_equal 'pass', @response.headers['X-Cascade']
- get '/products'
- assert_equal 'products#root', @response.body
- get '/products/favorite'
- assert_equal 'products#favorite', @response.body
- get '/products/0001'
- assert_equal 'products#show', @response.body
+ get "/products/1"
+ assert_equal "pass", @response.headers["X-Cascade"]
+ get "/products"
+ assert_equal "products#root", @response.body
+ get "/products/favorite"
+ assert_equal "products#favorite", @response.body
+ get "/products/0001"
+ assert_equal "products#show", @response.body
- get '/products/1/images'
- assert_equal 'pass', @response.headers['X-Cascade']
- get '/products/0001/images'
- assert_equal 'images#index', @response.body
- get '/products/0001/images/0001'
- assert_equal 'images#show', @response.body
+ get "/products/1/images"
+ assert_equal "pass", @response.headers["X-Cascade"]
+ get "/products/0001/images"
+ assert_equal "images#index", @response.body
+ get "/products/0001/images/0001"
+ assert_equal "images#show", @response.body
- get '/dashboard', headers: { 'REMOTE_ADDR' => '10.0.0.100' }
- assert_equal 'pass', @response.headers['X-Cascade']
- get '/dashboard', headers: { 'REMOTE_ADDR' => '192.168.1.100' }
- assert_equal 'dashboards#show', @response.body
+ get "/dashboard", headers: { "REMOTE_ADDR" => "10.0.0.100" }
+ assert_equal "pass", @response.headers["X-Cascade"]
+ get "/dashboard", headers: { "REMOTE_ADDR" => "192.168.1.100" }
+ assert_equal "dashboards#show", @response.body
end
def test_root_works_in_the_resources_scope
draw do
resources :products do
- root :to => "products#root"
+ root to: "products#root"
end
end
- get '/products'
- assert_equal 'products#root', @response.body
- assert_equal '/products', products_root_path
+ get "/products"
+ assert_equal "products#root", @response.body
+ assert_equal "/products", products_root_path
end
def test_module_scope
draw do
- resource :token, :module => :api
+ resource :token, module: :api
end
- get '/token'
- assert_equal 'api/tokens#show', @response.body
- assert_equal '/token', token_path
+ get "/token"
+ assert_equal "api/tokens#show", @response.body
+ assert_equal "/token", token_path
end
def test_path_scope
draw do
- scope :path => 'api' do
+ scope path: "api" do
resource :me
- get '/' => 'mes#index'
+ get "/" => "mes#index"
end
end
- get '/api/me'
- assert_equal 'mes#show', @response.body
- assert_equal '/api/me', me_path
+ get "/api/me"
+ assert_equal "mes#show", @response.body
+ assert_equal "/api/me", me_path
- get '/api'
- assert_equal 'mes#index', @response.body
+ get "/api"
+ assert_equal "mes#index", @response.body
end
def test_symbol_scope
draw do
- scope :path => 'api' do
+ scope path: "api" do
scope :v2 do
- resource :me, as: 'v2_me'
- get '/' => 'mes#index'
+ resource :me, as: "v2_me"
+ get "/" => "mes#index"
end
scope :v3, :admin do
- resource :me, as: 'v3_me'
+ resource :me, as: "v3_me"
end
end
end
- get '/api/v2/me'
- assert_equal 'mes#show', @response.body
- assert_equal '/api/v2/me', v2_me_path
+ get "/api/v2/me"
+ assert_equal "mes#show", @response.body
+ assert_equal "/api/v2/me", v2_me_path
- get '/api/v2'
- assert_equal 'mes#index', @response.body
+ get "/api/v2"
+ assert_equal "mes#index", @response.body
- get '/api/v3/admin/me'
- assert_equal 'mes#show', @response.body
+ get "/api/v3/admin/me"
+ assert_equal "mes#show", @response.body
end
def test_url_generator_for_generic_route
draw do
- get "whatever/:controller(/:action(/:id))"
+ ActiveSupport::Deprecation.silence do
+ get "whatever/:controller(/:action(/:id))"
+ end
end
- get '/whatever/foo/bar'
- assert_equal 'foo#bar', @response.body
+ get "/whatever/foo/bar"
+ assert_equal "foo#bar", @response.body
- assert_equal 'http://www.example.com/whatever/foo/bar/1',
- url_for(:controller => "foo", :action => "bar", :id => 1)
+ assert_equal "http://www.example.com/whatever/foo/bar/1",
+ url_for(controller: "foo", action: "bar", id: 1)
end
def test_url_generator_for_namespaced_generic_route
draw do
- get "whatever/:controller(/:action(/:id))", :id => /\d+/
+ ActiveSupport::Deprecation.silence do
+ get "whatever/:controller(/:action(/:id))", id: /\d+/
+ end
end
- get '/whatever/foo/bar/show'
- assert_equal 'foo/bar#show', @response.body
+ get "/whatever/foo/bar/show"
+ assert_equal "foo/bar#show", @response.body
- get '/whatever/foo/bar/show/1'
- assert_equal 'foo/bar#show', @response.body
+ get "/whatever/foo/bar/show/1"
+ assert_equal "foo/bar#show", @response.body
- assert_equal 'http://www.example.com/whatever/foo/bar/show',
- url_for(:controller => "foo/bar", :action => "show")
+ assert_equal "http://www.example.com/whatever/foo/bar/show",
+ url_for(controller: "foo/bar", action: "show")
- assert_equal 'http://www.example.com/whatever/foo/bar/show/1',
- url_for(:controller => "foo/bar", :action => "show", :id => '1')
+ assert_equal "http://www.example.com/whatever/foo/bar/show/1",
+ url_for(controller: "foo/bar", action: "show", id: "1")
end
def test_resource_new_actions
@@ -1874,16 +1962,16 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
- scope 'pt', :as => 'pt' do
- resources :projects, :path_names => { :new => 'novo' }, :path => 'projetos' do
- post :preview, :on => :new
+ scope "pt", as: "pt" do
+ resources :projects, path_names: { new: "novo" }, path: "projetos" do
+ post :preview, on: :new
end
- resource :admin, :path_names => { :new => 'novo' }, :path => 'administrador' do
- post :preview, :on => :new
+ resource :admin, path_names: { new: "novo" }, path: "administrador" do
+ post :preview, on: :new
end
- resources :products, :path_names => { :new => 'novo' } do
+ resources :products, path_names: { new: "novo" } do
new do
post :preview
end
@@ -1897,58 +1985,58 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
- assert_equal '/replies/new/preview', preview_new_reply_path
- assert_equal '/pt/projetos/novo/preview', preview_new_pt_project_path
- assert_equal '/pt/administrador/novo/preview', preview_new_pt_admin_path
- assert_equal '/pt/products/novo/preview', preview_new_pt_product_path
- assert_equal '/profile/new/preview', preview_new_profile_path
+ assert_equal "/replies/new/preview", preview_new_reply_path
+ assert_equal "/pt/projetos/novo/preview", preview_new_pt_project_path
+ assert_equal "/pt/administrador/novo/preview", preview_new_pt_admin_path
+ assert_equal "/pt/products/novo/preview", preview_new_pt_product_path
+ assert_equal "/profile/new/preview", preview_new_profile_path
- post '/replies/new/preview'
- assert_equal 'replies#preview', @response.body
+ post "/replies/new/preview"
+ assert_equal "replies#preview", @response.body
- post '/pt/projetos/novo/preview'
- assert_equal 'projects#preview', @response.body
+ post "/pt/projetos/novo/preview"
+ assert_equal "projects#preview", @response.body
- post '/pt/administrador/novo/preview'
- assert_equal 'admins#preview', @response.body
+ post "/pt/administrador/novo/preview"
+ assert_equal "admins#preview", @response.body
- post '/pt/products/novo/preview'
- assert_equal 'products#preview', @response.body
+ post "/pt/products/novo/preview"
+ assert_equal "products#preview", @response.body
- post '/profile/new/preview'
- assert_equal 'profiles#preview', @response.body
+ post "/profile/new/preview"
+ assert_equal "profiles#preview", @response.body
end
def test_resource_merges_options_from_scope
draw do
- scope :only => :show do
+ scope only: :show do
resource :account
end
end
assert_raise(NoMethodError) { new_account_path }
- get '/account/new'
+ get "/account/new"
assert_equal 404, status
end
def test_resources_merges_options_from_scope
draw do
- scope :only => [:index, :show] do
+ scope only: [:index, :show] do
resources :products do
resources :images
end
end
end
- assert_raise(NoMethodError) { edit_product_path('1') }
+ assert_raise(NoMethodError) { edit_product_path("1") }
- get '/products/1/edit'
+ get "/products/1/edit"
assert_equal 404, status
- assert_raise(NoMethodError) { edit_product_image_path('1', '2') }
+ assert_raise(NoMethodError) { edit_product_image_path("1", "2") }
- post '/products/1/images/2/edit'
+ post "/products/1/images/2/edit"
assert_equal 404, status
end
@@ -1963,7 +2051,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
- resources :threads, :shallow => true do
+ resources :threads, shallow: true do
resource :owner
resources :messages do
resources :comments do
@@ -1975,105 +2063,105 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
- get '/api/teams'
- assert_equal 'api/teams#index', @response.body
- assert_equal '/api/teams', api_teams_path
+ get "/api/teams"
+ assert_equal "api/teams#index", @response.body
+ assert_equal "/api/teams", api_teams_path
- get '/api/teams/new'
- assert_equal 'api/teams#new', @response.body
- assert_equal '/api/teams/new', new_api_team_path
+ get "/api/teams/new"
+ assert_equal "api/teams#new", @response.body
+ assert_equal "/api/teams/new", new_api_team_path
- get '/api/teams/1'
- assert_equal 'api/teams#show', @response.body
- assert_equal '/api/teams/1', api_team_path(:id => '1')
+ get "/api/teams/1"
+ assert_equal "api/teams#show", @response.body
+ assert_equal "/api/teams/1", api_team_path(id: "1")
- get '/api/teams/1/edit'
- assert_equal 'api/teams#edit', @response.body
- assert_equal '/api/teams/1/edit', edit_api_team_path(:id => '1')
+ get "/api/teams/1/edit"
+ assert_equal "api/teams#edit", @response.body
+ assert_equal "/api/teams/1/edit", edit_api_team_path(id: "1")
- get '/api/teams/1/players'
- assert_equal 'api/players#index', @response.body
- assert_equal '/api/teams/1/players', api_team_players_path(:team_id => '1')
+ get "/api/teams/1/players"
+ assert_equal "api/players#index", @response.body
+ assert_equal "/api/teams/1/players", api_team_players_path(team_id: "1")
- get '/api/teams/1/players/new'
- assert_equal 'api/players#new', @response.body
- assert_equal '/api/teams/1/players/new', new_api_team_player_path(:team_id => '1')
+ get "/api/teams/1/players/new"
+ assert_equal "api/players#new", @response.body
+ assert_equal "/api/teams/1/players/new", new_api_team_player_path(team_id: "1")
- get '/api/players/2'
- assert_equal 'api/players#show', @response.body
- assert_equal '/api/players/2', api_player_path(:id => '2')
+ get "/api/players/2"
+ assert_equal "api/players#show", @response.body
+ assert_equal "/api/players/2", api_player_path(id: "2")
- get '/api/players/2/edit'
- assert_equal 'api/players#edit', @response.body
- assert_equal '/api/players/2/edit', edit_api_player_path(:id => '2')
+ get "/api/players/2/edit"
+ assert_equal "api/players#edit", @response.body
+ assert_equal "/api/players/2/edit", edit_api_player_path(id: "2")
- get '/api/teams/1/captain'
- assert_equal 'api/captains#show', @response.body
- assert_equal '/api/teams/1/captain', api_team_captain_path(:team_id => '1')
+ get "/api/teams/1/captain"
+ assert_equal "api/captains#show", @response.body
+ assert_equal "/api/teams/1/captain", api_team_captain_path(team_id: "1")
- get '/api/teams/1/captain/new'
- assert_equal 'api/captains#new', @response.body
- assert_equal '/api/teams/1/captain/new', new_api_team_captain_path(:team_id => '1')
+ get "/api/teams/1/captain/new"
+ assert_equal "api/captains#new", @response.body
+ assert_equal "/api/teams/1/captain/new", new_api_team_captain_path(team_id: "1")
- get '/api/teams/1/captain/edit'
- assert_equal 'api/captains#edit', @response.body
- assert_equal '/api/teams/1/captain/edit', edit_api_team_captain_path(:team_id => '1')
+ get "/api/teams/1/captain/edit"
+ assert_equal "api/captains#edit", @response.body
+ assert_equal "/api/teams/1/captain/edit", edit_api_team_captain_path(team_id: "1")
- get '/threads'
- assert_equal 'threads#index', @response.body
- assert_equal '/threads', threads_path
+ get "/threads"
+ assert_equal "threads#index", @response.body
+ assert_equal "/threads", threads_path
- get '/threads/new'
- assert_equal 'threads#new', @response.body
- assert_equal '/threads/new', new_thread_path
+ get "/threads/new"
+ assert_equal "threads#new", @response.body
+ assert_equal "/threads/new", new_thread_path
- get '/threads/1'
- assert_equal 'threads#show', @response.body
- assert_equal '/threads/1', thread_path(:id => '1')
+ get "/threads/1"
+ assert_equal "threads#show", @response.body
+ assert_equal "/threads/1", thread_path(id: "1")
- get '/threads/1/edit'
- assert_equal 'threads#edit', @response.body
- assert_equal '/threads/1/edit', edit_thread_path(:id => '1')
+ get "/threads/1/edit"
+ assert_equal "threads#edit", @response.body
+ assert_equal "/threads/1/edit", edit_thread_path(id: "1")
- get '/threads/1/owner'
- assert_equal 'owners#show', @response.body
- assert_equal '/threads/1/owner', thread_owner_path(:thread_id => '1')
+ get "/threads/1/owner"
+ assert_equal "owners#show", @response.body
+ assert_equal "/threads/1/owner", thread_owner_path(thread_id: "1")
- get '/threads/1/messages'
- assert_equal 'messages#index', @response.body
- assert_equal '/threads/1/messages', thread_messages_path(:thread_id => '1')
+ get "/threads/1/messages"
+ assert_equal "messages#index", @response.body
+ assert_equal "/threads/1/messages", thread_messages_path(thread_id: "1")
- get '/threads/1/messages/new'
- assert_equal 'messages#new', @response.body
- assert_equal '/threads/1/messages/new', new_thread_message_path(:thread_id => '1')
+ get "/threads/1/messages/new"
+ assert_equal "messages#new", @response.body
+ assert_equal "/threads/1/messages/new", new_thread_message_path(thread_id: "1")
- get '/messages/2'
- assert_equal 'messages#show', @response.body
- assert_equal '/messages/2', message_path(:id => '2')
+ get "/messages/2"
+ assert_equal "messages#show", @response.body
+ assert_equal "/messages/2", message_path(id: "2")
- get '/messages/2/edit'
- assert_equal 'messages#edit', @response.body
- assert_equal '/messages/2/edit', edit_message_path(:id => '2')
+ get "/messages/2/edit"
+ assert_equal "messages#edit", @response.body
+ assert_equal "/messages/2/edit", edit_message_path(id: "2")
- get '/messages/2/comments'
- assert_equal 'comments#index', @response.body
- assert_equal '/messages/2/comments', message_comments_path(:message_id => '2')
+ get "/messages/2/comments"
+ assert_equal "comments#index", @response.body
+ assert_equal "/messages/2/comments", message_comments_path(message_id: "2")
- get '/messages/2/comments/new'
- assert_equal 'comments#new', @response.body
- assert_equal '/messages/2/comments/new', new_message_comment_path(:message_id => '2')
+ get "/messages/2/comments/new"
+ assert_equal "comments#new", @response.body
+ assert_equal "/messages/2/comments/new", new_message_comment_path(message_id: "2")
- get '/comments/3'
- assert_equal 'comments#show', @response.body
- assert_equal '/comments/3', comment_path(:id => '3')
+ get "/comments/3"
+ assert_equal "comments#show", @response.body
+ assert_equal "/comments/3", comment_path(id: "3")
- get '/comments/3/edit'
- assert_equal 'comments#edit', @response.body
- assert_equal '/comments/3/edit', edit_comment_path(:id => '3')
+ get "/comments/3/edit"
+ assert_equal "comments#edit", @response.body
+ assert_equal "/comments/3/edit", edit_comment_path(id: "3")
- post '/comments/3/preview'
- assert_equal 'comments#preview', @response.body
- assert_equal '/comments/3/preview', preview_comment_path(:id => '3')
+ post "/comments/3/preview"
+ assert_equal "comments#preview", @response.body
+ assert_equal "/comments/3/preview", preview_comment_path(id: "3")
end
def test_shallow_nested_resources_inside_resource
@@ -2083,33 +2171,33 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
- get '/membership/cards'
- assert_equal 'cards#index', @response.body
- assert_equal '/membership/cards', membership_cards_path
+ get "/membership/cards"
+ assert_equal "cards#index", @response.body
+ assert_equal "/membership/cards", membership_cards_path
- get '/membership/cards/new'
- assert_equal 'cards#new', @response.body
- assert_equal '/membership/cards/new', new_membership_card_path
+ get "/membership/cards/new"
+ assert_equal "cards#new", @response.body
+ assert_equal "/membership/cards/new", new_membership_card_path
- post '/membership/cards'
- assert_equal 'cards#create', @response.body
+ post "/membership/cards"
+ assert_equal "cards#create", @response.body
- get '/cards/1'
- assert_equal 'cards#show', @response.body
- assert_equal '/cards/1', card_path('1')
+ get "/cards/1"
+ assert_equal "cards#show", @response.body
+ assert_equal "/cards/1", card_path("1")
- get '/cards/1/edit'
- assert_equal 'cards#edit', @response.body
- assert_equal '/cards/1/edit', edit_card_path('1')
+ get "/cards/1/edit"
+ assert_equal "cards#edit", @response.body
+ assert_equal "/cards/1/edit", edit_card_path("1")
- put '/cards/1'
- assert_equal 'cards#update', @response.body
+ put "/cards/1"
+ assert_equal "cards#update", @response.body
- patch '/cards/1'
- assert_equal 'cards#update', @response.body
+ patch "/cards/1"
+ assert_equal "cards#update", @response.body
- delete '/cards/1'
- assert_equal 'cards#destroy', @response.body
+ delete "/cards/1"
+ assert_equal "cards#destroy", @response.body
end
def test_shallow_deeply_nested_resources
@@ -2121,13 +2209,13 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
- get '/comments/1'
- assert_equal 'comments#show', @response.body
+ get "/comments/1"
+ assert_equal "comments#show", @response.body
- assert_equal '/comments/1', comment_path('1')
- assert_equal '/blogs/new', new_blog_path
- assert_equal '/blogs/1/posts/new', new_blog_post_path(:blog_id => 1)
- assert_equal '/blogs/1/posts/2/comments/new', new_blog_post_comment_path(:blog_id => 1, :post_id => 2)
+ assert_equal "/comments/1", comment_path("1")
+ assert_equal "/blogs/new", new_blog_path
+ assert_equal "/blogs/1/posts/new", new_blog_post_path(blog_id: 1)
+ assert_equal "/blogs/1/posts/2/comments/new", new_blog_post_comment_path(blog_id: 1, post_id: 2)
end
def test_direct_children_of_shallow_resources
@@ -2139,22 +2227,22 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
- post '/posts/1/comments'
- assert_equal 'comments#create', @response.body
- assert_equal '/posts/1/comments', post_comments_path('1')
+ post "/posts/1/comments"
+ assert_equal "comments#create", @response.body
+ assert_equal "/posts/1/comments", post_comments_path("1")
- get '/posts/2/comments/new'
- assert_equal 'comments#new', @response.body
- assert_equal '/posts/2/comments/new', new_post_comment_path('2')
+ get "/posts/2/comments/new"
+ assert_equal "comments#new", @response.body
+ assert_equal "/posts/2/comments/new", new_post_comment_path("2")
- get '/posts/1/comments'
- assert_equal 'comments#index', @response.body
- assert_equal '/posts/1/comments', post_comments_path('1')
+ get "/posts/1/comments"
+ assert_equal "comments#index", @response.body
+ assert_equal "/posts/1/comments", post_comments_path("1")
end
def test_shallow_nested_resources_within_scope
draw do
- scope '/hello' do
+ scope "/hello" do
shallow do
resources :notes do
resources :trackbacks
@@ -2163,120 +2251,120 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
- get '/hello/notes/1/trackbacks'
- assert_equal 'trackbacks#index', @response.body
- assert_equal '/hello/notes/1/trackbacks', note_trackbacks_path(:note_id => 1)
+ get "/hello/notes/1/trackbacks"
+ assert_equal "trackbacks#index", @response.body
+ assert_equal "/hello/notes/1/trackbacks", note_trackbacks_path(note_id: 1)
- get '/hello/notes/1/edit'
- assert_equal 'notes#edit', @response.body
- assert_equal '/hello/notes/1/edit', edit_note_path(:id => '1')
+ get "/hello/notes/1/edit"
+ assert_equal "notes#edit", @response.body
+ assert_equal "/hello/notes/1/edit", edit_note_path(id: "1")
- get '/hello/notes/1/trackbacks/new'
- assert_equal 'trackbacks#new', @response.body
- assert_equal '/hello/notes/1/trackbacks/new', new_note_trackback_path(:note_id => 1)
+ get "/hello/notes/1/trackbacks/new"
+ assert_equal "trackbacks#new", @response.body
+ assert_equal "/hello/notes/1/trackbacks/new", new_note_trackback_path(note_id: 1)
- get '/hello/trackbacks/1'
- assert_equal 'trackbacks#show', @response.body
- assert_equal '/hello/trackbacks/1', trackback_path(:id => '1')
+ get "/hello/trackbacks/1"
+ assert_equal "trackbacks#show", @response.body
+ assert_equal "/hello/trackbacks/1", trackback_path(id: "1")
- get '/hello/trackbacks/1/edit'
- assert_equal 'trackbacks#edit', @response.body
- assert_equal '/hello/trackbacks/1/edit', edit_trackback_path(:id => '1')
+ get "/hello/trackbacks/1/edit"
+ assert_equal "trackbacks#edit", @response.body
+ assert_equal "/hello/trackbacks/1/edit", edit_trackback_path(id: "1")
- put '/hello/trackbacks/1'
- assert_equal 'trackbacks#update', @response.body
+ put "/hello/trackbacks/1"
+ assert_equal "trackbacks#update", @response.body
- post '/hello/notes/1/trackbacks'
- assert_equal 'trackbacks#create', @response.body
+ post "/hello/notes/1/trackbacks"
+ assert_equal "trackbacks#create", @response.body
- delete '/hello/trackbacks/1'
- assert_equal 'trackbacks#destroy', @response.body
+ delete "/hello/trackbacks/1"
+ assert_equal "trackbacks#destroy", @response.body
- get '/hello/notes'
- assert_equal 'notes#index', @response.body
+ get "/hello/notes"
+ assert_equal "notes#index", @response.body
- post '/hello/notes'
- assert_equal 'notes#create', @response.body
+ post "/hello/notes"
+ assert_equal "notes#create", @response.body
- get '/hello/notes/new'
- assert_equal 'notes#new', @response.body
- assert_equal '/hello/notes/new', new_note_path
+ get "/hello/notes/new"
+ assert_equal "notes#new", @response.body
+ assert_equal "/hello/notes/new", new_note_path
- get '/hello/notes/1'
- assert_equal 'notes#show', @response.body
- assert_equal '/hello/notes/1', note_path(:id => 1)
+ get "/hello/notes/1"
+ assert_equal "notes#show", @response.body
+ assert_equal "/hello/notes/1", note_path(id: 1)
- put '/hello/notes/1'
- assert_equal 'notes#update', @response.body
+ put "/hello/notes/1"
+ assert_equal "notes#update", @response.body
- delete '/hello/notes/1'
- assert_equal 'notes#destroy', @response.body
+ delete "/hello/notes/1"
+ assert_equal "notes#destroy", @response.body
end
def test_shallow_option_nested_resources_within_scope
draw do
- scope '/hello' do
- resources :notes, :shallow => true do
+ scope "/hello" do
+ resources :notes, shallow: true do
resources :trackbacks
end
end
end
- get '/hello/notes/1/trackbacks'
- assert_equal 'trackbacks#index', @response.body
- assert_equal '/hello/notes/1/trackbacks', note_trackbacks_path(:note_id => 1)
+ get "/hello/notes/1/trackbacks"
+ assert_equal "trackbacks#index", @response.body
+ assert_equal "/hello/notes/1/trackbacks", note_trackbacks_path(note_id: 1)
- get '/hello/notes/1/edit'
- assert_equal 'notes#edit', @response.body
- assert_equal '/hello/notes/1/edit', edit_note_path(:id => '1')
+ get "/hello/notes/1/edit"
+ assert_equal "notes#edit", @response.body
+ assert_equal "/hello/notes/1/edit", edit_note_path(id: "1")
- get '/hello/notes/1/trackbacks/new'
- assert_equal 'trackbacks#new', @response.body
- assert_equal '/hello/notes/1/trackbacks/new', new_note_trackback_path(:note_id => 1)
+ get "/hello/notes/1/trackbacks/new"
+ assert_equal "trackbacks#new", @response.body
+ assert_equal "/hello/notes/1/trackbacks/new", new_note_trackback_path(note_id: 1)
- get '/hello/trackbacks/1'
- assert_equal 'trackbacks#show', @response.body
- assert_equal '/hello/trackbacks/1', trackback_path(:id => '1')
+ get "/hello/trackbacks/1"
+ assert_equal "trackbacks#show", @response.body
+ assert_equal "/hello/trackbacks/1", trackback_path(id: "1")
- get '/hello/trackbacks/1/edit'
- assert_equal 'trackbacks#edit', @response.body
- assert_equal '/hello/trackbacks/1/edit', edit_trackback_path(:id => '1')
+ get "/hello/trackbacks/1/edit"
+ assert_equal "trackbacks#edit", @response.body
+ assert_equal "/hello/trackbacks/1/edit", edit_trackback_path(id: "1")
- put '/hello/trackbacks/1'
- assert_equal 'trackbacks#update', @response.body
+ put "/hello/trackbacks/1"
+ assert_equal "trackbacks#update", @response.body
- post '/hello/notes/1/trackbacks'
- assert_equal 'trackbacks#create', @response.body
+ post "/hello/notes/1/trackbacks"
+ assert_equal "trackbacks#create", @response.body
- delete '/hello/trackbacks/1'
- assert_equal 'trackbacks#destroy', @response.body
+ delete "/hello/trackbacks/1"
+ assert_equal "trackbacks#destroy", @response.body
- get '/hello/notes'
- assert_equal 'notes#index', @response.body
+ get "/hello/notes"
+ assert_equal "notes#index", @response.body
- post '/hello/notes'
- assert_equal 'notes#create', @response.body
+ post "/hello/notes"
+ assert_equal "notes#create", @response.body
- get '/hello/notes/new'
- assert_equal 'notes#new', @response.body
- assert_equal '/hello/notes/new', new_note_path
+ get "/hello/notes/new"
+ assert_equal "notes#new", @response.body
+ assert_equal "/hello/notes/new", new_note_path
- get '/hello/notes/1'
- assert_equal 'notes#show', @response.body
- assert_equal '/hello/notes/1', note_path(:id => 1)
+ get "/hello/notes/1"
+ assert_equal "notes#show", @response.body
+ assert_equal "/hello/notes/1", note_path(id: 1)
- put '/hello/notes/1'
- assert_equal 'notes#update', @response.body
+ put "/hello/notes/1"
+ assert_equal "notes#update", @response.body
- delete '/hello/notes/1'
- assert_equal 'notes#destroy', @response.body
+ delete "/hello/notes/1"
+ assert_equal "notes#destroy", @response.body
end
def test_custom_resource_routes_are_scoped
draw do
resources :customers do
- get :recent, :on => :collection
- get "profile", :on => :member
+ get :recent, on: :collection
+ get "profile", on: :member
get "secret/profile" => "customers#secret", :on => :member
post "preview" => "customers#preview", :as => :another_preview, :on => :new
resource :avatar do
@@ -2284,11 +2372,11 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
resources :invoices do
get "outstanding" => "invoices#outstanding", :on => :collection
- get "overdue", :action => :overdue, :on => :collection
+ get "overdue", action: :overdue, on: :collection
get "print" => "invoices#print", :as => :print, :on => :member
post "preview" => "invoices#preview", :as => :preview, :on => :new
end
- resources :notes, :shallow => true do
+ resources :notes, shallow: true do
get "preview" => "notes#preview", :as => :preview, :on => :new
get "print" => "notes#print", :as => :print, :on => :member
end
@@ -2303,79 +2391,79 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
- assert_equal '/customers/recent', recent_customers_path
- assert_equal '/customers/1/profile', profile_customer_path(:id => '1')
- assert_equal '/customers/1/secret/profile', secret_profile_customer_path(:id => '1')
- assert_equal '/customers/new/preview', another_preview_new_customer_path
- assert_equal '/customers/1/avatar/thumbnail.jpg', thumbnail_customer_avatar_path(:customer_id => '1', :format => :jpg)
- assert_equal '/customers/1/invoices/outstanding', outstanding_customer_invoices_path(:customer_id => '1')
- assert_equal '/customers/1/invoices/2/print', print_customer_invoice_path(:customer_id => '1', :id => '2')
- assert_equal '/customers/1/invoices/new/preview', preview_new_customer_invoice_path(:customer_id => '1')
- assert_equal '/customers/1/notes/new/preview', preview_new_customer_note_path(:customer_id => '1')
- assert_equal '/notes/1/print', print_note_path(:id => '1')
- assert_equal '/api/customers/recent', recent_api_customers_path
- assert_equal '/api/customers/1/profile', profile_api_customer_path(:id => '1')
- assert_equal '/api/customers/new/preview', preview_new_api_customer_path
+ assert_equal "/customers/recent", recent_customers_path
+ assert_equal "/customers/1/profile", profile_customer_path(id: "1")
+ assert_equal "/customers/1/secret/profile", secret_profile_customer_path(id: "1")
+ assert_equal "/customers/new/preview", another_preview_new_customer_path
+ assert_equal "/customers/1/avatar/thumbnail.jpg", thumbnail_customer_avatar_path(customer_id: "1", format: :jpg)
+ assert_equal "/customers/1/invoices/outstanding", outstanding_customer_invoices_path(customer_id: "1")
+ assert_equal "/customers/1/invoices/2/print", print_customer_invoice_path(customer_id: "1", id: "2")
+ assert_equal "/customers/1/invoices/new/preview", preview_new_customer_invoice_path(customer_id: "1")
+ assert_equal "/customers/1/notes/new/preview", preview_new_customer_note_path(customer_id: "1")
+ assert_equal "/notes/1/print", print_note_path(id: "1")
+ assert_equal "/api/customers/recent", recent_api_customers_path
+ assert_equal "/api/customers/1/profile", profile_api_customer_path(id: "1")
+ assert_equal "/api/customers/new/preview", preview_new_api_customer_path
- get '/customers/1/invoices/overdue'
- assert_equal 'invoices#overdue', @response.body
+ get "/customers/1/invoices/overdue"
+ assert_equal "invoices#overdue", @response.body
- get '/customers/1/secret/profile'
- assert_equal 'customers#secret', @response.body
+ get "/customers/1/secret/profile"
+ assert_equal "customers#secret", @response.body
end
def test_shallow_nested_routes_ignore_module
draw do
- scope :module => :api do
- resources :errors, :shallow => true do
+ scope module: :api do
+ resources :errors, shallow: true do
resources :notices
end
end
end
- get '/errors/1/notices'
- assert_equal 'api/notices#index', @response.body
- assert_equal '/errors/1/notices', error_notices_path(:error_id => '1')
+ get "/errors/1/notices"
+ assert_equal "api/notices#index", @response.body
+ assert_equal "/errors/1/notices", error_notices_path(error_id: "1")
- get '/notices/1'
- assert_equal 'api/notices#show', @response.body
- assert_equal '/notices/1', notice_path(:id => '1')
+ get "/notices/1"
+ assert_equal "api/notices#show", @response.body
+ assert_equal "/notices/1", notice_path(id: "1")
end
def test_non_greedy_regexp
draw do
namespace :api do
- scope(':version', :version => /.+/) do
- resources :users, :id => /.+?/, :format => /json|xml/
+ scope(":version", version: /.+/) do
+ resources :users, id: /.+?/, format: /json|xml/
end
end
end
- get '/api/1.0/users'
- assert_equal 'api/users#index', @response.body
- assert_equal '/api/1.0/users', api_users_path(:version => '1.0')
+ get "/api/1.0/users"
+ assert_equal "api/users#index", @response.body
+ assert_equal "/api/1.0/users", api_users_path(version: "1.0")
- get '/api/1.0/users.json'
- assert_equal 'api/users#index', @response.body
+ get "/api/1.0/users.json"
+ assert_equal "api/users#index", @response.body
assert_equal true, @request.format.json?
- assert_equal '/api/1.0/users.json', api_users_path(:version => '1.0', :format => :json)
+ assert_equal "/api/1.0/users.json", api_users_path(version: "1.0", format: :json)
- get '/api/1.0/users/first.last'
- assert_equal 'api/users#show', @response.body
- assert_equal 'first.last', @request.params[:id]
- assert_equal '/api/1.0/users/first.last', api_user_path(:version => '1.0', :id => 'first.last')
+ get "/api/1.0/users/first.last"
+ assert_equal "api/users#show", @response.body
+ assert_equal "first.last", @request.params[:id]
+ assert_equal "/api/1.0/users/first.last", api_user_path(version: "1.0", id: "first.last")
- get '/api/1.0/users/first.last.xml'
- assert_equal 'api/users#show', @response.body
- assert_equal 'first.last', @request.params[:id]
+ get "/api/1.0/users/first.last.xml"
+ assert_equal "api/users#show", @response.body
+ assert_equal "first.last", @request.params[:id]
assert_equal true, @request.format.xml?
- assert_equal '/api/1.0/users/first.last.xml', api_user_path(:version => '1.0', :id => 'first.last', :format => :xml)
+ assert_equal "/api/1.0/users/first.last.xml", api_user_path(version: "1.0", id: "first.last", format: :xml)
end
def test_match_without_via
assert_raises(ArgumentError) do
draw do
- match '/foo/bar', :to => 'files#show'
+ match "/foo/bar", to: "files#show"
end
end
end
@@ -2383,17 +2471,17 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
def test_match_with_empty_via
assert_raises(ArgumentError) do
draw do
- match '/foo/bar', :to => 'files#show', :via => []
+ match "/foo/bar", to: "files#show", via: []
end
end
end
def test_glob_parameter_accepts_regexp
draw do
- get '/:locale/*file.:format', :to => 'files#show', :file => /path\/to\/existing\/file/
+ get "/:locale/*file.:format", to: "files#show", file: /path\/to\/existing\/file/
end
- get '/en/path/to/existing/file.html'
+ get "/en/path/to/existing/file.html"
assert_equal 200, @response.status
end
@@ -2402,8 +2490,8 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
resources :content
end
- get '/content'
- assert_equal 'content#index', @response.body
+ get "/content"
+ assert_equal "content#index", @response.body
end
def test_url_generator_for_optional_prefix_dynamic_segment
@@ -2411,15 +2499,15 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
get "(/:username)/followers" => "followers#index"
end
- get '/bob/followers'
- assert_equal 'followers#index', @response.body
- assert_equal 'http://www.example.com/bob/followers',
- url_for(:controller => "followers", :action => "index", :username => "bob")
+ get "/bob/followers"
+ assert_equal "followers#index", @response.body
+ assert_equal "http://www.example.com/bob/followers",
+ url_for(controller: "followers", action: "index", username: "bob")
- get '/followers'
- assert_equal 'followers#index', @response.body
- assert_equal 'http://www.example.com/followers',
- url_for(:controller => "followers", :action => "index", :username => nil)
+ get "/followers"
+ assert_equal "followers#index", @response.body
+ assert_equal "http://www.example.com/followers",
+ url_for(controller: "followers", action: "index", username: nil)
end
def test_url_generator_for_optional_suffix_static_and_dynamic_segment
@@ -2427,15 +2515,15 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
get "/groups(/user/:username)" => "groups#index"
end
- get '/groups/user/bob'
- assert_equal 'groups#index', @response.body
- assert_equal 'http://www.example.com/groups/user/bob',
- url_for(:controller => "groups", :action => "index", :username => "bob")
+ get "/groups/user/bob"
+ assert_equal "groups#index", @response.body
+ assert_equal "http://www.example.com/groups/user/bob",
+ url_for(controller: "groups", action: "index", username: "bob")
- get '/groups'
- assert_equal 'groups#index', @response.body
- assert_equal 'http://www.example.com/groups',
- url_for(:controller => "groups", :action => "index", :username => nil)
+ get "/groups"
+ assert_equal "groups#index", @response.body
+ assert_equal "http://www.example.com/groups",
+ url_for(controller: "groups", action: "index", username: nil)
end
def test_url_generator_for_optional_prefix_static_and_dynamic_segment
@@ -2443,66 +2531,66 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
get "(/user/:username)/photos" => "photos#index"
end
- get '/user/bob/photos'
- assert_equal 'photos#index', @response.body
- assert_equal 'http://www.example.com/user/bob/photos',
- url_for(:controller => "photos", :action => "index", :username => "bob")
+ get "/user/bob/photos"
+ assert_equal "photos#index", @response.body
+ assert_equal "http://www.example.com/user/bob/photos",
+ url_for(controller: "photos", action: "index", username: "bob")
- get '/photos'
- assert_equal 'photos#index', @response.body
- assert_equal 'http://www.example.com/photos',
- url_for(:controller => "photos", :action => "index", :username => nil)
+ get "/photos"
+ assert_equal "photos#index", @response.body
+ assert_equal "http://www.example.com/photos",
+ url_for(controller: "photos", action: "index", username: nil)
end
def test_url_recognition_for_optional_static_segments
draw do
- scope '(groups)' do
- scope '(discussions)' do
+ scope "(groups)" do
+ scope "(discussions)" do
resources :messages
end
end
end
- get '/groups/discussions/messages'
- assert_equal 'messages#index', @response.body
+ get "/groups/discussions/messages"
+ assert_equal "messages#index", @response.body
- get '/groups/discussions/messages/1'
- assert_equal 'messages#show', @response.body
+ get "/groups/discussions/messages/1"
+ assert_equal "messages#show", @response.body
- get '/groups/messages'
- assert_equal 'messages#index', @response.body
+ get "/groups/messages"
+ assert_equal "messages#index", @response.body
- get '/groups/messages/1'
- assert_equal 'messages#show', @response.body
+ get "/groups/messages/1"
+ assert_equal "messages#show", @response.body
- get '/discussions/messages'
- assert_equal 'messages#index', @response.body
+ get "/discussions/messages"
+ assert_equal "messages#index", @response.body
- get '/discussions/messages/1'
- assert_equal 'messages#show', @response.body
+ get "/discussions/messages/1"
+ assert_equal "messages#show", @response.body
- get '/messages'
- assert_equal 'messages#index', @response.body
+ get "/messages"
+ assert_equal "messages#index", @response.body
- get '/messages/1'
- assert_equal 'messages#show', @response.body
+ get "/messages/1"
+ assert_equal "messages#show", @response.body
end
def test_router_removes_invalid_conditions
draw do
- scope :constraints => { :id => /\d+/ } do
- get '/tickets', :to => 'tickets#index', :as => :tickets
+ scope constraints: { id: /\d+/ } do
+ get "/tickets", to: "tickets#index", as: :tickets
end
end
- get '/tickets'
- assert_equal 'tickets#index', @response.body
- assert_equal '/tickets', tickets_path
+ get "/tickets"
+ assert_equal "tickets#index", @response.body
+ assert_equal "/tickets", tickets_path
end
def test_constraints_are_merged_from_scope
draw do
- scope :constraints => { :id => /\d{4}/ } do
+ scope constraints: { id: /\d{4}/ } do
resources :movies do
resources :reviews
resource :trailer
@@ -2510,42 +2598,42 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
- get '/movies/0001'
- assert_equal 'movies#show', @response.body
- assert_equal '/movies/0001', movie_path(:id => '0001')
+ get "/movies/0001"
+ assert_equal "movies#show", @response.body
+ assert_equal "/movies/0001", movie_path(id: "0001")
- get '/movies/00001'
- assert_equal 'Not Found', @response.body
- assert_raises(ActionController::UrlGenerationError){ movie_path(:id => '00001') }
+ get "/movies/00001"
+ assert_equal "Not Found", @response.body
+ assert_raises(ActionController::UrlGenerationError) { movie_path(id: "00001") }
- get '/movies/0001/reviews'
- assert_equal 'reviews#index', @response.body
- assert_equal '/movies/0001/reviews', movie_reviews_path(:movie_id => '0001')
+ get "/movies/0001/reviews"
+ assert_equal "reviews#index", @response.body
+ assert_equal "/movies/0001/reviews", movie_reviews_path(movie_id: "0001")
- get '/movies/00001/reviews'
- assert_equal 'Not Found', @response.body
- assert_raises(ActionController::UrlGenerationError){ movie_reviews_path(:movie_id => '00001') }
+ get "/movies/00001/reviews"
+ assert_equal "Not Found", @response.body
+ assert_raises(ActionController::UrlGenerationError) { movie_reviews_path(movie_id: "00001") }
- get '/movies/0001/reviews/0001'
- assert_equal 'reviews#show', @response.body
- assert_equal '/movies/0001/reviews/0001', movie_review_path(:movie_id => '0001', :id => '0001')
+ get "/movies/0001/reviews/0001"
+ assert_equal "reviews#show", @response.body
+ assert_equal "/movies/0001/reviews/0001", movie_review_path(movie_id: "0001", id: "0001")
- get '/movies/00001/reviews/0001'
- assert_equal 'Not Found', @response.body
- assert_raises(ActionController::UrlGenerationError){ movie_path(:movie_id => '00001', :id => '00001') }
+ get "/movies/00001/reviews/0001"
+ assert_equal "Not Found", @response.body
+ assert_raises(ActionController::UrlGenerationError) { movie_path(movie_id: "00001", id: "00001") }
- get '/movies/0001/trailer'
- assert_equal 'trailers#show', @response.body
- assert_equal '/movies/0001/trailer', movie_trailer_path(:movie_id => '0001')
+ get "/movies/0001/trailer"
+ assert_equal "trailers#show", @response.body
+ assert_equal "/movies/0001/trailer", movie_trailer_path(movie_id: "0001")
- get '/movies/00001/trailer'
- assert_equal 'Not Found', @response.body
- assert_raises(ActionController::UrlGenerationError){ movie_trailer_path(:movie_id => '00001') }
+ get "/movies/00001/trailer"
+ assert_equal "Not Found", @response.body
+ assert_raises(ActionController::UrlGenerationError) { movie_trailer_path(movie_id: "00001") }
end
def test_only_should_be_read_from_scope
draw do
- scope :only => [:index, :show] do
+ scope only: [:index, :show] do
namespace :only do
resources :clubs do
resources :players
@@ -2555,34 +2643,34 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
- get '/only/clubs'
- assert_equal 'only/clubs#index', @response.body
- assert_equal '/only/clubs', only_clubs_path
+ get "/only/clubs"
+ assert_equal "only/clubs#index", @response.body
+ assert_equal "/only/clubs", only_clubs_path
- get '/only/clubs/1/edit'
- assert_equal 'Not Found', @response.body
- assert_raise(NoMethodError) { edit_only_club_path(:id => '1') }
+ get "/only/clubs/1/edit"
+ assert_equal "Not Found", @response.body
+ assert_raise(NoMethodError) { edit_only_club_path(id: "1") }
- get '/only/clubs/1/players'
- assert_equal 'only/players#index', @response.body
- assert_equal '/only/clubs/1/players', only_club_players_path(:club_id => '1')
+ get "/only/clubs/1/players"
+ assert_equal "only/players#index", @response.body
+ assert_equal "/only/clubs/1/players", only_club_players_path(club_id: "1")
- get '/only/clubs/1/players/2/edit'
- assert_equal 'Not Found', @response.body
- assert_raise(NoMethodError) { edit_only_club_player_path(:club_id => '1', :id => '2') }
+ get "/only/clubs/1/players/2/edit"
+ assert_equal "Not Found", @response.body
+ assert_raise(NoMethodError) { edit_only_club_player_path(club_id: "1", id: "2") }
- get '/only/clubs/1/chairman'
- assert_equal 'only/chairmen#show', @response.body
- assert_equal '/only/clubs/1/chairman', only_club_chairman_path(:club_id => '1')
+ get "/only/clubs/1/chairman"
+ assert_equal "only/chairmen#show", @response.body
+ assert_equal "/only/clubs/1/chairman", only_club_chairman_path(club_id: "1")
- get '/only/clubs/1/chairman/edit'
- assert_equal 'Not Found', @response.body
- assert_raise(NoMethodError) { edit_only_club_chairman_path(:club_id => '1') }
+ get "/only/clubs/1/chairman/edit"
+ assert_equal "Not Found", @response.body
+ assert_raise(NoMethodError) { edit_only_club_chairman_path(club_id: "1") }
end
def test_except_should_be_read_from_scope
draw do
- scope :except => [:new, :create, :edit, :update, :destroy] do
+ scope except: [:new, :create, :edit, :update, :destroy] do
namespace :except do
resources :clubs do
resources :players
@@ -2592,54 +2680,54 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
- get '/except/clubs'
- assert_equal 'except/clubs#index', @response.body
- assert_equal '/except/clubs', except_clubs_path
+ get "/except/clubs"
+ assert_equal "except/clubs#index", @response.body
+ assert_equal "/except/clubs", except_clubs_path
- get '/except/clubs/1/edit'
- assert_equal 'Not Found', @response.body
- assert_raise(NoMethodError) { edit_except_club_path(:id => '1') }
+ get "/except/clubs/1/edit"
+ assert_equal "Not Found", @response.body
+ assert_raise(NoMethodError) { edit_except_club_path(id: "1") }
- get '/except/clubs/1/players'
- assert_equal 'except/players#index', @response.body
- assert_equal '/except/clubs/1/players', except_club_players_path(:club_id => '1')
+ get "/except/clubs/1/players"
+ assert_equal "except/players#index", @response.body
+ assert_equal "/except/clubs/1/players", except_club_players_path(club_id: "1")
- get '/except/clubs/1/players/2/edit'
- assert_equal 'Not Found', @response.body
- assert_raise(NoMethodError) { edit_except_club_player_path(:club_id => '1', :id => '2') }
+ get "/except/clubs/1/players/2/edit"
+ assert_equal "Not Found", @response.body
+ assert_raise(NoMethodError) { edit_except_club_player_path(club_id: "1", id: "2") }
- get '/except/clubs/1/chairman'
- assert_equal 'except/chairmen#show', @response.body
- assert_equal '/except/clubs/1/chairman', except_club_chairman_path(:club_id => '1')
+ get "/except/clubs/1/chairman"
+ assert_equal "except/chairmen#show", @response.body
+ assert_equal "/except/clubs/1/chairman", except_club_chairman_path(club_id: "1")
- get '/except/clubs/1/chairman/edit'
- assert_equal 'Not Found', @response.body
- assert_raise(NoMethodError) { edit_except_club_chairman_path(:club_id => '1') }
+ get "/except/clubs/1/chairman/edit"
+ assert_equal "Not Found", @response.body
+ assert_raise(NoMethodError) { edit_except_club_chairman_path(club_id: "1") }
end
def test_only_option_should_override_scope
draw do
- scope :only => :show do
+ scope only: :show do
namespace :only do
- resources :sectors, :only => :index
+ resources :sectors, only: :index
end
end
end
- get '/only/sectors'
- assert_equal 'only/sectors#index', @response.body
- assert_equal '/only/sectors', only_sectors_path
+ get "/only/sectors"
+ assert_equal "only/sectors#index", @response.body
+ assert_equal "/only/sectors", only_sectors_path
- get '/only/sectors/1'
- assert_equal 'Not Found', @response.body
- assert_raise(NoMethodError) { only_sector_path(:id => '1') }
+ get "/only/sectors/1"
+ assert_equal "Not Found", @response.body
+ assert_raise(NoMethodError) { only_sector_path(id: "1") }
end
def test_only_option_should_not_inherit
draw do
- scope :only => :show do
+ scope only: :show do
namespace :only do
- resources :sectors, :only => :index do
+ resources :sectors, only: :index do
resources :companies
resource :leader
end
@@ -2647,38 +2735,38 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
- get '/only/sectors/1/companies/2'
- assert_equal 'only/companies#show', @response.body
- assert_equal '/only/sectors/1/companies/2', only_sector_company_path(:sector_id => '1', :id => '2')
+ get "/only/sectors/1/companies/2"
+ assert_equal "only/companies#show", @response.body
+ assert_equal "/only/sectors/1/companies/2", only_sector_company_path(sector_id: "1", id: "2")
- get '/only/sectors/1/leader'
- assert_equal 'only/leaders#show', @response.body
- assert_equal '/only/sectors/1/leader', only_sector_leader_path(:sector_id => '1')
+ get "/only/sectors/1/leader"
+ assert_equal "only/leaders#show", @response.body
+ assert_equal "/only/sectors/1/leader", only_sector_leader_path(sector_id: "1")
end
def test_except_option_should_override_scope
draw do
- scope :except => :index do
+ scope except: :index do
namespace :except do
- resources :sectors, :except => [:show, :update, :destroy]
+ resources :sectors, except: [:show, :update, :destroy]
end
end
end
- get '/except/sectors'
- assert_equal 'except/sectors#index', @response.body
- assert_equal '/except/sectors', except_sectors_path
+ get "/except/sectors"
+ assert_equal "except/sectors#index", @response.body
+ assert_equal "/except/sectors", except_sectors_path
- get '/except/sectors/1'
- assert_equal 'Not Found', @response.body
- assert_raise(NoMethodError) { except_sector_path(:id => '1') }
+ get "/except/sectors/1"
+ assert_equal "Not Found", @response.body
+ assert_raise(NoMethodError) { except_sector_path(id: "1") }
end
def test_except_option_should_not_inherit
draw do
- scope :except => :index do
+ scope except: :index do
namespace :except do
- resources :sectors, :except => [:show, :update, :destroy] do
+ resources :sectors, except: [:show, :update, :destroy] do
resources :companies
resource :leader
end
@@ -2686,62 +2774,62 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
- get '/except/sectors/1/companies/2'
- assert_equal 'except/companies#show', @response.body
- assert_equal '/except/sectors/1/companies/2', except_sector_company_path(:sector_id => '1', :id => '2')
+ get "/except/sectors/1/companies/2"
+ assert_equal "except/companies#show", @response.body
+ assert_equal "/except/sectors/1/companies/2", except_sector_company_path(sector_id: "1", id: "2")
- get '/except/sectors/1/leader'
- assert_equal 'except/leaders#show', @response.body
- assert_equal '/except/sectors/1/leader', except_sector_leader_path(:sector_id => '1')
+ get "/except/sectors/1/leader"
+ assert_equal "except/leaders#show", @response.body
+ assert_equal "/except/sectors/1/leader", except_sector_leader_path(sector_id: "1")
end
def test_except_option_should_override_scoped_only
draw do
- scope :only => :show do
+ scope only: :show do
namespace :only do
- resources :sectors, :only => :index do
- resources :managers, :except => [:show, :update, :destroy]
+ resources :sectors, only: :index do
+ resources :managers, except: [:show, :update, :destroy]
end
end
end
end
- get '/only/sectors/1/managers'
- assert_equal 'only/managers#index', @response.body
- assert_equal '/only/sectors/1/managers', only_sector_managers_path(:sector_id => '1')
+ get "/only/sectors/1/managers"
+ assert_equal "only/managers#index", @response.body
+ assert_equal "/only/sectors/1/managers", only_sector_managers_path(sector_id: "1")
- get '/only/sectors/1/managers/2'
- assert_equal 'Not Found', @response.body
- assert_raise(NoMethodError) { only_sector_manager_path(:sector_id => '1', :id => '2') }
+ get "/only/sectors/1/managers/2"
+ assert_equal "Not Found", @response.body
+ assert_raise(NoMethodError) { only_sector_manager_path(sector_id: "1", id: "2") }
end
def test_only_option_should_override_scoped_except
draw do
- scope :except => :index do
+ scope except: :index do
namespace :except do
- resources :sectors, :except => [:show, :update, :destroy] do
- resources :managers, :only => :index
+ resources :sectors, except: [:show, :update, :destroy] do
+ resources :managers, only: :index
end
end
end
end
- get '/except/sectors/1/managers'
- assert_equal 'except/managers#index', @response.body
- assert_equal '/except/sectors/1/managers', except_sector_managers_path(:sector_id => '1')
+ get "/except/sectors/1/managers"
+ assert_equal "except/managers#index", @response.body
+ assert_equal "/except/sectors/1/managers", except_sector_managers_path(sector_id: "1")
- get '/except/sectors/1/managers/2'
- assert_equal 'Not Found', @response.body
- assert_raise(NoMethodError) { except_sector_manager_path(:sector_id => '1', :id => '2') }
+ get "/except/sectors/1/managers/2"
+ assert_equal "Not Found", @response.body
+ assert_raise(NoMethodError) { except_sector_manager_path(sector_id: "1", id: "2") }
end
def test_only_scope_should_override_parent_scope
draw do
- scope :only => :show do
+ scope only: :show do
namespace :only do
- resources :sectors, :only => :index do
+ resources :sectors, only: :index do
resources :companies do
- scope :only => :index do
+ scope only: :index do
resources :divisions
end
end
@@ -2750,22 +2838,22 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
- get '/only/sectors/1/companies/2/divisions'
- assert_equal 'only/divisions#index', @response.body
- assert_equal '/only/sectors/1/companies/2/divisions', only_sector_company_divisions_path(:sector_id => '1', :company_id => '2')
+ get "/only/sectors/1/companies/2/divisions"
+ assert_equal "only/divisions#index", @response.body
+ assert_equal "/only/sectors/1/companies/2/divisions", only_sector_company_divisions_path(sector_id: "1", company_id: "2")
- get '/only/sectors/1/companies/2/divisions/3'
- assert_equal 'Not Found', @response.body
- assert_raise(NoMethodError) { only_sector_company_division_path(:sector_id => '1', :company_id => '2', :id => '3') }
+ get "/only/sectors/1/companies/2/divisions/3"
+ assert_equal "Not Found", @response.body
+ assert_raise(NoMethodError) { only_sector_company_division_path(sector_id: "1", company_id: "2", id: "3") }
end
def test_except_scope_should_override_parent_scope
draw do
- scope :except => :index do
+ scope except: :index do
namespace :except do
- resources :sectors, :except => [:show, :update, :destroy] do
+ resources :sectors, except: [:show, :update, :destroy] do
resources :companies do
- scope :except => [:show, :update, :destroy] do
+ scope except: [:show, :update, :destroy] do
resources :divisions
end
end
@@ -2774,22 +2862,22 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
- get '/except/sectors/1/companies/2/divisions'
- assert_equal 'except/divisions#index', @response.body
- assert_equal '/except/sectors/1/companies/2/divisions', except_sector_company_divisions_path(:sector_id => '1', :company_id => '2')
+ get "/except/sectors/1/companies/2/divisions"
+ assert_equal "except/divisions#index", @response.body
+ assert_equal "/except/sectors/1/companies/2/divisions", except_sector_company_divisions_path(sector_id: "1", company_id: "2")
- get '/except/sectors/1/companies/2/divisions/3'
- assert_equal 'Not Found', @response.body
- assert_raise(NoMethodError) { except_sector_company_division_path(:sector_id => '1', :company_id => '2', :id => '3') }
+ get "/except/sectors/1/companies/2/divisions/3"
+ assert_equal "Not Found", @response.body
+ assert_raise(NoMethodError) { except_sector_company_division_path(sector_id: "1", company_id: "2", id: "3") }
end
def test_except_scope_should_override_parent_only_scope
draw do
- scope :only => :show do
+ scope only: :show do
namespace :only do
- resources :sectors, :only => :index do
+ resources :sectors, only: :index do
resources :companies do
- scope :except => [:show, :update, :destroy] do
+ scope except: [:show, :update, :destroy] do
resources :departments
end
end
@@ -2798,22 +2886,22 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
- get '/only/sectors/1/companies/2/departments'
- assert_equal 'only/departments#index', @response.body
- assert_equal '/only/sectors/1/companies/2/departments', only_sector_company_departments_path(:sector_id => '1', :company_id => '2')
+ get "/only/sectors/1/companies/2/departments"
+ assert_equal "only/departments#index", @response.body
+ assert_equal "/only/sectors/1/companies/2/departments", only_sector_company_departments_path(sector_id: "1", company_id: "2")
- get '/only/sectors/1/companies/2/departments/3'
- assert_equal 'Not Found', @response.body
- assert_raise(NoMethodError) { only_sector_company_department_path(:sector_id => '1', :company_id => '2', :id => '3') }
+ get "/only/sectors/1/companies/2/departments/3"
+ assert_equal "Not Found", @response.body
+ assert_raise(NoMethodError) { only_sector_company_department_path(sector_id: "1", company_id: "2", id: "3") }
end
def test_only_scope_should_override_parent_except_scope
draw do
- scope :except => :index do
+ scope except: :index do
namespace :except do
- resources :sectors, :except => [:show, :update, :destroy] do
+ resources :sectors, except: [:show, :update, :destroy] do
resources :companies do
- scope :only => :index do
+ scope only: :index do
resources :departments
end
end
@@ -2822,13 +2910,13 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
- get '/except/sectors/1/companies/2/departments'
- assert_equal 'except/departments#index', @response.body
- assert_equal '/except/sectors/1/companies/2/departments', except_sector_company_departments_path(:sector_id => '1', :company_id => '2')
+ get "/except/sectors/1/companies/2/departments"
+ assert_equal "except/departments#index", @response.body
+ assert_equal "/except/sectors/1/companies/2/departments", except_sector_company_departments_path(sector_id: "1", company_id: "2")
- get '/except/sectors/1/companies/2/departments/3'
- assert_equal 'Not Found', @response.body
- assert_raise(NoMethodError) { except_sector_company_department_path(:sector_id => '1', :company_id => '2', :id => '3') }
+ get "/except/sectors/1/companies/2/departments/3"
+ assert_equal "Not Found", @response.body
+ assert_raise(NoMethodError) { except_sector_company_department_path(sector_id: "1", company_id: "2", id: "3") }
end
def test_resources_are_not_pluralized
@@ -2838,30 +2926,30 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
- get '/transport/taxis'
- assert_equal 'transport/taxis#index', @response.body
- assert_equal '/transport/taxis', transport_taxis_path
+ get "/transport/taxis"
+ assert_equal "transport/taxis#index", @response.body
+ assert_equal "/transport/taxis", transport_taxis_path
- get '/transport/taxis/new'
- assert_equal 'transport/taxis#new', @response.body
- assert_equal '/transport/taxis/new', new_transport_taxi_path
+ get "/transport/taxis/new"
+ assert_equal "transport/taxis#new", @response.body
+ assert_equal "/transport/taxis/new", new_transport_taxi_path
- post '/transport/taxis'
- assert_equal 'transport/taxis#create', @response.body
+ post "/transport/taxis"
+ assert_equal "transport/taxis#create", @response.body
- get '/transport/taxis/1'
- assert_equal 'transport/taxis#show', @response.body
- assert_equal '/transport/taxis/1', transport_taxi_path(:id => '1')
+ get "/transport/taxis/1"
+ assert_equal "transport/taxis#show", @response.body
+ assert_equal "/transport/taxis/1", transport_taxi_path(id: "1")
- get '/transport/taxis/1/edit'
- assert_equal 'transport/taxis#edit', @response.body
- assert_equal '/transport/taxis/1/edit', edit_transport_taxi_path(:id => '1')
+ get "/transport/taxis/1/edit"
+ assert_equal "transport/taxis#edit", @response.body
+ assert_equal "/transport/taxis/1/edit", edit_transport_taxi_path(id: "1")
- put '/transport/taxis/1'
- assert_equal 'transport/taxis#update', @response.body
+ put "/transport/taxis/1"
+ assert_equal "transport/taxis#update", @response.body
- delete '/transport/taxis/1'
- assert_equal 'transport/taxis#destroy', @response.body
+ delete "/transport/taxis/1"
+ assert_equal "transport/taxis#destroy", @response.body
end
def test_singleton_resources_are_not_singularized
@@ -2871,169 +2959,169 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
- get '/medical/taxis/new'
- assert_equal 'medical/taxis#new', @response.body
- assert_equal '/medical/taxis/new', new_medical_taxis_path
+ get "/medical/taxis/new"
+ assert_equal "medical/taxis#new", @response.body
+ assert_equal "/medical/taxis/new", new_medical_taxis_path
- post '/medical/taxis'
- assert_equal 'medical/taxis#create', @response.body
+ post "/medical/taxis"
+ assert_equal "medical/taxis#create", @response.body
- get '/medical/taxis'
- assert_equal 'medical/taxis#show', @response.body
- assert_equal '/medical/taxis', medical_taxis_path
+ get "/medical/taxis"
+ assert_equal "medical/taxis#show", @response.body
+ assert_equal "/medical/taxis", medical_taxis_path
- get '/medical/taxis/edit'
- assert_equal 'medical/taxis#edit', @response.body
- assert_equal '/medical/taxis/edit', edit_medical_taxis_path
+ get "/medical/taxis/edit"
+ assert_equal "medical/taxis#edit", @response.body
+ assert_equal "/medical/taxis/edit", edit_medical_taxis_path
- put '/medical/taxis'
- assert_equal 'medical/taxis#update', @response.body
+ put "/medical/taxis"
+ assert_equal "medical/taxis#update", @response.body
- delete '/medical/taxis'
- assert_equal 'medical/taxis#destroy', @response.body
+ delete "/medical/taxis"
+ assert_equal "medical/taxis#destroy", @response.body
end
def test_greedy_resource_id_regexp_doesnt_match_edit_and_custom_action
draw do
- resources :sections, :id => /.+/ do
- get :preview, :on => :member
+ resources :sections, id: /.+/ do
+ get :preview, on: :member
end
end
- get '/sections/1/edit'
- assert_equal 'sections#edit', @response.body
- assert_equal '/sections/1/edit', edit_section_path(:id => '1')
+ get "/sections/1/edit"
+ assert_equal "sections#edit", @response.body
+ assert_equal "/sections/1/edit", edit_section_path(id: "1")
- get '/sections/1/preview'
- assert_equal 'sections#preview', @response.body
- assert_equal '/sections/1/preview', preview_section_path(:id => '1')
+ get "/sections/1/preview"
+ assert_equal "sections#preview", @response.body
+ assert_equal "/sections/1/preview", preview_section_path(id: "1")
end
def test_resource_constraints_are_pushed_to_scope
draw do
namespace :wiki do
- resources :articles, :id => /[^\/]+/ do
- resources :comments, :only => [:create, :new]
+ resources :articles, id: /[^\/]+/ do
+ resources :comments, only: [:create, :new]
end
end
end
- get '/wiki/articles/Ruby_on_Rails_3.0'
- assert_equal 'wiki/articles#show', @response.body
- assert_equal '/wiki/articles/Ruby_on_Rails_3.0', wiki_article_path(:id => 'Ruby_on_Rails_3.0')
+ get "/wiki/articles/Ruby_on_Rails_3.0"
+ assert_equal "wiki/articles#show", @response.body
+ assert_equal "/wiki/articles/Ruby_on_Rails_3.0", wiki_article_path(id: "Ruby_on_Rails_3.0")
- get '/wiki/articles/Ruby_on_Rails_3.0/comments/new'
- assert_equal 'wiki/comments#new', @response.body
- assert_equal '/wiki/articles/Ruby_on_Rails_3.0/comments/new', new_wiki_article_comment_path(:article_id => 'Ruby_on_Rails_3.0')
+ get "/wiki/articles/Ruby_on_Rails_3.0/comments/new"
+ assert_equal "wiki/comments#new", @response.body
+ assert_equal "/wiki/articles/Ruby_on_Rails_3.0/comments/new", new_wiki_article_comment_path(article_id: "Ruby_on_Rails_3.0")
- post '/wiki/articles/Ruby_on_Rails_3.0/comments'
- assert_equal 'wiki/comments#create', @response.body
- assert_equal '/wiki/articles/Ruby_on_Rails_3.0/comments', wiki_article_comments_path(:article_id => 'Ruby_on_Rails_3.0')
+ post "/wiki/articles/Ruby_on_Rails_3.0/comments"
+ assert_equal "wiki/comments#create", @response.body
+ assert_equal "/wiki/articles/Ruby_on_Rails_3.0/comments", wiki_article_comments_path(article_id: "Ruby_on_Rails_3.0")
end
def test_resources_path_can_be_a_symbol
draw do
- resources :wiki_pages, :path => :pages
- resource :wiki_account, :path => :my_account
+ resources :wiki_pages, path: :pages
+ resource :wiki_account, path: :my_account
end
- get '/pages'
- assert_equal 'wiki_pages#index', @response.body
- assert_equal '/pages', wiki_pages_path
+ get "/pages"
+ assert_equal "wiki_pages#index", @response.body
+ assert_equal "/pages", wiki_pages_path
- get '/pages/Ruby_on_Rails'
- assert_equal 'wiki_pages#show', @response.body
- assert_equal '/pages/Ruby_on_Rails', wiki_page_path(:id => 'Ruby_on_Rails')
+ get "/pages/Ruby_on_Rails"
+ assert_equal "wiki_pages#show", @response.body
+ assert_equal "/pages/Ruby_on_Rails", wiki_page_path(id: "Ruby_on_Rails")
- get '/my_account'
- assert_equal 'wiki_accounts#show', @response.body
- assert_equal '/my_account', wiki_account_path
+ get "/my_account"
+ assert_equal "wiki_accounts#show", @response.body
+ assert_equal "/my_account", wiki_account_path
end
def test_redirect_https
draw do
- get 'secure', :to => redirect("/secure/login")
+ get "secure", to: redirect("/secure/login")
end
with_https do
- get '/secure'
- verify_redirect 'https://www.example.com/secure/login'
+ get "/secure"
+ verify_redirect "https://www.example.com/secure/login"
end
end
def test_path_parameters_is_not_stale
draw do
- scope '/countries/:country', :constraints => lambda { |params, req| %w(all France).include?(params[:country]) } do
- get '/', :to => 'countries#index'
- get '/cities', :to => 'countries#cities'
+ scope "/countries/:country", constraints: lambda { |params, req| %w(all France).include?(params[:country]) } do
+ get "/", to: "countries#index"
+ get "/cities", to: "countries#cities"
end
- get '/countries/:country/(*other)', :to => redirect{ |params, req| params[:other] ? "/countries/all/#{params[:other]}" : '/countries/all' }
+ get "/countries/:country/(*other)", to: redirect { |params, req| params[:other] ? "/countries/all/#{params[:other]}" : "/countries/all" }
end
- get '/countries/France'
- assert_equal 'countries#index', @response.body
+ get "/countries/France"
+ assert_equal "countries#index", @response.body
- get '/countries/France/cities'
- assert_equal 'countries#cities', @response.body
+ get "/countries/France/cities"
+ assert_equal "countries#cities", @response.body
- get '/countries/UK'
- verify_redirect 'http://www.example.com/countries/all'
+ get "/countries/UK"
+ verify_redirect "http://www.example.com/countries/all"
- get '/countries/UK/cities'
- verify_redirect 'http://www.example.com/countries/all/cities'
+ get "/countries/UK/cities"
+ verify_redirect "http://www.example.com/countries/all/cities"
end
def test_constraints_block_not_carried_to_following_routes
draw do
- scope '/italians' do
- get '/writers', :to => 'italians#writers', :constraints => ::TestRoutingMapper::IpRestrictor
- get '/sculptors', :to => 'italians#sculptors'
- get '/painters/:painter', :to => 'italians#painters', :constraints => {:painter => /michelangelo/}
+ scope "/italians" do
+ get "/writers", to: "italians#writers", constraints: ::TestRoutingMapper::IpRestrictor
+ get "/sculptors", to: "italians#sculptors"
+ get "/painters/:painter", to: "italians#painters", constraints: { painter: /michelangelo/ }
end
end
- get '/italians/writers'
- assert_equal 'Not Found', @response.body
+ get "/italians/writers"
+ assert_equal "Not Found", @response.body
- get '/italians/sculptors'
- assert_equal 'italians#sculptors', @response.body
+ get "/italians/sculptors"
+ assert_equal "italians#sculptors", @response.body
- get '/italians/painters/botticelli'
- assert_equal 'Not Found', @response.body
+ get "/italians/painters/botticelli"
+ assert_equal "Not Found", @response.body
- get '/italians/painters/michelangelo'
- assert_equal 'italians#painters', @response.body
+ get "/italians/painters/michelangelo"
+ assert_equal "italians#painters", @response.body
end
def test_custom_resource_actions_defined_using_string
draw do
resources :customers do
resources :invoices do
- get "aged/:months", :on => :collection, :action => :aged, :as => :aged
+ get "aged/:months", on: :collection, action: :aged, as: :aged
end
- get "inactive", :on => :collection
- post "deactivate", :on => :member
- get "old", :on => :collection, :as => :stale
+ get "inactive", on: :collection
+ post "deactivate", on: :member
+ get "old", on: :collection, as: :stale
end
end
- get '/customers/inactive'
- assert_equal 'customers#inactive', @response.body
- assert_equal '/customers/inactive', inactive_customers_path
+ get "/customers/inactive"
+ assert_equal "customers#inactive", @response.body
+ assert_equal "/customers/inactive", inactive_customers_path
- post '/customers/1/deactivate'
- assert_equal 'customers#deactivate', @response.body
- assert_equal '/customers/1/deactivate', deactivate_customer_path(:id => '1')
+ post "/customers/1/deactivate"
+ assert_equal "customers#deactivate", @response.body
+ assert_equal "/customers/1/deactivate", deactivate_customer_path(id: "1")
- get '/customers/old'
- assert_equal 'customers#old', @response.body
- assert_equal '/customers/old', stale_customers_path
+ get "/customers/old"
+ assert_equal "customers#old", @response.body
+ assert_equal "/customers/old", stale_customers_path
- get '/customers/1/invoices/aged/3'
- assert_equal 'invoices#aged', @response.body
- assert_equal '/customers/1/invoices/aged/3', aged_customer_invoices_path(:customer_id => '1', :months => '3')
+ get "/customers/1/invoices/aged/3"
+ assert_equal "invoices#aged", @response.body
+ assert_equal "/customers/1/invoices/aged/3", aged_customer_invoices_path(customer_id: "1", months: "3")
end
def test_route_defined_in_resources_scope_level
@@ -3043,43 +3131,43 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
- get '/customers/1/export'
- assert_equal 'customers#export', @response.body
- assert_equal '/customers/1/export', customer_export_path(:customer_id => '1')
+ get "/customers/1/export"
+ assert_equal "customers#export", @response.body
+ assert_equal "/customers/1/export", customer_export_path(customer_id: "1")
end
def test_named_character_classes_in_regexp_constraints
draw do
- get '/purchases/:token/:filename',
- :to => 'purchases#fetch',
- :token => /[[:alnum:]]{10}/,
- :filename => /(.+)/,
- :as => :purchase
+ get "/purchases/:token/:filename",
+ to: "purchases#fetch",
+ token: /[[:alnum:]]{10}/,
+ filename: /(.+)/,
+ as: :purchase
end
- get '/purchases/315004be7e/Ruby_on_Rails_3.pdf'
- assert_equal 'purchases#fetch', @response.body
- assert_equal '/purchases/315004be7e/Ruby_on_Rails_3.pdf', purchase_path(:token => '315004be7e', :filename => 'Ruby_on_Rails_3.pdf')
+ get "/purchases/315004be7e/Ruby_on_Rails_3.pdf"
+ assert_equal "purchases#fetch", @response.body
+ assert_equal "/purchases/315004be7e/Ruby_on_Rails_3.pdf", purchase_path(token: "315004be7e", filename: "Ruby_on_Rails_3.pdf")
end
def test_nested_resource_constraints
draw do
- resources :lists, :id => /([A-Za-z0-9]{25})|default/ do
- resources :todos, :id => /\d+/
+ resources :lists, id: /([A-Za-z0-9]{25})|default/ do
+ resources :todos, id: /\d+/
end
end
- get '/lists/01234012340123401234fffff'
- assert_equal 'lists#show', @response.body
- assert_equal '/lists/01234012340123401234fffff', list_path(:id => '01234012340123401234fffff')
+ get "/lists/01234012340123401234fffff"
+ assert_equal "lists#show", @response.body
+ assert_equal "/lists/01234012340123401234fffff", list_path(id: "01234012340123401234fffff")
- get '/lists/01234012340123401234fffff/todos/1'
- assert_equal 'todos#show', @response.body
- assert_equal '/lists/01234012340123401234fffff/todos/1', list_todo_path(:list_id => '01234012340123401234fffff', :id => '1')
+ get "/lists/01234012340123401234fffff/todos/1"
+ assert_equal "todos#show", @response.body
+ assert_equal "/lists/01234012340123401234fffff/todos/1", list_todo_path(list_id: "01234012340123401234fffff", id: "1")
- get '/lists/2/todos/1'
- assert_equal 'Not Found', @response.body
- assert_raises(ActionController::UrlGenerationError){ list_todo_path(:list_id => '2', :id => '1') }
+ get "/lists/2/todos/1"
+ assert_equal "Not Found", @response.body
+ assert_raises(ActionController::UrlGenerationError) { list_todo_path(list_id: "2", id: "1") }
end
def test_redirect_argument_error
@@ -3096,73 +3184,67 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
after = has_named_route?(:hello)
end
- assert !before, "expected to not have named route :hello before route definition"
+ assert_not before, "expected to not have named route :hello before route definition"
assert after, "expected to have named route :hello after route definition"
end
def test_explicitly_avoiding_the_named_route
draw do
- scope :as => "routes" do
- get "/c/:id", :as => :collision, :to => "collision#show"
- get "/collision", :to => "collision#show"
- get "/no_collision", :to => "collision#show", :as => nil
+ scope as: "routes" do
+ get "/c/:id", as: :collision, to: "collision#show"
+ get "/collision", to: "collision#show"
+ get "/no_collision", to: "collision#show", as: nil
end
end
- assert !respond_to?(:routes_no_collision_path)
+ assert_not respond_to?(:routes_no_collision_path)
end
def test_controller_name_with_leading_slash_raise_error
assert_raise(ArgumentError) do
- draw { get '/feeds/:service', :to => '/feeds#show' }
+ draw { get "/feeds/:service", to: "/feeds#show" }
end
assert_raise(ArgumentError) do
- draw { get '/feeds/:service', :controller => '/feeds', :action => 'show' }
+ draw { get "/feeds/:service", controller: "/feeds", action: "show" }
end
assert_raise(ArgumentError) do
- draw { get '/api/feeds/:service', :to => '/api/feeds#show' }
+ draw { get "/api/feeds/:service", to: "/api/feeds#show" }
end
assert_raise(ArgumentError) do
- assert_deprecated do
- draw { controller("/feeds") { get '/feeds/:service', :to => :show } }
- end
- end
-
- assert_raise(ArgumentError) do
- draw { resources :feeds, :controller => '/feeds' }
+ draw { resources :feeds, controller: "/feeds" }
end
end
def test_invalid_route_name_raises_error
assert_raise(ArgumentError) do
- draw { get '/products', :to => 'products#index', :as => 'products ' }
+ draw { get "/products", to: "products#index", as: "products " }
end
assert_raise(ArgumentError) do
- draw { get '/products', :to => 'products#index', :as => ' products' }
+ draw { get "/products", to: "products#index", as: " products" }
end
assert_raise(ArgumentError) do
- draw { get '/products', :to => 'products#index', :as => 'products!' }
+ draw { get "/products", to: "products#index", as: "products!" }
end
assert_raise(ArgumentError) do
- draw { get '/products', :to => 'products#index', :as => 'products index' }
+ draw { get "/products", to: "products#index", as: "products index" }
end
assert_raise(ArgumentError) do
- draw { get '/products', :to => 'products#index', :as => '1products' }
+ draw { get "/products", to: "products#index", as: "1products" }
end
end
def test_duplicate_route_name_raises_error
assert_raise(ArgumentError) do
draw do
- get '/collision', :to => 'collision#show', :as => 'collision'
- get '/duplicate', :to => 'duplicate#show', :as => 'collision'
+ get "/collision", to: "collision#show", as: "collision"
+ get "/duplicate", to: "duplicate#show", as: "collision"
end
end
end
@@ -3171,15 +3253,15 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
assert_raise(ArgumentError) do
draw do
resources :collisions
- get '/collision', :to => 'collision#show', :as => 'collision'
+ get "/collision", to: "collision#show", as: "collision"
end
end
end
def test_nested_route_in_nested_resource
draw do
- resources :posts, :only => [:index, :show] do
- resources :comments, :except => :destroy do
+ resources :posts, only: [:index, :show] do
+ resources :comments, except: :destroy do
get "views" => "comments#views", :as => :views
end
end
@@ -3187,124 +3269,124 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
get "/posts/1/comments/2/views"
assert_equal "comments#views", @response.body
- assert_equal "/posts/1/comments/2/views", post_comment_views_path(:post_id => '1', :comment_id => '2')
+ assert_equal "/posts/1/comments/2/views", post_comment_views_path(post_id: "1", comment_id: "2")
end
def test_root_in_deeply_nested_scope
draw do
- resources :posts, :only => [:index, :show] do
+ resources :posts, only: [:index, :show] do
namespace :admin do
- root :to => "index#index"
+ root to: "index#index"
end
end
end
get "/posts/1/admin"
assert_equal "admin/index#index", @response.body
- assert_equal "/posts/1/admin", post_admin_root_path(:post_id => '1')
+ assert_equal "/posts/1/admin", post_admin_root_path(post_id: "1")
end
def test_custom_param
draw do
- resources :profiles, :param => :username do
- get :details, :on => :member
+ resources :profiles, param: :username do
+ get :details, on: :member
resources :messages
end
end
- get '/profiles/bob'
- assert_equal 'profiles#show', @response.body
- assert_equal 'bob', @request.params[:username]
+ get "/profiles/bob"
+ assert_equal "profiles#show", @response.body
+ assert_equal "bob", @request.params[:username]
- get '/profiles/bob/details'
- assert_equal 'bob', @request.params[:username]
+ get "/profiles/bob/details"
+ assert_equal "bob", @request.params[:username]
- get '/profiles/bob/messages/34'
- assert_equal 'bob', @request.params[:profile_username]
- assert_equal '34', @request.params[:id]
+ get "/profiles/bob/messages/34"
+ assert_equal "bob", @request.params[:profile_username]
+ assert_equal "34", @request.params[:id]
end
def test_custom_param_constraint
draw do
- resources :profiles, :param => :username, :username => /[a-z]+/ do
- get :details, :on => :member
+ resources :profiles, param: :username, username: /[a-z]+/ do
+ get :details, on: :member
resources :messages
end
end
- get '/profiles/bob1'
+ get "/profiles/bob1"
assert_equal 404, @response.status
- get '/profiles/bob1/details'
+ get "/profiles/bob1/details"
assert_equal 404, @response.status
- get '/profiles/bob1/messages/34'
+ get "/profiles/bob1/messages/34"
assert_equal 404, @response.status
end
def test_shallow_custom_param
draw do
resources :orders do
- constraints :download => /[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}/ do
- resources :downloads, :param => :download, :shallow => true
+ constraints download: /[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}/ do
+ resources :downloads, param: :download, shallow: true
end
end
end
- get '/downloads/0c0c0b68-d24b-11e1-a861-001ff3fffe6f.zip'
- assert_equal 'downloads#show', @response.body
- assert_equal '0c0c0b68-d24b-11e1-a861-001ff3fffe6f', @request.params[:download]
+ get "/downloads/0c0c0b68-d24b-11e1-a861-001ff3fffe6f.zip"
+ assert_equal "downloads#show", @response.body
+ assert_equal "0c0c0b68-d24b-11e1-a861-001ff3fffe6f", @request.params[:download]
end
def test_action_from_path_is_not_frozen
draw do
- get 'search' => 'search'
+ get "search" => "search"
end
- get '/search'
- assert !@request.params[:action].frozen?
+ get "/search"
+ assert_not_predicate @request.params[:action], :frozen?
end
def test_multiple_positional_args_with_the_same_name
draw do
- get '/downloads/:id/:id.tar' => 'downloads#show', as: :download, format: false
+ get "/downloads/:id/:id.tar" => "downloads#show", as: :download, format: false
end
expected_params = {
- controller: 'downloads',
- action: 'show',
- id: '1'
+ controller: "downloads",
+ action: "show",
+ id: "1"
}
- get '/downloads/1/1.tar'
- assert_equal 'downloads#show', @response.body
+ get "/downloads/1/1.tar"
+ assert_equal "downloads#show", @response.body
assert_equal expected_params, @request.path_parameters
- assert_equal '/downloads/1/1.tar', download_path('1')
- assert_equal '/downloads/1/1.tar', download_path('1', '1')
+ assert_equal "/downloads/1/1.tar", download_path("1")
+ assert_equal "/downloads/1/1.tar", download_path("1", "1")
end
def test_absolute_controller_namespace
draw do
namespace :foo do
- get '/', to: '/bar#index', as: 'root'
+ get "/", to: "/bar#index", as: "root"
end
end
- get '/foo'
- assert_equal 'bar#index', @response.body
- assert_equal '/foo', foo_root_path
+ get "/foo"
+ assert_equal "bar#index", @response.body
+ assert_equal "/foo", foo_root_path
end
def test_namespace_as_controller
draw do
namespace :foo do
- get '/', to: '/bar#index', as: 'root'
+ get "/", to: "/bar#index", as: "root"
end
end
- get '/foo'
- assert_equal 'bar#index', @response.body
- assert_equal '/foo', foo_root_path
+ get "/foo"
+ assert_equal "bar#index", @response.body
+ assert_equal "/foo", foo_root_path
end
def test_trailing_slash
@@ -3312,63 +3394,63 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
resources :streams
end
- get '/streams'
- assert @response.ok?, 'route without trailing slash should work'
+ get "/streams"
+ assert @response.ok?, "route without trailing slash should work"
- get '/streams/'
- assert @response.ok?, 'route with trailing slash should work'
+ get "/streams/"
+ assert @response.ok?, "route with trailing slash should work"
- get '/streams?foobar'
- assert @response.ok?, 'route without trailing slash and with QUERY_STRING should work'
+ get "/streams?foobar"
+ assert @response.ok?, "route without trailing slash and with QUERY_STRING should work"
- get '/streams/?foobar'
- assert @response.ok?, 'route with trailing slash and with QUERY_STRING should work'
+ get "/streams/?foobar"
+ assert @response.ok?, "route with trailing slash and with QUERY_STRING should work"
end
def test_route_with_dashes_in_path
draw do
- get '/contact-us', to: 'pages#contact_us'
+ get "/contact-us", to: "pages#contact_us"
end
- get '/contact-us'
- assert_equal 'pages#contact_us', @response.body
- assert_equal '/contact-us', contact_us_path
+ get "/contact-us"
+ assert_equal "pages#contact_us", @response.body
+ assert_equal "/contact-us", contact_us_path
end
def test_shorthand_route_with_dashes_in_path
draw do
- get '/about-us/index'
+ get "/about-us/index"
end
- get '/about-us/index'
- assert_equal 'about_us#index', @response.body
- assert_equal '/about-us/index', about_us_index_path
+ get "/about-us/index"
+ assert_equal "about_us#index", @response.body
+ assert_equal "/about-us/index", about_us_index_path
end
def test_resource_routes_with_dashes_in_path
draw do
resources :photos, only: [:show] do
- get 'user-favorites', on: :collection
- get 'preview-photo', on: :member
- get 'summary-text'
+ get "user-favorites", on: :collection
+ get "preview-photo", on: :member
+ get "summary-text"
end
end
- get '/photos/user-favorites'
- assert_equal 'photos#user_favorites', @response.body
- assert_equal '/photos/user-favorites', user_favorites_photos_path
+ get "/photos/user-favorites"
+ assert_equal "photos#user_favorites", @response.body
+ assert_equal "/photos/user-favorites", user_favorites_photos_path
- get '/photos/1/preview-photo'
- assert_equal 'photos#preview_photo', @response.body
- assert_equal '/photos/1/preview-photo', preview_photo_photo_path('1')
+ get "/photos/1/preview-photo"
+ assert_equal "photos#preview_photo", @response.body
+ assert_equal "/photos/1/preview-photo", preview_photo_photo_path("1")
- get '/photos/1/summary-text'
- assert_equal 'photos#summary_text', @response.body
- assert_equal '/photos/1/summary-text', photo_summary_text_path('1')
+ get "/photos/1/summary-text"
+ assert_equal "photos#summary_text", @response.body
+ assert_equal "/photos/1/summary-text", photo_summary_text_path("1")
- get '/photos/1'
- assert_equal 'photos#show', @response.body
- assert_equal '/photos/1', photo_path('1')
+ get "/photos/1"
+ assert_equal "photos#show", @response.body
+ assert_equal "/photos/1", photo_path("1")
end
def test_shallow_path_inside_namespace_is_not_added_twice
@@ -3382,208 +3464,305 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
- get '/admin/posts/1/comments'
- assert_equal 'admin/comments#index', @response.body
- assert_equal '/admin/posts/1/comments', admin_post_comments_path('1')
+ get "/admin/posts/1/comments"
+ assert_equal "admin/comments#index", @response.body
+ assert_equal "/admin/posts/1/comments", admin_post_comments_path("1")
end
def test_mix_string_to_controller_action
draw do
- get '/projects', controller: 'project_files',
- action: 'index',
- to: 'comments#index'
+ get "/projects", controller: "project_files",
+ action: "index",
+ to: "comments#index"
end
- get '/projects'
- assert_equal 'comments#index', @response.body
+ get "/projects"
+ assert_equal "comments#index", @response.body
end
def test_mix_string_to_controller
draw do
- get '/projects', controller: 'project_files',
- to: 'comments#index'
+ get "/projects", controller: "project_files",
+ to: "comments#index"
end
- get '/projects'
- assert_equal 'comments#index', @response.body
+ get "/projects"
+ assert_equal "comments#index", @response.body
end
def test_mix_string_to_action
draw do
- get '/projects', action: 'index',
- to: 'comments#index'
+ get "/projects", action: "index",
+ to: "comments#index"
end
- get '/projects'
- assert_equal 'comments#index', @response.body
+ get "/projects"
+ assert_equal "comments#index", @response.body
end
def test_shallow_path_and_prefix_are_not_added_to_non_shallow_routes
draw do
- scope shallow_path: 'projects', shallow_prefix: 'project' do
+ scope shallow_path: "projects", shallow_prefix: "project" do
resources :projects do
- resources :files, controller: 'project_files', shallow: true
+ resources :files, controller: "project_files", shallow: true
end
end
end
- get '/projects'
- assert_equal 'projects#index', @response.body
- assert_equal '/projects', projects_path
+ get "/projects"
+ assert_equal "projects#index", @response.body
+ assert_equal "/projects", projects_path
- get '/projects/new'
- assert_equal 'projects#new', @response.body
- assert_equal '/projects/new', new_project_path
+ get "/projects/new"
+ assert_equal "projects#new", @response.body
+ assert_equal "/projects/new", new_project_path
- post '/projects'
- assert_equal 'projects#create', @response.body
+ post "/projects"
+ assert_equal "projects#create", @response.body
- get '/projects/1'
- assert_equal 'projects#show', @response.body
- assert_equal '/projects/1', project_path('1')
+ get "/projects/1"
+ assert_equal "projects#show", @response.body
+ assert_equal "/projects/1", project_path("1")
- get '/projects/1/edit'
- assert_equal 'projects#edit', @response.body
- assert_equal '/projects/1/edit', edit_project_path('1')
+ get "/projects/1/edit"
+ assert_equal "projects#edit", @response.body
+ assert_equal "/projects/1/edit", edit_project_path("1")
- patch '/projects/1'
- assert_equal 'projects#update', @response.body
+ patch "/projects/1"
+ assert_equal "projects#update", @response.body
- delete '/projects/1'
- assert_equal 'projects#destroy', @response.body
+ delete "/projects/1"
+ assert_equal "projects#destroy", @response.body
- get '/projects/1/files'
- assert_equal 'project_files#index', @response.body
- assert_equal '/projects/1/files', project_files_path('1')
+ get "/projects/1/files"
+ assert_equal "project_files#index", @response.body
+ assert_equal "/projects/1/files", project_files_path("1")
- get '/projects/1/files/new'
- assert_equal 'project_files#new', @response.body
- assert_equal '/projects/1/files/new', new_project_file_path('1')
+ get "/projects/1/files/new"
+ assert_equal "project_files#new", @response.body
+ assert_equal "/projects/1/files/new", new_project_file_path("1")
- post '/projects/1/files'
- assert_equal 'project_files#create', @response.body
+ post "/projects/1/files"
+ assert_equal "project_files#create", @response.body
- get '/projects/files/2'
- assert_equal 'project_files#show', @response.body
- assert_equal '/projects/files/2', project_file_path('2')
+ get "/projects/files/2"
+ assert_equal "project_files#show", @response.body
+ assert_equal "/projects/files/2", project_file_path("2")
- get '/projects/files/2/edit'
- assert_equal 'project_files#edit', @response.body
- assert_equal '/projects/files/2/edit', edit_project_file_path('2')
+ get "/projects/files/2/edit"
+ assert_equal "project_files#edit", @response.body
+ assert_equal "/projects/files/2/edit", edit_project_file_path("2")
- patch '/projects/files/2'
- assert_equal 'project_files#update', @response.body
+ patch "/projects/files/2"
+ assert_equal "project_files#update", @response.body
- delete '/projects/files/2'
- assert_equal 'project_files#destroy', @response.body
+ delete "/projects/files/2"
+ assert_equal "project_files#destroy", @response.body
end
def test_scope_path_is_copied_to_shallow_path
draw do
- scope path: 'foo' do
+ scope path: "foo" do
resources :posts do
resources :comments, shallow: true
end
end
end
- assert_equal '/foo/comments/1', comment_path('1')
+ assert_equal "/foo/comments/1", comment_path("1")
end
def test_scope_as_is_copied_to_shallow_prefix
draw do
- scope as: 'foo' do
+ scope as: "foo" do
resources :posts do
resources :comments, shallow: true
end
end
end
- assert_equal '/comments/1', foo_comment_path('1')
+ assert_equal "/comments/1", foo_comment_path("1")
end
def test_scope_shallow_prefix_is_not_overwritten_by_as
draw do
- scope as: 'foo', shallow_prefix: 'bar' do
+ scope as: "foo", shallow_prefix: "bar" do
resources :posts do
resources :comments, shallow: true
end
end
end
- assert_equal '/comments/1', bar_comment_path('1')
+ assert_equal "/comments/1", bar_comment_path("1")
end
def test_scope_shallow_path_is_not_overwritten_by_path
draw do
- scope path: 'foo', shallow_path: 'bar' do
+ scope path: "foo", shallow_path: "bar" do
resources :posts do
resources :comments, shallow: true
end
end
end
- assert_equal '/bar/comments/1', comment_path('1')
+ assert_equal "/bar/comments/1", comment_path("1")
end
def test_resource_where_as_is_empty
draw do
- resource :post, as: ''
+ resource :post, as: ""
- scope 'post', as: 'post' do
- resource :comment, as: ''
+ scope "post", as: "post" do
+ resource :comment, as: ""
end
end
- assert_equal '/post/new', new_path
- assert_equal '/post/comment/new', new_post_path
+ assert_equal "/post/new", new_path
+ assert_equal "/post/comment/new", new_post_path
end
def test_resources_where_as_is_empty
draw do
- resources :posts, as: ''
+ resources :posts, as: ""
- scope 'posts', as: 'posts' do
- resources :comments, as: ''
+ scope "posts", as: "posts" do
+ resources :comments, as: ""
end
end
- assert_equal '/posts/new', new_path
- assert_equal '/posts/comments/new', new_posts_path
+ assert_equal "/posts/new", new_path
+ assert_equal "/posts/comments/new", new_posts_path
end
def test_scope_where_as_is_empty
draw do
- scope 'post', as: '' do
+ scope "post", as: "" do
resource :user
resources :comments
end
end
- assert_equal '/post/user/new', new_user_path
- assert_equal '/post/comments/new', new_comment_path
+ assert_equal "/post/user/new", new_user_path
+ assert_equal "/post/comments/new", new_comment_path
end
def test_head_fetch_with_mount_on_root
draw do
- get '/home' => 'test#index'
- mount lambda { |env| [200, {}, [env['REQUEST_METHOD']]] }, at: '/'
+ get "/home" => "test#index"
+ mount lambda { |env| [200, {}, [env["REQUEST_METHOD"]]] }, at: "/"
end
# HEAD request should match `get /home` rather than the
# lower-precedence Rack app mounted at `/`.
- head '/home'
+ head "/home"
assert_response :ok
- assert_equal 'test#index', @response.body
+ assert_equal "test#index", @response.body
# But the Rack app can still respond to its own HEAD requests.
- head '/foobar'
+ head "/foobar"
assert_response :ok
- assert_equal 'HEAD', @response.body
+ assert_equal "HEAD", @response.body
+ end
+
+ def test_passing_action_parameters_to_url_helpers_raises_error_if_parameters_are_not_permitted
+ draw do
+ root to: "projects#index"
+ end
+ params = ActionController::Parameters.new(id: "1")
+
+ assert_raises ActionController::UnfilteredParameters do
+ root_path(params)
+ end
+ end
+
+ def test_passing_action_parameters_to_url_helpers_is_allowed_if_parameters_are_permitted
+ draw do
+ root to: "projects#index"
+ end
+ params = ActionController::Parameters.new(id: "1")
+ params.permit!
+
+ assert_equal "/?id=1", root_path(params)
+ end
+
+ def test_dynamic_controller_segments_are_deprecated
+ assert_deprecated do
+ draw do
+ get "/:controller", action: "index"
+ end
+ end
+ end
+
+ def test_dynamic_action_segments_are_deprecated
+ assert_deprecated do
+ draw do
+ get "/pages/:action", controller: "pages"
+ end
+ end
+ end
+
+ def test_multiple_roots
+ draw do
+ namespace :foo do
+ root "pages#index", constraints: { host: "www.example.com" }
+ root "admin/pages#index", constraints: { host: "admin.example.com" }
+ end
+
+ root "pages#index", constraints: { host: "www.example.com" }
+ root "admin/pages#index", constraints: { host: "admin.example.com" }
+ end
+
+ get "http://www.example.com/foo"
+ assert_equal "foo/pages#index", @response.body
+
+ get "http://admin.example.com/foo"
+ assert_equal "foo/admin/pages#index", @response.body
+
+ get "http://www.example.com/"
+ assert_equal "pages#index", @response.body
+
+ get "http://admin.example.com/"
+ assert_equal "admin/pages#index", @response.body
+ end
+
+ def test_multiple_namespaced_roots
+ draw do
+ namespace :foo do
+ root "test#index"
+ end
+
+ root "test#index"
+
+ namespace :bar do
+ root "test#index"
+ end
+ end
+
+ assert_equal "/foo", foo_root_path
+ assert_equal "/", root_path
+ assert_equal "/bar", bar_root_path
+ end
+
+ def test_nested_routes_under_format_resource
+ draw do
+ resources :formats do
+ resources :items
+ end
+ end
+
+ get "/formats/1/items.json"
+ assert_equal 200, @response.status
+ assert_equal "items#index", @response.body
+ assert_equal "/formats/1/items.json", format_items_path(1, :json)
+
+ get "/formats/1/items/2.json"
+ assert_equal 200, @response.status
+ assert_equal "items#show", @response.body
+ assert_equal "/formats/1/items/2.json", format_item_path(1, 2, :json)
end
private
def draw(&block)
self.class.stub_controllers do |routes|
- routes.default_url_options = { host: 'www.example.com' }
+ routes.default_url_options = { host: "www.example.com" }
routes.draw(&block)
@app = RoutedRackApp.new routes
end
@@ -3609,9 +3788,9 @@ private
https!(old_https)
end
- def verify_redirect(url, status=301)
+ def verify_redirect(url, status = 301)
assert_equal status, @response.status
- assert_equal url, @response.headers['Location']
+ assert_equal url, @response.headers["Location"]
assert_equal expected_redirect_body(url), @response.body
end
@@ -3621,7 +3800,7 @@ private
end
class TestAltApp < ActionDispatch::IntegrationTest
- class AltRequest
+ class AltRequest < ActionDispatch::Request
attr_accessor :path_parameters, :path_info, :script_name
attr_reader :env
@@ -3630,6 +3809,7 @@ class TestAltApp < ActionDispatch::IntegrationTest
@env = env
@path_info = "/"
@script_name = ""
+ super
end
def request_method
@@ -3647,13 +3827,13 @@ class TestAltApp < ActionDispatch::IntegrationTest
class XHeader
def call(env)
- [200, {"Content-Type" => "text/html"}, ["XHeader"]]
+ [200, { "Content-Type" => "text/html" }, ["XHeader"]]
end
end
class AltApp
def call(env)
- [200, {"Content-Type" => "text/html"}, ["Alternative App"]]
+ [200, { "Content-Type" => "text/html" }, ["Alternative App"]]
end
end
@@ -3663,7 +3843,7 @@ class TestAltApp < ActionDispatch::IntegrationTest
end
}.new
AltRoutes.draw do
- get "/" => TestAltApp::XHeader.new, :constraints => {:x_header => /HEADER/}
+ get "/" => TestAltApp::XHeader.new, :constraints => { x_header: /HEADER/ }
get "/" => TestAltApp::AltApp.new
end
@@ -3691,7 +3871,7 @@ end
class TestAppendingRoutes < ActionDispatch::IntegrationTest
def simple_app(resp)
- lambda { |e| [ 200, { 'Content-Type' => 'text/plain' }, [resp] ] }
+ lambda { |e| [ 200, { "Content-Type" => "text/plain" }, [resp] ] }
end
def setup
@@ -3699,28 +3879,28 @@ class TestAppendingRoutes < ActionDispatch::IntegrationTest
s = self
routes = ActionDispatch::Routing::RouteSet.new
routes.append do
- get '/hello' => s.simple_app('fail')
- get '/goodbye' => s.simple_app('goodbye')
+ get "/hello" => s.simple_app("fail")
+ get "/goodbye" => s.simple_app("goodbye")
end
routes.draw do
- get '/hello' => s.simple_app('hello')
+ get "/hello" => s.simple_app("hello")
end
@app = self.class.build_app routes
end
def test_goodbye_should_be_available
- get '/goodbye'
- assert_equal 'goodbye', @response.body
+ get "/goodbye"
+ assert_equal "goodbye", @response.body
end
def test_hello_should_not_be_overwritten
- get '/hello'
- assert_equal 'hello', @response.body
+ get "/hello"
+ assert_equal "hello", @response.body
end
def test_missing_routes_are_still_missing
- get '/random'
+ get "/random"
assert_equal 404, @response.status
end
end
@@ -3729,7 +3909,7 @@ class TestNamespaceWithControllerOption < ActionDispatch::IntegrationTest
module ::Admin
class StorageFilesController < ActionController::Base
def index
- render :text => "admin/storage_files#index"
+ render plain: "admin/storage_files#index"
end
end
end
@@ -3743,7 +3923,7 @@ class TestNamespaceWithControllerOption < ActionDispatch::IntegrationTest
def test_missing_controller
ex = assert_raises(ArgumentError) {
draw do
- get '/foo/bar', :action => :index
+ get "/foo/bar", action: :index
end
}
assert_match(/Missing :controller/, ex.message)
@@ -3752,7 +3932,7 @@ class TestNamespaceWithControllerOption < ActionDispatch::IntegrationTest
def test_missing_controller_with_to
ex = assert_raises(ArgumentError) {
draw do
- get '/foo/bar', :to => 'foo'
+ get "/foo/bar", to: "foo"
end
}
assert_match(/Missing :controller/, ex.message)
@@ -3761,7 +3941,7 @@ class TestNamespaceWithControllerOption < ActionDispatch::IntegrationTest
def test_missing_action_on_hash
ex = assert_raises(ArgumentError) {
draw do
- get '/foo/bar', :to => 'foo#'
+ get "/foo/bar", to: "foo#"
end
}
assert_match(/Missing :action/, ex.message)
@@ -3770,20 +3950,20 @@ class TestNamespaceWithControllerOption < ActionDispatch::IntegrationTest
def test_valid_controller_options_inside_namespace
draw do
namespace :admin do
- resources :storage_files, :controller => "storage_files"
+ resources :storage_files, controller: "storage_files"
end
end
- get '/admin/storage_files'
+ get "/admin/storage_files"
assert_equal "admin/storage_files#index", @response.body
end
def test_resources_with_valid_namespaced_controller_option
draw do
- resources :storage_files, :controller => 'admin/storage_files'
+ resources :storage_files, controller: "admin/storage_files"
end
- get '/storage_files'
+ get "/storage_files"
assert_equal "admin/storage_files#index", @response.body
end
@@ -3791,7 +3971,7 @@ class TestNamespaceWithControllerOption < ActionDispatch::IntegrationTest
e = assert_raise(ArgumentError) do
draw do
namespace :admin do
- resources :storage_files, :controller => "StorageFiles"
+ resources :storage_files, controller: "StorageFiles"
end
end
end
@@ -3802,7 +3982,7 @@ class TestNamespaceWithControllerOption < ActionDispatch::IntegrationTest
def test_warn_with_ruby_constant_syntax_namespaced_controller_option
e = assert_raise(ArgumentError) do
draw do
- resources :storage_files, :controller => 'Admin::StorageFiles'
+ resources :storage_files, controller: "Admin::StorageFiles"
end
end
@@ -3812,7 +3992,7 @@ class TestNamespaceWithControllerOption < ActionDispatch::IntegrationTest
def test_warn_with_ruby_constant_syntax_no_colons
e = assert_raise(ArgumentError) do
draw do
- resources :storage_files, :controller => 'Admin'
+ resources :storage_files, controller: "Admin"
end
end
@@ -3824,13 +4004,13 @@ class TestDefaultScope < ActionDispatch::IntegrationTest
module ::Blog
class PostsController < ActionController::Base
def index
- render :text => "blog/posts#index"
+ render plain: "blog/posts#index"
end
end
end
DefaultScopeRoutes = ActionDispatch::Routing::RouteSet.new
- DefaultScopeRoutes.default_scope = {:module => :blog}
+ DefaultScopeRoutes.default_scope = { module: :blog }
DefaultScopeRoutes.draw do
resources :posts
end
@@ -3844,7 +4024,7 @@ class TestDefaultScope < ActionDispatch::IntegrationTest
include DefaultScopeRoutes.url_helpers
def test_default_scope
- get '/posts'
+ get "/posts"
assert_equal "blog/posts#index", @response.body
end
end
@@ -3860,7 +4040,7 @@ class TestHttpMethods < ActionDispatch::IntegrationTest
RFC5789 = %w(PATCH)
def simple_app(response)
- lambda { |env| [ 200, { 'Content-Type' => 'text/plain' }, [response] ] }
+ lambda { |env| [ 200, { "Content-Type" => "text/plain" }, [response] ] }
end
attr_reader :app
@@ -3872,14 +4052,14 @@ class TestHttpMethods < ActionDispatch::IntegrationTest
routes.draw do
(RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789).each do |method|
- match '/' => s.simple_app(method), :via => method.underscore.to_sym
+ match "/" => s.simple_app(method), :via => method.underscore.to_sym
end
end
end
(RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789).each do |method|
test "request method #{method.underscore} can be matched" do
- get '/', headers: { 'REQUEST_METHOD' => method }
+ get "/", headers: { "REQUEST_METHOD" => method }
assert_equal method, @response.body
end
end
@@ -3888,14 +4068,14 @@ end
class TestUriPathEscaping < ActionDispatch::IntegrationTest
Routes = ActionDispatch::Routing::RouteSet.new.tap do |app|
app.draw do
- get '/:segment' => lambda { |env|
- path_params = env['action_dispatch.request.path_parameters']
- [200, { 'Content-Type' => 'text/plain' }, [path_params[:segment]]]
+ get "/:segment" => lambda { |env|
+ path_params = env["action_dispatch.request.path_parameters"]
+ [200, { "Content-Type" => "text/plain" }, [path_params[:segment]]]
}, :as => :segment
- get '/*splat' => lambda { |env|
- path_params = env['action_dispatch.request.path_parameters']
- [200, { 'Content-Type' => 'text/plain' }, [path_params[:splat]]]
+ get "/*splat" => lambda { |env|
+ path_params = env["action_dispatch.request.path_parameters"]
+ [200, { "Content-Type" => "text/plain" }, [path_params[:splat]]]
}, :as => :splat
end
end
@@ -3904,22 +4084,22 @@ class TestUriPathEscaping < ActionDispatch::IntegrationTest
APP = build_app Routes
def app; APP end
- test 'escapes slash in generated path segment' do
- assert_equal '/a%20b%2Fc+d', segment_path(:segment => 'a b/c+d')
+ test "escapes slash in generated path segment" do
+ assert_equal "/a%20b%2Fc+d", segment_path(segment: "a b/c+d")
end
- test 'unescapes recognized path segment' do
- get '/a%20b%2Fc+d'
- assert_equal 'a b/c+d', @response.body
+ test "unescapes recognized path segment" do
+ get "/a%20b%2Fc+d"
+ assert_equal "a b/c+d", @response.body
end
- test 'does not escape slash in generated path splat' do
- assert_equal '/a%20b/c+d', splat_path(:splat => 'a b/c+d')
+ test "does not escape slash in generated path splat" do
+ assert_equal "/a%20b/c+d", splat_path(splat: "a b/c+d")
end
- test 'unescapes recognized path splat' do
- get '/a%20b/c+d'
- assert_equal 'a b/c+d', @response.body
+ test "unescapes recognized path splat" do
+ get "/a%20b/c+d"
+ assert_equal "a b/c+d", @response.body
end
end
@@ -3927,7 +4107,7 @@ class TestUnicodePaths < ActionDispatch::IntegrationTest
Routes = ActionDispatch::Routing::RouteSet.new.tap do |app|
app.draw do
get "/ほげ" => lambda { |env|
- [200, { 'Content-Type' => 'text/plain' }, []]
+ [200, { "Content-Type" => "text/plain" }, []]
}, :as => :unicode_path
end
end
@@ -3936,23 +4116,13 @@ class TestUnicodePaths < ActionDispatch::IntegrationTest
APP = build_app Routes
def app; APP end
- test 'recognizes unicode path' do
+ test "recognizes unicode path" do
get "/#{Rack::Utils.escape("ほげ")}"
assert_equal "200", @response.code
end
end
class TestMultipleNestedController < ActionDispatch::IntegrationTest
- module ::Foo
- module Bar
- class BazController < ActionController::Base
- def index
- render :inline => "<%= url_for :controller => '/pooh', :action => 'index' %>"
- end
- end
- end
- end
-
Routes = ActionDispatch::Routing::RouteSet.new.tap do |app|
app.draw do
namespace :foo do
@@ -3964,7 +4134,18 @@ class TestMultipleNestedController < ActionDispatch::IntegrationTest
end
end
- include Routes.url_helpers
+ module ::Foo
+ module Bar
+ class BazController < ActionController::Base
+ include Routes.url_helpers
+
+ def index
+ render inline: "<%= url_for :controller => '/pooh', :action => 'index' %>"
+ end
+ end
+ end
+ end
+
APP = build_app Routes
def app; APP end
@@ -3977,7 +4158,7 @@ end
class TestTildeAndMinusPaths < ActionDispatch::IntegrationTest
Routes = ActionDispatch::Routing::RouteSet.new.tap do |app|
app.draw do
- ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] }
+ ok = lambda { |env| [200, { "Content-Type" => "text/plain" }, []] }
get "/~user" => ok
get "/young-and-fine" => ok
@@ -3988,25 +4169,24 @@ class TestTildeAndMinusPaths < ActionDispatch::IntegrationTest
APP = build_app Routes
def app; APP end
- test 'recognizes tilde path' do
+ test "recognizes tilde path" do
get "/~user"
assert_equal "200", @response.code
end
- test 'recognizes minus path' do
+ test "recognizes minus path" do
get "/young-and-fine"
assert_equal "200", @response.code
end
-
end
class TestRedirectInterpolation < ActionDispatch::IntegrationTest
Routes = ActionDispatch::Routing::RouteSet.new.tap do |app|
app.draw do
- ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] }
+ ok = lambda { |env| [200, { "Content-Type" => "text/plain" }, []] }
get "/foo/:id" => redirect("/foo/bar/%{id}")
- get "/bar/:id" => redirect(:path => "/foo/bar/%{id}")
+ get "/bar/:id" => redirect(path: "/foo/bar/%{id}")
get "/baz/:id" => redirect("/baz?id=%{id}&foo=?&bar=1#id-%{id}")
get "/foo/bar/:id" => ok
get "/baz" => ok
@@ -4035,9 +4215,9 @@ class TestRedirectInterpolation < ActionDispatch::IntegrationTest
end
private
- def verify_redirect(url, status=301)
+ def verify_redirect(url, status = 301)
assert_equal status, @response.status
- assert_equal url, @response.headers['Location']
+ assert_equal url, @response.headers["Location"]
assert_equal expected_redirect_body(url), @response.body
end
@@ -4049,9 +4229,9 @@ end
class TestConstraintsAccessingParameters < ActionDispatch::IntegrationTest
Routes = ActionDispatch::Routing::RouteSet.new.tap do |app|
app.draw do
- ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] }
+ ok = lambda { |env| [200, { "Content-Type" => "text/plain" }, []] }
- get "/:foo" => ok, :constraints => lambda { |r| r.params[:foo] == 'foo' }
+ get "/:foo" => ok, :constraints => lambda { |r| r.params[:foo] == "foo" }
get "/:bar" => ok
end
end
@@ -4061,7 +4241,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
@@ -4069,21 +4249,21 @@ end
class TestGlobRoutingMapper < ActionDispatch::IntegrationTest
Routes = ActionDispatch::Routing::RouteSet.new.tap do |app|
app.draw do
- ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] }
+ ok = lambda { |env| [200, { "Content-Type" => "text/plain" }, []] }
- get "/*id" => redirect("/not_cars"), :constraints => {id: /dummy/}
+ get "/*id" => redirect("/not_cars"), :constraints => { id: /dummy/ }
get "/cars" => ok
end
end
- #include Routes.url_helpers
+ # include Routes.url_helpers
APP = build_app Routes
def app; APP end
def test_glob_constraint
get "/dummy"
assert_equal "301", @response.code
- assert_equal "/not_cars", @response.header['Location'].match('/[^/]+$')[0]
+ assert_equal "/not_cars", @response.header["Location"].match("/[^/]+$")[0]
end
def test_glob_constraint_skip_route
@@ -4099,13 +4279,17 @@ end
class TestOptimizedNamedRoutes < ActionDispatch::IntegrationTest
Routes = ActionDispatch::Routing::RouteSet.new.tap do |app|
app.draw do
- ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] }
- get '/foo' => ok, as: :foo
- get '/post(/:action(/:id))' => ok, as: :posts
- get '/:foo/:foo_type/bars/:id' => ok, as: :bar
- get '/projects/:id.:format' => ok, as: :project
- get '/pages/:id' => ok, as: :page
- get '/wiki/*page' => ok, as: :wiki
+ ok = lambda { |env| [200, { "Content-Type" => "text/plain" }, []] }
+ get "/foo" => ok, as: :foo
+
+ ActiveSupport::Deprecation.silence do
+ get "/post(/:action(/:id))" => ok, as: :posts
+ end
+
+ get "/:foo/:foo_type/bars/:id" => ok, as: :bar
+ get "/projects/:id.:format" => ok, as: :project
+ get "/pages/:id" => ok, as: :page
+ get "/wiki/*page" => ok, as: :wiki
end
end
@@ -4113,72 +4297,72 @@ class TestOptimizedNamedRoutes < ActionDispatch::IntegrationTest
APP = build_app Routes
def app; APP end
- test 'enabled when not mounted and default_url_options is empty' do
- assert Routes.url_helpers.optimize_routes_generation?
+ test "enabled when not mounted and default_url_options is empty" do
+ assert_predicate Routes.url_helpers, :optimize_routes_generation?
end
- test 'named route called as singleton method' do
- assert_equal '/foo', Routes.url_helpers.foo_path
+ test "named route called as singleton method" do
+ assert_equal "/foo", Routes.url_helpers.foo_path
end
- test 'named route called on included module' do
- assert_equal '/foo', foo_path
+ test "named route called on included module" do
+ assert_equal "/foo", foo_path
end
- test 'nested optional segments are removed' do
- assert_equal '/post', Routes.url_helpers.posts_path
- assert_equal '/post', posts_path
+ test "nested optional segments are removed" do
+ assert_equal "/post", Routes.url_helpers.posts_path
+ assert_equal "/post", posts_path
end
- test 'segments with same prefix are replaced correctly' do
- assert_equal '/foo/baz/bars/1', Routes.url_helpers.bar_path('foo', 'baz', '1')
- assert_equal '/foo/baz/bars/1', bar_path('foo', 'baz', '1')
+ test "segments with same prefix are replaced correctly" do
+ assert_equal "/foo/baz/bars/1", Routes.url_helpers.bar_path("foo", "baz", "1")
+ assert_equal "/foo/baz/bars/1", bar_path("foo", "baz", "1")
end
- test 'segments separated with a period are replaced correctly' do
- assert_equal '/projects/1.json', Routes.url_helpers.project_path(1, :json)
- assert_equal '/projects/1.json', project_path(1, :json)
+ test "segments separated with a period are replaced correctly" do
+ assert_equal "/projects/1.json", Routes.url_helpers.project_path(1, :json)
+ assert_equal "/projects/1.json", project_path(1, :json)
end
- test 'segments with question marks are escaped' do
- assert_equal '/pages/foo%3Fbar', Routes.url_helpers.page_path('foo?bar')
- assert_equal '/pages/foo%3Fbar', page_path('foo?bar')
+ test "segments with question marks are escaped" do
+ assert_equal "/pages/foo%3Fbar", Routes.url_helpers.page_path("foo?bar")
+ assert_equal "/pages/foo%3Fbar", page_path("foo?bar")
end
- test 'segments with slashes are escaped' do
- assert_equal '/pages/foo%2Fbar', Routes.url_helpers.page_path('foo/bar')
- assert_equal '/pages/foo%2Fbar', page_path('foo/bar')
+ test "segments with slashes are escaped" do
+ assert_equal "/pages/foo%2Fbar", Routes.url_helpers.page_path("foo/bar")
+ assert_equal "/pages/foo%2Fbar", page_path("foo/bar")
end
- test 'glob segments with question marks are escaped' do
- assert_equal '/wiki/foo%3Fbar', Routes.url_helpers.wiki_path('foo?bar')
- assert_equal '/wiki/foo%3Fbar', wiki_path('foo?bar')
+ test "glob segments with question marks are escaped" do
+ assert_equal "/wiki/foo%3Fbar", Routes.url_helpers.wiki_path("foo?bar")
+ assert_equal "/wiki/foo%3Fbar", wiki_path("foo?bar")
end
- test 'glob segments with slashes are not escaped' do
- assert_equal '/wiki/foo/bar', Routes.url_helpers.wiki_path('foo/bar')
- assert_equal '/wiki/foo/bar', wiki_path('foo/bar')
+ test "glob segments with slashes are not escaped" do
+ assert_equal "/wiki/foo/bar", Routes.url_helpers.wiki_path("foo/bar")
+ assert_equal "/wiki/foo/bar", wiki_path("foo/bar")
end
end
class TestNamedRouteUrlHelpers < ActionDispatch::IntegrationTest
class CategoriesController < ActionController::Base
def show
- render :text => "categories#show"
+ render plain: "categories#show"
end
end
class ProductsController < ActionController::Base
def show
- render :text => "products#show"
+ render plain: "products#show"
end
end
Routes = ActionDispatch::Routing::RouteSet.new.tap do |app|
app.draw do
- scope :module => "test_named_route_url_helpers" do
- get "/categories/:id" => 'categories#show', :as => :category
- get "/products/:id" => 'products#show', :as => :product
+ scope module: "test_named_route_url_helpers" do
+ get "/categories/:id" => "categories#show", :as => :category
+ get "/products/:id" => "products#show", :as => :product
end
end
end
@@ -4189,32 +4373,32 @@ class TestNamedRouteUrlHelpers < ActionDispatch::IntegrationTest
include Routes.url_helpers
test "url helpers do not ignore nil parameters when using non-optimized routes" do
- Routes.stubs(:optimize_routes_generation?).returns(false)
-
- get "/categories/1"
- assert_response :success
- assert_raises(ActionController::UrlGenerationError) { product_path(nil) }
+ Routes.stub :optimize_routes_generation?, false do
+ get "/categories/1"
+ assert_response :success
+ assert_raises(ActionController::UrlGenerationError) { product_path(nil) }
+ end
end
end
class TestUrlConstraints < ActionDispatch::IntegrationTest
Routes = ActionDispatch::Routing::RouteSet.new.tap do |app|
app.draw do
- ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] }
+ ok = lambda { |env| [200, { "Content-Type" => "text/plain" }, []] }
- constraints :subdomain => 'admin' do
- get '/' => ok, :as => :admin_root
+ constraints subdomain: "admin" do
+ get "/" => ok, :as => :admin_root
end
- scope :constraints => { :protocol => 'https://' } do
- get '/' => ok, :as => :secure_root
+ scope constraints: { protocol: "https://" } do
+ get "/" => ok, :as => :secure_root
end
- get '/' => ok, :as => :alternate_root, :constraints => { :port => 8080 }
+ get "/" => ok, :as => :alternate_root, :constraints => { port: 8080 }
- get '/search' => ok, :constraints => { :subdomain => false }
+ get "/search" => ok, :constraints => { subdomain: false }
- get '/logs' => ok, :constraints => { :subdomain => true }
+ get "/logs" => ok, :constraints => { subdomain: true }
end
end
@@ -4223,59 +4407,67 @@ class TestUrlConstraints < ActionDispatch::IntegrationTest
def app; APP end
test "constraints are copied to defaults when using constraints method" do
- assert_equal 'http://admin.example.com/', admin_root_url
+ assert_equal "http://admin.example.com/", admin_root_url
- get 'http://admin.example.com/'
+ get "http://admin.example.com/"
assert_response :success
end
test "constraints are copied to defaults when using scope constraints hash" do
- assert_equal 'https://www.example.com/', secure_root_url
+ assert_equal "https://www.example.com/", secure_root_url
- get 'https://www.example.com/'
+ get "https://www.example.com/"
assert_response :success
end
test "constraints are copied to defaults when using route constraints hash" do
- assert_equal 'http://www.example.com:8080/', alternate_root_url
+ assert_equal "http://www.example.com:8080/", alternate_root_url
- get 'http://www.example.com:8080/'
+ get "http://www.example.com:8080/"
assert_response :success
end
test "false constraint expressions check for absence of values" do
- get 'http://example.com/search'
+ get "http://example.com/search"
assert_response :success
- assert_equal 'http://example.com/search', search_url
+ assert_equal "http://example.com/search", search_url
- get 'http://api.example.com/search'
+ get "http://api.example.com/search"
assert_response :not_found
end
test "true constraint expressions check for presence of values" do
- get 'http://api.example.com/logs'
+ get "http://api.example.com/logs"
assert_response :success
- assert_equal 'http://api.example.com/logs', logs_url
+ assert_equal "http://api.example.com/logs", logs_url
- get 'http://example.com/logs'
+ get "http://example.com/logs"
assert_response :not_found
end
end
class TestInvalidUrls < ActionDispatch::IntegrationTest
class FooController < ActionController::Base
+ def self.binary_params_for?(action)
+ action == "show"
+ end
+
def show
- render :text => "foo#show"
+ render plain: "foo#show"
end
end
- test "invalid UTF-8 encoding returns a 400 Bad Request" do
+ test "invalid UTF-8 encoding returns a bad request" do
with_routing do |set|
set.draw do
- get "/bar/:id", :to => redirect("/foo/show/%{id}")
- get "/foo/show(/:id)", :to => "test_invalid_urls/foo#show"
- get "/foo(/:action(/:id))", :controller => "test_invalid_urls/foo"
- get "/:controller(/:action(/:id))"
+ get "/bar/:id", to: redirect("/foo/show/%{id}")
+
+ ok = lambda { |env| [200, { "Content-Type" => "text/plain" }, []] }
+ get "/foobar/:id", to: ok
+
+ ActiveSupport::Deprecation.silence do
+ get "/:controller(/:action(/:id))"
+ end
end
get "/%E2%EF%BF%BD%A6"
@@ -4284,20 +4476,31 @@ class TestInvalidUrls < ActionDispatch::IntegrationTest
get "/foo/%E2%EF%BF%BD%A6"
assert_response :bad_request
- get "/foo/show/%E2%EF%BF%BD%A6"
+ get "/bar/%E2%EF%BF%BD%A6"
assert_response :bad_request
- get "/bar/%E2%EF%BF%BD%A6"
+ get "/foobar/%E2%EF%BF%BD%A6"
assert_response :bad_request
end
end
+
+ test "params encoded with binary_params_for? are treated as ASCII 8bit" do
+ with_routing do |set|
+ set.draw do
+ get "/foo/show(/:id)", to: "test_invalid_urls/foo#show"
+ end
+
+ get "/foo/show/%E2%EF%BF%BD%A6"
+ assert_response :ok
+ end
+ end
end
class TestOptionalRootSegments < ActionDispatch::IntegrationTest
stub_controllers do |routes|
Routes = routes
Routes.draw do
- get '/(page/:page)', :to => 'pages#index', :as => :root
+ get "/(page/:page)", to: "pages#index", as: :root
end
end
@@ -4309,27 +4512,27 @@ class TestOptionalRootSegments < ActionDispatch::IntegrationTest
include Routes.url_helpers
def test_optional_root_segments
- get '/'
- assert_equal 'pages#index', @response.body
- assert_equal '/', root_path
+ get "/"
+ assert_equal "pages#index", @response.body
+ assert_equal "/", root_path
- get '/page/1'
- assert_equal 'pages#index', @response.body
- assert_equal '1', @request.params[:page]
- assert_equal '/page/1', root_path('1')
- assert_equal '/page/1', root_path(:page => '1')
+ get "/page/1"
+ assert_equal "pages#index", @response.body
+ assert_equal "1", @request.params[:page]
+ assert_equal "/page/1", root_path("1")
+ assert_equal "/page/1", root_path(page: "1")
end
end
class TestPortConstraints < ActionDispatch::IntegrationTest
Routes = ActionDispatch::Routing::RouteSet.new.tap do |app|
app.draw do
- ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] }
+ ok = lambda { |env| [200, { "Content-Type" => "text/plain" }, []] }
- get '/integer', to: ok, constraints: { :port => 8080 }
- get '/string', to: ok, constraints: { :port => '8080' }
- get '/array', to: ok, constraints: { :port => [8080] }
- get '/regexp', to: ok, constraints: { :port => /8080/ }
+ get "/integer", to: ok, constraints: { port: 8080 }
+ get "/string", to: ok, constraints: { port: "8080" }
+ get "/array/:idx", to: ok, constraints: { port: [8080], idx: %w[first last] }
+ get "/regexp", to: ok, constraints: { port: /8080/ }
end
end
@@ -4338,34 +4541,37 @@ class TestPortConstraints < ActionDispatch::IntegrationTest
def app; APP end
def test_integer_port_constraints
- get 'http://www.example.com/integer'
+ get "http://www.example.com/integer"
assert_response :not_found
- get 'http://www.example.com:8080/integer'
+ get "http://www.example.com:8080/integer"
assert_response :success
end
def test_string_port_constraints
- get 'http://www.example.com/string'
+ get "http://www.example.com/string"
assert_response :not_found
- get 'http://www.example.com:8080/string'
+ get "http://www.example.com:8080/string"
assert_response :success
end
def test_array_port_constraints
- get 'http://www.example.com/array'
+ get "http://www.example.com/array"
+ assert_response :not_found
+
+ get "http://www.example.com:8080/array/middle"
assert_response :not_found
- get 'http://www.example.com:8080/array'
+ get "http://www.example.com:8080/array/first"
assert_response :success
end
def test_regexp_port_constraints
- get 'http://www.example.com/regexp'
+ get "http://www.example.com/regexp"
assert_response :not_found
- get 'http://www.example.com:8080/regexp'
+ get "http://www.example.com:8080/regexp"
assert_response :success
end
end
@@ -4373,12 +4579,12 @@ end
class TestFormatConstraints < ActionDispatch::IntegrationTest
Routes = ActionDispatch::Routing::RouteSet.new.tap do |app|
app.draw do
- ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] }
+ ok = lambda { |env| [200, { "Content-Type" => "text/plain" }, []] }
- get '/string', to: ok, constraints: { format: 'json' }
- get '/regexp', to: ok, constraints: { format: /json/ }
- get '/json_only', to: ok, format: true, constraints: { format: /json/ }
- get '/xml_only', to: ok, format: 'xml'
+ get "/string", to: ok, constraints: { format: "json" }
+ get "/regexp", to: ok, constraints: { format: /json/ }
+ get "/json_only", to: ok, format: true, constraints: { format: /json/ }
+ get "/xml_only", to: ok, format: "xml"
end
end
@@ -4387,46 +4593,46 @@ class TestFormatConstraints < ActionDispatch::IntegrationTest
def app; APP end
def test_string_format_constraints
- get 'http://www.example.com/string'
+ get "http://www.example.com/string"
assert_response :success
- get 'http://www.example.com/string.json'
+ get "http://www.example.com/string.json"
assert_response :success
- get 'http://www.example.com/string.html'
+ get "http://www.example.com/string.html"
assert_response :not_found
end
def test_regexp_format_constraints
- get 'http://www.example.com/regexp'
+ get "http://www.example.com/regexp"
assert_response :success
- get 'http://www.example.com/regexp.json'
+ get "http://www.example.com/regexp.json"
assert_response :success
- get 'http://www.example.com/regexp.html'
+ get "http://www.example.com/regexp.html"
assert_response :not_found
end
def test_enforce_with_format_true_with_constraint
- get 'http://www.example.com/json_only.json'
+ get "http://www.example.com/json_only.json"
assert_response :success
- get 'http://www.example.com/json_only.html'
+ get "http://www.example.com/json_only.html"
assert_response :not_found
- get 'http://www.example.com/json_only'
+ get "http://www.example.com/json_only"
assert_response :not_found
end
def test_enforce_with_string
- get 'http://www.example.com/xml_only.xml'
+ get "http://www.example.com/xml_only.xml"
assert_response :success
- get 'http://www.example.com/xml_only'
+ get "http://www.example.com/xml_only"
assert_response :success
- get 'http://www.example.com/xml_only.json'
+ get "http://www.example.com/xml_only.json"
assert_response :not_found
end
end
@@ -4435,8 +4641,8 @@ class TestCallableConstraintValidation < ActionDispatch::IntegrationTest
def test_constraint_with_object_not_callable
assert_raises(ArgumentError) do
ActionDispatch::Routing::RouteSet.new.draw do
- ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] }
- get '/test', to: ok, constraints: Object.new
+ ok = lambda { |env| [200, { "Content-Type" => "text/plain" }, []] }
+ get "/test", to: ok, constraints: Object.new
end
end
end
@@ -4446,8 +4652,8 @@ class TestRouteDefaults < ActionDispatch::IntegrationTest
stub_controllers do |routes|
Routes = routes
Routes.draw do
- resources :posts, bucket_type: 'post'
- resources :projects, defaults: { bucket_type: 'project' }
+ resources :posts, bucket_type: "post"
+ resources :projects, defaults: { bucket_type: "project" }
end
end
@@ -4460,14 +4666,14 @@ class TestRouteDefaults < ActionDispatch::IntegrationTest
def test_route_options_are_required_for_url_for
assert_raises(ActionController::UrlGenerationError) do
- assert_equal '/posts/1', url_for(controller: 'posts', action: 'show', id: 1, only_path: true)
+ assert_equal "/posts/1", url_for(controller: "posts", action: "show", id: 1, only_path: true)
end
- assert_equal '/posts/1', url_for(controller: 'posts', action: 'show', id: 1, bucket_type: 'post', only_path: true)
+ assert_equal "/posts/1", url_for(controller: "posts", action: "show", id: 1, bucket_type: "post", only_path: true)
end
def test_route_defaults_are_not_required_for_url_for
- assert_equal '/projects/1', url_for(controller: 'projects', action: 'show', id: 1, only_path: true)
+ assert_equal "/projects/1", url_for(controller: "projects", action: "show", id: 1, only_path: true)
end
end
@@ -4475,9 +4681,9 @@ class TestRackAppRouteGeneration < ActionDispatch::IntegrationTest
stub_controllers do |routes|
Routes = routes
Routes.draw do
- rack_app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] }
- mount rack_app, at: '/account', as: 'account'
- mount rack_app, at: '/:locale/account', as: 'localized_account'
+ rack_app = lambda { |env| [200, { "Content-Type" => "text/plain" }, []] }
+ mount rack_app, at: "/account", as: "account"
+ mount rack_app, at: "/:locale/account", as: "localized_account"
end
end
@@ -4490,11 +4696,11 @@ class TestRackAppRouteGeneration < ActionDispatch::IntegrationTest
def test_mounted_application_doesnt_match_unnamed_route
assert_raise(ActionController::UrlGenerationError) do
- assert_equal '/account?controller=products', url_for(controller: 'products', action: 'index', only_path: true)
+ assert_equal "/account?controller=products", url_for(controller: "products", action: "index", only_path: true)
end
assert_raise(ActionController::UrlGenerationError) do
- assert_equal '/de/account?controller=products', url_for(controller: 'products', action: 'index', :locale => 'de', only_path: true)
+ assert_equal "/de/account?controller=products", url_for(controller: "products", action: "index", locale: "de", only_path: true)
end
end
end
@@ -4503,8 +4709,8 @@ class TestRedirectRouteGeneration < ActionDispatch::IntegrationTest
stub_controllers do |routes|
Routes = routes
Routes.draw do
- get '/account', to: redirect('/myaccount'), as: 'account'
- get '/:locale/account', to: redirect('/%{locale}/myaccount'), as: 'localized_account'
+ get "/account", to: redirect("/myaccount"), as: "account"
+ get "/:locale/account", to: redirect("/%{locale}/myaccount"), as: "localized_account"
end
end
@@ -4517,11 +4723,11 @@ class TestRedirectRouteGeneration < ActionDispatch::IntegrationTest
def test_redirect_doesnt_match_unnamed_route
assert_raise(ActionController::UrlGenerationError) do
- assert_equal '/account?controller=products', url_for(controller: 'products', action: 'index', only_path: true)
+ assert_equal "/account?controller=products", url_for(controller: "products", action: "index", only_path: true)
end
assert_raise(ActionController::UrlGenerationError) do
- assert_equal '/de/account?controller=products', url_for(controller: 'products', action: 'index', :locale => 'de', only_path: true)
+ assert_equal "/de/account?controller=products", url_for(controller: "products", action: "index", locale: "de", only_path: true)
end
end
end
@@ -4529,7 +4735,7 @@ end
class TestUrlGenerationErrors < ActionDispatch::IntegrationTest
Routes = ActionDispatch::Routing::RouteSet.new.tap do |app|
app.draw do
- get "/products/:id" => 'products#show', :as => :product
+ get "/products/:id" => "products#show", :as => :product
end
end
@@ -4538,29 +4744,32 @@ class TestUrlGenerationErrors < ActionDispatch::IntegrationTest
include Routes.url_helpers
- test "url helpers raise a helpful error message when generation fails" do
- url, missing = { action: 'show', controller: 'products', id: nil }, [:id]
- message = "No route matches #{url.inspect} missing required keys: #{missing.inspect}"
+ test "url helpers raise a 'missing keys' error for a nil param with optimized helpers" do
+ url, missing = { action: "show", controller: "products", id: nil }, [:id]
+ message = "No route matches #{url.inspect}, missing required keys: #{missing.inspect}"
- # Optimized url helper
- error = assert_raises(ActionController::UrlGenerationError){ product_path(nil) }
+ error = assert_raises(ActionController::UrlGenerationError) { product_path(nil) }
assert_equal message, error.message
+ end
- # Non-optimized url helper
- error = assert_raises(ActionController::UrlGenerationError, message){ product_path(id: nil) }
+ test "url helpers raise a 'constraint failure' error for a nil param with non-optimized helpers" do
+ url, missing = { action: "show", controller: "products", id: nil }, [:id]
+ message = "No route matches #{url.inspect}, possible unmatched constraints: #{missing.inspect}"
+
+ error = assert_raises(ActionController::UrlGenerationError, message) { product_path(id: nil) }
assert_equal message, error.message
end
- test "url helpers raise message with mixed parameters when generation fails " do
- url, missing = { action: 'show', controller: 'products', id: nil, "id"=>"url-tested"}, [:id]
- message = "No route matches #{url.inspect} missing required keys: #{missing.inspect}"
+ test "url helpers raise message with mixed parameters when generation fails" do
+ url, missing = { action: "show", controller: "products", id: nil, "id" => "url-tested" }, [:id]
+ message = "No route matches #{url.inspect}, possible unmatched constraints: #{missing.inspect}"
# Optimized url helper
- error = assert_raises(ActionController::UrlGenerationError){ product_path(nil, 'id'=>'url-tested') }
+ error = assert_raises(ActionController::UrlGenerationError) { product_path(nil, "id" => "url-tested") }
assert_equal message, error.message
# Non-optimized url helper
- error = assert_raises(ActionController::UrlGenerationError, message){ product_path(id: nil, 'id'=>'url-tested') }
+ error = assert_raises(ActionController::UrlGenerationError, message) { product_path(id: nil, "id" => "url-tested") }
assert_equal message, error.message
end
end
@@ -4568,15 +4777,15 @@ end
class TestDefaultUrlOptions < ActionDispatch::IntegrationTest
class PostsController < ActionController::Base
def archive
- render :text => "posts#archive"
+ render plain: "posts#archive"
end
end
Routes = ActionDispatch::Routing::RouteSet.new
Routes.draw do
- default_url_options locale: 'en'
- scope ':locale', format: false do
- get '/posts/:year/:month/:day', to: 'posts#archive', as: 'archived_posts'
+ default_url_options locale: "en"
+ scope ":locale", format: false do
+ get "/posts/:year/:month/:day", to: "posts#archive", as: "archived_posts"
end
end
@@ -4589,6 +4798,333 @@ class TestDefaultUrlOptions < ActionDispatch::IntegrationTest
include Routes.url_helpers
def test_positional_args_with_format_false
- assert_equal '/en/posts/2014/12/13', archived_posts_path(2014, 12, 13)
+ assert_equal "/en/posts/2014/12/13", archived_posts_path(2014, 12, 13)
+ end
+end
+
+class TestErrorsInController < ActionDispatch::IntegrationTest
+ class ::PostsController < ActionController::Base
+ def foo
+ nil.i_do_not_exist
+ end
+
+ def bar
+ NonExistingClass.new
+ end
+ end
+
+ Routes = ActionDispatch::Routing::RouteSet.new
+ Routes.draw do
+ ActiveSupport::Deprecation.silence do
+ get "/:controller(/:action)"
+ end
+ end
+
+ APP = build_app Routes
+
+ def app
+ APP
+ end
+
+ def test_legit_no_method_errors_are_not_caught
+ get "/posts/foo"
+ assert_equal 500, response.status
+ end
+
+ def test_legit_name_errors_are_not_caught
+ get "/posts/bar"
+ assert_equal 500, response.status
+ end
+
+ def test_legit_routing_not_found_responses
+ get "/posts/baz"
+ assert_equal 404, response.status
+
+ get "/i_do_not_exist"
+ assert_equal 404, response.status
+ end
+end
+
+class TestPartialDynamicPathSegments < ActionDispatch::IntegrationTest
+ Routes = ActionDispatch::Routing::RouteSet.new
+ Routes.draw do
+ ok = lambda { |env| [200, { "Content-Type" => "text/plain" }, []] }
+
+ get "/songs/song-:song", to: ok
+ get "/songs/:song-song", to: ok
+ get "/:artist/song-:song", to: ok
+ get "/:artist/:song-song", to: ok
+
+ get "/optional/songs(/song-:song)", to: ok
+ get "/optional/songs(/:song-song)", to: ok
+ get "/optional/:artist(/song-:song)", to: ok
+ get "/optional/:artist(/:song-song)", to: ok
+ end
+
+ APP = build_app Routes
+
+ def app
+ APP
+ end
+
+ def test_paths_with_partial_dynamic_segments_are_recognised
+ get "/david-bowie/changes-song"
+ assert_equal 200, response.status
+ assert_params artist: "david-bowie", song: "changes"
+
+ get "/david-bowie/song-changes"
+ assert_equal 200, response.status
+ assert_params artist: "david-bowie", song: "changes"
+
+ get "/songs/song-changes"
+ assert_equal 200, response.status
+ assert_params song: "changes"
+
+ get "/songs/changes-song"
+ assert_equal 200, response.status
+ assert_params song: "changes"
+
+ get "/optional/songs/song-changes"
+ assert_equal 200, response.status
+ assert_params song: "changes"
+
+ get "/optional/songs/changes-song"
+ assert_equal 200, response.status
+ assert_params song: "changes"
+
+ get "/optional/david-bowie/changes-song"
+ assert_equal 200, response.status
+ assert_params artist: "david-bowie", song: "changes"
+
+ get "/optional/david-bowie/song-changes"
+ assert_equal 200, response.status
+ assert_params artist: "david-bowie", song: "changes"
+ end
+
+ private
+
+ def assert_params(params)
+ assert_equal(params, request.path_parameters)
+ end
+end
+
+class TestPathParameters < ActionDispatch::IntegrationTest
+ Routes = ActionDispatch::Routing::RouteSet.new.tap do |app|
+ app.draw do
+ scope module: "test_path_parameters" do
+ scope ":locale", locale: /en|ar/ do
+ root to: "home#index"
+ get "/about", to: "pages#about"
+ end
+ end
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller(/:action/(:id))"
+ end
+ end
+ end
+
+ class HomeController < ActionController::Base
+ include Routes.url_helpers
+
+ def index
+ render inline: "<%= root_path %>"
+ end
+ end
+
+ class PagesController < ActionController::Base
+ include Routes.url_helpers
+
+ def about
+ render inline: "<%= root_path(locale: :ar) %> | <%= url_for(locale: :ar) %>"
+ end
+ end
+
+ APP = build_app Routes
+ def app; APP end
+
+ def test_path_parameters_are_not_mutated
+ get "/en/about"
+ assert_equal "/ar | /ar/about", @response.body
+ end
+end
+
+class TestInternalRoutingParams < ActionDispatch::IntegrationTest
+ Routes = ActionDispatch::Routing::RouteSet.new.tap do |app|
+ app.draw do
+ get "/test_internal/:internal" => "internal#internal"
+ end
+ end
+
+ class ::InternalController < ActionController::Base
+ def internal
+ head :ok
+ end
+ end
+
+ APP = build_app Routes
+
+ def app
+ APP
+ end
+
+ def test_paths_with_partial_dynamic_segments_are_recognised
+ get "/test_internal/123"
+ assert_equal 200, response.status
+
+ assert_equal(
+ { controller: "internal", action: "internal", internal: "123" },
+ request.path_parameters
+ )
+ end
+end
+
+class FlashRedirectTest < ActionDispatch::IntegrationTest
+ SessionKey = "_myapp_session"
+ Generator = ActiveSupport::LegacyKeyGenerator.new("b3c631c314c0bbca50c1b2843150fe33")
+ Rotations = ActiveSupport::Messages::RotationConfiguration.new
+
+ class KeyGeneratorMiddleware
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ env["action_dispatch.key_generator"] ||= Generator
+ env["action_dispatch.cookies_rotations"] ||= Rotations
+
+ @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
+
+class TestRelativeUrlRootGeneration < ActionDispatch::IntegrationTest
+ config = ActionDispatch::Routing::RouteSet::Config.new("/blog", false)
+
+ stub_controllers(config) do |routes|
+ Routes = routes
+
+ routes.draw do
+ get "/", to: "posts#index", as: :posts
+ get "/:id", to: "posts#show", as: :post
+ end
+ end
+
+ include Routes.url_helpers
+
+ APP = build_app Routes
+
+ def app
+ APP
+ end
+
+ def test_url_helpers
+ assert_equal "/blog/", posts_path({})
+ assert_equal "/blog/", Routes.url_helpers.posts_path({})
+
+ assert_equal "/blog/1", post_path(id: "1")
+ assert_equal "/blog/1", Routes.url_helpers.post_path(id: "1")
+ end
+
+ def test_optimized_url_helpers
+ assert_equal "/blog/", posts_path
+ assert_equal "/blog/", Routes.url_helpers.posts_path
+
+ assert_equal "/blog/1", post_path("1")
+ assert_equal "/blog/1", Routes.url_helpers.post_path("1")
end
end
diff --git a/actionpack/test/dispatch/runner_test.rb b/actionpack/test/dispatch/runner_test.rb
new file mode 100644
index 0000000000..f16c7963af
--- /dev/null
+++ b/actionpack/test/dispatch/runner_test.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class RunnerTest < ActiveSupport::TestCase
+ test "runner preserves the setting of integration_session" do
+ runner = Class.new do
+ def before_setup
+ end
+ end.new
+
+ runner.extend(ActionDispatch::Integration::Runner)
+ runner.integration_session.host! "lvh.me"
+
+ runner.before_setup
+
+ assert_equal "lvh.me", runner.integration_session.host
+ end
+end
diff --git a/actionpack/test/dispatch/session/abstract_store_test.rb b/actionpack/test/dispatch/session/abstract_store_test.rb
index fe1a7b4f86..47616db15a 100644
--- a/actionpack/test/dispatch/session/abstract_store_test.rb
+++ b/actionpack/test/dispatch/session/abstract_store_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'action_dispatch/middleware/session/abstract_store'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "action_dispatch/middleware/session/abstract_store"
module ActionDispatch
module Session
@@ -10,13 +12,13 @@ module ActionDispatch
super
end
- def get_session(env, sid)
+ def find_session(env, sid)
sid ||= 1
session = @sessions[sid] ||= {}
[sid, session]
end
- def set_session(env, sid, session, options)
+ def write_session(env, sid, session, options)
@sessions[sid] = session
end
end
@@ -27,7 +29,7 @@ module ActionDispatch
as.call(env)
assert @env
- assert Request::Session.find @env
+ assert Request::Session.find ActionDispatch::Request.new @env
end
def test_new_session_object_is_merged_with_old
@@ -36,21 +38,21 @@ module ActionDispatch
as.call(env)
assert @env
- session = Request::Session.find @env
- session['foo'] = 'bar'
+ session = Request::Session.find ActionDispatch::Request.new @env
+ session["foo"] = "bar"
as.call(@env)
- session1 = Request::Session.find @env
+ session1 = Request::Session.find ActionDispatch::Request.new @env
assert_not_equal session, session1
assert_equal session.to_hash, session1.to_hash
end
private
- def app(&block)
- @env = nil
- lambda { |env| @env = env }
- end
+ def app(&block)
+ @env = nil
+ lambda { |env| @env = env }
+ end
end
end
end
diff --git a/actionpack/test/dispatch/session/cache_store_test.rb b/actionpack/test/dispatch/session/cache_store_test.rb
index 22a46b0930..06e67fac9f 100644
--- a/actionpack/test/dispatch/session/cache_store_test.rb
+++ b/actionpack/test/dispatch/session/cache_store_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'fixtures/session_autoload_test/session_autoload_test/foo'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "fixtures/session_autoload_test/session_autoload_test/foo"
class CacheStoreTest < ActionDispatch::IntegrationTest
class TestController < ActionController::Base
@@ -18,11 +20,11 @@ class CacheStoreTest < ActionDispatch::IntegrationTest
end
def get_session_value
- render :text => "foo: #{session[:foo].inspect}"
+ render plain: "foo: #{session[:foo].inspect}"
end
def get_session_id
- render :text => "#{request.session.id}"
+ render plain: "#{request.session.id}"
end
def call_reset_session
@@ -35,11 +37,11 @@ class CacheStoreTest < ActionDispatch::IntegrationTest
def test_setting_and_getting_session_value
with_test_route_set do
- get '/set_session_value'
+ get "/set_session_value"
assert_response :success
- assert cookies['_session_id']
+ assert cookies["_session_id"]
- get '/get_session_value'
+ get "/get_session_value"
assert_response :success
assert_equal 'foo: "bar"', response.body
end
@@ -47,56 +49,56 @@ class CacheStoreTest < ActionDispatch::IntegrationTest
def test_getting_nil_session_value
with_test_route_set do
- get '/get_session_value'
+ get "/get_session_value"
assert_response :success
- assert_equal 'foo: nil', response.body
+ assert_equal "foo: nil", response.body
end
end
def test_getting_session_value_after_session_reset
with_test_route_set do
- get '/set_session_value'
+ get "/set_session_value"
assert_response :success
- assert cookies['_session_id']
- session_cookie = cookies.send(:hash_for)['_session_id']
+ assert cookies["_session_id"]
+ session_cookie = cookies.send(:hash_for)["_session_id"]
- get '/call_reset_session'
+ get "/call_reset_session"
assert_response :success
- assert_not_equal [], headers['Set-Cookie']
+ assert_not_equal [], headers["Set-Cookie"]
cookies << session_cookie # replace our new session_id with our old, pre-reset session_id
- get '/get_session_value'
+ get "/get_session_value"
assert_response :success
- assert_equal 'foo: nil', response.body, "data for this session should have been obliterated from cache"
+ assert_equal "foo: nil", response.body, "data for this session should have been obliterated from cache"
end
end
def test_getting_from_nonexistent_session
with_test_route_set do
- get '/get_session_value'
+ get "/get_session_value"
assert_response :success
- assert_equal 'foo: nil', response.body
- assert_nil cookies['_session_id'], "should only create session on write, not read"
+ assert_equal "foo: nil", response.body
+ assert_nil cookies["_session_id"], "should only create session on write, not read"
end
end
def test_setting_session_value_after_session_reset
with_test_route_set do
- get '/set_session_value'
+ get "/set_session_value"
assert_response :success
- assert cookies['_session_id']
- session_id = cookies['_session_id']
+ assert cookies["_session_id"]
+ session_id = cookies["_session_id"]
- get '/call_reset_session'
+ get "/call_reset_session"
assert_response :success
- assert_not_equal [], headers['Set-Cookie']
+ assert_not_equal [], headers["Set-Cookie"]
- get '/get_session_value'
+ get "/get_session_value"
assert_response :success
- assert_equal 'foo: nil', response.body
+ assert_equal "foo: nil", response.body
- get '/get_session_id'
+ get "/get_session_id"
assert_response :success
assert_not_equal session_id, response.body
end
@@ -104,12 +106,12 @@ class CacheStoreTest < ActionDispatch::IntegrationTest
def test_getting_session_id
with_test_route_set do
- get '/set_session_value'
+ get "/set_session_value"
assert_response :success
- assert cookies['_session_id']
- session_id = cookies['_session_id']
+ assert cookies["_session_id"]
+ session_id = cookies["_session_id"]
- get '/get_session_id'
+ get "/get_session_id"
assert_response :success
assert_equal session_id, response.body, "should be able to read session id without accessing the session hash"
end
@@ -118,16 +120,16 @@ class CacheStoreTest < ActionDispatch::IntegrationTest
def test_deserializes_unloaded_class
with_test_route_set do
with_autoload_path "session_autoload_test" do
- get '/set_serialized_session_value'
+ get "/set_serialized_session_value"
assert_response :success
- assert cookies['_session_id']
+ assert cookies["_session_id"]
end
with_autoload_path "session_autoload_test" do
- get '/get_session_id'
+ get "/get_session_id"
assert_response :success
end
with_autoload_path "session_autoload_test" do
- get '/get_session_value'
+ get "/get_session_value"
assert_response :success
assert_equal 'foo: #<SessionAutoloadTest::Foo bar:"baz">', response.body, "should auto-load unloaded class"
end
@@ -136,27 +138,27 @@ class CacheStoreTest < ActionDispatch::IntegrationTest
def test_doesnt_write_session_cookie_if_session_id_is_already_exists
with_test_route_set do
- get '/set_session_value'
+ get "/set_session_value"
assert_response :success
- assert cookies['_session_id']
+ assert cookies["_session_id"]
- get '/get_session_value'
+ 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'
+ 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_equal({'foo' => 'bar'}, @cache.read("_session_id:#{cookies['_session_id']}"))
+ assert_not_equal "0xhax", cookies["_session_id"]
+ assert_nil @cache.read("_session_id:0xhax")
+ assert_equal({ "foo" => "bar" }, @cache.read("_session_id:#{cookies['_session_id']}"))
end
end
@@ -164,13 +166,15 @@ class CacheStoreTest < ActionDispatch::IntegrationTest
def with_test_route_set
with_routing do |set|
set.draw do
- get ':action', :to => ::CacheStoreTest::TestController
+ ActiveSupport::Deprecation.silence do
+ get ":action", to: ::CacheStoreTest::TestController
+ end
end
@app = self.class.build_app(set) do |middleware|
@cache = ActiveSupport::Cache::MemoryStore.new
- middleware.use ActionDispatch::Session::CacheStore, :key => '_session_id', :cache => @cache
- middleware.delete "ActionDispatch::ShowExceptions"
+ middleware.use ActionDispatch::Session::CacheStore, key: "_session_id", cache: @cache
+ middleware.delete ActionDispatch::ShowExceptions
end
yield
diff --git a/actionpack/test/dispatch/session/cookie_store_test.rb b/actionpack/test/dispatch/session/cookie_store_test.rb
index e7f4235de8..e34426a471 100644
--- a/actionpack/test/dispatch/session/cookie_store_test.rb
+++ b/actionpack/test/dispatch/session/cookie_store_test.rb
@@ -1,14 +1,21 @@
-require 'abstract_unit'
-require 'stringio'
-require 'active_support/key_generator'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "stringio"
+require "active_support/key_generator"
+require "active_support/messages/rotation_configuration"
class CookieStoreTest < ActionDispatch::IntegrationTest
- SessionKey = '_myapp_session'
- SessionSecret = 'b3c631c314c0bbca50c1b2843150fe33'
- Generator = ActiveSupport::LegacyKeyGenerator.new(SessionSecret)
+ SessionKey = "_myapp_session"
+ SessionSecret = "b3c631c314c0bbca50c1b2843150fe33"
+ SessionSalt = "authenticated encrypted cookie"
+
+ Generator = ActiveSupport::KeyGenerator.new(SessionSecret, iterations: 1000)
+ Rotations = ActiveSupport::Messages::RotationConfiguration.new
- Verifier = ActiveSupport::MessageVerifier.new(SessionSecret, :digest => 'SHA1')
- SignedBar = Verifier.generate(:foo => "bar", :session_id => SecureRandom.hex(16))
+ Encryptor = ActiveSupport::MessageEncryptor.new(
+ Generator.generate_key(SessionSalt, 32), cipher: "aes-256-gcm", serializer: Marshal
+ )
class TestController < ActionController::Base
def no_session_access
@@ -16,25 +23,25 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
end
def persistent_session_id
- render :text => session[:session_id]
+ render plain: session[:session_id]
end
def set_session_value
session[:foo] = "bar"
- render :text => Rack::Utils.escape(Verifier.generate(session.to_hash))
+ render body: nil
end
def get_session_value
- render :text => "foo: #{session[:foo].inspect}"
+ render plain: "foo: #{session[:foo].inspect}"
end
def get_session_id
- render :text => "id: #{request.session.id}"
+ render plain: "id: #{request.session.id}"
end
def get_class_after_reset_session
reset_session
- render :text => "class: #{session.class}"
+ render plain: "class: #{session.class}"
end
def call_session_clear
@@ -48,7 +55,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
end
def raise_data_overflow
- session[:foo] = 'bye!' * 1024
+ session[:foo] = "bye!" * 1024
head :ok
end
@@ -63,19 +70,35 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
end
end
+ def parse_cookie_from_header
+ cookie_matches = headers["Set-Cookie"].match(/#{SessionKey}=([^;]+)/)
+ cookie_matches && cookie_matches[1]
+ end
+
+ def assert_session_cookie(cookie_string, contents)
+ assert_includes headers["Set-Cookie"], cookie_string
+
+ session_value = parse_cookie_from_header
+ session_data = Encryptor.decrypt_and_verify(Rack::Utils.unescape(session_value)) rescue nil
+
+ assert_not_nil session_data, "session failed to decrypt"
+ assert_equal session_data.slice(*contents.keys), contents
+ end
+
def test_setting_session_value
with_test_route_set do
- get '/set_session_value'
+ get "/set_session_value"
+
assert_response :success
- assert_equal "_myapp_session=#{response.body}; path=/; HttpOnly",
- headers['Set-Cookie']
+ assert_session_cookie "path=/; HttpOnly", "foo" => "bar"
end
end
def test_getting_session_value
with_test_route_set do
- cookies[SessionKey] = SignedBar
- get '/get_session_value'
+ get "/set_session_value"
+ get "/get_session_value"
+
assert_response :success
assert_equal 'foo: "bar"', response.body
end
@@ -83,13 +106,14 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
def test_getting_session_id
with_test_route_set do
- cookies[SessionKey] = SignedBar
- get '/persistent_session_id'
+ get "/set_session_value"
+ get "/persistent_session_id"
+
assert_response :success
assert_equal 32, response.body.size
session_id = response.body
- get '/get_session_id'
+ get "/get_session_id"
assert_response :success
assert_equal "id: #{session_id}", response.body, "should be able to read session id without accessing the session hash"
end
@@ -97,51 +121,55 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
def test_disregards_tampered_sessions
with_test_route_set do
- cookies[SessionKey] = "BAh7BjoIZm9vIghiYXI%3D--123456780"
- get '/get_session_value'
+ encryptor = ActiveSupport::MessageEncryptor.new("A" * 32, cipher: "aes-256-gcm", serializer: Marshal)
+
+ cookies[SessionKey] = encryptor.encrypt_and_sign("foo" => "bar", "session_id" => "abc")
+
+ get "/get_session_value"
+
assert_response :success
- assert_equal 'foo: nil', response.body
+ assert_equal "foo: nil", response.body
end
end
def test_does_not_set_secure_cookies_over_http
- with_test_route_set(:secure => true) do
- get '/set_session_value'
+ 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
def test_properly_renew_cookies
with_test_route_set do
- get '/set_session_value'
- get '/persistent_session_id'
+ get "/set_session_value"
+ get "/persistent_session_id"
session_id = response.body
- get '/renew_session_id'
- get '/persistent_session_id'
+ get "/renew_session_id"
+ get "/persistent_session_id"
assert_not_equal response.body, session_id
end
end
def test_does_set_secure_cookies_over_https
- with_test_route_set(:secure => true) do
- get '/set_session_value', headers: { 'HTTPS' => 'on' }
+ with_test_route_set(secure: true) do
+ get "/set_session_value", headers: { "HTTPS" => "on" }
+
assert_response :success
- assert_equal "_myapp_session=#{response.body}; path=/; secure; HttpOnly",
- headers['Set-Cookie']
+ assert_session_cookie "path=/; secure; HttpOnly", "foo" => "bar"
end
end
# {:foo=>#<SessionAutoloadTest::Foo bar:"baz">, :session_id=>"ce8b0752a6ab7c7af3cdb8a80e6b9e46"}
- SignedSerializedCookie = "BAh7BzoIZm9vbzodU2Vzc2lvbkF1dG9sb2FkVGVzdDo6Rm9vBjoJQGJhciIIYmF6Og9zZXNzaW9uX2lkIiVjZThiMDc1MmE2YWI3YzdhZjNjZGI4YTgwZTZiOWU0Ng==--2bf3af1ae8bd4e52b9ac2099258ace0c380e601c"
+ EncryptedSerializedCookie = "9RZ2Fij0qLveUwM4s+CCjGqhpjyUC8jiBIf/AiBr9M3TB8xh2vQZtvSOMfN3uf6oYbbpIDHAcOFIEl69FcW1ozQYeSrCLonYCazoh34ZdYskIQfGwCiSYleVXG1OD9Z4jFqeVArw4Ewm0paOOPLbN1rc6A==--I359v/KWdZ1ok0ey--JFFhuPOY7WUo6tB/eP05Aw=="
def test_deserializes_unloaded_classes_on_get_id
with_test_route_set do
with_autoload_path "session_autoload_test" do
- cookies[SessionKey] = SignedSerializedCookie
- get '/get_session_id'
+ cookies[SessionKey] = EncryptedSerializedCookie
+ get "/get_session_id"
assert_response :success
- assert_equal 'id: ce8b0752a6ab7c7af3cdb8a80e6b9e46', response.body, "should auto-load unloaded class"
+ assert_equal "id: ce8b0752a6ab7c7af3cdb8a80e6b9e46", response.body, "should auto-load unloaded class"
end
end
end
@@ -149,8 +177,8 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
def test_deserializes_unloaded_classes_on_get_value
with_test_route_set do
with_autoload_path "session_autoload_test" do
- cookies[SessionKey] = SignedSerializedCookie
- get '/get_session_value'
+ cookies[SessionKey] = EncryptedSerializedCookie
+ get "/get_session_value"
assert_response :success
assert_equal 'foo: #<SessionAutoloadTest::Foo bar:"baz">', response.body, "should auto-load unloaded class"
end
@@ -160,107 +188,103 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
def test_close_raises_when_data_overflows
with_test_route_set do
assert_raise(ActionDispatch::Cookies::CookieOverflow) {
- get '/raise_data_overflow'
+ get "/raise_data_overflow"
}
end
end
def test_doesnt_write_session_cookie_if_session_is_not_accessed
with_test_route_set do
- get '/no_session_access'
+ 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'
+ get "/no_session_access"
assert_response :success
- assert_equal nil, headers['Set-Cookie']
+ assert_nil headers["Set-Cookie"]
end
end
def test_setting_session_value_after_session_reset
with_test_route_set do
- get '/set_session_value'
+ get "/set_session_value"
assert_response :success
session_payload = response.body
- assert_equal "_myapp_session=#{response.body}; path=/; HttpOnly",
- headers['Set-Cookie']
+ assert_session_cookie "path=/; HttpOnly", "foo" => "bar"
- get '/call_reset_session'
+ get "/call_reset_session"
assert_response :success
- assert_not_equal [], headers['Set-Cookie']
+ assert_not_equal [], headers["Set-Cookie"]
assert_not_nil session_payload
assert_not_equal session_payload, cookies[SessionKey]
- get '/get_session_value'
+ get "/get_session_value"
assert_response :success
- assert_equal 'foo: nil', response.body
+ assert_equal "foo: nil", response.body
end
end
def test_class_type_after_session_reset
with_test_route_set do
- get '/set_session_value'
+ get "/set_session_value"
assert_response :success
- assert_equal "_myapp_session=#{response.body}; path=/; HttpOnly",
- headers['Set-Cookie']
+ assert_session_cookie "path=/; HttpOnly", "foo" => "bar"
- get '/get_class_after_reset_session'
+ get "/get_class_after_reset_session"
assert_response :success
- assert_not_equal [], headers['Set-Cookie']
- assert_equal 'class: ActionDispatch::Request::Session', response.body
+ assert_not_equal [], headers["Set-Cookie"]
+ assert_equal "class: ActionDispatch::Request::Session", response.body
end
end
def test_getting_from_nonexistent_session
with_test_route_set do
- get '/get_session_value'
+ get "/get_session_value"
assert_response :success
- assert_equal 'foo: nil', response.body
- assert_nil headers['Set-Cookie'], "should only create session on write, not read"
+ assert_equal "foo: nil", response.body
+ assert_nil headers["Set-Cookie"], "should only create session on write, not read"
end
end
def test_setting_session_value_after_session_clear
with_test_route_set do
- get '/set_session_value'
+ get "/set_session_value"
assert_response :success
- assert_equal "_myapp_session=#{response.body}; path=/; HttpOnly",
- headers['Set-Cookie']
+ assert_session_cookie "path=/; HttpOnly", "foo" => "bar"
- get '/call_session_clear'
+ get "/call_session_clear"
assert_response :success
- get '/get_session_value'
+ get "/get_session_value"
assert_response :success
- assert_equal 'foo: nil', response.body
+ assert_equal "foo: nil", response.body
end
end
def test_persistent_session_id
with_test_route_set do
- cookies[SessionKey] = SignedBar
- get '/persistent_session_id'
+ get "/set_session_value"
+ get "/persistent_session_id"
assert_response :success
assert_equal 32, response.body.size
session_id = response.body
- get '/persistent_session_id'
+ get "/persistent_session_id"
assert_equal session_id, response.body
reset!
- get '/persistent_session_id'
+ get "/persistent_session_id"
assert_not_equal session_id, response.body
end
end
def test_setting_session_id_to_nil_is_respected
with_test_route_set do
- cookies[SessionKey] = SignedBar
-
+ get "/set_session_value"
get "/get_session_id"
sid = response.body
assert_equal 36, sid.size
@@ -271,60 +295,86 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
end
def test_session_store_with_expire_after
- with_test_route_set(:expire_after => 5.hours) do
+ with_test_route_set(expire_after: 5.hours) do
# First request accesses the session
time = Time.local(2008, 4, 24)
- Time.stubs(:now).returns(time)
- expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d %b %Y %H:%M:%S -0000")
- cookies[SessionKey] = SignedBar
+ Time.stub :now, time do
+ expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d %b %Y %H:%M:%S -0000")
- get '/set_session_value'
- assert_response :success
+ get "/set_session_value"
- cookie_body = response.body
- assert_equal "_myapp_session=#{cookie_body}; path=/; expires=#{expected_expiry}; HttpOnly",
- headers['Set-Cookie']
+ assert_response :success
+ assert_session_cookie "path=/; expires=#{expected_expiry}; HttpOnly", "foo" => "bar"
+ end
# Second request does not access the session
- time = Time.local(2008, 4, 25)
- Time.stubs(:now).returns(time)
- expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d %b %Y %H:%M:%S -0000")
+ time = time + 3.hours
+ Time.stub :now, time do
+ expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d %b %Y %H:%M:%S -0000")
- get '/no_session_access'
- assert_response :success
+ get "/no_session_access"
- assert_equal "_myapp_session=#{cookie_body}; path=/; expires=#{expected_expiry}; HttpOnly",
- headers['Set-Cookie']
+ assert_response :success
+ assert_session_cookie "path=/; expires=#{expected_expiry}; HttpOnly", "foo" => "bar"
+ end
+ end
+ end
+
+ def test_session_store_with_expire_after_does_not_accept_expired_session
+ with_test_route_set(expire_after: 5.hours) do
+ # First request accesses the session
+ time = Time.local(2017, 11, 12)
+
+ Time.stub :now, time do
+ expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d %b %Y %H:%M:%S -0000")
+
+ get "/set_session_value"
+ get "/get_session_value"
+
+ assert_response :success
+ assert_equal 'foo: "bar"', response.body
+ assert_session_cookie "path=/; expires=#{expected_expiry}; HttpOnly", "foo" => "bar"
+ end
+
+ # Second request is beyond the expiry time and the session is invalidated
+ time += 5.hours + 1.minute
+
+ Time.stub :now, time do
+ get "/get_session_value"
+
+ assert_response :success
+ assert_equal "foo: nil", response.body
+ end
end
end
def test_session_store_with_explicit_domain
- with_test_route_set(:domain => "example.es") do
- get '/set_session_value'
- assert_match(/domain=example\.es/, headers['Set-Cookie'])
- headers['Set-Cookie']
+ with_test_route_set(domain: "example.es") do
+ get "/set_session_value"
+ assert_match(/domain=example\.es/, headers["Set-Cookie"])
+ headers["Set-Cookie"]
end
end
def test_session_store_without_domain
with_test_route_set do
- get '/set_session_value'
- assert_no_match(/domain\=/, headers['Set-Cookie'])
+ get "/set_session_value"
+ assert_no_match(/domain\=/, headers["Set-Cookie"])
end
end
def test_session_store_with_nil_domain
- with_test_route_set(:domain => nil) do
- get '/set_session_value'
- assert_no_match(/domain\=/, headers['Set-Cookie'])
+ with_test_route_set(domain: nil) do
+ get "/set_session_value"
+ assert_no_match(/domain\=/, headers["Set-Cookie"])
end
end
def test_session_store_with_all_domains
- with_test_route_set(:domain => :all) do
- get '/set_session_value'
- assert_match(/domain=\.example\.com/, headers['Set-Cookie'])
+ with_test_route_set(domain: :all) do
+ get "/set_session_value"
+ assert_match(/domain=\.example\.com/, headers["Set-Cookie"])
end
end
@@ -334,25 +384,34 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
def get(path, *args)
args[0] ||= {}
args[0][:headers] ||= {}
- args[0][:headers]["action_dispatch.key_generator"] ||= Generator
+ args[0][:headers].tap do |config|
+ config["action_dispatch.secret_key_base"] = SessionSecret
+ config["action_dispatch.authenticated_encrypted_cookie_salt"] = SessionSalt
+ config["action_dispatch.use_authenticated_cookie_encryption"] = true
+
+ config["action_dispatch.key_generator"] ||= Generator
+ config["action_dispatch.cookies_rotations"] ||= Rotations
+ end
+
super(path, *args)
end
def with_test_route_set(options = {})
with_routing do |set|
set.draw do
- get ':action', :to => ::CookieStoreTest::TestController
+ ActiveSupport::Deprecation.silence do
+ get ":action", to: ::CookieStoreTest::TestController
+ end
end
- options = { :key => SessionKey }.merge!(options)
+ options = { key: SessionKey }.merge!(options)
@app = self.class.build_app(set) do |middleware|
middleware.use ActionDispatch::Session::CookieStore, options
- middleware.delete "ActionDispatch::ShowExceptions"
+ middleware.delete ActionDispatch::ShowExceptions
end
yield
end
end
-
end
diff --git a/actionpack/test/dispatch/session/mem_cache_store_test.rb b/actionpack/test/dispatch/session/mem_cache_store_test.rb
index 9a5d5131c0..9b51ee1cad 100644
--- a/actionpack/test/dispatch/session/mem_cache_store_test.rb
+++ b/actionpack/test/dispatch/session/mem_cache_store_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'securerandom'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "securerandom"
# You need to start a memcached server inorder to run these tests
class MemCacheStoreTest < ActionDispatch::IntegrationTest
@@ -19,11 +21,11 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest
end
def get_session_value
- render :text => "foo: #{session[:foo].inspect}"
+ render plain: "foo: #{session[:foo].inspect}"
end
def get_session_id
- render :text => "#{request.session.id}"
+ render plain: "#{request.session.id}"
end
def call_reset_session
@@ -35,17 +37,17 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest
end
begin
- require 'dalli'
- ss = Dalli::Client.new('localhost:11211').stats
- raise Dalli::DalliError unless ss['localhost:11211']
+ require "dalli"
+ ss = Dalli::Client.new("localhost:11211").stats
+ raise Dalli::DalliError unless ss["localhost:11211"]
def test_setting_and_getting_session_value
with_test_route_set do
- get '/set_session_value'
+ get "/set_session_value"
assert_response :success
- assert cookies['_session_id']
+ assert cookies["_session_id"]
- get '/get_session_value'
+ get "/get_session_value"
assert_response :success
assert_equal 'foo: "bar"', response.body
end
@@ -55,9 +57,9 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest
def test_getting_nil_session_value
with_test_route_set do
- get '/get_session_value'
+ get "/get_session_value"
assert_response :success
- assert_equal 'foo: nil', response.body
+ assert_equal "foo: nil", response.body
end
rescue Dalli::RingError => ex
skip ex.message, ex.backtrace
@@ -65,20 +67,20 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest
def test_getting_session_value_after_session_reset
with_test_route_set do
- get '/set_session_value'
+ get "/set_session_value"
assert_response :success
- assert cookies['_session_id']
- session_cookie = cookies.send(:hash_for)['_session_id']
+ assert cookies["_session_id"]
+ session_cookie = cookies.send(:hash_for)["_session_id"]
- get '/call_reset_session'
+ get "/call_reset_session"
assert_response :success
- assert_not_equal [], headers['Set-Cookie']
+ assert_not_equal [], headers["Set-Cookie"]
cookies << session_cookie # replace our new session_id with our old, pre-reset session_id
- get '/get_session_value'
+ get "/get_session_value"
assert_response :success
- assert_equal 'foo: nil', response.body, "data for this session should have been obliterated from memcached"
+ assert_equal "foo: nil", response.body, "data for this session should have been obliterated from memcached"
end
rescue Dalli::RingError => ex
skip ex.message, ex.backtrace
@@ -86,10 +88,10 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest
def test_getting_from_nonexistent_session
with_test_route_set do
- get '/get_session_value'
+ get "/get_session_value"
assert_response :success
- assert_equal 'foo: nil', response.body
- assert_nil cookies['_session_id'], "should only create session on write, not read"
+ assert_equal "foo: nil", response.body
+ assert_nil cookies["_session_id"], "should only create session on write, not read"
end
rescue Dalli::RingError => ex
skip ex.message, ex.backtrace
@@ -97,20 +99,20 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest
def test_setting_session_value_after_session_reset
with_test_route_set do
- get '/set_session_value'
+ get "/set_session_value"
assert_response :success
- assert cookies['_session_id']
- session_id = cookies['_session_id']
+ assert cookies["_session_id"]
+ session_id = cookies["_session_id"]
- get '/call_reset_session'
+ get "/call_reset_session"
assert_response :success
- assert_not_equal [], headers['Set-Cookie']
+ assert_not_equal [], headers["Set-Cookie"]
- get '/get_session_value'
+ get "/get_session_value"
assert_response :success
- assert_equal 'foo: nil', response.body
+ assert_equal "foo: nil", response.body
- get '/get_session_id'
+ get "/get_session_id"
assert_response :success
assert_not_equal session_id, response.body
end
@@ -120,12 +122,12 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest
def test_getting_session_id
with_test_route_set do
- get '/set_session_value'
+ get "/set_session_value"
assert_response :success
- assert cookies['_session_id']
- session_id = cookies['_session_id']
+ assert cookies["_session_id"]
+ session_id = cookies["_session_id"]
- get '/get_session_id'
+ get "/get_session_id"
assert_response :success
assert_equal session_id, response.body, "should be able to read session id without accessing the session hash"
end
@@ -136,12 +138,12 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest
def test_deserializes_unloaded_class
with_test_route_set do
with_autoload_path "session_autoload_test" do
- get '/set_serialized_session_value'
+ get "/set_serialized_session_value"
assert_response :success
- assert cookies['_session_id']
+ assert cookies["_session_id"]
end
with_autoload_path "session_autoload_test" do
- get '/get_session_id'
+ get "/get_session_id"
assert_response :success
end
end
@@ -151,13 +153,13 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest
def test_doesnt_write_session_cookie_if_session_id_is_already_exists
with_test_route_set do
- get '/set_session_value'
+ get "/set_session_value"
assert_response :success
- assert cookies['_session_id']
+ assert cookies["_session_id"]
- get '/get_session_value'
+ 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
@@ -165,16 +167,16 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest
def test_prevents_session_fixation
with_test_route_set do
- get '/get_session_value'
+ get "/get_session_value"
assert_response :success
- assert_equal 'foo: nil', response.body
- session_id = cookies['_session_id']
+ assert_equal "foo: nil", response.body
+ session_id = cookies["_session_id"]
reset!
- get '/set_session_value', params: { _session_id: session_id }
+ get "/set_session_value", params: { _session_id: session_id }
assert_response :success
- assert_not_equal session_id, cookies['_session_id']
+ assert_not_equal session_id, cookies["_session_id"]
end
rescue Dalli::RingError => ex
skip ex.message, ex.backtrace
@@ -187,12 +189,14 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest
def with_test_route_set
with_routing do |set|
set.draw do
- get ':action', :to => ::MemCacheStoreTest::TestController
+ ActiveSupport::Deprecation.silence do
+ get ":action", to: ::MemCacheStoreTest::TestController
+ end
end
@app = self.class.build_app(set) do |middleware|
- middleware.use ActionDispatch::Session::MemCacheStore, :key => '_session_id', :namespace => "mem_cache_store_test:#{SecureRandom.hex(10)}"
- middleware.delete "ActionDispatch::ShowExceptions"
+ middleware.use ActionDispatch::Session::MemCacheStore, key: "_session_id", namespace: "mem_cache_store_test:#{SecureRandom.hex(10)}"
+ middleware.delete ActionDispatch::ShowExceptions
end
yield
diff --git a/actionpack/test/dispatch/session/test_session_test.rb b/actionpack/test/dispatch/session/test_session_test.rb
index d30461a623..e90162a5fe 100644
--- a/actionpack/test/dispatch/session/test_session_test.rb
+++ b/actionpack/test/dispatch/session/test_session_test.rb
@@ -1,43 +1,65 @@
-require 'abstract_unit'
-require 'stringio'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "stringio"
class ActionController::TestSessionTest < ActiveSupport::TestCase
def test_initialize_with_values
- session = ActionController::TestSession.new(one: 'one', two: 'two')
- assert_equal('one', session[:one])
- assert_equal('two', session[:two])
+ session = ActionController::TestSession.new(one: "one", two: "two")
+ assert_equal("one", session[:one])
+ assert_equal("two", session[:two])
end
def test_setting_session_item_sets_item
session = ActionController::TestSession.new
- session[:key] = 'value'
- assert_equal('value', session[:key])
+ session[:key] = "value"
+ assert_equal("value", session[:key])
end
def test_calling_delete_removes_item_and_returns_its_value
session = ActionController::TestSession.new
- session[:key] = 'value'
- assert_equal('value', session[:key])
- assert_equal('value', session.delete(:key))
+ session[:key] = "value"
+ assert_equal("value", session[:key])
+ assert_equal("value", session.delete(:key))
assert_nil(session[:key])
end
def test_calling_update_with_params_passes_to_attributes
session = ActionController::TestSession.new
- session.update('key' => 'value')
- assert_equal('value', session[:key])
+ session.update("key" => "value")
+ assert_equal("value", session[:key])
end
def test_clear_empties_session
- session = ActionController::TestSession.new(one: 'one', two: 'two')
+ session = ActionController::TestSession.new(one: "one", two: "two")
session.clear
assert_nil(session[:one])
assert_nil(session[:two])
end
def test_keys_and_values
- session = ActionController::TestSession.new(one: '1', two: '2')
+ session = ActionController::TestSession.new(one: "1", two: "2")
assert_equal %w(one two), session.keys
assert_equal %w(1 2), session.values
end
+
+ def test_fetch_returns_default
+ session = ActionController::TestSession.new(one: "1")
+ assert_equal("2", session.fetch(:two, "2"))
+ end
+
+ def test_fetch_on_symbol_returns_value
+ session = ActionController::TestSession.new(one: "1")
+ assert_equal("1", session.fetch(:one))
+ end
+
+ def test_fetch_on_string_returns_value
+ session = ActionController::TestSession.new(one: "1")
+ assert_equal("1", session.fetch("one"))
+ end
+
+ def test_fetch_returns_block_value
+ session = ActionController::TestSession.new(one: "1")
+ assert_equal(2, session.fetch("2") { |key| key.to_i })
+ end
end
diff --git a/actionpack/test/dispatch/show_exceptions_test.rb b/actionpack/test/dispatch/show_exceptions_test.rb
index cbb12a2209..b69071b44b 100644
--- a/actionpack/test/dispatch/show_exceptions_test.rb
+++ b/actionpack/test/dispatch/show_exceptions_test.rb
@@ -1,21 +1,30 @@
-require 'abstract_unit'
+# frozen_string_literal: true
-class ShowExceptionsTest < ActionDispatch::IntegrationTest
+require "abstract_unit"
+class ShowExceptionsTest < ActionDispatch::IntegrationTest
class Boomer
def call(env)
req = ActionDispatch::Request.new(env)
case req.path
when "/not_found"
raise AbstractController::ActionNotFound
- when "/bad_params"
- raise ActionDispatch::ParamsParser::ParseError.new("", StandardError.new)
+ when "/bad_params", "/bad_params.json"
+ begin
+ raise StandardError.new
+ rescue
+ raise ActionDispatch::Http::Parameters::ParseError
+ end
when "/method_not_allowed"
- raise ActionController::MethodNotAllowed, 'PUT'
+ raise ActionController::MethodNotAllowed, "PUT"
when "/unknown_http_method"
raise ActionController::UnknownHttpMethod
when "/not_found_original_exception"
- raise ActionView::Template::Error.new('template', AbstractController::ActionNotFound.new)
+ begin
+ raise AbstractController::ActionNotFound.new
+ rescue
+ raise ActionView::Template::Error.new("template")
+ end
else
raise "puke!"
end
@@ -27,30 +36,30 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest
test "skip exceptions app if not showing exceptions" do
@app = ProductionApp
assert_raise RuntimeError do
- get "/", headers: { 'action_dispatch.show_exceptions' => false }
+ get "/", headers: { "action_dispatch.show_exceptions" => false }
end
end
test "rescue with error page" do
@app = ProductionApp
- get "/", headers: { 'action_dispatch.show_exceptions' => true }
+ get "/", headers: { "action_dispatch.show_exceptions" => true }
assert_response 500
assert_equal "500 error fixture\n", body
- get "/bad_params", headers: { 'action_dispatch.show_exceptions' => true }
+ get "/bad_params", headers: { "action_dispatch.show_exceptions" => true }
assert_response 400
assert_equal "400 error fixture\n", body
- get "/not_found", headers: { 'action_dispatch.show_exceptions' => true }
+ get "/not_found", headers: { "action_dispatch.show_exceptions" => true }
assert_response 404
assert_equal "404 error fixture\n", body
- get "/method_not_allowed", headers: { 'action_dispatch.show_exceptions' => true }
+ get "/method_not_allowed", headers: { "action_dispatch.show_exceptions" => true }
assert_response 405
assert_equal "", body
- get "/unknown_http_method", headers: { 'action_dispatch.show_exceptions' => true }
+ get "/unknown_http_method", headers: { "action_dispatch.show_exceptions" => true }
assert_response 405
assert_equal "", body
end
@@ -61,11 +70,11 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest
begin
@app = ProductionApp
- get "/", headers: { 'action_dispatch.show_exceptions' => true }
+ get "/", headers: { "action_dispatch.show_exceptions" => true }
assert_response 500
assert_equal "500 localized error fixture\n", body
- get "/not_found", headers: { 'action_dispatch.show_exceptions' => true }
+ get "/not_found", headers: { "action_dispatch.show_exceptions" => true }
assert_response 404
assert_equal "404 error fixture\n", body
ensure
@@ -76,14 +85,14 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest
test "sets the HTTP charset parameter" do
@app = ProductionApp
- get "/", headers: { 'action_dispatch.show_exceptions' => true }
+ get "/", headers: { "action_dispatch.show_exceptions" => true }
assert_equal "text/html; charset=utf-8", response.headers["Content-Type"]
end
test "show registered original exception for wrapped exceptions" do
@app = ProductionApp
- get "/not_found_original_exception", headers: { 'action_dispatch.show_exceptions' => true }
+ get "/not_found_original_exception", headers: { "action_dispatch.show_exceptions" => true }
assert_response 404
assert_match(/404 error/, body)
end
@@ -93,13 +102,13 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest
assert_kind_of AbstractController::ActionNotFound, env["action_dispatch.exception"]
assert_equal "/404", env["PATH_INFO"]
assert_equal "/not_found_original_exception", env["action_dispatch.original_path"]
- [404, { "Content-Type" => "text/plain" }, ["YOU FAILED BRO"]]
+ [404, { "Content-Type" => "text/plain" }, ["YOU FAILED"]]
end
@app = ActionDispatch::ShowExceptions.new(Boomer.new, exceptions_app)
- get "/not_found_original_exception", headers: { 'action_dispatch.show_exceptions' => true }
+ get "/not_found_original_exception", headers: { "action_dispatch.show_exceptions" => true }
assert_response 404
- assert_equal "YOU FAILED BRO", body
+ assert_equal "YOU FAILED", body
end
test "returns an empty response if custom exceptions app returns X-Cascade pass" do
@@ -108,8 +117,22 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest
end
@app = ActionDispatch::ShowExceptions.new(Boomer.new, exceptions_app)
- get "/method_not_allowed", headers: { 'action_dispatch.show_exceptions' => true }
+ get "/method_not_allowed", headers: { "action_dispatch.show_exceptions" => true }
assert_response 405
assert_equal "", body
end
+
+ test "bad params exception is returned in the correct format" do
+ @app = ProductionApp
+
+ get "/bad_params", headers: { "action_dispatch.show_exceptions" => true }
+ assert_equal "text/html; charset=utf-8", response.headers["Content-Type"]
+ assert_response 400
+ assert_match(/400 error/, body)
+
+ get "/bad_params.json", headers: { "action_dispatch.show_exceptions" => true }
+ assert_equal "application/json; charset=utf-8", response.headers["Content-Type"]
+ assert_response 400
+ assert_equal("{\"status\":400,\"error\":\"Bad Request\"}", body)
+ end
end
diff --git a/actionpack/test/dispatch/ssl_test.rb b/actionpack/test/dispatch/ssl_test.rb
index 017e9ba2dd..baf46e7c7e 100644
--- a/actionpack/test/dispatch/ssl_test.rb
+++ b/actionpack/test/dispatch/ssl_test.rb
@@ -1,230 +1,228 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class SSLTest < ActionDispatch::IntegrationTest
- def default_app
- lambda { |env|
- headers = {'Content-Type' => "text/html"}
- headers['Set-Cookie'] = "id=1; path=/\ntoken=abc; path=/; secure; HttpOnly"
- [200, headers, ["OK"]]
- }
+ HEADERS = Rack::Utils::HeaderHash.new "Content-Type" => "text/html"
+
+ attr_accessor :app
+
+ def build_app(headers: {}, ssl_options: {})
+ headers = HEADERS.merge(headers)
+ ActionDispatch::SSL.new lambda { |env| [200, headers, []] }, ssl_options.reverse_merge(hsts: { subdomains: true })
end
+end
- def app
- @app ||= ActionDispatch::SSL.new(default_app)
+class RedirectSSLTest < SSLTest
+ def assert_not_redirected(url, headers: {}, redirect: {})
+ self.app = build_app ssl_options: { redirect: redirect }
+ get url, headers: headers
+ assert_response :ok
end
- attr_writer :app
- def test_allows_https_url
- get "https://example.org/path?key=value"
- assert_response :success
+ def assert_redirected(redirect: {}, from: "http://a/b?c=d", to: from.sub("http", "https"))
+ redirect = { status: 301, body: [] }.merge(redirect)
+
+ self.app = build_app ssl_options: { redirect: redirect }
+
+ get from
+ assert_response redirect[:status] || 301
+ assert_redirected_to to
+ assert_equal redirect[:body].join, @response.body
end
- def test_allows_https_proxy_header_url
- get "http://example.org/", headers: { 'HTTP_X_FORWARDED_PROTO' => "https" }
- assert_response :success
+ def assert_post_redirected(redirect: {}, from: "http://a/b?c=d",
+ to: from.sub("http", "https"))
+
+ self.app = build_app ssl_options: { redirect: redirect }
+
+ post from
+ assert_response redirect[:status] || 307
+ assert_redirected_to to
end
- def test_redirects_http_to_https
- get "http://example.org/path?key=value"
- assert_response :redirect
- assert_equal "https://example.org/path?key=value",
- response.headers['Location']
+ test "exclude can avoid redirect" do
+ excluding = { exclude: -> request { request.path =~ /healthcheck/ } }
+
+ assert_not_redirected "http://example.org/healthcheck", redirect: excluding
+ assert_redirected from: "http://example.org/", redirect: excluding
end
- def test_hsts_header_by_default
- get "https://example.org/"
- assert_equal "max-age=31536000",
- response.headers['Strict-Transport-Security']
+ test "https is not redirected" do
+ assert_not_redirected "https://example.org"
end
- def test_no_hsts_with_insecure_connection
- get "http://example.org/"
- assert_not response.headers['Strict-Transport-Security']
+ test "proxied https is not redirected" do
+ assert_not_redirected "http://example.org", headers: { "HTTP_X_FORWARDED_PROTO" => "https" }
end
- def test_hsts_header
- self.app = ActionDispatch::SSL.new(default_app, :hsts => true)
- get "https://example.org/"
- assert_equal "max-age=31536000",
- response.headers['Strict-Transport-Security']
+ test "http is redirected to https" do
+ assert_redirected
end
- def test_disable_hsts_header
- self.app = ActionDispatch::SSL.new(default_app, :hsts => false)
- get "https://example.org/"
- assert_not response.headers['Strict-Transport-Security']
+ test "http POST is redirected to https with status 307" do
+ assert_post_redirected
end
- def test_hsts_expires
- self.app = ActionDispatch::SSL.new(default_app, :hsts => { :expires => 500 })
- get "https://example.org/"
- assert_equal "max-age=500",
- response.headers['Strict-Transport-Security']
+ test "redirect with non-301 status" do
+ assert_redirected redirect: { status: 307 }
end
- def test_hsts_expires_with_duration
- self.app = ActionDispatch::SSL.new(default_app, :hsts => { :expires => 1.year })
- get "https://example.org/"
- assert_equal "max-age=31557600",
- response.headers['Strict-Transport-Security']
+ test "redirect with custom body" do
+ assert_redirected redirect: { body: ["foo"] }
end
- def test_hsts_include_subdomains
- self.app = ActionDispatch::SSL.new(default_app, :hsts => { :subdomains => true })
- get "https://example.org/"
- assert_equal "max-age=31536000; includeSubDomains",
- response.headers['Strict-Transport-Security']
+ test "redirect to specific host" do
+ assert_redirected redirect: { host: "ssl" }, to: "https://ssl/b?c=d"
end
- def test_flag_cookies_as_secure
- get "https://example.org/"
- assert_equal ["id=1; path=/; secure", "token=abc; path=/; secure; HttpOnly" ],
- response.headers['Set-Cookie'].split("\n")
+ test "redirect to default port" do
+ assert_redirected redirect: { port: 443 }
end
- def test_flag_cookies_as_secure_at_end_of_line
- self.app = ActionDispatch::SSL.new(lambda { |env|
- headers = {
- 'Content-Type' => "text/html",
- 'Set-Cookie' => "problem=def; path=/; HttpOnly; secure"
- }
- [200, headers, ["OK"]]
- })
+ test "redirect to non-default port" do
+ assert_redirected redirect: { port: 8443 }, to: "https://a:8443/b?c=d"
+ end
- get "https://example.org/"
- assert_equal ["problem=def; path=/; HttpOnly; secure"],
- response.headers['Set-Cookie'].split("\n")
+ test "redirect to different host and non-default port" do
+ assert_redirected redirect: { host: "ssl", port: 8443 }, to: "https://ssl:8443/b?c=d"
end
- def test_flag_cookies_as_secure_with_more_spaces_before
- self.app = ActionDispatch::SSL.new(lambda { |env|
- headers = {
- 'Content-Type' => "text/html",
- 'Set-Cookie' => "problem=def; path=/; HttpOnly; secure"
- }
- [200, headers, ["OK"]]
- })
+ test "redirect to different host including port" do
+ assert_redirected redirect: { host: "ssl:443" }, to: "https://ssl:443/b?c=d"
+ end
- get "https://example.org/"
- assert_equal ["problem=def; path=/; HttpOnly; secure"],
- response.headers['Set-Cookie'].split("\n")
+ test "no redirect with redirect set to false" do
+ assert_not_redirected "http://example.org", redirect: false
end
+end
- def test_flag_cookies_as_secure_with_more_spaces_after
- self.app = ActionDispatch::SSL.new(lambda { |env|
- headers = {
- 'Content-Type' => "text/html",
- 'Set-Cookie' => "problem=def; path=/; secure; HttpOnly"
- }
- [200, headers, ["OK"]]
- })
+class StrictTransportSecurityTest < SSLTest
+ EXPECTED = "max-age=31536000"
+ EXPECTED_WITH_SUBDOMAINS = "max-age=31536000; includeSubDomains"
- get "https://example.org/"
- assert_equal ["problem=def; path=/; secure; HttpOnly"],
- response.headers['Set-Cookie'].split("\n")
+ def assert_hsts(expected, url: "https://example.org", hsts: { subdomains: true }, headers: {})
+ self.app = build_app ssl_options: { hsts: hsts }, headers: headers
+ get url
+ 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
+ assert_hsts EXPECTED_WITH_SUBDOMAINS
+ end
- def test_flag_cookies_as_secure_with_has_not_spaces_before
- self.app = ActionDispatch::SSL.new(lambda { |env|
- headers = {
- 'Content-Type' => "text/html",
- 'Set-Cookie' => "problem=def; path=/;secure; HttpOnly"
- }
- [200, headers, ["OK"]]
- })
+ test "not sent with http:// responses" do
+ assert_hsts nil, url: "http://example.org"
+ end
- get "https://example.org/"
- assert_equal ["problem=def; path=/;secure; HttpOnly"],
- response.headers['Set-Cookie'].split("\n")
+ test "defers to app-provided header" do
+ assert_hsts "app-provided", headers: { "Strict-Transport-Security" => "app-provided" }
end
- def test_flag_cookies_as_secure_with_has_not_spaces_after
- self.app = ActionDispatch::SSL.new(lambda { |env|
- headers = {
- 'Content-Type' => "text/html",
- 'Set-Cookie' => "problem=def; path=/; secure;HttpOnly"
- }
- [200, headers, ["OK"]]
- })
+ test "hsts: true enables default settings" do
+ assert_hsts EXPECTED_WITH_SUBDOMAINS, hsts: true
+ end
- get "https://example.org/"
- assert_equal ["problem=def; path=/; secure;HttpOnly"],
- response.headers['Set-Cookie'].split("\n")
+ test "hsts: false sets max-age to zero, clearing browser HSTS settings" do
+ assert_hsts "max-age=0; includeSubDomains", hsts: false
end
- def test_flag_cookies_as_secure_with_ignore_case
- self.app = ActionDispatch::SSL.new(lambda { |env|
- headers = {
- 'Content-Type' => "text/html",
- 'Set-Cookie' => "problem=def; path=/; Secure; HttpOnly"
- }
- [200, headers, ["OK"]]
- })
+ test ":expires sets max-age" do
+ assert_hsts "max-age=500; includeSubDomains", hsts: { expires: 500 }
+ end
- get "https://example.org/"
- assert_equal ["problem=def; path=/; Secure; HttpOnly"],
- response.headers['Set-Cookie'].split("\n")
+ test ":expires supports AS::Duration arguments" do
+ assert_hsts "max-age=31556952; includeSubDomains", hsts: { expires: 1.year }
end
- def test_no_cookies
- self.app = ActionDispatch::SSL.new(lambda { |env|
- [200, {'Content-Type' => "text/html"}, ["OK"]]
- })
- get "https://example.org/"
- assert !response.headers['Set-Cookie']
+ test "include subdomains" do
+ assert_hsts "#{EXPECTED}; includeSubDomains", hsts: { subdomains: true }
+ end
+
+ test "exclude subdomains" do
+ assert_hsts EXPECTED, hsts: { subdomains: false }
+ end
+
+ test "opt in to browser preload lists" do
+ assert_hsts "#{EXPECTED_WITH_SUBDOMAINS}; preload", hsts: { preload: true }
end
- def test_redirect_to_host
- self.app = ActionDispatch::SSL.new(default_app, :host => "ssl.example.org")
- get "http://example.org/path?key=value"
- assert_equal "https://ssl.example.org/path?key=value",
- response.headers['Location']
+ test "opt out of browser preload lists" do
+ assert_hsts EXPECTED_WITH_SUBDOMAINS, hsts: { preload: false }
end
+end
- def test_redirect_to_port
- self.app = ActionDispatch::SSL.new(default_app, :port => 8443)
- get "http://example.org/path?key=value"
- assert_equal "https://example.org:8443/path?key=value",
- response.headers['Location']
+class SecureCookiesTest < SSLTest
+ DEFAULT = %(id=1; path=/\ntoken=abc; path=/; secure; HttpOnly)
+
+ def get(**options)
+ self.app = build_app(**options)
+ super "https://example.org"
end
- def test_redirect_to_host_and_port
- self.app = ActionDispatch::SSL.new(default_app, :host => "ssl.example.org", :port => 8443)
- get "http://example.org/path?key=value"
- assert_equal "https://ssl.example.org:8443/path?key=value",
- response.headers['Location']
+ def assert_cookies(*expected)
+ assert_equal expected, response.headers["Set-Cookie"].split("\n")
end
- def test_redirect_to_host_with_port
- self.app = ActionDispatch::SSL.new(default_app, :host => "ssl.example.org:443")
- get "http://example.org/path?key=value"
- assert_equal "https://ssl.example.org:443/path?key=value",
- response.headers['Location']
+ def test_flag_cookies_as_secure
+ get headers: { "Set-Cookie" => DEFAULT }
+ assert_cookies "id=1; path=/; secure", "token=abc; path=/; secure; HttpOnly"
end
- def test_redirect_to_secure_host_when_on_subdomain
- self.app = ActionDispatch::SSL.new(default_app, :host => "ssl.example.org")
- get "http://ssl.example.org/path?key=value"
- assert_equal "https://ssl.example.org/path?key=value",
- response.headers['Location']
+ def test_flag_cookies_as_secure_at_end_of_line
+ get headers: { "Set-Cookie" => "problem=def; path=/; HttpOnly; secure" }
+ assert_cookies "problem=def; path=/; HttpOnly; secure"
end
- def test_redirect_to_secure_subdomain_when_on_deep_subdomain
- self.app = ActionDispatch::SSL.new(default_app, :host => "example.co.uk")
- get "http://double.rainbow.what.does.it.mean.example.co.uk/path?key=value"
- assert_equal "https://example.co.uk/path?key=value",
- response.headers['Location']
+ def test_flag_cookies_as_secure_with_more_spaces_before
+ get headers: { "Set-Cookie" => "problem=def; path=/; HttpOnly; secure" }
+ assert_cookies "problem=def; path=/; HttpOnly; secure"
end
- def test_keeps_original_headers_behavior
- headers = Rack::Utils::HeaderHash.new(
- "Content-Type" => "text/html",
- "Connection" => ["close"]
- )
- self.app = ActionDispatch::SSL.new(lambda { |env| [200, headers, ["OK"]] })
+ def test_flag_cookies_as_secure_with_more_spaces_after
+ get headers: { "Set-Cookie" => "problem=def; path=/; secure; HttpOnly" }
+ assert_cookies "problem=def; path=/; secure; HttpOnly"
+ end
- get "https://example.org/"
+ def test_flag_cookies_as_secure_with_has_not_spaces_before
+ get headers: { "Set-Cookie" => "problem=def; path=/;secure; HttpOnly" }
+ assert_cookies "problem=def; path=/;secure; HttpOnly"
+ end
+
+ def test_flag_cookies_as_secure_with_has_not_spaces_after
+ get headers: { "Set-Cookie" => "problem=def; path=/; secure;HttpOnly" }
+ assert_cookies "problem=def; path=/; secure;HttpOnly"
+ end
+
+ def test_flag_cookies_as_secure_with_ignore_case
+ get headers: { "Set-Cookie" => "problem=def; path=/; Secure; HttpOnly" }
+ assert_cookies "problem=def; path=/; Secure; HttpOnly"
+ end
+
+ def test_cookies_as_not_secure_with_secure_cookies_disabled
+ get headers: { "Set-Cookie" => DEFAULT }, ssl_options: { secure_cookies: false }
+ assert_cookies(*DEFAULT.split("\n"))
+ end
+
+ def test_cookies_as_not_secure_with_exclude
+ excluding = { exclude: -> request { request.domain =~ /example/ } }
+ get headers: { "Set-Cookie" => DEFAULT }, ssl_options: { redirect: excluding }
+
+ assert_cookies(*DEFAULT.split("\n"))
+ assert_response :ok
+ end
+
+ def test_no_cookies
+ get
+ assert_nil response.headers["Set-Cookie"]
+ end
+
+ def test_keeps_original_headers_behavior
+ get headers: { "Connection" => %w[close] }
assert_equal "close", response.headers["Connection"]
end
end
diff --git a/actionpack/test/dispatch/static_test.rb b/actionpack/test/dispatch/static_test.rb
index 95971b3a0e..d44aa00122 100644
--- a/actionpack/test/dispatch/static_test.rb
+++ b/actionpack/test/dispatch/static_test.rb
@@ -1,7 +1,13 @@
-require 'abstract_unit'
-require 'zlib'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "zlib"
module StaticTests
+ DummyApp = lambda { |env|
+ [200, { "Content-Type" => "text/plain" }, ["Hello, World!"]]
+ }
+
def setup
silence_warnings do
@default_internal_encoding = Encoding.default_internal
@@ -25,7 +31,7 @@ module StaticTests
end
def test_handles_urls_with_ascii_8bit
- assert_equal "Hello, World!", get("/doorkeeper%E3E4".force_encoding('ASCII-8BIT')).body
+ assert_equal "Hello, World!", get((+"/doorkeeper%E3E4").force_encoding("ASCII-8BIT")).body
end
def test_handles_urls_with_ascii_8bit_on_win_31j
@@ -33,13 +39,11 @@ module StaticTests
Encoding.default_internal = "Windows-31J"
Encoding.default_external = "Windows-31J"
end
- assert_equal "Hello, World!", get("/doorkeeper%E3E4".force_encoding('ASCII-8BIT')).body
+ assert_equal "Hello, World!", get((+"/doorkeeper%E3E4").force_encoding("ASCII-8BIT")).body
end
- def test_sets_cache_control
- response = get("/index.html")
- assert_html "/index.html", response
- assert_equal "public, max-age=60", response.headers["Cache-Control"]
+ def test_handles_urls_with_null_byte
+ assert_equal "Hello, World!", get("/doorkeeper%00").body
end
def test_serves_static_index_at_root
@@ -67,11 +71,17 @@ module StaticTests
end
def test_served_static_file_with_non_english_filename
- jruby_skip "Stop skipping if following bug gets fixed: " \
- "http://jira.codehaus.org/browse/JRUBY-7192"
- assert_html "means hello in Japanese\n", get("/foo/#{Rack::Utils.escape("こんにちは.html")}")
+ assert_html "means hello in Japanese\n", get("/foo/%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF.html")
end
+ def test_served_gzipped_static_file_with_non_english_filename
+ response = get("/foo/%E3%81%95%E3%82%88%E3%81%86%E3%81%AA%E3%82%89.html", "HTTP_ACCEPT_ENCODING" => "gzip")
+
+ assert_gzip "/foo/さようなら.html", response
+ assert_equal "text/html", response.headers["Content-Type"]
+ assert_equal "Accept-Encoding", response.headers["Vary"]
+ assert_equal "gzip", response.headers["Content-Encoding"]
+ end
def test_serves_static_file_with_exclamation_mark_in_filename
with_static_file "/foo/foo!bar.html" do |file|
@@ -138,50 +148,74 @@ module StaticTests
def test_serves_gzip_files_when_header_set
file_name = "/gzip/application-a71b3024f80aea3181c09774ca17e712.js"
- response = get(file_name, 'HTTP_ACCEPT_ENCODING' => 'gzip')
+ response = get(file_name, "HTTP_ACCEPT_ENCODING" => "gzip")
assert_gzip file_name, response
- assert_equal 'application/javascript', response.headers['Content-Type']
- assert_equal 'Accept-Encoding', response.headers["Vary"]
- assert_equal 'gzip', response.headers["Content-Encoding"]
+ assert_equal "application/javascript", response.headers["Content-Type"]
+ assert_equal "Accept-Encoding", response.headers["Vary"]
+ assert_equal "gzip", response.headers["Content-Encoding"]
- response = get(file_name, 'HTTP_ACCEPT_ENCODING' => 'Gzip')
- assert_gzip file_name, response
+ response = get(file_name, "HTTP_ACCEPT_ENCODING" => "Gzip")
+ assert_gzip file_name, response
- response = get(file_name, 'HTTP_ACCEPT_ENCODING' => 'GZIP')
- assert_gzip file_name, response
+ response = get(file_name, "HTTP_ACCEPT_ENCODING" => "GZIP")
+ assert_gzip file_name, response
+
+ response = get(file_name, "HTTP_ACCEPT_ENCODING" => "compress;q=0.5, gzip;q=1.0")
+ assert_gzip file_name, response
- response = get(file_name, 'HTTP_ACCEPT_ENCODING' => '')
- assert_not_equal 'gzip', response.headers["Content-Encoding"]
+ response = get(file_name, "HTTP_ACCEPT_ENCODING" => "")
+ assert_not_equal "gzip", response.headers["Content-Encoding"]
end
def test_does_not_modify_path_info
file_name = "/gzip/application-a71b3024f80aea3181c09774ca17e712.js"
- env = {'PATH_INFO' => file_name, 'HTTP_ACCEPT_ENCODING' => 'gzip'}
+ env = { "PATH_INFO" => file_name, "HTTP_ACCEPT_ENCODING" => "gzip", "REQUEST_METHOD" => "POST" }
@app.call(env)
- assert_equal file_name, env['PATH_INFO']
+ 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
+ response = get(file_name, "HTTP_ACCEPT_ENCODING" => "gzip")
+ assert_gzip file_name, response
default_response = get(file_name) # no gzip
- assert_equal default_response.headers['Content-Type'], response.headers['Content-Type']
+ assert_equal default_response.headers["Content-Type"], response.headers["Content-Type"]
end
def test_serves_gzip_files_with_not_modified
file_name = "/gzip/application-a71b3024f80aea3181c09774ca17e712.js"
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)
+ 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
+ headers = {
+ "Access-Control-Allow-Origin" => "http://rubyonrails.org",
+ "Cache-Control" => "public, max-age=60",
+ "X-Custom-Header" => "I'm a teapot"
+ }
+
+ app = ActionDispatch::Static.new(DummyApp, @root, headers: headers)
+ response = Rack::MockRequest.new(app).request("GET", "/foo/bar.html")
+
+ assert_equal "http://rubyonrails.org", response.headers["Access-Control-Allow-Origin"]
+ assert_equal "public, max-age=60", response.headers["Cache-Control"]
+ assert_equal "I'm a teapot", response.headers["X-Custom-Header"]
+ end
+
+ def test_ignores_unknown_http_methods
+ app = ActionDispatch::Static.new(DummyApp, @root)
+
+ assert_nothing_raised { Rack::MockRequest.new(app).request("BAD_METHOD", "/foo/bar.html") }
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")
@@ -201,7 +235,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
@@ -230,14 +264,10 @@ module StaticTests
end
class StaticTest < ActiveSupport::TestCase
- DummyApp = lambda { |env|
- [200, {"Content-Type" => "text/plain"}, ["Hello, World!"]]
- }
-
def setup
super
@root = "#{FIXTURE_LOAD_PATH}/public"
- @app = ActionDispatch::Static.new(DummyApp, @root, "public, max-age=60")
+ @app = ActionDispatch::Static.new(DummyApp, @root, headers: { "Cache-Control" => "public, max-age=60" })
end
def public_path
@@ -247,23 +277,23 @@ class StaticTest < ActiveSupport::TestCase
include StaticTests
def test_custom_handler_called_when_file_is_outside_root
- filename = 'shared.html.erb'
- assert File.exist?(File.join(@root, '..', filename))
+ filename = "shared.html.erb"
+ assert File.exist?(File.join(@root, "..", filename))
env = {
- "REQUEST_METHOD"=>"GET",
- "REQUEST_PATH"=>"/..%2F#{filename}",
- "PATH_INFO"=>"/..%2F#{filename}",
- "REQUEST_URI"=>"/..%2F#{filename}",
- "HTTP_VERSION"=>"HTTP/1.1",
- "SERVER_NAME"=>"localhost",
- "SERVER_PORT"=>"8080",
- "QUERY_STRING"=>""
+ "REQUEST_METHOD" => "GET",
+ "REQUEST_PATH" => "/..%2F#{filename}",
+ "PATH_INFO" => "/..%2F#{filename}",
+ "REQUEST_URI" => "/..%2F#{filename}",
+ "HTTP_VERSION" => "HTTP/1.1",
+ "SERVER_NAME" => "localhost",
+ "SERVER_PORT" => "8080",
+ "QUERY_STRING" => ""
}
assert_equal(DummyApp.call(nil), @app.call(env))
end
def test_non_default_static_index
- @app = ActionDispatch::Static.new(DummyApp, @root, "public, max-age=60", index: "other-index")
+ @app = ActionDispatch::Static.new(DummyApp, @root, index: "other-index")
assert_html "/other-index.html", get("/other-index.html")
assert_html "/other-index.html", get("/other-index")
assert_html "/other-index.html", get("/")
@@ -273,14 +303,13 @@ class StaticTest < ActiveSupport::TestCase
assert_html "/foo/other-index.html", get("/foo/")
assert_html "/foo/other-index.html", get("/foo")
end
-
end
class StaticEncodingTest < StaticTest
def setup
super
@root = "#{FIXTURE_LOAD_PATH}/公共"
- @app = ActionDispatch::Static.new(DummyApp, @root, "public, max-age=60")
+ @app = ActionDispatch::Static.new(DummyApp, @root, headers: { "Cache-Control" => "public, max-age=60" })
end
def public_path
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..a824ee0c84
--- /dev/null
+++ b/actionpack/test/dispatch/system_testing/driver_test.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+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).name
+ assert_nil driver.instance_variable_get(:@browser).options
+ assert_equal [1400, 1400], driver.instance_variable_get(:@screen_size)
+ assert_equal ({ url: "http://example.com/wd/hub" }), driver.instance_variable_get(:@options)
+ end
+
+ test "initializing the driver with a headless chrome" do
+ driver = ActionDispatch::SystemTesting::Driver.new(:selenium, using: :headless_chrome, screen_size: [1400, 1400], options: { url: "http://example.com/wd/hub" })
+ assert_equal :selenium, driver.instance_variable_get(:@name)
+ assert_equal :headless_chrome, driver.instance_variable_get(:@browser).name
+ assert_equal [1400, 1400], driver.instance_variable_get(:@screen_size)
+ assert_equal ({ url: "http://example.com/wd/hub" }), driver.instance_variable_get(:@options)
+ end
+
+ test "initializing the driver with a headless firefox" do
+ driver = ActionDispatch::SystemTesting::Driver.new(:selenium, using: :headless_firefox, screen_size: [1400, 1400], options: { url: "http://example.com/wd/hub" })
+ assert_equal :selenium, driver.instance_variable_get(:@name)
+ assert_equal :headless_firefox, driver.instance_variable_get(:@browser).name
+ assert_equal [1400, 1400], driver.instance_variable_get(:@screen_size)
+ assert_equal ({ url: "http://example.com/wd/hub" }), driver.instance_variable_get(:@options)
+ end
+
+ test "initializing the driver with a poltergeist" do
+ driver = ActionDispatch::SystemTesting::Driver.new(:poltergeist, screen_size: [1400, 1400], options: { js_errors: false })
+ assert_equal :poltergeist, driver.instance_variable_get(:@name)
+ assert_equal [1400, 1400], driver.instance_variable_get(:@screen_size)
+ assert_equal ({ js_errors: false }), driver.instance_variable_get(:@options)
+ end
+
+ test "initializing the driver with a webkit" do
+ driver = ActionDispatch::SystemTesting::Driver.new(:webkit, screen_size: [1400, 1400], options: { skip_image_loading: true })
+ assert_equal :webkit, driver.instance_variable_get(:@name)
+ assert_equal [1400, 1400], driver.instance_variable_get(:@screen_size)
+ assert_equal ({ skip_image_loading: true }), driver.instance_variable_get(:@options)
+ end
+
+ test "registerable? returns false if driver is rack_test" do
+ assert_not ActionDispatch::SystemTesting::Driver.new(:rack_test).send(:registerable?)
+ 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..de79c05657
--- /dev/null
+++ b/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+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")
+
+ Rails.stub :root, Pathname.getwd do
+ assert_equal Rails.root.join("tmp/screenshots/x.png").to_s, new_test.send(:image_path)
+ end
+ end
+
+ test "image path includes failures text if test did not pass" do
+ new_test = DrivenBySeleniumWithChrome.new("x")
+
+ Rails.stub :root, Pathname.getwd do
+ new_test.stub :passed?, false do
+ assert_equal Rails.root.join("tmp/screenshots/failures_x.png").to_s, new_test.send(:image_path)
+ end
+ end
+ end
+
+ test "image path does not include failures text if test skipped" do
+ new_test = DrivenBySeleniumWithChrome.new("x")
+
+ Rails.stub :root, Pathname.getwd do
+ new_test.stub :passed?, false do
+ new_test.stub :skipped?, true do
+ assert_equal Rails.root.join("tmp/screenshots/x.png").to_s, new_test.send(:image_path)
+ end
+ end
+ end
+ end
+
+ test "defaults to simple output for the screenshot" do
+ new_test = DrivenBySeleniumWithChrome.new("x")
+ assert_equal "simple", new_test.send(:output_type)
+ end
+
+ test "display_image return artifact format when specify RAILS_SYSTEM_TESTING_SCREENSHOT environment" do
+ begin
+ original_output_type = ENV["RAILS_SYSTEM_TESTING_SCREENSHOT"]
+ ENV["RAILS_SYSTEM_TESTING_SCREENSHOT"] = "artifact"
+
+ new_test = DrivenBySeleniumWithChrome.new("x")
+
+ assert_equal "artifact", new_test.send(:output_type)
+
+ Rails.stub :root, Pathname.getwd do
+ new_test.stub :passed?, false do
+ assert_match %r|url=artifact://.+?tmp/screenshots/failures_x\.png|, new_test.send(:display_image)
+ end
+ end
+ ensure
+ ENV["RAILS_SYSTEM_TESTING_SCREENSHOT"] = original_output_type
+ end
+ end
+
+ test "image path returns the absolute path from root" do
+ new_test = DrivenBySeleniumWithChrome.new("x")
+
+ Rails.stub :root, Pathname.getwd.join("..") do
+ assert_equal Rails.root.join("tmp/screenshots/x.png").to_s, new_test.send(:image_path)
+ 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..740e90a4da
--- /dev/null
+++ b/actionpack/test/dispatch/system_testing/server_test.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "capybara/dsl"
+require "action_dispatch/system_testing/server"
+
+class ServerTest < ActiveSupport::TestCase
+ setup do
+ @old_capybara_server = Capybara.server
+ end
+
+ test "port is always included" do
+ ActionDispatch::SystemTesting::Server.new.run
+ assert Capybara.always_include_port, "expected Capybara.always_include_port to be true"
+ end
+
+ test "server is changed from `default` to `puma`" do
+ Capybara.server = :default
+ ActionDispatch::SystemTesting::Server.new.run
+ assert_not_equal Capybara.server, Capybara.servers[:default]
+ end
+
+ test "server is not changed to `puma` when is different than default" do
+ Capybara.server = :webrick
+ ActionDispatch::SystemTesting::Server.new.run
+ assert_equal Capybara.server, Capybara.servers[:webrick]
+ end
+
+ teardown do
+ Capybara.server = @old_capybara_server
+ end
+end
diff --git a/actionpack/test/dispatch/system_testing/system_test_case_test.rb b/actionpack/test/dispatch/system_testing/system_test_case_test.rb
new file mode 100644
index 0000000000..b078a5abc5
--- /dev/null
+++ b/actionpack/test/dispatch/system_testing/system_test_case_test.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class SetDriverToRackTestTest < DrivenByRackTest
+ test "uses rack_test" do
+ assert_equal :rack_test, Capybara.current_driver
+ end
+end
+
+class OverrideSeleniumSubclassToRackTestTest < DrivenBySeleniumWithChrome
+ driven_by :rack_test
+
+ 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
+
+class SetDriverToSeleniumHeadlessChromeTest < DrivenBySeleniumWithHeadlessChrome
+ test "uses selenium headless chrome" do
+ assert_equal :selenium, Capybara.current_driver
+ end
+end
+
+class SetDriverToSeleniumHeadlessFirefoxTest < DrivenBySeleniumWithHeadlessFirefox
+ test "uses selenium headless firefox" do
+ assert_equal :selenium, Capybara.current_driver
+ end
+end
+
+class SetHostTest < DrivenByRackTest
+ test "sets default host" do
+ assert_equal "http://127.0.0.1", Capybara.app_host
+ end
+
+ test "overrides host" do
+ host! "http://example.com"
+
+ assert_equal "http://example.com", Capybara.app_host
+ end
+end
+
+class UndefMethodsTest < DrivenBySeleniumWithChrome
+ test "get" do
+ exception = assert_raise NoMethodError do
+ get "http://example.com"
+ end
+ assert_equal "System tests cannot make direct requests via #get; use #visit and #click_on instead. See http://www.rubydoc.info/github/teamcapybara/capybara/master#The_DSL for more information.", exception.message
+ end
+
+ test "post" do
+ exception = assert_raise NoMethodError do
+ post "http://example.com"
+ end
+ assert_equal "System tests cannot make direct requests via #post; use #visit and #click_on instead. See http://www.rubydoc.info/github/teamcapybara/capybara/master#The_DSL for more information.", exception.message
+ end
+
+ test "put" do
+ exception = assert_raise NoMethodError do
+ put "http://example.com"
+ end
+ assert_equal "System tests cannot make direct requests via #put; use #visit and #click_on instead. See http://www.rubydoc.info/github/teamcapybara/capybara/master#The_DSL for more information.", exception.message
+ end
+
+ test "patch" do
+ exception = assert_raise NoMethodError do
+ patch "http://example.com"
+ end
+ assert_equal "System tests cannot make direct requests via #patch; use #visit and #click_on instead. See http://www.rubydoc.info/github/teamcapybara/capybara/master#The_DSL for more information.", exception.message
+ end
+
+ test "delete" do
+ exception = assert_raise NoMethodError do
+ delete "http://example.com"
+ end
+ assert_equal "System tests cannot make direct requests via #delete; use #visit and #click_on instead. See http://www.rubydoc.info/github/teamcapybara/capybara/master#The_DSL for more information.", exception.message
+ end
+end
diff --git a/actionpack/test/dispatch/test_request_test.rb b/actionpack/test/dispatch/test_request_test.rb
index ede1cec4e6..e56537d80b 100644
--- a/actionpack/test/dispatch/test_request_test.rb
+++ b/actionpack/test/dispatch/test_request_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class TestRequestTest < ActiveSupport::TestCase
test "sane defaults" do
@@ -30,64 +32,96 @@ 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)
+ assert_cookies({ "user_name" => "david" }, req.cookie_jar)
req.cookie_jar["login"] = "XJ-122"
- assert_cookies({"user_name" => "david", "login" => "XJ-122"}, req.cookie_jar)
+ assert_cookies({ "user_name" => "david", "login" => "XJ-122" }, req.cookie_jar)
assert_nothing_raised do
req.cookie_jar["login"] = nil
- assert_cookies({"user_name" => "david", "login" => nil}, req.cookie_jar)
+ assert_cookies({ "user_name" => "david", "login" => nil }, req.cookie_jar)
end
req.cookie_jar.delete(:login)
- assert_cookies({"user_name" => "david"}, req.cookie_jar)
+ assert_cookies({ "user_name" => "david" }, req.cookie_jar)
req.cookie_jar.clear
assert_cookies({}, req.cookie_jar)
- req.cookie_jar.update(:user_name => "david")
- assert_cookies({"user_name" => "david"}, req.cookie_jar)
+ req.cookie_jar.update(user_name: "david")
+ assert_cookies({ "user_name" => "david" }, req.cookie_jar)
end
- test "does not complain when Rails.application is nil" do
- Rails.stubs(:application).returns(nil)
+ test "does not complain when there is no application config" do
req = ActionDispatch::TestRequest.create({})
-
assert_equal false, req.env.empty?
end
test "default remote address is 0.0.0.0" do
req = ActionDispatch::TestRequest.create({})
- assert_equal '0.0.0.0', req.remote_addr
+ assert_equal "0.0.0.0", req.remote_addr
end
test "allows remote address to be overridden" do
- req = ActionDispatch::TestRequest.create('REMOTE_ADDR' => '127.0.0.1')
- assert_equal '127.0.0.1', req.remote_addr
+ req = ActionDispatch::TestRequest.create("REMOTE_ADDR" => "127.0.0.1")
+ assert_equal "127.0.0.1", req.remote_addr
end
test "default host is test.host" do
req = ActionDispatch::TestRequest.create({})
- assert_equal 'test.host', req.host
+ assert_equal "test.host", req.host
end
test "allows host to be overridden" do
- req = ActionDispatch::TestRequest.create('HTTP_HOST' => 'www.example.com')
- assert_equal 'www.example.com', req.host
+ req = ActionDispatch::TestRequest.create("HTTP_HOST" => "www.example.com")
+ assert_equal "www.example.com", req.host
end
test "default user agent is 'Rails Testing'" do
req = ActionDispatch::TestRequest.create({})
- assert_equal 'Rails Testing', req.user_agent
+ assert_equal "Rails Testing", req.user_agent
end
test "allows user agent to be overridden" do
- req = ActionDispatch::TestRequest.create('HTTP_USER_AGENT' => 'GoogleBot')
- assert_equal 'GoogleBot', req.user_agent
+ req = ActionDispatch::TestRequest.create("HTTP_USER_AGENT" => "GoogleBot")
+ assert_equal "GoogleBot", req.user_agent
+ end
+
+ test "request_method getter and setter" do
+ req = ActionDispatch::TestRequest.create
+ req.request_method # to reproduce bug caused by memoization
+ req.request_method = "POST"
+ assert_equal "POST", req.request_method
+ end
+
+ test "setter methods" do
+ req = ActionDispatch::TestRequest.create({})
+ get = "GET"
+
+ [
+ "request_method=", "host=", "request_uri=", "path=", "if_modified_since=", "if_none_match=",
+ "remote_addr=", "user_agent=", "accept="
+ ].each do |method|
+ req.send(method, get)
+ end
+
+ req.port = 8080
+ req.accept = "hello goodbye"
+
+ assert_equal(get, req.get_header("REQUEST_METHOD"))
+ assert_equal(get, req.get_header("HTTP_HOST"))
+ assert_equal(8080, req.get_header("SERVER_PORT"))
+ assert_equal(get, req.get_header("REQUEST_URI"))
+ assert_equal(get, req.get_header("PATH_INFO"))
+ assert_equal(get, req.get_header("HTTP_IF_MODIFIED_SINCE"))
+ assert_equal(get, req.get_header("HTTP_IF_NONE_MATCH"))
+ assert_equal(get, req.get_header("REMOTE_ADDR"))
+ assert_equal(get, req.get_header("HTTP_USER_AGENT"))
+ assert_nil(req.get_header("action_dispatch.request.accepts"))
+ assert_equal("hello goodbye", req.get_header("HTTP_ACCEPT"))
end
private
diff --git a/actionpack/test/dispatch/test_response_test.rb b/actionpack/test/dispatch/test_response_test.rb
index dc17668def..f0b8f7785d 100644
--- a/actionpack/test/dispatch/test_response_test.rb
+++ b/actionpack/test/dispatch/test_response_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class TestResponseTest < ActiveSupport::TestCase
def assert_response_code_range(range, predicate)
@@ -11,11 +13,25 @@ class TestResponseTest < ActiveSupport::TestCase
end
test "helpers" do
- assert_response_code_range 200..299, :success?
- assert_response_code_range [404], :missing?
- assert_response_code_range 300..399, :redirect?
- assert_response_code_range 500..599, :error?
+ assert_response_code_range 200..299, :successful?
+ assert_response_code_range [404], :not_found?
+ assert_response_code_range 300..399, :redirection?
assert_response_code_range 500..599, :server_error?
assert_response_code_range 400..499, :client_error?
end
+
+ test "response parsing" do
+ response = ActionDispatch::TestResponse.create(200, {}, "")
+ assert_equal response.body, response.parsed_body
+
+ response = ActionDispatch::TestResponse.create(200, { "Content-Type" => "application/json" }, '{ "foo": "fighters" }')
+ assert_equal({ "foo" => "fighters" }, response.parsed_body)
+ end
+
+ test "response status aliases deprecated" do
+ response = ActionDispatch::TestResponse.create
+ assert_deprecated { response.success? }
+ assert_deprecated { response.missing? }
+ assert_deprecated { response.error? }
+ end
end
diff --git a/actionpack/test/dispatch/uploaded_file_test.rb b/actionpack/test/dispatch/uploaded_file_test.rb
index 55ebbd5143..21169fcb5c 100644
--- a/actionpack/test/dispatch/uploaded_file_test.rb
+++ b/actionpack/test/dispatch/uploaded_file_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module ActionDispatch
class UploadedFileTest < ActiveSupport::TestCase
@@ -9,81 +11,87 @@ module ActionDispatch
end
def test_original_filename
- uf = Http::UploadedFile.new(:filename => 'foo', :tempfile => Object.new)
- assert_equal 'foo', uf.original_filename
+ uf = Http::UploadedFile.new(filename: "foo", tempfile: Object.new)
+ assert_equal "foo", uf.original_filename
+ end
+
+ def test_filename_is_different_object
+ file_str = "foo"
+ uf = Http::UploadedFile.new(filename: file_str, tempfile: Object.new)
+ assert_not_equal file_str.object_id, uf.original_filename.object_id
end
def test_filename_should_be_in_utf_8
- uf = Http::UploadedFile.new(:filename => 'foo', :tempfile => Object.new)
+ uf = Http::UploadedFile.new(filename: "foo", tempfile: Object.new)
assert_equal "UTF-8", uf.original_filename.encoding.to_s
end
def test_filename_should_always_be_in_utf_8
- uf = Http::UploadedFile.new(:filename => 'foo'.encode(Encoding::SHIFT_JIS),
- :tempfile => Object.new)
+ uf = Http::UploadedFile.new(filename: "foo".encode(Encoding::SHIFT_JIS),
+ tempfile: Object.new)
assert_equal "UTF-8", uf.original_filename.encoding.to_s
end
def test_content_type
- uf = Http::UploadedFile.new(:type => 'foo', :tempfile => Object.new)
- assert_equal 'foo', uf.content_type
+ uf = Http::UploadedFile.new(type: "foo", tempfile: Object.new)
+ assert_equal "foo", uf.content_type
end
def test_headers
- uf = Http::UploadedFile.new(:head => 'foo', :tempfile => Object.new)
- assert_equal 'foo', uf.headers
+ uf = Http::UploadedFile.new(head: "foo", tempfile: Object.new)
+ assert_equal "foo", uf.headers
end
def test_tempfile
- uf = Http::UploadedFile.new(:tempfile => 'foo')
- assert_equal 'foo', uf.tempfile
+ uf = Http::UploadedFile.new(tempfile: "foo")
+ assert_equal "foo", uf.tempfile
end
def test_to_io_returns_the_tempfile
tf = Object.new
- uf = Http::UploadedFile.new(:tempfile => tf)
+ uf = Http::UploadedFile.new(tempfile: tf)
assert_equal tf, uf.to_io
end
def test_delegates_path_to_tempfile
- tf = Class.new { def path; 'thunderhorse' end }
- uf = Http::UploadedFile.new(:tempfile => tf.new)
- assert_equal 'thunderhorse', uf.path
+ tf = Class.new { def path; "thunderhorse" end }
+ uf = Http::UploadedFile.new(tempfile: tf.new)
+ assert_equal "thunderhorse", uf.path
end
def test_delegates_open_to_tempfile
- tf = Class.new { def open; 'thunderhorse' end }
- uf = Http::UploadedFile.new(:tempfile => tf.new)
- assert_equal 'thunderhorse', uf.open
+ tf = Class.new { def open; "thunderhorse" end }
+ uf = Http::UploadedFile.new(tempfile: tf.new)
+ assert_equal "thunderhorse", uf.open
end
def test_delegates_close_to_tempfile
- tf = Class.new { def close(unlink_now=false); 'thunderhorse' end }
- uf = Http::UploadedFile.new(:tempfile => tf.new)
- assert_equal 'thunderhorse', uf.close
+ tf = Class.new { def close(unlink_now = false); "thunderhorse" end }
+ uf = Http::UploadedFile.new(tempfile: tf.new)
+ assert_equal "thunderhorse", uf.close
end
def test_close_accepts_parameter
- tf = Class.new { def close(unlink_now=false); "thunderhorse: #{unlink_now}" end }
- uf = Http::UploadedFile.new(:tempfile => tf.new)
- assert_equal 'thunderhorse: true', uf.close(true)
+ tf = Class.new { def close(unlink_now = false); "thunderhorse: #{unlink_now}" end }
+ uf = Http::UploadedFile.new(tempfile: tf.new)
+ assert_equal "thunderhorse: true", uf.close(true)
end
def test_delegates_read_to_tempfile
- tf = Class.new { def read(length=nil, buffer=nil); 'thunderhorse' end }
- uf = Http::UploadedFile.new(:tempfile => tf.new)
- assert_equal 'thunderhorse', uf.read
+ tf = Class.new { def read(length = nil, buffer = nil); "thunderhorse" end }
+ uf = Http::UploadedFile.new(tempfile: tf.new)
+ assert_equal "thunderhorse", uf.read
end
def test_delegates_read_to_tempfile_with_params
- tf = Class.new { def read(length=nil, buffer=nil); [length, buffer] end }
- uf = Http::UploadedFile.new(:tempfile => tf.new)
+ tf = Class.new { def read(length = nil, buffer = nil); [length, buffer] end }
+ uf = Http::UploadedFile.new(tempfile: tf.new)
assert_equal %w{ thunder horse }, uf.read(*%w{ thunder horse })
end
def test_delegate_respects_respond_to?
tf = Class.new { def read; yield end; private :read }
- uf = Http::UploadedFile.new(:tempfile => tf.new)
+ uf = Http::UploadedFile.new(tempfile: tf.new)
assert_raises(NoMethodError) do
uf.read
end
@@ -91,15 +99,21 @@ module ActionDispatch
def test_delegate_eof_to_tempfile
tf = Class.new { def eof?; true end; }
- uf = Http::UploadedFile.new(:tempfile => tf.new)
- assert uf.eof?
+ uf = Http::UploadedFile.new(tempfile: tf.new)
+ assert_predicate uf, :eof?
+ end
+
+ def test_delegate_to_path_to_tempfile
+ tf = Class.new { def to_path; "/any/file/path" end; }
+ uf = Http::UploadedFile.new(tempfile: tf.new)
+ assert_equal "/any/file/path", uf.to_path
end
def test_respond_to?
tf = Class.new { def read; yield end }
- uf = Http::UploadedFile.new(:tempfile => tf.new)
- assert uf.respond_to?(:headers), 'responds to headers'
- assert uf.respond_to?(:read), 'responds to read'
+ uf = Http::UploadedFile.new(tempfile: tf.new)
+ assert_respond_to uf, :headers
+ assert_respond_to uf, :read
end
end
end
diff --git a/actionpack/test/dispatch/url_generation_test.rb b/actionpack/test/dispatch/url_generation_test.rb
index ce1e1d0a6a..aef9351de1 100644
--- a/actionpack/test/dispatch/url_generation_test.rb
+++ b/actionpack/test/dispatch/url_generation_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module TestUrlGeneration
class WithMountPoint < ActionDispatch::IntegrationTest
@@ -8,16 +10,16 @@ module TestUrlGeneration
class ::MyRouteGeneratingController < ActionController::Base
include Routes.url_helpers
def index
- render :text => foo_path
+ render plain: foo_path
end
end
Routes.draw do
- get "/foo", :to => "my_route_generating#index", :as => :foo
+ get "/foo", to: "my_route_generating#index", as: :foo
resources :bars
- mount MyRouteGeneratingController.action(:index), at: '/bar'
+ mount MyRouteGeneratingController.action(:index), at: "/bar"
end
APP = build_app Routes
@@ -35,22 +37,22 @@ module TestUrlGeneration
end
test "accepting a :script_name option" do
- assert_equal "/bar/foo", foo_path(:script_name => "/bar")
+ assert_equal "/bar/foo", foo_path(script_name: "/bar")
end
test "the request's SCRIPT_NAME takes precedence over the route" do
- get "/foo", headers: { 'SCRIPT_NAME' => "/new", 'action_dispatch.routes' => Routes }
+ get "/foo", headers: { "SCRIPT_NAME" => "/new", "action_dispatch.routes" => Routes }
assert_equal "/new/foo", response.body
end
test "the request's SCRIPT_NAME wraps the mounted app's" do
- get '/new/bar/foo', headers: { 'SCRIPT_NAME' => '/new', 'PATH_INFO' => '/bar/foo', 'action_dispatch.routes' => Routes }
+ get "/new/bar/foo", headers: { "SCRIPT_NAME" => "/new", "PATH_INFO" => "/bar/foo", "action_dispatch.routes" => Routes }
assert_equal "/new/bar/foo", response.body
end
test "handling http protocol with https set" do
https!
- assert_equal "http://www.example.com/foo", foo_url(:protocol => "http")
+ assert_equal "http://www.example.com/foo", foo_url(protocol: "http")
end
test "extracting protocol from host when protocol not present" do
@@ -117,18 +119,23 @@ module TestUrlGeneration
test "generating URLs with trailing slashes" do
assert_equal "/bars.json", bars_path(
trailing_slash: true,
- format: 'json'
+ format: "json"
)
end
test "generating URLS with querystring and trailing slashes" do
assert_equal "/bars.json?a=b", bars_path(
trailing_slash: true,
- a: 'b',
- format: 'json'
+ a: "b",
+ format: "json"
)
end
+ test "generating URLS with empty querystring" do
+ assert_equal "/bars.json", bars_path(
+ a: {},
+ format: "json"
+ )
+ end
end
end
-
diff --git a/actionpack/test/fixtures/alternate_helpers/foo_helper.rb b/actionpack/test/fixtures/alternate_helpers/foo_helper.rb
index 2528584473..c1a995af5f 100644
--- a/actionpack/test/fixtures/alternate_helpers/foo_helper.rb
+++ b/actionpack/test/fixtures/alternate_helpers/foo_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module FooHelper
- redefine_method(:baz) {}
+ redefine_method(:baz) { }
end
diff --git a/actionpack/test/fixtures/collection_cache/index.html.erb b/actionpack/test/fixtures/collection_cache/index.html.erb
index 521b1450df..853e501ab4 100644
--- a/actionpack/test/fixtures/collection_cache/index.html.erb
+++ b/actionpack/test/fixtures/collection_cache/index.html.erb
@@ -1 +1 @@
-<%= render @customers %> \ No newline at end of file
+<%= render partial: 'customers/customer', collection: @customers, cached: true %>
diff --git a/actionpack/test/fixtures/company.rb b/actionpack/test/fixtures/company.rb
index f3ac3642fa..93afdd5472 100644
--- a/actionpack/test/fixtures/company.rb
+++ b/actionpack/test/fixtures/company.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
class Company < ActiveRecord::Base
has_one :mascot
self.sequence_name = :companies_nonstd_seq
validates_presence_of :name
def validate
- errors.add('rating', 'rating should not be 2') if rating == 2
+ errors.add("rating", "rating should not be 2") if rating == 2
end
end
diff --git a/actionpack/test/fixtures/functional_caching/_formatted_partial.html.erb b/actionpack/test/fixtures/functional_caching/_formatted_partial.html.erb
new file mode 100644
index 0000000000..aad73c0d6b
--- /dev/null
+++ b/actionpack/test/fixtures/functional_caching/_formatted_partial.html.erb
@@ -0,0 +1 @@
+<p>Hello!</p>
diff --git a/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.html.erb b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.html.erb
index 9b88fa1f5a..dfcd423978 100644
--- a/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.html.erb
+++ b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.html.erb
@@ -1,3 +1,3 @@
<body>
-<%= cache do %><p>ERB</p><% end %>
+<%= cache("fragment") do %><p>ERB</p><% end %>
</body>
diff --git a/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.xml.builder b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.xml.builder
index efdcc28e0f..6599579740 100644
--- a/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.xml.builder
+++ b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.xml.builder
@@ -1,5 +1,5 @@
xml.body do
- cache do
+ cache("fragment") do
xml.p "Builder"
end
end
diff --git a/actionpack/test/fixtures/functional_caching/formatted_fragment_cached_with_variant.html+phone.erb b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached_with_variant.html+phone.erb
index e523b74ae3..abf7017ce6 100644
--- a/actionpack/test/fixtures/functional_caching/formatted_fragment_cached_with_variant.html+phone.erb
+++ b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached_with_variant.html+phone.erb
@@ -1,3 +1,3 @@
<body>
-<%= cache do %><p>PHONE</p><% end %>
+<%= cache("fragment") do %><p>PHONE</p><% end %>
</body>
diff --git a/actionpack/test/fixtures/functional_caching/fragment_cached.html.erb b/actionpack/test/fixtures/functional_caching/fragment_cached.html.erb
index fa5e6bd318..1148d83ad7 100644
--- a/actionpack/test/fixtures/functional_caching/fragment_cached.html.erb
+++ b/actionpack/test/fixtures/functional_caching/fragment_cached.html.erb
@@ -1,3 +1,3 @@
Hello
-<%= cache do %>This bit's fragment cached<% end %>
+<%= cache "fragment" do %>This bit's fragment cached<% end %>
<%= 'Ciao' %>
diff --git a/actionpack/test/fixtures/functional_caching/fragment_cached_with_options.html.erb b/actionpack/test/fixtures/functional_caching/fragment_cached_with_options.html.erb
new file mode 100644
index 0000000000..951c761995
--- /dev/null
+++ b/actionpack/test/fixtures/functional_caching/fragment_cached_with_options.html.erb
@@ -0,0 +1,3 @@
+<body>
+<%= cache 'with_options', skip_digest: true, expires_in: 10 do %><p>ERB</p><% end %>
+</body>
diff --git a/actionpack/test/fixtures/functional_caching/xml_fragment_cached_with_html_partial.xml.builder b/actionpack/test/fixtures/functional_caching/xml_fragment_cached_with_html_partial.xml.builder
new file mode 100644
index 0000000000..2bdda3af18
--- /dev/null
+++ b/actionpack/test/fixtures/functional_caching/xml_fragment_cached_with_html_partial.xml.builder
@@ -0,0 +1,5 @@
+cache do
+ xml.title "Hello!"
+end
+
+xml.body cdata_section(render("formatted_partial"))
diff --git a/actionpack/test/fixtures/helpers/abc_helper.rb b/actionpack/test/fixtures/helpers/abc_helper.rb
index cf2774bb5f..999b9b5c6e 100644
--- a/actionpack/test/fixtures/helpers/abc_helper.rb
+++ b/actionpack/test/fixtures/helpers/abc_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module AbcHelper
def bare_a() end
end
diff --git a/actionpack/test/fixtures/helpers/fun/games_helper.rb b/actionpack/test/fixtures/helpers/fun/games_helper.rb
index 3b7adce086..8b325927f3 100644
--- a/actionpack/test/fixtures/helpers/fun/games_helper.rb
+++ b/actionpack/test/fixtures/helpers/fun/games_helper.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
module Fun
module GamesHelper
def stratego() "Iz guuut!" end
end
-end \ No newline at end of file
+end
diff --git a/actionpack/test/fixtures/helpers/fun/pdf_helper.rb b/actionpack/test/fixtures/helpers/fun/pdf_helper.rb
index 0171be8500..7ce6591de3 100644
--- a/actionpack/test/fixtures/helpers/fun/pdf_helper.rb
+++ b/actionpack/test/fixtures/helpers/fun/pdf_helper.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
module Fun
module PdfHelper
- def foobar() 'baz' end
+ def foobar() "baz" end
end
end
diff --git a/actionpack/test/fixtures/helpers/just_me_helper.rb b/actionpack/test/fixtures/helpers/just_me_helper.rb
index b140a7b9b4..bd977a22d9 100644
--- a/actionpack/test/fixtures/helpers/just_me_helper.rb
+++ b/actionpack/test/fixtures/helpers/just_me_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module JustMeHelper
def me() "mine!" end
-end \ No newline at end of file
+end
diff --git a/actionpack/test/fixtures/helpers/me_too_helper.rb b/actionpack/test/fixtures/helpers/me_too_helper.rb
index ce56042143..c6fc053dee 100644
--- a/actionpack/test/fixtures/helpers/me_too_helper.rb
+++ b/actionpack/test/fixtures/helpers/me_too_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module MeTooHelper
def me() "me too!" end
-end \ No newline at end of file
+end
diff --git a/actionpack/test/fixtures/helpers1_pack/pack1_helper.rb b/actionpack/test/fixtures/helpers1_pack/pack1_helper.rb
index 9faa427736..cf75b6875e 100644
--- a/actionpack/test/fixtures/helpers1_pack/pack1_helper.rb
+++ b/actionpack/test/fixtures/helpers1_pack/pack1_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Pack1Helper
def conflicting_helper
"pack1"
diff --git a/actionpack/test/fixtures/helpers2_pack/pack2_helper.rb b/actionpack/test/fixtures/helpers2_pack/pack2_helper.rb
index cf56697dfb..c8e51d40a2 100644
--- a/actionpack/test/fixtures/helpers2_pack/pack2_helper.rb
+++ b/actionpack/test/fixtures/helpers2_pack/pack2_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Pack2Helper
def conflicting_helper
"pack2"
diff --git a/actionpack/test/fixtures/helpers_typo/admin/users_helper.rb b/actionpack/test/fixtures/helpers_typo/admin/users_helper.rb
index 7d2326e04d..0455e26b93 100644
--- a/actionpack/test/fixtures/helpers_typo/admin/users_helper.rb
+++ b/actionpack/test/fixtures/helpers_typo/admin/users_helper.rb
@@ -1,5 +1,6 @@
+# frozen_string_literal: true
+
module Admin
module UsersHelpeR
end
end
-
diff --git a/actionpack/test/fixtures/implicit_render_test/empty_action_with_mobile_variant.html+mobile.erb b/actionpack/test/fixtures/implicit_render_test/empty_action_with_mobile_variant.html+mobile.erb
new file mode 100644
index 0000000000..ded99ba52d
--- /dev/null
+++ b/actionpack/test/fixtures/implicit_render_test/empty_action_with_mobile_variant.html+mobile.erb
@@ -0,0 +1 @@
+mobile
diff --git a/actionpack/test/fixtures/implicit_render_test/empty_action_with_template.html.erb b/actionpack/test/fixtures/implicit_render_test/empty_action_with_template.html.erb
new file mode 100644
index 0000000000..dd294f8cf6
--- /dev/null
+++ b/actionpack/test/fixtures/implicit_render_test/empty_action_with_template.html.erb
@@ -0,0 +1 @@
+<h1>Empty action rendered this implicitly.</h1>
diff --git a/actionpack/test/fixtures/layouts/builder.builder b/actionpack/test/fixtures/layouts/builder.builder
index 7c7d4b2dd1..c55488edd0 100644
--- a/actionpack/test/fixtures/layouts/builder.builder
+++ b/actionpack/test/fixtures/layouts/builder.builder
@@ -1,3 +1,3 @@
xml.wrapper do
xml << yield
-end \ No newline at end of file
+end
diff --git a/actionpack/test/fixtures/load_me.rb b/actionpack/test/fixtures/load_me.rb
new file mode 100644
index 0000000000..efafe6898f
--- /dev/null
+++ b/actionpack/test/fixtures/load_me.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+class LoadMe
+end
diff --git a/actionpack/test/fixtures/multipart/mona_lisa.jpg b/actionpack/test/fixtures/multipart/mona_lisa.jpg
deleted file mode 100644
index 5cf3bef3d0..0000000000
--- a/actionpack/test/fixtures/multipart/mona_lisa.jpg
+++ /dev/null
Binary files differ
diff --git a/actionpack/test/fixtures/multipart/ruby_on_rails.jpg b/actionpack/test/fixtures/multipart/ruby_on_rails.jpg
new file mode 100644
index 0000000000..ed284ea0ba
--- /dev/null
+++ b/actionpack/test/fixtures/multipart/ruby_on_rails.jpg
Binary files differ
diff --git a/actionpack/test/fixtures/multipart/utf8_filename b/actionpack/test/fixtures/multipart/utf8_filename
new file mode 100644
index 0000000000..60738d53b0
--- /dev/null
+++ b/actionpack/test/fixtures/multipart/utf8_filename
@@ -0,0 +1,10 @@
+--AaB03x
+Content-Disposition: form-data; name="foo"
+
+bar
+--AaB03x
+Content-Disposition: form-data; name="file"; filename="ファイル%名.txt"
+Content-Type: text/plain
+
+contents
+--AaB03x--
diff --git a/actionpack/test/fixtures/namespaced/implicit_render_test/hello_world.erb b/actionpack/test/fixtures/namespaced/implicit_render_test/hello_world.erb
new file mode 100644
index 0000000000..cd0875583a
--- /dev/null
+++ b/actionpack/test/fixtures/namespaced/implicit_render_test/hello_world.erb
@@ -0,0 +1 @@
+Hello world!
diff --git a/actionpack/test/fixtures/old_content_type/render_default_for_builder.builder b/actionpack/test/fixtures/old_content_type/render_default_for_builder.builder
index 598d62e2fc..15c8a7f5cf 100644
--- a/actionpack/test/fixtures/old_content_type/render_default_for_builder.builder
+++ b/actionpack/test/fixtures/old_content_type/render_default_for_builder.builder
@@ -1 +1 @@
-xml.p "Hello world!" \ No newline at end of file
+xml.p "Hello world!"
diff --git a/actionpack/test/fixtures/public/foo/さようなら.html b/actionpack/test/fixtures/public/foo/さようなら.html
new file mode 100644
index 0000000000..627bb2469f
--- /dev/null
+++ b/actionpack/test/fixtures/public/foo/さようなら.html
@@ -0,0 +1 @@
+means goodbye in Japanese
diff --git a/actionpack/test/fixtures/public/foo/さようなら.html.gz b/actionpack/test/fixtures/public/foo/さようなら.html.gz
new file mode 100644
index 0000000000..4f484cfe86
--- /dev/null
+++ b/actionpack/test/fixtures/public/foo/さようなら.html.gz
Binary files differ
diff --git a/actionpack/test/fixtures/respond_to/using_defaults.xml.builder b/actionpack/test/fixtures/respond_to/using_defaults.xml.builder
index 598d62e2fc..15c8a7f5cf 100644
--- a/actionpack/test/fixtures/respond_to/using_defaults.xml.builder
+++ b/actionpack/test/fixtures/respond_to/using_defaults.xml.builder
@@ -1 +1 @@
-xml.p "Hello world!" \ No newline at end of file
+xml.p "Hello world!"
diff --git a/actionpack/test/fixtures/respond_to/using_defaults_with_type_list.xml.builder b/actionpack/test/fixtures/respond_to/using_defaults_with_type_list.xml.builder
index 598d62e2fc..15c8a7f5cf 100644
--- a/actionpack/test/fixtures/respond_to/using_defaults_with_type_list.xml.builder
+++ b/actionpack/test/fixtures/respond_to/using_defaults_with_type_list.xml.builder
@@ -1 +1 @@
-xml.p "Hello world!" \ No newline at end of file
+xml.p "Hello world!"
diff --git a/actionpack/test/fixtures/respond_to/variant_with_implicit_rendering.html+mobile.erb b/actionpack/test/fixtures/respond_to/variant_with_implicit_template_rendering.html+mobile.erb
index 317801ad30..317801ad30 100644
--- a/actionpack/test/fixtures/respond_to/variant_with_implicit_rendering.html+mobile.erb
+++ b/actionpack/test/fixtures/respond_to/variant_with_implicit_template_rendering.html+mobile.erb
diff --git a/actionpack/test/fixtures/session_autoload_test/session_autoload_test/foo.rb b/actionpack/test/fixtures/session_autoload_test/session_autoload_test/foo.rb
index 4ee7a24561..deb81c647d 100644
--- a/actionpack/test/fixtures/session_autoload_test/session_autoload_test/foo.rb
+++ b/actionpack/test/fixtures/session_autoload_test/session_autoload_test/foo.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
module SessionAutoloadTest
class Foo
- def initialize(bar='baz')
+ def initialize(bar = "baz")
@bar = bar
end
def inspect
diff --git a/actionpack/test/fixtures/test/formatted_xml_erb.builder b/actionpack/test/fixtures/test/formatted_xml_erb.builder
index 14fd3549fb..f98aaa34a5 100644
--- a/actionpack/test/fixtures/test/formatted_xml_erb.builder
+++ b/actionpack/test/fixtures/test/formatted_xml_erb.builder
@@ -1 +1 @@
-xml.test 'failed' \ No newline at end of file
+xml.test "failed"
diff --git a/actionpack/test/fixtures/test/hello_xml_world.builder b/actionpack/test/fixtures/test/hello_xml_world.builder
index e7081b89fe..d16bb6b5cb 100644
--- a/actionpack/test/fixtures/test/hello_xml_world.builder
+++ b/actionpack/test/fixtures/test/hello_xml_world.builder
@@ -8,4 +8,4 @@ xml.html do
xml.p "monks"
xml.p "wiseguys"
end
-end \ No newline at end of file
+end
diff --git a/actionpack/test/fixtures/test/with_implicit_template.erb b/actionpack/test/fixtures/test/with_implicit_template.erb
new file mode 100644
index 0000000000..474488cd13
--- /dev/null
+++ b/actionpack/test/fixtures/test/with_implicit_template.erb
@@ -0,0 +1 @@
+Hello explicitly!
diff --git a/actionpack/test/fixtures/公共/foo/さようなら.html b/actionpack/test/fixtures/公共/foo/さようなら.html
new file mode 100644
index 0000000000..627bb2469f
--- /dev/null
+++ b/actionpack/test/fixtures/公共/foo/さようなら.html
@@ -0,0 +1 @@
+means goodbye in Japanese
diff --git a/actionpack/test/fixtures/公共/foo/さようなら.html.gz b/actionpack/test/fixtures/公共/foo/さようなら.html.gz
new file mode 100644
index 0000000000..4f484cfe86
--- /dev/null
+++ b/actionpack/test/fixtures/公共/foo/さようなら.html.gz
Binary files differ
diff --git a/actionpack/test/journey/gtg/builder_test.rb b/actionpack/test/journey/gtg/builder_test.rb
index c1da374007..b92460884d 100644
--- a/actionpack/test/journey/gtg/builder_test.rb
+++ b/actionpack/test/journey/gtg/builder_test.rb
@@ -1,28 +1,30 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module ActionDispatch
module Journey
module GTG
class TestBuilder < ActiveSupport::TestCase
def test_following_states_multi
- table = tt ['a|a']
- assert_equal 1, table.move([0], 'a').length
+ table = tt ["a|a"]
+ assert_equal 1, table.move([0], "a").length
end
def test_following_states_multi_regexp
- table = tt [':a|b']
- assert_equal 1, table.move([0], 'fooo').length
- assert_equal 2, table.move([0], 'b').length
+ table = tt [":a|b"]
+ assert_equal 1, table.move([0], "fooo").length
+ assert_equal 2, table.move([0], "b").length
end
def test_multi_path
- table = tt ['/:a/d', '/b/c']
+ table = tt ["/:a/d", "/b/c"]
[
- [1, '/'],
- [2, 'b'],
- [2, '/'],
- [1, 'c'],
+ [1, "/"],
+ [2, "b"],
+ [2, "/"],
+ [1, "c"],
].inject([0]) { |state, (exp, sym)|
new = table.move(state, sym)
assert_equal exp, new.length
@@ -38,9 +40,9 @@ module ActionDispatch
/articles/:id(.:format)
}
- sim = NFA::Simulator.new table
+ sim = NFA::Simulator.new table
- match = sim.match '/articles/new'
+ match = sim.match "/articles/new"
assert_equal 2, match.memos.length
end
@@ -52,27 +54,27 @@ module ActionDispatch
/articles/new(.:format)
}
- sim = NFA::Simulator.new table
+ sim = NFA::Simulator.new table
- match = sim.match '/articles/new'
+ match = sim.match "/articles/new"
assert_equal 2, match.memos.length
end
private
- def ast strings
- parser = Journey::Parser.new
- asts = strings.map { |string|
- memo = Object.new
- ast = parser.parse string
- ast.each { |n| n.memo = memo }
- ast
- }
- Nodes::Or.new asts
- end
+ def ast(strings)
+ parser = Journey::Parser.new
+ asts = strings.map { |string|
+ memo = Object.new
+ ast = parser.parse string
+ ast.each { |n| n.memo = memo }
+ ast
+ }
+ Nodes::Or.new asts
+ end
- def tt strings
- Builder.new(ast(strings)).transition_table
- end
+ def tt(strings)
+ Builder.new(ast(strings)).transition_table
+ end
end
end
end
diff --git a/actionpack/test/journey/gtg/transition_table_test.rb b/actionpack/test/journey/gtg/transition_table_test.rb
index b968780d8d..9044934f05 100644
--- a/actionpack/test/journey/gtg/transition_table_test.rb
+++ b/actionpack/test/journey/gtg/transition_table_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'active_support/json/decoding'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/json/decoding"
module ActionDispatch
module Journey
@@ -14,12 +16,12 @@ module ActionDispatch
}
json = ActiveSupport::JSON.decode table.to_json
- assert json['regexp_states']
- assert json['string_states']
- assert json['accepting']
+ assert json["regexp_states"]
+ assert json["string_states"]
+ assert json["accepting"]
end
- if system("dot -V 2>/dev/null")
+ if system("dot -V", 2 => File::NULL)
def test_to_svg
table = tt %w{
/articles(.:format)
@@ -34,26 +36,26 @@ module ActionDispatch
end
def test_simulate_gt
- sim = simulator_for ['/foo', '/bar']
- assert_match sim, '/foo'
+ sim = simulator_for ["/foo", "/bar"]
+ assert_match_route sim, "/foo"
end
def test_simulate_gt_regexp
- sim = simulator_for [':foo']
- assert_match sim, 'foo'
+ sim = simulator_for [":foo"]
+ assert_match_route sim, "foo"
end
def test_simulate_gt_regexp_mix
- sim = simulator_for ['/get', '/:method/foo']
- assert_match sim, '/get'
- assert_match sim, '/get/foo'
+ sim = simulator_for ["/get", "/:method/foo"]
+ assert_match_route sim, "/get"
+ assert_match_route sim, "/get/foo"
end
def test_simulate_optional
- sim = simulator_for ['/foo(/bar)']
- assert_match sim, '/foo'
- assert_match sim, '/foo/bar'
- assert_no_match sim, '/foo/'
+ sim = simulator_for ["/foo(/bar)"]
+ assert_match_route sim, "/foo"
+ assert_match_route sim, "/foo/bar"
+ assert_no_match_route sim, "/foo/"
end
def test_match_data
@@ -65,11 +67,11 @@ module ActionDispatch
sim = GTG::Simulator.new tt
- match = sim.match '/get'
- assert_equal [paths.first], match.memos
+ memos = sim.memos "/get"
+ assert_equal [paths.first], memos
- match = sim.match '/get/foo'
- assert_equal [paths.last], match.memos
+ memos = sim.memos "/get/foo"
+ assert_equal [paths.last], memos
end
def test_match_data_ambiguous
@@ -86,29 +88,37 @@ module ActionDispatch
builder = GTG::Builder.new ast
sim = GTG::Simulator.new builder.transition_table
- match = sim.match '/articles/new'
- assert_equal [paths[1], paths[3]], match.memos
+ memos = sim.memos "/articles/new"
+ assert_equal [paths[1], paths[3]], memos
end
private
- def asts paths
- parser = Journey::Parser.new
- paths.map { |x|
- ast = parser.parse x
- ast.each { |n| n.memo = ast}
- ast
- }
- end
+ def asts(paths)
+ parser = Journey::Parser.new
+ paths.map { |x|
+ ast = parser.parse x
+ ast.each { |n| n.memo = ast }
+ ast
+ }
+ end
- def tt paths
- x = asts paths
- builder = GTG::Builder.new Nodes::Or.new x
- builder.transition_table
- end
+ def tt(paths)
+ x = asts paths
+ builder = GTG::Builder.new Nodes::Or.new x
+ builder.transition_table
+ end
- def simulator_for paths
- GTG::Simulator.new tt(paths)
- end
+ def simulator_for(paths)
+ GTG::Simulator.new tt(paths)
+ end
+
+ def assert_match_route(simulator, path)
+ assert simulator.memos(path), "Simulator should match #{path}."
+ end
+
+ def assert_no_match_route(simulator, path)
+ assert_not simulator.memos(path) { nil }, "Simulator should not match #{path}."
+ end
end
end
end
diff --git a/actionpack/test/journey/nfa/simulator_test.rb b/actionpack/test/journey/nfa/simulator_test.rb
index 673a491fe5..6b9f87b452 100644
--- a/actionpack/test/journey/nfa/simulator_test.rb
+++ b/actionpack/test/journey/nfa/simulator_test.rb
@@ -1,47 +1,49 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module ActionDispatch
module Journey
module NFA
class TestSimulator < ActiveSupport::TestCase
def test_simulate_simple
- sim = simulator_for ['/foo']
- assert_match sim, '/foo'
+ sim = simulator_for ["/foo"]
+ assert_match sim, "/foo"
end
def test_simulate_simple_no_match
- sim = simulator_for ['/foo']
- assert_no_match sim, 'foo'
+ sim = simulator_for ["/foo"]
+ assert_no_match sim, "foo"
end
def test_simulate_simple_no_match_too_long
- sim = simulator_for ['/foo']
- assert_no_match sim, '/foo/bar'
+ sim = simulator_for ["/foo"]
+ assert_no_match sim, "/foo/bar"
end
def test_simulate_simple_no_match_wrong_string
- sim = simulator_for ['/foo']
- assert_no_match sim, '/bar'
+ sim = simulator_for ["/foo"]
+ assert_no_match sim, "/bar"
end
def test_simulate_regex
- sim = simulator_for ['/:foo/bar']
- assert_match sim, '/bar/bar'
- assert_match sim, '/foo/bar'
+ sim = simulator_for ["/:foo/bar"]
+ assert_match sim, "/bar/bar"
+ assert_match sim, "/foo/bar"
end
def test_simulate_or
- sim = simulator_for ['/foo', '/bar']
- assert_match sim, '/bar'
- assert_match sim, '/foo'
- assert_no_match sim, '/baz'
+ sim = simulator_for ["/foo", "/bar"]
+ assert_match sim, "/bar"
+ assert_match sim, "/foo"
+ assert_no_match sim, "/baz"
end
def test_simulate_optional
- sim = simulator_for ['/foo(/bar)']
- assert_match sim, '/foo'
- assert_match sim, '/foo/bar'
- assert_no_match sim, '/foo/'
+ sim = simulator_for ["/foo(/bar)"]
+ assert_match sim, "/foo"
+ assert_match sim, "/foo/bar"
+ assert_no_match sim, "/foo/"
end
def test_matchdata_has_memos
@@ -49,7 +51,7 @@ module ActionDispatch
parser = Journey::Parser.new
asts = paths.map { |x|
ast = parser.parse x
- ast.each { |n| n.memo = ast}
+ ast.each { |n| n.memo = ast }
ast
}
@@ -59,17 +61,17 @@ module ActionDispatch
sim = Simulator.new builder.transition_table
- md = sim.match '/foo'
+ md = sim.match "/foo"
assert_equal [expected], md.memos
end
def test_matchdata_memos_on_merge
parser = Journey::Parser.new
routes = [
- '/articles(.:format)',
- '/articles/new(.:format)',
- '/articles/:id/edit(.:format)',
- '/articles/:id(.:format)',
+ "/articles(.:format)",
+ "/articles/new(.:format)",
+ "/articles/:id/edit(.:format)",
+ "/articles/:id(.:format)",
].map { |path|
ast = parser.parse path
ast.each { |n| n.memo = ast }
@@ -80,13 +82,13 @@ module ActionDispatch
ast = Nodes::Or.new routes
- nfa = Journey::NFA::Builder.new ast
+ nfa = Journey::NFA::Builder.new ast
sim = Simulator.new nfa.transition_table
- md = sim.match '/articles'
+ md = sim.match "/articles"
assert_equal [asts.first], md.memos
end
- def simulator_for paths
+ def simulator_for(paths)
parser = Journey::Parser.new
asts = paths.map { |x| parser.parse x }
builder = Builder.new Nodes::Or.new asts
diff --git a/actionpack/test/journey/nfa/transition_table_test.rb b/actionpack/test/journey/nfa/transition_table_test.rb
index 1248082c03..c23611e980 100644
--- a/actionpack/test/journey/nfa/transition_table_test.rb
+++ b/actionpack/test/journey/nfa/transition_table_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module ActionDispatch
module Journey
@@ -9,63 +11,63 @@ module ActionDispatch
end
def test_eclosure
- table = tt '/'
+ table = tt "/"
assert_equal [0], table.eclosure(0)
- table = tt ':a|:b'
+ table = tt ":a|:b"
assert_equal 3, table.eclosure(0).length
- table = tt '(:a|:b)'
+ table = tt "(:a|:b)"
assert_equal 5, table.eclosure(0).length
assert_equal 5, table.eclosure([0]).length
end
def test_following_states_one
- table = tt '/'
+ table = tt "/"
- assert_equal [1], table.following_states(0, '/')
- assert_equal [1], table.following_states([0], '/')
+ assert_equal [1], table.following_states(0, "/")
+ assert_equal [1], table.following_states([0], "/")
end
def test_following_states_group
- table = tt 'a|b'
+ table = tt "a|b"
states = table.eclosure 0
- assert_equal 1, table.following_states(states, 'a').length
- assert_equal 1, table.following_states(states, 'b').length
+ assert_equal 1, table.following_states(states, "a").length
+ assert_equal 1, table.following_states(states, "b").length
end
def test_following_states_multi
- table = tt 'a|a'
+ table = tt "a|a"
states = table.eclosure 0
- assert_equal 2, table.following_states(states, 'a').length
- assert_equal 0, table.following_states(states, 'b').length
+ assert_equal 2, table.following_states(states, "a").length
+ assert_equal 0, table.following_states(states, "b").length
end
def test_following_states_regexp
- table = tt 'a|:a'
+ table = tt "a|:a"
states = table.eclosure 0
- assert_equal 1, table.following_states(states, 'a').length
+ assert_equal 1, table.following_states(states, "a").length
assert_equal 1, table.following_states(states, /[^\.\/\?]+/).length
- assert_equal 0, table.following_states(states, 'b').length
+ assert_equal 0, table.following_states(states, "b").length
end
def test_alphabet
- table = tt 'a|:a'
- assert_equal [/[^\.\/\?]+/, 'a'], table.alphabet
+ table = tt "a|:a"
+ assert_equal [/[^\.\/\?]+/, "a"], table.alphabet
- table = tt 'a|a'
- assert_equal ['a'], table.alphabet
+ table = tt "a|a"
+ assert_equal ["a"], table.alphabet
end
private
- def tt string
- ast = @parser.parse string
- builder = Builder.new ast
- builder.transition_table
- end
+ def tt(string)
+ ast = @parser.parse string
+ builder = Builder.new ast
+ builder.transition_table
+ end
end
end
end
diff --git a/actionpack/test/journey/nodes/symbol_test.rb b/actionpack/test/journey/nodes/symbol_test.rb
index d411a5018a..b0622ac71a 100644
--- a/actionpack/test/journey/nodes/symbol_test.rb
+++ b/actionpack/test/journey/nodes/symbol_test.rb
@@ -1,15 +1,17 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module ActionDispatch
module Journey
module Nodes
class TestSymbol < ActiveSupport::TestCase
def test_default_regexp?
- sym = Symbol.new nil
- assert sym.default_regexp?
+ sym = Symbol.new "foo"
+ assert_predicate sym, :default_regexp?
sym.regexp = nil
- assert_not sym.default_regexp?
+ assert_not_predicate sym, :default_regexp?
end
end
end
diff --git a/actionpack/test/journey/path/pattern_test.rb b/actionpack/test/journey/path/pattern_test.rb
index 6939b426f6..3e7aea57f1 100644
--- a/actionpack/test/journey/path/pattern_test.rb
+++ b/actionpack/test/journey/path/pattern_test.rb
@@ -1,103 +1,106 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module ActionDispatch
module Journey
module Path
class TestPattern < ActiveSupport::TestCase
+ SEPARATORS = ["/", ".", "?"].join
+
x = /.+/
{
- '/:controller(/:action)' => %r{\A/(#{x})(?:/([^/.?]+))?\Z},
- '/:controller/foo' => %r{\A/(#{x})/foo\Z},
- '/:controller/:action' => %r{\A/(#{x})/([^/.?]+)\Z},
- '/:controller' => %r{\A/(#{x})\Z},
- '/:controller(/:action(/:id))' => %r{\A/(#{x})(?:/([^/.?]+)(?:/([^/.?]+))?)?\Z},
- '/:controller/:action.xml' => %r{\A/(#{x})/([^/.?]+)\.xml\Z},
- '/:controller.:format' => %r{\A/(#{x})\.([^/.?]+)\Z},
- '/:controller(.:format)' => %r{\A/(#{x})(?:\.([^/.?]+))?\Z},
- '/:controller/*foo' => %r{\A/(#{x})/(.+)\Z},
- '/:controller/*foo/bar' => %r{\A/(#{x})/(.+)/bar\Z},
- '/:foo|*bar' => %r{\A/(?:([^/.?]+)|(.+))\Z},
+ "/:controller(/:action)" => %r{\A/(#{x})(?:/([^/.?]+))?\Z},
+ "/:controller/foo" => %r{\A/(#{x})/foo\Z},
+ "/:controller/:action" => %r{\A/(#{x})/([^/.?]+)\Z},
+ "/:controller" => %r{\A/(#{x})\Z},
+ "/:controller(/:action(/:id))" => %r{\A/(#{x})(?:/([^/.?]+)(?:/([^/.?]+))?)?\Z},
+ "/:controller/:action.xml" => %r{\A/(#{x})/([^/.?]+)\.xml\Z},
+ "/:controller.:format" => %r{\A/(#{x})\.([^/.?]+)\Z},
+ "/:controller(.:format)" => %r{\A/(#{x})(?:\.([^/.?]+))?\Z},
+ "/:controller/*foo" => %r{\A/(#{x})/(.+)\Z},
+ "/:controller/*foo/bar" => %r{\A/(#{x})/(.+)/bar\Z},
+ "/:foo|*bar" => %r{\A/(?:([^/.?]+)|(.+))\Z},
}.each do |path, expected|
- define_method(:"test_to_regexp_#{path}") do
- strexp = Router::Strexp.build(
+ define_method(:"test_to_regexp_#{Regexp.escape(path)}") do
+ path = Pattern.build(
path,
- { :controller => /.+/ },
- ["/", ".", "?"]
+ { controller: /.+/ },
+ SEPARATORS,
+ true
)
- path = Pattern.new strexp
assert_equal(expected, path.to_regexp)
end
end
{
- '/:controller(/:action)' => %r{\A/(#{x})(?:/([^/.?]+))?},
- '/:controller/foo' => %r{\A/(#{x})/foo},
- '/:controller/:action' => %r{\A/(#{x})/([^/.?]+)},
- '/:controller' => %r{\A/(#{x})},
- '/:controller(/:action(/:id))' => %r{\A/(#{x})(?:/([^/.?]+)(?:/([^/.?]+))?)?},
- '/:controller/:action.xml' => %r{\A/(#{x})/([^/.?]+)\.xml},
- '/:controller.:format' => %r{\A/(#{x})\.([^/.?]+)},
- '/:controller(.:format)' => %r{\A/(#{x})(?:\.([^/.?]+))?},
- '/:controller/*foo' => %r{\A/(#{x})/(.+)},
- '/:controller/*foo/bar' => %r{\A/(#{x})/(.+)/bar},
- '/:foo|*bar' => %r{\A/(?:([^/.?]+)|(.+))},
+ "/:controller(/:action)" => %r{\A/(#{x})(?:/([^/.?]+))?},
+ "/:controller/foo" => %r{\A/(#{x})/foo},
+ "/:controller/:action" => %r{\A/(#{x})/([^/.?]+)},
+ "/:controller" => %r{\A/(#{x})},
+ "/:controller(/:action(/:id))" => %r{\A/(#{x})(?:/([^/.?]+)(?:/([^/.?]+))?)?},
+ "/:controller/:action.xml" => %r{\A/(#{x})/([^/.?]+)\.xml},
+ "/:controller.:format" => %r{\A/(#{x})\.([^/.?]+)},
+ "/:controller(.:format)" => %r{\A/(#{x})(?:\.([^/.?]+))?},
+ "/:controller/*foo" => %r{\A/(#{x})/(.+)},
+ "/:controller/*foo/bar" => %r{\A/(#{x})/(.+)/bar},
+ "/:foo|*bar" => %r{\A/(?:([^/.?]+)|(.+))},
}.each do |path, expected|
- define_method(:"test_to_non_anchored_regexp_#{path}") do
- strexp = Router::Strexp.build(
+ define_method(:"test_to_non_anchored_regexp_#{Regexp.escape(path)}") do
+ path = Pattern.build(
path,
- { :controller => /.+/ },
- ["/", ".", "?"],
+ { controller: /.+/ },
+ SEPARATORS,
false
)
- path = Pattern.new strexp
assert_equal(expected, path.to_regexp)
end
end
{
- '/:controller(/:action)' => %w{ controller action },
- '/:controller/foo' => %w{ controller },
- '/:controller/:action' => %w{ controller action },
- '/:controller' => %w{ controller },
- '/:controller(/:action(/:id))' => %w{ controller action id },
- '/:controller/:action.xml' => %w{ controller action },
- '/:controller.:format' => %w{ controller format },
- '/:controller(.:format)' => %w{ controller format },
- '/:controller/*foo' => %w{ controller foo },
- '/:controller/*foo/bar' => %w{ controller foo },
+ "/:controller(/:action)" => %w{ controller action },
+ "/:controller/foo" => %w{ controller },
+ "/:controller/:action" => %w{ controller action },
+ "/:controller" => %w{ controller },
+ "/:controller(/:action(/:id))" => %w{ controller action id },
+ "/:controller/:action.xml" => %w{ controller action },
+ "/:controller.:format" => %w{ controller format },
+ "/:controller(.:format)" => %w{ controller format },
+ "/:controller/*foo" => %w{ controller foo },
+ "/:controller/*foo/bar" => %w{ controller foo },
}.each do |path, expected|
- define_method(:"test_names_#{path}") do
- strexp = Router::Strexp.build(
+ define_method(:"test_names_#{Regexp.escape(path)}") do
+ path = Pattern.build(
path,
- { :controller => /.+/ },
- ["/", ".", "?"]
+ { controller: /.+/ },
+ SEPARATORS,
+ true
)
- path = Pattern.new strexp
assert_equal(expected, path.names)
end
end
def test_to_regexp_with_extended_group
- strexp = Router::Strexp.build(
- '/page/:name',
- { :name => /
+ path = Pattern.build(
+ "/page/:name",
+ { name: /
#ROFL
(tender|love
#MAO
)/x },
- ["/", ".", "?"]
+ SEPARATORS,
+ true
)
- path = Pattern.new strexp
- assert_match(path, '/page/tender')
- assert_match(path, '/page/love')
- assert_no_match(path, '/page/loving')
+ assert_match(path, "/page/tender")
+ assert_match(path, "/page/love")
+ assert_no_match(path, "/page/loving")
end
def test_optional_names
[
- ['/:foo(/:bar(/:baz))', %w{ bar baz }],
- ['/:foo(/:bar)', %w{ bar }],
- ['/:foo(/:bar)/:lol(/:baz)', %w{ bar baz }],
+ ["/:foo(/:bar(/:baz))", %w{ bar baz }],
+ ["/:foo(/:bar)", %w{ bar }],
+ ["/:foo(/:bar)/:lol(/:baz)", %w{ bar baz }],
].each do |pattern, list|
path = Pattern.from_string pattern
assert_equal list.sort, path.optional_names.sort
@@ -105,39 +108,37 @@ module ActionDispatch
end
def test_to_regexp_match_non_optional
- strexp = Router::Strexp.build(
- '/:name',
- { :name => /\d+/ },
- ["/", ".", "?"]
+ path = Pattern.build(
+ "/:name",
+ { name: /\d+/ },
+ SEPARATORS,
+ true
)
- path = Pattern.new strexp
- assert_match(path, '/123')
- assert_no_match(path, '/')
+ assert_match(path, "/123")
+ assert_no_match(path, "/")
end
def test_to_regexp_with_group
- strexp = Router::Strexp.build(
- '/page/:name',
- { :name => /(tender|love)/ },
- ["/", ".", "?"]
+ path = Pattern.build(
+ "/page/:name",
+ { name: /(tender|love)/ },
+ SEPARATORS,
+ true
)
- path = Pattern.new strexp
- assert_match(path, '/page/tender')
- assert_match(path, '/page/love')
- assert_no_match(path, '/page/loving')
+ assert_match(path, "/page/tender")
+ assert_match(path, "/page/love")
+ assert_no_match(path, "/page/loving")
end
def test_ast_sets_regular_expressions
- requirements = { :name => /(tender|love)/, :value => /./ }
- strexp = Router::Strexp.build(
- '/page/:name/:value',
+ requirements = { name: /(tender|love)/, value: /./ }
+ path = Pattern.build(
+ "/page/:name/:value",
requirements,
- ["/", ".", "?"]
+ SEPARATORS,
+ true
)
- assert_equal requirements, strexp.requirements
-
- path = Pattern.new strexp
nodes = path.ast.grep(Nodes::Symbol)
assert_equal 2, nodes.length
nodes.each do |node|
@@ -146,115 +147,114 @@ module ActionDispatch
end
def test_match_data_with_group
- strexp = Router::Strexp.build(
- '/page/:name',
- { :name => /(tender|love)/ },
- ["/", ".", "?"]
+ path = Pattern.build(
+ "/page/:name",
+ { name: /(tender|love)/ },
+ SEPARATORS,
+ true
)
- path = Pattern.new strexp
- match = path.match '/page/tender'
- assert_equal 'tender', match[1]
+ match = path.match "/page/tender"
+ assert_equal "tender", match[1]
assert_equal 2, match.length
end
def test_match_data_with_multi_group
- strexp = Router::Strexp.build(
- '/page/:name/:id',
- { :name => /t(((ender|love)))()/ },
- ["/", ".", "?"]
+ path = Pattern.build(
+ "/page/:name/:id",
+ { name: /t(((ender|love)))()/ },
+ SEPARATORS,
+ true
)
- path = Pattern.new strexp
- match = path.match '/page/tender/10'
- assert_equal 'tender', match[1]
- assert_equal '10', match[2]
+ match = path.match "/page/tender/10"
+ assert_equal "tender", match[1]
+ assert_equal "10", match[2]
assert_equal 3, match.length
assert_equal %w{ tender 10 }, match.captures
end
def test_star_with_custom_re
z = /\d+/
- strexp = Router::Strexp.build(
- '/page/*foo',
- { :foo => z },
- ["/", ".", "?"]
+ path = Pattern.build(
+ "/page/*foo",
+ { foo: z },
+ SEPARATORS,
+ true
)
- path = Pattern.new strexp
assert_equal(%r{\A/page/(#{z})\Z}, path.to_regexp)
end
def test_insensitive_regexp_with_group
- strexp = Router::Strexp.build(
- '/page/:name/aaron',
- { :name => /(tender|love)/i },
- ["/", ".", "?"]
+ path = Pattern.build(
+ "/page/:name/aaron",
+ { name: /(tender|love)/i },
+ SEPARATORS,
+ true
)
- path = Pattern.new strexp
- assert_match(path, '/page/TENDER/aaron')
- assert_match(path, '/page/loVE/aaron')
- assert_no_match(path, '/page/loVE/AAron')
+ assert_match(path, "/page/TENDER/aaron")
+ assert_match(path, "/page/loVE/aaron")
+ assert_no_match(path, "/page/loVE/AAron")
end
def test_to_regexp_with_strexp
- strexp = Router::Strexp.build('/:controller', { }, ["/", ".", "?"])
- path = Pattern.new strexp
+ path = Pattern.build("/:controller", {}, SEPARATORS, true)
x = %r{\A/([^/.?]+)\Z}
assert_equal(x.source, path.source)
end
def test_to_regexp_defaults
- path = Pattern.from_string '/:controller(/:action(/:id))'
+ path = Pattern.from_string "/:controller(/:action(/:id))"
expected = %r{\A/([^/.?]+)(?:/([^/.?]+)(?:/([^/.?]+))?)?\Z}
assert_equal expected, path.to_regexp
end
def test_failed_match
- path = Pattern.from_string '/:controller(/:action(/:id(.:format)))'
- uri = 'content'
+ path = Pattern.from_string "/:controller(/:action(/:id(.:format)))"
+ uri = "content"
assert_not path =~ uri
end
def test_match_controller
- path = Pattern.from_string '/:controller(/:action(/:id(.:format)))'
- uri = '/content'
+ path = Pattern.from_string "/:controller(/:action(/:id(.:format)))"
+ uri = "/content"
match = path =~ uri
assert_equal %w{ controller action id format }, match.names
- assert_equal 'content', match[1]
+ assert_equal "content", match[1]
assert_nil match[2]
assert_nil match[3]
assert_nil match[4]
end
def test_match_controller_action
- path = Pattern.from_string '/:controller(/:action(/:id(.:format)))'
- uri = '/content/list'
+ path = Pattern.from_string "/:controller(/:action(/:id(.:format)))"
+ uri = "/content/list"
match = path =~ uri
assert_equal %w{ controller action id format }, match.names
- assert_equal 'content', match[1]
- assert_equal 'list', match[2]
+ assert_equal "content", match[1]
+ assert_equal "list", match[2]
assert_nil match[3]
assert_nil match[4]
end
def test_match_controller_action_id
- path = Pattern.from_string '/:controller(/:action(/:id(.:format)))'
- uri = '/content/list/10'
+ path = Pattern.from_string "/:controller(/:action(/:id(.:format)))"
+ uri = "/content/list/10"
match = path =~ uri
assert_equal %w{ controller action id format }, match.names
- assert_equal 'content', match[1]
- assert_equal 'list', match[2]
- assert_equal '10', match[3]
+ assert_equal "content", match[1]
+ assert_equal "list", match[2]
+ assert_equal "10", match[3]
assert_nil match[4]
end
def test_match_literal
path = Path::Pattern.from_string "/books(/:action(.:format))"
- uri = '/books'
+ uri = "/books"
match = path =~ uri
assert_equal %w{ action format }, match.names
assert_nil match[1]
@@ -264,21 +264,21 @@ module ActionDispatch
def test_match_literal_with_action
path = Path::Pattern.from_string "/books(/:action(.:format))"
- uri = '/books/list'
+ uri = "/books/list"
match = path =~ uri
assert_equal %w{ action format }, match.names
- assert_equal 'list', match[1]
+ assert_equal "list", match[1]
assert_nil match[2]
end
def test_match_literal_with_action_and_format
path = Path::Pattern.from_string "/books(/:action(.:format))"
- uri = '/books/list.rss'
+ uri = "/books/list.rss"
match = path =~ uri
assert_equal %w{ action format }, match.names
- assert_equal 'list', match[1]
- assert_equal 'rss', match[2]
+ assert_equal "list", match[1]
+ assert_equal "rss", match[2]
end
end
end
diff --git a/actionpack/test/journey/route/definition/parser_test.rb b/actionpack/test/journey/route/definition/parser_test.rb
index d7d7172a40..39693198b8 100644
--- a/actionpack/test/journey/route/definition/parser_test.rb
+++ b/actionpack/test/journey/route/definition/parser_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module ActionDispatch
module Journey
@@ -9,88 +11,88 @@ module ActionDispatch
end
def test_slash
- assert_equal :SLASH, @parser.parse('/').type
- assert_round_trip '/'
+ assert_equal :SLASH, @parser.parse("/").type
+ assert_round_trip "/"
end
def test_segment
- assert_round_trip '/foo'
+ assert_round_trip "/foo"
end
def test_segments
- assert_round_trip '/foo/bar'
+ assert_round_trip "/foo/bar"
end
def test_segment_symbol
- assert_round_trip '/foo/:id'
+ assert_round_trip "/foo/:id"
end
def test_symbol
- assert_round_trip '/:foo'
+ assert_round_trip "/:foo"
end
def test_group
- assert_round_trip '(/:foo)'
+ assert_round_trip "(/:foo)"
end
def test_groups
- assert_round_trip '(/:foo)(/:bar)'
+ assert_round_trip "(/:foo)(/:bar)"
end
def test_nested_groups
- assert_round_trip '(/:foo(/:bar))'
+ assert_round_trip "(/:foo(/:bar))"
end
def test_dot_symbol
- assert_round_trip('.:format')
+ assert_round_trip(".:format")
end
def test_dot_literal
- assert_round_trip('.xml')
+ assert_round_trip(".xml")
end
def test_segment_dot
- assert_round_trip('/foo.:bar')
+ assert_round_trip("/foo.:bar")
end
def test_segment_group_dot
- assert_round_trip('/foo(.:bar)')
+ assert_round_trip("/foo(.:bar)")
end
def test_segment_group
- assert_round_trip('/foo(/:action)')
+ assert_round_trip("/foo(/:action)")
end
def test_segment_groups
- assert_round_trip('/foo(/:action)(/:bar)')
+ assert_round_trip("/foo(/:action)(/:bar)")
end
def test_segment_nested_groups
- assert_round_trip('/foo(/:action(/:bar))')
+ assert_round_trip("/foo(/:action(/:bar))")
end
def test_group_followed_by_path
- assert_round_trip('/foo(/:action)/:bar')
+ assert_round_trip("/foo(/:action)/:bar")
end
def test_star
- assert_round_trip('*foo')
- assert_round_trip('/*foo')
- assert_round_trip('/bar/*foo')
- assert_round_trip('/bar/(*foo)')
+ assert_round_trip("*foo")
+ assert_round_trip("/*foo")
+ assert_round_trip("/bar/*foo")
+ assert_round_trip("/bar/(*foo)")
end
def test_or
- assert_round_trip('a|b')
- assert_round_trip('a|b|c')
- assert_round_trip('(a|b)|c')
- assert_round_trip('a|(b|c)')
- assert_round_trip('*a|(b|c)')
- assert_round_trip('*a|:b|c')
+ assert_round_trip("a|b")
+ assert_round_trip("a|b|c")
+ assert_round_trip("(a|b)|c")
+ assert_round_trip("a|(b|c)")
+ assert_round_trip("*a|(b|c)")
+ assert_round_trip("*a|:b|c")
end
def test_arbitrary
- assert_round_trip('/bar/*foo#')
+ assert_round_trip("/bar/*foo#")
end
def test_literal_dot_paren
@@ -101,7 +103,7 @@ module ActionDispatch
assert_round_trip "/(:locale)(.:format)"
end
- def assert_round_trip str
+ def assert_round_trip(str)
assert_equal str, @parser.parse(str).to_s
end
end
diff --git a/actionpack/test/journey/route/definition/scanner_test.rb b/actionpack/test/journey/route/definition/scanner_test.rb
index 7a510f1e07..092177d315 100644
--- a/actionpack/test/journey/route/definition/scanner_test.rb
+++ b/actionpack/test/journey/route/definition/scanner_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module ActionDispatch
module Journey
@@ -8,61 +10,70 @@ module ActionDispatch
@scanner = Scanner.new
end
- # /page/:id(/:action)(.:format)
- def test_tokens
- [
- ['/', [[:SLASH, '/']]],
- ['*omg', [[:STAR, '*omg']]],
- ['/page', [[:SLASH, '/'], [:LITERAL, 'page']]],
- ['/page!', [[:SLASH, '/'], [:LITERAL, 'page!']]],
- ['/page$', [[:SLASH, '/'], [:LITERAL, 'page$']]],
- ['/page&', [[:SLASH, '/'], [:LITERAL, 'page&']]],
- ["/page'", [[:SLASH, '/'], [:LITERAL, "page'"]]],
- ['/page*', [[:SLASH, '/'], [:LITERAL, 'page*']]],
- ['/page+', [[:SLASH, '/'], [:LITERAL, 'page+']]],
- ['/page,', [[:SLASH, '/'], [:LITERAL, 'page,']]],
- ['/page;', [[:SLASH, '/'], [:LITERAL, 'page;']]],
- ['/page=', [[:SLASH, '/'], [:LITERAL, 'page=']]],
- ['/page@', [[:SLASH, '/'], [:LITERAL, 'page@']]],
- ['/page\:', [[:SLASH, '/'], [:LITERAL, 'page:']]],
- ['/page\(', [[:SLASH, '/'], [:LITERAL, 'page(']]],
- ['/page\)', [[:SLASH, '/'], [:LITERAL, 'page)']]],
- ['/~page', [[:SLASH, '/'], [:LITERAL, '~page']]],
- ['/pa-ge', [[:SLASH, '/'], [:LITERAL, 'pa-ge']]],
- ['/:page', [[:SLASH, '/'], [:SYMBOL, ':page']]],
- ['/(:page)', [
- [:SLASH, '/'],
- [:LPAREN, '('],
- [:SYMBOL, ':page'],
- [:RPAREN, ')'],
- ]],
- ['(/:action)', [
- [:LPAREN, '('],
- [:SLASH, '/'],
- [:SYMBOL, ':action'],
- [:RPAREN, ')'],
- ]],
- ['(())', [[:LPAREN, '('],
- [:LPAREN, '('], [:RPAREN, ')'], [:RPAREN, ')']]],
- ['(.:format)', [
- [:LPAREN, '('],
- [:DOT, '.'],
- [:SYMBOL, ':format'],
- [:RPAREN, ')'],
+ CASES = [
+ ["/", [[:SLASH, "/"]]],
+ ["*omg", [[:STAR, "*omg"]]],
+ ["/page", [[:SLASH, "/"], [:LITERAL, "page"]]],
+ ["/page!", [[:SLASH, "/"], [:LITERAL, "page!"]]],
+ ["/page$", [[:SLASH, "/"], [:LITERAL, "page$"]]],
+ ["/page&", [[:SLASH, "/"], [:LITERAL, "page&"]]],
+ ["/page'", [[:SLASH, "/"], [:LITERAL, "page'"]]],
+ ["/page*", [[:SLASH, "/"], [:LITERAL, "page*"]]],
+ ["/page+", [[:SLASH, "/"], [:LITERAL, "page+"]]],
+ ["/page,", [[:SLASH, "/"], [:LITERAL, "page,"]]],
+ ["/page;", [[:SLASH, "/"], [:LITERAL, "page;"]]],
+ ["/page=", [[:SLASH, "/"], [:LITERAL, "page="]]],
+ ["/page@", [[:SLASH, "/"], [:LITERAL, "page@"]]],
+ ['/page\:', [[:SLASH, "/"], [:LITERAL, "page:"]]],
+ ['/page\(', [[:SLASH, "/"], [:LITERAL, "page("]]],
+ ['/page\)', [[:SLASH, "/"], [:LITERAL, "page)"]]],
+ ["/~page", [[:SLASH, "/"], [:LITERAL, "~page"]]],
+ ["/pa-ge", [[:SLASH, "/"], [:LITERAL, "pa-ge"]]],
+ ["/:page", [[:SLASH, "/"], [:SYMBOL, ":page"]]],
+ ["/:page|*foo", [
+ [:SLASH, "/"],
+ [:SYMBOL, ":page"],
+ [:OR, "|"],
+ [:STAR, "*foo"]
]],
- ].each do |str, expected|
- @scanner.scan_setup str
- assert_tokens expected, @scanner
+ ["/(:page)", [
+ [:SLASH, "/"],
+ [:LPAREN, "("],
+ [:SYMBOL, ":page"],
+ [:RPAREN, ")"],
+ ]],
+ ["(/:action)", [
+ [:LPAREN, "("],
+ [:SLASH, "/"],
+ [:SYMBOL, ":action"],
+ [:RPAREN, ")"],
+ ]],
+ ["(())", [[:LPAREN, "("],
+ [:LPAREN, "("], [:RPAREN, ")"], [:RPAREN, ")"]]],
+ ["(.:format)", [
+ [:LPAREN, "("],
+ [:DOT, "."],
+ [:SYMBOL, ":format"],
+ [:RPAREN, ")"],
+ ]],
+ ]
+
+ CASES.each do |pattern, expected_tokens|
+ test "Scanning `#{pattern}`" do
+ @scanner.scan_setup pattern
+ assert_tokens expected_tokens, @scanner, pattern
end
end
- def assert_tokens tokens, scanner
- toks = []
- while tok = scanner.next_token
- toks << tok
+ private
+
+ def assert_tokens(expected_tokens, scanner, pattern)
+ actual_tokens = []
+ while token = scanner.next_token
+ actual_tokens << token
+ end
+ assert_equal expected_tokens, actual_tokens, "Wrong tokens for `#{pattern}`"
end
- assert_equal tokens, toks
- end
end
end
end
diff --git a/actionpack/test/journey/route_test.rb b/actionpack/test/journey/route_test.rb
index eff96a0abc..a8bf4a11e2 100644
--- a/actionpack/test/journey/route_test.rb
+++ b/actionpack/test/journey/route_test.rb
@@ -1,13 +1,15 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module ActionDispatch
module Journey
class TestRoute < ActiveSupport::TestCase
def test_initialize
app = Object.new
- path = Path::Pattern.from_string '/:controller(/:action(/:id(.:format)))'
+ path = Path::Pattern.from_string "/:controller(/:action(/:id(.:format)))"
defaults = {}
- route = Route.new("name", app, path, {}, [], defaults)
+ route = Route.build("name", app, path, {}, [], defaults)
assert_equal app, route.app
assert_equal path, route.path
@@ -16,9 +18,9 @@ module ActionDispatch
def test_route_adds_itself_as_memo
app = Object.new
- path = Path::Pattern.from_string '/:controller(/:action(/:id(.:format)))'
+ path = Path::Pattern.from_string "/:controller(/:action(/:id(.:format)))"
defaults = {}
- route = Route.new("name", app, path, {}, [], defaults)
+ route = Route.build("name", app, path, {}, [], defaults)
route.ast.grep(Nodes::Terminal).each do |node|
assert_equal route, node.memo
@@ -26,80 +28,77 @@ module ActionDispatch
end
def test_path_requirements_override_defaults
- strexp = Router::Strexp.build(':name', { name: /love/ }, ['/'])
- path = Path::Pattern.new strexp
- defaults = { name: 'tender' }
- route = Route.new('name', nil, path, nil, [], defaults)
+ path = Path::Pattern.build(":name", { name: /love/ }, "/", true)
+ defaults = { name: "tender" }
+ route = Route.build("name", nil, path, {}, [], defaults)
assert_equal(/love/, route.requirements[:name])
end
def test_ip_address
- path = Path::Pattern.from_string '/messages/:id(.:format)'
- route = Route.new("name", nil, path, {:ip => '192.168.1.1'}, [],
- { :controller => 'foo', :action => 'bar' })
- assert_equal '192.168.1.1', route.ip
+ path = Path::Pattern.from_string "/messages/:id(.:format)"
+ route = Route.build("name", nil, path, { ip: "192.168.1.1" }, [],
+ controller: "foo", action: "bar")
+ assert_equal "192.168.1.1", route.ip
end
def test_default_ip
- path = Path::Pattern.from_string '/messages/:id(.:format)'
- route = Route.new("name", nil, path, {}, [],
- { :controller => 'foo', :action => 'bar' })
+ path = Path::Pattern.from_string "/messages/:id(.:format)"
+ route = Route.build("name", nil, path, {}, [],
+ controller: "foo", action: "bar")
assert_equal(//, route.ip)
end
def test_format_with_star
- path = Path::Pattern.from_string '/:controller/*extra'
- route = Route.new("name", nil, path, {}, [],
- { :controller => 'foo', :action => 'bar' })
- assert_equal '/foo/himom', route.format({
- :controller => 'foo',
- :extra => 'himom',
- })
+ path = Path::Pattern.from_string "/:controller/*extra"
+ route = Route.build("name", nil, path, {}, [],
+ controller: "foo", action: "bar")
+ assert_equal "/foo/himom", route.format(
+ controller: "foo",
+ extra: "himom")
end
def test_connects_all_match
- path = Path::Pattern.from_string '/:controller(/:action(/:id(.:format)))'
- route = Route.new("name", nil, path, {:action => 'bar'}, [], { :controller => 'foo' })
-
- assert_equal '/foo/bar/10', route.format({
- :controller => 'foo',
- :action => 'bar',
- :id => 10
- })
+ path = Path::Pattern.from_string "/:controller(/:action(/:id(.:format)))"
+ route = Route.build("name", nil, path, { action: "bar" }, [], controller: "foo")
+
+ assert_equal "/foo/bar/10", route.format(
+ controller: "foo",
+ action: "bar",
+ id: 10)
end
def test_extras_are_not_included_if_optional
- path = Path::Pattern.from_string '/page/:id(/:action)'
- route = Route.new("name", nil, path, { }, [], { :action => 'show' })
+ path = Path::Pattern.from_string "/page/:id(/:action)"
+ route = Route.build("name", nil, path, {}, [], action: "show")
- assert_equal '/page/10', route.format({ :id => 10 })
+ assert_equal "/page/10", route.format(id: 10)
end
def test_extras_are_not_included_if_optional_with_parameter
- path = Path::Pattern.from_string '(/sections/:section)/pages/:id'
- route = Route.new("name", nil, path, { }, [], { :action => 'show' })
+ path = Path::Pattern.from_string "(/sections/:section)/pages/:id"
+ route = Route.build("name", nil, path, {}, [], action: "show")
- assert_equal '/pages/10', route.format({:id => 10})
+ assert_equal "/pages/10", route.format(id: 10)
end
def test_extras_are_not_included_if_optional_parameter_is_nil
- path = Path::Pattern.from_string '(/sections/:section)/pages/:id'
- route = Route.new("name", nil, path, { }, [], { :action => 'show' })
+ path = Path::Pattern.from_string "(/sections/:section)/pages/:id"
+ route = Route.build("name", nil, path, {}, [], action: "show")
- assert_equal '/pages/10', route.format({:id => 10, :section => nil})
+ assert_equal "/pages/10", route.format(id: 10, section: nil)
end
def test_score
constraints = {}
- defaults = {:controller=>"pages", :action=>"show"}
+ defaults = { controller: "pages", action: "show" }
path = Path::Pattern.from_string "/page/:id(/:action)(.:format)"
- specific = Route.new "name", nil, path, constraints, [:controller, :action], defaults
+ specific = Route.build "name", nil, path, constraints, [:controller, :action], defaults
path = Path::Pattern.from_string "/:controller(/:action(/:id))(.:format)"
- generic = Route.new "name", nil, path, constraints, [], {}
+ 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/journey/router/utils_test.rb b/actionpack/test/journey/router/utils_test.rb
index 2b505f081e..472f1bf35e 100644
--- a/actionpack/test/journey/router/utils_test.rb
+++ b/actionpack/test/journey/router/utils_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module ActionDispatch
module Journey
@@ -21,7 +23,7 @@ module ActionDispatch
end
def test_uri_unescape_with_utf8_string
- assert_equal "Šašinková", Utils.unescape_uri("%C5%A0a%C5%A1inkov%C3%A1".force_encoding(Encoding::US_ASCII))
+ assert_equal "Šašinková", Utils.unescape_uri((+"%C5%A0a%C5%A1inkov%C3%A1").force_encoding(Encoding::US_ASCII))
end
def test_normalize_path_not_greedy
@@ -31,6 +33,15 @@ module ActionDispatch
def test_normalize_path_uppercase
assert_equal "/foo%AAbar%AAbaz", Utils.normalize_path("/foo%aabar%aabaz")
end
+
+ def test_normalize_path_maintains_string_encoding
+ path = "/foo%AAbar%AAbaz".b
+ assert_equal Encoding::ASCII_8BIT, Utils.normalize_path(path).encoding
+ end
+
+ def test_normalize_path_with_nil
+ assert_equal "/", Utils.normalize_path(nil)
+ end
end
end
end
diff --git a/actionpack/test/journey/router_test.rb b/actionpack/test/journey/router_test.rb
index 802fb93c69..1f4e14aef6 100644
--- a/actionpack/test/journey/router_test.rb
+++ b/actionpack/test/journey/router_test.rb
@@ -1,43 +1,25 @@
-# encoding: UTF-8
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module ActionDispatch
module Journey
class TestRouter < ActiveSupport::TestCase
- attr_reader :routes
+ attr_reader :mapper, :routes, :route_set, :router
def setup
- @app = Routing::RouteSet::Dispatcher.new({})
- @routes = Routes.new
- @router = Router.new(@routes)
- @formatter = Formatter.new(@routes)
- end
-
- class FakeRequestFeeler < Struct.new(:env, :called)
- def new env
- self.env = env
- self
- end
-
- def hello
- self.called = true
- 'world'
- end
-
- def path_info; env['PATH_INFO']; end
- def request_method; env['REQUEST_METHOD']; end
- def ip; env['REMOTE_ADDR']; end
+ @app = Routing::RouteSet::Dispatcher.new({})
+ @route_set = ActionDispatch::Routing::RouteSet.new
+ @routes = @route_set.router.routes
+ @router = @route_set.router
+ @formatter = @route_set.formatter
+ @mapper = ActionDispatch::Routing::Mapper.new @route_set
end
def test_dashes
- router = Router.new(routes)
-
- exp = Router::Strexp.build '/foo-bar-baz', {}, ['/.?']
- path = Path::Pattern.new exp
-
- routes.add_route nil, path, {}, [], {:id => nil}, {}
+ get "/foo-bar-baz", to: "foo#bar"
- env = rails_env 'PATH_INFO' => '/foo-bar-baz'
+ env = rails_env "PATH_INFO" => "/foo-bar-baz"
called = false
router.recognize(env) do |r, params|
called = true
@@ -46,15 +28,10 @@ module ActionDispatch
end
def test_unicode
- router = Router.new(routes)
-
- #match the escaped version of /ほげ
- exp = Router::Strexp.build '/%E3%81%BB%E3%81%92', {}, ['/.?']
- path = Path::Pattern.new exp
-
- routes.add_route nil, path, {}, [], {:id => nil}, {}
+ get "/ほげ", to: "foo#bar"
- env = rails_env 'PATH_INFO' => '/%E3%81%BB%E3%81%92'
+ # match the escaped version of /ほげ
+ env = rails_env "PATH_INFO" => "/%E3%81%BB%E3%81%92"
called = false
router.recognize(env) do |r, params|
called = true
@@ -62,140 +39,61 @@ module ActionDispatch
assert called
end
- def test_request_class_and_requirements_success
- klass = FakeRequestFeeler.new nil
- router = Router.new(routes)
-
- requirements = { :hello => /world/ }
-
- exp = Router::Strexp.build '/foo(/:id)', {}, ['/.?']
- path = Path::Pattern.new exp
-
- routes.add_route nil, path, requirements, [], {:id => nil}, {}
-
- env = rails_env({'PATH_INFO' => '/foo/10'}, klass)
- router.recognize(env) do |r, params|
- assert_equal({:id => '10'}, params)
- end
-
- assert klass.called, 'hello should have been called'
- assert_equal env.env, klass.env
- end
-
- def test_request_class_and_requirements_fail
- klass = FakeRequestFeeler.new nil
- router = Router.new(routes)
-
- requirements = { :hello => /mom/ }
-
- exp = Router::Strexp.build '/foo(/:id)', {}, ['/.?']
- path = Path::Pattern.new exp
-
- router.routes.add_route nil, path, requirements, [], {:id => nil}, {}
-
- env = rails_env({'PATH_INFO' => '/foo/10'}, klass)
- router.recognize(env) do |r, params|
- flunk 'route should not be found'
- end
-
- assert klass.called, 'hello should have been called'
- assert_equal env.env, klass.env
- end
-
- class CustomPathRequest < ActionDispatch::Request
- def path_info
- env['custom.path_info']
- end
-
- def path_info=(x)
- env['custom.path_info'] = x
- end
- end
-
- def test_request_class_overrides_path_info
- router = Router.new(routes)
-
- exp = Router::Strexp.build '/bar', {}, ['/.?']
- path = Path::Pattern.new exp
-
- routes.add_route nil, path, {}, [], {}, {}
-
- env = rails_env({'PATH_INFO' => '/foo',
- 'custom.path_info' => '/bar'}, CustomPathRequest)
-
- recognized = false
- router.recognize(env) do |r, params|
- recognized = true
- end
-
- assert recognized, "route should have been recognized"
- end
-
def test_regexp_first_precedence
- add_routes @router, [
- Router::Strexp.build("/whois/:domain", {:domain => /\w+\.[\w\.]+/}, ['/', '.', '?']),
- Router::Strexp.build("/whois/:id(.:format)", {}, ['/', '.', '?'])
- ]
+ get "/whois/:domain", domain: /\w+\.[\w\.]+/, to: "foo#bar"
+ get "/whois/:id(.:format)", to: "foo#baz"
- env = rails_env 'PATH_INFO' => '/whois/example.com'
+ env = rails_env "PATH_INFO" => "/whois/example.com"
list = []
- @router.recognize(env) do |r, params|
+ router.recognize(env) do |r, params|
list << r
end
assert_equal 2, list.length
r = list.first
- assert_equal '/whois/:domain', r.path.spec.to_s
+ assert_equal "/whois/:domain(.:format)", r.path.spec.to_s
end
def test_required_parts_verified_are_anchored
- add_routes @router, [
- Router::Strexp.build("/foo/:id", { :id => /\d/ }, ['/', '.', '?'], false)
- ]
+ get "/foo/:id", id: /\d/, anchor: false, to: "foo#bar"
assert_raises(ActionController::UrlGenerationError) do
- @formatter.generate(nil, { :id => '10' }, { })
+ @formatter.generate(nil, { controller: "foo", action: "bar", id: "10" }, {})
end
end
def test_required_parts_are_verified_when_building
- add_routes @router, [
- Router::Strexp.build("/foo/:id", { :id => /\d+/ }, ['/', '.', '?'], false)
- ]
+ get "/foo/:id", id: /\d+/, anchor: false, to: "foo#bar"
- path, _ = @formatter.generate(nil, { :id => '10' }, { })
- assert_equal '/foo/10', path
+ path, _ = @formatter.generate(nil, { controller: "foo", action: "bar", id: "10" }, {})
+ assert_equal "/foo/10", path
assert_raises(ActionController::UrlGenerationError) do
- @formatter.generate(nil, { :id => 'aa' }, { })
+ @formatter.generate(nil, { id: "aa" }, {})
end
end
def test_only_required_parts_are_verified
- add_routes @router, [
- Router::Strexp.build("/foo(/:id)", {:id => /\d/}, ['/', '.', '?'], false)
- ]
+ get "/foo(/:id)", id: /\d/, to: "foo#bar"
- path, _ = @formatter.generate(nil, { :id => '10' }, { })
- assert_equal '/foo/10', path
+ path, _ = @formatter.generate(nil, { controller: "foo", action: "bar", id: "10" }, {})
+ assert_equal "/foo/10", path
- path, _ = @formatter.generate(nil, { }, { })
- assert_equal '/foo', path
+ path, _ = @formatter.generate(nil, { controller: "foo", action: "bar" }, {})
+ assert_equal "/foo", path
- path, _ = @formatter.generate(nil, { :id => 'aa' }, { })
- assert_equal '/foo/aa', path
+ path, _ = @formatter.generate(nil, { controller: "foo", action: "bar", id: "aa" }, {})
+ assert_equal "/foo/aa", path
end
def test_knows_what_parts_are_missing_from_named_route
route_name = "gorby_thunderhorse"
- pattern = Router::Strexp.build("/foo/:id", { :id => /\d+/ }, ['/', '.', '?'], false)
- path = Path::Pattern.new pattern
- @router.routes.add_route nil, path, {}, [], {}, route_name
+ get "/foo/:id", as: route_name, id: /\d+/, to: "foo#bar"
error = assert_raises(ActionController::UrlGenerationError) do
- @formatter.generate(route_name, { }, { })
+ @formatter.generate(route_name, {}, {})
end
assert_match(/missing required keys: \[:id\]/, error.message)
@@ -205,142 +103,129 @@ module ActionDispatch
route_name = "gorby_thunderhorse"
error = assert_raises(ActionController::UrlGenerationError) do
- @formatter.generate(route_name, { }, { })
+ @formatter.generate(route_name, {}, {})
end
assert_no_match(/missing required keys: \[\]/, error.message)
end
def test_X_Cascade
- add_routes @router, [ "/messages(.:format)" ]
- resp = @router.serve(rails_env({ 'REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/lol' }))
- assert_equal ['Not Found'], resp.last
- assert_equal 'pass', resp[1]['X-Cascade']
+ get "/messages(.:format)", to: "foo#bar"
+ resp = router.serve(rails_env("REQUEST_METHOD" => "GET", "PATH_INFO" => "/lol"))
+ assert_equal ["Not Found"], resp.last
+ assert_equal "pass", resp[1]["X-Cascade"]
assert_equal 404, resp.first
end
def test_clear_trailing_slash_from_script_name_on_root_unanchored_routes
- route_set = Routing::RouteSet.new
- mapper = Routing::Mapper.new route_set
+ app = lambda { |env| [200, {}, ["success!"]] }
+ get "/weblog", to: app
- app = lambda { |env| [200, {}, ['success!']] }
- mapper.get '/weblog', :to => app
-
- env = rack_env('SCRIPT_NAME' => '', 'PATH_INFO' => '/weblog')
+ env = rack_env("SCRIPT_NAME" => "", "PATH_INFO" => "/weblog")
resp = route_set.call env
- assert_equal ['success!'], resp.last
- assert_equal '', env['SCRIPT_NAME']
+ assert_equal ["success!"], resp.last
+ assert_equal "", env["SCRIPT_NAME"]
end
def test_defaults_merge_correctly
- path = Path::Pattern.from_string '/foo(/:id)'
- @router.routes.add_route nil, path, {}, [], {:id => nil}, {}
+ get "/foo(/:id)", to: "foo#bar", id: nil
- env = rails_env 'PATH_INFO' => '/foo/10'
- @router.recognize(env) do |r, params|
- assert_equal({:id => '10'}, params)
+ env = rails_env "PATH_INFO" => "/foo/10"
+ router.recognize(env) do |r, params|
+ assert_equal({ id: "10", controller: "foo", action: "bar" }, params)
end
- env = rails_env 'PATH_INFO' => '/foo'
- @router.recognize(env) do |r, params|
- assert_equal({:id => nil}, params)
+ env = rails_env "PATH_INFO" => "/foo"
+ router.recognize(env) do |r, params|
+ assert_equal({ id: nil, controller: "foo", action: "bar" }, params)
end
end
def test_recognize_with_unbound_regexp
- add_routes @router, [
- Router::Strexp.build("/foo", { }, ['/', '.', '?'], false)
- ]
+ get "/foo", anchor: false, to: "foo#bar"
- env = rails_env 'PATH_INFO' => '/foo/bar'
+ env = rails_env "PATH_INFO" => "/foo/bar"
- @router.recognize(env) { |*_| }
+ router.recognize(env) { |*_| }
- assert_equal '/foo', env.env['SCRIPT_NAME']
- assert_equal '/bar', env.env['PATH_INFO']
+ assert_equal "/foo", env.env["SCRIPT_NAME"]
+ assert_equal "/bar", env.env["PATH_INFO"]
end
def test_bound_regexp_keeps_path_info
- add_routes @router, [
- Router::Strexp.build("/foo", { }, ['/', '.', '?'], true)
- ]
+ get "/foo", to: "foo#bar"
- env = rails_env 'PATH_INFO' => '/foo'
+ env = rails_env "PATH_INFO" => "/foo"
- before = env.env['SCRIPT_NAME']
+ before = env.env["SCRIPT_NAME"]
- @router.recognize(env) { |*_| }
+ router.recognize(env) { |*_| }
- assert_equal before, env.env['SCRIPT_NAME']
- assert_equal '/foo', env.env['PATH_INFO']
+ assert_equal before, env.env["SCRIPT_NAME"]
+ assert_equal "/foo", env.env["PATH_INFO"]
end
def test_path_not_found
- add_routes @router, [
+ [
"/messages(.:format)",
"/messages/new(.:format)",
"/messages/:id/edit(.:format)",
"/messages/:id(.:format)"
- ]
- env = rails_env 'PATH_INFO' => '/messages/unknown/path'
+ ].each do |path|
+ get path, to: "foo#bar"
+ end
+ env = rails_env "PATH_INFO" => "/messages/unknown/path"
yielded = false
- @router.recognize(env) do |*whatever|
+ router.recognize(env) do |*whatever|
yielded = true
end
assert_not yielded
end
def test_required_part_in_recall
- add_routes @router, [ "/messages/:a/:b" ]
+ get "/messages/:a/:b", to: "foo#bar"
- path, _ = @formatter.generate(nil, { :a => 'a' }, { :b => 'b' })
+ path, _ = @formatter.generate(nil, { controller: "foo", action: "bar", a: "a" }, { b: "b" })
assert_equal "/messages/a/b", path
end
def test_splat_in_recall
- add_routes @router, [ "/*path" ]
+ get "/*path", to: "foo#bar"
- path, _ = @formatter.generate(nil, { }, { :path => 'b' })
+ path, _ = @formatter.generate(nil, { controller: "foo", action: "bar" }, { path: "b" })
assert_equal "/b", path
end
def test_recall_should_be_used_when_scoring
- add_routes @router, [
- "/messages/:action(/:id(.:format))",
- "/messages/:id(.:format)"
- ]
+ get "/messages/:action(/:id(.:format))", to: "foo#bar"
+ get "/messages/:id(.:format)", to: "bar#baz"
- path, _ = @formatter.generate(nil, { :id => 10 }, { :action => 'index' })
+ path, _ = @formatter.generate(nil, { controller: "foo", id: 10 }, { action: "index" })
assert_equal "/messages/index/10", path
end
def test_nil_path_parts_are_ignored
- path = Path::Pattern.from_string "/:controller(/:action(.:format))"
- @router.routes.add_route @app, path, {}, [], {}, {}
+ get "/:controller(/:action(.:format))", to: "tasks#lol"
- params = { :controller => "tasks", :format => nil }
- extras = { :action => 'lol' }
+ params = { controller: "tasks", format: nil }
+ extras = { action: "lol" }
path, _ = @formatter.generate(nil, params, extras)
- assert_equal '/tasks', path
+ assert_equal "/tasks", path
end
def test_generate_slash
params = [ [:controller, "tasks"],
[:action, "show"] ]
- str = Router::Strexp.build("/", Hash[params], ['/', '.', '?'], true)
- path = Path::Pattern.new str
-
- @router.routes.add_route @app, path, {}, [], {}, {}
+ get "/", Hash[params]
path, _ = @formatter.generate(nil, Hash[params], {})
- assert_equal '/', path
+ assert_equal "/", path
end
def test_generate_calls_param_proc
- path = Path::Pattern.from_string '/:controller(/:action)'
- @router.routes.add_route @app, path, {}, [], {}, {}
+ get "/:controller(/:action)", to: "foo#bar"
parameterized = []
params = [ [:controller, "tasks"],
@@ -350,76 +235,71 @@ module ActionDispatch
nil,
Hash[params],
{},
- lambda { |k,v| parameterized << [k,v]; v })
+ lambda { |k, v| parameterized << [k, v]; v })
assert_equal params.map(&:to_s).sort, parameterized.map(&:to_s).sort
end
def test_generate_id
- path = Path::Pattern.from_string '/:controller(/:action)'
- @router.routes.add_route @app, path, {}, [], {}, {}
+ get "/:controller(/:action)", to: "foo#bar"
path, params = @formatter.generate(
- nil, {:id=>1, :controller=>"tasks", :action=>"show"}, {})
- assert_equal '/tasks/show', path
- assert_equal({:id => 1}, params)
+ nil, { id: 1, controller: "tasks", action: "show" }, {})
+ assert_equal "/tasks/show", path
+ assert_equal({ id: 1 }, params)
end
def test_generate_escapes
- path = Path::Pattern.from_string '/:controller(/:action)'
- @router.routes.add_route @app, path, {}, [], {}, {}
+ get "/:controller(/:action)", to: "foo#bar"
path, _ = @formatter.generate(nil,
- { :controller => "tasks",
- :action => "a/b c+d",
+ { controller: "tasks",
+ action: "a/b c+d",
}, {})
- assert_equal '/tasks/a%2Fb%20c+d', path
+ assert_equal "/tasks/a%2Fb%20c+d", path
end
def test_generate_escapes_with_namespaced_controller
- path = Path::Pattern.from_string '/:controller(/:action)'
- @router.routes.add_route @app, path, {}, [], {}, {}
+ get "/:controller(/:action)", to: "foo#bar"
path, _ = @formatter.generate(
- nil, { :controller => "admin/tasks",
- :action => "a/b c+d",
+ nil, { controller: "admin/tasks",
+ action: "a/b c+d",
}, {})
- assert_equal '/admin/tasks/a%2Fb%20c+d', path
+ assert_equal "/admin/tasks/a%2Fb%20c+d", path
end
def test_generate_extra_params
- path = Path::Pattern.from_string '/:controller(/:action)'
- @router.routes.add_route @app, path, {}, [], {}, {}
+ get "/:controller(/:action)", to: "foo#bar"
path, params = @formatter.generate(
- nil, { :id => 1,
- :controller => "tasks",
- :action => "show",
- :relative_url_root => nil
+ nil, { id: 1,
+ controller: "tasks",
+ action: "show",
+ relative_url_root: nil
}, {})
- assert_equal '/tasks/show', path
- assert_equal({:id => 1, :relative_url_root => nil}, params)
+ assert_equal "/tasks/show", path
+ assert_equal({ id: 1, relative_url_root: nil }, params)
end
def test_generate_missing_keys_no_matches_different_format_keys
- path = Path::Pattern.from_string '/:controller/:action/:name'
- @router.routes.add_route @app, path, {}, [], {}, {}
+ get "/:controller/:action/:name", to: "foo#bar"
primarty_parameters = {
- :id => 1,
- :controller => "tasks",
- :action => "show",
- :relative_url_root => nil
+ id: 1,
+ controller: "tasks",
+ action: "show",
+ relative_url_root: nil
}
redirection_parameters = {
- 'action'=>'show',
+ "action" => "show",
}
- missing_key = 'name'
- missing_parameters ={
+ missing_key = "name"
+ missing_parameters = {
missing_key => "task_1"
}
request_parameters = primarty_parameters.merge(redirection_parameters).merge(missing_parameters)
- message = "No route matches #{Hash[request_parameters.sort_by{|k,v|k.to_s}].inspect} missing required keys: #{[missing_key.to_sym].inspect}"
+ message = "No route matches #{Hash[request_parameters.sort_by { |k, v|k.to_s }].inspect}, missing required keys: #{[missing_key.to_sym].inspect}"
error = assert_raises(ActionController::UrlGenerationError) do
@formatter.generate(
@@ -429,45 +309,42 @@ module ActionDispatch
end
def test_generate_uses_recall_if_needed
- path = Path::Pattern.from_string '/:controller(/:action(/:id))'
- @router.routes.add_route @app, path, {}, [], {}, {}
+ get "/:controller(/:action(/:id))", to: "foo#bar"
path, params = @formatter.generate(
nil,
- {:controller =>"tasks", :id => 10},
- {:action =>"index"})
- assert_equal '/tasks/index/10', path
+ { controller: "tasks", id: 10 },
+ { action: "index" })
+ assert_equal "/tasks/index/10", path
assert_equal({}, params)
end
def test_generate_with_name
- path = Path::Pattern.from_string '/:controller(/:action)'
- @router.routes.add_route @app, path, {}, [], {}, "tasks"
+ get "/:controller(/:action)", to: "foo#bar", as: "tasks"
path, params = @formatter.generate(
"tasks",
- {:controller=>"tasks"},
- {:controller=>"tasks", :action=>"index"})
- assert_equal '/tasks', path
+ { controller: "tasks" },
+ { controller: "tasks", action: "index" })
+ assert_equal "/tasks", path
assert_equal({}, params)
end
{
- '/content' => { :controller => 'content' },
- '/content/list' => { :controller => 'content', :action => 'list' },
- '/content/show/10' => { :controller => 'content', :action => 'show', :id => "10" },
+ "/content" => { controller: "content" },
+ "/content/list" => { controller: "content", action: "list" },
+ "/content/show/10" => { controller: "content", action: "show", id: "10" },
}.each do |request_path, expected|
define_method("test_recognize_#{expected.keys.map(&:to_s).join('_')}") do
- path = Path::Pattern.from_string "/:controller(/:action(/:id))"
- app = Object.new
- route = @router.routes.add_route(app, path, {}, [], {}, {})
+ get "/:controller(/:action(/:id))", to: "foo#bar"
+ route = @routes.first
- env = rails_env 'PATH_INFO' => request_path
- called = false
+ env = rails_env "PATH_INFO" => request_path
+ called = false
- @router.recognize(env) do |r, params|
+ router.recognize(env) do |r, params|
assert_equal route, r
- assert_equal(expected, params)
+ assert_equal({ action: "bar" }.merge(expected), params)
called = true
end
@@ -476,20 +353,19 @@ module ActionDispatch
end
{
- :segment => ['/a%2Fb%20c+d/splat', { :segment => 'a/b c+d', :splat => 'splat' }],
- :splat => ['/segment/a/b%20c+d', { :segment => 'segment', :splat => 'a/b c+d' }]
+ segment: ["/a%2Fb%20c+d/splat", { segment: "a/b c+d", splat: "splat" }],
+ splat: ["/segment/a/b%20c+d", { segment: "segment", splat: "a/b c+d" }]
}.each do |name, (request_path, expected)|
define_method("test_recognize_#{name}") do
- path = Path::Pattern.from_string '/:segment/*splat'
- app = Object.new
- route = @router.routes.add_route(app, path, {}, [], {}, {})
+ get "/:segment/*splat", to: "foo#bar"
- env = rails_env 'PATH_INFO' => request_path
- called = false
+ env = rails_env "PATH_INFO" => request_path
+ called = false
+ route = @routes.first
- @router.recognize(env) do |r, params|
+ router.recognize(env) do |r, params|
assert_equal route, r
- assert_equal(expected, params)
+ assert_equal(expected.merge(controller: "foo", action: "bar"), params)
called = true
end
@@ -498,24 +374,18 @@ module ActionDispatch
end
def test_namespaced_controller
- strexp = Router::Strexp.build(
- "/:controller(/:action(/:id))",
- { :controller => /.+?/ },
- ["/", ".", "?"]
- )
- path = Path::Pattern.new strexp
- app = Object.new
- route = @router.routes.add_route(app, path, {}, [], {}, {})
+ get "/:controller(/:action(/:id))", controller: /.+?/
+ route = @routes.first
- env = rails_env 'PATH_INFO' => '/admin/users/show/10'
+ env = rails_env "PATH_INFO" => "/admin/users/show/10"
called = false
expected = {
- :controller => 'admin/users',
- :action => 'show',
- :id => '10'
+ controller: "admin/users",
+ action: "show",
+ id: "10"
}
- @router.recognize(env) do |r, params|
+ router.recognize(env) do |r, params|
assert_equal route, r
assert_equal(expected, params)
called = true
@@ -524,14 +394,13 @@ module ActionDispatch
end
def test_recognize_literal
- path = Path::Pattern.from_string "/books(/:action(.:format))"
- app = Object.new
- route = @router.routes.add_route(app, path, {}, [], {:controller => 'books'})
+ get "/books(/:action(.:format))", controller: "books"
+ route = @routes.first
- env = rails_env 'PATH_INFO' => '/books/list.rss'
- expected = { :controller => 'books', :action => 'list', :format => 'rss' }
+ env = rails_env "PATH_INFO" => "/books/list.rss"
+ expected = { controller: "books", action: "list", format: "rss" }
called = false
- @router.recognize(env) do |r, params|
+ router.recognize(env) do |r, params|
assert_equal route, r
assert_equal(expected, params)
called = true
@@ -541,18 +410,15 @@ module ActionDispatch
end
def test_recognize_head_route
- path = Path::Pattern.from_string "/books(/:action(.:format))"
- app = Object.new
- conditions = { request_method: 'HEAD' }
- @router.routes.add_route(app, path, conditions, [], {})
+ match "/books(/:action(.:format))", via: "head", to: "foo#bar"
env = rails_env(
- 'PATH_INFO' => '/books/list.rss',
- 'REQUEST_METHOD' => 'HEAD'
+ "PATH_INFO" => "/books/list.rss",
+ "REQUEST_METHOD" => "HEAD"
)
called = false
- @router.recognize(env) do |r, params|
+ router.recognize(env) do |r, params|
called = true
end
@@ -560,90 +426,119 @@ module ActionDispatch
end
def test_recognize_head_request_as_get_route
- path = Path::Pattern.from_string "/books(/:action(.:format))"
- app = Object.new
- conditions = {
- :request_method => 'GET'
- }
- @router.routes.add_route(app, path, conditions, [], {})
+ get "/books(/:action(.:format))", to: "foo#bar"
- env = rails_env 'PATH_INFO' => '/books/list.rss',
- "REQUEST_METHOD" => "HEAD"
+ env = rails_env "PATH_INFO" => "/books/list.rss",
+ "REQUEST_METHOD" => "HEAD"
called = false
- @router.recognize(env) do |r, params|
+ router.recognize(env) do |r, params|
called = true
end
assert called
end
- def test_recognize_cares_about_verbs
- path = Path::Pattern.from_string "/books(/:action(.:format))"
- app = Object.new
- conditions = { request_method: 'GET' }
- @router.routes.add_route(app, path, conditions, [], {})
+ def test_recognize_cares_about_get_verbs
+ match "/books(/:action(.:format))", to: "foo#bar", via: :get
- env = rails_env 'PATH_INFO' => '/books/list.rss',
+ env = rails_env "PATH_INFO" => "/books/list.rss",
"REQUEST_METHOD" => "POST"
called = false
- @router.recognize(env) do |r, params|
+ router.recognize(env) do |r, params|
called = true
end
assert_not called
+ end
- conditions = conditions.dup
- conditions[:request_method] = 'POST'
+ def test_recognize_cares_about_post_verbs
+ match "/books(/:action(.:format))", to: "foo#bar", via: :post
- post = @router.routes.add_route(app, path, conditions, [], {})
+ env = rails_env "PATH_INFO" => "/books/list.rss",
+ "REQUEST_METHOD" => "POST"
called = false
- @router.recognize(env) do |r, params|
- assert_equal post, r
+ router.recognize(env) do |r, params|
called = true
end
assert called
end
- private
+ def test_multi_verb_recognition
+ match "/books(/:action(.:format))", to: "foo#bar", via: [:post, :get]
- def add_routes router, paths
- paths.each do |path|
- if String === path
- path = Path::Pattern.from_string path
- else
- path = Path::Pattern.new path
+ %w( POST GET ).each do |verb|
+ env = rails_env "PATH_INFO" => "/books/list.rss",
+ "REQUEST_METHOD" => verb
+
+ called = false
+ router.recognize(env) do |r, params|
+ called = true
end
- router.routes.add_route @app, path, {}, [], {}, {}
+
+ assert called
end
+
+ env = rails_env "PATH_INFO" => "/books/list.rss",
+ "REQUEST_METHOD" => "PUT"
+
+ called = false
+ router.recognize(env) do |r, params|
+ called = true
+ end
+
+ assert_not called
+ end
+
+ def test_eager_load_with_routes
+ get "/foo-bar", to: "foo#bar"
+ assert_nil router.eager_load!
end
- def rails_env env, klass = ActionDispatch::Request
- klass.new(rack_env(env))
- end
-
- def rack_env env
- {
- "rack.version" => [1, 1],
- "rack.input" => StringIO.new,
- "rack.errors" => StringIO.new,
- "rack.multithread" => true,
- "rack.multiprocess" => true,
- "rack.run_once" => false,
- "REQUEST_METHOD" => "GET",
- "SERVER_NAME" => "example.org",
- "SERVER_PORT" => "80",
- "QUERY_STRING" => "",
- "PATH_INFO" => "/content",
- "rack.url_scheme" => "http",
- "HTTPS" => "off",
- "SCRIPT_NAME" => "",
- "CONTENT_LENGTH" => "0"
- }.merge env
+ def test_eager_load_without_routes
+ assert_nil router.eager_load!
end
+
+ private
+
+ def get(*args)
+ ActiveSupport::Deprecation.silence do
+ mapper.get(*args)
+ end
+ end
+
+ def match(*args)
+ ActiveSupport::Deprecation.silence do
+ mapper.match(*args)
+ end
+ end
+
+ def rails_env(env, klass = ActionDispatch::Request)
+ klass.new(rack_env(env))
+ end
+
+ def rack_env(env)
+ {
+ "rack.version" => [1, 1],
+ "rack.input" => StringIO.new,
+ "rack.errors" => StringIO.new,
+ "rack.multithread" => true,
+ "rack.multiprocess" => true,
+ "rack.run_once" => false,
+ "REQUEST_METHOD" => "GET",
+ "SERVER_NAME" => "example.org",
+ "SERVER_PORT" => "80",
+ "QUERY_STRING" => "",
+ "PATH_INFO" => "/content",
+ "rack.url_scheme" => "http",
+ "HTTPS" => "off",
+ "SCRIPT_NAME" => "",
+ "CONTENT_LENGTH" => "0"
+ }.merge env
+ end
end
end
end
diff --git a/actionpack/test/journey/routes_test.rb b/actionpack/test/journey/routes_test.rb
index b9dac8751c..d5c81a8421 100644
--- a/actionpack/test/journey/routes_test.rb
+++ b/actionpack/test/journey/routes_test.rb
@@ -1,74 +1,61 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module ActionDispatch
module Journey
class TestRoutes < ActiveSupport::TestCase
- setup do
- @routes = Routes.new
+ attr_reader :routes, :mapper
+
+ def setup
+ @route_set = ActionDispatch::Routing::RouteSet.new
+ @routes = @route_set.router.routes
+ @router = @route_set.router
+ @mapper = ActionDispatch::Routing::Mapper.new @route_set
+ super
end
def test_clear
- routes = Routes.new
- exp = Router::Strexp.build '/foo(/:id)', {}, ['/.?']
- path = Path::Pattern.new exp
- requirements = { :hello => /world/ }
-
- routes.add_route nil, path, requirements, [], {:id => nil}, {}
- assert_not routes.empty?
+ mapper.get "/foo(/:id)", to: "foo#bar", as: "aaron"
+ assert_not_empty routes
assert_equal 1, routes.length
routes.clear
- assert routes.empty?
+ assert_empty routes
assert_equal 0, routes.length
end
def test_ast
- routes = Routes.new
- path = Path::Pattern.from_string '/hello'
-
- routes.add_route nil, path, {}, [], {}, {}
+ mapper.get "/foo(/:id)", to: "foo#bar", as: "aaron"
ast = routes.ast
- routes.add_route nil, path, {}, [], {}, {}
+ mapper.get "/foo(/:id)", to: "foo#bar", as: "gorby"
assert_not_equal ast, routes.ast
end
def test_simulator_changes
- routes = Routes.new
- path = Path::Pattern.from_string '/hello'
-
- routes.add_route nil, path, {}, [], {}, {}
+ mapper.get "/foo(/:id)", to: "foo#bar", as: "aaron"
sim = routes.simulator
- routes.add_route nil, path, {}, [], {}, {}
+ mapper.get "/foo(/:id)", to: "foo#bar", as: "gorby"
assert_not_equal sim, routes.simulator
end
def test_partition_route
- path = Path::Pattern.from_string '/hello'
+ mapper.get "/foo(/:id)", to: "foo#bar", as: "aaron"
- anchored_route = @routes.add_route nil, path, {}, [], {}, {}
- assert_equal [anchored_route], @routes.anchored_routes
- assert_equal [], @routes.custom_routes
+ assert_equal 1, @routes.anchored_routes.length
+ assert_empty @routes.custom_routes
- strexp = Router::Strexp.build(
- "/hello/:who", { who: /\d/ }, ['/', '.', '?']
- )
- path = Path::Pattern.new strexp
+ mapper.get "/hello/:who", to: "foo#bar", as: "bar", who: /\d/
- custom_route = @routes.add_route nil, path, {}, [], {}, {}
- assert_equal [custom_route], @routes.custom_routes
- assert_equal [anchored_route], @routes.anchored_routes
+ assert_equal 1, @routes.custom_routes.length
+ assert_equal 1, @routes.anchored_routes.length
end
def test_first_name_wins
- routes = Routes.new
-
- one = Path::Pattern.from_string '/hello'
- two = Path::Pattern.from_string '/aaron'
-
- routes.add_route nil, one, {}, [], {}, 'aaron'
- routes.add_route nil, two, {}, [], {}, 'aaron'
-
- assert_equal '/hello', routes.named_routes['aaron'].path.spec.to_s
+ mapper.get "/hello", to: "foo#bar", as: "aaron"
+ assert_raise(ArgumentError) do
+ mapper.get "/aaron", to: "foo#bar", as: "aaron"
+ end
end
end
end
diff --git a/actionpack/test/lib/controller/fake_controllers.rb b/actionpack/test/lib/controller/fake_controllers.rb
index 1a2863b689..e985716f43 100644
--- a/actionpack/test/lib/controller/fake_controllers.rb
+++ b/actionpack/test/lib/controller/fake_controllers.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ContentController < ActionController::Base; end
module Admin
diff --git a/actionpack/test/lib/controller/fake_models.rb b/actionpack/test/lib/controller/fake_models.rb
index ce9522d12a..01c7ec26ae 100644
--- a/actionpack/test/lib/controller/fake_models.rb
+++ b/actionpack/test/lib/controller/fake_models.rb
@@ -1,12 +1,14 @@
+# frozen_string_literal: true
+
require "active_model"
-class Customer < Struct.new(:name, :id)
+Customer = Struct.new(:name, :id) do
extend ActiveModel::Naming
include ActiveModel::Conversion
undef_method :to_json
- def to_xml(options={})
+ def to_xml(options = {})
if options[:builder]
options[:builder].name name
else
@@ -14,7 +16,7 @@ class Customer < Struct.new(:name, :id)
end
end
- def to_js(options={})
+ def to_js(options = {})
"name: #{name.inspect}"
end
alias :to_text :to_js
@@ -26,9 +28,13 @@ class Customer < Struct.new(:name, :id)
def persisted?
id.present?
end
+
+ def cache_key
+ "#{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/actionpack/test/routing/helper_test.rb b/actionpack/test/routing/helper_test.rb
index 0028aaa629..d13b043b0b 100644
--- a/actionpack/test/routing/helper_test.rb
+++ b/actionpack/test/routing/helper_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module ActionDispatch
module Routing
diff --git a/actionview/.gitignore b/actionview/.gitignore
new file mode 100644
index 0000000000..246aabbb7f
--- /dev/null
+++ b/actionview/.gitignore
@@ -0,0 +1,5 @@
+/lib/assets/compiled/
+/log/
+/test/fixtures/public/absolute/
+/test/ujs/log/
+/tmp/
diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md
index 1f6bb31cd4..237a25ba4f 100644
--- a/actionview/CHANGELOG.md
+++ b/actionview/CHANGELOG.md
@@ -1,160 +1,164 @@
-* Allow defining explicit collection caching using a `# Template Collection: ...`
- directive inside templates.
+* Prevent `ActionView::TextHelper#word_wrap` from unexpectedly stripping white space from the _left_ side of lines.
- *Dov Murik*
+ For example, given input like this:
-* Asset helpers raise `ArgumentError` when `nil` is passed as a source.
-
- *Anton Kolomiychuk*
-
-* Always attach the template digest to the cache key for collection caching
- even when `virtual_path` is not available from the view context.
- Which could happen if the rendering was done directly in the controller
- and not in a template.
-
- Fixes #20535
+ ```
+ This is a paragraph with an initial indent,
+ followed by additional lines that are not indented,
+ and finally terminated with a blockquote:
+ "A pithy saying"
+ ```
- *Roque Pinel*
+ Calling `word_wrap` should not trim the indents on the first and last lines.
-* Improve detection of partial templates eligible for collection caching,
- now allowing multi-line comments at the beginning of the template file.
+ Fixes #34487
- *Dov Murik*
+ *Lyle Mullican*
-* Raise an ArgumentError when a false value for `include_blank` is passed to a
- required select field (to comply with the HTML5 spec).
- *Grey Baker*
+* Add allocations to template rendering instrumentation.
-* Do not put partial name to `local_assigns` when rendering without
- an object or a collection.
+ Adds the allocations for template and partial rendering to the server output on render.
- *Henrik Nygren*
+ ```
+ Rendered posts/_form.html.erb (Duration: 7.1ms | Allocations: 6004)
+ Rendered posts/new.html.erb within layouts/application (Duration: 8.3ms | Allocations: 6654)
+ Completed 200 OK in 858ms (Views: 848.4ms | ActiveRecord: 0.4ms | Allocations: 1539564)
+ ```
-* Remove `:rescue_format` option for `translate` helper since it's no longer
- supported by I18n.
+ *Eileen M. Uchitelle*, *Aaron Patterson*
- *Bernard Potocki*
+* Respect the `only_path` option passed to `url_for` when the options are passed in as an array
-* `translate` should handle `raise` flag correctly in case of both main and default
- translation is missing.
+ Fixes #33237.
- Fixes #19967
+ *Joel Ambass*
- *Bernard Potocki*
+* Deprecate calling private model methods from view helpers.
-* Load the `default_form_builder` from the controller on initialization, which overrides
- the global config if it is present.
+ For example, in methods like `options_from_collection_for_select`
+ and `collection_select` it is possible to call private methods from
+ the objects used.
- *Kevin McPhillips*
+ Fixes #33546.
-* Accept lambda as `child_index` option in `fields_for` method.
+ *Ana María Martínez Gómez*
- *Karol Galanciak*
+* Fix issue with `button_to`'s `to_form_params`
-* `translate` allows `default: [[]]` again for a default value of `[]`.
+ `button_to` was throwing exception when invoked with `params` hash that
+ contains symbol and string keys. The reason for the exception was that
+ `to_form_params` was comparing the given symbol and string keys.
- Fixes #19640.
+ The issue is fixed by turning all keys to strings inside
+ `to_form_params` before comparing them.
- *Adam Prescott*
+ *Georgi Georgiev*
-* `translate` should accept nils as members of the `:default`
- parameter without raising a translation missing error.
+* Mark arrays of translations as trusted safe by using the `_html` suffix.
- Fixes #19419
+ Example:
- *Justin Coyne*
+ en:
+ foo_html:
+ - "One"
+ - "<strong>Two</strong>"
+ - "Three &#128075; &#128578;"
-* `number_to_percentage` does not crash with `Float::NAN` or `Float::INFINITY`
- as input when `precision: 0` is used.
+ *Juan Broullon*
- Fixes #19227.
+* Add `year_format` option to date_select tag. This option makes it possible to customize year
+ names. Lambda should be passed to use this option.
- *Yves Senn*
+ Example:
-* Fixed the translation helper method to accept different default values types
- besides String.
+ date_select('user_birthday', '', start_year: 1998, end_year: 2000, year_format: ->year { "Heisei #{year - 1988}" })
- *Ulisses Almeida*
+ The HTML produced:
-* Collection rendering automatically caches and fetches multiple partials.
+ <select id="user_birthday__1i" name="user_birthday[(1i)]">
+ <option value="1998">Heisei 10</option>
+ <option value="1999">Heisei 11</option>
+ <option value="2000">Heisei 12</option>
+ </select>
+ /* The rest is omitted */
- Collections rendered as:
+ *Koki Ryu*
- ```ruby
- <%= render @notifications %>
- <%= render partial: 'notifications/notification', collection: @notifications, as: :notification %>
- ```
+* Fix JavaScript views rendering does not work with Firefox when using
+ Content Security Policy.
- will now read several partials from cache at once, if the template starts with a cache call:
+ Fixes #32577.
- ```ruby
- # notifications/_notification.html.erb
- <% cache notification do %>
- <%# ... %>
- <% end %>
- ```
+ *Yuji Yaginuma*
- *Kasper Timm Hansen*
+* Add the `nonce: true` option for `javascript_include_tag` helper to
+ support automatic nonce generation for Content Security Policy.
+ Works the same way as `javascript_tag nonce: true` does.
-* Fixed a dependency tracker bug that caused template dependencies not
- count layouts as dependencies for partials.
+ *Yaroslav Markin*
- *Juho Leinonen*
+* Remove `ActionView::Helpers::RecordTagHelper`.
-* Extracted `ActionView::Helpers::RecordTagHelper` to external gem
- (`record_tag_helper`) and added removal notices.
+ *Yoshiyuki Hirano*
- *Todd Bealmear*
+* Disable `ActionView::Template` finalizers in test environment.
-* Allow to pass a string value to `size` option in `image_tag` and `video_tag`.
+ Template finalization can be expensive in large view test suites.
+ Add a configuration option,
+ `action_view.finalize_compiled_template_methods`, and turn it off in
+ the test environment.
- This makes the behavior more consistent with `width` or `height` options.
+ *Simon Coffey*
- *Mehdi Lahmam*
+* Extract the `confirm` call in its own, overridable method in `rails_ujs`.
-* Partial template name does no more have to be a valid Ruby identifier.
+ Example:
- There used to be a naming rule that the partial name should start with
- underscore, and should be followed by any combination of letters, numbers
- and underscores.
- But now we can give our partials any name starting with underscore, such as
- _🍔.html.erb.
+ Rails.confirm = function(message, element) {
+ return (my_bootstrap_modal_confirm(message));
+ }
- *Akira Matsuda*
+ *Mathieu Mahé*
-* Change the default template handler from `ERB` to `Raw`.
+* Enable select tag helper to mark `prompt` option as `selected` and/or `disabled` for `required`
+ field.
- Files without a template handler in their extension will be rendered using the raw
- handler instead of ERB.
+ Example:
- *Rafael Mendonça França*
+ select :post,
+ :category,
+ ["lifestyle", "programming", "spiritual"],
+ { selected: "", disabled: "", prompt: "Choose one" },
+ { required: true }
-* Remove deprecated `AbstractController::Base::parent_prefixes`.
+ Placeholder option would be selected and disabled.
- *Rafael Mendonça França*
+ The HTML produced:
-* Default translations that have a lower precedence than a html safe default,
- but are not themselves safe, should not be marked as html_safe.
+ <select required="required" name="post[category]" id="post_category">
+ <option disabled="disabled" selected="selected" value="">Choose one</option>
+ <option value="lifestyle">lifestyle</option>
+ <option value="programming">programming</option>
+ <option value="spiritual">spiritual</option></select>
- *Justin Coyne*
+ *Sergey Prikhodko*
-* Make possible to use blocks with short version of `render "partial"` helper.
+* Don't enforce UTF-8 by default.
- *Nikolay Shebanov*
+ With the disabling of TLS 1.0 by most major websites, continuing to run
+ IE8 or lower becomes increasingly difficult so default to not enforcing
+ UTF-8 encoding as it's not relevant to other browsers.
-* Add a `hidden_field` on the `file_field` to avoid raise a error when the only
- input on the form is the `file_field`.
+ *Andrew White*
- *Mauro George*
+* Change translation key of `submit_tag` from `module_name_class_name` to `module_name/class_name`.
-* Add an explicit error message, in `ActionView::PartialRenderer` for partial
- `rendering`, when the value of option `as` has invalid characters.
+ *Rui Onodera*
- *Angelo Capilleri*
+* Rails 6 requires Ruby 2.4.1 or newer.
-* Allow entries without a link tag in `AtomFeedHelper`.
+ *Jeremy Daer*
- *Daniel Gomez de Souza*
-Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/actionview/CHANGELOG.md) for previous changes.
+Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/actionview/CHANGELOG.md) for previous changes.
diff --git a/actionview/MIT-LICENSE b/actionview/MIT-LICENSE
index 3ec7a617cf..1cb3add0fc 100644
--- a/actionview/MIT-LICENSE
+++ b/actionview/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2015 David Heinemeier Hansson
+Copyright (c) 2004-2018 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/actionview/README.rdoc b/actionview/README.rdoc
index 8b1f85f748..03a0723564 100644
--- a/actionview/README.rdoc
+++ b/actionview/README.rdoc
@@ -9,9 +9,9 @@ used to inline short Ruby snippets inside HTML), and XML Builder.
The latest version of Action View can be installed with RubyGems:
- % gem install actionview
+ $ gem install actionview
-Source code can be downloaded as part of the Rails project on GitHub
+Source code can be downloaded as part of the Rails project on GitHub:
* https://github.com/rails/rails/tree/master/actionview
@@ -20,7 +20,7 @@ Source code can be downloaded as part of the Rails project on GitHub
Action View is released under the MIT license:
-* http://www.opensource.org/licenses/MIT
+* https://opensource.org/licenses/MIT
== Support
@@ -29,11 +29,10 @@ API documentation is at
* http://api.rubyonrails.org
-Bug reports can be filed for the Ruby on Rails project here:
+Bug reports for the Ruby on Rails project can be filed here:
* https://github.com/rails/rails/issues
Feature requests should be discussed on the rails-core mailing list here:
* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core
-
diff --git a/actionview/RUNNING_UJS_TESTS.rdoc b/actionview/RUNNING_UJS_TESTS.rdoc
new file mode 100644
index 0000000000..e30c2aee55
--- /dev/null
+++ b/actionview/RUNNING_UJS_TESTS.rdoc
@@ -0,0 +1,8 @@
+== Running UJS tests
+
+Ensure that you can build the project by running:
+ rake ujs:server
+
+Then run the web tests by visiting the following URL in your browser:
+
+ http://localhost:4567
diff --git a/actionview/RUNNING_UNIT_TESTS.rdoc b/actionview/RUNNING_UNIT_TESTS.rdoc
index 6c4e5e983a..4442dbdb9e 100644
--- a/actionview/RUNNING_UNIT_TESTS.rdoc
+++ b/actionview/RUNNING_UNIT_TESTS.rdoc
@@ -2,13 +2,13 @@
The easiest way to run the unit tests is through Rake. The default task runs
the entire test suite for all classes. For more information, checkout the
-full array of rake tasks with "rake -T"
+full array of rake tasks with <tt>rake -T</tt>
-Rake can be found at http://docs.seattlerb.org/rake/.
+Rake can be found at https://ruby.github.io/rake/.
== Running by hand
-To run a single test suite
+Run a single test suite:
rake test TEST=path/to/test.rb
@@ -18,10 +18,9 @@ which can be further narrowed down to one test:
== Dependency on Active Record and database setup
-Test cases in the test/activerecord/ directory depend on having
-activerecord and sqlite3 installed. If Active Record is not in
-actionview/../activerecord directory, or the sqlite3 rubygem is not installed,
-these tests are skipped.
-
+Test cases in the +test/activerecord/+ directory depend on having
+activerecord+ and +sqlite3+ installed. If Active Record is not in
+actionview/../activerecord+ directory, or the +sqlite3+ Ruby gem is not installed,
+ these tests are skipped.
Other tests are runnable from a fresh copy of actionview without any configuration.
diff --git a/actionview/Rakefile b/actionview/Rakefile
index 93be50721d..7851a2b6bf 100644
--- a/actionview/Rakefile
+++ b/actionview/Rakefile
@@ -1,41 +1,74 @@
-require 'rake/testtask'
+# frozen_string_literal: true
+
+require "rake/testtask"
+require "fileutils"
+require "open3"
desc "Default Task"
-task :default => :test
+task default: :test
+
+task package: %w( assets:compile assets:verify )
# Run the unit tests
desc "Run all unit tests"
-task :test => ["test:template", "test:integration:action_pack", "test:integration:active_record"]
+task test: ["test:template", "test:integration:action_pack", "test:integration:active_record"]
namespace :test do
task :isolated do
Dir.glob("test/{actionpack,activerecord,template}/**/*_test.rb").all? do |file|
- sh(Gem.ruby, '-w', '-Ilib:test', file)
- end or raise "Failures"
+ sh(Gem.ruby, "-w", "-Ilib:test", file)
+ end || raise("Failures")
end
Rake::TestTask.new(:template) do |t|
- t.libs << 'test'
- t.test_files = Dir.glob('test/template/**/*_test.rb')
+ t.libs << "test"
+ t.test_files = Dir.glob("test/template/**/*_test.rb")
t.warning = true
t.verbose = true
t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
end
+ desc "Run tests for rails-ujs"
+ task :ujs do
+ begin
+ Dir.mkdir("log")
+ pid = spawn("bundle exec rackup test/ujs/config.ru -p 4567 -s puma > log/test.log 2>&1", pgroup: true)
+
+ start_time = Time.now
+
+ loop do
+ break if system("lsof -i :4567", 1 => File::NULL)
+
+ if Time.now - start_time > 5
+ puts "Timed out after 5 seconds"
+ exit 1
+ end
+ end
+
+ system("npm run lint && bundle exec ruby ../ci/qunit-selenium-runner.rb http://localhost:4567/")
+ status = $?.exitstatus
+ ensure
+ Process.kill("KILL", -pid) if pid
+ FileUtils.rm_rf("log")
+ end
+
+ exit status
+ end
+
namespace :integration do
- desc 'ActiveRecord Integration Tests'
+ # Active Record Integration Tests
Rake::TestTask.new(:active_record) do |t|
- t.libs << 'test'
+ t.libs << "test"
t.test_files = Dir.glob("test/activerecord/*_test.rb")
t.warning = true
t.verbose = true
t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
end
- desc 'ActionPack Integration Tests'
+ # Action Pack Integration Tests
Rake::TestTask.new(:action_pack) do |t|
- t.libs << 'test'
+ t.libs << "test"
t.test_files = Dir.glob("test/actionpack/**/*_test.rb")
t.warning = true
t.verbose = true
@@ -44,8 +77,61 @@ 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"
+ require "sprockets"
+ require "sprockets/export"
+ Blade.build
+ end
+
+ desc "Verify compiled Action View assets"
+ task :verify do
+ file = "lib/assets/compiled/rails-ujs.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 /module\.exports.*define\.amd/m.match?(pathname.read)
+ puts "[OK]"
+ else
+ $stderr.puts "[FAIL]"
+ fail
+ end
+
+ print "[verify] #{__dir__} can be required as a module "
+ js = <<-JS
+ window = { Event: class {} }
+ class Element {}
+ require('#{__dir__}')
+ JS
+ _, stderr, status = Open3.capture3("node", "--print", js)
+ if status.success?
+ puts "[OK]"
+ else
+ $stderr.puts "[FAIL]\n#{stderr}"
+ fail
+ end
+ end
+end
+
task :lines do
- load File.expand_path('..', File.dirname(__FILE__)) + '/tools/line_statistics'
+ load File.expand_path("../tools/line_statistics", __dir__)
files = FileList["lib/**/*.rb"]
CodeTools::LineStatistics.new(files).print_loc
end
diff --git a/actionview/actionview.gemspec b/actionview/actionview.gemspec
index 612e94021d..5f1e746421 100644
--- a/actionview/actionview.gemspec
+++ b/actionview/actionview.gemspec
@@ -1,31 +1,41 @@
-version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).strip
+# frozen_string_literal: true
+
+version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip
Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
- s.name = 'actionview'
+ s.name = "actionview"
s.version = version
- s.summary = 'Rendering framework putting the V in MVC (part of Rails).'
- s.description = 'Simple, battle-tested conventions and helpers for building web pages.'
+ s.summary = "Rendering framework putting the V in MVC (part of Rails)."
+ s.description = "Simple, battle-tested conventions and helpers for building web pages."
+
+ s.required_ruby_version = ">= 2.4.1"
+
+ s.license = "MIT"
- s.required_ruby_version = '>= 2.2.2'
+ s.author = "David Heinemeier Hansson"
+ s.email = "david@loudthinking.com"
+ s.homepage = "http://rubyonrails.org"
- s.license = 'MIT'
+ s.files = Dir["CHANGELOG.md", "README.rdoc", "MIT-LICENSE", "lib/**/*"]
+ s.require_path = "lib"
+ s.requirements << "none"
- s.author = 'David Heinemeier Hansson'
- s.email = 'david@loudthinking.com'
- s.homepage = 'http://www.rubyonrails.org'
+ s.metadata = {
+ "source_code_uri" => "https://github.com/rails/rails/tree/v#{version}/actionview",
+ "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/actionview/CHANGELOG.md"
+ }
- s.files = Dir['CHANGELOG.md', 'README.rdoc', 'MIT-LICENSE', 'lib/**/*']
- s.require_path = 'lib'
- s.requirements << 'none'
+ # NOTE: Please read our dependency guidelines before updating versions:
+ # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves
- s.add_dependency 'activesupport', version
+ 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 'rails-dom-testing', '~> 1.0', '>= 1.0.5'
+ s.add_dependency "builder", "~> 3.1"
+ 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
- s.add_development_dependency 'activemodel', version
+ s.add_development_dependency "actionpack", version
+ s.add_development_dependency "activemodel", version
end
diff --git a/actionview/app/assets/javascripts/MIT-LICENSE b/actionview/app/assets/javascripts/MIT-LICENSE
new file mode 100644
index 0000000000..28e1b12496
--- /dev/null
+++ b/actionview/app/assets/javascripts/MIT-LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2007-2018 Rails Core team
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"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..b74fa1afad
--- /dev/null
+++ b/actionview/app/assets/javascripts/README.md
@@ -0,0 +1,57 @@
+# 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.
+
+## Optional prerequisites
+
+Note that the `data` attributes this library adds are a feature of HTML5. If you're not targeting HTML5, these attributes may make your HTML to fail [validation][validator]. However, this shouldn't create any issues for web browsers or other user agents.
+
+## Installation
+
+### NPM
+
+ npm install rails-ujs --save
+
+### Yarn
+
+ yarn add rails-ujs
+
+Ensure that `.yarnclean` does not include `assets` if you use [yarn autoclean](https://yarnpkg.com/lang/en/docs/cli/autoclean/).
+
+## Usage
+
+### Asset pipeline
+
+In a conventional Rails application that uses the asset pipeline, require `rails-ujs` in your `application.js` manifest:
+
+```javascript
+//= require rails-ujs
+```
+
+### ES2015+
+
+If you're using the Webpacker gem or some other JavaScript bundler, add the following to your main JS file:
+
+```javascript
+import Rails from 'rails-ujs';
+Rails.start()
+```
+
+## How to run tests
+
+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]: https://www.w3.org/TR/html5/dom.html#embedding-custom-non-visible-data-with-the-data-attributes "Embedding custom non-visible data with the data-* attributes"
+[validator]: http://validator.w3.org/
+[csrf]: http://api.rubyonrails.org/classes/ActionController/RequestForgeryProtection.html
diff --git a/actionview/app/assets/javascripts/rails-ujs.coffee b/actionview/app/assets/javascripts/rails-ujs.coffee
new file mode 100644
index 0000000000..bd6e9bb881
--- /dev/null
+++ b/actionview/app/assets/javascripts/rails-ujs.coffee
@@ -0,0 +1,39 @@
+#= require ./rails-ujs/BANNER
+#= export Rails
+#= require_self
+#= require_tree ./rails-ujs/utils
+#= require_tree ./rails-ujs/features
+#= require ./rails-ujs/start
+
+@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/rails-ujs/BANNER.js b/actionview/app/assets/javascripts/rails-ujs/BANNER.js
new file mode 100644
index 0000000000..47ecd66003
--- /dev/null
+++ b/actionview/app/assets/javascripts/rails-ujs/BANNER.js
@@ -0,0 +1,5 @@
+/*
+Unobtrusive JavaScript
+https://github.com/rails/rails/blob/master/actionview/app/assets/javascripts
+Released under the MIT license
+ */
diff --git a/actionview/app/assets/javascripts/rails-ujs/features/confirm.coffee b/actionview/app/assets/javascripts/rails-ujs/features/confirm.coffee
new file mode 100644
index 0000000000..0738ffcdc9
--- /dev/null
+++ b/actionview/app/assets/javascripts/rails-ujs/features/confirm.coffee
@@ -0,0 +1,30 @@
+#= require_tree ../utils
+
+{ fire, stopEverything } = Rails
+
+Rails.handleConfirm = (e) ->
+ stopEverything(e) unless allowAction(this)
+
+# Default confirm dialog, may be overridden with custom confirm dialog in Rails.confirm
+Rails.confirm = (message, element) ->
+ confirm(message)
+
+# For 'data-confirm' attribute:
+# - Fires `confirm` event
+# - Shows the confirmation dialog
+# - 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 = Rails.confirm(message, element)
+ callback = fire(element, 'confirm:complete', [answer])
+
+ answer and callback
diff --git a/actionview/app/assets/javascripts/rails-ujs/features/disable.coffee b/actionview/app/assets/javascripts/rails-ujs/features/disable.coffee
new file mode 100644
index 0000000000..a8c692ee62
--- /dev/null
+++ b/actionview/app/assets/javascripts/rails-ujs/features/disable.coffee
@@ -0,0 +1,91 @@
+#= 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) ->
+ if e instanceof Event
+ return if isXhrRedirect(e)
+ element = e.target
+ else
+ element = 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)
+
+isXhrRedirect = (event) ->
+ xhr = event.detail?[0]
+ xhr?.getResponseHeader("X-Xhr-Redirect")?
diff --git a/actionview/app/assets/javascripts/rails-ujs/features/method.coffee b/actionview/app/assets/javascripts/rails-ujs/features/method.coffee
new file mode 100644
index 0000000000..d04d9414dd
--- /dev/null
+++ b/actionview/app/assets/javascripts/rails-ujs/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/rails-ujs/features/remote.coffee b/actionview/app/assets/javascripts/rails-ujs/features/remote.coffee
new file mode 100644
index 0000000000..b3448dabac
--- /dev/null
+++ b/actionview/app/assets/javascripts/rails-ujs/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')
+ return false
+ 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/start.coffee b/actionview/app/assets/javascripts/rails-ujs/start.coffee
new file mode 100644
index 0000000000..32a915ac0b
--- /dev/null
+++ b/actionview/app/assets/javascripts/rails-ujs/start.coffee
@@ -0,0 +1,71 @@
+{
+ fire, delegate
+ getData, $
+ refreshCSRFTokens, CSRFProtection
+ enableElement, disableElement, handleDisabledElement
+ handleConfirm
+ handleRemote, formSubmitButtonClick, handleMetaClick
+ handleMethod
+} = Rails
+
+# For backward compatibility
+if jQuery? and jQuery.ajax?
+ throw new Error('If you load both jquery_ujs and rails-ujs, use rails-ujs only.') if 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/rails-ujs/utils/ajax.coffee b/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee
new file mode 100644
index 0000000000..019bda635a
--- /dev/null
+++ b/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee
@@ -0,0 +1,97 @@
+#= require ./csp
+#= require ./csrf
+#= require ./event
+
+{ cspNonce, 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.responseText, 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)
+
+ if options.beforeSend? && !options.beforeSend(xhr, options)
+ return false
+
+ if xhr.readyState is XMLHttpRequest.OPENED
+ xhr.send(options.data)
+
+prepareOptions = (options) ->
+ options.url = options.url or location.href
+ 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(/\b(?:java|ecma)script\b/)
+ script = document.createElement('script')
+ script.setAttribute('nonce', cspNonce())
+ script.text = response
+ document.head.appendChild(script).parentNode.removeChild(script)
+ else if type.match(/\bxml\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/rails-ujs/utils/csp.coffee b/actionview/app/assets/javascripts/rails-ujs/utils/csp.coffee
new file mode 100644
index 0000000000..8d2d6ce447
--- /dev/null
+++ b/actionview/app/assets/javascripts/rails-ujs/utils/csp.coffee
@@ -0,0 +1,4 @@
+# Content-Security-Policy nonce for inline scripts
+cspNonce = Rails.cspNonce = ->
+ meta = document.querySelector('meta[name=csp-nonce]')
+ meta and meta.content
diff --git a/actionview/app/assets/javascripts/rails-ujs/utils/csrf.coffee b/actionview/app/assets/javascripts/rails-ujs/utils/csrf.coffee
new file mode 100644
index 0000000000..4eb5ebb414
--- /dev/null
+++ b/actionview/app/assets/javascripts/rails-ujs/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/rails-ujs/utils/dom.coffee b/actionview/app/assets/javascripts/rails-ujs/utils/dom.coffee
new file mode 100644
index 0000000000..3d3c5bb330
--- /dev/null
+++ b/actionview/app/assets/javascripts/rails-ujs/utils/dom.coffee
@@ -0,0 +1,35 @@
+m = Element.prototype.matches or
+ Element.prototype.matchesSelector or
+ Element.prototype.mozMatchesSelector or
+ Element.prototype.msMatchesSelector or
+ Element.prototype.oMatchesSelector or
+ Element.prototype.webkitMatchesSelector
+
+# Checks if the given native dom element matches the selector
+# element::
+# native DOM element
+# selector::
+# css selector string or
+# a javascript object with `selector` and `exclude` properties
+# Examples: "form", { selector: "form", exclude: "form[data-remote='true']"}
+Rails.matches = (element, selector) ->
+ if selector.exclude?
+ m.call(element, selector.selector) and not m.call(element, selector.exclude)
+ 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/rails-ujs/utils/event.coffee b/actionview/app/assets/javascripts/rails-ujs/utils/event.coffee
new file mode 100644
index 0000000000..a7eee52060
--- /dev/null
+++ b/actionview/app/assets/javascripts/rails-ujs/utils/event.coffee
@@ -0,0 +1,68 @@
+#= 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
+
+ # Fix setting `defaultPrevented` when `preventDefault()` is called
+ # http://stackoverflow.com/questions/23349191/event-preventdefault-is-not-working-in-ie-11-for-custom-events
+ { preventDefault } = CustomEvent.prototype
+ CustomEvent.prototype.preventDefault = ->
+ result = preventDefault.call(this)
+ if @cancelable and not @defaultPrevented
+ Object.defineProperty(this, 'defaultPrevented', get: -> true)
+ result
+
+# Triggers a custom event on an element and returns false if the event result is false
+# obj::
+# a native DOM element
+# name::
+# string that corrspends to the event you want to trigger
+# e.g. 'click', 'submit'
+# data::
+# data you want to pass when you dispatch an event
+fire = Rails.fire = (obj, name, data) ->
+ event = new CustomEvent(
+ name,
+ 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()
+
+# Delegates events
+# to a specified parent `element`, which fires event `handler`
+# for the specified `selector` when an event of `eventType` is triggered
+# element::
+# parent element that will listen for events e.g. document
+# selector::
+# css selector; or an object that has `selector` and `exclude` properties (see: Rails.matches)
+# eventType::
+# string representing the event e.g. 'submit', 'click'
+# handler::
+# the event handler to be called
+Rails.delegate = (element, selector, eventType, handler) ->
+ element.addEventListener eventType, (e) ->
+ target = e.target
+ 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/rails-ujs/utils/form.coffee b/actionview/app/assets/javascripts/rails-ujs/utils/form.coffee
new file mode 100644
index 0000000000..736cab08db
--- /dev/null
+++ b/actionview/app/assets/javascripts/rails-ujs/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 if !input.name || input.disabled
+ if matches(input, 'select')
+ toArray(input.options).forEach (option) ->
+ params.push(name: input.name, value: option.value) if option.selected
+ 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 404cabba51..c53377cc97 100755
--- a/actionview/bin/test
+++ b/actionview/bin/test
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
-COMPONENT_ROOT = File.expand_path("../../", __FILE__)
-require File.expand_path("../tools/test", COMPONENT_ROOT)
-exit Minitest.run(ARGV)
+# frozen_string_literal: true
+
+COMPONENT_ROOT = File.expand_path("..", __dir__)
+require_relative "../../tools/test"
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 c3bbac27fd..c1eeda75f5 100644
--- a/actionview/lib/action_view.rb
+++ b/actionview/lib/action_view.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
#--
-# Copyright (c) 2004-2015 David Heinemeier Hansson
+# Copyright (c) 2004-2018 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -21,9 +23,9 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
-require 'active_support'
-require 'active_support/rails'
-require 'action_view/version'
+require "active_support"
+require "active_support/rails"
+require "action_view/version"
module ActionView
extend ActiveSupport::Autoload
@@ -74,7 +76,6 @@ module ActionView
autoload :MissingTemplate
autoload :ActionViewError
autoload :EncodingError
- autoload :MissingRequestError
autoload :TemplateError
autoload :WrongEncodingError
end
@@ -89,8 +90,8 @@ module ActionView
end
end
-require 'active_support/core_ext/string/output_safety'
+require "active_support/core_ext/string/output_safety"
ActiveSupport.on_load(:i18n) do
- I18n.load_path << "#{File.dirname(__FILE__)}/action_view/locale/en.yml"
+ I18n.load_path << File.expand_path("action_view/locale/en.yml", __dir__)
end
diff --git a/actionview/lib/action_view/base.rb b/actionview/lib/action_view/base.rb
index 43124bb904..d41fe2a608 100644
--- a/actionview/lib/action_view/base.rb
+++ b/actionview/lib/action_view/base.rb
@@ -1,23 +1,25 @@
-require 'active_support/core_ext/module/attr_internal'
-require 'active_support/core_ext/module/attribute_accessors'
-require 'active_support/ordered_options'
-require 'action_view/log_subscriber'
-require 'action_view/helpers'
-require 'action_view/context'
-require 'action_view/template'
-require 'action_view/lookup_context'
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/attr_internal"
+require "active_support/core_ext/module/attribute_accessors"
+require "active_support/ordered_options"
+require "action_view/log_subscriber"
+require "action_view/helpers"
+require "action_view/context"
+require "action_view/template"
+require "action_view/lookup_context"
module ActionView #:nodoc:
# = Action View Base
#
# 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.
#
# == ERB
#
- # You trigger ERB by using embeddings such as <% %>, <% -%>, and <%= %>. The <%= %> tag set is used when you want output. Consider the
+ # You trigger ERB by using embeddings such as <tt><% %></tt>, <tt><% -%></tt>, and <tt><%= %></tt>. The <tt><%= %></tt> tag set is used when you want output. Consider the
# following loop for names:
#
# <b>Names of all the people</b>
@@ -25,7 +27,7 @@ module ActionView #:nodoc:
# Name: <%= person.name %><br/>
# <% end %>
#
- # The loop is setup in regular embedding tags <% %> and the name is written using the output embedding tag <%= %>. Note that this
+ # The loop is setup in regular embedding tags <tt><% %></tt>, and the name is written using the output embedding tag <tt><%= %></tt>. Note that this
# is not just a usage suggestion. Regular output functions like print or puts won't work with ERB templates. So this would be wrong:
#
# <%# WRONG %>
@@ -33,9 +35,9 @@ module ActionView #:nodoc:
#
# If you absolutely must write from within a function use +concat+.
#
- # When on a line that only contains whitespaces except for the tag, <% %> suppress leading and trailing whitespace,
- # including the trailing newline. <% %> and <%- -%> are the same.
- # Note however that <%= %> and <%= -%> are different: only the latter removes trailing whitespaces.
+ # When on a line that only contains whitespaces except for the tag, <tt><% %></tt> suppresses leading and trailing whitespace,
+ # including the trailing newline. <tt><% %></tt> and <tt><%- -%></tt> are the same.
+ # Note however that <tt><%= %></tt> and <tt><%= -%></tt> are different: only the latter removes trailing whitespaces.
#
# === Using sub templates
#
@@ -75,7 +77,7 @@ module ActionView #:nodoc:
#
# Headline: <%= local_assigns[:headline] %>
#
- # This is useful in cases where you aren't sure if the local variable has been assigned. Alternately, you could also use
+ # This is useful in cases where you aren't sure if the local variable has been assigned. Alternatively, you could also use
# <tt>defined? headline</tt> to first check if the variable has been assigned before using it.
#
# === Template caching
@@ -110,7 +112,7 @@ module ActionView #:nodoc:
# <p>A product of Danish Design during the Winter of '79...</p>
# </div>
#
- # A full-length RSS example actually used on Basecamp:
+ # Here is a full-length RSS example actually used on Basecamp:
#
# xml.rss("version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/") do
# xml.channel do
@@ -140,32 +142,31 @@ module ActionView #:nodoc:
include Helpers, ::ERB::Util, Context
# Specify the proc used to decorate input tags that refer to attributes with errors.
- cattr_accessor :field_error_proc
- @@field_error_proc = Proc.new{ |html_tag, instance| "<div class=\"field_with_errors\">#{html_tag}</div>".html_safe }
+ cattr_accessor :field_error_proc, default: Proc.new { |html_tag, instance| "<div class=\"field_with_errors\">#{html_tag}</div>".html_safe }
# How to complete the streaming when an exception occurs.
# This is our best guess: first try to close the attribute, then the tag.
- cattr_accessor :streaming_completion_on_exception
- @@streaming_completion_on_exception = %("><script>window.location = "/500.html"</script></html>)
+ cattr_accessor :streaming_completion_on_exception, default: %("><script>window.location = "/500.html"</script></html>)
# Specify whether rendering within namespaced controllers should prefix
# the partial paths for ActiveModel objects with the namespace.
# (e.g., an Admin::PostsController would render @post using /admin/posts/_post.erb)
- cattr_accessor :prefix_partial_path_with_controller_namespace
- @@prefix_partial_path_with_controller_namespace = true
+ cattr_accessor :prefix_partial_path_with_controller_namespace, default: true
# Specify default_formats that can be rendered.
cattr_accessor :default_formats
# Specify whether an error should be raised for missing translations
- cattr_accessor :raise_on_missing_translations
- @@raise_on_missing_translations = false
+ cattr_accessor :raise_on_missing_translations, default: false
+
+ # Specify whether submit_tag should automatically disable on click
+ cattr_accessor :automatically_disable_submit_tag, default: true
class_attribute :_routes
class_attribute :logger
class << self
- delegate :erb_trim_mode=, :to => 'ActionView::Template::Handlers::ERB'
+ delegate :erb_trim_mode=, to: "ActionView::Template::Handlers::ERB"
def cache_template_loading
ActionView::Resolver.caching?
@@ -183,8 +184,8 @@ module ActionView #:nodoc:
attr_accessor :view_renderer
attr_internal :config, :assigns
- delegate :lookup_context, :to => :view_renderer
- delegate :formats, :formats=, :locale, :locale=, :view_paths, :view_paths=, :to => :lookup_context
+ delegate :lookup_context, to: :view_renderer
+ delegate :formats, :formats=, :locale, :locale=, :view_paths, :view_paths=, to: :lookup_context
def assign(new_assigns) # :nodoc:
@_assigns = new_assigns.each { |key, value| instance_variable_set("@#{key}", value) }
@@ -203,6 +204,7 @@ module ActionView #:nodoc:
@view_renderer = ActionView::Renderer.new(lookup_context)
end
+ @cache_hit = {}
assign(assigns)
assign_controller(controller)
_prepare_context
diff --git a/actionview/lib/action_view/buffers.rb b/actionview/lib/action_view/buffers.rb
index be5d86b1dc..18eaee5d79 100644
--- a/actionview/lib/action_view/buffers.rb
+++ b/actionview/lib/action_view/buffers.rb
@@ -1,6 +1,23 @@
-require 'active_support/core_ext/string/output_safety'
+# frozen_string_literal: true
+
+require "active_support/core_ext/string/output_safety"
module ActionView
+ # Used as a buffer for views
+ #
+ # The main difference between this and ActiveSupport::SafeBuffer
+ # is for the methods `<<` and `safe_expr_append=` the inputs are
+ # checked for nil before they are assigned and `to_s` is called on
+ # the input. For example:
+ #
+ # obuf = ActionView::OutputBuffer.new "hello"
+ # obuf << 5
+ # puts obuf # => "hello5"
+ #
+ # sbuf = ActiveSupport::SafeBuffer.new "hello"
+ # sbuf << 5
+ # puts sbuf # => "hello\u0005"
+ #
class OutputBuffer < ActiveSupport::SafeBuffer #:nodoc:
def initialize(*)
super
diff --git a/actionview/lib/action_view/context.rb b/actionview/lib/action_view/context.rb
index ee263df484..3c605c3ee3 100644
--- a/actionview/lib/action_view/context.rb
+++ b/actionview/lib/action_view/context.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionView
module CompiledTemplates #:nodoc:
# holds compiled template code
@@ -8,16 +10,16 @@ module ActionView
# Action View contexts are supplied to Action Controller to render a template.
# The default Action View context is ActionView::Base.
#
- # In order to work with ActionController, a Context must just include this module.
- # The initialization of the variables used by the context (@output_buffer, @view_flow,
- # and @virtual_path) is responsibility of the object that includes this module
- # (although you can call _prepare_context defined below).
+ # In order to work with Action Controller, a Context must just include this
+ # module. The initialization of the variables used by the context
+ # (@output_buffer, @view_flow, and @virtual_path) is responsibility of the
+ # object that includes this module (although you can call _prepare_context
+ # defined below).
module Context
include CompiledTemplates
attr_accessor :output_buffer, :view_flow
# Prepares the context by setting the appropriate instance variables.
- # :api: plugin
def _prepare_context
@view_flow = OutputFlow.new
@output_buffer = nil
@@ -27,8 +29,7 @@ module ActionView
# Encapsulates the interaction with the view flow so it
# returns the correct buffer on +yield+. This is usually
# overwritten by helpers to add more behavior.
- # :api: plugin
- def _layout_for(name=nil)
+ def _layout_for(name = nil)
name ||= :layout
view_flow.get(name).html_safe
end
diff --git a/actionview/lib/action_view/dependency_tracker.rb b/actionview/lib/action_view/dependency_tracker.rb
index 7a7e116dbb..182f6e2eef 100644
--- a/actionview/lib/action_view/dependency_tracker.rb
+++ b/actionview/lib/action_view/dependency_tracker.rb
@@ -1,22 +1,28 @@
-require 'thread_safe'
+# frozen_string_literal: true
+
+require "concurrent/map"
+require "action_view/path_set"
module ActionView
class DependencyTracker # :nodoc:
- @trackers = ThreadSafe::Cache.new
+ @trackers = Concurrent::Map.new
- def self.find_dependencies(name, template)
+ def self.find_dependencies(name, template, view_paths = nil)
tracker = @trackers[template.handler]
+ return [] unless tracker
- if tracker.present?
- tracker.call(name, template)
- else
- []
- end
+ tracker.call(name, template, view_paths)
end
def self.register_tracker(extension, tracker)
handler = Template.handler_for_extension(extension)
- @trackers[handler] = tracker
+ if tracker.respond_to?(:supports_view_paths?)
+ @trackers[handler] = tracker
+ else
+ @trackers[handler] = lambda { |name, template, _|
+ tracker.call(name, template)
+ }
+ end
end
def self.remove_tracker(handler)
@@ -82,12 +88,16 @@ module ActionView
(?:#{STRING}|#{VARIABLE_OR_METHOD_CHAIN}) # finally, the dependency name of interest
/xm
- def self.call(name, template)
- new(name, template).dependencies
+ def self.supports_view_paths? # :nodoc:
+ true
+ end
+
+ def self.call(name, template, view_paths = nil)
+ new(name, template, view_paths).dependencies
end
- def initialize(name, template)
- @name, @template = name, template
+ def initialize(name, template, view_paths = nil)
+ @name, @template, @view_paths = name, template, view_paths
end
def dependencies
@@ -97,7 +107,6 @@ module ActionView
attr_reader :name, :template
private :name, :template
-
private
def source
template.source
@@ -134,7 +143,7 @@ module ActionView
def add_static_dependency(dependencies, dependency)
if dependency
- if dependency.include?('/')
+ if dependency.include?("/")
dependencies << dependency
else
dependencies << "#{directory}/#{dependency}"
@@ -142,8 +151,22 @@ module ActionView
end
end
+ def resolve_directories(wildcard_dependencies)
+ return [] unless @view_paths
+
+ wildcard_dependencies.flat_map { |query, templates|
+ @view_paths.find_all_with_query(query).map do |template|
+ "#{File.dirname(query)}/#{File.basename(template).split('.').first}"
+ end
+ }.sort
+ end
+
def explicit_dependencies
- source.scan(EXPLICIT_DEPENDENCY).flatten.uniq
+ dependencies = source.scan(EXPLICIT_DEPENDENCY).flatten.uniq
+
+ wildcards, explicits = dependencies.partition { |dependency| dependency[-1] == "*" }
+
+ (explicits + resolve_directories(wildcards)).uniq
end
end
diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb
index df8059d04e..6d2e471a44 100644
--- a/actionview/lib/action_view/digestor.rb
+++ b/actionview/lib/action_view/digestor.rb
@@ -1,12 +1,16 @@
-require 'thread_safe'
-require 'action_view/dependency_tracker'
-require 'monitor'
+# frozen_string_literal: true
+
+require "action_view/dependency_tracker"
module ActionView
class Digestor
- cattr_reader(:cache)
- @@cache = ThreadSafe::Cache.new
- @@digest_monitor = Monitor.new
+ @@digest_mutex = Mutex.new
+
+ module PerExecutionDigestCacheExpiry
+ def self.before(target)
+ ActionView::LookupContext::DetailsKey.clear
+ end
+ end
class << self
# Supported options:
@@ -14,110 +18,118 @@ module ActionView
# * <tt>name</tt> - Template name
# * <tt>finder</tt> - An instance of <tt>ActionView::LookupContext</tt>
# * <tt>dependencies</tt> - An array of dependent views
- # * <tt>partial</tt> - Specifies whether the template is a partial
- def digest(options)
- options.assert_valid_keys(:name, :finder, :dependencies, :partial)
-
- cache_key = ([ options[:name], options[:finder].details_key.hash ].compact + Array.wrap(options[:dependencies])).join('.')
+ def digest(name:, finder:, dependencies: nil)
+ if dependencies.nil? || dependencies.empty?
+ cache_key = "#{name}.#{finder.rendered_format}"
+ else
+ cache_key = [ name, finder.rendered_format, dependencies ].flatten.compact.join(".")
+ end
# this is a correctly done double-checked locking idiom
- # (ThreadSafe::Cache's lookups have volatile semantics)
- @@cache[cache_key] || @@digest_monitor.synchronize do
- @@cache.fetch(cache_key) do # re-check under lock
- compute_and_store_digest(cache_key, options)
+ # (Concurrent::Map's lookups have volatile semantics)
+ finder.digest_cache[cache_key] || @@digest_mutex.synchronize do
+ finder.digest_cache.fetch(cache_key) do # re-check under lock
+ partial = name.include?("/_")
+ root = tree(name, finder, partial)
+ dependencies.each do |injected_dep|
+ root.children << Injected.new(injected_dep, nil, nil)
+ end if dependencies
+ finder.digest_cache[cache_key] = root.digest(finder)
end
end
end
- private
- def compute_and_store_digest(cache_key, options) # called under @@digest_monitor lock
- klass = if options[:partial] || options[:name].include?("/_")
- # Prevent re-entry or else recursive templates will blow the stack.
- # There is no need to worry about other threads seeing the +false+ value,
- # as they will then have to wait for this thread to let go of the @@digest_monitor lock.
- pre_stored = @@cache.put_if_absent(cache_key, false).nil? # put_if_absent returns nil on insertion
- PartialDigestor
- else
- Digestor
- end
+ def logger
+ ActionView::Base.logger || NullLogger
+ end
- digest = klass.new(options).digest
- # Store the actual digest if config.cache_template_loading is true
- @@cache[cache_key] = stored_digest = digest if ActionView::Resolver.caching?
- digest
- ensure
- # something went wrong or ActionView::Resolver.caching? is false, make sure not to corrupt the @@cache
- @@cache.delete_pair(cache_key, false) if pre_stored && !stored_digest
- end
- end
+ # Create a dependency tree for template named +name+.
+ def tree(name, finder, partial = false, seen = {})
+ logical_name = name.gsub(%r|/_|, "/")
- attr_reader :name, :finder, :options
+ if template = find_template(finder, logical_name, [], partial, [])
+ finder.rendered_format ||= template.formats.first
- def initialize(options)
- @name, @finder = options.values_at(:name, :finder)
- @options = options.except(:name, :finder)
- end
+ if node = seen[template.identifier] # handle cycles in the tree
+ node
+ else
+ node = seen[template.identifier] = Node.create(name, logical_name, template, partial)
+
+ deps = DependencyTracker.find_dependencies(name, template, finder.view_paths)
+ deps.uniq { |n| n.gsub(%r|/_|, "/") }.each do |dep_file|
+ node.children << tree(dep_file, finder, true, seen)
+ end
+ node
+ end
+ else
+ unless name.include?("#") # Dynamic template partial names can never be tracked
+ logger.error " Couldn't find template for digesting: #{name}"
+ end
- def digest
- Digest::MD5.hexdigest("#{source}-#{dependency_digest}").tap do |digest|
- logger.try :debug, " Cache digest for #{template.inspect}: #{digest}"
+ seen[name] ||= Missing.new(name, logical_name, nil)
+ end
end
- rescue ActionView::MissingTemplate
- logger.try :error, " Couldn't find template for digesting: #{name}"
- ''
- end
- def dependencies
- DependencyTracker.find_dependencies(name, template)
- rescue ActionView::MissingTemplate
- logger.try :error, " '#{name}' file doesn't exist, so no dependencies"
- []
+ private
+ def find_template(finder, name, prefixes, partial, keys)
+ finder.disable_cache do
+ format = finder.rendered_format
+ result = finder.find_all(name, prefixes, partial, keys, formats: [format]).first if format
+ result || finder.find_all(name, prefixes, partial, keys).first
+ end
+ end
end
- def nested_dependencies
- dependencies.collect do |dependency|
- dependencies = PartialDigestor.new(name: dependency, finder: finder).nested_dependencies
- dependencies.any? ? { dependency => dependencies } : dependency
- end
- end
+ class Node
+ attr_reader :name, :logical_name, :template, :children
- private
- def logger
- ActionView::Base.logger
+ def self.create(name, logical_name, template, partial)
+ klass = partial ? Partial : Node
+ klass.new(name, logical_name, template, [])
end
- def logical_name
- name.gsub(%r|/_|, "/")
+ def initialize(name, logical_name, template, children = [])
+ @name = name
+ @logical_name = logical_name
+ @template = template
+ @children = children
end
- def partial?
- false
+ def digest(finder, stack = [])
+ ActiveSupport::Digest.hexdigest("#{template.source}-#{dependency_digest(finder, stack)}")
end
- def template
- @template ||= finder.disable_cache { finder.find(logical_name, [], partial?) }
+ def dependency_digest(finder, stack)
+ children.map do |node|
+ if stack.include?(node)
+ false
+ else
+ finder.digest_cache[node.name] ||= begin
+ stack.push node
+ node.digest(finder, stack).tap { stack.pop }
+ end
+ end
+ end.join("-")
end
- def source
- template.source
+ def to_dep_map
+ children.any? ? { name => children.map(&:to_dep_map) } : name
end
+ end
- def dependency_digest
- template_digests = dependencies.collect do |template_name|
- Digestor.digest(name: template_name, finder: finder, partial: true)
- end
+ class Partial < Node; end
- (template_digests + injected_dependencies).join("-")
- end
+ class Missing < Node
+ def digest(finder, _ = []) "" end
+ end
- def injected_dependencies
- Array.wrap(options[:dependencies])
- end
- end
+ class Injected < Node
+ def digest(finder, _ = []) name end
+ end
- class PartialDigestor < Digestor # :nodoc:
- def partial?
- true
+ class NullLogger
+ def self.debug(_); end
+ def self.error(_); end
end
end
end
diff --git a/actionview/lib/action_view/flows.rb b/actionview/lib/action_view/flows.rb
index ba24510e56..ff44fa6619 100644
--- a/actionview/lib/action_view/flows.rb
+++ b/actionview/lib/action_view/flows.rb
@@ -1,11 +1,13 @@
-require 'active_support/core_ext/string/output_safety'
+# frozen_string_literal: true
+
+require "active_support/core_ext/string/output_safety"
module ActionView
class OutputFlow #:nodoc:
attr_reader :content
def initialize
- @content = Hash.new { |h,k| h[k] = ActiveSupport::SafeBuffer.new }
+ @content = Hash.new { |h, k| h[k] = ActiveSupport::SafeBuffer.new }
end
# Called by _layout_for to read stored values.
@@ -15,7 +17,7 @@ module ActionView
# Called by each renderer object to set the layout contents.
def set(key, value)
- @content[key] = value
+ @content[key] = ActiveSupport::SafeBuffer.new(value)
end
# Called by content_for
@@ -23,7 +25,6 @@ module ActionView
@content[key] << value
end
alias_method :append!, :append
-
end
class StreamingFlow < OutputFlow #:nodoc:
@@ -37,9 +38,8 @@ module ActionView
end
# Try to get stored content. If the content
- # is not available and we are inside the layout
- # fiber, we set that we are waiting for the given
- # key and yield.
+ # is not available and we're inside the layout fiber,
+ # then it will begin waiting for the given key and yield.
def get(key)
return super if @content.key?(key)
@@ -60,8 +60,8 @@ module ActionView
end
# Appends the contents for the given key. This is called
- # by provides and resumes back to the fiber if it is
- # the key it is waiting for.
+ # by providing and resuming back to the fiber,
+ # if that's the key it's waiting for.
def append!(key, value)
super
@fiber.resume if @waiting_for == key
@@ -69,8 +69,8 @@ module ActionView
private
- def inside_fiber?
- Fiber.current.object_id != @root
- end
+ def inside_fiber?
+ Fiber.current.object_id != @root
+ end
end
end
diff --git a/actionview/lib/action_view/gem_version.rb b/actionview/lib/action_view/gem_version.rb
index 4f45f5b8c8..77ae444a58 100644
--- a/actionview/lib/action_view/gem_version.rb
+++ b/actionview/lib/action_view/gem_version.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionView
# Returns the version of the currently loaded Action View as a <tt>Gem::Version</tt>
def self.gem_version
@@ -5,7 +7,7 @@ module ActionView
end
module VERSION
- MAJOR = 5
+ MAJOR = 6
MINOR = 0
TINY = 0
PRE = "alpha"
diff --git a/actionview/lib/action_view/helpers.rb b/actionview/lib/action_view/helpers.rb
index 787e9d67b2..0d77f74171 100644
--- a/actionview/lib/action_view/helpers.rb
+++ b/actionview/lib/action_view/helpers.rb
@@ -1,4 +1,6 @@
-require 'active_support/benchmarkable'
+# frozen_string_literal: true
+
+require "active_support/benchmarkable"
module ActionView #:nodoc:
module Helpers #:nodoc:
@@ -11,6 +13,7 @@ module ActionView #:nodoc:
autoload :CacheHelper
autoload :CaptureHelper
autoload :ControllerHelper
+ autoload :CspHelper
autoload :CsrfHelper
autoload :DateHelper
autoload :DebugHelper
@@ -20,7 +23,6 @@ module ActionView #:nodoc:
autoload :JavaScriptHelper, "action_view/helpers/javascript_helper"
autoload :NumberHelper
autoload :OutputSafetyHelper
- autoload :RecordTagHelper
autoload :RenderingHelper
autoload :SanitizeHelper
autoload :TagHelper
@@ -44,6 +46,7 @@ module ActionView #:nodoc:
include CacheHelper
include CaptureHelper
include ControllerHelper
+ include CspHelper
include CsrfHelper
include DateHelper
include DebugHelper
@@ -53,7 +56,6 @@ module ActionView #:nodoc:
include JavaScriptHelper
include NumberHelper
include OutputSafetyHelper
- include RecordTagHelper
include RenderingHelper
include SanitizeHelper
include TagHelper
diff --git a/actionview/lib/action_view/helpers/active_model_helper.rb b/actionview/lib/action_view/helpers/active_model_helper.rb
index d5222e3616..e41a95d2ce 100644
--- a/actionview/lib/action_view/helpers/active_model_helper.rb
+++ b/actionview/lib/action_view/helpers/active_model_helper.rb
@@ -1,9 +1,11 @@
-require 'active_support/core_ext/module/attribute_accessors'
-require 'active_support/core_ext/enumerable'
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/attribute_accessors"
+require "active_support/core_ext/enumerable"
module ActionView
# = Active Model Helpers
- module Helpers
+ module Helpers #:nodoc:
module ActiveModelHelper
end
@@ -15,8 +17,8 @@ module ActionView
end
end
- def content_tag(*)
- error_wrapping(super)
+ def content_tag(type, options, *)
+ select_markup_helper?(type) ? super : error_wrapping(super)
end
def tag(type, options, *)
@@ -37,13 +39,17 @@ module ActionView
private
- def object_has_errors?
- object.respond_to?(:errors) && object.errors.respond_to?(:[]) && error_message.present?
- end
+ def object_has_errors?
+ object.respond_to?(:errors) && object.errors.respond_to?(:[]) && error_message.present?
+ end
- def tag_generate_errors?(options)
- options['type'] != 'hidden'
- end
+ def select_markup_helper?(type)
+ ["optgroup", "option"].include?(type)
+ end
+
+ def tag_generate_errors?(options)
+ options["type"] != "hidden"
+ end
end
end
end
diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb
index e32f8e219e..3d7c8dae75 100644
--- a/actionview/lib/action_view/helpers/asset_tag_helper.rb
+++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb
@@ -1,7 +1,11 @@
-require 'active_support/core_ext/array/extract_options'
-require 'active_support/core_ext/hash/keys'
-require 'action_view/helpers/asset_url_helper'
-require 'action_view/helpers/tag_helper'
+# frozen_string_literal: true
+
+require "active_support/core_ext/array/extract_options"
+require "active_support/core_ext/hash/keys"
+require "active_support/core_ext/object/inclusion"
+require "active_support/core_ext/object/try"
+require "action_view/helpers/asset_url_helper"
+require "action_view/helpers/tag_helper"
module ActionView
# = Action View Asset Tag Helpers
@@ -11,7 +15,7 @@ module ActionView
# the assets exist before linking to them:
#
# image_tag("rails.png")
- # # => <img alt="Rails" src="/assets/rails.png" />
+ # # => <img src="/assets/rails.png" />
# stylesheet_link_tag("application")
# # => <link href="/assets/application.css?body=1" media="screen" rel="stylesheet" />
module AssetTagHelper
@@ -35,33 +39,71 @@ module ActionView
# When the Asset Pipeline is enabled, you can pass the name of your manifest as
# source, and include other JavaScript or CoffeeScript files inside the manifest.
#
+ # If the server supports Early Hints header links for these assets will be
+ # automatically pushed.
+ #
+ # ==== Options
+ #
+ # When the last parameter is a hash you can add HTML attributes using that
+ # parameter. The following options are supported:
+ #
+ # * <tt>:extname</tt> - Append an extension to the generated URL unless the extension
+ # already exists. This only applies for relative URLs.
+ # * <tt>:protocol</tt> - Sets the protocol of the generated URL. This option only
+ # applies when a relative URL and +host+ options are provided.
+ # * <tt>:host</tt> - When a relative URL is provided the host is added to the
+ # that path.
+ # * <tt>:skip_pipeline</tt> - This option is used to bypass the asset pipeline
+ # when it is set to true.
+ # * <tt>:nonce</tt> - When set to true, adds an automatic nonce value if
+ # you have Content Security Policy enabled.
+ #
+ # ==== Examples
+ #
# 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>
#
# javascript_include_tag "http://www.example.com/xmlhr.js"
# # => <script src="http://www.example.com/xmlhr.js"></script>
+ #
+ # javascript_include_tag "http://www.example.com/xmlhr.js", nonce: true
+ # # => <script src="http://www.example.com/xmlhr.js" nonce="..."></script>
def javascript_include_tag(*sources)
options = sources.extract_options!.stringify_keys
- path_options = options.extract!('protocol', 'extname').symbolize_keys
- sources.uniq.map { |source|
+ path_options = options.extract!("protocol", "extname", "host", "skip_pipeline").symbolize_keys
+ early_hints_links = []
+
+ sources_tags = sources.uniq.map { |source|
+ href = path_to_javascript(source, path_options)
+ early_hints_links << "<#{href}>; rel=preload; as=script"
tag_options = {
- "src" => path_to_javascript(source, path_options)
+ "src" => href
}.merge!(options)
- content_tag(:script, "", tag_options)
+ if tag_options["nonce"] == true
+ tag_options["nonce"] = content_security_policy_nonce
+ end
+ content_tag("script", "", tag_options)
}.join("\n").html_safe
+
+ request.send_early_hints("Link" => early_hints_links.join("\n")) if respond_to?(:request) && request
+
+ sources_tags
end
# Returns a stylesheet link tag for the sources specified as arguments. If
@@ -71,6 +113,9 @@ module ActionView
# to "screen", so you must explicitly set it to "all" for the stylesheet(s) to
# apply to all media types.
#
+ # If the server supports Early Hints header links for these assets will be
+ # automatically pushed.
+ #
# stylesheet_link_tag "style"
# # => <link href="/assets/style.css" media="screen" rel="stylesheet" />
#
@@ -91,22 +136,29 @@ module ActionView
# # <link href="/css/stylish.css" media="screen" rel="stylesheet" />
def stylesheet_link_tag(*sources)
options = sources.extract_options!.stringify_keys
- path_options = options.extract!('protocol').symbolize_keys
+ path_options = options.extract!("protocol", "host", "skip_pipeline").symbolize_keys
+ early_hints_links = []
- sources.uniq.map { |source|
+ sources_tags = sources.uniq.map { |source|
+ href = path_to_stylesheet(source, path_options)
+ early_hints_links << "<#{href}>; rel=preload; as=style"
tag_options = {
"rel" => "stylesheet",
"media" => "screen",
- "href" => path_to_stylesheet(source, path_options)
+ "href" => href
}.merge!(options)
tag(:link, tag_options)
}.join("\n").html_safe
+
+ request.send_early_hints("Link" => early_hints_links.join("\n")) if respond_to?(:request) && request
+
+ sources_tags
end
# Returns a link tag that browsers and feed readers can use to auto-detect
- # an RSS or Atom feed. The +type+ can either be <tt>:rss</tt> (default) or
- # <tt>:atom</tt>. Control the link options in url_for format using the
- # +url_options+. You can modify the LINK tag itself in +tag_options+.
+ # an RSS, Atom, or JSON feed. The +type+ can be <tt>:rss</tt> (default),
+ # <tt>:atom</tt>, or <tt>:json</tt>. Control the link options in url_for format
+ # using the +url_options+. You can modify the LINK tag itself in +tag_options+.
#
# ==== Options
#
@@ -120,6 +172,8 @@ module ActionView
# # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/action" />
# auto_discovery_link_tag(:atom)
# # => <link rel="alternate" type="application/atom+xml" title="ATOM" href="http://www.currenthost.com/controller/action" />
+ # auto_discovery_link_tag(:json)
+ # # => <link rel="alternate" type="application/json" title="JSON" href="http://www.currenthost.com/controller/action" />
# auto_discovery_link_tag(:rss, {action: "feed"})
# # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/feed" />
# auto_discovery_link_tag(:rss, {action: "feed"}, {title: "My RSS"})
@@ -129,16 +183,16 @@ module ActionView
# auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", {title: "Example RSS"})
# # => <link rel="alternate" type="application/rss+xml" title="Example RSS" href="http://www.example.com/feed.rss" />
def auto_discovery_link_tag(type = :rss, url_options = {}, tag_options = {})
- if !(type == :rss || type == :atom) && tag_options[:type].blank?
- raise ArgumentError.new("You should pass :type tag_option key explicitly, because you have passed #{type} type other than :rss or :atom.")
+ if !(type == :rss || type == :atom || type == :json) && tag_options[:type].blank?
+ raise ArgumentError.new("You should pass :type tag_option key explicitly, because you have passed #{type} type other than :rss, :atom, or :json.")
end
tag(
"link",
"rel" => tag_options[:rel] || "alternate",
- "type" => tag_options[:type] || Mime::Type.lookup_by_extension(type.to_s).to_s,
+ "type" => tag_options[:type] || Template::Types[type].to_s,
"title" => tag_options[:title] || type.to_s.upcase,
- "href" => url_options.is_a?(Hash) ? url_for(url_options.merge(:only_path => false)) : url_options
+ "href" => url_options.is_a?(Hash) ? url_for(url_options.merge(only_path: false)) : url_options
)
end
@@ -169,50 +223,132 @@ module ActionView
#
# favicon_link_tag 'mb-icon.png', rel: 'apple-touch-icon', type: 'image/png'
# # => <link href="/assets/mb-icon.png" rel="apple-touch-icon" type="image/png" />
- def favicon_link_tag(source='favicon.ico', options={})
- tag('link', {
- :rel => 'shortcut icon',
- :type => 'image/x-icon',
- :href => path_to_image(source)
+ def favicon_link_tag(source = "favicon.ico", options = {})
+ tag("link", {
+ rel: "shortcut icon",
+ type: "image/x-icon",
+ href: path_to_image(source, skip_pipeline: options.delete(:skip_pipeline))
}.merge!(options.symbolize_keys))
end
+ # Returns a link tag that browsers can use to preload the +source+.
+ # The +source+ can be the path of a resource managed by asset pipeline,
+ # a full path, or an URI.
+ #
+ # ==== Options
+ #
+ # * <tt>:type</tt> - Override the auto-generated mime type, defaults to the mime type for +source+ extension.
+ # * <tt>:as</tt> - Override the auto-generated value for as attribute, calculated using +source+ extension and mime type.
+ # * <tt>:crossorigin</tt> - Specify the crossorigin attribute, required to load cross-origin resources.
+ # * <tt>:nopush</tt> - Specify if the use of server push is not desired for the resource. Defaults to +false+.
+ #
+ # ==== Examples
+ #
+ # preload_link_tag("custom_theme.css")
+ # # => <link rel="preload" href="/assets/custom_theme.css" as="style" type="text/css" />
+ #
+ # preload_link_tag("/videos/video.webm")
+ # # => <link rel="preload" href="/videos/video.mp4" as="video" type="video/webm" />
+ #
+ # preload_link_tag(post_path(format: :json), as: "fetch")
+ # # => <link rel="preload" href="/posts.json" as="fetch" type="application/json" />
+ #
+ # preload_link_tag("worker.js", as: "worker")
+ # # => <link rel="preload" href="/assets/worker.js" as="worker" type="text/javascript" />
+ #
+ # preload_link_tag("//example.com/font.woff2")
+ # # => <link rel="preload" href="//example.com/font.woff2" as="font" type="font/woff2" crossorigin="anonymous"/>
+ #
+ # preload_link_tag("//example.com/font.woff2", crossorigin: "use-credentials")
+ # # => <link rel="preload" href="//example.com/font.woff2" as="font" type="font/woff2" crossorigin="use-credentials" />
+ #
+ # preload_link_tag("/media/audio.ogg", nopush: true)
+ # # => <link rel="preload" href="/media/audio.ogg" as="audio" type="audio/ogg" />
+ #
+ def preload_link_tag(source, options = {})
+ href = asset_path(source, skip_pipeline: options.delete(:skip_pipeline))
+ extname = File.extname(source).downcase.delete(".")
+ mime_type = options.delete(:type) || Template::Types[extname].try(:to_s)
+ as_type = options.delete(:as) || resolve_link_as(extname, mime_type)
+ crossorigin = options.delete(:crossorigin)
+ crossorigin = "anonymous" if crossorigin == true || (crossorigin.blank? && as_type == "font")
+ nopush = options.delete(:nopush) || false
+
+ link_tag = tag.link({
+ rel: "preload",
+ href: href,
+ as: as_type,
+ type: mime_type,
+ crossorigin: crossorigin
+ }.merge!(options.symbolize_keys))
+
+ early_hints_link = "<#{href}>; rel=preload; as=#{as_type}"
+ early_hints_link += "; type=#{mime_type}" if mime_type
+ early_hints_link += "; crossorigin=#{crossorigin}" if crossorigin
+ early_hints_link += "; nopush" if nopush
+
+ request.send_early_hints("Link" => early_hints_link) if respond_to?(:request) && request
+
+ link_tag
+ end
+
# Returns an HTML image tag for the +source+. The +source+ can be a full
- # path or a file.
+ # path, a file, or an Active Storage attachment.
#
# ==== Options
#
# You can add HTML attributes using the +options+. The +options+ supports
- # two additional keys for convenience and conformance:
+ # additional keys for convenience and conformance:
#
- # * <tt>:alt</tt> - If no alt text is given, the file name part of the
- # +source+ is used (capitalized and without the extension)
# * <tt>:size</tt> - Supplied as "{Width}x{Height}" or "{Number}", so "30x45" becomes
# width="30" and height="45", and "50" becomes width="50" and height="50".
# <tt>:size</tt> will be ignored if the value is not in the correct format.
+ # * <tt>:srcset</tt> - If supplied as a hash or array of <tt>[source, descriptor]</tt>
+ # pairs, each image path will be expanded before the list is formatted as a string.
#
# ==== Examples
#
+ # Assets (images that are part of your app):
+ #
# image_tag("icon")
- # # => <img alt="Icon" src="/assets/icon" />
+ # # => <img src="/assets/icon" />
# image_tag("icon.png")
- # # => <img alt="Icon" src="/assets/icon.png" />
+ # # => <img src="/assets/icon.png" />
# image_tag("icon.png", size: "16x10", alt: "Edit Entry")
# # => <img src="/assets/icon.png" width="16" height="10" alt="Edit Entry" />
# image_tag("/icons/icon.gif", size: "16")
- # # => <img src="/icons/icon.gif" width="16" height="16" alt="Icon" />
+ # # => <img src="/icons/icon.gif" width="16" height="16" />
# image_tag("/icons/icon.gif", height: '32', width: '32')
- # # => <img alt="Icon" height="32" src="/icons/icon.gif" width="32" />
+ # # => <img height="32" src="/icons/icon.gif" width="32" />
# image_tag("/icons/icon.gif", class: "menu_icon")
- # # => <img alt="Icon" class="menu_icon" src="/icons/icon.gif" />
- def image_tag(source, options={})
+ # # => <img class="menu_icon" src="/icons/icon.gif" />
+ # image_tag("/icons/icon.gif", data: { title: 'Rails Application' })
+ # # => <img data-title="Rails Application" src="/icons/icon.gif" />
+ # image_tag("icon.png", srcset: { "icon_2x.png" => "2x", "icon_4x.png" => "4x" })
+ # # => <img src="/assets/icon.png" srcset="/assets/icon_2x.png 2x, /assets/icon_4x.png 4x">
+ # image_tag("pic.jpg", srcset: [["pic_1024.jpg", "1024w"], ["pic_1980.jpg", "1980w"]], sizes: "100vw")
+ # # => <img src="/assets/pic.jpg" srcset="/assets/pic_1024.jpg 1024w, /assets/pic_1980.jpg 1980w" sizes="100vw">
+ #
+ # Active Storage (images that are uploaded by the users of your app):
+ #
+ # image_tag(user.avatar)
+ # # => <img src="/rails/active_storage/blobs/.../tiger.jpg" />
+ # image_tag(user.avatar.variant(resize_to_fit: [100, 100]))
+ # # => <img src="/rails/active_storage/variants/.../tiger.jpg" />
+ # image_tag(user.avatar.variant(resize_to_fit: [100, 100]), size: '100')
+ # # => <img width="100" height="100" src="/rails/active_storage/variants/.../tiger.jpg" />
+ def image_tag(source, options = {})
options = options.symbolize_keys
check_for_image_tag_errors(options)
+ skip_pipeline = options.delete(:skip_pipeline)
- src = options[:src] = path_to_image(source)
+ options[:src] = resolve_image_source(source, skip_pipeline)
- unless src =~ /^(?:cid|data):/ || src.blank?
- options[:alt] = options.fetch(:alt){ image_alt(src) }
+ if options[:srcset] && !options[:srcset].is_a?(String)
+ options[:srcset] = options[:srcset].map do |src_path, size|
+ src_path = path_to_image(src_path, skip_pipeline: skip_pipeline)
+ "#{src_path} #{size}"
+ end.join(", ")
end
options[:width], options[:height] = extract_dimensions(options.delete(:size)) if options[:size]
@@ -237,24 +373,29 @@ module ActionView
# image_alt('underscored_file_name.png')
# # => Underscored file name
def image_alt(src)
- File.basename(src, '.*').sub(/-[[:xdigit:]]{32}\z/, '').tr('-_', ' ').capitalize
+ ActiveSupport::Deprecation.warn("image_alt is deprecated and will be removed from Rails 6.0. You must explicitly set alt text on images.")
+
+ File.basename(src, ".*").sub(/-[[:xdigit:]]{32,64}\z/, "").tr("-_", " ").capitalize
end
# Returns an HTML video tag for the +sources+. If +sources+ is a string,
# a single video tag will be returned. If +sources+ is an array, a video
# tag with nested source tags for each source will be returned. The
- # +sources+ can be full paths or files that exists in your public videos
+ # +sources+ can be full paths or files that exist in your public videos
# directory.
#
# ==== Options
- # You can add HTML attributes using the +options+. The +options+ supports
- # two additional keys for convenience and conformance:
+ #
+ # When the last parameter is a hash you can add HTML attributes using that
+ # parameter. The following options are supported:
#
# * <tt>:poster</tt> - Set an image (like a screenshot) to be shown
# before the video loads. The path is calculated like the +src+ of +image_tag+.
# * <tt>:size</tt> - Supplied as "{Width}x{Height}" or "{Number}", so "30x45" becomes
# width="30" and height="45", and "50" becomes width="50" and height="50".
# <tt>:size</tt> will be ignored if the value is not in the correct format.
+ # * <tt>:poster_skip_pipeline</tt> will bypass the asset pipeline when using
+ # the <tt>:poster</tt> option instead using an asset in the public folder.
#
# ==== Examples
#
@@ -262,10 +403,12 @@ module ActionView
# # => <video src="/videos/trailer"></video>
# video_tag("trailer.ogg")
# # => <video src="/videos/trailer.ogg"></video>
- # video_tag("trailer.ogg", controls: true, autobuffer: true)
- # # => <video autobuffer="autobuffer" controls="controls" src="/videos/trailer.ogg" ></video>
+ # video_tag("trailer.ogg", controls: true, preload: 'none')
+ # # => <video preload="none" controls="controls" src="/videos/trailer.ogg"></video>
# video_tag("trailer.m4v", size: "16x10", poster: "screenshot.png")
# # => <video src="/videos/trailer.m4v" width="16" height="10" poster="/assets/screenshot.png"></video>
+ # video_tag("trailer.m4v", size: "16x10", poster: "screenshot.png", poster_skip_pipeline: true)
+ # # => <video src="/videos/trailer.m4v" width="16" height="10" poster="screenshot.png"></video>
# video_tag("/trailers/hd.avi", size: "16x16")
# # => <video src="/trailers/hd.avi" width="16" height="16"></video>
# video_tag("/trailers/hd.avi", size: "16")
@@ -279,15 +422,23 @@ module ActionView
# video_tag(["trailer.ogg", "trailer.flv"], size: "160x120")
# # => <video height="120" width="160"><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video>
def video_tag(*sources)
- multiple_sources_tag('video', sources) do |options|
- options[:poster] = path_to_image(options[:poster]) if options[:poster]
- options[:width], options[:height] = extract_dimensions(options.delete(:size)) if options[:size]
+ options = sources.extract_options!.symbolize_keys
+ public_poster_folder = options.delete(:poster_skip_pipeline)
+ sources << options
+ multiple_sources_tag_builder("video", sources) do |tag_options|
+ tag_options[:poster] = path_to_image(tag_options[:poster], skip_pipeline: public_poster_folder) if tag_options[:poster]
+ tag_options[:width], tag_options[:height] = extract_dimensions(tag_options.delete(:size)) if tag_options[:size]
end
end
- # Returns an HTML audio tag for the +source+.
- # The +source+ can be full path or file that exists in
- # your public audios directory.
+ # Returns an HTML audio tag for the +sources+. If +sources+ is a string,
+ # a single audio tag will be returned. If +sources+ is an array, an audio
+ # tag with nested source tags for each source will be returned. The
+ # +sources+ can be full paths or files that exist in your public audios
+ # directory.
+ #
+ # When the last parameter is a hash you can add HTML attributes using that
+ # parameter.
#
# audio_tag("sound")
# # => <audio src="/audios/sound"></audio>
@@ -298,31 +449,42 @@ module ActionView
# audio_tag("sound.wav", "sound.mid")
# # => <audio><source src="/audios/sound.wav" /><source src="/audios/sound.mid" /></audio>
def audio_tag(*sources)
- multiple_sources_tag('audio', sources)
+ multiple_sources_tag_builder("audio", sources)
end
private
- def multiple_sources_tag(type, sources)
- options = sources.extract_options!.symbolize_keys
+ def multiple_sources_tag_builder(type, sources)
+ options = sources.extract_options!.symbolize_keys
+ skip_pipeline = options.delete(:skip_pipeline)
sources.flatten!
yield options if block_given?
if sources.size > 1
content_tag(type, options) do
- safe_join sources.map { |source| tag("source", :src => send("path_to_#{type}", source)) }
+ safe_join sources.map { |source| tag("source", src: send("path_to_#{type}", source, skip_pipeline: skip_pipeline)) }
end
else
- options[:src] = send("path_to_#{type}", sources.first)
+ options[:src] = send("path_to_#{type}", sources.first, skip_pipeline: skip_pipeline)
content_tag(type, nil, options)
end
end
+ def resolve_image_source(source, skip_pipeline)
+ if source.is_a?(Symbol) || source.is_a?(String)
+ path_to_image(source, skip_pipeline: skip_pipeline)
+ else
+ polymorphic_url(source)
+ end
+ rescue NoMethodError => e
+ raise ArgumentError, "Can't resolve image into URL: #{e}"
+ end
+
def extract_dimensions(size)
size = size.to_s
- if size =~ %r{\A\d+x\d+\z}
- size.split('x')
- elsif size =~ %r{\A\d+\z}
+ if /\A\d+x\d+\z/.match?(size)
+ size.split("x")
+ elsif /\A\d+\z/.match?(size)
[size, size]
end
end
@@ -332,6 +494,18 @@ module ActionView
raise ArgumentError, "Cannot pass a :size option with a :height or :width option"
end
end
+
+ def resolve_link_as(extname, mime_type)
+ if extname == "js"
+ "script"
+ elsif extname == "css"
+ "style"
+ elsif extname == "vtt"
+ "track"
+ elsif (type = mime_type.to_s.split("/")[0]) && type.in?(%w(audio video font))
+ type
+ end
+ end
end
end
end
diff --git a/actionview/lib/action_view/helpers/asset_url_helper.rb b/actionview/lib/action_view/helpers/asset_url_helper.rb
index b19dc25025..cc62783d60 100644
--- a/actionview/lib/action_view/helpers/asset_url_helper.rb
+++ b/actionview/lib/action_view/helpers/asset_url_helper.rb
@@ -1,10 +1,12 @@
-require 'zlib'
+# frozen_string_literal: true
+
+require "zlib"
module ActionView
# = Action View Asset URL Helpers
- module Helpers
+ module Helpers #:nodoc:
# This module provides methods for generating asset paths and
- # urls.
+ # URLs.
#
# image_path("rails.png")
# # => "/assets/rails.png"
@@ -27,30 +29,37 @@ module ActionView
# Helpers take that into account:
#
# image_tag("rails.png")
- # # => <img alt="Rails" src="http://assets.example.com/assets/rails.png" />
+ # # => <img src="http://assets.example.com/assets/rails.png" />
# stylesheet_link_tag("application")
# # => <link href="http://assets.example.com/assets/application.css" media="screen" rel="stylesheet" />
#
- # Browsers typically open at most two simultaneous connections to a single
- # host, which means your assets often have to wait for other assets to finish
- # downloading. You can alleviate this by using a <tt>%d</tt> wildcard in the
- # +asset_host+. For example, "assets%d.example.com". If that wildcard is
- # present Rails distributes asset requests among the corresponding four hosts
- # "assets0.example.com", ..., "assets3.example.com". With this trick browsers
- # will open eight simultaneous connections rather than two.
+ # Browsers open a limited number of simultaneous connections to a single
+ # host. The exact number varies by browser and version. This limit may cause
+ # some asset downloads to wait for previous assets to finish before they can
+ # begin. You can use the <tt>%d</tt> wildcard in the +asset_host+ to
+ # distribute the requests over four hosts. For example,
+ # <tt>assets%d.example.com</tt> will spread the asset requests over
+ # "assets0.example.com", ..., "assets3.example.com".
#
# image_tag("rails.png")
- # # => <img alt="Rails" src="http://assets0.example.com/assets/rails.png" />
+ # # => <img src="http://assets0.example.com/assets/rails.png" />
# stylesheet_link_tag("application")
# # => <link href="http://assets2.example.com/assets/application.css" media="screen" rel="stylesheet" />
#
- # To do this, you can either setup four actual hosts, or you can use wildcard
- # DNS to CNAME the wildcard to a single asset host. You can read more about
- # setting up your DNS CNAME records from your ISP.
+ # This may improve the asset loading performance of your application.
+ # It is also possible the combination of additional connection overhead
+ # (DNS, SSL) and the overall browser connection limits may result in this
+ # solution being slower. You should be sure to measure your actual
+ # performance across targeted browsers both before and after this change.
+ #
+ # To implement the corresponding hosts you can either setup four actual
+ # hosts or use wildcard DNS to CNAME the wildcard to a single asset host.
+ # You can read more about setting up your DNS CNAME records from your ISP.
#
# Note: This is purely a browser performance optimization and is not meant
- # for server load balancing. See http://www.die.net/musings/page_load_time/
- # for background.
+ # for server load balancing. See https://www.die.net/musings/page_load_time/
+ # for background and https://www.browserscope.org/?category=network for
+ # connection limit data.
#
# Alternatively, you can exert more control over the asset host by setting
# +asset_host+ to a proc like this:
@@ -59,7 +68,7 @@ module ActionView
# "http://assets#{Digest::MD5.hexdigest(source).to_i(16) % 2 + 1}.example.com"
# }
# image_tag("rails.png")
- # # => <img alt="Rails" src="http://assets1.example.com/assets/rails.png" />
+ # # => <img src="http://assets1.example.com/assets/rails.png" />
# stylesheet_link_tag("application")
# # => <link href="http://assets2.example.com/assets/application.css" media="screen" rel="stylesheet" />
#
@@ -78,7 +87,7 @@ module ActionView
# end
# }
# image_tag("rails.png")
- # # => <img alt="Rails" src="http://assets.example.com/assets/rails.png" />
+ # # => <img src="http://assets.example.com/assets/rails.png" />
# stylesheet_link_tag("application")
# # => <link href="http://stylesheets.example.com/assets/application.css" media="screen" rel="stylesheet" />
#
@@ -88,9 +97,10 @@ module ActionView
# still sending assets for plain HTTP requests from asset hosts. If you don't
# have SSL certificates for each of the asset hosts this technique allows you
# to avoid warnings in the client about mixed media.
- # Note that the request parameter might not be supplied, e.g. when the assets
- # are precompiled via a Rake task. Make sure to use a Proc instead of a lambda,
- # since a Proc allows missing parameters and sets them to nil.
+ # Note that the +request+ parameter might not be supplied, e.g. when the assets
+ # are precompiled with the command `rails assets:precompile`. Make sure to use a
+ # +Proc+ instead of a lambda, since a +Proc+ allows missing parameters and sets them
+ # to +nil+.
#
# config.action_controller.asset_host = Proc.new { |source, request|
# if request && request.ssl?
@@ -110,31 +120,86 @@ module ActionView
module AssetUrlHelper
URI_REGEXP = %r{^[-a-z]+://|^(?:cid|data):|^//}i
- # Computes the path to asset in public directory. If :type
- # options is set, a file extension will be appended and scoped
- # to the corresponding public directory.
+ # This is the entry point for all assets.
+ # When using the asset pipeline (i.e. sprockets and sprockets-rails), the
+ # behavior is "enhanced". You can bypass the asset pipeline by passing in
+ # <tt>skip_pipeline: true</tt> to the options.
#
# All other asset *_path helpers delegate through this method.
#
- # asset_path "application.js" # => /assets/application.js
- # asset_path "application", type: :javascript # => /assets/application.js
- # asset_path "application", type: :stylesheet # => /assets/application.css
- # asset_path "http://www.example.com/js/xmlhr.js" # => http://www.example.com/js/xmlhr.js
+ # === With the asset pipeline
+ #
+ # All options passed to +asset_path+ will be passed to +compute_asset_path+
+ # which is implemented by sprockets-rails.
+ #
+ # asset_path("application.js") # => "/assets/application-60aa4fdc5cea14baf5400fba1abf4f2a46a5166bad4772b1effe341570f07de9.js"
+ #
+ # === Without the asset pipeline (<tt>skip_pipeline: true</tt>)
+ #
+ # Accepts a <tt>type</tt> option that can specify the asset's extension. No error
+ # checking is done to verify the source passed into +asset_path+ is valid
+ # and that the file exists on disk.
+ #
+ # asset_path("application.js", skip_pipeline: true) # => "application.js"
+ # asset_path("filedoesnotexist.png", skip_pipeline: true) # => "filedoesnotexist.png"
+ # asset_path("application", type: :javascript, skip_pipeline: true) # => "/javascripts/application.js"
+ # asset_path("application", type: :stylesheet, skip_pipeline: true) # => "/stylesheets/application.css"
+ #
+ # === Options applying to all assets
+ #
+ # Below lists scenarios that apply to +asset_path+ whether or not you're
+ # using the asset pipeline.
+ #
+ # - All fully qualified URLs are returned immediately. This bypasses the
+ # asset pipeline and all other behavior described.
+ #
+ # asset_path("http://www.example.com/js/xmlhr.js") # => "http://www.example.com/js/xmlhr.js"
+ #
+ # - All assets that begin with a forward slash are assumed to be full
+ # URLs and will not be expanded. This will bypass the asset pipeline.
+ #
+ # asset_path("/foo.png") # => "/foo.png"
+ #
+ # - All blank strings will be returned immediately. This bypasses the
+ # asset pipeline and all other behavior described.
+ #
+ # asset_path("") # => ""
+ #
+ # - If <tt>config.relative_url_root</tt> is specified, all assets will have that
+ # root prepended.
+ #
+ # Rails.application.config.relative_url_root = "bar"
+ # asset_path("foo.js", skip_pipeline: true) # => "bar/foo.js"
+ #
+ # - A different asset host can be specified via <tt>config.action_controller.asset_host</tt>
+ # this is commonly used in conjunction with a CDN.
+ #
+ # Rails.application.config.action_controller.asset_host = "assets.example.com"
+ # asset_path("foo.js", skip_pipeline: true) # => "http://assets.example.com/foo.js"
+ #
+ # - An extension name can be specified manually with <tt>extname</tt>.
+ #
+ # asset_path("foo", skip_pipeline: true, extname: ".js") # => "/foo.js"
+ # asset_path("foo.css", skip_pipeline: true, extname: ".js") # => "/foo.css.js"
def asset_path(source, options = {})
raise ArgumentError, "nil is not a valid asset source" if source.nil?
source = source.to_s
- return "" unless source.present?
- return source if source =~ URI_REGEXP
+ return "" if source.blank?
+ return source if URI_REGEXP.match?(source)
- tail, source = source[/([\?#].+)$/], source.sub(/([\?#].+)$/, '')
+ tail, source = source[/([\?#].+)$/], source.sub(/([\?#].+)$/, "")
if extname = compute_asset_extname(source, options)
source = "#{source}#{extname}"
end
if source[0] != ?/
- source = compute_asset_path(source, options)
+ if options[:skip_pipeline]
+ source = public_compute_asset_path(source, options)
+ else
+ source = compute_asset_path(source, options)
+ end
end
relative_url_root = defined?(config.relative_url_root) && config.relative_url_root
@@ -161,31 +226,35 @@ module ActionView
# asset_url "application.js", host: "http://cdn.example.com" # => http://cdn.example.com/assets/application.js
#
def asset_url(source, options = {})
- path_to_asset(source, options.merge(:protocol => :request))
+ path_to_asset(source, options.merge(protocol: :request))
end
alias_method :url_to_asset, :asset_url # aliased to avoid conflicts with an asset_url named route
ASSET_EXTENSIONS = {
- javascript: '.js',
- stylesheet: '.css'
+ javascript: ".js",
+ stylesheet: ".css"
}
- # Compute extname to append to asset path. Returns nil if
+ # Compute extname to append to asset path. Returns +nil+ if
# nothing should be added.
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.
ASSET_PUBLIC_DIRECTORIES = {
- audio: '/audios',
- font: '/fonts',
- image: '/images',
- javascript: '/javascripts',
- stylesheet: '/stylesheets',
- video: '/videos'
+ audio: "/audios",
+ font: "/fonts",
+ image: "/images",
+ javascript: "/javascripts",
+ stylesheet: "/stylesheets",
+ video: "/videos"
}
# Computes asset path to public directory. Plugins and
@@ -195,6 +264,7 @@ module ActionView
dir = ASSET_PUBLIC_DIRECTORIES[options[:type]] || ""
File.join(dir, source)
end
+ alias :public_compute_asset_path :compute_asset_path
# Pick an asset host for this source. Returns +nil+ if no host is set,
# the host if no wildcard is set, the host interpolated with the
@@ -206,19 +276,21 @@ module ActionView
host = options[:host]
host ||= config.asset_host if defined? config.asset_host
- if host.respond_to?(:call)
- arity = host.respond_to?(:arity) ? host.arity : host.method(:call).arity
- args = [source]
- args << request if request && (arity > 1 || arity < 0)
- host = host.call(*args)
- elsif host =~ /%d/
- host = host % (Zlib.crc32(source) % 4)
+ if host
+ if host.respond_to?(:call)
+ arity = host.respond_to?(:arity) ? host.arity : host.method(:call).arity
+ args = [source]
+ args << request if request && (arity > 1 || arity < 0)
+ host = host.call(*args)
+ elsif host.include?("%d")
+ host = host % (Zlib.crc32(source) % 4)
+ end
end
host ||= request.base_url if request && options[:protocol] == :request
return unless host
- if host =~ URI_REGEXP
+ if URI_REGEXP.match?(host)
host
else
protocol = options[:protocol] || config.default_asset_host_protocol || (request ? :request : :relative)
@@ -244,7 +316,7 @@ module ActionView
# javascript_path "http://www.example.com/js/xmlhr" # => http://www.example.com/js/xmlhr
# javascript_path "http://www.example.com/js/xmlhr.js" # => http://www.example.com/js/xmlhr.js
def javascript_path(source, options = {})
- path_to_asset(source, {type: :javascript}.merge!(options))
+ path_to_asset(source, { type: :javascript }.merge!(options))
end
alias_method :path_to_javascript, :javascript_path # aliased to avoid conflicts with a javascript_path named route
@@ -253,10 +325,10 @@ module ActionView
# Since +javascript_url+ is based on +asset_url+ method you can set :host options. If :host
# options is set, it overwrites global +config.action_controller.asset_host+ setting.
#
- # javascript_url "js/xmlhr.js", host: "http://stage.example.com" # => http://stage.example.com/assets/dir/xmlhr.js
+ # javascript_url "js/xmlhr.js", host: "http://stage.example.com" # => http://stage.example.com/assets/js/xmlhr.js
#
def javascript_url(source, options = {})
- url_to_asset(source, {type: :javascript}.merge!(options))
+ url_to_asset(source, { type: :javascript }.merge!(options))
end
alias_method :url_to_javascript, :javascript_url # aliased to avoid conflicts with a javascript_url named route
@@ -271,7 +343,7 @@ module ActionView
# stylesheet_path "http://www.example.com/css/style" # => http://www.example.com/css/style
# stylesheet_path "http://www.example.com/css/style.css" # => http://www.example.com/css/style.css
def stylesheet_path(source, options = {})
- path_to_asset(source, {type: :stylesheet}.merge!(options))
+ path_to_asset(source, { type: :stylesheet }.merge!(options))
end
alias_method :path_to_stylesheet, :stylesheet_path # aliased to avoid conflicts with a stylesheet_path named route
@@ -280,10 +352,10 @@ module ActionView
# Since +stylesheet_url+ is based on +asset_url+ method you can set :host options. If :host
# options is set, it overwrites global +config.action_controller.asset_host+ setting.
#
- # stylesheet_url "css/style.css", host: "http://stage.example.com" # => http://stage.example.com/css/style.css
+ # stylesheet_url "css/style.css", host: "http://stage.example.com" # => http://stage.example.com/assets/css/style.css
#
def stylesheet_url(source, options = {})
- url_to_asset(source, {type: :stylesheet}.merge!(options))
+ url_to_asset(source, { type: :stylesheet }.merge!(options))
end
alias_method :url_to_stylesheet, :stylesheet_url # aliased to avoid conflicts with a stylesheet_url named route
@@ -301,7 +373,7 @@ module ActionView
# The alias +path_to_image+ is provided to avoid that. Rails uses the alias internally, and
# plugin authors are encouraged to do so.
def image_path(source, options = {})
- path_to_asset(source, {type: :image}.merge!(options))
+ path_to_asset(source, { type: :image }.merge!(options))
end
alias_method :path_to_image, :image_path # aliased to avoid conflicts with an image_path named route
@@ -310,10 +382,10 @@ module ActionView
# Since +image_url+ is based on +asset_url+ method you can set :host options. If :host
# options is set, it overwrites global +config.action_controller.asset_host+ setting.
#
- # image_url "edit.png", host: "http://stage.example.com" # => http://stage.example.com/edit.png
+ # image_url "edit.png", host: "http://stage.example.com" # => http://stage.example.com/assets/edit.png
#
def image_url(source, options = {})
- url_to_asset(source, {type: :image}.merge!(options))
+ url_to_asset(source, { type: :image }.merge!(options))
end
alias_method :url_to_image, :image_url # aliased to avoid conflicts with an image_url named route
@@ -327,7 +399,7 @@ module ActionView
# video_path("/trailers/hd.avi") # => /trailers/hd.avi
# video_path("http://www.example.com/vid/hd.avi") # => http://www.example.com/vid/hd.avi
def video_path(source, options = {})
- path_to_asset(source, {type: :video}.merge!(options))
+ path_to_asset(source, { type: :video }.merge!(options))
end
alias_method :path_to_video, :video_path # aliased to avoid conflicts with a video_path named route
@@ -336,12 +408,12 @@ module ActionView
# Since +video_url+ is based on +asset_url+ method you can set :host options. If :host
# options is set, it overwrites global +config.action_controller.asset_host+ setting.
#
- # video_url "hd.avi", host: "http://stage.example.com" # => http://stage.example.com/hd.avi
+ # video_url "hd.avi", host: "http://stage.example.com" # => http://stage.example.com/videos/hd.avi
#
def video_url(source, options = {})
- url_to_asset(source, {type: :video}.merge!(options))
+ 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.
@@ -353,7 +425,7 @@ module ActionView
# audio_path("/sounds/horse.wav") # => /sounds/horse.wav
# audio_path("http://www.example.com/sounds/horse.wav") # => http://www.example.com/sounds/horse.wav
def audio_path(source, options = {})
- path_to_asset(source, {type: :audio}.merge!(options))
+ path_to_asset(source, { type: :audio }.merge!(options))
end
alias_method :path_to_audio, :audio_path # aliased to avoid conflicts with an audio_path named route
@@ -362,10 +434,10 @@ module ActionView
# Since +audio_url+ is based on +asset_url+ method you can set :host options. If :host
# options is set, it overwrites global +config.action_controller.asset_host+ setting.
#
- # audio_url "horse.wav", host: "http://stage.example.com" # => http://stage.example.com/horse.wav
+ # audio_url "horse.wav", host: "http://stage.example.com" # => http://stage.example.com/audios/horse.wav
#
def audio_url(source, options = {})
- url_to_asset(source, {type: :audio}.merge!(options))
+ url_to_asset(source, { type: :audio }.merge!(options))
end
alias_method :url_to_audio, :audio_url # aliased to avoid conflicts with an audio_url named route
@@ -378,21 +450,21 @@ module ActionView
# font_path("/dir/font.ttf") # => /dir/font.ttf
# font_path("http://www.example.com/dir/font.ttf") # => http://www.example.com/dir/font.ttf
def font_path(source, options = {})
- path_to_asset(source, {type: :font}.merge!(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.
# Since +font_url+ is based on +asset_url+ method you can set :host options. If :host
# options is set, it overwrites global +config.action_controller.asset_host+ setting.
#
- # font_url "font.ttf", host: "http://stage.example.com" # => http://stage.example.com/font.ttf
+ # font_url "font.ttf", host: "http://stage.example.com" # => http://stage.example.com/fonts/font.ttf
#
def font_url(source, options = {})
- url_to_asset(source, {type: :font}.merge!(options))
+ 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/atom_feed_helper.rb b/actionview/lib/action_view/helpers/atom_feed_helper.rb
index bb1cdd0f8d..e6b9878271 100644
--- a/actionview/lib/action_view/helpers/atom_feed_helper.rb
+++ b/actionview/lib/action_view/helpers/atom_feed_helper.rb
@@ -1,8 +1,10 @@
-require 'set'
+# frozen_string_literal: true
+
+require "set"
module ActionView
# = Action View Atom Feed Helpers
- module Helpers
+ module Helpers #:nodoc:
module AtomFeedHelper
# Adds easy defaults to writing Atom feeds with the Builder template engine (this does not work on ERB or any other
# template languages).
@@ -51,7 +53,7 @@ module ActionView
# * <tt>:language</tt>: Defaults to "en-US".
# * <tt>:root_url</tt>: The HTML alternative that this feed is doubling for. Defaults to / on the current host.
# * <tt>:url</tt>: The URL for this feed. Defaults to the current URL.
- # * <tt>:id</tt>: The id for this feed. Defaults to "tag:localhost,2005:/posts", in this case.
+ # * <tt>:id</tt>: The id for this feed. Defaults to "tag:localhost,2005:/posts", in this case.
# * <tt>:schema_date</tt>: The date at which the tag scheme for the feed was first used. A good default is the year you
# created the feed. See http://feedvalidator.org/docs/error/InvalidTAG.html for more information. If not specified,
# 2005 is used (as an "I don't care" value).
@@ -103,7 +105,7 @@ module ActionView
xml = options.delete(:xml) || eval("xml", block.binding)
xml.instruct!
if options[:instruct]
- options[:instruct].each do |target,attrs|
+ options[:instruct].each do |target, attrs|
if attrs.respond_to?(:keys)
xml.instruct!(target, attrs)
elsif attrs.respond_to?(:each)
@@ -112,13 +114,13 @@ module ActionView
end
end
- feed_opts = {"xml:lang" => options[:language] || "en-US", "xmlns" => 'http://www.w3.org/2005/Atom'}
- feed_opts.merge!(options).reject!{|k,v| !k.to_s.match(/^xml/)}
+ feed_opts = { "xml:lang" => options[:language] || "en-US", "xmlns" => "http://www.w3.org/2005/Atom" }
+ feed_opts.merge!(options).reject! { |k, v| !k.to_s.match(/^xml/) }
xml.feed(feed_opts) do
xml.id(options[:id] || "tag:#{request.host},#{options[:schema_date]}:#{request.fullpath.split(".")[0]}")
- xml.link(:rel => 'alternate', :type => 'text/html', :href => options[:root_url] || (request.protocol + request.host_with_port))
- xml.link(:rel => 'self', :type => 'application/atom+xml', :href => options[:url] || request.url)
+ xml.link(rel: "alternate", type: "text/html", href: options[:root_url] || (request.protocol + request.host_with_port))
+ xml.link(rel: "self", type: "application/atom+xml", href: options[:url] || request.url)
yield AtomFeedBuilder.new(xml, self, options)
end
@@ -132,13 +134,13 @@ module ActionView
end
private
- # Delegate to xml builder, first wrapping the element in a xhtml
+ # Delegate to xml builder, first wrapping the element in an xhtml
# namespaced div element if the method and arguments indicate
# that an xhtml_block? is desired.
def method_missing(method, *arguments, &block)
if xhtml_block?(method, arguments)
@xml.__send__(method, *arguments) do
- @xml.div(:xmlns => 'http://www.w3.org/1999/xhtml') do |xhtml|
+ @xml.div(xmlns: "http://www.w3.org/1999/xhtml") do |xhtml|
block.call(xhtml)
end
end
@@ -153,7 +155,7 @@ module ActionView
def xhtml_block?(method, arguments)
if XHTML_TAG_NAMES.include?(method.to_s)
last = arguments.last
- last.is_a?(Hash) && last[:type].to_s == 'xhtml'
+ last.is_a?(Hash) && last[:type].to_s == "xhtml"
end
end
end
@@ -163,7 +165,7 @@ module ActionView
@xml, @view, @feed_options = xml, view, feed_options
end
- # Accepts a Date or Time object and inserts it in the proper format. If nil is passed, current time in UTC is used.
+ # Accepts a Date or Time object and inserts it in the proper format. If +nil+ is passed, current time in UTC is used.
def updated(date_or_time = nil)
@xml.updated((date_or_time || Time.now.utc).xmlschema)
end
@@ -174,7 +176,7 @@ module ActionView
#
# * <tt>:published</tt>: Time first published. Defaults to the created_at attribute on the record if one such exists.
# * <tt>:updated</tt>: Time of update. Defaults to the updated_at attribute on the record if one such exists.
- # * <tt>:url</tt>: The URL for this entry or false or nil for not having a link tag. Defaults to the polymorphic_url for the record.
+ # * <tt>:url</tt>: The URL for this entry or +false+ or +nil+ for not having a link tag. Defaults to the +polymorphic_url+ for the record.
# * <tt>:id</tt>: The ID for this entry. Defaults to "tag:#{@view.request.host},#{@feed_options[:schema_date]}:#{record.class}/#{record.id}"
# * <tt>:type</tt>: The TYPE for this entry. Defaults to "text/html".
def entry(record, options = {})
@@ -189,16 +191,15 @@ module ActionView
@xml.updated((options[:updated] || record.updated_at).xmlschema)
end
- type = options.fetch(:type, 'text/html')
+ type = options.fetch(:type, "text/html")
url = options.fetch(:url) { @view.polymorphic_url(record) }
- @xml.link(:rel => 'alternate', :type => type, :href => url) if url
+ @xml.link(rel: "alternate", type: type, href: url) if url
yield AtomBuilder.new(@xml)
end
end
end
-
end
end
end
diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb
index 797d029317..b1a14250c3 100644
--- a/actionview/lib/action_view/helpers/cache_helper.rb
+++ b/actionview/lib/action_view/helpers/cache_helper.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
module ActionView
# = Action View Cache Helper
- module Helpers
+ module Helpers #:nodoc:
module CacheHelper
# This helper exposes a method for caching fragments of a view
# rather than an entire action or page. This technique is useful
@@ -8,10 +10,9 @@ module ActionView
# fragments, and so on. This method takes a block that contains
# the content you wish to cache.
#
- # The best way to use this is by doing key-based cache expiration
- # on top of a cache store like Memcached that'll automatically
- # kick out old entries. For more on key-based expiration, see:
- # http://signalvnoise.com/posts/3113-how-key-based-cache-expiration-works
+ # The best way to use this is by doing recyclable key-based cache expiration
+ # on top of a cache store like Memcached or Redis that'll automatically
+ # kick out old entries.
#
# When using this method, you list the cache dependency as the name of the cache, like so:
#
@@ -23,10 +24,14 @@ module ActionView
# This approach will assume that when a new topic is added, you'll touch
# the project. The cache key generated from this call will be something like:
#
- # views/projects/123-20120806214154/7a1156131a6928cb0026877f8b749ac9
- # ^class ^id ^updated_at ^template tree digest
+ # views/template/action.html.erb:7a1156131a6928cb0026877f8b749ac9/projects/123
+ # ^template path ^template tree digest ^class ^id
#
- # The cache is thus automatically bumped whenever the project updated_at is touched.
+ # This cache key is stable, but it's combined with a cache version derived from the project
+ # record. When the project updated_at is touched, the #cache_version changes, even
+ # if the key stays stable. This means that unlike a traditional key-based cache expiration
+ # approach, you won't be generating cache trash, unused keys, simply because the dependent
+ # record is updated.
#
# If your template cache depends on multiple sources (try to avoid this to keep things simple),
# you can name all these dependencies as part of an array:
@@ -41,11 +46,11 @@ module ActionView
#
# ==== \Template digest
#
- # The template digest that's added to the cache key is computed by taking an md5 of the
+ # The template digest that's added to the cache key is computed by taking an MD5 of the
# contents of the entire template file. This ensures that your caches will automatically
# expire when you change the template file.
#
- # Note that the md5 is taken of the entire template file, not just what's within the
+ # Note that the MD5 is taken of the entire template file, not just what's within the
# cache do/end call. So it's possible that changing something outside of that call will
# still expire the cache.
#
@@ -69,11 +74,11 @@ module ActionView
# render 'comments/comments'
# render('comments/comments')
#
- # render "header" => render("comments/header")
+ # render "header" translates to render("comments/header")
#
- # render(@topic) => render("topics/topic")
- # render(topics) => render("topics/topic")
- # render(message.topics) => render("topics/topic")
+ # render(@topic) translates to render("topics/topic")
+ # render(topics) translates to render("topics/topic")
+ # render(message.topics) translates to render("topics/topic")
#
# It's not possible to derive all render calls like that, though.
# Here are a few examples of things that can't be derived:
@@ -88,7 +93,7 @@ module ActionView
#
# === Explicit dependencies
#
- # Some times you'll have template dependencies that can't be derived at all. This is typically
+ # Sometimes you'll have template dependencies that can't be derived at all. This is typically
# the case when you have template rendering that happens in helpers. Here's an example:
#
# <%= render_sortable_todolists @project.todolists %>
@@ -98,7 +103,19 @@ module ActionView
# <%# Template Dependency: todolists/todolist %>
# <%= render_sortable_todolists @project.todolists %>
#
- # The pattern used to match these is <tt>/# Template Dependency: (\S+)/</tt>,
+ # In some cases, like a single table inheritance setup, you might have
+ # a bunch of explicit dependencies. Instead of writing every template out,
+ # you can use a wildcard to match any template in a directory:
+ #
+ # <%# Template Dependency: events/* %>
+ # <%= render_categorizable_events @person.events %>
+ #
+ # This marks every template in the directory as a dependency. To find those
+ # templates, the wildcard path must be absolutely defined from <tt>app/views</tt> or paths
+ # otherwise added with +prepend_view_path+ or +append_view_path+.
+ # This way the wildcard for <tt>app/views/recordings/events</tt> would be <tt>recordings/events/*</tt> etc.
+ #
+ # The pattern used to match explicit dependencies is <tt>/# Template Dependency: (\S+)/</tt>,
# so it's important that you type it out just so.
# You can only declare one template dependency per line.
#
@@ -106,7 +123,7 @@ module ActionView
#
# If you use a helper method, for example, inside a cached block and
# you then update that helper, you'll have to bump the cache as well.
- # It doesn't really matter how you do it, but the md5 of the template file
+ # It doesn't really matter how you do it, but the MD5 of the template file
# must change. One recommendation is to simply be explicit in a comment, like:
#
# <%# Helper Dependency Updated: May 6, 2012 at 6pm %>
@@ -114,47 +131,42 @@ module ActionView
#
# Now all you have to do is change that timestamp when the helper method changes.
#
- # === Automatic Collection Caching
- #
- # When rendering collections such as:
+ # === Collection Caching
#
- # <%= render @notifications %>
- # <%= render partial: 'notifications/notification', collection: @notifications %>
+ # When rendering a collection of objects that each use the same partial, a <tt>:cached</tt>
+ # option can be passed.
#
- # If the notifications/_notification partial starts with a cache call as:
+ # For collections rendered such:
#
- # <% cache notification do %>
- # <%= notification.name %>
- # <% end %>
+ # <%= render partial: 'projects/project', collection: @projects, cached: true %>
#
- # The collection can then automatically use any cached renders for that
- # template by reading them at once instead of one by one.
+ # The <tt>cached: true</tt> will make Action View's rendering read several templates
+ # from cache at once instead of one call per template.
#
- # See ActionView::Template::Handlers::ERB.resource_cache_call_pattern for
- # more information on what cache calls make a template eligible for this
- # collection caching.
+ # Templates in the collection not already cached are written to cache.
#
- # The automatic cache multi read can be turned off like so:
+ # Works great alongside individual template fragment caching.
+ # For instance if the template the collection renders is cached like:
#
- # <%= render @notifications, cache: false %>
+ # # projects/_project.html.erb
+ # <% cache project do %>
+ # <%# ... %>
+ # <% end %>
#
- # === Explicit Collection Caching
+ # Any collection renders will find those cached templates when attempting
+ # to read multiple templates at once.
#
- # If the partial template doesn't start with a clean cache call as
- # mentioned above, you can still benefit from collection caching by
- # adding a special comment format anywhere in the template, like:
+ # If your collection cache depends on multiple sources (try to avoid this to keep things simple),
+ # you can name all these dependencies as part of a block that returns an array:
#
- # <%# Template Collection: notification %>
- # <% my_helper_that_calls_cache(some_arg, notification) do %>
- # <%= notification.name %>
- # <% end %>
+ # <%= render partial: 'projects/project', collection: @projects, cached: -> project { [ project, current_user ] } %>
#
- # The pattern used to match these is <tt>/# Template Collection: (\S+)/</tt>,
- # so it's important that you type it out just so.
- # You can only declare one collection in a partial template file.
+ # This will include both records as part of the cache key and updating either of them will
+ # expire the cache.
def cache(name = {}, options = {}, &block)
if controller.respond_to?(:perform_caching) && controller.perform_caching
- safe_concat(fragment_for(cache_fragment_name(name, options), options, &block))
+ name_options = options.slice(:skip_digest, :virtual_path)
+ safe_concat(fragment_for(cache_fragment_name(name, name_options), options, &block))
else
yield
end
@@ -189,55 +201,62 @@ module ActionView
end
# This helper returns the name of a cache key for a given fragment cache
- # call. By supplying +skip_digest:+ true to cache, the digestion of cache
+ # call. By supplying <tt>skip_digest: true</tt> to cache, the digestion of cache
# fragments can be manually bypassed. This is useful when cache fragments
# cannot be manually expired unless you know the exact key which is the
# case when using memcached.
#
# The digest will be generated using +virtual_path:+ if it is provided.
#
- def cache_fragment_name(name = {}, skip_digest: nil, virtual_path: nil)
+ def cache_fragment_name(name = {}, skip_digest: nil, virtual_path: nil, digest_path: nil)
if skip_digest
name
else
- fragment_name_with_digest(name, virtual_path)
+ fragment_name_with_digest(name, virtual_path, digest_path)
end
end
- # Given a key (as described in ActionController::Caching::Fragments.expire_fragment),
- # returns a key suitable for use in reading, writing, or expiring a
- # cached fragment. All keys are prefixed with <tt>views/</tt> and uses
- # ActiveSupport::Cache.expand_cache_key for the expansion.
- def fragment_cache_key(key)
- ActiveSupport::Cache.expand_cache_key(key.is_a?(Hash) ? url_for(key).split("://").last : key, :views)
+ def digest_path_from_virtual(virtual_path) # :nodoc:
+ digest = Digestor.digest(name: virtual_path, finder: lookup_context, dependencies: view_cache_dependencies)
+
+ if digest.present?
+ "#{virtual_path}:#{digest}"
+ else
+ virtual_path
+ end
end
private
- def fragment_name_with_digest(name, virtual_path) #:nodoc:
+ def fragment_name_with_digest(name, virtual_path, digest_path)
virtual_path ||= @virtual_path
- if virtual_path
- names = Array(name.is_a?(Hash) ? controller.url_for(name).split("://").last : name)
- digest = Digestor.digest name: virtual_path, finder: lookup_context, dependencies: view_cache_dependencies
- [ *names, digest ]
+ if virtual_path || digest_path
+ name = controller.url_for(name).split("://").last if name.is_a?(Hash)
+
+ digest_path ||= digest_path_from_virtual(virtual_path)
+
+ [ digest_path, name ]
else
name
end
end
- # TODO: Create an object that has caching read/write on it
- def fragment_for(name = {}, options = nil, &block) #:nodoc:
- read_fragment_for(name, options) || write_fragment_for(name, options, &block)
+ def fragment_for(name = {}, options = nil, &block)
+ if content = read_fragment_for(name, options)
+ @view_renderer.cache_hits[@virtual_path] = :hit if defined?(@view_renderer)
+ content
+ else
+ @view_renderer.cache_hits[@virtual_path] = :miss if defined?(@view_renderer)
+ write_fragment_for(name, options, &block)
+ 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/capture_helper.rb b/actionview/lib/action_view/helpers/capture_helper.rb
index a67ba580f1..c87c212cc7 100644
--- a/actionview/lib/action_view/helpers/capture_helper.rb
+++ b/actionview/lib/action_view/helpers/capture_helper.rb
@@ -1,16 +1,18 @@
-require 'active_support/core_ext/string/output_safety'
+# frozen_string_literal: true
+
+require "active_support/core_ext/string/output_safety"
module ActionView
# = Action View Capture Helper
- module Helpers
+ module Helpers #:nodoc:
# CaptureHelper exposes methods to let you extract generated markup which
# can be used in other parts of a template or layout file.
#
# It provides a method to capture blocks into variables through capture and
- # a way to capture a block of markup for use in a layout through content_for.
+ # a way to capture a block of markup for use in a layout through {content_for}[rdoc-ref:ActionView::Helpers::CaptureHelper#content_for].
module CaptureHelper
- # The capture method allows you to extract part of a template into a
- # variable. You can then use this variable anywhere in your templates or layout.
+ # The capture method extracts part of a template as a String object.
+ # You can then use this object anywhere in your templates, layout, or helpers.
#
# The capture method can be used in ERB templates...
#
@@ -34,15 +36,19 @@ module ActionView
# </body>
# </html>
#
+ # The return of capture is the string generated by the block. For Example:
+ #
+ # @greeting # => "Welcome to my shiny new web page! The date and time is 2018-09-06 11:09:16 -0500"
+ #
def capture(*args)
value = nil
buffer = with_output_buffer { value = yield(*args) }
- if string = buffer.presence || value and string.is_a?(String)
+ if (string = buffer.presence || value) && string.is_a?(String)
ERB::Util.html_escape string
end
end
- # Calling content_for stores a block of markup in an identifier for later use.
+ # Calling <tt>content_for</tt> stores a block of markup in an identifier for later use.
# In order to access this stored content in other templates, helper modules
# or the layout, you would pass the identifier as an argument to <tt>content_for</tt>.
#
@@ -108,14 +114,14 @@ module ActionView
# That will place +script+ tags for your default set of JavaScript files on the page;
# this technique is useful if you'll only be using these scripts in a few views.
#
- # Note that content_for concatenates (default) the blocks it is given for a particular
+ # Note that <tt>content_for</tt> concatenates (default) the blocks it is given for a particular
# identifier in order. For example:
#
# <% content_for :navigation do %>
# <li><%= link_to 'Home', action: 'index' %></li>
# <% end %>
#
- # And in other place:
+ # And in another place:
#
# <% content_for :navigation do %>
# <li><%= link_to 'Login', action: 'login' %></li>
@@ -125,7 +131,7 @@ module ActionView
#
# <ul><%= content_for :navigation %></ul>
#
- # If the flush parameter is true content_for replaces the blocks it is given. For example:
+ # If the flush parameter is +true+ <tt>content_for</tt> replaces the blocks it is given. For example:
#
# <% content_for :navigation do %>
# <li><%= link_to 'Home', action: 'index' %></li>
@@ -145,7 +151,7 @@ module ActionView
#
# <% content_for :script, javascript_include_tag(:defaults) %>
#
- # WARNING: content_for is ignored in caches. So you shouldn't use it for elements that will be fragment cached.
+ # WARNING: <tt>content_for</tt> is ignored in caches. So you shouldn't use it for elements that will be fragment cached.
def content_for(name, content = nil, options = {}, &block)
if content || block_given?
if block_given?
@@ -172,7 +178,7 @@ module ActionView
result unless content
end
- # content_for? checks whether any content has been captured yet using `content_for`.
+ # <tt>content_for?</tt> checks whether any content has been captured yet using <tt>content_for</tt>.
# Useful to render parts of your layout differently based on what is in your views.
#
# <%# This is the layout %>
diff --git a/actionview/lib/action_view/helpers/controller_helper.rb b/actionview/lib/action_view/helpers/controller_helper.rb
index 3569fba8c6..79cf86c7d1 100644
--- a/actionview/lib/action_view/helpers/controller_helper.rb
+++ b/actionview/lib/action_view/helpers/controller_helper.rb
@@ -1,14 +1,19 @@
-require 'active_support/core_ext/module/attr_internal'
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/attr_internal"
module ActionView
- module Helpers
+ module Helpers #:nodoc:
# This module keeps all methods and behavior in ActionView
# that simply delegates to the controller.
module ControllerHelper #:nodoc:
attr_internal :controller, :request
- delegate :request_forgery_protection_token, :params, :session, :cookies, :response, :headers,
- :flash, :action_name, :controller_name, :controller_path, :to => :controller
+ CONTROLLER_DELEGATES = [:request_forgery_protection_token, :params,
+ :session, :cookies, :response, :headers, :flash, :action_name,
+ :controller_name, :controller_path]
+
+ delegate(*CONTROLLER_DELEGATES, to: :controller)
def assign_controller(controller)
if @_controller = controller
@@ -21,6 +26,11 @@ module ActionView
def logger
controller.logger if controller.respond_to?(:logger)
end
+
+ def respond_to?(method_name, include_private = false)
+ return controller.respond_to?(method_name) if CONTROLLER_DELEGATES.include?(method_name.to_sym)
+ super
+ end
end
end
end
diff --git a/actionview/lib/action_view/helpers/csp_helper.rb b/actionview/lib/action_view/helpers/csp_helper.rb
new file mode 100644
index 0000000000..e2e065c218
--- /dev/null
+++ b/actionview/lib/action_view/helpers/csp_helper.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module ActionView
+ # = Action View CSP Helper
+ module Helpers #:nodoc:
+ module CspHelper
+ # Returns a meta tag "csp-nonce" with the per-session nonce value
+ # for allowing inline <script> tags.
+ #
+ # <head>
+ # <%= csp_meta_tag %>
+ # </head>
+ #
+ # This is used by the Rails UJS helper to create dynamically
+ # loaded inline <script> elements.
+ #
+ def csp_meta_tag
+ if content_security_policy?
+ tag("meta", name: "csp-nonce", content: content_security_policy_nonce)
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/csrf_helper.rb b/actionview/lib/action_view/helpers/csrf_helper.rb
index 5af92c4ff2..69c59844a6 100644
--- a/actionview/lib/action_view/helpers/csrf_helper.rb
+++ b/actionview/lib/action_view/helpers/csrf_helper.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
module ActionView
# = Action View CSRF Helper
- module Helpers
+ module Helpers #:nodoc:
module CsrfHelper
# Returns meta tags "csrf-param" and "csrf-token" with the name of the cross-site
# request forgery protection parameter and token, respectively.
@@ -14,14 +16,14 @@ module ActionView
#
# You don't need to use these tags for regular forms as they generate their own hidden fields.
#
- # For AJAX requests other than GETs, extract the "csrf-token" from the meta-tag and send as the
- # "X-CSRF-Token" HTTP header. If you are using jQuery with jquery-rails this happens automatically.
+ # For AJAX requests other than GETs, extract the "csrf-token" from the meta-tag and send as the
+ # "X-CSRF-Token" HTTP header. If you are using rails-ujs this happens automatically.
#
def csrf_meta_tags
if protect_against_forgery?
[
- tag('meta', :name => 'csrf-param', :content => request_forgery_protection_token),
- tag('meta', :name => 'csrf-token', :content => form_authenticity_token)
+ tag("meta", name: "csrf-param", content: request_forgery_protection_token),
+ tag("meta", name: "csrf-token", content: form_authenticity_token)
].join("\n").html_safe
end
end
diff --git a/actionview/lib/action_view/helpers/date_helper.rb b/actionview/lib/action_view/helpers/date_helper.rb
index 9c8edc69a9..9d5e5eaba3 100644
--- a/actionview/lib/action_view/helpers/date_helper.rb
+++ b/actionview/lib/action_view/helpers/date_helper.rb
@@ -1,12 +1,15 @@
-require 'date'
-require 'action_view/helpers/tag_helper'
-require 'active_support/core_ext/array/extract_options'
-require 'active_support/core_ext/date/conversions'
-require 'active_support/core_ext/hash/slice'
-require 'active_support/core_ext/object/with_options'
+# frozen_string_literal: true
+
+require "date"
+require "action_view/helpers/tag_helper"
+require "active_support/core_ext/array/extract_options"
+require "active_support/core_ext/date/conversions"
+require "active_support/core_ext/hash/slice"
+require "active_support/core_ext/object/acts_like"
+require "active_support/core_ext/object/with_options"
module ActionView
- module Helpers
+ module Helpers #:nodoc:
# = Action View Date Helpers
#
# The Date Helper primarily creates select/option tags for different kinds of dates and times or date and time
@@ -70,7 +73,7 @@ module ActionView
# distance_of_time_in_words(Time.now, Time.now) # => less than a minute
#
# With the <tt>scope</tt> option, you can define a custom scope for Rails
- # to lookup the translation.
+ # to look up the translation.
#
# For example you can define the following in your locale (e.g. en.yml).
#
@@ -94,66 +97,62 @@ module ActionView
scope: :'datetime.distance_in_words'
}.merge!(options)
- from_time = from_time.to_time if from_time.respond_to?(:to_time)
- to_time = to_time.to_time if to_time.respond_to?(:to_time)
+ from_time = normalize_distance_of_time_argument_to_time(from_time)
+ to_time = normalize_distance_of_time_argument_to_time(to_time)
from_time, to_time = to_time, from_time if from_time > to_time
- distance_in_minutes = ((to_time - from_time)/60.0).round
+ distance_in_minutes = ((to_time - from_time) / 60.0).round
distance_in_seconds = (to_time - from_time).round
- I18n.with_options :locale => options[:locale], :scope => options[:scope] do |locale|
+ I18n.with_options locale: options[:locale], scope: options[:scope] do |locale|
case distance_in_minutes
- when 0..1
- return distance_in_minutes == 0 ?
- locale.t(:less_than_x_minutes, :count => 1) :
- locale.t(:x_minutes, :count => distance_in_minutes) unless options[:include_seconds]
-
- case distance_in_seconds
- when 0..4 then locale.t :less_than_x_seconds, :count => 5
- when 5..9 then locale.t :less_than_x_seconds, :count => 10
- when 10..19 then locale.t :less_than_x_seconds, :count => 20
- when 20..39 then locale.t :half_a_minute
- when 40..59 then locale.t :less_than_x_minutes, :count => 1
- else locale.t :x_minutes, :count => 1
- end
-
- when 2...45 then locale.t :x_minutes, :count => distance_in_minutes
- when 45...90 then locale.t :about_x_hours, :count => 1
+ when 0..1
+ return distance_in_minutes == 0 ?
+ locale.t(:less_than_x_minutes, count: 1) :
+ locale.t(:x_minutes, count: distance_in_minutes) unless options[:include_seconds]
+
+ case distance_in_seconds
+ when 0..4 then locale.t :less_than_x_seconds, count: 5
+ when 5..9 then locale.t :less_than_x_seconds, count: 10
+ when 10..19 then locale.t :less_than_x_seconds, count: 20
+ when 20..39 then locale.t :half_a_minute
+ when 40..59 then locale.t :less_than_x_minutes, count: 1
+ else locale.t :x_minutes, count: 1
+ end
+
+ when 2...45 then locale.t :x_minutes, count: distance_in_minutes
+ when 45...90 then locale.t :about_x_hours, count: 1
# 90 mins up to 24 hours
- when 90...1440 then locale.t :about_x_hours, :count => (distance_in_minutes.to_f / 60.0).round
+ when 90...1440 then locale.t :about_x_hours, count: (distance_in_minutes.to_f / 60.0).round
# 24 hours up to 42 hours
- when 1440...2520 then locale.t :x_days, :count => 1
+ when 1440...2520 then locale.t :x_days, count: 1
# 42 hours up to 30 days
- when 2520...43200 then locale.t :x_days, :count => (distance_in_minutes.to_f / 1440.0).round
+ when 2520...43200 then locale.t :x_days, count: (distance_in_minutes.to_f / 1440.0).round
# 30 days up to 60 days
- when 43200...86400 then locale.t :about_x_months, :count => (distance_in_minutes.to_f / 43200.0).round
+ when 43200...86400 then locale.t :about_x_months, count: (distance_in_minutes.to_f / 43200.0).round
# 60 days up to 365 days
- when 86400...525600 then locale.t :x_months, :count => (distance_in_minutes.to_f / 43200.0).round
+ when 86400...525600 then locale.t :x_months, count: (distance_in_minutes.to_f / 43200.0).round
+ else
+ from_year = from_time.year
+ from_year += 1 if from_time.month >= 3
+ to_year = to_time.year
+ to_year -= 1 if to_time.month < 3
+ leap_years = (from_year > to_year) ? 0 : (from_year..to_year).count { |x| Date.leap?(x) }
+ minute_offset_for_leap_year = leap_years * 1440
+ # Discount the leap year days when calculating year distance.
+ # e.g. if there are 20 leap year days between 2 dates having the same day
+ # and month then the based on 365 days calculation
+ # the distance in years will come out to over 80 years when in written
+ # English it would read better as about 80 years.
+ minutes_with_offset = distance_in_minutes - minute_offset_for_leap_year
+ remainder = (minutes_with_offset % MINUTES_IN_YEAR)
+ distance_in_years = (minutes_with_offset.div MINUTES_IN_YEAR)
+ if remainder < MINUTES_IN_QUARTER_YEAR
+ locale.t(:about_x_years, count: distance_in_years)
+ elsif remainder < MINUTES_IN_THREE_QUARTERS_YEAR
+ locale.t(:over_x_years, count: distance_in_years)
else
- if from_time.acts_like?(:time) && to_time.acts_like?(:time)
- fyear = from_time.year
- fyear += 1 if from_time.month >= 3
- tyear = to_time.year
- tyear -= 1 if to_time.month < 3
- leap_years = (fyear > tyear) ? 0 : (fyear..tyear).count{|x| Date.leap?(x)}
- minute_offset_for_leap_year = leap_years * 1440
- # Discount the leap year days when calculating year distance.
- # e.g. if there are 20 leap year days between 2 dates having the same day
- # and month then the based on 365 days calculation
- # the distance in years will come out to over 80 years when in written
- # English it would read better as about 80 years.
- minutes_with_offset = distance_in_minutes - minute_offset_for_leap_year
- else
- minutes_with_offset = distance_in_minutes
- end
- remainder = (minutes_with_offset % MINUTES_IN_YEAR)
- distance_in_years = (minutes_with_offset.div MINUTES_IN_YEAR)
- if remainder < MINUTES_IN_QUARTER_YEAR
- locale.t(:about_x_years, :count => distance_in_years)
- elsif remainder < MINUTES_IN_THREE_QUARTERS_YEAR
- locale.t(:over_x_years, :count => distance_in_years)
- else
- locale.t(:almost_x_years, :count => distance_in_years + 1)
- end
+ locale.t(:almost_x_years, count: distance_in_years + 1)
+ end
end
end
end
@@ -206,6 +205,7 @@ module ActionView
# * <tt>:end_year</tt> - Set the end year for the year select. Default is <tt>Date.today.year + 5</tt> if
# you are creating new record. While editing existing record, <tt>:end_year</tt> defaults to
# the current selected year plus 5.
+ # * <tt>:year_format</tt> - Set format of years for year select. Lambda should be passed.
# * <tt>:discard_day</tt> - Set to true if you don't want to show a day select. This includes the day
# as a hidden field instead of showing a select field. Also note that this implicitly sets the day to be the
# first of the given month in order to not create invalid dates like 31 February.
@@ -219,15 +219,18 @@ module ActionView
# the respective locale (e.g. [:year, :month, :day] in the en locale that ships with Rails).
# * <tt>:include_blank</tt> - Include a blank option in every select field so it's possible to set empty
# dates.
- # * <tt>:default</tt> - Set a default date if the affected date isn't set or is nil.
+ # * <tt>:default</tt> - Set a default date if the affected date isn't set or is +nil+.
# * <tt>:selected</tt> - Set a date that overrides the actual value.
# * <tt>:disabled</tt> - Set to true if you want show the select fields as disabled.
# * <tt>:prompt</tt> - Set to true (for a generic prompt), a prompt string or a hash of prompt strings
# for <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>, <tt>:hour</tt>, <tt>:minute</tt> and <tt>:second</tt>.
# Setting this option prepends a select option with a generic prompt (Day, Month, Year, Hour, Minute, Seconds)
# or the given prompt string.
- # * <tt>:with_css_classes</tt> - Set to true if you want assign different styles for 'select' tags. This option
- # automatically set classes 'year', 'month', 'day', 'hour', 'minute' and 'second' for your 'select' tags.
+ # * <tt>:with_css_classes</tt> - Set to true or a hash of strings. Use true if you want to assign generic styles for
+ # select tags. This automatically set classes 'year', 'month', 'day', 'hour', 'minute' and 'second'. A hash of
+ # strings for <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>, <tt>:hour</tt>, <tt>:minute</tt>, <tt>:second</tt>
+ # will extend the select type with the given value. Use +html_options+ to modify every select tag in the set.
+ # * <tt>:use_hidden</tt> - Set to true if you only want to generate hidden input tags.
#
# If anything is passed in the +html_options+ hash it will be applied to every select tag in the set.
#
@@ -263,7 +266,7 @@ module ActionView
# date_select("article", "written_on", default: 3.days.from_now)
#
# # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute
- # # which is set in the form with todays date, regardless of the value in the Active Record object.
+ # # which is set in the form with today's date, regardless of the value in the Active Record object.
# date_select("article", "written_on", selected: Date.today)
#
# # Generates a date select that when POSTed is stored in the credit_card variable, in the bill_due attribute
@@ -273,6 +276,9 @@ module ActionView
# # Generates a date select with custom prompts.
# date_select("article", "written_on", prompt: { day: 'Select day', month: 'Select month', year: 'Select year' })
#
+ # # Generates a date select with custom year format.
+ # date_select("article", "written_on", year_format: ->(year) { "Heisei #{year - 1988}" })
+ #
# The selects are prepared for multi-parameter assignment to an Active Record object.
#
# Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that
@@ -299,16 +305,16 @@ module ActionView
# # the sunrise attribute.
# time_select("article", "start_time", include_seconds: true)
#
- # # You can set the <tt>:minute_step</tt> to 15 which will give you: 00, 15, 30 and 45.
- # time_select 'game', 'game_time', {minute_step: 15}
+ # # You can set the <tt>:minute_step</tt> to 15 which will give you: 00, 15, 30, and 45.
+ # time_select 'game', 'game_time', { minute_step: 15 }
#
# # Creates a time select tag with a custom prompt. Use <tt>prompt: true</tt> for generic prompts.
- # time_select("article", "written_on", prompt: {hour: 'Choose hour', minute: 'Choose minute', second: 'Choose seconds'})
- # time_select("article", "written_on", prompt: {hour: true}) # generic prompt for hours
+ # time_select("article", "written_on", prompt: { hour: 'Choose hour', minute: 'Choose minute', second: 'Choose seconds' })
+ # time_select("article", "written_on", prompt: { hour: true }) # generic prompt for hours
# time_select("article", "written_on", prompt: true) # generic prompts for all
#
# # You can set :ampm option to true which will show the hours as: 12 PM, 01 AM .. 11 PM.
- # time_select 'game', 'game_time', {ampm: true}
+ # time_select 'game', 'game_time', { ampm: true }
#
# The selects are prepared for multi-parameter assignment to an Active Record object.
#
@@ -344,8 +350,8 @@ module ActionView
# datetime_select("article", "written_on", discard_type: true)
#
# # Generates a datetime select with a custom prompt. Use <tt>prompt: true</tt> for generic prompts.
- # datetime_select("article", "written_on", prompt: {day: 'Choose day', month: 'Choose month', year: 'Choose year'})
- # datetime_select("article", "written_on", prompt: {hour: true}) # generic prompt for hours
+ # datetime_select("article", "written_on", prompt: { day: 'Choose day', month: 'Choose month', year: 'Choose year' })
+ # datetime_select("article", "written_on", prompt: { hour: true }) # generic prompt for hours
# datetime_select("article", "written_on", prompt: true) # generic prompts for all
#
# The selects are prepared for multi-parameter assignment to an Active Record object.
@@ -395,8 +401,8 @@ module ActionView
# select_datetime(my_date_time, prefix: 'payday')
#
# # Generates a datetime select with a custom prompt. Use <tt>prompt: true</tt> for generic prompts.
- # select_datetime(my_date_time, prompt: {day: 'Choose day', month: 'Choose month', year: 'Choose year'})
- # select_datetime(my_date_time, prompt: {hour: true}) # generic prompt for hours
+ # select_datetime(my_date_time, prompt: { day: 'Choose day', month: 'Choose month', year: 'Choose year' })
+ # select_datetime(my_date_time, prompt: { hour: true }) # generic prompt for hours
# select_datetime(my_date_time, prompt: true) # generic prompts for all
def select_datetime(datetime = Time.current, options = {}, html_options = {})
DateTimeSelector.new(datetime, options, html_options).select_datetime
@@ -434,8 +440,8 @@ module ActionView
# select_date(my_date, prefix: 'payday')
#
# # Generates a date select with a custom prompt. Use <tt>prompt: true</tt> for generic prompts.
- # select_date(my_date, prompt: {day: 'Choose day', month: 'Choose month', year: 'Choose year'})
- # select_date(my_date, prompt: {hour: true}) # generic prompt for hours
+ # select_date(my_date, prompt: { day: 'Choose day', month: 'Choose month', year: 'Choose year' })
+ # select_date(my_date, prompt: { hour: true }) # generic prompt for hours
# select_date(my_date, prompt: true) # generic prompts for all
def select_date(date = Date.current, options = {}, html_options = {})
DateTimeSelector.new(date, options, html_options).select_date
@@ -474,8 +480,8 @@ module ActionView
# select_time(my_time, start_hour: 2, end_hour: 14)
#
# # Generates a time select with a custom prompt. Use <tt>:prompt</tt> to true for generic prompts.
- # select_time(my_time, prompt: {day: 'Choose day', month: 'Choose month', year: 'Choose year'})
- # select_time(my_time, prompt: {hour: true}) # generic prompt for hours
+ # select_time(my_time, prompt: { day: 'Choose day', month: 'Choose month', year: 'Choose year' })
+ # select_time(my_time, prompt: { hour: true }) # generic prompt for hours
# select_time(my_time, prompt: true) # generic prompts for all
def select_time(datetime = Time.current, options = {}, html_options = {})
DateTimeSelector.new(datetime, options, html_options).select_time
@@ -666,8 +672,6 @@ module ActionView
# <time datetime="2010-11-04T17:55:45+01:00">November 04, 2010 17:55</time>
# time_tag Date.yesterday, 'Yesterday' # =>
# <time datetime="2010-11-03">Yesterday</time>
- # time_tag Date.today, pubdate: true # =>
- # <time datetime="2010-11-04" pubdate="pubdate">November 04, 2010</time>
# time_tag Date.today, datetime: Date.today.strftime('%G-W%V') # =>
# <time datetime="2010-W44">November 04, 2010</time>
#
@@ -678,19 +682,30 @@ module ActionView
def time_tag(date_or_time, *args, &block)
options = args.extract_options!
format = options.delete(:format) || :long
- content = args.first || I18n.l(date_or_time, :format => format)
- datetime = date_or_time.acts_like?(:time) ? date_or_time.xmlschema : date_or_time.iso8601
+ content = args.first || I18n.l(date_or_time, format: format)
- content_tag(:time, content, options.reverse_merge(:datetime => datetime), &block)
+ content_tag("time", content, options.reverse_merge(datetime: date_or_time.iso8601), &block)
end
+
+ private
+
+ def normalize_distance_of_time_argument_to_time(value)
+ if value.is_a?(Numeric)
+ Time.at(value)
+ elsif value.respond_to?(:to_time)
+ value.to_time
+ else
+ raise ArgumentError, "#{value.inspect} can't be converted to a Time value"
+ end
+ end
end
class DateTimeSelector #:nodoc:
include ActionView::Helpers::TagHelper
- DEFAULT_PREFIX = 'date'.freeze
+ DEFAULT_PREFIX = "date"
POSITION = {
- :year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6
+ year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6
}.freeze
AMPM_TRANSLATION = Hash[
@@ -706,8 +721,8 @@ module ActionView
@options = options.dup
@html_options = html_options.dup
@datetime = datetime
- @options[:datetime_separator] ||= ' &mdash; '
- @options[:time_separator] ||= ' : '
+ @options[:datetime_separator] ||= " &mdash; "
+ @options[:time_separator] ||= " : "
end
def select_datetime
@@ -777,7 +792,7 @@ module ActionView
if @options[:use_hidden] || @options[:discard_minute]
build_hidden(:minute, min)
else
- build_options_and_select(:minute, min, :step => @options[:minute_step])
+ build_options_and_select(:minute, min, step: @options[:minute_step])
end
end
@@ -797,7 +812,7 @@ module ActionView
if @options[:use_hidden] || @options[:discard_day]
build_hidden(:day, day || 1)
else
- build_options_and_select(:day, day, :start => 1, :end => 31, :leading_zeros => false, :use_two_digit_numbers => @options[:use_two_digit_numbers])
+ build_options_and_select(:day, day, start: 1, end: 31, leading_zeros: false, use_two_digit_numbers: @options[:use_two_digit_numbers])
end
end
@@ -807,9 +822,9 @@ module ActionView
else
month_options = []
1.upto(12) do |month_number|
- options = { :value => month_number }
+ options = { value: month_number }
options[:selected] = "selected" if month == month_number
- month_options << content_tag(:option, month_name(month_number), options) + "\n"
+ month_options << content_tag("option", month_name(month_number), options) + "\n"
end
build_select(:month, month_options.join)
end
@@ -817,7 +832,7 @@ module ActionView
def select_year
if !@datetime || @datetime == 0
- val = '1'
+ val = "1"
middle_year = Date.today.year
else
val = middle_year = year
@@ -837,14 +852,19 @@ module ActionView
raise ArgumentError, "There are too many years options to be built. Are you sure you haven't mistyped something? You can provide the :max_years_allowed parameter."
end
- build_options_and_select(:year, val, options)
+ build_select(:year, build_year_options(val, options))
end
end
private
%w( sec min hour day month year ).each do |method|
define_method(method) do
- @datetime.kind_of?(Numeric) ? @datetime : @datetime.send(method) if @datetime
+ case @datetime
+ when Hash then @datetime[method.to_sym]
+ when Numeric then @datetime
+ when nil then nil
+ else @datetime.send(method)
+ end
end
end
@@ -852,12 +872,12 @@ module ActionView
# valid. Otherwise, February 31st or February 29th, 2011 can be selected, which are invalid.
def set_day_if_discarded
if @datetime && @options[:discard_day]
- @datetime = @datetime.change(:day => 1)
+ @datetime = @datetime.change(day: 1)
end
end
# Returns translated month names, but also ensures that a custom month
- # name array has a leading nil element.
+ # name array has a leading +nil+ element.
def month_names
@month_names ||= begin
month_names = @options[:use_month_names] || translated_month_names
@@ -877,7 +897,7 @@ module ActionView
# "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
def translated_month_names
key = @options[:use_short_month] ? :'date.abbr_month_names' : :'date.month_names'
- I18n.translate(key, :locale => @options[:locale])
+ I18n.translate(key, locale: @options[:locale])
end
# Looks up month names by number (1-based):
@@ -905,22 +925,37 @@ module ActionView
if @options[:use_month_numbers]
number
elsif @options[:use_two_digit_numbers]
- '%02d' % number
+ "%02d" % number
elsif @options[:add_month_numbers]
"#{number} - #{month_names[number]}"
elsif format_string = @options[:month_format_string]
- format_string % {number: number, name: month_names[number]}
+ format_string % { number: number, name: month_names[number] }
else
month_names[number]
end
end
+ # Looks up year names by number.
+ #
+ # year_name(1998) # => 1998
+ #
+ # If the <tt>:year_format</tt> option is passed:
+ #
+ # year_name(1998) # => "Heisei 10"
+ def year_name(number)
+ if year_format_lambda = @options[:year_format]
+ year_format_lambda.call(number)
+ else
+ number
+ end
+ end
+
def date_order
@date_order ||= @options[:order] || translated_date_order
end
def translated_date_order
- date_order = I18n.translate(:'date.order', :locale => @options[:locale], :default => [])
+ date_order = I18n.translate(:'date.order', locale: @options[:locale], default: [])
date_order = date_order.map(&:to_sym)
forbidden_elements = date_order - [:year, :month, :day]
@@ -967,11 +1002,39 @@ module ActionView
select_options = []
start.step(stop, step) do |i|
value = leading_zeros ? sprintf("%02d", i) : i
- tag_options = { :value => value }
+ tag_options = { value: value }
tag_options[:selected] = "selected" if selected == i
text = options[:use_two_digit_numbers] ? sprintf("%02d", i) : value
text = options[:ampm] ? AMPM_TRANSLATION[i] : text
- select_options << content_tag(:option, text, tag_options)
+ select_options << content_tag("option", text, tag_options)
+ end
+
+ (select_options.join("\n") + "\n").html_safe
+ end
+
+ # Build select option HTML for year.
+ # If <tt>year_format</tt> option is not passed
+ # build_year_options(1998, start: 1998, end: 2000)
+ # => "<option value="1998" selected="selected">1998</option>
+ # <option value="1999">1999</option>
+ # <option value="2000">2000</option>"
+ #
+ # If <tt>year_format</tt> option is passed
+ # build_year_options(1998, start: 1998, end: 2000, year_format: ->year { "Heisei #{ year - 1988 }" })
+ # => "<option value="1998" selected="selected">Heisei 10</option>
+ # <option value="1999">Heisei 11</option>
+ # <option value="2000">Heisei 12</option>"
+ def build_year_options(selected, options = {})
+ start = options.delete(:start)
+ stop = options.delete(:end)
+ step = options.delete(:step)
+
+ select_options = []
+ start.step(stop, step) do |value|
+ tag_options = { value: value }
+ tag_options[:selected] = "selected" if selected == value
+ text = year_name(value)
+ select_options << content_tag("option", text, tag_options)
end
(select_options.join("\n") + "\n").html_safe
@@ -984,35 +1047,51 @@ module ActionView
# </select>"
def build_select(type, select_options_as_html)
select_options = {
- :id => input_id_from_type(type),
- :name => input_name_from_type(type)
+ id: input_id_from_type(type),
+ name: input_name_from_type(type)
}.merge!(@html_options)
- select_options[:disabled] = 'disabled' if @options[:disabled]
- select_options[:class] = [select_options[:class], type].compact.join(' ') if @options[:with_css_classes]
+ select_options[:disabled] = "disabled" if @options[:disabled]
+ select_options[:class] = css_class_attribute(type, select_options[:class], @options[:with_css_classes]) if @options[:with_css_classes]
- select_html = "\n"
- select_html << content_tag(:option, '', :value => '') + "\n" if @options[:include_blank]
+ select_html = +"\n"
+ select_html << content_tag("option", "", value: "") + "\n" if @options[:include_blank]
select_html << prompt_option_tag(type, @options[:prompt]) + "\n" if @options[:prompt]
select_html << select_options_as_html
- (content_tag(:select, select_html.html_safe, select_options) + "\n").html_safe
+ (content_tag("select", select_html.html_safe, select_options) + "\n").html_safe
+ end
+
+ # Builds the css class value for the select element
+ # css_class_attribute(:year, 'date optional', { year: 'my-year' })
+ # => "date optional my-year"
+ def css_class_attribute(type, html_options_class, options) # :nodoc:
+ css_class = \
+ case options
+ when Hash
+ options[type.to_sym]
+ else
+ type
+ end
+
+ [html_options_class, css_class].compact.join(" ")
end
# Builds a prompt option tag with supplied options or from default options.
# prompt_option_tag(:month, prompt: 'Select month')
# => "<option value="">Select month</option>"
def prompt_option_tag(type, options)
- prompt = case options
+ prompt = \
+ case options
when Hash
- default_options = {:year => false, :month => false, :day => false, :hour => false, :minute => false, :second => false}
+ default_options = { year: false, month: false, day: false, hour: false, minute: false, second: false }
default_options.merge!(options)[type.to_sym]
when String
options
else
- I18n.translate(:"datetime.prompts.#{type}", :locale => @options[:locale])
- end
+ I18n.translate(:"datetime.prompts.#{type}", locale: @options[:locale])
+ end
- prompt ? content_tag(:option, prompt, :value => '') : ''
+ prompt ? content_tag("option", prompt, value: "") : ""
end
# Builds hidden input tag for date part and value.
@@ -1020,12 +1099,12 @@ module ActionView
# => "<input id="post_written_on_1i" name="post[written_on(1i)]" type="hidden" value="2008" />"
def build_hidden(type, value)
select_options = {
- :type => "hidden",
- :id => input_id_from_type(type),
- :name => input_name_from_type(type),
- :value => value
+ type: "hidden",
+ id: input_id_from_type(type),
+ name: input_name_from_type(type),
+ value: value
}.merge!(@html_options.slice(:disabled))
- select_options[:disabled] = 'disabled' if @options[:disabled]
+ select_options[:disabled] = "disabled" if @options[:disabled]
tag(:input, select_options) + "\n".html_safe
end
@@ -1036,7 +1115,7 @@ module ActionView
prefix = @options[:prefix] || ActionView::Helpers::DateTimeSelector::DEFAULT_PREFIX
prefix += "[#{@options[:index]}]" if @options.has_key?(:index)
- field_name = @options[:field_name] || type
+ field_name = @options[:field_name] || type.to_s
if @options[:include_position]
field_name += "(#{ActionView::Helpers::DateTimeSelector::POSITION[type]}i)"
end
@@ -1047,8 +1126,8 @@ module ActionView
# Returns the id attribute for the input tag.
# => "post_written_on_1i"
def input_id_from_type(type)
- id = input_name_from_type(type).gsub(/([\[\(])|(\]\[)/, '_').gsub(/[\]\)]/, '')
- id = @options[:namespace] + '_' + id if @options[:namespace]
+ id = input_name_from_type(type).gsub(/([\[\(])|(\]\[)/, "_").gsub(/[\]\)]/, "")
+ id = @options[:namespace] + "_" + id if @options[:namespace]
id
end
@@ -1056,7 +1135,7 @@ module ActionView
# Given an ordering of datetime components, create the selection HTML
# and join them with their appropriate separators.
def build_selects_from_types(order)
- select = ''
+ select = +""
first_visible = order.find { |type| !@options[:"discard_#{type}"] }
order.reverse_each do |type|
separator = separator(type) unless type == first_visible # don't add before first visible field
@@ -1070,12 +1149,12 @@ module ActionView
return "" if @options[:use_hidden]
case type
- when :year, :month, :day
- @options[:"discard_#{type}"] ? "" : @options[:date_separator]
- when :hour
- (@options[:discard_year] && @options[:discard_day]) ? "" : @options[:datetime_separator]
- when :minute, :second
- @options[:"discard_#{type}"] ? "" : @options[:time_separator]
+ when :year, :month, :day
+ @options[:"discard_#{type}"] ? "" : @options[:date_separator]
+ when :hour
+ (@options[:discard_year] && @options[:discard_day]) ? "" : @options[:datetime_separator]
+ when :minute, :second
+ @options[:"discard_#{type}"] ? "" : @options[:time_separator]
end
end
end
diff --git a/actionview/lib/action_view/helpers/debug_helper.rb b/actionview/lib/action_view/helpers/debug_helper.rb
index e9dccbad1c..88ceba414b 100644
--- a/actionview/lib/action_view/helpers/debug_helper.rb
+++ b/actionview/lib/action_view/helpers/debug_helper.rb
@@ -1,10 +1,11 @@
+# frozen_string_literal: true
+
module ActionView
# = Action View Debug Helper
#
# Provides a set of methods for making it easier to debug Rails objects.
- module Helpers
+ module Helpers #:nodoc:
module DebugHelper
-
include TagHelper
# Returns a YAML representation of +object+ wrapped with <pre> and </pre>.
@@ -23,12 +24,12 @@ module ActionView
# created_at:
# </pre>
def debug(object)
- Marshal::dump(object)
+ Marshal.dump(object)
object = ERB::Util.html_escape(object.to_yaml)
- content_tag(:pre, object, :class => "debug_dump")
+ content_tag(:pre, object, class: "debug_dump")
rescue # errors from Marshal or YAML
# Object couldn't be dumped, perhaps because of singleton methods -- this is the fallback
- content_tag(:code, object.inspect, :class => "debug_dump")
+ content_tag(:code, object.inspect, class: "debug_dump")
end
end
end
diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb
index 3a9acafaa2..c2caa77afb 100644
--- a/actionview/lib/action_view/helpers/form_helper.rb
+++ b/actionview/lib/action_view/helpers/form_helper.rb
@@ -1,23 +1,25 @@
-require 'cgi'
-require 'action_view/helpers/date_helper'
-require 'action_view/helpers/tag_helper'
-require 'action_view/helpers/form_tag_helper'
-require 'action_view/helpers/active_model_helper'
-require 'action_view/model_naming'
-require 'action_view/record_identifier'
-require 'active_support/core_ext/module/attribute_accessors'
-require 'active_support/core_ext/hash/slice'
-require 'active_support/core_ext/string/output_safety'
-require 'active_support/core_ext/string/inflections'
+# frozen_string_literal: true
+
+require "cgi"
+require "action_view/helpers/date_helper"
+require "action_view/helpers/tag_helper"
+require "action_view/helpers/form_tag_helper"
+require "action_view/helpers/active_model_helper"
+require "action_view/model_naming"
+require "action_view/record_identifier"
+require "active_support/core_ext/module/attribute_accessors"
+require "active_support/core_ext/hash/slice"
+require "active_support/core_ext/string/output_safety"
+require "active_support/core_ext/string/inflections"
module ActionView
# = Action View Form Helpers
- module Helpers
+ module Helpers #:nodoc:
# Form helpers are designed to make working with resources much easier
# compared to using vanilla HTML.
#
# Typically, a form designed to create or update a resource reflects the
- # identity of the resource in several ways: (i) the url that the form is
+ # identity of the resource in several ways: (i) the URL that the form is
# sent to (the form element's +action+ attribute) should result in a request
# being routed to the appropriate controller action (with the appropriate <tt>:id</tt>
# parameter in the case of an existing resource), (ii) input fields should
@@ -164,7 +166,7 @@ module ActionView
# So for example you may use a named route directly. When the model is
# represented by a string or symbol, as in the example above, if the
# <tt>:url</tt> option is not specified, by default the form will be
- # sent back to the current url (We will describe below an alternative
+ # sent back to the current URL (We will describe below an alternative
# resource-oriented usage of +form_for+ in which the URL does not need
# to be specified explicitly).
# * <tt>:namespace</tt> - A namespace for your form to ensure uniqueness of
@@ -201,9 +203,9 @@ module ActionView
# <%= f.submit %>
# <% end %>
#
- # This also works for the methods in FormOptionHelper and DateHelper that
+ # This also works for the methods in FormOptionsHelper and DateHelper that
# are designed to work with an object as base, like
- # FormOptionHelper#collection_select and DateHelper#datetime_select.
+ # FormOptionsHelper#collection_select and DateHelper#datetime_select.
#
# === #form_for with a model object
#
@@ -416,13 +418,13 @@ module ActionView
#
# To set an authenticity token you need to pass an <tt>:authenticity_token</tt> parameter
#
- # <%= form_for @invoice, url: external_url, authenticity_token: 'external_token' do |f|
+ # <%= form_for @invoice, url: external_url, authenticity_token: 'external_token' do |f| %>
# ...
# <% end %>
#
# If you don't want to an authenticity token field be rendered at all just pass <tt>false</tt>:
#
- # <%= form_for @invoice, url: external_url, authenticity_token: false do |f|
+ # <%= form_for @invoice, url: external_url, authenticity_token: false do |f| %>
# ...
# <% end %>
def form_for(record, options = {}, &block)
@@ -467,13 +469,300 @@ module ActionView
)
options[:url] ||= if options.key?(:format)
- polymorphic_path(record, format: options.delete(:format))
- else
- polymorphic_path(record, {})
- end
+ polymorphic_path(record, format: options.delete(:format))
+ else
+ polymorphic_path(record, {})
+ end
end
private :apply_form_for_options!
+ mattr_accessor :form_with_generates_remote_forms, default: true
+
+ mattr_accessor :form_with_generates_ids, default: false
+
+ # Creates a form tag based on mixing URLs, scopes, or models.
+ #
+ # # Using just a URL:
+ # <%= 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.
+ #
+ # === Resource-oriented style
+ #
+ # In many of the examples just shown, the +:model+ passed to +form_with+
+ # is a _resource_. It corresponds to a set of RESTful routes, most likely
+ # defined via +resources+ in <tt>config/routes.rb</tt>.
+ #
+ # So when passing such a model record, Rails infers the URL and method.
+ #
+ # <%= form_with model: @post do |form| %>
+ # ...
+ # <% end %>
+ #
+ # is then equivalent to something like:
+ #
+ # <%= form_with scope: :post, url: post_path(@post), method: :patch do |form| %>
+ # ...
+ # <% end %>
+ #
+ # And for a new record
+ #
+ # <%= form_with model: Post.new do |form| %>
+ # ...
+ # <% end %>
+ #
+ # is equivalent to something like:
+ #
+ # <%= form_with scope: :post, url: posts_path do |form| %>
+ # ...
+ # <% end %>
+ #
+ # ==== +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>:namespace</tt> - A namespace for your form to ensure uniqueness of
+ # id attributes on form elements. The namespace attribute will be prefixed
+ # with underscore on the generated HTML id.
+ # * <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 unobtrusive XHRs.
+ # Disable remote submits with <tt>local: true</tt>.
+ # * <tt>:skip_enforcing_utf8</tt> - If set to true, a hidden input with name
+ # utf8 is not output.
+ # * <tt>:builder</tt> - Override the object used to build the form.
+ # * <tt>:id</tt> - Optional HTML id attribute.
+ # * <tt>:class</tt> - Optional HTML class attribute.
+ # * <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>.
+ #
+ # === 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 FormOptionsHelper and DateHelper designed
+ # to work with an object as a base, like
+ # FormOptionsHelper#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] = !form_with_generates_ids
+
+ 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.
@@ -531,9 +820,9 @@ module ActionView
# _class_ of the model object, e.g. if <tt>@person.permission</tt>, is
# of class +Permission+, the field will still be named <tt>permission[admin]</tt>.
#
- # Note: This also works for the methods in FormOptionHelper and
+ # Note: This also works for the methods in FormOptionsHelper and
# DateHelper that are designed to work with an object as base, like
- # FormOptionHelper#collection_select and DateHelper#datetime_select.
+ # FormOptionsHelper#collection_select and DateHelper#datetime_select.
#
# === Nested Attributes Examples
#
@@ -720,6 +1009,63 @@ 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>.
+ #
+ # === 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 FormOptionsHelper and DateHelper designed
+ # to work with an object as a base, like
+ # FormOptionsHelper#collection_select and DateHelper#datetime_select.
+ def fields(scope = nil, model: nil, **options, &block)
+ options[:allow_method_names_outside_object] = true
+ options[:skip_default_ids] = !form_with_generates_ids
+
+ 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.
@@ -765,7 +1111,7 @@ module ActionView
# # => <label for="post_privacy_public">Public Post</label>
#
# label(:post, :terms) do
- # 'Accept <a href="/terms">Terms</a>.'.html_safe
+ # raw('Accept <a href="/terms">Terms</a>.')
# end
# # => <label for="post_terms">Accept <a href="/terms">Terms</a>.</label>
def label(object_name, method, content_or_options = nil, options = nil, &block)
@@ -784,6 +1130,9 @@ module ActionView
# text_field(:post, :title, class: "create_input")
# # => <input type="text" id="post_title" name="post[title]" value="#{@post.title}" class="create_input" />
#
+ # text_field(:post, :title, maxlength: 30, class: "title_input")
+ # # => <input type="text" id="post_title" name="post[title]" maxlength="30" size="30" value="#{@post.title}" class="title_input" />
+ #
# text_field(:session, :user, onchange: "if ($('#session_user').val() === 'admin') { alert('Your login cannot be admin!'); }")
# # => <input type="text" id="session_user" name="session[user]" value="#{@session.user}" onchange="if ($('#session_user').val() === 'admin') { alert('Your login cannot be admin!'); }"/>
#
@@ -849,8 +1198,8 @@ module ActionView
# file_field(:user, :avatar)
# # => <input type="file" id="user_avatar" name="user[avatar]" />
#
- # file_field(:post, :image, :multiple => true)
- # # => <input type="file" id="post_image" name="post[image]" multiple="true" />
+ # file_field(:post, :image, multiple: true)
+ # # => <input type="file" id="post_image" name="post[image][]" multiple="multiple" />
#
# file_field(:post, :attached, accept: 'text/html')
# # => <input accept="text/html" type="file" id="post_attached" name="post[attached]" />
@@ -860,26 +1209,8 @@ module ActionView
#
# file_field(:attachment, :file, class: 'file_input')
# # => <input type="file" id="attachment_file" name="attachment[file]" class="file_input" />
- #
- # ==== Gotcha
- #
- # The HTML specification says that when a file field is empty, web browsers
- # do not send any value to the server. Unfortunately this introduces a
- # gotcha: if a +User+ model has an +avatar+ field, and no file is selected,
- # then the +avatar+ parameter is empty. Thus, any mass-assignment idiom like
- #
- # @user.update(params[:user])
- #
- # wouldn't update the +avatar+ field.
- #
- # To prevent this, the helper generates an auxiliary hidden field before
- # every file field. The hidden field has the same name as the file one and
- # a blank value.
- #
- # In case you don't want the helper to generate this hidden field you can
- # specify the <tt>include_hidden: false</tt> option.
def file_field(object_name, method, options = {})
- Tags::FileField.new(object_name, method, self, options).render
+ Tags::FileField.new(object_name, method, self, convert_direct_upload_option_to_url(options.dup)).render
end
# Returns a textarea opening and closing tag set tailored for accessing a specified attribute (identified by +method+)
@@ -983,6 +1314,7 @@ module ActionView
# # => <input type="radio" id="post_category_rails" name="post[category]" value="rails" checked="checked" />
# # <input type="radio" id="post_category_java" name="post[category]" value="java" />
#
+ # # Let's say that @user.receive_newsletter returns "no":
# radio_button("user", "receive_newsletter", "yes")
# radio_button("user", "receive_newsletter", "no")
# # => <input type="radio" id="user_receive_newsletter_yes" name="user[receive_newsletter]" value="yes" />
@@ -1038,7 +1370,7 @@ module ActionView
# date_field("user", "born_on")
# # => <input id="user_born_on" name="user[born_on]" type="date" />
#
- # The default value is generated by trying to call "to_date"
+ # The default value is generated by trying to call +strftime+ with "%Y-%m-%d"
# on the object's value, which makes it behave as expected for instances
# of DateTime and ActiveSupport::TimeWithZone. You can still override that
# by passing the "value" option explicitly, e.g.
@@ -1066,7 +1398,7 @@ module ActionView
# Returns a text_field of type "time".
#
# The default value is generated by trying to call +strftime+ with "%T.%L"
- # on the objects's value. It is still possible to override that
+ # on the object's value. It is still possible to override that
# by passing the "value" option.
#
# === Options
@@ -1092,38 +1424,9 @@ module ActionView
Tags::TimeField.new(object_name, method, self, options).render
end
- # Returns a text_field of type "datetime".
- #
- # datetime_field("user", "born_on")
- # # => <input id="user_born_on" name="user[born_on]" type="datetime" />
- #
- # The default value is generated by trying to call +strftime+ with "%Y-%m-%dT%T.%L%z"
- # on the object's value, which makes it behave as expected for instances
- # of DateTime and ActiveSupport::TimeWithZone.
- #
- # @user.born_on = Date.new(1984, 1, 12)
- # datetime_field("user", "born_on")
- # # => <input id="user_born_on" name="user[born_on]" type="datetime" value="1984-01-12T00:00:00.000+0000" />
- #
- # You can create values for the "min" and "max" attributes by passing
- # instances of Date or Time to the options hash.
- #
- # datetime_field("user", "born_on", min: Date.today)
- # # => <input id="user_born_on" name="user[born_on]" type="datetime" min="2014-05-20T00:00:00.000+0000" />
- #
- # Alternatively, you can pass a String formatted as an ISO8601 datetime
- # with UTC offset as the values for "min" and "max."
- #
- # datetime_field("user", "born_on", min: "2014-05-20T00:00:00+0000")
- # # => <input id="user_born_on" name="user[born_on]" type="datetime" min="2014-05-20T00:00:00.000+0000" />
- #
- def datetime_field(object_name, method, options = {})
- Tags::DatetimeField.new(object_name, method, self, options).render
- end
-
# Returns a text_field of type "datetime-local".
#
- # datetime_local_field("user", "born_on")
+ # datetime_field("user", "born_on")
# # => <input id="user_born_on" name="user[born_on]" type="datetime-local" />
#
# The default value is generated by trying to call +strftime+ with "%Y-%m-%dT%T"
@@ -1131,25 +1434,27 @@ module ActionView
# of DateTime and ActiveSupport::TimeWithZone.
#
# @user.born_on = Date.new(1984, 1, 12)
- # datetime_local_field("user", "born_on")
+ # datetime_field("user", "born_on")
# # => <input id="user_born_on" name="user[born_on]" type="datetime-local" value="1984-01-12T00:00:00" />
#
# You can create values for the "min" and "max" attributes by passing
# instances of Date or Time to the options hash.
#
- # datetime_local_field("user", "born_on", min: Date.today)
+ # datetime_field("user", "born_on", min: Date.today)
# # => <input id="user_born_on" name="user[born_on]" type="datetime-local" min="2014-05-20T00:00:00.000" />
#
# Alternatively, you can pass a String formatted as an ISO8601 datetime as
# the values for "min" and "max."
#
- # datetime_local_field("user", "born_on", min: "2014-05-20T00:00:00")
+ # datetime_field("user", "born_on", min: "2014-05-20T00:00:00")
# # => <input id="user_born_on" name="user[born_on]" type="datetime-local" min="2014-05-20T00:00:00.000" />
#
- def datetime_local_field(object_name, method, options = {})
+ def datetime_field(object_name, method, options = {})
Tags::DatetimeLocalField.new(object_name, method, self, options).render
end
+ alias datetime_local_field datetime_field
+
# Returns a text_field of type "month".
#
# month_field("user", "born_on")
@@ -1219,6 +1524,34 @@ module ActionView
end
private
+ def html_options_for_form_with(url_for_options = nil, model = nil, html: {}, local: !form_with_generates_remote_forms,
+ skip_enforcing_utf8: nil, **options)
+ html_options = options.slice(:id, :class, :multipart, :method, :data).merge(html)
+ html_options[:method] ||= :patch if model.respond_to?(:persisted?) && model.persisted?
+ html_options[:enforce_utf8] = !skip_enforcing_utf8 unless skip_enforcing_utf8.nil?
+
+ 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
+
+ html_options[:authenticity_token] = options.delete(:authenticity_token)
+
+ if !local && html_options[:authenticity_token].blank?
+ html_options[:authenticity_token] = embed_authenticity_token_in_remote_forms
+ end
+
+ if html_options[:authenticity_token] == true
+ # Include the default authenticity_token, which is only generated when it's 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
@@ -1227,7 +1560,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
@@ -1253,7 +1586,7 @@ module ActionView
# In the above block, a +FormBuilder+ object is yielded as the
# +person_form+ variable. This allows you to generate the +text_field+
# and +check_box+ fields by specifying their eponymous methods, which
- # modify the underlying template and associates the +@person+ model object
+ # modify the underlying template and associates the <tt>@person</tt> model object
# with the form.
#
# The +FormBuilder+ object can be thought of as serving as a proxy for the
@@ -1292,14 +1625,15 @@ module ActionView
include ModelNaming
# The methods which wrap a form helper call.
- class_attribute :field_helpers
- self.field_helpers = [:fields_for, :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,
- :time_field, :datetime_field, :datetime_local_field,
- :month_field, :week_field, :url_field, :email_field,
- :number_field, :range_field]
+ class_attribute :field_helpers, default: [
+ :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,
+ :time_field, :datetime_field, :datetime_local_field,
+ :month_field, :week_field, :url_field, :email_field,
+ :number_field, :range_field
+ ]
attr_accessor :object_name, :object, :options
@@ -1315,7 +1649,7 @@ module ActionView
end
def self._to_partial_path
- @_to_partial_path ||= name.demodulize.underscore.sub!(/_builder$/, '')
+ @_to_partial_path ||= name.demodulize.underscore.sub!(/_builder$/, "")
end
def to_partial_path
@@ -1329,19 +1663,24 @@ 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) : {}
+ @default_html_options = @default_options.except(: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}") and object.respond_to?(:to_param)
+ if (object ||= @template.instance_variable_get("@#{Regexp.last_match.pre_match}")) && object.respond_to?(:to_param)
@auto_index = object.to_param
else
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(
@@ -1410,9 +1749,9 @@ module ActionView
# _class_ of the model object, e.g. if <tt>@person.permission</tt>, is
# of class +Permission+, the field will still be named <tt>permission[admin]</tt>.
#
- # Note: This also works for the methods in FormOptionHelper and
+ # Note: This also works for the methods in FormOptionsHelper and
# DateHelper that are designed to work with an object as base, like
- # FormOptionHelper#collection_select and DateHelper#datetime_select.
+ # FormOptionsHelper#collection_select and DateHelper#datetime_select.
#
# === Nested Attributes Examples
#
@@ -1610,19 +1949,37 @@ 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
- record_name = index ? "#{object_name}[#{index}][#{record_name}]" : "#{object_name}[#{record_name}]"
+ record_name = if index
+ "#{object_name}[#{index}][#{record_name}]"
+ elsif record_name.to_s.end_with?("[]")
+ record_name = record_name.to_s.sub(/(.*)\[\]$/, "[\\1][#{record_object.id}]")
+ "#{object_name}#{record_name}"
+ else
+ "#{object_name}[#{record_name}]"
+ end
fields_options[:child_index] = index
@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] = !FormHelper.form_with_generates_ids
+
+ 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.
@@ -1668,7 +2025,7 @@ module ActionView
# # => <label for="post_privacy_public">Public Post</label>
#
# label(:terms) do
- # 'Accept <a href="/terms">Terms</a>.'.html_safe
+ # raw('Accept <a href="/terms">Terms</a>.')
# end
# # => <label for="post_terms">Accept <a href="/terms">Terms</a>.</label>
def label(method, text = nil, options = {}, &block)
@@ -1749,7 +2106,7 @@ module ActionView
# # => <input type="radio" id="post_category_rails" name="post[category]" value="rails" checked="checked" />
# # <input type="radio" id="post_category_java" name="post[category]" value="java" />
#
- # # Let's say that @user.category returns "no":
+ # # Let's say that @user.receive_newsletter returns "no":
# radio_button("receive_newsletter", "yes")
# radio_button("receive_newsletter", "no")
# # => <input type="radio" id="user_receive_newsletter_yes" name="user[receive_newsletter]" value="yes" />
@@ -1826,11 +2183,11 @@ module ActionView
# <%= f.submit %>
# <% end %>
#
- # In the example above, if @post is a new record, it will use "Create Post" as
- # submit button label, otherwise, it uses "Update Post".
+ # In the example above, if <tt>@post</tt> is a new record, it will use "Create Post" as
+ # submit button label; otherwise, it uses "Update Post".
#
- # Those labels can be customized using I18n, under the helpers.submit key and accept
- # the %{model} as translation interpolation:
+ # Those labels can be customized using I18n under the +helpers.submit+ key and using
+ # <tt>%{model}</tt> for translation interpolation:
#
# en:
# helpers:
@@ -1838,7 +2195,7 @@ module ActionView
# create: "Create a %{model}"
# update: "Confirm changes to %{model}"
#
- # It also searches for a key specific for the given object:
+ # It also searches for a key specific to the given object:
#
# en:
# helpers:
@@ -1846,7 +2203,7 @@ module ActionView
# post:
# create: "Add %{model}"
#
- def submit(value=nil, options={})
+ def submit(value = nil, options = {})
value, options = nil, value if value.is_a?(Hash)
value ||= submit_default_value
@template.submit_tag(value, options)
@@ -1859,11 +2216,11 @@ module ActionView
# <%= f.button %>
# <% end %>
#
- # In the example above, if @post is a new record, it will use "Create Post" as
- # button label, otherwise, it uses "Update Post".
+ # In the example above, if <tt>@post</tt> is a new record, it will use "Create Post" as
+ # button label; otherwise, it uses "Update Post".
#
- # Those labels can be customized using I18n, under the helpers.submit key
- # (the same as submit helper) and accept the %{model} as translation interpolation:
+ # Those labels can be customized using I18n under the +helpers.submit+ key
+ # (the same as submit helper) and using <tt>%{model}</tt> for translation interpolation:
#
# en:
# helpers:
@@ -1871,7 +2228,7 @@ module ActionView
# create: "Create a %{model}"
# update: "Confirm changes to %{model}"
#
- # It also searches for a key specific for the given object:
+ # It also searches for a key specific to the given object:
#
# en:
# helpers:
@@ -1880,7 +2237,7 @@ module ActionView
# create: "Add %{model}"
#
# ==== Examples
- # button("Create a post")
+ # button("Create post")
# # => <button name='button' type='submit'>Create post</button>
#
# button do
@@ -1896,7 +2253,7 @@ module ActionView
@template.button_tag(value, options, &block)
end
- def emitted_hidden_id?
+ def emitted_hidden_id? # :nodoc:
@emitted_hidden_id ||= nil
end
@@ -1916,7 +2273,12 @@ module ActionView
end
defaults = []
- defaults << :"helpers.submit.#{object_name}.#{key}"
+ # Object is a model and it is not overwritten by as and scope option.
+ if object.respond_to?(:model_name) && object_name.to_s == model.downcase
+ defaults << :"helpers.submit.#{object.model_name.i18n_key}.#{key}"
+ else
+ defaults << :"helpers.submit.#{object_name}.#{key}"
+ end
defaults << :"helpers.submit.#{key}"
defaults << "#{key.to_s.humanize} #{model}"
@@ -1971,12 +2333,16 @@ 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
ActiveSupport.on_load(:action_view) do
- cattr_accessor(:default_form_builder, instance_writer: false, instance_reader: false) do
- ::ActionView::Helpers::FormBuilder
- end
+ cattr_accessor :default_form_builder, instance_writer: false, instance_reader: false, default: ::ActionView::Helpers::FormBuilder
end
end
diff --git a/actionview/lib/action_view/helpers/form_options_helper.rb b/actionview/lib/action_view/helpers/form_options_helper.rb
index 8e729b3c39..ebdd96f570 100644
--- a/actionview/lib/action_view/helpers/form_options_helper.rb
+++ b/actionview/lib/action_view/helpers/form_options_helper.rb
@@ -1,20 +1,22 @@
-require 'cgi'
-require 'erb'
-require 'action_view/helpers/form_helper'
-require 'active_support/core_ext/string/output_safety'
-require 'active_support/core_ext/array/extract_options'
-require 'active_support/core_ext/array/wrap'
+# frozen_string_literal: true
+
+require "cgi"
+require "erb"
+require "action_view/helpers/form_helper"
+require "active_support/core_ext/string/output_safety"
+require "active_support/core_ext/array/extract_options"
+require "active_support/core_ext/array/wrap"
module ActionView
# = Action View Form Option Helpers
- module Helpers
+ module Helpers #:nodoc:
# Provides a number of methods for turning different kinds of containers into a set of option tags.
#
# The <tt>collection_select</tt>, <tt>select</tt> and <tt>time_zone_select</tt> methods take an <tt>options</tt> parameter, a hash:
#
# * <tt>:include_blank</tt> - set to true or a prompt string if the first option element of the select element is a blank. Useful if there is not a default value required for the select element.
#
- # select("post", "category", Post::CATEGORIES, {include_blank: true})
+ # select("post", "category", Post::CATEGORIES, { include_blank: true })
#
# could become:
#
@@ -28,7 +30,7 @@ module ActionView
#
# Example with <tt>@post.person_id => 2</tt>:
#
- # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {include_blank: 'None'})
+ # select("post", "person_id", Person.all.collect { |p| [ p.name, p.id ] }, { include_blank: 'None' })
#
# could become:
#
@@ -41,7 +43,7 @@ module ActionView
#
# * <tt>:prompt</tt> - set to true or a prompt string. When the select element doesn't have a value yet, this prepends an option with a generic prompt -- "Please select" -- or the given prompt string.
#
- # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {prompt: 'Select Person'})
+ # select("post", "person_id", Person.all.collect { |p| [ p.name, p.id ] }, { prompt: 'Select Person' })
#
# could become:
#
@@ -67,7 +69,7 @@ module ActionView
#
# * <tt>:disabled</tt> - can be a single value or an array of values that will be disabled options in the final output.
#
- # select("post", "category", Post::CATEGORIES, {disabled: 'restricted'})
+ # select("post", "category", Post::CATEGORIES, { disabled: 'restricted' })
#
# could become:
#
@@ -80,7 +82,7 @@ module ActionView
#
# When used with the <tt>collection_select</tt> helper, <tt>:disabled</tt> can also be a Proc that identifies those options that should be disabled.
#
- # collection_select(:post, :category_id, Category.all, :id, :name, {disabled: -> (category) { category.archived? }})
+ # collection_select(:post, :category_id, Category.all, :id, :name, { disabled: -> (category) { category.archived? } })
#
# If the categories "2008 stuff" and "Christmas" return true when the method <tt>archived?</tt> is called, this would return:
# <select name="post[category_id]" id="post_category_id">
@@ -105,7 +107,7 @@ module ActionView
#
# For example:
#
- # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, { include_blank: true })
+ # select("post", "person_id", Person.all.collect { |p| [ p.name, p.id ] }, { include_blank: true })
#
# would become:
#
@@ -212,9 +214,13 @@ module ActionView
# * +method+ - The attribute of +object+ corresponding to the select tag
# * +collection+ - An array of objects representing the <tt><optgroup></tt> tags.
# * +group_method+ - The name of a method which, when called on a member of +collection+, returns an
- # array of child objects representing the <tt><option></tt> tags.
+ # array of child objects representing the <tt><option></tt> tags. It can also be any object that responds
+ # to +call+, such as a +proc+, that will be called for each member of the +collection+ to retrieve the
+ # value.
# * +group_label_method+ - The name of a method which, when called on a member of +collection+, returns a
- # string to be used as the +label+ attribute for its <tt><optgroup></tt> tag.
+ # string to be used as the +label+ attribute for its <tt><optgroup></tt> tag. It can also be any object
+ # that responds to +call+, such as a +proc+, that will be called for each member of the +collection+ to
+ # retrieve the label.
# * +option_key_method+ - The name of a method which, when called on a child object of a member of
# +collection+, returns a value to be used as the +value+ attribute for its <tt><option></tt> tag.
# * +option_value_method+ - The name of a method which, when called on a child object of a member of
@@ -268,25 +274,26 @@ module ActionView
# for more information.)
#
# You can also supply an array of ActiveSupport::TimeZone objects
- # as +priority_zones+, so that they will be listed above the rest of the
- # (long) list. (You can use ActiveSupport::TimeZone.us_zones as a convenience
- # for obtaining a list of the US time zones, or a Regexp to select the zones
- # of your choice)
+ # as +priority_zones+ so that they will be listed above the rest of the
+ # (long) list. You can use ActiveSupport::TimeZone.us_zones for a list
+ # of US time zones, ActiveSupport::TimeZone.country_zones(country_code)
+ # for another country's time zones, or a Regexp to select the zones of
+ # your choice.
#
# Finally, this method supports a <tt>:default</tt> option, which selects
# a default ActiveSupport::TimeZone if the object's time zone is +nil+.
#
- # time_zone_select( "user", "time_zone", nil, include_blank: true)
+ # time_zone_select("user", "time_zone", nil, include_blank: true)
#
- # time_zone_select( "user", "time_zone", nil, default: "Pacific Time (US & Canada)" )
+ # time_zone_select("user", "time_zone", nil, default: "Pacific Time (US & Canada)")
#
- # time_zone_select( "user", 'time_zone', ActiveSupport::TimeZone.us_zones, default: "Pacific Time (US & Canada)")
+ # time_zone_select("user", 'time_zone', ActiveSupport::TimeZone.us_zones, default: "Pacific Time (US & Canada)")
#
- # time_zone_select( "user", 'time_zone', [ ActiveSupport::TimeZone['Alaska'], ActiveSupport::TimeZone['Hawaii'] ])
+ # time_zone_select("user", 'time_zone', [ ActiveSupport::TimeZone['Alaska'], ActiveSupport::TimeZone['Hawaii'] ])
#
- # time_zone_select( "user", 'time_zone', /Australia/)
+ # time_zone_select("user", 'time_zone', /Australia/)
#
- # time_zone_select( "user", "time_zone", ActiveSupport::TimeZone.all.sort, model: ActiveSupport::TimeZone)
+ # time_zone_select("user", "time_zone", ActiveSupport::TimeZone.all.sort, model: ActiveSupport::TimeZone)
def time_zone_select(object, method, priority_zones = nil, options = {}, html_options = {})
Tags::TimeZoneSelect.new(object, method, self, priority_zones, options, html_options).render
end
@@ -316,12 +323,12 @@ module ActionView
#
# You can optionally provide HTML attributes as the last element of the array.
#
- # options_for_select([ "Denmark", ["USA", {class: 'bold'}], "Sweden" ], ["USA", "Sweden"])
+ # options_for_select([ "Denmark", ["USA", { class: 'bold' }], "Sweden" ], ["USA", "Sweden"])
# # => <option value="Denmark">Denmark</option>
# # => <option value="USA" class="bold" selected="selected">USA</option>
# # => <option value="Sweden" selected="selected">Sweden</option>
#
- # options_for_select([["Dollar", "$", {class: "bold"}], ["Kroner", "DKK", {onclick: "alert('HI');"}]])
+ # options_for_select([["Dollar", "$", { class: "bold" }], ["Kroner", "DKK", { onclick: "alert('HI');" }]])
# # => <option value="$" class="bold">Dollar</option>
# # => <option value="DKK" onclick="alert('HI');">Kroner</option>
#
@@ -362,7 +369,7 @@ module ActionView
html_attributes[:disabled] ||= disabled && option_value_selected?(value, disabled)
html_attributes[:value] = value
- content_tag_string(:option, text, html_attributes)
+ tag_builder.content_tag_string(:option, text, html_attributes)
end.join("\n").html_safe
end
@@ -454,9 +461,9 @@ module ActionView
def option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, selected_key = nil)
collection.map do |group|
option_tags = options_from_collection_for_select(
- group.send(group_method), option_key_method, option_value_method, selected_key)
+ value_for_collection(group, group_method), option_key_method, option_value_method, selected_key)
- content_tag(:optgroup, option_tags, label: group.send(group_label_method))
+ content_tag("optgroup", option_tags, label: value_for_collection(group, group_label_method))
end.join.html_safe
end
@@ -528,7 +535,7 @@ module ActionView
body = "".html_safe
if prompt
- body.safe_concat content_tag(:option, prompt_text(prompt), value: "")
+ body.safe_concat content_tag("option", prompt_text(prompt), value: "")
end
grouped_options.each do |container|
@@ -541,14 +548,14 @@ module ActionView
end
html_attributes = { label: label }.merge!(html_attributes)
- body.safe_concat content_tag(:optgroup, options_for_select(container, selected_key), html_attributes)
+ body.safe_concat content_tag("optgroup", options_for_select(container, selected_key), html_attributes)
end
body
end
# Returns a string of option tags for pretty much any time zone in the
- # world. Supply a ActiveSupport::TimeZone name as +selected+ to have it
+ # world. Supply an ActiveSupport::TimeZone name as +selected+ to have it
# marked as the selected option tag. You can also supply an array of
# ActiveSupport::TimeZone objects as +priority_zones+, so that they will
# be listed above the rest of the (long) list. (You can use
@@ -556,7 +563,7 @@ module ActionView
# of the US time zones, or a Regexp to select the zones of your choice)
#
# The +selected+ parameter must be either +nil+, or a string that names
- # a ActiveSupport::TimeZone.
+ # an ActiveSupport::TimeZone.
#
# By default, +model+ is the ActiveSupport::TimeZone constant (which can
# be obtained in Active Record as a value object). The only requirement
@@ -577,7 +584,7 @@ module ActionView
end
zone_options.safe_concat options_for_select(convert_zones[priority_zones], selected)
- zone_options.safe_concat content_tag(:option, '-------------', value: '', disabled: true)
+ zone_options.safe_concat content_tag("option", "-------------", value: "", disabled: true)
zone_options.safe_concat "\n"
zones = zones - priority_zones
@@ -644,6 +651,24 @@ module ActionView
# collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) do |b|
# b.label(:"data-value" => b.value) { b.radio_button + b.text }
# end
+ #
+ # ==== Gotcha
+ #
+ # The HTML specification says when nothing is select on a collection of radio buttons
+ # web browsers do not send any value to server.
+ # Unfortunately this introduces a gotcha:
+ # if a +User+ model has a +category_id+ field and in the form no category is selected, no +category_id+ parameter is sent. So,
+ # any strong parameters idiom like:
+ #
+ # params.require(:user).permit(...)
+ #
+ # will raise an error since no <tt>{user: ...}</tt> will be present.
+ #
+ # To prevent this the helper generates an auxiliary hidden field before
+ # every collection of radio buttons. The hidden field has the same name as collection radio button and blank value.
+ #
+ # In case if you don't want the helper to generate this hidden field you can specify
+ # <tt>include_hidden: false</tt> option.
def collection_radio_buttons(object, method, collection, value_method, text_method, options = {}, html_options = {}, &block)
Tags::CollectionRadioButtons.new(object, method, self, collection, value_method, text_method, options, html_options).render(&block)
end
@@ -769,7 +794,7 @@ module ActionView
def extract_values_from_collection(collection, value_method, selected)
if selected.is_a?(Proc)
collection.map do |element|
- element.send(value_method) if selected.call(element)
+ public_or_deprecated_send(element, value_method) if selected.call(element)
end.compact
else
selected
@@ -777,11 +802,19 @@ module ActionView
end
def value_for_collection(item, value)
- value.respond_to?(:call) ? value.call(item) : item.send(value)
+ value.respond_to?(:call) ? value.call(item) : public_or_deprecated_send(item, value)
+ end
+
+ def public_or_deprecated_send(item, value)
+ item.public_send(value)
+ rescue NoMethodError
+ raise unless item.respond_to?(value, true) && !item.respond_to?(value)
+ ActiveSupport::Deprecation.warn "Using private methods from view helpers is deprecated (calling private #{item.class}##{value})"
+ item.send(value)
end
def prompt_text(prompt)
- prompt.kind_of?(String) ? prompt : I18n.translate('helpers.select.prompt', default: 'Please select')
+ prompt.kind_of?(String) ? prompt : I18n.translate("helpers.select.prompt", default: "Please select")
end
end
@@ -795,7 +828,7 @@ module ActionView
#
# Please refer to the documentation of the base helper for details.
def select(method, choices = nil, options = {}, html_options = {}, &block)
- @template.select(@object_name, method, choices, objectify_options(options), @default_options.merge(html_options), &block)
+ @template.select(@object_name, method, choices, objectify_options(options), @default_html_options.merge(html_options), &block)
end
# Wraps ActionView::Helpers::FormOptionsHelper#collection_select for form builders:
@@ -807,7 +840,7 @@ module ActionView
#
# Please refer to the documentation of the base helper for details.
def collection_select(method, collection, value_method, text_method, options = {}, html_options = {})
- @template.collection_select(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options))
+ @template.collection_select(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_html_options.merge(html_options))
end
# Wraps ActionView::Helpers::FormOptionsHelper#grouped_collection_select for form builders:
@@ -819,7 +852,7 @@ module ActionView
#
# Please refer to the documentation of the base helper for details.
def grouped_collection_select(method, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, html_options = {})
- @template.grouped_collection_select(@object_name, method, collection, group_method, group_label_method, option_key_method, option_value_method, objectify_options(options), @default_options.merge(html_options))
+ @template.grouped_collection_select(@object_name, method, collection, group_method, group_label_method, option_key_method, option_value_method, objectify_options(options), @default_html_options.merge(html_options))
end
# Wraps ActionView::Helpers::FormOptionsHelper#time_zone_select for form builders:
@@ -831,7 +864,7 @@ module ActionView
#
# Please refer to the documentation of the base helper for details.
def time_zone_select(method, priority_zones = nil, options = {}, html_options = {})
- @template.time_zone_select(@object_name, method, priority_zones, objectify_options(options), @default_options.merge(html_options))
+ @template.time_zone_select(@object_name, method, priority_zones, objectify_options(options), @default_html_options.merge(html_options))
end
# Wraps ActionView::Helpers::FormOptionsHelper#collection_check_boxes for form builders:
@@ -843,7 +876,7 @@ module ActionView
#
# Please refer to the documentation of the base helper for details.
def collection_check_boxes(method, collection, value_method, text_method, options = {}, html_options = {}, &block)
- @template.collection_check_boxes(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options), &block)
+ @template.collection_check_boxes(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_html_options.merge(html_options), &block)
end
# Wraps ActionView::Helpers::FormOptionsHelper#collection_radio_buttons for form builders:
@@ -855,7 +888,7 @@ module ActionView
#
# Please refer to the documentation of the base helper for details.
def collection_radio_buttons(method, collection, value_method, text_method, options = {}, html_options = {}, &block)
- @template.collection_radio_buttons(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options), &block)
+ @template.collection_radio_buttons(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_html_options.merge(html_options), &block)
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 896020bc15..c0996049f0 100644
--- a/actionview/lib/action_view/helpers/form_tag_helper.rb
+++ b/actionview/lib/action_view/helpers/form_tag_helper.rb
@@ -1,11 +1,13 @@
-require 'cgi'
-require 'action_view/helpers/tag_helper'
-require 'active_support/core_ext/string/output_safety'
-require 'active_support/core_ext/module/attribute_accessors'
+# frozen_string_literal: true
+
+require "cgi"
+require "action_view/helpers/tag_helper"
+require "active_support/core_ext/string/output_safety"
+require "active_support/core_ext/module/attribute_accessors"
module ActionView
# = Action View Form Tag Helpers
- module Helpers
+ module Helpers #:nodoc:
# Provides a number of methods for creating form tags that don't rely on an Active Record object assigned to the template like
# FormHelper does. Instead, you provide the names and values manually.
#
@@ -18,9 +20,11 @@ module ActionView
include TextHelper
mattr_accessor :embed_authenticity_token_in_remote_forms
- self.embed_authenticity_token_in_remote_forms = false
+ self.embed_authenticity_token_in_remote_forms = nil
+
+ mattr_accessor :default_enforce_utf8, default: true
- # Starts a form tag that points the action to an url configured with <tt>url_for_options</tt> just like
+ # Starts a form tag that points the action to a url configured with <tt>url_for_options</tt> just like
# ActionController::Base#url_for. The method for the form defaults to POST.
#
# ==== Options
@@ -93,27 +97,27 @@ module ActionView
# select_tag "people", options_from_collection_for_select(@people, "id", "name", "1")
# # <select id="people" name="people"><option value="1" selected="selected">David</option></select>
#
- # select_tag "people", "<option>David</option>".html_safe
+ # select_tag "people", raw("<option>David</option>")
# # => <select id="people" name="people"><option>David</option></select>
#
- # select_tag "count", "<option>1</option><option>2</option><option>3</option><option>4</option>".html_safe
+ # select_tag "count", raw("<option>1</option><option>2</option><option>3</option><option>4</option>")
# # => <select id="count" name="count"><option>1</option><option>2</option>
# # <option>3</option><option>4</option></select>
#
- # select_tag "colors", "<option>Red</option><option>Green</option><option>Blue</option>".html_safe, multiple: true
+ # select_tag "colors", raw("<option>Red</option><option>Green</option><option>Blue</option>"), multiple: true
# # => <select id="colors" multiple="multiple" name="colors[]"><option>Red</option>
# # <option>Green</option><option>Blue</option></select>
#
- # select_tag "locations", "<option>Home</option><option selected='selected'>Work</option><option>Out</option>".html_safe
+ # select_tag "locations", raw("<option>Home</option><option selected='selected'>Work</option><option>Out</option>")
# # => <select id="locations" name="locations"><option>Home</option><option selected='selected'>Work</option>
# # <option>Out</option></select>
#
- # select_tag "access", "<option>Read</option><option>Write</option>".html_safe, multiple: true, class: 'form_input', id: 'unique_id'
+ # select_tag "access", raw("<option>Read</option><option>Write</option>"), multiple: true, class: 'form_input', id: 'unique_id'
# # => <select class="form_input" id="unique_id" multiple="multiple" name="access[]"><option>Read</option>
# # <option>Write</option></select>
#
# select_tag "people", options_from_collection_for_select(@people, "id", "name"), include_blank: true
- # # => <select id="people" name="people"><option value=""></option><option value="1">David</option></select>
+ # # => <select id="people" name="people"><option value="" label=" "></option><option value="1">David</option></select>
#
# select_tag "people", options_from_collection_for_select(@people, "id", "name"), include_blank: "All"
# # => <select id="people" name="people"><option value="">All</option><option value="1">David</option></select>
@@ -121,7 +125,7 @@ module ActionView
# select_tag "people", options_from_collection_for_select(@people, "id", "name"), prompt: "Select something"
# # => <select id="people" name="people"><option value="">Select something</option><option value="1">David</option></select>
#
- # select_tag "destination", "<option>NYC</option><option>Paris</option><option>Rome</option>".html_safe, disabled: true
+ # select_tag "destination", raw("<option>NYC</option><option>Paris</option><option>Rome</option>"), disabled: true
# # => <select disabled="disabled" id="destination" name="destination"><option>NYC</option>
# # <option>Paris</option><option>Rome</option></select>
#
@@ -134,21 +138,23 @@ module ActionView
if options.include?(:include_blank)
include_blank = options.delete(:include_blank)
+ options_for_blank_options_tag = { value: "" }
if include_blank == true
- include_blank = ''
+ include_blank = ""
+ options_for_blank_options_tag[:label] = " "
end
if include_blank
- option_tags = content_tag(:option, include_blank, value: '').safe_concat(option_tags)
+ option_tags = content_tag("option", include_blank, options_for_blank_options_tag).safe_concat(option_tags)
end
end
if prompt = options.delete(:prompt)
- option_tags = content_tag(:option, prompt, value: '').safe_concat(option_tags)
+ option_tags = content_tag("option", prompt, value: "").safe_concat(option_tags)
end
- content_tag :select, option_tags, { "name" => html_name, "id" => sanitize_to_id(name) }.update(options.stringify_keys)
+ content_tag "select", option_tags, { "name" => html_name, "id" => sanitize_to_id(name) }.update(options.stringify_keys)
end
# Creates a standard text field; use these text fields to input smaller chunks of text like a username
@@ -270,7 +276,7 @@ module ActionView
# file_field_tag 'file', accept: 'text/html', class: 'upload', value: 'index.html'
# # => <input accept="text/html" class="upload" id="file" name="file" type="file" value="index.html" />
def file_field_tag(name, options = {})
- text_field_tag(name, nil, options.merge(type: :file))
+ text_field_tag(name, nil, convert_direct_upload_option_to_url(options.merge(type: :file)))
end
# Creates a password field, a masked text field that will hide the users input behind a mask character.
@@ -383,14 +389,14 @@ module ActionView
# * Any other key creates standard HTML options for the tag.
#
# ==== Examples
- # radio_button_tag 'gender', 'male'
- # # => <input id="gender_male" name="gender" type="radio" value="male" />
+ # radio_button_tag 'favorite_color', 'maroon'
+ # # => <input id="favorite_color_maroon" name="favorite_color" type="radio" value="maroon" />
#
# radio_button_tag 'receive_updates', 'no', true
# # => <input checked="checked" id="receive_updates_no" name="receive_updates" type="radio" value="no" />
#
# radio_button_tag 'time_slot', "3:00 p.m.", false, disabled: true
- # # => <input disabled="disabled" id="time_slot_300_pm" name="time_slot" type="radio" value="3:00 p.m." />
+ # # => <input disabled="disabled" id="time_slot_3:00_p.m." name="time_slot" type="radio" value="3:00 p.m." />
#
# radio_button_tag 'color', "green", true, class: "color_input"
# # => <input checked="checked" class="color_input" id="color_green" name="color" type="radio" value="green" />
@@ -414,43 +420,45 @@ module ActionView
# the form 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.
+ # provided by the unobtrusive JavaScript driver. To disable this feature for a single submit tag
+ # pass <tt>:data => { disable_with: false }</tt> Defaults to value attribute.
#
# ==== Examples
# submit_tag
- # # => <input name="commit" type="submit" value="Save changes" />
+ # # => <input name="commit" data-disable-with="Save changes" type="submit" value="Save changes" />
#
# submit_tag "Edit this article"
- # # => <input name="commit" type="submit" value="Edit this article" />
+ # # => <input name="commit" data-disable-with="Edit this article" type="submit" value="Edit this article" />
#
# submit_tag "Save edits", disabled: true
- # # => <input disabled="disabled" name="commit" type="submit" value="Save edits" />
+ # # => <input disabled="disabled" name="commit" data-disable-with="Save edits" type="submit" value="Save edits" />
#
- # submit_tag "Complete sale", data: { disable_with: "Please wait..." }
- # # => <input name="commit" data-disable-with="Please wait..." type="submit" value="Complete sale" />
+ # submit_tag "Complete sale", data: { disable_with: "Submitting..." }
+ # # => <input name="commit" data-disable-with="Submitting..." type="submit" value="Complete sale" />
#
# submit_tag nil, class: "form_submit"
# # => <input class="form_submit" name="commit" type="submit" />
#
# submit_tag "Edit", class: "edit_button"
- # # => <input class="edit_button" name="commit" type="submit" value="Edit" />
+ # # => <input class="edit_button" data-disable-with="Edit" name="commit" type="submit" value="Edit" />
#
# submit_tag "Save", data: { confirm: "Are you sure?" }
- # # => <input name='commit' type='submit' value='Save' data-confirm="Are you sure?" />
+ # # => <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
-
- tag :input, { "type" => "submit", "name" => "commit", "value" => value }.update(options)
+ options = options.deep_stringify_keys
+ tag_options = { "type" => "submit", "name" => "commit", "value" => value }.update(options)
+ 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,
# so this helper will also accept a block. By default, it will create
- # a button tag with type `submit`, if type is not given.
+ # a button tag with type <tt>submit</tt>, if type is not given.
#
# ==== Options
# * <tt>:data</tt> - This option can be used to add custom data attributes.
@@ -502,12 +510,12 @@ module ActionView
options ||= {}
end
- options = { 'name' => 'button', 'type' => 'submit' }.merge!(options.stringify_keys)
+ options = { "name" => "button", "type" => "submit" }.merge!(options.stringify_keys)
if block_given?
content_tag :button, options, &block
else
- content_tag :button, content_or_options || 'Button', options
+ content_tag :button, content_or_options || "Button", options
end
end
@@ -528,22 +536,23 @@ module ActionView
#
# ==== Examples
# image_submit_tag("login.png")
- # # => <input alt="Login" src="/assets/login.png" type="image" />
+ # # => <input src="/assets/login.png" type="image" />
#
# image_submit_tag("purchase.png", disabled: true)
- # # => <input alt="Purchase" disabled="disabled" src="/assets/purchase.png" type="image" />
+ # # => <input disabled="disabled" src="/assets/purchase.png" type="image" />
#
# image_submit_tag("search.png", class: 'search_button', alt: 'Find')
- # # => <input alt="Find" class="search_button" src="/assets/search.png" type="image" />
+ # # => <input class="search_button" src="/assets/search.png" type="image" />
#
# image_submit_tag("agree.png", disabled: true, class: "agree_disagree_button")
- # # => <input alt="Agree" class="agree_disagree_button" disabled="disabled" src="/assets/agree.png" type="image" />
+ # # => <input class="agree_disagree_button" disabled="disabled" src="/assets/agree.png" type="image" />
#
# image_submit_tag("save.png", data: { confirm: "Are you sure?" })
- # # => <input alt="Save" src="/assets/save.png" data-confirm="Are you sure?" type="image" />
+ # # => <input src="/assets/save.png" data-confirm="Are you sure?" type="image" />
def image_submit_tag(source, options = {})
options = options.stringify_keys
- tag :input, { "alt" => image_alt(source), "type" => "image", "src" => path_to_image(source) }.update(options)
+ src = path_to_image(source, skip_pipeline: options.delete("skip_pipeline"))
+ tag :input, { "type" => "image", "src" => src }.update(options)
end
# Creates a field set for grouping HTML form elements.
@@ -568,7 +577,7 @@ module ActionView
# # => <fieldset class="format"><p><input id="name" name="name" type="text" /></p></fieldset>
def field_set_tag(legend = nil, options = nil, &block)
output = tag(:fieldset, options, true)
- output.safe_concat(content_tag(:legend, legend)) unless legend.blank?
+ output.safe_concat(content_tag("legend", legend)) unless legend.blank?
output.concat(capture(&block)) if block_given?
output.safe_concat("</fieldset>")
end
@@ -669,7 +678,7 @@ module ActionView
text_field_tag(name, value, options.merge(type: :time))
end
- # Creates a text field of type "datetime".
+ # Creates a text field of type "datetime-local".
#
# === Options
# * <tt>:min</tt> - The minimum acceptable value.
@@ -677,19 +686,10 @@ module ActionView
# * <tt>:step</tt> - The acceptable value granularity.
# * Otherwise accepts the same options as text_field_tag.
def datetime_field_tag(name, value = nil, options = {})
- text_field_tag(name, value, options.merge(type: :datetime))
+ text_field_tag(name, value, options.merge(type: "datetime-local"))
end
- # Creates a text field of type "datetime-local".
- #
- # === Options
- # * <tt>:min</tt> - The minimum acceptable value.
- # * <tt>:max</tt> - The maximum acceptable value.
- # * <tt>:step</tt> - The acceptable value granularity.
- # * Otherwise accepts the same options as text_field_tag.
- def datetime_local_field_tag(name, value = nil, options = {})
- text_field_tag(name, value, options.merge(type: 'datetime-local'))
- end
+ alias datetime_local_field_tag datetime_field_tag
# Creates a text field of type "month".
#
@@ -848,21 +848,28 @@ module ActionView
def extra_tags_for_form(html_options)
authenticity_token = html_options.delete("authenticity_token")
- method = html_options.delete("method").to_s
+ method = html_options.delete("method").to_s.downcase
- method_tag = case method
- when /^get$/i # must be case-insensitive, but can't use downcase as might be nil
+ method_tag = \
+ case method
+ when "get"
html_options["method"] = "get"
- ''
- when /^post$/i, "", nil
+ ""
+ when "post", ""
html_options["method"] = "post"
- token_tag(authenticity_token)
+ token_tag(authenticity_token, form_options: {
+ action: html_options["action"],
+ method: "post"
+ })
else
html_options["method"] = "post"
- method_tag(method) + token_tag(authenticity_token)
- end
+ method_tag(method) + token_tag(authenticity_token, form_options: {
+ action: html_options["action"],
+ method: method
+ })
+ end
- if html_options.delete("enforce_utf8") { true }
+ if html_options.delete("enforce_utf8") { default_enforce_utf8 }
utf8_enforcer_tag + method_tag
else
method_tag
@@ -882,7 +889,30 @@ module ActionView
# see http://www.w3.org/TR/html4/types.html#type-name
def sanitize_to_id(name)
- name.to_s.delete(']').tr('^-a-zA-Z0-9:.', "_")
+ 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
+
+ def convert_direct_upload_option_to_url(options)
+ if options.delete(:direct_upload) && respond_to?(:rails_direct_uploads_url)
+ options["data-direct-upload-url"] = rails_direct_uploads_url
+ end
+ options
end
end
end
diff --git a/actionview/lib/action_view/helpers/javascript_helper.rb b/actionview/lib/action_view/helpers/javascript_helper.rb
index e237a32cb7..b680cb1bd3 100644
--- a/actionview/lib/action_view/helpers/javascript_helper.rb
+++ b/actionview/lib/action_view/helpers/javascript_helper.rb
@@ -1,11 +1,13 @@
-require 'action_view/helpers/tag_helper'
+# frozen_string_literal: true
+
+require "action_view/helpers/tag_helper"
module ActionView
- module Helpers
+ module Helpers #:nodoc:
module JavaScriptHelper
JS_ESCAPE_MAP = {
'\\' => '\\\\',
- '</' => '<\/',
+ "</" => '<\/',
"\r\n" => '\n',
"\n" => '\n',
"\r" => '\n',
@@ -13,8 +15,8 @@ module ActionView
"'" => "\\'"
}
- JS_ESCAPE_MAP["\342\200\250".force_encoding(Encoding::UTF_8).encode!] = '&#x2028;'
- JS_ESCAPE_MAP["\342\200\251".force_encoding(Encoding::UTF_8).encode!] = '&#x2029;'
+ JS_ESCAPE_MAP[(+"\342\200\250").force_encoding(Encoding::UTF_8).encode!] = "&#x2028;"
+ JS_ESCAPE_MAP[(+"\342\200\251").force_encoding(Encoding::UTF_8).encode!] = "&#x2029;"
# Escapes carriage returns and single and double quotes for JavaScript segments.
#
@@ -23,12 +25,13 @@ module ActionView
#
# $('some_element').replaceWith('<%= j render 'some/element_template' %>');
def escape_javascript(javascript)
- if javascript
- result = javascript.gsub(/(\\|<\/|\r\n|\342\200\250|\342\200\251|[\n\r"'])/u) {|match| JS_ESCAPE_MAP[match] }
- javascript.html_safe? ? result.html_safe : result
+ javascript = javascript.to_s
+ if javascript.empty?
+ result = ""
else
- ''
+ result = javascript.gsub(/(\\|<\/|\r\n|\342\200\250|\342\200\251|[\n\r"'])/u) { |match| JS_ESCAPE_MAP[match] }
end
+ javascript.html_safe? ? result.html_safe : result
end
alias_method :j, :escape_javascript
@@ -47,8 +50,8 @@ module ActionView
# tag.
#
# javascript_tag "alert('All is good')", defer: 'defer'
- #
- # Returns:
+ #
+ # Returns:
# <script defer="defer">
# //<![CDATA[
# alert('All is good')
@@ -61,6 +64,13 @@ module ActionView
# <%= javascript_tag defer: 'defer' do -%>
# alert('All is good')
# <% end -%>
+ #
+ # If you have a content security policy enabled then you can add an automatic
+ # nonce value by passing <tt>nonce: true</tt> as part of +html_options+. Example:
+ #
+ # <%= javascript_tag nonce: true do -%>
+ # alert('All is good')
+ # <% end -%>
def javascript_tag(content_or_options_with_block = nil, html_options = {}, &block)
content =
if block_given?
@@ -70,7 +80,11 @@ module ActionView
content_or_options_with_block
end
- content_tag(:script, javascript_cdata_section(content), html_options)
+ if html_options[:nonce] == true
+ html_options[:nonce] = content_security_policy_nonce
+ end
+
+ content_tag("script", javascript_cdata_section(content), html_options)
end
def javascript_cdata_section(content) #:nodoc:
diff --git a/actionview/lib/action_view/helpers/number_helper.rb b/actionview/lib/action_view/helpers/number_helper.rb
index 13effa592d..35206b7e48 100644
--- a/actionview/lib/action_view/helpers/number_helper.rb
+++ b/actionview/lib/action_view/helpers/number_helper.rb
@@ -1,12 +1,12 @@
+# frozen_string_literal: true
-require 'active_support/core_ext/hash/keys'
-require 'active_support/core_ext/string/output_safety'
-require 'active_support/number_helper'
+require "active_support/core_ext/hash/keys"
+require "active_support/core_ext/string/output_safety"
+require "active_support/number_helper"
module ActionView
# = Action View Number Helpers
module Helpers #:nodoc:
-
# Provides methods for converting numbers into formatted strings.
# Methods are provided for phone numbers, currency, percentage,
# precision, positional notation, file size and pretty printing.
@@ -14,7 +14,6 @@ module ActionView
# Most methods expect a +number+ argument, and will return it
# unchanged if can't be converted into a valid number.
module NumberHelper
-
# Raised when argument +number+ param given to the helpers is invalid and
# the option :raise is set to +true+.
class InvalidNumberError < StandardError
@@ -24,7 +23,7 @@ module ActionView
end
end
- # Formats a +number+ into a US phone number (e.g., (555)
+ # Formats a +number+ into a phone number (US by default e.g., (555)
# 123-9876). You can customize the format in the +options+ hash.
#
# ==== Options
@@ -36,6 +35,8 @@ module ActionView
# end of the generated number.
# * <tt>:country_code</tt> - Sets the country code for the phone
# number.
+ # * <tt>:pattern</tt> - Specifies how the number is divided into three
+ # groups with the custom regexp to override the default format.
# * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when
# the argument is invalid.
#
@@ -53,6 +54,11 @@ module ActionView
#
# number_to_phone(1235551234, country_code: 1, extension: 1343, delimiter: ".")
# # => +1.123.555.1234 x 1343
+ #
+ # number_to_phone(75561234567, pattern: /(\d{1,4})(\d{4})(\d{4})$/, area_code: true)
+ # # => "(755) 6123-4567"
+ # number_to_phone(13312345678, pattern: /(\d{3})(\d{4})(\d{4})$/))
+ # # => "133-1234-5678"
def number_to_phone(number, options = {})
return unless number
options = options.symbolize_keys
@@ -64,6 +70,14 @@ module ActionView
# Formats a +number+ into a currency string (e.g., $13.65). You
# can customize the format in the +options+ hash.
#
+ # The currency unit and number formatting of the current locale will be used
+ # unless otherwise specified in the provided options. No currency conversion
+ # is performed. If the user is given a way to change their locale, they will
+ # also be able to change the relative value of the currency displayed with
+ # this helper. If your application will ever support multiple locales, you
+ # may want to specify a constant <tt>:locale</tt> option or consider
+ # using a library capable of currency conversion.
+ #
# ==== Options
#
# * <tt>:locale</tt> - Sets the locale to be used for formatting
@@ -80,12 +94,15 @@ 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.
# * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when
# the argument is invalid.
+ # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes
+ # insignificant zeros after the decimal separator (defaults to
+ # +false+).
#
# ==== Examples
#
@@ -103,6 +120,8 @@ module ActionView
# # => R$1234567890,50
# number_to_currency(1234567890.50, unit: "R$", separator: ",", delimiter: "", format: "%n %u")
# # => 1234567890,50 R$
+ # number_to_currency(1234567890.50, strip_insignificant_zeros: true)
+ # # => "$1,234,567,890.5"
def number_to_currency(number, options = {})
delegate_number_helper_method(:number_to_currency, number, options)
end
@@ -140,7 +159,7 @@ module ActionView
# number_to_percentage(302.24398923423, precision: 5) # => 302.24399%
# number_to_percentage(1000, locale: :fr) # => 1 000,000%
# number_to_percentage("98a") # => 98a%
- # number_to_percentage(100, format: "%n %") # => 100 %
+ # number_to_percentage(100, format: "%n %") # => 100.000 %
#
# number_to_percentage("98a", raise: true) # => InvalidNumberError
def number_to_percentage(number, options = {})
@@ -159,6 +178,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.
#
@@ -175,6 +197,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)
@@ -249,8 +274,6 @@ module ActionView
# * <tt>:strip_insignificant_zeros</tt> - If +true+ removes
# insignificant zeros after the decimal separator (defaults to
# +true+)
- # * <tt>:prefix</tt> - If +:si+ formats the number using the SI
- # prefix (defaults to :binary)
# * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when
# the argument is invalid.
#
@@ -262,6 +285,8 @@ module ActionView
# number_to_human_size(1234567) # => 1.18 MB
# number_to_human_size(1234567890) # => 1.15 GB
# number_to_human_size(1234567890123) # => 1.12 TB
+ # number_to_human_size(1234567890123456) # => 1.1 PB
+ # number_to_human_size(1234567890123456789) # => 1.07 EB
# number_to_human_size(1234567, precision: 2) # => 1.2 MB
# number_to_human_size(483989, precision: 2) # => 470 KB
# number_to_human_size(1234567, precision: 2, separator: ',') # => 1,2 MB
@@ -379,53 +404,53 @@ module ActionView
private
- def delegate_number_helper_method(method, number, options)
- return unless number
- options = escape_unsafe_options(options.symbolize_keys)
+ def delegate_number_helper_method(method, number, options)
+ return unless number
+ options = escape_unsafe_options(options.symbolize_keys)
- wrap_with_output_safety_handling(number, options.delete(:raise)) {
- ActiveSupport::NumberHelper.public_send(method, number, options)
- }
- end
+ wrap_with_output_safety_handling(number, options.delete(:raise)) {
+ ActiveSupport::NumberHelper.public_send(method, number, options)
+ }
+ end
- def escape_unsafe_options(options)
- options[:format] = ERB::Util.html_escape(options[:format]) if options[:format]
- options[:negative_format] = ERB::Util.html_escape(options[:negative_format]) if options[:negative_format]
- options[:separator] = ERB::Util.html_escape(options[:separator]) if options[:separator]
- options[:delimiter] = ERB::Util.html_escape(options[:delimiter]) if options[:delimiter]
- options[:unit] = ERB::Util.html_escape(options[:unit]) if options[:unit] && !options[:unit].html_safe?
- options[:units] = escape_units(options[:units]) if options[:units] && Hash === options[:units]
- options
- end
+ def escape_unsafe_options(options)
+ options[:format] = ERB::Util.html_escape(options[:format]) if options[:format]
+ options[:negative_format] = ERB::Util.html_escape(options[:negative_format]) if options[:negative_format]
+ options[:separator] = ERB::Util.html_escape(options[:separator]) if options[:separator]
+ options[:delimiter] = ERB::Util.html_escape(options[:delimiter]) if options[:delimiter]
+ options[:unit] = ERB::Util.html_escape(options[:unit]) if options[:unit] && !options[:unit].html_safe?
+ options[:units] = escape_units(options[:units]) if options[:units] && Hash === options[:units]
+ options
+ end
- def escape_units(units)
- Hash[units.map do |k, v|
- [k, ERB::Util.html_escape(v)]
- end]
- end
+ def escape_units(units)
+ Hash[units.map do |k, v|
+ [k, ERB::Util.html_escape(v)]
+ end]
+ end
- def wrap_with_output_safety_handling(number, raise_on_invalid, &block)
- valid_float = valid_float?(number)
- raise InvalidNumberError, number if raise_on_invalid && !valid_float
+ def wrap_with_output_safety_handling(number, raise_on_invalid, &block)
+ valid_float = valid_float?(number)
+ raise InvalidNumberError, number if raise_on_invalid && !valid_float
- formatted_number = yield
+ formatted_number = yield
- if valid_float || number.html_safe?
- formatted_number.html_safe
- else
- formatted_number
+ if valid_float || number.html_safe?
+ formatted_number.html_safe
+ else
+ formatted_number
+ end
end
- end
- def valid_float?(number)
- !parse_float(number, false).nil?
- end
+ def valid_float?(number)
+ !parse_float(number, false).nil?
+ end
- def parse_float(number, raise_error)
- Float(number)
- rescue ArgumentError, TypeError
- raise InvalidNumberError, number if raise_error
- end
+ def parse_float(number, raise_error)
+ Float(number)
+ rescue ArgumentError, TypeError
+ raise InvalidNumberError, number if raise_error
+ end
end
end
end
diff --git a/actionview/lib/action_view/helpers/output_safety_helper.rb b/actionview/lib/action_view/helpers/output_safety_helper.rb
index 1c2a400245..279cde5e76 100644
--- a/actionview/lib/action_view/helpers/output_safety_helper.rb
+++ b/actionview/lib/action_view/helpers/output_safety_helper.rb
@@ -1,4 +1,6 @@
-require 'active_support/core_ext/string/output_safety'
+# frozen_string_literal: true
+
+require "active_support/core_ext/string/output_safety"
module ActionView #:nodoc:
# = Action View Raw Output Helper
@@ -22,17 +24,47 @@ module ActionView #:nodoc:
# the supplied separator, are HTML escaped unless they are HTML
# safe, and the returned string is marked as HTML safe.
#
- # safe_join(["<p>foo</p>".html_safe, "<p>bar</p>"], "<br />")
+ # 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(["<p>foo</p>".html_safe, "<p>bar</p>".html_safe], "<br />".html_safe)
+ # safe_join([raw("<p>foo</p>"), raw("<p>bar</p>")], raw("<br />"))
# # => "<p>foo</p><br /><p>bar</p>"
#
- def safe_join(array, sep=$,)
+ def safe_join(array, sep = $,)
sep = ERB::Util.unwrapped_html_escape(sep)
array.flatten.map! { |i| ERB::Util.unwrapped_html_escape(i) }.join(sep).html_safe
end
+
+ # Converts the array to a comma-separated sentence where the last element is
+ # joined by the connector word. This is the html_safe-aware version of
+ # ActiveSupport's {Array#to_sentence}[http://api.rubyonrails.org/classes/Array.html#method-i-to_sentence].
+ #
+ def to_sentence(array, options = {})
+ options.assert_valid_keys(:words_connector, :two_words_connector, :last_word_connector, :locale)
+
+ default_connectors = {
+ words_connector: ", ",
+ two_words_connector: " and ",
+ last_word_connector: ", and "
+ }
+ if defined?(I18n)
+ i18n_connectors = I18n.translate(:'support.array', locale: options[:locale], default: {})
+ default_connectors.merge!(i18n_connectors)
+ end
+ options = default_connectors.merge!(options)
+
+ case array.length
+ when 0
+ "".html_safe
+ when 1
+ ERB::Util.html_escape(array[0])
+ 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]], nil)
+ end
+ end
end
end
end
diff --git a/actionview/lib/action_view/helpers/record_tag_helper.rb b/actionview/lib/action_view/helpers/record_tag_helper.rb
deleted file mode 100644
index f7ee573035..0000000000
--- a/actionview/lib/action_view/helpers/record_tag_helper.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-module ActionView
- module Helpers
- module RecordTagHelper
- def div_for(*)
- raise NoMethodError, "The `div_for` method has been removed from " \
- "Rails. To continue using it, add the `record_tag_helper` gem to " \
- "your Gemfile:\n" \
- " gem 'record_tag_helper', '~> 1.0'\n" \
- "Consult the Rails upgrade guide for details."
- end
-
- def content_tag_for(*)
- raise NoMethodError, "The `content_tag_for` method has been removed from " \
- "Rails. To continue using it, add the `record_tag_helper` gem to " \
- "your Gemfile:\n" \
- " gem 'record_tag_helper', '~> 1.0'\n" \
- "Consult the Rails upgrade guide for details."
- end
- end
- end
-end
diff --git a/actionview/lib/action_view/helpers/rendering_helper.rb b/actionview/lib/action_view/helpers/rendering_helper.rb
index 827932d8e2..1e12aa2736 100644
--- a/actionview/lib/action_view/helpers/rendering_helper.rb
+++ b/actionview/lib/action_view/helpers/rendering_helper.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
module ActionView
- module Helpers
+ module Helpers #:nodoc:
# = Action View Rendering
#
# Implements methods that allow rendering from a view context.
@@ -11,14 +13,13 @@ module ActionView
# * <tt>:partial</tt> - See <tt>ActionView::PartialRenderer</tt>.
# * <tt>:file</tt> - Renders an explicit template file (this used to be the old default), add :locals to pass in those.
# * <tt>:inline</tt> - Renders an inline template similar to how it's done in the controller.
- # * <tt>:text</tt> - Renders the text passed in out.
# * <tt>:plain</tt> - Renders the text passed in out. Setting the content
# type as <tt>text/plain</tt>.
# * <tt>:html</tt> - Renders the HTML safe string passed in out, otherwise
# performs HTML escape on the string first. Setting the content type as
# <tt>text/html</tt>.
# * <tt>:body</tt> - Renders the text passed in, and inherits the content
- # type of <tt>text/html</tt> from <tt>ActionDispatch::Response</tt>
+ # type of <tt>text/plain</tt> from <tt>ActionDispatch::Response</tt>
# object.
#
# If no options hash is passed or :update specified, the default is to render a partial and use the second parameter
@@ -27,12 +28,12 @@ module ActionView
case options
when Hash
if block_given?
- view_renderer.render_partial(self, options.merge(:partial => options[:layout]), &block)
+ view_renderer.render_partial(self, options.merge(partial: options[:layout]), &block)
else
view_renderer.render(self, options)
end
else
- view_renderer.render_partial(self, :partial => options, :locals => locals, &block)
+ view_renderer.render_partial(self, partial: options, locals: locals, &block)
end
end
diff --git a/actionview/lib/action_view/helpers/sanitize_helper.rb b/actionview/lib/action_view/helpers/sanitize_helper.rb
index a2e9f37453..f4fa133f55 100644
--- a/actionview/lib/action_view/helpers/sanitize_helper.rb
+++ b/actionview/lib/action_view/helpers/sanitize_helper.rb
@@ -1,18 +1,21 @@
-require 'active_support/core_ext/object/try'
-require 'rails-html-sanitizer'
+# frozen_string_literal: true
+
+require "active_support/core_ext/object/try"
+require "rails-html-sanitizer"
module ActionView
# = Action View Sanitize Helpers
- module Helpers
+ module Helpers #:nodoc:
# The SanitizeHelper module provides a set of methods for scrubbing text of undesired HTML elements.
# These helper methods extend Action View making them callable within your template files.
module SanitizeHelper
extend ActiveSupport::Concern
- # Sanitizes HTML input, stripping all tags and attributes that aren't whitelisted.
+ # Sanitizes HTML input, stripping all but known-safe tags and attributes.
#
# 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 +23,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
#
@@ -38,24 +40,22 @@ module ActionView
#
# <%= sanitize @comment.body %>
#
- # Providing custom whitelisted tags and attributes:
+ # Providing custom lists of permitted tags and attributes:
#
# <%= sanitize @comment.body, tags: %w(strong em a), attributes: %w(href) %>
#
# 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 +88,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 +98,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 +115,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
@@ -120,7 +126,7 @@ module ActionView
attr_writer :full_sanitizer, :link_sanitizer, :white_list_sanitizer
# Vendors the full, link and white list sanitizers.
- # Provided strictly for compabitility and can be removed in Rails 5.
+ # Provided strictly for compatibility and can be removed in Rails 6.
def sanitizer_vendor
Rails::Html::Sanitizer
end
diff --git a/actionview/lib/action_view/helpers/tag_helper.rb b/actionview/lib/action_view/helpers/tag_helper.rb
index a87c223a71..3979721d34 100644
--- a/actionview/lib/action_view/helpers/tag_helper.rb
+++ b/actionview/lib/action_view/helpers/tag_helper.rb
@@ -1,38 +1,208 @@
-require 'active_support/core_ext/string/output_safety'
-require 'set'
+# frozen_string_literal: true
+
+require "active_support/core_ext/string/output_safety"
+require "set"
module ActionView
# = Action View Tag Helpers
module Helpers #:nodoc:
- # Provides methods to generate HTML tags programmatically when you can't use
- # a Builder. By default, they output XHTML compliant tags.
+ # Provides methods to generate HTML tags programmatically both as a modern
+ # HTML5 compliant builder style and legacy XHTML compliant tags.
module TagHelper
extend ActiveSupport::Concern
include CaptureHelper
include OutputSafetyHelper
- BOOLEAN_ATTRIBUTES = %w(disabled readonly multiple checked autobuffer
- autoplay controls loop selected hidden scoped async
- defer reversed ismap seamless muted required
- autofocus novalidate formnovalidate open pubdate
- itemscope allowfullscreen default inert sortable
- truespeed typemustmatch).to_set
+ BOOLEAN_ATTRIBUTES = %w(allowfullscreen async autofocus autoplay checked
+ compact controls declare default defaultchecked
+ defaultmuted defaultselected defer disabled
+ enabled formnovalidate hidden indeterminate inert
+ ismap itemscope loop multiple muted nohref
+ noresize noshade novalidate nowrap open
+ pauseonexit readonly required reversed scoped
+ seamless selected sortable truespeed typemustmatch
+ visible).to_set
BOOLEAN_ATTRIBUTES.merge(BOOLEAN_ATTRIBUTES.map(&:to_sym))
- TAG_PREFIXES = ['aria', 'data', :aria, :data].to_set
+ TAG_PREFIXES = ["aria", "data", :aria, :data].to_set
+
+ PRE_CONTENT_STRINGS = Hash.new { "" }
+ PRE_CONTENT_STRINGS[:textarea] = "\n"
+ PRE_CONTENT_STRINGS["textarea"] = "\n"
+
+ class TagBuilder #:nodoc:
+ include CaptureHelper
+ include OutputSafetyHelper
+
+ VOID_ELEMENTS = %i(area base br col embed hr img input keygen link meta param source track wbr).to_set
+
+ def initialize(view_context)
+ @view_context = view_context
+ end
+
+ def tag_string(name, content = nil, escape_attributes: true, **options, &block)
+ content = @view_context.capture(self, &block) if block_given?
+ if VOID_ELEMENTS.include?(name) && content.nil?
+ "<#{name.to_s.dasherize}#{tag_options(options, escape_attributes)}>".html_safe
+ else
+ content_tag_string(name.to_s.dasherize, content || "", options, escape_attributes)
+ end
+ end
+
+ def content_tag_string(name, content, options, escape = true)
+ tag_options = tag_options(options, escape) if options
+ content = ERB::Util.unwrapped_html_escape(content) if escape
+ "<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name]}#{content}</#{name}>".html_safe
+ end
+
+ def tag_options(options, escape = true)
+ return if options.blank?
+ output = +""
+ sep = " "
+ options.each_pair do |key, value|
+ if TAG_PREFIXES.include?(key) && value.is_a?(Hash)
+ value.each_pair do |k, v|
+ next if v.nil?
+ output << sep
+ output << prefix_tag_option(key, k, v, escape)
+ end
+ elsif BOOLEAN_ATTRIBUTES.include?(key)
+ if value
+ output << sep
+ output << boolean_tag_option(key)
+ end
+ elsif !value.nil?
+ output << sep
+ output << tag_option(key, value, escape)
+ end
+ end
+ output unless output.empty?
+ end
- PRE_CONTENT_STRINGS = {
- :textarea => "\n"
- }
+ def boolean_tag_option(key)
+ %(#{key}="#{key}")
+ end
- # Returns an empty HTML tag of type +name+ which by default is XHTML
+ def tag_option(key, value, escape)
+ if value.is_a?(Array)
+ value = escape ? safe_join(value, " ") : value.join(" ")
+ else
+ value = escape ? ERB::Util.unwrapped_html_escape(value) : value.to_s.dup
+ end
+ value.gsub!('"', "&quot;")
+ %(#{key}="#{value}")
+ end
+
+ private
+ def prefix_tag_option(prefix, key, value, escape)
+ key = "#{prefix}-#{key.to_s.dasherize}"
+ unless value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(BigDecimal)
+ value = value.to_json
+ end
+ tag_option(key, value, escape)
+ end
+
+ def respond_to_missing?(*args)
+ true
+ end
+
+ def method_missing(called, *args, &block)
+ tag_string(called, *args, &block)
+ end
+ end
+
+ # Returns an HTML tag.
+ #
+ # === Building HTML tags
+ #
+ # Builds HTML5 compliant tags with a tag proxy. Every tag can be built with:
+ #
+ # tag.<tag name>(optional content, options)
+ #
+ # where tag name can be e.g. br, div, section, article, or any tag really.
+ #
+ # ==== Passing content
+ #
+ # Tags can pass content to embed within it:
+ #
+ # tag.h1 'All titles fit to print' # => <h1>All titles fit to print</h1>
+ #
+ # tag.div tag.p('Hello world!') # => <div><p>Hello world!</p></div>
+ #
+ # Content can also be captured with a block, which is useful in templates:
+ #
+ # <%= tag.p do %>
+ # The next great American novel starts here.
+ # <% end %>
+ # # => <p>The next great American novel starts here.</p>
+ #
+ # ==== Options
+ #
+ # Use symbol keyed options to add attributes to the generated tag.
+ #
+ # tag.section class: %w( kitties puppies )
+ # # => <section class="kitties puppies"></section>
+ #
+ # tag.section id: dom_id(@post)
+ # # => <section id="<generated dom id>"></section>
+ #
+ # Pass +true+ for any attributes that can render with no values, like +disabled+ and +readonly+.
+ #
+ # tag.input type: 'text', disabled: true
+ # # => <input type="text" disabled="disabled">
+ #
+ # HTML5 <tt>data-*</tt> attributes can be set with a single +data+ key
+ # pointing to a hash of sub-attributes.
+ #
+ # To play nicely with JavaScript conventions, sub-attributes are dasherized.
+ #
+ # tag.article data: { user_id: 123 }
+ # # => <article data-user-id="123"></article>
+ #
+ # Thus <tt>data-user-id</tt> can be accessed as <tt>dataset.userId</tt>.
+ #
+ # Data attribute values are encoded to JSON, with the exception of strings, symbols and
+ # BigDecimals.
+ # This may come in handy when using jQuery's HTML5-aware <tt>.data()</tt>
+ # from 1.4.3.
+ #
+ # tag.div data: { city_state: %w( Chicago IL ) }
+ # # => <div data-city-state="[&quot;Chicago&quot;,&quot;IL&quot;]"></div>
+ #
+ # The generated attributes are escaped by default. This can be disabled using
+ # +escape_attributes+.
+ #
+ # tag.img src: 'open & shut.png'
+ # # => <img src="open &amp; shut.png">
+ #
+ # tag.img src: 'open & shut.png', escape_attributes: false
+ # # => <img src="open & shut.png">
+ #
+ # The tag builder respects
+ # {HTML5 void elements}[https://www.w3.org/TR/html5/syntax.html#void-elements]
+ # if no content is passed, and omits closing tags for those elements.
+ #
+ # # A standard element:
+ # tag.div # => <div></div>
+ #
+ # # A void element:
+ # tag.br # => <br>
+ #
+ # === Legacy syntax
+ #
+ # The following format is for legacy syntax support. It will be deprecated in future versions of Rails.
+ #
+ # tag(name, options = nil, open = false, escape = true)
+ #
+ # It returns an empty HTML tag of type +name+ which by default is XHTML
# compliant. Set +open+ to true to create an open tag compatible
# with HTML 4.0 and below. Add HTML attributes by passing an attributes
# hash to +options+. Set +escape+ to false to disable attribute value
# escaping.
#
# ==== Options
+ #
# You can use symbols or strings for the attribute names.
#
# Use +true+ with boolean attributes that can render with no value, like
@@ -41,16 +211,8 @@ module ActionView
# HTML5 <tt>data-*</tt> attributes can be set with a single +data+ key
# pointing to a hash of sub-attributes.
#
- # To play nicely with JavaScript conventions sub-attributes are dasherized.
- # For example, a key +user_id+ would render as <tt>data-user-id</tt> and
- # thus accessed as <tt>dataset.userId</tt>.
- #
- # Values are encoded to JSON, with the exception of strings, symbols and
- # BigDecimals.
- # This may come in handy when using jQuery's HTML5-aware <tt>.data()</tt>
- # from 1.4.3.
- #
# ==== Examples
+ #
# tag("br")
# # => <br />
#
@@ -66,13 +228,17 @@ module ActionView
# tag("img", src: "open & shut.png")
# # => <img src="open &amp; shut.png" />
#
- # tag("img", {src: "open &amp; shut.png"}, false, false)
+ # tag("img", { src: "open &amp; shut.png" }, false, false)
# # => <img src="open &amp; shut.png" />
#
- # tag("div", data: {name: 'Stephen', city_state: %w(Chicago IL)})
+ # tag("div", data: { name: 'Stephen', city_state: %w(Chicago IL) })
# # => <div data-name="Stephen" data-city-state="[&quot;Chicago&quot;,&quot;IL&quot;]" />
- def tag(name, options = nil, open = false, escape = true)
- "<#{name}#{tag_options(options, escape) if options}#{open ? ">" : " />"}".html_safe
+ def tag(name = nil, options = nil, open = false, escape = true)
+ if name.nil?
+ tag_builder
+ else
+ "<#{name}#{tag_builder.tag_options(options, escape) if options}#{open ? ">" : " />"}".html_safe
+ end
end
# Returns an HTML block tag of type +name+ surrounding the +content+. Add
@@ -80,6 +246,7 @@ module ActionView
# Instead of passing the content as an argument, you can also use a block
# in which case, you pass your +options+ as the second parameter.
# Set escape to false to disable attribute value escaping.
+ # Note: this is legacy syntax, see +tag+ method description for details.
#
# ==== Options
# The +options+ hash can be used with attributes with no value like (<tt>disabled</tt> and
@@ -103,9 +270,9 @@ module ActionView
def content_tag(name, content_or_options_with_block = nil, options = nil, escape = true, &block)
if block_given?
options = content_or_options_with_block if content_or_options_with_block.is_a?(Hash)
- content_tag_string(name, capture(&block), options, escape)
+ tag_builder.content_tag_string(name, capture(&block), options, escape)
else
- content_tag_string(name, content_or_options_with_block, options, escape)
+ tag_builder.content_tag_string(name, content_or_options_with_block, options, escape)
end
end
@@ -123,7 +290,7 @@ module ActionView
# cdata_section("hello]]>world")
# # => <![CDATA[hello]]]]><![CDATA[>world]]>
def cdata_section(content)
- splitted = content.to_s.gsub(/\]\]\>/, ']]]]><![CDATA[>')
+ splitted = content.to_s.gsub(/\]\]\>/, "]]]]><![CDATA[>")
"<![CDATA[#{splitted}]]>".html_safe
end
@@ -139,49 +306,8 @@ module ActionView
end
private
-
- def content_tag_string(name, content, options, escape = true)
- tag_options = tag_options(options, escape) if options
- content = ERB::Util.unwrapped_html_escape(content) if escape
- "<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name.to_sym]}#{content}</#{name}>".html_safe
- end
-
- def tag_options(options, escape = true)
- return if options.blank?
- attrs = []
- options.each_pair do |key, value|
- if TAG_PREFIXES.include?(key) && value.is_a?(Hash)
- value.each_pair do |k, v|
- attrs << prefix_tag_option(key, k, v, escape)
- end
- elsif BOOLEAN_ATTRIBUTES.include?(key)
- attrs << boolean_tag_option(key) if value
- elsif !value.nil?
- attrs << tag_option(key, value, escape)
- end
- end
- " #{attrs * ' '}" unless attrs.empty?
- end
-
- def prefix_tag_option(prefix, key, value, escape)
- key = "#{prefix}-#{key.to_s.dasherize}"
- unless value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(BigDecimal)
- value = value.to_json
- end
- tag_option(key, value, escape)
- end
-
- def boolean_tag_option(key)
- %(#{key}="#{key}")
- end
-
- def tag_option(key, value, escape)
- if value.is_a?(Array)
- value = escape ? safe_join(value, " ") : value.join(" ")
- else
- value = escape ? ERB::Util.unwrapped_html_escape(value) : value
- end
- %(#{key}="#{value}")
+ def tag_builder
+ @tag_builder ||= TagBuilder.new(self)
end
end
end
diff --git a/actionview/lib/action_view/helpers/tags.rb b/actionview/lib/action_view/helpers/tags.rb
index a4f6eb0150..566668b958 100644
--- a/actionview/lib/action_view/helpers/tags.rb
+++ b/actionview/lib/action_view/helpers/tags.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
module ActionView
- module Helpers
+ module Helpers #:nodoc:
module Tags #:nodoc:
extend ActiveSupport::Autoload
diff --git a/actionview/lib/action_view/helpers/tags/base.rb b/actionview/lib/action_view/helpers/tags/base.rb
index d57f26ba4f..eef527d36f 100644
--- a/actionview/lib/action_view/helpers/tags/base.rb
+++ b/actionview/lib/action_view/helpers/tags/base.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionView
module Helpers
module Tags # :nodoc:
@@ -11,10 +13,19 @@ module ActionView
@object_name, @method_name = object_name.to_s.dup, method_name.to_s.dup
@template_object = template_object
- @object_name.sub!(/\[\]$/,"") || @object_name.sub!(/\[\]\]$/,"]")
+ @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.
@@ -24,136 +35,161 @@ module ActionView
private
- def value(object)
- object.public_send @method_name if object
- end
+ def value
+ 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)
- unless object.nil?
- method_before_type_cast = @method_name + "_before_type_cast"
+ def value_before_type_cast
+ unless object.nil?
+ method_before_type_cast = @method_name + "_before_type_cast"
- if value_came_from_user?(object) && object.respond_to?(method_before_type_cast)
- object.public_send(method_before_type_cast)
- else
- value(object)
+ if value_came_from_user? && object.respond_to?(method_before_type_cast)
+ object.public_send(method_before_type_cast)
+ else
+ value
+ end
end
end
- end
- def value_came_from_user?(object)
- method_name = "#{@method_name}_came_from_user?"
- !object.respond_to?(method_name) || object.public_send(method_name)
- end
+ def value_came_from_user?
+ method_name = "#{@method_name}_came_from_user?"
+ !object.respond_to?(method_name) || object.public_send(method_name)
+ end
- def retrieve_object(object)
- if object
- object
- elsif @template_object.instance_variable_defined?("@#{@object_name}")
- @template_object.instance_variable_get("@#{@object_name}")
+ def retrieve_object(object)
+ if object
+ object
+ elsif @template_object.instance_variable_defined?("@#{@object_name}")
+ @template_object.instance_variable_get("@#{@object_name}")
+ end
+ rescue NameError
+ # As @object_name may contain the nested syntax (item[subobject]) we need to fallback to nil.
+ nil
end
- rescue NameError
- # As @object_name may contain the nested syntax (item[subobject]) we need to fallback to nil.
- nil
- end
- def retrieve_autoindex(pre_match)
- object = self.object || @template_object.instance_variable_get("@#{pre_match}")
- if object && object.respond_to?(:to_param)
- object.to_param
- else
- raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}"
+ def retrieve_autoindex(pre_match)
+ object = self.object || @template_object.instance_variable_get("@#{pre_match}")
+ if object && object.respond_to?(:to_param)
+ object.to_param
+ else
+ raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}"
+ end
end
- end
- def add_default_name_and_id_for_value(tag_value, options)
- if tag_value.nil?
- add_default_name_and_id(options)
- else
- specified_id = options["id"]
- add_default_name_and_id(options)
+ def add_default_name_and_id_for_value(tag_value, options)
+ if tag_value.nil?
+ add_default_name_and_id(options)
+ else
+ specified_id = options["id"]
+ add_default_name_and_id(options)
- if specified_id.blank? && options["id"].present?
- options["id"] += "_#{sanitized_value(tag_value)}"
+ if specified_id.blank? && options["id"].present?
+ options["id"] += "_#{sanitized_value(tag_value)}"
+ end
end
end
- end
- 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
+ def add_default_name_and_id(options)
+ index = name_and_id_index(options)
+ options["name"] = options.fetch("name") { tag_name(options["multiple"], index) }
+
+ if generate_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
- end
- def tag_name(multiple = false, index = nil)
- # a little duplication to construct less strings
- if index
- "#{@object_name}[#{index}][#{sanitized_method_name}]#{"[]" if multiple}"
- else
- "#{@object_name}[#{sanitized_method_name}]#{"[]" if multiple}"
+ def tag_name(multiple = false, index = nil)
+ # a little duplication to construct less strings
+ case
+ when @object_name.empty?
+ "#{sanitized_method_name}#{multiple ? "[]" : ""}"
+ when index
+ "#{@object_name}[#{index}][#{sanitized_method_name}]#{multiple ? "[]" : ""}"
+ else
+ "#{@object_name}[#{sanitized_method_name}]#{multiple ? "[]" : ""}"
+ end
end
- end
- def tag_id(index = nil)
- # a little duplication to construct less strings
- if index
- "#{sanitized_object_name}_#{index}_#{sanitized_method_name}"
- else
- "#{sanitized_object_name}_#{sanitized_method_name}"
+ def tag_id(index = nil)
+ # a little duplication to construct less strings
+ 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}"
+ end
end
- end
- def sanitized_object_name
- @sanitized_object_name ||= @object_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
- end
+ def sanitized_object_name
+ @sanitized_object_name ||= @object_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
+ end
- def sanitized_method_name
- @sanitized_method_name ||= @method_name.sub(/\?$/,"")
- end
+ def sanitized_method_name
+ @sanitized_method_name ||= @method_name.sub(/\?$/, "")
+ end
- def sanitized_value(value)
- value.to_s.gsub(/\s/, "_").gsub(/[^-\w]/, "").downcase
- end
+ def sanitized_value(value)
+ value.to_s.gsub(/\s/, "_").gsub(/[^-[[:word:]]]/, "").mb_chars.downcase.to_s
+ end
- def select_content_tag(option_tags, options, html_options)
- html_options = html_options.stringify_keys
- add_default_name_and_id(html_options)
+ def select_content_tag(option_tags, options, html_options)
+ html_options = html_options.stringify_keys
+ add_default_name_and_id(html_options)
- if placeholder_required?(html_options)
- raise ArgumentError, "include_blank cannot be false for a required field." if options[:include_blank] == false
- options[:include_blank] ||= true unless options[:prompt]
- end
+ if placeholder_required?(html_options)
+ raise ArgumentError, "include_blank cannot be false for a required field." if options[:include_blank] == false
+ options[:include_blank] ||= true unless options[:prompt]
+ end
- value = options.fetch(:selected) { value(object) }
- select = content_tag("select", add_options(option_tags, options, value), html_options)
+ value = options.fetch(:selected) { value() }
+ select = content_tag("select", add_options(option_tags, options, value), html_options)
- if html_options["multiple"] && options.fetch(:include_hidden, true)
- tag("input", :disabled => html_options["disabled"], :name => html_options["name"], :type => "hidden", :value => "") + select
- else
- select
+ if html_options["multiple"] && options.fetch(:include_hidden, true)
+ tag("input", disabled: html_options["disabled"], name: html_options["name"], type: "hidden", value: "") + select
+ else
+ select
+ end
end
- end
- def placeholder_required?(html_options)
- # See https://html.spec.whatwg.org/multipage/forms.html#attr-select-required
- html_options["required"] && !html_options["multiple"] && html_options.fetch("size", 1).to_i == 1
- end
+ def placeholder_required?(html_options)
+ # See https://html.spec.whatwg.org/multipage/forms.html#attr-select-required
+ html_options["required"] && !html_options["multiple"] && html_options.fetch("size", 1).to_i == 1
+ end
- def add_options(option_tags, options, value = nil)
- if options[:include_blank]
- option_tags = content_tag_string('option', options[:include_blank].kind_of?(String) ? options[:include_blank] : nil, :value => '') + "\n" + option_tags
+ def add_options(option_tags, options, value = nil)
+ if options[:include_blank]
+ option_tags = tag_builder.content_tag_string("option", options[:include_blank].kind_of?(String) ? options[:include_blank] : nil, value: "") + "\n" + option_tags
+ end
+ if value.blank? && options[:prompt]
+ tag_options = { value: "" }.tap do |prompt_opts|
+ prompt_opts[:disabled] = true if options[:disabled] == ""
+ prompt_opts[:selected] = true if options[:selected] == ""
+ end
+ option_tags = tag_builder.content_tag_string("option", prompt_text(options[:prompt]), tag_options) + "\n" + option_tags
+ end
+ option_tags
end
- if value.blank? && options[:prompt]
- option_tags = content_tag_string('option', prompt_text(options[:prompt]), :value => '') + "\n" + option_tags
+
+ def name_and_id_index(options)
+ if options.key?("index")
+ options.delete("index") || ""
+ elsif @generate_indexed_names
+ @auto_index || ""
+ end
end
- option_tags
- end
- def name_and_id_index(options)
- options.key?("index") ? options.delete("index") || "" : @auto_index
- end
+ def generate_ids?
+ !@skip_default_ids
+ end
end
end
end
diff --git a/actionview/lib/action_view/helpers/tags/check_box.rb b/actionview/lib/action_view/helpers/tags/check_box.rb
index 6d51f2629a..4327e07cae 100644
--- a/actionview/lib/action_view/helpers/tags/check_box.rb
+++ b/actionview/lib/action_view/helpers/tags/check_box.rb
@@ -1,4 +1,6 @@
-require 'action_view/helpers/tags/checkable'
+# frozen_string_literal: true
+
+require "action_view/helpers/tags/checkable"
module ActionView
module Helpers
@@ -16,7 +18,7 @@ module ActionView
options = @options.stringify_keys
options["type"] = "checkbox"
options["value"] = @checked_value
- options["checked"] = "checked" if input_checked?(object, options)
+ options["checked"] = "checked" if input_checked?(options)
if options["multiple"]
add_default_name_and_id_for_value(@checked_value, options)
@@ -38,26 +40,26 @@ module ActionView
private
- def checked?(value)
- case value
- when TrueClass, FalseClass
- value == !!@checked_value
- when NilClass
- false
- when String
- value == @checked_value
- else
- if value.respond_to?(:include?)
- value.include?(@checked_value)
+ def checked?(value)
+ case value
+ when TrueClass, FalseClass
+ value == !!@checked_value
+ when NilClass
+ false
+ when String
+ value == @checked_value
else
- value.to_i == @checked_value.to_i
+ if value.respond_to?(:include?)
+ value.include?(@checked_value)
+ else
+ value.to_i == @checked_value.to_i
+ end
end
end
- end
- def hidden_field_for_checkbox(options)
- @unchecked_value ? tag("input", options.slice("name", "disabled", "form").merge!("type" => "hidden", "value" => @unchecked_value)) : "".html_safe
- end
+ def hidden_field_for_checkbox(options)
+ @unchecked_value ? tag("input", options.slice("name", "disabled", "form").merge!("type" => "hidden", "value" => @unchecked_value)) : "".html_safe
+ end
end
end
end
diff --git a/actionview/lib/action_view/helpers/tags/checkable.rb b/actionview/lib/action_view/helpers/tags/checkable.rb
index 052e9df662..776fefe778 100644
--- a/actionview/lib/action_view/helpers/tags/checkable.rb
+++ b/actionview/lib/action_view/helpers/tags/checkable.rb
@@ -1,13 +1,15 @@
+# frozen_string_literal: true
+
module ActionView
module Helpers
module Tags # :nodoc:
module Checkable # :nodoc:
- def input_checked?(object, options)
+ def input_checked?(options)
if options.has_key?("checked")
checked = options.delete "checked"
checked == true || checked == "checked"
else
- checked?(value(object))
+ checked?(value)
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 1765fa6558..455442178e 100644
--- a/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb
+++ b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb
@@ -1,4 +1,6 @@
-require 'action_view/helpers/tags/collection_helpers'
+# frozen_string_literal: true
+
+require "action_view/helpers/tags/collection_helpers"
module ActionView
module Helpers
@@ -7,43 +9,27 @@ module ActionView
include CollectionHelpers
class CheckBoxBuilder < Builder # :nodoc:
- def check_box(extra_html_options={})
+ def check_box(extra_html_options = {})
html_options = extra_html_options.merge(@input_html_options)
+ html_options[:multiple] = true
+ html_options[:skip_default_ids] = false
@template_object.check_box(@object_name, @method_name, html_options, @value, nil)
end
end
def render(&block)
- rendered_collection = render_collection do |item, value, text, default_html_options|
- default_html_options[:multiple] = true
- builder = instantiate_builder(CheckBoxBuilder, item, value, text, default_html_options)
-
- if block_given?
- @template_object.capture(builder, &block)
- else
- render_component(builder)
- end
- end
-
- # Append a hidden field to make sure something will be sent back to the
- # server if all check boxes are unchecked.
- if @options.fetch(:include_hidden, true)
- rendered_collection + hidden_field
- else
- rendered_collection
- end
+ render_collection_for(CheckBoxBuilder, &block)
end
private
- def render_component(builder)
- builder.check_box + builder.label
- end
+ def render_component(builder)
+ builder.check_box + builder.label
+ end
- def hidden_field
- hidden_name = @html_options[:name] || "#{tag_name(false, @options[:index])}[]"
- @template_object.hidden_field_tag(hidden_name, "", id: nil)
- end
+ def hidden_field_name
+ "#{super}[]"
+ end
end
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 8050638363..e1ad11bff8 100644
--- a/actionview/lib/action_view/helpers/tags/collection_helpers.rb
+++ b/actionview/lib/action_view/helpers/tags/collection_helpers.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionView
module Helpers
module Tags # :nodoc:
@@ -17,8 +19,10 @@ module ActionView
@input_html_options = input_html_options
end
- def label(label_html_options={}, &block)
+ def label(label_html_options = {}, &block)
html_options = @input_html_options.slice(:index, :namespace).merge(label_html_options)
+ html_options[:for] ||= @input_html_options[:id] if @input_html_options[:id]
+
@template_object.label(@object_name, @sanitized_attribute_name, @text, html_options, &block)
end
end
@@ -34,51 +38,81 @@ module ActionView
private
- def instantiate_builder(builder_class, item, value, text, html_options)
- builder_class.new(@template_object, @object_name, @method_name, item,
- sanitize_attribute_name(value), text, value, html_options)
- end
+ def instantiate_builder(builder_class, item, value, text, html_options)
+ builder_class.new(@template_object, @object_name, @method_name, item,
+ sanitize_attribute_name(value), text, value, html_options)
+ end
- # Generate default options for collection helpers, such as :checked and
- # :disabled.
- def default_html_options_for_collection(item, value) #:nodoc:
- html_options = @html_options.dup
+ # Generate default options for collection helpers, such as :checked and
+ # :disabled.
+ def default_html_options_for_collection(item, value)
+ html_options = @html_options.dup
- [:checked, :selected, :disabled, :readonly].each do |option|
- current_value = @options[option]
- next if current_value.nil?
+ [:checked, :selected, :disabled, :readonly].each do |option|
+ current_value = @options[option]
+ next if current_value.nil?
- accept = if current_value.respond_to?(:call)
- current_value.call(item)
- else
- Array(current_value).map(&:to_s).include?(value.to_s)
- end
+ accept = if current_value.respond_to?(:call)
+ current_value.call(item)
+ else
+ Array(current_value).map(&:to_s).include?(value.to_s)
+ end
- if accept
- html_options[option] = true
- elsif option == :checked
- html_options[option] = false
+ if accept
+ html_options[option] = true
+ elsif option == :checked
+ html_options[option] = false
+ end
end
+
+ html_options[:object] = @object
+ html_options
end
- html_options[:object] = @object
- html_options
- end
+ def sanitize_attribute_name(value)
+ "#{sanitized_method_name}_#{sanitized_value(value)}"
+ end
- def sanitize_attribute_name(value) #:nodoc:
- "#{sanitized_method_name}_#{sanitized_value(value)}"
- end
+ def render_collection
+ @collection.map do |item|
+ value = value_for_collection(item, @value_method)
+ text = value_for_collection(item, @text_method)
+ default_html_options = default_html_options_for_collection(item, value)
+ additional_html_options = option_html_attributes(item)
+
+ yield item, value, text, default_html_options.merge(additional_html_options)
+ end.join.html_safe
+ end
- def render_collection #:nodoc:
- @collection.map do |item|
- value = value_for_collection(item, @value_method)
- text = value_for_collection(item, @text_method)
- default_html_options = default_html_options_for_collection(item, value)
- additional_html_options = option_html_attributes(item)
+ 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)
- yield item, value, text, default_html_options.merge(additional_html_options)
- end.join.html_safe
- end
+ if block_given?
+ @template_object.capture(builder, &block)
+ else
+ render_component(builder)
+ end
+ end
+
+ # Prepend a hidden field to make sure something will be sent back to the
+ # server if all radio buttons are unchecked.
+ if options.fetch("include_hidden", true)
+ hidden_field + rendered_collection
+ else
+ rendered_collection
+ end
+ end
+
+ 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
+ "#{tag_name(false, @options[:index])}"
+ end
end
end
end
diff --git a/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb b/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb
index 20be34c1f2..16d37134e5 100644
--- a/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb
+++ b/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb
@@ -1,4 +1,6 @@
-require 'action_view/helpers/tags/collection_helpers'
+# frozen_string_literal: true
+
+require "action_view/helpers/tags/collection_helpers"
module ActionView
module Helpers
@@ -7,22 +9,15 @@ module ActionView
include CollectionHelpers
class RadioButtonBuilder < Builder # :nodoc:
- def radio_button(extra_html_options={})
+ def radio_button(extra_html_options = {})
html_options = extra_html_options.merge(@input_html_options)
+ html_options[:skip_default_ids] = false
@template_object.radio_button(@object_name, @method_name, @value, html_options)
end
end
def render(&block)
- render_collection do |item, value, text, default_html_options|
- builder = instantiate_builder(RadioButtonBuilder, item, value, text, default_html_options)
-
- if block_given?
- @template_object.capture(builder, &block)
- else
- render_component(builder)
- end
- end
+ render_collection_for(RadioButtonBuilder, &block)
end
private
diff --git a/actionview/lib/action_view/helpers/tags/collection_select.rb b/actionview/lib/action_view/helpers/tags/collection_select.rb
index 6cb2b2e0d3..6a3af1b256 100644
--- a/actionview/lib/action_view/helpers/tags/collection_select.rb
+++ b/actionview/lib/action_view/helpers/tags/collection_select.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionView
module Helpers
module Tags # :nodoc:
@@ -13,8 +15,8 @@ module ActionView
def render
option_tags_options = {
- :selected => @options.fetch(:selected) { value(@object) },
- :disabled => @options[:disabled]
+ selected: @options.fetch(:selected) { value },
+ disabled: @options[:disabled]
}
select_content_tag(
diff --git a/actionview/lib/action_view/helpers/tags/color_field.rb b/actionview/lib/action_view/helpers/tags/color_field.rb
index b4bbe92746..39ab1285c3 100644
--- a/actionview/lib/action_view/helpers/tags/color_field.rb
+++ b/actionview/lib/action_view/helpers/tags/color_field.rb
@@ -1,10 +1,12 @@
+# frozen_string_literal: true
+
module ActionView
module Helpers
module Tags # :nodoc:
class ColorField < TextField # :nodoc:
def render
options = @options.stringify_keys
- options["value"] ||= validate_color_string(value(object))
+ options["value"] ||= validate_color_string(value)
@options = options
super
end
@@ -13,7 +15,7 @@ module ActionView
def validate_color_string(string)
regex = /#[0-9a-fA-F]{6}/
- if regex.match(string)
+ if regex.match?(string)
string.downcase
else
"#000000"
diff --git a/actionview/lib/action_view/helpers/tags/date_field.rb b/actionview/lib/action_view/helpers/tags/date_field.rb
index c22be0db29..b17a907651 100644
--- a/actionview/lib/action_view/helpers/tags/date_field.rb
+++ b/actionview/lib/action_view/helpers/tags/date_field.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionView
module Helpers
module Tags # :nodoc:
diff --git a/actionview/lib/action_view/helpers/tags/date_select.rb b/actionview/lib/action_view/helpers/tags/date_select.rb
index 0c4ac40070..fe4e3914d7 100644
--- a/actionview/lib/action_view/helpers/tags/date_select.rb
+++ b/actionview/lib/action_view/helpers/tags/date_select.rb
@@ -1,4 +1,6 @@
-require 'active_support/core_ext/time/calculations'
+# frozen_string_literal: true
+
+require "active_support/core_ext/time/calculations"
module ActionView
module Helpers
@@ -16,56 +18,56 @@ 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
private
- def select_type
- self.class.select_type
- end
+ def select_type
+ self.class.select_type
+ end
- def datetime_selector(options, html_options)
- datetime = options.fetch(:selected) { value(object) || default_datetime(options) }
- @auto_index ||= nil
+ def datetime_selector(options, html_options)
+ datetime = options.fetch(:selected) { value || default_datetime(options) }
+ @auto_index ||= nil
- options = options.dup
- options[:field_name] = @method_name
- options[:include_position] = true
- options[:prefix] ||= @object_name
- options[:index] = @auto_index if @auto_index && !options.has_key?(:index)
+ options = options.dup
+ options[:field_name] = @method_name
+ options[:include_position] = true
+ options[:prefix] ||= @object_name
+ options[:index] = @auto_index if @auto_index && !options.has_key?(:index)
- DateTimeSelector.new(datetime, options, html_options)
- end
+ DateTimeSelector.new(datetime, options, html_options)
+ end
- def default_datetime(options)
- return if options[:include_blank] || options[:prompt]
+ def default_datetime(options)
+ return if options[:include_blank] || options[:prompt]
- case options[:default]
- when nil
- Time.current
- when Date, Time
- options[:default]
- else
- default = options[:default].dup
+ case options[:default]
+ when nil
+ Time.current
+ when Date, Time
+ options[:default]
+ else
+ default = options[:default].dup
- # Rename :minute and :second to :min and :sec
- default[:min] ||= default[:minute]
- default[:sec] ||= default[:second]
+ # Rename :minute and :second to :min and :sec
+ default[:min] ||= default[:minute]
+ default[:sec] ||= default[:second]
- time = Time.current
+ time = Time.current
- [:year, :month, :day, :hour, :min, :sec].each do |key|
- default[key] ||= time.send(key)
- end
+ [:year, :month, :day, :hour, :min, :sec].each do |key|
+ default[key] ||= time.send(key)
+ end
- Time.utc(
- default[:year], default[:month], default[:day],
- default[:hour], default[:min], default[:sec]
- )
+ Time.utc(
+ default[:year], default[:month], default[:day],
+ default[:hour], default[:min], default[:sec]
+ )
+ end
end
- end
end
end
end
diff --git a/actionview/lib/action_view/helpers/tags/datetime_field.rb b/actionview/lib/action_view/helpers/tags/datetime_field.rb
index b2cee9d198..5d9b639b1b 100644
--- a/actionview/lib/action_view/helpers/tags/datetime_field.rb
+++ b/actionview/lib/action_view/helpers/tags/datetime_field.rb
@@ -1,10 +1,12 @@
+# frozen_string_literal: true
+
module ActionView
module Helpers
module Tags # :nodoc:
class DatetimeField < TextField # :nodoc:
def render
options = @options.stringify_keys
- options["value"] ||= format_date(value(object))
+ options["value"] ||= format_date(value)
options["min"] = format_date(datetime_value(options["min"]))
options["max"] = format_date(datetime_value(options["max"]))
@options = options
@@ -14,7 +16,7 @@ module ActionView
private
def format_date(value)
- value.try(:strftime, "%Y-%m-%dT%T.%L%z")
+ raise NotImplementedError
end
def datetime_value(value)
diff --git a/actionview/lib/action_view/helpers/tags/datetime_local_field.rb b/actionview/lib/action_view/helpers/tags/datetime_local_field.rb
index b4a74185d1..d8f8fd00d1 100644
--- a/actionview/lib/action_view/helpers/tags/datetime_local_field.rb
+++ b/actionview/lib/action_view/helpers/tags/datetime_local_field.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionView
module Helpers
module Tags # :nodoc:
diff --git a/actionview/lib/action_view/helpers/tags/datetime_select.rb b/actionview/lib/action_view/helpers/tags/datetime_select.rb
index 563de1840e..dc5570931d 100644
--- a/actionview/lib/action_view/helpers/tags/datetime_select.rb
+++ b/actionview/lib/action_view/helpers/tags/datetime_select.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionView
module Helpers
module Tags # :nodoc:
diff --git a/actionview/lib/action_view/helpers/tags/email_field.rb b/actionview/lib/action_view/helpers/tags/email_field.rb
index 7ce3ccb9bf..0c3b9224fa 100644
--- a/actionview/lib/action_view/helpers/tags/email_field.rb
+++ b/actionview/lib/action_view/helpers/tags/email_field.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionView
module Helpers
module Tags # :nodoc:
diff --git a/actionview/lib/action_view/helpers/tags/file_field.rb b/actionview/lib/action_view/helpers/tags/file_field.rb
index e6a1d9c62d..0b1d9bb778 100644
--- a/actionview/lib/action_view/helpers/tags/file_field.rb
+++ b/actionview/lib/action_view/helpers/tags/file_field.rb
@@ -1,22 +1,9 @@
+# frozen_string_literal: true
+
module ActionView
module Helpers
module Tags # :nodoc:
class FileField < TextField # :nodoc:
-
- def render
- options = @options.stringify_keys
-
- if options.fetch("include_hidden", true)
- add_default_name_and_id(options)
- options[:type] = "file"
- tag("input", name: options["name"], type: "hidden", value: "") + tag("input", options)
- else
- options.delete("include_hidden")
- @options = options
-
- super
- end
- end
end
end
end
diff --git a/actionview/lib/action_view/helpers/tags/grouped_collection_select.rb b/actionview/lib/action_view/helpers/tags/grouped_collection_select.rb
index 2ed4712dac..f24cb4beea 100644
--- a/actionview/lib/action_view/helpers/tags/grouped_collection_select.rb
+++ b/actionview/lib/action_view/helpers/tags/grouped_collection_select.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionView
module Helpers
module Tags # :nodoc:
@@ -15,8 +17,8 @@ module ActionView
def render
option_tags_options = {
- :selected => @options.fetch(:selected) { value(@object) },
- :disabled => @options[:disabled]
+ selected: @options.fetch(:selected) { value },
+ disabled: @options[:disabled]
}
select_content_tag(
diff --git a/actionview/lib/action_view/helpers/tags/hidden_field.rb b/actionview/lib/action_view/helpers/tags/hidden_field.rb
index c3757c2461..e014bd3aef 100644
--- a/actionview/lib/action_view/helpers/tags/hidden_field.rb
+++ b/actionview/lib/action_view/helpers/tags/hidden_field.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionView
module Helpers
module Tags # :nodoc:
diff --git a/actionview/lib/action_view/helpers/tags/label.rb b/actionview/lib/action_view/helpers/tags/label.rb
index b31d5fda66..02bd099784 100644
--- a/actionview/lib/action_view/helpers/tags/label.rb
+++ b/actionview/lib/action_view/helpers/tags/label.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionView
module Helpers
module Tags # :nodoc:
diff --git a/actionview/lib/action_view/helpers/tags/month_field.rb b/actionview/lib/action_view/helpers/tags/month_field.rb
index 4c0fb846ee..93b2bf11f0 100644
--- a/actionview/lib/action_view/helpers/tags/month_field.rb
+++ b/actionview/lib/action_view/helpers/tags/month_field.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionView
module Helpers
module Tags # :nodoc:
diff --git a/actionview/lib/action_view/helpers/tags/number_field.rb b/actionview/lib/action_view/helpers/tags/number_field.rb
index 4f95b1b4de..41c696423c 100644
--- a/actionview/lib/action_view/helpers/tags/number_field.rb
+++ b/actionview/lib/action_view/helpers/tags/number_field.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionView
module Helpers
module Tags # :nodoc:
diff --git a/actionview/lib/action_view/helpers/tags/password_field.rb b/actionview/lib/action_view/helpers/tags/password_field.rb
index 6099fa6f19..9f10f5236e 100644
--- a/actionview/lib/action_view/helpers/tags/password_field.rb
+++ b/actionview/lib/action_view/helpers/tags/password_field.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
module ActionView
module Helpers
module Tags # :nodoc:
class PasswordField < TextField # :nodoc:
def render
- @options = {:value => nil}.merge!(@options)
+ @options = { value: nil }.merge!(@options)
super
end
end
diff --git a/actionview/lib/action_view/helpers/tags/placeholderable.rb b/actionview/lib/action_view/helpers/tags/placeholderable.rb
index cf7b117614..e9f7601e57 100644
--- a/actionview/lib/action_view/helpers/tags/placeholderable.rb
+++ b/actionview/lib/action_view/helpers/tags/placeholderable.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionView
module Helpers
module Tags # :nodoc:
diff --git a/actionview/lib/action_view/helpers/tags/radio_button.rb b/actionview/lib/action_view/helpers/tags/radio_button.rb
index 4849c537a5..621db2b1b5 100644
--- a/actionview/lib/action_view/helpers/tags/radio_button.rb
+++ b/actionview/lib/action_view/helpers/tags/radio_button.rb
@@ -1,4 +1,6 @@
-require 'action_view/helpers/tags/checkable'
+# frozen_string_literal: true
+
+require "action_view/helpers/tags/checkable"
module ActionView
module Helpers
@@ -15,16 +17,16 @@ module ActionView
options = @options.stringify_keys
options["type"] = "radio"
options["value"] = @tag_value
- options["checked"] = "checked" if input_checked?(object, options)
+ options["checked"] = "checked" if input_checked?(options)
add_default_name_and_id_for_value(@tag_value, options)
tag("input", options)
end
private
- def checked?(value)
- value.to_s == @tag_value.to_s
- end
+ def checked?(value)
+ value.to_s == @tag_value.to_s
+ end
end
end
end
diff --git a/actionview/lib/action_view/helpers/tags/range_field.rb b/actionview/lib/action_view/helpers/tags/range_field.rb
index f98ae88043..66d1bbac5b 100644
--- a/actionview/lib/action_view/helpers/tags/range_field.rb
+++ b/actionview/lib/action_view/helpers/tags/range_field.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionView
module Helpers
module Tags # :nodoc:
diff --git a/actionview/lib/action_view/helpers/tags/search_field.rb b/actionview/lib/action_view/helpers/tags/search_field.rb
index a848aeabfa..f209348904 100644
--- a/actionview/lib/action_view/helpers/tags/search_field.rb
+++ b/actionview/lib/action_view/helpers/tags/search_field.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionView
module Helpers
module Tags # :nodoc:
diff --git a/actionview/lib/action_view/helpers/tags/select.rb b/actionview/lib/action_view/helpers/tags/select.rb
index 180900cc8d..790721a0b7 100644
--- a/actionview/lib/action_view/helpers/tags/select.rb
+++ b/actionview/lib/action_view/helpers/tags/select.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionView
module Helpers
module Tags # :nodoc:
@@ -13,8 +15,8 @@ module ActionView
def render
option_tags_options = {
- :selected => @options.fetch(:selected) { value(@object) },
- :disabled => @options[:disabled]
+ selected: @options.fetch(:selected) { value },
+ disabled: @options[:disabled]
}
option_tags = if grouped_choices?
@@ -28,13 +30,13 @@ module ActionView
private
- # Grouped choices look like this:
- #
- # [nil, []]
- # { nil => [] }
- def grouped_choices?
- !@choices.empty? && @choices.first.respond_to?(:last) && Array === @choices.first.last
- end
+ # Grouped choices look like this:
+ #
+ # [nil, []]
+ # { nil => [] }
+ def grouped_choices?
+ !@choices.blank? && @choices.first.respond_to?(:last) && Array === @choices.first.last
+ end
end
end
end
diff --git a/actionview/lib/action_view/helpers/tags/tel_field.rb b/actionview/lib/action_view/helpers/tags/tel_field.rb
index 987bb9e67a..ab1caaac48 100644
--- a/actionview/lib/action_view/helpers/tags/tel_field.rb
+++ b/actionview/lib/action_view/helpers/tags/tel_field.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionView
module Helpers
module Tags # :nodoc:
diff --git a/actionview/lib/action_view/helpers/tags/text_area.rb b/actionview/lib/action_view/helpers/tags/text_area.rb
index 69038c1498..4519082ff6 100644
--- a/actionview/lib/action_view/helpers/tags/text_area.rb
+++ b/actionview/lib/action_view/helpers/tags/text_area.rb
@@ -1,4 +1,6 @@
-require 'action_view/helpers/tags/placeholderable'
+# frozen_string_literal: true
+
+require "action_view/helpers/tags/placeholderable"
module ActionView
module Helpers
@@ -14,7 +16,7 @@ module ActionView
options["cols"], options["rows"] = size.split("x") if size.respond_to?(:split)
end
- content_tag("textarea", options.delete("value") { value_before_type_cast(object) }, options)
+ content_tag("textarea", options.delete("value") { value_before_type_cast }, options)
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 5c576a20ca..d92967e212 100644
--- a/actionview/lib/action_view/helpers/tags/text_field.rb
+++ b/actionview/lib/action_view/helpers/tags/text_field.rb
@@ -1,4 +1,6 @@
-require 'action_view/helpers/tags/placeholderable'
+# frozen_string_literal: true
+
+require "action_view/helpers/tags/placeholderable"
module ActionView
module Helpers
@@ -10,22 +12,22 @@ module ActionView
options = @options.stringify_keys
options["size"] = options["maxlength"] unless options.key?("size")
options["type"] ||= field_type
- options["value"] = options.fetch("value") { value_before_type_cast(object) } unless field_type == "file"
+ options["value"] = options.fetch("value") { value_before_type_cast } unless field_type == "file"
add_default_name_and_id(options)
tag("input", options)
end
class << self
def field_type
- @field_type ||= self.name.split("::").last.sub("Field", "").downcase
+ @field_type ||= name.split("::").last.sub("Field", "").downcase
end
end
private
- def field_type
- self.class.field_type
- end
+ def field_type
+ self.class.field_type
+ end
end
end
end
diff --git a/actionview/lib/action_view/helpers/tags/time_field.rb b/actionview/lib/action_view/helpers/tags/time_field.rb
index 0e90a3aed7..9384a83a3e 100644
--- a/actionview/lib/action_view/helpers/tags/time_field.rb
+++ b/actionview/lib/action_view/helpers/tags/time_field.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionView
module Helpers
module Tags # :nodoc:
diff --git a/actionview/lib/action_view/helpers/tags/time_select.rb b/actionview/lib/action_view/helpers/tags/time_select.rb
index 0b06311d25..ba3dcb64e3 100644
--- a/actionview/lib/action_view/helpers/tags/time_select.rb
+++ b/actionview/lib/action_view/helpers/tags/time_select.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionView
module Helpers
module Tags # :nodoc:
diff --git a/actionview/lib/action_view/helpers/tags/time_zone_select.rb b/actionview/lib/action_view/helpers/tags/time_zone_select.rb
index 80d165ec7e..1d06096096 100644
--- a/actionview/lib/action_view/helpers/tags/time_zone_select.rb
+++ b/actionview/lib/action_view/helpers/tags/time_zone_select.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionView
module Helpers
module Tags # :nodoc:
@@ -11,7 +13,7 @@ module ActionView
def render
select_content_tag(
- time_zone_options_for_select(value(@object) || @options[:default], @priority_zones, @options[:model] || ActiveSupport::TimeZone), @options, @html_options
+ time_zone_options_for_select(value || @options[:default], @priority_zones, @options[:model] || ActiveSupport::TimeZone), @options, @html_options
)
end
end
diff --git a/actionview/lib/action_view/helpers/tags/translator.rb b/actionview/lib/action_view/helpers/tags/translator.rb
index 8b6655481d..e81ca3aef0 100644
--- a/actionview/lib/action_view/helpers/tags/translator.rb
+++ b/actionview/lib/action_view/helpers/tags/translator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionView
module Helpers
module Tags # :nodoc:
@@ -14,26 +16,23 @@ module ActionView
translated_attribute || human_attribute_name
end
- protected
-
- attr_reader :object_name, :method_and_value, :scope, :model
-
private
+ attr_reader :object_name, :method_and_value, :scope, :model
- def i18n_default
- if model
- key = model.model_name.i18n_key
- ["#{key}.#{method_and_value}".to_sym, ""]
- else
- ""
+ def i18n_default
+ if model
+ key = model.model_name.i18n_key
+ ["#{key}.#{method_and_value}".to_sym, ""]
+ else
+ ""
+ end
end
- end
- def human_attribute_name
- if model && model.class.respond_to?(:human_attribute_name)
- model.class.human_attribute_name(method_and_value)
+ def human_attribute_name
+ if model && model.class.respond_to?(:human_attribute_name)
+ model.class.human_attribute_name(method_and_value)
+ end
end
- end
end
end
end
diff --git a/actionview/lib/action_view/helpers/tags/url_field.rb b/actionview/lib/action_view/helpers/tags/url_field.rb
index d76340178d..395fec67e7 100644
--- a/actionview/lib/action_view/helpers/tags/url_field.rb
+++ b/actionview/lib/action_view/helpers/tags/url_field.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionView
module Helpers
module Tags # :nodoc:
diff --git a/actionview/lib/action_view/helpers/tags/week_field.rb b/actionview/lib/action_view/helpers/tags/week_field.rb
index 5b3d0494e9..572535d1d6 100644
--- a/actionview/lib/action_view/helpers/tags/week_field.rb
+++ b/actionview/lib/action_view/helpers/tags/week_field.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionView
module Helpers
module Tags # :nodoc:
@@ -5,7 +7,7 @@ module ActionView
private
def format_date(value)
- value.try(:strftime, "%Y-W%W")
+ value.try(:strftime, "%Y-W%V")
end
end
end
diff --git a/actionview/lib/action_view/helpers/text_helper.rb b/actionview/lib/action_view/helpers/text_helper.rb
index 6a3d01667d..3d378dcb2f 100644
--- a/actionview/lib/action_view/helpers/text_helper.rb
+++ b/actionview/lib/action_view/helpers/text_helper.rb
@@ -1,5 +1,7 @@
-require 'active_support/core_ext/string/filters'
-require 'active_support/core_ext/array/extract_options'
+# frozen_string_literal: true
+
+require "active_support/core_ext/string/filters"
+require "active_support/core_ext/array/extract_options"
module ActionView
# = Action View Text Helpers
@@ -11,9 +13,9 @@ module ActionView
#
# ==== Sanitization
#
- # Most text helpers by default sanitize the given content, but do not escape it.
- # This means HTML tags will appear in the page but all malicious code will be removed.
- # Let's look at some examples using the +simple_format+ method:
+ # Most text helpers that generate HTML output sanitize the given input by default,
+ # but do not escape it. This means HTML tags will appear in the page but all malicious
+ # code will be removed. Let's look at some examples using the +simple_format+ method:
#
# simple_format('<a href="http://example.com/">Example</a>')
# # => "<p><a href=\"http://example.com/\">Example</a></p>"
@@ -126,7 +128,7 @@ module ActionView
# # => You searched for: <a href="search?q=rails">rails</a>
#
# highlight('<a href="javascript:alert(\'no!\')">ruby</a> on rails', 'rails', sanitize: false)
- # # => "<a>ruby</a> on <mark>rails</mark>"
+ # # => <a href="javascript:alert('no!')">ruby</a> on <mark>rails</mark>
def highlight(text, phrases, options = {})
text = sanitize(text) if options.fetch(:sanitize, true)
@@ -135,7 +137,7 @@ module ActionView
else
match = Array(phrases).map do |p|
Regexp === p ? p.to_s : Regexp.escape(p)
- end.join('|')
+ end.join("|")
if block_given?
text.gsub(/(#{match})(?![^<]*?>)/i) { |found| yield found }
@@ -151,7 +153,7 @@ module ActionView
# defined in <tt>:radius</tt> (which defaults to 100). If the excerpt radius overflows the beginning or end of the +text+,
# then the <tt>:omission</tt> option (which defaults to "...") will be prepended/appended accordingly. Use the
# <tt>:separator</tt> option to choose the delimitation. The resulting string will be stripped in any case. If the +phrase+
- # isn't found, nil is returned.
+ # isn't found, +nil+ is returned.
#
# excerpt('This is an example', 'an', radius: 5)
# # => ...s is an exam...
@@ -186,8 +188,8 @@ module ActionView
unless separator.empty?
text.split(separator).each do |value|
- if value.match(regex)
- regex = phrase = value
+ if value.match?(regex)
+ phrase = value
break
end
end
@@ -204,12 +206,12 @@ module ActionView
# Attempts to pluralize the +singular+ word unless +count+ is 1. If
# +plural+ is supplied, it will use that when count is > 1, otherwise
- # it will use the Inflector to determine the plural form.
+ # it will use the Inflector to determine the plural form for the given locale,
+ # which defaults to I18n.locale
#
- # If passed an optional +locale:+ parameter, the word will be pluralized
- # using rules defined for that language (you must define your own
- # inflection rules for languages other than English). See
- # ActiveSupport::Inflector.pluralize
+ # The word will be pluralized using rules defined for the locale
+ # (you must define your own inflection rules for languages other than English).
+ # See ActiveSupport::Inflector.pluralize
#
# pluralize(1, 'person')
# # => 1 person
@@ -217,7 +219,7 @@ module ActionView
# pluralize(2, 'person')
# # => 2 people
#
- # pluralize(3, 'person', 'users')
+ # pluralize(3, 'person', plural: 'users')
# # => 3 users
#
# pluralize(0, 'person')
@@ -225,8 +227,8 @@ module ActionView
#
# pluralize(2, 'Person', locale: :de)
# # => 2 Personen
- def pluralize(count, singular, plural = nil, locale: nil)
- word = if (count == 1 || count =~ /^1(\.0+)?$/)
+ def pluralize(count, singular, plural_arg = nil, plural: plural_arg, locale: I18n.locale)
+ word = if count == 1 || count =~ /^1(\.0+)?$/
singular
else
plural || singular.pluralize(locale)
@@ -250,19 +252,23 @@ module ActionView
#
# word_wrap('Once upon a time', line_width: 1)
# # => Once\nupon\na\ntime
- def word_wrap(text, options = {})
- line_width = options.fetch(:line_width, 80)
-
+ #
+ # You can also specify a custom +break_sequence+ ("\n" by default)
+ #
+ # word_wrap('Once upon a time', line_width: 1, break_sequence: "\r\n")
+ # # => Once\r\nupon\r\na\r\ntime
+ def word_wrap(text, line_width: 80, break_sequence: "\n")
text.split("\n").collect! do |line|
- line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1\n").strip : line
- end * "\n"
+ line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1#{break_sequence}").rstrip : line
+ end * break_sequence
end
# Returns +text+ transformed into HTML using simple formatting rules.
- # Two or more consecutive newlines(<tt>\n\n</tt>) are considered as a
- # paragraph and wrapped in <tt><p></tt> tags. One newline (<tt>\n</tt>) is
- # considered as a linebreak and a <tt><br /></tt> tag is appended. This
- # method does not remove the newlines from the +text+.
+ # Two or more consecutive newlines(<tt>\n\n</tt> or <tt>\r\n\r\n</tt>) are
+ # considered a paragraph and wrapped in <tt><p></tt> tags. One newline
+ # (<tt>\n</tt> or <tt>\r\n</tt>) is considered a linebreak and a
+ # <tt><br /></tt> tag is appended. This method does not remove the
+ # newlines from the +text+.
#
# You can pass any HTML attributes into <tt>html_options</tt>. These
# will be added to all created paragraphs.
@@ -347,7 +353,7 @@ module ActionView
# <% end %>
def cycle(first_value, *values)
options = values.extract_options!
- name = options.fetch(:name, 'default')
+ name = options.fetch(:name, "default")
values.unshift(*first_value)
@@ -416,22 +422,22 @@ module ActionView
def to_s
value = @values[@index].to_s
@index = next_index
- return value
+ value
end
private
- def next_index
- step_index(1)
- end
+ def next_index
+ step_index(1)
+ end
- def previous_index
- step_index(-1)
- end
+ def previous_index
+ step_index(-1)
+ end
- def step_index(n)
- (@index + n) % @values.size
- end
+ def step_index(n)
+ (@index + n) % @values.size
+ end
end
private
@@ -440,7 +446,7 @@ module ActionView
# uses an instance variable of ActionView::Base.
def get_cycle(name)
@_cycles = Hash.new unless defined?(@_cycles)
- return @_cycles[name]
+ @_cycles[name]
end
def set_cycle(name, cycle_object)
diff --git a/actionview/lib/action_view/helpers/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb
index 0615bd2e0d..ae1c93e12f 100644
--- a/actionview/lib/action_view/helpers/translation_helper.rb
+++ b/actionview/lib/action_view/helpers/translation_helper.rb
@@ -1,12 +1,21 @@
-require 'action_view/helpers/tag_helper'
-require 'active_support/core_ext/string/access'
-require 'i18n/exceptions'
+# frozen_string_literal: true
+
+require "action_view/helpers/tag_helper"
+require "active_support/core_ext/string/access"
+require "i18n/exceptions"
module ActionView
# = Action View Translation Helpers
- module Helpers
+ module Helpers #:nodoc:
module TranslationHelper
+ extend ActiveSupport::Concern
+
include TagHelper
+
+ included do
+ mattr_accessor :debug_missing_translation, default: true
+ end
+
# Delegates to <tt>I18n#translate</tt> but also performs three additional
# functions.
#
@@ -50,11 +59,9 @@ module ActionView
# they can provide HTML values for.
def translate(key, options = {})
options = options.dup
- has_default = options.has_key?(:default)
- remaining_defaults = Array(options.delete(:default)).compact
-
- if has_default && !remaining_defaults.first.kind_of?(Symbol)
- options[:default] = remaining_defaults
+ if options.has_key?(:default)
+ remaining_defaults = Array(options.delete(:default)).compact
+ options[:default] = remaining_defaults unless remaining_defaults.first.kind_of?(Symbol)
end
# If the user has explicitly decided to NOT raise errors, pass that option to I18n.
@@ -76,8 +83,11 @@ module ActionView
end
end
translation = I18n.translate(scope_key_by_partial(key), html_safe_options.merge(raise: i18n_raise))
-
- translation.respond_to?(:html_safe) ? translation.html_safe : translation
+ if translation.respond_to?(:map)
+ translation.map { |element| element.respond_to?(:html_safe) ? element.html_safe : element }
+ else
+ translation.respond_to?(:html_safe) ? translation.html_safe : translation
+ end
else
I18n.translate(scope_key_by_partial(key), options.merge(raise: i18n_raise))
end
@@ -88,7 +98,16 @@ module ActionView
raise e if raise_error
keys = I18n.normalize_keys(e.locale, e.key, e.options[:scope])
- content_tag('span', keys.last.to_s.titleize, :class => 'translation_missing', :title => "translation missing: #{keys.join('.')}")
+ title = +"translation missing: #{keys.join('.')}"
+
+ interpolations = options.except(:default, :scope)
+ if interpolations.any?
+ title << ", " << interpolations.map { |k, v| "#{k}: #{ERB::Util.html_escape(v)}" }.join(", ")
+ end
+
+ return title unless ActionView::Base.debug_missing_translation
+
+ content_tag("span", keys.last.to_s.titleize, class: "translation_missing", title: title)
end
end
alias :t :translate
@@ -104,9 +123,12 @@ module ActionView
private
def scope_key_by_partial(key)
- if key.to_s.first == "."
+ stringified_key = key.to_s
+ if stringified_key.first == "."
if @virtual_path
- @virtual_path.gsub(%r{/_?}, ".") + key.to_s
+ @_scope_key_by_partial_cache ||= {}
+ @_scope_key_by_partial_cache[@virtual_path] ||= @virtual_path.gsub(%r{/_?}, ".")
+ "#{@_scope_key_by_partial_cache[@virtual_path]}#{stringified_key}"
else
raise "Cannot use t(#{key.inspect}) shortcut because path is not available"
end
@@ -116,7 +138,7 @@ module ActionView
end
def html_safe_translation_key?(key)
- key.to_s =~ /(\b|_|\.)html$/
+ /(\b|_|\.)html$/.match?(key.to_s)
end
end
end
diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb
index afb1265ad9..948dd1551f 100644
--- a/actionview/lib/action_view/helpers/url_helper.rb
+++ b/actionview/lib/action_view/helpers/url_helper.rb
@@ -1,7 +1,9 @@
-require 'action_view/helpers/javascript_helper'
-require 'active_support/core_ext/array/access'
-require 'active_support/core_ext/hash/keys'
-require 'active_support/core_ext/string/output_safety'
+# frozen_string_literal: true
+
+require "action_view/helpers/javascript_helper"
+require "active_support/core_ext/array/access"
+require "active_support/core_ext/hash/keys"
+require "active_support/core_ext/string/output_safety"
module ActionView
# = Action View URL Helpers
@@ -35,17 +37,27 @@ 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
def _back_url # :nodoc:
- referrer = controller.respond_to?(:request) && controller.request.env["HTTP_REFERER"]
- referrer || 'javascript:history.back()'
+ _filtered_referrer || "javascript:history.back()"
end
protected :_back_url
+ def _filtered_referrer # :nodoc:
+ if controller.respond_to?(:request)
+ referrer = controller.request.env["HTTP_REFERER"]
+ if referrer && URI(referrer).scheme != "javascript"
+ referrer
+ end
+ end
+ rescue URI::InvalidURIError
+ end
+ protected :_filtered_referrer
+
# Creates an anchor element of the given +name+ using a URL created by the set of +options+.
# See the valid options in the documentation for +url_for+. It's also possible to
# pass a String instead of an options hash, which generates an anchor element that uses the
@@ -95,10 +107,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
@@ -128,6 +139,11 @@ module ActionView
# link_to "Profiles", controller: "profiles"
# # => <a href="/profiles">Profiles</a>
#
+ # When name is +nil+ the href is presented instead
+ #
+ # link_to nil, "http://example.com"
+ # # => <a href="http://www.example.com">http://www.example.com</a>
+ #
# You can use a block as well if your link target is hard to fit into the name parameter. ERB example:
#
# <%= link_to(@profile) do %>
@@ -184,9 +200,9 @@ module ActionView
html_options = convert_options_to_data_attributes(options, html_options)
url = url_for(options)
- html_options['href'] ||= url
+ html_options["href"] ||= url
- content_tag(:a, name || url, html_options, &block)
+ content_tag("a", name || url, html_options, &block)
end
# Generates a form containing a single button that submits to the URL created
@@ -288,38 +304,43 @@ module ActionView
html_options = html_options.stringify_keys
url = options.is_a?(String) ? options : url_for(options)
- remote = html_options.delete('remote')
- params = html_options.delete('params')
+ remote = html_options.delete("remote")
+ params = html_options.delete("params")
- method = html_options.delete('method').to_s
- method_tag = BUTTON_TAG_METHOD_VERBS.include?(method) ? method_tag(method) : ''.html_safe
+ method = html_options.delete("method").to_s
+ method_tag = BUTTON_TAG_METHOD_VERBS.include?(method) ? method_tag(method) : "".html_safe
- form_method = method == 'get' ? 'get' : 'post'
- form_options = html_options.delete('form') || {}
- form_options[:class] ||= html_options.delete('form_class') || 'button_to'
+ form_method = method == "get" ? "get" : "post"
+ form_options = html_options.delete("form") || {}
+ form_options[:class] ||= html_options.delete("form_class") || "button_to"
form_options[:method] = form_method
form_options[:action] = url
form_options[:'data-remote'] = true if remote
- request_token_tag = form_method == 'post' ? token_tag : ''
+ request_token_tag = if form_method == "post"
+ request_method = method.empty? ? "post" : method
+ token_tag(nil, form_options: { action: url, method: request_method })
+ else
+ ""
+ end
html_options = convert_options_to_data_attributes(options, html_options)
- html_options['type'] = 'submit'
+ html_options["type"] = "submit"
button = if block_given?
- content_tag('button', html_options, &block)
+ content_tag("button", html_options, &block)
else
- html_options['value'] = name || url
- tag('input', html_options)
+ html_options["value"] = name || url
+ tag("input", html_options)
end
inner_tags = method_tag.safe_concat(button).safe_concat(request_token_tag)
if params
- params.each do |param_name, value|
- inner_tags.safe_concat tag(:input, type: "hidden", name: param_name, value: value.to_param)
+ to_form_params(params).each do |param|
+ inner_tags.safe_concat tag(:input, type: "hidden", name: param[:name], value: param[:value])
end
end
- content_tag('form', inner_tags, form_options)
+ content_tag("form", inner_tags, form_options)
end
# Creates a link tag of the given +name+ using a URL created by the set of
@@ -464,13 +485,14 @@ module ActionView
extras = %w{ cc bcc body subject reply_to }.map! { |item|
option = html_options.delete(item).presence || next
- "#{item.dasherize}=#{Rack::Utils.escape_path(option)}"
+ "#{item.dasherize}=#{ERB::Util.url_encode(option)}"
}.compact
- extras = extras.empty? ? '' : '?' + extras.join('&')
+ extras = extras.empty? ? "" : "?" + extras.join("&")
- html_options["href"] = "mailto:#{email_address}#{extras}"
+ encoded_email_address = ERB::Util.url_encode(email_address).gsub("%40", "@")
+ html_options["href"] = "mailto:#{encoded_email_address}#{extras}"
- content_tag(:a, name || email_address, html_options, &block)
+ content_tag("a", name || email_address, html_options, &block)
end
# True if the current request URI was generated by the given +options+.
@@ -502,6 +524,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
#
@@ -515,7 +540,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 " \
@@ -524,15 +549,22 @@ module ActionView
return false unless request.get? || request.head?
+ check_parameters ||= options.is_a?(Hash) && options.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)
- if url_string =~ /^\w+:\/\//
+ if url_string.start_with?("/") && url_string != "/"
+ url_string.chomp!("/")
+ request_uri.chomp!("/")
+ end
+
+ if %r{^\w+://}.match?(url_string)
url_string == "#{request.protocol}#{request.host_with_port}#{request_uri}"
else
url_string == request_uri
@@ -543,42 +575,101 @@ module ActionView
def convert_options_to_data_attributes(options, html_options)
if html_options
html_options = html_options.stringify_keys
- html_options['data-remote'] = 'true' if link_to_remote_options?(options) || link_to_remote_options?(html_options)
+ html_options["data-remote"] = "true" if link_to_remote_options?(options) || link_to_remote_options?(html_options)
- method = html_options.delete('method')
+ method = html_options.delete("method")
add_method_to_attributes!(html_options, method) if method
html_options
else
- link_to_remote_options?(options) ? {'data-remote' => 'true'} : {}
+ link_to_remote_options?(options) ? { "data-remote" => "true" } : {}
end
end
def link_to_remote_options?(options)
if options.is_a?(Hash)
- options.delete('remote') || options.delete(:remote)
+ options.delete("remote") || options.delete(:remote)
end
end
def add_method_to_attributes!(html_options, method)
- if method && method.to_s.downcase != "get" && html_options["rel"] !~ /nofollow/
- html_options["rel"] = "#{html_options["rel"]} nofollow".lstrip
+ if method_not_get_method?(method) && html_options["rel"] !~ /nofollow/
+ if html_options["rel"].blank?
+ html_options["rel"] = "nofollow"
+ else
+ html_options["rel"] = "#{html_options["rel"]} nofollow"
+ end
end
html_options["data-method"] = method
end
- def token_tag(token=nil)
+ STRINGIFIED_COMMON_METHODS = {
+ get: "get",
+ delete: "delete",
+ patch: "patch",
+ post: "post",
+ put: "put",
+ }.freeze
+
+ def method_not_get_method?(method)
+ return false unless method
+ (STRINGIFIED_COMMON_METHODS[method] || method.to_s.downcase) != "get"
+ end
+
+ def token_tag(token = nil, form_options: {})
if token != false && protect_against_forgery?
- token ||= form_authenticity_token
+ token ||= form_authenticity_token(form_options: form_options)
tag(:input, type: "hidden", name: request_forgery_protection_token.to_s, value: token)
else
- ''
+ ""
end
end
def method_tag(method)
- tag('input', type: 'hidden', name: '_method', value: method.to_s)
+ tag("input", type: "hidden", name: "_method", value: method.to_s)
+ end
+
+ # Returns an array of hashes each containing :name and :value keys
+ # suitable for use as the names and values of form input fields:
+ #
+ # to_form_params(name: 'David', nationality: 'Danish')
+ # # => [{name: 'name', value: 'David'}, {name: 'nationality', value: 'Danish'}]
+ #
+ # to_form_params(country: { name: 'Denmark' })
+ # # => [{name: 'country[name]', value: 'Denmark'}]
+ #
+ # to_form_params(countries: ['Denmark', 'Sweden']})
+ # # => [{name: 'countries[]', value: 'Denmark'}, {name: 'countries[]', value: 'Sweden'}]
+ #
+ # An optional namespace can be passed to enclose key names:
+ #
+ # to_form_params({ name: 'Denmark' }, 'country')
+ # # => [{name: 'country[name]', value: 'Denmark'}]
+ def to_form_params(attribute, namespace = nil)
+ attribute = if attribute.respond_to?(:permitted?)
+ attribute.to_h
+ else
+ attribute
+ end
+
+ params = []
+ case attribute
+ when Hash
+ attribute.each do |key, value|
+ prefix = namespace ? "#{namespace}[#{key}]" : key
+ params.push(*to_form_params(value, prefix))
+ end
+ when Array
+ array_prefix = "#{namespace}[]"
+ attribute.each do |value|
+ params.push(*to_form_params(value, array_prefix))
+ end
+ else
+ params << { name: namespace.to_s, value: attribute.to_param }
+ end
+
+ params.sort_by { |pair| pair[:name] }
end
end
end
diff --git a/actionview/lib/action_view/layouts.rb b/actionview/lib/action_view/layouts.rb
index 9d636c8c9e..3e6d352c15 100644
--- a/actionview/lib/action_view/layouts.rb
+++ b/actionview/lib/action_view/layouts.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
require "action_view/rendering"
-require "active_support/core_ext/module/remove_method"
+require "active_support/core_ext/module/redefine_method"
module ActionView
# Layouts reverse the common pattern of including shared headers and footers in many templates to isolate changes in
@@ -91,16 +93,16 @@ module ActionView
# layout false
#
# In these examples, we have three implicit lookup scenarios:
- # * The BankController uses the "bank" layout.
- # * The ExchangeController uses the "exchange" layout.
- # * The CurrencyController inherits the layout from BankController.
+ # * The +BankController+ uses the "bank" layout.
+ # * The +ExchangeController+ uses the "exchange" layout.
+ # * The +CurrencyController+ inherits the layout from BankController.
#
# However, when a layout is explicitly set, the explicitly set layout wins:
- # * The InformationController uses the "information" layout, explicitly set.
- # * The TellerController also uses the "information" layout, because the parent explicitly set it.
- # * The EmployeeController uses the "employee" layout, because it set the layout to nil, resetting the parent configuration.
- # * The VaultController chooses a layout dynamically by calling the <tt>access_level_layout</tt> method.
- # * The TillController does not use a layout at all.
+ # * The +InformationController+ uses the "information" layout, explicitly set.
+ # * The +TellerController+ also uses the "information" layout, because the parent explicitly set it.
+ # * The +EmployeeController+ uses the "employee" layout, because it set the layout to +nil+, resetting the parent configuration.
+ # * The +VaultController+ chooses a layout dynamically by calling the <tt>access_level_layout</tt> method.
+ # * The +TillController+ does not use a layout at all.
#
# == Types of layouts
#
@@ -148,8 +150,8 @@ module ActionView
# The template will be looked always in <tt>app/views/layouts/</tt> folder. But you can point
# <tt>layouts</tt> folder direct also. <tt>layout "layouts/demo"</tt> is the same as <tt>layout "demo"</tt>.
#
- # Setting the layout to nil forces it to be looked up in the filesystem and fallbacks to the parent behavior if none exists.
- # Setting it to nil is useful to re-enable template lookup overriding a previous configuration set in the parent:
+ # Setting the layout to +nil+ forces it to be looked up in the filesystem and fallbacks to the parent behavior if none exists.
+ # Setting it to +nil+ is useful to re-enable template lookup overriding a previous configuration set in the parent:
#
# class ApplicationController < ActionController::Base
# layout "application"
@@ -204,9 +206,9 @@ module ActionView
include ActionView::Rendering
included do
- class_attribute :_layout, :_layout_conditions, :instance_accessor => false
- self._layout = nil
- self._layout_conditions = {}
+ class_attribute :_layout, instance_accessor: false
+ class_attribute :_layout_conditions, instance_accessor: false, default: {}
+
_write_layout_method
end
@@ -223,36 +225,39 @@ module ActionView
module LayoutConditions # :nodoc:
private
- # Determines whether the current action has a layout definition by
- # checking the action name against the :only and :except conditions
- # set by the <tt>layout</tt> method.
- #
- # ==== Returns
- # * <tt>Boolean</tt> - True if the action has a layout definition, false otherwise.
- def _conditional_layout?
- return unless super
-
- conditions = _layout_conditions
-
- if only = conditions[:only]
- only.include?(action_name)
- elsif except = conditions[:except]
- !except.include?(action_name)
- else
- true
+ # Determines whether the current action has a layout definition by
+ # checking the action name against the :only and :except conditions
+ # set by the <tt>layout</tt> method.
+ #
+ # ==== Returns
+ # * <tt>Boolean</tt> - True if the action has a layout definition, false otherwise.
+ def _conditional_layout?
+ return unless super
+
+ conditions = _layout_conditions
+
+ if only = conditions[:only]
+ only.include?(action_name)
+ elsif except = conditions[:except]
+ !except.include?(action_name)
+ else
+ true
+ end
end
- end
end
# Specify the layout to use for this class.
#
# If the specified layout is a:
# String:: the String is the template name
- # Symbol:: call the method specified by the symbol, which will return the template name
+ # Symbol:: call the method specified by the symbol
+ # Proc:: call the passed Proc
# false:: There is no layout
# true:: raise an ArgumentError
# nil:: Force default layout behavior with inheritance
#
+ # Return value of +Proc+ and +Symbol+ arguments should be +String+, +false+, +true+ or +nil+
+ # with the same meaning as described above.
# ==== Parameters
# * <tt>layout</tt> - The layout to use.
#
@@ -262,7 +267,7 @@ module ActionView
def layout(layout, conditions = {})
include LayoutConditions unless conditions.empty?
- conditions.each {|k, v| conditions[k] = Array(v).map(&:to_s) }
+ conditions.each { |k, v| conditions[k] = Array(v).map(&:to_s) }
self._layout_conditions = conditions
self._layout = layout
@@ -274,10 +279,10 @@ module ActionView
# If a layout is not explicitly mentioned then look for a layout with the controller's name.
# if nothing is found then try same procedure to find super class's layout.
def _write_layout_method # :nodoc:
- remove_possible_method(:_layout)
+ silence_redefinition_of_method(:_layout)
- prefixes = _implied_layout_name =~ /\blayouts/ ? [] : ["layouts"]
- default_behavior = "lookup_context.find_all('#{_implied_layout_name}', #{prefixes.inspect}).first || super"
+ prefixes = /\blayouts/.match?(_implied_layout_name) ? [] : ["layouts"]
+ default_behavior = "lookup_context.find_all('#{_implied_layout_name}', #{prefixes.inspect}, false, [], { formats: formats }).first || super"
name_clause = if name
default_behavior
else
@@ -286,7 +291,8 @@ module ActionView
RUBY
end
- layout_definition = case _layout
+ layout_definition = \
+ case _layout
when String
_layout.inspect
when Symbol
@@ -313,10 +319,10 @@ module ActionView
raise ArgumentError, "Layouts must be specified as a String, Symbol, Proc, false, or nil"
when nil
name_clause
- end
+ end
- self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
- def _layout
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def _layout(formats)
if _conditional_layout?
#{layout_definition}
else
@@ -329,14 +335,14 @@ module ActionView
private
- # If no layout is supplied, look for a template named the return
- # value of this method.
- #
- # ==== Returns
- # * <tt>String</tt> - A template name
- def _implied_layout_name # :nodoc:
- controller_path
- end
+ # If no layout is supplied, look for a template named the return
+ # value of this method.
+ #
+ # ==== Returns
+ # * <tt>String</tt> - A template name
+ def _implied_layout_name
+ controller_path
+ end
end
def _normalize_options(options) # :nodoc:
@@ -372,7 +378,7 @@ module ActionView
end
# This will be overwritten by _write_layout_method
- def _layout; end
+ def _layout(*); end
# Determine the layout for a given name, taking into account the name type.
#
@@ -382,8 +388,8 @@ module ActionView
case name
when String then _normalize_layout(name)
when Proc then name
- when true then Proc.new { _default_layout(true) }
- when :default then Proc.new { _default_layout(false) }
+ when true then Proc.new { |formats| _default_layout(formats, true) }
+ when :default then Proc.new { |formats| _default_layout(formats, false) }
when false, nil then nil
else
raise ArgumentError,
@@ -399,14 +405,15 @@ module ActionView
# Optionally raises an exception if the layout could not be found.
#
# ==== Parameters
- # * <tt>require_layout</tt> - If set to true and layout is not found,
- # an ArgumentError exception is raised (defaults to false)
+ # * <tt>formats</tt> - The formats accepted to this layout
+ # * <tt>require_layout</tt> - If set to +true+ and layout is not found,
+ # an +ArgumentError+ exception is raised (defaults to +false+)
#
# ==== Returns
- # * <tt>template</tt> - The template object for the default layout (or nil)
- def _default_layout(require_layout = false)
+ # * <tt>template</tt> - The template object for the default layout (or +nil+)
+ def _default_layout(formats, require_layout = false)
begin
- value = _layout if action_has_layout?
+ value = _layout(formats) if action_has_layout?
rescue NameError => e
raise e, "Could not render layout: #{e.message}"
end
@@ -420,7 +427,7 @@ module ActionView
end
def _include_layout?(options)
- (options.keys & [:body, :text, :plain, :html, :inline, :partial]).empty? || options.key?(:layout)
+ (options.keys & [:body, :plain, :html, :inline, :partial]).empty? || options.key?(:layout)
end
end
end
diff --git a/actionview/lib/action_view/log_subscriber.rb b/actionview/lib/action_view/log_subscriber.rb
index 9047dbdd85..227f025385 100644
--- a/actionview/lib/action_view/log_subscriber.rb
+++ b/actionview/lib/action_view/log_subscriber.rb
@@ -1,4 +1,6 @@
-require 'active_support/log_subscriber'
+# frozen_string_literal: true
+
+require "active_support/log_subscriber"
module ActionView
# = Action View Log Subscriber
@@ -14,30 +16,80 @@ module ActionView
def render_template(event)
info do
- message = " Rendered #{from_rails_root(event.payload[:identifier])}"
+ message = +" Rendered #{from_rails_root(event.payload[:identifier])}"
message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout]
- message << " (#{event.duration.round(1)}ms)"
+ message << " (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})"
+ end
+ end
+
+ def render_partial(event)
+ info do
+ message = +" Rendered #{from_rails_root(event.payload[:identifier])}"
+ message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout]
+ message << " (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})"
+ message << " #{cache_message(event.payload)}" unless event.payload[:cache_hit].nil?
+ message
+ end
+ end
+
+ def render_collection(event)
+ identifier = event.payload[:identifier] || "templates"
+
+ info do
+ " Rendered collection of #{from_rails_root(identifier)}" \
+ " #{render_count(event.payload)} (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})"
+ end
+ end
+
+ def start(name, id, payload)
+ if name == "render_template.action_view"
+ log_rendering_start(payload)
end
+
+ super
end
- alias :render_partial :render_template
- alias :render_collection :render_template
def logger
ActionView::Base.logger
end
- protected
+ private
- EMPTY = ''
- def from_rails_root(string)
+ EMPTY = ""
+ 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) # :doc:
+ if payload[:cache_hits]
+ "[#{payload[:cache_hits]} / #{payload[:count]} cache hits]"
+ else
+ "[#{payload[:count]} times]"
+ end
+ end
+
+ def cache_message(payload) # :doc:
+ case payload[:cache_hit]
+ when :hit
+ "[cache hit]"
+ when :miss
+ "[cache miss]"
+ end
+ end
+
+ def log_rendering_start(payload)
+ info do
+ message = +" Rendering #{from_rails_root(payload[:identifier])}"
+ message << " within #{from_rails_root(payload[:layout])}" if payload[:layout]
+ message
+ end
+ end
end
end
diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb
index 817b30dd2f..af67ffa12d 100644
--- a/actionview/lib/action_view/lookup_context.rb
+++ b/actionview/lib/action_view/lookup_context.rb
@@ -1,27 +1,28 @@
-require 'thread_safe'
-require 'active_support/core_ext/module/remove_method'
-require 'active_support/core_ext/module/attribute_accessors'
-require 'action_view/template/resolver'
+# frozen_string_literal: true
+
+require "concurrent/map"
+require "active_support/core_ext/module/remove_method"
+require "active_support/core_ext/module/attribute_accessors"
+require "action_view/template/resolver"
module ActionView
# = Action View Lookup Context
#
- # <tt>LookupContext</tt> is the object responsible to hold all information required to lookup
- # templates, i.e. view paths and details. The LookupContext is also responsible to
- # generate a key, given to view paths, used in the resolver cache lookup. Since
- # this key is generated just once during the request, it speeds up all cache accesses.
+ # <tt>LookupContext</tt> is the object responsible for holding all information
+ # required for looking up templates, i.e. view paths and details.
+ # <tt>LookupContext</tt> is also responsible for generating a key, given to
+ # view paths, used in the resolver cache lookup. Since this key is generated
+ # only once during the request, it speeds up all cache accesses.
class LookupContext #:nodoc:
attr_accessor :prefixes, :rendered_format
- mattr_accessor :fallbacks
- @@fallbacks = FallbackFileSystemResolver.instances
+ mattr_accessor :fallbacks, default: FallbackFileSystemResolver.instances
- mattr_accessor :registered_details
- self.registered_details = []
+ mattr_accessor :registered_details, default: []
- def self.register_detail(name, options = {}, &block)
- self.registered_details << name
- initialize = registered_details.map { |n| "@details[:#{n}] = details[:#{n}] || default_#{n}" }
+ def self.register_detail(name, &block)
+ registered_details << name
+ Accessors::DEFAULT_PROCS[name] = block
Accessors.send :define_method, :"default_#{name}", &block
Accessors.module_eval <<-METHOD, __FILE__, __LINE__ + 1
@@ -33,16 +34,12 @@ module ActionView
value = value.present? ? Array(value) : default_#{name}
_set_detail(:#{name}, value) if value != @details[:#{name}]
end
-
- remove_possible_method :initialize_details
- def initialize_details(details)
- #{initialize.join("\n")}
- end
METHOD
end
# Holds accessors for the registered details.
module Accessors #:nodoc:
+ DEFAULT_PROCS = {}
end
register_detail(:locale) do
@@ -54,29 +51,27 @@ module ActionView
end
register_detail(:formats) { ActionView::Base.default_formats || [:html, :text, :js, :css, :xml, :json] }
register_detail(:variants) { [] }
- register_detail(:handlers){ Template::Handlers.extensions }
+ register_detail(:handlers) { Template::Handlers.extensions }
class DetailsKey #:nodoc:
alias :eql? :equal?
- alias :object_hash :hash
- attr_reader :hash
- @details_keys = ThreadSafe::Cache.new
+ @details_keys = Concurrent::Map.new
def self.get(details)
if details[:formats]
details = details.dup
- details[:formats] &= Mime::SET.symbols
+ details[:formats] &= Template::Types.symbols
end
- @details_keys[details] ||= new
+ @details_keys[details] ||= Concurrent::Map.new
end
def self.clear
@details_keys.clear
end
- def initialize
- @hash = object_hash
+ def self.digest_caches
+ @details_keys.values
end
end
@@ -98,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
@@ -122,6 +117,10 @@ module ActionView
end
alias :find_template :find
+ def find_file(name, prefixes = [], partial = false, keys = [], options = {})
+ @view_paths.find_file(*args_for_lookup(name, prefixes, partial, keys, options))
+ end
+
def find_all(name, prefixes = [], partial = false, keys = [], options = {})
@view_paths.find_all(*args_for_lookup(name, prefixes, partial, keys, options))
end
@@ -131,6 +130,11 @@ module ActionView
end
alias :template_exists? :exists?
+ def any?(name, prefixes = [], partial = false)
+ @view_paths.exists?(*args_for_any(name, prefixes, partial))
+ end
+ alias :any_templates? :any?
+
# Adds fallbacks to the view paths. Useful in cases when you are rendering
# a :file.
def with_fallbacks
@@ -145,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)
@@ -167,18 +171,44 @@ module ActionView
[user_details, details_key]
end
+ 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
+ @detail_args_for_any ||= begin
+ details = {}
+
+ registered_details.each do |k|
+ if k == :variants
+ details[k] = :any
+ else
+ details[k] = Accessors::DEFAULT_PROCS[k].call
+ end
+ end
+
+ if @cache
+ [details, DetailsKey.get(details)]
+ else
+ [details, nil]
+ end
+ end
+ end
+
# Support legacy foo.erb names even though we now ignore .erb
# as well as incorrectly putting part of the path in the template
# name instead of the prefix.
- def normalize_name(name, prefixes) #:nodoc:
+ def normalize_name(name, prefixes)
prefixes = prefixes.presence
- parts = name.to_s.split('/')
+ parts = name.to_s.split("/")
parts.shift if parts.first.empty?
- name = parts.pop
+ name = parts.pop
return name, prefixes || [""] if parts.empty?
- parts = parts.join('/')
+ parts = parts.join("/")
prefixes = prefixes ? prefixes.map { |p| "#{p}/#{parts}" } : [parts]
return name, prefixes
@@ -190,15 +220,27 @@ module ActionView
include ViewPaths
def initialize(view_paths, details = {}, prefixes = [])
- @details, @details_key = {}, nil
+ @details_key = nil
@cache = true
@prefixes = prefixes
@rendered_format = nil
+ @details = initialize_details({}, details)
self.view_paths = view_paths
- initialize_details(details)
end
+ def digest_cache
+ details_key
+ end
+
+ def initialize_details(target, details)
+ registered_details.each do |k|
+ target[k] = details[k] || Accessors::DEFAULT_PROCS[k].call
+ end
+ target
+ end
+ private :initialize_details
+
# Override formats= to expand ["*/*"] values and automatically
# add :html as fallback to :js.
def formats=(values)
@@ -228,21 +270,5 @@ module ActionView
super(default_locale)
end
-
- # Uses the first format in the formats array for layout lookup.
- def with_layout_format
- if formats.size == 1
- yield
- else
- old_formats = formats
- _set_detail(:formats, formats[0,1])
-
- begin
- yield
- ensure
- _set_detail(:formats, old_formats)
- end
- end
- end
end
end
diff --git a/actionview/lib/action_view/model_naming.rb b/actionview/lib/action_view/model_naming.rb
index b6ed13424e..23cca8d607 100644
--- a/actionview/lib/action_view/model_naming.rb
+++ b/actionview/lib/action_view/model_naming.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionView
module ModelNaming #:nodoc:
# Converts the given object to an ActiveModel compliant one.
diff --git a/actionview/lib/action_view/path_set.rb b/actionview/lib/action_view/path_set.rb
index 91ee2ea8f5..691b53e2da 100644
--- a/actionview/lib/action_view/path_set.rb
+++ b/actionview/lib/action_view/path_set.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionView #:nodoc:
# = Action View PathSet
#
@@ -46,32 +48,53 @@ module ActionView #:nodoc:
find_all(*args).first || raise(MissingTemplate.new(self, *args))
end
+ def find_file(path, prefixes = [], *args)
+ _find_all(path, prefixes, args, true).first || raise(MissingTemplate.new(self, path, prefixes, *args))
+ end
+
def find_all(path, prefixes = [], *args)
- prefixes = [prefixes] if String === prefixes
- prefixes.each do |prefix|
- paths.each do |resolver|
- templates = resolver.find_all(path, prefix, *args)
- return templates unless templates.empty?
- end
- end
- []
+ _find_all path, prefixes, args, false
end
def exists?(path, prefixes, *args)
find_all(path, prefixes, *args).any?
end
+ def find_all_with_query(query) # :nodoc:
+ paths.each do |resolver|
+ templates = resolver.find_all_with_query(query)
+ return templates unless templates.empty?
+ end
+
+ []
+ end
+
private
- def typecast(paths)
- paths.map do |path|
- case path
- when Pathname, String
- OptimizedFileSystemResolver.new path.to_s
- else
- path
+ def _find_all(path, prefixes, args, outside_app)
+ prefixes = [prefixes] if String === prefixes
+ prefixes.each do |prefix|
+ paths.each do |resolver|
+ if outside_app
+ templates = resolver.find_all_anywhere(path, prefix, *args)
+ else
+ templates = resolver.find_all(path, prefix, *args)
+ end
+ return templates unless templates.empty?
+ end
+ end
+ []
+ end
+
+ def typecast(paths)
+ paths.map do |path|
+ case path
+ when Pathname, String
+ OptimizedFileSystemResolver.new path.to_s
+ else
+ path
+ end
end
end
- end
end
end
diff --git a/actionview/lib/action_view/railtie.rb b/actionview/lib/action_view/railtie.rb
index 5dc7950d6b..12d06bf376 100644
--- a/actionview/lib/action_view/railtie.rb
+++ b/actionview/lib/action_view/railtie.rb
@@ -1,11 +1,16 @@
+# frozen_string_literal: true
+
require "action_view"
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.embed_authenticity_token_in_remote_forms = nil
+ config.action_view.debug_missing_translation = true
+ config.action_view.default_enforce_utf8 = nil
+ config.action_view.finalize_compiled_template_methods = true
config.eager_load_namespaces << ActionView
@@ -16,13 +21,45 @@ module ActionView
end
end
+ initializer "action_view.form_with_generates_remote_forms" do |app|
+ ActiveSupport.on_load(:action_view) do
+ form_with_generates_remote_forms = app.config.action_view.delete(:form_with_generates_remote_forms)
+ ActionView::Helpers::FormHelper.form_with_generates_remote_forms = form_with_generates_remote_forms
+ end
+ end
+
+ initializer "action_view.form_with_generates_ids" do |app|
+ ActiveSupport.on_load(:action_view) do
+ form_with_generates_ids = app.config.action_view.delete(:form_with_generates_ids)
+ unless form_with_generates_ids.nil?
+ ActionView::Helpers::FormHelper.form_with_generates_ids = form_with_generates_ids
+ end
+ end
+ end
+
+ initializer "action_view.default_enforce_utf8" do |app|
+ ActiveSupport.on_load(:action_view) do
+ default_enforce_utf8 = app.config.action_view.delete(:default_enforce_utf8)
+ unless default_enforce_utf8.nil?
+ ActionView::Helpers::FormTagHelper.default_enforce_utf8 = default_enforce_utf8
+ end
+ end
+ end
+
+ initializer "action_view.finalize_compiled_template_methods" do |app|
+ ActiveSupport.on_load(:action_view) do
+ ActionView::Template.finalize_compiled_template_methods =
+ app.config.action_view.delete(:finalize_compiled_template_methods)
+ end
+ end
+
initializer "action_view.logger" do
ActiveSupport.on_load(:action_view) { self.logger ||= Rails.logger }
end
initializer "action_view.set_configs" do |app|
ActiveSupport.on_load(:action_view) do
- app.config.action_view.each do |k,v|
+ app.config.action_view.each do |k, v|
send "#{k}=", v
end
end
@@ -36,9 +73,11 @@ module ActionView
end
end
- initializer "action_view.collection_caching" do |app|
- ActiveSupport.on_load(:action_controller) do
- PartialRenderer.collection_cache = app.config.action_controller.cache_store
+ initializer "action_view.per_request_digest_cache" do |app|
+ ActiveSupport.on_load(:action_view) do
+ unless ActionView::Resolver.caching?
+ app.executor.to_run ActionView::Digestor::PerExecutionDigestCacheExpiry
+ end
end
end
@@ -48,9 +87,13 @@ module ActionView
end
end
+ initializer "action_view.collection_caching", after: "action_controller.set_configs" do |app|
+ PartialRenderer.collection_cache = app.config.action_controller.cache_store
+ end
+
rake_tasks do |app|
unless app.config.api_only
- load "action_view/tasks/dependencies.rake"
+ load "action_view/tasks/cache_digests.rake"
end
end
end
diff --git a/actionview/lib/action_view/record_identifier.rb b/actionview/lib/action_view/record_identifier.rb
index 6c6e69101b..ee39b6050d 100644
--- a/actionview/lib/action_view/record_identifier.rb
+++ b/actionview/lib/action_view/record_identifier.rb
@@ -1,28 +1,31 @@
-require 'active_support/core_ext/module'
-require 'action_view/model_naming'
+# frozen_string_literal: true
+
+require "active_support/core_ext/module"
+require "action_view/model_naming"
module ActionView
# RecordIdentifier encapsulates methods used by various ActionView helpers
# to associate records with DOM elements.
#
- # Consider for example the following code that displays the body of a post:
+ # Consider for example the following code that form of post:
#
- # <%= div_for(post) do %>
- # <%= post.body %>
+ # <%= form_for(post) do |f| %>
+ # <%= f.text_field :body %>
# <% end %>
#
- # When +post+ is a new, unsaved ActiveRecord::Base intance, the resulting HTML
+ # When +post+ is a new, unsaved ActiveRecord::Base instance, the resulting HTML
# is:
#
- # <div id="new_post" class="post">
- # </div>
+ # <form class="new_post" id="new_post" action="/posts" accept-charset="UTF-8" method="post">
+ # <input type="text" name="post[body]" id="post_body" />
+ # </form>
#
# When +post+ is a persisted ActiveRecord::Base instance, the resulting HTML
# is:
#
- # <div id="post_42" class="post">
- # What a wonderful world!
- # </div>
+ # <form class="edit_post" id="edit_post_42" action="/posts/42" accept-charset="UTF-8" method="post">
+ # <input type="text" value="What a wonderful world!" name="post[body]" id="post_body" />
+ # </form>
#
# In both cases, the +id+ and +class+ of the wrapping DOM element are
# automatically generated, following naming conventions encapsulated by the
@@ -56,8 +59,8 @@ module ActionView
include ModelNaming
- JOIN = '_'.freeze
- NEW = 'new'.freeze
+ JOIN = "_"
+ NEW = "new"
# The DOM class convention is to use the singular form of an object or class.
#
@@ -91,7 +94,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.
@@ -101,7 +104,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 1f122f9bc6..20b2523cac 100644
--- a/actionview/lib/action_view/renderer/abstract_renderer.rb
+++ b/actionview/lib/action_view/renderer/abstract_renderer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionView
# This class defines the interface for a renderer. Each class that
# subclasses +AbstractRenderer+ is used by the base +Renderer+ class to
@@ -15,7 +17,7 @@ module ActionView
# that new object is called in turn. This abstracts the setup and rendering
# into a separate classes for partials and templates.
class AbstractRenderer #:nodoc:
- delegate :find_template, :template_exists?, :with_fallbacks, :with_layout_format, :formats, :to => :@lookup_context
+ delegate :find_template, :find_file, :template_exists?, :any_templates?, :with_fallbacks, :with_layout_format, :formats, to: :@lookup_context
def initialize(lookup_context)
@lookup_context = lookup_context
@@ -25,25 +27,29 @@ module ActionView
raise NotImplementedError
end
- protected
+ private
- def extract_details(options)
- @lookup_context.registered_details.each_with_object({}) do |key, details|
- value = options[key]
+ def extract_details(options) # :doc:
+ @lookup_context.registered_details.each_with_object({}) do |key, details|
+ value = options[key]
- details[key] = Array(value) if value
+ details[key] = Array(value) if value
+ end
end
- end
- def instrument(name, options={})
- ActiveSupport::Notifications.instrument("render_#{name}.action_view", options){ yield }
- end
+ def instrument(name, **options) # :doc:
+ options[:identifier] ||= (@template && @template.identifier) || @path
- def prepend_formats(formats)
- formats = Array(formats)
- return if formats.empty? || @lookup_context.html_fallback_for_js
+ ActiveSupport::Notifications.instrument("render_#{name}.action_view", options) do |payload|
+ yield payload
+ end
+ end
- @lookup_context.formats = formats | @lookup_context.formats
- end
+ def prepend_formats(formats) # :doc:
+ formats = Array(formats)
+ return if formats.empty? || @lookup_context.html_fallback_for_js
+
+ @lookup_context.formats = formats | @lookup_context.formats
+ end
end
end
diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb
index 780fdabbd1..cb850d75ee 100644
--- a/actionview/lib/action_view/renderer/partial_renderer.rb
+++ b/actionview/lib/action_view/renderer/partial_renderer.rb
@@ -1,5 +1,7 @@
-require 'action_view/renderer/partial_renderer/collection_caching'
-require 'thread_safe'
+# frozen_string_literal: true
+
+require "concurrent/map"
+require "action_view/renderer/partial_renderer/collection_caching"
module ActionView
class PartialIteration
@@ -50,12 +52,12 @@ module ActionView
# <%= render partial: "ad", locals: { ad: ad } %>
# <% end %>
#
- # This would first render "advertiser/_account.html.erb" with @buyer passed in as the local variable +account+, then
- # render "advertiser/_ad.html.erb" and pass the local variable +ad+ to the template for display.
+ # This would first render <tt>advertiser/_account.html.erb</tt> with <tt>@buyer</tt> passed in as the local variable +account+, then
+ # render <tt>advertiser/_ad.html.erb</tt> and pass the local variable +ad+ to the template for display.
#
# == The :as and :object options
#
- # By default <tt>ActionView::PartialRenderer</tt> doesn't have any local variables.
+ # By default ActionView::PartialRenderer doesn't have any local variables.
# The <tt>:object</tt> option can be used to pass an object to the partial. For instance:
#
# <%= render partial: "account", object: @buyer %>
@@ -83,7 +85,7 @@ module ActionView
#
# <%= render partial: "ad", collection: @advertisements %>
#
- # This will render "advertiser/_ad.html.erb" and pass the local variable +ad+ to the template for display. An
+ # This will render <tt>advertiser/_ad.html.erb</tt> and pass the local variable +ad+ to the template for display. An
# iteration object will automatically be made available to the template with a name of the form
# +partial_name_iteration+. The iteration object has knowledge about which index the current object has in
# the collection and the total size of the collection. The iteration object also has two convenience methods,
@@ -98,8 +100,8 @@ 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:
+ # 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 be displayed instead by using this form:
#
# <%= render(partial: "ad", collection: @advertisements) || "There's no ad to be displayed" %>
#
@@ -112,18 +114,18 @@ module ActionView
#
# <%= render partial: "advertisement/ad", locals: { ad: @advertisement } %>
#
- # This will render the partial "advertisement/_ad.html.erb" regardless of which controller this is being called from.
+ # This will render the partial <tt>advertisement/_ad.html.erb</tt> regardless of which controller this is being called from.
#
- # == \Rendering objects that respond to `to_partial_path`
+ # == \Rendering objects that respond to +to_partial_path+
#
# Instead of explicitly naming the location of a partial, you can also let PartialRenderer do the work
- # and pick the proper path by checking `to_partial_path` method.
+ # and pick the proper path by checking +to_partial_path+ method.
#
# # @account.to_partial_path returns 'accounts/account', so it can be used to replace:
# # <%= render partial: "accounts/account", locals: { account: @account} %>
# <%= render partial: @account %>
#
- # # @posts is an array of Post instances, so every post record returns 'posts/post' on `to_partial_path`,
+ # # @posts is an array of Post instances, so every post record returns 'posts/post' on +to_partial_path+,
# # that's why we can replace:
# # <%= render partial: "posts/post", collection: @posts %>
# <%= render partial: @posts %>
@@ -143,7 +145,7 @@ module ActionView
# # <%= render partial: "accounts/account", locals: { account: @account} %>
# <%= render @account %>
#
- # # @posts is an array of Post instances, so every post record returns 'posts/post' on `to_partial_path`,
+ # # @posts is an array of Post instances, so every post record returns 'posts/post' on +to_partial_path+,
# # that's why we can replace:
# # <%= render partial: "posts/post", collection: @posts %>
# <%= render @posts %>
@@ -283,8 +285,8 @@ module ActionView
class PartialRenderer < AbstractRenderer
include CollectionCaching
- PREFIXED_PARTIAL_NAMES = ThreadSafe::Cache.new do |h, k|
- h[k] = ThreadSafe::Cache.new
+ PREFIXED_PARTIAL_NAMES = Concurrent::Map.new do |h, k|
+ h[k] = Concurrent::Map.new
end
def initialize(*)
@@ -294,7 +296,7 @@ module ActionView
def render(context, options, block)
setup(context, options, block)
- identifier = (@template = find_partial) ? @template.identifier : @path
+ @template = find_partial
@lookup_context.rendered_format ||= begin
if @template && @template.formats.present?
@@ -305,245 +307,246 @@ module ActionView
end
if @collection
- instrument(:collection, :identifier => identifier || "collection", :count => @collection.size) do
- render_collection
- end
+ render_collection
else
- instrument(:partial, :identifier => identifier) do
- render_partial
- end
+ render_partial
end
end
private
- def render_collection
- return nil if @collection.blank?
+ def render_collection
+ instrument(:collection, count: @collection.size) do |payload|
+ return nil if @collection.blank?
+
+ if @options.key?(:spacer_template)
+ spacer = find_template(@options[:spacer_template], @locals.keys).render(@view, @locals)
+ end
- if @options.key?(:spacer_template)
- spacer = find_template(@options[:spacer_template], @locals.keys).render(@view, @locals)
+ cache_collection_render(payload) do
+ @template ? collection_with_template : collection_without_template
+ end.join(spacer).html_safe
+ end
end
- cache_collection_render do
- @template ? collection_with_template : collection_without_template
- end.join(spacer).html_safe
- end
+ def render_partial
+ instrument(:partial) do |payload|
+ view, locals, block = @view, @locals, @block
+ object, as = @object, @variable
- def render_partial
- view, locals, block = @view, @locals, @block
- object, as = @object, @variable
+ if !block && (layout = @options[:layout])
+ layout = find_template(layout.to_s, @template_keys)
+ end
- if !block && (layout = @options[:layout])
- layout = find_template(layout.to_s, @template_keys)
- end
+ object = locals[as] if object.nil? # Respect object when object is false
+ locals[as] = object if @has_object
- object ||= locals[as]
- locals[as] = object if @has_object
+ content = @template.render(view, locals) do |*name|
+ view._layout_for(*name, &block)
+ end
- content = @template.render(view, locals) do |*name|
- view._layout_for(*name, &block)
+ content = layout.render(view, locals) { content } if layout
+ payload[:cache_hit] = view.view_renderer.cache_hits[@template.virtual_path]
+ content
+ end
end
- content = layout.render(view, locals){ content } if layout
- content
- end
+ # Sets up instance variables needed for rendering a partial. This method
+ # finds the options and details and extracts them. The method also contains
+ # logic that handles the type of object passed in as the partial.
+ #
+ # If +options[:partial]+ is a string, then the <tt>@path</tt> instance variable is
+ # set to that string. Otherwise, the +options[:partial]+ object must
+ # respond to +to_partial_path+ in order to setup the path.
+ def setup(context, options, block)
+ @view = context
+ @options = options
+ @block = block
+
+ @locals = options[:locals] ? options[:locals].symbolize_keys : {}
+ @details = extract_details(options)
+
+ prepend_formats(options[:formats])
+
+ partial = options[:partial]
+
+ if String === partial
+ @has_object = options.key?(:object)
+ @object = options[:object]
+ @collection = collection_from_options
+ @path = partial
+ else
+ @has_object = true
+ @object = partial
+ @collection = collection_from_object || collection_from_options
+
+ if @collection
+ paths = @collection_data = @collection.map { |o| partial_path(o) }
+ @path = paths.uniq.one? ? paths.first : nil
+ else
+ @path = partial_path
+ end
+ end
- # Sets up instance variables needed for rendering a partial. This method
- # finds the options and details and extracts them. The method also contains
- # logic that handles the type of object passed in as the partial.
- #
- # If +options[:partial]+ is a string, then the +@path+ instance variable is
- # set to that string. Otherwise, the +options[:partial]+ object must
- # respond to +to_partial_path+ in order to setup the path.
- def setup(context, options, block)
- @view = context
- @options = options
- @block = block
-
- @locals = options[:locals] || {}
- @details = extract_details(options)
-
- prepend_formats(options[:formats])
-
- partial = options[:partial]
-
- if String === partial
- @has_object = options.key?(:object)
- @object = options[:object]
- @collection = collection_from_options
- @path = partial
- else
- @has_object = true
- @object = partial
- @collection = collection_from_object || collection_from_options
+ if as = options[:as]
+ raise_invalid_option_as(as) unless /\A[a-z_]\w*\z/.match?(as.to_s)
+ as = as.to_sym
+ end
- if @collection
- paths = @collection_data = @collection.map { |o| partial_path(o) }
- @path = paths.uniq.one? ? paths.first : nil
+ if @path
+ @variable, @variable_counter, @variable_iteration = retrieve_variable(@path, as)
+ @template_keys = retrieve_template_keys
else
- @path = partial_path
+ paths.map! { |path| retrieve_variable(path, as).unshift(path) }
end
+
+ self
end
- if as = options[:as]
- raise_invalid_option_as(as) unless as.to_s =~ /\A[a-z_]\w*\z/
- as = as.to_sym
+ def collection_from_options
+ if @options.key?(:collection)
+ collection = @options[:collection]
+ collection ? collection.to_a : []
+ end
end
- if @path
- @variable, @variable_counter, @variable_iteration = retrieve_variable(@path, as)
- @template_keys = retrieve_template_keys
- else
- paths.map! { |path| retrieve_variable(path, as).unshift(path) }
+ def collection_from_object
+ @object.to_ary if @object.respond_to?(:to_ary)
end
- self
- end
+ def find_partial
+ find_template(@path, @template_keys) if @path
+ end
- def collection_from_options
- if @options.key?(:collection)
- collection = @options[:collection]
- collection.respond_to?(:to_ary) ? collection.to_ary : []
+ def find_template(path, locals)
+ prefixes = path.include?(?/) ? [] : @lookup_context.prefixes
+ @lookup_context.find_template(path, prefixes, true, locals, @details)
end
- end
- def collection_from_object
- @object.to_ary if @object.respond_to?(:to_ary)
- end
+ def collection_with_template
+ view, locals, template = @view, @locals, @template
+ as, counter, iteration = @variable, @variable_counter, @variable_iteration
- def find_partial
- find_template(@path, @template_keys) if @path
- end
+ if layout = @options[:layout]
+ layout = find_template(layout, @template_keys)
+ end
- def find_template(path, locals)
- prefixes = path.include?(?/) ? [] : @lookup_context.prefixes
- @lookup_context.find_template(path, prefixes, true, locals, @details)
- end
+ partial_iteration = PartialIteration.new(@collection.size)
+ locals[iteration] = partial_iteration
- def collection_with_template
- view, locals, template = @view, @locals, @template
- as, counter, iteration = @variable, @variable_counter, @variable_iteration
+ @collection.map do |object|
+ locals[as] = object
+ locals[counter] = partial_iteration.index
- if layout = @options[:layout]
- layout = find_template(layout, @template_keys)
+ content = template.render(view, locals)
+ content = layout.render(view, locals) { content } if layout
+ partial_iteration.iterate!
+ content
+ end
end
- partial_iteration = PartialIteration.new(@collection.size)
- locals[iteration] = partial_iteration
+ def collection_without_template
+ view, locals, collection_data = @view, @locals, @collection_data
+ cache = {}
+ keys = @locals.keys
- @collection.map do |object|
- locals[as] = object
- locals[counter] = partial_iteration.index
+ partial_iteration = PartialIteration.new(@collection.size)
- content = template.render(view, locals)
- content = layout.render(view, locals) { content } if layout
- partial_iteration.iterate!
- content
- end
- end
+ @collection.map do |object|
+ index = partial_iteration.index
+ path, as, counter, iteration = collection_data[index]
- def collection_without_template
- view, locals, collection_data = @view, @locals, @collection_data
- cache = {}
- keys = @locals.keys
+ locals[as] = object
+ locals[counter] = index
+ locals[iteration] = partial_iteration
- partial_iteration = PartialIteration.new(@collection.size)
-
- @collection.map do |object|
- index = partial_iteration.index
- path, as, counter, iteration = collection_data[index]
-
- locals[as] = object
- locals[counter] = index
- locals[iteration] = partial_iteration
-
- template = (cache[path] ||= find_template(path, keys + [as, counter]))
- content = template.render(view, locals)
- partial_iteration.iterate!
- content
+ template = (cache[path] ||= find_template(path, keys + [as, counter, iteration]))
+ content = template.render(view, locals)
+ partial_iteration.iterate!
+ content
+ end
end
- end
- # Obtains the path to where the object's partial is located. If the object
- # responds to +to_partial_path+, then +to_partial_path+ will be called and
- # will provide the path. If the object does not respond to +to_partial_path+,
- # then an +ArgumentError+ is raised.
- #
- # If +prefix_partial_path_with_controller_namespace+ is true, then this
- # method will prefix the partial paths with a namespace.
- def partial_path(object = @object)
- object = object.to_model if object.respond_to?(:to_model)
-
- path = if object.respond_to?(:to_partial_path)
- object.to_partial_path
- else
- raise ArgumentError.new("'#{object.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.")
+ # Obtains the path to where the object's partial is located. If the object
+ # responds to +to_partial_path+, then +to_partial_path+ will be called and
+ # will provide the path. If the object does not respond to +to_partial_path+,
+ # then an +ArgumentError+ is raised.
+ #
+ # If +prefix_partial_path_with_controller_namespace+ is true, then this
+ # method will prefix the partial paths with a namespace.
+ def partial_path(object = @object)
+ object = object.to_model if object.respond_to?(:to_model)
+
+ path = if object.respond_to?(:to_partial_path)
+ object.to_partial_path
+ else
+ raise ArgumentError.new("'#{object.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.")
+ end
+
+ if @view.prefix_partial_path_with_controller_namespace
+ prefixed_partial_names[path] ||= merge_prefix_into_object_path(@context_prefix, path.dup)
+ else
+ path
+ end
end
- if @view.prefix_partial_path_with_controller_namespace
- prefixed_partial_names[path] ||= merge_prefix_into_object_path(@context_prefix, path.dup)
- else
- path
+ def prefixed_partial_names
+ @prefixed_partial_names ||= PREFIXED_PARTIAL_NAMES[@context_prefix]
end
- end
- def prefixed_partial_names
- @prefixed_partial_names ||= PREFIXED_PARTIAL_NAMES[@context_prefix]
- end
+ def merge_prefix_into_object_path(prefix, object_path)
+ if prefix.include?(?/) && object_path.include?(?/)
+ prefixes = []
+ prefix_array = File.dirname(prefix).split("/")
+ object_path_array = object_path.split("/")[0..-3] # skip model dir & partial
- def merge_prefix_into_object_path(prefix, object_path)
- if prefix.include?(?/) && object_path.include?(?/)
- prefixes = []
- prefix_array = File.dirname(prefix).split('/')
- object_path_array = object_path.split('/')[0..-3] # skip model dir & partial
+ prefix_array.each_with_index do |dir, index|
+ break if dir == object_path_array[index]
+ prefixes << dir
+ end
- prefix_array.each_with_index do |dir, index|
- break if dir == object_path_array[index]
- prefixes << dir
+ (prefixes << object_path).join("/")
+ else
+ object_path
end
-
- (prefixes << object_path).join("/")
- else
- object_path
end
- end
- def retrieve_template_keys
- keys = @locals.keys
- keys << @variable if @has_object || @collection
- if @collection
- keys << @variable_counter
- keys << @variable_iteration
+ def retrieve_template_keys
+ keys = @locals.keys
+ keys << @variable if @has_object || @collection
+ if @collection
+ keys << @variable_counter
+ keys << @variable_iteration
+ end
+ keys
end
- keys
- end
- def retrieve_variable(path, as)
- variable = as || begin
- base = path[-1] == "/" ? "" : File.basename(path)
- raise_invalid_identifier(path) unless base =~ /\A_?(.*)(?:\.\w+)*\z/
- $1.to_sym
- end
- if @collection
- variable_counter = :"#{variable}_counter"
- variable_iteration = :"#{variable}_iteration"
+ def retrieve_variable(path, as)
+ variable = as || begin
+ base = path[-1] == "/" ? "" : File.basename(path)
+ raise_invalid_identifier(path) unless base =~ /\A_?(.*?)(?:\.\w+)*\z/
+ $1.to_sym
+ end
+ if @collection
+ variable_counter = :"#{variable}_counter"
+ variable_iteration = :"#{variable}_iteration"
+ end
+ [variable, variable_counter, variable_iteration]
end
- [variable, variable_counter, variable_iteration]
- end
- IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " +
- "make sure your partial name starts with underscore."
+ 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, " +
- "and is followed by any combination of letters, numbers and underscores."
+ 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)
- raise ArgumentError.new(IDENTIFIER_ERROR_MESSAGE % (path))
- end
+ def raise_invalid_identifier(path)
+ raise ArgumentError.new(IDENTIFIER_ERROR_MESSAGE % (path))
+ end
- def raise_invalid_option_as(as)
- raise ArgumentError.new(OPTION_AS_ERROR_MESSAGE % (as))
- end
+ def raise_invalid_option_as(as)
+ raise ArgumentError.new(OPTION_AS_ERROR_MESSAGE % (as))
+ end
end
end
diff --git a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb
index 1147963882..5aa6f77902 100644
--- a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb
+++ b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb
@@ -1,4 +1,4 @@
-require 'active_support/core_ext/object/try'
+# frozen_string_literal: true
module ActionView
module CollectionCaching # :nodoc:
@@ -7,43 +7,52 @@ module ActionView
included do
# Fallback cache store if Action View is used without Rails.
# Otherwise overridden in Railtie to use Rails.cache.
- mattr_accessor(:collection_cache) { ActiveSupport::Cache::MemoryStore.new }
+ mattr_accessor :collection_cache, default: ActiveSupport::Cache::MemoryStore.new
end
private
- def cache_collection_render
- return yield unless cache_collection?
+ def cache_collection_render(instrumentation_payload)
+ return yield unless @options[:cached]
+ # Result is a hash with the key represents the
+ # key used for cache lookup and the value is the item
+ # on which the partial is being rendered
keyed_collection = collection_by_cache_keys
- partial_cache = collection_cache.read_multi(*keyed_collection.keys)
- @collection = keyed_collection.reject { |key, _| partial_cache.key?(key) }.values
- rendered_partials = @collection.any? ? yield.dup : []
+ # Pull all partials from cache
+ # Result is a hash, key matches the entry in
+ # `keyed_collection` where the cache was retrieved and the
+ # value is the value that was present in the cache
+ cached_partials = collection_cache.read_multi(*keyed_collection.keys)
+ instrumentation_payload[:cache_hits] = cached_partials.size
- fetch_or_cache_partial(partial_cache, order_by: keyed_collection.each_key) do
- rendered_partials.shift
- end
- end
+ # Extract the items for the keys that are not found
+ # Set the uncached values to instance variable @collection
+ # which is used by the caller
+ @collection = keyed_collection.reject { |key, _| cached_partials.key?(key) }.values
- def cache_collection?
- @options.fetch(:cache, automatic_cache_eligible?)
- end
-
- def automatic_cache_eligible?
- single_template_render? && !callable_cache_key? &&
- @template.eligible_for_collection_caching?(as: @options[:as])
- end
+ # If all elements are already in cache then
+ # rendered partials will be an empty array
+ #
+ # If the cache is missing elements then
+ # the block will be called against the remaining items
+ # in the @collection.
+ rendered_partials = @collection.empty? ? [] : yield
- def single_template_render?
- @template # Template is only set when a collection renders one template.
+ index = 0
+ fetch_or_cache_partial(cached_partials, order_by: keyed_collection.each_key) do
+ # This block is called once
+ # for every cache miss while preserving order.
+ rendered_partials[index].tap { index += 1 }
+ end
end
def callable_cache_key?
- @options[:cache].respond_to?(:call)
+ @options[:cached].respond_to?(:call)
end
def collection_by_cache_keys
- seed = callable_cache_key? ? @options[:cache] : ->(i) { i }
+ seed = callable_cache_key? ? @options[:cached] : ->(i) { i }
@collection.each_with_object({}) do |item, hash|
hash[expanded_cache_key(seed.call(item))] = item
@@ -51,17 +60,34 @@ module ActionView
end
def expanded_cache_key(key)
- key = @view.fragment_cache_key(@view.cache_fragment_name(key, virtual_path: @template.virtual_path))
+ key = @view.combined_fragment_cache_key(@view.cache_fragment_name(key, virtual_path: @template.virtual_path, digest_path: digest_path))
key.frozen? ? key.dup : key # #read_multi & #write may require mutability, Dalli 2.6.0.
end
- def fetch_or_cache_partial(cached_partials, order_by:)
- cache_options = @options[:cache_options] || @locals[:cache_options] || {}
+ def digest_path
+ @digest_path ||= @view.digest_path_from_virtual(@template.virtual_path)
+ end
- order_by.map do |key|
- cached_partials.fetch(key) do
+ # `order_by` is an enumerable object containing keys of the cache,
+ # all keys are passed in whether found already or not.
+ #
+ # `cached_partials` is a hash. If the value exists
+ # it represents the rendered partial from the cache
+ # otherwise `Hash#fetch` will take the value of its block.
+ #
+ # This method expects a block that will return the rendered
+ # partial. An example is to render all results
+ # for each element that was not found in the cache and store it as an array.
+ # Order it so that the first empty cache element in `cached_partials`
+ # corresponds to the first element in `rendered_partials`.
+ #
+ # If the partial is not already cached it will also be
+ # written back to the underlying cache store.
+ def fetch_or_cache_partial(cached_partials, order_by:)
+ order_by.map do |cache_key|
+ cached_partials.fetch(cache_key) do
yield.tap do |rendered_partial|
- collection_cache.write(key, rendered_partial, cache_options)
+ collection_cache.write(cache_key, rendered_partial)
end
end
end
diff --git a/actionview/lib/action_view/renderer/renderer.rb b/actionview/lib/action_view/renderer/renderer.rb
index 1bee35d80d..3f3a97529d 100644
--- a/actionview/lib/action_view/renderer/renderer.rb
+++ b/actionview/lib/action_view/renderer/renderer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionView
# This is the main entry point for rendering. It basically delegates
# to other objects like TemplateRenderer and PartialRenderer which
@@ -15,7 +17,7 @@ module ActionView
@lookup_context = lookup_context
end
- # Main render entry point shared by AV and AC.
+ # Main render entry point shared by Action View and Action Controller.
def render(context, options)
if options.key?(:partial)
render_partial(context, options)
@@ -46,5 +48,9 @@ module ActionView
def render_partial(context, options, &block) #:nodoc:
PartialRenderer.new(@lookup_context).render(context, options, block)
end
+
+ def cache_hits # :nodoc:
+ @cache_hits ||= {}
+ end
end
end
diff --git a/actionview/lib/action_view/renderer/streaming_template_renderer.rb b/actionview/lib/action_view/renderer/streaming_template_renderer.rb
index 3ab2cd36fc..bb9db21e32 100644
--- a/actionview/lib/action_view/renderer/streaming_template_renderer.rb
+++ b/actionview/lib/action_view/renderer/streaming_template_renderer.rb
@@ -1,10 +1,11 @@
-require 'fiber'
+# frozen_string_literal: true
+
+require "fiber"
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).
@@ -27,17 +28,16 @@ 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:
- logger = ActionView::Base.logger
- return unless logger
+ # This is the same logging logic as in ShowExceptions middleware.
+ def log_error(exception)
+ logger = ActionView::Base.logger
+ return unless logger
- message = "\n#{exception.class} (#{exception.message}):\n"
- message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
- message << " " << exception.backtrace.join("\n ")
- logger.fatal("#{message}\n\n")
- end
+ message = +"\n#{exception.class} (#{exception.message}):\n"
+ message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
+ message << " " << exception.backtrace.join("\n ")
+ logger.fatal("#{message}\n\n")
+ end
end
# For streaming, instead of rendering a given a template, we return a Body
@@ -47,7 +47,7 @@ module ActionView
return [super] unless layout_name && template.supports_streaming?
locals ||= {}
- layout = layout_name && find_layout(layout_name, locals.keys)
+ layout = layout_name && find_layout(layout_name, locals.keys, [formats.first])
Body.new do |buffer|
delayed_render(buffer, template, layout, @view, locals)
@@ -56,48 +56,50 @@ module ActionView
private
- def delayed_render(buffer, template, layout, view, locals)
- # Wrap the given buffer in the StreamingBuffer and pass it to the
- # underlying template handler. Now, every time something is concatenated
- # to the buffer, it is not appended to an array, but streamed straight
- # to the client.
- output = ActionView::StreamingBuffer.new(buffer)
- yielder = lambda { |*name| view._layout_for(*name) }
-
- instrument(:template, :identifier => template.identifier, :layout => layout.try(:virtual_path)) do
- fiber = Fiber.new do
- if layout
- layout.render(view, locals, output, &yielder)
- else
- # If you don't have a layout, just render the thing
- # and concatenate the final result. This is the same
- # as a layout with just <%= yield %>
- output.safe_concat view._layout_for
+ def delayed_render(buffer, template, layout, view, locals)
+ # Wrap the given buffer in the StreamingBuffer and pass it to the
+ # underlying template handler. Now, every time something is concatenated
+ # to the buffer, it is not appended to an array, but streamed straight
+ # to the client.
+ output = ActionView::StreamingBuffer.new(buffer)
+ yielder = lambda { |*name| view._layout_for(*name) }
+
+ instrument(:template, identifier: template.identifier, layout: layout.try(:virtual_path)) do
+ outer_config = I18n.config
+ fiber = Fiber.new do
+ I18n.config = outer_config
+ if layout
+ layout.render(view, locals, output, &yielder)
+ else
+ # If you don't have a layout, just render the thing
+ # and concatenate the final result. This is the same
+ # as a layout with just <%= yield %>
+ output.safe_concat view._layout_for
+ end
end
- end
- # Set the view flow to support streaming. It will be aware
- # when to stop rendering the layout because it needs to search
- # something in the template and vice-versa.
- view.view_flow = StreamingFlow.new(view, fiber)
+ # Set the view flow to support streaming. It will be aware
+ # when to stop rendering the layout because it needs to search
+ # something in the template and vice-versa.
+ view.view_flow = StreamingFlow.new(view, fiber)
- # Yo! Start the fiber!
- fiber.resume
+ # Yo! Start the fiber!
+ fiber.resume
- # If the fiber is still alive, it means we need something
- # from the template, so start rendering it. If not, it means
- # the layout exited without requiring anything from the template.
- if fiber.alive?
- content = template.render(view, locals, &yielder)
+ # If the fiber is still alive, it means we need something
+ # from the template, so start rendering it. If not, it means
+ # the layout exited without requiring anything from the template.
+ if fiber.alive?
+ content = template.render(view, locals, &yielder)
- # Once rendering the template is done, sets its content in the :layout key.
- view.view_flow.set(:layout, content)
+ # Once rendering the template is done, sets its content in the :layout key.
+ view.view_flow.set(:layout, content)
- # In case the layout continues yielding, we need to resume
- # the fiber until all yields are handled.
- fiber.resume while fiber.alive?
+ # In case the layout continues yielding, we need to resume
+ # the fiber until all yields are handled.
+ fiber.resume while fiber.alive?
+ end
end
end
- end
end
end
diff --git a/actionview/lib/action_view/renderer/template_renderer.rb b/actionview/lib/action_view/renderer/template_renderer.rb
index dbb4855e39..ce8908924a 100644
--- a/actionview/lib/action_view/renderer/template_renderer.rb
+++ b/actionview/lib/action_view/renderer/template_renderer.rb
@@ -1,4 +1,6 @@
-require 'active_support/core_ext/object/try'
+# frozen_string_literal: true
+
+require "active_support/core_ext/object/try"
module ActionView
class TemplateRenderer < AbstractRenderer #:nodoc:
@@ -16,86 +18,85 @@ module ActionView
private
- # Determine the template to be rendered using the given options.
- def determine_template(options)
- keys = options.has_key?(:locals) ? options[:locals].keys : []
+ # Determine the template to be rendered using the given options.
+ def determine_template(options)
+ keys = options.has_key?(:locals) ? options[:locals].keys : []
- if options.key?(:body)
- Template::Text.new(options[:body])
- elsif options.key?(:text)
- Template::Text.new(options[:text], formats.first)
- elsif options.key?(:plain)
- Template::Text.new(options[:plain])
- elsif options.key?(:html)
- Template::HTML.new(options[:html], formats.first)
- elsif options.key?(:file)
- with_fallbacks { find_template(options[:file], nil, false, keys, @details) }
- elsif options.key?(:inline)
- handler = Template.handler_for_extension(options[:type] || "erb")
- Template.new(options[:inline], "inline template", handler, :locals => keys)
- elsif options.key?(:template)
- if options[:template].respond_to?(:render)
- options[:template]
+ if options.key?(:body)
+ Template::Text.new(options[:body])
+ elsif options.key?(:plain)
+ Template::Text.new(options[:plain])
+ elsif options.key?(:html)
+ Template::HTML.new(options[:html], formats.first)
+ elsif options.key?(:file)
+ with_fallbacks { find_file(options[:file], nil, false, keys, @details) }
+ elsif options.key?(:inline)
+ handler = Template.handler_for_extension(options[:type] || "erb")
+ Template.new(options[:inline], "inline template", handler, locals: keys)
+ elsif options.key?(:template)
+ if options[:template].respond_to?(:render)
+ options[:template]
+ else
+ find_template(options[:template], options[:prefixes], false, keys, @details)
+ end
else
- find_template(options[:template], options[:prefixes], false, keys, @details)
+ raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file, :plain, :html or :body option."
end
- else
- raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file, :plain, :html, :text or :body option."
end
- end
- # Renders the given template. A string representing the layout can be
- # supplied as well.
- def render_template(template, layout_name = nil, locals = nil) #:nodoc:
- view, locals = @view, locals || {}
+ # Renders the given template. A string representing the layout can be
+ # supplied as well.
+ def render_template(template, layout_name = nil, locals = nil)
+ view, locals = @view, locals || {}
- render_with_layout(layout_name, locals) do |layout|
- instrument(:template, :identifier => template.identifier, :layout => layout.try(:virtual_path)) do
- template.render(view, locals) { |*name| view._layout_for(*name) }
+ render_with_layout(layout_name, locals) do |layout|
+ instrument(:template, identifier: template.identifier, layout: layout.try(:virtual_path)) do
+ template.render(view, locals) { |*name| view._layout_for(*name) }
+ end
end
end
- end
- def render_with_layout(path, locals) #:nodoc:
- layout = path && find_layout(path, locals.keys)
- content = yield(layout)
+ def render_with_layout(path, locals)
+ layout = path && find_layout(path, locals.keys, [formats.first])
+ content = yield(layout)
- if layout
- view = @view
- view.view_flow.set(:layout, content)
- layout.render(view, locals){ |*name| view._layout_for(*name) }
- else
- content
+ if layout
+ view = @view
+ view.view_flow.set(:layout, content)
+ layout.render(view, locals) { |*name| view._layout_for(*name) }
+ else
+ content
+ end
end
- end
- # This is the method which actually finds the layout using details in the lookup
- # context object. If no layout is found, it checks if at least a layout with
- # the given name exists across all details before raising the error.
- def find_layout(layout, keys)
- with_layout_format { resolve_layout(layout, keys) }
- end
+ # This is the method which actually finds the layout using details in the lookup
+ # context object. If no layout is found, it checks if at least a layout with
+ # the given name exists across all details before raising the error.
+ def find_layout(layout, keys, formats)
+ resolve_layout(layout, keys, formats)
+ end
- def resolve_layout(layout, keys)
- case layout
- when String
- begin
- if layout =~ /^\//
- with_fallbacks { find_template(layout, nil, false, keys, @details) }
- else
- find_template(layout, nil, false, keys, @details)
+ def resolve_layout(layout, keys, formats)
+ details = @details.dup
+ details[:formats] = formats
+
+ case layout
+ when String
+ begin
+ if layout.start_with?("/")
+ with_fallbacks { find_template(layout, nil, false, [], details) }
+ else
+ find_template(layout, nil, false, [], details)
+ end
+ rescue ActionView::MissingTemplate
+ all_details = @details.merge(formats: @lookup_context.default_formats)
+ raise unless template_exists?(layout, nil, false, [], all_details)
end
- rescue ActionView::MissingTemplate
- all_details = @details.merge(:formats => @lookup_context.default_formats)
- raise unless template_exists?(layout, nil, false, keys, all_details)
+ when Proc
+ resolve_layout(layout.call(formats), keys, formats)
+ else
+ layout
end
- when Proc
- resolve_layout(layout.call, keys)
- when FalseClass
- nil
- else
- layout
end
- end
end
end
diff --git a/actionview/lib/action_view/rendering.rb b/actionview/lib/action_view/rendering.rb
index 91b7e845d6..4e5fdfbb2d 100644
--- a/actionview/lib/action_view/rendering.rb
+++ b/actionview/lib/action_view/rendering.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "action_view/view_paths"
module ActionView
@@ -73,8 +75,7 @@ module ActionView
end
# Returns an object that is able to render templates.
- # :api: private
- def view_renderer
+ def view_renderer # :nodoc:
@_view_renderer ||= ActionView::Renderer.new(lookup_context)
end
@@ -84,14 +85,13 @@ module ActionView
end
def rendered_format
- Mime[lookup_context.rendered_format]
+ Template::Types[lookup_context.rendered_format]
end
private
# 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
@@ -103,8 +103,8 @@ module ActionView
view_renderer.render(context, options)
end
- # Assign the rendered format to lookup context.
- def _process_format(format, options = {}) #:nodoc:
+ # Assign the rendered format to look up context.
+ def _process_format(format)
super
lookup_context.formats = [format.to_sym]
lookup_context.rendered_format = lookup_context.formats.first
@@ -112,8 +112,7 @@ module ActionView
# Normalize args by converting render "foo" to render :action => "foo" and
# render "foo/bar" to render :template => "foo/bar".
- # :api: private
- def _normalize_args(action=nil, options={})
+ def _normalize_args(action = nil, options = {})
options = super(action, options)
case action
when NilClass
@@ -124,14 +123,17 @@ 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
end
# Normalize options.
- # :api: private
def _normalize_options(options)
options = super(options)
if options[:partial] == true
diff --git a/actionview/lib/action_view/routing_url_for.rb b/actionview/lib/action_view/routing_url_for.rb
index 0371db07dc..f8ea3aa770 100644
--- a/actionview/lib/action_view/routing_url_for.rb
+++ b/actionview/lib/action_view/routing_url_for.rb
@@ -1,8 +1,9 @@
-require 'action_dispatch/routing/polymorphic_routes'
+# frozen_string_literal: true
+
+require "action_dispatch/routing/polymorphic_routes"
module ActionView
module RoutingUrlFor
-
# Returns the URL for the set of +options+ provided. This takes the
# same options as +url_for+ in Action Controller (see the
# documentation for <tt>ActionController::Base#url_for</tt>). Note that by default
@@ -32,7 +33,7 @@ module ActionView
#
# ==== Examples
# <%= url_for(action: 'index') %>
- # # => /blog/
+ # # => /blogs/
#
# <%= url_for(action: 'find', controller: 'books') %>
# # => /books/find
@@ -83,22 +84,24 @@ module ActionView
super(only_path: _generate_paths_by_default)
when Hash
options = options.symbolize_keys
- unless options.key?(:only_path)
- if options[:host].nil?
- options[:only_path] = _generate_paths_by_default
- else
- options[:only_path] = false
- end
- end
+ ensure_only_path_option(options)
+
+ super(options)
+ when ActionController::Parameters
+ ensure_only_path_option(options)
super(options)
when :back
_back_url
when Array
- if _generate_paths_by_default
- polymorphic_path(options, options.extract_options!)
+ components = options.dup
+ options = components.extract_options!
+ ensure_only_path_option(options)
+
+ if options[:only_path]
+ polymorphic_path(components, options)
else
- polymorphic_url(options, options.extract_options!)
+ polymorphic_url(components, options)
end
else
method = _generate_paths_by_default ? :path : :url
@@ -120,21 +123,24 @@ 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
end
+
+ def ensure_only_path_option(options)
+ unless options.key?(:only_path)
+ options[:only_path] = _generate_paths_by_default unless options[:host]
+ end
+ end
end
end
diff --git a/actionview/lib/action_view/tasks/cache_digests.rake b/actionview/lib/action_view/tasks/cache_digests.rake
new file mode 100644
index 0000000000..dd8e94bd88
--- /dev/null
+++ b/actionview/lib/action_view/tasks/cache_digests.rake
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+namespace :cache_digests do
+ desc "Lookup nested dependencies for TEMPLATE (like messages/show or comments/_comment.html)"
+ task nested_dependencies: :environment do
+ abort "You must provide TEMPLATE for the task to run" unless ENV["TEMPLATE"].present?
+ puts JSON.pretty_generate ActionView::Digestor.tree(CacheDigests.template_name, CacheDigests.finder).children.map(&:to_dep_map)
+ end
+
+ desc "Lookup first-level dependencies for TEMPLATE (like messages/show or comments/_comment.html)"
+ task dependencies: :environment do
+ abort "You must provide TEMPLATE for the task to run" unless ENV["TEMPLATE"].present?
+ puts JSON.pretty_generate ActionView::Digestor.tree(CacheDigests.template_name, CacheDigests.finder).children.map(&:name)
+ end
+
+ class CacheDigests
+ def self.template_name
+ ENV["TEMPLATE"].split(".", 2).first
+ end
+
+ def self.finder
+ ApplicationController.new.lookup_context
+ end
+ end
+end
diff --git a/actionview/lib/action_view/tasks/dependencies.rake b/actionview/lib/action_view/tasks/dependencies.rake
deleted file mode 100644
index f394c319c1..0000000000
--- a/actionview/lib/action_view/tasks/dependencies.rake
+++ /dev/null
@@ -1,23 +0,0 @@
-namespace :cache_digests do
- desc 'Lookup nested dependencies for TEMPLATE (like messages/show or comments/_comment.html)'
- task :nested_dependencies => :environment do
- abort 'You must provide TEMPLATE for the task to run' unless ENV['TEMPLATE'].present?
- puts JSON.pretty_generate ActionView::Digestor.new(name: CacheDigests.template_name, finder: CacheDigests.finder).nested_dependencies
- end
-
- desc 'Lookup first-level dependencies for TEMPLATE (like messages/show or comments/_comment.html)'
- task :dependencies => :environment do
- abort 'You must provide TEMPLATE for the task to run' unless ENV['TEMPLATE'].present?
- puts JSON.pretty_generate ActionView::Digestor.new(name: CacheDigests.template_name, finder: CacheDigests.finder).dependencies
- end
-
- class CacheDigests
- def self.template_name
- ENV['TEMPLATE'].split('.', 2).first
- end
-
- def self.finder
- ApplicationController.new.lookup_context
- end
- end
-end
diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb
index d8585514d5..070d82cf17 100644
--- a/actionview/lib/action_view/template.rb
+++ b/actionview/lib/action_view/template.rb
@@ -1,12 +1,16 @@
-require 'active_support/core_ext/object/try'
-require 'active_support/core_ext/kernel/singleton_class'
-require 'thread'
+# frozen_string_literal: true
+
+require "active_support/core_ext/object/try"
+require "active_support/core_ext/kernel/singleton_class"
+require "thread"
module ActionView
# = Action View Template
class Template
extend ActiveSupport::Autoload
+ mattr_accessor :finalize_compiled_template_methods, default: true
+
# === Encodings in ActionView::Template
#
# ActionView::Template is one of a few sources of potential
@@ -65,8 +69,7 @@ module ActionView
# If you want to provide an alternate mechanism for
# specifying encodings (like ERB does via <%# encoding: ... %>),
# you may indicate that you will handle encodings yourself
- # by implementing <tt>self.handles_encoding?</tt>
- # on your handler.
+ # by implementing <tt>handles_encoding?</tt> on your handler.
#
# If you do, Rails will not try to encode the String
# into the default_internal, passing you the unaltered
@@ -130,7 +133,6 @@ module ActionView
@source = source
@identifier = identifier
@handler = handler
- @cache_name = extract_resource_cache_name
@compiled = false
@original_encoding = nil
@locals = details[:locals] || []
@@ -141,8 +143,8 @@ module ActionView
@compile_mutex = Mutex.new
end
- # Returns if the underlying handler supports streaming. If so,
- # a streaming buffer *may* be passed when it start rendering.
+ # Returns whether the underlying handler supports streaming. If so,
+ # a streaming buffer *may* be passed when it starts rendering.
def supports_streaming?
handler.respond_to?(:supports_streaming?) && handler.supports_streaming?
end
@@ -153,8 +155,8 @@ module ActionView
# This method is instrumented as "!render_template.action_view". Notice that
# we use a bang in this instrumentation because you don't want to
# consume this in production. This is only slow if it's being listened to.
- def render(view, locals, buffer=nil, &block)
- instrument("!render_template") do
+ def render(view, locals, buffer = nil, &block)
+ instrument_render_template do
compile!(view)
view.send(method_name, locals, buffer, &block)
end
@@ -166,10 +168,6 @@ module ActionView
@type ||= Types[@formats.first] if @formats.first
end
- def eligible_for_collection_caching?(as: nil)
- @cache_name == (as || inferred_cache_name).to_s
- end
-
# Receives a view object and return a template similar to self by using @virtual_path.
#
# This method is useful if you have a template object but it does not contain its source
@@ -185,12 +183,12 @@ module ActionView
name = pieces.pop
partial = !!name.sub!(/^_/, "")
lookup.disable_cache do
- lookup.find_template(name, [ pieces.join('/') ], partial, @locals)
+ lookup.find_template(name, [ pieces.join("/") ], partial, @locals)
end
end
def inspect
- @inspect ||= defined?(Rails.root) ? identifier.sub("#{Rails.root}/", '') : identifier
+ @inspect ||= defined?(Rails.root) ? identifier.sub("#{Rails.root}/", "") : identifier
end
# This method is responsible for properly setting the encoding of the
@@ -209,7 +207,7 @@ module ActionView
# Look for # encoding: *. If we find one, we'll encode the
# String in that encoding, otherwise, we'll use the
# default external encoding.
- if source.sub!(/\A#{ENCODING_FLAG}/, '')
+ if source.sub!(/\A#{ENCODING_FLAG}/, "")
encoding = magic_encoding = $1
else
encoding = Encoding.default_external
@@ -237,11 +235,24 @@ module ActionView
end
end
- protected
+
+ # Exceptions are marshalled when using the parallel test runner with DRb, so we need
+ # to ensure that references to the template object can be marshalled as well. This means forgoing
+ # the marshalling of the compiler mutex and instantiating that again on unmarshalling.
+ def marshal_dump # :nodoc:
+ [ @source, @identifier, @handler, @compiled, @original_encoding, @locals, @virtual_path, @updated_at, @formats, @variants ]
+ end
+
+ def marshal_load(array) # :nodoc:
+ @source, @identifier, @handler, @compiled, @original_encoding, @locals, @virtual_path, @updated_at, @formats, @variants = *array
+ @compile_mutex = Mutex.new
+ end
+
+ 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
@@ -282,14 +293,13 @@ 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
# encoding of the code
- source = <<-end_src
+ source = +<<-end_src
def #{method_name}(local_assigns, output_buffer)
_old_virtual_path, @virtual_path = @virtual_path, #{@virtual_path.inspect};_old_output_buffer = @output_buffer;#{locals_code};#{code}
ensure
@@ -312,10 +322,12 @@ module ActionView
end
mod.module_eval(source, identifier, 0)
- ObjectSpace.define_finalizer(self, Finalizer[method_name, mod])
+ if finalize_compiled_template_methods
+ ObjectSpace.define_finalizer(self, Finalizer[method_name, mod])
+ end
end
- def handle_render_error(view, e) #:nodoc:
+ def handle_render_error(view, e)
if e.is_a?(Template::Error)
e.sub_template_of(self)
raise e
@@ -325,48 +337,42 @@ module ActionView
template = refresh(view)
template.encode!
end
- raise Template::Error.new(template, e)
+ raise Template::Error.new(template)
end
end
- def locals_code #:nodoc:
- # Double assign to suppress the dreaded 'assigned but unused variable' warning
- @locals.each_with_object('') { |key, code| code << "#{key} = #{key} = local_assigns[:#{key}];" }
+ def locals_code
+ # Only locals with valid variable names get set directly. Others will
+ # still be available in local_assigns.
+ locals = @locals - Module::RUBY_RESERVED_KEYWORDS
+ locals = locals.grep(/\A@?(?![A-Z0-9])(?:[[:alnum:]_]|[^\0-\177])+\z/)
+
+ # Assign for the same variable is to suppress unused variable warning
+ locals.each_with_object(+"") { |key, code| code << "#{key} = local_assigns[:#{key}]; #{key} = #{key};" }
end
- def method_name #:nodoc:
+ def method_name
@method_name ||= begin
- m = "_#{identifier_method_name}__#{@identifier.hash}_#{__id__}"
- m.tr!('-', '_')
+ m = +"_#{identifier_method_name}__#{@identifier.hash}_#{__id__}"
+ m.tr!("-", "_")
m
end
end
- def identifier_method_name #:nodoc:
- inspect.tr('^a-z_', '_')
- end
-
- def instrument(action, &block)
- payload = { virtual_path: @virtual_path, identifier: @identifier }
- ActiveSupport::Notifications.instrument("#{action}.action_view", payload, &block)
+ def identifier_method_name
+ inspect.tr("^a-z_", "_")
end
- EXPLICIT_COLLECTION = /# Template Collection: (?<resource_name>\w+)/
-
- def extract_resource_cache_name
- if match = @source.match(EXPLICIT_COLLECTION) || resource_cache_call_match
- match[:resource_name]
- end
+ def instrument(action, &block) # :doc:
+ ActiveSupport::Notifications.instrument("#{action}.action_view", instrument_payload, &block)
end
- def resource_cache_call_match
- if @handler.respond_to?(:resource_cache_call_pattern)
- @source.match(@handler.resource_cache_call_pattern)
- end
+ def instrument_render_template(&block)
+ ActiveSupport::Notifications.instrument("!render_template.action_view", instrument_payload, &block)
end
- def inferred_cache_name
- @inferred_cache_name ||= @virtual_path.split('/').last.sub('_', '')
+ def instrument_payload
+ { virtual_path: @virtual_path, identifier: @identifier }
end
end
end
diff --git a/actionview/lib/action_view/template/error.rb b/actionview/lib/action_view/template/error.rb
index 390bce98a2..4e3c02e05e 100644
--- a/actionview/lib/action_view/template/error.rb
+++ b/actionview/lib/action_view/template/error.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "active_support/core_ext/enumerable"
module ActionView
@@ -8,9 +10,6 @@ module ActionView
class EncodingError < StandardError #:nodoc:
end
- class MissingRequestError < StandardError #:nodoc:
- end
-
class WrongEncodingError < EncodingError #:nodoc:
def initialize(string, encoding)
@string, @encoding = string, encoding
@@ -35,10 +34,10 @@ module ActionView
prefixes = Array(prefixes)
template_type = if partial
"partial"
- elsif path =~ /layouts/i
- 'layout'
+ elsif /layouts/i.match?(path)
+ "layout"
else
- 'template'
+ "template"
end
if partial && path.present?
@@ -59,13 +58,14 @@ module ActionView
class Error < ActionViewError #:nodoc:
SOURCE_CODE_RADIUS = 3
- attr_reader :original_exception
+ # Override to prevent #cause resetting during re-raise.
+ attr_reader :cause
- def initialize(template, original_exception)
- super(original_exception.message)
- @template, @original_exception = template, original_exception
- @sub_templates = nil
- set_backtrace(original_exception.backtrace)
+ def initialize(template)
+ super($!.message)
+ set_backtrace($!.backtrace)
+ @cause = $!
+ @template, @sub_templates = template, nil
end
def file_name
@@ -119,18 +119,18 @@ module ActionView
if line_number
"on line ##{line_number} of "
else
- 'in '
+ "in "
end + file_name
end
def formatted_code_for(source_code, line_counter, indent, output)
- start_value = (output == :html) ? {} : ""
+ start_value = (output == :html) ? {} : []
source_code.inject(start_value) do |result, line|
line_counter += 1
if output == :html
result.update(line_counter.to_s => "%#{indent}s %s\n" % ["", line])
else
- result << "%#{indent}s: %s\n" % [line_counter, line]
+ result << "%#{indent}s: %s" % [line_counter, line]
end
end
end
diff --git a/actionview/lib/action_view/template/handlers.rb b/actionview/lib/action_view/template/handlers.rb
index 0105e88a49..7ec76dcc3f 100644
--- a/actionview/lib/action_view/template/handlers.rb
+++ b/actionview/lib/action_view/template/handlers.rb
@@ -1,14 +1,18 @@
+# frozen_string_literal: true
+
module ActionView #:nodoc:
# = Action View Template Handlers
- class Template
+ class Template #:nodoc:
module Handlers #:nodoc:
- autoload :ERB, 'action_view/template/handlers/erb'
- autoload :Builder, 'action_view/template/handlers/builder'
- autoload :Raw, 'action_view/template/handlers/raw'
+ autoload :Raw, "action_view/template/handlers/raw"
+ autoload :ERB, "action_view/template/handlers/erb"
+ autoload :Html, "action_view/template/handlers/html"
+ autoload :Builder, "action_view/template/handlers/builder"
def self.extended(base)
base.register_default_template_handler :raw, Raw.new
base.register_template_handler :erb, ERB.new
+ base.register_template_handler :html, Html.new
base.register_template_handler :builder, Builder.new
base.register_template_handler :ruby, :source.to_proc
end
diff --git a/actionview/lib/action_view/template/handlers/builder.rb b/actionview/lib/action_view/template/handlers/builder.rb
index d90b0c6378..61492ce448 100644
--- a/actionview/lib/action_view/template/handlers/builder.rb
+++ b/actionview/lib/action_view/template/handlers/builder.rb
@@ -1,26 +1,25 @@
+# frozen_string_literal: true
+
module ActionView
module Template::Handlers
class Builder
- # Default format used by Builder.
- class_attribute :default_format
- self.default_format = :xml
+ class_attribute :default_format, default: :xml
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
-
- def require_engine
- @required ||= begin
- require "builder"
- true
+ private
+ def require_engine # :doc:
+ @required ||= begin
+ require "builder"
+ true
+ end
end
- end
end
end
end
diff --git a/actionview/lib/action_view/template/handlers/erb.rb b/actionview/lib/action_view/template/handlers/erb.rb
index 1f8459c24b..270be0a380 100644
--- a/actionview/lib/action_view/template/handlers/erb.rb
+++ b/actionview/lib/action_view/template/handlers/erb.rb
@@ -1,91 +1,30 @@
-require 'erubis'
+# frozen_string_literal: true
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 code =~ BLOCK_EXPR
- 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 code =~ BLOCK_EXPR
- 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
-
class ERB
+ autoload :Erubi, "action_view/template/handlers/erb/erubi"
+
# Specify trim mode for the ERB compiler. Defaults to '-'.
# See ERB documentation for suitable values.
- class_attribute :erb_trim_mode
- self.erb_trim_mode = '-'
+ class_attribute :erb_trim_mode, default: "-"
# Default implementation used.
- class_attribute :erb_implementation
- self.erb_implementation = Erubis
+ class_attribute :erb_implementation, default: Erubi
# Do not escape templates of these mime types.
- class_attribute :escape_whitelist
- self.escape_whitelist = ["text/plain"]
+ class_attribute :escape_ignore_list, default: ["text/plain"]
+
+ [self, singleton_class].each do |base|
+ base.send(:alias_method, :escape_whitelist, :escape_ignore_list)
+ base.send(:alias_method, :escape_whitelist=, :escape_ignore_list=)
+
+ base.deprecate(
+ escape_whitelist: "use #escape_ignore_list instead",
+ :escape_whitelist= => "use #escape_ignore_list= instead"
+ )
+ end
ENCODING_TAG = Regexp.new("\\A(<%#{ENCODING_FLAG}-?%>)[ \\t]*")
@@ -108,7 +47,7 @@ module ActionView
# expression
template_source = template.source.dup.force_encoding(Encoding::ASCII_8BIT)
- erb = template_source.gsub(ENCODING_TAG, '')
+ erb = template_source.gsub(ENCODING_TAG, "")
encoding = $2
erb.force_encoding valid_encoding(template.source.dup, encoding)
@@ -118,36 +57,11 @@ module ActionView
self.class.erb_implementation.new(
erb,
- :escape => (self.class.escape_whitelist.include? template.type),
- :trim => (self.class.erb_trim_mode == "-")
+ escape: (self.class.escape_ignore_list.include? template.type),
+ trim: (self.class.erb_trim_mode == "-")
).src
end
- # Returns Regexp to extract a cached resource's name from a cache call at the
- # first line of a template.
- # The extracted cache name is captured as :resource_name.
- #
- # <% cache notification do %> # => notification
- #
- # The pattern should support templates with a beginning comment:
- #
- # <%# Still extractable even though there's a comment %>
- # <% cache notification do %> # => notification
- #
- # But fail to extract a name if a resource association is cached.
- #
- # <% cache notification.event do %> # => nil
- def resource_cache_call_pattern
- /\A
- (?:<%\#.*%>)* # optional initial comment
- \s* # followed by optional spaces or newlines
- <%\s*cache[\(\s] # followed by an ERB call to cache
- \s* # followed by optional spaces or newlines
- (?<resource_name>\w+) # capture the cache call argument as :resource_name
- [\s\)] # followed by a space or close paren
- /xm
- end
-
private
def valid_encoding(string, encoding)
diff --git a/actionview/lib/action_view/template/handlers/erb/erubi.rb b/actionview/lib/action_view/template/handlers/erb/erubi.rb
new file mode 100644
index 0000000000..db75f028ed
--- /dev/null
+++ b/actionview/lib/action_view/template/handlers/erb/erubi.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+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/html.rb b/actionview/lib/action_view/template/handlers/html.rb
new file mode 100644
index 0000000000..27004a318c
--- /dev/null
+++ b/actionview/lib/action_view/template/handlers/html.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module ActionView
+ module Template::Handlers
+ class Html < Raw
+ def call(template)
+ "ActionView::OutputBuffer.new #{super}"
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/template/handlers/raw.rb b/actionview/lib/action_view/template/handlers/raw.rb
index b08fb0870f..5cd23a0060 100644
--- a/actionview/lib/action_view/template/handlers/raw.rb
+++ b/actionview/lib/action_view/template/handlers/raw.rb
@@ -1,10 +1,10 @@
+# frozen_string_literal: true
+
module ActionView
module Template::Handlers
class Raw
def call(template)
- escaped = template.source.gsub(':'.freeze, '\:'.freeze)
-
- '%q:' + escaped + ':;'
+ "#{template.source.inspect}.html_safe;"
end
end
end
diff --git a/actionview/lib/action_view/template/html.rb b/actionview/lib/action_view/template/html.rb
index 0321f819b5..a262c6d9ad 100644
--- a/actionview/lib/action_view/template/html.rb
+++ b/actionview/lib/action_view/template/html.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
module ActionView #:nodoc:
# = Action View HTML Template
- class Template
+ class Template #:nodoc:
class HTML #:nodoc:
attr_accessor :type
@@ -11,12 +13,10 @@ module ActionView #:nodoc:
end
def identifier
- 'html template'
+ "html template"
end
- def inspect
- 'html template'
- end
+ alias_method :inspect, :identifier
def to_str
ERB::Util.h(@string)
diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb
index 2b00b0303d..08dd6fb510 100644
--- a/actionview/lib/action_view/template/resolver.rb
+++ b/actionview/lib/action_view/template/resolver.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
require "pathname"
require "active_support/core_ext/class"
require "active_support/core_ext/module/attribute_accessors"
require "action_view/template"
require "thread"
-require "thread_safe"
+require "concurrent/map"
module ActionView
# = Action View Resolver
@@ -14,7 +16,7 @@ module ActionView
alias_method :partial?, :partial
def self.build(name, prefix, partial)
- virtual = ""
+ virtual = +""
virtual << "#{prefix}/" unless prefix.empty?
virtual << (partial ? "_#{name}" : name)
new name, prefix, partial, virtual
@@ -35,23 +37,28 @@ module ActionView
# Threadsafe template cache
class Cache #:nodoc:
- class SmallCache < ThreadSafe::Cache
+ class SmallCache < Concurrent::Map
def initialize(options = {})
- super(options.merge(:initial_capacity => 2))
+ super(options.merge(initial_capacity: 2))
end
end
# preallocate all the default blocks for performance/memory consumption reasons
- PARTIAL_BLOCK = lambda {|cache, partial| cache[partial] = SmallCache.new}
- PREFIX_BLOCK = lambda {|cache, prefix| cache[prefix] = SmallCache.new(&PARTIAL_BLOCK)}
- NAME_BLOCK = lambda {|cache, name| cache[name] = SmallCache.new(&PREFIX_BLOCK)}
- KEY_BLOCK = lambda {|cache, key| cache[key] = SmallCache.new(&NAME_BLOCK)}
+ PARTIAL_BLOCK = lambda { |cache, partial| cache[partial] = SmallCache.new }
+ PREFIX_BLOCK = lambda { |cache, prefix| cache[prefix] = SmallCache.new(&PARTIAL_BLOCK) }
+ NAME_BLOCK = lambda { |cache, name| cache[name] = SmallCache.new(&PREFIX_BLOCK) }
+ KEY_BLOCK = lambda { |cache, key| cache[key] = SmallCache.new(&NAME_BLOCK) }
# usually a majority of template look ups return nothing, use this canonical preallocated array to save memory
NO_TEMPLATES = [].freeze
def initialize
@data = SmallCache.new(&KEY_BLOCK)
+ @query_cache = SmallCache.new
+ end
+
+ def inspect
+ "#<#{self.class.name}:0x#{(object_id << 1).to_s(16)} keys=#{@data.size} queries=#{@query_cache.size}>"
end
# Cache the templates returned by the block
@@ -70,32 +77,57 @@ module ActionView
end
end
+ def cache_query(query) # :nodoc:
+ if Resolver.caching?
+ @query_cache[query] ||= canonical_no_templates(yield)
+ else
+ yield
+ end
+ end
+
def clear
@data.clear
+ @query_cache.clear
end
- private
+ # Get the cache size. Do not call this
+ # method. This method is not guaranteed to be here ever.
+ def size # :nodoc:
+ size = 0
+ @data.each_value do |v1|
+ v1.each_value do |v2|
+ v2.each_value do |v3|
+ v3.each_value do |v4|
+ size += v4.size
+ end
+ end
+ end
+ end
- def canonical_no_templates(templates)
- templates.empty? ? NO_TEMPLATES : templates
+ size + @query_cache.size
end
- def templates_have_changed?(cached_templates, fresh_templates)
- # if either the old or new template list is empty, we don't need to (and can't)
- # compare modification times, and instead just check whether the lists are different
- if cached_templates.blank? || fresh_templates.blank?
- return fresh_templates.blank? != cached_templates.blank?
+ private
+
+ def canonical_no_templates(templates)
+ templates.empty? ? NO_TEMPLATES : templates
end
- cached_templates_max_updated_at = cached_templates.map(&:updated_at).max
+ def templates_have_changed?(cached_templates, fresh_templates)
+ # if either the old or new template list is empty, we don't need to (and can't)
+ # compare modification times, and instead just check whether the lists are different
+ if cached_templates.blank? || fresh_templates.blank?
+ return fresh_templates.blank? != cached_templates.blank?
+ end
- # if a template has changed, it will be now be newer than all the cached templates
- fresh_templates.any? { |t| t.updated_at > cached_templates_max_updated_at }
- end
+ cached_templates_max_updated_at = cached_templates.map(&:updated_at).max
+
+ # if a template has changed, it will be now be newer than all the cached templates
+ fresh_templates.any? { |t| t.updated_at > cached_templates_max_updated_at }
+ end
end
- cattr_accessor :caching
- self.caching = true
+ cattr_accessor :caching, default: true
class << self
alias :caching? :caching
@@ -110,12 +142,22 @@ module ActionView
end
# Normalizes the arguments and passes it on to find_templates.
- def find_all(name, prefix=nil, partial=false, details={}, key=nil, locals=[])
+ def find_all(name, prefix = nil, partial = false, details = {}, key = nil, locals = [])
cached(key, [name, prefix, partial], details, locals) do
find_templates(name, prefix, partial, details)
end
end
+ def find_all_anywhere(name, prefix, partial = false, details = {}, key = nil, locals = [])
+ cached(key, [name, prefix, partial], details, locals) do
+ find_templates(name, prefix, partial, details, true)
+ end
+ end
+
+ def find_all_with_query(query) # :nodoc:
+ @cache.cache_query(query) { find_template_paths(File.join(@path, query)) }
+ end
+
private
delegate :caching?, to: :class
@@ -123,8 +165,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.
@@ -136,7 +178,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!
@@ -150,7 +192,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
@@ -163,88 +205,106 @@ module ActionView
# An abstract class that implements a Resolver with path semantics.
class PathResolver < Resolver #:nodoc:
- EXTENSIONS = { :locale => ".", :formats => ".", :variants => "+", :handlers => "." }
+ EXTENSIONS = { locale: ".", formats: ".", variants: "+", handlers: "." }
DEFAULT_PATTERN = ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}"
- def initialize(pattern=nil)
+ def initialize(pattern = nil)
@pattern = pattern || DEFAULT_PATTERN
super()
end
private
- def find_templates(name, prefix, partial, details)
- path = Path.build(name, prefix, partial)
- query(path, details, details[:formats])
- end
+ def find_templates(name, prefix, partial, details, outside_app_allowed = false)
+ path = Path.build(name, prefix, partial)
+ query(path, details, details[:formats], outside_app_allowed)
+ end
+
+ def query(path, details, formats, outside_app_allowed)
+ template_paths = find_template_paths_from_details(path, details)
+ template_paths = reject_files_external_to_app(template_paths) unless outside_app_allowed
- def query(path, details, formats)
- query = build_query(path, details)
+ template_paths.map do |template|
+ handler, format, variant = extract_handler_and_format_and_variant(template)
+ contents = File.binread(template)
- template_paths = find_template_paths(query)
+ Template.new(contents, File.expand_path(template), handler,
+ virtual_path: path.virtual,
+ format: format,
+ variant: variant,
+ updated_at: mtime(template)
+ )
+ end
+ end
- template_paths.map do |template|
- handler, format, variant = extract_handler_and_format_and_variant(template, formats)
- contents = File.binread(template)
+ def reject_files_external_to_app(files)
+ files.reject { |filename| !inside_path?(@path, filename) }
+ end
- Template.new(contents, File.expand_path(template), handler,
- :virtual_path => path.virtual,
- :format => format,
- :variant => variant,
- :updated_at => mtime(template)
- )
+ def find_template_paths_from_details(path, details)
+ query = build_query(path, details)
+ find_template_paths(query)
end
- end
- def find_template_paths(query)
- Dir[query].reject do |filename|
- File.directory?(filename) ||
- # deals with case-insensitive file systems.
- !File.fnmatch(query, filename, File::FNM_EXTGLOB)
+ def find_template_paths(query)
+ Dir[query].uniq.reject do |filename|
+ File.directory?(filename) ||
+ # deals with case-insensitive file systems.
+ !File.fnmatch(query, filename, File::FNM_EXTGLOB)
+ end
end
- end
- # Helper for building query glob string based on resolver's pattern.
- def build_query(path, details)
- query = @pattern.dup
+ def inside_path?(path, filename)
+ filename = File.expand_path(filename)
+ path = File.join(path, "")
+ filename.start_with?(path)
+ end
- prefix = path.prefix.empty? ? '' : "#{escape_entry(path.prefix)}\\1"
- query.gsub!(/:prefix(\/)?/, prefix)
+ # Helper for building query glob string based on resolver's pattern.
+ def build_query(path, details)
+ query = @pattern.dup
- partial = escape_entry(path.partial? ? "_#{path.name}" : path.name)
- query.gsub!(/:action/, partial)
+ prefix = path.prefix.empty? ? "" : "#{escape_entry(path.prefix)}\\1"
+ query.gsub!(/:prefix(\/)?/, prefix)
- details.each do |ext, variants|
- query.gsub!(/:#{ext}/, "{#{variants.compact.uniq.join(',')}}")
- end
+ partial = escape_entry(path.partial? ? "_#{path.name}" : path.name)
+ query.gsub!(/:action/, partial)
- File.expand_path(query, @path)
- end
+ details.each do |ext, candidates|
+ if ext == :variants && candidates == :any
+ query.gsub!(/:#{ext}/, "*")
+ else
+ query.gsub!(/:#{ext}/, "{#{candidates.compact.uniq.join(',')}}")
+ end
+ end
- def escape_entry(entry)
- entry.gsub(/[*?{}\[\]]/, '\\\\\\&')
- end
+ File.expand_path(query, @path)
+ end
- # Returns the file mtime from the filesystem.
- def mtime(p)
- File.mtime(p)
- end
+ def escape_entry(entry)
+ entry.gsub(/[*?{}\[\]]/, '\\\\\\&')
+ end
- # 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)
- pieces = File.basename(path).split('.')
- pieces.shift
+ # Returns the file mtime from the filesystem.
+ def mtime(p)
+ File.mtime(p)
+ end
- extension = pieces.pop
+ # 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)
+ pieces = File.basename(path).split(".")
+ pieces.shift
- handler = Template.handler_for_extension(extension)
- format, variant = pieces.last.split(EXTENSIONS[:variants], 2) if pieces.last
- format &&= Template::Types[format]
+ extension = pieces.pop
- [handler, format, variant]
- end
+ handler = Template.handler_for_extension(extension)
+ format, variant = pieces.last.split(EXTENSIONS[:variants], 2) if pieces.last
+ format &&= Template::Types[format]
+
+ [handler, format, variant]
+ end
end
# A resolver that loads files from the filesystem. It allows setting your own
@@ -253,13 +313,13 @@ module ActionView
# ==== Examples
#
# Default pattern, loads views the same way as previous versions of rails, eg. when you're
- # looking for `users/new` it will produce query glob: `users/new{.{en},}{.{html,js},}{.{erb,haml},}`
+ # looking for <tt>users/new</tt> it will produce query glob: <tt>users/new{.{en},}{.{html,js},}{.{erb,haml},}</tt>
#
# FileSystemResolver.new("/path/to/views", ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}")
#
# This one allows you to keep files with different formats in separate subdirectories,
- # eg. `users/new.html` will be loaded from `users/html/new.erb` or `users/new.html.erb`,
- # `users/new.js` from `users/js/new.erb` or `users/new.js.erb`, etc.
+ # eg. <tt>users/new.html</tt> will be loaded from <tt>users/html/new.erb</tt> or <tt>users/new.html.erb</tt>,
+ # <tt>users/new.js</tt> from <tt>users/js/new.erb</tt> or <tt>users/new.js.erb</tt>, etc.
#
# FileSystemResolver.new("/path/to/views", ":prefix/{:formats/,}:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}")
#
@@ -286,7 +346,7 @@ module ActionView
# * <tt>:handlers</tt> - possible handlers (for example erb, haml, builder...)
#
class FileSystemResolver < PathResolver
- def initialize(path, pattern=nil)
+ def initialize(path, pattern = nil)
raise ArgumentError, "path already is a Resolver class" if path.is_a?(Resolver)
super(pattern)
@path = File.expand_path(path)
@@ -305,15 +365,56 @@ module ActionView
# An Optimized resolver for Rails' most common case.
class OptimizedFileSystemResolver < FileSystemResolver #:nodoc:
- def build_query(path, details)
- query = escape_entry(File.join(@path, path))
+ private
- exts = EXTENSIONS.map do |ext, prefix|
- "{#{details[ext].compact.uniq.map { |e| "#{prefix}#{e}," }.join}}"
- end.join
+ def find_template_paths_from_details(path, details)
+ # Instead of checking for every possible path, as our other globs would
+ # do, scan the directory for files with the right prefix.
+ query = "#{escape_entry(File.join(@path, path))}*"
+
+ regex = build_regex(path, details)
+
+ Dir[query].uniq.reject do |filename|
+ # This regex match does double duty of finding only files which match
+ # details (instead of just matching the prefix) and also filtering for
+ # case-insensitive file systems.
+ !filename.match(regex) ||
+ File.directory?(filename)
+ end.sort_by do |filename|
+ # Because we scanned the directory, instead of checking for files
+ # one-by-one, they will be returned in an arbitrary order.
+ # We can use the matches found by the regex and sort by their index in
+ # details.
+ match = filename.match(regex)
+ EXTENSIONS.keys.reverse.map do |ext|
+ if ext == :variants && details[ext] == :any
+ match[ext].nil? ? 0 : 1
+ elsif match[ext].nil?
+ # No match should be last
+ details[ext].length
+ else
+ found = match[ext].to_sym
+ details[ext].index(found)
+ end
+ end
+ end
+ end
- query + exts
- end
+ def build_regex(path, details)
+ query = escape_entry(File.join(@path, path))
+ exts = EXTENSIONS.map do |ext, prefix|
+ match =
+ if ext == :variants && details[ext] == :any
+ ".*?"
+ else
+ details[ext].compact.uniq.map { |e| Regexp.escape(e) }.join("|")
+ end
+ prefix = Regexp.escape(prefix)
+ "(#{prefix}(?<#{ext}>#{match}))?"
+ end.join
+
+ %r{\A#{query}#{exts}\z}
+ end
end
# The same as FileSystemResolver but does not allow templates to store
diff --git a/actionview/lib/action_view/template/text.rb b/actionview/lib/action_view/template/text.rb
index 04f5b8d17a..f8d6c2811f 100644
--- a/actionview/lib/action_view/template/text.rb
+++ b/actionview/lib/action_view/template/text.rb
@@ -1,22 +1,21 @@
+# frozen_string_literal: true
+
module ActionView #:nodoc:
# = Action View Text Template
- class Template
+ class Template #: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
- 'text template'
+ "text template"
end
- def inspect
- 'text template'
- end
+ alias_method :inspect, :identifier
def to_str
@string
@@ -27,7 +26,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/template/types.rb b/actionview/lib/action_view/template/types.rb
index be45fcf742..67b7a62de6 100644
--- a/actionview/lib/action_view/template/types.rb
+++ b/actionview/lib/action_view/template/types.rb
@@ -1,23 +1,17 @@
-require 'set'
-require 'active_support/core_ext/module/attribute_accessors'
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/attribute_accessors"
module ActionView
- class Template
+ class Template #:nodoc:
class Types
class Type
- cattr_accessor :types
- self.types = Set.new
-
- def self.register(*t)
- types.merge(t.map(&:to_s))
- end
-
- register :html, :text, :js, :css, :xml, :json
+ SET = Struct.new(:symbols).new([ :html, :text, :js, :css, :xml, :json ])
def self.[](type)
- return type if type.is_a?(self)
-
- if type.is_a?(Symbol) || types.member?(type.to_s)
+ if type.is_a?(self)
+ type
+ else
new(type)
end
end
@@ -28,16 +22,18 @@ module ActionView
@symbol = symbol.to_sym
end
- delegate :to_s, :to_sym, :to => :symbol
+ def to_s
+ @symbol.to_s
+ end
alias to_str to_s
def ref
- to_sym || to_s
+ @symbol
end
+ alias to_sym ref
def ==(type)
- return false if type.blank?
- symbol.to_sym == type.to_sym
+ @symbol == type.to_sym unless type.blank?
end
end
@@ -52,6 +48,10 @@ module ActionView
def self.[](type)
type_klass[type]
end
+
+ def self.symbols
+ type_klass::SET.symbols
+ end
end
end
end
diff --git a/actionview/lib/action_view/test_case.rb b/actionview/lib/action_view/test_case.rb
index b4f36c1f78..e14f7aaec7 100644
--- a/actionview/lib/action_view/test_case.rb
+++ b/actionview/lib/action_view/test_case.rb
@@ -1,9 +1,11 @@
-require 'active_support/core_ext/module/remove_method'
-require 'action_controller'
-require 'action_controller/test_case'
-require 'action_view'
+# frozen_string_literal: true
-require 'rails-dom-testing'
+require "active_support/core_ext/module/redefine_method"
+require "action_controller"
+require "action_controller/test_case"
+require "action_view"
+
+require "rails-dom-testing"
module ActionView
# = Action View Test Case
@@ -18,17 +20,17 @@ module ActionView
end
def controller_path=(path)
- self.class.controller_path=(path)
+ self.class.controller_path = (path)
end
def initialize
super
self.class.controller_path = ""
- @request = ActionController::TestRequest.create
- @response = ActionController::TestResponse.new
+ @request = ActionController::TestRequest.create(self.class)
+ @response = ActionDispatch::TestResponse.new
- @request.env.delete('PATH_INFO')
- @params = {}
+ @request.env.delete("PATH_INFO")
+ @params = ActionController::Parameters.new
end
end
@@ -49,7 +51,7 @@ module ActionView
include ActiveSupport::Testing::ConstantLookup
- delegate :lookup_context, :to => :controller
+ delegate :lookup_context, to: :controller
attr_accessor :controller, :output_buffer, :rendered
module ClassMethods
@@ -71,7 +73,7 @@ module ActionView
def helper_method(*methods)
# Almost a duplicate from ActionController::Helpers
methods.flatten.each do |method|
- _helpers.module_eval <<-end_eval
+ _helpers.module_eval <<-end_eval, __FILE__, __LINE__ + 1
def #{method}(*args, &block) # def current_user(*args, &block)
_test_case.send(%(#{method}), *args, &block) # _test_case.send(%(current_user), *args, &block)
end # end
@@ -96,16 +98,16 @@ module ActionView
helper(helper_class) if helper_class
include _helpers
end
-
end
def setup_with_controller
@controller = ActionView::TestCase::TestController.new
@request = @controller.request
+ @view_flow = ActionView::OutputFlow.new
# empty string ensures buffer has UTF-8 encoding as
# new without arguments returns ASCII-8BIT encoded buffer like String#new
- @output_buffer = ActiveSupport::SafeBuffer.new ''
- @rendered = ''
+ @output_buffer = ActiveSupport::SafeBuffer.new ""
+ @rendered = +""
make_test_case_available_to_view!
say_no_to_protect_against_forgery!
@@ -125,6 +127,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
@@ -146,13 +152,14 @@ module ActionView
def view_rendered?(view, expected_locals)
locals_for(view).any? do |actual_locals|
- expected_locals.all? {|key, value| value == actual_locals[key] }
+ expected_locals.all? { |key, value| value == actual_locals[key] }
end
end
end
included do
setup :setup_with_controller
+ ActiveSupport.run_load_hooks(:action_view_test_case, self)
end
private
@@ -164,7 +171,7 @@ module ActionView
def say_no_to_protect_against_forgery!
_helpers.module_eval do
- remove_possible_method :protect_against_forgery?
+ silence_redefinition_of_method :protect_against_forgery?
def protect_against_forgery?
false
end
@@ -206,8 +213,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
@@ -240,6 +247,7 @@ module ActionView
:@test_passed,
:@view,
:@view_context_class,
+ :@view_flow,
:@_subscribers,
:@html_document
]
@@ -258,19 +266,33 @@ module ActionView
end]
end
- def _routes
- @controller._routes if @controller.respond_to?(:_routes)
- end
-
def method_missing(selector, *args)
- if @controller.respond_to?(:_routes) &&
- ( @controller._routes.named_routes.route_defined?(selector) ||
- @controller._routes.mounted_helpers.method_defined?(selector) )
+ begin
+ routes = @controller.respond_to?(:_routes) && @controller._routes
+ rescue
+ # Don't call routes, if there is an error on _routes call
+ end
+
+ if routes &&
+ (routes.named_routes.route_defined?(selector) ||
+ routes.mounted_helpers.method_defined?(selector))
@controller.__send__(selector, *args)
else
super
end
end
+
+ def respond_to_missing?(name, include_private = false)
+ begin
+ routes = @controller.respond_to?(:_routes) && @controller._routes
+ rescue
+ # Don't call routes, if there is an error on _routes call
+ end
+
+ routes &&
+ (routes.named_routes.route_defined?(name) ||
+ routes.mounted_helpers.method_defined?(name))
+ end
end
include Behavior
diff --git a/actionview/lib/action_view/testing/resolvers.rb b/actionview/lib/action_view/testing/resolvers.rb
index dfb7d463b4..1fad08a689 100644
--- a/actionview/lib/action_view/testing/resolvers.rb
+++ b/actionview/lib/action_view/testing/resolvers.rb
@@ -1,4 +1,6 @@
-require 'action_view/template/resolver'
+# frozen_string_literal: true
+
+require "action_view/template/resolver"
module ActionView #:nodoc:
# Use FixtureResolver in your tests to simulate the presence of files on the
@@ -8,47 +10,45 @@ module ActionView #:nodoc:
class FixtureResolver < PathResolver
attr_reader :hash
- def initialize(hash = {}, pattern=nil)
+ def initialize(hash = {}, pattern = nil)
super(pattern)
@hash = hash
end
def to_s
- @hash.keys.join(', ')
+ @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('|') << '|)'
+ 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
- query = /^(#{Regexp.escape(path)})#{query}$/
-
- templates = []
- @hash.each do |_path, array|
- source, updated_at = array
- next unless _path =~ query
- 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
- )
- 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)
- [ActionView::Template.new("Template generated by Null Resolver", path, handler, :virtual_path => path, :format => format, :variant => variant)]
+ 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
-
end
-
diff --git a/actionview/lib/action_view/version.rb b/actionview/lib/action_view/version.rb
index f55d3fdaef..be53797a14 100644
--- a/actionview/lib/action_view/version.rb
+++ b/actionview/lib/action_view/version.rb
@@ -1,4 +1,6 @@
-require_relative 'gem_version'
+# frozen_string_literal: true
+
+require_relative "gem_version"
module ActionView
# Returns the version of the currently loaded ActionView as a <tt>Gem::Version</tt>
diff --git a/actionview/lib/action_view/view_paths.rb b/actionview/lib/action_view/view_paths.rb
index ebca598337..d5694d77f4 100644
--- a/actionview/lib/action_view/view_paths.rb
+++ b/actionview/lib/action_view/view_paths.rb
@@ -1,17 +1,15 @@
-require 'action_view/base'
+# frozen_string_literal: true
module ActionView
module ViewPaths
extend ActiveSupport::Concern
included do
- class_attribute :_view_paths
- self._view_paths = ActionView::PathSet.new
- self._view_paths.freeze
+ class_attribute :_view_paths, default: ActionView::PathSet.new.freeze
end
- delegate :template_exists?, :view_paths, :formats, :formats=,
- :locale, :locale=, :to => :lookup_context
+ delegate :template_exists?, :any_templates?, :view_paths, :formats, :formats=,
+ :locale, :locale=, to: :lookup_context
module ClassMethods
def _prefixes # :nodoc:
@@ -24,11 +22,11 @@ module ActionView
private
- # Override this method in your controller if you want to change paths prefixes for finding views.
- # Prefixes defined here will still be added to parents' <tt>._prefixes</tt>.
- def local_prefixes
- [controller_path]
- end
+ # Override this method in your controller if you want to change paths prefixes for finding views.
+ # Prefixes defined here will still be added to parents' <tt>._prefixes</tt>.
+ def local_prefixes
+ [controller_path]
+ end
end
# The prefixes used in render "foo" shortcuts.
@@ -36,22 +34,34 @@ module ActionView
self.class._prefixes
end
- # <tt>LookupContext</tt> is the object responsible to hold all information required to lookup
- # templates, i.e. view paths and details. Check <tt>ActionView::LookupContext</tt> for more
- # information.
+ # <tt>LookupContext</tt> is the object responsible for holding all
+ # information required for looking up templates, i.e. view paths and
+ # details. Check <tt>ActionView::LookupContext</tt> for more information.
def lookup_context
@_lookup_context ||=
ActionView::LookupContext.new(self.class._view_paths, details_for_lookup, _prefixes)
end
def details_for_lookup
- { }
+ {}
end
+ # Append a path to the list of view paths for the current <tt>LookupContext</tt>.
+ #
+ # ==== Parameters
+ # * <tt>path</tt> - If a String is provided, it gets converted into
+ # the default view path. You may also provide a custom view path
+ # (see ActionView::PathSet for more information)
def append_view_path(path)
lookup_context.view_paths.push(*path)
end
+ # Prepend a path to the list of view paths for the current <tt>LookupContext</tt>.
+ #
+ # ==== Parameters
+ # * <tt>path</tt> - If a String is provided, it gets converted into
+ # the default view path. You may also provide a custom view path
+ # (see ActionView::PathSet for more information)
def prepend_view_path(path)
lookup_context.view_paths.unshift(*path)
end
diff --git a/actionview/package.json b/actionview/package.json
new file mode 100644
index 0000000000..1f74df79d3
--- /dev/null
+++ b/actionview/package.json
@@ -0,0 +1,36 @@
+{
+ "name": "rails-ujs",
+ "version": "6.0.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/blob/master/actionview/app/assets/javascripts#how-to-run-tests\" && exit 1",
+ "lint": "coffeelint app/assets/javascripts && eslint test/ujs/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": "^2.1.0",
+ "eslint": "^2.13.1"
+ }
+}
diff --git a/actionview/test/abstract_unit.rb b/actionview/test/abstract_unit.rb
index 4635c645d0..f90626ad9e 100644
--- a/actionview/test/abstract_unit.rb
+++ b/actionview/test/abstract_unit.rb
@@ -1,37 +1,30 @@
-require File.expand_path('../../../load_paths', __FILE__)
+# frozen_string_literal: true
-$:.unshift(File.dirname(__FILE__) + '/lib')
-$:.unshift(File.dirname(__FILE__) + '/fixtures/helpers')
-$:.unshift(File.dirname(__FILE__) + '/fixtures/alternate_helpers')
+$:.unshift File.expand_path("lib", __dir__)
+$:.unshift File.expand_path("fixtures/helpers", __dir__)
+$:.unshift File.expand_path("fixtures/alternate_helpers", __dir__)
-ENV['TMPDIR'] = File.join(File.dirname(__FILE__), 'tmp')
+ENV["TMPDIR"] = File.expand_path("tmp", __dir__)
-require 'active_support/core_ext/kernel/reporting'
+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'
-require 'action_controller'
-require 'action_view'
-require 'action_view/testing/resolvers'
-require 'active_support/dependencies'
-require 'active_model'
-require 'active_record'
+require "active_support/testing/autorun"
+require "active_support/testing/method_call_assertions"
+require "action_controller"
+require "action_view"
+require "action_view/testing/resolvers"
+require "active_support/dependencies"
+require "active_model"
+require "active_record"
-require 'pp' # require 'pp' early to prevent hidden_methods from not picking up the pretty-print methods until too late
-
-module Rails
- class << self
- def env
- @_env ||= ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "test")
- end
- end
-end
+require "pp" # require 'pp' early to prevent hidden_methods from not picking up the pretty-print methods until too late
ActiveSupport::Dependencies.hook!
@@ -44,11 +37,11 @@ ActiveSupport::Deprecation.debug = true
I18n.enforce_available_locales = false
# Register danish language for testing
-I18n.backend.store_translations 'da', {}
-I18n.backend.store_translations 'pt-BR', {}
+I18n.backend.store_translations "da", {}
+I18n.backend.store_translations "pt-BR", {}
ORIGINAL_LOCALES = I18n.available_locales.map(&:to_s).sort
-FIXTURE_LOAD_PATH = File.join(File.dirname(__FILE__), 'fixtures')
+FIXTURE_LOAD_PATH = File.expand_path("fixtures", __dir__)
module RenderERBUtils
def view
@@ -72,47 +65,6 @@ module RenderERBUtils
end
end
-SharedTestRoutes = ActionDispatch::Routing::RouteSet.new
-
-module ActionDispatch
- module SharedRoutes
- def before_setup
- @routes = SharedTestRoutes
- super
- end
- end
-
- # Hold off drawing routes until all the possible controller classes
- # have been loaded.
- module DrawOnce
- class << self
- attr_accessor :drew
- end
- self.drew = false
-
- def before_setup
- super
- return if DrawOnce.drew
-
- SharedTestRoutes.draw do
- get ':controller(/:action)'
- end
-
- ActionDispatch::IntegrationTest.app.routes.draw do
- get ':controller(/:action)'
- end
-
- DrawOnce.drew = true
- end
- end
-end
-
-module ActiveSupport
- class TestCase
- include ActionDispatch::DrawOnce
- end
-end
-
class RoutedRackApp
attr_reader :routes
@@ -132,82 +84,41 @@ class BasicController
def config
@config ||= ActiveSupport::InheritableOptions.new(ActionController::Base.config).tap do |config|
# VIEW TODO: View tests should not require a controller
- public_dir = File.expand_path("../fixtures/public", __FILE__)
+ public_dir = File.expand_path("fixtures/public", __dir__)
config.assets_dir = public_dir
config.javascripts_dir = "#{public_dir}/javascripts"
config.stylesheets_dir = "#{public_dir}/stylesheets"
- config.assets = ActiveSupport::InheritableOptions.new({ :prefix => "assets" })
+ config.assets = ActiveSupport::InheritableOptions.new(prefix: "assets")
config
end
end
end
class ActionDispatch::IntegrationTest < ActiveSupport::TestCase
- include ActionDispatch::SharedRoutes
-
def self.build_app(routes = nil)
- RoutedRackApp.new(routes || ActionDispatch::Routing::RouteSet.new) do |middleware|
- middleware.use "ActionDispatch::ShowExceptions", ActionDispatch::PublicExceptions.new("#{FIXTURE_LOAD_PATH}/public")
- middleware.use "ActionDispatch::DebugExceptions"
- middleware.use "ActionDispatch::Callbacks"
- middleware.use "ActionDispatch::ParamsParser"
- middleware.use "ActionDispatch::Cookies"
- middleware.use "ActionDispatch::Flash"
- middleware.use "Rack::Head"
+ routes ||= ActionDispatch::Routing::RouteSet.new.tap { |rs|
+ rs.draw { }
+ }
+ RoutedRackApp.new(routes) do |middleware|
+ middleware.use ActionDispatch::ShowExceptions, ActionDispatch::PublicExceptions.new("#{FIXTURE_LOAD_PATH}/public")
+ middleware.use ActionDispatch::DebugExceptions
+ middleware.use ActionDispatch::Callbacks
+ middleware.use ActionDispatch::Cookies
+ middleware.use ActionDispatch::Flash
+ middleware.use Rack::Head
yield(middleware) if block_given?
end
end
self.app = build_app
- # Stub Rails dispatcher so it does not get controller references and
- # simply return the controller#action as Rack::Body.
- class StubDispatcher < ::ActionDispatch::Routing::RouteSet::Dispatcher
- protected
- def controller_reference(controller_param)
- controller_param
- end
-
- def dispatch(controller, action, env)
- [200, {'Content-Type' => 'text/html'}, ["#{controller}##{action}"]]
- end
- end
-
- def self.stub_controllers
- old_dispatcher = ActionDispatch::Routing::RouteSet::Dispatcher
- ActionDispatch::Routing::RouteSet.module_eval { remove_const :Dispatcher }
- ActionDispatch::Routing::RouteSet.module_eval { const_set :Dispatcher, StubDispatcher }
- yield ActionDispatch::Routing::RouteSet.new
- ensure
- ActionDispatch::Routing::RouteSet.module_eval { remove_const :Dispatcher }
- ActionDispatch::Routing::RouteSet.module_eval { const_set :Dispatcher, old_dispatcher }
- end
-
def with_routing(&block)
temporary_routes = ActionDispatch::Routing::RouteSet.new
old_app, self.class.app = self.class.app, self.class.build_app(temporary_routes)
- old_routes = SharedTestRoutes
- silence_warnings { Object.const_set(:SharedTestRoutes, temporary_routes) }
yield temporary_routes
ensure
self.class.app = old_app
- silence_warnings { Object.const_set(:SharedTestRoutes, old_routes) }
- end
-
- def with_autoload_path(path)
- path = File.join(File.dirname(__FILE__), "fixtures", path)
- if ActiveSupport::Dependencies.autoload_paths.include?(path)
- yield
- else
- begin
- ActiveSupport::Dependencies.autoload_paths << path
- yield
- ensure
- ActiveSupport::Dependencies.autoload_paths.reject! {|p| p == path}
- ActiveSupport::Dependencies.clear
- end
- end
end
end
@@ -215,30 +126,37 @@ ActionView::RoutingUrlFor.include(ActionDispatch::Routing::UrlFor)
module ActionController
class Base
- # This stub emulates the Railtie including the URL helpers from a Rails application
- include SharedTestRoutes.url_helpers
- include SharedTestRoutes.mounted_helpers
-
self.view_paths = FIXTURE_LOAD_PATH
def self.test_routes(&block)
routes = ActionDispatch::Routing::RouteSet.new
routes.draw(&block)
include routes.url_helpers
+ routes
end
end
class TestCase
include ActionDispatch::TestProcess
- include ActionDispatch::SharedRoutes
- end
-end
-module ActionView
- class TestCase
- # Must repeat the setup because AV::TestCase is a duplication
- # of AC::TestCase
- include ActionDispatch::SharedRoutes
+ def self.with_routes(&block)
+ routes = ActionDispatch::Routing::RouteSet.new
+ routes.draw(&block)
+ include Module.new {
+ define_method(:setup) do
+ super()
+ @routes = routes
+ @controller.singleton_class.include @routes.url_helpers
+ end
+ }
+ routes
+ end
+
+ def with_routes(&block)
+ @routes = ActionDispatch::Routing::RouteSet.new
+ @routes.draw(&block)
+ @routes
+ end
end
end
@@ -263,21 +181,25 @@ end
module ActionDispatch
class DebugExceptions
private
- remove_method :stderr_logger
- # Silence logger
- def stderr_logger
- nil
- end
+ remove_method :stderr_logger
+ # Silence logger
+ def stderr_logger
+ nil
+ end
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
+
+ private
+ # Skips the current run on Rubinius using Minitest::Assertions#skip
+ def rubinius_skip(message = "")
+ skip message if RUBY_ENGINE == "rbx"
+ end
-require 'mocha/setup' # FIXME: stop using mocha
+ # Skips the current run on JRuby using Minitest::Assertions#skip
+ def jruby_skip(message = "")
+ skip message if defined?(JRUBY_VERSION)
+ end
+end
diff --git a/actionview/test/actionpack/abstract/abstract_controller_test.rb b/actionview/test/actionpack/abstract/abstract_controller_test.rb
index 490932fef0..4d4e2b8ef2 100644
--- a/actionview/test/actionpack/abstract/abstract_controller_test.rb
+++ b/actionview/test/actionpack/abstract/abstract_controller_test.rb
@@ -1,9 +1,10 @@
-require 'abstract_unit'
-require 'set'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "set"
module AbstractController
module Testing
-
# Test basic dispatching.
# ====
# * Call process
@@ -38,12 +39,12 @@ module AbstractController
def render(options = {})
if options.is_a?(String)
- options = {:_template_name => options}
+ options = { _template_name: options }
end
super
end
- append_view_path File.expand_path(File.join(File.dirname(__FILE__), "views"))
+ append_view_path File.expand_path("views", __dir__)
end
class Me2 < RenderingController
@@ -65,11 +66,11 @@ module AbstractController
end
def rendering_to_body
- self.response_body = render_to_body :template => "naked_render"
+ self.response_body = render_to_body template: "naked_render"
end
def rendering_to_string
- self.response_body = render_to_string :template => "naked_render"
+ self.response_body = render_to_string template: "naked_render"
end
end
@@ -114,13 +115,13 @@ module AbstractController
# * self._prefix is used when defined
class PrefixedViews < RenderingController
private
- def self.prefix
- name.underscore
- end
+ def self.prefix
+ name.underscore
+ end
- def _prefixes
- [self.class.prefix]
- end
+ def _prefixes
+ [self.class.prefix]
+ end
end
class Me3 < PrefixedViews
@@ -153,7 +154,7 @@ module AbstractController
class OverridingLocalPrefixes < AbstractController::Base
include AbstractController::Rendering
include ActionView::Rendering
- append_view_path File.expand_path(File.join(File.dirname(__FILE__), "views"))
+ append_view_path File.expand_path("views", __dir__)
def index
render
@@ -189,19 +190,19 @@ module AbstractController
include ActionView::Layouts
private
- def self.layout(formats)
- find_template(name.underscore, {:formats => formats}, :_prefixes => ["layouts"])
- rescue ActionView::MissingTemplate
- begin
- find_template("application", {:formats => formats}, :_prefixes => ["layouts"])
+ def self.layout(formats)
+ find_template(name.underscore, { formats: formats }, { _prefixes: ["layouts"] })
rescue ActionView::MissingTemplate
+ begin
+ find_template("application", { formats: formats }, { _prefixes: ["layouts"] })
+ rescue ActionView::MissingTemplate
+ end
end
- end
- def render_to_body(options = {})
- options[:_layout] = options[:layout] || _default_layout({})
- super
- end
+ def render_to_body(options = {})
+ options[:_layout] = options[:layout] || _default_layout({})
+ super
+ end
end
class Me4 < WithLayouts
@@ -228,14 +229,14 @@ module AbstractController
end
class ActionMissingRespondToActionController < AbstractController::Base
- # No actions
+ # No actions
private
def action_missing(action_name)
self.response_body = "success"
end
end
- class RespondToActionController < AbstractController::Base;
+ class RespondToActionController < AbstractController::Base
def index() self.response_body = "success" end
def fail() self.response_body = "fail" end
@@ -248,7 +249,6 @@ module AbstractController
end
class TestRespondToAction < ActiveSupport::TestCase
-
def assert_dispatch(klass, body = "success", action = :index)
controller = klass.new
controller.process(action)
@@ -277,18 +277,16 @@ module AbstractController
end
class Me6 < AbstractController::Base
- self.action_methods
+ action_methods
def index
end
end
class TestActionMethodsReloading < ActiveSupport::TestCase
-
test "action_methods should be reloaded after defining a new method" do
assert_equal Set.new(["index"]), Me6.action_methods
end
end
-
end
end
diff --git a/actionview/test/actionpack/abstract/helper_test.rb b/actionview/test/actionpack/abstract/helper_test.rb
index 7d346e917d..480ff60ba2 100644
--- a/actionview/test/actionpack/abstract/helper_test.rb
+++ b/actionview/test/actionpack/abstract/helper_test.rb
@@ -1,17 +1,18 @@
-require 'abstract_unit'
+# frozen_string_literal: true
-ActionController::Base.helpers_path = File.expand_path('../../../fixtures/helpers', __FILE__)
+require "abstract_unit"
+
+ActionController::Base.helpers_path = File.expand_path("../../fixtures/helpers", __dir__)
module AbstractController
module Testing
-
class ControllerWithHelpers < AbstractController::Base
include AbstractController::Helpers
include AbstractController::Rendering
include ActionView::Rendering
def with_module
- render :inline => "Module <%= included_method %>"
+ render inline: "Module <%= included_method %>"
end
end
@@ -31,11 +32,11 @@ module AbstractController
helper :abc
def with_block
- render :inline => "Hello <%= helpery_test %>"
+ render inline: "Hello <%= helpery_test %>"
end
def with_symbol
- render :inline => "I respond to bare_a: <%= respond_to?(:bare_a) %>"
+ render inline: "I respond to bare_a: <%= respond_to?(:bare_a) %>"
end
end
@@ -52,7 +53,7 @@ module AbstractController
class AbstractInvalidHelpers < AbstractHelpers
include ActionController::Helpers
- path = File.expand_path('../../../fixtures/helpers_missing', __FILE__)
+ path = File.expand_path("../../fixtures/helpers_missing", __dir__)
$:.unshift(path)
self.helpers_path = path
end
@@ -110,7 +111,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/abstract/layouts_test.rb b/actionview/test/actionpack/abstract/layouts_test.rb
index a6786d9b6b..1146e6f64b 100644
--- a/actionview/test/actionpack/abstract/layouts_test.rb
+++ b/actionview/test/actionpack/abstract/layouts_test.rb
@@ -1,8 +1,9 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module AbstractControllerTests
module Layouts
-
# Base controller for these tests
class Base < AbstractController::Base
include AbstractController::Rendering
@@ -12,7 +13,9 @@ module AbstractControllerTests
abstract!
self.view_paths = [ActionView::FixtureResolver.new(
+ "some/template.erb" => "hello <%= foo %> bar",
"layouts/hello.erb" => "With String <%= yield %>",
+ "layouts/hello_locals.erb" => "With String <%= yield %>",
"layouts/hello_override.erb" => "With Override <%= yield %>",
"layouts/overwrite.erb" => "Overwrite <%= yield %>",
"layouts/with_false_layout.erb" => "False Layout <%= yield %>",
@@ -28,7 +31,15 @@ module AbstractControllerTests
self.view_paths = []
def index
- render :template => ActionView::Template::Text.new("Hello blank!")
+ render template: ActionView::Template::Text.new("Hello blank!")
+ end
+ end
+
+ class WithStringLocals < Base
+ layout "hello_locals"
+
+ def index
+ render template: "some/template", locals: { foo: "less than 3" }
end
end
@@ -36,23 +47,27 @@ module AbstractControllerTests
layout "hello"
def index
- render :template => ActionView::Template::Text.new("Hello string!")
+ render template: ActionView::Template::Text.new("Hello string!")
+ end
+
+ def action_has_layout_false
+ render template: ActionView::Template::Text.new("Hello string!")
end
def overwrite_default
- render :template => ActionView::Template::Text.new("Hello string!"), :layout => :default
+ render template: ActionView::Template::Text.new("Hello string!"), layout: :default
end
def overwrite_false
- render :template => ActionView::Template::Text.new("Hello string!"), :layout => false
+ render template: ActionView::Template::Text.new("Hello string!"), layout: false
end
def overwrite_string
- render :template => ActionView::Template::Text.new("Hello string!"), :layout => "overwrite"
+ render template: ActionView::Template::Text.new("Hello string!"), layout: "overwrite"
end
def overwrite_skip
- render :text => "Hello text!"
+ render plain: "Hello text!"
end
end
@@ -78,11 +93,11 @@ module AbstractControllerTests
layout proc { "overwrite" }
def index
- render :template => ActionView::Template::Text.new("Hello proc!")
+ render template: ActionView::Template::Text.new("Hello proc!")
end
end
- class WithProcReturningNil < Base
+ class WithProcReturningNil < WithString
layout proc { nil }
def index
@@ -90,11 +105,19 @@ module AbstractControllerTests
end
end
+ class WithProcReturningFalse < WithString
+ layout proc { false }
+
+ def index
+ render template: ActionView::Template::Text.new("Hello false!")
+ end
+ end
+
class WithZeroArityProc < Base
layout proc { "overwrite" }
def index
- render :template => ActionView::Template::Text.new("Hello zero arity proc!")
+ render template: ActionView::Template::Text.new("Hello zero arity proc!")
end
end
@@ -107,7 +130,7 @@ module AbstractControllerTests
}
def index
- render :template => ActionView::Template::Text.new("Hello again zero arity proc!")
+ render template: ActionView::Template::Text.new("Hello again zero arity proc!")
end
end
@@ -115,7 +138,7 @@ module AbstractControllerTests
layout :hello
def index
- render :template => ActionView::Template::Text.new("Hello symbol!")
+ render template: ActionView::Template::Text.new("Hello symbol!")
end
private
def hello
@@ -127,7 +150,7 @@ module AbstractControllerTests
layout :nilz
def index
- render :template => ActionView::Template::Text.new("Hello nilz!")
+ render template: ActionView::Template::Text.new("Hello nilz!")
end
def nilz() end
@@ -137,7 +160,7 @@ module AbstractControllerTests
layout :objekt
def index
- render :template => ActionView::Template::Text.new("Hello nilz!")
+ render template: ActionView::Template::Text.new("Hello nilz!")
end
def objekt
@@ -149,7 +172,7 @@ module AbstractControllerTests
layout :no_method
def index
- render :template => ActionView::Template::Text.new("Hello boom!")
+ render template: ActionView::Template::Text.new("Hello boom!")
end
end
@@ -157,7 +180,7 @@ module AbstractControllerTests
layout "missing"
def index
- render :template => ActionView::Template::Text.new("Hello missing!")
+ render template: ActionView::Template::Text.new("Hello missing!")
end
end
@@ -165,7 +188,7 @@ module AbstractControllerTests
layout false
def index
- render :template => ActionView::Template::Text.new("Hello false!")
+ render template: ActionView::Template::Text.new("Hello false!")
end
end
@@ -173,34 +196,81 @@ module AbstractControllerTests
layout nil
def index
- render :template => ActionView::Template::Text.new("Hello nil!")
+ render template: ActionView::Template::Text.new("Hello nil!")
end
end
class WithOnlyConditional < WithStringImpliedChild
- layout "overwrite", :only => :show
+ layout "overwrite", only: :show
def index
- render :template => ActionView::Template::Text.new("Hello index!")
+ render template: ActionView::Template::Text.new("Hello index!")
end
def show
- render :template => ActionView::Template::Text.new("Hello show!")
+ render template: ActionView::Template::Text.new("Hello show!")
end
end
+ class WithOnlyConditionalFlipped < WithOnlyConditional
+ layout "hello_override", only: :index
+ end
+
+ class WithOnlyConditionalFlippedAndInheriting < WithOnlyConditional
+ layout nil, only: :index
+ end
+
class WithExceptConditional < WithStringImpliedChild
- layout "overwrite", :except => :show
+ layout "overwrite", except: :show
def index
- render :template => ActionView::Template::Text.new("Hello index!")
+ render template: ActionView::Template::Text.new("Hello index!")
end
def show
- render :template => ActionView::Template::Text.new("Hello show!")
+ render template: ActionView::Template::Text.new("Hello show!")
+ end
+ end
+
+ class AbstractWithString < Base
+ layout "hello"
+ abstract!
+ end
+
+ class AbstractWithStringChild < AbstractWithString
+ def index
+ render template: ActionView::Template::Text.new("Hello abstract child!")
end
end
+ class AbstractWithStringChildDefaultsToInherited < AbstractWithString
+ layout nil
+
+ def index
+ render template: ActionView::Template::Text.new("Hello abstract child!")
+ end
+ end
+
+ class WithConditionalOverride < WithString
+ layout "overwrite", only: :overwritten
+
+ def non_overwritten
+ render template: ActionView::Template::Text.new("Hello non overwritten!")
+ end
+
+ def overwritten
+ render template: ActionView::Template::Text.new("Hello overwritten!")
+ end
+ end
+
+ class WithConditionalOverrideFlipped < WithConditionalOverride
+ layout "hello_override", only: :non_overwritten
+ end
+
+ class WithConditionalOverrideFlippedAndInheriting < WithConditionalOverride
+ layout nil, only: :non_overwritten
+ end
+
class TestBase < ActiveSupport::TestCase
test "when no layout is specified, and no default is available, render without a layout" do
controller = Blank.new
@@ -208,6 +278,31 @@ module AbstractControllerTests
assert_equal "Hello blank!", controller.response_body
end
+ test "with locals" do
+ controller = WithStringLocals.new
+ controller.process(:index)
+ assert_equal "With String hello less than 3 bar", controller.response_body
+ end
+
+ test "cache should not grow when locals change for a string template" do
+ cache = WithString.view_paths.paths.first.instance_variable_get(:@cache)
+
+ controller = WithString.new
+ controller.process(:index) # heat the cache
+
+ size = cache.size
+
+ 10.times do |x|
+ controller = WithString.new
+ controller.define_singleton_method :index do
+ render template: ActionView::Template::Text.new("Hello string!"), locals: { :"x#{x}" => :omg }
+ end
+ controller.process(:index)
+ end
+
+ assert_equal size, cache.size
+ end
+
test "when layout is specified as a string, render with that layout" do
controller = WithString.new
controller.process(:index)
@@ -255,7 +350,7 @@ module AbstractControllerTests
end
test "when layout is specified as a proc, do not leak any methods into controller's action_methods" do
- assert_equal Set.new(['index']), WithProc.action_methods
+ assert_equal Set.new(["index"]), WithProc.action_methods
end
test "when layout is specified as a proc, call it and use the layout returned" do
@@ -264,10 +359,16 @@ module AbstractControllerTests
assert_equal "Overwrite Hello proc!", controller.response_body
end
- test "when layout is specified as a proc and the proc returns nil, don't use a layout" do
+ test "when layout is specified as a proc and the proc returns nil, use inherited layout" do
controller = WithProcReturningNil.new
controller.process(:index)
- assert_equal "Hello nil!", controller.response_body
+ assert_equal "With String Hello nil!", controller.response_body
+ end
+
+ test "when layout is specified as a proc and the proc returns false, use no layout instead of inherited layout" do
+ controller = WithProcReturningFalse.new
+ controller.process(:index)
+ assert_equal "Hello false!", controller.response_body
end
test "when layout is specified as a proc without parameters it works just the same" do
@@ -322,16 +423,28 @@ module AbstractControllerTests
test "when a grandchild has no layout specified, the child has an implied layout, and the " \
"parent has specified a layout, use the child controller layout" do
- controller = WithChildOfImplied.new
- controller.process(:index)
- assert_equal "With Implied Hello string!", controller.response_body
+ controller = WithChildOfImplied.new
+ controller.process(:index)
+ assert_equal "With Implied Hello string!", controller.response_body
end
test "when a grandchild has nil layout specified, the child has an implied layout, and the " \
- "parent has specified a layout, use the child controller layout" do
- controller = WithGrandChildOfImplied.new
- controller.process(:index)
- assert_equal "With Grand Child Hello string!", controller.response_body
+ "parent has specified a layout, use the grand child controller layout" do
+ controller = WithGrandChildOfImplied.new
+ controller.process(:index)
+ assert_equal "With Grand Child Hello string!", controller.response_body
+ end
+
+ test "a child inherits layout from abstract controller" do
+ controller = AbstractWithStringChild.new
+ controller.process(:index)
+ assert_equal "With String Hello abstract child!", controller.response_body
+ end
+
+ test "a child inherits layout from abstract controller2" do
+ controller = AbstractWithStringChildDefaultsToInherited.new
+ controller.process(:index)
+ assert_equal "With String Hello abstract child!", controller.response_body
end
test "raises an exception when specifying layout true" do
@@ -356,6 +469,30 @@ module AbstractControllerTests
assert_equal "With Implied Hello index!", controller.response_body
end
+ test "when specify an :only option which match current action name and is opposite from parent controller" do
+ controller = WithOnlyConditionalFlipped.new
+ controller.process(:show)
+ assert_equal "With Implied Hello show!", controller.response_body
+ end
+
+ test "when specify an :only option which does not match current action name and is opposite from parent controller" do
+ controller = WithOnlyConditionalFlipped.new
+ controller.process(:index)
+ assert_equal "With Override Hello index!", controller.response_body
+ end
+
+ test "when specify to inherit and an :only option which match current action name and is opposite from parent controller" do
+ controller = WithOnlyConditionalFlippedAndInheriting.new
+ controller.process(:show)
+ assert_equal "With Implied Hello show!", controller.response_body
+ end
+
+ test "when specify to inherit and an :only option which does not match current action name and is opposite from parent controller" do
+ controller = WithOnlyConditionalFlippedAndInheriting.new
+ controller.process(:index)
+ assert_equal "Overwrite Hello index!", controller.response_body
+ end
+
test "when specify an :except option which match current action name" do
controller = WithExceptConditional.new
controller.process(:show)
@@ -368,10 +505,46 @@ module AbstractControllerTests
assert_equal "Overwrite Hello index!", controller.response_body
end
+ test "when specify overwrite as an :only option which match current action name" do
+ controller = WithConditionalOverride.new
+ controller.process(:overwritten)
+ assert_equal "Overwrite Hello overwritten!", controller.response_body
+ end
+
+ test "when specify overwrite as an :only option which does not match current action name" do
+ controller = WithConditionalOverride.new
+ controller.process(:non_overwritten)
+ assert_equal "Hello non overwritten!", controller.response_body
+ end
+
+ test "when specify overwrite as an :only option which match current action name and is opposite from parent controller" do
+ controller = WithConditionalOverrideFlipped.new
+ controller.process(:overwritten)
+ assert_equal "Hello overwritten!", controller.response_body
+ end
+
+ test "when specify overwrite as an :only option which does not match current action name and is opposite from parent controller" do
+ controller = WithConditionalOverrideFlipped.new
+ controller.process(:non_overwritten)
+ assert_equal "With Override Hello non overwritten!", controller.response_body
+ end
+
+ test "when specify to inherit and overwrite as an :only option which match current action name and is opposite from parent controller" do
+ controller = WithConditionalOverrideFlippedAndInheriting.new
+ controller.process(:overwritten)
+ assert_equal "Hello overwritten!", controller.response_body
+ end
+
+ test "when specify to inherit and overwrite as an :only option which does not match current action name and is opposite from parent controller" do
+ controller = WithConditionalOverrideFlippedAndInheriting.new
+ controller.process(:non_overwritten)
+ assert_equal "Overwrite Hello non overwritten!", controller.response_body
+ end
+
test "layout for anonymous controller" do
klass = Class.new(WithString) do
def index
- render :text => 'index', :layout => true
+ render plain: "index", layout: true
end
end
@@ -379,6 +552,17 @@ module AbstractControllerTests
controller.process(:index)
assert_equal "With String index", controller.response_body
end
+
+ test "when layout is disabled with #action_has_layout? returning false, render no layout" do
+ controller = WithString.new
+ controller.instance_eval do
+ def action_has_layout?
+ false
+ end
+ end
+ controller.process(:action_has_layout_false)
+ assert_equal "Hello string!", controller.response_body
+ end
end
end
end
diff --git a/actionview/test/actionpack/abstract/render_test.rb b/actionview/test/actionpack/abstract/render_test.rb
index d09f91c1e2..d863548a5c 100644
--- a/actionview/test/actionpack/abstract/render_test.rb
+++ b/actionview/test/actionpack/abstract/render_test.rb
@@ -1,8 +1,9 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module AbstractController
module Testing
-
class ControllerRenderer < AbstractController::Base
include AbstractController::Rendering
include ActionView::Rendering
@@ -21,19 +22,19 @@ module AbstractController
)]
def template
- render :template => "template"
+ render template: "template"
end
def file
- render :file => "some/file"
+ render file: "some/file"
end
def inline
- render :inline => "With <%= :Inline %>"
+ render inline: "With <%= :Inline %>"
end
def text
- render :text => "With Text"
+ render plain: "With Text"
end
def default
@@ -54,7 +55,6 @@ module AbstractController
end
class TestRenderer < ActiveSupport::TestCase
-
def setup
@controller = ControllerRenderer.new
end
diff --git a/actionview/test/actionpack/controller/capture_test.rb b/actionview/test/actionpack/controller/capture_test.rb
index f8387b27b0..d02125bafa 100644
--- a/actionview/test/actionpack/controller/capture_test.rb
+++ b/actionview/test/actionpack/controller/capture_test.rb
@@ -1,30 +1,32 @@
-require 'abstract_unit'
-require 'active_support/logger'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/logger"
class CaptureController < ActionController::Base
- self.view_paths = [ File.dirname(__FILE__) + '/../../fixtures/actionpack' ]
+ self.view_paths = [ File.expand_path("../../fixtures/actionpack", __dir__) ]
def self.controller_name; "test"; end
def self.controller_path; "test"; end
def content_for
@title = nil
- render :layout => "talk_from_action"
+ render layout: "talk_from_action"
end
def content_for_with_parameter
@title = nil
- render :layout => "talk_from_action"
+ render layout: "talk_from_action"
end
def content_for_concatenated
@title = nil
- render :layout => "talk_from_action"
+ render layout: "talk_from_action"
end
def non_erb_block_content_for
@title = nil
- render :layout => "talk_from_action"
+ render layout: "talk_from_action"
end
def proper_block_detection
@@ -35,6 +37,15 @@ end
class CaptureTest < ActionController::TestCase
tests CaptureController
+ with_routes do
+ get :content_for, to: "test#content_for"
+ get :capturing, to: "test#capturing"
+ get :proper_block_detection, to: "test#proper_block_detection"
+ get :non_erb_block_content_for, to: "test#non_erb_block_content_for"
+ get :content_for_concatenated, to: "test#content_for_concatenated"
+ get :content_for_with_parameter, to: "test#content_for_with_parameter"
+ end
+
def setup
super
# enable a logger so that (e.g.) the benchmarking stuff runs, so we can get
@@ -54,7 +65,7 @@ class CaptureTest < ActionController::TestCase
assert_equal expected_content_for_output, @response.body
end
- def test_should_concatentate_content_for
+ def test_should_concatenate_content_for
get :content_for_concatenated
assert_equal expected_content_for_output, @response.body
end
diff --git a/actionview/test/actionpack/controller/layout_test.rb b/actionview/test/actionpack/controller/layout_test.rb
index 64bc4c41d6..6d5c97b7fd 100644
--- a/actionview/test/actionpack/controller/layout_test.rb
+++ b/actionview/test/actionpack/controller/layout_test.rb
@@ -1,15 +1,17 @@
-require 'abstract_unit'
-require 'active_support/core_ext/array/extract_options'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/array/extract_options"
# The view_paths array must be set on Base and not LayoutTest so that LayoutTest's inherited
# method has access to the view_paths array when looking for a layout to automatically assign.
old_load_paths = ActionController::Base.view_paths
-ActionController::Base.view_paths = [ File.dirname(__FILE__) + '/../../fixtures/actionpack/layout_tests/' ]
+ActionController::Base.view_paths = [ File.expand_path("../../fixtures/actionpack/layout_tests", __dir__) ]
class LayoutTest < ActionController::Base
- def self.controller_path; 'views' end
- def self._implied_layout_name; to_s.underscore.gsub(/_controller$/, '') ; end
+ def self.controller_path; "views" end
+ def self._implied_layout_name; to_s.underscore.gsub(/_controller$/, "") ; end
self.view_paths = ActionController::Base.view_paths.dup
end
@@ -46,6 +48,10 @@ end
class LayoutAutoDiscoveryTest < ActionController::TestCase
include TemplateHandlerHelper
+ with_routes do
+ get :hello, to: "views#hello"
+ end
+
def setup
super
@request.host = "www.nextangle.com"
@@ -54,13 +60,13 @@ class LayoutAutoDiscoveryTest < ActionController::TestCase
def test_application_layout_is_default_when_no_controller_match
@controller = ProductController.new
get :hello
- assert_equal 'layout_test.erb hello.erb', @response.body
+ assert_equal "layout_test.erb hello.erb", @response.body
end
def test_controller_name_layout_name_match
@controller = ItemController.new
get :hello
- assert_equal 'item.erb hello.erb', @response.body
+ assert_equal "item.erb hello.erb", @response.body
end
def test_third_party_template_library_auto_discovers_layout
@@ -68,20 +74,20 @@ class LayoutAutoDiscoveryTest < ActionController::TestCase
@controller = ThirdPartyTemplateLibraryController.new
get :hello
assert_response :success
- assert_equal 'layouts/third_party_template_library.mab', @response.body
+ assert_equal "layouts/third_party_template_library.mab", @response.body
end
end
def test_namespaced_controllers_auto_detect_layouts1
@controller = ControllerNameSpace::NestedController.new
get :hello
- assert_equal 'controller_name_space/nested.erb hello.erb', @response.body
+ assert_equal "controller_name_space/nested.erb hello.erb", @response.body
end
def test_namespaced_controllers_auto_detect_layouts2
@controller = MultipleExtensions.new
get :hello
- assert_equal 'multiple_extensions.html.erb hello.erb', @response.body.strip
+ assert_equal "multiple_extensions.html.erb hello.erb", @response.body.strip
end
end
@@ -91,16 +97,16 @@ end
class StreamingLayoutController < LayoutTest
def render(*args)
options = args.extract_options!
- super(*args, options.merge(:stream => true))
+ super(*args, options.merge(stream: true))
end
end
class AbsolutePathLayoutController < LayoutTest
- layout File.expand_path(File.expand_path(__FILE__) + '/../../../fixtures/actionpack/layout_tests/layouts/layout_test')
+ layout File.expand_path("../../fixtures/actionpack/layout_tests/layouts/layout_test", __dir__)
end
class HasOwnLayoutController < LayoutTest
- layout 'item'
+ layout "item"
end
class HasNilLayoutSymbol < LayoutTest
@@ -117,28 +123,28 @@ end
class PrependsViewPathController < LayoutTest
def hello
- prepend_view_path File.dirname(__FILE__) + '/../../fixtures/actionpack/layout_tests/alt/'
- render :layout => 'alt'
+ prepend_view_path File.expand_path("../../fixtures/actionpack/layout_tests/alt", __dir__)
+ render layout: "alt"
end
end
class OnlyLayoutController < LayoutTest
- layout 'item', :only => "hello"
+ layout "item", only: "hello"
end
class ExceptLayoutController < LayoutTest
- layout 'item', :except => "goodbye"
+ layout "item", except: "goodbye"
end
class SetsLayoutInRenderController < LayoutTest
def hello
- render :layout => 'third_party_template_library'
+ render layout: "third_party_template_library"
end
end
class RendersNoLayoutController < LayoutTest
def hello
- render :layout => false
+ render layout: false
end
end
@@ -146,78 +152,83 @@ class LayoutSetInResponseTest < ActionController::TestCase
include ActionView::Template::Handlers
include TemplateHandlerHelper
+ with_routes do
+ get :hello, to: "views#hello"
+ get :hello, to: "views#goodbye"
+ end
+
def test_layout_set_when_using_default_layout
@controller = DefaultLayoutController.new
get :hello
- assert_includes @response.body, 'layout_test.erb'
+ assert_includes @response.body, "layout_test.erb"
end
def test_layout_set_when_using_streaming_layout
@controller = StreamingLayoutController.new
get :hello
- assert_includes @response.body, 'layout_test.erb'
+ assert_includes @response.body, "layout_test.erb"
end
def test_layout_set_when_set_in_controller
@controller = HasOwnLayoutController.new
get :hello
- assert_includes @response.body, 'item.erb'
+ assert_includes @response.body, "item.erb"
end
def test_layout_symbol_set_in_controller_returning_nil_falls_back_to_default
@controller = HasNilLayoutSymbol.new
get :hello
- assert_includes @response.body, 'layout_test.erb'
+ assert_includes @response.body, "layout_test.erb"
end
def test_layout_proc_set_in_controller_returning_nil_falls_back_to_default
@controller = HasNilLayoutProc.new
get :hello
- assert_includes @response.body, 'layout_test.erb'
+ assert_includes @response.body, "layout_test.erb"
end
def test_layout_only_exception_when_included
@controller = OnlyLayoutController.new
get :hello
- assert_includes @response.body, 'item.erb'
+ assert_includes @response.body, "item.erb"
end
def test_layout_only_exception_when_excepted
@controller = OnlyLayoutController.new
get :goodbye
- assert_not_includes @response.body, 'item.erb'
+ assert_not_includes @response.body, "item.erb"
end
def test_layout_except_exception_when_included
@controller = ExceptLayoutController.new
get :hello
- assert_includes @response.body, 'item.erb'
+ assert_includes @response.body, "item.erb"
end
def test_layout_except_exception_when_excepted
@controller = ExceptLayoutController.new
get :goodbye
- assert_not_includes @response.body, 'item.erb'
+ assert_not_includes @response.body, "item.erb"
end
def test_layout_set_when_using_render
with_template_handler :mab, lambda { |template| template.source.inspect } do
@controller = SetsLayoutInRenderController.new
get :hello
- assert_includes @response.body, 'layouts/third_party_template_library.mab'
+ assert_includes @response.body, "layouts/third_party_template_library.mab"
end
end
def test_layout_is_not_set_when_none_rendered
@controller = RendersNoLayoutController.new
get :hello
- assert_equal 'hello.erb', @response.body
+ assert_equal "hello.erb", @response.body
end
def test_layout_is_picked_from_the_controller_instances_view_path
@controller = PrependsViewPathController.new
get :hello
- assert_includes @response.body, 'alt.erb'
+ assert_includes @response.body, "alt.erb"
end
def test_absolute_pathed_layout
@@ -232,6 +243,10 @@ class SetsNonExistentLayoutFile < LayoutTest
end
class LayoutExceptionRaisedTest < ActionController::TestCase
+ with_routes do
+ get :hello, to: "views#hello"
+ end
+
def test_exception_raised_when_layout_file_not_found
@controller = SetsNonExistentLayoutFile.new
assert_raise(ActionView::MissingTemplate) { get :hello }
@@ -240,11 +255,15 @@ end
class LayoutStatusIsRendered < LayoutTest
def hello
- render :status => 401
+ render status: 401
end
end
class LayoutStatusIsRenderedTest < ActionController::TestCase
+ with_routes do
+ get :hello, to: "views#hello"
+ end
+
def test_layout_status_is_rendered
@controller = LayoutStatusIsRendered.new
get :hello
@@ -252,17 +271,21 @@ class LayoutStatusIsRenderedTest < ActionController::TestCase
end
end
-unless RbConfig::CONFIG['host_os'] =~ /mswin|mingw/
+unless Gem.win_platform?
class LayoutSymlinkedTest < LayoutTest
layout "symlinked/symlinked_layout"
end
class LayoutSymlinkedIsRenderedTest < ActionController::TestCase
+ with_routes do
+ get :hello, to: "views#hello"
+ end
+
def test_symlinked_layout_is_rendered
@controller = LayoutSymlinkedTest.new
get :hello
assert_response 200
- assert_includes @response.body, 'This is my layout'
+ assert_includes @response.body, "This is my layout"
end
end
end
diff --git a/actionview/test/actionpack/controller/render_test.rb b/actionview/test/actionpack/controller/render_test.rb
index ebaf39a12d..204903c60c 100644
--- a/actionview/test/actionpack/controller/render_test.rb
+++ b/actionview/test/actionpack/controller/render_test.rb
@@ -1,45 +1,12 @@
-require 'abstract_unit'
-require 'active_model'
+# frozen_string_literal: true
-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
+require "abstract_unit"
+require "active_model"
+require "controller/fake_models"
module Quiz
- #Models
- class Question < Struct.new(:name, :id)
+ # Models
+ Question = Struct.new(:name, :id) do
extend ActiveModel::Naming
include ActiveModel::Conversion
@@ -49,27 +16,24 @@ module Quiz
end
# Controller
- class QuestionsController < ApplicationController
+ class QuestionsController < ActionController::Base
def new
- render :partial => Quiz::Question.new("Namespaced Partial")
+ render partial: Quiz::Question.new("Namespaced Partial")
end
end
end
-class BadCustomer < Customer; end
-class GoodCustomer < Customer; end
-
module Fun
- class GamesController < ApplicationController
+ class GamesController < ActionController::Base
def hello_world; end
def nested_partial_with_form_builder
- render :partial => ActionView::Helpers::FormBuilder.new(:post, nil, view_context, {})
+ render partial: ActionView::Helpers::FormBuilder.new(:post, nil, view_context, {})
end
end
end
-class TestController < ApplicationController
+class TestController < ActionController::Base
protect_from_forgery
before_action :set_variable_for_layout
@@ -90,7 +54,7 @@ class TestController < ApplicationController
end
def hello_world_file
- render :file => File.expand_path("../../../fixtures/actionpack/hello", __FILE__), :formats => [:html]
+ render file: File.expand_path("../../fixtures/actionpack/hello", __dir__), formats: [:html]
end
# :ported:
@@ -110,27 +74,27 @@ class TestController < ApplicationController
# :ported:
def render_template_in_top_directory
- render :template => 'shared'
+ render template: "shared"
end
# :deprecated:
def render_template_in_top_directory_with_slash
- render '/shared'
+ render "/shared"
end
# :ported:
def render_hello_world_from_variable
@person = "david"
- render :text => "hello #{@person}"
+ render plain: "hello #{@person}"
end
# :ported:
def render_action_hello_world
- render :action => "hello_world"
+ render action: "hello_world"
end
def render_action_upcased_hello_world
- render :action => "Hello_world"
+ render action: "Hello_world"
end
def render_action_hello_world_as_string
@@ -138,100 +102,100 @@ class TestController < ApplicationController
end
def render_action_hello_world_with_symbol
- render :action => :hello_world
+ render action: :hello_world
end
# :ported:
def render_text_hello_world
- render :text => "hello world"
+ render plain: "hello world"
end
# :ported:
def render_text_hello_world_with_layout
@variable_for_layout = ", I am here!"
- render :text => "hello world", :layout => true
+ render plain: "hello world", layout: true
end
def hello_world_with_layout_false
- render :layout => false
+ render layout: false
end
# :ported:
def render_file_with_instance_variables
- @secret = 'in the sauce'
- path = File.join(File.dirname(__FILE__), '../../fixtures/test/render_file_with_ivar')
- render :file => path
+ @secret = "in the sauce"
+ path = File.expand_path("../../fixtures/test/render_file_with_ivar", __dir__)
+ render file: path
end
# :ported:
def render_file_not_using_full_path
- @secret = 'in the sauce'
- render :file => 'test/render_file_with_ivar'
+ @secret = "in the sauce"
+ render file: "test/render_file_with_ivar"
end
def render_file_not_using_full_path_with_dot_in_path
- @secret = 'in the sauce'
- render :file => 'test/dot.directory/render_file_with_ivar'
+ @secret = "in the sauce"
+ render file: "test/dot.directory/render_file_with_ivar"
end
def render_file_using_pathname
- @secret = 'in the sauce'
- render :file => Pathname.new(File.dirname(__FILE__)).join('..', '..', 'fixtures', 'test', 'dot.directory', 'render_file_with_ivar')
+ @secret = "in the sauce"
+ render file: Pathname.new(__dir__).join("..", "..", "fixtures", "test", "dot.directory", "render_file_with_ivar")
end
def render_file_from_template
- @secret = 'in the sauce'
- @path = File.expand_path(File.join(File.dirname(__FILE__), '../../fixtures/test/render_file_with_ivar'))
+ @secret = "in the sauce"
+ @path = File.expand_path("../../fixtures/test/render_file_with_ivar", __dir__)
end
def render_file_with_locals
- path = File.join(File.dirname(__FILE__), '../../fixtures/test/render_file_with_locals')
- render :file => path, :locals => {:secret => 'in the sauce'}
+ path = File.expand_path("../../fixtures/test/render_file_with_locals", __dir__)
+ render file: path, locals: { secret: "in the sauce" }
end
def render_file_as_string_with_locals
- path = File.expand_path(File.join(File.dirname(__FILE__), '../../fixtures/test/render_file_with_locals'))
- render file: path, :locals => {:secret => 'in the sauce'}
+ path = File.expand_path("../../fixtures/test/render_file_with_locals", __dir__)
+ render file: path, locals: { secret: "in the sauce" }
end
def accessing_request_in_template
- render :inline => "Hello: <%= request.host %>"
+ render inline: "Hello: <%= request.host %>"
end
def accessing_logger_in_template
- render :inline => "<%= logger.class %>"
+ render inline: "<%= logger.class %>"
end
def accessing_action_name_in_template
- render :inline => "<%= action_name %>"
+ render inline: "<%= action_name %>"
end
def accessing_controller_name_in_template
- render :inline => "<%= controller_name %>"
+ render inline: "<%= controller_name %>"
end
# :ported:
def render_custom_code
- render :text => "hello world", :status => 404
+ render plain: "hello world", status: 404
end
# :ported:
def render_text_with_nil
- render :text => nil
+ render plain: nil
end
# :ported:
def render_text_with_false
- render :text => false
+ render plain: false
end
def render_text_with_resource
- render :text => Customer.new("David")
+ render plain: Customer.new("David")
end
# :ported:
def render_nothing_with_appendix
- render :text => "appended"
+ render plain: "appended"
end
# This test is testing 3 things:
@@ -240,7 +204,7 @@ class TestController < ApplicationController
# setting content type
def render_xml_hello
@name = "David"
- render :template => "test/hello"
+ render template: "test/hello"
end
def render_xml_hello_as_string_template
@@ -249,7 +213,7 @@ class TestController < ApplicationController
end
def render_line_offset
- render :inline => '<% raise %>', :locals => {:foo => 'bar'}
+ render inline: "<% raise %>", locals: { foo: "bar" }
end
def heading
@@ -262,49 +226,49 @@ class TestController < ApplicationController
# :ported:
def blank_response
- render :text => ' '
+ render plain: " "
end
# :ported:
def layout_test
- render :action => "hello_world"
+ render action: "hello_world"
end
# :ported:
def builder_layout_test
@name = nil
- render :action => "hello", :layout => "layouts/builder"
+ render action: "hello", layout: "layouts/builder"
end
# :move: test this in Action View
def builder_partial_test
- render :action => "hello_world_container"
+ render action: "hello_world_container"
end
# :ported:
def partials_list
- @test_unchanged = 'hello'
+ @test_unchanged = "hello"
@customers = [ Customer.new("david"), Customer.new("mary") ]
- render :action => "list"
+ render action: "list"
end
def partial_only
- render :partial => true
+ render partial: true
end
def hello_in_a_string
@customers = [ Customer.new("david"), Customer.new("mary") ]
- render :text => "How's there? " + render_to_string(:template => "test/list")
+ render plain: "How's there? " + render_to_string(template: "test/list")
end
def accessing_params_in_template
- render :inline => "Hello: <%= params[:name] %>"
+ render inline: "Hello: <%= params[:name] %>"
end
def accessing_local_assigns_in_inline_template
name = params[:local_name]
- render :inline => "<%= 'Goodbye, ' + local_name %>",
- :locals => { :local_name => name }
+ render inline: "<%= 'Goodbye, ' + local_name %>",
+ locals: { local_name: name }
end
def render_implicit_html_template_from_xhr_request
@@ -320,7 +284,7 @@ class TestController < ApplicationController
end
def render_to_string_test
- @foo = render_to_string :inline => "this is a test"
+ @foo = render_to_string inline: "this is a test"
end
def default_render
@@ -333,27 +297,27 @@ class TestController < ApplicationController
end
def render_action_hello_world_as_symbol
- render :action => :hello_world
+ render action: :hello_world
end
def layout_test_with_different_layout
- render :action => "hello_world", :layout => "standard"
+ render action: "hello_world", layout: "standard"
end
def layout_test_with_different_layout_and_string_action
- render "hello_world", :layout => "standard"
+ render "hello_world", layout: "standard"
end
def layout_test_with_different_layout_and_symbol_action
- render :hello_world, :layout => "standard"
+ render :hello_world, layout: "standard"
end
def rendering_without_layout
- render :action => "hello_world", :layout => false
+ render action: "hello_world", layout: false
end
def layout_overriding_layout
- render :action => "hello_world", :layout => "standard"
+ render action: "hello_world", layout: "standard"
end
def rendering_nothing_on_layout
@@ -362,40 +326,40 @@ class TestController < ApplicationController
def render_to_string_with_assigns
@before = "i'm before the render"
- render_to_string :text => "foo"
+ render_to_string plain: "foo"
@after = "i'm after the render"
- render :template => "test/hello_world"
+ render template: "test/hello_world"
end
def render_to_string_with_exception
- render_to_string :file => "exception that will not be caught - this will certainly not work"
+ render_to_string file: "exception that will not be caught - this will certainly not work"
end
def render_to_string_with_caught_exception
@before = "i'm before the render"
begin
- render_to_string :file => "exception that will be caught- hope my future instance vars still work!"
+ render_to_string file: "exception that will be caught- hope my future instance vars still work!"
rescue
end
@after = "i'm after the render"
- render :template => "test/hello_world"
+ render template: "test/hello_world"
end
def accessing_params_in_template_with_layout
- render :layout => true, :inline => "Hello: <%= params[:name] %>"
+ render layout: true, inline: "Hello: <%= params[:name] %>"
end
# :ported:
def render_with_explicit_template
- render :template => "test/hello_world"
+ render template: "test/hello_world"
end
def render_with_explicit_unescaped_template
- render :template => "test/h*llo_world"
+ render template: "test/h*llo_world"
end
def render_with_explicit_escaped_template
- render :template => "test/hello,world"
+ render template: "test/hello,world"
end
def render_with_explicit_string_template
@@ -404,47 +368,47 @@ class TestController < ApplicationController
# :ported:
def render_with_explicit_template_with_locals
- render :template => "test/render_file_with_locals", :locals => { :secret => 'area51' }
+ render template: "test/render_file_with_locals", locals: { secret: "area51" }
end
# :ported:
def double_render
- render :text => "hello"
- render :text => "world"
+ render plain: "hello"
+ render plain: "world"
end
def double_redirect
- redirect_to :action => "double_render"
- redirect_to :action => "double_render"
+ redirect_to action: "double_render"
+ redirect_to action: "double_render"
end
def render_and_redirect
- render :text => "hello"
- redirect_to :action => "double_render"
+ render plain: "hello"
+ redirect_to action: "double_render"
end
def render_to_string_and_render
- @stuff = render_to_string :text => "here is some cached stuff"
- render :text => "Hi web users! #{@stuff}"
+ @stuff = render_to_string plain: "here is some cached stuff"
+ render plain: "Hi web users! #{@stuff}"
end
def render_to_string_with_inline_and_render
- render_to_string :inline => "<%= 'dlrow olleh'.reverse %>"
- render :template => "test/hello_world"
+ render_to_string inline: "<%= 'dlrow olleh'.reverse %>"
+ render template: "test/hello_world"
end
def rendering_with_conflicting_local_vars
@name = "David"
- render :action => "potential_conflicts"
+ render action: "potential_conflicts"
end
def hello_world_from_rxml_using_action
- render :action => "hello_world_from_rxml", :handlers => [:builder]
+ render action: "hello_world_from_rxml", handlers: [:builder]
end
# :deprecated:
def hello_world_from_rxml_using_template
- render :template => "test/hello_world_from_rxml", :handlers => [:builder]
+ render template: "test/hello_world_from_rxml", handlers: [:builder]
end
def action_talk_to_layout
@@ -454,134 +418,138 @@ class TestController < ApplicationController
# :addressed:
def render_text_with_assigns
@hello = "world"
- render :text => "foo"
+ render plain: "foo"
end
def render_with_assigns_option
- render inline: '<%= @hello %>', assigns: { hello: "world" }
+ render inline: "<%= @hello %>", assigns: { hello: "world" }
end
def yield_content_for
- render :action => "content_for", :layout => "yield"
+ render action: "content_for", layout: "yield"
end
def render_content_type_from_body
- response.content_type = Mime::RSS
- render :text => "hello world!"
+ response.content_type = Mime[:rss]
+ render body: "hello world!"
end
def render_using_layout_around_block
- render :action => "using_layout_around_block"
+ render action: "using_layout_around_block"
end
def render_using_layout_around_block_in_main_layout_and_within_content_for_layout
- render :action => "using_layout_around_block", :layout => "layouts/block_with_layout"
+ render action: "using_layout_around_block", layout: "layouts/block_with_layout"
end
def partial_formats_html
- render :partial => 'partial', :formats => [:html]
+ render partial: "partial", formats: [:html]
end
def partial
- render :partial => 'partial'
+ render partial: "partial"
end
def partial_html_erb
- render :partial => 'partial_html_erb'
+ render partial: "partial_html_erb"
end
def render_to_string_with_partial
- @partial_only = render_to_string :partial => "partial_only"
- @partial_with_locals = render_to_string :partial => "customer", :locals => { :customer => Customer.new("david") }
- render :template => "test/hello_world"
+ @partial_only = render_to_string partial: "partial_only"
+ @partial_with_locals = render_to_string partial: "customer", locals: { customer: Customer.new("david") }
+ render template: "test/hello_world"
end
def render_to_string_with_template_and_html_partial
- @text = render_to_string :template => "test/with_partial", :formats => [:text]
- @html = render_to_string :template => "test/with_partial", :formats => [:html]
- render :template => "test/with_html_partial"
+ @text = render_to_string template: "test/with_partial", formats: [:text]
+ @html = render_to_string template: "test/with_partial", formats: [:html]
+ render template: "test/with_html_partial"
end
def render_to_string_and_render_with_different_formats
- @html = render_to_string :template => "test/with_partial", :formats => [:html]
- render :template => "test/with_partial", :formats => [:text]
+ @html = render_to_string template: "test/with_partial", formats: [:html]
+ render template: "test/with_partial", formats: [:text]
end
def render_template_within_a_template_with_other_format
- render :template => "test/with_xml_template",
- :formats => [:html],
- :layout => "with_html_partial"
+ render template: "test/with_xml_template",
+ formats: [:html],
+ layout: "with_html_partial"
end
def partial_with_counter
- render :partial => "counter", :locals => { :counter_counter => 5 }
+ render partial: "counter", locals: { counter_counter: 5 }
end
def partial_with_locals
- render :partial => "customer", :locals => { :customer => Customer.new("david") }
+ render partial: "customer", locals: { customer: Customer.new("david") }
+ end
+
+ def partial_with_string_locals
+ render partial: "customer", locals: { "customer" => Customer.new("david") }
end
def partial_with_form_builder
- render :partial => ActionView::Helpers::FormBuilder.new(:post, nil, view_context, {})
+ render partial: ActionView::Helpers::FormBuilder.new(:post, nil, view_context, {})
end
def partial_with_form_builder_subclass
- render :partial => LabellingFormBuilder.new(:post, nil, view_context, {})
+ render partial: LabellingFormBuilder.new(:post, nil, view_context, {})
end
def partial_collection
- render :partial => "customer", :collection => [ Customer.new("david"), Customer.new("mary") ]
+ render partial: "customer", collection: [ Customer.new("david"), Customer.new("mary") ]
end
def partial_collection_with_as
- render :partial => "customer_with_var", :collection => [ Customer.new("david"), Customer.new("mary") ], :as => :customer
+ render partial: "customer_with_var", collection: [ Customer.new("david"), Customer.new("mary") ], as: :customer
end
def partial_collection_with_iteration
- render partial: "customer_iteration", collection: [ Customer.new("david"), Customer.new("mary"), Customer.new('christine') ]
+ render partial: "customer_iteration", collection: [ Customer.new("david"), Customer.new("mary"), Customer.new("christine") ]
end
def partial_collection_with_as_and_iteration
- render partial: "customer_iteration_with_as", collection: [ Customer.new("david"), Customer.new("mary"), Customer.new('christine') ], as: :client
+ render partial: "customer_iteration_with_as", collection: [ Customer.new("david"), Customer.new("mary"), Customer.new("christine") ], as: :client
end
def partial_collection_with_counter
- render :partial => "customer_counter", :collection => [ Customer.new("david"), Customer.new("mary") ]
+ render partial: "customer_counter", collection: [ Customer.new("david"), Customer.new("mary") ]
end
def partial_collection_with_as_and_counter
- render :partial => "customer_counter_with_as", :collection => [ Customer.new("david"), Customer.new("mary") ], :as => :client
+ render partial: "customer_counter_with_as", collection: [ Customer.new("david"), Customer.new("mary") ], as: :client
end
def partial_collection_with_locals
- render :partial => "customer_greeting", :collection => [ Customer.new("david"), Customer.new("mary") ], :locals => { :greeting => "Bonjour" }
+ render partial: "customer_greeting", collection: [ Customer.new("david"), Customer.new("mary") ], locals: { greeting: "Bonjour" }
end
def partial_collection_with_spacer
- render :partial => "customer", :spacer_template => "partial_only", :collection => [ Customer.new("david"), Customer.new("mary") ]
+ render partial: "customer", spacer_template: "partial_only", collection: [ Customer.new("david"), Customer.new("mary") ]
end
def partial_collection_with_spacer_which_uses_render
- render :partial => "customer", :spacer_template => "partial_with_partial", :collection => [ Customer.new("david"), Customer.new("mary") ]
+ render partial: "customer", spacer_template: "partial_with_partial", collection: [ Customer.new("david"), Customer.new("mary") ]
end
def partial_collection_shorthand_with_locals
- render :partial => [ Customer.new("david"), Customer.new("mary") ], :locals => { :greeting => "Bonjour" }
+ render partial: [ Customer.new("david"), Customer.new("mary") ], locals: { greeting: "Bonjour" }
end
def partial_collection_shorthand_with_different_types_of_records
- render :partial => [
+ render partial: [
BadCustomer.new("mark"),
GoodCustomer.new("craig"),
BadCustomer.new("john"),
GoodCustomer.new("zach"),
GoodCustomer.new("brandon"),
BadCustomer.new("dan") ],
- :locals => { :greeting => "Bonjour" }
+ locals: { greeting: "Bonjour" }
end
def empty_partial_collection
- render :partial => "customer", :collection => []
+ render partial: "customer", collection: []
end
def partial_collection_shorthand_with_different_types_of_records_with_counter
@@ -589,15 +557,15 @@ class TestController < ApplicationController
end
def missing_partial
- render :partial => 'thisFileIsntHere'
+ render partial: "thisFileIsntHere"
end
def partial_with_hash_object
- render :partial => "hash_object", :object => {:first_name => "Sam"}
+ render partial: "hash_object", object: { first_name: "Sam" }
end
def partial_with_nested_object
- render :partial => "quiz/questions/question", :object => Quiz::Question.new("first")
+ render partial: "quiz/questions/question", object: Quiz::Question.new("first")
end
def partial_with_nested_object_shorthand
@@ -605,24 +573,24 @@ class TestController < ApplicationController
end
def partial_hash_collection
- render :partial => "hash_object", :collection => [ {:first_name => "Pratik"}, {:first_name => "Amy"} ]
+ render partial: "hash_object", collection: [ { first_name: "Pratik" }, { first_name: "Amy" } ]
end
def partial_hash_collection_with_locals
- render :partial => "hash_greeting", :collection => [ {:first_name => "Pratik"}, {:first_name => "Amy"} ], :locals => { :greeting => "Hola" }
+ render partial: "hash_greeting", collection: [ { first_name: "Pratik" }, { first_name: "Amy" } ], locals: { greeting: "Hola" }
end
def partial_with_implicit_local_assignment
@customer = Customer.new("Marcel")
- render :partial => "customer"
+ render partial: "customer"
end
def render_call_to_partial_with_layout
- render :action => "calling_partial_with_layout"
+ render action: "calling_partial_with_layout"
end
def render_call_to_partial_with_layout_in_main_layout_and_within_content_for_layout
- render :action => "calling_partial_with_layout", :layout => "layouts/partial_with_layout"
+ render action: "calling_partial_with_layout", layout: "layouts/partial_with_layout"
end
before_action only: :render_with_filters do
@@ -631,7 +599,7 @@ class TestController < ApplicationController
# Ensure that the before filter is executed *before* self.formats is set.
def render_with_filters
- render :action => :formatted_xml_erb
+ render action: :formatted_xml_erb
end
private
@@ -642,7 +610,7 @@ class TestController < ApplicationController
def determine_layout
case action_name
- when "hello_world", "layout_test", "rendering_without_layout",
+ when "hello_world", "layout_test", "rendering_without_layout",
"rendering_nothing_on_layout", "render_text_hello_world",
"render_text_hello_world_with_layout",
"hello_world_with_layout_false",
@@ -652,11 +620,11 @@ class TestController < ApplicationController
"render_with_explicit_string_template",
"update_page", "update_page_with_instance_variables"
- "layouts/standard"
- when "action_talk_to_layout", "layout_overriding_layout"
- "layouts/talk_from_action"
- when "render_implicit_html_template_from_xhr_request"
- (request.xhr? ? 'layouts/xhr' : 'layouts/standard')
+ "layouts/standard"
+ when "action_talk_to_layout", "layout_overriding_layout"
+ "layouts/talk_from_action"
+ when "render_implicit_html_template_from_xhr_request"
+ (request.xhr? ? "layouts/xhr" : "layouts/standard")
end
end
end
@@ -664,6 +632,124 @@ end
class RenderTest < ActionController::TestCase
tests TestController
+ with_routes do
+ get :"hyphen-ated", to: "test#hyphen-ated"
+ get :accessing_action_name_in_template, to: "test#accessing_action_name_in_template"
+ get :accessing_controller_name_in_template, to: "test#accessing_controller_name_in_template"
+ get :accessing_local_assigns_in_inline_template, to: "test#accessing_local_assigns_in_inline_template"
+ get :accessing_logger_in_template, to: "test#accessing_logger_in_template"
+ get :accessing_params_in_template, to: "test#accessing_params_in_template"
+ get :accessing_params_in_template_with_layout, to: "test#accessing_params_in_template_with_layout"
+ get :accessing_request_in_template, to: "test#accessing_request_in_template"
+ get :action_talk_to_layout, to: "test#action_talk_to_layout"
+ get :builder_layout_test, to: "test#builder_layout_test"
+ get :builder_partial_test, to: "test#builder_partial_test"
+ get :clone, to: "test#clone"
+ get :determine_layout, to: "test#determine_layout"
+ get :double_redirect, to: "test#double_redirect"
+ get :double_render, to: "test#double_render"
+ get :empty_partial_collection, to: "test#empty_partial_collection"
+ get :formatted_html_erb, to: "test#formatted_html_erb"
+ get :formatted_xml_erb, to: "test#formatted_xml_erb"
+ get :greeting, to: "test#greeting"
+ get :hello_in_a_string, to: "test#hello_in_a_string"
+ get :hello_world, to: "fun/games#hello_world"
+ get :hello_world, to: "test#hello_world"
+ get :hello_world_file, to: "test#hello_world_file"
+ get :hello_world_from_rxml_using_action, to: "test#hello_world_from_rxml_using_action"
+ get :hello_world_from_rxml_using_template, to: "test#hello_world_from_rxml_using_template"
+ get :hello_world_with_layout_false, to: "test#hello_world_with_layout_false"
+ get :layout_overriding_layout, to: "test#layout_overriding_layout"
+ get :layout_test, to: "test#layout_test"
+ get :layout_test_with_different_layout, to: "test#layout_test_with_different_layout"
+ get :layout_test_with_different_layout_and_string_action, to: "test#layout_test_with_different_layout_and_string_action"
+ get :layout_test_with_different_layout_and_symbol_action, to: "test#layout_test_with_different_layout_and_symbol_action"
+ get :missing_partial, to: "test#missing_partial"
+ get :nested_partial_with_form_builder, to: "fun/games#nested_partial_with_form_builder"
+ get :new, to: "quiz/questions#new"
+ get :partial, to: "test#partial"
+ get :partial_collection, to: "test#partial_collection"
+ get :partial_collection_shorthand_with_different_types_of_records, to: "test#partial_collection_shorthand_with_different_types_of_records"
+ get :partial_collection_shorthand_with_locals, to: "test#partial_collection_shorthand_with_locals"
+ get :partial_collection_with_as, to: "test#partial_collection_with_as"
+ get :partial_collection_with_as_and_counter, to: "test#partial_collection_with_as_and_counter"
+ get :partial_collection_with_as_and_iteration, to: "test#partial_collection_with_as_and_iteration"
+ get :partial_collection_with_counter, to: "test#partial_collection_with_counter"
+ get :partial_collection_with_iteration, to: "test#partial_collection_with_iteration"
+ get :partial_collection_with_locals, to: "test#partial_collection_with_locals"
+ get :partial_collection_with_spacer, to: "test#partial_collection_with_spacer"
+ get :partial_collection_with_spacer_which_uses_render, to: "test#partial_collection_with_spacer_which_uses_render"
+ get :partial_formats_html, to: "test#partial_formats_html"
+ get :partial_hash_collection, to: "test#partial_hash_collection"
+ get :partial_hash_collection_with_locals, to: "test#partial_hash_collection_with_locals"
+ get :partial_html_erb, to: "test#partial_html_erb"
+ get :partial_only, to: "test#partial_only"
+ get :partial_with_counter, to: "test#partial_with_counter"
+ get :partial_with_form_builder, to: "test#partial_with_form_builder"
+ get :partial_with_form_builder_subclass, to: "test#partial_with_form_builder_subclass"
+ get :partial_with_hash_object, to: "test#partial_with_hash_object"
+ get :partial_with_locals, to: "test#partial_with_locals"
+ get :partial_with_nested_object, to: "test#partial_with_nested_object"
+ get :partial_with_nested_object_shorthand, to: "test#partial_with_nested_object_shorthand"
+ get :partial_with_string_locals, to: "test#partial_with_string_locals"
+ get :partials_list, to: "test#partials_list"
+ get :render_action_hello_world, to: "test#render_action_hello_world"
+ get :render_action_hello_world_as_string, to: "test#render_action_hello_world_as_string"
+ get :render_action_hello_world_with_symbol, to: "test#render_action_hello_world_with_symbol"
+ get :render_action_upcased_hello_world, to: "test#render_action_upcased_hello_world"
+ get :render_and_redirect, to: "test#render_and_redirect"
+ get :render_call_to_partial_with_layout, to: "test#render_call_to_partial_with_layout"
+ get :render_call_to_partial_with_layout_in_main_layout_and_within_content_for_layout, to: "test#render_call_to_partial_with_layout_in_main_layout_and_within_content_for_layout"
+ get :render_custom_code, to: "test#render_custom_code"
+ get :render_file_as_string_with_locals, to: "test#render_file_as_string_with_locals"
+ get :render_file_from_template, to: "test#render_file_from_template"
+ get :render_file_not_using_full_path, to: "test#render_file_not_using_full_path"
+ get :render_file_not_using_full_path_with_dot_in_path, to: "test#render_file_not_using_full_path_with_dot_in_path"
+ get :render_file_using_pathname, to: "test#render_file_using_pathname"
+ get :render_file_with_instance_variables, to: "test#render_file_with_instance_variables"
+ get :render_file_with_locals, to: "test#render_file_with_locals"
+ get :render_hello_world, to: "test#render_hello_world"
+ get :render_hello_world_from_variable, to: "test#render_hello_world_from_variable"
+ get :render_hello_world_with_forward_slash, to: "test#render_hello_world_with_forward_slash"
+ get :render_implicit_html_template_from_xhr_request, to: "test#render_implicit_html_template_from_xhr_request"
+ get :render_implicit_js_template_without_layout, to: "test#render_implicit_js_template_without_layout"
+ get :render_line_offset, to: "test#render_line_offset"
+ get :render_nothing_with_appendix, to: "test#render_nothing_with_appendix"
+ get :render_template_in_top_directory, to: "test#render_template_in_top_directory"
+ get :render_template_in_top_directory_with_slash, to: "test#render_template_in_top_directory_with_slash"
+ get :render_template_within_a_template_with_other_format, to: "test#render_template_within_a_template_with_other_format"
+ get :render_text_hello_world, to: "test#render_text_hello_world"
+ get :render_text_hello_world_with_layout, to: "test#render_text_hello_world_with_layout"
+ get :render_text_with_assigns, to: "test#render_text_with_assigns"
+ get :render_text_with_false, to: "test#render_text_with_false"
+ get :render_text_with_nil, to: "test#render_text_with_nil"
+ get :render_text_with_resource, to: "test#render_text_with_resource"
+ get :render_to_string_and_render, to: "test#render_to_string_and_render"
+ get :render_to_string_and_render_with_different_formats, to: "test#render_to_string_and_render_with_different_formats"
+ get :render_to_string_test, to: "test#render_to_string_test"
+ get :render_to_string_with_assigns, to: "test#render_to_string_with_assigns"
+ get :render_to_string_with_caught_exception, to: "test#render_to_string_with_caught_exception"
+ get :render_to_string_with_exception, to: "test#render_to_string_with_exception"
+ get :render_to_string_with_inline_and_render, to: "test#render_to_string_with_inline_and_render"
+ get :render_to_string_with_partial, to: "test#render_to_string_with_partial"
+ get :render_to_string_with_template_and_html_partial, to: "test#render_to_string_with_template_and_html_partial"
+ get :render_using_layout_around_block, to: "test#render_using_layout_around_block"
+ get :render_using_layout_around_block_in_main_layout_and_within_content_for_layout, to: "test#render_using_layout_around_block_in_main_layout_and_within_content_for_layout"
+ get :render_with_assigns_option, to: "test#render_with_assigns_option"
+ get :render_with_explicit_escaped_template, to: "test#render_with_explicit_escaped_template"
+ get :render_with_explicit_string_template, to: "test#render_with_explicit_string_template"
+ get :render_with_explicit_template, to: "test#render_with_explicit_template"
+ get :render_with_explicit_template_with_locals, to: "test#render_with_explicit_template_with_locals"
+ get :render_with_explicit_unescaped_template, to: "test#render_with_explicit_unescaped_template"
+ get :render_with_filters, to: "test#render_with_filters"
+ get :render_xml_hello, to: "test#render_xml_hello"
+ get :render_xml_hello_as_string_template, to: "test#render_xml_hello_as_string_template"
+ get :rendering_nothing_on_layout, to: "test#rendering_nothing_on_layout"
+ get :rendering_with_conflicting_local_vars, to: "test#rendering_with_conflicting_local_vars"
+ get :rendering_without_layout, to: "test#rendering_without_layout"
+ get :yield_content_for, to: "test#yield_content_for"
+ end
+
def setup
# enable a logger so that (e.g.) the benchmarking stuff runs, so we can get
# a more accurate simulation of what happens in "real life".
@@ -672,10 +758,15 @@ class RenderTest < ActionController::TestCase
ActionView::Base.logger = ActiveSupport::Logger.new(nil)
@request.host = "www.nextangle.com"
+
+ @old_view_paths = ActionController::Base.view_paths
+ ActionController::Base.view_paths = File.join(FIXTURE_LOAD_PATH, "actionpack")
end
def teardown
ActionView::Base.logger = nil
+
+ ActionController::Base.view_paths = @old_view_paths
end
# :ported:
@@ -726,11 +817,6 @@ class RenderTest < ActionController::TestCase
assert_equal "Elastica", @response.body
end
- def test_render_process
- get :render_action_hello_world_as_string
- assert_equal ["Hello world!"], @controller.process(:render_action_hello_world_as_string)
- end
-
# :ported:
def test_render_from_variable
get :render_hello_world_from_variable
@@ -770,13 +856,13 @@ class RenderTest < ActionController::TestCase
# :ported:
def test_do_with_render_text_and_layout
get :render_text_hello_world_with_layout
- assert_equal "<html>hello world, I am here!</html>", @response.body
+ assert_equal "{{hello world, I am here!}}\n", @response.body
end
# :ported:
def test_do_with_render_action_and_layout_false
get :hello_world_with_layout_false
- assert_equal 'Hello world!', @response.body
+ assert_equal "Hello world!", @response.body
end
# :ported:
@@ -831,27 +917,27 @@ class RenderTest < ActionController::TestCase
get :render_custom_code
assert_response 404
assert_response :missing
- assert_equal 'hello world', @response.body
+ assert_equal "hello world", @response.body
end
# :ported:
def test_render_text_with_nil
get :render_text_with_nil
assert_response 200
- assert_equal '', @response.body
+ assert_equal "", @response.body
end
# :ported:
def test_render_text_with_false
get :render_text_with_false
- assert_equal 'false', @response.body
+ assert_equal "false", @response.body
end
# :ported:
def test_render_nothing_with_appendix
get :render_nothing_with_appendix
assert_response 200
- assert_equal 'appended', @response.body
+ assert_equal "appended", @response.body
end
def test_render_text_with_resource
@@ -946,7 +1032,7 @@ class RenderTest < ActionController::TestCase
def test_render_to_string_inline
get :render_to_string_with_inline_and_render
- assert_equal 'Hello world!', @response.body
+ assert_equal "Hello world!", @response.body
end
# :ported:
@@ -979,24 +1065,24 @@ class RenderTest < ActionController::TestCase
def test_should_render_formatted_template
get :formatted_html_erb
- assert_equal 'formatted html erb', @response.body
+ assert_equal "formatted html erb", @response.body
end
def test_should_render_formatted_html_erb_template
get :formatted_xml_erb
- assert_equal '<test>passed formatted html erb</test>', @response.body
+ assert_equal "<test>passed formatted html erb</test>", @response.body
end
def test_should_render_formatted_html_erb_template_with_bad_accepts_header
@request.env["HTTP_ACCEPT"] = "; a=dsf"
get :formatted_xml_erb
- assert_equal '<test>passed formatted html erb</test>', @response.body
+ assert_equal "<test>passed formatted html erb</test>", @response.body
end
def test_should_render_formatted_html_erb_template_with_faulty_accepts_header
@request.accept = "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, */*"
get :formatted_xml_erb
- assert_equal '<test>passed formatted html erb</test>', @response.body
+ assert_equal "<test>passed formatted html erb</test>", @response.body
end
def test_layout_test_with_different_layout
@@ -1026,7 +1112,7 @@ class RenderTest < ActionController::TestCase
def test_rendering_nothing_on_layout
get :rendering_nothing_on_layout
- assert_equal '', @response.body
+ assert_equal "", @response.body
end
def test_render_to_string_doesnt_break_assigns
@@ -1108,7 +1194,7 @@ class RenderTest < ActionController::TestCase
def test_render_text_with_assigns_option
get :render_with_assigns_option
- assert_equal 'world', response.body
+ assert_equal "world", response.body
end
# :ported:
@@ -1122,7 +1208,7 @@ class RenderTest < ActionController::TestCase
assert_equal "<title>Putting stuff in the title!</title>\nGreat stuff!\n", @response.body
end
- def test_overwritting_rendering_relative_file_with_extension
+ def test_overwriting_rendering_relative_file_with_extension
get :hello_world_from_rxml_using_template
assert_equal "<html>\n <p>Hello</p>\n</html>\n", @response.body
@@ -1206,6 +1292,11 @@ class RenderTest < ActionController::TestCase
assert_equal "Hello: david", @response.body
end
+ def test_partial_with_string_locals
+ get :partial_with_string_locals
+ assert_equal "Hello: david", @response.body
+ end
+
def test_partial_with_form_builder
get :partial_with_form_builder
assert_equal "<label for=\"post_title\">Title</label>\n", @response.body
diff --git a/actionview/test/actionpack/controller/view_paths_test.rb b/actionview/test/actionpack/controller/view_paths_test.rb
index 2dd27358f7..7f3fe0fa08 100644
--- a/actionview/test/actionpack/controller/view_paths_test.rb
+++ b/actionview/test/actionpack/controller/view_paths_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class ViewLoadPathsTest < ActionController::TestCase
class TestController < ActionController::Base
@@ -7,7 +9,7 @@ class ViewLoadPathsTest < ActionController::TestCase
before_action :add_view_path, only: :hello_world_at_request_time
def hello_world() end
- def hello_world_at_request_time() render(:action => 'hello_world') end
+ def hello_world_at_request_time() render(action: "hello_world") end
private
def add_view_path
@@ -17,16 +19,22 @@ class ViewLoadPathsTest < ActionController::TestCase
module Test
class SubController < ActionController::Base
- layout 'test/sub'
- def hello_world; render(:template => 'test/hello_world'); end
+ layout "test/sub"
+ def hello_world; render(template: "test/hello_world"); end
end
end
+ with_routes do
+ get :hello_world, to: "test#hello_world"
+ get :hello_world_at_request_time, to: "test#hello_world_at_request_time"
+ end
+
def setup
- @request = ActionController::TestRequest.create
- @response = ActionController::TestResponse.new
@controller = TestController.new
+ @request = ActionController::TestRequest.create(@controller.class)
+ @response = ActionDispatch::TestResponse.new
@paths = TestController.view_paths
+ super
end
def teardown
@@ -34,7 +42,7 @@ class ViewLoadPathsTest < ActionController::TestCase
end
def expand(array)
- array.map {|x| File.expand_path(x.to_s)}
+ array.map { |x| File.expand_path(x.to_s) }
end
def assert_paths(*paths)
@@ -47,7 +55,7 @@ class ViewLoadPathsTest < ActionController::TestCase
end
def test_controller_appends_view_path_correctly
- @controller.append_view_path 'foo'
+ @controller.append_view_path "foo"
assert_paths(FIXTURE_LOAD_PATH, "foo")
@controller.append_view_path(%w(bar baz))
@@ -58,7 +66,7 @@ class ViewLoadPathsTest < ActionController::TestCase
end
def test_controller_prepends_view_path_correctly
- @controller.prepend_view_path 'baz'
+ @controller.prepend_view_path "baz"
assert_paths("baz", FIXTURE_LOAD_PATH)
@controller.prepend_view_path(%w(foo bar))
@@ -72,7 +80,7 @@ class ViewLoadPathsTest < ActionController::TestCase
@controller.instance_variable_set :@template, ActionView::Base.new(TestController.view_paths, {}, @controller)
class_view_paths = TestController.view_paths
- @controller.append_view_path 'foo'
+ @controller.append_view_path "foo"
assert_paths FIXTURE_LOAD_PATH, "foo"
@controller.append_view_path(%w(bar baz))
@@ -84,7 +92,7 @@ class ViewLoadPathsTest < ActionController::TestCase
@controller.instance_variable_set :@template, ActionView::Base.new(TestController.view_paths, {}, @controller)
class_view_paths = TestController.view_paths
- @controller.prepend_view_path 'baz'
+ @controller.prepend_view_path "baz"
assert_paths "baz", FIXTURE_LOAD_PATH
@controller.prepend_view_path(%w(foo bar))
@@ -107,6 +115,10 @@ class ViewLoadPathsTest < ActionController::TestCase
def test_view_paths_override_for_layouts_in_controllers_with_a_module
@controller = Test::SubController.new
+ with_routes do
+ get :hello_world, to: "view_load_paths_test/test/sub#hello_world"
+ end
+
Test::SubController.view_paths = [ "#{FIXTURE_LOAD_PATH}/override", FIXTURE_LOAD_PATH, "#{FIXTURE_LOAD_PATH}/override2" ]
get :hello_world
assert_response :success
@@ -131,10 +143,8 @@ class ViewLoadPathsTest < ActionController::TestCase
"Decorated body",
template.identifier,
template.handler,
- {
- :virtual_path => template.virtual_path,
- :format => template.formats
- }
+ virtual_path: template.virtual_path,
+ format: template.formats
)
end
end
@@ -157,14 +167,14 @@ class ViewLoadPathsTest < ActionController::TestCase
class C < ActionController::Base; end
}
- A.view_paths = ['a/path']
+ A.view_paths = ["a/path"]
assert_paths A, "a/path"
assert_paths A, *B.view_paths
assert_paths C, *original_load_paths
C.view_paths = []
- assert_nothing_raised { C.append_view_path 'c/path' }
+ assert_nothing_raised { C.append_view_path "c/path" }
assert_paths C, "c/path"
end
diff --git a/actionview/test/active_record_unit.rb b/actionview/test/active_record_unit.rb
index f9e94413b5..e4ea6a426d 100644
--- a/actionview/test/active_record_unit.rb
+++ b/actionview/test/active_record_unit.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
# Define the essentials
class ActiveRecordTestConnector
@@ -13,10 +15,10 @@ end
# Try to grab AR
unless defined?(ActiveRecord) && defined?(FixtureSet)
begin
- PATH_TO_AR = "#{File.dirname(__FILE__)}/../../activerecord/lib"
+ PATH_TO_AR = File.expand_path("../../activerecord/lib", __dir__)
raise LoadError, "#{PATH_TO_AR} doesn't exist" unless File.directory?(PATH_TO_AR)
$LOAD_PATH.unshift PATH_TO_AR
- require 'active_record'
+ require "active_record"
rescue LoadError => e
$stderr.print "Failed to load Active Record. Skipping Active Record assertion tests: #{e}"
ActiveRecordTestConnector.able_to_connect = false
@@ -24,12 +26,11 @@ unless defined?(ActiveRecord) && defined?(FixtureSet)
end
$stderr.flush
-
# Define the rest of the connector
class ActiveRecordTestConnector
class << self
def setup
- unless self.connected || !self.able_to_connect
+ unless connected || !able_to_connect
setup_connection
load_schema
require_fixture_models
@@ -37,21 +38,21 @@ class ActiveRecordTestConnector
end
rescue Exception => e # errors from ActiveRecord setup
$stderr.puts "\nSkipping ActiveRecord assertion tests: #{e}"
- #$stderr.puts " #{e.backtrace.join("\n ")}\n"
+ # $stderr.puts " #{e.backtrace.join("\n ")}\n"
self.able_to_connect = false
end
private
def setup_connection
if Object.const_defined?(:ActiveRecord)
- defaults = { :database => ':memory:' }
- adapter = defined?(JRUBY_VERSION) ? 'jdbcsqlite3' : 'sqlite3'
- options = defaults.merge :adapter => adapter, :timeout => 500
+ defaults = { database: ":memory:" }
+ adapter = defined?(JRUBY_VERSION) ? "jdbcsqlite3" : "sqlite3"
+ options = defaults.merge adapter: adapter, timeout: 500
ActiveRecord::Base.establish_connection(options)
- ActiveRecord::Base.configurations = { 'sqlite3_ar_integration' => options }
+ ActiveRecord::Base.configurations = { "sqlite3_ar_integration" => options }
ActiveRecord::Base.connection
- Object.send(:const_set, :QUOTED_TYPE, ActiveRecord::Base.connection.quote_column_name('type')) unless Object.const_defined?(:QUOTED_TYPE)
+ Object.send(:const_set, :QUOTED_TYPE, ActiveRecord::Base.connection.quote_column_name("type")) unless Object.const_defined?(:QUOTED_TYPE)
else
raise "Can't setup connection since ActiveRecord isn't loaded."
end
@@ -59,13 +60,13 @@ class ActiveRecordTestConnector
# Load actionpack sqlite3 tables
def load_schema
- File.read(File.dirname(__FILE__) + "/fixtures/db_definitions/sqlite.sql").split(';').each do |sql|
+ File.read(File.expand_path("fixtures/db_definitions/sqlite.sql", __dir__)).split(";").each do |sql|
ActiveRecord::Base.connection.execute(sql) unless sql.blank?
end
end
def require_fixture_models
- Dir.glob(File.dirname(__FILE__) + "/fixtures/*.rb").each {|f| require f}
+ Dir.glob(File.expand_path("fixtures/*.rb", __dir__)).each { |f| require f }
end
end
end
@@ -73,6 +74,18 @@ end
class ActiveRecordTestCase < ActionController::TestCase
include ActiveRecord::TestFixtures
+ def self.tests(controller)
+ super
+ if defined? controller::ROUTES
+ include Module.new {
+ define_method(:setup) do
+ super()
+ @routes = controller::ROUTES
+ end
+ }
+ end
+ end
+
# Set our fixture path
if ActiveRecordTestConnector.able_to_connect
self.fixture_path = [FIXTURE_LOAD_PATH]
diff --git a/actionview/test/activerecord/controller_runtime_test.rb b/actionview/test/activerecord/controller_runtime_test.rb
index af91348d76..563044f11e 100644
--- a/actionview/test/activerecord/controller_runtime_test.rb
+++ b/actionview/test/activerecord/controller_runtime_test.rb
@@ -1,19 +1,21 @@
-require 'active_record_unit'
-require 'active_record/railties/controller_runtime'
-require 'fixtures/project'
-require 'active_support/log_subscriber/test_helper'
-require 'action_controller/log_subscriber'
+# frozen_string_literal: true
+
+require "active_record_unit"
+require "active_record/railties/controller_runtime"
+require "fixtures/project"
+require "active_support/log_subscriber/test_helper"
+require "action_controller/log_subscriber"
ActionController::Base.include(ActiveRecord::Railties::ControllerRuntime)
class ControllerRuntimeLogSubscriberTest < ActionController::TestCase
class LogSubscriberController < ActionController::Base
def show
- render :inline => "<%= Project.all %>"
+ render inline: "<%= Project.all %>"
end
def zero
- render :inline => "Zero DB runtime"
+ render inline: "Zero DB runtime"
end
def create
@@ -24,11 +26,11 @@ class ControllerRuntimeLogSubscriberTest < ActionController::TestCase
def redirect
Project.all
- redirect_to :action => 'show'
+ redirect_to action: "show"
end
def db_after_render
- render :inline => "Hello world"
+ render inline: "Hello world"
Project.all
ActiveRecord::LogSubscriber.runtime += 100
end
@@ -37,9 +39,17 @@ class ControllerRuntimeLogSubscriberTest < ActionController::TestCase
include ActiveSupport::LogSubscriber::TestHelper
tests LogSubscriberController
+ with_routes do
+ get :show, to: "#{LogSubscriberController.controller_path}#show"
+ get :zero, to: "#{LogSubscriberController.controller_path}#zero"
+ get :db_after_render, to: "#{LogSubscriberController.controller_path}#db_after_render"
+ get :redirect, to: "#{LogSubscriberController.controller_path}#redirect"
+ post :create, to: "#{LogSubscriberController.controller_path}#create"
+ end
+
def setup
- super
@old_logger = ActionController::Base.logger
+ super
ActionController::LogSubscriber.attach_to :action_controller
end
@@ -58,7 +68,7 @@ class ControllerRuntimeLogSubscriberTest < ActionController::TestCase
wait
assert_equal 2, @logger.logged(:info).size
- assert_match(/\(Views: [\d.]+ms \| ActiveRecord: [\d.]+ms\)/, @logger.logged(:info)[1])
+ assert_match(/\(Views: [\d.]+ms \| ActiveRecord: [\d.]+ms \| Allocations: [\d.]+\)/, @logger.logged(:info)[1])
end
def test_runtime_reset_before_requests
@@ -67,20 +77,20 @@ class ControllerRuntimeLogSubscriberTest < ActionController::TestCase
wait
assert_equal 2, @logger.logged(:info).size
- assert_match(/\(Views: [\d.]+ms \| ActiveRecord: 0.0ms\)/, @logger.logged(:info)[1])
+ assert_match(/\(Views: [\d.]+ms \| ActiveRecord: [\d.]+ms \| Allocations: [\d.]+\)/, @logger.logged(:info)[1])
end
def test_log_with_active_record_when_post
post :create
wait
- assert_match(/ActiveRecord: ([1-9][\d.]+)ms\)/, @logger.logged(:info)[2])
+ assert_match(/ActiveRecord: ([1-9][\d.]+)ms \| Allocations: [\d.]+\)/, @logger.logged(:info)[2])
end
def test_log_with_active_record_when_redirecting
get :redirect
wait
assert_equal 3, @logger.logged(:info).size
- assert_match(/\(ActiveRecord: [\d.]+ms\)/, @logger.logged(:info)[2])
+ assert_match(/\(ActiveRecord: [\d.]+ms \| Allocations: [\d.]+\)/, @logger.logged(:info)[2])
end
def test_include_time_query_time_after_rendering
@@ -88,6 +98,6 @@ class ControllerRuntimeLogSubscriberTest < ActionController::TestCase
wait
assert_equal 2, @logger.logged(:info).size
- assert_match(/\(Views: [\d.]+ms \| ActiveRecord: ([1-9][\d.]+)ms\)/, @logger.logged(:info)[1])
+ assert_match(/\(Views: [\d.]+ms \| ActiveRecord: ([1-9][\d.]+)ms \| Allocations: [\d.]+\)/, @logger.logged(:info)[1])
end
end
diff --git a/actionview/test/activerecord/debug_helper_test.rb b/actionview/test/activerecord/debug_helper_test.rb
index 03cb1d5a91..87a1791573 100644
--- a/actionview/test/activerecord/debug_helper_test.rb
+++ b/actionview/test/activerecord/debug_helper_test.rb
@@ -1,10 +1,15 @@
-require 'active_record_unit'
-require 'nokogiri'
+# frozen_string_literal: true
+
+require "active_record_unit"
+require "nokogiri"
class DebugHelperTest < ActionView::TestCase
def test_debug
company = Company.new(name: "firebase")
- assert_match "name: firebase", debug(company)
+ output = debug(company)
+ assert_match "name: name", output
+ assert_match "value_before_type_cast: firebase", output
+ assert_match "active_record_yaml_version: 2", output
end
def test_debug_with_marshal_error
diff --git a/actionview/test/activerecord/form_helper_activerecord_test.rb b/actionview/test/activerecord/form_helper_activerecord_test.rb
index 2769b97445..34655bfe23 100644
--- a/actionview/test/activerecord/form_helper_activerecord_test.rb
+++ b/actionview/test/activerecord/form_helper_activerecord_test.rb
@@ -1,6 +1,8 @@
-require 'active_record_unit'
-require 'fixtures/project'
-require 'fixtures/developer'
+# frozen_string_literal: true
+
+require "active_record_unit"
+require "fixtures/project"
+require "fixtures/developer"
class FormHelperActiveRecordTest < ActionView::TestCase
tests ActionView::Helpers::FormHelper
@@ -21,9 +23,12 @@ class FormHelperActiveRecordTest < ActionView::TestCase
@developer.projects << @project
@developer.save
+ super
+ @controller.singleton_class.include Routes.url_helpers
end
def teardown
+ super
Project.delete(321)
Developer.delete(123)
end
@@ -39,50 +44,50 @@ class FormHelperActiveRecordTest < ActionView::TestCase
def test_nested_fields_for_with_child_index_option_override_on_a_nested_attributes_collection_association
form_for(@developer) do |f|
- concat f.fields_for(:projects, @developer.projects.first, :child_index => 'abc') { |cf|
+ concat f.fields_for(:projects, @developer.projects.first, child_index: "abc") { |cf|
concat cf.text_field(:name)
}
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" />' +
+ 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_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;" />}
- def hidden_fields(method = nil)
- txt = %{<input name="utf8" type="hidden" value="&#x2713;" />}
+ if method && !%w(get post).include?(method.to_s)
+ txt << %{<input name="_method" type="hidden" value="#{method}" />}
+ end
- if method && !%w(get post).include?(method.to_s)
- txt << %{<input name="_method" type="hidden" value="#{method}" />}
+ txt
end
- txt
- end
+ def form_text(action = "/", id = nil, html_class = nil, remote = nil, multipart = nil, method = nil)
+ txt = +%{<form accept-charset="UTF-8" action="#{action}"}
+ txt << %{ enctype="multipart/form-data"} if multipart
+ txt << %{ data-remote="true"} if remote
+ txt << %{ class="#{html_class}"} if html_class
+ txt << %{ id="#{id}"} if id
+ method = method.to_s == "get" ? "get" : "post"
+ txt << %{ method="#{method}">}
+ end
- def form_text(action = "/", id = nil, html_class = nil, remote = nil, multipart = nil, method = nil)
- txt = %{<form accept-charset="UTF-8" action="#{action}"}
- txt << %{ enctype="multipart/form-data"} if multipart
- txt << %{ data-remote="true"} if remote
- 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, options = nil)
+ contents = block_given? ? yield : ""
- def whole_form(action = "/", id = nil, html_class = nil, options = nil)
- contents = block_given? ? yield : ""
+ if options.is_a?(Hash)
+ method, remote, multipart = options.values_at(:method, :remote, :multipart)
+ else
+ method = options
+ end
- if options.is_a?(Hash)
- method, remote, multipart = options.values_at(:method, :remote, :multipart)
- else
- method = options
+ form_text(action, id, html_class, remote, multipart, method) + hidden_fields(method) + contents + "</form>"
end
-
- form_text(action, id, html_class, remote, multipart, method) + hidden_fields(method) + contents + "</form>"
- end
end
diff --git a/actionview/test/activerecord/multifetch_cache_test.rb b/actionview/test/activerecord/multifetch_cache_test.rb
new file mode 100644
index 0000000000..12be069e69
--- /dev/null
+++ b/actionview/test/activerecord/multifetch_cache_test.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require "active_record_unit"
+require "active_record/railties/collection_cache_association_loading"
+
+ActionView::PartialRenderer.prepend(ActiveRecord::Railties::CollectionCacheAssociationLoading)
+
+class MultifetchCacheTest < ActiveRecordTestCase
+ fixtures :topics, :replies
+
+ def setup
+ view_paths = ActionController::Base.view_paths
+
+ @view = Class.new(ActionView::Base) do
+ def view_cache_dependencies
+ []
+ end
+
+ def combined_fragment_cache_key(key)
+ [ :views, key ]
+ end
+ end.new(view_paths, {})
+ end
+
+ def test_only_preloading_for_records_that_miss_the_cache
+ @view.render partial: "test/partial", collection: [topics(:rails)], cached: true
+
+ @topics = Topic.preload(:replies)
+
+ @view.render partial: "test/partial", collection: @topics, cached: true
+
+ assert_not @topics.detect { |topic| topic.id == topics(:rails).id }.replies.loaded?
+ assert @topics.detect { |topic| topic.id != topics(:rails).id }.replies.loaded?
+ end
+end
diff --git a/actionview/test/activerecord/polymorphic_routes_test.rb b/actionview/test/activerecord/polymorphic_routes_test.rb
index 34b2698c7f..724129a7d9 100644
--- a/actionview/test/activerecord/polymorphic_routes_test.rb
+++ b/actionview/test/activerecord/polymorphic_routes_test.rb
@@ -1,28 +1,30 @@
-require 'active_record_unit'
-require 'fixtures/project'
+# frozen_string_literal: true
+
+require "active_record_unit"
+require "fixtures/project"
class Task < ActiveRecord::Base
- self.table_name = 'projects'
+ self.table_name = "projects"
end
class Step < ActiveRecord::Base
- self.table_name = 'projects'
+ self.table_name = "projects"
end
class Bid < ActiveRecord::Base
- self.table_name = 'projects'
+ self.table_name = "projects"
end
class Tax < ActiveRecord::Base
- self.table_name = 'projects'
+ self.table_name = "projects"
end
class Fax < ActiveRecord::Base
- self.table_name = 'projects'
+ self.table_name = "projects"
end
class Series < ActiveRecord::Base
- self.table_name = 'projects'
+ self.table_name = "projects"
end
class ModelDelegator
@@ -41,17 +43,17 @@ class ModelDelegate
end
def to_param
- 'overridden'
+ "overridden"
end
end
-module Blog
+module Weblog
class Post < ActiveRecord::Base
- self.table_name = 'projects'
+ self.table_name = "projects"
end
class Blog < ActiveRecord::Base
- self.table_name = 'projects'
+ self.table_name = "projects"
end
def self.use_relative_model_naming?
@@ -60,10 +62,14 @@ module Blog
end
class PolymorphicRoutesTest < ActionController::TestCase
- include SharedTestRoutes.url_helpers
- self.default_url_options[:host] = 'example.com'
+ Routes = ActionDispatch::Routing::RouteSet.new
+ Routes.draw { }
+ include Routes.url_helpers
+
+ default_url_options[:host] = "example.com"
def setup
+ super
@project = Project.new
@task = Task.new
@step = Step.new
@@ -72,22 +78,21 @@ 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)
host = self.class.default_url_options[:host]
- assert_equal url.sub(/http:\/\/#{host}/, ''), polymorphic_path(args)
+ assert_equal url.sub(/http:\/\/#{host}/, ""), polymorphic_path(args)
assert_equal url, polymorphic_url(args)
assert_equal url, url_for(args)
end
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
@@ -95,7 +100,7 @@ class PolymorphicRoutesTest < ActionController::TestCase
def test_string_with_options
with_test_routes do
- assert_equal "http://example.com/projects?id=10", polymorphic_url("projects", :id => 10)
+ assert_equal "http://example.com/projects?id=10", polymorphic_url("projects", id: 10)
end
end
@@ -107,7 +112,7 @@ class PolymorphicRoutesTest < ActionController::TestCase
def test_symbol_with_options
with_test_routes do
- assert_equal "http://example.com/projects?id=10", polymorphic_url(:projects, :id => 10)
+ assert_equal "http://example.com/projects?id=10", polymorphic_url(:projects, id: 10)
end
end
@@ -179,7 +184,7 @@ class PolymorphicRoutesTest < ActionController::TestCase
def test_with_nil_id
with_test_routes do
exception = assert_raise ArgumentError do
- polymorphic_url({ :id => nil })
+ polymorphic_url(id: nil)
end
assert_equal "Nil location provided. Can't build URI.", exception.message
end
@@ -233,8 +238,8 @@ class PolymorphicRoutesTest < ActionController::TestCase
def test_class_with_options
with_test_routes do
- assert_equal "http://example.com/projects?foo=bar", polymorphic_url(@project.class, { :foo => :bar })
- assert_equal "/projects?foo=bar", polymorphic_path(@project.class, { :foo => :bar })
+ assert_equal "http://example.com/projects?foo=bar", polymorphic_url(@project.class, foo: :bar)
+ assert_equal "/projects?foo=bar", polymorphic_path(@project.class, foo: :bar)
end
end
@@ -274,7 +279,7 @@ class PolymorphicRoutesTest < ActionController::TestCase
def test_with_record_and_action
with_test_routes do
- assert_equal "http://example.com/projects/new", polymorphic_url(@project, :action => 'new')
+ assert_equal "http://example.com/projects/new", polymorphic_url(@project, action: "new")
end
end
@@ -303,35 +308,35 @@ class PolymorphicRoutesTest < ActionController::TestCase
def test_url_helper_prefixed_with_edit_with_url_options
with_test_routes do
@project.save
- assert_equal "http://example.com/projects/#{@project.id}/edit?param1=10", edit_polymorphic_url(@project, :param1 => '10')
+ assert_equal "http://example.com/projects/#{@project.id}/edit?param1=10", edit_polymorphic_url(@project, param1: "10")
end
end
def test_url_helper_with_url_options
with_test_routes do
@project.save
- assert_equal "http://example.com/projects/#{@project.id}?param1=10", polymorphic_url(@project, :param1 => '10')
+ assert_equal "http://example.com/projects/#{@project.id}?param1=10", polymorphic_url(@project, param1: "10")
end
end
def test_format_option
with_test_routes do
@project.save
- assert_equal "http://example.com/projects/#{@project.id}.pdf", polymorphic_url(@project, :format => :pdf)
+ assert_equal "http://example.com/projects/#{@project.id}.pdf", polymorphic_url(@project, format: :pdf)
end
end
def test_format_option_with_url_options
with_test_routes do
@project.save
- assert_equal "http://example.com/projects/#{@project.id}.pdf?param1=10", polymorphic_url(@project, :format => :pdf, :param1 => '10')
+ assert_equal "http://example.com/projects/#{@project.id}.pdf?param1=10", polymorphic_url(@project, format: :pdf, param1: "10")
end
end
def test_id_and_format_option
with_test_routes do
@project.save
- assert_equal "http://example.com/projects/#{@project.id}.pdf", polymorphic_url(:id => @project, :format => :pdf)
+ assert_equal "http://example.com/projects/#{@project.id}.pdf", polymorphic_url(id: @project, format: :pdf)
end
end
@@ -373,7 +378,7 @@ class PolymorphicRoutesTest < ActionController::TestCase
def test_new_with_array_and_namespace
with_admin_test_routes do
- assert_equal "http://example.com/admin/projects/new", polymorphic_url([:admin, @project], :action => 'new')
+ assert_equal "http://example.com/admin/projects/new", polymorphic_url([:admin, @project], action: "new")
end
end
@@ -426,7 +431,7 @@ class PolymorphicRoutesTest < ActionController::TestCase
with_test_routes do
@project.save
@task.save
- assert_equal "http://example.com/projects/#{@project.id}/bid/tasks/#{@task.id}.pdf", polymorphic_url([@project, :bid, @task], :format => :pdf)
+ assert_equal "http://example.com/projects/#{@project.id}/bid/tasks/#{@task.id}.pdf", polymorphic_url([@project, :bid, @task], format: :pdf)
end
end
@@ -474,13 +479,13 @@ class PolymorphicRoutesTest < ActionController::TestCase
def test_with_hash
with_test_routes do
@project.save
- assert_equal "http://example.com/projects/#{@project.id}", polymorphic_url(:id => @project)
+ assert_equal "http://example.com/projects/#{@project.id}", polymorphic_url(id: @project)
end
end
def test_polymorphic_path_accepts_options
with_test_routes do
- assert_equal "/projects/new", polymorphic_path(@project, :action => 'new')
+ assert_equal "/projects/new", polymorphic_path(@project, action: "new")
end
end
@@ -493,7 +498,7 @@ class PolymorphicRoutesTest < ActionController::TestCase
object_array = [:admin, @project, @task]
original_args = [object_array.dup, options.dup]
- assert_no_difference('object_array.size') { polymorphic_path(object_array, options) }
+ assert_no_difference("object_array.size") { polymorphic_path(object_array, options) }
assert_equal original_args, [object_array, options]
end
end
@@ -527,7 +532,7 @@ class PolymorphicRoutesTest < ActionController::TestCase
def test_with_irregular_plural_record_and_action
with_test_routes do
- assert_equal "http://example.com/taxes/new", polymorphic_url(@tax, :action => 'new')
+ assert_equal "http://example.com/taxes/new", polymorphic_url(@tax, action: "new")
end
end
@@ -561,7 +566,7 @@ class PolymorphicRoutesTest < ActionController::TestCase
def test_new_with_irregular_plural_array_and_namespace
with_admin_test_routes do
- assert_equal "http://example.com/admin/taxes/new", polymorphic_url([:admin, @tax], :action => 'new')
+ assert_equal "http://example.com/admin/taxes/new", polymorphic_url([:admin, @tax], action: "new")
end
end
@@ -598,7 +603,7 @@ class PolymorphicRoutesTest < ActionController::TestCase
end
end
- # Tests for uncountable names
+ # Tests for uncountable names
def test_uncountable_resource
with_test_routes do
@series.save
@@ -622,7 +627,7 @@ class PolymorphicRoutesTest < ActionController::TestCase
def with_namespaced_routes(name)
with_routing do |set|
set.draw do
- scope(:module => name) do
+ scope(module: name) do
resources :blogs do
resources :posts do
resources :faxes
@@ -728,6 +733,54 @@ class PolymorphicPathRoutesTest < PolymorphicRoutesTest
def assert_url(url, args)
host = self.class.default_url_options[:host]
- assert_equal url.sub(/http:\/\/#{host}/, ''), url_for(args)
+ 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
+ super
+ @category = Category.new("1")
+ @collection = Collection.new("2")
+ @product = Product.new("3")
+ @controller.singleton_class.include Routes.url_helpers
+ 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/relation_cache_test.rb b/actionview/test/activerecord/relation_cache_test.rb
new file mode 100644
index 0000000000..a6befc3ee5
--- /dev/null
+++ b/actionview/test/activerecord/relation_cache_test.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require "active_record_unit"
+
+class RelationCacheTest < ActionView::TestCase
+ tests ActionView::Helpers::CacheHelper
+
+ def setup
+ super
+ view_paths = ActionController::Base.view_paths
+ lookup_context = ActionView::LookupContext.new(view_paths, {}, ["test"])
+ @view_renderer = ActionView::Renderer.new(lookup_context)
+ @virtual_path = "path"
+
+ controller.cache_store = ActiveSupport::Cache::MemoryStore.new
+ end
+
+ def test_cache_relation_other
+ cache(Project.all) { concat("Hello World") }
+ assert_equal "Hello World", controller.cache_store.read("views/path/projects-#{Project.count}")
+ end
+
+ def view_cache_dependencies; []; 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 9772ebb39e..2bb3cfeb5b 100644
--- a/actionview/test/activerecord/render_partial_with_record_identification_test.rb
+++ b/actionview/test/activerecord/render_partial_with_record_identification_test.rb
@@ -1,48 +1,50 @@
-require 'active_record_unit'
+# frozen_string_literal: true
+
+require "active_record_unit"
class RenderPartialWithRecordIdentificationController < ActionController::Base
+ ROUTES = test_routes do
+ get :render_with_record_collection, to: "render_partial_with_record_identification#render_with_record_collection"
+ get :render_with_scope, to: "render_partial_with_record_identification#render_with_scope"
+ get :render_with_record, to: "render_partial_with_record_identification#render_with_record"
+ get :render_with_has_many_association, to: "render_partial_with_record_identification#render_with_has_many_association"
+ get :render_with_has_many_and_belongs_to_association, to: "render_partial_with_record_identification#render_with_has_many_and_belongs_to_association"
+ get :render_with_has_one_association, to: "render_partial_with_record_identification#render_with_has_one_association"
+ get :render_with_record_collection_and_spacer_template, to: "render_partial_with_record_identification#render_with_record_collection_and_spacer_template"
+ end
+
def render_with_has_many_and_belongs_to_association
@developer = Developer.find(1)
- render :partial => @developer.projects
+ render partial: @developer.projects
end
def render_with_has_many_association
@topic = Topic.find(1)
- render :partial => @topic.replies
+ render partial: @topic.replies
end
def render_with_scope
- render :partial => Reply.base
- end
-
- def render_with_has_many_through_association
- @developer = Developer.first
- render :partial => @developer.topics
+ render partial: Reply.base
end
def render_with_has_one_association
@company = Company.find(1)
- render :partial => @company.mascot
- end
-
- def render_with_belongs_to_association
- @reply = Reply.find(1)
- render :partial => @reply.topic
+ render partial: @company.mascot
end
def render_with_record
@developer = Developer.first
- render :partial => @developer
+ render partial: @developer
end
def render_with_record_collection
@developers = Developer.all
- render :partial => @developers
+ render partial: @developers
end
def render_with_record_collection_and_spacer_template
@developer = Developer.find(1)
- render :partial => @developer.projects, :spacer_template => 'test/partial_only'
+ render partial: @developer.projects, spacer_template: "test/partial_only"
end
end
@@ -57,27 +59,27 @@ class RenderPartialWithRecordIdentificationTest < ActiveRecordTestCase
def test_rendering_partial_with_has_many_association
get :render_with_has_many_association
- assert_equal 'Birdman is better!', @response.body
+ assert_equal "Birdman is better!", @response.body
end
def test_rendering_partial_with_scope
get :render_with_scope
- assert_equal 'Birdman is better!Nuh uh!', @response.body
+ assert_equal "Birdman is better!Nuh uh!", @response.body
end
def test_render_with_record
get :render_with_record
- assert_equal 'David', @response.body
+ assert_equal "David", @response.body
end
def test_render_with_record_collection
get :render_with_record_collection
- assert_equal 'DavidJamisfixture_3fixture_4fixture_5fixture_6fixture_7fixture_8fixture_9fixture_10Jamis', @response.body
+ assert_equal "DavidJamisfixture_3fixture_4fixture_5fixture_6fixture_7fixture_8fixture_9fixture_10Jamis", @response.body
end
def test_render_with_record_collection_and_spacer_template
get :render_with_record_collection_and_spacer_template
- assert_equal Developer.find(1).projects.map(&:name).join('only partial'), @response.body
+ assert_equal Developer.find(1).projects.map(&:name).join("only partial"), @response.body
end
def test_rendering_partial_with_has_one_association
@@ -87,7 +89,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
@@ -97,23 +99,33 @@ end
module Fun
class NestedController < ActionController::Base
+ ROUTES = test_routes do
+ get :render_with_record_in_nested_controller, to: "fun/nested#render_with_record_in_nested_controller"
+ get :render_with_record_collection_in_nested_controller, to: "fun/nested#render_with_record_collection_in_nested_controller"
+ end
+
def render_with_record_in_nested_controller
- render :partial => Game.new("Pong")
+ render partial: Game.new("Pong")
end
def render_with_record_collection_in_nested_controller
- render :partial => [ Game.new("Pong"), Game.new("Tank") ]
+ render partial: [ Game.new("Pong"), Game.new("Tank") ]
end
end
module Serious
class NestedDeeperController < ActionController::Base
+ ROUTES = test_routes do
+ get :render_with_record_in_deeper_nested_controller, to: "fun/serious/nested_deeper#render_with_record_in_deeper_nested_controller"
+ get :render_with_record_collection_in_deeper_nested_controller, to: "fun/serious/nested_deeper#render_with_record_collection_in_deeper_nested_controller"
+ end
+
def render_with_record_in_deeper_nested_controller
- render :partial => Game.new("Chess")
+ render partial: Game.new("Chess")
end
def render_with_record_collection_in_deeper_nested_controller
- render :partial => [ Game.new("Chess"), Game.new("Sudoku"), Game.new("Solitaire") ]
+ render partial: [ Game.new("Chess"), Game.new("Sudoku"), Game.new("Solitaire") ]
end
end
end
diff --git a/actionview/test/fixtures/actionpack/layouts/builder.builder b/actionview/test/fixtures/actionpack/layouts/builder.builder
index 7c7d4b2dd1..c55488edd0 100644
--- a/actionview/test/fixtures/actionpack/layouts/builder.builder
+++ b/actionview/test/fixtures/actionpack/layouts/builder.builder
@@ -1,3 +1,3 @@
xml.wrapper do
xml << yield
-end \ No newline at end of file
+end
diff --git a/actionview/test/fixtures/actionpack/layouts/standard.text.erb b/actionview/test/fixtures/actionpack/layouts/standard.text.erb
new file mode 100644
index 0000000000..a58afb1aa2
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/layouts/standard.text.erb
@@ -0,0 +1 @@
+{{<%= yield %><%= @variable_for_layout %>}}
diff --git a/actionview/test/fixtures/actionpack/test/_hello.builder b/actionview/test/fixtures/actionpack/test/_hello.builder
index ef52f632d1..fc72df16d0 100644
--- a/actionview/test/fixtures/actionpack/test/_hello.builder
+++ b/actionview/test/fixtures/actionpack/test/_hello.builder
@@ -1 +1 @@
-xm.hello \ No newline at end of file
+xm.hello
diff --git a/actionview/test/fixtures/actionpack/test/formatted_xml_erb.builder b/actionview/test/fixtures/actionpack/test/formatted_xml_erb.builder
index 14fd3549fb..f98aaa34a5 100644
--- a/actionview/test/fixtures/actionpack/test/formatted_xml_erb.builder
+++ b/actionview/test/fixtures/actionpack/test/formatted_xml_erb.builder
@@ -1 +1 @@
-xml.test 'failed' \ No newline at end of file
+xml.test "failed"
diff --git a/actionview/test/fixtures/actionpack/test/hello.builder b/actionview/test/fixtures/actionpack/test/hello.builder
index a471553941..b8ab17ad5b 100644
--- a/actionview/test/fixtures/actionpack/test/hello.builder
+++ b/actionview/test/fixtures/actionpack/test/hello.builder
@@ -1,4 +1,4 @@
xml.html do
xml.p "Hello #{@name}"
- xml << render(:file => "test/greeting")
-end \ No newline at end of file
+ xml << render(file: "test/greeting")
+end
diff --git a/actionview/test/fixtures/actionpack/test/hello_world_container.builder b/actionview/test/fixtures/actionpack/test/hello_world_container.builder
index e48d75c405..24032ab5e0 100644
--- a/actionview/test/fixtures/actionpack/test/hello_world_container.builder
+++ b/actionview/test/fixtures/actionpack/test/hello_world_container.builder
@@ -1,3 +1,3 @@
xml.test do
- render :partial => 'hello', :locals => { :xm => xml }
-end \ No newline at end of file
+ render partial: "hello", locals: { xm: xml }
+end
diff --git a/actionview/test/fixtures/actionpack/test/hello_xml_world.builder b/actionview/test/fixtures/actionpack/test/hello_xml_world.builder
index e7081b89fe..d16bb6b5cb 100644
--- a/actionview/test/fixtures/actionpack/test/hello_xml_world.builder
+++ b/actionview/test/fixtures/actionpack/test/hello_xml_world.builder
@@ -8,4 +8,4 @@ xml.html do
xml.p "monks"
xml.p "wiseguys"
end
-end \ No newline at end of file
+end
diff --git a/actionview/test/fixtures/actionpack/test/non_erb_block_content_for.builder b/actionview/test/fixtures/actionpack/test/non_erb_block_content_for.builder
index d539a425a4..cd65da751b 100644
--- a/actionview/test/fixtures/actionpack/test/non_erb_block_content_for.builder
+++ b/actionview/test/fixtures/actionpack/test/non_erb_block_content_for.builder
@@ -1,4 +1,4 @@
content_for :title do
- 'Putting stuff in the title!'
+ "Putting stuff in the title!"
end
xml << "Great stuff!"
diff --git a/actionview/test/fixtures/comments/empty.html+grid.erb b/actionview/test/fixtures/comments/empty.html+grid.erb
new file mode 100644
index 0000000000..dc3fa32a81
--- /dev/null
+++ b/actionview/test/fixtures/comments/empty.html+grid.erb
@@ -0,0 +1 @@
+<h1>No Comment</h1>
diff --git a/actionview/test/fixtures/comments/empty.html.builder b/actionview/test/fixtures/comments/empty.html.builder
index 2b0c7207a3..12d6fdd9a5 100644
--- a/actionview/test/fixtures/comments/empty.html.builder
+++ b/actionview/test/fixtures/comments/empty.html.builder
@@ -1 +1 @@
-xml.h1 'No Comment' \ No newline at end of file
+xml.h1 "No Comment"
diff --git a/actionview/test/fixtures/company.rb b/actionview/test/fixtures/company.rb
index f3ac3642fa..93afdd5472 100644
--- a/actionview/test/fixtures/company.rb
+++ b/actionview/test/fixtures/company.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
class Company < ActiveRecord::Base
has_one :mascot
self.sequence_name = :companies_nonstd_seq
validates_presence_of :name
def validate
- errors.add('rating', 'rating should not be 2') if rating == 2
+ errors.add("rating", "rating should not be 2") if rating == 2
end
end
diff --git a/actionview/test/fixtures/developer.rb b/actionview/test/fixtures/developer.rb
index 8b3f0a8039..cb7ee49eed 100644
--- a/actionview/test/fixtures/developer.rb
+++ b/actionview/test/fixtures/developer.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
class Developer < ActiveRecord::Base
has_and_belongs_to_many :projects
has_many :replies
- has_many :topics, :through => :replies
+ has_many :topics, through: :replies
accepts_nested_attributes_for :projects
end
diff --git a/actionview/test/fixtures/digestor/api/comments/_comment.json.erb b/actionview/test/fixtures/digestor/api/comments/_comment.json.erb
new file mode 100644
index 0000000000..696eb13917
--- /dev/null
+++ b/actionview/test/fixtures/digestor/api/comments/_comment.json.erb
@@ -0,0 +1 @@
+{"content": "Great story!"}
diff --git a/actionview/test/fixtures/digestor/api/comments/_comments.json.erb b/actionview/test/fixtures/digestor/api/comments/_comments.json.erb
new file mode 100644
index 0000000000..c28646a283
--- /dev/null
+++ b/actionview/test/fixtures/digestor/api/comments/_comments.json.erb
@@ -0,0 +1 @@
+<%= render partial: "comments/comment", collection: commentable.comments %>
diff --git a/actionview/test/fixtures/digestor/comments/_comment.html.erb b/actionview/test/fixtures/digestor/comments/_comment.html.erb
index f172e749da..a8fa21f644 100644
--- a/actionview/test/fixtures/digestor/comments/_comment.html.erb
+++ b/actionview/test/fixtures/digestor/comments/_comment.html.erb
@@ -1 +1 @@
-Great story, bro!
+Great story!
diff --git a/actionview/test/fixtures/digestor/comments/show.js.erb b/actionview/test/fixtures/digestor/comments/show.js.erb
new file mode 100644
index 0000000000..38b37dfa2b
--- /dev/null
+++ b/actionview/test/fixtures/digestor/comments/show.js.erb
@@ -0,0 +1 @@
+alert("<%=j render("comments/comment") %>")
diff --git a/actionpack/test/tmp/.gitignore b/actionview/test/fixtures/digestor/events/_completed.html.erb
index e69de29bb2..e69de29bb2 100644
--- a/actionpack/test/tmp/.gitignore
+++ b/actionview/test/fixtures/digestor/events/_completed.html.erb
diff --git a/actionview/test/fixtures/digestor/events/index.html.erb b/actionview/test/fixtures/digestor/events/index.html.erb
new file mode 100644
index 0000000000..bc45e41bcb
--- /dev/null
+++ b/actionview/test/fixtures/digestor/events/index.html.erb
@@ -0,0 +1 @@
+<% # Template Dependency: events/* %> \ No newline at end of file
diff --git a/actionview/test/fixtures/digestor/messages/peek.html.erb b/actionview/test/fixtures/digestor/messages/peek.html.erb
new file mode 100644
index 0000000000..84885ab0bc
--- /dev/null
+++ b/actionview/test/fixtures/digestor/messages/peek.html.erb
@@ -0,0 +1,2 @@
+<%# Template Dependency: messages/message %>
+<%= render "comments/comments" %>
diff --git a/actionview/test/fixtures/digestor/messages/thread.json.erb b/actionview/test/fixtures/digestor/messages/thread.json.erb
new file mode 100644
index 0000000000..e4c1ba97cd
--- /dev/null
+++ b/actionview/test/fixtures/digestor/messages/thread.json.erb
@@ -0,0 +1 @@
+<%= render "comments/comments" %>
diff --git a/actionview/test/fixtures/helpers/abc_helper.rb b/actionview/test/fixtures/helpers/abc_helper.rb
index cf2774bb5f..999b9b5c6e 100644
--- a/actionview/test/fixtures/helpers/abc_helper.rb
+++ b/actionview/test/fixtures/helpers/abc_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module AbcHelper
def bare_a() end
end
diff --git a/actionview/test/fixtures/helpers/helpery_test_helper.rb b/actionview/test/fixtures/helpers/helpery_test_helper.rb
index a4f2951efa..9836143848 100644
--- a/actionview/test/fixtures/helpers/helpery_test_helper.rb
+++ b/actionview/test/fixtures/helpers/helpery_test_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module HelperyTestHelper
def helpery_test
"Default"
diff --git a/actionview/test/fixtures/helpers_missing/invalid_require_helper.rb b/actionview/test/fixtures/helpers_missing/invalid_require_helper.rb
index d8801e54d5..c77121046d 100644
--- a/actionview/test/fixtures/helpers_missing/invalid_require_helper.rb
+++ b/actionview/test/fixtures/helpers_missing/invalid_require_helper.rb
@@ -1,5 +1,6 @@
-require 'very_invalid_file_name'
+# frozen_string_literal: true
+
+require "very_invalid_file_name"
module InvalidRequireHelper
end
-
diff --git a/actionview/test/fixtures/layouts/render_partial_html.erb b/actionview/test/fixtures/layouts/render_partial_html.erb
new file mode 100644
index 0000000000..d4dbb6c76c
--- /dev/null
+++ b/actionview/test/fixtures/layouts/render_partial_html.erb
@@ -0,0 +1,2 @@
+<%= render :partial => 'test/partialhtml' %>
+<%= yield %>
diff --git a/actionview/test/fixtures/layouts/streaming_with_locale.erb b/actionview/test/fixtures/layouts/streaming_with_locale.erb
new file mode 100644
index 0000000000..e1fdad2073
--- /dev/null
+++ b/actionview/test/fixtures/layouts/streaming_with_locale.erb
@@ -0,0 +1,2 @@
+layout.locale: <%= I18n.locale %>
+<%= yield %>
diff --git a/actionview/test/fixtures/mascot.rb b/actionview/test/fixtures/mascot.rb
index f9f1448b8f..26a2c7bbe1 100644
--- a/actionview/test/fixtures/mascot.rb
+++ b/actionview/test/fixtures/mascot.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Mascot < ActiveRecord::Base
belongs_to :company
-end \ No newline at end of file
+end
diff --git a/actionview/test/fixtures/project.rb b/actionview/test/fixtures/project.rb
index c124a9e605..019ddb7aef 100644
--- a/actionview/test/fixtures/project.rb
+++ b/actionview/test/fixtures/project.rb
@@ -1,3 +1,9 @@
+# frozen_string_literal: true
+
class Project < ActiveRecord::Base
has_and_belongs_to_many :developers, -> { uniq }
+
+ def self.collection_cache_key(collection = all, timestamp_column = :updated_at)
+ "projects-#{collection.count}"
+ end
end
diff --git a/actionview/test/fixtures/public/.gitignore b/actionview/test/fixtures/public/.gitignore
deleted file mode 100644
index 312e635ee6..0000000000
--- a/actionview/test/fixtures/public/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-absolute/*
diff --git a/actionview/test/fixtures/reply.rb b/actionview/test/fixtures/reply.rb
index 047522c55b..b2b662e1b5 100644
--- a/actionview/test/fixtures/reply.rb
+++ b/actionview/test/fixtures/reply.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Reply < ActiveRecord::Base
scope :base, -> { all }
belongs_to :topic, -> { includes(:replies) }
diff --git a/actionview/test/fixtures/ruby_template.ruby b/actionview/test/fixtures/ruby_template.ruby
index 5097bce47c..3e0bc445a2 100644
--- a/actionview/test/fixtures/ruby_template.ruby
+++ b/actionview/test/fixtures/ruby_template.ruby
@@ -1,2 +1,2 @@
-body = ""
+body = +""
body << ["Hello", "from", "Ruby", "code"].join(" ")
diff --git a/actionview/test/fixtures/test/_builder_tag_nested_in_content_tag.erb b/actionview/test/fixtures/test/_builder_tag_nested_in_content_tag.erb
new file mode 100644
index 0000000000..ddad7ec3ac
--- /dev/null
+++ b/actionview/test/fixtures/test/_builder_tag_nested_in_content_tag.erb
@@ -0,0 +1,3 @@
+<%= tag.p do %>
+ <%= tag.b 'Hello' %>
+<% end %>
diff --git a/actionview/test/fixtures/test/_cached_nested_cached_customer.erb b/actionview/test/fixtures/test/_cached_nested_cached_customer.erb
new file mode 100644
index 0000000000..01bf025cd3
--- /dev/null
+++ b/actionview/test/fixtures/test/_cached_nested_cached_customer.erb
@@ -0,0 +1,3 @@
+<% cache cached_customer do %>
+ <%= render partial: "test/cached_customer", locals: { cached_customer: cached_customer } %>
+<% end %>
diff --git a/actionview/test/fixtures/test/_customer.mobile.erb b/actionview/test/fixtures/test/_customer.mobile.erb
new file mode 100644
index 0000000000..d8220afeda
--- /dev/null
+++ b/actionview/test/fixtures/test/_customer.mobile.erb
@@ -0,0 +1 @@
+Hello: <%= customer.name rescue "Anonymous" %> \ No newline at end of file
diff --git a/actionview/test/fixtures/test/_klass.erb b/actionview/test/fixtures/test/_klass.erb
new file mode 100644
index 0000000000..9936f86001
--- /dev/null
+++ b/actionview/test/fixtures/test/_klass.erb
@@ -0,0 +1 @@
+<%= klass.class.name %> \ No newline at end of file
diff --git a/actionview/test/fixtures/test/_nested_cached_customer.erb b/actionview/test/fixtures/test/_nested_cached_customer.erb
new file mode 100644
index 0000000000..f43adc94c9
--- /dev/null
+++ b/actionview/test/fixtures/test/_nested_cached_customer.erb
@@ -0,0 +1 @@
+<%= render partial: "test/cached_customer", locals: { cached_customer: cached_customer } %>
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/_partial_with_variants.html+grid.erb b/actionview/test/fixtures/test/_partial_with_variants.html+grid.erb
new file mode 100644
index 0000000000..225363c8c3
--- /dev/null
+++ b/actionview/test/fixtures/test/_partial_with_variants.html+grid.erb
@@ -0,0 +1 @@
+<h1>Partial with variants</h1>
diff --git a/actionview/test/fixtures/test/_partialhtml.html b/actionview/test/fixtures/test/_partialhtml.html
new file mode 100644
index 0000000000..afe39b730a
--- /dev/null
+++ b/actionview/test/fixtures/test/_partialhtml.html
@@ -0,0 +1 @@
+<h1>partial html</h1> \ No newline at end of file
diff --git a/actionview/test/fixtures/test/_🍣.erb b/actionview/test/fixtures/test/_🍣.erb
new file mode 100644
index 0000000000..4bbe59410a
--- /dev/null
+++ b/actionview/test/fixtures/test/_🍣.erb
@@ -0,0 +1 @@
+🍣
diff --git a/actionview/test/fixtures/test/hello.builder b/actionview/test/fixtures/test/hello.builder
index a471553941..b8ab17ad5b 100644
--- a/actionview/test/fixtures/test/hello.builder
+++ b/actionview/test/fixtures/test/hello.builder
@@ -1,4 +1,4 @@
xml.html do
xml.p "Hello #{@name}"
- xml << render(:file => "test/greeting")
-end \ No newline at end of file
+ xml << render(file: "test/greeting")
+end
diff --git a/actionview/test/fixtures/test/render_file_inspect_local_assigns.erb b/actionview/test/fixtures/test/render_file_inspect_local_assigns.erb
new file mode 100644
index 0000000000..aea5c351c5
--- /dev/null
+++ b/actionview/test/fixtures/test/render_file_inspect_local_assigns.erb
@@ -0,0 +1 @@
+<%= local_assigns.inspect.html_safe %> \ No newline at end of file
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/render_file_unicode_local.erb b/actionview/test/fixtures/test/render_file_unicode_local.erb
new file mode 100644
index 0000000000..cbfd040a76
--- /dev/null
+++ b/actionview/test/fixtures/test/render_file_unicode_local.erb
@@ -0,0 +1 @@
+<%= 🎃 %> \ No newline at end of file
diff --git a/actionview/test/fixtures/test/render_file_with_ruby_keyword_locals.erb b/actionview/test/fixtures/test/render_file_with_ruby_keyword_locals.erb
new file mode 100644
index 0000000000..7e3fe6c6d9
--- /dev/null
+++ b/actionview/test/fixtures/test/render_file_with_ruby_keyword_locals.erb
@@ -0,0 +1 @@
+The class is <%= local_assigns[:class] %> \ No newline at end of file
diff --git a/actionview/test/fixtures/test/streaming_with_locale.erb b/actionview/test/fixtures/test/streaming_with_locale.erb
new file mode 100644
index 0000000000..b0f2b2f7e9
--- /dev/null
+++ b/actionview/test/fixtures/test/streaming_with_locale.erb
@@ -0,0 +1 @@
+view.locale: <%= I18n.locale %>
diff --git a/actionview/test/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/fixtures/topic.rb b/actionview/test/fixtures/topic.rb
index 9fa9746535..ff194ce567 100644
--- a/actionview/test/fixtures/topic.rb
+++ b/actionview/test/fixtures/topic.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Topic < ActiveRecord::Base
- has_many :replies, :dependent => :destroy
+ has_many :replies, dependent: :destroy
end
diff --git a/actionview/test/lib/controller/fake_models.rb b/actionview/test/lib/controller/fake_models.rb
index 65c68fc34a..f8b7ddaecc 100644
--- a/actionview/test/lib/controller/fake_models.rb
+++ b/actionview/test/lib/controller/fake_models.rb
@@ -1,12 +1,14 @@
+# frozen_string_literal: true
+
require "active_model"
-class Customer < Struct.new(:name, :id)
+Customer = Struct.new(:name, :id) do
extend ActiveModel::Naming
include ActiveModel::Conversion
undef_method :to_json
- def to_xml(options={})
+ def to_xml(options = {})
if options[:builder]
options[:builder].name name
else
@@ -14,7 +16,7 @@ class Customer < Struct.new(:name, :id)
end
end
- def to_js(options={})
+ def to_js(options = {})
"name: #{name.inspect}"
end
alias :to_text :to_js
@@ -26,12 +28,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 +86,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,14 +107,13 @@ 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
attr_accessor :relevances
def relevances_attributes=(attributes); end
-
end
class CommentRelevance
@@ -121,7 +126,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
@@ -137,7 +142,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
@@ -164,7 +169,7 @@ module Blog
true
end
- class Post < Struct.new(:title, :id)
+ Post = Struct.new(:title, :id) do
extend ActiveModel::Naming
include ActiveModel::Conversion
@@ -184,14 +189,13 @@ class ArelLike
end
end
-class Car < Struct.new(:color)
-end
+Car = Struct.new(:color)
class Plane
attr_reader :to_key
def model_name
- OpenStruct.new param_key: 'airplane'
+ OpenStruct.new param_key: "airplane"
end
def save
diff --git a/actionview/test/template/active_model_helper_test.rb b/actionview/test/template/active_model_helper_test.rb
index 86bccdfade..36afed6dd6 100644
--- a/actionview/test/template/active_model_helper_test.rb
+++ b/actionview/test/template/active_model_helper_test.rb
@@ -1,10 +1,12 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
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, :category, :published, :updated_at) do
include ActiveModel::Conversion
include ActiveModel::Validations
@@ -20,10 +22,14 @@ class ActiveModelHelperTest < ActionView::TestCase
@post = Post.new
@post.errors[:author_name] << "can't be empty"
@post.errors[:body] << "foo"
+ @post.errors[:category] << "must exist"
+ @post.errors[:published] << "must be accepted"
@post.errors[:updated_at] << "bar"
@post.author_name = ""
@post.body = "Back to the hill and over it again!"
+ @post.category = "rails"
+ @post.published = false
@post.updated_at = Date.new(2004, 6, 15)
end
@@ -50,28 +56,96 @@ class ActiveModelHelperTest < ActionView::TestCase
def test_select_with_errors_and_blank_option
expected_dom = %(<div class="field_with_errors"><select name="post[author_name]" id="post_author_name"><option value="">Choose one...</option>\n<option value="a">a</option>\n<option value="b">b</option></select></div>)
- assert_dom_equal(expected_dom, select("post", "author_name", [:a, :b], :include_blank => 'Choose one...'))
- assert_dom_equal(expected_dom, select("post", "author_name", [:a, :b], :prompt => 'Choose one...'))
+ assert_dom_equal(expected_dom, select("post", "author_name", [:a, :b], include_blank: "Choose one..."))
+ assert_dom_equal(expected_dom, select("post", "author_name", [:a, :b], prompt: "Choose one..."))
+ end
+
+ def test_select_grouped_options_with_errors
+ grouped_options = [
+ ["A", [["A1"], ["A2"]]],
+ ["B", [["B1"], ["B2"]]],
+ ]
+
+ assert_dom_equal(
+ %(<div class="field_with_errors"><select name="post[category]" id="post_category"><optgroup label="A"><option value="A1">A1</option>\n<option value="A2">A2</option></optgroup><optgroup label="B"><option value="B1">B1</option>\n<option value="B2">B2</option></optgroup></select></div>),
+ select("post", "category", grouped_options)
+ )
+ end
+
+ def test_collection_select_with_errors
+ assert_dom_equal(
+ %(<div class="field_with_errors"><select name="post[author_name]" id="post_author_name"><option value="a">a</option>\n<option value="b">b</option></select></div>),
+ collection_select("post", "author_name", [:a, :b], :to_s, :to_s)
+ )
end
def test_date_select_with_errors
assert_dom_equal(
%(<div class="field_with_errors"><select id="post_updated_at_1i" name="post[updated_at(1i)]">\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n</select>\n<input id="post_updated_at_2i" name="post[updated_at(2i)]" type="hidden" value="6" />\n<input id="post_updated_at_3i" name="post[updated_at(3i)]" type="hidden" value="1" />\n</div>),
- date_select("post", "updated_at", :discard_month => true, :discard_day => true, :start_year => 2004, :end_year => 2005)
+ date_select("post", "updated_at", discard_month: true, discard_day: true, start_year: 2004, end_year: 2005)
)
end
def test_datetime_select_with_errors
assert_dom_equal(
%(<div class="field_with_errors"><input id="post_updated_at_1i" name="post[updated_at(1i)]" type="hidden" value="2004" />\n<input id="post_updated_at_2i" name="post[updated_at(2i)]" type="hidden" value="6" />\n<input id="post_updated_at_3i" name="post[updated_at(3i)]" type="hidden" value="1" />\n<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n<option selected="selected" value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n</select>\n : <select id="post_updated_at_5i" name="post[updated_at(5i)]">\n<option selected="selected" value="00">00</option>\n</select>\n</div>),
- datetime_select("post", "updated_at", :discard_year => true, :discard_month => true, :discard_day => true, :minute_step => 60)
+ datetime_select("post", "updated_at", discard_year: true, discard_month: true, discard_day: true, minute_step: 60)
)
end
def test_time_select_with_errors
assert_dom_equal(
%(<div class="field_with_errors"><input id="post_updated_at_1i" name="post[updated_at(1i)]" type="hidden" value="2004" />\n<input id="post_updated_at_2i" name="post[updated_at(2i)]" type="hidden" value="6" />\n<input id="post_updated_at_3i" name="post[updated_at(3i)]" type="hidden" value="15" />\n<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n<option selected="selected" value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n</select>\n : <select id="post_updated_at_5i" name="post[updated_at(5i)]">\n<option selected="selected" value="00">00</option>\n</select>\n</div>),
- time_select("post", "updated_at", :minute_step => 60)
+ time_select("post", "updated_at", minute_step: 60)
+ )
+ end
+
+ def test_label_with_errors
+ assert_dom_equal(
+ %(<div class="field_with_errors"><label for="post_body">Body</label></div>),
+ label("post", "body")
+ )
+ end
+
+ def test_check_box_with_errors
+ assert_dom_equal(
+ %(<input name="post[published]" type="hidden" value="0" /><div class="field_with_errors"><input type="checkbox" value="1" name="post[published]" id="post_published" /></div>),
+ check_box("post", "published")
+ )
+ end
+
+ def test_check_boxes_with_errors
+ assert_dom_equal(
+ %(<input name="post[published]" type="hidden" value="0" /><div class="field_with_errors"><input type="checkbox" value="1" name="post[published]" id="post_published" /></div><input name="post[published]" type="hidden" value="0" /><div class="field_with_errors"><input type="checkbox" value="1" name="post[published]" id="post_published" /></div>),
+ check_box("post", "published") + check_box("post", "published")
+ )
+ end
+
+ def test_radio_button_with_errors
+ assert_dom_equal(
+ %(<div class="field_with_errors"><input type="radio" value="rails" checked="checked" name="post[category]" id="post_category_rails" /></div>),
+ radio_button("post", "category", "rails")
+ )
+ end
+
+ def test_radio_buttons_with_errors
+ assert_dom_equal(
+ %(<div class="field_with_errors"><input type="radio" value="rails" checked="checked" name="post[category]" id="post_category_rails" /></div><div class="field_with_errors"><input type="radio" value="java" name="post[category]" id="post_category_java" /></div>),
+ radio_button("post", "category", "rails") + radio_button("post", "category", "java")
+ )
+ end
+
+ def test_collection_check_boxes_with_errors
+ assert_dom_equal(
+ %(<input type="hidden" name="post[category][]" value="" /><div class="field_with_errors"><input type="checkbox" value="ruby" name="post[category][]" id="post_category_ruby" /></div><label for="post_category_ruby">ruby</label><div class="field_with_errors"><input type="checkbox" value="java" name="post[category][]" id="post_category_java" /></div><label for="post_category_java">java</label>),
+ collection_check_boxes("post", "category", [:ruby, :java], :to_s, :to_s)
+ )
+ end
+
+ def test_collection_radio_buttons_with_errors
+ assert_dom_equal(
+ %(<input type="hidden" name="post[category]" value="" /><div class="field_with_errors"><input type="radio" value="ruby" name="post[category]" id="post_category_ruby" /></div><label for="post_category_ruby">ruby</label><div class="field_with_errors"><input type="radio" value="java" name="post[category]" id="post_category_java" /></div><label for="post_category_java">java</label>),
+ collection_radio_buttons("post", "category", [:ruby, :java], :to_s, :to_s)
)
end
@@ -85,7 +159,7 @@ class ActiveModelHelperTest < ActionView::TestCase
def test_field_error_proc
old_proc = ActionView::Base.field_error_proc
ActionView::Base.field_error_proc = Proc.new do |html_tag, instance|
- %(<div class=\"field_with_errors\">#{html_tag} <span class="error">#{[instance.error_message].join(', ')}</span></div>).html_safe
+ raw(%(<div class=\"field_with_errors\">#{html_tag} <span class="error">#{[instance.error_message].join(', ')}</span></div>))
end
assert_dom_equal(
@@ -95,5 +169,4 @@ class ActiveModelHelperTest < ActionView::TestCase
ensure
ActionView::Base.field_error_proc = old_proc if old_proc
end
-
end
diff --git a/actionview/test/template/asset_tag_helper_test.rb b/actionview/test/template/asset_tag_helper_test.rb
index 01fc66bed6..e68f03d1f4 100644
--- a/actionview/test/template/asset_tag_helper_test.rb
+++ b/actionview/test/template/asset_tag_helper_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'active_support/ordered_options'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/ordered_options"
class AssetTagHelperTest < ActionView::TestCase
tests ActionView::Helpers::AssetTagHelper
@@ -13,10 +15,11 @@ class AssetTagHelperTest < ActionView::TestCase
@request = Class.new do
attr_accessor :script_name
- def protocol() 'http://' end
+ def protocol() "http://" end
def ssl?() false end
- def host_with_port() 'localhost' end
- def base_url() 'http://www.example.com' end
+ def host_with_port() "localhost" end
+ def base_url() "http://www.example.com" end
+ def send_early_hints(links) end
end.new
@controller.request = @request
@@ -26,7 +29,13 @@ class AssetTagHelperTest < ActionView::TestCase
"http://www.example.com"
end
+ def content_security_policy_nonce
+ "iyhD0Yc0W+c="
+ end
+
AssetPathToTag = {
+ %(asset_path("")) => %(),
+ %(asset_path(" ")) => %(),
%(asset_path("foo")) => %(/foo),
%(asset_path("style.css")) => %(/style.css),
%(asset_path("xmlhr.js")) => %(/xmlhr.js),
@@ -51,6 +60,7 @@ class AssetTagHelperTest < ActionView::TestCase
%(auto_discovery_link_tag) => %(<link href="http://www.example.com" rel="alternate" title="RSS" type="application/rss+xml" />),
%(auto_discovery_link_tag(:rss)) => %(<link href="http://www.example.com" rel="alternate" title="RSS" type="application/rss+xml" />),
%(auto_discovery_link_tag(:atom)) => %(<link href="http://www.example.com" rel="alternate" title="ATOM" type="application/atom+xml" />),
+ %(auto_discovery_link_tag(:json)) => %(<link href="http://www.example.com" rel="alternate" title="JSON" type="application/json" />),
%(auto_discovery_link_tag(:rss, :action => "feed")) => %(<link href="http://www.example.com" rel="alternate" title="RSS" type="application/rss+xml" />),
%(auto_discovery_link_tag(:rss, "http://localhost/feed")) => %(<link href="http://localhost/feed" rel="alternate" title="RSS" type="application/rss+xml" />),
%(auto_discovery_link_tag(:rss, "//localhost/feed")) => %(<link href="//localhost/feed" rel="alternate" title="RSS" type="application/rss+xml" />),
@@ -97,6 +107,7 @@ class AssetTagHelperTest < ActionView::TestCase
%(javascript_include_tag("bank")) => %(<script src="/javascripts/bank.js" ></script>),
%(javascript_include_tag("bank.js")) => %(<script src="/javascripts/bank.js" ></script>),
%(javascript_include_tag("bank", :lang => "vbscript")) => %(<script lang="vbscript" src="/javascripts/bank.js" ></script>),
+ %(javascript_include_tag("bank", :host => "assets.example.com")) => %(<script src="http://assets.example.com/javascripts/bank.js"></script>),
%(javascript_include_tag("http://example.com/all")) => %(<script src="http://example.com/all"></script>),
%(javascript_include_tag("http://example.com/all.js")) => %(<script src="http://example.com/all.js"></script>),
@@ -141,6 +152,7 @@ class AssetTagHelperTest < ActionView::TestCase
%(stylesheet_link_tag("/elsewhere/file")) => %(<link href="/elsewhere/file.css" media="screen" rel="stylesheet" />),
%(stylesheet_link_tag("subdir/subdir")) => %(<link href="/stylesheets/subdir/subdir.css" media="screen" rel="stylesheet" />),
%(stylesheet_link_tag("bank", :media => "all")) => %(<link href="/stylesheets/bank.css" media="all" rel="stylesheet" />),
+ %(stylesheet_link_tag("bank", :host => "assets.example.com")) => %(<link href="http://assets.example.com/stylesheets/bank.css" media="screen" rel="stylesheet" />),
%(stylesheet_link_tag("http://www.example.com/styles/style")) => %(<link href="http://www.example.com/styles/style" media="screen" rel="stylesheet" />),
%(stylesheet_link_tag("http://www.example.com/styles/style.css")) => %(<link href="http://www.example.com/styles/style.css" media="screen" rel="stylesheet" />),
@@ -176,22 +188,26 @@ class AssetTagHelperTest < ActionView::TestCase
}
ImageLinkToTag = {
- %(image_tag("xml.png")) => %(<img alt="Xml" src="/images/xml.png" />),
+ %(image_tag("xml.png")) => %(<img src="/images/xml.png" />),
%(image_tag("rss.gif", :alt => "rss syndication")) => %(<img alt="rss syndication" src="/images/rss.gif" />),
- %(image_tag("gold.png", :size => "20")) => %(<img alt="Gold" height="20" src="/images/gold.png" width="20" />),
- %(image_tag("gold.png", :size => 20)) => %(<img alt="Gold" height="20" src="/images/gold.png" width="20" />),
- %(image_tag("gold.png", :size => "45x70")) => %(<img alt="Gold" height="70" src="/images/gold.png" width="45" />),
- %(image_tag("gold.png", "size" => "45x70")) => %(<img alt="Gold" height="70" src="/images/gold.png" width="45" />),
- %(image_tag("error.png", "size" => "45 x 70")) => %(<img alt="Error" src="/images/error.png" />),
- %(image_tag("error.png", "size" => "x")) => %(<img alt="Error" src="/images/error.png" />),
- %(image_tag("google.com.png")) => %(<img alt="Google.com" src="/images/google.com.png" />),
- %(image_tag("slash..png")) => %(<img alt="Slash." src="/images/slash..png" />),
- %(image_tag(".pdf.png")) => %(<img alt=".pdf" src="/images/.pdf.png" />),
- %(image_tag("http://www.rubyonrails.com/images/rails.png")) => %(<img alt="Rails" src="http://www.rubyonrails.com/images/rails.png" />),
- %(image_tag("//www.rubyonrails.com/images/rails.png")) => %(<img alt="Rails" src="//www.rubyonrails.com/images/rails.png" />),
+ %(image_tag("gold.png", :size => "20")) => %(<img height="20" src="/images/gold.png" width="20" />),
+ %(image_tag("gold.png", :size => 20)) => %(<img height="20" src="/images/gold.png" width="20" />),
+ %(image_tag("gold.png", :size => "45x70")) => %(<img height="70" src="/images/gold.png" width="45" />),
+ %(image_tag("gold.png", "size" => "45x70")) => %(<img height="70" src="/images/gold.png" width="45" />),
+ %(image_tag("error.png", "size" => "45 x 70")) => %(<img src="/images/error.png" />),
+ %(image_tag("error.png", "size" => "x")) => %(<img src="/images/error.png" />),
+ %(image_tag("google.com.png")) => %(<img src="/images/google.com.png" />),
+ %(image_tag("slash..png")) => %(<img src="/images/slash..png" />),
+ %(image_tag(".pdf.png")) => %(<img src="/images/.pdf.png" />),
+ %(image_tag("http://www.rubyonrails.com/images/rails.png")) => %(<img src="http://www.rubyonrails.com/images/rails.png" />),
+ %(image_tag("//www.rubyonrails.com/images/rails.png")) => %(<img src="//www.rubyonrails.com/images/rails.png" />),
%(image_tag("mouse.png", :alt => nil)) => %(<img src="/images/mouse.png" />),
%(image_tag("data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==", :alt => nil)) => %(<img src="data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" />),
- %(image_tag("")) => %(<img src="" />)
+ %(image_tag("")) => %(<img src="" />),
+ %(image_tag("gold.png", data: { title: 'Rails Application' })) => %(<img data-title="Rails Application" src="/images/gold.png" />),
+ %(image_tag("rss.gif", srcset: "/assets/pic_640.jpg 640w, /assets/pic_1024.jpg 1024w")) => %(<img srcset="/assets/pic_640.jpg 640w, /assets/pic_1024.jpg 1024w" src="/images/rss.gif" />),
+ %(image_tag("rss.gif", srcset: { "pic_640.jpg" => "640w", "pic_1024.jpg" => "1024w" })) => %(<img srcset="/images/pic_640.jpg 640w, /images/pic_1024.jpg 1024w" src="/images/rss.gif" />),
+ %(image_tag("rss.gif", srcset: [["pic_640.jpg", "640w"], ["pic_1024.jpg", "1024w"]])) => %(<img srcset="/images/pic_640.jpg 640w, /images/pic_1024.jpg 1024w" src="/images/rss.gif" />)
}
FaviconLinkToTag = {
@@ -202,6 +218,17 @@ class AssetTagHelperTest < ActionView::TestCase
%(favicon_link_tag 'mb-icon.png', :rel => 'apple-touch-icon', :type => 'image/png') => %(<link href="/images/mb-icon.png" rel="apple-touch-icon" type="image/png" />)
}
+ PreloadLinkToTag = {
+ %(preload_link_tag '/styles/custom_theme.css') => %(<link rel="preload" href="/styles/custom_theme.css" as="style" type="text/css" />),
+ %(preload_link_tag '/videos/video.webm') => %(<link rel="preload" href="/videos/video.webm" as="video" type="video/webm" />),
+ %(preload_link_tag '/posts.json', as: 'fetch') => %(<link rel="preload" href="/posts.json" as="fetch" type="application/json" />),
+ %(preload_link_tag '/users', as: 'fetch', type: 'application/json') => %(<link rel="preload" href="/users" as="fetch" type="application/json" />),
+ %(preload_link_tag '//example.com/map?callback=initMap', as: 'fetch', type: 'application/javascript') => %(<link rel="preload" href="//example.com/map?callback=initMap" as="fetch" type="application/javascript" />),
+ %(preload_link_tag '//example.com/font.woff2') => %(<link rel="preload" href="//example.com/font.woff2" as="font" type="font/woff2" crossorigin="anonymous"/>),
+ %(preload_link_tag '//example.com/font.woff2', crossorigin: 'use-credentials') => %(<link rel="preload" href="//example.com/font.woff2" as="font" type="font/woff2" crossorigin="use-credentials" />),
+ %(preload_link_tag '/media/audio.ogg', nopush: true) => %(<link rel="preload" href="/media/audio.ogg" as="audio" type="audio/ogg" />)
+ }
+
VideoPathToTag = {
%(video_path("xml")) => %(/videos/xml),
%(video_path("xml.ogg")) => %(/videos/xml.ogg),
@@ -233,7 +260,7 @@ class AssetTagHelperTest < ActionView::TestCase
VideoLinkToTag = {
%(video_tag("xml.ogg")) => %(<video src="/videos/xml.ogg"></video>),
%(video_tag("rss.m4v", :autoplay => true, :controls => true)) => %(<video autoplay="autoplay" controls="controls" src="/videos/rss.m4v"></video>),
- %(video_tag("rss.m4v", :autobuffer => true)) => %(<video autobuffer="autobuffer" src="/videos/rss.m4v"></video>),
+ %(video_tag("rss.m4v", :preload => 'none')) => %(<video preload="none" src="/videos/rss.m4v"></video>),
%(video_tag("gold.m4v", :size => "160x120")) => %(<video height="120" src="/videos/gold.m4v" width="160"></video>),
%(video_tag("gold.m4v", "size" => "320x240")) => %(<video height="240" src="/videos/gold.m4v" width="320"></video>),
%(video_tag("trailer.ogg", :poster => "screenshot.png")) => %(<video poster="/images/screenshot.png" src="/videos/trailer.ogg"></video>),
@@ -283,7 +310,7 @@ class AssetTagHelperTest < ActionView::TestCase
%(audio_tag("//media.rubyonrails.org/audio/rails_blog_2.mov")) => %(<audio src="//media.rubyonrails.org/audio/rails_blog_2.mov"></audio>),
%(audio_tag("audio.mp3", "audio.ogg")) => %(<audio><source src="/audios/audio.mp3" /><source src="/audios/audio.ogg" /></audio>),
%(audio_tag(["audio.mp3", "audio.ogg"])) => %(<audio><source src="/audios/audio.mp3" /><source src="/audios/audio.ogg" /></audio>),
- %(audio_tag(["audio.mp3", "audio.ogg"], :autobuffer => true, :controls => true)) => %(<audio autobuffer="autobuffer" controls="controls"><source src="/audios/audio.mp3" /><source src="/audios/audio.ogg" /></audio>)
+ %(audio_tag(["audio.mp3", "audio.ogg"], :preload => 'none', :controls => true)) => %(<audio preload="none" controls="controls"><source src="/audios/audio.mp3" /><source src="/audios/audio.ogg" /></audio>)
}
FontPathToTag = {
@@ -294,6 +321,24 @@ class AssetTagHelperTest < ActionView::TestCase
%(font_path("font.ttf?123")) => %(/fonts/font.ttf?123)
}
+ FontUrlToTag = {
+ %(font_url("font.eot")) => %(http://www.example.com/fonts/font.eot),
+ %(font_url("font.eot#iefix")) => %(http://www.example.com/fonts/font.eot#iefix),
+ %(font_url("font.woff")) => %(http://www.example.com/fonts/font.woff),
+ %(font_url("font.ttf")) => %(http://www.example.com/fonts/font.ttf),
+ %(font_url("font.ttf?123")) => %(http://www.example.com/fonts/font.ttf?123),
+ %(font_url("font.ttf", host: "http://assets.example.com")) => %(http://assets.example.com/fonts/font.ttf)
+ }
+
+ UrlToFontToTag = {
+ %(url_to_font("font.eot")) => %(http://www.example.com/fonts/font.eot),
+ %(url_to_font("font.eot#iefix")) => %(http://www.example.com/fonts/font.eot#iefix),
+ %(url_to_font("font.woff")) => %(http://www.example.com/fonts/font.woff),
+ %(url_to_font("font.ttf")) => %(http://www.example.com/fonts/font.ttf),
+ %(url_to_font("font.ttf?123")) => %(http://www.example.com/fonts/font.ttf?123),
+ %(url_to_font("font.ttf", host: "http://assets.example.com")) => %(http://assets.example.com/fonts/font.ttf)
+ }
+
def test_autodiscovery_link_tag_with_unknown_type_but_not_pass_type_option_key
assert_raise(ArgumentError) do
auto_discovery_link_tag(:xml)
@@ -301,7 +346,7 @@ class AssetTagHelperTest < ActionView::TestCase
end
def test_autodiscovery_link_tag_with_unknown_type
- result = auto_discovery_link_tag(:xml, '/feed.xml', :type => 'application/xml')
+ result = auto_discovery_link_tag(:xml, "/feed.xml", type: "application/xml")
expected = %(<link href="/feed.xml" rel="alternate" title="XML" type="application/xml" />)
assert_dom_equal expected, result
end
@@ -317,18 +362,18 @@ class AssetTagHelperTest < ActionView::TestCase
def test_asset_path_tag_to_not_create_duplicate_slashes
@controller.config.asset_host = "host/"
- assert_dom_equal('http://host/foo', asset_path("foo"))
+ assert_dom_equal("http://host/foo", asset_path("foo"))
- @controller.config.relative_url_root = '/some/root/'
- assert_dom_equal('http://host/some/root/foo', asset_path("foo"))
+ @controller.config.relative_url_root = "/some/root/"
+ assert_dom_equal("http://host/some/root/foo", asset_path("foo"))
end
def test_compute_asset_public_path
assert_equal "/robots.txt", compute_asset_path("robots.txt")
assert_equal "/robots.txt", compute_asset_path("/robots.txt")
- assert_equal "/javascripts/foo.js", compute_asset_path("foo.js", :type => :javascript)
- assert_equal "/javascripts/foo.js", compute_asset_path("/foo.js", :type => :javascript)
- assert_equal "/stylesheets/foo.css", compute_asset_path("foo.css", :type => :stylesheet)
+ assert_equal "/javascripts/foo.js", compute_asset_path("foo.js", type: :javascript)
+ assert_equal "/javascripts/foo.js", compute_asset_path("/foo.js", type: :javascript)
+ assert_equal "/stylesheets/foo.css", compute_asset_path("foo.css", type: :stylesheet)
end
def test_auto_discovery_link_tag
@@ -357,27 +402,31 @@ class AssetTagHelperTest < ActionView::TestCase
def test_javascript_include_tag_with_missing_source
assert_nothing_raised {
- javascript_include_tag('missing_security_guard')
+ javascript_include_tag("missing_security_guard")
}
assert_nothing_raised {
- javascript_include_tag('http://example.com/css/missing_security_guard')
+ javascript_include_tag("http://example.com/css/missing_security_guard")
}
end
def test_javascript_include_tag_is_html_safe
- assert javascript_include_tag("prototype").html_safe?
+ assert_predicate javascript_include_tag("prototype"), :html_safe?
end
def test_javascript_include_tag_relative_protocol
@controller.config.asset_host = "assets.example.com"
- assert_dom_equal %(<script src="//assets.example.com/javascripts/prototype.js"></script>), javascript_include_tag('prototype', protocol: :relative)
+ assert_dom_equal %(<script src="//assets.example.com/javascripts/prototype.js"></script>), javascript_include_tag("prototype", protocol: :relative)
end
def test_javascript_include_tag_default_protocol
@controller.config.asset_host = "assets.example.com"
@controller.config.default_asset_host_protocol = :relative
- assert_dom_equal %(<script src="//assets.example.com/javascripts/prototype.js"></script>), javascript_include_tag('prototype')
+ assert_dom_equal %(<script src="//assets.example.com/javascripts/prototype.js"></script>), javascript_include_tag("prototype")
+ end
+
+ def test_javascript_include_tag_nonce
+ assert_dom_equal %(<script src="/javascripts/bank.js" nonce="iyhD0Yc0W+c="></script>), javascript_include_tag("bank", nonce: true)
end
def test_stylesheet_path
@@ -402,36 +451,49 @@ class AssetTagHelperTest < ActionView::TestCase
def test_stylesheet_link_tag_with_missing_source
assert_nothing_raised {
- stylesheet_link_tag('missing_security_guard')
+ stylesheet_link_tag("missing_security_guard")
}
assert_nothing_raised {
- stylesheet_link_tag('http://example.com/css/missing_security_guard')
+ stylesheet_link_tag("http://example.com/css/missing_security_guard")
}
end
+ def test_stylesheet_link_tag_without_request
+ @request = nil
+ assert_dom_equal(
+ %(<link rel="stylesheet" media="screen" href="/stylesheets/foo.css" />),
+ stylesheet_link_tag("foo.css")
+ )
+ end
+
def test_stylesheet_link_tag_is_html_safe
- assert stylesheet_link_tag('dir/file').html_safe?
- assert stylesheet_link_tag('dir/other/file', 'dir/file2').html_safe?
+ assert_predicate stylesheet_link_tag("dir/file"), :html_safe?
+ assert_predicate stylesheet_link_tag("dir/other/file", "dir/file2"), :html_safe?
end
def test_stylesheet_link_tag_escapes_options
- assert_dom_equal %(<link href="/file.css" media="&lt;script&gt;" rel="stylesheet" />), stylesheet_link_tag('/file', :media => '<script>')
+ assert_dom_equal %(<link href="/file.css" media="&lt;script&gt;" rel="stylesheet" />), stylesheet_link_tag("/file", media: "<script>")
end
def test_stylesheet_link_tag_should_not_output_the_same_asset_twice
- assert_dom_equal %(<link href="/stylesheets/wellington.css" media="screen" rel="stylesheet" />\n<link href="/stylesheets/amsterdam.css" media="screen" rel="stylesheet" />), stylesheet_link_tag('wellington', 'wellington', 'amsterdam')
+ assert_dom_equal %(<link href="/stylesheets/wellington.css" media="screen" rel="stylesheet" />\n<link href="/stylesheets/amsterdam.css" media="screen" rel="stylesheet" />), stylesheet_link_tag("wellington", "wellington", "amsterdam")
end
def test_stylesheet_link_tag_with_relative_protocol
@controller.config.asset_host = "assets.example.com"
- assert_dom_equal %(<link href="//assets.example.com/stylesheets/wellington.css" media="screen" rel="stylesheet" />), stylesheet_link_tag('wellington', protocol: :relative)
+ assert_dom_equal %(<link href="//assets.example.com/stylesheets/wellington.css" media="screen" rel="stylesheet" />), stylesheet_link_tag("wellington", protocol: :relative)
end
def test_stylesheet_link_tag_with_default_protocol
@controller.config.asset_host = "assets.example.com"
@controller.config.default_asset_host_protocol = :relative
- assert_dom_equal %(<link href="//assets.example.com/stylesheets/wellington.css" media="screen" rel="stylesheet" />), stylesheet_link_tag('wellington')
+ assert_dom_equal %(<link href="//assets.example.com/stylesheets/wellington.css" media="screen" rel="stylesheet" />), stylesheet_link_tag("wellington")
+ end
+
+ def test_javascript_include_tag_without_request
+ @request = nil
+ assert_dom_equal %(<script src="/javascripts/foo.js"></script>), javascript_include_tag("foo.js")
end
def test_image_path
@@ -451,11 +513,22 @@ class AssetTagHelperTest < ActionView::TestCase
end
def test_image_alt
- [nil, '/', '/foo/bar/', 'foo/bar/'].each do |prefix|
- assert_equal 'Rails', image_alt("#{prefix}rails.png")
- assert_equal 'Rails', image_alt("#{prefix}rails-9c0a079bdd7701d7e729bd956823d153.png")
- assert_equal 'Long file name with hyphens', image_alt("#{prefix}long-file-name-with-hyphens.png")
- assert_equal 'Long file name with underscores', image_alt("#{prefix}long_file_name_with_underscores.png")
+ [nil, "/", "/foo/bar/", "foo/bar/"].each do |prefix|
+ assert_deprecated do
+ assert_equal "Rails", image_alt("#{prefix}rails.png")
+ end
+ assert_deprecated do
+ assert_equal "Rails", image_alt("#{prefix}rails-9c0a079bdd7701d7e729bd956823d153.png")
+ end
+ assert_deprecated do
+ assert_equal "Rails", image_alt("#{prefix}rails-f56ef62bc41b040664e801a38f068082a75d506d9048307e8096737463503d0b.png")
+ end
+ assert_deprecated do
+ assert_equal "Long file name with hyphens", image_alt("#{prefix}long-file-name-with-hyphens.png")
+ end
+ assert_deprecated do
+ assert_equal "Long file name with underscores", image_alt("#{prefix}long_file_name_with_underscores.png")
+ end
end
end
@@ -464,14 +537,14 @@ class AssetTagHelperTest < ActionView::TestCase
end
def test_image_tag_does_not_modify_options
- options = {:size => '16x10'}
- image_tag('icon', options)
- assert_equal({:size => '16x10'}, options)
+ options = { size: "16x10" }
+ image_tag("icon", options)
+ assert_equal({ size: "16x10" }, options)
end
def test_image_tag_raises_an_error_for_competing_size_arguments
exception = assert_raise(ArgumentError) do
- image_tag("gold.png", :height => "100", :width => "200", :size => "45x70")
+ image_tag("gold.png", height: "100", width: "200", size: "45x70")
end
assert_equal("Cannot pass a :size option with a :height or :width option", exception.message)
@@ -481,6 +554,10 @@ class AssetTagHelperTest < ActionView::TestCase
FaviconLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end
+ def test_preload_link_tag
+ PreloadLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
def test_video_path
VideoPathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end
@@ -525,12 +602,20 @@ class AssetTagHelperTest < ActionView::TestCase
FontPathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end
+ def test_font_url
+ FontUrlToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
+ def test_url_to_font_alias_for_font_url
+ UrlToFontToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
def test_video_audio_tag_does_not_modify_options
- options = {:autoplay => true}
- video_tag('video', options)
- assert_equal({:autoplay => true}, options)
- audio_tag('audio', options)
- assert_equal({:autoplay => true}, options)
+ options = { autoplay: true }
+ video_tag("video", options)
+ assert_equal({ autoplay: true }, options)
+ audio_tag("audio", options)
+ assert_equal({ autoplay: true }, options)
end
def test_image_tag_interpreting_email_cid_correctly
@@ -539,11 +624,11 @@ class AssetTagHelperTest < ActionView::TestCase
end
def test_image_tag_interpreting_email_adding_optional_alt_tag
- assert_equal '<img alt="Image" src="cid:thi%25%25sis@acontentid" />', image_tag("cid:thi%25%25sis@acontentid", :alt => "Image")
+ assert_equal '<img alt="Image" src="cid:thi%25%25sis@acontentid" />', image_tag("cid:thi%25%25sis@acontentid", alt: "Image")
end
def test_should_not_modify_source_string
- source = '/images/rails.png'
+ source = "/images/rails.png"
copy = source.dup
image_tag(source)
assert_equal copy, source
@@ -551,13 +636,10 @@ class AssetTagHelperTest < ActionView::TestCase
class PlaceholderImage
def blank?; true; end
- def to_s; 'no-image-yet.png'; end
- end
- def test_image_tag_with_blank_placeholder
- assert_equal '<img alt="" src="/images/no-image-yet.png" />', image_tag(PlaceholderImage.new, alt: "")
+ def to_s; "no-image-yet.png"; end
end
def test_image_path_with_blank_placeholder
- assert_equal '/images/no-image-yet.png', image_path(PlaceholderImage.new)
+ assert_equal "/images/no-image-yet.png", image_path(PlaceholderImage.new)
end
def test_image_path_with_asset_host_proc_returning_nil
@@ -588,11 +670,13 @@ class AssetTagHelperTest < ActionView::TestCase
end
end
- @controller.request.stubs(:ssl?).returns(false)
- assert_equal "http://assets15.example.com/images/xml.png", image_path("xml.png")
+ @controller.request.stub(:ssl?, false) do
+ assert_equal "http://assets15.example.com/images/xml.png", image_path("xml.png")
+ end
- @controller.request.stubs(:ssl?).returns(true)
- assert_equal "http://localhost/images/xml.png", image_path("xml.png")
+ @controller.request.stub(:ssl?, true) do
+ assert_equal "http://localhost/images/xml.png", image_path("xml.png")
+ end
end
end
@@ -606,7 +690,9 @@ class AssetTagHelperNonVhostTest < ActionView::TestCase
@controller = BasicController.new
@controller.config.relative_url_root = "/collaboration/hieraki"
- @request = Struct.new(:protocol, :base_url).new("gopher://", "gopher://www.example.com")
+ @request = Struct.new(:protocol, :base_url) do
+ def send_early_hints(links); end
+ end.new("gopher://", "gopher://www.example.com")
@controller.request = @request
end
@@ -622,15 +708,15 @@ 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
- assert_equal "gopher://www.example.com", compute_asset_host("foo", :protocol => :request)
+ assert_equal "gopher://www.example.com", compute_asset_host("foo", protocol: :request)
end
def test_should_return_custom_host_if_passed_in_options
- assert_equal "http://custom.example.com", compute_asset_host("foo", :host => "http://custom.example.com")
+ assert_equal "http://custom.example.com", compute_asset_host("foo", host: "http://custom.example.com")
end
def test_should_ignore_relative_root_path_on_complete_url
@@ -644,12 +730,12 @@ class AssetTagHelperNonVhostTest < ActionView::TestCase
def test_should_return_relative_asset_host
@controller.config.asset_host = "assets.example.com"
- assert_equal "//assets.example.com", compute_asset_host("foo", :protocol => :relative)
+ assert_equal "//assets.example.com", compute_asset_host("foo", protocol: :relative)
end
def test_should_return_custom_protocol_asset_host
@controller.config.asset_host = "assets.example.com"
- assert_equal "ftp://assets.example.com", compute_asset_host("foo", :protocol => "ftp")
+ assert_equal "ftp://assets.example.com", compute_asset_host("foo", protocol: "ftp")
end
def test_should_compute_proper_path_with_asset_host
@@ -700,26 +786,26 @@ class AssetTagHelperNonVhostTest < ActionView::TestCase
end
def test_should_wildcard_asset_host
- @controller.config.asset_host = 'http://a%d.example.com'
- assert_match(%r(http://a[0123].example.com), compute_asset_host("foo"))
+ @controller.config.asset_host = "http://a%d.example.com"
+ assert_match(%r(http://a[0123]\.example\.com), compute_asset_host("foo"))
end
def test_should_wildcard_asset_host_between_zero_and_four
- @controller.config.asset_host = 'http://a%d.example.com'
- assert_match(%r(http://a[0123].example.com/collaboration/hieraki/images/xml.png), image_path('xml.png'))
- assert_match(%r(http://a[0123].example.com/collaboration/hieraki/images/xml.png), image_url('xml.png'))
+ @controller.config.asset_host = "http://a%d.example.com"
+ assert_match(%r(http://a[0123]\.example\.com/collaboration/hieraki/images/xml\.png), image_path("xml.png"))
+ assert_match(%r(http://a[0123]\.example\.com/collaboration/hieraki/images/xml\.png), image_url("xml.png"))
end
def test_asset_host_without_protocol_should_be_protocol_relative
- @controller.config.asset_host = 'a.example.com'
- assert_equal 'gopher://a.example.com/collaboration/hieraki/images/xml.png', image_path('xml.png')
- assert_equal 'gopher://a.example.com/collaboration/hieraki/images/xml.png', image_url('xml.png')
+ @controller.config.asset_host = "a.example.com"
+ assert_equal "gopher://a.example.com/collaboration/hieraki/images/xml.png", image_path("xml.png")
+ assert_equal "gopher://a.example.com/collaboration/hieraki/images/xml.png", image_url("xml.png")
end
def test_asset_host_without_protocol_should_be_protocol_relative_even_if_path_present
- @controller.config.asset_host = 'a.example.com/files/go/here'
- assert_equal 'gopher://a.example.com/files/go/here/collaboration/hieraki/images/xml.png', image_path('xml.png')
- assert_equal 'gopher://a.example.com/files/go/here/collaboration/hieraki/images/xml.png', image_url('xml.png')
+ @controller.config.asset_host = "a.example.com/files/go/here"
+ assert_equal "gopher://a.example.com/files/go/here/collaboration/hieraki/images/xml.png", image_path("xml.png")
+ assert_equal "gopher://a.example.com/files/go/here/collaboration/hieraki/images/xml.png", image_url("xml.png")
end
def test_assert_css_and_js_of_the_same_name_return_correct_extension
@@ -728,6 +814,23 @@ class AssetTagHelperNonVhostTest < ActionView::TestCase
end
end
+class AssetTagHelperWithoutRequestTest < ActionView::TestCase
+ tests ActionView::Helpers::AssetTagHelper
+
+ undef :request
+
+ def test_stylesheet_link_tag_without_request
+ assert_dom_equal(
+ %(<link rel="stylesheet" media="screen" href="/stylesheets/foo.css" />),
+ stylesheet_link_tag("foo.css")
+ )
+ end
+
+ def test_javascript_include_tag_without_request
+ assert_dom_equal %(<script src="/javascripts/foo.js"></script>), javascript_include_tag("foo.js")
+ end
+end
+
class AssetUrlHelperControllerTest < ActionView::TestCase
tests ActionView::Helpers::AssetUrlHelper
@@ -739,10 +842,10 @@ class AssetUrlHelperControllerTest < ActionView::TestCase
@request = Class.new do
attr_accessor :script_name
- def protocol() 'http://' end
+ def protocol() "http://" end
def ssl?() false end
- def host_with_port() 'www.example.com' end
- def base_url() 'http://www.example.com' end
+ def host_with_port() "www.example.com" end
+ def base_url() "http://www.example.com" end
end.new
@controller.request = @request
@@ -805,6 +908,6 @@ class AssetUrlHelperEmptyModuleTest < ActionView::TestCase
end
assert @module.config.asset_host
- assert_equal "http://custom.example.com/foo", @module.asset_url("foo", :host => "http://custom.example.com")
+ assert_equal "http://custom.example.com/foo", @module.asset_url("foo", host: "http://custom.example.com")
end
end
diff --git a/actionview/test/template/atom_feed_helper_test.rb b/actionview/test/template/atom_feed_helper_test.rb
index 591cd71404..8e683cb48a 100644
--- a/actionview/test/template/atom_feed_helper_test.rb
+++ b/actionview/test/template/atom_feed_helper_test.rb
@@ -1,6 +1,8 @@
-require 'abstract_unit'
+# frozen_string_literal: true
-class Scroll < Struct.new(:id, :to_param, :title, :body, :updated_at, :created_at)
+require "abstract_unit"
+
+Scroll = Struct.new(:id, :to_param, :title, :body, :updated_at, :created_at) do
extend ActiveModel::Naming
include ActiveModel::Conversion
@@ -28,7 +30,7 @@ class ScrollsController < ActionController::Base
end
end
EOT
- FEEDS["entry_options"] = <<-EOT
+ FEEDS["entry_options"] = <<-EOT
atom_feed do |feed|
feed.title("My great blog!")
feed.updated(@scrolls.first.created_at)
@@ -45,7 +47,7 @@ class ScrollsController < ActionController::Base
end
end
EOT
- FEEDS["entry_type_options"] = <<-EOT
+ FEEDS["entry_type_options"] = <<-EOT
atom_feed(:schema_date => '2008') do |feed|
feed.title("My great blog!")
feed.updated(@scrolls.first.created_at)
@@ -62,7 +64,7 @@ class ScrollsController < ActionController::Base
end
end
EOT
- FEEDS["entry_url_false_option"] = <<-EOT
+ FEEDS["entry_url_false_option"] = <<-EOT
atom_feed do |feed|
feed.title("My great blog!")
feed.updated(@scrolls.first.created_at)
@@ -79,7 +81,7 @@ class ScrollsController < ActionController::Base
end
end
EOT
- FEEDS["xml_block"] = <<-EOT
+ FEEDS["xml_block"] = <<-EOT
atom_feed do |feed|
feed.title("My great blog!")
feed.updated(@scrolls.first.created_at)
@@ -96,7 +98,7 @@ class ScrollsController < ActionController::Base
end
end
EOT
- FEEDS["feed_with_atomPub_namespace"] = <<-EOT
+ FEEDS["feed_with_atomPub_namespace"] = <<-EOT
atom_feed({'xmlns:app' => 'http://www.w3.org/2007/app',
'xmlns:openSearch' => 'http://a9.com/-/spec/opensearch/1.1/'}) do |feed|
feed.title("My great blog!")
@@ -115,7 +117,7 @@ class ScrollsController < ActionController::Base
end
end
EOT
- FEEDS["feed_with_overridden_ids"] = <<-EOT
+ FEEDS["feed_with_overridden_ids"] = <<-EOT
atom_feed({:id => 'tag:test.rubyonrails.org,2008:test/'}) do |feed|
feed.title("My great blog!")
feed.updated(@scrolls.first.created_at)
@@ -169,7 +171,7 @@ class ScrollsController < ActionController::Base
end
end
EOT
- FEEDS["feed_with_xhtml_content"] = <<-'EOT'
+ FEEDS["feed_with_xhtml_content"] = <<-'EOT'
atom_feed do |feed|
feed.title("My great blog!")
feed.updated(@scrolls.first.created_at)
@@ -191,10 +193,10 @@ class ScrollsController < ActionController::Base
end
end
EOT
- FEEDS["provide_builder"] = <<-'EOT'
+ FEEDS["provide_builder"] = <<-'EOT'
# we pass in the new_xml to the helper so it doesn't
# call anything on the original builder
- new_xml = Builder::XmlMarkup.new(:target=>'')
+ new_xml = Builder::XmlMarkup.new(:target=>''.dup)
atom_feed(:xml => new_xml) do |feed|
feed.title("My great blog!")
feed.updated(@scrolls.first.created_at)
@@ -217,7 +219,7 @@ class ScrollsController < ActionController::Base
Scroll.new(2, "2", "Hello Two", "Something Boring", Time.utc(2007, 12, 12, 15)),
]
- render :inline => FEEDS[params[:id]], :type => :builder
+ render inline: FEEDS[params[:id]], type: :builder
end
end
@@ -255,7 +257,7 @@ class AtomFeedTest < ActionController::TestCase
get :index, params: { id: "provide_builder" }
# because we pass in the non-default builder, the content generated by the
# helper should go 'nowhere'. Leaving the response body blank.
- assert @response.body.blank?
+ assert_predicate @response.body, :blank?
end
end
@@ -278,22 +280,22 @@ class AtomFeedTest < ActionController::TestCase
def test_feed_id_should_be_a_valid_tag
with_restful_routing(:scrolls) do
get :index, params: { id: "defaults" }
- assert_select "id", :text => "tag:www.nextangle.com,2008:/scrolls?id=defaults"
+ assert_select "id", text: "tag:www.nextangle.com,2008:/scrolls?id=defaults"
end
end
def test_entry_id_should_be_a_valid_tag
with_restful_routing(:scrolls) do
get :index, params: { id: "defaults" }
- assert_select "entry id", :text => "tag:www.nextangle.com,2008:Scroll/1"
- assert_select "entry id", :text => "tag:www.nextangle.com,2008:Scroll/2"
+ assert_select "entry id", text: "tag:www.nextangle.com,2008:Scroll/1"
+ assert_select "entry id", text: "tag:www.nextangle.com,2008:Scroll/2"
end
end
def test_feed_should_allow_nested_xml_blocks
with_restful_routing(:scrolls) do
get :index, params: { id: "xml_block" }
- assert_select "author name", :text => "DHH"
+ assert_select "author name", text: "DHH"
end
end
@@ -301,31 +303,31 @@ class AtomFeedTest < ActionController::TestCase
with_restful_routing(:scrolls) do
get :index, params: { id: "feed_with_atomPub_namespace" }
assert_match %r{xml:lang="en-US"}, @response.body
- assert_match %r{xmlns="http://www.w3.org/2005/Atom"}, @response.body
- assert_match %r{xmlns:app="http://www.w3.org/2007/app"}, @response.body
+ assert_match %r{xmlns="http://www\.w3\.org/2005/Atom"}, @response.body
+ assert_match %r{xmlns:app="http://www\.w3\.org/2007/app"}, @response.body
end
end
def test_feed_should_allow_overriding_ids
with_restful_routing(:scrolls) do
get :index, params: { id: "feed_with_overridden_ids" }
- assert_select "id", :text => "tag:test.rubyonrails.org,2008:test/"
- assert_select "entry id", :text => "tag:test.rubyonrails.org,2008:1"
- assert_select "entry id", :text => "tag:test.rubyonrails.org,2008:2"
+ assert_select "id", text: "tag:test.rubyonrails.org,2008:test/"
+ assert_select "entry id", text: "tag:test.rubyonrails.org,2008:1"
+ assert_select "entry id", text: "tag:test.rubyonrails.org,2008:2"
end
end
def test_feed_xml_processing_instructions
with_restful_routing(:scrolls) do
- get :index, params: { id: 'feed_with_xml_processing_instructions' }
+ get :index, params: { id: "feed_with_xml_processing_instructions" }
assert_match %r{<\?xml-stylesheet [^\?]*type="text/css"}, @response.body
- assert_match %r{<\?xml-stylesheet [^\?]*href="t.css"}, @response.body
+ assert_match %r{<\?xml-stylesheet [^\?]*href="t\.css"}, @response.body
end
end
def test_feed_xml_processing_instructions_duplicate_targets
with_restful_routing(:scrolls) do
- get :index, params: { id: 'feed_with_xml_processing_instructions_duplicate_targets' }
+ get :index, params: { id: "feed_with_xml_processing_instructions_duplicate_targets" }
assert_match %r{<\?target1 (a="1" b="2"|b="2" a="1")\?>}, @response.body
assert_match %r{<\?target1 (c="3" d="4"|d="4" c="3")\?>}, @response.body
end
@@ -334,29 +336,29 @@ class AtomFeedTest < ActionController::TestCase
def test_feed_xhtml
with_restful_routing(:scrolls) do
get :index, params: { id: "feed_with_xhtml_content" }
- assert_match %r{xmlns="http://www.w3.org/1999/xhtml"}, @response.body
- assert_select "summary", :text => /Something Boring/
- assert_select "summary", :text => /after 2/
+ assert_match %r{xmlns="http://www\.w3\.org/1999/xhtml"}, @response.body
+ assert_select "summary", text: /Something Boring/
+ assert_select "summary", text: /after 2/
end
end
def test_feed_entry_type_option_default_to_text_html
with_restful_routing(:scrolls) do
- get :index, params: { id: 'defaults' }
+ get :index, params: { id: "defaults" }
assert_select "entry link[rel=alternate][type=\"text/html\"]"
end
end
def test_feed_entry_type_option_specified
with_restful_routing(:scrolls) do
- get :index, params: { id: 'entry_type_options' }
+ get :index, params: { id: "entry_type_options" }
assert_select "entry link[rel=alternate][type=\"text/xml\"]"
end
end
def test_feed_entry_url_false_option_adds_no_link
with_restful_routing(:scrolls) do
- get :index, params: { id: 'entry_url_false_option' }
+ get :index, params: { id: "entry_url_false_option" }
assert_select "entry link", false
end
end
diff --git a/actionview/test/template/capture_helper_test.rb b/actionview/test/template/capture_helper_test.rb
index 1e099d482c..e172497c88 100644
--- a/actionview/test/template/capture_helper_test.rb
+++ b/actionview/test/template/capture_helper_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class CaptureHelperTest < ActionView::TestCase
def setup
@@ -10,18 +12,18 @@ class CaptureHelperTest < ActionView::TestCase
def test_capture_captures_the_temporary_output_buffer_in_its_block
assert_nil @av.output_buffer
string = @av.capture do
- @av.output_buffer << 'foo'
- @av.output_buffer << 'bar'
+ @av.output_buffer << "foo"
+ @av.output_buffer << "bar"
end
assert_nil @av.output_buffer
- assert_equal 'foobar', string
+ assert_equal "foobar", string
end
def test_capture_captures_the_value_returned_by_the_block_if_the_temporary_buffer_is_blank
- string = @av.capture('foo', 'bar') do |a, b|
+ string = @av.capture("foo", "bar") do |a, b|
a + b
end
- assert_equal 'foobar', string
+ assert_equal "foobar", string
end
def test_capture_returns_nil_if_the_returned_value_is_not_a_string
@@ -29,127 +31,140 @@ class CaptureHelperTest < ActionView::TestCase
end
def test_capture_escapes_html
- string = @av.capture { '<em>bar</em>' }
- assert_equal '&lt;em&gt;bar&lt;/em&gt;', string
+ string = @av.capture { "<em>bar</em>" }
+ assert_equal "&lt;em&gt;bar&lt;/em&gt;", string
end
def test_capture_doesnt_escape_twice
- string = @av.capture { '&lt;em&gt;bar&lt;/em&gt;'.html_safe }
- assert_equal '&lt;em&gt;bar&lt;/em&gt;', string
+ string = @av.capture { raw("&lt;em&gt;bar&lt;/em&gt;") }
+ assert_equal "&lt;em&gt;bar&lt;/em&gt;", string
end
- def test_capture_used_for_read
+ def test_content_for_used_for_read
content_for :foo, "foo"
assert_equal "foo", content_for(:foo)
- content_for(:bar){ "bar" }
+ content_for(:bar) { "bar" }
assert_equal "bar", content_for(:bar)
end
def test_content_for_with_multiple_calls
- assert ! content_for?(:title)
- content_for :title, 'foo'
- content_for :title, 'bar'
- assert_equal 'foobar', content_for(:title)
+ assert_not content_for?(:title)
+ content_for :title, "foo"
+ content_for :title, "bar"
+ assert_equal "foobar", content_for(:title)
end
def test_content_for_with_multiple_calls_and_flush
- assert ! content_for?(:title)
- content_for :title, 'foo'
- content_for :title, 'bar', flush: true
- assert_equal 'bar', content_for(:title)
+ assert_not content_for?(:title)
+ content_for :title, "foo"
+ content_for :title, "bar", flush: true
+ assert_equal "bar", content_for(:title)
end
def test_content_for_with_block
- assert ! content_for?(:title)
+ assert_not content_for?(:title)
content_for :title do
- output_buffer << 'foo'
- output_buffer << 'bar'
+ output_buffer << "foo"
+ output_buffer << "bar"
nil
end
- assert_equal 'foobar', content_for(:title)
+ assert_equal "foobar", content_for(:title)
end
def test_content_for_with_block_and_multiple_calls_with_flush
- assert ! content_for?(:title)
+ assert_not content_for?(:title)
content_for :title do
- 'foo'
+ "foo"
end
content_for :title, flush: true do
- 'bar'
+ "bar"
end
- assert_equal 'bar', content_for(:title)
+ assert_equal "bar", content_for(:title)
end
def test_content_for_with_block_and_multiple_calls_with_flush_nil_content
- assert ! content_for?(:title)
+ assert_not content_for?(:title)
content_for :title do
- 'foo'
+ "foo"
end
content_for :title, nil, flush: true do
- 'bar'
+ "bar"
end
- assert_equal 'bar', content_for(:title)
+ assert_equal "bar", content_for(:title)
end
def test_content_for_with_block_and_multiple_calls_without_flush
- assert ! content_for?(:title)
+ assert_not content_for?(:title)
content_for :title do
- 'foo'
+ "foo"
end
content_for :title, flush: false do
- 'bar'
+ "bar"
end
- assert_equal 'foobar', content_for(:title)
+ assert_equal "foobar", content_for(:title)
end
def test_content_for_with_whitespace_block
- assert ! content_for?(:title)
- content_for :title, 'foo'
+ assert_not content_for?(:title)
+ content_for :title, "foo"
content_for :title do
output_buffer << " \n "
nil
end
- content_for :title, 'bar'
- assert_equal 'foobar', content_for(:title)
+ content_for :title, "bar"
+ assert_equal "foobar", content_for(:title)
end
def test_content_for_with_whitespace_block_and_flush
- assert ! content_for?(:title)
- content_for :title, 'foo'
+ assert_not content_for?(:title)
+ content_for :title, "foo"
content_for :title, flush: true do
output_buffer << " \n "
nil
end
- content_for :title, 'bar', flush: true
- assert_equal 'bar', content_for(:title)
+ content_for :title, "bar", flush: true
+ assert_equal "bar", content_for(:title)
end
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_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_equal 'bar', content_for(:title)
+ assert_not content_for?(:title)
+ assert_nil content_for(:title, "foo")
+ assert_nil content_for(:title) { output_buffer << "bar"; nil }
+ assert_nil content_for(:title) { output_buffer << " \n "; nil }
+ assert_equal "foobar", content_for(:title)
+ 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
- assert ! content_for?(:title)
- content_for :title, 'title'
+ assert_not content_for?(:title)
+ content_for :title, "title"
assert content_for?(:title)
- assert ! content_for?(:something_else)
+ assert_not content_for?(:something_else)
+ end
+
+ def test_content_for_should_be_html_safe_after_flush_empty
+ assert_not content_for?(:title)
+ content_for :title do
+ content_tag(:p, "title")
+ end
+ assert_predicate content_for(:title), :html_safe?
+ content_for :title, "", flush: true
+ content_for(:title) do
+ content_tag(:p, "title")
+ end
+ assert_predicate content_for(:title), :html_safe?
end
def test_provide
- assert !content_for?(:title)
+ assert_not content_for?(:title)
provide :title, "hi"
assert content_for?(:title)
assert_equal "hi", content_for(:title)
@@ -158,26 +173,26 @@ class CaptureHelperTest < ActionView::TestCase
@view_flow = ActionView::OutputFlow.new
provide :title, "hi"
- provide :title, "<p>title</p>".html_safe
+ provide :title, raw("<p>title</p>")
assert_equal "hi<p>title</p>", content_for(:title)
end
def test_with_output_buffer_swaps_the_output_buffer_given_no_argument
assert_nil @av.output_buffer
buffer = @av.with_output_buffer do
- @av.output_buffer << '.'
+ @av.output_buffer << "."
end
- assert_equal '.', buffer
+ assert_equal ".", buffer
assert_nil @av.output_buffer
end
def test_with_output_buffer_swaps_the_output_buffer_with_an_argument
assert_nil @av.output_buffer
- buffer = ActionView::OutputBuffer.new('.')
+ buffer = ActionView::OutputBuffer.new(".")
@av.with_output_buffer(buffer) do
- @av.output_buffer << '.'
+ @av.output_buffer << "."
end
- assert_equal '..', buffer
+ assert_equal "..", buffer
assert_nil @av.output_buffer
end
@@ -185,7 +200,7 @@ class CaptureHelperTest < ActionView::TestCase
buffer = ActionView::OutputBuffer.new
@av.output_buffer = buffer
@av.with_output_buffer do
- @av.output_buffer << '.'
+ @av.output_buffer << "."
end
assert buffer.equal?(@av.output_buffer)
end
@@ -204,7 +219,7 @@ class CaptureHelperTest < ActionView::TestCase
def test_with_output_buffer_does_not_assume_there_is_an_output_buffer
assert_nil @av.output_buffer
- assert_equal "", @av.with_output_buffer {}
+ assert_equal "", @av.with_output_buffer { }
end
def alt_encoding(output_buffer)
diff --git a/actionview/test/template/compiled_templates_test.rb b/actionview/test/template/compiled_templates_test.rb
index f6c1283b92..3cd6448e38 100644
--- a/actionview/test/template/compiled_templates_test.rb
+++ b/actionview/test/template/compiled_templates_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class CompiledTemplatesTest < ActiveSupport::TestCase
teardown do
@@ -6,28 +8,61 @@ class CompiledTemplatesTest < ActiveSupport::TestCase
end
def test_template_with_nil_erb_return
- assert_equal "This is nil: \n", render(:template => "test/nil_return")
+ assert_equal "This is nil: \n", render(template: "test/nil_return")
+ end
+
+ def test_template_with_ruby_keyword_locals
+ assert_equal "The class is foo",
+ render(file: "test/render_file_with_ruby_keyword_locals", locals: { class: "foo" })
+ end
+
+ def test_template_with_invalid_identifier_locals
+ locals = {
+ foo: "bar",
+ Foo: "bar",
+ "d-a-s-h-e-s": "",
+ "white space": "",
+ }
+ 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" })
+ 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" })
end
def test_template_changes_are_not_reflected_with_cached_templates
- assert_equal "Hello world!", render(:file => "test/hello_world")
+ assert_equal "Hello world!", render(file: "test/hello_world")
modify_template "test/hello_world.erb", "Goodbye world!" do
- assert_equal "Hello world!", render(:file => "test/hello_world")
+ assert_equal "Hello world!", render(file: "test/hello_world")
end
- assert_equal "Hello world!", render(:file => "test/hello_world")
+ assert_equal "Hello world!", render(file: "test/hello_world")
end
def test_template_changes_are_reflected_with_uncached_templates
- assert_equal "Hello world!", render_without_cache(:file => "test/hello_world")
+ assert_equal "Hello world!", render_without_cache(file: "test/hello_world")
modify_template "test/hello_world.erb", "Goodbye world!" do
- assert_equal "Goodbye world!", render_without_cache(:file => "test/hello_world")
+ assert_equal "Goodbye world!", render_without_cache(file: "test/hello_world")
end
- assert_equal "Hello world!", render_without_cache(:file => "test/hello_world")
+ assert_equal "Hello world!", render_without_cache(file: "test/hello_world")
end
private
diff --git a/actionview/test/template/controller_helper_test.rb b/actionview/test/template/controller_helper_test.rb
index b5e94ea4f1..46d20c188c 100644
--- a/actionview/test/template/controller_helper_test.rb
+++ b/actionview/test/template/controller_helper_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class ControllerHelperTest < ActionView::TestCase
tests ActionView::Helpers::ControllerHelper
@@ -9,13 +11,24 @@ class ControllerHelperTest < ActionView::TestCase
@controller = OpenStruct.new(default_form_builder: SpecializedFormBuilder)
assign_controller(@controller)
- assert_equal SpecializedFormBuilder, self.default_form_builder
+ assert_equal SpecializedFormBuilder, default_form_builder
end
def test_assign_controller_skips_default_form_builder
@controller = OpenStruct.new
assign_controller(@controller)
- assert_nil self.default_form_builder
+ assert_nil default_form_builder
+ end
+
+ def test_respond_to
+ @controller = OpenStruct.new
+ assign_controller(@controller)
+ assert_not respond_to?(:params)
+ assert respond_to?(:assign_controller)
+
+ @controller.params = {}
+ assert respond_to?(:params)
+ assert respond_to?(:assign_controller)
end
end
diff --git a/actionview/test/template/date_helper_i18n_test.rb b/actionview/test/template/date_helper_i18n_test.rb
index 21fca35185..60303b4c91 100644
--- a/actionview/test/template/date_helper_i18n_test.rb
+++ b/actionview/test/template/date_helper_i18n_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class DateHelperDistanceOfTimeInWordsI18nTests < ActiveSupport::TestCase
include ActionView::Helpers::DateHelper
@@ -12,24 +14,24 @@ class DateHelperDistanceOfTimeInWordsI18nTests < ActiveSupport::TestCase
def test_distance_of_time_in_words_calls_i18n
{ # with include_seconds
- [2.seconds, { :include_seconds => true }] => [:'less_than_x_seconds', 5],
- [9.seconds, { :include_seconds => true }] => [:'less_than_x_seconds', 10],
- [19.seconds, { :include_seconds => true }] => [:'less_than_x_seconds', 20],
- [30.seconds, { :include_seconds => true }] => [:'half_a_minute', nil],
- [59.seconds, { :include_seconds => true }] => [:'less_than_x_minutes', 1],
- [60.seconds, { :include_seconds => true }] => [:'x_minutes', 1],
+ [2.seconds, { include_seconds: true }] => [:'less_than_x_seconds', 5],
+ [9.seconds, { include_seconds: true }] => [:'less_than_x_seconds', 10],
+ [19.seconds, { include_seconds: true }] => [:'less_than_x_seconds', 20],
+ [30.seconds, { include_seconds: true }] => [:'half_a_minute', nil],
+ [59.seconds, { include_seconds: true }] => [:'less_than_x_minutes', 1],
+ [60.seconds, { include_seconds: true }] => [:'x_minutes', 1],
# without include_seconds
- [29.seconds, { :include_seconds => false }] => [:'less_than_x_minutes', 1],
- [60.seconds, { :include_seconds => false }] => [:'x_minutes', 1],
- [44.minutes, { :include_seconds => false }] => [:'x_minutes', 44],
- [61.minutes, { :include_seconds => false }] => [:'about_x_hours', 1],
- [24.hours, { :include_seconds => false }] => [:'x_days', 1],
- [30.days, { :include_seconds => false }] => [:'about_x_months', 1],
- [60.days, { :include_seconds => false }] => [:'x_months', 2],
- [1.year, { :include_seconds => false }] => [:'about_x_years', 1],
- [3.years + 6.months, { :include_seconds => false }] => [:'over_x_years', 3],
- [3.years + 10.months, { :include_seconds => false }] => [:'almost_x_years', 4]
+ [29.seconds, { include_seconds: false }] => [:'less_than_x_minutes', 1],
+ [60.seconds, { include_seconds: false }] => [:'x_minutes', 1],
+ [44.minutes, { include_seconds: false }] => [:'x_minutes', 44],
+ [61.minutes, { include_seconds: false }] => [:'about_x_hours', 1],
+ [24.hours, { include_seconds: false }] => [:'x_days', 1],
+ [30.days, { include_seconds: false }] => [:'about_x_months', 1],
+ [60.days, { include_seconds: false }] => [:'x_months', 2],
+ [1.year, { include_seconds: false }] => [:'about_x_years', 1],
+ [3.years + 6.months, { include_seconds: false }] => [:'over_x_years', 3],
+ [3.years + 10.months, { include_seconds: false }] => [:'almost_x_years', 4]
}.each do |passed, expected|
assert_distance_of_time_in_words_translates_key passed, expected
@@ -46,29 +48,30 @@ class DateHelperDistanceOfTimeInWordsI18nTests < ActiveSupport::TestCase
end
def test_time_ago_in_words_passes_locale
- I18n.expects(:t).with(:less_than_x_minutes, :scope => :'datetime.distance_in_words', :count => 1, :locale => 'ru')
- time_ago_in_words(15.seconds.ago, :locale => 'ru')
+ assert_called_with(I18n, :t, [:less_than_x_minutes, scope: :'datetime.distance_in_words', count: 1, locale: "ru"]) do
+ time_ago_in_words(15.seconds.ago, locale: "ru")
+ end
end
def test_distance_of_time_pluralizations
- { [:'less_than_x_seconds', 1] => 'less than 1 second',
- [:'less_than_x_seconds', 2] => 'less than 2 seconds',
- [:'less_than_x_minutes', 1] => 'less than a minute',
- [:'less_than_x_minutes', 2] => 'less than 2 minutes',
- [:'x_minutes', 1] => '1 minute',
- [:'x_minutes', 2] => '2 minutes',
- [:'about_x_hours', 1] => 'about 1 hour',
- [:'about_x_hours', 2] => 'about 2 hours',
- [:'x_days', 1] => '1 day',
- [:'x_days', 2] => '2 days',
- [:'about_x_years', 1] => 'about 1 year',
- [:'about_x_years', 2] => 'about 2 years',
- [:'over_x_years', 1] => 'over 1 year',
- [:'over_x_years', 2] => 'over 2 years'
+ { [:'less_than_x_seconds', 1] => "less than 1 second",
+ [:'less_than_x_seconds', 2] => "less than 2 seconds",
+ [:'less_than_x_minutes', 1] => "less than a minute",
+ [:'less_than_x_minutes', 2] => "less than 2 minutes",
+ [:'x_minutes', 1] => "1 minute",
+ [:'x_minutes', 2] => "2 minutes",
+ [:'about_x_hours', 1] => "about 1 hour",
+ [:'about_x_hours', 2] => "about 2 hours",
+ [:'x_days', 1] => "1 day",
+ [:'x_days', 2] => "2 days",
+ [:'about_x_years', 1] => "about 1 year",
+ [:'about_x_years', 2] => "about 2 years",
+ [:'over_x_years', 1] => "over 1 year",
+ [:'over_x_years', 2] => "over 2 years"
}.each do |args, expected|
key, count = *args
- assert_equal expected, I18n.t(key, :count => count, :scope => 'datetime.distance_in_words')
+ assert_equal expected, I18n.t(key, count: count, scope: "datetime.distance_in_words")
end
end
@@ -77,11 +80,12 @@ class DateHelperDistanceOfTimeInWordsI18nTests < ActiveSupport::TestCase
key, count = *expected
to = @from + diff
- options = { locale: 'en', scope: :'datetime.distance_in_words' }.merge!(expected_options)
+ options = { locale: "en", scope: :'datetime.distance_in_words' }.merge!(expected_options)
options[:count] = count if count
- I18n.expects(:t).with(key, options)
- distance_of_time_in_words(@from, to, passed_options.merge(locale: 'en'))
+ assert_called_with(I18n, :t, [key, options]) do
+ distance_of_time_in_words(@from, to, passed_options.merge(locale: "en"))
+ end
end
end
@@ -89,60 +93,74 @@ class DateHelperSelectTagsI18nTests < ActiveSupport::TestCase
include ActionView::Helpers::DateHelper
attr_reader :request
- def setup
- @prompt_defaults = {:year => 'Year', :month => 'Month', :day => 'Day', :hour => 'Hour', :minute => 'Minute', :second => 'Seconds'}
-
- I18n.stubs(:translate).with(:'date.month_names', :locale => 'en').returns Date::MONTHNAMES
- end
-
# select_month
def test_select_month_given_use_month_names_option_does_not_translate_monthnames
- I18n.expects(:translate).never
- select_month(8, :locale => 'en', :use_month_names => Date::MONTHNAMES)
+ assert_not_called(I18n, :translate) do
+ select_month(8, locale: "en", use_month_names: Date::MONTHNAMES)
+ end
end
def test_select_month_translates_monthnames
- I18n.expects(:translate).with(:'date.month_names', :locale => 'en').returns Date::MONTHNAMES
- select_month(8, :locale => 'en')
+ assert_called_with(I18n, :translate, [:'date.month_names', locale: "en"], returns: Date::MONTHNAMES) do
+ select_month(8, locale: "en")
+ end
end
def test_select_month_given_use_short_month_option_translates_abbr_monthnames
- I18n.expects(:translate).with(:'date.abbr_month_names', :locale => 'en').returns Date::ABBR_MONTHNAMES
- select_month(8, :locale => 'en', :use_short_month => true)
+ assert_called_with(I18n, :translate, [:'date.abbr_month_names', locale: "en"], returns: Date::ABBR_MONTHNAMES) do
+ select_month(8, locale: "en", use_short_month: true)
+ end
end
def test_date_or_time_select_translates_prompts
- @prompt_defaults.each do |key, prompt|
- I18n.expects(:translate).with(('datetime.prompts.' + key.to_s).to_sym, :locale => 'en').returns prompt
+ prompt_defaults = { year: "Year", month: "Month", day: "Day", hour: "Hour", minute: "Minute", second: "Seconds" }
+ defaults = { [:'date.order', locale: "en", default: []] => %w(year month day) }
+
+ prompt_defaults.each do |key, prompt|
+ defaults[[("datetime.prompts." + key.to_s).to_sym, locale: "en"]] = prompt
+ end
+
+ prompts_check = -> (prompt, x) do
+ @prompt_called ||= 0
+
+ return_value = defaults[[prompt, x]]
+ @prompt_called += 1 if return_value.present?
+
+ return_value
end
- I18n.expects(:translate).with(:'date.order', :locale => 'en', :default => []).returns %w(year month day)
- datetime_select('post', 'updated_at', :locale => 'en', :include_seconds => true, :prompt => true)
+ I18n.stub(:translate, prompts_check) do
+ datetime_select("post", "updated_at", locale: "en", include_seconds: true, prompt: true, use_month_names: Date::MONTHNAMES)
+ end
+ assert_equal defaults.count, @prompt_called
end
# date_or_time_select
def test_date_or_time_select_given_an_order_options_does_not_translate_order
- I18n.expects(:translate).never
- datetime_select('post', 'updated_at', :order => [:year, :month, :day], :locale => 'en')
+ assert_not_called(I18n, :translate) do
+ datetime_select("post", "updated_at", order: [:year, :month, :day], locale: "en", use_month_names: Date::MONTHNAMES)
+ end
end
def test_date_or_time_select_given_no_order_options_translates_order
- I18n.expects(:translate).with(:'date.order', :locale => 'en', :default => []).returns %w(year month day)
- datetime_select('post', 'updated_at', :locale => 'en')
+ assert_called_with(I18n, :translate, [ [:'date.order', locale: "en", default: []], [:"date.month_names", { locale: "en" }] ], returns: %w(year month day)) do
+ datetime_select("post", "updated_at", locale: "en")
+ end
end
def test_date_or_time_select_given_invalid_order
- I18n.expects(:translate).with(:'date.order', :locale => 'en', :default => []).returns %w(invalid month day)
-
- assert_raise StandardError do
- datetime_select('post', 'updated_at', :locale => 'en')
+ assert_called_with(I18n, :translate, [:'date.order', locale: "en", default: []], returns: %w(invalid month day)) do
+ assert_raise StandardError do
+ datetime_select("post", "updated_at", locale: "en")
+ end
end
end
def test_date_or_time_select_given_symbol_keys
- I18n.expects(:translate).with(:'date.order', :locale => 'en', :default => []).returns [:year, :month, :day]
- datetime_select('post', 'updated_at', :locale => 'en')
+ assert_called_with(I18n, :translate, [ [:'date.order', locale: "en", default: []], [:"date.month_names", { locale: "en" }] ], returns: [:year, :month, :day]) do
+ datetime_select("post", "updated_at", locale: "en")
+ end
end
end
diff --git a/actionview/test/template/date_helper_test.rb b/actionview/test/template/date_helper_test.rb
index 9212420ec9..0a294ec674 100644
--- a/actionview/test/template/date_helper_test.rb
+++ b/actionview/test/template/date_helper_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class DateHelperTest < ActionView::TestCase
tests ActionView::Helpers::DateHelper
@@ -13,41 +15,41 @@ class DateHelperTest < ActionView::TestCase
123
end
def to_param
- '123'
+ "123"
end
end
end
- def assert_distance_of_time_in_words(from, to=nil)
+ def assert_distance_of_time_in_words(from, to = nil)
to ||= from
# 0..1 minute with :include_seconds => true
- assert_equal "less than 5 seconds", distance_of_time_in_words(from, to + 0.seconds, :include_seconds => true)
- assert_equal "less than 5 seconds", distance_of_time_in_words(from, to + 4.seconds, :include_seconds => true)
- assert_equal "less than 10 seconds", distance_of_time_in_words(from, to + 5.seconds, :include_seconds => true)
- assert_equal "less than 10 seconds", distance_of_time_in_words(from, to + 9.seconds, :include_seconds => true)
- assert_equal "less than 20 seconds", distance_of_time_in_words(from, to + 10.seconds, :include_seconds => true)
- assert_equal "less than 20 seconds", distance_of_time_in_words(from, to + 19.seconds, :include_seconds => true)
- assert_equal "half a minute", distance_of_time_in_words(from, to + 20.seconds, :include_seconds => true)
- assert_equal "half a minute", distance_of_time_in_words(from, to + 39.seconds, :include_seconds => true)
- assert_equal "less than a minute", distance_of_time_in_words(from, to + 40.seconds, :include_seconds => true)
- assert_equal "less than a minute", distance_of_time_in_words(from, to + 59.seconds, :include_seconds => true)
- assert_equal "1 minute", distance_of_time_in_words(from, to + 60.seconds, :include_seconds => true)
- assert_equal "1 minute", distance_of_time_in_words(from, to + 89.seconds, :include_seconds => true)
+ assert_equal "less than 5 seconds", distance_of_time_in_words(from, to + 0.seconds, include_seconds: true)
+ assert_equal "less than 5 seconds", distance_of_time_in_words(from, to + 4.seconds, include_seconds: true)
+ assert_equal "less than 10 seconds", distance_of_time_in_words(from, to + 5.seconds, include_seconds: true)
+ assert_equal "less than 10 seconds", distance_of_time_in_words(from, to + 9.seconds, include_seconds: true)
+ assert_equal "less than 20 seconds", distance_of_time_in_words(from, to + 10.seconds, include_seconds: true)
+ assert_equal "less than 20 seconds", distance_of_time_in_words(from, to + 19.seconds, include_seconds: true)
+ assert_equal "half a minute", distance_of_time_in_words(from, to + 20.seconds, include_seconds: true)
+ assert_equal "half a minute", distance_of_time_in_words(from, to + 39.seconds, include_seconds: true)
+ assert_equal "less than a minute", distance_of_time_in_words(from, to + 40.seconds, include_seconds: true)
+ assert_equal "less than a minute", distance_of_time_in_words(from, to + 59.seconds, include_seconds: true)
+ assert_equal "1 minute", distance_of_time_in_words(from, to + 60.seconds, include_seconds: true)
+ assert_equal "1 minute", distance_of_time_in_words(from, to + 89.seconds, include_seconds: true)
# 0..1 minute with :include_seconds => false
- assert_equal "less than a minute", distance_of_time_in_words(from, to + 0.seconds, :include_seconds => false)
- assert_equal "less than a minute", distance_of_time_in_words(from, to + 4.seconds, :include_seconds => false)
- assert_equal "less than a minute", distance_of_time_in_words(from, to + 5.seconds, :include_seconds => false)
- assert_equal "less than a minute", distance_of_time_in_words(from, to + 9.seconds, :include_seconds => false)
- assert_equal "less than a minute", distance_of_time_in_words(from, to + 10.seconds, :include_seconds => false)
- assert_equal "less than a minute", distance_of_time_in_words(from, to + 19.seconds, :include_seconds => false)
- assert_equal "less than a minute", distance_of_time_in_words(from, to + 20.seconds, :include_seconds => false)
- assert_equal "1 minute", distance_of_time_in_words(from, to + 39.seconds, :include_seconds => false)
- assert_equal "1 minute", distance_of_time_in_words(from, to + 40.seconds, :include_seconds => false)
- assert_equal "1 minute", distance_of_time_in_words(from, to + 59.seconds, :include_seconds => false)
- assert_equal "1 minute", distance_of_time_in_words(from, to + 60.seconds, :include_seconds => false)
- assert_equal "1 minute", distance_of_time_in_words(from, to + 89.seconds, :include_seconds => false)
+ assert_equal "less than a minute", distance_of_time_in_words(from, to + 0.seconds, include_seconds: false)
+ assert_equal "less than a minute", distance_of_time_in_words(from, to + 4.seconds, include_seconds: false)
+ assert_equal "less than a minute", distance_of_time_in_words(from, to + 5.seconds, include_seconds: false)
+ assert_equal "less than a minute", distance_of_time_in_words(from, to + 9.seconds, include_seconds: false)
+ assert_equal "less than a minute", distance_of_time_in_words(from, to + 10.seconds, include_seconds: false)
+ assert_equal "less than a minute", distance_of_time_in_words(from, to + 19.seconds, include_seconds: false)
+ assert_equal "less than a minute", distance_of_time_in_words(from, to + 20.seconds, include_seconds: false)
+ assert_equal "1 minute", distance_of_time_in_words(from, to + 39.seconds, include_seconds: false)
+ assert_equal "1 minute", distance_of_time_in_words(from, to + 40.seconds, include_seconds: false)
+ assert_equal "1 minute", distance_of_time_in_words(from, to + 59.seconds, include_seconds: false)
+ assert_equal "1 minute", distance_of_time_in_words(from, to + 60.seconds, include_seconds: false)
+ assert_equal "1 minute", distance_of_time_in_words(from, to + 89.seconds, include_seconds: false)
# Note that we are including a 30-second boundary around the interval we
# want to test. For instance, "1 minute" is actually 30s to 1m29s. The
@@ -119,8 +121,8 @@ class DateHelperTest < ActionView::TestCase
# test to < from
assert_equal "about 4 hours", distance_of_time_in_words(from + 4.hours, to)
- assert_equal "less than 20 seconds", distance_of_time_in_words(from + 19.seconds, to, :include_seconds => true)
- assert_equal "less than a minute", distance_of_time_in_words(from + 19.seconds, to, :include_seconds => false)
+ assert_equal "less than 20 seconds", distance_of_time_in_words(from + 19.seconds, to, include_seconds: true)
+ assert_equal "less than a minute", distance_of_time_in_words(from + 19.seconds, to, include_seconds: false)
end
def test_distance_in_words
@@ -128,29 +130,45 @@ class DateHelperTest < ActionView::TestCase
assert_distance_of_time_in_words(from)
end
- def test_distance_in_words_with_mathn_required
- # test we avoid Integer#/ (redefined by mathn)
- silence_warnings { require "mathn" }
+ def test_distance_in_words_with_nil_input
+ assert_raises(ArgumentError) { distance_of_time_in_words(nil) }
+ assert_raises(ArgumentError) { distance_of_time_in_words(0, nil) }
+ end
+
+ def test_distance_in_words_with_mixed_argument_types
+ assert_equal "1 minute", distance_of_time_in_words(0, Time.at(60))
+ assert_equal "10 minutes", distance_of_time_in_words(Time.at(600), 0)
+ end
+
+ def test_distance_in_words_doesnt_use_the_quotient_operator
+ rubinius_skip "Date is written in Ruby and relies on Integer#/"
+ jruby_skip "Date is written in Ruby and relies on Integer#/"
+
+ # Make sure that we avoid Integer#/ (redefined by mathn)
+ Integer.send :private, :/
+
from = Time.utc(2004, 6, 6, 21, 45, 0)
assert_distance_of_time_in_words(from)
+ ensure
+ Integer.send :public, :/
end
def test_time_ago_in_words_passes_include_seconds
- assert_equal "less than 20 seconds", time_ago_in_words(15.seconds.ago, :include_seconds => true)
- assert_equal "less than a minute", time_ago_in_words(15.seconds.ago, :include_seconds => false)
+ assert_equal "less than 20 seconds", time_ago_in_words(15.seconds.ago, include_seconds: true)
+ assert_equal "less than a minute", time_ago_in_words(15.seconds.ago, include_seconds: false)
end
def test_distance_in_words_with_time_zones
from = Time.mktime(2004, 6, 6, 21, 45, 0)
- assert_distance_of_time_in_words(from.in_time_zone('Alaska'))
- assert_distance_of_time_in_words(from.in_time_zone('Hawaii'))
+ assert_distance_of_time_in_words(from.in_time_zone("Alaska"))
+ assert_distance_of_time_in_words(from.in_time_zone("Hawaii"))
end
def test_distance_in_words_with_different_time_zones
from = Time.mktime(2004, 6, 6, 21, 45, 0)
assert_distance_of_time_in_words(
- from.in_time_zone('Alaska'),
- from.in_time_zone('Hawaii')
+ from.in_time_zone("Alaska"),
+ from.in_time_zone("Hawaii")
)
end
@@ -167,9 +185,9 @@ class DateHelperTest < ActionView::TestCase
def test_distance_in_words_with_integers
assert_equal "1 minute", distance_of_time_in_words(59)
- assert_equal "about 1 hour", distance_of_time_in_words(60*60)
+ assert_equal "about 1 hour", distance_of_time_in_words(60 * 60)
assert_equal "1 minute", distance_of_time_in_words(0, 59)
- assert_equal "about 1 hour", distance_of_time_in_words(60*60, 0)
+ assert_equal "about 1 hour", distance_of_time_in_words(60 * 60, 0)
assert_equal "about 3 years", distance_of_time_in_words(10**8)
assert_equal "about 3 years", distance_of_time_in_words(0, 10**8)
end
@@ -185,10 +203,10 @@ class DateHelperTest < ActionView::TestCase
assert_equal "about 1 hour", distance_of_time_in_words(60.minutes)
# include seconds
- assert_equal "half a minute", distance_of_time_in_words(39.seconds, 0, :include_seconds => true)
- assert_equal "less than a minute", distance_of_time_in_words(40.seconds, 0, :include_seconds => true)
- assert_equal "less than a minute", distance_of_time_in_words(59.seconds, 0, :include_seconds => true)
- assert_equal "1 minute", distance_of_time_in_words(60.seconds, 0, :include_seconds => true)
+ assert_equal "half a minute", distance_of_time_in_words(39.seconds, 0, include_seconds: true)
+ assert_equal "less than a minute", distance_of_time_in_words(40.seconds, 0, include_seconds: true)
+ assert_equal "less than a minute", distance_of_time_in_words(59.seconds, 0, include_seconds: true)
+ assert_equal "1 minute", distance_of_time_in_words(60.seconds, 0, include_seconds: true)
end
def test_time_ago_in_words
@@ -196,7 +214,7 @@ class DateHelperTest < ActionView::TestCase
end
def test_select_day
- expected = %(<select id="date_day" name="date[day]">\n)
+ expected = +%(<select id="date_day" name="date[day]">\n)
expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
expected << "</select>\n"
@@ -205,58 +223,74 @@ class DateHelperTest < ActionView::TestCase
end
def test_select_day_with_blank
- expected = %(<select id="date_day" name="date[day]">\n)
+ expected = +%(<select id="date_day" name="date[day]">\n)
expected << %(<option value=""></option>\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_day(Time.mktime(2003, 8, 16), :include_blank => true)
- assert_dom_equal expected, select_day(16, :include_blank => true)
+ assert_dom_equal expected, select_day(Time.mktime(2003, 8, 16), include_blank: true)
+ assert_dom_equal expected, select_day(16, include_blank: true)
end
def test_select_day_nil_with_blank
- expected = %(<select id="date_day" name="date[day]">\n)
+ expected = +%(<select id="date_day" name="date[day]">\n)
expected << %(<option value=""></option>\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_day(nil, :include_blank => true)
+ assert_dom_equal expected, select_day(nil, include_blank: true)
end
def test_select_day_with_two_digit_numbers
- expected = %(<select id="date_day" name="date[day]">\n)
+ expected = +%(<select id="date_day" name="date[day]">\n)
expected << %(<option value="1">01</option>\n<option selected="selected" value="2">02</option>\n<option value="3">03</option>\n<option value="4">04</option>\n<option value="5">05</option>\n<option value="6">06</option>\n<option value="7">07</option>\n<option value="8">08</option>\n<option value="9">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_day(Time.mktime(2011, 8, 2), :use_two_digit_numbers => true)
- assert_dom_equal expected, select_day(2, :use_two_digit_numbers => true)
+ assert_dom_equal expected, select_day(Time.mktime(2011, 8, 2), use_two_digit_numbers: true)
+ assert_dom_equal expected, select_day(2, use_two_digit_numbers: true)
end
def test_select_day_with_html_options
- expected = %(<select id="date_day" name="date[day]" class="selector">\n)
+ expected = +%(<select id="date_day" name="date[day]" class="selector">\n)
expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_day(Time.mktime(2003, 8, 16), {}, :class => 'selector')
- assert_dom_equal expected, select_day(16, {}, :class => 'selector')
+ assert_dom_equal expected, select_day(Time.mktime(2003, 8, 16), {}, { class: "selector" })
+ assert_dom_equal expected, select_day(16, {}, { class: "selector" })
end
def test_select_day_with_default_prompt
- expected = %(<select id="date_day" name="date[day]">\n)
+ expected = +%(<select id="date_day" name="date[day]">\n)
expected << %(<option value="">Day</option>\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_day(16, :prompt => true)
+ assert_dom_equal expected, select_day(16, prompt: true)
end
def test_select_day_with_custom_prompt
- expected = %(<select id="date_day" name="date[day]">\n)
+ expected = +%(<select id="date_day" name="date[day]">\n)
expected << %(<option value="">Choose day</option>\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_day(16, :prompt => 'Choose day')
+ assert_dom_equal expected, select_day(16, prompt: "Choose day")
+ end
+
+ def test_select_day_with_generic_with_css_classes
+ expected = +%(<select id="date_day" name="date[day]" class="day">\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_day(16, with_css_classes: true)
+ end
+
+ def test_select_day_with_custom_with_css_classes
+ expected = +%(<select id="date_day" name="date[day]" class="my-day">\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_day(16, with_css_classes: { day: "my-day" })
end
def test_select_month
- expected = %(<select id="date_month" name="date[month]">\n)
+ expected = +%(<select id="date_month" name="date[month]">\n)
expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
expected << "</select>\n"
@@ -265,240 +299,278 @@ class DateHelperTest < ActionView::TestCase
end
def test_select_month_with_two_digit_numbers
- expected = %(<select id="date_month" name="date[month]">\n)
+ expected = +%(<select id="date_month" name="date[month]">\n)
expected << %(<option value="1">01</option>\n<option value="2">02</option>\n<option value="3">03</option>\n<option value="4">04</option>\n<option value="5">05</option>\n<option value="6">06</option>\n<option value="7">07</option>\n<option value="8" selected="selected">08</option>\n<option value="9">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_month(Time.mktime(2011, 8, 16), :use_two_digit_numbers => true)
- assert_dom_equal expected, select_month(8, :use_two_digit_numbers => true)
+ assert_dom_equal expected, select_month(Time.mktime(2011, 8, 16), use_two_digit_numbers: true)
+ assert_dom_equal expected, select_month(8, use_two_digit_numbers: true)
end
def test_select_month_with_disabled
- expected = %(<select id="date_month" name="date[month]" disabled="disabled">\n)
+ expected = +%(<select id="date_month" name="date[month]" disabled="disabled">\n)
expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :disabled => true)
- assert_dom_equal expected, select_month(8, :disabled => true)
+ assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), disabled: true)
+ assert_dom_equal expected, select_month(8, disabled: true)
end
def test_select_month_with_field_name_override
- expected = %(<select id="date_mois" name="date[mois]">\n)
+ expected = +%(<select id="date_mois" name="date[mois]">\n)
expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :field_name => 'mois')
- assert_dom_equal expected, select_month(8, :field_name => 'mois')
+ assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), field_name: "mois")
+ assert_dom_equal expected, select_month(8, field_name: "mois")
end
def test_select_month_with_blank
- expected = %(<select id="date_month" name="date[month]">\n)
+ expected = +%(<select id="date_month" name="date[month]">\n)
expected << %(<option value=""></option>\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :include_blank => true)
- assert_dom_equal expected, select_month(8, :include_blank => true)
+ assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), include_blank: true)
+ assert_dom_equal expected, select_month(8, include_blank: true)
end
def test_select_month_nil_with_blank
- expected = %(<select id="date_month" name="date[month]">\n)
+ expected = +%(<select id="date_month" name="date[month]">\n)
expected << %(<option value=""></option>\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_month(nil, :include_blank => true)
+ assert_dom_equal expected, select_month(nil, include_blank: true)
end
def test_select_month_with_numbers
- expected = %(<select id="date_month" name="date[month]">\n)
+ expected = +%(<select id="date_month" name="date[month]">\n)
expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8" selected="selected">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :use_month_numbers => true)
- assert_dom_equal expected, select_month(8, :use_month_numbers => true)
+ assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), use_month_numbers: true)
+ assert_dom_equal expected, select_month(8, use_month_numbers: true)
end
def test_select_month_with_numbers_and_names
- expected = %(<select id="date_month" name="date[month]">\n)
+ expected = +%(<select id="date_month" name="date[month]">\n)
expected << %(<option value="1">1 - January</option>\n<option value="2">2 - February</option>\n<option value="3">3 - March</option>\n<option value="4">4 - April</option>\n<option value="5">5 - May</option>\n<option value="6">6 - June</option>\n<option value="7">7 - July</option>\n<option value="8" selected="selected">8 - August</option>\n<option value="9">9 - September</option>\n<option value="10">10 - October</option>\n<option value="11">11 - November</option>\n<option value="12">12 - December</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :add_month_numbers => true)
- assert_dom_equal expected, select_month(8, :add_month_numbers => true)
+ assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), add_month_numbers: true)
+ assert_dom_equal expected, select_month(8, add_month_numbers: true)
end
def test_select_month_with_format_string
- expected = %(<select id="date_month" name="date[month]">\n)
+ expected = +%(<select id="date_month" name="date[month]">\n)
expected << %(<option value="1">January (01)</option>\n<option value="2">February (02)</option>\n<option value="3">March (03)</option>\n<option value="4">April (04)</option>\n<option value="5">May (05)</option>\n<option value="6">June (06)</option>\n<option value="7">July (07)</option>\n<option value="8" selected="selected">August (08)</option>\n<option value="9">September (09)</option>\n<option value="10">October (10)</option>\n<option value="11">November (11)</option>\n<option value="12">December (12)</option>\n)
expected << "</select>\n"
- format_string = '%{name} (%<number>02d)'
- assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :month_format_string => format_string)
- assert_dom_equal expected, select_month(8, :month_format_string => format_string)
+ format_string = "%{name} (%<number>02d)"
+ assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), month_format_string: format_string)
+ assert_dom_equal expected, select_month(8, month_format_string: format_string)
end
def test_select_month_with_numbers_and_names_with_abbv
- expected = %(<select id="date_month" name="date[month]">\n)
+ expected = +%(<select id="date_month" name="date[month]">\n)
expected << %(<option value="1">1 - Jan</option>\n<option value="2">2 - Feb</option>\n<option value="3">3 - Mar</option>\n<option value="4">4 - Apr</option>\n<option value="5">5 - May</option>\n<option value="6">6 - Jun</option>\n<option value="7">7 - Jul</option>\n<option value="8" selected="selected">8 - Aug</option>\n<option value="9">9 - Sep</option>\n<option value="10">10 - Oct</option>\n<option value="11">11 - Nov</option>\n<option value="12">12 - Dec</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :add_month_numbers => true, :use_short_month => true)
- assert_dom_equal expected, select_month(8, :add_month_numbers => true, :use_short_month => true)
+ assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), add_month_numbers: true, use_short_month: true)
+ assert_dom_equal expected, select_month(8, add_month_numbers: true, use_short_month: true)
end
def test_select_month_with_abbv
- expected = %(<select id="date_month" name="date[month]">\n)
+ expected = +%(<select id="date_month" name="date[month]">\n)
expected << %(<option value="1">Jan</option>\n<option value="2">Feb</option>\n<option value="3">Mar</option>\n<option value="4">Apr</option>\n<option value="5">May</option>\n<option value="6">Jun</option>\n<option value="7">Jul</option>\n<option value="8" selected="selected">Aug</option>\n<option value="9">Sep</option>\n<option value="10">Oct</option>\n<option value="11">Nov</option>\n<option value="12">Dec</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :use_short_month => true)
- assert_dom_equal expected, select_month(8, :use_short_month => true)
+ assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), use_short_month: true)
+ assert_dom_equal expected, select_month(8, use_short_month: true)
end
def test_select_month_with_custom_names
month_names = %w(nil Januar Februar Marts April Maj Juni Juli August September Oktober November December)
- expected = %(<select id="date_month" name="date[month]">\n)
+ expected = +%(<select id="date_month" name="date[month]">\n)
1.upto(12) { |month| expected << %(<option value="#{month}"#{' selected="selected"' if month == 8}>#{month_names[month]}</option>\n) }
expected << "</select>\n"
- assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :use_month_names => month_names)
- assert_dom_equal expected, select_month(8, :use_month_names => month_names)
+ assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), use_month_names: month_names)
+ assert_dom_equal expected, select_month(8, use_month_names: month_names)
end
def test_select_month_with_zero_indexed_custom_names
month_names = %w(Januar Februar Marts April Maj Juni Juli August September Oktober November December)
- expected = %(<select id="date_month" name="date[month]">\n)
- 1.upto(12) { |month| expected << %(<option value="#{month}"#{' selected="selected"' if month == 8}>#{month_names[month-1]}</option>\n) }
+ expected = +%(<select id="date_month" name="date[month]">\n)
+ 1.upto(12) { |month| expected << %(<option value="#{month}"#{' selected="selected"' if month == 8}>#{month_names[month - 1]}</option>\n) }
expected << "</select>\n"
- assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :use_month_names => month_names)
- assert_dom_equal expected, select_month(8, :use_month_names => month_names)
+ assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), use_month_names: month_names)
+ assert_dom_equal expected, select_month(8, use_month_names: month_names)
end
def test_select_month_with_hidden
- assert_dom_equal "<input type=\"hidden\" id=\"date_month\" name=\"date[month]\" value=\"8\" />\n", select_month(8, :use_hidden => true)
+ assert_dom_equal "<input type=\"hidden\" id=\"date_month\" name=\"date[month]\" value=\"8\" />\n", select_month(8, use_hidden: true)
end
def test_select_month_with_hidden_and_field_name
- assert_dom_equal "<input type=\"hidden\" id=\"date_mois\" name=\"date[mois]\" value=\"8\" />\n", select_month(8, :use_hidden => true, :field_name => 'mois')
+ assert_dom_equal "<input type=\"hidden\" id=\"date_mois\" name=\"date[mois]\" value=\"8\" />\n", select_month(8, use_hidden: true, field_name: "mois")
end
def test_select_month_with_html_options
- expected = %(<select id="date_month" name="date[month]" class="selector" accesskey="M">\n)
+ expected = +%(<select id="date_month" name="date[month]" class="selector" accesskey="M">\n)
expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), {}, :class => 'selector', :accesskey => 'M')
- #result = select_month(Time.mktime(2003, 8, 16), {}, :class => 'selector', :accesskey => 'M')
- #assert result.include?('<select id="date_month" name="date[month]"')
- #assert result.include?('class="selector"')
- #assert result.include?('accesskey="M"')
- #assert result.include?('<option value="1">January')
+ assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), {}, { class: "selector", accesskey: "M" })
end
def test_select_month_with_default_prompt
- expected = %(<select id="date_month" name="date[month]">\n)
+ expected = +%(<select id="date_month" name="date[month]">\n)
expected << %(<option value="">Month</option>\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_month(8, :prompt => true)
+ assert_dom_equal expected, select_month(8, prompt: true)
end
def test_select_month_with_custom_prompt
- expected = %(<select id="date_month" name="date[month]">\n)
+ expected = +%(<select id="date_month" name="date[month]">\n)
expected << %(<option value="">Choose month</option>\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_month(8, :prompt => 'Choose month')
+ assert_dom_equal expected, select_month(8, prompt: "Choose month")
+ end
+
+ def test_select_month_with_generic_with_css_classes
+ expected = +%(<select id="date_month" name="date[month]" class="month">\n)
+ expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_month(8, with_css_classes: true)
+ end
+
+ def test_select_month_with_custom_with_css_classes
+ expected = +%(<select id="date_month" name="date[month]" class="my-month">\n)
+ expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_month(8, with_css_classes: { month: "my-month" })
end
def test_select_year
- expected = %(<select id="date_year" name="date[year]">\n)
+ expected = +%(<select id="date_year" name="date[year]">\n)
expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_year(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005)
- assert_dom_equal expected, select_year(2003, :start_year => 2003, :end_year => 2005)
+ assert_dom_equal expected, select_year(Time.mktime(2003, 8, 16), start_year: 2003, end_year: 2005)
+ assert_dom_equal expected, select_year(2003, start_year: 2003, end_year: 2005)
end
def test_select_year_with_disabled
- expected = %(<select id="date_year" name="date[year]" disabled="disabled">\n)
+ expected = +%(<select id="date_year" name="date[year]" disabled="disabled">\n)
expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_year(Time.mktime(2003, 8, 16), :disabled => true, :start_year => 2003, :end_year => 2005)
- assert_dom_equal expected, select_year(2003, :disabled => true, :start_year => 2003, :end_year => 2005)
+ assert_dom_equal expected, select_year(Time.mktime(2003, 8, 16), disabled: true, start_year: 2003, end_year: 2005)
+ assert_dom_equal expected, select_year(2003, disabled: true, start_year: 2003, end_year: 2005)
end
def test_select_year_with_field_name_override
- expected = %(<select id="date_annee" name="date[annee]">\n)
+ expected = +%(<select id="date_annee" name="date[annee]">\n)
expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_year(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005, :field_name => 'annee')
- assert_dom_equal expected, select_year(2003, :start_year => 2003, :end_year => 2005, :field_name => 'annee')
+ assert_dom_equal expected, select_year(Time.mktime(2003, 8, 16), start_year: 2003, end_year: 2005, field_name: "annee")
+ assert_dom_equal expected, select_year(2003, start_year: 2003, end_year: 2005, field_name: "annee")
end
def test_select_year_with_type_discarding
- expected = %(<select id="date_year" name="date_year">\n)
+ expected = +%(<select id="date_year" name="date_year">\n)
expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
expected << "</select>\n"
assert_dom_equal expected, select_year(
- Time.mktime(2003, 8, 16), :prefix => "date_year", :discard_type => true, :start_year => 2003, :end_year => 2005)
+ Time.mktime(2003, 8, 16), prefix: "date_year", discard_type: true, start_year: 2003, end_year: 2005)
assert_dom_equal expected, select_year(
- 2003, :prefix => "date_year", :discard_type => true, :start_year => 2003, :end_year => 2005)
+ 2003, prefix: "date_year", discard_type: true, start_year: 2003, end_year: 2005)
end
def test_select_year_descending
- expected = %(<select id="date_year" name="date[year]">\n)
+ expected = +%(<select id="date_year" name="date[year]">\n)
expected << %(<option value="2005" selected="selected">2005</option>\n<option value="2004">2004</option>\n<option value="2003">2003</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_year(Time.mktime(2005, 8, 16), :start_year => 2005, :end_year => 2003)
- assert_dom_equal expected, select_year(2005, :start_year => 2005, :end_year => 2003)
+ assert_dom_equal expected, select_year(Time.mktime(2005, 8, 16), start_year: 2005, end_year: 2003)
+ assert_dom_equal expected, select_year(2005, start_year: 2005, end_year: 2003)
end
def test_select_year_with_hidden
- assert_dom_equal "<input type=\"hidden\" id=\"date_year\" name=\"date[year]\" value=\"2007\" />\n", select_year(2007, :use_hidden => true)
+ assert_dom_equal "<input type=\"hidden\" id=\"date_year\" name=\"date[year]\" value=\"2007\" />\n", select_year(2007, use_hidden: true)
end
def test_select_year_with_hidden_and_field_name
- assert_dom_equal "<input type=\"hidden\" id=\"date_anno\" name=\"date[anno]\" value=\"2007\" />\n", select_year(2007, :use_hidden => true, :field_name => 'anno')
+ assert_dom_equal "<input type=\"hidden\" id=\"date_anno\" name=\"date[anno]\" value=\"2007\" />\n", select_year(2007, use_hidden: true, field_name: "anno")
end
def test_select_year_with_html_options
- expected = %(<select id="date_year" name="date[year]" class="selector" accesskey="M">\n)
+ expected = +%(<select id="date_year" name="date[year]" class="selector" accesskey="M">\n)
expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_year(Time.mktime(2003, 8, 16), {:start_year => 2003, :end_year => 2005}, :class => 'selector', :accesskey => 'M')
- #result = select_year(Time.mktime(2003, 8, 16), {:start_year => 2003, :end_year => 2005}, :class => 'selector', :accesskey => 'M')
- #assert result.include?('<select id="date_year" name="date[year]"')
- #assert result.include?('class="selector"')
- #assert result.include?('accesskey="M"')
- #assert result.include?('<option value="2003"')
+ assert_dom_equal expected, select_year(Time.mktime(2003, 8, 16), { start_year: 2003, end_year: 2005 }, { class: "selector", accesskey: "M" })
end
def test_select_year_with_default_prompt
- expected = %(<select id="date_year" name="date[year]">\n)
+ expected = +%(<select id="date_year" name="date[year]">\n)
expected << %(<option value="">Year</option>\n<option value="2003">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_year(nil, :start_year => 2003, :end_year => 2005, :prompt => true)
+ assert_dom_equal expected, select_year(nil, start_year: 2003, end_year: 2005, prompt: true)
end
def test_select_year_with_custom_prompt
- expected = %(<select id="date_year" name="date[year]">\n)
+ expected = +%(<select id="date_year" name="date[year]">\n)
expected << %(<option value="">Choose year</option>\n<option value="2003">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_year(nil, :start_year => 2003, :end_year => 2005, :prompt => 'Choose year')
+ assert_dom_equal expected, select_year(nil, start_year: 2003, end_year: 2005, prompt: "Choose year")
+ end
+
+ def test_select_year_with_generic_with_css_classes
+ expected = +%(<select id="date_year" name="date[year]" class="year">\n)
+ expected << %(<option value="2003">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_year(nil, start_year: 2003, end_year: 2005, with_css_classes: true)
+ end
+
+ def test_select_year_with_custom_with_css_classes
+ expected = +%(<select id="date_year" name="date[year]" class="my-year">\n)
+ expected << %(<option value="2003">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_year(nil, start_year: 2003, end_year: 2005, with_css_classes: { year: "my-year" })
+ end
+
+ def test_select_year_with_position
+ expected = +%(<select id="date_year_1i" name="date[year(1i)]">\n)
+ expected << %(<option value="2003">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
+ expected << "</select>\n"
+ assert_dom_equal expected, select_year(Date.current, include_position: true, start_year: 2003, end_year: 2005)
+ end
+
+ def test_select_year_with_custom_names
+ year_format_lambda = ->year { "Heisei #{ year - 1988 }" }
+ expected = %(<select id="date_year" name="date[year]">\n).dup
+ expected << %(<option value="2003">Heisei 15</option>\n<option value="2004">Heisei 16</option>\n<option value="2005">Heisei 17</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_year(nil, start_year: 2003, end_year: 2005, year_format: year_format_lambda)
end
def test_select_hour
- expected = %(<select id="date_hour" name="date[hour]">\n)
+ expected = +%(<select id="date_hour" name="date[hour]">\n)
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
expected << "</select>\n"
@@ -506,71 +578,87 @@ class DateHelperTest < ActionView::TestCase
end
def test_select_hour_with_ampm
- expected = %(<select id="date_hour" name="date[hour]">\n)
+ expected = +%(<select id="date_hour" name="date[hour]">\n)
expected << %(<option value="00">12 AM</option>\n<option value="01">01 AM</option>\n<option value="02">02 AM</option>\n<option value="03">03 AM</option>\n<option value="04">04 AM</option>\n<option value="05">05 AM</option>\n<option value="06">06 AM</option>\n<option value="07">07 AM</option>\n<option value="08" selected="selected">08 AM</option>\n<option value="09">09 AM</option>\n<option value="10">10 AM</option>\n<option value="11">11 AM</option>\n<option value="12">12 PM</option>\n<option value="13">01 PM</option>\n<option value="14">02 PM</option>\n<option value="15">03 PM</option>\n<option value="16">04 PM</option>\n<option value="17">05 PM</option>\n<option value="18">06 PM</option>\n<option value="19">07 PM</option>\n<option value="20">08 PM</option>\n<option value="21">09 PM</option>\n<option value="22">10 PM</option>\n<option value="23">11 PM</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), :ampm => true)
+ assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), ampm: true)
end
def test_select_hour_with_disabled
- expected = %(<select id="date_hour" name="date[hour]" disabled="disabled">\n)
+ expected = +%(<select id="date_hour" name="date[hour]" disabled="disabled">\n)
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), :disabled => true)
+ assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), disabled: true)
end
def test_select_hour_with_field_name_override
- expected = %(<select id="date_heure" name="date[heure]">\n)
+ expected = +%(<select id="date_heure" name="date[heure]">\n)
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), :field_name => 'heure')
+ assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), field_name: "heure")
end
def test_select_hour_with_blank
- expected = %(<select id="date_hour" name="date[hour]">\n)
+ expected = +%(<select id="date_hour" name="date[hour]">\n)
expected << %(<option value=""></option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), :include_blank => true)
+ assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), include_blank: true)
end
def test_select_hour_nil_with_blank
- expected = %(<select id="date_hour" name="date[hour]">\n)
+ expected = +%(<select id="date_hour" name="date[hour]">\n)
expected << %(<option value=""></option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_hour(nil, :include_blank => true)
+ assert_dom_equal expected, select_hour(nil, include_blank: true)
end
def test_select_hour_with_html_options
- expected = %(<select id="date_hour" name="date[hour]" class="selector" accesskey="M">\n)
+ expected = +%(<select id="date_hour" name="date[hour]" class="selector" accesskey="M">\n)
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), {}, :class => 'selector', :accesskey => 'M')
+ assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), {}, { class: "selector", accesskey: "M" })
end
def test_select_hour_with_default_prompt
- expected = %(<select id="date_hour" name="date[hour]">\n)
+ expected = +%(<select id="date_hour" name="date[hour]">\n)
expected << %(<option value="">Hour</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => true)
+ assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), prompt: true)
end
def test_select_hour_with_custom_prompt
- expected = %(<select id="date_hour" name="date[hour]">\n)
+ expected = +%(<select id="date_hour" name="date[hour]">\n)
expected << %(<option value="">Choose hour</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => 'Choose hour')
+ assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), prompt: "Choose hour")
+ end
+
+ def test_select_hour_with_generic_with_css_classes
+ expected = +%(<select id="date_hour" name="date[hour]" class="hour">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), with_css_classes: true)
+ end
+
+ def test_select_hour_with_custom_with_css_classes
+ expected = +%(<select id="date_hour" name="date[hour]" class="my-hour">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), with_css_classes: { hour: "my-hour" })
end
def test_select_minute
- expected = %(<select id="date_minute" name="date[minute]">\n)
+ expected = +%(<select id="date_minute" name="date[minute]">\n)
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
@@ -578,93 +666,103 @@ class DateHelperTest < ActionView::TestCase
end
def test_select_minute_with_disabled
- expected = %(<select id="date_minute" name="date[minute]" disabled="disabled">\n)
+ expected = +%(<select id="date_minute" name="date[minute]" disabled="disabled">\n)
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), :disabled => true)
+ assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), disabled: true)
end
def test_select_minute_with_field_name_override
- expected = %(<select id="date_minuto" name="date[minuto]">\n)
+ expected = +%(<select id="date_minuto" name="date[minuto]">\n)
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), :field_name => 'minuto')
+ assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), field_name: "minuto")
end
def test_select_minute_with_blank
- expected = %(<select id="date_minute" name="date[minute]">\n)
+ expected = +%(<select id="date_minute" name="date[minute]">\n)
expected << %(<option value=""></option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), :include_blank => true)
+ assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), include_blank: true)
end
def test_select_minute_with_blank_and_step
- expected = %(<select id="date_minute" name="date[minute]">\n)
+ expected = +%(<select id="date_minute" name="date[minute]">\n)
expected << %(<option value=""></option>\n<option value="00">00</option>\n<option value="15">15</option>\n<option value="30">30</option>\n<option value="45">45</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), { :include_blank => true , :minute_step => 15 })
+ assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), include_blank: true, minute_step: 15)
end
def test_select_minute_nil_with_blank
- expected = %(<select id="date_minute" name="date[minute]">\n)
+ expected = +%(<select id="date_minute" name="date[minute]">\n)
expected << %(<option value=""></option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_minute(nil, :include_blank => true)
+ assert_dom_equal expected, select_minute(nil, include_blank: true)
end
def test_select_minute_nil_with_blank_and_step
- expected = %(<select id="date_minute" name="date[minute]">\n)
+ expected = +%(<select id="date_minute" name="date[minute]">\n)
expected << %(<option value=""></option>\n<option value="00">00</option>\n<option value="15">15</option>\n<option value="30">30</option>\n<option value="45">45</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_minute(nil, { :include_blank => true , :minute_step => 15 })
+ assert_dom_equal expected, select_minute(nil, include_blank: true, minute_step: 15)
end
def test_select_minute_with_hidden
- assert_dom_equal "<input type=\"hidden\" id=\"date_minute\" name=\"date[minute]\" value=\"8\" />\n", select_minute(8, :use_hidden => true)
+ assert_dom_equal "<input type=\"hidden\" id=\"date_minute\" name=\"date[minute]\" value=\"8\" />\n", select_minute(8, use_hidden: true)
end
def test_select_minute_with_hidden_and_field_name
- assert_dom_equal "<input type=\"hidden\" id=\"date_minuto\" name=\"date[minuto]\" value=\"8\" />\n", select_minute(8, :use_hidden => true, :field_name => 'minuto')
+ assert_dom_equal "<input type=\"hidden\" id=\"date_minuto\" name=\"date[minuto]\" value=\"8\" />\n", select_minute(8, use_hidden: true, field_name: "minuto")
end
def test_select_minute_with_html_options
- expected = %(<select id="date_minute" name="date[minute]" class="selector" accesskey="M">\n)
+ expected = +%(<select id="date_minute" name="date[minute]" class="selector" accesskey="M">\n)
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), {}, :class => 'selector', :accesskey => 'M')
-
- #result = select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), {}, :class => 'selector', :accesskey => 'M')
- #assert result.include?('<select id="date_minute" name="date[minute]"')
- #assert result.include?('class="selector"')
- #assert result.include?('accesskey="M"')
- #assert result.include?('<option value="00">00')
+ assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), {}, { class: "selector", accesskey: "M" })
end
def test_select_minute_with_default_prompt
- expected = %(<select id="date_minute" name="date[minute]">\n)
+ expected = +%(<select id="date_minute" name="date[minute]">\n)
expected << %(<option value="">Minute</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => true)
+ assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), prompt: true)
end
def test_select_minute_with_custom_prompt
- expected = %(<select id="date_minute" name="date[minute]">\n)
+ expected = +%(<select id="date_minute" name="date[minute]">\n)
expected << %(<option value="">Choose minute</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => 'Choose minute')
+ assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), prompt: "Choose minute")
+ end
+
+ def test_select_minute_with_generic_with_css_classes
+ expected = +%(<select id="date_minute" name="date[minute]" class="minute">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), with_css_classes: true)
+ end
+
+ def test_select_minute_with_custom_with_css_classes
+ expected = +%(<select id="date_minute" name="date[minute]" class="my-minute">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), with_css_classes: { minute: "my-minute" })
end
def test_select_second
- expected = %(<select id="date_second" name="date[second]">\n)
+ expected = +%(<select id="date_second" name="date[second]">\n)
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
@@ -672,69 +770,79 @@ class DateHelperTest < ActionView::TestCase
end
def test_select_second_with_disabled
- expected = %(<select id="date_second" name="date[second]" disabled="disabled">\n)
+ expected = +%(<select id="date_second" name="date[second]" disabled="disabled">\n)
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), :disabled => true)
+ assert_dom_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), disabled: true)
end
def test_select_second_with_field_name_override
- expected = %(<select id="date_segundo" name="date[segundo]">\n)
+ expected = +%(<select id="date_segundo" name="date[segundo]">\n)
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), :field_name => 'segundo')
+ assert_dom_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), field_name: "segundo")
end
def test_select_second_with_blank
- expected = %(<select id="date_second" name="date[second]">\n)
+ expected = +%(<select id="date_second" name="date[second]">\n)
expected << %(<option value=""></option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), :include_blank => true)
+ assert_dom_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), include_blank: true)
end
def test_select_second_nil_with_blank
- expected = %(<select id="date_second" name="date[second]">\n)
+ expected = +%(<select id="date_second" name="date[second]">\n)
expected << %(<option value=""></option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_second(nil, :include_blank => true)
+ assert_dom_equal expected, select_second(nil, include_blank: true)
end
def test_select_second_with_html_options
- expected = %(<select id="date_second" name="date[second]" class="selector" accesskey="M">\n)
+ expected = +%(<select id="date_second" name="date[second]" class="selector" accesskey="M">\n)
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), {}, :class => 'selector', :accesskey => 'M')
-
- #result = select_second(Time.mktime(2003, 8, 16, 8, 4, 18), {}, :class => 'selector', :accesskey => 'M')
- #assert result.include?('<select id="date_second" name="date[second]"')
- #assert result.include?('class="selector"')
- #assert result.include?('accesskey="M"')
- #assert result.include?('<option value="00">00')
+ assert_dom_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), {}, { class: "selector", accesskey: "M" })
end
def test_select_second_with_default_prompt
- expected = %(<select id="date_second" name="date[second]">\n)
+ expected = +%(<select id="date_second" name="date[second]">\n)
expected << %(<option value="">Seconds</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => true)
+ assert_dom_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), prompt: true)
end
def test_select_second_with_custom_prompt
- expected = %(<select id="date_second" name="date[second]">\n)
+ expected = +%(<select id="date_second" name="date[second]">\n)
expected << %(<option value="">Choose seconds</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => 'Choose seconds')
+ assert_dom_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), prompt: "Choose seconds")
+ end
+
+ def test_select_second_with_generic_with_css_classes
+ expected = +%(<select id="date_second" name="date[second]" class="second">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), with_css_classes: true)
+ end
+
+ def test_select_second_with_custom_with_css_classes
+ expected = +%(<select id="date_second" name="date[second]" class="my-second">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), with_css_classes: { second: "my-second" })
end
def test_select_date
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ expected = +%(<select id="date_first_year" name="date[first][year]">\n)
expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
expected << "</select>\n"
@@ -746,20 +854,20 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005, :prefix => "date[first]")
+ assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), start_year: 2003, end_year: 2005, prefix: "date[first]")
end
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: 2000, end_year: 20000, 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
- assert_nothing_raised { select_date(Time.mktime(2003, 8, 16), :start_year => 2000, :end_year => 3100, :max_years_allowed => 2000) }
+ assert_nothing_raised { select_date(Time.mktime(2003, 8, 16), start_year: 2000, end_year: 3100, max_years_allowed: 2000) }
end
def test_select_date_with_order
- expected = %(<select id="date_first_month" name="date[first][month]">\n)
+ expected = +%(<select id="date_first_month" name="date[first][month]">\n)
expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
expected << "</select>\n"
@@ -767,24 +875,24 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
expected << "</select>\n"
- expected << %(<select id="date_first_year" name="date[first][year]">\n)
+ expected << %(<select id="date_first_year" name="date[first][year]">\n)
expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005, :prefix => "date[first]", :order => [:month, :day, :year])
+ assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), start_year: 2003, end_year: 2005, prefix: "date[first]", order: [:month, :day, :year])
end
def test_select_date_with_incomplete_order
# Since the order is incomplete nothing will be shown
- expected = %(<input id="date_first_year" name="date[first][year]" type="hidden" value="2003" />\n)
+ expected = +%(<input id="date_first_year" name="date[first][year]" type="hidden" value="2003" />\n)
expected << %(<input id="date_first_month" name="date[first][month]" type="hidden" value="8" />\n)
expected << %(<input id="date_first_day" name="date[first][day]" type="hidden" value="1" />\n)
- assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005, :prefix => "date[first]", :order => [:day])
+ assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), start_year: 2003, end_year: 2005, prefix: "date[first]", order: [:day])
end
def test_select_date_with_disabled
- expected = %(<select id="date_first_year" name="date[first][year]" disabled="disabled">\n)
+ expected = +%(<select id="date_first_year" name="date[first][year]" disabled="disabled">\n)
expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
expected << "</select>\n"
@@ -796,12 +904,12 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005, :prefix => "date[first]", :disabled => true)
+ assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), start_year: 2003, end_year: 2005, prefix: "date[first]", disabled: true)
end
def test_select_date_with_no_start_year
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
- (Date.today.year-5).upto(Date.today.year+1) do |y|
+ expected = +%(<select id="date_first_year" name="date[first][year]">\n)
+ (Date.today.year - 5).upto(Date.today.year + 1) do |y|
if y == Date.today.year
expected << %(<option value="#{y}" selected="selected">#{y}</option>\n)
else
@@ -819,12 +927,12 @@ class DateHelperTest < ActionView::TestCase
expected << "</select>\n"
assert_dom_equal expected, select_date(
- Time.mktime(Date.today.year, 8, 16), :end_year => Date.today.year+1, :prefix => "date[first]"
+ Time.mktime(Date.today.year, 8, 16), end_year: Date.today.year + 1, prefix: "date[first]"
)
end
def test_select_date_with_no_end_year
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ expected = +%(<select id="date_first_year" name="date[first][year]">\n)
2003.upto(2008) do |y|
if y == 2003
expected << %(<option value="#{y}" selected="selected">#{y}</option>\n)
@@ -843,13 +951,13 @@ class DateHelperTest < ActionView::TestCase
expected << "</select>\n"
assert_dom_equal expected, select_date(
- Time.mktime(2003, 8, 16), :start_year => 2003, :prefix => "date[first]"
+ Time.mktime(2003, 8, 16), start_year: 2003, prefix: "date[first]"
)
end
def test_select_date_with_no_start_or_end_year
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
- (Date.today.year-5).upto(Date.today.year+5) do |y|
+ expected = +%(<select id="date_first_year" name="date[first][year]">\n)
+ (Date.today.year - 5).upto(Date.today.year + 5) do |y|
if y == Date.today.year
expected << %(<option value="#{y}" selected="selected">#{y}</option>\n)
else
@@ -867,12 +975,12 @@ class DateHelperTest < ActionView::TestCase
expected << "</select>\n"
assert_dom_equal expected, select_date(
- Time.mktime(Date.today.year, 8, 16), :prefix => "date[first]"
+ Time.mktime(Date.today.year, 8, 16), prefix: "date[first]"
)
end
def test_select_date_with_zero_value
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ expected = +%(<select id="date_first_year" name="date[first][year]">\n)
expected << %(<option value="2003">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
expected << "</select>\n"
@@ -884,12 +992,12 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_date(0, :start_year => 2003, :end_year => 2005, :prefix => "date[first]")
+ assert_dom_equal expected, select_date(0, start_year: 2003, end_year: 2005, prefix: "date[first]")
end
def test_select_date_with_zero_value_and_no_start_year
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
- (Date.today.year-5).upto(Date.today.year+1) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
+ expected = +%(<select id="date_first_year" name="date[first][year]">\n)
+ (Date.today.year - 5).upto(Date.today.year + 1) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
expected << "</select>\n"
expected << %(<select id="date_first_month" name="date[first][month]">\n)
@@ -900,11 +1008,11 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_date(0, :end_year => Date.today.year+1, :prefix => "date[first]")
+ assert_dom_equal expected, select_date(0, end_year: Date.today.year + 1, prefix: "date[first]")
end
def test_select_date_with_zero_value_and_no_end_year
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ expected = +%(<select id="date_first_year" name="date[first][year]">\n)
last_year = Time.now.year + 5
2003.upto(last_year) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
expected << "</select>\n"
@@ -917,12 +1025,12 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_date(0, :start_year => 2003, :prefix => "date[first]")
+ assert_dom_equal expected, select_date(0, start_year: 2003, prefix: "date[first]")
end
def test_select_date_with_zero_value_and_no_start_and_end_year
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
- (Date.today.year-5).upto(Date.today.year+5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
+ expected = +%(<select id="date_first_year" name="date[first][year]">\n)
+ (Date.today.year - 5).upto(Date.today.year + 5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
expected << "</select>\n"
expected << %(<select id="date_first_month" name="date[first][month]">\n)
@@ -933,12 +1041,12 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_date(0, :prefix => "date[first]")
+ assert_dom_equal expected, select_date(0, prefix: "date[first]")
end
def test_select_date_with_nil_value_and_no_start_and_end_year
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
- (Date.today.year-5).upto(Date.today.year+5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
+ expected = +%(<select id="date_first_year" name="date[first][year]">\n)
+ (Date.today.year - 5).upto(Date.today.year + 5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
expected << "</select>\n"
expected << %(<select id="date_first_month" name="date[first][month]">\n)
@@ -949,11 +1057,11 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_date(nil, :prefix => "date[first]")
+ assert_dom_equal expected, select_date(nil, prefix: "date[first]")
end
def test_select_date_with_html_options
- expected = %(<select id="date_first_year" name="date[first][year]" class="selector">\n)
+ expected = +%(<select id="date_first_year" name="date[first][year]" class="selector">\n)
expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
expected << "</select>\n"
@@ -965,11 +1073,11 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), {:start_year => 2003, :end_year => 2005, :prefix => "date[first]"}, :class => "selector")
+ assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { start_year: 2003, end_year: 2005, prefix: "date[first]" }, { class: "selector" })
end
def test_select_date_with_separator
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ expected = +%(<select id="date_first_year" name="date[first][year]">\n)
expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
expected << "</select>\n"
@@ -985,11 +1093,11 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { :date_separator => " / ", :start_year => 2003, :end_year => 2005, :prefix => "date[first]"})
+ assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), date_separator: " / ", start_year: 2003, end_year: 2005, prefix: "date[first]")
end
def test_select_date_with_separator_and_discard_day
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ expected = +%(<select id="date_first_year" name="date[first][year]">\n)
expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
expected << "</select>\n"
@@ -1001,31 +1109,31 @@ class DateHelperTest < ActionView::TestCase
expected << %(<input type="hidden" id="date_first_day" name="date[first][day]" value="1" />\n)
- assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { :date_separator => " / ", :discard_day => true, :start_year => 2003, :end_year => 2005, :prefix => "date[first]"})
+ assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), date_separator: " / ", discard_day: true, start_year: 2003, end_year: 2005, prefix: "date[first]")
end
def test_select_date_with_separator_discard_month_and_day
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ expected = +%(<select id="date_first_year" name="date[first][year]">\n)
expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
expected << "</select>\n"
expected << %(<input type="hidden" id="date_first_month" name="date[first][month]" value="8" />\n)
expected << %(<input type="hidden" id="date_first_day" name="date[first][day]" value="1" />\n)
- assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { :date_separator => " / ", :discard_month => true, :discard_day => true, :start_year => 2003, :end_year => 2005, :prefix => "date[first]"})
+ assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), date_separator: " / ", discard_month: true, discard_day: true, start_year: 2003, end_year: 2005, prefix: "date[first]")
end
def test_select_date_with_hidden
- expected = %(<input id="date_first_year" name="date[first][year]" type="hidden" value="2003"/>\n)
+ expected = +%(<input id="date_first_year" name="date[first][year]" type="hidden" value="2003"/>\n)
expected << %(<input id="date_first_month" name="date[first][month]" type="hidden" value="8" />\n)
expected << %(<input id="date_first_day" name="date[first][day]" type="hidden" value="16" />\n)
- assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { :prefix => "date[first]", :use_hidden => true })
- assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { :date_separator => " / ", :prefix => "date[first]", :use_hidden => true })
+ assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), prefix: "date[first]", use_hidden: true)
+ assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), date_separator: " / ", prefix: "date[first]", use_hidden: true)
end
def test_select_date_with_css_classes_option
- expected = %(<select id="date_first_year" name="date[first][year]" class="year">\n)
+ expected = +%(<select id="date_first_year" name="date[first][year]" class="year">\n)
expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
expected << "</select>\n"
@@ -1037,11 +1145,27 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), {:start_year => 2003, :end_year => 2005, :prefix => "date[first]", :with_css_classes => true})
+ assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), start_year: 2003, end_year: 2005, prefix: "date[first]", with_css_classes: true)
+ end
+
+ def test_select_date_with_custom_with_css_classes
+ expected = +%(<select id="date_year" name="date[year]" class="my-year">\n)
+ expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_month" name="date[month]" class="my-month">\n)
+ expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_day" name="date[day]" class="my-day">\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), start_year: 2003, end_year: 2005, with_css_classes: { year: "my-year", month: "my-month", day: "my-day" })
end
def test_select_date_with_css_classes_option_and_html_class_option
- expected = %(<select id="date_first_year" name="date[first][year]" class="datetime optional year">\n)
+ expected = +%(<select id="date_first_year" name="date[first][year]" class="datetime optional year">\n)
expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
expected << "</select>\n"
@@ -1053,11 +1177,59 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), {:start_year => 2003, :end_year => 2005, :prefix => "date[first]", :with_css_classes => true}, { class: 'datetime optional' })
+ assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { start_year: 2003, end_year: 2005, prefix: "date[first]", with_css_classes: true }, { class: "datetime optional" })
+ end
+
+ def test_select_date_with_custom_with_css_classes_and_html_class_option
+ expected = +%(<select id="date_year" name="date[year]" class="date optional my-year">\n)
+ expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_month" name="date[month]" class="date optional my-month">\n)
+ expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_day" name="date[day]" class="date optional my-day">\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { start_year: 2003, end_year: 2005, with_css_classes: { year: "my-year", month: "my-month", day: "my-day" } }, { class: "date optional" })
+ end
+
+ def test_select_date_with_partial_with_css_classes_and_html_class_option
+ expected = +%(<select id="date_year" name="date[year]" class="date optional">\n)
+ expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_month" name="date[month]" class="date optional my-month custom-grid">\n)
+ expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_day" name="date[day]" class="date optional">\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { start_year: 2003, end_year: 2005, with_css_classes: { month: "my-month custom-grid" } }, { class: "date optional" })
+ end
+
+ def test_select_date_with_html_class_option
+ expected = +%(<select id="date_year" name="date[year]" class="date optional custom-grid">\n)
+ expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_month" name="date[month]" class="date optional custom-grid">\n)
+ expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_day" name="date[day]" class="date optional custom-grid">\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { start_year: 2003, end_year: 2005 }, { class: "date optional custom-grid" })
end
def test_select_datetime
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ expected = +%(<select id="date_first_year" name="date[first][year]">\n)
expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
expected << "</select>\n"
@@ -1081,11 +1253,11 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :start_year => 2003, :end_year => 2005, :prefix => "date[first]")
+ assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), start_year: 2003, end_year: 2005, prefix: "date[first]")
end
def test_select_datetime_with_ampm
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ expected = +%(<select id="date_first_year" name="date[first][year]">\n)
expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
expected << "</select>\n"
@@ -1109,11 +1281,11 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :start_year => 2003, :end_year => 2005, :prefix => "date[first]", :ampm => true)
+ assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), start_year: 2003, end_year: 2005, prefix: "date[first]", ampm: true)
end
def test_select_datetime_with_separators
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ expected = +%(<select id="date_first_year" name="date[first][year]">\n)
expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
expected << "</select>\n"
@@ -1137,12 +1309,12 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :start_year => 2003, :end_year => 2005, :prefix => "date[first]", :datetime_separator => ' &mdash; ', :time_separator => ' : ')
+ assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), start_year: 2003, end_year: 2005, prefix: "date[first]", datetime_separator: " &mdash; ", time_separator: " : ")
end
def test_select_datetime_with_nil_value_and_no_start_and_end_year
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
- (Date.today.year-5).upto(Date.today.year+5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
+ expected = +%(<select id="date_first_year" name="date[first][year]">\n)
+ (Date.today.year - 5).upto(Date.today.year + 5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
expected << "</select>\n"
expected << %(<select id="date_first_month" name="date[first][month]">\n)
@@ -1165,15 +1337,14 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_datetime(nil, :prefix => "date[first]")
+ assert_dom_equal expected, select_datetime(nil, prefix: "date[first]")
end
def test_select_datetime_with_html_options
- expected = %(<select id="date_first_year" name="date[first][year]" class="selector">\n)
+ expected = +%(<select id="date_first_year" name="date[first][year]" class="selector">\n)
expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
expected << "</select>\n"
-
expected << %(<select id="date_first_month" name="date[first][month]" class="selector">\n)
expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
expected << "</select>\n"
@@ -1194,11 +1365,11 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), {:start_year => 2003, :end_year => 2005, :prefix => "date[first]"}, :class => 'selector')
+ assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), { start_year: 2003, end_year: 2005, prefix: "date[first]" }, { class: "selector" })
end
def test_select_datetime_with_all_separators
- expected = %(<select id="date_first_year" name="date[first][year]" class="selector">\n)
+ expected = +%(<select id="date_first_year" name="date[first][year]" class="selector">\n)
expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
expected << "</select>\n"
@@ -1226,7 +1397,7 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), { :datetime_separator => "&mdash;", :date_separator => "/", :time_separator => ":", :start_year => 2003, :end_year => 2005, :prefix => "date[first]"}, :class => 'selector')
+ assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), { datetime_separator: "&mdash;", date_separator: "/", time_separator: ":", start_year: 2003, end_year: 2005, prefix: "date[first]" }, { class: "selector" })
end
def test_select_datetime_should_work_with_date
@@ -1234,7 +1405,7 @@ class DateHelperTest < ActionView::TestCase
end
def test_select_datetime_with_default_prompt
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ expected = +%(<select id="date_first_year" name="date[first][year]">\n)
expected << %(<option value="">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"
@@ -1258,13 +1429,12 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="">Minute</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :start_year => 2003, :end_year => 2005,
- :prefix => "date[first]", :prompt => true)
+ assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), start_year: 2003, end_year: 2005,
+ prefix: "date[first]", prompt: true)
end
def test_select_datetime_with_custom_prompt
-
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ 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"
@@ -1288,12 +1458,68 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="">Choose minute</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :start_year => 2003, :end_year => 2005, :prefix => "date[first]",
- :prompt => {:day => 'Choose day', :month => 'Choose month', :year => 'Choose year', :hour => 'Choose hour', :minute => 'Choose minute'})
+ assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), start_year: 2003, end_year: 2005, prefix: "date[first]",
+ prompt: { day: "Choose day", month: "Choose month", year: "Choose year", hour: "Choose hour", minute: "Choose minute" })
+ end
+
+ def test_select_datetime_with_generic_with_css_classes
+ expected = +%(<select id="date_year" name="date[year]" class="year">\n)
+ expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_month" name="date[month]" class="month">\n)
+ expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_day" name="date[day]" class="day">\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ expected << " &mdash; "
+
+ expected << %(<select id="date_hour" name="date[hour]" class="hour">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
+ expected << "</select>\n"
+
+ expected << " : "
+
+ expected << %(<select id="date_minute" name="date[minute]" class="minute">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), start_year: 2003, end_year: 2005, with_css_classes: true)
+ end
+
+ def test_select_datetime_with_custom_with_css_classes
+ expected = +%(<select id="date_year" name="date[year]" class="my-year">\n)
+ expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_month" name="date[month]" class="my-month">\n)
+ expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_day" name="date[day]" class="my-day">\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ expected << " &mdash; "
+
+ expected << %(<select id="date_hour" name="date[hour]" class="my-hour">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
+ expected << "</select>\n"
+
+ expected << " : "
+
+ expected << %(<select id="date_minute" name="date[minute]" class="my-minute">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), start_year: 2003, end_year: 2005, with_css_classes: { day: "my-day", month: "my-month", year: "my-year", hour: "my-hour", minute: "my-minute" })
end
def test_select_datetime_with_custom_hours
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ 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"
@@ -1317,24 +1543,24 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="">Choose minute</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :start_year => 2003, :end_year => 2005, :start_hour => 1, :end_hour => 9, :prefix => "date[first]",
- :prompt => {:day => 'Choose day', :month => 'Choose month', :year => 'Choose year', :hour => 'Choose hour', :minute => 'Choose minute'})
+ assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), start_year: 2003, end_year: 2005, start_hour: 1, end_hour: 9, prefix: "date[first]",
+ prompt: { day: "Choose day", month: "Choose month", year: "Choose year", hour: "Choose hour", minute: "Choose minute" })
end
def test_select_datetime_with_hidden
- expected = %(<input id="date_first_year" name="date[first][year]" type="hidden" value="2003" />\n)
+ expected = +%(<input id="date_first_year" name="date[first][year]" type="hidden" value="2003" />\n)
expected << %(<input id="date_first_month" name="date[first][month]" type="hidden" value="8" />\n)
expected << %(<input id="date_first_day" name="date[first][day]" type="hidden" value="16" />\n)
expected << %(<input id="date_first_hour" name="date[first][hour]" type="hidden" value="8" />\n)
expected << %(<input id="date_first_minute" name="date[first][minute]" type="hidden" value="4" />\n)
- assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :prefix => "date[first]", :use_hidden => true)
- assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :datetime_separator => "&mdash;", :date_separator => "/",
- :time_separator => ":", :prefix => "date[first]", :use_hidden => true)
+ assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), prefix: "date[first]", use_hidden: true)
+ assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), datetime_separator: "&mdash;", date_separator: "/",
+ time_separator: ":", prefix: "date[first]", use_hidden: true)
end
def test_select_time
- expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
+ expected = +%(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n)
expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n)
@@ -1349,11 +1575,11 @@ class DateHelperTest < ActionView::TestCase
expected << "</select>\n"
assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18))
- assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :include_seconds => false)
+ assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), include_seconds: false)
end
def test_select_time_with_ampm
- expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
+ expected = +%(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n)
expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n)
@@ -1367,11 +1593,11 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :include_seconds => false, :ampm => true)
+ assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), include_seconds: false, ampm: true)
end
def test_select_time_with_separator
- expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
+ expected = +%(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n)
expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n)
expected << %(<select id="date_hour" name="date[hour]">\n)
@@ -1384,12 +1610,12 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :time_separator => ' : ')
- assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :time_separator => ' : ', :include_seconds => false)
+ assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), time_separator: " : ")
+ assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), time_separator: " : ", include_seconds: false)
end
def test_select_time_with_seconds
- expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
+ expected = +%(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n)
expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n)
@@ -1397,23 +1623,23 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
expected << "</select>\n"
- expected << ' : '
+ expected << " : "
expected << %(<select id="date_minute" name="date[minute]">\n)
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
- expected << ' : '
+ expected << " : "
expected << %(<select id="date_second" name="date[second]">\n)
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :include_seconds => true)
+ assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), include_seconds: true)
end
def test_select_time_with_seconds_and_separator
- expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
+ expected = +%(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n)
expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n)
@@ -1433,11 +1659,11 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :include_seconds => true, :time_separator => ' : ')
+ assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), include_seconds: true, time_separator: " : ")
end
def test_select_time_with_html_options
- expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
+ expected = +%(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n)
expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n)
@@ -1451,8 +1677,8 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), {}, :class => 'selector')
- assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), {:include_seconds => false}, :class => 'selector')
+ assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), {}, { class: "selector" })
+ assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), { include_seconds: false }, { class: "selector" })
end
def test_select_time_should_work_with_date
@@ -1460,7 +1686,7 @@ class DateHelperTest < ActionView::TestCase
end
def test_select_time_with_default_prompt
- expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
+ expected = +%(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n)
expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n)
@@ -1480,11 +1706,11 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="">Seconds</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :include_seconds => true, :prompt => true)
+ assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), include_seconds: true, prompt: true)
end
def test_select_time_with_custom_prompt
- expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
+ expected = +%(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n)
expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n)
@@ -1504,26 +1730,74 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="">Choose seconds</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :include_seconds => true,
- :prompt => {:hour => 'Choose hour', :minute => 'Choose minute', :second => 'Choose seconds'})
+ assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), include_seconds: true,
+ prompt: { hour: "Choose hour", minute: "Choose minute", second: "Choose seconds" })
+ end
+
+ def test_select_time_with_generic_with_css_classes
+ expected = +%(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
+ expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n)
+ expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n)
+
+ expected << %(<select id="date_hour" name="date[hour]" class="hour">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
+ expected << "</select>\n"
+
+ expected << " : "
+
+ expected << %(<select id="date_minute" name="date[minute]" class="minute">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ expected << " : "
+
+ expected << %(<select id="date_second" name="date[second]" class="second">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), include_seconds: true, with_css_classes: true)
+ end
+
+ def test_select_time_with_custom_with_css_classes
+ expected = +%(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
+ expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n)
+ expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n)
+
+ expected << %(<select id="date_hour" name="date[hour]" class="my-hour">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
+ expected << "</select>\n"
+
+ expected << " : "
+
+ expected << %(<select id="date_minute" name="date[minute]" class="my-minute">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ expected << " : "
+
+ expected << %(<select id="date_second" name="date[second]" class="my-second">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), include_seconds: true, with_css_classes: { hour: "my-hour", minute: "my-minute", second: "my-second" })
end
def test_select_time_with_hidden
- expected = %(<input id="date_first_year" name="date[first][year]" type="hidden" value="2003" />\n)
+ expected = +%(<input id="date_first_year" name="date[first][year]" type="hidden" value="2003" />\n)
expected << %(<input id="date_first_month" name="date[first][month]" type="hidden" value="8" />\n)
expected << %(<input id="date_first_day" name="date[first][day]" type="hidden" value="16" />\n)
expected << %(<input id="date_first_hour" name="date[first][hour]" type="hidden" value="8" />\n)
expected << %(<input id="date_first_minute" name="date[first][minute]" type="hidden" value="4" />\n)
- assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :prefix => "date[first]", :use_hidden => true)
- assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :time_separator => ":", :prefix => "date[first]", :use_hidden => true)
+ assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), prefix: "date[first]", use_hidden: true)
+ assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), time_separator: ":", prefix: "date[first]", use_hidden: true)
end
def test_date_select
@post = Post.new
@post.written_on = Date.new(2004, 6, 15)
- expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
+ expected = +%{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
expected << "</select>\n"
@@ -1543,7 +1817,7 @@ class DateHelperTest < ActionView::TestCase
@post = Post.new
@post.written_on = Date.new(2004, 6, 15)
- expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
+ expected = +%{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
expected << "</select>\n"
@@ -1556,7 +1830,27 @@ class DateHelperTest < ActionView::TestCase
expected << "</select>\n"
- assert_dom_equal expected, date_select("post", "written_on", :selected => Date.new(2004, 07, 10))
+ assert_dom_equal expected, date_select("post", "written_on", selected: Date.new(2004, 07, 10))
+ end
+
+ def test_date_select_with_selected_in_hash
+ @post = Post.new
+ @post.written_on = Date.new(2004, 6, 15)
+
+ expected = +%{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
+ expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n}
+ expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7" selected="selected">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n}
+ expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10" selected="selected">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
+
+ expected << "</select>\n"
+
+ assert_dom_equal expected, date_select("post", "written_on", selected: { day: 10, month: 07, year: 2004 })
end
def test_date_select_with_selected_nil
@@ -1581,9 +1875,9 @@ class DateHelperTest < ActionView::TestCase
@post = Post.new
@post.written_on = Date.new(2004, 6, 15)
- expected = "<input type=\"hidden\" id=\"post_written_on_3i\" name=\"post[written_on(3i)]\" value=\"1\" />\n"
+ expected = +"<input type=\"hidden\" id=\"post_written_on_3i\" name=\"post[written_on(3i)]\" value=\"1\" />\n"
- expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n}
+ expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n}
expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
expected << "</select>\n"
@@ -1591,30 +1885,30 @@ class DateHelperTest < ActionView::TestCase
expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
expected << "</select>\n"
- assert_dom_equal expected, date_select("post", "written_on", :order => [ :month, :year ])
+ assert_dom_equal expected, date_select("post", "written_on", order: [ :month, :year ])
end
def test_date_select_without_day_and_month
@post = Post.new
@post.written_on = Date.new(2004, 2, 29)
- expected = "<input type=\"hidden\" id=\"post_written_on_2i\" name=\"post[written_on(2i)]\" value=\"2\" />\n"
+ expected = +"<input type=\"hidden\" id=\"post_written_on_2i\" name=\"post[written_on(2i)]\" value=\"2\" />\n"
expected << "<input type=\"hidden\" id=\"post_written_on_3i\" name=\"post[written_on(3i)]\" value=\"1\" />\n"
expected << %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
expected << "</select>\n"
- assert_dom_equal expected, date_select("post", "written_on", :order => [ :year ])
+ assert_dom_equal expected, date_select("post", "written_on", order: [ :year ])
end
def test_date_select_without_day_with_separator
@post = Post.new
@post.written_on = Date.new(2004, 6, 15)
- expected = "<input type=\"hidden\" id=\"post_written_on_3i\" name=\"post[written_on(3i)]\" value=\"1\" />\n"
+ expected = +"<input type=\"hidden\" id=\"post_written_on_3i\" name=\"post[written_on(3i)]\" value=\"1\" />\n"
- expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n}
+ expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n}
expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
expected << "</select>\n"
@@ -1624,16 +1918,16 @@ class DateHelperTest < ActionView::TestCase
expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
expected << "</select>\n"
- assert_dom_equal expected, date_select("post", "written_on", :date_separator => '/', :order => [ :month, :year ])
+ assert_dom_equal expected, date_select("post", "written_on", date_separator: "/", order: [ :month, :year ])
end
def test_date_select_without_day_and_with_disabled_html_option
@post = Post.new
@post.written_on = Date.new(2004, 6, 15)
- expected = "<input type=\"hidden\" id=\"post_written_on_3i\" disabled=\"disabled\" name=\"post[written_on(3i)]\" value=\"1\" />\n"
+ expected = +"<input type=\"hidden\" id=\"post_written_on_3i\" disabled=\"disabled\" name=\"post[written_on(3i)]\" value=\"1\" />\n"
- expected << %{<select id="post_written_on_2i" disabled="disabled" name="post[written_on(2i)]">\n}
+ expected << %{<select id="post_written_on_2i" disabled="disabled" name="post[written_on(2i)]">\n}
expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
expected << "</select>\n"
@@ -1641,7 +1935,7 @@ class DateHelperTest < ActionView::TestCase
expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
expected << "</select>\n"
- assert_dom_equal expected, date_select("post", "written_on", { :order => [ :month, :year ] }, :disabled => true)
+ assert_dom_equal expected, date_select("post", "written_on", { order: [ :month, :year ] }, { disabled: true })
end
def test_date_select_within_fields_for
@@ -1652,7 +1946,7 @@ class DateHelperTest < ActionView::TestCase
concat f.date_select(:written_on)
end
- expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n</select>\n}
+ expected = +%{<select id="post_written_on_1i" name="post[written_on(1i)]">\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n</select>\n}
expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option selected="selected" value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n</select>\n}
expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option selected="selected" value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n</select>\n}
@@ -1664,11 +1958,11 @@ class DateHelperTest < ActionView::TestCase
@post.written_on = Date.new(2004, 6, 15)
id = 27
- output_buffer = fields_for :post, @post, :index => id do |f|
+ output_buffer = fields_for :post, @post, index: id do |f|
concat f.date_select(:written_on)
end
- expected = %{<select id="post_#{id}_written_on_1i" name="post[#{id}][written_on(1i)]">\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n</select>\n}
+ expected = +%{<select id="post_#{id}_written_on_1i" name="post[#{id}][written_on(1i)]">\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n</select>\n}
expected << %{<select id="post_#{id}_written_on_2i" name="post[#{id}][written_on(2i)]">\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option selected="selected" value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n</select>\n}
expected << %{<select id="post_#{id}_written_on_3i" name="post[#{id}][written_on(3i)]">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option selected="selected" value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n</select>\n}
@@ -1680,12 +1974,11 @@ class DateHelperTest < ActionView::TestCase
@post.written_on = Date.new(2004, 6, 15)
id = nil
- output_buffer = fields_for :post, @post, :index => id do |f|
+ output_buffer = fields_for :post, @post, index: id do |f|
concat f.date_select(:written_on)
end
-
- expected = %{<select id="post_#{id}_written_on_1i" name="post[#{id}][written_on(1i)]">\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n</select>\n}
+ expected = +%{<select id="post_#{id}_written_on_1i" name="post[#{id}][written_on(1i)]">\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n</select>\n}
expected << %{<select id="post_#{id}_written_on_2i" name="post[#{id}][written_on(2i)]">\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option selected="selected" value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n</select>\n}
expected << %{<select id="post_#{id}_written_on_3i" name="post[#{id}][written_on(3i)]">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option selected="selected" value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n</select>\n}
@@ -1697,7 +1990,7 @@ class DateHelperTest < ActionView::TestCase
@post.written_on = Date.new(2004, 6, 15)
id = 456
- expected = %{<select id="post_456_written_on_1i" name="post[#{id}][written_on(1i)]">\n}
+ expected = +%{<select id="post_456_written_on_1i" name="post[#{id}][written_on(1i)]">\n}
expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
expected << "</select>\n"
@@ -1709,7 +2002,7 @@ class DateHelperTest < ActionView::TestCase
expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
expected << "</select>\n"
- assert_dom_equal expected, date_select("post", "written_on", :index => id)
+ assert_dom_equal expected, date_select("post", "written_on", index: id)
end
def test_date_select_with_auto_index
@@ -1717,7 +2010,7 @@ class DateHelperTest < ActionView::TestCase
@post.written_on = Date.new(2004, 6, 15)
id = 123
- expected = %{<select id="post_123_written_on_1i" name="post[#{id}][written_on(1i)]">\n}
+ expected = +%{<select id="post_123_written_on_1i" name="post[#{id}][written_on(1i)]">\n}
expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
expected << "</select>\n"
@@ -1736,7 +2029,7 @@ class DateHelperTest < ActionView::TestCase
@post = Post.new
@post.written_on = Date.new(2004, 6, 15)
- expected = %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n}
+ expected = +%{<select id="post_written_on_3i" name="post[written_on(3i)]">\n}
1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 15}>#{i}</option>\n) }
expected << "</select>\n"
@@ -1744,19 +2037,19 @@ class DateHelperTest < ActionView::TestCase
1.upto(12) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 6}>#{Date::MONTHNAMES[i]}</option>\n) }
expected << "</select>\n"
- expected << %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
+ expected << %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
1999.upto(2009) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2004}>#{i}</option>\n) }
expected << "</select>\n"
- assert_dom_equal expected, date_select("post", "written_on", :order => [:day, :month, :year])
+ assert_dom_equal expected, date_select("post", "written_on", order: [:day, :month, :year])
end
def test_date_select_with_nil
@post = Post.new
- start_year = Time.now.year-5
- end_year = Time.now.year+5
- expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
+ start_year = Time.now.year - 5
+ end_year = Time.now.year + 5
+ expected = +%{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
start_year.upto(end_year) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == Time.now.year}>#{i}</option>\n) }
expected << "</select>\n"
@@ -1774,9 +2067,9 @@ class DateHelperTest < ActionView::TestCase
def test_date_select_with_nil_and_blank
@post = Post.new
- start_year = Time.now.year-5
- end_year = Time.now.year+5
- expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
+ start_year = Time.now.year - 5
+ end_year = Time.now.year + 5
+ expected = +%{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
expected << "<option value=\"\"></option>\n"
start_year.upto(end_year) { |i| expected << %(<option value="#{i}">#{i}</option>\n) }
expected << "</select>\n"
@@ -1791,17 +2084,17 @@ class DateHelperTest < ActionView::TestCase
1.upto(31) { |i| expected << %(<option value="#{i}">#{i}</option>\n) }
expected << "</select>\n"
- assert_dom_equal expected, date_select("post", "written_on", :include_blank => true)
+ assert_dom_equal expected, date_select("post", "written_on", include_blank: true)
end
def test_date_select_with_nil_and_blank_and_order
@post = Post.new
- start_year = Time.now.year-5
- end_year = Time.now.year+5
+ start_year = Time.now.year - 5
+ end_year = Time.now.year + 5
expected = '<input name="post[written_on(3i)]" type="hidden" id="post_written_on_3i" value="1"/>' + "\n"
- expected << %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
+ expected << %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
expected << "<option value=\"\"></option>\n"
start_year.upto(end_year) { |i| expected << %(<option value="#{i}">#{i}</option>\n) }
expected << "</select>\n"
@@ -1811,23 +2104,23 @@ class DateHelperTest < ActionView::TestCase
1.upto(12) { |i| expected << %(<option value="#{i}">#{Date::MONTHNAMES[i]}</option>\n) }
expected << "</select>\n"
- assert_dom_equal expected, date_select("post", "written_on", :order=>[:year, :month], :include_blank=>true)
+ assert_dom_equal expected, date_select("post", "written_on", order: [:year, :month], include_blank: true)
end
def test_date_select_with_nil_and_blank_and_discard_month
@post = Post.new
- start_year = Time.now.year-5
- end_year = Time.now.year+5
+ start_year = Time.now.year - 5
+ end_year = Time.now.year + 5
- expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
+ expected = +%{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
expected << "<option value=\"\"></option>\n"
start_year.upto(end_year) { |i| expected << %(<option value="#{i}">#{i}</option>\n) }
expected << "</select>\n"
expected << '<input name="post[written_on(2i)]" type="hidden" id="post_written_on_2i" value="1"/>' + "\n"
expected << '<input name="post[written_on(3i)]" type="hidden" id="post_written_on_3i" value="1"/>' + "\n"
- assert_dom_equal expected, date_select("post", "written_on", :discard_month => true, :include_blank=>true)
+ assert_dom_equal expected, date_select("post", "written_on", discard_month: true, include_blank: true)
end
def test_date_select_with_nil_and_blank_and_discard_year
@@ -1845,14 +2138,14 @@ class DateHelperTest < ActionView::TestCase
1.upto(31) { |i| expected << %(<option value="#{i}">#{i}</option>\n) }
expected << "</select>\n"
- assert_dom_equal expected, date_select("post", "written_on", :discard_year => true, :include_blank=>true)
+ assert_dom_equal expected, date_select("post", "written_on", discard_year: true, include_blank: true)
end
def test_date_select_cant_override_discard_hour
@post = Post.new
@post.written_on = Date.new(2004, 6, 15)
- expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
+ expected = +%{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
expected << "</select>\n"
@@ -1864,14 +2157,14 @@ class DateHelperTest < ActionView::TestCase
expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
expected << "</select>\n"
- assert_dom_equal expected, date_select("post", "written_on", :discard_hour => false)
+ assert_dom_equal expected, date_select("post", "written_on", discard_hour: false)
end
def test_date_select_with_html_options
@post = Post.new
@post.written_on = Date.new(2004, 6, 15)
- expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]" class="selector">\n}
+ expected = +%{<select id="post_written_on_1i" name="post[written_on(1i)]" class="selector">\n}
expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
expected << "</select>\n"
@@ -1884,7 +2177,7 @@ class DateHelperTest < ActionView::TestCase
expected << "</select>\n"
- assert_dom_equal expected, date_select("post", "written_on", {}, :class => 'selector')
+ assert_dom_equal expected, date_select("post", "written_on", {}, { class: "selector" })
end
def test_date_select_with_html_options_within_fields_for
@@ -1892,10 +2185,10 @@ class DateHelperTest < ActionView::TestCase
@post.written_on = Date.new(2004, 6, 15)
output_buffer = fields_for :post, @post do |f|
- concat f.date_select(:written_on, {}, :class => 'selector')
+ concat f.date_select(:written_on, {}, { class: "selector" })
end
- expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]" class="selector">\n}
+ expected = +%{<select id="post_written_on_1i" name="post[written_on(1i)]" class="selector">\n}
expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
expected << "</select>\n"
@@ -1915,7 +2208,7 @@ class DateHelperTest < ActionView::TestCase
@post = Post.new
@post.written_on = Date.new(2004, 6, 15)
- expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
+ expected = +%{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
expected << "</select>\n"
@@ -1932,14 +2225,14 @@ class DateHelperTest < ActionView::TestCase
expected << "</select>\n"
- assert_dom_equal expected, date_select("post", "written_on", { :date_separator => " / " })
+ assert_dom_equal expected, date_select("post", "written_on", date_separator: " / ")
end
def test_date_select_with_separator_and_order
@post = Post.new
@post.written_on = Date.new(2004, 6, 15)
- expected = %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n}
+ expected = +%{<select id="post_written_on_3i" name="post[written_on(3i)]">\n}
expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
expected << "</select>\n"
@@ -1955,14 +2248,14 @@ class DateHelperTest < ActionView::TestCase
expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
expected << "</select>\n"
- assert_dom_equal expected, date_select("post", "written_on", { :order => [:day, :month, :year], :date_separator => " / " })
+ assert_dom_equal expected, date_select("post", "written_on", order: [:day, :month, :year], date_separator: " / ")
end
def test_date_select_with_separator_and_order_and_year_discarded
@post = Post.new
@post.written_on = Date.new(2004, 6, 15)
- expected = %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n}
+ expected = +%{<select id="post_written_on_3i" name="post[written_on(3i)]">\n}
expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
expected << "</select>\n"
@@ -1973,14 +2266,14 @@ class DateHelperTest < ActionView::TestCase
expected << "</select>\n"
expected << %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
- assert_dom_equal expected, date_select("post", "written_on", { :order => [:day, :month, :year], :discard_year => true, :date_separator => " / " })
+ assert_dom_equal expected, date_select("post", "written_on", order: [:day, :month, :year], discard_year: true, date_separator: " / ")
end
def test_date_select_with_default_prompt
@post = Post.new
@post.written_on = Date.new(2004, 6, 15)
- expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
+ expected = +%{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
expected << %{<option value="">Year</option>\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
expected << "</select>\n"
@@ -1993,14 +2286,14 @@ class DateHelperTest < ActionView::TestCase
expected << "</select>\n"
- assert_dom_equal expected, date_select("post", "written_on", :prompt => true)
+ assert_dom_equal expected, date_select("post", "written_on", prompt: true)
end
def test_date_select_with_custom_prompt
@post = Post.new
@post.written_on = Date.new(2004, 6, 15)
- expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
+ expected = +%{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
expected << %{<option value="">Choose year</option>\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
expected << "</select>\n"
@@ -2013,14 +2306,54 @@ class DateHelperTest < ActionView::TestCase
expected << "</select>\n"
- assert_dom_equal expected, date_select("post", "written_on", :prompt => {:year => 'Choose year', :month => 'Choose month', :day => 'Choose day'})
+ assert_dom_equal expected, date_select("post", "written_on", prompt: { year: "Choose year", month: "Choose month", day: "Choose day" })
+ end
+
+ def test_date_select_with_generic_with_css_classes
+ @post = Post.new
+ @post.written_on = Date.new(2004, 6, 15)
+
+ expected = +%{<select id="post_written_on_1i" name="post[written_on(1i)]" class="year">\n}
+ expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]" class="month">\n}
+ expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]" class="day">\n}
+ expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
+
+ expected << "</select>\n"
+
+ assert_dom_equal expected, date_select("post", "written_on", with_css_classes: true)
+ end
+
+ def test_date_select_with_custom_with_css_classes
+ @post = Post.new
+ @post.written_on = Date.new(2004, 6, 15)
+
+ expected = +%{<select id="post_written_on_1i" name="post[written_on(1i)]" class="my-year">\n}
+ expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]" class="my-month">\n}
+ expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]" class="my-day">\n}
+ expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
+
+ expected << "</select>\n"
+
+ assert_dom_equal expected, date_select("post", "written_on", with_css_classes: { year: "my-year", month: "my-month", day: "my-day" })
end
def test_time_select
@post = Post.new
@post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
- expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
+ expected = +%{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n}
expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n}
@@ -2039,7 +2372,7 @@ class DateHelperTest < ActionView::TestCase
@post = Post.new
@post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
- expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
+ expected = +%{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n}
expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n}
@@ -2058,7 +2391,7 @@ class DateHelperTest < ActionView::TestCase
@post = Post.new
@post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
- expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="1" />\n}
+ expected = +%{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="1" />\n}
expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="1" />\n}
expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="1" />\n}
@@ -2077,7 +2410,7 @@ class DateHelperTest < ActionView::TestCase
@post = Post.new
@post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
- expected = %(<select id="post_written_on_4i" name="post[written_on(4i)]">\n)
+ expected = +%(<select id="post_written_on_4i" name="post[written_on(4i)]">\n)
0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
expected << " : "
@@ -2085,14 +2418,14 @@ class DateHelperTest < ActionView::TestCase
0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
- assert_dom_equal expected, time_select("post", "written_on", :ignore_date => true)
+ assert_dom_equal expected, time_select("post", "written_on", ignore_date: true)
end
def test_time_select_with_seconds
@post = Post.new
@post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
- expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
+ expected = +%{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n}
expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n}
@@ -2108,14 +2441,14 @@ class DateHelperTest < ActionView::TestCase
0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 35}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
- assert_dom_equal expected, time_select("post", "written_on", :include_seconds => true)
+ assert_dom_equal expected, time_select("post", "written_on", include_seconds: true)
end
def test_time_select_with_html_options
@post = Post.new
@post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
- expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
+ expected = +%{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n}
expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n}
@@ -2127,7 +2460,7 @@ class DateHelperTest < ActionView::TestCase
0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
- assert_dom_equal expected, time_select("post", "written_on", {}, :class => 'selector')
+ assert_dom_equal expected, time_select("post", "written_on", {}, { class: "selector" })
end
def test_time_select_with_html_options_within_fields_for
@@ -2135,10 +2468,10 @@ class DateHelperTest < ActionView::TestCase
@post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
output_buffer = fields_for :post, @post do |f|
- concat f.time_select(:written_on, {}, :class => 'selector')
+ concat f.time_select(:written_on, {}, { class: "selector" })
end
- expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
+ expected = +%{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n}
expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n}
@@ -2157,7 +2490,7 @@ class DateHelperTest < ActionView::TestCase
@post = Post.new
@post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
- expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
+ expected = +%{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n}
expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n}
@@ -2177,14 +2510,14 @@ class DateHelperTest < ActionView::TestCase
0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 35}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
- assert_dom_equal expected, time_select("post", "written_on", { :time_separator => " - ", :include_seconds => true })
+ assert_dom_equal expected, time_select("post", "written_on", time_separator: " - ", include_seconds: true)
end
def test_time_select_with_default_prompt
@post = Post.new
@post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
- expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
+ expected = +%{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n}
expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n}
@@ -2194,18 +2527,18 @@ class DateHelperTest < ActionView::TestCase
expected << "</select>\n"
expected << " : "
expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]">\n)
- expected << %(<option value="">Minute</option>\n)
+ expected << %(<option value="">Minute</option>\n)
0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
- assert_dom_equal expected, time_select("post", "written_on", :prompt => true)
+ assert_dom_equal expected, time_select("post", "written_on", prompt: true)
end
def test_time_select_with_custom_prompt
@post = Post.new
@post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
- expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
+ expected = +%{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n}
expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n}
@@ -2215,18 +2548,60 @@ class DateHelperTest < ActionView::TestCase
expected << "</select>\n"
expected << " : "
expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]">\n)
- expected << %(<option value="">Choose minute</option>\n)
+ expected << %(<option value="">Choose minute</option>\n)
0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
- assert_dom_equal expected, time_select("post", "written_on", :prompt => {:hour => 'Choose hour', :minute => 'Choose minute'})
+ assert_dom_equal expected, time_select("post", "written_on", prompt: { hour: "Choose hour", minute: "Choose minute" })
+ end
+
+ def test_time_select_with_generic_with_css_classes
+ @post = Post.new
+ @post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
+
+ expected = +%{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
+ expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n}
+ expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n}
+
+ expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]" class="hour">\n)
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+
+ expected << " : "
+
+ expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]" class="minute">\n)
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+
+ assert_dom_equal expected, time_select("post", "written_on", with_css_classes: true)
+ end
+
+ def test_time_select_with_custom_with_css_classes
+ @post = Post.new
+ @post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
+
+ expected = +%{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
+ expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n}
+ expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n}
+
+ expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]" class="my-hour">\n)
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+
+ expected << " : "
+
+ expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]" class="my-minute">\n)
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+
+ assert_dom_equal expected, time_select("post", "written_on", with_css_classes: { hour: "my-hour", minute: "my-minute" })
end
def test_time_select_with_disabled_html_option
@post = Post.new
@post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
- expected = %{<input type="hidden" id="post_written_on_1i" disabled="disabled" name="post[written_on(1i)]" value="2004" />\n}
+ expected = +%{<input type="hidden" id="post_written_on_1i" disabled="disabled" name="post[written_on(1i)]" value="2004" />\n}
expected << %{<input type="hidden" id="post_written_on_2i" disabled="disabled" name="post[written_on(2i)]" value="6" />\n}
expected << %{<input type="hidden" id="post_written_on_3i" disabled="disabled" name="post[written_on(3i)]" value="15" />\n}
@@ -2238,14 +2613,14 @@ class DateHelperTest < ActionView::TestCase
0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
- assert_dom_equal expected, time_select("post", "written_on", {}, :disabled => true)
+ assert_dom_equal expected, time_select("post", "written_on", {}, { disabled: true })
end
def test_datetime_select
@post = Post.new
@post.updated_at = Time.local(2004, 6, 15, 16, 35)
- expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
+ expected = +%{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
expected << "</select>\n"
@@ -2274,7 +2649,7 @@ class DateHelperTest < ActionView::TestCase
@post = Post.new
@post.updated_at = Time.local(2004, 6, 15, 16, 35)
- expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
+ expected = +%{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
expected << "</select>\n"
@@ -2296,7 +2671,7 @@ class DateHelperTest < ActionView::TestCase
expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30" selected="selected">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n}
expected << "</select>\n"
- assert_dom_equal expected, datetime_select("post", "updated_at", :selected => Time.local(2004, 3, 10, 12, 30))
+ assert_dom_equal expected, datetime_select("post", "updated_at", selected: Time.local(2004, 3, 10, 12, 30))
end
def test_datetime_select_with_selected_nil
@@ -2330,7 +2705,7 @@ class DateHelperTest < ActionView::TestCase
# The love zone is UTC+0
mytz = Class.new(ActiveSupport::TimeZone) {
attr_accessor :now
- }.create('tenderlove', 0, ActiveSupport::TimeZone.find_tzinfo('UTC'))
+ }.create("tenderlove", 0, ActiveSupport::TimeZone.find_tzinfo("UTC"))
now = Time.mktime(2004, 6, 15, 16, 35, 0)
mytz.now = now
@@ -2340,7 +2715,7 @@ class DateHelperTest < ActionView::TestCase
@post = Post.new
- expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
+ expected = +%{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
expected << "</select>\n"
@@ -2372,10 +2747,10 @@ class DateHelperTest < ActionView::TestCase
@post.updated_at = Time.local(2004, 6, 15, 16, 35)
output_buffer = fields_for :post, @post do |f|
- concat f.datetime_select(:updated_at, {}, :class => 'selector')
+ concat f.datetime_select(:updated_at, {}, { class: "selector" })
end
- expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]" class="selector">\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n</select>\n}
+ expected = +%{<select id="post_updated_at_1i" name="post[updated_at(1i)]" class="selector">\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n</select>\n}
expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]" class="selector">\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option selected="selected" value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n</select>\n}
expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]" class="selector">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option selected="selected" value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n</select>\n}
expected << %{ &mdash; <select id="post_updated_at_4i" name="post[updated_at(4i)]" class="selector">\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option selected="selected" value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n</select>\n}
@@ -2388,7 +2763,7 @@ class DateHelperTest < ActionView::TestCase
@post = Post.new
@post.updated_at = Time.local(2004, 6, 15, 15, 16, 35)
- expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
+ expected = +%{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
expected << "</select>\n"
@@ -2422,7 +2797,7 @@ class DateHelperTest < ActionView::TestCase
0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 35}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
- assert_dom_equal expected, datetime_select("post", "updated_at", { :date_separator => " / ", :datetime_separator => " , ", :time_separator => " - ", :include_seconds => true })
+ assert_dom_equal expected, datetime_select("post", "updated_at", date_separator: " / ", datetime_separator: " , ", time_separator: " - ", include_seconds: true)
end
def test_datetime_select_with_integer
@@ -2433,7 +2808,7 @@ class DateHelperTest < ActionView::TestCase
def test_datetime_select_with_infinity # Float
@post = Post.new
- @post.updated_at = (-1.0/0)
+ @post.updated_at = (-1.0 / 0)
datetime_select("post", "updated_at")
end
@@ -2441,7 +2816,7 @@ class DateHelperTest < ActionView::TestCase
@post = Post.new
@post.updated_at = nil
- expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
+ expected = +%{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
expected << %{<option value="">Year</option>\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
expected << "</select>\n"
@@ -2463,14 +2838,14 @@ class DateHelperTest < ActionView::TestCase
expected << %{<option value="">Minute</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n}
expected << "</select>\n"
- assert_dom_equal expected, datetime_select("post", "updated_at", :start_year=>1999, :end_year=>2009, :prompt => true)
+ assert_dom_equal expected, datetime_select("post", "updated_at", start_year: 1999, end_year: 2009, prompt: true)
end
def test_datetime_select_with_custom_prompt
@post = Post.new
@post.updated_at = nil
- expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
+ expected = +%{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
expected << %{<option value="">Choose year</option>\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
expected << "</select>\n"
@@ -2492,12 +2867,70 @@ class DateHelperTest < ActionView::TestCase
expected << %{<option value="">Choose minute</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n}
expected << "</select>\n"
- assert_dom_equal expected, datetime_select("post", "updated_at", :start_year=>1999, :end_year=>2009, :prompt => {:year => 'Choose year', :month => 'Choose month', :day => 'Choose day', :hour => 'Choose hour', :minute => 'Choose minute'})
+ assert_dom_equal expected, datetime_select("post", "updated_at", start_year: 1999, end_year: 2009, prompt: { year: "Choose year", month: "Choose month", day: "Choose day", hour: "Choose hour", minute: "Choose minute" })
+ end
+
+ def test_datetime_select_with_generic_with_css_classes
+ @post = Post.new
+ @post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
+
+ expected = +%{<select id="post_written_on_1i" name="post[written_on(1i)]" class="year">\n}
+ expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]" class="month">\n}
+ expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]" class="day">\n}
+ expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
+ expected << "</select>\n"
+
+ expected << " &mdash; "
+
+ expected << %{<select id="post_written_on_4i" name="post[written_on(4i)]" class="hour">\n}
+ expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n}
+ expected << "</select>\n"
+ expected << " : "
+ expected << %{<select id="post_written_on_5i" name="post[written_on(5i)]" class="minute">\n}
+ expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n}
+ expected << "</select>\n"
+
+ assert_dom_equal expected, datetime_select("post", "written_on", start_year: 1999, end_year: 2009, with_css_classes: true)
+ end
+
+ def test_datetime_select_with_custom_with_css_classes
+ @post = Post.new
+ @post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
+
+ expected = +%{<select id="post_written_on_1i" name="post[written_on(1i)]" class="my-year">\n}
+ expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]" class="my-month">\n}
+ expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]" class="my-day">\n}
+ expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
+ expected << "</select>\n"
+
+ expected << " &mdash; "
+
+ expected << %{<select id="post_written_on_4i" name="post[written_on(4i)]" class="my-hour">\n}
+ expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n}
+ expected << "</select>\n"
+ expected << " : "
+ expected << %{<select id="post_written_on_5i" name="post[written_on(5i)]" class="my-minute">\n}
+ expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n}
+ expected << "</select>\n"
+
+ assert_dom_equal expected, datetime_select("post", "written_on", start_year: 1999, end_year: 2009, with_css_classes: { year: "my-year", month: "my-month", day: "my-day", hour: "my-hour", minute: "my-minute" })
end
def test_date_select_with_zero_value_and_no_start_year
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
- (Date.today.year-5).upto(Date.today.year+1) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
+ expected = +%(<select id="date_first_year" name="date[first][year]">\n)
+ (Date.today.year - 5).upto(Date.today.year + 1) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
expected << "</select>\n"
expected << %(<select id="date_first_month" name="date[first][month]">\n)
@@ -2508,11 +2941,11 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_date(0, :end_year => Date.today.year+1, :prefix => "date[first]")
+ assert_dom_equal expected, select_date(0, end_year: Date.today.year + 1, prefix: "date[first]")
end
def test_date_select_with_zero_value_and_no_end_year
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ expected = +%(<select id="date_first_year" name="date[first][year]">\n)
last_year = Time.now.year + 5
2003.upto(last_year) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
expected << "</select>\n"
@@ -2525,12 +2958,12 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_date(0, :start_year => 2003, :prefix => "date[first]")
+ assert_dom_equal expected, select_date(0, start_year: 2003, prefix: "date[first]")
end
def test_date_select_with_zero_value_and_no_start_and_end_year
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
- (Date.today.year-5).upto(Date.today.year+5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
+ expected = +%(<select id="date_first_year" name="date[first][year]">\n)
+ (Date.today.year - 5).upto(Date.today.year + 5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
expected << "</select>\n"
expected << %(<select id="date_first_month" name="date[first][month]">\n)
@@ -2541,12 +2974,12 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_date(0, :prefix => "date[first]")
+ assert_dom_equal expected, select_date(0, prefix: "date[first]")
end
def test_date_select_with_nil_value_and_no_start_and_end_year
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
- (Date.today.year-5).upto(Date.today.year+5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
+ expected = +%(<select id="date_first_year" name="date[first][year]">\n)
+ (Date.today.year - 5).upto(Date.today.year + 5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
expected << "</select>\n"
expected << %(<select id="date_first_month" name="date[first][month]">\n)
@@ -2557,12 +2990,12 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_date(nil, :prefix => "date[first]")
+ assert_dom_equal expected, select_date(nil, prefix: "date[first]")
end
def test_datetime_select_with_nil_value_and_no_start_and_end_year
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
- (Date.today.year-5).upto(Date.today.year+5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
+ expected = +%(<select id="date_first_year" name="date[first][year]">\n)
+ (Date.today.year - 5).upto(Date.today.year + 5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
expected << "</select>\n"
expected << %(<select id="date_first_month" name="date[first][month]">\n)
@@ -2585,7 +3018,7 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_datetime(nil, :prefix => "date[first]")
+ assert_dom_equal expected, select_datetime(nil, prefix: "date[first]")
end
def test_datetime_select_with_options_index
@@ -2593,7 +3026,7 @@ class DateHelperTest < ActionView::TestCase
@post.updated_at = Time.local(2004, 6, 15, 16, 35)
id = 456
- expected = %{<select id="post_456_updated_at_1i" name="post[#{id}][updated_at(1i)]">\n}
+ expected = +%{<select id="post_456_updated_at_1i" name="post[#{id}][updated_at(1i)]">\n}
expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
expected << "</select>\n"
@@ -2615,7 +3048,7 @@ class DateHelperTest < ActionView::TestCase
expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35" selected="selected">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n}
expected << "</select>\n"
- assert_dom_equal expected, datetime_select("post", "updated_at", :index => id)
+ assert_dom_equal expected, datetime_select("post", "updated_at", index: id)
end
def test_datetime_select_within_fields_for_with_options_index
@@ -2623,11 +3056,11 @@ class DateHelperTest < ActionView::TestCase
@post.updated_at = Time.local(2004, 6, 15, 16, 35)
id = 456
- output_buffer = fields_for :post, @post, :index => id do |f|
+ output_buffer = fields_for :post, @post, index: id do |f|
concat f.datetime_select(:updated_at)
end
- expected = %{<select id="post_456_updated_at_1i" name="post[#{id}][updated_at(1i)]">\n}
+ expected = +%{<select id="post_456_updated_at_1i" name="post[#{id}][updated_at(1i)]">\n}
expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
expected << "</select>\n"
@@ -2657,7 +3090,7 @@ class DateHelperTest < ActionView::TestCase
@post.updated_at = Time.local(2004, 6, 15, 16, 35)
id = @post.id
- expected = %{<select id="post_123_updated_at_1i" name="post[#{id}][updated_at(1i)]">\n}
+ expected = +%{<select id="post_123_updated_at_1i" name="post[#{id}][updated_at(1i)]">\n}
expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
expected << "</select>\n"
@@ -2686,7 +3119,7 @@ class DateHelperTest < ActionView::TestCase
@post = Post.new
@post.updated_at = Time.local(2004, 6, 15, 15, 16, 35)
- expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
+ expected = +%{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
1999.upto(2009) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2004}>#{i}</option>\n) }
expected << "</select>\n"
expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
@@ -2710,14 +3143,14 @@ class DateHelperTest < ActionView::TestCase
0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 35}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
- assert_dom_equal expected, datetime_select("post", "updated_at", :include_seconds => true)
+ assert_dom_equal expected, datetime_select("post", "updated_at", include_seconds: true)
end
def test_datetime_select_discard_year
@post = Post.new
@post.updated_at = Time.local(2004, 6, 15, 15, 16, 35)
- expected = %{<input type="hidden" id="post_updated_at_1i" name="post[updated_at(1i)]" value="2004" />\n}
+ expected = +%{<input type="hidden" id="post_updated_at_1i" name="post[updated_at(1i)]" value="2004" />\n}
expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
1.upto(12) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 6}>#{Date::MONTHNAMES[i]}</option>\n) }
expected << "</select>\n"
@@ -2735,14 +3168,14 @@ class DateHelperTest < ActionView::TestCase
0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
- assert_dom_equal expected, datetime_select("post", "updated_at", :discard_year => true)
+ assert_dom_equal expected, datetime_select("post", "updated_at", discard_year: true)
end
def test_datetime_select_discard_month
@post = Post.new
@post.updated_at = Time.local(2004, 6, 15, 15, 16, 35)
- expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
+ expected = +%{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
1999.upto(2009) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2004}>#{i}</option>\n) }
expected << "</select>\n"
expected << %{<input type="hidden" id="post_updated_at_2i" name="post[updated_at(2i)]" value="6" />\n}
@@ -2758,14 +3191,14 @@ class DateHelperTest < ActionView::TestCase
0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
- assert_dom_equal expected, datetime_select("post", "updated_at", :discard_month => true)
+ assert_dom_equal expected, datetime_select("post", "updated_at", discard_month: true)
end
def test_datetime_select_discard_year_and_month
@post = Post.new
@post.updated_at = Time.local(2004, 6, 15, 15, 16, 35)
- expected = %{<input type="hidden" id="post_updated_at_1i" name="post[updated_at(1i)]" value="2004" />\n}
+ expected = +%{<input type="hidden" id="post_updated_at_1i" name="post[updated_at(1i)]" value="2004" />\n}
expected << %{<input type="hidden" id="post_updated_at_2i" name="post[updated_at(2i)]" value="6" />\n}
expected << %{<input type="hidden" id="post_updated_at_3i" name="post[updated_at(3i)]" value="1" />\n}
@@ -2777,14 +3210,14 @@ class DateHelperTest < ActionView::TestCase
0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
- assert_dom_equal expected, datetime_select("post", "updated_at", :discard_year => true, :discard_month => true)
+ assert_dom_equal expected, datetime_select("post", "updated_at", discard_year: true, discard_month: true)
end
def test_datetime_select_discard_year_and_month_with_disabled_html_option
@post = Post.new
@post.updated_at = Time.local(2004, 6, 15, 15, 16, 35)
- expected = %{<input type="hidden" id="post_updated_at_1i" disabled="disabled" name="post[updated_at(1i)]" value="2004" />\n}
+ expected = +%{<input type="hidden" id="post_updated_at_1i" disabled="disabled" name="post[updated_at(1i)]" value="2004" />\n}
expected << %{<input type="hidden" id="post_updated_at_2i" disabled="disabled" name="post[updated_at(2i)]" value="6" />\n}
expected << %{<input type="hidden" id="post_updated_at_3i" disabled="disabled" name="post[updated_at(3i)]" value="1" />\n}
@@ -2796,14 +3229,14 @@ class DateHelperTest < ActionView::TestCase
0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
- assert_dom_equal expected, datetime_select("post", "updated_at", { :discard_year => true, :discard_month => true }, :disabled => true)
+ assert_dom_equal expected, datetime_select("post", "updated_at", { discard_year: true, discard_month: true }, { disabled: true })
end
def test_datetime_select_discard_hour
@post = Post.new
@post.updated_at = Time.local(2004, 6, 15, 15, 16, 35)
- expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
+ expected = +%{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
1999.upto(2009) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2004}>#{i}</option>\n) }
expected << "</select>\n"
expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
@@ -2813,14 +3246,14 @@ class DateHelperTest < ActionView::TestCase
1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 15}>#{i}</option>\n) }
expected << "</select>\n"
- assert_dom_equal expected, datetime_select("post", "updated_at", :discard_hour => true)
+ assert_dom_equal expected, datetime_select("post", "updated_at", discard_hour: true)
end
def test_datetime_select_discard_minute
@post = Post.new
@post.updated_at = Time.local(2004, 6, 15, 15, 16, 35)
- expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
+ expected = +%{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
1999.upto(2009) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2004}>#{i}</option>\n) }
expected << "</select>\n"
expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
@@ -2837,14 +3270,14 @@ class DateHelperTest < ActionView::TestCase
expected << "</select>\n"
expected << %{<input type="hidden" id="post_updated_at_5i" name="post[updated_at(5i)]" value="16" />\n}
- assert_dom_equal expected, datetime_select("post", "updated_at", :discard_minute => true)
+ assert_dom_equal expected, datetime_select("post", "updated_at", discard_minute: true)
end
def test_datetime_select_disabled_and_discard_minute
@post = Post.new
@post.updated_at = Time.local(2004, 6, 15, 15, 16, 35)
- expected = %{<select id="post_updated_at_1i" disabled="disabled" name="post[updated_at(1i)]">\n}
+ expected = +%{<select id="post_updated_at_1i" disabled="disabled" name="post[updated_at(1i)]">\n}
1999.upto(2009) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2004}>#{i}</option>\n) }
expected << "</select>\n"
expected << %{<select id="post_updated_at_2i" disabled="disabled" name="post[updated_at(2i)]">\n}
@@ -2861,14 +3294,14 @@ class DateHelperTest < ActionView::TestCase
expected << "</select>\n"
expected << %{<input type="hidden" id="post_updated_at_5i" disabled="disabled" name="post[updated_at(5i)]" value="16" />\n}
- assert_dom_equal expected, datetime_select("post", "updated_at", :discard_minute => true, :disabled => true)
+ assert_dom_equal expected, datetime_select("post", "updated_at", discard_minute: true, disabled: true)
end
def test_datetime_select_invalid_order
@post = Post.new
@post.updated_at = Time.local(2004, 6, 15, 15, 16, 35)
- expected = %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n}
+ expected = +%{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n}
1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 15}>#{i}</option>\n) }
expected << "</select>\n"
expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
@@ -2888,14 +3321,14 @@ class DateHelperTest < ActionView::TestCase
0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
- assert_dom_equal expected, datetime_select("post", "updated_at", :order => [:minute, :day, :hour, :month, :year, :second])
+ assert_dom_equal expected, datetime_select("post", "updated_at", order: [:minute, :day, :hour, :month, :year, :second])
end
def test_datetime_select_discard_with_order
@post = Post.new
@post.updated_at = Time.local(2004, 6, 15, 15, 16, 35)
- expected = %{<input type="hidden" id="post_updated_at_1i" name="post[updated_at(1i)]" value="2004" />\n}
+ expected = +%{<input type="hidden" id="post_updated_at_1i" name="post[updated_at(1i)]" value="2004" />\n}
expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n}
1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 15}>#{i}</option>\n) }
expected << "</select>\n"
@@ -2913,14 +3346,14 @@ class DateHelperTest < ActionView::TestCase
0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
- assert_dom_equal expected, datetime_select("post", "updated_at", :order => [:day, :month])
+ assert_dom_equal expected, datetime_select("post", "updated_at", order: [:day, :month])
end
def test_datetime_select_with_default_value_as_time
@post = Post.new
@post.updated_at = nil
- expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
+ expected = +%{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
2001.upto(2011) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2006}>#{i}</option>\n) }
expected << "</select>\n"
expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
@@ -2940,14 +3373,14 @@ class DateHelperTest < ActionView::TestCase
0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
- assert_dom_equal expected, datetime_select("post", "updated_at", :default => Time.local(2006, 9, 19, 15, 16, 35))
+ assert_dom_equal expected, datetime_select("post", "updated_at", default: Time.local(2006, 9, 19, 15, 16, 35))
end
def test_include_blank_overrides_default_option
@post = Post.new
@post.updated_at = nil
- expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
+ expected = +%{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
expected << %(<option value=""></option>\n)
(Time.now.year - 5).upto(Time.now.year + 5) { |i| expected << %(<option value="#{i}">#{i}</option>\n) }
expected << "</select>\n"
@@ -2960,14 +3393,14 @@ class DateHelperTest < ActionView::TestCase
1.upto(31) { |i| expected << %(<option value="#{i}">#{i}</option>\n) }
expected << "</select>\n"
- assert_dom_equal expected, date_select("post", "updated_at", :default => Time.local(2006, 9, 19, 15, 16, 35), :include_blank => true)
+ assert_dom_equal expected, date_select("post", "updated_at", default: Time.local(2006, 9, 19, 15, 16, 35), include_blank: true)
end
def test_datetime_select_with_default_value_as_hash
@post = Post.new
@post.updated_at = nil
- expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
+ expected = +%{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
(Time.now.year - 5).upto(Time.now.year + 5) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == Time.now.year}>#{i}</option>\n) }
expected << "</select>\n"
expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
@@ -2987,14 +3420,14 @@ class DateHelperTest < ActionView::TestCase
0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 42}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
- assert_dom_equal expected, datetime_select("post", "updated_at", :default => { :month => 10, :minute => 42, :hour => 9 })
+ assert_dom_equal expected, datetime_select("post", "updated_at", default: { month: 10, minute: 42, hour: 9 })
end
def test_datetime_select_with_html_options
@post = Post.new
@post.updated_at = Time.local(2004, 6, 15, 16, 35)
- expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]" class="selector">\n}
+ expected = +%{<select id="post_updated_at_1i" name="post[updated_at(1i)]" class="selector">\n}
expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
expected << "</select>\n"
@@ -3016,7 +3449,7 @@ class DateHelperTest < ActionView::TestCase
expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35" selected="selected">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n}
expected << "</select>\n"
- assert_dom_equal expected, datetime_select("post", "updated_at", {}, :class => 'selector')
+ assert_dom_equal expected, datetime_select("post", "updated_at", {}, { class: "selector" })
end
def test_date_select_should_not_change_passed_options_hash
@@ -3024,24 +3457,24 @@ class DateHelperTest < ActionView::TestCase
@post.updated_at = Time.local(2008, 7, 16, 23, 30)
options = {
- :order => [ :year, :month, :day ],
- :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
- :discard_type => false,
- :include_blank => false,
- :ignore_date => false,
- :include_seconds => true
+ order: [ :year, :month, :day ],
+ default: { year: 2008, month: 7, day: 16, hour: 23, minute: 30, second: 1 },
+ discard_type: false,
+ include_blank: false,
+ ignore_date: false,
+ include_seconds: true
}
date_select(@post, :updated_at, options)
# note: the literal hash is intentional to show that the actual options hash isn't modified
# don't change this!
assert_equal({
- :order => [ :year, :month, :day ],
- :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
- :discard_type => false,
- :include_blank => false,
- :ignore_date => false,
- :include_seconds => true
+ order: [ :year, :month, :day ],
+ default: { year: 2008, month: 7, day: 16, hour: 23, minute: 30, second: 1 },
+ discard_type: false,
+ include_blank: false,
+ ignore_date: false,
+ include_seconds: true
}, options)
end
@@ -3050,24 +3483,24 @@ class DateHelperTest < ActionView::TestCase
@post.updated_at = Time.local(2008, 7, 16, 23, 30)
options = {
- :order => [ :year, :month, :day ],
- :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
- :discard_type => false,
- :include_blank => false,
- :ignore_date => false,
- :include_seconds => true
+ order: [ :year, :month, :day ],
+ default: { year: 2008, month: 7, day: 16, hour: 23, minute: 30, second: 1 },
+ discard_type: false,
+ include_blank: false,
+ ignore_date: false,
+ include_seconds: true
}
datetime_select(@post, :updated_at, options)
# note: the literal hash is intentional to show that the actual options hash isn't modified
# don't change this!
assert_equal({
- :order => [ :year, :month, :day ],
- :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
- :discard_type => false,
- :include_blank => false,
- :ignore_date => false,
- :include_seconds => true
+ order: [ :year, :month, :day ],
+ default: { year: 2008, month: 7, day: 16, hour: 23, minute: 30, second: 1 },
+ discard_type: false,
+ include_blank: false,
+ ignore_date: false,
+ include_seconds: true
}, options)
end
@@ -3076,116 +3509,116 @@ class DateHelperTest < ActionView::TestCase
@post.updated_at = Time.local(2008, 7, 16, 23, 30)
options = {
- :order => [ :year, :month, :day ],
- :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
- :discard_type => false,
- :include_blank => false,
- :ignore_date => false,
- :include_seconds => true
+ order: [ :year, :month, :day ],
+ default: { year: 2008, month: 7, day: 16, hour: 23, minute: 30, second: 1 },
+ discard_type: false,
+ include_blank: false,
+ ignore_date: false,
+ include_seconds: true
}
time_select(@post, :updated_at, options)
# note: the literal hash is intentional to show that the actual options hash isn't modified
# don't change this!
assert_equal({
- :order => [ :year, :month, :day ],
- :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
- :discard_type => false,
- :include_blank => false,
- :ignore_date => false,
- :include_seconds => true
+ order: [ :year, :month, :day ],
+ default: { year: 2008, month: 7, day: 16, hour: 23, minute: 30, second: 1 },
+ discard_type: false,
+ include_blank: false,
+ ignore_date: false,
+ include_seconds: true
}, options)
end
def test_select_date_should_not_change_passed_options_hash
options = {
- :order => [ :year, :month, :day ],
- :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
- :discard_type => false,
- :include_blank => false,
- :ignore_date => false,
- :include_seconds => true
+ order: [ :year, :month, :day ],
+ default: { year: 2008, month: 7, day: 16, hour: 23, minute: 30, second: 1 },
+ discard_type: false,
+ include_blank: false,
+ ignore_date: false,
+ include_seconds: true
}
select_date(Date.today, options)
# note: the literal hash is intentional to show that the actual options hash isn't modified
# don't change this!
assert_equal({
- :order => [ :year, :month, :day ],
- :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
- :discard_type => false,
- :include_blank => false,
- :ignore_date => false,
- :include_seconds => true
+ order: [ :year, :month, :day ],
+ default: { year: 2008, month: 7, day: 16, hour: 23, minute: 30, second: 1 },
+ discard_type: false,
+ include_blank: false,
+ ignore_date: false,
+ include_seconds: true
}, options)
end
def test_select_datetime_should_not_change_passed_options_hash
options = {
- :order => [ :year, :month, :day ],
- :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
- :discard_type => false,
- :include_blank => false,
- :ignore_date => false,
- :include_seconds => true
+ order: [ :year, :month, :day ],
+ default: { year: 2008, month: 7, day: 16, hour: 23, minute: 30, second: 1 },
+ discard_type: false,
+ include_blank: false,
+ ignore_date: false,
+ include_seconds: true
}
select_datetime(Time.now, options)
# note: the literal hash is intentional to show that the actual options hash isn't modified
# don't change this!
assert_equal({
- :order => [ :year, :month, :day ],
- :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
- :discard_type => false,
- :include_blank => false,
- :ignore_date => false,
- :include_seconds => true
+ order: [ :year, :month, :day ],
+ default: { year: 2008, month: 7, day: 16, hour: 23, minute: 30, second: 1 },
+ discard_type: false,
+ include_blank: false,
+ ignore_date: false,
+ include_seconds: true
}, options)
end
def test_select_time_should_not_change_passed_options_hash
options = {
- :order => [ :year, :month, :day ],
- :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
- :discard_type => false,
- :include_blank => false,
- :ignore_date => false,
- :include_seconds => true
+ order: [ :year, :month, :day ],
+ default: { year: 2008, month: 7, day: 16, hour: 23, minute: 30, second: 1 },
+ discard_type: false,
+ include_blank: false,
+ ignore_date: false,
+ include_seconds: true
}
select_time(Time.now, options)
# note: the literal hash is intentional to show that the actual options hash isn't modified
# don't change this!
assert_equal({
- :order => [ :year, :month, :day ],
- :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
- :discard_type => false,
- :include_blank => false,
- :ignore_date => false,
- :include_seconds => true
+ order: [ :year, :month, :day ],
+ default: { year: 2008, month: 7, day: 16, hour: 23, minute: 30, second: 1 },
+ discard_type: false,
+ include_blank: false,
+ ignore_date: false,
+ include_seconds: true
}, options)
end
def test_select_html_safety
- assert select_day(16).html_safe?
- assert select_month(8).html_safe?
- assert select_year(Time.mktime(2003, 8, 16, 8, 4, 18)).html_safe?
- assert select_minute(Time.mktime(2003, 8, 16, 8, 4, 18)).html_safe?
- assert select_second(Time.mktime(2003, 8, 16, 8, 4, 18)).html_safe?
+ assert_predicate select_day(16), :html_safe?
+ assert_predicate select_month(8), :html_safe?
+ assert_predicate select_year(Time.mktime(2003, 8, 16, 8, 4, 18)), :html_safe?
+ assert_predicate select_minute(Time.mktime(2003, 8, 16, 8, 4, 18)), :html_safe?
+ assert_predicate select_second(Time.mktime(2003, 8, 16, 8, 4, 18)), :html_safe?
- assert select_minute(8, :use_hidden => true).html_safe?
- assert select_month(8, :prompt => 'Choose month').html_safe?
+ assert_predicate select_minute(8, use_hidden: true), :html_safe?
+ assert_predicate select_month(8, prompt: "Choose month"), :html_safe?
- assert select_time(Time.mktime(2003, 8, 16, 8, 4, 18), {}, :class => 'selector').html_safe?
- assert select_date(Time.mktime(2003, 8, 16), :date_separator => " / ", :start_year => 2003, :end_year => 2005, :prefix => "date[first]").html_safe?
+ assert_predicate select_time(Time.mktime(2003, 8, 16, 8, 4, 18), {}, { class: "selector" }), :html_safe?
+ assert_predicate select_date(Time.mktime(2003, 8, 16), date_separator: " / ", start_year: 2003, end_year: 2005, prefix: "date[first]"), :html_safe?
end
def test_object_select_html_safety
@post = Post.new
@post.written_on = Date.new(2004, 6, 15)
- assert date_select("post", "written_on", :default => Time.local(2006, 9, 19, 15, 16, 35), :include_blank => true).html_safe?
- assert time_select("post", "written_on", :ignore_date => true).html_safe?
+ assert_predicate date_select("post", "written_on", default: Time.local(2006, 9, 19, 15, 16, 35), include_blank: true), :html_safe?
+ assert_predicate time_select("post", "written_on", ignore_date: true), :html_safe?
end
def test_time_tag_with_date
@@ -3195,26 +3628,22 @@ class DateHelperTest < ActionView::TestCase
end
def test_time_tag_with_time
- time = Time.new(2013, 2, 20, 0, 0, 0, '+00:00')
+ time = Time.new(2013, 2, 20, 0, 0, 0, "+00:00")
expected = '<time datetime="2013-02-20T00:00:00+00:00">February 20, 2013 00:00</time>'
assert_equal expected, time_tag(time)
end
- def test_time_tag_pubdate_option
- assert_match(/<time.*pubdate="pubdate">.*<\/time>/, time_tag(Time.now, :pubdate => true))
- end
-
def test_time_tag_with_given_text
- assert_match(/<time.*>Right now<\/time>/, time_tag(Time.now, 'Right now'))
+ assert_match(/<time.*>Right now<\/time>/, time_tag(Time.now, "Right now"))
end
def test_time_tag_with_given_block
- assert_match(/<time.*><span>Right now<\/span><\/time>/, time_tag(Time.now){ '<span>Right now</span>'.html_safe })
+ assert_match(/<time.*><span>Right now<\/span><\/time>/, time_tag(Time.now) { raw("<span>Right now</span>") })
end
def test_time_tag_with_different_format
- time = Time.new(2013, 2, 20, 0, 0, 0, '+00:00')
+ time = Time.new(2013, 2, 20, 0, 0, 0, "+00:00")
expected = '<time datetime="2013-02-20T00:00:00+00:00">20 Feb 00:00</time>'
- assert_equal expected, time_tag(time, :format => :short)
+ assert_equal expected, time_tag(time, format: :short)
end
end
diff --git a/actionview/test/template/dependency_tracker_test.rb b/actionview/test/template/dependency_tracker_test.rb
index 672b4747ec..ef7aeac039 100644
--- a/actionview/test/template/dependency_tracker_test.rb
+++ b/actionview/test/template/dependency_tracker_test.rb
@@ -1,6 +1,7 @@
+# frozen_string_literal: true
-require 'abstract_unit'
-require 'action_view/dependency_tracker'
+require "abstract_unit"
+require "action_view/dependency_tracker"
class NeckbeardTracker
def self.call(name, template)
@@ -16,8 +17,8 @@ class FakeTemplate
end
end
-Neckbeard = lambda {|template| template.source }
-Bowtie = lambda {|template| template.source }
+Neckbeard = lambda { |template| template.source }
+Bowtie = lambda { |template| template.source }
class DependencyTrackerTest < ActionView::TestCase
def tracker
diff --git a/actionview/test/template/digestor_test.rb b/actionview/test/template/digestor_test.rb
index f0afcdb5ae..ddaa7febb3 100644
--- a/actionview/test/template/digestor_test.rb
+++ b/actionview/test/template/digestor_test.rb
@@ -1,44 +1,21 @@
-require 'abstract_unit'
-require 'fileutils'
+# frozen_string_literal: true
-class FixtureTemplate
- attr_reader :source, :handler
+require "abstract_unit"
+require "fileutils"
+require "action_view/dependency_tracker"
- def initialize(template_path)
- @source = File.read(template_path)
- @handler = ActionView::Template.handler_for_extension(:erb)
- rescue Errno::ENOENT
- raise ActionView::MissingTemplate.new([], "", [], true, [])
- end
-end
-
-class FixtureFinder
- FIXTURES_DIR = "#{File.dirname(__FILE__)}/../fixtures/digestor"
-
- attr_reader :details
- attr_accessor :formats
- attr_accessor :variants
+class FixtureFinder < ActionView::LookupContext
+ FIXTURES_DIR = File.expand_path("../fixtures/digestor", __dir__)
- def initialize
- @details = {}
- @formats = []
- @variants = []
- end
-
- def details_key
- details.hash
- end
-
- def find(name, prefixes = [], partial = false, keys = [], options = {})
- partial_name = partial ? name.gsub(%r|/([^/]+)$|, '/_\1') : name
- format = @formats.first.to_s
- format += "+#{@variants.first}" if @variants.any?
-
- FixtureTemplate.new("digestor/#{partial_name}.#{format}.erb")
+ def initialize(details = {})
+ super(ActionView::PathSet.new(["digestor", "digestor/api"]), details, [])
+ @rendered_format = :html
end
+end
- def disable_cache(&block)
- yield
+class ActionView::Digestor::Node
+ def flatten
+ [self] + children.flat_map(&:flatten)
end
end
@@ -47,6 +24,7 @@ class TemplateDigestorTest < ActionView::TestCase
@cwd = Dir.pwd
@tmp_dir = Dir.mktmpdir
+ ActionView::LookupContext::DetailsKey.clear
FileUtils.cp_r FixtureFinder::FIXTURES_DIR, @tmp_dir
Dir.chdir @tmp_dir
end
@@ -54,7 +32,6 @@ class TemplateDigestorTest < ActionView::TestCase
def teardown
Dir.chdir @cwd
FileUtils.rm_r @tmp_dir
- ActionView::Digestor.cache.clear
end
def test_top_level_change_reflected
@@ -75,6 +52,30 @@ class TemplateDigestorTest < ActionView::TestCase
end
end
+ def test_explicit_dependency_wildcard
+ assert_digest_difference("events/index") do
+ change_template("events/_completed")
+ end
+ end
+
+ def test_explicit_dependency_wildcard_picks_up_added_file
+ disable_resolver_caching do
+ assert_digest_difference("events/index") do
+ add_template("events/_uncompleted")
+ end
+ end
+ end
+
+ def test_explicit_dependency_wildcard_picks_up_removed_file
+ disable_resolver_caching do
+ add_template("events/_subscribers_changed")
+
+ assert_digest_difference("events/index") do
+ remove_template("events/_subscribers_changed")
+ end
+ end
+ end
+
def test_second_level_dependency
assert_digest_difference("messages/show") do
change_template("comments/_comments")
@@ -112,23 +113,65 @@ 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
+ def test_getting_of_singly_nested_dependencies
+ singly_nested_dependencies = ["messages/header", "messages/form", "messages/message", "events/event", "comments/comment"]
+ assert_equal singly_nested_dependencies, nested_dependencies("messages/edit")
+ end
+
+ def test_getting_of_doubly_nested_dependencies
+ doubly_nested = [{ "comments/comments" => ["comments/comment"] }, "messages/message"]
+ assert_equal doubly_nested, nested_dependencies("messages/peek")
+ end
+
def test_nested_template_directory
assert_digest_difference("messages/show") do
change_template("messages/actions/_move")
end
end
+ def test_nested_template_deps
+ nested_deps = ["messages/header", { "comments/comments" => ["comments/comment"] }, "messages/actions/move", "events/event", "messages/something_missing", "messages/something_missing_1", "messages/message", "messages/form"]
+ assert_equal nested_deps, nested_dependencies("messages/show")
+ end
+
+ def test_nested_template_deps_with_non_default_rendered_format
+ finder.rendered_format = nil
+ nested_deps = [{ "comments/comments" => ["comments/comment"] }]
+ assert_equal nested_deps, nested_dependencies("messages/thread")
+ end
+
+ def test_template_formats_of_nested_deps_with_non_default_rendered_format
+ finder.rendered_format = nil
+ assert_equal [:json], tree_template_formats("messages/thread").uniq
+ end
+
+ def test_template_formats_of_dependencies_with_same_logical_name_and_different_rendered_format
+ assert_equal [:html], tree_template_formats("messages/show").uniq
+ end
+
+ def test_template_dependencies_with_fallback_from_js_to_html_format
+ finder.rendered_format = :js
+ assert_equal ["comments/comment"], dependencies("comments/show")
+ end
+
+ def test_template_digest_with_fallback_from_js_to_html_format
+ finder.rendered_format = :js
+ assert_digest_difference("comments/show") do
+ change_template("comments/_comment")
+ end
+ end
+
def test_recursion_in_renders
assert digest("level/recursion") # assert recursion is possible
assert_not_nil digest("level/recursion") # assert digest is stored
@@ -155,7 +198,7 @@ class TemplateDigestorTest < ActionView::TestCase
end
def test_dont_generate_a_digest_for_missing_templates
- assert_equal '', digest("nothing/there")
+ assert_equal "", digest("nothing/there")
end
def test_collection_dependency
@@ -176,13 +219,14 @@ class TemplateDigestorTest < ActionView::TestCase
def test_details_are_included_in_cache_key
# Cache the template digest.
+ @finder = FixtureFinder.new(formats: [:html])
old_digest = digest("events/_event")
# Change the template; the cached digest remains unchanged.
change_template("events/_event")
# The details are changed, so a new cache key is generated.
- finder.details[:foo] = "bar"
+ @finder = FixtureFinder.new
# The cache is busted.
assert_not_equal old_digest, digest("events/_event")
@@ -219,7 +263,7 @@ class TemplateDigestorTest < ActionView::TestCase
end
def test_variants
- assert_digest_difference("messages/new", false, variants: [:iphone]) do
+ assert_digest_difference("messages/new", variants: [:iphone]) do
change_template("messages/new", :iphone)
change_template("messages/_header", :iphone)
end
@@ -239,14 +283,11 @@ class TemplateDigestorTest < ActionView::TestCase
assert_not_equal digest_phone, digest_fridge_phone
end
- def test_cache_template_loading
- resolver_before = ActionView::Resolver.caching
- ActionView::Resolver.caching = false
- assert_digest_difference("messages/edit", true) do
- change_template("comments/_comment")
- end
- ensure
- ActionView::Resolver.caching = resolver_before
+ def test_different_formats_with_same_logical_template_names_results_in_different_digests
+ html_digest = digest("comments/_comment", format: :html)
+ json_digest = digest("comments/_comment", format: :json)
+
+ assert_not_equal html_digest, json_digest
end
def test_digest_cache_cleanup_with_recursion
@@ -260,21 +301,17 @@ class TemplateDigestorTest < ActionView::TestCase
end
def test_digest_cache_cleanup_with_recursion_and_template_caching_off
- resolver_before = ActionView::Resolver.caching
- ActionView::Resolver.caching = false
+ disable_resolver_caching do
+ first_digest = digest("level/_recursion")
+ second_digest = digest("level/_recursion")
- first_digest = digest("level/_recursion")
- second_digest = digest("level/_recursion")
-
- assert first_digest
+ assert first_digest
- # If the cache is cleaned up correctly, subsequent digests should return the same
- assert_equal first_digest, second_digest
- ensure
- ActionView::Resolver.caching = resolver_before
+ # If the cache is cleaned up correctly, subsequent digests should return the same
+ assert_equal first_digest, second_digest
+ end
end
-
private
def assert_logged(message)
old_logger = ActionView::Base.logger
@@ -291,31 +328,46 @@ class TemplateDigestorTest < ActionView::TestCase
end
end
- def assert_digest_difference(template_name, persistent = false, options = {})
+ def assert_digest_difference(template_name, options = {})
previous_digest = digest(template_name, options)
- ActionView::Digestor.cache.clear unless persistent
+ finder.digest_cache.clear
yield
- assert previous_digest != digest(template_name, options), "digest didn't change"
- ActionView::Digestor.cache.clear
+ assert_not_equal previous_digest, digest(template_name, options), "digest didn't change"
+ finder.digest_cache.clear
end
def digest(template_name, options = {})
options = options.dup
+ finder_options = options.extract!(:variants, :format)
- finder.formats = [:html]
- finder.variants = options.delete(:variants) || []
+ finder.variants = finder_options[:variants] || []
+ finder.rendered_format = finder_options[:format] if finder_options[:format]
- ActionView::Digestor.digest({ name: template_name, finder: finder }.merge(options))
+ ActionView::Digestor.digest(name: template_name, finder: finder, dependencies: (options[:dependencies] || []))
end
def dependencies(template_name)
- ActionView::Digestor.new({ name: template_name, finder: finder }).dependencies
+ tree = ActionView::Digestor.tree(template_name, finder)
+ tree.children.map(&:name)
end
def nested_dependencies(template_name)
- ActionView::Digestor.new({ name: template_name, finder: finder }).nested_dependencies
+ tree = ActionView::Digestor.tree(template_name, finder)
+ tree.children.map(&:to_dep_map)
+ end
+
+ def tree_template_formats(template_name)
+ tree = ActionView::Digestor.tree(template_name, finder)
+ tree.flatten.map(&:template).compact.flat_map(&:formats)
+ end
+
+ def disable_resolver_caching
+ old_caching, ActionView::Resolver.caching = ActionView::Resolver.caching, false
+ yield
+ ensure
+ ActionView::Resolver.caching = old_caching
end
def finder
@@ -329,4 +381,9 @@ class TemplateDigestorTest < ActionView::TestCase
f.write "\nTHIS WAS CHANGED!"
end
end
+ alias_method :add_template, :change_template
+
+ def remove_template(template_name)
+ File.delete("digestor/#{template_name}.html.erb")
+ end
end
diff --git a/actionview/test/template/erb/form_for_test.rb b/actionview/test/template/erb/form_for_test.rb
index e722b40a9a..b3a47e17a4 100644
--- a/actionview/test/template/erb/form_for_test.rb
+++ b/actionview/test/template/erb/form_for_test.rb
@@ -1,10 +1,16 @@
+# frozen_string_literal: true
+
require "abstract_unit"
require "template/erb/helper"
module ERBTest
class TagHelperTest < BlockTestCase
test "form_for works" do
- output = render_content "form_for(:staticpage, :url => {:controller => 'blah', :action => 'update'})", ""
+ routes = ActionDispatch::Routing::RouteSet.new
+ routes.draw do
+ get "/blah/update", to: "blah#update"
+ end
+ output = render_content "form_for(:staticpage, :url => {:controller => 'blah', :action => 'update'})", "", routes
assert_match %r{<form.*action="/blah/update".*method="post">.*</form>}, output
end
end
diff --git a/actionview/test/template/erb/helper.rb b/actionview/test/template/erb/helper.rb
index a1973068d5..727cc3dcf2 100644
--- a/actionview/test/template/erb/helper.rb
+++ b/actionview/test/template/erb/helper.rb
@@ -1,7 +1,8 @@
+# frozen_string_literal: true
+
module ERBTest
class ViewContext
include ActionView::Helpers::UrlHelper
- include SharedTestRoutes.url_helpers
include ActionView::Helpers::TagHelper
include ActionView::Helpers::JavaScriptHelper
include ActionView::Helpers::FormHelper
@@ -12,9 +13,15 @@ module ERBTest
end
class BlockTestCase < ActiveSupport::TestCase
- def render_content(start, inside)
+ def render_content(start, inside, routes = nil)
+ routes ||= ActionDispatch::Routing::RouteSet.new.tap do |rs|
+ rs.draw { }
+ end
+ context = Class.new(ViewContext) {
+ include routes.url_helpers
+ }.new
template = block_helper(start, inside)
- ActionView::Template::Handlers::Erubis.new(template).evaluate(ViewContext.new)
+ ActionView::Template::Handlers::ERB.erb_implementation.new(template).evaluate(context)
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..24a7592950 100644
--- a/actionview/test/template/erb/tag_helper_test.rb
+++ b/actionview/test/template/erb/tag_helper_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "abstract_unit"
require "template/erb/helper"
@@ -18,8 +20,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 3e72be31de..bd702dbe94 100644
--- a/actionview/test/template/erb_util_test.rb
+++ b/actionview/test/template/erb_util_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'active_support/json'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/json"
class ErbUtilTest < ActiveSupport::TestCase
include ERB::Util
@@ -17,19 +19,19 @@ class ErbUtilTest < ActiveSupport::TestCase
end
HTML_ESCAPE_TEST_CASES = [
- ['<br>', '&lt;br&gt;'],
- ['a & b', 'a &amp; b'],
- ['"quoted" string', '&quot;quoted&quot; string'],
- ["'quoted' string", '&#39;quoted&#39; string'],
+ ["<br>", "&lt;br&gt;"],
+ ["a & b", "a &amp; b"],
+ ['"quoted" string', "&quot;quoted&quot; string"],
+ ["'quoted' string", "&#39;quoted&#39; string"],
[
'<script type="application/javascript">alert("You are \'pwned\'!")</script>',
- '&lt;script type=&quot;application/javascript&quot;&gt;alert(&quot;You are &#39;pwned&#39;!&quot;)&lt;/script&gt;'
+ "&lt;script type=&quot;application/javascript&quot;&gt;alert(&quot;You are &#39;pwned&#39;!&quot;)&lt;/script&gt;"
]
]
JSON_ESCAPE_TEST_CASES = [
- ['1', '1'],
- ['null', 'null'],
+ ["1", "1"],
+ ["null", "null"],
['"&"', '"\u0026"'],
['"</script>"', '"\u003c/script\u003e"'],
['["</script>"]', '["\u003c/script\u003e"]'],
@@ -51,7 +53,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
@@ -63,24 +70,24 @@ class ErbUtilTest < ActiveSupport::TestCase
def test_json_escape_returns_unsafe_strings_when_passed_unsafe_strings
value = json_escape("asdf")
- assert !value.html_safe?
+ assert_not_predicate value, :html_safe?
end
def test_json_escape_returns_safe_strings_when_passed_safe_strings
value = json_escape("asdf".html_safe)
- assert value.html_safe?
+ assert_predicate value, :html_safe?
end
def test_html_escape_is_html_safe
escaped = h("<p>")
assert_equal "&lt;p&gt;", escaped
- assert escaped.html_safe?
+ assert_predicate escaped, :html_safe?
end
def test_html_escape_passes_html_escape_unmodified
escaped = h("<p>".html_safe)
assert_equal "<p>", escaped
- assert escaped.html_safe?
+ assert_predicate escaped, :html_safe?
end
def test_rest_in_ascii
@@ -91,17 +98,17 @@ class ErbUtilTest < ActiveSupport::TestCase
end
def test_html_escape_once
- assert_equal '1 &lt;&gt;&amp;&quot;&#39; 2 &amp; 3', html_escape_once('1 <>&"\' 2 &amp; 3')
+ assert_equal "1 &lt;&gt;&amp;&quot;&#39; 2 &amp; 3", html_escape_once('1 <>&"\' 2 &amp; 3')
assert_equal " &#X27; &#x27; &#x03BB; &#X03bb; &quot; &#39; &lt; &gt; ", html_escape_once(" &#X27; &#x27; &#x03BB; &#X03bb; \" ' < > ")
end
def test_html_escape_once_returns_unsafe_strings_when_passed_unsafe_strings
- value = html_escape_once('1 < 2 &amp; 3')
- assert !value.html_safe?
+ value = html_escape_once("1 < 2 &amp; 3")
+ assert_not_predicate value, :html_safe?
end
def test_html_escape_once_returns_safe_strings_when_passed_safe_strings
- value = html_escape_once('1 < 2 &amp; 3'.html_safe)
- assert value.html_safe?
+ value = html_escape_once("1 < 2 &amp; 3".html_safe)
+ assert_predicate value, :html_safe?
end
end
diff --git a/actionview/test/template/form_collections_helper_test.rb b/actionview/test/template/form_collections_helper_test.rb
index b193d387c3..6db55a1447 100644
--- a/actionview/test/template/form_collections_helper_test.rb
+++ b/actionview/test/template/form_collections_helper_test.rb
@@ -1,11 +1,12 @@
-require 'abstract_unit'
+# frozen_string_literal: true
-class Category < Struct.new(:id, :name)
-end
+require "abstract_unit"
+
+Category = Struct.new(:id, :name)
class FormCollectionsHelperTest < ActionView::TestCase
def assert_no_select(selector, value = nil)
- assert_select(selector, :text => value, :count => 0)
+ assert_select(selector, text: value, count: 0)
end
def with_collection_radio_buttons(*args, &block)
@@ -17,170 +18,177 @@ class FormCollectionsHelperTest < ActionView::TestCase
end
# COLLECTION RADIO BUTTONS
- test 'collection radio accepts a collection and generates inputs from value method' do
+ test "collection radio accepts a collection and generates inputs from value method" do
with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s
- assert_select 'input[type=radio][value=true]#user_active_true'
- assert_select 'input[type=radio][value=false]#user_active_false'
+ assert_select "input[type=radio][value=true]#user_active_true"
+ assert_select "input[type=radio][value=false]#user_active_false"
end
- test 'collection radio accepts a collection and generates inputs from label method' do
+ test "collection radio accepts a collection and generates inputs from label method" do
with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s
- assert_select 'label[for=user_active_true]', 'true'
- assert_select 'label[for=user_active_false]', 'false'
+ assert_select "label[for=user_active_true]", "true"
+ assert_select "label[for=user_active_false]", "false"
+ end
+
+ test "collection radio handles camelized collection values for labels correctly" do
+ with_collection_radio_buttons :user, :active, ["Yes", "No"], :to_s, :to_s
+
+ assert_select "label[for=user_active_yes]", "Yes"
+ assert_select "label[for=user_active_no]", "No"
end
- test 'collection radio handles camelized collection values for labels correctly' do
- with_collection_radio_buttons :user, :active, ['Yes', 'No'], :to_s, :to_s
+ test "collection radio generates labels for non-English values correctly" do
+ with_collection_radio_buttons :user, :title, ["Господин", "Госпожа"], :to_s, :to_s
- assert_select 'label[for=user_active_yes]', 'Yes'
- assert_select 'label[for=user_active_no]', 'No'
+ assert_select "input[type=radio]#user_title_господин"
+ assert_select "label[for=user_title_господин]", "Господин"
end
- test 'collection radio should sanitize collection values for labels correctly' do
- with_collection_radio_buttons :user, :name, ['$0.99', '$1.99'], :to_s, :to_s
- assert_select 'label[for=user_name_099]', '$0.99'
- assert_select 'label[for=user_name_199]', '$1.99'
+ test "collection radio should sanitize collection values for labels correctly" do
+ with_collection_radio_buttons :user, :name, ["$0.99", "$1.99"], :to_s, :to_s
+ assert_select "label[for=user_name_099]", "$0.99"
+ assert_select "label[for=user_name_199]", "$1.99"
end
- test 'collection radio accepts checked item' do
- with_collection_radio_buttons :user, :active, [[1, true], [0, false]], :last, :first, :checked => true
+ test "collection radio accepts checked item" do
+ with_collection_radio_buttons :user, :active, [[1, true], [0, false]], :last, :first, checked: true
- assert_select 'input[type=radio][value=true][checked=checked]'
- assert_no_select 'input[type=radio][value=false][checked=checked]'
+ assert_select "input[type=radio][value=true][checked=checked]"
+ assert_no_select "input[type=radio][value=false][checked=checked]"
end
- test 'collection radio accepts multiple disabled items' do
- collection = [[1, true], [0, false], [2, 'other']]
- with_collection_radio_buttons :user, :active, collection, :last, :first, :disabled => [true, false]
+ test "collection radio accepts multiple disabled items" do
+ collection = [[1, true], [0, false], [2, "other"]]
+ with_collection_radio_buttons :user, :active, collection, :last, :first, disabled: [true, false]
- assert_select 'input[type=radio][value=true][disabled=disabled]'
- assert_select 'input[type=radio][value=false][disabled=disabled]'
- assert_no_select 'input[type=radio][value=other][disabled=disabled]'
+ assert_select "input[type=radio][value=true][disabled=disabled]"
+ assert_select "input[type=radio][value=false][disabled=disabled]"
+ assert_no_select "input[type=radio][value=other][disabled=disabled]"
end
- test 'collection radio accepts single disabled item' do
+ test "collection radio accepts single disabled item" do
collection = [[1, true], [0, false]]
- with_collection_radio_buttons :user, :active, collection, :last, :first, :disabled => true
+ with_collection_radio_buttons :user, :active, collection, :last, :first, disabled: true
- assert_select 'input[type=radio][value=true][disabled=disabled]'
- assert_no_select 'input[type=radio][value=false][disabled=disabled]'
+ assert_select "input[type=radio][value=true][disabled=disabled]"
+ assert_no_select "input[type=radio][value=false][disabled=disabled]"
end
- test 'collection radio accepts multiple readonly items' do
- collection = [[1, true], [0, false], [2, 'other']]
- with_collection_radio_buttons :user, :active, collection, :last, :first, :readonly => [true, false]
+ test "collection radio accepts multiple readonly items" do
+ collection = [[1, true], [0, false], [2, "other"]]
+ with_collection_radio_buttons :user, :active, collection, :last, :first, readonly: [true, false]
- assert_select 'input[type=radio][value=true][readonly=readonly]'
- assert_select 'input[type=radio][value=false][readonly=readonly]'
- assert_no_select 'input[type=radio][value=other][readonly=readonly]'
+ assert_select "input[type=radio][value=true][readonly=readonly]"
+ assert_select "input[type=radio][value=false][readonly=readonly]"
+ assert_no_select "input[type=radio][value=other][readonly=readonly]"
end
- test 'collection radio accepts single readonly item' do
+ test "collection radio accepts single readonly item" do
collection = [[1, true], [0, false]]
- with_collection_radio_buttons :user, :active, collection, :last, :first, :readonly => true
+ with_collection_radio_buttons :user, :active, collection, :last, :first, readonly: true
- assert_select 'input[type=radio][value=true][readonly=readonly]'
- assert_no_select 'input[type=radio][value=false][readonly=readonly]'
+ assert_select "input[type=radio][value=true][readonly=readonly]"
+ assert_no_select "input[type=radio][value=false][readonly=readonly]"
end
- test 'collection radio accepts html options as input' do
+ test "collection radio accepts html options as input" do
collection = [[1, true], [0, false]]
- with_collection_radio_buttons :user, :active, collection, :last, :first, {}, :class => 'special-radio'
+ with_collection_radio_buttons :user, :active, collection, :last, :first, {}, { class: "special-radio" }
- assert_select 'input[type=radio][value=true].special-radio#user_active_true'
- assert_select 'input[type=radio][value=false].special-radio#user_active_false'
+ assert_select "input[type=radio][value=true].special-radio#user_active_true"
+ assert_select "input[type=radio][value=false].special-radio#user_active_false"
end
- test 'collection radio accepts html options as the last element of array' do
- collection = [[1, true, {class: 'foo'}], [0, false, {class: 'bar'}]]
+ test "collection radio accepts html options as the last element of array" do
+ collection = [[1, true, { class: "foo" }], [0, false, { class: "bar" }]]
with_collection_radio_buttons :user, :active, collection, :second, :first
- assert_select 'input[type=radio][value=true].foo#user_active_true'
- assert_select 'input[type=radio][value=false].bar#user_active_false'
+ assert_select "input[type=radio][value=true].foo#user_active_true"
+ assert_select "input[type=radio][value=false].bar#user_active_false"
end
- test 'collection radio sets the label class defined inside the block' do
- collection = [[1, true, {class: 'foo'}], [0, false, {class: 'bar'}]]
+ test "collection radio sets the label class defined inside the block" do
+ collection = [[1, true, { class: "foo" }], [0, false, { class: "bar" }]]
with_collection_radio_buttons :user, :active, collection, :second, :first do |b|
b.label(class: "collection_radio_buttons")
end
- assert_select 'label.collection_radio_buttons[for=user_active_true]'
- assert_select 'label.collection_radio_buttons[for=user_active_false]'
+ assert_select "label.collection_radio_buttons[for=user_active_true]"
+ assert_select "label.collection_radio_buttons[for=user_active_false]"
end
- test 'collection radio does not include the input class in the respective label' do
- collection = [[1, true, {class: 'foo'}], [0, false, {class: 'bar'}]]
+ test "collection radio does not include the input class in the respective label" do
+ collection = [[1, true, { class: "foo" }], [0, false, { class: "bar" }]]
with_collection_radio_buttons :user, :active, collection, :second, :first
- assert_no_select 'label.foo[for=user_active_true]'
- assert_no_select 'label.bar[for=user_active_false]'
+ assert_no_select "label.foo[for=user_active_true]"
+ assert_no_select "label.bar[for=user_active_false]"
end
- test 'collection radio does not wrap input inside the label' do
+ test "collection radio does not wrap input inside the label" do
with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s
- assert_select 'input[type=radio] + label'
- assert_no_select 'label input'
+ assert_select "input[type=radio] + label"
+ assert_no_select "label input"
end
- test 'collection radio accepts a block to render the label as radio button wrapper' do
+ test "collection radio accepts a block to render the label as radio button wrapper" do
with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s do |b|
b.label { b.radio_button }
end
- assert_select 'label[for=user_active_true] > input#user_active_true[type=radio]'
- assert_select 'label[for=user_active_false] > input#user_active_false[type=radio]'
+ assert_select "label[for=user_active_true] > input#user_active_true[type=radio]"
+ assert_select "label[for=user_active_false] > input#user_active_false[type=radio]"
end
- test 'collection radio accepts a block to change the order of label and radio button' do
+ test "collection radio accepts a block to change the order of label and radio button" do
with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s do |b|
b.label + b.radio_button
end
- assert_select 'label[for=user_active_true] + input#user_active_true[type=radio]'
- assert_select 'label[for=user_active_false] + input#user_active_false[type=radio]'
+ assert_select "label[for=user_active_true] + input#user_active_true[type=radio]"
+ assert_select "label[for=user_active_false] + input#user_active_false[type=radio]"
end
- test 'collection radio with block helpers accept extra html options' do
+ test "collection radio with block helpers accept extra html options" do
with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s do |b|
- b.label(:class => "radio_button") + b.radio_button(:class => "radio_button")
+ b.label(class: "radio_button") + b.radio_button(class: "radio_button")
end
- assert_select 'label.radio_button[for=user_active_true] + input#user_active_true.radio_button[type=radio]'
- assert_select 'label.radio_button[for=user_active_false] + input#user_active_false.radio_button[type=radio]'
+ assert_select "label.radio_button[for=user_active_true] + input#user_active_true.radio_button[type=radio]"
+ assert_select "label.radio_button[for=user_active_false] + input#user_active_false.radio_button[type=radio]"
end
- test 'collection radio with block helpers allows access to current text and value' do
+ test "collection radio with block helpers allows access to current text and value" do
with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s do |b|
- b.label(:"data-value" => b.value) { b.radio_button + b.text }
+ b.label("data-value": b.value) { b.radio_button + b.text }
end
- assert_select 'label[for=user_active_true][data-value=true]', 'true' do
- assert_select 'input#user_active_true[type=radio]'
+ assert_select "label[for=user_active_true][data-value=true]", "true" do
+ assert_select "input#user_active_true[type=radio]"
end
- assert_select 'label[for=user_active_false][data-value=false]', 'false' do
- assert_select 'input#user_active_false[type=radio]'
+ assert_select "label[for=user_active_false][data-value=false]", "false" do
+ assert_select "input#user_active_false[type=radio]"
end
end
- test 'collection radio with block helpers allows access to the current object item in the collection to access extra properties' do
+ test "collection radio with block helpers allows access to the current object item in the collection to access extra properties" do
with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s do |b|
- b.label(:class => b.object) { b.radio_button + b.text }
+ b.label(class: b.object) { b.radio_button + b.text }
end
- assert_select 'label.true[for=user_active_true]', 'true' do
- assert_select 'input#user_active_true[type=radio]'
+ assert_select "label.true[for=user_active_true]", "true" do
+ assert_select "input#user_active_true[type=radio]"
end
- assert_select 'label.false[for=user_active_false]', 'false' do
- assert_select 'input#user_active_false[type=radio]'
+ assert_select "label.false[for=user_active_false]", "false" do
+ assert_select "input#user_active_false[type=radio]"
end
end
- test 'collection radio buttons with fields for' do
- collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
+ test "collection radio buttons with fields for" do
+ collection = [Category.new(1, "Category 1"), Category.new(2, "Category 2")]
@output_buffer = fields_for(:post) do |p|
p.collection_radio_buttons :category_id, collection, :id, :name
end
@@ -188,133 +196,193 @@ class FormCollectionsHelperTest < ActionView::TestCase
assert_select 'input#post_category_id_1[type=radio][value="1"]'
assert_select 'input#post_category_id_2[type=radio][value="2"]'
- assert_select 'label[for=post_category_id_1]', 'Category 1'
- assert_select 'label[for=post_category_id_2]', 'Category 2'
+ assert_select "label[for=post_category_id_1]", "Category 1"
+ assert_select "label[for=post_category_id_2]", "Category 2"
+ end
+
+ test "collection radio accepts checked item which has a value of false" do
+ with_collection_radio_buttons :user, :active, [[1, true], [0, false]], :last, :first, checked: false
+ assert_no_select "input[type=radio][value=true][checked=checked]"
+ assert_select "input[type=radio][value=false][checked=checked]"
+ end
+
+ test "collection radio buttons generates only one hidden field for the entire collection, to ensure something will be sent back to the server when posting an empty collection" do
+ collection = [Category.new(1, "Category 1"), Category.new(2, "Category 2")]
+ with_collection_radio_buttons :user, :category_ids, collection, :id, :name
+
+ assert_select "input[type=hidden][name='user[category_ids]'][value='']", count: 1
+ end
+
+ test "collection radio buttons generates a hidden field using the given :name in :html_options" do
+ collection = [Category.new(1, "Category 1"), Category.new(2, "Category 2")]
+ with_collection_radio_buttons :user, :category_ids, collection, :id, :name, {}, { name: "user[other_category_ids]" }
+
+ assert_select "input[type=hidden][name='user[other_category_ids]'][value='']", count: 1
end
- test 'collection radio accepts checked item which has a value of false' do
- with_collection_radio_buttons :user, :active, [[1, true], [0, false]], :last, :first, :checked => false
- assert_no_select 'input[type=radio][value=true][checked=checked]'
- assert_select 'input[type=radio][value=false][checked=checked]'
+ test "collection radio buttons generates a hidden field with index if it was provided" do
+ collection = [Category.new(1, "Category 1"), Category.new(2, "Category 2")]
+ with_collection_radio_buttons :user, :category_ids, collection, :id, :name, index: 322
+
+ assert_select "input[type=hidden][name='user[322][category_ids]'][value='']", count: 1
+ end
+
+ test "collection radio buttons does not generate a hidden field if include_hidden option is false" do
+ collection = [Category.new(1, "Category 1"), Category.new(2, "Category 2")]
+ with_collection_radio_buttons :user, :category_ids, collection, :id, :name, include_hidden: false
+
+ assert_select "input[type=hidden][name='user[category_ids]'][value='']", count: 0
+ end
+
+ test "collection radio buttons does not generate a hidden field if include_hidden option is false with key as string" do
+ collection = [Category.new(1, "Category 1"), Category.new(2, "Category 2")]
+ with_collection_radio_buttons :user, :category_ids, collection, :id, :name, "include_hidden" => false
+
+ assert_select "input[type=hidden][name='user[category_ids]'][value='']", count: 0
end
# COLLECTION CHECK BOXES
- test 'collection check boxes accepts a collection and generate a series of checkboxes for value method' do
- collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
+ test "collection check boxes accepts a collection and generate a series of checkboxes for value method" do
+ collection = [Category.new(1, "Category 1"), Category.new(2, "Category 2")]
with_collection_check_boxes :user, :category_ids, collection, :id, :name
assert_select 'input#user_category_ids_1[type=checkbox][value="1"]'
assert_select 'input#user_category_ids_2[type=checkbox][value="2"]'
end
- test 'collection check boxes generates only one hidden field for the entire collection, to ensure something will be sent back to the server when posting an empty collection' do
- collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
+ test "collection check boxes generates only one hidden field for the entire collection, to ensure something will be sent back to the server when posting an empty collection" do
+ collection = [Category.new(1, "Category 1"), Category.new(2, "Category 2")]
with_collection_check_boxes :user, :category_ids, collection, :id, :name
- assert_select "input[type=hidden][name='user[category_ids][]'][value='']", :count => 1
+ assert_select "input[type=hidden][name='user[category_ids][]'][value='']", count: 1
end
- test 'collection check boxes generates a hidden field using the given :name in :html_options' do
- collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
- with_collection_check_boxes :user, :category_ids, collection, :id, :name, {}, {name: "user[other_category_ids][]"}
+ test "collection check boxes generates a hidden field using the given :name in :html_options" do
+ collection = [Category.new(1, "Category 1"), Category.new(2, "Category 2")]
+ with_collection_check_boxes :user, :category_ids, collection, :id, :name, {}, { name: "user[other_category_ids][]" }
- assert_select "input[type=hidden][name='user[other_category_ids][]'][value='']", :count => 1
+ assert_select "input[type=hidden][name='user[other_category_ids][]'][value='']", count: 1
end
- test 'collection check boxes generates a hidden field with index if it was provided' do
- collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
- with_collection_check_boxes :user, :category_ids, collection, :id, :name, { index: 322 }
+ test "collection check boxes generates a hidden field with index if it was provided" do
+ collection = [Category.new(1, "Category 1"), Category.new(2, "Category 2")]
+ with_collection_check_boxes :user, :category_ids, collection, :id, :name, index: 322
assert_select "input[type=hidden][name='user[322][category_ids][]'][value='']", count: 1
end
- test 'collection check boxes does not generate a hidden field if include_hidden option is false' do
- collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
+ test "collection check boxes does not generate a hidden field if include_hidden option is false" do
+ collection = [Category.new(1, "Category 1"), Category.new(2, "Category 2")]
with_collection_check_boxes :user, :category_ids, collection, :id, :name, include_hidden: false
- assert_select "input[type=hidden][name='user[category_ids][]'][value='']", :count => 0
+ assert_select "input[type=hidden][name='user[category_ids][]'][value='']", count: 0
end
- test 'collection check boxes accepts a collection and generate a series of checkboxes with labels for label method' do
- collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
+ test "collection check boxes does not generate a hidden field if include_hidden option is false with key as string" do
+ collection = [Category.new(1, "Category 1"), Category.new(2, "Category 2")]
+ with_collection_check_boxes :user, :category_ids, collection, :id, :name, "include_hidden" => false
+
+ assert_select "input[type=hidden][name='user[category_ids][]'][value='']", count: 0
+ end
+
+ test "collection check boxes accepts a collection and generate a series of checkboxes with labels for label method" do
+ collection = [Category.new(1, "Category 1"), Category.new(2, "Category 2")]
with_collection_check_boxes :user, :category_ids, collection, :id, :name
- assert_select 'label[for=user_category_ids_1]', 'Category 1'
- assert_select 'label[for=user_category_ids_2]', 'Category 2'
+ assert_select "label[for=user_category_ids_1]", "Category 1"
+ assert_select "label[for=user_category_ids_2]", "Category 2"
end
- test 'collection check boxes handles camelized collection values for labels correctly' do
- with_collection_check_boxes :user, :active, ['Yes', 'No'], :to_s, :to_s
+ test "collection check boxes handles camelized collection values for labels correctly" do
+ with_collection_check_boxes :user, :active, ["Yes", "No"], :to_s, :to_s
- assert_select 'label[for=user_active_yes]', 'Yes'
- assert_select 'label[for=user_active_no]', 'No'
+ assert_select "label[for=user_active_yes]", "Yes"
+ assert_select "label[for=user_active_no]", "No"
end
- test 'collection check box should sanitize collection values for labels correctly' do
- with_collection_check_boxes :user, :name, ['$0.99', '$1.99'], :to_s, :to_s
- assert_select 'label[for=user_name_099]', '$0.99'
- assert_select 'label[for=user_name_199]', '$1.99'
+ test "collection check box should sanitize collection values for labels correctly" do
+ with_collection_check_boxes :user, :name, ["$0.99", "$1.99"], :to_s, :to_s
+ assert_select "label[for=user_name_099]", "$0.99"
+ assert_select "label[for=user_name_199]", "$1.99"
end
- test 'collection check boxes accepts html options as the last element of array' do
- collection = [[1, 'Category 1', {class: 'foo'}], [2, 'Category 2', {class: 'bar'}]]
+ test "collection check boxes generates labels for non-English values correctly" do
+ with_collection_check_boxes :user, :title, ["Господин", "Госпожа"], :to_s, :to_s
+
+ assert_select "input[type=checkbox]#user_title_господин"
+ assert_select "label[for=user_title_господин]", "Господин"
+ end
+
+ test "collection check boxes accepts html options as the last element of array" do
+ collection = [[1, "Category 1", { class: "foo" }], [2, "Category 2", { class: "bar" }]]
with_collection_check_boxes :user, :active, collection, :first, :second
assert_select 'input[type=checkbox][value="1"].foo'
assert_select 'input[type=checkbox][value="2"].bar'
end
- test 'collection check boxes sets the label class defined inside the block' do
- collection = [[1, 'Category 1', {class: 'foo'}], [2, 'Category 2', {class: 'bar'}]]
+ test "collection check boxes propagates input id to the label for attribute" do
+ collection = [[1, "Category 1", { id: "foo" }], [2, "Category 2", { id: "bar" }]]
+ with_collection_check_boxes :user, :active, collection, :first, :second
+
+ assert_select 'input[type=checkbox][value="1"]#foo'
+ assert_select 'input[type=checkbox][value="2"]#bar'
+
+ assert_select "label[for=foo]"
+ assert_select "label[for=bar]"
+ end
+
+ test "collection check boxes sets the label class defined inside the block" do
+ collection = [[1, "Category 1", { class: "foo" }], [2, "Category 2", { class: "bar" }]]
with_collection_check_boxes :user, :active, collection, :second, :first do |b|
- b.label(class: 'collection_check_boxes')
+ b.label(class: "collection_check_boxes")
end
- assert_select 'label.collection_check_boxes[for=user_active_category_1]'
- assert_select 'label.collection_check_boxes[for=user_active_category_2]'
+ assert_select "label.collection_check_boxes[for=user_active_category_1]"
+ assert_select "label.collection_check_boxes[for=user_active_category_2]"
end
- test 'collection check boxes does not include the input class in the respective label' do
- collection = [[1, 'Category 1', {class: 'foo'}], [2, 'Category 2', {class: 'bar'}]]
+ test "collection check boxes does not include the input class in the respective label" do
+ collection = [[1, "Category 1", { class: "foo" }], [2, "Category 2", { class: "bar" }]]
with_collection_check_boxes :user, :active, collection, :second, :first
- assert_no_select 'label.foo[for=user_active_category_1]'
- assert_no_select 'label.bar[for=user_active_category_2]'
+ assert_no_select "label.foo[for=user_active_category_1]"
+ assert_no_select "label.bar[for=user_active_category_2]"
end
- test 'collection check boxes accepts selected values as :checked option' do
- collection = (1..3).map{|i| [i, "Category #{i}"] }
- with_collection_check_boxes :user, :category_ids, collection, :first, :last, :checked => [1, 3]
+ test "collection check boxes accepts selected values as :checked option" do
+ collection = (1..3).map { |i| [i, "Category #{i}"] }
+ with_collection_check_boxes :user, :category_ids, collection, :first, :last, checked: [1, 3]
assert_select 'input[type=checkbox][value="1"][checked=checked]'
assert_select 'input[type=checkbox][value="3"][checked=checked]'
assert_no_select 'input[type=checkbox][value="2"][checked=checked]'
end
- test 'collection check boxes accepts selected string values as :checked option' do
- collection = (1..3).map{|i| [i, "Category #{i}"] }
- with_collection_check_boxes :user, :category_ids, collection, :first, :last, :checked => ['1', '3']
+ test "collection check boxes accepts selected string values as :checked option" do
+ collection = (1..3).map { |i| [i, "Category #{i}"] }
+ with_collection_check_boxes :user, :category_ids, collection, :first, :last, checked: ["1", "3"]
assert_select 'input[type=checkbox][value="1"][checked=checked]'
assert_select 'input[type=checkbox][value="3"][checked=checked]'
assert_no_select 'input[type=checkbox][value="2"][checked=checked]'
end
- test 'collection check boxes accepts a single checked value' do
- collection = (1..3).map{|i| [i, "Category #{i}"] }
- with_collection_check_boxes :user, :category_ids, collection, :first, :last, :checked => 3
+ test "collection check boxes accepts a single checked value" do
+ collection = (1..3).map { |i| [i, "Category #{i}"] }
+ with_collection_check_boxes :user, :category_ids, collection, :first, :last, checked: 3
assert_select 'input[type=checkbox][value="3"][checked=checked]'
assert_no_select 'input[type=checkbox][value="1"][checked=checked]'
assert_no_select 'input[type=checkbox][value="2"][checked=checked]'
end
- test 'collection check boxes accepts selected values as :checked option and override the model values' do
+ test "collection check boxes accepts selected values as :checked option and override the model values" do
user = Struct.new(:category_ids).new(2)
- collection = (1..3).map{|i| [i, "Category #{i}"] }
+ collection = (1..3).map { |i| [i, "Category #{i}"] }
@output_buffer = fields_for(:user, user) do |p|
- p.collection_check_boxes :category_ids, collection, :first, :last, :checked => [1, 3]
+ p.collection_check_boxes :category_ids, collection, :first, :last, checked: [1, 3]
end
assert_select 'input[type=checkbox][value="1"][checked=checked]'
@@ -322,70 +390,70 @@ class FormCollectionsHelperTest < ActionView::TestCase
assert_no_select 'input[type=checkbox][value="2"][checked=checked]'
end
- test 'collection check boxes accepts multiple disabled items' do
- collection = (1..3).map{|i| [i, "Category #{i}"] }
- with_collection_check_boxes :user, :category_ids, collection, :first, :last, :disabled => [1, 3]
+ test "collection check boxes accepts multiple disabled items" do
+ collection = (1..3).map { |i| [i, "Category #{i}"] }
+ with_collection_check_boxes :user, :category_ids, collection, :first, :last, disabled: [1, 3]
assert_select 'input[type=checkbox][value="1"][disabled=disabled]'
assert_select 'input[type=checkbox][value="3"][disabled=disabled]'
assert_no_select 'input[type=checkbox][value="2"][disabled=disabled]'
end
- test 'collection check boxes accepts single disabled item' do
- collection = (1..3).map{|i| [i, "Category #{i}"] }
- with_collection_check_boxes :user, :category_ids, collection, :first, :last, :disabled => 1
+ test "collection check boxes accepts single disabled item" do
+ collection = (1..3).map { |i| [i, "Category #{i}"] }
+ with_collection_check_boxes :user, :category_ids, collection, :first, :last, disabled: 1
assert_select 'input[type=checkbox][value="1"][disabled=disabled]'
assert_no_select 'input[type=checkbox][value="3"][disabled=disabled]'
assert_no_select 'input[type=checkbox][value="2"][disabled=disabled]'
end
- test 'collection check boxes accepts a proc to disabled items' do
- collection = (1..3).map{|i| [i, "Category #{i}"] }
- with_collection_check_boxes :user, :category_ids, collection, :first, :last, :disabled => proc { |i| i.first == 1 }
+ test "collection check boxes accepts a proc to disabled items" do
+ collection = (1..3).map { |i| [i, "Category #{i}"] }
+ with_collection_check_boxes :user, :category_ids, collection, :first, :last, disabled: proc { |i| i.first == 1 }
assert_select 'input[type=checkbox][value="1"][disabled=disabled]'
assert_no_select 'input[type=checkbox][value="3"][disabled=disabled]'
assert_no_select 'input[type=checkbox][value="2"][disabled=disabled]'
end
- test 'collection check boxes accepts multiple readonly items' do
- collection = (1..3).map{|i| [i, "Category #{i}"] }
- with_collection_check_boxes :user, :category_ids, collection, :first, :last, :readonly => [1, 3]
+ test "collection check boxes accepts multiple readonly items" do
+ collection = (1..3).map { |i| [i, "Category #{i}"] }
+ with_collection_check_boxes :user, :category_ids, collection, :first, :last, readonly: [1, 3]
assert_select 'input[type=checkbox][value="1"][readonly=readonly]'
assert_select 'input[type=checkbox][value="3"][readonly=readonly]'
assert_no_select 'input[type=checkbox][value="2"][readonly=readonly]'
end
- test 'collection check boxes accepts single readonly item' do
- collection = (1..3).map{|i| [i, "Category #{i}"] }
- with_collection_check_boxes :user, :category_ids, collection, :first, :last, :readonly => 1
+ test "collection check boxes accepts single readonly item" do
+ collection = (1..3).map { |i| [i, "Category #{i}"] }
+ with_collection_check_boxes :user, :category_ids, collection, :first, :last, readonly: 1
assert_select 'input[type=checkbox][value="1"][readonly=readonly]'
assert_no_select 'input[type=checkbox][value="3"][readonly=readonly]'
assert_no_select 'input[type=checkbox][value="2"][readonly=readonly]'
end
- test 'collection check boxes accepts a proc to readonly items' do
- collection = (1..3).map{|i| [i, "Category #{i}"] }
- with_collection_check_boxes :user, :category_ids, collection, :first, :last, :readonly => proc { |i| i.first == 1 }
+ test "collection check boxes accepts a proc to readonly items" do
+ collection = (1..3).map { |i| [i, "Category #{i}"] }
+ with_collection_check_boxes :user, :category_ids, collection, :first, :last, readonly: proc { |i| i.first == 1 }
assert_select 'input[type=checkbox][value="1"][readonly=readonly]'
assert_no_select 'input[type=checkbox][value="3"][readonly=readonly]'
assert_no_select 'input[type=checkbox][value="2"][readonly=readonly]'
end
- test 'collection check boxes accepts html options' do
- collection = [[1, 'Category 1'], [2, 'Category 2']]
- with_collection_check_boxes :user, :category_ids, collection, :first, :last, {}, :class => 'check'
+ test "collection check boxes accepts html options" do
+ collection = [[1, "Category 1"], [2, "Category 2"]]
+ with_collection_check_boxes :user, :category_ids, collection, :first, :last, {}, { class: "check" }
assert_select 'input.check[type=checkbox][value="1"]'
assert_select 'input.check[type=checkbox][value="2"]'
end
- test 'collection check boxes with fields for' do
- collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
+ test "collection check boxes with fields for" do
+ collection = [Category.new(1, "Category 1"), Category.new(2, "Category 2")]
@output_buffer = fields_for(:post) do |p|
p.collection_check_boxes :category_ids, collection, :id, :name
end
@@ -393,67 +461,67 @@ class FormCollectionsHelperTest < ActionView::TestCase
assert_select 'input#post_category_ids_1[type=checkbox][value="1"]'
assert_select 'input#post_category_ids_2[type=checkbox][value="2"]'
- assert_select 'label[for=post_category_ids_1]', 'Category 1'
- assert_select 'label[for=post_category_ids_2]', 'Category 2'
+ assert_select "label[for=post_category_ids_1]", "Category 1"
+ assert_select "label[for=post_category_ids_2]", "Category 2"
end
- test 'collection check boxes does not wrap input inside the label' do
+ test "collection check boxes does not wrap input inside the label" do
with_collection_check_boxes :user, :active, [true, false], :to_s, :to_s
- assert_select 'input[type=checkbox] + label'
- assert_no_select 'label input'
+ assert_select "input[type=checkbox] + label"
+ assert_no_select "label input"
end
- test 'collection check boxes accepts a block to render the label as check box wrapper' do
+ test "collection check boxes accepts a block to render the label as check box wrapper" do
with_collection_check_boxes :user, :active, [true, false], :to_s, :to_s do |b|
b.label { b.check_box }
end
- assert_select 'label[for=user_active_true] > input#user_active_true[type=checkbox]'
- assert_select 'label[for=user_active_false] > input#user_active_false[type=checkbox]'
+ assert_select "label[for=user_active_true] > input#user_active_true[type=checkbox]"
+ assert_select "label[for=user_active_false] > input#user_active_false[type=checkbox]"
end
- test 'collection check boxes accepts a block to change the order of label and check box' do
+ test "collection check boxes accepts a block to change the order of label and check box" do
with_collection_check_boxes :user, :active, [true, false], :to_s, :to_s do |b|
b.label + b.check_box
end
- assert_select 'label[for=user_active_true] + input#user_active_true[type=checkbox]'
- assert_select 'label[for=user_active_false] + input#user_active_false[type=checkbox]'
+ assert_select "label[for=user_active_true] + input#user_active_true[type=checkbox]"
+ assert_select "label[for=user_active_false] + input#user_active_false[type=checkbox]"
end
- test 'collection check boxes with block helpers accept extra html options' do
+ test "collection check boxes with block helpers accept extra html options" do
with_collection_check_boxes :user, :active, [true, false], :to_s, :to_s do |b|
- b.label(:class => "check_box") + b.check_box(:class => "check_box")
+ b.label(class: "check_box") + b.check_box(class: "check_box")
end
- assert_select 'label.check_box[for=user_active_true] + input#user_active_true.check_box[type=checkbox]'
- assert_select 'label.check_box[for=user_active_false] + input#user_active_false.check_box[type=checkbox]'
+ assert_select "label.check_box[for=user_active_true] + input#user_active_true.check_box[type=checkbox]"
+ assert_select "label.check_box[for=user_active_false] + input#user_active_false.check_box[type=checkbox]"
end
- test 'collection check boxes with block helpers allows access to current text and value' do
+ test "collection check boxes with block helpers allows access to current text and value" do
with_collection_check_boxes :user, :active, [true, false], :to_s, :to_s do |b|
- b.label(:"data-value" => b.value) { b.check_box + b.text }
+ b.label("data-value": b.value) { b.check_box + b.text }
end
- assert_select 'label[for=user_active_true][data-value=true]', 'true' do
- assert_select 'input#user_active_true[type=checkbox]'
+ assert_select "label[for=user_active_true][data-value=true]", "true" do
+ assert_select "input#user_active_true[type=checkbox]"
end
- assert_select 'label[for=user_active_false][data-value=false]', 'false' do
- assert_select 'input#user_active_false[type=checkbox]'
+ assert_select "label[for=user_active_false][data-value=false]", "false" do
+ assert_select "input#user_active_false[type=checkbox]"
end
end
- test 'collection check boxes with block helpers allows access to the current object item in the collection to access extra properties' do
+ test "collection check boxes with block helpers allows access to the current object item in the collection to access extra properties" do
with_collection_check_boxes :user, :active, [true, false], :to_s, :to_s do |b|
- b.label(:class => b.object) { b.check_box + b.text }
+ b.label(class: b.object) { b.check_box + b.text }
end
- assert_select 'label.true[for=user_active_true]', 'true' do
- assert_select 'input#user_active_true[type=checkbox]'
+ assert_select "label.true[for=user_active_true]", "true" do
+ assert_select "input#user_active_true[type=checkbox]"
end
- assert_select 'label.false[for=user_active_false]', 'false' do
- assert_select 'input#user_active_false[type=checkbox]'
+ assert_select "label.false[for=user_active_false]", "false" do
+ assert_select "input#user_active_false[type=checkbox]"
end
end
end
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..f84c9b2b73
--- /dev/null
+++ b/actionview/test/template/form_helper/form_with_test.rb
@@ -0,0 +1,2367 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "controller/fake_models"
+
+class FormWithTest < ActionView::TestCase
+ include RenderERBUtils
+
+ setup do
+ @old_value = ActionView::Helpers::FormHelper.form_with_generates_ids
+ ActionView::Helpers::FormHelper.form_with_generates_ids = true
+ end
+
+ teardown do
+ ActionView::Helpers::FormHelper.form_with_generates_ids = @old_value
+ end
+
+ private
+ def with_default_enforce_utf8(value)
+ old_value = ActionView::Helpers::FormTagHelper.default_enforce_utf8
+ ActionView::Helpers::FormTagHelper.default_enforce_utf8 = value
+
+ yield
+ ensure
+ ActionView::Helpers::FormTagHelper.default_enforce_utf8 = old_value
+ end
+end
+
+class FormWithActsLikeFormTagTest < FormWithTest
+ 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_predicate actual, :html_safe?
+ end
+
+ def test_form_with_default_enforce_utf8_false
+ with_default_enforce_utf8 false do
+ actual = form_with
+ expected = whole_form("http://www.example.com", skip_enforcing_utf8: true)
+ assert_dom_equal expected, actual
+ assert_predicate actual, :html_safe?
+ end
+ end
+
+ def test_form_with_default_enforce_utf8_true
+ with_default_enforce_utf8 true do
+ actual = form_with
+ expected = whole_form("http://www.example.com", skip_enforcing_utf8: false)
+ assert_dom_equal expected, actual
+ assert_predicate actual, :html_safe?
+ end
+ end
+
+ def test_form_with_with_block_in_erb
+ 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}"
+ },
+ "blog/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")
+ @controller.singleton_class.include Routes.url_helpers
+ 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
+
+ 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[:controller] = "main"
+ object[: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.select(:category, %w( animal economy sports ))
+ 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' id='post_title' />" \
+ "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" \
+ "<input name='post[secret]' type='hidden' value='0' />" \
+ "<input name='post[secret]' checked='checked' type='checkbox' value='1' id='post_secret' />" \
+ "<select name='post[category]' id='post_category'><option value='animal'>animal</option>\n<option value='economy'>economy</option>\n<option value='sports'>sports</option></select>" \
+ "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />" \
+ "<button name='button' type='submit'>Create post</button>" \
+ "<button name='button' type='submit'><span>Create post</span></button>"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_not_outputting_ids
+ old_value = ActionView::Helpers::FormHelper.form_with_generates_ids
+ ActionView::Helpers::FormHelper.form_with_generates_ids = false
+
+ form_with(model: @post, id: "create-post") do |f|
+ concat f.label(:title) { "The Title" }
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ concat f.select(:category, %w( animal economy sports ))
+ concat f.submit("Create post")
+ end
+
+ expected = whole_form("/posts/123", "create-post", method: "patch") do
+ "<label>The Title</label>" \
+ "<input name='post[title]' type='text' value='Hello World' />" \
+ "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" \
+ "<input name='post[secret]' type='hidden' value='0' />" \
+ "<input name='post[secret]' checked='checked' type='checkbox' value='1' />" \
+ "<select name='post[category]'><option value='animal'>animal</option>\n<option value='economy'>economy</option>\n<option value='sports'>sports</option></select>" \
+ "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ ensure
+ ActionView::Helpers::FormHelper.form_with_generates_ids = old_value
+ end
+
+ def test_form_with_only_url_on_create
+ 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" id="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" id="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" id="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]" id="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]" id="other_name_private_property">', f.hidden_field(:private_property)
+ assert_dom_equal '<input type="hidden" name="other_name[protected_property]" id="other_name_protected_property">', f.hidden_field(:protected_property)
+ end
+ end
+
+ def test_form_with_with_collection_select
+ post = Post.new
+ def post.active; false; end
+ form_with(model: post) do |f|
+ concat f.collection_select(:active, [true, false], :to_s, :to_s)
+ end
+
+ expected = whole_form("/posts") do
+ "<select name='post[active]' id='post_active'>" \
+ "<option value='true'>true</option>\n" \
+ "<option selected='selected' value='false'>false</option>" \
+ "</select>"
+ end
+
+ assert_dom_equal expected, output_buffer
+ 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' id='post_active_true' />" \
+ "<label for='post_active_true'>true</label>" \
+ "<input checked='checked' name='post[active]' type='radio' value='false' id='post_active_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' id='post_active_true' />" \
+ "true</label>" \
+ "<label for='post_active_false'>" \
+ "<input checked='checked' name='post[active]' type='radio' value='false' id='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_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' id='post_active_true' />" \
+ "true</label>" \
+ "<label for='post_active_false'>" \
+ "<input checked='checked' name='post[active]' type='radio' value='false' id='post_active_false' />" \
+ "false</label>" \
+ "<input name='post[id]' type='hidden' value='1' id='post_id' />"
+ 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' id='post_1_active_true' />" \
+ "<label for='post_1_active_true'>true</label>" \
+ "<input checked='checked' name='post[1][active]' type='radio' value='false' id='post_1_active_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' id='post_tag_ids_1' />" \
+ "<label for='post_tag_ids_1'>Tag 1</label>" \
+ "<input name='post[tag_ids][]' type='checkbox' value='2' id='post_tag_ids_2' />" \
+ "<label for='post_tag_ids_2'>Tag 2</label>" \
+ "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='3' id='post_tag_ids_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' id='post_tag_ids_1' />" \
+ "Tag 1</label>" \
+ "<label for='post_tag_ids_2'>" \
+ "<input name='post[tag_ids][]' type='checkbox' value='2' id='post_tag_ids_2' />" \
+ "Tag 2</label>" \
+ "<label for='post_tag_ids_3'>" \
+ "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='3' id='post_tag_ids_3' />" \
+ "Tag 3</label>"
+ 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' id='post_tag_ids_1' />" \
+ "Tag 1</label>" \
+ "<label for='post_tag_ids_2'>" \
+ "<input name='post[tag_ids][]' type='checkbox' value='2' id='post_tag_ids_2' />" \
+ "Tag 2</label>" \
+ "<label for='post_tag_ids_3'>" \
+ "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='3' id='post_tag_ids_3' />" \
+ "Tag 3</label>" \
+ "<input name='post[id]' type='hidden' value='1' id='post_id' />"
+ 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' id='post_1_tag_ids_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' id='post_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' id='post_comment_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.' id='post_title' />" \
+ "<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' id='other_name_title' />" \
+ "<textarea name='other_name[body]' id='other_name_body'>\nBack to the hill and over it again!</textarea>" \
+ "<input name='other_name[secret]' value='0' type='hidden' />" \
+ "<input name='other_name[secret]' checked='checked' value='1' type='checkbox' id='other_name_secret' />" \
+ "<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' id='post_title' />" \
+ "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" \
+ "<input name='post[secret]' type='hidden' value='0' />" \
+ "<input name='post[secret]' checked='checked' type='checkbox' value='1' id='post_secret'/>"
+ 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' id='post_title' />" \
+ "<textarea name='post[body]' id='post_body' >\nBack to the hill and over it again!</textarea>" \
+ "<input name='post[secret]' type='hidden' value='0' />" \
+ "<input name='post[secret]' checked='checked' type='checkbox' value='1' id='post_secret' />"
+ 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' id='post_title' />"
+ 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' id='post_title' />" \
+ "<textarea name='post[body]' id='post_body' >\nBack to the hill and over it again!</textarea>" \
+ "<input name='post[secret]' type='hidden' value='0' />" \
+ "<input name='post[secret]' checked='checked' type='checkbox' value='1' id='post_secret' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_is_not_remote_by_default_if_form_with_generates_remote_forms_is_false
+ old_value = ActionView::Helpers::FormHelper.form_with_generates_remote_forms
+ ActionView::Helpers::FormHelper.form_with_generates_remote_forms = false
+
+ 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", local: true) do
+ "<input name='post[title]' type='text' value='Hello World' id='post_title' />" \
+ "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" \
+ "<input name='post[secret]' type='hidden' value='0' />" \
+ "<input name='post[secret]' checked='checked' type='checkbox' value='1' id='post_secret' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ ensure
+ ActionView::Helpers::FormHelper.form_with_generates_remote_forms = old_value
+ 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' id='post_title' />"
+ 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' id='post_title' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_default_enforce_utf8_true
+ with_default_enforce_utf8 true do
+ form_with(scope: :post) do |f|
+ concat f.text_field(:title)
+ end
+
+ expected = whole_form("/", skip_enforcing_utf8: false) do
+ "<input name='post[title]' type='text' value='Hello World' id='post_title' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+ end
+
+ def test_form_with_default_enforce_utf8_false
+ with_default_enforce_utf8 false do
+ form_with(scope: :post) do |f|
+ concat f.text_field(:title)
+ end
+
+ expected = whole_form("/", skip_enforcing_utf8: true) do
+ "<input name='post[title]' type='text' value='Hello World' id='post_title' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+ end
+
+ def test_form_with_without_object
+ form_with(scope: :post, id: "create-post") do |f|
+ concat f.text_field(:title)
+ 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' id='post_title' />" \
+ "<textarea name='post[body]' id='post_body' >\nBack to the hill and over it again!</textarea>" \
+ "<input name='post[secret]' type='hidden' value='0' />" \
+ "<input name='post[secret]' checked='checked' type='checkbox' value='1' id='post_secret' />"
+ 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' id='post_123_title' />" \
+ "<textarea name='post[123][body]' id='post_123_body'>\nBack to the hill and over it again!</textarea>" \
+ "<input name='post[123][secret]' type='hidden' value='0' />" \
+ "<input name='post[123][secret]' checked='checked' type='checkbox' value='1' id='post_123_secret' />"
+ 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' id='post__title' />" \
+ "<textarea name='post[][body]' id='post__body' >\nBack to the hill and over it again!</textarea>" \
+ "<input name='post[][secret]' type='hidden' value='0' />" \
+ "<input name='post[][secret]' checked='checked' type='checkbox' value='1' id='post__secret' />"
+ 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='' id='post_author_name' /></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='' id='post_author_name' /></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_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_which_is_overwritten_by_scope_option
+ 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_submit_with_object_which_is_namespaced
+ blog_post = Blog::Post.new("And his name will be forty and four.", 44)
+ with_locale :submit do
+ form_with(model: blog_post) do |f|
+ concat f.submit
+ end
+
+ expected = whole_form("/posts/44", method: "patch") do
+ "<input name='commit' data-disable-with='Update your Post' type='submit' value='Update your Post' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+ end
+
+ def test_fields_with_attributes_not_on_model
+ form_with(model: @post) do |f|
+ concat f.fields(:comment) { |c|
+ 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]" id="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" id="posts_post_0_comment_1_dont_exist_on_model" >'
+ 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' id='post_comment_body' />"
+ 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' id='posts_post_0_comment_1_name' />"
+ 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' id='post_123_title' />" \
+ "<input name='post[123][comment][][name]' type='text' value='new comment' id='post_123_comment__name' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ 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' id='post_1_title' />" \
+ "<input name='post[1][comment][1][name]' type='text' value='new comment' id='post_1_comment_1_name' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ 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' id='post_1_comment_title' />"
+ 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' id='post_1_comment_5_title' />"
+ 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' id='post_123_comment_title' />"
+ 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' id='post_comment_5_title_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' id='post_123_comment_123_title' />"
+ 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' id='post_123_comment_5_title' />"
+ end + whole_form("/posts/123", method: "patch") do
+ "<input name='post[1][comment][123][title]' type='text' value='Hello World' id='post_1_comment_123_title' />"
+ 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" id="post_title" />' \
+ '<input name="post[author_attributes][name]" type="text" value="new author" id="post_author_attributes_name" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ 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" id="post_title" />' \
+ '<input name="post[author_attributes][name]" type="text" value="author #321" id="post_author_attributes_name" />' \
+ '<input name="post[author_attributes][id]" type="hidden" value="321" id="post_author_attributes_id" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ 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" id="post_title" />' \
+ '<input name="post[author_attributes][name]" type="text" value="author #321" id="post_author_attributes_name" />' \
+ '<input name="post[author_attributes][id]" type="hidden" value="321" id="post_author_attributes_id" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ 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" id="post_title" />' \
+ '<input name="post[author_attributes][name]" type="text" value="author #321" id="post_author_attributes_name" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ 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" id="post_title" />' \
+ '<input name="post[author_attributes][name]" type="text" value="author #321" id="post_author_attributes_name" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ 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" id="post_title" />' \
+ '<input name="post[author_attributes][name]" type="text" value="author #321" id="post_author_attributes_name" />' \
+ '<input name="post[author_attributes][id]" type="hidden" value="321" id="post_author_attributes_id" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ 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" id="post_title" />' \
+ '<input name="post[author_attributes][id]" type="hidden" value="321" id="post_author_attributes_id" />' \
+ '<input name="post[author_attributes][name]" type="text" value="author #321" id="post_author_attributes_name" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ 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" id="post_title" />' \
+ '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" id="post_comments_attributes_0_name" />' \
+ '<input name="post[comments_attributes][0][id]" type="hidden" value="1" id="post_comments_attributes_0_id" />' \
+ '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" id="post_comments_attributes_1_name" />' \
+ '<input name="post[comments_attributes][1][id]" type="hidden" value="2" id="post_comments_attributes_1_id" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ 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" id="post_title" />' \
+ '<input name="post[author_attributes][name]" type="text" value="author #321" id="post_author_attributes_name" />' \
+ '<input name="post[author_attributes][id]" type="hidden" value="321" id="post_author_attributes_id" />' \
+ '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" id="post_comments_attributes_0_name" />' \
+ '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" id="post_comments_attributes_1_name" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ 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" id="post_title" />' \
+ '<input name="post[author_attributes][name]" type="text" value="author #321" id="post_author_attributes_name" />' \
+ '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" id="post_comments_attributes_0_name" />' \
+ '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" id="post_comments_attributes_1_name" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ 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" id="post_title" />' \
+ '<input name="post[author_attributes][name]" type="text" value="author #321" id="post_author_attributes_name" />' \
+ '<input name="post[author_attributes][id]" type="hidden" value="321" id="post_author_attributes_id" />' \
+ '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" id="post_comments_attributes_0_name" />' \
+ '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" id="post_comments_attributes_1_name" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ 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" id="post_title" />' \
+ '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" id="post_comments_attributes_0_name" />' \
+ '<input name="post[comments_attributes][0][id]" type="hidden" value="1" id="post_comments_attributes_0_id" />' \
+ '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" id="post_comments_attributes_1_name" />' \
+ '<input name="post[comments_attributes][1][id]" type="hidden" value="2" id="post_comments_attributes_1_id" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ 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" id="post_title" />' \
+ '<input name="post[comments_attributes][0][id]" type="hidden" value="1" id="post_comments_attributes_0_id" />' \
+ '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" id="post_comments_attributes_0_name" />' \
+ '<input name="post[comments_attributes][1][id]" type="hidden" value="2" id="post_comments_attributes_1_id" />' \
+ '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" id="post_comments_attributes_1_name" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ 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" id="post_title" />' \
+ '<input name="post[comments_attributes][0][name]" type="text" value="new comment" id="post_comments_attributes_0_name" />' \
+ '<input name="post[comments_attributes][1][name]" type="text" value="new comment" id="post_comments_attributes_1_name" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ 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" id="post_title" />' \
+ '<input name="post[comments_attributes][0][name]" type="text" value="comment #321" id="post_comments_attributes_0_name" />' \
+ '<input name="post[comments_attributes][0][id]" type="hidden" value="321" id="post_comments_attributes_0_id"/>' \
+ '<input name="post[comments_attributes][1][name]" type="text" value="new comment" id="post_comments_attributes_1_name" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ 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" id="post_title" />'
+ 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" id="post_title" />' \
+ '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" id="post_comments_attributes_0_name" />' \
+ '<input name="post[comments_attributes][0][id]" type="hidden" value="1" id="post_comments_attributes_0_id" />' \
+ '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" id="post_comments_attributes_1_name" />' \
+ '<input name="post[comments_attributes][1][id]" type="hidden" value="2" id="post_comments_attributes_1_id" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ 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" id="post_title" />' \
+ '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" id="post_comments_attributes_0_name" />' \
+ '<input name="post[comments_attributes][0][id]" type="hidden" value="1" id="post_comments_attributes_0_id" />' \
+ '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" id="post_comments_attributes_1_name" />' \
+ '<input name="post[comments_attributes][1][id]" type="hidden" value="2" id="post_comments_attributes_1_id" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ 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" id="post_title" />' \
+ '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" id="post_comments_attributes_0_name" />' \
+ '<input name="post[comments_attributes][0][id]" type="hidden" value="1" id="post_comments_attributes_0_id" />' \
+ '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" id="post_comments_attributes_1_name" />' \
+ '<input name="post[comments_attributes][1][id]" type="hidden" value="2" id="post_comments_attributes_1_id" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ 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" id="post_title" />' \
+ '<input name="post[comments_attributes][0][name]" type="text" value="comment #321" id="post_comments_attributes_0_name" />' \
+ '<input name="post[comments_attributes][0][id]" type="hidden" value="321" id="post_comments_attributes_0_id" />' \
+ '<input name="post[comments_attributes][1][name]" type="text" value="new comment" id="post_comments_attributes_1_name" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ 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" id="post_comments_attributes_abc_name" />' \
+ '<input name="post[comments_attributes][abc][id]" type="hidden" value="321" id="post_comments_attributes_abc_id" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ 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" id="post_comments_attributes_abc_name" />' \
+ '<input name="post[comments_attributes][abc][id]" type="hidden" value="321" id="post_comments_attributes_abc_id" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ 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" id="post_comments_attributes_abc_name" />' \
+ '<input name="post[comments_attributes][abc][id]" type="hidden" value="321" id="post_comments_attributes_abc_id" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ 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 expected, cf.index
+ 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 expected, cf.index
+ 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 expected, cf.index
+ 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 "abc", cf.index
+ }
+ 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" id="post_comments_attributes_0_name" />' \
+ '<input name="post[comments_attributes][0][relevances_attributes][0][value]" type="text" value="commentrelevance #314" id="post_comments_attributes_0_relevances_attributes_0_value" />' \
+ '<input name="post[comments_attributes][0][relevances_attributes][0][id]" type="hidden" value="314" id="post_comments_attributes_0_relevances_attributes_0_id"/>' \
+ '<input name="post[comments_attributes][0][id]" type="hidden" value="321" id="post_comments_attributes_0_id"/>' \
+ '<input name="post[tags_attributes][0][value]" type="text" value="tag #123" id="post_tags_attributes_0_value"/>' \
+ '<input name="post[tags_attributes][0][relevances_attributes][0][value]" type="text" value="tagrelevance #3141" id="post_tags_attributes_0_relevances_attributes_0_value"/>' \
+ '<input name="post[tags_attributes][0][relevances_attributes][0][id]" type="hidden" value="3141" id="post_tags_attributes_0_relevances_attributes_0_id"/>' \
+ '<input name="post[tags_attributes][0][id]" type="hidden" value="123" id="post_tags_attributes_0_id"/>' \
+ '<input name="post[tags_attributes][1][value]" type="text" value="tag #456" id="post_tags_attributes_1_value"/>' \
+ '<input name="post[tags_attributes][1][relevances_attributes][0][value]" type="text" value="tagrelevance #31415" id="post_tags_attributes_1_relevances_attributes_0_value"/>' \
+ '<input name="post[tags_attributes][1][relevances_attributes][0][id]" type="hidden" value="31415" id="post_tags_attributes_1_relevances_attributes_0_id"/>' \
+ '<input name="post[tags_attributes][1][id]" type="hidden" value="456" id="post_tags_attributes_1_id"/>'
+ end
+
+ assert_dom_equal expected, output_buffer
+ 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" id="post_author_attributes_name" />'
+ 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' id='post_title' />" \
+ "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" \
+ "<input name='post[secret]' type='hidden' value='0' />" \
+ "<input name='post[secret]' checked='checked' type='checkbox' value='1' id='post_secret' />"
+
+ 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' id='post_123_title' />" \
+ "<textarea name='post[123][body]' id='post_123_body'>\nBack to the hill and over it again!</textarea>" \
+ "<input name='post[123][secret]' type='hidden' value='0' />" \
+ "<input name='post[123][secret]' checked='checked' type='checkbox' value='1' id='post_123_secret' />"
+
+ 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' id='post__title' />" \
+ "<textarea name='post[][body]' id='post__body'>\nBack to the hill and over it again!</textarea>" \
+ "<input name='post[][secret]' type='hidden' value='0' />" \
+ "<input name='post[][secret]' checked='checked' type='checkbox' value='1' id='post__secret' />"
+
+ 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' id='post_abc_title' />" \
+ "<textarea name='post[abc][body]' id='post_abc_body'>\nBack to the hill and over it again!</textarea>" \
+ "<input name='post[abc][secret]' type='hidden' value='0' />" \
+ "<input name='post[abc][secret]' checked='checked' type='checkbox' value='1' id='post_abc_secret' />"
+
+ 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' id='post_title' />" \
+ "<textarea name='post[body]' id='post_body' >\nBack to the hill and over it again!</textarea>" \
+ "<input name='post[secret]' type='hidden' value='0' />" \
+ "<input name='post[secret]' checked='checked' type='checkbox' value='1' id='post_secret' />"
+
+ 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' id='post_title' />" \
+ "<textarea name='post[body]' id='post_body' >\nBack to the hill and over it again!</textarea>" \
+ "<input name='post[secret]' type='hidden' value='0' />" \
+ "<input name='post[secret]' checked='checked' type='checkbox' value='1' id='post_secret' />"
+
+ 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' id='author_post_title' id='author_post_1_title' />",
+ 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' id='author_post_1_title' />",
+ 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' id='post_title' />" \
+ "<textarea name='post[body]' id='post_body' >\nBack to the hill and over it again!</textarea>" \
+ "<input name='parent_post[secret]' type='hidden' value='0' />" \
+ "<input name='parent_post[secret]' checked='checked' type='checkbox' value='1' id='parent_post_secret' />"
+ 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' id='post_title' />" \
+ "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" \
+ "<input name='post[comment][name]' type='text' value='new comment' id='post_comment_name' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ 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' id='post_category_name' />"
+ 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' id='post_title'/><br/>" \
+ "<label for='body'>Body:</label> <textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea><br/>" \
+ "<label for='secret'>Secret:</label> <input name='post[secret]' type='hidden' value='0' /><input name='post[secret]' checked='checked' type='checkbox' value='1' id='post_secret' /><br/>"
+ end
+
+ assert_dom_equal expected, output_buffer
+ 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' id='post_title' /><br/>" \
+ "<label for='body'>Body:</label> <textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea><br/>" \
+ "<label for='secret'>Secret:</label> <input name='post[secret]' type='hidden' value='0' /><input name='post[secret]' checked='checked' type='checkbox' value='1' id='post_secret' /><br/>"
+ end
+
+ assert_dom_equal expected, output_buffer
+ 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' id='post_title' /><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' id='post_title' /><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' id='post_title' /><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' id='post_title'/><br/>" \
+ "<label for='body'>Body:</label> <textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea><br/>" \
+ "<label for='secret'>Secret:</label> <input name='post[secret]' type='hidden' value='0' /><input name='post[secret]' checked='checked' type='checkbox' value='1' id='post_secret' /><br/>"
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ 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 b8cb5bd746..5972946074 100644
--- a/actionview/test/template/form_helper_test.rb
+++ b/actionview/test/template/form_helper_test.rb
@@ -1,11 +1,23 @@
-require 'abstract_unit'
-require 'controller/fake_models'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "controller/fake_models"
class FormHelperTest < ActionView::TestCase
include RenderERBUtils
tests ActionView::Helpers::FormHelper
+ class WithActiveStorageRoutesControllers < ActionController::Base
+ test_routes do
+ post "/rails/active_storage/direct_uploads" => "active_storage/direct_uploads#create", as: :rails_direct_uploads
+ end
+
+ def url_options
+ { host: "testtwo.host" }
+ end
+ end
+
def form_for(*)
@output_buffer = super
end
@@ -16,13 +28,13 @@ class FormHelperTest < ActionView::TestCase
setup do
# Create "label" locale for testing I18n label helpers
- I18n.backend.store_translations 'label', {
+ I18n.backend.store_translations "label",
activemodel: {
attributes: {
post: {
cost: "Total cost"
},
- :"post/language" => {
+ "post/language": {
spanish: "Espanol"
}
}
@@ -42,33 +54,34 @@ class FormHelperTest < ActionView::TestCase
value: "Tag"
},
post_delegate: {
- title: 'Delegate model_name title'
+ title: "Delegate model_name title"
}
}
}
- }
# Create "submit" locale for testing I18n submit helpers
- I18n.backend.store_translations 'submit', {
+ I18n.backend.store_translations "submit",
helpers: {
submit: {
- create: 'Create %{model}',
- update: 'Confirm %{model} changes',
- submit: 'Save changes',
+ create: "Create %{model}",
+ update: "Confirm %{model} changes",
+ submit: "Save changes",
another_post: {
- update: 'Update your %{model}'
+ update: "Update your %{model}"
+ },
+ "blog/post": {
+ update: "Update your %{model}"
}
}
}
- }
- I18n.backend.store_translations 'placeholder', {
+ I18n.backend.store_translations "placeholder",
activemodel: {
attributes: {
post: {
cost: "Total cost"
},
- :"post/cost" => {
+ "post/cost": {
uk: "Pounds"
}
}
@@ -85,18 +98,17 @@ class FormHelperTest < ActionView::TestCase
}
},
post_delegate: {
- title: 'Delegate model_name title'
+ title: "Delegate model_name title"
},
tag: {
value: "Tag"
}
}
}
- }
@post = Post.new
@comment = Comment.new
- def @post.errors()
+ def @post.errors
Class.new {
def [](field); field == "author_name" ? ["can't be empty"] : [] end
def empty?() false end
@@ -108,7 +120,7 @@ class FormHelperTest < ActionView::TestCase
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
+ def @post.to_param; "123"; end
@post.persisted = true
@post.title = "Hello World"
@@ -125,9 +137,10 @@ class FormHelperTest < ActionView::TestCase
@post_delegator = PostDelegator.new
- @post_delegator.title = 'Hello World'
+ @post_delegator.title = "Hello World"
@car = Car.new("#000FFF")
+ @controller.singleton_class.include Routes.url_helpers
end
Routes = ActionDispatch::Routing::RouteSet.new
@@ -156,7 +169,8 @@ class FormHelperTest < ActionView::TestCase
@url_for_options = object
if object.is_a?(Hash) && object[:use_route].blank? && object[:controller].blank?
- object.merge!(controller: "main", action: "index")
+ object[:controller] = "main"
+ object[:action] = "index"
end
super
@@ -178,7 +192,7 @@ class FormHelperTest < ActionView::TestCase
)
assert_dom_equal(
'<label class="title_label" for="post_title">Title</label>',
- label("post", "title", nil, class: 'title_label')
+ label("post", "title", nil, class: "title_label")
)
assert_dom_equal('<label for="post_secret">Secret?</label>', label("post", "secret?"))
end
@@ -229,7 +243,7 @@ class FormHelperTest < ActionView::TestCase
def test_label_with_locales_and_nested_attributes
with_locale :label do
- form_for(@post, html: { id: 'create-post' }) do |f|
+ form_for(@post, html: { id: "create-post" }) do |f|
f.fields_for(:comments) do |cf|
concat cf.label(:body)
end
@@ -245,7 +259,7 @@ class FormHelperTest < ActionView::TestCase
def test_label_with_locales_fallback_and_nested_attributes
with_locale :label do
- form_for(@post, html: { id: 'create-post' }) do |f|
+ form_for(@post, html: { id: "create-post" }) do |f|
f.fields_for(:tags) do |cf|
concat cf.label(:value)
end
@@ -260,11 +274,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
@@ -280,7 +294,7 @@ class FormHelperTest < ActionView::TestCase
end
def test_label_does_not_generate_for_attribute_when_given_nil
- assert_dom_equal('<label>Title</label>', label(:post, :title, for: nil))
+ assert_dom_equal("<label>Title</label>", label(:post, :title, for: nil))
end
def test_label_with_id_attribute_as_symbol
@@ -332,7 +346,7 @@ class FormHelperTest < ActionView::TestCase
def test_label_with_block_and_html
assert_dom_equal(
'<label for="post_terms">Accept <a href="/terms">Terms</a>.</label>',
- label(:post, :terms) { 'Accept <a href="/terms">Terms</a>.'.html_safe }
+ label(:post, :terms) { raw('Accept <a href="/terms">Terms</a>.') }
)
end
@@ -347,7 +361,7 @@ class FormHelperTest < ActionView::TestCase
with_locale :label do
assert_dom_equal(
'<label for="post_body"><b>Write entire text here</b></label>',
- label(:post, :body) { |b| "<b>#{b.translation}</b>".html_safe }
+ label(:post, :body) { |b| raw("<b>#{b.translation}</b>") }
)
end
end
@@ -366,7 +380,7 @@ class FormHelperTest < ActionView::TestCase
)
end
- def test_label_with_to_model_and_overriden_model_name
+ def test_label_with_to_model_and_overridden_model_name
with_locale :label do
assert_dom_equal(
%{<label for="post_delegator_title">Delegate model_name title</label>},
@@ -429,7 +443,7 @@ class FormHelperTest < ActionView::TestCase
def test_text_field_placeholder_with_locales_and_nested_attributes
with_locale :placeholder do
- form_for(@post, html: { id: 'create-post' }) do |f|
+ form_for(@post, html: { id: "create-post" }) do |f|
f.fields_for(:comments) do |cf|
concat cf.text_field(:body, placeholder: true)
end
@@ -445,7 +459,7 @@ class FormHelperTest < ActionView::TestCase
def test_text_field_placeholder_with_locales_fallback_and_nested_attributes
with_locale :placeholder do
- form_for(@post, html: { id: 'create-post' }) do |f|
+ form_for(@post, html: { id: "create-post" }) do |f|
f.fields_for(:tags) do |cf|
concat cf.text_field(:value, placeholder: true)
end
@@ -523,39 +537,45 @@ class FormHelperTest < ActionView::TestCase
end
def test_text_field_doesnt_change_param_values
- object_name = 'post[]'
+ object_name = "post[]"
expected = '<input id="post_123_title" name="post[123][title]" type="text" value="Hello World" />'
assert_dom_equal expected, text_field(object_name, "title")
end
- def test_file_field_does_generate_a_hidden_field
- expected = '<input name="user[avatar]" type="hidden" value="" /><input id="user_avatar" name="user[avatar]" type="file" />'
+ def test_file_field_has_no_size
+ expected = '<input id="user_avatar" name="user[avatar]" type="file" />'
assert_dom_equal expected, file_field("user", "avatar")
end
- def test_file_field_does_not_generate_a_hidden_field_if_included_hidden_option_is_false
- expected = '<input id="user_avatar" name="user[avatar]" type="file" />'
- assert_dom_equal expected, file_field("user", "avatar", include_hidden: false)
+ def test_file_field_with_multiple_behavior
+ expected = '<input id="import_file" multiple="multiple" name="import[file][]" type="file" />'
+ assert_dom_equal expected, file_field("import", "file", multiple: true)
end
- def test_file_field_does_not_generate_a_hidden_field_if_included_hidden_option_is_false_with_key_as_string
- expected = '<input id="user_avatar" name="user[avatar]" type="file" />'
- assert_dom_equal expected, file_field("user", "avatar", "include_hidden" => false)
+ def test_file_field_with_multiple_behavior_and_explicit_name
+ expected = '<input id="import_file" multiple="multiple" name="custom" type="file" />'
+ assert_dom_equal expected, file_field("import", "file", multiple: true, name: "custom")
end
- def test_file_field_has_no_size
- expected = '<input name="user[avatar]" type="hidden" value="" /><input id="user_avatar" name="user[avatar]" type="file" />'
- assert_dom_equal expected, file_field("user", "avatar")
+ def test_file_field_with_direct_upload_when_rails_direct_uploads_url_is_not_defined
+ expected = '<input type="file" name="import[file]" id="import_file" />'
+ assert_dom_equal expected, file_field("import", "file", direct_upload: true)
end
- def test_file_field_with_multiple_behavior
- expected = '<input name="import[file][]" type="hidden" value="" /><input id="import_file" multiple="multiple" name="import[file][]" type="file" />'
- assert_dom_equal expected, file_field("import", "file", :multiple => true)
+ def test_file_field_with_direct_upload_when_rails_direct_uploads_url_is_defined
+ @controller = WithActiveStorageRoutesControllers.new
+
+ expected = '<input data-direct-upload-url="http://testtwo.host/rails/active_storage/direct_uploads" type="file" name="import[file]" id="import_file" />'
+ assert_dom_equal expected, file_field("import", "file", direct_upload: true)
end
- def test_file_field_with_multiple_behavior_and_explicit_name
- expected = '<input name="custom" type="hidden" value="" /><input id="import_file" multiple="multiple" name="custom" type="file" />'
- assert_dom_equal expected, file_field("import", "file", :multiple => true, :name => "custom")
+ def test_file_field_with_direct_upload_dont_mutate_arguments
+ original_options = { class: "pix", direct_upload: true }
+
+ expected = '<input class="pix" type="file" name="import[file]" id="import_file" />'
+ assert_dom_equal expected, file_field("import", "file", original_options)
+
+ assert_equal({ class: "pix", direct_upload: true }, original_options)
end
def test_hidden_field
@@ -597,7 +617,7 @@ class FormHelperTest < ActionView::TestCase
end
def test_check_box_is_html_safe
- assert check_box("post", "secret").html_safe?
+ assert_predicate check_box("post", "secret"), :html_safe?
end
def test_check_box_checked_if_object_value_is_same_that_check_value
@@ -618,7 +638,7 @@ class FormHelperTest < ActionView::TestCase
def test_check_box_checked_if_option_checked_is_present
assert_dom_equal(
'<input name="post[secret]" type="hidden" value="0" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="1" />',
- check_box("post", "secret", "checked"=>"checked")
+ check_box("post", "secret", "checked" => "checked")
)
end
@@ -636,19 +656,19 @@ class FormHelperTest < ActionView::TestCase
end
def test_check_box_checked_if_object_value_includes_checked_value
- @post.secret = ['0']
+ @post.secret = ["0"]
assert_dom_equal(
'<input name="post[secret]" type="hidden" value="0" /><input id="post_secret" name="post[secret]" type="checkbox" value="1" />',
check_box("post", "secret")
)
- @post.secret = ['1']
+ @post.secret = ["1"]
assert_dom_equal(
'<input name="post[secret]" type="hidden" value="0" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="1" />',
check_box("post", "secret")
)
- @post.secret = Set.new(['1'])
+ @post.secret = Set.new(["1"])
assert_dom_equal(
'<input name="post[secret]" type="hidden" value="0" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="1" />',
check_box("post", "secret")
@@ -732,19 +752,19 @@ class FormHelperTest < ActionView::TestCase
end
def test_check_box_with_explicit_checked_and_unchecked_values_when_object_value_is_big_decimal
- @post.secret = BigDecimal.new(0)
+ @post.secret = BigDecimal(0)
assert_dom_equal(
'<input name="post[secret]" type="hidden" value="1" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="0" />',
check_box("post", "secret", {}, 0, 1)
)
- @post.secret = BigDecimal.new(1)
+ @post.secret = BigDecimal(1)
assert_dom_equal(
'<input name="post[secret]" type="hidden" value="1" /><input id="post_secret" name="post[secret]" type="checkbox" value="0" />',
check_box("post", "secret", {}, 0, 1)
)
- @post.secret = BigDecimal.new(2.2, 1)
+ @post.secret = BigDecimal(2.2, 1)
assert_dom_equal(
'<input name="post[secret]" type="hidden" value="1" /><input id="post_secret" name="post[secret]" type="checkbox" value="0" />',
check_box("post", "secret", {}, 0, 1)
@@ -760,11 +780,11 @@ class FormHelperTest < ActionView::TestCase
end
def test_check_box_with_nil_unchecked_value_is_html_safe
- assert check_box("post", "secret", {}, "on", nil).html_safe?
+ assert_predicate check_box("post", "secret", {}, "on", nil), :html_safe?
end
def test_check_box_with_multiple_behavior
- @post.comment_ids = [2,3]
+ @post.comment_ids = [2, 3]
assert_dom_equal(
'<input name="post[comment_ids][]" type="hidden" value="0" /><input id="post_comment_ids_1" name="post[comment_ids][]" type="checkbox" value="1" />',
check_box("post", "comment_ids", { multiple: true }, 1)
@@ -776,7 +796,7 @@ class FormHelperTest < ActionView::TestCase
end
def test_check_box_with_multiple_behavior_and_index
- @post.comment_ids = [2,3]
+ @post.comment_ids = [2, 3]
assert_dom_equal(
'<input name="post[foo][comment_ids][]" type="hidden" value="0" /><input id="post_foo_comment_ids_1" name="post[foo][comment_ids][]" type="checkbox" value="1" />',
check_box("post", "comment_ids", { multiple: true, index: "foo" }, 1)
@@ -785,13 +805,12 @@ 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
assert_dom_equal(
'<input name="post[secret]" type="hidden" value="0" disabled="disabled"/><input checked="checked" disabled="disabled" id="post_secret" name="post[secret]" type="checkbox" value="1" />',
- check_box("post", "secret", { disabled: true })
+ check_box("post", "secret", disabled: true)
)
end
@@ -826,9 +845,9 @@ class FormHelperTest < ActionView::TestCase
end
def test_radio_button_respects_passed_in_id
- assert_dom_equal('<input checked="checked" id="foo" name="post[secret]" type="radio" value="1" />',
- radio_button("post", "secret", "1", id: "foo")
- )
+ assert_dom_equal('<input checked="checked" id="foo" name="post[secret]" type="radio" value="1" />',
+ radio_button("post", "secret", "1", id: "foo")
+ )
end
def test_radio_button_with_booleans
@@ -897,7 +916,7 @@ class FormHelperTest < ActionView::TestCase
def test_text_area_placeholder_with_locales_and_nested_attributes
with_locale :placeholder do
- form_for(@post, html: { id: 'create-post' }) do |f|
+ form_for(@post, html: { id: "create-post" }) do |f|
f.fields_for(:comments) do |cf|
concat cf.text_area(:body, placeholder: true)
end
@@ -913,7 +932,7 @@ class FormHelperTest < ActionView::TestCase
def test_text_area_placeholder_with_locales_fallback_and_nested_attributes
with_locale :placeholder do
- form_for(@post, html: { id: 'create-post' }) do |f|
+ form_for(@post, html: { id: "create-post" }) do |f|
f.fields_for(:tags) do |cf|
concat cf.text_area(:value, placeholder: true)
end
@@ -1051,14 +1070,14 @@ class FormHelperTest < ActionView::TestCase
def test_date_field_with_value_attr
expected = %{<input id="post_written_on" name="post[written_on]" type="date" value="2013-06-29" />}
- value = Date.new(2013,6,29)
+ value = Date.new(2013, 6, 29)
assert_dom_equal(expected, date_field("post", "written_on", value: value))
end
def test_date_field_with_timewithzone_value
- previous_time_zone, Time.zone = Time.zone, 'UTC'
+ previous_time_zone, Time.zone = Time.zone, "UTC"
expected = %{<input id="post_written_on" name="post[written_on]" type="date" value="2004-06-15" />}
- @post.written_on = Time.zone.parse('2004-06-15 15:30:45')
+ @post.written_on = Time.zone.parse("2004-06-15 15:30:45")
assert_dom_equal(expected, date_field("post", "written_on"))
ensure
Time.zone = previous_time_zone
@@ -1107,9 +1126,9 @@ class FormHelperTest < ActionView::TestCase
end
def test_time_field_with_timewithzone_value
- previous_time_zone, Time.zone = Time.zone, 'UTC'
+ previous_time_zone, Time.zone = Time.zone, "UTC"
expected = %{<input id="post_written_on" name="post[written_on]" type="time" value="01:02:03.000" />}
- @post.written_on = Time.zone.parse('2004-06-15 01:02:03')
+ @post.written_on = Time.zone.parse("2004-06-15 01:02:03")
assert_dom_equal(expected, time_field("post", "written_on"))
ensure
Time.zone = previous_time_zone
@@ -1138,18 +1157,18 @@ class FormHelperTest < ActionView::TestCase
end
def test_datetime_field
- expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2004-06-15T00:00:00.000+0000" />}
+ expected = %{<input id="post_written_on" name="post[written_on]" type="datetime-local" value="2004-06-15T00:00:00" />}
assert_dom_equal(expected, datetime_field("post", "written_on"))
end
def test_datetime_field_with_datetime_value
- expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2004-06-15T01:02:03.000+0000" />}
+ expected = %{<input id="post_written_on" name="post[written_on]" type="datetime-local" value="2004-06-15T01:02:03" />}
@post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
assert_dom_equal(expected, datetime_field("post", "written_on"))
end
def test_datetime_field_with_extra_attrs
- expected = %{<input id="post_written_on" step="60" max="2010-08-15T10:25:00.000+0000" min="2000-06-15T20:45:30.000+0000" name="post[written_on]" type="datetime" value="2004-06-15T01:02:03.000+0000" />}
+ expected = %{<input id="post_written_on" step="60" max="2010-08-15T10:25:00" min="2000-06-15T20:45:30" name="post[written_on]" type="datetime-local" value="2004-06-15T01:02:03" />}
@post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
min_value = DateTime.new(2000, 6, 15, 20, 45, 30)
max_value = DateTime.new(2010, 8, 15, 10, 25, 00)
@@ -1158,36 +1177,36 @@ class FormHelperTest < ActionView::TestCase
end
def test_datetime_field_with_value_attr
- expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2013-06-29T13:37:00+00:00" />}
- value = DateTime.new(2013,6,29,13,37)
+ expected = %{<input id="post_written_on" name="post[written_on]" type="datetime-local" value="2013-06-29T13:37:00+00:00" />}
+ value = DateTime.new(2013, 6, 29, 13, 37)
assert_dom_equal(expected, datetime_field("post", "written_on", value: value))
end
def test_datetime_field_with_timewithzone_value
- previous_time_zone, Time.zone = Time.zone, 'UTC'
- expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2004-06-15T15:30:45.000+0000" />}
- @post.written_on = Time.zone.parse('2004-06-15 15:30:45')
+ previous_time_zone, Time.zone = Time.zone, "UTC"
+ expected = %{<input id="post_written_on" name="post[written_on]" type="datetime-local" value="2004-06-15T15:30:45" />}
+ @post.written_on = Time.zone.parse("2004-06-15 15:30:45")
assert_dom_equal(expected, datetime_field("post", "written_on"))
ensure
Time.zone = previous_time_zone
end
def test_datetime_field_with_nil_value
- expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" />}
+ expected = %{<input id="post_written_on" name="post[written_on]" type="datetime-local" />}
@post.written_on = nil
assert_dom_equal(expected, datetime_field("post", "written_on"))
end
def test_datetime_field_with_string_values_for_min_and_max
- expected = %{<input id="post_written_on" max="2010-08-15T10:25:00.000+0000" min="2000-06-15T20:45:30.000+0000" name="post[written_on]" type="datetime" value="2004-06-15T01:02:03.000+0000" />}
+ expected = %{<input id="post_written_on" max="2010-08-15T10:25:00" min="2000-06-15T20:45:30" name="post[written_on]" type="datetime-local" value="2004-06-15T01:02:03" />}
@post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
- min_value = "2000-06-15T20:45:30.000+0000"
- max_value = "2010-08-15T10:25:00.000+0000"
+ min_value = "2000-06-15T20:45:30"
+ max_value = "2010-08-15T10:25:00"
assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value))
end
def test_datetime_field_with_invalid_string_values_for_min_and_max
- expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2004-06-15T01:02:03.000+0000" />}
+ expected = %{<input id="post_written_on" name="post[written_on]" type="datetime-local" value="2004-06-15T01:02:03" />}
@post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
min_value = "foo"
max_value = "bar"
@@ -1199,52 +1218,6 @@ class FormHelperTest < ActionView::TestCase
assert_dom_equal(expected, datetime_local_field("post", "written_on"))
end
- def test_datetime_local_field_with_datetime_value
- expected = %{<input id="post_written_on" name="post[written_on]" type="datetime-local" value="2004-06-15T01:02:03" />}
- @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
- assert_dom_equal(expected, datetime_local_field("post", "written_on"))
- end
-
- def test_datetime_local_field_with_extra_attrs
- expected = %{<input id="post_written_on" step="60" max="2010-08-15T10:25:00" min="2000-06-15T20:45:30" name="post[written_on]" type="datetime-local" value="2004-06-15T01:02:03" />}
- @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
- min_value = DateTime.new(2000, 6, 15, 20, 45, 30)
- max_value = DateTime.new(2010, 8, 15, 10, 25, 00)
- step = 60
- assert_dom_equal(expected, datetime_local_field("post", "written_on", min: min_value, max: max_value, step: step))
- end
-
- def test_datetime_local_field_with_timewithzone_value
- previous_time_zone, Time.zone = Time.zone, 'UTC'
- expected = %{<input id="post_written_on" name="post[written_on]" type="datetime-local" value="2004-06-15T15:30:45" />}
- @post.written_on = Time.zone.parse('2004-06-15 15:30:45')
- assert_dom_equal(expected, datetime_local_field("post", "written_on"))
- ensure
- Time.zone = previous_time_zone
- end
-
- def test_datetime_local_field_with_nil_value
- expected = %{<input id="post_written_on" name="post[written_on]" type="datetime-local" />}
- @post.written_on = nil
- assert_dom_equal(expected, datetime_local_field("post", "written_on"))
- end
-
- def test_datetime_local_field_with_string_values_for_min_and_max
- expected = %{<input id="post_written_on" max="2010-08-15T10:25:00" min="2000-06-15T20:45:30" name="post[written_on]" type="datetime-local" value="2004-06-15T01:02:03" />}
- @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
- min_value = "2000-06-15T20:45:30"
- max_value = "2010-08-15T10:25:00"
- assert_dom_equal(expected, datetime_local_field("post", "written_on", min: min_value, max: max_value))
- end
-
- def test_datetime_local_field_with_invalid_string_values_for_min_and_max
- expected = %{<input id="post_written_on" name="post[written_on]" type="datetime-local" value="2004-06-15T01:02:03" />}
- @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
- min_value = "foo"
- max_value = "bar"
- assert_dom_equal(expected, datetime_local_field("post", "written_on", min: min_value, max: max_value))
- end
-
def test_month_field
expected = %{<input id="post_written_on" name="post[written_on]" type="month" value="2004-06" />}
assert_dom_equal(expected, month_field("post", "written_on"))
@@ -1272,16 +1245,16 @@ class FormHelperTest < ActionView::TestCase
end
def test_month_field_with_timewithzone_value
- previous_time_zone, Time.zone = Time.zone, 'UTC'
+ previous_time_zone, Time.zone = Time.zone, "UTC"
expected = %{<input id="post_written_on" name="post[written_on]" type="month" value="2004-06" />}
- @post.written_on = Time.zone.parse('2004-06-15 15:30:45')
+ @post.written_on = Time.zone.parse("2004-06-15 15:30:45")
assert_dom_equal(expected, month_field("post", "written_on"))
ensure
Time.zone = previous_time_zone
end
def test_week_field
- expected = %{<input id="post_written_on" name="post[written_on]" type="week" value="2004-W24" />}
+ expected = %{<input id="post_written_on" name="post[written_on]" type="week" value="2004-W25" />}
assert_dom_equal(expected, week_field("post", "written_on"))
end
@@ -1292,13 +1265,13 @@ class FormHelperTest < ActionView::TestCase
end
def test_week_field_with_datetime_value
- expected = %{<input id="post_written_on" name="post[written_on]" type="week" value="2004-W24" />}
+ expected = %{<input id="post_written_on" name="post[written_on]" type="week" value="2004-W25" />}
@post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
assert_dom_equal(expected, week_field("post", "written_on"))
end
def test_week_field_with_extra_attrs
- expected = %{<input id="post_written_on" step="2" max="2010-W51" min="2000-W06" name="post[written_on]" type="week" value="2004-W24" />}
+ expected = %{<input id="post_written_on" step="2" max="2010-W51" min="2000-W06" name="post[written_on]" type="week" value="2004-W25" />}
@post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
min_value = DateTime.new(2000, 2, 13)
max_value = DateTime.new(2010, 12, 23)
@@ -1307,14 +1280,20 @@ class FormHelperTest < ActionView::TestCase
end
def test_week_field_with_timewithzone_value
- previous_time_zone, Time.zone = Time.zone, 'UTC'
- expected = %{<input id="post_written_on" name="post[written_on]" type="week" value="2004-W24" />}
- @post.written_on = Time.zone.parse('2004-06-15 15:30:45')
+ previous_time_zone, Time.zone = Time.zone, "UTC"
+ expected = %{<input id="post_written_on" name="post[written_on]" type="week" value="2004-W25" />}
+ @post.written_on = Time.zone.parse("2004-06-15 15:30:45")
assert_dom_equal(expected, week_field("post", "written_on"))
ensure
Time.zone = previous_time_zone
end
+ def test_week_field_week_number_base
+ expected = %{<input id="post_written_on" name="post[written_on]" type="week" value="2015-W01" />}
+ @post.written_on = DateTime.new(2015, 1, 1, 1, 2, 3)
+ assert_dom_equal(expected, week_field("post", "written_on"))
+ end
+
def test_url_field
expected = %{<input id="user_homepage" name="user[homepage]" type="url" />}
assert_dom_equal(expected, url_field("user", "homepage"))
@@ -1412,7 +1391,7 @@ class FormHelperTest < ActionView::TestCase
)
assert_dom_equal(
'<select name="post[secret]"></select>',
- select("post", "secret", [], {}, "id" => nil)
+ select("post", "secret", [], {}, { "id" => nil })
)
assert_dom_equal(
text_field("post", "title", "id" => nil),
@@ -1462,26 +1441,26 @@ class FormHelperTest < ActionView::TestCase
def test_index_with_nil_id
assert_dom_equal(
'<input name="post[5][title]" type="text" value="Hello World" />',
- text_field("post", "title", "index" => 5, 'id' => nil)
+ text_field("post", "title", "index" => 5, "id" => nil)
)
assert_dom_equal(
%{<textarea name="post[5][body]">\nBack to the hill and over it again!</textarea>},
- text_area("post", "body", "index" => 5, 'id' => nil)
+ text_area("post", "body", "index" => 5, "id" => nil)
)
assert_dom_equal(
'<input name="post[5][secret]" type="hidden" value="0" /><input checked="checked" name="post[5][secret]" type="checkbox" value="1" />',
- check_box("post", "secret", "index" => 5, 'id' => nil)
+ check_box("post", "secret", "index" => 5, "id" => nil)
)
assert_dom_equal(
- text_field("post", "title", "index" => 5, 'id' => nil),
+ text_field("post", "title", "index" => 5, "id" => nil),
text_field("post", "title", index: 5, id: nil)
)
assert_dom_equal(
- text_area("post", "body", "index" => 5, 'id' => nil),
+ text_area("post", "body", "index" => 5, "id" => nil),
text_area("post", "body", index: 5, id: nil)
)
assert_dom_equal(
- check_box("post", "secret", "index" => 5, 'id' => nil),
+ check_box("post", "secret", "index" => 5, "id" => nil),
check_box("post", "secret", index: 5, id: nil)
)
end
@@ -1494,7 +1473,7 @@ class FormHelperTest < ActionView::TestCase
)
assert_dom_equal(
%{<input id="post_#{pid}_title" name="post[#{pid}][title]" type="text" value="Hello World" />},
- text_field("post[]","title")
+ text_field("post[]", "title")
)
assert_dom_equal(
%{<textarea id="post_#{pid}_body" name="post[#{pid}][body]">\nBack to the hill and over it again!</textarea>},
@@ -1504,10 +1483,10 @@ class FormHelperTest < ActionView::TestCase
%{<input name="post[#{pid}][secret]" type="hidden" value="0" /><input checked="checked" id="post_#{pid}_secret" name="post[#{pid}][secret]" type="checkbox" value="1" />},
check_box("post[]", "secret")
)
- assert_dom_equal(
+ assert_dom_equal(
%{<input checked="checked" id="post_#{pid}_title_hello_world" name="post[#{pid}][title]" type="radio" value="Hello World" />},
radio_button("post[]", "title", "Hello World")
- )
+ )
assert_dom_equal(
%{<input id="post_#{pid}_title_goodbye_world" name="post[#{pid}][title]" type="radio" value="Goodbye World" />},
radio_button("post[]", "title", "Goodbye World")
@@ -1528,10 +1507,10 @@ class FormHelperTest < ActionView::TestCase
%{<input name="post[#{pid}][secret]" type="hidden" value="0" /><input checked="checked" name="post[#{pid}][secret]" type="checkbox" value="1" />},
check_box("post[]", "secret", id: nil)
)
- assert_dom_equal(
- %{<input checked="checked" name="post[#{pid}][title]" type="radio" value="Hello World" />},
- radio_button("post[]", "title", "Hello World", id: nil)
- )
+ assert_dom_equal(
+ %{<input checked="checked" name="post[#{pid}][title]" type="radio" value="Hello World" />},
+ radio_button("post[]", "title", "Hello World", id: nil)
+ )
assert_dom_equal(
%{<input name="post[#{pid}][title]" type="radio" value="Goodbye World" />},
radio_button("post[]", "title", "Goodbye World", id: nil)
@@ -1539,50 +1518,83 @@ class FormHelperTest < ActionView::TestCase
end
def test_form_for_requires_block
- assert_raises(ArgumentError) do
- form_for(:post, @post, html: { id: 'create-post' })
+ error = assert_raises(ArgumentError) do
+ form_for(@post, html: { id: "create-post" })
end
+ assert_equal "Missing block", error.message
end
def test_form_for_requires_arguments
error = assert_raises(ArgumentError) do
- form_for(nil, html: { id: 'create-post' }) 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
+ 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_for
- form_for(@post, html: { id: 'create-post' }) do |f|
+ form_for(@post, html: { id: "create-post" }) do |f|
+ concat f.label(:title) { "The Title" }
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ concat f.submit("Create post")
+ concat f.button("Create post")
+ concat f.button {
+ concat content_tag(:span, "Create post")
+ }
+ end
+
+ expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do
+ "<label for='post_title'>The Title</label>" \
+ "<input name='post[title]' type='text' id='post_title' value='Hello World' />" \
+ "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" \
+ "<input name='post[secret]' type='hidden' value='0' />" \
+ "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />" \
+ "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />" \
+ "<button name='button' type='submit'>Create post</button>" \
+ "<button name='button' type='submit'><span>Create post</span></button>"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_is_not_affected_by_form_with_generates_ids
+ old_value = ActionView::Helpers::FormHelper.form_with_generates_ids
+ ActionView::Helpers::FormHelper.form_with_generates_ids = false
+
+ form_for(@post, html: { id: "create-post" }) do |f|
concat f.label(:title) { "The Title" }
concat f.text_field(:title)
concat f.text_area(:body)
concat f.check_box(:secret)
- concat f.submit('Create post')
- concat f.button('Create post')
+ concat f.submit("Create post")
+ concat f.button("Create post")
concat f.button {
- concat content_tag(:span, 'Create post')
+ concat content_tag(:span, "Create post")
}
end
expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do
- "<label for='post_title'>The Title</label>" +
- "<input name='post[title]' type='text' id='post_title' value='Hello World' />" +
- "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" +
- "<input name='post[secret]' type='hidden' value='0' />" +
- "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />" +
- "<input name='commit' 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
assert_dom_equal expected, output_buffer
+ ensure
+ ActionView::Helpers::FormHelper.form_with_generates_ids = old_value
end
def test_form_for_with_collection_radio_buttons
@@ -1593,9 +1605,10 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts", "new_post", "new_post") do
- "<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
@@ -1614,11 +1627,12 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts", "new_post", "new_post") do
- "<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
@@ -1639,12 +1653,13 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts", "new_post_1", "new_post") do
- "<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
@@ -1655,14 +1670,15 @@ class FormHelperTest < ActionView::TestCase
post = Post.new
def post.active; false; end
- form_for(post, namespace: 'foo') do |f|
+ form_for(post, namespace: "foo") do |f|
concat f.collection_radio_buttons(:active, [true, false], :to_s, :to_s)
end
expected = whole_form("/posts", "foo_new_post", "new_post") do
- "<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
@@ -1673,14 +1689,15 @@ class FormHelperTest < ActionView::TestCase
post = Post.new
def post.active; false; end
- form_for(post, index: '1') do |f|
+ form_for(post, index: "1") do |f|
concat f.collection_radio_buttons(:active, [true, false], :to_s, :to_s)
end
expected = whole_form("/posts", "new_post", "new_post") do
- "<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
@@ -1696,13 +1713,13 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts", "new_post", "new_post") do
- "<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>" +
- "<input name='post[tag_ids][]' type='hidden' value='' />"
+ "<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
assert_dom_equal expected, output_buffer
@@ -1720,16 +1737,16 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts", "new_post", "new_post") do
- "<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='' />"
+ "<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
assert_dom_equal expected, output_buffer
@@ -1750,16 +1767,16 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts", "new_post_1", "new_post") do
- "<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='' />"+
+ "<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
@@ -1771,14 +1788,14 @@ class FormHelperTest < ActionView::TestCase
def post.tag_ids; [1]; end
collection = [[1, "Tag 1"]]
- form_for(post, namespace: 'foo') do |f|
+ form_for(post, namespace: "foo") do |f|
concat f.collection_check_boxes(:tag_ids, collection, :first, :last)
end
expected = whole_form("/posts", "foo_new_post", "new_post") do
- "<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>" +
- "<input name='post[tag_ids][]' type='hidden' value='' />"
+ "<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
assert_dom_equal expected, output_buffer
@@ -1789,36 +1806,32 @@ class FormHelperTest < ActionView::TestCase
def post.tag_ids; [1]; end
collection = [[1, "Tag 1"]]
- form_for(post, index: '1') do |f|
+ form_for(post, index: "1") do |f|
concat f.collection_check_boxes(:tag_ids, collection, :first, :last)
end
expected = whole_form("/posts", "new_post", "new_post") do
- "<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>" +
- "<input name='post[1][tag_ids][]' type='hidden' value='' />"
+ "<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
assert_dom_equal expected, output_buffer
end
def test_form_for_with_file_field_generate_multipart
- Post.send :attr_accessor, :file
-
- form_for(@post, html: { id: 'create-post' }) do |f|
+ form_for(@post, html: { id: "create-post" }) do |f|
concat f.file_field(:file)
end
expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch", multipart: true) do
- "<input name='post[file]' type='hidden' value='' /><input name='post[file]' type='file' id='post_file' />"
+ "<input name='post[file]' type='file' id='post_file' />"
end
assert_dom_equal expected, output_buffer
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)
@@ -1826,7 +1839,7 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch", multipart: true) do
- "<input name='post[comment][file]' type='hidden' value='' /><input name='post[comment][file]' type='file' id='post_comment_file' />"
+ "<input name='post[comment][file]' type='file' id='post_comment_file' />"
end
assert_dom_equal expected, output_buffer
@@ -1837,7 +1850,7 @@ class FormHelperTest < ActionView::TestCase
concat f.label(:title)
end
- expected = whole_form("/posts/123.json", "edit_post_123", "edit_post", method: 'patch') do
+ expected = whole_form("/posts/123.json", "edit_post_123", "edit_post", method: "patch") do
"<label for='post_title'>Title</label>"
end
@@ -1849,33 +1862,33 @@ class FormHelperTest < ActionView::TestCase
form_for(blog_post) do |f|
concat f.text_field :title
- concat f.submit('Edit post')
+ concat f.submit("Edit post")
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='commit' type='submit' value='Edit post' />"
+ "<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
assert_dom_equal expected, output_buffer
end
- def test_form_for_with_symbol_object_name
+ def test_form_for_with_symbol_as
form_for(@post, as: "other_name", html: { id: "create-post" }) do |f|
- concat f.label(:title, class: 'post_title')
+ 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')
+ concat f.submit("Create post")
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' />" +
- "<input name='commit' value='Create post' type='submit' />"
+ 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' />" \
+ "<input name='commit' value='Create post' data-disable-with='Create post' type='submit' />"
end
assert_dom_equal expected, output_buffer
@@ -1885,27 +1898,27 @@ 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|
+ form_for(obj, as: "other_name", url: "/", html: { id: "edit-other-name" }) do |f|
assert_raise(NoMethodError) { f.hidden_field(:private_property) }
end
end
def test_form_for_with_method_as_part_of_html_options
- form_for(@post, url: '/', html: { id: 'create-post', method: :delete }) do |f|
+ form_for(@post, url: "/", html: { id: "create-post", 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", "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' />" +
+ 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[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />"
end
@@ -1913,16 +1926,16 @@ class FormHelperTest < ActionView::TestCase
end
def test_form_for_with_method
- form_for(@post, url: '/', method: :delete, html: { id: 'create-post' }) do |f|
+ form_for(@post, url: "/", method: :delete, html: { 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", "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' />" +
+ 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[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />"
end
@@ -1936,7 +1949,7 @@ class FormHelperTest < ActionView::TestCase
concat f.search_field(:title)
end
- expected = whole_form("/search", "search-post", "new_post", method: "get") do
+ expected = whole_form("/search", "search-post", "new_post", method: "get") do
"<input name='post[title]' type='search' id='post_title' />"
end
@@ -1944,16 +1957,16 @@ class FormHelperTest < ActionView::TestCase
end
def test_form_for_with_remote
- form_for(@post, url: '/', remote: true, html: { id: 'create-post', method: :patch }) do |f|
+ form_for(@post, url: "/", remote: true, html: { 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", "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' />" +
+ 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[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />"
end
@@ -1984,17 +1997,45 @@ class FormHelperTest < ActionView::TestCase
assert_dom_equal expected, output_buffer
end
+ def test_form_for_default_enforce_utf8_false
+ with_default_enforce_utf8 false do
+ form_for(:post) do |f|
+ concat f.text_field(:title)
+ end
+
+ expected = whole_form("/", nil, nil, enforce_utf8: false) do
+ "<input name='post[title]' type='text' id='post_title' value='Hello World' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+ end
+
+ def test_form_for_default_enforce_utf8_true
+ with_default_enforce_utf8 true do
+ form_for(:post) do |f|
+ concat f.text_field(:title)
+ end
+
+ expected = whole_form("/", nil, nil, enforce_utf8: true) do
+ "<input name='post[title]' type='text' id='post_title' value='Hello World' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+ end
+
def test_form_for_with_remote_in_html
- form_for(@post, url: '/', html: { remote: true, id: 'create-post', method: :patch }) do |f|
+ form_for(@post, url: "/", html: { remote: true, 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", "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' />" +
+ 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[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />"
end
@@ -2003,34 +2044,35 @@ class FormHelperTest < ActionView::TestCase
def test_form_for_with_remote_without_html
@post.persisted = false
- @post.stubs(:to_key).returns(nil)
- form_for(@post, remote: true) do |f|
- concat f.text_field(:title)
- concat f.text_area(:body)
- concat f.check_box(:secret)
- end
+ @post.stub(:to_key, nil) do
+ form_for(@post, remote: true) do |f|
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ 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[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />"
- 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[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />"
+ end
- assert_dom_equal expected, output_buffer
+ assert_dom_equal expected, output_buffer
+ end
end
def test_form_for_without_object
- form_for(:post, html: { id: 'create-post' }) do |f|
+ form_for(:post, html: { 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' 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
@@ -2045,11 +2087,11 @@ class FormHelperTest < ActionView::TestCase
concat f.check_box(:secret)
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' />" +
+ 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' />" \
"<input name='post[123][secret]' checked='checked' type='checkbox' id='post_123_secret' value='1' />"
end
@@ -2063,10 +2105,10 @@ class FormHelperTest < ActionView::TestCase
concat f.check_box(:secret)
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' />" +
+ 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[][secret]' checked='checked' type='checkbox' id='post__secret' value='1' />"
end
@@ -2075,15 +2117,15 @@ class FormHelperTest < ActionView::TestCase
def test_form_for_label_error_wrapping
form_for(@post) do |f|
- concat f.label(:author_name, class: 'label')
+ concat f.label(:author_name, class: "label")
concat f.text_field(:author_name)
- concat f.submit('Create post')
+ concat f.submit("Create post")
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>" +
- "<input name='commit' type='submit' value='Create post' />"
+ 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>" \
+ "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />"
end
assert_dom_equal expected, output_buffer
@@ -2093,15 +2135,15 @@ class FormHelperTest < ActionView::TestCase
post = remove_instance_variable :@post
form_for(post) do |f|
- concat f.label(:author_name, class: 'label')
+ concat f.label(:author_name, class: "label")
concat f.text_field(:author_name)
- concat f.submit('Create post')
+ concat f.submit("Create post")
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>" +
- "<input name='commit' type='submit' value='Create post' />"
+ 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>" \
+ "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />"
end
assert_dom_equal expected, output_buffer
@@ -2109,12 +2151,12 @@ class FormHelperTest < ActionView::TestCase
def test_form_for_label_error_wrapping_block_and_non_block_versions
form_for(@post) do |f|
- concat f.label(:author_name, 'Name', class: 'label')
- concat f.label(:author_name, class: 'label') { 'Name' }
+ concat f.label(:author_name, "Name", class: "label")
+ concat f.label(:author_name, class: "label") { "Name" }
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>" +
+ 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>"
end
@@ -2122,16 +2164,16 @@ class FormHelperTest < ActionView::TestCase
end
def test_form_for_with_namespace
- form_for(@post, namespace: 'namespace') do |f|
+ 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' 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' />" +
+ 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[secret]' checked='checked' type='checkbox' id='namespace_post_secret' value='1' />"
end
@@ -2139,21 +2181,21 @@ class FormHelperTest < ActionView::TestCase
end
def test_form_for_with_namespace_with_date_select
- form_for(@post, namespace: 'namespace') do |f|
+ form_for(@post, namespace: "namespace") do |f|
concat f.date_select(:written_on)
end
- assert_select 'select#namespace_post_written_on_1i'
+ assert_select "select#namespace_post_written_on_1i"
end
def test_form_for_with_namespace_with_label
- form_for(@post, namespace: 'namespace') do |f|
+ form_for(@post, namespace: "namespace") do |f|
concat f.label(:title)
concat f.text_field(:title)
end
- expected = whole_form('/posts/123', 'namespace_edit_post_123', 'edit_post', method: 'patch') do
- "<label for='namespace_post_title'>Title</label>" +
+ expected = whole_form("/posts/123", "namespace_edit_post_123", "edit_post", method: "patch") do
+ "<label for='namespace_post_title'>Title</label>" \
"<input name='post[title]' type='text' id='namespace_post_title' value='Hello World' />"
end
@@ -2161,11 +2203,11 @@ class FormHelperTest < ActionView::TestCase
end
def test_form_for_with_namespace_and_as_option
- form_for(@post, namespace: 'namespace', as: 'custom_name') do |f|
+ form_for(@post, namespace: "namespace", as: "custom_name") do |f|
concat f.text_field(:title)
end
- expected = whole_form('/posts/123', 'namespace_edit_custom_name', 'edit_custom_name', method: 'patch') do
+ expected = whole_form("/posts/123", "namespace_edit_custom_name", "edit_custom_name", method: "patch") do
"<input id='namespace_custom_name_title' name='custom_name[title]' type='text' value='Hello World' />"
end
@@ -2173,25 +2215,25 @@ class FormHelperTest < ActionView::TestCase
end
def test_two_form_for_with_namespace
- form_for(@post, namespace: 'namespace_1') do |f|
+ form_for(@post, namespace: "namespace_1") do |f|
concat f.label(:title)
concat f.text_field(:title)
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>" +
+ expected_1 = whole_form("/posts/123", "namespace_1_edit_post_123", "edit_post", method: "patch") do
+ "<label for='namespace_1_post_title'>Title</label>" \
"<input name='post[title]' type='text' id='namespace_1_post_title' value='Hello World' />"
end
assert_dom_equal expected_1, output_buffer
- form_for(@post, namespace: 'namespace_2') do |f|
+ form_for(@post, namespace: "namespace_2") do |f|
concat f.label(:title)
concat f.text_field(:title)
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>" +
+ expected_2 = whole_form("/posts/123", "namespace_2_edit_post_123", "edit_post", method: "patch") do
+ "<label for='namespace_2_post_title'>Title</label>" \
"<input name='post[title]' type='text' id='namespace_2_post_title' value='Hello World' />"
end
@@ -2199,8 +2241,8 @@ class FormHelperTest < ActionView::TestCase
end
def test_fields_for_with_namespace
- @comment.body = 'Hello World'
- form_for(@post, namespace: 'namespace') do |f|
+ @comment.body = "Hello World"
+ form_for(@post, namespace: "namespace") do |f|
concat f.text_field(:title)
concat f.text_area(:body)
concat f.fields_for(@comment) { |c|
@@ -2208,9 +2250,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>" +
+ 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[comment][body]' type='text' id='namespace_post_comment_body' value='Hello World' />"
end
@@ -2220,16 +2262,17 @@ class FormHelperTest < ActionView::TestCase
def test_submit_with_object_as_new_record_and_locale_strings
with_locale :submit do
@post.persisted = false
- @post.stubs(:to_key).returns(nil)
- form_for(@post) do |f|
- concat f.submit
- end
+ @post.stub(:to_key, nil) do
+ form_for(@post) do |f|
+ concat f.submit
+ end
- expected = whole_form('/posts', 'new_post', 'new_post') do
- "<input name='commit' type='submit' value='Create Post' />"
- end
+ expected = whole_form("/posts", "new_post", "new_post") do
+ "<input name='commit' data-disable-with='Create Post' type='submit' value='Create Post' />"
+ end
- assert_dom_equal expected, output_buffer
+ assert_dom_equal expected, output_buffer
+ end
end
end
@@ -2239,8 +2282,8 @@ class FormHelperTest < ActionView::TestCase
concat f.submit
end
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
- "<input name='commit' type='submit' value='Confirm Post changes' />"
+ expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do
+ "<input name='commit' data-disable-with='Confirm Post changes' type='submit' value='Confirm Post changes' />"
end
assert_dom_equal expected, output_buffer
@@ -2254,21 +2297,36 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form do
- "<input name='commit' class='extra' type='submit' value='Save changes' />"
+ "<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
+ def test_submit_with_object_which_is_overwritten_by_as_option
with_locale :submit do
form_for(@post, as: :another_post) do |f|
concat f.submit
end
- expected = whole_form('/posts/123', 'edit_another_post', 'edit_another_post', method: 'patch') do
- "<input name='commit' type='submit' value='Update your Post' />"
+ expected = whole_form("/posts/123", "edit_another_post", "edit_another_post", method: "patch") do
+ "<input name='commit' data-disable-with='Update your Post' type='submit' value='Update your Post' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+ end
+
+ def test_submit_with_object_which_is_namespaced
+ blog_post = Blog::Post.new("And his name will be forty and four.", 44)
+ with_locale :submit do
+ form_for(blog_post) do |f|
+ concat f.submit
+ end
+
+ expected = whole_form("/posts/44", "edit_post_44", "edit_post", method: "patch") do
+ "<input name='commit' data-disable-with='Update your Post' type='submit' value='Update your Post' />"
end
assert_dom_equal expected, output_buffer
@@ -2276,31 +2334,53 @@ class FormHelperTest < ActionView::TestCase
end
def test_nested_fields_for
- @comment.body = 'Hello World'
+ @comment.body = "Hello World"
form_for(@post) do |f|
concat f.fields_for(@comment) { |c|
concat c.text_field(:body)
}
end
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do
"<input name='post[comment][body]' type='text' id='post_comment_body' value='Hello World' />"
end
assert_dom_equal expected, output_buffer
end
+ def test_deep_nested_fields_for
+ @comment.save
+ form_for(:posts) do |f|
+ f.fields_for("post[]", @post) do |f2|
+ f2.text_field(:id)
+ @post.comments.each do |comment|
+ concat f2.fields_for("comment[]", comment) { |c|
+ concat c.text_field(:name)
+ }
+ end
+ end
+ end
+
+ expected = whole_form do
+ "<input name='posts[post][0][comment][1][name]' type='text' id='posts_post_0_comment_1_name' value='comment #1' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
def test_nested_fields_for_with_nested_collections
- form_for(@post, as: 'post[]') do |f|
+ form_for(@post, as: "post[]") do |f|
concat f.text_field(:title)
- concat f.fields_for('comment[]', @comment) { |c|
+ 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' />"
+ 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][body]' type='text' id='post_123_body' value='Back to the hill and over it again!' />"
end
assert_dom_equal expected, output_buffer
@@ -2309,13 +2389,13 @@ class FormHelperTest < ActionView::TestCase
def test_nested_fields_for_with_index_and_parent_fields
form_for(@post, index: 1) do |c|
concat c.text_field(:title)
- concat c.fields_for('comment', @comment, index: 1) { |r|
+ concat c.fields_for("comment", @comment, index: 1) { |r|
concat r.text_field(:name)
}
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' />" +
+ 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][comment][1][name]' type='text' id='post_1_comment_1_name' value='new comment' />"
end
@@ -2329,7 +2409,7 @@ class FormHelperTest < ActionView::TestCase
}
end
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do
"<input name='post[1][comment][title]' type='text' id='post_1_comment_title' value='Hello World' />"
end
@@ -2343,7 +2423,7 @@ class FormHelperTest < ActionView::TestCase
}
end
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do
"<input name='post[1][comment][5][title]' type='text' id='post_1_comment_5_title' value='Hello World' />"
end
@@ -2357,7 +2437,7 @@ class FormHelperTest < ActionView::TestCase
}
end
- expected = whole_form('/posts/123', 'edit_post[]', 'edit_post[]', method: 'patch') do
+ expected = whole_form("/posts/123", "edit_post[]", "edit_post[]", method: "patch") do
"<input name='post[123][comment][title]' type='text' id='post_123_comment_title' value='Hello World' />"
end
@@ -2371,7 +2451,7 @@ class FormHelperTest < ActionView::TestCase
}
end
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do
"<input name='post[comment][5][title]' type='radio' id='post_comment_5_title_hello' value='hello' />"
end
@@ -2385,7 +2465,7 @@ class FormHelperTest < ActionView::TestCase
}
end
- expected = whole_form('/posts/123', 'edit_post[]', 'edit_post[]', method: 'patch') do
+ expected = whole_form("/posts/123", "edit_post[]", "edit_post[]", method: "patch") do
"<input name='post[123][comment][123][title]' type='text' id='post_123_comment_123_title' value='Hello World' />"
end
@@ -2405,9 +2485,9 @@ class FormHelperTest < ActionView::TestCase
}
end
- expected = whole_form('/posts/123', 'edit_post[]', 'edit_post[]', method: 'patch') do
+ expected = whole_form("/posts/123", "edit_post[]", "edit_post[]", method: "patch") do
"<input name='post[123][comment][5][title]' type='text' id='post_123_comment_5_title' value='Hello World' />"
- end + whole_form('/posts/123', 'edit_post', 'edit_post', method: 'patch') do
+ end + whole_form("/posts/123", "edit_post", "edit_post", method: "patch") do
"<input name='post[1][comment][123][title]' type='text' id='post_1_comment_123_title' value='Hello World' />"
end
@@ -2424,8 +2504,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" />' +
+ 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="new author" />'
end
@@ -2451,11 +2531,11 @@ 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" />'
- 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" />'
+ end
assert_dom_equal expected, output_buffer
end
@@ -2470,9 +2550,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" />' +
+ 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" />'
end
@@ -2489,8 +2569,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" />' +
+ 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" />'
end
@@ -2507,8 +2587,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" />' +
+ 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" />'
end
@@ -2525,9 +2605,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" />' +
+ 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" />'
end
@@ -2545,9 +2625,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_id" name="post[author_attributes][id]" type="hidden" value="321" />' +
+ 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 id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />'
end
@@ -2566,11 +2646,11 @@ class FormHelperTest < ActionView::TestCase
end
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" />' +
+ 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 id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />'
end
@@ -2593,11 +2673,11 @@ class FormHelperTest < ActionView::TestCase
end
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" />' +
+ 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 id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />'
end
@@ -2620,10 +2700,10 @@ class FormHelperTest < ActionView::TestCase
end
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" />' +
+ 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 id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />'
end
@@ -2646,11 +2726,11 @@ class FormHelperTest < ActionView::TestCase
end
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" />' +
+ 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 id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />'
end
@@ -2669,11 +2749,11 @@ class FormHelperTest < ActionView::TestCase
end
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" />' +
+ 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 id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />'
end
@@ -2693,11 +2773,11 @@ class FormHelperTest < ActionView::TestCase
end
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" />' +
+ 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 id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />'
end
@@ -2716,9 +2796,9 @@ class FormHelperTest < ActionView::TestCase
end
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" />' +
+ 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 id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="new comment" />'
end
@@ -2737,10 +2817,10 @@ class FormHelperTest < ActionView::TestCase
end
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" />' +
+ 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 id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="new comment" />'
end
@@ -2755,7 +2835,7 @@ class FormHelperTest < ActionView::TestCase
end
end
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ 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" />'
end
@@ -2772,11 +2852,11 @@ 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" />' +
+ 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 id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />'
end
@@ -2793,11 +2873,11 @@ 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" />' +
+ 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 id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />'
end
@@ -2807,11 +2887,12 @@ class FormHelperTest < ActionView::TestCase
def test_nested_fields_label_translation_with_more_than_10_records
@post.comments = Array.new(11) { |id| Comment.new(id + 1) }
- I18n.expects(:t).with('post.comments.body', default: [:"comment.body", ''], scope: "helpers.label").times(11).returns "Write body here"
-
- form_for(@post) do |f|
- f.fields_for(:comments) do |cf|
- concat cf.label(:body)
+ 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_for(@post) do |f|
+ f.fields_for(:comments) do |cf|
+ concat cf.label(:body)
+ end
end
end
end
@@ -2827,11 +2908,11 @@ 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" />' +
+ 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 id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />'
end
@@ -2850,10 +2931,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 #321" />' +
- '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' +
+ 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 id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="new comment" />'
end
@@ -2865,13 +2946,13 @@ class FormHelperTest < ActionView::TestCase
@post.comments = []
form_for(@post) do |f|
- concat f.fields_for(:comments, Comment.new(321), child_index: 'abc') { |cf|
+ concat f.fields_for(:comments, Comment.new(321), child_index: "abc") { |cf|
concat cf.text_field(:name)
}
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" />' +
+ 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_id" name="post[comments_attributes][abc][id]" type="hidden" value="321" />'
end
@@ -2882,13 +2963,13 @@ class FormHelperTest < ActionView::TestCase
@post.comments = []
form_for(@post) do |f|
- concat f.fields_for(:comments, Comment.new(321), child_index: -> { 'abc' } ) { |cf|
+ concat f.fields_for(:comments, Comment.new(321), child_index: -> { "abc" }) { |cf|
concat cf.text_field(:name)
}
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" />' +
+ 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_id" name="post[comments_attributes][abc][id]" type="hidden" value="321" />'
end
@@ -2905,13 +2986,13 @@ class FormHelperTest < ActionView::TestCase
@post.comments = FakeAssociationProxy.new
form_for(@post) do |f|
- concat f.fields_for(:comments, Comment.new(321), child_index: 'abc') { |cf|
+ concat f.fields_for(:comments, Comment.new(321), child_index: "abc") { |cf|
concat cf.text_field(:name)
}
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" />' +
+ 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_id" name="post[comments_attributes][abc][id]" type="hidden" value="321" />'
end
@@ -2925,7 +3006,7 @@ class FormHelperTest < ActionView::TestCase
expected = 0
@post.comments.each do |comment|
f.fields_for(:comments, comment) { |cf|
- assert_equal cf.index, expected
+ assert_equal expected, cf.index
expected += 1
}
end
@@ -2939,7 +3020,7 @@ class FormHelperTest < ActionView::TestCase
expected = 0
@post.comments.each do |comment|
f.fields_for(:comments, comment) { |cf|
- assert_equal cf.index, expected
+ assert_equal expected, cf.index
expected += 1
}
end
@@ -2952,7 +3033,7 @@ class FormHelperTest < ActionView::TestCase
form_for(@post) do |f|
expected = 0
f.fields_for(:comments, @post.comments) { |cf|
- assert_equal cf.index, expected
+ assert_equal expected, cf.index
expected += 1
}
end
@@ -2962,8 +3043,8 @@ class FormHelperTest < ActionView::TestCase
@post.comments = []
form_for(@post) do |f|
- f.fields_for(:comments, Comment.new(321), child_index: 'abc') { |cf|
- assert_equal cf.index, 'abc'
+ f.fields_for(:comments, Comment.new(321), child_index: "abc") { |cf|
+ assert_equal "abc", cf.index
}
end
end
@@ -2988,7 +3069,7 @@ class FormHelperTest < ActionView::TestCase
concat trf.text_field(:value)
}
}
- concat f.fields_for('tags', @post.tags[1]) { |tf|
+ concat f.fields_for("tags", @post.tags[1]) { |tf|
concat tf.text_field(:value)
concat tf.fields_for(:relevances, TagRelevance.new(31415)) { |trf|
concat trf.text_field(:value)
@@ -2996,18 +3077,18 @@ 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" />' +
+ 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_tags_attributes_1_id" name="post[tags_attributes][1][id]" type="hidden" value="456" />'
end
@@ -3023,7 +3104,7 @@ class FormHelperTest < ActionView::TestCase
}
end
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do
'<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="hash backed author" />'
end
@@ -3038,9 +3119,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
@@ -3054,9 +3135,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
@@ -3070,9 +3151,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
@@ -3086,9 +3167,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
@@ -3102,9 +3183,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
@@ -3118,9 +3199,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
@@ -3132,7 +3213,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
@@ -3143,17 +3224,17 @@ 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
def test_form_builder_does_not_have_form_for_method
- assert !ActionView::Helpers::FormBuilder.instance_methods.include?(:form_for)
+ assert_not_includes ActionView::Helpers::FormBuilder.instance_methods, :form_for
end
def test_form_for_and_fields_for
- form_for(@post, as: :post, html: { id: 'create-post' }) do |post_form|
+ form_for(@post, as: :post, html: { id: "create-post" }) do |post_form|
concat post_form.text_field(:title)
concat post_form.text_area(:body)
@@ -3162,10 +3243,10 @@ 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' />" +
+ 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='parent_post[secret]' checked='checked' type='checkbox' id='parent_post_secret' value='1' />"
end
@@ -3173,7 +3254,7 @@ class FormHelperTest < ActionView::TestCase
end
def test_form_for_and_fields_for_with_object
- form_for(@post, as: :post, html: { id: 'create-post' }) do |post_form|
+ form_for(@post, as: :post, html: { id: "create-post" }) do |post_form|
concat post_form.text_field(:title)
concat post_form.text_area(:body)
@@ -3182,9 +3263,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>" +
+ 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[comment][name]' type='text' id='post_comment_name' value='new comment' />"
end
@@ -3198,7 +3279,7 @@ class FormHelperTest < ActionView::TestCase
}
end
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do
"<input name='post[category][name]' type='text' id='post_category_name' />"
end
@@ -3222,9 +3303,9 @@ class FormHelperTest < ActionView::TestCase
concat f.check_box(:secret)
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/>" +
+ 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='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
@@ -3241,9 +3322,9 @@ class FormHelperTest < ActionView::TestCase
concat f.check_box(:secret)
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/>" +
+ 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='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
@@ -3260,7 +3341,7 @@ class FormHelperTest < ActionView::TestCase
concat f.text_field(:title)
end
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ 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/>"
end
@@ -3301,8 +3382,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
@@ -3314,7 +3395,7 @@ class FormHelperTest < ActionView::TestCase
form_for(@post, builder: LabelledFormBuilder) do |f|
f.fields_for(:comments, Comment.new) do |nested_fields|
klass = nested_fields.class
- ''
+ ""
end
end
@@ -3325,9 +3406,9 @@ class FormHelperTest < ActionView::TestCase
klass = nil
form_for(@post, builder: LabelledFormBuilder) do |f|
- f.fields_for(:comments, Comment.new, index: 'foo') do |nested_fields|
+ f.fields_for(:comments, Comment.new, index: "foo") do |nested_fields|
klass = nested_fields.class
- ''
+ ""
end
end
@@ -3339,10 +3420,10 @@ class FormHelperTest < ActionView::TestCase
form_for(@post, builder: LabelledFormBuilder) do |f|
path = f.to_partial_path
- ''
+ ""
end
- assert_equal 'labelled_form', path
+ assert_equal "labelled_form", path
end
class LabelledFormBuilderSubclass < LabelledFormBuilder; end
@@ -3353,7 +3434,7 @@ class FormHelperTest < ActionView::TestCase
form_for(@post, builder: LabelledFormBuilder) do |f|
f.fields_for(:comments, Comment.new, builder: LabelledFormBuilderSubclass) do |nested_fields|
klass = nested_fields.class
- ''
+ ""
end
end
@@ -3361,23 +3442,23 @@ class FormHelperTest < ActionView::TestCase
end
def test_form_for_with_html_options_adds_options_to_form_tag
- form_for(@post, html: { id: 'some_form', class: 'some_class', multipart: true }) do |f| end
+ form_for(@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_for_with_string_url_option
- form_for(@post, url: 'http://www.otherdomain.com') do |f| end
+ form_for(@post, url: "http://www.otherdomain.com") do |f| end
assert_dom_equal whole_form("http://www.otherdomain.com", "edit_post_123", "edit_post", method: "patch"), output_buffer
end
def test_form_for_with_hash_url_option
- form_for(@post, url: { controller: 'controller', action: 'action' }) do |f| end
+ form_for(@post, url: { controller: "controller", action: "action" }) do |f| end
- assert_equal 'controller', @url_for_options[:controller]
- assert_equal 'action', @url_for_options[:action]
+ assert_equal "controller", @url_for_options[:controller]
+ assert_equal "action", @url_for_options[:action]
end
def test_form_for_with_record_url_option
@@ -3407,14 +3488,14 @@ class FormHelperTest < ActionView::TestCase
def test_form_for_with_existing_object_in_list
@comment.save
- form_for([@post, @comment]) {}
+ form_for([@post, @comment]) { }
expected = whole_form(post_comment_path(@post, @comment), "edit_comment_1", "edit_comment", method: "patch")
assert_dom_equal expected, output_buffer
end
def test_form_for_with_new_object_in_list
- form_for([@post, @comment]) {}
+ form_for([@post, @comment]) { }
expected = whole_form(post_comments_path(@post), "new_comment", "new_comment")
assert_dom_equal expected, output_buffer
@@ -3422,14 +3503,14 @@ class FormHelperTest < ActionView::TestCase
def test_form_for_with_existing_object_and_namespace_in_list
@comment.save
- form_for([:admin, @post, @comment]) {}
+ form_for([:admin, @post, @comment]) { }
expected = whole_form(admin_post_comment_path(@post, @comment), "edit_comment_1", "edit_comment", method: "patch")
assert_dom_equal expected, output_buffer
end
def test_form_for_with_new_object_and_namespace_in_list
- form_for([:admin, @post, @comment]) {}
+ form_for([:admin, @post, @comment]) { }
expected = whole_form(admin_post_comments_path(@post), "new_comment", "new_comment")
assert_dom_equal expected, output_buffer
@@ -3443,13 +3524,13 @@ class FormHelperTest < ActionView::TestCase
end
def test_form_for_with_default_method_as_patch
- form_for(@post) {}
+ form_for(@post) { }
expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch")
assert_dom_equal expected, output_buffer
end
def test_form_for_with_data_attributes
- form_for(@post, data: { behavior: "stuff" }, remote: true) {}
+ form_for(@post, data: { behavior: "stuff" }, remote: true) { }
assert_match %r|data-behavior="stuff"|, output_buffer
assert_match %r|data-remote="true"|, output_buffer
end
@@ -3469,53 +3550,62 @@ class FormHelperTest < ActionView::TestCase
end
form_for(@post, builder: builder_class) { }
- assert_equal 1, initialization_count, 'form builder instantiated more than once'
+ assert_equal 1, initialization_count, "form builder instantiated more than once"
end
- protected
+ private
+
+ def hidden_fields(options = {})
+ method = options[:method]
+
+ if options.fetch(:enforce_utf8, true)
+ txt = +%{<input name="utf8" type="hidden" value="&#x2713;" />}
+ else
+ txt = +""
+ end
- def hidden_fields(options = {})
- method = options[:method]
+ if method && !%w(get post).include?(method.to_s)
+ txt << %{<input name="_method" type="hidden" value="#{method}" />}
+ end
- if options.fetch(:enforce_utf8, true)
- txt = %{<input name="utf8" type="hidden" value="&#x2713;" />}
- else
- txt = ''
+ txt
end
- if method && !%w(get post).include?(method.to_s)
- txt << %{<input name="_method" type="hidden" value="#{method}" />}
+ def form_text(action = "/", id = nil, html_class = nil, remote = nil, multipart = nil, method = nil)
+ txt = +%{<form accept-charset="UTF-8" action="#{action}"}
+ txt << %{ enctype="multipart/form-data"} if multipart
+ txt << %{ data-remote="true"} if remote
+ txt << %{ class="#{html_class}"} if html_class
+ txt << %{ id="#{id}"} if id
+ method = method.to_s == "get" ? "get" : "post"
+ txt << %{ method="#{method}">}
end
- txt
- end
+ def whole_form(action = "/", id = nil, html_class = nil, options = {})
+ contents = block_given? ? yield : ""
- def form_text(action = "/", id = nil, html_class = nil, remote = nil, multipart = nil, method = nil)
- txt = %{<form accept-charset="UTF-8" action="#{action}"}
- txt << %{ enctype="multipart/form-data"} if multipart
- txt << %{ data-remote="true"} if remote
- txt << %{ class="#{html_class}"} if html_class
- txt << %{ id="#{id}"} if id
- method = method.to_s == "get" ? "get" : "post"
- txt << %{ method="#{method}">}
- end
+ method, remote, multipart = options.values_at(:method, :remote, :multipart)
- def whole_form(action = "/", id = nil, html_class = nil, options = {})
- contents = block_given? ? yield : ""
+ form_text(action, id, html_class, remote, multipart, method) + hidden_fields(options.slice :method, :enforce_utf8) + contents + "</form>"
+ end
- method, remote, multipart = options.values_at(:method, :remote, :multipart)
+ def protect_against_forgery?
+ false
+ end
- form_text(action, id, html_class, remote, multipart, method) + hidden_fields(options.slice :method, :enforce_utf8) + contents + "</form>"
- end
+ def with_locale(testing_locale = :label)
+ old_locale, I18n.locale = I18n.locale, testing_locale
+ yield
+ ensure
+ I18n.locale = old_locale
+ end
- def protect_against_forgery?
- false
- end
+ def with_default_enforce_utf8(value)
+ old_value = ActionView::Helpers::FormTagHelper.default_enforce_utf8
+ ActionView::Helpers::FormTagHelper.default_enforce_utf8 = value
- def with_locale(testing_locale = :label)
- old_locale, I18n.locale = I18n.locale, testing_locale
- yield
- ensure
- I18n.locale = old_locale
- end
+ yield
+ ensure
+ ActionView::Helpers::FormTagHelper.default_enforce_utf8 = old_value
+ end
end
diff --git a/actionview/test/template/form_options_helper_i18n_test.rb b/actionview/test/template/form_options_helper_i18n_test.rb
index 4972ea6511..21295fa547 100644
--- a/actionview/test/template/form_options_helper_i18n_test.rb
+++ b/actionview/test/template/form_options_helper_i18n_test.rb
@@ -1,12 +1,14 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class FormOptionsHelperI18nTests < ActionView::TestCase
tests ActionView::Helpers::FormOptionsHelper
def setup
- @prompt_message = 'Select!'
+ @prompt_message = "Select!"
I18n.backend.send(:init_translations)
- I18n.backend.store_translations :en, :helpers => { :select => { :prompt => @prompt_message } }
+ I18n.backend.store_translations :en, helpers: { select: { prompt: @prompt_message } }
end
def teardown
@@ -14,14 +16,15 @@ class FormOptionsHelperI18nTests < ActionView::TestCase
end
def test_select_with_prompt_true_translates_prompt_message
- I18n.expects(:translate).with('helpers.select.prompt', { :default => 'Please select' })
- select('post', 'category', [], :prompt => true)
+ assert_called_with(I18n, :translate, ["helpers.select.prompt", { default: "Please select" }]) do
+ select("post", "category", [], prompt: true)
+ end
end
def test_select_with_translated_prompt
assert_dom_equal(
%Q(<select id="post_category" name="post[category]"><option value="">#{@prompt_message}</option>\n</select>),
- select('post', 'category', [], :prompt => true)
+ select("post", "category", [], prompt: true)
)
end
end
diff --git a/actionview/test/template/form_options_helper_test.rb b/actionview/test/template/form_options_helper_test.rb
index d7daba8bf3..a2d1474a94 100644
--- a/actionview/test/template/form_options_helper_test.rb
+++ b/actionview/test/template/form_options_helper_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class Map < Hash
def category
@@ -6,24 +8,62 @@ class Map < Hash
end
end
+class CustomEnumerable
+ include Enumerable
+
+ def each
+ yield "one"
+ yield "two"
+ end
+end
+
class FormOptionsHelperTest < ActionView::TestCase
tests ActionView::Helpers::FormOptionsHelper
silence_warnings do
- Post = Struct.new('Post', :title, :author_name, :body, :secret, :written_on, :category, :origin, :allow_comments)
- Continent = Struct.new('Continent', :continent_name, :countries)
- Country = Struct.new('Country', :country_id, :country_name)
- Firm = Struct.new('Firm', :time_zone)
- Album = Struct.new('Album', :id, :title, :genre)
- end
-
- def setup
- @fake_timezones = %w(A B C D E).map do |id|
- tz = stub(:name => id, :to_s => id)
- ActiveSupport::TimeZone.stubs(:[]).with(id).returns(tz)
- tz
+ Post = Struct.new("Post", :title, :author_name, :body, :written_on, :category, :origin, :allow_comments) do
+ private
+ def secret
+ "This is super secret: #{author_name} is not the real author of #{title}"
+ end
+ end
+ Continent = Struct.new("Continent", :continent_name, :countries)
+ Country = Struct.new("Country", :country_id, :country_name)
+ Firm = Struct.new("Firm", :time_zone)
+ Album = Struct.new("Album", :id, :title, :genre)
+ end
+
+ module FakeZones
+ FakeZone = Struct.new(:name) do
+ def to_s; name; end
+ end
+
+ module ClassMethods
+ def [](id); fake_zones ? fake_zones[id] : super; end
+ def all; fake_zones ? fake_zones.values : super; end
+ def dummy; :test; end
+ end
+
+ def self.prepended(base)
+ class << base
+ mattr_accessor(:fake_zones)
+ prepend ClassMethods
+ end
end
- ActiveSupport::TimeZone.stubs(:all).returns(@fake_timezones)
+ end
+
+ ActiveSupport::TimeZone.prepend FakeZones
+
+ setup do
+ ActiveSupport::TimeZone.fake_zones = %w(A B C D E).map do |id|
+ [ id, FakeZones::FakeZone.new(id) ]
+ end.to_h
+
+ @fake_timezones = ActiveSupport::TimeZone.all
+ end
+
+ teardown do
+ ActiveSupport::TimeZone.fake_zones = nil
end
def test_collection_options
@@ -33,6 +73,13 @@ class FormOptionsHelperTest < ActionView::TestCase
)
end
+ def test_collection_options_with_private_value_method
+ assert_deprecated("Using private methods from view helpers is deprecated (calling private Struct::Post#secret)") { options_from_collection_for_select(dummy_posts, "secret", "title") }
+ end
+
+ def test_collection_options_with_private_text_method
+ assert_deprecated("Using private methods from view helpers is deprecated (calling private Struct::Post#secret)") { options_from_collection_for_select(dummy_posts, "author_name", "secret") }
+ end
def test_collection_options_with_preselected_value
assert_dom_equal(
@@ -42,44 +89,44 @@ class FormOptionsHelperTest < ActionView::TestCase
end
def test_collection_options_with_preselected_value_array
- assert_dom_equal(
- "<option value=\"&lt;Abe&gt;\">&lt;Abe&gt; went home</option>\n<option value=\"Babe\" selected=\"selected\">Babe went home</option>\n<option value=\"Cabe\" selected=\"selected\">Cabe went home</option>",
- options_from_collection_for_select(dummy_posts, "author_name", "title", [ "Babe", "Cabe" ])
- )
+ assert_dom_equal(
+ "<option value=\"&lt;Abe&gt;\">&lt;Abe&gt; went home</option>\n<option value=\"Babe\" selected=\"selected\">Babe went home</option>\n<option value=\"Cabe\" selected=\"selected\">Cabe went home</option>",
+ options_from_collection_for_select(dummy_posts, "author_name", "title", [ "Babe", "Cabe" ])
+ )
end
def test_collection_options_with_proc_for_selected
assert_dom_equal(
"<option value=\"&lt;Abe&gt;\">&lt;Abe&gt; went home</option>\n<option value=\"Babe\" selected=\"selected\">Babe went home</option>\n<option value=\"Cabe\">Cabe went home</option>",
- options_from_collection_for_select(dummy_posts, "author_name", "title", lambda{|p| p.author_name == 'Babe' })
+ options_from_collection_for_select(dummy_posts, "author_name", "title", lambda { |p| p.author_name == "Babe" })
)
end
def test_collection_options_with_disabled_value
assert_dom_equal(
"<option value=\"&lt;Abe&gt;\">&lt;Abe&gt; went home</option>\n<option value=\"Babe\" disabled=\"disabled\">Babe went home</option>\n<option value=\"Cabe\">Cabe went home</option>",
- options_from_collection_for_select(dummy_posts, "author_name", "title", :disabled => "Babe")
+ options_from_collection_for_select(dummy_posts, "author_name", "title", disabled: "Babe")
)
end
def test_collection_options_with_disabled_array
assert_dom_equal(
"<option value=\"&lt;Abe&gt;\">&lt;Abe&gt; went home</option>\n<option value=\"Babe\" disabled=\"disabled\">Babe went home</option>\n<option value=\"Cabe\" disabled=\"disabled\">Cabe went home</option>",
- options_from_collection_for_select(dummy_posts, "author_name", "title", :disabled => [ "Babe", "Cabe" ])
+ options_from_collection_for_select(dummy_posts, "author_name", "title", disabled: [ "Babe", "Cabe" ])
)
end
def test_collection_options_with_preselected_and_disabled_value
assert_dom_equal(
"<option value=\"&lt;Abe&gt;\">&lt;Abe&gt; went home</option>\n<option value=\"Babe\" disabled=\"disabled\">Babe went home</option>\n<option value=\"Cabe\" selected=\"selected\">Cabe went home</option>",
- options_from_collection_for_select(dummy_posts, "author_name", "title", :selected => "Cabe", :disabled => "Babe")
+ options_from_collection_for_select(dummy_posts, "author_name", "title", selected: "Cabe", disabled: "Babe")
)
end
def test_collection_options_with_proc_for_disabled
assert_dom_equal(
"<option value=\"&lt;Abe&gt;\">&lt;Abe&gt; went home</option>\n<option value=\"Babe\" disabled=\"disabled\">Babe went home</option>\n<option value=\"Cabe\" disabled=\"disabled\">Cabe went home</option>",
- options_from_collection_for_select(dummy_posts, "author_name", "title", :disabled => lambda {|p| %w(Babe Cabe).include?(p.author_name)})
+ options_from_collection_for_select(dummy_posts, "author_name", "title", disabled: lambda { |p| %w(Babe Cabe).include?(p.author_name) })
)
end
@@ -100,7 +147,7 @@ class FormOptionsHelperTest < ActionView::TestCase
def test_collection_options_with_element_attributes
assert_dom_equal(
"<option value=\"USA\" class=\"bold\">USA</option>",
- options_from_collection_for_select([[ "USA", "USA", { :class => 'bold' } ]], :first, :second)
+ options_from_collection_for_select([[ "USA", "USA", { class: "bold" } ]], :first, :second)
)
end
@@ -123,8 +170,8 @@ class FormOptionsHelperTest < ActionView::TestCase
assert_dom_equal(
"<option selected=\"selected\" type=\"Coach\" value=\"1\">Richard Bandler</option>\n<option type=\"Coachee\" value=\"1\">Richard Bandler</option>",
options_for_select([
- ['Richard Bandler', 1, { type: 'Coach', selected: 'selected' }],
- ['Richard Bandler', 1, { type: 'Coachee' }]
+ ["Richard Bandler", 1, { type: "Coach", selected: "selected" }],
+ ["Richard Bandler", 1, { type: "Coachee" }]
])
)
end
@@ -133,8 +180,8 @@ class FormOptionsHelperTest < ActionView::TestCase
assert_dom_equal(
"<option disabled=\"disabled\" type=\"Coach\" value=\"1\">Richard Bandler</option>\n<option type=\"Coachee\" value=\"1\">Richard Bandler</option>",
options_for_select([
- ['Richard Bandler', 1, { type: 'Coach', disabled: 'disabled' }],
- ['Richard Bandler', 1, { type: 'Coachee' }]
+ ["Richard Bandler", 1, { type: "Coach", disabled: "disabled" }],
+ ["Richard Bandler", 1, { type: "Coachee" }]
])
)
end
@@ -147,37 +194,37 @@ class FormOptionsHelperTest < ActionView::TestCase
end
def test_array_options_for_select_with_selection_array
- assert_dom_equal(
- "<option value=\"Denmark\">Denmark</option>\n<option value=\"&lt;USA&gt;\" selected=\"selected\">&lt;USA&gt;</option>\n<option value=\"Sweden\" selected=\"selected\">Sweden</option>",
- options_for_select([ "Denmark", "<USA>", "Sweden" ], [ "<USA>", "Sweden" ])
- )
+ assert_dom_equal(
+ "<option value=\"Denmark\">Denmark</option>\n<option value=\"&lt;USA&gt;\" selected=\"selected\">&lt;USA&gt;</option>\n<option value=\"Sweden\" selected=\"selected\">Sweden</option>",
+ options_for_select([ "Denmark", "<USA>", "Sweden" ], [ "<USA>", "Sweden" ])
+ )
end
def test_array_options_for_select_with_disabled_value
assert_dom_equal(
"<option value=\"Denmark\">Denmark</option>\n<option value=\"&lt;USA&gt;\" disabled=\"disabled\">&lt;USA&gt;</option>\n<option value=\"Sweden\">Sweden</option>",
- options_for_select([ "Denmark", "<USA>", "Sweden" ], :disabled => "<USA>")
+ options_for_select([ "Denmark", "<USA>", "Sweden" ], disabled: "<USA>")
)
end
def test_array_options_for_select_with_disabled_array
assert_dom_equal(
"<option value=\"Denmark\">Denmark</option>\n<option value=\"&lt;USA&gt;\" disabled=\"disabled\">&lt;USA&gt;</option>\n<option value=\"Sweden\" disabled=\"disabled\">Sweden</option>",
- options_for_select([ "Denmark", "<USA>", "Sweden" ], :disabled => ["<USA>", "Sweden"])
+ options_for_select([ "Denmark", "<USA>", "Sweden" ], disabled: ["<USA>", "Sweden"])
)
end
def test_array_options_for_select_with_selection_and_disabled_value
assert_dom_equal(
"<option value=\"Denmark\" selected=\"selected\">Denmark</option>\n<option value=\"&lt;USA&gt;\" disabled=\"disabled\">&lt;USA&gt;</option>\n<option value=\"Sweden\">Sweden</option>",
- options_for_select([ "Denmark", "<USA>", "Sweden" ], :selected => "Denmark", :disabled => "<USA>")
+ options_for_select([ "Denmark", "<USA>", "Sweden" ], selected: "Denmark", disabled: "<USA>")
)
end
def test_boolean_array_options_for_select_with_selection_and_disabled_value
assert_dom_equal(
"<option value=\"true\">true</option>\n<option value=\"false\" selected=\"selected\">false</option>",
- options_for_select([ true, false ], :selected => false, :disabled => nil)
+ options_for_select([ true, false ], selected: false, disabled: nil)
)
end
@@ -189,18 +236,18 @@ class FormOptionsHelperTest < ActionView::TestCase
end
def test_array_options_for_string_include_in_other_string_bug_fix
- assert_dom_equal(
- "<option value=\"ruby\">ruby</option>\n<option value=\"rubyonrails\" selected=\"selected\">rubyonrails</option>",
- options_for_select([ "ruby", "rubyonrails" ], "rubyonrails")
- )
- assert_dom_equal(
- "<option value=\"ruby\" selected=\"selected\">ruby</option>\n<option value=\"rubyonrails\">rubyonrails</option>",
- options_for_select([ "ruby", "rubyonrails" ], "ruby")
- )
- assert_dom_equal(
- %(<option value="ruby" selected="selected">ruby</option>\n<option value="rubyonrails">rubyonrails</option>\n<option value=""></option>),
- options_for_select([ "ruby", "rubyonrails", nil ], "ruby")
- )
+ assert_dom_equal(
+ "<option value=\"ruby\">ruby</option>\n<option value=\"rubyonrails\" selected=\"selected\">rubyonrails</option>",
+ options_for_select([ "ruby", "rubyonrails" ], "rubyonrails")
+ )
+ assert_dom_equal(
+ "<option value=\"ruby\" selected=\"selected\">ruby</option>\n<option value=\"rubyonrails\">rubyonrails</option>",
+ options_for_select([ "ruby", "rubyonrails" ], "ruby")
+ )
+ assert_dom_equal(
+ %(<option value="ruby" selected="selected">ruby</option>\n<option value="rubyonrails">rubyonrails</option>\n<option value=""></option>),
+ options_for_select([ "ruby", "rubyonrails", nil ], "ruby")
+ )
end
def test_hash_options_for_select
@@ -235,64 +282,64 @@ class FormOptionsHelperTest < ActionView::TestCase
end
def test_collection_options_with_preselected_value_as_string_and_option_value_is_integer
- albums = [ Album.new(1, "first","rap"), Album.new(2, "second","pop")]
+ albums = [ Album.new(1, "first", "rap"), Album.new(2, "second", "pop")]
assert_dom_equal(
- %(<option selected="selected" value="1">rap</option>\n<option value="2">pop</option>),
- options_from_collection_for_select(albums, "id", "genre", :selected => "1")
+ %(<option selected="selected" value="1">rap</option>\n<option value="2">pop</option>),
+ options_from_collection_for_select(albums, "id", "genre", selected: "1")
)
end
def test_collection_options_with_preselected_value_as_integer_and_option_value_is_string
- albums = [ Album.new("1", "first","rap"), Album.new("2", "second","pop")]
+ albums = [ Album.new("1", "first", "rap"), Album.new("2", "second", "pop")]
assert_dom_equal(
- %(<option selected="selected" value="1">rap</option>\n<option value="2">pop</option>),
- options_from_collection_for_select(albums, "id", "genre", :selected => 1)
+ %(<option selected="selected" value="1">rap</option>\n<option value="2">pop</option>),
+ options_from_collection_for_select(albums, "id", "genre", selected: 1)
)
end
def test_collection_options_with_preselected_value_as_string_and_option_value_is_float
- albums = [ Album.new(1.0, "first","rap"), Album.new(2.0, "second","pop")]
+ albums = [ Album.new(1.0, "first", "rap"), Album.new(2.0, "second", "pop")]
assert_dom_equal(
- %(<option value="1.0">rap</option>\n<option value="2.0" selected="selected">pop</option>),
- options_from_collection_for_select(albums, "id", "genre", :selected => "2.0")
+ %(<option value="1.0">rap</option>\n<option value="2.0" selected="selected">pop</option>),
+ options_from_collection_for_select(albums, "id", "genre", selected: "2.0")
)
end
def test_collection_options_with_preselected_value_as_nil
- albums = [ Album.new(1.0, "first","rap"), Album.new(2.0, "second","pop")]
+ albums = [ Album.new(1.0, "first", "rap"), Album.new(2.0, "second", "pop")]
assert_dom_equal(
- %(<option value="1.0">rap</option>\n<option value="2.0">pop</option>),
- options_from_collection_for_select(albums, "id", "genre", :selected => nil)
+ %(<option value="1.0">rap</option>\n<option value="2.0">pop</option>),
+ options_from_collection_for_select(albums, "id", "genre", selected: nil)
)
end
def test_collection_options_with_disabled_value_as_nil
- albums = [ Album.new(1.0, "first","rap"), Album.new(2.0, "second","pop")]
+ albums = [ Album.new(1.0, "first", "rap"), Album.new(2.0, "second", "pop")]
assert_dom_equal(
- %(<option value="1.0">rap</option>\n<option value="2.0">pop</option>),
- options_from_collection_for_select(albums, "id", "genre", :disabled => nil)
+ %(<option value="1.0">rap</option>\n<option value="2.0">pop</option>),
+ options_from_collection_for_select(albums, "id", "genre", disabled: nil)
)
end
def test_collection_options_with_disabled_value_as_array
- albums = [ Album.new(1.0, "first","rap"), Album.new(2.0, "second","pop")]
+ albums = [ Album.new(1.0, "first", "rap"), Album.new(2.0, "second", "pop")]
assert_dom_equal(
- %(<option disabled="disabled" value="1.0">rap</option>\n<option disabled="disabled" value="2.0">pop</option>),
- options_from_collection_for_select(albums, "id", "genre", :disabled => ["1.0", 2.0])
+ %(<option disabled="disabled" value="1.0">rap</option>\n<option disabled="disabled" value="2.0">pop</option>),
+ options_from_collection_for_select(albums, "id", "genre", disabled: ["1.0", 2.0])
)
end
def test_collection_options_with_preselected_values_as_string_array_and_option_value_is_float
- albums = [ Album.new(1.0, "first","rap"), Album.new(2.0, "second","pop"), Album.new(3.0, "third","country") ]
+ albums = [ Album.new(1.0, "first", "rap"), Album.new(2.0, "second", "pop"), Album.new(3.0, "third", "country") ]
assert_dom_equal(
- %(<option value="1.0" selected="selected">rap</option>\n<option value="2.0">pop</option>\n<option value="3.0" selected="selected">country</option>),
- options_from_collection_for_select(albums, "id", "genre", ["1.0","3.0"])
+ %(<option value="1.0" selected="selected">rap</option>\n<option value="2.0">pop</option>\n<option value="3.0" selected="selected">country</option>),
+ options_from_collection_for_select(albums, "id", "genre", ["1.0", "3.0"])
)
end
@@ -303,8 +350,24 @@ class FormOptionsHelperTest < ActionView::TestCase
)
end
+ def test_option_groups_from_collection_for_select_with_callable_group_method
+ group_proc = Proc.new { |c| c.countries }
+ assert_dom_equal(
+ "<optgroup label=\"&lt;Africa&gt;\"><option value=\"&lt;sa&gt;\">&lt;South Africa&gt;</option>\n<option value=\"so\">Somalia</option></optgroup><optgroup label=\"Europe\"><option value=\"dk\" selected=\"selected\">Denmark</option>\n<option value=\"ie\">Ireland</option></optgroup>",
+ option_groups_from_collection_for_select(dummy_continents, group_proc, "continent_name", "country_id", "country_name", "dk")
+ )
+ end
+
+ def test_option_groups_from_collection_for_select_with_callable_group_label_method
+ label_proc = Proc.new { |c| c.continent_name }
+ assert_dom_equal(
+ "<optgroup label=\"&lt;Africa&gt;\"><option value=\"&lt;sa&gt;\">&lt;South Africa&gt;</option>\n<option value=\"so\">Somalia</option></optgroup><optgroup label=\"Europe\"><option value=\"dk\" selected=\"selected\">Denmark</option>\n<option value=\"ie\">Ireland</option></optgroup>",
+ option_groups_from_collection_for_select(dummy_continents, "countries", label_proc, "country_id", "country_name", "dk")
+ )
+ end
+
def test_option_groups_from_collection_for_select_returns_html_safe_string
- assert option_groups_from_collection_for_select(dummy_continents, "countries", "continent_name", "country_id", "country_name", "dk").html_safe?
+ assert_predicate option_groups_from_collection_for_select(dummy_continents, "countries", "continent_name", "country_id", "country_name", "dk"), :html_safe?
end
def test_grouped_options_for_select_with_array
@@ -312,9 +375,9 @@ class FormOptionsHelperTest < ActionView::TestCase
"<optgroup label=\"North America\"><option value=\"US\">United States</option>\n<option value=\"Canada\">Canada</option></optgroup><optgroup label=\"Europe\"><option value=\"GB\">Great Britain</option>\n<option value=\"Germany\">Germany</option></optgroup>",
grouped_options_for_select([
["North America",
- [['United States','US'],"Canada"]],
+ [["United States", "US"], "Canada"]],
["Europe",
- [["Great Britain","GB"], "Germany"]]
+ [["Great Britain", "GB"], "Germany"]]
])
)
end
@@ -323,8 +386,8 @@ class FormOptionsHelperTest < ActionView::TestCase
assert_dom_equal(
"<optgroup label=\"North America\" data-foo=\"bar\"><option value=\"US\">United States</option>\n<option value=\"Canada\">Canada</option></optgroup><optgroup label=\"Europe\" disabled=\"disabled\"><option value=\"GB\">Great Britain</option>\n<option value=\"Germany\">Germany</option></optgroup>",
grouped_options_for_select([
- ["North America", [['United States','US'],"Canada"], :data => { :foo => 'bar' }],
- ["Europe", [["Great Britain","GB"], "Germany"], :disabled => 'disabled']
+ ["North America", [["United States", "US"], "Canada"], data: { foo: "bar" }],
+ ["Europe", [["Great Britain", "GB"], "Germany"], disabled: "disabled"]
])
)
end
@@ -333,116 +396,116 @@ class FormOptionsHelperTest < ActionView::TestCase
assert_dom_equal(
"<optgroup label=\"----------\"><option value=\"US\">US</option>\n<option value=\"Canada\">Canada</option></optgroup><optgroup label=\"----------\"><option value=\"GB\">GB</option>\n<option value=\"Germany\">Germany</option></optgroup>",
- grouped_options_for_select([['US',"Canada"] , ["GB", "Germany"]], nil, divider: "----------")
+ grouped_options_for_select([["US", "Canada"], ["GB", "Germany"]], nil, divider: "----------")
)
end
def test_grouped_options_for_select_with_selected_and_prompt
assert_dom_equal(
- "<option value=\"\">Choose a product...</option><optgroup label=\"Hats\"><option value=\"Baseball Cap\">Baseball Cap</option>\n<option selected=\"selected\" value=\"Cowboy Hat\">Cowboy Hat</option></optgroup>",
- grouped_options_for_select([["Hats", ["Baseball Cap","Cowboy Hat"]]], "Cowboy Hat", prompt: "Choose a product...")
+ "<option value=\"\">Choose a product...</option><optgroup label=\"Hats\"><option value=\"Baseball Cap\">Baseball Cap</option>\n<option selected=\"selected\" value=\"Cowboy Hat\">Cowboy Hat</option></optgroup>",
+ grouped_options_for_select([["Hats", ["Baseball Cap", "Cowboy Hat"]]], "Cowboy Hat", prompt: "Choose a product...")
)
end
def test_grouped_options_for_select_with_selected_and_prompt_true
assert_dom_equal(
- "<option value=\"\">Please select</option><optgroup label=\"Hats\"><option value=\"Baseball Cap\">Baseball Cap</option>\n<option selected=\"selected\" value=\"Cowboy Hat\">Cowboy Hat</option></optgroup>",
- grouped_options_for_select([["Hats", ["Baseball Cap","Cowboy Hat"]]], "Cowboy Hat", prompt: true)
+ "<option value=\"\">Please select</option><optgroup label=\"Hats\"><option value=\"Baseball Cap\">Baseball Cap</option>\n<option selected=\"selected\" value=\"Cowboy Hat\">Cowboy Hat</option></optgroup>",
+ grouped_options_for_select([["Hats", ["Baseball Cap", "Cowboy Hat"]]], "Cowboy Hat", prompt: true)
)
end
def test_grouped_options_for_select_returns_html_safe_string
- assert grouped_options_for_select([["Hats", ["Baseball Cap","Cowboy Hat"]]]).html_safe?
+ assert_predicate grouped_options_for_select([["Hats", ["Baseball Cap", "Cowboy Hat"]]]), :html_safe?
end
def test_grouped_options_for_select_with_prompt_returns_html_escaped_string
assert_dom_equal(
"<option value=\"\">&lt;Choose One&gt;</option><optgroup label=\"Hats\"><option value=\"Baseball Cap\">Baseball Cap</option>\n<option value=\"Cowboy Hat\">Cowboy Hat</option></optgroup>",
- grouped_options_for_select([["Hats", ["Baseball Cap","Cowboy Hat"]]], nil, prompt: '<Choose One>'))
+ grouped_options_for_select([["Hats", ["Baseball Cap", "Cowboy Hat"]]], nil, prompt: "<Choose One>"))
end
def test_optgroups_with_with_options_with_hash
assert_dom_equal(
- "<optgroup label=\"North America\"><option value=\"United States\">United States</option>\n<option value=\"Canada\">Canada</option></optgroup><optgroup label=\"Europe\"><option value=\"Denmark\">Denmark</option>\n<option value=\"Germany\">Germany</option></optgroup>",
- grouped_options_for_select({'North America' => ['United States','Canada'], 'Europe' => ['Denmark','Germany']})
+ "<optgroup label=\"North America\"><option value=\"United States\">United States</option>\n<option value=\"Canada\">Canada</option></optgroup><optgroup label=\"Europe\"><option value=\"Denmark\">Denmark</option>\n<option value=\"Germany\">Germany</option></optgroup>",
+ grouped_options_for_select("North America" => ["United States", "Canada"], "Europe" => ["Denmark", "Germany"])
)
end
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" +
+ 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" \
"<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" +
+ 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" \
"<option value=\"E\">E</option>",
opts
end
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" +
+ 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" \
"<option value=\"D\">D</option>",
opts
end
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" +
+ 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" \
"<option value=\"D\">D</option>",
opts
end
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" +
+ 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" \
"<option value=\"D\">D</option>",
opts
end
def test_time_zone_options_with_priority_zones_does_not_mutate_time_zones
original_zones = ActiveSupport::TimeZone.all.dup
- zones = [ ActiveSupport::TimeZone.new( "B" ), ActiveSupport::TimeZone.new( "E" ) ]
+ zones = [ ActiveSupport::TimeZone.new("B"), ActiveSupport::TimeZone.new("E") ]
time_zone_options_for_select(nil, zones)
assert_equal original_zones, ActiveSupport::TimeZone.all
end
def test_time_zone_options_returns_html_safe_string
- assert time_zone_options_for_select.html_safe?
+ assert_predicate time_zone_options_for_select, :html_safe?
end
def test_select
@@ -457,7 +520,17 @@ class FormOptionsHelperTest < ActionView::TestCase
def test_select_without_multiple
assert_dom_equal(
"<select id=\"post_category\" name=\"post[category]\"></select>",
- select(:post, :category, "", {}, :multiple => false)
+ select(:post, :category, "", {}, { multiple: false })
+ )
+ end
+
+ def test_required_select_with_default_and_selected_placeholder
+ assert_dom_equal(
+ ['<select required="required" name="post[category]" id="post_category"><option disabled="disabled" selected="selected" value="">Choose one</option>',
+ '<option value="lifestyle">lifestyle</option>',
+ '<option value="programming">programming</option>',
+ '<option value="spiritual">spiritual</option></select>'].join("\n"),
+ select(:post, :category, ["lifestyle", "programming", "spiritual"], { selected: "", disabled: "", prompt: "Choose one" }, { required: true })
)
end
@@ -471,9 +544,9 @@ class FormOptionsHelperTest < ActionView::TestCase
assert_dom_equal(
[
- %Q{<select id="post_origin" name="post[origin]"><optgroup label="&lt;Africa&gt;"><option value="&lt;sa&gt;">&lt;South Africa&gt;</option>},
- %Q{<option value="so">Somalia</option></optgroup><optgroup label="Europe"><option value="dk">Denmark</option>},
- %Q{<option value="ie">Ireland</option></optgroup></select>},
+ '<select id="post_origin" name="post[origin]"><optgroup label="&lt;Africa&gt;"><option value="&lt;sa&gt;">&lt;South Africa&gt;</option>',
+ '<option value="so">Somalia</option></optgroup><optgroup label="Europe"><option value="dk">Denmark</option>',
+ '<option value="ie">Ireland</option></optgroup></select>',
].join("\n"),
select("post", "origin", countries_by_continent)
)
@@ -489,9 +562,9 @@ class FormOptionsHelperTest < ActionView::TestCase
assert_dom_equal(
[
- %Q{<select id="post_origin" name="post[origin]"><optgroup label="&lt;Africa&gt;"><option value="&lt;sa&gt;">&lt;South Africa&gt;</option>},
- %Q{<option value="so">Somalia</option></optgroup><optgroup label="Europe"><option value="dk">Denmark</option>},
- %Q{<option value="ie">Ireland</option></optgroup></select>},
+ '<select id="post_origin" name="post[origin]"><optgroup label="&lt;Africa&gt;"><option value="&lt;sa&gt;">&lt;South Africa&gt;</option>',
+ '<option value="so">Somalia</option></optgroup><optgroup label="Europe"><option value="dk">Denmark</option>',
+ '<option value="ie">Ireland</option></optgroup></select>',
].join("\n"),
select("post", "origin", countries_by_continent)
)
@@ -537,7 +610,7 @@ class FormOptionsHelperTest < ActionView::TestCase
@post = Post.new
@post.category = "<mus>"
- output_buffer = fields_for :post, @post, :index => 108 do |f|
+ output_buffer = fields_for :post, @post, index: 108 do |f|
concat f.select(:category, %w( abe <mus> hest))
end
@@ -564,10 +637,10 @@ class FormOptionsHelperTest < ActionView::TestCase
def test_select_under_fields_for_with_string_and_given_prompt
@post = Post.new
- options = "<option value=\"abe\">abe</option><option value=\"mus\">mus</option><option value=\"hest\">hest</option>".html_safe
+ options = raw("<option value=\"abe\">abe</option><option value=\"mus\">mus</option><option value=\"hest\">hest</option>")
output_buffer = fields_for :post, @post do |f|
- concat f.select(:category, options, :prompt => 'The prompt')
+ concat f.select(:category, options, prompt: "The prompt")
end
assert_dom_equal(
@@ -595,7 +668,7 @@ class FormOptionsHelperTest < ActionView::TestCase
@post = Post.new
output_buffer = fields_for :post, @post do |f|
- concat(f.select(:category) {})
+ concat(f.select(:category) { })
end
assert_dom_equal(
@@ -605,7 +678,7 @@ class FormOptionsHelperTest < ActionView::TestCase
end
def test_select_with_multiple_to_add_hidden_input
- output_buffer = select(:post, :category, "", {}, :multiple => true)
+ output_buffer = select(:post, :category, "", {}, { multiple: true })
assert_dom_equal(
"<input type=\"hidden\" name=\"post[category][]\" value=\"\"/><select multiple=\"multiple\" id=\"post_category\" name=\"post[category][]\"></select>",
output_buffer
@@ -613,7 +686,7 @@ class FormOptionsHelperTest < ActionView::TestCase
end
def test_select_with_multiple_and_without_hidden_input
- output_buffer = select(:post, :category, "", {:include_hidden => false}, :multiple => true)
+ output_buffer = select(:post, :category, "", { include_hidden: false }, { multiple: true })
assert_dom_equal(
"<select multiple=\"multiple\" id=\"post_category\" name=\"post[category][]\"></select>",
output_buffer
@@ -621,7 +694,7 @@ class FormOptionsHelperTest < ActionView::TestCase
end
def test_select_with_multiple_and_with_explicit_name_ending_with_brackets
- output_buffer = select(:post, :category, [], {include_hidden: false}, multiple: true, name: 'post[category][]')
+ output_buffer = select(:post, :category, [], { include_hidden: false }, { multiple: true, name: "post[category][]" })
assert_dom_equal(
"<select multiple=\"multiple\" id=\"post_category\" name=\"post[category][]\"></select>",
output_buffer
@@ -629,7 +702,7 @@ class FormOptionsHelperTest < ActionView::TestCase
end
def test_select_with_multiple_and_disabled_to_add_disabled_hidden_input
- output_buffer = select(:post, :category, "", {}, :multiple => true, :disabled => true)
+ output_buffer = select(:post, :category, "", {}, { multiple: true, disabled: true })
assert_dom_equal(
"<input disabled=\"disabled\"type=\"hidden\" name=\"post[category][]\" value=\"\"/><select multiple=\"multiple\" disabled=\"disabled\" id=\"post_category\" name=\"post[category][]\"></select>",
output_buffer
@@ -641,14 +714,14 @@ class FormOptionsHelperTest < ActionView::TestCase
@post.category = "<mus>"
assert_dom_equal(
"<select id=\"post_category\" name=\"post[category]\"><option value=\"\"></option>\n<option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\" selected=\"selected\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
- select("post", "category", %w( abe <mus> hest), :include_blank => true)
+ select("post", "category", %w( abe <mus> hest), include_blank: true)
)
end
def test_select_with_include_blank_false_and_required
@post = Post.new
@post.category = "<mus>"
- e = assert_raises(ArgumentError) { select("post", "category", %w( abe <mus> hest), { include_blank: false }, required: 'required') }
+ e = assert_raises(ArgumentError) { select("post", "category", %w( abe <mus> hest), { include_blank: false }, { required: "required" }) }
assert_match(/include_blank cannot be false for a required field./, e.message)
end
@@ -657,7 +730,7 @@ class FormOptionsHelperTest < ActionView::TestCase
@post.category = "<mus>"
assert_dom_equal(
"<select id=\"post_category\" name=\"post[category]\"><option value=\"\">None</option>\n<option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\" selected=\"selected\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
- select("post", "category", %w( abe <mus> hest), :include_blank => 'None')
+ select("post", "category", %w( abe <mus> hest), include_blank: "None")
)
end
@@ -666,7 +739,7 @@ class FormOptionsHelperTest < ActionView::TestCase
@post.category = "<mus>"
assert_dom_equal(
"<select id=\"post_category\" name=\"post[category]\"><option value=\"\">&lt;None&gt;</option>\n<option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\" selected=\"selected\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
- select("post", "category", %w( abe <mus> hest), :include_blank => '<None>')
+ select("post", "category", %w( abe <mus> hest), include_blank: "<None>")
)
end
@@ -675,7 +748,7 @@ class FormOptionsHelperTest < ActionView::TestCase
@post.category = ""
assert_dom_equal(
"<select id=\"post_category\" name=\"post[category]\"><option value=\"\">Please select</option>\n<option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
- select("post", "category", %w( abe <mus> hest), :prompt => true)
+ select("post", "category", %w( abe <mus> hest), prompt: true)
)
end
@@ -684,7 +757,7 @@ class FormOptionsHelperTest < ActionView::TestCase
@post.category = "<mus>"
assert_dom_equal(
"<select id=\"post_category\" name=\"post[category]\"><option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\" selected=\"selected\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
- select("post", "category", %w( abe <mus> hest), :prompt => true)
+ select("post", "category", %w( abe <mus> hest), prompt: true)
)
end
@@ -693,7 +766,7 @@ class FormOptionsHelperTest < ActionView::TestCase
@post.category = ""
assert_dom_equal(
"<select id=\"post_category\" name=\"post[category]\"><option value=\"\">The prompt</option>\n<option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
- select("post", "category", %w( abe <mus> hest), :prompt => 'The prompt')
+ select("post", "category", %w( abe <mus> hest), prompt: "The prompt")
)
end
@@ -701,7 +774,7 @@ class FormOptionsHelperTest < ActionView::TestCase
@post = Post.new
assert_dom_equal(
"<select id=\"post_category\" name=\"post[category]\"><option value=\"\">&lt;The prompt&gt;</option>\n<option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
- select("post", "category", %w( abe <mus> hest), :prompt => '<The prompt>')
+ select("post", "category", %w( abe <mus> hest), prompt: "<The prompt>")
)
end
@@ -710,16 +783,25 @@ class FormOptionsHelperTest < ActionView::TestCase
@post.category = ""
assert_dom_equal(
"<select id=\"post_category\" name=\"post[category]\"><option value=\"\">Please select</option>\n<option value=\"\"></option>\n<option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
- select("post", "category", %w( abe <mus> hest), :prompt => true, :include_blank => true)
+ select("post", "category", %w( abe <mus> hest), prompt: true, include_blank: true)
)
end
- def test_empty
+ def test_select_with_empty
@post = Post.new
@post.category = ""
assert_dom_equal(
"<select id=\"post_category\" name=\"post[category]\"><option value=\"\">Please select</option>\n<option value=\"\"></option>\n</select>",
- select("post", "category", [], :prompt => true, :include_blank => true)
+ select("post", "category", [], prompt: true, include_blank: true)
+ )
+ end
+
+ def test_select_with_html_options
+ @post = Post.new
+ @post.category = ""
+ assert_dom_equal(
+ "<select class=\"disabled\" disabled=\"disabled\" name=\"post[category]\" id=\"post_category\"><option value=\"\">Please select</option>\n<option value=\"\"></option>\n</select>",
+ select("post", "category", [], { prompt: true, include_blank: true }, { class: "disabled", disabled: true })
)
end
@@ -735,51 +817,51 @@ class FormOptionsHelperTest < ActionView::TestCase
def test_required_select
assert_dom_equal(
%(<select id="post_category" name="post[category]" required="required"><option value=""></option>\n<option value="abe">abe</option>\n<option value="mus">mus</option>\n<option value="hest">hest</option></select>),
- select("post", "category", %w(abe mus hest), {}, required: true)
+ select("post", "category", %w(abe mus hest), {}, { required: true })
)
end
def test_required_select_with_include_blank_prompt
assert_dom_equal(
%(<select id="post_category" name="post[category]" required="required"><option value="">Select one</option>\n<option value="abe">abe</option>\n<option value="mus">mus</option>\n<option value="hest">hest</option></select>),
- select("post", "category", %w(abe mus hest), { include_blank: "Select one" }, required: true)
+ select("post", "category", %w(abe mus hest), { include_blank: "Select one" }, { required: true })
)
end
def test_required_select_with_prompt
assert_dom_equal(
%(<select id="post_category" name="post[category]" required="required"><option value="">Select one</option>\n<option value="abe">abe</option>\n<option value="mus">mus</option>\n<option value="hest">hest</option></select>),
- select("post", "category", %w(abe mus hest), { prompt: "Select one" }, required: true)
+ select("post", "category", %w(abe mus hest), { prompt: "Select one" }, { required: true })
)
end
def test_required_select_display_size_equals_to_one
assert_dom_equal(
%(<select id="post_category" name="post[category]" required="required" size="1"><option value=""></option>\n<option value="abe">abe</option>\n<option value="mus">mus</option>\n<option value="hest">hest</option></select>),
- select("post", "category", %w(abe mus hest), {}, required: true, size: 1)
+ select("post", "category", %w(abe mus hest), {}, { required: true, size: 1 })
)
end
def test_required_select_with_display_size_bigger_than_one
assert_dom_equal(
%(<select id="post_category" name="post[category]" required="required" size="2"><option value="abe">abe</option>\n<option value="mus">mus</option>\n<option value="hest">hest</option></select>),
- select("post", "category", %w(abe mus hest), {}, required: true, size: 2)
+ select("post", "category", %w(abe mus hest), {}, { required: true, size: 2 })
)
end
def test_required_select_with_multiple_option
assert_dom_equal(
%(<input name="post[category][]" type="hidden" value=""/><select id="post_category" multiple="multiple" name="post[category][]" required="required"><option value="abe">abe</option>\n<option value="mus">mus</option>\n<option value="hest">hest</option></select>),
- select("post", "category", %w(abe mus hest), {}, required: true, multiple: true)
+ select("post", "category", %w(abe mus hest), {}, { required: true, multiple: true })
)
end
- def test_select_with_fixnum
+ def test_select_with_integer
@post = Post.new
@post.category = ""
assert_dom_equal(
"<select id=\"post_category\" name=\"post[category]\"><option value=\"\">Please select</option>\n<option value=\"\"></option>\n<option value=\"1\">1</option></select>",
- select("post", "category", [1], :prompt => true, :include_blank => true)
+ select("post", "category", [1], prompt: true, include_blank: true)
)
end
@@ -788,7 +870,7 @@ class FormOptionsHelperTest < ActionView::TestCase
@post.category = ""
assert_dom_equal(
"<select id=\"post_category\" name=\"post[category]\"><option value=\"\">Please select</option>\n<option value=\"\"></option>\n<option value=\"number\">Number</option>\n<option value=\"text\">Text</option>\n<option value=\"boolean\">Yes/No</option></select>",
- select("post", "category", [["Number", "number"], ["Text", "text"], ["Yes/No", "boolean"]], :prompt => true, :include_blank => true)
+ select("post", "category", [["Number", "number"], ["Text", "text"], ["Yes/No", "boolean"]], prompt: true, include_blank: true)
)
end
@@ -797,7 +879,7 @@ class FormOptionsHelperTest < ActionView::TestCase
@post.category = "<mus>"
assert_dom_equal(
"<select id=\"post_category\" name=\"post[category]\"><option value=\"abe\" selected=\"selected\">abe</option>\n<option value=\"&lt;mus&gt;\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
- select("post", "category", %w( abe <mus> hest ), :selected => 'abe')
+ select("post", "category", %w( abe <mus> hest ), selected: "abe")
)
end
@@ -809,14 +891,14 @@ class FormOptionsHelperTest < ActionView::TestCase
assert_dom_equal(
expected,
- select("album[]", "genre", %w[rap rock country], {}, { :index => nil })
+ select("album[]", "genre", %w[rap rock country], {}, { index: nil })
)
end
def test_select_escapes_options
assert_dom_equal(
'<select id="post_title" name="post[title]">&lt;script&gt;alert(1)&lt;/script&gt;</select>',
- select('post', 'title', '<script>alert(1)</script>')
+ select("post", "title", "<script>alert(1)</script>")
)
end
@@ -825,7 +907,7 @@ class FormOptionsHelperTest < ActionView::TestCase
@post.category = "<mus>"
assert_dom_equal(
"<select id=\"post_category\" name=\"post[category]\"><option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
- select("post", "category", %w( abe <mus> hest ), :selected => nil)
+ select("post", "category", %w( abe <mus> hest ), selected: nil)
)
end
@@ -834,7 +916,7 @@ class FormOptionsHelperTest < ActionView::TestCase
@post.category = "<mus>"
assert_dom_equal(
"<select id=\"post_category\" name=\"post[category]\"><option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\" selected=\"selected\">&lt;mus&gt;</option>\n<option value=\"hest\" disabled=\"disabled\">hest</option></select>",
- select("post", "category", %w( abe <mus> hest ), :disabled => 'hest')
+ select("post", "category", %w( abe <mus> hest ), disabled: "hest")
)
end
@@ -842,7 +924,7 @@ class FormOptionsHelperTest < ActionView::TestCase
@post = Post.new
assert_dom_equal(
"<select id=\"post_locale\" name=\"post[locale]\"><option value=\"en\">en</option>\n<option value=\"ru\" selected=\"selected\">ru</option></select>",
- select("post", "locale", %w( en ru ), :selected => 'ru')
+ select("post", "locale", %w( en ru ), selected: "ru")
)
end
@@ -850,7 +932,7 @@ class FormOptionsHelperTest < ActionView::TestCase
@post = Post.new
assert_dom_equal(
"<select id=\"post_category\" name=\"post[category]\"><option value=\"one\">one</option>\n<option selected=\"selected\" value=\"two\">two</option></select>",
- select("post", "category", %w( one two ), :selected => 'two', :prompt => true)
+ select("post", "category", %w( one two ), selected: "two", prompt: true)
)
end
@@ -859,7 +941,7 @@ class FormOptionsHelperTest < ActionView::TestCase
@post.category = "<mus>"
assert_dom_equal(
"<select id=\"post_category\" name=\"post[category]\"><option value=\"abe\" disabled=\"disabled\">abe</option>\n<option value=\"&lt;mus&gt;\" selected=\"selected\">&lt;mus&gt;</option>\n<option value=\"hest\" disabled=\"disabled\">hest</option></select>",
- select("post", "category", %w( abe <mus> hest ), :disabled => ['hest', 'abe'])
+ select("post", "category", %w( abe <mus> hest ), disabled: ["hest", "abe"])
)
end
@@ -872,6 +954,14 @@ class FormOptionsHelperTest < ActionView::TestCase
)
end
+ def test_select_with_enumerable
+ @post = Post.new
+ assert_dom_equal(
+ "<select id=\"post_category\" name=\"post[category]\"><option value=\"one\">one</option>\n<option value=\"two\">two</option></select>",
+ select("post", "category", CustomEnumerable.new)
+ )
+ end
+
def test_collection_select
@post = Post.new
@post.author_name = "Babe"
@@ -900,7 +990,7 @@ class FormOptionsHelperTest < ActionView::TestCase
@post = Post.new
@post.author_name = "Babe"
- output_buffer = fields_for :post, @post, :index => 815 do |f|
+ output_buffer = fields_for :post, @post, index: 815 do |f|
concat f.collection_select(:author_name, dummy_posts, :author_name, :author_name)
end
@@ -931,7 +1021,7 @@ class FormOptionsHelperTest < ActionView::TestCase
assert_dom_equal(
"<select id=\"post_author_name\" name=\"post[author_name]\" style=\"width: 200px\"><option value=\"\"></option>\n<option value=\"&lt;Abe&gt;\">&lt;Abe&gt;</option>\n<option value=\"Babe\" selected=\"selected\">Babe</option>\n<option value=\"Cabe\">Cabe</option></select>",
- collection_select("post", "author_name", dummy_posts, "author_name", "author_name", { :include_blank => true }, "style" => "width: 200px")
+ collection_select("post", "author_name", dummy_posts, "author_name", "author_name", { include_blank: true }, { "style" => "width: 200px" })
)
end
@@ -941,7 +1031,7 @@ class FormOptionsHelperTest < ActionView::TestCase
assert_dom_equal(
"<select id=\"post_author_name\" name=\"post[author_name]\" style=\"width: 200px\"><option value=\"\">No Selection</option>\n<option value=\"&lt;Abe&gt;\">&lt;Abe&gt;</option>\n<option value=\"Babe\" selected=\"selected\">Babe</option>\n<option value=\"Cabe\">Cabe</option></select>",
- collection_select("post", "author_name", dummy_posts, "author_name", "author_name", { :include_blank => 'No Selection' }, "style" => "width: 200px")
+ collection_select("post", "author_name", dummy_posts, "author_name", "author_name", { include_blank: "No Selection" }, { "style" => "width: 200px" })
)
end
@@ -952,10 +1042,10 @@ class FormOptionsHelperTest < ActionView::TestCase
expected = "<input type=\"hidden\" name=\"post[author_name][]\" value=\"\"/><select id=\"post_author_name\" name=\"post[author_name][]\" multiple=\"multiple\"><option value=\"\"></option>\n<option value=\"&lt;Abe&gt;\">&lt;Abe&gt;</option>\n<option value=\"Babe\" selected=\"selected\">Babe</option>\n<option value=\"Cabe\">Cabe</option></select>"
# Should suffix default name with [].
- assert_dom_equal expected, collection_select("post", "author_name", dummy_posts, "author_name", "author_name", { :include_blank => true }, :multiple => true)
+ assert_dom_equal expected, collection_select("post", "author_name", dummy_posts, "author_name", "author_name", { include_blank: true }, { multiple: true })
# Shouldn't suffix custom name with [].
- assert_dom_equal expected, collection_select("post", "author_name", dummy_posts, "author_name", "author_name", { :include_blank => true, :name => 'post[author_name][]' }, :multiple => true)
+ assert_dom_equal expected, collection_select("post", "author_name", dummy_posts, "author_name", "author_name", { include_blank: true, name: "post[author_name][]" }, { multiple: true })
end
def test_collection_select_with_blank_and_selected
@@ -964,7 +1054,7 @@ class FormOptionsHelperTest < ActionView::TestCase
assert_dom_equal(
%{<select id="post_author_name" name="post[author_name]"><option value=""></option>\n<option value="&lt;Abe&gt;" selected="selected">&lt;Abe&gt;</option>\n<option value="Babe">Babe</option>\n<option value="Cabe">Cabe</option></select>},
- collection_select("post", "author_name", dummy_posts, "author_name", "author_name", {:include_blank => true, :selected => "<Abe>"})
+ collection_select("post", "author_name", dummy_posts, "author_name", "author_name", include_blank: true, selected: "<Abe>")
)
end
@@ -974,7 +1064,7 @@ class FormOptionsHelperTest < ActionView::TestCase
assert_dom_equal(
"<select id=\"post_author_name\" name=\"post[author_name]\"><option value=\"&lt;Abe&gt;\">&lt;Abe&gt;</option>\n<option value=\"Babe\" selected=\"selected\">Babe</option>\n<option value=\"Cabe\" disabled=\"disabled\">Cabe</option></select>",
- collection_select("post", "author_name", dummy_posts, "author_name", "author_name", :disabled => 'Cabe')
+ collection_select("post", "author_name", dummy_posts, "author_name", "author_name", disabled: "Cabe")
)
end
@@ -998,13 +1088,13 @@ 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>" +
+ 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>" \
"</select>",
html
end
@@ -1017,12 +1107,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
)
@@ -1031,17 +1121,17 @@ class FormOptionsHelperTest < ActionView::TestCase
def test_time_zone_select_under_fields_for_with_index
@firm = Firm.new("D")
- output_buffer = fields_for :firm, @firm, :index => 305 do |f|
+ output_buffer = fields_for :firm, @firm, index: 305 do |f|
concat f.time_zone_select(:time_zone)
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
)
@@ -1056,12 +1146,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
)
@@ -1069,28 +1159,28 @@ 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>" +
+ 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>" \
"</select>",
html
end
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>" +
+ 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>" \
"</select>",
html
end
@@ -1098,64 +1188,64 @@ class FormOptionsHelperTest < ActionView::TestCase
def test_time_zone_select_with_style
@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>" +
+ { "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>" \
"</select>",
html
assert_dom_equal html, time_zone_select("firm", "time_zone", nil, {},
- :style => "color: red")
+ { style: "color: red" })
end
def test_time_zone_select_with_blank_and_style
@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>" +
+ { 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>" \
"</select>",
html
assert_dom_equal html, time_zone_select("firm", "time_zone", nil,
- { :include_blank => true }, :style => "color: red")
+ { include_blank: true }, { style: "color: red" })
end
def test_time_zone_select_with_blank_as_string_and_style
@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>" +
+ { 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>" \
"</select>",
html
assert_dom_equal html, time_zone_select("firm", "time_zone", nil,
- { :include_blank => 'No Zone' }, :style => "color: red")
+ { include_blank: "No Zone" }, { style: "color: red" })
end
def test_time_zone_select_with_priority_zones
@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>" +
+ 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>" \
"</select>",
html
end
@@ -1163,67 +1253,87 @@ class FormOptionsHelperTest < ActionView::TestCase
def test_time_zone_select_with_priority_zones_as_regexp
@firm = Firm.new("D")
- @fake_timezones.each_with_index do |tz, i|
- tz.stubs(:=~).returns(i.zero? || i == 3)
+ @fake_timezones.each do |tz|
+ def tz.=~(re); %(A D).include?(name) end
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
- def test_time_zone_select_with_priority_zones_as_regexp_using_grep_finds_no_zones
+ def test_time_zone_select_with_priority_zones_is_not_implemented_with_grep
@firm = Firm.new("D")
- priority_zones = /A|D/
+ # `time_zone_select` can't be written with `grep` because Active Support
+ # time zones don't support implicit string coercion with `to_str`.
@fake_timezones.each do |tz|
- priority_zones.stubs(:===).with(tz).raises(Exception)
+ def tz.===(zone); raise Exception; end
end
- html = time_zone_select("firm", "time_zone", priority_zones)
- 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>" +
+ 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>" \
"</select>",
html
end
- def test_time_zone_select_with_default_time_zone_and_nil_value
- @firm = Firm.new()
- @firm.time_zone = nil
+ def test_time_zone_select_with_priority_zones_and_errors
+ @firm = Firm.new("D")
+ @firm.extend ActiveModel::Validations
+ @firm.errors[:time_zone] << "invalid"
+ zones = [ ActiveSupport::TimeZone.new("A"), ActiveSupport::TimeZone.new("D") ]
+ html = time_zone_select("firm", "time_zone", zones)
+ assert_dom_equal "<div class=\"field_with_errors\">" \
+ "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" \
+ "<option value=\"A\">A</option>\n" \
+ "<option value=\"D\" selected=\"selected\">D</option>" \
+ "<option value=\"\" disabled=\"disabled\">-------------</option>\n" \
+ "<option value=\"B\">B</option>\n" \
+ "<option value=\"C\">C</option>\n" \
+ "<option value=\"E\">E</option>" \
+ "</select>" \
+ "</div>",
+ html
+ end
- 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>" +
- "</select>",
- html
+ def test_time_zone_select_with_default_time_zone_and_nil_value
+ @firm = Firm.new()
+ @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>" \
+ "</select>",
+ html
end
def test_time_zone_select_with_default_time_zone_and_value
- @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>" +
+ @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>" \
"</select>",
html
end
@@ -1231,86 +1341,86 @@ class FormOptionsHelperTest < ActionView::TestCase
def test_options_for_select_with_element_attributes
assert_dom_equal(
"<option value=\"&lt;Denmark&gt;\" class=\"bold\">&lt;Denmark&gt;</option>\n<option value=\"USA\" onclick=\"alert(&#39;Hello World&#39;);\">USA</option>\n<option value=\"Sweden\">Sweden</option>\n<option value=\"Germany\">Germany</option>",
- options_for_select([ [ "<Denmark>", { :class => 'bold' } ], [ "USA", { :onclick => "alert('Hello World');" } ], [ "Sweden" ], "Germany" ])
+ options_for_select([ [ "<Denmark>", { class: "bold" } ], [ "USA", { onclick: "alert('Hello World');" } ], [ "Sweden" ], "Germany" ])
)
end
def test_options_for_select_with_data_element
assert_dom_equal(
"<option value=\"&lt;Denmark&gt;\" data-test=\"bold\">&lt;Denmark&gt;</option>",
- options_for_select([ [ "<Denmark>", { :data => { :test => 'bold' } } ] ])
+ options_for_select([ [ "<Denmark>", { data: { test: "bold" } } ] ])
)
end
def test_options_for_select_with_data_element_with_special_characters
assert_dom_equal(
"<option value=\"&lt;Denmark&gt;\" data-test=\"&lt;bold&gt;\">&lt;Denmark&gt;</option>",
- options_for_select([ [ "<Denmark>", { :data => { :test => '<bold>' } } ] ])
+ options_for_select([ [ "<Denmark>", { data: { test: "<bold>" } } ] ])
)
end
def test_options_for_select_with_element_attributes_and_selection
assert_dom_equal(
"<option value=\"&lt;Denmark&gt;\">&lt;Denmark&gt;</option>\n<option value=\"USA\" class=\"bold\" selected=\"selected\">USA</option>\n<option value=\"Sweden\">Sweden</option>",
- options_for_select([ "<Denmark>", [ "USA", { :class => 'bold' } ], "Sweden" ], "USA")
+ options_for_select([ "<Denmark>", [ "USA", { class: "bold" } ], "Sweden" ], "USA")
)
end
def test_options_for_select_with_element_attributes_and_selection_array
assert_dom_equal(
"<option value=\"&lt;Denmark&gt;\">&lt;Denmark&gt;</option>\n<option value=\"USA\" class=\"bold\" selected=\"selected\">USA</option>\n<option value=\"Sweden\" selected=\"selected\">Sweden</option>",
- options_for_select([ "<Denmark>", [ "USA", { :class => 'bold' } ], "Sweden" ], [ "USA", "Sweden" ])
+ options_for_select([ "<Denmark>", [ "USA", { class: "bold" } ], "Sweden" ], [ "USA", "Sweden" ])
)
end
def test_options_for_select_with_special_characters
assert_dom_equal(
"<option value=\"&lt;Denmark&gt;\" onclick=\"alert(&quot;&lt;code&gt;&quot;)\">&lt;Denmark&gt;</option>",
- options_for_select([ [ "<Denmark>", { :onclick => %(alert("<code>")) } ] ])
+ options_for_select([ [ "<Denmark>", { onclick: %(alert("<code>")) } ] ])
)
end
def test_option_html_attributes_with_no_array_element
- assert_equal({}, option_html_attributes('foo'))
+ assert_equal({}, option_html_attributes("foo"))
end
def test_option_html_attributes_without_hash
- assert_equal({}, option_html_attributes([ 'foo', 'bar' ]))
+ assert_equal({}, option_html_attributes([ "foo", "bar" ]))
end
def test_option_html_attributes_with_single_element_hash
assert_equal(
- {:class => 'fancy'},
- option_html_attributes([ 'foo', 'bar', { :class => 'fancy' } ])
+ { class: "fancy" },
+ option_html_attributes([ "foo", "bar", { class: "fancy" } ])
)
end
def test_option_html_attributes_with_multiple_element_hash
assert_equal(
- {:class => 'fancy', 'onclick' => "alert('Hello World');"},
- option_html_attributes([ 'foo', 'bar', { :class => 'fancy', 'onclick' => "alert('Hello World');" } ])
+ { :class => "fancy", "onclick" => "alert('Hello World');" },
+ option_html_attributes([ "foo", "bar", { :class => "fancy", "onclick" => "alert('Hello World');" } ])
)
end
def test_option_html_attributes_with_multiple_hashes
assert_equal(
- {:class => 'fancy', 'onclick' => "alert('Hello World');"},
- option_html_attributes([ 'foo', 'bar', { :class => 'fancy' }, { 'onclick' => "alert('Hello World');" } ])
+ { :class => "fancy", "onclick" => "alert('Hello World');" },
+ option_html_attributes([ "foo", "bar", { class: "fancy" }, { "onclick" => "alert('Hello World');" } ])
)
end
def test_option_html_attributes_with_multiple_hashes_does_not_modify_them
- options1 = { class: 'fancy' }
+ options1 = { class: "fancy" }
options2 = { onclick: "alert('Hello World');" }
- option_html_attributes([ 'foo', 'bar', options1, options2 ])
+ option_html_attributes([ "foo", "bar", options1, options2 ])
- assert_equal({ class: 'fancy' }, options1)
+ assert_equal({ class: "fancy" }, options1)
assert_equal({ onclick: "alert('Hello World');" }, options2)
end
def test_grouped_collection_select
@post = Post.new
- @post.origin = 'dk'
+ @post.origin = "dk"
assert_dom_equal(
%Q{<select id="post_origin" name="post[origin]"><optgroup label="&lt;Africa&gt;"><option value="&lt;sa&gt;">&lt;South Africa&gt;</option>\n<option value="so">Somalia</option></optgroup><optgroup label="Europe"><option value="dk" selected="selected">Denmark</option>\n<option value="ie">Ireland</option></optgroup></select>},
@@ -1323,7 +1433,7 @@ class FormOptionsHelperTest < ActionView::TestCase
assert_dom_equal(
%Q{<select id="post_origin" name="post[origin]"><optgroup label="&lt;Africa&gt;"><option value="&lt;sa&gt;">&lt;South Africa&gt;</option>\n<option value="so">Somalia</option></optgroup><optgroup label="Europe"><option value="dk" selected="selected">Denmark</option>\n<option value="ie">Ireland</option></optgroup></select>},
- grouped_collection_select("post", "origin", dummy_continents, :countries, :continent_name, :country_id, :country_name, :selected => 'dk')
+ grouped_collection_select("post", "origin", dummy_continents, :countries, :continent_name, :country_id, :country_name, selected: "dk")
)
end
@@ -1332,13 +1442,13 @@ class FormOptionsHelperTest < ActionView::TestCase
assert_dom_equal(
%Q{<select id="post_origin" name="post[origin]"><optgroup label="&lt;Africa&gt;"><option value="&lt;sa&gt;">&lt;South Africa&gt;</option>\n<option value="so">Somalia</option></optgroup><optgroup label="Europe"><option disabled="disabled" value="dk">Denmark</option>\n<option value="ie">Ireland</option></optgroup></select>},
- grouped_collection_select("post", "origin", dummy_continents, :countries, :continent_name, :country_id, :country_name, :disabled => 'dk')
+ grouped_collection_select("post", "origin", dummy_continents, :countries, :continent_name, :country_id, :country_name, disabled: "dk")
)
end
def test_grouped_collection_select_under_fields_for
@post = Post.new
- @post.origin = 'dk'
+ @post.origin = "dk"
output_buffer = fields_for :post, @post do |f|
concat f.grouped_collection_select("origin", dummy_continents, :countries, :continent_name, :country_id, :country_name)
@@ -1352,14 +1462,14 @@ class FormOptionsHelperTest < ActionView::TestCase
private
- def dummy_posts
- [ Post.new("<Abe> went home", "<Abe>", "To a little house", "shh!"),
- Post.new("Babe went home", "Babe", "To a little house", "shh!"),
- Post.new("Cabe went home", "Cabe", "To a little house", "shh!") ]
- end
+ def dummy_posts
+ [ Post.new("<Abe> went home", "<Abe>", "To a little house", "shh!"),
+ Post.new("Babe went home", "Babe", "To a little house", "shh!"),
+ Post.new("Cabe went home", "Cabe", "To a little house", "shh!") ]
+ end
- def dummy_continents
- [ Continent.new("<Africa>", [Country.new("<sa>", "<South Africa>"), Country.new("so", "Somalia")]),
- Continent.new("Europe", [Country.new("dk", "Denmark"), Country.new("ie", "Ireland")]) ]
- end
+ def dummy_continents
+ [ Continent.new("<Africa>", [Country.new("<sa>", "<South Africa>"), Country.new("so", "Somalia")]),
+ Continent.new("Europe", [Country.new("dk", "Denmark"), Country.new("ie", "Ireland")]) ]
+ end
end
diff --git a/actionview/test/template/form_tag_helper_test.rb b/actionview/test/template/form_tag_helper_test.rb
index f602c82c42..9ece9f3ad1 100644
--- a/actionview/test/template/form_tag_helper_test.rb
+++ b/actionview/test/template/form_tag_helper_test.rb
@@ -1,10 +1,22 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class FormTagHelperTest < ActionView::TestCase
include RenderERBUtils
tests ActionView::Helpers::FormTagHelper
+ class WithActiveStorageRoutesControllers < ActionController::Base
+ test_routes do
+ post "/rails/active_storage/direct_uploads" => "active_storage/direct_uploads#create", as: :rails_direct_uploads
+ end
+
+ def url_options
+ { host: "testtwo.host" }
+ end
+ end
+
def setup
super
@controller = BasicController.new
@@ -14,7 +26,7 @@ class FormTagHelperTest < ActionView::TestCase
method = options[:method]
enforce_utf8 = options.fetch(:enforce_utf8, true)
- ''.tap do |txt|
+ (+"").tap do |txt|
if enforce_utf8
txt << %{<input name="utf8" type="hidden" value="&#x2713;" />}
end
@@ -30,7 +42,7 @@ class FormTagHelperTest < ActionView::TestCase
method = method.to_s == "get" ? "get" : "post"
- txt = %{<form accept-charset="UTF-8" action="#{action}"}
+ txt = +%{<form accept-charset="UTF-8" action="#{action}"}
txt << %{ enctype="multipart/form-data"} if enctype
txt << %{ data-remote="true"} if remote
txt << %{ class="#{html_class}"} if html_class
@@ -64,9 +76,21 @@ class FormTagHelperTest < ActionView::TestCase
assert_dom_equal expected, actual
end
+ def test_check_box_tag_disabled
+ actual = check_box_tag "admin", "1", false, disabled: true
+ expected = %(<input id="admin" disabled="disabled" name="admin" type="checkbox" value="1" />)
+ assert_dom_equal expected, actual
+ end
+
+ def test_check_box_tag_default_checked
+ actual = check_box_tag "admin", "1", true
+ expected = %(<input id="admin" checked="checked" name="admin" type="checkbox" value="1" />)
+ assert_dom_equal expected, actual
+ end
+
def test_check_box_tag_id_sanitized
label_elem = root_elem(check_box_tag("project[2][admin]"))
- assert_match VALID_HTML_ID, label_elem['id']
+ assert_match VALID_HTML_ID, label_elem["id"]
end
def test_form_tag
@@ -76,56 +100,74 @@ class FormTagHelperTest < ActionView::TestCase
end
def test_form_tag_multipart
- actual = form_tag({}, { 'multipart' => true })
- expected = whole_form("http://www.example.com", :enctype => true)
+ actual = form_tag({}, { "multipart" => true })
+ expected = whole_form("http://www.example.com", enctype: true)
assert_dom_equal expected, actual
end
def test_form_tag_with_method_patch
- actual = form_tag({}, { :method => :patch })
- expected = whole_form("http://www.example.com", :method => :patch)
+ actual = form_tag({}, { method: :patch })
+ expected = whole_form("http://www.example.com", method: :patch)
assert_dom_equal expected, actual
end
def test_form_tag_with_method_put
- actual = form_tag({}, { :method => :put })
- expected = whole_form("http://www.example.com", :method => :put)
+ actual = form_tag({}, { method: :put })
+ expected = whole_form("http://www.example.com", method: :put)
assert_dom_equal expected, actual
end
def test_form_tag_with_method_delete
- actual = form_tag({}, { :method => :delete })
+ actual = form_tag({}, { method: :delete })
- expected = whole_form("http://www.example.com", :method => :delete)
+ expected = whole_form("http://www.example.com", method: :delete)
assert_dom_equal expected, actual
end
def test_form_tag_with_remote
- actual = form_tag({}, :remote => true)
+ actual = form_tag({}, { remote: true })
- expected = whole_form("http://www.example.com", :remote => true)
+ expected = whole_form("http://www.example.com", remote: true)
assert_dom_equal expected, actual
end
def test_form_tag_with_remote_false
- actual = form_tag({}, :remote => false)
+ actual = form_tag({}, { remote: false })
expected = whole_form
assert_dom_equal expected, actual
end
def test_form_tag_enforce_utf8_true
- actual = form_tag({}, { :enforce_utf8 => true })
- expected = whole_form("http://www.example.com", :enforce_utf8 => true)
+ actual = form_tag({}, { enforce_utf8: true })
+ expected = whole_form("http://www.example.com", enforce_utf8: true)
assert_dom_equal expected, actual
- assert actual.html_safe?
+ assert_predicate actual, :html_safe?
end
def test_form_tag_enforce_utf8_false
- actual = form_tag({}, { :enforce_utf8 => false })
- expected = whole_form("http://www.example.com", :enforce_utf8 => false)
+ actual = form_tag({}, { enforce_utf8: false })
+ expected = whole_form("http://www.example.com", enforce_utf8: false)
assert_dom_equal expected, actual
- assert actual.html_safe?
+ assert_predicate actual, :html_safe?
+ end
+
+ def test_form_tag_default_enforce_utf8_false
+ with_default_enforce_utf8 false do
+ actual = form_tag({})
+ expected = whole_form("http://www.example.com", enforce_utf8: false)
+ assert_dom_equal expected, actual
+ assert_predicate actual, :html_safe?
+ end
+ end
+
+ def test_form_tag_default_enforce_utf8_true
+ with_default_enforce_utf8 true do
+ actual = form_tag({})
+ expected = whole_form("http://www.example.com", enforce_utf8: true)
+ assert_dom_equal expected, actual
+ assert_predicate actual, :html_safe?
+ end
end
def test_form_tag_with_block_in_erb
@@ -138,7 +180,7 @@ class FormTagHelperTest < ActionView::TestCase
def test_form_tag_with_block_and_method_in_erb
output_buffer = render_erb("<%= form_tag('http://www.example.com', :method => :put) do %>Hello world!<% end %>")
- expected = whole_form("http://www.example.com", :method => "put") do
+ expected = whole_form("http://www.example.com", method: "put") do
"Hello world!"
end
@@ -153,7 +195,7 @@ class FormTagHelperTest < ActionView::TestCase
def test_hidden_field_tag_id_sanitized
input_elem = root_elem(hidden_field_tag("item[][title]"))
- assert_match VALID_HTML_ID, input_elem['id']
+ assert_match VALID_HTML_ID, input_elem["id"]
end
def test_file_field_tag
@@ -161,7 +203,34 @@ class FormTagHelperTest < ActionView::TestCase
end
def test_file_field_tag_with_options
- assert_dom_equal "<input name=\"picsplz\" type=\"file\" id=\"picsplz\" class=\"pix\"/>", file_field_tag("picsplz", :class => "pix")
+ assert_dom_equal "<input name=\"picsplz\" type=\"file\" id=\"picsplz\" class=\"pix\"/>", file_field_tag("picsplz", class: "pix")
+ end
+
+ def test_file_field_tag_with_direct_upload_when_rails_direct_uploads_url_is_not_defined
+ assert_dom_equal(
+ "<input name=\"picsplz\" type=\"file\" id=\"picsplz\" class=\"pix\"/>",
+ file_field_tag("picsplz", class: "pix", direct_upload: true)
+ )
+ end
+
+ def test_file_field_tag_with_direct_upload_when_rails_direct_uploads_url_is_defined
+ @controller = WithActiveStorageRoutesControllers.new
+
+ assert_dom_equal(
+ "<input name=\"picsplz\" type=\"file\" id=\"picsplz\" class=\"pix\" data-direct-upload-url=\"http://testtwo.host/rails/active_storage/direct_uploads\"/>",
+ file_field_tag("picsplz", class: "pix", direct_upload: true)
+ )
+ end
+
+ def test_file_field_tag_with_direct_upload_dont_mutate_arguments
+ original_options = { class: "pix", direct_upload: true }
+
+ assert_dom_equal(
+ "<input name=\"picsplz\" type=\"file\" id=\"picsplz\" class=\"pix\"/>",
+ file_field_tag("picsplz", original_options)
+ )
+
+ assert_equal({ class: "pix", direct_upload: true }, original_options)
end
def test_password_field_tag
@@ -171,7 +240,7 @@ class FormTagHelperTest < ActionView::TestCase
end
def test_multiple_field_tags_with_same_options
- options = {class: 'important'}
+ options = { class: "important" }
assert_dom_equal %(<input name="title" type="file" id="title" class="important"/>), file_field_tag("title", options)
assert_dom_equal %(<input type="password" name="title" id="title" value="Hello!" class="important" />), password_field_tag("title", "Hello!", options)
assert_dom_equal %(<input type="text" name="title" id="title" value="Hello!" class="important" />), text_field_tag("title", "Hello!", options)
@@ -198,78 +267,78 @@ class FormTagHelperTest < ActionView::TestCase
expected = %(<input id="person_gender_m" name="person[gender]" type="radio" value="m" />)
assert_dom_equal expected, actual
- actual = radio_button_tag('ctrlname', 'apache2.2')
+ actual = radio_button_tag("ctrlname", "apache2.2")
expected = %(<input id="ctrlname_apache2.2" name="ctrlname" type="radio" value="apache2.2" />)
assert_dom_equal expected, actual
end
def test_select_tag
- actual = select_tag "people", "<option>david</option>".html_safe
+ actual = select_tag "people", raw("<option>david</option>")
expected = %(<select id="people" name="people"><option>david</option></select>)
assert_dom_equal expected, actual
end
def test_select_tag_with_multiple
- actual = select_tag "colors", "<option>Red</option><option>Blue</option><option>Green</option>".html_safe, multiple: true
+ actual = select_tag "colors", raw("<option>Red</option><option>Blue</option><option>Green</option>"), multiple: true
expected = %(<select id="colors" multiple="multiple" name="colors[]"><option>Red</option><option>Blue</option><option>Green</option></select>)
assert_dom_equal expected, actual
end
def test_select_tag_disabled
- actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, disabled: true
+ actual = select_tag "places", raw("<option>Home</option><option>Work</option><option>Pub</option>"), disabled: true
expected = %(<select id="places" disabled="disabled" name="places"><option>Home</option><option>Work</option><option>Pub</option></select>)
assert_dom_equal expected, actual
end
def test_select_tag_id_sanitized
input_elem = root_elem(select_tag("project[1]people", "<option>david</option>"))
- assert_match VALID_HTML_ID, input_elem['id']
+ assert_match VALID_HTML_ID, input_elem["id"]
end
def test_select_tag_with_include_blank
- actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, :include_blank => true
- expected = %(<select id="places" name="places"><option value=""></option><option>Home</option><option>Work</option><option>Pub</option></select>)
+ actual = select_tag "places", raw("<option>Home</option><option>Work</option><option>Pub</option>"), include_blank: true
+ expected = %(<select id="places" name="places"><option value="" label=" "></option><option>Home</option><option>Work</option><option>Pub</option></select>)
assert_dom_equal expected, actual
end
def test_select_tag_with_include_blank_false
- actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, include_blank: false
+ actual = select_tag "places", raw("<option>Home</option><option>Work</option><option>Pub</option>"), include_blank: false
expected = %(<select id="places" name="places"><option>Home</option><option>Work</option><option>Pub</option></select>)
assert_dom_equal expected, actual
end
def test_select_tag_with_include_blank_string
- actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, include_blank: 'Choose'
+ actual = select_tag "places", raw("<option>Home</option><option>Work</option><option>Pub</option>"), include_blank: "Choose"
expected = %(<select id="places" name="places"><option value="">Choose</option><option>Home</option><option>Work</option><option>Pub</option></select>)
assert_dom_equal expected, actual
end
def test_select_tag_with_prompt
- actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, :prompt => "string"
+ actual = select_tag "places", raw("<option>Home</option><option>Work</option><option>Pub</option>"), prompt: "string"
expected = %(<select id="places" name="places"><option value="">string</option><option>Home</option><option>Work</option><option>Pub</option></select>)
assert_dom_equal expected, actual
end
def test_select_tag_escapes_prompt
- actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, :prompt => "<script>alert(1337)</script>"
+ actual = select_tag "places", raw("<option>Home</option><option>Work</option><option>Pub</option>"), prompt: "<script>alert(1337)</script>"
expected = %(<select id="places" name="places"><option value="">&lt;script&gt;alert(1337)&lt;/script&gt;</option><option>Home</option><option>Work</option><option>Pub</option></select>)
assert_dom_equal expected, actual
end
def test_select_tag_with_prompt_and_include_blank
- actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, :prompt => "string", :include_blank => true
- expected = %(<select name="places" id="places"><option value="">string</option><option value=""></option><option>Home</option><option>Work</option><option>Pub</option></select>)
+ actual = select_tag "places", raw("<option>Home</option><option>Work</option><option>Pub</option>"), prompt: "string", include_blank: true
+ expected = %(<select name="places" id="places"><option value="">string</option><option value="" label=" "></option><option>Home</option><option>Work</option><option>Pub</option></select>)
assert_dom_equal expected, actual
end
def test_select_tag_with_nil_option_tags_and_include_blank
- actual = select_tag "places", nil, :include_blank => true
- expected = %(<select id="places" name="places"><option value=""></option></select>)
+ actual = select_tag "places", nil, include_blank: true
+ expected = %(<select id="places" name="places"><option value="" label=" "></option></select>)
assert_dom_equal expected, actual
end
def test_select_tag_with_nil_option_tags_and_prompt
- actual = select_tag "places", nil, :prompt => "string"
+ actual = select_tag "places", nil, prompt: "string"
expected = %(<select id="places" name="places"><option value="">string</option></select>)
assert_dom_equal expected, actual
end
@@ -281,36 +350,36 @@ class FormTagHelperTest < ActionView::TestCase
end
def test_text_area_tag_size_symbol
- actual = text_area_tag "body", "hello world", :size => "20x40"
+ actual = text_area_tag "body", "hello world", size: "20x40"
expected = %(<textarea cols="20" id="body" name="body" rows="40">\nhello world</textarea>)
assert_dom_equal expected, actual
end
def test_text_area_tag_should_disregard_size_if_its_given_as_an_integer
- actual = text_area_tag "body", "hello world", :size => 20
+ actual = text_area_tag "body", "hello world", size: 20
expected = %(<textarea id="body" name="body">\nhello world</textarea>)
assert_dom_equal expected, actual
end
def test_text_area_tag_id_sanitized
input_elem = root_elem(text_area_tag("item[][description]"))
- assert_match VALID_HTML_ID, input_elem['id']
+ assert_match VALID_HTML_ID, input_elem["id"]
end
def test_text_area_tag_escape_content
- actual = text_area_tag "body", "<b>hello world</b>", :size => "20x40"
+ actual = text_area_tag "body", "<b>hello world</b>", size: "20x40"
expected = %(<textarea cols="20" id="body" name="body" rows="40">\n&lt;b&gt;hello world&lt;/b&gt;</textarea>)
assert_dom_equal expected, actual
end
def test_text_area_tag_unescaped_content
- actual = text_area_tag "body", "<b>hello world</b>", :size => "20x40", :escape => false
+ actual = text_area_tag "body", "<b>hello world</b>", size: "20x40", escape: false
expected = %(<textarea cols="20" id="body" name="body" rows="40">\n<b>hello world</b></textarea>)
assert_dom_equal expected, actual
end
def test_text_area_tag_unescaped_nil_content
- actual = text_area_tag "body", nil, :escape => false
+ actual = text_area_tag "body", nil, escape: false
expected = %(<textarea id="body" name="body">\n</textarea>)
assert_dom_equal expected, actual
end
@@ -328,11 +397,17 @@ class FormTagHelperTest < ActionView::TestCase
end
def test_text_field_tag_size_symbol
- actual = text_field_tag "title", "Hello!", :size => 75
+ actual = text_field_tag "title", "Hello!", size: 75
expected = %(<input id="title" name="title" size="75" type="text" value="Hello!" />)
assert_dom_equal expected, actual
end
+ def test_text_field_tag_with_ac_parameters
+ actual = text_field_tag "title", ActionController::Parameters.new(key: "value")
+ expected = %(<input id="title" name="title" type="text" value="{&quot;key&quot;=&gt;&quot;value&quot;}" />)
+ assert_dom_equal expected, actual
+ end
+
def test_text_field_tag_size_string
actual = text_field_tag "title", "Hello!", "size" => "75"
expected = %(<input id="title" name="title" size="75" type="text" value="Hello!" />)
@@ -340,7 +415,7 @@ class FormTagHelperTest < ActionView::TestCase
end
def test_text_field_tag_maxlength_symbol
- actual = text_field_tag "title", "Hello!", :maxlength => 75
+ actual = text_field_tag "title", "Hello!", maxlength: 75
expected = %(<input id="title" name="title" maxlength="75" type="text" value="Hello!" />)
assert_dom_equal expected, actual
end
@@ -351,21 +426,27 @@ class FormTagHelperTest < ActionView::TestCase
assert_dom_equal expected, actual
end
- def test_text_field_disabled
+ def test_text_field_tag_disabled
actual = text_field_tag "title", "Hello!", disabled: true
expected = %(<input id="title" name="title" disabled="disabled" type="text" value="Hello!" />)
assert_dom_equal expected, actual
end
+ def test_text_field_tag_with_placeholder_option
+ actual = text_field_tag "title", "Hello!", placeholder: "Enter search term..."
+ expected = %(<input id="title" name="title" placeholder="Enter search term..." type="text" value="Hello!" />)
+ assert_dom_equal expected, actual
+ end
+
def test_text_field_tag_with_multiple_options
- actual = text_field_tag "title", "Hello!", :size => 70, :maxlength => 80
+ actual = text_field_tag "title", "Hello!", size: 70, maxlength: 80
expected = %(<input id="title" name="title" size="70" maxlength="80" type="text" value="Hello!" />)
assert_dom_equal expected, actual
end
def test_text_field_tag_id_sanitized
input_elem = root_elem(text_field_tag("item[][title]"))
- assert_match VALID_HTML_ID, input_elem['id']
+ assert_match VALID_HTML_ID, input_elem["id"]
end
def test_label_tag_without_text
@@ -394,11 +475,11 @@ class FormTagHelperTest < ActionView::TestCase
def test_label_tag_id_sanitized
label_elem = root_elem(label_tag("item[title]"))
- assert_match VALID_HTML_ID, label_elem['for']
+ assert_match VALID_HTML_ID, label_elem["for"]
end
def test_label_tag_with_block
- assert_dom_equal('<label>Blocked</label>', label_tag { "Blocked" })
+ assert_dom_equal("<label>Blocked</label>", label_tag { "Blocked" })
end
def test_label_tag_with_block_and_argument
@@ -407,21 +488,21 @@ class FormTagHelperTest < ActionView::TestCase
end
def test_label_tag_with_block_and_argument_and_options
- output = label_tag("clock", :id => "label_clock") { "Grandfather" }
+ output = label_tag("clock", id: "label_clock") { "Grandfather" }
assert_dom_equal('<label for="clock" id="label_clock">Grandfather</label>', output)
end
def test_boolean_options
- assert_dom_equal %(<input checked="checked" disabled="disabled" id="admin" name="admin" readonly="readonly" type="checkbox" value="1" />), check_box_tag("admin", 1, true, 'disabled' => true, :readonly => "yes")
- assert_dom_equal %(<input checked="checked" id="admin" name="admin" type="checkbox" value="1" />), check_box_tag("admin", 1, true, :disabled => false, :readonly => nil)
- assert_dom_equal %(<input type="checkbox" />), tag(:input, :type => "checkbox", :checked => false)
- assert_dom_equal %(<select id="people" multiple="multiple" name="people[]"><option>david</option></select>), select_tag("people", "<option>david</option>".html_safe, :multiple => true)
- assert_dom_equal %(<select id="people_" multiple="multiple" name="people[]"><option>david</option></select>), select_tag("people[]", "<option>david</option>".html_safe, :multiple => true)
- assert_dom_equal %(<select id="people" name="people"><option>david</option></select>), select_tag("people", "<option>david</option>".html_safe, :multiple => nil)
+ assert_dom_equal %(<input checked="checked" disabled="disabled" id="admin" name="admin" readonly="readonly" type="checkbox" value="1" />), check_box_tag("admin", 1, true, "disabled" => true, :readonly => "yes")
+ assert_dom_equal %(<input checked="checked" id="admin" name="admin" type="checkbox" value="1" />), check_box_tag("admin", 1, true, disabled: false, readonly: nil)
+ assert_dom_equal %(<input type="checkbox" />), tag(:input, type: "checkbox", checked: false)
+ assert_dom_equal %(<select id="people" multiple="multiple" name="people[]"><option>david</option></select>), select_tag("people", raw("<option>david</option>"), multiple: true)
+ assert_dom_equal %(<select id="people_" multiple="multiple" name="people[]"><option>david</option></select>), select_tag("people[]", raw("<option>david</option>"), multiple: true)
+ assert_dom_equal %(<select id="people" name="people"><option>david</option></select>), select_tag("people", raw("<option>david</option>"), multiple: nil)
end
def test_stringify_symbol_keys
- actual = text_field_tag "title", "Hello!", :id => "admin"
+ actual = text_field_tag "title", "Hello!", id: "admin"
expected = %(<input id="admin" name="title" type="text" value="Hello!" />)
assert_dom_equal expected, actual
end
@@ -429,21 +510,80 @@ class FormTagHelperTest < ActionView::TestCase
def test_submit_tag
assert_dom_equal(
%(<input name='commit' data-disable-with="Saving..." onclick="alert(&#39;hello!&#39;)" type="submit" value="Save" />),
- submit_tag("Save", :onclick => "alert('hello!')", :data => { :disable_with => "Saving..." })
+ submit_tag("Save", onclick: "alert('hello!')", data: { disable_with: "Saving..." })
+ )
+ end
+
+ def test_empty_submit_tag
+ assert_dom_equal(
+ %(<input data-disable-with="Save" name='commit' type="submit" value="Save" />),
+ submit_tag("Save")
+ )
+ end
+
+ def test_empty_submit_tag_with_opt_out
+ ActionView::Base.automatically_disable_submit_tag = false
+ assert_dom_equal(
+ %(<input name='commit' type="submit" value="Save" />),
+ submit_tag("Save")
+ )
+ ensure
+ ActionView::Base.automatically_disable_submit_tag = true
+ end
+
+ def test_submit_tag_having_data_disable_with_string
+ assert_dom_equal(
+ %(<input data-disable-with="Processing..." data-confirm="Are you sure?" name='commit' type="submit" value="Save" />),
+ submit_tag("Save", "data-disable-with" => "Processing...", "data-confirm" => "Are you sure?")
+ )
+ end
+
+ def test_submit_tag_having_data_disable_with_boolean
+ assert_dom_equal(
+ %(<input data-confirm="Are you sure?" name='commit' type="submit" value="Save" />),
+ submit_tag("Save", "data-disable-with" => false, "data-confirm" => "Are you sure?")
+ )
+ end
+
+ def test_submit_tag_having_data_hash_disable_with_boolean
+ assert_dom_equal(
+ %(<input data-confirm="Are you sure?" name='commit' type="submit" value="Save" />),
+ submit_tag("Save", data: { confirm: "Are you sure?", disable_with: false })
)
end
def test_submit_tag_with_no_onclick_options
assert_dom_equal(
%(<input name='commit' data-disable-with="Saving..." type="submit" value="Save" />),
- submit_tag("Save", :data => { :disable_with => "Saving..." })
+ submit_tag("Save", data: { disable_with: "Saving..." })
)
end
def test_submit_tag_with_confirmation
assert_dom_equal(
- %(<input name='commit' type='submit' value='Save' data-confirm="Are you sure?" />),
- submit_tag("Save", :data => { :confirm => "Are you sure?" })
+ %(<input name='commit' type='submit' value='Save' data-confirm="Are you sure?" data-disable-with="Save" />),
+ submit_tag("Save", data: { confirm: "Are you sure?" })
+ )
+ end
+
+ def test_submit_tag_doesnt_have_data_disable_with_twice
+ assert_equal(
+ %(<input type="submit" name="commit" value="Save" data-confirm="Are you sure?" data-disable-with="Processing..." />),
+ submit_tag("Save", "data-disable-with" => "Processing...", "data-confirm" => "Are you sure?")
+ )
+ 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" />),
+ submit_tag(:Save)
)
end
@@ -457,56 +597,56 @@ class FormTagHelperTest < ActionView::TestCase
def test_button_tag_with_submit_type
assert_dom_equal(
%(<button name="button" type="submit">Save</button>),
- button_tag("Save", :type => "submit")
+ button_tag("Save", type: "submit")
)
end
def test_button_tag_with_button_type
assert_dom_equal(
%(<button name="button" type="button">Button</button>),
- button_tag("Button", :type => "button")
+ button_tag("Button", type: "button")
)
end
def test_button_tag_with_reset_type
assert_dom_equal(
%(<button name="button" type="reset">Reset</button>),
- button_tag("Reset", :type => "reset")
+ button_tag("Reset", type: "reset")
)
end
def test_button_tag_with_disabled_option
assert_dom_equal(
%(<button name="button" type="reset" disabled="disabled">Reset</button>),
- button_tag("Reset", :type => "reset", :disabled => true)
+ button_tag("Reset", type: "reset", disabled: true)
)
end
def test_button_tag_escape_content
assert_dom_equal(
%(<button name="button" type="reset" disabled="disabled">&lt;b&gt;Reset&lt;/b&gt;</button>),
- button_tag("<b>Reset</b>", :type => "reset", :disabled => true)
+ button_tag("<b>Reset</b>", type: "reset", disabled: true)
)
end
def test_button_tag_with_block
- assert_dom_equal('<button name="button" type="submit">Content</button>', button_tag { 'Content' })
+ assert_dom_equal('<button name="button" type="submit">Content</button>', button_tag { "Content" })
end
def test_button_tag_with_block_and_options
- output = button_tag(:name => 'temptation', :type => 'button') { content_tag(:strong, 'Do not press me') }
+ output = button_tag(name: "temptation", type: "button") { content_tag(:strong, "Do not press me") }
assert_dom_equal('<button name="temptation" type="button"><strong>Do not press me</strong></button>', output)
end
def test_button_tag_defaults_with_block_and_options
- output = button_tag(:name => 'temptation', :value => 'within') { content_tag(:strong, 'Do not press me') }
+ output = button_tag(name: "temptation", value: "within") { content_tag(:strong, "Do not press me") }
assert_dom_equal('<button name="temptation" value="within" type="submit" ><strong>Do not press me</strong></button>', output)
end
def test_button_tag_with_confirmation
assert_dom_equal(
%(<button name="button" type="submit" data-confirm="Are you sure?">Save</button>),
- button_tag("Save", :type => "submit", :data => { :confirm => "Are you sure?" })
+ button_tag("Save", type: "submit", data: { confirm: "Are you sure?" })
)
end
@@ -519,8 +659,8 @@ class FormTagHelperTest < ActionView::TestCase
def test_image_submit_tag_with_confirmation
assert_dom_equal(
- %(<input alt="Save" type="image" src="/images/save.gif" data-confirm="Are you sure?" />),
- image_submit_tag("save.gif", :data => { :confirm => "Are you sure?" })
+ %(<input type="image" src="/images/save.gif" data-confirm="Are you sure?" />),
+ image_submit_tag("save.gif", data: { confirm: "Are you sure?" })
)
end
@@ -550,7 +690,7 @@ class FormTagHelperTest < ActionView::TestCase
end
def test_datetime_field_tag
- expected = %{<input id="appointment" name="appointment" type="datetime" />}
+ expected = %{<input id="appointment" name="appointment" type="datetime-local" />}
assert_dom_equal(expected, datetime_field_tag("appointment"))
end
@@ -581,12 +721,12 @@ class FormTagHelperTest < ActionView::TestCase
def test_number_field_tag
expected = %{<input name="quantity" max="9" id="quantity" type="number" min="1" />}
- assert_dom_equal(expected, number_field_tag("quantity", nil, :in => 1...10))
+ assert_dom_equal(expected, number_field_tag("quantity", nil, in: 1...10))
end
def test_range_input_tag
expected = %{<input name="volume" step="0.1" max="11" id="volume" type="range" min="0" />}
- assert_dom_equal(expected, range_field_tag("volume", nil, :in => 0..11, :step => 0.1))
+ assert_dom_equal(expected, range_field_tag("volume", nil, in: 0..11, step: 0.1))
end
def test_field_set_tag_in_erb
@@ -622,33 +762,33 @@ class FormTagHelperTest < ActionView::TestCase
end
def test_text_area_tag_options_symbolize_keys_side_effects
- options = { :option => "random_option" }
+ 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" }
+ 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" }
+ 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" }
+ 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" }
+ 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?
@@ -657,7 +797,16 @@ class FormTagHelperTest < ActionView::TestCase
private
- def root_elem(rendered_content)
- Nokogiri::HTML::DocumentFragment.parse(rendered_content).children.first # extract from nodeset
- end
+ def root_elem(rendered_content)
+ Nokogiri::HTML::DocumentFragment.parse(rendered_content).children.first # extract from nodeset
+ end
+
+ def with_default_enforce_utf8(value)
+ old_value = ActionView::Helpers::FormTagHelper.default_enforce_utf8
+ ActionView::Helpers::FormTagHelper.default_enforce_utf8 = value
+
+ yield
+ ensure
+ ActionView::Helpers::FormTagHelper.default_enforce_utf8 = old_value
+ end
end
diff --git a/actionview/test/template/html_test.rb b/actionview/test/template/html_test.rb
index 549c12c88c..5cdff74d60 100644
--- a/actionview/test/template/html_test.rb
+++ b/actionview/test/template/html_test.rb
@@ -1,17 +1,19 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class HTMLTest < ActiveSupport::TestCase
- test 'formats returns symbol for recognized MIME type' do
- assert_equal [:html], ActionView::Template::HTML.new('', :html).formats
+ test "formats returns symbol for recognized MIME type" do
+ assert_equal [:html], ActionView::Template::HTML.new("", :html).formats
end
- test 'formats returns string for recognized MIME type when MIME does not have symbol' do
+ 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::HTML.new('', foo).formats
+ assert_equal ["foo"], ActionView::Template::HTML.new("", foo).formats
end
- test 'formats returns string for unknown MIME type' do
- assert_equal ['foo'], ActionView::Template::HTML.new('', 'foo').formats
+ test "formats returns string for unknown MIME type" do
+ assert_equal ["foo"], ActionView::Template::HTML.new("", "foo").formats
end
end
diff --git a/actionview/test/template/javascript_helper_test.rb b/actionview/test/template/javascript_helper_test.rb
index 9f1535ef53..4c28aeaee1 100644
--- a/actionview/test/template/javascript_helper_test.rb
+++ b/actionview/test/template/javascript_helper_test.rb
@@ -1,14 +1,20 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class JavaScriptHelperTest < ActionView::TestCase
tests ActionView::Helpers::JavaScriptHelper
attr_accessor :output_buffer
+ attr_reader :request
setup do
@old_escape_html_entities_in_json = ActiveSupport.escape_html_entities_in_json
- ActiveSupport.escape_html_entities_in_json = true
+ ActiveSupport.escape_html_entities_in_json = true
@template = self
+ @request = Class.new do
+ def send_early_hints(links) end
+ end.new
end
def teardown
@@ -16,12 +22,16 @@ class JavaScriptHelperTest < ActionView::TestCase
end
def test_escape_javascript
- assert_equal '', escape_javascript(nil)
+ assert_equal "", escape_javascript(nil)
+ assert_equal "123", escape_javascript(123)
+ assert_equal "en", escape_javascript(:en)
+ assert_equal "false", escape_javascript(false)
+ assert_equal "true", escape_javascript(true)
assert_equal %(This \\"thing\\" is really\\n netos\\'), escape_javascript(%(This "thing" is really\n netos'))
- assert_equal %(backslash\\\\test), escape_javascript( %(backslash\\test) )
+ assert_equal %(backslash\\\\test), escape_javascript(%(backslash\\test))
assert_equal %(dont <\\/close> tags), escape_javascript(%(dont </close> tags))
- assert_equal %(unicode &#x2028; newline), escape_javascript(%(unicode \342\200\250 newline).force_encoding(Encoding::UTF_8).encode!)
- assert_equal %(unicode &#x2029; newline), escape_javascript(%(unicode \342\200\251 newline).force_encoding(Encoding::UTF_8).encode!)
+ assert_equal %(unicode &#x2028; newline), escape_javascript((+%(unicode \342\200\250 newline)).force_encoding(Encoding::UTF_8).encode!)
+ assert_equal %(unicode &#x2029; newline), escape_javascript((+%(unicode \342\200\251 newline)).force_encoding(Encoding::UTF_8).encode!)
assert_equal %(dont <\\/close> tags), j(%(dont </close> tags))
end
@@ -36,24 +46,24 @@ class JavaScriptHelperTest < ActionView::TestCase
end
def test_javascript_tag
- self.output_buffer = 'foo'
+ self.output_buffer = "foo"
assert_dom_equal "<script>\n//<![CDATA[\nalert('hello')\n//]]>\n</script>",
javascript_tag("alert('hello')")
- assert_equal 'foo', output_buffer, 'javascript_tag without a block should not concat to output_buffer'
+ assert_equal "foo", output_buffer, "javascript_tag without a block should not concat to output_buffer"
end
# Setting the :extname option will control what extension (if any) is appended to the url for assets
def test_javascript_include_tag
- assert_dom_equal "<script src='/foo.js'></script>", javascript_include_tag('/foo')
- assert_dom_equal "<script src='/foo'></script>", javascript_include_tag('/foo', extname: false )
- assert_dom_equal "<script src='/foo.bar'></script>", javascript_include_tag('/foo', extname: '.bar' )
+ assert_dom_equal "<script src='/foo.js'></script>", javascript_include_tag("/foo")
+ assert_dom_equal "<script src='/foo'></script>", javascript_include_tag("/foo", extname: false)
+ assert_dom_equal "<script src='/foo.bar'></script>", javascript_include_tag("/foo", extname: ".bar")
end
def test_javascript_tag_with_options
assert_dom_equal "<script id=\"the_js_tag\">\n//<![CDATA[\nalert('hello')\n//]]>\n</script>",
- javascript_tag("alert('hello')", :id => "the_js_tag")
+ javascript_tag("alert('hello')", id: "the_js_tag")
end
def test_javascript_cdata_section
diff --git a/actionview/test/template/log_subscriber_test.rb b/actionview/test/template/log_subscriber_test.rb
index 7f4c84929f..9fcf80bb24 100644
--- a/actionview/test/template/log_subscriber_test.rb
+++ b/actionview/test/template/log_subscriber_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "abstract_unit"
require "active_support/log_subscriber/test_helper"
require "action_view/log_subscriber"
@@ -8,84 +10,215 @@ class AVLogSubscriberTest < ActiveSupport::TestCase
def setup
super
- view_paths = ActionController::Base.view_paths
+
+ view_paths = ActionController::Base.view_paths
lookup_context = ActionView::LookupContext.new(view_paths, {}, ["test"])
- renderer = ActionView::Renderer.new(lookup_context)
- @view = ActionView::Base.new(renderer, {})
- Rails.stubs(:root).returns(File.expand_path(FIXTURE_LOAD_PATH))
+ renderer = ActionView::Renderer.new(lookup_context)
+ @view = ActionView::Base.new(renderer, {})
+
ActionView::LogSubscriber.attach_to :action_view
+
+ unless Rails.respond_to?(:root)
+ @defined_root = true
+ def Rails.root; :defined_root; end # Minitest `stub` expects the method to be defined.
+ end
end
def teardown
super
+
ActiveSupport::LogSubscriber.log_subscribers.clear
+
+ # We need to undef `root`, RenderTestCases don't want this to be defined
+ Rails.instance_eval { undef :root } if defined?(@defined_root)
end
def set_logger(logger)
ActionView::Base.logger = logger
end
- def test_render_file_template
- @view.render(:file => "test/hello_world")
- wait
+ def set_cache_controller
+ controller = ActionController::Base.new
+ controller.perform_caching = true
+ controller.cache_store = ActiveSupport::Cache::MemoryStore.new
+ @view.controller = controller
+ end
- assert_equal 1, @logger.logged(:info).size
- assert_match(/Rendered test\/hello_world\.erb/, @logger.logged(:info).last)
+ def set_view_cache_dependencies
+ def @view.view_cache_dependencies; []; end
+ def @view.combined_fragment_cache_key(*); "ahoy `controller` dependency"; end
end
- def test_render_text_template
- @view.render(:text => "TEXT")
- wait
+ def test_render_file_template
+ Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do
+ @view.render(file: "test/hello_world")
+ wait
+
+ assert_equal 2, @logger.logged(:info).size
+ assert_match(/Rendering test\/hello_world\.erb/, @logger.logged(:info).first)
+ assert_match(/Rendered test\/hello_world\.erb/, @logger.logged(:info).last)
+ end
+ end
- assert_equal 1, @logger.logged(:info).size
- assert_match(/Rendered text template/, @logger.logged(:info).last)
+ def test_render_text_template
+ Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do
+ @view.render(plain: "TEXT")
+ wait
+
+ assert_equal 2, @logger.logged(:info).size
+ assert_match(/Rendering text template/, @logger.logged(:info).first)
+ assert_match(/Rendered text template/, @logger.logged(:info).last)
+ end
end
def test_render_inline_template
- @view.render(:inline => "<%= 'TEXT' %>")
- wait
+ Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do
+ @view.render(inline: "<%= 'TEXT' %>")
+ wait
+
+ assert_equal 2, @logger.logged(:info).size
+ assert_match(/Rendering inline template/, @logger.logged(:info).first)
+ assert_match(/Rendered inline template/, @logger.logged(:info).last)
+ end
+ end
+
+ def test_render_partial_with_implicit_path
+ Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do
+ @view.render(Customer.new("david"), greeting: "hi")
+ wait
- assert_equal 1, @logger.logged(:info).size
- assert_match(/Rendered inline template/, @logger.logged(:info).last)
+ assert_equal 1, @logger.logged(:info).size
+ assert_match(/Rendered customers\/_customer\.html\.erb/, @logger.logged(:info).last)
+ end
end
- def test_render_partial_template
- @view.render(:partial => "test/customer")
- wait
+ def test_render_partial_with_cache_missed
+ Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do
+ set_view_cache_dependencies
+ set_cache_controller
+
+ @view.render(partial: "test/cached_customer", locals: { cached_customer: Customer.new("david") })
+ wait
- assert_equal 1, @logger.logged(:info).size
- assert_match(/Rendered test\/_customer.erb/, @logger.logged(:info).last)
+ assert_equal 1, @logger.logged(:info).size
+ assert_match(/Rendered test\/_cached_customer\.erb (.*) \[cache miss\]/, @logger.logged(:info).last)
+ end
end
- def test_render_partial_with_implicit_path
- @view.render(Customer.new("david"), :greeting => "hi")
- wait
+ def test_render_partial_with_cache_hitted
+ Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do
+ set_view_cache_dependencies
+ set_cache_controller
+
+ # Second render should hit cache.
+ @view.render(partial: "test/cached_customer", locals: { cached_customer: Customer.new("david") })
+ @view.render(partial: "test/cached_customer", locals: { cached_customer: Customer.new("david") })
+ wait
+
+ assert_equal 2, @logger.logged(:info).size
+ assert_match(/Rendered test\/_cached_customer\.erb (.*) \[cache hit\]/, @logger.logged(:info).last)
+ end
+ end
- assert_equal 1, @logger.logged(:info).size
- assert_match(/Rendered customers\/_customer\.html\.erb/, @logger.logged(:info).last)
+ def test_render_uncached_outer_partial_with_inner_cached_partial_wont_mix_cache_hits_or_misses
+ Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do
+ set_view_cache_dependencies
+ set_cache_controller
+
+ @view.render(partial: "test/nested_cached_customer", locals: { cached_customer: Customer.new("Stan") })
+ wait
+ *, cached_inner, uncached_outer = @logger.logged(:info)
+ assert_match(/Rendered test\/_cached_customer\.erb (.*) \[cache miss\]/, cached_inner)
+ assert_match(/Rendered test\/_nested_cached_customer\.erb \(Duration: .*?ms \| Allocations: .*?\)$/, uncached_outer)
+
+ # Second render hits the cache for the _cached_customer partial. Outer template's log shouldn't be affected.
+ @view.render(partial: "test/nested_cached_customer", locals: { cached_customer: Customer.new("Stan") })
+ wait
+ *, cached_inner, uncached_outer = @logger.logged(:info)
+ assert_match(/Rendered test\/_cached_customer\.erb (.*) \[cache hit\]/, cached_inner)
+ assert_match(/Rendered test\/_nested_cached_customer\.erb \(Duration: .*?ms \| Allocations: .*?\)$/, uncached_outer)
+ end
+ end
+
+ def test_render_cached_outer_partial_with_cached_inner_partial
+ Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do
+ set_view_cache_dependencies
+ set_cache_controller
+
+ @view.render(partial: "test/cached_nested_cached_customer", locals: { cached_customer: Customer.new("Stan") })
+ wait
+ *, cached_inner, cached_outer = @logger.logged(:info)
+ assert_match(/Rendered test\/_cached_customer\.erb (.*) \[cache miss\]/, cached_inner)
+ assert_match(/Rendered test\/_cached_nested_cached_customer\.erb (.*) \[cache miss\]/, cached_outer)
+
+ # One render: inner partial skipped, because the outer has been cached.
+ assert_difference -> { @logger.logged(:info).size }, +1 do
+ @view.render(partial: "test/cached_nested_cached_customer", locals: { cached_customer: Customer.new("Stan") })
+ wait
+ end
+ assert_match(/Rendered test\/_cached_nested_cached_customer\.erb (.*) \[cache hit\]/, @logger.logged(:info).last)
+ end
+ end
+
+ def test_render_partial_with_cache_hitted_and_missed
+ Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do
+ set_view_cache_dependencies
+ set_cache_controller
+
+ @view.render(partial: "test/cached_customer", locals: { cached_customer: Customer.new("david") })
+ wait
+ assert_match(/Rendered test\/_cached_customer\.erb (.*) \[cache miss\]/, @logger.logged(:info).last)
+
+ @view.render(partial: "test/cached_customer", locals: { cached_customer: Customer.new("david") })
+ wait
+ assert_match(/Rendered test\/_cached_customer\.erb (.*) \[cache hit\]/, @logger.logged(:info).last)
+
+ @view.render(partial: "test/cached_customer", locals: { cached_customer: Customer.new("Stan") })
+ wait
+ assert_match(/Rendered test\/_cached_customer\.erb (.*) \[cache miss\]/, @logger.logged(:info).last)
+ end
end
def test_render_collection_template
- @view.render(:partial => "test/customer", :collection => [ Customer.new("david"), Customer.new("mary") ])
- wait
+ Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do
+ @view.render(partial: "test/customer", collection: [ Customer.new("david"), Customer.new("mary") ])
+ wait
- assert_equal 1, @logger.logged(:info).size
- assert_match(/Rendered test\/_customer.erb/, @logger.logged(:info).last)
+ assert_equal 1, @logger.logged(:info).size
+ assert_match(/Rendered collection of test\/_customer.erb \[2 times\]/, @logger.logged(:info).last)
+ end
end
def test_render_collection_with_implicit_path
- @view.render([ Customer.new("david"), Customer.new("mary") ], :greeting => "hi")
- wait
+ Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do
+ @view.render([ Customer.new("david"), Customer.new("mary") ], greeting: "hi")
+ wait
- assert_equal 1, @logger.logged(:info).size
- assert_match(/Rendered customers\/_customer\.html\.erb/, @logger.logged(:info).last)
+ assert_equal 1, @logger.logged(:info).size
+ assert_match(/Rendered collection of customers\/_customer\.html\.erb \[2 times\]/, @logger.logged(:info).last)
+ end
end
def test_render_collection_template_without_path
- @view.render([ GoodCustomer.new("david"), Customer.new("mary") ], :greeting => "hi")
- wait
+ Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do
+ @view.render([ GoodCustomer.new("david"), Customer.new("mary") ], greeting: "hi")
+ wait
+
+ assert_equal 1, @logger.logged(:info).size
+ assert_match(/Rendered collection of templates/, @logger.logged(:info).last)
+ end
+ end
+
+ def test_render_collection_with_cached_set
+ Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do
+ set_view_cache_dependencies
+
+ @view.render(partial: "customers/customer", collection: [ Customer.new("david"), Customer.new("mary") ], cached: true,
+ locals: { greeting: "hi" })
+ wait
- assert_equal 1, @logger.logged(:info).size
- assert_match(/Rendered collection/, @logger.logged(:info).last)
+ assert_equal 1, @logger.logged(:info).size
+ assert_match(/Rendered collection of customers\/_customer\.html\.erb \[0 \/ 2 cache hits\]/, @logger.logged(:info).last)
+ end
end
end
diff --git a/actionview/test/template/lookup_context_test.rb b/actionview/test/template/lookup_context_test.rb
index 4f7823045e..38469cbe3d 100644
--- a/actionview/test/template/lookup_context_test.rb
+++ b/actionview/test/template/lookup_context_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "abstract_unit"
require "abstract_controller/rendering"
@@ -27,13 +29,13 @@ class LookupContextTest < ActiveSupport::TestCase
end
test "normalizes details on initialization" do
- assert_equal Mime::SET, @lookup_context.formats
+ assert_equal Mime::SET.to_a, @lookup_context.formats
assert_equal :en, @lookup_context.locale
end
test "allows me to freeze and retrieve frozen formats" do
@lookup_context.formats.freeze
- assert @lookup_context.formats.frozen?
+ assert_predicate @lookup_context.formats, :frozen?
end
test "provides getters and setters for variants" do
@@ -48,7 +50,7 @@ class LookupContextTest < ActiveSupport::TestCase
test "handles */* formats" do
@lookup_context.formats = ["*/*"]
- assert_equal Mime::SET, @lookup_context.formats
+ assert_equal Mime::SET.to_a, @lookup_context.formats
end
test "handles explicitly defined */* formats fallback to :js" do
@@ -108,10 +110,11 @@ class LookupContextTest < ActiveSupport::TestCase
end
test "found templates respects given formats if one cannot be found from template or handler" do
- ActionView::Template::Handlers::Builder.expects(:default_format).returns(nil)
- @lookup_context.formats = [:text]
- template = @lookup_context.find("hello", %w(test))
- assert_equal [:text], template.formats
+ assert_called(ActionView::Template::Handlers::Builder, :default_format, returns: nil) do
+ @lookup_context.formats = [:text]
+ template = @lookup_context.find("hello", %w(test))
+ assert_equal [:text], template.formats
+ end
end
test "adds fallbacks to view paths when required" do
@@ -119,8 +122,8 @@ class LookupContextTest < ActiveSupport::TestCase
@lookup_context.with_fallbacks do
assert_equal 3, @lookup_context.view_paths.size
- assert @lookup_context.view_paths.include?(ActionView::FallbackFileSystemResolver.new(""))
- assert @lookup_context.view_paths.include?(ActionView::FallbackFileSystemResolver.new("/"))
+ assert_includes @lookup_context.view_paths, ActionView::FallbackFileSystemResolver.new("")
+ assert_includes @lookup_context.view_paths, ActionView::FallbackFileSystemResolver.new("/")
end
end
@@ -192,7 +195,7 @@ class LookupContextTest < ActiveSupport::TestCase
assert @lookup_context.cache
template = @lookup_context.disable_cache do
- assert !@lookup_context.cache
+ assert_not @lookup_context.cache
@lookup_context.find("foo", %w(test), true)
end
assert @lookup_context.cache
@@ -210,45 +213,50 @@ end
class LookupContextWithFalseCaching < ActiveSupport::TestCase
def setup
@resolver = ActionView::FixtureResolver.new("test/_foo.erb" => ["Foo", Time.utc(2000)])
- ActionView::Resolver.stubs(:caching?).returns(false)
@lookup_context = ActionView::LookupContext.new(@resolver, {})
end
test "templates are always found in the resolver but timestamp is checked before being compiled" do
- template = @lookup_context.find("foo", %w(test), true)
- assert_equal "Foo", template.source
-
- # Now we are going to change the template, but it won't change the returned template
- # since the timestamp is the same.
- @resolver.hash["test/_foo.erb"][0] = "Bar"
- template = @lookup_context.find("foo", %w(test), true)
- assert_equal "Foo", template.source
-
- # Now update the timestamp.
- @resolver.hash["test/_foo.erb"][1] = Time.now.utc
- template = @lookup_context.find("foo", %w(test), true)
- assert_equal "Bar", template.source
+ ActionView::Resolver.stub(:caching?, false) do
+ template = @lookup_context.find("foo", %w(test), true)
+ assert_equal "Foo", template.source
+
+ # Now we are going to change the template, but it won't change the returned template
+ # since the timestamp is the same.
+ @resolver.hash["test/_foo.erb"][0] = "Bar"
+ template = @lookup_context.find("foo", %w(test), true)
+ assert_equal "Foo", template.source
+
+ # Now update the timestamp.
+ @resolver.hash["test/_foo.erb"][1] = Time.now.utc
+ template = @lookup_context.find("foo", %w(test), true)
+ assert_equal "Bar", template.source
+ end
end
test "if no template was found in the second lookup, with no cache, raise error" do
- template = @lookup_context.find("foo", %w(test), true)
- assert_equal "Foo", template.source
+ ActionView::Resolver.stub(:caching?, false) do
+ template = @lookup_context.find("foo", %w(test), true)
+ assert_equal "Foo", template.source
- @resolver.hash.clear
- assert_raise ActionView::MissingTemplate do
- @lookup_context.find("foo", %w(test), true)
+ @resolver.hash.clear
+ assert_raise ActionView::MissingTemplate do
+ @lookup_context.find("foo", %w(test), true)
+ end
end
end
test "if no template was cached in the first lookup, retrieval should work in the second call" do
- @resolver.hash.clear
- assert_raise ActionView::MissingTemplate do
- @lookup_context.find("foo", %w(test), true)
- end
+ ActionView::Resolver.stub(:caching?, false) do
+ @resolver.hash.clear
+ assert_raise ActionView::MissingTemplate do
+ @lookup_context.find("foo", %w(test), true)
+ end
- @resolver.hash["test/_foo.erb"] = ["Foo", Time.utc(2000)]
- template = @lookup_context.find("foo", %w(test), true)
- assert_equal "Foo", template.source
+ @resolver.hash["test/_foo.erb"] = ["Foo", Time.utc(2000)]
+ template = @lookup_context.find("foo", %w(test), true)
+ assert_equal "Foo", template.source
+ end
end
end
@@ -273,10 +281,9 @@ class TestMissingTemplate < ActiveSupport::TestCase
test "if a single prefix is passed as a string and the lookup fails, MissingTemplate accepts it" do
e = assert_raise ActionView::MissingTemplate do
- details = {:handlers=>[], :formats=>[], :variants=>[], :locale=>[]}
+ details = { handlers: [], formats: [], variants: [], locale: [] }
@lookup_context.view_paths.find("foo", "parent", true, details)
end
assert_match %r{Missing partial parent/_foo with .* Searched in:\n \* "/Path/to/views"\n}, e.message
end
-
end
diff --git a/actionview/test/template/number_helper_test.rb b/actionview/test/template/number_helper_test.rb
index b70b750869..357ae1326a 100644
--- a/actionview/test/template/number_helper_test.rb
+++ b/actionview/test/template/number_helper_test.rb
@@ -1,10 +1,12 @@
+# frozen_string_literal: true
+
require "abstract_unit"
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,22 +15,23 @@ 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")
assert_equal "&amp;pound;1,234,567,890.50", number_to_currency("1234567890.50", unit: "&pound;")
assert_equal "&lt;b&gt;1,234,567,890.50&lt;/b&gt; $", number_to_currency("1234567890.50", format: "<b>%n</b> %u")
assert_equal "&lt;b&gt;1,234,567,890.50&lt;/b&gt; $", number_to_currency("-1234567890.50", negative_format: "<b>%n</b> %u")
- assert_equal "&lt;b&gt;1,234,567,890.50&lt;/b&gt; $", number_to_currency("-1234567890.50", 'negative_format' => "<b>%n</b> %u")
+ assert_equal "&lt;b&gt;1,234,567,890.50&lt;/b&gt; $", number_to_currency("-1234567890.50", "negative_format" => "<b>%n</b> %u")
+ assert_equal "₹ 12,30,000.00", number_to_currency(1230000, delimiter_pattern: /(\d+?)(?=(\d\d)+(\d)(?!\d))/, unit: "₹", format: "%u %n")
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> %')
- assert_equal "<b>100.000</b> %", number_to_percentage(100, format: raw('<b>%n</b> %'))
+ 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> %")
+ assert_equal "<b>100.000</b> %", number_to_percentage(100, format: raw("<b>%n</b> %"))
assert_equal "100%", number_to_percentage(100, precision: 0)
assert_equal "123.4%", number_to_percentage(123.400, precision: 3, strip_insignificant_zeros: true)
assert_equal "1.000,000%", number_to_percentage(1000, delimiter: ".", separator: ",")
@@ -42,13 +45,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)
@@ -56,13 +59,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)
@@ -70,27 +73,27 @@ class NumberHelperTest < ActionView::TestCase
def test_number_to_human_escape_units
volume = { unit: "<b>ml</b>", thousand: "<b>lt</b>", million: "<b>m3</b>", trillion: "<b>km3</b>", quadrillion: "<b>Pl</b>" }
- assert_equal '123 &lt;b&gt;lt&lt;/b&gt;', number_to_human(123456, :units => volume)
- assert_equal '12 &lt;b&gt;ml&lt;/b&gt;', number_to_human(12, :units => volume)
- assert_equal '1.23 &lt;b&gt;m3&lt;/b&gt;', number_to_human(1234567, :units => volume)
- assert_equal '1.23 &lt;b&gt;km3&lt;/b&gt;', number_to_human(1_234_567_000_000, :units => volume)
- assert_equal '1.23 &lt;b&gt;Pl&lt;/b&gt;', number_to_human(1_234_567_000_000_000, :units => volume)
+ assert_equal "123 &lt;b&gt;lt&lt;/b&gt;", number_to_human(123456, units: volume)
+ assert_equal "12 &lt;b&gt;ml&lt;/b&gt;", number_to_human(12, units: volume)
+ assert_equal "1.23 &lt;b&gt;m3&lt;/b&gt;", number_to_human(1234567, units: volume)
+ assert_equal "1.23 &lt;b&gt;km3&lt;/b&gt;", number_to_human(1_234_567_000_000, units: volume)
+ assert_equal "1.23 &lt;b&gt;Pl&lt;/b&gt;", number_to_human(1_234_567_000_000_000, units: volume)
- #Including fractionals
+ # Including fractionals
distance = { mili: "<b>mm</b>", centi: "<b>cm</b>", deci: "<b>dm</b>", unit: "<b>m</b>",
ten: "<b>dam</b>", hundred: "<b>hm</b>", thousand: "<b>km</b>",
- micro: "<b>um</b>", nano: "<b>nm</b>", pico: "<b>pm</b>", femto: "<b>fm</b>"}
- assert_equal '1.23 &lt;b&gt;mm&lt;/b&gt;', number_to_human(0.00123, :units => distance)
- assert_equal '1.23 &lt;b&gt;cm&lt;/b&gt;', number_to_human(0.0123, :units => distance)
- assert_equal '1.23 &lt;b&gt;dm&lt;/b&gt;', number_to_human(0.123, :units => distance)
- assert_equal '1.23 &lt;b&gt;m&lt;/b&gt;', number_to_human(1.23, :units => distance)
- assert_equal '1.23 &lt;b&gt;dam&lt;/b&gt;', number_to_human(12.3, :units => distance)
- assert_equal '1.23 &lt;b&gt;hm&lt;/b&gt;', number_to_human(123, :units => distance)
- assert_equal '1.23 &lt;b&gt;km&lt;/b&gt;', number_to_human(1230, :units => distance)
- assert_equal '1.23 &lt;b&gt;um&lt;/b&gt;', number_to_human(0.00000123, :units => distance)
- assert_equal '1.23 &lt;b&gt;nm&lt;/b&gt;', number_to_human(0.00000000123, :units => distance)
- assert_equal '1.23 &lt;b&gt;pm&lt;/b&gt;', number_to_human(0.00000000000123, :units => distance)
- assert_equal '1.23 &lt;b&gt;fm&lt;/b&gt;', number_to_human(0.00000000000000123, :units => distance)
+ micro: "<b>um</b>", nano: "<b>nm</b>", pico: "<b>pm</b>", femto: "<b>fm</b>" }
+ assert_equal "1.23 &lt;b&gt;mm&lt;/b&gt;", number_to_human(0.00123, units: distance)
+ assert_equal "1.23 &lt;b&gt;cm&lt;/b&gt;", number_to_human(0.0123, units: distance)
+ assert_equal "1.23 &lt;b&gt;dm&lt;/b&gt;", number_to_human(0.123, units: distance)
+ assert_equal "1.23 &lt;b&gt;m&lt;/b&gt;", number_to_human(1.23, units: distance)
+ assert_equal "1.23 &lt;b&gt;dam&lt;/b&gt;", number_to_human(12.3, units: distance)
+ assert_equal "1.23 &lt;b&gt;hm&lt;/b&gt;", number_to_human(123, units: distance)
+ assert_equal "1.23 &lt;b&gt;km&lt;/b&gt;", number_to_human(1230, units: distance)
+ assert_equal "1.23 &lt;b&gt;um&lt;/b&gt;", number_to_human(0.00000123, units: distance)
+ assert_equal "1.23 &lt;b&gt;nm&lt;/b&gt;", number_to_human(0.00000000123, units: distance)
+ assert_equal "1.23 &lt;b&gt;pm&lt;/b&gt;", number_to_human(0.00000000000123, units: distance)
+ assert_equal "1.23 &lt;b&gt;fm&lt;/b&gt;", number_to_human(0.00000000000000123, units: distance)
end
def test_number_helpers_escape_delimiter_and_separator
@@ -115,51 +118,51 @@ class NumberHelperTest < ActionView::TestCase
end
def test_number_to_human_with_custom_translation_scope
- I18n.backend.store_translations 'ts',
- :custom_units_for_number_to_human => {:mili => "mm", :centi => "cm", :deci => "dm", :unit => "m", :ten => "dam", :hundred => "hm", :thousand => "km"}
- assert_equal "1.01 cm", number_to_human(0.0101, :locale => 'ts', :units => :custom_units_for_number_to_human)
+ I18n.backend.store_translations "ts",
+ custom_units_for_number_to_human: { mili: "mm", centi: "cm", deci: "dm", unit: "m", ten: "dam", hundred: "hm", thousand: "km" }
+ assert_equal "1.01 cm", number_to_human(0.0101, locale: "ts", units: :custom_units_for_number_to_human)
ensure
I18n.reload!
end
def test_number_helpers_outputs_are_html_safe
- assert number_to_human(1).html_safe?
- assert !number_to_human("<script></script>").html_safe?
- assert number_to_human("asdf".html_safe).html_safe?
- assert number_to_human("1".html_safe).html_safe?
-
- assert number_to_human_size(1).html_safe?
- assert number_to_human_size(1000000).html_safe?
- assert !number_to_human_size("<script></script>").html_safe?
- assert number_to_human_size("asdf".html_safe).html_safe?
- assert number_to_human_size("1".html_safe).html_safe?
-
- assert number_with_precision(1, strip_insignificant_zeros: false).html_safe?
- assert number_with_precision(1, strip_insignificant_zeros: true).html_safe?
- assert !number_with_precision("<script></script>").html_safe?
- assert number_with_precision("asdf".html_safe).html_safe?
- assert number_with_precision("1".html_safe).html_safe?
-
- assert number_to_currency(1).html_safe?
- assert !number_to_currency("<script></script>").html_safe?
- assert number_to_currency("asdf".html_safe).html_safe?
- assert number_to_currency("1".html_safe).html_safe?
-
- assert number_to_percentage(1).html_safe?
- assert !number_to_percentage("<script></script>").html_safe?
- assert number_to_percentage("asdf".html_safe).html_safe?
- assert number_to_percentage("1".html_safe).html_safe?
-
- assert number_to_phone(1).html_safe?
+ assert_predicate number_to_human(1), :html_safe?
+ assert_not_predicate number_to_human("<script></script>"), :html_safe?
+ assert_predicate number_to_human("asdf".html_safe), :html_safe?
+ assert_predicate number_to_human("1".html_safe), :html_safe?
+
+ assert_predicate number_to_human_size(1), :html_safe?
+ assert_predicate number_to_human_size(1000000), :html_safe?
+ assert_not_predicate number_to_human_size("<script></script>"), :html_safe?
+ assert_predicate number_to_human_size("asdf".html_safe), :html_safe?
+ assert_predicate number_to_human_size("1".html_safe), :html_safe?
+
+ assert_predicate number_with_precision(1, strip_insignificant_zeros: false), :html_safe?
+ assert_predicate number_with_precision(1, strip_insignificant_zeros: true), :html_safe?
+ assert_not_predicate number_with_precision("<script></script>"), :html_safe?
+ assert_predicate number_with_precision("asdf".html_safe), :html_safe?
+ assert_predicate number_with_precision("1".html_safe), :html_safe?
+
+ assert_predicate number_to_currency(1), :html_safe?
+ assert_not_predicate number_to_currency("<script></script>"), :html_safe?
+ assert_predicate number_to_currency("asdf".html_safe), :html_safe?
+ assert_predicate number_to_currency("1".html_safe), :html_safe?
+
+ assert_predicate number_to_percentage(1), :html_safe?
+ assert_not_predicate number_to_percentage("<script></script>"), :html_safe?
+ assert_predicate number_to_percentage("asdf".html_safe), :html_safe?
+ assert_predicate number_to_percentage("1".html_safe), :html_safe?
+
+ assert_predicate number_to_phone(1), :html_safe?
assert_equal "&lt;script&gt;&lt;/script&gt;", number_to_phone("<script></script>")
- assert number_to_phone("<script></script>").html_safe?
- assert number_to_phone("asdf".html_safe).html_safe?
- assert number_to_phone("1".html_safe).html_safe?
-
- assert number_with_delimiter(1).html_safe?
- assert !number_with_delimiter("<script></script>").html_safe?
- assert number_with_delimiter("asdf".html_safe).html_safe?
- assert number_with_delimiter("1".html_safe).html_safe?
+ assert_predicate number_to_phone("<script></script>"), :html_safe?
+ assert_predicate number_to_phone("asdf".html_safe), :html_safe?
+ assert_predicate number_to_phone("1".html_safe), :html_safe?
+
+ assert_predicate number_with_delimiter(1), :html_safe?
+ assert_not_predicate number_with_delimiter("<script></script>"), :html_safe?
+ assert_predicate number_with_delimiter("asdf".html_safe), :html_safe?
+ assert_predicate number_with_delimiter("1".html_safe), :html_safe?
end
def test_number_helpers_should_raise_error_if_invalid_when_specified
diff --git a/actionview/test/template/output_safety_helper_test.rb b/actionview/test/template/output_safety_helper_test.rb
index a1bf0e1a5f..faeeded1c8 100644
--- a/actionview/test/template/output_safety_helper_test.rb
+++ b/actionview/test/template/output_safety_helper_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class OutputSafetyHelperTest < ActionView::TestCase
tests ActionView::Helpers::OutputSafetyHelper
@@ -10,7 +12,7 @@ class OutputSafetyHelperTest < ActionView::TestCase
test "raw returns the safe string" do
result = raw(@string)
assert_equal @string, result
- assert result.html_safe?
+ assert_predicate result, :html_safe?
end
test "raw handles nil values correctly" do
@@ -18,18 +20,100 @@ class OutputSafetyHelperTest < ActionView::TestCase
end
test "safe_join should html_escape any items, including the separator, if they are not html_safe" do
- joined = safe_join(["<p>foo</p>".html_safe, "<p>bar</p>"], "<br />")
+ joined = safe_join([raw("<p>foo</p>"), "<p>bar</p>"], "<br />")
assert_equal "<p>foo</p>&lt;br /&gt;&lt;p&gt;bar&lt;/p&gt;", joined
- joined = safe_join(["<p>foo</p>".html_safe, "<p>bar</p>".html_safe], "<br />".html_safe)
+ joined = safe_join([raw("<p>foo</p>"), raw("<p>bar</p>")], raw("<br />"))
assert_equal "<p>foo</p><br /><p>bar</p>", joined
end
test "safe_join should work recursively similarly to Array.join" do
- joined = safe_join(['a',['b','c']], ':')
- assert_equal 'a:b:c', joined
+ joined = safe_join(["a", ["b", "c"]], ":")
+ assert_equal "a:b:c", joined
+
+ joined = safe_join(['"a"', ["<b>", "<c>"]], " <br/> ")
+ 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_predicate actual, :html_safe?
+ assert_equal("&lt;, &gt;, &amp;, &#39;, and &quot;", actual)
+
+ actual = to_sentence(%w(<script>))
+ assert_predicate actual, :html_safe?
+ assert_equal("&lt;script&gt;", actual)
+ end
+
+ test "to_sentence does not double escape if single value is html_safe" do
+ assert_equal("&lt;script&gt;", to_sentence([ERB::Util.html_escape("<script>")]))
+ assert_equal("&lt;script&gt;", to_sentence(["&lt;script&gt;".html_safe]))
+ assert_equal("&amp;lt;script&amp;gt;", to_sentence(["&lt;script&gt;"]))
+ end
+
+ test "to_sentence connector words are checked for html safety" do
+ assert_equal "one & two, and three", to_sentence(["one", "two", "three"], words_connector: " & ".html_safe)
+ assert_equal "one & two", to_sentence(["one", "two"], two_words_connector: " & ".html_safe)
+ assert_equal "one, two &lt;script&gt;alert(1)&lt;/script&gt; three", to_sentence(["one", "two", "three"], last_word_connector: " <script>alert(1)</script> ")
+ end
+
+ test "to_sentence should not escape html_safe values" do
+ ptag = content_tag("p") do
+ safe_join(["<marquee>shady stuff</marquee>", tag("br")])
+ end
+ url = "https://example.com"
+ expected = %(<a href="#{url}">#{url}</a> and <p>&lt;marquee&gt;shady stuff&lt;/marquee&gt;<br /></p>)
+ actual = to_sentence([link_to(url, url), ptag])
+ assert_predicate actual, :html_safe?
+ assert_equal(expected, actual)
+ end
+
+ test "to_sentence handles blank strings" do
+ actual = to_sentence(["", "two", "three"])
+ assert_predicate actual, :html_safe?
+ assert_equal ", two, and three", actual
+ end
+
+ test "to_sentence handles nil values" do
+ actual = to_sentence([nil, "two", "three"])
+ assert_predicate actual, :html_safe?
+ assert_equal ", two, and three", actual
+ end
+
+ test "to_sentence still supports ActiveSupports Array#to_sentence arguments" do
+ assert_equal "one two, and three", to_sentence(["one", "two", "three"], words_connector: " ")
+ assert_equal "one & two, and three", to_sentence(["one", "two", "three"], words_connector: " & ".html_safe)
+ assert_equal "onetwo, and three", to_sentence(["one", "two", "three"], words_connector: nil)
+ assert_equal "one, two, and also three", to_sentence(["one", "two", "three"], last_word_connector: ", and also ")
+ assert_equal "one, twothree", to_sentence(["one", "two", "three"], last_word_connector: nil)
+ 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
- joined = safe_join(['"a"',['<b>','<c>']], ' <br/> ')
- assert_equal '&quot;a&quot; &lt;br/&gt; &lt;b&gt; &lt;br/&gt; &lt;c&gt;', joined
+ 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/partial_iteration_test.rb b/actionview/test/template/partial_iteration_test.rb
index 695f9f1bef..1c3c566667 100644
--- a/actionview/test/template/partial_iteration_test.rb
+++ b/actionview/test/template/partial_iteration_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'action_view/renderer/partial_renderer'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "action_view/renderer/partial_renderer"
class PartialIterationTest < ActiveSupport::TestCase
def test_has_size_and_index
@@ -16,7 +18,7 @@ class PartialIterationTest < ActiveSupport::TestCase
def test_first_is_false_unless_current_is_at_the_first_index
iteration = ActionView::PartialIteration.new 3
iteration.iterate!
- assert !iteration.first?, "not first when current is 1"
+ assert_not iteration.first?, "not first when current is 1"
end
def test_last_is_true_when_current_is_at_the_last_index
@@ -28,6 +30,6 @@ class PartialIterationTest < ActiveSupport::TestCase
def test_last_is_false_unless_current_is_at_the_last_index
iteration = ActionView::PartialIteration.new 3
- assert !iteration.last?, "not last when current is 0"
+ assert_not iteration.last?, "not last when current is 0"
end
end
diff --git a/actionview/test/template/record_identifier_test.rb b/actionview/test/template/record_identifier_test.rb
index 04898c0b0e..29012e943d 100644
--- a/actionview/test/template/record_identifier_test.rb
+++ b/actionview/test/template/record_identifier_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'controller/fake_models'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "controller/fake_models"
class RecordIdentifierTest < ActiveSupport::TestCase
include ActionView::RecordIdentifier
@@ -7,8 +9,8 @@ class RecordIdentifierTest < ActiveSupport::TestCase
def setup
@klass = Comment
@record = @klass.new
- @singular = 'comment'
- @plural = 'comments'
+ @singular = "comment"
+ @plural = "comments"
end
def test_dom_id_with_new_record
@@ -73,7 +75,7 @@ class RecordIdentifierWithoutActiveModelTest < ActiveSupport::TestCase
end
def test_dom_class
- assert_equal 'airplane', dom_class(@record)
+ assert_equal "airplane", dom_class(@record)
end
def test_dom_class_with_prefix
@@ -86,6 +88,6 @@ class RecordIdentifierWithoutActiveModelTest < ActiveSupport::TestCase
end
def test_dom_class_as_singleton_method
- assert_equal 'airplane', ActionView::RecordIdentifier.dom_class(@record)
+ assert_equal "airplane", ActionView::RecordIdentifier.dom_class(@record)
end
end
diff --git a/actionview/test/template/record_tag_helper_test.rb b/actionview/test/template/record_tag_helper_test.rb
deleted file mode 100644
index bfc5d04bed..0000000000
--- a/actionview/test/template/record_tag_helper_test.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-require 'abstract_unit'
-
-class RecordTagPost
- extend ActiveModel::Naming
-
- attr_accessor :id, :body
-
- def initialize
- @id = 45
- @body = "What a wonderful world!"
-
- yield self if block_given?
- end
-end
-
-class RecordTagHelperTest < ActionView::TestCase
-
- tests ActionView::Helpers::RecordTagHelper
-
- def setup
- super
- @post = RecordTagPost.new
- end
-
- def test_content_tag_for
- assert_raises(NoMethodError) { content_tag_for(:li, @post) }
- end
-
- def test_div_for
- assert_raises(NoMethodError) { div_for(@post, class: "special") }
- end
-end
diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb
index 9c2c9507b7..afe68b7ff0 100644
--- a/actionview/test/template/render_test.rb
+++ b/actionview/test/template/render_test.rb
@@ -1,21 +1,27 @@
-require 'abstract_unit'
-require 'controller/fake_models'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "controller/fake_models"
class TestController < ActionController::Base
end
module RenderTestCases
def setup_view(paths)
- @assigns = { :secret => 'in the sauce' }
+ @assigns = { secret: "in the sauce" }
@view = Class.new(ActionView::Base) do
- def view_cache_dependencies; end
+ def view_cache_dependencies; []; end
+
+ def combined_fragment_cache_key(key)
+ [ :views, key ]
+ end
end.new(paths, @assigns)
@controller_view = TestController.new.view_context
# Reload and register danish language for testing
- I18n.backend.store_translations 'da', {}
- I18n.backend.store_translations 'pt-BR', {}
+ I18n.backend.store_translations "da", {}
+ I18n.backend.store_translations "pt-BR", {}
# Ensure original are still the same since we are reindexing view paths
assert_equal ORIGINAL_LOCALES, I18n.available_locales.map(&:to_s).sort
@@ -23,205 +29,231 @@ module RenderTestCases
def test_render_without_options
e = assert_raises(ArgumentError) { @view.render() }
- assert_match(/You invoked render but did not give any of (.+) option./, e.message)
+ assert_match(/You invoked render but did not give any of (.+) option\./, e.message)
end
def test_render_file
- assert_equal "Hello world!", @view.render(:file => "test/hello_world")
+ assert_equal "Hello world!", @view.render(file: "test/hello_world")
end
# Test if :formats, :locale etc. options are passed correctly to the resolvers.
def test_render_file_with_format
- assert_match "<h1>No Comment</h1>", @view.render(:file => "comments/empty", :formats => [:html])
- assert_match "<error>No Comment</error>", @view.render(:file => "comments/empty", :formats => [:xml])
- assert_match "<error>No Comment</error>", @view.render(:file => "comments/empty", :formats => :xml)
+ assert_match "<h1>No Comment</h1>", @view.render(file: "comments/empty", formats: [:html])
+ assert_match "<error>No Comment</error>", @view.render(file: "comments/empty", formats: [:xml])
+ assert_match "<error>No Comment</error>", @view.render(file: "comments/empty", formats: :xml)
end
def test_render_template_with_format
- assert_match "<h1>No Comment</h1>", @view.render(:template => "comments/empty", :formats => [:html])
- assert_match "<error>No Comment</error>", @view.render(:template => "comments/empty", :formats => [:xml])
+ assert_match "<h1>No Comment</h1>", @view.render(template: "comments/empty", formats: [:html])
+ assert_match "<error>No Comment</error>", @view.render(template: "comments/empty", formats: [:xml])
end
def test_rendered_format_without_format
- @view.render(:inline => "test")
+ @view.render(inline: "test")
assert_equal :html, @view.lookup_context.rendered_format
end
def test_render_partial_implicitly_use_format_of_the_rendered_template
@view.lookup_context.formats = [:json]
- assert_equal "Hello world", @view.render(:template => "test/one", :formats => [:html])
+ assert_equal "Hello world", @view.render(template: "test/one", formats: [:html])
end
def test_render_partial_implicitly_use_format_of_the_rendered_partial
@view.lookup_context.formats = [:html]
- assert_equal "Third level", @view.render(:template => "test/html_template")
+ assert_equal "Third level", @view.render(template: "test/html_template")
end
def test_render_partial_use_last_prepended_format_for_partials_with_the_same_names
@view.lookup_context.formats = [:html]
- assert_equal "\nHTML Template, but JSON partial", @view.render(:template => "test/change_priority")
+ assert_equal "\nHTML Template, but JSON partial", @view.render(template: "test/change_priority")
end
def test_render_template_with_a_missing_partial_of_another_format
@view.lookup_context.formats = [:html]
e = assert_raise ActionView::Template::Error do
- @view.render(:template => "with_format", :formats => [:json])
+ @view.render(template: "with_format", formats: [:json])
end
- assert_includes(e.message, "Missing partial /_missing with {:locale=>[:en], :formats=>[:json], :variants=>[], :handlers=>[:raw, :erb, :builder, :ruby]}.")
+ assert_includes(e.message, "Missing partial /_missing with {:locale=>[:en], :formats=>[:json], :variants=>[], :handlers=>[:raw, :erb, :html, :builder, :ruby]}.")
end
def test_render_file_with_locale
- assert_equal "<h1>Kein Kommentar</h1>", @view.render(:file => "comments/empty", :locale => [:de])
- assert_equal "<h1>Kein Kommentar</h1>", @view.render(:file => "comments/empty", :locale => :de)
+ assert_equal "<h1>Kein Kommentar</h1>", @view.render(file: "comments/empty", locale: [:de])
+ assert_equal "<h1>Kein Kommentar</h1>", @view.render(file: "comments/empty", locale: :de)
end
def test_render_template_with_locale
- assert_equal "<h1>Kein Kommentar</h1>", @view.render(:template => "comments/empty", :locale => [:de])
+ assert_equal "<h1>Kein Kommentar</h1>", @view.render(template: "comments/empty", locale: [:de])
+ end
+
+ def test_render_template_with_variants
+ assert_equal "<h1>No Comment</h1>\n", @view.render(template: "comments/empty", variants: :grid)
end
def test_render_file_with_handlers
- assert_equal "<h1>No Comment</h1>\n", @view.render(:file => "comments/empty", :handlers => [:builder])
- assert_equal "<h1>No Comment</h1>\n", @view.render(:file => "comments/empty", :handlers => :builder)
+ assert_equal "<h1>No Comment</h1>\n", @view.render(file: "comments/empty", handlers: [:builder])
+ assert_equal "<h1>No Comment</h1>\n", @view.render(file: "comments/empty", handlers: :builder)
end
def test_render_template_with_handlers
- assert_equal "<h1>No Comment</h1>\n", @view.render(:template => "comments/empty", :handlers => [:builder])
+ assert_equal "<h1>No Comment</h1>\n", @view.render(template: "comments/empty", handlers: [:builder])
end
def test_render_raw_template_with_handlers
- assert_equal "<%= hello_world %>\n", @view.render(:template => "plain_text")
+ assert_equal "<%= hello_world %>\n", @view.render(template: "plain_text")
end
def test_render_raw_template_with_quotes
- assert_equal %q;Here are some characters: !@#$%^&*()-="'}{`; + "\n", @view.render(:template => "plain_text_with_characters")
+ assert_equal %q;Here are some characters: !@#$%^&*()-="'}{`; + "\n", @view.render(template: "plain_text_with_characters")
+ end
+
+ def test_render_raw_is_html_safe_and_does_not_escape_output
+ buffer = ActiveSupport::SafeBuffer.new
+ buffer << @view.render(file: "plain_text")
+ assert_equal true, buffer.html_safe?
+ assert_equal buffer, "<%= hello_world %>\n"
end
def test_render_ruby_template_with_handlers
- assert_equal "Hello from Ruby code", @view.render(:template => "ruby_template")
+ assert_equal "Hello from Ruby code", @view.render(template: "ruby_template")
end
def test_render_ruby_template_inline
- assert_equal '4', @view.render(:inline => "(2**2).to_s", :type => :ruby)
+ assert_equal "4", @view.render(inline: "(2**2).to_s", type: :ruby)
end
def test_render_file_with_localization_on_context_level
old_locale, @view.locale = @view.locale, :da
- assert_equal "Hey verden", @view.render(:file => "test/hello_world")
+ assert_equal "Hey verden", @view.render(file: "test/hello_world")
ensure
@view.locale = old_locale
end
def test_render_file_with_dashed_locale
old_locale, @view.locale = @view.locale, :"pt-BR"
- assert_equal "Ola mundo", @view.render(:file => "test/hello_world")
+ assert_equal "Ola mundo", @view.render(file: "test/hello_world")
ensure
@view.locale = old_locale
end
def test_render_file_at_top_level
- assert_equal 'Elastica', @view.render(:file => '/shared')
+ assert_equal "Elastica", @view.render(file: "/shared")
end
def test_render_file_with_full_path
- template_path = File.join(File.dirname(__FILE__), '../fixtures/test/hello_world')
- assert_equal "Hello world!", @view.render(:file => template_path)
+ template_path = File.expand_path("../fixtures/test/hello_world", __dir__)
+ assert_equal "Hello world!", @view.render(file: template_path)
end
def test_render_file_with_instance_variables
- assert_equal "The secret is in the sauce\n", @view.render(:file => "test/render_file_with_ivar")
+ assert_equal "The secret is in the sauce\n", @view.render(file: "test/render_file_with_ivar")
end
def test_render_file_with_locals
- locals = { :secret => 'in the sauce' }
- assert_equal "The secret is in the sauce\n", @view.render(:file => "test/render_file_with_locals", :locals => locals)
+ locals = { secret: "in the sauce" }
+ assert_equal "The secret is in the sauce\n", @view.render(file: "test/render_file_with_locals", locals: locals)
end
def test_render_file_not_using_full_path_with_dot_in_path
- assert_equal "The secret is in the sauce\n", @view.render(:file => "test/dot.directory/render_file_with_ivar")
+ assert_equal "The secret is in the sauce\n", @view.render(file: "test/dot.directory/render_file_with_ivar")
end
def test_render_partial_from_default
assert_equal "only partial", @view.render("test/partial_only")
end
+ def test_render_outside_path
+ assert File.exist?(File.expand_path("../../test/abstract_unit.rb", __dir__))
+ assert_raises ActionView::MissingTemplate do
+ @view.render(template: "../\\../test/abstract_unit.rb")
+ end
+ end
+
def test_render_partial
- assert_equal "only partial", @view.render(:partial => "test/partial_only")
+ assert_equal "only partial", @view.render(partial: "test/partial_only")
end
def test_render_partial_with_format
- assert_equal 'partial html', @view.render(:partial => 'test/partial')
+ assert_equal "partial html", @view.render(partial: "test/partial")
+ end
+
+ def test_render_partial_with_variants
+ assert_equal "<h1>Partial with variants</h1>\n", @view.render(partial: "test/partial_with_variants", variants: :grid)
end
def test_render_partial_with_selected_format
- assert_equal 'partial html', @view.render(:partial => 'test/partial', :formats => :html)
- assert_equal 'partial js', @view.render(:partial => 'test/partial', :formats => [:js])
+ assert_equal "partial html", @view.render(partial: "test/partial", formats: :html)
+ assert_equal "partial js", @view.render(partial: "test/partial", formats: [:js])
end
def test_render_partial_at_top_level
# file fixtures/_top_level_partial_only (not fixtures/test)
- assert_equal 'top level partial', @view.render(:partial => '/top_level_partial_only')
+ assert_equal "top level partial", @view.render(partial: "/top_level_partial_only")
end
def test_render_partial_with_format_at_top_level
# file fixtures/_top_level_partial.html (not fixtures/test, with format extension)
- assert_equal 'top level partial html', @view.render(:partial => '/top_level_partial')
+ assert_equal "top level partial html", @view.render(partial: "/top_level_partial")
end
def test_render_partial_with_locals
- assert_equal "5", @view.render(:partial => "test/counter", :locals => { :counter_counter => 5 })
+ assert_equal "5", @view.render(partial: "test/counter", locals: { counter_counter: 5 })
end
def test_render_partial_with_locals_from_default
- assert_equal "only partial", @view.render("test/partial_only", :counter_counter => 5)
+ assert_equal "only partial", @view.render("test/partial_only", counter_counter: 5)
end
def test_render_partial_with_number
- assert_nothing_raised { @view.render(:partial => "test/200") }
+ assert_nothing_raised { @view.render(partial: "test/200") }
end
def test_render_partial_with_missing_filename
- assert_raises(ActionView::MissingTemplate) { @view.render(:partial => "test/") }
+ assert_raises(ActionView::MissingTemplate) { @view.render(partial: "test/") }
end
def test_render_partial_with_incompatible_object
- e = assert_raises(ArgumentError) { @view.render(:partial => nil) }
+ e = assert_raises(ArgumentError) { @view.render(partial: nil) }
assert_equal "'#{nil.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.", e.message
end
def test_render_partial_starting_with_a_capital
- assert_nothing_raised { @view.render(:partial => 'test/FooBar') }
+ assert_nothing_raised { @view.render(partial: "test/FooBar") }
end
def test_render_partial_with_hyphen
- assert_nothing_raised { @view.render(:partial => "test/a-in") }
+ assert_nothing_raised { @view.render(partial: "test/a-in") }
+ end
+
+ def test_render_partial_with_unicode_text
+ assert_nothing_raised { @view.render(partial: "test/🍣") }
end
def test_render_partial_with_invalid_option_as
- e = assert_raises(ArgumentError) { @view.render(:partial => "test/partial_only", :as => 'a-in') }
- assert_equal "The value (a-in) of the option `as` is not a valid Ruby identifier; " +
- "make sure it starts with lowercase letter, " +
+ 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, " \
"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, " +
+ 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, " \
"and is followed by any combination of letters, numbers and underscores.", e.message
end
def test_render_partial_with_errors
- e = assert_raises(ActionView::Template::Error) { @view.render(:partial => "test/raise") }
+ e = assert_raises(ActionView::Template::Error) { @view.render(partial: "test/raise") }
assert_match %r!method.*doesnt_exist!, e.message
assert_equal "", e.sub_template_message
assert_equal "1", e.line_number
- assert_equal "1: <%= doesnt_exist %>", e.annoted_source_code.strip
+ assert_equal "1: <%= doesnt_exist %>", e.annoted_source_code[0].strip
assert_equal File.expand_path("#{FIXTURE_LOAD_PATH}/test/_raise.html.erb"), e.file_name
end
def test_render_error_indentation
- e = assert_raises(ActionView::Template::Error) { @view.render(:partial => "test/raise_indentation") }
- error_lines = e.annoted_source_code.split("\n")
+ e = assert_raises(ActionView::Template::Error) { @view.render(partial: "test/raise_indentation") }
+ error_lines = e.annoted_source_code
assert_match %r!error\shere!, e.message
assert_equal "11", e.line_number
assert_equal " 9: <p>Ninth paragraph</p>", error_lines.second
@@ -229,119 +261,143 @@ module RenderTestCases
end
def test_render_sub_template_with_errors
- e = assert_raises(ActionView::Template::Error) { @view.render(:template => "test/sub_template_raise") }
+ 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
def test_render_file_with_errors
- e = assert_raises(ActionView::Template::Error) { @view.render(:file => File.expand_path("test/_raise", FIXTURE_LOAD_PATH)) }
+ e = assert_raises(ActionView::Template::Error) { @view.render(file: File.expand_path("test/_raise", FIXTURE_LOAD_PATH)) }
assert_match %r!method.*doesnt_exist!, e.message
assert_equal "", e.sub_template_message
assert_equal "1", e.line_number
- assert_equal "1: <%= doesnt_exist %>", e.annoted_source_code.strip
+ assert_equal "1: <%= doesnt_exist %>", e.annoted_source_code[0].strip
assert_equal File.expand_path("#{FIXTURE_LOAD_PATH}/test/_raise.html.erb"), e.file_name
end
def test_render_object
- assert_equal "Hello: david", @view.render(:partial => "test/customer", :object => Customer.new("david"))
+ assert_equal "Hello: david", @view.render(partial: "test/customer", object: Customer.new("david"))
+ assert_equal "FalseClass", @view.render(partial: "test/klass", object: false)
+ assert_equal "NilClass", @view.render(partial: "test/klass", object: nil)
end
def test_render_object_with_array
- assert_equal "[1, 2, 3]", @view.render(:partial => "test/object_inspector", :object => [1, 2, 3])
+ assert_equal "[1, 2, 3]", @view.render(partial: "test/object_inspector", object: [1, 2, 3])
end
def test_render_partial_collection
- assert_equal "Hello: davidHello: mary", @view.render(:partial => "test/customer", :collection => [ Customer.new("david"), Customer.new("mary") ])
+ assert_equal "Hello: davidHello: mary", @view.render(partial: "test/customer", collection: [ Customer.new("david"), Customer.new("mary") ])
+ end
+
+ def test_render_partial_collection_with_partial_name_containing_dot
+ assert_equal "Hello: davidHello: mary",
+ @view.render(partial: "test/customer.mobile", collection: [ Customer.new("david"), Customer.new("mary") ])
end
def test_render_partial_collection_as_by_string
assert_equal "david david davidmary mary mary",
- @view.render(:partial => "test/customer_with_var", :collection => [ Customer.new("david"), Customer.new("mary") ], :as => 'customer')
+ @view.render(partial: "test/customer_with_var", collection: [ Customer.new("david"), Customer.new("mary") ], as: "customer")
end
def test_render_partial_collection_as_by_symbol
assert_equal "david david davidmary mary mary",
- @view.render(:partial => "test/customer_with_var", :collection => [ Customer.new("david"), Customer.new("mary") ], :as => :customer)
+ @view.render(partial: "test/customer_with_var", collection: [ Customer.new("david"), Customer.new("mary") ], as: :customer)
end
def test_render_partial_collection_without_as
assert_equal "local_inspector,local_inspector_counter,local_inspector_iteration",
- @view.render(:partial => "test/local_inspector", :collection => [ Customer.new("mary") ])
+ @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 => [])
+ assert_nil @view.render(partial: "test/customer", collection: [])
end
def test_render_partial_with_nil_collection_should_return_nil
- assert_nil @view.render(:partial => "test/customer", :collection => nil)
+ assert_nil @view.render(partial: "test/customer", collection: nil)
+ end
+
+ def test_render_partial_collection_for_non_array
+ customers = Enumerator.new do |y|
+ y.yield(Customer.new("david"))
+ y.yield(Customer.new("mary"))
+ end
+ assert_equal "Hello: davidHello: mary", @view.render(partial: "test/customer", collection: customers)
end
def test_render_partial_without_object_does_not_put_partial_name_to_local_assigns
- assert_equal 'false', @view.render(partial: 'test/partial_name_in_local_assigns')
+ assert_equal "false", @view.render(partial: "test/partial_name_in_local_assigns")
end
def test_render_partial_with_nil_object_puts_partial_name_to_local_assigns
- assert_equal 'true', @view.render(partial: 'test/partial_name_in_local_assigns', object: nil)
+ assert_equal "true", @view.render(partial: "test/partial_name_in_local_assigns", object: nil)
end
def test_render_partial_with_nil_values_in_collection
- assert_equal "Hello: davidHello: Anonymous", @view.render(:partial => "test/customer", :collection => [ Customer.new("david"), nil ])
+ assert_equal "Hello: davidHello: Anonymous", @view.render(partial: "test/customer", collection: [ Customer.new("david"), nil ])
end
def test_render_partial_with_layout_using_collection_and_template
- assert_equal "<b>Hello: Amazon</b><b>Hello: Yahoo</b>", @view.render(:partial => "test/customer", :layout => 'test/b_layout_for_partial', :collection => [ Customer.new("Amazon"), Customer.new("Yahoo") ])
+ assert_equal "<b>Hello: Amazon</b><b>Hello: Yahoo</b>", @view.render(partial: "test/customer", layout: "test/b_layout_for_partial", collection: [ Customer.new("Amazon"), Customer.new("Yahoo") ])
end
def test_render_partial_with_layout_using_collection_and_template_makes_current_item_available_in_layout
assert_equal '<b class="amazon">Hello: Amazon</b><b class="yahoo">Hello: Yahoo</b>',
- @view.render(:partial => "test/customer", :layout => 'test/b_layout_for_partial_with_object', :collection => [ Customer.new("Amazon"), Customer.new("Yahoo") ])
+ @view.render(partial: "test/customer", layout: "test/b_layout_for_partial_with_object", collection: [ Customer.new("Amazon"), Customer.new("Yahoo") ])
end
def test_render_partial_with_layout_using_collection_and_template_makes_current_item_counter_available_in_layout
assert_equal '<b data-counter="0">Hello: Amazon</b><b data-counter="1">Hello: Yahoo</b>',
- @view.render(:partial => "test/customer", :layout => 'test/b_layout_for_partial_with_object_counter', :collection => [ Customer.new("Amazon"), Customer.new("Yahoo") ])
+ @view.render(partial: "test/customer", layout: "test/b_layout_for_partial_with_object_counter", collection: [ Customer.new("Amazon"), Customer.new("Yahoo") ])
end
def test_render_partial_with_layout_using_object_and_template_makes_object_available_in_layout
assert_equal '<b class="amazon">Hello: Amazon</b>',
- @view.render(:partial => "test/customer", :layout => 'test/b_layout_for_partial_with_object', :object => Customer.new("Amazon"))
+ @view.render(partial: "test/customer", layout: "test/b_layout_for_partial_with_object", object: Customer.new("Amazon"))
end
def test_render_partial_with_empty_array_should_return_nil
- assert_nil @view.render(:partial => [])
+ assert_nil @view.render(partial: [])
end
def test_render_partial_using_string
- assert_equal "Hello: Anonymous", @controller_view.render('customer')
+ assert_equal "Hello: Anonymous", @controller_view.render("customer")
end
def test_render_partial_with_locals_using_string
- assert_equal "Hola: david", @controller_view.render('customer_greeting', :greeting => 'Hola', :customer_greeting => Customer.new("david"))
+ assert_equal "Hola: david", @controller_view.render("customer_greeting", greeting: "Hola", customer_greeting: Customer.new("david"))
end
def test_render_partial_with_object_uses_render_partial_path
assert_equal "Hello: lifo",
- @controller_view.render(:partial => Customer.new("lifo"), :locals => {:greeting => "Hello"})
+ @controller_view.render(partial: Customer.new("lifo"), locals: { greeting: "Hello" })
end
def test_render_partial_with_object_and_format_uses_render_partial_path
assert_equal "<greeting>Hello</greeting><name>lifo</name>",
- @controller_view.render(:partial => Customer.new("lifo"), :formats => :xml, :locals => {:greeting => "Hello"})
+ @controller_view.render(partial: Customer.new("lifo"), formats: :xml, locals: { greeting: "Hello" })
end
def test_render_partial_using_object
assert_equal "Hello: lifo",
- @controller_view.render(Customer.new("lifo"), :greeting => "Hello")
+ @controller_view.render(Customer.new("lifo"), greeting: "Hello")
end
def test_render_partial_using_collection
customers = [ Customer.new("Amazon"), Customer.new("Yahoo") ]
assert_equal "Hello: AmazonHello: Yahoo",
- @controller_view.render(customers, :greeting => "Hello")
+ @controller_view.render(customers, greeting: "Hello")
end
def test_render_partial_using_collection_without_path
@@ -352,48 +408,46 @@ module RenderTestCases
exception = assert_raises ActionView::Template::Error do
@controller_view.render("partial_name_local_variable")
end
- assert_instance_of NameError, exception.original_exception
- assert_equal :partial_name_local_variable, exception.original_exception.name
+ assert_instance_of NameError, exception.cause
+ 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
- assert_equal "Before (Josh)\n\nAfter", @view.render(:partial => "test/layout_for_partial", :locals => { :name => "Josh" })
+ 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") }
+ assert_raises(ActionView::MissingTemplate) { @view.render(partial: "test/layout_for_partial") }
ensure
@view.formats = nil
end
def test_render_layout_with_block_and_other_partial_inside
- render = @view.render(:layout => "test/layout_with_partial_and_yield") { "Yield!" }
+ render = @view.render(layout: "test/layout_with_partial_and_yield") { "Yield!" }
assert_equal "Before\npartial html\nYield!\nAfter\n", render
end
def test_render_inline
- assert_equal "Hello, World!", @view.render(:inline => "Hello, World!")
+ assert_equal "Hello, World!", @view.render(inline: "Hello, World!")
end
def test_render_inline_with_locals
- assert_equal "Hello, Josh!", @view.render(:inline => "Hello, <%= name %>!", :locals => { :name => "Josh" })
+ assert_equal "Hello, Josh!", @view.render(inline: "Hello, <%= name %>!", locals: { name: "Josh" })
end
def test_render_fallbacks_to_erb_for_unknown_types
- assert_equal "Hello, World!", @view.render(:inline => "Hello, World!", :type => :bar)
+ assert_equal "Hello, World!", @view.render(inline: "Hello, World!", type: :bar)
end
CustomHandler = lambda do |template|
- "@output_buffer = ''\n" +
+ "@output_buffer = ''.dup\n" \
"@output_buffer << 'source: #{template.source.inspect}'\n"
end
def test_render_inline_with_render_from_to_proc
ActionView::Template.register_template_handler :ruby_handler, :source.to_proc
- assert_equal '3', @view.render(inline: "(1 + 2).to_s", type: :ruby_handler)
+ assert_equal "3", @view.render(inline: "(1 + 2).to_s", type: :ruby_handler)
ensure
ActionView::Template.unregister_template_handler :ruby_handler
end
@@ -413,17 +467,17 @@ module RenderTestCases
end
def test_render_body
- assert_equal 'some body', @view.render(body: 'some body')
+ assert_equal "some body", @view.render(body: "some body")
end
def test_render_plain
- assert_equal 'some plaintext', @view.render(plain: 'some plaintext')
+ assert_equal "some plaintext", @view.render(plain: "some plaintext")
end
def test_render_knows_about_types_registered_when_extensions_are_checked_earlier_in_initialization
ActionView::Template::Handlers.extensions
ActionView::Template.register_template_handler :foo, CustomHandler
- assert ActionView::Template::Handlers.extensions.include?(:foo)
+ assert_includes ActionView::Template::Handlers.extensions, :foo
ensure
ActionView::Template.unregister_template_handler :foo
end
@@ -438,52 +492,55 @@ 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
def test_render_with_layout
assert_equal %(<title></title>\nHello world!\n),
- @view.render(:file => "test/hello_world", :layout => "layouts/yield")
+ @view.render(file: "test/hello_world", layout: "layouts/yield")
end
def test_render_with_layout_which_has_render_inline
assert_equal %(welcome\nHello world!\n),
- @view.render(:file => "test/hello_world", :layout => "layouts/yield_with_render_inline_inside")
+ @view.render(file: "test/hello_world", layout: "layouts/yield_with_render_inline_inside")
end
def test_render_with_layout_which_renders_another_partial
assert_equal %(partial html\nHello world!\n),
- @view.render(:file => "test/hello_world", :layout => "layouts/yield_with_render_partial_inside")
+ @view.render(file: "test/hello_world", layout: "layouts/yield_with_render_partial_inside")
+ end
+
+ def test_render_partial_with_html_only_extension
+ assert_equal %(<h1>partial html</h1>\nHello world!\n),
+ @view.render(file: "test/hello_world", layout: "layouts/render_partial_html")
end
def test_render_layout_with_block_and_yield
assert_equal %(Content from block!\n),
- @view.render(:layout => "layouts/yield_only") { "Content from block!" }
+ @view.render(layout: "layouts/yield_only") { "Content from block!" }
end
def test_render_layout_with_block_and_yield_with_params
assert_equal %(Yield! Content from block!\n),
- @view.render(:layout => "layouts/yield_with_params") { |param| "#{param} Content from block!" }
+ @view.render(layout: "layouts/yield_with_params") { |param| "#{param} Content from block!" }
end
def test_render_layout_with_block_which_renders_another_partial_and_yields
assert_equal %(partial html\nContent from block!\n),
- @view.render(:layout => "layouts/partial_and_yield") { "Content from block!" }
+ @view.render(layout: "layouts/partial_and_yield") { "Content from block!" }
end
def test_render_partial_and_layout_without_block_with_locals
assert_equal %(Before (Foo!)\npartial html\nAfter),
- @view.render(:partial => 'test/partial', :layout => 'test/layout_for_partial', :locals => { :name => 'Foo!'})
+ @view.render(partial: "test/partial", layout: "test/layout_for_partial", locals: { name: "Foo!" })
end
def test_render_partial_and_layout_without_block_with_locals_and_rendering_another_partial
assert_equal %(Before (Foo!)\npartial html\npartial with partial\n\nAfter),
- @view.render(:partial => 'test/partial_with_partial', :layout => 'test/layout_for_partial', :locals => { :name => 'Foo!'})
+ @view.render(partial: "test/partial_with_partial", layout: "test/layout_for_partial", locals: { name: "Foo!" })
end
def test_render_partial_shortcut_with_block_content
@@ -493,42 +550,42 @@ module RenderTestCases
def test_render_layout_with_a_nested_render_layout_call
assert_equal %(Before (Foo!)\nBefore (Bar!)\npartial html\nAfter\npartial with layout\n\nAfter),
- @view.render(:partial => 'test/partial_with_layout', :layout => 'test/layout_for_partial', :locals => { :name => 'Foo!'})
+ @view.render(partial: "test/partial_with_layout", layout: "test/layout_for_partial", locals: { name: "Foo!" })
end
def test_render_layout_with_a_nested_render_layout_call_using_block_with_render_partial
assert_equal %(Before (Foo!)\nBefore (Bar!)\n\n partial html\n\nAfterpartial with layout\n\nAfter),
- @view.render(:partial => 'test/partial_with_layout_block_partial', :layout => 'test/layout_for_partial', :locals => { :name => 'Foo!'})
+ @view.render(partial: "test/partial_with_layout_block_partial", layout: "test/layout_for_partial", locals: { name: "Foo!" })
end
def test_render_layout_with_a_nested_render_layout_call_using_block_with_render_content
assert_equal %(Before (Foo!)\nBefore (Bar!)\n\n Content from inside layout!\n\nAfterpartial with layout\n\nAfter),
- @view.render(:partial => 'test/partial_with_layout_block_content', :layout => 'test/layout_for_partial', :locals => { :name => 'Foo!'})
+ @view.render(partial: "test/partial_with_layout_block_content", layout: "test/layout_for_partial", locals: { name: "Foo!" })
end
def test_render_partial_with_layout_raises_descriptive_error
- e = assert_raises(ActionView::MissingTemplate) { @view.render(partial: 'test/partial', layout: true) }
+ e = assert_raises(ActionView::MissingTemplate) { @view.render(partial: "test/partial", layout: true) }
assert_match "Missing partial /_true with", e.message
end
def test_render_with_nested_layout
assert_equal %(<title>title</title>\n\n<div id="column">column</div>\n<div id="content">content</div>\n),
- @view.render(:file => "test/nested_layout", :layout => "layouts/yield")
+ @view.render(file: "test/nested_layout", layout: "layouts/yield")
end
def test_render_with_file_in_layout
assert_equal %(\n<title>title</title>\n\n),
- @view.render(:file => "test/layout_render_file")
+ @view.render(file: "test/layout_render_file")
end
def test_render_layout_with_object
assert_equal %(<title>David</title>),
- @view.render(:file => "test/layout_render_object")
+ @view.render(file: "test/layout_render_object")
end
def test_render_with_passing_couple_extensions_to_one_register_template_handler_function_call
ActionView::Template.register_template_handler :foo1, :foo2, CustomHandler
- assert_equal @view.render(inline: "Hello, World!", type: :foo1), @view.render(inline: "Hello, World!", type: :foo2)
+ assert_equal @view.render(inline: +"Hello, World!", type: :foo1), @view.render(inline: +"Hello, World!", type: :foo2)
ensure
ActionView::Template.unregister_template_handler :foo1, :foo2
end
@@ -573,7 +630,7 @@ class LazyViewRenderTest < ActiveSupport::TestCase
def test_render_utf8_template_with_magic_comment
with_external_encoding Encoding::ASCII_8BIT do
- result = @view.render(:file => "test/utf8_magic", :formats => [:html], :layouts => "layouts/yield")
+ result = @view.render(file: "test/utf8_magic", formats: [:html], layouts: "layouts/yield")
assert_equal Encoding::UTF_8, result.encoding
assert_equal "\nРусский \nтекст\n\nUTF-8\nUTF-8\nUTF-8\n", result
end
@@ -581,7 +638,7 @@ class LazyViewRenderTest < ActiveSupport::TestCase
def test_render_utf8_template_with_default_external_encoding
with_external_encoding Encoding::UTF_8 do
- result = @view.render(:file => "test/utf8", :formats => [:html], :layouts => "layouts/yield")
+ result = @view.render(file: "test/utf8", formats: [:html], layouts: "layouts/yield")
assert_equal Encoding::UTF_8, result.encoding
assert_equal "Русский текст\n\nUTF-8\nUTF-8\nUTF-8\n", result
end
@@ -589,15 +646,15 @@ class LazyViewRenderTest < ActiveSupport::TestCase
def test_render_utf8_template_with_incompatible_external_encoding
with_external_encoding Encoding::SHIFT_JIS do
- e = assert_raises(ActionView::Template::Error) { @view.render(:file => "test/utf8", :formats => [:html], :layouts => "layouts/yield") }
- assert_match 'Your template was not saved as valid Shift_JIS', e.original_exception.message
+ e = assert_raises(ActionView::Template::Error) { @view.render(file: "test/utf8", formats: [:html], layouts: "layouts/yield") }
+ assert_match "Your template was not saved as valid Shift_JIS", e.cause.message
end
end
def test_render_utf8_template_with_partial_with_incompatible_encoding
with_external_encoding Encoding::SHIFT_JIS do
- e = assert_raises(ActionView::Template::Error) { @view.render(:file => "test/utf8_magic_with_bare_partial", :formats => [:html], :layouts => "layouts/yield") }
- assert_match 'Your template was not saved as valid Shift_JIS', e.original_exception.message
+ e = assert_raises(ActionView::Template::Error) { @view.render(file: "test/utf8_magic_with_bare_partial", formats: [:html], layouts: "layouts/yield") }
+ assert_match "Your template was not saved as valid Shift_JIS", e.cause.message
end
end
@@ -610,46 +667,59 @@ class LazyViewRenderTest < ActiveSupport::TestCase
end
end
-class CachedCollectionViewRenderTest < CachedViewRenderTest
+class CachedCollectionViewRenderTest < ActiveSupport::TestCase
class CachedCustomer < Customer; end
+ include RenderTestCases
+
+ # Ensure view path cache is primed
+ setup do
+ view_paths = ActionController::Base.view_paths
+ assert_equal ActionView::OptimizedFileSystemResolver, view_paths.first.class
+
+ ActionView::PartialRenderer.collection_cache = ActiveSupport::Cache::MemoryStore.new
+
+ setup_view(view_paths)
+ end
+
teardown do
- ActionView::PartialRenderer.collection_cache.clear
+ GC.start
+ I18n.reload!
end
- test "with custom key" do
- customer = Customer.new("david")
- key = cache_key([customer, 'key'], "test/_customer")
+ test "collection caching does not cache by default" do
+ customer = Customer.new("david", 1)
+ key = cache_key(customer, "test/_customer")
- ActionView::PartialRenderer.collection_cache.write(key, 'Hello')
+ ActionView::PartialRenderer.collection_cache.write(key, "Cached")
- assert_equal "Hello",
- @view.render(partial: "test/customer", collection: [customer], cache: ->(item) { [item, 'key'] })
+ assert_not_equal "Cached",
+ @view.render(partial: "test/customer", collection: [customer])
end
- test "automatic caching with inferred cache name" do
- customer = CachedCustomer.new("david")
- key = cache_key(customer, "test/_cached_customer")
+ test "collection caching with partial that doesn't use fragment caching" do
+ customer = Customer.new("david", 1)
+ key = cache_key(customer, "test/_customer")
- ActionView::PartialRenderer.collection_cache.write(key, 'Cached')
+ ActionView::PartialRenderer.collection_cache.write(key, "Cached")
assert_equal "Cached",
- @view.render(partial: "test/cached_customer", collection: [customer])
+ @view.render(partial: "test/customer", collection: [customer], cached: true)
end
- test "automatic caching with as name" do
- customer = CachedCustomer.new("david")
- key = cache_key(customer, "test/_cached_customer_as")
+ test "collection caching with cached true" do
+ customer = CachedCustomer.new("david", 1)
+ key = cache_key(customer, "test/_cached_customer")
- ActionView::PartialRenderer.collection_cache.write(key, 'Cached')
+ ActionView::PartialRenderer.collection_cache.write(key, "Cached")
assert_equal "Cached",
- @view.render(partial: "test/cached_customer_as", collection: [customer], as: :buyer)
+ @view.render(partial: "test/cached_customer", collection: [customer], cached: true)
end
private
- def cache_key(names, virtual_path)
+ def cache_key(*names, virtual_path)
digest = ActionView::Digestor.digest name: virtual_path, finder: @view.lookup_context, dependencies: []
- @view.fragment_cache_key([ *Array(names), digest ])
+ @view.combined_fragment_cache_key([ "#{virtual_path}:#{digest}", *names ])
end
end
diff --git a/actionview/test/template/resolver_cache_test.rb b/actionview/test/template/resolver_cache_test.rb
new file mode 100644
index 0000000000..8a5db1346a
--- /dev/null
+++ b/actionview/test/template/resolver_cache_test.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class ResolverCacheTest < ActiveSupport::TestCase
+ def test_inspect_shields_cache_internals
+ assert_match %r(#<ActionView::Resolver:0x[0-9a-f]+ @cache=#<ActionView::Resolver::Cache:0x[0-9a-f]+ keys=0 queries=0>>), ActionView::Resolver.new.inspect
+ end
+end
diff --git a/actionview/test/template/resolver_patterns_test.rb b/actionview/test/template/resolver_patterns_test.rb
index 575eb9bd28..1e1a4c5063 100644
--- a/actionview/test/template/resolver_patterns_test.rb
+++ b/actionview/test/template/resolver_patterns_test.rb
@@ -1,19 +1,21 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class ResolverPatternsTest < ActiveSupport::TestCase
def setup
- path = File.expand_path("../../fixtures/", __FILE__)
- pattern = ":prefix/{:formats/,}:action{.:formats,}{.:handlers,}"
+ path = File.expand_path("../fixtures", __dir__)
+ pattern = ":prefix/{:formats/,}:action{.:formats,}{+:variants,}{.:handlers,}"
@resolver = ActionView::FileSystemResolver.new(path, pattern)
end
def test_should_return_empty_list_for_unknown_path
- templates = @resolver.find_all("unknown", "custom_pattern", false, {:locale => [], :formats => [:html], :handlers => [:erb]})
+ templates = @resolver.find_all("unknown", "custom_pattern", false, locale: [], formats: [:html], variants: [], handlers: [:erb])
assert_equal [], templates, "expected an empty list of templates"
end
def test_should_return_template_for_declared_path
- templates = @resolver.find_all("path", "custom_pattern", false, {:locale => [], :formats => [:html], :handlers => [:erb]})
+ templates = @resolver.find_all("path", "custom_pattern", false, locale: [], formats: [:html], variants: [], handlers: [:erb])
assert_equal 1, templates.size, "expected one template"
assert_equal "Hello custom patterns!", templates.first.source
assert_equal "custom_pattern/path", templates.first.virtual_path
@@ -21,11 +23,22 @@ class ResolverPatternsTest < ActiveSupport::TestCase
end
def test_should_return_all_templates_when_ambiguous_pattern
- templates = @resolver.find_all("another", "custom_pattern", false, {:locale => [], :formats => [:html], :handlers => [:erb]})
+ templates = @resolver.find_all("another", "custom_pattern", false, locale: [], formats: [:html], variants: [], handlers: [:erb])
assert_equal 2, templates.size, "expected two templates"
assert_equal "Another template!", templates[0].source
assert_equal "custom_pattern/another", templates[0].virtual_path
assert_equal "Hello custom patterns!", templates[1].source
assert_equal "custom_pattern/another", templates[1].virtual_path
end
+
+ def test_should_return_all_variants_for_any
+ templates = @resolver.find_all("hello_world", "test", false, locale: [], formats: [:html, :text], variants: :any, handlers: [:erb])
+ assert_equal 3, templates.size, "expected three templates"
+ assert_equal "Hello phone!", templates[0].source
+ assert_equal "test/hello_world", templates[0].virtual_path
+ assert_equal "Hello texty phone!", templates[1].source
+ assert_equal "test/hello_world", templates[1].virtual_path
+ assert_equal "Hello world!", templates[2].source
+ assert_equal "test/hello_world", templates[2].virtual_path
+ end
end
diff --git a/actionview/test/template/sanitize_helper_test.rb b/actionview/test/template/sanitize_helper_test.rb
index efe846a7eb..181f09ab65 100644
--- a/actionview/test/template/sanitize_helper_test.rb
+++ b/actionview/test/template/sanitize_helper_test.rb
@@ -1,6 +1,8 @@
-require 'abstract_unit'
+# frozen_string_literal: true
-# The exhaustive tests are in test/controller/html/sanitizer_test.rb.
+require "abstract_unit"
+
+# The exhaustive tests are in the rails-html-sanitizer gem.
# This tests that the helpers hook up correctly to the sanitizer classes.
class SanitizeHelperTest < ActionView::TestCase
tests ActionView::Helpers::SanitizeHelper
@@ -10,22 +12,24 @@ 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
- assert_equal '', sanitize("<form action=\"/foo/bar\" method=\"post\"><input></form>")
+ assert_equal "", sanitize("<form action=\"/foo/bar\" method=\"post\"><input></form>")
end
def test_should_sanitize_illegal_style_properties
raw = %(display:block; position:absolute; left:0; top:0; width:100%; height:100%; z-index:1; background-color:black; background-image:url(http://www.ragingplatypus.com/i/cam-full.jpg); background-x:center; background-y:center; background-repeat:repeat;)
- expected = %(display: block; width: 100%; height: 100%; background-color: black; background-x: center; background-y: center;)
- assert_equal expected, sanitize_css(raw)
+ expected = %r(\Adisplay:\s?block;\s?width:\s?100%;\s?height:\s?100%;\s?background-color:\s?black;\s?background-x:\s?center;\s?background-y:\s?center;\z)
+ assert_match expected, sanitize_css(raw)
end
def test_strip_tags
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
@@ -34,6 +38,6 @@ class SanitizeHelperTest < ActionView::TestCase
end
def test_sanitize_is_marked_safe
- assert sanitize("<html><script></script></html>").html_safe?
+ assert_predicate sanitize("<html><script></script></html>"), :html_safe?
end
end
diff --git a/actionview/test/template/streaming_render_test.rb b/actionview/test/template/streaming_render_test.rb
index d06ba4ceb0..f196c42c4f 100644
--- a/actionview/test/template/streaming_render_test.rb
+++ b/actionview/test/template/streaming_render_test.rb
@@ -1,12 +1,14 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class TestController < ActionController::Base
end
-class FiberedTest < ActiveSupport::TestCase
+class SetupFiberedBase < ActiveSupport::TestCase
def setup
view_paths = ActionController::Base.view_paths
- @assigns = { :secret => 'in the sauce', :name => nil }
+ @assigns = { secret: "in the sauce", name: nil }
@view = ActionView::Base.new(view_paths, @assigns)
@controller_view = TestController.new.view_context
end
@@ -17,16 +19,18 @@ class FiberedTest < ActiveSupport::TestCase
def buffered_render(options)
body = render_body(options)
- string = ""
+ string = +""
body.each do |piece|
string << piece
end
string
end
+end
+class FiberedTest < SetupFiberedBase
def test_streaming_works
content = []
- body = render_body(:template => "test/hello_world", :layout => "layouts/yield")
+ body = render_body(template: "test/hello_world", layout: "layouts/yield")
body.each do |piece|
content << piece
@@ -40,68 +44,68 @@ class FiberedTest < ActiveSupport::TestCase
end
def test_render_file
- assert_equal "Hello world!", buffered_render(:file => "test/hello_world")
+ assert_equal "Hello world!", buffered_render(file: "test/hello_world")
end
def test_render_file_with_locals
- locals = { :secret => 'in the sauce' }
- assert_equal "The secret is in the sauce\n", buffered_render(:file => "test/render_file_with_locals", :locals => locals)
+ locals = { secret: "in the sauce" }
+ assert_equal "The secret is in the sauce\n", buffered_render(file: "test/render_file_with_locals", locals: locals)
end
def test_render_partial
- assert_equal "only partial", buffered_render(:partial => "test/partial_only")
+ assert_equal "only partial", buffered_render(partial: "test/partial_only")
end
def test_render_inline
- assert_equal "Hello, World!", buffered_render(:inline => "Hello, World!")
+ assert_equal "Hello, World!", buffered_render(inline: "Hello, World!")
end
def test_render_without_layout
- assert_equal "Hello world!", buffered_render(:template => "test/hello_world")
+ assert_equal "Hello world!", buffered_render(template: "test/hello_world")
end
def test_render_with_layout
assert_equal %(<title></title>\nHello world!\n),
- buffered_render(:template => "test/hello_world", :layout => "layouts/yield")
+ buffered_render(template: "test/hello_world", layout: "layouts/yield")
end
def test_render_with_layout_which_has_render_inline
assert_equal %(welcome\nHello world!\n),
- buffered_render(:template => "test/hello_world", :layout => "layouts/yield_with_render_inline_inside")
+ buffered_render(template: "test/hello_world", layout: "layouts/yield_with_render_inline_inside")
end
def test_render_with_layout_which_renders_another_partial
assert_equal %(partial html\nHello world!\n),
- buffered_render(:template => "test/hello_world", :layout => "layouts/yield_with_render_partial_inside")
+ buffered_render(template: "test/hello_world", layout: "layouts/yield_with_render_partial_inside")
end
def test_render_with_nested_layout
assert_equal %(<title>title</title>\n\n<div id="column">column</div>\n<div id="content">content</div>\n),
- buffered_render(:template => "test/nested_layout", :layout => "layouts/yield")
+ buffered_render(template: "test/nested_layout", layout: "layouts/yield")
end
def test_render_with_file_in_layout
assert_equal %(\n<title>title</title>\n\n),
- buffered_render(:template => "test/layout_render_file")
+ buffered_render(template: "test/layout_render_file")
end
def test_render_with_handler_without_streaming_support
- assert_match "<p>This is grand!</p>", buffered_render(:template => "test/hello")
+ assert_match "<p>This is grand!</p>", buffered_render(template: "test/hello")
end
def test_render_with_streaming_multiple_yields_provide_and_content_for
assert_equal "Yes, \nthis works\n like a charm.",
- buffered_render(:template => "test/streaming", :layout => "layouts/streaming")
+ buffered_render(template: "test/streaming", layout: "layouts/streaming")
end
def test_render_with_streaming_with_fake_yields_and_streaming_buster
assert_equal "This won't look\n good.",
- buffered_render(:template => "test/streaming_buster", :layout => "layouts/streaming")
+ buffered_render(template: "test/streaming_buster", layout: "layouts/streaming")
end
def test_render_with_nested_streaming_multiple_yields_provide_and_content_for
assert_equal "?Yes, \n\nthis works\n\n? like a charm.",
- buffered_render(:template => "test/nested_streaming", :layout => "layouts/streaming")
+ buffered_render(template: "test/nested_streaming", layout: "layouts/streaming")
end
def test_render_with_streaming_and_capture
@@ -109,3 +113,20 @@ class FiberedTest < ActiveSupport::TestCase
buffered_render(template: "test/streaming", layout: "layouts/streaming_with_capture")
end
end
+
+class FiberedWithLocaleTest < SetupFiberedBase
+ def setup
+ @old_locale = I18n.locale
+ I18n.locale = "da"
+ super
+ end
+
+ def teardown
+ I18n.locale = @old_locale
+ end
+
+ def test_render_with_streaming_and_locale
+ assert_equal "layout.locale: da\nview.locale: da\n\n",
+ buffered_render(template: "test/streaming_with_locale", layout: "layouts/streaming_with_locale")
+ end
+end
diff --git a/actionview/test/template/tag_helper_test.rb b/actionview/test/template/tag_helper_test.rb
index d037447567..9a6226fd04 100644
--- a/actionview/test/template/tag_helper_test.rb
+++ b/actionview/test/template/tag_helper_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class TagHelperTest < ActionView::TestCase
include RenderERBUtils
@@ -7,10 +9,28 @@ class TagHelperTest < ActionView::TestCase
def test_tag
assert_equal "<br />", tag("br")
- assert_equal "<br clear=\"left\" />", tag(:br, :clear => "left")
+ assert_equal "<br clear=\"left\" />", tag(:br, clear: "left")
assert_equal "<br>", tag("br", nil, true)
end
+ def test_tag_builder
+ assert_equal "<span></span>", tag.span
+ assert_equal "<span class=\"bookmark\"></span>", tag.span(class: "bookmark")
+ end
+
+ def test_tag_builder_void_tag
+ assert_equal "<br>", tag.br
+ assert_equal "<br class=\"some_class\">", tag.br(class: "some_class")
+ end
+
+ def test_tag_builder_void_tag_with_forced_content
+ assert_equal "<br>some content</br>", tag.br("some content")
+ end
+
+ def test_tag_builder_is_singleton
+ assert_equal tag, tag
+ end
+
def test_tag_options
str = tag("p", "class" => "show", :class => "elsewhere")
assert_match(/class="show"/, str)
@@ -18,31 +38,76 @@ class TagHelperTest < ActionView::TestCase
end
def test_tag_options_rejects_nil_option
- assert_equal "<p />", tag("p", :ignored => nil)
+ assert_equal "<p />", tag("p", ignored: nil)
+ end
+
+ def test_tag_builder_options_rejects_nil_option
+ assert_equal "<p></p>", tag.p(ignored: nil)
end
def test_tag_options_accepts_false_option
- assert_equal "<p value=\"false\" />", tag("p", :value => false)
+ assert_equal "<p value=\"false\" />", tag("p", value: false)
+ end
+
+ def test_tag_builder_options_accepts_false_option
+ assert_equal "<p value=\"false\"></p>", tag.p(value: false)
end
def test_tag_options_accepts_blank_option
- assert_equal "<p included=\"\" />", tag("p", :included => '')
+ assert_equal "<p included=\"\" />", tag("p", included: "")
+ end
+
+ def test_tag_builder_options_accepts_blank_option
+ assert_equal "<p included=\"\"></p>", tag.p(included: "")
+ end
+
+ def test_tag_options_accepts_symbol_option_when_not_escaping
+ assert_equal "<p value=\"symbol\" />", tag("p", { value: :symbol }, false, false)
+ end
+
+ def test_tag_options_accepts_integer_option_when_not_escaping
+ assert_equal "<p value=\"42\" />", tag("p", { value: 42 }, false, false)
end
def test_tag_options_converts_boolean_option
assert_dom_equal '<p disabled="disabled" itemscope="itemscope" multiple="multiple" readonly="readonly" allowfullscreen="allowfullscreen" seamless="seamless" typemustmatch="typemustmatch" sortable="sortable" default="default" inert="inert" truespeed="truespeed" />',
- tag("p", :disabled => true, :itemscope => true, :multiple => true, :readonly => true, :allowfullscreen => true, :seamless => true, :typemustmatch => true, :sortable => true, :default => true, :inert => true, :truespeed => true)
+ tag("p", disabled: true, itemscope: true, multiple: true, readonly: true, allowfullscreen: true, seamless: true, typemustmatch: true, sortable: true, default: true, inert: true, truespeed: true)
+ end
+
+ def test_tag_builder_options_converts_boolean_option
+ assert_dom_equal '<p disabled="disabled" itemscope="itemscope" multiple="multiple" readonly="readonly" allowfullscreen="allowfullscreen" seamless="seamless" typemustmatch="typemustmatch" sortable="sortable" default="default" inert="inert" truespeed="truespeed" />',
+ tag.p(disabled: true, itemscope: true, multiple: true, readonly: true, allowfullscreen: true, seamless: true, typemustmatch: true, sortable: true, default: true, inert: true, truespeed: true)
end
def test_content_tag
assert_equal "<a href=\"create\">Create</a>", content_tag("a", "Create", "href" => "create")
- assert content_tag("a", "Create", "href" => "create").html_safe?
+ assert_predicate content_tag("a", "Create", "href" => "create"), :html_safe?
assert_equal content_tag("a", "Create", "href" => "create"),
- content_tag("a", "Create", :href => "create")
+ content_tag("a", "Create", href: "create")
+ assert_equal "<p>&lt;script&gt;evil_js&lt;/script&gt;</p>",
+ content_tag(:p, "<script>evil_js</script>")
+ assert_equal "<p><script>evil_js</script></p>",
+ content_tag(:p, "<script>evil_js</script>", nil, false)
+ end
+
+ def test_tag_builder_with_content
+ assert_equal "<div id=\"post_1\">Content</div>", tag.div("Content", id: "post_1")
+ assert_predicate tag.div("Content", id: "post_1"), :html_safe?
+ assert_equal tag.div("Content", id: "post_1"),
+ tag.div("Content", "id": "post_1")
assert_equal "<p>&lt;script&gt;evil_js&lt;/script&gt;</p>",
- content_tag(:p, '<script>evil_js</script>')
+ tag.p("<script>evil_js</script>")
assert_equal "<p><script>evil_js</script></p>",
- content_tag(:p, '<script>evil_js</script>', nil, false)
+ tag.p("<script>evil_js</script>", escape_attributes: false)
+ end
+
+ def test_tag_builder_nested
+ assert_equal "<div>content</div>",
+ tag.div { "content" }
+ assert_equal "<div id=\"header\"><span>hello</span></div>",
+ tag.div(id: "header") { |tag| tag.span "hello" }
+ assert_equal "<div id=\"header\"><div class=\"world\"><span>hello</span></div></div>",
+ tag.div(id: "header") { |tag| tag.div(class: "world") { tag.span "hello" } }
end
def test_content_tag_with_block_in_erb
@@ -50,72 +115,138 @@ class TagHelperTest < ActionView::TestCase
assert_dom_equal "<div>Hello world!</div>", buffer
end
+ def test_tag_builder_with_block_in_erb
+ buffer = render_erb("<%= tag.div do %>Hello world!<% end %>")
+ assert_dom_equal "<div>Hello world!</div>", buffer
+ end
+
def test_content_tag_with_block_in_erb_containing_non_displayed_erb
buffer = render_erb("<%= content_tag(:p) do %><% 1 %><% end %>")
assert_dom_equal "<p></p>", buffer
end
+ def test_tag_builder_with_block_in_erb_containing_non_displayed_erb
+ buffer = render_erb("<%= tag.p do %><% 1 %><% end %>")
+ assert_dom_equal "<p></p>", buffer
+ end
+
def test_content_tag_with_block_and_options_in_erb
buffer = render_erb("<%= content_tag(:div, :class => 'green') do %>Hello world!<% end %>")
assert_dom_equal %(<div class="green">Hello world!</div>), buffer
end
+ def test_tag_builder_with_block_and_options_in_erb
+ buffer = render_erb("<%= tag.div(class: 'green') do %>Hello world!<% end %>")
+ assert_dom_equal %(<div class="green">Hello world!</div>), buffer
+ end
+
def test_content_tag_with_block_and_options_out_of_erb
- assert_dom_equal %(<div class="green">Hello world!</div>), content_tag(:div, :class => "green") { "Hello world!" }
+ assert_dom_equal %(<div class="green">Hello world!</div>), content_tag(:div, class: "green") { "Hello world!" }
+ end
+
+ def test_tag_builder_with_block_and_options_out_of_erb
+ assert_dom_equal %(<div class="green">Hello world!</div>), tag.div(class: "green") { "Hello world!" }
end
def test_content_tag_with_block_and_options_outside_out_of_erb
- assert_equal content_tag("a", "Create", :href => "create"),
+ assert_equal content_tag("a", "Create", href: "create"),
content_tag("a", "href" => "create") { "Create" }
end
+ def test_tag_builder_with_block_and_options_outside_out_of_erb
+ assert_equal tag.a("Create", href: "create"),
+ tag.a("href": "create") { "Create" }
+ end
+
def test_content_tag_with_block_and_non_string_outside_out_of_erb
assert_equal content_tag("p"),
content_tag("p") { 3.times { "do_something" } }
end
+ def test_tag_builder_with_block_and_non_string_outside_out_of_erb
+ assert_equal tag.p,
+ tag.p { 3.times { "do_something" } }
+ end
+
def test_content_tag_nested_in_content_tag_out_of_erb
assert_equal content_tag("p", content_tag("b", "Hello")),
content_tag("p") { content_tag("b", "Hello") },
output_buffer
+ assert_equal tag.p(tag.b("Hello")),
+ tag.p { tag.b("Hello") },
+ output_buffer
end
def test_content_tag_nested_in_content_tag_in_erb
assert_equal "<p>\n <b>Hello</b>\n</p>", view.render("test/content_tag_nested_in_content_tag")
+ assert_equal "<p>\n <b>Hello</b>\n</p>", view.render("test/builder_tag_nested_in_content_tag")
end
def test_content_tag_with_escaped_array_class
- str = content_tag('p', "limelight", :class => ["song", "play>"])
+ str = content_tag("p", "limelight", class: ["song", "play>"])
assert_equal "<p class=\"song play&gt;\">limelight</p>", str
- str = content_tag('p', "limelight", :class => ["song", "play"])
+ str = content_tag("p", "limelight", class: ["song", "play"])
assert_equal "<p class=\"song play\">limelight</p>", str
- str = content_tag('p', "limelight", :class => ["song", ["play"]])
+ str = content_tag("p", "limelight", class: ["song", ["play"]])
+ assert_equal "<p class=\"song play\">limelight</p>", str
+ end
+
+ def test_tag_builder_with_escaped_array_class
+ str = tag.p "limelight", class: ["song", "play>"]
+ assert_equal "<p class=\"song play&gt;\">limelight</p>", str
+
+ str = tag.p "limelight", class: ["song", "play"]
+ assert_equal "<p class=\"song play\">limelight</p>", str
+
+ str = tag.p "limelight", class: ["song", ["play"]]
assert_equal "<p class=\"song play\">limelight</p>", str
end
def test_content_tag_with_unescaped_array_class
- str = content_tag('p', "limelight", {:class => ["song", "play>"]}, false)
+ str = content_tag("p", "limelight", { class: ["song", "play>"] }, false)
assert_equal "<p class=\"song play>\">limelight</p>", str
- str = content_tag('p', "limelight", {:class => ["song", ["play>"]]}, false)
+ str = content_tag("p", "limelight", { class: ["song", ["play>"]] }, false)
+ assert_equal "<p class=\"song play>\">limelight</p>", str
+ end
+
+ def test_tag_builder_with_unescaped_array_class
+ str = tag.p "limelight", class: ["song", "play>"], escape_attributes: false
+ assert_equal "<p class=\"song play>\">limelight</p>", str
+
+ str = tag.p "limelight", class: ["song", ["play>"]], escape_attributes: false
assert_equal "<p class=\"song play>\">limelight</p>", str
end
def test_content_tag_with_empty_array_class
- str = content_tag('p', 'limelight', {:class => []})
+ str = content_tag("p", "limelight", class: [])
assert_equal '<p class="">limelight</p>', str
end
+ def test_tag_builder_with_empty_array_class
+ assert_equal '<p class="">limelight</p>', tag.p("limelight", class: [])
+ end
+
def test_content_tag_with_unescaped_empty_array_class
- str = content_tag('p', 'limelight', {:class => []}, false)
+ str = content_tag("p", "limelight", { class: [] }, false)
+ assert_equal '<p class="">limelight</p>', str
+ end
+
+ def test_tag_builder_with_unescaped_empty_array_class
+ str = tag.p "limelight", class: [], escape_attributes: false
assert_equal '<p class="">limelight</p>', str
end
def test_content_tag_with_data_attributes
assert_dom_equal '<p data-number="1" data-string="hello" data-string-with-quotes="double&quot;quote&quot;party&quot;">limelight</p>',
- content_tag('p', "limelight", data: { number: 1, string: 'hello', string_with_quotes: 'double"quote"party"' })
+ content_tag("p", "limelight", data: { number: 1, string: "hello", string_with_quotes: 'double"quote"party"' })
+ end
+
+ def test_tag_builder_with_data_attributes
+ assert_dom_equal '<p data-number="1" data-string="hello" data-string-with-quotes="double&quot;quote&quot;party&quot;">limelight</p>',
+ tag.p("limelight", data: { number: 1, string: "hello", string_with_quotes: 'double"quote"party"' })
end
def test_cdata_section
@@ -132,45 +263,95 @@ class TagHelperTest < ActionView::TestCase
end
def test_escape_once
- assert_equal '1 &lt; 2 &amp; 3', escape_once('1 < 2 &amp; 3')
+ assert_equal "1 &lt; 2 &amp; 3", escape_once("1 < 2 &amp; 3")
assert_equal " &#X27; &#x27; &#x03BB; &#X03bb; &quot; &#39; &lt; &gt; ", escape_once(" &#X27; &#x27; &#x03BB; &#X03bb; \" ' < > ")
end
def test_tag_honors_html_safe_for_param_values
- ['1&amp;2', '1 &lt; 2', '&#8220;test&#8220;'].each do |escaped|
- assert_equal %(<a href="#{escaped}" />), tag('a', :href => escaped.html_safe)
+ ["1&amp;2", "1 &lt; 2", "&#8220;test&#8220;"].each do |escaped|
+ assert_equal %(<a href="#{escaped}" />), tag("a", href: escaped.html_safe)
+ assert_equal %(<a href="#{escaped}"></a>), tag.a(href: escaped.html_safe)
end
end
def test_tag_honors_html_safe_with_escaped_array_class
- str = tag('p', :class => ['song>', 'play>'.html_safe])
- assert_equal '<p class="song&gt; play>" />', str
+ assert_equal '<p class="song&gt; play>" />', tag("p", class: ["song>", raw("play>")])
+ assert_equal '<p class="song> play&gt;" />', tag("p", class: [raw("song>"), "play>"])
+ end
+
+ def test_tag_builder_honors_html_safe_with_escaped_array_class
+ assert_equal '<p class="song&gt; play>"></p>', tag.p(class: ["song>", raw("play>")])
+ assert_equal '<p class="song> play&gt;"></p>', tag.p(class: [raw("song>"), "play>"])
+ end
+
+ def test_tag_does_not_honor_html_safe_double_quotes_as_attributes
+ assert_dom_equal '<p title="&quot;">content</p>',
+ content_tag("p", "content", title: '"'.html_safe)
+ end
- str = tag('p', :class => ['song>'.html_safe, 'play>'])
- assert_equal '<p class="song> play&gt;" />', str
+ def test_data_tag_does_not_honor_html_safe_double_quotes_as_attributes
+ assert_dom_equal '<p data-title="&quot;">content</p>',
+ content_tag("p", "content", data: { title: '"'.html_safe })
end
def test_skip_invalid_escaped_attributes
- ['&1;', '&#1dfa3;', '& #123;'].each do |escaped|
- assert_equal %(<a href="#{escaped.gsub(/&/, '&amp;')}" />), tag('a', :href => escaped)
+ ["&1;", "&#1dfa3;", "& #123;"].each do |escaped|
+ assert_equal %(<a href="#{escaped.gsub(/&/, '&amp;')}" />), tag("a", href: escaped)
+ assert_equal %(<a href="#{escaped.gsub(/&/, '&amp;')}"></a>), tag.a(href: escaped)
end
end
def test_disable_escaping
- assert_equal '<a href="&amp;" />', tag('a', { :href => '&amp;' }, false, false)
+ assert_equal '<a href="&amp;" />', tag("a", { href: "&amp;" }, false, false)
+ end
+
+ def test_tag_builder_disable_escaping
+ assert_equal '<a href="&amp;"></a>', tag.a(href: "&amp;", escape_attributes: false)
+ assert_equal '<a href="&amp;">cnt</a>', tag.a(href: "&amp;", escape_attributes: false) { "cnt" }
+ assert_equal '<br data-hidden="&amp;">', tag.br("data-hidden": "&amp;", escape_attributes: false)
+ assert_equal '<a href="&amp;">content</a>', tag.a("content", href: "&amp;", escape_attributes: false)
+ assert_equal '<a href="&amp;">content</a>', tag.a(href: "&amp;", escape_attributes: false) { "content" }
end
def test_data_attributes
- ['data', :data].each { |data|
+ ["data", :data].each { |data|
assert_dom_equal '<a data-a-float="3.14" data-a-big-decimal="-123.456" data-a-number="1" data-array="[1,2,3]" data-hash="{&quot;key&quot;:&quot;value&quot;}" data-string-with-quotes="double&quot;quote&quot;party&quot;" data-string="hello" data-symbol="foo" />',
- tag('a', { data => { a_float: 3.14, a_big_decimal: BigDecimal.new("-123.456"), a_number: 1, string: 'hello', symbol: :foo, array: [1, 2, 3], hash: { key: 'value'}, string_with_quotes: 'double"quote"party"' } })
+ tag("a", data => { a_float: 3.14, a_big_decimal: BigDecimal("-123.456"), a_number: 1, string: "hello", symbol: :foo, array: [1, 2, 3], hash: { key: "value" }, string_with_quotes: 'double"quote"party"' })
+ assert_dom_equal '<a data-a-float="3.14" data-a-big-decimal="-123.456" data-a-number="1" data-array="[1,2,3]" data-hash="{&quot;key&quot;:&quot;value&quot;}" data-string-with-quotes="double&quot;quote&quot;party&quot;" data-string="hello" data-symbol="foo" />',
+ tag.a(data: { a_float: 3.14, a_big_decimal: BigDecimal("-123.456"), a_number: 1, string: "hello", symbol: :foo, array: [1, 2, 3], hash: { key: "value" }, string_with_quotes: 'double"quote"party"' })
}
end
def test_aria_attributes
- ['aria', :aria].each { |aria|
+ ["aria", :aria].each { |aria|
+ assert_dom_equal '<a aria-a-float="3.14" aria-a-big-decimal="-123.456" aria-a-number="1" aria-array="[1,2,3]" aria-hash="{&quot;key&quot;:&quot;value&quot;}" aria-string-with-quotes="double&quot;quote&quot;party&quot;" aria-string="hello" aria-symbol="foo" />',
+ tag("a", aria => { a_float: 3.14, a_big_decimal: BigDecimal("-123.456"), a_number: 1, string: "hello", symbol: :foo, array: [1, 2, 3], hash: { key: "value" }, string_with_quotes: 'double"quote"party"' })
assert_dom_equal '<a aria-a-float="3.14" aria-a-big-decimal="-123.456" aria-a-number="1" aria-array="[1,2,3]" aria-hash="{&quot;key&quot;:&quot;value&quot;}" aria-string-with-quotes="double&quot;quote&quot;party&quot;" aria-string="hello" aria-symbol="foo" />',
- tag('a', { aria => { a_float: 3.14, a_big_decimal: BigDecimal.new("-123.456"), a_number: 1, string: 'hello', symbol: :foo, array: [1, 2, 3], hash: { key: 'value'}, string_with_quotes: 'double"quote"party"' } })
+ tag.a(aria: { a_float: 3.14, a_big_decimal: BigDecimal("-123.456"), a_number: 1, string: "hello", symbol: :foo, array: [1, 2, 3], hash: { key: "value" }, string_with_quotes: 'double"quote"party"' })
}
end
+
+ def test_link_to_data_nil_equal
+ div_type1 = content_tag(:div, "test", "data-tooltip" => nil)
+ div_type2 = content_tag(:div, "test", data: { tooltip: nil })
+ assert_dom_equal div_type1, div_type2
+ end
+
+ def test_tag_builder_link_to_data_nil_equal
+ div_type1 = tag.div "test", 'data-tooltip': nil
+ div_type2 = tag.div "test", data: { tooltip: nil }
+ assert_dom_equal div_type1, div_type2
+ end
+
+ def test_tag_builder_allow_call_via_method_object
+ assert_equal "<foo></foo>", tag.method(:foo).call
+ end
+
+ def test_tag_builder_dasherize_names
+ assert_equal "<img-slider></img-slider>", tag.img_slider
+ end
+
+ def test_respond_to
+ assert_respond_to tag, :any_tag
+ end
end
diff --git a/actionview/test/template/template_error_test.rb b/actionview/test/template/template_error_test.rb
index 3971ec809c..c4dc88e4aa 100644
--- a/actionview/test/template/template_error_test.rb
+++ b/actionview/test/template/template_error_test.rb
@@ -1,20 +1,37 @@
+# frozen_string_literal: true
+
require "abstract_unit"
class TemplateErrorTest < ActiveSupport::TestCase
def test_provides_original_message
- error = ActionView::Template::Error.new("test", Exception.new("original"))
+ error = begin
+ raise Exception.new("original")
+ rescue Exception
+ raise ActionView::Template::Error.new("test") rescue $!
+ end
+
assert_equal "original", error.message
end
def test_provides_original_backtrace
- original_exception = Exception.new
- original_exception.set_backtrace(%W[ foo bar baz ])
- error = ActionView::Template::Error.new("test", original_exception)
+ error = begin
+ original_exception = Exception.new
+ original_exception.set_backtrace(%W[ foo bar baz ])
+ raise original_exception
+ rescue Exception
+ raise ActionView::Template::Error.new("test") rescue $!
+ end
+
assert_equal %W[ foo bar baz ], error.backtrace
end
def test_provides_useful_inspect
- error = ActionView::Template::Error.new("test", Exception.new("original"))
+ error = begin
+ raise Exception.new("original")
+ rescue Exception
+ raise ActionView::Template::Error.new("test") rescue $!
+ end
+
assert_equal "#<ActionView::Template::Error: original>", error.inspect
end
end
diff --git a/actionview/test/template/template_test.rb b/actionview/test/template/template_test.rb
index d3b51cd629..b348d1f17b 100644
--- a/actionview/test/template/template_test.rb
+++ b/actionview/test/template/template_test.rb
@@ -1,4 +1,6 @@
# encoding: US-ASCII
+# frozen_string_literal: true
+
require "abstract_unit"
require "logger"
@@ -35,7 +37,7 @@ class TestERBTemplate < ActiveSupport::TestCase
"<%= @virtual_path %>",
"partial",
ERBHandler,
- :virtual_path => "partial"
+ virtual_path: "partial"
)
end
@@ -53,7 +55,7 @@ class TestERBTemplate < ActiveSupport::TestCase
end
def new_template(body = "<%= hello %>", details = { format: :html })
- ActionView::Template.new(body, "hello template", details.fetch(:handler) { ERBHandler }, {:virtual_path => "hello"}.merge!(details))
+ ActionView::Template.new(body.dup, "hello template", details.fetch(:handler) { ERBHandler }, { virtual_path: "hello" }.merge!(details))
end
def render(locals = {})
@@ -80,7 +82,7 @@ class TestERBTemplate < ActiveSupport::TestCase
end
def test_raw_template
- @template = new_template("<%= hello %>", :handler => ActionView::Template::Handlers::Raw.new)
+ @template = new_template("<%= hello %>", handler: ActionView::Template::Handlers::Raw.new)
assert_equal "<%= hello %>", render
end
@@ -91,7 +93,7 @@ class TestERBTemplate < ActiveSupport::TestCase
end
def test_template_does_not_lose_its_source_after_rendering_if_it_does_not_have_a_virtual_path
- @template = new_template("Hello", :virtual_path => nil)
+ @template = new_template("Hello", virtual_path: nil)
render
assert_equal "Hello", @template.source
end
@@ -99,7 +101,7 @@ class TestERBTemplate < ActiveSupport::TestCase
def test_locals
@template = new_template("<%= my_local %>")
@template.locals = [:my_local]
- assert_equal "I am a local", render(:my_local => "I am a local")
+ assert_equal "I am a local", render(my_local: "I am a local")
end
def test_restores_buffer
@@ -116,21 +118,23 @@ class TestERBTemplate < ActiveSupport::TestCase
end
def test_refresh_with_templates
- @template = new_template("Hello", :virtual_path => "test/foo/bar")
+ @template = new_template("Hello", virtual_path: "test/foo/bar")
@template.locals = [:key]
- @context.lookup_context.expects(:find_template).with("bar", %w(test/foo), false, [:key]).returns("template")
- assert_equal "template", @template.refresh(@context)
+ assert_called_with(@context.lookup_context, :find_template, ["bar", %w(test/foo), false, [:key]], returns: "template") do
+ assert_equal "template", @template.refresh(@context)
+ end
end
def test_refresh_with_partials
- @template = new_template("Hello", :virtual_path => "test/_foo")
+ @template = new_template("Hello", virtual_path: "test/_foo")
@template.locals = [:key]
- @context.lookup_context.expects(:find_template).with("foo", %w(test), true, [:key]).returns("partial")
- assert_equal "partial", @template.refresh(@context)
+ assert_called_with(@context.lookup_context, :find_template, ["foo", %w(test), true, [:key]], returns: "partial") do
+ assert_equal "partial", @template.refresh(@context)
+ end
end
def test_refresh_raises_an_error_without_virtual_path
- @template = new_template("Hello", :virtual_path => nil)
+ @template = new_template("Hello", virtual_path: nil)
assert_raise RuntimeError do
@template.refresh(@context)
end
@@ -169,14 +173,14 @@ class TestERBTemplate < ActiveSupport::TestCase
# inside Rails.
def test_lying_with_magic_comment
assert_raises(ActionView::Template::Error) do
- @template = new_template("# encoding: UTF-8\nhello \xFCmlat", :virtual_path => nil)
+ @template = new_template("# encoding: UTF-8\nhello \xFCmlat", virtual_path: nil)
render
end
end
def test_encoding_can_be_specified_with_magic_comment_in_erb
with_external_encoding Encoding::UTF_8 do
- @template = new_template("<%# encoding: ISO-8859-1 %>hello \xFCmlat", :virtual_path => nil)
+ @template = new_template("<%# encoding: ISO-8859-1 %>hello \xFCmlat", virtual_path: nil)
assert_equal Encoding::UTF_8, render.encoding
assert_equal "hello \u{fc}mlat", render
end
@@ -184,42 +188,19 @@ class TestERBTemplate < ActiveSupport::TestCase
def test_error_when_template_isnt_valid_utf8
e = assert_raises ActionView::Template::Error do
- @template = new_template("hello \xFCmlat", :virtual_path => nil)
+ @template = new_template("hello \xFCmlat", virtual_path: nil)
render
end
- assert_match(/\xFC/, e.message)
- end
-
- def test_not_eligible_for_collection_caching_without_cache_call
- [
- "<%= 'Hello' %>",
- "<% cache_customer = 42 %>",
- "<% cache customer.name do %><% end %>",
- "<% my_cache customer do %><% end %>"
- ].each do |body|
- template = new_template(body, virtual_path: "test/foo/_customer")
- assert_not template.eligible_for_collection_caching?, "Template #{body.inspect} should not be eligible for collection caching"
- end
- end
-
- def test_eligible_for_collection_caching_with_cache_call_or_explicit
- [
- "<% cache customer do %><% end %>",
- "<% cache(customer) do %><% end %>",
- "<% cache( customer) do %><% end %>",
- "<% cache( customer ) do %><% end %>",
- "<%cache customer do %><% end %>",
- "<% cache customer do %><% end %>",
- " <% cache customer do %>\n<% end %>\n",
- "<%# comment %><% cache customer do %><% end %>",
- "<%# comment %>\n<% cache customer do %><% end %>",
- "<%# comment\n line 2\n line 3 %>\n<% cache customer do %><% end %>",
- "<%# comment 1 %>\n<%# comment 2 %>\n<% cache customer do %><% end %>",
- "<%# comment 1 %>\n<%# Template Collection: customer %>\n<% my_cache customer do %><% end %>"
- ].each do |body|
- template = new_template(body, virtual_path: "test/foo/_customer")
- assert template.eligible_for_collection_caching?, "Template #{body.inspect} should be eligible for collection caching"
- end
+ # Hack: We write the regexp this way because the parser of RuboCop
+ # errs with /\xFC/.
+ assert_match(Regexp.new("\xFC"), e.message)
+ end
+
+ def test_template_is_marshalable
+ template = new_template
+ serialized = Marshal.load(Marshal.dump(template))
+ assert_equal template.identifier, serialized.identifier
+ assert_equal template.source, serialized.source
end
def with_external_encoding(encoding)
diff --git a/actionview/test/template/test_case_test.rb b/actionview/test/template/test_case_test.rb
index 05c3dc0613..976b6bc77e 100644
--- a/actionview/test/template/test_case_test.rb
+++ b/actionview/test/template/test_case_test.rb
@@ -1,32 +1,34 @@
-require 'abstract_unit'
-require 'rails/engine'
+# frozen_string_literal: true
-module ActionView
+require "abstract_unit"
+require "rails/engine"
+module ActionView
module ATestHelper
end
module AnotherTestHelper
def from_another_helper
- 'Howdy!'
+ "Howdy!"
end
end
module ASharedTestHelper
def from_shared_helper
- 'Holla!'
+ "Holla!"
end
end
class TestCase
helper ASharedTestHelper
+ DeveloperStruct = Struct.new(:name)
module SharedTests
def self.included(test_case)
test_case.class_eval do
test "helpers defined on ActionView::TestCase are available" do
- assert test_case.ancestors.include?(ASharedTestHelper)
- assert_equal 'Holla!', from_shared_helper
+ assert_includes test_case.ancestors, ASharedTestHelper
+ assert_equal "Holla!", from_shared_helper
end
end
end
@@ -41,27 +43,31 @@ module ActionView
assert_same view, view
end
+ test "exposes params" do
+ assert params.is_a? ActionController::Parameters
+ end
+
test "exposes view as _view for backwards compatibility" do
assert_same _view, view
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
- assert_equal 'Eloy', render('developers/developer', :developer => stub(:name => 'Eloy'))
+ assert_equal "Eloy", render("developers/developer", developer: DeveloperStruct.new("Eloy"))
end
test "can render a layout with block" do
assert_equal "Before (ChrisCruft)\n!\nAfter",
- render(:layout => "test/layout_for_partial", :locals => {:name => "ChrisCruft"}) {"!"}
+ render(layout: "test/layout_for_partial", locals: { name: "ChrisCruft" }) { "!" }
end
helper AnotherTestHelper
test "additional helper classes can be specified as in a controller" do
- assert test_case.ancestors.include?(AnotherTestHelper)
- assert_equal 'Howdy!', from_another_helper
+ assert_includes test_case.ancestors, AnotherTestHelper
+ assert_equal "Howdy!", from_another_helper
end
test "determine_default_helper_class returns nil if the test name constant resolves to a class" do
@@ -69,17 +75,19 @@ module ActionView
end
test "delegates notice to request.flash[:notice]" do
- view.request.flash.expects(:[]).with(:notice)
- view.notice
+ assert_called_with(view.request.flash, :[], [:notice]) do
+ view.notice
+ end
end
test "delegates alert to request.flash[:alert]" do
- view.request.flash.expects(:[]).with(:alert)
- view.alert
+ assert_called_with(view.request.flash, :[], [:alert]) do
+ view.alert
+ end
end
test "uses controller lookup context" do
- assert_equal self.lookup_context, @controller.lookup_context
+ assert_equal lookup_context, @controller.lookup_context
end
end
@@ -90,44 +98,44 @@ module ActionView
tests ATestHelper
test "tests the specified helper module" do
assert_equal ATestHelper, test_case.helper_class
- assert test_case.ancestors.include?(ATestHelper)
+ assert_includes test_case.ancestors, ATestHelper
end
helper AnotherTestHelper
test "additional helper classes can be specified as in a controller" do
- assert test_case.ancestors.include?(AnotherTestHelper)
- assert_equal 'Howdy!', from_another_helper
+ assert_includes test_case.ancestors, AnotherTestHelper
+ assert_equal "Howdy!", from_another_helper
test_case.helper_class.module_eval do
def render_from_helper
from_another_helper
end
end
- assert_equal 'Howdy!', render(:partial => 'test/from_helper')
+ assert_equal "Howdy!", render(partial: "test/from_helper")
end
end
class HelperInclusionTest < ActionView::TestCase
module RenderHelper
def render_from_helper
- render :partial => 'customer', :collection => @customers
+ render partial: "customer", collection: @customers
end
end
helper RenderHelper
test "helper class that is being tested is always included in view instance" do
- @controller.controller_path = 'test'
+ @controller.controller_path = "test"
- @customers = [stub(:name => 'Eloy'), stub(:name => 'Manfred')]
- assert_match(/Hello: EloyHello: Manfred/, render(:partial => 'test/from_helper'))
+ @customers = [DeveloperStruct.new("Eloy"), DeveloperStruct.new("Manfred")]
+ assert_match(/Hello: EloyHello: Manfred/, render(partial: "test/from_helper"))
end
end
class ControllerHelperMethod < ActionView::TestCase
module SomeHelper
def some_method
- render :partial => 'test/from_helper'
+ render partial: "test/from_helper"
end
end
@@ -141,21 +149,21 @@ module ActionView
EOF
@controller.class.helper_method :render_from_helper
- assert_equal 'controller_helper_method', some_method
+ assert_equal "controller_helper_method", some_method
end
end
class ViewAssignsTest < ActionView::TestCase
test "view_assigns returns a Hash of user defined ivars" do
- @a = 'b'
- @c = 'd'
- assert_equal({:a => 'b', :c => 'd'}, view_assigns)
+ @a = "b"
+ @c = "d"
+ assert_equal({ a: "b", c: "d" }, view_assigns)
end
test "view_assigns excludes internal ivars" do
INTERNAL_IVARS.each do |ivar|
assert defined?(ivar), "expected #{ivar} to be defined"
- assert !view_assigns.keys.include?(ivar.to_s.tr('@', '').to_sym), "expected #{ivar} to be excluded from view_assigns"
+ assert_not_includes view_assigns.keys, ivar.to_s.tr("@", "").to_sym, "expected #{ivar} to be excluded from view_assigns"
end
end
end
@@ -167,10 +175,10 @@ module ActionView
end
end)
test "is able to make methods available to the view" do
- assert_equal 'Word!', render(:partial => 'test/from_helper')
+ assert_equal "Word!", render(partial: "test/from_helper")
end
- def from_test_case; 'Word!'; end
+ def from_test_case; "Word!"; end
helper_method :from_test_case
end
@@ -184,9 +192,8 @@ module ActionView
helper HelperThatInvokesProtectAgainstForgery
test "protect_from_forgery? in any helpers returns false" do
- assert !view.help_me
+ assert_not view.help_me
end
-
end
class ATestHelperTest < ActionView::TestCase
@@ -195,30 +202,36 @@ module ActionView
test "inflects the name of the helper module to test from the test case class" do
assert_equal ATestHelper, test_case.helper_class
- assert test_case.ancestors.include?(ATestHelper)
+ assert_includes test_case.ancestors, ATestHelper
end
test "a configured test controller is available" do
assert_kind_of ActionController::Base, controller
- assert_equal '', controller.controller_path
+ assert_equal "", controller.controller_path
end
test "no additional helpers should shared across test cases" do
- assert !test_case.ancestors.include?(AnotherTestHelper)
+ assert_not_includes test_case.ancestors, AnotherTestHelper
assert_raise(NoMethodError) { send :from_another_helper }
end
test "is able to use routes" do
- controller.request.assign_parameters(@routes, 'foo', 'index')
- assert_equal '/foo', url_for
- assert_equal '/bar', url_for(:controller => 'bar')
+ controller.request.assign_parameters(@routes, "foo", "index", {}, "/foo", [])
+ with_routing do |set|
+ set.draw {
+ get :foo, to: "foo#index"
+ get :bar, to: "bar#index"
+ }
+ assert_equal "/foo", url_for
+ assert_equal "/bar", url_for(controller: "bar")
+ end
end
test "is able to use named routes" do
with_routing do |set|
set.draw { resources :contents }
- assert_equal 'http://test.host/contents/new', new_content_url
- assert_equal 'http://test.host/contents/1', content_url(:id => 1)
+ assert_equal "http://test.host/contents/new", new_content_url
+ assert_equal "http://test.host/contents/1", content_url(id: 1)
end
end
@@ -229,7 +242,7 @@ module ActionView
@routes ||= ActionDispatch::Routing::RouteSet.new
end
- routes.draw { get "bar", :to => lambda {} }
+ routes.draw { get "bar", to: lambda { } }
def self.call(*)
end
@@ -237,7 +250,9 @@ module ActionView
set.draw { mount app => "/foo", :as => "foo_app" }
- assert_equal '/foo/bar', foo_app.bar_path
+ singleton_class.include set.mounted_helpers
+
+ assert_equal "/foo/bar", foo_app.bar_path
end
end
@@ -250,21 +265,21 @@ module ActionView
end
end
- assert_equal 'http://test.host/contents/new', render(:partial => 'test/from_helper')
+ assert_equal "http://test.host/contents/new", render(partial: "test/from_helper")
end
end
test "is able to render partials with local variables" do
- assert_equal 'Eloy', render('developers/developer', :developer => stub(:name => 'Eloy'))
- assert_equal 'Eloy', render(:partial => 'developers/developer',
- :locals => { :developer => stub(:name => 'Eloy') })
+ assert_equal "Eloy", render("developers/developer", developer: DeveloperStruct.new("Eloy"))
+ assert_equal "Eloy", render(partial: "developers/developer",
+ locals: { developer: DeveloperStruct.new("Eloy") })
end
test "is able to render partials from templates and also use instance variables" do
@controller.controller_path = "test"
- @customers = [stub(:name => 'Eloy'), stub(:name => 'Manfred')]
- assert_match(/Hello: EloyHello: Manfred/, render(:file => 'test/list'))
+ @customers = [DeveloperStruct.new("Eloy"), DeveloperStruct.new("Manfred")]
+ assert_match(/Hello: EloyHello: Manfred/, render(file: "test/list"))
end
test "is able to render partials from templates and also use instance variables after view has been referenced" do
@@ -272,37 +287,44 @@ module ActionView
view
- @customers = [stub(:name => 'Eloy'), stub(:name => 'Manfred')]
- assert_match(/Hello: EloyHello: Manfred/, render(:file => 'test/list'))
+ @customers = [DeveloperStruct.new("Eloy"), DeveloperStruct.new("Manfred")]
+ assert_match(/Hello: EloyHello: Manfred/, render(file: "test/list"))
end
+ test "is able to use helpers that depend on the view flow" do
+ assert_not content_for?(:foo)
+
+ content_for :foo, "bar"
+ assert content_for?(:foo)
+ assert_equal "bar", content_for(:foo)
+ end
end
class AssertionsTest < ActionView::TestCase
def render_from_helper
- form_tag('/foo') do
- safe_concat render(:text => '<ul><li>foo</li></ul>')
+ form_tag("/foo") do
+ safe_concat render(plain: "<ul><li>foo</li></ul>")
end
end
helper_method :render_from_helper
test "uses the output_buffer for assert_select" do
- render(:partial => 'test/from_helper')
+ render(partial: "test/from_helper")
- assert_select 'form' do
- assert_select 'li', :text => 'foo'
+ assert_select "form" do
+ assert_select "li", text: "foo"
end
end
test "do not memoize the document_root_element in view tests" do
- concat form_tag('/foo')
+ concat form_tag("/foo")
- assert_select 'form'
+ assert_select "form"
- concat content_tag(:b, 'Strong', class: 'foo')
+ concat content_tag(:b, "Strong", class: "foo")
- assert_select 'form'
- assert_select 'b.foo'
+ assert_select "form"
+ assert_select "b.foo"
end
end
diff --git a/actionview/test/template/test_test.rb b/actionview/test/template/test_test.rb
index 88bac85039..78ba536dfc 100644
--- a/actionview/test/template/test_test.rb
+++ b/actionview/test/template/test_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module PeopleHelper
def title(text)
@@ -41,12 +43,12 @@ class PeopleHelperTest < ActionView::TestCase
extend ActiveModel::Naming
def to_model; self; end
def persisted?; true; end
- def self.name; 'Mocha::Mock'; end
+ def self.name; "Minitest::Mock"; end
}.new "David"
the_model = nil
extend Module.new {
- define_method(:mocha_mock_path) { |model, *args|
+ define_method(:minitest_mock_path) { |model, *args|
the_model = model
"/people/1"
}
@@ -60,7 +62,7 @@ class PeopleHelperTest < ActionView::TestCase
def with_test_route_set
with_routing do |set|
set.draw do
- get 'people', :to => 'people#index', :as => :people
+ get "people", to: "people#index", as: :people
end
yield
end
@@ -84,7 +86,7 @@ class CrazySymbolHelperTest < ActionView::TestCase
end
class CrazyStringHelperTest < ActionView::TestCase
- tests 'people'
+ tests "people"
def test_set_helper_class_using_string
assert_equal PeopleHelper, self.class.helper_class
diff --git a/actionview/test/template/testing/fixture_resolver_test.rb b/actionview/test/template/testing/fixture_resolver_test.rb
index d6cfa997cd..9954e3500d 100644
--- a/actionview/test/template/testing/fixture_resolver_test.rb
+++ b/actionview/test/template/testing/fixture_resolver_test.rb
@@ -1,15 +1,17 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class FixtureResolverTest < ActiveSupport::TestCase
def test_should_return_empty_list_for_unknown_path
resolver = ActionView::FixtureResolver.new()
- templates = resolver.find_all("path", "arbitrary", false, {:locale => [], :formats => [:html], :variants => [], :handlers => []})
+ templates = resolver.find_all("path", "arbitrary", false, locale: [], formats: [:html], variants: [], handlers: [])
assert_equal [], templates, "expected an empty list of templates"
end
def test_should_return_template_for_declared_path
resolver = ActionView::FixtureResolver.new("arbitrary/path.erb" => "this text")
- templates = resolver.find_all("path", "arbitrary", false, {:locale => [], :formats => [:html], :variants => [], :handlers => [:erb]})
+ templates = resolver.find_all("path", "arbitrary", false, locale: [], formats: [:html], variants: [], handlers: [:erb])
assert_equal 1, templates.size, "expected one template"
assert_equal "this text", templates.first.source
assert_equal "arbitrary/path", templates.first.virtual_path
diff --git a/actionview/test/template/testing/null_resolver_test.rb b/actionview/test/template/testing/null_resolver_test.rb
index 55ec36e753..53364c1d90 100644
--- a/actionview/test/template/testing/null_resolver_test.rb
+++ b/actionview/test/template/testing/null_resolver_test.rb
@@ -1,9 +1,11 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class NullResolverTest < ActiveSupport::TestCase
def test_should_return_template_for_any_path
resolver = ActionView::NullResolver.new()
- templates = resolver.find_all("path.erb", "arbitrary", false, {:locale => [], :formats => [:html], :handlers => []})
+ templates = resolver.find_all("path.erb", "arbitrary", false, locale: [], formats: [:html], handlers: [])
assert_equal 1, templates.size, "expected one template"
assert_equal "Template generated by Null Resolver", templates.first.source
assert_equal "arbitrary/path.erb", templates.first.virtual_path.to_s
diff --git a/actionview/test/template/text_helper_test.rb b/actionview/test/template/text_helper_test.rb
index 5791f33069..e961a770e6 100644
--- a/actionview/test/template/text_helper_test.rb
+++ b/actionview/test/template/text_helper_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class TextHelperTest < ActionView::TestCase
tests ActionView::Helpers::TextHelper
@@ -7,22 +9,22 @@ class TextHelperTest < ActionView::TestCase
super
# This simulates the fact that instance variables are reset every time
# a view is rendered. The cycle helper depends on this behavior.
- @_cycles = nil if (defined? @_cycles)
+ @_cycles = nil if defined?(@_cycles)
end
def test_concat
- self.output_buffer = 'foo'
- assert_equal 'foobar', concat('bar')
- assert_equal 'foobar', output_buffer
+ self.output_buffer = +"foo"
+ assert_equal "foobar", concat("bar")
+ assert_equal "foobar", output_buffer
end
def test_simple_format_should_be_html_safe
- assert simple_format("<b> test with html tags </b>").html_safe?
+ assert_predicate simple_format("<b> test with html tags </b>"), :html_safe?
end
def test_simple_format_included_in_isolation
helper_klass = Class.new { include ActionView::Helpers::TextHelper }
- assert helper_klass.new.simple_format("<b> test with html tags </b>").html_safe?
+ assert_predicate helper_klass.new.simple_format("<b> test with html tags </b>"), :html_safe?
end
def test_simple_format
@@ -32,35 +34,35 @@ class TextHelperTest < ActionView::TestCase
assert_equal "<p>A paragraph</p>\n\n<p>and another one!</p>", simple_format("A paragraph\n\nand another one!")
assert_equal "<p>A paragraph\n<br /> With a newline</p>", simple_format("A paragraph\n With a newline")
- text = "A\nB\nC\nD".freeze
+ text = "A\nB\nC\nD"
assert_equal "<p>A\n<br />B\n<br />C\n<br />D</p>", simple_format(text)
- text = "A\r\n \nB\n\n\r\n\t\nC\nD".freeze
+ text = "A\r\n \nB\n\n\r\n\t\nC\nD"
assert_equal "<p>A\n<br /> \n<br />B</p>\n\n<p>\t\n<br />C\n<br />D</p>", simple_format(text)
- assert_equal %q(<p class="test">This is a classy test</p>), simple_format("This is a classy test", :class => 'test')
- assert_equal %Q(<p class="test">para 1</p>\n\n<p class="test">para 2</p>), simple_format("para 1\n\npara 2", :class => 'test')
+ assert_equal '<p class="test">This is a classy test</p>', simple_format("This is a classy test", class: "test")
+ assert_equal %Q(<p class="test">para 1</p>\n\n<p class="test">para 2</p>), simple_format("para 1\n\npara 2", class: "test")
end
def test_simple_format_should_sanitize_input_when_sanitize_option_is_not_false
- assert_equal "<p><b> test with unsafe string </b></p>", simple_format("<b> test with unsafe string </b><script>code!</script>")
+ assert_equal "<p><b> test with unsafe string </b>code!</p>", simple_format("<b> test with unsafe string </b><script>code!</script>")
end
def test_simple_format_should_sanitize_input_when_sanitize_option_is_true
- assert_equal '<p><b> test with unsafe string </b></p>',
- simple_format('<b> test with unsafe string </b><script>code!</script>', {}, sanitize: true)
+ assert_equal "<p><b> test with unsafe string </b>code!</p>",
+ simple_format("<b> test with unsafe string </b><script>code!</script>", {}, { sanitize: true })
end
def test_simple_format_should_not_sanitize_input_when_sanitize_option_is_false
- assert_equal "<p><b> test with unsafe string </b><script>code!</script></p>", simple_format("<b> test with unsafe string </b><script>code!</script>", {}, :sanitize => false)
+ assert_equal "<p><b> test with unsafe string </b><script>code!</script></p>", simple_format("<b> test with unsafe string </b><script>code!</script>", {}, { sanitize: false })
end
def test_simple_format_with_custom_wrapper
- assert_equal "<div></div>", simple_format(nil, {}, :wrapper_tag => "div")
+ assert_equal "<div></div>", simple_format(nil, {}, { wrapper_tag: "div" })
end
def test_simple_format_with_custom_wrapper_and_multi_line_breaks
- assert_equal "<div>We want to put a wrapper...</div>\n\n<div>...right there.</div>", simple_format("We want to put a wrapper...\n\n...right there.", {}, :wrapper_tag => "div")
+ assert_equal "<div>We want to put a wrapper...</div>\n\n<div>...right there.</div>", simple_format("We want to put a wrapper...\n\n...right there.", {}, { wrapper_tag: "div" })
end
def test_simple_format_should_not_change_the_text_passed
@@ -71,22 +73,22 @@ class TextHelperTest < ActionView::TestCase
end
def test_simple_format_does_not_modify_the_html_options_hash
- options = { :class => "foobar"}
+ options = { class: "foobar" }
passed_options = options.dup
simple_format("some text", passed_options)
assert_equal options, passed_options
end
def test_simple_format_does_not_modify_the_options_hash
- options = { :wrapper_tag => :div, :sanitize => false }
+ options = { wrapper_tag: :div, sanitize: false }
passed_options = options.dup
simple_format("some text", {}, passed_options)
assert_equal options, passed_options
end
def test_truncate
- assert_equal "Hello World!", truncate("Hello World!", :length => 12)
- assert_equal "Hello Wor...", truncate("Hello World!!", :length => 12)
+ assert_equal "Hello World!", truncate("Hello World!", length: 12)
+ assert_equal "Hello Wor...", truncate("Hello World!!", length: 12)
end
def test_truncate_should_use_default_length_of_30
@@ -95,21 +97,21 @@ class TextHelperTest < ActionView::TestCase
end
def test_truncate_with_options_hash
- assert_equal "This is a string that wil[...]", truncate("This is a string that will go longer then the default truncate length of 30", :omission => "[...]")
- assert_equal "Hello W...", truncate("Hello World!", :length => 10)
- assert_equal "Hello[...]", truncate("Hello World!", :omission => "[...]", :length => 10)
- assert_equal "Hello[...]", truncate("Hello Big World!", :omission => "[...]", :length => 13, :separator => ' ')
- assert_equal "Hello Big[...]", truncate("Hello Big World!", :omission => "[...]", :length => 14, :separator => ' ')
- assert_equal "Hello Big[...]", truncate("Hello Big World!", :omission => "[...]", :length => 15, :separator => ' ')
+ assert_equal "This is a string that wil[...]", truncate("This is a string that will go longer then the default truncate length of 30", omission: "[...]")
+ assert_equal "Hello W...", truncate("Hello World!", length: 10)
+ assert_equal "Hello[...]", truncate("Hello World!", omission: "[...]", length: 10)
+ assert_equal "Hello[...]", truncate("Hello Big World!", omission: "[...]", length: 13, separator: " ")
+ assert_equal "Hello Big[...]", truncate("Hello Big World!", omission: "[...]", length: 14, separator: " ")
+ assert_equal "Hello Big[...]", truncate("Hello Big World!", omission: "[...]", length: 15, separator: " ")
end
def test_truncate_multibyte
- assert_equal "\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 ...".force_encoding(Encoding::UTF_8),
- truncate("\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 \354\225\204\353\235\274\353\246\254\354\230\244".force_encoding(Encoding::UTF_8), :length => 10)
+ assert_equal (+"\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 ...").force_encoding(Encoding::UTF_8),
+ truncate((+"\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 \354\225\204\353\235\274\353\246\254\354\230\244").force_encoding(Encoding::UTF_8), length: 10)
end
def test_truncate_does_not_modify_the_options_hash
- options = { :length => 10 }
+ options = { length: 10 }
passed_options = options.dup
truncate("some text", passed_options)
assert_equal options, passed_options
@@ -117,53 +119,53 @@ class TextHelperTest < ActionView::TestCase
def test_truncate_with_link_options
assert_equal "Here is a long test and ...<a href=\"#\">Continue</a>",
- truncate("Here is a long test and I need a continue to read link", :length => 27) { link_to 'Continue', '#' }
+ truncate("Here is a long test and I need a continue to read link", length: 27) { link_to "Continue", "#" }
end
def test_truncate_should_be_html_safe
- assert truncate("Hello World!", :length => 12).html_safe?
+ assert_predicate truncate("Hello World!", length: 12), :html_safe?
end
def test_truncate_should_escape_the_input
- assert_equal "Hello &lt;sc...", truncate("Hello <script>code!</script>World!!", :length => 12)
+ assert_equal "Hello &lt;sc...", truncate("Hello <script>code!</script>World!!", length: 12)
end
def test_truncate_should_not_escape_the_input_with_escape_false
- assert_equal "Hello <sc...", truncate("Hello <script>code!</script>World!!", :length => 12, :escape => false)
+ assert_equal "Hello <sc...", truncate("Hello <script>code!</script>World!!", length: 12, escape: false)
end
def test_truncate_with_escape_false_should_be_html_safe
- truncated = truncate("Hello <script>code!</script>World!!", :length => 12, :escape => false)
- assert truncated.html_safe?
+ truncated = truncate("Hello <script>code!</script>World!!", length: 12, escape: false)
+ assert_predicate truncated, :html_safe?
end
def test_truncate_with_block_should_be_html_safe
- truncated = truncate("Here's a long test and I need a continue to read link", :length => 27) { link_to 'Continue', '#' }
- assert truncated.html_safe?
+ truncated = truncate("Here's a long test and I need a continue to read link", length: 27) { link_to "Continue", "#" }
+ assert_predicate truncated, :html_safe?
end
def test_truncate_with_block_should_escape_the_input
assert_equal "&lt;script&gt;code!&lt;/script&gt;He...<a href=\"#\">Continue</a>",
- truncate("<script>code!</script>Here's a long test and I need a continue to read link", :length => 27) { link_to 'Continue', '#' }
+ truncate("<script>code!</script>Here's a long test and I need a continue to read link", length: 27) { link_to "Continue", "#" }
end
def test_truncate_with_block_should_not_escape_the_input_with_escape_false
assert_equal "<script>code!</script>He...<a href=\"#\">Continue</a>",
- truncate("<script>code!</script>Here's a long test and I need a continue to read link", :length => 27, :escape => false) { link_to 'Continue', '#' }
+ truncate("<script>code!</script>Here's a long test and I need a continue to read link", length: 27, escape: false) { link_to "Continue", "#" }
end
def test_truncate_with_block_with_escape_false_should_be_html_safe
- truncated = truncate("<script>code!</script>Here's a long test and I need a continue to read link", :length => 27, :escape => false) { link_to 'Continue', '#' }
- assert truncated.html_safe?
+ truncated = truncate("<script>code!</script>Here's a long test and I need a continue to read link", length: 27, escape: false) { link_to "Continue", "#" }
+ assert_predicate truncated, :html_safe?
end
def test_truncate_with_block_should_escape_the_block
assert_equal "Here is a long test and ...&lt;script&gt;alert(&#39;foo&#39;);&lt;/script&gt;",
- truncate("Here is a long test and I need a continue to read link", :length => 27) { "<script>alert('foo');</script>" }
+ truncate("Here is a long test and I need a continue to read link", length: 27) { "<script>alert('foo');</script>" }
end
def test_highlight_should_be_html_safe
- assert highlight("This is a beautiful morning", "beautiful").html_safe?
+ assert_predicate highlight("This is a beautiful morning", "beautiful"), :html_safe?
end
def test_highlight
@@ -179,7 +181,7 @@ class TextHelperTest < ActionView::TestCase
assert_equal(
"This is a <b>beautiful</b> morning, but also a <b>beautiful</b> day",
- highlight("This is a beautiful morning, but also a beautiful day", "beautiful", :highlighter => '<b>\1</b>')
+ highlight("This is a beautiful morning, but also a beautiful day", "beautiful", highlighter: '<b>\1</b>')
)
assert_equal(
@@ -189,16 +191,16 @@ class TextHelperTest < ActionView::TestCase
end
def test_highlight_pending
- assert_equal ' ', highlight(' ', 'blank text is returned verbatim')
+ assert_equal " ", highlight(" ", "blank text is returned verbatim")
end
def test_highlight_should_return_blank_string_for_nil
- assert_equal '', highlight(nil, 'blank string is returned for nil')
+ assert_equal "", highlight(nil, "blank string is returned for nil")
end
def test_highlight_should_sanitize_input
assert_equal(
- "This is a <mark>beautiful</mark> morning",
+ "This is a <mark>beautiful</mark> morningcode!",
highlight("This is a beautiful morning<script>code!</script>", "beautiful")
)
end
@@ -206,7 +208,7 @@ class TextHelperTest < ActionView::TestCase
def test_highlight_should_not_sanitize_if_sanitize_option_if_false
assert_equal(
"This is a <mark>beautiful</mark> morning<script>code!</script>",
- highlight("This is a beautiful morning<script>code!</script>", "beautiful", :sanitize => false)
+ highlight("This is a beautiful morning<script>code!</script>", "beautiful", sanitize: false)
)
end
@@ -233,7 +235,7 @@ class TextHelperTest < ActionView::TestCase
end
def test_highlight_with_multiple_phrases_in_one_pass
- assert_equal %(<em>wow</em> <em>em</em>), highlight('wow em', %w(wow em), :highlighter => '<em>\1</em>')
+ assert_equal %(<em>wow</em> <em>em</em>), highlight("wow em", %w(wow em), highlighter: '<em>\1</em>')
end
def test_highlight_with_html
@@ -259,12 +261,12 @@ class TextHelperTest < ActionView::TestCase
)
assert_equal(
"<div>abc <b>div</b></div>",
- highlight("<div>abc div</div>", "div", :highlighter => '<b>\1</b>')
+ highlight("<div>abc div</div>", "div", highlighter: '<b>\1</b>')
)
end
def test_highlight_does_not_modify_the_options_hash
- options = { :highlighter => '<b>\1</b>', :sanitize => false }
+ options = { highlighter: '<b>\1</b>', sanitize: false }
passed_options = options.dup
highlight("<div>abc div</div>", "div", passed_options)
assert_equal options, passed_options
@@ -278,121 +280,137 @@ class TextHelperTest < ActionView::TestCase
end
def test_excerpt
- assert_equal("...is a beautiful morn...", excerpt("This is a beautiful morning", "beautiful", :radius => 5))
- assert_equal("This is a...", excerpt("This is a beautiful morning", "this", :radius => 5))
- assert_equal("...iful morning", excerpt("This is a beautiful morning", "morning", :radius => 5))
+ assert_equal("...is a beautiful morn...", excerpt("This is a beautiful morning", "beautiful", radius: 5))
+ assert_equal("This is a...", excerpt("This is a beautiful morning", "this", radius: 5))
+ assert_equal("...iful morning", excerpt("This is a beautiful morning", "morning", radius: 5))
assert_nil excerpt("This is a beautiful morning", "day")
end
def test_excerpt_with_regex
- assert_equal('...is a beautiful! mor...', excerpt('This is a beautiful! morning', 'beautiful', :radius => 5))
- assert_equal('...is a beautiful? mor...', excerpt('This is a beautiful? morning', 'beautiful', :radius => 5))
- assert_equal('...is a beautiful? mor...', excerpt('This is a beautiful? morning', /\bbeau\w*\b/i, :radius => 5))
- assert_equal('...is a beautiful? mor...', excerpt('This is a beautiful? morning', /\b(beau\w*)\b/i, :radius => 5))
- assert_equal("...udge Allen and...", excerpt("This day was challenging for judge Allen and his colleagues.", /\ballen\b/i, :radius => 5))
- assert_equal("...judge Allen and...", excerpt("This day was challenging for judge Allen and his colleagues.", /\ballen\b/i, :radius => 1, :separator => ' '))
- assert_equal("...was challenging for...", excerpt("This day was challenging for judge Allen and his colleagues.", /\b(\w*allen\w*)\b/i, :radius => 5))
+ assert_equal("...is a beautiful! mor...", excerpt("This is a beautiful! morning", "beautiful", radius: 5))
+ assert_equal("...is a beautiful? mor...", excerpt("This is a beautiful? morning", "beautiful", radius: 5))
+ assert_equal("...is a beautiful? mor...", excerpt("This is a beautiful? morning", /\bbeau\w*\b/i, radius: 5))
+ assert_equal("...is a beautiful? mor...", excerpt("This is a beautiful? morning", /\b(beau\w*)\b/i, radius: 5))
+ assert_equal("...udge Allen and...", excerpt("This day was challenging for judge Allen and his colleagues.", /\ballen\b/i, radius: 5))
+ assert_equal("...judge Allen and...", excerpt("This day was challenging for judge Allen and his colleagues.", /\ballen\b/i, radius: 1, separator: " "))
+ assert_equal("...was challenging for...", excerpt("This day was challenging for judge Allen and his colleagues.", /\b(\w*allen\w*)\b/i, radius: 5))
end
def test_excerpt_should_not_be_html_safe
- assert !excerpt('This is a beautiful! morning', 'beautiful', :radius => 5).html_safe?
+ assert_not_predicate excerpt("This is a beautiful! morning", "beautiful", radius: 5), :html_safe?
end
def test_excerpt_in_borderline_cases
- assert_equal("", excerpt("", "", :radius => 0))
- assert_equal("a", excerpt("a", "a", :radius => 0))
- assert_equal("...b...", excerpt("abc", "b", :radius => 0))
- assert_equal("abc", excerpt("abc", "b", :radius => 1))
- assert_equal("abc...", excerpt("abcd", "b", :radius => 1))
- assert_equal("...abc", excerpt("zabc", "b", :radius => 1))
- assert_equal("...abc...", excerpt("zabcd", "b", :radius => 1))
- assert_equal("zabcd", excerpt("zabcd", "b", :radius => 2))
+ assert_equal("", excerpt("", "", radius: 0))
+ assert_equal("a", excerpt("a", "a", radius: 0))
+ assert_equal("...b...", excerpt("abc", "b", radius: 0))
+ assert_equal("abc", excerpt("abc", "b", radius: 1))
+ assert_equal("abc...", excerpt("abcd", "b", radius: 1))
+ assert_equal("...abc", excerpt("zabc", "b", radius: 1))
+ assert_equal("...abc...", excerpt("zabcd", "b", radius: 1))
+ assert_equal("zabcd", excerpt("zabcd", "b", radius: 2))
# excerpt strips the resulting string before ap-/prepending excerpt_string.
# whether this behavior is meaningful when excerpt_string is not to be
# appended is questionable.
- assert_equal("zabcd", excerpt(" zabcd ", "b", :radius => 4))
- assert_equal("...abc...", excerpt("z abc d", "b", :radius => 1))
+ assert_equal("zabcd", excerpt(" zabcd ", "b", radius: 4))
+ assert_equal("...abc...", excerpt("z abc d", "b", radius: 1))
end
def test_excerpt_with_omission
- assert_equal("[...]is a beautiful morn[...]", excerpt("This is a beautiful morning", "beautiful", :omission => "[...]",:radius => 5))
+ assert_equal("[...]is a beautiful morn[...]", excerpt("This is a beautiful morning", "beautiful", omission: "[...]", radius: 5))
assert_equal(
"This is the ultimate supercalifragilisticexpialidoceous very looooooooooooooooooong looooooooooooong beautiful morning with amazing sunshine and awesome tempera[...]",
excerpt("This is the ultimate supercalifragilisticexpialidoceous very looooooooooooooooooong looooooooooooong beautiful morning with amazing sunshine and awesome temperatures. So what are you gonna do about it?", "very",
- :omission => "[...]")
+ omission: "[...]")
)
end
def test_excerpt_with_utf8
- assert_equal("...\357\254\203ciency could not be...".force_encoding(Encoding::UTF_8), excerpt("That's why e\357\254\203ciency could not be helped".force_encoding(Encoding::UTF_8), 'could', :radius => 8))
+ assert_equal((+"...\357\254\203ciency could not be...").force_encoding(Encoding::UTF_8), excerpt((+"That's why e\357\254\203ciency could not be helped").force_encoding(Encoding::UTF_8), "could", radius: 8))
end
def test_excerpt_does_not_modify_the_options_hash
- options = { :omission => "[...]",:radius => 5 }
+ options = { omission: "[...]", radius: 5 }
passed_options = options.dup
excerpt("This is a beautiful morning", "beautiful", passed_options)
assert_equal options, passed_options
end
def test_excerpt_with_separator
- options = { :separator => ' ', :radius => 1 }
- assert_equal('...a very beautiful...', excerpt('This is a very beautiful morning', 'very', options))
- assert_equal('This is...', excerpt('This is a very beautiful morning', 'this', options))
- assert_equal('...beautiful morning', excerpt('This is a very beautiful morning', 'morning', options))
+ options = { separator: " ", radius: 1 }
+ assert_equal("...a very beautiful...", excerpt("This is a very beautiful morning", "very", options))
+ assert_equal("This is...", excerpt("This is a very beautiful morning", "this", options))
+ assert_equal("...beautiful morning", excerpt("This is a very beautiful morning", "morning", options))
- options = { :separator => "\n", :radius => 0 }
- assert_equal("...very long...", excerpt("my very\nvery\nvery long\nstring", 'long', options))
+ options = { separator: "\n", radius: 0 }
+ assert_equal("...very long...", excerpt("my very\nvery\nvery long\nstring", "long", options))
- options = { :separator => "\n", :radius => 1 }
- assert_equal("...very\nvery long\nstring", excerpt("my very\nvery\nvery long\nstring", 'long', options))
+ options = { separator: "\n", radius: 1 }
+ assert_equal("...very\nvery long\nstring", excerpt("my very\nvery\nvery long\nstring", "long", options))
- assert_equal excerpt('This is a beautiful morning', 'a'),
- excerpt('This is a beautiful morning', 'a', separator: nil)
+ assert_equal excerpt("This is a beautiful morning", "a"),
+ excerpt("This is a beautiful morning", "a", separator: nil)
end
def test_word_wrap
- assert_equal("my very very\nvery long\nstring", word_wrap("my very very very long string", :line_width => 15))
+ assert_equal("my very very\nvery long\nstring", word_wrap("my very very very long string", line_width: 15))
end
def test_word_wrap_with_extra_newlines
- assert_equal("my very very\nvery long\nstring\n\nwith another\nline", word_wrap("my very very very long string\n\nwith another line", :line_width => 15))
+ assert_equal("my very very\nvery long\nstring\n\nwith another\nline", word_wrap("my very very very long string\n\nwith another line", line_width: 15))
+ end
+
+ def test_word_wrap_with_leading_spaces
+ assert_equal(" This is a paragraph\nthat includes some\nindented lines:\n Like this sample\n blockquote", word_wrap(" This is a paragraph that includes some\nindented lines:\n Like this sample\n blockquote", line_width: 25))
end
def test_word_wrap_does_not_modify_the_options_hash
- options = { :line_width => 15 }
+ options = { line_width: 15 }
passed_options = options.dup
word_wrap("some text", passed_options)
assert_equal options, passed_options
end
+ def test_word_wrap_with_custom_break_sequence
+ assert_equal("1234567890\r\n1234567890\r\n1234567890", word_wrap("1234567890 " * 3, line_width: 2, break_sequence: "\r\n"))
+ end
+
def test_pluralization
assert_equal("1 count", pluralize(1, "count"))
assert_equal("2 counts", pluralize(2, "count"))
- assert_equal("1 count", pluralize('1', "count"))
- assert_equal("2 counts", pluralize('2', "count"))
- assert_equal("1,066 counts", pluralize('1,066', "count"))
- assert_equal("1.25 counts", pluralize('1.25', "count"))
- assert_equal("1.0 count", pluralize('1.0', "count"))
- assert_equal("1.00 count", pluralize('1.00', "count"))
+ assert_equal("1 count", pluralize("1", "count"))
+ assert_equal("2 counts", pluralize("2", "count"))
+ assert_equal("1,066 counts", pluralize("1,066", "count"))
+ assert_equal("1.25 counts", pluralize("1.25", "count"))
+ assert_equal("1.0 count", pluralize("1.0", "count"))
+ assert_equal("1.00 count", pluralize("1.00", "count"))
assert_equal("2 counters", pluralize(2, "count", "counters"))
assert_equal("0 counters", pluralize(nil, "count", "counters"))
+ assert_equal("2 counters", pluralize(2, "count", plural: "counters"))
+ assert_equal("0 counters", pluralize(nil, "count", plural: "counters"))
assert_equal("2 people", pluralize(2, "person"))
assert_equal("10 buffaloes", pluralize(10, "buffalo"))
assert_equal("1 berry", pluralize(1, "berry"))
assert_equal("12 berries", pluralize(12, "berry"))
end
- def test_pluralization_with_locale
- ActiveSupport::Inflector.inflections(:de) do |inflect|
- inflect.plural(/(person)$/i, '\1en')
- inflect.singular(/(person)en$/i, '\1')
- end
+ def test_localized_pluralization
+ old_locale = I18n.locale
- assert_equal("2 People", pluralize(2, "Person", locale: :en))
- assert_equal("2 Personen", pluralize(2, "Person", locale: :de))
+ begin
+ I18n.locale = :de
- ActiveSupport::Inflector.inflections(:de).clear
+ ActiveSupport::Inflector.inflections(:de) do |inflect|
+ inflect.irregular "region", "regionen"
+ end
+
+ assert_equal("1 region", pluralize(1, "region"))
+ assert_equal("2 regionen", pluralize(2, "region"))
+ assert_equal("2 regions", pluralize(2, "region", locale: :en))
+ ensure
+ I18n.locale = old_locale
+ end
end
def test_cycle_class
@@ -442,29 +460,29 @@ class TextHelperTest < ActionView::TestCase
end
def test_named_cycles
- assert_equal("1", cycle(1, 2, 3, :name => "numbers"))
- assert_equal("red", cycle("red", "blue", :name => "colors"))
- assert_equal("2", cycle(1, 2, 3, :name => "numbers"))
- assert_equal("blue", cycle("red", "blue", :name => "colors"))
- assert_equal("3", cycle(1, 2, 3, :name => "numbers"))
- assert_equal("red", cycle("red", "blue", :name => "colors"))
+ assert_equal("1", cycle(1, 2, 3, name: "numbers"))
+ assert_equal("red", cycle("red", "blue", name: "colors"))
+ assert_equal("2", cycle(1, 2, 3, name: "numbers"))
+ assert_equal("blue", cycle("red", "blue", name: "colors"))
+ assert_equal("3", cycle(1, 2, 3, name: "numbers"))
+ assert_equal("red", cycle("red", "blue", name: "colors"))
end
def test_current_cycle_with_default_name
- cycle("even","odd")
+ cycle("even", "odd")
assert_equal "even", current_cycle
- cycle("even","odd")
+ cycle("even", "odd")
assert_equal "odd", current_cycle
- cycle("even","odd")
+ cycle("even", "odd")
assert_equal "even", current_cycle
end
def test_current_cycle_with_named_cycles
- cycle("red", "blue", :name => "colors")
+ cycle("red", "blue", name: "colors")
assert_equal "red", current_cycle("colors")
- cycle("red", "blue", :name => "colors")
+ cycle("red", "blue", name: "colors")
assert_equal "blue", current_cycle("colors")
- cycle("red", "blue", :name => "colors")
+ cycle("red", "blue", name: "colors")
assert_equal "red", current_cycle("colors")
end
@@ -474,19 +492,19 @@ class TextHelperTest < ActionView::TestCase
end
def test_current_cycle_with_more_than_two_names
- cycle(1,2,3)
+ cycle(1, 2, 3)
assert_equal "1", current_cycle
- cycle(1,2,3)
+ cycle(1, 2, 3)
assert_equal "2", current_cycle
- cycle(1,2,3)
+ cycle(1, 2, 3)
assert_equal "3", current_cycle
- cycle(1,2,3)
+ cycle(1, 2, 3)
assert_equal "1", current_cycle
end
def test_default_named_cycle
assert_equal("1", cycle(1, 2, 3))
- assert_equal("2", cycle(1, 2, 3, :name => "default"))
+ assert_equal("2", cycle(1, 2, 3, name: "default"))
assert_equal("3", cycle(1, 2, 3))
end
@@ -502,13 +520,13 @@ class TextHelperTest < ActionView::TestCase
end
def test_reset_named_cycle
- assert_equal("1", cycle(1, 2, 3, :name => "numbers"))
- assert_equal("red", cycle("red", "blue", :name => "colors"))
+ assert_equal("1", cycle(1, 2, 3, name: "numbers"))
+ assert_equal("red", cycle("red", "blue", name: "colors"))
reset_cycle("numbers")
- assert_equal("1", cycle(1, 2, 3, :name => "numbers"))
- assert_equal("blue", cycle("red", "blue", :name => "colors"))
- assert_equal("2", cycle(1, 2, 3, :name => "numbers"))
- assert_equal("red", cycle("red", "blue", :name => "colors"))
+ assert_equal("1", cycle(1, 2, 3, name: "numbers"))
+ assert_equal("blue", cycle("red", "blue", name: "colors"))
+ assert_equal("2", cycle(1, 2, 3, name: "numbers"))
+ assert_equal("red", cycle("red", "blue", name: "colors"))
end
def test_cycle_no_instance_variable_clashes
diff --git a/actionview/test/template/text_test.rb b/actionview/test/template/text_test.rb
index d899d54589..0c6470df21 100644
--- a/actionview/test/template/text_test.rb
+++ b/actionview/test/template/text_test.rb
@@ -1,17 +1,25 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+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 "identifier should return 'text template'" do
+ assert_equal "text template", ActionView::Template::Text.new("").identifier
+ end
+
+ test "inspect should return 'text template'" do
+ assert_equal "text template", ActionView::Template::Text.new("").inspect
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 "to_str should return a given string" do
+ assert_equal "a cat", ActionView::Template::Text.new("a cat").to_str
end
- test 'formats returns string for unknown MIME type' do
- assert_equal ['foo'], ActionView::Template::Text.new('', 'foo').formats
+ 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/translation_helper_test.rb b/actionview/test/template/translation_helper_test.rb
index 5dc281adb2..e756348938 100644
--- a/actionview/test/template/translation_helper_test.rb
+++ b/actionview/test/template/translation_helper_test.rb
@@ -1,9 +1,11 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module I18n
class CustomExceptionHandler
def self.call(exception, locale, key, options)
- 'from CustomExceptionHandler'
+ "from CustomExceptionHandler"
end
end
end
@@ -15,22 +17,22 @@ class TranslationHelperTest < ActiveSupport::TestCase
setup do
I18n.backend.store_translations(:en,
- :translations => {
- :templates => {
- :found => { :foo => 'Foo' },
- :array => { :foo => { :bar => 'Foo Bar' } },
- :default => { :foo => 'Foo' }
+ translations: {
+ templates: {
+ found: { foo: "Foo" },
+ array: { foo: { bar: "Foo Bar" } },
+ default: { foo: "Foo" }
},
- :foo => 'Foo',
- :hello => '<a>Hello World</a>',
- :html => '<a>Hello World</a>',
- :hello_html => '<a>Hello World</a>',
- :interpolated_html => '<a>Hello %{word}</a>',
- :array_html => %w(foo bar),
- :array => %w(foo bar),
- :count_html => {
- :one => '<a>One %{count}</a>',
- :other => '<a>Other %{count}</a>'
+ foo: "Foo",
+ hello: "<a>Hello World</a>",
+ html: "<a>Hello World</a>",
+ hello_html: "<a>Hello World</a>",
+ interpolated_html: "<a>Hello %{word}</a>",
+ array_html: %w(foo bar),
+ array: %w(foo bar),
+ count_html: {
+ one: "<a>One %{count}</a>",
+ other: "<a>Other %{count}</a>"
}
}
)
@@ -42,14 +44,26 @@ class TranslationHelperTest < ActiveSupport::TestCase
end
def test_delegates_setting_to_i18n
- I18n.expects(:translate).with(:foo, :locale => 'en', :raise => true).returns("")
- translate :foo, :locale => 'en'
+ assert_called_with(I18n, :translate, [:foo, locale: "en", raise: true], returns: "") do
+ translate :foo, locale: "en"
+ end
end
def test_delegates_localize_to_i18n
@time = Time.utc(2008, 7, 8, 12, 18, 38)
- I18n.expects(:localize).with(@time)
- localize @time
+ assert_called_with(I18n, :localize, [@time]) do
+ localize @time
+ end
+ end
+
+ def test_returns_missing_translation_message_without_span_wrap
+ old_value = ActionView::Base.debug_missing_translation
+ ActionView::Base.debug_missing_translation = false
+
+ expected = "translation missing: en.translations.missing"
+ assert_equal expected, translate(:"translations.missing")
+ ensure
+ ActionView::Base.debug_missing_translation = old_value
end
def test_returns_missing_translation_message_wrapped_into_span
@@ -58,6 +72,20 @@ class TranslationHelperTest < ActiveSupport::TestCase
assert_equal true, translate(:"translations.missing").html_safe?
end
+ def test_returns_missing_translation_message_with_unescaped_interpolation
+ expected = '<span class="translation_missing" title="translation missing: en.translations.missing, name: Kir, year: 2015, vulnerable: &amp;quot; onclick=&amp;quot;alert()&amp;quot;">Missing</span>'
+ assert_equal expected, translate(:"translations.missing", name: "Kir", year: "2015", vulnerable: %{" onclick="alert()"})
+ assert_predicate translate(:"translations.missing"), :html_safe?
+ end
+
+ def test_returns_missing_translation_message_does_filters_out_i18n_options
+ expected = '<span class="translation_missing" title="translation missing: en.translations.missing, year: 2015">Missing</span>'
+ assert_equal expected, translate(:"translations.missing", year: "2015", default: [])
+
+ expected = '<span class="translation_missing" title="translation missing: en.scoped.translations.missing, year: 2015">Missing</span>'
+ assert_equal expected, translate(:"translations.missing", year: "2015", scope: %i(scoped))
+ end
+
def test_raises_missing_translation_message_with_raise_config_option
ActionView::Base.raise_on_missing_translations = true
@@ -70,14 +98,14 @@ class TranslationHelperTest < ActiveSupport::TestCase
def test_raises_missing_translation_message_with_raise_option
assert_raise(I18n::MissingTranslationData) do
- translate(:"translations.missing", :raise => true)
+ translate(:"translations.missing", raise: true)
end
end
def test_uses_custom_exception_handler_when_specified
old_exception_handler = I18n.exception_handler
I18n.exception_handler = I18n::CustomExceptionHandler
- assert_equal 'from CustomExceptionHandler', translate(:"translations.missing", raise: false)
+ assert_equal "from CustomExceptionHandler", translate(:"translations.missing", raise: false)
ensure
I18n.exception_handler = old_exception_handler
end
@@ -85,7 +113,7 @@ class TranslationHelperTest < ActiveSupport::TestCase
def test_uses_custom_exception_handler_when_specified_for_html
old_exception_handler = I18n.exception_handler
I18n.exception_handler = I18n::CustomExceptionHandler
- assert_equal 'from CustomExceptionHandler', translate(:"translations.missing_html", raise: false)
+ assert_equal "from CustomExceptionHandler", translate(:"translations.missing_html", raise: false)
ensure
I18n.exception_handler = old_exception_handler
end
@@ -96,20 +124,20 @@ class TranslationHelperTest < ActiveSupport::TestCase
end
def test_finds_translation_scoped_by_partial
- assert_equal 'Foo', view.render(:file => 'translations/templates/found').strip
+ assert_equal "Foo", view.render(file: "translations/templates/found").strip
end
def test_finds_array_of_translations_scoped_by_partial
- assert_equal 'Foo Bar', @view.render(:file => 'translations/templates/array').strip
+ assert_equal "Foo Bar", @view.render(file: "translations/templates/array").strip
end
def test_default_lookup_scoped_by_partial
- assert_equal 'Foo', view.render(:file => 'translations/templates/default').strip
+ assert_equal "Foo", view.render(file: "translations/templates/default").strip
end
def test_missing_translation_scoped_by_partial
expected = '<span class="translation_missing" title="translation missing: en.translations.templates.missing.missing">Missing</span>'
- assert_equal expected, view.render(:file => 'translations/templates/missing').strip
+ assert_equal expected, view.render(file: "translations/templates/missing").strip
end
def test_translate_does_not_mark_plain_text_as_safe_html
@@ -117,36 +145,40 @@ class TranslationHelperTest < ActiveSupport::TestCase
end
def test_translate_marks_translations_named_html_as_safe_html
- assert translate(:'translations.html').html_safe?
+ assert_predicate translate(:'translations.html'), :html_safe?
end
def test_translate_marks_translations_with_a_html_suffix_as_safe_html
- assert translate(:'translations.hello_html').html_safe?
+ assert_predicate translate(:'translations.hello_html'), :html_safe?
end
def test_translate_escapes_interpolations_in_translations_with_a_html_suffix
- assert_equal '<a>Hello &lt;World&gt;</a>', translate(:'translations.interpolated_html', :word => '<World>')
- assert_equal '<a>Hello &lt;World&gt;</a>', translate(:'translations.interpolated_html', :word => stub(:to_s => "<World>"))
+ word_struct = Struct.new(:to_s)
+ assert_equal "<a>Hello &lt;World&gt;</a>", translate(:'translations.interpolated_html', word: "<World>")
+ assert_equal "<a>Hello &lt;World&gt;</a>", translate(:'translations.interpolated_html', word: word_struct.new("<World>"))
end
def test_translate_with_html_count
- assert_equal '<a>One 1</a>', translate(:'translations.count_html', :count => 1)
- assert_equal '<a>Other 2</a>', translate(:'translations.count_html', :count => 2)
- assert_equal '<a>Other &lt;One&gt;</a>', translate(:'translations.count_html', :count => '<One>')
+ assert_equal "<a>One 1</a>", translate(:'translations.count_html', count: 1)
+ assert_equal "<a>Other 2</a>", translate(:'translations.count_html', count: 2)
+ assert_equal "<a>Other &lt;One&gt;</a>", translate(:'translations.count_html', count: "<One>")
end
- def test_translation_returning_an_array_ignores_html_suffix
- assert_equal ["foo", "bar"], translate(:'translations.array_html')
+ def test_translate_marks_array_of_translations_with_a_html_safe_suffix_as_safe_html
+ translate(:'translations.array_html').tap do |translated|
+ assert_equal %w( foo bar ), translated
+ assert translated.all?(&:html_safe?)
+ end
end
def test_translate_with_default_named_html
- translation = translate(:'translations.missing', :default => :'translations.hello_html')
- assert_equal '<a>Hello World</a>', translation
+ translation = translate(:'translations.missing', default: :'translations.hello_html')
+ assert_equal "<a>Hello World</a>", translation
assert_equal true, translation.html_safe?
end
def test_translate_with_missing_default
- translation = translate(:'translations.missing', :default => :'translations.missing_html')
+ translation = translate(:'translations.missing', default: :'translations.missing_html')
expected = '<span class="translation_missing" title="translation missing: en.translations.missing_html">Missing Html</span>'
assert_equal expected, translation
assert_equal true, translation.html_safe?
@@ -154,31 +186,31 @@ class TranslationHelperTest < ActiveSupport::TestCase
def test_translate_with_missing_default_and_raise_option
assert_raise(I18n::MissingTranslationData) do
- translate(:'translations.missing', :default => :'translations.missing_html', :raise => true)
+ translate(:'translations.missing', default: :'translations.missing_html', raise: true)
end
end
def test_translate_with_two_defaults_named_html
- translation = translate(:'translations.missing', :default => [:'translations.missing_html', :'translations.hello_html'])
- assert_equal '<a>Hello World</a>', translation
+ translation = translate(:'translations.missing', default: [:'translations.missing_html', :'translations.hello_html'])
+ assert_equal "<a>Hello World</a>", translation
assert_equal true, translation.html_safe?
end
def test_translate_with_last_default_named_html
- translation = translate(:'translations.missing', :default => [:'translations.missing', :'translations.hello_html'])
- assert_equal '<a>Hello World</a>', translation
+ translation = translate(:'translations.missing', default: [:'translations.missing', :'translations.hello_html'])
+ assert_equal "<a>Hello World</a>", translation
assert_equal true, translation.html_safe?
end
def test_translate_with_last_default_not_named_html
- translation = translate(:'translations.missing', :default => [:'translations.missing_html', :'translations.foo'])
- assert_equal 'Foo', translation
+ translation = translate(:'translations.missing', default: [:'translations.missing_html', :'translations.foo'])
+ assert_equal "Foo", translation
assert_equal false, translation.html_safe?
end
def test_translate_with_string_default
- translation = translate(:'translations.missing', default: 'A Generic String')
- assert_equal 'A Generic String', translation
+ translation = translate(:'translations.missing', default: "A Generic String")
+ assert_equal "A Generic String", translation
end
def test_translate_with_object_default
@@ -187,13 +219,13 @@ class TranslationHelperTest < ActiveSupport::TestCase
end
def test_translate_with_array_of_string_defaults
- translation = translate(:'translations.missing', default: ['A Generic String', 'Second generic string'])
- assert_equal 'A Generic String', translation
+ translation = translate(:'translations.missing', default: ["A Generic String", "Second generic string"])
+ assert_equal "A Generic String", translation
end
def test_translate_with_array_of_defaults_with_nil
- translation = translate(:'translations.missing', default: [:'also_missing', nil, 'A Generic String'])
- assert_equal 'A Generic String', translation
+ translation = translate(:'translations.missing', default: [:'also_missing', nil, "A Generic String"])
+ assert_equal "A Generic String", translation
end
def test_translate_with_array_of_array_default
diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb
index 0e35c67516..1ab28e4749 100644
--- a/actionview/test/template/url_helper_test.rb
+++ b/actionview/test/template/url_helper_test.rb
@@ -1,16 +1,15 @@
-require 'abstract_unit'
-require 'minitest/mock'
+# frozen_string_literal: true
-class UrlHelperTest < ActiveSupport::TestCase
+require "abstract_unit"
+class UrlHelperTest < ActiveSupport::TestCase
# In a few cases, the helper proxies to 'controller'
# or request.
#
# In those cases, we'll set up a simple mock
attr_accessor :controller, :request
- cattr_accessor :request_forgery
- self.request_forgery = false
+ cattr_accessor :request_forgery, default: false
routes = ActionDispatch::Routing::RouteSet.new
routes.draw do
@@ -18,6 +17,10 @@ class UrlHelperTest < ActiveSupport::TestCase
get "/other" => "foo#other"
get "/article/:id" => "foo#article", :as => :article
get "/category/:category" => "foo#category"
+
+ scope :engine do
+ get "/" => "foo#bar"
+ end
end
include ActionView::Helpers::UrlHelper
@@ -39,16 +42,81 @@ class UrlHelperTest < ActiveSupport::TestCase
assert_equal "/?a=b&c=d", url_for(hash_for(a: :b, c: :d))
end
+ def test_url_for_does_not_include_empty_hashes
+ assert_equal "/", url_for(hash_for(a: {}))
+ end
+
def test_url_for_with_back
- referer = 'http://www.example.com/referer'
+ referer = "http://www.example.com/referer"
@controller = Struct.new(:request).new(Struct.new(:env).new("HTTP_REFERER" => referer))
- assert_equal 'http://www.example.com/referer', url_for(:back)
+ assert_equal "http://www.example.com/referer", url_for(:back)
end
def test_url_for_with_back_and_no_referer
@controller = Struct.new(:request).new(Struct.new(:env).new({}))
- assert_equal 'javascript:history.back()', url_for(:back)
+ assert_equal "javascript:history.back()", url_for(:back)
+ end
+
+ def test_url_for_with_back_and_no_controller
+ @controller = nil
+ assert_equal "javascript:history.back()", url_for(:back)
+ end
+
+ def test_url_for_with_back_and_javascript_referer
+ referer = "javascript:alert(document.cookie)"
+ @controller = Struct.new(:request).new(Struct.new(:env).new("HTTP_REFERER" => referer))
+ assert_equal "javascript:history.back()", url_for(:back)
+ end
+
+ def test_url_for_with_invalid_referer
+ referer = "THIS IS NOT A URL"
+ @controller = Struct.new(:request).new(Struct.new(:env).new("HTTP_REFERER" => referer))
+ assert_equal "javascript:history.back()", url_for(:back)
+ end
+
+ def test_url_for_with_array_defaults_to_only_path_true
+ assert_equal "/other", url_for([:other, { controller: "foo" }])
+ end
+
+ def test_url_for_with_array_and_only_path_set_to_false
+ default_url_options[:host] = "http://example.com"
+ assert_equal "http://example.com/other", url_for([:other, { controller: "foo", only_path: false }])
+ end
+
+ def test_to_form_params_with_hash
+ assert_equal(
+ [{ name: "name", value: "David" }, { name: "nationality", value: "Danish" }],
+ to_form_params(name: "David", nationality: "Danish")
+ )
+ end
+
+ def test_to_form_params_with_hash_having_symbol_and_string_keys
+ assert_equal(
+ [{ name: "name", value: "David" }, { name: "nationality", value: "Danish" }],
+ to_form_params("name" => "David", :nationality => "Danish")
+ )
+ end
+
+ def test_to_form_params_with_nested_hash
+ assert_equal(
+ [{ name: "country[name]", value: "Denmark" }],
+ to_form_params(country: { name: "Denmark" })
+ )
+ end
+
+ def test_to_form_params_with_array_nested_in_hash
+ assert_equal(
+ [{ name: "countries[]", value: "Denmark" }, { name: "countries[]", value: "Sweden" }],
+ to_form_params(countries: ["Denmark", "Sweden"])
+ )
+ end
+
+ def test_to_form_params_with_namespace
+ assert_equal(
+ [{ name: "country[name]", value: "Denmark" }],
+ to_form_params({ name: "Denmark" }, "country")
+ )
end
def test_button_to_with_straight_url
@@ -58,7 +126,7 @@ class UrlHelperTest < ActiveSupport::TestCase
def test_button_to_with_path
assert_dom_equal(
%{<form method="post" action="/article/Hello" class="button_to"><input type="submit" value="Hello" /></form>},
- button_to("Hello", article_path("Hello".html_safe))
+ button_to("Hello", article_path("Hello"))
)
end
@@ -74,11 +142,11 @@ class UrlHelperTest < ActiveSupport::TestCase
end
def test_button_to_with_form_class
- assert_dom_equal %{<form method="post" action="http://www.example.com" class="custom-class"><input type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com", form_class: 'custom-class')
+ assert_dom_equal %{<form method="post" action="http://www.example.com" class="custom-class"><input type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com", form_class: "custom-class")
end
def test_button_to_with_form_class_escapes
- assert_dom_equal %{<form method="post" action="http://www.example.com" class="&lt;script&gt;evil_js&lt;/script&gt;"><input type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com", form_class: '<script>evil_js</script>')
+ assert_dom_equal %{<form method="post" action="http://www.example.com" class="&lt;script&gt;evil_js&lt;/script&gt;"><input type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com", form_class: "<script>evil_js</script>")
end
def test_button_to_with_query
@@ -86,7 +154,7 @@ class UrlHelperTest < ActiveSupport::TestCase
end
def test_button_to_with_html_safe_URL
- assert_dom_equal %{<form method="post" action="http://www.example.com/q1=v1&amp;q2=v2" class="button_to"><input type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com/q1=v1&amp;q2=v2".html_safe)
+ assert_dom_equal %{<form method="post" action="http://www.example.com/q1=v1&amp;q2=v2" class="button_to"><input type="submit" value="Hello" /></form>}, button_to("Hello", raw("http://www.example.com/q1=v1&amp;q2=v2"))
end
def test_button_to_with_query_and_no_name
@@ -163,14 +231,59 @@ class UrlHelperTest < ActiveSupport::TestCase
def test_button_to_with_block
assert_dom_equal(
%{<form method="post" action="http://www.example.com" class="button_to"><button type="submit"><span>Hello</span></button></form>},
- button_to("http://www.example.com") { content_tag(:span, 'Hello') }
+ button_to("http://www.example.com") { content_tag(:span, "Hello") }
)
end
def test_button_to_with_params
assert_dom_equal(
- %{<form action="http://www.example.com" class="button_to" method="post"><input type="submit" value="Hello" /><input type="hidden" name="foo" value="bar" /><input type="hidden" name="baz" value="quux" /></form>},
- button_to("Hello", "http://www.example.com", params: {foo: :bar, baz: "quux"})
+ %{<form action="http://www.example.com" class="button_to" method="post"><input type="submit" value="Hello" /><input type="hidden" name="baz" value="quux" /><input type="hidden" name="foo" value="bar" /></form>},
+ button_to("Hello", "http://www.example.com", params: { foo: :bar, baz: "quux" })
+ )
+ end
+
+ class FakeParams
+ def initialize(permitted = true)
+ @permitted = permitted
+ end
+
+ def permitted?
+ @permitted
+ end
+
+ def to_h
+ if permitted?
+ { foo: :bar, baz: "quux" }
+ else
+ raise ArgumentError
+ end
+ end
+ end
+
+ 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_unpermitted_strong_params
+ assert_raises(ArgumentError) do
+ button_to("Hello", "http://www.example.com", params: FakeParams.new(false))
+ end
+ end
+
+ def test_button_to_with_nested_hash_params
+ assert_dom_equal(
+ %{<form action="http://www.example.com" class="button_to" method="post"><input type="submit" value="Hello" /><input type="hidden" name="foo[bar]" value="baz" /></form>},
+ button_to("Hello", "http://www.example.com", params: { foo: { bar: "baz" } })
+ )
+ end
+
+ def test_button_to_with_nested_array_params
+ assert_dom_equal(
+ %{<form action="http://www.example.com" class="button_to" method="post"><input type="submit" value="Hello" /><input type="hidden" name="foo[]" value="bar" /></form>},
+ button_to("Hello", "http://www.example.com", params: { foo: ["bar"] })
)
end
@@ -179,13 +292,13 @@ class UrlHelperTest < ActiveSupport::TestCase
end
def test_link_tag_without_host_option
- assert_dom_equal(%{<a href="/">Test Link</a>}, link_to('Test Link', url_hash))
+ assert_dom_equal(%{<a href="/">Test Link</a>}, link_to("Test Link", url_hash))
end
def test_link_tag_with_host_option
hash = hash_for(host: "www.example.com")
expected = %{<a href="http://www.example.com/">Test Link</a>}
- assert_dom_equal(expected, link_to('Test Link', hash))
+ assert_dom_equal(expected, link_to("Test Link", hash))
end
def test_link_tag_with_query
@@ -199,20 +312,20 @@ class UrlHelperTest < ActiveSupport::TestCase
end
def test_link_tag_with_back
- env = {"HTTP_REFERER" => "http://www.example.com/referer"}
+ env = { "HTTP_REFERER" => "http://www.example.com/referer" }
@controller = Struct.new(:request).new(Struct.new(:env).new(env))
expected = %{<a href="#{env["HTTP_REFERER"]}">go back</a>}
- assert_dom_equal expected, link_to('go back', :back)
+ assert_dom_equal expected, link_to("go back", :back)
end
def test_link_tag_with_back_and_no_referer
@controller = Struct.new(:request).new(Struct.new(:env).new({}))
- link = link_to('go back', :back)
+ link = link_to("go back", :back)
assert_dom_equal %{<a href="javascript:history.back()">go back</a>}, link
end
def test_link_tag_with_img
- link = link_to("<img src='/favicon.jpg' />".html_safe, "/")
+ link = link_to(raw("<img src='/favicon.jpg' />"), "/")
expected = %{<a href="/"><img src='/favicon.jpg' /></a>}
assert_dom_equal expected, link
end
@@ -267,7 +380,7 @@ class UrlHelperTest < ActiveSupport::TestCase
def test_link_to_with_string_remote_in_non_html_options
assert_dom_equal(
%{<a href="/" data-remote="true">Hello</a>},
- link_to("Hello", hash_for('remote' => true), {})
+ link_to("Hello", hash_for("remote" => true), {})
)
end
@@ -288,14 +401,14 @@ class UrlHelperTest < ActiveSupport::TestCase
def test_link_tag_using_delete_javascript_and_href
assert_dom_equal(
%{<a href="\#" rel="nofollow" data-method="delete">Destroy</a>},
- link_to("Destroy", "http://www.example.com", method: :delete, href: '#')
+ link_to("Destroy", "http://www.example.com", method: :delete, href: "#")
)
end
def test_link_tag_using_post_javascript_and_rel
assert_dom_equal(
%{<a href="http://www.example.com" data-method="post" rel="example nofollow">Hello</a>},
- link_to("Hello", "http://www.example.com", method: :post, rel: 'example')
+ link_to("Hello", "http://www.example.com", method: :post, rel: "example")
)
end
@@ -309,24 +422,24 @@ class UrlHelperTest < ActiveSupport::TestCase
def test_link_tag_using_delete_javascript_and_href_and_confirm
assert_dom_equal(
%{<a href="\#" rel="nofollow" data-confirm="Are you serious?" data-method="delete">Destroy</a>},
- link_to("Destroy", "http://www.example.com", method: :delete, href: '#', data: { confirm: "Are you serious?" })
+ link_to("Destroy", "http://www.example.com", method: :delete, href: "#", data: { confirm: "Are you serious?" })
)
end
def test_link_tag_with_block
assert_dom_equal %{<a href="/"><span>Example site</span></a>},
- link_to('/') { content_tag(:span, 'Example site') }
+ link_to("/") { content_tag(:span, "Example site") }
end
def test_link_tag_with_block_and_html_options
assert_dom_equal %{<a class="special" href="/"><span>Example site</span></a>},
- link_to('/', class: "special") { content_tag(:span, 'Example site') }
+ link_to("/", class: "special") { content_tag(:span, "Example site") }
end
def test_link_tag_using_block_and_hash
assert_dom_equal(
%{<a href="/"><span>Example site</span></a>},
- link_to(url_hash) { content_tag(:span, 'Example site') }
+ link_to(url_hash) { content_tag(:span, "Example site") }
)
end
@@ -338,7 +451,7 @@ class UrlHelperTest < ActiveSupport::TestCase
def test_link_tag_with_html_safe_string
assert_dom_equal(
%{<a href="/article/Gerd_M%C3%BCller">Gerd Müller</a>},
- link_to("Gerd Müller", article_path("Gerd_Müller".html_safe))
+ link_to("Gerd Müller", article_path("Gerd_Müller"))
)
end
@@ -349,7 +462,7 @@ class UrlHelperTest < ActiveSupport::TestCase
def test_link_tag_does_not_escape_html_safe_content
assert_dom_equal %{<a href="/">Malicious <script>content</script></a>},
- link_to("Malicious <script>content</script>".html_safe, "/")
+ link_to(raw("Malicious <script>content</script>"), "/")
end
def test_link_to_unless
@@ -360,7 +473,7 @@ class UrlHelperTest < ActiveSupport::TestCase
assert_equal "<strong>Showing</strong>",
link_to_unless(true, "Showing", url_hash) { |name|
- "<strong>#{name}</strong>".html_safe
+ raw "<strong>#{name}</strong>"
}
assert_equal "test",
@@ -370,8 +483,8 @@ class UrlHelperTest < ActiveSupport::TestCase
assert_equal %{&lt;b&gt;Showing&lt;/b&gt;}, link_to_unless(true, "<b>Showing</b>", url_hash)
assert_equal %{<a href="/">&lt;b&gt;Showing&lt;/b&gt;</a>}, link_to_unless(false, "<b>Showing</b>", url_hash)
- assert_equal %{<b>Showing</b>}, link_to_unless(true, "<b>Showing</b>".html_safe, url_hash)
- assert_equal %{<a href="/"><b>Showing</b></a>}, link_to_unless(false, "<b>Showing</b>".html_safe, url_hash)
+ assert_equal %{<b>Showing</b>}, link_to_unless(true, raw("<b>Showing</b>"), url_hash)
+ assert_equal %{<a href="/"><b>Showing</b></a>}, link_to_unless(false, raw("<b>Showing</b>"), url_hash)
end
def test_link_to_if
@@ -390,7 +503,7 @@ class UrlHelperTest < ActiveSupport::TestCase
end
def test_current_page_with_http_head_method
- @request = request_for_url("/", :method => :head)
+ @request = request_for_url("/", method: :head)
assert current_page?(url_hash)
assert current_page?("http://www.example.com/")
end
@@ -408,6 +521,21 @@ 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_not current_page?(url_hash, check_parameters: true)
+ assert_not current_page?(url_hash.merge(check_parameters: true))
+ assert_not current_page?(ActionController::Parameters.new(url_hash.merge(check_parameters: true)).permit!)
+ assert_not current_page?("http://www.example.com/", check_parameters: true)
+ end
+
+ def test_current_page_considering_params_when_options_does_not_respond_to_to_hash
+ @request = request_for_url("/?order=desc&page=1")
+
+ assert_not current_page?(:back, check_parameters: false)
+ end
+
def test_current_page_with_params_that_match
@request = request_for_url("/?order=desc&page=1")
@@ -415,22 +543,22 @@ class UrlHelperTest < ActiveSupport::TestCase
assert current_page?("http://www.example.com/?order=desc&page=1")
end
- def test_current_page_with_not_get_verb
- @request = request_for_url("/events", method: :post)
+ def test_current_page_with_scope_that_match
+ @request = request_for_url("/engine/")
- assert !current_page?('/events')
+ assert current_page?("/engine")
end
def test_current_page_with_escaped_params
@request = request_for_url("/category/administra%c3%a7%c3%a3o")
- assert current_page?(controller: 'foo', action: 'category', category: 'administração')
+ assert current_page?(controller: "foo", action: "category", category: "administração")
end
def test_current_page_with_escaped_params_with_different_encoding
@request = request_for_url("/")
- @request.stub(:path, "/category/administra%c3%a7%c3%a3o".force_encoding(Encoding::ASCII_8BIT)) do
- assert current_page?(:controller => 'foo', :action => 'category', category: 'administração')
+ @request.stub(:path, (+"/category/administra%c3%a7%c3%a3o").force_encoding(Encoding::ASCII_8BIT)) do
+ assert current_page?(controller: "foo", action: "category", category: "administração")
assert current_page?("http://www.example.com/category/administra%c3%a7%c3%a3o")
end
end
@@ -438,7 +566,19 @@ class UrlHelperTest < ActiveSupport::TestCase
def test_current_page_with_double_escaped_params
@request = request_for_url("/category/administra%c3%a7%c3%a3o?callback_url=http%3a%2f%2fexample.com%2ffoo")
- assert current_page?(controller: 'foo', action: 'category', category: 'administração', callback_url: 'http://example.com/foo')
+ assert current_page?(controller: "foo", action: "category", category: "administração", callback_url: "http://example.com/foo")
+ end
+
+ def test_current_page_with_trailing_slash
+ @request = request_for_url("/posts")
+
+ assert current_page?("/posts/")
+ end
+
+ def test_current_page_with_not_get_verb
+ @request = request_for_url("/events", method: :post)
+
+ assert_not current_page?("/events")
end
def test_link_unless_current
@@ -459,7 +599,7 @@ class UrlHelperTest < ActiveSupport::TestCase
@request = request_for_url("/?order=desc&page=1")
assert_equal "Showing",
- link_to_unless_current("Showing", hash_for(order: 'desc', page: '1'))
+ link_to_unless_current("Showing", hash_for(order: "desc", page: "1"))
assert_equal "Showing",
link_to_unless_current("Showing", "http://www.example.com/?order=desc&page=1")
@@ -500,6 +640,13 @@ class UrlHelperTest < ActiveSupport::TestCase
mail_to("david@loudthinking.com", "David Heinemeier Hansson", class: "admin")
end
+ def test_mail_to_with_special_characters
+ assert_dom_equal(
+ %{<a href="mailto:%23%21%24%25%26%27%2A%2B-%2F%3D%3F%5E_%60%7B%7D%7C@example.org">#!$%&amp;&#39;*+-/=?^_`{}|@example.org</a>},
+ mail_to("#!$%&'*+-/=?^_`{}|@example.org")
+ )
+ end
+
def test_mail_with_options
assert_dom_equal(
%{<a href="mailto:me@example.com?cc=ccaddress%40example.com&amp;bcc=bccaddress%40example.com&amp;body=This%20is%20the%20body%20of%20the%20message.&amp;subject=This%20is%20an%20example%20email&amp;reply-to=foo%40bar.com">My email</a>},
@@ -508,84 +655,93 @@ class UrlHelperTest < ActiveSupport::TestCase
assert_dom_equal(
%{<a href="mailto:me@example.com?body=This%20is%20the%20body%20of%20the%20message.&amp;subject=This%20is%20an%20example%20email">My email</a>},
- mail_to("me@example.com", "My email", cc: '', bcc: '', subject: "This is an example email", body: "This is the body of the message.")
+ mail_to("me@example.com", "My email", cc: "", bcc: "", subject: "This is an example email", body: "This is the body of the message.")
)
end
def test_mail_to_with_img
assert_dom_equal %{<a href="mailto:feedback@example.com"><img src="/feedback.png" /></a>},
- mail_to('feedback@example.com', '<img src="/feedback.png" />'.html_safe)
+ mail_to("feedback@example.com", raw('<img src="/feedback.png" />'))
+ end
+
+ def test_mail_to_with_html_safe_string
+ assert_dom_equal(
+ %{<a href="mailto:david@loudthinking.com">david@loudthinking.com</a>},
+ mail_to(raw("david@loudthinking.com"))
+ )
+ end
+
+ def test_mail_to_with_nil
+ assert_dom_equal(
+ %{<a href="mailto:"></a>},
+ mail_to(nil)
+ )
end
def test_mail_to_returns_html_safe_string
- assert mail_to("david@loudthinking.com").html_safe?
+ assert_predicate mail_to("david@loudthinking.com"), :html_safe?
end
def test_mail_to_with_block
assert_dom_equal %{<a href="mailto:me@example.com"><span>Email me</span></a>},
- mail_to('me@example.com') { content_tag(:span, 'Email me') }
+ mail_to("me@example.com") { content_tag(:span, "Email me") }
end
def test_mail_to_with_block_and_options
assert_dom_equal %{<a class="special" href="mailto:me@example.com?cc=ccaddress%40example.com"><span>Email me</span></a>},
- mail_to('me@example.com', cc: "ccaddress@example.com", class: "special") { content_tag(:span, 'Email me') }
+ mail_to("me@example.com", cc: "ccaddress@example.com", class: "special") { content_tag(:span, "Email me") }
end
def test_mail_to_does_not_modify_html_options_hash
- options = { class: 'special' }
- mail_to 'me@example.com', 'ME!', options
- assert_equal({ class: 'special' }, options)
+ options = { class: "special" }
+ mail_to "me@example.com", "ME!", options
+ assert_equal({ class: "special" }, options)
end
def protect_against_forgery?
- self.request_forgery
+ request_forgery
end
- def form_authenticity_token
+ def form_authenticity_token(*args)
"secret"
end
def request_forgery_protection_token
"form_token"
end
-
- private
- def sort_query_string_params(uri)
- path, qs = uri.split('?')
- qs = qs.split('&amp;').sort.join('&amp;') if qs
- qs ? "#{path}?#{qs}" : path
- end
end
class UrlHelperControllerTest < ActionController::TestCase
class UrlHelperController < ActionController::Base
- test_routes do
- get 'url_helper_controller_test/url_helper/show/:id',
- to: 'url_helper_controller_test/url_helper#show',
+ ROUTES = test_routes do
+ get "url_helper_controller_test/url_helper/show/:id",
+ to: "url_helper_controller_test/url_helper#show",
as: :show
- get 'url_helper_controller_test/url_helper/profile/:name',
- to: 'url_helper_controller_test/url_helper#show',
+ get "url_helper_controller_test/url_helper/profile/:name",
+ to: "url_helper_controller_test/url_helper#show",
as: :profile
- get 'url_helper_controller_test/url_helper/show_named_route',
- to: 'url_helper_controller_test/url_helper#show_named_route',
+ get "url_helper_controller_test/url_helper/show_named_route",
+ to: "url_helper_controller_test/url_helper#show_named_route",
as: :show_named_route
- get "/:controller(/:action(/:id))"
+ ActiveSupport::Deprecation.silence do
+ get "/:controller(/:action(/:id))"
+ end
- get 'url_helper_controller_test/url_helper/normalize_recall_params',
+ get "url_helper_controller_test/url_helper/normalize_recall_params",
to: UrlHelperController.action(:normalize_recall),
as: :normalize_recall_params
- get '/url_helper_controller_test/url_helper/override_url_helper/default',
- to: 'url_helper_controller_test/url_helper#override_url_helper',
+ get "/url_helper_controller_test/url_helper/override_url_helper/default",
+ to: "url_helper_controller_test/url_helper#override_url_helper",
as: :override_url_helper
end
def show
if params[:name]
- render inline: 'ok'
+ render inline: "ok"
else
redirect_to profile_path(params[:id])
end
@@ -595,102 +751,98 @@ class UrlHelperControllerTest < ActionController::TestCase
render inline: "<%= url_for controller: 'url_helper_controller_test/url_helper', action: 'show_url_for' %>"
end
- def show_overridden_url_for
- render inline: "<%= url_for params.merge(controller: 'url_helper_controller_test/url_helper', action: 'show_url_for') %>"
- end
-
def show_named_route
render inline: "<%= show_named_route_#{params[:kind]} %>"
end
def nil_url_for
- render inline: '<%= url_for(nil) %>'
+ render inline: "<%= url_for(nil) %>"
end
def normalize_recall_params
- render inline: '<%= normalize_recall_params_path %>'
+ render inline: "<%= normalize_recall_params_path %>"
end
def recall_params_not_changed
- render inline: '<%= url_for(action: :show_url_for) %>'
+ render inline: "<%= url_for(action: :show_url_for) %>"
end
def override_url_helper
- render inline: '<%= override_url_helper_path %>'
+ render inline: "<%= override_url_helper_path %>"
end
def override_url_helper_path
- '/url_helper_controller_test/url_helper/override_url_helper/override'
+ "/url_helper_controller_test/url_helper/override_url_helper/override"
end
helper_method :override_url_helper_path
end
+ def setup
+ super
+ @routes = UrlHelperController::ROUTES
+ end
+
tests UrlHelperController
def test_url_for_shows_only_path
get :show_url_for
- assert_equal '/url_helper_controller_test/url_helper/show_url_for', @response.body
- end
-
- def test_overridden_url_for_shows_only_path
- get :show_overridden_url_for
- assert_equal '/url_helper_controller_test/url_helper/show_url_for', @response.body
+ assert_equal "/url_helper_controller_test/url_helper/show_url_for", @response.body
end
def test_named_route_url_shows_host_and_path
- get :show_named_route, params: { kind: 'url' }
- assert_equal 'http://test.host/url_helper_controller_test/url_helper/show_named_route',
+ get :show_named_route, params: { kind: "url" }
+ assert_equal "http://test.host/url_helper_controller_test/url_helper/show_named_route",
@response.body
end
def test_named_route_path_shows_only_path
- get :show_named_route, params: { kind: 'path' }
- assert_equal '/url_helper_controller_test/url_helper/show_named_route', @response.body
+ get :show_named_route, params: { kind: "path" }
+ assert_equal "/url_helper_controller_test/url_helper/show_named_route", @response.body
end
def test_url_for_nil_returns_current_path
get :nil_url_for
- assert_equal '/url_helper_controller_test/url_helper/nil_url_for', @response.body
+ assert_equal "/url_helper_controller_test/url_helper/nil_url_for", @response.body
end
def test_named_route_should_show_host_and_path_using_controller_default_url_options
class << @controller
def default_url_options
- { host: 'testtwo.host' }
+ { host: "testtwo.host" }
end
end
- get :show_named_route, params: { kind: 'url' }
- assert_equal 'http://testtwo.host/url_helper_controller_test/url_helper/show_named_route', @response.body
+ get :show_named_route, params: { kind: "url" }
+ assert_equal "http://testtwo.host/url_helper_controller_test/url_helper/show_named_route", @response.body
end
def test_recall_params_should_be_normalized
get :normalize_recall_params
- assert_equal '/url_helper_controller_test/url_helper/normalize_recall_params', @response.body
+ assert_equal "/url_helper_controller_test/url_helper/normalize_recall_params", @response.body
end
def test_recall_params_should_not_be_changed
get :recall_params_not_changed
- assert_equal '/url_helper_controller_test/url_helper/show_url_for', @response.body
+ assert_equal "/url_helper_controller_test/url_helper/show_url_for", @response.body
end
def test_recall_params_should_normalize_id
- get :show, params: { id: '123' }
+ get :show, params: { id: "123" }
assert_equal 302, @response.status
- assert_equal 'http://test.host/url_helper_controller_test/url_helper/profile/123', @response.location
+ assert_equal "http://test.host/url_helper_controller_test/url_helper/profile/123", @response.location
- get :show, params: { name: '123' }
- assert_equal 'ok', @response.body
+ get :show, params: { name: "123" }
+ assert_equal "ok", @response.body
end
def test_url_helper_can_be_overridden
get :override_url_helper
- assert_equal '/url_helper_controller_test/url_helper/override_url_helper/override', @response.body
+ assert_equal "/url_helper_controller_test/url_helper/override_url_helper/override", @response.body
end
end
class TasksController < ActionController::Base
- test_routes do
+ ROUTES = test_routes do
resources :tasks
end
@@ -702,9 +854,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
@@ -712,6 +864,11 @@ end
class LinkToUnlessCurrentWithControllerTest < ActionController::TestCase
tests TasksController
+ def setup
+ super
+ @routes = TasksController::ROUTES
+ end
+
def test_link_to_unless_current_to_current
get :index
assert_equal "tasks\ntasks", @response.body
@@ -744,7 +901,7 @@ class Session
end
class WorkshopsController < ActionController::Base
- test_routes do
+ ROUTES = test_routes do
resources :workshops do
resources :sessions
end
@@ -759,10 +916,15 @@ class WorkshopsController < ActionController::Base
@workshop = Workshop.new(params[:id])
render inline: "<%= url_for(@workshop) %>\n<%= link_to('Workshop', @workshop) %>"
end
+
+ def edit
+ @workshop = Workshop.new(params[:id])
+ render inline: "<%= current_page?(@workshop) %>"
+ end
end
class SessionsController < ActionController::Base
- test_routes do
+ ROUTES = test_routes do
resources :workshops do
resources :sessions
end
@@ -779,9 +941,21 @@ class SessionsController < ActionController::Base
@session = Session.new(params[:id])
render inline: "<%= url_for([@workshop, @session]) %>\n<%= link_to('Session', [@workshop, @session]) %>"
end
+
+ def edit
+ @workshop = Workshop.new(params[:workshop_id])
+ @session = Session.new(params[:id])
+ @url = [@workshop, @session, format: params[:format]]
+ render inline: "<%= url_for(@url) %>\n<%= link_to('Session', @url) %>"
+ end
end
class PolymorphicControllerTest < ActionController::TestCase
+ def setup
+ super
+ @routes = WorkshopsController::ROUTES
+ end
+
def test_new_resource
@controller = WorkshopsController.new
@@ -796,6 +970,20 @@ class PolymorphicControllerTest < ActionController::TestCase
assert_equal %{/workshops/1\n<a href="/workshops/1">Workshop</a>}, @response.body
end
+ def test_current_page_when_options_does_not_respond_to_to_hash
+ @controller = WorkshopsController.new
+
+ get :edit, params: { id: 1 }
+ assert_equal "false", @response.body
+ end
+end
+
+class PolymorphicSessionsControllerTest < ActionController::TestCase
+ def setup
+ super
+ @routes = SessionsController::ROUTES
+ end
+
def test_new_nested_resource
@controller = SessionsController.new
@@ -809,4 +997,11 @@ class PolymorphicControllerTest < ActionController::TestCase
get :show, params: { workshop_id: 1, id: 1 }
assert_equal %{/workshops/1/sessions/1\n<a href="/workshops/1/sessions/1">Session</a>}, @response.body
end
+
+ def test_existing_nested_resource_with_params
+ @controller = SessionsController.new
+
+ get :edit, params: { workshop_id: 1, id: 1, format: "json" }
+ assert_equal %{/workshops/1/sessions/1.json\n<a href="/workshops/1/sessions/1.json">Session</a>}, @response.body
+ end
end
diff --git a/actionview/test/ujs/config.ru b/actionview/test/ujs/config.ru
new file mode 100644
index 0000000000..7cd3a16acb
--- /dev/null
+++ b/actionview/test/ujs/config.ru
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+$LOAD_PATH.unshift __dir__
+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-ajax.js b/actionview/test/ujs/public/test/call-ajax.js
new file mode 100644
index 0000000000..4d0bfb0806
--- /dev/null
+++ b/actionview/test/ujs/public/test/call-ajax.js
@@ -0,0 +1,26 @@
+(function() {
+
+module('call-ajax', {
+ setup: function() {
+ $('#qunit-fixture')
+ .append($('<a />', { href: '#' }))
+ }
+})
+
+asyncTest('call ajax without "ajax:beforeSend"', 1, function() {
+ var link = $('#qunit-fixture a')
+ link.bindNative('click', function() {
+ Rails.ajax({
+ type: 'get',
+ url: '/',
+ success: function() {
+ ok(true, 'calling request in ajax:success')
+ }
+ })
+ })
+
+ link.triggerNative('click')
+ setTimeout(function() { start() }, 50)
+})
+
+})()
diff --git a/actionview/test/ujs/public/test/call-remote-callbacks.js b/actionview/test/ujs/public/test/call-remote-callbacks.js
new file mode 100644
index 0000000000..9c0c8cfb4b
--- /dev/null
+++ b/actionview/test/ujs/public/test/call-remote-callbacks.js
@@ -0,0 +1,239 @@
+(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('iframe:loading')
+ }
+})
+
+function submit(fn) {
+ var form = $('form')
+
+ if (fn) fn(form)
+ form.triggerNative('submit')
+
+ setTimeout(function() { start() }, 13)
+}
+
+asyncTest('modifying form fields with "ajax:before" sends modified data in request', 3, function() {
+ $('form[data-remote]')
+ .append($('<input type="text" name="user_name" value="john">'))
+ .append($('<input type="text" name="removed_user_name" value="john">'))
+ .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', 1, 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', 1, 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(e) {
+ ok(true, 'aborting request in ajax:beforeSend')
+ e.preventDefault()
+ })
+ form.unbind('ajax:send').bindNative('ajax:send', function() {
+ ok(false, 'ajax:send should not run')
+ })
+ form.bindNative('ajax:error', function(e, response, status, xhr) {
+ ok(false, 'ajax:error should not run')
+ })
+ form.bindNative('ajax:complete', function() {
+ ok(false, 'ajax:complete should not run')
+ })
+ })
+})
+
+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(e) {
+ e.preventDefault()
+ })
+ .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(e) {
+ ok(true, 'ajax:beforeSend observed with event delegation')
+ e.preventDefault()
+ })
+
+ submit(function(form) {
+ form.unbind('ajax:send').bindNative('ajax:send', function() {
+ ok(false, 'ajax:send should not run')
+ })
+ form.bindNative('ajax:complete', function() {
+ ok(false, 'ajax:complete should not run')
+ })
+ })
+})
+
+asyncTest('"ajax:beforeSend", "ajax:send", "ajax:success" and "ajax:complete" are triggered', 8, function() {
+ submit(function(form) {
+ form.bindNative('ajax:beforeSend', function(e, xhr, settings) {
+ ok(xhr.setRequestHeader, 'first argument to "ajax:beforeSend" should be an XHR object')
+ 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')
+ })
+ })
+})
+
+asyncTest('"ajax:beforeSend", "ajax:send", "ajax:error" and "ajax:complete" are triggered on error', 8, function() {
+ submit(function(form) {
+ form.attr('action', '/error')
+ form.bindNative('ajax:beforeSend', function(arg) { ok(true, 'ajax:beforeSend') })
+ form.bindNative('ajax:send', function(arg) { ok(true, 'ajax:send') })
+ form.bindNative('ajax:error', function(e, response, status, xhr) {
+ equal(response, '', 'first argument to ajax:error should be an HTTP status response')
+ equal(status, 'Forbidden', 'second argument to ajax:error should be a status string')
+ ok(xhr.getResponseHeader, 'third argument to "ajax:error" should be an XHR object')
+ // Opera returns "0" for HTTP code
+ equal(xhr.status, window.opera ? 0 : 403, 'status code should be 403')
+ })
+ form.bindNative('ajax:complete', function(e, xhr, status) {
+ ok(xhr.getResponseHeader, 'first argument to "ajax:complete" should be an XHR object')
+ equal(status, 'Forbidden', 'second argument to ajax:complete should be a status string')
+ })
+ })
+})
+
+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:success', function() {
+ ok(true, 'ajax:success handler is triggered')
+ })
+ .delegate('form[data-remote]', 'ajax:complete', function() {
+ ok(true, 'ajax:complete handler is triggered')
+ })
+ $('form[data-remote]').triggerNative('submit')
+
+ setTimeout(function() { start() }, 13)
+})
+
+})()
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..778dc1b09a
--- /dev/null
+++ b/actionview/test/ujs/public/test/call-remote.js
@@ -0,0 +1,286 @@
+(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('ecmascript code should be executed', 1, function() {
+ buildForm({ method: 'post', 'data-type': 'script' })
+
+ $('form').append('<input type="text" name="content_type" value="application/ecmascript">')
+ $('form').append('<input type="text" name="content" value="ok(true, \'remote code should be run\')">')
+
+ submit()
+})
+
+asyncTest('execution of JS code does not modify current DOM', 1, function() {
+ var docLength, newDocLength
+ function getDocLength() {
+ return document.documentElement.outerHTML.length
+ }
+
+ 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="\'remote code should be run\'">')
+
+ docLength = getDocLength()
+
+ submit(function() {
+ newDocLength = getDocLength()
+ ok(docLength === newDocLength, 'executed JS should not present in the document')
+ })
+})
+
+asyncTest('HTML content should be plain-text', 1, function() {
+ buildForm({ method: 'post', 'data-type': 'html' })
+
+ $('form').append('<input type="text" name="content_type" value="text/html">')
+ $('form').append('<input type="text" name="content" value="<p>hello</p>">')
+
+ submit(function(e, data, status, xhr) {
+ ok(data === '<p>hello</p>', 'returned data should be a plain-text string')
+ })
+})
+
+asyncTest('XML document should be parsed', 1, function() {
+ buildForm({ method: 'post', 'data-type': 'html' })
+
+ $('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(evt, 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.
+ evt.preventDefault()
+ })
+ .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
+ evt.preventDefault()
+ })
+ .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
+ evt.preventDefault()
+ })
+ .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..1bd57b69ad
--- /dev/null
+++ b/actionview/test/ujs/public/test/data-confirm.js
@@ -0,0 +1,342 @@
+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(e) {
+ App.assertCallbackInvoked('confirm')
+ e.preventDefault()
+ })
+ .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(e) {
+ App.assertCallbackInvoked('confirm')
+ e.preventDefault()
+ })
+ .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(e) {
+ App.assertCallbackInvoked('confirm:complete')
+ e.preventDefault()
+ })
+ .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(e) {
+ App.assertCallbackInvoked('confirm:complete')
+ e.preventDefault()
+ })
+ .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)
+})
+
+asyncTest('clicking on a link with data-confirm attribute with custom confirm handler. Confirm yes.', 7, function() {
+ var message, element
+ // redefine confirm function so we can make sure it's not called
+ window.confirm = function(msg) {
+ ok(false, 'confirm dialog should not be called')
+ }
+ // custom auto-confirm:
+ Rails.confirm = function(msg, elem) { message = msg; element = elem; return true }
+
+ $('a[data-confirm]')
+ .bindNative('confirm:complete', function(e, data) {
+ App.assertCallbackInvoked('confirm:complete')
+ ok(data == true, 'confirm:complete passes in confirm answer (true)')
+ })
+ .bindNative('ajax:success', function(e, data, status, xhr) {
+ App.assertCallbackInvoked('ajax:success')
+ App.assertRequestPath(data, '/echo')
+ App.assertGetRequest(data)
+
+ equal(message, 'Are you absolutely sure?')
+ equal(element, $('a[data-confirm]').get(0))
+ start()
+ })
+ .triggerNative('click')
+})
diff --git a/actionview/test/ujs/public/test/data-disable-with.js b/actionview/test/ujs/public/test/data-disable-with.js
new file mode 100644
index 0000000000..645ad494c3
--- /dev/null
+++ b/actionview/test/ujs/public/test/data-disable-with.js
@@ -0,0 +1,394 @@
+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 = $('#qunit-fixture 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 = $('#qunit-fixture form:not([data-remote])').attr('data-remote', 'true'),
+ input = form.find('input[type=submit]'),
+ newDisabledInput = input.clone().attr('disabled', 'disabled')
+
+ form.bindNative('ajax:success', function() {
+ 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(e) {
+ App.checkDisabledState(link, 'clicking...')
+ e.preventDefault()
+ })
+ .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(e) {
+ App.checkDisabledState(link, 'clicking...')
+ e.preventDefault()
+ })
+ .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(e) {
+ e.preventDefault()
+ e.stopPropagation()
+ })
+ .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(e) {
+ App.checkDisabledState(button, 'clicking...')
+ e.preventDefault()
+ })
+ .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(e) {
+ App.checkDisabledState(button, 'clicking...')
+ e.preventDefault()
+ })
+ .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..88dc801b2f
--- /dev/null
+++ b/actionview/test/ujs/public/test/data-disable.js
@@ -0,0 +1,339 @@
+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 = $('#qunit-fixture 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 = $('#qunit-fixture form:not([data-remote])').attr('data-remote', 'true'), input = form.find('input[type=submit]'),
+ newDisabledInput = input.clone().attr('disabled', 'disabled')
+
+ form.bindNative('ajax:success', function() {
+ 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(e) {
+ App.checkDisabledState(link, 'Click me')
+ e.preventDefault()
+ })
+ .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(e) {
+ App.checkDisabledState(link, 'Click me')
+ e.preventDefault()
+ })
+ .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(e) {
+ e.preventDefault()
+ e.stopPropagation()
+ })
+ .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(e) {
+ App.checkDisabledState(button, 'Click me')
+ e.preventDefault()
+ })
+ .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(e) {
+ App.checkDisabledState(button, 'Click me')
+ e.preventDefault()
+ })
+ .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)
+})
+
+asyncTest('do not enable elements for XHR redirects', 6, function() {
+ var link = $('a[data-disable]').attr('data-remote', true).attr('href', '/echo?with_xhr_redirect=true')
+
+ App.checkEnabledState(link, 'Click me')
+
+ link
+ .bindNative('ajax:send', function() {
+ App.checkDisabledState(link, 'Click me')
+ })
+ .triggerNative('click')
+
+ setTimeout(function() {
+ App.checkDisabledState(link, '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..3503c2cff3
--- /dev/null
+++ b/actionview/test/ujs/public/test/data-remote.js
@@ -0,0 +1,441 @@
+(function() {
+
+function buildSelect(attrs) {
+ attrs = $.extend({
+ 'name': 'user_data', 'data-remote': 'true', 'data-url': '/echo', 'data-params': 'data1=value1'
+ }, attrs)
+
+ $('#qunit-fixture').append(
+ $('<select />', attrs)
+ .append($('<option />', {value: 'optionValue1', text: 'option1'}))
+ .append($('<option />', {value: 'optionValue2', text: 'option2'}))
+ )
+}
+
+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() {
+ buildSelect()
+
+ $('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', 6, function() {
+ $('#qunit-fixture')
+ .append($('<input type="text" name="user_data" value="value1" form="my-remote-form">'))
+ .append($('<input type="text" name="user_email" value="from@example.com" disabled="disabled" form="my-remote-form">'))
+
+ $('form[data-remote]')
+ .bindNative('ajax:success', function(e, data, status, xhr) {
+ 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')
+ equal(data.params.user_email, undefined, 'ajax arguments should not have disabled field')
+ 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(e) {
+ ok(true, 'binding handler is called')
+ e.preventDefault()
+ e.stopPropagation()
+ })
+ .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(e) {
+ e.preventDefault()
+ })
+ .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(e) {
+ e.preventDefault()
+ })
+ .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(e) {
+ e.preventDefault()
+ })
+ .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(e) {
+ e.preventDefault()
+ })
+ .triggerNative('submit')
+
+ setTimeout(function() { start() }, 20)
+})
+
+asyncTest('changing a select option with falsy "data-remote" attribute', 0, function() {
+ buildSelect({'data-remote': 'false'})
+
+ $('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')
+})
+
+asyncTest('changing a select option without "data-url" attribute still fires ajax request to current location', 1, function() {
+ var currentLocation, ajaxLocation
+
+ buildSelect({'data-url': ''})
+
+ $('select[data-remote]')
+ .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
+ }
+
+ ajaxLocation = settings.url.replace(settings.data, '').replace(/&$/, '').replace(/\?$/, '')
+ equal(ajaxLocation, currentLocation, 'URL should be current page by default')
+
+ e.preventDefault()
+ })
+ .val('optionValue2')
+ .triggerNative('change')
+
+ setTimeout(function() { start() }, 20)
+})
+
+})()
diff --git a/actionview/test/ujs/public/test/override.js b/actionview/test/ujs/public/test/override.js
new file mode 100644
index 0000000000..d73276ee4f
--- /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)
+ e.preventDefault()
+ })
+ .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)
+ e.preventDefault()
+ })
+ .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..05677f2595
--- /dev/null
+++ b/actionview/test/ujs/public/test/settings.js
@@ -0,0 +1,122 @@
+var App = App || {}
+var Turbolinks = Turbolinks || {}
+
+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
+
+ var el = this[0]
+ el.addEventListener(event, function(e) {
+ var args = []
+ if (e.detail) {
+ args = e.detail.slice()
+ }
+ args.unshift(e)
+ return handler.apply(el, args)
+ }, false)
+
+ return this
+ }
+})
+
+Turbolinks.clearCache = function() {}
+Turbolinks.visit = function() {}
diff --git a/actionview/test/ujs/public/vendor/jquery-2.2.0.js b/actionview/test/ujs/public/vendor/jquery-2.2.0.js
new file mode 100644
index 0000000000..1e0ba99740
--- /dev/null
+++ b/actionview/test/ujs/public/vendor/jquery-2.2.0.js
@@ -0,0 +1,9831 @@
+/*!
+ * jQuery JavaScript Library v2.2.0
+ * http://jquery.com/
+ *
+ * Includes Sizzle.js
+ * http://sizzlejs.com/
+ *
+ * Copyright jQuery Foundation and other contributors
+ * Released under the MIT license
+ * http://jquery.org/license
+ *
+ * Date: 2016-01-08T20:02Z
+ */
+
+(function( global, factory ) {
+
+ if ( typeof module === "object" && typeof module.exports === "object" ) {
+ // For CommonJS and CommonJS-like environments where a proper `window`
+ // is present, execute the factory and get jQuery.
+ // For environments that do not have a `window` with a `document`
+ // (such as Node.js), expose a factory as module.exports.
+ // This accentuates the need for the creation of a real `window`.
+ // e.g. var jQuery = require("jquery")(window);
+ // See ticket #14549 for more info.
+ module.exports = global.document ?
+ factory( global, true ) :
+ function( w ) {
+ if ( !w.document ) {
+ throw new Error( "jQuery requires a window with a document" );
+ }
+ return factory( w );
+ };
+ } else {
+ factory( global );
+ }
+
+// Pass this if window is not defined yet
+}(typeof window !== "undefined" ? window : this, function( window, noGlobal ) {
+
+// Support: Firefox 18+
+// Can't be in strict mode, several libs including ASP.NET trace
+// the stack via arguments.caller.callee and Firefox dies if
+// you try to trace through "use strict" call chains. (#13335)
+//"use strict";
+var arr = [];
+
+var document = window.document;
+
+var slice = arr.slice;
+
+var concat = arr.concat;
+
+var push = arr.push;
+
+var indexOf = arr.indexOf;
+
+var class2type = {};
+
+var toString = class2type.toString;
+
+var hasOwn = class2type.hasOwnProperty;
+
+var support = {};
+
+
+
+var
+ version = "2.2.0",
+
+ // Define a local copy of jQuery
+ jQuery = function( selector, context ) {
+
+ // The jQuery object is actually just the init constructor 'enhanced'
+ // Need init if jQuery is called (just allow error to be thrown if not included)
+ return new jQuery.fn.init( selector, context );
+ },
+
+ // Support: Android<4.1
+ // Make sure we trim BOM and NBSP
+ rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,
+
+ // Matches dashed string for camelizing
+ rmsPrefix = /^-ms-/,
+ rdashAlpha = /-([\da-z])/gi,
+
+ // Used by jQuery.camelCase as callback to replace()
+ fcamelCase = function( all, letter ) {
+ return letter.toUpperCase();
+ };
+
+jQuery.fn = jQuery.prototype = {
+
+ // The current version of jQuery being used
+ jquery: version,
+
+ constructor: jQuery,
+
+ // Start with an empty selector
+ selector: "",
+
+ // The default length of a jQuery object is 0
+ length: 0,
+
+ toArray: function() {
+ return slice.call( this );
+ },
+
+ // Get the Nth element in the matched element set OR
+ // Get the whole matched element set as a clean array
+ get: function( num ) {
+ return num != null ?
+
+ // Return just the one element from the set
+ ( num < 0 ? this[ num + this.length ] : this[ num ] ) :
+
+ // Return all the elements in a clean array
+ slice.call( this );
+ },
+
+ // Take an array of elements and push it onto the stack
+ // (returning the new matched element set)
+ pushStack: function( elems ) {
+
+ // Build a new jQuery matched element set
+ var ret = jQuery.merge( this.constructor(), elems );
+
+ // Add the old object onto the stack (as a reference)
+ ret.prevObject = this;
+ ret.context = this.context;
+
+ // Return the newly-formed element set
+ return ret;
+ },
+
+ // Execute a callback for every element in the matched set.
+ each: function( callback ) {
+ return jQuery.each( this, callback );
+ },
+
+ map: function( callback ) {
+ return this.pushStack( jQuery.map( this, function( elem, i ) {
+ return callback.call( elem, i, elem );
+ } ) );
+ },
+
+ slice: function() {
+ return this.pushStack( slice.apply( this, arguments ) );
+ },
+
+ first: function() {
+ return this.eq( 0 );
+ },
+
+ last: function() {
+ return this.eq( -1 );
+ },
+
+ eq: function( i ) {
+ var len = this.length,
+ j = +i + ( i < 0 ? len : 0 );
+ return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] );
+ },
+
+ end: function() {
+ return this.prevObject || this.constructor();
+ },
+
+ // For internal use only.
+ // Behaves like an Array's method, not like a jQuery method.
+ push: push,
+ sort: arr.sort,
+ splice: arr.splice
+};
+
+jQuery.extend = jQuery.fn.extend = function() {
+ var options, name, src, copy, copyIsArray, clone,
+ target = arguments[ 0 ] || {},
+ i = 1,
+ length = arguments.length,
+ deep = false;
+
+ // Handle a deep copy situation
+ if ( typeof target === "boolean" ) {
+ deep = target;
+
+ // Skip the boolean and the target
+ target = arguments[ i ] || {};
+ i++;
+ }
+
+ // Handle case when target is a string or something (possible in deep copy)
+ if ( typeof target !== "object" && !jQuery.isFunction( target ) ) {
+ target = {};
+ }
+
+ // Extend jQuery itself if only one argument is passed
+ if ( i === length ) {
+ target = this;
+ i--;
+ }
+
+ for ( ; i < length; i++ ) {
+
+ // Only deal with non-null/undefined values
+ if ( ( options = arguments[ i ] ) != null ) {
+
+ // Extend the base object
+ for ( name in options ) {
+ src = target[ name ];
+ copy = options[ name ];
+
+ // Prevent never-ending loop
+ if ( target === copy ) {
+ continue;
+ }
+
+ // Recurse if we're merging plain objects or arrays
+ if ( deep && copy && ( jQuery.isPlainObject( copy ) ||
+ ( copyIsArray = jQuery.isArray( copy ) ) ) ) {
+
+ if ( copyIsArray ) {
+ copyIsArray = false;
+ clone = src && jQuery.isArray( src ) ? src : [];
+
+ } else {
+ clone = src && jQuery.isPlainObject( src ) ? src : {};
+ }
+
+ // Never move original objects, clone them
+ target[ name ] = jQuery.extend( deep, clone, copy );
+
+ // Don't bring in undefined values
+ } else if ( copy !== undefined ) {
+ target[ name ] = copy;
+ }
+ }
+ }
+ }
+
+ // Return the modified object
+ return target;
+};
+
+jQuery.extend( {
+
+ // Unique for each copy of jQuery on the page
+ expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ),
+
+ // Assume jQuery is ready without the ready module
+ isReady: true,
+
+ error: function( msg ) {
+ throw new Error( msg );
+ },
+
+ noop: function() {},
+
+ isFunction: function( obj ) {
+ return jQuery.type( obj ) === "function";
+ },
+
+ isArray: Array.isArray,
+
+ isWindow: function( obj ) {
+ return obj != null && obj === obj.window;
+ },
+
+ isNumeric: function( obj ) {
+
+ // parseFloat NaNs numeric-cast false positives (null|true|false|"")
+ // ...but misinterprets leading-number strings, particularly hex literals ("0x...")
+ // subtraction forces infinities to NaN
+ // adding 1 corrects loss of precision from parseFloat (#15100)
+ var realStringObj = obj && obj.toString();
+ return !jQuery.isArray( obj ) && ( realStringObj - parseFloat( realStringObj ) + 1 ) >= 0;
+ },
+
+ isPlainObject: function( obj ) {
+
+ // Not plain objects:
+ // - Any object or value whose internal [[Class]] property is not "[object Object]"
+ // - DOM nodes
+ // - window
+ if ( jQuery.type( obj ) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) {
+ return false;
+ }
+
+ if ( obj.constructor &&
+ !hasOwn.call( obj.constructor.prototype, "isPrototypeOf" ) ) {
+ return false;
+ }
+
+ // If the function hasn't returned already, we're confident that
+ // |obj| is a plain object, created by {} or constructed with new Object
+ return true;
+ },
+
+ isEmptyObject: function( obj ) {
+ var name;
+ for ( name in obj ) {
+ return false;
+ }
+ return true;
+ },
+
+ type: function( obj ) {
+ if ( obj == null ) {
+ return obj + "";
+ }
+
+ // Support: Android<4.0, iOS<6 (functionish RegExp)
+ return typeof obj === "object" || typeof obj === "function" ?
+ class2type[ toString.call( obj ) ] || "object" :
+ typeof obj;
+ },
+
+ // Evaluates a script in a global context
+ globalEval: function( code ) {
+ var script,
+ indirect = eval;
+
+ code = jQuery.trim( code );
+
+ if ( code ) {
+
+ // If the code includes a valid, prologue position
+ // strict mode pragma, execute code by injecting a
+ // script tag into the document.
+ if ( code.indexOf( "use strict" ) === 1 ) {
+ script = document.createElement( "script" );
+ script.text = code;
+ document.head.appendChild( script ).parentNode.removeChild( script );
+ } else {
+
+ // Otherwise, avoid the DOM node creation, insertion
+ // and removal by using an indirect global eval
+
+ indirect( code );
+ }
+ }
+ },
+
+ // Convert dashed to camelCase; used by the css and data modules
+ // Support: IE9-11+
+ // Microsoft forgot to hump their vendor prefix (#9572)
+ camelCase: function( string ) {
+ return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase );
+ },
+
+ nodeName: function( elem, name ) {
+ return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();
+ },
+
+ each: function( obj, callback ) {
+ var length, i = 0;
+
+ if ( isArrayLike( obj ) ) {
+ length = obj.length;
+ for ( ; i < length; i++ ) {
+ if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {
+ break;
+ }
+ }
+ } else {
+ for ( i in obj ) {
+ if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {
+ break;
+ }
+ }
+ }
+
+ return obj;
+ },
+
+ // Support: Android<4.1
+ trim: function( text ) {
+ return text == null ?
+ "" :
+ ( text + "" ).replace( rtrim, "" );
+ },
+
+ // results is for internal usage only
+ makeArray: function( arr, results ) {
+ var ret = results || [];
+
+ if ( arr != null ) {
+ if ( isArrayLike( Object( arr ) ) ) {
+ jQuery.merge( ret,
+ typeof arr === "string" ?
+ [ arr ] : arr
+ );
+ } else {
+ push.call( ret, arr );
+ }
+ }
+
+ return ret;
+ },
+
+ inArray: function( elem, arr, i ) {
+ return arr == null ? -1 : indexOf.call( arr, elem, i );
+ },
+
+ merge: function( first, second ) {
+ var len = +second.length,
+ j = 0,
+ i = first.length;
+
+ for ( ; j < len; j++ ) {
+ first[ i++ ] = second[ j ];
+ }
+
+ first.length = i;
+
+ return first;
+ },
+
+ grep: function( elems, callback, invert ) {
+ var callbackInverse,
+ matches = [],
+ i = 0,
+ length = elems.length,
+ callbackExpect = !invert;
+
+ // Go through the array, only saving the items
+ // that pass the validator function
+ for ( ; i < length; i++ ) {
+ callbackInverse = !callback( elems[ i ], i );
+ if ( callbackInverse !== callbackExpect ) {
+ matches.push( elems[ i ] );
+ }
+ }
+
+ return matches;
+ },
+
+ // arg is for internal usage only
+ map: function( elems, callback, arg ) {
+ var length, value,
+ i = 0,
+ ret = [];
+
+ // Go through the array, translating each of the items to their new values
+ if ( isArrayLike( elems ) ) {
+ length = elems.length;
+ for ( ; i < length; i++ ) {
+ value = callback( elems[ i ], i, arg );
+
+ if ( value != null ) {
+ ret.push( value );
+ }
+ }
+
+ // Go through every key on the object,
+ } else {
+ for ( i in elems ) {
+ value = callback( elems[ i ], i, arg );
+
+ if ( value != null ) {
+ ret.push( value );
+ }
+ }
+ }
+
+ // Flatten any nested arrays
+ return concat.apply( [], ret );
+ },
+
+ // A global GUID counter for objects
+ guid: 1,
+
+ // Bind a function to a context, optionally partially applying any
+ // arguments.
+ proxy: function( fn, context ) {
+ var tmp, args, proxy;
+
+ if ( typeof context === "string" ) {
+ tmp = fn[ context ];
+ context = fn;
+ fn = tmp;
+ }
+
+ // Quick check to determine if target is callable, in the spec
+ // this throws a TypeError, but we will just return undefined.
+ if ( !jQuery.isFunction( fn ) ) {
+ return undefined;
+ }
+
+ // Simulated bind
+ args = slice.call( arguments, 2 );
+ proxy = function() {
+ return fn.apply( context || this, args.concat( slice.call( arguments ) ) );
+ };
+
+ // Set the guid of unique handler to the same of original handler, so it can be removed
+ proxy.guid = fn.guid = fn.guid || jQuery.guid++;
+
+ return proxy;
+ },
+
+ now: Date.now,
+
+ // jQuery.support is not used in Core but other projects attach their
+ // properties to it so it needs to exist.
+ support: support
+} );
+
+// JSHint would error on this code due to the Symbol not being defined in ES5.
+// Defining this global in .jshintrc would create a danger of using the global
+// unguarded in another place, it seems safer to just disable JSHint for these
+// three lines.
+/* jshint ignore: start */
+if ( typeof Symbol === "function" ) {
+ jQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ];
+}
+/* jshint ignore: end */
+
+// Populate the class2type map
+jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ),
+function( i, name ) {
+ class2type[ "[object " + name + "]" ] = name.toLowerCase();
+} );
+
+function isArrayLike( obj ) {
+
+ // Support: iOS 8.2 (not reproducible in simulator)
+ // `in` check used to prevent JIT error (gh-2145)
+ // hasOwn isn't used here due to false negatives
+ // regarding Nodelist length in IE
+ var length = !!obj && "length" in obj && obj.length,
+ type = jQuery.type( obj );
+
+ if ( type === "function" || jQuery.isWindow( obj ) ) {
+ return false;
+ }
+
+ return type === "array" || length === 0 ||
+ typeof length === "number" && length > 0 && ( length - 1 ) in obj;
+}
+var Sizzle =
+/*!
+ * Sizzle CSS Selector Engine v2.2.1
+ * http://sizzlejs.com/
+ *
+ * Copyright jQuery Foundation and other contributors
+ * Released under the MIT license
+ * http://jquery.org/license
+ *
+ * Date: 2015-10-17
+ */
+(function( window ) {
+
+var i,
+ support,
+ Expr,
+ getText,
+ isXML,
+ tokenize,
+ compile,
+ select,
+ outermostContext,
+ sortInput,
+ hasDuplicate,
+
+ // Local document vars
+ setDocument,
+ document,
+ docElem,
+ documentIsHTML,
+ rbuggyQSA,
+ rbuggyMatches,
+ matches,
+ contains,
+
+ // Instance-specific data
+ expando = "sizzle" + 1 * new Date(),
+ preferredDoc = window.document,
+ dirruns = 0,
+ done = 0,
+ classCache = createCache(),
+ tokenCache = createCache(),
+ compilerCache = createCache(),
+ sortOrder = function( a, b ) {
+ if ( a === b ) {
+ hasDuplicate = true;
+ }
+ return 0;
+ },
+
+ // General-purpose constants
+ MAX_NEGATIVE = 1 << 31,
+
+ // Instance methods
+ hasOwn = ({}).hasOwnProperty,
+ arr = [],
+ pop = arr.pop,
+ push_native = arr.push,
+ push = arr.push,
+ slice = arr.slice,
+ // Use a stripped-down indexOf as it's faster than native
+ // http://jsperf.com/thor-indexof-vs-for/5
+ indexOf = function( list, elem ) {
+ var i = 0,
+ len = list.length;
+ for ( ; i < len; i++ ) {
+ if ( list[i] === elem ) {
+ return i;
+ }
+ }
+ return -1;
+ },
+
+ booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",
+
+ // Regular expressions
+
+ // http://www.w3.org/TR/css3-selectors/#whitespace
+ whitespace = "[\\x20\\t\\r\\n\\f]",
+
+ // http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier
+ identifier = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",
+
+ // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors
+ attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace +
+ // Operator (capture 2)
+ "*([*^$|!~]?=)" + whitespace +
+ // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]"
+ "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace +
+ "*\\]",
+
+ pseudos = ":(" + identifier + ")(?:\\((" +
+ // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments:
+ // 1. quoted (capture 3; capture 4 or capture 5)
+ "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" +
+ // 2. simple (capture 6)
+ "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" +
+ // 3. anything else (capture 2)
+ ".*" +
+ ")\\)|)",
+
+ // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter
+ rwhitespace = new RegExp( whitespace + "+", "g" ),
+ rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ),
+
+ rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ),
+ rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ),
+
+ rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*?)" + whitespace + "*\\]", "g" ),
+
+ rpseudo = new RegExp( pseudos ),
+ ridentifier = new RegExp( "^" + identifier + "$" ),
+
+ matchExpr = {
+ "ID": new RegExp( "^#(" + identifier + ")" ),
+ "CLASS": new RegExp( "^\\.(" + identifier + ")" ),
+ "TAG": new RegExp( "^(" + identifier + "|[*])" ),
+ "ATTR": new RegExp( "^" + attributes ),
+ "PSEUDO": new RegExp( "^" + pseudos ),
+ "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace +
+ "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace +
+ "*(\\d+)|))" + whitespace + "*\\)|)", "i" ),
+ "bool": new RegExp( "^(?:" + booleans + ")$", "i" ),
+ // For use in libraries implementing .is()
+ // We use this for POS matching in `select`
+ "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" +
+ whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" )
+ },
+
+ rinputs = /^(?:input|select|textarea|button)$/i,
+ rheader = /^h\d$/i,
+
+ rnative = /^[^{]+\{\s*\[native \w/,
+
+ // Easily-parseable/retrievable ID or TAG or CLASS selectors
+ rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,
+
+ rsibling = /[+~]/,
+ rescape = /'|\\/g,
+
+ // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters
+ runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ),
+ funescape = function( _, escaped, escapedWhitespace ) {
+ var high = "0x" + escaped - 0x10000;
+ // NaN means non-codepoint
+ // Support: Firefox<24
+ // Workaround erroneous numeric interpretation of +"0x"
+ return high !== high || escapedWhitespace ?
+ escaped :
+ high < 0 ?
+ // BMP codepoint
+ String.fromCharCode( high + 0x10000 ) :
+ // Supplemental Plane codepoint (surrogate pair)
+ String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );
+ },
+
+ // Used for iframes
+ // See setDocument()
+ // Removing the function wrapper causes a "Permission Denied"
+ // error in IE
+ unloadHandler = function() {
+ setDocument();
+ };
+
+// Optimize for push.apply( _, NodeList )
+try {
+ push.apply(
+ (arr = slice.call( preferredDoc.childNodes )),
+ preferredDoc.childNodes
+ );
+ // Support: Android<4.0
+ // Detect silently failing push.apply
+ arr[ preferredDoc.childNodes.length ].nodeType;
+} catch ( e ) {
+ push = { apply: arr.length ?
+
+ // Leverage slice if possible
+ function( target, els ) {
+ push_native.apply( target, slice.call(els) );
+ } :
+
+ // Support: IE<9
+ // Otherwise append directly
+ function( target, els ) {
+ var j = target.length,
+ i = 0;
+ // Can't trust NodeList.length
+ while ( (target[j++] = els[i++]) ) {}
+ target.length = j - 1;
+ }
+ };
+}
+
+function Sizzle( selector, context, results, seed ) {
+ var m, i, elem, nid, nidselect, match, groups, newSelector,
+ newContext = context && context.ownerDocument,
+
+ // nodeType defaults to 9, since context defaults to document
+ nodeType = context ? context.nodeType : 9;
+
+ results = results || [];
+
+ // Return early from calls with invalid selector or context
+ if ( typeof selector !== "string" || !selector ||
+ nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) {
+
+ return results;
+ }
+
+ // Try to shortcut find operations (as opposed to filters) in HTML documents
+ if ( !seed ) {
+
+ if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) {
+ setDocument( context );
+ }
+ context = context || document;
+
+ if ( documentIsHTML ) {
+
+ // If the selector is sufficiently simple, try using a "get*By*" DOM method
+ // (excepting DocumentFragment context, where the methods don't exist)
+ if ( nodeType !== 11 && (match = rquickExpr.exec( selector )) ) {
+
+ // ID selector
+ if ( (m = match[1]) ) {
+
+ // Document context
+ if ( nodeType === 9 ) {
+ if ( (elem = context.getElementById( m )) ) {
+
+ // Support: IE, Opera, Webkit
+ // TODO: identify versions
+ // getElementById can match elements by name instead of ID
+ if ( elem.id === m ) {
+ results.push( elem );
+ return results;
+ }
+ } else {
+ return results;
+ }
+
+ // Element context
+ } else {
+
+ // Support: IE, Opera, Webkit
+ // TODO: identify versions
+ // getElementById can match elements by name instead of ID
+ if ( newContext && (elem = newContext.getElementById( m )) &&
+ contains( context, elem ) &&
+ elem.id === m ) {
+
+ results.push( elem );
+ return results;
+ }
+ }
+
+ // Type selector
+ } else if ( match[2] ) {
+ push.apply( results, context.getElementsByTagName( selector ) );
+ return results;
+
+ // Class selector
+ } else if ( (m = match[3]) && support.getElementsByClassName &&
+ context.getElementsByClassName ) {
+
+ push.apply( results, context.getElementsByClassName( m ) );
+ return results;
+ }
+ }
+
+ // Take advantage of querySelectorAll
+ if ( support.qsa &&
+ !compilerCache[ selector + " " ] &&
+ (!rbuggyQSA || !rbuggyQSA.test( selector )) ) {
+
+ if ( nodeType !== 1 ) {
+ newContext = context;
+ newSelector = selector;
+
+ // qSA looks outside Element context, which is not what we want
+ // Thanks to Andrew Dupont for this workaround technique
+ // Support: IE <=8
+ // Exclude object elements
+ } else if ( context.nodeName.toLowerCase() !== "object" ) {
+
+ // Capture the context ID, setting it first if necessary
+ if ( (nid = context.getAttribute( "id" )) ) {
+ nid = nid.replace( rescape, "\\$&" );
+ } else {
+ context.setAttribute( "id", (nid = expando) );
+ }
+
+ // Prefix every selector in the list
+ groups = tokenize( selector );
+ i = groups.length;
+ nidselect = ridentifier.test( nid ) ? "#" + nid : "[id='" + nid + "']";
+ while ( i-- ) {
+ groups[i] = nidselect + " " + toSelector( groups[i] );
+ }
+ newSelector = groups.join( "," );
+
+ // Expand context for sibling selectors
+ newContext = rsibling.test( selector ) && testContext( context.parentNode ) ||
+ context;
+ }
+
+ if ( newSelector ) {
+ try {
+ push.apply( results,
+ newContext.querySelectorAll( newSelector )
+ );
+ return results;
+ } catch ( qsaError ) {
+ } finally {
+ if ( nid === expando ) {
+ context.removeAttribute( "id" );
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // All others
+ return select( selector.replace( rtrim, "$1" ), context, results, seed );
+}
+
+/**
+ * Create key-value caches of limited size
+ * @returns {function(string, object)} Returns the Object data after storing it on itself with
+ * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength)
+ * deleting the oldest entry
+ */
+function createCache() {
+ var keys = [];
+
+ function cache( key, value ) {
+ // Use (key + " ") to avoid collision with native prototype properties (see Issue #157)
+ if ( keys.push( key + " " ) > Expr.cacheLength ) {
+ // Only keep the most recent entries
+ delete cache[ keys.shift() ];
+ }
+ return (cache[ key + " " ] = value);
+ }
+ return cache;
+}
+
+/**
+ * Mark a function for special use by Sizzle
+ * @param {Function} fn The function to mark
+ */
+function markFunction( fn ) {
+ fn[ expando ] = true;
+ return fn;
+}
+
+/**
+ * Support testing using an element
+ * @param {Function} fn Passed the created div and expects a boolean result
+ */
+function assert( fn ) {
+ var div = document.createElement("div");
+
+ try {
+ return !!fn( div );
+ } catch (e) {
+ return false;
+ } finally {
+ // Remove from its parent by default
+ if ( div.parentNode ) {
+ div.parentNode.removeChild( div );
+ }
+ // release memory in IE
+ div = null;
+ }
+}
+
+/**
+ * Adds the same handler for all of the specified attrs
+ * @param {String} attrs Pipe-separated list of attributes
+ * @param {Function} handler The method that will be applied
+ */
+function addHandle( attrs, handler ) {
+ var arr = attrs.split("|"),
+ i = arr.length;
+
+ while ( i-- ) {
+ Expr.attrHandle[ arr[i] ] = handler;
+ }
+}
+
+/**
+ * Checks document order of two siblings
+ * @param {Element} a
+ * @param {Element} b
+ * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b
+ */
+function siblingCheck( a, b ) {
+ var cur = b && a,
+ diff = cur && a.nodeType === 1 && b.nodeType === 1 &&
+ ( ~b.sourceIndex || MAX_NEGATIVE ) -
+ ( ~a.sourceIndex || MAX_NEGATIVE );
+
+ // Use IE sourceIndex if available on both nodes
+ if ( diff ) {
+ return diff;
+ }
+
+ // Check if b follows a
+ if ( cur ) {
+ while ( (cur = cur.nextSibling) ) {
+ if ( cur === b ) {
+ return -1;
+ }
+ }
+ }
+
+ return a ? 1 : -1;
+}
+
+/**
+ * Returns a function to use in pseudos for input types
+ * @param {String} type
+ */
+function createInputPseudo( type ) {
+ return function( elem ) {
+ var name = elem.nodeName.toLowerCase();
+ return name === "input" && elem.type === type;
+ };
+}
+
+/**
+ * Returns a function to use in pseudos for buttons
+ * @param {String} type
+ */
+function createButtonPseudo( type ) {
+ return function( elem ) {
+ var name = elem.nodeName.toLowerCase();
+ return (name === "input" || name === "button") && elem.type === type;
+ };
+}
+
+/**
+ * Returns a function to use in pseudos for positionals
+ * @param {Function} fn
+ */
+function createPositionalPseudo( fn ) {
+ return markFunction(function( argument ) {
+ argument = +argument;
+ return markFunction(function( seed, matches ) {
+ var j,
+ matchIndexes = fn( [], seed.length, argument ),
+ i = matchIndexes.length;
+
+ // Match elements found at the specified indexes
+ while ( i-- ) {
+ if ( seed[ (j = matchIndexes[i]) ] ) {
+ seed[j] = !(matches[j] = seed[j]);
+ }
+ }
+ });
+ });
+}
+
+/**
+ * Checks a node for validity as a Sizzle context
+ * @param {Element|Object=} context
+ * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value
+ */
+function testContext( context ) {
+ return context && typeof context.getElementsByTagName !== "undefined" && context;
+}
+
+// Expose support vars for convenience
+support = Sizzle.support = {};
+
+/**
+ * Detects XML nodes
+ * @param {Element|Object} elem An element or a document
+ * @returns {Boolean} True iff elem is a non-HTML XML node
+ */
+isXML = Sizzle.isXML = function( elem ) {
+ // documentElement is verified for cases where it doesn't yet exist
+ // (such as loading iframes in IE - #4833)
+ var documentElement = elem && (elem.ownerDocument || elem).documentElement;
+ return documentElement ? documentElement.nodeName !== "HTML" : false;
+};
+
+/**
+ * Sets document-related variables once based on the current document
+ * @param {Element|Object} [doc] An element or document object to use to set the document
+ * @returns {Object} Returns the current document
+ */
+setDocument = Sizzle.setDocument = function( node ) {
+ var hasCompare, parent,
+ doc = node ? node.ownerDocument || node : preferredDoc;
+
+ // Return early if doc is invalid or already selected
+ if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) {
+ return document;
+ }
+
+ // Update global variables
+ document = doc;
+ docElem = document.documentElement;
+ documentIsHTML = !isXML( document );
+
+ // Support: IE 9-11, Edge
+ // Accessing iframe documents after unload throws "permission denied" errors (jQuery #13936)
+ if ( (parent = document.defaultView) && parent.top !== parent ) {
+ // Support: IE 11
+ if ( parent.addEventListener ) {
+ parent.addEventListener( "unload", unloadHandler, false );
+
+ // Support: IE 9 - 10 only
+ } else if ( parent.attachEvent ) {
+ parent.attachEvent( "onunload", unloadHandler );
+ }
+ }
+
+ /* Attributes
+ ---------------------------------------------------------------------- */
+
+ // Support: IE<8
+ // Verify that getAttribute really returns attributes and not properties
+ // (excepting IE8 booleans)
+ support.attributes = assert(function( div ) {
+ div.className = "i";
+ return !div.getAttribute("className");
+ });
+
+ /* getElement(s)By*
+ ---------------------------------------------------------------------- */
+
+ // Check if getElementsByTagName("*") returns only elements
+ support.getElementsByTagName = assert(function( div ) {
+ div.appendChild( document.createComment("") );
+ return !div.getElementsByTagName("*").length;
+ });
+
+ // Support: IE<9
+ support.getElementsByClassName = rnative.test( document.getElementsByClassName );
+
+ // Support: IE<10
+ // Check if getElementById returns elements by name
+ // The broken getElementById methods don't pick up programatically-set names,
+ // so use a roundabout getElementsByName test
+ support.getById = assert(function( div ) {
+ docElem.appendChild( div ).id = expando;
+ return !document.getElementsByName || !document.getElementsByName( expando ).length;
+ });
+
+ // ID find and filter
+ if ( support.getById ) {
+ Expr.find["ID"] = function( id, context ) {
+ if ( typeof context.getElementById !== "undefined" && documentIsHTML ) {
+ var m = context.getElementById( id );
+ return m ? [ m ] : [];
+ }
+ };
+ Expr.filter["ID"] = function( id ) {
+ var attrId = id.replace( runescape, funescape );
+ return function( elem ) {
+ return elem.getAttribute("id") === attrId;
+ };
+ };
+ } else {
+ // Support: IE6/7
+ // getElementById is not reliable as a find shortcut
+ delete Expr.find["ID"];
+
+ Expr.filter["ID"] = function( id ) {
+ var attrId = id.replace( runescape, funescape );
+ return function( elem ) {
+ var node = typeof elem.getAttributeNode !== "undefined" &&
+ elem.getAttributeNode("id");
+ return node && node.value === attrId;
+ };
+ };
+ }
+
+ // Tag
+ Expr.find["TAG"] = support.getElementsByTagName ?
+ function( tag, context ) {
+ if ( typeof context.getElementsByTagName !== "undefined" ) {
+ return context.getElementsByTagName( tag );
+
+ // DocumentFragment nodes don't have gEBTN
+ } else if ( support.qsa ) {
+ return context.querySelectorAll( tag );
+ }
+ } :
+
+ function( tag, context ) {
+ var elem,
+ tmp = [],
+ i = 0,
+ // By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too
+ results = context.getElementsByTagName( tag );
+
+ // Filter out possible comments
+ if ( tag === "*" ) {
+ while ( (elem = results[i++]) ) {
+ if ( elem.nodeType === 1 ) {
+ tmp.push( elem );
+ }
+ }
+
+ return tmp;
+ }
+ return results;
+ };
+
+ // Class
+ Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) {
+ if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) {
+ return context.getElementsByClassName( className );
+ }
+ };
+
+ /* QSA/matchesSelector
+ ---------------------------------------------------------------------- */
+
+ // QSA and matchesSelector support
+
+ // matchesSelector(:active) reports false when true (IE9/Opera 11.5)
+ rbuggyMatches = [];
+
+ // qSa(:focus) reports false when true (Chrome 21)
+ // We allow this because of a bug in IE8/9 that throws an error
+ // whenever `document.activeElement` is accessed on an iframe
+ // So, we allow :focus to pass through QSA all the time to avoid the IE error
+ // See http://bugs.jquery.com/ticket/13378
+ rbuggyQSA = [];
+
+ if ( (support.qsa = rnative.test( document.querySelectorAll )) ) {
+ // Build QSA regex
+ // Regex strategy adopted from Diego Perini
+ assert(function( div ) {
+ // Select is set to empty string on purpose
+ // This is to test IE's treatment of not explicitly
+ // setting a boolean content attribute,
+ // since its presence should be enough
+ // http://bugs.jquery.com/ticket/12359
+ docElem.appendChild( div ).innerHTML = "<a id='" + expando + "'></a>" +
+ "<select id='" + expando + "-\r\\' msallowcapture=''>" +
+ "<option selected=''></option></select>";
+
+ // Support: IE8, Opera 11-12.16
+ // Nothing should be selected when empty strings follow ^= or $= or *=
+ // The test attribute must be unknown in Opera but "safe" for WinRT
+ // http://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section
+ if ( div.querySelectorAll("[msallowcapture^='']").length ) {
+ rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" );
+ }
+
+ // Support: IE8
+ // Boolean attributes and "value" are not treated correctly
+ if ( !div.querySelectorAll("[selected]").length ) {
+ rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" );
+ }
+
+ // Support: Chrome<29, Android<4.4, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.8+
+ if ( !div.querySelectorAll( "[id~=" + expando + "-]" ).length ) {
+ rbuggyQSA.push("~=");
+ }
+
+ // Webkit/Opera - :checked should return selected option elements
+ // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
+ // IE8 throws error here and will not see later tests
+ if ( !div.querySelectorAll(":checked").length ) {
+ rbuggyQSA.push(":checked");
+ }
+
+ // Support: Safari 8+, iOS 8+
+ // https://bugs.webkit.org/show_bug.cgi?id=136851
+ // In-page `selector#id sibing-combinator selector` fails
+ if ( !div.querySelectorAll( "a#" + expando + "+*" ).length ) {
+ rbuggyQSA.push(".#.+[+~]");
+ }
+ });
+
+ assert(function( div ) {
+ // Support: Windows 8 Native Apps
+ // The type and name attributes are restricted during .innerHTML assignment
+ var input = document.createElement("input");
+ input.setAttribute( "type", "hidden" );
+ div.appendChild( input ).setAttribute( "name", "D" );
+
+ // Support: IE8
+ // Enforce case-sensitivity of name attribute
+ if ( div.querySelectorAll("[name=d]").length ) {
+ rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" );
+ }
+
+ // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled)
+ // IE8 throws error here and will not see later tests
+ if ( !div.querySelectorAll(":enabled").length ) {
+ rbuggyQSA.push( ":enabled", ":disabled" );
+ }
+
+ // Opera 10-11 does not throw on post-comma invalid pseudos
+ div.querySelectorAll("*,:x");
+ rbuggyQSA.push(",.*:");
+ });
+ }
+
+ if ( (support.matchesSelector = rnative.test( (matches = docElem.matches ||
+ docElem.webkitMatchesSelector ||
+ docElem.mozMatchesSelector ||
+ docElem.oMatchesSelector ||
+ docElem.msMatchesSelector) )) ) {
+
+ assert(function( div ) {
+ // Check to see if it's possible to do matchesSelector
+ // on a disconnected node (IE 9)
+ support.disconnectedMatch = matches.call( div, "div" );
+
+ // This should fail with an exception
+ // Gecko does not error, returns false instead
+ matches.call( div, "[s!='']:x" );
+ rbuggyMatches.push( "!=", pseudos );
+ });
+ }
+
+ rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") );
+ rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") );
+
+ /* Contains
+ ---------------------------------------------------------------------- */
+ hasCompare = rnative.test( docElem.compareDocumentPosition );
+
+ // Element contains another
+ // Purposefully self-exclusive
+ // As in, an element does not contain itself
+ contains = hasCompare || rnative.test( docElem.contains ) ?
+ function( a, b ) {
+ var adown = a.nodeType === 9 ? a.documentElement : a,
+ bup = b && b.parentNode;
+ return a === bup || !!( bup && bup.nodeType === 1 && (
+ adown.contains ?
+ adown.contains( bup ) :
+ a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16
+ ));
+ } :
+ function( a, b ) {
+ if ( b ) {
+ while ( (b = b.parentNode) ) {
+ if ( b === a ) {
+ return true;
+ }
+ }
+ }
+ return false;
+ };
+
+ /* Sorting
+ ---------------------------------------------------------------------- */
+
+ // Document order sorting
+ sortOrder = hasCompare ?
+ function( a, b ) {
+
+ // Flag for duplicate removal
+ if ( a === b ) {
+ hasDuplicate = true;
+ return 0;
+ }
+
+ // Sort on method existence if only one input has compareDocumentPosition
+ var compare = !a.compareDocumentPosition - !b.compareDocumentPosition;
+ if ( compare ) {
+ return compare;
+ }
+
+ // Calculate position if both inputs belong to the same document
+ compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ?
+ a.compareDocumentPosition( b ) :
+
+ // Otherwise we know they are disconnected
+ 1;
+
+ // Disconnected nodes
+ if ( compare & 1 ||
+ (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) {
+
+ // Choose the first element that is related to our preferred document
+ if ( a === document || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) {
+ return -1;
+ }
+ if ( b === document || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) {
+ return 1;
+ }
+
+ // Maintain original order
+ return sortInput ?
+ ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) :
+ 0;
+ }
+
+ return compare & 4 ? -1 : 1;
+ } :
+ function( a, b ) {
+ // Exit early if the nodes are identical
+ if ( a === b ) {
+ hasDuplicate = true;
+ return 0;
+ }
+
+ var cur,
+ i = 0,
+ aup = a.parentNode,
+ bup = b.parentNode,
+ ap = [ a ],
+ bp = [ b ];
+
+ // Parentless nodes are either documents or disconnected
+ if ( !aup || !bup ) {
+ return a === document ? -1 :
+ b === document ? 1 :
+ aup ? -1 :
+ bup ? 1 :
+ sortInput ?
+ ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) :
+ 0;
+
+ // If the nodes are siblings, we can do a quick check
+ } else if ( aup === bup ) {
+ return siblingCheck( a, b );
+ }
+
+ // Otherwise we need full lists of their ancestors for comparison
+ cur = a;
+ while ( (cur = cur.parentNode) ) {
+ ap.unshift( cur );
+ }
+ cur = b;
+ while ( (cur = cur.parentNode) ) {
+ bp.unshift( cur );
+ }
+
+ // Walk down the tree looking for a discrepancy
+ while ( ap[i] === bp[i] ) {
+ i++;
+ }
+
+ return i ?
+ // Do a sibling check if the nodes have a common ancestor
+ siblingCheck( ap[i], bp[i] ) :
+
+ // Otherwise nodes in our document sort first
+ ap[i] === preferredDoc ? -1 :
+ bp[i] === preferredDoc ? 1 :
+ 0;
+ };
+
+ return document;
+};
+
+Sizzle.matches = function( expr, elements ) {
+ return Sizzle( expr, null, null, elements );
+};
+
+Sizzle.matchesSelector = function( elem, expr ) {
+ // Set document vars if needed
+ if ( ( elem.ownerDocument || elem ) !== document ) {
+ setDocument( elem );
+ }
+
+ // Make sure that attribute selectors are quoted
+ expr = expr.replace( rattributeQuotes, "='$1']" );
+
+ if ( support.matchesSelector && documentIsHTML &&
+ !compilerCache[ expr + " " ] &&
+ ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) &&
+ ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) {
+
+ try {
+ var ret = matches.call( elem, expr );
+
+ // IE 9's matchesSelector returns false on disconnected nodes
+ if ( ret || support.disconnectedMatch ||
+ // As well, disconnected nodes are said to be in a document
+ // fragment in IE 9
+ elem.document && elem.document.nodeType !== 11 ) {
+ return ret;
+ }
+ } catch (e) {}
+ }
+
+ return Sizzle( expr, document, null, [ elem ] ).length > 0;
+};
+
+Sizzle.contains = function( context, elem ) {
+ // Set document vars if needed
+ if ( ( context.ownerDocument || context ) !== document ) {
+ setDocument( context );
+ }
+ return contains( context, elem );
+};
+
+Sizzle.attr = function( elem, name ) {
+ // Set document vars if needed
+ if ( ( elem.ownerDocument || elem ) !== document ) {
+ setDocument( elem );
+ }
+
+ var fn = Expr.attrHandle[ name.toLowerCase() ],
+ // Don't get fooled by Object.prototype properties (jQuery #13807)
+ val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ?
+ fn( elem, name, !documentIsHTML ) :
+ undefined;
+
+ return val !== undefined ?
+ val :
+ support.attributes || !documentIsHTML ?
+ elem.getAttribute( name ) :
+ (val = elem.getAttributeNode(name)) && val.specified ?
+ val.value :
+ null;
+};
+
+Sizzle.error = function( msg ) {
+ throw new Error( "Syntax error, unrecognized expression: " + msg );
+};
+
+/**
+ * Document sorting and removing duplicates
+ * @param {ArrayLike} results
+ */
+Sizzle.uniqueSort = function( results ) {
+ var elem,
+ duplicates = [],
+ j = 0,
+ i = 0;
+
+ // Unless we *know* we can detect duplicates, assume their presence
+ hasDuplicate = !support.detectDuplicates;
+ sortInput = !support.sortStable && results.slice( 0 );
+ results.sort( sortOrder );
+
+ if ( hasDuplicate ) {
+ while ( (elem = results[i++]) ) {
+ if ( elem === results[ i ] ) {
+ j = duplicates.push( i );
+ }
+ }
+ while ( j-- ) {
+ results.splice( duplicates[ j ], 1 );
+ }
+ }
+
+ // Clear input after sorting to release objects
+ // See https://github.com/jquery/sizzle/pull/225
+ sortInput = null;
+
+ return results;
+};
+
+/**
+ * Utility function for retrieving the text value of an array of DOM nodes
+ * @param {Array|Element} elem
+ */
+getText = Sizzle.getText = function( elem ) {
+ var node,
+ ret = "",
+ i = 0,
+ nodeType = elem.nodeType;
+
+ if ( !nodeType ) {
+ // If no nodeType, this is expected to be an array
+ while ( (node = elem[i++]) ) {
+ // Do not traverse comment nodes
+ ret += getText( node );
+ }
+ } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {
+ // Use textContent for elements
+ // innerText usage removed for consistency of new lines (jQuery #11153)
+ if ( typeof elem.textContent === "string" ) {
+ return elem.textContent;
+ } else {
+ // Traverse its children
+ for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
+ ret += getText( elem );
+ }
+ }
+ } else if ( nodeType === 3 || nodeType === 4 ) {
+ return elem.nodeValue;
+ }
+ // Do not include comment or processing instruction nodes
+
+ return ret;
+};
+
+Expr = Sizzle.selectors = {
+
+ // Can be adjusted by the user
+ cacheLength: 50,
+
+ createPseudo: markFunction,
+
+ match: matchExpr,
+
+ attrHandle: {},
+
+ find: {},
+
+ relative: {
+ ">": { dir: "parentNode", first: true },
+ " ": { dir: "parentNode" },
+ "+": { dir: "previousSibling", first: true },
+ "~": { dir: "previousSibling" }
+ },
+
+ preFilter: {
+ "ATTR": function( match ) {
+ match[1] = match[1].replace( runescape, funescape );
+
+ // Move the given value to match[3] whether quoted or unquoted
+ match[3] = ( match[3] || match[4] || match[5] || "" ).replace( runescape, funescape );
+
+ if ( match[2] === "~=" ) {
+ match[3] = " " + match[3] + " ";
+ }
+
+ return match.slice( 0, 4 );
+ },
+
+ "CHILD": function( match ) {
+ /* matches from matchExpr["CHILD"]
+ 1 type (only|nth|...)
+ 2 what (child|of-type)
+ 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...)
+ 4 xn-component of xn+y argument ([+-]?\d*n|)
+ 5 sign of xn-component
+ 6 x of xn-component
+ 7 sign of y-component
+ 8 y of y-component
+ */
+ match[1] = match[1].toLowerCase();
+
+ if ( match[1].slice( 0, 3 ) === "nth" ) {
+ // nth-* requires argument
+ if ( !match[3] ) {
+ Sizzle.error( match[0] );
+ }
+
+ // numeric x and y parameters for Expr.filter.CHILD
+ // remember that false/true cast respectively to 0/1
+ match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) );
+ match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" );
+
+ // other types prohibit arguments
+ } else if ( match[3] ) {
+ Sizzle.error( match[0] );
+ }
+
+ return match;
+ },
+
+ "PSEUDO": function( match ) {
+ var excess,
+ unquoted = !match[6] && match[2];
+
+ if ( matchExpr["CHILD"].test( match[0] ) ) {
+ return null;
+ }
+
+ // Accept quoted arguments as-is
+ if ( match[3] ) {
+ match[2] = match[4] || match[5] || "";
+
+ // Strip excess characters from unquoted arguments
+ } else if ( unquoted && rpseudo.test( unquoted ) &&
+ // Get excess from tokenize (recursively)
+ (excess = tokenize( unquoted, true )) &&
+ // advance to the next closing parenthesis
+ (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) {
+
+ // excess is a negative index
+ match[0] = match[0].slice( 0, excess );
+ match[2] = unquoted.slice( 0, excess );
+ }
+
+ // Return only captures needed by the pseudo filter method (type and argument)
+ return match.slice( 0, 3 );
+ }
+ },
+
+ filter: {
+
+ "TAG": function( nodeNameSelector ) {
+ var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase();
+ return nodeNameSelector === "*" ?
+ function() { return true; } :
+ function( elem ) {
+ return elem.nodeName && elem.nodeName.toLowerCase() === nodeName;
+ };
+ },
+
+ "CLASS": function( className ) {
+ var pattern = classCache[ className + " " ];
+
+ return pattern ||
+ (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) &&
+ classCache( className, function( elem ) {
+ return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== "undefined" && elem.getAttribute("class") || "" );
+ });
+ },
+
+ "ATTR": function( name, operator, check ) {
+ return function( elem ) {
+ var result = Sizzle.attr( elem, name );
+
+ if ( result == null ) {
+ return operator === "!=";
+ }
+ if ( !operator ) {
+ return true;
+ }
+
+ result += "";
+
+ return operator === "=" ? result === check :
+ operator === "!=" ? result !== check :
+ operator === "^=" ? check && result.indexOf( check ) === 0 :
+ operator === "*=" ? check && result.indexOf( check ) > -1 :
+ operator === "$=" ? check && result.slice( -check.length ) === check :
+ operator === "~=" ? ( " " + result.replace( rwhitespace, " " ) + " " ).indexOf( check ) > -1 :
+ operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" :
+ false;
+ };
+ },
+
+ "CHILD": function( type, what, argument, first, last ) {
+ var simple = type.slice( 0, 3 ) !== "nth",
+ forward = type.slice( -4 ) !== "last",
+ ofType = what === "of-type";
+
+ return first === 1 && last === 0 ?
+
+ // Shortcut for :nth-*(n)
+ function( elem ) {
+ return !!elem.parentNode;
+ } :
+
+ function( elem, context, xml ) {
+ var cache, uniqueCache, outerCache, node, nodeIndex, start,
+ dir = simple !== forward ? "nextSibling" : "previousSibling",
+ parent = elem.parentNode,
+ name = ofType && elem.nodeName.toLowerCase(),
+ useCache = !xml && !ofType,
+ diff = false;
+
+ if ( parent ) {
+
+ // :(first|last|only)-(child|of-type)
+ if ( simple ) {
+ while ( dir ) {
+ node = elem;
+ while ( (node = node[ dir ]) ) {
+ if ( ofType ?
+ node.nodeName.toLowerCase() === name :
+ node.nodeType === 1 ) {
+
+ return false;
+ }
+ }
+ // Reverse direction for :only-* (if we haven't yet done so)
+ start = dir = type === "only" && !start && "nextSibling";
+ }
+ return true;
+ }
+
+ start = [ forward ? parent.firstChild : parent.lastChild ];
+
+ // non-xml :nth-child(...) stores cache data on `parent`
+ if ( forward && useCache ) {
+
+ // Seek `elem` from a previously-cached index
+
+ // ...in a gzip-friendly way
+ node = parent;
+ outerCache = node[ expando ] || (node[ expando ] = {});
+
+ // Support: IE <9 only
+ // Defend against cloned attroperties (jQuery gh-1709)
+ uniqueCache = outerCache[ node.uniqueID ] ||
+ (outerCache[ node.uniqueID ] = {});
+
+ cache = uniqueCache[ type ] || [];
+ nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ];
+ diff = nodeIndex && cache[ 2 ];
+ node = nodeIndex && parent.childNodes[ nodeIndex ];
+
+ while ( (node = ++nodeIndex && node && node[ dir ] ||
+
+ // Fallback to seeking `elem` from the start
+ (diff = nodeIndex = 0) || start.pop()) ) {
+
+ // When found, cache indexes on `parent` and break
+ if ( node.nodeType === 1 && ++diff && node === elem ) {
+ uniqueCache[ type ] = [ dirruns, nodeIndex, diff ];
+ break;
+ }
+ }
+
+ } else {
+ // Use previously-cached element index if available
+ if ( useCache ) {
+ // ...in a gzip-friendly way
+ node = elem;
+ outerCache = node[ expando ] || (node[ expando ] = {});
+
+ // Support: IE <9 only
+ // Defend against cloned attroperties (jQuery gh-1709)
+ uniqueCache = outerCache[ node.uniqueID ] ||
+ (outerCache[ node.uniqueID ] = {});
+
+ cache = uniqueCache[ type ] || [];
+ nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ];
+ diff = nodeIndex;
+ }
+
+ // xml :nth-child(...)
+ // or :nth-last-child(...) or :nth(-last)?-of-type(...)
+ if ( diff === false ) {
+ // Use the same loop as above to seek `elem` from the start
+ while ( (node = ++nodeIndex && node && node[ dir ] ||
+ (diff = nodeIndex = 0) || start.pop()) ) {
+
+ if ( ( ofType ?
+ node.nodeName.toLowerCase() === name :
+ node.nodeType === 1 ) &&
+ ++diff ) {
+
+ // Cache the index of each encountered element
+ if ( useCache ) {
+ outerCache = node[ expando ] || (node[ expando ] = {});
+
+ // Support: IE <9 only
+ // Defend against cloned attroperties (jQuery gh-1709)
+ uniqueCache = outerCache[ node.uniqueID ] ||
+ (outerCache[ node.uniqueID ] = {});
+
+ uniqueCache[ type ] = [ dirruns, diff ];
+ }
+
+ if ( node === elem ) {
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ // Incorporate the offset, then check against cycle size
+ diff -= last;
+ return diff === first || ( diff % first === 0 && diff / first >= 0 );
+ }
+ };
+ },
+
+ "PSEUDO": function( pseudo, argument ) {
+ // pseudo-class names are case-insensitive
+ // http://www.w3.org/TR/selectors/#pseudo-classes
+ // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters
+ // Remember that setFilters inherits from pseudos
+ var args,
+ fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] ||
+ Sizzle.error( "unsupported pseudo: " + pseudo );
+
+ // The user may use createPseudo to indicate that
+ // arguments are needed to create the filter function
+ // just as Sizzle does
+ if ( fn[ expando ] ) {
+ return fn( argument );
+ }
+
+ // But maintain support for old signatures
+ if ( fn.length > 1 ) {
+ args = [ pseudo, pseudo, "", argument ];
+ return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ?
+ markFunction(function( seed, matches ) {
+ var idx,
+ matched = fn( seed, argument ),
+ i = matched.length;
+ while ( i-- ) {
+ idx = indexOf( seed, matched[i] );
+ seed[ idx ] = !( matches[ idx ] = matched[i] );
+ }
+ }) :
+ function( elem ) {
+ return fn( elem, 0, args );
+ };
+ }
+
+ return fn;
+ }
+ },
+
+ pseudos: {
+ // Potentially complex pseudos
+ "not": markFunction(function( selector ) {
+ // Trim the selector passed to compile
+ // to avoid treating leading and trailing
+ // spaces as combinators
+ var input = [],
+ results = [],
+ matcher = compile( selector.replace( rtrim, "$1" ) );
+
+ return matcher[ expando ] ?
+ markFunction(function( seed, matches, context, xml ) {
+ var elem,
+ unmatched = matcher( seed, null, xml, [] ),
+ i = seed.length;
+
+ // Match elements unmatched by `matcher`
+ while ( i-- ) {
+ if ( (elem = unmatched[i]) ) {
+ seed[i] = !(matches[i] = elem);
+ }
+ }
+ }) :
+ function( elem, context, xml ) {
+ input[0] = elem;
+ matcher( input, null, xml, results );
+ // Don't keep the element (issue #299)
+ input[0] = null;
+ return !results.pop();
+ };
+ }),
+
+ "has": markFunction(function( selector ) {
+ return function( elem ) {
+ return Sizzle( selector, elem ).length > 0;
+ };
+ }),
+
+ "contains": markFunction(function( text ) {
+ text = text.replace( runescape, funescape );
+ return function( elem ) {
+ return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1;
+ };
+ }),
+
+ // "Whether an element is represented by a :lang() selector
+ // is based solely on the element's language value
+ // being equal to the identifier C,
+ // or beginning with the identifier C immediately followed by "-".
+ // The matching of C against the element's language value is performed case-insensitively.
+ // The identifier C does not have to be a valid language name."
+ // http://www.w3.org/TR/selectors/#lang-pseudo
+ "lang": markFunction( function( lang ) {
+ // lang value must be a valid identifier
+ if ( !ridentifier.test(lang || "") ) {
+ Sizzle.error( "unsupported lang: " + lang );
+ }
+ lang = lang.replace( runescape, funescape ).toLowerCase();
+ return function( elem ) {
+ var elemLang;
+ do {
+ if ( (elemLang = documentIsHTML ?
+ elem.lang :
+ elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) {
+
+ elemLang = elemLang.toLowerCase();
+ return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0;
+ }
+ } while ( (elem = elem.parentNode) && elem.nodeType === 1 );
+ return false;
+ };
+ }),
+
+ // Miscellaneous
+ "target": function( elem ) {
+ var hash = window.location && window.location.hash;
+ return hash && hash.slice( 1 ) === elem.id;
+ },
+
+ "root": function( elem ) {
+ return elem === docElem;
+ },
+
+ "focus": function( elem ) {
+ return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex);
+ },
+
+ // Boolean properties
+ "enabled": function( elem ) {
+ return elem.disabled === false;
+ },
+
+ "disabled": function( elem ) {
+ return elem.disabled === true;
+ },
+
+ "checked": function( elem ) {
+ // In CSS3, :checked should return both checked and selected elements
+ // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
+ var nodeName = elem.nodeName.toLowerCase();
+ return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected);
+ },
+
+ "selected": function( elem ) {
+ // Accessing this property makes selected-by-default
+ // options in Safari work properly
+ if ( elem.parentNode ) {
+ elem.parentNode.selectedIndex;
+ }
+
+ return elem.selected === true;
+ },
+
+ // Contents
+ "empty": function( elem ) {
+ // http://www.w3.org/TR/selectors/#empty-pseudo
+ // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5),
+ // but not by others (comment: 8; processing instruction: 7; etc.)
+ // nodeType < 6 works because attributes (2) do not appear as children
+ for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
+ if ( elem.nodeType < 6 ) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+ "parent": function( elem ) {
+ return !Expr.pseudos["empty"]( elem );
+ },
+
+ // Element/input types
+ "header": function( elem ) {
+ return rheader.test( elem.nodeName );
+ },
+
+ "input": function( elem ) {
+ return rinputs.test( elem.nodeName );
+ },
+
+ "button": function( elem ) {
+ var name = elem.nodeName.toLowerCase();
+ return name === "input" && elem.type === "button" || name === "button";
+ },
+
+ "text": function( elem ) {
+ var attr;
+ return elem.nodeName.toLowerCase() === "input" &&
+ elem.type === "text" &&
+
+ // Support: IE<8
+ // New HTML5 attribute values (e.g., "search") appear with elem.type === "text"
+ ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text" );
+ },
+
+ // Position-in-collection
+ "first": createPositionalPseudo(function() {
+ return [ 0 ];
+ }),
+
+ "last": createPositionalPseudo(function( matchIndexes, length ) {
+ return [ length - 1 ];
+ }),
+
+ "eq": createPositionalPseudo(function( matchIndexes, length, argument ) {
+ return [ argument < 0 ? argument + length : argument ];
+ }),
+
+ "even": createPositionalPseudo(function( matchIndexes, length ) {
+ var i = 0;
+ for ( ; i < length; i += 2 ) {
+ matchIndexes.push( i );
+ }
+ return matchIndexes;
+ }),
+
+ "odd": createPositionalPseudo(function( matchIndexes, length ) {
+ var i = 1;
+ for ( ; i < length; i += 2 ) {
+ matchIndexes.push( i );
+ }
+ return matchIndexes;
+ }),
+
+ "lt": createPositionalPseudo(function( matchIndexes, length, argument ) {
+ var i = argument < 0 ? argument + length : argument;
+ for ( ; --i >= 0; ) {
+ matchIndexes.push( i );
+ }
+ return matchIndexes;
+ }),
+
+ "gt": createPositionalPseudo(function( matchIndexes, length, argument ) {
+ var i = argument < 0 ? argument + length : argument;
+ for ( ; ++i < length; ) {
+ matchIndexes.push( i );
+ }
+ return matchIndexes;
+ })
+ }
+};
+
+Expr.pseudos["nth"] = Expr.pseudos["eq"];
+
+// Add button/input type pseudos
+for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) {
+ Expr.pseudos[ i ] = createInputPseudo( i );
+}
+for ( i in { submit: true, reset: true } ) {
+ Expr.pseudos[ i ] = createButtonPseudo( i );
+}
+
+// Easy API for creating new setFilters
+function setFilters() {}
+setFilters.prototype = Expr.filters = Expr.pseudos;
+Expr.setFilters = new setFilters();
+
+tokenize = Sizzle.tokenize = function( selector, parseOnly ) {
+ var matched, match, tokens, type,
+ soFar, groups, preFilters,
+ cached = tokenCache[ selector + " " ];
+
+ if ( cached ) {
+ return parseOnly ? 0 : cached.slice( 0 );
+ }
+
+ soFar = selector;
+ groups = [];
+ preFilters = Expr.preFilter;
+
+ while ( soFar ) {
+
+ // Comma and first run
+ if ( !matched || (match = rcomma.exec( soFar )) ) {
+ if ( match ) {
+ // Don't consume trailing commas as valid
+ soFar = soFar.slice( match[0].length ) || soFar;
+ }
+ groups.push( (tokens = []) );
+ }
+
+ matched = false;
+
+ // Combinators
+ if ( (match = rcombinators.exec( soFar )) ) {
+ matched = match.shift();
+ tokens.push({
+ value: matched,
+ // Cast descendant combinators to space
+ type: match[0].replace( rtrim, " " )
+ });
+ soFar = soFar.slice( matched.length );
+ }
+
+ // Filters
+ for ( type in Expr.filter ) {
+ if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] ||
+ (match = preFilters[ type ]( match ))) ) {
+ matched = match.shift();
+ tokens.push({
+ value: matched,
+ type: type,
+ matches: match
+ });
+ soFar = soFar.slice( matched.length );
+ }
+ }
+
+ if ( !matched ) {
+ break;
+ }
+ }
+
+ // Return the length of the invalid excess
+ // if we're just parsing
+ // Otherwise, throw an error or return tokens
+ return parseOnly ?
+ soFar.length :
+ soFar ?
+ Sizzle.error( selector ) :
+ // Cache the tokens
+ tokenCache( selector, groups ).slice( 0 );
+};
+
+function toSelector( tokens ) {
+ var i = 0,
+ len = tokens.length,
+ selector = "";
+ for ( ; i < len; i++ ) {
+ selector += tokens[i].value;
+ }
+ return selector;
+}
+
+function addCombinator( matcher, combinator, base ) {
+ var dir = combinator.dir,
+ checkNonElements = base && dir === "parentNode",
+ doneName = done++;
+
+ return combinator.first ?
+ // Check against closest ancestor/preceding element
+ function( elem, context, xml ) {
+ while ( (elem = elem[ dir ]) ) {
+ if ( elem.nodeType === 1 || checkNonElements ) {
+ return matcher( elem, context, xml );
+ }
+ }
+ } :
+
+ // Check against all ancestor/preceding elements
+ function( elem, context, xml ) {
+ var oldCache, uniqueCache, outerCache,
+ newCache = [ dirruns, doneName ];
+
+ // We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching
+ if ( xml ) {
+ while ( (elem = elem[ dir ]) ) {
+ if ( elem.nodeType === 1 || checkNonElements ) {
+ if ( matcher( elem, context, xml ) ) {
+ return true;
+ }
+ }
+ }
+ } else {
+ while ( (elem = elem[ dir ]) ) {
+ if ( elem.nodeType === 1 || checkNonElements ) {
+ outerCache = elem[ expando ] || (elem[ expando ] = {});
+
+ // Support: IE <9 only
+ // Defend against cloned attroperties (jQuery gh-1709)
+ uniqueCache = outerCache[ elem.uniqueID ] || (outerCache[ elem.uniqueID ] = {});
+
+ if ( (oldCache = uniqueCache[ dir ]) &&
+ oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) {
+
+ // Assign to newCache so results back-propagate to previous elements
+ return (newCache[ 2 ] = oldCache[ 2 ]);
+ } else {
+ // Reuse newcache so results back-propagate to previous elements
+ uniqueCache[ dir ] = newCache;
+
+ // A match means we're done; a fail means we have to keep checking
+ if ( (newCache[ 2 ] = matcher( elem, context, xml )) ) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+ };
+}
+
+function elementMatcher( matchers ) {
+ return matchers.length > 1 ?
+ function( elem, context, xml ) {
+ var i = matchers.length;
+ while ( i-- ) {
+ if ( !matchers[i]( elem, context, xml ) ) {
+ return false;
+ }
+ }
+ return true;
+ } :
+ matchers[0];
+}
+
+function multipleContexts( selector, contexts, results ) {
+ var i = 0,
+ len = contexts.length;
+ for ( ; i < len; i++ ) {
+ Sizzle( selector, contexts[i], results );
+ }
+ return results;
+}
+
+function condense( unmatched, map, filter, context, xml ) {
+ var elem,
+ newUnmatched = [],
+ i = 0,
+ len = unmatched.length,
+ mapped = map != null;
+
+ for ( ; i < len; i++ ) {
+ if ( (elem = unmatched[i]) ) {
+ if ( !filter || filter( elem, context, xml ) ) {
+ newUnmatched.push( elem );
+ if ( mapped ) {
+ map.push( i );
+ }
+ }
+ }
+ }
+
+ return newUnmatched;
+}
+
+function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) {
+ if ( postFilter && !postFilter[ expando ] ) {
+ postFilter = setMatcher( postFilter );
+ }
+ if ( postFinder && !postFinder[ expando ] ) {
+ postFinder = setMatcher( postFinder, postSelector );
+ }
+ return markFunction(function( seed, results, context, xml ) {
+ var temp, i, elem,
+ preMap = [],
+ postMap = [],
+ preexisting = results.length,
+
+ // Get initial elements from seed or context
+ elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ),
+
+ // Prefilter to get matcher input, preserving a map for seed-results synchronization
+ matcherIn = preFilter && ( seed || !selector ) ?
+ condense( elems, preMap, preFilter, context, xml ) :
+ elems,
+
+ matcherOut = matcher ?
+ // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results,
+ postFinder || ( seed ? preFilter : preexisting || postFilter ) ?
+
+ // ...intermediate processing is necessary
+ [] :
+
+ // ...otherwise use results directly
+ results :
+ matcherIn;
+
+ // Find primary matches
+ if ( matcher ) {
+ matcher( matcherIn, matcherOut, context, xml );
+ }
+
+ // Apply postFilter
+ if ( postFilter ) {
+ temp = condense( matcherOut, postMap );
+ postFilter( temp, [], context, xml );
+
+ // Un-match failing elements by moving them back to matcherIn
+ i = temp.length;
+ while ( i-- ) {
+ if ( (elem = temp[i]) ) {
+ matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem);
+ }
+ }
+ }
+
+ if ( seed ) {
+ if ( postFinder || preFilter ) {
+ if ( postFinder ) {
+ // Get the final matcherOut by condensing this intermediate into postFinder contexts
+ temp = [];
+ i = matcherOut.length;
+ while ( i-- ) {
+ if ( (elem = matcherOut[i]) ) {
+ // Restore matcherIn since elem is not yet a final match
+ temp.push( (matcherIn[i] = elem) );
+ }
+ }
+ postFinder( null, (matcherOut = []), temp, xml );
+ }
+
+ // Move matched elements from seed to results to keep them synchronized
+ i = matcherOut.length;
+ while ( i-- ) {
+ if ( (elem = matcherOut[i]) &&
+ (temp = postFinder ? indexOf( seed, elem ) : preMap[i]) > -1 ) {
+
+ seed[temp] = !(results[temp] = elem);
+ }
+ }
+ }
+
+ // Add elements to results, through postFinder if defined
+ } else {
+ matcherOut = condense(
+ matcherOut === results ?
+ matcherOut.splice( preexisting, matcherOut.length ) :
+ matcherOut
+ );
+ if ( postFinder ) {
+ postFinder( null, results, matcherOut, xml );
+ } else {
+ push.apply( results, matcherOut );
+ }
+ }
+ });
+}
+
+function matcherFromTokens( tokens ) {
+ var checkContext, matcher, j,
+ len = tokens.length,
+ leadingRelative = Expr.relative[ tokens[0].type ],
+ implicitRelative = leadingRelative || Expr.relative[" "],
+ i = leadingRelative ? 1 : 0,
+
+ // The foundational matcher ensures that elements are reachable from top-level context(s)
+ matchContext = addCombinator( function( elem ) {
+ return elem === checkContext;
+ }, implicitRelative, true ),
+ matchAnyContext = addCombinator( function( elem ) {
+ return indexOf( checkContext, elem ) > -1;
+ }, implicitRelative, true ),
+ matchers = [ function( elem, context, xml ) {
+ var ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || (
+ (checkContext = context).nodeType ?
+ matchContext( elem, context, xml ) :
+ matchAnyContext( elem, context, xml ) );
+ // Avoid hanging onto element (issue #299)
+ checkContext = null;
+ return ret;
+ } ];
+
+ for ( ; i < len; i++ ) {
+ if ( (matcher = Expr.relative[ tokens[i].type ]) ) {
+ matchers = [ addCombinator(elementMatcher( matchers ), matcher) ];
+ } else {
+ matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches );
+
+ // Return special upon seeing a positional matcher
+ if ( matcher[ expando ] ) {
+ // Find the next relative operator (if any) for proper handling
+ j = ++i;
+ for ( ; j < len; j++ ) {
+ if ( Expr.relative[ tokens[j].type ] ) {
+ break;
+ }
+ }
+ return setMatcher(
+ i > 1 && elementMatcher( matchers ),
+ i > 1 && toSelector(
+ // If the preceding token was a descendant combinator, insert an implicit any-element `*`
+ tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" })
+ ).replace( rtrim, "$1" ),
+ matcher,
+ i < j && matcherFromTokens( tokens.slice( i, j ) ),
+ j < len && matcherFromTokens( (tokens = tokens.slice( j )) ),
+ j < len && toSelector( tokens )
+ );
+ }
+ matchers.push( matcher );
+ }
+ }
+
+ return elementMatcher( matchers );
+}
+
+function matcherFromGroupMatchers( elementMatchers, setMatchers ) {
+ var bySet = setMatchers.length > 0,
+ byElement = elementMatchers.length > 0,
+ superMatcher = function( seed, context, xml, results, outermost ) {
+ var elem, j, matcher,
+ matchedCount = 0,
+ i = "0",
+ unmatched = seed && [],
+ setMatched = [],
+ contextBackup = outermostContext,
+ // We must always have either seed elements or outermost context
+ elems = seed || byElement && Expr.find["TAG"]( "*", outermost ),
+ // Use integer dirruns iff this is the outermost matcher
+ dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1),
+ len = elems.length;
+
+ if ( outermost ) {
+ outermostContext = context === document || context || outermost;
+ }
+
+ // Add elements passing elementMatchers directly to results
+ // Support: IE<9, Safari
+ // Tolerate NodeList properties (IE: "length"; Safari: <number>) matching elements by id
+ for ( ; i !== len && (elem = elems[i]) != null; i++ ) {
+ if ( byElement && elem ) {
+ j = 0;
+ if ( !context && elem.ownerDocument !== document ) {
+ setDocument( elem );
+ xml = !documentIsHTML;
+ }
+ while ( (matcher = elementMatchers[j++]) ) {
+ if ( matcher( elem, context || document, xml) ) {
+ results.push( elem );
+ break;
+ }
+ }
+ if ( outermost ) {
+ dirruns = dirrunsUnique;
+ }
+ }
+
+ // Track unmatched elements for set filters
+ if ( bySet ) {
+ // They will have gone through all possible matchers
+ if ( (elem = !matcher && elem) ) {
+ matchedCount--;
+ }
+
+ // Lengthen the array for every element, matched or not
+ if ( seed ) {
+ unmatched.push( elem );
+ }
+ }
+ }
+
+ // `i` is now the count of elements visited above, and adding it to `matchedCount`
+ // makes the latter nonnegative.
+ matchedCount += i;
+
+ // Apply set filters to unmatched elements
+ // NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount`
+ // equals `i`), unless we didn't visit _any_ elements in the above loop because we have
+ // no element matchers and no seed.
+ // Incrementing an initially-string "0" `i` allows `i` to remain a string only in that
+ // case, which will result in a "00" `matchedCount` that differs from `i` but is also
+ // numerically zero.
+ if ( bySet && i !== matchedCount ) {
+ j = 0;
+ while ( (matcher = setMatchers[j++]) ) {
+ matcher( unmatched, setMatched, context, xml );
+ }
+
+ if ( seed ) {
+ // Reintegrate element matches to eliminate the need for sorting
+ if ( matchedCount > 0 ) {
+ while ( i-- ) {
+ if ( !(unmatched[i] || setMatched[i]) ) {
+ setMatched[i] = pop.call( results );
+ }
+ }
+ }
+
+ // Discard index placeholder values to get only actual matches
+ setMatched = condense( setMatched );
+ }
+
+ // Add matches to results
+ push.apply( results, setMatched );
+
+ // Seedless set matches succeeding multiple successful matchers stipulate sorting
+ if ( outermost && !seed && setMatched.length > 0 &&
+ ( matchedCount + setMatchers.length ) > 1 ) {
+
+ Sizzle.uniqueSort( results );
+ }
+ }
+
+ // Override manipulation of globals by nested matchers
+ if ( outermost ) {
+ dirruns = dirrunsUnique;
+ outermostContext = contextBackup;
+ }
+
+ return unmatched;
+ };
+
+ return bySet ?
+ markFunction( superMatcher ) :
+ superMatcher;
+}
+
+compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) {
+ var i,
+ setMatchers = [],
+ elementMatchers = [],
+ cached = compilerCache[ selector + " " ];
+
+ if ( !cached ) {
+ // Generate a function of recursive functions that can be used to check each element
+ if ( !match ) {
+ match = tokenize( selector );
+ }
+ i = match.length;
+ while ( i-- ) {
+ cached = matcherFromTokens( match[i] );
+ if ( cached[ expando ] ) {
+ setMatchers.push( cached );
+ } else {
+ elementMatchers.push( cached );
+ }
+ }
+
+ // Cache the compiled function
+ cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) );
+
+ // Save selector and tokenization
+ cached.selector = selector;
+ }
+ return cached;
+};
+
+/**
+ * A low-level selection function that works with Sizzle's compiled
+ * selector functions
+ * @param {String|Function} selector A selector or a pre-compiled
+ * selector function built with Sizzle.compile
+ * @param {Element} context
+ * @param {Array} [results]
+ * @param {Array} [seed] A set of elements to match against
+ */
+select = Sizzle.select = function( selector, context, results, seed ) {
+ var i, tokens, token, type, find,
+ compiled = typeof selector === "function" && selector,
+ match = !seed && tokenize( (selector = compiled.selector || selector) );
+
+ results = results || [];
+
+ // Try to minimize operations if there is only one selector in the list and no seed
+ // (the latter of which guarantees us context)
+ if ( match.length === 1 ) {
+
+ // Reduce context if the leading compound selector is an ID
+ tokens = match[0] = match[0].slice( 0 );
+ if ( tokens.length > 2 && (token = tokens[0]).type === "ID" &&
+ support.getById && context.nodeType === 9 && documentIsHTML &&
+ Expr.relative[ tokens[1].type ] ) {
+
+ context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0];
+ if ( !context ) {
+ return results;
+
+ // Precompiled matchers will still verify ancestry, so step up a level
+ } else if ( compiled ) {
+ context = context.parentNode;
+ }
+
+ selector = selector.slice( tokens.shift().value.length );
+ }
+
+ // Fetch a seed set for right-to-left matching
+ i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length;
+ while ( i-- ) {
+ token = tokens[i];
+
+ // Abort if we hit a combinator
+ if ( Expr.relative[ (type = token.type) ] ) {
+ break;
+ }
+ if ( (find = Expr.find[ type ]) ) {
+ // Search, expanding context for leading sibling combinators
+ if ( (seed = find(
+ token.matches[0].replace( runescape, funescape ),
+ rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context
+ )) ) {
+
+ // If seed is empty or no tokens remain, we can return early
+ tokens.splice( i, 1 );
+ selector = seed.length && toSelector( tokens );
+ if ( !selector ) {
+ push.apply( results, seed );
+ return results;
+ }
+
+ break;
+ }
+ }
+ }
+ }
+
+ // Compile and execute a filtering function if one is not provided
+ // Provide `match` to avoid retokenization if we modified the selector above
+ ( compiled || compile( selector, match ) )(
+ seed,
+ context,
+ !documentIsHTML,
+ results,
+ !context || rsibling.test( selector ) && testContext( context.parentNode ) || context
+ );
+ return results;
+};
+
+// One-time assignments
+
+// Sort stability
+support.sortStable = expando.split("").sort( sortOrder ).join("") === expando;
+
+// Support: Chrome 14-35+
+// Always assume duplicates if they aren't passed to the comparison function
+support.detectDuplicates = !!hasDuplicate;
+
+// Initialize against the default document
+setDocument();
+
+// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27)
+// Detached nodes confoundingly follow *each other*
+support.sortDetached = assert(function( div1 ) {
+ // Should return 1, but returns 4 (following)
+ return div1.compareDocumentPosition( document.createElement("div") ) & 1;
+});
+
+// Support: IE<8
+// Prevent attribute/property "interpolation"
+// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx
+if ( !assert(function( div ) {
+ div.innerHTML = "<a href='#'></a>";
+ return div.firstChild.getAttribute("href") === "#" ;
+}) ) {
+ addHandle( "type|href|height|width", function( elem, name, isXML ) {
+ if ( !isXML ) {
+ return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 );
+ }
+ });
+}
+
+// Support: IE<9
+// Use defaultValue in place of getAttribute("value")
+if ( !support.attributes || !assert(function( div ) {
+ div.innerHTML = "<input/>";
+ div.firstChild.setAttribute( "value", "" );
+ return div.firstChild.getAttribute( "value" ) === "";
+}) ) {
+ addHandle( "value", function( elem, name, isXML ) {
+ if ( !isXML && elem.nodeName.toLowerCase() === "input" ) {
+ return elem.defaultValue;
+ }
+ });
+}
+
+// Support: IE<9
+// Use getAttributeNode to fetch booleans when getAttribute lies
+if ( !assert(function( div ) {
+ return div.getAttribute("disabled") == null;
+}) ) {
+ addHandle( booleans, function( elem, name, isXML ) {
+ var val;
+ if ( !isXML ) {
+ return elem[ name ] === true ? name.toLowerCase() :
+ (val = elem.getAttributeNode( name )) && val.specified ?
+ val.value :
+ null;
+ }
+ });
+}
+
+return Sizzle;
+
+})( window );
+
+
+
+jQuery.find = Sizzle;
+jQuery.expr = Sizzle.selectors;
+jQuery.expr[ ":" ] = jQuery.expr.pseudos;
+jQuery.uniqueSort = jQuery.unique = Sizzle.uniqueSort;
+jQuery.text = Sizzle.getText;
+jQuery.isXMLDoc = Sizzle.isXML;
+jQuery.contains = Sizzle.contains;
+
+
+
+var dir = function( elem, dir, until ) {
+ var matched = [],
+ truncate = until !== undefined;
+
+ while ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) {
+ if ( elem.nodeType === 1 ) {
+ if ( truncate && jQuery( elem ).is( until ) ) {
+ break;
+ }
+ matched.push( elem );
+ }
+ }
+ return matched;
+};
+
+
+var siblings = function( n, elem ) {
+ var matched = [];
+
+ for ( ; n; n = n.nextSibling ) {
+ if ( n.nodeType === 1 && n !== elem ) {
+ matched.push( n );
+ }
+ }
+
+ return matched;
+};
+
+
+var rneedsContext = jQuery.expr.match.needsContext;
+
+var rsingleTag = ( /^<([\w-]+)\s*\/?>(?:<\/\1>|)$/ );
+
+
+
+var risSimple = /^.[^:#\[\.,]*$/;
+
+// Implement the identical functionality for filter and not
+function winnow( elements, qualifier, not ) {
+ if ( jQuery.isFunction( qualifier ) ) {
+ return jQuery.grep( elements, function( elem, i ) {
+ /* jshint -W018 */
+ return !!qualifier.call( elem, i, elem ) !== not;
+ } );
+
+ }
+
+ if ( qualifier.nodeType ) {
+ return jQuery.grep( elements, function( elem ) {
+ return ( elem === qualifier ) !== not;
+ } );
+
+ }
+
+ if ( typeof qualifier === "string" ) {
+ if ( risSimple.test( qualifier ) ) {
+ return jQuery.filter( qualifier, elements, not );
+ }
+
+ qualifier = jQuery.filter( qualifier, elements );
+ }
+
+ return jQuery.grep( elements, function( elem ) {
+ return ( indexOf.call( qualifier, elem ) > -1 ) !== not;
+ } );
+}
+
+jQuery.filter = function( expr, elems, not ) {
+ var elem = elems[ 0 ];
+
+ if ( not ) {
+ expr = ":not(" + expr + ")";
+ }
+
+ return elems.length === 1 && elem.nodeType === 1 ?
+ jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] :
+ jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) {
+ return elem.nodeType === 1;
+ } ) );
+};
+
+jQuery.fn.extend( {
+ find: function( selector ) {
+ var i,
+ len = this.length,
+ ret = [],
+ self = this;
+
+ if ( typeof selector !== "string" ) {
+ return this.pushStack( jQuery( selector ).filter( function() {
+ for ( i = 0; i < len; i++ ) {
+ if ( jQuery.contains( self[ i ], this ) ) {
+ return true;
+ }
+ }
+ } ) );
+ }
+
+ for ( i = 0; i < len; i++ ) {
+ jQuery.find( selector, self[ i ], ret );
+ }
+
+ // Needed because $( selector, context ) becomes $( context ).find( selector )
+ ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret );
+ ret.selector = this.selector ? this.selector + " " + selector : selector;
+ return ret;
+ },
+ filter: function( selector ) {
+ return this.pushStack( winnow( this, selector || [], false ) );
+ },
+ not: function( selector ) {
+ return this.pushStack( winnow( this, selector || [], true ) );
+ },
+ is: function( selector ) {
+ return !!winnow(
+ this,
+
+ // If this is a positional/relative selector, check membership in the returned set
+ // so $("p:first").is("p:last") won't return true for a doc with two "p".
+ typeof selector === "string" && rneedsContext.test( selector ) ?
+ jQuery( selector ) :
+ selector || [],
+ false
+ ).length;
+ }
+} );
+
+
+// Initialize a jQuery object
+
+
+// A central reference to the root jQuery(document)
+var rootjQuery,
+
+ // A simple way to check for HTML strings
+ // Prioritize #id over <tag> to avoid XSS via location.hash (#9521)
+ // Strict HTML recognition (#11290: must start with <)
+ rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,
+
+ init = jQuery.fn.init = function( selector, context, root ) {
+ var match, elem;
+
+ // HANDLE: $(""), $(null), $(undefined), $(false)
+ if ( !selector ) {
+ return this;
+ }
+
+ // Method init() accepts an alternate rootjQuery
+ // so migrate can support jQuery.sub (gh-2101)
+ root = root || rootjQuery;
+
+ // Handle HTML strings
+ if ( typeof selector === "string" ) {
+ if ( selector[ 0 ] === "<" &&
+ selector[ selector.length - 1 ] === ">" &&
+ selector.length >= 3 ) {
+
+ // Assume that strings that start and end with <> are HTML and skip the regex check
+ match = [ null, selector, null ];
+
+ } else {
+ match = rquickExpr.exec( selector );
+ }
+
+ // Match html or make sure no context is specified for #id
+ if ( match && ( match[ 1 ] || !context ) ) {
+
+ // HANDLE: $(html) -> $(array)
+ if ( match[ 1 ] ) {
+ context = context instanceof jQuery ? context[ 0 ] : context;
+
+ // Option to run scripts is true for back-compat
+ // Intentionally let the error be thrown if parseHTML is not present
+ jQuery.merge( this, jQuery.parseHTML(
+ match[ 1 ],
+ context && context.nodeType ? context.ownerDocument || context : document,
+ true
+ ) );
+
+ // HANDLE: $(html, props)
+ if ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) {
+ for ( match in context ) {
+
+ // Properties of context are called as methods if possible
+ if ( jQuery.isFunction( this[ match ] ) ) {
+ this[ match ]( context[ match ] );
+
+ // ...and otherwise set as attributes
+ } else {
+ this.attr( match, context[ match ] );
+ }
+ }
+ }
+
+ return this;
+
+ // HANDLE: $(#id)
+ } else {
+ elem = document.getElementById( match[ 2 ] );
+
+ // Support: Blackberry 4.6
+ // gEBID returns nodes no longer in the document (#6963)
+ if ( elem && elem.parentNode ) {
+
+ // Inject the element directly into the jQuery object
+ this.length = 1;
+ this[ 0 ] = elem;
+ }
+
+ this.context = document;
+ this.selector = selector;
+ return this;
+ }
+
+ // HANDLE: $(expr, $(...))
+ } else if ( !context || context.jquery ) {
+ return ( context || root ).find( selector );
+
+ // HANDLE: $(expr, context)
+ // (which is just equivalent to: $(context).find(expr)
+ } else {
+ return this.constructor( context ).find( selector );
+ }
+
+ // HANDLE: $(DOMElement)
+ } else if ( selector.nodeType ) {
+ this.context = this[ 0 ] = selector;
+ this.length = 1;
+ return this;
+
+ // HANDLE: $(function)
+ // Shortcut for document ready
+ } else if ( jQuery.isFunction( selector ) ) {
+ return root.ready !== undefined ?
+ root.ready( selector ) :
+
+ // Execute immediately if ready is not present
+ selector( jQuery );
+ }
+
+ if ( selector.selector !== undefined ) {
+ this.selector = selector.selector;
+ this.context = selector.context;
+ }
+
+ return jQuery.makeArray( selector, this );
+ };
+
+// Give the init function the jQuery prototype for later instantiation
+init.prototype = jQuery.fn;
+
+// Initialize central reference
+rootjQuery = jQuery( document );
+
+
+var rparentsprev = /^(?:parents|prev(?:Until|All))/,
+
+ // Methods guaranteed to produce a unique set when starting from a unique set
+ guaranteedUnique = {
+ children: true,
+ contents: true,
+ next: true,
+ prev: true
+ };
+
+jQuery.fn.extend( {
+ has: function( target ) {
+ var targets = jQuery( target, this ),
+ l = targets.length;
+
+ return this.filter( function() {
+ var i = 0;
+ for ( ; i < l; i++ ) {
+ if ( jQuery.contains( this, targets[ i ] ) ) {
+ return true;
+ }
+ }
+ } );
+ },
+
+ closest: function( selectors, context ) {
+ var cur,
+ i = 0,
+ l = this.length,
+ matched = [],
+ pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ?
+ jQuery( selectors, context || this.context ) :
+ 0;
+
+ for ( ; i < l; i++ ) {
+ for ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) {
+
+ // Always skip document fragments
+ if ( cur.nodeType < 11 && ( pos ?
+ pos.index( cur ) > -1 :
+
+ // Don't pass non-elements to Sizzle
+ cur.nodeType === 1 &&
+ jQuery.find.matchesSelector( cur, selectors ) ) ) {
+
+ matched.push( cur );
+ break;
+ }
+ }
+ }
+
+ return this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched );
+ },
+
+ // Determine the position of an element within the set
+ index: function( elem ) {
+
+ // No argument, return index in parent
+ if ( !elem ) {
+ return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1;
+ }
+
+ // Index in selector
+ if ( typeof elem === "string" ) {
+ return indexOf.call( jQuery( elem ), this[ 0 ] );
+ }
+
+ // Locate the position of the desired element
+ return indexOf.call( this,
+
+ // If it receives a jQuery object, the first element is used
+ elem.jquery ? elem[ 0 ] : elem
+ );
+ },
+
+ add: function( selector, context ) {
+ return this.pushStack(
+ jQuery.uniqueSort(
+ jQuery.merge( this.get(), jQuery( selector, context ) )
+ )
+ );
+ },
+
+ addBack: function( selector ) {
+ return this.add( selector == null ?
+ this.prevObject : this.prevObject.filter( selector )
+ );
+ }
+} );
+
+function sibling( cur, dir ) {
+ while ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {}
+ return cur;
+}
+
+jQuery.each( {
+ parent: function( elem ) {
+ var parent = elem.parentNode;
+ return parent && parent.nodeType !== 11 ? parent : null;
+ },
+ parents: function( elem ) {
+ return dir( elem, "parentNode" );
+ },
+ parentsUntil: function( elem, i, until ) {
+ return dir( elem, "parentNode", until );
+ },
+ next: function( elem ) {
+ return sibling( elem, "nextSibling" );
+ },
+ prev: function( elem ) {
+ return sibling( elem, "previousSibling" );
+ },
+ nextAll: function( elem ) {
+ return dir( elem, "nextSibling" );
+ },
+ prevAll: function( elem ) {
+ return dir( elem, "previousSibling" );
+ },
+ nextUntil: function( elem, i, until ) {
+ return dir( elem, "nextSibling", until );
+ },
+ prevUntil: function( elem, i, until ) {
+ return dir( elem, "previousSibling", until );
+ },
+ siblings: function( elem ) {
+ return siblings( ( elem.parentNode || {} ).firstChild, elem );
+ },
+ children: function( elem ) {
+ return siblings( elem.firstChild );
+ },
+ contents: function( elem ) {
+ return elem.contentDocument || jQuery.merge( [], elem.childNodes );
+ }
+}, function( name, fn ) {
+ jQuery.fn[ name ] = function( until, selector ) {
+ var matched = jQuery.map( this, fn, until );
+
+ if ( name.slice( -5 ) !== "Until" ) {
+ selector = until;
+ }
+
+ if ( selector && typeof selector === "string" ) {
+ matched = jQuery.filter( selector, matched );
+ }
+
+ if ( this.length > 1 ) {
+
+ // Remove duplicates
+ if ( !guaranteedUnique[ name ] ) {
+ jQuery.uniqueSort( matched );
+ }
+
+ // Reverse order for parents* and prev-derivatives
+ if ( rparentsprev.test( name ) ) {
+ matched.reverse();
+ }
+ }
+
+ return this.pushStack( matched );
+ };
+} );
+var rnotwhite = ( /\S+/g );
+
+
+
+// Convert String-formatted options into Object-formatted ones
+function createOptions( options ) {
+ var object = {};
+ jQuery.each( options.match( rnotwhite ) || [], function( _, flag ) {
+ object[ flag ] = true;
+ } );
+ return object;
+}
+
+/*
+ * Create a callback list using the following parameters:
+ *
+ * options: an optional list of space-separated options that will change how
+ * the callback list behaves or a more traditional option object
+ *
+ * By default a callback list will act like an event callback list and can be
+ * "fired" multiple times.
+ *
+ * Possible options:
+ *
+ * once: will ensure the callback list can only be fired once (like a Deferred)
+ *
+ * memory: will keep track of previous values and will call any callback added
+ * after the list has been fired right away with the latest "memorized"
+ * values (like a Deferred)
+ *
+ * unique: will ensure a callback can only be added once (no duplicate in the list)
+ *
+ * stopOnFalse: interrupt callings when a callback returns false
+ *
+ */
+jQuery.Callbacks = function( options ) {
+
+ // Convert options from String-formatted to Object-formatted if needed
+ // (we check in cache first)
+ options = typeof options === "string" ?
+ createOptions( options ) :
+ jQuery.extend( {}, options );
+
+ var // Flag to know if list is currently firing
+ firing,
+
+ // Last fire value for non-forgettable lists
+ memory,
+
+ // Flag to know if list was already fired
+ fired,
+
+ // Flag to prevent firing
+ locked,
+
+ // Actual callback list
+ list = [],
+
+ // Queue of execution data for repeatable lists
+ queue = [],
+
+ // Index of currently firing callback (modified by add/remove as needed)
+ firingIndex = -1,
+
+ // Fire callbacks
+ fire = function() {
+
+ // Enforce single-firing
+ locked = options.once;
+
+ // Execute callbacks for all pending executions,
+ // respecting firingIndex overrides and runtime changes
+ fired = firing = true;
+ for ( ; queue.length; firingIndex = -1 ) {
+ memory = queue.shift();
+ while ( ++firingIndex < list.length ) {
+
+ // Run callback and check for early termination
+ if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false &&
+ options.stopOnFalse ) {
+
+ // Jump to end and forget the data so .add doesn't re-fire
+ firingIndex = list.length;
+ memory = false;
+ }
+ }
+ }
+
+ // Forget the data if we're done with it
+ if ( !options.memory ) {
+ memory = false;
+ }
+
+ firing = false;
+
+ // Clean up if we're done firing for good
+ if ( locked ) {
+
+ // Keep an empty list if we have data for future add calls
+ if ( memory ) {
+ list = [];
+
+ // Otherwise, this object is spent
+ } else {
+ list = "";
+ }
+ }
+ },
+
+ // Actual Callbacks object
+ self = {
+
+ // Add a callback or a collection of callbacks to the list
+ add: function() {
+ if ( list ) {
+
+ // If we have memory from a past run, we should fire after adding
+ if ( memory && !firing ) {
+ firingIndex = list.length - 1;
+ queue.push( memory );
+ }
+
+ ( function add( args ) {
+ jQuery.each( args, function( _, arg ) {
+ if ( jQuery.isFunction( arg ) ) {
+ if ( !options.unique || !self.has( arg ) ) {
+ list.push( arg );
+ }
+ } else if ( arg && arg.length && jQuery.type( arg ) !== "string" ) {
+
+ // Inspect recursively
+ add( arg );
+ }
+ } );
+ } )( arguments );
+
+ if ( memory && !firing ) {
+ fire();
+ }
+ }
+ return this;
+ },
+
+ // Remove a callback from the list
+ remove: function() {
+ jQuery.each( arguments, function( _, arg ) {
+ var index;
+ while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {
+ list.splice( index, 1 );
+
+ // Handle firing indexes
+ if ( index <= firingIndex ) {
+ firingIndex--;
+ }
+ }
+ } );
+ return this;
+ },
+
+ // Check if a given callback is in the list.
+ // If no argument is given, return whether or not list has callbacks attached.
+ has: function( fn ) {
+ return fn ?
+ jQuery.inArray( fn, list ) > -1 :
+ list.length > 0;
+ },
+
+ // Remove all callbacks from the list
+ empty: function() {
+ if ( list ) {
+ list = [];
+ }
+ return this;
+ },
+
+ // Disable .fire and .add
+ // Abort any current/pending executions
+ // Clear all callbacks and values
+ disable: function() {
+ locked = queue = [];
+ list = memory = "";
+ return this;
+ },
+ disabled: function() {
+ return !list;
+ },
+
+ // Disable .fire
+ // Also disable .add unless we have memory (since it would have no effect)
+ // Abort any pending executions
+ lock: function() {
+ locked = queue = [];
+ if ( !memory ) {
+ list = memory = "";
+ }
+ return this;
+ },
+ locked: function() {
+ return !!locked;
+ },
+
+ // Call all callbacks with the given context and arguments
+ fireWith: function( context, args ) {
+ if ( !locked ) {
+ args = args || [];
+ args = [ context, args.slice ? args.slice() : args ];
+ queue.push( args );
+ if ( !firing ) {
+ fire();
+ }
+ }
+ return this;
+ },
+
+ // Call all the callbacks with the given arguments
+ fire: function() {
+ self.fireWith( this, arguments );
+ return this;
+ },
+
+ // To know if the callbacks have already been called at least once
+ fired: function() {
+ return !!fired;
+ }
+ };
+
+ return self;
+};
+
+
+jQuery.extend( {
+
+ Deferred: function( func ) {
+ var tuples = [
+
+ // action, add listener, listener list, final state
+ [ "resolve", "done", jQuery.Callbacks( "once memory" ), "resolved" ],
+ [ "reject", "fail", jQuery.Callbacks( "once memory" ), "rejected" ],
+ [ "notify", "progress", jQuery.Callbacks( "memory" ) ]
+ ],
+ state = "pending",
+ promise = {
+ state: function() {
+ return state;
+ },
+ always: function() {
+ deferred.done( arguments ).fail( arguments );
+ return this;
+ },
+ then: function( /* fnDone, fnFail, fnProgress */ ) {
+ var fns = arguments;
+ return jQuery.Deferred( function( newDefer ) {
+ jQuery.each( tuples, function( i, tuple ) {
+ var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ];
+
+ // deferred[ done | fail | progress ] for forwarding actions to newDefer
+ deferred[ tuple[ 1 ] ]( function() {
+ var returned = fn && fn.apply( this, arguments );
+ if ( returned && jQuery.isFunction( returned.promise ) ) {
+ returned.promise()
+ .progress( newDefer.notify )
+ .done( newDefer.resolve )
+ .fail( newDefer.reject );
+ } else {
+ newDefer[ tuple[ 0 ] + "With" ](
+ this === promise ? newDefer.promise() : this,
+ fn ? [ returned ] : arguments
+ );
+ }
+ } );
+ } );
+ fns = null;
+ } ).promise();
+ },
+
+ // Get a promise for this deferred
+ // If obj is provided, the promise aspect is added to the object
+ promise: function( obj ) {
+ return obj != null ? jQuery.extend( obj, promise ) : promise;
+ }
+ },
+ deferred = {};
+
+ // Keep pipe for back-compat
+ promise.pipe = promise.then;
+
+ // Add list-specific methods
+ jQuery.each( tuples, function( i, tuple ) {
+ var list = tuple[ 2 ],
+ stateString = tuple[ 3 ];
+
+ // promise[ done | fail | progress ] = list.add
+ promise[ tuple[ 1 ] ] = list.add;
+
+ // Handle state
+ if ( stateString ) {
+ list.add( function() {
+
+ // state = [ resolved | rejected ]
+ state = stateString;
+
+ // [ reject_list | resolve_list ].disable; progress_list.lock
+ }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock );
+ }
+
+ // deferred[ resolve | reject | notify ]
+ deferred[ tuple[ 0 ] ] = function() {
+ deferred[ tuple[ 0 ] + "With" ]( this === deferred ? promise : this, arguments );
+ return this;
+ };
+ deferred[ tuple[ 0 ] + "With" ] = list.fireWith;
+ } );
+
+ // Make the deferred a promise
+ promise.promise( deferred );
+
+ // Call given func if any
+ if ( func ) {
+ func.call( deferred, deferred );
+ }
+
+ // All done!
+ return deferred;
+ },
+
+ // Deferred helper
+ when: function( subordinate /* , ..., subordinateN */ ) {
+ var i = 0,
+ resolveValues = slice.call( arguments ),
+ length = resolveValues.length,
+
+ // the count of uncompleted subordinates
+ remaining = length !== 1 ||
+ ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0,
+
+ // the master Deferred.
+ // If resolveValues consist of only a single Deferred, just use that.
+ deferred = remaining === 1 ? subordinate : jQuery.Deferred(),
+
+ // Update function for both resolve and progress values
+ updateFunc = function( i, contexts, values ) {
+ return function( value ) {
+ contexts[ i ] = this;
+ values[ i ] = arguments.length > 1 ? slice.call( arguments ) : value;
+ if ( values === progressValues ) {
+ deferred.notifyWith( contexts, values );
+ } else if ( !( --remaining ) ) {
+ deferred.resolveWith( contexts, values );
+ }
+ };
+ },
+
+ progressValues, progressContexts, resolveContexts;
+
+ // Add listeners to Deferred subordinates; treat others as resolved
+ if ( length > 1 ) {
+ progressValues = new Array( length );
+ progressContexts = new Array( length );
+ resolveContexts = new Array( length );
+ for ( ; i < length; i++ ) {
+ if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) {
+ resolveValues[ i ].promise()
+ .progress( updateFunc( i, progressContexts, progressValues ) )
+ .done( updateFunc( i, resolveContexts, resolveValues ) )
+ .fail( deferred.reject );
+ } else {
+ --remaining;
+ }
+ }
+ }
+
+ // If we're not waiting on anything, resolve the master
+ if ( !remaining ) {
+ deferred.resolveWith( resolveContexts, resolveValues );
+ }
+
+ return deferred.promise();
+ }
+} );
+
+
+// The deferred used on DOM ready
+var readyList;
+
+jQuery.fn.ready = function( fn ) {
+
+ // Add the callback
+ jQuery.ready.promise().done( fn );
+
+ return this;
+};
+
+jQuery.extend( {
+
+ // Is the DOM ready to be used? Set to true once it occurs.
+ isReady: false,
+
+ // A counter to track how many items to wait for before
+ // the ready event fires. See #6781
+ readyWait: 1,
+
+ // Hold (or release) the ready event
+ holdReady: function( hold ) {
+ if ( hold ) {
+ jQuery.readyWait++;
+ } else {
+ jQuery.ready( true );
+ }
+ },
+
+ // Handle when the DOM is ready
+ ready: function( wait ) {
+
+ // Abort if there are pending holds or we're already ready
+ if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) {
+ return;
+ }
+
+ // Remember that the DOM is ready
+ jQuery.isReady = true;
+
+ // If a normal DOM Ready event fired, decrement, and wait if need be
+ if ( wait !== true && --jQuery.readyWait > 0 ) {
+ return;
+ }
+
+ // If there are functions bound, to execute
+ readyList.resolveWith( document, [ jQuery ] );
+
+ // Trigger any bound ready events
+ if ( jQuery.fn.triggerHandler ) {
+ jQuery( document ).triggerHandler( "ready" );
+ jQuery( document ).off( "ready" );
+ }
+ }
+} );
+
+/**
+ * The ready event handler and self cleanup method
+ */
+function completed() {
+ document.removeEventListener( "DOMContentLoaded", completed );
+ window.removeEventListener( "load", completed );
+ jQuery.ready();
+}
+
+jQuery.ready.promise = function( obj ) {
+ if ( !readyList ) {
+
+ readyList = jQuery.Deferred();
+
+ // Catch cases where $(document).ready() is called
+ // after the browser event has already occurred.
+ // Support: IE9-10 only
+ // Older IE sometimes signals "interactive" too soon
+ if ( document.readyState === "complete" ||
+ ( document.readyState !== "loading" && !document.documentElement.doScroll ) ) {
+
+ // Handle it asynchronously to allow scripts the opportunity to delay ready
+ window.setTimeout( jQuery.ready );
+
+ } else {
+
+ // Use the handy event callback
+ document.addEventListener( "DOMContentLoaded", completed );
+
+ // A fallback to window.onload, that will always work
+ window.addEventListener( "load", completed );
+ }
+ }
+ return readyList.promise( obj );
+};
+
+// Kick off the DOM ready check even if the user does not
+jQuery.ready.promise();
+
+
+
+
+// Multifunctional method to get and set values of a collection
+// The value/s can optionally be executed if it's a function
+var access = function( elems, fn, key, value, chainable, emptyGet, raw ) {
+ var i = 0,
+ len = elems.length,
+ bulk = key == null;
+
+ // Sets many values
+ if ( jQuery.type( key ) === "object" ) {
+ chainable = true;
+ for ( i in key ) {
+ access( elems, fn, i, key[ i ], true, emptyGet, raw );
+ }
+
+ // Sets one value
+ } else if ( value !== undefined ) {
+ chainable = true;
+
+ if ( !jQuery.isFunction( value ) ) {
+ raw = true;
+ }
+
+ if ( bulk ) {
+
+ // Bulk operations run against the entire set
+ if ( raw ) {
+ fn.call( elems, value );
+ fn = null;
+
+ // ...except when executing function values
+ } else {
+ bulk = fn;
+ fn = function( elem, key, value ) {
+ return bulk.call( jQuery( elem ), value );
+ };
+ }
+ }
+
+ if ( fn ) {
+ for ( ; i < len; i++ ) {
+ fn(
+ elems[ i ], key, raw ?
+ value :
+ value.call( elems[ i ], i, fn( elems[ i ], key ) )
+ );
+ }
+ }
+ }
+
+ return chainable ?
+ elems :
+
+ // Gets
+ bulk ?
+ fn.call( elems ) :
+ len ? fn( elems[ 0 ], key ) : emptyGet;
+};
+var acceptData = function( owner ) {
+
+ // Accepts only:
+ // - Node
+ // - Node.ELEMENT_NODE
+ // - Node.DOCUMENT_NODE
+ // - Object
+ // - Any
+ /* jshint -W018 */
+ return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType );
+};
+
+
+
+
+function Data() {
+ this.expando = jQuery.expando + Data.uid++;
+}
+
+Data.uid = 1;
+
+Data.prototype = {
+
+ register: function( owner, initial ) {
+ var value = initial || {};
+
+ // If it is a node unlikely to be stringify-ed or looped over
+ // use plain assignment
+ if ( owner.nodeType ) {
+ owner[ this.expando ] = value;
+
+ // Otherwise secure it in a non-enumerable, non-writable property
+ // configurability must be true to allow the property to be
+ // deleted with the delete operator
+ } else {
+ Object.defineProperty( owner, this.expando, {
+ value: value,
+ writable: true,
+ configurable: true
+ } );
+ }
+ return owner[ this.expando ];
+ },
+ cache: function( owner ) {
+
+ // We can accept data for non-element nodes in modern browsers,
+ // but we should not, see #8335.
+ // Always return an empty object.
+ if ( !acceptData( owner ) ) {
+ return {};
+ }
+
+ // Check if the owner object already has a cache
+ var value = owner[ this.expando ];
+
+ // If not, create one
+ if ( !value ) {
+ value = {};
+
+ // We can accept data for non-element nodes in modern browsers,
+ // but we should not, see #8335.
+ // Always return an empty object.
+ if ( acceptData( owner ) ) {
+
+ // If it is a node unlikely to be stringify-ed or looped over
+ // use plain assignment
+ if ( owner.nodeType ) {
+ owner[ this.expando ] = value;
+
+ // Otherwise secure it in a non-enumerable property
+ // configurable must be true to allow the property to be
+ // deleted when data is removed
+ } else {
+ Object.defineProperty( owner, this.expando, {
+ value: value,
+ configurable: true
+ } );
+ }
+ }
+ }
+
+ return value;
+ },
+ set: function( owner, data, value ) {
+ var prop,
+ cache = this.cache( owner );
+
+ // Handle: [ owner, key, value ] args
+ if ( typeof data === "string" ) {
+ cache[ data ] = value;
+
+ // Handle: [ owner, { properties } ] args
+ } else {
+
+ // Copy the properties one-by-one to the cache object
+ for ( prop in data ) {
+ cache[ prop ] = data[ prop ];
+ }
+ }
+ return cache;
+ },
+ get: function( owner, key ) {
+ return key === undefined ?
+ this.cache( owner ) :
+ owner[ this.expando ] && owner[ this.expando ][ key ];
+ },
+ access: function( owner, key, value ) {
+ var stored;
+
+ // In cases where either:
+ //
+ // 1. No key was specified
+ // 2. A string key was specified, but no value provided
+ //
+ // Take the "read" path and allow the get method to determine
+ // which value to return, respectively either:
+ //
+ // 1. The entire cache object
+ // 2. The data stored at the key
+ //
+ if ( key === undefined ||
+ ( ( key && typeof key === "string" ) && value === undefined ) ) {
+
+ stored = this.get( owner, key );
+
+ return stored !== undefined ?
+ stored : this.get( owner, jQuery.camelCase( key ) );
+ }
+
+ // When the key is not a string, or both a key and value
+ // are specified, set or extend (existing objects) with either:
+ //
+ // 1. An object of properties
+ // 2. A key and value
+ //
+ this.set( owner, key, value );
+
+ // Since the "set" path can have two possible entry points
+ // return the expected data based on which path was taken[*]
+ return value !== undefined ? value : key;
+ },
+ remove: function( owner, key ) {
+ var i, name, camel,
+ cache = owner[ this.expando ];
+
+ if ( cache === undefined ) {
+ return;
+ }
+
+ if ( key === undefined ) {
+ this.register( owner );
+
+ } else {
+
+ // Support array or space separated string of keys
+ if ( jQuery.isArray( key ) ) {
+
+ // If "name" is an array of keys...
+ // When data is initially created, via ("key", "val") signature,
+ // keys will be converted to camelCase.
+ // Since there is no way to tell _how_ a key was added, remove
+ // both plain key and camelCase key. #12786
+ // This will only penalize the array argument path.
+ name = key.concat( key.map( jQuery.camelCase ) );
+ } else {
+ camel = jQuery.camelCase( key );
+
+ // Try the string as a key before any manipulation
+ if ( key in cache ) {
+ name = [ key, camel ];
+ } else {
+
+ // If a key with the spaces exists, use it.
+ // Otherwise, create an array by matching non-whitespace
+ name = camel;
+ name = name in cache ?
+ [ name ] : ( name.match( rnotwhite ) || [] );
+ }
+ }
+
+ i = name.length;
+
+ while ( i-- ) {
+ delete cache[ name[ i ] ];
+ }
+ }
+
+ // Remove the expando if there's no more data
+ if ( key === undefined || jQuery.isEmptyObject( cache ) ) {
+
+ // Support: Chrome <= 35-45+
+ // Webkit & Blink performance suffers when deleting properties
+ // from DOM nodes, so set to undefined instead
+ // https://code.google.com/p/chromium/issues/detail?id=378607
+ if ( owner.nodeType ) {
+ owner[ this.expando ] = undefined;
+ } else {
+ delete owner[ this.expando ];
+ }
+ }
+ },
+ hasData: function( owner ) {
+ var cache = owner[ this.expando ];
+ return cache !== undefined && !jQuery.isEmptyObject( cache );
+ }
+};
+var dataPriv = new Data();
+
+var dataUser = new Data();
+
+
+
+// Implementation Summary
+//
+// 1. Enforce API surface and semantic compatibility with 1.9.x branch
+// 2. Improve the module's maintainability by reducing the storage
+// paths to a single mechanism.
+// 3. Use the same single mechanism to support "private" and "user" data.
+// 4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData)
+// 5. Avoid exposing implementation details on user objects (eg. expando properties)
+// 6. Provide a clear path for implementation upgrade to WeakMap in 2014
+
+var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,
+ rmultiDash = /[A-Z]/g;
+
+function dataAttr( elem, key, data ) {
+ var name;
+
+ // If nothing was found internally, try to fetch any
+ // data from the HTML5 data-* attribute
+ if ( data === undefined && elem.nodeType === 1 ) {
+ name = "data-" + key.replace( rmultiDash, "-$&" ).toLowerCase();
+ data = elem.getAttribute( name );
+
+ if ( typeof data === "string" ) {
+ try {
+ data = data === "true" ? true :
+ data === "false" ? false :
+ data === "null" ? null :
+
+ // Only convert to a number if it doesn't change the string
+ +data + "" === data ? +data :
+ rbrace.test( data ) ? jQuery.parseJSON( data ) :
+ data;
+ } catch ( e ) {}
+
+ // Make sure we set the data so it isn't changed later
+ dataUser.set( elem, key, data );
+ } else {
+ data = undefined;
+ }
+ }
+ return data;
+}
+
+jQuery.extend( {
+ hasData: function( elem ) {
+ return dataUser.hasData( elem ) || dataPriv.hasData( elem );
+ },
+
+ data: function( elem, name, data ) {
+ return dataUser.access( elem, name, data );
+ },
+
+ removeData: function( elem, name ) {
+ dataUser.remove( elem, name );
+ },
+
+ // TODO: Now that all calls to _data and _removeData have been replaced
+ // with direct calls to dataPriv methods, these can be deprecated.
+ _data: function( elem, name, data ) {
+ return dataPriv.access( elem, name, data );
+ },
+
+ _removeData: function( elem, name ) {
+ dataPriv.remove( elem, name );
+ }
+} );
+
+jQuery.fn.extend( {
+ data: function( key, value ) {
+ var i, name, data,
+ elem = this[ 0 ],
+ attrs = elem && elem.attributes;
+
+ // Gets all values
+ if ( key === undefined ) {
+ if ( this.length ) {
+ data = dataUser.get( elem );
+
+ if ( elem.nodeType === 1 && !dataPriv.get( elem, "hasDataAttrs" ) ) {
+ i = attrs.length;
+ while ( i-- ) {
+
+ // Support: IE11+
+ // The attrs elements can be null (#14894)
+ if ( attrs[ i ] ) {
+ name = attrs[ i ].name;
+ if ( name.indexOf( "data-" ) === 0 ) {
+ name = jQuery.camelCase( name.slice( 5 ) );
+ dataAttr( elem, name, data[ name ] );
+ }
+ }
+ }
+ dataPriv.set( elem, "hasDataAttrs", true );
+ }
+ }
+
+ return data;
+ }
+
+ // Sets multiple values
+ if ( typeof key === "object" ) {
+ return this.each( function() {
+ dataUser.set( this, key );
+ } );
+ }
+
+ return access( this, function( value ) {
+ var data, camelKey;
+
+ // The calling jQuery object (element matches) is not empty
+ // (and therefore has an element appears at this[ 0 ]) and the
+ // `value` parameter was not undefined. An empty jQuery object
+ // will result in `undefined` for elem = this[ 0 ] which will
+ // throw an exception if an attempt to read a data cache is made.
+ if ( elem && value === undefined ) {
+
+ // Attempt to get data from the cache
+ // with the key as-is
+ data = dataUser.get( elem, key ) ||
+
+ // Try to find dashed key if it exists (gh-2779)
+ // This is for 2.2.x only
+ dataUser.get( elem, key.replace( rmultiDash, "-$&" ).toLowerCase() );
+
+ if ( data !== undefined ) {
+ return data;
+ }
+
+ camelKey = jQuery.camelCase( key );
+
+ // Attempt to get data from the cache
+ // with the key camelized
+ data = dataUser.get( elem, camelKey );
+ if ( data !== undefined ) {
+ return data;
+ }
+
+ // Attempt to "discover" the data in
+ // HTML5 custom data-* attrs
+ data = dataAttr( elem, camelKey, undefined );
+ if ( data !== undefined ) {
+ return data;
+ }
+
+ // We tried really hard, but the data doesn't exist.
+ return;
+ }
+
+ // Set the data...
+ camelKey = jQuery.camelCase( key );
+ this.each( function() {
+
+ // First, attempt to store a copy or reference of any
+ // data that might've been store with a camelCased key.
+ var data = dataUser.get( this, camelKey );
+
+ // For HTML5 data-* attribute interop, we have to
+ // store property names with dashes in a camelCase form.
+ // This might not apply to all properties...*
+ dataUser.set( this, camelKey, value );
+
+ // *... In the case of properties that might _actually_
+ // have dashes, we need to also store a copy of that
+ // unchanged property.
+ if ( key.indexOf( "-" ) > -1 && data !== undefined ) {
+ dataUser.set( this, key, value );
+ }
+ } );
+ }, null, value, arguments.length > 1, null, true );
+ },
+
+ removeData: function( key ) {
+ return this.each( function() {
+ dataUser.remove( this, key );
+ } );
+ }
+} );
+
+
+jQuery.extend( {
+ queue: function( elem, type, data ) {
+ var queue;
+
+ if ( elem ) {
+ type = ( type || "fx" ) + "queue";
+ queue = dataPriv.get( elem, type );
+
+ // Speed up dequeue by getting out quickly if this is just a lookup
+ if ( data ) {
+ if ( !queue || jQuery.isArray( data ) ) {
+ queue = dataPriv.access( elem, type, jQuery.makeArray( data ) );
+ } else {
+ queue.push( data );
+ }
+ }
+ return queue || [];
+ }
+ },
+
+ dequeue: function( elem, type ) {
+ type = type || "fx";
+
+ var queue = jQuery.queue( elem, type ),
+ startLength = queue.length,
+ fn = queue.shift(),
+ hooks = jQuery._queueHooks( elem, type ),
+ next = function() {
+ jQuery.dequeue( elem, type );
+ };
+
+ // If the fx queue is dequeued, always remove the progress sentinel
+ if ( fn === "inprogress" ) {
+ fn = queue.shift();
+ startLength--;
+ }
+
+ if ( fn ) {
+
+ // Add a progress sentinel to prevent the fx queue from being
+ // automatically dequeued
+ if ( type === "fx" ) {
+ queue.unshift( "inprogress" );
+ }
+
+ // Clear up the last queue stop function
+ delete hooks.stop;
+ fn.call( elem, next, hooks );
+ }
+
+ if ( !startLength && hooks ) {
+ hooks.empty.fire();
+ }
+ },
+
+ // Not public - generate a queueHooks object, or return the current one
+ _queueHooks: function( elem, type ) {
+ var key = type + "queueHooks";
+ return dataPriv.get( elem, key ) || dataPriv.access( elem, key, {
+ empty: jQuery.Callbacks( "once memory" ).add( function() {
+ dataPriv.remove( elem, [ type + "queue", key ] );
+ } )
+ } );
+ }
+} );
+
+jQuery.fn.extend( {
+ queue: function( type, data ) {
+ var setter = 2;
+
+ if ( typeof type !== "string" ) {
+ data = type;
+ type = "fx";
+ setter--;
+ }
+
+ if ( arguments.length < setter ) {
+ return jQuery.queue( this[ 0 ], type );
+ }
+
+ return data === undefined ?
+ this :
+ this.each( function() {
+ var queue = jQuery.queue( this, type, data );
+
+ // Ensure a hooks for this queue
+ jQuery._queueHooks( this, type );
+
+ if ( type === "fx" && queue[ 0 ] !== "inprogress" ) {
+ jQuery.dequeue( this, type );
+ }
+ } );
+ },
+ dequeue: function( type ) {
+ return this.each( function() {
+ jQuery.dequeue( this, type );
+ } );
+ },
+ clearQueue: function( type ) {
+ return this.queue( type || "fx", [] );
+ },
+
+ // Get a promise resolved when queues of a certain type
+ // are emptied (fx is the type by default)
+ promise: function( type, obj ) {
+ var tmp,
+ count = 1,
+ defer = jQuery.Deferred(),
+ elements = this,
+ i = this.length,
+ resolve = function() {
+ if ( !( --count ) ) {
+ defer.resolveWith( elements, [ elements ] );
+ }
+ };
+
+ if ( typeof type !== "string" ) {
+ obj = type;
+ type = undefined;
+ }
+ type = type || "fx";
+
+ while ( i-- ) {
+ tmp = dataPriv.get( elements[ i ], type + "queueHooks" );
+ if ( tmp && tmp.empty ) {
+ count++;
+ tmp.empty.add( resolve );
+ }
+ }
+ resolve();
+ return defer.promise( obj );
+ }
+} );
+var pnum = ( /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/ ).source;
+
+var rcssNum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" );
+
+
+var cssExpand = [ "Top", "Right", "Bottom", "Left" ];
+
+var isHidden = function( elem, el ) {
+
+ // isHidden might be called from jQuery#filter function;
+ // in that case, element will be second argument
+ elem = el || elem;
+ return jQuery.css( elem, "display" ) === "none" ||
+ !jQuery.contains( elem.ownerDocument, elem );
+ };
+
+
+
+function adjustCSS( elem, prop, valueParts, tween ) {
+ var adjusted,
+ scale = 1,
+ maxIterations = 20,
+ currentValue = tween ?
+ function() { return tween.cur(); } :
+ function() { return jQuery.css( elem, prop, "" ); },
+ initial = currentValue(),
+ unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ),
+
+ // Starting value computation is required for potential unit mismatches
+ initialInUnit = ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) &&
+ rcssNum.exec( jQuery.css( elem, prop ) );
+
+ if ( initialInUnit && initialInUnit[ 3 ] !== unit ) {
+
+ // Trust units reported by jQuery.css
+ unit = unit || initialInUnit[ 3 ];
+
+ // Make sure we update the tween properties later on
+ valueParts = valueParts || [];
+
+ // Iteratively approximate from a nonzero starting point
+ initialInUnit = +initial || 1;
+
+ do {
+
+ // If previous iteration zeroed out, double until we get *something*.
+ // Use string for doubling so we don't accidentally see scale as unchanged below
+ scale = scale || ".5";
+
+ // Adjust and apply
+ initialInUnit = initialInUnit / scale;
+ jQuery.style( elem, prop, initialInUnit + unit );
+
+ // Update scale, tolerating zero or NaN from tween.cur()
+ // Break the loop if scale is unchanged or perfect, or if we've just had enough.
+ } while (
+ scale !== ( scale = currentValue() / initial ) && scale !== 1 && --maxIterations
+ );
+ }
+
+ if ( valueParts ) {
+ initialInUnit = +initialInUnit || +initial || 0;
+
+ // Apply relative offset (+=/-=) if specified
+ adjusted = valueParts[ 1 ] ?
+ initialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] :
+ +valueParts[ 2 ];
+ if ( tween ) {
+ tween.unit = unit;
+ tween.start = initialInUnit;
+ tween.end = adjusted;
+ }
+ }
+ return adjusted;
+}
+var rcheckableType = ( /^(?:checkbox|radio)$/i );
+
+var rtagName = ( /<([\w:-]+)/ );
+
+var rscriptType = ( /^$|\/(?:java|ecma)script/i );
+
+
+
+// We have to close these tags to support XHTML (#13200)
+var wrapMap = {
+
+ // Support: IE9
+ option: [ 1, "<select multiple='multiple'>", "</select>" ],
+
+ // XHTML parsers do not magically insert elements in the
+ // same way that tag soup parsers do. So we cannot shorten
+ // this by omitting <tbody> or other required elements.
+ thead: [ 1, "<table>", "</table>" ],
+ col: [ 2, "<table><colgroup>", "</colgroup></table>" ],
+ tr: [ 2, "<table><tbody>", "</tbody></table>" ],
+ td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
+
+ _default: [ 0, "", "" ]
+};
+
+// Support: IE9
+wrapMap.optgroup = wrapMap.option;
+
+wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
+wrapMap.th = wrapMap.td;
+
+
+function getAll( context, tag ) {
+
+ // Support: IE9-11+
+ // Use typeof to avoid zero-argument method invocation on host objects (#15151)
+ var ret = typeof context.getElementsByTagName !== "undefined" ?
+ context.getElementsByTagName( tag || "*" ) :
+ typeof context.querySelectorAll !== "undefined" ?
+ context.querySelectorAll( tag || "*" ) :
+ [];
+
+ return tag === undefined || tag && jQuery.nodeName( context, tag ) ?
+ jQuery.merge( [ context ], ret ) :
+ ret;
+}
+
+
+// Mark scripts as having already been evaluated
+function setGlobalEval( elems, refElements ) {
+ var i = 0,
+ l = elems.length;
+
+ for ( ; i < l; i++ ) {
+ dataPriv.set(
+ elems[ i ],
+ "globalEval",
+ !refElements || dataPriv.get( refElements[ i ], "globalEval" )
+ );
+ }
+}
+
+
+var rhtml = /<|&#?\w+;/;
+
+function buildFragment( elems, context, scripts, selection, ignored ) {
+ var elem, tmp, tag, wrap, contains, j,
+ fragment = context.createDocumentFragment(),
+ nodes = [],
+ i = 0,
+ l = elems.length;
+
+ for ( ; i < l; i++ ) {
+ elem = elems[ i ];
+
+ if ( elem || elem === 0 ) {
+
+ // Add nodes directly
+ if ( jQuery.type( elem ) === "object" ) {
+
+ // Support: Android<4.1, PhantomJS<2
+ // push.apply(_, arraylike) throws on ancient WebKit
+ jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem );
+
+ // Convert non-html into a text node
+ } else if ( !rhtml.test( elem ) ) {
+ nodes.push( context.createTextNode( elem ) );
+
+ // Convert html into DOM nodes
+ } else {
+ tmp = tmp || fragment.appendChild( context.createElement( "div" ) );
+
+ // Deserialize a standard representation
+ tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase();
+ wrap = wrapMap[ tag ] || wrapMap._default;
+ tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ];
+
+ // Descend through wrappers to the right content
+ j = wrap[ 0 ];
+ while ( j-- ) {
+ tmp = tmp.lastChild;
+ }
+
+ // Support: Android<4.1, PhantomJS<2
+ // push.apply(_, arraylike) throws on ancient WebKit
+ jQuery.merge( nodes, tmp.childNodes );
+
+ // Remember the top-level container
+ tmp = fragment.firstChild;
+
+ // Ensure the created nodes are orphaned (#12392)
+ tmp.textContent = "";
+ }
+ }
+ }
+
+ // Remove wrapper from fragment
+ fragment.textContent = "";
+
+ i = 0;
+ while ( ( elem = nodes[ i++ ] ) ) {
+
+ // Skip elements already in the context collection (trac-4087)
+ if ( selection && jQuery.inArray( elem, selection ) > -1 ) {
+ if ( ignored ) {
+ ignored.push( elem );
+ }
+ continue;
+ }
+
+ contains = jQuery.contains( elem.ownerDocument, elem );
+
+ // Append to fragment
+ tmp = getAll( fragment.appendChild( elem ), "script" );
+
+ // Preserve script evaluation history
+ if ( contains ) {
+ setGlobalEval( tmp );
+ }
+
+ // Capture executables
+ if ( scripts ) {
+ j = 0;
+ while ( ( elem = tmp[ j++ ] ) ) {
+ if ( rscriptType.test( elem.type || "" ) ) {
+ scripts.push( elem );
+ }
+ }
+ }
+ }
+
+ return fragment;
+}
+
+
+( function() {
+ var fragment = document.createDocumentFragment(),
+ div = fragment.appendChild( document.createElement( "div" ) ),
+ input = document.createElement( "input" );
+
+ // Support: Android 4.0-4.3, Safari<=5.1
+ // Check state lost if the name is set (#11217)
+ // Support: Windows Web Apps (WWA)
+ // `name` and `type` must use .setAttribute for WWA (#14901)
+ input.setAttribute( "type", "radio" );
+ input.setAttribute( "checked", "checked" );
+ input.setAttribute( "name", "t" );
+
+ div.appendChild( input );
+
+ // Support: Safari<=5.1, Android<4.2
+ // Older WebKit doesn't clone checked state correctly in fragments
+ support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked;
+
+ // Support: IE<=11+
+ // Make sure textarea (and checkbox) defaultValue is properly cloned
+ div.innerHTML = "<textarea>x</textarea>";
+ support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue;
+} )();
+
+
+var
+ rkeyEvent = /^key/,
+ rmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/,
+ rtypenamespace = /^([^.]*)(?:\.(.+)|)/;
+
+function returnTrue() {
+ return true;
+}
+
+function returnFalse() {
+ return false;
+}
+
+// Support: IE9
+// See #13393 for more info
+function safeActiveElement() {
+ try {
+ return document.activeElement;
+ } catch ( err ) { }
+}
+
+function on( elem, types, selector, data, fn, one ) {
+ var origFn, type;
+
+ // Types can be a map of types/handlers
+ if ( typeof types === "object" ) {
+
+ // ( types-Object, selector, data )
+ if ( typeof selector !== "string" ) {
+
+ // ( types-Object, data )
+ data = data || selector;
+ selector = undefined;
+ }
+ for ( type in types ) {
+ on( elem, type, selector, data, types[ type ], one );
+ }
+ return elem;
+ }
+
+ if ( data == null && fn == null ) {
+
+ // ( types, fn )
+ fn = selector;
+ data = selector = undefined;
+ } else if ( fn == null ) {
+ if ( typeof selector === "string" ) {
+
+ // ( types, selector, fn )
+ fn = data;
+ data = undefined;
+ } else {
+
+ // ( types, data, fn )
+ fn = data;
+ data = selector;
+ selector = undefined;
+ }
+ }
+ if ( fn === false ) {
+ fn = returnFalse;
+ } else if ( !fn ) {
+ return this;
+ }
+
+ if ( one === 1 ) {
+ origFn = fn;
+ fn = function( event ) {
+
+ // Can use an empty set, since event contains the info
+ jQuery().off( event );
+ return origFn.apply( this, arguments );
+ };
+
+ // Use same guid so caller can remove using origFn
+ fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );
+ }
+ return elem.each( function() {
+ jQuery.event.add( this, types, fn, data, selector );
+ } );
+}
+
+/*
+ * Helper functions for managing events -- not part of the public interface.
+ * Props to Dean Edwards' addEvent library for many of the ideas.
+ */
+jQuery.event = {
+
+ global: {},
+
+ add: function( elem, types, handler, data, selector ) {
+
+ var handleObjIn, eventHandle, tmp,
+ events, t, handleObj,
+ special, handlers, type, namespaces, origType,
+ elemData = dataPriv.get( elem );
+
+ // Don't attach events to noData or text/comment nodes (but allow plain objects)
+ if ( !elemData ) {
+ return;
+ }
+
+ // Caller can pass in an object of custom data in lieu of the handler
+ if ( handler.handler ) {
+ handleObjIn = handler;
+ handler = handleObjIn.handler;
+ selector = handleObjIn.selector;
+ }
+
+ // Make sure that the handler has a unique ID, used to find/remove it later
+ if ( !handler.guid ) {
+ handler.guid = jQuery.guid++;
+ }
+
+ // Init the element's event structure and main handler, if this is the first
+ if ( !( events = elemData.events ) ) {
+ events = elemData.events = {};
+ }
+ if ( !( eventHandle = elemData.handle ) ) {
+ eventHandle = elemData.handle = function( e ) {
+
+ // Discard the second event of a jQuery.event.trigger() and
+ // when an event is called after a page has unloaded
+ return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ?
+ jQuery.event.dispatch.apply( elem, arguments ) : undefined;
+ };
+ }
+
+ // Handle multiple events separated by a space
+ types = ( types || "" ).match( rnotwhite ) || [ "" ];
+ t = types.length;
+ while ( t-- ) {
+ tmp = rtypenamespace.exec( types[ t ] ) || [];
+ type = origType = tmp[ 1 ];
+ namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort();
+
+ // There *must* be a type, no attaching namespace-only handlers
+ if ( !type ) {
+ continue;
+ }
+
+ // If event changes its type, use the special event handlers for the changed type
+ special = jQuery.event.special[ type ] || {};
+
+ // If selector defined, determine special event api type, otherwise given type
+ type = ( selector ? special.delegateType : special.bindType ) || type;
+
+ // Update special based on newly reset type
+ special = jQuery.event.special[ type ] || {};
+
+ // handleObj is passed to all event handlers
+ handleObj = jQuery.extend( {
+ type: type,
+ origType: origType,
+ data: data,
+ handler: handler,
+ guid: handler.guid,
+ selector: selector,
+ needsContext: selector && jQuery.expr.match.needsContext.test( selector ),
+ namespace: namespaces.join( "." )
+ }, handleObjIn );
+
+ // Init the event handler queue if we're the first
+ if ( !( handlers = events[ type ] ) ) {
+ handlers = events[ type ] = [];
+ handlers.delegateCount = 0;
+
+ // Only use addEventListener if the special events handler returns false
+ if ( !special.setup ||
+ special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
+
+ if ( elem.addEventListener ) {
+ elem.addEventListener( type, eventHandle );
+ }
+ }
+ }
+
+ if ( special.add ) {
+ special.add.call( elem, handleObj );
+
+ if ( !handleObj.handler.guid ) {
+ handleObj.handler.guid = handler.guid;
+ }
+ }
+
+ // Add to the element's handler list, delegates in front
+ if ( selector ) {
+ handlers.splice( handlers.delegateCount++, 0, handleObj );
+ } else {
+ handlers.push( handleObj );
+ }
+
+ // Keep track of which events have ever been used, for event optimization
+ jQuery.event.global[ type ] = true;
+ }
+
+ },
+
+ // Detach an event or set of events from an element
+ remove: function( elem, types, handler, selector, mappedTypes ) {
+
+ var j, origCount, tmp,
+ events, t, handleObj,
+ special, handlers, type, namespaces, origType,
+ elemData = dataPriv.hasData( elem ) && dataPriv.get( elem );
+
+ if ( !elemData || !( events = elemData.events ) ) {
+ return;
+ }
+
+ // Once for each type.namespace in types; type may be omitted
+ types = ( types || "" ).match( rnotwhite ) || [ "" ];
+ t = types.length;
+ while ( t-- ) {
+ tmp = rtypenamespace.exec( types[ t ] ) || [];
+ type = origType = tmp[ 1 ];
+ namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort();
+
+ // Unbind all events (on this namespace, if provided) for the element
+ if ( !type ) {
+ for ( type in events ) {
+ jQuery.event.remove( elem, type + types[ t ], handler, selector, true );
+ }
+ continue;
+ }
+
+ special = jQuery.event.special[ type ] || {};
+ type = ( selector ? special.delegateType : special.bindType ) || type;
+ handlers = events[ type ] || [];
+ tmp = tmp[ 2 ] &&
+ new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" );
+
+ // Remove matching events
+ origCount = j = handlers.length;
+ while ( j-- ) {
+ handleObj = handlers[ j ];
+
+ if ( ( mappedTypes || origType === handleObj.origType ) &&
+ ( !handler || handler.guid === handleObj.guid ) &&
+ ( !tmp || tmp.test( handleObj.namespace ) ) &&
+ ( !selector || selector === handleObj.selector ||
+ selector === "**" && handleObj.selector ) ) {
+ handlers.splice( j, 1 );
+
+ if ( handleObj.selector ) {
+ handlers.delegateCount--;
+ }
+ if ( special.remove ) {
+ special.remove.call( elem, handleObj );
+ }
+ }
+ }
+
+ // Remove generic event handler if we removed something and no more handlers exist
+ // (avoids potential for endless recursion during removal of special event handlers)
+ if ( origCount && !handlers.length ) {
+ if ( !special.teardown ||
+ special.teardown.call( elem, namespaces, elemData.handle ) === false ) {
+
+ jQuery.removeEvent( elem, type, elemData.handle );
+ }
+
+ delete events[ type ];
+ }
+ }
+
+ // Remove data and the expando if it's no longer used
+ if ( jQuery.isEmptyObject( events ) ) {
+ dataPriv.remove( elem, "handle events" );
+ }
+ },
+
+ dispatch: function( event ) {
+
+ // Make a writable jQuery.Event from the native event object
+ event = jQuery.event.fix( event );
+
+ var i, j, ret, matched, handleObj,
+ handlerQueue = [],
+ args = slice.call( arguments ),
+ handlers = ( dataPriv.get( this, "events" ) || {} )[ event.type ] || [],
+ special = jQuery.event.special[ event.type ] || {};
+
+ // Use the fix-ed jQuery.Event rather than the (read-only) native event
+ args[ 0 ] = event;
+ event.delegateTarget = this;
+
+ // Call the preDispatch hook for the mapped type, and let it bail if desired
+ if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) {
+ return;
+ }
+
+ // Determine handlers
+ handlerQueue = jQuery.event.handlers.call( this, event, handlers );
+
+ // Run delegates first; they may want to stop propagation beneath us
+ i = 0;
+ while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) {
+ event.currentTarget = matched.elem;
+
+ j = 0;
+ while ( ( handleObj = matched.handlers[ j++ ] ) &&
+ !event.isImmediatePropagationStopped() ) {
+
+ // Triggered event must either 1) have no namespace, or 2) have namespace(s)
+ // a subset or equal to those in the bound event (both can have no namespace).
+ if ( !event.rnamespace || event.rnamespace.test( handleObj.namespace ) ) {
+
+ event.handleObj = handleObj;
+ event.data = handleObj.data;
+
+ ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle ||
+ handleObj.handler ).apply( matched.elem, args );
+
+ if ( ret !== undefined ) {
+ if ( ( event.result = ret ) === false ) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+ }
+ }
+ }
+
+ // Call the postDispatch hook for the mapped type
+ if ( special.postDispatch ) {
+ special.postDispatch.call( this, event );
+ }
+
+ return event.result;
+ },
+
+ handlers: function( event, handlers ) {
+ var i, matches, sel, handleObj,
+ handlerQueue = [],
+ delegateCount = handlers.delegateCount,
+ cur = event.target;
+
+ // Support (at least): Chrome, IE9
+ // Find delegate handlers
+ // Black-hole SVG <use> instance trees (#13180)
+ //
+ // Support: Firefox<=42+
+ // Avoid non-left-click in FF but don't block IE radio events (#3861, gh-2343)
+ if ( delegateCount && cur.nodeType &&
+ ( event.type !== "click" || isNaN( event.button ) || event.button < 1 ) ) {
+
+ for ( ; cur !== this; cur = cur.parentNode || this ) {
+
+ // Don't check non-elements (#13208)
+ // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764)
+ if ( cur.nodeType === 1 && ( cur.disabled !== true || event.type !== "click" ) ) {
+ matches = [];
+ for ( i = 0; i < delegateCount; i++ ) {
+ handleObj = handlers[ i ];
+
+ // Don't conflict with Object.prototype properties (#13203)
+ sel = handleObj.selector + " ";
+
+ if ( matches[ sel ] === undefined ) {
+ matches[ sel ] = handleObj.needsContext ?
+ jQuery( sel, this ).index( cur ) > -1 :
+ jQuery.find( sel, this, null, [ cur ] ).length;
+ }
+ if ( matches[ sel ] ) {
+ matches.push( handleObj );
+ }
+ }
+ if ( matches.length ) {
+ handlerQueue.push( { elem: cur, handlers: matches } );
+ }
+ }
+ }
+ }
+
+ // Add the remaining (directly-bound) handlers
+ if ( delegateCount < handlers.length ) {
+ handlerQueue.push( { elem: this, handlers: handlers.slice( delegateCount ) } );
+ }
+
+ return handlerQueue;
+ },
+
+ // Includes some event props shared by KeyEvent and MouseEvent
+ props: ( "altKey bubbles cancelable ctrlKey currentTarget detail eventPhase " +
+ "metaKey relatedTarget shiftKey target timeStamp view which" ).split( " " ),
+
+ fixHooks: {},
+
+ keyHooks: {
+ props: "char charCode key keyCode".split( " " ),
+ filter: function( event, original ) {
+
+ // Add which for key events
+ if ( event.which == null ) {
+ event.which = original.charCode != null ? original.charCode : original.keyCode;
+ }
+
+ return event;
+ }
+ },
+
+ mouseHooks: {
+ props: ( "button buttons clientX clientY offsetX offsetY pageX pageY " +
+ "screenX screenY toElement" ).split( " " ),
+ filter: function( event, original ) {
+ var eventDoc, doc, body,
+ button = original.button;
+
+ // Calculate pageX/Y if missing and clientX/Y available
+ if ( event.pageX == null && original.clientX != null ) {
+ eventDoc = event.target.ownerDocument || document;
+ doc = eventDoc.documentElement;
+ body = eventDoc.body;
+
+ event.pageX = original.clientX +
+ ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) -
+ ( doc && doc.clientLeft || body && body.clientLeft || 0 );
+ event.pageY = original.clientY +
+ ( doc && doc.scrollTop || body && body.scrollTop || 0 ) -
+ ( doc && doc.clientTop || body && body.clientTop || 0 );
+ }
+
+ // Add which for click: 1 === left; 2 === middle; 3 === right
+ // Note: button is not normalized, so don't use it
+ if ( !event.which && button !== undefined ) {
+ event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) );
+ }
+
+ return event;
+ }
+ },
+
+ fix: function( event ) {
+ if ( event[ jQuery.expando ] ) {
+ return event;
+ }
+
+ // Create a writable copy of the event object and normalize some properties
+ var i, prop, copy,
+ type = event.type,
+ originalEvent = event,
+ fixHook = this.fixHooks[ type ];
+
+ if ( !fixHook ) {
+ this.fixHooks[ type ] = fixHook =
+ rmouseEvent.test( type ) ? this.mouseHooks :
+ rkeyEvent.test( type ) ? this.keyHooks :
+ {};
+ }
+ copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props;
+
+ event = new jQuery.Event( originalEvent );
+
+ i = copy.length;
+ while ( i-- ) {
+ prop = copy[ i ];
+ event[ prop ] = originalEvent[ prop ];
+ }
+
+ // Support: Cordova 2.5 (WebKit) (#13255)
+ // All events should have a target; Cordova deviceready doesn't
+ if ( !event.target ) {
+ event.target = document;
+ }
+
+ // Support: Safari 6.0+, Chrome<28
+ // Target should not be a text node (#504, #13143)
+ if ( event.target.nodeType === 3 ) {
+ event.target = event.target.parentNode;
+ }
+
+ return fixHook.filter ? fixHook.filter( event, originalEvent ) : event;
+ },
+
+ special: {
+ load: {
+
+ // Prevent triggered image.load events from bubbling to window.load
+ noBubble: true
+ },
+ focus: {
+
+ // Fire native event if possible so blur/focus sequence is correct
+ trigger: function() {
+ if ( this !== safeActiveElement() && this.focus ) {
+ this.focus();
+ return false;
+ }
+ },
+ delegateType: "focusin"
+ },
+ blur: {
+ trigger: function() {
+ if ( this === safeActiveElement() && this.blur ) {
+ this.blur();
+ return false;
+ }
+ },
+ delegateType: "focusout"
+ },
+ click: {
+
+ // For checkbox, fire native event so checked state will be right
+ trigger: function() {
+ if ( this.type === "checkbox" && this.click && jQuery.nodeName( this, "input" ) ) {
+ this.click();
+ return false;
+ }
+ },
+
+ // For cross-browser consistency, don't fire native .click() on links
+ _default: function( event ) {
+ return jQuery.nodeName( event.target, "a" );
+ }
+ },
+
+ beforeunload: {
+ postDispatch: function( event ) {
+
+ // Support: Firefox 20+
+ // Firefox doesn't alert if the returnValue field is not set.
+ if ( event.result !== undefined && event.originalEvent ) {
+ event.originalEvent.returnValue = event.result;
+ }
+ }
+ }
+ }
+};
+
+jQuery.removeEvent = function( elem, type, handle ) {
+
+ // This "if" is needed for plain objects
+ if ( elem.removeEventListener ) {
+ elem.removeEventListener( type, handle );
+ }
+};
+
+jQuery.Event = function( src, props ) {
+
+ // Allow instantiation without the 'new' keyword
+ if ( !( this instanceof jQuery.Event ) ) {
+ return new jQuery.Event( src, props );
+ }
+
+ // Event object
+ if ( src && src.type ) {
+ this.originalEvent = src;
+ this.type = src.type;
+
+ // Events bubbling up the document may have been marked as prevented
+ // by a handler lower down the tree; reflect the correct value.
+ this.isDefaultPrevented = src.defaultPrevented ||
+ src.defaultPrevented === undefined &&
+
+ // Support: Android<4.0
+ src.returnValue === false ?
+ returnTrue :
+ returnFalse;
+
+ // Event type
+ } else {
+ this.type = src;
+ }
+
+ // Put explicitly provided properties onto the event object
+ if ( props ) {
+ jQuery.extend( this, props );
+ }
+
+ // Create a timestamp if incoming event doesn't have one
+ this.timeStamp = src && src.timeStamp || jQuery.now();
+
+ // Mark it as fixed
+ this[ jQuery.expando ] = true;
+};
+
+// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding
+// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
+jQuery.Event.prototype = {
+ constructor: jQuery.Event,
+ isDefaultPrevented: returnFalse,
+ isPropagationStopped: returnFalse,
+ isImmediatePropagationStopped: returnFalse,
+
+ preventDefault: function() {
+ var e = this.originalEvent;
+
+ this.isDefaultPrevented = returnTrue;
+
+ if ( e ) {
+ e.preventDefault();
+ }
+ },
+ stopPropagation: function() {
+ var e = this.originalEvent;
+
+ this.isPropagationStopped = returnTrue;
+
+ if ( e ) {
+ e.stopPropagation();
+ }
+ },
+ stopImmediatePropagation: function() {
+ var e = this.originalEvent;
+
+ this.isImmediatePropagationStopped = returnTrue;
+
+ if ( e ) {
+ e.stopImmediatePropagation();
+ }
+
+ this.stopPropagation();
+ }
+};
+
+// Create mouseenter/leave events using mouseover/out and event-time checks
+// so that event delegation works in jQuery.
+// Do the same for pointerenter/pointerleave and pointerover/pointerout
+//
+// Support: Safari 7 only
+// Safari sends mouseenter too often; see:
+// https://code.google.com/p/chromium/issues/detail?id=470258
+// for the description of the bug (it existed in older Chrome versions as well).
+jQuery.each( {
+ mouseenter: "mouseover",
+ mouseleave: "mouseout",
+ pointerenter: "pointerover",
+ pointerleave: "pointerout"
+}, function( orig, fix ) {
+ jQuery.event.special[ orig ] = {
+ delegateType: fix,
+ bindType: fix,
+
+ handle: function( event ) {
+ var ret,
+ target = this,
+ related = event.relatedTarget,
+ handleObj = event.handleObj;
+
+ // For mouseenter/leave call the handler if related is outside the target.
+ // NB: No relatedTarget if the mouse left/entered the browser window
+ if ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) {
+ event.type = handleObj.origType;
+ ret = handleObj.handler.apply( this, arguments );
+ event.type = fix;
+ }
+ return ret;
+ }
+ };
+} );
+
+jQuery.fn.extend( {
+ on: function( types, selector, data, fn ) {
+ return on( this, types, selector, data, fn );
+ },
+ one: function( types, selector, data, fn ) {
+ return on( this, types, selector, data, fn, 1 );
+ },
+ off: function( types, selector, fn ) {
+ var handleObj, type;
+ if ( types && types.preventDefault && types.handleObj ) {
+
+ // ( event ) dispatched jQuery.Event
+ handleObj = types.handleObj;
+ jQuery( types.delegateTarget ).off(
+ handleObj.namespace ?
+ handleObj.origType + "." + handleObj.namespace :
+ handleObj.origType,
+ handleObj.selector,
+ handleObj.handler
+ );
+ return this;
+ }
+ if ( typeof types === "object" ) {
+
+ // ( types-object [, selector] )
+ for ( type in types ) {
+ this.off( type, selector, types[ type ] );
+ }
+ return this;
+ }
+ if ( selector === false || typeof selector === "function" ) {
+
+ // ( types [, fn] )
+ fn = selector;
+ selector = undefined;
+ }
+ if ( fn === false ) {
+ fn = returnFalse;
+ }
+ return this.each( function() {
+ jQuery.event.remove( this, types, fn, selector );
+ } );
+ }
+} );
+
+
+var
+ rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,
+
+ // Support: IE 10-11, Edge 10240+
+ // In IE/Edge using regex groups here causes severe slowdowns.
+ // See https://connect.microsoft.com/IE/feedback/details/1736512/
+ rnoInnerhtml = /<script|<style|<link/i,
+
+ // checked="checked" or checked
+ rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i,
+ rscriptTypeMasked = /^true\/(.*)/,
+ rcleanScript = /^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g;
+
+function manipulationTarget( elem, content ) {
+ if ( jQuery.nodeName( elem, "table" ) &&
+ jQuery.nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ) {
+
+ return elem.getElementsByTagName( "tbody" )[ 0 ] || elem;
+ }
+
+ return elem;
+}
+
+// Replace/restore the type attribute of script elements for safe DOM manipulation
+function disableScript( elem ) {
+ elem.type = ( elem.getAttribute( "type" ) !== null ) + "/" + elem.type;
+ return elem;
+}
+function restoreScript( elem ) {
+ var match = rscriptTypeMasked.exec( elem.type );
+
+ if ( match ) {
+ elem.type = match[ 1 ];
+ } else {
+ elem.removeAttribute( "type" );
+ }
+
+ return elem;
+}
+
+function cloneCopyEvent( src, dest ) {
+ var i, l, type, pdataOld, pdataCur, udataOld, udataCur, events;
+
+ if ( dest.nodeType !== 1 ) {
+ return;
+ }
+
+ // 1. Copy private data: events, handlers, etc.
+ if ( dataPriv.hasData( src ) ) {
+ pdataOld = dataPriv.access( src );
+ pdataCur = dataPriv.set( dest, pdataOld );
+ events = pdataOld.events;
+
+ if ( events ) {
+ delete pdataCur.handle;
+ pdataCur.events = {};
+
+ for ( type in events ) {
+ for ( i = 0, l = events[ type ].length; i < l; i++ ) {
+ jQuery.event.add( dest, type, events[ type ][ i ] );
+ }
+ }
+ }
+ }
+
+ // 2. Copy user data
+ if ( dataUser.hasData( src ) ) {
+ udataOld = dataUser.access( src );
+ udataCur = jQuery.extend( {}, udataOld );
+
+ dataUser.set( dest, udataCur );
+ }
+}
+
+// Fix IE bugs, see support tests
+function fixInput( src, dest ) {
+ var nodeName = dest.nodeName.toLowerCase();
+
+ // Fails to persist the checked state of a cloned checkbox or radio button.
+ if ( nodeName === "input" && rcheckableType.test( src.type ) ) {
+ dest.checked = src.checked;
+
+ // Fails to return the selected option to the default selected state when cloning options
+ } else if ( nodeName === "input" || nodeName === "textarea" ) {
+ dest.defaultValue = src.defaultValue;
+ }
+}
+
+function domManip( collection, args, callback, ignored ) {
+
+ // Flatten any nested arrays
+ args = concat.apply( [], args );
+
+ var fragment, first, scripts, hasScripts, node, doc,
+ i = 0,
+ l = collection.length,
+ iNoClone = l - 1,
+ value = args[ 0 ],
+ isFunction = jQuery.isFunction( value );
+
+ // We can't cloneNode fragments that contain checked, in WebKit
+ if ( isFunction ||
+ ( l > 1 && typeof value === "string" &&
+ !support.checkClone && rchecked.test( value ) ) ) {
+ return collection.each( function( index ) {
+ var self = collection.eq( index );
+ if ( isFunction ) {
+ args[ 0 ] = value.call( this, index, self.html() );
+ }
+ domManip( self, args, callback, ignored );
+ } );
+ }
+
+ if ( l ) {
+ fragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored );
+ first = fragment.firstChild;
+
+ if ( fragment.childNodes.length === 1 ) {
+ fragment = first;
+ }
+
+ // Require either new content or an interest in ignored elements to invoke the callback
+ if ( first || ignored ) {
+ scripts = jQuery.map( getAll( fragment, "script" ), disableScript );
+ hasScripts = scripts.length;
+
+ // Use the original fragment for the last item
+ // instead of the first because it can end up
+ // being emptied incorrectly in certain situations (#8070).
+ for ( ; i < l; i++ ) {
+ node = fragment;
+
+ if ( i !== iNoClone ) {
+ node = jQuery.clone( node, true, true );
+
+ // Keep references to cloned scripts for later restoration
+ if ( hasScripts ) {
+
+ // Support: Android<4.1, PhantomJS<2
+ // push.apply(_, arraylike) throws on ancient WebKit
+ jQuery.merge( scripts, getAll( node, "script" ) );
+ }
+ }
+
+ callback.call( collection[ i ], node, i );
+ }
+
+ if ( hasScripts ) {
+ doc = scripts[ scripts.length - 1 ].ownerDocument;
+
+ // Reenable scripts
+ jQuery.map( scripts, restoreScript );
+
+ // Evaluate executable scripts on first document insertion
+ for ( i = 0; i < hasScripts; i++ ) {
+ node = scripts[ i ];
+ if ( rscriptType.test( node.type || "" ) &&
+ !dataPriv.access( node, "globalEval" ) &&
+ jQuery.contains( doc, node ) ) {
+
+ if ( node.src ) {
+
+ // Optional AJAX dependency, but won't run scripts if not present
+ if ( jQuery._evalUrl ) {
+ jQuery._evalUrl( node.src );
+ }
+ } else {
+ jQuery.globalEval( node.textContent.replace( rcleanScript, "" ) );
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return collection;
+}
+
+function remove( elem, selector, keepData ) {
+ var node,
+ nodes = selector ? jQuery.filter( selector, elem ) : elem,
+ i = 0;
+
+ for ( ; ( node = nodes[ i ] ) != null; i++ ) {
+ if ( !keepData && node.nodeType === 1 ) {
+ jQuery.cleanData( getAll( node ) );
+ }
+
+ if ( node.parentNode ) {
+ if ( keepData && jQuery.contains( node.ownerDocument, node ) ) {
+ setGlobalEval( getAll( node, "script" ) );
+ }
+ node.parentNode.removeChild( node );
+ }
+ }
+
+ return elem;
+}
+
+jQuery.extend( {
+ htmlPrefilter: function( html ) {
+ return html.replace( rxhtmlTag, "<$1></$2>" );
+ },
+
+ clone: function( elem, dataAndEvents, deepDataAndEvents ) {
+ var i, l, srcElements, destElements,
+ clone = elem.cloneNode( true ),
+ inPage = jQuery.contains( elem.ownerDocument, elem );
+
+ // Fix IE cloning issues
+ if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) &&
+ !jQuery.isXMLDoc( elem ) ) {
+
+ // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2
+ destElements = getAll( clone );
+ srcElements = getAll( elem );
+
+ for ( i = 0, l = srcElements.length; i < l; i++ ) {
+ fixInput( srcElements[ i ], destElements[ i ] );
+ }
+ }
+
+ // Copy the events from the original to the clone
+ if ( dataAndEvents ) {
+ if ( deepDataAndEvents ) {
+ srcElements = srcElements || getAll( elem );
+ destElements = destElements || getAll( clone );
+
+ for ( i = 0, l = srcElements.length; i < l; i++ ) {
+ cloneCopyEvent( srcElements[ i ], destElements[ i ] );
+ }
+ } else {
+ cloneCopyEvent( elem, clone );
+ }
+ }
+
+ // Preserve script evaluation history
+ destElements = getAll( clone, "script" );
+ if ( destElements.length > 0 ) {
+ setGlobalEval( destElements, !inPage && getAll( elem, "script" ) );
+ }
+
+ // Return the cloned set
+ return clone;
+ },
+
+ cleanData: function( elems ) {
+ var data, elem, type,
+ special = jQuery.event.special,
+ i = 0;
+
+ for ( ; ( elem = elems[ i ] ) !== undefined; i++ ) {
+ if ( acceptData( elem ) ) {
+ if ( ( data = elem[ dataPriv.expando ] ) ) {
+ if ( data.events ) {
+ for ( type in data.events ) {
+ if ( special[ type ] ) {
+ jQuery.event.remove( elem, type );
+
+ // This is a shortcut to avoid jQuery.event.remove's overhead
+ } else {
+ jQuery.removeEvent( elem, type, data.handle );
+ }
+ }
+ }
+
+ // Support: Chrome <= 35-45+
+ // Assign undefined instead of using delete, see Data#remove
+ elem[ dataPriv.expando ] = undefined;
+ }
+ if ( elem[ dataUser.expando ] ) {
+
+ // Support: Chrome <= 35-45+
+ // Assign undefined instead of using delete, see Data#remove
+ elem[ dataUser.expando ] = undefined;
+ }
+ }
+ }
+ }
+} );
+
+jQuery.fn.extend( {
+
+ // Keep domManip exposed until 3.0 (gh-2225)
+ domManip: domManip,
+
+ detach: function( selector ) {
+ return remove( this, selector, true );
+ },
+
+ remove: function( selector ) {
+ return remove( this, selector );
+ },
+
+ text: function( value ) {
+ return access( this, function( value ) {
+ return value === undefined ?
+ jQuery.text( this ) :
+ this.empty().each( function() {
+ if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
+ this.textContent = value;
+ }
+ } );
+ }, null, value, arguments.length );
+ },
+
+ append: function() {
+ return domManip( this, arguments, function( elem ) {
+ if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
+ var target = manipulationTarget( this, elem );
+ target.appendChild( elem );
+ }
+ } );
+ },
+
+ prepend: function() {
+ return domManip( this, arguments, function( elem ) {
+ if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
+ var target = manipulationTarget( this, elem );
+ target.insertBefore( elem, target.firstChild );
+ }
+ } );
+ },
+
+ before: function() {
+ return domManip( this, arguments, function( elem ) {
+ if ( this.parentNode ) {
+ this.parentNode.insertBefore( elem, this );
+ }
+ } );
+ },
+
+ after: function() {
+ return domManip( this, arguments, function( elem ) {
+ if ( this.parentNode ) {
+ this.parentNode.insertBefore( elem, this.nextSibling );
+ }
+ } );
+ },
+
+ empty: function() {
+ var elem,
+ i = 0;
+
+ for ( ; ( elem = this[ i ] ) != null; i++ ) {
+ if ( elem.nodeType === 1 ) {
+
+ // Prevent memory leaks
+ jQuery.cleanData( getAll( elem, false ) );
+
+ // Remove any remaining nodes
+ elem.textContent = "";
+ }
+ }
+
+ return this;
+ },
+
+ clone: function( dataAndEvents, deepDataAndEvents ) {
+ dataAndEvents = dataAndEvents == null ? false : dataAndEvents;
+ deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents;
+
+ return this.map( function() {
+ return jQuery.clone( this, dataAndEvents, deepDataAndEvents );
+ } );
+ },
+
+ html: function( value ) {
+ return access( this, function( value ) {
+ var elem = this[ 0 ] || {},
+ i = 0,
+ l = this.length;
+
+ if ( value === undefined && elem.nodeType === 1 ) {
+ return elem.innerHTML;
+ }
+
+ // See if we can take a shortcut and just use innerHTML
+ if ( typeof value === "string" && !rnoInnerhtml.test( value ) &&
+ !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) {
+
+ value = jQuery.htmlPrefilter( value );
+
+ try {
+ for ( ; i < l; i++ ) {
+ elem = this[ i ] || {};
+
+ // Remove element nodes and prevent memory leaks
+ if ( elem.nodeType === 1 ) {
+ jQuery.cleanData( getAll( elem, false ) );
+ elem.innerHTML = value;
+ }
+ }
+
+ elem = 0;
+
+ // If using innerHTML throws an exception, use the fallback method
+ } catch ( e ) {}
+ }
+
+ if ( elem ) {
+ this.empty().append( value );
+ }
+ }, null, value, arguments.length );
+ },
+
+ replaceWith: function() {
+ var ignored = [];
+
+ // Make the changes, replacing each non-ignored context element with the new content
+ return domManip( this, arguments, function( elem ) {
+ var parent = this.parentNode;
+
+ if ( jQuery.inArray( this, ignored ) < 0 ) {
+ jQuery.cleanData( getAll( this ) );
+ if ( parent ) {
+ parent.replaceChild( elem, this );
+ }
+ }
+
+ // Force callback invocation
+ }, ignored );
+ }
+} );
+
+jQuery.each( {
+ appendTo: "append",
+ prependTo: "prepend",
+ insertBefore: "before",
+ insertAfter: "after",
+ replaceAll: "replaceWith"
+}, function( name, original ) {
+ jQuery.fn[ name ] = function( selector ) {
+ var elems,
+ ret = [],
+ insert = jQuery( selector ),
+ last = insert.length - 1,
+ i = 0;
+
+ for ( ; i <= last; i++ ) {
+ elems = i === last ? this : this.clone( true );
+ jQuery( insert[ i ] )[ original ]( elems );
+
+ // Support: QtWebKit
+ // .get() because push.apply(_, arraylike) throws
+ push.apply( ret, elems.get() );
+ }
+
+ return this.pushStack( ret );
+ };
+} );
+
+
+var iframe,
+ elemdisplay = {
+
+ // Support: Firefox
+ // We have to pre-define these values for FF (#10227)
+ HTML: "block",
+ BODY: "block"
+ };
+
+/**
+ * Retrieve the actual display of a element
+ * @param {String} name nodeName of the element
+ * @param {Object} doc Document object
+ */
+
+// Called only from within defaultDisplay
+function actualDisplay( name, doc ) {
+ var elem = jQuery( doc.createElement( name ) ).appendTo( doc.body ),
+
+ display = jQuery.css( elem[ 0 ], "display" );
+
+ // We don't have any data stored on the element,
+ // so use "detach" method as fast way to get rid of the element
+ elem.detach();
+
+ return display;
+}
+
+/**
+ * Try to determine the default display value of an element
+ * @param {String} nodeName
+ */
+function defaultDisplay( nodeName ) {
+ var doc = document,
+ display = elemdisplay[ nodeName ];
+
+ if ( !display ) {
+ display = actualDisplay( nodeName, doc );
+
+ // If the simple way fails, read from inside an iframe
+ if ( display === "none" || !display ) {
+
+ // Use the already-created iframe if possible
+ iframe = ( iframe || jQuery( "<iframe frameborder='0' width='0' height='0'/>" ) )
+ .appendTo( doc.documentElement );
+
+ // Always write a new HTML skeleton so Webkit and Firefox don't choke on reuse
+ doc = iframe[ 0 ].contentDocument;
+
+ // Support: IE
+ doc.write();
+ doc.close();
+
+ display = actualDisplay( nodeName, doc );
+ iframe.detach();
+ }
+
+ // Store the correct default display
+ elemdisplay[ nodeName ] = display;
+ }
+
+ return display;
+}
+var rmargin = ( /^margin/ );
+
+var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" );
+
+var getStyles = function( elem ) {
+
+ // Support: IE<=11+, Firefox<=30+ (#15098, #14150)
+ // IE throws on elements created in popups
+ // FF meanwhile throws on frame elements through "defaultView.getComputedStyle"
+ var view = elem.ownerDocument.defaultView;
+
+ if ( !view.opener ) {
+ view = window;
+ }
+
+ return view.getComputedStyle( elem );
+ };
+
+var swap = function( elem, options, callback, args ) {
+ var ret, name,
+ old = {};
+
+ // Remember the old values, and insert the new ones
+ for ( name in options ) {
+ old[ name ] = elem.style[ name ];
+ elem.style[ name ] = options[ name ];
+ }
+
+ ret = callback.apply( elem, args || [] );
+
+ // Revert the old values
+ for ( name in options ) {
+ elem.style[ name ] = old[ name ];
+ }
+
+ return ret;
+};
+
+
+var documentElement = document.documentElement;
+
+
+
+( function() {
+ var pixelPositionVal, boxSizingReliableVal, pixelMarginRightVal, reliableMarginLeftVal,
+ container = document.createElement( "div" ),
+ div = document.createElement( "div" );
+
+ // Finish early in limited (non-browser) environments
+ if ( !div.style ) {
+ return;
+ }
+
+ // Support: IE9-11+
+ // Style of cloned element affects source element cloned (#8908)
+ div.style.backgroundClip = "content-box";
+ div.cloneNode( true ).style.backgroundClip = "";
+ support.clearCloneStyle = div.style.backgroundClip === "content-box";
+
+ container.style.cssText = "border:0;width:8px;height:0;top:0;left:-9999px;" +
+ "padding:0;margin-top:1px;position:absolute";
+ container.appendChild( div );
+
+ // Executing both pixelPosition & boxSizingReliable tests require only one layout
+ // so they're executed at the same time to save the second computation.
+ function computeStyleTests() {
+ div.style.cssText =
+
+ // Support: Firefox<29, Android 2.3
+ // Vendor-prefix box-sizing
+ "-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;" +
+ "position:relative;display:block;" +
+ "margin:auto;border:1px;padding:1px;" +
+ "top:1%;width:50%";
+ div.innerHTML = "";
+ documentElement.appendChild( container );
+
+ var divStyle = window.getComputedStyle( div );
+ pixelPositionVal = divStyle.top !== "1%";
+ reliableMarginLeftVal = divStyle.marginLeft === "2px";
+ boxSizingReliableVal = divStyle.width === "4px";
+
+ // Support: Android 4.0 - 4.3 only
+ // Some styles come back with percentage values, even though they shouldn't
+ div.style.marginRight = "50%";
+ pixelMarginRightVal = divStyle.marginRight === "4px";
+
+ documentElement.removeChild( container );
+ }
+
+ jQuery.extend( support, {
+ pixelPosition: function() {
+
+ // This test is executed only once but we still do memoizing
+ // since we can use the boxSizingReliable pre-computing.
+ // No need to check if the test was already performed, though.
+ computeStyleTests();
+ return pixelPositionVal;
+ },
+ boxSizingReliable: function() {
+ if ( boxSizingReliableVal == null ) {
+ computeStyleTests();
+ }
+ return boxSizingReliableVal;
+ },
+ pixelMarginRight: function() {
+
+ // Support: Android 4.0-4.3
+ // We're checking for boxSizingReliableVal here instead of pixelMarginRightVal
+ // since that compresses better and they're computed together anyway.
+ if ( boxSizingReliableVal == null ) {
+ computeStyleTests();
+ }
+ return pixelMarginRightVal;
+ },
+ reliableMarginLeft: function() {
+
+ // Support: IE <=8 only, Android 4.0 - 4.3 only, Firefox <=3 - 37
+ if ( boxSizingReliableVal == null ) {
+ computeStyleTests();
+ }
+ return reliableMarginLeftVal;
+ },
+ reliableMarginRight: function() {
+
+ // Support: Android 2.3
+ // Check if div with explicit width and no margin-right incorrectly
+ // gets computed margin-right based on width of container. (#3333)
+ // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right
+ // This support function is only executed once so no memoizing is needed.
+ var ret,
+ marginDiv = div.appendChild( document.createElement( "div" ) );
+
+ // Reset CSS: box-sizing; display; margin; border; padding
+ marginDiv.style.cssText = div.style.cssText =
+
+ // Support: Android 2.3
+ // Vendor-prefix box-sizing
+ "-webkit-box-sizing:content-box;box-sizing:content-box;" +
+ "display:block;margin:0;border:0;padding:0";
+ marginDiv.style.marginRight = marginDiv.style.width = "0";
+ div.style.width = "1px";
+ documentElement.appendChild( container );
+
+ ret = !parseFloat( window.getComputedStyle( marginDiv ).marginRight );
+
+ documentElement.removeChild( container );
+ div.removeChild( marginDiv );
+
+ return ret;
+ }
+ } );
+} )();
+
+
+function curCSS( elem, name, computed ) {
+ var width, minWidth, maxWidth, ret,
+ style = elem.style;
+
+ computed = computed || getStyles( elem );
+
+ // Support: IE9
+ // getPropertyValue is only needed for .css('filter') (#12537)
+ if ( computed ) {
+ ret = computed.getPropertyValue( name ) || computed[ name ];
+
+ if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) {
+ ret = jQuery.style( elem, name );
+ }
+
+ // A tribute to the "awesome hack by Dean Edwards"
+ // Android Browser returns percentage for some values,
+ // but width seems to be reliably pixels.
+ // This is against the CSSOM draft spec:
+ // http://dev.w3.org/csswg/cssom/#resolved-values
+ if ( !support.pixelMarginRight() && rnumnonpx.test( ret ) && rmargin.test( name ) ) {
+
+ // Remember the original values
+ width = style.width;
+ minWidth = style.minWidth;
+ maxWidth = style.maxWidth;
+
+ // Put in the new values to get a computed value out
+ style.minWidth = style.maxWidth = style.width = ret;
+ ret = computed.width;
+
+ // Revert the changed values
+ style.width = width;
+ style.minWidth = minWidth;
+ style.maxWidth = maxWidth;
+ }
+ }
+
+ return ret !== undefined ?
+
+ // Support: IE9-11+
+ // IE returns zIndex value as an integer.
+ ret + "" :
+ ret;
+}
+
+
+function addGetHookIf( conditionFn, hookFn ) {
+
+ // Define the hook, we'll check on the first run if it's really needed.
+ return {
+ get: function() {
+ if ( conditionFn() ) {
+
+ // Hook not needed (or it's not possible to use it due
+ // to missing dependency), remove it.
+ delete this.get;
+ return;
+ }
+
+ // Hook needed; redefine it so that the support test is not executed again.
+ return ( this.get = hookFn ).apply( this, arguments );
+ }
+ };
+}
+
+
+var
+
+ // Swappable if display is none or starts with table
+ // except "table", "table-cell", or "table-caption"
+ // See here for display values: https://developer.mozilla.org/en-US/docs/CSS/display
+ rdisplayswap = /^(none|table(?!-c[ea]).+)/,
+
+ cssShow = { position: "absolute", visibility: "hidden", display: "block" },
+ cssNormalTransform = {
+ letterSpacing: "0",
+ fontWeight: "400"
+ },
+
+ cssPrefixes = [ "Webkit", "O", "Moz", "ms" ],
+ emptyStyle = document.createElement( "div" ).style;
+
+// Return a css property mapped to a potentially vendor prefixed property
+function vendorPropName( name ) {
+
+ // Shortcut for names that are not vendor prefixed
+ if ( name in emptyStyle ) {
+ return name;
+ }
+
+ // Check for vendor prefixed names
+ var capName = name[ 0 ].toUpperCase() + name.slice( 1 ),
+ i = cssPrefixes.length;
+
+ while ( i-- ) {
+ name = cssPrefixes[ i ] + capName;
+ if ( name in emptyStyle ) {
+ return name;
+ }
+ }
+}
+
+function setPositiveNumber( elem, value, subtract ) {
+
+ // Any relative (+/-) values have already been
+ // normalized at this point
+ var matches = rcssNum.exec( value );
+ return matches ?
+
+ // Guard against undefined "subtract", e.g., when used as in cssHooks
+ Math.max( 0, matches[ 2 ] - ( subtract || 0 ) ) + ( matches[ 3 ] || "px" ) :
+ value;
+}
+
+function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) {
+ var i = extra === ( isBorderBox ? "border" : "content" ) ?
+
+ // If we already have the right measurement, avoid augmentation
+ 4 :
+
+ // Otherwise initialize for horizontal or vertical properties
+ name === "width" ? 1 : 0,
+
+ val = 0;
+
+ for ( ; i < 4; i += 2 ) {
+
+ // Both box models exclude margin, so add it if we want it
+ if ( extra === "margin" ) {
+ val += jQuery.css( elem, extra + cssExpand[ i ], true, styles );
+ }
+
+ if ( isBorderBox ) {
+
+ // border-box includes padding, so remove it if we want content
+ if ( extra === "content" ) {
+ val -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );
+ }
+
+ // At this point, extra isn't border nor margin, so remove border
+ if ( extra !== "margin" ) {
+ val -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
+ }
+ } else {
+
+ // At this point, extra isn't content, so add padding
+ val += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );
+
+ // At this point, extra isn't content nor padding, so add border
+ if ( extra !== "padding" ) {
+ val += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
+ }
+ }
+ }
+
+ return val;
+}
+
+function getWidthOrHeight( elem, name, extra ) {
+
+ // Start with offset property, which is equivalent to the border-box value
+ var valueIsBorderBox = true,
+ val = name === "width" ? elem.offsetWidth : elem.offsetHeight,
+ styles = getStyles( elem ),
+ isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box";
+
+ // Support: IE11 only
+ // In IE 11 fullscreen elements inside of an iframe have
+ // 100x too small dimensions (gh-1764).
+ if ( document.msFullscreenElement && window.top !== window ) {
+
+ // Support: IE11 only
+ // Running getBoundingClientRect on a disconnected node
+ // in IE throws an error.
+ if ( elem.getClientRects().length ) {
+ val = Math.round( elem.getBoundingClientRect()[ name ] * 100 );
+ }
+ }
+
+ // Some non-html elements return undefined for offsetWidth, so check for null/undefined
+ // svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285
+ // MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668
+ if ( val <= 0 || val == null ) {
+
+ // Fall back to computed then uncomputed css if necessary
+ val = curCSS( elem, name, styles );
+ if ( val < 0 || val == null ) {
+ val = elem.style[ name ];
+ }
+
+ // Computed unit is not pixels. Stop here and return.
+ if ( rnumnonpx.test( val ) ) {
+ return val;
+ }
+
+ // Check for style in case a browser which returns unreliable values
+ // for getComputedStyle silently falls back to the reliable elem.style
+ valueIsBorderBox = isBorderBox &&
+ ( support.boxSizingReliable() || val === elem.style[ name ] );
+
+ // Normalize "", auto, and prepare for extra
+ val = parseFloat( val ) || 0;
+ }
+
+ // Use the active box-sizing model to add/subtract irrelevant styles
+ return ( val +
+ augmentWidthOrHeight(
+ elem,
+ name,
+ extra || ( isBorderBox ? "border" : "content" ),
+ valueIsBorderBox,
+ styles
+ )
+ ) + "px";
+}
+
+function showHide( elements, show ) {
+ var display, elem, hidden,
+ values = [],
+ index = 0,
+ length = elements.length;
+
+ for ( ; index < length; index++ ) {
+ elem = elements[ index ];
+ if ( !elem.style ) {
+ continue;
+ }
+
+ values[ index ] = dataPriv.get( elem, "olddisplay" );
+ display = elem.style.display;
+ if ( show ) {
+
+ // Reset the inline display of this element to learn if it is
+ // being hidden by cascaded rules or not
+ if ( !values[ index ] && display === "none" ) {
+ elem.style.display = "";
+ }
+
+ // Set elements which have been overridden with display: none
+ // in a stylesheet to whatever the default browser style is
+ // for such an element
+ if ( elem.style.display === "" && isHidden( elem ) ) {
+ values[ index ] = dataPriv.access(
+ elem,
+ "olddisplay",
+ defaultDisplay( elem.nodeName )
+ );
+ }
+ } else {
+ hidden = isHidden( elem );
+
+ if ( display !== "none" || !hidden ) {
+ dataPriv.set(
+ elem,
+ "olddisplay",
+ hidden ? display : jQuery.css( elem, "display" )
+ );
+ }
+ }
+ }
+
+ // Set the display of most of the elements in a second loop
+ // to avoid the constant reflow
+ for ( index = 0; index < length; index++ ) {
+ elem = elements[ index ];
+ if ( !elem.style ) {
+ continue;
+ }
+ if ( !show || elem.style.display === "none" || elem.style.display === "" ) {
+ elem.style.display = show ? values[ index ] || "" : "none";
+ }
+ }
+
+ return elements;
+}
+
+jQuery.extend( {
+
+ // Add in style property hooks for overriding the default
+ // behavior of getting and setting a style property
+ cssHooks: {
+ opacity: {
+ get: function( elem, computed ) {
+ if ( computed ) {
+
+ // We should always get a number back from opacity
+ var ret = curCSS( elem, "opacity" );
+ return ret === "" ? "1" : ret;
+ }
+ }
+ }
+ },
+
+ // Don't automatically add "px" to these possibly-unitless properties
+ cssNumber: {
+ "animationIterationCount": true,
+ "columnCount": true,
+ "fillOpacity": true,
+ "flexGrow": true,
+ "flexShrink": true,
+ "fontWeight": true,
+ "lineHeight": true,
+ "opacity": true,
+ "order": true,
+ "orphans": true,
+ "widows": true,
+ "zIndex": true,
+ "zoom": true
+ },
+
+ // Add in properties whose names you wish to fix before
+ // setting or getting the value
+ cssProps: {
+ "float": "cssFloat"
+ },
+
+ // Get and set the style property on a DOM Node
+ style: function( elem, name, value, extra ) {
+
+ // Don't set styles on text and comment nodes
+ if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) {
+ return;
+ }
+
+ // Make sure that we're working with the right name
+ var ret, type, hooks,
+ origName = jQuery.camelCase( name ),
+ style = elem.style;
+
+ name = jQuery.cssProps[ origName ] ||
+ ( jQuery.cssProps[ origName ] = vendorPropName( origName ) || origName );
+
+ // Gets hook for the prefixed version, then unprefixed version
+ hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
+
+ // Check if we're setting a value
+ if ( value !== undefined ) {
+ type = typeof value;
+
+ // Convert "+=" or "-=" to relative numbers (#7345)
+ if ( type === "string" && ( ret = rcssNum.exec( value ) ) && ret[ 1 ] ) {
+ value = adjustCSS( elem, name, ret );
+
+ // Fixes bug #9237
+ type = "number";
+ }
+
+ // Make sure that null and NaN values aren't set (#7116)
+ if ( value == null || value !== value ) {
+ return;
+ }
+
+ // If a number was passed in, add the unit (except for certain CSS properties)
+ if ( type === "number" ) {
+ value += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? "" : "px" );
+ }
+
+ // Support: IE9-11+
+ // background-* props affect original clone's values
+ if ( !support.clearCloneStyle && value === "" && name.indexOf( "background" ) === 0 ) {
+ style[ name ] = "inherit";
+ }
+
+ // If a hook was provided, use that value, otherwise just set the specified value
+ if ( !hooks || !( "set" in hooks ) ||
+ ( value = hooks.set( elem, value, extra ) ) !== undefined ) {
+
+ style[ name ] = value;
+ }
+
+ } else {
+
+ // If a hook was provided get the non-computed value from there
+ if ( hooks && "get" in hooks &&
+ ( ret = hooks.get( elem, false, extra ) ) !== undefined ) {
+
+ return ret;
+ }
+
+ // Otherwise just get the value from the style object
+ return style[ name ];
+ }
+ },
+
+ css: function( elem, name, extra, styles ) {
+ var val, num, hooks,
+ origName = jQuery.camelCase( name );
+
+ // Make sure that we're working with the right name
+ name = jQuery.cssProps[ origName ] ||
+ ( jQuery.cssProps[ origName ] = vendorPropName( origName ) || origName );
+
+ // Try prefixed name followed by the unprefixed name
+ hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
+
+ // If a hook was provided get the computed value from there
+ if ( hooks && "get" in hooks ) {
+ val = hooks.get( elem, true, extra );
+ }
+
+ // Otherwise, if a way to get the computed value exists, use that
+ if ( val === undefined ) {
+ val = curCSS( elem, name, styles );
+ }
+
+ // Convert "normal" to computed value
+ if ( val === "normal" && name in cssNormalTransform ) {
+ val = cssNormalTransform[ name ];
+ }
+
+ // Make numeric if forced or a qualifier was provided and val looks numeric
+ if ( extra === "" || extra ) {
+ num = parseFloat( val );
+ return extra === true || isFinite( num ) ? num || 0 : val;
+ }
+ return val;
+ }
+} );
+
+jQuery.each( [ "height", "width" ], function( i, name ) {
+ jQuery.cssHooks[ name ] = {
+ get: function( elem, computed, extra ) {
+ if ( computed ) {
+
+ // Certain elements can have dimension info if we invisibly show them
+ // but it must have a current display style that would benefit
+ return rdisplayswap.test( jQuery.css( elem, "display" ) ) &&
+ elem.offsetWidth === 0 ?
+ swap( elem, cssShow, function() {
+ return getWidthOrHeight( elem, name, extra );
+ } ) :
+ getWidthOrHeight( elem, name, extra );
+ }
+ },
+
+ set: function( elem, value, extra ) {
+ var matches,
+ styles = extra && getStyles( elem ),
+ subtract = extra && augmentWidthOrHeight(
+ elem,
+ name,
+ extra,
+ jQuery.css( elem, "boxSizing", false, styles ) === "border-box",
+ styles
+ );
+
+ // Convert to pixels if value adjustment is needed
+ if ( subtract && ( matches = rcssNum.exec( value ) ) &&
+ ( matches[ 3 ] || "px" ) !== "px" ) {
+
+ elem.style[ name ] = value;
+ value = jQuery.css( elem, name );
+ }
+
+ return setPositiveNumber( elem, value, subtract );
+ }
+ };
+} );
+
+jQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft,
+ function( elem, computed ) {
+ if ( computed ) {
+ return ( parseFloat( curCSS( elem, "marginLeft" ) ) ||
+ elem.getBoundingClientRect().left -
+ swap( elem, { marginLeft: 0 }, function() {
+ return elem.getBoundingClientRect().left;
+ } )
+ ) + "px";
+ }
+ }
+);
+
+// Support: Android 2.3
+jQuery.cssHooks.marginRight = addGetHookIf( support.reliableMarginRight,
+ function( elem, computed ) {
+ if ( computed ) {
+ return swap( elem, { "display": "inline-block" },
+ curCSS, [ elem, "marginRight" ] );
+ }
+ }
+);
+
+// These hooks are used by animate to expand properties
+jQuery.each( {
+ margin: "",
+ padding: "",
+ border: "Width"
+}, function( prefix, suffix ) {
+ jQuery.cssHooks[ prefix + suffix ] = {
+ expand: function( value ) {
+ var i = 0,
+ expanded = {},
+
+ // Assumes a single number if not a string
+ parts = typeof value === "string" ? value.split( " " ) : [ value ];
+
+ for ( ; i < 4; i++ ) {
+ expanded[ prefix + cssExpand[ i ] + suffix ] =
+ parts[ i ] || parts[ i - 2 ] || parts[ 0 ];
+ }
+
+ return expanded;
+ }
+ };
+
+ if ( !rmargin.test( prefix ) ) {
+ jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber;
+ }
+} );
+
+jQuery.fn.extend( {
+ css: function( name, value ) {
+ return access( this, function( elem, name, value ) {
+ var styles, len,
+ map = {},
+ i = 0;
+
+ if ( jQuery.isArray( name ) ) {
+ styles = getStyles( elem );
+ len = name.length;
+
+ for ( ; i < len; i++ ) {
+ map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles );
+ }
+
+ return map;
+ }
+
+ return value !== undefined ?
+ jQuery.style( elem, name, value ) :
+ jQuery.css( elem, name );
+ }, name, value, arguments.length > 1 );
+ },
+ show: function() {
+ return showHide( this, true );
+ },
+ hide: function() {
+ return showHide( this );
+ },
+ toggle: function( state ) {
+ if ( typeof state === "boolean" ) {
+ return state ? this.show() : this.hide();
+ }
+
+ return this.each( function() {
+ if ( isHidden( this ) ) {
+ jQuery( this ).show();
+ } else {
+ jQuery( this ).hide();
+ }
+ } );
+ }
+} );
+
+
+function Tween( elem, options, prop, end, easing ) {
+ return new Tween.prototype.init( elem, options, prop, end, easing );
+}
+jQuery.Tween = Tween;
+
+Tween.prototype = {
+ constructor: Tween,
+ init: function( elem, options, prop, end, easing, unit ) {
+ this.elem = elem;
+ this.prop = prop;
+ this.easing = easing || jQuery.easing._default;
+ this.options = options;
+ this.start = this.now = this.cur();
+ this.end = end;
+ this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" );
+ },
+ cur: function() {
+ var hooks = Tween.propHooks[ this.prop ];
+
+ return hooks && hooks.get ?
+ hooks.get( this ) :
+ Tween.propHooks._default.get( this );
+ },
+ run: function( percent ) {
+ var eased,
+ hooks = Tween.propHooks[ this.prop ];
+
+ if ( this.options.duration ) {
+ this.pos = eased = jQuery.easing[ this.easing ](
+ percent, this.options.duration * percent, 0, 1, this.options.duration
+ );
+ } else {
+ this.pos = eased = percent;
+ }
+ this.now = ( this.end - this.start ) * eased + this.start;
+
+ if ( this.options.step ) {
+ this.options.step.call( this.elem, this.now, this );
+ }
+
+ if ( hooks && hooks.set ) {
+ hooks.set( this );
+ } else {
+ Tween.propHooks._default.set( this );
+ }
+ return this;
+ }
+};
+
+Tween.prototype.init.prototype = Tween.prototype;
+
+Tween.propHooks = {
+ _default: {
+ get: function( tween ) {
+ var result;
+
+ // Use a property on the element directly when it is not a DOM element,
+ // or when there is no matching style property that exists.
+ if ( tween.elem.nodeType !== 1 ||
+ tween.elem[ tween.prop ] != null && tween.elem.style[ tween.prop ] == null ) {
+ return tween.elem[ tween.prop ];
+ }
+
+ // Passing an empty string as a 3rd parameter to .css will automatically
+ // attempt a parseFloat and fallback to a string if the parse fails.
+ // Simple values such as "10px" are parsed to Float;
+ // complex values such as "rotate(1rad)" are returned as-is.
+ result = jQuery.css( tween.elem, tween.prop, "" );
+
+ // Empty strings, null, undefined and "auto" are converted to 0.
+ return !result || result === "auto" ? 0 : result;
+ },
+ set: function( tween ) {
+
+ // Use step hook for back compat.
+ // Use cssHook if its there.
+ // Use .style if available and use plain properties where available.
+ if ( jQuery.fx.step[ tween.prop ] ) {
+ jQuery.fx.step[ tween.prop ]( tween );
+ } else if ( tween.elem.nodeType === 1 &&
+ ( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null ||
+ jQuery.cssHooks[ tween.prop ] ) ) {
+ jQuery.style( tween.elem, tween.prop, tween.now + tween.unit );
+ } else {
+ tween.elem[ tween.prop ] = tween.now;
+ }
+ }
+ }
+};
+
+// Support: IE9
+// Panic based approach to setting things on disconnected nodes
+Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = {
+ set: function( tween ) {
+ if ( tween.elem.nodeType && tween.elem.parentNode ) {
+ tween.elem[ tween.prop ] = tween.now;
+ }
+ }
+};
+
+jQuery.easing = {
+ linear: function( p ) {
+ return p;
+ },
+ swing: function( p ) {
+ return 0.5 - Math.cos( p * Math.PI ) / 2;
+ },
+ _default: "swing"
+};
+
+jQuery.fx = Tween.prototype.init;
+
+// Back Compat <1.8 extension point
+jQuery.fx.step = {};
+
+
+
+
+var
+ fxNow, timerId,
+ rfxtypes = /^(?:toggle|show|hide)$/,
+ rrun = /queueHooks$/;
+
+// Animations created synchronously will run synchronously
+function createFxNow() {
+ window.setTimeout( function() {
+ fxNow = undefined;
+ } );
+ return ( fxNow = jQuery.now() );
+}
+
+// Generate parameters to create a standard animation
+function genFx( type, includeWidth ) {
+ var which,
+ i = 0,
+ attrs = { height: type };
+
+ // If we include width, step value is 1 to do all cssExpand values,
+ // otherwise step value is 2 to skip over Left and Right
+ includeWidth = includeWidth ? 1 : 0;
+ for ( ; i < 4 ; i += 2 - includeWidth ) {
+ which = cssExpand[ i ];
+ attrs[ "margin" + which ] = attrs[ "padding" + which ] = type;
+ }
+
+ if ( includeWidth ) {
+ attrs.opacity = attrs.width = type;
+ }
+
+ return attrs;
+}
+
+function createTween( value, prop, animation ) {
+ var tween,
+ collection = ( Animation.tweeners[ prop ] || [] ).concat( Animation.tweeners[ "*" ] ),
+ index = 0,
+ length = collection.length;
+ for ( ; index < length; index++ ) {
+ if ( ( tween = collection[ index ].call( animation, prop, value ) ) ) {
+
+ // We're done with this property
+ return tween;
+ }
+ }
+}
+
+function defaultPrefilter( elem, props, opts ) {
+ /* jshint validthis: true */
+ var prop, value, toggle, tween, hooks, oldfire, display, checkDisplay,
+ anim = this,
+ orig = {},
+ style = elem.style,
+ hidden = elem.nodeType && isHidden( elem ),
+ dataShow = dataPriv.get( elem, "fxshow" );
+
+ // Handle queue: false promises
+ if ( !opts.queue ) {
+ hooks = jQuery._queueHooks( elem, "fx" );
+ if ( hooks.unqueued == null ) {
+ hooks.unqueued = 0;
+ oldfire = hooks.empty.fire;
+ hooks.empty.fire = function() {
+ if ( !hooks.unqueued ) {
+ oldfire();
+ }
+ };
+ }
+ hooks.unqueued++;
+
+ anim.always( function() {
+
+ // Ensure the complete handler is called before this completes
+ anim.always( function() {
+ hooks.unqueued--;
+ if ( !jQuery.queue( elem, "fx" ).length ) {
+ hooks.empty.fire();
+ }
+ } );
+ } );
+ }
+
+ // Height/width overflow pass
+ if ( elem.nodeType === 1 && ( "height" in props || "width" in props ) ) {
+
+ // Make sure that nothing sneaks out
+ // Record all 3 overflow attributes because IE9-10 do not
+ // change the overflow attribute when overflowX and
+ // overflowY are set to the same value
+ opts.overflow = [ style.overflow, style.overflowX, style.overflowY ];
+
+ // Set display property to inline-block for height/width
+ // animations on inline elements that are having width/height animated
+ display = jQuery.css( elem, "display" );
+
+ // Test default display if display is currently "none"
+ checkDisplay = display === "none" ?
+ dataPriv.get( elem, "olddisplay" ) || defaultDisplay( elem.nodeName ) : display;
+
+ if ( checkDisplay === "inline" && jQuery.css( elem, "float" ) === "none" ) {
+ style.display = "inline-block";
+ }
+ }
+
+ if ( opts.overflow ) {
+ style.overflow = "hidden";
+ anim.always( function() {
+ style.overflow = opts.overflow[ 0 ];
+ style.overflowX = opts.overflow[ 1 ];
+ style.overflowY = opts.overflow[ 2 ];
+ } );
+ }
+
+ // show/hide pass
+ for ( prop in props ) {
+ value = props[ prop ];
+ if ( rfxtypes.exec( value ) ) {
+ delete props[ prop ];
+ toggle = toggle || value === "toggle";
+ if ( value === ( hidden ? "hide" : "show" ) ) {
+
+ // If there is dataShow left over from a stopped hide or show
+ // and we are going to proceed with show, we should pretend to be hidden
+ if ( value === "show" && dataShow && dataShow[ prop ] !== undefined ) {
+ hidden = true;
+ } else {
+ continue;
+ }
+ }
+ orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop );
+
+ // Any non-fx value stops us from restoring the original display value
+ } else {
+ display = undefined;
+ }
+ }
+
+ if ( !jQuery.isEmptyObject( orig ) ) {
+ if ( dataShow ) {
+ if ( "hidden" in dataShow ) {
+ hidden = dataShow.hidden;
+ }
+ } else {
+ dataShow = dataPriv.access( elem, "fxshow", {} );
+ }
+
+ // Store state if its toggle - enables .stop().toggle() to "reverse"
+ if ( toggle ) {
+ dataShow.hidden = !hidden;
+ }
+ if ( hidden ) {
+ jQuery( elem ).show();
+ } else {
+ anim.done( function() {
+ jQuery( elem ).hide();
+ } );
+ }
+ anim.done( function() {
+ var prop;
+
+ dataPriv.remove( elem, "fxshow" );
+ for ( prop in orig ) {
+ jQuery.style( elem, prop, orig[ prop ] );
+ }
+ } );
+ for ( prop in orig ) {
+ tween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim );
+
+ if ( !( prop in dataShow ) ) {
+ dataShow[ prop ] = tween.start;
+ if ( hidden ) {
+ tween.end = tween.start;
+ tween.start = prop === "width" || prop === "height" ? 1 : 0;
+ }
+ }
+ }
+
+ // If this is a noop like .hide().hide(), restore an overwritten display value
+ } else if ( ( display === "none" ? defaultDisplay( elem.nodeName ) : display ) === "inline" ) {
+ style.display = display;
+ }
+}
+
+function propFilter( props, specialEasing ) {
+ var index, name, easing, value, hooks;
+
+ // camelCase, specialEasing and expand cssHook pass
+ for ( index in props ) {
+ name = jQuery.camelCase( index );
+ easing = specialEasing[ name ];
+ value = props[ index ];
+ if ( jQuery.isArray( value ) ) {
+ easing = value[ 1 ];
+ value = props[ index ] = value[ 0 ];
+ }
+
+ if ( index !== name ) {
+ props[ name ] = value;
+ delete props[ index ];
+ }
+
+ hooks = jQuery.cssHooks[ name ];
+ if ( hooks && "expand" in hooks ) {
+ value = hooks.expand( value );
+ delete props[ name ];
+
+ // Not quite $.extend, this won't overwrite existing keys.
+ // Reusing 'index' because we have the correct "name"
+ for ( index in value ) {
+ if ( !( index in props ) ) {
+ props[ index ] = value[ index ];
+ specialEasing[ index ] = easing;
+ }
+ }
+ } else {
+ specialEasing[ name ] = easing;
+ }
+ }
+}
+
+function Animation( elem, properties, options ) {
+ var result,
+ stopped,
+ index = 0,
+ length = Animation.prefilters.length,
+ deferred = jQuery.Deferred().always( function() {
+
+ // Don't match elem in the :animated selector
+ delete tick.elem;
+ } ),
+ tick = function() {
+ if ( stopped ) {
+ return false;
+ }
+ var currentTime = fxNow || createFxNow(),
+ remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ),
+
+ // Support: Android 2.3
+ // Archaic crash bug won't allow us to use `1 - ( 0.5 || 0 )` (#12497)
+ temp = remaining / animation.duration || 0,
+ percent = 1 - temp,
+ index = 0,
+ length = animation.tweens.length;
+
+ for ( ; index < length ; index++ ) {
+ animation.tweens[ index ].run( percent );
+ }
+
+ deferred.notifyWith( elem, [ animation, percent, remaining ] );
+
+ if ( percent < 1 && length ) {
+ return remaining;
+ } else {
+ deferred.resolveWith( elem, [ animation ] );
+ return false;
+ }
+ },
+ animation = deferred.promise( {
+ elem: elem,
+ props: jQuery.extend( {}, properties ),
+ opts: jQuery.extend( true, {
+ specialEasing: {},
+ easing: jQuery.easing._default
+ }, options ),
+ originalProperties: properties,
+ originalOptions: options,
+ startTime: fxNow || createFxNow(),
+ duration: options.duration,
+ tweens: [],
+ createTween: function( prop, end ) {
+ var tween = jQuery.Tween( elem, animation.opts, prop, end,
+ animation.opts.specialEasing[ prop ] || animation.opts.easing );
+ animation.tweens.push( tween );
+ return tween;
+ },
+ stop: function( gotoEnd ) {
+ var index = 0,
+
+ // If we are going to the end, we want to run all the tweens
+ // otherwise we skip this part
+ length = gotoEnd ? animation.tweens.length : 0;
+ if ( stopped ) {
+ return this;
+ }
+ stopped = true;
+ for ( ; index < length ; index++ ) {
+ animation.tweens[ index ].run( 1 );
+ }
+
+ // Resolve when we played the last frame; otherwise, reject
+ if ( gotoEnd ) {
+ deferred.notifyWith( elem, [ animation, 1, 0 ] );
+ deferred.resolveWith( elem, [ animation, gotoEnd ] );
+ } else {
+ deferred.rejectWith( elem, [ animation, gotoEnd ] );
+ }
+ return this;
+ }
+ } ),
+ props = animation.props;
+
+ propFilter( props, animation.opts.specialEasing );
+
+ for ( ; index < length ; index++ ) {
+ result = Animation.prefilters[ index ].call( animation, elem, props, animation.opts );
+ if ( result ) {
+ if ( jQuery.isFunction( result.stop ) ) {
+ jQuery._queueHooks( animation.elem, animation.opts.queue ).stop =
+ jQuery.proxy( result.stop, result );
+ }
+ return result;
+ }
+ }
+
+ jQuery.map( props, createTween, animation );
+
+ if ( jQuery.isFunction( animation.opts.start ) ) {
+ animation.opts.start.call( elem, animation );
+ }
+
+ jQuery.fx.timer(
+ jQuery.extend( tick, {
+ elem: elem,
+ anim: animation,
+ queue: animation.opts.queue
+ } )
+ );
+
+ // attach callbacks from options
+ return animation.progress( animation.opts.progress )
+ .done( animation.opts.done, animation.opts.complete )
+ .fail( animation.opts.fail )
+ .always( animation.opts.always );
+}
+
+jQuery.Animation = jQuery.extend( Animation, {
+ tweeners: {
+ "*": [ function( prop, value ) {
+ var tween = this.createTween( prop, value );
+ adjustCSS( tween.elem, prop, rcssNum.exec( value ), tween );
+ return tween;
+ } ]
+ },
+
+ tweener: function( props, callback ) {
+ if ( jQuery.isFunction( props ) ) {
+ callback = props;
+ props = [ "*" ];
+ } else {
+ props = props.match( rnotwhite );
+ }
+
+ var prop,
+ index = 0,
+ length = props.length;
+
+ for ( ; index < length ; index++ ) {
+ prop = props[ index ];
+ Animation.tweeners[ prop ] = Animation.tweeners[ prop ] || [];
+ Animation.tweeners[ prop ].unshift( callback );
+ }
+ },
+
+ prefilters: [ defaultPrefilter ],
+
+ prefilter: function( callback, prepend ) {
+ if ( prepend ) {
+ Animation.prefilters.unshift( callback );
+ } else {
+ Animation.prefilters.push( callback );
+ }
+ }
+} );
+
+jQuery.speed = function( speed, easing, fn ) {
+ var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : {
+ complete: fn || !fn && easing ||
+ jQuery.isFunction( speed ) && speed,
+ duration: speed,
+ easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing
+ };
+
+ opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ?
+ opt.duration : opt.duration in jQuery.fx.speeds ?
+ jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default;
+
+ // Normalize opt.queue - true/undefined/null -> "fx"
+ if ( opt.queue == null || opt.queue === true ) {
+ opt.queue = "fx";
+ }
+
+ // Queueing
+ opt.old = opt.complete;
+
+ opt.complete = function() {
+ if ( jQuery.isFunction( opt.old ) ) {
+ opt.old.call( this );
+ }
+
+ if ( opt.queue ) {
+ jQuery.dequeue( this, opt.queue );
+ }
+ };
+
+ return opt;
+};
+
+jQuery.fn.extend( {
+ fadeTo: function( speed, to, easing, callback ) {
+
+ // Show any hidden elements after setting opacity to 0
+ return this.filter( isHidden ).css( "opacity", 0 ).show()
+
+ // Animate to the value specified
+ .end().animate( { opacity: to }, speed, easing, callback );
+ },
+ animate: function( prop, speed, easing, callback ) {
+ var empty = jQuery.isEmptyObject( prop ),
+ optall = jQuery.speed( speed, easing, callback ),
+ doAnimation = function() {
+
+ // Operate on a copy of prop so per-property easing won't be lost
+ var anim = Animation( this, jQuery.extend( {}, prop ), optall );
+
+ // Empty animations, or finishing resolves immediately
+ if ( empty || dataPriv.get( this, "finish" ) ) {
+ anim.stop( true );
+ }
+ };
+ doAnimation.finish = doAnimation;
+
+ return empty || optall.queue === false ?
+ this.each( doAnimation ) :
+ this.queue( optall.queue, doAnimation );
+ },
+ stop: function( type, clearQueue, gotoEnd ) {
+ var stopQueue = function( hooks ) {
+ var stop = hooks.stop;
+ delete hooks.stop;
+ stop( gotoEnd );
+ };
+
+ if ( typeof type !== "string" ) {
+ gotoEnd = clearQueue;
+ clearQueue = type;
+ type = undefined;
+ }
+ if ( clearQueue && type !== false ) {
+ this.queue( type || "fx", [] );
+ }
+
+ return this.each( function() {
+ var dequeue = true,
+ index = type != null && type + "queueHooks",
+ timers = jQuery.timers,
+ data = dataPriv.get( this );
+
+ if ( index ) {
+ if ( data[ index ] && data[ index ].stop ) {
+ stopQueue( data[ index ] );
+ }
+ } else {
+ for ( index in data ) {
+ if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) {
+ stopQueue( data[ index ] );
+ }
+ }
+ }
+
+ for ( index = timers.length; index--; ) {
+ if ( timers[ index ].elem === this &&
+ ( type == null || timers[ index ].queue === type ) ) {
+
+ timers[ index ].anim.stop( gotoEnd );
+ dequeue = false;
+ timers.splice( index, 1 );
+ }
+ }
+
+ // Start the next in the queue if the last step wasn't forced.
+ // Timers currently will call their complete callbacks, which
+ // will dequeue but only if they were gotoEnd.
+ if ( dequeue || !gotoEnd ) {
+ jQuery.dequeue( this, type );
+ }
+ } );
+ },
+ finish: function( type ) {
+ if ( type !== false ) {
+ type = type || "fx";
+ }
+ return this.each( function() {
+ var index,
+ data = dataPriv.get( this ),
+ queue = data[ type + "queue" ],
+ hooks = data[ type + "queueHooks" ],
+ timers = jQuery.timers,
+ length = queue ? queue.length : 0;
+
+ // Enable finishing flag on private data
+ data.finish = true;
+
+ // Empty the queue first
+ jQuery.queue( this, type, [] );
+
+ if ( hooks && hooks.stop ) {
+ hooks.stop.call( this, true );
+ }
+
+ // Look for any active animations, and finish them
+ for ( index = timers.length; index--; ) {
+ if ( timers[ index ].elem === this && timers[ index ].queue === type ) {
+ timers[ index ].anim.stop( true );
+ timers.splice( index, 1 );
+ }
+ }
+
+ // Look for any animations in the old queue and finish them
+ for ( index = 0; index < length; index++ ) {
+ if ( queue[ index ] && queue[ index ].finish ) {
+ queue[ index ].finish.call( this );
+ }
+ }
+
+ // Turn off finishing flag
+ delete data.finish;
+ } );
+ }
+} );
+
+jQuery.each( [ "toggle", "show", "hide" ], function( i, name ) {
+ var cssFn = jQuery.fn[ name ];
+ jQuery.fn[ name ] = function( speed, easing, callback ) {
+ return speed == null || typeof speed === "boolean" ?
+ cssFn.apply( this, arguments ) :
+ this.animate( genFx( name, true ), speed, easing, callback );
+ };
+} );
+
+// Generate shortcuts for custom animations
+jQuery.each( {
+ slideDown: genFx( "show" ),
+ slideUp: genFx( "hide" ),
+ slideToggle: genFx( "toggle" ),
+ fadeIn: { opacity: "show" },
+ fadeOut: { opacity: "hide" },
+ fadeToggle: { opacity: "toggle" }
+}, function( name, props ) {
+ jQuery.fn[ name ] = function( speed, easing, callback ) {
+ return this.animate( props, speed, easing, callback );
+ };
+} );
+
+jQuery.timers = [];
+jQuery.fx.tick = function() {
+ var timer,
+ i = 0,
+ timers = jQuery.timers;
+
+ fxNow = jQuery.now();
+
+ for ( ; i < timers.length; i++ ) {
+ timer = timers[ i ];
+
+ // Checks the timer has not already been removed
+ if ( !timer() && timers[ i ] === timer ) {
+ timers.splice( i--, 1 );
+ }
+ }
+
+ if ( !timers.length ) {
+ jQuery.fx.stop();
+ }
+ fxNow = undefined;
+};
+
+jQuery.fx.timer = function( timer ) {
+ jQuery.timers.push( timer );
+ if ( timer() ) {
+ jQuery.fx.start();
+ } else {
+ jQuery.timers.pop();
+ }
+};
+
+jQuery.fx.interval = 13;
+jQuery.fx.start = function() {
+ if ( !timerId ) {
+ timerId = window.setInterval( jQuery.fx.tick, jQuery.fx.interval );
+ }
+};
+
+jQuery.fx.stop = function() {
+ window.clearInterval( timerId );
+
+ timerId = null;
+};
+
+jQuery.fx.speeds = {
+ slow: 600,
+ fast: 200,
+
+ // Default speed
+ _default: 400
+};
+
+
+// Based off of the plugin by Clint Helfers, with permission.
+// http://web.archive.org/web/20100324014747/http://blindsignals.com/index.php/2009/07/jquery-delay/
+jQuery.fn.delay = function( time, type ) {
+ time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time;
+ type = type || "fx";
+
+ return this.queue( type, function( next, hooks ) {
+ var timeout = window.setTimeout( next, time );
+ hooks.stop = function() {
+ window.clearTimeout( timeout );
+ };
+ } );
+};
+
+
+( function() {
+ var input = document.createElement( "input" ),
+ select = document.createElement( "select" ),
+ opt = select.appendChild( document.createElement( "option" ) );
+
+ input.type = "checkbox";
+
+ // Support: iOS<=5.1, Android<=4.2+
+ // Default value for a checkbox should be "on"
+ support.checkOn = input.value !== "";
+
+ // Support: IE<=11+
+ // Must access selectedIndex to make default options select
+ support.optSelected = opt.selected;
+
+ // Support: Android<=2.3
+ // Options inside disabled selects are incorrectly marked as disabled
+ select.disabled = true;
+ support.optDisabled = !opt.disabled;
+
+ // Support: IE<=11+
+ // An input loses its value after becoming a radio
+ input = document.createElement( "input" );
+ input.value = "t";
+ input.type = "radio";
+ support.radioValue = input.value === "t";
+} )();
+
+
+var boolHook,
+ attrHandle = jQuery.expr.attrHandle;
+
+jQuery.fn.extend( {
+ attr: function( name, value ) {
+ return access( this, jQuery.attr, name, value, arguments.length > 1 );
+ },
+
+ removeAttr: function( name ) {
+ return this.each( function() {
+ jQuery.removeAttr( this, name );
+ } );
+ }
+} );
+
+jQuery.extend( {
+ attr: function( elem, name, value ) {
+ var ret, hooks,
+ nType = elem.nodeType;
+
+ // Don't get/set attributes on text, comment and attribute nodes
+ if ( nType === 3 || nType === 8 || nType === 2 ) {
+ return;
+ }
+
+ // Fallback to prop when attributes are not supported
+ if ( typeof elem.getAttribute === "undefined" ) {
+ return jQuery.prop( elem, name, value );
+ }
+
+ // All attributes are lowercase
+ // Grab necessary hook if one is defined
+ if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) {
+ name = name.toLowerCase();
+ hooks = jQuery.attrHooks[ name ] ||
+ ( jQuery.expr.match.bool.test( name ) ? boolHook : undefined );
+ }
+
+ if ( value !== undefined ) {
+ if ( value === null ) {
+ jQuery.removeAttr( elem, name );
+ return;
+ }
+
+ if ( hooks && "set" in hooks &&
+ ( ret = hooks.set( elem, value, name ) ) !== undefined ) {
+ return ret;
+ }
+
+ elem.setAttribute( name, value + "" );
+ return value;
+ }
+
+ if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) {
+ return ret;
+ }
+
+ ret = jQuery.find.attr( elem, name );
+
+ // Non-existent attributes return null, we normalize to undefined
+ return ret == null ? undefined : ret;
+ },
+
+ attrHooks: {
+ type: {
+ set: function( elem, value ) {
+ if ( !support.radioValue && value === "radio" &&
+ jQuery.nodeName( elem, "input" ) ) {
+ var val = elem.value;
+ elem.setAttribute( "type", value );
+ if ( val ) {
+ elem.value = val;
+ }
+ return value;
+ }
+ }
+ }
+ },
+
+ removeAttr: function( elem, value ) {
+ var name, propName,
+ i = 0,
+ attrNames = value && value.match( rnotwhite );
+
+ if ( attrNames && elem.nodeType === 1 ) {
+ while ( ( name = attrNames[ i++ ] ) ) {
+ propName = jQuery.propFix[ name ] || name;
+
+ // Boolean attributes get special treatment (#10870)
+ if ( jQuery.expr.match.bool.test( name ) ) {
+
+ // Set corresponding property to false
+ elem[ propName ] = false;
+ }
+
+ elem.removeAttribute( name );
+ }
+ }
+ }
+} );
+
+// Hooks for boolean attributes
+boolHook = {
+ set: function( elem, value, name ) {
+ if ( value === false ) {
+
+ // Remove boolean attributes when set to false
+ jQuery.removeAttr( elem, name );
+ } else {
+ elem.setAttribute( name, name );
+ }
+ return name;
+ }
+};
+jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( i, name ) {
+ var getter = attrHandle[ name ] || jQuery.find.attr;
+
+ attrHandle[ name ] = function( elem, name, isXML ) {
+ var ret, handle;
+ if ( !isXML ) {
+
+ // Avoid an infinite loop by temporarily removing this function from the getter
+ handle = attrHandle[ name ];
+ attrHandle[ name ] = ret;
+ ret = getter( elem, name, isXML ) != null ?
+ name.toLowerCase() :
+ null;
+ attrHandle[ name ] = handle;
+ }
+ return ret;
+ };
+} );
+
+
+
+
+var rfocusable = /^(?:input|select|textarea|button)$/i,
+ rclickable = /^(?:a|area)$/i;
+
+jQuery.fn.extend( {
+ prop: function( name, value ) {
+ return access( this, jQuery.prop, name, value, arguments.length > 1 );
+ },
+
+ removeProp: function( name ) {
+ return this.each( function() {
+ delete this[ jQuery.propFix[ name ] || name ];
+ } );
+ }
+} );
+
+jQuery.extend( {
+ prop: function( elem, name, value ) {
+ var ret, hooks,
+ nType = elem.nodeType;
+
+ // Don't get/set properties on text, comment and attribute nodes
+ if ( nType === 3 || nType === 8 || nType === 2 ) {
+ return;
+ }
+
+ if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) {
+
+ // Fix name and attach hooks
+ name = jQuery.propFix[ name ] || name;
+ hooks = jQuery.propHooks[ name ];
+ }
+
+ if ( value !== undefined ) {
+ if ( hooks && "set" in hooks &&
+ ( ret = hooks.set( elem, value, name ) ) !== undefined ) {
+ return ret;
+ }
+
+ return ( elem[ name ] = value );
+ }
+
+ if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) {
+ return ret;
+ }
+
+ return elem[ name ];
+ },
+
+ propHooks: {
+ tabIndex: {
+ get: function( elem ) {
+
+ // elem.tabIndex doesn't always return the
+ // correct value when it hasn't been explicitly set
+ // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/
+ // Use proper attribute retrieval(#12072)
+ var tabindex = jQuery.find.attr( elem, "tabindex" );
+
+ return tabindex ?
+ parseInt( tabindex, 10 ) :
+ rfocusable.test( elem.nodeName ) ||
+ rclickable.test( elem.nodeName ) && elem.href ?
+ 0 :
+ -1;
+ }
+ }
+ },
+
+ propFix: {
+ "for": "htmlFor",
+ "class": "className"
+ }
+} );
+
+if ( !support.optSelected ) {
+ jQuery.propHooks.selected = {
+ get: function( elem ) {
+ var parent = elem.parentNode;
+ if ( parent && parent.parentNode ) {
+ parent.parentNode.selectedIndex;
+ }
+ return null;
+ }
+ };
+}
+
+jQuery.each( [
+ "tabIndex",
+ "readOnly",
+ "maxLength",
+ "cellSpacing",
+ "cellPadding",
+ "rowSpan",
+ "colSpan",
+ "useMap",
+ "frameBorder",
+ "contentEditable"
+], function() {
+ jQuery.propFix[ this.toLowerCase() ] = this;
+} );
+
+
+
+
+var rclass = /[\t\r\n\f]/g;
+
+function getClass( elem ) {
+ return elem.getAttribute && elem.getAttribute( "class" ) || "";
+}
+
+jQuery.fn.extend( {
+ addClass: function( value ) {
+ var classes, elem, cur, curValue, clazz, j, finalValue,
+ i = 0;
+
+ if ( jQuery.isFunction( value ) ) {
+ return this.each( function( j ) {
+ jQuery( this ).addClass( value.call( this, j, getClass( this ) ) );
+ } );
+ }
+
+ if ( typeof value === "string" && value ) {
+ classes = value.match( rnotwhite ) || [];
+
+ while ( ( elem = this[ i++ ] ) ) {
+ curValue = getClass( elem );
+ cur = elem.nodeType === 1 &&
+ ( " " + curValue + " " ).replace( rclass, " " );
+
+ if ( cur ) {
+ j = 0;
+ while ( ( clazz = classes[ j++ ] ) ) {
+ if ( cur.indexOf( " " + clazz + " " ) < 0 ) {
+ cur += clazz + " ";
+ }
+ }
+
+ // Only assign if different to avoid unneeded rendering.
+ finalValue = jQuery.trim( cur );
+ if ( curValue !== finalValue ) {
+ elem.setAttribute( "class", finalValue );
+ }
+ }
+ }
+ }
+
+ return this;
+ },
+
+ removeClass: function( value ) {
+ var classes, elem, cur, curValue, clazz, j, finalValue,
+ i = 0;
+
+ if ( jQuery.isFunction( value ) ) {
+ return this.each( function( j ) {
+ jQuery( this ).removeClass( value.call( this, j, getClass( this ) ) );
+ } );
+ }
+
+ if ( !arguments.length ) {
+ return this.attr( "class", "" );
+ }
+
+ if ( typeof value === "string" && value ) {
+ classes = value.match( rnotwhite ) || [];
+
+ while ( ( elem = this[ i++ ] ) ) {
+ curValue = getClass( elem );
+
+ // This expression is here for better compressibility (see addClass)
+ cur = elem.nodeType === 1 &&
+ ( " " + curValue + " " ).replace( rclass, " " );
+
+ if ( cur ) {
+ j = 0;
+ while ( ( clazz = classes[ j++ ] ) ) {
+
+ // Remove *all* instances
+ while ( cur.indexOf( " " + clazz + " " ) > -1 ) {
+ cur = cur.replace( " " + clazz + " ", " " );
+ }
+ }
+
+ // Only assign if different to avoid unneeded rendering.
+ finalValue = jQuery.trim( cur );
+ if ( curValue !== finalValue ) {
+ elem.setAttribute( "class", finalValue );
+ }
+ }
+ }
+ }
+
+ return this;
+ },
+
+ toggleClass: function( value, stateVal ) {
+ var type = typeof value;
+
+ if ( typeof stateVal === "boolean" && type === "string" ) {
+ return stateVal ? this.addClass( value ) : this.removeClass( value );
+ }
+
+ if ( jQuery.isFunction( value ) ) {
+ return this.each( function( i ) {
+ jQuery( this ).toggleClass(
+ value.call( this, i, getClass( this ), stateVal ),
+ stateVal
+ );
+ } );
+ }
+
+ return this.each( function() {
+ var className, i, self, classNames;
+
+ if ( type === "string" ) {
+
+ // Toggle individual class names
+ i = 0;
+ self = jQuery( this );
+ classNames = value.match( rnotwhite ) || [];
+
+ while ( ( className = classNames[ i++ ] ) ) {
+
+ // Check each className given, space separated list
+ if ( self.hasClass( className ) ) {
+ self.removeClass( className );
+ } else {
+ self.addClass( className );
+ }
+ }
+
+ // Toggle whole class name
+ } else if ( value === undefined || type === "boolean" ) {
+ className = getClass( this );
+ if ( className ) {
+
+ // Store className if set
+ dataPriv.set( this, "__className__", className );
+ }
+
+ // If the element has a class name or if we're passed `false`,
+ // then remove the whole classname (if there was one, the above saved it).
+ // Otherwise bring back whatever was previously saved (if anything),
+ // falling back to the empty string if nothing was stored.
+ if ( this.setAttribute ) {
+ this.setAttribute( "class",
+ className || value === false ?
+ "" :
+ dataPriv.get( this, "__className__" ) || ""
+ );
+ }
+ }
+ } );
+ },
+
+ hasClass: function( selector ) {
+ var className, elem,
+ i = 0;
+
+ className = " " + selector + " ";
+ while ( ( elem = this[ i++ ] ) ) {
+ if ( elem.nodeType === 1 &&
+ ( " " + getClass( elem ) + " " ).replace( rclass, " " )
+ .indexOf( className ) > -1
+ ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+} );
+
+
+
+
+var rreturn = /\r/g;
+
+jQuery.fn.extend( {
+ val: function( value ) {
+ var hooks, ret, isFunction,
+ elem = this[ 0 ];
+
+ if ( !arguments.length ) {
+ if ( elem ) {
+ hooks = jQuery.valHooks[ elem.type ] ||
+ jQuery.valHooks[ elem.nodeName.toLowerCase() ];
+
+ if ( hooks &&
+ "get" in hooks &&
+ ( ret = hooks.get( elem, "value" ) ) !== undefined
+ ) {
+ return ret;
+ }
+
+ ret = elem.value;
+
+ return typeof ret === "string" ?
+
+ // Handle most common string cases
+ ret.replace( rreturn, "" ) :
+
+ // Handle cases where value is null/undef or number
+ ret == null ? "" : ret;
+ }
+
+ return;
+ }
+
+ isFunction = jQuery.isFunction( value );
+
+ return this.each( function( i ) {
+ var val;
+
+ if ( this.nodeType !== 1 ) {
+ return;
+ }
+
+ if ( isFunction ) {
+ val = value.call( this, i, jQuery( this ).val() );
+ } else {
+ val = value;
+ }
+
+ // Treat null/undefined as ""; convert numbers to string
+ if ( val == null ) {
+ val = "";
+
+ } else if ( typeof val === "number" ) {
+ val += "";
+
+ } else if ( jQuery.isArray( val ) ) {
+ val = jQuery.map( val, function( value ) {
+ return value == null ? "" : value + "";
+ } );
+ }
+
+ hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ];
+
+ // If set returns undefined, fall back to normal setting
+ if ( !hooks || !( "set" in hooks ) || hooks.set( this, val, "value" ) === undefined ) {
+ this.value = val;
+ }
+ } );
+ }
+} );
+
+jQuery.extend( {
+ valHooks: {
+ option: {
+ get: function( elem ) {
+
+ // Support: IE<11
+ // option.value not trimmed (#14858)
+ return jQuery.trim( elem.value );
+ }
+ },
+ select: {
+ get: function( elem ) {
+ var value, option,
+ options = elem.options,
+ index = elem.selectedIndex,
+ one = elem.type === "select-one" || index < 0,
+ values = one ? null : [],
+ max = one ? index + 1 : options.length,
+ i = index < 0 ?
+ max :
+ one ? index : 0;
+
+ // Loop through all the selected options
+ for ( ; i < max; i++ ) {
+ option = options[ i ];
+
+ // IE8-9 doesn't update selected after form reset (#2551)
+ if ( ( option.selected || i === index ) &&
+
+ // Don't return options that are disabled or in a disabled optgroup
+ ( support.optDisabled ?
+ !option.disabled : option.getAttribute( "disabled" ) === null ) &&
+ ( !option.parentNode.disabled ||
+ !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) {
+
+ // Get the specific value for the option
+ value = jQuery( option ).val();
+
+ // We don't need an array for one selects
+ if ( one ) {
+ return value;
+ }
+
+ // Multi-Selects return an array
+ values.push( value );
+ }
+ }
+
+ return values;
+ },
+
+ set: function( elem, value ) {
+ var optionSet, option,
+ options = elem.options,
+ values = jQuery.makeArray( value ),
+ i = options.length;
+
+ while ( i-- ) {
+ option = options[ i ];
+ if ( option.selected =
+ jQuery.inArray( jQuery.valHooks.option.get( option ), values ) > -1
+ ) {
+ optionSet = true;
+ }
+ }
+
+ // Force browsers to behave consistently when non-matching value is set
+ if ( !optionSet ) {
+ elem.selectedIndex = -1;
+ }
+ return values;
+ }
+ }
+ }
+} );
+
+// Radios and checkboxes getter/setter
+jQuery.each( [ "radio", "checkbox" ], function() {
+ jQuery.valHooks[ this ] = {
+ set: function( elem, value ) {
+ if ( jQuery.isArray( value ) ) {
+ return ( elem.checked = jQuery.inArray( jQuery( elem ).val(), value ) > -1 );
+ }
+ }
+ };
+ if ( !support.checkOn ) {
+ jQuery.valHooks[ this ].get = function( elem ) {
+ return elem.getAttribute( "value" ) === null ? "on" : elem.value;
+ };
+ }
+} );
+
+
+
+
+// Return jQuery for attributes-only inclusion
+
+
+var rfocusMorph = /^(?:focusinfocus|focusoutblur)$/;
+
+jQuery.extend( jQuery.event, {
+
+ trigger: function( event, data, elem, onlyHandlers ) {
+
+ var i, cur, tmp, bubbleType, ontype, handle, special,
+ eventPath = [ elem || document ],
+ type = hasOwn.call( event, "type" ) ? event.type : event,
+ namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split( "." ) : [];
+
+ cur = tmp = elem = elem || document;
+
+ // Don't do events on text and comment nodes
+ if ( elem.nodeType === 3 || elem.nodeType === 8 ) {
+ return;
+ }
+
+ // focus/blur morphs to focusin/out; ensure we're not firing them right now
+ if ( rfocusMorph.test( type + jQuery.event.triggered ) ) {
+ return;
+ }
+
+ if ( type.indexOf( "." ) > -1 ) {
+
+ // Namespaced trigger; create a regexp to match event type in handle()
+ namespaces = type.split( "." );
+ type = namespaces.shift();
+ namespaces.sort();
+ }
+ ontype = type.indexOf( ":" ) < 0 && "on" + type;
+
+ // Caller can pass in a jQuery.Event object, Object, or just an event type string
+ event = event[ jQuery.expando ] ?
+ event :
+ new jQuery.Event( type, typeof event === "object" && event );
+
+ // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true)
+ event.isTrigger = onlyHandlers ? 2 : 3;
+ event.namespace = namespaces.join( "." );
+ event.rnamespace = event.namespace ?
+ new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ) :
+ null;
+
+ // Clean up the event in case it is being reused
+ event.result = undefined;
+ if ( !event.target ) {
+ event.target = elem;
+ }
+
+ // Clone any incoming data and prepend the event, creating the handler arg list
+ data = data == null ?
+ [ event ] :
+ jQuery.makeArray( data, [ event ] );
+
+ // Allow special events to draw outside the lines
+ special = jQuery.event.special[ type ] || {};
+ if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) {
+ return;
+ }
+
+ // Determine event propagation path in advance, per W3C events spec (#9951)
+ // Bubble up to document, then to window; watch for a global ownerDocument var (#9724)
+ if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) {
+
+ bubbleType = special.delegateType || type;
+ if ( !rfocusMorph.test( bubbleType + type ) ) {
+ cur = cur.parentNode;
+ }
+ for ( ; cur; cur = cur.parentNode ) {
+ eventPath.push( cur );
+ tmp = cur;
+ }
+
+ // Only add window if we got to document (e.g., not plain obj or detached DOM)
+ if ( tmp === ( elem.ownerDocument || document ) ) {
+ eventPath.push( tmp.defaultView || tmp.parentWindow || window );
+ }
+ }
+
+ // Fire handlers on the event path
+ i = 0;
+ while ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) {
+
+ event.type = i > 1 ?
+ bubbleType :
+ special.bindType || type;
+
+ // jQuery handler
+ handle = ( dataPriv.get( cur, "events" ) || {} )[ event.type ] &&
+ dataPriv.get( cur, "handle" );
+ if ( handle ) {
+ handle.apply( cur, data );
+ }
+
+ // Native handler
+ handle = ontype && cur[ ontype ];
+ if ( handle && handle.apply && acceptData( cur ) ) {
+ event.result = handle.apply( cur, data );
+ if ( event.result === false ) {
+ event.preventDefault();
+ }
+ }
+ }
+ event.type = type;
+
+ // If nobody prevented the default action, do it now
+ if ( !onlyHandlers && !event.isDefaultPrevented() ) {
+
+ if ( ( !special._default ||
+ special._default.apply( eventPath.pop(), data ) === false ) &&
+ acceptData( elem ) ) {
+
+ // Call a native DOM method on the target with the same name name as the event.
+ // Don't do default actions on window, that's where global variables be (#6170)
+ if ( ontype && jQuery.isFunction( elem[ type ] ) && !jQuery.isWindow( elem ) ) {
+
+ // Don't re-trigger an onFOO event when we call its FOO() method
+ tmp = elem[ ontype ];
+
+ if ( tmp ) {
+ elem[ ontype ] = null;
+ }
+
+ // Prevent re-triggering of the same event, since we already bubbled it above
+ jQuery.event.triggered = type;
+ elem[ type ]();
+ jQuery.event.triggered = undefined;
+
+ if ( tmp ) {
+ elem[ ontype ] = tmp;
+ }
+ }
+ }
+ }
+
+ return event.result;
+ },
+
+ // Piggyback on a donor event to simulate a different one
+ simulate: function( type, elem, event ) {
+ var e = jQuery.extend(
+ new jQuery.Event(),
+ event,
+ {
+ type: type,
+ isSimulated: true
+
+ // Previously, `originalEvent: {}` was set here, so stopPropagation call
+ // would not be triggered on donor event, since in our own
+ // jQuery.event.stopPropagation function we had a check for existence of
+ // originalEvent.stopPropagation method, so, consequently it would be a noop.
+ //
+ // But now, this "simulate" function is used only for events
+ // for which stopPropagation() is noop, so there is no need for that anymore.
+ //
+ // For the compat branch though, guard for "click" and "submit"
+ // events is still used, but was moved to jQuery.event.stopPropagation function
+ // because `originalEvent` should point to the original event for the constancy
+ // with other events and for more focused logic
+ }
+ );
+
+ jQuery.event.trigger( e, null, elem );
+
+ if ( e.isDefaultPrevented() ) {
+ event.preventDefault();
+ }
+ }
+
+} );
+
+jQuery.fn.extend( {
+
+ trigger: function( type, data ) {
+ return this.each( function() {
+ jQuery.event.trigger( type, data, this );
+ } );
+ },
+ triggerHandler: function( type, data ) {
+ var elem = this[ 0 ];
+ if ( elem ) {
+ return jQuery.event.trigger( type, data, elem, true );
+ }
+ }
+} );
+
+
+jQuery.each( ( "blur focus focusin focusout load resize scroll unload click dblclick " +
+ "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
+ "change select submit keydown keypress keyup error contextmenu" ).split( " " ),
+ function( i, name ) {
+
+ // Handle event binding
+ jQuery.fn[ name ] = function( data, fn ) {
+ return arguments.length > 0 ?
+ this.on( name, null, data, fn ) :
+ this.trigger( name );
+ };
+} );
+
+jQuery.fn.extend( {
+ hover: function( fnOver, fnOut ) {
+ return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );
+ }
+} );
+
+
+
+
+support.focusin = "onfocusin" in window;
+
+
+// Support: Firefox
+// Firefox doesn't have focus(in | out) events
+// Related ticket - https://bugzilla.mozilla.org/show_bug.cgi?id=687787
+//
+// Support: Chrome, Safari
+// focus(in | out) events fire after focus & blur events,
+// which is spec violation - http://www.w3.org/TR/DOM-Level-3-Events/#events-focusevent-event-order
+// Related ticket - https://code.google.com/p/chromium/issues/detail?id=449857
+if ( !support.focusin ) {
+ jQuery.each( { focus: "focusin", blur: "focusout" }, function( orig, fix ) {
+
+ // Attach a single capturing handler on the document while someone wants focusin/focusout
+ var handler = function( event ) {
+ jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ) );
+ };
+
+ jQuery.event.special[ fix ] = {
+ setup: function() {
+ var doc = this.ownerDocument || this,
+ attaches = dataPriv.access( doc, fix );
+
+ if ( !attaches ) {
+ doc.addEventListener( orig, handler, true );
+ }
+ dataPriv.access( doc, fix, ( attaches || 0 ) + 1 );
+ },
+ teardown: function() {
+ var doc = this.ownerDocument || this,
+ attaches = dataPriv.access( doc, fix ) - 1;
+
+ if ( !attaches ) {
+ doc.removeEventListener( orig, handler, true );
+ dataPriv.remove( doc, fix );
+
+ } else {
+ dataPriv.access( doc, fix, attaches );
+ }
+ }
+ };
+ } );
+}
+var location = window.location;
+
+var nonce = jQuery.now();
+
+var rquery = ( /\?/ );
+
+
+
+// Support: Android 2.3
+// Workaround failure to string-cast null input
+jQuery.parseJSON = function( data ) {
+ return JSON.parse( data + "" );
+};
+
+
+// Cross-browser xml parsing
+jQuery.parseXML = function( data ) {
+ var xml;
+ if ( !data || typeof data !== "string" ) {
+ return null;
+ }
+
+ // Support: IE9
+ try {
+ xml = ( new window.DOMParser() ).parseFromString( data, "text/xml" );
+ } catch ( e ) {
+ xml = undefined;
+ }
+
+ if ( !xml || xml.getElementsByTagName( "parsererror" ).length ) {
+ jQuery.error( "Invalid XML: " + data );
+ }
+ return xml;
+};
+
+
+var
+ rhash = /#.*$/,
+ rts = /([?&])_=[^&]*/,
+ rheaders = /^(.*?):[ \t]*([^\r\n]*)$/mg,
+
+ // #7653, #8125, #8152: local protocol detection
+ rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/,
+ rnoContent = /^(?:GET|HEAD)$/,
+ rprotocol = /^\/\//,
+
+ /* Prefilters
+ * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example)
+ * 2) These are called:
+ * - BEFORE asking for a transport
+ * - AFTER param serialization (s.data is a string if s.processData is true)
+ * 3) key is the dataType
+ * 4) the catchall symbol "*" can be used
+ * 5) execution will start with transport dataType and THEN continue down to "*" if needed
+ */
+ prefilters = {},
+
+ /* Transports bindings
+ * 1) key is the dataType
+ * 2) the catchall symbol "*" can be used
+ * 3) selection will start with transport dataType and THEN go to "*" if needed
+ */
+ transports = {},
+
+ // Avoid comment-prolog char sequence (#10098); must appease lint and evade compression
+ allTypes = "*/".concat( "*" ),
+
+ // Anchor tag for parsing the document origin
+ originAnchor = document.createElement( "a" );
+ originAnchor.href = location.href;
+
+// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport
+function addToPrefiltersOrTransports( structure ) {
+
+ // dataTypeExpression is optional and defaults to "*"
+ return function( dataTypeExpression, func ) {
+
+ if ( typeof dataTypeExpression !== "string" ) {
+ func = dataTypeExpression;
+ dataTypeExpression = "*";
+ }
+
+ var dataType,
+ i = 0,
+ dataTypes = dataTypeExpression.toLowerCase().match( rnotwhite ) || [];
+
+ if ( jQuery.isFunction( func ) ) {
+
+ // For each dataType in the dataTypeExpression
+ while ( ( dataType = dataTypes[ i++ ] ) ) {
+
+ // Prepend if requested
+ if ( dataType[ 0 ] === "+" ) {
+ dataType = dataType.slice( 1 ) || "*";
+ ( structure[ dataType ] = structure[ dataType ] || [] ).unshift( func );
+
+ // Otherwise append
+ } else {
+ ( structure[ dataType ] = structure[ dataType ] || [] ).push( func );
+ }
+ }
+ }
+ };
+}
+
+// Base inspection function for prefilters and transports
+function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) {
+
+ var inspected = {},
+ seekingTransport = ( structure === transports );
+
+ function inspect( dataType ) {
+ var selected;
+ inspected[ dataType ] = true;
+ jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) {
+ var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR );
+ if ( typeof dataTypeOrTransport === "string" &&
+ !seekingTransport && !inspected[ dataTypeOrTransport ] ) {
+
+ options.dataTypes.unshift( dataTypeOrTransport );
+ inspect( dataTypeOrTransport );
+ return false;
+ } else if ( seekingTransport ) {
+ return !( selected = dataTypeOrTransport );
+ }
+ } );
+ return selected;
+ }
+
+ return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" );
+}
+
+// A special extend for ajax options
+// that takes "flat" options (not to be deep extended)
+// Fixes #9887
+function ajaxExtend( target, src ) {
+ var key, deep,
+ flatOptions = jQuery.ajaxSettings.flatOptions || {};
+
+ for ( key in src ) {
+ if ( src[ key ] !== undefined ) {
+ ( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ];
+ }
+ }
+ if ( deep ) {
+ jQuery.extend( true, target, deep );
+ }
+
+ return target;
+}
+
+/* Handles responses to an ajax request:
+ * - finds the right dataType (mediates between content-type and expected dataType)
+ * - returns the corresponding response
+ */
+function ajaxHandleResponses( s, jqXHR, responses ) {
+
+ var ct, type, finalDataType, firstDataType,
+ contents = s.contents,
+ dataTypes = s.dataTypes;
+
+ // Remove auto dataType and get content-type in the process
+ while ( dataTypes[ 0 ] === "*" ) {
+ dataTypes.shift();
+ if ( ct === undefined ) {
+ ct = s.mimeType || jqXHR.getResponseHeader( "Content-Type" );
+ }
+ }
+
+ // Check if we're dealing with a known content-type
+ if ( ct ) {
+ for ( type in contents ) {
+ if ( contents[ type ] && contents[ type ].test( ct ) ) {
+ dataTypes.unshift( type );
+ break;
+ }
+ }
+ }
+
+ // Check to see if we have a response for the expected dataType
+ if ( dataTypes[ 0 ] in responses ) {
+ finalDataType = dataTypes[ 0 ];
+ } else {
+
+ // Try convertible dataTypes
+ for ( type in responses ) {
+ if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[ 0 ] ] ) {
+ finalDataType = type;
+ break;
+ }
+ if ( !firstDataType ) {
+ firstDataType = type;
+ }
+ }
+
+ // Or just use first one
+ finalDataType = finalDataType || firstDataType;
+ }
+
+ // If we found a dataType
+ // We add the dataType to the list if needed
+ // and return the corresponding response
+ if ( finalDataType ) {
+ if ( finalDataType !== dataTypes[ 0 ] ) {
+ dataTypes.unshift( finalDataType );
+ }
+ return responses[ finalDataType ];
+ }
+}
+
+/* Chain conversions given the request and the original response
+ * Also sets the responseXXX fields on the jqXHR instance
+ */
+function ajaxConvert( s, response, jqXHR, isSuccess ) {
+ var conv2, current, conv, tmp, prev,
+ converters = {},
+
+ // Work with a copy of dataTypes in case we need to modify it for conversion
+ dataTypes = s.dataTypes.slice();
+
+ // Create converters map with lowercased keys
+ if ( dataTypes[ 1 ] ) {
+ for ( conv in s.converters ) {
+ converters[ conv.toLowerCase() ] = s.converters[ conv ];
+ }
+ }
+
+ current = dataTypes.shift();
+
+ // Convert to each sequential dataType
+ while ( current ) {
+
+ if ( s.responseFields[ current ] ) {
+ jqXHR[ s.responseFields[ current ] ] = response;
+ }
+
+ // Apply the dataFilter if provided
+ if ( !prev && isSuccess && s.dataFilter ) {
+ response = s.dataFilter( response, s.dataType );
+ }
+
+ prev = current;
+ current = dataTypes.shift();
+
+ if ( current ) {
+
+ // There's only work to do if current dataType is non-auto
+ if ( current === "*" ) {
+
+ current = prev;
+
+ // Convert response if prev dataType is non-auto and differs from current
+ } else if ( prev !== "*" && prev !== current ) {
+
+ // Seek a direct converter
+ conv = converters[ prev + " " + current ] || converters[ "* " + current ];
+
+ // If none found, seek a pair
+ if ( !conv ) {
+ for ( conv2 in converters ) {
+
+ // If conv2 outputs current
+ tmp = conv2.split( " " );
+ if ( tmp[ 1 ] === current ) {
+
+ // If prev can be converted to accepted input
+ conv = converters[ prev + " " + tmp[ 0 ] ] ||
+ converters[ "* " + tmp[ 0 ] ];
+ if ( conv ) {
+
+ // Condense equivalence converters
+ if ( conv === true ) {
+ conv = converters[ conv2 ];
+
+ // Otherwise, insert the intermediate dataType
+ } else if ( converters[ conv2 ] !== true ) {
+ current = tmp[ 0 ];
+ dataTypes.unshift( tmp[ 1 ] );
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ // Apply converter (if not an equivalence)
+ if ( conv !== true ) {
+
+ // Unless errors are allowed to bubble, catch and return them
+ if ( conv && s.throws ) {
+ response = conv( response );
+ } else {
+ try {
+ response = conv( response );
+ } catch ( e ) {
+ return {
+ state: "parsererror",
+ error: conv ? e : "No conversion from " + prev + " to " + current
+ };
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return { state: "success", data: response };
+}
+
+jQuery.extend( {
+
+ // Counter for holding the number of active queries
+ active: 0,
+
+ // Last-Modified header cache for next request
+ lastModified: {},
+ etag: {},
+
+ ajaxSettings: {
+ url: location.href,
+ type: "GET",
+ isLocal: rlocalProtocol.test( location.protocol ),
+ global: true,
+ processData: true,
+ async: true,
+ contentType: "application/x-www-form-urlencoded; charset=UTF-8",
+ /*
+ timeout: 0,
+ data: null,
+ dataType: null,
+ username: null,
+ password: null,
+ cache: null,
+ throws: false,
+ traditional: false,
+ headers: {},
+ */
+
+ accepts: {
+ "*": allTypes,
+ text: "text/plain",
+ html: "text/html",
+ xml: "application/xml, text/xml",
+ json: "application/json, text/javascript"
+ },
+
+ contents: {
+ xml: /\bxml\b/,
+ html: /\bhtml/,
+ json: /\bjson\b/
+ },
+
+ responseFields: {
+ xml: "responseXML",
+ text: "responseText",
+ json: "responseJSON"
+ },
+
+ // Data converters
+ // Keys separate source (or catchall "*") and destination types with a single space
+ converters: {
+
+ // Convert anything to text
+ "* text": String,
+
+ // Text to html (true = no transformation)
+ "text html": true,
+
+ // Evaluate text as a json expression
+ "text json": jQuery.parseJSON,
+
+ // Parse text as xml
+ "text xml": jQuery.parseXML
+ },
+
+ // For options that shouldn't be deep extended:
+ // you can add your own custom options here if
+ // and when you create one that shouldn't be
+ // deep extended (see ajaxExtend)
+ flatOptions: {
+ url: true,
+ context: true
+ }
+ },
+
+ // Creates a full fledged settings object into target
+ // with both ajaxSettings and settings fields.
+ // If target is omitted, writes into ajaxSettings.
+ ajaxSetup: function( target, settings ) {
+ return settings ?
+
+ // Building a settings object
+ ajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) :
+
+ // Extending ajaxSettings
+ ajaxExtend( jQuery.ajaxSettings, target );
+ },
+
+ ajaxPrefilter: addToPrefiltersOrTransports( prefilters ),
+ ajaxTransport: addToPrefiltersOrTransports( transports ),
+
+ // Main method
+ ajax: function( url, options ) {
+
+ // If url is an object, simulate pre-1.5 signature
+ if ( typeof url === "object" ) {
+ options = url;
+ url = undefined;
+ }
+
+ // Force options to be an object
+ options = options || {};
+
+ var transport,
+
+ // URL without anti-cache param
+ cacheURL,
+
+ // Response headers
+ responseHeadersString,
+ responseHeaders,
+
+ // timeout handle
+ timeoutTimer,
+
+ // Url cleanup var
+ urlAnchor,
+
+ // To know if global events are to be dispatched
+ fireGlobals,
+
+ // Loop variable
+ i,
+
+ // Create the final options object
+ s = jQuery.ajaxSetup( {}, options ),
+
+ // Callbacks context
+ callbackContext = s.context || s,
+
+ // Context for global events is callbackContext if it is a DOM node or jQuery collection
+ globalEventContext = s.context &&
+ ( callbackContext.nodeType || callbackContext.jquery ) ?
+ jQuery( callbackContext ) :
+ jQuery.event,
+
+ // Deferreds
+ deferred = jQuery.Deferred(),
+ completeDeferred = jQuery.Callbacks( "once memory" ),
+
+ // Status-dependent callbacks
+ statusCode = s.statusCode || {},
+
+ // Headers (they are sent all at once)
+ requestHeaders = {},
+ requestHeadersNames = {},
+
+ // The jqXHR state
+ state = 0,
+
+ // Default abort message
+ strAbort = "canceled",
+
+ // Fake xhr
+ jqXHR = {
+ readyState: 0,
+
+ // Builds headers hashtable if needed
+ getResponseHeader: function( key ) {
+ var match;
+ if ( state === 2 ) {
+ if ( !responseHeaders ) {
+ responseHeaders = {};
+ while ( ( match = rheaders.exec( responseHeadersString ) ) ) {
+ responseHeaders[ match[ 1 ].toLowerCase() ] = match[ 2 ];
+ }
+ }
+ match = responseHeaders[ key.toLowerCase() ];
+ }
+ return match == null ? null : match;
+ },
+
+ // Raw string
+ getAllResponseHeaders: function() {
+ return state === 2 ? responseHeadersString : null;
+ },
+
+ // Caches the header
+ setRequestHeader: function( name, value ) {
+ var lname = name.toLowerCase();
+ if ( !state ) {
+ name = requestHeadersNames[ lname ] = requestHeadersNames[ lname ] || name;
+ requestHeaders[ name ] = value;
+ }
+ return this;
+ },
+
+ // Overrides response content-type header
+ overrideMimeType: function( type ) {
+ if ( !state ) {
+ s.mimeType = type;
+ }
+ return this;
+ },
+
+ // Status-dependent callbacks
+ statusCode: function( map ) {
+ var code;
+ if ( map ) {
+ if ( state < 2 ) {
+ for ( code in map ) {
+
+ // Lazy-add the new callback in a way that preserves old ones
+ statusCode[ code ] = [ statusCode[ code ], map[ code ] ];
+ }
+ } else {
+
+ // Execute the appropriate callbacks
+ jqXHR.always( map[ jqXHR.status ] );
+ }
+ }
+ return this;
+ },
+
+ // Cancel the request
+ abort: function( statusText ) {
+ var finalText = statusText || strAbort;
+ if ( transport ) {
+ transport.abort( finalText );
+ }
+ done( 0, finalText );
+ return this;
+ }
+ };
+
+ // Attach deferreds
+ deferred.promise( jqXHR ).complete = completeDeferred.add;
+ jqXHR.success = jqXHR.done;
+ jqXHR.error = jqXHR.fail;
+
+ // Remove hash character (#7531: and string promotion)
+ // Add protocol if not provided (prefilters might expect it)
+ // Handle falsy url in the settings object (#10093: consistency with old signature)
+ // We also use the url parameter if available
+ s.url = ( ( url || s.url || location.href ) + "" ).replace( rhash, "" )
+ .replace( rprotocol, location.protocol + "//" );
+
+ // Alias method option to type as per ticket #12004
+ s.type = options.method || options.type || s.method || s.type;
+
+ // Extract dataTypes list
+ s.dataTypes = jQuery.trim( s.dataType || "*" ).toLowerCase().match( rnotwhite ) || [ "" ];
+
+ // A cross-domain request is in order when the origin doesn't match the current origin.
+ if ( s.crossDomain == null ) {
+ urlAnchor = document.createElement( "a" );
+
+ // Support: IE8-11+
+ // IE throws exception if url is malformed, e.g. http://example.com:80x/
+ try {
+ urlAnchor.href = s.url;
+
+ // Support: IE8-11+
+ // Anchor's host property isn't correctly set when s.url is relative
+ urlAnchor.href = urlAnchor.href;
+ s.crossDomain = originAnchor.protocol + "//" + originAnchor.host !==
+ urlAnchor.protocol + "//" + urlAnchor.host;
+ } catch ( e ) {
+
+ // If there is an error parsing the URL, assume it is crossDomain,
+ // it can be rejected by the transport if it is invalid
+ s.crossDomain = true;
+ }
+ }
+
+ // Convert data if not already a string
+ if ( s.data && s.processData && typeof s.data !== "string" ) {
+ s.data = jQuery.param( s.data, s.traditional );
+ }
+
+ // Apply prefilters
+ inspectPrefiltersOrTransports( prefilters, s, options, jqXHR );
+
+ // If request was aborted inside a prefilter, stop there
+ if ( state === 2 ) {
+ return jqXHR;
+ }
+
+ // We can fire global events as of now if asked to
+ // Don't fire events if jQuery.event is undefined in an AMD-usage scenario (#15118)
+ fireGlobals = jQuery.event && s.global;
+
+ // Watch for a new set of requests
+ if ( fireGlobals && jQuery.active++ === 0 ) {
+ jQuery.event.trigger( "ajaxStart" );
+ }
+
+ // Uppercase the type
+ s.type = s.type.toUpperCase();
+
+ // Determine if request has content
+ s.hasContent = !rnoContent.test( s.type );
+
+ // Save the URL in case we're toying with the If-Modified-Since
+ // and/or If-None-Match header later on
+ cacheURL = s.url;
+
+ // More options handling for requests with no content
+ if ( !s.hasContent ) {
+
+ // If data is available, append data to url
+ if ( s.data ) {
+ cacheURL = ( s.url += ( rquery.test( cacheURL ) ? "&" : "?" ) + s.data );
+
+ // #9682: remove data so that it's not used in an eventual retry
+ delete s.data;
+ }
+
+ // Add anti-cache in url if needed
+ if ( s.cache === false ) {
+ s.url = rts.test( cacheURL ) ?
+
+ // If there is already a '_' parameter, set its value
+ cacheURL.replace( rts, "$1_=" + nonce++ ) :
+
+ // Otherwise add one to the end
+ cacheURL + ( rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + nonce++;
+ }
+ }
+
+ // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
+ if ( s.ifModified ) {
+ if ( jQuery.lastModified[ cacheURL ] ) {
+ jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ cacheURL ] );
+ }
+ if ( jQuery.etag[ cacheURL ] ) {
+ jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ cacheURL ] );
+ }
+ }
+
+ // Set the correct header, if data is being sent
+ if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) {
+ jqXHR.setRequestHeader( "Content-Type", s.contentType );
+ }
+
+ // Set the Accepts header for the server, depending on the dataType
+ jqXHR.setRequestHeader(
+ "Accept",
+ s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[ 0 ] ] ?
+ s.accepts[ s.dataTypes[ 0 ] ] +
+ ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) :
+ s.accepts[ "*" ]
+ );
+
+ // Check for headers option
+ for ( i in s.headers ) {
+ jqXHR.setRequestHeader( i, s.headers[ i ] );
+ }
+
+ // Allow custom headers/mimetypes and early abort
+ if ( s.beforeSend &&
+ ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || state === 2 ) ) {
+
+ // Abort if not done already and return
+ return jqXHR.abort();
+ }
+
+ // Aborting is no longer a cancellation
+ strAbort = "abort";
+
+ // Install callbacks on deferreds
+ for ( i in { success: 1, error: 1, complete: 1 } ) {
+ jqXHR[ i ]( s[ i ] );
+ }
+
+ // Get transport
+ transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR );
+
+ // If no transport, we auto-abort
+ if ( !transport ) {
+ done( -1, "No Transport" );
+ } else {
+ jqXHR.readyState = 1;
+
+ // Send global event
+ if ( fireGlobals ) {
+ globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] );
+ }
+
+ // If request was aborted inside ajaxSend, stop there
+ if ( state === 2 ) {
+ return jqXHR;
+ }
+
+ // Timeout
+ if ( s.async && s.timeout > 0 ) {
+ timeoutTimer = window.setTimeout( function() {
+ jqXHR.abort( "timeout" );
+ }, s.timeout );
+ }
+
+ try {
+ state = 1;
+ transport.send( requestHeaders, done );
+ } catch ( e ) {
+
+ // Propagate exception as error if not done
+ if ( state < 2 ) {
+ done( -1, e );
+
+ // Simply rethrow otherwise
+ } else {
+ throw e;
+ }
+ }
+ }
+
+ // Callback for when everything is done
+ function done( status, nativeStatusText, responses, headers ) {
+ var isSuccess, success, error, response, modified,
+ statusText = nativeStatusText;
+
+ // Called once
+ if ( state === 2 ) {
+ return;
+ }
+
+ // State is "done" now
+ state = 2;
+
+ // Clear timeout if it exists
+ if ( timeoutTimer ) {
+ window.clearTimeout( timeoutTimer );
+ }
+
+ // Dereference transport for early garbage collection
+ // (no matter how long the jqXHR object will be used)
+ transport = undefined;
+
+ // Cache response headers
+ responseHeadersString = headers || "";
+
+ // Set readyState
+ jqXHR.readyState = status > 0 ? 4 : 0;
+
+ // Determine if successful
+ isSuccess = status >= 200 && status < 300 || status === 304;
+
+ // Get response data
+ if ( responses ) {
+ response = ajaxHandleResponses( s, jqXHR, responses );
+ }
+
+ // Convert no matter what (that way responseXXX fields are always set)
+ response = ajaxConvert( s, response, jqXHR, isSuccess );
+
+ // If successful, handle type chaining
+ if ( isSuccess ) {
+
+ // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
+ if ( s.ifModified ) {
+ modified = jqXHR.getResponseHeader( "Last-Modified" );
+ if ( modified ) {
+ jQuery.lastModified[ cacheURL ] = modified;
+ }
+ modified = jqXHR.getResponseHeader( "etag" );
+ if ( modified ) {
+ jQuery.etag[ cacheURL ] = modified;
+ }
+ }
+
+ // if no content
+ if ( status === 204 || s.type === "HEAD" ) {
+ statusText = "nocontent";
+
+ // if not modified
+ } else if ( status === 304 ) {
+ statusText = "notmodified";
+
+ // If we have data, let's convert it
+ } else {
+ statusText = response.state;
+ success = response.data;
+ error = response.error;
+ isSuccess = !error;
+ }
+ } else {
+
+ // Extract error from statusText and normalize for non-aborts
+ error = statusText;
+ if ( status || !statusText ) {
+ statusText = "error";
+ if ( status < 0 ) {
+ status = 0;
+ }
+ }
+ }
+
+ // Set data for the fake xhr object
+ jqXHR.status = status;
+ jqXHR.statusText = ( nativeStatusText || statusText ) + "";
+
+ // Success/Error
+ if ( isSuccess ) {
+ deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );
+ } else {
+ deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );
+ }
+
+ // Status-dependent callbacks
+ jqXHR.statusCode( statusCode );
+ statusCode = undefined;
+
+ if ( fireGlobals ) {
+ globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError",
+ [ jqXHR, s, isSuccess ? success : error ] );
+ }
+
+ // Complete
+ completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] );
+
+ if ( fireGlobals ) {
+ globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] );
+
+ // Handle the global AJAX counter
+ if ( !( --jQuery.active ) ) {
+ jQuery.event.trigger( "ajaxStop" );
+ }
+ }
+ }
+
+ return jqXHR;
+ },
+
+ getJSON: function( url, data, callback ) {
+ return jQuery.get( url, data, callback, "json" );
+ },
+
+ getScript: function( url, callback ) {
+ return jQuery.get( url, undefined, callback, "script" );
+ }
+} );
+
+jQuery.each( [ "get", "post" ], function( i, method ) {
+ jQuery[ method ] = function( url, data, callback, type ) {
+
+ // Shift arguments if data argument was omitted
+ if ( jQuery.isFunction( data ) ) {
+ type = type || callback;
+ callback = data;
+ data = undefined;
+ }
+
+ // The url can be an options object (which then must have .url)
+ return jQuery.ajax( jQuery.extend( {
+ url: url,
+ type: method,
+ dataType: type,
+ data: data,
+ success: callback
+ }, jQuery.isPlainObject( url ) && url ) );
+ };
+} );
+
+
+jQuery._evalUrl = function( url ) {
+ return jQuery.ajax( {
+ url: url,
+
+ // Make this explicit, since user can override this through ajaxSetup (#11264)
+ type: "GET",
+ dataType: "script",
+ async: false,
+ global: false,
+ "throws": true
+ } );
+};
+
+
+jQuery.fn.extend( {
+ wrapAll: function( html ) {
+ var wrap;
+
+ if ( jQuery.isFunction( html ) ) {
+ return this.each( function( i ) {
+ jQuery( this ).wrapAll( html.call( this, i ) );
+ } );
+ }
+
+ if ( this[ 0 ] ) {
+
+ // The elements to wrap the target around
+ wrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true );
+
+ if ( this[ 0 ].parentNode ) {
+ wrap.insertBefore( this[ 0 ] );
+ }
+
+ wrap.map( function() {
+ var elem = this;
+
+ while ( elem.firstElementChild ) {
+ elem = elem.firstElementChild;
+ }
+
+ return elem;
+ } ).append( this );
+ }
+
+ return this;
+ },
+
+ wrapInner: function( html ) {
+ if ( jQuery.isFunction( html ) ) {
+ return this.each( function( i ) {
+ jQuery( this ).wrapInner( html.call( this, i ) );
+ } );
+ }
+
+ return this.each( function() {
+ var self = jQuery( this ),
+ contents = self.contents();
+
+ if ( contents.length ) {
+ contents.wrapAll( html );
+
+ } else {
+ self.append( html );
+ }
+ } );
+ },
+
+ wrap: function( html ) {
+ var isFunction = jQuery.isFunction( html );
+
+ return this.each( function( i ) {
+ jQuery( this ).wrapAll( isFunction ? html.call( this, i ) : html );
+ } );
+ },
+
+ unwrap: function() {
+ return this.parent().each( function() {
+ if ( !jQuery.nodeName( this, "body" ) ) {
+ jQuery( this ).replaceWith( this.childNodes );
+ }
+ } ).end();
+ }
+} );
+
+
+jQuery.expr.filters.hidden = function( elem ) {
+ return !jQuery.expr.filters.visible( elem );
+};
+jQuery.expr.filters.visible = function( elem ) {
+
+ // Support: Opera <= 12.12
+ // Opera reports offsetWidths and offsetHeights less than zero on some elements
+ // Use OR instead of AND as the element is not visible if either is true
+ // See tickets #10406 and #13132
+ return elem.offsetWidth > 0 || elem.offsetHeight > 0 || elem.getClientRects().length > 0;
+};
+
+
+
+
+var r20 = /%20/g,
+ rbracket = /\[\]$/,
+ rCRLF = /\r?\n/g,
+ rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i,
+ rsubmittable = /^(?:input|select|textarea|keygen)/i;
+
+function buildParams( prefix, obj, traditional, add ) {
+ var name;
+
+ if ( jQuery.isArray( obj ) ) {
+
+ // Serialize array item.
+ jQuery.each( obj, function( i, v ) {
+ if ( traditional || rbracket.test( prefix ) ) {
+
+ // Treat each array item as a scalar.
+ add( prefix, v );
+
+ } else {
+
+ // Item is non-scalar (array or object), encode its numeric index.
+ buildParams(
+ prefix + "[" + ( typeof v === "object" && v != null ? i : "" ) + "]",
+ v,
+ traditional,
+ add
+ );
+ }
+ } );
+
+ } else if ( !traditional && jQuery.type( obj ) === "object" ) {
+
+ // Serialize object item.
+ for ( name in obj ) {
+ buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add );
+ }
+
+ } else {
+
+ // Serialize scalar item.
+ add( prefix, obj );
+ }
+}
+
+// Serialize an array of form elements or a set of
+// key/values into a query string
+jQuery.param = function( a, traditional ) {
+ var prefix,
+ s = [],
+ add = function( key, value ) {
+
+ // If value is a function, invoke it and return its value
+ value = jQuery.isFunction( value ) ? value() : ( value == null ? "" : value );
+ s[ s.length ] = encodeURIComponent( key ) + "=" + encodeURIComponent( value );
+ };
+
+ // Set traditional to true for jQuery <= 1.3.2 behavior.
+ if ( traditional === undefined ) {
+ traditional = jQuery.ajaxSettings && jQuery.ajaxSettings.traditional;
+ }
+
+ // If an array was passed in, assume that it is an array of form elements.
+ if ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) {
+
+ // Serialize the form elements
+ jQuery.each( a, function() {
+ add( this.name, this.value );
+ } );
+
+ } else {
+
+ // If traditional, encode the "old" way (the way 1.3.2 or older
+ // did it), otherwise encode params recursively.
+ for ( prefix in a ) {
+ buildParams( prefix, a[ prefix ], traditional, add );
+ }
+ }
+
+ // Return the resulting serialization
+ return s.join( "&" ).replace( r20, "+" );
+};
+
+jQuery.fn.extend( {
+ serialize: function() {
+ return jQuery.param( this.serializeArray() );
+ },
+ serializeArray: function() {
+ return this.map( function() {
+
+ // Can add propHook for "elements" to filter or add form elements
+ var elements = jQuery.prop( this, "elements" );
+ return elements ? jQuery.makeArray( elements ) : this;
+ } )
+ .filter( function() {
+ var type = this.type;
+
+ // Use .is( ":disabled" ) so that fieldset[disabled] works
+ return this.name && !jQuery( this ).is( ":disabled" ) &&
+ rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) &&
+ ( this.checked || !rcheckableType.test( type ) );
+ } )
+ .map( function( i, elem ) {
+ var val = jQuery( this ).val();
+
+ return val == null ?
+ null :
+ jQuery.isArray( val ) ?
+ jQuery.map( val, function( val ) {
+ return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
+ } ) :
+ { name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
+ } ).get();
+ }
+} );
+
+
+jQuery.ajaxSettings.xhr = function() {
+ try {
+ return new window.XMLHttpRequest();
+ } catch ( e ) {}
+};
+
+var xhrSuccessStatus = {
+
+ // File protocol always yields status code 0, assume 200
+ 0: 200,
+
+ // Support: IE9
+ // #1450: sometimes IE returns 1223 when it should be 204
+ 1223: 204
+ },
+ xhrSupported = jQuery.ajaxSettings.xhr();
+
+support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported );
+support.ajax = xhrSupported = !!xhrSupported;
+
+jQuery.ajaxTransport( function( options ) {
+ var callback, errorCallback;
+
+ // Cross domain only allowed if supported through XMLHttpRequest
+ if ( support.cors || xhrSupported && !options.crossDomain ) {
+ return {
+ send: function( headers, complete ) {
+ var i,
+ xhr = options.xhr();
+
+ xhr.open(
+ options.type,
+ options.url,
+ options.async,
+ options.username,
+ options.password
+ );
+
+ // Apply custom fields if provided
+ if ( options.xhrFields ) {
+ for ( i in options.xhrFields ) {
+ xhr[ i ] = options.xhrFields[ i ];
+ }
+ }
+
+ // Override mime type if needed
+ if ( options.mimeType && xhr.overrideMimeType ) {
+ xhr.overrideMimeType( options.mimeType );
+ }
+
+ // X-Requested-With header
+ // For cross-domain requests, seeing as conditions for a preflight are
+ // akin to a jigsaw puzzle, we simply never set it to be sure.
+ // (it can always be set on a per-request basis or even using ajaxSetup)
+ // For same-domain requests, won't change header if already provided.
+ if ( !options.crossDomain && !headers[ "X-Requested-With" ] ) {
+ headers[ "X-Requested-With" ] = "XMLHttpRequest";
+ }
+
+ // Set headers
+ for ( i in headers ) {
+ xhr.setRequestHeader( i, headers[ i ] );
+ }
+
+ // Callback
+ callback = function( type ) {
+ return function() {
+ if ( callback ) {
+ callback = errorCallback = xhr.onload =
+ xhr.onerror = xhr.onabort = xhr.onreadystatechange = null;
+
+ if ( type === "abort" ) {
+ xhr.abort();
+ } else if ( type === "error" ) {
+
+ // Support: IE9
+ // On a manual native abort, IE9 throws
+ // errors on any property access that is not readyState
+ if ( typeof xhr.status !== "number" ) {
+ complete( 0, "error" );
+ } else {
+ complete(
+
+ // File: protocol always yields status 0; see #8605, #14207
+ xhr.status,
+ xhr.statusText
+ );
+ }
+ } else {
+ complete(
+ xhrSuccessStatus[ xhr.status ] || xhr.status,
+ xhr.statusText,
+
+ // Support: IE9 only
+ // IE9 has no XHR2 but throws on binary (trac-11426)
+ // For XHR2 non-text, let the caller handle it (gh-2498)
+ ( xhr.responseType || "text" ) !== "text" ||
+ typeof xhr.responseText !== "string" ?
+ { binary: xhr.response } :
+ { text: xhr.responseText },
+ xhr.getAllResponseHeaders()
+ );
+ }
+ }
+ };
+ };
+
+ // Listen to events
+ xhr.onload = callback();
+ errorCallback = xhr.onerror = callback( "error" );
+
+ // Support: IE9
+ // Use onreadystatechange to replace onabort
+ // to handle uncaught aborts
+ if ( xhr.onabort !== undefined ) {
+ xhr.onabort = errorCallback;
+ } else {
+ xhr.onreadystatechange = function() {
+
+ // Check readyState before timeout as it changes
+ if ( xhr.readyState === 4 ) {
+
+ // Allow onerror to be called first,
+ // but that will not handle a native abort
+ // Also, save errorCallback to a variable
+ // as xhr.onerror cannot be accessed
+ window.setTimeout( function() {
+ if ( callback ) {
+ errorCallback();
+ }
+ } );
+ }
+ };
+ }
+
+ // Create the abort callback
+ callback = callback( "abort" );
+
+ try {
+
+ // Do send the request (this may raise an exception)
+ xhr.send( options.hasContent && options.data || null );
+ } catch ( e ) {
+
+ // #14683: Only rethrow if this hasn't been notified as an error yet
+ if ( callback ) {
+ throw e;
+ }
+ }
+ },
+
+ abort: function() {
+ if ( callback ) {
+ callback();
+ }
+ }
+ };
+ }
+} );
+
+
+
+
+// Install script dataType
+jQuery.ajaxSetup( {
+ accepts: {
+ script: "text/javascript, application/javascript, " +
+ "application/ecmascript, application/x-ecmascript"
+ },
+ contents: {
+ script: /\b(?:java|ecma)script\b/
+ },
+ converters: {
+ "text script": function( text ) {
+ jQuery.globalEval( text );
+ return text;
+ }
+ }
+} );
+
+// Handle cache's special case and crossDomain
+jQuery.ajaxPrefilter( "script", function( s ) {
+ if ( s.cache === undefined ) {
+ s.cache = false;
+ }
+ if ( s.crossDomain ) {
+ s.type = "GET";
+ }
+} );
+
+// Bind script tag hack transport
+jQuery.ajaxTransport( "script", function( s ) {
+
+ // This transport only deals with cross domain requests
+ if ( s.crossDomain ) {
+ var script, callback;
+ return {
+ send: function( _, complete ) {
+ script = jQuery( "<script>" ).prop( {
+ charset: s.scriptCharset,
+ src: s.url
+ } ).on(
+ "load error",
+ callback = function( evt ) {
+ script.remove();
+ callback = null;
+ if ( evt ) {
+ complete( evt.type === "error" ? 404 : 200, evt.type );
+ }
+ }
+ );
+
+ // Use native DOM manipulation to avoid our domManip AJAX trickery
+ document.head.appendChild( script[ 0 ] );
+ },
+ abort: function() {
+ if ( callback ) {
+ callback();
+ }
+ }
+ };
+ }
+} );
+
+
+
+
+var oldCallbacks = [],
+ rjsonp = /(=)\?(?=&|$)|\?\?/;
+
+// Default jsonp settings
+jQuery.ajaxSetup( {
+ jsonp: "callback",
+ jsonpCallback: function() {
+ var callback = oldCallbacks.pop() || ( jQuery.expando + "_" + ( nonce++ ) );
+ this[ callback ] = true;
+ return callback;
+ }
+} );
+
+// Detect, normalize options and install callbacks for jsonp requests
+jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) {
+
+ var callbackName, overwritten, responseContainer,
+ jsonProp = s.jsonp !== false && ( rjsonp.test( s.url ) ?
+ "url" :
+ typeof s.data === "string" &&
+ ( s.contentType || "" )
+ .indexOf( "application/x-www-form-urlencoded" ) === 0 &&
+ rjsonp.test( s.data ) && "data"
+ );
+
+ // Handle iff the expected data type is "jsonp" or we have a parameter to set
+ if ( jsonProp || s.dataTypes[ 0 ] === "jsonp" ) {
+
+ // Get callback name, remembering preexisting value associated with it
+ callbackName = s.jsonpCallback = jQuery.isFunction( s.jsonpCallback ) ?
+ s.jsonpCallback() :
+ s.jsonpCallback;
+
+ // Insert callback into url or form data
+ if ( jsonProp ) {
+ s[ jsonProp ] = s[ jsonProp ].replace( rjsonp, "$1" + callbackName );
+ } else if ( s.jsonp !== false ) {
+ s.url += ( rquery.test( s.url ) ? "&" : "?" ) + s.jsonp + "=" + callbackName;
+ }
+
+ // Use data converter to retrieve json after script execution
+ s.converters[ "script json" ] = function() {
+ if ( !responseContainer ) {
+ jQuery.error( callbackName + " was not called" );
+ }
+ return responseContainer[ 0 ];
+ };
+
+ // Force json dataType
+ s.dataTypes[ 0 ] = "json";
+
+ // Install callback
+ overwritten = window[ callbackName ];
+ window[ callbackName ] = function() {
+ responseContainer = arguments;
+ };
+
+ // Clean-up function (fires after converters)
+ jqXHR.always( function() {
+
+ // If previous value didn't exist - remove it
+ if ( overwritten === undefined ) {
+ jQuery( window ).removeProp( callbackName );
+
+ // Otherwise restore preexisting value
+ } else {
+ window[ callbackName ] = overwritten;
+ }
+
+ // Save back as free
+ if ( s[ callbackName ] ) {
+
+ // Make sure that re-using the options doesn't screw things around
+ s.jsonpCallback = originalSettings.jsonpCallback;
+
+ // Save the callback name for future use
+ oldCallbacks.push( callbackName );
+ }
+
+ // Call if it was a function and we have a response
+ if ( responseContainer && jQuery.isFunction( overwritten ) ) {
+ overwritten( responseContainer[ 0 ] );
+ }
+
+ responseContainer = overwritten = undefined;
+ } );
+
+ // Delegate to script
+ return "script";
+ }
+} );
+
+
+
+
+// Support: Safari 8+
+// In Safari 8 documents created via document.implementation.createHTMLDocument
+// collapse sibling forms: the second one becomes a child of the first one.
+// Because of that, this security measure has to be disabled in Safari 8.
+// https://bugs.webkit.org/show_bug.cgi?id=137337
+support.createHTMLDocument = ( function() {
+ var body = document.implementation.createHTMLDocument( "" ).body;
+ body.innerHTML = "<form></form><form></form>";
+ return body.childNodes.length === 2;
+} )();
+
+
+// Argument "data" should be string of html
+// context (optional): If specified, the fragment will be created in this context,
+// defaults to document
+// keepScripts (optional): If true, will include scripts passed in the html string
+jQuery.parseHTML = function( data, context, keepScripts ) {
+ if ( !data || typeof data !== "string" ) {
+ return null;
+ }
+ if ( typeof context === "boolean" ) {
+ keepScripts = context;
+ context = false;
+ }
+
+ // Stop scripts or inline event handlers from being executed immediately
+ // by using document.implementation
+ context = context || ( support.createHTMLDocument ?
+ document.implementation.createHTMLDocument( "" ) :
+ document );
+
+ var parsed = rsingleTag.exec( data ),
+ scripts = !keepScripts && [];
+
+ // Single tag
+ if ( parsed ) {
+ return [ context.createElement( parsed[ 1 ] ) ];
+ }
+
+ parsed = buildFragment( [ data ], context, scripts );
+
+ if ( scripts && scripts.length ) {
+ jQuery( scripts ).remove();
+ }
+
+ return jQuery.merge( [], parsed.childNodes );
+};
+
+
+// Keep a copy of the old load method
+var _load = jQuery.fn.load;
+
+/**
+ * Load a url into a page
+ */
+jQuery.fn.load = function( url, params, callback ) {
+ if ( typeof url !== "string" && _load ) {
+ return _load.apply( this, arguments );
+ }
+
+ var selector, type, response,
+ self = this,
+ off = url.indexOf( " " );
+
+ if ( off > -1 ) {
+ selector = jQuery.trim( url.slice( off ) );
+ url = url.slice( 0, off );
+ }
+
+ // If it's a function
+ if ( jQuery.isFunction( params ) ) {
+
+ // We assume that it's the callback
+ callback = params;
+ params = undefined;
+
+ // Otherwise, build a param string
+ } else if ( params && typeof params === "object" ) {
+ type = "POST";
+ }
+
+ // If we have elements to modify, make the request
+ if ( self.length > 0 ) {
+ jQuery.ajax( {
+ url: url,
+
+ // If "type" variable is undefined, then "GET" method will be used.
+ // Make value of this field explicit since
+ // user can override it through ajaxSetup method
+ type: type || "GET",
+ dataType: "html",
+ data: params
+ } ).done( function( responseText ) {
+
+ // Save response for use in complete callback
+ response = arguments;
+
+ self.html( selector ?
+
+ // If a selector was specified, locate the right elements in a dummy div
+ // Exclude scripts to avoid IE 'Permission Denied' errors
+ jQuery( "<div>" ).append( jQuery.parseHTML( responseText ) ).find( selector ) :
+
+ // Otherwise use the full result
+ responseText );
+
+ // If the request succeeds, this function gets "data", "status", "jqXHR"
+ // but they are ignored because response was set above.
+ // If it fails, this function gets "jqXHR", "status", "error"
+ } ).always( callback && function( jqXHR, status ) {
+ self.each( function() {
+ callback.apply( self, response || [ jqXHR.responseText, status, jqXHR ] );
+ } );
+ } );
+ }
+
+ return this;
+};
+
+
+
+
+// Attach a bunch of functions for handling common AJAX events
+jQuery.each( [
+ "ajaxStart",
+ "ajaxStop",
+ "ajaxComplete",
+ "ajaxError",
+ "ajaxSuccess",
+ "ajaxSend"
+], function( i, type ) {
+ jQuery.fn[ type ] = function( fn ) {
+ return this.on( type, fn );
+ };
+} );
+
+
+
+
+jQuery.expr.filters.animated = function( elem ) {
+ return jQuery.grep( jQuery.timers, function( fn ) {
+ return elem === fn.elem;
+ } ).length;
+};
+
+
+
+
+/**
+ * Gets a window from an element
+ */
+function getWindow( elem ) {
+ return jQuery.isWindow( elem ) ? elem : elem.nodeType === 9 && elem.defaultView;
+}
+
+jQuery.offset = {
+ setOffset: function( elem, options, i ) {
+ var curPosition, curLeft, curCSSTop, curTop, curOffset, curCSSLeft, calculatePosition,
+ position = jQuery.css( elem, "position" ),
+ curElem = jQuery( elem ),
+ props = {};
+
+ // Set position first, in-case top/left are set even on static elem
+ if ( position === "static" ) {
+ elem.style.position = "relative";
+ }
+
+ curOffset = curElem.offset();
+ curCSSTop = jQuery.css( elem, "top" );
+ curCSSLeft = jQuery.css( elem, "left" );
+ calculatePosition = ( position === "absolute" || position === "fixed" ) &&
+ ( curCSSTop + curCSSLeft ).indexOf( "auto" ) > -1;
+
+ // Need to be able to calculate position if either
+ // top or left is auto and position is either absolute or fixed
+ if ( calculatePosition ) {
+ curPosition = curElem.position();
+ curTop = curPosition.top;
+ curLeft = curPosition.left;
+
+ } else {
+ curTop = parseFloat( curCSSTop ) || 0;
+ curLeft = parseFloat( curCSSLeft ) || 0;
+ }
+
+ if ( jQuery.isFunction( options ) ) {
+
+ // Use jQuery.extend here to allow modification of coordinates argument (gh-1848)
+ options = options.call( elem, i, jQuery.extend( {}, curOffset ) );
+ }
+
+ if ( options.top != null ) {
+ props.top = ( options.top - curOffset.top ) + curTop;
+ }
+ if ( options.left != null ) {
+ props.left = ( options.left - curOffset.left ) + curLeft;
+ }
+
+ if ( "using" in options ) {
+ options.using.call( elem, props );
+
+ } else {
+ curElem.css( props );
+ }
+ }
+};
+
+jQuery.fn.extend( {
+ offset: function( options ) {
+ if ( arguments.length ) {
+ return options === undefined ?
+ this :
+ this.each( function( i ) {
+ jQuery.offset.setOffset( this, options, i );
+ } );
+ }
+
+ var docElem, win,
+ elem = this[ 0 ],
+ box = { top: 0, left: 0 },
+ doc = elem && elem.ownerDocument;
+
+ if ( !doc ) {
+ return;
+ }
+
+ docElem = doc.documentElement;
+
+ // Make sure it's not a disconnected DOM node
+ if ( !jQuery.contains( docElem, elem ) ) {
+ return box;
+ }
+
+ box = elem.getBoundingClientRect();
+ win = getWindow( doc );
+ return {
+ top: box.top + win.pageYOffset - docElem.clientTop,
+ left: box.left + win.pageXOffset - docElem.clientLeft
+ };
+ },
+
+ position: function() {
+ if ( !this[ 0 ] ) {
+ return;
+ }
+
+ var offsetParent, offset,
+ elem = this[ 0 ],
+ parentOffset = { top: 0, left: 0 };
+
+ // Fixed elements are offset from window (parentOffset = {top:0, left: 0},
+ // because it is its only offset parent
+ if ( jQuery.css( elem, "position" ) === "fixed" ) {
+
+ // Assume getBoundingClientRect is there when computed position is fixed
+ offset = elem.getBoundingClientRect();
+
+ } else {
+
+ // Get *real* offsetParent
+ offsetParent = this.offsetParent();
+
+ // Get correct offsets
+ offset = this.offset();
+ if ( !jQuery.nodeName( offsetParent[ 0 ], "html" ) ) {
+ parentOffset = offsetParent.offset();
+ }
+
+ // Add offsetParent borders
+ // Subtract offsetParent scroll positions
+ parentOffset.top += jQuery.css( offsetParent[ 0 ], "borderTopWidth", true ) -
+ offsetParent.scrollTop();
+ parentOffset.left += jQuery.css( offsetParent[ 0 ], "borderLeftWidth", true ) -
+ offsetParent.scrollLeft();
+ }
+
+ // Subtract parent offsets and element margins
+ return {
+ top: offset.top - parentOffset.top - jQuery.css( elem, "marginTop", true ),
+ left: offset.left - parentOffset.left - jQuery.css( elem, "marginLeft", true )
+ };
+ },
+
+ // This method will return documentElement in the following cases:
+ // 1) For the element inside the iframe without offsetParent, this method will return
+ // documentElement of the parent window
+ // 2) For the hidden or detached element
+ // 3) For body or html element, i.e. in case of the html node - it will return itself
+ //
+ // but those exceptions were never presented as a real life use-cases
+ // and might be considered as more preferable results.
+ //
+ // This logic, however, is not guaranteed and can change at any point in the future
+ offsetParent: function() {
+ return this.map( function() {
+ var offsetParent = this.offsetParent;
+
+ while ( offsetParent && jQuery.css( offsetParent, "position" ) === "static" ) {
+ offsetParent = offsetParent.offsetParent;
+ }
+
+ return offsetParent || documentElement;
+ } );
+ }
+} );
+
+// Create scrollLeft and scrollTop methods
+jQuery.each( { scrollLeft: "pageXOffset", scrollTop: "pageYOffset" }, function( method, prop ) {
+ var top = "pageYOffset" === prop;
+
+ jQuery.fn[ method ] = function( val ) {
+ return access( this, function( elem, method, val ) {
+ var win = getWindow( elem );
+
+ if ( val === undefined ) {
+ return win ? win[ prop ] : elem[ method ];
+ }
+
+ if ( win ) {
+ win.scrollTo(
+ !top ? val : win.pageXOffset,
+ top ? val : win.pageYOffset
+ );
+
+ } else {
+ elem[ method ] = val;
+ }
+ }, method, val, arguments.length );
+ };
+} );
+
+// Support: Safari<7-8+, Chrome<37-44+
+// Add the top/left cssHooks using jQuery.fn.position
+// Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084
+// Blink bug: https://code.google.com/p/chromium/issues/detail?id=229280
+// getComputedStyle returns percent when specified for top/left/bottom/right;
+// rather than make the css module depend on the offset module, just check for it here
+jQuery.each( [ "top", "left" ], function( i, prop ) {
+ jQuery.cssHooks[ prop ] = addGetHookIf( support.pixelPosition,
+ function( elem, computed ) {
+ if ( computed ) {
+ computed = curCSS( elem, prop );
+
+ // If curCSS returns percentage, fallback to offset
+ return rnumnonpx.test( computed ) ?
+ jQuery( elem ).position()[ prop ] + "px" :
+ computed;
+ }
+ }
+ );
+} );
+
+
+// Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods
+jQuery.each( { Height: "height", Width: "width" }, function( name, type ) {
+ jQuery.each( { padding: "inner" + name, content: type, "": "outer" + name },
+ function( defaultExtra, funcName ) {
+
+ // Margin is only for outerHeight, outerWidth
+ jQuery.fn[ funcName ] = function( margin, value ) {
+ var chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" ),
+ extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" );
+
+ return access( this, function( elem, type, value ) {
+ var doc;
+
+ if ( jQuery.isWindow( elem ) ) {
+
+ // As of 5/8/2012 this will yield incorrect results for Mobile Safari, but there
+ // isn't a whole lot we can do. See pull request at this URL for discussion:
+ // https://github.com/jquery/jquery/pull/764
+ return elem.document.documentElement[ "client" + name ];
+ }
+
+ // Get document width or height
+ if ( elem.nodeType === 9 ) {
+ doc = elem.documentElement;
+
+ // Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height],
+ // whichever is greatest
+ return Math.max(
+ elem.body[ "scroll" + name ], doc[ "scroll" + name ],
+ elem.body[ "offset" + name ], doc[ "offset" + name ],
+ doc[ "client" + name ]
+ );
+ }
+
+ return value === undefined ?
+
+ // Get width or height on the element, requesting but not forcing parseFloat
+ jQuery.css( elem, type, extra ) :
+
+ // Set width or height on the element
+ jQuery.style( elem, type, value, extra );
+ }, type, chainable ? margin : undefined, chainable, null );
+ };
+ } );
+} );
+
+
+jQuery.fn.extend( {
+
+ bind: function( types, data, fn ) {
+ return this.on( types, null, data, fn );
+ },
+ unbind: function( types, fn ) {
+ return this.off( types, null, fn );
+ },
+
+ delegate: function( selector, types, data, fn ) {
+ return this.on( types, selector, data, fn );
+ },
+ undelegate: function( selector, types, fn ) {
+
+ // ( namespace ) or ( selector, types [, fn] )
+ return arguments.length === 1 ?
+ this.off( selector, "**" ) :
+ this.off( types, selector || "**", fn );
+ },
+ size: function() {
+ return this.length;
+ }
+} );
+
+jQuery.fn.andSelf = jQuery.fn.addBack;
+
+
+
+
+// Register as a named AMD module, since jQuery can be concatenated with other
+// files that may use define, but not via a proper concatenation script that
+// understands anonymous AMD modules. A named AMD is safest and most robust
+// way to register. Lowercase jquery is used because AMD module names are
+// derived from file names, and jQuery is normally delivered in a lowercase
+// file name. Do this after creating the global so that if an AMD module wants
+// to call noConflict to hide this version of jQuery, it will work.
+
+// Note that for maximum portability, libraries that are not jQuery should
+// declare themselves as anonymous modules, and avoid setting a global if an
+// AMD loader is present. jQuery is a special case. For more information, see
+// https://github.com/jrburke/requirejs/wiki/Updating-existing-libraries#wiki-anon
+
+if ( typeof define === "function" && define.amd ) {
+ define( "jquery", [], function() {
+ return jQuery;
+ } );
+}
+
+
+
+var
+
+ // Map over jQuery in case of overwrite
+ _jQuery = window.jQuery,
+
+ // Map over the $ in case of overwrite
+ _$ = window.$;
+
+jQuery.noConflict = function( deep ) {
+ if ( window.$ === jQuery ) {
+ window.$ = _$;
+ }
+
+ if ( deep && window.jQuery === jQuery ) {
+ window.jQuery = _jQuery;
+ }
+
+ return jQuery;
+};
+
+// Expose jQuery and $ identifiers, even in AMD
+// (#7102#comment:10, https://github.com/jquery/jquery/pull/557)
+// and CommonJS for browser emulators (#13566)
+if ( !noGlobal ) {
+ window.jQuery = window.$ = jQuery;
+}
+
+return jQuery;
+}));
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..50a9e6455d
--- /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 https://es5.github.io/#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..56f436c8b8
--- /dev/null
+++ b/actionview/test/ujs/server.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+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
+
+ config.content_security_policy do |policy|
+ policy.default_src :self, :https
+ policy.font_src :self, :https, :data
+ policy.img_src :self, :https, :data
+ policy.object_src :none
+ policy.script_src :self, :https
+ policy.style_src :self, :https
+ end
+
+ config.content_security_policy_nonce_generator = ->(req) { SecureRandom.base64(16) }
+ end
+end
+
+module TestsHelper
+ def test_to(*names)
+ names = names.map { |name| "/test/#{name}.js" }
+ names = %w[/vendor/qunit.js /test/settings.js] + names
+
+ capture do
+ names.each do |name|
+ concat(javascript_include_tag(name))
+ end
+ end
+ 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?
+ if params[:with_xhr_redirect]
+ response.set_header("X-Xhr-Redirect", "http://example.com/")
+ render inline: %{Turbolinks.clearCache()\nTurbolinks.visit("http://example.com/", {"action":"replace"})}
+ else
+ render json: JSON.generate(data)
+ end
+ elsif params[:iframe]
+ payload = JSON.generate(data).gsub("<", "&lt;").gsub(">", "&gt;")
+ html = <<-HTML
+ <script nonce="#{request.content_security_policy_nonce}">
+ 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..8f6f6fc17f
--- /dev/null
+++ b/actionview/test/ujs/views/layouts/application.html.erb
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html id="html">
+ <head>
+ <title><%= @title %></title>
+ <%= csp_meta_tag %>
+ <link href="/vendor/qunit.css" media="screen" rel="stylesheet" type="text/css" media="screen, projection" />
+ <script src="/vendor/jquery-2.2.0.js" type="text/javascript"></script>
+ <%= javascript_tag nonce: true do %>
+ // 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();
+ });
+ });
+ <% end %>
+ <%= javascript_include_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..6b16535216
--- /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', 'call-ajax' %>
+
+<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/.gitignore b/activejob/.gitignore
deleted file mode 100644
index b3aaf55871..0000000000
--- a/activejob/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-test/dummy
diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md
index c523723fc4..8bbecd5a5a 100644
--- a/activejob/CHANGELOG.md
+++ b/activejob/CHANGELOG.md
@@ -1,97 +1,100 @@
-* Allow `DelayedJob`, `Sidekiq`, `qu`, and `que` to report the job id back to
- `ActiveJob::Base` as `provider_job_id`.
+* Restore HashWithIndifferentAccess support to ActiveJob::Arguments.deserialize.
- Fixes #18821.
+ *Gannon McGibbon*
- *Kevin Deisz*, *Jeroen van Baarsen*
+* Include deserialized arguments in job instances returned from
+ `assert_enqueued_with` and `assert_performed_with`
-* `assert_enqueued_jobs` and `assert_performed_jobs` in block form use the
- given number as expected value. This makes the error message much easier to
- understand.
+ *Alan Wu*
- *y-yagi*
+* Allow `assert_enqueued_with`/`assert_performed_with` methods to accept
+ a proc for the `args` argument. This is useful to check if only a subset of arguments
+ matches your expectations.
-* A generated job now inherits from `app/jobs/application_job.rb` by default.
+ *Edouard Chin*
- *Jeroen van Baarsen*
+* `ActionDispatch::IntegrationTest` includes `ActiveJob::TestHelper` module by default.
-* Add an `:only` option to `perform_enqueued_jobs` to filter jobs based on
- type.
+ *Ricardo Díaz*
- This allows specific jobs to be tested, while preventing others from
- being performed unnecessarily.
+* Added `enqueue_retry.active_job`, `retry_stopped.active_job`, and `discard.active_job` hooks.
- Example:
+ *steves*
- def test_hello_job
- assert_performed_jobs 1, only: HelloJob do
- HelloJob.perform_later('jeremy')
- LoggingJob.perform_later
- end
- end
+* Allow `assert_performed_with` to be called without a block.
- An array may also be specified, to support testing multiple jobs.
+ *bogdanvlviv*
- Example:
+* Execution of `assert_performed_jobs`, and `assert_no_performed_jobs`
+ without a block should respect passed `:except`, `:only`, and `:queue` options.
+
+ *bogdanvlviv*
+
+* Allow `:queue` option to job assertions and helpers.
- def test_hello_and_logging_jobs
- assert_nothing_raised do
- assert_performed_jobs 2, only: [HelloJob, LoggingJob] do
- HelloJob.perform_later('jeremy')
- LoggingJob.perform_later('stewie')
- RescueJob.perform_later('david')
- end
- end
- end
+ *bogdanvlviv*
- Fixes #18802.
+* Allow `perform_enqueued_jobs` to be called without a block.
- *Michael Ryan*
+ Performs all of the jobs that have been enqueued up to this point in the test.
-* Allow keyword arguments to be used with Active Job.
+ *Kevin Deisz*
- Fixes #18741.
+* Move `enqueue`/`enqueue_at` notifications to an around callback.
- *Sean Griffin*
+ Improves timing accuracy over the old after callback by including
+ time spent writing to the adapter's IO implementation.
-* Add `:only` option to `assert_enqueued_jobs`, to check the number of times
- a specific kind of job is enqueued.
+ *Zach Kemp*
+
+* Allow call `assert_enqueued_with` with no block.
Example:
+ ```
+ def test_assert_enqueued_with
+ MyJob.perform_later(1,2,3)
+ assert_enqueued_with(job: MyJob, args: [1,2,3], queue: 'low')
+
+ MyJob.set(wait_until: Date.tomorrow.noon).perform_later
+ assert_enqueued_with(job: MyJob, at: Date.tomorrow.noon)
+ end
+ ```
- def test_logging_job
- assert_enqueued_jobs 1, only: LoggingJob do
- LoggingJob.perform_later
- HelloJob.perform_later('jeremy')
- end
- end
+ *bogdanvlviv*
+
+* Allow passing multiple exceptions to `retry_on`, and `discard_on`.
*George Claghorn*
-* `ActiveJob::Base.deserialize` delegates to the job class.
+* Pass the error instance as the second parameter of block executed by `discard_on`.
- Since `ActiveJob::Base#deserialize` can be overridden by subclasses (like
- `ActiveJob::Base#serialize`) this allows jobs to attach arbitrary metadata
- when they get serialized and read it back when they get performed.
+ Fixes #32853.
- Example:
+ *Yuji Yaginuma*
+
+* Remove support for Qu gem.
+
+ Reasons are that the Qu gem wasn't compatible since Rails 5.1,
+ gem development was stopped in 2014 and maintainers have
+ confirmed its demise. See issue #32273
+
+ *Alberto Almagro*
+
+* Add support for timezones to Active Job.
+
+ Record what was the current timezone in effect when the job was
+ enqueued and then restore when the job is executed in same way
+ that the current locale is recorded and restored.
+
+ *Andrew White*
+
+* Rails 6 requires Ruby 2.4.1 or newer.
- class DeliverWebhookJob < ActiveJob::Base
- def serialize
- super.merge('attempt_number' => (@attempt_number || 0) + 1)
- end
+ *Jeremy Daer*
- def deserialize(job_data)
- super
- @attempt_number = job_data['attempt_number']
- end
+* Add support to define custom argument serializers.
- rescue_from(TimeoutError) do |exception|
- raise exception if @attempt_number > 5
- retry_job(wait: 10)
- end
- end
+ *Evgenii Pecherkin*, *Rafael Mendonça França*
- *Isaac Seymour*
-Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/activejob/CHANGELOG.md) for previous changes.
+Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/activejob/CHANGELOG.md) for previous changes.
diff --git a/activejob/MIT-LICENSE b/activejob/MIT-LICENSE
index 0cef8cdda0..274211f710 100644
--- a/activejob/MIT-LICENSE
+++ b/activejob/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2014-2015 David Heinemeier Hansson
+Copyright (c) 2014-2018 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/activejob/README.md b/activejob/README.md
index f9a3183b1a..a2a5289ab7 100644
--- a/activejob/README.md
+++ b/activejob/README.md
@@ -1,7 +1,7 @@
# Active Job -- Make work happen later
Active Job is a framework for declaring jobs and making them run on a variety
-of queueing backends. These jobs can be everything from regularly scheduled
+of queuing backends. These jobs can be everything from regularly scheduled
clean-ups, to billing charges, to mailings. Anything that can be chopped up into
small units of work and run in parallel, really.
@@ -20,12 +20,7 @@ switch between them without having to rewrite your jobs.
## Usage
-Set the queue adapter for Active Job:
-
-``` ruby
-ActiveJob::Base.queue_adapter = :inline # default queue adapter
-```
-Note: To learn how to use your preferred queueing backend see its adapter
+To learn how to use your preferred queuing backend see its adapter
documentation at
[ActiveJob::QueueAdapters](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html).
@@ -44,7 +39,7 @@ end
Enqueue a job like so:
```ruby
-MyJob.perform_later record # Enqueue a job to be performed as soon the queueing system is free.
+MyJob.perform_later record # Enqueue a job to be performed as soon as the queuing system is free.
```
```ruby
@@ -87,12 +82,18 @@ This works with any class that mixes in GlobalID::Identification, which
by default has been mixed into Active Record classes.
-## Supported queueing systems
+## Supported queuing systems
-Active Job has built-in adapters for multiple queueing backends (Sidekiq,
+Active Job has built-in adapters for multiple queuing backends (Sidekiq,
Resque, Delayed Job and others). To get an up-to-date list of the adapters
see the API Documentation for [ActiveJob::QueueAdapters](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html).
+**Please note:** We are not accepting pull requests for new adapters. We
+encourage library authors to provide an ActiveJob adapter as part of
+their gem, or as a stand-alone gem. For discussion about this see the
+following PRs: [23311](https://github.com/rails/rails/issues/23311#issuecomment-176275718),
+[21406](https://github.com/rails/rails/pull/21406#issuecomment-138813484), and [#32285](https://github.com/rails/rails/pull/32285).
+
## Auxiliary gems
* [activejob-stats](https://github.com/seuros/activejob-stats)
@@ -102,10 +103,10 @@ see the API Documentation for [ActiveJob::QueueAdapters](http://api.rubyonrails.
The latest version of Active Job can be installed with RubyGems:
```
- % gem install activejob
+ $ gem install activejob
```
-Source code can be downloaded as part of the Rails project on GitHub
+Source code can be downloaded as part of the Rails project on GitHub:
* https://github.com/rails/rails/tree/master/activejob
@@ -113,7 +114,7 @@ Source code can be downloaded as part of the Rails project on GitHub
Active Job is released under the MIT license:
-* http://www.opensource.org/licenses/MIT
+* https://opensource.org/licenses/MIT
## Support
@@ -122,7 +123,7 @@ API documentation is at:
* http://api.rubyonrails.org
-Bug reports can be filed for the Ruby on Rails project here:
+Bug reports for the Ruby on Rails project can be filed here:
* https://github.com/rails/rails/issues
diff --git a/activejob/Rakefile b/activejob/Rakefile
index 8c86df3c91..0f88b22e8d 100644
--- a/activejob/Rakefile
+++ b/activejob/Rakefile
@@ -1,59 +1,62 @@
-require 'rake/testtask'
+# frozen_string_literal: true
-ACTIVEJOB_ADAPTERS = %w(inline delayed_job qu que queue_classic resque sidekiq sneakers sucker_punch backburner test)
-ACTIVEJOB_ADAPTERS -= %w(queue_classic) if defined?(JRUBY_VERSION)
+require "rake/testtask"
+
+ACTIVEJOB_ADAPTERS = %w(async inline delayed_job que queue_classic resque sidekiq sneakers sucker_punch backburner test)
+ACTIVEJOB_ADAPTERS.delete("queue_classic") if defined?(JRUBY_VERSION)
task default: :test
-task test: 'test:default'
+task test: "test:default"
+
+task :package
namespace :test do
- desc 'Run all adapter tests'
+ desc "Run all adapter tests"
task :default do
run_without_aborting ACTIVEJOB_ADAPTERS.map { |a| "test:#{a}" }
end
- desc 'Run all adapter tests in isolation'
+ desc "Run all adapter tests in isolation"
task :isolated do
run_without_aborting ACTIVEJOB_ADAPTERS.map { |a| "test:isolated:#{a}" }
end
- desc 'Run integration tests for all adapters'
+ desc "Run integration tests for all adapters"
task :integration do
- run_without_aborting (ACTIVEJOB_ADAPTERS - ['test']).map { |a| "test:integration:#{a}" }
+ run_without_aborting (ACTIVEJOB_ADAPTERS - ["test"]).map { |a| "test:integration:#{a}" }
end
- task 'env:integration' do
- ENV['AJ_INTEGRATION_TESTS'] = "1"
+ task "env:integration" do
+ ENV["AJ_INTEGRATION_TESTS"] = "1"
end
ACTIVEJOB_ADAPTERS.each do |adapter|
- task("env:#{adapter}") { ENV['AJ_ADAPTER'] = adapter }
+ task("env:#{adapter}") { ENV["AJ_ADAPTER"] = adapter }
Rake::TestTask.new(adapter => "test:env:#{adapter}") do |t|
t.description = "Run adapter tests for #{adapter}"
- t.libs << 'test'
- t.test_files = FileList['test/cases/**/*_test.rb']
+ t.libs << "test"
+ t.test_files = FileList["test/cases/**/*_test.rb"]
t.verbose = true
- t.warning = false
+ t.warning = true
t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
end
namespace :isolated do
task adapter => "test:env:#{adapter}" do
- dir = File.dirname(__FILE__)
- Dir.glob("#{dir}/test/cases/**/*_test.rb").all? do |file|
- sh(Gem.ruby, '-w', "-I#{dir}/lib", "-I#{dir}/test", file)
- end or raise 'Failures'
+ Dir.glob("#{__dir__}/test/cases/**/*_test.rb").all? do |file|
+ sh(Gem.ruby, "-w", "-I#{__dir__}/lib", "-I#{__dir__}/test", file)
+ end || raise("Failures")
end
end
namespace :integration do
- Rake::TestTask.new(adapter => ["test:env:#{adapter}", 'test:env:integration']) do |t|
+ Rake::TestTask.new(adapter => ["test:env:#{adapter}", "test:env:integration"]) do |t|
t.description = "Run integration tests for #{adapter}"
- t.libs << 'test'
- t.test_files = FileList['test/integration/**/*_test.rb']
+ t.libs << "test"
+ t.test_files = FileList["test/integration/**/*_test.rb"]
t.verbose = true
- t.warning = false
+ t.warning = true
t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
end
end
diff --git a/activejob/activejob.gemspec b/activejob/activejob.gemspec
index 24e38e495f..20b9d4ccdd 100644
--- a/activejob/activejob.gemspec
+++ b/activejob/activejob.gemspec
@@ -1,23 +1,33 @@
-version = File.read(File.expand_path('../../RAILS_VERSION', __FILE__)).strip
+# frozen_string_literal: true
+
+version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip
Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
- s.name = 'activejob'
+ s.name = "activejob"
s.version = version
- s.summary = 'Job framework with pluggable queues.'
- s.description = 'Declare job classes that can be run by a variety of queueing backends.'
+ s.summary = "Job framework with pluggable queues."
+ s.description = "Declare job classes that can be run by a variety of queuing backends."
+
+ s.required_ruby_version = ">= 2.4.1"
+
+ s.license = "MIT"
- s.required_ruby_version = '>= 2.2.2'
+ s.author = "David Heinemeier Hansson"
+ s.email = "david@loudthinking.com"
+ s.homepage = "http://rubyonrails.org"
- s.license = 'MIT'
+ s.files = Dir["CHANGELOG.md", "MIT-LICENSE", "README.md", "lib/**/*"]
+ s.require_path = "lib"
- s.author = 'David Heinemeier Hansson'
- s.email = 'david@loudthinking.com'
- s.homepage = 'http://www.rubyonrails.org'
+ s.metadata = {
+ "source_code_uri" => "https://github.com/rails/rails/tree/v#{version}/activejob",
+ "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/activejob/CHANGELOG.md"
+ }
- s.files = Dir['CHANGELOG.md', 'MIT-LICENSE', 'README.md', 'lib/**/*']
- s.require_path = 'lib'
+ # NOTE: Please read our dependency guidelines before updating versions:
+ # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves
- s.add_dependency 'activesupport', version
- s.add_dependency 'globalid', '>= 0.3.0'
+ s.add_dependency "activesupport", version
+ s.add_dependency "globalid", ">= 0.3.6"
end
diff --git a/activejob/bin/test b/activejob/bin/test
new file mode 100755
index 0000000000..c53377cc97
--- /dev/null
+++ b/activejob/bin/test
@@ -0,0 +1,5 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+COMPONENT_ROOT = File.expand_path("..", __dir__)
+require_relative "../../tools/test"
diff --git a/activejob/lib/active_job.rb b/activejob/lib/active_job.rb
index 3d4f63b261..01fab4d918 100644
--- a/activejob/lib/active_job.rb
+++ b/activejob/lib/active_job.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
#--
-# Copyright (c) 2014-2015 David Heinemeier Hansson
+# Copyright (c) 2014-2018 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -21,16 +23,17 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
-require 'active_support'
-require 'active_support/rails'
-require 'active_job/version'
-require 'global_id'
+require "active_support"
+require "active_support/rails"
+require "active_job/version"
+require "global_id"
module ActiveJob
extend ActiveSupport::Autoload
autoload :Base
autoload :QueueAdapters
+ autoload :Serializers
autoload :ConfiguredJob
autoload :TestCase
autoload :TestHelper
diff --git a/activejob/lib/active_job/arguments.rb b/activejob/lib/active_job/arguments.rb
index 8e462bfe5d..fa58c50ed0 100644
--- a/activejob/lib/active_job/arguments.rb
+++ b/activejob/lib/active_job/arguments.rb
@@ -1,78 +1,90 @@
-require 'active_support/core_ext/hash'
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash"
module ActiveJob
# Raised when an exception is raised during job arguments deserialization.
#
- # Wraps the original exception raised as +original_exception+.
+ # Wraps the original exception raised as +cause+.
class DeserializationError < StandardError
- # The original exception that was raised during deserialization of job
- # arguments.
- attr_reader :original_exception
-
- def initialize(e) #:nodoc:
- super("Error while trying to deserialize arguments: #{e.message}")
- @original_exception = e
- set_backtrace e.backtrace
+ def initialize #:nodoc:
+ super("Error while trying to deserialize arguments: #{$!.message}")
+ set_backtrace $!.backtrace
end
end
# Raised when an unsupported argument type is set as a job argument. We
- # currently support NilClass, Fixnum, Float, String, TrueClass, FalseClass,
- # Bignum and objects that can be represented as GlobalIDs (ex: Active Record).
+ # currently support String, Integer, Float, NilClass, TrueClass, FalseClass,
+ # BigDecimal, Symbol, Date, Time, DateTime, ActiveSupport::TimeWithZone,
+ # ActiveSupport::Duration, Hash, ActiveSupport::HashWithIndifferentAccess,
+ # Array or GlobalID::Identification instances, although this can be extended
+ # by adding custom serializers.
# Raised if you set the key for a Hash something else than a string or
# a symbol. Also raised when trying to serialize an object which can't be
- # identified with a Global ID - such as an unpersisted Active Record model.
+ # identified with a GlobalID - such as an unpersisted Active Record model.
class SerializationError < ArgumentError; end
module Arguments
extend self
- # :nodoc:
- TYPE_WHITELIST = [ NilClass, Fixnum, Float, String, TrueClass, FalseClass, Bignum ]
-
- # Serializes a set of arguments. Whitelisted types are returned
- # as-is. Arrays/Hashes are serialized element by element.
- # All other types are serialized using GlobalID.
+ # Serializes a set of arguments. Intrinsic types that can safely be
+ # serialized without mutation are returned as-is. Arrays/Hashes are
+ # serialized element by element. All other types are serialized using
+ # GlobalID.
def serialize(arguments)
arguments.map { |argument| serialize_argument(argument) }
end
- # Deserializes a set of arguments. Whitelisted types are returned
- # as-is. Arrays/Hashes are deserialized element by element.
- # All other types are deserialized using GlobalID.
+ # Deserializes a set of arguments. Intrinsic types that can safely be
+ # deserialized without mutation are returned as-is. Arrays/Hashes are
+ # deserialized element by element. All other types are deserialized using
+ # GlobalID.
def deserialize(arguments)
arguments.map { |argument| deserialize_argument(argument) }
- rescue => e
- raise DeserializationError.new(e)
+ rescue
+ raise DeserializationError
end
private
+
+ # :nodoc:
+ PERMITTED_TYPES = [ NilClass, String, Integer, Float, BigDecimal, TrueClass, FalseClass ]
# :nodoc:
- GLOBALID_KEY = '_aj_globalid'.freeze
+ GLOBALID_KEY = "_aj_globalid"
# :nodoc:
- SYMBOL_KEYS_KEY = '_aj_symbol_keys'.freeze
+ SYMBOL_KEYS_KEY = "_aj_symbol_keys"
# :nodoc:
- WITH_INDIFFERENT_ACCESS_KEY = '_aj_hash_with_indifferent_access'.freeze
- private_constant :GLOBALID_KEY, :SYMBOL_KEYS_KEY, :WITH_INDIFFERENT_ACCESS_KEY
+ WITH_INDIFFERENT_ACCESS_KEY = "_aj_hash_with_indifferent_access"
+ # :nodoc:
+ OBJECT_SERIALIZER_KEY = "_aj_serialized"
+
+ # :nodoc:
+ RESERVED_KEYS = [
+ GLOBALID_KEY, GLOBALID_KEY.to_sym,
+ SYMBOL_KEYS_KEY, SYMBOL_KEYS_KEY.to_sym,
+ OBJECT_SERIALIZER_KEY, OBJECT_SERIALIZER_KEY.to_sym,
+ WITH_INDIFFERENT_ACCESS_KEY, WITH_INDIFFERENT_ACCESS_KEY.to_sym,
+ ]
+ private_constant :PERMITTED_TYPES, :RESERVED_KEYS, :GLOBALID_KEY, :SYMBOL_KEYS_KEY, :WITH_INDIFFERENT_ACCESS_KEY
def serialize_argument(argument)
case argument
- when *TYPE_WHITELIST
+ when *PERMITTED_TYPES
argument
when GlobalID::Identification
convert_to_global_id_hash(argument)
when Array
argument.map { |arg| serialize_argument(arg) }
when ActiveSupport::HashWithIndifferentAccess
- result = serialize_hash(argument)
- result[WITH_INDIFFERENT_ACCESS_KEY] = serialize_argument(true)
- result
+ serialize_indifferent_hash(argument)
when Hash
symbol_keys = argument.each_key.grep(Symbol).map(&:to_s)
result = serialize_hash(argument)
result[SYMBOL_KEYS_KEY] = symbol_keys
result
+ when -> (arg) { arg.respond_to?(:permitted?) }
+ serialize_indifferent_hash(argument.to_h)
else
- raise SerializationError.new("Unsupported argument type: #{argument.class.name}")
+ Serializers.serialize(argument)
end
end
@@ -80,13 +92,15 @@ module ActiveJob
case argument
when String
GlobalID::Locator.locate(argument) || argument
- when *TYPE_WHITELIST
+ when *PERMITTED_TYPES
argument
when Array
argument.map { |arg| deserialize_argument(arg) }
when Hash
if serialized_global_id?(argument)
deserialize_global_id argument
+ elsif custom_serialized?(argument)
+ Serializers.deserialize(argument)
else
deserialize_hash(argument)
end
@@ -96,13 +110,17 @@ module ActiveJob
end
def serialized_global_id?(hash)
- hash.size == 1 and hash.include?(GLOBALID_KEY)
+ hash.size == 1 && hash.include?(GLOBALID_KEY)
end
def deserialize_global_id(hash)
GlobalID::Locator.locate hash[GLOBALID_KEY]
end
+ def custom_serialized?(hash)
+ hash.key?(OBJECT_SERIALIZER_KEY)
+ end
+
def serialize_hash(argument)
argument.each_with_object({}) do |(key, value), hash|
hash[serialize_hash_key(key)] = serialize_argument(value)
@@ -119,14 +137,6 @@ module ActiveJob
result
end
- # :nodoc:
- RESERVED_KEYS = [
- GLOBALID_KEY, GLOBALID_KEY.to_sym,
- SYMBOL_KEYS_KEY, SYMBOL_KEYS_KEY.to_sym,
- WITH_INDIFFERENT_ACCESS_KEY, WITH_INDIFFERENT_ACCESS_KEY.to_sym,
- ]
- private_constant :RESERVED_KEYS
-
def serialize_hash_key(key)
case key
when *RESERVED_KEYS
@@ -138,8 +148,17 @@ module ActiveJob
end
end
+ def serialize_indifferent_hash(indifferent_hash)
+ result = serialize_hash(indifferent_hash)
+ result[WITH_INDIFFERENT_ACCESS_KEY] = serialize_argument(true)
+ result
+ end
+
def transform_symbol_keys(hash, symbol_keys)
- hash.transform_keys do |key|
+ # NOTE: HashWithIndifferentAccess#transform_keys always
+ # returns stringified keys with indifferent access
+ # so we call #to_h here to ensure keys are symbolized.
+ hash.to_h.transform_keys do |key|
if symbol_keys.include?(key)
key.to_sym
else
diff --git a/activejob/lib/active_job/base.rb b/activejob/lib/active_job/base.rb
index fd49b3fda5..ed41fac4b8 100644
--- a/activejob/lib/active_job/base.rb
+++ b/activejob/lib/active_job/base.rb
@@ -1,10 +1,16 @@
-require 'active_job/core'
-require 'active_job/queue_adapter'
-require 'active_job/queue_name'
-require 'active_job/enqueuing'
-require 'active_job/execution'
-require 'active_job/callbacks'
-require 'active_job/logging'
+# frozen_string_literal: true
+
+require "active_job/core"
+require "active_job/queue_adapter"
+require "active_job/queue_name"
+require "active_job/queue_priority"
+require "active_job/enqueuing"
+require "active_job/execution"
+require "active_job/callbacks"
+require "active_job/exceptions"
+require "active_job/logging"
+require "active_job/timezones"
+require "active_job/translation"
module ActiveJob #:nodoc:
# = Active Job
@@ -34,7 +40,7 @@ module ActiveJob #:nodoc:
# Records that are passed in are serialized/deserialized using Global
# ID. More information can be found in Arguments.
#
- # To enqueue a job to be performed as soon the queueing system is free:
+ # To enqueue a job to be performed as soon as the queuing system is free:
#
# ProcessPhotoJob.perform_later(photo)
#
@@ -56,10 +62,14 @@ module ActiveJob #:nodoc:
include Core
include QueueAdapter
include QueueName
+ include QueuePriority
include Enqueuing
include Execution
include Callbacks
+ include Exceptions
include Logging
+ include Timezones
+ include Translation
ActiveSupport.run_load_hooks(:active_job, self)
end
diff --git a/activejob/lib/active_job/callbacks.rb b/activejob/lib/active_job/callbacks.rb
index 2b6149e84e..61317c7cfc 100644
--- a/activejob/lib/active_job/callbacks.rb
+++ b/activejob/lib/active_job/callbacks.rb
@@ -1,10 +1,12 @@
-require 'active_support/callbacks'
+# frozen_string_literal: true
+
+require "active_support/callbacks"
module ActiveJob
# = Active Job Callbacks
#
# Active Job provides hooks during the life cycle of a job. Callbacks allow you
- # to trigger logic during the life cycle of a job. Available callbacks are:
+ # to trigger logic during this cycle. Available callbacks are:
#
# * <tt>before_enqueue</tt>
# * <tt>around_enqueue</tt>
@@ -13,10 +15,17 @@ module ActiveJob
# * <tt>around_perform</tt>
# * <tt>after_perform</tt>
#
+ # NOTE: Calling the same callback multiple times will overwrite previous callback definitions.
+ #
module Callbacks
extend ActiveSupport::Concern
include ActiveSupport::Callbacks
+ class << self
+ include ActiveSupport::Callbacks
+ define_callbacks :execute
+ end
+
included do
define_callbacks :perform
define_callbacks :enqueue
@@ -121,8 +130,8 @@ module ActiveJob
set_callback(:enqueue, :after, *filters, &blk)
end
- # Defines a callback that will get called before and after the
- # job is enqueued.
+ # Defines a callback that will get called around the enqueuing
+ # of the job.
#
# class VideoProcessJob < ActiveJob::Base
# queue_as :default
diff --git a/activejob/lib/active_job/configured_job.rb b/activejob/lib/active_job/configured_job.rb
index 979280b910..67daf48b36 100644
--- a/activejob/lib/active_job/configured_job.rb
+++ b/activejob/lib/active_job/configured_job.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
module ActiveJob
class ConfiguredJob #:nodoc:
- def initialize(job_class, options={})
+ def initialize(job_class, options = {})
@options = options
@job_class = job_class
end
diff --git a/activejob/lib/active_job/core.rb b/activejob/lib/active_job/core.rb
index 0528572cd0..698153636b 100644
--- a/activejob/lib/active_job/core.rb
+++ b/activejob/lib/active_job/core.rb
@@ -1,33 +1,45 @@
+# frozen_string_literal: true
+
module ActiveJob
# Provides general behavior that will be included into every Active Job
# object that inherits from ActiveJob::Base.
module Core
extend ActiveSupport::Concern
- included do
- # Job arguments
- attr_accessor :arguments
- attr_writer :serialized_arguments
+ # Job arguments
+ attr_accessor :arguments
+ attr_writer :serialized_arguments
- # Timestamp when the job should be performed
- attr_accessor :scheduled_at
+ # Timestamp when the job should be performed
+ attr_accessor :scheduled_at
- # Job Identifier
- attr_accessor :job_id
+ # Job Identifier
+ attr_accessor :job_id
- # Queue in which the job will reside.
- attr_writer :queue_name
+ # Queue in which the job will reside.
+ attr_writer :queue_name
- # ID optionally provided by adapter
- attr_accessor :provider_job_id
- end
+ # Priority that the job will have (lower is more priority).
+ attr_writer :priority
+
+ # ID optionally provided by adapter
+ attr_accessor :provider_job_id
+
+ # Number of times this job has been executed (which increments on every retry, like after an exception).
+ attr_accessor :executions
+
+ # I18n.locale to be used during the job.
+ attr_accessor :locale
+
+ # Timezone to be used during the job.
+ attr_accessor :timezone
# These methods will be included into any Active Job object, adding
# helpers for de/serialization and creation of job instances.
module ClassMethods
# Creates a new job instance from a hash created with +serialize+
def deserialize(job_data)
- job = job_data['job_class'].constantize.new
+ job = job_data["job_class"].constantize.new
job.deserialize(job_data)
job
end
@@ -40,6 +52,7 @@ module ActiveJob
# * <tt>:wait</tt> - Enqueues the job with the specified delay
# * <tt>:wait_until</tt> - Enqueues the job at the time specified
# * <tt>:queue</tt> - Enqueues the job on the specified queue
+ # * <tt>:priority</tt> - Enqueues the job with the specified priority
#
# ==== Examples
#
@@ -48,7 +61,8 @@ module ActiveJob
# VideoJob.set(wait_until: Time.now.tomorrow).perform_later(Video.last)
# VideoJob.set(queue: :some_queue, wait: 5.minutes).perform_later(Video.last)
# VideoJob.set(queue: :some_queue, wait_until: Time.now.tomorrow).perform_later(Video.last)
- def set(options={})
+ # VideoJob.set(queue: :some_queue, wait: 5.minutes, priority: 10).perform_later(Video.last)
+ def set(options = {})
ConfiguredJob.new(self, options)
end
end
@@ -59,16 +73,23 @@ module ActiveJob
@arguments = arguments
@job_id = SecureRandom.uuid
@queue_name = self.class.queue_name
+ @priority = self.class.priority
+ @executions = 0
end
# Returns a hash with the job data that can safely be passed to the
- # queueing adapter.
+ # queuing adapter.
def serialize
{
- 'job_class' => self.class.name,
- 'job_id' => job_id,
- 'queue_name' => queue_name,
- 'arguments' => serialize_arguments(arguments)
+ "job_class" => self.class.name,
+ "job_id" => job_id,
+ "provider_job_id" => provider_job_id,
+ "queue_name" => queue_name,
+ "priority" => priority,
+ "arguments" => serialize_arguments_if_needed(arguments),
+ "executions" => executions,
+ "locale" => I18n.locale.to_s,
+ "timezone" => Time.zone.try(:name)
}
end
@@ -78,40 +99,63 @@ module ActiveJob
# ==== Examples
#
# class DeliverWebhookJob < ActiveJob::Base
+ # attr_writer :attempt_number
+ #
+ # def attempt_number
+ # @attempt_number ||= 0
+ # end
+ #
# def serialize
- # super.merge('attempt_number' => (@attempt_number || 0) + 1)
+ # super.merge('attempt_number' => attempt_number + 1)
# end
#
# def deserialize(job_data)
# super
- # @attempt_number = job_data['attempt_number']
+ # self.attempt_number = job_data['attempt_number']
# end
#
- # rescue_from(TimeoutError) do |exception|
- # raise exception if @attempt_number > 5
+ # rescue_from(Timeout::Error) do |exception|
+ # raise exception if attempt_number > 5
# retry_job(wait: 10)
# end
# end
def deserialize(job_data)
- self.job_id = job_data['job_id']
- self.queue_name = job_data['queue_name']
- self.serialized_arguments = job_data['arguments']
+ self.job_id = job_data["job_id"]
+ self.provider_job_id = job_data["provider_job_id"]
+ self.queue_name = job_data["queue_name"]
+ self.priority = job_data["priority"]
+ self.serialized_arguments = job_data["arguments"]
+ self.executions = job_data["executions"]
+ self.locale = job_data["locale"] || I18n.locale.to_s
+ self.timezone = job_data["timezone"] || Time.zone.try(:name)
end
private
+ def serialize_arguments_if_needed(arguments)
+ if arguments_serialized?
+ @serialized_arguments
+ else
+ serialize_arguments(arguments)
+ end
+ end
+
def deserialize_arguments_if_needed
- if defined?(@serialized_arguments) && @serialized_arguments.present?
+ if arguments_serialized?
@arguments = deserialize_arguments(@serialized_arguments)
@serialized_arguments = nil
end
end
- def serialize_arguments(serialized_args)
- Arguments.serialize(serialized_args)
+ def serialize_arguments(arguments)
+ Arguments.serialize(arguments)
end
def deserialize_arguments(serialized_args)
Arguments.deserialize(serialized_args)
end
+
+ def arguments_serialized?
+ defined?(@serialized_arguments) && @serialized_arguments
+ end
end
end
diff --git a/activejob/lib/active_job/enqueuing.rb b/activejob/lib/active_job/enqueuing.rb
index 98d92385dd..b5b9f23c00 100644
--- a/activejob/lib/active_job/enqueuing.rb
+++ b/activejob/lib/active_job/enqueuing.rb
@@ -1,16 +1,20 @@
-require 'active_job/arguments'
+# frozen_string_literal: true
+
+require "active_job/arguments"
module ActiveJob
- # Provides behavior for enqueuing and retrying jobs.
+ # Provides behavior for enqueuing jobs.
module Enqueuing
extend ActiveSupport::Concern
# Includes the +perform_later+ method for job initialization.
module ClassMethods
- # Push a job onto the queue. The arguments must be legal JSON types
- # (string, int, float, nil, true, false, hash or array) or
- # GlobalID::Identification instances. Arbitrary Ruby objects
- # are not supported.
+ # Push a job onto the queue. By default the arguments must be either String,
+ # Integer, Float, NilClass, TrueClass, FalseClass, BigDecimal, Symbol, Date,
+ # Time, DateTime, ActiveSupport::TimeWithZone, ActiveSupport::Duration,
+ # Hash, ActiveSupport::HashWithIndifferentAccess, Array or
+ # GlobalID::Identification instances, although this can be extended by adding
+ # custom serializers.
#
# Returns an instance of the job class queued with arguments available in
# Job#arguments.
@@ -18,42 +22,19 @@ 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
- # Reschedules the job to be re-executed. This is useful in combination
- # with the +rescue_from+ option. When you rescue an exception from your job
- # you can ask Active Job to retry performing your job.
- #
- # ==== Options
- # * <tt>:wait</tt> - Enqueues the job with the specified delay
- # * <tt>:wait_until</tt> - Enqueues the job at the time specified
- # * <tt>:queue</tt> - Enqueues the job on the specified queue
- #
- # ==== Examples
- #
- # class SiteScrapperJob < ActiveJob::Base
- # rescue_from(ErrorLoadingSite) do
- # retry_job queue: :low_priority
- # end
- #
- # def perform(*args)
- # # raise ErrorLoadingSite if cannot scrape
- # end
- # end
- def retry_job(options={})
- enqueue options
- end
-
# Enqueues the job to be performed by the queue adapter.
#
# ==== Options
# * <tt>:wait</tt> - Enqueues the job with the specified delay
# * <tt>:wait_until</tt> - Enqueues the job at the time specified
# * <tt>:queue</tt> - Enqueues the job on the specified queue
+ # * <tt>:priority</tt> - Enqueues the job with the specified priority
#
# ==== Examples
#
@@ -61,13 +42,15 @@ module ActiveJob
# my_job_instance.enqueue wait: 5.minutes
# my_job_instance.enqueue queue: :important
# my_job_instance.enqueue wait_until: Date.tomorrow.midnight
- def enqueue(options={})
+ # my_job_instance.enqueue priority: 10
+ def enqueue(options = {})
self.scheduled_at = options[:wait].seconds.from_now.to_f if options[:wait]
self.scheduled_at = options[:wait_until].to_f if options[:wait_until]
self.queue_name = self.class.queue_name_from_part(options[:queue]) if options[:queue]
+ self.priority = options[:priority].to_i if options[:priority]
run_callbacks :enqueue do
- if self.scheduled_at
- self.class.queue_adapter.enqueue_at self, self.scheduled_at
+ if scheduled_at
+ self.class.queue_adapter.enqueue_at self, scheduled_at
else
self.class.queue_adapter.enqueue self
end
diff --git a/activejob/lib/active_job/exceptions.rb b/activejob/lib/active_job/exceptions.rb
new file mode 100644
index 0000000000..bc9e168971
--- /dev/null
+++ b/activejob/lib/active_job/exceptions.rb
@@ -0,0 +1,143 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/numeric/time"
+
+module ActiveJob
+ # Provides behavior for retrying and discarding jobs on exceptions.
+ module Exceptions
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ # Catch the exception and reschedule job for re-execution after so many seconds, for a specific number of attempts.
+ # The number of attempts includes the total executions of a job, not just the retried executions.
+ # If the exception keeps getting raised beyond the specified number of attempts, the exception is allowed to
+ # bubble up to the underlying queuing system, which may have its own retry mechanism or place it in a
+ # holding queue for inspection.
+ #
+ # You can also pass a block that'll be invoked if the retry attempts fail for custom logic rather than letting
+ # the exception bubble up. This block is yielded with the job instance as the first and the error instance as the second parameter.
+ #
+ # ==== Options
+ # * <tt>:wait</tt> - Re-enqueues the job with a delay specified either in seconds (default: 3 seconds),
+ # as a computing proc that the number of executions so far as an argument, or as a symbol reference of
+ # <tt>:exponentially_longer</tt>, which applies the wait algorithm of <tt>(executions ** 4) + 2</tt>
+ # (first wait 3s, then 18s, then 83s, etc)
+ # * <tt>:attempts</tt> - Re-enqueues the job the specified number of times (default: 5 attempts),
+ # attempts here refers to the total number of times the job is executed, not just retried executions
+ # * <tt>:queue</tt> - Re-enqueues the job on a different queue
+ # * <tt>:priority</tt> - Re-enqueues the job with a different priority
+ #
+ # ==== Examples
+ #
+ # class RemoteServiceJob < ActiveJob::Base
+ # retry_on CustomAppException # defaults to 3s wait, 5 attempts
+ # retry_on AnotherCustomAppException, wait: ->(executions) { executions * 2 }
+ # retry_on(YetAnotherCustomAppException) do |job, error|
+ # ExceptionNotifier.caught(error)
+ # end
+ # retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3
+ # retry_on Net::OpenTimeout, wait: :exponentially_longer, attempts: 10
+ #
+ # def perform(*args)
+ # # Might raise CustomAppException, AnotherCustomAppException, or YetAnotherCustomAppException 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
+ def retry_on(*exceptions, wait: 3.seconds, attempts: 5, queue: nil, priority: nil)
+ rescue_from(*exceptions) do |error|
+ if executions < attempts
+ retry_job wait: determine_delay(wait), queue: queue, priority: priority, error: error
+ else
+ if block_given?
+ instrument :retry_stopped, error: error do
+ yield self, error
+ end
+ else
+ instrument :retry_stopped, error: error
+ raise error
+ end
+ end
+ end
+ end
+
+ # Discard the job with no attempts to retry, if the exception is raised. This is useful when the subject of the job,
+ # like an Active Record, is no longer available, and the job is thus no longer relevant.
+ #
+ # You can also pass a block that'll be invoked. This block is yielded with the job instance as the first and the error instance as the second parameter.
+ #
+ # ==== Example
+ #
+ # class SearchIndexingJob < ActiveJob::Base
+ # discard_on ActiveJob::DeserializationError
+ # discard_on(CustomAppException) do |job, error|
+ # ExceptionNotifier.caught(error)
+ # end
+ #
+ # def perform(record)
+ # # Will raise ActiveJob::DeserializationError if the record can't be deserialized
+ # # Might raise CustomAppException for something domain specific
+ # end
+ # end
+ def discard_on(*exceptions)
+ rescue_from(*exceptions) do |error|
+ instrument :discard, error: error do
+ yield self, error if block_given?
+ end
+ end
+ end
+ end
+
+ # Reschedules the job to be re-executed. This is useful in combination
+ # with the +rescue_from+ option. When you rescue an exception from your job
+ # you can ask Active Job to retry performing your job.
+ #
+ # ==== Options
+ # * <tt>:wait</tt> - Enqueues the job with the specified delay in seconds
+ # * <tt>:wait_until</tt> - Enqueues the job at the time specified
+ # * <tt>:queue</tt> - Enqueues the job on the specified queue
+ # * <tt>:priority</tt> - Enqueues the job with the specified priority
+ #
+ # ==== Examples
+ #
+ # class SiteScraperJob < ActiveJob::Base
+ # rescue_from(ErrorLoadingSite) do
+ # retry_job queue: :low_priority
+ # end
+ #
+ # def perform(*args)
+ # # raise ErrorLoadingSite if cannot scrape
+ # end
+ # end
+ def retry_job(options = {})
+ instrument :enqueue_retry, options.slice(:error, :wait) do
+ enqueue options
+ end
+ end
+
+ private
+ def determine_delay(seconds_or_duration_or_algorithm)
+ case seconds_or_duration_or_algorithm
+ when :exponentially_longer
+ (executions**4) + 2
+ when ActiveSupport::Duration
+ duration = seconds_or_duration_or_algorithm
+ duration.to_i
+ when Integer
+ seconds = seconds_or_duration_or_algorithm
+ seconds
+ when Proc
+ algorithm = seconds_or_duration_or_algorithm
+ algorithm.call(executions)
+ else
+ raise "Couldn't determine a delay based on #{seconds_or_duration_or_algorithm.inspect}"
+ end
+ end
+
+ def instrument(name, error: nil, wait: nil, &block)
+ payload = { job: self, adapter: self.class.queue_adapter, error: error, wait: wait }
+
+ ActiveSupport::Notifications.instrument("#{name}.active_job", payload, &block)
+ end
+ end
+end
diff --git a/activejob/lib/active_job/execution.rb b/activejob/lib/active_job/execution.rb
index 79d232da4a..e96dbcd4c9 100644
--- a/activejob/lib/active_job/execution.rb
+++ b/activejob/lib/active_job/execution.rb
@@ -1,5 +1,7 @@
-require 'active_support/rescuable'
-require 'active_job/arguments'
+# frozen_string_literal: true
+
+require "active_support/rescuable"
+require "active_job/arguments"
module ActiveJob
module Execution
@@ -17,22 +19,27 @@ module ActiveJob
end
def execute(job_data) #:nodoc:
- job = deserialize(job_data)
- job.perform_now
+ ActiveJob::Callbacks.run_callbacks(:execute) do
+ job = deserialize(job_data)
+ job.perform_now
+ end
end
end
- # Performs the job immediately. The job is not sent to the queueing adapter
+ # Performs the job immediately. The job is not sent to the queuing adapter
# but directly executed by blocking the execution of others until it's finished.
#
# MyJob.new(*args).perform_now
def perform_now
+ # Guard against jobs that were persisted before we started counting executions by zeroing out nil counters
+ self.executions = (executions || 0) + 1
+
deserialize_arguments_if_needed
run_callbacks :perform do
perform(*arguments)
end
rescue => exception
- rescue_with_handler(exception) || raise(exception)
+ rescue_with_handler(exception) || raise
end
def perform(*)
diff --git a/activejob/lib/active_job/gem_version.rb b/activejob/lib/active_job/gem_version.rb
index 27a5de93f4..770f70dc5e 100644
--- a/activejob/lib/active_job/gem_version.rb
+++ b/activejob/lib/active_job/gem_version.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveJob
# Returns the version of the currently loaded Active Job as a <tt>Gem::Version</tt>
def self.gem_version
@@ -5,7 +7,7 @@ module ActiveJob
end
module VERSION
- MAJOR = 5
+ MAJOR = 6
MINOR = 0
TINY = 0
PRE = "alpha"
diff --git a/activejob/lib/active_job/logging.rb b/activejob/lib/active_job/logging.rb
index 54774db601..416be83c24 100644
--- a/activejob/lib/active_job/logging.rb
+++ b/activejob/lib/active_job/logging.rb
@@ -1,23 +1,25 @@
-require 'active_support/core_ext/string/filters'
-require 'active_support/tagged_logging'
-require 'active_support/logger'
+# frozen_string_literal: true
+
+require "active_support/core_ext/string/filters"
+require "active_support/tagged_logging"
+require "active_support/logger"
module ActiveJob
module Logging #:nodoc:
extend ActiveSupport::Concern
included do
- cattr_accessor(:logger) { ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT)) }
+ cattr_accessor :logger, default: ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
- around_enqueue do |_, block, _|
+ around_enqueue do |_, block|
tag_logger do
block.call
end
end
- around_perform do |job, block, _|
+ around_perform do |job, block|
tag_logger(job.class.name, job.job_id) do
- payload = {adapter: job.class.queue_adapter, job: job}
+ payload = { adapter: job.class.queue_adapter, job: job }
ActiveSupport::Notifications.instrument("perform_start.active_job", payload.dup)
ActiveSupport::Notifications.instrument("perform.active_job", payload) do
block.call
@@ -25,13 +27,13 @@ module ActiveJob
end
end
- before_enqueue do |job|
+ around_enqueue do |job, block|
if job.scheduled_at
- ActiveSupport::Notifications.instrument "enqueue_at.active_job",
- adapter: job.class.queue_adapter, job: job
+ ActiveSupport::Notifications.instrument("enqueue_at.active_job",
+ adapter: job.class.queue_adapter, job: job, &block)
else
- ActiveSupport::Notifications.instrument "enqueue.active_job",
- adapter: job.class.queue_adapter, job: job
+ ActiveSupport::Notifications.instrument("enqueue.active_job",
+ adapter: job.class.queue_adapter, job: job, &block)
end
end
end
@@ -40,7 +42,7 @@ module ActiveJob
def tag_logger(*tags)
if logger.respond_to?(:tagged)
tags.unshift "ActiveJob" unless logger_tagged_by_active_job?
- ActiveJob::Base.logger.tagged(*tags){ yield }
+ logger.tagged(*tags) { yield }
else
yield
end
@@ -50,57 +52,109 @@ module ActiveJob
logger.formatter.current_tags.include?("ActiveJob")
end
- class LogSubscriber < ActiveSupport::LogSubscriber #:nodoc:
- def enqueue(event)
- info do
- job = event.payload[:job]
- "Enqueued #{job.class.name} (Job ID: #{job.job_id}) to #{queue_name(event)}" + args_info(job)
+ class LogSubscriber < ActiveSupport::LogSubscriber #:nodoc:
+ def enqueue(event)
+ info do
+ job = event.payload[:job]
+ "Enqueued #{job.class.name} (Job ID: #{job.job_id}) to #{queue_name(event)}" + args_info(job)
+ end
end
- end
- def enqueue_at(event)
- info do
- job = event.payload[:job]
- "Enqueued #{job.class.name} (Job ID: #{job.job_id}) to #{queue_name(event)} at #{scheduled_at(event)}" + args_info(job)
+ def enqueue_at(event)
+ info do
+ job = event.payload[:job]
+ "Enqueued #{job.class.name} (Job ID: #{job.job_id}) to #{queue_name(event)} at #{scheduled_at(event)}" + args_info(job)
+ end
end
- end
- def perform_start(event)
- info do
- job = event.payload[:job]
- "Performing #{job.class.name} from #{queue_name(event)}" + args_info(job)
+ def perform_start(event)
+ info do
+ job = event.payload[:job]
+ "Performing #{job.class.name} (Job ID: #{job.job_id}) from #{queue_name(event)}" + args_info(job)
+ end
end
- end
- def perform(event)
- info do
+ def perform(event)
job = event.payload[:job]
- "Performed #{job.class.name} from #{queue_name(event)} in #{event.duration.round(2)}ms"
+ ex = event.payload[:exception_object]
+ if ex
+ error do
+ "Error performing #{job.class.name} (Job ID: #{job.job_id}) from #{queue_name(event)} in #{event.duration.round(2)}ms: #{ex.class} (#{ex.message}):\n" + Array(ex.backtrace).join("\n")
+ end
+ else
+ info do
+ "Performed #{job.class.name} (Job ID: #{job.job_id}) from #{queue_name(event)} in #{event.duration.round(2)}ms"
+ end
+ end
end
- end
- private
- def queue_name(event)
- event.payload[:adapter].class.name.demodulize.remove('Adapter') + "(#{event.payload[:job].queue_name})"
+ def enqueue_retry(event)
+ job = event.payload[:job]
+ ex = event.payload[:error]
+ wait = event.payload[:wait]
+
+ info do
+ if ex
+ "Retrying #{job.class} in #{wait.to_i} seconds, due to a #{ex.class}."
+ else
+ "Retrying #{job.class} in #{wait.to_i} seconds."
+ end
+ end
end
- def args_info(job)
- if job.arguments.any?
- ' with arguments: ' +
- job.arguments.map { |arg| arg.try(:to_global_id).try(:to_s) || arg.inspect }.join(', ')
- else
- ''
+ def retry_stopped(event)
+ job = event.payload[:job]
+ ex = event.payload[:error]
+
+ error do
+ "Stopped retrying #{job.class} due to a #{ex.class}, which reoccurred on #{job.executions} attempts."
end
end
- def scheduled_at(event)
- Time.at(event.payload[:job].scheduled_at).utc
- end
+ def discard(event)
+ job = event.payload[:job]
+ ex = event.payload[:error]
- def logger
- ActiveJob::Base.logger
+ error do
+ "Discarded #{job.class} due to a #{ex.class}."
+ end
end
- end
+
+ private
+ def queue_name(event)
+ event.payload[:adapter].class.name.demodulize.remove("Adapter") + "(#{event.payload[:job].queue_name})"
+ end
+
+ def args_info(job)
+ if job.arguments.any?
+ " with arguments: " +
+ job.arguments.map { |arg| format(arg).inspect }.join(", ")
+ else
+ ""
+ end
+ end
+
+ def format(arg)
+ case arg
+ when Hash
+ arg.transform_values { |value| format(value) }
+ when Array
+ arg.map { |value| format(value) }
+ when GlobalID::Identification
+ arg.to_global_id rescue arg
+ else
+ arg
+ end
+ end
+
+ def scheduled_at(event)
+ Time.at(event.payload[:job].scheduled_at).utc
+ end
+
+ def logger
+ ActiveJob::Base.logger
+ end
+ end
end
end
diff --git a/activejob/lib/active_job/queue_adapter.rb b/activejob/lib/active_job/queue_adapter.rb
index 457015b741..954bfd1dd1 100644
--- a/activejob/lib/active_job/queue_adapter.rb
+++ b/activejob/lib/active_job/queue_adapter.rb
@@ -1,63 +1,62 @@
-require 'active_job/queue_adapters/inline_adapter'
-require 'active_support/core_ext/class/attribute'
-require 'active_support/core_ext/string/inflections'
+# frozen_string_literal: true
+
+require "active_support/core_ext/string/inflections"
module ActiveJob
# The <tt>ActiveJob::QueueAdapter</tt> module is used to load the
- # correct adapter. The default queue adapter is the +:inline+ queue.
+ # correct adapter. The default queue adapter is the +:async+ queue.
module QueueAdapter #:nodoc:
extend ActiveSupport::Concern
included do
+ class_attribute :_queue_adapter_name, instance_accessor: false, instance_predicate: false
class_attribute :_queue_adapter, instance_accessor: false, instance_predicate: false
- self.queue_adapter = :inline
+ self.queue_adapter = :async
end
# Includes the setter method for changing the active queue adapter.
module ClassMethods
# Returns the backend queue provider. The default queue adapter
- # is the +:inline+ queue. See QueueAdapters for more information.
+ # is the +:async+ queue. See QueueAdapters for more information.
def queue_adapter
_queue_adapter
end
- # Specify the backend queue provider. The default queue adapter
- # is the +:inline+ queue. See QueueAdapters for more
- # information.
- def queue_adapter=(name_or_adapter_or_class)
- self._queue_adapter = interpret_adapter(name_or_adapter_or_class)
+ # Returns string denoting the name of the configured queue adapter.
+ # By default returns +"async"+.
+ def queue_adapter_name
+ _queue_adapter_name
end
- private
-
- def interpret_adapter(name_or_adapter_or_class)
- case name_or_adapter_or_class
+ # Specify the backend queue provider. The default queue adapter
+ # is the +:async+ queue. See QueueAdapters for more
+ # information.
+ def queue_adapter=(name_or_adapter)
+ case name_or_adapter
when Symbol, String
- ActiveJob::QueueAdapters.lookup(name_or_adapter_or_class).new
+ queue_adapter = ActiveJob::QueueAdapters.lookup(name_or_adapter).new
+ assign_adapter(name_or_adapter.to_s, queue_adapter)
else
- if queue_adapter?(name_or_adapter_or_class)
- name_or_adapter_or_class
- elsif queue_adapter_class?(name_or_adapter_or_class)
- ActiveSupport::Deprecation.warn "Passing an adapter class is deprecated " \
- "and will be removed in Rails 5.1. Please pass an adapter name " \
- "(.queue_adapter = :#{name_or_adapter_or_class.name.demodulize.remove('Adapter').underscore}) " \
- "or an instance (.queue_adapter = #{name_or_adapter_or_class.name}.new) instead."
- name_or_adapter_or_class.new
+ if queue_adapter?(name_or_adapter)
+ adapter_name = "#{name_or_adapter.class.name.demodulize.remove('Adapter').underscore}"
+ assign_adapter(adapter_name, name_or_adapter)
else
raise ArgumentError
end
end
end
- QUEUE_ADAPTER_METHODS = [:enqueue, :enqueue_at].freeze
+ private
+ def assign_adapter(adapter_name, queue_adapter)
+ self._queue_adapter_name = adapter_name
+ self._queue_adapter = queue_adapter
+ end
- def queue_adapter?(object)
- QUEUE_ADAPTER_METHODS.all? { |meth| object.respond_to?(meth) }
- end
+ QUEUE_ADAPTER_METHODS = [:enqueue, :enqueue_at].freeze
- def queue_adapter_class?(object)
- object.is_a?(Class) && QUEUE_ADAPTER_METHODS.all? { |meth| object.public_method_defined?(meth) }
- end
+ def queue_adapter?(object)
+ QUEUE_ADAPTER_METHODS.all? { |meth| object.respond_to?(meth) }
+ end
end
end
end
diff --git a/activejob/lib/active_job/queue_adapters.rb b/activejob/lib/active_job/queue_adapters.rb
index 1335e3236e..525e79e302 100644
--- a/activejob/lib/active_job/queue_adapters.rb
+++ b/activejob/lib/active_job/queue_adapters.rb
@@ -1,36 +1,41 @@
+# frozen_string_literal: true
+
module ActiveJob
# == Active Job adapters
#
- # Active Job has adapters for the following queueing backends:
+ # Active Job has adapters for the following queuing backends:
#
# * {Backburner}[https://github.com/nesquena/backburner]
# * {Delayed Job}[https://github.com/collectiveidea/delayed_job]
- # * {Qu}[https://github.com/bkeepers/qu]
# * {Que}[https://github.com/chanks/que]
# * {queue_classic}[https://github.com/QueueClassic/queue_classic]
- # * {Resque 1.x}[https://github.com/resque/resque/tree/1-x-stable]
+ # * {Resque}[https://github.com/resque/resque]
# * {Sidekiq}[http://sidekiq.org]
# * {Sneakers}[https://github.com/jondot/sneakers]
# * {Sucker Punch}[https://github.com/brandonhilkert/sucker_punch]
+ # * {Active Job Async Job}[http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters/AsyncAdapter.html]
+ # * {Active Job Inline}[http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters/InlineAdapter.html]
+ # * Please Note: We are not accepting pull requests for new adapters. See the {README}[link:files/activejob/README_md.html] for more details.
#
# === Backends Features
#
- # | | Async | Queues | Delayed | Priorities | Timeout | Retries |
- # |-------------------|-------|--------|-----------|------------|---------|---------|
- # | Backburner | Yes | Yes | Yes | Yes | Job | Global |
- # | Delayed Job | Yes | Yes | Yes | Job | Global | Global |
- # | Qu | Yes | Yes | No | No | No | Global |
- # | Que | Yes | Yes | Yes | Job | No | Job |
- # | queue_classic | Yes | Yes | No* | No | No | No |
- # | Resque | Yes | Yes | Yes (Gem) | Queue | Global | Yes |
- # | Sidekiq | Yes | Yes | Yes | Queue | No | Job |
- # | Sneakers | Yes | Yes | No | Queue | Queue | No |
- # | Sucker Punch | Yes | Yes | No | No | No | No |
- # | Active Job Inline | No | Yes | N/A | N/A | N/A | N/A |
+ # | | Async | Queues | Delayed | Priorities | Timeout | Retries |
+ # |-------------------|-------|--------|------------|------------|---------|---------|
+ # | Backburner | Yes | Yes | Yes | Yes | Job | Global |
+ # | Delayed Job | Yes | Yes | Yes | Job | Global | Global |
+ # | Que | Yes | Yes | Yes | Job | No | Job |
+ # | queue_classic | Yes | Yes | Yes* | No | No | No |
+ # | Resque | Yes | Yes | Yes (Gem) | Queue | Global | Yes |
+ # | Sidekiq | Yes | Yes | Yes | Queue | No | Job |
+ # | Sneakers | Yes | Yes | No | Queue | Queue | No |
+ # | Sucker Punch | Yes | Yes | Yes | No | No | No |
+ # | Active Job Async | Yes | Yes | Yes | No | No | No |
+ # | Active Job Inline | No | Yes | N/A | N/A | N/A | N/A |
#
# ==== Async
#
- # Yes: The Queue Adapter runs the jobs in a separate or forked process.
+ # Yes: The Queue Adapter has the ability to run the job in a non-blocking manner.
+ # It either runs on a separate or forked process, or on a different thread.
#
# No: The job is run in the same process.
#
@@ -47,12 +52,11 @@ module ActiveJob
#
# No: The adapter will run jobs at the next opportunity and cannot use perform_later.
#
- # N/A: The adapter does not support queueing.
+ # N/A: The adapter does not support queuing.
#
# NOTE:
- # queue_classic does not support job scheduling.
- # However, you can use the queue_classic-later gem.
- # See the documentation for ActiveJob::QueueAdapters::QueueClassicAdapter.
+ # queue_classic supports job scheduling since version 3.1.
+ # For older versions you can use the queue_classic-later gem.
#
# ==== Priorities
#
@@ -70,7 +74,7 @@ module ActiveJob
#
# No: Does not allow the priority of jobs to be configured.
#
- # N/A: The adapter does not support queueing, and therefore sorting them.
+ # N/A: The adapter does not support queuing, and therefore sorting them.
#
# ==== Timeout
#
@@ -97,13 +101,18 @@ module ActiveJob
#
# N/A: The adapter does not run in a separate process, and therefore doesn't
# support retries.
+ #
+ # === Async and Inline Queue Adapters
+ #
+ # Active Job has two built-in queue adapters intended for development and
+ # testing: +:async+ and +:inline+.
module QueueAdapters
extend ActiveSupport::Autoload
+ autoload :AsyncAdapter
autoload :InlineAdapter
autoload :BackburnerAdapter
autoload :DelayedJobAdapter
- autoload :QuAdapter
autoload :QueAdapter
autoload :QueueClassicAdapter
autoload :ResqueAdapter
@@ -112,7 +121,7 @@ module ActiveJob
autoload :SuckerPunchAdapter
autoload :TestAdapter
- ADAPTER = 'Adapter'.freeze
+ ADAPTER = "Adapter"
private_constant :ADAPTER
class << self
diff --git a/activejob/lib/active_job/queue_adapters/async_adapter.rb b/activejob/lib/active_job/queue_adapters/async_adapter.rb
new file mode 100644
index 0000000000..53a7e3d53e
--- /dev/null
+++ b/activejob/lib/active_job/queue_adapters/async_adapter.rb
@@ -0,0 +1,116 @@
+# frozen_string_literal: true
+
+require "securerandom"
+require "concurrent/scheduled_task"
+require "concurrent/executor/thread_pool_executor"
+require "concurrent/utility/processor_counter"
+
+module ActiveJob
+ module QueueAdapters
+ # == Active Job Async adapter
+ #
+ # The Async adapter runs jobs with an in-process thread pool.
+ #
+ # This is the default queue adapter. It's well-suited for dev/test since
+ # it doesn't need an external infrastructure, but it's a poor fit for
+ # production since it drops pending jobs on restart.
+ #
+ # To use this adapter, set queue adapter to +:async+:
+ #
+ # config.active_job.queue_adapter = :async
+ #
+ # To configure the adapter's thread pool, instantiate the adapter and
+ # pass your own config:
+ #
+ # config.active_job.queue_adapter = ActiveJob::QueueAdapters::AsyncAdapter.new \
+ # min_threads: 1,
+ # max_threads: 2 * Concurrent.processor_count,
+ # idletime: 600.seconds
+ #
+ # The adapter uses a {Concurrent Ruby}[https://github.com/ruby-concurrency/concurrent-ruby] thread pool to schedule and execute
+ # jobs. Since jobs share a single thread pool, long-running jobs will block
+ # short-lived jobs. Fine for dev/test; bad for production.
+ class AsyncAdapter
+ # See {Concurrent::ThreadPoolExecutor}[https://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/ThreadPoolExecutor.html] for executor options.
+ def initialize(**executor_options)
+ @scheduler = Scheduler.new(**executor_options)
+ end
+
+ def enqueue(job) #:nodoc:
+ @scheduler.enqueue JobWrapper.new(job), queue_name: job.queue_name
+ end
+
+ def enqueue_at(job, timestamp) #:nodoc:
+ @scheduler.enqueue_at JobWrapper.new(job), timestamp, queue_name: job.queue_name
+ end
+
+ # Gracefully stop processing jobs. Finishes in-progress work and handles
+ # any new jobs following the executor's fallback policy (`caller_runs`).
+ # Waits for termination by default. Pass `wait: false` to continue.
+ def shutdown(wait: true) #:nodoc:
+ @scheduler.shutdown wait: wait
+ end
+
+ # Used for our test suite.
+ def immediate=(immediate) #:nodoc:
+ @scheduler.immediate = immediate
+ end
+
+ # Note that we don't actually need to serialize the jobs since we're
+ # performing them in-process, but we do so anyway for parity with other
+ # adapters and deployment environments. Otherwise, serialization bugs
+ # may creep in undetected.
+ class JobWrapper #:nodoc:
+ def initialize(job)
+ job.provider_job_id = SecureRandom.uuid
+ @job_data = job.serialize
+ end
+
+ def perform
+ Base.execute @job_data
+ end
+ end
+
+ class Scheduler #:nodoc:
+ DEFAULT_EXECUTOR_OPTIONS = {
+ min_threads: 0,
+ max_threads: Concurrent.processor_count,
+ auto_terminate: true,
+ idletime: 60, # 1 minute
+ max_queue: 0, # unlimited
+ fallback_policy: :caller_runs # shouldn't matter -- 0 max queue
+ }.freeze
+
+ attr_accessor :immediate
+
+ def initialize(**options)
+ self.immediate = false
+ @immediate_executor = Concurrent::ImmediateExecutor.new
+ @async_executor = Concurrent::ThreadPoolExecutor.new(DEFAULT_EXECUTOR_OPTIONS.merge(options))
+ end
+
+ def enqueue(job, queue_name:)
+ executor.post(job, &:perform)
+ end
+
+ def enqueue_at(job, timestamp, queue_name:)
+ delay = timestamp - Time.current.to_f
+ if delay > 0
+ Concurrent::ScheduledTask.execute(delay, args: [job], executor: executor, &:perform)
+ else
+ enqueue(job, queue_name: queue_name)
+ end
+ end
+
+ def shutdown(wait: true)
+ @async_executor.shutdown
+ @async_executor.wait_for_termination if wait
+ end
+
+ def executor
+ immediate ? @immediate_executor : @async_executor
+ end
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/queue_adapters/backburner_adapter.rb b/activejob/lib/active_job/queue_adapters/backburner_adapter.rb
index 17703e3e41..7dc49310ac 100644
--- a/activejob/lib/active_job/queue_adapters/backburner_adapter.rb
+++ b/activejob/lib/active_job/queue_adapters/backburner_adapter.rb
@@ -1,4 +1,6 @@
-require 'backburner'
+# frozen_string_literal: true
+
+require "backburner"
module ActiveJob
module QueueAdapters
@@ -14,12 +16,12 @@ module ActiveJob
# Rails.application.config.active_job.queue_adapter = :backburner
class BackburnerAdapter
def enqueue(job) #:nodoc:
- Backburner::Worker.enqueue JobWrapper, [ job.serialize ], queue: job.queue_name
+ Backburner::Worker.enqueue(JobWrapper, [job.serialize], queue: job.queue_name, pri: job.priority)
end
def enqueue_at(job, timestamp) #:nodoc:
delay = timestamp - Time.current.to_f
- Backburner::Worker.enqueue JobWrapper, [ job.serialize ], queue: job.queue_name, delay: delay
+ Backburner::Worker.enqueue(JobWrapper, [job.serialize], queue: job.queue_name, pri: job.priority, delay: delay)
end
class JobWrapper #:nodoc:
diff --git a/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb b/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb
index ac83da2b9c..8eeef32b99 100644
--- a/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb
+++ b/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb
@@ -1,4 +1,6 @@
-require 'delayed_job'
+# frozen_string_literal: true
+
+require "delayed_job"
module ActiveJob
module QueueAdapters
@@ -14,13 +16,13 @@ module ActiveJob
# Rails.application.config.active_job.queue_adapter = :delayed_job
class DelayedJobAdapter
def enqueue(job) #:nodoc:
- delayed_job = Delayed::Job.enqueue(JobWrapper.new(job.serialize), queue: job.queue_name)
+ delayed_job = Delayed::Job.enqueue(JobWrapper.new(job.serialize), queue: job.queue_name, priority: job.priority)
job.provider_job_id = delayed_job.id
delayed_job
end
def enqueue_at(job, timestamp) #:nodoc:
- delayed_job = Delayed::Job.enqueue(JobWrapper.new(job.serialize), queue: job.queue_name, run_at: Time.at(timestamp))
+ delayed_job = Delayed::Job.enqueue(JobWrapper.new(job.serialize), queue: job.queue_name, priority: job.priority, run_at: Time.at(timestamp))
job.provider_job_id = delayed_job.id
delayed_job
end
@@ -32,6 +34,10 @@ module ActiveJob
@job_data = job_data
end
+ def display_name
+ "#{job_data['job_class']} [#{job_data['job_id']}] from DelayedJob(#{job_data['queue_name']}) with arguments: #{job_data['arguments']}"
+ end
+
def perform
Base.execute(job_data)
end
diff --git a/activejob/lib/active_job/queue_adapters/inline_adapter.rb b/activejob/lib/active_job/queue_adapters/inline_adapter.rb
index 8ad5f4de07..ca04dc943c 100644
--- a/activejob/lib/active_job/queue_adapters/inline_adapter.rb
+++ b/activejob/lib/active_job/queue_adapters/inline_adapter.rb
@@ -1,8 +1,10 @@
+# frozen_string_literal: true
+
module ActiveJob
module QueueAdapters
# == Active Job Inline adapter
#
- # When enqueueing jobs with the Inline adapter the job will be executed
+ # When enqueuing jobs with the Inline adapter the job will be executed
# immediately.
#
# To use the Inline set the queue_adapter config to +:inline+.
@@ -14,7 +16,7 @@ module ActiveJob
end
def enqueue_at(*) #:nodoc:
- raise NotImplementedError, "Use a queueing backend to enqueue jobs in the future. Read more at http://guides.rubyonrails.org/active_job_basics.html"
+ raise NotImplementedError, "Use a queueing backend to enqueue jobs in the future. Read more at https://guides.rubyonrails.org/active_job_basics.html"
end
end
end
diff --git a/activejob/lib/active_job/queue_adapters/qu_adapter.rb b/activejob/lib/active_job/queue_adapters/qu_adapter.rb
deleted file mode 100644
index 0e198922fc..0000000000
--- a/activejob/lib/active_job/queue_adapters/qu_adapter.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-require 'qu'
-
-module ActiveJob
- module QueueAdapters
- # == Qu adapter for Active Job
- #
- # Qu is a Ruby library for queuing and processing background jobs. It is
- # heavily inspired by delayed_job and Resque. Qu was created to overcome
- # some shortcomings in the existing queuing libraries.
- # The advantages of Qu are: Multiple backends (redis, mongo), jobs are
- # requeued when worker is killed, resque-like API.
- #
- # Read more about Qu {here}[https://github.com/bkeepers/qu].
- #
- # To use Qu set the queue_adapter config to +:qu+.
- #
- # Rails.application.config.active_job.queue_adapter = :qu
- class QuAdapter
- def enqueue(job, *args) #:nodoc:
- qu_job = Qu::Payload.new(klass: JobWrapper, args: [job.serialize]).tap do |payload|
- payload.instance_variable_set(:@queue, job.queue_name)
- end.push
-
- # qu_job can be nil depending on the configured backend
- job.provider_job_id = qu_job.id unless qu_job.nil?
- qu_job
- end
-
- def enqueue_at(job, timestamp, *args) #:nodoc:
- raise NotImplementedError, "This queueing backend does not support scheduling jobs. To see what features are supported go to http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html"
- end
-
- class JobWrapper < Qu::Job #:nodoc:
- def initialize(job_data)
- @job_data = job_data
- end
-
- def perform
- Base.execute @job_data
- end
- end
- end
- end
-end
diff --git a/activejob/lib/active_job/queue_adapters/que_adapter.rb b/activejob/lib/active_job/queue_adapters/que_adapter.rb
index 90947aa98d..86b5e07743 100644
--- a/activejob/lib/active_job/queue_adapters/que_adapter.rb
+++ b/activejob/lib/active_job/queue_adapters/que_adapter.rb
@@ -1,4 +1,6 @@
-require 'que'
+# frozen_string_literal: true
+
+require "que"
module ActiveJob
module QueueAdapters
@@ -16,13 +18,13 @@ module ActiveJob
# Rails.application.config.active_job.queue_adapter = :que
class QueAdapter
def enqueue(job) #:nodoc:
- que_job = JobWrapper.enqueue job.serialize
+ que_job = JobWrapper.enqueue job.serialize, priority: job.priority
job.provider_job_id = que_job.attrs["job_id"]
que_job
end
def enqueue_at(job, timestamp) #:nodoc:
- que_job = JobWrapper.enqueue job.serialize, run_at: Time.at(timestamp)
+ que_job = JobWrapper.enqueue job.serialize, priority: job.priority, run_at: Time.at(timestamp)
job.provider_job_id = que_job.attrs["job_id"]
que_job
end
diff --git a/activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb b/activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb
index 059754a87f..ccc1881091 100644
--- a/activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb
+++ b/activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb
@@ -1,4 +1,6 @@
-require 'queue_classic'
+# frozen_string_literal: true
+
+require "queue_classic"
module ActiveJob
module QueueAdapters
@@ -18,17 +20,21 @@ module ActiveJob
# Rails.application.config.active_job.queue_adapter = :queue_classic
class QueueClassicAdapter
def enqueue(job) #:nodoc:
- build_queue(job.queue_name).enqueue("#{JobWrapper.name}.perform", job.serialize)
+ qc_job = build_queue(job.queue_name).enqueue("#{JobWrapper.name}.perform", job.serialize)
+ job.provider_job_id = qc_job["id"] if qc_job.is_a?(Hash)
+ qc_job
end
def enqueue_at(job, timestamp) #:nodoc:
queue = build_queue(job.queue_name)
unless queue.respond_to?(:enqueue_at)
- raise NotImplementedError, 'To be able to schedule jobs with queue_classic ' \
- 'the QC::Queue needs to respond to `enqueue_at(timestamp, method, *args)`. ' \
- 'You can implement this yourself or you can use the queue_classic-later gem.'
+ raise NotImplementedError, "To be able to schedule jobs with queue_classic " \
+ "the QC::Queue needs to respond to `enqueue_at(timestamp, method, *args)`. " \
+ "You can implement this yourself or you can use the queue_classic-later gem."
end
- queue.enqueue_at(timestamp, "#{JobWrapper.name}.perform", job.serialize)
+ qc_job = queue.enqueue_at(timestamp, "#{JobWrapper.name}.perform", job.serialize)
+ job.provider_job_id = qc_job["id"] if qc_job.is_a?(Hash)
+ qc_job
end
# Builds a <tt>QC::Queue</tt> object to schedule jobs on.
diff --git a/activejob/lib/active_job/queue_adapters/resque_adapter.rb b/activejob/lib/active_job/queue_adapters/resque_adapter.rb
index 417854afd8..590b4ee98d 100644
--- a/activejob/lib/active_job/queue_adapters/resque_adapter.rb
+++ b/activejob/lib/active_job/queue_adapters/resque_adapter.rb
@@ -1,12 +1,14 @@
-require 'resque'
-require 'active_support/core_ext/enumerable'
-require 'active_support/core_ext/array/access'
+# frozen_string_literal: true
+
+require "resque"
+require "active_support/core_ext/enumerable"
+require "active_support/core_ext/array/access"
begin
- require 'resque-scheduler'
+ require "resque-scheduler"
rescue LoadError
begin
- require 'resque_scheduler'
+ require "resque_scheduler"
rescue LoadError
false
end
@@ -27,6 +29,7 @@ module ActiveJob
# Rails.application.config.active_job.queue_adapter = :resque
class ResqueAdapter
def enqueue(job) #:nodoc:
+ JobWrapper.instance_variable_set(:@queue, job.queue_name)
Resque.enqueue_to job.queue_name, JobWrapper, job.serialize
end
diff --git a/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb b/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb
index c321776bf5..f726e6ad93 100644
--- a/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb
+++ b/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb
@@ -1,4 +1,6 @@
-require 'sidekiq'
+# frozen_string_literal: true
+
+require "sidekiq"
module ActiveJob
module QueueAdapters
@@ -16,28 +18,28 @@ module ActiveJob
# Rails.application.config.active_job.queue_adapter = :sidekiq
class SidekiqAdapter
def enqueue(job) #:nodoc:
- #Sidekiq::Client does not support symbols as keys
+ # Sidekiq::Client does not support symbols as keys
job.provider_job_id = Sidekiq::Client.push \
- 'class' => JobWrapper,
- 'wrapped' => job.class.to_s,
- 'queue' => job.queue_name,
- 'args' => [ job.serialize ]
+ "class" => JobWrapper,
+ "wrapped" => job.class.to_s,
+ "queue" => job.queue_name,
+ "args" => [ job.serialize ]
end
def enqueue_at(job, timestamp) #:nodoc:
job.provider_job_id = Sidekiq::Client.push \
- 'class' => JobWrapper,
- 'wrapped' => job.class.to_s,
- 'queue' => job.queue_name,
- 'args' => [ job.serialize ],
- 'at' => timestamp
+ "class" => JobWrapper,
+ "wrapped" => job.class.to_s,
+ "queue" => job.queue_name,
+ "args" => [ job.serialize ],
+ "at" => timestamp
end
class JobWrapper #:nodoc:
include Sidekiq::Worker
def perform(job_data)
- Base.execute job_data
+ Base.execute job_data.merge("provider_job_id" => jid)
end
end
end
diff --git a/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb b/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb
index f102c6567e..de98a950d0 100644
--- a/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb
+++ b/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb
@@ -1,5 +1,7 @@
-require 'sneakers'
-require 'thread'
+# frozen_string_literal: true
+
+require "sneakers"
+require "monitor"
module ActiveJob
module QueueAdapters
@@ -33,7 +35,7 @@ module ActiveJob
class JobWrapper #:nodoc:
include Sneakers::Worker
- from_queue 'default'
+ from_queue "default"
def work(msg)
job_data = ActiveSupport::JSON.decode(msg)
diff --git a/activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb b/activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb
index c6c35f8ab4..d09e1e9143 100644
--- a/activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb
+++ b/activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb
@@ -1,16 +1,16 @@
-require 'sucker_punch'
+# frozen_string_literal: true
+
+require "sucker_punch"
module ActiveJob
module QueueAdapters
# == Sucker Punch adapter for Active Job
#
# Sucker Punch is a single-process Ruby asynchronous processing library.
- # It's girl_friday and DSL sugar on top of Celluloid. With Celluloid's
- # actor pattern, we can do asynchronous processing within a single process.
- # This reduces costs of hosting on a service like Heroku along with the
- # memory footprint of having to maintain additional jobs if hosting on
- # a dedicated server. All queues can run within a single Rails/Sinatra
- # process.
+ # This reduces the cost of hosting on a service like Heroku along
+ # with the memory footprint of having to maintain additional jobs if
+ # hosting on a dedicated server. All queues can run within a
+ # single application (eg. Rails, Sinatra, etc.) process.
#
# Read more about Sucker Punch {here}[https://github.com/brandonhilkert/sucker_punch].
#
@@ -19,11 +19,22 @@ module ActiveJob
# Rails.application.config.active_job.queue_adapter = :sucker_punch
class SuckerPunchAdapter
def enqueue(job) #:nodoc:
- JobWrapper.new.async.perform job.serialize
+ if JobWrapper.respond_to?(:perform_async)
+ # sucker_punch 2.0 API
+ JobWrapper.perform_async job.serialize
+ else
+ # sucker_punch 1.0 API
+ JobWrapper.new.async.perform job.serialize
+ end
end
def enqueue_at(job, timestamp) #:nodoc:
- raise NotImplementedError, "This queueing backend does not support scheduling jobs. To see what features are supported go to http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html"
+ if JobWrapper.respond_to?(:perform_in)
+ delay = timestamp - Time.current.to_f
+ JobWrapper.perform_in delay, job.serialize
+ else
+ raise NotImplementedError, "sucker_punch 1.0 does not support `enqueued_at`. Please upgrade to version ~> 2.0.0 to enable this behavior."
+ end
end
class JobWrapper #:nodoc:
diff --git a/activejob/lib/active_job/queue_adapters/test_adapter.rb b/activejob/lib/active_job/queue_adapters/test_adapter.rb
index 9b7b7139f4..f73ad444ba 100644
--- a/activejob/lib/active_job/queue_adapters/test_adapter.rb
+++ b/activejob/lib/active_job/queue_adapters/test_adapter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveJob
module QueueAdapters
# == Test adapter for Active Job
@@ -10,7 +12,7 @@ module ActiveJob
#
# Rails.application.config.active_job.queue_adapter = :test
class TestAdapter
- attr_accessor(:perform_enqueued_jobs, :perform_enqueued_at_jobs, :filter)
+ attr_accessor(:perform_enqueued_jobs, :perform_enqueued_at_jobs, :filter, :reject, :queue)
attr_writer(:enqueued_jobs, :performed_jobs)
# Provides a store of all the enqueued jobs with the TestAdapter so you can check them.
@@ -27,34 +29,47 @@ module ActiveJob
return if filtered?(job)
job_data = job_to_hash(job)
- enqueue_or_perform(perform_enqueued_jobs, job, job_data)
+ perform_or_enqueue(perform_enqueued_jobs, job, job_data)
end
def enqueue_at(job, timestamp) #:nodoc:
return if filtered?(job)
job_data = job_to_hash(job, at: timestamp)
- enqueue_or_perform(perform_enqueued_at_jobs, job, job_data)
+ perform_or_enqueue(perform_enqueued_at_jobs, job, job_data)
end
private
+ def job_to_hash(job, extras = {})
+ { job: job.class, args: job.serialize.fetch("arguments"), queue: job.queue_name }.merge!(extras)
+ end
- def job_to_hash(job, extras = {})
- { job: job.class, args: job.serialize.fetch('arguments'), queue: job.queue_name }.merge!(extras)
- end
+ def perform_or_enqueue(perform, job, job_data)
+ if perform
+ performed_jobs << job_data
+ Base.execute job.serialize
+ else
+ enqueued_jobs << job_data
+ end
+ end
- def enqueue_or_perform(perform, job, job_data)
- if perform
- performed_jobs << job_data
- Base.execute job.serialize
- else
- enqueued_jobs << job_data
+ def filtered?(job)
+ filtered_queue?(job) || filtered_job_class?(job)
end
- end
- def filtered?(job)
- filter && !Array(filter).include?(job.class)
- end
+ def filtered_queue?(job)
+ if queue
+ job.queue_name != queue.to_s
+ end
+ end
+
+ def filtered_job_class?(job)
+ if filter
+ !Array(filter).include?(job.class)
+ elsif reject
+ Array(reject).include?(job.class)
+ end
+ end
end
end
end
diff --git a/activejob/lib/active_job/queue_name.rb b/activejob/lib/active_job/queue_name.rb
index 65786a49ff..9dc6bc7f2e 100644
--- a/activejob/lib/active_job/queue_name.rb
+++ b/activejob/lib/active_job/queue_name.rb
@@ -1,11 +1,13 @@
+# frozen_string_literal: true
+
module ActiveJob
module QueueName
extend ActiveSupport::Concern
# Includes the ability to override the default queue name and prefix.
module ClassMethods
- mattr_accessor(:queue_name_prefix)
- mattr_accessor(:default_queue_name) { "default" }
+ mattr_accessor :queue_name_prefix
+ mattr_accessor :default_queue_name, default: "default"
# Specifies the name of the queue to process the job on.
#
@@ -16,7 +18,7 @@ module ActiveJob
# post.to_feed!
# end
# end
- def queue_as(part_name=nil, &block)
+ def queue_as(part_name = nil, &block)
if block_given?
self.queue_name = block
else
@@ -32,11 +34,8 @@ module ActiveJob
end
included do
- class_attribute :queue_name, instance_accessor: false
- class_attribute :queue_name_delimiter, instance_accessor: false
-
- self.queue_name = default_queue_name
- self.queue_name_delimiter = '_' # set default delimiter to '_'
+ class_attribute :queue_name, instance_accessor: false, default: default_queue_name
+ class_attribute :queue_name_delimiter, instance_accessor: false, default: "_"
end
# Returns the name of the queue the job will be run on.
@@ -46,6 +45,5 @@ module ActiveJob
end
@queue_name
end
-
end
end
diff --git a/activejob/lib/active_job/queue_priority.rb b/activejob/lib/active_job/queue_priority.rb
new file mode 100644
index 0000000000..063bccdb01
--- /dev/null
+++ b/activejob/lib/active_job/queue_priority.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module ActiveJob
+ module QueuePriority
+ extend ActiveSupport::Concern
+
+ # Includes the ability to override the default queue priority.
+ module ClassMethods
+ mattr_accessor :default_priority
+
+ # Specifies the priority of the queue to create the job with.
+ #
+ # class PublishToFeedJob < ActiveJob::Base
+ # queue_with_priority 50
+ #
+ # def perform(post)
+ # post.to_feed!
+ # end
+ # end
+ #
+ # Specify either an argument or a block.
+ def queue_with_priority(priority = nil, &block)
+ if block_given?
+ self.priority = block
+ else
+ self.priority = priority
+ end
+ end
+ end
+
+ included do
+ class_attribute :priority, instance_accessor: false, default: default_priority
+ end
+
+ # Returns the priority that the job will be created with
+ def priority
+ if @priority.is_a?(Proc)
+ @priority = instance_exec(&@priority)
+ end
+ @priority
+ end
+ end
+end
diff --git a/activejob/lib/active_job/railtie.rb b/activejob/lib/active_job/railtie.rb
index 6538ac1b30..ecc0908d5f 100644
--- a/activejob/lib/active_job/railtie.rb
+++ b/activejob/lib/active_job/railtie.rb
@@ -1,23 +1,49 @@
-require 'global_id/railtie'
-require 'active_job'
+# frozen_string_literal: true
+
+require "global_id/railtie"
+require "active_job"
module ActiveJob
# = Active Job Railtie
class Railtie < Rails::Railtie # :nodoc:
config.active_job = ActiveSupport::OrderedOptions.new
+ config.active_job.custom_serializers = []
- initializer 'active_job.logger' do
+ initializer "active_job.logger" do
ActiveSupport.on_load(:active_job) { self.logger = ::Rails.logger }
end
+ initializer "active_job.custom_serializers" do |app|
+ config.after_initialize do
+ custom_serializers = app.config.active_job.delete(:custom_serializers)
+ ActiveJob::Serializers.add_serializers custom_serializers
+ end
+ end
+
initializer "active_job.set_configs" do |app|
options = app.config.active_job
- options.queue_adapter ||= :inline
+ options.queue_adapter ||= :async
ActiveSupport.on_load(:active_job) do
- options.each { |k,v| send("#{k}=", v) }
+ options.each do |k, v|
+ k = "#{k}="
+ send(k, v) if respond_to? k
+ end
+ end
+
+ ActiveSupport.on_load(:action_dispatch_integration_test) do
+ include ActiveJob::TestHelper
end
end
+ initializer "active_job.set_reloader_hook" do |app|
+ ActiveSupport.on_load(:active_job) do
+ ActiveJob::Callbacks.singleton_class.set_callback(:execute, :around, prepend: true) do |_, inner|
+ app.reloader.wrap do
+ inner.call
+ end
+ end
+ end
+ end
end
end
diff --git a/activejob/lib/active_job/serializers.rb b/activejob/lib/active_job/serializers.rb
new file mode 100644
index 0000000000..a5d90f48b8
--- /dev/null
+++ b/activejob/lib/active_job/serializers.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require "set"
+
+module ActiveJob
+ # The <tt>ActiveJob::Serializers</tt> module is used to store a list of known serializers
+ # and to add new ones. It also has helpers to serialize/deserialize objects.
+ module Serializers # :nodoc:
+ extend ActiveSupport::Autoload
+
+ autoload :ObjectSerializer
+ autoload :SymbolSerializer
+ autoload :DurationSerializer
+ autoload :DateTimeSerializer
+ autoload :DateSerializer
+ autoload :TimeWithZoneSerializer
+ autoload :TimeSerializer
+
+ mattr_accessor :_additional_serializers
+ self._additional_serializers = Set.new
+
+ class << self
+ # Returns serialized representative of the passed object.
+ # Will look up through all known serializers.
+ # Raises <tt>ActiveJob::SerializationError</tt> if it can't find a proper serializer.
+ def serialize(argument)
+ serializer = serializers.detect { |s| s.serialize?(argument) }
+ raise SerializationError.new("Unsupported argument type: #{argument.class.name}") unless serializer
+ serializer.serialize(argument)
+ end
+
+ # Returns deserialized object.
+ # Will look up through all known serializers.
+ # If no serializer found will raise <tt>ArgumentError</tt>.
+ def deserialize(argument)
+ serializer_name = argument[Arguments::OBJECT_SERIALIZER_KEY]
+ raise ArgumentError, "Serializer name is not present in the argument: #{argument.inspect}" unless serializer_name
+
+ serializer = serializer_name.safe_constantize
+ raise ArgumentError, "Serializer #{serializer_name} is not known" unless serializer
+
+ serializer.deserialize(argument)
+ end
+
+ # Returns list of known serializers.
+ def serializers
+ self._additional_serializers
+ end
+
+ # Adds new serializers to a list of known serializers.
+ def add_serializers(*new_serializers)
+ self._additional_serializers += new_serializers.flatten
+ end
+ end
+
+ add_serializers SymbolSerializer,
+ DurationSerializer,
+ DateTimeSerializer,
+ DateSerializer,
+ TimeWithZoneSerializer,
+ TimeSerializer
+ end
+end
diff --git a/activejob/lib/active_job/serializers/date_serializer.rb b/activejob/lib/active_job/serializers/date_serializer.rb
new file mode 100644
index 0000000000..e995d30faa
--- /dev/null
+++ b/activejob/lib/active_job/serializers/date_serializer.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module ActiveJob
+ module Serializers
+ class DateSerializer < ObjectSerializer # :nodoc:
+ def serialize(date)
+ super("value" => date.iso8601)
+ end
+
+ def deserialize(hash)
+ Date.iso8601(hash["value"])
+ end
+
+ private
+
+ def klass
+ Date
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/serializers/date_time_serializer.rb b/activejob/lib/active_job/serializers/date_time_serializer.rb
new file mode 100644
index 0000000000..fe780a1978
--- /dev/null
+++ b/activejob/lib/active_job/serializers/date_time_serializer.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module ActiveJob
+ module Serializers
+ class DateTimeSerializer < ObjectSerializer # :nodoc:
+ def serialize(time)
+ super("value" => time.iso8601)
+ end
+
+ def deserialize(hash)
+ DateTime.iso8601(hash["value"])
+ end
+
+ private
+
+ def klass
+ DateTime
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/serializers/duration_serializer.rb b/activejob/lib/active_job/serializers/duration_serializer.rb
new file mode 100644
index 0000000000..715fe27a5c
--- /dev/null
+++ b/activejob/lib/active_job/serializers/duration_serializer.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module ActiveJob
+ module Serializers
+ class DurationSerializer < ObjectSerializer # :nodoc:
+ def serialize(duration)
+ super("value" => duration.value, "parts" => Arguments.serialize(duration.parts))
+ end
+
+ def deserialize(hash)
+ value = hash["value"]
+ parts = Arguments.deserialize(hash["parts"])
+
+ klass.new(value, parts)
+ end
+
+ private
+
+ def klass
+ ActiveSupport::Duration
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/serializers/object_serializer.rb b/activejob/lib/active_job/serializers/object_serializer.rb
new file mode 100644
index 0000000000..6d280969be
--- /dev/null
+++ b/activejob/lib/active_job/serializers/object_serializer.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module ActiveJob
+ module Serializers
+ # Base class for serializing and deserializing custom objects.
+ #
+ # Example:
+ #
+ # class MoneySerializer < ActiveJob::Serializers::ObjectSerializer
+ # def serialize(money)
+ # super("amount" => money.amount, "currency" => money.currency)
+ # end
+ #
+ # def deserialize(hash)
+ # Money.new(hash["amount"], hash["currency"])
+ # end
+ #
+ # private
+ #
+ # def klass
+ # Money
+ # end
+ # end
+ class ObjectSerializer
+ include Singleton
+
+ class << self
+ delegate :serialize?, :serialize, :deserialize, to: :instance
+ end
+
+ # Determines if an argument should be serialized by a serializer.
+ def serialize?(argument)
+ argument.is_a?(klass)
+ end
+
+ # Serializes an argument to a JSON primitive type.
+ def serialize(hash)
+ { Arguments::OBJECT_SERIALIZER_KEY => self.class.name }.merge!(hash)
+ end
+
+ # Deserializes an argument from a JSON primitive type.
+ def deserialize(_argument)
+ raise NotImplementedError
+ end
+
+ private
+
+ # The class of the object that will be serialized.
+ def klass # :doc:
+ raise NotImplementedError
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/serializers/symbol_serializer.rb b/activejob/lib/active_job/serializers/symbol_serializer.rb
new file mode 100644
index 0000000000..7e1f9553a2
--- /dev/null
+++ b/activejob/lib/active_job/serializers/symbol_serializer.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module ActiveJob
+ module Serializers
+ class SymbolSerializer < ObjectSerializer # :nodoc:
+ def serialize(argument)
+ super("value" => argument.to_s)
+ end
+
+ def deserialize(argument)
+ argument["value"].to_sym
+ end
+
+ private
+
+ def klass
+ Symbol
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/serializers/time_serializer.rb b/activejob/lib/active_job/serializers/time_serializer.rb
new file mode 100644
index 0000000000..fe20772f35
--- /dev/null
+++ b/activejob/lib/active_job/serializers/time_serializer.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module ActiveJob
+ module Serializers
+ class TimeSerializer < ObjectSerializer # :nodoc:
+ def serialize(time)
+ super("value" => time.iso8601)
+ end
+
+ def deserialize(hash)
+ Time.iso8601(hash["value"])
+ end
+
+ private
+
+ def klass
+ Time
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/serializers/time_with_zone_serializer.rb b/activejob/lib/active_job/serializers/time_with_zone_serializer.rb
new file mode 100644
index 0000000000..43017fc75b
--- /dev/null
+++ b/activejob/lib/active_job/serializers/time_with_zone_serializer.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module ActiveJob
+ module Serializers
+ class TimeWithZoneSerializer < ObjectSerializer # :nodoc:
+ def serialize(time)
+ super("value" => time.iso8601)
+ end
+
+ def deserialize(hash)
+ Time.iso8601(hash["value"]).in_time_zone
+ end
+
+ private
+
+ def klass
+ ActiveSupport::TimeWithZone
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/test_case.rb b/activejob/lib/active_job/test_case.rb
index d894a7b5cd..49cd51bdd0 100644
--- a/activejob/lib/active_job/test_case.rb
+++ b/activejob/lib/active_job/test_case.rb
@@ -1,7 +1,11 @@
-require 'active_support/test_case'
+# frozen_string_literal: true
+
+require "active_support/test_case"
module ActiveJob
class TestCase < ActiveSupport::TestCase
include ActiveJob::TestHelper
+
+ ActiveSupport.run_load_hooks(:active_job_test_case, self)
end
end
diff --git a/activejob/lib/active_job/test_helper.rb b/activejob/lib/active_job/test_helper.rb
index 9b307e8dc8..0deb68d0d2 100644
--- a/activejob/lib/active_job/test_helper.rb
+++ b/activejob/lib/active_job/test_helper.rb
@@ -1,316 +1,633 @@
-require 'active_support/core_ext/class/subclasses'
-require 'active_support/core_ext/hash/keys'
+# frozen_string_literal: true
+
+require "active_support/core_ext/class/subclasses"
+require "active_support/core_ext/hash/keys"
module ActiveJob
# Provides helper methods for testing Active Job
module TestHelper
- extend ActiveSupport::Concern
-
- included do
- def before_setup
- test_adapter = ActiveJob::QueueAdapters::TestAdapter.new
+ delegate :enqueued_jobs, :enqueued_jobs=,
+ :performed_jobs, :performed_jobs=,
+ to: :queue_adapter
- @old_queue_adapters = (ActiveJob::Base.subclasses << ActiveJob::Base).select do |klass|
- # only override explicitly set adapters, a quirk of `class_attribute`
- klass.singleton_class.public_instance_methods(false).include?(:_queue_adapter)
- end.map do |klass|
- [klass, klass.queue_adapter].tap do
- klass.queue_adapter = test_adapter
- end
- end
+ module TestQueueAdapter
+ extend ActiveSupport::Concern
- clear_enqueued_jobs
- clear_performed_jobs
- super
+ included do
+ class_attribute :_test_adapter, instance_accessor: false, instance_predicate: false
end
- def after_teardown
- super
- @old_queue_adapters.each do |(klass, adapter)|
- klass.queue_adapter = adapter
+ module ClassMethods
+ def queue_adapter
+ self._test_adapter.nil? ? super : self._test_adapter
+ end
+
+ def disable_test_adapter
+ self._test_adapter = nil
end
- end
- # Asserts that the number of enqueued jobs matches the given number.
- #
- # def test_jobs
- # assert_enqueued_jobs 0
- # HelloJob.perform_later('david')
- # assert_enqueued_jobs 1
- # HelloJob.perform_later('abdelkader')
- # assert_enqueued_jobs 2
- # end
- #
- # If a block is passed, that block should cause the specified number of
- # jobs to be enqueued.
- #
- # def test_jobs_again
- # assert_enqueued_jobs 1 do
- # HelloJob.perform_later('cristian')
- # end
- #
- # assert_enqueued_jobs 2 do
- # HelloJob.perform_later('aaron')
- # HelloJob.perform_later('rafael')
- # end
- # end
- #
- # The number of times a specific job is enqueued can be asserted.
- #
- # def test_logging_job
- # assert_enqueued_jobs 2, only: LoggingJob do
- # LoggingJob.perform_later
- # HelloJob.perform_later('jeremy')
- # end
- # end
- def assert_enqueued_jobs(number, only: nil)
- if block_given?
- original_count = enqueued_jobs_size(only: only)
- yield
- new_count = enqueued_jobs_size(only: only)
- assert_equal number, new_count - original_count, "#{number} jobs expected, but #{new_count - original_count} were enqueued"
- else
- actual_count = enqueued_jobs_size(only: only)
- assert_equal number, actual_count, "#{number} jobs expected, but #{actual_count} were enqueued"
+ 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
- # Asserts that no jobs have been enqueued.
- #
- # def test_jobs
- # assert_no_enqueued_jobs
- # HelloJob.perform_later('jeremy')
- # assert_enqueued_jobs 1
- # end
- #
- # If a block is passed, that block should not cause any job to be enqueued.
- #
- # def test_jobs_again
- # assert_no_enqueued_jobs do
- # # No job should be enqueued from this block
- # end
- # end
- #
- # It can be asserted that no jobs of a specific kind are enqueued:
- #
- # def test_no_logging
- # assert_no_enqueued_jobs only: LoggingJob do
- # HelloJob.perform_later('jeremy')
- # end
- # end
- #
- # Note: This assertion is simply a shortcut for:
- #
- # assert_enqueued_jobs 0, &block
- def assert_no_enqueued_jobs(only: nil, &block)
- assert_enqueued_jobs 0, only: only, &block
+ queue_adapter_changed_jobs.each do |klass|
+ klass.enable_test_adapter(test_adapter)
end
- # Asserts that the number of performed jobs matches the given number.
- # If no block is passed, <tt>perform_enqueued_jobs</tt>
- # must be called around the job call.
- #
- # def test_jobs
- # assert_performed_jobs 0
- #
- # perform_enqueued_jobs do
- # HelloJob.perform_later('xavier')
- # end
- # assert_performed_jobs 1
- #
- # perform_enqueued_jobs do
- # HelloJob.perform_later('yves')
- # assert_performed_jobs 2
- # end
- # end
- #
- # If a block is passed, that block should cause the specified number of
- # jobs to be performed.
- #
- # def test_jobs_again
- # assert_performed_jobs 1 do
- # HelloJob.perform_later('robin')
- # end
- #
- # assert_performed_jobs 2 do
- # HelloJob.perform_later('carlos')
- # HelloJob.perform_later('sean')
- # end
- # end
- #
- # The block form supports filtering. If the :only option is specified,
- # then only the listed job(s) will be performed.
- #
- # def test_hello_job
- # assert_performed_jobs 1, only: HelloJob do
- # HelloJob.perform_later('jeremy')
- # LoggingJob.perform_later
- # end
- # end
- #
- # An array may also be specified, to support testing multiple jobs.
- #
- # def test_hello_and_logging_jobs
- # assert_nothing_raised do
- # assert_performed_jobs 2, only: [HelloJob, LoggingJob] do
- # HelloJob.perform_later('jeremy')
- # LoggingJob.perform_later('stewie')
- # RescueJob.perform_later('david')
- # end
- # end
- # end
- def assert_performed_jobs(number, only: nil)
- if block_given?
- original_count = performed_jobs.size
- perform_enqueued_jobs(only: only) { yield }
- new_count = performed_jobs.size
- assert_equal number, new_count - original_count,
- "#{number} jobs expected, but #{new_count - original_count} were performed"
- else
- performed_jobs_size = performed_jobs.size
- assert_equal number, performed_jobs_size, "#{number} jobs expected, but #{performed_jobs_size} were performed"
- end
+ clear_enqueued_jobs
+ clear_performed_jobs
+ super
+ end
+
+ def after_teardown # :nodoc:
+ super
+
+ queue_adapter_changed_jobs.each { |klass| klass.disable_test_adapter }
+ end
+
+ # Specifies the queue adapter to use with all Active Job test helpers.
+ #
+ # Returns an instance of the queue adapter and defaults to
+ # <tt>ActiveJob::QueueAdapters::TestAdapter</tt>.
+ #
+ # Note: The adapter provided by this method must provide some additional
+ # methods from those expected of a standard <tt>ActiveJob::QueueAdapter</tt>
+ # in order to be used with the active job test helpers. Refer to
+ # <tt>ActiveJob::QueueAdapters::TestAdapter</tt>.
+ def queue_adapter_for_test
+ ActiveJob::QueueAdapters::TestAdapter.new
+ end
+
+ # Asserts that the number of enqueued jobs matches the given number.
+ #
+ # def test_jobs
+ # assert_enqueued_jobs 0
+ # HelloJob.perform_later('david')
+ # assert_enqueued_jobs 1
+ # HelloJob.perform_later('abdelkader')
+ # assert_enqueued_jobs 2
+ # end
+ #
+ # If a block is passed, asserts that the block will cause the specified number of
+ # jobs to be enqueued.
+ #
+ # def test_jobs_again
+ # assert_enqueued_jobs 1 do
+ # HelloJob.perform_later('cristian')
+ # end
+ #
+ # assert_enqueued_jobs 2 do
+ # HelloJob.perform_later('aaron')
+ # HelloJob.perform_later('rafael')
+ # end
+ # end
+ #
+ # Asserts the number of times a specific job was enqueued by passing +:only+ option.
+ #
+ # def test_logging_job
+ # assert_enqueued_jobs 1, only: LoggingJob do
+ # LoggingJob.perform_later
+ # HelloJob.perform_later('jeremy')
+ # end
+ # end
+ #
+ # Asserts the number of times a job except specific class was enqueued by passing +:except+ option.
+ #
+ # def test_logging_job
+ # assert_enqueued_jobs 1, except: HelloJob do
+ # LoggingJob.perform_later
+ # HelloJob.perform_later('jeremy')
+ # end
+ # end
+ #
+ # Asserts the number of times a job is enqueued to a specific queue by passing +:queue+ option.
+ #
+ # 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, except: nil, queue: nil)
+ if block_given?
+ original_count = enqueued_jobs_with(only: only, except: except, queue: queue)
+
+ yield
+
+ new_count = enqueued_jobs_with(only: only, except: except, queue: queue)
+
+ actual_count = new_count - original_count
+ else
+ actual_count = enqueued_jobs_with(only: only, except: except, queue: queue)
end
- # Asserts that no jobs have been performed.
- #
- # def test_jobs
- # assert_no_performed_jobs
- #
- # perform_enqueued_jobs do
- # HelloJob.perform_later('matthew')
- # assert_performed_jobs 1
- # end
- # end
- #
- # If a block is passed, that block should not cause any job to be performed.
- #
- # def test_jobs_again
- # assert_no_performed_jobs do
- # # No job should be performed from this block
- # end
- # end
- #
- # The block form supports filtering. If the :only option is specified,
- # then only the listed job(s) will be performed.
- #
- # def test_hello_job
- # assert_performed_jobs 1, only: HelloJob do
- # HelloJob.perform_later('jeremy')
- # LoggingJob.perform_later
- # end
- # end
- #
- # An array may also be specified, to support testing multiple jobs.
- #
- # def test_hello_and_logging_jobs
- # assert_nothing_raised do
- # assert_performed_jobs 2, only: [HelloJob, LoggingJob] do
- # HelloJob.perform_later('jeremy')
- # LoggingJob.perform_later('stewie')
- # RescueJob.perform_later('david')
- # end
- # end
- # end
- #
- # Note: This assertion is simply a shortcut for:
- #
- # assert_performed_jobs 0, &block
- def assert_no_performed_jobs(only: nil, &block)
- assert_performed_jobs 0, only: only, &block
+ assert_equal number, actual_count, "#{number} jobs expected, but #{actual_count} were enqueued"
+ end
+
+ # Asserts that no jobs have been enqueued.
+ #
+ # def test_jobs
+ # assert_no_enqueued_jobs
+ # HelloJob.perform_later('jeremy')
+ # assert_enqueued_jobs 1
+ # end
+ #
+ # If a block is passed, asserts that the block will not cause any job to be enqueued.
+ #
+ # def test_jobs_again
+ # assert_no_enqueued_jobs do
+ # # No job should be enqueued from this block
+ # end
+ # end
+ #
+ # Asserts that no jobs of a specific kind are enqueued by passing +:only+ option.
+ #
+ # def test_no_logging
+ # assert_no_enqueued_jobs only: LoggingJob do
+ # HelloJob.perform_later('jeremy')
+ # end
+ # end
+ #
+ # Asserts that no jobs except specific class are enqueued by passing +:except+ option.
+ #
+ # def test_no_logging
+ # assert_no_enqueued_jobs except: HelloJob do
+ # HelloJob.perform_later('jeremy')
+ # end
+ # end
+ #
+ # Asserts that no jobs are enqueued to a specific queue by passing +:queue+ option
+ #
+ # def test_no_logging
+ # assert_no_enqueued_jobs queue: 'default' do
+ # LoggingJob.set(queue: :some_queue).perform_later
+ # end
+ # end
+ #
+ # Note: This assertion is simply a shortcut for:
+ #
+ # assert_enqueued_jobs 0, &block
+ def assert_no_enqueued_jobs(only: nil, except: nil, queue: nil, &block)
+ assert_enqueued_jobs 0, only: only, except: except, queue: queue, &block
+ end
+
+ # Asserts that the number of performed jobs matches the given number.
+ # If no block is passed, <tt>perform_enqueued_jobs</tt>
+ # must be called around or after the job call.
+ #
+ # def test_jobs
+ # assert_performed_jobs 0
+ #
+ # perform_enqueued_jobs do
+ # HelloJob.perform_later('xavier')
+ # end
+ # assert_performed_jobs 1
+ #
+ # HelloJob.perform_later('yves')
+ #
+ # perform_enqueued_jobs
+ #
+ # assert_performed_jobs 2
+ # end
+ #
+ # If a block is passed, asserts that the block will cause the specified number of
+ # jobs to be performed.
+ #
+ # def test_jobs_again
+ # assert_performed_jobs 1 do
+ # HelloJob.perform_later('robin')
+ # end
+ #
+ # assert_performed_jobs 2 do
+ # HelloJob.perform_later('carlos')
+ # HelloJob.perform_later('sean')
+ # end
+ # end
+ #
+ # This method also supports filtering. If the +:only+ option is specified,
+ # then only the listed job(s) will be performed.
+ #
+ # def test_hello_job
+ # assert_performed_jobs 1, only: HelloJob do
+ # HelloJob.perform_later('jeremy')
+ # LoggingJob.perform_later
+ # end
+ # end
+ #
+ # Also if the +:except+ option is specified,
+ # then the job(s) except specific class will be performed.
+ #
+ # def test_hello_job
+ # assert_performed_jobs 1, except: LoggingJob do
+ # HelloJob.perform_later('jeremy')
+ # LoggingJob.perform_later
+ # end
+ # end
+ #
+ # An array may also be specified, to support testing multiple jobs.
+ #
+ # def test_hello_and_logging_jobs
+ # assert_nothing_raised do
+ # assert_performed_jobs 2, only: [HelloJob, LoggingJob] do
+ # HelloJob.perform_later('jeremy')
+ # LoggingJob.perform_later('stewie')
+ # RescueJob.perform_later('david')
+ # end
+ # end
+ # end
+ #
+ # If the +:queue+ option is specified,
+ # then only the job(s) enqueued to a specific queue will be performed.
+ #
+ # def test_assert_performed_jobs_with_queue_option
+ # assert_performed_jobs 1, queue: :some_queue do
+ # HelloJob.set(queue: :some_queue).perform_later("jeremy")
+ # HelloJob.set(queue: :other_queue).perform_later("bogdan")
+ # end
+ # end
+ def assert_performed_jobs(number, only: nil, except: nil, queue: nil, &block)
+ if block_given?
+ original_count = performed_jobs.size
+
+ perform_enqueued_jobs(only: only, except: except, queue: queue, &block)
+
+ new_count = performed_jobs.size
+
+ performed_jobs_size = new_count - original_count
+ else
+ performed_jobs_size = performed_jobs_with(only: only, except: except, queue: queue)
end
- # Asserts that the job passed in the block has been enqueued with the given arguments.
- #
- # def test_assert_enqueued_with
- # assert_enqueued_with(job: MyJob, args: [1,2,3], queue: 'low') do
- # MyJob.perform_later(1,2,3)
- # end
- # end
- def assert_enqueued_with(args = {}, &_block)
- original_enqueued_jobs = enqueued_jobs.dup
- clear_enqueued_jobs
- args.assert_valid_keys(:job, :args, :at, :queue)
- serialized_args = serialize_args_for_assertion(args)
+ assert_equal number, performed_jobs_size, "#{number} jobs expected, but #{performed_jobs_size} were performed"
+ end
+
+ # Asserts that no jobs have been performed.
+ #
+ # def test_jobs
+ # assert_no_performed_jobs
+ #
+ # perform_enqueued_jobs do
+ # HelloJob.perform_later('matthew')
+ # assert_performed_jobs 1
+ # end
+ # end
+ #
+ # If a block is passed, asserts that the block will not cause any job to be performed.
+ #
+ # def test_jobs_again
+ # assert_no_performed_jobs do
+ # # No job should be performed from this block
+ # end
+ # end
+ #
+ # The block form supports filtering. If the +:only+ option is specified,
+ # then only the listed job(s) will not be performed.
+ #
+ # def test_no_logging
+ # assert_no_performed_jobs only: LoggingJob do
+ # HelloJob.perform_later('jeremy')
+ # end
+ # end
+ #
+ # Also if the +:except+ option is specified,
+ # then the job(s) except specific class will not be performed.
+ #
+ # def test_no_logging
+ # assert_no_performed_jobs except: HelloJob do
+ # HelloJob.perform_later('jeremy')
+ # end
+ # end
+ #
+ # If the +:queue+ option is specified,
+ # then only the job(s) enqueued to a specific queue will not be performed.
+ #
+ # def test_assert_no_performed_jobs_with_queue_option
+ # assert_no_performed_jobs queue: :some_queue do
+ # HelloJob.set(queue: :other_queue).perform_later("jeremy")
+ # end
+ # end
+ #
+ # Note: This assertion is simply a shortcut for:
+ #
+ # assert_performed_jobs 0, &block
+ def assert_no_performed_jobs(only: nil, except: nil, queue: nil, &block)
+ assert_performed_jobs 0, only: only, except: except, queue: queue, &block
+ end
+
+ # Asserts that the job has been enqueued with the given arguments.
+ #
+ # def test_assert_enqueued_with
+ # MyJob.perform_later(1,2,3)
+ # assert_enqueued_with(job: MyJob, args: [1,2,3], queue: 'low')
+ #
+ # MyJob.set(wait_until: Date.tomorrow.noon).perform_later
+ # assert_enqueued_with(job: MyJob, at: Date.tomorrow.noon)
+ # end
+ #
+ #
+ # The +args+ argument also accepts a proc which will get passed the actual
+ # job's arguments. Your proc needs to returns a boolean value determining if
+ # the job's arguments matches your expectation. This is useful to check only
+ # for a subset of arguments.
+ #
+ # def test_assert_enqueued_with
+ # expected_args = ->(job_args) do
+ # assert job_args.first.key?(:foo)
+ # end
+ #
+ # MyJob.perform_later(foo: 'bar', other_arg: 'No need to check in the test')
+ # assert_enqueued_with(job: MyJob, args: expected_args, queue: 'low')
+ # end
+ #
+ #
+ # If a block is passed, asserts that the block will cause the job to be
+ # enqueued with the given arguments.
+ #
+ # def test_assert_enqueued_with
+ # assert_enqueued_with(job: MyJob, args: [1,2,3], queue: 'low') do
+ # MyJob.perform_later(1,2,3)
+ # end
+ #
+ # assert_enqueued_with(job: MyJob, at: Date.tomorrow.noon) do
+ # MyJob.set(wait_until: Date.tomorrow.noon).perform_later
+ # end
+ # end
+ def assert_enqueued_with(job: nil, args: nil, at: nil, queue: nil)
+ expected = { job: job, args: args, at: at, queue: queue }.compact
+ expected_args = prepare_args_for_assertion(expected)
+
+ if block_given?
+ original_enqueued_jobs_count = enqueued_jobs.count
+
yield
- matching_job = enqueued_jobs.any? do |job|
- serialized_args.all? { |key, value| value == job[key] }
+
+ jobs = enqueued_jobs.drop(original_enqueued_jobs_count)
+ else
+ jobs = enqueued_jobs
+ end
+
+ matching_job = jobs.find do |enqueued_job|
+ deserialized_job = deserialize_args_for_assertion(enqueued_job)
+
+ expected_args.all? do |key, value|
+ if value.respond_to?(:call)
+ value.call(deserialized_job[key])
+ else
+ value == deserialized_job[key]
+ end
end
- assert matching_job, "No enqueued job found with #{args}"
- ensure
- queue_adapter.enqueued_jobs = original_enqueued_jobs + enqueued_jobs
end
- # Asserts that the job passed in the block has been performed with the given arguments.
- #
- # def test_assert_performed_with
- # assert_performed_with(job: MyJob, args: [1,2,3], queue: 'high') do
- # MyJob.perform_later(1,2,3)
- # end
- # end
- def assert_performed_with(args = {}, &_block)
- original_performed_jobs = performed_jobs.dup
- clear_performed_jobs
- args.assert_valid_keys(:job, :args, :at, :queue)
- serialized_args = serialize_args_for_assertion(args)
- perform_enqueued_jobs { yield }
- matching_job = performed_jobs.any? do |job|
- serialized_args.all? { |key, value| value == job[key] }
+ assert matching_job, "No enqueued job found with #{expected}"
+ instantiate_job(matching_job)
+ end
+
+ # Asserts that the job has been performed with the given arguments.
+ #
+ # def test_assert_performed_with
+ # MyJob.perform_later(1,2,3)
+ #
+ # perform_enqueued_jobs
+ #
+ # assert_performed_with(job: MyJob, args: [1,2,3], queue: 'high')
+ #
+ # MyJob.set(wait_until: Date.tomorrow.noon).perform_later
+ #
+ # perform_enqueued_jobs
+ #
+ # assert_performed_with(job: MyJob, at: Date.tomorrow.noon)
+ # end
+ #
+ # The +args+ argument also accepts a proc which will get passed the actual
+ # job's arguments. Your proc needs to returns a boolean value determining if
+ # the job's arguments matches your expectation. This is useful to check only
+ # for a subset of arguments.
+ #
+ # def test_assert_performed_with
+ # expected_args = ->(job_args) do
+ # assert job_args.first.key?(:foo)
+ # end
+ # MyJob.perform_later(foo: 'bar', other_arg: 'No need to check in the test')
+ #
+ # perform_enqueued_jobs
+ #
+ # assert_performed_with(job: MyJob, args: expected_args, queue: 'high')
+ # end
+ #
+ # If a block is passed, that block performs all of the jobs that were
+ # enqueued throughout the duration of the block and asserts that
+ # the job has been performed with the given arguments in the block.
+ #
+ # def test_assert_performed_with
+ # assert_performed_with(job: MyJob, args: [1,2,3], queue: 'high') do
+ # MyJob.perform_later(1,2,3)
+ # end
+ #
+ # assert_performed_with(job: MyJob, at: Date.tomorrow.noon) do
+ # MyJob.set(wait_until: Date.tomorrow.noon).perform_later
+ # end
+ # end
+ def assert_performed_with(job: nil, args: nil, at: nil, queue: nil, &block)
+ expected = { job: job, args: args, at: at, queue: queue }.compact
+ expected_args = prepare_args_for_assertion(expected)
+
+ if block_given?
+ original_performed_jobs_count = performed_jobs.count
+
+ perform_enqueued_jobs(&block)
+
+ jobs = performed_jobs.drop(original_performed_jobs_count)
+ else
+ jobs = performed_jobs
+ end
+
+ matching_job = jobs.find do |enqueued_job|
+ deserialized_job = deserialize_args_for_assertion(enqueued_job)
+
+ expected_args.all? do |key, value|
+ if value.respond_to?(:call)
+ value.call(deserialized_job[key])
+ else
+ value == deserialized_job[key]
+ end
end
- assert matching_job, "No performed job found with #{args}"
+ end
+
+ assert matching_job, "No performed job found with #{expected}"
+ instantiate_job(matching_job)
+ end
+
+ # Performs all enqueued jobs. If a block is given, performs all of the jobs
+ # that were enqueued throughout the duration of the block. If a block is
+ # not given, performs all of the enqueued jobs up to this point in the test.
+ #
+ # def test_perform_enqueued_jobs
+ # perform_enqueued_jobs do
+ # MyJob.perform_later(1, 2, 3)
+ # end
+ # assert_performed_jobs 1
+ # end
+ #
+ # def test_perform_enqueued_jobs_without_block
+ # MyJob.perform_later(1, 2, 3)
+ #
+ # perform_enqueued_jobs
+ #
+ # assert_performed_jobs 1
+ # end
+ #
+ # This method also supports filtering. If the +:only+ option is specified,
+ # then only the listed job(s) will be performed.
+ #
+ # 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 performed
+ # end
+ # assert_performed_jobs 1
+ # end
+ #
+ # Also if the +:except+ option is specified,
+ # then the job(s) except specific class will be performed.
+ #
+ # def test_perform_enqueued_jobs_with_except
+ # perform_enqueued_jobs(except: HelloJob) do
+ # MyJob.perform_later(1, 2, 3) # will be performed
+ # HelloJob.perform_later(1, 2, 3) # will not be performed
+ # end
+ # assert_performed_jobs 1
+ # end
+ #
+ # If the +:queue+ option is specified,
+ # then only the job(s) enqueued to a specific queue will be performed.
+ #
+ # def test_perform_enqueued_jobs_with_queue
+ # perform_enqueued_jobs queue: :some_queue do
+ # MyJob.set(queue: :some_queue).perform_later(1, 2, 3) # will be performed
+ # HelloJob.set(queue: :other_queue).perform_later(1, 2, 3) # will not be performed
+ # end
+ # assert_performed_jobs 1
+ # end
+ #
+ def perform_enqueued_jobs(only: nil, except: nil, queue: nil)
+ return flush_enqueued_jobs(only: only, except: except, queue: queue) unless block_given?
+
+ validate_option(only: only, except: except)
+
+ old_perform_enqueued_jobs = queue_adapter.perform_enqueued_jobs
+ old_perform_enqueued_at_jobs = queue_adapter.perform_enqueued_at_jobs
+ old_filter = queue_adapter.filter
+ old_reject = queue_adapter.reject
+ old_queue = queue_adapter.queue
+
+ begin
+ queue_adapter.perform_enqueued_jobs = true
+ queue_adapter.perform_enqueued_at_jobs = true
+ queue_adapter.filter = only
+ queue_adapter.reject = except
+ queue_adapter.queue = queue
+
+ yield
ensure
- queue_adapter.performed_jobs = original_performed_jobs + performed_jobs
+ queue_adapter.perform_enqueued_jobs = old_perform_enqueued_jobs
+ queue_adapter.perform_enqueued_at_jobs = old_perform_enqueued_at_jobs
+ queue_adapter.filter = old_filter
+ queue_adapter.reject = old_reject
+ queue_adapter.queue = old_queue
+ end
+ end
+
+ # Accesses the queue_adapter set by ActiveJob::Base.
+ #
+ # def test_assert_job_has_custom_queue_adapter_set
+ # assert_instance_of CustomQueueAdapter, HelloJob.queue_adapter
+ # end
+ def queue_adapter
+ ActiveJob::Base.queue_adapter
+ end
+
+ private
+ def clear_enqueued_jobs
+ enqueued_jobs.clear
end
- def perform_enqueued_jobs(only: nil)
- old_perform_enqueued_jobs = queue_adapter.perform_enqueued_jobs
- old_perform_enqueued_at_jobs = queue_adapter.perform_enqueued_at_jobs
- old_filter = queue_adapter.filter
-
- begin
- queue_adapter.perform_enqueued_jobs = true
- queue_adapter.perform_enqueued_at_jobs = true
- queue_adapter.filter = only
- yield
- ensure
- queue_adapter.perform_enqueued_jobs = old_perform_enqueued_jobs
- queue_adapter.perform_enqueued_at_jobs = old_perform_enqueued_at_jobs
- queue_adapter.filter = old_filter
+ def clear_performed_jobs
+ performed_jobs.clear
+ end
+
+ def jobs_with(jobs, only: nil, except: nil, queue: nil)
+ validate_option(only: only, except: except)
+
+ jobs.count do |job|
+ job_class = job.fetch(:job)
+
+ if only
+ next false unless Array(only).include?(job_class)
+ elsif except
+ next false if Array(except).include?(job_class)
+ end
+
+ if queue
+ next false unless queue.to_s == job.fetch(:queue, job_class.queue_name)
+ end
+
+ yield job if block_given?
+
+ true
end
end
- def queue_adapter
- ActiveJob::Base.queue_adapter
+ def enqueued_jobs_with(only: nil, except: nil, queue: nil, &block)
+ jobs_with(enqueued_jobs, only: only, except: except, queue: queue, &block)
end
- delegate :enqueued_jobs, :enqueued_jobs=,
- :performed_jobs, :performed_jobs=,
- to: :queue_adapter
+ def performed_jobs_with(only: nil, except: nil, queue: nil, &block)
+ jobs_with(performed_jobs, only: only, except: except, queue: queue, &block)
+ end
- private
- def clear_enqueued_jobs
- enqueued_jobs.clear
+ def flush_enqueued_jobs(only: nil, except: nil, queue: nil)
+ enqueued_jobs_with(only: only, except: except, queue: queue) do |payload|
+ instantiate_job(payload).perform_now
+ queue_adapter.performed_jobs << payload
end
+ end
- def clear_performed_jobs
- performed_jobs.clear
+ def prepare_args_for_assertion(args)
+ args.dup.tap do |arguments|
+ arguments[:at] = arguments[:at].to_f if arguments[:at]
end
+ end
- def enqueued_jobs_size(only: nil)
- if only
- enqueued_jobs.select { |job| job.fetch(:job) == only }.size
- else
- enqueued_jobs.size
- end
+ def deserialize_args_for_assertion(job)
+ job.dup.tap do |new_job|
+ new_job[:args] = ActiveJob::Arguments.deserialize(new_job[:args]) if new_job[:args]
end
+ end
- def serialize_args_for_assertion(args)
- serialized_args = args.dup
- if job_args = serialized_args.delete(:args)
- serialized_args[:args] = ActiveJob::Arguments.serialize(job_args)
- end
- serialized_args
+ def instantiate_job(payload)
+ args = ActiveJob::Arguments.deserialize(payload[:args])
+ job = payload[:job].new(*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
+
+ def validate_option(only: nil, except: nil)
+ raise ArgumentError, "Cannot specify both `:only` and `:except` options." if only && except
+ end
end
end
diff --git a/activejob/lib/active_job/timezones.rb b/activejob/lib/active_job/timezones.rb
new file mode 100644
index 0000000000..ac018eb752
--- /dev/null
+++ b/activejob/lib/active_job/timezones.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module ActiveJob
+ module Timezones #:nodoc:
+ extend ActiveSupport::Concern
+
+ included do
+ around_perform do |job, block|
+ Time.use_zone(job.timezone, &block)
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/translation.rb b/activejob/lib/active_job/translation.rb
new file mode 100644
index 0000000000..0fd9b9fc06
--- /dev/null
+++ b/activejob/lib/active_job/translation.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module ActiveJob
+ module Translation #:nodoc:
+ extend ActiveSupport::Concern
+
+ included do
+ around_perform do |job, block|
+ I18n.with_locale(job.locale, &block)
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/version.rb b/activejob/lib/active_job/version.rb
index 971ba9fe0c..eae7da4d05 100644
--- a/activejob/lib/active_job/version.rb
+++ b/activejob/lib/active_job/version.rb
@@ -1,4 +1,6 @@
-require_relative 'gem_version'
+# frozen_string_literal: true
+
+require_relative "gem_version"
module ActiveJob
# Returns the version of the currently loaded Active Job as a <tt>Gem::Version</tt>
diff --git a/activejob/lib/rails/generators/job/job_generator.rb b/activejob/lib/rails/generators/job/job_generator.rb
index 2115fb9f71..03346a7f12 100644
--- a/activejob/lib/rails/generators/job/job_generator.rb
+++ b/activejob/lib/rails/generators/job/job_generator.rb
@@ -1,23 +1,44 @@
-require 'rails/generators/named_base'
+# frozen_string_literal: true
+
+require "rails/generators/named_base"
module Rails # :nodoc:
module Generators # :nodoc:
class JobGenerator < Rails::Generators::NamedBase # :nodoc:
- desc 'This generator creates an active job file at app/jobs'
+ desc "This generator creates an active job file at app/jobs"
- class_option :queue, type: :string, default: 'default', desc: 'The queue name for the generated job'
+ class_option :queue, type: :string, default: "default", desc: "The queue name for the generated job"
- check_class_collision suffix: 'Job'
+ check_class_collision suffix: "Job"
hook_for :test_framework
def self.default_generator_root
- File.dirname(__FILE__)
+ __dir__
end
def create_job_file
- template 'job.rb', File.join('app/jobs', class_path, "#{file_name}_job.rb")
+ template "job.rb", File.join("app/jobs", class_path, "#{file_name}_job.rb")
+
+ in_root do
+ if behavior == :invoke && !File.exist?(application_job_file_name)
+ template "application_job.rb", application_job_file_name
+ end
+ end
end
+
+ private
+ def file_name
+ @_file_name ||= super.sub(/_job\z/i, "")
+ end
+
+ def application_job_file_name
+ @application_job_file_name ||= if mountable_engine?
+ "app/jobs/#{namespaced_path}/application_job.rb"
+ else
+ "app/jobs/application_job.rb"
+ end
+ end
end
end
end
diff --git a/activejob/lib/rails/generators/job/templates/application_job.rb.tt b/activejob/lib/rails/generators/job/templates/application_job.rb.tt
new file mode 100644
index 0000000000..f93745a31a
--- /dev/null
+++ b/activejob/lib/rails/generators/job/templates/application_job.rb.tt
@@ -0,0 +1,9 @@
+<% module_namespacing do -%>
+class ApplicationJob < ActiveJob::Base
+ # Automatically retry jobs that encountered a deadlock
+ # retry_on ActiveRecord::Deadlocked
+
+ # Most jobs are safe to ignore if the underlying records are no longer available
+ # discard_on ActiveJob::DeserializationError
+end
+<% end -%>
diff --git a/activejob/lib/rails/generators/job/templates/job.rb b/activejob/lib/rails/generators/job/templates/job.rb.tt
index 4ad2914a45..4ad2914a45 100644
--- a/activejob/lib/rails/generators/job/templates/job.rb
+++ b/activejob/lib/rails/generators/job/templates/job.rb.tt
diff --git a/activejob/test/adapters/async.rb b/activejob/test/adapters/async.rb
new file mode 100644
index 0000000000..a4fed7c2f7
--- /dev/null
+++ b/activejob/test/adapters/async.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+ActiveJob::Base.queue_adapter = :async
+ActiveJob::Base.queue_adapter.immediate = true
diff --git a/activejob/test/adapters/backburner.rb b/activejob/test/adapters/backburner.rb
index 65d05f850b..bc34c78e9c 100644
--- a/activejob/test/adapters/backburner.rb
+++ b/activejob/test/adapters/backburner.rb
@@ -1,3 +1,5 @@
-require 'support/backburner/inline'
+# frozen_string_literal: true
-ActiveJob::Base.queue_adapter = :backburner \ No newline at end of file
+require "support/backburner/inline"
+
+ActiveJob::Base.queue_adapter = :backburner
diff --git a/activejob/test/adapters/delayed_job.rb b/activejob/test/adapters/delayed_job.rb
index afd9c9deb7..904b4c3f90 100644
--- a/activejob/test/adapters/delayed_job.rb
+++ b/activejob/test/adapters/delayed_job.rb
@@ -1,7 +1,8 @@
+# frozen_string_literal: true
+
ActiveJob::Base.queue_adapter = :delayed_job
-$LOAD_PATH << File.dirname(__FILE__) + "/../support/delayed_job"
+$LOAD_PATH << File.expand_path("../support/delayed_job", __dir__)
Delayed::Worker.delay_jobs = false
Delayed::Worker.backend = :test
-
diff --git a/activejob/test/adapters/inline.rb b/activejob/test/adapters/inline.rb
index e0092552c4..b1ddcb28f1 100644
--- a/activejob/test/adapters/inline.rb
+++ b/activejob/test/adapters/inline.rb
@@ -1 +1,3 @@
-ActiveJob::Base.queue_adapter = :inline \ No newline at end of file
+# frozen_string_literal: true
+
+ActiveJob::Base.queue_adapter = :inline
diff --git a/activejob/test/adapters/qu.rb b/activejob/test/adapters/qu.rb
deleted file mode 100644
index 7728c843b4..0000000000
--- a/activejob/test/adapters/qu.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-require 'qu-immediate'
-
-ActiveJob::Base.queue_adapter = :qu
diff --git a/activejob/test/adapters/que.rb b/activejob/test/adapters/que.rb
index e6abc57457..af77b0d4d1 100644
--- a/activejob/test/adapters/que.rb
+++ b/activejob/test/adapters/que.rb
@@ -1,4 +1,6 @@
-require 'support/que/inline'
+# frozen_string_literal: true
+
+require "support/que/inline"
ActiveJob::Base.queue_adapter = :que
Que.mode = :sync
diff --git a/activejob/test/adapters/queue_classic.rb b/activejob/test/adapters/queue_classic.rb
index ad5ced3cc2..73902a5e62 100644
--- a/activejob/test/adapters/queue_classic.rb
+++ b/activejob/test/adapters/queue_classic.rb
@@ -1,2 +1,4 @@
-require 'support/queue_classic/inline'
+# frozen_string_literal: true
+
+require "support/queue_classic/inline"
ActiveJob::Base.queue_adapter = :queue_classic
diff --git a/activejob/test/adapters/resque.rb b/activejob/test/adapters/resque.rb
index af7080358d..ad84a49372 100644
--- a/activejob/test/adapters/resque.rb
+++ b/activejob/test/adapters/resque.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
ActiveJob::Base.queue_adapter = :resque
Resque.inline = true
diff --git a/activejob/test/adapters/sidekiq.rb b/activejob/test/adapters/sidekiq.rb
index cd9d2034de..7df1c36488 100644
--- a/activejob/test/adapters/sidekiq.rb
+++ b/activejob/test/adapters/sidekiq.rb
@@ -1,2 +1,4 @@
-require 'sidekiq/testing/inline'
+# frozen_string_literal: true
+
+require "sidekiq/testing/inline"
ActiveJob::Base.queue_adapter = :sidekiq
diff --git a/activejob/test/adapters/sneakers.rb b/activejob/test/adapters/sneakers.rb
index 204166a700..38d82aa778 100644
--- a/activejob/test/adapters/sneakers.rb
+++ b/activejob/test/adapters/sneakers.rb
@@ -1,2 +1,4 @@
-require 'support/sneakers/inline'
+# frozen_string_literal: true
+
+require "support/sneakers/inline"
ActiveJob::Base.queue_adapter = :sneakers
diff --git a/activejob/test/adapters/sucker_punch.rb b/activejob/test/adapters/sucker_punch.rb
index d2d1712946..04bad984d4 100644
--- a/activejob/test/adapters/sucker_punch.rb
+++ b/activejob/test/adapters/sucker_punch.rb
@@ -1,2 +1,4 @@
-require 'sucker_punch/testing/inline'
+# frozen_string_literal: true
+
+require "sucker_punch/testing/inline"
ActiveJob::Base.queue_adapter = :sucker_punch
diff --git a/activejob/test/adapters/test.rb b/activejob/test/adapters/test.rb
index 7180b38a57..0a1367dacf 100644
--- a/activejob/test/adapters/test.rb
+++ b/activejob/test/adapters/test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
ActiveJob::Base.queue_adapter = :test
ActiveJob::Base.queue_adapter.perform_enqueued_jobs = true
ActiveJob::Base.queue_adapter.perform_enqueued_at_jobs = true
diff --git a/activejob/test/cases/adapter_test.rb b/activejob/test/cases/adapter_test.rb
index 6d75ae9a7c..2c179b2d38 100644
--- a/activejob/test/cases/adapter_test.rb
+++ b/activejob/test/cases/adapter_test.rb
@@ -1,4 +1,6 @@
-require 'helper'
+# frozen_string_literal: true
+
+require "helper"
class AdapterTest < ActiveSupport::TestCase
test "should load #{ENV['AJ_ADAPTER']} adapter" do
diff --git a/activejob/test/cases/argument_serialization_test.rb b/activejob/test/cases/argument_serialization_test.rb
index 933972a52b..e4e14016d9 100644
--- a/activejob/test/cases/argument_serialization_test.rb
+++ b/activejob/test/cases/argument_serialization_test.rb
@@ -1,25 +1,31 @@
-require 'helper'
-require 'active_job/arguments'
-require 'models/person'
-require 'active_support/core_ext/hash/indifferent_access'
-require 'jobs/kwargs_job'
+# frozen_string_literal: true
+
+require "helper"
+require "active_job/arguments"
+require "models/person"
+require "active_support/core_ext/hash/indifferent_access"
+require "jobs/kwargs_job"
+require "support/stubs/strong_parameters"
class ArgumentSerializationTest < ActiveSupport::TestCase
setup do
- @person = Person.find('5')
+ @person = Person.find("5")
end
[ nil, 1, 1.0, 1_000_000_000_000_000_000_000,
- 'a', true, false,
- [ 1, 'a' ],
- { 'a' => 1 }
+ "a", true, false, BigDecimal(5),
+ :a, 1.day, Date.new(2001, 2, 3), Time.new(2002, 10, 31, 2, 2, 2, "+02:00"),
+ DateTime.new(2001, 2, 3, 4, 5, 6, "+03:00"),
+ ActiveSupport::TimeWithZone.new(Time.utc(1999, 12, 31, 23, 59, 59), ActiveSupport::TimeZone["UTC"]),
+ [ 1, "a" ],
+ { "a" => 1 }
].each do |arg|
- test "serializes #{arg.class} verbatim" do
+ test "serializes #{arg.class} - #{arg} verbatim" do
assert_arguments_unchanged arg
end
end
- [ :a, Object.new, self, Person.find('5').to_gid ].each do |arg|
+ [ Object.new, self, Person.find("5").to_gid ].each do |arg|
test "does not serialize #{arg.class}" do
assert_raises ActiveJob::SerializationError do
ActiveJob::Arguments.serialize [ arg ]
@@ -31,22 +37,79 @@ class ArgumentSerializationTest < ActiveSupport::TestCase
end
end
- test 'should convert records to Global IDs' do
+ test "should convert records to Global IDs" do
assert_arguments_roundtrip [@person]
end
- test 'should dive deep into arrays and hashes' do
+ test "should dive deep into arrays and hashes" do
assert_arguments_roundtrip [3, [@person]]
- assert_arguments_roundtrip [{ 'a' => @person }]
+ assert_arguments_roundtrip [{ "a" => @person }]
end
- test 'should maintain string and symbol keys' do
+ test "should maintain string and symbol keys" do
assert_arguments_roundtrip([a: 1, "b" => 2])
end
- test 'should maintain hash with indifferent access' do
+ test "serialize a ActionController::Parameters" do
+ parameters = Parameters.new(a: 1)
+
+ assert_equal(
+ { "a" => 1, "_aj_hash_with_indifferent_access" => true },
+ ActiveJob::Arguments.serialize([parameters]).first
+ )
+ end
+
+ test "serialize a hash" do
symbol_key = { a: 1 }
- string_key = { 'a' => 1 }
+ string_key = { "a" => 1 }
+ indifferent_access = { a: 1 }.with_indifferent_access
+
+ assert_equal(
+ { "a" => 1, "_aj_symbol_keys" => ["a"] },
+ ActiveJob::Arguments.serialize([symbol_key]).first
+ )
+ assert_equal(
+ { "a" => 1, "_aj_symbol_keys" => [] },
+ ActiveJob::Arguments.serialize([string_key]).first
+ )
+ assert_equal(
+ { "a" => 1, "_aj_hash_with_indifferent_access" => true },
+ ActiveJob::Arguments.serialize([indifferent_access]).first
+ )
+ end
+
+ test "deserialize a hash" do
+ symbol_key = { "a" => 1, "_aj_symbol_keys" => ["a"] }
+ string_key = { "a" => 1, "_aj_symbol_keys" => [] }
+ another_string_key = { "a" => 1 }
+ indifferent_access = { "a" => 1, "_aj_hash_with_indifferent_access" => true }
+ indifferent_access_symbol_key = symbol_key.with_indifferent_access
+
+ assert_equal(
+ { a: 1 },
+ ActiveJob::Arguments.deserialize([symbol_key]).first
+ )
+ assert_equal(
+ { "a" => 1 },
+ ActiveJob::Arguments.deserialize([string_key]).first
+ )
+ assert_equal(
+ { "a" => 1 },
+ ActiveJob::Arguments.deserialize([another_string_key]).first
+ )
+ assert_equal(
+ { "a" => 1 },
+ ActiveJob::Arguments.deserialize([indifferent_access]).first
+ )
+ assert_equal(
+ { a: 1 },
+ ActiveJob::Arguments.deserialize([indifferent_access_symbol_key]).first
+ )
+ end
+
+ test "should maintain hash with indifferent access" do
+ symbol_key = { a: 1 }
+ string_key = { "a" => 1 }
indifferent_access = { a: 1 }.with_indifferent_access
assert_not_instance_of ActiveSupport::HashWithIndifferentAccess, perform_round_trip([symbol_key]).first
@@ -54,25 +117,36 @@ class ArgumentSerializationTest < ActiveSupport::TestCase
assert_instance_of ActiveSupport::HashWithIndifferentAccess, perform_round_trip([indifferent_access]).first
end
- test 'should disallow non-string/symbol hash keys' do
- assert_raises ActiveJob::SerializationError do
- ActiveJob::Arguments.serialize [ { 1 => 2 } ]
+ test "should maintain time with zone" do
+ Time.use_zone "Alaska" do
+ time_with_zone = Time.new(2002, 10, 31, 2, 2, 2).in_time_zone
+ assert_instance_of ActiveSupport::TimeWithZone, perform_round_trip([time_with_zone]).first
+ assert_arguments_unchanged time_with_zone
end
+ end
+ test "should disallow non-string/symbol hash keys" do
assert_raises ActiveJob::SerializationError do
- ActiveJob::Arguments.serialize [ { :a => [{ 2 => 3 }] } ]
+ ActiveJob::Arguments.serialize [ { 1 => 2 } ]
end
assert_raises ActiveJob::SerializationError do
- ActiveJob::Arguments.serialize [ '_aj_globalid' => 1 ]
+ ActiveJob::Arguments.serialize [ { a: [{ 2 => 3 }] } ]
end
+ end
- assert_raises ActiveJob::SerializationError do
- ActiveJob::Arguments.serialize [ :_aj_globalid => 1 ]
+ test "should not allow reserved hash keys" do
+ ["_aj_globalid", :_aj_globalid,
+ "_aj_symbol_keys", :_aj_symbol_keys,
+ "_aj_hash_with_indifferent_access", :_aj_hash_with_indifferent_access,
+ "_aj_serialized", :_aj_serialized].each do |key|
+ assert_raises ActiveJob::SerializationError do
+ ActiveJob::Arguments.serialize [key => 1]
+ end
end
end
- test 'should not allow non-primitive objects' do
+ test "should not allow non-primitive objects" do
assert_raises ActiveJob::SerializationError do
ActiveJob::Arguments.serialize [Object.new]
end
@@ -82,17 +156,17 @@ class ArgumentSerializationTest < ActiveSupport::TestCase
end
end
- test 'allows for keyword arguments' do
+ test "allows for keyword arguments" do
KwargsJob.perform_later(argument: 2)
assert_equal "Job with argument: 2", JobBuffer.last_value
end
- test 'raises a friendly SerializationError for records without ids' do
+ test "raises a friendly SerializationError for records without ids" do
err = assert_raises ActiveJob::SerializationError do
ActiveJob::Arguments.serialize [Person.new(nil)]
end
- assert_match 'Unable to serialize Person without an id.', err.message
+ assert_match "Unable to serialize Person without an id.", err.message
end
private
diff --git a/activejob/test/cases/callbacks_test.rb b/activejob/test/cases/callbacks_test.rb
index 9af2380767..df6ce16858 100644
--- a/activejob/test/cases/callbacks_test.rb
+++ b/activejob/test/cases/callbacks_test.rb
@@ -1,10 +1,12 @@
-require 'helper'
-require 'jobs/callback_job'
+# frozen_string_literal: true
-require 'active_support/core_ext/object/inclusion'
+require "helper"
+require "jobs/callback_job"
+
+require "active_support/core_ext/object/inclusion"
class CallbacksTest < ActiveSupport::TestCase
- test 'perform callbacks' do
+ test "perform callbacks" do
performed_callback_job = CallbackJob.new("A-JOB-ID")
performed_callback_job.perform_now
assert "CallbackJob ran before_perform".in? performed_callback_job.history
@@ -13,7 +15,7 @@ class CallbacksTest < ActiveSupport::TestCase
assert "CallbackJob ran around_perform_stop".in? performed_callback_job.history
end
- test 'enqueue callbacks' do
+ test "enqueue callbacks" do
enqueued_callback_job = CallbackJob.perform_later
assert "CallbackJob ran before_enqueue".in? enqueued_callback_job.history
assert "CallbackJob ran after_enqueue".in? enqueued_callback_job.history
diff --git a/activejob/test/cases/exceptions_test.rb b/activejob/test/cases/exceptions_test.rb
new file mode 100644
index 0000000000..37bb65538a
--- /dev/null
+++ b/activejob/test/cases/exceptions_test.rb
@@ -0,0 +1,142 @@
+# frozen_string_literal: true
+
+require "helper"
+require "jobs/retry_job"
+require "models/person"
+
+class ExceptionsTest < ActiveJob::TestCase
+ setup do
+ JobBuffer.clear
+ skip if ActiveJob::Base.queue_adapter.is_a?(ActiveJob::QueueAdapters::InlineAdapter)
+ end
+
+ test "successfully retry job throwing exception against defaults" do
+ perform_enqueued_jobs do
+ RetryJob.perform_later "DefaultsError", 5
+
+ assert_equal [
+ "Raised DefaultsError for the 1st time",
+ "Raised DefaultsError for the 2nd time",
+ "Raised DefaultsError for the 3rd time",
+ "Raised DefaultsError for the 4th time",
+ "Successfully completed job" ], JobBuffer.values
+ end
+ end
+
+ test "successfully retry job throwing exception against higher limit" do
+ perform_enqueued_jobs do
+ RetryJob.perform_later "ShortWaitTenAttemptsError", 9
+ assert_equal 9, JobBuffer.values.count
+ end
+ end
+
+ test "failed retry job when exception kept occurring against defaults" do
+ perform_enqueued_jobs do
+ begin
+ RetryJob.perform_later "DefaultsError", 6
+ assert_equal "Raised DefaultsError for the 5th time", JobBuffer.last_value
+ rescue DefaultsError
+ pass
+ end
+ end
+ end
+
+ test "failed retry job when exception kept occurring against higher limit" do
+ perform_enqueued_jobs do
+ begin
+ RetryJob.perform_later "ShortWaitTenAttemptsError", 11
+ assert_equal "Raised ShortWaitTenAttemptsError for the 10th time", JobBuffer.last_value
+ rescue ShortWaitTenAttemptsError
+ pass
+ end
+ end
+ end
+
+ test "discard job" do
+ perform_enqueued_jobs do
+ RetryJob.perform_later "DiscardableError", 2
+ assert_equal "Raised DiscardableError for the 1st time", JobBuffer.last_value
+ end
+ end
+
+ test "custom handling of discarded job" do
+ perform_enqueued_jobs do
+ RetryJob.perform_later "CustomDiscardableError", 2
+ assert_equal "Dealt with a job that was discarded in a custom way. Message: CustomDiscardableError", JobBuffer.last_value
+ end
+ end
+
+ test "custom handling of job that exceeds retry attempts" do
+ perform_enqueued_jobs do
+ RetryJob.perform_later "CustomCatchError", 6
+ assert_equal "Dealt with a job that failed to retry in a custom way after 6 attempts. Message: CustomCatchError", JobBuffer.last_value
+ end
+ end
+
+ test "long wait job" do
+ travel_to Time.now
+
+ perform_enqueued_jobs do
+ assert_performed_with at: (Time.now + 3600.seconds).to_i do
+ RetryJob.perform_later "LongWaitError", 5
+ end
+ end
+ end
+
+ test "exponentially retrying job" do
+ travel_to Time.now
+
+ perform_enqueued_jobs do
+ assert_performed_with at: (Time.now + 3.seconds).to_i do
+ assert_performed_with at: (Time.now + 18.seconds).to_i do
+ assert_performed_with at: (Time.now + 83.seconds).to_i do
+ assert_performed_with at: (Time.now + 258.seconds).to_i do
+ RetryJob.perform_later "ExponentialWaitTenAttemptsError", 5
+ end
+ end
+ end
+ end
+ end
+ end
+
+ test "custom wait retrying job" do
+ travel_to Time.now
+
+ perform_enqueued_jobs do
+ assert_performed_with at: (Time.now + 2.seconds).to_i do
+ assert_performed_with at: (Time.now + 4.seconds).to_i do
+ assert_performed_with at: (Time.now + 6.seconds).to_i do
+ assert_performed_with at: (Time.now + 8.seconds).to_i do
+ RetryJob.perform_later "CustomWaitTenAttemptsError", 5
+ end
+ end
+ end
+ end
+ end
+ end
+
+ test "successfully retry job throwing one of two retryable exceptions" do
+ perform_enqueued_jobs do
+ RetryJob.perform_later "SecondRetryableErrorOfTwo", 3
+
+ assert_equal [
+ "Raised SecondRetryableErrorOfTwo for the 1st time",
+ "Raised SecondRetryableErrorOfTwo for the 2nd time",
+ "Successfully completed job" ], JobBuffer.values
+ end
+ end
+
+ test "discard job throwing one of two discardable exceptions" do
+ perform_enqueued_jobs do
+ RetryJob.perform_later "SecondDiscardableErrorOfTwo", 2
+ assert_equal [ "Raised SecondDiscardableErrorOfTwo for the 1st time" ], JobBuffer.values
+ end
+ end
+
+ test "successfully retry job throwing DeserializationError" do
+ perform_enqueued_jobs do
+ RetryJob.perform_later Person.new(404), 5
+ assert_equal ["Raised ActiveJob::DeserializationError for the 5 time"], JobBuffer.values
+ end
+ end
+end
diff --git a/activejob/test/cases/job_serialization_test.rb b/activejob/test/cases/job_serialization_test.rb
index db22783030..86f3651564 100644
--- a/activejob/test/cases/job_serialization_test.rb
+++ b/activejob/test/cases/job_serialization_test.rb
@@ -1,6 +1,10 @@
-require 'helper'
-require 'jobs/gid_job'
-require 'models/person'
+# frozen_string_literal: true
+
+require "helper"
+require "jobs/gid_job"
+require "jobs/hello_job"
+require "models/person"
+require "json"
class JobSerializationTest < ActiveSupport::TestCase
setup do
@@ -8,8 +12,53 @@ class JobSerializationTest < ActiveSupport::TestCase
@person = Person.find(5)
end
- test 'serialize job with gid' do
+ test "serialize job with gid" do
GidJob.perform_later @person
assert_equal "Person with ID: 5", JobBuffer.last_value
end
+
+ test "serialize includes current locale" do
+ assert_equal "en", HelloJob.new.serialize["locale"]
+ end
+
+ test "serialize and deserialize are symmetric" do
+ # Round trip a job in memory only
+ h1 = HelloJob.new("Rafael")
+ h2 = HelloJob.deserialize(h1.serialize)
+ assert_equal h1.serialize, h2.serialize
+
+ # Now verify it's identical to a JSON round trip.
+ # We don't want any non-native JSON elements in the job hash,
+ # like symbols.
+ payload = JSON.dump(h2.serialize)
+ h3 = HelloJob.deserialize(JSON.load(payload))
+ assert_equal h2.serialize, h3.serialize
+ end
+
+ test "deserialize sets locale" do
+ job = HelloJob.new
+ job.deserialize "locale" => "es"
+ assert_equal "es", job.locale
+ end
+
+ test "deserialize sets default locale" do
+ job = HelloJob.new
+ job.deserialize({})
+ assert_equal "en", job.locale
+ end
+
+ test "serialize stores provider_job_id" do
+ job = HelloJob.new
+ assert_nil job.serialize["provider_job_id"]
+
+ job.provider_job_id = "some value set by adapter"
+ assert_equal job.provider_job_id, job.serialize["provider_job_id"]
+ end
+
+ test "serialize stores the current timezone" do
+ Time.use_zone "Hawaii" do
+ job = HelloJob.new
+ assert_equal "Hawaii", job.serialize["timezone"]
+ end
+ end
end
diff --git a/activejob/test/cases/logging_test.rb b/activejob/test/cases/logging_test.rb
index b18be553ec..48ef39aaca 100644
--- a/activejob/test/cases/logging_test.rb
+++ b/activejob/test/cases/logging_test.rb
@@ -1,12 +1,18 @@
-require 'helper'
+# frozen_string_literal: true
+
+require "helper"
require "active_support/log_subscriber/test_helper"
-require 'active_support/core_ext/numeric/time'
-require 'jobs/hello_job'
-require 'jobs/logging_job'
-require 'jobs/nested_job'
-require 'models/person'
+require "active_support/core_ext/numeric/time"
+require "jobs/hello_job"
+require "jobs/logging_job"
+require "jobs/overridden_logging_job"
+require "jobs/nested_job"
+require "jobs/rescue_job"
+require "jobs/retry_job"
+require "models/person"
class LoggingTest < ActiveSupport::TestCase
+ include ActiveJob::TestHelper
include ActiveSupport::LogSubscriber::TestHelper
include ActiveSupport::Logger::Severity
@@ -41,6 +47,13 @@ class LoggingTest < ActiveSupport::TestCase
ActiveJob::Base.logger = logger
end
+ def subscribed
+ [].tap do |events|
+ ActiveSupport::Notifications.subscribed(-> (*args) { events << args }, /enqueue.*\.active_job/) do
+ yield
+ end
+ end
+ end
def test_uses_active_job_as_tag
HelloJob.perform_later "Cristian"
@@ -48,13 +61,17 @@ class LoggingTest < ActiveSupport::TestCase
end
def test_uses_job_name_as_tag
- LoggingJob.perform_later "Dummy"
- assert_match(/\[LoggingJob\]/, @logger.messages)
+ perform_enqueued_jobs do
+ LoggingJob.perform_later "Dummy"
+ assert_match(/\[LoggingJob\]/, @logger.messages)
+ end
end
def test_uses_job_id_as_tag
- LoggingJob.perform_later "Dummy"
- assert_match(/\[LOGGING-JOB-ID\]/, @logger.messages)
+ perform_enqueued_jobs do
+ LoggingJob.perform_later "Dummy"
+ assert_match(/\[LOGGING-JOB-ID\]/, @logger.messages)
+ end
end
def test_logs_correct_queue_name
@@ -67,48 +84,121 @@ class LoggingTest < ActiveSupport::TestCase
end
def test_globalid_parameter_logging
- person = Person.new(123)
- LoggingJob.perform_later person
- assert_match(%r{Enqueued.*gid://aj/Person/123}, @logger.messages)
- assert_match(%r{Dummy, here is it: #<Person:.*>}, @logger.messages)
- assert_match(%r{Performing.*gid://aj/Person/123}, @logger.messages)
+ perform_enqueued_jobs do
+ person = Person.new(123)
+ LoggingJob.perform_later person
+ assert_match(%r{Enqueued.*gid://aj/Person/123}, @logger.messages)
+ assert_match(%r{Dummy, here is it: #<Person:.*>}, @logger.messages)
+ assert_match(%r{Performing.*gid://aj/Person/123}, @logger.messages)
+ end
+ end
+
+ def test_globalid_nested_parameter_logging
+ perform_enqueued_jobs do
+ person = Person.new(123)
+ LoggingJob.perform_later(person: person)
+ assert_match(%r{Enqueued.*gid://aj/Person/123}, @logger.messages)
+ assert_match(%r{Dummy, here is it: .*#<Person:.*>}, @logger.messages)
+ assert_match(%r{Performing.*gid://aj/Person/123}, @logger.messages)
+ end
end
def test_enqueue_job_logging
- HelloJob.perform_later "Cristian"
+ events = subscribed { HelloJob.perform_later "Cristian" }
assert_match(/Enqueued HelloJob \(Job ID: .*?\) to .*?:.*Cristian/, @logger.messages)
+ assert_equal(events.count, 1)
+ key, * = events.first
+ assert_equal(key, "enqueue.active_job")
end
def test_perform_job_logging
- LoggingJob.perform_later "Dummy"
- assert_match(/Performing LoggingJob from .*? with arguments:.*Dummy/, @logger.messages)
- assert_match(/Dummy, here is it: Dummy/, @logger.messages)
- assert_match(/Performed LoggingJob from .*? in .*ms/, @logger.messages)
+ perform_enqueued_jobs do
+ LoggingJob.perform_later "Dummy"
+ 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 \(Job ID: .*?\) from .*? in .*ms/, @logger.messages)
+ end
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\] 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\] 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)
+ perform_enqueued_jobs do
+ 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 \(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 \(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 \(Job ID: .*?\) from .* in/, @logger.messages)
+ assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Performed NestedJob \(Job ID: .*?\) from .* in/, @logger.messages)
+ end
end
def test_enqueue_at_job_logging
- HelloJob.set(wait_until: 24.hours.from_now).perform_later "Cristian"
+ events = subscribed { HelloJob.set(wait_until: 24.hours.from_now).perform_later "Cristian" }
assert_match(/Enqueued HelloJob \(Job ID: .*\) to .*? at.*Cristian/, @logger.messages)
+ assert_equal(events.count, 1)
+ key, * = events.first
+ assert_equal(key, "enqueue_at.active_job")
rescue NotImplementedError
skip
end
def test_enqueue_in_job_logging
- HelloJob.set(wait: 2.seconds).perform_later "Cristian"
+ events = subscribed { HelloJob.set(wait: 2.seconds).perform_later "Cristian" }
assert_match(/Enqueued HelloJob \(Job ID: .*\) to .*? at.*Cristian/, @logger.messages)
+ assert_equal(events.count, 1)
+ key, * = events.first
+ assert_equal(key, "enqueue_at.active_job")
rescue NotImplementedError
skip
end
+
+ def test_for_tagged_logger_support_is_consistent
+ set_logger ::Logger.new(nil)
+ OverriddenLoggingJob.perform_later "Dummy"
+ end
+
+ def test_job_error_logging
+ perform_enqueued_jobs { RescueJob.perform_later "other" }
+ rescue RescueJob::OtherError
+ assert_match(/Performing RescueJob \(Job ID: .*?\) from .*? with arguments:.*other/, @logger.messages)
+ assert_match(/Error performing RescueJob \(Job ID: .*?\) from .*? in .*ms: RescueJob::OtherError \(Bad hair\):\n.*\brescue_job\.rb:\d+:in `perform'/, @logger.messages)
+ end
+
+ def test_enqueue_retry_logging
+ perform_enqueued_jobs do
+ RetryJob.perform_later "DefaultsError", 2
+ assert_match(/Retrying RetryJob in 3 seconds, due to a DefaultsError\./, @logger.messages)
+ end
+ end
+
+ def test_enqueue_retry_logging_on_retry_job
+ perform_enqueued_jobs { RescueJob.perform_later "david" }
+ assert_match(/Retrying RescueJob in 0 seconds\./, @logger.messages)
+ end
+
+ def test_retry_stopped_logging
+ perform_enqueued_jobs do
+ RetryJob.perform_later "CustomCatchError", 6
+ assert_match(/Stopped retrying RetryJob due to a CustomCatchError, which reoccurred on \d+ attempts\./, @logger.messages)
+ end
+ end
+
+ def test_retry_stopped_logging_without_block
+ perform_enqueued_jobs do
+ begin
+ RetryJob.perform_later "DefaultsError", 6
+ rescue DefaultsError
+ assert_match(/Stopped retrying RetryJob due to a DefaultsError, which reoccurred on \d+ attempts\./, @logger.messages)
+ end
+ end
+ end
+
+ def test_discard_logging
+ perform_enqueued_jobs do
+ RetryJob.perform_later "DiscardableError", 2
+ assert_match(/Discarded RetryJob due to a DiscardableError\./, @logger.messages)
+ end
+ end
end
diff --git a/activejob/test/cases/queue_adapter_test.rb b/activejob/test/cases/queue_adapter_test.rb
index fb3fdc392f..e71cfa49cf 100644
--- a/activejob/test/cases/queue_adapter_test.rb
+++ b/activejob/test/cases/queue_adapter_test.rb
@@ -1,4 +1,6 @@
-require 'helper'
+# frozen_string_literal: true
+
+require "helper"
module ActiveJob
module QueueAdapters
@@ -15,36 +17,29 @@ module ActiveJob
end
class QueueAdapterTest < ActiveJob::TestCase
- test 'should forbid nonsense arguments' do
+ test "should forbid nonsense arguments" do
assert_raises(ArgumentError) { ActiveJob::Base.queue_adapter = Mutex }
assert_raises(ArgumentError) { ActiveJob::Base.queue_adapter = Mutex.new }
end
- test 'should warn on passing an adapter class' do
- klass = Class.new do
- def self.name
- 'fake'
- end
-
- def enqueue(*); end
- def enqueue_at(*); end
- end
-
- assert_deprecated { ActiveJob::Base.queue_adapter = klass }
- end
-
- test 'should allow overriding the queue_adapter at the child class level without affecting the parent or its sibling' do
+ 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)
+ assert_equal child_job_one.queue_adapter_name, ActiveJob::Base.queue_adapter_name
+
child_job_one.queue_adapter = :stub_one
assert_not_equal ActiveJob::Base.queue_adapter, child_job_one.queue_adapter
+ assert_equal "stub_one", child_job_one.queue_adapter_name
assert_kind_of ActiveJob::QueueAdapters::StubOneAdapter, child_job_one.queue_adapter
child_job_two = Class.new(ActiveJob::Base)
child_job_two.queue_adapter = :stub_two
+ assert_equal "stub_two", child_job_two.queue_adapter_name
+
assert_kind_of ActiveJob::QueueAdapters::StubTwoAdapter, child_job_two.queue_adapter
assert_kind_of ActiveJob::QueueAdapters::StubOneAdapter, child_job_one.queue_adapter, "child_job_one's queue adapter should remain unchanged"
assert_equal base_queue_adapter, ActiveJob::Base.queue_adapter, "ActiveJob::Base's queue adapter should remain unchanged"
diff --git a/activejob/test/cases/queue_naming_test.rb b/activejob/test/cases/queue_naming_test.rb
index 898016a704..b64a38f91e 100644
--- a/activejob/test/cases/queue_naming_test.rb
+++ b/activejob/test/cases/queue_naming_test.rb
@@ -1,14 +1,16 @@
-require 'helper'
-require 'jobs/hello_job'
-require 'jobs/logging_job'
-require 'jobs/nested_job'
+# frozen_string_literal: true
+
+require "helper"
+require "jobs/hello_job"
+require "jobs/logging_job"
+require "jobs/nested_job"
class QueueNamingTest < ActiveSupport::TestCase
- test 'name derived from base' do
+ test "name derived from base" do
assert_equal "default", HelloJob.queue_name
end
- test 'uses given queue name job' do
+ test "uses given queue name job" do
original_queue_name = HelloJob.queue_name
begin
@@ -19,7 +21,7 @@ class QueueNamingTest < ActiveSupport::TestCase
end
end
- test 'allows a blank queue name' do
+ test "allows a blank queue name" do
original_queue_name = HelloJob.queue_name
begin
@@ -30,7 +32,7 @@ class QueueNamingTest < ActiveSupport::TestCase
end
end
- test 'does not use a nil queue name' do
+ test "does not use a nil queue name" do
original_queue_name = HelloJob.queue_name
begin
@@ -41,7 +43,7 @@ class QueueNamingTest < ActiveSupport::TestCase
end
end
- test 'evals block given to queue_as to determine queue' do
+ test "evals block given to queue_as to determine queue" do
original_queue_name = HelloJob.queue_name
begin
@@ -52,42 +54,42 @@ class QueueNamingTest < ActiveSupport::TestCase
end
end
- test 'can use arguments to determine queue_name in queue_as block' do
+ test "can use arguments to determine queue_name in queue_as block" do
original_queue_name = HelloJob.queue_name
begin
- HelloJob.queue_as { self.arguments.first=='1' ? :one : :two }
- assert_equal "one", HelloJob.new('1').queue_name
- assert_equal "two", HelloJob.new('3').queue_name
+ 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
HelloJob.queue_name = original_queue_name
end
end
- test 'queue_name_prefix prepended to the queue name with default delimiter' do
+ test "queue_name_prefix prepended to the queue name with default delimiter" do
original_queue_name_prefix = ActiveJob::Base.queue_name_prefix
original_queue_name = HelloJob.queue_name
begin
- ActiveJob::Base.queue_name_prefix = 'aj'
+ ActiveJob::Base.queue_name_prefix = "aj"
HelloJob.queue_as :low
- assert_equal 'aj_low', HelloJob.queue_name
+ assert_equal "aj_low", HelloJob.queue_name
ensure
ActiveJob::Base.queue_name_prefix = original_queue_name_prefix
HelloJob.queue_name = original_queue_name
end
end
- test 'queue_name_prefix prepended to the queue name with custom delimiter' do
+ test "queue_name_prefix prepended to the queue name with custom delimiter" do
original_queue_name_prefix = ActiveJob::Base.queue_name_prefix
original_queue_name_delimiter = ActiveJob::Base.queue_name_delimiter
original_queue_name = HelloJob.queue_name
begin
- ActiveJob::Base.queue_name_delimiter = '.'
- ActiveJob::Base.queue_name_prefix = 'aj'
+ ActiveJob::Base.queue_name_delimiter = "."
+ ActiveJob::Base.queue_name_prefix = "aj"
HelloJob.queue_as :low
- assert_equal 'aj.low', HelloJob.queue_name
+ assert_equal "aj.low", HelloJob.queue_name
ensure
ActiveJob::Base.queue_name_prefix = original_queue_name_prefix
ActiveJob::Base.queue_name_delimiter = original_queue_name_delimiter
@@ -95,7 +97,7 @@ class QueueNamingTest < ActiveSupport::TestCase
end
end
- test 'uses queue passed to #set' do
+ test "uses queue passed to #set" do
job = HelloJob.set(queue: :some_queue).perform_later
assert_equal "some_queue", job.queue_name
end
diff --git a/activejob/test/cases/queue_priority_test.rb b/activejob/test/cases/queue_priority_test.rb
new file mode 100644
index 0000000000..4b3006ae81
--- /dev/null
+++ b/activejob/test/cases/queue_priority_test.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require "helper"
+require "jobs/hello_job"
+
+class QueuePriorityTest < ActiveSupport::TestCase
+ test "priority unset by default" do
+ assert_nil HelloJob.priority
+ end
+
+ test "uses given priority" do
+ original_priority = HelloJob.priority
+
+ begin
+ HelloJob.queue_with_priority 90
+ assert_equal 90, HelloJob.new.priority
+ ensure
+ HelloJob.priority = original_priority
+ end
+ end
+
+ test "evals block given to priority to determine priority" do
+ original_priority = HelloJob.priority
+
+ begin
+ HelloJob.queue_with_priority { 25 }
+ assert_equal 25, HelloJob.new.priority
+ ensure
+ HelloJob.priority = original_priority
+ end
+ end
+
+ test "can use arguments to determine priority in priority block" do
+ original_priority = HelloJob.priority
+
+ begin
+ HelloJob.queue_with_priority { arguments.first == "1" ? 99 : 11 }
+ assert_equal 99, HelloJob.new("1").priority
+ assert_equal 11, HelloJob.new("3").priority
+ ensure
+ HelloJob.priority = original_priority
+ end
+ end
+
+ test "uses priority passed to #set" do
+ job = HelloJob.set(priority: 123).perform_later
+ assert_equal 123, job.priority
+ end
+end
diff --git a/activejob/test/cases/queuing_test.rb b/activejob/test/cases/queuing_test.rb
index 0eeabbf693..0e843b7215 100644
--- a/activejob/test/cases/queuing_test.rb
+++ b/activejob/test/cases/queuing_test.rb
@@ -1,24 +1,25 @@
-require 'helper'
-require 'jobs/hello_job'
-require 'active_support/core_ext/numeric/time'
+# frozen_string_literal: true
+require "helper"
+require "jobs/hello_job"
+require "active_support/core_ext/numeric/time"
class QueuingTest < ActiveSupport::TestCase
setup do
JobBuffer.clear
end
- test 'run queued job' do
+ test "run queued job" do
HelloJob.perform_later
assert_equal "David says hello", JobBuffer.last_value
end
- test 'run queued job with arguments' do
+ test "run queued job with arguments" do
HelloJob.perform_later "Jamie"
assert_equal "Jamie says hello", JobBuffer.last_value
end
- test 'run queued job later' do
+ test "run queued job later" do
begin
result = HelloJob.set(wait_until: 1.second.ago).perform_later "Jamie"
assert result
@@ -27,13 +28,12 @@ class QueuingTest < ActiveSupport::TestCase
end
end
- test 'job returned by enqueue has the arguments available' do
+ test "job returned by enqueue has the arguments available" do
job = HelloJob.perform_later "Jamie"
assert_equal [ "Jamie" ], job.arguments
end
-
- test 'job returned by perform_at has the timestamp available' do
+ test "job returned by perform_at has the timestamp available" do
begin
job = HelloJob.set(wait_until: Time.utc(2014, 1, 1)).perform_later
assert_equal Time.utc(2014, 1, 1).to_f, job.scheduled_at
diff --git a/activejob/test/cases/rescue_test.rb b/activejob/test/cases/rescue_test.rb
index 58c9ca8992..da9c87dbf0 100644
--- a/activejob/test/cases/rescue_test.rb
+++ b/activejob/test/cases/rescue_test.rb
@@ -1,34 +1,36 @@
-require 'helper'
-require 'jobs/rescue_job'
-require 'models/person'
+# frozen_string_literal: true
+
+require "helper"
+require "jobs/rescue_job"
+require "models/person"
class RescueTest < ActiveSupport::TestCase
setup do
JobBuffer.clear
end
- test 'rescue perform exception with retry' do
+ test "rescue perform exception with retry" do
job = RescueJob.new("david")
job.perform_now
assert_equal [ "rescued from ArgumentError", "performed beautifully" ], JobBuffer.values
end
- test 'let through unhandled perform exception' do
+ test "let through unhandled perform exception" do
job = RescueJob.new("other")
assert_raises(RescueJob::OtherError) do
job.perform_now
end
end
- test 'rescue from deserialization errors' do
+ test "rescue from deserialization errors" do
RescueJob.perform_later Person.new(404)
- assert_includes JobBuffer.values, 'rescued from DeserializationError'
- assert_includes JobBuffer.values, 'DeserializationError original exception was Person::RecordNotFound'
- assert_not_includes JobBuffer.values, 'performed beautifully'
+ assert_includes JobBuffer.values, "rescued from DeserializationError"
+ assert_includes JobBuffer.values, "DeserializationError original exception was Person::RecordNotFound"
+ assert_not_includes JobBuffer.values, "performed beautifully"
end
test "should not wrap DeserializationError in DeserializationError" do
RescueJob.perform_later [Person.new(404)]
- assert_includes JobBuffer.values, 'DeserializationError original exception was Person::RecordNotFound'
+ assert_includes JobBuffer.values, "DeserializationError original exception was Person::RecordNotFound"
end
end
diff --git a/activejob/test/cases/serializers_test.rb b/activejob/test/cases/serializers_test.rb
new file mode 100644
index 0000000000..bee0c061bd
--- /dev/null
+++ b/activejob/test/cases/serializers_test.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+require "helper"
+require "active_job/serializers"
+
+class SerializersTest < ActiveSupport::TestCase
+ class DummyValueObject
+ attr_accessor :value
+
+ def initialize(value)
+ @value = value
+ end
+
+ def ==(other)
+ self.value == other.value
+ end
+ end
+
+ class DummySerializer < ActiveJob::Serializers::ObjectSerializer
+ def serialize(object)
+ super({ "value" => object.value })
+ end
+
+ def deserialize(hash)
+ DummyValueObject.new(hash["value"])
+ end
+
+ private
+
+ def klass
+ DummyValueObject
+ end
+ end
+
+ setup do
+ @value_object = DummyValueObject.new 123
+ @original_serializers = ActiveJob::Serializers.serializers
+ end
+
+ teardown do
+ ActiveJob::Serializers._additional_serializers = @original_serializers
+ end
+
+ test "can't serialize unknown object" do
+ assert_raises ActiveJob::SerializationError do
+ ActiveJob::Serializers.serialize @value_object
+ end
+ end
+
+ test "will serialize objects with serializers registered" do
+ ActiveJob::Serializers.add_serializers DummySerializer
+
+ assert_equal(
+ { "_aj_serialized" => "SerializersTest::DummySerializer", "value" => 123 },
+ ActiveJob::Serializers.serialize(@value_object)
+ )
+ end
+
+ test "won't deserialize unknown hash" do
+ hash = { "_dummy_serializer" => 123, "_aj_symbol_keys" => [] }
+ error = assert_raises(ArgumentError) do
+ ActiveJob::Serializers.deserialize(hash)
+ end
+ assert_equal(
+ 'Serializer name is not present in the argument: {"_dummy_serializer"=>123, "_aj_symbol_keys"=>[]}',
+ error.message
+ )
+ end
+
+ test "won't deserialize unknown serializer" do
+ hash = { "_aj_serialized" => "DoNotExist", "value" => 123 }
+ error = assert_raises(ArgumentError) do
+ ActiveJob::Serializers.deserialize(hash)
+ end
+ assert_equal(
+ "Serializer DoNotExist is not known",
+ error.message
+ )
+ end
+
+ test "will deserialize know serialized objects" do
+ ActiveJob::Serializers.add_serializers DummySerializer
+ hash = { "_aj_serialized" => "SerializersTest::DummySerializer", "value" => 123 }
+ assert_equal DummyValueObject.new(123), ActiveJob::Serializers.deserialize(hash)
+ end
+
+ test "adds new serializer" do
+ ActiveJob::Serializers.add_serializers DummySerializer
+ assert ActiveJob::Serializers.serializers.include?(DummySerializer)
+ end
+
+ test "can't add serializer with the same key twice" do
+ ActiveJob::Serializers.add_serializers DummySerializer
+ assert_no_difference(-> { ActiveJob::Serializers.serializers.size }) do
+ ActiveJob::Serializers.add_serializers DummySerializer
+ end
+ end
+end
diff --git a/activejob/test/cases/test_case_test.rb b/activejob/test/cases/test_case_test.rb
index ee816e1dd5..4ae2add3a8 100644
--- a/activejob/test/cases/test_case_test.rb
+++ b/activejob/test/cases/test_case_test.rb
@@ -1,7 +1,9 @@
-require 'helper'
-require 'jobs/hello_job'
-require 'jobs/logging_job'
-require 'jobs/nested_job'
+# frozen_string_literal: true
+
+require "helper"
+require "jobs/hello_job"
+require "jobs/logging_job"
+require "jobs/nested_job"
class ActiveJobTestCaseTest < ActiveJob::TestCase
# this tests that this job class doesn't get its adapter set.
@@ -9,7 +11,7 @@ class ActiveJobTestCaseTest < ActiveJob::TestCase
# the `class_attribute` inheritance
class TestClassAttributeInheritanceJob < ActiveJob::Base
def self.queue_adapter=(*)
- raise 'Attemping to break `class_attribute` inheritance, bad!'
+ raise "Attempting to break `class_attribute` inheritance, bad!"
end
end
@@ -18,6 +20,6 @@ class ActiveJobTestCaseTest < ActiveJob::TestCase
end
def test_set_test_adapter
- assert_kind_of ActiveJob::QueueAdapters::TestAdapter, self.queue_adapter
+ assert_kind_of ActiveJob::QueueAdapters::TestAdapter, queue_adapter
end
end
diff --git a/activejob/test/cases/test_helper_test.rb b/activejob/test/cases/test_helper_test.rb
index 04c4c446e2..8dc32037ff 100644
--- a/activejob/test/cases/test_helper_test.rb
+++ b/activejob/test/cases/test_helper_test.rb
@@ -1,17 +1,21 @@
-require 'helper'
-require 'active_support/core_ext/time'
-require 'active_support/core_ext/date'
-require 'jobs/hello_job'
-require 'jobs/logging_job'
-require 'jobs/nested_job'
-require 'jobs/rescue_job'
-require 'models/person'
+# frozen_string_literal: true
+
+require "helper"
+require "active_support/core_ext/time"
+require "active_support/core_ext/date"
+require "jobs/hello_job"
+require "jobs/logging_job"
+require "jobs/nested_job"
+require "jobs/rescue_job"
+require "jobs/inherited_job"
+require "jobs/multiple_kwargs_job"
+require "models/person"
class EnqueuedJobsTest < ActiveJob::TestCase
def test_assert_enqueued_jobs
assert_nothing_raised do
assert_enqueued_jobs 1 do
- HelloJob.perform_later('david')
+ HelloJob.perform_later("david")
end
end
end
@@ -19,23 +23,23 @@ class EnqueuedJobsTest < ActiveJob::TestCase
def test_repeated_enqueued_jobs_calls
assert_nothing_raised do
assert_enqueued_jobs 1 do
- HelloJob.perform_later('abdelkader')
+ HelloJob.perform_later("abdelkader")
end
end
assert_nothing_raised do
assert_enqueued_jobs 2 do
- HelloJob.perform_later('sean')
- HelloJob.perform_later('yves')
+ HelloJob.perform_later("sean")
+ HelloJob.perform_later("yves")
end
end
end
def test_assert_enqueued_jobs_message
- HelloJob.perform_later('sean')
+ HelloJob.perform_later("sean")
e = assert_raises Minitest::Assertion do
assert_enqueued_jobs 2 do
- HelloJob.perform_later('sean')
+ HelloJob.perform_later("sean")
end
end
assert_match "Expected: 2", e.message
@@ -44,13 +48,13 @@ class EnqueuedJobsTest < ActiveJob::TestCase
def test_assert_enqueued_jobs_with_no_block
assert_nothing_raised do
- HelloJob.perform_later('rafael')
+ HelloJob.perform_later("rafael")
assert_enqueued_jobs 1
end
assert_nothing_raised do
- HelloJob.perform_later('aaron')
- HelloJob.perform_later('matthew')
+ HelloJob.perform_later("aaron")
+ HelloJob.perform_later("matthew")
assert_enqueued_jobs 3
end
end
@@ -72,7 +76,7 @@ class EnqueuedJobsTest < ActiveJob::TestCase
def test_assert_enqueued_jobs_too_few_sent
error = assert_raise ActiveSupport::TestCase::Assertion do
assert_enqueued_jobs 2 do
- HelloJob.perform_later('xavier')
+ HelloJob.perform_later("xavier")
end
end
@@ -82,8 +86,8 @@ class EnqueuedJobsTest < ActiveJob::TestCase
def test_assert_enqueued_jobs_too_many_sent
error = assert_raise ActiveSupport::TestCase::Assertion do
assert_enqueued_jobs 1 do
- HelloJob.perform_later('cristian')
- HelloJob.perform_later('guillermo')
+ HelloJob.perform_later("cristian")
+ HelloJob.perform_later("guillermo")
end
end
@@ -93,7 +97,7 @@ class EnqueuedJobsTest < ActiveJob::TestCase
def test_assert_no_enqueued_jobs_failure
error = assert_raise ActiveSupport::TestCase::Assertion do
assert_no_enqueued_jobs do
- HelloJob.perform_later('jeremy')
+ HelloJob.perform_later("jeremy")
end
end
@@ -103,8 +107,74 @@ class EnqueuedJobsTest < ActiveJob::TestCase
def test_assert_enqueued_jobs_with_only_option
assert_nothing_raised do
assert_enqueued_jobs 1, only: HelloJob do
- HelloJob.perform_later('jeremy')
+ HelloJob.perform_later("jeremy")
LoggingJob.perform_later
+ LoggingJob.perform_later
+ end
+ end
+ end
+
+ def test_assert_enqueued_jobs_with_except_option
+ assert_nothing_raised do
+ assert_enqueued_jobs 1, except: LoggingJob do
+ HelloJob.perform_later("jeremy")
+ LoggingJob.perform_later
+ LoggingJob.perform_later
+ end
+ end
+ end
+
+ def test_assert_enqueued_jobs_with_only_and_except_option
+ error = assert_raise ArgumentError do
+ assert_enqueued_jobs 1, only: HelloJob, except: HelloJob do
+ HelloJob.perform_later("jeremy")
+ LoggingJob.perform_later
+ LoggingJob.perform_later
+ end
+ end
+
+ assert_match(/`:only` and `:except`/, error.message)
+ 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_except_and_queue_option
+ assert_nothing_raised do
+ assert_enqueued_jobs 1, except: LoggingJob, 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_only_and_except_and_queue_option
+ error = assert_raise ArgumentError do
+ assert_enqueued_jobs 1, only: HelloJob, except: HelloJob, queue: :some_queue do
+ HelloJob.set(queue: :some_queue).perform_later
+ HelloJob.set(queue: :other_queue).perform_later
+ LoggingJob.perform_later
+ end
+ end
+
+ assert_match(/`:only` and `:except`/, error.message)
+ 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
@@ -119,10 +189,41 @@ class EnqueuedJobsTest < ActiveJob::TestCase
assert_match(/1 .* but 0/, error.message)
end
+ def test_assert_enqueued_jobs_with_except_option_and_none_sent
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_enqueued_jobs 1, except: LoggingJob do
+ LoggingJob.perform_later
+ end
+ end
+
+ assert_match(/1 .* but 0/, error.message)
+ end
+
+ def test_assert_enqueued_jobs_with_only_and_except_option_and_none_sent
+ error = assert_raise ArgumentError do
+ assert_enqueued_jobs 1, only: HelloJob, except: HelloJob do
+ LoggingJob.perform_later
+ end
+ end
+
+ assert_match(/`:only` and `:except`/, error.message)
+ end
+
def test_assert_enqueued_jobs_with_only_option_and_too_few_sent
error = assert_raise ActiveSupport::TestCase::Assertion do
assert_enqueued_jobs 5, only: HelloJob do
- HelloJob.perform_later('jeremy')
+ HelloJob.perform_later("jeremy")
+ 4.times { LoggingJob.perform_later }
+ end
+ end
+
+ assert_match(/5 .* but 1/, error.message)
+ end
+
+ def test_assert_enqueued_jobs_with_except_option_and_too_few_sent
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_enqueued_jobs 5, except: LoggingJob do
+ HelloJob.perform_later("jeremy")
4.times { LoggingJob.perform_later }
end
end
@@ -130,16 +231,79 @@ class EnqueuedJobsTest < ActiveJob::TestCase
assert_match(/5 .* but 1/, error.message)
end
+ def test_assert_enqueued_jobs_with_only_and_except_option_and_too_few_sent
+ error = assert_raise ArgumentError do
+ assert_enqueued_jobs 5, only: HelloJob, except: HelloJob do
+ HelloJob.perform_later("jeremy")
+ 4.times { LoggingJob.perform_later }
+ end
+ end
+
+ assert_match(/`:only` and `:except`/, error.message)
+ end
+
def test_assert_enqueued_jobs_with_only_option_and_too_many_sent
error = assert_raise ActiveSupport::TestCase::Assertion do
assert_enqueued_jobs 1, only: HelloJob do
- 2.times { HelloJob.perform_later('jeremy') }
+ 2.times { HelloJob.perform_later("jeremy") }
end
end
assert_match(/1 .* but 2/, error.message)
end
+ def test_assert_enqueued_jobs_with_except_option_and_too_many_sent
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_enqueued_jobs 1, except: LoggingJob do
+ 2.times { HelloJob.perform_later("jeremy") }
+ end
+ end
+
+ assert_match(/1 .* but 2/, error.message)
+ end
+
+ def test_assert_enqueued_jobs_with_only_and_except_option_and_too_many_sent
+ error = assert_raise ArgumentError do
+ assert_enqueued_jobs 1, only: HelloJob, except: HelloJob do
+ 2.times { HelloJob.perform_later("jeremy") }
+ end
+ end
+
+ assert_match(/`:only` and `:except`/, error.message)
+ end
+
+ def test_assert_enqueued_jobs_with_only_option_as_array
+ assert_nothing_raised do
+ assert_enqueued_jobs 2, only: [HelloJob, LoggingJob] do
+ HelloJob.perform_later("jeremy")
+ LoggingJob.perform_later("stewie")
+ RescueJob.perform_later("david")
+ end
+ end
+ end
+
+ def test_assert_enqueued_jobs_with_except_option_as_array
+ assert_nothing_raised do
+ assert_enqueued_jobs 1, except: [HelloJob, LoggingJob] do
+ HelloJob.perform_later("jeremy")
+ LoggingJob.perform_later("stewie")
+ RescueJob.perform_later("david")
+ end
+ end
+ end
+
+ def test_assert_enqueued_jobs_with_only_and_except_option_as_array
+ error = assert_raise ArgumentError do
+ assert_enqueued_jobs 2, only: [HelloJob, LoggingJob], except: [HelloJob, LoggingJob] do
+ HelloJob.perform_later("jeremy")
+ LoggingJob.perform_later("stewie")
+ RescueJob.perform_later("david")
+ end
+ end
+
+ assert_match(/`:only` and `:except`/, error.message)
+ end
+
def test_assert_no_enqueued_jobs_with_only_option
assert_nothing_raised do
assert_no_enqueued_jobs only: HelloJob do
@@ -148,32 +312,203 @@ class EnqueuedJobsTest < ActiveJob::TestCase
end
end
+ def test_assert_no_enqueued_jobs_with_except_option
+ assert_nothing_raised do
+ assert_no_enqueued_jobs except: LoggingJob do
+ LoggingJob.perform_later
+ end
+ end
+ end
+
+ def test_assert_no_enqueued_jobs_with_only_and_except_option
+ error = assert_raise ArgumentError do
+ assert_no_enqueued_jobs only: HelloJob, except: HelloJob do
+ LoggingJob.perform_later
+ end
+ end
+
+ assert_match(/`:only` and `:except`/, error.message)
+ end
+
def test_assert_no_enqueued_jobs_with_only_option_failure
error = assert_raise ActiveSupport::TestCase::Assertion do
assert_no_enqueued_jobs only: HelloJob do
- HelloJob.perform_later('jeremy')
+ HelloJob.perform_later("jeremy")
+ LoggingJob.perform_later
+ end
+ end
+
+ assert_match(/0 .* but 1/, error.message)
+ end
+
+ def test_assert_no_enqueued_jobs_with_except_option_failure
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_no_enqueued_jobs except: LoggingJob do
+ HelloJob.perform_later("jeremy")
+ LoggingJob.perform_later
+ end
+ end
+
+ assert_match(/0 .* but 1/, error.message)
+ end
+
+ def test_assert_no_enqueued_jobs_with_only_and_except_option_failure
+ error = assert_raise ArgumentError do
+ assert_no_enqueued_jobs only: HelloJob, except: HelloJob do
+ HelloJob.perform_later("jeremy")
+ LoggingJob.perform_later
+ end
+ end
+
+ assert_match(/`:only` and `:except`/, error.message)
+ end
+
+ def test_assert_no_enqueued_jobs_with_only_option_as_array
+ assert_nothing_raised do
+ assert_no_enqueued_jobs only: [HelloJob, RescueJob] do
+ LoggingJob.perform_later
+ end
+ end
+ end
+
+ def test_assert_no_enqueued_jobs_with_except_option_as_array
+ assert_nothing_raised do
+ assert_no_enqueued_jobs except: [HelloJob, RescueJob] do
+ HelloJob.perform_later
+ RescueJob.perform_later
+ end
+ end
+ end
+
+ def test_assert_no_enqueued_jobs_with_only_and_except_option_as_array
+ error = assert_raise ArgumentError do
+ assert_no_enqueued_jobs only: [HelloJob, RescueJob], except: [HelloJob, RescueJob] do
LoggingJob.perform_later
end
end
+ assert_match(/`:only` and `:except`/, error.message)
+ end
+
+ def test_assert_no_enqueued_jobs_with_queue_option
+ assert_nothing_raised do
+ assert_no_enqueued_jobs queue: :default do
+ HelloJob.set(queue: :other_queue).perform_later
+ LoggingJob.set(queue: :other_queue).perform_later
+ end
+ end
+ end
+
+ def test_assert_no_enqueued_jobs_with_queue_option_failure
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_no_enqueued_jobs queue: :other_queue do
+ HelloJob.set(queue: :other_queue).perform_later
+ end
+ end
+
+ assert_match(/0 .* but 1/, error.message)
+ end
+
+ def test_assert_no_enqueued_jobs_with_only_and_queue_option
+ assert_nothing_raised do
+ assert_no_enqueued_jobs only: HelloJob, queue: :some_queue do
+ HelloJob.set(queue: :other_queue).perform_later
+ HelloJob.set(queue: :other_queue).perform_later
+ LoggingJob.set(queue: :some_queue).perform_later
+ end
+ end
+ end
+
+ def test_assert_no_enqueued_jobs_with_only_and_queue_option_failure
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_no_enqueued_jobs only: HelloJob, queue: :some_queue do
+ HelloJob.set(queue: :other_queue).perform_later
+ HelloJob.set(queue: :some_queue).perform_later
+ LoggingJob.set(queue: :some_queue).perform_later
+ end
+ end
+
+ assert_match(/0 .* but 1/, error.message)
+ end
+
+ def test_assert_no_enqueued_jobs_with_except_and_queue_option
+ assert_nothing_raised do
+ assert_no_enqueued_jobs except: LoggingJob, queue: :some_queue do
+ HelloJob.set(queue: :other_queue).perform_later
+ HelloJob.set(queue: :other_queue).perform_later
+ LoggingJob.set(queue: :some_queue).perform_later
+ end
+ end
+ end
+
+ def test_assert_no_enqueued_jobs_with_except_and_queue_option_failure
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_no_enqueued_jobs except: LoggingJob, queue: :some_queue do
+ HelloJob.set(queue: :other_queue).perform_later
+ HelloJob.set(queue: :some_queue).perform_later
+ LoggingJob.set(queue: :some_queue).perform_later
+ end
+ end
+
assert_match(/0 .* but 1/, error.message)
end
- def test_assert_enqueued_job
- assert_enqueued_with(job: LoggingJob, queue: 'default') do
+ def test_assert_no_enqueued_jobs_with_only_and_except_and_queue_option
+ error = assert_raise ArgumentError do
+ assert_no_enqueued_jobs only: HelloJob, except: HelloJob, queue: :some_queue do
+ HelloJob.set(queue: :other_queue).perform_later
+ end
+ end
+
+ assert_match(/`:only` and `:except`/, error.message)
+ end
+
+ def test_assert_enqueued_with
+ assert_enqueued_with(job: LoggingJob, queue: "default") do
LoggingJob.set(wait_until: Date.tomorrow.noon).perform_later
end
end
- def test_assert_enqueued_job_failure
+ def test_assert_enqueued_with_with_no_block
+ LoggingJob.set(wait_until: Date.tomorrow.noon).perform_later
+ assert_enqueued_with(job: LoggingJob, queue: "default")
+ end
+
+ def test_assert_enqueued_with_returns
+ job = assert_enqueued_with(job: LoggingJob) do
+ LoggingJob.set(wait_until: 5.minutes.from_now).perform_later(1, 2, 3, keyword: true)
+ end
+
+ assert_instance_of LoggingJob, job
+ assert_in_delta 5.minutes.from_now, job.scheduled_at, 1
+ assert_equal "default", job.queue_name
+ assert_equal [1, 2, 3, { keyword: true }], job.arguments
+ end
+
+ def test_assert_enqueued_with_with_no_block_returns
+ LoggingJob.set(wait_until: 5.minutes.from_now).perform_later(1, 2, 3, keyword: true)
+ job = assert_enqueued_with(job: LoggingJob)
+
+ assert_instance_of LoggingJob, job
+ assert_in_delta 5.minutes.from_now, job.scheduled_at, 1
+ assert_equal "default", job.queue_name
+ assert_equal [1, 2, 3, { keyword: true }], job.arguments
+ end
+
+ def test_assert_enqueued_with_failure
assert_raise ActiveSupport::TestCase::Assertion do
- assert_enqueued_with(job: LoggingJob, queue: 'default') do
+ assert_enqueued_with(job: LoggingJob, queue: "default") do
NestedJob.perform_later
end
end
+ assert_raise ActiveSupport::TestCase::Assertion do
+ LoggingJob.perform_later
+ assert_enqueued_with(job: LoggingJob) { }
+ end
+
error = assert_raise ActiveSupport::TestCase::Assertion do
- assert_enqueued_with(job: NestedJob, queue: 'low') do
+ assert_enqueued_with(job: NestedJob, queue: "low") do
NestedJob.perform_later
end
end
@@ -181,7 +516,21 @@ class EnqueuedJobsTest < ActiveJob::TestCase
assert_equal 'No enqueued job found with {:job=>NestedJob, :queue=>"low"}', error.message
end
- def test_assert_enqueued_job_args
+ def test_assert_enqueued_with_with_no_block_failure
+ assert_raise ActiveSupport::TestCase::Assertion do
+ NestedJob.perform_later
+ assert_enqueued_with(job: LoggingJob, queue: "default")
+ end
+
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ NestedJob.perform_later
+ assert_enqueued_with(job: NestedJob, queue: "low")
+ end
+
+ assert_equal 'No enqueued job found with {:job=>NestedJob, :queue=>"low"}', error.message
+ end
+
+ def test_assert_enqueued_with_args
assert_raise ArgumentError do
assert_enqueued_with(class: LoggingJob) do
NestedJob.set(wait_until: Date.tomorrow.noon).perform_later
@@ -189,14 +538,67 @@ class EnqueuedJobsTest < ActiveJob::TestCase
end
end
- def test_assert_enqueued_job_with_global_id_args
+ def test_assert_enqueued_with_selective_args
+ args = ->(job_args) do
+ assert_equal 1, job_args.first[:argument1]
+ assert job_args.first[:argument2].key?(:b)
+ end
+
+ assert_enqueued_with(job: MultipleKwargsJob, args: args) do
+ MultipleKwargsJob.perform_later(argument2: { b: 2, a: 1 }, argument1: 1)
+ end
+ end
+
+ def test_assert_enqueued_with_selective_args_fails
+ args = ->(job_args) do
+ false
+ end
+
+ assert_raise ActiveSupport::TestCase::Assertion do
+ assert_enqueued_with(job: MultipleKwargsJob, args: args) do
+ MultipleKwargsJob.perform_later(argument2: { b: 2, a: 1 }, argument1: 1)
+ end
+ end
+ end
+
+ def test_assert_enqueued_with_with_no_block_args
+ assert_raise ArgumentError do
+ NestedJob.set(wait_until: Date.tomorrow.noon).perform_later
+ assert_enqueued_with(class: LoggingJob)
+ end
+ end
+
+ def test_assert_enqueued_with_with_at_option
+ assert_enqueued_with(job: HelloJob, at: Date.tomorrow.noon) do
+ HelloJob.set(wait_until: Date.tomorrow.noon).perform_later
+ end
+ end
+
+ def test_assert_enqueued_with_with_no_block_with_at_option
+ HelloJob.set(wait_until: Date.tomorrow.noon).perform_later
+ assert_enqueued_with(job: HelloJob, at: Date.tomorrow.noon)
+ end
+
+ def test_assert_enqueued_with_with_hash_arg
+ assert_enqueued_with(job: MultipleKwargsJob, args: [{ argument1: 1, argument2: { a: 1, b: 2 } }]) do
+ MultipleKwargsJob.perform_later(argument2: { b: 2, a: 1 }, argument1: 1)
+ end
+ end
+
+ def test_assert_enqueued_with_with_global_id_args
ricardo = Person.new(9)
assert_enqueued_with(job: HelloJob, args: [ricardo]) do
HelloJob.perform_later(ricardo)
end
end
- def test_assert_enqueued_job_failure_with_global_id_args
+ def test_assert_enqueued_with_with_no_block_with_global_id_args
+ ricardo = Person.new(9)
+ HelloJob.perform_later(ricardo)
+ assert_enqueued_with(job: HelloJob, args: [ricardo])
+ end
+
+ def test_assert_enqueued_with_failure_with_global_id_args
ricardo = Person.new(9)
wilma = Person.new(11)
error = assert_raise ActiveSupport::TestCase::Assertion do
@@ -207,21 +609,207 @@ class EnqueuedJobsTest < ActiveJob::TestCase
assert_equal "No enqueued job found with {:job=>HelloJob, :args=>[#{wilma.inspect}]}", error.message
end
+
+ def test_assert_enqueued_with_failure_with_no_block_with_global_id_args
+ ricardo = Person.new(9)
+ wilma = Person.new(11)
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ HelloJob.perform_later(ricardo)
+ assert_enqueued_with(job: HelloJob, args: [wilma])
+ end
+
+ assert_equal "No enqueued job found with {:job=>HelloJob, :args=>[#{wilma.inspect}]}", error.message
+ end
+
+ def test_assert_enqueued_with_does_not_change_jobs_count
+ HelloJob.perform_later
+ assert_enqueued_with(job: HelloJob) do
+ HelloJob.perform_later
+ end
+
+ assert_equal 2, queue_adapter.enqueued_jobs.count
+ end
+
+ def test_assert_enqueued_with_with_no_block_does_not_change_jobs_count
+ HelloJob.perform_later
+ HelloJob.perform_later
+ assert_enqueued_with(job: HelloJob)
+
+ assert_equal 2, queue_adapter.enqueued_jobs.count
+ end
end
class PerformedJobsTest < ActiveJob::TestCase
- def test_performed_enqueue_jobs_with_only_option_doesnt_leak_outside_the_block
- assert_equal nil, queue_adapter.filter
+ def test_perform_enqueued_jobs_with_only_option_doesnt_leak_outside_the_block
+ 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_perform_enqueued_jobs_without_block_with_only_option_doesnt_leak
+ perform_enqueued_jobs only: HelloJob
+
+ assert_nil queue_adapter.filter
+ end
+
+ def test_perform_enqueued_jobs_with_except_option_doesnt_leak_outside_the_block
+ assert_nil queue_adapter.reject
+ perform_enqueued_jobs except: HelloJob do
+ assert_equal HelloJob, queue_adapter.reject
+ end
+ assert_nil queue_adapter.reject
+ end
+
+ def test_perform_enqueued_jobs_without_block_with_except_option_doesnt_leak
+ perform_enqueued_jobs except: HelloJob
+
+ assert_nil queue_adapter.reject
+ end
+
+ def test_perform_enqueued_jobs_with_queue_option_doesnt_leak_outside_the_block
+ assert_nil queue_adapter.queue
+ perform_enqueued_jobs queue: :some_queue do
+ assert_equal :some_queue, queue_adapter.queue
+ end
+ assert_nil queue_adapter.queue
+ end
+
+ def test_perform_enqueued_jobs_without_block_with_queue_option_doesnt_leak
+ perform_enqueued_jobs queue: :some_queue
+
+ assert_nil queue_adapter.reject
+ end
+
+ def test_perform_enqueued_jobs_with_block
+ perform_enqueued_jobs do
+ HelloJob.perform_later("kevin")
+ LoggingJob.perform_later("bogdan")
+ end
+
+ assert_performed_jobs 2
+ end
+
+ def test_perform_enqueued_jobs_without_block
+ HelloJob.perform_later("kevin")
+ LoggingJob.perform_later("bogdan")
+
+ perform_enqueued_jobs
+
+ assert_performed_jobs 2
+ end
+
+ def test_perform_enqueued_jobs_with_block_with_only_option
+ perform_enqueued_jobs only: LoggingJob do
+ HelloJob.perform_later("kevin")
+ LoggingJob.perform_later("bogdan")
+ end
+
+ assert_performed_jobs 1
+ assert_performed_jobs 1, only: LoggingJob
+ end
+
+ def test_perform_enqueued_jobs_without_block_with_only_option
+ HelloJob.perform_later("kevin")
+ LoggingJob.perform_later("bogdan")
+
+ perform_enqueued_jobs only: LoggingJob
+
+ assert_performed_jobs 1
+ assert_performed_jobs 1, only: LoggingJob
+ end
+
+ def test_perform_enqueued_jobs_with_block_with_except_option
+ perform_enqueued_jobs except: HelloJob do
+ HelloJob.perform_later("kevin")
+ LoggingJob.perform_later("bogdan")
+ end
+
+ assert_performed_jobs 1
+ assert_performed_jobs 1, only: LoggingJob
+ end
+
+ def test_perform_enqueued_jobs_without_block_with_except_option
+ HelloJob.perform_later("kevin")
+ LoggingJob.perform_later("bogdan")
+
+ perform_enqueued_jobs except: HelloJob
+
+ assert_performed_jobs 1
+ assert_performed_jobs 1, only: LoggingJob
+ end
+
+ def test_perform_enqueued_jobs_with_block_with_queue_option
+ perform_enqueued_jobs queue: :some_queue do
+ HelloJob.set(queue: :some_queue).perform_later("kevin")
+ HelloJob.set(queue: :other_queue).perform_later("bogdan")
+ LoggingJob.perform_later("bogdan")
+ end
+
+ assert_performed_jobs 1
+ assert_performed_jobs 1, only: HelloJob, queue: :some_queue
+ end
+
+ def test_perform_enqueued_jobs_without_block_with_queue_option
+ HelloJob.set(queue: :some_queue).perform_later("kevin")
+ HelloJob.set(queue: :other_queue).perform_later("bogdan")
+ LoggingJob.perform_later("bogdan")
+
+ perform_enqueued_jobs queue: :some_queue
+
+ assert_performed_jobs 1
+ assert_performed_jobs 1, only: HelloJob, queue: :some_queue
+ end
+
+ def test_perform_enqueued_jobs_with_block_with_only_and_queue_options
+ perform_enqueued_jobs only: HelloJob, queue: :other_queue do
+ HelloJob.set(queue: :some_queue).perform_later("kevin")
+ HelloJob.set(queue: :other_queue).perform_later("bogdan")
+ LoggingJob.set(queue: :other_queue).perform_later("bogdan")
+ end
+
+ assert_performed_jobs 1
+ assert_performed_jobs 1, only: HelloJob, queue: :other_queue
+ end
+
+ def test_perform_enqueued_jobs_without_block_with_only_and_queue_options
+ HelloJob.set(queue: :some_queue).perform_later("kevin")
+ HelloJob.set(queue: :other_queue).perform_later("bogdan")
+ LoggingJob.set(queue: :other_queue).perform_later("bogdan")
+
+ perform_enqueued_jobs only: HelloJob, queue: :other_queue
+
+ assert_performed_jobs 1
+ assert_performed_jobs 1, only: HelloJob, queue: :other_queue
+ end
+
+ def test_perform_enqueued_jobs_with_block_with_except_and_queue_options
+ perform_enqueued_jobs except: HelloJob, queue: :other_queue do
+ HelloJob.set(queue: :other_queue).perform_later("kevin")
+ LoggingJob.set(queue: :some_queue).perform_later("bogdan")
+ LoggingJob.set(queue: :other_queue).perform_later("bogdan")
+ end
+
+ assert_performed_jobs 1
+ assert_performed_jobs 1, only: LoggingJob, queue: :other_queue
+ end
+
+ def test_perform_enqueued_jobs_without_block_with_except_and_queue_options
+ HelloJob.set(queue: :other_queue).perform_later("kevin")
+ LoggingJob.set(queue: :some_queue).perform_later("bogdan")
+ LoggingJob.set(queue: :other_queue).perform_later("bogdan")
+
+ perform_enqueued_jobs except: HelloJob, queue: :other_queue
+
+ assert_performed_jobs 1
+ assert_performed_jobs 1, only: LoggingJob, queue: :other_queue
end
def test_assert_performed_jobs
assert_nothing_raised do
assert_performed_jobs 1 do
- HelloJob.perform_later('david')
+ HelloJob.perform_later("david")
end
end
end
@@ -229,23 +817,23 @@ class PerformedJobsTest < ActiveJob::TestCase
def test_repeated_performed_jobs_calls
assert_nothing_raised do
assert_performed_jobs 1 do
- HelloJob.perform_later('abdelkader')
+ HelloJob.perform_later("abdelkader")
end
end
assert_nothing_raised do
assert_performed_jobs 2 do
- HelloJob.perform_later('sean')
- HelloJob.perform_later('yves')
+ HelloJob.perform_later("sean")
+ HelloJob.perform_later("yves")
end
end
end
def test_assert_performed_jobs_message
- HelloJob.perform_later('sean')
+ HelloJob.perform_later("sean")
e = assert_raises Minitest::Assertion do
assert_performed_jobs 2 do
- HelloJob.perform_later('sean')
+ HelloJob.perform_later("sean")
end
end
assert_match "Expected: 2", e.message
@@ -255,15 +843,15 @@ class PerformedJobsTest < ActiveJob::TestCase
def test_assert_performed_jobs_with_no_block
assert_nothing_raised do
perform_enqueued_jobs do
- HelloJob.perform_later('rafael')
+ HelloJob.perform_later("rafael")
end
assert_performed_jobs 1
end
assert_nothing_raised do
perform_enqueued_jobs do
- HelloJob.perform_later('aaron')
- HelloJob.perform_later('matthew')
+ HelloJob.perform_later("aaron")
+ HelloJob.perform_later("matthew")
assert_performed_jobs 3
end
end
@@ -286,7 +874,7 @@ class PerformedJobsTest < ActiveJob::TestCase
def test_assert_performed_jobs_too_few_sent
error = assert_raise ActiveSupport::TestCase::Assertion do
assert_performed_jobs 2 do
- HelloJob.perform_later('xavier')
+ HelloJob.perform_later("xavier")
end
end
@@ -296,8 +884,8 @@ class PerformedJobsTest < ActiveJob::TestCase
def test_assert_performed_jobs_too_many_sent
error = assert_raise ActiveSupport::TestCase::Assertion do
assert_performed_jobs 1 do
- HelloJob.perform_later('cristian')
- HelloJob.perform_later('guillermo')
+ HelloJob.perform_later("cristian")
+ HelloJob.perform_later("guillermo")
end
end
@@ -307,7 +895,7 @@ class PerformedJobsTest < ActiveJob::TestCase
def test_assert_no_performed_jobs_failure
error = assert_raise ActiveSupport::TestCase::Assertion do
assert_no_performed_jobs do
- HelloJob.perform_later('jeremy')
+ HelloJob.perform_later("jeremy")
end
end
@@ -317,20 +905,119 @@ class PerformedJobsTest < ActiveJob::TestCase
def test_assert_performed_jobs_with_only_option
assert_nothing_raised do
assert_performed_jobs 1, only: HelloJob do
- HelloJob.perform_later('jeremy')
+ HelloJob.perform_later("jeremy")
LoggingJob.perform_later
end
end
end
+ def test_assert_performed_jobs_without_block_with_only_option
+ HelloJob.perform_later("jeremy")
+ LoggingJob.perform_later("bogdan")
+
+ perform_enqueued_jobs
+
+ assert_performed_jobs 1, only: HelloJob
+ end
+
+ def test_assert_performed_jobs_without_block_with_only_option_failure
+ LoggingJob.perform_later("jeremy")
+ LoggingJob.perform_later("bogdan")
+
+ perform_enqueued_jobs
+
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_performed_jobs 1, only: HelloJob
+ end
+
+ assert_match(/1 .* but 0/, error.message)
+ end
+
+ def test_assert_performed_jobs_with_except_option
+ assert_nothing_raised do
+ assert_performed_jobs 1, except: LoggingJob do
+ HelloJob.perform_later("jeremy")
+ LoggingJob.perform_later
+ end
+ end
+ end
+
+ def test_assert_performed_jobs_without_block_with_except_option
+ HelloJob.perform_later("jeremy")
+ LoggingJob.perform_later("bogdan")
+
+ perform_enqueued_jobs
+
+ assert_performed_jobs 1, except: HelloJob
+ end
+
+ def test_assert_performed_jobs_without_block_with_except_option_failure
+ HelloJob.perform_later("jeremy")
+ HelloJob.perform_later("bogdan")
+
+ perform_enqueued_jobs
+
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_performed_jobs 1, except: HelloJob
+ end
+
+ assert_match(/1 .* but 0/, error.message)
+ end
+
+ def test_assert_performed_jobs_with_only_and_except_option
+ error = assert_raise ArgumentError do
+ assert_performed_jobs 1, only: HelloJob, except: HelloJob do
+ HelloJob.perform_later("jeremy")
+ LoggingJob.perform_later
+ end
+ end
+
+ assert_match(/`:only` and `:except`/, error.message)
+ end
+
+ def test_assert_performed_jobs_without_block_with_only_and_except_options
+ error = assert_raise ArgumentError do
+ HelloJob.perform_later("jeremy")
+ LoggingJob.perform_later("bogdan")
+
+ perform_enqueued_jobs
+
+ assert_performed_jobs 1, only: HelloJob, except: HelloJob
+ end
+
+ assert_match(/`:only` and `:except`/, error.message)
+ end
+
def test_assert_performed_jobs_with_only_option_as_array
assert_nothing_raised do
assert_performed_jobs 2, only: [HelloJob, LoggingJob] do
- HelloJob.perform_later('jeremy')
- LoggingJob.perform_later('stewie')
- RescueJob.perform_later('david')
+ HelloJob.perform_later("jeremy")
+ LoggingJob.perform_later("stewie")
+ RescueJob.perform_later("david")
+ end
+ end
+ end
+
+ def test_assert_performed_jobs_with_except_option_as_array
+ assert_nothing_raised do
+ assert_performed_jobs 1, except: [LoggingJob, RescueJob] do
+ HelloJob.perform_later("jeremy")
+ LoggingJob.perform_later("stewie")
+ RescueJob.perform_later("david")
+ end
+ end
+ end
+
+ def test_assert_performed_jobs_with_only_and_except_option_as_array
+ error = assert_raise ArgumentError do
+ assert_performed_jobs 2, only: [HelloJob, LoggingJob], except: [HelloJob, LoggingJob] do
+ HelloJob.perform_later("jeremy")
+ LoggingJob.perform_later("stewie")
+ RescueJob.perform_later("david")
end
end
+
+ assert_match(/`:only` and `:except`/, error.message)
end
def test_assert_performed_jobs_with_only_option_and_none_sent
@@ -343,10 +1030,41 @@ class PerformedJobsTest < ActiveJob::TestCase
assert_match(/1 .* but 0/, error.message)
end
+ def test_assert_performed_jobs_with_except_option_and_none_sent
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_performed_jobs 1, except: LoggingJob do
+ LoggingJob.perform_later
+ end
+ end
+
+ assert_match(/1 .* but 0/, error.message)
+ end
+
+ def test_assert_performed_jobs_with_only_and_except_option_and_none_sent
+ error = assert_raise ArgumentError do
+ assert_performed_jobs 1, only: HelloJob, except: HelloJob do
+ LoggingJob.perform_later
+ end
+ end
+
+ assert_match(/`:only` and `:except`/, error.message)
+ end
+
def test_assert_performed_jobs_with_only_option_and_too_few_sent
error = assert_raise ActiveSupport::TestCase::Assertion do
assert_performed_jobs 5, only: HelloJob do
- HelloJob.perform_later('jeremy')
+ HelloJob.perform_later("jeremy")
+ 4.times { LoggingJob.perform_later }
+ end
+ end
+
+ assert_match(/5 .* but 1/, error.message)
+ end
+
+ def test_assert_performed_jobs_with_except_option_and_too_few_sent
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_performed_jobs 5, except: LoggingJob do
+ HelloJob.perform_later("jeremy")
4.times { LoggingJob.perform_later }
end
end
@@ -354,16 +1072,175 @@ class PerformedJobsTest < ActiveJob::TestCase
assert_match(/5 .* but 1/, error.message)
end
+ def test_assert_performed_jobs_with_only_and_except_option_and_too_few_sent
+ error = assert_raise ArgumentError do
+ assert_performed_jobs 5, only: HelloJob, except: HelloJob do
+ HelloJob.perform_later("jeremy")
+ 4.times { LoggingJob.perform_later }
+ end
+ end
+
+ assert_match(/`:only` and `:except`/, error.message)
+ end
+
def test_assert_performed_jobs_with_only_option_and_too_many_sent
error = assert_raise ActiveSupport::TestCase::Assertion do
assert_performed_jobs 1, only: HelloJob do
- 2.times { HelloJob.perform_later('jeremy') }
+ 2.times { HelloJob.perform_later("jeremy") }
end
end
assert_match(/1 .* but 2/, error.message)
end
+ def test_assert_performed_jobs_with_except_option_and_too_many_sent
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_performed_jobs 1, except: LoggingJob do
+ 2.times { HelloJob.perform_later("jeremy") }
+ end
+ end
+
+ assert_match(/1 .* but 2/, error.message)
+ end
+
+ def test_assert_performed_jobs_with_only_and_except_option_and_too_many_sent
+ error = assert_raise ArgumentError do
+ assert_performed_jobs 1, only: HelloJob, except: HelloJob do
+ 2.times { HelloJob.perform_later("jeremy") }
+ end
+ end
+
+ assert_match(/`:only` and `:except`/, error.message)
+ end
+
+ def test_assert_performed_jobs_with_queue_option
+ assert_performed_jobs 1, queue: :some_queue do
+ HelloJob.set(queue: :some_queue).perform_later("jeremy")
+ HelloJob.set(queue: :other_queue).perform_later("bogdan")
+ end
+ end
+
+ def test_assert_performed_jobs_with_queue_option_failure
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_performed_jobs 1, queue: :some_queue do
+ HelloJob.set(queue: :other_queue).perform_later("jeremy")
+ HelloJob.set(queue: :other_queue).perform_later("bogdan")
+ end
+ end
+
+ assert_match(/1 .* but 0/, error.message)
+ end
+
+ def test_assert_performed_jobs_without_block_with_queue_option
+ HelloJob.set(queue: :some_queue).perform_later("jeremy")
+ HelloJob.set(queue: :other_queue).perform_later("bogdan")
+
+ perform_enqueued_jobs
+
+ assert_performed_jobs 1, queue: :some_queue
+ end
+
+ def test_assert_performed_jobs_without_block_with_queue_option_failure
+ HelloJob.set(queue: :other_queue).perform_later("jeremy")
+ HelloJob.set(queue: :other_queue).perform_later("bogdan")
+
+ perform_enqueued_jobs
+
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_performed_jobs 1, queue: :some_queue
+ end
+
+ assert_match(/1 .* but 0/, error.message)
+ end
+
+ def test_assert_performed_jobs_with_only_and_queue_options
+ assert_performed_jobs 1, only: HelloJob, queue: :some_queue do
+ HelloJob.set(queue: :some_queue).perform_later("jeremy")
+ HelloJob.set(queue: :other_queue).perform_later("bogdan")
+ LoggingJob.set(queue: :some_queue).perform_later("jeremy")
+ end
+ end
+
+ def test_assert_performed_jobs_with_only_and_queue_options_failure
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_performed_jobs 1, only: HelloJob, queue: :some_queue do
+ HelloJob.set(queue: :other_queue).perform_later("jeremy")
+ HelloJob.set(queue: :other_queue).perform_later("bogdan")
+ LoggingJob.set(queue: :some_queue).perform_later("jeremy")
+ end
+ end
+
+ assert_match(/1 .* but 0/, error.message)
+ end
+
+ def test_assert_performed_jobs_without_block_with_only_and_queue_options
+ HelloJob.set(queue: :some_queue).perform_later("jeremy")
+ HelloJob.set(queue: :other_queue).perform_later("bogdan")
+ LoggingJob.set(queue: :some_queue).perform_later("jeremy")
+
+ perform_enqueued_jobs
+
+ assert_performed_jobs 1, only: HelloJob, queue: :some_queue
+ end
+
+ def test_assert_performed_jobs_without_block_with_only_and_queue_options_failure
+ HelloJob.set(queue: :other_queue).perform_later("jeremy")
+ HelloJob.set(queue: :other_queue).perform_later("bogdan")
+ LoggingJob.set(queue: :some_queue).perform_later("jeremy")
+
+ perform_enqueued_jobs
+
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_performed_jobs 1, only: HelloJob, queue: :some_queue
+ end
+
+ assert_match(/1 .* but 0/, error.message)
+ end
+
+ def test_assert_performed_jobs_with_except_and_queue_options
+ assert_performed_jobs 1, except: HelloJob, queue: :other_queue do
+ HelloJob.set(queue: :other_queue).perform_later("jeremy")
+ LoggingJob.set(queue: :some_queue).perform_later("bogdan")
+ LoggingJob.set(queue: :other_queue).perform_later("jeremy")
+ end
+ end
+
+ def test_assert_performed_jobs_with_except_and_queue_options_failure
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_performed_jobs 1, except: HelloJob, queue: :other_queue do
+ HelloJob.set(queue: :other_queue).perform_later("jeremy")
+ LoggingJob.set(queue: :some_queue).perform_later("bogdan")
+ LoggingJob.set(queue: :some_queue).perform_later("jeremy")
+ end
+ end
+
+ assert_match(/1 .* but 0/, error.message)
+ end
+
+ def test_assert_performed_jobs_without_block_with_except_and_queue_options
+ HelloJob.set(queue: :other_queue).perform_later("jeremy")
+ LoggingJob.set(queue: :some_queue).perform_later("bogdan")
+ LoggingJob.set(queue: :other_queue).perform_later("jeremy")
+
+ perform_enqueued_jobs
+
+ assert_performed_jobs 1, except: HelloJob, queue: :other_queue
+ end
+
+ def test_assert_performed_jobs_without_block_with_except_and_queue_options_failure
+ HelloJob.set(queue: :other_queue).perform_later("jeremy")
+ LoggingJob.set(queue: :some_queue).perform_later("bogdan")
+ LoggingJob.set(queue: :some_queue).perform_later("jeremy")
+
+ perform_enqueued_jobs
+
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_performed_jobs 1, except: HelloJob, queue: :other_queue
+ end
+
+ assert_match(/1 .* but 0/, error.message)
+ end
+
def test_assert_no_performed_jobs_with_only_option
assert_nothing_raised do
assert_no_performed_jobs only: HelloJob do
@@ -372,6 +1249,77 @@ class PerformedJobsTest < ActiveJob::TestCase
end
end
+ def test_assert_no_performed_jobs_without_block_with_only_option
+ LoggingJob.perform_later("bogdan")
+
+ perform_enqueued_jobs
+
+ assert_no_performed_jobs only: HelloJob
+ end
+
+ def test_assert_no_performed_jobs_without_block_with_only_option_failure
+ HelloJob.perform_later("bogdan")
+
+ perform_enqueued_jobs
+
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_no_performed_jobs only: HelloJob
+ end
+
+ assert_match(/0 .* but 1/, error.message)
+ end
+
+ def test_assert_no_performed_jobs_with_except_option
+ assert_nothing_raised do
+ assert_no_performed_jobs except: LoggingJob do
+ LoggingJob.perform_later
+ end
+ end
+ end
+
+ def test_assert_no_performed_jobs_without_block_with_except_option
+ HelloJob.perform_later("jeremy")
+
+ perform_enqueued_jobs
+
+ assert_no_performed_jobs except: HelloJob
+ end
+
+ def test_assert_no_performed_jobs_without_block_with_except_option_failure
+ LoggingJob.perform_later("jeremy")
+
+ perform_enqueued_jobs
+
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_no_performed_jobs except: HelloJob
+ end
+
+ assert_match(/0 .* but 1/, error.message)
+ end
+
+ def test_assert_no_performed_jobs_with_only_and_except_option
+ error = assert_raise ArgumentError do
+ assert_no_performed_jobs only: HelloJob, except: HelloJob do
+ LoggingJob.perform_later
+ end
+ end
+
+ assert_match(/`:only` and `:except`/, error.message)
+ end
+
+ def test_assert_no_performed_jobs_without_block_with_only_and_except_options
+ error = assert_raise ArgumentError do
+ HelloJob.perform_later("jeremy")
+ LoggingJob.perform_later("bogdan")
+
+ perform_enqueued_jobs
+
+ assert_no_performed_jobs only: HelloJob, except: HelloJob
+ end
+
+ assert_match(/`:only` and `:except`/, error.message)
+ end
+
def test_assert_no_performed_jobs_with_only_option_as_array
assert_nothing_raised do
assert_no_performed_jobs only: [HelloJob, RescueJob] do
@@ -380,10 +1328,29 @@ class PerformedJobsTest < ActiveJob::TestCase
end
end
+ def test_assert_no_performed_jobs_with_except_option_as_array
+ assert_nothing_raised do
+ assert_no_performed_jobs except: [HelloJob, RescueJob] do
+ HelloJob.perform_later
+ RescueJob.perform_later
+ end
+ end
+ end
+
+ def test_assert_no_performed_jobs_with_only_and_except_option_as_array
+ error = assert_raise ArgumentError do
+ assert_no_performed_jobs only: [HelloJob, RescueJob], except: [HelloJob, RescueJob] do
+ LoggingJob.perform_later
+ end
+ end
+
+ assert_match(/`:only` and `:except`/, error.message)
+ end
+
def test_assert_no_performed_jobs_with_only_option_failure
error = assert_raise ActiveSupport::TestCase::Assertion do
assert_no_performed_jobs only: HelloJob do
- HelloJob.perform_later('jeremy')
+ HelloJob.perform_later("jeremy")
LoggingJob.perform_later
end
end
@@ -391,34 +1358,288 @@ class PerformedJobsTest < ActiveJob::TestCase
assert_match(/0 .* but 1/, error.message)
end
- def test_assert_performed_job
- assert_performed_with(job: NestedJob, queue: 'default') do
+ def test_assert_no_performed_jobs_with_except_option_failure
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_no_performed_jobs except: LoggingJob do
+ HelloJob.perform_later("jeremy")
+ LoggingJob.perform_later
+ end
+ end
+
+ assert_match(/0 .* but 1/, error.message)
+ end
+
+ def test_assert_no_performed_jobs_with_only_and_except_option_failure
+ error = assert_raise ArgumentError do
+ assert_no_performed_jobs only: HelloJob, except: HelloJob do
+ HelloJob.perform_later("jeremy")
+ LoggingJob.perform_later
+ end
+ end
+
+ assert_match(/`:only` and `:except`/, error.message)
+ end
+
+ def test_assert_no_performed_jobs_with_queue_option
+ assert_no_performed_jobs queue: :some_queue do
+ HelloJob.set(queue: :other_queue).perform_later("jeremy")
+ end
+ end
+
+ def test_assert_no_performed_jobs_with_queue_option_failure
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_no_performed_jobs queue: :some_queue do
+ HelloJob.set(queue: :some_queue).perform_later("jeremy")
+ end
+ end
+
+ assert_match(/0 .* but 1/, error.message)
+ end
+
+ def test_assert_no_performed_jobs_without_block_with_queue_option
+ HelloJob.set(queue: :other_queue).perform_later("jeremy")
+
+ perform_enqueued_jobs
+
+ assert_no_performed_jobs queue: :some_queue
+ end
+
+ def test_assert_no_performed_jobs_without_block_with_queue_option_failure
+ HelloJob.set(queue: :some_queue).perform_later("jeremy")
+
+ perform_enqueued_jobs
+
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_no_performed_jobs queue: :some_queue
+ end
+
+ assert_match(/0 .* but 1/, error.message)
+ end
+
+ def test_assert_no_performed_jobs_with_only_and_queue_options
+ assert_no_performed_jobs only: HelloJob, queue: :some_queue do
+ HelloJob.set(queue: :other_queue).perform_later("bogdan")
+ LoggingJob.set(queue: :some_queue).perform_later("jeremy")
+ end
+ end
+
+ def test_assert_no_performed_jobs_with_only_and_queue_options_failure
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_no_performed_jobs only: HelloJob, queue: :some_queue do
+ HelloJob.set(queue: :some_queue).perform_later("bogdan")
+ LoggingJob.set(queue: :some_queue).perform_later("jeremy")
+ end
+ end
+
+ assert_match(/0 .* but 1/, error.message)
+ end
+
+ def test_assert_no_performed_jobs_without_block_with_only_and_queue_options
+ HelloJob.set(queue: :other_queue).perform_later("bogdan")
+ LoggingJob.set(queue: :some_queue).perform_later("jeremy")
+
+ perform_enqueued_jobs
+
+ assert_no_performed_jobs only: HelloJob, queue: :some_queue
+ end
+
+ def test_assert_no_performed_jobs_without_block_with_only_and_queue_options_failure
+ HelloJob.set(queue: :some_queue).perform_later("bogdan")
+ LoggingJob.set(queue: :some_queue).perform_later("jeremy")
+
+ perform_enqueued_jobs
+
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_no_performed_jobs only: HelloJob, queue: :some_queue
+ end
+
+ assert_match(/0 .* but 1/, error.message)
+ end
+
+ def test_assert_no_performed_jobs_with_except_and_queue_options
+ assert_no_performed_jobs except: HelloJob, queue: :some_queue do
+ HelloJob.set(queue: :other_queue).perform_later("bogdan")
+ HelloJob.set(queue: :some_queue).perform_later("bogdan")
+ LoggingJob.set(queue: :other_queue).perform_later("jeremy")
+ end
+ end
+
+ def test_assert_no_performed_jobs_with_except_and_queue_options_failure
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_no_performed_jobs except: HelloJob, queue: :some_queue do
+ HelloJob.set(queue: :other_queue).perform_later("bogdan")
+ HelloJob.set(queue: :some_queue).perform_later("bogdan")
+ LoggingJob.set(queue: :some_queue).perform_later("jeremy")
+ end
+ end
+
+ assert_match(/0 .* but 1/, error.message)
+ end
+
+ def test_assert_no_performed_jobs_without_block_with_except_and_queue_options
+ HelloJob.set(queue: :other_queue).perform_later("bogdan")
+ HelloJob.set(queue: :some_queue).perform_later("bogdan")
+ LoggingJob.set(queue: :other_queue).perform_later("jeremy")
+
+ perform_enqueued_jobs
+
+ assert_no_performed_jobs except: HelloJob, queue: :some_queue
+ end
+
+ def test_assert_no_performed_jobs_without_block_with_except_and_queue_options_failure
+ HelloJob.set(queue: :other_queue).perform_later("bogdan")
+ HelloJob.set(queue: :some_queue).perform_later("bogdan")
+ LoggingJob.set(queue: :some_queue).perform_later("jeremy")
+
+ perform_enqueued_jobs
+
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_no_performed_jobs except: HelloJob, queue: :some_queue
+ end
+
+ assert_match(/0 .* but 1/, error.message)
+ end
+
+ def test_assert_performed_with
+ assert_performed_with(job: NestedJob, queue: "default") do
NestedJob.perform_later
end
end
- def test_assert_performed_job_failure
+ def test_assert_performed_with_without_block
+ NestedJob.perform_later
+
+ perform_enqueued_jobs
+
+ assert_performed_with(job: NestedJob, queue: "default")
+ end
+
+ def test_assert_performed_with_returns
+ job = assert_performed_with(job: LoggingJob, queue: "default") do
+ LoggingJob.perform_later(keyword: :sym)
+ end
+
+ assert_instance_of LoggingJob, job
+ assert_nil job.scheduled_at
+ assert_equal [{ keyword: :sym }], job.arguments
+ assert_equal "default", job.queue_name
+ end
+
+ def test_assert_performed_with_without_block_returns
+ LoggingJob.perform_later(keyword: :sym)
+
+ perform_enqueued_jobs
+
+ job = assert_performed_with(job: LoggingJob, queue: "default")
+
+ assert_instance_of LoggingJob, job
+ assert_nil job.scheduled_at
+ assert_equal [{ keyword: :sym }], job.arguments
+ assert_equal "default", job.queue_name
+ end
+
+ def test_assert_performed_with_failure
assert_raise ActiveSupport::TestCase::Assertion do
- assert_performed_with(job: LoggingJob, at: Date.tomorrow.noon, queue: 'default') do
- NestedJob.set(wait_until: Date.tomorrow.noon).perform_later
+ assert_performed_with(job: LoggingJob) do
+ HelloJob.perform_later
+ end
+ end
+
+ assert_raise ActiveSupport::TestCase::Assertion do
+ assert_performed_with(job: HelloJob, queue: "low") do
+ HelloJob.set(queue: "important").perform_later
end
end
+ end
+
+ def test_assert_performed_with_without_block_failure
+ HelloJob.perform_later
+
+ perform_enqueued_jobs
+
+ assert_raise ActiveSupport::TestCase::Assertion do
+ assert_performed_with(job: LoggingJob)
+ end
+
+ HelloJob.set(queue: "important").perform_later
+
+ assert_raise ActiveSupport::TestCase::Assertion do
+ assert_performed_with(job: HelloJob, queue: "low")
+ end
+ end
+
+ def test_assert_performed_with_with_at_option
+ assert_performed_with(job: HelloJob, at: Date.tomorrow.noon) do
+ HelloJob.set(wait_until: Date.tomorrow.noon).perform_later
+ end
+
+ assert_raise ActiveSupport::TestCase::Assertion do
+ assert_performed_with(job: HelloJob, at: Date.today.noon) do
+ HelloJob.set(wait_until: Date.tomorrow.noon).perform_later
+ end
+ end
+ end
+
+ def test_assert_performed_with_without_block_with_at_option
+ HelloJob.set(wait_until: Date.tomorrow.noon).perform_later
+
+ perform_enqueued_jobs
+
+ assert_performed_with(job: HelloJob, at: Date.tomorrow.noon)
+
+ HelloJob.set(wait_until: Date.tomorrow.noon).perform_later
+
+ perform_enqueued_jobs
+
+ assert_raise ActiveSupport::TestCase::Assertion do
+ assert_performed_with(job: HelloJob, at: Date.today.noon)
+ end
+ end
+
+ def test_assert_performed_with_with_hash_arg
+ assert_performed_with(job: MultipleKwargsJob, args: [{ argument1: 1, argument2: { a: 1, b: 2 } }]) do
+ MultipleKwargsJob.perform_later(argument2: { b: 2, a: 1 }, argument1: 1)
+ end
+ end
+
+ def test_assert_performed_with_selective_args
+ args = ->(job_args) do
+ assert_equal 1, job_args.first[:argument1]
+ assert job_args.first[:argument2].key?(:b)
+ end
+
+ assert_performed_with(job: MultipleKwargsJob, args: args) do
+ MultipleKwargsJob.perform_later(argument2: { b: 2, a: 1 }, argument1: 1)
+ end
+ end
+
+ def test_assert_performed_with_selective_args_fails
+ args = ->(job_args) do
+ false
+ end
assert_raise ActiveSupport::TestCase::Assertion do
- assert_performed_with(job: NestedJob, at: Date.tomorrow.noon, queue: 'low') do
- NestedJob.set(queue: 'low', wait_until: Date.tomorrow.noon).perform_later
+ assert_performed_with(job: MultipleKwargsJob, args: args) do
+ MultipleKwargsJob.perform_later(argument2: { b: 2, a: 1 }, argument1: 1)
end
end
end
- def test_assert_performed_job_with_global_id_args
+ def test_assert_performed_with_with_global_id_args
ricardo = Person.new(9)
assert_performed_with(job: HelloJob, args: [ricardo]) do
HelloJob.perform_later(ricardo)
end
end
- def test_assert_performed_job_failure_with_global_id_args
+ def test_assert_performed_with_without_bllock_with_global_id_args
+ ricardo = Person.new(9)
+ HelloJob.perform_later(ricardo)
+ perform_enqueued_jobs
+ assert_performed_with(job: HelloJob, args: [ricardo])
+ end
+
+ def test_assert_performed_with_failure_with_global_id_args
ricardo = Person.new(9)
wilma = Person.new(11)
error = assert_raise ActiveSupport::TestCase::Assertion do
@@ -429,4 +1650,75 @@ class PerformedJobsTest < ActiveJob::TestCase
assert_equal "No performed job found with {:job=>HelloJob, :args=>[#{wilma.inspect}]}", error.message
end
+
+ def test_assert_performed_with_without_block_failure_with_global_id_args
+ ricardo = Person.new(9)
+ wilma = Person.new(11)
+ HelloJob.perform_later(ricardo)
+ perform_enqueued_jobs
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_performed_with(job: HelloJob, args: [wilma])
+ end
+
+ assert_equal "No performed job found with {:job=>HelloJob, :args=>[#{wilma.inspect}]}", error.message
+ end
+
+ def test_assert_performed_with_does_not_change_jobs_count
+ assert_performed_with(job: HelloJob) do
+ HelloJob.perform_later
+ end
+
+ assert_performed_with(job: HelloJob) do
+ HelloJob.perform_later
+ end
+
+ assert_equal 2, queue_adapter.performed_jobs.count
+ end
+
+ def test_assert_performed_with_without_block_does_not_change_jobs_count
+ HelloJob.perform_later
+ perform_enqueued_jobs
+ assert_performed_with(job: HelloJob)
+
+ perform_enqueued_jobs
+ HelloJob.perform_later
+ assert_performed_with(job: HelloJob)
+
+ assert_equal 2, queue_adapter.performed_jobs.count
+ end
+end
+
+class OverrideQueueAdapterTest < ActiveJob::TestCase
+ class CustomQueueAdapter < ActiveJob::QueueAdapters::TestAdapter; end
+
+ def queue_adapter_for_test
+ CustomQueueAdapter.new
+ end
+
+ def test_assert_job_has_custom_queue_adapter_set
+ assert_instance_of CustomQueueAdapter, HelloJob.queue_adapter
+ end
+end
+
+class InheritedJobTest < ActiveJob::TestCase
+ def test_queue_adapter_is_test_adapter
+ 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/cases/timezones_test.rb b/activejob/test/cases/timezones_test.rb
new file mode 100644
index 0000000000..e2095b020d
--- /dev/null
+++ b/activejob/test/cases/timezones_test.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require "helper"
+require "jobs/timezone_dependent_job"
+
+class TimezonesTest < ActiveSupport::TestCase
+ setup do
+ JobBuffer.clear
+ end
+
+ test "it performs the job in the given timezone" do
+ job = TimezoneDependentJob.new("2018-01-01T00:00:00Z")
+ job.timezone = "London"
+ job.perform_now
+
+ assert_equal "Happy New Year!", JobBuffer.last_value
+
+ job = TimezoneDependentJob.new("2018-01-01T00:00:00Z")
+ job.timezone = "Eastern Time (US & Canada)"
+ job.perform_now
+
+ assert_equal "Just 5 hours to go", JobBuffer.last_value
+ end
+end
diff --git a/activejob/test/cases/translation_test.rb b/activejob/test/cases/translation_test.rb
new file mode 100644
index 0000000000..6a0d6d3e54
--- /dev/null
+++ b/activejob/test/cases/translation_test.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require "helper"
+require "jobs/translated_hello_job"
+
+class TranslationTest < ActiveSupport::TestCase
+ setup do
+ JobBuffer.clear
+ I18n.available_locales = [:en, :de]
+ @job = TranslatedHelloJob.new("Johannes")
+ end
+
+ teardown do
+ I18n.available_locales = [:en]
+ end
+
+ test "it performs the job in the given locale" do
+ @job.locale = :de
+ @job.perform_now
+ assert_equal "Johannes says Guten Tag", JobBuffer.last_value
+ end
+end
diff --git a/activejob/test/helper.rb b/activejob/test/helper.rb
index 57907042d9..694232d7ef 100644
--- a/activejob/test/helper.rb
+++ b/activejob/test/helper.rb
@@ -1,16 +1,18 @@
-require File.expand_path('../../../load_paths', __FILE__)
+# frozen_string_literal: true
-require 'active_job'
-require 'support/job_buffer'
+require "active_job"
+require "support/job_buffer"
-GlobalID.app = 'aj'
+GlobalID.app = "aj"
-@adapter = ENV['AJ_ADAPTER'] || 'inline'
+@adapter = ENV["AJ_ADAPTER"] || "inline"
+puts "Using #{@adapter}"
-if ENV['AJ_INTEGRATION_TESTS']
- require 'support/integration/helper'
+if ENV["AJ_INTEGRATION_TESTS"]
+ require "support/integration/helper"
else
+ ActiveJob::Base.logger = Logger.new(nil)
require "adapters/#{@adapter}"
end
-require 'active_support/testing/autorun'
+require "active_support/testing/autorun"
diff --git a/activejob/test/integration/queuing_test.rb b/activejob/test/integration/queuing_test.rb
index d345092dee..96253773c7 100644
--- a/activejob/test/integration/queuing_test.rb
+++ b/activejob/test/integration/queuing_test.rb
@@ -1,17 +1,20 @@
-require 'helper'
-require 'jobs/logging_job'
-require 'jobs/hello_job'
-require 'active_support/core_ext/numeric/time'
+# frozen_string_literal: true
+
+require "helper"
+require "jobs/logging_job"
+require "jobs/hello_job"
+require "jobs/provider_jid_job"
+require "active_support/core_ext/numeric/time"
class QueuingTest < ActiveSupport::TestCase
- test 'should run jobs enqueued on a listening queue' do
+ test "should run jobs enqueued on a listening queue" do
TestJob.perform_later @id
wait_for_jobs_to_finish_for(5.seconds)
assert job_executed
end
- test 'should not run jobs queued on a non-listening queue' do
- skip if adapter_is?(:inline, :sucker_punch, :que)
+ test "should not run jobs queued on a non-listening queue" do
+ skip if adapter_is?(:inline, :async, :sucker_punch, :que)
old_queue = TestJob.queue_name
begin
@@ -24,17 +27,39 @@ class QueuingTest < ActiveSupport::TestCase
end
end
- test 'should supply a wrapped class name to Sidekiq' do
+ test "should supply a wrapped class name to Sidekiq" do
skip unless adapter_is?(:sidekiq)
Sidekiq::Testing.fake! do
::HelloJob.perform_later
hash = ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper.jobs.first
- assert_equal "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper", hash['class']
- assert_equal "HelloJob", hash['wrapped']
+ assert_equal "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper", hash["class"]
+ assert_equal "HelloJob", hash["wrapped"]
+ end
+ end
+
+ test "should access provider_job_id inside Sidekiq job" do
+ skip unless adapter_is?(:sidekiq)
+ Sidekiq::Testing.inline! do
+ job = ::ProviderJidJob.perform_later
+ assert_equal "Provider Job ID: #{job.provider_job_id}", JobBuffer.last_value
end
end
- test 'should not run job enqueued in the future' do
+ test "should supply a wrapped class name to DelayedJob" do
+ skip unless adapter_is?(:delayed_job)
+ ::HelloJob.perform_later
+ job = Delayed::Job.first
+ assert_match(/HelloJob \[[0-9a-f-]+\] from DelayedJob\(default\) with arguments: \[\]/, job.name)
+ end
+
+ test "resque JobWrapper should have instance variable queue" do
+ skip unless adapter_is?(:resque)
+ job = ::HelloJob.set(wait: 5.seconds).perform_later
+ hash = Resque.decode(Resque.find_delayed_selection { true }[0])
+ assert_equal hash["queue"], job.queue_name
+ end
+
+ test "should not run job enqueued in the future" do
begin
TestJob.set(wait: 10.minutes).perform_later @id
wait_for_jobs_to_finish_for(5.seconds)
@@ -44,7 +69,7 @@ class QueuingTest < ActiveSupport::TestCase
end
end
- test 'should run job enqueued in the future at the specified time' do
+ test "should run job enqueued in the future at the specified time" do
begin
TestJob.set(wait: 5.seconds).perform_later @id
wait_for_jobs_to_finish_for(2.seconds)
@@ -56,16 +81,72 @@ class QueuingTest < ActiveSupport::TestCase
end
end
- test 'should supply a provider_job_id when available for immediate jobs' do
- skip unless adapter_is?(:delayed_job, :sidekiq, :qu, :que)
+ test "should supply a provider_job_id when available for immediate jobs" do
+ skip unless adapter_is?(:async, :delayed_job, :sidekiq, :que, :queue_classic)
test_job = TestJob.perform_later @id
- refute test_job.provider_job_id.nil?, 'Provider job id should be set by provider'
+ assert test_job.provider_job_id, "Provider job id should be set by provider"
end
- test 'should supply a provider_job_id when available for delayed jobs' do
- skip unless adapter_is?(:delayed_job, :sidekiq, :que)
+ test "should supply a provider_job_id when available for delayed jobs" do
+ skip unless adapter_is?(:async, :delayed_job, :sidekiq, :que, :queue_classic)
delayed_test_job = TestJob.set(wait: 1.minute).perform_later @id
- refute delayed_test_job.provider_job_id.nil?,
- 'Provider job id should by set for delayed jobs by provider'
+ assert delayed_test_job.provider_job_id, "Provider job id should by set for delayed jobs by provider"
+ end
+
+ test "current locale is kept while running perform_later" do
+ skip if adapter_is?(:inline)
+
+ begin
+ I18n.available_locales = [:en, :de]
+ I18n.locale = :de
+
+ TestJob.perform_later @id
+ wait_for_jobs_to_finish_for(5.seconds)
+ assert job_executed
+ assert_equal "de", job_executed_in_locale
+ ensure
+ I18n.available_locales = [:en]
+ I18n.locale = :en
+ end
+ end
+
+ test "current timezone is kept while running perform_later" do
+ skip if adapter_is?(:inline)
+
+ begin
+ current_zone = Time.zone
+ Time.zone = "Hawaii"
+
+ TestJob.perform_later @id
+ wait_for_jobs_to_finish_for(5.seconds)
+ assert job_executed
+ assert_equal "Hawaii", job_executed_in_timezone
+ ensure
+ Time.zone = current_zone
+ end
+ end
+
+ test "should run job with higher priority first" do
+ skip unless adapter_is?(:delayed_job, :que)
+
+ wait_until = Time.now + 3.seconds
+ TestJob.set(wait_until: wait_until, priority: 20).perform_later "#{@id}.1"
+ TestJob.set(wait_until: wait_until, priority: 10).perform_later "#{@id}.2"
+ wait_for_jobs_to_finish_for(10.seconds)
+ assert job_executed "#{@id}.1"
+ assert job_executed "#{@id}.2"
+ assert job_executed_at("#{@id}.2") < job_executed_at("#{@id}.1")
+ end
+
+ test "should run job with higher priority first in Backburner" do
+ skip unless adapter_is?(:backburner)
+
+ jobs_manager.tube.pause(3)
+ TestJob.set(priority: 20).perform_later "#{@id}.1"
+ TestJob.set(priority: 10).perform_later "#{@id}.2"
+ wait_for_jobs_to_finish_for(10.seconds)
+ assert job_executed "#{@id}.1"
+ assert job_executed "#{@id}.2"
+ assert job_executed_at("#{@id}.2") < job_executed_at("#{@id}.1")
end
end
diff --git a/activejob/test/jobs/application_job.rb b/activejob/test/jobs/application_job.rb
new file mode 100644
index 0000000000..d92ffddcb5
--- /dev/null
+++ b/activejob/test/jobs/application_job.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+class ApplicationJob < ActiveJob::Base
+end
diff --git a/activejob/test/jobs/callback_job.rb b/activejob/test/jobs/callback_job.rb
index 891ed9464e..436cb55492 100644
--- a/activejob/test/jobs/callback_job.rb
+++ b/activejob/test/jobs/callback_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CallbackJob < ActiveJob::Base
before_perform ->(job) { job.history << "CallbackJob ran before_perform" }
after_perform ->(job) { job.history << "CallbackJob ran after_perform" }
@@ -17,7 +19,6 @@ class CallbackJob < ActiveJob::Base
job.history << "CallbackJob ran around_enqueue_stop"
end
-
def perform(person = "david")
# NOTHING!
end
@@ -25,5 +26,4 @@ class CallbackJob < ActiveJob::Base
def history
@history ||= []
end
-
end
diff --git a/activejob/test/jobs/gid_job.rb b/activejob/test/jobs/gid_job.rb
index e485bfa2dd..2136f57e05 100644
--- a/activejob/test/jobs/gid_job.rb
+++ b/activejob/test/jobs/gid_job.rb
@@ -1,8 +1,9 @@
-require_relative '../support/job_buffer'
+# frozen_string_literal: true
+
+require_relative "../support/job_buffer"
class GidJob < ActiveJob::Base
def perform(person)
JobBuffer.add("Person with ID: #{person.id}")
end
end
-
diff --git a/activejob/test/jobs/hello_job.rb b/activejob/test/jobs/hello_job.rb
index 022fa58e4a..404df6150a 100644
--- a/activejob/test/jobs/hello_job.rb
+++ b/activejob/test/jobs/hello_job.rb
@@ -1,4 +1,6 @@
-require_relative '../support/job_buffer'
+# frozen_string_literal: true
+
+require_relative "../support/job_buffer"
class HelloJob < ActiveJob::Base
def perform(greeter = "David")
diff --git a/activejob/test/jobs/inherited_job.rb b/activejob/test/jobs/inherited_job.rb
new file mode 100644
index 0000000000..14f852ed06
--- /dev/null
+++ b/activejob/test/jobs/inherited_job.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require_relative "application_job"
+
+class InheritedJob < ApplicationJob
+ self.queue_adapter = :inline
+end
diff --git a/activejob/test/jobs/kwargs_job.rb b/activejob/test/jobs/kwargs_job.rb
index 2df17d15ae..b86b06bada 100644
--- a/activejob/test/jobs/kwargs_job.rb
+++ b/activejob/test/jobs/kwargs_job.rb
@@ -1,4 +1,6 @@
-require_relative '../support/job_buffer'
+# frozen_string_literal: true
+
+require_relative "../support/job_buffer"
class KwargsJob < ActiveJob::Base
def perform(argument: 1)
diff --git a/activejob/test/jobs/logging_job.rb b/activejob/test/jobs/logging_job.rb
index d84ed8589b..4605fa6937 100644
--- a/activejob/test/jobs/logging_job.rb
+++ b/activejob/test/jobs/logging_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class LoggingJob < ActiveJob::Base
def perform(dummy)
logger.info "Dummy, here is it: #{dummy}"
@@ -7,4 +9,3 @@ class LoggingJob < ActiveJob::Base
"LOGGING-JOB-ID"
end
end
-
diff --git a/activejob/test/jobs/multiple_kwargs_job.rb b/activejob/test/jobs/multiple_kwargs_job.rb
new file mode 100644
index 0000000000..b355c4ce1a
--- /dev/null
+++ b/activejob/test/jobs/multiple_kwargs_job.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require_relative "../support/job_buffer"
+
+class MultipleKwargsJob < ActiveJob::Base
+ def perform(argument1:, argument2:)
+ JobBuffer.add("Job with argument1: #{argument1}, argument2: #{argument2}")
+ end
+end
diff --git a/activejob/test/jobs/nested_job.rb b/activejob/test/jobs/nested_job.rb
index 8c4ec549a6..aafad0dba9 100644
--- a/activejob/test/jobs/nested_job.rb
+++ b/activejob/test/jobs/nested_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class NestedJob < ActiveJob::Base
def perform
LoggingJob.perform_later "NestedJob"
@@ -7,4 +9,3 @@ class NestedJob < ActiveJob::Base
"NESTED-JOB-ID"
end
end
-
diff --git a/activejob/test/jobs/overridden_logging_job.rb b/activejob/test/jobs/overridden_logging_job.rb
new file mode 100644
index 0000000000..2ee363637d
--- /dev/null
+++ b/activejob/test/jobs/overridden_logging_job.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class OverriddenLoggingJob < ActiveJob::Base
+ def perform(dummy)
+ logger.info "Dummy, here is it: #{dummy}"
+ end
+
+ def logger
+ @logger ||= ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(nil))
+ end
+end
diff --git a/activejob/test/jobs/provider_jid_job.rb b/activejob/test/jobs/provider_jid_job.rb
new file mode 100644
index 0000000000..dacd09afdc
--- /dev/null
+++ b/activejob/test/jobs/provider_jid_job.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require_relative "../support/job_buffer"
+
+class ProviderJidJob < ActiveJob::Base
+ def perform
+ JobBuffer.add("Provider Job ID: #{provider_job_id}")
+ end
+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..1c31a60ba2
--- /dev/null
+++ b/activejob/test/jobs/queue_adapter_job.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class QueueAdapterJob < ActiveJob::Base
+ self.queue_adapter = :inline
+end
diff --git a/activejob/test/jobs/queue_as_job.rb b/activejob/test/jobs/queue_as_job.rb
new file mode 100644
index 0000000000..0feee99e87
--- /dev/null
+++ b/activejob/test/jobs/queue_as_job.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require_relative "../support/job_buffer"
+
+class QueueAsJob < ActiveJob::Base
+ MY_QUEUE = :low_priority
+ queue_as MY_QUEUE
+
+ def perform(greeter = "David")
+ JobBuffer.add("#{greeter} says hello")
+ end
+end
diff --git a/activejob/test/jobs/rescue_job.rb b/activejob/test/jobs/rescue_job.rb
index f1b9c9349e..d142cec1ea 100644
--- a/activejob/test/jobs/rescue_job.rb
+++ b/activejob/test/jobs/rescue_job.rb
@@ -1,17 +1,19 @@
-require_relative '../support/job_buffer'
+# frozen_string_literal: true
+
+require_relative "../support/job_buffer"
class RescueJob < ActiveJob::Base
class OtherError < StandardError; end
rescue_from(ArgumentError) do
- JobBuffer.add('rescued from ArgumentError')
+ JobBuffer.add("rescued from ArgumentError")
arguments[0] = "DIFFERENT!"
retry_job
end
rescue_from(ActiveJob::DeserializationError) do |e|
- JobBuffer.add('rescued from DeserializationError')
- JobBuffer.add("DeserializationError original exception was #{e.original_exception.class.name}")
+ JobBuffer.add("rescued from DeserializationError")
+ JobBuffer.add("DeserializationError original exception was #{e.cause.class.name}")
end
def perform(person = "david")
@@ -19,9 +21,9 @@ class RescueJob < ActiveJob::Base
when "david"
raise ArgumentError, "Hair too good"
when "other"
- raise OtherError
+ raise OtherError, "Bad hair"
else
- JobBuffer.add('performed beautifully')
+ JobBuffer.add("performed beautifully")
end
end
end
diff --git a/activejob/test/jobs/retry_job.rb b/activejob/test/jobs/retry_job.rb
new file mode 100644
index 0000000000..68dc17e16c
--- /dev/null
+++ b/activejob/test/jobs/retry_job.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require_relative "../support/job_buffer"
+require "active_support/core_ext/integer/inflections"
+
+class DefaultsError < StandardError; end
+class FirstRetryableErrorOfTwo < StandardError; end
+class SecondRetryableErrorOfTwo < StandardError; end
+class LongWaitError < StandardError; end
+class ShortWaitTenAttemptsError < StandardError; end
+class ExponentialWaitTenAttemptsError < StandardError; end
+class CustomWaitTenAttemptsError < StandardError; end
+class CustomCatchError < StandardError; end
+class DiscardableError < StandardError; end
+class FirstDiscardableErrorOfTwo < StandardError; end
+class SecondDiscardableErrorOfTwo < StandardError; end
+class CustomDiscardableError < StandardError; end
+
+class RetryJob < ActiveJob::Base
+ retry_on DefaultsError
+ retry_on FirstRetryableErrorOfTwo, SecondRetryableErrorOfTwo
+ retry_on LongWaitError, wait: 1.hour, attempts: 10
+ retry_on ShortWaitTenAttemptsError, wait: 1.second, attempts: 10
+ retry_on ExponentialWaitTenAttemptsError, wait: :exponentially_longer, attempts: 10
+ retry_on CustomWaitTenAttemptsError, wait: ->(executions) { executions * 2 }, attempts: 10
+ retry_on(CustomCatchError) { |job, error| JobBuffer.add("Dealt with a job that failed to retry in a custom way after #{job.arguments.second} attempts. Message: #{error.message}") }
+ retry_on(ActiveJob::DeserializationError) { |job, error| JobBuffer.add("Raised #{error.class} for the #{job.executions} time") }
+
+ discard_on DiscardableError
+ discard_on FirstDiscardableErrorOfTwo, SecondDiscardableErrorOfTwo
+ discard_on(CustomDiscardableError) { |job, error| JobBuffer.add("Dealt with a job that was discarded in a custom way. Message: #{error.message}") }
+
+ def perform(raising, attempts)
+ if executions < attempts
+ JobBuffer.add("Raised #{raising} for the #{executions.ordinalize} time")
+ raise raising.constantize
+ else
+ JobBuffer.add("Successfully completed job")
+ end
+ end
+end
diff --git a/activejob/test/jobs/timezone_dependent_job.rb b/activejob/test/jobs/timezone_dependent_job.rb
new file mode 100644
index 0000000000..41f473d533
--- /dev/null
+++ b/activejob/test/jobs/timezone_dependent_job.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require_relative "../support/job_buffer"
+
+class TimezoneDependentJob < ActiveJob::Base
+ def perform(now)
+ now = now.in_time_zone
+ new_year = localtime(2018, 1, 1)
+
+ if now >= new_year
+ JobBuffer.add("Happy New Year!")
+ else
+ JobBuffer.add("Just #{(new_year - now).div(3600)} hours to go")
+ end
+ end
+
+ private
+
+ def localtime(*args)
+ Time.zone ? Time.zone.local(*args) : Time.utc(*args)
+ end
+end
diff --git a/activejob/test/jobs/translated_hello_job.rb b/activejob/test/jobs/translated_hello_job.rb
new file mode 100644
index 0000000000..a0a68b4040
--- /dev/null
+++ b/activejob/test/jobs/translated_hello_job.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require_relative "../support/job_buffer"
+
+class TranslatedHelloJob < ActiveJob::Base
+ def perform(greeter = "David")
+ translations = { en: "Hello", de: "Guten Tag" }
+ hello = translations[I18n.locale]
+
+ JobBuffer.add("#{greeter} says #{hello}")
+ end
+end
diff --git a/activejob/test/models/person.rb b/activejob/test/models/person.rb
index 76a8f40616..9a3bfab25f 100644
--- a/activejob/test/models/person.rb
+++ b/activejob/test/models/person.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Person
class RecordNotFound < StandardError; end
@@ -6,7 +8,7 @@ class Person
attr_reader :id
def self.find(id)
- raise RecordNotFound.new("Cannot find person with ID=404") if id.to_i==404
+ raise RecordNotFound.new("Cannot find person with ID=404") if id.to_i == 404
new(id)
end
diff --git a/activejob/test/support/backburner/inline.rb b/activejob/test/support/backburner/inline.rb
index f761b53e27..6c708c0b7b 100644
--- a/activejob/test/support/backburner/inline.rb
+++ b/activejob/test/support/backburner/inline.rb
@@ -1,8 +1,10 @@
-require 'backburner'
+# frozen_string_literal: true
+
+require "backburner"
Backburner::Worker.class_eval do
class << self; alias_method :original_enqueue, :enqueue; end
- def self.enqueue(job_class, args=[], opts={})
+ def self.enqueue(job_class, args = [], opts = {})
job_class.perform(*args)
end
-end \ No newline at end of file
+end
diff --git a/activejob/test/support/delayed_job/delayed/backend/test.rb b/activejob/test/support/delayed_job/delayed/backend/test.rb
index f80ec3a5a6..1691896b7c 100644
--- a/activejob/test/support/delayed_job/delayed/backend/test.rb
+++ b/activejob/test/support/delayed_job/delayed/backend/test.rb
@@ -1,5 +1,7 @@
-#copied from https://github.com/collectiveidea/delayed_job/blob/master/spec/delayed/backend/test.rb
-require 'ostruct'
+# frozen_string_literal: true
+
+# copied from https://github.com/collectiveidea/delayed_job/blob/master/spec/delayed/backend/test.rb
+require "ostruct"
# An in-memory backend suitable only for testing. Tries to behave as if it were an ORM.
module Delayed
@@ -19,14 +21,13 @@ module Delayed
include Delayed::Backend::Base
- cattr_accessor :id
- self.id = 0
+ cattr_accessor :id, default: 0
def initialize(hash = {})
self.attempts = 0
self.priority = 0
self.id = (self.class.id += 1)
- hash.each{|k,v| send(:"#{k}=", v)}
+ hash.each { |k, v| send(:"#{k}=", v) }
end
@jobs = []
@@ -49,7 +50,7 @@ module Delayed
def self.create!(*args); create(*args); end
def self.clear_locks!(worker_name)
- all.select{|j| j.locked_by == worker_name}.each {|j| j.locked_by = nil; j.locked_at = nil}
+ all.select { |j| j.locked_by == worker_name }.each { |j| j.locked_by = nil; j.locked_at = nil }
end
# Find a few candidate jobs to run (in case some immediately get locked by others).
@@ -60,10 +61,10 @@ module Delayed
!j.failed?
end
- jobs = jobs.select{|j| Worker.queues.include?(j.queue)} if Worker.queues.any?
- jobs = jobs.select{|j| j.priority >= Worker.min_priority} if Worker.min_priority
- jobs = jobs.select{|j| j.priority <= Worker.max_priority} if Worker.max_priority
- jobs.sort_by{|j| [j.priority, j.run_at]}[0..limit-1]
+ jobs = jobs.select { |j| Worker.queues.include?(j.queue) } if Worker.queues.any?
+ jobs = jobs.select { |j| j.priority >= Worker.min_priority } if Worker.min_priority
+ jobs = jobs.select { |j| j.priority <= Worker.max_priority } if Worker.max_priority
+ jobs.sort_by { |j| [j.priority, j.run_at] }[0..limit - 1]
end
# Lock this job for this worker.
@@ -76,7 +77,7 @@ module Delayed
self.locked_by = worker
end
- return true
+ true
end
def self.db_time_now
@@ -84,7 +85,7 @@ module Delayed
end
def update_attributes(attrs = {})
- attrs.each{|k,v| send(:"#{k}=", v)}
+ attrs.each { |k, v| send(:"#{k}=", v) }
save
end
diff --git a/activejob/test/support/integration/adapters/async.rb b/activejob/test/support/integration/adapters/async.rb
new file mode 100644
index 0000000000..ba9674d7a1
--- /dev/null
+++ b/activejob/test/support/integration/adapters/async.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module AsyncJobsManager
+ def setup
+ ActiveJob::Base.queue_adapter = :async
+ ActiveJob::Base.queue_adapter.immediate = false
+ end
+
+ def clear_jobs
+ ActiveJob::Base.queue_adapter.shutdown
+ end
+end
diff --git a/activejob/test/support/integration/adapters/backburner.rb b/activejob/test/support/integration/adapters/backburner.rb
index 2e82562948..1163ae8178 100644
--- a/activejob/test/support/integration/adapters/backburner.rb
+++ b/activejob/test/support/integration/adapters/backburner.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module BackburnerJobsManager
def setup
ActiveJob::Base.queue_adapter = :backburner
@@ -6,7 +8,8 @@ module BackburnerJobsManager
end
unless can_run?
puts "Cannot run integration tests for backburner. To be able to run integration tests for backburner you need to install and start beanstalkd.\n"
- exit
+ status = ENV["CI"] ? false : true
+ exit status
end
end
@@ -23,16 +26,15 @@ 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
true
end
end
-
diff --git a/activejob/test/support/integration/adapters/delayed_job.rb b/activejob/test/support/integration/adapters/delayed_job.rb
index 0b591964bc..fc9bae47fa 100644
--- a/activejob/test/support/integration/adapters/delayed_job.rb
+++ b/activejob/test/support/integration/adapters/delayed_job.rb
@@ -1,5 +1,7 @@
-require 'delayed_job'
-require 'delayed_job_active_record'
+# frozen_string_literal: true
+
+require "delayed_job"
+require "delayed_job_active_record"
module DelayedJobJobsManager
def setup
@@ -16,5 +18,6 @@ module DelayedJobJobsManager
def stop_workers
@worker.stop
+ @thread.join
end
end
diff --git a/activejob/test/support/integration/adapters/inline.rb b/activejob/test/support/integration/adapters/inline.rb
index 83c38f706f..10a97fb941 100644
--- a/activejob/test/support/integration/adapters/inline.rb
+++ b/activejob/test/support/integration/adapters/inline.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module InlineJobsManager
def setup
ActiveJob::Base.queue_adapter = :inline
@@ -12,4 +14,3 @@ module InlineJobsManager
def stop_workers
end
end
-
diff --git a/activejob/test/support/integration/adapters/qu.rb b/activejob/test/support/integration/adapters/qu.rb
deleted file mode 100644
index 256ddb3cf3..0000000000
--- a/activejob/test/support/integration/adapters/qu.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-module QuJobsManager
- def setup
- require 'qu-rails'
- require 'qu-redis'
- ActiveJob::Base.queue_adapter = :qu
- ENV['REDISTOGO_URL'] = "redis://127.0.0.1:6379/12"
- backend = Qu::Backend::Redis.new
- backend.namespace = "active_jobs_int_test"
- Qu.backend = backend
- Qu.logger = Rails.logger
- Qu.interval = 0.5
- unless can_run?
- puts "Cannot run integration tests for qu. To be able to run integration tests for qu you need to install and start redis.\n"
- exit
- end
- end
-
- def clear_jobs
- Qu.clear "integration_tests"
- end
-
- def start_workers
- @thread = Thread.new { Qu::Worker.new("integration_tests").start }
- end
-
- def stop_workers
- @thread.kill
- end
-
- def can_run?
- begin
- Qu.backend.connection.client.connect
- rescue
- return false
- end
- true
- end
-end
diff --git a/activejob/test/support/integration/adapters/que.rb b/activejob/test/support/integration/adapters/que.rb
index 0cd8952a28..f231e5e12d 100644
--- a/activejob/test/support/integration/adapters/que.rb
+++ b/activejob/test/support/integration/adapters/que.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
module QueJobsManager
def setup
- require 'sequel'
+ require "sequel"
ActiveJob::Base.queue_adapter = :que
Que.mode = :off
Que.worker_count = 1
@@ -11,13 +13,13 @@ module QueJobsManager
end
def start_workers
- que_url = ENV['QUE_DATABASE_URL'] || 'postgres:///active_jobs_que_int_test'
+ que_url = ENV["QUE_DATABASE_URL"] || "postgres:///active_jobs_que_int_test"
uri = URI.parse(que_url)
- user = uri.user||ENV['USER']
+ user = uri.user || ENV["USER"]
pass = uri.password
db = uri.path[1..-1]
- %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'drop database if exists "#{db}"' -U #{user} -t template1}
- %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'create database "#{db}"' -U #{user} -t template1}
+ %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -X -c 'drop database if exists "#{db}"' -U #{user} -t template1}
+ %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -X -c 'create database "#{db}"' -U #{user} -t template1}
Que.connection = Sequel.connect(que_url)
Que.migrate!
@@ -30,7 +32,8 @@ module QueJobsManager
rescue Sequel::DatabaseConnectionError
puts "Cannot run integration tests for que. To be able to run integration tests for que you need to install and start postgresql.\n"
- exit
+ status = ENV["CI"] ? false : true
+ exit status
end
def stop_workers
diff --git a/activejob/test/support/integration/adapters/queue_classic.rb b/activejob/test/support/integration/adapters/queue_classic.rb
index 29c04bf625..2b5375461a 100644
--- a/activejob/test/support/integration/adapters/queue_classic.rb
+++ b/activejob/test/support/integration/adapters/queue_classic.rb
@@ -1,8 +1,10 @@
+# frozen_string_literal: true
+
module QueueClassicJobsManager
def setup
- ENV['QC_DATABASE_URL'] ||= 'postgres:///active_jobs_qc_int_test'
- ENV['QC_RAILS_DATABASE'] = 'false'
- ENV['QC_LISTEN_TIME'] = "0.5"
+ ENV["QC_DATABASE_URL"] ||= "postgres:///active_jobs_qc_int_test"
+ ENV["QC_RAILS_DATABASE"] = "false"
+ ENV["QC_LISTEN_TIME"] = "0.5"
ActiveJob::Base.queue_adapter = :queue_classic
end
@@ -11,27 +13,28 @@ module QueueClassicJobsManager
end
def start_workers
- uri = URI.parse(ENV['QC_DATABASE_URL'])
- user = uri.user||ENV['USER']
+ uri = URI.parse(ENV["QC_DATABASE_URL"])
+ user = uri.user || ENV["USER"]
pass = uri.password
db = uri.path[1..-1]
- %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'drop database if exists "#{db}"' -U #{user} -t template1}
- %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'create database "#{db}"' -U #{user} -t template1}
+ %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -X -c 'drop database if exists "#{db}"' -U #{user} -t template1}
+ %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -X -c 'create database "#{db}"' -U #{user} -t template1}
QC::Setup.create
QC.default_conn_adapter.disconnect
QC.default_conn_adapter = nil
@pid = fork do
- worker = QC::Worker.new(q_name: 'integration_tests')
+ worker = QC::Worker.new(q_name: "integration_tests")
worker.start
end
rescue PG::ConnectionBad
puts "Cannot run integration tests for queue_classic. To be able to run integration tests for queue_classic you need to install and start postgresql.\n"
- exit
+ status = ENV["CI"] ? false : true
+ exit status
end
def stop_workers
- Process.kill 'HUP', @pid
+ Process.kill "HUP", @pid
end
end
diff --git a/activejob/test/support/integration/adapters/resque.rb b/activejob/test/support/integration/adapters/resque.rb
index 912f4bc387..2ed8302277 100644
--- a/activejob/test/support/integration/adapters/resque.rb
+++ b/activejob/test/support/integration/adapters/resque.rb
@@ -1,11 +1,14 @@
+# frozen_string_literal: true
+
module ResqueJobsManager
def setup
ActiveJob::Base.queue_adapter = :resque
- Resque.redis = Redis::Namespace.new 'active_jobs_int_test', redis: Redis.connect(url: "redis://127.0.0.1:6379/12", :thread_safe => true)
+ Resque.redis = Redis::Namespace.new "active_jobs_int_test", redis: Redis.new(url: "redis://127.0.0.1:6379/12", thread_safe: true)
Resque.logger = Rails.logger
unless can_run?
puts "Cannot run integration tests for resque. To be able to run integration tests for resque you need to install and start redis.\n"
- exit
+ status = ENV["CI"] ? false : true
+ exit status
end
end
@@ -39,11 +42,8 @@ module ResqueJobsManager
end
def can_run?
- begin
- Resque.redis.client.connect
- rescue
- return false
- end
- true
+ Resque.redis.ping == "PONG"
+ rescue
+ false
end
end
diff --git a/activejob/test/support/integration/adapters/sidekiq.rb b/activejob/test/support/integration/adapters/sidekiq.rb
index 4988cdb33f..c79de12eaf 100644
--- a/activejob/test/support/integration/adapters/sidekiq.rb
+++ b/activejob/test/support/integration/adapters/sidekiq.rb
@@ -1,15 +1,17 @@
-require 'sidekiq/api'
+# frozen_string_literal: true
-require 'sidekiq/testing'
+require "sidekiq/api"
+
+require "sidekiq/testing"
Sidekiq::Testing.disable!
module SidekiqJobsManager
-
def setup
ActiveJob::Base.queue_adapter = :sidekiq
unless can_run?
puts "Cannot run integration tests for sidekiq. To be able to run integration tests for sidekiq you need to install and start redis.\n"
- exit
+ status = ENV["CI"] ? false : true
+ exit status
end
end
@@ -26,10 +28,10 @@ module SidekiqJobsManager
continue_read.close
death_write.close
- # Celluloid & Sidekiq are not warning-clean :(
+ # Sidekiq is not warning-clean :(
$VERBOSE = false
- $stdin.reopen('/dev/null')
+ $stdin.reopen(File::NULL)
$stdout.sync = true
$stderr.sync = true
@@ -49,16 +51,14 @@ module SidekiqJobsManager
self_write.puts("TERM")
end
- require 'celluloid'
- Celluloid.logger = nil
- require 'sidekiq/launcher'
- sidekiq = Sidekiq::Launcher.new({queues: ["integration_tests"],
+ require "sidekiq/cli"
+ require "sidekiq/launcher"
+ sidekiq = Sidekiq::Launcher.new(queues: ["integration_tests"],
environment: "test",
concurrency: 1,
- timeout: 1,
- })
- Sidekiq.poll_interval = 0.5
- Sidekiq::Scheduled.const_set :INITIAL_WAIT, 1
+ timeout: 1)
+ Sidekiq.average_scheduled_poll_interval = 0.5
+ Sidekiq.options[:poll_interval_average] = 1
begin
sidekiq.run
continue_write.puts "started"
@@ -81,7 +81,7 @@ module SidekiqJobsManager
def stop_workers
if @pid
- Process.kill 'TERM', @pid
+ Process.kill "TERM", @pid
Process.wait @pid
end
end
diff --git a/activejob/test/support/integration/adapters/sneakers.rb b/activejob/test/support/integration/adapters/sneakers.rb
index 875803a2d8..eb8d4cc2d5 100644
--- a/activejob/test/support/integration/adapters/sneakers.rb
+++ b/activejob/test/support/integration/adapters/sneakers.rb
@@ -1,34 +1,25 @@
-require 'sneakers/runner'
-require 'sneakers/publisher'
-require 'timeout'
-
-module Sneakers
- class Publisher
- def safe_ensure_connected
- @mutex.synchronize do
- ensure_connection! unless connected?
- end
- end
- end
-end
+# frozen_string_literal: true
+require "sneakers/runner"
+require "timeout"
module SneakersJobsManager
def setup
ActiveJob::Base.queue_adapter = :sneakers
- Sneakers.configure :heartbeat => 2,
- :amqp => 'amqp://guest:guest@localhost:5672',
- :vhost => '/',
- :exchange => 'active_jobs_sneakers_int_test',
- :exchange_type => :direct,
- :daemonize => true,
- :threads => 1,
- :workers => 1,
- :pid_path => Rails.root.join("tmp/sneakers.pid").to_s,
- :log => Rails.root.join("log/sneakers.log").to_s
+ Sneakers.configure heartbeat: 2,
+ amqp: "amqp://guest:guest@localhost:5672",
+ vhost: "/",
+ exchange: "active_jobs_sneakers_int_test",
+ exchange_type: :direct,
+ daemonize: true,
+ threads: 1,
+ workers: 1,
+ pid_path: Rails.root.join("tmp/sneakers.pid").to_s,
+ log: Rails.root.join("log/sneakers.log").to_s
unless can_run?
puts "Cannot run integration tests for sneakers. To be able to run integration tests for sneakers you need to install and start rabbitmq.\n"
- exit
+ status = ENV["CI"] ? false : true
+ exit status
end
end
@@ -40,7 +31,7 @@ module SneakersJobsManager
@pid = fork do
queues = %w(integration_tests)
workers = queues.map do |q|
- worker_klass = "ActiveJobWorker"+Digest::MD5.hexdigest(q)
+ worker_klass = "ActiveJobWorker" + Digest::MD5.hexdigest(q)
Sneakers.const_set(worker_klass, Class.new(ActiveJob::QueueAdapters::SneakersAdapter::JobWrapper) do
from_queue q
end)
@@ -60,8 +51,8 @@ module SneakersJobsManager
end
def stop_workers
- Process.kill 'TERM', @pid
- Process.kill 'TERM', File.open(Rails.root.join("tmp/sneakers.pid").to_s).read.to_i
+ Process.kill "TERM", @pid
+ Process.kill "TERM", File.open(Rails.root.join("tmp/sneakers.pid").to_s).read.to_i
rescue
end
@@ -74,11 +65,11 @@ module SneakersJobsManager
true
end
- protected
+ private
def bunny_publisher
@bunny_publisher ||= begin
p = ActiveJob::QueueAdapters::SneakersAdapter::JobWrapper.send(:publisher)
- p.safe_ensure_connected
+ p.ensure_connection!
p
end
end
@@ -86,5 +77,4 @@ module SneakersJobsManager
def bunny_queue
@queue ||= bunny_publisher.exchange.channel.queue "integration_tests", durable: true
end
-
end
diff --git a/activejob/test/support/integration/adapters/sucker_punch.rb b/activejob/test/support/integration/adapters/sucker_punch.rb
index 9c0d66b469..099d412c8f 100644
--- a/activejob/test/support/integration/adapters/sucker_punch.rb
+++ b/activejob/test/support/integration/adapters/sucker_punch.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module SuckerPunchJobsManager
def setup
ActiveJob::Base.queue_adapter = :sucker_punch
diff --git a/activejob/test/support/integration/dummy_app_template.rb b/activejob/test/support/integration/dummy_app_template.rb
index 09a68738ad..b56dd3e591 100644
--- a/activejob/test/support/integration/dummy_app_template.rb
+++ b/activejob/test/support/integration/dummy_app_template.rb
@@ -1,21 +1,31 @@
-if ENV['AJ_ADAPTER'] == 'delayed_job'
+# frozen_string_literal: true
+
+if ENV["AJ_ADAPTER"] == "delayed_job"
generate "delayed_job:active_record", "--quiet"
- rake("db:migrate")
end
-initializer 'activejob.rb', <<-CODE
-require "#{File.expand_path("../jobs_manager.rb", __FILE__)}"
+initializer "activejob.rb", <<-CODE
+require "#{File.expand_path("jobs_manager.rb", __dir__)}"
JobsManager.current_manager.setup
CODE
-file 'app/jobs/test_job.rb', <<-CODE
+initializer "i18n.rb", <<-CODE
+I18n.available_locales = [:en, :de]
+CODE
+
+file "app/jobs/test_job.rb", <<-CODE
class TestJob < ActiveJob::Base
queue_as :integration_tests
def perform(x)
- File.open(Rails.root.join("tmp/\#{x}"), "w+") do |f|
- f.write x
+ File.open(Rails.root.join("tmp/\#{x}.new"), "wb+") do |f|
+ f.write Marshal.dump({
+ "locale" => I18n.locale.to_s || "en",
+ "timezone" => Time.zone.try(:name) || "UTC",
+ "executed_at" => Time.now.to_r
+ })
end
+ 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 8c2e5a86c2..c5fa2b136f 100644
--- a/activejob/test/support/integration/helper.rb
+++ b/activejob/test/support/integration/helper.rb
@@ -1,12 +1,15 @@
-puts "*** rake aj:integration:#{ENV['AJ_ADAPTER']} ***\n"
+# frozen_string_literal: true
+
+puts "\n\n*** rake test:integration:#{ENV['AJ_ADAPTER']} ***\n"
ENV["RAILS_ENV"] = "test"
ActiveJob::Base.queue_name_prefix = nil
-require 'rails/generators/rails/app/app_generator'
+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__)
+dummy_app_template = File.expand_path("dummy_app_template.rb", __dir__)
args = Rails::Generators::ARGVScrubber.new(["new", dummy_app_path, "--skip-gemfile", "--skip-bundle",
"--skip-git", "--skip-spring", "-d", "sqlite3", "--skip-javascript", "--force", "--quiet",
"--template", dummy_app_template]).prepare!
@@ -14,12 +17,13 @@ Rails::Generators::AppGenerator.start args
require "#{dummy_app_path}/config/environment.rb"
-ActiveRecord::Migrator.migrations_paths = [ Rails.root.join('db/migrate').to_s ]
-require 'rails/test_help'
+ActiveRecord::Migrator.migrations_paths = [ Rails.root.join("db/migrate").to_s ]
+ActiveRecord::Tasks::DatabaseTasks.migrate
+require "rails/test_help"
Rails.backtrace_cleaner.remove_silencers!
-require_relative 'test_case_helpers'
+require_relative "test_case_helpers"
ActiveSupport::TestCase.include(TestCaseHelpers)
JobsManager.current_manager.start_workers
diff --git a/activejob/test/support/integration/jobs_manager.rb b/activejob/test/support/integration/jobs_manager.rb
index 78d48e8d9a..4775f52b2f 100644
--- a/activejob/test/support/integration/jobs_manager.rb
+++ b/activejob/test/support/integration/jobs_manager.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
class JobsManager
@@managers = {}
attr :adapter_name
def self.current_manager
- @@managers[ENV['AJ_ADAPTER']] ||= new(ENV['AJ_ADAPTER'])
+ @@managers[ENV["AJ_ADAPTER"]] ||= new(ENV["AJ_ADAPTER"])
end
def initialize(adapter_name)
diff --git a/activejob/test/support/integration/test_case_helpers.rb b/activejob/test/support/integration/test_case_helpers.rb
index 7e87ede275..3d9b265b66 100644
--- a/activejob/test/support/integration/test_case_helpers.rb
+++ b/activejob/test/support/integration/test_case_helpers.rb
@@ -1,5 +1,6 @@
-require 'active_support/concern'
-require 'support/integration/jobs_manager'
+# frozen_string_literal: true
+
+require "support/integration/jobs_manager"
module TestCaseHelpers
extend ActiveSupport::Concern
@@ -17,7 +18,7 @@ module TestCaseHelpers
end
end
- protected
+ private
def jobs_manager
JobsManager.current_manager
@@ -28,10 +29,10 @@ module TestCaseHelpers
end
def adapter_is?(*adapter_class_symbols)
- adapter_class_symbols.map(&:to_s).include?(ActiveJob::Base.queue_adapter.class.name.split("::").last.gsub(/Adapter$/, '').underscore)
+ adapter_class_symbols.map(&:to_s).include? ActiveJob::Base.queue_adapter_name
end
- def wait_for_jobs_to_finish_for(seconds=60)
+ def wait_for_jobs_to_finish_for(seconds = 60)
begin
Timeout.timeout(seconds) do
while !job_executed do
@@ -42,7 +43,27 @@ module TestCaseHelpers
end
end
- def job_executed
- Dummy::Application.root.join("tmp/#{@id}").exist?
+ def job_file(id)
+ Dummy::Application.root.join("tmp/#{id}")
+ end
+
+ def job_executed(id = @id)
+ job_file(id).exist?
+ end
+
+ def job_data(id)
+ Marshal.load(File.binread(job_file(id)))
+ end
+
+ def job_executed_at(id = @id)
+ job_data(id)["executed_at"]
+ end
+
+ def job_executed_in_locale(id = @id)
+ job_data(id)["locale"]
+ end
+
+ def job_executed_in_timezone(id = @id)
+ job_data(id)["timezone"]
end
end
diff --git a/activejob/test/support/job_buffer.rb b/activejob/test/support/job_buffer.rb
index 620cb5288d..45a6437685 100644
--- a/activejob/test/support/job_buffer.rb
+++ b/activejob/test/support/job_buffer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module JobBuffer
class << self
def clear
diff --git a/activejob/test/support/que/inline.rb b/activejob/test/support/que/inline.rb
index 0232da1370..4ca65c1cd4 100644
--- a/activejob/test/support/que/inline.rb
+++ b/activejob/test/support/que/inline.rb
@@ -1,4 +1,6 @@
-require 'que'
+# frozen_string_literal: true
+
+require "que"
Que::Job.class_eval do
class << self; alias_method :original_enqueue, :enqueue; end
@@ -6,8 +8,9 @@ Que::Job.class_eval do
if args.last.is_a?(Hash)
options = args.pop
options.delete(:run_at)
+ options.delete(:priority)
args << options unless options.empty?
end
- self.run(*args)
+ run(*args)
end
end
diff --git a/activejob/test/support/queue_classic/inline.rb b/activejob/test/support/queue_classic/inline.rb
index 5743d5bbb5..0695a34c27 100644
--- a/activejob/test/support/queue_classic/inline.rb
+++ b/activejob/test/support/queue_classic/inline.rb
@@ -1,21 +1,24 @@
-require 'queue_classic'
+# frozen_string_literal: true
+
+require "queue_classic"
+require "active_support/core_ext/module/redefine_method"
module QC
class Queue
- def enqueue(method, *args)
- receiver_str, _, message = method.rpartition('.')
+ redefine_method(:enqueue) do |method, *args|
+ receiver_str, _, message = method.rpartition(".")
receiver = eval(receiver_str)
receiver.send(message, *args)
end
- def enqueue_in(seconds, method, *args)
- receiver_str, _, message = method.rpartition('.')
+ redefine_method(:enqueue_in) do |seconds, method, *args|
+ receiver_str, _, message = method.rpartition(".")
receiver = eval(receiver_str)
receiver.send(message, *args)
end
- def enqueue_at(not_before, method, *args)
- receiver_str, _, message = method.rpartition('.')
+ redefine_method(:enqueue_at) do |not_before, method, *args|
+ receiver_str, _, message = method.rpartition(".")
receiver = eval(receiver_str)
receiver.send(message, *args)
end
diff --git a/activejob/test/support/sneakers/inline.rb b/activejob/test/support/sneakers/inline.rb
index 16d9b830fa..e772c68c6e 100644
--- a/activejob/test/support/sneakers/inline.rb
+++ b/activejob/test/support/sneakers/inline.rb
@@ -1,10 +1,13 @@
-require 'sneakers'
+# frozen_string_literal: true
+
+require "sneakers"
+require "active_support/core_ext/module/redefine_method"
module Sneakers
module Worker
module ClassMethods
- def enqueue(msg)
- worker = self.new(nil, nil, {})
+ redefine_method(:enqueue) do |msg|
+ worker = new(nil, nil, {})
worker.work(*msg)
end
end
diff --git a/activejob/test/support/stubs/strong_parameters.rb b/activejob/test/support/stubs/strong_parameters.rb
new file mode 100644
index 0000000000..acba3a4504
--- /dev/null
+++ b/activejob/test/support/stubs/strong_parameters.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class Parameters
+ def initialize(parameters = {})
+ @parameters = parameters.with_indifferent_access
+ end
+
+ def permitted?
+ true
+ end
+
+ def to_h
+ @parameters.to_h
+ end
+end
diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md
index dddfd940bb..6048911217 100644
--- a/activemodel/CHANGELOG.md
+++ b/activemodel/CHANGELOG.md
@@ -1,103 +1,53 @@
-* Ensure `method_missing` is called for methods passed to
- `ActiveModel::Serialization#serializable_hash` that don't exist.
+* Fix numericality validator to still use value before type cast except Active Record.
- *Jay Elaraj*
+ Fixes #33651, #33686.
-* Add `ActiveModel::Dirty#[attr_name]_previously_changed?` and
- `ActiveModel::Dirty#[attr_name]_previous_change` to improve access
- to recorded changes after the model has been saved.
+ *Ryuta Kamizono*
- It makes the dirty-attributes query methods consistent before and after
- saving.
+* Fix `ActiveModel::Serializers::JSON#as_json` method for timestamps.
- *Fernando Tapia Rico*
+ Before:
+ ```
+ contact = Contact.new(created_at: Time.utc(2006, 8, 1))
+ contact.as_json["created_at"] # => 2006-08-01 00:00:00 UTC
+ ```
-* Deprecate the `:tokenizer` option for `validates_length_of`, in favor of
- plain Ruby.
-
- *Sean Griffin*
-
-* Deprecate `ActiveModel::Errors#add_on_empty` and `ActiveModel::Errors#add_on_blank`
- with no replacement.
-
- *Wojciech Wnętrzak*
-
-* Deprecate `ActiveModel::Errors#get`, `ActiveModel::Errors#set` and
- `ActiveModel::Errors#[]=` methods that have inconsistent behavior.
-
- *Wojciech Wnętrzak*
-
-* Allow symbol as values for `tokenize` of `LengthValidator`.
-
- *Kensuke Naito*
-
-* Assigning an unknown attribute key to an `ActiveModel` instance during initialization
- will now raise `ActiveModel::AttributeAssignment::UnknownAttributeError` instead of
- `NoMethodError`.
-
- Example:
-
- User.new(foo: 'some value')
- # => ActiveModel::AttributeAssignment::UnknownAttributeError: unknown attribute 'foo' for User.
-
- *Eugene Gilburg*
-
-* Extracted `ActiveRecord::AttributeAssignment` to `ActiveModel::AttributeAssignment`
- allowing to use it for any object as an includable module.
-
- Example:
-
- class Cat
- include ActiveModel::AttributeAssignment
- attr_accessor :name, :status
- end
-
- cat = Cat.new
- cat.assign_attributes(name: "Gorby", status: "yawning")
- cat.name # => 'Gorby'
- cat.status # => 'yawning'
- cat.assign_attributes(status: "sleeping")
- cat.name # => 'Gorby'
- cat.status # => 'sleeping'
+ After:
+ ```
+ contact = Contact.new(created_at: Time.utc(2006, 8, 1))
+ contact.as_json["created_at"] # => "2006-08-01T00:00:00.000Z"
+ ```
*Bogdan Gusiev*
-* Add `ActiveModel::Errors#details`
-
- To be able to return type of used validator, one can now call `details`
- on errors instance.
+* Allows configurable attribute name for `#has_secure_password`. This
+ still defaults to an attribute named 'password', causing no breaking
+ change. There is a new method `#authenticate_XXX` where XXX is the
+ configured attribute name, making the existing `#authenticate` now an
+ alias for this when the attribute is the default 'password'.
Example:
class User < ActiveRecord::Base
- validates :name, presence: true
+ has_secure_password :recovery_password, validations: false
end
- user = User.new; user.valid?; user.errors.details
- => {name: [{error: :blank}]}
-
- *Wojciech Wnętrzak*
-
-* Change validates_acceptance_of to accept true by default.
+ user = User.new()
+ user.recovery_password = "42password"
+ user.recovery_password_digest # => "$2a$04$iOfhwahFymCs5weB3BNH/uX..."
+ user.authenticate_recovery_password('42password') # => user
- The default for validates_acceptance_of is now "1" and true.
- In the past, only "1" was the default and you were required to add
- accept: true.
+ *Unathi Chonco*
-* Remove deprecated `ActiveModel::Dirty#reset_#{attribute}` and
- `ActiveModel::Dirty#reset_changes`.
+* Add `config.active_model.i18n_full_message` in order to control whether
+ the `full_message` error format can be overridden at the attribute or model
+ level in the locale files. This is `false` by default.
- *Rafael Mendonça França*
+ *Martin Larochelle*
-* Change the way in which callback chains can be halted.
+* Rails 6 requires Ruby 2.4.1 or newer.
- The preferred method to halt a callback chain from now on is to explicitly
- `throw(:abort)`.
- In the past, returning `false` in an ActiveModel or ActiveModel::Validations
- `before_` callback had the side effect of halting the callback chain.
- This is not recommended anymore and, depending on the value of the
- `config.active_support.halt_callback_chains_on_return_false` option, will
- either not work at all or display a deprecation warning.
+ *Jeremy Daer*
-Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/activemodel/CHANGELOG.md) for previous changes.
+Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/activemodel/CHANGELOG.md) for previous changes.
diff --git a/activemodel/MIT-LICENSE b/activemodel/MIT-LICENSE
index 3ec7a617cf..1cb3add0fc 100644
--- a/activemodel/MIT-LICENSE
+++ b/activemodel/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2015 David Heinemeier Hansson
+Copyright (c) 2004-2018 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/activemodel/README.rdoc b/activemodel/README.rdoc
index d954467387..1aaf4813ea 100644
--- a/activemodel/README.rdoc
+++ b/activemodel/README.rdoc
@@ -155,7 +155,7 @@ behavior out of the box:
* Making objects serializable
<tt>ActiveModel::Serialization</tt> provides a standard interface for your object
- to provide +to_json+ or +to_xml+ serialization.
+ to provide +to_json+ serialization.
class SerialPerson
include ActiveModel::Serialization
@@ -177,13 +177,6 @@ behavior out of the box:
s = SerialPerson.new
s.to_json # => "{\"name\":null}"
- class SerialPerson
- include ActiveModel::Serializers::Xml
- end
-
- s = SerialPerson.new
- s.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person...
-
{Learn more}[link:classes/ActiveModel/Serialization.html]
* Internationalization (i18n) support
@@ -242,7 +235,7 @@ behavior out of the box:
The latest version of Active Model can be installed with RubyGems:
- % gem install activemodel
+ $ gem install activemodel
Source code can be downloaded as part of the Rails project on GitHub
@@ -253,20 +246,19 @@ Source code can be downloaded as part of the Rails project on GitHub
Active Model is released under the MIT license:
-* http://www.opensource.org/licenses/MIT
+* https://opensource.org/licenses/MIT
== Support
-API documentation is at
+API documentation is at:
* http://api.rubyonrails.org
-Bug reports can be filed for the Ruby on Rails project here:
+Bug reports for the Ruby on Rails project can be filed here:
* https://github.com/rails/rails/issues
Feature requests should be discussed on the rails-core mailing list here:
* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core
-
diff --git a/activemodel/Rakefile b/activemodel/Rakefile
index 5a67f0a151..d39f50a962 100644
--- a/activemodel/Rakefile
+++ b/activemodel/Rakefile
@@ -1,12 +1,14 @@
-require 'rake/testtask'
+# frozen_string_literal: true
-dir = File.dirname(__FILE__)
+require "rake/testtask"
-task :default => :test
+task default: :test
+
+task :package
Rake::TestTask.new do |t|
t.libs << "test"
- t.test_files = Dir.glob("#{dir}/test/cases/**/*_test.rb")
+ t.test_files = Dir.glob("#{__dir__}/test/cases/**/*_test.rb")
t.warning = true
t.verbose = true
t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
@@ -14,8 +16,8 @@ end
namespace :test do
task :isolated do
- Dir.glob("#{dir}/test/**/*_test.rb").all? do |file|
- sh(Gem.ruby, '-w', "-I#{dir}/lib", "-I#{dir}/test", file)
- end or raise "Failures"
+ Dir.glob("#{__dir__}/test/**/*_test.rb").all? do |file|
+ sh(Gem.ruby, "-w", "-I#{__dir__}/lib", "-I#{__dir__}/test", file)
+ end || raise("Failures")
end
end
diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec
index 8d00b3aa27..493fca698a 100644
--- a/activemodel/activemodel.gemspec
+++ b/activemodel/activemodel.gemspec
@@ -1,24 +1,32 @@
-version = File.read(File.expand_path('../../RAILS_VERSION', __FILE__)).strip
+# frozen_string_literal: true
+
+version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip
Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
- s.name = 'activemodel'
+ s.name = "activemodel"
s.version = version
- s.summary = 'A toolkit for building modeling frameworks (part of Rails).'
- s.description = 'A toolkit for building modeling frameworks like Active Record. Rich support for attributes, callbacks, validations, serialization, internationalization, and testing.'
+ s.summary = "A toolkit for building modeling frameworks (part of Rails)."
+ s.description = "A toolkit for building modeling frameworks like Active Record. Rich support for attributes, callbacks, validations, serialization, internationalization, and testing."
+
+ s.required_ruby_version = ">= 2.4.1"
- s.required_ruby_version = '>= 2.2.2'
+ s.license = "MIT"
- s.license = 'MIT'
+ s.author = "David Heinemeier Hansson"
+ s.email = "david@loudthinking.com"
+ s.homepage = "http://rubyonrails.org"
- s.author = 'David Heinemeier Hansson'
- s.email = 'david@loudthinking.com'
- s.homepage = 'http://www.rubyonrails.org'
+ s.files = Dir["CHANGELOG.md", "MIT-LICENSE", "README.rdoc", "lib/**/*"]
+ s.require_path = "lib"
- s.files = Dir['CHANGELOG.md', 'MIT-LICENSE', 'README.rdoc', 'lib/**/*']
- s.require_path = 'lib'
+ s.metadata = {
+ "source_code_uri" => "https://github.com/rails/rails/tree/v#{version}/activemodel",
+ "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/activemodel/CHANGELOG.md"
+ }
- s.add_dependency 'activesupport', version
+ # NOTE: Please read our dependency guidelines before updating versions:
+ # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves
- s.add_dependency 'builder', '~> 3.1'
+ s.add_dependency "activesupport", version
end
diff --git a/activemodel/bin/test b/activemodel/bin/test
index 404cabba51..c53377cc97 100755
--- a/activemodel/bin/test
+++ b/activemodel/bin/test
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
-COMPONENT_ROOT = File.expand_path("../../", __FILE__)
-require File.expand_path("../tools/test", COMPONENT_ROOT)
-exit Minitest.run(ARGV)
+# frozen_string_literal: true
+
+COMPONENT_ROOT = File.expand_path("..", __dir__)
+require_relative "../../tools/test"
diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb
index 8aa1b6f664..bc10d6b4b9 100644
--- a/activemodel/lib/active_model.rb
+++ b/activemodel/lib/active_model.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
#--
-# Copyright (c) 2004-2015 David Heinemeier Hansson
+# Copyright (c) 2004-2018 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -21,36 +23,39 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
-require 'active_support'
-require 'active_support/rails'
-require 'active_model/version'
+require "active_support"
+require "active_support/rails"
+require "active_model/version"
module ActiveModel
extend ActiveSupport::Autoload
+ autoload :Attribute
+ autoload :Attributes
autoload :AttributeAssignment
autoload :AttributeMethods
- autoload :BlockValidator, 'active_model/validator'
+ autoload :BlockValidator, "active_model/validator"
autoload :Callbacks
autoload :Conversion
autoload :Dirty
- autoload :EachValidator, 'active_model/validator'
+ autoload :EachValidator, "active_model/validator"
autoload :ForbiddenAttributesProtection
autoload :Lint
autoload :Model
- autoload :Name, 'active_model/naming'
+ autoload :Name, "active_model/naming"
autoload :Naming
autoload :SecurePassword
autoload :Serialization
- autoload :TestCase
autoload :Translation
+ autoload :Type
autoload :Validations
autoload :Validator
eager_autoload do
autoload :Errors
- autoload :StrictValidationFailed, 'active_model/errors'
- autoload :UnknownAttributeError, 'active_model/errors'
+ autoload :RangeError, "active_model/errors"
+ autoload :StrictValidationFailed, "active_model/errors"
+ autoload :UnknownAttributeError, "active_model/errors"
end
module Serializers
@@ -58,7 +63,6 @@ module ActiveModel
eager_autoload do
autoload :JSON
- autoload :Xml
end
end
@@ -69,5 +73,5 @@ module ActiveModel
end
ActiveSupport.on_load(:i18n) do
- I18n.load_path << File.dirname(__FILE__) + '/active_model/locale/en.yml'
+ I18n.load_path << File.expand_path("active_model/locale/en.yml", __dir__)
end
diff --git a/activemodel/lib/active_model/attribute.rb b/activemodel/lib/active_model/attribute.rb
new file mode 100644
index 0000000000..75f60d205e
--- /dev/null
+++ b/activemodel/lib/active_model/attribute.rb
@@ -0,0 +1,247 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/object/duplicable"
+
+module ActiveModel
+ class Attribute # :nodoc:
+ class << self
+ def from_database(name, value, type)
+ FromDatabase.new(name, value, type)
+ end
+
+ def from_user(name, value, type, original_attribute = nil)
+ FromUser.new(name, value, type, original_attribute)
+ end
+
+ def with_cast_value(name, value, type)
+ WithCastValue.new(name, value, type)
+ end
+
+ def null(name)
+ Null.new(name)
+ end
+
+ def uninitialized(name, type)
+ Uninitialized.new(name, type)
+ end
+ end
+
+ attr_reader :name, :value_before_type_cast, :type
+
+ # This method should not be called directly.
+ # Use #from_database or #from_user
+ def initialize(name, value_before_type_cast, type, original_attribute = nil)
+ @name = name
+ @value_before_type_cast = value_before_type_cast
+ @type = type
+ @original_attribute = original_attribute
+ end
+
+ def value
+ # `defined?` is cheaper than `||=` when we get back falsy values
+ @value = type_cast(value_before_type_cast) unless defined?(@value)
+ @value
+ end
+
+ def original_value
+ if assigned?
+ original_attribute.original_value
+ else
+ type_cast(value_before_type_cast)
+ end
+ end
+
+ def value_for_database
+ type.serialize(value)
+ end
+
+ def changed?
+ changed_from_assignment? || changed_in_place?
+ end
+
+ def changed_in_place?
+ has_been_read? && type.changed_in_place?(original_value_for_database, value)
+ end
+
+ def forgetting_assignment
+ with_value_from_database(value_for_database)
+ end
+
+ def with_value_from_user(value)
+ type.assert_valid_value(value)
+ self.class.from_user(name, value, type, original_attribute || self)
+ end
+
+ def with_value_from_database(value)
+ self.class.from_database(name, value, type)
+ end
+
+ def with_cast_value(value)
+ self.class.with_cast_value(name, value, type)
+ end
+
+ def with_type(type)
+ if changed_in_place?
+ with_value_from_user(value).with_type(type)
+ else
+ self.class.new(name, value_before_type_cast, type, original_attribute)
+ end
+ end
+
+ def type_cast(*)
+ raise NotImplementedError
+ end
+
+ def initialized?
+ true
+ end
+
+ def came_from_user?
+ false
+ end
+
+ def has_been_read?
+ defined?(@value)
+ end
+
+ def ==(other)
+ self.class == other.class &&
+ name == other.name &&
+ value_before_type_cast == other.value_before_type_cast &&
+ type == other.type
+ end
+ alias eql? ==
+
+ def hash
+ [self.class, name, value_before_type_cast, type].hash
+ end
+
+ def init_with(coder)
+ @name = coder["name"]
+ @value_before_type_cast = coder["value_before_type_cast"]
+ @type = coder["type"]
+ @original_attribute = coder["original_attribute"]
+ @value = coder["value"] if coder.map.key?("value")
+ end
+
+ def encode_with(coder)
+ coder["name"] = name
+ coder["value_before_type_cast"] = value_before_type_cast unless value_before_type_cast.nil?
+ coder["type"] = type if type
+ coder["original_attribute"] = original_attribute if original_attribute
+ coder["value"] = value if defined?(@value)
+ end
+
+ protected
+ def original_value_for_database
+ if assigned?
+ original_attribute.original_value_for_database
+ else
+ _original_value_for_database
+ end
+ end
+
+ private
+ attr_reader :original_attribute
+ alias :assigned? :original_attribute
+
+ def initialize_dup(other)
+ if defined?(@value) && @value.duplicable?
+ @value = @value.dup
+ end
+ end
+
+ def changed_from_assignment?
+ assigned? && type.changed?(original_value, value, value_before_type_cast)
+ end
+
+ def _original_value_for_database
+ type.serialize(original_value)
+ end
+
+ class FromDatabase < Attribute # :nodoc:
+ def type_cast(value)
+ type.deserialize(value)
+ end
+
+ def _original_value_for_database
+ value_before_type_cast
+ end
+ end
+
+ class FromUser < Attribute # :nodoc:
+ def type_cast(value)
+ type.cast(value)
+ end
+
+ def came_from_user?
+ !type.value_constructed_by_mass_assignment?(value_before_type_cast)
+ end
+ end
+
+ class WithCastValue < Attribute # :nodoc:
+ def type_cast(value)
+ value
+ end
+
+ def changed_in_place?
+ false
+ end
+ end
+
+ class Null < Attribute # :nodoc:
+ def initialize(name)
+ super(name, nil, Type.default_value)
+ end
+
+ def type_cast(*)
+ nil
+ end
+
+ def with_type(type)
+ self.class.with_cast_value(name, nil, type)
+ end
+
+ def with_value_from_database(value)
+ raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`"
+ end
+ alias_method :with_value_from_user, :with_value_from_database
+ alias_method :with_cast_value, :with_value_from_database
+ end
+
+ class Uninitialized < Attribute # :nodoc:
+ UNINITIALIZED_ORIGINAL_VALUE = Object.new
+
+ def initialize(name, type)
+ super(name, nil, type)
+ end
+
+ def value
+ if block_given?
+ yield name
+ end
+ end
+
+ def original_value
+ UNINITIALIZED_ORIGINAL_VALUE
+ end
+
+ def value_for_database
+ end
+
+ def initialized?
+ false
+ end
+
+ def forgetting_assignment
+ dup
+ end
+
+ def with_type(type)
+ self.class.new(name, type)
+ end
+ end
+
+ private_constant :FromDatabase, :FromUser, :Null, :Uninitialized, :WithCastValue
+ end
+end
diff --git a/activemodel/lib/active_model/attribute/user_provided_default.rb b/activemodel/lib/active_model/attribute/user_provided_default.rb
new file mode 100644
index 0000000000..9dc16e882d
--- /dev/null
+++ b/activemodel/lib/active_model/attribute/user_provided_default.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require "active_model/attribute"
+
+module ActiveModel
+ class Attribute # :nodoc:
+ class UserProvidedDefault < FromUser # :nodoc:
+ def initialize(name, value, type, database_default)
+ @user_provided_value = value
+ super(name, value, type, database_default)
+ end
+
+ def value_before_type_cast
+ if user_provided_value.is_a?(Proc)
+ @memoized_value_before_type_cast ||= user_provided_value.call
+ else
+ @user_provided_value
+ end
+ end
+
+ def with_type(type)
+ self.class.new(name, user_provided_value, type, original_attribute)
+ end
+
+ def marshal_dump
+ result = [
+ name,
+ value_before_type_cast,
+ type,
+ original_attribute,
+ ]
+ result << value if defined?(@value)
+ result
+ end
+
+ def marshal_load(values)
+ name, user_provided_value, type, original_attribute, value = values
+ @name = name
+ @user_provided_value = user_provided_value
+ @type = type
+ @original_attribute = original_attribute
+ if values.length == 5
+ @value = value
+ end
+ end
+
+ private
+ attr_reader :user_provided_value
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/attribute_assignment.rb b/activemodel/lib/active_model/attribute_assignment.rb
index 087d11f708..f0e3458f51 100644
--- a/activemodel/lib/active_model/attribute_assignment.rb
+++ b/activemodel/lib/active_model/attribute_assignment.rb
@@ -1,4 +1,6 @@
-require 'active_support/core_ext/hash/keys'
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/keys"
module ActiveModel
module AttributeAssignment
@@ -19,34 +21,37 @@ module ActiveModel
# cat = Cat.new
# cat.assign_attributes(name: "Gorby", status: "yawning")
# cat.name # => 'Gorby'
- # cat.status => 'yawning'
+ # cat.status # => 'yawning'
# cat.assign_attributes(status: "sleeping")
# cat.name # => 'Gorby'
- # cat.status => 'sleeping'
+ # cat.status # => 'sleeping'
def assign_attributes(new_attributes)
if !new_attributes.respond_to?(:stringify_keys)
- raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
+ raise ArgumentError, "When assigning attributes, you must pass a hash as an argument, #{new_attributes.class} passed."
end
- return if new_attributes.blank?
+ return if new_attributes.empty?
attributes = new_attributes.stringify_keys
_assign_attributes(sanitize_for_mass_assignment(attributes))
end
+ alias attributes= assign_attributes
+
private
- def _assign_attributes(attributes)
- attributes.each do |k, v|
- _assign_attribute(k, v)
+ def _assign_attributes(attributes)
+ attributes.each do |k, v|
+ _assign_attribute(k, v)
+ end
end
- end
- def _assign_attribute(k, v)
- if respond_to?("#{k}=")
- public_send("#{k}=", v)
- else
- raise UnknownAttributeError.new(self, k)
+ def _assign_attribute(k, v)
+ setter = :"#{k}="
+ if respond_to?(setter)
+ public_send(setter, v)
+ else
+ raise UnknownAttributeError.new(self, k)
+ end
end
- end
end
end
diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb
index 286cd6c206..d8352343e9 100644
--- a/activemodel/lib/active_model/attribute_methods.rb
+++ b/activemodel/lib/active_model/attribute_methods.rb
@@ -1,5 +1,6 @@
-require 'thread_safe'
-require 'mutex_m'
+# frozen_string_literal: true
+
+require "concurrent/map"
module ActiveModel
# Raised when an attribute is not defined.
@@ -68,9 +69,8 @@ module ActiveModel
CALL_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?]?\z/
included do
- class_attribute :attribute_aliases, :attribute_method_matchers, instance_writer: false
- self.attribute_aliases = {}
- self.attribute_method_matchers = [ClassMethods::AttributeMethodMatcher.new]
+ class_attribute :attribute_aliases, instance_writer: false, default: {}
+ class_attribute :attribute_method_matchers, instance_writer: false, default: [ ClassMethods::AttributeMethodMatcher.new ]
end
module ClassMethods
@@ -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
@@ -328,32 +328,29 @@ module ActiveModel
attribute_method_matchers_cache.clear
end
- def generated_attribute_methods #:nodoc:
- @generated_attribute_methods ||= Module.new {
- extend Mutex_m
- }.tap { |mod| include mod }
- end
+ private
+ def generated_attribute_methods
+ @generated_attribute_methods ||= Module.new.tap { |mod| include mod }
+ end
- protected
- def instance_method_already_implemented?(method_name) #:nodoc:
+ 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
- # +match_attribute_method?+. The latter method iterates through an
+ # +matched_attribute_method+. The latter method iterates through an
# array doing regular expression matches, which results in a lot of
# object creations. Most of the time it returns a +nil+ match. As the
# match result is always the same given a +method_name+, this cache is
# 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:
- @attribute_method_matchers_cache ||= ThreadSafe::Cache.new(initial_capacity: 4)
+ 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,8 +362,8 @@ 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:
- defn = if name =~ NAME_COMPILABLE_REGEXP
+ def define_proxy_call(include_private, mod, name, send, *extra)
+ defn = if NAME_COMPILABLE_REGEXP.match?(name)
"def #{name}(*args)"
else
"define_method(:'#{name}') do |*args|"
@@ -374,7 +371,7 @@ module ActiveModel
extra = (extra.map!(&:inspect) << "*args").join(", ")
- target = if send =~ CALL_COMPILABLE_REGEXP
+ target = if CALL_COMPILABLE_REGEXP.match?(send)
"#{"self." unless include_private}#{send}(#{extra})"
else
"send(:'#{send}', #{extra})"
@@ -393,7 +390,7 @@ module ActiveModel
AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name)
def initialize(options = {})
- @prefix, @suffix = options.fetch(:prefix, ''), options.fetch(:suffix, '')
+ @prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
@regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
@method_missing_target = "#{@prefix}attribute#{@suffix}"
@method_name = "#{prefix}%s#{suffix}"
@@ -429,7 +426,7 @@ module ActiveModel
if respond_to_without_attributes?(method, true)
super
else
- match = match_attribute_method?(method.to_s)
+ match = matched_attribute_method(method.to_s)
match ? attribute_missing(match, *args, &block) : super
end
end
@@ -454,19 +451,18 @@ module ActiveModel
# but found among all methods. Which means that the given method is private.
false
else
- !match_attribute_method?(method.to_s).nil?
+ !matched_attribute_method(method.to_s).nil?
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 match_attribute_method?(method_name)
+ def matched_attribute_method(method_name)
matches = self.class.send(:attribute_method_matchers_matching, method_name)
matches.detect { |match| attribute_method?(match.attr_name) }
end
@@ -474,5 +470,47 @@ module ActiveModel
def missing_attribute(attr_name, stack)
raise ActiveModel::MissingAttributeError, "missing attribute: #{attr_name}", stack
end
+
+ def _read_attribute(attr)
+ __send__(attr)
+ end
+
+ module AttrNames # :nodoc:
+ DEF_SAFE_NAME = /\A[a-zA-Z_]\w*\z/
+
+ # We want to generate the methods via module_eval rather than
+ # define_method, because define_method is slower on dispatch.
+ # Evaluating many similar methods may use more memory as the instruction
+ # sequences are duplicated and cached (in MRI). define_method may
+ # be slower on dispatch, but if you're careful about the closure
+ # created, then define_method will consume much less memory.
+ #
+ # But sometimes the database might return columns with
+ # characters that are not allowed in normal method names (like
+ # 'my_column(omg)'. So to work around this we first define with
+ # the __temp__ identifier, and then use alias method to rename
+ # it to what we want.
+ #
+ # We are also defining a constant to hold the frozen string of
+ # the attribute name. Using a constant means that we do not have
+ # to allocate an object on each call to the attribute method.
+ # Making it frozen means that it doesn't get duped when used to
+ # key the @attributes in read_attribute.
+ def self.define_attribute_accessor_method(mod, attr_name, writer: false)
+ method_name = "#{attr_name}#{'=' if writer}"
+ if attr_name.ascii_only? && DEF_SAFE_NAME.match?(attr_name)
+ yield method_name, "'#{attr_name}'.freeze"
+ else
+ safe_name = attr_name.unpack1("h*")
+ const_name = "ATTR_#{safe_name}"
+ const_set(const_name, attr_name) unless const_defined?(const_name)
+ temp_method_name = "__temp__#{safe_name}#{'=' if writer}"
+ attr_name_expr = "::ActiveModel::AttributeMethods::AttrNames::#{const_name}"
+ yield temp_method_name, attr_name_expr
+ mod.send(:alias_method, method_name, temp_method_name)
+ mod.send(:undef_method, temp_method_name)
+ end
+ end
+ end
end
end
diff --git a/activemodel/lib/active_model/attribute_mutation_tracker.rb b/activemodel/lib/active_model/attribute_mutation_tracker.rb
new file mode 100644
index 0000000000..6abf37bd44
--- /dev/null
+++ b/activemodel/lib/active_model/attribute_mutation_tracker.rb
@@ -0,0 +1,119 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/indifferent_access"
+
+module ActiveModel
+ class AttributeMutationTracker # :nodoc:
+ OPTION_NOT_GIVEN = Object.new
+
+ def initialize(attributes)
+ @attributes = attributes
+ @forced_changes = Set.new
+ end
+
+ def changed_attribute_names
+ attr_names.select { |attr_name| changed?(attr_name) }
+ end
+
+ def changed_values
+ attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
+ if changed?(attr_name)
+ result[attr_name] = attributes[attr_name].original_value
+ end
+ end
+ end
+
+ def changes
+ attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
+ change = change_to_attribute(attr_name)
+ if change
+ result.merge!(attr_name => change)
+ end
+ end
+ end
+
+ def change_to_attribute(attr_name)
+ attr_name = attr_name.to_s
+ if changed?(attr_name)
+ [attributes[attr_name].original_value, attributes.fetch_value(attr_name)]
+ end
+ end
+
+ def any_changes?
+ attr_names.any? { |attr| changed?(attr) }
+ end
+
+ def changed?(attr_name, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN)
+ attr_name = attr_name.to_s
+ forced_changes.include?(attr_name) ||
+ attributes[attr_name].changed? &&
+ (OPTION_NOT_GIVEN == from || attributes[attr_name].original_value == from) &&
+ (OPTION_NOT_GIVEN == to || attributes[attr_name].value == to)
+ end
+
+ def changed_in_place?(attr_name)
+ attributes[attr_name.to_s].changed_in_place?
+ end
+
+ def forget_change(attr_name)
+ attr_name = attr_name.to_s
+ attributes[attr_name] = attributes[attr_name].forgetting_assignment
+ forced_changes.delete(attr_name)
+ end
+
+ def original_value(attr_name)
+ attributes[attr_name.to_s].original_value
+ end
+
+ def force_change(attr_name)
+ forced_changes << attr_name.to_s
+ end
+
+ private
+ attr_reader :attributes, :forced_changes
+
+ def attr_names
+ attributes.keys
+ end
+ end
+
+ class NullMutationTracker # :nodoc:
+ include Singleton
+
+ def changed_attribute_names(*)
+ []
+ end
+
+ def changed_values(*)
+ {}
+ end
+
+ def changes(*)
+ {}
+ end
+
+ def change_to_attribute(attr_name)
+ end
+
+ def any_changes?(*)
+ false
+ end
+
+ def changed?(*)
+ false
+ end
+
+ def changed_in_place?(*)
+ false
+ end
+
+ def forget_change(*)
+ end
+
+ def original_value(*)
+ end
+
+ def force_change(*)
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/attribute_set.rb b/activemodel/lib/active_model/attribute_set.rb
new file mode 100644
index 0000000000..a890ee3932
--- /dev/null
+++ b/activemodel/lib/active_model/attribute_set.rb
@@ -0,0 +1,114 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/object/deep_dup"
+require "active_model/attribute_set/builder"
+require "active_model/attribute_set/yaml_encoder"
+
+module ActiveModel
+ class AttributeSet # :nodoc:
+ delegate :each_value, :fetch, :except, to: :attributes
+
+ def initialize(attributes)
+ @attributes = attributes
+ end
+
+ def [](name)
+ attributes[name] || Attribute.null(name)
+ end
+
+ def []=(name, value)
+ attributes[name] = value
+ end
+
+ def values_before_type_cast
+ attributes.transform_values(&:value_before_type_cast)
+ end
+
+ def to_hash
+ initialized_attributes.transform_values(&:value)
+ end
+ alias_method :to_h, :to_hash
+
+ def key?(name)
+ attributes.key?(name) && self[name].initialized?
+ end
+
+ def keys
+ attributes.each_key.select { |name| self[name].initialized? }
+ end
+
+ if defined?(JRUBY_VERSION)
+ # This form is significantly faster on JRuby, and this is one of our biggest hotspots.
+ # https://github.com/jruby/jruby/pull/2562
+ def fetch_value(name, &block)
+ self[name].value(&block)
+ end
+ else
+ def fetch_value(name)
+ self[name].value { |n| yield n if block_given? }
+ end
+ end
+
+ def write_from_database(name, value)
+ attributes[name] = self[name].with_value_from_database(value)
+ end
+
+ def write_from_user(name, value)
+ attributes[name] = self[name].with_value_from_user(value)
+ end
+
+ def write_cast_value(name, value)
+ attributes[name] = self[name].with_cast_value(value)
+ end
+
+ def freeze
+ @attributes.freeze
+ super
+ end
+
+ def deep_dup
+ self.class.allocate.tap do |copy|
+ copy.instance_variable_set(:@attributes, attributes.deep_dup)
+ end
+ end
+
+ def initialize_dup(_)
+ @attributes = attributes.dup
+ super
+ end
+
+ def initialize_clone(_)
+ @attributes = attributes.clone
+ super
+ end
+
+ def reset(key)
+ if key?(key)
+ write_from_database(key, nil)
+ end
+ end
+
+ def accessed
+ attributes.select { |_, attr| attr.has_been_read? }.keys
+ end
+
+ def map(&block)
+ new_attributes = attributes.transform_values(&block)
+ AttributeSet.new(new_attributes)
+ end
+
+ def ==(other)
+ attributes == other.attributes
+ end
+
+ protected
+
+ attr_reader :attributes
+
+ private
+
+ def initialized_attributes
+ attributes.select { |_, attr| attr.initialized? }
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/attribute_set/builder.rb b/activemodel/lib/active_model/attribute_set/builder.rb
new file mode 100644
index 0000000000..2b1c2206ec
--- /dev/null
+++ b/activemodel/lib/active_model/attribute_set/builder.rb
@@ -0,0 +1,124 @@
+# frozen_string_literal: true
+
+require "active_model/attribute"
+
+module ActiveModel
+ class AttributeSet # :nodoc:
+ class Builder # :nodoc:
+ attr_reader :types, :default_attributes
+
+ def initialize(types, default_attributes = {})
+ @types = types
+ @default_attributes = default_attributes
+ end
+
+ def build_from_database(values = {}, additional_types = {})
+ attributes = LazyAttributeHash.new(types, values, additional_types, default_attributes)
+ AttributeSet.new(attributes)
+ end
+ end
+ end
+
+ class LazyAttributeHash # :nodoc:
+ delegate :transform_values, :each_key, :each_value, :fetch, :except, to: :materialize
+
+ def initialize(types, values, additional_types, default_attributes, delegate_hash = {})
+ @types = types
+ @values = values
+ @additional_types = additional_types
+ @materialized = false
+ @delegate_hash = delegate_hash
+ @default_attributes = default_attributes
+ end
+
+ def key?(key)
+ delegate_hash.key?(key) || values.key?(key) || types.key?(key)
+ end
+
+ def [](key)
+ delegate_hash[key] || assign_default_value(key)
+ end
+
+ def []=(key, value)
+ if frozen?
+ raise RuntimeError, "Can't modify frozen hash"
+ end
+ delegate_hash[key] = value
+ end
+
+ def deep_dup
+ dup.tap do |copy|
+ copy.instance_variable_set(:@delegate_hash, delegate_hash.transform_values(&:dup))
+ end
+ end
+
+ def initialize_dup(_)
+ @delegate_hash = Hash[delegate_hash]
+ super
+ end
+
+ def select
+ keys = types.keys | values.keys | delegate_hash.keys
+ keys.each_with_object({}) do |key, hash|
+ attribute = self[key]
+ if yield(key, attribute)
+ hash[key] = attribute
+ end
+ end
+ end
+
+ def ==(other)
+ if other.is_a?(LazyAttributeHash)
+ materialize == other.materialize
+ else
+ materialize == other
+ end
+ end
+
+ def marshal_dump
+ [@types, @values, @additional_types, @default_attributes, @delegate_hash]
+ end
+
+ def marshal_load(values)
+ if values.is_a?(Hash)
+ empty_hash = {}.freeze
+ initialize(empty_hash, empty_hash, empty_hash, empty_hash, values)
+ @materialized = true
+ else
+ initialize(*values)
+ end
+ end
+
+ protected
+ def materialize
+ unless @materialized
+ values.each_key { |key| self[key] }
+ types.each_key { |key| self[key] }
+ unless frozen?
+ @materialized = true
+ end
+ end
+ delegate_hash
+ end
+
+ private
+ attr_reader :types, :values, :additional_types, :delegate_hash, :default_attributes
+
+ def assign_default_value(name)
+ type = additional_types.fetch(name, types[name])
+ value_present = true
+ value = values.fetch(name) { value_present = false }
+
+ if value_present
+ delegate_hash[name] = Attribute.from_database(name, value, type)
+ elsif types.key?(name)
+ attr = default_attributes[name]
+ if attr
+ delegate_hash[name] = attr.dup
+ else
+ delegate_hash[name] = Attribute.uninitialized(name, type)
+ end
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/attribute_set/yaml_encoder.rb b/activemodel/lib/active_model/attribute_set/yaml_encoder.rb
new file mode 100644
index 0000000000..ea1efc160e
--- /dev/null
+++ b/activemodel/lib/active_model/attribute_set/yaml_encoder.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module ActiveModel
+ class AttributeSet
+ # Attempts to do more intelligent YAML dumping of an
+ # ActiveModel::AttributeSet to reduce the size of the resulting string
+ class YAMLEncoder # :nodoc:
+ def initialize(default_types)
+ @default_types = default_types
+ end
+
+ def encode(attribute_set, coder)
+ coder["concise_attributes"] = attribute_set.each_value.map do |attr|
+ if attr.type.equal?(default_types[attr.name])
+ attr.with_type(nil)
+ else
+ attr
+ end
+ end
+ end
+
+ def decode(coder)
+ if coder["attributes"]
+ coder["attributes"]
+ else
+ attributes_hash = Hash[coder["concise_attributes"].map do |attr|
+ if attr.type.nil?
+ attr = attr.with_type(default_types[attr.name])
+ end
+ [attr.name, attr]
+ end]
+ AttributeSet.new(attributes_hash)
+ end
+ end
+
+ private
+ attr_reader :default_types
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/attributes.rb b/activemodel/lib/active_model/attributes.rb
new file mode 100644
index 0000000000..c3a446098c
--- /dev/null
+++ b/activemodel/lib/active_model/attributes.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+require "active_model/attribute_set"
+require "active_model/attribute/user_provided_default"
+
+module ActiveModel
+ module Attributes #:nodoc:
+ extend ActiveSupport::Concern
+ include ActiveModel::AttributeMethods
+
+ included do
+ attribute_method_suffix "="
+ class_attribute :attribute_types, :_default_attributes, instance_accessor: false
+ self.attribute_types = Hash.new(Type.default_value)
+ self._default_attributes = AttributeSet.new({})
+ end
+
+ module ClassMethods
+ def attribute(name, type = Type::Value.new, **options)
+ name = name.to_s
+ if type.is_a?(Symbol)
+ type = ActiveModel::Type.lookup(type, **options.except(:default))
+ end
+ self.attribute_types = attribute_types.merge(name => type)
+ define_default_attribute(name, options.fetch(:default, NO_DEFAULT_PROVIDED), type)
+ define_attribute_method(name)
+ end
+
+ private
+
+ def define_method_attribute=(name)
+ ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
+ generated_attribute_methods, name, writer: true,
+ ) do |temp_method_name, attr_name_expr|
+ generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def #{temp_method_name}(value)
+ name = #{attr_name_expr}
+ write_attribute(name, value)
+ end
+ RUBY
+ end
+ end
+
+ NO_DEFAULT_PROVIDED = Object.new # :nodoc:
+ private_constant :NO_DEFAULT_PROVIDED
+
+ def define_default_attribute(name, value, type)
+ self._default_attributes = _default_attributes.deep_dup
+ if value == NO_DEFAULT_PROVIDED
+ default_attribute = _default_attributes[name].with_type(type)
+ else
+ default_attribute = Attribute::UserProvidedDefault.new(
+ name,
+ value,
+ type,
+ _default_attributes.fetch(name.to_s) { nil },
+ )
+ end
+ _default_attributes[name] = default_attribute
+ end
+ end
+
+ def initialize(*)
+ @attributes = self.class._default_attributes.deep_dup
+ super
+ end
+
+ def attributes
+ @attributes.to_hash
+ end
+
+ private
+
+ def write_attribute(attr_name, value)
+ name = if self.class.attribute_alias?(attr_name)
+ self.class.attribute_alias(attr_name).to_s
+ else
+ attr_name.to_s
+ end
+
+ @attributes.write_from_user(name, value)
+ value
+ end
+
+ def attribute(attr_name)
+ name = if self.class.attribute_alias?(attr_name)
+ self.class.attribute_alias(attr_name).to_s
+ else
+ attr_name.to_s
+ end
+ @attributes.fetch_value(name)
+ end
+
+ # Handle *= for method_missing.
+ def attribute=(attribute_name, value)
+ write_attribute(attribute_name, value)
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb
index 2cf39b68fb..fde3381df2 100644
--- a/activemodel/lib/active_model/callbacks.rb
+++ b/activemodel/lib/active_model/callbacks.rb
@@ -1,4 +1,6 @@
-require 'active_support/core_ext/array/extract_options'
+# frozen_string_literal: true
+
+require "active_support/core_ext/array/extract_options"
module ActiveModel
# == Active \Model \Callbacks
@@ -56,6 +58,9 @@ module ActiveModel
#
# Would only create the +after_create+ and +before_create+ callback methods in
# your class.
+ #
+ # NOTE: Calling the same callback multiple times will overwrite previous callback definitions.
+ #
module Callbacks
def self.extended(base) #:nodoc:
base.class_eval do
@@ -98,8 +103,8 @@ module ActiveModel
# end
# end
#
- # NOTE: +method_name+ passed to `define_model_callbacks` must not end with
- # `!`, `?` or `=`.
+ # NOTE: +method_name+ passed to define_model_callbacks must not end with
+ # <tt>!</tt>, <tt>?</tt> or <tt>=</tt>.
def define_model_callbacks(*callbacks)
options = callbacks.extract_options!
options = {
@@ -121,28 +126,30 @@ module ActiveModel
private
- def _define_before_model_callback(klass, callback) #:nodoc:
- klass.define_singleton_method("before_#{callback}") do |*args, &block|
- set_callback(:"#{callback}", :before, *args, &block)
+ def _define_before_model_callback(klass, callback)
+ klass.define_singleton_method("before_#{callback}") do |*args, **options, &block|
+ options.assert_valid_keys(:if, :unless, :prepend)
+ set_callback(:"#{callback}", :before, *args, options, &block)
+ end
end
- end
- def _define_around_model_callback(klass, callback) #:nodoc:
- klass.define_singleton_method("around_#{callback}") do |*args, &block|
- set_callback(:"#{callback}", :around, *args, &block)
+ def _define_around_model_callback(klass, callback)
+ klass.define_singleton_method("around_#{callback}") do |*args, **options, &block|
+ options.assert_valid_keys(:if, :unless, :prepend)
+ set_callback(:"#{callback}", :around, *args, options, &block)
+ end
end
- end
- def _define_after_model_callback(klass, callback) #:nodoc:
- klass.define_singleton_method("after_#{callback}") do |*args, &block|
- options = args.extract_options!
- options[:prepend] = true
- conditional = ActiveSupport::Callbacks::Conditionals::Value.new { |v|
- v != false
- }
- options[:if] = Array(options[:if]) << conditional
- set_callback(:"#{callback}", :after, *(args << options), &block)
+ def _define_after_model_callback(klass, callback)
+ klass.define_singleton_method("after_#{callback}") do |*args, **options, &block|
+ options.assert_valid_keys(:if, :unless, :prepend)
+ options[:prepend] = true
+ conditional = ActiveSupport::Callbacks::Conditionals::Value.new { |v|
+ v != false
+ }
+ options[:if] = Array(options[:if]) << conditional
+ set_callback(:"#{callback}", :after, *args, options, &block)
+ end
end
- end
end
end
diff --git a/activemodel/lib/active_model/conversion.rb b/activemodel/lib/active_model/conversion.rb
index 9de6ea65be..82713ddc81 100644
--- a/activemodel/lib/active_model/conversion.rb
+++ b/activemodel/lib/active_model/conversion.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveModel
# == Active \Model \Conversion
#
@@ -40,15 +42,19 @@ module ActiveModel
self
end
- # Returns an Array of all key attributes if any is set, regardless if
- # the object is persisted or not. Returns +nil+ if there are no key attributes.
+ # Returns an Array of all key attributes if any of the attributes is set, whether or not
+ # the object is persisted. Returns +nil+ if there are no key attributes.
#
# class Person
# include ActiveModel::Conversion
# attr_accessor :id
+ #
+ # def initialize(id)
+ # @id = id
+ # end
# end
#
- # person = Person.create(id: 1)
+ # person = Person.new(1)
# person.to_key # => [1]
def to_key
key = respond_to?(:id) && id
@@ -61,15 +67,20 @@ module ActiveModel
# class Person
# include ActiveModel::Conversion
# attr_accessor :id
+ #
+ # def initialize(id)
+ # @id = id
+ # end
+ #
# def persisted?
# true
# end
# end
#
- # person = Person.create(id: 1)
+ # person = Person.new(1)
# person.to_param # => "1"
def to_param
- (persisted? && key = to_key) ? key.join('-') : nil
+ (persisted? && key = to_key) ? key.join("-") : nil
end
# Returns a +string+ identifying the path associated with the object.
@@ -92,7 +103,7 @@ module ActiveModel
@_to_partial_path ||= begin
element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(name))
collection = ActiveSupport::Inflector.tableize(name)
- "#{collection}/#{element}".freeze
+ "#{collection}/#{element}"
end
end
end
diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb
index 0169c20e0b..0d9e761b1e 100644
--- a/activemodel/lib/active_model/dirty.rb
+++ b/activemodel/lib/active_model/dirty.rb
@@ -1,5 +1,8 @@
-require 'active_support/hash_with_indifferent_access'
-require 'active_support/core_ext/object/duplicable'
+# frozen_string_literal: true
+
+require "active_support/hash_with_indifferent_access"
+require "active_support/core_ext/object/duplicable"
+require "active_model/attribute_mutation_tracker"
module ActiveModel
# == Active \Model \Dirty
@@ -26,6 +29,10 @@ module ActiveModel
#
# define_attribute_methods :name
#
+ # def initialize
+ # @name = nil
+ # end
+ #
# def name
# @name
# end
@@ -62,11 +69,11 @@ module ActiveModel
# person.name = 'Bob'
# person.changed? # => true
# person.name_changed? # => true
- # person.name_changed?(from: "Uncle Bob", to: "Bob") # => true
- # person.name_was # => "Uncle Bob"
- # person.name_change # => ["Uncle Bob", "Bob"]
+ # person.name_changed?(from: nil, to: "Bob") # => true
+ # person.name_was # => nil
+ # person.name_change # => [nil, "Bob"]
# person.name = 'Bill'
- # person.name_change # => ["Uncle Bob", "Bill"]
+ # person.name_change # => [nil, "Bill"]
#
# Save the changes:
#
@@ -76,9 +83,9 @@ module ActiveModel
#
# Reset the changes:
#
- # person.previous_changes # => {"name" => ["Uncle Bob", "Bill"]}
+ # person.previous_changes # => {"name" => [nil, "Bill"]}
# person.name_previously_changed? # => true
- # person.name_previous_change # => ["Uncle Bob", "Bill"]
+ # person.name_previous_change # => [nil, "Bill"]
# person.reload!
# person.previous_changes # => {}
#
@@ -115,13 +122,38 @@ module ActiveModel
extend ActiveSupport::Concern
include ActiveModel::AttributeMethods
+ OPTION_NOT_GIVEN = Object.new # :nodoc:
+ private_constant :OPTION_NOT_GIVEN
+
included do
- attribute_method_suffix '_changed?', '_change', '_will_change!', '_was'
- attribute_method_suffix '_previously_changed?', '_previous_change'
- attribute_method_affix prefix: 'restore_', suffix: '!'
+ attribute_method_suffix "_changed?", "_change", "_will_change!", "_was"
+ attribute_method_suffix "_previously_changed?", "_previous_change"
+ attribute_method_affix prefix: "restore_", suffix: "!"
end
- # Returns +true+ if any of the attributes have unsaved changes, +false+ otherwise.
+ def initialize_dup(other) # :nodoc:
+ super
+ if self.class.respond_to?(:_default_attributes)
+ @attributes = self.class._default_attributes.map do |attr|
+ attr.with_value_from_user(@attributes.fetch_value(attr.name))
+ end
+ end
+ @mutations_from_database = nil
+ end
+
+ # Clears dirty data and moves +changes+ to +previously_changed+ and
+ # +mutations_from_database+ to +mutations_before_last_save+ respectively.
+ def changes_applied
+ unless defined?(@attributes)
+ @previously_changed = changes
+ end
+ @mutations_before_last_save = mutations_from_database
+ @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
+ forget_attribute_assignments
+ @mutations_from_database = nil
+ end
+
+ # Returns +true+ if any of the attributes has unsaved changes, +false+ otherwise.
#
# person.changed? # => false
# person.name = 'bob'
@@ -139,6 +171,60 @@ module ActiveModel
changed_attributes.keys
end
+ # Handles <tt>*_changed?</tt> for +method_missing+.
+ def attribute_changed?(attr, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) # :nodoc:
+ !!changes_include?(attr) &&
+ (to == OPTION_NOT_GIVEN || to == _read_attribute(attr)) &&
+ (from == OPTION_NOT_GIVEN || from == changed_attributes[attr])
+ end
+
+ # Handles <tt>*_was</tt> for +method_missing+.
+ def attribute_was(attr) # :nodoc:
+ attribute_changed?(attr) ? changed_attributes[attr] : _read_attribute(attr)
+ end
+
+ # Handles <tt>*_previously_changed?</tt> for +method_missing+.
+ def attribute_previously_changed?(attr) #:nodoc:
+ previous_changes_include?(attr)
+ end
+
+ # Restore all previous data of the provided attributes.
+ def restore_attributes(attributes = changed)
+ attributes.each { |attr| restore_attribute! attr }
+ end
+
+ # Clears all dirty data: current changes and previous changes.
+ def clear_changes_information
+ @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
+ @mutations_before_last_save = nil
+ @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
+ forget_attribute_assignments
+ @mutations_from_database = nil
+ end
+
+ def clear_attribute_changes(attr_names)
+ attributes_changed_by_setter.except!(*attr_names)
+ attr_names.each do |attr_name|
+ clear_attribute_change(attr_name)
+ end
+ end
+
+ # Returns a hash of the attributes with unsaved changes indicating their original
+ # values like <tt>attr => original value</tt>.
+ #
+ # person.name # => "bob"
+ # person.name = 'robert'
+ # person.changed_attributes # => {"name" => "bob"}
+ def changed_attributes
+ # This should only be set by methods which will call changed_attributes
+ # multiple times when it is known that the computed value cannot change.
+ if defined?(@cached_changed_attributes)
+ @cached_changed_attributes
+ else
+ attributes_changed_by_setter.reverse_merge(mutations_from_database.changed_values).freeze
+ end
+ end
+
# Returns a hash of changed attributes indicating their original
# and new values like <tt>attr => [original value, new value]</tt>.
#
@@ -146,7 +232,9 @@ module ActiveModel
# person.name = 'bob'
# person.changes # => { "name" => ["bill", "bob"] }
def changes
- ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }]
+ cache_changed_attributes do
+ ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }]
+ end
end
# Returns a hash of attributes that were changed before the model was saved.
@@ -157,88 +245,82 @@ module ActiveModel
# person.previous_changes # => {"name" => ["bob", "robert"]}
def previous_changes
@previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new
+ @previously_changed.merge(mutations_before_last_save.changes)
end
- # Returns a hash of the attributes with unsaved changes indicating their original
- # values like <tt>attr => original value</tt>.
- #
- # person.name # => "bob"
- # person.name = 'robert'
- # person.changed_attributes # => {"name" => "bob"}
- def changed_attributes
- @changed_attributes ||= ActiveSupport::HashWithIndifferentAccess.new
+ def attribute_changed_in_place?(attr_name) # :nodoc:
+ mutations_from_database.changed_in_place?(attr_name)
end
- # Handles <tt>*_changed?</tt> for +method_missing+.
- def attribute_changed?(attr, options = {}) #:nodoc:
- result = changes_include?(attr)
- result &&= options[:to] == __send__(attr) if options.key?(:to)
- result &&= options[:from] == changed_attributes[attr] if options.key?(:from)
- result
- end
+ private
+ def clear_attribute_change(attr_name)
+ mutations_from_database.forget_change(attr_name)
+ end
- # Handles <tt>*_was</tt> for +method_missing+.
- def attribute_was(attr) # :nodoc:
- attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
- end
+ def mutations_from_database
+ unless defined?(@mutations_from_database)
+ @mutations_from_database = nil
+ end
+ @mutations_from_database ||= if defined?(@attributes)
+ ActiveModel::AttributeMutationTracker.new(@attributes)
+ else
+ NullMutationTracker.instance
+ end
+ end
- # Handles <tt>*_previously_changed?</tt> for +method_missing+.
- def attribute_previously_changed?(attr, options = {}) #:nodoc:
- previous_changes_include?(attr)
- end
+ def forget_attribute_assignments
+ @attributes = @attributes.map(&:forgetting_assignment) if defined?(@attributes)
+ end
- # Restore all previous data of the provided attributes.
- def restore_attributes(attributes = changed)
- attributes.each { |attr| restore_attribute! attr }
- end
+ def mutations_before_last_save
+ @mutations_before_last_save ||= ActiveModel::NullMutationTracker.instance
+ end
- private
+ def cache_changed_attributes
+ @cached_changed_attributes = changed_attributes
+ yield
+ ensure
+ clear_changed_attributes_cache
+ end
+
+ def clear_changed_attributes_cache
+ remove_instance_variable(:@cached_changed_attributes) if defined?(@cached_changed_attributes)
+ end
# Returns +true+ if attr_name is changed, +false+ otherwise.
def changes_include?(attr_name)
- attributes_changed_by_setter.include?(attr_name)
+ attributes_changed_by_setter.include?(attr_name) || mutations_from_database.changed?(attr_name)
end
alias attribute_changed_by_setter? changes_include?
# Returns +true+ if attr_name were changed before the model was saved,
# +false+ otherwise.
def previous_changes_include?(attr_name)
- @previously_changed.include?(attr_name)
- end
-
- # Removes current changes and makes them accessible through +previous_changes+.
- def changes_applied # :doc:
- @previously_changed = changes
- @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
- end
-
- # Clears all dirty data: current changes and previous changes.
- def clear_changes_information # :doc:
- @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
- @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
+ previous_changes.include?(attr_name)
end
# Handles <tt>*_change</tt> for +method_missing+.
def attribute_change(attr)
- [changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
+ [changed_attributes[attr], _read_attribute(attr)] if attribute_changed?(attr)
end
# Handles <tt>*_previous_change</tt> for +method_missing+.
def attribute_previous_change(attr)
- @previously_changed[attr] if attribute_previously_changed?(attr)
+ previous_changes[attr]
end
# Handles <tt>*_will_change!</tt> for +method_missing+.
def attribute_will_change!(attr)
- return if attribute_changed?(attr)
+ unless attribute_changed?(attr)
+ begin
+ value = _read_attribute(attr)
+ value = value.duplicable? ? value.clone : value
+ rescue TypeError, NoMethodError
+ end
- begin
- value = __send__(attr)
- value = value.duplicable? ? value.clone : value
- rescue TypeError, NoMethodError
+ set_attribute_was(attr, value)
end
-
- set_attribute_was(attr, value)
+ mutations_from_database.force_change(attr)
end
# Handles <tt>restore_*!</tt> for +method_missing+.
@@ -249,18 +331,13 @@ module ActiveModel
end
end
- # This is necessary because `changed_attributes` might be overridden in
- # other implementations (e.g. in `ActiveRecord`)
- alias_method :attributes_changed_by_setter, :changed_attributes # :nodoc:
+ def attributes_changed_by_setter
+ @attributes_changed_by_setter ||= ActiveSupport::HashWithIndifferentAccess.new
+ end
# Force an attribute to have a particular "before" value
def set_attribute_was(attr, old_value)
attributes_changed_by_setter[attr] = old_value
end
-
- # Remove changes information for the provided attributes.
- def clear_attribute_changes(attributes) # :doc:
- attributes_changed_by_setter.except!(*attributes)
- end
end
end
diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb
index 29e0c977ce..9de6b609a3 100644
--- a/activemodel/lib/active_model/errors.rb
+++ b/activemodel/lib/active_model/errors.rb
@@ -1,9 +1,9 @@
-# -*- coding: utf-8 -*-
+# frozen_string_literal: true
-require 'active_support/core_ext/array/conversions'
-require 'active_support/core_ext/string/inflections'
-require 'active_support/core_ext/object/deep_dup'
-require 'active_support/core_ext/string/filters'
+require "active_support/core_ext/array/conversions"
+require "active_support/core_ext/string/inflections"
+require "active_support/core_ext/object/deep_dup"
+require "active_support/core_ext/string/filters"
module ActiveModel
# == Active \Model \Errors
@@ -62,6 +62,11 @@ module ActiveModel
CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank, :strict]
MESSAGE_OPTIONS = [:message]
+ class << self
+ attr_accessor :i18n_full_message # :nodoc:
+ end
+ self.i18n_full_message = false
+
attr_reader :messages, :details
# Pass in the instance of the object that is using the errors object.
@@ -73,8 +78,8 @@ module ActiveModel
# end
def initialize(base)
@base = base
- @messages = Hash.new { |messages, attribute| messages[attribute] = [] }
- @details = Hash.new { |details, attribute| details[attribute] = [] }
+ @messages = apply_default_array({})
+ @details = apply_default_array({})
end
def initialize_dup(other) # :nodoc:
@@ -83,6 +88,30 @@ module ActiveModel
super
end
+ # Copies the errors from <tt>other</tt>.
+ #
+ # other - The ActiveModel::Errors instance.
+ #
+ # Examples
+ #
+ # person.errors.copy!(other)
+ def copy!(other) # :nodoc:
+ @messages = other.messages.dup
+ @details = other.details.dup
+ end
+
+ # Merges the errors from <tt>other</tt>.
+ #
+ # other - The ActiveModel::Errors instance.
+ #
+ # Examples
+ #
+ # person.errors.merge!(other)
+ def merge!(other)
+ @messages.merge!(other.messages) { |_, ary1, ary2| ary1 + ary2 }
+ @details.merge!(other.details) { |_, ary1, ary2| ary1 + ary2 }
+ end
+
# Clear the error messages.
#
# person.errors.full_messages # => ["name cannot be nil"]
@@ -100,49 +129,21 @@ module ActiveModel
# person.errors.include?(:name) # => true
# person.errors.include?(:age) # => false
def include?(attribute)
- messages[attribute].present?
+ attribute = attribute.to_sym
+ messages.key?(attribute) && messages[attribute].present?
end
alias :has_key? :include?
alias :key? :include?
- # Get messages for +key+.
- #
- # person.errors.messages # => {:name=>["cannot be nil"]}
- # person.errors.get(:name) # => ["cannot be nil"]
- # person.errors.get(:age) # => []
- def get(key)
- ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
- ActiveModel::Errors#get is deprecated and will be removed in Rails 5.1.
-
- To achieve the same use model.errors[:#{key}].
- MESSAGE
-
- messages[key]
- end
-
- # Set messages for +key+ to +value+.
- #
- # person.errors[:name] # => ["cannot be nil"]
- # person.errors.set(:name, ["can't be nil"])
- # person.errors[:name] # => ["can't be nil"]
- def set(key, value)
- ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
- ActiveModel::Errors#set is deprecated and will be removed in Rails 5.1.
-
- Use model.errors.add(:#{key}, #{value.inspect}) instead.
- MESSAGE
-
- messages[key] = value
- end
-
# Delete messages for +key+. Returns the deleted messages.
#
- # person.errors[:name] # => ["cannot be nil"]
+ # person.errors[:name] # => ["cannot be nil"]
# person.errors.delete(:name) # => ["cannot be nil"]
- # person.errors[:name] # => []
+ # 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
@@ -154,20 +155,6 @@ module ActiveModel
messages[attribute.to_sym]
end
- # Adds to the supplied attribute the supplied error message.
- #
- # person.errors[:name] = "must be set"
- # person.errors[:name] # => ['must be set']
- def []=(attribute, error)
- ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
- ActiveModel::Errors#[]= is deprecated and will be removed in Rails 5.1.
-
- Use model.errors.add(:#{attribute}, #{error.inspect}) instead.
- MESSAGE
-
- messages[attribute.to_sym] << error
- end
-
# Iterates through each error key, value pair in the error messages hash.
# Yields the attribute and the error for that attribute. If the attribute
# has more than one error message, yields once for each error message.
@@ -204,7 +191,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.select do |key, value|
+ !value.empty?
+ end.values
end
# Returns all message keys.
@@ -212,7 +201,9 @@ module ActiveModel
# person.errors.messages # => {:name=>["cannot be nil", "must be specified"]}
# person.errors.keys # => [:name]
def keys
- messages.keys
+ messages.select do |key, value|
+ !value.empty?
+ end.keys
end
# Returns +true+ if no errors are found, +false+ otherwise.
@@ -236,7 +227,7 @@ module ActiveModel
# # <error>name can't be blank</error>
# # <error>name must be specified</error>
# # </errors>
- def to_xml(options={})
+ def to_xml(options = {})
to_a.to_xml({ root: "errors", skip_types: true }.merge!(options))
end
@@ -246,7 +237,7 @@ module ActiveModel
#
# person.errors.as_json # => {:name=>["cannot be nil"]}
# person.errors.as_json(full_messages: true) # => {:name=>["name cannot be nil"]}
- def as_json(options=nil)
+ def as_json(options = nil)
to_hash(options && options[:full_messages])
end
@@ -257,11 +248,11 @@ module ActiveModel
# person.errors.to_hash(true) # => {:name=>["name cannot be nil"]}
def to_hash(full_messages = false)
if full_messages
- self.messages.each_with_object({}) do |(attribute, array), messages|
+ messages.each_with_object({}) do |(attribute, array), messages|
messages[attribute] = array.map { |message| full_message(attribute, message) }
end
else
- self.messages.dup
+ without_default_proc(messages)
end
end
@@ -291,9 +282,9 @@ module ActiveModel
# <tt>:strict</tt> option can also be set to any other exception.
#
# person.errors.add(:name, :invalid, strict: true)
- # # => ActiveModel::StrictValidationFailed: name is invalid
+ # # => ActiveModel::StrictValidationFailed: Name is invalid
# person.errors.add(:name, :invalid, strict: NameIsInvalid)
- # # => NameIsInvalid: name is invalid
+ # # => NameIsInvalid: Name is invalid
#
# person.errors.messages # => {}
#
@@ -308,7 +299,7 @@ module ActiveModel
# # => {:base=>[{error: :name_or_email_blank}]}
def add(attribute, message = :invalid, options = {})
message = message.call if message.respond_to?(:call)
- detail = normalize_detail(attribute, message, options)
+ detail = normalize_detail(message, options)
message = normalize_message(attribute, message, options)
if exception = options[:strict]
exception = ActiveModel::StrictValidationFailed if exception == true
@@ -319,58 +310,30 @@ module ActiveModel
messages[attribute.to_sym] << message
end
- # Will add an error message to each of the attributes in +attributes+
- # that is empty.
- #
- # person.errors.add_on_empty(:name)
- # person.errors.messages
- # # => {:name=>["can't be empty"]}
- def add_on_empty(attributes, options = {})
- ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
- ActiveModel::Errors#add_on_empty is deprecated and will be removed in Rails 5.1
-
- To achieve the same use:
-
- errors.add(attribute, :empty, options) if value.nil? || value.empty?
- MESSAGE
-
- Array(attributes).each do |attribute|
- value = @base.send(:read_attribute_for_validation, attribute)
- is_empty = value.respond_to?(:empty?) ? value.empty? : false
- add(attribute, :empty, options) if value.nil? || is_empty
- end
- end
-
- # Will add an error message to each of the attributes in +attributes+ that
- # is blank (using Object#blank?).
- #
- # person.errors.add_on_blank(:name)
- # person.errors.messages
- # # => {:name=>["can't be blank"]}
- def add_on_blank(attributes, options = {})
- ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
- ActiveModel::Errors#add_on_blank is deprecated and will be removed in Rails 5.1
-
- To achieve the same use:
-
- errors.add(attribute, :empty, options) if value.blank?
- MESSAGE
-
- Array(attributes).each do |attribute|
- value = @base.send(:read_attribute_for_validation, attribute)
- add(attribute, :blank, options) if value.blank?
- end
- end
-
# Returns +true+ if an error on the attribute with the given message is
- # present, +false+ otherwise. +message+ is treated the same as for +add+.
+ # present, or +false+ otherwise. +message+ is treated the same as for +add+.
#
# person.errors.add :name, :blank
- # person.errors.added? :name, :blank # => true
+ # person.errors.added? :name, :blank # => true
+ # person.errors.added? :name, "can't be blank" # => true
+ #
+ # If the error message requires an option, then it returns +true+ with
+ # the correct option, or +false+ with an incorrect or missing option.
+ #
+ # person.errors.add :name, :too_long, { count: 25 }
+ # person.errors.added? :name, :too_long, count: 25 # => true
+ # person.errors.added? :name, "is too long (maximum is 25 characters)" # => true
+ # person.errors.added? :name, :too_long, count: 24 # => false
+ # person.errors.added? :name, :too_long # => false
+ # person.errors.added? :name, "is too long" # => false
def added?(attribute, message = :invalid, options = {})
message = message.call if message.respond_to?(:call)
- message = normalize_message(attribute, message, options)
- self[attribute].include? message
+
+ if message.is_a? Symbol
+ details[attribute.to_sym].include? normalize_detail(message, options)
+ else
+ self[attribute].include? message
+ end
end
# Returns all the full error messages in an array.
@@ -399,21 +362,63 @@ 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
# Returns a full message for a given attribute.
#
# person.errors.full_message(:name, 'is invalid') # => "Name is invalid"
+ #
+ # The `"%{attribute} %{message}"` error format can be overridden with either
+ #
+ # * <tt>activemodel.errors.models.person/contacts/addresses.attributes.street.format</tt>
+ # * <tt>activemodel.errors.models.person/contacts/addresses.format</tt>
+ # * <tt>activemodel.errors.models.person.attributes.name.format</tt>
+ # * <tt>activemodel.errors.models.person.format</tt>
+ # * <tt>errors.format</tt>
def full_message(attribute, message)
return message if attribute == :base
- attr_name = attribute.to_s.tr('.', '_').humanize
+ attribute = attribute.to_s
+
+ if self.class.i18n_full_message && @base.class.respond_to?(:i18n_scope)
+ attribute = attribute.remove(/\[\d\]/)
+ parts = attribute.split(".")
+ attribute_name = parts.pop
+ namespace = parts.join("/") unless parts.empty?
+ attributes_scope = "#{@base.class.i18n_scope}.errors.models"
+
+ if namespace
+ defaults = @base.class.lookup_ancestors.map do |klass|
+ [
+ :"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.attributes.#{attribute_name}.format",
+ :"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.format",
+ ]
+ end
+ else
+ defaults = @base.class.lookup_ancestors.map do |klass|
+ [
+ :"#{attributes_scope}.#{klass.model_name.i18n_key}.attributes.#{attribute_name}.format",
+ :"#{attributes_scope}.#{klass.model_name.i18n_key}.format",
+ ]
+ end
+ end
+
+ defaults.flatten!
+ else
+ defaults = []
+ end
+
+ defaults << :"errors.format"
+ defaults << "%{attribute} %{message}"
+
+ attr_name = attribute.tr(".", "_").humanize
attr_name = @base.class.human_attribute_name(attribute, default: attr_name)
- I18n.t(:"errors.format", {
- default: "%{attribute} %{message}",
+
+ I18n.t(defaults.shift,
+ default: defaults,
attribute: attr_name,
- message: message
- })
+ message: message)
end
# Translates an error message in its default scope
@@ -444,21 +449,19 @@ module ActiveModel
type = options.delete(:message) if options[:message].is_a?(Symbol)
if @base.class.respond_to?(:i18n_scope)
- defaults = @base.class.lookup_ancestors.map do |klass|
- [ :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}",
- :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ]
+ i18n_scope = @base.class.i18n_scope.to_s
+ defaults = @base.class.lookup_ancestors.flat_map do |klass|
+ [ :"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}",
+ :"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ]
end
+ defaults << :"#{i18n_scope}.errors.messages.#{type}"
else
defaults = []
end
- defaults << :"#{@base.class.i18n_scope}.errors.messages.#{type}" if @base.class.respond_to?(:i18n_scope)
defaults << :"errors.attributes.#{attribute}.#{type}"
defaults << :"errors.messages.#{type}"
- defaults.compact!
- defaults.flatten!
-
key = defaults.shift
defaults = options.delete(:message) if options[:message]
value = (attribute != :base ? @base.send(:read_attribute_for_validation, attribute) : nil)
@@ -467,12 +470,30 @@ module ActiveModel
default: defaults,
model: @base.model_name.human,
attribute: @base.class.human_attribute_name(attribute),
- value: value
+ value: value,
+ object: @base
}.merge!(options)
I18n.translate(key, options)
end
+ def marshal_dump # :nodoc:
+ [@base, without_default_proc(@messages), without_default_proc(@details)]
+ end
+
+ 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
@@ -483,9 +504,20 @@ module ActiveModel
end
end
- def normalize_detail(attribute, message, options)
+ def normalize_detail(message, options)
{ error: message }.merge(options.except(*CALLBACKS_OPTIONS + MESSAGE_OPTIONS))
end
+
+ def without_default_proc(hash)
+ hash.dup.tap do |new_h|
+ new_h.default_proc = nil
+ end
+ end
+
+ def apply_default_array(hash)
+ hash.default_proc = proc { |h, key| h[key] = [] }
+ hash
+ end
end
# Raised when a validation cannot be corrected by end users and are considered
@@ -506,7 +538,20 @@ module ActiveModel
class StrictValidationFailed < StandardError
end
+ # Raised when attribute values are out of range.
+ class RangeError < ::RangeError
+ end
+
# Raised when unknown attributes are supplied via mass assignment.
+ #
+ # class Person
+ # include ActiveModel::AttributeAssignment
+ # include ActiveModel::Validations
+ # end
+ #
+ # person = Person.new
+ # person.assign_attributes(name: 'Gorby')
+ # # => ActiveModel::UnknownAttributeError: unknown attribute 'name' for Person.
class UnknownAttributeError < NoMethodError
attr_reader :record, :attribute
diff --git a/activemodel/lib/active_model/forbidden_attributes_protection.rb b/activemodel/lib/active_model/forbidden_attributes_protection.rb
index b4fa378601..4b37f80c52 100644
--- a/activemodel/lib/active_model/forbidden_attributes_protection.rb
+++ b/activemodel/lib/active_model/forbidden_attributes_protection.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveModel
# Raised when forbidden attributes are used for mass assignment.
#
@@ -15,10 +17,11 @@ module ActiveModel
end
module ForbiddenAttributesProtection # :nodoc:
- protected
+ private
def sanitize_for_mass_assignment(attributes)
- if attributes.respond_to?(:permitted?) && !attributes.permitted?
- raise ActiveModel::ForbiddenAttributesError
+ if attributes.respond_to?(:permitted?)
+ raise ActiveModel::ForbiddenAttributesError if !attributes.permitted?
+ attributes.to_h
else
attributes
end
diff --git a/activemodel/lib/active_model/gem_version.rb b/activemodel/lib/active_model/gem_version.rb
index 762f4fe939..cef5441e4a 100644
--- a/activemodel/lib/active_model/gem_version.rb
+++ b/activemodel/lib/active_model/gem_version.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveModel
# Returns the version of the currently loaded \Active \Model as a <tt>Gem::Version</tt>
def self.gem_version
@@ -5,7 +7,7 @@ module ActiveModel
end
module VERSION
- MAJOR = 5
+ MAJOR = 6
MINOR = 0
TINY = 0
PRE = "alpha"
diff --git a/activemodel/lib/active_model/lint.rb b/activemodel/lib/active_model/lint.rb
index 010eaeb170..b7ceabb59a 100644
--- a/activemodel/lib/active_model/lint.rb
+++ b/activemodel/lib/active_model/lint.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveModel
module Lint
# == Active \Model \Lint \Tests
@@ -20,7 +22,6 @@ module ActiveModel
# to <tt>to_model</tt>. It is perfectly fine for <tt>to_model</tt> to return
# +self+.
module Tests
-
# Passes if the object's model responds to <tt>to_key</tt> and if calling
# this method returns +nil+ when the object is not persisted.
# Fails otherwise.
@@ -28,7 +29,7 @@ module ActiveModel
# <tt>to_key</tt> returns an Enumerable of all (primary) key attributes
# of the model, and is used to a generate unique DOM id for the object.
def test_to_key
- assert model.respond_to?(:to_key), "The model should respond to to_key"
+ assert_respond_to model, :to_key
def model.persisted?() false end
assert model.to_key.nil?, "to_key should return nil when `persisted?` returns false"
end
@@ -43,7 +44,7 @@ module ActiveModel
# tests for this behavior in lint because it doesn't make sense to force
# any of the possible implementation strategies on the implementer.
def test_to_param
- assert model.respond_to?(:to_param), "The model should respond to to_param"
+ assert_respond_to model, :to_param
def model.to_key() [1] end
def model.persisted?() false end
assert model.to_param.nil?, "to_param should return nil when `persisted?` returns false"
@@ -55,7 +56,7 @@ module ActiveModel
# <tt>to_partial_path</tt> is used for looking up partials. For example,
# a BlogPost model might return "blog_posts/blog_post".
def test_to_partial_path
- assert model.respond_to?(:to_partial_path), "The model should respond to to_partial_path"
+ assert_respond_to model, :to_partial_path
assert_kind_of String, model.to_partial_path
end
@@ -67,7 +68,7 @@ module ActiveModel
# will route to the create action. If it is persisted, a form for the
# object will route to the update action.
def test_persisted?
- assert model.respond_to?(:persisted?), "The model should respond to persisted?"
+ assert_respond_to model, :persisted?
assert_boolean model.persisted?, "persisted?"
end
@@ -78,14 +79,14 @@ module ActiveModel
#
# Check ActiveModel::Naming for more information.
def test_model_naming
- assert model.class.respond_to?(:model_name), "The model class should respond to model_name"
+ assert_respond_to model.class, :model_name
model_name = model.class.model_name
- assert model_name.respond_to?(:to_str)
- assert model_name.human.respond_to?(:to_str)
- assert model_name.singular.respond_to?(:to_str)
- assert model_name.plural.respond_to?(:to_str)
+ assert_respond_to model_name, :to_str
+ assert_respond_to model_name.human, :to_str
+ assert_respond_to model_name.singular, :to_str
+ assert_respond_to model_name.plural, :to_str
- assert model.respond_to?(:model_name), "The model instance should respond to model_name"
+ assert_respond_to model, :model_name
assert_equal model.model_name, model.class.model_name
end
@@ -99,13 +100,13 @@ module ActiveModel
# If localization is used, the strings should be localized for the current
# locale. If no error is present, the method should return an empty array.
def test_errors_aref
- assert model.respond_to?(:errors), "The model should respond to errors"
+ assert_respond_to model, :errors
assert model.errors[:hello].is_a?(Array), "errors#[] should return an Array"
end
private
def model
- assert @model.respond_to?(:to_model), "The object should respond to to_model"
+ assert_respond_to @model, :to_model
@model.to_model
end
diff --git a/activemodel/lib/active_model/model.rb b/activemodel/lib/active_model/model.rb
index dac8d549a7..fc52cd4fdf 100644
--- a/activemodel/lib/active_model/model.rb
+++ b/activemodel/lib/active_model/model.rb
@@ -1,12 +1,13 @@
-module ActiveModel
+# frozen_string_literal: true
+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:
#
@@ -76,7 +77,7 @@ module ActiveModel
# person = Person.new(name: 'bob', age: '18')
# person.name # => "bob"
# person.age # => "18"
- def initialize(attributes={})
+ def initialize(attributes = {})
assign_attributes(attributes) if attributes
super()
diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb
index 1f1749af4e..bf23fa3c05 100644
--- a/activemodel/lib/active_model/naming.rb
+++ b/activemodel/lib/active_model/naming.rb
@@ -1,7 +1,8 @@
-require 'active_support/core_ext/hash/except'
-require 'active_support/core_ext/module/introspection'
-require 'active_support/core_ext/module/remove_method'
-require 'active_support/core_ext/module/delegation'
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/except"
+require "active_support/core_ext/module/introspection"
+require "active_support/core_ext/module/redefine_method"
module ActiveModel
class Name
@@ -48,7 +49,7 @@ module ActiveModel
# :method: <=>
#
# :call-seq:
- # ==(other)
+ # <=>(other)
#
# Equivalent to <tt>String#<=></tt>.
#
@@ -110,6 +111,22 @@ module ActiveModel
# BlogPost.model_name.eql?('Blog Post') # => false
##
+ # :method: match?
+ #
+ # :call-seq:
+ # match?(regexp)
+ #
+ # Equivalent to <tt>String#match?</tt>. Match the class name against the
+ # given regexp. Returns +true+ if there is a match, otherwise +false+.
+ #
+ # class BlogPost
+ # extend ActiveModel::Naming
+ # end
+ #
+ # BlogPost.model_name.match?(/Post/) # => true
+ # BlogPost.model_name.match?(/\d/) # => false
+
+ ##
# :method: to_s
#
# :call-seq:
@@ -130,7 +147,7 @@ module ActiveModel
# to_str()
#
# Equivalent to +to_s+.
- delegate :==, :===, :<=>, :=~, :"!~", :eql?, :to_s,
+ delegate :==, :===, :<=>, :=~, :"!~", :eql?, :match?, :to_s,
:to_str, :as_json, to: :name
# Returns a new ActiveModel::Name instance. By default, the +namespace+
@@ -149,7 +166,7 @@ module ActiveModel
raise ArgumentError, "Class name cannot be blank. You need to supply a name argument when anonymous class given" if @name.blank?
- @unnamespaced = @name.sub(/^#{namespace.name}::/, '') if namespace
+ @unnamespaced = @name.sub(/^#{namespace.name}::/, "") if namespace
@klass = klass
@singular = _singularize(@name)
@plural = ActiveSupport::Inflector.pluralize(@singular)
@@ -164,7 +181,7 @@ module ActiveModel
@route_key << "_index" if @plural == @singular
end
- # Transform the model name into a more humane format, using I18n. By default,
+ # Transform the model name into a more human format, using I18n. By default,
# it will underscore then humanize the class name.
#
# class BlogPost
@@ -174,7 +191,7 @@ module ActiveModel
# BlogPost.model_name.human # => "Blog post"
#
# Specify +options+ with additional translating options.
- def human(options={})
+ def human(options = {})
return @human unless @klass.respond_to?(:lookup_ancestors) &&
@klass.respond_to?(:i18n_scope)
@@ -191,9 +208,9 @@ module ActiveModel
private
- def _singularize(string)
- ActiveSupport::Inflector.underscore(string).tr('/', '_')
- end
+ def _singularize(string)
+ ActiveSupport::Inflector.underscore(string).tr("/", "_")
+ end
end
# == Active \Model \Naming
@@ -217,7 +234,7 @@ module ActiveModel
# provided method below, or rolling your own is required.
module Naming
def self.extended(base) #:nodoc:
- base.remove_possible_method :model_name
+ base.silence_redefinition_of_method :model_name
base.delegate :model_name, to: :class
end
@@ -226,7 +243,7 @@ module ActiveModel
# (See ActiveModel::Name for more information).
#
# class Person
- # include ActiveModel::Model
+ # extend ActiveModel::Naming
# end
#
# Person.model_name.name # => "Person"
@@ -235,7 +252,7 @@ module ActiveModel
# Person.model_name.plural # => "people"
def model_name
@_model_name ||= begin
- namespace = self.parents.detect do |n|
+ namespace = module_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/railtie.rb b/activemodel/lib/active_model/railtie.rb
index 1671eb7bd4..0ed70bd473 100644
--- a/activemodel/lib/active_model/railtie.rb
+++ b/activemodel/lib/active_model/railtie.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "active_model"
require "rails"
@@ -5,8 +7,14 @@ module ActiveModel
class Railtie < Rails::Railtie # :nodoc:
config.eager_load_namespaces << ActiveModel
+ config.active_model = ActiveSupport::OrderedOptions.new
+
initializer "active_model.secure_password" do
ActiveModel::SecurePassword.min_cost = Rails.env.test?
end
+
+ initializer "active_model.i18n_full_message" do
+ ActiveModel::Errors.i18n_full_message = config.active_model.delete(:i18n_full_message) || false
+ end
end
end
diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb
index 89da74efa8..51d54f34f3 100644
--- a/activemodel/lib/active_model/secure_password.rb
+++ b/activemodel/lib/active_model/secure_password.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
module ActiveModel
module SecurePassword
extend ActiveSupport::Concern
- # BCrypt hash function can handle maximum 72 characters, and if we pass
- # password of length more than 72 characters it ignores extra characters.
+ # BCrypt hash function can handle maximum 72 bytes, and if we pass
+ # password of length more than 72 bytes it ignores extra characters.
# Hence need to put a restriction on password length.
MAX_PASSWORD_LENGTH_ALLOWED = 72
@@ -14,15 +16,16 @@ module ActiveModel
module ClassMethods
# Adds methods to set and authenticate against a BCrypt password.
- # This mechanism requires you to have a +password_digest+ attribute.
+ # This mechanism requires you to have a +XXX_digest+ attribute.
+ # Where +XXX+ is the attribute name of your desired password.
#
# The following validations are added automatically:
# * Password must be present on creation
- # * Password length should be less than or equal to 72 characters
- # * Confirmation of password (using a +password_confirmation+ attribute)
+ # * Password length should be less than or equal to 72 bytes
+ # * Confirmation of password (using a +XXX_confirmation+ attribute)
#
- # If password confirmation validation is not needed, simply leave out the
- # value for +password_confirmation+ (i.e. don't provide a form field for
+ # If confirmation validation is not needed, simply leave out the
+ # value for +XXX_confirmation+ (i.e. don't provide a form field for
# it). When this attribute has a +nil+ value, the validation will not be
# triggered.
#
@@ -35,9 +38,10 @@ module ActiveModel
#
# Example using Active Record (which automatically includes ActiveModel::SecurePassword):
#
- # # Schema: User(name:string, password_digest:string)
+ # # Schema: User(name:string, password_digest:string, recovery_password_digest:string)
# class User < ActiveRecord::Base
# has_secure_password
+ # has_secure_password :recovery_password, validations: false
# end
#
# user = User.new(name: 'david', password: '', password_confirmation: 'nomatch')
@@ -46,24 +50,59 @@ module ActiveModel
# user.save # => false, confirmation doesn't match
# user.password_confirmation = 'mUc3m00RsqyRe'
# user.save # => true
+ # user.recovery_password = "42password"
+ # user.recovery_password_digest # => "$2a$04$iOfhwahFymCs5weB3BNH/uXkTG65HR.qpW.bNhEjFP3ftli3o5DQC"
+ # user.save # => true
# user.authenticate('notright') # => false
# user.authenticate('mUc3m00RsqyRe') # => user
+ # user.authenticate_recovery_password('42password') # => user
# User.find_by(name: 'david').try(:authenticate, 'notright') # => false
# User.find_by(name: 'david').try(:authenticate, 'mUc3m00RsqyRe') # => user
- def has_secure_password(options = {})
+ def has_secure_password(attribute = :password, validations: true)
# Load bcrypt gem only when has_secure_password is used.
# This is to avoid ActiveModel (and by extension the entire framework)
# being dependent on a binary library.
begin
- require 'bcrypt'
+ require "bcrypt"
rescue LoadError
$stderr.puts "You don't have bcrypt installed in your application. Please add it to your Gemfile and run bundle install"
raise
end
- include InstanceMethodsOnActivation
+ attr_reader attribute
+
+ define_method("#{attribute}=") do |unencrypted_password|
+ if unencrypted_password.nil?
+ self.send("#{attribute}_digest=", nil)
+ elsif !unencrypted_password.empty?
+ instance_variable_set("@#{attribute}", unencrypted_password)
+ cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
+ self.send("#{attribute}_digest=", BCrypt::Password.create(unencrypted_password, cost: cost))
+ end
+ end
+
+ define_method("#{attribute}_confirmation=") do |unencrypted_password|
+ instance_variable_set("@#{attribute}_confirmation", unencrypted_password)
+ end
+
+ # Returns +self+ if the password is correct, otherwise +false+.
+ #
+ # class User < ActiveRecord::Base
+ # has_secure_password validations: false
+ # end
+ #
+ # user = User.new(name: 'david', password: 'mUc3m00RsqyRe')
+ # user.save
+ # user.authenticate_password('notright') # => false
+ # user.authenticate_password('mUc3m00RsqyRe') # => user
+ define_method("authenticate_#{attribute}") do |unencrypted_password|
+ attribute_digest = send("#{attribute}_digest")
+ BCrypt::Password.new(attribute_digest).is_password?(unencrypted_password) && self
+ end
+
+ alias_method :authenticate, :authenticate_password if attribute == :password
- if options.fetch(:validations, true)
+ if validations
include ActiveModel::Validations
# This ensures the model has a password by checking whether the password_digest
@@ -71,57 +110,13 @@ module ActiveModel
# when there is an error, the message is added to the password attribute instead
# so that the error message will make sense to the end-user.
validate do |record|
- record.errors.add(:password, :blank) unless record.password_digest.present?
+ record.errors.add(attribute, :blank) unless record.send("#{attribute}_digest").present?
end
- validates_length_of :password, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
- validates_confirmation_of :password, allow_blank: true
- end
- end
- end
-
- module InstanceMethodsOnActivation
- # Returns +self+ if the password is correct, otherwise +false+.
- #
- # class User < ActiveRecord::Base
- # has_secure_password validations: false
- # end
- #
- # user = User.new(name: 'david', password: 'mUc3m00RsqyRe')
- # user.save
- # user.authenticate('notright') # => false
- # user.authenticate('mUc3m00RsqyRe') # => user
- def authenticate(unencrypted_password)
- BCrypt::Password.new(password_digest).is_password?(unencrypted_password) && self
- end
-
- attr_reader :password
-
- # Encrypts the password into the +password_digest+ attribute, only if the
- # new password is not empty.
- #
- # class User < ActiveRecord::Base
- # has_secure_password validations: false
- # end
- #
- # user = User.new
- # user.password = nil
- # user.password_digest # => nil
- # user.password = 'mUc3m00RsqyRe'
- # user.password_digest # => "$2a$10$4LEA7r4YmNHtvlAvHhsYAeZmk/xeUVtMTYqwIvYY76EW5GUqDiP4."
- def password=(unencrypted_password)
- if unencrypted_password.nil?
- self.password_digest = nil
- elsif !unencrypted_password.empty?
- @password = unencrypted_password
- cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
- self.password_digest = BCrypt::Password.create(unencrypted_password, cost: cost)
+ validates_length_of attribute, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
+ validates_confirmation_of attribute, allow_blank: true
end
end
-
- def password_confirmation=(unencrypted_password)
- @password_confirmation = unencrypted_password
- end
end
end
end
diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb
index f95849eb84..c4b7b32291 100644
--- a/activemodel/lib/active_model/serialization.rb
+++ b/activemodel/lib/active_model/serialization.rb
@@ -1,5 +1,7 @@
-require 'active_support/core_ext/hash/except'
-require 'active_support/core_ext/hash/slice'
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/except"
+require "active_support/core_ext/hash/slice"
module ActiveModel
# == Active \Model \Serialization
@@ -31,16 +33,14 @@ module ActiveModel
# of the attributes hash's keys. In order to override this behavior, take a look
# at the private method +read_attribute_for_serialization+.
#
- # Most of the time though, either the JSON or XML serializations are needed.
- # Both of these modules automatically include the
- # <tt>ActiveModel::Serialization</tt> module, so there is no need to
- # explicitly include it.
+ # ActiveModel::Serializers::JSON module automatically includes
+ # the <tt>ActiveModel::Serialization</tt> module, so there is no need to
+ # explicitly include <tt>ActiveModel::Serialization</tt>.
#
- # A minimal implementation including XML and JSON would be:
+ # A minimal implementation including JSON would be:
#
# class Person
# include ActiveModel::Serializers::JSON
- # include ActiveModel::Serializers::Xml
#
# attr_accessor :name
#
@@ -55,13 +55,11 @@ module ActiveModel
# person.serializable_hash # => {"name"=>nil}
# person.as_json # => {"name"=>nil}
# person.to_json # => "{\"name\":null}"
- # person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person...
#
# person.name = "Bob"
# person.serializable_hash # => {"name"=>"Bob"}
# person.as_json # => {"name"=>"Bob"}
# person.to_json # => "{\"name\":\"Bob\"}"
- # person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person...
#
# Valid options are <tt>:only</tt>, <tt>:except</tt>, <tt>:methods</tt> and
# <tt>:include</tt>. The following are all valid examples:
@@ -181,7 +179,7 @@ module ActiveModel
return unless includes = options[:include]
unless includes.is_a?(Hash)
- includes = Hash[Array(includes).map { |n| n.is_a?(Hash) ? n.to_a.first : [n, {}] }]
+ includes = Hash[Array(includes).flat_map { |n| n.is_a?(Hash) ? n.to_a : [[n, {}]] }]
end
includes.each do |association, opts|
diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb
index b66dbf1afe..f77fb98c32 100644
--- a/activemodel/lib/active_model/serializers/json.rb
+++ b/activemodel/lib/active_model/serializers/json.rb
@@ -1,4 +1,6 @@
-require 'active_support/json'
+# frozen_string_literal: true
+
+require "active_support/json"
module ActiveModel
module Serializers
@@ -10,8 +12,7 @@ module ActiveModel
included do
extend ActiveModel::Naming
- class_attribute :include_root_in_json
- self.include_root_in_json = false
+ class_attribute :include_root_in_json, instance_writer: false, default: false
end
# Returns a hash representing the model. Some configuration can be
@@ -25,13 +26,13 @@ module ActiveModel
# user = User.find(1)
# user.as_json
# # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
- # # "created_at" => "2006/08/01", "awesome" => true}
+ # # "created_at" => "2006-08-01T17:27:133.000Z", "awesome" => true}
#
# ActiveRecord::Base.include_root_in_json = true
#
# user.as_json
# # => { "user" => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
- # # "created_at" => "2006/08/01", "awesome" => true } }
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true } }
#
# This behavior can also be achieved by setting the <tt>:root</tt> option
# to +true+ as in:
@@ -39,7 +40,7 @@ module ActiveModel
# user = User.find(1)
# user.as_json(root: true)
# # => { "user" => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
- # # "created_at" => "2006/08/01", "awesome" => true } }
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true } }
#
# Without any +options+, the returned Hash will include all the model's
# attributes.
@@ -47,7 +48,7 @@ module ActiveModel
# user = User.find(1)
# user.as_json
# # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
- # # "created_at" => "2006/08/01", "awesome" => true}
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true}
#
# The <tt>:only</tt> and <tt>:except</tt> options can be used to limit
# the attributes included, and work similar to the +attributes+ method.
@@ -62,14 +63,14 @@ module ActiveModel
#
# user.as_json(methods: :permalink)
# # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
- # # "created_at" => "2006/08/01", "awesome" => true,
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true,
# # "permalink" => "1-konata-izumi" }
#
# To include associations use <tt>:include</tt>:
#
# user.as_json(include: :posts)
# # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
- # # "created_at" => "2006/08/01", "awesome" => true,
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true,
# # "posts" => [ { "id" => 1, "author_id" => 1, "title" => "Welcome to the weblog" },
# # { "id" => 2, "author_id" => 1, "title" => "So I was thinking" } ] }
#
@@ -80,7 +81,7 @@ module ActiveModel
# only: :body } },
# only: :title } })
# # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
- # # "created_at" => "2006/08/01", "awesome" => true,
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true,
# # "posts" => [ { "comments" => [ { "body" => "1st post!" }, { "body" => "Second!" } ],
# # "title" => "Welcome to the weblog" },
# # { "comments" => [ { "body" => "Don't think too hard" } ],
@@ -92,11 +93,12 @@ module ActiveModel
include_root_in_json
end
+ hash = serializable_hash(options).as_json
if root
root = model_name.element if root == true
- { root => serializable_hash(options) }
+ { root => hash }
else
- serializable_hash(options)
+ hash
end
end
@@ -134,7 +136,7 @@ module ActiveModel
# person.name # => "bob"
# person.age # => 22
# person.awesome # => true
- def from_json(json, include_root=include_root_in_json)
+ def from_json(json, include_root = include_root_in_json)
hash = ActiveSupport::JSON.decode(json)
hash = hash.values.first if include_root
self.attributes = hash
diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb
deleted file mode 100644
index e33c766627..0000000000
--- a/activemodel/lib/active_model/serializers/xml.rb
+++ /dev/null
@@ -1,238 +0,0 @@
-require 'active_support/core_ext/module/attribute_accessors'
-require 'active_support/core_ext/array/conversions'
-require 'active_support/core_ext/hash/conversions'
-require 'active_support/core_ext/hash/slice'
-require 'active_support/core_ext/time/acts_like'
-
-module ActiveModel
- module Serializers
- # == \Active \Model XML Serializer
- module Xml
- extend ActiveSupport::Concern
- include ActiveModel::Serialization
-
- included do
- extend ActiveModel::Naming
- end
-
- class Serializer #:nodoc:
- class Attribute #:nodoc:
- attr_reader :name, :value, :type
-
- def initialize(name, serializable, value)
- @name, @serializable = name, serializable
-
- if value.acts_like?(:time) && value.respond_to?(:in_time_zone)
- value = value.in_time_zone
- end
-
- @value = value
- @type = compute_type
- end
-
- def decorations
- decorations = {}
- decorations[:encoding] = 'base64' if type == :binary
- decorations[:type] = (type == :string) ? nil : type
- decorations[:nil] = true if value.nil?
- decorations
- end
-
- protected
-
- def compute_type
- return if value.nil?
- type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name]
- type ||= :string if value.respond_to?(:to_str)
- type ||= :yaml
- type
- end
- end
-
- class MethodAttribute < Attribute #:nodoc:
- end
-
- attr_reader :options
-
- def initialize(serializable, options = nil)
- @serializable = serializable
- @options = options ? options.dup : {}
- end
-
- def serializable_hash
- @serializable.serializable_hash(@options.except(:include))
- end
-
- def serializable_collection
- methods = Array(options[:methods]).map(&:to_s)
- serializable_hash.map do |name, value|
- name = name.to_s
- if methods.include?(name)
- self.class::MethodAttribute.new(name, @serializable, value)
- else
- self.class::Attribute.new(name, @serializable, value)
- end
- end
- end
-
- def serialize
- require 'builder' unless defined? ::Builder
-
- options[:indent] ||= 2
- options[:builder] ||= ::Builder::XmlMarkup.new(indent: options[:indent])
-
- @builder = options[:builder]
- @builder.instruct! unless options[:skip_instruct]
-
- root = (options[:root] || @serializable.model_name.element).to_s
- root = ActiveSupport::XmlMini.rename_key(root, options)
-
- args = [root]
- args << { xmlns: options[:namespace] } if options[:namespace]
- args << { type: options[:type] } if options[:type] && !options[:skip_types]
-
- @builder.tag!(*args) do
- add_attributes_and_methods
- add_includes
- add_extra_behavior
- add_procs
- yield @builder if block_given?
- end
- end
-
- private
-
- def add_extra_behavior
- end
-
- def add_attributes_and_methods
- serializable_collection.each do |attribute|
- key = ActiveSupport::XmlMini.rename_key(attribute.name, options)
- ActiveSupport::XmlMini.to_tag(key, attribute.value,
- options.merge(attribute.decorations))
- end
- end
-
- def add_includes
- @serializable.send(:serializable_add_includes, options) do |association, records, opts|
- add_associations(association, records, opts)
- end
- end
-
- # TODO: This can likely be cleaned up to simple use ActiveSupport::XmlMini.to_tag as well.
- def add_associations(association, records, opts)
- merged_options = opts.merge(options.slice(:builder, :indent))
- merged_options[:skip_instruct] = true
-
- [:skip_types, :dasherize, :camelize].each do |key|
- merged_options[key] = options[key] if merged_options[key].nil? && !options[key].nil?
- end
-
- if records.respond_to?(:to_ary)
- records = records.to_ary
-
- tag = ActiveSupport::XmlMini.rename_key(association.to_s, options)
- type = options[:skip_types] ? { } : { type: "array" }
- association_name = association.to_s.singularize
- merged_options[:root] = association_name
-
- if records.empty?
- @builder.tag!(tag, type)
- else
- @builder.tag!(tag, type) do
- records.each do |record|
- if options[:skip_types]
- record_type = {}
- else
- record_class = (record.class.to_s.underscore == association_name) ? nil : record.class.name
- record_type = { type: record_class }
- end
-
- record.to_xml merged_options.merge(record_type)
- end
- end
- end
- else
- merged_options[:root] = association.to_s
-
- unless records.class.to_s.underscore == association.to_s
- merged_options[:type] = records.class.name
- end
-
- records.to_xml merged_options
- end
- end
-
- def add_procs
- if procs = options.delete(:procs)
- Array(procs).each do |proc|
- if proc.arity == 1
- proc.call(options)
- else
- proc.call(options, @serializable)
- end
- end
- end
- end
- end
-
- # Returns XML representing the model. Configuration can be
- # passed through +options+.
- #
- # Without any +options+, the returned XML string will include all the
- # model's attributes.
- #
- # user = User.find(1)
- # user.to_xml
- #
- # <?xml version="1.0" encoding="UTF-8"?>
- # <user>
- # <id type="integer">1</id>
- # <name>David</name>
- # <age type="integer">16</age>
- # <created-at type="dateTime">2011-01-30T22:29:23Z</created-at>
- # </user>
- #
- # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the
- # attributes included, and work similar to the +attributes+ method.
- #
- # To include the result of some method calls on the model use <tt>:methods</tt>.
- #
- # To include associations use <tt>:include</tt>.
- #
- # For further documentation, see <tt>ActiveRecord::Serialization#to_xml</tt>
- def to_xml(options = {}, &block)
- Serializer.new(self, options).serialize(&block)
- end
-
- # Sets the model +attributes+ from an XML string. Returns +self+.
- #
- # class Person
- # include ActiveModel::Serializers::Xml
- #
- # attr_accessor :name, :age, :awesome
- #
- # def attributes=(hash)
- # hash.each do |key, value|
- # instance_variable_set("@#{key}", value)
- # end
- # end
- #
- # def attributes
- # instance_values
- # end
- # end
- #
- # xml = { name: 'bob', age: 22, awesome:true }.to_xml
- # person = Person.new
- # person.from_xml(xml) # => #<Person:0x007fec5e3b3c40 @age=22, @awesome=true, @name="bob">
- # person.name # => "bob"
- # person.age # => 22
- # person.awesome # => true
- def from_xml(xml)
- self.attributes = Hash.from_xml(xml).values.first
- self
- end
- end
- end
-end
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 8470915abb..f3d0d3dc27 100644
--- a/activemodel/lib/active_model/translation.rb
+++ b/activemodel/lib/active_model/translation.rb
@@ -1,5 +1,6 @@
-module ActiveModel
+# frozen_string_literal: true
+module ActiveModel
# == Active \Model \Translation
#
# Provides integration between your object and the Rails internationalization
@@ -31,7 +32,7 @@ module ActiveModel
# ActiveModel::Errors#full_messages and
# ActiveModel::Translation#human_attribute_name.
def lookup_ancestors
- self.ancestors.select { |x| x.respond_to?(:model_name) }
+ ancestors.select { |x| x.respond_to?(:model_name) }
end
# Transforms attribute names into a more human format, such as "First name"
@@ -45,7 +46,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
new file mode 100644
index 0000000000..1d7a26fff5
--- /dev/null
+++ b/activemodel/lib/active_model/type.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require "active_model/type/helpers"
+require "active_model/type/value"
+
+require "active_model/type/big_integer"
+require "active_model/type/binary"
+require "active_model/type/boolean"
+require "active_model/type/date"
+require "active_model/type/date_time"
+require "active_model/type/decimal"
+require "active_model/type/float"
+require "active_model/type/immutable_string"
+require "active_model/type/integer"
+require "active_model/type/string"
+require "active_model/type/time"
+
+require "active_model/type/registry"
+
+module ActiveModel
+ module Type
+ @registry = Registry.new
+
+ class << self
+ attr_accessor :registry # :nodoc:
+
+ # Add a new type to the registry, allowing it to be gotten through ActiveModel::Type#lookup
+ def register(type_name, klass = nil, **options, &block)
+ registry.register(type_name, klass, **options, &block)
+ end
+
+ def lookup(*args, **kwargs) # :nodoc:
+ registry.lookup(*args, **kwargs)
+ end
+
+ def default_value # :nodoc:
+ @default_value ||= Value.new
+ end
+ end
+
+ register(:big_integer, Type::BigInteger)
+ register(:binary, Type::Binary)
+ register(:boolean, Type::Boolean)
+ register(:date, Type::Date)
+ register(:datetime, Type::DateTime)
+ register(:decimal, Type::Decimal)
+ register(:float, Type::Float)
+ register(:immutable_string, Type::ImmutableString)
+ register(:integer, Type::Integer)
+ register(:string, Type::String)
+ register(:time, Type::Time)
+ end
+end
diff --git a/activemodel/lib/active_model/type/big_integer.rb b/activemodel/lib/active_model/type/big_integer.rb
new file mode 100644
index 0000000000..89e43bcc5f
--- /dev/null
+++ b/activemodel/lib/active_model/type/big_integer.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require "active_model/type/integer"
+
+module ActiveModel
+ module Type
+ class BigInteger < Integer # :nodoc:
+ private
+
+ def max_value
+ ::Float::INFINITY
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/type/binary.rb b/activemodel/lib/active_model/type/binary.rb
new file mode 100644
index 0000000000..76203c5a88
--- /dev/null
+++ b/activemodel/lib/active_model/type/binary.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module ActiveModel
+ module Type
+ class Binary < Value # :nodoc:
+ def type
+ :binary
+ end
+
+ def binary?
+ true
+ end
+
+ def cast(value)
+ if value.is_a?(Data)
+ value.to_s
+ else
+ super
+ end
+ end
+
+ def serialize(value)
+ return if value.nil?
+ Data.new(super)
+ end
+
+ def changed_in_place?(raw_old_value, value)
+ old_value = deserialize(raw_old_value)
+ old_value != value
+ end
+
+ class Data # :nodoc:
+ def initialize(value)
+ @value = value.to_s
+ end
+
+ def to_s
+ @value
+ end
+ alias_method :to_str, :to_s
+
+ def hex
+ @value.unpack1("H*")
+ end
+
+ def ==(other)
+ other == to_s || super
+ end
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/type/boolean.rb b/activemodel/lib/active_model/type/boolean.rb
new file mode 100644
index 0000000000..f6c6efbc87
--- /dev/null
+++ b/activemodel/lib/active_model/type/boolean.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module ActiveModel
+ module Type
+ # == Active \Model \Type \Boolean
+ #
+ # A class that behaves like a boolean type, including rules for coercion of user input.
+ #
+ # === Coercion
+ # Values set from user input will first be coerced into the appropriate ruby type.
+ # Coercion behavior is roughly mapped to Ruby's boolean semantics.
+ #
+ # - "false", "f" , "0", +0+ or any other value in +FALSE_VALUES+ will be coerced to +false+
+ # - Empty strings are coerced to +nil+
+ # - All other values will be coerced to +true+
+ class Boolean < Value
+ FALSE_VALUES = [false, 0, "0", "f", "F", "false", "FALSE", "off", "OFF"].to_set
+
+ def type # :nodoc:
+ :boolean
+ end
+
+ def serialize(value) # :nodoc:
+ cast(value)
+ end
+
+ private
+
+ def cast_value(value)
+ if value == ""
+ nil
+ else
+ !FALSE_VALUES.include?(value)
+ end
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/type/date.rb b/activemodel/lib/active_model/type/date.rb
new file mode 100644
index 0000000000..8ec5deedc4
--- /dev/null
+++ b/activemodel/lib/active_model/type/date.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+module ActiveModel
+ module Type
+ class Date < Value # :nodoc:
+ include Helpers::AcceptsMultiparameterTime.new
+
+ def type
+ :date
+ end
+
+ def serialize(value)
+ cast(value)
+ end
+
+ def type_cast_for_schema(value)
+ value.to_s(:db).inspect
+ end
+
+ private
+
+ def cast_value(value)
+ if value.is_a?(::String)
+ return if value.empty?
+ fast_string_to_date(value) || fallback_string_to_date(value)
+ elsif value.respond_to?(:to_date)
+ value.to_date
+ else
+ value
+ end
+ end
+
+ ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/
+ def fast_string_to_date(string)
+ if string =~ ISO_DATE
+ new_date $1.to_i, $2.to_i, $3.to_i
+ end
+ end
+
+ def fallback_string_to_date(string)
+ new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday))
+ end
+
+ def new_date(year, mon, mday)
+ unless year.nil? || (year == 0 && mon == 0 && mday == 0)
+ ::Date.new(year, mon, mday) rescue nil
+ end
+ end
+
+ def value_from_multiparameter_assignment(*)
+ time = super
+ time && time.to_date
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/type/date_time.rb b/activemodel/lib/active_model/type/date_time.rb
new file mode 100644
index 0000000000..d48598376e
--- /dev/null
+++ b/activemodel/lib/active_model/type/date_time.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module ActiveModel
+ module Type
+ class DateTime < Value # :nodoc:
+ include Helpers::TimeValue
+ include Helpers::AcceptsMultiparameterTime.new(
+ defaults: { 4 => 0, 5 => 0 }
+ )
+
+ def type
+ :datetime
+ end
+
+ def serialize(value)
+ super(cast(value))
+ end
+
+ private
+
+ def cast_value(value)
+ return apply_seconds_precision(value) unless value.is_a?(::String)
+ return if value.empty?
+
+ fast_string_to_time(value) || fallback_string_to_time(value)
+ end
+
+ # '0.123456' -> 123456
+ # '1.123456' -> 123456
+ def microseconds(time)
+ time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0
+ end
+
+ def fallback_string_to_time(string)
+ time_hash = ::Date._parse(string)
+ time_hash[:sec_fraction] = microseconds(time_hash)
+
+ new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset))
+ end
+
+ def value_from_multiparameter_assignment(values_hash)
+ missing_parameters = (1..3).select { |key| !values_hash.key?(key) }
+ if missing_parameters.any?
+ raise ArgumentError, "Provided hash #{values_hash} doesn't contain necessary keys: #{missing_parameters}"
+ end
+ super
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/type/decimal.rb b/activemodel/lib/active_model/type/decimal.rb
new file mode 100644
index 0000000000..b37dad1c41
--- /dev/null
+++ b/activemodel/lib/active_model/type/decimal.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+require "bigdecimal/util"
+
+module ActiveModel
+ module Type
+ class Decimal < Value # :nodoc:
+ include Helpers::Numeric
+ BIGDECIMAL_PRECISION = 18
+
+ def type
+ :decimal
+ end
+
+ def serialize(value)
+ cast(value)
+ end
+
+ def type_cast_for_schema(value)
+ value.to_s.inspect
+ end
+
+ private
+
+ def cast_value(value)
+ casted_value = \
+ case value
+ when ::Float
+ convert_float_to_big_decimal(value)
+ 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
+ else
+ cast_value(value.to_s)
+ end
+ end
+
+ apply_scale(casted_value)
+ end
+
+ def convert_float_to_big_decimal(value)
+ if precision
+ BigDecimal(apply_scale(value), float_precision)
+ else
+ value.to_d
+ end
+ end
+
+ def float_precision
+ if precision.to_i > ::Float::DIG + 1
+ ::Float::DIG + 1
+ else
+ precision.to_i
+ end
+ end
+
+ def apply_scale(value)
+ if scale
+ value.round(scale)
+ else
+ value
+ end
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/type/float.rb b/activemodel/lib/active_model/type/float.rb
new file mode 100644
index 0000000000..9dbe32e5a6
--- /dev/null
+++ b/activemodel/lib/active_model/type/float.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module ActiveModel
+ module Type
+ class Float < Value # :nodoc:
+ include Helpers::Numeric
+
+ def type
+ :float
+ end
+
+ def type_cast_for_schema(value)
+ return "::Float::NAN" if value.try(:nan?)
+ case value
+ when ::Float::INFINITY then "::Float::INFINITY"
+ when -::Float::INFINITY then "-::Float::INFINITY"
+ else super
+ end
+ end
+
+ alias serialize cast
+
+ private
+
+ def cast_value(value)
+ case value
+ when ::Float then value
+ when "Infinity" then ::Float::INFINITY
+ when "-Infinity" then -::Float::INFINITY
+ when "NaN" then ::Float::NAN
+ else value.to_f
+ end
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/type/helpers.rb b/activemodel/lib/active_model/type/helpers.rb
new file mode 100644
index 0000000000..403f0a9e6b
--- /dev/null
+++ b/activemodel/lib/active_model/type/helpers.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+require "active_model/type/helpers/accepts_multiparameter_time"
+require "active_model/type/helpers/numeric"
+require "active_model/type/helpers/mutable"
+require "active_model/type/helpers/time_value"
diff --git a/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb b/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb
new file mode 100644
index 0000000000..ad891f841e
--- /dev/null
+++ b/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module ActiveModel
+ module Type
+ module Helpers # :nodoc: all
+ class AcceptsMultiparameterTime < Module
+ def initialize(defaults: {})
+ define_method(:cast) do |value|
+ if value.is_a?(Hash)
+ value_from_multiparameter_assignment(value)
+ else
+ super(value)
+ end
+ end
+
+ define_method(:assert_valid_value) do |value|
+ if value.is_a?(Hash)
+ value_from_multiparameter_assignment(value)
+ else
+ super(value)
+ end
+ end
+
+ define_method(:value_constructed_by_mass_assignment?) do |value|
+ value.is_a?(Hash)
+ end
+
+ define_method(:value_from_multiparameter_assignment) do |values_hash|
+ defaults.each do |k, v|
+ values_hash[k] ||= v
+ end
+ return unless values_hash[1] && values_hash[2] && values_hash[3]
+ values = values_hash.sort.map(&:last)
+ ::Time.send(default_timezone, *values)
+ end
+ private :value_from_multiparameter_assignment
+ end
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/type/helpers/mutable.rb b/activemodel/lib/active_model/type/helpers/mutable.rb
new file mode 100644
index 0000000000..1cbea644c4
--- /dev/null
+++ b/activemodel/lib/active_model/type/helpers/mutable.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module ActiveModel
+ module Type
+ module Helpers # :nodoc: all
+ module Mutable
+ def cast(value)
+ deserialize(serialize(value))
+ end
+
+ # +raw_old_value+ will be the `_before_type_cast` version of the
+ # value (likely a string). +new_value+ will be the current, type
+ # cast value.
+ def changed_in_place?(raw_old_value, new_value)
+ raw_old_value != serialize(new_value)
+ end
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/type/helpers/numeric.rb b/activemodel/lib/active_model/type/helpers/numeric.rb
new file mode 100644
index 0000000000..473cdb0c67
--- /dev/null
+++ b/activemodel/lib/active_model/type/helpers/numeric.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module ActiveModel
+ module Type
+ module Helpers # :nodoc: all
+ module Numeric
+ def cast(value)
+ value = \
+ case value
+ when true then 1
+ when false then 0
+ when ::String then value.presence
+ else value
+ end
+ super(value)
+ end
+
+ def changed?(old_value, _new_value, new_value_before_type_cast) # :nodoc:
+ super || number_to_non_number?(old_value, new_value_before_type_cast)
+ end
+
+ private
+
+ def number_to_non_number?(old_value, new_value_before_type_cast)
+ old_value != nil && non_numeric_string?(new_value_before_type_cast)
+ end
+
+ def non_numeric_string?(value)
+ # 'wibble'.to_i will give zero, we want to make sure
+ # that we aren't marking int zero to string zero as
+ # changed.
+ !/\A[-+]?\d+/.match?(value.to_s)
+ end
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/type/helpers/time_value.rb b/activemodel/lib/active_model/type/helpers/time_value.rb
new file mode 100644
index 0000000000..da56073436
--- /dev/null
+++ b/activemodel/lib/active_model/type/helpers/time_value.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/string/zones"
+require "active_support/core_ext/time/zones"
+
+module ActiveModel
+ module Type
+ module Helpers # :nodoc: all
+ module TimeValue
+ def serialize(value)
+ value = apply_seconds_precision(value)
+
+ if value.acts_like?(:time)
+ zone_conversion_method = is_utc? ? :getutc : :getlocal
+
+ if value.respond_to?(zone_conversion_method)
+ value = value.send(zone_conversion_method)
+ end
+ end
+
+ value
+ end
+
+ def is_utc?
+ ::Time.zone_default.nil? || ::Time.zone_default =~ "UTC"
+ end
+
+ def default_timezone
+ if is_utc?
+ :utc
+ else
+ :local
+ end
+ end
+
+ def apply_seconds_precision(value)
+ 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 - value.usec % round_power)
+ end
+
+ def type_cast_for_schema(value)
+ value.to_s(:db).inspect
+ end
+
+ def user_input_in_time_zone(value)
+ value.in_time_zone
+ end
+
+ private
+
+ def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil)
+ # Treat 0000-00-00 00:00:00 as nil.
+ return if year.nil? || (year == 0 && mon == 0 && mday == 0)
+
+ if offset
+ time = ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil
+ return unless time
+
+ time -= offset
+ is_utc? ? time : time.getlocal
+ else
+ ::Time.public_send(default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
+ end
+ end
+
+ ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
+
+ # Doesn't handle time zones.
+ def fast_string_to_time(string)
+ if string =~ ISO_DATETIME
+ microsec_part = $7
+ if microsec_part && microsec_part.start_with?(".") && microsec_part.length == 7
+ microsec_part[0] = ""
+ microsec = microsec_part.to_i
+ else
+ microsec = (microsec_part.to_r * 1_000_000).to_i
+ end
+ new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/type/immutable_string.rb b/activemodel/lib/active_model/type/immutable_string.rb
new file mode 100644
index 0000000000..826bd7038f
--- /dev/null
+++ b/activemodel/lib/active_model/type/immutable_string.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module ActiveModel
+ module Type
+ class ImmutableString < Value # :nodoc:
+ def type
+ :string
+ end
+
+ def serialize(value)
+ case value
+ when ::Numeric, ActiveSupport::Duration then value.to_s
+ when true then "t"
+ when false then "f"
+ else super
+ end
+ end
+
+ private
+
+ def cast_value(value)
+ result = \
+ case value
+ when true then "t"
+ when false then "f"
+ else value.to_s
+ end
+ result.freeze
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/type/integer.rb b/activemodel/lib/active_model/type/integer.rb
new file mode 100644
index 0000000000..da74aaa3c5
--- /dev/null
+++ b/activemodel/lib/active_model/type/integer.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+module ActiveModel
+ module Type
+ class Integer < Value # :nodoc:
+ include Helpers::Numeric
+
+ # Column storage size in bytes.
+ # 4 bytes means an integer as opposed to smallint etc.
+ DEFAULT_LIMIT = 4
+
+ def initialize(*)
+ super
+ @range = min_value...max_value
+ end
+
+ def type
+ :integer
+ end
+
+ def deserialize(value)
+ return if value.nil?
+ value.to_i
+ end
+
+ def serialize(value)
+ result = cast(value)
+ if result
+ ensure_in_range(result)
+ end
+ result
+ end
+
+ private
+ attr_reader :range
+
+ def cast_value(value)
+ case value
+ when true then 1
+ when false then 0
+ else
+ value.to_i rescue nil
+ end
+ end
+
+ def ensure_in_range(value)
+ unless range.cover?(value)
+ raise ActiveModel::RangeError, "#{value} is out of range for #{self.class} with limit #{_limit} bytes"
+ end
+ end
+
+ def max_value
+ 1 << (_limit * 8 - 1) # 8 bits per byte with one bit for sign
+ end
+
+ def min_value
+ -max_value
+ end
+
+ def _limit
+ limit || DEFAULT_LIMIT
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/type/registry.rb b/activemodel/lib/active_model/type/registry.rb
new file mode 100644
index 0000000000..a19dc0f011
--- /dev/null
+++ b/activemodel/lib/active_model/type/registry.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+module ActiveModel
+ # :stopdoc:
+ module Type
+ class Registry
+ def initialize
+ @registrations = []
+ end
+
+ def register(type_name, klass = nil, **options, &block)
+ block ||= proc { |_, *args| klass.new(*args) }
+ registrations << registration_klass.new(type_name, block, **options)
+ end
+
+ def lookup(symbol, *args)
+ registration = find_registration(symbol, *args)
+
+ if registration
+ registration.call(self, symbol, *args)
+ else
+ raise ArgumentError, "Unknown type #{symbol.inspect}"
+ end
+ end
+
+ private
+ attr_reader :registrations
+
+ def registration_klass
+ Registration
+ end
+
+ def find_registration(symbol, *args)
+ registrations.find { |r| r.matches?(symbol, *args) }
+ end
+ end
+
+ class Registration
+ # Options must be taken because of https://bugs.ruby-lang.org/issues/10856
+ def initialize(name, block, **)
+ @name = name
+ @block = block
+ end
+
+ def call(_registry, *args, **kwargs)
+ if kwargs.any? # https://bugs.ruby-lang.org/issues/10856
+ block.call(*args, **kwargs)
+ else
+ block.call(*args)
+ end
+ end
+
+ def matches?(type_name, *args, **kwargs)
+ type_name == name
+ end
+
+ private
+ attr_reader :name, :block
+ end
+ end
+ # :startdoc:
+end
diff --git a/activemodel/lib/active_model/type/string.rb b/activemodel/lib/active_model/type/string.rb
new file mode 100644
index 0000000000..a9c9bfadb6
--- /dev/null
+++ b/activemodel/lib/active_model/type/string.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require "active_model/type/immutable_string"
+
+module ActiveModel
+ module Type
+ class String < ImmutableString # :nodoc:
+ def changed_in_place?(raw_old_value, new_value)
+ if new_value.is_a?(::String)
+ raw_old_value != new_value
+ end
+ end
+
+ private
+
+ def cast_value(value)
+ case value
+ when ::String then ::String.new(value)
+ when true then "t"
+ when false then "f"
+ else value.to_s
+ end
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/type/time.rb b/activemodel/lib/active_model/type/time.rb
new file mode 100644
index 0000000000..b3056b1333
--- /dev/null
+++ b/activemodel/lib/active_model/type/time.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module ActiveModel
+ module Type
+ class Time < Value # :nodoc:
+ include Helpers::TimeValue
+ include Helpers::AcceptsMultiparameterTime.new(
+ defaults: { 1 => 1970, 2 => 1, 3 => 1, 4 => 0, 5 => 0 }
+ )
+
+ def type
+ :time
+ end
+
+ def user_input_in_time_zone(value)
+ return unless value.present?
+
+ case value
+ when ::String
+ value = "2000-01-01 #{value}"
+ time_hash = ::Date._parse(value)
+ return if time_hash[:hour].nil?
+ when ::Time
+ value = value.change(year: 2000, day: 1, month: 1)
+ end
+
+ super(value)
+ end
+
+ private
+
+ def cast_value(value)
+ return apply_seconds_precision(value) unless value.is_a?(::String)
+ return if value.empty?
+
+ dummy_time_value = value.sub(/\A(\d\d\d\d-\d\d-\d\d |)/, "2000-01-01 ")
+
+ fast_string_to_time(dummy_time_value) || begin
+ time_hash = ::Date._parse(dummy_time_value)
+ return if time_hash[:hour].nil?
+ new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset))
+ end
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/type/value.rb b/activemodel/lib/active_model/type/value.rb
new file mode 100644
index 0000000000..b6914dd63c
--- /dev/null
+++ b/activemodel/lib/active_model/type/value.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+
+module ActiveModel
+ module Type
+ class Value
+ attr_reader :precision, :scale, :limit
+
+ def initialize(precision: nil, limit: nil, scale: nil)
+ @precision = precision
+ @scale = scale
+ @limit = limit
+ end
+
+ def type # :nodoc:
+ end
+
+ # Converts a value from database input to the appropriate ruby type. The
+ # return value of this method will be returned from
+ # ActiveRecord::AttributeMethods::Read#read_attribute. The default
+ # implementation just calls Value#cast.
+ #
+ # +value+ The raw input, as provided from the database.
+ def deserialize(value)
+ cast(value)
+ end
+
+ # Type casts a value from user input (e.g. from a setter). This value may
+ # be a string from the form builder, or a ruby object passed to a setter.
+ # There is currently no way to differentiate between which source it came
+ # from.
+ #
+ # The return value of this method will be returned from
+ # ActiveRecord::AttributeMethods::Read#read_attribute. See also:
+ # Value#cast_value.
+ #
+ # +value+ The raw input, as provided to the attribute setter.
+ def cast(value)
+ cast_value(value) unless value.nil?
+ end
+
+ # Casts a value from the ruby type to a type that the database knows how
+ # to understand. The returned value from this method should be a
+ # +String+, +Numeric+, +Date+, +Time+, +Symbol+, +true+, +false+, or
+ # +nil+.
+ def serialize(value)
+ value
+ end
+
+ # Type casts a value for schema dumping. This method is private, as we are
+ # hoping to remove it entirely.
+ def type_cast_for_schema(value) # :nodoc:
+ value.inspect
+ end
+
+ # These predicates are not documented, as I need to look further into
+ # their use, and see if they can be removed entirely.
+ def binary? # :nodoc:
+ false
+ end
+
+ # Determines whether a value has changed for dirty checking. +old_value+
+ # and +new_value+ will always be type-cast. Types should not need to
+ # override this method.
+ def changed?(old_value, new_value, _new_value_before_type_cast)
+ old_value != new_value
+ end
+
+ # Determines whether the mutable value has been modified since it was
+ # read. Returns +false+ by default. If your type returns an object
+ # which could be mutated, you should override this method. You will need
+ # to either:
+ #
+ # - pass +new_value+ to Value#serialize and compare it to
+ # +raw_old_value+
+ #
+ # or
+ #
+ # - pass +raw_old_value+ to Value#deserialize and compare it to
+ # +new_value+
+ #
+ # +raw_old_value+ The original value, before being passed to
+ # +deserialize+.
+ #
+ # +new_value+ The current value, after type casting.
+ def changed_in_place?(raw_old_value, new_value)
+ false
+ end
+
+ def value_constructed_by_mass_assignment?(_value) # :nodoc:
+ false
+ end
+
+ def force_equality?(_value) # :nodoc:
+ false
+ end
+
+ def map(value) # :nodoc:
+ yield value
+ end
+
+ def ==(other)
+ self.class == other.class &&
+ precision == other.precision &&
+ scale == other.scale &&
+ limit == other.limit
+ end
+ alias eql? ==
+
+ def hash
+ [self.class, precision, scale, limit].hash
+ end
+
+ def assert_valid_value(*)
+ end
+
+ private
+
+ # Convenience method for types which do not need separate type casting
+ # behavior for user and database inputs. Called by Value#cast for
+ # values except +nil+.
+ def cast_value(value) # :doc:
+ value
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb
index 72094a26c0..7f14d102dd 100644
--- a/activemodel/lib/active_model/validations.rb
+++ b/activemodel/lib/active_model/validations.rb
@@ -1,9 +1,10 @@
-require 'active_support/core_ext/array/extract_options'
-require 'active_support/core_ext/hash/keys'
-require 'active_support/core_ext/hash/except'
+# frozen_string_literal: true
-module ActiveModel
+require "active_support/core_ext/array/extract_options"
+require "active_support/core_ext/hash/keys"
+require "active_support/core_ext/hash/except"
+module ActiveModel
# == Active \Model \Validations
#
# Provides a full validation framework to your objects.
@@ -47,10 +48,10 @@ module ActiveModel
include HelperMethods
attr_accessor :validation_context
+ private :validation_context=
define_callbacks :validate, scope: :name
- class_attribute :_validators
- self._validators = Hash.new { |h,k| h[k] = [] }
+ class_attribute :_validators, instance_writer: false, default: Hash.new { |h, k| h[k] = [] }
end
module ClassMethods
@@ -68,7 +69,7 @@ module ActiveModel
#
# Options:
# * <tt>:on</tt> - Specifies the contexts where this validation is active.
- # Runs in all validation contexts by default (nil). You can pass a symbol
+ # Runs in all validation contexts by default +nil+. You can pass a symbol
# or an array of symbols. (e.g. <tt>on: :create</tt> or
# <tt>on: :custom_validation_context</tt> or
# <tt>on: [:create, :custom_validation_context]</tt>)
@@ -134,7 +135,7 @@ module ActiveModel
#
# Options:
# * <tt>:on</tt> - Specifies the contexts where this validation is active.
- # Runs in all validation contexts by default (nil). You can pass a symbol
+ # Runs in all validation contexts by default +nil+. You can pass a symbol
# or an array of symbols. (e.g. <tt>on: :create</tt> or
# <tt>on: :custom_validation_context</tt> or
# <tt>on: [:create, :custom_validation_context]</tt>)
@@ -147,6 +148,9 @@ module ActiveModel
# 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.
+ #
+ # NOTE: Calling +validate+ multiple times on the same method will overwrite previous definitions.
+ #
def validate(*args, &block)
options = args.extract_options!
@@ -160,14 +164,14 @@ module ActiveModel
if options.key?(:on)
options = options.dup
+ options[:on] = Array(options[:on])
options[:if] = Array(options[:if])
options[:if].unshift ->(o) {
- Array(options[:on]).include?(o.validation_context)
+ !(options[:on] & Array(o.validation_context)).empty?
}
end
- args << options
- set_callback(:validate, *args, &block)
+ set_callback(:validate, *args, options, &block)
end
# List all validators that are being used to validate the model using
@@ -303,8 +307,6 @@ module ActiveModel
# Runs all the specified validations and returns +true+ if no errors were
# added otherwise +false+.
#
- # Aliased as validate.
- #
# class Person
# include ActiveModel::Validations
#
@@ -401,14 +403,14 @@ module ActiveModel
# end
alias :read_attribute_for_validation :send
- protected
+ private
- def run_validations! #:nodoc:
- run_callbacks :validate
+ def run_validations!
+ _run_validate_callbacks
errors.empty?
end
- def raise_validation_error
+ def raise_validation_error # :doc:
raise(ValidationError.new(self))
end
end
@@ -434,4 +436,4 @@ module ActiveModel
end
end
-Dir[File.dirname(__FILE__) + "/validations/*.rb"].each { |file| require file }
+Dir[File.expand_path("validations/*.rb", __dir__)].each { |file| require file }
diff --git a/activemodel/lib/active_model/validations/absence.rb b/activemodel/lib/active_model/validations/absence.rb
index 75bf655578..385d9f27e0 100644
--- a/activemodel/lib/active_model/validations/absence.rb
+++ b/activemodel/lib/active_model/validations/absence.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveModel
module Validations
# == \Active \Model Absence Validator
@@ -22,7 +24,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 1bcfedb35d..ea3a6b52ab 100644
--- a/activemodel/lib/active_model/validations/acceptance.rb
+++ b/activemodel/lib/active_model/validations/acceptance.rb
@@ -1,5 +1,6 @@
-module ActiveModel
+# frozen_string_literal: true
+module ActiveModel
module Validations
class AcceptanceValidator < EachValidator # :nodoc:
def initialize(options)
@@ -14,16 +15,56 @@ module ActiveModel
end
private
- def setup!(klass)
- attr_readers = attributes.reject { |name| klass.attribute_method?(name) }
- attr_writers = attributes.reject { |name| klass.attribute_method?("#{name}=") }
- klass.send(:attr_reader, *attr_readers)
- klass.send(:attr_writer, *attr_writers)
- end
- def acceptable_option?(value)
- Array(options[:accept]).include?(value)
- end
+ def setup!(klass)
+ klass.include(LazilyDefineAttributes.new(AttributeDefinition.new(attributes)))
+ end
+
+ def acceptable_option?(value)
+ Array(options[:accept]).include?(value)
+ end
+
+ class LazilyDefineAttributes < Module
+ def initialize(attribute_definition)
+ define_method(:respond_to_missing?) do |method_name, include_private = false|
+ super(method_name, include_private) || attribute_definition.matches?(method_name)
+ end
+
+ define_method(:method_missing) do |method_name, *args, &block|
+ if attribute_definition.matches?(method_name)
+ attribute_definition.define_on(self.class)
+ send(method_name, *args, &block)
+ else
+ super(method_name, *args, &block)
+ end
+ end
+ end
+ end
+
+ class AttributeDefinition
+ def initialize(attributes)
+ @attributes = attributes.map(&:to_s)
+ end
+
+ def matches?(method_name)
+ attr_name = convert_to_reader_name(method_name)
+ attributes.include?(attr_name)
+ end
+
+ def define_on(klass)
+ attr_readers = attributes.reject { |name| klass.attribute_method?(name) }
+ attr_writers = attributes.reject { |name| klass.attribute_method?("#{name}=") }
+ klass.send(:attr_reader, *attr_readers)
+ klass.send(:attr_writer, *attr_writers)
+ end
+
+ private
+ attr_reader :attributes
+
+ def convert_to_reader_name(method_name)
+ method_name.to_s.chomp("=")
+ end
+ end
end
module HelperMethods
@@ -51,7 +92,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 b4301c23e4..887d31ae2a 100644
--- a/activemodel/lib/active_model/validations/callbacks.rb
+++ b/activemodel/lib/active_model/validations/callbacks.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveModel
module Validations
# == Active \Model \Validation \Callbacks
@@ -28,8 +30,7 @@ module ActiveModel
end
module ClassMethods
- # Defines a callback that will get called right before validation
- # happens.
+ # Defines a callback that will get called right before validation.
#
# class Person
# include ActiveModel::Validations
@@ -53,19 +54,21 @@ module ActiveModel
# person.valid? # => true
# person.name # => "bob"
def before_validation(*args, &block)
- options = args.last
- if options.is_a?(Hash) && options[:on]
- options[:if] = Array(options[:if])
+ options = args.extract_options!
+
+ if options.key?(:on)
+ options = options.dup
options[:on] = Array(options[:on])
+ options[:if] = Array(options[:if])
options[:if].unshift ->(o) {
- options[:on].include? o.validation_context
+ !(options[:on] & Array(o.validation_context)).empty?
}
end
- set_callback(:validation, :before, *args, &block)
+
+ set_callback(:validation, :before, *args, options, &block)
end
- # Defines a callback that will get called right after validation
- # happens.
+ # Defines a callback that will get called right after validation.
#
# class Person
# include ActiveModel::Validations
@@ -93,23 +96,26 @@ module ActiveModel
# person.status # => true
def after_validation(*args, &block)
options = args.extract_options!
+ options = options.dup
options[:prepend] = true
- options[:if] = Array(options[:if])
- if options[:on]
+
+ if options.key?(:on)
options[:on] = Array(options[:on])
+ options[:if] = Array(options[:if])
options[:if].unshift ->(o) {
- options[:on].include? o.validation_context
+ !(options[:on] & Array(o.validation_context)).empty?
}
end
- set_callback(:validation, :after, *(args << options), &block)
+
+ set_callback(:validation, :after, *args, options, &block)
end
end
- protected
+ private
# Overwrite run validations to include callbacks.
- def run_validations! #:nodoc:
- run_callbacks(:validation) { super }
+ def run_validations!
+ _run_validation_callbacks { super }
end
end
end
diff --git a/activemodel/lib/active_model/validations/clusivity.rb b/activemodel/lib/active_model/validations/clusivity.rb
index bad9e4f9a9..bafb8e2106 100644
--- a/activemodel/lib/active_model/validations/clusivity.rb
+++ b/activemodel/lib/active_model/validations/clusivity.rb
@@ -1,4 +1,6 @@
-require 'active_support/core_ext/range'
+# frozen_string_literal: true
+
+require "active_support/core_ext/range"
module ActiveModel
module Validations
@@ -16,12 +18,12 @@ module ActiveModel
def include?(record, value)
members = if delimiter.respond_to?(:call)
- delimiter.call(record)
- elsif delimiter.respond_to?(:to_sym)
- record.send(delimiter)
- else
- delimiter
- end
+ delimiter.call(record)
+ elsif delimiter.respond_to?(:to_sym)
+ record.send(delimiter)
+ else
+ delimiter
+ end
members.send(inclusion_method(members), value)
end
@@ -30,14 +32,15 @@ module ActiveModel
@delimiter ||= options[:in] || options[:within]
end
- # In Ruby 1.9 <tt>Range#include?</tt> on non-number-or-time-ish ranges checks all
+ # After Ruby 2.2, <tt>Range#include?</tt> on non-number-or-time-ish ranges checks all
# possible values in the range for equality, which is slower but more accurate.
# <tt>Range#cover?</tt> uses the previous logic of comparing a value with the range
- # endpoints, which is fast but is only accurate on Numeric, Time, or DateTime ranges.
+ # endpoints, which is fast but is only accurate on Numeric, Time, Date,
+ # or DateTime ranges.
def inclusion_method(enumerable)
if enumerable.is_a? Range
case enumerable.first
- when Numeric, Time, DateTime
+ when Numeric, Time, DateTime, Date
:cover?
else
:include?
diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb
index 1b11c28087..1b5d5b09ab 100644
--- a/activemodel/lib/active_model/validations/confirmation.rb
+++ b/activemodel/lib/active_model/validations/confirmation.rb
@@ -1,29 +1,40 @@
-module ActiveModel
+# frozen_string_literal: true
+module ActiveModel
module Validations
class ConfirmationValidator < EachValidator # :nodoc:
def initialize(options)
- super
+ super({ case_sensitive: true }.merge!(options))
setup!(options[:class])
end
def validate_each(record, attribute, value)
- if (confirmed = record.send("#{attribute}_confirmation")) && (value != confirmed)
- human_attribute_name = record.class.human_attribute_name(attribute)
- record.errors.add(:"#{attribute}_confirmation", :confirmation, options.merge(attribute: human_attribute_name))
+ unless (confirmed = record.send("#{attribute}_confirmation")).nil?
+ unless confirmation_value_equal?(record, attribute, value, confirmed)
+ human_attribute_name = record.class.human_attribute_name(attribute)
+ record.errors.add(:"#{attribute}_confirmation", :confirmation, options.except(:case_sensitive).merge!(attribute: human_attribute_name))
+ end
end
end
private
- def setup!(klass)
- klass.send(:attr_reader, *attributes.map do |attribute|
- :"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation")
- end.compact)
+ def setup!(klass)
+ klass.send(:attr_reader, *attributes.map do |attribute|
+ :"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation")
+ end.compact)
- klass.send(:attr_writer, *attributes.map do |attribute|
- :"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation=")
- end.compact)
- end
+ klass.send(:attr_writer, *attributes.map do |attribute|
+ :"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation=")
+ end.compact)
+ end
+
+ def confirmation_value_equal?(record, attribute, value, confirmed)
+ if !options[:case_sensitive] && value.is_a?(String)
+ value.casecmp(confirmed) == 0
+ else
+ value == confirmed
+ end
+ end
end
module HelperMethods
@@ -55,10 +66,12 @@ module ActiveModel
# Configuration options:
# * <tt>:message</tt> - A custom error message (default is: "doesn't match
# <tt>%{translated_attribute_name}</tt>").
+ # * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by
+ # non-text columns (+true+ by default).
#
# 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 6f4276cc2a..3be7ab6ba8 100644
--- a/activemodel/lib/active_model/validations/exclusion.rb
+++ b/activemodel/lib/active_model/validations/exclusion.rb
@@ -1,7 +1,8 @@
+# frozen_string_literal: true
+
require "active_model/validations/clusivity"
module ActiveModel
-
module Validations
class ExclusionValidator < EachValidator # :nodoc:
include Clusivity
@@ -39,7 +40,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 46a2e54fba..7c3f091473 100644
--- a/activemodel/lib/active_model/validations/format.rb
+++ b/activemodel/lib/active_model/validations/format.rb
@@ -1,5 +1,6 @@
-module ActiveModel
+# frozen_string_literal: true
+module ActiveModel
module Validations
class FormatValidator < EachValidator # :nodoc:
def validate_each(record, attribute, value)
@@ -8,7 +9,7 @@ module ActiveModel
record_error(record, attribute, :with, value) if value.to_s !~ regexp
elsif options[:without]
regexp = option_call(record, :without)
- record_error(record, attribute, :without, value) if value.to_s =~ regexp
+ record_error(record, attribute, :without, value) if regexp.match?(value.to_s)
end
end
@@ -23,33 +24,33 @@ module ActiveModel
private
- def option_call(record, name)
- option = options[name]
- option.respond_to?(:call) ? option.call(record) : option
- end
+ def option_call(record, name)
+ option = options[name]
+ option.respond_to?(:call) ? option.call(record) : option
+ end
- def record_error(record, attribute, name, value)
- record.errors.add(attribute, :invalid, options.except(name).merge!(value: value))
- end
+ def record_error(record, attribute, name, value)
+ record.errors.add(attribute, :invalid, options.except(name).merge!(value: value))
+ end
- def check_options_validity(name)
- if option = options[name]
- if option.is_a?(Regexp)
- if options[:multiline] != true && regexp_using_multiline_anchors?(option)
- raise ArgumentError, "The provided regular expression is using multiline anchors (^ or $), " \
- "which may present a security risk. Did you mean to use \\A and \\z, or forgot to add the " \
- ":multiline => true option?"
+ def check_options_validity(name)
+ if option = options[name]
+ if option.is_a?(Regexp)
+ if options[:multiline] != true && regexp_using_multiline_anchors?(option)
+ raise ArgumentError, "The provided regular expression is using multiline anchors (^ or $), " \
+ "which may present a security risk. Did you mean to use \\A and \\z, or forgot to add the " \
+ ":multiline => true option?"
+ end
+ elsif !option.respond_to?(:call)
+ raise ArgumentError, "A regular expression or a proc or lambda must be supplied as :#{name}"
end
- elsif !option.respond_to?(:call)
- raise ArgumentError, "A regular expression or a proc or lambda must be supplied as :#{name}"
end
end
- end
- def regexp_using_multiline_anchors?(regexp)
- source = regexp.source
- source.start_with?("^") || (source.end_with?("$") && !source.end_with?("\\$"))
- end
+ def regexp_using_multiline_anchors?(regexp)
+ source = regexp.source
+ source.start_with?("^") || (source.end_with?("$") && !source.end_with?("\\$"))
+ end
end
module HelperMethods
@@ -104,7 +105,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/helper_methods.rb b/activemodel/lib/active_model/validations/helper_methods.rb
index 2176115334..730173f2f9 100644
--- a/activemodel/lib/active_model/validations/helper_methods.rb
+++ b/activemodel/lib/active_model/validations/helper_methods.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveModel
module Validations
module HelperMethods # :nodoc:
diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb
index 03e0ef56d8..9c12dc14c5 100644
--- a/activemodel/lib/active_model/validations/inclusion.rb
+++ b/activemodel/lib/active_model/validations/inclusion.rb
@@ -1,7 +1,8 @@
+# frozen_string_literal: true
+
require "active_model/validations/clusivity"
module ActiveModel
-
module Validations
class InclusionValidator < EachValidator # :nodoc:
include Clusivity
@@ -18,7 +19,7 @@ module ActiveModel
# particular enumerable object.
#
# class Person < ActiveRecord::Base
- # validates_inclusion_of :gender, in: %w( m f )
+ # validates_inclusion_of :role, in: %w( admin contributor )
# validates_inclusion_of :age, in: 0..99
# validates_inclusion_of :format, in: %w( jpg gif png ), message: "extension %{value} is not included in the list"
# validates_inclusion_of :states, in: ->(person) { STATES[person.country] }
@@ -37,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_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 910cca2f49..d6c80b2c5d 100644
--- a/activemodel/lib/active_model/validations/length.rb
+++ b/activemodel/lib/active_model/validations/length.rb
@@ -1,4 +1,4 @@
-require "active_support/core_ext/string/strip"
+# frozen_string_literal: true
module ActiveModel
module Validations
@@ -6,7 +6,7 @@ module ActiveModel
MESSAGES = { is: :wrong_length, minimum: :too_short, maximum: :too_long }.freeze
CHECKS = { is: :==, minimum: :>=, maximum: :<= }.freeze
- RESERVED_OPTIONS = [:minimum, :maximum, :within, :is, :tokenizer, :too_short, :too_long]
+ RESERVED_OPTIONS = [:minimum, :maximum, :within, :is, :too_short, :too_long]
def initialize(options)
if range = (options.delete(:in) || options.delete(:within))
@@ -18,27 +18,6 @@ module ActiveModel
options[:minimum] = 1
end
- if options[:tokenizer]
- ActiveSupport::Deprecation.warn(<<-EOS.strip_heredoc)
- The `:tokenizer` option is deprecated, and will be removed in Rails 5.1.
- You can achieve the same functionality by defining an instance method
- with the value that you want to validate the length of. For example,
-
- validates_length_of :essay, minimum: 100,
- tokenizer: ->(str) { str.scan(/\w+/) }
-
- should be written as
-
- validates_length_of :words_in_essay, minimum: 100
-
- private
-
- def words_in_essay
- essay.scan(/\w+/)
- end
- EOS
- end
-
super
end
@@ -46,20 +25,19 @@ module ActiveModel
keys = CHECKS.keys & options.keys
if keys.empty?
- raise ArgumentError, 'Range unspecified. Specify the :in, :within, :maximum, :minimum, or :is option.'
+ raise ArgumentError, "Range unspecified. Specify the :in, :within, :maximum, :minimum, or :is option."
end
keys.each do |key|
value = options[key]
- unless (value.is_a?(Integer) && value >= 0) || value == Float::INFINITY
- raise ArgumentError, ":#{key} must be a nonnegative Integer or Infinity"
+ unless (value.is_a?(Integer) && value >= 0) || value == Float::INFINITY || value.is_a?(Symbol) || value.is_a?(Proc)
+ raise ArgumentError, ":#{key} must be a nonnegative Integer, Infinity, Symbol, or Proc"
end
end
end
def validate_each(record, attribute, value)
- value = tokenize(record, value)
value_length = value.respond_to?(:length) ? value.length : value.to_s.length
errors_options = options.except(*RESERVED_OPTIONS)
@@ -67,6 +45,12 @@ module ActiveModel
next unless check_value = options[key]
if !value.nil? || skip_nil_check?(key)
+ case check_value
+ when Proc
+ check_value = check_value.call(record)
+ when Symbol
+ check_value = record.send(check_value)
+ end
next if value_length.send(validity_check, check_value)
end
@@ -80,24 +64,12 @@ module ActiveModel
end
private
- def tokenize(record, value)
- tokenizer = options[:tokenizer]
- if tokenizer && value.kind_of?(String)
- if tokenizer.kind_of?(Proc)
- tokenizer.call(value)
- elsif record.respond_to?(tokenizer)
- record.send(tokenizer, value)
- end
- end || value
- end
-
- def skip_nil_check?(key)
- key == :maximum && options[:allow_nil].nil? && options[:allow_blank].nil?
- end
+ def skip_nil_check?(key)
+ key == :maximum && options[:allow_nil].nil? && options[:allow_blank].nil?
+ end
end
module HelperMethods
-
# Validates that the specified attributes match the length restrictions
# supplied. Only one constraint option can be used at a time apart from
# +:minimum+ and +:maximum+ that can be combined together:
@@ -136,7 +108,7 @@ module ActiveModel
# * <tt>:too_long</tt> - The error message if the attribute goes over the
# maximum (default is: "is too long (maximum is %{count} characters)").
# * <tt>:too_short</tt> - The error message if the attribute goes under the
- # minimum (default is: "is too short (min is %{count} characters)").
+ # minimum (default is: "is too short (minimum is %{count} characters)").
# * <tt>:wrong_length</tt> - The error message if using the <tt>:is</tt>
# method and the attribute is the wrong size (default is: "is the wrong
# length (should be %{count} characters)").
@@ -146,7 +118,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 4ba4e3e8f7..126a87ac6e 100644
--- a/activemodel/lib/active_model/validations/numericality.rb
+++ b/activemodel/lib/active_model/validations/numericality.rb
@@ -1,5 +1,6 @@
-module ActiveModel
+# frozen_string_literal: true
+module ActiveModel
module Validations
class NumericalityValidator < EachValidator # :nodoc:
CHECKS = { greater_than: :>, greater_than_or_equal_to: :>=,
@@ -18,27 +19,40 @@ module ActiveModel
end
def validate_each(record, attr_name, value)
- before_type_cast = :"#{attr_name}_before_type_cast"
+ came_from_user = :"#{attr_name}_came_from_user?"
- raw_value = record.send(before_type_cast) if record.respond_to?(before_type_cast)
+ if record.respond_to?(came_from_user)
+ if record.public_send(came_from_user)
+ raw_value = record.read_attribute_before_type_cast(attr_name)
+ elsif record.respond_to?(:read_attribute)
+ raw_value = record.read_attribute(attr_name)
+ end
+ else
+ before_type_cast = :"#{attr_name}_before_type_cast"
+ if record.respond_to?(before_type_cast)
+ raw_value = record.public_send(before_type_cast)
+ end
+ end
raw_value ||= value
if record_attribute_changed_in_place?(record, attr_name)
raw_value = value
end
- return if options[:allow_nil] && raw_value.nil?
-
- unless value = parse_raw_value_as_a_number(raw_value)
+ unless is_number?(raw_value)
record.errors.add(attr_name, :not_a_number, filtered_options(raw_value))
return
end
- if allow_only_integer?(record)
- unless value = parse_raw_value_as_an_integer(raw_value)
- record.errors.add(attr_name, :not_an_integer, filtered_options(raw_value))
- return
- end
+ if allow_only_integer?(record) && !is_integer?(raw_value)
+ record.errors.add(attr_name, :not_an_integer, filtered_options(raw_value))
+ return
+ end
+
+ if raw_value.is_a?(Numeric)
+ value = raw_value
+ else
+ value = parse_raw_value_as_a_number(raw_value)
end
options.slice(*CHECKS.keys).each do |option, option_value|
@@ -62,16 +76,21 @@ module ActiveModel
end
end
- protected
+ private
+
+ def is_number?(raw_value)
+ !parse_raw_value_as_a_number(raw_value).nil?
+ rescue ArgumentError, TypeError
+ false
+ 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]/
- rescue ArgumentError, TypeError
- nil
end
- def parse_raw_value_as_an_integer(raw_value)
- raw_value.to_i if raw_value.to_s =~ /\A[+-]?\d+\z/
+ def is_integer?(raw_value)
+ /\A[+-]?\d+\z/ === raw_value.to_s
end
def filtered_options(value)
@@ -91,8 +110,6 @@ 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)
@@ -102,7 +119,7 @@ module ActiveModel
module HelperMethods
# Validates whether the value of the specified attribute is numeric by
# trying to convert it to a float with Kernel.Float (if <tt>only_integer</tt>
- # is +false+) or applying it to the regular expression <tt>/\A[\+\-]?\d+\Z/</tt>
+ # is +false+) or applying it to the regular expression <tt>/\A[\+\-]?\d+\z/</tt>
# (if <tt>only_integer</tt> is set to +true+).
#
# class Person < ActiveRecord::Base
@@ -114,7 +131,7 @@ module ActiveModel
# * <tt>:only_integer</tt> - Specifies whether the value has to be an
# integer, e.g. an integral value (default is +false+).
# * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+ (default is
- # +false+). Notice that for fixnum and float columns empty strings are
+ # +false+). Notice that for Integer and Float columns empty strings are
# converted to +nil+.
# * <tt>:greater_than</tt> - Specifies the value must be greater than the
# supplied value.
@@ -133,7 +150,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 5d593274eb..8787a75afa 100644
--- a/activemodel/lib/active_model/validations/presence.rb
+++ b/activemodel/lib/active_model/validations/presence.rb
@@ -1,6 +1,6 @@
+# frozen_string_literal: true
module ActiveModel
-
module Validations
class PresenceValidator < EachValidator # :nodoc:
def validate_each(record, attr_name, value)
@@ -30,7 +30,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 bda436d8d0..21c4ce0dfe 100644
--- a/activemodel/lib/active_model/validations/validates.rb
+++ b/activemodel/lib/active_model/validations/validates.rb
@@ -1,4 +1,6 @@
-require 'active_support/core_ext/hash/slice'
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/slice"
module ActiveModel
module Validations
@@ -18,7 +20,6 @@ module ActiveModel
# validates :first_name, length: { maximum: 30 }
# validates :age, numericality: true
# validates :username, presence: true
- # validates :username, uniqueness: true
#
# The power of the +validates+ method comes when using custom validators
# and default validators in one call for a given attribute.
@@ -34,7 +35,7 @@ module ActiveModel
# include ActiveModel::Validations
# attr_accessor :name, :email
#
- # validates :name, presence: true, uniqueness: true, length: { maximum: 100 }
+ # validates :name, presence: true, length: { maximum: 100 }
# validates :email, presence: true, email: true
# end
#
@@ -62,7 +63,7 @@ module ActiveModel
# and strings in shortcut form.
#
# validates :email, format: /@/
- # validates :gender, inclusion: %w(male female)
+ # validates :role, inclusion: %(admin contributor)
# validates :password, length: 6..20
#
# When using shortcut form, ranges and arrays are passed to your
@@ -72,7 +73,7 @@ module ActiveModel
# There is also a list of options that could be used along with validators:
#
# * <tt>:on</tt> - Specifies the contexts where this validation is active.
- # Runs in all validation contexts by default (nil). You can pass a symbol
+ # Runs in all validation contexts by default +nil+. You can pass a symbol
# or an array of symbols. (e.g. <tt>on: :create</tt> or
# <tt>on: :custom_validation_context</tt> or
# <tt>on: [:create, :custom_validation_context]</tt>)
@@ -94,7 +95,7 @@ module ActiveModel
# Example:
#
# validates :password, presence: true, confirmation: true, if: :password_required?
- # validates :token, uniqueness: true, strict: TokenGenerationException
+ # validates :token, length: 24, strict: TokenLengthException
#
#
# Finally, the options +:if+, +:unless+, +:on+, +:allow_blank+, +:allow_nil+, +:strict+
@@ -115,7 +116,7 @@ module ActiveModel
key = "#{key.to_s.camelize}Validator"
begin
- validator = key.include?('::') ? key.constantize : const_get(key)
+ validator = key.include?("::") ? key.constantize : const_get(key)
rescue NameError
raise ArgumentError, "Unknown validator: '#{key}'"
end
@@ -148,15 +149,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:
- [:if, :unless, :on, :allow_blank, :allow_nil , :strict]
+ 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 6de01b3392..d777ac836e 100644
--- a/activemodel/lib/active_model/validations/with.rb
+++ b/activemodel/lib/active_model/validations/with.rb
@@ -1,3 +1,7 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/array/extract_options"
+
module ActiveModel
module Validations
class WithValidator < EachValidator # :nodoc:
@@ -43,7 +47,7 @@ module ActiveModel
#
# Configuration options:
# * <tt>:on</tt> - Specifies the contexts where this validation is active.
- # Runs in all validation contexts by default (nil). You can pass a symbol
+ # Runs in all validation contexts by default +nil+. You can pass a symbol
# or an array of symbols. (e.g. <tt>on: :create</tt> or
# <tt>on: :custom_validation_context</tt> or
# <tt>on: [:create, :custom_validation_context]</tt>)
@@ -59,7 +63,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 1d2888a818..e17c3ca7b3 100644
--- a/activemodel/lib/active_model/validator.rb
+++ b/activemodel/lib/active_model/validator.rb
@@ -1,7 +1,8 @@
+# frozen_string_literal: true
+
require "active_support/core_ext/module/anonymous"
module ActiveModel
-
# == Active \Model \Validator
#
# A simple base class that can be used along with
@@ -83,7 +84,7 @@ module ActiveModel
# end
#
# It can be useful to access the class that is using that validator when there are prerequisites such
- # as an +attr_accessor+ being present. This class is accessible via +options[:class]+ in the constructor.
+ # as an +attr_accessor+ being present. This class is accessible via <tt>options[:class]</tt> in the constructor.
# To setup your validator override the constructor.
#
# class MyValidator < ActiveModel::Validator
@@ -98,20 +99,20 @@ module ActiveModel
# Returns the kind of the validator.
#
# PresenceValidator.kind # => :presence
- # UniquenessValidator.kind # => :uniqueness
+ # AcceptanceValidator.kind # => :acceptance
def self.kind
- @kind ||= name.split('::').last.underscore.sub(/_validator$/, '').to_sym unless anonymous?
+ @kind ||= name.split("::").last.underscore.chomp("_validator").to_sym unless anonymous?
end
# Accepts options that will be made available through the +options+ reader.
def initialize(options = {})
- @options = options.except(:class).freeze
+ @options = options.except(:class).freeze
end
# Returns the kind for this validator.
#
- # PresenceValidator.new.kind # => :presence
- # UniquenessValidator.new.kind # => :uniqueness
+ # PresenceValidator.new(attributes: [:username]).kind # => :presence
+ # AcceptanceValidator.new(attributes: [:terms]).kind # => :acceptance
def kind
self.class.kind
end
@@ -142,8 +143,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)
@@ -163,10 +164,6 @@ module ActiveModel
# +ArgumentError+ when invalid options are supplied.
def check_validity!
end
-
- def should_validate?(record) # :nodoc:
- !record.persisted? || record.changed? || record.marked_for_destruction?
- end
end
# +BlockValidator+ is a special +EachValidator+ which receives a block on initialization
@@ -179,8 +176,8 @@ module ActiveModel
private
- def validate_each(record, attribute, value)
- @block.call(record, attribute, value)
- end
+ def validate_each(record, attribute, value)
+ @block.call(record, attribute, value)
+ end
end
end
diff --git a/activemodel/lib/active_model/version.rb b/activemodel/lib/active_model/version.rb
index 6da3b4117b..dd817f5639 100644
--- a/activemodel/lib/active_model/version.rb
+++ b/activemodel/lib/active_model/version.rb
@@ -1,4 +1,6 @@
-require_relative 'gem_version'
+# frozen_string_literal: true
+
+require_relative "gem_version"
module ActiveModel
# Returns the version of the currently loaded \Active \Model as a <tt>Gem::Version</tt>
diff --git a/activemodel/test/cases/attribute_assignment_test.rb b/activemodel/test/cases/attribute_assignment_test.rb
index 64a85e01eb..30e8419685 100644
--- a/activemodel/test/cases/attribute_assignment_test.rb
+++ b/activemodel/test/cases/attribute_assignment_test.rb
@@ -1,4 +1,7 @@
+# frozen_string_literal: true
+
require "cases/helper"
+require "active_support/core_ext/hash/indifferent_access"
require "active_support/hash_with_indifferent_access"
class AttributeAssignmentTest < ActiveModel::TestCase
@@ -15,21 +18,39 @@ class AttributeAssignmentTest < ActiveModel::TestCase
raise ErrorFromAttributeWriter
end
- protected
-
- attr_writer :metadata
+ private
+ attr_writer :metadata
end
class ErrorFromAttributeWriter < StandardError
end
- class ProtectedParams < ActiveSupport::HashWithIndifferentAccess
+ class ProtectedParams
+ attr_accessor :permitted
+ alias :permitted? :permitted
+
+ delegate :keys, :key?, :has_key?, :empty?, to: :@parameters
+
+ def initialize(attributes)
+ @parameters = attributes.with_indifferent_access
+ @permitted = false
+ end
+
def permit!
@permitted = true
+ self
+ end
+
+ def [](key)
+ @parameters[key]
end
- def permitted?
- @permitted ||= false
+ def to_h
+ @parameters
+ end
+
+ def stringify_keys
+ dup
end
def dup
@@ -47,6 +68,14 @@ class AttributeAssignmentTest < ActiveModel::TestCase
assert_equal "world", model.description
end
+ test "simple assignment alias" do
+ model = Model.new
+
+ model.attributes = { name: "hello", description: "world" }
+ assert_equal "hello", model.name
+ assert_equal "world", model.description
+ end
+
test "assign non-existing attribute" do
model = Model.new
error = assert_raises(ActiveModel::UnknownAttributeError) do
@@ -70,10 +99,12 @@ class AttributeAssignmentTest < ActiveModel::TestCase
end
end
- test "an ArgumentError is raised if a non-hash-like obejct is passed" do
- assert_raises(ArgumentError) do
+ test "an ArgumentError is raised if a non-hash-like object is passed" do
+ err = assert_raises(ArgumentError) do
Model.new(1)
end
+
+ assert_equal("When assigning attributes, you must pass a hash as an argument, Integer passed.", err.message)
end
test "forbidden attributes cannot be used for mass assignment" do
diff --git a/activemodel/test/cases/attribute_methods_test.rb b/activemodel/test/cases/attribute_methods_test.rb
index e81b7ac424..0cfc6f4b6b 100644
--- a/activemodel/test/cases/attribute_methods_test.rb
+++ b/activemodel/test/cases/attribute_methods_test.rb
@@ -1,16 +1,18 @@
-require 'cases/helper'
+# frozen_string_literal: true
+
+require "cases/helper"
class ModelWithAttributes
include ActiveModel::AttributeMethods
class << self
define_method(:bar) do
- 'original bar'
+ "original bar"
end
end
def attributes
- { foo: 'value of foo', baz: 'value of baz' }
+ { foo: "value of foo", baz: "value of baz" }
end
private
@@ -24,7 +26,7 @@ class ModelWithAttributes2
attr_accessor :attributes
- attribute_method_suffix '_test'
+ attribute_method_suffix "_test"
private
def attribute(name)
@@ -48,7 +50,7 @@ class ModelWithAttributesWithSpaces
include ActiveModel::AttributeMethods
def attributes
- { :'foo bar' => 'value of foo bar'}
+ { 'foo bar': "value of foo bar" }
end
private
@@ -62,12 +64,12 @@ class ModelWithWeirdNamesAttributes
class << self
define_method(:'c?d') do
- 'original c?d'
+ "original c?d"
end
end
def attributes
- { :'a?b' => 'value of a?b' }
+ { 'a?b': "value of a?b" }
end
private
@@ -80,7 +82,7 @@ class ModelWithRubyKeywordNamedAttributes
include ActiveModel::AttributeMethods
def attributes
- { begin: 'value of begin', end: 'value of end' }
+ { begin: "value of begin", end: "value of end" }
end
private
@@ -94,16 +96,16 @@ class ModelWithoutAttributesMethod
end
class AttributeMethodsTest < ActiveModel::TestCase
- test 'method missing works correctly even if attributes method is not defined' do
+ test "method missing works correctly even if attributes method is not defined" do
assert_raises(NoMethodError) { ModelWithoutAttributesMethod.new.foo }
end
- test 'unrelated classes should not share attribute method matchers' do
+ test "unrelated classes should not share attribute method matchers" do
assert_not_equal ModelWithAttributes.send(:attribute_method_matchers),
ModelWithAttributes2.send(:attribute_method_matchers)
end
- test '#define_attribute_method generates attribute method' do
+ test "#define_attribute_method generates attribute method" do
begin
ModelWithAttributes.define_attribute_method(:foo)
@@ -114,19 +116,19 @@ class AttributeMethodsTest < ActiveModel::TestCase
end
end
- test '#define_attribute_method does not generate attribute method if already defined in attribute module' do
+ test "#define_attribute_method does not generate attribute method if already defined in attribute module" do
klass = Class.new(ModelWithAttributes)
- klass.generated_attribute_methods.module_eval do
+ klass.send(:generated_attribute_methods).module_eval do
def foo
- '<3'
+ "<3"
end
end
klass.define_attribute_method(:foo)
- assert_equal '<3', klass.new.foo
+ assert_equal "<3", klass.new.foo
end
- test '#define_attribute_method generates a method that is already defined on the host' do
+ test "#define_attribute_method generates a method that is already defined on the host" do
klass = Class.new(ModelWithAttributes) do
def foo
super
@@ -134,21 +136,21 @@ class AttributeMethodsTest < ActiveModel::TestCase
end
klass.define_attribute_method(:foo)
- assert_equal 'value of foo', klass.new.foo
+ assert_equal "value of foo", klass.new.foo
end
- test '#define_attribute_method generates attribute method with invalid identifier characters' do
+ test "#define_attribute_method generates attribute method with invalid identifier characters" do
begin
ModelWithWeirdNamesAttributes.define_attribute_method(:'a?b')
assert_respond_to ModelWithWeirdNamesAttributes.new, :'a?b'
- assert_equal "value of a?b", ModelWithWeirdNamesAttributes.new.send('a?b')
+ assert_equal "value of a?b", ModelWithWeirdNamesAttributes.new.send("a?b")
ensure
ModelWithWeirdNamesAttributes.undefine_attribute_methods
end
end
- test '#define_attribute_methods works passing multiple arguments' do
+ test "#define_attribute_methods works passing multiple arguments" do
begin
ModelWithAttributes.define_attribute_methods(:foo, :baz)
@@ -159,7 +161,7 @@ class AttributeMethodsTest < ActiveModel::TestCase
end
end
- test '#define_attribute_methods generates attribute methods' do
+ test "#define_attribute_methods generates attribute methods" do
begin
ModelWithAttributes.define_attribute_methods(:foo)
@@ -170,7 +172,7 @@ class AttributeMethodsTest < ActiveModel::TestCase
end
end
- test '#alias_attribute generates attribute_aliases lookup hash' do
+ test "#alias_attribute generates attribute_aliases lookup hash" do
klass = Class.new(ModelWithAttributes) do
define_attribute_methods :foo
alias_attribute :bar, :foo
@@ -179,7 +181,7 @@ class AttributeMethodsTest < ActiveModel::TestCase
assert_equal({ "bar" => "foo" }, klass.attribute_aliases)
end
- test '#define_attribute_methods generates attribute methods with spaces in their names' do
+ test "#define_attribute_methods generates attribute methods with spaces in their names" do
begin
ModelWithAttributesWithSpaces.define_attribute_methods(:'foo bar')
@@ -190,7 +192,7 @@ class AttributeMethodsTest < ActiveModel::TestCase
end
end
- test '#alias_attribute works with attributes with spaces in their names' do
+ test "#alias_attribute works with attributes with spaces in their names" do
begin
ModelWithAttributesWithSpaces.define_attribute_methods(:'foo bar')
ModelWithAttributesWithSpaces.alias_attribute(:'foo_bar', :'foo bar')
@@ -201,7 +203,7 @@ class AttributeMethodsTest < ActiveModel::TestCase
end
end
- test '#alias_attribute works with attributes named as a ruby keyword' do
+ test "#alias_attribute works with attributes named as a ruby keyword" do
begin
ModelWithRubyKeywordNamedAttributes.define_attribute_methods([:begin, :end])
ModelWithRubyKeywordNamedAttributes.alias_attribute(:from, :begin)
@@ -214,29 +216,29 @@ class AttributeMethodsTest < ActiveModel::TestCase
end
end
- test '#undefine_attribute_methods removes attribute methods' do
+ test "#undefine_attribute_methods removes attribute methods" do
ModelWithAttributes.define_attribute_methods(:foo)
ModelWithAttributes.undefine_attribute_methods
- assert !ModelWithAttributes.new.respond_to?(:foo)
+ assert_not_respond_to ModelWithAttributes.new, :foo
assert_raises(NoMethodError) { ModelWithAttributes.new.foo }
end
- test 'accessing a suffixed attribute' do
+ test "accessing a suffixed attribute" do
m = ModelWithAttributes2.new
- m.attributes = { 'foo' => 'bar' }
+ m.attributes = { "foo" => "bar" }
- assert_equal 'bar', m.foo
- assert_equal 'bar', m.foo_test
+ assert_equal "bar", m.foo
+ assert_equal "bar", m.foo_test
end
- test 'should not interfere with method_missing if the attr has a private/protected method' do
+ test "should not interfere with method_missing if the attr has a private/protected method" do
m = ModelWithAttributes2.new
- m.attributes = { 'private_method' => '<3', 'protected_method' => 'O_o' }
+ m.attributes = { "private_method" => "<3", "protected_method" => "O_o" }
# dispatches to the *method*, not the attribute
- assert_equal '<3 <3', m.send(:private_method)
- assert_equal 'O_o O_o', m.send(:protected_method)
+ assert_equal "<3 <3", m.send(:private_method)
+ assert_equal "O_o O_o", m.send(:protected_method)
# sees that a method is already defined, so doesn't intervene
assert_raises(NoMethodError) { m.private_method }
@@ -245,15 +247,15 @@ class AttributeMethodsTest < ActiveModel::TestCase
class ClassWithProtected
protected
- def protected_method
- end
+ def protected_method
+ end
end
- test 'should not interfere with respond_to? if the attribute has a private/protected method' do
+ test "should not interfere with respond_to? if the attribute has a private/protected method" do
m = ModelWithAttributes2.new
- m.attributes = { 'private_method' => '<3', 'protected_method' => 'O_o' }
+ m.attributes = { "private_method" => "<3", "protected_method" => "O_o" }
- assert !m.respond_to?(:private_method)
+ assert_not_respond_to m, :private_method
assert m.respond_to?(:private_method, true)
c = ClassWithProtected.new
@@ -264,9 +266,9 @@ class AttributeMethodsTest < ActiveModel::TestCase
assert m.respond_to?(:protected_method, true)
end
- test 'should use attribute_missing to dispatch a missing attribute' do
+ test "should use attribute_missing to dispatch a missing attribute" do
m = ModelWithAttributes2.new
- m.attributes = { 'foo' => 'bar' }
+ m.attributes = { "foo" => "bar" }
def m.attribute_missing(match, *args, &block)
match
@@ -274,8 +276,8 @@ class AttributeMethodsTest < ActiveModel::TestCase
match = m.foo_test
- assert_equal 'foo', match.attr_name
- assert_equal 'attribute_test', match.target
- assert_equal 'foo_test', match.method_name
+ assert_equal "foo", match.attr_name
+ assert_equal "attribute_test", match.target
+ assert_equal "foo_test", match.method_name
end
end
diff --git a/activemodel/test/cases/attribute_set_test.rb b/activemodel/test/cases/attribute_set_test.rb
new file mode 100644
index 0000000000..62feb9074e
--- /dev/null
+++ b/activemodel/test/cases/attribute_set_test.rb
@@ -0,0 +1,274 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "active_model/attribute_set"
+
+module ActiveModel
+ class AttributeSetTest < ActiveModel::TestCase
+ test "building a new set from raw attributes" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
+ attributes = builder.build_from_database(foo: "1.1", bar: "2.2")
+
+ assert_equal 1, attributes[:foo].value
+ assert_equal 2.2, attributes[:bar].value
+ assert_equal :foo, attributes[:foo].name
+ assert_equal :bar, attributes[:bar].name
+ end
+
+ test "building with custom types" do
+ builder = AttributeSet::Builder.new(foo: Type::Float.new)
+ attributes = builder.build_from_database({ foo: "3.3", bar: "4.4" }, { bar: Type::Integer.new })
+
+ assert_equal 3.3, attributes[:foo].value
+ assert_equal 4, attributes[:bar].value
+ end
+
+ test "[] returns a null object" do
+ builder = AttributeSet::Builder.new(foo: Type::Float.new)
+ attributes = builder.build_from_database(foo: "3.3")
+
+ assert_equal "3.3", attributes[:foo].value_before_type_cast
+ assert_nil attributes[:bar].value_before_type_cast
+ assert_equal :bar, attributes[:bar].name
+ end
+
+ test "duping creates a new hash, but does not dup the attributes" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new)
+ attributes = builder.build_from_database(foo: 1, bar: "foo")
+
+ # Ensure the type cast value is cached
+ attributes[:foo].value
+ attributes[:bar].value
+
+ duped = attributes.dup
+ duped.write_from_database(:foo, 2)
+ duped[:bar].value << "bar"
+
+ assert_equal 1, attributes[:foo].value
+ assert_equal 2, duped[:foo].value
+ assert_equal "foobar", attributes[:bar].value
+ assert_equal "foobar", duped[:bar].value
+ end
+
+ test "deep_duping creates a new hash and dups each attribute" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new)
+ attributes = builder.build_from_database(foo: 1, bar: "foo")
+
+ # Ensure the type cast value is cached
+ attributes[:foo].value
+ attributes[:bar].value
+
+ duped = attributes.deep_dup
+ duped.write_from_database(:foo, 2)
+ duped[:bar].value << "bar"
+
+ assert_equal 1, attributes[:foo].value
+ assert_equal 2, duped[:foo].value
+ assert_equal "foo", attributes[:bar].value
+ assert_equal "foobar", duped[:bar].value
+ end
+
+ test "freezing cloned set does not freeze original" do
+ attributes = AttributeSet.new({})
+ clone = attributes.clone
+
+ clone.freeze
+
+ assert_predicate clone, :frozen?
+ assert_not_predicate attributes, :frozen?
+ end
+
+ test "to_hash returns a hash of the type cast values" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
+ attributes = builder.build_from_database(foo: "1.1", bar: "2.2")
+
+ assert_equal({ foo: 1, bar: 2.2 }, attributes.to_hash)
+ assert_equal({ foo: 1, bar: 2.2 }, attributes.to_h)
+ end
+
+ test "to_hash maintains order" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
+ attributes = builder.build_from_database(foo: "2.2", bar: "3.3")
+
+ attributes[:bar]
+ hash = attributes.to_h
+
+ assert_equal [[:foo, 2], [:bar, 3.3]], hash.to_a
+ end
+
+ test "values_before_type_cast" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new)
+ attributes = builder.build_from_database(foo: "1.1", bar: "2.2")
+
+ assert_equal({ foo: "1.1", bar: "2.2" }, attributes.values_before_type_cast)
+ end
+
+ test "known columns are built with uninitialized attributes" do
+ attributes = attributes_with_uninitialized_key
+ assert_predicate attributes[:foo], :initialized?
+ assert_not_predicate attributes[:bar], :initialized?
+ end
+
+ test "uninitialized attributes are not included in the attributes hash" do
+ attributes = attributes_with_uninitialized_key
+ assert_equal({ foo: 1 }, attributes.to_hash)
+ end
+
+ test "uninitialized attributes are not included in keys" do
+ attributes = attributes_with_uninitialized_key
+ assert_equal [:foo], attributes.keys
+ end
+
+ test "uninitialized attributes return false for key?" do
+ attributes = attributes_with_uninitialized_key
+ assert attributes.key?(:foo)
+ assert_not attributes.key?(:bar)
+ end
+
+ test "unknown attributes return false for key?" do
+ attributes = attributes_with_uninitialized_key
+ assert_not attributes.key?(:wibble)
+ end
+
+ test "fetch_value returns the value for the given initialized attribute" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
+ attributes = builder.build_from_database(foo: "1.1", bar: "2.2")
+
+ assert_equal 1, attributes.fetch_value(:foo)
+ assert_equal 2.2, attributes.fetch_value(:bar)
+ end
+
+ test "fetch_value returns nil for unknown attributes" do
+ attributes = attributes_with_uninitialized_key
+ assert_nil attributes.fetch_value(:wibble) { "hello" }
+ end
+
+ test "fetch_value returns nil for unknown attributes when types has a default" do
+ types = Hash.new(Type::Value.new)
+ builder = AttributeSet::Builder.new(types)
+ attributes = builder.build_from_database
+
+ assert_nil attributes.fetch_value(:wibble) { "hello" }
+ end
+
+ test "fetch_value uses the given block for uninitialized attributes" do
+ attributes = attributes_with_uninitialized_key
+ value = attributes.fetch_value(:bar) { |n| n.to_s + "!" }
+ assert_equal "bar!", value
+ end
+
+ test "fetch_value returns nil for uninitialized attributes if no block is given" do
+ attributes = attributes_with_uninitialized_key
+ assert_nil attributes.fetch_value(:bar)
+ end
+
+ test "the primary_key is always initialized" do
+ defaults = { foo: Attribute.from_user(:foo, nil, nil) }
+ builder = AttributeSet::Builder.new({ foo: Type::Integer.new }, defaults)
+ attributes = builder.build_from_database
+
+ assert attributes.key?(:foo)
+ assert_equal [:foo], attributes.keys
+ assert_predicate attributes[:foo], :initialized?
+ end
+
+ class MyType
+ def cast(value)
+ return if value.nil?
+ value + " from user"
+ end
+
+ def deserialize(value)
+ return if value.nil?
+ value + " from database"
+ end
+
+ def assert_valid_value(*)
+ end
+ end
+
+ test "write_from_database sets the attribute with database typecasting" do
+ builder = AttributeSet::Builder.new(foo: MyType.new)
+ attributes = builder.build_from_database
+
+ assert_nil attributes.fetch_value(:foo)
+
+ attributes.write_from_database(:foo, "value")
+
+ assert_equal "value from database", attributes.fetch_value(:foo)
+ end
+
+ test "write_from_user sets the attribute with user typecasting" do
+ builder = AttributeSet::Builder.new(foo: MyType.new)
+ attributes = builder.build_from_database
+
+ assert_nil attributes.fetch_value(:foo)
+
+ attributes.write_from_user(:foo, "value")
+
+ assert_equal "value from user", attributes.fetch_value(:foo)
+ end
+
+ test "freezing doesn't prevent the set from materializing" do
+ builder = AttributeSet::Builder.new(foo: Type::String.new)
+ attributes = builder.build_from_database(foo: "1")
+
+ attributes.freeze
+ assert_equal({ foo: "1" }, attributes.to_hash)
+ end
+
+ test "marshalling dump/load legacy materialized attribute hash" do
+ builder = AttributeSet::Builder.new(foo: Type::String.new)
+ attributes = builder.build_from_database(foo: "1")
+
+ attributes.instance_variable_get(:@attributes).instance_eval do
+ class << self
+ def marshal_dump
+ materialize
+ end
+ end
+ end
+
+ attributes = Marshal.load(Marshal.dump(attributes))
+ assert_equal({ foo: "1" }, attributes.to_hash)
+ end
+
+ test "#accessed_attributes returns only attributes which have been read" do
+ builder = AttributeSet::Builder.new(foo: Type::Value.new, bar: Type::Value.new)
+ attributes = builder.build_from_database(foo: "1", bar: "2")
+
+ assert_equal [], attributes.accessed
+
+ attributes.fetch_value(:foo)
+
+ assert_equal [:foo], attributes.accessed
+ end
+
+ test "#map returns a new attribute set with the changes applied" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new)
+ attributes = builder.build_from_database(foo: "1", bar: "2")
+ new_attributes = attributes.map do |attr|
+ attr.with_cast_value(attr.value + 1)
+ end
+
+ assert_equal 2, new_attributes.fetch_value(:foo)
+ assert_equal 3, new_attributes.fetch_value(:bar)
+ end
+
+ test "comparison for equality is correctly implemented" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new)
+ attributes = builder.build_from_database(foo: "1", bar: "2")
+ attributes2 = builder.build_from_database(foo: "1", bar: "2")
+ attributes3 = builder.build_from_database(foo: "2", bar: "2")
+
+ assert_equal attributes, attributes2
+ assert_not_equal attributes2, attributes3
+ end
+
+ private
+ def attributes_with_uninitialized_key
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
+ builder.build_from_database(foo: "1.1")
+ end
+ end
+end
diff --git a/activemodel/test/cases/attribute_test.rb b/activemodel/test/cases/attribute_test.rb
new file mode 100644
index 0000000000..20c02e689c
--- /dev/null
+++ b/activemodel/test/cases/attribute_test.rb
@@ -0,0 +1,255 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+
+module ActiveModel
+ class AttributeTest < ActiveModel::TestCase
+ setup do
+ @type = Minitest::Mock.new
+ end
+
+ teardown do
+ assert @type.verify
+ end
+
+ test "from_database + read type casts from database" do
+ @type.expect(:deserialize, "type cast from database", ["a value"])
+ attribute = Attribute.from_database(nil, "a value", @type)
+
+ type_cast_value = attribute.value
+
+ assert_equal "type cast from database", type_cast_value
+ end
+
+ test "from_user + read type casts from user" do
+ @type.expect(:cast, "type cast from user", ["a value"])
+ attribute = Attribute.from_user(nil, "a value", @type)
+
+ type_cast_value = attribute.value
+
+ assert_equal "type cast from user", type_cast_value
+ end
+
+ test "reading memoizes the value" do
+ @type.expect(:deserialize, "from the database", ["whatever"])
+ attribute = Attribute.from_database(nil, "whatever", @type)
+
+ type_cast_value = attribute.value
+ second_read = attribute.value
+
+ assert_equal "from the database", type_cast_value
+ assert_same type_cast_value, second_read
+ end
+
+ test "reading memoizes falsy values" do
+ @type.expect(:deserialize, false, ["whatever"])
+ attribute = Attribute.from_database(nil, "whatever", @type)
+
+ attribute.value
+ attribute.value
+ end
+
+ test "read_before_typecast returns the given value" do
+ attribute = Attribute.from_database(nil, "raw value", @type)
+
+ raw_value = attribute.value_before_type_cast
+
+ assert_equal "raw value", raw_value
+ end
+
+ test "from_database + read_for_database type casts to and from database" do
+ @type.expect(:deserialize, "read from database", ["whatever"])
+ @type.expect(:serialize, "ready for database", ["read from database"])
+ attribute = Attribute.from_database(nil, "whatever", @type)
+
+ serialize = attribute.value_for_database
+
+ assert_equal "ready for database", serialize
+ end
+
+ test "from_user + read_for_database type casts from the user to the database" do
+ @type.expect(:cast, "read from user", ["whatever"])
+ @type.expect(:serialize, "ready for database", ["read from user"])
+ attribute = Attribute.from_user(nil, "whatever", @type)
+
+ serialize = attribute.value_for_database
+
+ assert_equal "ready for database", serialize
+ end
+
+ test "duping dups the value" do
+ @type.expect(:deserialize, +"type cast", ["a value"])
+ attribute = Attribute.from_database(nil, "a value", @type)
+
+ value_from_orig = attribute.value
+ value_from_clone = attribute.dup.value
+ value_from_orig << " foo"
+
+ assert_equal "type cast foo", value_from_orig
+ assert_equal "type cast", value_from_clone
+ end
+
+ test "duping does not dup the value if it is not dupable" do
+ @type.expect(:deserialize, false, ["a value"])
+ attribute = Attribute.from_database(nil, "a value", @type)
+
+ assert_same attribute.value, attribute.dup.value
+ end
+
+ test "duping does not eagerly type cast if we have not yet type cast" do
+ attribute = Attribute.from_database(nil, "a value", @type)
+ attribute.dup
+ end
+
+ class MyType
+ def cast(value)
+ value + " from user"
+ end
+
+ def deserialize(value)
+ value + " from database"
+ end
+
+ def assert_valid_value(*)
+ end
+ end
+
+ test "with_value_from_user returns a new attribute with the value from the user" do
+ old = Attribute.from_database(nil, "old", MyType.new)
+ new = old.with_value_from_user("new")
+
+ assert_equal "old from database", old.value
+ assert_equal "new from user", new.value
+ end
+
+ test "with_value_from_database returns a new attribute with the value from the database" do
+ old = Attribute.from_user(nil, "old", MyType.new)
+ new = old.with_value_from_database("new")
+
+ assert_equal "old from user", old.value
+ assert_equal "new from database", new.value
+ end
+
+ test "uninitialized attributes yield their name if a block is given to value" do
+ block = proc { |name| name.to_s + "!" }
+ foo = Attribute.uninitialized(:foo, nil)
+ bar = Attribute.uninitialized(:bar, nil)
+
+ assert_equal "foo!", foo.value(&block)
+ assert_equal "bar!", bar.value(&block)
+ end
+
+ test "uninitialized attributes have no value" do
+ assert_nil Attribute.uninitialized(:foo, nil).value
+ end
+
+ test "attributes equal other attributes with the same constructor arguments" do
+ first = Attribute.from_database(:foo, 1, Type::Integer.new)
+ second = Attribute.from_database(:foo, 1, Type::Integer.new)
+ assert_equal first, second
+ end
+
+ test "attributes do not equal attributes with different names" do
+ first = Attribute.from_database(:foo, 1, Type::Integer.new)
+ second = Attribute.from_database(:bar, 1, Type::Integer.new)
+ assert_not_equal first, second
+ end
+
+ test "attributes do not equal attributes with different types" do
+ first = Attribute.from_database(:foo, 1, Type::Integer.new)
+ second = Attribute.from_database(:foo, 1, Type::Float.new)
+ assert_not_equal first, second
+ end
+
+ test "attributes do not equal attributes with different values" do
+ first = Attribute.from_database(:foo, 1, Type::Integer.new)
+ second = Attribute.from_database(:foo, 2, Type::Integer.new)
+ assert_not_equal first, second
+ end
+
+ test "attributes do not equal attributes of other classes" do
+ first = Attribute.from_database(:foo, 1, Type::Integer.new)
+ second = Attribute.from_user(:foo, 1, Type::Integer.new)
+ assert_not_equal first, second
+ end
+
+ test "an attribute has not been read by default" do
+ attribute = Attribute.from_database(:foo, 1, Type::Value.new)
+ assert_not_predicate attribute, :has_been_read?
+ end
+
+ test "an attribute has been read when its value is calculated" do
+ attribute = Attribute.from_database(:foo, 1, Type::Value.new)
+ attribute.value
+ assert_predicate attribute, :has_been_read?
+ end
+
+ test "an attribute is not changed if it hasn't been assigned or mutated" do
+ attribute = Attribute.from_database(:foo, 1, Type::Value.new)
+
+ assert_not_predicate attribute, :changed?
+ end
+
+ test "an attribute is changed if it's been assigned a new value" do
+ attribute = Attribute.from_database(:foo, 1, Type::Value.new)
+ changed = attribute.with_value_from_user(2)
+
+ assert_predicate changed, :changed?
+ end
+
+ test "an attribute is not changed if it's assigned the same value" do
+ attribute = Attribute.from_database(:foo, 1, Type::Value.new)
+ unchanged = attribute.with_value_from_user(1)
+
+ assert_not_predicate unchanged, :changed?
+ end
+
+ test "an attribute can not be mutated if it has not been read,
+ and skips expensive calculations" do
+ type_which_raises_from_all_methods = Object.new
+ attribute = Attribute.from_database(:foo, "bar", type_which_raises_from_all_methods)
+
+ assert_not_predicate attribute, :changed_in_place?
+ end
+
+ test "an attribute is changed if it has been mutated" do
+ attribute = Attribute.from_database(:foo, "bar", Type::String.new)
+ attribute.value << "!"
+
+ assert_predicate attribute, :changed_in_place?
+ assert_predicate attribute, :changed?
+ end
+
+ test "an attribute can forget its changes" do
+ attribute = Attribute.from_database(:foo, "bar", Type::String.new)
+ changed = attribute.with_value_from_user("foo")
+ forgotten = changed.forgetting_assignment
+
+ assert changed.changed? # sanity check
+ assert_not_predicate forgotten, :changed?
+ end
+
+ test "with_value_from_user validates the value" do
+ type = Type::Value.new
+ type.define_singleton_method(:assert_valid_value) do |value|
+ if value == 1
+ raise ArgumentError
+ end
+ end
+
+ attribute = Attribute.from_database(:foo, 1, type)
+ assert_equal 1, attribute.value
+ assert_equal 2, attribute.with_value_from_user(2).value
+ assert_raises ArgumentError do
+ attribute.with_value_from_user(1)
+ end
+ end
+
+ test "with_type preserves mutations" do
+ attribute = Attribute.from_database(:foo, +"", Type::Value.new)
+ attribute.value << "1"
+
+ assert_equal 1, attribute.with_type(Type::Integer.new).value
+ end
+ end
+end
diff --git a/activemodel/test/cases/attributes_dirty_test.rb b/activemodel/test/cases/attributes_dirty_test.rb
new file mode 100644
index 0000000000..f9693a23cd
--- /dev/null
+++ b/activemodel/test/cases/attributes_dirty_test.rb
@@ -0,0 +1,205 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+
+class AttributesDirtyTest < ActiveModel::TestCase
+ class DirtyModel
+ include ActiveModel::Model
+ include ActiveModel::Attributes
+ include ActiveModel::Dirty
+ attribute :name, :string
+ attribute :color, :string
+ attribute :size, :integer
+
+ def save
+ changes_applied
+ end
+
+ def reload
+ clear_changes_information
+ end
+ end
+
+ setup do
+ @model = DirtyModel.new
+ end
+
+ test "setting attribute will result in change" do
+ assert_not_predicate @model, :changed?
+ assert_not_predicate @model, :name_changed?
+ @model.name = "Ringo"
+ assert_predicate @model, :changed?
+ assert_predicate @model, :name_changed?
+ end
+
+ test "list of changed attribute keys" do
+ assert_equal [], @model.changed
+ @model.name = "Paul"
+ assert_equal ["name"], @model.changed
+ end
+
+ test "changes to attribute values" do
+ assert_not @model.changes["name"]
+ @model.name = "John"
+ assert_equal [nil, "John"], @model.changes["name"]
+ end
+
+ test "checking if an attribute has changed to a particular value" do
+ @model.name = "Ringo"
+ assert @model.name_changed?(from: nil, to: "Ringo")
+ assert_not @model.name_changed?(from: "Pete", to: "Ringo")
+ assert @model.name_changed?(to: "Ringo")
+ assert_not @model.name_changed?(to: "Pete")
+ assert @model.name_changed?(from: nil)
+ assert_not @model.name_changed?(from: "Pete")
+ end
+
+ test "changes accessible through both strings and symbols" do
+ @model.name = "David"
+ assert_not_nil @model.changes[:name]
+ assert_not_nil @model.changes["name"]
+ end
+
+ test "be consistent with symbols arguments after the changes are applied" do
+ @model.name = "David"
+ assert @model.attribute_changed?(:name)
+ @model.save
+ @model.name = "Rafael"
+ assert @model.attribute_changed?(:name)
+ end
+
+ test "attribute mutation" do
+ @model.name = "Yam"
+ @model.save
+ assert_not_predicate @model, :name_changed?
+ @model.name.replace("Hadad")
+ assert_predicate @model, :name_changed?
+ end
+
+ test "resetting attribute" do
+ @model.name = "Bob"
+ @model.restore_name!
+ assert_nil @model.name
+ assert_not_predicate @model, :name_changed?
+ end
+
+ test "setting color to same value should not result in change being recorded" do
+ @model.color = "red"
+ assert_predicate @model, :color_changed?
+ @model.save
+ assert_not_predicate @model, :color_changed?
+ assert_not_predicate @model, :changed?
+ @model.color = "red"
+ assert_not_predicate @model, :color_changed?
+ assert_not_predicate @model, :changed?
+ end
+
+ test "saving should reset model's changed status" do
+ @model.name = "Alf"
+ assert_predicate @model, :changed?
+ @model.save
+ assert_not_predicate @model, :changed?
+ assert_not_predicate @model, :name_changed?
+ end
+
+ test "saving should preserve previous changes" do
+ @model.name = "Jericho Cane"
+ @model.save
+ assert_equal [nil, "Jericho Cane"], @model.previous_changes["name"]
+ end
+
+ test "setting new attributes should not affect previous changes" do
+ @model.name = "Jericho Cane"
+ @model.save
+ @model.name = "DudeFella ManGuy"
+ assert_equal [nil, "Jericho Cane"], @model.name_previous_change
+ end
+
+ test "saving should preserve model's previous changed status" do
+ @model.name = "Jericho Cane"
+ @model.save
+ assert_predicate @model, :name_previously_changed?
+ end
+
+ test "previous value is preserved when changed after save" do
+ assert_equal({}, @model.changed_attributes)
+ @model.name = "Paul"
+ assert_equal({ "name" => nil }, @model.changed_attributes)
+
+ @model.save
+
+ @model.name = "John"
+ assert_equal({ "name" => "Paul" }, @model.changed_attributes)
+ end
+
+ test "changing the same attribute multiple times retains the correct original value" do
+ @model.name = "Otto"
+ @model.save
+ @model.name = "DudeFella ManGuy"
+ @model.name = "Mr. Manfredgensonton"
+ assert_equal ["Otto", "Mr. Manfredgensonton"], @model.name_change
+ assert_equal @model.name_was, "Otto"
+ end
+
+ test "using attribute_will_change! with a symbol" do
+ @model.size = 1
+ assert_predicate @model, :size_changed?
+ end
+
+ test "reload should reset all changes" do
+ @model.name = "Dmitry"
+ @model.name_changed?
+ @model.save
+ @model.name = "Bob"
+
+ assert_equal [nil, "Dmitry"], @model.previous_changes["name"]
+ assert_equal "Dmitry", @model.changed_attributes["name"]
+
+ @model.reload
+
+ assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.previous_changes
+ assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.changed_attributes
+ end
+
+ test "restore_attributes should restore all previous data" do
+ @model.name = "Dmitry"
+ @model.color = "Red"
+ @model.save
+ @model.name = "Bob"
+ @model.color = "White"
+
+ @model.restore_attributes
+
+ assert_not_predicate @model, :changed?
+ assert_equal "Dmitry", @model.name
+ assert_equal "Red", @model.color
+ end
+
+ test "restore_attributes can restore only some attributes" do
+ @model.name = "Dmitry"
+ @model.color = "Red"
+ @model.save
+ @model.name = "Bob"
+ @model.color = "White"
+
+ @model.restore_attributes(["name"])
+
+ assert_predicate @model, :changed?
+ assert_equal "Dmitry", @model.name
+ assert_equal "White", @model.color
+ end
+
+ test "changing the attribute reports a change only when the cast value changes" do
+ @model.size = "2.3"
+ @model.save
+ @model.size = "2.1"
+
+ assert_equal false, @model.changed?
+
+ @model.size = "5.1"
+
+ assert_equal true, @model.changed?
+ assert_equal true, @model.size_changed?
+ assert_equal({ "size" => [2, 5] }, @model.changes)
+ end
+end
diff --git a/activemodel/test/cases/attributes_test.rb b/activemodel/test/cases/attributes_test.rb
new file mode 100644
index 0000000000..5483fb101d
--- /dev/null
+++ b/activemodel/test/cases/attributes_test.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+
+module ActiveModel
+ class AttributesTest < ActiveModel::TestCase
+ class ModelForAttributesTest
+ include ActiveModel::Model
+ include ActiveModel::Attributes
+
+ attribute :integer_field, :integer
+ attribute :string_field, :string
+ attribute :decimal_field, :decimal
+ attribute :string_with_default, :string, default: "default string"
+ attribute :date_field, :date, default: -> { Date.new(2016, 1, 1) }
+ attribute :boolean_field, :boolean
+ end
+
+ class ChildModelForAttributesTest < ModelForAttributesTest
+ end
+
+ class GrandchildModelForAttributesTest < ChildModelForAttributesTest
+ attribute :integer_field, :string
+ end
+
+ test "properties assignment" do
+ data = ModelForAttributesTest.new(
+ integer_field: "2.3",
+ string_field: "Rails FTW",
+ decimal_field: "12.3",
+ boolean_field: "0"
+ )
+
+ assert_equal 2, data.integer_field
+ assert_equal "Rails FTW", data.string_field
+ assert_equal BigDecimal("12.3"), data.decimal_field
+ assert_equal "default string", data.string_with_default
+ assert_equal Date.new(2016, 1, 1), data.date_field
+ assert_equal false, data.boolean_field
+
+ data.integer_field = 10
+ data.string_with_default = nil
+ data.boolean_field = "1"
+
+ assert_equal 10, data.integer_field
+ assert_nil data.string_with_default
+ assert_equal true, data.boolean_field
+ end
+
+ test "reading attributes" do
+ data = ModelForAttributesTest.new(
+ integer_field: 1.1,
+ string_field: 1.1,
+ decimal_field: 1.1,
+ boolean_field: 1.1
+ )
+
+ expected_attributes = {
+ integer_field: 1,
+ string_field: "1.1",
+ decimal_field: BigDecimal("1.1"),
+ string_with_default: "default string",
+ date_field: Date.new(2016, 1, 1),
+ boolean_field: true
+ }.stringify_keys
+
+ assert_equal expected_attributes, data.attributes
+ end
+
+ test "nonexistent attribute" do
+ assert_raise ActiveModel::UnknownAttributeError do
+ ModelForAttributesTest.new(nonexistent: "nonexistent")
+ end
+ end
+
+ test "children inherit attributes" do
+ data = ChildModelForAttributesTest.new(integer_field: "4.4")
+
+ assert_equal 4, data.integer_field
+ end
+
+ test "children can override parents" do
+ data = GrandchildModelForAttributesTest.new(integer_field: "4.4")
+
+ assert_equal "4.4", data.integer_field
+ end
+
+ test "attributes with proc defaults can be marshalled" do
+ data = ModelForAttributesTest.new
+ attributes = data.instance_variable_get(:@attributes)
+ round_tripped = Marshal.load(Marshal.dump(data))
+ new_attributes = round_tripped.instance_variable_get(:@attributes)
+
+ assert_equal attributes, new_attributes
+ end
+ end
+end
diff --git a/activemodel/test/cases/callbacks_test.rb b/activemodel/test/cases/callbacks_test.rb
index 85455c112c..0711dc56ca 100644
--- a/activemodel/test/cases/callbacks_test.rb
+++ b/activemodel/test/cases/callbacks_test.rb
@@ -1,7 +1,8 @@
+# frozen_string_literal: true
+
require "cases/helper"
class CallbacksTest < ActiveModel::TestCase
-
class CallbackValidator
def around_create(model)
model.callbacks << :before_around_create
@@ -28,7 +29,7 @@ class CallbacksTest < ActiveModel::TestCase
false
end
- after_create "@callbacks << :final_callback"
+ after_create { |model| model.callbacks << :final_callback }
def initialize(options = {})
@callbacks = []
@@ -64,12 +65,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
@@ -86,21 +85,21 @@ class CallbacksTest < ActiveModel::TestCase
end
test "only selects which types of callbacks should be created" do
- assert !ModelCallbacks.respond_to?(:before_initialize)
- assert !ModelCallbacks.respond_to?(:around_initialize)
+ assert_not_respond_to ModelCallbacks, :before_initialize
+ assert_not_respond_to ModelCallbacks, :around_initialize
assert_respond_to ModelCallbacks, :after_initialize
end
test "only selects which types of callbacks should be created from an array list" do
assert_respond_to ModelCallbacks, :before_multiple
assert_respond_to ModelCallbacks, :around_multiple
- assert !ModelCallbacks.respond_to?(:after_multiple)
+ assert_not_respond_to ModelCallbacks, :after_multiple
end
test "no callbacks should be created" do
- assert !ModelCallbacks.respond_to?(:before_empty)
- assert !ModelCallbacks.respond_to?(:around_empty)
- assert !ModelCallbacks.respond_to?(:after_empty)
+ assert_not_respond_to ModelCallbacks, :before_empty
+ assert_not_respond_to ModelCallbacks, :around_empty
+ assert_not_respond_to ModelCallbacks, :after_empty
end
class Violin
@@ -110,10 +109,10 @@ class CallbacksTest < ActiveModel::TestCase
end
extend ActiveModel::Callbacks
define_model_callbacks :create
- def callback1; self.history << 'callback1'; end
- def callback2; self.history << 'callback2'; end
+ def callback1; history << "callback1"; end
+ def callback2; history << "callback2"; end
def create
- run_callbacks(:create) {}
+ run_callbacks(:create) { }
self
end
end
@@ -128,8 +127,8 @@ 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
-
end
diff --git a/activemodel/test/cases/conversion_test.rb b/activemodel/test/cases/conversion_test.rb
index 800cad6d9a..347896ed50 100644
--- a/activemodel/test/cases/conversion_test.rb
+++ b/activemodel/test/cases/conversion_test.rb
@@ -1,6 +1,8 @@
-require 'cases/helper'
-require 'models/contact'
-require 'models/helicopter'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/contact"
+require "models/helicopter"
class ConversionTest < ActiveModel::TestCase
test "to_model default implementation returns self" do
diff --git a/activemodel/test/cases/dirty_test.rb b/activemodel/test/cases/dirty_test.rb
index d17a12ad12..0edbbffa86 100644
--- a/activemodel/test/cases/dirty_test.rb
+++ b/activemodel/test/cases/dirty_test.rb
@@ -1,43 +1,41 @@
+# frozen_string_literal: true
+
require "cases/helper"
class DirtyTest < ActiveModel::TestCase
class DirtyModel
include ActiveModel::Dirty
- define_attribute_methods :name, :color, :size
+ define_attribute_methods :name, :color, :size, :status
def initialize
@name = nil
@color = nil
@size = nil
+ @status = "initialized"
end
- def name
- @name
- end
+ attr_reader :name, :color, :size, :status
def name=(val)
name_will_change!
@name = val
end
- def color
- @color
- end
-
def color=(val)
color_will_change! unless val == @color
@color = val
end
- def size
- @size
- end
-
def size=(val)
attribute_will_change!(:size) unless val == @size
@size = val
end
+ def status=(val)
+ status_will_change! unless val == @status
+ @status = val
+ end
+
def save
changes_applied
end
@@ -52,23 +50,23 @@ class DirtyTest < ActiveModel::TestCase
end
test "setting attribute will result in change" do
- assert !@model.changed?
- assert !@model.name_changed?
+ assert_not_predicate @model, :changed?
+ assert_not_predicate @model, :name_changed?
@model.name = "Ringo"
- assert @model.changed?
- assert @model.name_changed?
+ assert_predicate @model, :changed?
+ assert_predicate @model, :name_changed?
end
test "list of changed attribute keys" do
assert_equal [], @model.changed
@model.name = "Paul"
- assert_equal ['name'], @model.changed
+ assert_equal ["name"], @model.changed
end
test "changes to attribute values" do
- assert !@model.changes['name']
+ assert_not @model.changes["name"]
@model.name = "John"
- assert_equal [nil, "John"], @model.changes['name']
+ assert_equal [nil, "John"], @model.changes["name"]
end
test "checking if an attribute has changed to a particular value" do
@@ -84,105 +82,116 @@ class DirtyTest < ActiveModel::TestCase
test "changes accessible through both strings and symbols" do
@model.name = "David"
assert_not_nil @model.changes[:name]
- assert_not_nil @model.changes['name']
+ assert_not_nil @model.changes["name"]
end
test "be consistent with symbols arguments after the changes are applied" do
@model.name = "David"
assert @model.attribute_changed?(:name)
@model.save
- @model.name = 'Rafael'
+ @model.name = "Rafael"
assert @model.attribute_changed?(:name)
end
test "attribute mutation" do
- @model.instance_variable_set("@name", "Yam")
- assert !@model.name_changed?
+ @model.instance_variable_set("@name", +"Yam")
+ assert_not_predicate @model, :name_changed?
@model.name.replace("Hadad")
- assert !@model.name_changed?
+ assert_not_predicate @model, :name_changed?
@model.name_will_change!
@model.name.replace("Baal")
- assert @model.name_changed?
+ assert_predicate @model, :name_changed?
end
test "resetting attribute" do
@model.name = "Bob"
@model.restore_name!
assert_nil @model.name
- assert !@model.name_changed?
+ assert_not_predicate @model, :name_changed?
end
test "setting color to same value should not result in change being recorded" do
@model.color = "red"
- assert @model.color_changed?
+ assert_predicate @model, :color_changed?
@model.save
- assert !@model.color_changed?
- assert !@model.changed?
+ assert_not_predicate @model, :color_changed?
+ assert_not_predicate @model, :changed?
@model.color = "red"
- assert !@model.color_changed?
- assert !@model.changed?
+ assert_not_predicate @model, :color_changed?
+ assert_not_predicate @model, :changed?
end
test "saving should reset model's changed status" do
@model.name = "Alf"
- assert @model.changed?
+ assert_predicate @model, :changed?
@model.save
- assert !@model.changed?
- assert !@model.name_changed?
+ assert_not_predicate @model, :changed?
+ assert_not_predicate @model, :name_changed?
end
test "saving should preserve previous changes" do
@model.name = "Jericho Cane"
+ @model.status = "waiting"
@model.save
- assert_equal [nil, "Jericho Cane"], @model.previous_changes['name']
+ assert_equal [nil, "Jericho Cane"], @model.previous_changes["name"]
+ assert_equal ["initialized", "waiting"], @model.previous_changes["status"]
end
test "setting new attributes should not affect previous changes" do
@model.name = "Jericho Cane"
+ @model.status = "waiting"
@model.save
@model.name = "DudeFella ManGuy"
+ @model.status = "finished"
assert_equal [nil, "Jericho Cane"], @model.name_previous_change
+ assert_equal ["initialized", "waiting"], @model.previous_changes["status"]
end
test "saving should preserve model's previous changed status" do
@model.name = "Jericho Cane"
@model.save
- assert @model.name_previously_changed?
+ assert_predicate @model, :name_previously_changed?
end
test "previous value is preserved when changed after save" do
assert_equal({}, @model.changed_attributes)
@model.name = "Paul"
- assert_equal({ "name" => nil }, @model.changed_attributes)
+ @model.status = "waiting"
+ assert_equal({ "name" => nil, "status" => "initialized" }, @model.changed_attributes)
@model.save
@model.name = "John"
- assert_equal({ "name" => "Paul" }, @model.changed_attributes)
+ @model.status = "finished"
+ assert_equal({ "name" => "Paul", "status" => "waiting" }, @model.changed_attributes)
end
test "changing the same attribute multiple times retains the correct original value" do
@model.name = "Otto"
+ @model.status = "waiting"
@model.save
@model.name = "DudeFella ManGuy"
@model.name = "Mr. Manfredgensonton"
+ @model.status = "processing"
+ @model.status = "finished"
assert_equal ["Otto", "Mr. Manfredgensonton"], @model.name_change
+ assert_equal ["waiting", "finished"], @model.status_change
assert_equal @model.name_was, "Otto"
end
test "using attribute_will_change! with a symbol" do
@model.size = 1
- assert @model.size_changed?
+ assert_predicate @model, :size_changed?
end
test "reload should reset all changes" do
- @model.name = 'Dmitry'
+ @model.name = "Dmitry"
@model.name_changed?
@model.save
- @model.name = 'Bob'
+ @model.name = "Bob"
- assert_equal [nil, 'Dmitry'], @model.previous_changes['name']
- assert_equal 'Dmitry', @model.changed_attributes['name']
+ assert_equal [nil, "Dmitry"], @model.previous_changes["name"]
+ assert_equal "Dmitry", @model.changed_attributes["name"]
@model.reload
@@ -191,30 +200,34 @@ class DirtyTest < ActiveModel::TestCase
end
test "restore_attributes should restore all previous data" do
- @model.name = 'Dmitry'
- @model.color = 'Red'
+ @model.name = "Dmitry"
+ @model.color = "Red"
@model.save
- @model.name = 'Bob'
- @model.color = 'White'
+ @model.name = "Bob"
+ @model.color = "White"
@model.restore_attributes
- assert_not @model.changed?
- assert_equal 'Dmitry', @model.name
- assert_equal 'Red', @model.color
+ assert_not_predicate @model, :changed?
+ assert_equal "Dmitry", @model.name
+ assert_equal "Red", @model.color
end
test "restore_attributes can restore only some attributes" do
- @model.name = 'Dmitry'
- @model.color = 'Red'
+ @model.name = "Dmitry"
+ @model.color = "Red"
@model.save
- @model.name = 'Bob'
- @model.color = 'White'
+ @model.name = "Bob"
+ @model.color = "White"
+
+ @model.restore_attributes(["name"])
- @model.restore_attributes(['name'])
+ assert_predicate @model, :changed?
+ assert_equal "Dmitry", @model.name
+ assert_equal "White", @model.color
+ end
- assert @model.changed?
- assert_equal 'Dmitry', @model.name
- assert_equal 'White', @model.color
+ test "model can be dup-ed without Attributes" do
+ assert @model.dup
end
end
diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb
index f6d171bec6..185b5a24ae 100644
--- a/activemodel/test/cases/errors_test.rb
+++ b/activemodel/test/cases/errors_test.rb
@@ -1,4 +1,7 @@
+# frozen_string_literal: true
+
require "cases/helper"
+require "yaml"
class ErrorsTest < ActiveModel::TestCase
class Person
@@ -11,7 +14,7 @@ class ErrorsTest < ActiveModel::TestCase
attr_reader :errors
def validate!
- errors.add(:name, "cannot be nil") if name == nil
+ errors.add(:name, :blank, message: "cannot be nil") if name == nil
end
def read_attribute_for_validation(attr)
@@ -29,45 +32,48 @@ class ErrorsTest < ActiveModel::TestCase
def test_delete
errors = ActiveModel::Errors.new(self)
- errors[:foo] << 'omg'
- errors.delete(:foo)
+ errors[:foo] << "omg"
+ errors.delete("foo")
assert_empty errors[:foo]
end
def test_include?
errors = ActiveModel::Errors.new(self)
- errors[:foo] << 'omg'
- assert errors.include?(:foo), 'errors should include :foo'
+ 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
errors = ActiveModel::Errors.new(self)
- errors[:foo] << 'bar'
+ errors[:foo] << "bar"
errors_dup = errors.dup
- errors_dup[:bar] << 'omg'
+ errors_dup[:bar] << "omg"
assert_not_same errors_dup.messages, errors.messages
end
def test_has_key?
errors = ActiveModel::Errors.new(self)
- errors[:foo] << 'omg'
- assert_equal true, errors.has_key?(:foo), 'errors should have key :foo'
+ 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
errors = ActiveModel::Errors.new(self)
- assert_equal false, errors.has_key?(:name), 'errors should not have key :name'
+ assert_equal false, errors.has_key?(:name), "errors should not have key :name"
end
def test_key?
errors = ActiveModel::Errors.new(self)
- errors[:foo] << 'omg'
- assert_equal true, errors.key?(:foo), 'errors should have key :foo'
+ 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
errors = ActiveModel::Errors.new(self)
- assert_equal false, errors.key?(:name), 'errors should not have key :name'
+ assert_equal false, errors.key?(:name), "errors should not have key :name"
end
test "clear errors" do
@@ -76,25 +82,7 @@ class ErrorsTest < ActiveModel::TestCase
assert_equal 1, person.errors.count
person.errors.clear
- assert person.errors.empty?
- end
-
- test "get returns the errors for the provided key" do
- errors = ActiveModel::Errors.new(self)
- errors[:foo] << "omg"
-
- assert_deprecated do
- assert_equal ["omg"], errors.get(:foo)
- end
- end
-
- test "sets the error with the provided key" do
- errors = ActiveModel::Errors.new(self)
- assert_deprecated do
- errors.set(:foo, "omg")
- end
-
- assert_equal({ foo: "omg" }, errors.messages)
+ assert_empty person.errors
end
test "error access is indifferent" do
@@ -112,6 +100,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"
@@ -120,12 +116,27 @@ 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]
- assert person.errors.empty?
- assert person.errors.blank?
- assert !person.errors.include?(:foo)
+ assert_empty person.errors
+ assert_predicate person.errors, :blank?
+ assert_not_includes person.errors, :foo
+ end
+
+ test "include? does not add a key to messages hash" do
+ person = Person.new
+ person.errors.include?(:foo)
+
+ assert_not person.errors.messages.key?(:foo)
end
test "adding errors using conditionals with Person#validate!" do
@@ -135,14 +146,6 @@ class ErrorsTest < ActiveModel::TestCase
assert_equal ["cannot be nil"], person.errors[:name]
end
- test "assign error" do
- person = Person.new
- assert_deprecated do
- person.errors[:name] = 'should not be nil'
- end
- assert_equal ["should not be nil"], person.errors[:name]
- end
-
test "add an error message on a specific attribute" do
person = Person.new
person.errors.add(:name, "cannot be blank")
@@ -169,10 +172,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
@@ -181,6 +185,12 @@ class ErrorsTest < ActiveModel::TestCase
assert person.errors.added?(:name, :blank)
end
+ test "added? returns true when string attribute is used with a symbol message" do
+ person = Person.new
+ person.errors.add(:name, :blank)
+ assert person.errors.added?("name", :blank)
+ end
+
test "added? handles proc messages" do
person = Person.new
message = Proc.new { "cannot be blank" }
@@ -203,13 +213,36 @@ class ErrorsTest < ActiveModel::TestCase
test "added? returns false when no errors are present" do
person = Person.new
- assert !person.errors.added?(:name)
+ assert_not person.errors.added?(:name)
end
test "added? returns false when checking a nonexisting error and other errors are present for the given attribute" do
person = Person.new
person.errors.add(:name, "is invalid")
- assert !person.errors.added?(:name, "cannot be blank")
+ assert_not person.errors.added?(:name, "cannot be blank")
+ end
+
+ test "added? returns false when checking for an error, but not providing message arguments" do
+ person = Person.new
+ person.errors.add(:name, "cannot be blank")
+ assert_not person.errors.added?(:name)
+ end
+
+ test "added? returns false when checking for an error with an incorrect or missing option" do
+ person = Person.new
+ person.errors.add :name, :too_long, count: 25
+
+ assert person.errors.added? :name, :too_long, count: 25
+ assert_not person.errors.added? :name, :too_long, count: 24
+ assert_not person.errors.added? :name, :too_long
+ assert_not person.errors.added? :name, "is too long"
+ end
+
+ test "added? returns false when checking for an error by symbol and a different error with same message is present" do
+ I18n.backend.store_translations("en", errors: { attributes: { name: { wrong: "is wrong", used: "is wrong" } } })
+ person = Person.new
+ person.errors.add(:name, :wrong)
+ assert_not person.errors.added?(:name, :used)
end
test "size calculates the number of error messages" do
@@ -237,6 +270,16 @@ class ErrorsTest < ActiveModel::TestCase
assert_equal({ name: ["cannot be blank"] }, person.errors.to_hash)
end
+ test "to_hash returns a hash without default proc" do
+ person = Person.new
+ assert_nil person.errors.to_hash.default_proc
+ end
+
+ test "as_json returns a hash without default proc" do
+ person = Person.new
+ assert_nil person.errors.as_json.default_proc
+ end
+
test "full_messages creates a list of error messages with the attribute name included" do
person = Person.new
person.errors.add(:name, "cannot be blank")
@@ -244,7 +287,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")
@@ -256,6 +299,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
@@ -291,78 +335,12 @@ class ErrorsTest < ActiveModel::TestCase
test "generate_message works without i18n_scope" do
person = Person.new
- assert !Person.respond_to?(:i18n_scope)
+ assert_not_respond_to Person, :i18n_scope
assert_nothing_raised {
person.errors.generate_message(:name, :blank)
}
end
- test "add_on_empty generates message" do
- person = Person.new
- assert_called_with(person.errors, :generate_message, [:name, :empty, {}]) do
- assert_deprecated do
- person.errors.add_on_empty :name
- end
- end
- end
-
- test "add_on_empty generates message for multiple attributes" do
- person = Person.new
- expected_calls = [ [:name, :empty, {}], [:age, :empty, {}] ]
- assert_called_with(person.errors, :generate_message, expected_calls) do
- assert_deprecated do
- person.errors.add_on_empty [:name, :age]
- end
- end
- end
-
- test "add_on_empty generates message with custom default message" do
- person = Person.new
- assert_called_with(person.errors, :generate_message, [:name, :empty, { message: 'custom' }]) do
- assert_deprecated do
- person.errors.add_on_empty :name, message: 'custom'
- end
- end
- end
-
- test "add_on_empty generates message with empty string value" do
- person = Person.new
- person.name = ''
- assert_called_with(person.errors, :generate_message, [:name, :empty, {}]) do
- assert_deprecated do
- person.errors.add_on_empty :name
- end
- end
- end
-
- test "add_on_blank generates message" do
- person = Person.new
- assert_called_with(person.errors, :generate_message, [:name, :blank, {}]) do
- assert_deprecated do
- person.errors.add_on_blank :name
- end
- end
- end
-
- test "add_on_blank generates message for multiple attributes" do
- person = Person.new
- expected_calls = [ [:name, :blank, {}], [:age, :blank, {}] ]
- assert_called_with(person.errors, :generate_message, expected_calls) do
- assert_deprecated do
- person.errors.add_on_blank [:name, :age]
- end
- end
- end
-
- test "add_on_blank generates message with custom default message" do
- person = Person.new
- assert_called_with(person.errors, :generate_message, [:name, :blank, { message: 'custom' }]) do
- assert_deprecated do
- person.errors.add_on_blank :name, message: 'custom'
- end
- end
- end
-
test "details returns added error detail" do
person = Person.new
person.errors.add(:name, :invalid)
@@ -408,6 +386,57 @@ class ErrorsTest < ActiveModel::TestCase
assert_equal 1, person.errors.details.count
person.errors.clear
- assert person.errors.details.empty?
+ assert_empty person.errors.details
+ end
+
+ test "copy errors" do
+ errors = ActiveModel::Errors.new(Person.new)
+ errors.add(:name, :invalid)
+ person = Person.new
+ person.errors.copy!(errors)
+
+ assert_equal [:name], person.errors.messages.keys
+ assert_equal [:name], person.errors.details.keys
+ end
+
+ test "merge errors" do
+ errors = ActiveModel::Errors.new(Person.new)
+ errors.add(:name, :invalid)
+
+ person = Person.new
+ person.errors.add(:name, :blank)
+ person.errors.merge!(errors)
+
+ assert_equal({ name: ["can't be blank", "is invalid"] }, person.errors.messages)
+ assert_equal({ name: [{ error: :blank }, { error: :invalid }] }, person.errors.details)
+ end
+
+ test "errors are marshalable" do
+ errors = ActiveModel::Errors.new(Person.new)
+ errors.add(:name, :invalid)
+ serialized = Marshal.load(Marshal.dump(errors))
+
+ 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
+ --- !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/forbidden_attributes_protection_test.rb b/activemodel/test/cases/forbidden_attributes_protection_test.rb
index 3cb204a2c5..0fd0a2f8ee 100644
--- a/activemodel/test/cases/forbidden_attributes_protection_test.rb
+++ b/activemodel/test/cases/forbidden_attributes_protection_test.rb
@@ -1,13 +1,17 @@
-require 'cases/helper'
-require 'active_support/core_ext/hash/indifferent_access'
-require 'models/account'
+# frozen_string_literal: true
-class ProtectedParams < ActiveSupport::HashWithIndifferentAccess
+require "cases/helper"
+require "active_support/core_ext/hash/indifferent_access"
+require "models/account"
+
+class ProtectedParams
attr_accessor :permitted
alias :permitted? :permitted
+ delegate :keys, :key?, :has_key?, :empty?, to: :@parameters
+
def initialize(attributes)
- super(attributes)
+ @parameters = attributes
@permitted = false
end
@@ -15,22 +19,26 @@ class ProtectedParams < ActiveSupport::HashWithIndifferentAccess
@permitted = true
self
end
+
+ def to_h
+ @parameters
+ end
end
class ActiveModelMassUpdateProtectionTest < ActiveSupport::TestCase
test "forbidden attributes cannot be used for mass updating" do
- params = ProtectedParams.new({ "a" => "b" })
+ params = ProtectedParams.new("a" => "b")
assert_raises(ActiveModel::ForbiddenAttributesError) do
Account.new.sanitize_for_mass_assignment(params)
end
end
test "permitted attributes can be used for mass updating" do
- params = ProtectedParams.new({ "a" => "b" }).permit!
+ params = ProtectedParams.new("a" => "b").permit!
assert_equal({ "a" => "b" }, Account.new.sanitize_for_mass_assignment(params))
end
test "regular attributes should still be allowed" do
- assert_equal({ a: "b" }, Account.new.sanitize_for_mass_assignment(a: "b"))
+ assert_equal({ a: "b" }, Account.new.sanitize_for_mass_assignment(a: "b"))
end
end
diff --git a/activemodel/test/cases/helper.rb b/activemodel/test/cases/helper.rb
index c100646837..138b1d1bb9 100644
--- a/activemodel/test/cases/helper.rb
+++ b/activemodel/test/cases/helper.rb
@@ -1,8 +1,6 @@
-require File.expand_path('../../../../load_paths', __FILE__)
+# frozen_string_literal: true
-require 'config'
-require 'active_model'
-require 'active_support/core_ext/string/access'
+require "active_model"
# Show backtraces for deprecated behavior for quicker cleanup.
ActiveSupport::Deprecation.debug = true
@@ -10,20 +8,20 @@ ActiveSupport::Deprecation.debug = true
# Disable available locale checks to avoid warnings running the test suite.
I18n.enforce_available_locales = false
-require 'active_support/testing/autorun'
-require 'active_support/testing/method_call_assertions'
+require "active_support/testing/autorun"
+require "active_support/testing/method_call_assertions"
-require 'minitest/mock'
+class ActiveModel::TestCase < ActiveSupport::TestCase
+ include ActiveSupport::Testing::MethodCallAssertions
-# 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
+ private
+ # Skips the current run on Rubinius using Minitest::Assertions#skip
+ def rubinius_skip(message = "")
+ skip message if RUBY_ENGINE == "rbx"
+ end
-class ActiveModel::TestCase
- include ActiveSupport::Testing::MethodCallAssertions
+ # Skips the current run on JRuby using Minitest::Assertions#skip
+ def jruby_skip(message = "")
+ skip message if defined?(JRUBY_VERSION)
+ end
end
diff --git a/activemodel/test/cases/lint_test.rb b/activemodel/test/cases/lint_test.rb
index 8faf93c056..d62c80b71a 100644
--- a/activemodel/test/cases/lint_test.rb
+++ b/activemodel/test/cases/lint_test.rb
@@ -1,4 +1,6 @@
-require 'cases/helper'
+# frozen_string_literal: true
+
+require "cases/helper"
class LintTest < ActiveModel::TestCase
include ActiveModel::Lint::Tests
diff --git a/activemodel/test/cases/model_test.rb b/activemodel/test/cases/model_test.rb
index 3017f3541b..b24d7e3571 100644
--- a/activemodel/test/cases/model_test.rb
+++ b/activemodel/test/cases/model_test.rb
@@ -1,4 +1,6 @@
-require 'cases/helper'
+# frozen_string_literal: true
+
+require "cases/helper"
class ModelTest < ActiveModel::TestCase
include ActiveModel::Lint::Tests
@@ -9,7 +11,7 @@ class ModelTest < ActiveModel::TestCase
end
def initialize(*args)
- @attr ||= 'default value'
+ @attr ||= "default value"
super
end
end
@@ -50,7 +52,7 @@ class ModelTest < ActiveModel::TestCase
BasicModel.new()
BasicModel.new(nil)
BasicModel.new({})
- SimpleModel.new(attr: 'value')
+ SimpleModel.new(attr: "value")
end
end
@@ -61,17 +63,17 @@ class ModelTest < ActiveModel::TestCase
def test_mixin_inclusion_chain
object = BasicModel.new
- assert_equal 'default value', object.attr
+ assert_equal "default value", object.attr
end
def test_mixin_initializer_when_args_exist
- object = BasicModel.new(hello: 'world')
- assert_equal 'world', object.hello
+ object = BasicModel.new(hello: "world")
+ assert_equal "world", object.hello
end
def test_mixin_initializer_when_args_dont_exist
assert_raises(ActiveModel::UnknownAttributeError) do
- SimpleModel.new(hello: 'world')
+ SimpleModel.new(hello: "world")
end
end
end
diff --git a/activemodel/test/cases/naming_test.rb b/activemodel/test/cases/naming_test.rb
index 7b8287edbf..4693da434c 100644
--- a/activemodel/test/cases/naming_test.rb
+++ b/activemodel/test/cases/naming_test.rb
@@ -1,8 +1,10 @@
-require 'cases/helper'
-require 'models/contact'
-require 'models/sheep'
-require 'models/track_back'
-require 'models/blog_post'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/contact"
+require "models/sheep"
+require "models/track_back"
+require "models/blog_post"
class NamingTest < ActiveModel::TestCase
def setup
@@ -10,31 +12,31 @@ class NamingTest < ActiveModel::TestCase
end
def test_singular
- assert_equal 'post_track_back', @model_name.singular
+ assert_equal "post_track_back", @model_name.singular
end
def test_plural
- assert_equal 'post_track_backs', @model_name.plural
+ assert_equal "post_track_backs", @model_name.plural
end
def test_element
- assert_equal 'track_back', @model_name.element
+ assert_equal "track_back", @model_name.element
end
def test_collection
- assert_equal 'post/track_backs', @model_name.collection
+ assert_equal "post/track_backs", @model_name.collection
end
def test_human
- assert_equal 'Track back', @model_name.human
+ assert_equal "Track back", @model_name.human
end
def test_route_key
- assert_equal 'post_track_backs', @model_name.route_key
+ assert_equal "post_track_backs", @model_name.route_key
end
def test_param_key
- assert_equal 'post_track_back', @model_name.param_key
+ assert_equal "post_track_back", @model_name.param_key
end
def test_i18n_key
@@ -48,31 +50,31 @@ class NamingWithNamespacedModelInIsolatedNamespaceTest < ActiveModel::TestCase
end
def test_singular
- assert_equal 'blog_post', @model_name.singular
+ assert_equal "blog_post", @model_name.singular
end
def test_plural
- assert_equal 'blog_posts', @model_name.plural
+ assert_equal "blog_posts", @model_name.plural
end
def test_element
- assert_equal 'post', @model_name.element
+ assert_equal "post", @model_name.element
end
def test_collection
- assert_equal 'blog/posts', @model_name.collection
+ assert_equal "blog/posts", @model_name.collection
end
def test_human
- assert_equal 'Post', @model_name.human
+ assert_equal "Post", @model_name.human
end
def test_route_key
- assert_equal 'posts', @model_name.route_key
+ assert_equal "posts", @model_name.route_key
end
def test_param_key
- assert_equal 'post', @model_name.param_key
+ assert_equal "post", @model_name.param_key
end
def test_i18n_key
@@ -86,31 +88,31 @@ class NamingWithNamespacedModelInSharedNamespaceTest < ActiveModel::TestCase
end
def test_singular
- assert_equal 'blog_post', @model_name.singular
+ assert_equal "blog_post", @model_name.singular
end
def test_plural
- assert_equal 'blog_posts', @model_name.plural
+ assert_equal "blog_posts", @model_name.plural
end
def test_element
- assert_equal 'post', @model_name.element
+ assert_equal "post", @model_name.element
end
def test_collection
- assert_equal 'blog/posts', @model_name.collection
+ assert_equal "blog/posts", @model_name.collection
end
def test_human
- assert_equal 'Post', @model_name.human
+ assert_equal "Post", @model_name.human
end
def test_route_key
- assert_equal 'blog_posts', @model_name.route_key
+ assert_equal "blog_posts", @model_name.route_key
end
def test_param_key
- assert_equal 'blog_post', @model_name.param_key
+ assert_equal "blog_post", @model_name.param_key
end
def test_i18n_key
@@ -120,35 +122,35 @@ end
class NamingWithSuppliedModelNameTest < ActiveModel::TestCase
def setup
- @model_name = ActiveModel::Name.new(Blog::Post, nil, 'Article')
+ @model_name = ActiveModel::Name.new(Blog::Post, nil, "Article")
end
def test_singular
- assert_equal 'article', @model_name.singular
+ assert_equal "article", @model_name.singular
end
def test_plural
- assert_equal 'articles', @model_name.plural
+ assert_equal "articles", @model_name.plural
end
def test_element
- assert_equal 'article', @model_name.element
+ assert_equal "article", @model_name.element
end
def test_collection
- assert_equal 'articles', @model_name.collection
+ assert_equal "articles", @model_name.collection
end
def test_human
- assert_equal 'Article', @model_name.human
+ assert_equal "Article", @model_name.human
end
def test_route_key
- assert_equal 'articles', @model_name.route_key
+ assert_equal "articles", @model_name.route_key
end
def test_param_key
- assert_equal 'article', @model_name.param_key
+ assert_equal "article", @model_name.param_key
end
def test_i18n_key
@@ -162,31 +164,31 @@ class NamingUsingRelativeModelNameTest < ActiveModel::TestCase
end
def test_singular
- assert_equal 'blog_post', @model_name.singular
+ assert_equal "blog_post", @model_name.singular
end
def test_plural
- assert_equal 'blog_posts', @model_name.plural
+ assert_equal "blog_posts", @model_name.plural
end
def test_element
- assert_equal 'post', @model_name.element
+ assert_equal "post", @model_name.element
end
def test_collection
- assert_equal 'blog/posts', @model_name.collection
+ assert_equal "blog/posts", @model_name.collection
end
def test_human
- assert_equal 'Post', @model_name.human
+ assert_equal "Post", @model_name.human
end
def test_route_key
- assert_equal 'posts', @model_name.route_key
+ assert_equal "posts", @model_name.route_key
end
def test_param_key
- assert_equal 'post', @model_name.param_key
+ assert_equal "post", @model_name.param_key
end
def test_i18n_key
@@ -198,16 +200,16 @@ class NamingHelpersTest < ActiveModel::TestCase
def setup
@klass = Contact
@record = @klass.new
- @singular = 'contact'
- @plural = 'contacts'
+ @singular = "contact"
+ @plural = "contacts"
@uncountable = Sheep
- @singular_route_key = 'contact'
- @route_key = 'contacts'
- @param_key = 'contact'
+ @singular_route_key = "contact"
+ @route_key = "contacts"
+ @param_key = "contact"
end
def test_to_model_called_on_record
- assert_equal 'post_named_track_backs', plural(Post::TrackBack.new)
+ assert_equal "post_named_track_backs", plural(Post::TrackBack.new)
end
def test_singular
@@ -246,7 +248,7 @@ class NamingHelpersTest < ActiveModel::TestCase
def test_uncountable
assert uncountable?(@uncountable), "Expected 'sheep' to be uncountable"
- assert !uncountable?(@klass), "Expected 'contact' to be countable"
+ assert_not uncountable?(@klass), "Expected 'contact' to be countable"
end
def test_uncountable_route_key
diff --git a/activemodel/test/cases/railtie_test.rb b/activemodel/test/cases/railtie_test.rb
index 96b3b07e50..ab60285e2a 100644
--- a/activemodel/test/cases/railtie_test.rb
+++ b/activemodel/test/cases/railtie_test.rb
@@ -1,11 +1,13 @@
-require 'cases/helper'
-require 'active_support/testing/isolation'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "active_support/testing/isolation"
class RailtieTest < ActiveModel::TestCase
include ActiveSupport::Testing::Isolation
def setup
- require 'active_model/railtie'
+ require "active_model/railtie"
# Set a fake logger to avoid creating the log directory automatically
fake_logger = Logger.new(nil)
@@ -16,17 +18,37 @@ class RailtieTest < ActiveModel::TestCase
end
end
- test 'secure password min_cost is false in the development environment' do
- Rails.env = 'development'
+ test "secure password min_cost is false in the development environment" do
+ Rails.env = "development"
@app.initialize!
assert_equal false, ActiveModel::SecurePassword.min_cost
end
- test 'secure password min_cost is true in the test environment' do
- Rails.env = 'test'
+ test "secure password min_cost is true in the test environment" do
+ Rails.env = "test"
@app.initialize!
assert_equal true, ActiveModel::SecurePassword.min_cost
end
+
+ test "i18n full message defaults to false" do
+ @app.initialize!
+
+ assert_equal false, ActiveModel::Errors.i18n_full_message
+ end
+
+ test "i18n full message can be disabled" do
+ @app.config.active_model.i18n_full_message = false
+ @app.initialize!
+
+ assert_equal false, ActiveModel::Errors.i18n_full_message
+ end
+
+ test "i18n full message can be enabled" do
+ @app.config.active_model.i18n_full_message = true
+ @app.initialize!
+
+ assert_equal true, ActiveModel::Errors.i18n_full_message
+ end
end
diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb
index 6d56c8344a..9ef1148be8 100644
--- a/activemodel/test/cases/secure_password_test.rb
+++ b/activemodel/test/cases/secure_password_test.rb
@@ -1,6 +1,8 @@
-require 'cases/helper'
-require 'models/user'
-require 'models/visitor'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/user"
+require "models/visitor"
class SecurePasswordTest < ActiveModel::TestCase
setup do
@@ -13,7 +15,7 @@ class SecurePasswordTest < ActiveModel::TestCase
# Simulate loading an existing user from the DB
@existing_user = User.new
- @existing_user.password_digest = BCrypt::Password.create('password', cost: BCrypt::Engine::MIN_COST)
+ @existing_user.password_digest = BCrypt::Password.create("password", cost: BCrypt::Engine::MIN_COST)
end
teardown do
@@ -29,164 +31,171 @@ class SecurePasswordTest < ActiveModel::TestCase
end
test "create a new user with validations and valid password/confirmation" do
- @user.password = 'password'
- @user.password_confirmation = 'password'
+ @user.password = "password"
+ @user.password_confirmation = "password"
- assert @user.valid?(:create), 'user should be valid'
+ assert @user.valid?(:create), "user should be valid"
- @user.password = 'a' * 72
- @user.password_confirmation = 'a' * 72
+ @user.password = "a" * 72
+ @user.password_confirmation = "a" * 72
- assert @user.valid?(:create), 'user should be valid'
+ assert @user.valid?(:create), "user should be valid"
end
test "create a new user with validation and a spaces only password" do
- @user.password = ' ' * 72
- assert @user.valid?(:create), 'user should be valid'
+ @user.password = " " * 72
+ assert @user.valid?(:create), "user should be valid"
end
test "create a new user with validation and a blank password" do
- @user.password = ''
- assert !@user.valid?(:create), 'user should be invalid'
+ @user.password = ""
+ assert_not @user.valid?(:create), "user should be invalid"
assert_equal 1, @user.errors.count
assert_equal ["can't be blank"], @user.errors[:password]
end
test "create a new user with validation and a nil password" do
@user.password = nil
- assert !@user.valid?(:create), 'user should be invalid'
+ assert_not @user.valid?(:create), "user should be invalid"
assert_equal 1, @user.errors.count
assert_equal ["can't be blank"], @user.errors[:password]
end
- test 'create a new user with validation and password length greater than 72' do
- @user.password = 'a' * 73
- @user.password_confirmation = 'a' * 73
- assert !@user.valid?(:create), 'user should be invalid'
+ test "create a new user with validation and password length greater than 72" do
+ @user.password = "a" * 73
+ @user.password_confirmation = "a" * 73
+ assert_not @user.valid?(:create), "user should be invalid"
assert_equal 1, @user.errors.count
assert_equal ["is too long (maximum is 72 characters)"], @user.errors[:password]
end
test "create a new user with validation and a blank password confirmation" do
- @user.password = 'password'
- @user.password_confirmation = ''
- assert !@user.valid?(:create), 'user should be invalid'
+ @user.password = "password"
+ @user.password_confirmation = ""
+ assert_not @user.valid?(:create), "user should be invalid"
assert_equal 1, @user.errors.count
assert_equal ["doesn't match Password"], @user.errors[:password_confirmation]
end
test "create a new user with validation and a nil password confirmation" do
- @user.password = 'password'
+ @user.password = "password"
@user.password_confirmation = nil
- assert @user.valid?(:create), 'user should be valid'
+ assert @user.valid?(:create), "user should be valid"
end
test "create a new user with validation and an incorrect password confirmation" do
- @user.password = 'password'
- @user.password_confirmation = 'something else'
- assert !@user.valid?(:create), 'user should be invalid'
+ @user.password = "password"
+ @user.password_confirmation = "something else"
+ assert_not @user.valid?(:create), "user should be invalid"
assert_equal 1, @user.errors.count
assert_equal ["doesn't match Password"], @user.errors[:password_confirmation]
end
test "update an existing user with validation and no change in password" do
- assert @existing_user.valid?(:update), 'user should be valid'
+ assert @existing_user.valid?(:update), "user should be valid"
end
test "update an existing user with validations and valid password/confirmation" do
- @existing_user.password = 'password'
- @existing_user.password_confirmation = 'password'
+ @existing_user.password = "password"
+ @existing_user.password_confirmation = "password"
- assert @existing_user.valid?(:update), 'user should be valid'
+ assert @existing_user.valid?(:update), "user should be valid"
- @existing_user.password = 'a' * 72
- @existing_user.password_confirmation = 'a' * 72
+ @existing_user.password = "a" * 72
+ @existing_user.password_confirmation = "a" * 72
- assert @existing_user.valid?(:update), 'user should be valid'
+ assert @existing_user.valid?(:update), "user should be valid"
end
test "updating an existing user with validation and a blank password" do
- @existing_user.password = ''
- assert @existing_user.valid?(:update), 'user should be valid'
+ @existing_user.password = ""
+ assert @existing_user.valid?(:update), "user should be valid"
end
test "updating an existing user with validation and a spaces only password" do
- @user.password = ' ' * 72
- assert @user.valid?(:update), 'user should be valid'
+ @user.password = " " * 72
+ assert @user.valid?(:update), "user should be valid"
end
test "updating an existing user with validation and a blank password and password_confirmation" do
- @existing_user.password = ''
- @existing_user.password_confirmation = ''
- assert @existing_user.valid?(:update), 'user should be valid'
+ @existing_user.password = ""
+ @existing_user.password_confirmation = ""
+ assert @existing_user.valid?(:update), "user should be valid"
end
test "updating an existing user with validation and a nil password" do
@existing_user.password = nil
- assert !@existing_user.valid?(:update), 'user should be invalid'
+ assert_not @existing_user.valid?(:update), "user should be invalid"
assert_equal 1, @existing_user.errors.count
assert_equal ["can't be blank"], @existing_user.errors[:password]
end
- test 'updating an existing user with validation and password length greater than 72' do
- @existing_user.password = 'a' * 73
- @existing_user.password_confirmation = 'a' * 73
- assert !@existing_user.valid?(:update), 'user should be invalid'
+ test "updating an existing user with validation and password length greater than 72" do
+ @existing_user.password = "a" * 73
+ @existing_user.password_confirmation = "a" * 73
+ assert_not @existing_user.valid?(:update), "user should be invalid"
assert_equal 1, @existing_user.errors.count
assert_equal ["is too long (maximum is 72 characters)"], @existing_user.errors[:password]
end
test "updating an existing user with validation and a blank password confirmation" do
- @existing_user.password = 'password'
- @existing_user.password_confirmation = ''
- assert !@existing_user.valid?(:update), 'user should be invalid'
+ @existing_user.password = "password"
+ @existing_user.password_confirmation = ""
+ assert_not @existing_user.valid?(:update), "user should be invalid"
assert_equal 1, @existing_user.errors.count
assert_equal ["doesn't match Password"], @existing_user.errors[:password_confirmation]
end
test "updating an existing user with validation and a nil password confirmation" do
- @existing_user.password = 'password'
+ @existing_user.password = "password"
@existing_user.password_confirmation = nil
- assert @existing_user.valid?(:update), 'user should be valid'
+ assert @existing_user.valid?(:update), "user should be valid"
end
test "updating an existing user with validation and an incorrect password confirmation" do
- @existing_user.password = 'password'
- @existing_user.password_confirmation = 'something else'
- assert !@existing_user.valid?(:update), 'user should be invalid'
+ @existing_user.password = "password"
+ @existing_user.password_confirmation = "something else"
+ assert_not @existing_user.valid?(:update), "user should be invalid"
assert_equal 1, @existing_user.errors.count
assert_equal ["doesn't match Password"], @existing_user.errors[:password_confirmation]
end
test "updating an existing user with validation and a blank password digest" do
- @existing_user.password_digest = ''
- assert !@existing_user.valid?(:update), 'user should be invalid'
+ @existing_user.password_digest = ""
+ assert_not @existing_user.valid?(:update), "user should be invalid"
assert_equal 1, @existing_user.errors.count
assert_equal ["can't be blank"], @existing_user.errors[:password]
end
test "updating an existing user with validation and a nil password digest" do
@existing_user.password_digest = nil
- assert !@existing_user.valid?(:update), 'user should be invalid'
+ assert_not @existing_user.valid?(:update), "user should be invalid"
assert_equal 1, @existing_user.errors.count
assert_equal ["can't be blank"], @existing_user.errors[:password]
end
test "setting a blank password should not change an existing password" do
- @existing_user.password = ''
- assert @existing_user.password_digest == 'password'
+ @existing_user.password = ""
+ assert @existing_user.password_digest == "password"
end
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
@user.password = "secret"
+ @user.recovery_password = "42password"
+
+ assert_equal false, @user.authenticate("wrong")
+ assert_equal @user, @user.authenticate("secret")
+
+ assert_equal false, @user.authenticate_password("wrong")
+ assert_equal @user, @user.authenticate_password("secret")
- assert !@user.authenticate("wrong")
- assert @user.authenticate("secret")
+ assert_equal false, @user.authenticate_recovery_password("wrong")
+ assert_equal @user, @user.authenticate_recovery_password("42password")
end
test "Password digest cost defaults to bcrypt default cost when min_cost is false" do
diff --git a/activemodel/test/cases/serialization_test.rb b/activemodel/test/cases/serialization_test.rb
index 8d3165cd78..6826d2bbd1 100644
--- a/activemodel/test/cases/serialization_test.rb
+++ b/activemodel/test/cases/serialization_test.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'active_support/core_ext/object/instance_variables'
+require "active_support/core_ext/object/instance_variables"
class SerializationTest < ActiveModel::TestCase
class User
@@ -18,14 +20,14 @@ class SerializationTest < ActiveModel::TestCase
def method_missing(method_name, *args)
if method_name == :bar
- 'i_am_bar'
+ "i_am_bar"
else
super
end
end
def foo
- 'i_am_foo'
+ "i_am_foo"
end
end
@@ -40,43 +42,43 @@ class SerializationTest < ActiveModel::TestCase
end
setup do
- @user = User.new('David', 'david@example.com', 'male')
+ @user = User.new("David", "david@example.com", "male")
@user.address = Address.new
@user.address.street = "123 Lane"
@user.address.city = "Springfield"
@user.address.state = "CA"
@user.address.zip = 11111
- @user.friends = [User.new('Joe', 'joe@example.com', 'male'),
- User.new('Sue', 'sue@example.com', 'female')]
+ @user.friends = [User.new("Joe", "joe@example.com", "male"),
+ User.new("Sue", "sue@example.com", "female")]
end
def test_method_serializable_hash_should_work
- expected = {"name"=>"David", "gender"=>"male", "email"=>"david@example.com"}
+ expected = { "name" => "David", "gender" => "male", "email" => "david@example.com" }
assert_equal expected, @user.serializable_hash
end
def test_method_serializable_hash_should_work_with_only_option
- expected = {"name"=>"David"}
+ expected = { "name" => "David" }
assert_equal expected, @user.serializable_hash(only: [:name])
end
def test_method_serializable_hash_should_work_with_except_option
- expected = {"gender"=>"male", "email"=>"david@example.com"}
+ expected = { "gender" => "male", "email" => "david@example.com" }
assert_equal expected, @user.serializable_hash(except: [:name])
end
def test_method_serializable_hash_should_work_with_methods_option
- expected = {"name"=>"David", "gender"=>"male", "foo"=>"i_am_foo", "bar"=>"i_am_bar", "email"=>"david@example.com"}
+ expected = { "name" => "David", "gender" => "male", "foo" => "i_am_foo", "bar" => "i_am_bar", "email" => "david@example.com" }
assert_equal expected, @user.serializable_hash(methods: [:foo, :bar])
end
def test_method_serializable_hash_should_work_with_only_and_methods
- expected = {"foo"=>"i_am_foo", "bar"=>"i_am_bar"}
+ expected = { "foo" => "i_am_foo", "bar" => "i_am_bar" }
assert_equal expected, @user.serializable_hash(only: [], methods: [:foo, :bar])
end
def test_method_serializable_hash_should_work_with_except_and_methods
- expected = {"gender"=>"male", "foo"=>"i_am_foo", "bar"=>"i_am_bar"}
+ expected = { "gender" => "male", "foo" => "i_am_foo", "bar" => "i_am_bar" }
assert_equal expected, @user.serializable_hash(except: [:name, :email], methods: [:foo, :bar])
end
@@ -94,21 +96,21 @@ class SerializationTest < ActiveModel::TestCase
end
def test_include_option_with_singular_association
- expected = {"name"=>"David", "gender"=>"male", "email"=>"david@example.com",
- "address"=>{"street"=>"123 Lane", "city"=>"Springfield", "state"=>"CA", "zip"=>11111}}
+ expected = { "name" => "David", "gender" => "male", "email" => "david@example.com",
+ "address" => { "street" => "123 Lane", "city" => "Springfield", "state" => "CA", "zip" => 11111 } }
assert_equal expected, @user.serializable_hash(include: :address)
end
def test_include_option_with_plural_association
- expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David",
- "friends"=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male'},
- {"name"=>'Sue', "email"=>'sue@example.com', "gender"=>'female'}]}
+ expected = { "email" => "david@example.com", "gender" => "male", "name" => "David",
+ "friends" => [{ "name" => "Joe", "email" => "joe@example.com", "gender" => "male" },
+ { "name" => "Sue", "email" => "sue@example.com", "gender" => "female" }] }
assert_equal expected, @user.serializable_hash(include: :friends)
end
def test_include_option_with_empty_association
@user.friends = []
- expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David", "friends"=>[]}
+ expected = { "email" => "david@example.com", "gender" => "male", "name" => "David", "friends" => [] }
assert_equal expected, @user.serializable_hash(include: :friends)
end
@@ -124,52 +126,59 @@ class SerializationTest < ActiveModel::TestCase
def test_include_option_with_ary
@user.friends = FriendList.new(@user.friends)
- expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David",
- "friends"=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male'},
- {"name"=>'Sue', "email"=>'sue@example.com', "gender"=>'female'}]}
+ expected = { "email" => "david@example.com", "gender" => "male", "name" => "David",
+ "friends" => [{ "name" => "Joe", "email" => "joe@example.com", "gender" => "male" },
+ { "name" => "Sue", "email" => "sue@example.com", "gender" => "female" }] }
assert_equal expected, @user.serializable_hash(include: :friends)
end
def test_multiple_includes
- expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David",
- "address"=>{"street"=>"123 Lane", "city"=>"Springfield", "state"=>"CA", "zip"=>11111},
- "friends"=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male'},
- {"name"=>'Sue', "email"=>'sue@example.com', "gender"=>'female'}]}
+ expected = { "email" => "david@example.com", "gender" => "male", "name" => "David",
+ "address" => { "street" => "123 Lane", "city" => "Springfield", "state" => "CA", "zip" => 11111 },
+ "friends" => [{ "name" => "Joe", "email" => "joe@example.com", "gender" => "male" },
+ { "name" => "Sue", "email" => "sue@example.com", "gender" => "female" }] }
assert_equal expected, @user.serializable_hash(include: [:address, :friends])
end
def test_include_with_options
- expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David",
- "address"=>{"street"=>"123 Lane"}}
+ expected = { "email" => "david@example.com", "gender" => "male", "name" => "David",
+ "address" => { "street" => "123 Lane" } }
assert_equal expected, @user.serializable_hash(include: { address: { only: "street" } })
end
def test_nested_include
@user.friends.first.friends = [@user]
- expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David",
- "friends"=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male',
- "friends"=> [{"email"=>"david@example.com", "gender"=>"male", "name"=>"David"}]},
- {"name"=>'Sue', "email"=>'sue@example.com', "gender"=>'female', "friends"=> []}]}
+ expected = { "email" => "david@example.com", "gender" => "male", "name" => "David",
+ "friends" => [{ "name" => "Joe", "email" => "joe@example.com", "gender" => "male",
+ "friends" => [{ "email" => "david@example.com", "gender" => "male", "name" => "David" }] },
+ { "name" => "Sue", "email" => "sue@example.com", "gender" => "female", "friends" => [] }] }
assert_equal expected, @user.serializable_hash(include: { friends: { include: :friends } })
end
def test_only_include
- expected = {"name"=>"David", "friends" => [{"name" => "Joe"}, {"name" => "Sue"}]}
+ expected = { "name" => "David", "friends" => [{ "name" => "Joe" }, { "name" => "Sue" }] }
assert_equal expected, @user.serializable_hash(only: :name, include: { friends: { only: :name } })
end
def test_except_include
- expected = {"name"=>"David", "email"=>"david@example.com",
- "friends"=> [{"name" => 'Joe', "email" => 'joe@example.com'},
- {"name" => "Sue", "email" => 'sue@example.com'}]}
+ expected = { "name" => "David", "email" => "david@example.com",
+ "friends" => [{ "name" => "Joe", "email" => "joe@example.com" },
+ { "name" => "Sue", "email" => "sue@example.com" }] }
assert_equal expected, @user.serializable_hash(except: :gender, include: { friends: { except: :gender } })
end
def test_multiple_includes_with_options
- expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David",
- "address"=>{"street"=>"123 Lane"},
- "friends"=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male'},
- {"name"=>'Sue', "email"=>'sue@example.com', "gender"=>'female'}]}
- assert_equal expected, @user.serializable_hash(include: [{ address: {only: "street" } }, :friends])
+ expected = { "email" => "david@example.com", "gender" => "male", "name" => "David",
+ "address" => { "street" => "123 Lane" },
+ "friends" => [{ "name" => "Joe", "email" => "joe@example.com", "gender" => "male" },
+ { "name" => "Sue", "email" => "sue@example.com", "gender" => "female" }] }
+ assert_equal expected, @user.serializable_hash(include: [{ address: { only: "street" } }, :friends])
+ end
+
+ def test_all_includes_with_options
+ expected = { "email" => "david@example.com", "gender" => "male", "name" => "David",
+ "address" => { "street" => "123 Lane" },
+ "friends" => [{ "name" => "Joe" }, { "name" => "Sue" }] }
+ assert_equal expected, @user.serializable_hash(include: [address: { only: "street" }, friends: { only: "name" }])
end
end
diff --git a/activemodel/test/cases/serializers/json_serialization_test.rb b/activemodel/test/cases/serializers/json_serialization_test.rb
index d765a47636..625e0a427a 100644
--- a/activemodel/test/cases/serializers/json_serialization_test.rb
+++ b/activemodel/test/cases/serializers/json_serialization_test.rb
@@ -1,15 +1,17 @@
-require 'cases/helper'
-require 'models/contact'
-require 'active_support/core_ext/object/instance_variables'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/contact"
+require "active_support/core_ext/object/instance_variables"
class JsonSerializationTest < ActiveModel::TestCase
def setup
@contact = Contact.new
- @contact.name = 'Konata Izumi'
+ @contact.name = "Konata Izumi"
@contact.age = 16
@contact.created_at = Time.utc(2006, 8, 1)
@contact.awesome = true
- @contact.preferences = { 'shows' => 'anime' }
+ @contact.preferences = { "shows" => "anime" }
end
test "should not include root in json (class method)" do
@@ -18,7 +20,7 @@ class JsonSerializationTest < ActiveModel::TestCase
assert_no_match %r{^\{"contact":\{}, json
assert_match %r{"name":"Konata Izumi"}, json
assert_match %r{"age":16}, json
- assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}))
+ assert_includes json, %("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))})
assert_match %r{"awesome":true}, json
assert_match %r{"preferences":\{"shows":"anime"\}}, json
end
@@ -32,7 +34,7 @@ class JsonSerializationTest < ActiveModel::TestCase
assert_match %r{^\{"contact":\{}, json
assert_match %r{"name":"Konata Izumi"}, json
assert_match %r{"age":16}, json
- assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}))
+ assert_includes json, %("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))})
assert_match %r{"awesome":true}, json
assert_match %r{"preferences":\{"shows":"anime"\}}, json
ensure
@@ -53,12 +55,12 @@ class JsonSerializationTest < ActiveModel::TestCase
end
test "should include custom root in json" do
- json = @contact.to_json(root: 'json_contact')
+ json = @contact.to_json(root: "json_contact")
assert_match %r{^\{"json_contact":\{}, json
assert_match %r{"name":"Konata Izumi"}, json
assert_match %r{"age":16}, json
- assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}))
+ assert_includes json, %("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))})
assert_match %r{"awesome":true}, json
assert_match %r{"preferences":\{"shows":"anime"\}}, json
end
@@ -68,7 +70,7 @@ class JsonSerializationTest < ActiveModel::TestCase
assert_match %r{"name":"Konata Izumi"}, json
assert_match %r{"age":16}, json
- assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}))
+ assert_includes json, %("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))})
assert_match %r{"awesome":true}, json
assert_match %r{"preferences":\{"shows":"anime"\}}, json
end
@@ -79,7 +81,7 @@ class JsonSerializationTest < ActiveModel::TestCase
assert_match %r{"name":"Konata Izumi"}, json
assert_match %r{"age":16}, json
assert_no_match %r{"awesome":true}, json
- assert !json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}))
+ assert_not_includes json, %("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))})
assert_no_match %r{"preferences":\{"shows":"anime"\}}, json
end
@@ -89,7 +91,7 @@ class JsonSerializationTest < ActiveModel::TestCase
assert_no_match %r{"name":"Konata Izumi"}, json
assert_no_match %r{"age":16}, json
assert_match %r{"awesome":true}, json
- assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}))
+ assert_includes json, %("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))})
assert_match %r{"preferences":\{"shows":"anime"\}}, json
end
@@ -127,6 +129,10 @@ class JsonSerializationTest < ActiveModel::TestCase
assert_equal :name, options[:except]
end
+ test "as_json should serialize timestamps" do
+ assert_equal "2006-08-01T00:00:00.000Z", @contact.as_json["created_at"]
+ end
+
test "as_json should return a hash if include_root_in_json is true" do
begin
original_include_root_in_json = Contact.include_root_in_json
@@ -134,9 +140,9 @@ class JsonSerializationTest < ActiveModel::TestCase
json = @contact.as_json
assert_kind_of Hash, json
- assert_kind_of Hash, json['contact']
+ assert_kind_of Hash, json["contact"]
%w(name age created_at awesome preferences).each do |field|
- assert_equal @contact.send(field), json['contact'][field]
+ assert_equal @contact.send(field).as_json, json["contact"][field]
end
ensure
Contact.include_root_in_json = original_include_root_in_json
diff --git a/activemodel/test/cases/serializers/xml_serialization_test.rb b/activemodel/test/cases/serializers/xml_serialization_test.rb
deleted file mode 100644
index 37faf6cef8..0000000000
--- a/activemodel/test/cases/serializers/xml_serialization_test.rb
+++ /dev/null
@@ -1,251 +0,0 @@
-require 'cases/helper'
-require 'models/contact'
-require 'active_support/core_ext/object/instance_variables'
-require 'ostruct'
-require 'yaml'
-
-module Admin
- class Contact < ::Contact
- end
-end
-
-class Customer < Struct.new(:name)
-end
-
-class Address
- include ActiveModel::Serializers::Xml
-
- attr_accessor :street, :city, :state, :zip, :apt_number
-
- def attributes
- instance_values
- end
-end
-
-class SerializableContact < Contact
- def serializable_hash(options={})
- super(options.merge(only: [:name, :age]))
- end
-end
-
-class XmlSerializationTest < ActiveModel::TestCase
- def setup
- @contact = Contact.new
- @contact.name = 'aaron stack'
- @contact.age = 25
- @contact.created_at = Time.utc(2006, 8, 1)
- @contact.awesome = false
- customer = Customer.new
- customer.name = "John"
- @contact.preferences = customer
- @contact.address = Address.new
- @contact.address.city = "Springfield"
- @contact.address.apt_number = 35
- @contact.friends = [Contact.new, Contact.new]
- @contact.contact = SerializableContact.new
- end
-
- test "should serialize default root" do
- xml = @contact.to_xml
- assert_match %r{^<contact>}, xml
- assert_match %r{</contact>$}, xml
- end
-
- test "should serialize namespaced root" do
- xml = Admin::Contact.new(@contact.attributes).to_xml
- assert_match %r{^<contact>}, xml
- assert_match %r{</contact>$}, xml
- end
-
- test "should serialize default root with namespace" do
- xml = @contact.to_xml namespace: "http://xml.rubyonrails.org/contact"
- assert_match %r{^<contact xmlns="http://xml.rubyonrails.org/contact">}, xml
- assert_match %r{</contact>$}, xml
- end
-
- test "should serialize custom root" do
- xml = @contact.to_xml root: 'xml_contact'
- assert_match %r{^<xml-contact>}, xml
- assert_match %r{</xml-contact>$}, xml
- end
-
- test "should allow undasherized tags" do
- xml = @contact.to_xml root: 'xml_contact', dasherize: false
- assert_match %r{^<xml_contact>}, xml
- assert_match %r{</xml_contact>$}, xml
- assert_match %r{<created_at}, xml
- end
-
- test "should allow camelized tags" do
- xml = @contact.to_xml root: 'xml_contact', camelize: true
- assert_match %r{^<XmlContact>}, xml
- assert_match %r{</XmlContact>$}, xml
- assert_match %r{<CreatedAt}, xml
- end
-
- test "should allow lower-camelized tags" do
- xml = @contact.to_xml root: 'xml_contact', camelize: :lower
- assert_match %r{^<xmlContact>}, xml
- assert_match %r{</xmlContact>$}, xml
- assert_match %r{<createdAt}, xml
- end
-
- test "should use serializable hash" do
- @contact = SerializableContact.new
- @contact.name = 'aaron stack'
- @contact.age = 25
-
- xml = @contact.to_xml
- assert_match %r{<name>aaron stack</name>}, xml
- assert_match %r{<age type="integer">25</age>}, xml
- assert_no_match %r{<awesome>}, xml
- end
-
- test "should allow skipped types" do
- xml = @contact.to_xml skip_types: true
- assert_match %r{<age>25</age>}, xml
- end
-
- test "should include yielded additions" do
- xml_output = @contact.to_xml do |xml|
- xml.creator "David"
- end
- assert_match %r{<creator>David</creator>}, xml_output
- end
-
- test "should serialize string" do
- assert_match %r{<name>aaron stack</name>}, @contact.to_xml
- end
-
- test "should serialize nil" do
- assert_match %r{<pseudonyms nil="true"/>}, @contact.to_xml(methods: :pseudonyms)
- end
-
- test "should serialize integer" do
- assert_match %r{<age type="integer">25</age>}, @contact.to_xml
- end
-
- test "should serialize datetime" do
- assert_match %r{<created-at type="dateTime">2006-08-01T00:00:00Z</created-at>}, @contact.to_xml
- end
-
- test "should serialize boolean" do
- assert_match %r{<awesome type="boolean">false</awesome>}, @contact.to_xml
- end
-
- test "should serialize array" do
- assert_match %r{<social type="array">\s*<social>twitter</social>\s*<social>github</social>\s*</social>}, @contact.to_xml(methods: :social)
- end
-
- test "should serialize hash" do
- assert_match %r{<network>\s*<git type="symbol">github</git>\s*</network>}, @contact.to_xml(methods: :network)
- end
-
- test "should serialize yaml" do
- assert_match %r{<preferences type="yaml">--- !ruby/struct:Customer(\s*)\nname: John\n</preferences>}, @contact.to_xml
- end
-
- test "should call proc on object" do
- proc = Proc.new { |options| options[:builder].tag!('nationality', 'unknown') }
- xml = @contact.to_xml(procs: [ proc ])
- assert_match %r{<nationality>unknown</nationality>}, xml
- end
-
- test "should supply serializable to second proc argument" do
- proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) }
- xml = @contact.to_xml(procs: [ proc ])
- assert_match %r{<name-reverse>kcats noraa</name-reverse>}, xml
- end
-
- test "should serialize string correctly when type passed" do
- xml = @contact.to_xml type: 'Contact'
- assert_match %r{<contact type="Contact">}, xml
- assert_match %r{<name>aaron stack</name>}, xml
- end
-
- test "include option with singular association" do
- xml = @contact.to_xml include: :address, indent: 0
- assert xml.include?(@contact.address.to_xml(indent: 0, skip_instruct: true))
- end
-
- test "include option with plural association" do
- xml = @contact.to_xml include: :friends, indent: 0
- assert_match %r{<friends type="array">}, xml
- assert_match %r{<friend type="Contact">}, xml
- end
-
- class FriendList
- def initialize(friends)
- @friends = friends
- end
-
- def to_ary
- @friends
- end
- end
-
- test "include option with ary" do
- @contact.friends = FriendList.new(@contact.friends)
- xml = @contact.to_xml include: :friends, indent: 0
- assert_match %r{<friends type="array">}, xml
- assert_match %r{<friend type="Contact">}, xml
- end
-
- test "multiple includes" do
- xml = @contact.to_xml indent: 0, skip_instruct: true, include: [ :address, :friends ]
- assert xml.include?(@contact.address.to_xml(indent: 0, skip_instruct: true))
- assert_match %r{<friends type="array">}, xml
- assert_match %r{<friend type="Contact">}, xml
- end
-
- test "include with options" do
- xml = @contact.to_xml indent: 0, skip_instruct: true, include: { address: { only: :city } }
- assert xml.include?(%(><address><city>Springfield</city></address>))
- end
-
- test "propagates skip_types option to included associations" do
- xml = @contact.to_xml include: :friends, indent: 0, skip_types: true
- assert_match %r{<friends>}, xml
- assert_match %r{<friend>}, xml
- end
-
- test "propagates skip-types option to included associations and attributes" do
- xml = @contact.to_xml skip_types: true, include: :address, indent: 0
- assert_match %r{<address>}, xml
- assert_match %r{<apt-number>}, xml
- end
-
- test "propagates camelize option to included associations and attributes" do
- xml = @contact.to_xml camelize: true, include: :address, indent: 0
- assert_match %r{<Address>}, xml
- assert_match %r{<AptNumber type="integer">}, xml
- end
-
- test "propagates dasherize option to included associations and attributes" do
- xml = @contact.to_xml dasherize: false, include: :address, indent: 0
- assert_match %r{<apt_number type="integer">}, xml
- end
-
- test "don't propagate skip_types if skip_types is defined at the included association level" do
- xml = @contact.to_xml skip_types: true, include: { address: { skip_types: false } }, indent: 0
- assert_match %r{<address>}, xml
- assert_match %r{<apt-number type="integer">}, xml
- end
-
- test "don't propagate camelize if camelize is defined at the included association level" do
- xml = @contact.to_xml camelize: true, include: { address: { camelize: false } }, indent: 0
- assert_match %r{<address>}, xml
- assert_match %r{<apt-number type="integer">}, xml
- end
-
- test "don't propagate dasherize if dasherize is defined at the included association level" do
- xml = @contact.to_xml dasherize: false, include: { address: { dasherize: true } }, indent: 0
- assert_match %r{<address>}, xml
- assert_match %r{<apt-number type="integer">}, xml
- end
-
- test "association with sti" do
- xml = @contact.to_xml(include: :contact)
- assert xml.include?(%(<contact type="SerializableContact">))
- end
-end
diff --git a/activemodel/test/cases/translation_test.rb b/activemodel/test/cases/translation_test.rb
index cedc812ec7..cd75afec9e 100644
--- a/activemodel/test/cases/translation_test.rb
+++ b/activemodel/test/cases/translation_test.rb
@@ -1,8 +1,9 @@
-require 'cases/helper'
-require 'models/person'
+# frozen_string_literal: true
-class ActiveModelI18nTests < ActiveModel::TestCase
+require "cases/helper"
+require "models/person"
+class ActiveModelI18nTests < ActiveModel::TestCase
def setup
I18n.backend = I18n::Backend::Simple.new
end
@@ -12,97 +13,101 @@ class ActiveModelI18nTests < ActiveModel::TestCase
end
def test_translated_model_attributes
- I18n.backend.store_translations 'en', activemodel: { attributes: { person: { name: 'person name attribute' } } }
- assert_equal 'person name attribute', Person.human_attribute_name('name')
+ I18n.backend.store_translations "en", activemodel: { attributes: { person: { name: "person name attribute" } } }
+ assert_equal "person name attribute", Person.human_attribute_name("name")
end
def test_translated_model_attributes_with_default
- I18n.backend.store_translations 'en', attributes: { name: 'name default attribute' }
- assert_equal 'name default attribute', Person.human_attribute_name('name')
+ I18n.backend.store_translations "en", attributes: { name: "name default attribute" }
+ assert_equal "name default attribute", Person.human_attribute_name("name")
end
def test_translated_model_attributes_using_default_option
- assert_equal 'name default attribute', Person.human_attribute_name('name', default: "name default attribute")
+ assert_equal "name default attribute", Person.human_attribute_name("name", default: "name default attribute")
end
def test_translated_model_attributes_using_default_option_as_symbol
- I18n.backend.store_translations 'en', default_name: 'name default attribute'
- assert_equal 'name default attribute', Person.human_attribute_name('name', default: :default_name)
+ I18n.backend.store_translations "en", default_name: "name default attribute"
+ assert_equal "name default attribute", Person.human_attribute_name("name", default: :default_name)
end
def test_translated_model_attributes_falling_back_to_default
- assert_equal 'Name', Person.human_attribute_name('name')
+ assert_equal "Name", Person.human_attribute_name("name")
end
def test_translated_model_attributes_using_default_option_as_symbol_and_falling_back_to_default
- assert_equal 'Name', Person.human_attribute_name('name', default: :default_name)
+ assert_equal "Name", Person.human_attribute_name("name", default: :default_name)
end
def test_translated_model_attributes_with_symbols
- I18n.backend.store_translations 'en', activemodel: { attributes: { person: { name: 'person name attribute'} } }
- assert_equal 'person name attribute', Person.human_attribute_name(:name)
+ I18n.backend.store_translations "en", activemodel: { attributes: { person: { name: "person name attribute" } } }
+ assert_equal "person name attribute", Person.human_attribute_name(:name)
end
def test_translated_model_attributes_with_ancestor
- I18n.backend.store_translations 'en', activemodel: { attributes: { child: { name: 'child name attribute'} } }
- assert_equal 'child name attribute', Child.human_attribute_name('name')
+ I18n.backend.store_translations "en", activemodel: { attributes: { child: { name: "child name attribute" } } }
+ assert_equal "child name attribute", Child.human_attribute_name("name")
end
def test_translated_model_attributes_with_ancestors_fallback
- I18n.backend.store_translations 'en', activemodel: { attributes: { person: { name: 'person name attribute'} } }
- assert_equal 'person name attribute', Child.human_attribute_name('name')
+ I18n.backend.store_translations "en", activemodel: { attributes: { person: { name: "person name attribute" } } }
+ assert_equal "person name attribute", Child.human_attribute_name("name")
end
def test_translated_model_attributes_with_attribute_matching_namespaced_model_name
- I18n.backend.store_translations 'en', activemodel: { attributes: {
- person: { gender: 'person gender'},
- :"person/gender" => { attribute: 'person gender attribute' }
+ I18n.backend.store_translations "en", activemodel: { attributes: {
+ person: { gender: "person gender" },
+ "person/gender": { attribute: "person gender attribute" }
} }
- assert_equal 'person gender', Person.human_attribute_name('gender')
- assert_equal 'person gender attribute', Person::Gender.human_attribute_name('attribute')
+ assert_equal "person gender", Person.human_attribute_name("gender")
+ assert_equal "person gender attribute", Person::Gender.human_attribute_name("attribute")
end
def test_translated_deeply_nested_model_attributes
- I18n.backend.store_translations 'en', activemodel: { attributes: { :"person/contacts/addresses" => { street: 'Deeply Nested Address Street' } } }
- assert_equal 'Deeply Nested Address Street', Person.human_attribute_name('contacts.addresses.street')
+ I18n.backend.store_translations "en", activemodel: { attributes: { "person/contacts/addresses": { street: "Deeply Nested Address Street" } } }
+ assert_equal "Deeply Nested Address Street", Person.human_attribute_name("contacts.addresses.street")
end
def test_translated_nested_model_attributes
- I18n.backend.store_translations 'en', activemodel: { attributes: { :"person/addresses" => { street: 'Person Address Street' } } }
- assert_equal 'Person Address Street', Person.human_attribute_name('addresses.street')
+ I18n.backend.store_translations "en", activemodel: { attributes: { "person/addresses": { street: "Person Address Street" } } }
+ assert_equal "Person Address Street", Person.human_attribute_name("addresses.street")
end
def test_translated_nested_model_attributes_with_namespace_fallback
- I18n.backend.store_translations 'en', activemodel: { attributes: { addresses: { street: 'Cool Address Street' } } }
- assert_equal 'Cool Address Street', Person.human_attribute_name('addresses.street')
+ I18n.backend.store_translations "en", activemodel: { attributes: { addresses: { street: "Cool Address Street" } } }
+ assert_equal "Cool Address Street", Person.human_attribute_name("addresses.street")
end
def test_translated_model_names
- I18n.backend.store_translations 'en', activemodel: { models: { person: 'person model' } }
- assert_equal 'person model', Person.model_name.human
+ I18n.backend.store_translations "en", activemodel: { models: { person: "person model" } }
+ assert_equal "person model", Person.model_name.human
end
def test_translated_model_names_with_sti
- I18n.backend.store_translations 'en', activemodel: { models: { child: 'child model' } }
- assert_equal 'child model', Child.model_name.human
+ I18n.backend.store_translations "en", activemodel: { models: { child: "child model" } }
+ assert_equal "child model", Child.model_name.human
+ end
+
+ def test_translated_model_with_namespace
+ I18n.backend.store_translations "en", activemodel: { models: { 'person/gender': "gender model" } }
+ assert_equal "gender model", Person::Gender.model_name.human
end
def test_translated_model_names_with_ancestors_fallback
- I18n.backend.store_translations 'en', activemodel: { models: { person: 'person model' } }
- assert_equal 'person model', Child.model_name.human
+ I18n.backend.store_translations "en", activemodel: { models: { person: "person model" } }
+ assert_equal "person model", Child.model_name.human
end
def test_human_does_not_modify_options
- options = { default: 'person model' }
+ options = { default: "person model" }
Person.model_name.human(options)
- assert_equal({ default: 'person model' }, options)
+ assert_equal({ default: "person model" }, options)
end
def test_human_attribute_name_does_not_modify_options
- options = { default: 'Cool gender' }
- Person.human_attribute_name('gender', options)
- assert_equal({ default: 'Cool gender' }, options)
+ options = { default: "Cool gender" }
+ Person.human_attribute_name("gender", options)
+ assert_equal({ default: "Cool gender" }, options)
end
end
-
diff --git a/activemodel/test/cases/type/big_integer_test.rb b/activemodel/test/cases/type/big_integer_test.rb
new file mode 100644
index 0000000000..0fa0200df4
--- /dev/null
+++ b/activemodel/test/cases/type/big_integer_test.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+
+module ActiveModel
+ module Type
+ class BigIntegerTest < ActiveModel::TestCase
+ def test_type_cast_big_integer
+ type = Type::BigInteger.new
+ assert_equal 1, type.cast(1)
+ assert_equal 1, type.cast("1")
+ end
+
+ def test_small_values
+ type = Type::BigInteger.new
+ assert_equal(-9999999999999999999999999999999, type.serialize(-9999999999999999999999999999999))
+ end
+
+ def test_large_values
+ type = Type::BigInteger.new
+ assert_equal 9999999999999999999999999999999, type.serialize(9999999999999999999999999999999)
+ end
+ end
+ end
+end
diff --git a/activemodel/test/cases/type/binary_test.rb b/activemodel/test/cases/type/binary_test.rb
new file mode 100644
index 0000000000..3221a73e49
--- /dev/null
+++ b/activemodel/test/cases/type/binary_test.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+
+module ActiveModel
+ module Type
+ class BinaryTest < ActiveModel::TestCase
+ def test_type_cast_binary
+ type = Type::Binary.new
+ assert_nil type.cast(nil)
+ assert_equal "1", type.cast("1")
+ assert_equal 1, type.cast(1)
+ end
+ end
+ end
+end
diff --git a/activemodel/test/cases/type/boolean_test.rb b/activemodel/test/cases/type/boolean_test.rb
new file mode 100644
index 0000000000..2de0f53640
--- /dev/null
+++ b/activemodel/test/cases/type/boolean_test.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+
+module ActiveModel
+ module Type
+ class BooleanTest < ActiveModel::TestCase
+ def test_type_cast_boolean
+ type = Type::Boolean.new
+ assert_predicate type.cast(""), :nil?
+ assert_predicate type.cast(nil), :nil?
+
+ assert type.cast(true)
+ assert type.cast(1)
+ assert type.cast("1")
+ assert type.cast("t")
+ assert type.cast("T")
+ assert type.cast("true")
+ assert type.cast("TRUE")
+ assert type.cast("on")
+ assert type.cast("ON")
+ assert type.cast(" ")
+ assert type.cast("\u3000\r\n")
+ assert type.cast("\u0000")
+ assert type.cast("SOMETHING RANDOM")
+
+ # explicitly check for false vs nil
+ assert_equal false, type.cast(false)
+ assert_equal false, type.cast(0)
+ assert_equal false, type.cast("0")
+ assert_equal false, type.cast("f")
+ assert_equal false, type.cast("F")
+ assert_equal false, type.cast("false")
+ assert_equal false, type.cast("FALSE")
+ assert_equal false, type.cast("off")
+ assert_equal false, type.cast("OFF")
+ end
+ end
+ end
+end
diff --git a/activemodel/test/cases/type/date_test.rb b/activemodel/test/cases/type/date_test.rb
new file mode 100644
index 0000000000..e8cf178612
--- /dev/null
+++ b/activemodel/test/cases/type/date_test.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+
+module ActiveModel
+ module Type
+ class DateTest < ActiveModel::TestCase
+ def test_type_cast_date
+ type = Type::Date.new
+ 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")
+ end
+ end
+ end
+end
diff --git a/activemodel/test/cases/type/date_time_test.rb b/activemodel/test/cases/type/date_time_test.rb
new file mode 100644
index 0000000000..74b47d1b4d
--- /dev/null
+++ b/activemodel/test/cases/type/date_time_test.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+
+module ActiveModel
+ module Type
+ class DateTimeTest < ActiveModel::TestCase
+ def test_type_cast_datetime_and_timestamp
+ type = Type::DateTime.new
+ 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")
+ end
+
+ def test_string_to_time_with_timezone
+ ["UTC", "US/Eastern"].each do |zone|
+ with_timezone_config default: zone do
+ type = Type::DateTime.new
+ assert_equal ::Time.utc(2013, 9, 4, 0, 0, 0), type.cast("Wed, 04 Sep 2013 03:00:00 EAT")
+ end
+ end
+ end
+
+ def test_hash_to_time
+ type = Type::DateTime.new
+ assert_equal ::Time.utc(2018, 10, 15, 0, 0, 0), type.cast(1 => 2018, 2 => 10, 3 => 15)
+ end
+
+ def test_hash_with_wrong_keys
+ type = Type::DateTime.new
+ error = assert_raises(ArgumentError) { type.cast(a: 1) }
+ assert_equal "Provided hash {:a=>1} doesn't contain necessary keys: [1, 2, 3]", error.message
+ end
+
+ private
+
+ def with_timezone_config(default:)
+ old_zone_default = ::Time.zone_default
+ ::Time.zone_default = ::Time.find_zone(default)
+ yield
+ ensure
+ ::Time.zone_default = old_zone_default
+ end
+ end
+ end
+end
diff --git a/activemodel/test/cases/type/decimal_test.rb b/activemodel/test/cases/type/decimal_test.rb
new file mode 100644
index 0000000000..be60c4f7fa
--- /dev/null
+++ b/activemodel/test/cases/type/decimal_test.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+
+module ActiveModel
+ module Type
+ class DecimalTest < ActiveModel::TestCase
+ def test_type_cast_decimal
+ type = Decimal.new
+ assert_equal BigDecimal("0"), type.cast(BigDecimal("0"))
+ assert_equal BigDecimal("123"), type.cast(123.0)
+ assert_equal BigDecimal("1"), type.cast(:"1")
+ end
+
+ def test_type_cast_decimal_from_invalid_string
+ type = Decimal.new
+ assert_nil type.cast("")
+ assert_equal BigDecimal("1"), type.cast("1ignore")
+ assert_equal BigDecimal("0"), type.cast("bad1")
+ assert_equal BigDecimal("0"), type.cast("bad")
+ end
+
+ def test_type_cast_decimal_from_float_with_large_precision
+ type = Decimal.new(precision: ::Float::DIG + 2)
+ assert_equal BigDecimal("123.0"), type.cast(123.0)
+ end
+
+ def test_type_cast_from_float_with_unspecified_precision
+ type = Decimal.new
+ assert_equal 22.68.to_d, type.cast(22.68)
+ end
+
+ def test_type_cast_decimal_from_rational_with_precision
+ type = Decimal.new(precision: 2)
+ assert_equal BigDecimal("0.33"), type.cast(Rational(1, 3))
+ end
+
+ def test_type_cast_decimal_from_rational_with_precision_and_scale
+ type = Decimal.new(precision: 4, scale: 2)
+ assert_equal BigDecimal("0.33"), type.cast(Rational(1, 3))
+ end
+
+ def test_type_cast_decimal_from_rational_without_precision_defaults_to_18_36
+ type = Decimal.new
+ assert_equal BigDecimal("0.333333333333333333E0"), type.cast(Rational(1, 3))
+ end
+
+ def test_type_cast_decimal_from_object_responding_to_d
+ value = Object.new
+ def value.to_d
+ BigDecimal("1")
+ end
+ type = Decimal.new
+ assert_equal BigDecimal("1"), type.cast(value)
+ end
+
+ def test_changed?
+ type = Decimal.new
+
+ assert type.changed?(0.0, 0, "wibble")
+ assert type.changed?(5.0, 0, "wibble")
+ assert_not type.changed?(5.0, 5.0, "5.0wibble")
+ assert_not type.changed?(5.0, 5.0, "5.0")
+ assert_not type.changed?(-5.0, -5.0, "-5.0")
+ assert_not type.changed?(5.0, 5.0, "0.5e+1")
+ end
+
+ def test_scale_is_applied_before_precision_to_prevent_rounding_errors
+ type = Decimal.new(precision: 5, scale: 3)
+
+ assert_equal BigDecimal("1.250"), type.cast(1.250473853637869)
+ assert_equal BigDecimal("1.250"), type.cast("1.250473853637869")
+ end
+ end
+ end
+end
diff --git a/activemodel/test/cases/type/float_test.rb b/activemodel/test/cases/type/float_test.rb
new file mode 100644
index 0000000000..230a8dda32
--- /dev/null
+++ b/activemodel/test/cases/type/float_test.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+
+module ActiveModel
+ module Type
+ class FloatTest < ActiveModel::TestCase
+ def test_type_cast_float
+ type = Type::Float.new
+ 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
+
+ assert type.changed?(0.0, 0, "wibble")
+ assert type.changed?(5.0, 0, "wibble")
+ assert_not type.changed?(5.0, 5.0, "5wibble")
+ assert_not type.changed?(5.0, 5.0, "5")
+ assert_not type.changed?(5.0, 5.0, "5.0")
+ assert_not type.changed?(500.0, 500.0, "0.5E+4")
+ assert_not type.changed?(nil, nil, nil)
+ end
+ end
+ end
+end
diff --git a/activemodel/test/cases/type/immutable_string_test.rb b/activemodel/test/cases/type/immutable_string_test.rb
new file mode 100644
index 0000000000..751f753ddb
--- /dev/null
+++ b/activemodel/test/cases/type/immutable_string_test.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+
+module ActiveModel
+ module Type
+ class ImmutableStringTest < ActiveModel::TestCase
+ test "cast strings are frozen" do
+ s = "foo"
+ type = Type::ImmutableString.new
+ assert_equal true, type.cast(s).frozen?
+ end
+
+ test "immutable strings are not duped coming out" do
+ s = "foo"
+ type = Type::ImmutableString.new
+ assert_same s, type.cast(s)
+ assert_same s, type.deserialize(s)
+ end
+ end
+ end
+end
diff --git a/activemodel/test/cases/type/integer_test.rb b/activemodel/test/cases/type/integer_test.rb
new file mode 100644
index 0000000000..df12098974
--- /dev/null
+++ b/activemodel/test/cases/type/integer_test.rb
@@ -0,0 +1,121 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "active_support/core_ext/numeric/time"
+
+module ActiveModel
+ module Type
+ 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")
+ assert_equal 0, type.cast("bad1")
+ assert_equal 0, type.cast("bad")
+ assert_equal 1, type.cast(1.7)
+ assert_equal 0, type.cast(false)
+ assert_equal 1, type.cast(true)
+ assert_nil type.cast(nil)
+ end
+
+ test "random objects cast to nil" do
+ type = Type::Integer.new
+ assert_nil type.cast([1, 2])
+ assert_nil type.cast(1 => 2)
+ assert_nil type.cast(1..2)
+ end
+
+ test "casting objects without to_i" do
+ type = Type::Integer.new
+ assert_nil type.cast(::Object.new)
+ end
+
+ test "casting nan and infinity" do
+ type = Type::Integer.new
+ assert_nil type.cast(::Float::NAN)
+ assert_nil type.cast(1.0 / 0.0)
+ end
+
+ test "casting booleans for database" do
+ type = Type::Integer.new
+ assert_equal 1, type.serialize(true)
+ assert_equal 0, type.serialize(false)
+ end
+
+ test "casting duration" do
+ type = Type::Integer.new
+ assert_equal 1800, type.cast(30.minutes)
+ assert_equal 7200, type.cast(2.hours)
+ end
+
+ test "changed?" do
+ type = Type::Integer.new
+
+ assert type.changed?(0, 0, "wibble")
+ assert type.changed?(5, 0, "wibble")
+ assert_not type.changed?(5, 5, "5wibble")
+ assert_not type.changed?(5, 5, "5")
+ assert_not type.changed?(5, 5, "5.0")
+ assert_not type.changed?(5, 5, "+5")
+ assert_not type.changed?(5, 5, "+5.0")
+ assert_not type.changed?(-5, -5, "-5")
+ assert_not type.changed?(-5, -5, "-5.0")
+ assert_not type.changed?(nil, nil, nil)
+ end
+
+ test "values below int min value are out of range" do
+ assert_raises(ActiveModel::RangeError) do
+ Integer.new.serialize(-2147483649)
+ end
+ end
+
+ test "values above int max value are out of range" do
+ assert_raises(ActiveModel::RangeError) do
+ Integer.new.serialize(2147483648)
+ end
+ end
+
+ test "very small numbers are out of range" do
+ assert_raises(ActiveModel::RangeError) do
+ Integer.new.serialize(-9999999999999999999999999999999)
+ end
+ end
+
+ test "very large numbers are out of range" do
+ assert_raises(ActiveModel::RangeError) do
+ Integer.new.serialize(9999999999999999999999999999999)
+ end
+ end
+
+ test "normal numbers are in range" do
+ type = Integer.new
+ assert_equal(0, type.serialize(0))
+ assert_equal(-1, type.serialize(-1))
+ assert_equal(1, type.serialize(1))
+ end
+
+ test "int max value is in range" do
+ assert_equal(2147483647, Integer.new.serialize(2147483647))
+ end
+
+ test "int min value is in range" do
+ assert_equal(-2147483648, Integer.new.serialize(-2147483648))
+ end
+
+ test "columns with a larger limit have larger ranges" do
+ type = Integer.new(limit: 8)
+
+ assert_equal(9223372036854775807, type.serialize(9223372036854775807))
+ assert_equal(-9223372036854775808, type.serialize(-9223372036854775808))
+ assert_raises(ActiveModel::RangeError) do
+ type.serialize(-9999999999999999999999999999999)
+ end
+ assert_raises(ActiveModel::RangeError) do
+ type.serialize(9999999999999999999999999999999)
+ end
+ end
+ end
+ end
+end
diff --git a/activemodel/test/cases/type/registry_test.rb b/activemodel/test/cases/type/registry_test.rb
new file mode 100644
index 0000000000..0633ea2538
--- /dev/null
+++ b/activemodel/test/cases/type/registry_test.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+
+module ActiveModel
+ module Type
+ class RegistryTest < ActiveModel::TestCase
+ test "a class can be registered for a symbol" do
+ registry = Type::Registry.new
+ registry.register(:foo, ::String)
+ registry.register(:bar, ::Array)
+
+ assert_equal "", registry.lookup(:foo)
+ assert_equal [], registry.lookup(:bar)
+ end
+
+ test "a block can be registered" do
+ registry = Type::Registry.new
+ registry.register(:foo) do |*args|
+ [*args, "block for foo"]
+ end
+ registry.register(:bar) do |*args|
+ [*args, "block for bar"]
+ end
+
+ assert_equal [:foo, 1, "block for foo"], registry.lookup(:foo, 1)
+ assert_equal [:foo, 2, "block for foo"], registry.lookup(:foo, 2)
+ assert_equal [:bar, 1, 2, 3, "block for bar"], registry.lookup(:bar, 1, 2, 3)
+ end
+
+ test "a reasonable error is given when no type is found" do
+ registry = Type::Registry.new
+
+ e = assert_raises(ArgumentError) do
+ registry.lookup(:foo)
+ end
+
+ assert_equal "Unknown type :foo", e.message
+ end
+ end
+ end
+end
diff --git a/activemodel/test/cases/type/string_test.rb b/activemodel/test/cases/type/string_test.rb
new file mode 100644
index 0000000000..2d85556d20
--- /dev/null
+++ b/activemodel/test/cases/type/string_test.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+
+module ActiveModel
+ module Type
+ class StringTest < ActiveModel::TestCase
+ test "type casting" do
+ type = Type::String.new
+ assert_equal "t", type.cast(true)
+ assert_equal "f", type.cast(false)
+ assert_equal "123", type.cast(123)
+ end
+
+ test "cast strings are mutable" do
+ type = Type::String.new
+
+ s = +"foo"
+ assert_equal false, type.cast(s).frozen?
+ assert_equal false, s.frozen?
+
+ f = -"foo"
+ assert_equal false, type.cast(f).frozen?
+ assert_equal true, f.frozen?
+ end
+
+ test "values are duped coming out" do
+ type = Type::String.new
+
+ s = "foo"
+ assert_not_same s, type.cast(s)
+ assert_equal s, type.cast(s)
+ assert_not_same s, type.deserialize(s)
+ assert_equal s, type.deserialize(s)
+ end
+ end
+ end
+end
diff --git a/activemodel/test/cases/type/time_test.rb b/activemodel/test/cases/type/time_test.rb
new file mode 100644
index 0000000000..3fbae1a169
--- /dev/null
+++ b/activemodel/test/cases/type/time_test.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+
+module ActiveModel
+ module Type
+ class TimeTest < ActiveModel::TestCase
+ def test_type_cast_time
+ type = Type::Time.new
+ 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")
+
+ assert_equal ::Time.utc(2000, 1, 1, 16, 45, 54), type.cast("2015-06-13T19:45:54+03:00")
+ assert_equal ::Time.utc(1999, 12, 31, 21, 7, 8), type.cast("06:07:08+09:00")
+ end
+
+ def test_user_input_in_time_zone
+ ::Time.use_zone("Pacific Time (US & Canada)") do
+ type = Type::Time.new
+ assert_nil type.user_input_in_time_zone(nil)
+ assert_nil type.user_input_in_time_zone("")
+ assert_nil type.user_input_in_time_zone("ABC")
+
+ offset = ::Time.zone.formatted_offset
+ time_string = "2015-02-09T19:45:54#{offset}"
+
+ assert_equal 19, type.user_input_in_time_zone(time_string).hour
+ assert_equal offset, type.user_input_in_time_zone(time_string).formatted_offset
+ end
+ end
+ end
+ end
+end
diff --git a/activemodel/test/cases/type/value_test.rb b/activemodel/test/cases/type/value_test.rb
new file mode 100644
index 0000000000..55b5d9d584
--- /dev/null
+++ b/activemodel/test/cases/type/value_test.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+
+module ActiveModel
+ module Type
+ class ValueTest < ActiveModel::TestCase
+ def test_type_equality
+ assert_equal Type::Value.new, Type::Value.new
+ assert_not_equal Type::Value.new, Type::Integer.new
+ assert_not_equal Type::Value.new(precision: 1), Type::Value.new(precision: 2)
+ end
+ end
+ end
+end
diff --git a/activemodel/test/cases/validations/absence_validation_test.rb b/activemodel/test/cases/validations/absence_validation_test.rb
index 9cbc77dfb5..8bc4f4723a 100644
--- a/activemodel/test/cases/validations/absence_validation_test.rb
+++ b/activemodel/test/cases/validations/absence_validation_test.rb
@@ -1,7 +1,9 @@
-require 'cases/helper'
-require 'models/topic'
-require 'models/person'
-require 'models/custom_reader'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/topic"
+require "models/person"
+require "models/custom_reader"
class AbsenceValidationTest < ActiveModel::TestCase
teardown do
@@ -15,16 +17,16 @@ class AbsenceValidationTest < ActiveModel::TestCase
t = Topic.new
t.title = "foo"
t.content = "bar"
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal ["must be blank"], t.errors[:title]
assert_equal ["must be blank"], t.errors[:content]
t.title = ""
- t.content = "something"
- assert t.invalid?
+ t.content = "something"
+ assert_predicate t, :invalid?
assert_equal ["must be blank"], t.errors[:content]
assert_equal [], t.errors[:title]
t.content = ""
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_absence_of_with_array_arguments
@@ -32,7 +34,7 @@ class AbsenceValidationTest < ActiveModel::TestCase
t = Topic.new
t.title = "foo"
t.content = "bar"
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal ["must be blank"], t.errors[:title]
assert_equal ["must be blank"], t.errors[:content]
end
@@ -41,7 +43,7 @@ class AbsenceValidationTest < ActiveModel::TestCase
Person.validates_absence_of :karma, message: "This string contains 'single' and \"double\" quotes"
p = Person.new
p.karma = "good"
- assert p.invalid?
+ assert_predicate p, :invalid?
assert_equal "This string contains 'single' and \"double\" quotes", p.errors[:karma].last
end
@@ -49,19 +51,19 @@ class AbsenceValidationTest < ActiveModel::TestCase
Person.validates_absence_of :karma
p = Person.new
p.karma = "good"
- assert p.invalid?
+ assert_predicate p, :invalid?
assert_equal ["must be blank"], p.errors[:karma]
p.karma = nil
- assert p.valid?
+ assert_predicate p, :valid?
end
def test_validates_absence_of_for_ruby_class_with_custom_reader
CustomReader.validates_absence_of :karma
p = CustomReader.new
p[:karma] = "excellent"
- assert p.invalid?
+ assert_predicate p, :invalid?
assert_equal ["must be blank"], p.errors[:karma]
p[:karma] = ""
- assert p.valid?
+ assert_predicate p, :valid?
end
end
diff --git a/activemodel/test/cases/validations/acceptance_validation_test.rb b/activemodel/test/cases/validations/acceptance_validation_test.rb
index d3995ad5af..7662f996ae 100644
--- a/activemodel/test/cases/validations/acceptance_validation_test.rb
+++ b/activemodel/test/cases/validations/acceptance_validation_test.rb
@@ -1,11 +1,12 @@
-require 'cases/helper'
+# frozen_string_literal: true
-require 'models/topic'
-require 'models/reply'
-require 'models/person'
+require "cases/helper"
-class AcceptanceValidationTest < ActiveModel::TestCase
+require "models/topic"
+require "models/reply"
+require "models/person"
+class AcceptanceValidationTest < ActiveModel::TestCase
def teardown
Topic.clear_validators!
end
@@ -14,54 +15,54 @@ class AcceptanceValidationTest < ActiveModel::TestCase
Topic.validates_acceptance_of(:terms_of_service)
t = Topic.new("title" => "We should not be confirmed")
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_terms_of_service_agreement
Topic.validates_acceptance_of(:terms_of_service)
- t = Topic.new("title" => "We should be confirmed","terms_of_service" => "")
- assert t.invalid?
+ t = Topic.new("title" => "We should be confirmed", "terms_of_service" => "")
+ assert_predicate t, :invalid?
assert_equal ["must be accepted"], t.errors[:terms_of_service]
t.terms_of_service = "1"
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_eula
Topic.validates_acceptance_of(:eula, message: "must be abided")
- t = Topic.new("title" => "We should be confirmed","eula" => "")
- assert t.invalid?
+ t = Topic.new("title" => "We should be confirmed", "eula" => "")
+ assert_predicate t, :invalid?
assert_equal ["must be abided"], t.errors[:eula]
t.eula = "1"
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_terms_of_service_agreement_with_accept_value
Topic.validates_acceptance_of(:terms_of_service, accept: "I agree.")
t = Topic.new("title" => "We should be confirmed", "terms_of_service" => "")
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal ["must be accepted"], t.errors[:terms_of_service]
t.terms_of_service = "I agree."
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_terms_of_service_agreement_with_multiple_accept_values
Topic.validates_acceptance_of(:terms_of_service, accept: [1, "I concur."])
t = Topic.new("title" => "We should be confirmed", "terms_of_service" => "")
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal ["must be accepted"], t.errors[:terms_of_service]
t.terms_of_service = 1
- assert t.valid?
+ assert_predicate t, :valid?
t.terms_of_service = "I concur."
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_acceptance_of_for_ruby_class
@@ -70,11 +71,11 @@ class AcceptanceValidationTest < ActiveModel::TestCase
p = Person.new
p.karma = ""
- assert p.invalid?
+ assert_predicate p, :invalid?
assert_equal ["must be accepted"], p.errors[:karma]
p.karma = "1"
- assert p.valid?
+ assert_predicate p, :valid?
ensure
Person.clear_validators!
end
@@ -82,6 +83,6 @@ class AcceptanceValidationTest < ActiveModel::TestCase
def test_validates_acceptance_of_true
Topic.validates_acceptance_of(:terms_of_service)
- assert Topic.new(terms_of_service: true).valid?
+ assert_predicate Topic.new(terms_of_service: true), :valid?
end
end
diff --git a/activemodel/test/cases/validations/callbacks_test.rb b/activemodel/test/cases/validations/callbacks_test.rb
index 75eb18e795..ff3cf61746 100644
--- a/activemodel/test/cases/validations/callbacks_test.rb
+++ b/activemodel/test/cases/validations/callbacks_test.rb
@@ -1,4 +1,6 @@
-require 'cases/helper'
+# frozen_string_literal: true
+
+require "cases/helper"
class Dog
include ActiveModel::Validations
@@ -15,37 +17,37 @@ class DogWithMethodCallbacks < Dog
before_validation :set_before_validation_marker
after_validation :set_after_validation_marker
- def set_before_validation_marker; self.history << 'before_validation_marker'; end
- def set_after_validation_marker; self.history << 'after_validation_marker' ; end
+ def set_before_validation_marker; history << "before_validation_marker"; end
+ def set_after_validation_marker; history << "after_validation_marker" ; end
end
class DogValidatorsAreProc < Dog
- before_validation { self.history << 'before_validation_marker' }
- after_validation { self.history << 'after_validation_marker' }
+ before_validation { history << "before_validation_marker" }
+ after_validation { history << "after_validation_marker" }
end
class DogWithTwoValidators < Dog
- before_validation { self.history << 'before_validation_marker1' }
- before_validation { self.history << 'before_validation_marker2' }
+ before_validation { history << "before_validation_marker1" }
+ before_validation { history << "before_validation_marker2" }
end
-class DogDeprecatedBeforeValidatorReturningFalse < Dog
+class DogBeforeValidatorReturningFalse < Dog
before_validation { false }
- before_validation { self.history << 'before_validation_marker2' }
+ before_validation { history << "before_validation_marker2" }
end
class DogBeforeValidatorThrowingAbort < Dog
before_validation { throw :abort }
- before_validation { self.history << 'before_validation_marker2' }
+ before_validation { history << "before_validation_marker2" }
end
class DogAfterValidatorReturningFalse < Dog
after_validation { false }
- after_validation { self.history << 'after_validation_marker' }
+ after_validation { history << "after_validation_marker" }
end
class DogWithMissingName < Dog
- before_validation { self.history << 'before_validation_marker' }
+ before_validation { history << "before_validation_marker" }
validates_presence_of :name
end
@@ -53,8 +55,20 @@ class DogValidatorWithOnCondition < Dog
before_validation :set_before_validation_marker, on: :create
after_validation :set_after_validation_marker, on: :create
- def set_before_validation_marker; self.history << 'before_validation_marker'; end
- def set_after_validation_marker; self.history << 'after_validation_marker' ; end
+ def set_before_validation_marker; history << "before_validation_marker"; end
+ def set_after_validation_marker; history << "after_validation_marker" ; end
+end
+
+class DogValidatorWithOnMultipleCondition < Dog
+ before_validation :set_before_validation_marker_on_context_a, on: :context_a
+ before_validation :set_before_validation_marker_on_context_b, on: :context_b
+ after_validation :set_after_validation_marker_on_context_a, on: :context_a
+ after_validation :set_after_validation_marker_on_context_b, on: :context_b
+
+ def set_before_validation_marker_on_context_a; history << "before_validation_marker on context_a"; end
+ def set_before_validation_marker_on_context_b; history << "before_validation_marker on context_b"; end
+ def set_after_validation_marker_on_context_a; history << "after_validation_marker on context_a" ; end
+ def set_after_validation_marker_on_context_b; history << "after_validation_marker on context_b" ; end
end
class DogValidatorWithIfCondition < Dog
@@ -64,16 +78,14 @@ class DogValidatorWithIfCondition < Dog
after_validation :set_after_validation_marker1, if: -> { true }
after_validation :set_after_validation_marker2, if: -> { false }
- def set_before_validation_marker1; self.history << 'before_validation_marker1'; end
- def set_before_validation_marker2; self.history << 'before_validation_marker2' ; end
+ def set_before_validation_marker1; history << "before_validation_marker1"; end
+ def set_before_validation_marker2; history << "before_validation_marker2" ; end
- def set_after_validation_marker1; self.history << 'after_validation_marker1'; end
- def set_after_validation_marker2; self.history << 'after_validation_marker2' ; end
+ def set_after_validation_marker1; history << "after_validation_marker1"; end
+ def set_after_validation_marker2; history << "after_validation_marker2" ; end
end
-
class CallbacksWithMethodNamesShouldBeCalled < ActiveModel::TestCase
-
def test_if_condition_is_respected_for_before_validation
d = DogValidatorWithIfCondition.new
d.valid?
@@ -98,22 +110,53 @@ class CallbacksWithMethodNamesShouldBeCalled < ActiveModel::TestCase
assert_equal [], d.history
end
+ def test_on_multiple_condition_is_respected_for_validation_with_matching_context
+ d = DogValidatorWithOnMultipleCondition.new
+ d.valid?(:context_a)
+ assert_equal ["before_validation_marker on context_a", "after_validation_marker on context_a"], d.history
+
+ d = DogValidatorWithOnMultipleCondition.new
+ d.valid?(:context_b)
+ assert_equal ["before_validation_marker on context_b", "after_validation_marker on context_b"], d.history
+
+ d = DogValidatorWithOnMultipleCondition.new
+ d.valid?([:context_a, :context_b])
+ assert_equal([
+ "before_validation_marker on context_a",
+ "before_validation_marker on context_b",
+ "after_validation_marker on context_a",
+ "after_validation_marker on context_b"
+ ], d.history)
+ end
+
+ def test_on_multiple_condition_is_respected_for_validation_without_matching_context
+ d = DogValidatorWithOnMultipleCondition.new
+ d.valid?(:save)
+ assert_equal [], d.history
+ end
+
+ def test_on_multiple_condition_is_respected_for_validation_without_context
+ d = DogValidatorWithOnMultipleCondition.new
+ d.valid?
+ assert_equal [], d.history
+ end
+
def test_before_validation_and_after_validation_callbacks_should_be_called
d = DogWithMethodCallbacks.new
d.valid?
- assert_equal ['before_validation_marker', 'after_validation_marker'], d.history
+ assert_equal ["before_validation_marker", "after_validation_marker"], d.history
end
def test_before_validation_and_after_validation_callbacks_should_be_called_with_proc
d = DogValidatorsAreProc.new
d.valid?
- assert_equal ['before_validation_marker', 'after_validation_marker'], d.history
+ assert_equal ["before_validation_marker", "after_validation_marker"], d.history
end
def test_before_validation_and_after_validation_callbacks_should_be_called_in_declared_order
d = DogWithTwoValidators.new
d.valid?
- assert_equal ['before_validation_marker1', 'before_validation_marker2'], d.history
+ assert_equal ["before_validation_marker1", "before_validation_marker2"], d.history
end
def test_further_callbacks_should_not_be_called_if_before_validation_throws_abort
@@ -123,26 +166,23 @@ 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
d = DogAfterValidatorReturningFalse.new
d.valid?
- assert_equal ['after_validation_marker'], d.history
+ assert_equal ["after_validation_marker"], d.history
end
def test_validation_test_should_be_done
d = DogWithMissingName.new
output = d.valid?
- assert_equal ['before_validation_marker'], d.history
+ assert_equal ["before_validation_marker"], d.history
assert_equal false, output
end
-
end
diff --git a/activemodel/test/cases/validations/conditional_validation_test.rb b/activemodel/test/cases/validations/conditional_validation_test.rb
index 296d3b4407..1704db9a48 100644
--- a/activemodel/test/cases/validations/conditional_validation_test.rb
+++ b/activemodel/test/cases/validations/conditional_validation_test.rb
@@ -1,9 +1,10 @@
-require 'cases/helper'
+# frozen_string_literal: true
-require 'models/topic'
+require "cases/helper"
-class ConditionalValidationTest < ActiveModel::TestCase
+require "models/topic"
+class ConditionalValidationTest < ActiveModel::TestCase
def teardown
Topic.clear_validators!
end
@@ -12,67 +13,63 @@ class ConditionalValidationTest < ActiveModel::TestCase
# When the method returns true
Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: :condition_is_true)
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["hoo 5"], t.errors["title"]
end
- def test_unless_validation_using_method_true
- # When the method returns true
- Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: :condition_is_true)
+ def test_if_validation_using_array_of_true_methods
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: [:condition_is_true, :condition_is_true])
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
- assert t.valid?
- assert_empty t.errors[:title]
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
+ assert_equal ["hoo 5"], t.errors["title"]
end
- def test_if_validation_using_method_false
- # When the method returns false
- Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: :condition_is_true_but_its_not)
+ def test_unless_validation_using_array_of_false_methods
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: [:condition_is_false, :condition_is_false])
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
- assert t.valid?
- assert_empty t.errors[:title]
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
+ assert_equal ["hoo 5"], t.errors["title"]
end
- def test_unless_validation_using_method_false
- # When the method returns false
- Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: :condition_is_true_but_its_not)
+ def test_unless_validation_using_method_true
+ # When the method returns true
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: :condition_is_true)
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
- assert t.invalid?
- assert t.errors[:title].any?
- assert_equal ["hoo 5"], t.errors["title"]
+ assert_predicate t, :valid?
+ assert_empty t.errors[:title]
end
- def test_if_validation_using_string_true
- # When the evaluated string returns true
- Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: "a = 1; a == 1")
+ def test_if_validation_using_array_of_true_and_false_methods
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: [:condition_is_true, :condition_is_false])
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
- assert t.invalid?
- assert t.errors[:title].any?
- assert_equal ["hoo 5"], t.errors["title"]
+ assert_predicate t, :valid?
+ assert_empty t.errors[:title]
end
- def test_unless_validation_using_string_true
- # When the evaluated string returns true
- Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: "a = 1; a == 1")
+ def test_unless_validation_using_array_of_true_and_felse_methods
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: [:condition_is_true, :condition_is_false])
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
- assert t.valid?
+ assert_predicate t, :valid?
assert_empty t.errors[:title]
end
- def test_if_validation_using_string_false
- # When the evaluated string returns false
- Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: "false")
+ def test_if_validation_using_method_false
+ # When the method returns false
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: :condition_is_false)
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
- assert t.valid?
+ assert_predicate t, :valid?
assert_empty t.errors[:title]
end
- def test_unless_validation_using_string_false
- # When the evaluated string returns false
- Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: "false")
+ def test_unless_validation_using_method_false
+ # When the method returns false
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: :condition_is_false)
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["hoo 5"], t.errors["title"]
end
@@ -81,8 +78,8 @@ class ConditionalValidationTest < ActiveModel::TestCase
Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}",
if: Proc.new { |r| r.content.size > 4 })
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["hoo 5"], t.errors["title"]
end
@@ -91,48 +88,41 @@ class ConditionalValidationTest < ActiveModel::TestCase
Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}",
unless: Proc.new { |r| r.content.size > 4 })
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
- assert t.valid?
+ assert_predicate t, :valid?
assert_empty t.errors[:title]
end
def test_if_validation_using_block_false
# When the block returns false
Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}",
- if: Proc.new { |r| r.title != "uhohuhoh"})
+ if: Proc.new { |r| r.title != "uhohuhoh" })
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
- assert t.valid?
+ assert_predicate t, :valid?
assert_empty t.errors[:title]
end
def test_unless_validation_using_block_false
# When the block returns false
Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}",
- unless: Proc.new { |r| r.title != "uhohuhoh"} )
+ unless: Proc.new { |r| r.title != "uhohuhoh" })
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["hoo 5"], t.errors["title"]
end
- # previous implementation of validates_presence_of eval'd the
- # string with the wrong binding, this regression test is to
- # ensure that it works correctly
- def test_validation_with_if_as_string
- Topic.validates_presence_of(:title)
- Topic.validates_presence_of(:author_name, if: "title.to_s.match('important')")
-
- t = Topic.new
- assert t.invalid?, "A topic without a title should not be valid"
- assert_empty t.errors[:author_name], "A topic without an 'important' title should not require an author"
-
- t.title = "Just a title"
- assert t.valid?, "A topic with a basic title should be valid"
-
- t.title = "A very important title"
- assert t.invalid?, "A topic with an important title, but without an author, should not be valid"
- assert t.errors[:author_name].any?, "A topic with an 'important' title should require an author"
+ def test_validation_using_conbining_if_true_and_unless_true_conditions
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: :condition_is_true, unless: :condition_is_true)
+ t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
+ assert_predicate t, :valid?
+ assert_empty t.errors[:title]
+ end
- t.author_name = "Hubert J. Farnsworth"
- assert t.valid?, "A topic with an important title and author should be valid"
+ def test_validation_using_conbining_if_true_and_unless_false_conditions
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: :condition_is_true, unless: :condition_is_false)
+ t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
+ assert_equal ["hoo 5"], t.errors["title"]
end
end
diff --git a/activemodel/test/cases/validations/confirmation_validation_test.rb b/activemodel/test/cases/validations/confirmation_validation_test.rb
index c1431548f7..8603a8ac5c 100644
--- a/activemodel/test/cases/validations/confirmation_validation_test.rb
+++ b/activemodel/test/cases/validations/confirmation_validation_test.rb
@@ -1,10 +1,11 @@
-require 'cases/helper'
+# frozen_string_literal: true
-require 'models/topic'
-require 'models/person'
+require "cases/helper"
-class ConfirmationValidationTest < ActiveModel::TestCase
+require "models/topic"
+require "models/person"
+class ConfirmationValidationTest < ActiveModel::TestCase
def teardown
Topic.clear_validators!
end
@@ -13,27 +14,40 @@ class ConfirmationValidationTest < ActiveModel::TestCase
Topic.validates_confirmation_of(:title)
t = Topic.new(author_name: "Plutarch")
- assert t.valid?
+ assert_predicate t, :valid?
t.title_confirmation = "Parallel Lives"
- assert t.invalid?
+ assert_predicate t, :invalid?
t.title_confirmation = nil
t.title = "Parallel Lives"
- assert t.valid?
+ assert_predicate t, :valid?
t.title_confirmation = "Parallel Lives"
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_title_confirmation
Topic.validates_confirmation_of(:title)
- t = Topic.new("title" => "We should be confirmed","title_confirmation" => "")
- assert t.invalid?
+ t = Topic.new("title" => "We should be confirmed", "title_confirmation" => "")
+ assert_predicate t, :invalid?
t.title_confirmation = "We should be confirmed"
- assert t.valid?
+ assert_predicate t, :valid?
+ end
+
+ def test_validates_confirmation_of_with_boolean_attribute
+ Topic.validates_confirmation_of(:approved)
+
+ t = Topic.new(approved: true, approved_confirmation: nil)
+ assert_predicate t, :valid?
+
+ t.approved_confirmation = false
+ assert_predicate t, :invalid?
+
+ t.approved_confirmation = true
+ assert_predicate t, :valid?
end
def test_validates_confirmation_of_for_ruby_class
@@ -41,12 +55,12 @@ class ConfirmationValidationTest < ActiveModel::TestCase
p = Person.new
p.karma_confirmation = "None"
- assert p.invalid?
+ assert_predicate p, :invalid?
assert_equal ["doesn't match Karma"], p.errors[:karma_confirmation]
p.karma = "None"
- assert p.valid?
+ assert_predicate p, :valid?
ensure
Person.clear_validators!
end
@@ -56,15 +70,14 @@ class ConfirmationValidationTest < ActiveModel::TestCase
@old_load_path, @old_backend = I18n.load_path.dup, I18n.backend
I18n.load_path.clear
I18n.backend = I18n::Backend::Simple.new
- I18n.backend.store_translations('en', {
+ I18n.backend.store_translations("en",
errors: { messages: { confirmation: "doesn't match %{attribute}" } },
- activemodel: { attributes: { topic: { title: 'Test Title'} } }
- })
+ activemodel: { attributes: { topic: { title: "Test Title" } } })
Topic.validates_confirmation_of(:title)
- t = Topic.new("title" => "We should be confirmed","title_confirmation" => "")
- assert t.invalid?
+ t = Topic.new("title" => "We should be confirmed", "title_confirmation" => "")
+ assert_predicate t, :invalid?
assert_equal ["doesn't match Test Title"], t.errors[:title_confirmation]
ensure
I18n.load_path.replace @old_load_path
@@ -104,4 +117,18 @@ class ConfirmationValidationTest < ActiveModel::TestCase
assert_equal "expected title", model.title_confirmation,
"confirmation validation should not override the writer"
end
+
+ def test_title_confirmation_with_case_sensitive_option_true
+ Topic.validates_confirmation_of(:title, case_sensitive: true)
+
+ t = Topic.new(title: "title", title_confirmation: "Title")
+ assert_predicate t, :invalid?
+ end
+
+ def test_title_confirmation_with_case_sensitive_option_false
+ Topic.validates_confirmation_of(:title, case_sensitive: false)
+
+ t = Topic.new(title: "title", title_confirmation: "Title")
+ assert_predicate t, :valid?
+ end
end
diff --git a/activemodel/test/cases/validations/exclusion_validation_test.rb b/activemodel/test/cases/validations/exclusion_validation_test.rb
index 005bc15df5..50bd47065c 100644
--- a/activemodel/test/cases/validations/exclusion_validation_test.rb
+++ b/activemodel/test/cases/validations/exclusion_validation_test.rb
@@ -1,11 +1,12 @@
-require 'cases/helper'
-require 'active_support/core_ext/numeric/time'
+# frozen_string_literal: true
-require 'models/topic'
-require 'models/person'
+require "cases/helper"
+require "active_support/core_ext/numeric/time"
-class ExclusionValidationTest < ActiveModel::TestCase
+require "models/topic"
+require "models/person"
+class ExclusionValidationTest < ActiveModel::TestCase
def teardown
Topic.clear_validators!
end
@@ -13,8 +14,8 @@ class ExclusionValidationTest < ActiveModel::TestCase
def test_validates_exclusion_of
Topic.validates_exclusion_of(:title, in: %w( abe monkey ))
- assert Topic.new("title" => "something", "content" => "abc").valid?
- assert Topic.new("title" => "monkey", "content" => "abc").invalid?
+ assert_predicate Topic.new("title" => "something", "content" => "abc"), :valid?
+ assert_predicate Topic.new("title" => "monkey", "content" => "abc"), :invalid?
end
def test_validates_exclusion_of_with_formatted_message
@@ -23,8 +24,8 @@ class ExclusionValidationTest < ActiveModel::TestCase
assert Topic.new("title" => "something", "content" => "abc")
t = Topic.new("title" => "monkey")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["option monkey is restricted"], t.errors[:title]
end
@@ -34,8 +35,8 @@ class ExclusionValidationTest < ActiveModel::TestCase
assert Topic.new("title" => "something", "content" => "abc")
t = Topic.new("title" => "monkey")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
end
def test_validates_exclusion_of_for_ruby_class
@@ -43,12 +44,12 @@ class ExclusionValidationTest < ActiveModel::TestCase
p = Person.new
p.karma = "abe"
- assert p.invalid?
+ assert_predicate p, :invalid?
assert_equal ["is reserved"], p.errors[:karma]
p.karma = "Lifo"
- assert p.valid?
+ assert_predicate p, :valid?
ensure
Person.clear_validators!
end
@@ -59,26 +60,26 @@ class ExclusionValidationTest < ActiveModel::TestCase
t = Topic.new
t.title = "elephant"
t.author_name = "sikachu"
- assert t.invalid?
+ assert_predicate t, :invalid?
t.title = "wasabi"
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_exclusion_of_with_range
Topic.validates_exclusion_of :content, in: ("a".."g")
- assert Topic.new(content: 'g').invalid?
- assert Topic.new(content: 'h').valid?
+ assert_predicate Topic.new(content: "g"), :invalid?
+ assert_predicate Topic.new(content: "h"), :valid?
end
def test_validates_exclusion_of_with_time_range
Topic.validates_exclusion_of :created_at, in: 6.days.ago..2.days.ago
- assert Topic.new(created_at: 5.days.ago).invalid?
- assert Topic.new(created_at: 3.days.ago).invalid?
- assert Topic.new(created_at: 7.days.ago).valid?
- assert Topic.new(created_at: 1.day.ago).valid?
+ assert_predicate Topic.new(created_at: 5.days.ago), :invalid?
+ assert_predicate Topic.new(created_at: 3.days.ago), :invalid?
+ assert_predicate Topic.new(created_at: 7.days.ago), :valid?
+ assert_predicate Topic.new(created_at: 1.day.ago), :valid?
end
def test_validates_inclusion_of_with_symbol
@@ -91,7 +92,7 @@ class ExclusionValidationTest < ActiveModel::TestCase
%w(abe)
end
- assert p.invalid?
+ assert_predicate p, :invalid?
assert_equal ["is reserved"], p.errors[:karma]
p = Person.new
@@ -101,7 +102,7 @@ class ExclusionValidationTest < ActiveModel::TestCase
%w()
end
- assert p.valid?
+ assert_predicate p, :valid?
ensure
Person.clear_validators!
end
diff --git a/activemodel/test/cases/validations/format_validation_test.rb b/activemodel/test/cases/validations/format_validation_test.rb
index 86bbbe6ebe..2a7088b3e8 100644
--- a/activemodel/test/cases/validations/format_validation_test.rb
+++ b/activemodel/test/cases/validations/format_validation_test.rb
@@ -1,10 +1,11 @@
-require 'cases/helper'
+# frozen_string_literal: true
-require 'models/topic'
-require 'models/person'
+require "cases/helper"
-class PresenceValidationTest < ActiveModel::TestCase
+require "models/topic"
+require "models/person"
+class FormatValidationTest < ActiveModel::TestCase
def teardown
Topic.clear_validators!
end
@@ -15,22 +16,22 @@ class PresenceValidationTest < ActiveModel::TestCase
t = Topic.new("title" => "i'm incorrect", "content" => "Validation macros rule!")
assert t.invalid?, "Shouldn't be valid"
assert_equal ["is bad data"], t.errors[:title]
- assert t.errors[:content].empty?
+ assert_empty t.errors[:content]
t.title = "Validation macros rule!"
- assert t.valid?
- assert t.errors[:title].empty?
+ assert_predicate t, :valid?
+ assert_empty t.errors[:title]
assert_raise(ArgumentError) { Topic.validates_format_of(:title, :content) }
end
def test_validate_format_with_allow_blank
Topic.validates_format_of(:title, with: /\AValidation\smacros \w+!\z/, allow_blank: true)
- assert Topic.new("title" => "Shouldn't be valid").invalid?
- assert Topic.new("title" => "").valid?
- assert Topic.new("title" => nil).valid?
- assert Topic.new("title" => "Validation macros rule!").valid?
+ assert_predicate Topic.new("title" => "Shouldn't be valid"), :invalid?
+ assert_predicate Topic.new("title" => ""), :valid?
+ assert_predicate Topic.new("title" => nil), :valid?
+ assert_predicate Topic.new("title" => "Validation macros rule!"), :valid?
end
# testing ticket #3142
@@ -41,7 +42,7 @@ class PresenceValidationTest < ActiveModel::TestCase
assert t.invalid?, "Shouldn't be valid"
assert_equal ["is bad data"], t.errors[:title]
- assert t.errors[:content].empty?
+ assert_empty t.errors[:content]
t.title = "-11"
assert t.invalid?, "Shouldn't be valid"
@@ -57,14 +58,14 @@ class PresenceValidationTest < ActiveModel::TestCase
t.title = "1"
- assert t.valid?
- assert t.errors[:title].empty?
+ assert_predicate t, :valid?
+ assert_empty t.errors[:title]
end
def test_validate_format_with_formatted_message
Topic.validates_format_of(:title, with: /\AValid Title\z/, message: "can't be %{value}")
- t = Topic.new(title: 'Invalid title')
- assert t.invalid?
+ t = Topic.new(title: "Invalid title")
+ assert_predicate t, :invalid?
assert_equal ["can't be Invalid title"], t.errors[:title]
end
@@ -73,7 +74,7 @@ class PresenceValidationTest < ActiveModel::TestCase
end
def test_validate_format_of_with_multiline_regexp_and_option
- assert_nothing_raised(ArgumentError) do
+ assert_nothing_raised do
Topic.validates_format_of(:title, with: /^Valid Title$/, multiline: true)
end
end
@@ -108,40 +109,40 @@ class PresenceValidationTest < ActiveModel::TestCase
end
def test_validates_format_of_with_lambda
- Topic.validates_format_of :content, with: lambda { |topic| topic.title == "digit" ? /\A\d+\Z/ : /\A\S+\Z/ }
+ Topic.validates_format_of :content, with: lambda { |topic| topic.title == "digit" ? /\A\d+\z/ : /\A\S+\z/ }
t = Topic.new
t.title = "digit"
t.content = "Pixies"
- assert t.invalid?
+ assert_predicate t, :invalid?
t.content = "1234"
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_format_of_without_lambda
- Topic.validates_format_of :content, without: lambda { |topic| topic.title == "characters" ? /\A\d+\Z/ : /\A\S+\Z/ }
+ Topic.validates_format_of :content, without: lambda { |topic| topic.title == "characters" ? /\A\d+\z/ : /\A\S+\z/ }
t = Topic.new
t.title = "characters"
t.content = "1234"
- assert t.invalid?
+ assert_predicate t, :invalid?
t.content = "Pixies"
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_format_of_for_ruby_class
- Person.validates_format_of :karma, with: /\A\d+\Z/
+ Person.validates_format_of :karma, with: /\A\d+\z/
p = Person.new
p.karma = "Pixies"
- assert p.invalid?
+ assert_predicate p, :invalid?
assert_equal ["is invalid"], p.errors[:karma]
p.karma = "1234"
- assert p.valid?
+ assert_predicate p, :valid?
ensure
Person.clear_validators!
end
diff --git a/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb b/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb
index da63df9152..d3e44945db 100644
--- a/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb
+++ b/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/person'
+require "models/person"
class I18nGenerateMessageValidationTest < ActiveModel::TestCase
def setup
@@ -10,29 +12,29 @@ class I18nGenerateMessageValidationTest < ActiveModel::TestCase
# validates_inclusion_of: generate_message(attr_name, :inclusion, message: custom_message, value: value)
def test_generate_message_inclusion_with_default_message
- assert_equal 'is not included in the list', @person.errors.generate_message(:title, :inclusion, value: 'title')
+ assert_equal "is not included in the list", @person.errors.generate_message(:title, :inclusion, value: "title")
end
def test_generate_message_inclusion_with_custom_message
- assert_equal 'custom message title', @person.errors.generate_message(:title, :inclusion, message: 'custom message %{value}', value: 'title')
+ assert_equal "custom message title", @person.errors.generate_message(:title, :inclusion, message: "custom message %{value}", value: "title")
end
# validates_exclusion_of: generate_message(attr_name, :exclusion, message: custom_message, value: value)
def test_generate_message_exclusion_with_default_message
- assert_equal 'is reserved', @person.errors.generate_message(:title, :exclusion, value: 'title')
+ assert_equal "is reserved", @person.errors.generate_message(:title, :exclusion, value: "title")
end
def test_generate_message_exclusion_with_custom_message
- assert_equal 'custom message title', @person.errors.generate_message(:title, :exclusion, message: 'custom message %{value}', value: 'title')
+ assert_equal "custom message title", @person.errors.generate_message(:title, :exclusion, message: "custom message %{value}", value: "title")
end
# validates_format_of: generate_message(attr_name, :invalid, message: custom_message, value: value)
def test_generate_message_invalid_with_default_message
- assert_equal 'is invalid', @person.errors.generate_message(:title, :invalid, value: 'title')
+ assert_equal "is invalid", @person.errors.generate_message(:title, :invalid, value: "title")
end
def test_generate_message_invalid_with_custom_message
- assert_equal 'custom message title', @person.errors.generate_message(:title, :invalid, message: 'custom message %{value}', value: 'title')
+ assert_equal "custom message title", @person.errors.generate_message(:title, :invalid, message: "custom message %{value}", value: "title")
end
# validates_confirmation_of: generate_message(attr_name, :confirmation, message: custom_message)
@@ -41,7 +43,7 @@ class I18nGenerateMessageValidationTest < ActiveModel::TestCase
end
def test_generate_message_confirmation_with_custom_message
- assert_equal 'custom message', @person.errors.generate_message(:title, :confirmation, message: 'custom message')
+ assert_equal "custom message", @person.errors.generate_message(:title, :confirmation, message: "custom message")
end
# validates_acceptance_of: generate_message(attr_name, :accepted, message: custom_message)
@@ -50,7 +52,7 @@ class I18nGenerateMessageValidationTest < ActiveModel::TestCase
end
def test_generate_message_accepted_with_custom_message
- assert_equal 'custom message', @person.errors.generate_message(:title, :accepted, message: 'custom message')
+ assert_equal "custom message", @person.errors.generate_message(:title, :accepted, message: "custom message")
end
# add_on_empty: generate_message(attr, :empty, message: custom_message)
@@ -59,7 +61,7 @@ class I18nGenerateMessageValidationTest < ActiveModel::TestCase
end
def test_generate_message_empty_with_custom_message
- assert_equal 'custom message', @person.errors.generate_message(:title, :empty, message: 'custom message')
+ assert_equal "custom message", @person.errors.generate_message(:title, :empty, message: "custom message")
end
# validates_presence_of: generate_message(attr, :blank, message: custom_message)
@@ -68,7 +70,7 @@ class I18nGenerateMessageValidationTest < ActiveModel::TestCase
end
def test_generate_message_blank_with_custom_message
- assert_equal 'custom message', @person.errors.generate_message(:title, :blank, message: 'custom message')
+ assert_equal "custom message", @person.errors.generate_message(:title, :blank, message: "custom message")
end
# validates_length_of: generate_message(attr, :too_long, message: custom_message, count: option_value.end)
@@ -81,7 +83,7 @@ class I18nGenerateMessageValidationTest < ActiveModel::TestCase
end
def test_generate_message_too_long_with_custom_message
- assert_equal 'custom message 10', @person.errors.generate_message(:title, :too_long, message: 'custom message %{count}', count: 10)
+ assert_equal "custom message 10", @person.errors.generate_message(:title, :too_long, message: "custom message %{count}", count: 10)
end
# validates_length_of: generate_message(attr, :too_short, default: custom_message, count: option_value.begin)
@@ -94,7 +96,7 @@ class I18nGenerateMessageValidationTest < ActiveModel::TestCase
end
def test_generate_message_too_short_with_custom_message
- assert_equal 'custom message 10', @person.errors.generate_message(:title, :too_short, message: 'custom message %{count}', count: 10)
+ assert_equal "custom message 10", @person.errors.generate_message(:title, :too_short, message: "custom message %{count}", count: 10)
end
# validates_length_of: generate_message(attr, :wrong_length, message: custom_message, count: option_value)
@@ -107,44 +109,44 @@ class I18nGenerateMessageValidationTest < ActiveModel::TestCase
end
def test_generate_message_wrong_length_with_custom_message
- assert_equal 'custom message 10', @person.errors.generate_message(:title, :wrong_length, message: 'custom message %{count}', count: 10)
+ assert_equal "custom message 10", @person.errors.generate_message(:title, :wrong_length, message: "custom message %{count}", count: 10)
end
# validates_numericality_of: generate_message(attr_name, :not_a_number, value: raw_value, message: custom_message)
def test_generate_message_not_a_number_with_default_message
- assert_equal "is not a number", @person.errors.generate_message(:title, :not_a_number, value: 'title')
+ assert_equal "is not a number", @person.errors.generate_message(:title, :not_a_number, value: "title")
end
def test_generate_message_not_a_number_with_custom_message
- assert_equal 'custom message title', @person.errors.generate_message(:title, :not_a_number, message: 'custom message %{value}', value: 'title')
+ assert_equal "custom message title", @person.errors.generate_message(:title, :not_a_number, message: "custom message %{value}", value: "title")
end
# validates_numericality_of: generate_message(attr_name, option, value: raw_value, default: custom_message)
def test_generate_message_greater_than_with_default_message
- assert_equal "must be greater than 10", @person.errors.generate_message(:title, :greater_than, value: 'title', count: 10)
+ assert_equal "must be greater than 10", @person.errors.generate_message(:title, :greater_than, value: "title", count: 10)
end
def test_generate_message_greater_than_or_equal_to_with_default_message
- assert_equal "must be greater than or equal to 10", @person.errors.generate_message(:title, :greater_than_or_equal_to, value: 'title', count: 10)
+ assert_equal "must be greater than or equal to 10", @person.errors.generate_message(:title, :greater_than_or_equal_to, value: "title", count: 10)
end
def test_generate_message_equal_to_with_default_message
- assert_equal "must be equal to 10", @person.errors.generate_message(:title, :equal_to, value: 'title', count: 10)
+ assert_equal "must be equal to 10", @person.errors.generate_message(:title, :equal_to, value: "title", count: 10)
end
def test_generate_message_less_than_with_default_message
- assert_equal "must be less than 10", @person.errors.generate_message(:title, :less_than, value: 'title', count: 10)
+ assert_equal "must be less than 10", @person.errors.generate_message(:title, :less_than, value: "title", count: 10)
end
def test_generate_message_less_than_or_equal_to_with_default_message
- assert_equal "must be less than or equal to 10", @person.errors.generate_message(:title, :less_than_or_equal_to, value: 'title', count: 10)
+ assert_equal "must be less than or equal to 10", @person.errors.generate_message(:title, :less_than_or_equal_to, value: "title", count: 10)
end
def test_generate_message_odd_with_default_message
- assert_equal "must be odd", @person.errors.generate_message(:title, :odd, value: 'title', count: 10)
+ assert_equal "must be odd", @person.errors.generate_message(:title, :odd, value: "title", count: 10)
end
def test_generate_message_even_with_default_message
- assert_equal "must be even", @person.errors.generate_message(:title, :even, value: 'title', count: 10)
+ assert_equal "must be even", @person.errors.generate_message(:title, :even, value: "title", count: 10)
end
end
diff --git a/activemodel/test/cases/validations/i18n_validation_test.rb b/activemodel/test/cases/validations/i18n_validation_test.rb
index ce9a782f73..ccb565c5bd 100644
--- a/activemodel/test/cases/validations/i18n_validation_test.rb
+++ b/activemodel/test/cases/validations/i18n_validation_test.rb
@@ -1,8 +1,9 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/person'
+require "models/person"
class I18nValidationTest < ActiveModel::TestCase
-
def setup
Person.clear_validators!
@person = Person.new
@@ -10,7 +11,10 @@ class I18nValidationTest < ActiveModel::TestCase
@old_load_path, @old_backend = I18n.load_path.dup, I18n.backend
I18n.load_path.clear
I18n.backend = I18n::Backend::Simple.new
- I18n.backend.store_translations('en', errors: { messages: { custom: nil } })
+ I18n.backend.store_translations("en", errors: { messages: { custom: nil } })
+
+ @original_i18n_full_message = ActiveModel::Errors.i18n_full_message
+ ActiveModel::Errors.i18n_full_message = true
end
def teardown
@@ -18,48 +22,161 @@ class I18nValidationTest < ActiveModel::TestCase
I18n.load_path.replace @old_load_path
I18n.backend = @old_backend
I18n.backend.reload!
+ ActiveModel::Errors.i18n_full_message = @original_i18n_full_message
end
def test_full_message_encoding
- I18n.backend.store_translations('en', errors: {
- messages: { too_short: '猫舌' } })
+ I18n.backend.store_translations("en", errors: {
+ messages: { too_short: "猫舌" } })
Person.validates_length_of :title, within: 3..5
@person.valid?
- assert_equal ['Title 猫舌'], @person.errors.full_messages
+ assert_equal ["Title 猫舌"], @person.errors.full_messages
end
def test_errors_full_messages_translates_human_attribute_name_for_model_attributes
- @person.errors.add(:name, 'not found')
- assert_called_with(Person, :human_attribute_name, [:name, default: 'Name'], returns: "Person's name") do
+ @person.errors.add(:name, "not found")
+ assert_called_with(Person, :human_attribute_name, ["name", default: "Name"], returns: "Person's name") do
assert_equal ["Person's name not found"], @person.errors.full_messages
end
end
def test_errors_full_messages_uses_format
- I18n.backend.store_translations('en', errors: { format: "Field %{attribute} %{message}" })
- @person.errors.add('name', 'empty')
+ I18n.backend.store_translations("en", errors: { format: "Field %{attribute} %{message}" })
+ @person.errors.add("name", "empty")
assert_equal ["Field Name empty"], @person.errors.full_messages
end
+ def test_errors_full_messages_doesnt_use_attribute_format_without_config
+ ActiveModel::Errors.i18n_full_message = false
+
+ I18n.backend.store_translations("en", activemodel: {
+ errors: { models: { person: { attributes: { name: { format: "%{message}" } } } } } })
+
+ person = Person.new
+ assert_equal "Name cannot be blank", person.errors.full_message(:name, "cannot be blank")
+ assert_equal "Name test cannot be blank", person.errors.full_message(:name_test, "cannot be blank")
+ end
+
+ def test_errors_full_messages_uses_attribute_format
+ ActiveModel::Errors.i18n_full_message = true
+
+ I18n.backend.store_translations("en", activemodel: {
+ errors: { models: { person: { attributes: { name: { format: "%{message}" } } } } } })
+
+ person = Person.new
+ assert_equal "cannot be blank", person.errors.full_message(:name, "cannot be blank")
+ assert_equal "Name test cannot be blank", person.errors.full_message(:name_test, "cannot be blank")
+ end
+
+ def test_errors_full_messages_uses_model_format
+ ActiveModel::Errors.i18n_full_message = true
+
+ I18n.backend.store_translations("en", activemodel: {
+ errors: { models: { person: { format: "%{message}" } } } })
+
+ person = Person.new
+ assert_equal "cannot be blank", person.errors.full_message(:name, "cannot be blank")
+ assert_equal "cannot be blank", person.errors.full_message(:name_test, "cannot be blank")
+ end
+
+ def test_errors_full_messages_uses_deeply_nested_model_attributes_format
+ ActiveModel::Errors.i18n_full_message = true
+
+ I18n.backend.store_translations("en", activemodel: {
+ errors: { models: { 'person/contacts/addresses': { attributes: { street: { format: "%{message}" } } } } } })
+
+ person = Person.new
+ assert_equal "cannot be blank", person.errors.full_message(:'contacts/addresses.street', "cannot be blank")
+ assert_equal "Contacts/addresses country cannot be blank", person.errors.full_message(:'contacts/addresses.country', "cannot be blank")
+ end
+
+ def test_errors_full_messages_uses_deeply_nested_model_model_format
+ ActiveModel::Errors.i18n_full_message = true
+
+ I18n.backend.store_translations("en", activemodel: {
+ errors: { models: { 'person/contacts/addresses': { format: "%{message}" } } } })
+
+ person = Person.new
+ assert_equal "cannot be blank", person.errors.full_message(:'contacts/addresses.street', "cannot be blank")
+ assert_equal "cannot be blank", person.errors.full_message(:'contacts/addresses.country', "cannot be blank")
+ end
+
+ def test_errors_full_messages_with_indexed_deeply_nested_attributes_and_attributes_format
+ ActiveModel::Errors.i18n_full_message = true
+
+ I18n.backend.store_translations("en", activemodel: {
+ errors: { models: { 'person/contacts/addresses': { attributes: { street: { format: "%{message}" } } } } } })
+
+ person = Person.new
+ assert_equal "cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].street', "cannot be blank")
+ assert_equal "Contacts/addresses country cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].country', "cannot be blank")
+ end
+
+ def test_errors_full_messages_with_indexed_deeply_nested_attributes_and_model_format
+ ActiveModel::Errors.i18n_full_message = true
+
+ I18n.backend.store_translations("en", activemodel: {
+ errors: { models: { 'person/contacts/addresses': { format: "%{message}" } } } })
+
+ person = Person.new
+ assert_equal "cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].street', "cannot be blank")
+ assert_equal "cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].country', "cannot be blank")
+ end
+
+ def test_errors_full_messages_with_indexed_deeply_nested_attributes_and_i18n_attribute_name
+ ActiveModel::Errors.i18n_full_message = true
+
+ I18n.backend.store_translations("en", activemodel: {
+ attributes: { 'person/contacts/addresses': { country: "Country" } }
+ })
+
+ person = Person.new
+ assert_equal "Contacts/addresses street cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].street', "cannot be blank")
+ assert_equal "Country cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].country', "cannot be blank")
+ end
+
+ def test_errors_full_messages_with_indexed_deeply_nested_attributes_without_i18n_config
+ ActiveModel::Errors.i18n_full_message = false
+
+ I18n.backend.store_translations("en", activemodel: {
+ errors: { models: { 'person/contacts/addresses': { attributes: { street: { format: "%{message}" } } } } } })
+
+ person = Person.new
+ assert_equal "Contacts[0]/addresses[0] street cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].street', "cannot be blank")
+ assert_equal "Contacts[0]/addresses[0] country cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].country', "cannot be blank")
+ end
+
+ def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config
+ ActiveModel::Errors.i18n_full_message = false
+
+ I18n.backend.store_translations("en", activemodel: {
+ attributes: { 'person/contacts[0]/addresses[0]': { country: "Country" } }
+ })
+
+ person = Person.new
+ assert_equal "Contacts[0]/addresses[0] street cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].street', "cannot be blank")
+ assert_equal "Country cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].country', "cannot be blank")
+ end
+
# ActiveModel::Validations
# A set of common cases for ActiveModel::Validations message generation that
# are used to generate tests to keep things DRY
#
COMMON_CASES = [
- # [ case, validation_options, generate_message_options]
+ # [ case, validation_options, generate_message_options]
[ "given no options", {}, {}],
[ "given custom message", { message: "custom" }, { message: "custom" }],
- [ "given if condition", { if: lambda { true }}, {}],
- [ "given unless condition", { unless: lambda { false }}, {}],
+ [ "given if condition", { if: lambda { true } }, {}],
+ [ "given unless condition", { unless: lambda { false } }, {}],
[ "given option that is not reserved", { format: "jpg" }, { format: "jpg" }]
]
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_confirmation_of on generated message #{name}" do
Person.validates_confirmation_of :title, validation_options
- @person.title_confirmation = 'foo'
- call = [:title_confirmation, :confirmation, generate_message_options.merge(attribute: 'Title')]
+ @person.title_confirmation = "foo"
+ call = [:title_confirmation, :confirmation, generate_message_options.merge(attribute: "Title")]
assert_called_with(@person.errors, :generate_message, call) do
@person.valid?
end
@@ -99,9 +216,9 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_length_of for :too_long generated message #{name}" do
Person.validates_length_of :title, validation_options.merge(within: 3..5)
- @person.title = 'this title is too long'
+ @person.title = "this title is too long"
call = [:title, :too_long, generate_message_options.merge(count: 5)]
- assert_called_with(@person.errors, :generate_message, ) do
+ assert_called_with(@person.errors, :generate_message, call) do
@person.valid?
end
end
@@ -120,8 +237,8 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_format_of on generated message #{name}" do
Person.validates_format_of :title, validation_options.merge(with: /\A[1-9][0-9]*\z/)
- @person.title = '72x'
- call = [:title, :invalid, generate_message_options.merge(value: '72x')]
+ @person.title = "72x"
+ call = [:title, :invalid, generate_message_options.merge(value: "72x")]
assert_called_with(@person.errors, :generate_message, call) do
@person.valid?
end
@@ -131,8 +248,8 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_inclusion_of on generated message #{name}" do
Person.validates_inclusion_of :title, validation_options.merge(in: %w(a b c))
- @person.title = 'z'
- call = [:title, :inclusion, generate_message_options.merge(value: 'z')]
+ @person.title = "z"
+ call = [:title, :inclusion, generate_message_options.merge(value: "z")]
assert_called_with(@person.errors, :generate_message, call) do
@person.valid?
end
@@ -142,8 +259,8 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_inclusion_of using :within on generated message #{name}" do
Person.validates_inclusion_of :title, validation_options.merge(within: %w(a b c))
- @person.title = 'z'
- call = [:title, :inclusion, generate_message_options.merge(value: 'z')]
+ @person.title = "z"
+ call = [:title, :inclusion, generate_message_options.merge(value: "z")]
assert_called_with(@person.errors, :generate_message, call) do
@person.valid?
end
@@ -153,8 +270,8 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_exclusion_of generated message #{name}" do
Person.validates_exclusion_of :title, validation_options.merge(in: %w(a b c))
- @person.title = 'a'
- call = [:title, :exclusion, generate_message_options.merge(value: 'a')]
+ @person.title = "a"
+ call = [:title, :exclusion, generate_message_options.merge(value: "a")]
assert_called_with(@person.errors, :generate_message, call) do
@person.valid?
end
@@ -164,8 +281,8 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_exclusion_of using :within generated message #{name}" do
Person.validates_exclusion_of :title, validation_options.merge(within: %w(a b c))
- @person.title = 'a'
- call = [:title, :exclusion, generate_message_options.merge(value: 'a')]
+ @person.title = "a"
+ call = [:title, :exclusion, generate_message_options.merge(value: "a")]
assert_called_with(@person.errors, :generate_message, call) do
@person.valid?
end
@@ -175,8 +292,8 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_numericality_of generated message #{name}" do
Person.validates_numericality_of :title, validation_options
- @person.title = 'a'
- call = [:title, :not_a_number, generate_message_options.merge(value: 'a')]
+ @person.title = "a"
+ call = [:title, :not_a_number, generate_message_options.merge(value: "a")]
assert_called_with(@person.errors, :generate_message, call) do
@person.valid?
end
@@ -186,8 +303,8 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_numericality_of for :only_integer on generated message #{name}" do
Person.validates_numericality_of :title, validation_options.merge(only_integer: true)
- @person.title = '0.0'
- call = [:title, :not_an_integer, generate_message_options.merge(value: '0.0')]
+ @person.title = "0.0"
+ call = [:title, :not_an_integer, generate_message_options.merge(value: "0.0")]
assert_called_with(@person.errors, :generate_message, call) do
@person.valid?
end
@@ -225,35 +342,35 @@ class I18nValidationTest < ActiveModel::TestCase
end
test "#{validation} finds custom model key translation when #{error_type}" do
- I18n.backend.store_translations 'en', activemodel: { errors: { models: { person: { attributes: { attribute => { error_type => 'custom message' } } } } } }
- I18n.backend.store_translations 'en', errors: { messages: { error_type => 'global message'}}
+ I18n.backend.store_translations "en", activemodel: { errors: { models: { person: { attributes: { attribute => { error_type => "custom message" } } } } } }
+ I18n.backend.store_translations "en", errors: { messages: { error_type => "global message" } }
yield(@person, {})
@person.valid?
- assert_equal ['custom message'], @person.errors[attribute]
+ assert_equal ["custom message"], @person.errors[attribute]
end
test "#{validation} finds custom model key translation with interpolation when #{error_type}" do
- I18n.backend.store_translations 'en', activemodel: { errors: { models: { person: { attributes: { attribute => { error_type => 'custom message with %{extra}' } } } } } }
- I18n.backend.store_translations 'en', errors: { messages: {error_type => 'global message'} }
+ I18n.backend.store_translations "en", activemodel: { errors: { models: { person: { attributes: { attribute => { error_type => "custom message with %{extra}" } } } } } }
+ I18n.backend.store_translations "en", errors: { messages: { error_type => "global message" } }
yield(@person, { extra: "extra information" })
@person.valid?
- assert_equal ['custom message with extra information'], @person.errors[attribute]
+ assert_equal ["custom message with extra information"], @person.errors[attribute]
end
test "#{validation} finds global default key translation when #{error_type}" do
- I18n.backend.store_translations 'en', errors: { messages: {error_type => 'global message'} }
+ I18n.backend.store_translations "en", errors: { messages: { error_type => "global message" } }
yield(@person, {})
@person.valid?
- assert_equal ['global message'], @person.errors[attribute]
+ assert_equal ["global message"], @person.errors[attribute]
end
end
set_expectations_for_validation "validates_confirmation_of", :confirmation do |person, options_to_merge|
Person.validates_confirmation_of :title, options_to_merge
- person.title_confirmation = 'foo'
+ person.title_confirmation = "foo"
end
set_expectations_for_validation "validates_acceptance_of", :accepted do |person, options_to_merge|
@@ -287,17 +404,17 @@ class I18nValidationTest < ActiveModel::TestCase
set_expectations_for_validation "validates_exclusion_of", :exclusion do |person, options_to_merge|
Person.validates_exclusion_of :title, options_to_merge.merge(in: %w(a b c))
- person.title = 'a'
+ person.title = "a"
end
set_expectations_for_validation "validates_numericality_of", :not_a_number do |person, options_to_merge|
Person.validates_numericality_of :title, options_to_merge
- person.title = 'a'
+ person.title = "a"
end
set_expectations_for_validation "validates_numericality_of", :not_an_integer do |person, options_to_merge|
Person.validates_numericality_of :title, options_to_merge.merge(only_integer: true)
- person.title = '1.0'
+ person.title = "1.0"
end
set_expectations_for_validation "validates_numericality_of", :odd do |person, options_to_merge|
@@ -311,7 +428,7 @@ class I18nValidationTest < ActiveModel::TestCase
end
def test_validations_with_message_symbol_must_translate
- I18n.backend.store_translations 'en', errors: { messages: { custom_error: "I am a custom error" } }
+ I18n.backend.store_translations "en", errors: { messages: { custom_error: "I am a custom error" } }
Person.validates_presence_of :title, message: :custom_error
@person.title = nil
@person.valid?
@@ -319,7 +436,7 @@ class I18nValidationTest < ActiveModel::TestCase
end
def test_validates_with_message_symbol_must_translate_per_attribute
- I18n.backend.store_translations 'en', activemodel: { errors: { models: { person: { attributes: { title: { custom_error: "I am a custom error" } } } } } }
+ I18n.backend.store_translations "en", activemodel: { errors: { models: { person: { attributes: { title: { custom_error: "I am a custom error" } } } } } }
Person.validates_presence_of :title, message: :custom_error
@person.title = nil
@person.valid?
@@ -327,7 +444,7 @@ class I18nValidationTest < ActiveModel::TestCase
end
def test_validates_with_message_symbol_must_translate_per_model
- I18n.backend.store_translations 'en', activemodel: { errors: { models: { person: { custom_error: "I am a custom error" } } } }
+ I18n.backend.store_translations "en", activemodel: { errors: { models: { person: { custom_error: "I am a custom error" } } } }
Person.validates_presence_of :title, message: :custom_error
@person.title = nil
@person.valid?
diff --git a/activemodel/test/cases/validations/inclusion_validation_test.rb b/activemodel/test/cases/validations/inclusion_validation_test.rb
index 55d1fb4dcb..daad76759f 100644
--- a/activemodel/test/cases/validations/inclusion_validation_test.rb
+++ b/activemodel/test/cases/validations/inclusion_validation_test.rb
@@ -1,95 +1,110 @@
-require 'cases/helper'
-require 'active_support/all'
+# frozen_string_literal: true
-require 'models/topic'
-require 'models/person'
+require "cases/helper"
+require "active_support/all"
-class InclusionValidationTest < ActiveModel::TestCase
+require "models/topic"
+require "models/person"
+class InclusionValidationTest < ActiveModel::TestCase
def teardown
Topic.clear_validators!
end
def test_validates_inclusion_of_range
- Topic.validates_inclusion_of(:title, in: 'aaa'..'bbb')
- assert Topic.new("title" => "bbc", "content" => "abc").invalid?
- assert Topic.new("title" => "aa", "content" => "abc").invalid?
- assert Topic.new("title" => "aaab", "content" => "abc").invalid?
- assert Topic.new("title" => "aaa", "content" => "abc").valid?
- assert Topic.new("title" => "abc", "content" => "abc").valid?
- assert Topic.new("title" => "bbb", "content" => "abc").valid?
+ Topic.validates_inclusion_of(:title, in: "aaa".."bbb")
+ assert_predicate Topic.new("title" => "bbc", "content" => "abc"), :invalid?
+ assert_predicate Topic.new("title" => "aa", "content" => "abc"), :invalid?
+ assert_predicate Topic.new("title" => "aaab", "content" => "abc"), :invalid?
+ assert_predicate Topic.new("title" => "aaa", "content" => "abc"), :valid?
+ assert_predicate Topic.new("title" => "abc", "content" => "abc"), :valid?
+ assert_predicate Topic.new("title" => "bbb", "content" => "abc"), :valid?
end
def test_validates_inclusion_of_time_range
- Topic.validates_inclusion_of(:created_at, in: 1.year.ago..Time.now)
- assert Topic.new(title: 'aaa', created_at: 2.years.ago).invalid?
- assert Topic.new(title: 'aaa', created_at: 3.months.ago).valid?
- assert Topic.new(title: 'aaa', created_at: 37.weeks.from_now).invalid?
+ range_begin = 1.year.ago
+ range_end = Time.now
+ Topic.validates_inclusion_of(:created_at, in: range_begin..range_end)
+ assert_predicate Topic.new(title: "aaa", created_at: 2.years.ago), :invalid?
+ assert_predicate Topic.new(title: "aaa", created_at: 3.months.ago), :valid?
+ assert_predicate Topic.new(title: "aaa", created_at: 37.weeks.from_now), :invalid?
+ assert_predicate Topic.new(title: "aaa", created_at: range_begin), :valid?
+ assert_predicate Topic.new(title: "aaa", created_at: range_end), :valid?
end
def test_validates_inclusion_of_date_range
- Topic.validates_inclusion_of(:created_at, in: 1.year.until(Date.today)..Date.today)
- assert Topic.new(title: 'aaa', created_at: 2.years.until(Date.today)).invalid?
- assert Topic.new(title: 'aaa', created_at: 3.months.until(Date.today)).valid?
- assert Topic.new(title: 'aaa', created_at: 37.weeks.since(Date.today)).invalid?
+ range_begin = 1.year.until(Date.today)
+ range_end = Date.today
+ Topic.validates_inclusion_of(:created_at, in: range_begin..range_end)
+ assert_predicate Topic.new(title: "aaa", created_at: 2.years.until(Date.today)), :invalid?
+ assert_predicate Topic.new(title: "aaa", created_at: 3.months.until(Date.today)), :valid?
+ assert_predicate Topic.new(title: "aaa", created_at: 37.weeks.since(Date.today)), :invalid?
+ assert_predicate Topic.new(title: "aaa", created_at: 1.year.until(Date.today)), :valid?
+ assert_predicate Topic.new(title: "aaa", created_at: Date.today), :valid?
+ assert_predicate Topic.new(title: "aaa", created_at: range_begin), :valid?
+ assert_predicate Topic.new(title: "aaa", created_at: range_end), :valid?
end
def test_validates_inclusion_of_date_time_range
- Topic.validates_inclusion_of(:created_at, in: 1.year.until(DateTime.current)..DateTime.current)
- assert Topic.new(title: 'aaa', created_at: 2.years.until(DateTime.current)).invalid?
- assert Topic.new(title: 'aaa', created_at: 3.months.until(DateTime.current)).valid?
- assert Topic.new(title: 'aaa', created_at: 37.weeks.since(DateTime.current)).invalid?
+ range_begin = 1.year.until(DateTime.current)
+ range_end = DateTime.current
+ Topic.validates_inclusion_of(:created_at, in: range_begin..range_end)
+ assert_predicate Topic.new(title: "aaa", created_at: 2.years.until(DateTime.current)), :invalid?
+ assert_predicate Topic.new(title: "aaa", created_at: 3.months.until(DateTime.current)), :valid?
+ assert_predicate Topic.new(title: "aaa", created_at: 37.weeks.since(DateTime.current)), :invalid?
+ assert_predicate Topic.new(title: "aaa", created_at: range_begin), :valid?
+ assert_predicate Topic.new(title: "aaa", created_at: range_end), :valid?
end
def test_validates_inclusion_of
Topic.validates_inclusion_of(:title, in: %w( a b c d e f g ))
- assert Topic.new("title" => "a!", "content" => "abc").invalid?
- assert Topic.new("title" => "a b", "content" => "abc").invalid?
- assert Topic.new("title" => nil, "content" => "def").invalid?
+ assert_predicate Topic.new("title" => "a!", "content" => "abc"), :invalid?
+ assert_predicate Topic.new("title" => "a b", "content" => "abc"), :invalid?
+ assert_predicate Topic.new("title" => nil, "content" => "def"), :invalid?
t = Topic.new("title" => "a", "content" => "I know you are but what am I?")
- assert t.valid?
+ assert_predicate t, :valid?
t.title = "uhoh"
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["is not included in the list"], t.errors[:title]
assert_raise(ArgumentError) { Topic.validates_inclusion_of(:title, in: nil) }
assert_raise(ArgumentError) { Topic.validates_inclusion_of(:title, in: 0) }
- assert_nothing_raised(ArgumentError) { Topic.validates_inclusion_of(:title, in: "hi!") }
- assert_nothing_raised(ArgumentError) { Topic.validates_inclusion_of(:title, in: {}) }
- assert_nothing_raised(ArgumentError) { Topic.validates_inclusion_of(:title, in: []) }
+ assert_nothing_raised { Topic.validates_inclusion_of(:title, in: "hi!") }
+ assert_nothing_raised { Topic.validates_inclusion_of(:title, in: {}) }
+ assert_nothing_raised { Topic.validates_inclusion_of(:title, in: []) }
end
def test_validates_inclusion_of_with_allow_nil
Topic.validates_inclusion_of(:title, in: %w( a b c d e f g ), allow_nil: true)
- assert Topic.new("title" => "a!", "content" => "abc").invalid?
- assert Topic.new("title" => "", "content" => "abc").invalid?
- assert Topic.new("title" => nil, "content" => "abc").valid?
+ assert_predicate Topic.new("title" => "a!", "content" => "abc"), :invalid?
+ assert_predicate Topic.new("title" => "", "content" => "abc"), :invalid?
+ assert_predicate Topic.new("title" => nil, "content" => "abc"), :valid?
end
def test_validates_inclusion_of_with_formatted_message
Topic.validates_inclusion_of(:title, in: %w( a b c d e f g ), message: "option %{value} is not in the list")
- assert Topic.new("title" => "a", "content" => "abc").valid?
+ assert_predicate Topic.new("title" => "a", "content" => "abc"), :valid?
t = Topic.new("title" => "uhoh", "content" => "abc")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["option uhoh is not in the list"], t.errors[:title]
end
def test_validates_inclusion_of_with_within_option
Topic.validates_inclusion_of(:title, within: %w( a b c d e f g ))
- assert Topic.new("title" => "a", "content" => "abc").valid?
+ assert_predicate Topic.new("title" => "a", "content" => "abc"), :valid?
t = Topic.new("title" => "uhoh", "content" => "abc")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
end
def test_validates_inclusion_of_for_ruby_class
@@ -97,26 +112,26 @@ class InclusionValidationTest < ActiveModel::TestCase
p = Person.new
p.karma = "Lifo"
- assert p.invalid?
+ assert_predicate p, :invalid?
assert_equal ["is not included in the list"], p.errors[:karma]
p.karma = "monkey"
- assert p.valid?
+ assert_predicate p, :valid?
ensure
Person.clear_validators!
end
def test_validates_inclusion_of_with_lambda
- Topic.validates_inclusion_of :title, in: lambda{ |topic| topic.author_name == "sikachu" ? %w( monkey elephant ) : %w( abe wasabi ) }
+ Topic.validates_inclusion_of :title, in: lambda { |topic| topic.author_name == "sikachu" ? %w( monkey elephant ) : %w( abe wasabi ) }
t = Topic.new
t.title = "wasabi"
t.author_name = "sikachu"
- assert t.invalid?
+ assert_predicate t, :invalid?
t.title = "elephant"
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_inclusion_of_with_symbol
@@ -129,7 +144,7 @@ class InclusionValidationTest < ActiveModel::TestCase
%w()
end
- assert p.invalid?
+ assert_predicate p, :invalid?
assert_equal ["is not included in the list"], p.errors[:karma]
p = Person.new
@@ -139,7 +154,7 @@ class InclusionValidationTest < ActiveModel::TestCase
%w(Lifo)
end
- assert p.valid?
+ assert_predicate p, :valid?
ensure
Person.clear_validators!
end
diff --git a/activemodel/test/cases/validations/length_validation_test.rb b/activemodel/test/cases/validations/length_validation_test.rb
index ee901b75fb..774a2cde74 100644
--- a/activemodel/test/cases/validations/length_validation_test.rb
+++ b/activemodel/test/cases/validations/length_validation_test.rb
@@ -1,7 +1,9 @@
-require 'cases/helper'
+# frozen_string_literal: true
-require 'models/topic'
-require 'models/person'
+require "cases/helper"
+
+require "models/topic"
+require "models/person"
class LengthValidationTest < ActiveModel::TestCase
def teardown
@@ -9,160 +11,160 @@ class LengthValidationTest < ActiveModel::TestCase
end
def test_validates_length_of_with_allow_nil
- Topic.validates_length_of( :title, is: 5, allow_nil: true )
+ Topic.validates_length_of(:title, is: 5, allow_nil: true)
- assert Topic.new("title" => "ab").invalid?
- assert Topic.new("title" => "").invalid?
- assert Topic.new("title" => nil).valid?
- assert Topic.new("title" => "abcde").valid?
+ assert_predicate Topic.new("title" => "ab"), :invalid?
+ assert_predicate Topic.new("title" => ""), :invalid?
+ assert_predicate Topic.new("title" => nil), :valid?
+ assert_predicate Topic.new("title" => "abcde"), :valid?
end
def test_validates_length_of_with_allow_blank
- Topic.validates_length_of( :title, is: 5, allow_blank: true )
+ Topic.validates_length_of(:title, is: 5, allow_blank: true)
- assert Topic.new("title" => "ab").invalid?
- assert Topic.new("title" => "").valid?
- assert Topic.new("title" => nil).valid?
- assert Topic.new("title" => "abcde").valid?
+ assert_predicate Topic.new("title" => "ab"), :invalid?
+ assert_predicate Topic.new("title" => ""), :valid?
+ assert_predicate Topic.new("title" => nil), :valid?
+ assert_predicate Topic.new("title" => "abcde"), :valid?
end
def test_validates_length_of_using_minimum
Topic.validates_length_of :title, minimum: 5
t = Topic.new("title" => "valid", "content" => "whatever")
- assert t.valid?
+ assert_predicate t, :valid?
t.title = "not"
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["is too short (minimum is 5 characters)"], t.errors[:title]
t.title = ""
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["is too short (minimum is 5 characters)"], t.errors[:title]
t.title = nil
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["is too short (minimum is 5 characters)"], t.errors["title"]
end
def test_validates_length_of_using_maximum_should_allow_nil
Topic.validates_length_of :title, maximum: 10
t = Topic.new
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_optionally_validates_length_of_using_minimum
Topic.validates_length_of :title, minimum: 5, allow_nil: true
t = Topic.new("title" => "valid", "content" => "whatever")
- assert t.valid?
+ assert_predicate t, :valid?
t.title = nil
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_length_of_using_maximum
Topic.validates_length_of :title, maximum: 5
t = Topic.new("title" => "valid", "content" => "whatever")
- assert t.valid?
+ assert_predicate t, :valid?
t.title = "notvalid"
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["is too long (maximum is 5 characters)"], t.errors[:title]
t.title = ""
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_optionally_validates_length_of_using_maximum
Topic.validates_length_of :title, maximum: 5, allow_nil: true
t = Topic.new("title" => "valid", "content" => "whatever")
- assert t.valid?
+ assert_predicate t, :valid?
t.title = nil
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_length_of_using_within
Topic.validates_length_of(:title, :content, within: 3..5)
t = Topic.new("title" => "a!", "content" => "I'm ooooooooh so very long")
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal ["is too short (minimum is 3 characters)"], t.errors[:title]
assert_equal ["is too long (maximum is 5 characters)"], t.errors[:content]
t.title = nil
t.content = nil
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal ["is too short (minimum is 3 characters)"], t.errors[:title]
assert_equal ["is too short (minimum is 3 characters)"], t.errors[:content]
t.title = "abe"
- t.content = "mad"
- assert t.valid?
+ t.content = "mad"
+ assert_predicate t, :valid?
end
def test_validates_length_of_using_within_with_exclusive_range
Topic.validates_length_of(:title, within: 4...10)
t = Topic.new("title" => "9 chars!!")
- assert t.valid?
+ assert_predicate t, :valid?
t.title = "Now I'm 10"
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal ["is too long (maximum is 9 characters)"], t.errors[:title]
t.title = "Four"
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_optionally_validates_length_of_using_within
Topic.validates_length_of :title, :content, within: 3..5, allow_nil: true
- t = Topic.new('title' => 'abc', 'content' => 'abcd')
- assert t.valid?
+ t = Topic.new("title" => "abc", "content" => "abcd")
+ assert_predicate t, :valid?
t.title = nil
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_length_of_using_is
Topic.validates_length_of :title, is: 5
t = Topic.new("title" => "valid", "content" => "whatever")
- assert t.valid?
+ assert_predicate t, :valid?
t.title = "notvalid"
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["is the wrong length (should be 5 characters)"], t.errors[:title]
t.title = ""
- assert t.invalid?
+ assert_predicate t, :invalid?
t.title = nil
- assert t.invalid?
+ assert_predicate t, :invalid?
end
def test_optionally_validates_length_of_using_is
Topic.validates_length_of :title, is: 5, allow_nil: true
t = Topic.new("title" => "valid", "content" => "whatever")
- assert t.valid?
+ assert_predicate t, :valid?
t.title = nil
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_length_of_using_bignum
- bigmin = 2 ** 30
- bigmax = 2 ** 32
+ bigmin = 2**30
+ bigmax = 2**32
bigrange = bigmin...bigmax
assert_nothing_raised do
Topic.validates_length_of :title, is: bigmin + 5
@@ -183,77 +185,77 @@ class LengthValidationTest < ActiveModel::TestCase
end
def test_validates_length_of_custom_errors_for_minimum_with_message
- Topic.validates_length_of( :title, minimum: 5, message: "boo %{count}" )
+ Topic.validates_length_of(:title, minimum: 5, message: "boo %{count}")
t = Topic.new("title" => "uhoh", "content" => "whatever")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["boo 5"], t.errors[:title]
end
def test_validates_length_of_custom_errors_for_minimum_with_too_short
- Topic.validates_length_of( :title, minimum: 5, too_short: "hoo %{count}" )
+ Topic.validates_length_of(:title, minimum: 5, too_short: "hoo %{count}")
t = Topic.new("title" => "uhoh", "content" => "whatever")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["hoo 5"], t.errors[:title]
end
def test_validates_length_of_custom_errors_for_maximum_with_message
- Topic.validates_length_of( :title, maximum: 5, message: "boo %{count}" )
+ Topic.validates_length_of(:title, maximum: 5, message: "boo %{count}")
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["boo 5"], t.errors[:title]
end
def test_validates_length_of_custom_errors_for_in
Topic.validates_length_of(:title, in: 10..20, message: "hoo %{count}")
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["hoo 10"], t.errors["title"]
t = Topic.new("title" => "uhohuhohuhohuhohuhohuhohuhohuhoh", "content" => "whatever")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["hoo 20"], t.errors["title"]
end
def test_validates_length_of_custom_errors_for_maximum_with_too_long
- Topic.validates_length_of( :title, maximum: 5, too_long: "hoo %{count}" )
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}")
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["hoo 5"], t.errors["title"]
end
def test_validates_length_of_custom_errors_for_both_too_short_and_too_long
- Topic.validates_length_of :title, minimum: 3, maximum: 5, too_short: 'too short', too_long: 'too long'
+ Topic.validates_length_of :title, minimum: 3, maximum: 5, too_short: "too short", too_long: "too long"
- t = Topic.new(title: 'a')
- assert t.invalid?
- assert t.errors[:title].any?
- assert_equal ['too short'], t.errors['title']
+ t = Topic.new(title: "a")
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
+ assert_equal ["too short"], t.errors["title"]
- t = Topic.new(title: 'aaaaaa')
- assert t.invalid?
- assert t.errors[:title].any?
- assert_equal ['too long'], t.errors['title']
+ t = Topic.new(title: "aaaaaa")
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
+ assert_equal ["too long"], t.errors["title"]
end
def test_validates_length_of_custom_errors_for_is_with_message
- Topic.validates_length_of( :title, is: 5, message: "boo %{count}" )
+ Topic.validates_length_of(:title, is: 5, message: "boo %{count}")
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["boo 5"], t.errors["title"]
end
def test_validates_length_of_custom_errors_for_is_with_wrong_length
- Topic.validates_length_of( :title, is: 5, wrong_length: "hoo %{count}" )
+ Topic.validates_length_of(:title, is: 5, wrong_length: "hoo %{count}")
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["hoo 5"], t.errors["title"]
end
@@ -261,11 +263,11 @@ class LengthValidationTest < ActiveModel::TestCase
Topic.validates_length_of :title, minimum: 5
t = Topic.new("title" => "一二三四五", "content" => "whatever")
- assert t.valid?
+ assert_predicate t, :valid?
t.title = "一二三四"
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["is too short (minimum is 5 characters)"], t.errors["title"]
end
@@ -273,11 +275,11 @@ class LengthValidationTest < ActiveModel::TestCase
Topic.validates_length_of :title, maximum: 5
t = Topic.new("title" => "一二三四五", "content" => "whatever")
- assert t.valid?
+ assert_predicate t, :valid?
t.title = "一二34五六"
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["is too long (maximum is 5 characters)"], t.errors["title"]
end
@@ -285,12 +287,12 @@ class LengthValidationTest < ActiveModel::TestCase
Topic.validates_length_of(:title, :content, within: 3..5)
t = Topic.new("title" => "一二", "content" => "12三四五六七")
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal ["is too short (minimum is 3 characters)"], t.errors[:title]
assert_equal ["is too long (maximum is 5 characters)"], t.errors[:content]
t.title = "一二三"
- t.content = "12三"
- assert t.valid?
+ t.content = "12三"
+ assert_predicate t, :valid?
end
def test_optionally_validates_length_of_using_within_utf8
@@ -310,60 +312,23 @@ class LengthValidationTest < ActiveModel::TestCase
Topic.validates_length_of :title, is: 5
t = Topic.new("title" => "一二345", "content" => "whatever")
- assert t.valid?
+ assert_predicate t, :valid?
t.title = "一二345六"
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["is the wrong length (should be 5 characters)"], t.errors["title"]
end
- def test_validates_length_of_with_block
- assert_deprecated do
- Topic.validates_length_of(
- :content,
- minimum: 5,
- too_short: "Your essay must be at least %{count} words.",
- tokenizer: lambda {|str| str.scan(/\w+/) },
- )
- end
- t = Topic.new(content: "this content should be long enough")
- assert t.valid?
-
- t.content = "not long enough"
- assert t.invalid?
- assert t.errors[:content].any?
- assert_equal ["Your essay must be at least 5 words."], t.errors[:content]
- end
-
-
- def test_validates_length_of_with_symbol
- assert_deprecated do
- Topic.validates_length_of(
- :content,
- minimum: 5,
- too_short: "Your essay must be at least %{count} words.",
- tokenizer: :my_word_tokenizer,
- )
- end
- t = Topic.new(content: "this content should be long enough")
- assert t.valid?
-
- t.content = "not long enough"
- assert t.invalid?
- assert t.errors[:content].any?
- assert_equal ["Your essay must be at least 5 words."], t.errors[:content]
- end
-
- def test_validates_length_of_for_fixnum
+ def test_validates_length_of_for_integer
Topic.validates_length_of(:approved, is: 4)
t = Topic.new("title" => "uhohuhoh", "content" => "whatever", approved: 1)
- assert t.invalid?
- assert t.errors[:approved].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:approved], :any?
t = Topic.new("title" => "uhohuhoh", "content" => "whatever", approved: 1234)
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_length_of_for_ruby_class
@@ -371,12 +336,12 @@ class LengthValidationTest < ActiveModel::TestCase
p = Person.new
p.karma = "Pix"
- assert p.invalid?
+ assert_predicate p, :invalid?
assert_equal ["is too short (minimum is 5 characters)"], p.errors[:karma]
p.karma = "The Smiths"
- assert p.valid?
+ assert_predicate p, :valid?
ensure
Person.clear_validators!
end
@@ -385,64 +350,95 @@ class LengthValidationTest < ActiveModel::TestCase
Topic.validates_length_of(:title, within: 5..Float::INFINITY)
t = Topic.new("title" => "1234")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
t.title = "12345"
- assert t.valid?
+ assert_predicate t, :valid?
Topic.validates_length_of(:author_name, maximum: Float::INFINITY)
- assert t.valid?
+ assert_predicate t, :valid?
t.author_name = "A very long author name that should still be valid." * 100
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_length_of_using_maximum_should_not_allow_nil_when_nil_not_allowed
Topic.validates_length_of :title, maximum: 10, allow_nil: false
t = Topic.new
- assert t.invalid?
+ assert_predicate t, :invalid?
end
def test_validates_length_of_using_maximum_should_not_allow_nil_and_empty_string_when_blank_not_allowed
Topic.validates_length_of :title, maximum: 10, allow_blank: false
t = Topic.new
- assert t.invalid?
+ assert_predicate t, :invalid?
t.title = ""
- assert t.invalid?
+ assert_predicate t, :invalid?
end
def test_validates_length_of_using_both_minimum_and_maximum_should_not_allow_nil
Topic.validates_length_of :title, minimum: 5, maximum: 10
t = Topic.new
- assert t.invalid?
+ assert_predicate t, :invalid?
end
def test_validates_length_of_using_minimum_0_should_not_allow_nil
Topic.validates_length_of :title, minimum: 0
t = Topic.new
- assert t.invalid?
+ assert_predicate t, :invalid?
t.title = ""
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_length_of_using_is_0_should_not_allow_nil
Topic.validates_length_of :title, is: 0
t = Topic.new
- assert t.invalid?
+ assert_predicate t, :invalid?
t.title = ""
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_with_diff_in_option
Topic.validates_length_of(:title, is: 5)
- Topic.validates_length_of(:title, is: 5, if: Proc.new { false } )
+ Topic.validates_length_of(:title, is: 5, if: Proc.new { false })
+
+ assert_predicate Topic.new("title" => "david"), :valid?
+ assert_predicate Topic.new("title" => "david2"), :invalid?
+ end
+
+ def test_validates_length_of_using_proc_as_maximum
+ Topic.validates_length_of :title, maximum: ->(model) { 5 }
+
+ t = Topic.new("title" => "valid", "content" => "whatever")
+ assert_predicate t, :valid?
+
+ t.title = "notvalid"
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
+ assert_equal ["is too long (maximum is 5 characters)"], t.errors[:title]
+
+ t.title = ""
+ assert_predicate t, :valid?
+ end
- assert Topic.new("title" => "david").valid?
- assert Topic.new("title" => "david2").invalid?
+ def test_validates_length_of_using_proc_as_maximum_with_model_method
+ Topic.send(:define_method, :max_title_length, lambda { 5 })
+ Topic.validates_length_of :title, maximum: Proc.new(&:max_title_length)
+
+ t = Topic.new("title" => "valid", "content" => "whatever")
+ assert_predicate t, :valid?
+
+ t.title = "notvalid"
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
+ assert_equal ["is too long (maximum is 5 characters)"], t.errors[:title]
+
+ t.title = ""
+ assert_predicate t, :valid?
end
end
diff --git a/activemodel/test/cases/validations/numericality_validation_test.rb b/activemodel/test/cases/validations/numericality_validation_test.rb
index 05432abaff..ca3c3bc40d 100644
--- a/activemodel/test/cases/validations/numericality_validation_test.rb
+++ b/activemodel/test/cases/validations/numericality_validation_test.rb
@@ -1,12 +1,14 @@
-require 'cases/helper'
+# frozen_string_literal: true
-require 'models/topic'
-require 'models/person'
+require "cases/helper"
-require 'bigdecimal'
+require "models/topic"
+require "models/person"
-class NumericalityValidationTest < ActiveModel::TestCase
+require "bigdecimal"
+require "active_support/core_ext/big_decimal"
+class NumericalityValidationTest < ActiveModel::TestCase
def teardown
Topic.clear_validators!
end
@@ -18,9 +20,9 @@ class NumericalityValidationTest < ActiveModel::TestCase
INTEGER_STRINGS = %w(0 +0 -0 10 +10 -10 0090 -090)
FLOATS = [0.0, 10.0, 10.5, -10.5, -0.0001] + FLOAT_STRINGS
INTEGERS = [0, 10, -10] + INTEGER_STRINGS
- BIGDECIMAL = BIGDECIMAL_STRINGS.collect! { |bd| BigDecimal.new(bd) }
+ BIGDECIMAL = BIGDECIMAL_STRINGS.collect! { |bd| BigDecimal(bd) }
JUNK = ["not a number", "42 not a number", "0xdeadbeef", "0xinvalidhex", "0Xdeadbeef", "00-1", "--3", "+-3", "+3-1", "-+019.0", "12.12.13.12", "123\nnot a number"]
- INFINITY = [1.0/0.0]
+ INFINITY = [1.0 / 0.0]
def test_default_validates_numericality_of
Topic.validates_numericality_of :approved
@@ -35,6 +37,13 @@ class NumericalityValidationTest < ActiveModel::TestCase
valid!(NIL + FLOATS + INTEGERS + BIGDECIMAL + INFINITY)
end
+ def test_validates_numericality_of_with_blank_allowed
+ Topic.validates_numericality_of :approved, allow_blank: true
+
+ invalid!(JUNK)
+ valid!(NIL + BLANK + FLOATS + INTEGERS + BIGDECIMAL + INFINITY)
+ end
+
def test_validates_numericality_of_with_integer_only
Topic.validates_numericality_of :approved, only_integer: true
@@ -50,7 +59,7 @@ class NumericalityValidationTest < ActiveModel::TestCase
end
def test_validates_numericality_of_with_integer_only_and_symbol_as_value
- Topic.validates_numericality_of :approved, only_integer: :condition_is_true_but_its_not
+ Topic.validates_numericality_of :approved, only_integer: :condition_is_false
invalid!(NIL + BLANK + JUNK)
valid!(FLOATS + INTEGERS + BIGDECIMAL + INFINITY)
@@ -67,49 +76,119 @@ class NumericalityValidationTest < ActiveModel::TestCase
def test_validates_numericality_with_greater_than
Topic.validates_numericality_of :approved, greater_than: 10
- invalid!([-10, 10], 'must be greater than 10')
+ invalid!([-10, 10], "must be greater than 10")
valid!([11])
end
+ def test_validates_numericality_with_greater_than_using_differing_numeric_types
+ Topic.validates_numericality_of :approved, greater_than: BigDecimal("97.18")
+
+ invalid!([-97.18, BigDecimal("97.18"), BigDecimal("-97.18")], "must be greater than 97.18")
+ valid!([97.19, 98, BigDecimal("98"), BigDecimal("97.19")])
+ end
+
+ def test_validates_numericality_with_greater_than_using_string_value
+ Topic.validates_numericality_of :approved, greater_than: 10
+
+ invalid!(["-10", "9", "9.9", "10"], "must be greater than 10")
+ valid!(["10.1", "11"])
+ end
+
def test_validates_numericality_with_greater_than_or_equal
Topic.validates_numericality_of :approved, greater_than_or_equal_to: 10
- invalid!([-9, 9], 'must be greater than or equal to 10')
+ invalid!([-9, 9], "must be greater than or equal to 10")
valid!([10])
end
+ def test_validates_numericality_with_greater_than_or_equal_using_differing_numeric_types
+ Topic.validates_numericality_of :approved, greater_than_or_equal_to: BigDecimal("97.18")
+
+ invalid!([-97.18, 97.17, 97, BigDecimal("97.17"), BigDecimal("-97.18")], "must be greater than or equal to 97.18")
+ valid!([97.18, 98, BigDecimal("97.19")])
+ end
+
+ def test_validates_numericality_with_greater_than_or_equal_using_string_value
+ Topic.validates_numericality_of :approved, greater_than_or_equal_to: 10
+
+ invalid!(["-10", "9", "9.9"], "must be greater than or equal to 10")
+ valid!(["10", "10.1", "11"])
+ end
+
def test_validates_numericality_with_equal_to
Topic.validates_numericality_of :approved, equal_to: 10
- invalid!([-10, 11] + INFINITY, 'must be equal to 10')
+ invalid!([-10, 11] + INFINITY, "must be equal to 10")
valid!([10])
end
+ def test_validates_numericality_with_equal_to_using_differing_numeric_types
+ Topic.validates_numericality_of :approved, equal_to: BigDecimal("97.18")
+
+ invalid!([-97.18], "must be equal to 97.18")
+ valid!([BigDecimal("97.18")])
+ end
+
+ def test_validates_numericality_with_equal_to_using_string_value
+ Topic.validates_numericality_of :approved, equal_to: 10
+
+ invalid!(["-10", "9", "9.9", "10.1", "11"], "must be equal to 10")
+ valid!(["10"])
+ end
+
def test_validates_numericality_with_less_than
Topic.validates_numericality_of :approved, less_than: 10
- invalid!([10], 'must be less than 10')
+ invalid!([10], "must be less than 10")
valid!([-9, 9])
end
+ def test_validates_numericality_with_less_than_using_differing_numeric_types
+ Topic.validates_numericality_of :approved, less_than: BigDecimal("97.18")
+
+ invalid!([97.18, BigDecimal("97.18")], "must be less than 97.18")
+ valid!([-97.0, 97.0, -97, 97, BigDecimal("-97"), BigDecimal("97")])
+ end
+
+ def test_validates_numericality_with_less_than_using_string_value
+ Topic.validates_numericality_of :approved, less_than: 10
+
+ invalid!(["10", "10.1", "11"], "must be less than 10")
+ valid!(["-10", "9", "9.9"])
+ end
+
def test_validates_numericality_with_less_than_or_equal_to
Topic.validates_numericality_of :approved, less_than_or_equal_to: 10
- invalid!([11], 'must be less than or equal to 10')
+ invalid!([11], "must be less than or equal to 10")
valid!([-10, 10])
end
+ def test_validates_numericality_with_less_than_or_equal_to_using_differing_numeric_types
+ Topic.validates_numericality_of :approved, less_than_or_equal_to: BigDecimal("97.18")
+
+ invalid!([97.19, 98], "must be less than or equal to 97.18")
+ valid!([-97.18, BigDecimal("-97.18"), BigDecimal("97.18")])
+ end
+
+ def test_validates_numericality_with_less_than_or_equal_using_string_value
+ Topic.validates_numericality_of :approved, less_than_or_equal_to: 10
+
+ invalid!(["10.1", "11"], "must be less than or equal to 10")
+ valid!(["-10", "9", "9.9", "10"])
+ end
+
def test_validates_numericality_with_odd
Topic.validates_numericality_of :approved, odd: true
- invalid!([-2, 2], 'must be odd')
+ invalid!([-2, 2], "must be odd")
valid!([-1, 1])
end
def test_validates_numericality_with_even
Topic.validates_numericality_of :approved, even: true
- invalid!([-1, 1], 'must be even')
+ invalid!([-1, 1], "must be even")
valid!([-2, 2])
end
@@ -127,6 +206,13 @@ class NumericalityValidationTest < ActiveModel::TestCase
valid!([-1, 42])
end
+ def test_validates_numericality_with_other_than_using_string_value
+ Topic.validates_numericality_of :approved, other_than: 0
+
+ invalid!(["0", "0.0"])
+ valid!(["-1", "1.1", "42"])
+ end
+
def test_validates_numericality_with_proc
Topic.send(:define_method, :min_approved, lambda { 5 })
Topic.validates_numericality_of :approved, greater_than_or_equal_to: Proc.new(&:min_approved)
@@ -151,13 +237,13 @@ class NumericalityValidationTest < ActiveModel::TestCase
Topic.validates_numericality_of :approved, less_than: 4, message: "smaller than %{count}"
topic = Topic.new("title" => "numeric test", "approved" => 10)
- assert !topic.valid?
+ assert_not_predicate topic, :valid?
assert_equal ["smaller than 4"], topic.errors[:approved]
Topic.validates_numericality_of :approved, greater_than: 4, message: "greater than %{count}"
topic = Topic.new("title" => "numeric test", "approved" => 1)
- assert !topic.valid?
+ assert_not_predicate topic, :valid?
assert_equal ["greater than 4"], topic.errors[:approved]
end
@@ -166,45 +252,64 @@ class NumericalityValidationTest < ActiveModel::TestCase
p = Person.new
p.karma = "Pix"
- assert p.invalid?
+ assert_predicate p, :invalid?
assert_equal ["is not a number"], p.errors[:karma]
p.karma = "1234"
- assert p.valid?
+ assert_predicate p, :valid?
ensure
Person.clear_validators!
end
+ def test_validates_numericality_using_value_before_type_cast_if_possible
+ Topic.validates_numericality_of :price
+
+ topic = Topic.new(price: 50)
+
+ assert_equal "$50.00", topic.price
+ assert_equal 50, topic.price_before_type_cast
+ assert_predicate topic, :valid?
+ 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_predicate 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" }
- assert_raise(ArgumentError){ Topic.validates_numericality_of :approved, greater_than: "foo" }
- assert_raise(ArgumentError){ Topic.validates_numericality_of :approved, less_than: "foo" }
- assert_raise(ArgumentError){ Topic.validates_numericality_of :approved, equal_to: "foo" }
+ 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" }
+ assert_raise(ArgumentError) { Topic.validates_numericality_of :approved, greater_than: "foo" }
+ assert_raise(ArgumentError) { Topic.validates_numericality_of :approved, less_than: "foo" }
+ assert_raise(ArgumentError) { Topic.validates_numericality_of :approved, equal_to: "foo" }
end
private
- def invalid!(values, error = nil)
- with_each_topic_approved_value(values) do |topic, value|
- assert topic.invalid?, "#{value.inspect} not rejected as a number"
- assert topic.errors[:approved].any?, "FAILED for #{value.inspect}"
- assert_equal error, topic.errors[:approved].first if error
+ def invalid!(values, error = nil)
+ with_each_topic_approved_value(values) do |topic, value|
+ assert topic.invalid?, "#{value.inspect} not rejected as a number"
+ assert topic.errors[:approved].any?, "FAILED for #{value.inspect}"
+ assert_equal error, topic.errors[:approved].first if error
+ end
end
- end
- def valid!(values)
- with_each_topic_approved_value(values) do |topic, value|
- assert topic.valid?, "#{value.inspect} not accepted as a number"
+ def valid!(values)
+ with_each_topic_approved_value(values) do |topic, value|
+ assert topic.valid?, "#{value.inspect} not accepted as a number with validation error: #{topic.errors[:approved].first}"
+ end
end
- end
- def with_each_topic_approved_value(values)
- topic = Topic.new(title: "numeric test", content: "whatever")
- values.each do |value|
- topic.approved = value
- yield topic, value
+ def with_each_topic_approved_value(values)
+ topic = Topic.new(title: "numeric test", content: "whatever")
+ values.each do |value|
+ topic.approved = value
+ yield topic, value
+ end
end
- end
end
diff --git a/activemodel/test/cases/validations/presence_validation_test.rb b/activemodel/test/cases/validations/presence_validation_test.rb
index 59b9db0795..c3eca41070 100644
--- a/activemodel/test/cases/validations/presence_validation_test.rb
+++ b/activemodel/test/cases/validations/presence_validation_test.rb
@@ -1,11 +1,12 @@
-require 'cases/helper'
+# frozen_string_literal: true
-require 'models/topic'
-require 'models/person'
-require 'models/custom_reader'
+require "cases/helper"
-class PresenceValidationTest < ActiveModel::TestCase
+require "models/topic"
+require "models/person"
+require "models/custom_reader"
+class PresenceValidationTest < ActiveModel::TestCase
teardown do
Topic.clear_validators!
Person.clear_validators!
@@ -16,25 +17,25 @@ class PresenceValidationTest < ActiveModel::TestCase
Topic.validates_presence_of(:title, :content)
t = Topic.new
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal ["can't be blank"], t.errors[:title]
assert_equal ["can't be blank"], t.errors[:content]
t.title = "something"
- t.content = " "
+ t.content = " "
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal ["can't be blank"], t.errors[:content]
t.content = "like stuff"
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_accepts_array_arguments
Topic.validates_presence_of %w(title content)
t = Topic.new
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal ["can't be blank"], t.errors[:title]
assert_equal ["can't be blank"], t.errors[:content]
end
@@ -42,7 +43,7 @@ class PresenceValidationTest < ActiveModel::TestCase
def test_validates_acceptance_of_with_custom_error_using_quotes
Person.validates_presence_of :karma, message: "This string contains 'single' and \"double\" quotes"
p = Person.new
- assert p.invalid?
+ assert_predicate p, :invalid?
assert_equal "This string contains 'single' and \"double\" quotes", p.errors[:karma].last
end
@@ -50,24 +51,24 @@ class PresenceValidationTest < ActiveModel::TestCase
Person.validates_presence_of :karma
p = Person.new
- assert p.invalid?
+ assert_predicate p, :invalid?
assert_equal ["can't be blank"], p.errors[:karma]
p.karma = "Cold"
- assert p.valid?
+ assert_predicate p, :valid?
end
def test_validates_presence_of_for_ruby_class_with_custom_reader
CustomReader.validates_presence_of :karma
p = CustomReader.new
- assert p.invalid?
+ assert_predicate p, :invalid?
assert_equal ["can't be blank"], p.errors[:karma]
p[:karma] = "Cold"
- assert p.valid?
+ assert_predicate p, :valid?
end
def test_validates_presence_of_with_allow_nil_option
@@ -77,7 +78,7 @@ class PresenceValidationTest < ActiveModel::TestCase
assert t.valid?, t.errors.full_messages
t.title = ""
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal ["can't be blank"], t.errors[:title]
t.title = " "
diff --git a/activemodel/test/cases/validations/validates_test.rb b/activemodel/test/cases/validations/validates_test.rb
index 04101f3545..ae5a875c24 100644
--- a/activemodel/test/cases/validations/validates_test.rb
+++ b/activemodel/test/cases/validations/validates_test.rb
@@ -1,8 +1,10 @@
-require 'cases/helper'
-require 'models/person'
-require 'models/topic'
-require 'models/person_with_validator'
-require 'validators/namespace/email_validator'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/person"
+require "models/topic"
+require "models/person_with_validator"
+require "validators/namespace/email_validator"
class ValidatesTest < ActiveModel::TestCase
setup :reset_callbacks
@@ -17,106 +19,112 @@ class ValidatesTest < ActiveModel::TestCase
def test_validates_with_messages_empty
Person.validates :title, presence: { message: "" }
person = Person.new
- assert !person.valid?, 'person should not be valid.'
+ assert_not person.valid?, "person should not be valid."
end
def test_validates_with_built_in_validation
Person.validates :title, numericality: true
person = Person.new
person.valid?
- assert_equal ['is not a number'], person.errors[:title]
+ assert_equal ["is not a number"], person.errors[:title]
end
def test_validates_with_attribute_specified_as_string
Person.validates "title", numericality: true
person = Person.new
person.valid?
- assert_equal ['is not a number'], person.errors[:title]
+ assert_equal ["is not a number"], person.errors[:title]
person = Person.new
person.title = 123
- assert person.valid?
+ assert_predicate person, :valid?
end
def test_validates_with_built_in_validation_and_options
- Person.validates :salary, numericality: { message: 'my custom message' }
+ Person.validates :salary, numericality: { message: "my custom message" }
person = Person.new
person.valid?
- assert_equal ['my custom message'], person.errors[:salary]
+ assert_equal ["my custom message"], person.errors[:salary]
end
def test_validates_with_validator_class
Person.validates :karma, email: true
person = Person.new
person.valid?
- assert_equal ['is not an email'], person.errors[:karma]
+ assert_equal ["is not an email"], person.errors[:karma]
end
def test_validates_with_namespaced_validator_class
- Person.validates :karma, :'namespace/email' => true
+ Person.validates :karma, 'namespace/email': true
person = Person.new
person.valid?
- assert_equal ['is not an email'], person.errors[:karma]
+ assert_equal ["is not an email"], person.errors[:karma]
end
def test_validates_with_if_as_local_conditions
- Person.validates :karma, presence: true, email: { unless: :condition_is_true }
+ Person.validates :karma, presence: true, email: { if: :condition_is_false }
person = Person.new
person.valid?
assert_equal ["can't be blank"], person.errors[:karma]
end
def test_validates_with_if_as_shared_conditions
- Person.validates :karma, presence: true, email: true, if: :condition_is_true
+ Person.validates :karma, presence: true, email: true, if: :condition_is_false
+ person = Person.new
+ assert_predicate person, :valid?
+ end
+
+ def test_validates_with_unless_as_local_conditions
+ Person.validates :karma, presence: true, email: { unless: :condition_is_true }
person = Person.new
person.valid?
- assert_equal ["can't be blank", "is not an email"], person.errors[:karma].sort
+ assert_equal ["can't be blank"], person.errors[:karma]
end
def test_validates_with_unless_shared_conditions
Person.validates :karma, presence: true, email: true, unless: :condition_is_true
person = Person.new
- assert person.valid?
+ assert_predicate person, :valid?
end
def test_validates_with_allow_nil_shared_conditions
Person.validates :karma, length: { minimum: 20 }, email: true, allow_nil: true
person = Person.new
- assert person.valid?
+ assert_predicate person, :valid?
end
def test_validates_with_regexp
Person.validates :karma, format: /positive|negative/
person = Person.new
- assert person.invalid?
- assert_equal ['is invalid'], person.errors[:karma]
+ assert_predicate person, :invalid?
+ assert_equal ["is invalid"], person.errors[:karma]
person.karma = "positive"
- assert person.valid?
+ assert_predicate person, :valid?
end
def test_validates_with_array
Person.validates :gender, inclusion: %w(m f)
person = Person.new
- assert person.invalid?
- assert_equal ['is not included in the list'], person.errors[:gender]
+ assert_predicate person, :invalid?
+ assert_equal ["is not included in the list"], person.errors[:gender]
person.gender = "m"
- assert person.valid?
+ assert_predicate person, :valid?
end
def test_validates_with_range
Person.validates :karma, length: 6..20
person = Person.new
- assert person.invalid?
- assert_equal ['is too short (minimum is 6 characters)'], person.errors[:karma]
- person.karma = 'something'
- assert person.valid?
+ assert_predicate person, :invalid?
+ assert_equal ["is too short (minimum is 6 characters)"], person.errors[:karma]
+ person.karma = "something"
+ assert_predicate person, :valid?
end
def test_validates_with_validator_class_and_options
- Person.validates :karma, email: { message: 'my custom message' }
+ Person.validates :karma, email: { message: "my custom message" }
person = Person.new
person.valid?
- assert_equal ['my custom message'], person.errors[:karma]
+ assert_equal ["my custom message"], person.errors[:karma]
end
def test_validates_with_unknown_validator
@@ -127,14 +135,14 @@ class ValidatesTest < ActiveModel::TestCase
PersonWithValidator.validates :title, presence: true
person = PersonWithValidator.new
person.valid?
- assert_equal ['Local validator'], person.errors[:title]
+ assert_equal ["Local validator"], person.errors[:title]
end
def test_validates_with_included_validator_and_options
- PersonWithValidator.validates :title, presence: { custom: ' please' }
+ PersonWithValidator.validates :title, presence: { custom: " please" }
person = PersonWithValidator.new
person.valid?
- assert_equal ['Local validator please'], person.errors[:title]
+ assert_equal ["Local validator please"], person.errors[:title]
end
def test_validates_with_included_validator_and_wildcard_shortcut
@@ -143,15 +151,15 @@ class ValidatesTest < ActiveModel::TestCase
person = PersonWithValidator.new
person.title = "Ms. Pacman"
person.valid?
- assert_equal ['does not appear to be like Mr.'], person.errors[:title]
+ assert_equal ["does not appear to be like Mr."], person.errors[:title]
end
def test_defining_extra_default_keys_for_validates
- Topic.validates :title, confirmation: true, message: 'Y U NO CONFIRM'
+ Topic.validates :title, confirmation: true, message: "Y U NO CONFIRM"
topic = Topic.new
topic.title = "What's happening"
topic.title_confirmation = "Not this"
- assert !topic.valid?
- assert_equal ['Y U NO CONFIRM'], topic.errors[:title_confirmation]
+ assert_not_predicate topic, :valid?
+ assert_equal ["Y U NO CONFIRM"], topic.errors[:title_confirmation]
end
end
diff --git a/activemodel/test/cases/validations/validations_context_test.rb b/activemodel/test/cases/validations/validations_context_test.rb
index 150dce379f..024eb1882f 100644
--- a/activemodel/test/cases/validations/validations_context_test.rb
+++ b/activemodel/test/cases/validations/validations_context_test.rb
@@ -1,6 +1,8 @@
-require 'cases/helper'
+# frozen_string_literal: true
-require 'models/topic'
+require "cases/helper"
+
+require "models/topic"
class ValidationsContextTest < ActiveModel::TestCase
def teardown
@@ -8,6 +10,7 @@ class ValidationsContextTest < ActiveModel::TestCase
end
ERROR_MESSAGE = "Validation error from validator"
+ ANOTHER_ERROR_MESSAGE = "Another validation error from validator"
class ValidatorThatAddsErrors < ActiveModel::Validator
def validate(record)
@@ -15,6 +18,12 @@ class ValidationsContextTest < ActiveModel::TestCase
end
end
+ class AnotherValidatorThatAddsErrors < ActiveModel::Validator
+ def validate(record)
+ record.errors[:base] << ANOTHER_ERROR_MESSAGE
+ end
+ end
+
test "with a class that adds errors on create and validating a new model with no arguments" do
Topic.validates_with(ValidatorThatAddsErrors, on: :create)
topic = Topic.new
@@ -31,7 +40,7 @@ class ValidationsContextTest < ActiveModel::TestCase
Topic.validates_with(ValidatorThatAddsErrors, on: :create)
topic = Topic.new
assert topic.invalid?(:create), "Validation does run on create if 'on' is set to create"
- assert topic.errors[:base].include?(ERROR_MESSAGE)
+ assert_includes topic.errors[:base], ERROR_MESSAGE
end
test "with a class that adds errors on multiple contexts and validating a new model" do
@@ -41,9 +50,21 @@ class ValidationsContextTest < ActiveModel::TestCase
assert topic.valid?, "Validation ran with no context given when 'on' is set to context1 and context2"
assert topic.invalid?(:context1), "Validation did not run on context1 when 'on' is set to context1 and context2"
- assert topic.errors[:base].include?(ERROR_MESSAGE)
+ assert_includes topic.errors[:base], ERROR_MESSAGE
assert topic.invalid?(:context2), "Validation did not run on context2 when 'on' is set to context1 and context2"
- assert topic.errors[:base].include?(ERROR_MESSAGE)
+ assert_includes topic.errors[:base], ERROR_MESSAGE
+ end
+
+ test "with a class that validating a model for a multiple contexts" do
+ Topic.validates_with(ValidatorThatAddsErrors, on: :context1)
+ Topic.validates_with(AnotherValidatorThatAddsErrors, on: :context2)
+
+ topic = Topic.new
+ assert topic.valid?, "Validation ran with no context given when 'on' is set to context1 and context2"
+
+ assert topic.invalid?([:context1, :context2]), "Validation did not run on context1 when 'on' is set to context1 and context2"
+ assert_includes topic.errors[:base], ERROR_MESSAGE
+ assert_includes topic.errors[:base], ANOTHER_ERROR_MESSAGE
end
end
diff --git a/activemodel/test/cases/validations/with_validation_test.rb b/activemodel/test/cases/validations/with_validation_test.rb
index 03c7943308..8239792c79 100644
--- a/activemodel/test/cases/validations/with_validation_test.rb
+++ b/activemodel/test/cases/validations/with_validation_test.rb
@@ -1,9 +1,10 @@
-require 'cases/helper'
+# frozen_string_literal: true
-require 'models/topic'
+require "cases/helper"
-class ValidatesWithTest < ActiveModel::TestCase
+require "models/topic"
+class ValidatesWithTest < ActiveModel::TestCase
def teardown
Topic.clear_validators!
end
@@ -52,7 +53,7 @@ class ValidatesWithTest < ActiveModel::TestCase
Topic.validates_with(ValidatorThatAddsErrors)
topic = Topic.new
assert topic.invalid?, "A class that adds errors causes the record to be invalid"
- assert topic.errors[:base].include?(ERROR_MESSAGE)
+ assert_includes topic.errors[:base], ERROR_MESSAGE
end
test "with a class that returns valid" do
@@ -64,60 +65,35 @@ class ValidatesWithTest < ActiveModel::TestCase
test "with multiple classes" do
Topic.validates_with(ValidatorThatAddsErrors, OtherValidatorThatAddsErrors)
topic = Topic.new
- assert topic.invalid?
- assert topic.errors[:base].include?(ERROR_MESSAGE)
- assert topic.errors[:base].include?(OTHER_ERROR_MESSAGE)
- end
-
- test "with if statements that return false" do
- Topic.validates_with(ValidatorThatAddsErrors, if: "1 == 2")
- topic = Topic.new
- assert topic.valid?
- end
-
- test "with if statements that return true" do
- Topic.validates_with(ValidatorThatAddsErrors, if: "1 == 1")
- topic = Topic.new
- assert topic.invalid?
- assert topic.errors[:base].include?(ERROR_MESSAGE)
- end
-
- test "with unless statements that return true" do
- Topic.validates_with(ValidatorThatAddsErrors, unless: "1 == 1")
- topic = Topic.new
- assert topic.valid?
- end
-
- test "with unless statements that returns false" do
- Topic.validates_with(ValidatorThatAddsErrors, unless: "1 == 2")
- topic = Topic.new
- assert topic.invalid?
- assert topic.errors[:base].include?(ERROR_MESSAGE)
+ assert_predicate topic, :invalid?
+ assert_includes topic.errors[:base], ERROR_MESSAGE
+ assert_includes topic.errors[:base], OTHER_ERROR_MESSAGE
end
test "passes all configuration options to the validator class" do
topic = Topic.new
validator = Minitest::Mock.new
- validator.expect(:new, validator, [{foo: :bar, if: "1 == 1", class: Topic}])
+ validator.expect(:new, validator, [{ foo: :bar, if: :condition_is_true, class: Topic }])
validator.expect(:validate, nil, [topic])
validator.expect(:is_a?, false, [Symbol])
+ validator.expect(:is_a?, false, [String])
- Topic.validates_with(validator, if: "1 == 1", foo: :bar)
- assert topic.valid?
+ Topic.validates_with(validator, if: :condition_is_true, foo: :bar)
+ assert_predicate topic, :valid?
validator.verify
end
test "validates_with with options" do
Topic.validates_with(ValidatorThatValidatesOptions, field: :first_name)
topic = Topic.new
- assert topic.invalid?
- assert topic.errors[:base].include?(ERROR_MESSAGE)
+ assert_predicate topic, :invalid?
+ assert_includes topic.errors[:base], ERROR_MESSAGE
end
test "validates_with each validator" do
Topic.validates_with(ValidatorPerEachAttribute, attributes: [:title, :content])
topic = Topic.new title: "Title", content: "Content"
- assert topic.invalid?
+ assert_predicate topic, :invalid?
assert_equal ["Value is Title"], topic.errors[:title]
assert_equal ["Value is Content"], topic.errors[:content]
end
@@ -137,37 +113,37 @@ class ValidatesWithTest < ActiveModel::TestCase
test "each validator skip nil values if :allow_nil is set to true" do
Topic.validates_with(ValidatorPerEachAttribute, attributes: [:title, :content], allow_nil: true)
topic = Topic.new content: ""
- assert topic.invalid?
- assert topic.errors[:title].empty?
+ assert_predicate topic, :invalid?
+ assert_empty topic.errors[:title]
assert_equal ["Value is "], topic.errors[:content]
end
test "each validator skip blank values if :allow_blank is set to true" do
Topic.validates_with(ValidatorPerEachAttribute, attributes: [:title, :content], allow_blank: true)
topic = Topic.new content: ""
- assert topic.valid?
- assert topic.errors[:title].empty?
- assert topic.errors[:content].empty?
+ assert_predicate topic, :valid?
+ assert_empty topic.errors[:title]
+ assert_empty topic.errors[:content]
end
test "validates_with can validate with an instance method" do
Topic.validates :title, with: :my_validation
topic = Topic.new title: "foo"
- assert topic.valid?
- assert topic.errors[:title].empty?
+ assert_predicate topic, :valid?
+ assert_empty topic.errors[:title]
topic = Topic.new
- assert !topic.valid?
- assert_equal ['is missing'], topic.errors[:title]
+ assert_not_predicate topic, :valid?
+ assert_equal ["is missing"], topic.errors[:title]
end
test "optionally pass in the attribute being validated when validating with an instance method" do
Topic.validates :title, :content, with: :my_validation_with_arg
topic = Topic.new title: "foo"
- assert !topic.valid?
- assert topic.errors[:title].empty?
- assert_equal ['is missing'], topic.errors[:content]
+ assert_not_predicate topic, :valid?
+ assert_empty topic.errors[:title]
+ assert_equal ["is missing"], topic.errors[:content]
end
end
diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb
index f0317ad219..7776233db5 100644
--- a/activemodel/test/cases/validations_test.rb
+++ b/activemodel/test/cases/validations_test.rb
@@ -1,11 +1,13 @@
-require 'cases/helper'
+# frozen_string_literal: true
-require 'models/topic'
-require 'models/reply'
-require 'models/custom_reader'
+require "cases/helper"
-require 'active_support/json'
-require 'active_support/xml_mini'
+require "models/topic"
+require "models/reply"
+require "models/custom_reader"
+
+require "active_support/json"
+require "active_support/xml_mini"
class ValidationsTest < ActiveModel::TestCase
class CustomStrictValidationException < StandardError; end
@@ -28,7 +30,7 @@ class ValidationsTest < ActiveModel::TestCase
def test_single_attr_validation_and_error_msg
r = Reply.new
r.title = "There's no content!"
- assert r.invalid?
+ assert_predicate r, :invalid?
assert r.errors[:content].any?, "A reply without content should mark that attribute as invalid"
assert_equal ["is Empty"], r.errors["content"], "A reply without content should contain an error"
assert_equal 1, r.errors.count
@@ -36,7 +38,7 @@ class ValidationsTest < ActiveModel::TestCase
def test_double_attr_validation_and_error_msg
r = Reply.new
- assert r.invalid?
+ assert_predicate r, :invalid?
assert r.errors[:title].any?, "A reply without title should mark that attribute as invalid"
assert_equal ["is Empty"], r.errors["title"], "A reply without title should contain an error"
@@ -51,10 +53,10 @@ class ValidationsTest < ActiveModel::TestCase
r = Reply.new
r.valid?
- errors = r.errors.collect {|attr, messages| [attr.to_s, messages]}
+ errors = r.errors.collect { |attr, messages| [attr.to_s, messages] }
- assert errors.include?(["title", "is Empty"])
- assert errors.include?(["content", "is Empty"])
+ assert_includes errors, ["title", "is Empty"]
+ assert_includes errors, ["content", "is Empty"]
end
def test_multiple_errors_per_attr_iteration_with_full_error_composition
@@ -86,8 +88,8 @@ class ValidationsTest < ActiveModel::TestCase
assert_equal ["Reply is not dignifying"], r.errors[:base]
- assert errors.include?("Title is Empty")
- assert errors.include?("Reply is not dignifying")
+ assert_includes errors, "Title is Empty"
+ assert_includes errors, "Reply is not dignifying"
assert_equal 2, r.errors.count
end
@@ -101,26 +103,26 @@ class ValidationsTest < ActiveModel::TestCase
assert_equal ["is invalid"], r.errors[:base]
- assert errors.include?("Title is Empty")
- assert errors.include?("is invalid")
+ assert_includes errors, "Title is Empty"
+ assert_includes errors, "is invalid"
assert_equal 2, r.errors.count
end
def test_errors_empty_after_errors_on_check
t = Topic.new
- assert t.errors[:id].empty?
- assert t.errors.empty?
+ assert_empty t.errors[:id]
+ assert_empty t.errors
end
def test_validates_each
hits = 0
Topic.validates_each(:title, :content, [:title, :content]) do |record, attr|
- record.errors.add attr, 'gotcha'
+ record.errors.add attr, "gotcha"
hits += 1
end
t = Topic.new("title" => "valid", "content" => "whatever")
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal 4, hits
assert_equal %w(gotcha gotcha), t.errors[:title]
assert_equal %w(gotcha gotcha), t.errors[:content]
@@ -129,11 +131,11 @@ class ValidationsTest < ActiveModel::TestCase
def test_validates_each_custom_reader
hits = 0
CustomReader.validates_each(:title, :content, [:title, :content]) do |record, attr|
- record.errors.add attr, 'gotcha'
+ record.errors.add attr, "gotcha"
hits += 1
end
t = CustomReader.new("title" => "valid", "content" => "whatever")
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal 4, hits
assert_equal %w(gotcha gotcha), t.errors[:title]
assert_equal %w(gotcha gotcha), t.errors[:content]
@@ -144,16 +146,16 @@ class ValidationsTest < ActiveModel::TestCase
def test_validate_block
Topic.validate { errors.add("title", "will never be valid") }
t = Topic.new("title" => "Title", "content" => "whatever")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["will never be valid"], t.errors["title"]
end
def test_validate_block_with_params
Topic.validate { |topic| topic.errors.add("title", "will never be valid") }
t = Topic.new("title" => "Title", "content" => "whatever")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["will never be valid"], t.errors["title"]
end
@@ -170,7 +172,7 @@ class ValidationsTest < ActiveModel::TestCase
# A common mistake -- we meant to call 'validates'
Topic.validate :title, presence: true
end
- message = 'Unknown key: :presence. Valid keys are: :on, :if, :unless, :prepend. Perhaps you meant to call `validates` instead of `validate`?'
+ message = "Unknown key: :presence. Valid keys are: :on, :if, :unless, :prepend. Perhaps you meant to call `validates` instead of `validate`?"
assert_equal message, error.message
end
@@ -198,9 +200,9 @@ class ValidationsTest < ActiveModel::TestCase
end
assert_nothing_raised do
- klass.validate :validator_a, if: ->{ true }
+ klass.validate :validator_a, if: -> { true }
klass.validate :validator_b, prepend: true
- klass.validate :validator_c, unless: ->{ true }
+ klass.validate :validator_c, unless: -> { true }
end
t = klass.new
@@ -212,7 +214,7 @@ class ValidationsTest < ActiveModel::TestCase
def test_errors_conversions
Topic.validates_presence_of %w(title content)
t = Topic.new
- assert t.invalid?
+ assert_predicate t, :invalid?
xml = t.errors.to_xml
assert_match %r{<errors>}, xml
@@ -230,34 +232,34 @@ class ValidationsTest < ActiveModel::TestCase
Topic.validates_length_of :title, minimum: 2
t = Topic.new("title" => "")
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal "can't be blank", t.errors["title"].first
Topic.validates_presence_of :title, :author_name
- Topic.validate {errors.add('author_email_address', 'will never be valid')}
+ Topic.validate { errors.add("author_email_address", "will never be valid") }
Topic.validates_length_of :title, :content, minimum: 2
- t = Topic.new title: ''
- assert t.invalid?
+ t = Topic.new title: ""
+ assert_predicate t, :invalid?
assert_equal :title, key = t.errors.keys[0]
assert_equal "can't be blank", t.errors[key][0]
- assert_equal 'is too short (minimum is 2 characters)', t.errors[key][1]
+ assert_equal "is too short (minimum is 2 characters)", t.errors[key][1]
assert_equal :author_name, key = t.errors.keys[1]
assert_equal "can't be blank", t.errors[key][0]
assert_equal :author_email_address, key = t.errors.keys[2]
- assert_equal 'will never be valid', t.errors[key][0]
+ assert_equal "will never be valid", t.errors[key][0]
assert_equal :content, key = t.errors.keys[3]
- assert_equal 'is too short (minimum is 2 characters)', t.errors[key][0]
+ assert_equal "is too short (minimum is 2 characters)", t.errors[key][0]
end
def test_validation_with_if_and_on
- Topic.validates_presence_of :title, if: Proc.new{|x| x.author_name = "bad"; true }, on: :update
+ Topic.validates_presence_of :title, if: Proc.new { |x| x.author_name = "bad"; true }, on: :update
t = Topic.new(title: "")
# If block should not fire
- assert t.valid?
- assert t.author_name.nil?
+ assert_predicate t, :valid?
+ assert_predicate t.author_name, :nil?
# If block should fire
assert t.invalid?(:update)
@@ -268,18 +270,18 @@ class ValidationsTest < ActiveModel::TestCase
Topic.validates_presence_of :title
t = Topic.new
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
- t.title = 'Things are going to change'
- assert !t.invalid?
+ t.title = "Things are going to change"
+ assert_not_predicate t, :invalid?
end
def test_validation_with_message_as_proc
Topic.validates_presence_of(:title, message: proc { "no blanks here".upcase })
t = Topic.new
- assert t.invalid?
+ assert_predicate t, :invalid?
assert_equal ["NO BLANKS HERE"], t.errors[:title]
end
@@ -329,13 +331,13 @@ class ValidationsTest < ActiveModel::TestCase
Topic.validates :content, length: { minimum: 10 }
topic = Topic.new
- assert topic.invalid?
+ assert_predicate topic, :invalid?
assert_equal 3, topic.errors.size
- topic.title = 'Some Title'
- topic.author_name = 'Some Author'
- topic.content = 'Some Content Whose Length is more than 10.'
- assert topic.valid?
+ topic.title = "Some Title"
+ topic.author_name = "Some Author"
+ topic.content = "Some Content Whose Length is more than 10."
+ assert_predicate topic, :valid?
end
def test_validate
@@ -379,7 +381,7 @@ class ValidationsTest < ActiveModel::TestCase
def test_strict_validation_not_fails
Topic.validates :title, strict: true, presence: true
- assert Topic.new(title: "hello").valid?
+ assert_predicate Topic.new(title: "hello"), :valid?
end
def test_strict_validation_particular_validator
@@ -412,7 +414,7 @@ class ValidationsTest < ActiveModel::TestCase
def test_validates_with_false_hash_value
Topic.validates :title, presence: false
- assert Topic.new.valid?
+ assert_predicate Topic.new, :valid?
end
def test_strict_validation_error_message
@@ -437,11 +439,27 @@ class ValidationsTest < ActiveModel::TestCase
duped = topic.dup
duped.title = nil
- assert duped.invalid?
+ assert_predicate duped, :invalid?
topic.title = nil
- duped.title = 'Mathematics'
- assert topic.invalid?
- assert duped.valid?
+ duped.title = "Mathematics"
+ assert_predicate topic, :invalid?
+ assert_predicate duped, :valid?
+ end
+
+ def test_validation_with_message_as_proc_that_takes_a_record_as_a_parameter
+ Topic.validates_presence_of(:title, message: proc { |record| "You have failed me for the last time, #{record.author_name}." })
+
+ t = Topic.new(author_name: "Admiral")
+ assert_predicate t, :invalid?
+ assert_equal ["You have failed me for the last time, Admiral."], t.errors[:title]
+ end
+
+ def test_validation_with_message_as_proc_that_takes_record_and_data_as_a_parameters
+ Topic.validates_presence_of(:title, message: proc { |record, data| "#{data[:attribute]} is missing. You have failed me for the last time, #{record.author_name}." })
+
+ t = Topic.new(author_name: "Admiral")
+ assert_predicate t, :invalid?
+ assert_equal ["Title is missing. You have failed me for the last time, Admiral."], t.errors[:title]
end
end
diff --git a/activemodel/test/config.rb b/activemodel/test/config.rb
deleted file mode 100644
index 0b577a9936..0000000000
--- a/activemodel/test/config.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-TEST_ROOT = File.expand_path(File.dirname(__FILE__))
-FIXTURES_ROOT = TEST_ROOT + "/fixtures"
-SCHEMA_FILE = TEST_ROOT + "/schema.rb"
diff --git a/activemodel/test/models/account.rb b/activemodel/test/models/account.rb
index eed668d38f..40408e5708 100644
--- a/activemodel/test/models/account.rb
+++ b/activemodel/test/models/account.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Account
include ActiveModel::ForbiddenAttributesProtection
diff --git a/activemodel/test/models/blog_post.rb b/activemodel/test/models/blog_post.rb
index 46eba857df..d4b02eeaa7 100644
--- a/activemodel/test/models/blog_post.rb
+++ b/activemodel/test/models/blog_post.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Blog
def self.use_relative_model_naming?
true
diff --git a/activemodel/test/models/contact.rb b/activemodel/test/models/contact.rb
index bcfd267a34..c40a6d6f0e 100644
--- a/activemodel/test/models/contact.rb
+++ b/activemodel/test/models/contact.rb
@@ -1,10 +1,11 @@
+# frozen_string_literal: true
+
class Contact
extend ActiveModel::Naming
include ActiveModel::Conversion
include ActiveModel::Validations
include ActiveModel::Serializers::JSON
- include ActiveModel::Serializers::Xml
attr_accessor :id, :name, :age, :created_at, :awesome, :preferences
attr_accessor :address, :friends, :contact
diff --git a/activemodel/test/models/custom_reader.rb b/activemodel/test/models/custom_reader.rb
index 2fbcf79c3d..df605e93d9 100644
--- a/activemodel/test/models/custom_reader.rb
+++ b/activemodel/test/models/custom_reader.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CustomReader
include ActiveModel::Validations
@@ -12,4 +14,4 @@ class CustomReader
def read_attribute_for_validation(key)
@data[key]
end
-end \ No newline at end of file
+end
diff --git a/activemodel/test/models/helicopter.rb b/activemodel/test/models/helicopter.rb
index 933f3c463a..fe82c463d3 100644
--- a/activemodel/test/models/helicopter.rb
+++ b/activemodel/test/models/helicopter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Helicopter
include ActiveModel::Conversion
end
diff --git a/activemodel/test/models/person.rb b/activemodel/test/models/person.rb
index e896e90f98..8dd8ceadad 100644
--- a/activemodel/test/models/person.rb
+++ b/activemodel/test/models/person.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Person
include ActiveModel::Validations
extend ActiveModel::Translation
@@ -7,6 +9,10 @@ class Person
def condition_is_true
true
end
+
+ def condition_is_false
+ false
+ end
end
class Person::Gender
diff --git a/activemodel/test/models/person_with_validator.rb b/activemodel/test/models/person_with_validator.rb
index 505ed880c1..44e78cbc29 100644
--- a/activemodel/test/models/person_with_validator.rb
+++ b/activemodel/test/models/person_with_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PersonWithValidator
include ActiveModel::Validations
diff --git a/activemodel/test/models/project.rb b/activemodel/test/models/project.rb
deleted file mode 100644
index 581b6dc0b3..0000000000
--- a/activemodel/test/models/project.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-class Project
- include ActiveModel::DeprecatedMassAssignmentSecurity
-end
diff --git a/activemodel/test/models/reply.rb b/activemodel/test/models/reply.rb
index b77910e671..6bb18f95fe 100644
--- a/activemodel/test/models/reply.rb
+++ b/activemodel/test/models/reply.rb
@@ -1,4 +1,6 @@
-require 'models/topic'
+# frozen_string_literal: true
+
+require "models/topic"
class Reply < Topic
validate :errors_on_empty_content
diff --git a/activemodel/test/models/sheep.rb b/activemodel/test/models/sheep.rb
index 7aba055c4f..30dd9ce192 100644
--- a/activemodel/test/models/sheep.rb
+++ b/activemodel/test/models/sheep.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Sheep
extend ActiveModel::Naming
end
diff --git a/activemodel/test/models/topic.rb b/activemodel/test/models/topic.rb
index fed50bc361..db3284f833 100644
--- a/activemodel/test/models/topic.rb
+++ b/activemodel/test/models/topic.rb
@@ -1,6 +1,13 @@
+# frozen_string_literal: true
+
class Topic
include ActiveModel::Validations
include ActiveModel::Validations::Callbacks
+ include ActiveModel::AttributeMethods
+ include ActiveSupport::NumberHelper
+
+ attribute_method_suffix "_before_type_cast"
+ define_attribute_method :price
def self._validates_default_keys
super | [ :message ]
@@ -8,6 +15,7 @@ class Topic
attr_accessor :title, :author_name, :content, :approved, :created_at
attr_accessor :after_validation_performed
+ attr_writer :price
after_validation :perform_after_validation
@@ -21,7 +29,7 @@ class Topic
true
end
- def condition_is_true_but_its_not
+ def condition_is_false
false
end
@@ -37,8 +45,11 @@ class Topic
errors.add attr, "is missing" unless send(attr)
end
- def my_word_tokenizer(str)
- str.scan(/\w+/)
+ def price
+ number_to_currency @price
end
+ def attribute_before_type_cast(attr)
+ instance_variable_get(:"@#{attr}")
+ end
end
diff --git a/activemodel/test/models/track_back.rb b/activemodel/test/models/track_back.rb
index 768c96ecf0..728a022db5 100644
--- a/activemodel/test/models/track_back.rb
+++ b/activemodel/test/models/track_back.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Post
class TrackBack
def to_model
@@ -8,4 +10,4 @@ class Post
class NamedTrackBack
extend ActiveModel::Naming
end
-end \ No newline at end of file
+end
diff --git a/activemodel/test/models/user.rb b/activemodel/test/models/user.rb
index 1ec6001c48..bb1b187694 100644
--- a/activemodel/test/models/user.rb
+++ b/activemodel/test/models/user.rb
@@ -1,10 +1,13 @@
+# frozen_string_literal: true
+
class User
extend ActiveModel::Callbacks
include ActiveModel::SecurePassword
-
+
define_model_callbacks :create
has_secure_password
+ has_secure_password :recovery_password, validations: false
- attr_accessor :password_digest
+ attr_accessor :password_digest, :recovery_password_digest
end
diff --git a/activemodel/test/models/visitor.rb b/activemodel/test/models/visitor.rb
index 22ad1a3c3d..96bf3ef10a 100644
--- a/activemodel/test/models/visitor.rb
+++ b/activemodel/test/models/visitor.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Visitor
extend ActiveModel::Callbacks
include ActiveModel::SecurePassword
@@ -6,5 +8,6 @@ class Visitor
has_secure_password(validations: false)
- attr_accessor :password_digest, :password_confirmation
+ attr_accessor :password_digest
+ attr_reader :password_confirmation
end
diff --git a/activemodel/test/validators/email_validator.rb b/activemodel/test/validators/email_validator.rb
index cff47ac230..0c634d8659 100644
--- a/activemodel/test/validators/email_validator.rb
+++ b/activemodel/test/validators/email_validator.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
class EmailValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
record.errors[attribute] << (options[:message] || "is not an email") unless
- value =~ /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
+ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i.match?(value)
end
-end \ No newline at end of file
+end
diff --git a/activemodel/test/validators/namespace/email_validator.rb b/activemodel/test/validators/namespace/email_validator.rb
index 57e2793ce2..e7815d92dc 100644
--- a/activemodel/test/validators/namespace/email_validator.rb
+++ b/activemodel/test/validators/namespace/email_validator.rb
@@ -1,4 +1,6 @@
-require 'validators/email_validator'
+# frozen_string_literal: true
+
+require "validators/email_validator"
module Namespace
class EmailValidator < ::EmailValidator
diff --git a/activerecord/.gitignore b/activerecord/.gitignore
new file mode 100644
index 0000000000..884ee009eb
--- /dev/null
+++ b/activerecord/.gitignore
@@ -0,0 +1,3 @@
+/sqlnet.log
+/test/config.yml
+/test/fixtures/*.sqlite*
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index e4a6734009..2a237f86cf 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,996 +1,371 @@
-* Fix through associations using scopes having the scope merged multiple
- times.
-
- Fixes #20721.
- Fixes #20727.
-
- *Sean Griffin*
-
-* `ActiveRecord::Base.dump_schema_after_migration` applies migration tasks
- other than `db:migrate`. (eg. `db:rollback`, `db:migrate:dup`, ...)
-
- Fixes #20743.
-
- *Yves Senn*
-
-* Add alternate syntax to make `change_column_default` reversible.
-
- User can pass in `:from` and `:to` to make `change_column_default` command
- become reversible.
+* Add an `:if_not_exists` option to `create_table`.
Example:
- change_column_default :posts, :status, from: nil, to: "draft"
- change_column_default :users, :authorized, from: true, to: false
-
- *Prem Sichanugrist*
-
-* Prevent error when using `force_reload: true` on an unassigned polymorphic
- belongs_to association.
-
- Fixes #20426.
-
- *James Dabbs*
+ create_table :posts, if_not_exists: true do |t|
+ t.string :title
+ end
-* Correctly raise `ActiveRecord::AssociationTypeMismatch` when assigning
- a wrong type to a namespaced association.
+ That would execute:
- Fixes #20545.
+ CREATE TABLE IF NOT EXISTS posts (
+ ...
+ )
- *Diego Carrion*
+ If the table already exists, `if_not_exists: false` (the default) raises an
+ exception whereas `if_not_exists: true` does nothing.
-* `validates_absence_of` respects `marked_for_destruction?`.
+ *fatkodima*, *Stefan Kanev*
- Fixes #20449.
+* Defining an Enum as a Hash with blank key, or as an Array with a blank value, now raises an `ArgumentError`.
- *Yves Senn*
+ *Christophe Maximin*
-* Include the `Enumerable` module in `ActiveRecord::Relation`
+* Adds support for multiple databases to `rails db:schema:cache:dump` and `rails db:schema:cache:clear`.
- *Sean Griffin & bogdan*
+ *Gannon McGibbon*
-* Use `Enumerable#sum` in `ActiveRecord::Relation` if a block is given.
+* `update_columns` now correctly raises `ActiveModel::MissingAttributeError`
+ if the attribute does not exist.
*Sean Griffin*
-* Let `WITH` queries (Common Table Expressions) be explainable.
-
- *Vladimir Kochnev*
-
-* Make `remove_index :table, :column` reversible.
-
- *Yves Senn*
-
-* Fixed an error which would occur in dirty checking when calling
- `update_attributes` from a getter.
-
- Fixes #20531.
-
- *Sean Griffin*
+* Add support for hash and url configs in database hash of `ActiveRecord::Base.connected_to`.
-* Make `remove_foreign_key` reversible. Any foreign key options must be
- specified, similar to `remove_column`.
+ ````
+ User.connected_to(database: { writing: "postgres://foo" }) do
+ User.create!(name: "Gannon")
+ end
- *Aster Ryan*
+ config = { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" }
+ User.connected_to(database: { reading: config }) do
+ User.count
+ end
+ ````
-* Add `:enum_prefix`/`:enum_suffix` option to `enum` definition.
+ *Gannon McGibbon*
- Fixes #17511, #17415.
+* Support default expression for MySQL.
- *Igor Kapkov*
+ MySQL 8.0.13 and higher supports default value to be a function or expression.
-* Correctly handle decimal arrays with defaults in the schema dumper.
-
- Fixes #20515.
-
- *Sean Griffin & jmondo*
-
-* Deprecate the PostgreSQL `:point` type in favor of a new one which will return
- `Point` objects instead of an `Array`
-
- *Sean Griffin*
-
-* Ensure symbols passed to `ActiveRecord::Relation#select` are always treated
- as columns.
-
- Fixes #20360.
-
- *Sean Griffin*
-
-* Do not set `sql_mode` if `strict: :default` is specified.
-
- ```
- # database.yml
- production:
- adapter: mysql2
- database: foo_prod
- user: foo
- strict: :default
- ```
+ https://dev.mysql.com/doc/refman/8.0/en/create-table.html
*Ryuta Kamizono*
-* Allow proc defaults to be passed to the attributes API. See documentation
- for examples.
-
- *Sean Griffin*, *Kir Shatrov*
-
-* SQLite: `:collation` support for string and text columns.
-
- Example:
-
- create_table :foo do |t|
- t.string :string_nocase, collation: 'NOCASE'
- t.text :text_rtrim, collation: 'RTRIM'
- end
-
- add_column :foo, :title, :string, collation: 'RTRIM'
-
- change_column :foo, :title, :string, collation: 'NOCASE'
-
- *Akshay Vishnoi*
+* Support expression indexes for MySQL.
-* Allow the use of symbols or strings to specify enum values in test
- fixtures:
+ MySQL 8.0.13 and higher supports functional key parts that index
+ expression values rather than column or column prefix values.
- awdr:
- title: "Agile Web Development with Rails"
- status: :proposed
-
- *George Claghorn*
-
-* Clear query cache when `ActiveRecord::Base#reload` is called.
-
- *Shane Hender, Pierre Nespo*
-
-* Include stored procedures and function on the MySQL structure dump.
-
- *Jonathan Worek*
-
-* Pass `:extend` option for `has_and_belongs_to_many` associations to the
- underlying `has_many :through`.
-
- *Jaehyun Shin*
-
-* Deprecate `Relation#uniq` use `Relation#distinct` instead.
-
- See #9683.
-
- *Yves Senn*
-
-* Allow single table inheritance instantiation to work when storing
- demodulized class names.
-
- *Alex Robbin*
-
-* Correctly pass MySQL options when using `structure_dump` or
- `structure_load`.
-
- Specifically, it fixes an issue when using SSL authentication.
-
- *Alex Coomans*
-
-* Dump indexes in `create_table` instead of `add_index`.
-
- If the adapter supports indexes in `create_table`, generated SQL is
- slightly more efficient.
+ https://dev.mysql.com/doc/refman/8.0/en/create-index.html
*Ryuta Kamizono*
-* Correctly dump `:options` on `create_table` for MySQL.
+* Fix collection cache key with limit and custom select to avoid ambiguous timestamp column error.
- *Ryuta Kamizono*
+ Fixes #33056.
-* PostgreSQL: `:collation` support for string and text columns.
+ *Federico Martinez*
- Example:
+* Add basic API for connection switching to support multiple databases.
- create_table :foos do |t|
- t.string :string_en, collation: 'en_US.UTF-8'
- t.text :text_ja, collation: 'ja_JP.UTF-8'
- end
+ 1) Adds a `connects_to` method for models to connect to multiple databases. Example:
- *Ryuta Kamizono*
+ ```
+ class AnimalsModel < ApplicationRecord
+ self.abstract_class = true
-* Make `unscope` aware of "less than" and "greater than" conditions.
+ connects_to database: { writing: :animals_primary, reading: :animals_replica }
+ end
- *TAKAHASHI Kazuaki*
+ class Dog < AnimalsModel
+ # connected to both the animals_primary db for writing and the animals_replica for reading
+ end
+ ```
-* `find_by` and `find_by!` raise `ArgumentError` when called without
- arguments.
+ 2) Adds a `connected_to` block method for switching connection roles or connecting to
+ a database that the model didn't connect to. Connecting to the database in this block is
+ useful when you have another defined connection, for example `slow_replica` that you don't
+ want to connect to by default but need in the console, or a specific code block.
- *Kohei Suzuki*
+ ```
+ ActiveRecord::Base.connected_to(role: :reading) do
+ Dog.first # finds dog from replica connected to AnimalsBase
+ Book.first # doesn't have a reading connection, will raise an error
+ end
+ ```
-* Revert behavior of `db:schema:load` back to loading the full
- environment. This ensures that initializers are run.
+ ```
+ ActiveRecord::Base.connected_to(database: :slow_replica) do
+ SlowReplicaModel.first # if the db config has a slow_replica configuration this will be used to do the lookup, otherwise this will throw an exception
+ end
+ ```
- Fixes #19545.
+ *Eileen M. Uchitelle*
- *Yves Senn*
+* Enum raises on invalid definition values
-* Fix missing index when using `timestamps` with the `index` option.
+ When defining a Hash enum it can be easy to use [] instead of {}. This
+ commit checks that only valid definition values are provided, those can
+ be a Hash, an array of Symbols or an array of Strings. Otherwise it
+ raises an ArgumentError.
- The `index` option used with `timestamps` should be passed to both
- `column` definitions for `created_at` and `updated_at` rather than just
- the first.
+ Fixes #33961
- *Paul Mucur*
+ *Alberto Almagro*
-* Rename `:class` to `:anonymous_class` in association options.
+* Reloading associations now clears the Query Cache like `Persistence#reload` does.
- Fixes #19659.
+ ```
+ class Post < ActiveRecord::Base
+ has_one :category
+ belongs_to :author
+ has_many :comments
+ end
- *Andrew White*
+ # Each of the following will now clear the query cache.
+ post.reload_category
+ post.reload_author
+ post.comments.reload
+ ```
-* Autosave existing records on a has many through association when the parent
- is new.
+ *Christophe Maximin*
- Fixes #19782.
-
- *Sean Griffin*
-
-* Fixed a bug where uniqueness validations would error on out of range values,
- even if an validation should have prevented it from hitting the database.
-
- *Andrey Voronkov*
-
-* MySQL: `:charset` and `:collation` support for string and text columns.
+* Added `index` option for `change_table` migration helpers.
+ With this change you can create indexes while adding new
+ columns into the existing tables.
Example:
- create_table :foos do |t|
- t.string :string_utf8_bin, charset: 'utf8', collation: 'utf8_bin'
- t.text :text_ascii, charset: 'ascii'
+ change_table(:languages) do |t|
+ t.string :country_code, index: true
end
- *Ryuta Kamizono*
-
-* Foreign key related methods in the migration DSL respect
- `ActiveRecord::Base.pluralize_table_names = false`.
-
- Fixes #19643.
-
*Mehmet Emin İNAÇ*
-* Reduce memory usage from loading types on PostgreSQL.
-
- Fixes #19578.
-
- *Sean Griffin*
-
-* Add `config.active_record.warn_on_records_fetched_greater_than` option.
-
- When set to an integer, a warning will be logged whenever a result set
- larger than the specified size is returned by a query.
-
- Fixes #16463.
-
- *Jason Nochlin*
-
-* Ignore `.psqlrc` when loading database structure.
-
- *Jason Weathered*
-
-* Fix referencing wrong table aliases while joining tables of has many through
- association (only when calling calculation methods).
-
- Fixes #19276.
+* Fix `transaction` reverting for migrations.
- *pinglamb*
+ Before: Commands inside a `transaction` in a reverted migration ran uninverted.
+ Now: This change fixes that by reverting commands inside `transaction` block.
-* Correctly persist a serialized attribute that has been returned to
- its default value by an in-place modification.
+ *fatkodima*, *David Verhasselt*
- Fixes #19467.
+* Raise an error instead of scanning the filesystem root when `fixture_path` is blank.
- *Matthew Draper*
+ *Gannon McGibbon*, *Max Albrecht*
-* Fix generating the schema file when using PostgreSQL `BigInt[]` data type.
- Previously the `limit: 8` was not coming through, and this caused it to
- become `Int[]` data type after rebuilding from the schema.
+* Allow `ActiveRecord::Base.configurations=` to be set with a symbolized hash.
- Fixes #19420.
+ *Gannon McGibbon*
- *Jake Waller*
+* Don't update counter cache unless the record is actually saved.
-* Reuse the `CollectionAssociation#reader` cache when the foreign key is
- available prior to save.
-
- *Ben Woosley*
-
-* Add `config.active_record.dump_schemas` to fix `db:structure:dump`
- when using schema_search_path and PostgreSQL extensions.
-
- Fixes #17157.
-
- *Ryan Wallace*
-
-* Renaming `use_transactional_fixtures` to `use_transactional_tests` for clarity.
-
- Fixes #18864.
-
- *Brandon Weiss*
-
-* Increase pg gem version requirement to `~> 0.18`. Earlier versions of the
- pg gem are known to have problems with Ruby 2.2.
-
- *Matt Brictson*
-
-* Correctly dump `serial` and `bigserial`.
-
- *Ryuta Kamizono*
-
-* Fix default `format` value in `ActiveRecord::Tasks::DatabaseTasks#schema_file`.
-
- *James Cox*
-
-* Don't enroll records in the transaction if they don't have commit callbacks.
- This was causing a memory leak when creating many records inside a transaction.
-
- Fixes #15549.
-
- *Will Bryant*, *Aaron Patterson*
-
-* Correctly create through records when created on a has many through
- association when using `where`.
-
- Fixes #19073.
-
- *Sean Griffin*
-
-* Add `SchemaMigration.create_table` support for any unicode charsets with MySQL.
-
- *Ryuta Kamizono*
-
-* PostgreSQL no longer disables user triggers if system triggers can't be
- disabled. Disabling user triggers does not fulfill what the method promises.
- Rails currently requires superuser privileges for this method.
-
- If you absolutely rely on this behavior, consider patching
- `disable_referential_integrity`.
-
- *Yves Senn*
-
-* Restore aborted transaction state when `disable_referential_integrity` fails
- due to missing permissions.
-
- *Toby Ovod-Everett*, *Yves Senn*
-
-* In PostgreSQL, print a warning message if `disable_referential_integrity`
- fails due to missing permissions.
-
- *Andrey Nering*, *Yves Senn*
-
-* Allow a `:limit` option for MySQL bigint primary key support.
-
- Example:
-
- create_table :foos, id: :primary_key, limit: 8 do |t|
- end
-
- # or
-
- create_table :foos, id: false do |t|
- t.primary_key :id, limit: 8
- end
-
- *Ryuta Kamizono*
-
-* `belongs_to` will now trigger a validation error by default if the association is not present.
- You can turn this off on a per-association basis with `optional: true`.
- (Note this new default only applies to new Rails apps that will be generated with
- `config.active_record.belongs_to_required_by_default = true` in initializer.)
-
- *Josef Šimánek*
-
-* Fixed `ActiveRecord::Relation#becomes!` and `changed_attributes` issues for type
- columns.
-
- Fixes #17139.
-
- *Miklos Fazekas*
-
-* Format the time string according to the precision of the time column.
+ Fixes #31493, #33113, #33117.
*Ryuta Kamizono*
-* Allow a `:precision` option for time type columns.
+* Deprecate `ActiveRecord::Result#to_hash` in favor of `ActiveRecord::Result#to_a`.
- *Ryuta Kamizono*
-
-* Add `ActiveRecord::Base.suppress` to prevent the receiver from being saved
- during the given block.
+ *Gannon McGibbon*, *Kevin Cheng*
- For example, here's a pattern of creating notifications when new comments
- are posted. (The notification may in turn trigger an email, a push
- notification, or just appear in the UI somewhere):
+* SQLite3 adapter supports expression indexes.
- class Comment < ActiveRecord::Base
- belongs_to :commentable, polymorphic: true
- after_create -> { Notification.create! comment: self,
- recipients: commentable.recipients }
- end
-
- That's what you want the bulk of the time. A new comment creates a new
- Notification. There may be edge cases where you don't want that, like
- when copying a commentable and its comments, in which case write a
- concern with something like this:
-
- module Copyable
- def copy_to(destination)
- Notification.suppress do
- # Copy logic that creates new comments that we do not want triggering
- # notifications.
- end
- end
- end
+ ```
+ create_table :users do |t|
+ t.string :email
+ end
- *Michael Ryan*
+ add_index :users, 'lower(email)', name: 'index_users_on_email', unique: true
+ ```
-* `:time` option added for `#touch`.
+ *Gray Kemmey*
- Fixes #18905.
+* Allow subclasses to redefine autosave callbacks for associated records.
- *Hyonjee Joo*
+ Fixes #33305.
-* Deprecate passing of `start` value to `find_in_batches` and `find_each`
- in favour of `begin_at` value.
+ *Andrey Subbota*
- *Vipul A M*
+* Bump minimum MySQL version to 5.5.8.
-* Add `foreign_key_exists?` method.
+ *Yasuo Honda*
- *Tõnis Simo*
+* Use MySQL utf8mb4 character set by default.
-* Use SQL COUNT and LIMIT 1 queries for `none?` and `one?` methods
- if no block or limit is given, instead of loading the entire
- collection into memory. This applies to relations (e.g. `User.all`)
- as well as associations (e.g. `account.users`)
+ `utf8mb4` character set with 4-Byte encoding supports supplementary characters including emoji.
+ The previous default 3-Byte encoding character set `utf8` is not enough to support them.
- # Before:
+ *Yasuo Honda*
- users.none?
- # SELECT "users".* FROM "users"
+* Fix duplicated record creation when using nested attributes with `create_with`.
- users.one?
- # SELECT "users".* FROM "users"
+ *Darwin Wu*
- # After:
+* Configuration item `config.filter_parameters` could also filter out
+ sensitive values of database columns when call `#inspect`.
+ We also added `ActiveRecord::Base::filter_attributes`/`=` in order to
+ specify sensitive attributes to specific model.
- users.none?
- # SELECT 1 AS one FROM "users" LIMIT 1
+ ```
+ Rails.application.config.filter_parameters += [:credit_card_number, /phone/]
+ Account.last.inspect # => #<Account id: 123, name: "DHH", credit_card_number: [FILTERED], telephone_number: [FILTERED] ...>
+ SecureAccount.filter_attributes += [:name]
+ SecureAccount.last.inspect # => #<SecureAccount id: 42, name: [FILTERED], credit_card_number: [FILTERED] ...>
+ ```
- users.one?
- # SELECT COUNT(*) FROM "users"
-
- *Eugene Gilburg*
-
-* Have `enum` perform type casting consistently with the rest of Active
- Record, such as `where`.
-
- *Sean Griffin*
-
-* `scoping` no longer pollutes the current scope of sibling classes when using
- STI. e.x.
-
- StiOne.none.scoping do
- StiTwo.all
- end
-
- Fixes #18806.
-
- *Sean Griffin*
+ *Zhang Kang*, *Yoshiyuki Kinjo*
-* `remove_reference` with `foreign_key: true` removes the foreign key before
- removing the column. This fixes a bug where it was not possible to remove
- the column on MySQL.
-
- Fixes #18664.
-
- *Yves Senn*
-
-* `find_in_batches` now accepts an `:end_at` parameter that complements the `:start`
- parameter to specify where to stop batch processing.
-
- *Vipul A M*
-
-* Fix a rounding problem for PostgreSQL timestamp columns.
-
- If a timestamp column has a precision specified, it needs to
- format according to that.
-
- *Ryuta Kamizono*
-
-* Respect the database default charset for `schema_migrations` table.
-
- The charset of `version` column in `schema_migrations` table depends
- on the database default charset and collation rather than the encoding
- of the connection.
+* Deprecate `column_name_length`, `table_name_length`, `columns_per_table`,
+ `indexes_per_table`, `columns_per_multicolumn_index`, `sql_query_length`,
+ and `joins_per_query` methods in `DatabaseLimits`.
*Ryuta Kamizono*
-* Raise `ArgumentError` when passing `nil` or `false` to `Relation#merge`.
+* `ActiveRecord::Base.configurations` now returns an object.
- These are not valid values to merge in a relation, so it should warn users
- early.
+ `ActiveRecord::Base.configurations` used to return a hash, but this
+ is an inflexible data model. In order to improve multiple-database
+ handling in Rails, we've changed this to return an object. Some methods
+ are provided to make the object behave hash-like in order to ease the
+ transition process. Since most applications don't manipulate the hash
+ we've decided to add backwards-compatible functionality that will throw
+ a deprecation warning if used, however calling `ActiveRecord::Base.configurations`
+ will use the new version internally and externally.
- *Rafael Mendonça França*
+ For example, the following `database.yml`:
-* Use `SCHEMA` instead of `DB_STRUCTURE` for specifying a structure file.
+ ```
+ development:
+ adapter: sqlite3
+ database: db/development.sqlite3
+ ```
- This makes the db:structure tasks consistent with test:load_structure.
+ Used to become a hash:
- *Dieter Komendera*
+ ```
+ { "development" => { "adapter" => "sqlite3", "database" => "db/development.sqlite3" } }
+ ```
-* Respect custom primary keys for associations when calling `Relation#where`
+ Is now converted into the following object:
- Fixes #18813.
+ ```
+ #<ActiveRecord::DatabaseConfigurations:0x00007fd1acbdf800 @configurations=[
+ #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbded10 @env_name="development",
+ @spec_name="primary", @config={"adapter"=>"sqlite3", "database"=>"db/development.sqlite3"}>
+ ]
+ ```
- *Sean Griffin*
-
-* Fix several edge cases which could result in a counter cache updating
- twice or not updating at all for `has_many` and `has_many :through`.
-
- Fixes #10865.
-
- *Sean Griffin*
+ Iterating over the database configurations has also changed. Instead of
+ calling hash methods on the `configurations` hash directly, a new method `configs_for` has
+ been provided that allows you to select the correct configuration. `env_name`, and
+ `spec_name` arguments are optional. For example these return an array of
+ database config objects for the requested environment and a single database config object
+ will be returned for the requested environment and specification name respectively.
-* Foreign keys added by migrations were given random, generated names. This
- meant a different `structure.sql` would be generated every time a developer
- ran migrations on their machine.
+ ```
+ ActiveRecord::Base.configurations.configs_for(env_name: "development")
+ ActiveRecord::Base.configurations.configs_for(env_name: "development", spec_name: "primary")
+ ```
- The generated part of foreign key names is now a hash of the table name and
- column name, which is consistent every time you run the migration.
+ *Eileen M. Uchitelle*, *Aaron Patterson*
- *Chris Sinjakli*
+* Add database configuration to disable advisory locks.
-* Validation errors would be raised for parent records when an association
- was saved when the parent had `validate: false`. It should not be the
- responsibility of the model to validate an associated object unless the
- object was created or modified by the parent.
+ ```
+ production:
+ adapter: postgresql
+ advisory_locks: false
+ ```
- This fixes the issue by skipping validations if the parent record is
- persisted, not changed, and not marked for destruction.
+ *Guo Xiang*
- Fixes #17621.
+* SQLite3 adapter `alter_table` method restores foreign keys.
- *Eileen M. Uchitelle, Aaron Patterson*
+ *Yasuo Honda*
-* Fix n+1 query problem when eager loading nil associations (fixes #18312)
-
- *Sammy Larbi*
-
-* Change the default error message from `can't be blank` to `must exist` for
- the presence validator of the `:required` option on `belongs_to`/`has_one`
- associations.
-
- *Henrik Nygren*
-
-* Fixed `ActiveRecord::Relation#group` method when an argument is an SQL
- reserved keyword:
+* Allow `:to_table` option to `invert_remove_foreign_key`.
Example:
- SplitTest.group(:key).count
- Property.group(:value).count
+ remove_foreign_key :accounts, to_table: :owners
- *Bogdan Gusiev*
+ *Nikolay Epifanov*, *Rich Chen*
-* Added the `#or` method on `ActiveRecord::Relation`, allowing use of the OR
- operator to combine WHERE or HAVING clauses.
+* Add environment & load_config dependency to `bin/rake db:seed` to enable
+ seed load in environments without Rails and custom DB configuration
- Example:
-
- Post.where('id = 1').or(Post.where('id = 2'))
- # => SELECT * FROM posts WHERE (id = 1) OR (id = 2)
-
- *Sean Griffin*, *Matthew Draper*, *Gael Muller*, *Olivier El Mekki*
-
-* Don't define autosave association callbacks twice from
- `accepts_nested_attributes_for`.
-
- Fixes #18704.
-
- *Sean Griffin*
-
-* Integer types will no longer raise a `RangeError` when assigning an
- attribute, but will instead raise when going to the database.
-
- Fixes several vague issues which were never reported directly. See the
- commit message from the commit which added this line for some examples.
-
- *Sean Griffin*
-
-* Values which would error while being sent to the database (such as an
- ASCII-8BIT string with invalid UTF-8 bytes on SQLite3), no longer error on
- assignment. They will still error when sent to the database, but you are
- given the ability to re-assign it to a valid value.
-
- Fixes #18580.
-
- *Sean Griffin*
-
-* Don't remove join dependencies in `Relation#exists?`
-
- Fixes #18632.
-
- *Sean Griffin*
-
-* Invalid values assigned to a JSON column are assumed to be `nil`.
-
- Fixes #18629.
-
- *Sean Griffin*
-
-* Add `ActiveRecord::Base#accessed_fields`, which can be used to quickly
- discover which fields were read from a model when you are looking to only
- select the data you need from the database.
-
- *Sean Griffin*
-
-* Introduce the `:if_exists` option for `drop_table`.
-
- Example:
-
- drop_table(:posts, if_exists: true)
-
- That would execute:
-
- DROP TABLE IF EXISTS posts
-
- If the table doesn't exist, `if_exists: false` (the default) raises an
- exception whereas `if_exists: true` does nothing.
-
- *Cody Cutrer*, *Stefan Kanev*, *Ryuta Kamizono*
-
-* Don't run SQL if attribute value is not changed for update_attribute method.
-
- *Prathamesh Sonpatki*
-
-* `time` columns can now get affected by `time_zone_aware_attributes`. If you have
- set `config.time_zone` to a value other than `'UTC'`, they will be treated
- as in that time zone by default in Rails 5.1. If this is not the desired
- behavior, you can set
-
- ActiveRecord::Base.time_zone_aware_types = [:datetime]
-
- A deprecation warning will be emitted if you have a `:time` column, and have
- not explicitly opted out.
-
- Fixes #3145.
-
- *Sean Griffin*
-
-* Tests now run after_commit callbacks. You no longer have to declare
- `uses_transaction ‘test name’` to test the results of an after_commit.
-
- after_commit callbacks run after committing a transaction whose parent
- is not `joinable?`: un-nested transactions, transactions within test cases,
- and transactions in `console --sandbox`.
-
- *arthurnn*, *Ravil Bayramgalin*, *Matthew Draper*
-
-* `nil` as a value for a binary column in a query no longer logs as
- "<NULL binary data>", and instead logs as just "nil".
-
- *Sean Griffin*
-
-* `attribute_will_change!` will no longer cause non-persistable attributes to
- be sent to the database.
-
- Fixes #18407.
-
- *Sean Griffin*
-
-* Remove support for the `protected_attributes` gem.
-
- *Carlos Antonio da Silva*, *Roberto Miranda*
-
-* Fix accessing of fixtures having non-string labels like Fixnum.
-
- *Prathamesh Sonpatki*
-
-* Remove deprecated support to preload instance-dependent associations.
-
- *Yves Senn*
-
-* Remove deprecated support for PostgreSQL ranges with exclusive lower bounds.
-
- *Yves Senn*
-
-* Remove deprecation when modifying a relation with cached Arel.
- This raises an `ImmutableRelation` error instead.
-
- *Yves Senn*
-
-* Added `ActiveRecord::SecureToken` in order to encapsulate generation of
- unique tokens for attributes in a model using `SecureRandom`.
-
- *Roberto Miranda*
-
-* Change the behavior of boolean columns to be closer to Ruby's semantics.
-
- Before this change we had a small set of "truthy", and all others are "falsy".
-
- Now, we have a small set of "falsy" values and all others are "truthy" matching
- Ruby's semantics.
-
- *Rafael Mendonça França*
-
-* Deprecate `ActiveRecord::Base.errors_in_transactional_callbacks=`.
-
- *Rafael Mendonça França*
-
-* Change transaction callbacks to not swallow errors.
-
- Before this change any errors raised inside a transaction callback
- were getting rescued and printed in the logs.
-
- Now these errors are not rescued anymore and just bubble up, as the other callbacks.
-
- *Rafael Mendonça França*
-
-* Remove deprecated `sanitize_sql_hash_for_conditions`.
-
- *Rafael Mendonça França*
-
-* Remove deprecated `Reflection#source_macro`.
-
- *Rafael Mendonça França*
-
-* Remove deprecated `symbolized_base_class` and `symbolized_sti_name`.
-
- *Rafael Mendonça França*
-
-* Remove deprecated `ActiveRecord::Base.disable_implicit_join_references=`.
+ *Tobias Bielohlawek*
- *Rafael Mendonça França*
+* Fix default value for mysql time types with specified precision.
-* Remove deprecated access to connection specification using a string accessor.
+ *Nikolay Kondratyev*
- Now all strings will be handled as a URL.
-
- *Rafael Mendonça França*
-
-* Change the default `null` value for `timestamps` to `false`.
-
- *Rafael Mendonça França*
-
-* Return an array of pools from `connection_pools`.
-
- *Rafael Mendonça França*
-
-* Return a null column from `column_for_attribute` when no column exists.
-
- *Rafael Mendonça França*
-
-* Remove deprecated `serialized_attributes`.
-
- *Rafael Mendonça França*
-
-* Remove deprecated automatic counter caches on `has_many :through`.
-
- *Rafael Mendonça França*
-
-* Change the way in which callback chains can be halted.
-
- The preferred method to halt a callback chain from now on is to explicitly
- `throw(:abort)`.
- In the past, returning `false` in an Active Record `before_` callback had the
- side effect of halting the callback chain.
- This is not recommended anymore and, depending on the value of the
- `config.active_support.halt_callback_chains_on_return_false` option, will
- either not work at all or display a deprecation warning.
-
- *claudiob*
-
-* Clear query cache on rollback.
-
- *Florian Weingarten*
-
-* Fix setting of foreign_key for through associations when building a new record.
-
- Fixes #12698.
-
- *Ivan Antropov*
-
-* Improve dumping of the primary key. If it is not a default primary key,
- correctly dump the type and options.
-
- Fixes #14169, #16599.
+* Fix `touch` option to behave consistently with `Persistence#touch` method.
*Ryuta Kamizono*
-* Format the datetime string according to the precision of the datetime field.
-
- Incompatible to rounding behavior between MySQL 5.6 and earlier.
-
- In 5.5, when you insert `2014-08-17 12:30:00.999999` the fractional part
- is ignored. In 5.6, it's rounded to `2014-08-17 12:30:01`:
-
- http://bugs.mysql.com/bug.php?id=68760
-
- *Ryuta Kamizono*
-
-* Allow a precision option for MySQL datetimes.
-
- *Ryuta Kamizono*
-
-* Fixed automatic `inverse_of` for models nested in a module.
-
- *Andrew McCloud*
-
-* Change `ActiveRecord::Relation#update` behavior so that it can
- be called without passing ids of the records to be updated.
-
- This change allows updating multiple records returned by
- `ActiveRecord::Relation` with callbacks and validations.
-
- # Before
- # ArgumentError: wrong number of arguments (1 for 2)
- Comment.where(group: 'expert').update(body: "Group of Rails Experts")
-
- # After
- # Comments with group expert updated with body "Group of Rails Experts"
- Comment.where(group: 'expert').update(body: "Group of Rails Experts")
-
- *Prathamesh Sonpatki*
-
-* Fix `reaping_frequency` option when the value is a string.
-
- This usually happens when it is configured using `DATABASE_URL`.
-
- *korbin*
-
-* Fix error message when trying to create an associated record and the foreign
- key is missing.
-
- Before this fix the following exception was being raised:
+* Migrations raise when duplicate column definition.
- NoMethodError: undefined method `val' for #<Arel::Nodes::BindParam:0x007fc64d19c218>
+ Fixes #33024.
- Now the message is:
+ *Federico Martinez*
- ActiveRecord::UnknownAttributeError: unknown attribute 'foreign_key' for Model.
+* Bump minimum SQLite version to 3.8
- *Rafael Mendonça França*
+ *Yasuo Honda*
-* Fix change detection problem for PostgreSQL bytea type and
- `ArgumentError: string contains null byte` exception with pg-0.18.
+* Fix parent record should not get saved with duplicate children records.
- Fixes #17680.
+ Fixes #32940.
- *Lars Kanis*
+ *Santosh Wadghule*
-* When a table has a composite primary key, the `primary_key` method for
- SQLite3 and PostgreSQL adapters was only returning the first field of the key.
- Ensures that it will return nil instead, as Active Record doesn't support
- composite primary keys.
+* Fix logic on disabling commit callbacks so they are not called unexpectedly when errors occur.
- Fixes #18070.
+ *Brian Durand*
- *arthurnn*
+* Ensure `Associations::CollectionAssociation#size` and `Associations::CollectionAssociation#empty?`
+ use loaded association ids if present.
-* `validates_size_of` / `validates_length_of` do not count records
- which are `marked_for_destruction?`.
+ *Graham Turner*
- Fixes #7247.
+* Add support to preload associations of polymorphic associations when not all the records have the requested associations.
- *Yves Senn*
+ *Dana Sherson*
-* Ensure `first!` and friends work on loaded associations.
-
- Fixes #18237.
-
- *Sean Griffin*
-
-* `eager_load` preserves readonly flag for associations.
-
- Closes #15853.
-
- *Takashi Kokubun*
-
-* Provide `:touch` option to `save()` to accommodate saving without updating
- timestamps.
-
- Fixes #18202.
-
- *Dan Olson*
-
-* Provide a more helpful error message when an unsupported class is passed to
- `serialize`.
-
- Fixes #18224.
-
- *Sean Griffin*
-
-* Add bigint primary key support for MySQL.
+* Add `touch_all` method to `ActiveRecord::Relation`.
Example:
- create_table :foos, id: :bigint do |t|
- end
-
- *Ryuta Kamizono*
-
-* Support for any type of primary key.
-
- Fixes #14194.
-
- *Ryuta Kamizono*
-
-* Dump the default `nil` for PostgreSQL UUID primary key.
-
- *Ryuta Kamizono*
-
-* Add a `:foreign_key` option to `references` and associated migration
- methods. The model and migration generators now use this option, rather than
- the `add_foreign_key` form.
-
- *Sean Griffin*
-
-* Don't raise when writing an attribute with an out-of-range datetime passed
- by the user.
-
- *Grey Baker*
-
-* Replace deprecated `ActiveRecord::Tasks::DatabaseTasks#load_schema` with
- `ActiveRecord::Tasks::DatabaseTasks#load_schema_for`.
+ Person.where(name: "David").touch_all(time: Time.new(2020, 5, 16, 0, 0, 0))
- *Yves Senn*
+ *fatkodima*, *duggiefresh*
-* Fix bug with `ActiveRecord::Type::Numeric` that caused negative values to
- be marked as having changed when set to the same negative value.
+* Add `ActiveRecord::Base.base_class?` predicate.
- Closes #18161.
-
- *Daniel Fox*
-
-* Introduce `force: :cascade` option for `create_table`. Using this option
- will recreate tables even if they have dependent objects (like foreign keys).
- `db/schema.rb` now uses `force: :cascade`. This makes it possible to
- reload the schema when foreign keys are in place.
-
- *Matthew Draper*, *Yves Senn*
-
-* `db:schema:load` and `db:structure:load` no longer purge the database
- before loading the schema. This is left for the user to do.
- `db:test:prepare` will still purge the database.
-
- Closes #17945.
-
- *Yves Senn*
-
-* Fix undesirable RangeError by `Type::Integer`. Add `Type::UnsignedInteger`.
-
- *Ryuta Kamizono*
+ *Bogdan Gusiev*
-* Add `foreign_type` option to `has_one` and `has_many` association macros.
+* Add custom prefix/suffix options to `ActiveRecord::Store.store_accessor`.
- This option enables to define the column name of associated object's type for polymorphic associations.
+ *Tan Huynh*, *Yukio Mizuta*
- *Ulisses Almeida*, *Kassio Borges*
+* Rails 6 requires Ruby 2.4.1 or newer.
-* Remove deprecated behavior allowing nested arrays to be passed as query
- values.
+ *Jeremy Daer*
- *Melanie Gilman*
+* Deprecate `update_attributes`/`!` in favor of `update`/`!`.
-* Deprecate passing a class as a value in a query. Users should pass strings
- instead.
+ *Eddie Lebow*
- *Melanie Gilman*
+* Add `ActiveRecord::Base.create_or_find_by`/`!` to deal with the SELECT/INSERT race condition in
+ `ActiveRecord::Base.find_or_create_by`/`!` by leaning on unique constraints in the database.
-* `add_timestamps` and `remove_timestamps` now properly reversible with
- options.
+ *DHH*
- *Noam Gagliardi-Rabinovich*
+* Add `Relation#pick` as short-hand for single-value plucks.
-* `ActiveRecord::ConnectionAdapters::ColumnDumper#column_spec` and
- `ActiveRecord::ConnectionAdapters::ColumnDumper#prepare_column_options` no
- longer have a `types` argument. They should access
- `connection#native_database_types` directly.
+ *DHH*
- *Yves Senn*
-Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/activerecord/CHANGELOG.md) for previous changes.
+Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/activerecord/CHANGELOG.md) for previous changes.
diff --git a/activerecord/MIT-LICENSE b/activerecord/MIT-LICENSE
index 7c2197229d..04ba107c48 100644
--- a/activerecord/MIT-LICENSE
+++ b/activerecord/MIT-LICENSE
@@ -1,4 +1,6 @@
-Copyright (c) 2004-2015 David Heinemeier Hansson
+Copyright (c) 2004-2018 David Heinemeier Hansson
+
+Arel originally copyright (c) 2007-2016 Nick Kallen, Bryan Helmkamp, Emilio Tagua, Aaron Patterson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
@@ -17,4 +19,4 @@ 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. \ No newline at end of file
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/activerecord/README.rdoc b/activerecord/README.rdoc
index 049c5d2b3b..19650b82ae 100644
--- a/activerecord/README.rdoc
+++ b/activerecord/README.rdoc
@@ -26,7 +26,7 @@ The Product class is automatically mapped to the table named "products",
which might look like this:
CREATE TABLE products (
- id int(11) NOT NULL auto_increment,
+ id bigint NOT NULL auto_increment,
name varchar(255),
PRIMARY KEY (id)
);
@@ -125,7 +125,7 @@ This would also define the following accessors: <tt>Product#name</tt> and
)
{Learn more}[link:classes/ActiveRecord/Base.html] and read about the built-in support for
- MySQL[link:classes/ActiveRecord/ConnectionAdapters/MysqlAdapter.html],
+ MySQL[link:classes/ActiveRecord/ConnectionAdapters/Mysql2Adapter.html],
PostgreSQL[link:classes/ActiveRecord/ConnectionAdapters/PostgreSQLAdapter.html], and
SQLite3[link:classes/ActiveRecord/ConnectionAdapters/SQLite3Adapter.html].
@@ -138,7 +138,7 @@ This would also define the following accessors: <tt>Product#name</tt> and
* Database agnostic schema management with Migrations.
- class AddSystemSettings < ActiveRecord::Migration
+ class AddSystemSettings < ActiveRecord::Migration[5.0]
def up
create_table :system_settings do |t|
t.string :name
@@ -162,7 +162,7 @@ This would also define the following accessors: <tt>Product#name</tt> and
== Philosophy
Active Record is an implementation of the object-relational mapping (ORM)
-pattern[http://www.martinfowler.com/eaaCatalog/activeRecord.html] by the same
+pattern[https://www.martinfowler.com/eaaCatalog/activeRecord.html] by the same
name described by Martin Fowler:
"An object that wraps a row in a database table or view,
@@ -188,7 +188,7 @@ Admit the Database:
The latest version of Active Record can be installed with RubyGems:
- % gem install activerecord
+ $ gem install activerecord
Source code can be downloaded as part of the Rails project on GitHub:
@@ -199,7 +199,7 @@ Source code can be downloaded as part of the Rails project on GitHub:
Active Record is released under the MIT license:
-* http://www.opensource.org/licenses/MIT
+* https://opensource.org/licenses/MIT
== Support
@@ -208,11 +208,10 @@ API documentation is at:
* http://api.rubyonrails.org
-Bug reports can be filed for the Ruby on Rails project here:
+Bug reports for the Ruby on Rails project can be filed here:
* https://github.com/rails/rails/issues
Feature requests should be discussed on the rails-core mailing list here:
* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core
-
diff --git a/activerecord/RUNNING_UNIT_TESTS.rdoc b/activerecord/RUNNING_UNIT_TESTS.rdoc
index bae40604b1..60561e2c0f 100644
--- a/activerecord/RUNNING_UNIT_TESTS.rdoc
+++ b/activerecord/RUNNING_UNIT_TESTS.rdoc
@@ -7,11 +7,11 @@ http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#setting-up-
To run a specific test:
- $ ruby -Itest test/cases/base_test.rb -n method_name
+ $ bundle exec ruby -Itest test/cases/base_test.rb -n method_name
To run a set of tests:
- $ ruby -Itest test/cases/base_test.rb
+ $ bundle exec ruby -Itest test/cases/base_test.rb
You can also run tests that depend upon a specific database backend. For
example:
@@ -20,7 +20,6 @@ example:
Simply executing <tt>bundle exec rake test</tt> is equivalent to the following:
- $ bundle exec rake test:mysql
$ bundle exec rake test:mysql2
$ bundle exec rake test:postgresql
$ bundle exec rake test:sqlite3
@@ -42,7 +41,11 @@ parameters in +test/config.example.yml+ are.
You can override the +connections:+ parameter in either file using the +ARCONN+
(Active Record CONNection) environment variable:
- $ ARCONN=postgresql ruby -Itest test/cases/base_test.rb
+ $ ARCONN=postgresql bundle exec ruby -Itest test/cases/base_test.rb
+
+Or
+
+ $ bundle exec rake test:postgresql TEST=test/cases/base_test.rb
You can specify a custom location for the config file using the +ARCONFIG+
environment variable.
diff --git a/activerecord/Rakefile b/activerecord/Rakefile
index a619204e6f..fae56a51bb 100644
--- a/activerecord/Rakefile
+++ b/activerecord/Rakefile
@@ -1,7 +1,9 @@
-require 'rake/testtask'
+# frozen_string_literal: true
-require File.expand_path(File.dirname(__FILE__)) + "/test/config"
-require File.expand_path(File.dirname(__FILE__)) + "/test/support/config"
+require "rake/testtask"
+
+require_relative "test/config"
+require_relative "test/support/config"
def run_without_aborting(*tasks)
errors = []
@@ -17,14 +19,16 @@ def run_without_aborting(*tasks)
abort "Errors running #{errors.join(', ')}" if errors.any?
end
-desc 'Run mysql, mysql2, sqlite, and postgresql tests by default'
-task :default => :test
+desc "Run mysql2, sqlite, and postgresql tests by default"
+task default: :test
+
+task :package
-desc 'Run mysql, mysql2, sqlite, and postgresql tests'
+desc "Run mysql2, sqlite, and postgresql tests"
task :test do
tasks = defined?(JRUBY_VERSION) ?
%w(test_jdbcmysql test_jdbcsqlite3 test_jdbcpostgresql) :
- %w(test_mysql test_mysql2 test_sqlite3 test_postgresql)
+ %w(test_mysql2 test_sqlite3 test_postgresql)
run_without_aborting(*tasks)
end
@@ -32,24 +36,26 @@ namespace :test do
task :isolated do
tasks = defined?(JRUBY_VERSION) ?
%w(isolated_test_jdbcmysql isolated_test_jdbcsqlite3 isolated_test_jdbcpostgresql) :
- %w(isolated_test_mysql isolated_test_mysql2 isolated_test_sqlite3 isolated_test_postgresql)
+ %w(isolated_test_mysql2 isolated_test_sqlite3 isolated_test_postgresql)
run_without_aborting(*tasks)
end
end
-desc 'Build MySQL and PostgreSQL test databases'
namespace :db do
- task :create => ['db:mysql:build', 'db:postgresql:build']
- task :drop => ['db:mysql:drop', 'db:postgresql:drop']
+ desc "Build MySQL and PostgreSQL test databases"
+ task create: ["db:mysql:build", "db:postgresql:build"]
+
+ desc "Drop MySQL and PostgreSQL test databases"
+ task drop: ["db:mysql:drop", "db:postgresql:drop"]
end
-%w( mysql mysql2 postgresql sqlite3 sqlite3_mem db2 oracle jdbcmysql jdbcpostgresql jdbcsqlite3 jdbcderby jdbch2 jdbchsqldb ).each do |adapter|
+%w( mysql2 postgresql sqlite3 sqlite3_mem db2 oracle jdbcmysql jdbcpostgresql jdbcsqlite3 jdbcderby jdbch2 jdbchsqldb ).each do |adapter|
namespace :test do
Rake::TestTask.new(adapter => "#{adapter}:env") { |t|
- adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z0-9]+/]
- t.libs << 'test'
- t.test_files = (Dir.glob( "test/cases/**/*_test.rb" ).reject {
- |x| x =~ /\/adapters\//
+ adapter_short = adapter == "db2" ? adapter : adapter[/^[a-z0-9]+/]
+ t.libs << "test"
+ t.test_files = (Dir.glob("test/cases/**/*_test.rb").reject {
+ |x| x.include?("/adapters/")
} + Dir.glob("test/cases/adapters/#{adapter_short}/**/*_test.rb"))
t.warning = true
@@ -59,23 +65,23 @@ end
namespace :isolated do
task adapter => "#{adapter}:env" do
- adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z0-9]+/]
+ adapter_short = adapter == "db2" ? adapter : adapter[/^[a-z0-9]+/]
puts [adapter, adapter_short].inspect
(Dir["test/cases/**/*_test.rb"].reject {
- |x| x =~ /\/adapters\//
+ |x| x.include?("/adapters/")
} + Dir["test/cases/adapters/#{adapter_short}/**/*_test.rb"]).all? do |file|
- sh(Gem.ruby, '-w' ,"-Itest", file)
- end or raise "Failures"
+ sh(Gem.ruby, "-w", "-Itest", file)
+ end || raise("Failures")
end
end
end
namespace adapter do
- task :test => "test_#{adapter}"
- task :isolated_test => "isolated_test_#{adapter}"
+ task test: "test_#{adapter}"
+ task isolated_test: "isolated_test_#{adapter}"
# Set the connection environment for the adapter
- task(:env) { ENV['ARCONN'] = adapter }
+ task(:env) { ENV["ARCONN"] = adapter }
end
# Make sure the adapter test evaluates the env setting task
@@ -83,70 +89,56 @@ end
task "isolated_test_#{adapter}" => ["#{adapter}:env", "test:isolated:#{adapter}"]
end
-rule '.sqlite3' do |t|
- sh %Q{sqlite3 "#{t.name}" "create table a (a integer); drop table a;"}
-end
-
-task :test_sqlite3 => [
- 'test/fixtures/fixture_database.sqlite3',
- 'test/fixtures/fixture_database_2.sqlite3'
-]
-
namespace :db do
namespace :mysql do
- desc 'Build the MySQL test databases'
+ desc "Build the MySQL test databases"
task :build do
- config = ARTest.config['connections']['mysql']
- %x( mysql --user=#{config['arunit']['username']} -e "create DATABASE #{config['arunit']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ")
- %x( mysql --user=#{config['arunit2']['username']} -e "create DATABASE #{config['arunit2']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ")
+ config = ARTest.config["connections"]["mysql2"]
+ %x( mysql --user=#{config["arunit"]["username"]} --password=#{config["arunit"]["password"]} -e "create DATABASE #{config["arunit"]["database"]} DEFAULT CHARACTER SET utf8mb4" )
+ %x( mysql --user=#{config["arunit2"]["username"]} --password=#{config["arunit2"]["password"]} -e "create DATABASE #{config["arunit2"]["database"]} DEFAULT CHARACTER SET utf8mb4" )
end
- desc 'Drop the MySQL test databases'
+ desc "Drop the MySQL test databases"
task :drop do
- config = ARTest.config['connections']['mysql']
- %x( mysqladmin --user=#{config['arunit']['username']} -f drop #{config['arunit']['database']} )
- %x( mysqladmin --user=#{config['arunit2']['username']} -f drop #{config['arunit2']['database']} )
+ config = ARTest.config["connections"]["mysql2"]
+ %x( mysqladmin --user=#{config["arunit"]["username"]} --password=#{config["arunit"]["password"]} -f drop #{config["arunit"]["database"]} )
+ %x( mysqladmin --user=#{config["arunit2"]["username"]} --password=#{config["arunit2"]["password"]} -f drop #{config["arunit2"]["database"]} )
end
- desc 'Rebuild the MySQL test databases'
- task :rebuild => [:drop, :build]
+ desc "Rebuild the MySQL test databases"
+ task rebuild: [:drop, :build]
end
namespace :postgresql do
- desc 'Build the PostgreSQL test databases'
+ desc "Build the PostgreSQL test databases"
task :build 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
+ config = ARTest.config["connections"]["postgresql"]
+ %x( createdb -E UTF8 -T template0 #{config["arunit"]["database"]} )
+ %x( createdb -E UTF8 -T template0 #{config["arunit2"]["database"]} )
end
- desc 'Drop the PostgreSQL test databases'
+ desc "Drop the PostgreSQL test databases"
task :drop do
- config = ARTest.config['connections']['postgresql']
- %x( dropdb #{config['arunit']['database']} )
- %x( dropdb #{config['arunit2']['database']} )
+ config = ARTest.config["connections"]["postgresql"]
+ %x( dropdb #{config["arunit"]["database"]} )
+ %x( dropdb #{config["arunit2"]["database"]} )
end
- desc 'Rebuild the PostgreSQL test databases'
- task :rebuild => [:drop, :build]
+ desc "Rebuild the PostgreSQL test databases"
+ task rebuild: [:drop, :build]
end
end
-task :build_mysql_databases => 'db:mysql:build'
-task :drop_mysql_databases => 'db:mysql:drop'
-task :rebuild_mysql_databases => 'db:mysql:rebuild'
+task build_mysql_databases: "db:mysql:build"
+task drop_mysql_databases: "db:mysql:drop"
+task rebuild_mysql_databases: "db:mysql:rebuild"
-task :build_postgresql_databases => 'db:postgresql:build'
-task :drop_postgresql_databases => 'db:postgresql:drop'
-task :rebuild_postgresql_databases => 'db:postgresql:rebuild'
+task build_postgresql_databases: "db:postgresql:build"
+task drop_postgresql_databases: "db:postgresql:drop"
+task rebuild_postgresql_databases: "db:postgresql:rebuild"
task :lines do
- load File.expand_path('..', File.dirname(__FILE__)) + '/tools/line_statistics'
+ load File.expand_path("../tools/line_statistics", __dir__)
files = FileList["lib/active_record/**/*.rb"]
CodeTools::LineStatistics.new(files).print_loc
end
diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec
index bd95b57303..bcdd82052c 100644
--- a/activerecord/activerecord.gemspec
+++ b/activerecord/activerecord.gemspec
@@ -1,28 +1,36 @@
-version = File.read(File.expand_path('../../RAILS_VERSION', __FILE__)).strip
+# frozen_string_literal: true
+
+version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip
Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
- s.name = 'activerecord'
+ s.name = "activerecord"
s.version = version
- s.summary = 'Object-relational mapper framework (part of Rails).'
- s.description = 'Databases on Rails. Build a persistent domain model by mapping database tables to Ruby classes. Strong conventions for associations, validations, aggregations, migrations, and testing come baked-in.'
+ s.summary = "Object-relational mapper framework (part of Rails)."
+ s.description = "Databases on Rails. Build a persistent domain model by mapping database tables to Ruby classes. Strong conventions for associations, validations, aggregations, migrations, and testing come baked-in."
- s.required_ruby_version = '>= 2.2.2'
+ s.required_ruby_version = ">= 2.4.1"
- s.license = 'MIT'
+ s.license = "MIT"
- s.author = 'David Heinemeier Hansson'
- s.email = 'david@loudthinking.com'
- s.homepage = 'http://www.rubyonrails.org'
+ s.author = "David Heinemeier Hansson"
+ s.email = "david@loudthinking.com"
+ s.homepage = "http://rubyonrails.org"
- s.files = Dir['CHANGELOG.md', 'MIT-LICENSE', 'README.rdoc', 'examples/**/*', 'lib/**/*']
- s.require_path = 'lib'
+ s.files = Dir["CHANGELOG.md", "MIT-LICENSE", "README.rdoc", "examples/**/*", "lib/**/*"]
+ s.require_path = "lib"
s.extra_rdoc_files = %w(README.rdoc)
- s.rdoc_options.concat ['--main', 'README.rdoc']
+ s.rdoc_options.concat ["--main", "README.rdoc"]
+
+ s.metadata = {
+ "source_code_uri" => "https://github.com/rails/rails/tree/v#{version}/activerecord",
+ "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/activerecord/CHANGELOG.md"
+ }
- s.add_dependency 'activesupport', version
- s.add_dependency 'activemodel', version
+ # NOTE: Please read our dependency guidelines before updating versions:
+ # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves
- s.add_dependency 'arel', '7.0.0.alpha'
+ s.add_dependency "activesupport", version
+ s.add_dependency "activemodel", version
end
diff --git a/activerecord/bin/test b/activerecord/bin/test
index f8adf2aabc..9ecf27ce67 100755
--- a/activerecord/bin/test
+++ b/activerecord/bin/test
@@ -1,12 +1,21 @@
#!/usr/bin/env ruby
-COMPONENT_ROOT = File.expand_path("../../", __FILE__)
-require File.expand_path("../tools/test", COMPONENT_ROOT)
+# frozen_string_literal: true
+
+adapter_index = ARGV.index("--adapter") || ARGV.index("-a")
+if adapter_index
+ ARGV.delete_at(adapter_index)
+ ENV["ARCONN"] = ARGV.delete_at(adapter_index).strip
+end
+
+COMPONENT_ROOT = File.expand_path("..", __dir__)
+require_relative "../../tools/test"
+
module Minitest
def self.plugin_active_record_options(opts, options)
opts.separator ""
opts.separator "Active Record options:"
opts.on("-a", "--adapter [ADAPTER]",
- "Run tests using a specific adapter (sqlite3, sqlite3_mem, mysql, mysql2, postgresql)") do |adapter|
+ "Run tests using a specific adapter (sqlite3, sqlite3_mem, mysql2, postgresql)") do |adapter|
ENV["ARCONN"] = adapter.strip
end
@@ -14,6 +23,5 @@ module Minitest
end
end
-Minitest.extensions.unshift 'active_record'
-
-exit Minitest.run(ARGV)
+Minitest.load_plugins
+Minitest.extensions.unshift "active_record"
diff --git a/activerecord/examples/.gitignore b/activerecord/examples/.gitignore
deleted file mode 100644
index 0dfc1cb7fb..0000000000
--- a/activerecord/examples/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-performance.sql
diff --git a/activerecord/examples/performance.rb b/activerecord/examples/performance.rb
index a5a1f284a0..024e503ec7 100644
--- a/activerecord/examples/performance.rb
+++ b/activerecord/examples/performance.rb
@@ -1,11 +1,12 @@
-require File.expand_path('../../../load_paths', __FILE__)
+# frozen_string_literal: true
+
require "active_record"
-require 'benchmark/ips'
+require "benchmark/ips"
-TIME = (ENV['BENCHMARK_TIME'] || 20).to_i
-RECORDS = (ENV['BENCHMARK_RECORDS'] || TIME*1000).to_i
+TIME = (ENV["BENCHMARK_TIME"] || 20).to_i
+RECORDS = (ENV["BENCHMARK_RECORDS"] || TIME * 1000).to_i
-conn = { adapter: 'sqlite3', database: ':memory:' }
+conn = { adapter: "sqlite3", database: ":memory:" }
ActiveRecord::Base.establish_connection(conn)
@@ -43,26 +44,26 @@ class Exhibit < ActiveRecord::Base
def self.feel(exhibits) exhibits.each(&:feel) end
end
-def progress_bar(int); print "." if (int%100).zero? ; end
+def progress_bar(int); print "." if (int % 100).zero? ; end
-puts 'Generating data...'
+puts "Generating data..."
module ActiveRecord
class Faker
- LOREM = %Q{Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse non aliquet diam. Curabitur vel urna metus, quis malesuada elit.
+ LOREM = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse non aliquet diam. Curabitur vel urna metus, quis malesuada elit.
Integer consequat tincidunt felis. Etiam non erat dolor. Vivamus imperdiet nibh sit amet diam eleifend id posuere diam malesuada. Mauris at accumsan sem.
Donec id lorem neque. Fusce erat lorem, ornare eu congue vitae, malesuada quis neque. Maecenas vel urna a velit pretium fermentum. Donec tortor enim,
tempor venenatis egestas a, tempor sed ipsum. Ut arcu justo, faucibus non imperdiet ac, interdum at diam. Pellentesque ipsum enim, venenatis ut iaculis vitae,
varius vitae sem. Sed rutrum quam ac elit euismod bibendum. Donec ultricies ultricies magna, at lacinia libero mollis aliquam. Sed ac arcu in tortor elementum
tincidunt vel interdum sem. Curabitur eget erat arcu. Praesent eget eros leo. Nam magna enim, sollicitudin vehicula scelerisque in, vulputate ut libero.
- Praesent varius tincidunt commodo}.split
+ Praesent varius tincidunt commodo".split
def self.name
- LOREM.grep(/^\w*$/).sort_by { rand }.first(2).join ' '
+ LOREM.grep(/^\w*$/).sort_by { rand }.first(2).join " "
end
def self.email
- LOREM.grep(/^\w*$/).sort_by { rand }.first(2).join('@') + ".com"
+ LOREM.grep(/^\w*$/).sort_by { rand }.first(2).join("@") + ".com"
end
end
end
@@ -73,7 +74,7 @@ end
# Using the same paragraph for all exhibits because it is very slow
# to generate unique paragraphs for all exhibits.
-notes = ActiveRecord::Faker::LOREM.join ' '
+notes = ActiveRecord::Faker::LOREM.join " "
today = Date.today
puts "Inserting #{RECORDS} users and exhibits..."
@@ -96,9 +97,9 @@ puts "Done!\n"
Benchmark.ips(TIME) do |x|
ar_obj = Exhibit.find(1)
- attrs = { name: 'sam' }
- attrs_first = { name: 'sam' }
- attrs_second = { name: 'tom' }
+ attrs = { name: "sam" }
+ attrs_first = { name: "sam" }
+ attrs_second = { name: "tom" }
exhibit = {
name: ActiveRecord::Faker.name,
notes: notes,
@@ -109,22 +110,22 @@ Benchmark.ips(TIME) do |x|
ar_obj.id
end
- x.report 'Model.new (instantiation)' do
+ x.report "Model.new (instantiation)" do
Exhibit.new
end
- x.report 'Model.new (setting attributes)' do
+ x.report "Model.new (setting attributes)" do
Exhibit.new(attrs)
end
- x.report 'Model.first' do
+ x.report "Model.first" do
Exhibit.first.look
end
- x.report 'Model.take' do
+ x.report "Model.take" do
Exhibit.take
end
-
+
x.report("Model.all limit(100)") do
Exhibit.look Exhibit.limit(100)
end
@@ -141,41 +142,41 @@ Benchmark.ips(TIME) do |x|
Exhibit.look Exhibit.limit(10000)
end
- x.report 'Model.named_scope' do
+ x.report "Model.named_scope" do
Exhibit.limit(10).with_name.with_notes
end
- x.report 'Model.create' do
+ x.report "Model.create" do
Exhibit.create(exhibit)
end
- x.report 'Resource#attributes=' do
+ x.report "Resource#attributes=" do
e = Exhibit.new(attrs_first)
e.attributes = attrs_second
end
- x.report 'Resource#update' do
- Exhibit.first.update(name: 'bob')
+ x.report "Resource#update" do
+ Exhibit.first.update(name: "bob")
end
- x.report 'Resource#destroy' do
+ x.report "Resource#destroy" do
Exhibit.first.destroy
end
- x.report 'Model.transaction' do
+ x.report "Model.transaction" do
Exhibit.transaction { Exhibit.new }
end
- x.report 'Model.find(id)' do
+ x.report "Model.find(id)" do
User.find(1)
end
- x.report 'Model.find_by_sql' do
+ x.report "Model.find_by_sql" do
Exhibit.find_by_sql("SELECT * FROM exhibits WHERE id = #{(rand * 1000 + 1).to_i}").first
end
x.report "Model.log" do
- Exhibit.connection.send(:log, "hello", "world") {}
+ Exhibit.connection.send(:log, "hello", "world") { }
end
x.report "AR.execute(query)" do
diff --git a/activerecord/examples/simple.rb b/activerecord/examples/simple.rb
index 4ed5d80eb2..280b786d73 100644
--- a/activerecord/examples/simple.rb
+++ b/activerecord/examples/simple.rb
@@ -1,14 +1,15 @@
-require File.expand_path('../../../load_paths', __FILE__)
-require 'active_record'
+# frozen_string_literal: true
+
+require "active_record"
class Person < ActiveRecord::Base
- establish_connection adapter: 'sqlite3', database: 'foobar.db'
+ establish_connection adapter: "sqlite3", database: "foobar.db"
connection.create_table table_name, force: true do |t|
t.string :name
end
end
-bob = Person.create!(name: 'bob')
+bob = Person.create!(name: "bob")
puts Person.all.inspect
bob.destroy
puts Person.all.inspect
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index f5cf92db64..d43378c64f 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
#--
-# Copyright (c) 2004-2015 David Heinemeier Hansson
+# Copyright (c) 2004-2018 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -21,18 +23,18 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
-require 'active_support'
-require 'active_support/rails'
-require 'active_model'
-require 'arel'
+require "active_support"
+require "active_support/rails"
+require "active_model"
+require "arel"
+require "yaml"
-require 'active_record/version'
-require 'active_record/attribute_set'
+require "active_record/version"
+require "active_model/attribute_set"
module ActiveRecord
extend ActiveSupport::Autoload
- autoload :Attribute
autoload :Base
autoload :Callbacks
autoload :Core
@@ -40,12 +42,12 @@ module ActiveRecord
autoload :CounterCache
autoload :DynamicMatchers
autoload :Enum
+ autoload :InternalMetadata
autoload :Explain
autoload :Inheritance
autoload :Integration
- autoload :LegacyYamlAdapter
autoload :Migration
- autoload :Migrator, 'active_record/migration'
+ autoload :Migrator, "active_record/migration"
autoload :ModelSchema
autoload :NestedAttributes
autoload :NoTouching
@@ -53,8 +55,9 @@ module ActiveRecord
autoload :Persistence
autoload :QueryCache
autoload :Querying
+ autoload :CollectionCacheKey
autoload :ReadonlyAttributes
- autoload :RecordInvalid, 'active_record/validations'
+ autoload :RecordInvalid, "active_record/validations"
autoload :Reflection
autoload :RuntimeRegistry
autoload :Sanitization
@@ -66,7 +69,6 @@ module ActiveRecord
autoload :StatementCache
autoload :Store
autoload :Suppressor
- autoload :TableMetadata
autoload :Timestamp
autoload :Transactions
autoload :Translation
@@ -74,9 +76,9 @@ module ActiveRecord
autoload :SecureToken
eager_autoload do
- autoload :ActiveRecordError, 'active_record/errors'
- autoload :ConnectionNotEstablished, 'active_record/errors'
- autoload :ConnectionAdapters, 'active_record/connection_adapters/abstract_adapter'
+ autoload :ActiveRecordError, "active_record/errors"
+ autoload :ConnectionNotEstablished, "active_record/errors"
+ autoload :ConnectionAdapters, "active_record/connection_adapters/abstract_adapter"
autoload :Aggregations
autoload :Associations
@@ -84,11 +86,13 @@ module ActiveRecord
autoload :AttributeMethods
autoload :AutosaveAssociation
+ autoload :LegacyYamlAdapter
+
autoload :Relation
autoload :AssociationRelation
autoload :NullRelation
- autoload_under 'relation' do
+ autoload_under "relation" do
autoload :QueryMethods
autoload :FinderMethods
autoload :Calculations
@@ -99,11 +103,13 @@ module ActiveRecord
end
autoload :Result
+ autoload :TableMetadata
+ autoload :Type
end
module Coders
- autoload :YAMLColumn, 'active_record/coders/yaml_column'
- autoload :JSON, 'active_record/coders/json'
+ autoload :YAMLColumn, "active_record/coders/yaml_column"
+ autoload :JSON, "active_record/coders/json"
end
module AttributeMethods
@@ -135,7 +141,6 @@ module ActiveRecord
eager_autoload do
autoload :AbstractAdapter
- autoload :ConnectionManagement, "active_record/connection_adapters/abstract/connection_pool"
end
end
@@ -152,13 +157,14 @@ module ActiveRecord
extend ActiveSupport::Autoload
autoload :DatabaseTasks
- autoload :SQLiteDatabaseTasks, 'active_record/tasks/sqlite_database_tasks'
- autoload :MySQLDatabaseTasks, 'active_record/tasks/mysql_database_tasks'
+ autoload :SQLiteDatabaseTasks, "active_record/tasks/sqlite_database_tasks"
+ autoload :MySQLDatabaseTasks, "active_record/tasks/mysql_database_tasks"
autoload :PostgreSQLDatabaseTasks,
- 'active_record/tasks/postgresql_database_tasks'
+ "active_record/tasks/postgresql_database_tasks"
end
- autoload :TestFixtures, 'active_record/fixtures'
+ autoload :TestDatabases, "active_record/test_databases"
+ autoload :TestFixtures, "active_record/fixtures"
def self.eager_load!
super
@@ -175,5 +181,9 @@ ActiveSupport.on_load(:active_record) do
end
ActiveSupport.on_load(:i18n) do
- I18n.load_path << File.dirname(__FILE__) + '/active_record/locale/en.yml'
+ I18n.load_path << File.expand_path("active_record/locale/en.yml", __dir__)
end
+
+YAML.load_tags["!ruby/object:ActiveRecord::AttributeSet"] = "ActiveModel::AttributeSet"
+YAML.load_tags["!ruby/object:ActiveRecord::Attribute::FromDatabase"] = "ActiveModel::Attribute::FromDatabase"
+YAML.load_tags["!ruby/object:ActiveRecord::LazyAttributeHash"] = "ActiveModel::LazyAttributeHash"
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb
index f7b50cd25a..3250e29b82 100644
--- a/activerecord/lib/active_record/aggregations.rb
+++ b/activerecord/lib/active_record/aggregations.rb
@@ -1,8 +1,8 @@
-module ActiveRecord
- # = Active Record Aggregations
- module Aggregations # :nodoc:
- extend ActiveSupport::Concern
+# frozen_string_literal: true
+module ActiveRecord
+ # See ActiveRecord::Aggregations::ClassMethods for documentation
+ module Aggregations
def initialize_dup(*) # :nodoc:
@aggregation_cache = {}
super
@@ -15,265 +15,271 @@ 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
- # Active Record implements aggregation through a macro-like class method called +composed_of+
- # for representing attributes as value objects. It expresses relationships like "Account [is]
- # composed of Money [among other things]" or "Person [is] composed of [an] address". Each call
- # to the macro adds a description of how the value objects are created from the attributes of
- # the entity object (when the entity is initialized either as a new object or from finding an
- # existing object) and how it can be turned back into attributes (when the entity is saved to
- # the database).
- #
- # class Customer < ActiveRecord::Base
- # composed_of :balance, class_name: "Money", mapping: %w(balance amount)
- # composed_of :address, mapping: [ %w(address_street street), %w(address_city city) ]
- # end
- #
- # The customer class now has the following methods to manipulate the value objects:
- # * <tt>Customer#balance, Customer#balance=(money)</tt>
- # * <tt>Customer#address, Customer#address=(address)</tt>
- #
- # These methods will operate with value objects like the ones described below:
- #
- # class Money
- # include Comparable
- # attr_reader :amount, :currency
- # EXCHANGE_RATES = { "USD_TO_DKK" => 6 }
- #
- # def initialize(amount, currency = "USD")
- # @amount, @currency = amount, currency
- # end
- #
- # def exchange_to(other_currency)
- # exchanged_amount = (amount * EXCHANGE_RATES["#{currency}_TO_#{other_currency}"]).floor
- # Money.new(exchanged_amount, other_currency)
- # end
- #
- # def ==(other_money)
- # amount == other_money.amount && currency == other_money.currency
- # end
- #
- # def <=>(other_money)
- # if currency == other_money.currency
- # amount <=> other_money.amount
- # else
- # amount <=> other_money.exchange_to(currency).amount
- # end
- # end
- # end
- #
- # class Address
- # attr_reader :street, :city
- # def initialize(street, city)
- # @street, @city = street, city
- # end
- #
- # def close_to?(other_address)
- # city == other_address.city
- # end
- #
- # def ==(other_address)
- # city == other_address.city && street == other_address.street
- # end
- # end
- #
- # Now it's possible to access attributes from the database through the value objects instead. If
- # you choose to name the composition the same as the attribute's name, it will be the only way to
- # access that attribute. That's the case with our +balance+ attribute. You interact with the value
- # objects just like you would with any other attribute:
- #
- # customer.balance = Money.new(20) # sets the Money value object and the attribute
- # customer.balance # => Money value object
- # customer.balance.exchange_to("DKK") # => Money.new(120, "DKK")
- # customer.balance > Money.new(10) # => true
- # customer.balance == Money.new(20) # => true
- # customer.balance < Money.new(5) # => false
- #
- # Value objects can also be composed of multiple attributes, such as the case of Address. The order
- # of the mappings will determine the order of the parameters.
- #
- # customer.address_street = "Hyancintvej"
- # customer.address_city = "Copenhagen"
- # customer.address # => Address.new("Hyancintvej", "Copenhagen")
- #
- # customer.address = Address.new("May Street", "Chicago")
- # customer.address_street # => "May Street"
- # customer.address_city # => "Chicago"
- #
- # == Writing value objects
- #
- # Value objects are immutable and interchangeable objects that represent a given value, such as
- # a Money object representing $5. Two Money objects both representing $5 should be equal (through
- # methods such as <tt>==</tt> and <tt><=></tt> from Comparable if ranking makes sense). This is
- # unlike entity objects where equality is determined by identity. An entity class such as Customer can
- # easily have two different objects that both have an address on Hyancintvej. Entity identity is
- # determined by object or relational unique identifiers (such as primary keys). Normal
- # ActiveRecord::Base classes are entity objects.
- #
- # It's also important to treat the value objects as immutable. Don't allow the Money object to have
- # its amount changed after creation. Create a new Money object with the new value instead. The
- # Money#exchange_to method is an example of this. It returns a new value object instead of changing
- # its own values. Active Record won't persist value objects that have been changed through means
- # other than the writer method.
- #
- # The immutable requirement is enforced by Active Record by freezing any object assigned as a value
- # object. Attempting to change it afterwards will result in a RuntimeError.
- #
- # Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not
- # keeping value objects immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable
- #
- # == Custom constructors and converters
- #
- # By default value objects are initialized by calling the <tt>new</tt> constructor of the value
- # class passing each of the mapped attributes, in the order specified by the <tt>:mapping</tt>
- # option, as arguments. If the value class doesn't support this convention then +composed_of+ allows
- # a custom constructor to be specified.
- #
- # When a new value is assigned to the value object, the default assumption is that the new value
- # is an instance of the value class. Specifying a custom converter allows the new value to be automatically
- # converted to an instance of value class if necessary.
- #
- # For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that should be
- # aggregated using the NetAddr::CIDR value class (http://www.rubydoc.info/gems/netaddr/1.5.0/NetAddr/CIDR).
- # The constructor for the value class is called +create+ and it expects a CIDR address string as a parameter.
- # New values can be assigned to the value object using either another NetAddr::CIDR object, a string
- # or an array. The <tt>:constructor</tt> and <tt>:converter</tt> options can be used to meet
- # these requirements:
- #
- # class NetworkResource < ActiveRecord::Base
- # composed_of :cidr,
- # class_name: 'NetAddr::CIDR',
- # mapping: [ %w(network_address network), %w(cidr_range bits) ],
- # allow_nil: true,
- # constructor: Proc.new { |network_address, cidr_range| NetAddr::CIDR.create("#{network_address}/#{cidr_range}") },
- # converter: Proc.new { |value| NetAddr::CIDR.create(value.is_a?(Array) ? value.join('/') : value) }
- # end
- #
- # # This calls the :constructor
- # network_resource = NetworkResource.new(network_address: '192.168.0.1', cidr_range: 24)
- #
- # # These assignments will both use the :converter
- # network_resource.cidr = [ '192.168.2.1', 8 ]
- # network_resource.cidr = '192.168.0.1/24'
- #
- # # This assignment won't use the :converter as the value is already an instance of the value class
- # network_resource.cidr = NetAddr::CIDR.create('192.168.2.1/8')
- #
- # # Saving and then reloading will use the :constructor on reload
- # network_resource.save
- # network_resource.reload
- #
- # == Finding records by a value object
- #
- # Once a +composed_of+ relationship is specified for a model, records can be loaded from the database
- # by specifying an instance of the value object in the conditions hash. The following example
- # finds all customers with +balance_amount+ equal to 20 and +balance_currency+ equal to "USD":
- #
- # Customer.where(balance: Money.new(20, "USD"))
- #
- module ClassMethods
- # Adds reader and writer methods for manipulating a value object:
- # <tt>composed_of :address</tt> adds <tt>address</tt> and <tt>address=(new_address)</tt> methods.
- #
- # Options are:
- # * <tt>:class_name</tt> - Specifies the class name of the association. Use it only if that name
- # can't be inferred from the part id. So <tt>composed_of :address</tt> will by default be linked
- # to the Address class, but if the real class name is CompanyAddress, you'll have to specify it
- # with this option.
- # * <tt>:mapping</tt> - Specifies the mapping of entity attributes to attributes of the value
- # object. Each mapping is represented as an array where the first item is the name of the
- # entity attribute and the second item is the name of the attribute in the value object. The
- # order in which mappings are defined determines the order in which attributes are sent to the
- # value class constructor.
- # * <tt>:allow_nil</tt> - Specifies that the value object will not be instantiated when all mapped
- # attributes are +nil+. Setting the value object to +nil+ has the effect of writing +nil+ to all
- # mapped attributes.
- # This defaults to +false+.
- # * <tt>:constructor</tt> - A symbol specifying the name of the constructor method or a Proc that
- # is called to initialize the value object. The constructor is passed all of the mapped attributes,
- # in the order that they are defined in the <tt>:mapping option</tt>, as arguments and uses them
- # to instantiate a <tt>:class_name</tt> object.
- # The default is <tt>:new</tt>.
- # * <tt>:converter</tt> - A symbol specifying the name of a class method of <tt>:class_name</tt>
- # or a Proc that is called when a new value is assigned to the value object. The converter is
- # passed the single value that is used in the assignment and is only called if the new value is
- # not an instance of <tt>:class_name</tt>. If <tt>:allow_nil</tt> is set to true, the converter
- # can return nil to skip the assignment.
- #
- # Option examples:
- # composed_of :temperature, mapping: %w(reading celsius)
- # composed_of :balance, class_name: "Money", mapping: %w(balance amount),
- # converter: Proc.new { |balance| balance.to_money }
- # composed_of :address, mapping: [ %w(address_street street), %w(address_city city) ]
- # composed_of :gps_location
- # composed_of :gps_location, allow_nil: true
- # composed_of :ip_address,
- # class_name: 'IPAddr',
- # mapping: %w(ip to_i),
- # constructor: Proc.new { |ip| IPAddr.new(ip, Socket::AF_INET) },
- # converter: Proc.new { |ip| ip.is_a?(Integer) ? IPAddr.new(ip, Socket::AF_INET) : IPAddr.new(ip.to_s) }
- #
- def composed_of(part_id, options = {})
- options.assert_valid_keys(:class_name, :mapping, :allow_nil, :constructor, :converter)
+ # Active Record implements aggregation through a macro-like class method called #composed_of
+ # for representing attributes as value objects. It expresses relationships like "Account [is]
+ # composed of Money [among other things]" or "Person [is] composed of [an] address". Each call
+ # to the macro adds a description of how the value objects are created from the attributes of
+ # the entity object (when the entity is initialized either as a new object or from finding an
+ # existing object) and how it can be turned back into attributes (when the entity is saved to
+ # the database).
+ #
+ # class Customer < ActiveRecord::Base
+ # composed_of :balance, class_name: "Money", mapping: %w(balance amount)
+ # composed_of :address, mapping: [ %w(address_street street), %w(address_city city) ]
+ # end
+ #
+ # The customer class now has the following methods to manipulate the value objects:
+ # * <tt>Customer#balance, Customer#balance=(money)</tt>
+ # * <tt>Customer#address, Customer#address=(address)</tt>
+ #
+ # These methods will operate with value objects like the ones described below:
+ #
+ # class Money
+ # include Comparable
+ # attr_reader :amount, :currency
+ # EXCHANGE_RATES = { "USD_TO_DKK" => 6 }
+ #
+ # def initialize(amount, currency = "USD")
+ # @amount, @currency = amount, currency
+ # end
+ #
+ # def exchange_to(other_currency)
+ # exchanged_amount = (amount * EXCHANGE_RATES["#{currency}_TO_#{other_currency}"]).floor
+ # Money.new(exchanged_amount, other_currency)
+ # end
+ #
+ # def ==(other_money)
+ # amount == other_money.amount && currency == other_money.currency
+ # end
+ #
+ # def <=>(other_money)
+ # if currency == other_money.currency
+ # amount <=> other_money.amount
+ # else
+ # amount <=> other_money.exchange_to(currency).amount
+ # end
+ # end
+ # end
+ #
+ # class Address
+ # attr_reader :street, :city
+ # def initialize(street, city)
+ # @street, @city = street, city
+ # end
+ #
+ # def close_to?(other_address)
+ # city == other_address.city
+ # end
+ #
+ # def ==(other_address)
+ # city == other_address.city && street == other_address.street
+ # end
+ # end
+ #
+ # Now it's possible to access attributes from the database through the value objects instead. If
+ # you choose to name the composition the same as the attribute's name, it will be the only way to
+ # access that attribute. That's the case with our +balance+ attribute. You interact with the value
+ # objects just like you would with any other attribute:
+ #
+ # customer.balance = Money.new(20) # sets the Money value object and the attribute
+ # customer.balance # => Money value object
+ # customer.balance.exchange_to("DKK") # => Money.new(120, "DKK")
+ # customer.balance > Money.new(10) # => true
+ # customer.balance == Money.new(20) # => true
+ # customer.balance < Money.new(5) # => false
+ #
+ # Value objects can also be composed of multiple attributes, such as the case of Address. The order
+ # of the mappings will determine the order of the parameters.
+ #
+ # customer.address_street = "Hyancintvej"
+ # customer.address_city = "Copenhagen"
+ # customer.address # => Address.new("Hyancintvej", "Copenhagen")
+ #
+ # customer.address = Address.new("May Street", "Chicago")
+ # customer.address_street # => "May Street"
+ # customer.address_city # => "Chicago"
+ #
+ # == Writing value objects
+ #
+ # Value objects are immutable and interchangeable objects that represent a given value, such as
+ # a Money object representing $5. Two Money objects both representing $5 should be equal (through
+ # methods such as <tt>==</tt> and <tt><=></tt> from Comparable if ranking makes sense). This is
+ # unlike entity objects where equality is determined by identity. An entity class such as Customer can
+ # easily have two different objects that both have an address on Hyancintvej. Entity identity is
+ # determined by object or relational unique identifiers (such as primary keys). Normal
+ # ActiveRecord::Base classes are entity objects.
+ #
+ # It's also important to treat the value objects as immutable. Don't allow the Money object to have
+ # its amount changed after creation. Create a new Money object with the new value instead. The
+ # <tt>Money#exchange_to</tt> method is an example of this. It returns a new value object instead of changing
+ # its own values. Active Record won't persist value objects that have been changed through means
+ # other than the writer method.
+ #
+ # The immutable requirement is enforced by Active Record by freezing any object assigned as a value
+ # object. Attempting to change it afterwards will result in a +RuntimeError+.
+ #
+ # Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not
+ # keeping value objects immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable
+ #
+ # == Custom constructors and converters
+ #
+ # By default value objects are initialized by calling the <tt>new</tt> constructor of the value
+ # class passing each of the mapped attributes, in the order specified by the <tt>:mapping</tt>
+ # option, as arguments. If the value class doesn't support this convention then #composed_of allows
+ # a custom constructor to be specified.
+ #
+ # When a new value is assigned to the value object, the default assumption is that the new value
+ # is an instance of the value class. Specifying a custom converter allows the new value to be automatically
+ # converted to an instance of value class if necessary.
+ #
+ # For example, the +NetworkResource+ model has +network_address+ and +cidr_range+ attributes that should be
+ # aggregated using the +NetAddr::CIDR+ value class (http://www.rubydoc.info/gems/netaddr/1.5.0/NetAddr/CIDR).
+ # The constructor for the value class is called +create+ and it expects a CIDR address string as a parameter.
+ # New values can be assigned to the value object using either another +NetAddr::CIDR+ object, a string
+ # or an array. The <tt>:constructor</tt> and <tt>:converter</tt> options can be used to meet
+ # these requirements:
+ #
+ # class NetworkResource < ActiveRecord::Base
+ # composed_of :cidr,
+ # class_name: 'NetAddr::CIDR',
+ # mapping: [ %w(network_address network), %w(cidr_range bits) ],
+ # allow_nil: true,
+ # constructor: Proc.new { |network_address, cidr_range| NetAddr::CIDR.create("#{network_address}/#{cidr_range}") },
+ # converter: Proc.new { |value| NetAddr::CIDR.create(value.is_a?(Array) ? value.join('/') : value) }
+ # end
+ #
+ # # This calls the :constructor
+ # network_resource = NetworkResource.new(network_address: '192.168.0.1', cidr_range: 24)
+ #
+ # # These assignments will both use the :converter
+ # network_resource.cidr = [ '192.168.2.1', 8 ]
+ # network_resource.cidr = '192.168.0.1/24'
+ #
+ # # This assignment won't use the :converter as the value is already an instance of the value class
+ # network_resource.cidr = NetAddr::CIDR.create('192.168.2.1/8')
+ #
+ # # Saving and then reloading will use the :constructor on reload
+ # network_resource.save
+ # network_resource.reload
+ #
+ # == Finding records by a value object
+ #
+ # Once a #composed_of relationship is specified for a model, records can be loaded from the database
+ # by specifying an instance of the value object in the conditions hash. The following example
+ # finds all customers with +address_street+ equal to "May Street" and +address_city+ equal to "Chicago":
+ #
+ # Customer.where(address: Address.new("May Street", "Chicago"))
+ #
+ module ClassMethods
+ # Adds reader and writer methods for manipulating a value object:
+ # <tt>composed_of :address</tt> adds <tt>address</tt> and <tt>address=(new_address)</tt> methods.
+ #
+ # Options are:
+ # * <tt>:class_name</tt> - Specifies the class name of the association. Use it only if that name
+ # can't be inferred from the part id. So <tt>composed_of :address</tt> will by default be linked
+ # to the Address class, but if the real class name is +CompanyAddress+, you'll have to specify it
+ # with this option.
+ # * <tt>:mapping</tt> - Specifies the mapping of entity attributes to attributes of the value
+ # object. Each mapping is represented as an array where the first item is the name of the
+ # entity attribute and the second item is the name of the attribute in the value object. The
+ # order in which mappings are defined determines the order in which attributes are sent to the
+ # value class constructor.
+ # * <tt>:allow_nil</tt> - Specifies that the value object will not be instantiated when all mapped
+ # attributes are +nil+. Setting the value object to +nil+ has the effect of writing +nil+ to all
+ # mapped attributes.
+ # This defaults to +false+.
+ # * <tt>:constructor</tt> - A symbol specifying the name of the constructor method or a Proc that
+ # is called to initialize the value object. The constructor is passed all of the mapped attributes,
+ # in the order that they are defined in the <tt>:mapping option</tt>, as arguments and uses them
+ # to instantiate a <tt>:class_name</tt> object.
+ # The default is <tt>:new</tt>.
+ # * <tt>:converter</tt> - A symbol specifying the name of a class method of <tt>:class_name</tt>
+ # or a Proc that is called when a new value is assigned to the value object. The converter is
+ # passed the single value that is used in the assignment and is only called if the new value is
+ # not an instance of <tt>:class_name</tt>. If <tt>:allow_nil</tt> is set to true, the converter
+ # can return +nil+ to skip the assignment.
+ #
+ # Option examples:
+ # composed_of :temperature, mapping: %w(reading celsius)
+ # composed_of :balance, class_name: "Money", mapping: %w(balance amount)
+ # composed_of :address, mapping: [ %w(address_street street), %w(address_city city) ]
+ # composed_of :gps_location
+ # composed_of :gps_location, allow_nil: true
+ # composed_of :ip_address,
+ # class_name: 'IPAddr',
+ # mapping: %w(ip to_i),
+ # constructor: Proc.new { |ip| IPAddr.new(ip, Socket::AF_INET) },
+ # converter: Proc.new { |ip| ip.is_a?(Integer) ? IPAddr.new(ip, Socket::AF_INET) : IPAddr.new(ip.to_s) }
+ #
+ def composed_of(part_id, options = {})
+ options.assert_valid_keys(:class_name, :mapping, :allow_nil, :constructor, :converter)
- name = part_id.id2name
- class_name = options[:class_name] || name.camelize
- mapping = options[:mapping] || [ name, name ]
- mapping = [ mapping ] unless mapping.first.is_a?(Array)
- allow_nil = options[:allow_nil] || false
- constructor = options[:constructor] || :new
- converter = options[:converter]
+ unless self < Aggregations
+ include Aggregations
+ end
- reader_method(name, class_name, mapping, allow_nil, constructor)
- writer_method(name, class_name, mapping, allow_nil, converter)
+ name = part_id.id2name
+ class_name = options[:class_name] || name.camelize
+ mapping = options[:mapping] || [ name, name ]
+ mapping = [ mapping ] unless mapping.first.is_a?(Array)
+ allow_nil = options[:allow_nil] || false
+ constructor = options[:constructor] || :new
+ converter = options[:converter]
- reflection = ActiveRecord::Reflection.create(:composed_of, part_id, nil, options, self)
- Reflection.add_aggregate_reflection self, part_id, reflection
- end
+ reader_method(name, class_name, mapping, allow_nil, constructor)
+ writer_method(name, class_name, mapping, allow_nil, converter)
- private
- def reader_method(name, class_name, mapping, allow_nil, constructor)
- define_method(name) do
- if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|key, _| !_read_attribute(key).nil? })
- attrs = mapping.collect {|key, _| _read_attribute(key)}
- object = constructor.respond_to?(:call) ?
- constructor.call(*attrs) :
- class_name.constantize.send(constructor, *attrs)
- @aggregation_cache[name] = object
- end
- @aggregation_cache[name]
- end
+ reflection = ActiveRecord::Reflection.create(:composed_of, part_id, nil, options, self)
+ Reflection.add_aggregate_reflection self, part_id, reflection
end
- def writer_method(name, class_name, mapping, allow_nil, converter)
- define_method("#{name}=") do |part|
- klass = class_name.constantize
- if part.is_a?(Hash)
- raise ArgumentError unless part.size == part.keys.max
- part = klass.new(*part.sort.map(&:last))
+ private
+ def reader_method(name, class_name, mapping, allow_nil, constructor)
+ define_method(name) do
+ if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? { |key, _| !_read_attribute(key).nil? })
+ attrs = mapping.collect { |key, _| _read_attribute(key) }
+ object = constructor.respond_to?(:call) ?
+ constructor.call(*attrs) :
+ class_name.constantize.send(constructor, *attrs)
+ @aggregation_cache[name] = object
+ end
+ @aggregation_cache[name]
end
+ end
- unless part.is_a?(klass) || converter.nil? || part.nil?
- part = converter.respond_to?(:call) ? converter.call(part) : klass.send(converter, part)
- end
+ def writer_method(name, class_name, mapping, allow_nil, converter)
+ define_method("#{name}=") do |part|
+ klass = class_name.constantize
- if part.nil? && allow_nil
- mapping.each { |key, _| self[key] = nil }
- @aggregation_cache[name] = nil
- else
- mapping.each { |key, value| self[key] = part.send(value) }
- @aggregation_cache[name] = part.freeze
+ unless part.is_a?(klass) || converter.nil? || part.nil?
+ part = converter.respond_to?(:call) ? converter.call(part) : klass.send(converter, part)
+ end
+
+ hash_from_multiparameter_assignment = part.is_a?(Hash) &&
+ part.each_key.all? { |k| k.is_a?(Integer) }
+ if hash_from_multiparameter_assignment
+ raise ArgumentError unless part.size == part.each_key.max
+ part = klass.new(*part.sort.map(&:last))
+ end
+
+ if part.nil? && allow_nil
+ mapping.each { |key, _| self[key] = nil }
+ @aggregation_cache[name] = nil
+ else
+ mapping.each { |key, value| self[key] = part.send(value) }
+ @aggregation_cache[name] = part.freeze
+ end
end
end
- end
- end
+ end
end
end
diff --git a/activerecord/lib/active_record/association_relation.rb b/activerecord/lib/active_record/association_relation.rb
index ee0bb8fafe..4c538ef2bd 100644
--- a/activerecord/lib/active_record/association_relation.rb
+++ b/activerecord/lib/active_record/association_relation.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
module ActiveRecord
class AssociationRelation < Relation
- def initialize(klass, table, predicate_builder, association)
- super(klass, table, predicate_builder)
+ def initialize(klass, association)
+ super(klass)
@association = association
end
@@ -10,7 +12,7 @@ module ActiveRecord
end
def ==(other)
- other == to_a
+ other == records
end
def build(*args, &block)
@@ -28,8 +30,11 @@ module ActiveRecord
private
- def exec_queries
- super.each { |r| @association.set_inverse_instance r }
- end
+ def exec_queries
+ super do |record|
+ @association.set_inverse_instance_from_queries(record)
+ yield record if block_given?
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 82cb3fed59..fb1df00dc8 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -1,99 +1,171 @@
-require 'active_support/core_ext/enumerable'
-require 'active_support/core_ext/string/conversions'
-require 'active_support/core_ext/module/remove_method'
-require 'active_record/errors'
+# frozen_string_literal: true
+
+require "active_support/core_ext/enumerable"
+require "active_support/core_ext/string/conversions"
+require "active_support/core_ext/module/remove_method"
+require "active_record/errors"
module ActiveRecord
class AssociationNotFoundError < ConfigurationError #:nodoc:
- def initialize(record, association_name)
- super("Association named '#{association_name}' was not found on #{record.class.name}; perhaps you misspelled it?")
+ def initialize(record = nil, association_name = nil)
+ if record && association_name
+ super("Association named '#{association_name}' was not found on #{record.class.name}; perhaps you misspelled it?")
+ else
+ super("Association was not found.")
+ end
end
end
class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc:
- def initialize(reflection, associated_class = nil)
- super("Could not find the inverse association for #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{associated_class.nil? ? reflection.class_name : associated_class.name})")
+ def initialize(reflection = nil, associated_class = nil)
+ if reflection
+ super("Could not find the inverse association for #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{associated_class.nil? ? reflection.class_name : associated_class.name})")
+ else
+ super("Could not find the inverse association.")
+ end
end
end
class HasManyThroughAssociationNotFoundError < ActiveRecordError #:nodoc:
- def initialize(owner_class_name, reflection)
- super("Could not find the association #{reflection.options[:through].inspect} in model #{owner_class_name}")
+ def initialize(owner_class_name = nil, reflection = nil)
+ if owner_class_name && reflection
+ super("Could not find the association #{reflection.options[:through].inspect} in model #{owner_class_name}")
+ else
+ super("Could not find the association.")
+ end
end
end
class HasManyThroughAssociationPolymorphicSourceError < ActiveRecordError #:nodoc:
- def initialize(owner_class_name, reflection, source_reflection)
- super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' on the polymorphic object '#{source_reflection.class_name}##{source_reflection.name}' without 'source_type'. Try adding 'source_type: \"#{reflection.name.to_s.classify}\"' to 'has_many :through' definition.")
+ def initialize(owner_class_name = nil, reflection = nil, source_reflection = nil)
+ if owner_class_name && reflection && source_reflection
+ super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' on the polymorphic object '#{source_reflection.class_name}##{source_reflection.name}' without 'source_type'. Try adding 'source_type: \"#{reflection.name.to_s.classify}\"' to 'has_many :through' definition.")
+ else
+ super("Cannot have a has_many :through association.")
+ end
end
end
class HasManyThroughAssociationPolymorphicThroughError < ActiveRecordError #:nodoc:
- def initialize(owner_class_name, reflection)
- super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.")
+ def initialize(owner_class_name = nil, reflection = nil)
+ if owner_class_name && reflection
+ super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.")
+ else
+ super("Cannot have a has_many :through association.")
+ end
end
end
class HasManyThroughAssociationPointlessSourceTypeError < ActiveRecordError #:nodoc:
- def initialize(owner_class_name, reflection, source_reflection)
- super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' with a :source_type option if the '#{reflection.through_reflection.class_name}##{source_reflection.name}' is not polymorphic. Try removing :source_type on your association.")
+ def initialize(owner_class_name = nil, reflection = nil, source_reflection = nil)
+ if owner_class_name && reflection && source_reflection
+ super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' with a :source_type option if the '#{reflection.through_reflection.class_name}##{source_reflection.name}' is not polymorphic. Try removing :source_type on your association.")
+ else
+ super("Cannot have a has_many :through association.")
+ end
end
end
class HasOneThroughCantAssociateThroughCollection < ActiveRecordError #:nodoc:
- def initialize(owner_class_name, reflection, through_reflection)
- super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' where the :through association '#{owner_class_name}##{through_reflection.name}' is a collection. Specify a has_one or belongs_to association in the :through option instead.")
+ def initialize(owner_class_name = nil, reflection = nil, through_reflection = nil)
+ if owner_class_name && reflection && through_reflection
+ super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' where the :through association '#{owner_class_name}##{through_reflection.name}' is a collection. Specify a has_one or belongs_to association in the :through option instead.")
+ else
+ super("Cannot have a has_one :through association.")
+ end
end
end
class HasOneAssociationPolymorphicThroughError < ActiveRecordError #:nodoc:
- def initialize(owner_class_name, reflection)
- super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.")
+ def initialize(owner_class_name = nil, reflection = nil)
+ if owner_class_name && reflection
+ super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.")
+ else
+ super("Cannot have a has_one :through association.")
+ end
end
end
class HasManyThroughSourceAssociationNotFoundError < ActiveRecordError #:nodoc:
- def initialize(reflection)
- through_reflection = reflection.through_reflection
- source_reflection_names = reflection.source_reflection_names
- source_associations = reflection.through_reflection.klass._reflections.keys
- super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?")
+ def initialize(reflection = nil)
+ if reflection
+ through_reflection = reflection.through_reflection
+ source_reflection_names = reflection.source_reflection_names
+ source_associations = reflection.through_reflection.klass._reflections.keys
+ super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence(two_words_connector: ' or ', last_word_connector: ', or ', locale: :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(two_words_connector: ' or ', last_word_connector: ', or ', locale: :en)}?")
+ else
+ super("Could not find the source association(s).")
+ end
end
end
- class HasManyThroughCantAssociateThroughHasOneOrManyReflection < ActiveRecordError #:nodoc:
- def initialize(owner, reflection)
- super("Cannot modify association '#{owner.class.name}##{reflection.name}' because the source reflection class '#{reflection.source_reflection.class_name}' is associated to '#{reflection.through_reflection.class_name}' via :#{reflection.source_reflection.macro}.")
+ 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 HasManyThroughCantAssociateNewRecords < ActiveRecordError #:nodoc:
- def initialize(owner, reflection)
- super("Cannot associate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to create the has_many :through record associating them.")
+ class ThroughCantAssociateThroughHasOneOrManyReflection < ActiveRecordError #:nodoc:
+ def initialize(owner = nil, reflection = nil)
+ if owner && reflection
+ super("Cannot modify association '#{owner.class.name}##{reflection.name}' because the source reflection class '#{reflection.source_reflection.class_name}' is associated to '#{reflection.through_reflection.class_name}' via :#{reflection.source_reflection.macro}.")
+ else
+ super("Cannot modify association.")
+ end
end
end
- class HasManyThroughCantDissociateNewRecords < ActiveRecordError #:nodoc:
- def initialize(owner, reflection)
- super("Cannot dissociate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to delete the has_many :through record associating them.")
+ 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 HasManyThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc:
- def initialize(owner, reflection)
- super("Cannot modify association '#{owner.class.name}##{reflection.name}' because it goes through more than one other association.")
- end
+ class HasManyThroughCantAssociateThroughHasOneOrManyReflection < ThroughCantAssociateThroughHasOneOrManyReflection #:nodoc:
+ end
+
+ class HasOneThroughCantAssociateThroughHasOneOrManyReflection < ThroughCantAssociateThroughHasOneOrManyReflection #:nodoc:
end
- class EagerLoadPolymorphicError < ActiveRecordError #:nodoc:
- def initialize(reflection)
- super("Cannot eagerly load the polymorphic association #{reflection.name.inspect}")
+ class ThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc:
+ def initialize(owner = nil, reflection = nil)
+ if owner && reflection
+ super("Cannot modify association '#{owner.class.name}##{reflection.name}' because it goes through more than one other association.")
+ else
+ super("Through nested associations are read-only.")
+ end
end
end
- class ReadOnlyAssociation < ActiveRecordError #:nodoc:
- def initialize(reflection)
- super("Cannot add to a has_many :through association. Try adding to #{reflection.through_reflection.name.inspect}.")
+ class HasManyThroughNestedAssociationsAreReadonly < ThroughNestedAssociationsAreReadonly #:nodoc:
+ end
+
+ class HasOneThroughNestedAssociationsAreReadonly < ThroughNestedAssociationsAreReadonly #:nodoc:
+ end
+
+ # This error is raised when trying to eager load a polymorphic association using a JOIN.
+ # Eager loading polymorphic associations is only possible with
+ # {ActiveRecord::Relation#preload}[rdoc-ref:QueryMethods#preload].
+ class EagerLoadPolymorphicError < ActiveRecordError
+ def initialize(reflection = nil)
+ if reflection
+ super("Cannot eagerly load the polymorphic association #{reflection.name.inspect}")
+ else
+ super("Eager load polymorphic error.")
+ end
end
end
@@ -101,8 +173,12 @@ module ActiveRecord
# (has_many, has_one) when there is at least 1 child associated instance.
# ex: if @project.tasks.size > 0, DeleteRestrictionError will be raised when trying to destroy @project
class DeleteRestrictionError < ActiveRecordError #:nodoc:
- def initialize(name)
- super("Cannot delete record because of dependent #{name}")
+ def initialize(name = nil)
+ if name
+ super("Cannot delete record because of dependent #{name}")
+ else
+ super("Delete restriction error.")
+ end
end
end
@@ -118,33 +194,38 @@ module ActiveRecord
autoload :CollectionAssociation
autoload :ForeignAssociation
autoload :CollectionProxy
-
- autoload :BelongsToAssociation
- autoload :BelongsToPolymorphicAssociation
- autoload :HasManyAssociation
- autoload :HasManyThroughAssociation
- autoload :HasOneAssociation
- autoload :HasOneThroughAssociation
autoload :ThroughAssociation
module Builder #:nodoc:
- autoload :Association, 'active_record/associations/builder/association'
- autoload :SingularAssociation, 'active_record/associations/builder/singular_association'
- autoload :CollectionAssociation, 'active_record/associations/builder/collection_association'
+ autoload :Association, "active_record/associations/builder/association"
+ autoload :SingularAssociation, "active_record/associations/builder/singular_association"
+ autoload :CollectionAssociation, "active_record/associations/builder/collection_association"
- autoload :BelongsTo, 'active_record/associations/builder/belongs_to'
- autoload :HasOne, 'active_record/associations/builder/has_one'
- autoload :HasMany, 'active_record/associations/builder/has_many'
- autoload :HasAndBelongsToMany, 'active_record/associations/builder/has_and_belongs_to_many'
+ autoload :BelongsTo, "active_record/associations/builder/belongs_to"
+ autoload :HasOne, "active_record/associations/builder/has_one"
+ autoload :HasMany, "active_record/associations/builder/has_many"
+ autoload :HasAndBelongsToMany, "active_record/associations/builder/has_and_belongs_to_many"
end
eager_autoload do
+ autoload :BelongsToAssociation
+ autoload :BelongsToPolymorphicAssociation
+ autoload :HasManyAssociation
+ autoload :HasManyThroughAssociation
+ autoload :HasOneAssociation
+ autoload :HasOneThroughAssociation
+
autoload :Preloader
autoload :JoinDependency
autoload :AssociationScope
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)
@@ -160,7 +241,7 @@ module ActiveRecord
association
end
- def association_cached?(name) # :nodoc
+ def association_cached?(name) # :nodoc:
@association_cache.key?(name)
end
@@ -176,16 +257,16 @@ 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
- # Returns the specified association instance if it exists, nil otherwise.
+ # Returns the specified association instance if it exists, +nil+ otherwise.
def association_instance_get(name)
@association_cache[name]
end
@@ -195,1550 +276,1587 @@ module ActiveRecord
@association_cache[name] = association
end
- # \Associations are a set of macro-like class methods for tying objects together through
- # foreign keys. They express relationships like "Project has one Project Manager"
- # or "Project belongs to a Portfolio". Each macro adds a number of methods to the
- # class which are specialized according to the collection or association symbol and the
- # options hash. It works much the same way as Ruby's own <tt>attr*</tt>
- # methods.
- #
- # class Project < ActiveRecord::Base
- # belongs_to :portfolio
- # has_one :project_manager
- # has_many :milestones
- # has_and_belongs_to_many :categories
- # end
- #
- # The project class now has the following methods (and more) to ease the traversal and
- # manipulation of its relationships:
- # * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?</tt>
- # * <tt>Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?,</tt>
- # * <tt>Project#milestones.empty?, Project#milestones.size, Project#milestones, Project#milestones<<(milestone),</tt>
- # <tt>Project#milestones.delete(milestone), Project#milestones.destroy(milestone), Project#milestones.find(milestone_id),</tt>
- # <tt>Project#milestones.build, Project#milestones.create</tt>
- # * <tt>Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1),</tt>
- # <tt>Project#categories.delete(category1), Project#categories.destroy(category1)</tt>
- #
- # === A word of warning
- #
- # Don't create associations that have the same name as instance methods of
- # <tt>ActiveRecord::Base</tt>. Since the association adds a method with that name to
- # its model, it will override the inherited method and break things.
- # For instance, +attributes+ and +connection+ would be bad choices for association names.
- #
- # == Auto-generated methods
- # See also Instance Public methods below for more details.
- #
- # === Singular associations (one-to-one)
- # | | belongs_to |
- # generated methods | belongs_to | :polymorphic | has_one
- # ----------------------------------+------------+--------------+---------
- # other(force_reload=false) | X | X | X
- # other=(other) | X | X | X
- # build_other(attributes={}) | X | | X
- # create_other(attributes={}) | X | | X
- # create_other!(attributes={}) | X | | X
- #
- # ===Collection associations (one-to-many / many-to-many)
- # | | | has_many
- # generated methods | habtm | has_many | :through
- # ----------------------------------+-------+----------+----------
- # others(force_reload=false) | X | X | X
- # others=(other,other,...) | X | X | X
- # other_ids | X | X | X
- # other_ids=(id,id,...) | X | X | X
- # others<< | X | X | X
- # others.push | X | X | X
- # others.concat | X | X | X
- # others.build(attributes={}) | X | X | X
- # others.create(attributes={}) | X | X | X
- # others.create!(attributes={}) | X | X | X
- # others.size | X | X | X
- # others.length | X | X | X
- # others.count | X | X | X
- # others.sum(*args) | X | X | X
- # others.empty? | X | X | X
- # others.clear | X | X | X
- # others.delete(other,other,...) | X | X | X
- # others.delete_all | X | X | X
- # others.destroy(other,other,...) | X | X | X
- # others.destroy_all | X | X | X
- # others.find(*args) | X | X | X
- # others.exists? | X | X | X
- # others.distinct | X | X | X
- # others.reset | X | X | X
- #
- # === 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:
- #
- # 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>, 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.
- #
- # == Cardinality and associations
- #
- # Active Record associations can be used to describe one-to-one, one-to-many and many-to-many
- # relationships between models. Each model uses an association to describe its role in
- # the relation. The +belongs_to+ association is always used in the model that has
- # the foreign key.
- #
- # === One-to-one
- #
- # Use +has_one+ in the base, and +belongs_to+ in the associated model.
- #
- # class Employee < ActiveRecord::Base
- # has_one :office
- # end
- # class Office < ActiveRecord::Base
- # belongs_to :employee # foreign key - employee_id
- # end
- #
- # === One-to-many
- #
- # Use +has_many+ in the base, and +belongs_to+ in the associated model.
- #
- # class Manager < ActiveRecord::Base
- # has_many :employees
- # end
- # class Employee < ActiveRecord::Base
- # belongs_to :manager # foreign key - manager_id
- # end
- #
- # === Many-to-many
- #
- # There are two ways to build a many-to-many relationship.
- #
- # The first way uses a +has_many+ association with the <tt>:through</tt> option and a join model, so
- # there are two stages of associations.
- #
- # class Assignment < ActiveRecord::Base
- # belongs_to :programmer # foreign key - programmer_id
- # belongs_to :project # foreign key - project_id
- # end
- # class Programmer < ActiveRecord::Base
- # has_many :assignments
- # has_many :projects, through: :assignments
- # end
- # class Project < ActiveRecord::Base
- # has_many :assignments
- # has_many :programmers, through: :assignments
- # end
- #
- # For the second way, use +has_and_belongs_to_many+ in both models. This requires a join table
- # that has no corresponding model or primary key.
- #
- # class Programmer < ActiveRecord::Base
- # has_and_belongs_to_many :projects # foreign keys in the join table
- # end
- # class Project < ActiveRecord::Base
- # has_and_belongs_to_many :programmers # foreign keys in the join table
- # end
- #
- # Choosing which way to build a many-to-many relationship is not always simple.
- # If you need to work with the relationship model as its own entity,
- # use <tt>has_many :through</tt>. Use +has_and_belongs_to_many+ when working with legacy schemas or when
- # you never work directly with the relationship itself.
- #
- # == Is it a +belongs_to+ or +has_one+ association?
- #
- # Both express a 1-1 relationship. The difference is mostly where to place the foreign
- # key, which goes on the table for the class declaring the +belongs_to+ relationship.
- #
- # class User < ActiveRecord::Base
- # # I reference an account.
- # belongs_to :account
- # end
- #
- # class Account < ActiveRecord::Base
- # # One user references me.
- # has_one :user
- # end
- #
- # The tables for these classes could look something like:
- #
- # CREATE TABLE users (
- # id int(11) NOT NULL auto_increment,
- # account_id int(11) default NULL,
- # name varchar default NULL,
- # PRIMARY KEY (id)
- # )
- #
- # CREATE TABLE accounts (
- # id int(11) NOT NULL auto_increment,
- # name varchar default NULL,
- # PRIMARY KEY (id)
- # )
- #
- # == Unsaved objects and associations
- #
- # You can manipulate objects and associations before they are saved to the database, but
- # there is some special behavior you should be aware of, mostly involving the saving of
- # associated objects.
- #
- # You can set the <tt>:autosave</tt> option on a <tt>has_one</tt>, <tt>belongs_to</tt>,
- # <tt>has_many</tt>, or <tt>has_and_belongs_to_many</tt> association. Setting it
- # to +true+ will _always_ save the members, whereas setting it to +false+ will
- # _never_ save the members. More details about <tt>:autosave</tt> option is available at
- # AutosaveAssociation.
- #
- # === One-to-one associations
- #
- # * Assigning an object to a +has_one+ association automatically saves that object and
- # the object being replaced (if there is one), in order to update their foreign
- # keys - except if the parent object is unsaved (<tt>new_record? == true</tt>).
- # * If either of these saves fail (due to one of the objects being invalid), an
- # <tt>ActiveRecord::RecordNotSaved</tt> exception is raised and the assignment is
- # cancelled.
- # * If you wish to assign an object to a +has_one+ association without saving it,
- # use the <tt>build_association</tt> method (documented below). The object being
- # replaced will still be saved to update its foreign key.
- # * Assigning an object to a +belongs_to+ association does not save the object, since
- # the foreign key field belongs on the parent. It does not save the parent either.
- #
- # === Collections
- #
- # * Adding an object to a collection (+has_many+ or +has_and_belongs_to_many+) automatically
- # saves that object, except if the parent object (the owner of the collection) is not yet
- # stored in the database.
- # * If saving any of the objects being added to a collection (via <tt>push</tt> or similar)
- # fails, then <tt>push</tt> returns +false+.
- # * If saving fails while replacing the collection (via <tt>association=</tt>), an
- # <tt>ActiveRecord::RecordNotSaved</tt> exception is raised and the assignment is
- # cancelled.
- # * You can add an object to a collection without automatically saving it by using the
- # <tt>collection.build</tt> method (documented below).
- # * All unsaved (<tt>new_record? == true</tt>) members of the collection are automatically
- # saved when the parent is saved.
- #
- # == Customizing the query
- #
- # \Associations are built from <tt>Relation</tt>s, and you can use the <tt>Relation</tt> syntax
- # to customize them. For example, to add a condition:
- #
- # class Blog < ActiveRecord::Base
- # has_many :published_posts, -> { where published: true }, class_name: 'Post'
- # end
- #
- # Inside the <tt>-> { ... }</tt> block you can use all of the usual <tt>Relation</tt> methods.
- #
- # === Accessing the owner object
- #
- # Sometimes it is useful to have access to the owner object when building the query. The owner
- # is passed as a parameter to the block. For example, the following association would find all
- # events that occur on the user's birthday:
- #
- # class User < ActiveRecord::Base
- # has_many :birthday_events, ->(user) { where starts_on: user.birthday }, class_name: 'Event'
- # end
- #
- # Note: Joining, eager loading and preloading of these associations is not fully possible.
- # These operations happen before instance creation and the scope will be called with a +nil+ argument.
- # This can lead to unexpected behavior and is deprecated.
- #
- # == Association callbacks
- #
- # Similar to the normal callbacks that hook into the life cycle of an Active Record object,
- # you can also define callbacks that get triggered when you add an object to or remove an
- # object from an association collection.
- #
- # class Project
- # has_and_belongs_to_many :developers, after_add: :evaluate_velocity
- #
- # def evaluate_velocity(developer)
- # ...
- # end
- # end
- #
- # It's possible to stack callbacks by passing them as an array. Example:
- #
- # class Project
- # has_and_belongs_to_many :developers,
- # after_add: [:evaluate_velocity, Proc.new { |p, d| p.shipping_date = Time.now}]
- # end
- #
- # Possible callbacks are: +before_add+, +after_add+, +before_remove+ and +after_remove+.
- #
- # If any of the +before_add+ callbacks throw an exception, the object will not be
- # added to the collection.
- #
- # Similarly, if any of the +before_remove+ callbacks throw an exception, the object
- # will not be removed from the collection.
- #
- # == Association extensions
- #
- # The proxy objects that control the access to associations can be extended through anonymous
- # modules. This is especially beneficial for adding new finders, creators, and other
- # factory-type methods that are only used as part of this association.
- #
- # class Account < ActiveRecord::Base
- # has_many :people do
- # def find_or_create_by_name(name)
- # first_name, last_name = name.split(" ", 2)
- # find_or_create_by(first_name: first_name, last_name: last_name)
- # end
- # end
- # end
- #
- # person = Account.first.people.find_or_create_by_name("David Heinemeier Hansson")
- # person.first_name # => "David"
- # person.last_name # => "Heinemeier Hansson"
- #
- # If you need to share the same extensions between many associations, you can use a named
- # extension module.
- #
- # module FindOrCreateByNameExtension
- # def find_or_create_by_name(name)
- # first_name, last_name = name.split(" ", 2)
- # find_or_create_by(first_name: first_name, last_name: last_name)
- # end
- # end
- #
- # class Account < ActiveRecord::Base
- # has_many :people, -> { extending FindOrCreateByNameExtension }
- # end
- #
- # class Company < ActiveRecord::Base
- # has_many :people, -> { extending FindOrCreateByNameExtension }
- # end
- #
- # Some extensions can only be made to work with knowledge of the association's internals.
- # Extensions can access relevant state using the following methods (where +items+ is the
- # name of the association):
- #
- # * <tt>record.association(:items).owner</tt> - Returns the object the association is part of.
- # * <tt>record.association(:items).reflection</tt> - Returns the reflection object that describes the association.
- # * <tt>record.association(:items).target</tt> - Returns the associated object for +belongs_to+ and +has_one+, or
- # the collection of associated objects for +has_many+ and +has_and_belongs_to_many+.
- #
- # However, inside the actual extension code, you will not have access to the <tt>record</tt> as
- # above. In this case, you can access <tt>proxy_association</tt>. For example,
- # <tt>record.association(:items)</tt> and <tt>record.items.proxy_association</tt> will return
- # the same object, allowing you to make calls like <tt>proxy_association.owner</tt> inside
- # association extensions.
- #
- # == Association Join Models
- #
- # Has Many associations can be configured with the <tt>:through</tt> option to use an
- # explicit join model to retrieve the data. This operates similarly to a
- # +has_and_belongs_to_many+ association. The advantage is that you're able to add validations,
- # callbacks, and extra attributes on the join model. Consider the following schema:
- #
- # class Author < ActiveRecord::Base
- # has_many :authorships
- # has_many :books, through: :authorships
- # end
- #
- # class Authorship < ActiveRecord::Base
- # belongs_to :author
- # belongs_to :book
- # end
- #
- # @author = Author.first
- # @author.authorships.collect { |a| a.book } # selects all books that the author's authorships belong to
- # @author.books # selects all books by using the Authorship join model
- #
- # You can also go through a +has_many+ association on the join model:
- #
- # class Firm < ActiveRecord::Base
- # has_many :clients
- # has_many :invoices, through: :clients
- # end
- #
- # class Client < ActiveRecord::Base
- # belongs_to :firm
- # has_many :invoices
- # end
- #
- # class Invoice < ActiveRecord::Base
- # belongs_to :client
- # end
- #
- # @firm = Firm.first
- # @firm.clients.flat_map { |c| c.invoices } # select all invoices for all clients of the firm
- # @firm.invoices # selects all invoices by going through the Client join model
- #
- # Similarly you can go through a +has_one+ association on the join model:
- #
- # class Group < ActiveRecord::Base
- # has_many :users
- # has_many :avatars, through: :users
- # end
- #
- # class User < ActiveRecord::Base
- # belongs_to :group
- # has_one :avatar
- # end
- #
- # class Avatar < ActiveRecord::Base
- # belongs_to :user
- # end
- #
- # @group = Group.first
- # @group.users.collect { |u| u.avatar }.compact # select all avatars for all users in the group
- # @group.avatars # selects all avatars by going through the User join model.
- #
- # An important caveat with going through +has_one+ or +has_many+ associations on the
- # join model is that these associations are *read-only*. For example, the following
- # would not work following the previous example:
- #
- # @group.avatars << Avatar.new # this would work if User belonged_to Avatar rather than the other way around
- # @group.avatars.delete(@group.avatars.last) # so would this
- #
- # == Setting Inverses
- #
- # If you are using a +belongs_to+ on the join model, it is a good idea to set the
- # <tt>:inverse_of</tt> option on the +belongs_to+, which will mean that the following example
- # works correctly (where <tt>tags</tt> is a +has_many+ <tt>:through</tt> association):
- #
- # @post = Post.first
- # @tag = @post.tags.build name: "ruby"
- # @tag.save
- #
- # The last line ought to save the through record (a <tt>Taggable</tt>). This will only work if the
- # <tt>:inverse_of</tt> is set:
- #
- # class Taggable < ActiveRecord::Base
- # belongs_to :post
- # belongs_to :tag, inverse_of: :taggings
- # end
- #
- # If you do not set the <tt>:inverse_of</tt> record, the association will
- # do its best to match itself up with the correct inverse. Automatic
- # inverse detection only works on <tt>has_many</tt>, <tt>has_one</tt>, and
- # <tt>belongs_to</tt> associations.
- #
- # Extra options on the associations, as defined in the
- # <tt>AssociationReflection::INVALID_AUTOMATIC_INVERSE_OPTIONS</tt> constant, will
- # also prevent the association's inverse from being found automatically.
- #
- # The automatic guessing of the inverse association uses a heuristic based
- # on the name of the class, so it may not work for all associations,
- # especially the ones with non-standard names.
- #
- # You can turn off the automatic detection of inverse associations by setting
- # the <tt>:inverse_of</tt> option to <tt>false</tt> like so:
- #
- # class Taggable < ActiveRecord::Base
- # belongs_to :tag, inverse_of: false
- # end
- #
- # == Nested \Associations
- #
- # You can actually specify *any* association with the <tt>:through</tt> option, including an
- # association which has a <tt>:through</tt> option itself. For example:
- #
- # class Author < ActiveRecord::Base
- # has_many :posts
- # has_many :comments, through: :posts
- # has_many :commenters, through: :comments
- # end
- #
- # class Post < ActiveRecord::Base
- # has_many :comments
- # end
- #
- # class Comment < ActiveRecord::Base
- # belongs_to :commenter
- # end
- #
- # @author = Author.first
- # @author.commenters # => People who commented on posts written by the author
- #
- # An equivalent way of setting up this association this would be:
- #
- # class Author < ActiveRecord::Base
- # has_many :posts
- # has_many :commenters, through: :posts
- # end
- #
- # class Post < ActiveRecord::Base
- # has_many :comments
- # has_many :commenters, through: :comments
- # end
- #
- # class Comment < ActiveRecord::Base
- # belongs_to :commenter
- # end
- #
- # When using a nested association, you will not be able to modify the association because there
- # is not enough information to know what modification to make. For example, if you tried to
- # add a <tt>Commenter</tt> in the example above, there would be no way to tell how to set up the
- # intermediate <tt>Post</tt> and <tt>Comment</tt> objects.
- #
- # == Polymorphic \Associations
- #
- # Polymorphic associations on models are not restricted on what types of models they
- # can be associated with. Rather, they specify an interface that a +has_many+ association
- # must adhere to.
- #
- # class Asset < ActiveRecord::Base
- # belongs_to :attachable, polymorphic: true
- # end
- #
- # class Post < ActiveRecord::Base
- # has_many :assets, as: :attachable # The :as option specifies the polymorphic interface to use.
- # end
- #
- # @asset.attachable = @post
- #
- # This works by using a type column in addition to a foreign key to specify the associated
- # record. In the Asset example, you'd need an +attachable_id+ integer column and an
- # +attachable_type+ string column.
- #
- # Using polymorphic associations in combination with single table inheritance (STI) is
- # a little tricky. In order for the associations to work as expected, ensure that you
- # store the base model for the STI models in the type column of the polymorphic
- # association. To continue with the asset example above, suppose there are guest posts
- # and member posts that use the posts table for STI. In this case, there must be a +type+
- # column in the posts table.
- #
- # Note: The <tt>attachable_type=</tt> method is being called when assigning an +attachable+.
- # The +class_name+ of the +attachable+ is passed as a String.
- #
- # class Asset < ActiveRecord::Base
- # belongs_to :attachable, polymorphic: true
- #
- # def attachable_type=(class_name)
- # super(class_name.constantize.base_class.to_s)
- # end
- # end
- #
- # class Post < ActiveRecord::Base
- # # because we store "Post" in attachable_type now dependent: :destroy will work
- # has_many :assets, as: :attachable, dependent: :destroy
- # end
- #
- # class GuestPost < Post
- # end
- #
- # class MemberPost < Post
- # end
- #
- # == Caching
- #
- # All of the methods are built on a simple caching principle that will keep the result
- # of the last query around unless specifically instructed not to. The cache is even
- # shared across methods to make it even cheaper to use the macro-added methods without
- # worrying too much about performance at the first go.
- #
- # project.milestones # fetches milestones from the database
- # project.milestones.size # uses the milestone cache
- # project.milestones.empty? # uses the milestone cache
- # project.milestones(true).size # fetches milestones from the database
- # project.milestones # uses the milestone cache
- #
- # == Eager loading of associations
- #
- # Eager loading is a way to find objects of a certain class and a number of named associations.
- # It is one of the easiest ways to prevent the dreaded N+1 problem in which fetching 100
- # posts that each need to display their author triggers 101 database queries. Through the
- # use of eager loading, the number of queries will be reduced from 101 to 2.
- #
- # class Post < ActiveRecord::Base
- # belongs_to :author
- # has_many :comments
- # end
- #
- # Consider the following loop using the class above:
- #
- # Post.all.each do |post|
- # puts "Post: " + post.title
- # puts "Written by: " + post.author.name
- # puts "Last comment on: " + post.comments.first.created_on
- # end
- #
- # To iterate over these one hundred posts, we'll generate 201 database queries. Let's
- # first just optimize it for retrieving the author:
- #
- # Post.includes(:author).each do |post|
- #
- # This references the name of the +belongs_to+ association that also used the <tt>:author</tt>
- # symbol. After loading the posts, find will collect the +author_id+ from each one and load
- # all the referenced authors with one query. Doing so will cut down the number of queries
- # from 201 to 102.
- #
- # We can improve upon the situation further by referencing both associations in the finder with:
- #
- # Post.includes(:author, :comments).each do |post|
- #
- # This will load all comments with a single query. This reduces the total number of queries
- # to 3. In general, the number of queries will be 1 plus the number of associations
- # named (except if some of the associations are polymorphic +belongs_to+ - see below).
- #
- # To include a deep hierarchy of associations, use a hash:
- #
- # Post.includes(:author, { comments: { author: :gravatar } }).each do |post|
- #
- # The above code will load all the comments and all of their associated
- # authors and gravatars. You can mix and match any combination of symbols,
- # arrays, and hashes to retrieve the associations you want to load.
- #
- # All of this power shouldn't fool you into thinking that you can pull out huge amounts
- # of data with no performance penalty just because you've reduced the number of queries.
- # The database still needs to send all the data to Active Record and it still needs to
- # be processed. So it's no catch-all for performance problems, but it's a great way to
- # cut down on the number of queries in a situation as the one described above.
- #
- # Since only one table is loaded at a time, conditions or orders cannot reference tables
- # other than the main one. If this is the case, Active Record falls back to the previously
- # used LEFT OUTER JOIN based strategy. For example:
- #
- # Post.includes([:author, :comments]).where(['comments.approved = ?', true])
- #
- # This will result in a single SQL query with joins along the lines of:
- # <tt>LEFT OUTER JOIN comments ON comments.post_id = posts.id</tt> and
- # <tt>LEFT OUTER JOIN authors ON authors.id = posts.author_id</tt>. Note that using conditions
- # like this can have unintended consequences.
- # In the above example posts with no approved comments are not returned at all, because
- # the conditions apply to the SQL statement as a whole and not just to the association.
- #
- # You must disambiguate column references for this fallback to happen, for example
- # <tt>order: "author.name DESC"</tt> will work but <tt>order: "name DESC"</tt> will not.
- #
- # If you want to load all posts (including posts with no approved comments) then write
- # your own LEFT OUTER JOIN query using ON
- #
- # Post.joins("LEFT OUTER JOIN comments ON comments.post_id = posts.id AND comments.approved = '1'")
- #
- # In this case it is usually more natural to include an association which has conditions defined on it:
- #
- # class Post < ActiveRecord::Base
- # has_many :approved_comments, -> { where approved: true }, class_name: 'Comment'
- # end
- #
- # Post.includes(:approved_comments)
- #
- # This will load posts and eager load the +approved_comments+ association, which contains
- # only those comments that have been approved.
- #
- # If you eager load an association with a specified <tt>:limit</tt> option, it will be ignored,
- # returning all the associated objects:
- #
- # class Picture < ActiveRecord::Base
- # has_many :most_recent_comments, -> { order('id DESC').limit(10) }, class_name: 'Comment'
- # end
- #
- # Picture.includes(:most_recent_comments).first.most_recent_comments # => returns all associated comments.
- #
- # Eager loading is supported with polymorphic associations.
- #
- # class Address < ActiveRecord::Base
- # belongs_to :addressable, polymorphic: true
- # end
- #
- # A call that tries to eager load the addressable model
- #
- # Address.includes(:addressable)
- #
- # This will execute one query to load the addresses and load the addressables with one
- # query per addressable type.
- # For example if all the addressables are either of class Person or Company then a total
- # of 3 queries will be executed. The list of addressable types to load is determined on
- # the back of the addresses loaded. This is not supported if Active Record has to fallback
- # to the previous implementation of eager loading and will raise <tt>ActiveRecord::EagerLoadPolymorphicError</tt>.
- # The reason is that the parent model's type is a column value so its corresponding table
- # name cannot be put in the +FROM+/+JOIN+ clauses of that query.
- #
- # == Table Aliasing
- #
- # Active Record uses table aliasing in the case that a table is referenced multiple times
- # in a join. If a table is referenced only once, the standard table name is used. The
- # second time, the table is aliased as <tt>#{reflection_name}_#{parent_table_name}</tt>.
- # Indexes are appended for any more successive uses of the table name.
- #
- # Post.joins(:comments)
- # # => SELECT ... FROM posts INNER JOIN comments ON ...
- # Post.joins(:special_comments) # STI
- # # => SELECT ... FROM posts INNER JOIN comments ON ... AND comments.type = 'SpecialComment'
- # Post.joins(:comments, :special_comments) # special_comments is the reflection name, posts is the parent table name
- # # => SELECT ... FROM posts INNER JOIN comments ON ... INNER JOIN comments special_comments_posts
- #
- # Acts as tree example:
- #
- # TreeMixin.joins(:children)
- # # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
- # TreeMixin.joins(children: :parent)
- # # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
- # INNER JOIN parents_mixins ...
- # TreeMixin.joins(children: {parent: :children})
- # # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
- # INNER JOIN parents_mixins ...
- # INNER JOIN mixins childrens_mixins_2
- #
- # Has and Belongs to Many join tables use the same idea, but add a <tt>_join</tt> suffix:
- #
- # Post.joins(:categories)
- # # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
- # Post.joins(categories: :posts)
- # # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
- # INNER JOIN categories_posts posts_categories_join INNER JOIN posts posts_categories
- # Post.joins(categories: {posts: :categories})
- # # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
- # INNER JOIN categories_posts posts_categories_join INNER JOIN posts posts_categories
- # INNER JOIN categories_posts categories_posts_join INNER JOIN categories categories_posts_2
- #
- # If you wish to specify your own custom joins using <tt>joins</tt> method, those table
- # names will take precedence over the eager associations:
- #
- # Post.joins(:comments).joins("inner join comments ...")
- # # => SELECT ... FROM posts INNER JOIN comments_posts ON ... INNER JOIN comments ...
- # Post.joins(:comments, :special_comments).joins("inner join comments ...")
- # # => SELECT ... FROM posts INNER JOIN comments comments_posts ON ...
- # INNER JOIN comments special_comments_posts ...
- # INNER JOIN comments ...
- #
- # Table aliases are automatically truncated according to the maximum length of table identifiers
- # according to the specific database.
- #
- # == Modules
- #
- # By default, associations will look for objects within the current module scope. Consider:
- #
- # module MyApplication
- # module Business
- # class Firm < ActiveRecord::Base
- # has_many :clients
- # end
- #
- # class Client < ActiveRecord::Base; end
- # end
- # end
- #
- # When <tt>Firm#clients</tt> is called, it will in turn call
- # <tt>MyApplication::Business::Client.find_all_by_firm_id(firm.id)</tt>.
- # If you want to associate with a class in another module scope, this can be done by
- # specifying the complete class name.
- #
- # module MyApplication
- # module Business
- # class Firm < ActiveRecord::Base; end
- # end
- #
- # module Billing
- # class Account < ActiveRecord::Base
- # belongs_to :firm, class_name: "MyApplication::Business::Firm"
- # end
- # end
- # end
- #
- # == Bi-directional associations
- #
- # When you specify an association there is usually an association on the associated model
- # that specifies the same relationship in reverse. For example, with the following models:
- #
- # class Dungeon < ActiveRecord::Base
- # has_many :traps
- # has_one :evil_wizard
- # end
- #
- # class Trap < ActiveRecord::Base
- # belongs_to :dungeon
- # end
- #
- # class EvilWizard < ActiveRecord::Base
- # belongs_to :dungeon
- # end
- #
- # The +traps+ association on +Dungeon+ and the +dungeon+ association on +Trap+ are
- # the inverse of each other and the inverse of the +dungeon+ association on +EvilWizard+
- # is the +evil_wizard+ association on +Dungeon+ (and vice-versa). By default,
- # Active Record can guess the inverse of the association based on the name
- # of the class. The result is the following:
- #
- # d = Dungeon.first
- # t = d.traps.first
- # d.object_id == t.dungeon.object_id # => true
- #
- # The +Dungeon+ instances +d+ and <tt>t.dungeon</tt> in the above example refer to
- # the same in-memory instance since the association matches the name of the class.
- # The result would be the same if we added +:inverse_of+ to our model definitions:
- #
- # class Dungeon < ActiveRecord::Base
- # has_many :traps, inverse_of: :dungeon
- # has_one :evil_wizard, inverse_of: :dungeon
- # end
- #
- # class Trap < ActiveRecord::Base
- # belongs_to :dungeon, inverse_of: :traps
- # end
- #
- # class EvilWizard < ActiveRecord::Base
- # belongs_to :dungeon, inverse_of: :evil_wizard
- # end
- #
- # There are limitations to <tt>:inverse_of</tt> support:
- #
- # * does not work with <tt>:through</tt> associations.
- # * does not work with <tt>:polymorphic</tt> associations.
- # * for +belongs_to+ associations +has_many+ inverse associations are ignored.
- #
- # For more information, see the documentation for the +:inverse_of+ option.
- #
- # == Deleting from associations
- #
- # === Dependent associations
- #
- # +has_many+, +has_one+ and +belongs_to+ associations support the <tt>:dependent</tt> option.
- # This allows you to specify that associated records should be deleted when the owner is
- # deleted.
- #
- # For example:
- #
- # class Author
- # has_many :posts, dependent: :destroy
- # end
- # Author.find(1).destroy # => Will destroy all of the author's posts, too
- #
- # The <tt>:dependent</tt> option can have different values which specify how the deletion
- # is done. For more information, see the documentation for this option on the different
- # specific association types. When no option is given, the behavior is to do nothing
- # with the associated records when destroying a record.
- #
- # Note that <tt>:dependent</tt> is implemented using Rails' callback
- # system, which works by processing callbacks in order. Therefore, other
- # callbacks declared either before or after the <tt>:dependent</tt> option
- # can affect what it does.
- #
- # Note that <tt>:dependent</tt> option is ignored for +has_one+ <tt>:through</tt> associations.
- #
- # === Delete or destroy?
- #
- # +has_many+ and +has_and_belongs_to_many+ associations have the methods <tt>destroy</tt>,
- # <tt>delete</tt>, <tt>destroy_all</tt> and <tt>delete_all</tt>.
- #
- # For +has_and_belongs_to_many+, <tt>delete</tt> and <tt>destroy</tt> are the same: they
- # cause the records in the join table to be removed.
- #
- # For +has_many+, <tt>destroy</tt> and <tt>destroy_all</tt> will always call the <tt>destroy</tt> method of the
- # record(s) being removed so that callbacks are run. However <tt>delete</tt> and <tt>delete_all</tt> will either
- # do the deletion according to the strategy specified by the <tt>:dependent</tt> option, or
- # if no <tt>:dependent</tt> option is given, then it will follow the default strategy.
- # The default strategy is to do nothing (leave the foreign keys with the parent ids set), except for
- # +has_many+ <tt>:through</tt>, where the default strategy is <tt>delete_all</tt> (delete
- # the join records, without running their callbacks).
- #
- # There is also a <tt>clear</tt> method which is the same as <tt>delete_all</tt>, except that
- # it returns the association rather than the records which have been deleted.
- #
- # === What gets deleted?
- #
- # There is a potential pitfall here: +has_and_belongs_to_many+ and +has_many+ <tt>:through</tt>
- # associations have records in join tables, as well as the associated records. So when we
- # call one of these deletion methods, what exactly should be deleted?
- #
- # The answer is that it is assumed that deletion on an association is about removing the
- # <i>link</i> between the owner and the associated object(s), rather than necessarily the
- # associated objects themselves. So with +has_and_belongs_to_many+ and +has_many+
- # <tt>:through</tt>, the join records will be deleted, but the associated records won't.
- #
- # This makes sense if you think about it: if you were to call <tt>post.tags.delete(Tag.find_by(name: 'food'))</tt>
- # you would want the 'food' tag to be unlinked from the post, rather than for the tag itself
- # to be removed from the database.
- #
- # However, there are examples where this strategy doesn't make sense. For example, suppose
- # a person has many projects, and each project has many tasks. If we deleted one of a person's
- # tasks, we would probably not want the project to be deleted. In this scenario, the delete method
- # won't actually work: it can only be used if the association on the join model is a
- # +belongs_to+. In other situations you are expected to perform operations directly on
- # either the associated records or the <tt>:through</tt> association.
- #
- # With a regular +has_many+ there is no distinction between the "associated records"
- # and the "link", so there is only one choice for what gets deleted.
- #
- # With +has_and_belongs_to_many+ and +has_many+ <tt>:through</tt>, if you want to delete the
- # associated records themselves, you can always do something along the lines of
- # <tt>person.tasks.each(&:destroy)</tt>.
- #
- # == Type safety with <tt>ActiveRecord::AssociationTypeMismatch</tt>
- #
- # If you attempt to assign an object to an association that doesn't match the inferred
- # or specified <tt>:class_name</tt>, you'll get an <tt>ActiveRecord::AssociationTypeMismatch</tt>.
- #
- # == Options
- #
- # All of the association macros can be specialized through options. This makes cases
- # more complex than the simple and guessable ones possible.
- module ClassMethods
- # Specifies a one-to-many association. The following methods for retrieval and query of
- # collections of associated objects will be added:
- #
- # +collection+ is a placeholder for the symbol passed as the +name+ argument, so
- # <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>.
- #
- # [collection(force_reload = false)]
- # Returns an array of all the associated objects.
- # An empty array is returned if none are found.
- # [collection<<(object, ...)]
- # Adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
- # Note that this operation instantly fires update SQL without waiting for the save or update call on the
- # parent object, unless the parent object is a new record.
- # [collection.delete(object, ...)]
- # Removes one or more objects from the collection by setting their foreign keys to +NULL+.
- # Objects will be in addition destroyed if they're associated with <tt>dependent: :destroy</tt>,
- # and deleted if they're associated with <tt>dependent: :delete_all</tt>.
- #
- # If the <tt>:through</tt> option is used, then the join records are deleted (rather than
- # nullified) by default, but you can specify <tt>dependent: :destroy</tt> or
- # <tt>dependent: :nullify</tt> to override this.
- # [collection.destroy(object, ...)]
- # Removes one or more objects from the collection by running <tt>destroy</tt> on
- # each record, regardless of any dependent option, ensuring callbacks are run.
- #
- # If the <tt>:through</tt> option is used, then the join records are destroyed
- # instead, not the objects themselves.
- # [collection=objects]
- # Replaces the collections content by deleting and adding objects as appropriate. If the <tt>:through</tt>
- # option is true callbacks in the join models are triggered except destroy callbacks, since deletion is
- # direct.
- # [collection_singular_ids]
- # Returns an array of the associated objects' ids
- # [collection_singular_ids=ids]
- # Replace the collection with the objects identified by the primary keys in +ids+. This
- # method loads the models and calls <tt>collection=</tt>. See above.
- # [collection.clear]
- # Removes every object from the collection. This destroys the associated objects if they
- # are associated with <tt>dependent: :destroy</tt>, deletes them directly from the
- # database if <tt>dependent: :delete_all</tt>, otherwise sets their foreign keys to +NULL+.
- # If the <tt>:through</tt> option is true no destroy callbacks are invoked on the join models.
- # Join models are directly deleted.
- # [collection.empty?]
- # Returns +true+ if there are no associated objects.
- # [collection.size]
- # Returns the number of associated objects.
- # [collection.find(...)]
- # Finds an associated object according to the same rules as <tt>ActiveRecord::Base.find</tt>.
- # [collection.exists?(...)]
- # Checks whether an associated object with the given conditions exists.
- # Uses the same rules as <tt>ActiveRecord::Base.exists?</tt>.
- # [collection.build(attributes = {}, ...)]
- # Returns one or more new objects of the collection type that have been instantiated
- # with +attributes+ and linked to this object through a foreign key, but have not yet
- # been saved.
- # [collection.create(attributes = {})]
- # Returns a new object of the collection type that has been instantiated
- # with +attributes+, linked to this object through a foreign key, and that has already
- # been saved (if it passed the validation). *Note*: This only works if the base model
- # already exists in the DB, not if it is a new (unsaved) record!
- # [collection.create!(attributes = {})]
- # Does the same as <tt>collection.create</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt>
- # if the record is invalid.
- #
- # === Example
- #
- # A <tt>Firm</tt> class declares <tt>has_many :clients</tt>, which will add:
- # * <tt>Firm#clients</tt> (similar to <tt>Client.where(firm_id: id)</tt>)
- # * <tt>Firm#clients<<</tt>
- # * <tt>Firm#clients.delete</tt>
- # * <tt>Firm#clients.destroy</tt>
- # * <tt>Firm#clients=</tt>
- # * <tt>Firm#client_ids</tt>
- # * <tt>Firm#client_ids=</tt>
- # * <tt>Firm#clients.clear</tt>
- # * <tt>Firm#clients.empty?</tt> (similar to <tt>firm.clients.size == 0</tt>)
- # * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "firm_id = #{id}"</tt>)
- # * <tt>Firm#clients.find</tt> (similar to <tt>Client.where(firm_id: id).find(id)</tt>)
- # * <tt>Firm#clients.exists?(name: 'ACME')</tt> (similar to <tt>Client.exists?(name: 'ACME', firm_id: firm.id)</tt>)
- # * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>)
- # * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>)
- # * <tt>Firm#clients.create!</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save!</tt>)
- # The declaration can also include an +options+ hash to specialize the behavior of the association.
- #
- # === Scopes
- #
- # You can pass a second argument +scope+ as a callable (i.e. proc or
- # lambda) to retrieve a specific set of records or customize the generated
- # query when you access the associated collection.
- #
- # Scope examples:
- # has_many :comments, -> { where(author_id: 1) }
- # has_many :employees, -> { joins(:address) }
- # has_many :posts, ->(post) { where("max_post_length > ?", post.length) }
- #
- # === Extensions
- #
- # The +extension+ argument allows you to pass a block into a has_many
- # association. This is useful for adding new finders, creators and other
- # factory-type methods to be used as part of the association.
- #
- # Extension examples:
- # has_many :employees do
- # def find_or_create_by_name(name)
- # first_name, last_name = name.split(" ", 2)
- # find_or_create_by(first_name: first_name, last_name: last_name)
+ # \Associations are a set of macro-like class methods for tying objects together through
+ # foreign keys. They express relationships like "Project has one Project Manager"
+ # or "Project belongs to a Portfolio". Each macro adds a number of methods to the
+ # class which are specialized according to the collection or association symbol and the
+ # options hash. It works much the same way as Ruby's own <tt>attr*</tt>
+ # methods.
+ #
+ # class Project < ActiveRecord::Base
+ # belongs_to :portfolio
+ # has_one :project_manager
+ # has_many :milestones
+ # has_and_belongs_to_many :categories
+ # end
+ #
+ # The project class now has the following methods (and more) to ease the traversal and
+ # manipulation of its relationships:
+ # * <tt>Project#portfolio</tt>, <tt>Project#portfolio=(portfolio)</tt>, <tt>Project#reload_portfolio</tt>
+ # * <tt>Project#project_manager</tt>, <tt>Project#project_manager=(project_manager)</tt>, <tt>Project#reload_project_manager</tt>
+ # * <tt>Project#milestones.empty?</tt>, <tt>Project#milestones.size</tt>, <tt>Project#milestones</tt>, <tt>Project#milestones<<(milestone)</tt>,
+ # <tt>Project#milestones.delete(milestone)</tt>, <tt>Project#milestones.destroy(milestone)</tt>, <tt>Project#milestones.find(milestone_id)</tt>,
+ # <tt>Project#milestones.build</tt>, <tt>Project#milestones.create</tt>
+ # * <tt>Project#categories.empty?</tt>, <tt>Project#categories.size</tt>, <tt>Project#categories</tt>, <tt>Project#categories<<(category1)</tt>,
+ # <tt>Project#categories.delete(category1)</tt>, <tt>Project#categories.destroy(category1)</tt>
+ #
+ # === A word of warning
+ #
+ # Don't create associations that have the same name as {instance methods}[rdoc-ref:ActiveRecord::Core] of
+ # <tt>ActiveRecord::Base</tt>. Since the association adds a method with that name to
+ # its model, using an association with the same name as one provided by <tt>ActiveRecord::Base</tt> will override the method inherited through <tt>ActiveRecord::Base</tt> and will break things.
+ # For instance, +attributes+ and +connection+ would be bad choices for association names, because those names already exist in the list of <tt>ActiveRecord::Base</tt> instance methods.
+ #
+ # == Auto-generated methods
+ # See also Instance Public methods below for more details.
+ #
+ # === Singular associations (one-to-one)
+ # | | belongs_to |
+ # generated methods | belongs_to | :polymorphic | has_one
+ # ----------------------------------+------------+--------------+---------
+ # other | X | X | X
+ # other=(other) | X | X | X
+ # build_other(attributes={}) | X | | X
+ # create_other(attributes={}) | X | | X
+ # create_other!(attributes={}) | X | | X
+ # reload_other | X | X | X
+ #
+ # === Collection associations (one-to-many / many-to-many)
+ # | | | has_many
+ # generated methods | habtm | has_many | :through
+ # ----------------------------------+-------+----------+----------
+ # others | X | X | X
+ # others=(other,other,...) | X | X | X
+ # other_ids | X | X | X
+ # other_ids=(id,id,...) | X | X | X
+ # others<< | X | X | X
+ # others.push | X | X | X
+ # others.concat | X | X | X
+ # others.build(attributes={}) | X | X | X
+ # others.create(attributes={}) | X | X | X
+ # others.create!(attributes={}) | X | X | X
+ # others.size | X | X | X
+ # others.length | X | X | X
+ # others.count | X | X | X
+ # others.sum(*args) | X | X | X
+ # others.empty? | X | X | X
+ # others.clear | X | X | X
+ # others.delete(other,other,...) | X | X | X
+ # others.delete_all | X | X | X
+ # others.destroy(other,other,...) | X | X | X
+ # others.destroy_all | X | X | X
+ # others.find(*args) | X | X | X
+ # others.exists? | X | X | X
+ # others.distinct | X | X | X
+ # others.reset | X | X | X
+ # others.reload | X | X | X
+ #
+ # === Overriding generated methods
+ #
+ # 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
#
- # === Options
- # [:class_name]
- # Specify the class name of the association. Use it only if that name can't be inferred
- # from the association name. So <tt>has_many :products</tt> will by default be linked
- # to the Product class, but if the real class name is SpecialProduct, you'll have to
- # specify it with this option.
- # [:foreign_key]
- # Specify the foreign key used for the association. By default this is guessed to be the name
- # of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_many+
- # association will use "person_id" as the default <tt>:foreign_key</tt>.
- # [:foreign_type]
- # Specify the column used to store the associated object's type, if this is a polymorphic
- # association. By default this is guessed to be the name of the polymorphic association
- # specified on "as" option with a "_type" suffix. So a class that defines a
- # <tt>has_many :tags, as: :taggable</tt> association will use "taggable_type" as the
- # default <tt>:foreign_type</tt>.
- # [:primary_key]
- # Specify the name of the column to use as the primary key for the association. By default this is +id+.
- # [:dependent]
- # Controls what happens to the associated objects when
- # their owner is destroyed. Note that these are implemented as
- # callbacks, and Rails executes callbacks in order. Therefore, other
- # similar callbacks may affect the <tt>:dependent</tt> behavior, and the
- # <tt>:dependent</tt> behavior may affect other callbacks.
- #
- # * <tt>:destroy</tt> causes all the associated objects to also be destroyed.
- # * <tt>:delete_all</tt> causes all the associated objects to be deleted directly from the database (so callbacks will not be executed).
- # * <tt>:nullify</tt> causes the foreign keys to be set to +NULL+. Callbacks are not executed.
- # * <tt>:restrict_with_exception</tt> causes an exception to be raised if there are any associated records.
- # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there are any associated objects.
- #
- # If using with the <tt>:through</tt> option, the association on the join model must be
- # a +belongs_to+, and the records which get deleted are the join records, rather than
- # the associated records.
- # [:counter_cache]
- # This option can be used to configure a custom named <tt>:counter_cache.</tt> You only need this option,
- # when you customized the name of your <tt>:counter_cache</tt> on the <tt>belongs_to</tt> association.
- # [:as]
- # Specifies a polymorphic interface (See <tt>belongs_to</tt>).
- # [:through]
- # Specifies an association through which to perform the query. This can be any other type
- # of association, including other <tt>:through</tt> associations. Options for <tt>:class_name</tt>,
- # <tt>:primary_key</tt> and <tt>:foreign_key</tt> are ignored, as the association uses the
- # source reflection.
- #
- # If the association on the join model is a +belongs_to+, the collection can be modified
- # and the records on the <tt>:through</tt> model will be automatically created and removed
- # as appropriate. Otherwise, the collection is read-only, so you should manipulate the
- # <tt>:through</tt> association directly.
- #
- # If you are going to modify the association (rather than just read from it), then it is
- # a good idea to set the <tt>:inverse_of</tt> option on the source association on the
- # join model. This allows associated records to be built which will automatically create
- # the appropriate join model records when they are saved. (See the 'Association Join Models'
- # section above.)
- # [:source]
- # Specifies the source association name used by <tt>has_many :through</tt> queries.
- # Only use it if the name cannot be inferred from the association.
- # <tt>has_many :subscribers, through: :subscriptions</tt> will look for either <tt>:subscribers</tt> or
- # <tt>:subscriber</tt> on Subscription, unless a <tt>:source</tt> is given.
- # [:source_type]
- # Specifies type of the source association used by <tt>has_many :through</tt> queries where the source
- # association is a polymorphic +belongs_to+.
- # [:validate]
- # If +false+, don't validate the associated objects when saving the parent object. true by default.
- # [:autosave]
- # If true, always save the associated objects or destroy them if marked for destruction,
- # when saving the parent object. If false, never save or destroy the associated objects.
- # By default, only save associated objects that are new records. This option is implemented as a
- # +before_save+ callback. Because callbacks are run in the order they are defined, associated objects
- # may need to be explicitly saved in any user-defined +before_save+ callbacks.
- #
- # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>.
- # [:inverse_of]
- # Specifies the name of the <tt>belongs_to</tt> association on the associated object
- # that is the inverse of this <tt>has_many</tt> association. Does not work in combination
- # with <tt>:through</tt> or <tt>:as</tt> options.
- # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
- # [:extend]
- # Specifies a module or array of modules that will be extended into the association object returned.
- # Useful for defining methods on associations, especially when they should be shared between multiple
- # association objects.
- #
- # Option examples:
- # has_many :comments, -> { order "posted_on" }
- # has_many :comments, -> { includes :author }
- # has_many :people, -> { where(deleted: false).order("name") }, class_name: "Person"
- # has_many :tracks, -> { order "position" }, dependent: :destroy
- # has_many :comments, dependent: :nullify
- # has_many :tags, as: :taggable
- # has_many :reports, -> { readonly }
- # has_many :subscribers, through: :subscriptions, source: :user
- def has_many(name, scope = nil, options = {}, &extension)
- reflection = Builder::HasMany.build(self, name, scope, options, &extension)
- Reflection.add_reflection self, name, reflection
- end
-
- # Specifies a one-to-one association with another class. This method should only be used
- # if the other class contains the foreign key. If the current class contains the foreign key,
- # then you should use +belongs_to+ instead. See also ActiveRecord::Associations::ClassMethods's overview
- # on when to use +has_one+ and when to use +belongs_to+.
- #
- # The following methods for retrieval and query of a single associated object will be added:
- #
- # +association+ is a placeholder for the symbol passed as the +name+ argument, so
- # <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>.
- #
- # [association(force_reload = false)]
- # Returns the associated object. +nil+ is returned if none is found.
- # [association=(associate)]
- # Assigns the associate object, extracts the primary key, sets it as the foreign key,
- # and saves the associate object. To avoid database inconsistencies, permanently deletes an existing
- # associated object when assigning a new one, even if the new one isn't saved to database.
- # [build_association(attributes = {})]
- # Returns a new object of the associated type that has been instantiated
- # with +attributes+ and linked to this object through a foreign key, but has not
- # yet been saved.
- # [create_association(attributes = {})]
- # Returns a new object of the associated type that has been instantiated
- # with +attributes+, linked to this object through a foreign key, and that
- # has already been saved (if it passed the validation).
- # [create_association!(attributes = {})]
- # Does the same as <tt>create_association</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt>
- # if the record is invalid.
- #
- # === Example
- #
- # An Account class declares <tt>has_one :beneficiary</tt>, which will add:
- # * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.where(account_id: id).first</tt>)
- # * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>)
- # * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>)
- # * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>)
- # * <tt>Account#create_beneficiary!</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save!; b</tt>)
- #
- # === Scopes
- #
- # You can pass a second argument +scope+ as a callable (i.e. proc or
- # lambda) to retrieve a specific record or customize the generated query
- # when you access the associated object.
- #
- # Scope examples:
- # has_one :author, -> { where(comment_id: 1) }
- # has_one :employer, -> { joins(:company) }
- # has_one :dob, ->(dob) { where("Date.new(2000, 01, 01) > ?", dob) }
- #
- # === Options
- #
- # The declaration can also include an +options+ hash to specialize the behavior of the association.
- #
- # Options are:
- # [:class_name]
- # Specify the class name of the association. Use it only if that name can't be inferred
- # from the association name. So <tt>has_one :manager</tt> will by default be linked to the Manager class, but
- # if the real class name is Person, you'll have to specify it with this option.
- # [:dependent]
- # Controls what happens to the associated object when
- # its owner is destroyed:
- #
- # * <tt>:destroy</tt> causes the associated object to also be destroyed
- # * <tt>:delete</tt> causes the associated object to be deleted directly from the database (so callbacks will not execute)
- # * <tt>:nullify</tt> causes the foreign key to be set to +NULL+. Callbacks are not executed.
- # * <tt>:restrict_with_exception</tt> causes an exception to be raised if there is an associated record
- # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there is an associated object
- #
- # Note that <tt>:dependent</tt> option is ignored when using <tt>:through</tt> option.
- # [:foreign_key]
- # Specify the foreign key used for the association. By default this is guessed to be the name
- # of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_one+ association
- # will use "person_id" as the default <tt>:foreign_key</tt>.
- # [:foreign_type]
- # Specify the column used to store the associated object's type, if this is a polymorphic
- # association. By default this is guessed to be the name of the polymorphic association
- # specified on "as" option with a "_type" suffix. So a class that defines a
- # <tt>has_one :tag, as: :taggable</tt> association will use "taggable_type" as the
- # default <tt>:foreign_type</tt>.
- # [:primary_key]
- # Specify the method that returns the primary key used for the association. By default this is +id+.
- # [:as]
- # Specifies a polymorphic interface (See <tt>belongs_to</tt>).
- # [:through]
- # Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt>,
- # <tt>:primary_key</tt>, and <tt>:foreign_key</tt> are ignored, as the association uses the
- # source reflection. You can only use a <tt>:through</tt> query through a <tt>has_one</tt>
- # or <tt>belongs_to</tt> association on the join model.
- # [:source]
- # Specifies the source association name used by <tt>has_one :through</tt> queries.
- # Only use it if the name cannot be inferred from the association.
- # <tt>has_one :favorite, through: :favorites</tt> will look for a
- # <tt>:favorite</tt> on Favorite, unless a <tt>:source</tt> is given.
- # [:source_type]
- # Specifies type of the source association used by <tt>has_one :through</tt> queries where the source
- # association is a polymorphic +belongs_to+.
- # [:validate]
- # If +false+, don't validate the associated object when saving the parent object. +false+ by default.
- # [:autosave]
- # If true, always save the associated object or destroy it if marked for destruction,
- # when saving the parent object. If false, never save or destroy the associated object.
- # By default, only save the associated object if it's a new record.
- #
- # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>.
- # [:inverse_of]
- # Specifies the name of the <tt>belongs_to</tt> association on the associated object
- # that is the inverse of this <tt>has_one</tt> association. Does not work in combination
- # with <tt>:through</tt> or <tt>:as</tt> options.
- # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
- # [:required]
- # When set to +true+, the association will also have its presence validated.
- # This will validate the association itself, not the id. You can use
- # +:inverse_of+ to avoid an extra query during validation.
- #
- # Option examples:
- # has_one :credit_card, dependent: :destroy # destroys the associated credit card
- # has_one :credit_card, dependent: :nullify # updates the associated records foreign
- # # key value to NULL rather than destroying it
- # has_one :last_comment, -> { order 'posted_on' }, class_name: "Comment"
- # has_one :project_manager, -> { where role: 'project_manager' }, class_name: "Person"
- # has_one :attachment, as: :attachable
- # has_one :boss, -> { readonly }
- # has_one :club, through: :membership
- # has_one :primary_address, -> { where primary: true }, through: :addressables, source: :addressable
- # has_one :credit_card, required: true
- def has_one(name, scope = nil, options = {})
- reflection = Builder::HasOne.build(self, name, scope, options)
- Reflection.add_reflection self, name, reflection
- end
-
- # Specifies a one-to-one association with another class. This method should only be used
- # if this class contains the foreign key. If the other class contains the foreign key,
- # then you should use +has_one+ instead. See also ActiveRecord::Associations::ClassMethods's overview
- # on when to use +has_one+ and when to use +belongs_to+.
- #
- # Methods will be added for retrieval and query for a single associated object, for which
- # this object holds an id:
- #
- # +association+ is a placeholder for the symbol passed as the +name+ argument, so
- # <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>.
- #
- # [association(force_reload = false)]
- # Returns the associated object. +nil+ is returned if none is found.
- # [association=(associate)]
- # Assigns the associate object, extracts the primary key, and sets it as the foreign key.
- # [build_association(attributes = {})]
- # Returns a new object of the associated type that has been instantiated
- # with +attributes+ and linked to this object through a foreign key, but has not yet been saved.
- # [create_association(attributes = {})]
- # Returns a new object of the associated type that has been instantiated
- # with +attributes+, linked to this object through a foreign key, and that
- # has already been saved (if it passed the validation).
- # [create_association!(attributes = {})]
- # Does the same as <tt>create_association</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt>
- # if the record is invalid.
- #
- # === Example
- #
- # A Post class declares <tt>belongs_to :author</tt>, which will add:
- # * <tt>Post#author</tt> (similar to <tt>Author.find(author_id)</tt>)
- # * <tt>Post#author=(author)</tt> (similar to <tt>post.author_id = author.id</tt>)
- # * <tt>Post#build_author</tt> (similar to <tt>post.author = Author.new</tt>)
- # * <tt>Post#create_author</tt> (similar to <tt>post.author = Author.new; post.author.save; post.author</tt>)
- # * <tt>Post#create_author!</tt> (similar to <tt>post.author = Author.new; post.author.save!; post.author</tt>)
- # The declaration can also include an +options+ hash to specialize the behavior of the association.
- #
- # === Scopes
- #
- # You can pass a second argument +scope+ as a callable (i.e. proc or
- # lambda) to retrieve a specific record or customize the generated query
- # when you access the associated object.
- #
- # Scope examples:
- # belongs_to :firm, -> { where(id: 2) }
- # belongs_to :user, -> { joins(:friends) }
- # belongs_to :level, ->(level) { where("game_level > ?", level.current) }
- #
- # === Options
- #
- # [:class_name]
- # Specify the class name of the association. Use it only if that name can't be inferred
- # from the association name. So <tt>belongs_to :author</tt> will by default be linked to the Author class, but
- # if the real class name is Person, you'll have to specify it with this option.
- # [:foreign_key]
- # Specify the foreign key used for the association. By default this is guessed to be the name
- # of the association with an "_id" suffix. So a class that defines a <tt>belongs_to :person</tt>
- # association will use "person_id" as the default <tt>:foreign_key</tt>. Similarly,
- # <tt>belongs_to :favorite_person, class_name: "Person"</tt> will use a foreign key
- # of "favorite_person_id".
- # [:foreign_type]
- # Specify the column used to store the associated object's type, if this is a polymorphic
- # association. By default this is guessed to be the name of the association with a "_type"
- # suffix. So a class that defines a <tt>belongs_to :taggable, polymorphic: true</tt>
- # association will use "taggable_type" as the default <tt>:foreign_type</tt>.
- # [:primary_key]
- # Specify the method that returns the primary key of associated object used for the association.
- # By default this is id.
- # [:dependent]
- # If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to
- # <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method.
- # This option should not be specified when <tt>belongs_to</tt> is used in conjunction with
- # a <tt>has_many</tt> relationship on another class because of the potential to leave
- # orphaned records behind.
- # [:counter_cache]
- # Caches the number of belonging objects on the associate class through the use of +increment_counter+
- # and +decrement_counter+. The counter cache is incremented when an object of this
- # class is created and decremented when it's destroyed. This requires that a column
- # named <tt>#{table_name}_count</tt> (such as +comments_count+ for a belonging Comment class)
- # is used on the associate class (such as a Post class) - that is the migration for
- # <tt>#{table_name}_count</tt> is created on the associate class (such that <tt>Post.comments_count</tt> will
- # return the count cached, see note below). You can also specify a custom counter
- # cache column by providing a column name instead of a +true+/+false+ value to this
- # option (e.g., <tt>counter_cache: :my_custom_counter</tt>.)
- # Note: Specifying a counter cache will add it to that model's list of readonly attributes
- # using +attr_readonly+.
- # [:polymorphic]
- # Specify this association is a polymorphic association by passing +true+.
- # Note: If you've enabled the counter cache, then you may want to add the counter cache attribute
- # to the +attr_readonly+ list in the associated classes (e.g. <tt>class Post; attr_readonly :comments_count; end</tt>).
- # [:validate]
- # If +false+, don't validate the associated objects when saving the parent object. +false+ by default.
- # [:autosave]
- # If true, always save the associated object or destroy it if marked for destruction, when
- # saving the parent object.
- # If false, never save or destroy the associated object.
- # By default, only save the associated object if it's a new record.
- #
- # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>.
- # [:touch]
- # If true, the associated object will be touched (the updated_at/on attributes set to current time)
- # when this record is either saved or destroyed. If you specify a symbol, that attribute
- # will be updated with the current time in addition to the updated_at/on attribute.
- # [:inverse_of]
- # Specifies the name of the <tt>has_one</tt> or <tt>has_many</tt> association on the associated
- # object that is the inverse of this <tt>belongs_to</tt> association. Does not work in
- # combination with the <tt>:polymorphic</tt> options.
- # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
- # [:optional]
- # When set to +true+, the association will not have its presence validated.
- # [:required]
- # When set to +true+, the association will also have its presence validated.
- # This will validate the association itself, not the id. You can use
- # +: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>.
- #
- # Option examples:
- # belongs_to :firm, foreign_key: "client_of"
- # belongs_to :person, primary_key: "name", foreign_key: "person_name"
- # belongs_to :author, class_name: "Person", foreign_key: "author_id"
- # belongs_to :valid_coupon, ->(o) { where "discounts > ?", o.payments_count },
- # class_name: "Coupon", foreign_key: "coupon_id"
- # belongs_to :attachable, polymorphic: true
- # belongs_to :project, -> { readonly }
- # belongs_to :post, counter_cache: true
- # belongs_to :comment, touch: true
- # belongs_to :company, touch: :employees_last_updated_at
- # belongs_to :user, optional: true
- def belongs_to(name, scope = nil, options = {})
- reflection = Builder::BelongsTo.build(self, name, scope, options)
- Reflection.add_reflection self, name, reflection
- end
-
- # Specifies a many-to-many relationship with another class. This associates two classes via an
- # intermediate join table. Unless the join table is explicitly specified as an option, it is
- # guessed using the lexical order of the class names. So a join between Developer and Project
- # will give the default join table name of "developers_projects" because "D" precedes "P" alphabetically.
- # Note that this precedence is calculated using the <tt><</tt> operator for String. This
- # means that if the strings are of different lengths, and the strings are equal when compared
- # up to the shortest length, then the longer string is considered of higher
- # lexical precedence than the shorter one. For example, one would expect the tables "paper_boxes" and "papers"
- # to generate a join table name of "papers_paper_boxes" because of the length of the name "paper_boxes",
- # but it in fact generates a join table name of "paper_boxes_papers". Be aware of this caveat, and use the
- # custom <tt>:join_table</tt> option if you need to.
- # If your tables share a common prefix, it will only appear once at the beginning. For example,
- # the tables "catalog_categories" and "catalog_products" generate a join table name of "catalog_categories_products".
- #
- # The join table should not have a primary key or a model associated with it. You must manually generate the
- # join table with a migration such as this:
- #
- # class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration
- # def change
- # create_join_table :developers, :projects
+ # 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
+ #
+ # Active Record associations can be used to describe one-to-one, one-to-many and many-to-many
+ # relationships between models. Each model uses an association to describe its role in
+ # the relation. The #belongs_to association is always used in the model that has
+ # the foreign key.
+ #
+ # === One-to-one
+ #
+ # Use #has_one in the base, and #belongs_to in the associated model.
+ #
+ # class Employee < ActiveRecord::Base
+ # has_one :office
+ # end
+ # class Office < ActiveRecord::Base
+ # belongs_to :employee # foreign key - employee_id
+ # end
+ #
+ # === One-to-many
+ #
+ # Use #has_many in the base, and #belongs_to in the associated model.
+ #
+ # class Manager < ActiveRecord::Base
+ # has_many :employees
+ # end
+ # class Employee < ActiveRecord::Base
+ # belongs_to :manager # foreign key - manager_id
+ # end
+ #
+ # === Many-to-many
+ #
+ # There are two ways to build a many-to-many relationship.
+ #
+ # The first way uses a #has_many association with the <tt>:through</tt> option and a join model, so
+ # there are two stages of associations.
+ #
+ # class Assignment < ActiveRecord::Base
+ # belongs_to :programmer # foreign key - programmer_id
+ # belongs_to :project # foreign key - project_id
+ # end
+ # class Programmer < ActiveRecord::Base
+ # has_many :assignments
+ # has_many :projects, through: :assignments
+ # end
+ # class Project < ActiveRecord::Base
+ # has_many :assignments
+ # has_many :programmers, through: :assignments
+ # end
+ #
+ # For the second way, use #has_and_belongs_to_many in both models. This requires a join table
+ # that has no corresponding model or primary key.
+ #
+ # class Programmer < ActiveRecord::Base
+ # has_and_belongs_to_many :projects # foreign keys in the join table
+ # end
+ # class Project < ActiveRecord::Base
+ # has_and_belongs_to_many :programmers # foreign keys in the join table
+ # end
+ #
+ # Choosing which way to build a many-to-many relationship is not always simple.
+ # If you need to work with the relationship model as its own entity,
+ # use #has_many <tt>:through</tt>. Use #has_and_belongs_to_many when working with legacy schemas or when
+ # you never work directly with the relationship itself.
+ #
+ # == Is it a #belongs_to or #has_one association?
+ #
+ # Both express a 1-1 relationship. The difference is mostly where to place the foreign
+ # key, which goes on the table for the class declaring the #belongs_to relationship.
+ #
+ # class User < ActiveRecord::Base
+ # # I reference an account.
+ # belongs_to :account
+ # end
+ #
+ # class Account < ActiveRecord::Base
+ # # One user references me.
+ # has_one :user
+ # end
+ #
+ # The tables for these classes could look something like:
+ #
+ # CREATE TABLE users (
+ # id bigint NOT NULL auto_increment,
+ # account_id bigint default NULL,
+ # name varchar default NULL,
+ # PRIMARY KEY (id)
+ # )
+ #
+ # CREATE TABLE accounts (
+ # id bigint NOT NULL auto_increment,
+ # name varchar default NULL,
+ # PRIMARY KEY (id)
+ # )
+ #
+ # == Unsaved objects and associations
+ #
+ # You can manipulate objects and associations before they are saved to the database, but
+ # there is some special behavior you should be aware of, mostly involving the saving of
+ # associated objects.
+ #
+ # You can set the <tt>:autosave</tt> option on a #has_one, #belongs_to,
+ # #has_many, or #has_and_belongs_to_many association. Setting it
+ # to +true+ will _always_ save the members, whereas setting it to +false+ will
+ # _never_ save the members. More details about <tt>:autosave</tt> option is available at
+ # AutosaveAssociation.
+ #
+ # === One-to-one associations
+ #
+ # * Assigning an object to a #has_one association automatically saves that object and
+ # the object being replaced (if there is one), in order to update their foreign
+ # keys - except if the parent object is unsaved (<tt>new_record? == true</tt>).
+ # * If either of these saves fail (due to one of the objects being invalid), an
+ # ActiveRecord::RecordNotSaved exception is raised and the assignment is
+ # cancelled.
+ # * If you wish to assign an object to a #has_one association without saving it,
+ # use the <tt>#build_association</tt> method (documented below). The object being
+ # replaced will still be saved to update its foreign key.
+ # * Assigning an object to a #belongs_to association does not save the object, since
+ # the foreign key field belongs on the parent. It does not save the parent either.
+ #
+ # === Collections
+ #
+ # * Adding an object to a collection (#has_many or #has_and_belongs_to_many) automatically
+ # saves that object, except if the parent object (the owner of the collection) is not yet
+ # stored in the database.
+ # * If saving any of the objects being added to a collection (via <tt>push</tt> or similar)
+ # fails, then <tt>push</tt> returns +false+.
+ # * If saving fails while replacing the collection (via <tt>association=</tt>), an
+ # ActiveRecord::RecordNotSaved exception is raised and the assignment is
+ # cancelled.
+ # * You can add an object to a collection without automatically saving it by using the
+ # <tt>collection.build</tt> method (documented below).
+ # * All unsaved (<tt>new_record? == true</tt>) members of the collection are automatically
+ # saved when the parent is saved.
+ #
+ # == Customizing the query
+ #
+ # \Associations are built from <tt>Relation</tt> objects, and you can use the Relation syntax
+ # to customize them. For example, to add a condition:
+ #
+ # class Blog < ActiveRecord::Base
+ # has_many :published_posts, -> { where(published: true) }, class_name: 'Post'
+ # end
+ #
+ # Inside the <tt>-> { ... }</tt> block you can use all of the usual Relation methods.
+ #
+ # === Accessing the owner object
+ #
+ # Sometimes it is useful to have access to the owner object when building the query. The owner
+ # is passed as a parameter to the block. For example, the following association would find all
+ # events that occur on the user's birthday:
+ #
+ # class User < ActiveRecord::Base
+ # has_many :birthday_events, ->(user) { where(starts_on: user.birthday) }, class_name: 'Event'
+ # end
+ #
+ # Note: Joining, eager loading and preloading of these associations is not possible.
+ # These operations happen before instance creation and the scope will be called with a +nil+ argument.
+ #
+ # == Association callbacks
+ #
+ # Similar to the normal callbacks that hook into the life cycle of an Active Record object,
+ # you can also define callbacks that get triggered when you add an object to or remove an
+ # object from an association collection.
+ #
+ # class Project
+ # has_and_belongs_to_many :developers, after_add: :evaluate_velocity
+ #
+ # def evaluate_velocity(developer)
+ # ...
# end
# end
#
- # It's also a good idea to add indexes to each of those columns to speed up the joins process.
- # However, in MySQL it is advised to add a compound index for both of the columns as MySQL only
- # uses one index per table during the lookup.
- #
- # Adds the following methods for retrieval and query:
- #
- # +collection+ is a placeholder for the symbol passed as the +name+ argument, so
- # <tt>has_and_belongs_to_many :categories</tt> would add among others <tt>categories.empty?</tt>.
- #
- # [collection(force_reload = false)]
- # Returns an array of all the associated objects.
- # An empty array is returned if none are found.
- # [collection<<(object, ...)]
- # Adds one or more objects to the collection by creating associations in the join table
- # (<tt>collection.push</tt> and <tt>collection.concat</tt> are aliases to this method).
- # Note that this operation instantly fires update SQL without waiting for the save or update call on the
- # parent object, unless the parent object is a new record.
- # [collection.delete(object, ...)]
- # Removes one or more objects from the collection by removing their associations from the join table.
- # This does not destroy the objects.
- # [collection.destroy(object, ...)]
- # Removes one or more objects from the collection by running destroy on each association in the join table, overriding any dependent option.
- # This does not destroy the objects.
- # [collection=objects]
- # Replaces the collection's content by deleting and adding objects as appropriate.
- # [collection_singular_ids]
- # Returns an array of the associated objects' ids.
- # [collection_singular_ids=ids]
- # Replace the collection by the objects identified by the primary keys in +ids+.
- # [collection.clear]
- # Removes every object from the collection. This does not destroy the objects.
- # [collection.empty?]
- # Returns +true+ if there are no associated objects.
- # [collection.size]
- # Returns the number of associated objects.
- # [collection.find(id)]
- # Finds an associated object responding to the +id+ and that
- # meets the condition that it has to be associated with this object.
- # Uses the same rules as <tt>ActiveRecord::Base.find</tt>.
- # [collection.exists?(...)]
- # Checks whether an associated object with the given conditions exists.
- # Uses the same rules as <tt>ActiveRecord::Base.exists?</tt>.
- # [collection.build(attributes = {})]
- # Returns a new object of the collection type that has been instantiated
- # with +attributes+ and linked to this object through the join table, but has not yet been saved.
- # [collection.create(attributes = {})]
- # Returns a new object of the collection type that has been instantiated
- # with +attributes+, linked to this object through the join table, and that has already been
- # saved (if it passed the validation).
- #
- # === Example
- #
- # A Developer class declares <tt>has_and_belongs_to_many :projects</tt>, which will add:
- # * <tt>Developer#projects</tt>
- # * <tt>Developer#projects<<</tt>
- # * <tt>Developer#projects.delete</tt>
- # * <tt>Developer#projects.destroy</tt>
- # * <tt>Developer#projects=</tt>
- # * <tt>Developer#project_ids</tt>
- # * <tt>Developer#project_ids=</tt>
- # * <tt>Developer#projects.clear</tt>
- # * <tt>Developer#projects.empty?</tt>
- # * <tt>Developer#projects.size</tt>
- # * <tt>Developer#projects.find(id)</tt>
- # * <tt>Developer#projects.exists?(...)</tt>
- # * <tt>Developer#projects.build</tt> (similar to <tt>Project.new("developer_id" => id)</tt>)
- # * <tt>Developer#projects.create</tt> (similar to <tt>c = Project.new("developer_id" => id); c.save; c</tt>)
- # The declaration may include an +options+ hash to specialize the behavior of the association.
- #
- # === Scopes
- #
- # You can pass a second argument +scope+ as a callable (i.e. proc or
- # lambda) to retrieve a specific set of records or customize the generated
- # query when you access the associated collection.
- #
- # Scope examples:
- # has_and_belongs_to_many :projects, -> { includes :milestones, :manager }
- # has_and_belongs_to_many :categories, ->(category) {
- # where("default_category = ?", category.name)
- # }
- #
- # === Extensions
- #
- # The +extension+ argument allows you to pass a block into a
- # has_and_belongs_to_many association. This is useful for adding new
- # finders, creators and other factory-type methods to be used as part of
- # the association.
- #
- # Extension examples:
- # has_and_belongs_to_many :contractors do
+ # It's possible to stack callbacks by passing them as an array. Example:
+ #
+ # class Project
+ # has_and_belongs_to_many :developers,
+ # after_add: [:evaluate_velocity, Proc.new { |p, d| p.shipping_date = Time.now}]
+ # end
+ #
+ # Possible callbacks are: +before_add+, +after_add+, +before_remove+ and +after_remove+.
+ #
+ # If any of the +before_add+ callbacks throw an exception, the object will not be
+ # added to the collection.
+ #
+ # Similarly, if any of the +before_remove+ callbacks throw an exception, the object
+ # will not be removed from the collection.
+ #
+ # == Association extensions
+ #
+ # The proxy objects that control the access to associations can be extended through anonymous
+ # modules. This is especially beneficial for adding new finders, creators, and other
+ # factory-type methods that are only used as part of this association.
+ #
+ # class Account < ActiveRecord::Base
+ # has_many :people do
+ # def find_or_create_by_name(name)
+ # first_name, last_name = name.split(" ", 2)
+ # find_or_create_by(first_name: first_name, last_name: last_name)
+ # end
+ # end
+ # end
+ #
+ # person = Account.first.people.find_or_create_by_name("David Heinemeier Hansson")
+ # person.first_name # => "David"
+ # person.last_name # => "Heinemeier Hansson"
+ #
+ # If you need to share the same extensions between many associations, you can use a named
+ # extension module.
+ #
+ # module FindOrCreateByNameExtension
# def find_or_create_by_name(name)
# first_name, last_name = name.split(" ", 2)
# find_or_create_by(first_name: first_name, last_name: last_name)
# end
# end
#
- # === Options
- #
- # [:class_name]
- # Specify the class name of the association. Use it only if that name can't be inferred
- # from the association name. So <tt>has_and_belongs_to_many :projects</tt> will by default be linked to the
- # Project class, but if the real class name is SuperProject, you'll have to specify it with this option.
- # [:join_table]
- # Specify the name of the join table if the default based on lexical order isn't what you want.
- # <b>WARNING:</b> If you're overwriting the table name of either class, the +table_name+ method
- # MUST be declared underneath any +has_and_belongs_to_many+ declaration in order to work.
- # [:foreign_key]
- # Specify the foreign key used for the association. By default this is guessed to be the name
- # of this class in lower-case and "_id" suffixed. So a Person class that makes
- # a +has_and_belongs_to_many+ association to Project will use "person_id" as the
- # default <tt>:foreign_key</tt>.
- # [:association_foreign_key]
- # Specify the foreign key used for the association on the receiving side of the association.
- # By default this is guessed to be the name of the associated class in lower-case and "_id" suffixed.
- # So if a Person class makes a +has_and_belongs_to_many+ association to Project,
- # the association will use "project_id" as the default <tt>:association_foreign_key</tt>.
- # [:readonly]
- # If true, all the associated objects are readonly through the association.
- # [:validate]
- # If +false+, don't validate the associated objects when saving the parent object. +true+ by default.
- # [:autosave]
- # If true, always save the associated objects or destroy them if marked for destruction, when
- # saving the parent object.
- # If false, never save or destroy the associated objects.
- # By default, only save associated objects that are new records.
- #
- # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>.
- #
- # Option examples:
- # has_and_belongs_to_many :projects
- # has_and_belongs_to_many :projects, -> { includes :milestones, :manager }
- # has_and_belongs_to_many :nations, class_name: "Country"
- # has_and_belongs_to_many :categories, join_table: "prods_cats"
- # has_and_belongs_to_many :categories, -> { readonly }
- def has_and_belongs_to_many(name, scope = nil, options = {}, &extension)
- if scope.is_a?(Hash)
- options = scope
- scope = nil
+ # class Account < ActiveRecord::Base
+ # has_many :people, -> { extending FindOrCreateByNameExtension }
+ # end
+ #
+ # class Company < ActiveRecord::Base
+ # has_many :people, -> { extending FindOrCreateByNameExtension }
+ # end
+ #
+ # Some extensions can only be made to work with knowledge of the association's internals.
+ # Extensions can access relevant state using the following methods (where +items+ is the
+ # name of the association):
+ #
+ # * <tt>record.association(:items).owner</tt> - Returns the object the association is part of.
+ # * <tt>record.association(:items).reflection</tt> - Returns the reflection object that describes the association.
+ # * <tt>record.association(:items).target</tt> - Returns the associated object for #belongs_to and #has_one, or
+ # the collection of associated objects for #has_many and #has_and_belongs_to_many.
+ #
+ # However, inside the actual extension code, you will not have access to the <tt>record</tt> as
+ # above. In this case, you can access <tt>proxy_association</tt>. For example,
+ # <tt>record.association(:items)</tt> and <tt>record.items.proxy_association</tt> will return
+ # the same object, allowing you to make calls like <tt>proxy_association.owner</tt> inside
+ # association extensions.
+ #
+ # == Association Join Models
+ #
+ # Has Many associations can be configured with the <tt>:through</tt> option to use an
+ # explicit join model to retrieve the data. This operates similarly to a
+ # #has_and_belongs_to_many association. The advantage is that you're able to add validations,
+ # callbacks, and extra attributes on the join model. Consider the following schema:
+ #
+ # class Author < ActiveRecord::Base
+ # has_many :authorships
+ # has_many :books, through: :authorships
+ # end
+ #
+ # class Authorship < ActiveRecord::Base
+ # belongs_to :author
+ # belongs_to :book
+ # end
+ #
+ # @author = Author.first
+ # @author.authorships.collect { |a| a.book } # selects all books that the author's authorships belong to
+ # @author.books # selects all books by using the Authorship join model
+ #
+ # You can also go through a #has_many association on the join model:
+ #
+ # class Firm < ActiveRecord::Base
+ # has_many :clients
+ # has_many :invoices, through: :clients
+ # end
+ #
+ # class Client < ActiveRecord::Base
+ # belongs_to :firm
+ # has_many :invoices
+ # end
+ #
+ # class Invoice < ActiveRecord::Base
+ # belongs_to :client
+ # end
+ #
+ # @firm = Firm.first
+ # @firm.clients.flat_map { |c| c.invoices } # select all invoices for all clients of the firm
+ # @firm.invoices # selects all invoices by going through the Client join model
+ #
+ # Similarly you can go through a #has_one association on the join model:
+ #
+ # class Group < ActiveRecord::Base
+ # has_many :users
+ # has_many :avatars, through: :users
+ # end
+ #
+ # class User < ActiveRecord::Base
+ # belongs_to :group
+ # has_one :avatar
+ # end
+ #
+ # class Avatar < ActiveRecord::Base
+ # belongs_to :user
+ # end
+ #
+ # @group = Group.first
+ # @group.users.collect { |u| u.avatar }.compact # select all avatars for all users in the group
+ # @group.avatars # selects all avatars by going through the User join model.
+ #
+ # An important caveat with going through #has_one or #has_many associations on the
+ # join model is that these associations are *read-only*. For example, the following
+ # would not work following the previous example:
+ #
+ # @group.avatars << Avatar.new # this would work if User belonged_to Avatar rather than the other way around
+ # @group.avatars.delete(@group.avatars.last) # so would this
+ #
+ # == Setting Inverses
+ #
+ # If you are using a #belongs_to on the join model, it is a good idea to set the
+ # <tt>:inverse_of</tt> option on the #belongs_to, which will mean that the following example
+ # works correctly (where <tt>tags</tt> is a #has_many <tt>:through</tt> association):
+ #
+ # @post = Post.first
+ # @tag = @post.tags.build name: "ruby"
+ # @tag.save
+ #
+ # The last line ought to save the through record (a <tt>Tagging</tt>). This will only work if the
+ # <tt>:inverse_of</tt> is set:
+ #
+ # class Tagging < ActiveRecord::Base
+ # belongs_to :post
+ # belongs_to :tag, inverse_of: :taggings
+ # end
+ #
+ # If you do not set the <tt>:inverse_of</tt> record, the association will
+ # do its best to match itself up with the correct inverse. Automatic
+ # inverse detection only works on #has_many, #has_one, and
+ # #belongs_to associations.
+ #
+ # Extra options on the associations, as defined in the
+ # <tt>AssociationReflection::INVALID_AUTOMATIC_INVERSE_OPTIONS</tt> constant, will
+ # also prevent the association's inverse from being found automatically.
+ #
+ # The automatic guessing of the inverse association uses a heuristic based
+ # on the name of the class, so it may not work for all associations,
+ # especially the ones with non-standard names.
+ #
+ # You can turn off the automatic detection of inverse associations by setting
+ # the <tt>:inverse_of</tt> option to <tt>false</tt> like so:
+ #
+ # class Tagging < ActiveRecord::Base
+ # belongs_to :tag, inverse_of: false
+ # end
+ #
+ # == Nested \Associations
+ #
+ # You can actually specify *any* association with the <tt>:through</tt> option, including an
+ # association which has a <tt>:through</tt> option itself. For example:
+ #
+ # class Author < ActiveRecord::Base
+ # has_many :posts
+ # has_many :comments, through: :posts
+ # has_many :commenters, through: :comments
+ # end
+ #
+ # class Post < ActiveRecord::Base
+ # has_many :comments
+ # end
+ #
+ # class Comment < ActiveRecord::Base
+ # belongs_to :commenter
+ # end
+ #
+ # @author = Author.first
+ # @author.commenters # => People who commented on posts written by the author
+ #
+ # An equivalent way of setting up this association this would be:
+ #
+ # class Author < ActiveRecord::Base
+ # has_many :posts
+ # has_many :commenters, through: :posts
+ # end
+ #
+ # class Post < ActiveRecord::Base
+ # has_many :comments
+ # has_many :commenters, through: :comments
+ # end
+ #
+ # class Comment < ActiveRecord::Base
+ # belongs_to :commenter
+ # end
+ #
+ # When using a nested association, you will not be able to modify the association because there
+ # is not enough information to know what modification to make. For example, if you tried to
+ # add a <tt>Commenter</tt> in the example above, there would be no way to tell how to set up the
+ # intermediate <tt>Post</tt> and <tt>Comment</tt> objects.
+ #
+ # == Polymorphic \Associations
+ #
+ # Polymorphic associations on models are not restricted on what types of models they
+ # can be associated with. Rather, they specify an interface that a #has_many association
+ # must adhere to.
+ #
+ # class Asset < ActiveRecord::Base
+ # belongs_to :attachable, polymorphic: true
+ # end
+ #
+ # class Post < ActiveRecord::Base
+ # has_many :assets, as: :attachable # The :as option specifies the polymorphic interface to use.
+ # end
+ #
+ # @asset.attachable = @post
+ #
+ # This works by using a type column in addition to a foreign key to specify the associated
+ # record. In the Asset example, you'd need an +attachable_id+ integer column and an
+ # +attachable_type+ string column.
+ #
+ # Using polymorphic associations in combination with single table inheritance (STI) is
+ # a little tricky. In order for the associations to work as expected, ensure that you
+ # store the base model for the STI models in the type column of the polymorphic
+ # association. To continue with the asset example above, suppose there are guest posts
+ # and member posts that use the posts table for STI. In this case, there must be a +type+
+ # column in the posts table.
+ #
+ # Note: The <tt>attachable_type=</tt> method is being called when assigning an +attachable+.
+ # The +class_name+ of the +attachable+ is passed as a String.
+ #
+ # class Asset < ActiveRecord::Base
+ # belongs_to :attachable, polymorphic: true
+ #
+ # def attachable_type=(class_name)
+ # super(class_name.constantize.base_class.to_s)
+ # end
+ # end
+ #
+ # class Post < ActiveRecord::Base
+ # # because we store "Post" in attachable_type now dependent: :destroy will work
+ # has_many :assets, as: :attachable, dependent: :destroy
+ # end
+ #
+ # class GuestPost < Post
+ # end
+ #
+ # class MemberPost < Post
+ # end
+ #
+ # == Caching
+ #
+ # All of the methods are built on a simple caching principle that will keep the result
+ # of the last query around unless specifically instructed not to. The cache is even
+ # shared across methods to make it even cheaper to use the macro-added methods without
+ # worrying too much about performance at the first go.
+ #
+ # project.milestones # fetches milestones from the database
+ # project.milestones.size # uses the milestone cache
+ # project.milestones.empty? # uses the milestone cache
+ # project.milestones.reload.size # fetches milestones from the database
+ # project.milestones # uses the milestone cache
+ #
+ # == Eager loading of associations
+ #
+ # Eager loading is a way to find objects of a certain class and a number of named associations.
+ # It is one of the easiest ways to prevent the dreaded N+1 problem in which fetching 100
+ # posts that each need to display their author triggers 101 database queries. Through the
+ # use of eager loading, the number of queries will be reduced from 101 to 2.
+ #
+ # class Post < ActiveRecord::Base
+ # belongs_to :author
+ # has_many :comments
+ # end
+ #
+ # Consider the following loop using the class above:
+ #
+ # Post.all.each do |post|
+ # puts "Post: " + post.title
+ # puts "Written by: " + post.author.name
+ # puts "Last comment on: " + post.comments.first.created_on
+ # end
+ #
+ # To iterate over these one hundred posts, we'll generate 201 database queries. Let's
+ # first just optimize it for retrieving the author:
+ #
+ # Post.includes(:author).each do |post|
+ #
+ # This references the name of the #belongs_to association that also used the <tt>:author</tt>
+ # symbol. After loading the posts, +find+ will collect the +author_id+ from each one and load
+ # all of the referenced authors with one query. Doing so will cut down the number of queries
+ # from 201 to 102.
+ #
+ # We can improve upon the situation further by referencing both associations in the finder with:
+ #
+ # Post.includes(:author, :comments).each do |post|
+ #
+ # This will load all comments with a single query. This reduces the total number of queries
+ # to 3. In general, the number of queries will be 1 plus the number of associations
+ # named (except if some of the associations are polymorphic #belongs_to - see below).
+ #
+ # To include a deep hierarchy of associations, use a hash:
+ #
+ # Post.includes(:author, { comments: { author: :gravatar } }).each do |post|
+ #
+ # The above code will load all the comments and all of their associated
+ # authors and gravatars. You can mix and match any combination of symbols,
+ # arrays, and hashes to retrieve the associations you want to load.
+ #
+ # All of this power shouldn't fool you into thinking that you can pull out huge amounts
+ # of data with no performance penalty just because you've reduced the number of queries.
+ # The database still needs to send all the data to Active Record and it still needs to
+ # be processed. So it's no catch-all for performance problems, but it's a great way to
+ # cut down on the number of queries in a situation as the one described above.
+ #
+ # Since only one table is loaded at a time, conditions or orders cannot reference tables
+ # other than the main one. If this is the case, Active Record falls back to the previously
+ # used <tt>LEFT OUTER JOIN</tt> based strategy. For example:
+ #
+ # Post.includes([:author, :comments]).where(['comments.approved = ?', true])
+ #
+ # This will result in a single SQL query with joins along the lines of:
+ # <tt>LEFT OUTER JOIN comments ON comments.post_id = posts.id</tt> and
+ # <tt>LEFT OUTER JOIN authors ON authors.id = posts.author_id</tt>. Note that using conditions
+ # like this can have unintended consequences.
+ # In the above example, posts with no approved comments are not returned at all because
+ # the conditions apply to the SQL statement as a whole and not just to the association.
+ #
+ # You must disambiguate column references for this fallback to happen, for example
+ # <tt>order: "author.name DESC"</tt> will work but <tt>order: "name DESC"</tt> will not.
+ #
+ # If you want to load all posts (including posts with no approved comments), then write
+ # your own <tt>LEFT OUTER JOIN</tt> query using <tt>ON</tt>:
+ #
+ # Post.joins("LEFT OUTER JOIN comments ON comments.post_id = posts.id AND comments.approved = '1'")
+ #
+ # In this case, it is usually more natural to include an association which has conditions defined on it:
+ #
+ # class Post < ActiveRecord::Base
+ # has_many :approved_comments, -> { where(approved: true) }, class_name: 'Comment'
+ # end
+ #
+ # Post.includes(:approved_comments)
+ #
+ # This will load posts and eager load the +approved_comments+ association, which contains
+ # only those comments that have been approved.
+ #
+ # If you eager load an association with a specified <tt>:limit</tt> option, it will be ignored,
+ # returning all the associated objects:
+ #
+ # class Picture < ActiveRecord::Base
+ # has_many :most_recent_comments, -> { order('id DESC').limit(10) }, class_name: 'Comment'
+ # end
+ #
+ # Picture.includes(:most_recent_comments).first.most_recent_comments # => returns all associated comments.
+ #
+ # Eager loading is supported with polymorphic associations.
+ #
+ # class Address < ActiveRecord::Base
+ # belongs_to :addressable, polymorphic: true
+ # end
+ #
+ # A call that tries to eager load the addressable model
+ #
+ # Address.includes(:addressable)
+ #
+ # This will execute one query to load the addresses and load the addressables with one
+ # query per addressable type.
+ # For example, if all the addressables are either of class Person or Company, then a total
+ # of 3 queries will be executed. The list of addressable types to load is determined on
+ # the back of the addresses loaded. This is not supported if Active Record has to fallback
+ # to the previous implementation of eager loading and will raise ActiveRecord::EagerLoadPolymorphicError.
+ # The reason is that the parent model's type is a column value so its corresponding table
+ # name cannot be put in the +FROM+/+JOIN+ clauses of that query.
+ #
+ # == Table Aliasing
+ #
+ # Active Record uses table aliasing in the case that a table is referenced multiple times
+ # in a join. If a table is referenced only once, the standard table name is used. The
+ # second time, the table is aliased as <tt>#{reflection_name}_#{parent_table_name}</tt>.
+ # Indexes are appended for any more successive uses of the table name.
+ #
+ # Post.joins(:comments)
+ # # => SELECT ... FROM posts INNER JOIN comments ON ...
+ # Post.joins(:special_comments) # STI
+ # # => SELECT ... FROM posts INNER JOIN comments ON ... AND comments.type = 'SpecialComment'
+ # Post.joins(:comments, :special_comments) # special_comments is the reflection name, posts is the parent table name
+ # # => SELECT ... FROM posts INNER JOIN comments ON ... INNER JOIN comments special_comments_posts
+ #
+ # Acts as tree example:
+ #
+ # TreeMixin.joins(:children)
+ # # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
+ # TreeMixin.joins(children: :parent)
+ # # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
+ # INNER JOIN parents_mixins ...
+ # TreeMixin.joins(children: {parent: :children})
+ # # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
+ # INNER JOIN parents_mixins ...
+ # INNER JOIN mixins childrens_mixins_2
+ #
+ # Has and Belongs to Many join tables use the same idea, but add a <tt>_join</tt> suffix:
+ #
+ # Post.joins(:categories)
+ # # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
+ # Post.joins(categories: :posts)
+ # # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
+ # INNER JOIN categories_posts posts_categories_join INNER JOIN posts posts_categories
+ # Post.joins(categories: {posts: :categories})
+ # # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
+ # INNER JOIN categories_posts posts_categories_join INNER JOIN posts posts_categories
+ # INNER JOIN categories_posts categories_posts_join INNER JOIN categories categories_posts_2
+ #
+ # If you wish to specify your own custom joins using ActiveRecord::QueryMethods#joins method, those table
+ # names will take precedence over the eager associations:
+ #
+ # Post.joins(:comments).joins("inner join comments ...")
+ # # => SELECT ... FROM posts INNER JOIN comments_posts ON ... INNER JOIN comments ...
+ # Post.joins(:comments, :special_comments).joins("inner join comments ...")
+ # # => SELECT ... FROM posts INNER JOIN comments comments_posts ON ...
+ # INNER JOIN comments special_comments_posts ...
+ # INNER JOIN comments ...
+ #
+ # Table aliases are automatically truncated according to the maximum length of table identifiers
+ # according to the specific database.
+ #
+ # == Modules
+ #
+ # By default, associations will look for objects within the current module scope. Consider:
+ #
+ # module MyApplication
+ # module Business
+ # class Firm < ActiveRecord::Base
+ # has_many :clients
+ # end
+ #
+ # class Client < ActiveRecord::Base; end
+ # end
+ # end
+ #
+ # When <tt>Firm#clients</tt> is called, it will in turn call
+ # <tt>MyApplication::Business::Client.find_all_by_firm_id(firm.id)</tt>.
+ # If you want to associate with a class in another module scope, this can be done by
+ # specifying the complete class name.
+ #
+ # module MyApplication
+ # module Business
+ # class Firm < ActiveRecord::Base; end
+ # end
+ #
+ # module Billing
+ # class Account < ActiveRecord::Base
+ # belongs_to :firm, class_name: "MyApplication::Business::Firm"
+ # end
+ # end
+ # end
+ #
+ # == Bi-directional associations
+ #
+ # When you specify an association, there is usually an association on the associated model
+ # that specifies the same relationship in reverse. For example, with the following models:
+ #
+ # class Dungeon < ActiveRecord::Base
+ # has_many :traps
+ # has_one :evil_wizard
+ # end
+ #
+ # class Trap < ActiveRecord::Base
+ # belongs_to :dungeon
+ # end
+ #
+ # class EvilWizard < ActiveRecord::Base
+ # belongs_to :dungeon
+ # end
+ #
+ # The +traps+ association on +Dungeon+ and the +dungeon+ association on +Trap+ are
+ # the inverse of each other, and the inverse of the +dungeon+ association on +EvilWizard+
+ # is the +evil_wizard+ association on +Dungeon+ (and vice-versa). By default,
+ # Active Record can guess the inverse of the association based on the name
+ # of the class. The result is the following:
+ #
+ # d = Dungeon.first
+ # t = d.traps.first
+ # d.object_id == t.dungeon.object_id # => true
+ #
+ # The +Dungeon+ instances +d+ and <tt>t.dungeon</tt> in the above example refer to
+ # the same in-memory instance since the association matches the name of the class.
+ # The result would be the same if we added +:inverse_of+ to our model definitions:
+ #
+ # class Dungeon < ActiveRecord::Base
+ # has_many :traps, inverse_of: :dungeon
+ # has_one :evil_wizard, inverse_of: :dungeon
+ # end
+ #
+ # class Trap < ActiveRecord::Base
+ # belongs_to :dungeon, inverse_of: :traps
+ # end
+ #
+ # class EvilWizard < ActiveRecord::Base
+ # belongs_to :dungeon, inverse_of: :evil_wizard
+ # end
+ #
+ # For more information, see the documentation for the +:inverse_of+ option.
+ #
+ # == Deleting from associations
+ #
+ # === Dependent associations
+ #
+ # #has_many, #has_one, and #belongs_to associations support the <tt>:dependent</tt> option.
+ # This allows you to specify that associated records should be deleted when the owner is
+ # deleted.
+ #
+ # For example:
+ #
+ # class Author
+ # has_many :posts, dependent: :destroy
+ # end
+ # Author.find(1).destroy # => Will destroy all of the author's posts, too
+ #
+ # The <tt>:dependent</tt> option can have different values which specify how the deletion
+ # is done. For more information, see the documentation for this option on the different
+ # specific association types. When no option is given, the behavior is to do nothing
+ # with the associated records when destroying a record.
+ #
+ # Note that <tt>:dependent</tt> is implemented using Rails' callback
+ # system, which works by processing callbacks in order. Therefore, other
+ # callbacks declared either before or after the <tt>:dependent</tt> option
+ # can affect what it does.
+ #
+ # Note that <tt>:dependent</tt> option is ignored for #has_one <tt>:through</tt> associations.
+ #
+ # === Delete or destroy?
+ #
+ # #has_many and #has_and_belongs_to_many associations have the methods <tt>destroy</tt>,
+ # <tt>delete</tt>, <tt>destroy_all</tt> and <tt>delete_all</tt>.
+ #
+ # For #has_and_belongs_to_many, <tt>delete</tt> and <tt>destroy</tt> are the same: they
+ # cause the records in the join table to be removed.
+ #
+ # For #has_many, <tt>destroy</tt> and <tt>destroy_all</tt> will always call the <tt>destroy</tt> method of the
+ # record(s) being removed so that callbacks are run. However <tt>delete</tt> and <tt>delete_all</tt> will either
+ # do the deletion according to the strategy specified by the <tt>:dependent</tt> option, or
+ # if no <tt>:dependent</tt> option is given, then it will follow the default strategy.
+ # The default strategy is to do nothing (leave the foreign keys with the parent ids set), except for
+ # #has_many <tt>:through</tt>, where the default strategy is <tt>delete_all</tt> (delete
+ # the join records, without running their callbacks).
+ #
+ # There is also a <tt>clear</tt> method which is the same as <tt>delete_all</tt>, except that
+ # it returns the association rather than the records which have been deleted.
+ #
+ # === What gets deleted?
+ #
+ # There is a potential pitfall here: #has_and_belongs_to_many and #has_many <tt>:through</tt>
+ # associations have records in join tables, as well as the associated records. So when we
+ # call one of these deletion methods, what exactly should be deleted?
+ #
+ # The answer is that it is assumed that deletion on an association is about removing the
+ # <i>link</i> between the owner and the associated object(s), rather than necessarily the
+ # associated objects themselves. So with #has_and_belongs_to_many and #has_many
+ # <tt>:through</tt>, the join records will be deleted, but the associated records won't.
+ #
+ # This makes sense if you think about it: if you were to call <tt>post.tags.delete(Tag.find_by(name: 'food'))</tt>
+ # you would want the 'food' tag to be unlinked from the post, rather than for the tag itself
+ # to be removed from the database.
+ #
+ # However, there are examples where this strategy doesn't make sense. For example, suppose
+ # a person has many projects, and each project has many tasks. If we deleted one of a person's
+ # tasks, we would probably not want the project to be deleted. In this scenario, the delete method
+ # won't actually work: it can only be used if the association on the join model is a
+ # #belongs_to. In other situations you are expected to perform operations directly on
+ # either the associated records or the <tt>:through</tt> association.
+ #
+ # With a regular #has_many there is no distinction between the "associated records"
+ # and the "link", so there is only one choice for what gets deleted.
+ #
+ # With #has_and_belongs_to_many and #has_many <tt>:through</tt>, if you want to delete the
+ # associated records themselves, you can always do something along the lines of
+ # <tt>person.tasks.each(&:destroy)</tt>.
+ #
+ # == Type safety with ActiveRecord::AssociationTypeMismatch
+ #
+ # If you attempt to assign an object to an association that doesn't match the inferred
+ # or specified <tt>:class_name</tt>, you'll get an ActiveRecord::AssociationTypeMismatch.
+ #
+ # == Options
+ #
+ # All of the association macros can be specialized through options. This makes cases
+ # more complex than the simple and guessable ones possible.
+ module ClassMethods
+ # Specifies a one-to-many association. The following methods for retrieval and query of
+ # collections of associated objects will be added:
+ #
+ # +collection+ is a placeholder for the symbol passed as the +name+ argument, so
+ # <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>.
+ #
+ # [collection]
+ # Returns a Relation of all the associated objects.
+ # An empty Relation is returned if none are found.
+ # [collection<<(object, ...)]
+ # Adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
+ # Note that this operation instantly fires update SQL without waiting for the save or update call on the
+ # parent object, unless the parent object is a new record.
+ # This will also run validations and callbacks of associated object(s).
+ # [collection.delete(object, ...)]
+ # Removes one or more objects from the collection by setting their foreign keys to +NULL+.
+ # Objects will be in addition destroyed if they're associated with <tt>dependent: :destroy</tt>,
+ # and deleted if they're associated with <tt>dependent: :delete_all</tt>.
+ #
+ # If the <tt>:through</tt> option is used, then the join records are deleted (rather than
+ # nullified) by default, but you can specify <tt>dependent: :destroy</tt> or
+ # <tt>dependent: :nullify</tt> to override this.
+ # [collection.destroy(object, ...)]
+ # Removes one or more objects from the collection by running <tt>destroy</tt> on
+ # each record, regardless of any dependent option, ensuring callbacks are run.
+ #
+ # If the <tt>:through</tt> option is used, then the join records are destroyed
+ # instead, not the objects themselves.
+ # [collection=objects]
+ # Replaces the collections content by deleting and adding objects as appropriate. If the <tt>:through</tt>
+ # option is true callbacks in the join models are triggered except destroy callbacks, since deletion is
+ # direct by default. You can specify <tt>dependent: :destroy</tt> or
+ # <tt>dependent: :nullify</tt> to override this.
+ # [collection_singular_ids]
+ # Returns an array of the associated objects' ids
+ # [collection_singular_ids=ids]
+ # Replace the collection with the objects identified by the primary keys in +ids+. This
+ # method loads the models and calls <tt>collection=</tt>. See above.
+ # [collection.clear]
+ # Removes every object from the collection. This destroys the associated objects if they
+ # are associated with <tt>dependent: :destroy</tt>, deletes them directly from the
+ # database if <tt>dependent: :delete_all</tt>, otherwise sets their foreign keys to +NULL+.
+ # If the <tt>:through</tt> option is true no destroy callbacks are invoked on the join models.
+ # Join models are directly deleted.
+ # [collection.empty?]
+ # Returns +true+ if there are no associated objects.
+ # [collection.size]
+ # Returns the number of associated objects.
+ # [collection.find(...)]
+ # Finds an associated object according to the same rules as ActiveRecord::FinderMethods#find.
+ # [collection.exists?(...)]
+ # Checks whether an associated object with the given conditions exists.
+ # Uses the same rules as ActiveRecord::FinderMethods#exists?.
+ # [collection.build(attributes = {}, ...)]
+ # Returns one or more new objects of the collection type that have been instantiated
+ # with +attributes+ and linked to this object through a foreign key, but have not yet
+ # been saved.
+ # [collection.create(attributes = {})]
+ # Returns a new object of the collection type that has been instantiated
+ # with +attributes+, linked to this object through a foreign key, and that has already
+ # been saved (if it passed the validation). *Note*: This only works if the base model
+ # already exists in the DB, not if it is a new (unsaved) record!
+ # [collection.create!(attributes = {})]
+ # Does the same as <tt>collection.create</tt>, but raises ActiveRecord::RecordInvalid
+ # if the record is invalid.
+ # [collection.reload]
+ # Returns a Relation of all of the associated objects, forcing a database read.
+ # An empty Relation is returned if none are found.
+ #
+ # === Example
+ #
+ # A <tt>Firm</tt> class declares <tt>has_many :clients</tt>, which will add:
+ # * <tt>Firm#clients</tt> (similar to <tt>Client.where(firm_id: id)</tt>)
+ # * <tt>Firm#clients<<</tt>
+ # * <tt>Firm#clients.delete</tt>
+ # * <tt>Firm#clients.destroy</tt>
+ # * <tt>Firm#clients=</tt>
+ # * <tt>Firm#client_ids</tt>
+ # * <tt>Firm#client_ids=</tt>
+ # * <tt>Firm#clients.clear</tt>
+ # * <tt>Firm#clients.empty?</tt> (similar to <tt>firm.clients.size == 0</tt>)
+ # * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "firm_id = #{id}"</tt>)
+ # * <tt>Firm#clients.find</tt> (similar to <tt>Client.where(firm_id: id).find(id)</tt>)
+ # * <tt>Firm#clients.exists?(name: 'ACME')</tt> (similar to <tt>Client.exists?(name: 'ACME', firm_id: firm.id)</tt>)
+ # * <tt>Firm#clients.build</tt> (similar to <tt>Client.new(firm_id: id)</tt>)
+ # * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new(firm_id: id); c.save; c</tt>)
+ # * <tt>Firm#clients.create!</tt> (similar to <tt>c = Client.new(firm_id: id); c.save!</tt>)
+ # * <tt>Firm#clients.reload</tt>
+ # The declaration can also include an +options+ hash to specialize the behavior of the association.
+ #
+ # === Scopes
+ #
+ # You can pass a second argument +scope+ as a callable (i.e. proc or
+ # lambda) to retrieve a specific set of records or customize the generated
+ # query when you access the associated collection.
+ #
+ # Scope examples:
+ # has_many :comments, -> { where(author_id: 1) }
+ # has_many :employees, -> { joins(:address) }
+ # has_many :posts, ->(blog) { where("max_post_length > ?", blog.max_post_length) }
+ #
+ # === Extensions
+ #
+ # The +extension+ argument allows you to pass a block into a has_many
+ # association. This is useful for adding new finders, creators and other
+ # factory-type methods to be used as part of the association.
+ #
+ # Extension examples:
+ # has_many :employees do
+ # def find_or_create_by_name(name)
+ # first_name, last_name = name.split(" ", 2)
+ # find_or_create_by(first_name: first_name, last_name: last_name)
+ # end
+ # end
+ #
+ # === Options
+ # [:class_name]
+ # Specify the class name of the association. Use it only if that name can't be inferred
+ # from the association name. So <tt>has_many :products</tt> will by default be linked
+ # to the +Product+ class, but if the real class name is +SpecialProduct+, you'll have to
+ # specify it with this option.
+ # [:foreign_key]
+ # Specify the foreign key used for the association. By default this is guessed to be the name
+ # of this class in lower-case and "_id" suffixed. So a Person class that makes a #has_many
+ # association will use "person_id" as the default <tt>:foreign_key</tt>.
+ #
+ # If you are going to modify the association (rather than just read from it), then it is
+ # a good idea to set the <tt>:inverse_of</tt> option.
+ # [:foreign_type]
+ # Specify the column used to store the associated object's type, if this is a polymorphic
+ # association. By default this is guessed to be the name of the polymorphic association
+ # specified on "as" option with a "_type" suffix. So a class that defines a
+ # <tt>has_many :tags, as: :taggable</tt> association will use "taggable_type" as the
+ # default <tt>:foreign_type</tt>.
+ # [:primary_key]
+ # Specify the name of the column to use as the primary key for the association. By default this is +id+.
+ # [:dependent]
+ # Controls what happens to the associated objects when
+ # their owner is destroyed. Note that these are implemented as
+ # callbacks, and Rails executes callbacks in order. Therefore, other
+ # similar callbacks may affect the <tt>:dependent</tt> behavior, and the
+ # <tt>:dependent</tt> behavior may affect other callbacks.
+ #
+ # * <tt>:destroy</tt> causes all the associated objects to also be destroyed.
+ # * <tt>:delete_all</tt> causes all the associated objects to be deleted directly from the database (so callbacks will not be executed).
+ # * <tt>:nullify</tt> causes the foreign keys to be set to +NULL+. Callbacks are not executed.
+ # * <tt>:restrict_with_exception</tt> causes an <tt>ActiveRecord::DeleteRestrictionError</tt> exception to be raised if there are any associated records.
+ # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there are any associated objects.
+ #
+ # If using with the <tt>:through</tt> option, the association on the join model must be
+ # a #belongs_to, and the records which get deleted are the join records, rather than
+ # the associated records.
+ #
+ # If using <tt>dependent: :destroy</tt> on a scoped association, only the scoped objects are destroyed.
+ # For example, if a Post model defines
+ # <tt>has_many :comments, -> { where published: true }, dependent: :destroy</tt> and <tt>destroy</tt> is
+ # called on a post, only published comments are destroyed. This means that any unpublished comments in the
+ # database would still contain a foreign key pointing to the now deleted post.
+ # [:counter_cache]
+ # This option can be used to configure a custom named <tt>:counter_cache.</tt> You only need this option,
+ # when you customized the name of your <tt>:counter_cache</tt> on the #belongs_to association.
+ # [:as]
+ # Specifies a polymorphic interface (See #belongs_to).
+ # [:through]
+ # Specifies an association through which to perform the query. This can be any other type
+ # of association, including other <tt>:through</tt> associations. Options for <tt>:class_name</tt>,
+ # <tt>:primary_key</tt> and <tt>:foreign_key</tt> are ignored, as the association uses the
+ # source reflection.
+ #
+ # If the association on the join model is a #belongs_to, the collection can be modified
+ # and the records on the <tt>:through</tt> model will be automatically created and removed
+ # as appropriate. Otherwise, the collection is read-only, so you should manipulate the
+ # <tt>:through</tt> association directly.
+ #
+ # If you are going to modify the association (rather than just read from it), then it is
+ # a good idea to set the <tt>:inverse_of</tt> option on the source association on the
+ # join model. This allows associated records to be built which will automatically create
+ # the appropriate join model records when they are saved. (See the 'Association Join Models'
+ # section above.)
+ # [:source]
+ # Specifies the source association name used by #has_many <tt>:through</tt> queries.
+ # Only use it if the name cannot be inferred from the association.
+ # <tt>has_many :subscribers, through: :subscriptions</tt> will look for either <tt>:subscribers</tt> or
+ # <tt>:subscriber</tt> on Subscription, unless a <tt>:source</tt> is given.
+ # [:source_type]
+ # Specifies type of the source association used by #has_many <tt>:through</tt> queries where the source
+ # association is a polymorphic #belongs_to.
+ # [:validate]
+ # When set to +true+, validates new objects added to association when saving the parent object. +true+ by default.
+ # If you want to ensure associated objects are revalidated on every update, use +validates_associated+.
+ # [:autosave]
+ # If true, always save the associated objects or destroy them if marked for destruction,
+ # when saving the parent object. If false, never save or destroy the associated objects.
+ # By default, only save associated objects that are new records. This option is implemented as a
+ # +before_save+ callback. Because callbacks are run in the order they are defined, associated objects
+ # may need to be explicitly saved in any user-defined +before_save+ callbacks.
+ #
+ # Note that NestedAttributes::ClassMethods#accepts_nested_attributes_for sets
+ # <tt>:autosave</tt> to <tt>true</tt>.
+ # [:inverse_of]
+ # Specifies the name of the #belongs_to association on the associated object
+ # that is the inverse of this #has_many association.
+ # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
+ # [:extend]
+ # Specifies a module or array of modules that will be extended into the association object returned.
+ # Useful for defining methods on associations, especially when they should be shared between multiple
+ # association objects.
+ #
+ # Option examples:
+ # has_many :comments, -> { order("posted_on") }
+ # has_many :comments, -> { includes(:author) }
+ # has_many :people, -> { where(deleted: false).order("name") }, class_name: "Person"
+ # has_many :tracks, -> { order("position") }, dependent: :destroy
+ # has_many :comments, dependent: :nullify
+ # has_many :tags, as: :taggable
+ # has_many :reports, -> { readonly }
+ # has_many :subscribers, through: :subscriptions, source: :user
+ def has_many(name, scope = nil, **options, &extension)
+ reflection = Builder::HasMany.build(self, name, scope, options, &extension)
+ Reflection.add_reflection self, name, reflection
end
- habtm_reflection = ActiveRecord::Reflection::HasAndBelongsToManyReflection.new(name, scope, options, self)
+ # Specifies a one-to-one association with another class. This method should only be used
+ # if the other class contains the foreign key. If the current class contains the foreign key,
+ # then you should use #belongs_to instead. See also ActiveRecord::Associations::ClassMethods's overview
+ # on when to use #has_one and when to use #belongs_to.
+ #
+ # The following methods for retrieval and query of a single associated object will be added:
+ #
+ # +association+ is a placeholder for the symbol passed as the +name+ argument, so
+ # <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>.
+ #
+ # [association]
+ # Returns the associated object. +nil+ is returned if none is found.
+ # [association=(associate)]
+ # Assigns the associate object, extracts the primary key, sets it as the foreign key,
+ # and saves the associate object. To avoid database inconsistencies, permanently deletes an existing
+ # associated object when assigning a new one, even if the new one isn't saved to database.
+ # [build_association(attributes = {})]
+ # Returns a new object of the associated type that has been instantiated
+ # with +attributes+ and linked to this object through a foreign key, but has not
+ # yet been saved.
+ # [create_association(attributes = {})]
+ # Returns a new object of the associated type that has been instantiated
+ # with +attributes+, linked to this object through a foreign key, and that
+ # has already been saved (if it passed the validation).
+ # [create_association!(attributes = {})]
+ # Does the same as <tt>create_association</tt>, but raises ActiveRecord::RecordInvalid
+ # if the record is invalid.
+ # [reload_association]
+ # Returns the associated object, forcing a database read.
+ #
+ # === Example
+ #
+ # An Account class declares <tt>has_one :beneficiary</tt>, which will add:
+ # * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.where(account_id: id).first</tt>)
+ # * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>)
+ # * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new(account_id: id)</tt>)
+ # * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new(account_id: id); b.save; b</tt>)
+ # * <tt>Account#create_beneficiary!</tt> (similar to <tt>b = Beneficiary.new(account_id: id); b.save!; b</tt>)
+ # * <tt>Account#reload_beneficiary</tt>
+ #
+ # === Scopes
+ #
+ # You can pass a second argument +scope+ as a callable (i.e. proc or
+ # lambda) to retrieve a specific record or customize the generated query
+ # when you access the associated object.
+ #
+ # Scope examples:
+ # has_one :author, -> { where(comment_id: 1) }
+ # has_one :employer, -> { joins(:company) }
+ # has_one :latest_post, ->(blog) { where("created_at > ?", blog.enabled_at) }
+ #
+ # === Options
+ #
+ # The declaration can also include an +options+ hash to specialize the behavior of the association.
+ #
+ # Options are:
+ # [:class_name]
+ # Specify the class name of the association. Use it only if that name can't be inferred
+ # from the association name. So <tt>has_one :manager</tt> will by default be linked to the Manager class, but
+ # if the real class name is Person, you'll have to specify it with this option.
+ # [:dependent]
+ # Controls what happens to the associated object when
+ # its owner is destroyed:
+ #
+ # * <tt>:destroy</tt> causes the associated object to also be destroyed
+ # * <tt>:delete</tt> causes the associated object to be deleted directly from the database (so callbacks will not execute)
+ # * <tt>:nullify</tt> causes the foreign key to be set to +NULL+. Callbacks are not executed.
+ # * <tt>:restrict_with_exception</tt> causes an <tt>ActiveRecord::DeleteRestrictionError</tt> exception to be raised if there is an associated record
+ # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there is an associated object
+ #
+ # Note that <tt>:dependent</tt> option is ignored when using <tt>:through</tt> option.
+ # [:foreign_key]
+ # Specify the foreign key used for the association. By default this is guessed to be the name
+ # of this class in lower-case and "_id" suffixed. So a Person class that makes a #has_one association
+ # will use "person_id" as the default <tt>:foreign_key</tt>.
+ #
+ # If you are going to modify the association (rather than just read from it), then it is
+ # a good idea to set the <tt>:inverse_of</tt> option.
+ # [:foreign_type]
+ # Specify the column used to store the associated object's type, if this is a polymorphic
+ # association. By default this is guessed to be the name of the polymorphic association
+ # specified on "as" option with a "_type" suffix. So a class that defines a
+ # <tt>has_one :tag, as: :taggable</tt> association will use "taggable_type" as the
+ # default <tt>:foreign_type</tt>.
+ # [:primary_key]
+ # Specify the method that returns the primary key used for the association. By default this is +id+.
+ # [:as]
+ # Specifies a polymorphic interface (See #belongs_to).
+ # [:through]
+ # Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt>,
+ # <tt>:primary_key</tt>, and <tt>:foreign_key</tt> are ignored, as the association uses the
+ # source reflection. You can only use a <tt>:through</tt> query through a #has_one
+ # or #belongs_to association on the join model.
+ #
+ # If you are going to modify the association (rather than just read from it), then it is
+ # a good idea to set the <tt>:inverse_of</tt> option.
+ # [:source]
+ # Specifies the source association name used by #has_one <tt>:through</tt> queries.
+ # Only use it if the name cannot be inferred from the association.
+ # <tt>has_one :favorite, through: :favorites</tt> will look for a
+ # <tt>:favorite</tt> on Favorite, unless a <tt>:source</tt> is given.
+ # [:source_type]
+ # Specifies type of the source association used by #has_one <tt>:through</tt> queries where the source
+ # association is a polymorphic #belongs_to.
+ # [:validate]
+ # When set to +true+, validates new objects added to association when saving the parent object. +false+ by default.
+ # If you want to ensure associated objects are revalidated on every update, use +validates_associated+.
+ # [:autosave]
+ # If true, always save the associated object or destroy it if marked for destruction,
+ # when saving the parent object. If false, never save or destroy the associated object.
+ # By default, only save the associated object if it's a new record.
+ #
+ # Note that NestedAttributes::ClassMethods#accepts_nested_attributes_for sets
+ # <tt>:autosave</tt> to <tt>true</tt>.
+ # [:inverse_of]
+ # Specifies the name of the #belongs_to association on the associated object
+ # that is the inverse of this #has_one association.
+ # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
+ # [:required]
+ # When set to +true+, the association will also have its presence validated.
+ # This will validate the association itself, not the id. You can use
+ # +:inverse_of+ to avoid an extra query during validation.
+ #
+ # Option examples:
+ # has_one :credit_card, dependent: :destroy # destroys the associated credit card
+ # has_one :credit_card, dependent: :nullify # updates the associated records foreign
+ # # key value to NULL rather than destroying it
+ # has_one :last_comment, -> { order('posted_on') }, class_name: "Comment"
+ # has_one :project_manager, -> { where(role: 'project_manager') }, class_name: "Person"
+ # has_one :attachment, as: :attachable
+ # has_one :boss, -> { readonly }
+ # has_one :club, through: :membership
+ # has_one :primary_address, -> { where(primary: true) }, through: :addressables, source: :addressable
+ # has_one :credit_card, required: true
+ def has_one(name, scope = nil, **options)
+ reflection = Builder::HasOne.build(self, name, scope, options)
+ Reflection.add_reflection self, name, reflection
+ end
- builder = Builder::HasAndBelongsToMany.new name, self, options
+ # Specifies a one-to-one association with another class. This method should only be used
+ # if this class contains the foreign key. If the other class contains the foreign key,
+ # then you should use #has_one instead. See also ActiveRecord::Associations::ClassMethods's overview
+ # on when to use #has_one and when to use #belongs_to.
+ #
+ # Methods will be added for retrieval and query for a single associated object, for which
+ # this object holds an id:
+ #
+ # +association+ is a placeholder for the symbol passed as the +name+ argument, so
+ # <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>.
+ #
+ # [association]
+ # Returns the associated object. +nil+ is returned if none is found.
+ # [association=(associate)]
+ # Assigns the associate object, extracts the primary key, and sets it as the foreign key.
+ # No modification or deletion of existing records takes place.
+ # [build_association(attributes = {})]
+ # Returns a new object of the associated type that has been instantiated
+ # with +attributes+ and linked to this object through a foreign key, but has not yet been saved.
+ # [create_association(attributes = {})]
+ # Returns a new object of the associated type that has been instantiated
+ # with +attributes+, linked to this object through a foreign key, and that
+ # has already been saved (if it passed the validation).
+ # [create_association!(attributes = {})]
+ # Does the same as <tt>create_association</tt>, but raises ActiveRecord::RecordInvalid
+ # if the record is invalid.
+ # [reload_association]
+ # Returns the associated object, forcing a database read.
+ #
+ # === Example
+ #
+ # A Post class declares <tt>belongs_to :author</tt>, which will add:
+ # * <tt>Post#author</tt> (similar to <tt>Author.find(author_id)</tt>)
+ # * <tt>Post#author=(author)</tt> (similar to <tt>post.author_id = author.id</tt>)
+ # * <tt>Post#build_author</tt> (similar to <tt>post.author = Author.new</tt>)
+ # * <tt>Post#create_author</tt> (similar to <tt>post.author = Author.new; post.author.save; post.author</tt>)
+ # * <tt>Post#create_author!</tt> (similar to <tt>post.author = Author.new; post.author.save!; post.author</tt>)
+ # * <tt>Post#reload_author</tt>
+ # The declaration can also include an +options+ hash to specialize the behavior of the association.
+ #
+ # === Scopes
+ #
+ # You can pass a second argument +scope+ as a callable (i.e. proc or
+ # lambda) to retrieve a specific record or customize the generated query
+ # when you access the associated object.
+ #
+ # Scope examples:
+ # belongs_to :firm, -> { where(id: 2) }
+ # belongs_to :user, -> { joins(:friends) }
+ # belongs_to :level, ->(game) { where("game_level > ?", game.current_level) }
+ #
+ # === Options
+ #
+ # [:class_name]
+ # Specify the class name of the association. Use it only if that name can't be inferred
+ # from the association name. So <tt>belongs_to :author</tt> will by default be linked to the Author class, but
+ # if the real class name is Person, you'll have to specify it with this option.
+ # [:foreign_key]
+ # Specify the foreign key used for the association. By default this is guessed to be the name
+ # of the association with an "_id" suffix. So a class that defines a <tt>belongs_to :person</tt>
+ # association will use "person_id" as the default <tt>:foreign_key</tt>. Similarly,
+ # <tt>belongs_to :favorite_person, class_name: "Person"</tt> will use a foreign key
+ # of "favorite_person_id".
+ #
+ # If you are going to modify the association (rather than just read from it), then it is
+ # a good idea to set the <tt>:inverse_of</tt> option.
+ # [:foreign_type]
+ # Specify the column used to store the associated object's type, if this is a polymorphic
+ # association. By default this is guessed to be the name of the association with a "_type"
+ # suffix. So a class that defines a <tt>belongs_to :taggable, polymorphic: true</tt>
+ # association will use "taggable_type" as the default <tt>:foreign_type</tt>.
+ # [:primary_key]
+ # Specify the method that returns the primary key of associated object used for the association.
+ # By default this is +id+.
+ # [:dependent]
+ # If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to
+ # <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method.
+ # This option should not be specified when #belongs_to is used in conjunction with
+ # a #has_many relationship on another class because of the potential to leave
+ # orphaned records behind.
+ # [:counter_cache]
+ # Caches the number of belonging objects on the associate class through the use of CounterCache::ClassMethods#increment_counter
+ # and CounterCache::ClassMethods#decrement_counter. The counter cache is incremented when an object of this
+ # class is created and decremented when it's destroyed. This requires that a column
+ # named <tt>#{table_name}_count</tt> (such as +comments_count+ for a belonging Comment class)
+ # is used on the associate class (such as a Post class) - that is the migration for
+ # <tt>#{table_name}_count</tt> is created on the associate class (such that <tt>Post.comments_count</tt> will
+ # return the count cached, see note below). You can also specify a custom counter
+ # cache column by providing a column name instead of a +true+/+false+ value to this
+ # option (e.g., <tt>counter_cache: :my_custom_counter</tt>.)
+ # Note: Specifying a counter cache will add it to that model's list of readonly attributes
+ # using +attr_readonly+.
+ # [:polymorphic]
+ # Specify this association is a polymorphic association by passing +true+.
+ # Note: If you've enabled the counter cache, then you may want to add the counter cache attribute
+ # to the +attr_readonly+ list in the associated classes (e.g. <tt>class Post; attr_readonly :comments_count; end</tt>).
+ # [:validate]
+ # When set to +true+, validates new objects added to association when saving the parent object. +false+ by default.
+ # If you want to ensure associated objects are revalidated on every update, use +validates_associated+.
+ # [:autosave]
+ # If true, always save the associated object or destroy it if marked for destruction, when
+ # saving the parent object.
+ # If false, never save or destroy the associated object.
+ # By default, only save the associated object if it's a new record.
+ #
+ # Note that NestedAttributes::ClassMethods#accepts_nested_attributes_for
+ # sets <tt>:autosave</tt> to <tt>true</tt>.
+ # [:touch]
+ # If true, the associated object will be touched (the updated_at/on attributes set to current time)
+ # when this record is either saved or destroyed. If you specify a symbol, that attribute
+ # will be updated with the current time in addition to the updated_at/on attribute.
+ # Please note that with touching no validation is performed and only the +after_touch+,
+ # +after_commit+ and +after_rollback+ callbacks are executed.
+ # [:inverse_of]
+ # Specifies the name of the #has_one or #has_many association on the associated
+ # object that is the inverse of this #belongs_to association.
+ # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
+ # [:optional]
+ # When set to +true+, the association will not have its presence validated.
+ # [:required]
+ # When set to +true+, the association will also have its presence validated.
+ # This will validate the association itself, not the id. You can use
+ # +: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"
+ # belongs_to :person, primary_key: "name", foreign_key: "person_name"
+ # belongs_to :author, class_name: "Person", foreign_key: "author_id"
+ # belongs_to :valid_coupon, ->(o) { where "discounts > ?", o.payments_count },
+ # class_name: "Coupon", foreign_key: "coupon_id"
+ # belongs_to :attachable, polymorphic: true
+ # belongs_to :project, -> { readonly }
+ # belongs_to :post, counter_cache: true
+ # 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
+ end
- join_model = builder.through_model
+ # Specifies a many-to-many relationship with another class. This associates two classes via an
+ # intermediate join table. Unless the join table is explicitly specified as an option, it is
+ # guessed using the lexical order of the class names. So a join between Developer and Project
+ # will give the default join table name of "developers_projects" because "D" precedes "P" alphabetically.
+ # Note that this precedence is calculated using the <tt><</tt> operator for String. This
+ # means that if the strings are of different lengths, and the strings are equal when compared
+ # up to the shortest length, then the longer string is considered of higher
+ # lexical precedence than the shorter one. For example, one would expect the tables "paper_boxes" and "papers"
+ # to generate a join table name of "papers_paper_boxes" because of the length of the name "paper_boxes",
+ # but it in fact generates a join table name of "paper_boxes_papers". Be aware of this caveat, and use the
+ # custom <tt>:join_table</tt> option if you need to.
+ # If your tables share a common prefix, it will only appear once at the beginning. For example,
+ # the tables "catalog_categories" and "catalog_products" generate a join table name of "catalog_categories_products".
+ #
+ # The join table should not have a primary key or a model associated with it. You must manually generate the
+ # join table with a migration such as this:
+ #
+ # class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration[5.0]
+ # def change
+ # create_join_table :developers, :projects
+ # end
+ # end
+ #
+ # It's also a good idea to add indexes to each of those columns to speed up the joins process.
+ # However, in MySQL it is advised to add a compound index for both of the columns as MySQL only
+ # uses one index per table during the lookup.
+ #
+ # Adds the following methods for retrieval and query:
+ #
+ # +collection+ is a placeholder for the symbol passed as the +name+ argument, so
+ # <tt>has_and_belongs_to_many :categories</tt> would add among others <tt>categories.empty?</tt>.
+ #
+ # [collection]
+ # Returns a Relation of all the associated objects.
+ # An empty Relation is returned if none are found.
+ # [collection<<(object, ...)]
+ # Adds one or more objects to the collection by creating associations in the join table
+ # (<tt>collection.push</tt> and <tt>collection.concat</tt> are aliases to this method).
+ # Note that this operation instantly fires update SQL without waiting for the save or update call on the
+ # parent object, unless the parent object is a new record.
+ # [collection.delete(object, ...)]
+ # Removes one or more objects from the collection by removing their associations from the join table.
+ # This does not destroy the objects.
+ # [collection.destroy(object, ...)]
+ # Removes one or more objects from the collection by running destroy on each association in the join table, overriding any dependent option.
+ # This does not destroy the objects.
+ # [collection=objects]
+ # Replaces the collection's content by deleting and adding objects as appropriate.
+ # [collection_singular_ids]
+ # Returns an array of the associated objects' ids.
+ # [collection_singular_ids=ids]
+ # Replace the collection by the objects identified by the primary keys in +ids+.
+ # [collection.clear]
+ # Removes every object from the collection. This does not destroy the objects.
+ # [collection.empty?]
+ # Returns +true+ if there are no associated objects.
+ # [collection.size]
+ # Returns the number of associated objects.
+ # [collection.find(id)]
+ # Finds an associated object responding to the +id+ and that
+ # meets the condition that it has to be associated with this object.
+ # Uses the same rules as ActiveRecord::FinderMethods#find.
+ # [collection.exists?(...)]
+ # Checks whether an associated object with the given conditions exists.
+ # Uses the same rules as ActiveRecord::FinderMethods#exists?.
+ # [collection.build(attributes = {})]
+ # Returns a new object of the collection type that has been instantiated
+ # with +attributes+ and linked to this object through the join table, but has not yet been saved.
+ # [collection.create(attributes = {})]
+ # Returns a new object of the collection type that has been instantiated
+ # with +attributes+, linked to this object through the join table, and that has already been
+ # saved (if it passed the validation).
+ # [collection.reload]
+ # Returns a Relation of all of the associated objects, forcing a database read.
+ # An empty Relation is returned if none are found.
+ #
+ # === Example
+ #
+ # A Developer class declares <tt>has_and_belongs_to_many :projects</tt>, which will add:
+ # * <tt>Developer#projects</tt>
+ # * <tt>Developer#projects<<</tt>
+ # * <tt>Developer#projects.delete</tt>
+ # * <tt>Developer#projects.destroy</tt>
+ # * <tt>Developer#projects=</tt>
+ # * <tt>Developer#project_ids</tt>
+ # * <tt>Developer#project_ids=</tt>
+ # * <tt>Developer#projects.clear</tt>
+ # * <tt>Developer#projects.empty?</tt>
+ # * <tt>Developer#projects.size</tt>
+ # * <tt>Developer#projects.find(id)</tt>
+ # * <tt>Developer#projects.exists?(...)</tt>
+ # * <tt>Developer#projects.build</tt> (similar to <tt>Project.new(developer_id: id)</tt>)
+ # * <tt>Developer#projects.create</tt> (similar to <tt>c = Project.new(developer_id: id); c.save; c</tt>)
+ # * <tt>Developer#projects.reload</tt>
+ # The declaration may include an +options+ hash to specialize the behavior of the association.
+ #
+ # === Scopes
+ #
+ # You can pass a second argument +scope+ as a callable (i.e. proc or
+ # lambda) to retrieve a specific set of records or customize the generated
+ # query when you access the associated collection.
+ #
+ # Scope examples:
+ # has_and_belongs_to_many :projects, -> { includes(:milestones, :manager) }
+ # has_and_belongs_to_many :categories, ->(post) {
+ # where("default_category = ?", post.default_category)
+ # }
+ #
+ # === Extensions
+ #
+ # The +extension+ argument allows you to pass a block into a
+ # has_and_belongs_to_many association. This is useful for adding new
+ # finders, creators and other factory-type methods to be used as part of
+ # the association.
+ #
+ # Extension examples:
+ # has_and_belongs_to_many :contractors do
+ # def find_or_create_by_name(name)
+ # first_name, last_name = name.split(" ", 2)
+ # find_or_create_by(first_name: first_name, last_name: last_name)
+ # end
+ # end
+ #
+ # === Options
+ #
+ # [:class_name]
+ # Specify the class name of the association. Use it only if that name can't be inferred
+ # from the association name. So <tt>has_and_belongs_to_many :projects</tt> will by default be linked to the
+ # Project class, but if the real class name is SuperProject, you'll have to specify it with this option.
+ # [:join_table]
+ # Specify the name of the join table if the default based on lexical order isn't what you want.
+ # <b>WARNING:</b> If you're overwriting the table name of either class, the +table_name+ method
+ # MUST be declared underneath any #has_and_belongs_to_many declaration in order to work.
+ # [:foreign_key]
+ # Specify the foreign key used for the association. By default this is guessed to be the name
+ # of this class in lower-case and "_id" suffixed. So a Person class that makes
+ # a #has_and_belongs_to_many association to Project will use "person_id" as the
+ # default <tt>:foreign_key</tt>.
+ #
+ # If you are going to modify the association (rather than just read from it), then it is
+ # a good idea to set the <tt>:inverse_of</tt> option.
+ # [:association_foreign_key]
+ # Specify the foreign key used for the association on the receiving side of the association.
+ # By default this is guessed to be the name of the associated class in lower-case and "_id" suffixed.
+ # So if a Person class makes a #has_and_belongs_to_many association to Project,
+ # the association will use "project_id" as the default <tt>:association_foreign_key</tt>.
+ # [:validate]
+ # When set to +true+, validates new objects added to association when saving the parent object. +true+ by default.
+ # If you want to ensure associated objects are revalidated on every update, use +validates_associated+.
+ # [:autosave]
+ # If true, always save the associated objects or destroy them if marked for destruction, when
+ # saving the parent object.
+ # If false, never save or destroy the associated objects.
+ # By default, only save associated objects that are new records.
+ #
+ # Note that NestedAttributes::ClassMethods#accepts_nested_attributes_for sets
+ # <tt>:autosave</tt> to <tt>true</tt>.
+ #
+ # Option examples:
+ # has_and_belongs_to_many :projects
+ # has_and_belongs_to_many :projects, -> { includes(:milestones, :manager) }
+ # has_and_belongs_to_many :nations, class_name: "Country"
+ # has_and_belongs_to_many :categories, join_table: "prods_cats"
+ # has_and_belongs_to_many :categories, -> { readonly }
+ def has_and_belongs_to_many(name, scope = nil, **options, &extension)
+ habtm_reflection = ActiveRecord::Reflection::HasAndBelongsToManyReflection.new(name, scope, options, self)
- const_set join_model.name, join_model
- private_constant join_model.name
+ builder = Builder::HasAndBelongsToMany.new name, self, options
- middle_reflection = builder.middle_reflection join_model
+ join_model = builder.through_model
- Builder::HasMany.define_callbacks self, middle_reflection
- Reflection.add_reflection self, middle_reflection.name, middle_reflection
- middle_reflection.parent_reflection = habtm_reflection
+ const_set join_model.name, join_model
+ private_constant join_model.name
- include Module.new {
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
- def destroy_associations
- association(:#{middle_reflection.name}).delete_all(:delete_all)
- association(:#{name}).reset
- super
- end
- RUBY
- }
+ middle_reflection = builder.middle_reflection join_model
- hm_options = {}
- hm_options[:through] = middle_reflection.name
- hm_options[:source] = join_model.right_reflection.name
+ Builder::HasMany.define_callbacks self, middle_reflection
+ Reflection.add_reflection self, middle_reflection.name, middle_reflection
+ middle_reflection.parent_reflection = habtm_reflection
- [:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate, :join_table, :class_name, :extend].each do |k|
- hm_options[k] = options[k] if options.key? k
- end
+ include Module.new {
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def destroy_associations
+ association(:#{middle_reflection.name}).delete_all(:delete_all)
+ association(:#{name}).reset
+ super
+ end
+ RUBY
+ }
- has_many name, scope, hm_options, &extension
- self._reflections[name.to_s].parent_reflection = habtm_reflection
+ hm_options = {}
+ hm_options[:through] = middle_reflection.name
+ hm_options[:source] = join_model.right_reflection.name
+
+ [:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate, :join_table, :class_name, :extend].each do |k|
+ hm_options[k] = options[k] if options.key? k
+ end
+
+ has_many name, scope, hm_options, &extension
+ _reflections[name.to_s].parent_reflection = habtm_reflection
+ end
end
- end
end
end
diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb
index 2b7e4f28c5..272eede824 100644
--- a/activerecord/lib/active_record/associations/alias_tracker.rb
+++ b/activerecord/lib/active_record/associations/alias_tracker.rb
@@ -1,52 +1,41 @@
-require 'active_support/core_ext/string/conversions'
+# frozen_string_literal: true
+
+require "active_support/core_ext/string/conversions"
module ActiveRecord
module Associations
- # Keeps track of table aliases for ActiveRecord::Associations::ClassMethods::JoinDependency and
- # ActiveRecord::Associations::ThroughAssociationScope
+ # Keeps track of table aliases for ActiveRecord::Associations::JoinDependency
class AliasTracker # :nodoc:
- attr_reader :aliases
-
- def self.create(connection, initial_table, type_caster)
- aliases = Hash.new(0)
- aliases[initial_table] = 1
- new connection, aliases, type_caster
- end
-
- def self.create_with_joins(connection, initial_table, joins, type_caster)
+ def self.create(connection, initial_table, joins)
if joins.empty?
- create(connection, initial_table, type_caster)
+ aliases = Hash.new(0)
else
aliases = Hash.new { |h, k|
h[k] = initial_count_for(connection, k, joins)
}
- aliases[initial_table] = 1
- new connection, aliases, type_caster
end
+ aliases[initial_table] = 1
+ new(connection, aliases)
end
def self.initial_count_for(connection, name, table_joins)
- # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase
- quoted_name = connection.quote_table_name(name).downcase
+ quoted_name = nil
counts = table_joins.map do |join|
if join.is_a?(Arel::Nodes::StringJoin)
+ # quoted_name should be case ignored as some database adapters (Oracle) return quoted name in uppercase
+ quoted_name ||= connection.quote_table_name(name)
+
# Table names + table aliases
- join.left.downcase.scan(
- /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/
+ join.left.scan(
+ /JOIN(?:\s+\w+)?\s+(?:\S+\s+)?(?:#{quoted_name}|#{name})\sON/i
).size
- elsif join.respond_to? :left
- join.left.table_name == name ? 1 : 0
+ elsif join.is_a?(Arel::Nodes::Join)
+ join.left.name == name ? 1 : 0
+ elsif join.is_a?(Hash)
+ join[name]
else
- # this branch is reached by two tests:
- #
- # activerecord/test/cases/associations/cascaded_eager_loading_test.rb:37
- # with :posts
- #
- # activerecord/test/cases/associations/eager_test.rb:1133
- # with :comments
- #
- 0
+ raise ArgumentError, "joins list should be initialized by list of Arel::Nodes::Join"
end
end
@@ -54,17 +43,16 @@ module ActiveRecord
end
# table_joins is an array of arel joins which might conflict with the aliases we assign here
- def initialize(connection, aliases, type_caster)
+ def initialize(connection, aliases)
@aliases = aliases
@connection = connection
- @type_caster = type_caster
end
- def aliased_table_for(table_name, aliased_name)
+ def aliased_table_for(table_name, aliased_name, type_caster)
if aliases[table_name].zero?
# If it's zero, we can have our table_name
aliases[table_name] = 1
- Arel::Table.new(table_name, type_caster: @type_caster)
+ Arel::Table.new(table_name, type_caster: type_caster)
else
# Otherwise, we need to use an alias
aliased_name = @connection.table_alias_for(aliased_name)
@@ -77,10 +65,12 @@ module ActiveRecord
else
aliased_name
end
- Arel::Table.new(table_name, type_caster: @type_caster).alias(table_alias)
+ Arel::Table.new(table_name, type_caster: type_caster).alias(table_alias)
end
end
+ attr_reader :aliases
+
private
def truncate(name)
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb
index 7c729676a7..bf4942aac8 100644
--- a/activerecord/lib/active_record/associations/association.rb
+++ b/activerecord/lib/active_record/associations/association.rb
@@ -1,4 +1,6 @@
-require 'active_support/core_ext/array/wrap'
+# frozen_string_literal: true
+
+require "active_support/core_ext/array/wrap"
module ActiveRecord
module Associations
@@ -17,9 +19,8 @@ module ActiveRecord
# HasManyThroughAssociation + ThroughAssociation
class Association #:nodoc:
attr_reader :owner, :target, :reflection
- attr_accessor :inversed
- delegate :options, :to => :reflection
+ delegate :options, to: :reflection
def initialize(owner, reflection)
reflection.check_validity!
@@ -30,14 +31,6 @@ module ActiveRecord
reset_scope
end
- # Returns the name of the table of the associated class:
- #
- # post.comments.aliased_table_name # => "comments"
- #
- def aliased_table_name
- klass.table_name
- end
-
# Resets the \loaded flag to +false+ and sets the \target to +nil+.
def reset
@loaded = false
@@ -47,7 +40,9 @@ module ActiveRecord
end
# Reloads the \target and returns +self+ on success.
- def reload
+ # The QueryCache is cleared if +force+ is true.
+ def reload(force = false)
+ klass.connection.clear_query_cache if force && klass
reset
reset_scope
load_target
@@ -73,7 +68,7 @@ module ActiveRecord
#
# Note that if the target has not been loaded, it is not considered stale.
def stale_target?
- !inversed && loaded? && @stale_state != stale_state
+ !@inversed && loaded? && @stale_state != stale_state
end
# Sets the target of this association to <tt>\target</tt>, and the \loaded flag to +true+.
@@ -83,19 +78,7 @@ module ActiveRecord
end
def scope
- target_scope.merge(association_scope)
- end
-
- # The scope for this association.
- #
- # Note that the association_scope is merged into the target_scope only when the
- # scope method is called. This is because at that point the call may be surrounded
- # by scope.scoping { ... } or with_scope { ... } etc, which affects the scope which
- # actually gets built.
- def association_scope
- if klass
- @association_scope ||= AssociationScope.scope(self, klass.connection)
- end
+ target_scope.merge!(association_scope)
end
def reset_scope
@@ -104,24 +87,46 @@ module ActiveRecord
# Set the inverse association, if possible
def set_inverse_instance(record)
- if invertible_for?(record)
- inverse = record.association(inverse_reflection_for(record).name)
- inverse.target = owner
- inverse.inversed = true
+ if inverse = inverse_association_for(record)
+ inverse.inversed_from(owner)
+ end
+ record
+ end
+
+ def set_inverse_instance_from_queries(record)
+ if inverse = inverse_association_for(record)
+ inverse.inversed_from_queries(owner)
end
record
end
+ # Remove the inverse association, if possible
+ def remove_inverse_instance(record)
+ if inverse = inverse_association_for(record)
+ inverse.inversed_from(nil)
+ end
+ end
+
+ def inversed_from(record)
+ self.target = record
+ @inversed = !!record
+ end
+ alias :inversed_from_queries :inversed_from
+
# Returns the class of the target. belongs_to polymorphic overrides this to look at the
# polymorphic_type field on the owner.
def klass
reflection.klass
end
- # Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the
- # through association's scope)
- def target_scope
- AssociationRelation.create(klass, klass.arel_table, klass.predicate_builder, self).merge!(klass.all)
+ def extensions
+ extensions = klass.default_extensions | reflection.extensions
+
+ if reflection.scope
+ extensions |= reflection.scope_for(klass.unscoped, owner).extensions
+ end
+
+ extensions
end
# Loads the \target if needed and returns it.
@@ -143,17 +148,9 @@ module ActiveRecord
reset
end
- def interpolate(sql, record = nil)
- if sql.respond_to?(:to_proc)
- owner.instance_exec(record, &sql)
- else
- sql
- end
- end
-
- # We can't dump @reflection since it contains the scope proc
+ # We can't dump @reflection and @through_reflection since it contains the scope proc
def marshal_dump
- ivars = (instance_variables - [:@reflection]).map { |name| [name, instance_variable_get(name)] }
+ ivars = (instance_variables - [:@reflection, :@through_reflection]).map { |name| [name, instance_variable_get(name)] }
[@reflection.name, ivars]
end
@@ -163,14 +160,46 @@ module ActiveRecord
@reflection = @owner.class._reflect_on_association(reflection_name)
end
- def initialize_attributes(record) #:nodoc:
+ def initialize_attributes(record, except_from_scope_attributes = nil) #:nodoc:
+ except_from_scope_attributes ||= {}
skip_assign = [reflection.foreign_key, reflection.type].compact
- attributes = create_scope.except(*(record.changed - skip_assign))
- record.assign_attributes(attributes)
+ assigned_keys = record.changed_attribute_names_to_save
+ assigned_keys += except_from_scope_attributes.keys.map(&:to_s)
+ attributes = scope_for_create.except!(*(assigned_keys - skip_assign))
+ record.send(:_assign_attributes, attributes) if attributes.any?
set_inverse_instance(record)
end
+ def create(attributes = {}, &block)
+ _create_record(attributes, &block)
+ end
+
+ def create!(attributes = {}, &block)
+ _create_record(attributes, true, &block)
+ end
+
private
+ # The scope for this association.
+ #
+ # Note that the association_scope is merged into the target_scope only when the
+ # scope method is called. This is because at that point the call may be surrounded
+ # by scope.scoping { ... } or unscoped { ... } etc, which affects the scope which
+ # actually gets built.
+ def association_scope
+ if klass
+ @association_scope ||= AssociationScope.scope(self)
+ end
+ end
+
+ # Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the
+ # through association's scope)
+ def target_scope
+ AssociationRelation.create(klass, self).merge!(klass.all)
+ end
+
+ def scope_for_create
+ scope.scope_for_create
+ end
def find_target?
!loaded? && (!owner.new_record? || foreign_key_present?) && klass
@@ -182,8 +211,8 @@ module ActiveRecord
if (reflection.has_one? || reflection.collection?) && !options[:through]
attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key]
- if reflection.options[:as]
- attributes[reflection.type] = owner.class.base_class.name
+ if reflection.type
+ attributes[reflection.type] = owner.class.polymorphic_name
end
end
@@ -214,12 +243,19 @@ module ActiveRecord
unless record.is_a?(reflection.klass)
fresh_class = reflection.class_name.safe_constantize
unless fresh_class && record.is_a?(fresh_class)
- message = "#{reflection.class_name}(##{reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})"
+ message = "#{reflection.class_name}(##{reflection.klass.object_id}) expected, "\
+ "got #{record.inspect} which is an instance of #{record.class}(##{record.class.object_id})"
raise ActiveRecord::AssociationTypeMismatch, message
end
end
end
+ def inverse_association_for(record)
+ if invertible_for?(record)
+ record.association(inverse_reflection_for(record).name)
+ end
+ end
+
# Can be redefined by subclasses, notably polymorphic belongs_to
# The record parameter is necessary to support polymorphic inverses as we must check for
# the association in the specific class of the record.
@@ -242,15 +278,24 @@ module ActiveRecord
# so that when stale_state is different from the value stored on the last find_target,
# the target is stale.
#
- # This is only relevant to certain associations, which is why it returns nil by default.
+ # This is only relevant to certain associations, which is why it returns +nil+ by default.
def stale_state
end
def build_record(attributes)
reflection.build_association(attributes) do |record|
- initialize_attributes(record)
+ initialize_attributes(record, attributes)
+ yield(record) if block_given?
end
end
+
+ # Returns true if statement cache should be skipped on the association reader.
+ def skip_statement_cache?(scope)
+ reflection.has_scope? ||
+ scope.eager_loading? ||
+ klass.scope_attributes? ||
+ reflection.source_reflection.active_record.default_scopes.any?
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb
index a140dc239c..0a90a6104a 100644
--- a/activerecord/lib/active_record/associations/association_scope.rb
+++ b/activerecord/lib/active_record/associations/association_scope.rb
@@ -1,8 +1,10 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Associations
class AssociationScope #:nodoc:
- def self.scope(association, connection)
- INSTANCE.scope(association, connection)
+ def self.scope(association)
+ INSTANCE.scope(association)
end
def self.create(&block)
@@ -16,20 +18,15 @@ module ActiveRecord
INSTANCE = create
- def scope(association, connection)
+ def scope(association)
klass = association.klass
reflection = association.reflection
scope = klass.unscoped
owner = association.owner
- alias_tracker = AliasTracker.create connection, association.klass.table_name, klass.type_caster
- 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)
- end
+ chain = get_chain(reflection, association, scope.alias_tracker)
- def join_type
- Arel::Nodes::InnerJoin
+ scope.extending! reflection.extensions
+ add_constraints(scope, owner, chain)
end
def self.get_bind_values(owner, chain)
@@ -38,129 +35,130 @@ module ActiveRecord
binds << last_reflection.join_id_for(owner)
if last_reflection.type
- binds << owner.class.base_class.name
+ binds << owner.class.polymorphic_name
end
chain.each_cons(2).each do |reflection, next_reflection|
if reflection.type
- binds << next_reflection.klass.base_class.name
+ binds << next_reflection.klass.polymorphic_name
end
end
binds
end
- protected
+ private
+ attr_reader :value_transformation
- attr_reader :value_transformation
+ def join(table, constraint)
+ table.create_join(table, table.create_on(constraint))
+ end
- private
- def join(table, constraint)
- table.create_join(table, table.create_on(constraint), join_type)
- end
+ def last_chain_scope(scope, reflection, owner)
+ join_keys = reflection.join_keys
+ key = join_keys.key
+ foreign_key = join_keys.foreign_key
- def last_chain_scope(scope, table, reflection, owner, association_klass)
- join_keys = reflection.join_keys(association_klass)
- key = join_keys.key
- foreign_key = join_keys.foreign_key
+ table = reflection.aliased_table
+ value = transform_value(owner[foreign_key])
+ scope = apply_scope(scope, table, key, value)
- value = transform_value(owner[foreign_key])
- scope = scope.where(table.name => { key => value })
+ if reflection.type
+ polymorphic_type = transform_value(owner.class.polymorphic_name)
+ scope = apply_scope(scope, table, reflection.type, polymorphic_type)
+ end
- if reflection.type
- polymorphic_type = transform_value(owner.class.base_class.name)
- scope = scope.where(table.name => { reflection.type => polymorphic_type })
+ scope
end
- scope
- end
+ def transform_value(value)
+ value_transformation.call(value)
+ end
- def transform_value(value)
- value_transformation.call(value)
- end
+ def next_chain_scope(scope, reflection, next_reflection)
+ join_keys = reflection.join_keys
+ key = join_keys.key
+ foreign_key = join_keys.foreign_key
- def next_chain_scope(scope, table, reflection, association_klass, foreign_table, next_reflection)
- join_keys = reflection.join_keys(association_klass)
- key = join_keys.key
- foreign_key = join_keys.foreign_key
+ table = reflection.aliased_table
+ foreign_table = next_reflection.aliased_table
+ constraint = table[key].eq(foreign_table[foreign_key])
- constraint = table[key].eq(foreign_table[foreign_key])
+ if reflection.type
+ value = transform_value(next_reflection.klass.polymorphic_name)
+ scope = apply_scope(scope, table, reflection.type, value)
+ end
- if reflection.type
- value = transform_value(next_reflection.klass.base_class.name)
- scope = scope.where(table.name => { reflection.type => value })
+ scope.joins!(join(foreign_table, constraint))
end
- scope = scope.joins(join(foreign_table, constraint))
- end
+ class ReflectionProxy < SimpleDelegator # :nodoc:
+ attr_reader :aliased_table
- class ReflectionProxy < SimpleDelegator # :nodoc:
- attr_accessor :next
- attr_reader :alias_name
+ def initialize(reflection, aliased_table)
+ super(reflection)
+ @aliased_table = aliased_table
+ end
- def initialize(reflection, alias_name)
- super(reflection)
- @alias_name = alias_name
+ def all_includes; nil; end
end
- def all_includes; nil; end
- end
-
- def get_chain(reflection, association, tracker)
- name = reflection.name
- runtime_reflection = Reflection::RuntimeReflection.new(reflection, association)
- previous_reflection = runtime_reflection
- reflection.chain.drop(1).each do |refl|
- alias_name = tracker.aliased_table_for(refl.table_name, refl.alias_candidate(name))
- proxy = ReflectionProxy.new(refl, alias_name)
- previous_reflection.next = proxy
- previous_reflection = proxy
+ def get_chain(reflection, association, tracker)
+ name = reflection.name
+ chain = [Reflection::RuntimeReflection.new(reflection, association)]
+ reflection.chain.drop(1).each do |refl|
+ aliased_table = tracker.aliased_table_for(
+ refl.table_name,
+ refl.alias_candidate(name),
+ refl.klass.type_caster
+ )
+ chain << ReflectionProxy.new(refl, aliased_table)
+ end
+ chain
end
- [runtime_reflection, previous_reflection]
- end
-
- def add_constraints(scope, owner, association_klass, 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)
- reflection = chain_head
- loop do
- break unless reflection
- table = reflection.alias_name
+ def add_constraints(scope, owner, chain)
+ scope = last_chain_scope(scope, chain.last, owner)
- 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)
+ chain.each_cons(2) do |reflection, next_reflection|
+ scope = next_chain_scope(scope, reflection, 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)
+ chain_head = chain.first
+ chain.reverse_each do |reflection|
+ # 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, scope_chain_item, owner)
- if scope_chain_item == refl.scope
- scope.merge! item.except(:where, :includes)
- end
+ if scope_chain_item == chain_head.scope
+ scope.merge! item.except(:where, :includes, :unscope, :order)
+ end
- reflection.all_includes do
- scope.includes! item.includes_values
- end
+ reflection.all_includes do
+ scope.includes! item.includes_values
+ end
- scope.where_clause += item.where_clause
- scope.order_values |= item.order_values
- scope.unscope!(*item.unscope_values)
+ scope.unscope!(*item.unscope_values)
+ scope.where_clause += item.where_clause
+ scope.order_values = item.order_values | scope.order_values
+ end
end
- reflection = reflection.next
+ scope
end
- scope
- end
+ def apply_scope(scope, table, key, value)
+ if scope.table == table
+ scope.where!(key => value)
+ else
+ scope.where!(table.name => { key => value })
+ end
+ end
- def eval_scope(klass, scope, owner)
- klass.unscoped.instance_exec(owner, &scope)
- end
+ def eval_scope(reflection, scope, owner)
+ relation = reflection.build_scope(reflection.aliased_table)
+ relation.instance_exec(owner, &scope) || relation
+ end
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 260a0c6a2d..3346725f2d 100644
--- a/activerecord/lib/active_record/associations/belongs_to_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_association.rb
@@ -1,25 +1,28 @@
+# frozen_string_literal: true
+
module ActiveRecord
- # = Active Record Belongs To Association
module Associations
+ # = Active Record Belongs To Association
class BelongsToAssociation < SingularAssociation #:nodoc:
-
def handle_dependency
- target.send(options[:dependent]) if load_target
- end
+ return unless load_target
- def replace(record)
- if record
- raise_on_type_mismatch!(record)
- update_counters(record)
- replace_keys(record)
- set_inverse_instance(record)
- @updated = true
+ case options[:dependent]
+ when :destroy
+ target.destroy
+ raise ActiveRecord::Rollback unless target.destroyed?
else
- decrement_counters
- remove_keys
+ target.send(options[:dependent])
end
+ end
+
+ def inversed_from(record)
+ replace_keys(record)
+ super
+ end
- self.target = record
+ def default(&block)
+ writer(owner.instance_exec(&block)) if reader.nil?
end
def reset
@@ -31,60 +34,74 @@ module ActiveRecord
@updated
end
- def decrement_counters # :nodoc:
- with_cache_name { |name| decrement_counter name }
+ def decrement_counters
+ update_counters(-1)
end
- def increment_counters # :nodoc:
- with_cache_name { |name| increment_counter name }
+ def increment_counters
+ update_counters(1)
end
- private
-
- def find_target?
- !loaded? && foreign_key_present? && klass
+ def decrement_counters_before_last_save
+ if reflection.polymorphic?
+ model_was = owner.attribute_before_last_save(reflection.foreign_type).try(:constantize)
+ else
+ model_was = klass
end
- def with_cache_name
- counter_cache_name = reflection.counter_cache_column
- return unless counter_cache_name && owner.persisted?
- yield counter_cache_name
- end
+ foreign_key_was = owner.attribute_before_last_save(reflection.foreign_key)
- def update_counters(record)
- with_cache_name do |name|
- return unless different_target? record
- record.class.increment_counter(name, record.id)
- decrement_counter name
- end
+ if foreign_key_was && model_was < ActiveRecord::Base
+ update_counters_via_scope(model_was, foreign_key_was, -1)
end
+ end
- def decrement_counter(counter_cache_name)
- if foreign_key_present?
- klass.decrement_counter(counter_cache_name, target_id)
+ def target_changed?
+ owner.saved_change_to_attribute?(reflection.foreign_key)
+ end
+
+ private
+ def replace(record)
+ if record
+ raise_on_type_mismatch!(record)
+ set_inverse_instance(record)
+ @updated = true
end
+
+ replace_keys(record)
+
+ self.target = record
end
- def increment_counter(counter_cache_name)
- if foreign_key_present?
- klass.increment_counter(counter_cache_name, target_id)
+ def update_counters(by)
+ if require_counter_update? && foreign_key_present?
if target && !stale_target?
- target.increment(counter_cache_name)
+ target.increment!(reflection.counter_cache_column, by, touch: reflection.options[:touch])
+ else
+ update_counters_via_scope(klass, owner._read_attribute(reflection.foreign_key), by)
end
end
end
- # Checks whether record is different to the current target, without loading it
- def different_target?(record)
- record.id != owner._read_attribute(reflection.foreign_key)
+ def update_counters_via_scope(klass, foreign_key, by)
+ scope = klass.unscoped.where!(primary_key(klass) => foreign_key)
+ scope.update_counters(reflection.counter_cache_column => by, touch: reflection.options[:touch])
+ end
+
+ def find_target?
+ !loaded? && foreign_key_present? && klass
+ end
+
+ def require_counter_update?
+ reflection.counter_cache_column && owner.persisted?
end
def replace_keys(record)
- owner[reflection.foreign_key] = record._read_attribute(reflection.association_primary_key(record.class))
+ owner[reflection.foreign_key] = record ? record._read_attribute(primary_key(record.class)) : nil
end
- def remove_keys
- owner[reflection.foreign_key] = nil
+ def primary_key(klass)
+ reflection.association_primary_key(klass)
end
def foreign_key_present?
@@ -98,14 +115,6 @@ module ActiveRecord
inverse && inverse.has_one?
end
- def target_id
- if options[:primary_key]
- owner.send(reflection.name).try(:id)
- else
- owner._read_attribute(reflection.foreign_key)
- end
- end
-
def stale_state
result = owner._read_attribute(reflection.foreign_key) { |n| owner.send(:missing_attribute, n, caller) }
result && result.to_s
diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
index b710cf6bdb..9ae452e7a1 100644
--- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
@@ -1,26 +1,22 @@
+# frozen_string_literal: true
+
module ActiveRecord
- # = Active Record Belongs To Polymorphic Association
module Associations
+ # = Active Record Belongs To Polymorphic Association
class BelongsToPolymorphicAssociation < BelongsToAssociation #:nodoc:
def klass
type = owner[reflection.foreign_type]
type.presence && type.constantize
end
- private
+ def target_changed?
+ super || owner.saved_change_to_attribute?(reflection.foreign_type)
+ end
+ private
def replace_keys(record)
super
- owner[reflection.foreign_type] = record.class.base_class.name
- end
-
- def remove_keys
- super
- owner[reflection.foreign_type] = nil
- end
-
- def different_target?(record)
- super || record.class != klass
+ owner[reflection.foreign_type] = record ? record.class.polymorphic_name : nil
end
def inverse_reflection_for(record)
diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb
index ba1b1814d1..7c69cd65ee 100644
--- a/activerecord/lib/active_record/associations/builder/association.rb
+++ b/activerecord/lib/active_record/associations/builder/association.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# This is the parent Association class which defines the variables
# used by all associations.
#
@@ -9,7 +11,7 @@
# - CollectionAssociation
# - HasManyAssociation
-module ActiveRecord::Associations::Builder
+module ActiveRecord::Associations::Builder # :nodoc:
class Association #:nodoc:
class << self
attr_accessor :extensions
@@ -36,11 +38,6 @@ module ActiveRecord::Associations::Builder
def self.create_reflection(model, name, scope, options, extension = nil)
raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol)
- if scope.is_a?(Hash)
- options = scope
- scope = nil
- end
-
validate_options(options)
scope = build_scope(scope, extension)
@@ -107,8 +104,8 @@ module ActiveRecord::Associations::Builder
def self.define_readers(mixin, name)
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
- def #{name}(*args)
- association(:#{name}).reader(*args)
+ def #{name}
+ association(:#{name}).reader
end
CODE
end
diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb
index 97eb007f62..fc00f1e900 100644
--- a/activerecord/lib/active_record/associations/builder/belongs_to.rb
+++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb
@@ -1,11 +1,13 @@
-module ActiveRecord::Associations::Builder
+# frozen_string_literal: true
+
+module ActiveRecord::Associations::Builder # :nodoc:
class BelongsTo < SingularAssociation #:nodoc:
def self.macro
:belongs_to
end
def self.valid_options(options)
- super + [:foreign_type, :polymorphic, :touch, :counter_cache, :optional]
+ super + [:polymorphic, :touch, :counter_cache, :optional, :default]
end
def self.valid_dependent_options
@@ -16,62 +18,40 @@ module ActiveRecord::Associations::Builder
super
add_counter_cache_callbacks(model, reflection) if reflection.options[:counter_cache]
add_touch_callbacks(model, reflection) if reflection.options[:touch]
- end
-
- def self.define_accessors(mixin, reflection)
- super
- add_counter_cache_methods mixin
- end
-
- def self.add_counter_cache_methods(mixin)
- return if mixin.method_defined? :belongs_to_counter_cache_after_update
-
- mixin.class_eval do
- def belongs_to_counter_cache_after_update(reflection)
- foreign_key = reflection.foreign_key
- cache_column = reflection.counter_cache_column
-
- if (@_after_create_counter_called ||= false)
- @_after_create_counter_called = false
- elsif attribute_changed?(foreign_key) && !new_record? && reflection.constructable?
- model = reflection.klass
- foreign_key_was = attribute_was foreign_key
- foreign_key = attribute foreign_key
-
- if foreign_key && model.respond_to?(:increment_counter)
- model.increment_counter(cache_column, foreign_key)
- end
- if foreign_key_was && model.respond_to?(:decrement_counter)
- model.decrement_counter(cache_column, foreign_key_was)
- end
- end
- end
- end
+ add_default_callbacks(model, reflection) if reflection.options[:default]
end
def self.add_counter_cache_callbacks(model, reflection)
cache_column = reflection.counter_cache_column
model.after_update lambda { |record|
- record.belongs_to_counter_cache_after_update(reflection)
+ association = association(reflection.name)
+
+ if association.target_changed?
+ association.increment_counters
+ association.decrement_counters_before_last_save
+ end
}
klass = reflection.class_name.safe_constantize
klass.attr_readonly cache_column if klass && klass.respond_to?(:attr_readonly)
end
- def self.touch_record(o, foreign_key, name, touch, touch_method) # :nodoc:
- old_foreign_id = o.changed_attributes[foreign_key]
+ def self.touch_record(o, changes, foreign_key, name, touch, touch_method) # :nodoc:
+ old_foreign_id = changes[foreign_key] && changes[foreign_key].first
if old_foreign_id
association = o.association(name)
reflection = association.reflection
if reflection.polymorphic?
- klass = o.public_send("#{reflection.foreign_type}_was").constantize
+ foreign_type = reflection.foreign_type
+ klass = changes[foreign_type] && changes[foreign_type].first || o.public_send(foreign_type)
+ klass = klass.constantize
else
klass = association.klass
end
- old_record = klass.find_by(klass.primary_key => old_foreign_id)
+ primary_key = reflection.association_primary_key(klass)
+ old_record = klass.find_by(primary_key => old_foreign_id)
if old_record
if touch != true
@@ -97,19 +77,33 @@ module ActiveRecord::Associations::Builder
n = reflection.name
touch = reflection.options[:touch]
- callback = lambda { |record|
- touch_method = touching_delayed_records? ? :touch : :touch_later
- BelongsTo.touch_record(record, foreign_key, n, touch, touch_method)
- }
+ callback = lambda { |changes_method| lambda { |record|
+ BelongsTo.touch_record(record, record.send(changes_method), foreign_key, n, touch, belongs_to_touch_method)
+ }}
- model.after_save callback, if: :changed?
- model.after_touch callback
- model.after_destroy callback
+ if reflection.counter_cache_column
+ touch_callback = callback.(:saved_changes)
+ update_callback = lambda { |record|
+ instance_exec(record, &touch_callback) unless association(reflection.name).target_changed?
+ }
+ model.after_update update_callback, if: :saved_changes?
+ else
+ model.after_create callback.(:saved_changes), if: :saved_changes?
+ model.after_update callback.(:saved_changes), if: :saved_changes?
+ model.after_destroy callback.(:changes_to_save)
+ end
+
+ model.after_touch 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)
- name = reflection.name
- model.after_destroy lambda { |o| o.association(name).handle_dependency }
+ model.after_destroy lambda { |o| o.association(reflection.name).handle_dependency }
end
def self.define_validations(model, reflection)
diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb
index 2ff67f904d..ff57c40121 100644
--- a/activerecord/lib/active_record/associations/builder/collection_association.rb
+++ b/activerecord/lib/active_record/associations/builder/collection_association.rb
@@ -1,10 +1,9 @@
-# This class is inherited by the has_many and has_many_and_belongs_to_many association classes
+# frozen_string_literal: true
-require 'active_record/associations'
+require "active_record/associations"
-module ActiveRecord::Associations::Builder
+module ActiveRecord::Associations::Builder # :nodoc:
class CollectionAssociation < Association #:nodoc:
-
CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove]
def self.valid_options(options)
@@ -25,7 +24,7 @@ module ActiveRecord::Associations::Builder
if block_given?
extension_module_name = "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension"
extension = Module.new(&Proc.new)
- model.parent.const_set(extension_module_name, extension)
+ model.module_parent.const_set(extension_module_name, extension)
end
end
@@ -70,7 +69,11 @@ module ActiveRecord::Associations::Builder
def self.wrap_scope(scope, mod)
if scope
- proc { |owner| instance_exec(owner, &scope).extending(mod) }
+ if scope.arity > 0
+ proc { |owner| instance_exec(owner, &scope).extending(mod) }
+ else
+ proc { instance_exec(&scope).extending(mod) }
+ end
else
proc { extending(mod) }
end
diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
index ffd9c9d6fc..0140aa15c8 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
@@ -1,38 +1,7 @@
-module ActiveRecord::Associations::Builder
- class HasAndBelongsToMany # :nodoc:
- class JoinTableResolver
- KnownTable = Struct.new :join_table
-
- class KnownClass
- def initialize(lhs_class, rhs_class_name)
- @lhs_class = lhs_class
- @rhs_class_name = rhs_class_name
- @join_table = nil
- end
-
- def join_table
- @join_table ||= [@lhs_class.table_name, klass.table_name].sort.join("\0").gsub(/^(.*[._])(.+)\0\1(.+)/, '\1\2_\3').tr("\0", "_")
- end
-
- private
-
- def klass
- @lhs_class.send(:compute_type, @rhs_class_name)
- end
- end
-
- def self.build(lhs_class, name, options)
- if options[:join_table]
- KnownTable.new options[:join_table].to_s
- else
- class_name = options.fetch(:class_name) {
- name.to_s.camelize.singularize
- }
- KnownClass.new lhs_class, class_name
- end
- end
- end
+# frozen_string_literal: true
+module ActiveRecord::Associations::Builder # :nodoc:
+ class HasAndBelongsToMany # :nodoc:
attr_reader :lhs_model, :association_name, :options
def initialize(association_name, lhs_model, options)
@@ -42,11 +11,9 @@ module ActiveRecord::Associations::Builder
end
def through_model
- habtm = JoinTableResolver.build lhs_model, association_name, options
-
join_model = Class.new(ActiveRecord::Base) {
- class << self;
- attr_accessor :class_resolver
+ class << self
+ attr_accessor :left_model
attr_accessor :name
attr_accessor :table_name_resolver
attr_accessor :left_reflection
@@ -54,29 +21,40 @@ module ActiveRecord::Associations::Builder
end
def self.table_name
- table_name_resolver.join_table
+ # Table name needs to be resolved lazily
+ # because RHS class might not have been loaded
+ @table_name ||= table_name_resolver.call
end
def self.compute_type(class_name)
- class_resolver.compute_type class_name
+ left_model.compute_type class_name
end
def self.add_left_association(name, options)
- belongs_to name, options
+ belongs_to name, required: false, **options
self.left_reflection = _reflect_on_association(name)
end
def self.add_right_association(name, options)
rhs_name = name.to_s.singularize.to_sym
- belongs_to rhs_name, options
+ belongs_to rhs_name, required: false, **options
self.right_reflection = _reflect_on_association(rhs_name)
end
+ def self.retrieve_connection
+ left_model.retrieve_connection
+ end
+
+ private
+
+ def self.suppress_composite_primary_key(pk)
+ pk unless pk.is_a?(Array)
+ end
}
join_model.name = "HABTM_#{association_name.to_s.camelize}"
- join_model.table_name_resolver = habtm
- join_model.class_resolver = lhs_model
+ join_model.table_name_resolver = -> { table_name }
+ join_model.left_model = lhs_model
join_model.add_left_association :left_side, anonymous_class: lhs_model
join_model.add_right_association association_name, belongs_to_options(options)
@@ -85,7 +63,7 @@ module ActiveRecord::Associations::Builder
def middle_reflection(join_model)
middle_name = [lhs_model.name.downcase.pluralize,
- association_name].join('_'.freeze).gsub('::'.freeze, '_'.freeze).to_sym
+ association_name].join("_").gsub("::", "_").to_sym
middle_options = middle_options join_model
HasMany.create_reflection(lhs_model,
@@ -96,29 +74,41 @@ module ActiveRecord::Associations::Builder
private
- def middle_options(join_model)
- middle_options = {}
- middle_options[:class_name] = "#{lhs_model.name}::#{join_model.name}"
- middle_options[:source] = join_model.left_reflection.name
- if options.key? :foreign_key
- middle_options[:foreign_key] = options[:foreign_key]
+ def middle_options(join_model)
+ middle_options = {}
+ middle_options[:class_name] = "#{lhs_model.name}::#{join_model.name}"
+ middle_options[:source] = join_model.left_reflection.name
+ if options.key? :foreign_key
+ middle_options[:foreign_key] = options[:foreign_key]
+ end
+ middle_options
end
- middle_options
- end
-
- def belongs_to_options(options)
- rhs_options = {}
- if options.key? :class_name
- rhs_options[:foreign_key] = options[:class_name].to_s.foreign_key
- rhs_options[:class_name] = options[:class_name]
+ def table_name
+ if options[:join_table]
+ options[:join_table].to_s
+ else
+ class_name = options.fetch(:class_name) {
+ association_name.to_s.camelize.singularize
+ }
+ klass = lhs_model.send(:compute_type, class_name.to_s)
+ [lhs_model.table_name, klass.table_name].sort.join("\0").gsub(/^(.*[._])(.+)\0\1(.+)/, '\1\2_\3').tr("\0", "_")
+ end
end
- if options.key? :association_foreign_key
- rhs_options[:foreign_key] = options[:association_foreign_key]
- end
+ def belongs_to_options(options)
+ rhs_options = {}
- rhs_options
- end
+ if options.key? :class_name
+ rhs_options[:foreign_key] = options[:class_name].to_s.foreign_key
+ rhs_options[:class_name] = options[:class_name]
+ end
+
+ if options.key? :association_foreign_key
+ rhs_options[:foreign_key] = options[:association_foreign_key]
+ end
+
+ rhs_options
+ end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb
index 1c1b47bd56..5b9617bc6d 100644
--- a/activerecord/lib/active_record/associations/builder/has_many.rb
+++ b/activerecord/lib/active_record/associations/builder/has_many.rb
@@ -1,11 +1,13 @@
-module ActiveRecord::Associations::Builder
+# frozen_string_literal: true
+
+module ActiveRecord::Associations::Builder # :nodoc:
class HasMany < CollectionAssociation #:nodoc:
def self.macro
:has_many
end
def self.valid_options(options)
- super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache, :join_table, :foreign_type]
+ super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache, :join_table, :foreign_type, :index_errors]
end
def self.valid_dependent_options
diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb
index a272d3c781..bfb37d6eee 100644
--- a/activerecord/lib/active_record/associations/builder/has_one.rb
+++ b/activerecord/lib/active_record/associations/builder/has_one.rb
@@ -1,11 +1,13 @@
-module ActiveRecord::Associations::Builder
+# frozen_string_literal: true
+
+module ActiveRecord::Associations::Builder # :nodoc:
class HasOne < SingularAssociation #:nodoc:
def self.macro
:has_one
end
def self.valid_options(options)
- valid = super + [:as, :foreign_type]
+ valid = super + [:as]
valid += [:through, :source, :source_type] if options[:through]
valid
end
diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb
index 42542f188e..0a02ef4cc1 100644
--- a/activerecord/lib/active_record/associations/builder/singular_association.rb
+++ b/activerecord/lib/active_record/associations/builder/singular_association.rb
@@ -1,14 +1,25 @@
+# frozen_string_literal: true
+
# This class is inherited by the has_one and belongs_to association classes
-module ActiveRecord::Associations::Builder
+module ActiveRecord::Associations::Builder # :nodoc:
class SingularAssociation < Association #:nodoc:
def self.valid_options(options)
- super + [:dependent, :primary_key, :inverse_of, :required]
+ super + [:foreign_type, :dependent, :primary_key, :inverse_of, :required]
end
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 6caadb4ce8..840d900bbc 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Associations
# = Active Record Association Collection
@@ -10,9 +12,9 @@ module ActiveRecord
# HasManyAssociation => has_many
# HasManyThroughAssociation + ThroughAssociation => has_many :through
#
- # CollectionAssociation class provides common methods to the collections
+ # The CollectionAssociation class provides common methods to the collections
# defined by +has_and_belongs_to_many+, +has_many+ or +has_many+ with
- # +:through association+ option.
+ # the +:through association+ option.
#
# You need to be careful with assumptions regarding the target: The proxy
# does not fetch records from the database until it needs them, but new
@@ -24,22 +26,14 @@ module ActiveRecord
# If you need to work on all current children, new and existing records,
# +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
- 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
+ @proxy ||= CollectionProxy.create(klass, self)
+ @proxy.reset_scope
end
# Implements the writer method, e.g. foo.items= for Foo.has_many :items
@@ -50,92 +44,60 @@ 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)
+ elsif !target.empty?
+ load_target.pluck(reflection.association_primary_key)
else
- column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}"
- scope.pluck(column)
+ @association_ids ||= scope.pluck(reflection.association_primary_key)
end
end
# 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
+ primary_key = reflection.association_primary_key
+ pk_type = klass.type_for_attribute(primary_key)
ids = Array(ids).reject(&:blank?)
ids.map! { |i| pk_type.cast(i) }
- replace(klass.find(ids).index_by(&:id).values_at(*ids))
- end
- def reset
- super
- @target = []
- end
+ records = klass.where(primary_key => ids).index_by do |r|
+ r.public_send(primary_key)
+ end.values_at(*ids).compact
- def select(*fields)
- if block_given?
- load_target.select.each { |e| yield e }
+ if records.size != ids.size
+ found_ids = records.map { |record| record.public_send(primary_key) }
+ not_found_ids = ids - found_ids
+ klass.all.raise_record_not_found_exception!(ids, records.size, ids.size, primary_key, not_found_ids)
else
- scope.select(*fields)
+ replace(records)
end
end
- def find(*args)
- if block_given?
- load_target.find(*args) { |*block_args| yield(*block_args) }
- else
- if options[:inverse_of] && loaded?
- args_flatten = args.flatten
- raise RecordNotFound, "Couldn't find #{scope.klass.name} without an ID" if args_flatten.blank?
- result = find_by_scan(*args)
-
- result_size = Array(result).size
- if !result || result_size != args_flatten.size
- scope.raise_record_not_found_exception!(args_flatten, result_size, args_flatten.size)
- else
- result
- end
- else
- scope.find(*args)
- end
- end
- end
-
- def first(*args)
- first_nth_or_last(:first, *args)
- end
-
- def second(*args)
- first_nth_or_last(:second, *args)
- end
-
- def third(*args)
- first_nth_or_last(:third, *args)
- end
-
- def fourth(*args)
- first_nth_or_last(:fourth, *args)
+ def reset
+ super
+ @target = []
+ @association_ids = nil
end
- def fifth(*args)
- first_nth_or_last(:fifth, *args)
- end
+ def find(*args)
+ if options[:inverse_of] && loaded?
+ args_flatten = args.flatten
+ model = scope.klass
- def forty_two(*args)
- first_nth_or_last(:forty_two, *args)
- end
+ if args_flatten.blank?
+ error_message = "Couldn't find #{model.name} without an ID"
+ raise RecordNotFound.new(error_message, model.name, model.primary_key, args)
+ end
- def last(*args)
- first_nth_or_last(:last, *args)
- end
+ result = find_by_scan(*args)
- def take(n = nil)
- if loaded?
- n ? target.take(n) : target.first
- else
- scope.take(n).tap do |record|
- set_inverse_instance record if record.is_a? ActiveRecord::Base
+ result_size = Array(result).size
+ if !result || result_size != args_flatten.size
+ scope.raise_record_not_found_exception!(args_flatten, result_size, args_flatten.size)
+ else
+ result
end
+ else
+ scope.find(*args)
end
end
@@ -143,20 +105,10 @@ module ActiveRecord
if attributes.is_a?(Array)
attributes.collect { |attr| build(attr, &block) }
else
- add_to_target(build_record(attributes)) do |record|
- yield(record) if block_given?
- end
+ add_to_target(build_record(attributes, &block))
end
end
- def create(attributes = {}, &block)
- _create_record(attributes, &block)
- end
-
- def create!(attributes = {}, &block)
- _create_record(attributes, true, &block)
- end
-
# Add +records+ to this association. Returns +self+ so method calls may
# be chained. Since << flattens its argument list and inserts each record,
# +push+ and +concat+ behave identically.
@@ -204,12 +156,12 @@ module ActiveRecord
end
dependent = if dependent
- dependent
- elsif options[:dependent] == :destroy
- :delete_all
- else
- options[:dependent]
- end
+ dependent
+ elsif options[:dependent] == :destroy
+ :delete_all
+ else
+ options[:dependent]
+ end
delete_or_nullify_all_records(dependent).tap do
reset
@@ -227,28 +179,6 @@ module ActiveRecord
end
end
- # Count all records using SQL. Construct options and pass them with
- # scope to the target class's +count+.
- def count(column_name = nil)
- relation = scope
- if association_scope.distinct_value
- # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
- column_name ||= reflection.klass.primary_key
- relation = relation.distinct
- end
-
- value = relation.count(column_name)
-
- limit = options[:limit]
- offset = options[:offset]
-
- if limit || offset
- [ [value - offset.to_i, 0].max, limit.to_i ].min
- else
- value
- end
- end
-
# Removes +records+ from this association calling +before_remove+ and
# +after_remove+ callbacks.
#
@@ -257,12 +187,7 @@ module ActiveRecord
# are actually removed from the database, that depends precisely on
# +delete_records+. They are in any case removed from the collection.
def delete(*records)
- return if records.empty?
- _options = records.extract_options!
- dependent = _options[:dependent] || options[:dependent]
-
- records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) }
- delete_or_destroy(records, dependent)
+ delete_or_destroy(records, options[:dependent])
end
# Deletes the +records+ and removes them from this association calling
@@ -271,8 +196,6 @@ module ActiveRecord
# Note that this method removes records from the database ignoring the
# +:dependent+ option.
def destroy(*records)
- return if records.empty?
- records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) }
delete_or_destroy(records, :destroy)
end
@@ -288,14 +211,12 @@ module ActiveRecord
# +count_records+, which is a method descendants have to provide.
def size
if !find_target? || loaded?
- if association_scope.distinct_value
- target.uniq.size
- else
- target.size
- end
- elsif !loaded? && !association_scope.group_values.empty?
+ target.size
+ elsif @association_ids
+ @association_ids.size
+ elsif !association_scope.group_values.empty?
load_target.size
- elsif !loaded? && !association_scope.distinct_value && target.is_a?(Array)
+ elsif !association_scope.distinct_value && !target.empty?
unsaved_records = target.select(&:new_record?)
unsaved_records.size + count_records
else
@@ -303,15 +224,6 @@ module ActiveRecord
end
end
- # Returns the size of the collection calling +size+ on the target.
- #
- # If the collection has been already loaded +length+ and +size+ are
- # equivalent. If not and you are going to need the records anyway this
- # method will take one less query. Otherwise +size+ is more efficient.
- def length
- load_target.size
- end
-
# Returns true if the collection is empty.
#
# If the collection has been loaded
@@ -321,43 +233,13 @@ module ActiveRecord
# loaded and you are going to fetch the records anyway it is better to
# check <tt>collection.length.zero?</tt>.
def empty?
- if loaded?
+ if loaded? || @association_ids
size.zero?
else
- @target.blank? && !scope.exists?
- end
- end
-
- # Returns true if the collections is not empty.
- # If block given, loads all records and checks for one or more matches.
- # Otherwise, equivalent to +!collection.empty?+.
- def any?
- if block_given?
- load_target.any? { |*block_args| yield(*block_args) }
- else
- !empty?
+ target.empty? && !scope.exists?
end
end
- # Returns true if the collection has more than 1 record.
- # If block given, loads all records and checks for two or more matches.
- # Otherwise, equivalent to +collection.size > 1+.
- def many?
- if block_given?
- load_target.many? { |*block_args| yield(*block_args) }
- else
- size > 1
- end
- end
-
- def distinct
- seen = {}
- load_target.find_all do |record|
- seen[record.id] = true unless seen.key?(record.id)
- end
- end
- alias uniq distinct
-
# Replace this collection with +other_array+. This will perform a diff
# and delete/add only records that have changed.
def replace(other_array)
@@ -404,25 +286,9 @@ module ActiveRecord
replace_on_target(record, index, skip_callbacks, &block)
end
- def replace_on_target(record, index, skip_callbacks)
- callback(:before_add, record) unless skip_callbacks
- yield(record) if block_given?
-
- if index
- @target[index] = record
- else
- @target << record
- 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
@@ -430,31 +296,28 @@ module ActiveRecord
owner.new_record? && !foreign_key_present?
end
+ def find_from_target?
+ loaded? ||
+ owner.new_record? ||
+ target.any? { |record| record.new_record? || record.changed? }
+ end
+
private
- def get_records
- if reflection.scope_chain.any?(&:any?) ||
- scope.eager_loading? ||
- klass.scope_attributes?
- return scope.to_a
- end
+ def find_target
+ scope = self.scope
+ return scope.to_a if skip_statement_cache?(scope)
- conn = klass.connection
- sc = reflection.association_scope_cache(conn, owner) do
- StatementCache.create(conn) { |params|
+ conn = klass.connection
+ sc = reflection.association_scope_cache(conn, owner) do |params|
as = AssociationScope.create { params.bind }
- target_scope.merge as.scope(self, conn)
- }
- end
-
- binds = AssociationScope.get_bind_values(owner, reflection.chain)
- sc.execute binds, klass, klass.connection
- end
+ target_scope.merge!(as.scope(self))
+ end
- def find_target
- records = get_records
- records.each { |record| set_inverse_instance(record) }
- records
+ binds = AssociationScope.get_bind_values(owner, reflection.chain)
+ sc.execute(binds, conn) do |record|
+ set_inverse_instance(record)
+ end
end
# We have some records loaded from the database (persisted) and some that are
@@ -474,7 +337,7 @@ module ActiveRecord
persisted.map! do |record|
if mem_record = memory.delete(record)
- ((record.attribute_names & mem_record.attribute_names) - mem_record.changes.keys).each do |name|
+ ((record.attribute_names & mem_record.attribute_names) - mem_record.changed_attribute_names_to_save).each do |name|
mem_record[name] = record[name]
end
@@ -495,25 +358,33 @@ module ActiveRecord
if attributes.is_a?(Array)
attributes.collect { |attr| _create_record(attr, raise, &block) }
else
+ record = build_record(attributes, &block)
transaction do
- add_to_target(build_record(attributes)) do |record|
- yield(record) if block_given?
- insert_record(record, true, raise)
+ result = nil
+ add_to_target(record) do
+ result = insert_record(record, true, raise) {
+ @_was_loaded = loaded?
+ @association_ids = nil
+ }
end
+ raise ActiveRecord::Rollback unless result
end
+ record
end
end
# Do the relevant stuff to insert the given record into the association collection.
- def insert_record(record, validate = true, raise = false)
- raise NotImplementedError
- end
-
- def create_scope
- scope.scope_for_create.stringify_keys
+ def insert_record(record, validate = true, raise = false, &block)
+ if raise
+ record.save!(validate: validate, &block)
+ else
+ record.save(validate: validate, &block)
+ end
end
def delete_or_destroy(records, method)
+ return if records.empty?
+ records = find(records) if records.any? { |record| record.kind_of?(Integer) || record.kind_of?(String) }
records = records.flatten
records.each { |record| raise_on_type_mismatch!(record) }
existing_records = records.reject(&:new_record?)
@@ -529,13 +400,14 @@ module ActiveRecord
records.each { |record| callback(:before_remove, record) }
delete_records(existing_records, method) if existing_records.any?
- records.each { |record| target.delete(record) }
+ @target -= records
records.each { |record| callback(:after_remove, record) }
end
- # Delete the given records from the association, using one of the methods :destroy,
- # :delete_all or :nullify (or nil, in which case a default is used).
+ # Delete the given records from the association,
+ # using one of the methods +:destroy+, +:delete_all+
+ # or +:nullify+ (or +nil+, in which case a default is used).
def delete_records(records, method)
raise NotImplementedError
end
@@ -560,17 +432,46 @@ module ActiveRecord
end
end
- def concat_records(records, should_raise = false)
+ def concat_records(records, raise = false)
result = true
records.each do |record|
raise_on_type_mismatch!(record)
- add_to_target(record) do |rec|
- result &&= insert_record(rec, true, should_raise) unless owner.new_record?
+ add_to_target(record) do
+ unless owner.new_record?
+ result &&= insert_record(record, true, raise) {
+ @_was_loaded = loaded?
+ @association_ids = nil
+ }
+ end
end
end
- result && records
+ raise ActiveRecord::Rollback unless result
+
+ records
+ end
+
+ def replace_on_target(record, index, skip_callbacks)
+ callback(:before_add, record) unless skip_callbacks
+
+ set_inverse_instance(record)
+
+ @_was_loaded = true
+
+ yield(record) if block_given?
+
+ if index
+ target[index] = record
+ elsif @_was_loaded || !loaded?
+ target << record
+ end
+
+ callback(:after_add, record) unless skip_callbacks
+
+ record
+ ensure
+ @_was_loaded = nil
end
def callback(method, record)
@@ -584,25 +485,6 @@ module ActiveRecord
owner.class.send(full_callback_name)
end
- # Should we deal with assoc.first or assoc.last by issuing an independent query to
- # the database, or by getting the target, and then taking the first/last item from that?
- #
- # If the args is just a non-empty options hash, go to the database.
- #
- # Otherwise, go to the database only if none of the following are true:
- # * target already loaded
- # * owner is new record
- # * target contains new or changed record(s)
- def fetch_first_nth_or_last_using_find?(args)
- if args.first.is_a?(Hash)
- true
- else
- !(loaded? ||
- owner.new_record? ||
- target.any? { |record| record.new_record? || record.changed? })
- end
- end
-
def include_in_memory?(record)
if reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
assoc = owner.association(reflection.through_reflection.name)
@@ -629,16 +511,6 @@ module ActiveRecord
load_target.select { |r| ids.include?(r.id.to_s) }
end
end
-
- # Fetches the first/last using SQL if possible, otherwise from the target array.
- def first_nth_or_last(type, *args)
- args.shift if args.first.is_a?(Hash) && args.first.empty?
-
- collection = fetch_first_nth_or_last_using_find?(args) ? scope : load_target
- collection.send(type, *args).tap do |record|
- set_inverse_instance record if record.is_a? ActiveRecord::Base
- end
- 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 ddeafb40ea..811fbfd927 100644
--- a/activerecord/lib/active_record/associations/collection_proxy.rb
+++ b/activerecord/lib/active_record/associations/collection_proxy.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Associations
# Association proxies in Active Record are middlemen between the object that
@@ -28,13 +30,12 @@ module ActiveRecord
# is computed directly through SQL and does not trigger by itself the
# instantiation of the actual post records.
class CollectionProxy < Relation
- delegate(*(ActiveRecord::Calculations.public_instance_methods - [:count]), to: :scope)
- delegate :find_nth, to: :scope
-
def initialize(klass, association) #:nodoc:
@association = association
- super klass, klass.arel_table, klass.predicate_builder
- merge! association.scope(nullify: false)
+ super klass
+
+ extensions = association.extensions
+ extend(*extensions) if extensions.any?
end
def target
@@ -54,6 +55,12 @@ module ActiveRecord
@association.loaded?
end
+ ##
+ # :method: select
+ #
+ # :call-seq:
+ # select(*fields, &block)
+ #
# Works in two ways.
#
# *First:* Specify a subset of fields to be selected from the result set.
@@ -76,7 +83,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">,
@@ -101,18 +108,9 @@ module ActiveRecord
# # #<Pet id: 2, name: "Spook", person_id: 1>,
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
# # ]
- #
- # person.pets.select(:name) { |pet| pet.name =~ /oo/ }
- # # => [
- # # #<Pet id: 2, name: "Spook">,
- # # #<Pet id: 3, name: "Choo-Choo">
- # # ]
- def select(*fields, &block)
- @association.select(*fields, &block)
- end
# Finds an object in the collection responding to the +id+. Uses the same
- # rules as <tt>ActiveRecord::Base.find</tt>. Returns <tt>ActiveRecord::RecordNotFound</tt>
+ # rules as ActiveRecord::Base.find. Returns ActiveRecord::RecordNotFound
# error if the object cannot be found.
#
# class Person < ActiveRecord::Base
@@ -127,7 +125,7 @@ module ActiveRecord
# # ]
#
# person.pets.find(1) # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>
- # person.pets.find(4) # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=4
+ # person.pets.find(4) # => ActiveRecord::RecordNotFound: Couldn't find Pet with 'id'=4
#
# person.pets.find(2) { |pet| pet.name.downcase! }
# # => #<Pet id: 2, name: "fancy-fancy", person_id: 1>
@@ -137,10 +135,17 @@ module ActiveRecord
# # #<Pet id: 2, name: "Spook", person_id: 1>,
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
# # ]
- def find(*args, &block)
- @association.find(*args, &block)
+ def find(*args)
+ return super if block_given?
+ @association.find(*args)
end
+ ##
+ # :method: first
+ #
+ # :call-seq:
+ # first(limit = nil)
+ #
# Returns the first record, or the first +n+ records, from the collection.
# If the collection is empty, the first form returns +nil+, and the second
# form returns an empty array.
@@ -167,35 +172,63 @@ module ActiveRecord
# another_person_without.pets # => []
# another_person_without.pets.first # => nil
# another_person_without.pets.first(3) # => []
- def first(*args)
- @association.first(*args)
- end
- # Same as +first+ except returns only the second record.
- def second(*args)
- @association.second(*args)
- end
+ ##
+ # :method: second
+ #
+ # :call-seq:
+ # second()
+ #
+ # Same as #first except returns only the second record.
- # Same as +first+ except returns only the third record.
- def third(*args)
- @association.third(*args)
- end
+ ##
+ # :method: third
+ #
+ # :call-seq:
+ # third()
+ #
+ # Same as #first except returns only the third record.
- # Same as +first+ except returns only the fourth record.
- def fourth(*args)
- @association.fourth(*args)
- end
+ ##
+ # :method: fourth
+ #
+ # :call-seq:
+ # fourth()
+ #
+ # Same as #first except returns only the fourth record.
- # Same as +first+ except returns only the fifth record.
- def fifth(*args)
- @association.fifth(*args)
- end
+ ##
+ # :method: fifth
+ #
+ # :call-seq:
+ # fifth()
+ #
+ # Same as #first except returns only the fifth record.
- # Same as +first+ except returns only the forty second record.
+ ##
+ # :method: forty_two
+ #
+ # :call-seq:
+ # forty_two()
+ #
+ # Same as #first except returns only the forty second record.
# Also known as accessing "the reddit".
- def forty_two(*args)
- @association.forty_two(*args)
- end
+
+ ##
+ # :method: third_to_last
+ #
+ # :call-seq:
+ # third_to_last()
+ #
+ # Same as #first except returns only the third-to-last record.
+
+ ##
+ # :method: second_to_last
+ #
+ # :call-seq:
+ # second_to_last()
+ #
+ # Same as #first except returns only the second-to-last record.
# Returns the last record, or the last +n+ records, from the collection.
# If the collection is empty, the first form returns +nil+, and the second
@@ -223,12 +256,39 @@ module ActiveRecord
# another_person_without.pets # => []
# another_person_without.pets.last # => nil
# another_person_without.pets.last(3) # => []
- def last(*args)
- @association.last(*args)
+ def last(limit = nil)
+ load_target if find_from_target?
+ super
end
- def take(n = nil)
- @association.take(n)
+ # Gives a record (or N records if a parameter is supplied) from the collection
+ # using the same rules as <tt>ActiveRecord::Base.take</tt>.
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :pets
+ # end
+ #
+ # person.pets
+ # # => [
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
+ # # ]
+ #
+ # person.pets.take # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>
+ #
+ # person.pets.take(2)
+ # # => [
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
+ # # #<Pet id: 2, name: "Spook", person_id: 1>
+ # # ]
+ #
+ # another_person_without.pets # => []
+ # another_person_without.pets.take # => nil
+ # another_person_without.pets.take(2) # => []
+ def take(limit = nil)
+ load_target if find_from_target?
+ super
end
# Returns a new object of the collection type that has been instantiated
@@ -290,7 +350,7 @@ module ActiveRecord
@association.create(attributes, &block)
end
- # Like +create+, except that if the record is invalid, raises an exception.
+ # Like #create, except that if the record is invalid, raises an exception.
#
# class Person
# has_many :pets
@@ -307,8 +367,8 @@ module ActiveRecord
end
# Add one or more records to the collection by setting their foreign keys
- # to the association's primary key. Since << flattens its argument list and
- # inserts each record, +push+ and +concat+ behave identically. Returns +self+
+ # to the association's primary key. Since #<< flattens its argument list and
+ # inserts each record, +push+ and #concat behave identically. Returns +self+
# so method calls may be chained.
#
# class Person < ActiveRecord::Base
@@ -364,7 +424,7 @@ module ActiveRecord
# specified by the +:dependent+ option. If no +:dependent+ option is given,
# then it will follow the default strategy.
#
- # For +has_many :through+ associations, the default deletion strategy is
+ # For <tt>has_many :through</tt> associations, the default deletion strategy is
# +:delete_all+.
#
# For +has_many+ associations, the default deletion strategy is +:nullify+.
@@ -399,7 +459,7 @@ module ActiveRecord
# # #<Pet id: 3, name: "Choo-Choo", person_id: nil>
# # ]
#
- # Both +has_many+ and +has_many :through+ dependencies default to the
+ # Both +has_many+ and <tt>has_many :through</tt> dependencies default to the
# +:delete_all+ strategy if the +:dependent+ option is set to +:destroy+.
# Records are not instantiated and callbacks will not be fired.
#
@@ -418,7 +478,7 @@ module ActiveRecord
# person.pets.delete_all
#
# Pet.find(1, 2, 3)
- # # => ActiveRecord::RecordNotFound
+ # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3)
#
# If it is set to <tt>:delete_all</tt>, all the objects are deleted
# *without* calling their +destroy+ method.
@@ -438,7 +498,7 @@ module ActiveRecord
# person.pets.delete_all
#
# Pet.find(1, 2, 3)
- # # => ActiveRecord::RecordNotFound
+ # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3)
def delete_all(dependent = nil)
@association.delete_all(dependent)
end
@@ -475,7 +535,7 @@ module ActiveRecord
# then it will follow the default strategy. Returns an array with the
# deleted records.
#
- # For +has_many :through+ associations, the default deletion strategy is
+ # For <tt>has_many :through</tt> associations, the default deletion strategy is
# +:delete_all+.
#
# For +has_many+ associations, the default deletion strategy is +:nullify+.
@@ -532,7 +592,7 @@ module ActiveRecord
# # => [#<Pet id: 2, name: "Spook", person_id: 1>]
#
# Pet.find(1, 3)
- # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (1, 3)
+ # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 3)
#
# If it is set to <tt>:delete_all</tt>, all the +records+ are deleted
# *without* calling their +destroy+ method.
@@ -560,9 +620,9 @@ module ActiveRecord
# # ]
#
# Pet.find(1)
- # # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=1
+ # # => ActiveRecord::RecordNotFound: Couldn't find Pet with 'id'=1
#
- # You can pass +Fixnum+ or +String+ values, it finds the records
+ # You can pass +Integer+ or +String+ values, it finds the records
# responding to the +id+ and executes delete on them.
#
# class Person < ActiveRecord::Base
@@ -624,9 +684,9 @@ module ActiveRecord
# person.pets.size # => 0
# person.pets # => []
#
- # Pet.find(1, 2, 3) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (1, 2, 3)
+ # Pet.find(1, 2, 3) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3)
#
- # You can pass +Fixnum+ or +String+ values, it finds the records
+ # You can pass +Integer+ or +String+ values, it finds the records
# responding to the +id+ and then deletes them from the database.
#
# person.pets.size # => 3
@@ -656,11 +716,17 @@ module ActiveRecord
# person.pets.size # => 0
# person.pets # => []
#
- # Pet.find(4, 5, 6) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (4, 5, 6)
+ # Pet.find(4, 5, 6) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (4, 5, 6)
def destroy(*records)
@association.destroy(*records)
end
+ ##
+ # :method: distinct
+ #
+ # :call-seq:
+ # distinct(value = true)
+ #
# Specifies whether the records should be unique or not.
#
# class Person < ActiveRecord::Base
@@ -675,17 +741,35 @@ module ActiveRecord
#
# person.pets.select(:name).distinct
# # => [#<Pet name: "Fancy-Fancy">]
- def distinct
- @association.distinct
+ #
+ # person.pets.select(:name).distinct.distinct(false)
+ # # => [
+ # # #<Pet name: "Fancy-Fancy">,
+ # # #<Pet name: "Fancy-Fancy">
+ # # ]
+
+ #--
+ def calculate(operation, column_name)
+ null_scope? ? scope.calculate(operation, column_name) : super
end
- alias uniq distinct
- # Count all records using SQL.
+ def pluck(*column_names)
+ null_scope? ? scope.pluck(*column_names) : super
+ end
+
+ ##
+ # :method: count
+ #
+ # :call-seq:
+ # count(column_name = nil, &block)
+ #
+ # Count all records.
#
# class Person < ActiveRecord::Base
# has_many :pets
# end
#
+ # # This will perform the count using SQL.
# person.pets.count # => 3
# person.pets
# # => [
@@ -693,9 +777,11 @@ module ActiveRecord
# # #<Pet id: 2, name: "Spook", person_id: 1>,
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
# # ]
- def count(column_name = nil)
- @association.count(column_name)
- end
+ #
+ # Passing a block will select all of a person's pets in SQL and then
+ # perform the count using Ruby.
+ #
+ # person.pets.count { |pet| pet.name.include?('-') } # => 2
# Returns the size of the collection. If the collection hasn't been loaded,
# it executes a <tt>SELECT COUNT(*)</tt> query. Else it calls <tt>collection.size</tt>.
@@ -725,6 +811,12 @@ module ActiveRecord
@association.size
end
+ ##
+ # :method: length
+ #
+ # :call-seq:
+ # length()
+ #
# Returns the size of the collection calling +size+ on the target.
# If the collection has been already loaded, +length+ and +size+ are
# equivalent. If not and you are going to need the records anyway this
@@ -745,14 +837,11 @@ module ActiveRecord
# # #<Pet id: 2, name: "Spook", person_id: 1>,
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
# # ]
- def length
- @association.length
- end
# Returns +true+ if the collection is empty. If the collection has been
# loaded it is equivalent
# to <tt>collection.size.zero?</tt>. If the collection has not been loaded,
- # it is equivalent to <tt>collection.exists?</tt>. If the collection has
+ # it is equivalent to <tt>!collection.exists?</tt>. If the collection has
# not already been loaded and you are going to fetch the records anyway it
# is better to check <tt>collection.length.zero?</tt>.
#
@@ -771,6 +860,12 @@ module ActiveRecord
@association.empty?
end
+ ##
+ # :method: any?
+ #
+ # :call-seq:
+ # any?()
+ #
# Returns +true+ if the collection is not empty.
#
# class Person < ActiveRecord::Base
@@ -781,7 +876,7 @@ module ActiveRecord
# person.pets.any? # => false
#
# person.pets << Pet.new(name: 'Snoop')
- # person.pets.count # => 0
+ # person.pets.count # => 1
# person.pets.any? # => true
#
# You can also pass a +block+ to define criteria. The behavior
@@ -800,10 +895,13 @@ module ActiveRecord
# pet.group == 'dogs'
# end
# # => true
- def any?(&block)
- @association.any?(&block)
- end
+ ##
+ # :method: many?
+ #
+ # :call-seq:
+ # many?()
+ #
# Returns true if the collection has more than one record.
# Equivalent to <tt>collection.size > 1</tt>.
#
@@ -838,9 +936,6 @@ module ActiveRecord
# pet.group == 'cats'
# end
# # => true
- def many?(&block)
- @association.many?(&block)
- end
# Returns +true+ if the given +record+ is present in the collection.
#
@@ -856,27 +951,14 @@ module ActiveRecord
!!@association.include?(record)
end
- def arel
- scope.arel
- end
-
def proxy_association
@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
@@ -906,6 +988,12 @@ module ActiveRecord
load_target == other
end
+ ##
+ # :method: to_ary
+ #
+ # :call-seq:
+ # to_ary()
+ #
# Returns a new array of objects from the collection. If the collection
# hasn't been loaded, it fetches the records from the database.
#
@@ -939,10 +1027,10 @@ module ActiveRecord
# # #<Pet id: 5, name: "Brain", person_id: 1>,
# # #<Pet id: 6, name: "Boss", person_id: 1>
# # ]
- def to_ary
- load_target.dup
+
+ def records # :nodoc:
+ load_target
end
- alias_method :to_a, :to_ary
# Adds one or more +records+ to the collection by setting their foreign keys
# to the association's primary key. Returns +self+, so several appends may be
@@ -971,7 +1059,7 @@ module ActiveRecord
alias_method :append, :<<
def prepend(*args)
- raise NoMethodError, "prepend on association is not defined. Please use << or append"
+ raise NoMethodError, "prepend on association is not defined. Please use <<, push or append"
end
# Equivalent to +delete_all+. The difference is that returns +self+, instead
@@ -986,7 +1074,6 @@ module ActiveRecord
end
# Reloads the collection from the database. Returns +self+.
- # Equivalent to <tt>collection(true)</tt>.
#
# class Person < ActiveRecord::Base
# has_many :pets
@@ -1000,12 +1087,9 @@ module ActiveRecord
#
# person.pets.reload # fetches pets from the database
# # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
- #
- # person.pets(true) # fetches pets from the database
- # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
def reload
- proxy_association.reload
- self
+ proxy_association.reload(true)
+ reset_scope
end
# Unloads the association. Returns +self+.
@@ -1027,8 +1111,47 @@ module ActiveRecord
def reset
proxy_association.reset
proxy_association.reset_scope
+ reset_scope
+ end
+
+ def reset_scope # :nodoc:
+ @offsets = {}
+ @scope = nil
self
end
+
+ delegate_methods = [
+ QueryMethods,
+ SpawnMethods,
+ ].flat_map { |klass|
+ klass.public_instance_methods(false)
+ } - self.public_instance_methods(false) - [:select] + [:scoping, :values]
+
+ delegate(*delegate_methods, to: :scope)
+
+ private
+
+ def find_nth_with_limit(index, limit)
+ load_target if find_from_target?
+ super
+ end
+
+ def find_nth_from_last(index)
+ load_target if find_from_target?
+ super
+ end
+
+ def null_scope?
+ @association.null_scope?
+ end
+
+ def find_from_target?
+ @association.find_from_target?
+ end
+
+ def exec_queries
+ load_target
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/foreign_association.rb b/activerecord/lib/active_record/associations/foreign_association.rb
index fe48ecec29..40010cde03 100644
--- a/activerecord/lib/active_record/associations/foreign_association.rb
+++ b/activerecord/lib/active_record/associations/foreign_association.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
module ActiveRecord::Associations
- module ForeignAssociation
+ module ForeignAssociation # :nodoc:
def foreign_key_present?
if reflection.klass.primary_key
owner.attribute_present?(reflection.active_record_primary_key)
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index ca27c9fdde..cf85a87fa7 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
module ActiveRecord
- # = Active Record Has Many Association
module Associations
+ # = Active Record Has Many Association
# This is the proxy that handles a has many association.
#
# If the association has a <tt>:through</tt> option further specialization
@@ -15,35 +17,27 @@ module ActiveRecord
when :restrict_with_error
unless empty?
- record = klass.human_attribute_name(reflection.name).downcase
- owner.errors.add(:base, :"restrict_dependent_destroy.many", record: record)
+ record = owner.class.human_attribute_name(reflection.name).downcase
+ owner.errors.add(:base, :'restrict_dependent_destroy.has_many', record: record)
throw(:abort)
end
+ when :destroy
+ # No point in executing the counter update since we're going to destroy the parent anyway
+ load_target.each { |t| t.destroyed_by_association = reflection }
+ destroy_all
else
- if options[:dependent] == :destroy
- # No point in executing the counter update since we're going to destroy the parent anyway
- load_target.each { |t| t.destroyed_by_association = reflection }
- destroy_all
- else
- delete_all
- end
+ delete_all
end
end
def insert_record(record, validate = true, raise = false)
set_owner_attributes(record)
- set_inverse_instance(record)
-
- if raise
- record.save!(:validate => validate)
- else
- record.save(:validate => validate)
- end
+ super
end
def empty?
- if has_cached_counter?
+ if reflection.has_cached_counter?
size.zero?
else
super
@@ -66,84 +60,34 @@ module ActiveRecord
# If the collection is empty the target is set to an empty array and
# the loaded flag is set to true as well.
def count_records
- count = if has_cached_counter?
- owner._read_attribute cached_counter_attribute_name
+ count = if reflection.has_cached_counter?
+ owner._read_attribute(reflection.counter_cache_column).to_i
else
- scope.count
+ scope.count(:all)
end
# If there's nothing in the database and @target has no new records
# we are certain the current target is an empty array. This is a
# documented side-effect of the method that may avoid an extra SELECT.
- @target ||= [] and loaded! if count == 0
+ (@target ||= []) && loaded! if count == 0
[association_scope.limit_value, count].compact.min
end
- def has_cached_counter?(reflection = reflection())
- owner.attribute_present?(cached_counter_attribute_name(reflection))
- end
-
- def cached_counter_attribute_name(reflection = reflection())
- if reflection.options[:counter_cache]
- reflection.options[:counter_cache].to_s
- else
- "#{reflection.name}_count"
- end
- end
-
def update_counter(difference, reflection = reflection())
- update_counter_in_database(difference, reflection)
- update_counter_in_memory(difference, reflection)
- end
-
- def update_counter_in_database(difference, reflection = reflection())
- if has_cached_counter?(reflection)
- counter = cached_counter_attribute_name(reflection)
- owner.class.update_counters(owner.id, counter => difference)
+ if reflection.has_cached_counter?
+ owner.increment!(reflection.counter_cache_column, difference)
end
end
def update_counter_in_memory(difference, reflection = reflection())
- if counter_must_be_updated_by_has_many?(reflection)
- counter = cached_counter_attribute_name(reflection)
- owner[counter] += difference
- owner.send(:clear_attribute_changes, counter) # eww
+ if reflection.counter_must_be_updated_by_has_many?
+ counter = reflection.counter_cache_column
+ owner.increment(counter, difference)
+ owner.send(:clear_attribute_change, counter) # eww
end
end
- # This shit is nasty. We need to avoid the following situation:
- #
- # * An associated record is deleted via record.destroy
- # * Hence the callbacks run, and they find a belongs_to on the record with a
- # :counter_cache options which points back at our owner. So they update the
- # counter cache.
- # * In which case, we must make sure to *not* update the counter cache, or else
- # it will be decremented twice.
- #
- # Hence this method.
- def inverse_which_updates_counter_cache(reflection = reflection())
- counter_name = cached_counter_attribute_name(reflection)
- inverse_which_updates_counter_named(counter_name, reflection)
- end
- alias inverse_updates_counter_cache? inverse_which_updates_counter_cache
-
- def inverse_which_updates_counter_named(counter_name, reflection)
- reflection.klass._reflections.values.find { |inverse_reflection|
- inverse_reflection.belongs_to? &&
- inverse_reflection.counter_cache_column == counter_name
- }
- end
-
- def inverse_updates_counter_in_memory?(reflection)
- inverse = inverse_which_updates_counter_cache(reflection)
- inverse && inverse == reflection.inverse_of
- end
-
- def counter_must_be_updated_by_has_many?(reflection)
- !inverse_updates_counter_in_memory?(reflection) && has_cached_counter?(reflection)
- end
-
def delete_count(method, scope)
if method == :delete_all
scope.delete_all
@@ -153,7 +97,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
@@ -161,7 +105,7 @@ module ActiveRecord
def delete_records(records, method)
if method == :destroy
records.each(&:destroy!)
- update_counter(-records.length) unless inverse_updates_counter_cache?
+ update_counter(-records.length) unless reflection.inverse_updates_counter_cache?
else
scope = self.scope.where(reflection.klass.primary_key => records)
update_counter(-delete_count(method, scope))
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 cd79266952..f84ac65fa2 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -1,14 +1,14 @@
+# frozen_string_literal: true
+
module ActiveRecord
- # = Active Record Has Many Through Association
module Associations
+ # = Active Record Has Many Through Association
class HasManyThroughAssociation < HasManyAssociation #:nodoc:
include ThroughAssociation
def initialize(owner, reflection)
super
-
- @through_records = {}
- @through_association = nil
+ @through_records = {}
end
def concat(*records)
@@ -21,27 +21,11 @@ module ActiveRecord
super
end
- def concat_records(records)
- ensure_not_nested
-
- records = super(records, true)
-
- if owner.new_record? && records
- records.flatten.each do |record|
- build_through_record(record)
- end
- end
-
- records
- end
-
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?
+ return unless super
end
save_through_record(record)
@@ -50,9 +34,18 @@ module ActiveRecord
end
private
+ def concat_records(records)
+ ensure_not_nested
+
+ records = super(records, true)
- def through_association
- @through_association ||= owner.association(through_reflection.name)
+ if owner.new_record? && records
+ records.flatten.each do |record|
+ build_through_record(record)
+ end
+ end
+
+ records
end
# The through record (built with build_record) is temporarily cached
@@ -66,6 +59,11 @@ module ActiveRecord
through_record = through_association.build(*options_for_through_record)
through_record.send("#{source_reflection.name}=", record)
+
+ if options[:source_type]
+ through_record.send("#{source_reflection.foreign_type}=", options[:source_type])
+ end
+
through_record
end
end
@@ -81,7 +79,10 @@ module ActiveRecord
end
def save_through_record(record)
- build_through_record(record).save!
+ association = build_through_record(record)
+ if association.changed?
+ association.save!
+ end
ensure
@through_records.delete(record.object_id)
end
@@ -89,7 +90,7 @@ module ActiveRecord
def build_record(attributes)
ensure_not_nested
- record = super(attributes)
+ record = super
inverse = source_reflection.inverse_of
if inverse
@@ -103,6 +104,11 @@ module ActiveRecord
record
end
+ def remove_records(existing_records, records, method)
+ super
+ delete_through_records(records)
+ end
+
def target_reflection_has_associated_record?
!(through_reflection.belongs_to? && owner[through_reflection.foreign_key].blank?)
end
@@ -110,7 +116,7 @@ module ActiveRecord
def update_through_counter?(method)
case method
when :destroy
- !inverse_updates_counter_cache?(through_reflection)
+ !through_reflection.inverse_updates_counter_cache?
when :nullify
false
else
@@ -127,21 +133,15 @@ module ActiveRecord
scope = through_association.scope
scope.where! construct_join_attributes(*records)
+ scope = scope.where(through_scope_attributes)
case method
when :destroy
if scope.klass.primary_key
- count = scope.destroy_all.length
+ count = scope.destroy_all.count(&:destroyed?)
else
- scope.each { |record| record.run_callbacks :destroy }
-
- arel = scope.arel
-
- stmt = Arel::DeleteManager.new
- stmt.from scope.klass.arel_table
- stmt.wheres = arel.constraints
-
- count = scope.klass.connection.delete(stmt, 'SQL', scope.bound_attributes)
+ scope.each(&:_run_destroy_callbacks)
+ count = scope.delete_all
end
when :nullify
count = scope.update_all(source_reflection.foreign_key => nil)
@@ -191,7 +191,7 @@ module ActiveRecord
def find_target
return [] unless target_reflection_has_associated_record?
- get_records
+ super
end
# NOTE - not sure that we can actually cope with inverses here
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index 41a75b820e..390bfd8b08 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
module ActiveRecord
- # = Active Record Belongs To Has One Association
module Associations
+ # = Active Record Has One Association
class HasOneAssociation < SingularAssociation #:nodoc:
include ForeignAssociation
@@ -11,8 +13,8 @@ module ActiveRecord
when :restrict_with_error
if load_target
- record = klass.human_attribute_name(reflection.name).downcase
- owner.errors.add(:base, :"restrict_dependent_destroy.one", record: record)
+ record = owner.class.human_attribute_name(reflection.name).downcase
+ owner.errors.add(:base, :'restrict_dependent_destroy.has_one', record: record)
throw(:abort)
end
@@ -21,49 +23,49 @@ module ActiveRecord
end
end
- def replace(record, save = true)
- raise_on_type_mismatch!(record) if record
- load_target
+ def delete(method = options[:dependent])
+ if load_target
+ case method
+ when :delete
+ target.delete
+ when :destroy
+ target.destroyed_by_association = reflection
+ target.destroy
+ throw(:abort) unless target.destroyed?
+ when :nullify
+ target.update_columns(reflection.foreign_key => nil) if target.persisted?
+ end
+ end
+ end
+
+ private
+ def replace(record, save = true)
+ raise_on_type_mismatch!(record) if record
- return self.target if !(target || record)
+ return target unless load_target || record
- assigning_another_record = target != record
- if assigning_another_record || record.changed?
- save &&= owner.persisted?
+ assigning_another_record = target != record
+ if assigning_another_record || record.has_changes_to_save?
+ save &&= owner.persisted?
- transaction_if(save) do
- remove_target!(options[:dependent]) if target && !target.destroyed? && assigning_another_record
+ transaction_if(save) do
+ remove_target!(options[:dependent]) if target && !target.destroyed? && assigning_another_record
- if record
- set_owner_attributes(record)
- set_inverse_instance(record)
+ if record
+ set_owner_attributes(record)
+ set_inverse_instance(record)
- if save && !record.save
- nullify_owner_attributes(record)
- set_owner_attributes(target) if target
- raise RecordNotSaved, "Failed to save the new associated #{reflection.name}."
+ if save && !record.save
+ nullify_owner_attributes(record)
+ set_owner_attributes(target) if target
+ raise RecordNotSaved, "Failed to save the new associated #{reflection.name}."
+ end
end
end
end
- end
- self.target = record
- end
-
- def delete(method = options[:dependent])
- if load_target
- case method
- when :delete
- target.delete
- when :destroy
- target.destroy
- when :nullify
- target.update_columns(reflection.foreign_key => nil)
- end
+ self.target = record
end
- end
-
- private
# The reason that the save param for replace is false, if for create (not just build),
# is because the setting of the foreign keys is actually handled by the scoping when
@@ -75,18 +77,20 @@ module ActiveRecord
def remove_target!(method)
case method
- when :delete
- target.delete
- when :destroy
- target.destroy
- else
- nullify_owner_attributes(target)
-
- if target.persisted? && owner.persisted? && !target.save
- set_owner_attributes(target)
- raise RecordNotSaved, "Failed to remove the existing associated #{reflection.name}. " +
- "The record failed to save after its foreign key was set to nil."
- end
+ when :delete
+ target.delete
+ when :destroy
+ target.destroyed_by_association = reflection
+ target.destroy
+ else
+ nullify_owner_attributes(target)
+ remove_inverse_instance(target)
+
+ if target.persisted? && owner.persisted? && !target.save
+ set_owner_attributes(target)
+ 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
end
@@ -101,6 +105,14 @@ module ActiveRecord
yield
end
end
+
+ def _create_record(attributes, raise_error = false, &block)
+ unless owner.persisted?
+ raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
+ end
+
+ super
+ end
end
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 08e0ec691f..10978b2d93 100644
--- a/activerecord/lib/active_record/associations/has_one_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_through_association.rb
@@ -1,30 +1,39 @@
+# frozen_string_literal: true
+
module ActiveRecord
- # = Active Record Has One Through Association
module Associations
+ # = Active Record Has One Through Association
class HasOneThroughAssociation < HasOneAssociation #:nodoc:
include ThroughAssociation
- def replace(record)
- create_through_record(record)
- self.target = record
- end
-
private
+ def replace(record, save = true)
+ create_through_record(record, save)
+ self.target = record
+ end
- def create_through_record(record)
+ def create_through_record(record, save)
ensure_not_nested
- through_proxy = owner.association(through_reflection.name)
- through_record = through_proxy.send(:load_target)
+ through_proxy = through_association
+ through_record = through_proxy.load_target
if through_record && !record
through_record.destroy
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?
+ if through_record.new_record?
+ through_record.assign_attributes(attributes)
+ else
+ through_record.update(attributes)
+ end
+ elsif owner.new_record? || !save
through_proxy.build(attributes)
else
through_proxy.create(attributes)
diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb
index 81eb5136a1..b76005b587 100644
--- a/activerecord/lib/active_record/associations/join_dependency.rb
+++ b/activerecord/lib/active_record/associations/join_dependency.rb
@@ -1,21 +1,21 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Associations
class JoinDependency # :nodoc:
- autoload :JoinBase, 'active_record/associations/join_dependency/join_base'
- autoload :JoinAssociation, 'active_record/associations/join_dependency/join_association'
+ autoload :JoinBase, "active_record/associations/join_dependency/join_base"
+ autoload :JoinAssociation, "active_record/associations/join_dependency/join_association"
class Aliases # :nodoc:
def initialize(tables)
@tables = tables
- @alias_cache = tables.each_with_object({}) { |table,h|
- h[table.node] = table.columns.each_with_object({}) { |column,i|
+ @alias_cache = tables.each_with_object({}) { |table, h|
+ h[table.node] = table.columns.each_with_object({}) { |column, i|
i[column.name] = column.alias
}
}
- @name_and_alias_cache = tables.each_with_object({}) { |table,h|
- h[table.node] = table.columns.map { |column|
- [column.name, column.alias]
- }
+ @columns_cache = tables.each_with_object({}) { |table, h|
+ h[table.node] = table.columns
}
end
@@ -23,30 +23,23 @@ module ActiveRecord
@tables.flat_map(&:column_aliases)
end
- # An array of [column_name, alias] pairs for the table
def column_aliases(node)
- @name_and_alias_cache[node]
+ @columns_cache[node]
end
def column_alias(node, column)
@alias_cache[node][column]
end
- class Table < Struct.new(:node, :columns)
- def table
- Arel::Nodes::TableAlias.new node.table, node.aliased_table_name
- end
-
+ Table = Struct.new(:node, :columns) do # :nodoc:
def column_aliases
- t = table
+ t = node.table
columns.map { |column| t[column.name].as Arel.sql column.alias }
end
end
Column = Struct.new(:name, :alias)
end
- attr_reader :alias_tracker, :base_klass, :join_root
-
def self.make_tree(associations)
hash = {}
walk_tree associations, hash
@@ -62,7 +55,7 @@ module ActiveRecord
walk_tree assoc, hash
end
when Hash
- associations.each do |k,v|
+ associations.each do |k, v|
cache = hash[k] ||= {}
walk_tree v, cache
end
@@ -71,73 +64,41 @@ module ActiveRecord
end
end
- # base is the base class on which operation is taking place.
- # associations is the list of associations which are joined using hash, symbol or array.
- # joins is the list of all string join commands and arel nodes.
- #
- # Example :
- #
- # class Physician < ActiveRecord::Base
- # has_many :appointments
- # has_many :patients, through: :appointments
- # end
- #
- # If I execute `@physician.patients.to_a` then
- # base # => Physician
- # associations # => []
- # joins # => [#<Arel::Nodes::InnerJoin: ...]
- #
- # However if I execute `Physician.joins(:appointments).to_a` then
- # base # => Physician
- # associations # => [:appointments]
- # joins # => []
- #
- def initialize(base, associations, joins)
- @alias_tracker = AliasTracker.create_with_joins(base.connection, base.table_name, joins, base.type_caster)
+ def initialize(base, table, associations)
tree = self.class.make_tree associations
- @join_root = JoinBase.new base, build(tree, base)
- @join_root.children.each { |child| construct_tables! @join_root, child }
+ @join_root = JoinBase.new(base, table, build(tree, base))
end
def reflections
join_root.drop(1).map!(&:reflection)
end
- def join_constraints(outer_joins)
- joins = join_root.children.flat_map { |child|
- make_inner_joins join_root, child
- }
+ def join_constraints(joins_to_add, join_type, alias_tracker)
+ @alias_tracker = alias_tracker
+
+ construct_tables!(join_root)
+ joins = make_join_constraints(join_root, join_type)
- joins.concat outer_joins.flat_map { |oj|
+ joins.concat joins_to_add.flat_map { |oj|
+ construct_tables!(oj.join_root)
if join_root.match? oj.join_root
walk join_root, oj.join_root
else
- oj.join_root.children.flat_map { |child|
- make_outer_joins oj.join_root, child
- }
+ make_join_constraints(oj.join_root, join_type)
end
}
end
- def aliases
- Aliases.new join_root.each_with_index.map { |join_part,i|
- columns = join_part.column_names.each_with_index.map { |column_name,j|
- Aliases::Column.new column_name, "t#{i}_r#{j}"
- }
- Aliases::Table.new(join_part, columns)
- }
- end
-
- def instantiate(result_set, aliases)
+ def instantiate(result_set, &block)
primary_key = aliases.column_alias(join_root, join_root.primary_key)
- seen = Hash.new { |h,parent_klass|
- h[parent_klass] = Hash.new { |i,parent_id|
- i[parent_id] = Hash.new { |j,child_klass| j[child_klass] = {} }
+ seen = Hash.new { |i, object_id|
+ i[object_id] = Hash.new { |j, child_class|
+ j[child_class] = {}
}
}
- model_cache = Hash.new { |h,klass| h[klass] = {} }
+ model_cache = Hash.new { |h, klass| h[klass] = {} }
parents = model_cache[join_root]
column_aliases = aliases.column_aliases join_root
@@ -148,138 +109,149 @@ module ActiveRecord
class_name: join_root.base_klass.name
}
- message_bus.instrument('instantiation.active_record', payload) do
+ message_bus.instrument("instantiation.active_record", payload) do
result_set.each { |row_hash|
- parent = parents[row_hash[primary_key]] ||= join_root.instantiate(row_hash, column_aliases)
- construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases)
+ parent_key = primary_key ? row_hash[primary_key] : row_hash
+ parent = parents[parent_key] ||= join_root.instantiate(row_hash, column_aliases, &block)
+ construct(parent, join_root, row_hash, seen, model_cache)
}
end
parents.values
end
- private
-
- def make_constraints(parent, child, tables, join_type)
- 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)
- end
-
- def make_outer_joins(parent, child)
- tables = table_aliases_for(parent, child)
- join_type = Arel::Nodes::OuterJoin
- info = make_constraints parent, child, tables, join_type
-
- [info] + child.children.flat_map { |c| make_outer_joins(child, c) }
+ def apply_column_aliases(relation)
+ relation._select!(-> { aliases.columns })
end
- def make_inner_joins(parent, child)
- tables = child.tables
- join_type = Arel::Nodes::InnerJoin
- info = make_constraints parent, child, tables, join_type
+ protected
+ attr_reader :join_root
- [info] + child.children.flat_map { |c| make_inner_joins(child, c) }
- end
+ private
+ attr_reader :alias_tracker
- def table_aliases_for(parent, node)
- node.reflection.chain.map { |reflection|
- alias_tracker.aliased_table_for(
- reflection.table_name,
- table_alias_for(reflection, parent, reflection != node.reflection)
- )
- }
- end
+ def aliases
+ @aliases ||= Aliases.new join_root.each_with_index.map { |join_part, i|
+ columns = join_part.column_names.each_with_index.map { |column_name, j|
+ Aliases::Column.new column_name, "t#{i}_r#{j}"
+ }
+ Aliases::Table.new(join_part, columns)
+ }
+ end
- def construct_tables!(parent, node)
- node.tables = table_aliases_for(parent, node)
- node.children.each { |child| construct_tables! node, child }
- end
+ def construct_tables!(join_root)
+ join_root.each_children do |parent, child|
+ child.tables = table_aliases_for(parent, child)
+ end
+ end
- def table_alias_for(reflection, parent, join)
- name = "#{reflection.plural_name}_#{parent.table_name}"
- name << "_join" if join
- name
- end
+ def make_join_constraints(join_root, join_type)
+ join_root.children.flat_map do |child|
+ make_constraints(join_root, child, join_type)
+ end
+ end
- def walk(left, right)
- intersection, missing = right.children.map { |node1|
- [left.children.find { |node2| node1.match? node2 }, node1]
- }.partition(&:first)
+ def make_constraints(parent, child, join_type = Arel::Nodes::OuterJoin)
+ foreign_table = parent.table
+ foreign_klass = parent.base_klass
+ joins = child.join_constraints(foreign_table, foreign_klass, join_type, alias_tracker)
+ joins.concat child.children.flat_map { |c| make_constraints(child, c, join_type) }
+ end
- ojs = missing.flat_map { |_,n| make_outer_joins left, n }
- intersection.flat_map { |l,r| walk l, r }.concat ojs
- end
+ def table_aliases_for(parent, node)
+ node.reflection.chain.map { |reflection|
+ alias_tracker.aliased_table_for(
+ reflection.table_name,
+ table_alias_for(reflection, parent, reflection != node.reflection),
+ reflection.klass.type_caster
+ )
+ }
+ end
- def find_reflection(klass, name)
- klass._reflect_on_association(name) or
- raise ConfigurationError, "Association named '#{ name }' was not found on #{ klass.name }; perhaps you misspelled it?"
- end
+ def table_alias_for(reflection, parent, join)
+ name = reflection.alias_candidate(parent.table_name)
+ join ? "#{name}_join" : name
+ end
- def build(associations, base_klass)
- associations.map do |name, right|
- reflection = find_reflection base_klass, name
- reflection.check_validity!
- reflection.check_eager_loadable!
+ def walk(left, right)
+ intersection, missing = right.children.map { |node1|
+ [left.children.find { |node2| node1.match? node2 }, node1]
+ }.partition(&:first)
- if reflection.polymorphic?
- raise EagerLoadPolymorphicError.new(reflection)
- end
+ joins = intersection.flat_map { |l, r| r.table = l.table; walk(l, r) }
+ joins.concat missing.flat_map { |_, n| make_constraints(left, n) }
+ end
- JoinAssociation.new reflection, build(right, reflection.klass)
+ def find_reflection(klass, name)
+ klass._reflect_on_association(name) ||
+ raise(ConfigurationError, "Can't join '#{klass.name}' to association named '#{name}'; perhaps you misspelled it?")
end
- end
- def construct(ar_parent, parent, row, rs, seen, model_cache, aliases)
- return if ar_parent.nil?
- primary_id = ar_parent.id
+ def build(associations, base_klass)
+ associations.map do |name, right|
+ reflection = find_reflection base_klass, name
+ reflection.check_validity!
+ reflection.check_eager_loadable!
- parent.children.each do |node|
- if node.reflection.collection?
- other = ar_parent.association(node.reflection.name)
- other.loaded!
- elsif ar_parent.association_cached?(node.reflection.name)
- model = ar_parent.association(node.reflection.name).target
- construct(model, node, row, rs, seen, model_cache, aliases)
- next
+ if reflection.polymorphic?
+ raise EagerLoadPolymorphicError.new(reflection)
+ end
+
+ JoinAssociation.new(reflection, build(right, reflection.klass))
end
+ end
- key = aliases.column_alias(node, node.primary_key)
- id = row[key]
- if id.nil?
- nil_association = ar_parent.association(node.reflection.name)
- nil_association.loaded!
- next
+ def construct(ar_parent, parent, row, seen, model_cache)
+ return if ar_parent.nil?
+
+ parent.children.each do |node|
+ if node.reflection.collection?
+ other = ar_parent.association(node.reflection.name)
+ other.loaded!
+ elsif ar_parent.association_cached?(node.reflection.name)
+ model = ar_parent.association(node.reflection.name).target
+ construct(model, node, row, seen, model_cache)
+ next
+ end
+
+ key = aliases.column_alias(node, node.primary_key)
+ id = row[key]
+ if id.nil?
+ nil_association = ar_parent.association(node.reflection.name)
+ nil_association.loaded!
+ next
+ end
+
+ model = seen[ar_parent.object_id][node][id]
+
+ if model
+ construct(model, node, row, seen, model_cache)
+ else
+ model = construct_model(ar_parent, node, row, model_cache, id)
+
+ seen[ar_parent.object_id][node][id] = model
+ construct(model, node, row, seen, model_cache)
+ end
end
+ end
+
+ def construct_model(record, node, row, model_cache, id)
+ other = record.association(node.reflection.name)
- model = seen[parent.base_klass][primary_id][node.base_klass][id]
+ model = model_cache[node][id] ||=
+ node.instantiate(row, aliases.column_aliases(node)) do |m|
+ other.set_inverse_instance(m)
+ end
- if model
- construct(model, node, row, rs, seen, model_cache, aliases)
+ if node.reflection.collection?
+ other.target.push(model)
else
- model = construct_model(ar_parent, node, row, model_cache, id, aliases)
- model.readonly!
- seen[parent.base_klass][primary_id][node.base_klass][id] = model
- construct(model, node, row, rs, seen, model_cache, aliases)
+ other.target = model
end
- end
- end
-
- def construct_model(record, node, row, model_cache, id, aliases)
- model = model_cache[node][id] ||= node.instantiate(row,
- aliases.column_aliases(node))
- other = record.association(node.reflection.name)
- if node.reflection.collection?
- other.target.push(model)
- else
- other.target = model
+ model.readonly! if node.readonly?
+ model
end
-
- other.set_inverse_instance(model)
- model
- end
end
end
end
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 a6ad09a38a..4583d89cba 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
@@ -1,19 +1,19 @@
-require 'active_record/associations/join_dependency/join_part'
+# frozen_string_literal: true
+
+require "active_record/associations/join_dependency/join_part"
module ActiveRecord
module Associations
class JoinDependency # :nodoc:
class JoinAssociation < JoinPart # :nodoc:
- # The reflection of the association represented
- attr_reader :reflection
-
- attr_accessor :tables
+ attr_reader :reflection, :tables
+ attr_accessor :table
def initialize(reflection, children)
super(reflection.klass, children)
- @reflection = reflection
- @tables = nil
+ @reflection = reflection
+ @tables = nil
end
def match?(other)
@@ -21,107 +21,44 @@ module ActiveRecord
super && reflection == other.reflection
end
- JoinInformation = Struct.new :joins, :binds
-
- def join_constraints(foreign_table, foreign_klass, node, join_type, tables, scope_chain, chain)
- joins = []
- binds = []
- tables = tables.reverse
-
- scope_chain_index = 0
- scope_chain = scope_chain.reverse
+ def join_constraints(foreign_table, foreign_klass, join_type, alias_tracker)
+ joins = []
# 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
+ reflection.chain.reverse_each.with_index(1) do |reflection, i|
+ table = tables[-i]
klass = reflection.klass
- join_keys = reflection.join_keys(klass)
- key = join_keys.key
- foreign_key = join_keys.foreign_key
-
- constraint = build_constraint(klass, table, key, foreign_table, foreign_key)
+ constraint = reflection.build_join_constraint(table, foreign_table)
- 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
-
- relation = ActiveRecord::Relation.create(
- klass,
- table,
- predicate_builder,
- )
- scope_chain_items.concat [klass.send(:build_default_scope, relation)].compact
-
- rel = scope_chain_items.inject(scope_chain_items.shift) do |left, right|
- left.merge right
- end
-
- if rel && !rel.arel.constraints.empty?
- binds += rel.bound_attributes
- constraint = constraint.and rel.arel.constraints
- end
+ joins << table.create_join(table, table.create_on(constraint), join_type)
- if reflection.type
- value = foreign_klass.base_class.name
- column = klass.columns_hash[reflection.type.to_s]
+ join_scope = reflection.join_scope(table, foreign_klass)
+ arel = join_scope.arel(alias_tracker.aliases)
- substitute = klass.connection.substitute_at(column)
- binds << Relation::QueryAttribute.new(column.name, value, klass.type_for_attribute(column.name))
- constraint = constraint.and table[reflection.type].eq substitute
+ if arel.constraints.any?
+ joins.concat arel.join_sources
+ right = joins.last.right
+ right.expr = right.expr.and(arel.constraints)
end
- joins << table.create_join(table, table.create_on(constraint), join_type)
-
# The current table in this iteration becomes the foreign table in the next
foreign_table, foreign_klass = table, klass
end
- JoinInformation.new joins, binds
+ joins
end
- # Builds equality condition.
- #
- # Example:
- #
- # class Physician < ActiveRecord::Base
- # has_many :appointments
- # end
- #
- # If I execute `Physician.joins(:appointments).to_a` then
- # klass # => Physician
- # table # => #<Arel::Table @name="appointments" ...>
- # key # => physician_id
- # foreign_table # => #<Arel::Table @name="physicians" ...>
- # foreign_key # => id
- #
- def build_constraint(klass, table, key, foreign_table, foreign_key)
- constraint = table[key].eq(foreign_table[foreign_key])
-
- if klass.finder_needs_type_condition?
- constraint = table.create_and([
- constraint,
- klass.send(:type_condition, table)
- ])
- end
-
- constraint
+ def tables=(tables)
+ @tables = tables
+ @table = tables.first
end
- def table
- tables.first
- end
+ def readonly?
+ return @readonly if defined?(@readonly)
- def aliased_table_name
- table.table_alias || table.name
+ @readonly = reflection.scope && reflection.scope_for(base_klass.unscoped).readonly_value
end
end
end
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_base.rb b/activerecord/lib/active_record/associations/join_dependency/join_base.rb
index 3a26c25737..988b4e8fa2 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_base.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_base.rb
@@ -1,20 +1,21 @@
-require 'active_record/associations/join_dependency/join_part'
+# frozen_string_literal: true
+
+require "active_record/associations/join_dependency/join_part"
module ActiveRecord
module Associations
class JoinDependency # :nodoc:
class JoinBase < JoinPart # :nodoc:
- def match?(other)
- return true if self == other
- super && base_klass == other.base_klass
- end
+ attr_reader :table
- def table
- base_klass.arel_table
+ def initialize(base_klass, table, children)
+ super(base_klass, children)
+ @table = table
end
- def aliased_table_name
- base_klass.table_name
+ def match?(other)
+ return true if self == other
+ super && base_klass == other.base_klass
end
end
end
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_part.rb b/activerecord/lib/active_record/associations/join_dependency/join_part.rb
index 9c6573f913..3ad72a3646 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Associations
class JoinDependency # :nodoc:
@@ -15,17 +17,13 @@ module ActiveRecord
# association.
attr_reader :base_klass, :children
- delegate :table_name, :column_names, :primary_key, :to => :base_klass
+ delegate :table_name, :column_names, :primary_key, to: :base_klass
def initialize(base_klass, children)
@base_klass = base_klass
@children = children
end
- def name
- reflection.name
- end
-
def match?(other)
self.class == other.class
end
@@ -35,13 +33,15 @@ module ActiveRecord
children.each { |child| child.each(&block) }
end
- # An Arel::Table for the active_record
- def table
- raise NotImplementedError
+ def each_children(&block)
+ children.each do |child|
+ yield self, child
+ child.each_children(&block)
+ end
end
- # The alias for the active_record's table
- def aliased_table_name
+ # An Arel::Table for the active_record
+ def table
raise NotImplementedError
end
@@ -54,16 +54,16 @@ module ActiveRecord
length = column_names_with_alias.length
while index < length
- column_name, alias_name = column_names_with_alias[index]
- hash[column_name] = row[alias_name]
+ column = column_names_with_alias[index]
+ hash[column.name] = row[column.alias]
index += 1
end
hash
end
- def instantiate(row, aliases)
- base_klass.instantiate(extract_record(row, aliases))
+ def instantiate(row, aliases, &block)
+ base_klass.instantiate(extract_record(row, aliases), &block)
end
end
end
diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb
index 6ecc741195..8997579527 100644
--- a/activerecord/lib/active_record/associations/preloader.rb
+++ b/activerecord/lib/active_record/associations/preloader.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Associations
# Implements the details of eager loading of Active Record associations.
@@ -42,16 +44,8 @@ module ActiveRecord
extend ActiveSupport::Autoload
eager_autoload do
- autoload :Association, 'active_record/associations/preloader/association'
- autoload :SingularAssociation, 'active_record/associations/preloader/singular_association'
- autoload :CollectionAssociation, 'active_record/associations/preloader/collection_association'
- autoload :ThroughAssociation, 'active_record/associations/preloader/through_association'
-
- autoload :HasMany, 'active_record/associations/preloader/has_many'
- autoload :HasManyThrough, 'active_record/associations/preloader/has_many_through'
- autoload :HasOne, 'active_record/associations/preloader/has_one'
- autoload :HasOneThrough, 'active_record/associations/preloader/has_one_through'
- autoload :BelongsTo, 'active_record/associations/preloader/belongs_to'
+ autoload :Association, "active_record/associations/preloader/association"
+ autoload :ThroughAssociation, "active_record/associations/preloader/through_association"
end
# Eager loads the named associations for the given Active Record record(s).
@@ -88,18 +82,13 @@ module ActiveRecord
# [ :books, :author ]
# { author: :avatar }
# [ :books, { author: :avatar } ]
-
- NULL_RELATION = Struct.new(:values, :where_clause, :joins_values).new({}, Relation::WhereClause.empty, [])
-
def preload(records, associations, preload_scope = nil)
- records = Array.wrap(records).compact.uniq
- associations = Array.wrap(associations)
- preload_scope = preload_scope || NULL_RELATION
+ records = Array.wrap(records).compact
if records.empty?
[]
else
- associations.flat_map { |association|
+ Array.wrap(associations).flat_map { |association|
preloaders_on association, records, preload_scope
}
end
@@ -107,97 +96,101 @@ module ActiveRecord
private
- def preloaders_on(association, records, scope)
- case association
- when Hash
- preloaders_for_hash(association, records, scope)
- when Symbol
- preloaders_for_one(association, records, scope)
- when String
- preloaders_for_one(association.to_sym, records, scope)
- else
- raise ArgumentError, "#{association.inspect} was not recognised for preload"
+ # Loads all the given data into +records+ for the +association+.
+ def preloaders_on(association, records, scope, polymorphic_parent = false)
+ case association
+ when Hash
+ preloaders_for_hash(association, records, scope, polymorphic_parent)
+ when Symbol, String
+ preloaders_for_one(association, records, scope, polymorphic_parent)
+ else
+ raise ArgumentError, "#{association.inspect} was not recognized for preload"
+ end
end
- end
-
- def preloaders_for_hash(association, records, scope)
- association.flat_map { |parent, child|
- loaders = preloaders_for_one parent, records, scope
- recs = loaders.flat_map(&:preloaded_records).uniq
- loaders.concat Array.wrap(child).flat_map { |assoc|
- preloaders_on assoc, recs, scope
+ def preloaders_for_hash(association, records, scope, polymorphic_parent)
+ association.flat_map { |parent, child|
+ grouped_records(parent, records, polymorphic_parent).flat_map do |reflection, reflection_records|
+ loaders = preloaders_for_reflection(reflection, reflection_records, scope)
+ recs = loaders.flat_map(&:preloaded_records)
+ child_polymorphic_parent = reflection && reflection.options[:polymorphic]
+ loaders.concat Array.wrap(child).flat_map { |assoc|
+ preloaders_on assoc, recs, scope, child_polymorphic_parent
+ }
+ loaders
+ end
}
- loaders
- }
- end
+ end
- # Not all records have the same class, so group then preload group on the reflection
- # itself so that if various subclass share the same association then we do not split
- # them unnecessarily
- #
- # Additionally, polymorphic belongs_to associations can have multiple associated
- # classes, depending on the polymorphic_type field. So we group by the classes as
- # well.
- def preloaders_for_one(association, records, scope)
- grouped_records(association, records).flat_map do |reflection, klasses|
- klasses.map do |rhs_klass, rs|
- loader = preloader_for(reflection, rs, rhs_klass).new(rhs_klass, rs, reflection, scope)
+ # Loads all the given data into +records+ for a singular +association+.
+ #
+ # Functions by instantiating a preloader class such as Preloader::Association and
+ # call the +run+ method for each passed in class in the +records+ argument.
+ #
+ # Not all records have the same class, so group then preload group on the reflection
+ # itself so that if various subclass share the same association then we do not split
+ # them unnecessarily
+ #
+ # Additionally, polymorphic belongs_to associations can have multiple associated
+ # classes, depending on the polymorphic_type field. So we group by the classes as
+ # well.
+ def preloaders_for_one(association, records, scope, polymorphic_parent)
+ grouped_records(association, records, polymorphic_parent)
+ .flat_map do |reflection, reflection_records|
+ preloaders_for_reflection reflection, reflection_records, scope
+ end
+ end
+
+ def preloaders_for_reflection(reflection, records, scope)
+ records.group_by { |record| record.association(reflection.name).klass }.map do |rhs_klass, rs|
+ loader = preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope)
loader.run self
loader
end
end
- end
- def grouped_records(association, records)
- h = {}
- records.each do |record|
- next unless record
- assoc = record.association(association)
- klasses = h[assoc.reflection] ||= {}
- (klasses[assoc.klass] ||= []) << record
+ def grouped_records(association, records, polymorphic_parent)
+ h = {}
+ records.each do |record|
+ next unless record
+ reflection = record.class._reflect_on_association(association)
+ next if polymorphic_parent && !reflection || !record.association(association).klass
+ (h[reflection] ||= []) << record
+ end
+ h
end
- h
- end
- class AlreadyLoaded # :nodoc:
- attr_reader :owners, :reflection
+ class AlreadyLoaded # :nodoc:
+ def initialize(klass, owners, reflection, preload_scope)
+ @owners = owners
+ @reflection = reflection
+ end
- def initialize(klass, owners, reflection, preload_scope)
- @owners = owners
- @reflection = reflection
- end
+ def run(preloader); end
- def run(preloader); end
+ def preloaded_records
+ owners.flat_map { |owner| owner.association(reflection.name).target }
+ end
- def preloaded_records
- owners.flat_map { |owner| owner.association(reflection.name).target }
+ private
+ attr_reader :owners, :reflection
end
- end
- class NullPreloader # :nodoc:
- def self.new(klass, owners, reflection, preload_scope); self; end
- def self.run(preloader); end
- def self.preloaded_records; []; end
- end
-
- def preloader_for(reflection, owners, rhs_klass)
- return NullPreloader unless rhs_klass
+ # Returns a class containing the logic needed to load preload the data
+ # and attach it to a relation. The class returned implements a `run` method
+ # that accepts a preloader.
+ def preloader_for(reflection, owners)
+ if owners.first.association(reflection.name).loaded?
+ return AlreadyLoaded
+ end
+ reflection.check_preloadable!
- if owners.first.association(reflection.name).loaded?
- return AlreadyLoaded
- end
- reflection.check_preloadable!
-
- case reflection.macro
- when :has_many
- reflection.options[:through] ? HasManyThrough : HasMany
- when :has_one
- reflection.options[:through] ? HasOneThrough : HasOne
- when :belongs_to
- BelongsTo
+ if reflection.options[:through]
+ ThroughAssociation
+ else
+ Association
+ end
end
- end
end
end
end
diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb
index 1dc8bff193..d6f7359055 100644
--- a/activerecord/lib/active_record/associations/preloader/association.rb
+++ b/activerecord/lib/active_record/associations/preloader/association.rb
@@ -1,8 +1,9 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Associations
class Preloader
class Association #:nodoc:
- attr_reader :owners, :reflection, :preload_scope, :model, :klass
attr_reader :preloaded_records
def initialize(klass, owners, reflection, preload_scope)
@@ -11,152 +12,118 @@ module ActiveRecord
@reflection = reflection
@preload_scope = preload_scope
@model = owners.first && owners.first.class
- @scope = nil
- @owners_by_key = nil
@preloaded_records = []
end
def run(preloader)
- preload(preloader)
- end
-
- def preload(preloader)
- raise NotImplementedError
- end
+ records = load_records do |record|
+ owner = owners_by_key[convert_key(record[association_key_name])]
+ association = owner.association(reflection.name)
+ association.set_inverse_instance(record)
+ end
- def scope
- @scope ||= build_scope
+ owners.each do |owner|
+ associate_records_to_owner(owner, records[convert_key(owner[owner_key_name])] || [])
+ end
end
- def records_for(ids)
- query_scope(ids)
- end
+ private
+ attr_reader :owners, :reflection, :preload_scope, :model, :klass
- def query_scope(ids)
- scope.where(association_key_name => ids)
- end
+ # The name of the key on the associated records
+ def association_key_name
+ reflection.join_primary_key(klass)
+ end
- def table
- klass.arel_table
- end
+ # The name of the key on the model which declares the association
+ def owner_key_name
+ reflection.join_foreign_key
+ end
- # The name of the key on the associated records
- def association_key_name
- raise NotImplementedError
- end
+ def associate_records_to_owner(owner, records)
+ association = owner.association(reflection.name)
+ association.loaded!
+ if reflection.collection?
+ association.target.concat(records)
+ else
+ association.target = records.first unless records.empty?
+ end
+ end
- # This is overridden by HABTM as the condition should be on the foreign_key column in
- # the join table
- def association_key
- table[association_key_name]
- end
+ def owner_keys
+ @owner_keys ||= owners_by_key.keys
+ end
- # The name of the key on the model which declares the association
- def owner_key_name
- raise NotImplementedError
- end
+ def owners_by_key
+ unless defined?(@owners_by_key)
+ @owners_by_key = owners.each_with_object({}) do |owner, h|
+ key = convert_key(owner[owner_key_name])
+ h[key] = owner if key
+ end
+ end
+ @owners_by_key
+ end
- def owners_by_key
- @owners_by_key ||= if key_conversion_required?
- owners.group_by do |owner|
- owner[owner_key_name].to_s
- end
- else
- owners.group_by do |owner|
- owner[owner_key_name]
- end
- end
- end
+ def key_conversion_required?
+ unless defined?(@key_conversion_required)
+ @key_conversion_required = (association_key_type != owner_key_type)
+ end
- def options
- reflection.options
- end
+ @key_conversion_required
+ end
- private
+ def convert_key(key)
+ if key_conversion_required?
+ key.to_s
+ else
+ key
+ end
+ end
- def associated_records_by_owner(preloader)
- owners_map = owners_by_key
- owner_keys = owners_map.keys.compact
+ def association_key_type
+ @klass.type_for_attribute(association_key_name).type
+ end
- # Each record may have multiple owners, and vice-versa
- records_by_owner = owners.each_with_object({}) do |owner,h|
- h[owner] = []
+ def owner_key_type
+ @model.type_for_attribute(owner_key_name).type
end
- if owner_keys.any?
+ def load_records(&block)
+ return {} if owner_keys.empty?
# Some databases impose a limit on the number of ids in a list (in Oracle it's 1000)
# Make several smaller queries if necessary or make one query if the adapter supports it
- sliced = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size)
-
- records = load_slices sliced
- records.each do |record, owner_key|
- owners_map[owner_key].each do |owner|
- records_by_owner[owner] << record
- end
+ slices = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size)
+ @preloaded_records = slices.flat_map do |slice|
+ records_for(slice, &block)
+ end
+ @preloaded_records.group_by do |record|
+ convert_key(record[association_key_name])
end
end
- records_by_owner
- end
-
- def key_conversion_required?
- association_key_type != owner_key_type
- end
-
- def association_key_type
- @klass.type_for_attribute(association_key_name.to_s).type
- end
-
- def owner_key_type
- @model.type_for_attribute(owner_key_name.to_s).type
- end
-
- def load_slices(slices)
- @preloaded_records = slices.flat_map { |slice|
- records_for(slice)
- }
-
- @preloaded_records.map { |record|
- key = record[association_key_name]
- key = key.to_s if key_conversion_required?
-
- [record, key]
- }
- end
-
- def reflection_scope
- @reflection_scope ||= reflection.scope ? klass.unscoped.instance_exec(nil, &reflection.scope) : klass.unscoped
- end
-
- def build_scope
- scope = klass.unscoped
-
- values = reflection_scope.values
- preload_values = preload_scope.values
-
- scope.where_clause = reflection_scope.where_clause + preload_scope.where_clause
- scope.references_values = Array(values[:references]) + Array(preload_values[:references])
-
- scope._select! preload_values[:select] || values[:select] || table[Arel.star]
- scope.includes! preload_values[:includes] || values[:includes]
- if preload_scope.joins_values.any?
- scope.joins!(preload_scope.joins_values)
- else
- scope.joins!(reflection_scope.joins_values)
+ def records_for(ids, &block)
+ scope.where(association_key_name => ids).load(&block)
end
- scope.order! preload_values[:order] || values[:order]
- if preload_values[:readonly] || values[:readonly]
- scope.readonly!
+ def scope
+ @scope ||= build_scope
end
- if options[:as]
- scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name })
+ def reflection_scope
+ @reflection_scope ||= reflection.scope ? reflection.scope_for(klass.unscoped) : klass.unscoped
end
- scope.unscope_values = Array(values[:unscope])
- klass.default_scoped.merge(scope)
- end
+ def build_scope
+ scope = klass.scope_for_association
+
+ if reflection.type
+ scope.where!(reflection.type => model.polymorphic_name)
+ end
+
+ scope.merge!(reflection_scope) if reflection.scope
+ scope.merge!(preload_scope) if preload_scope
+ scope
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/preloader/belongs_to.rb b/activerecord/lib/active_record/associations/preloader/belongs_to.rb
deleted file mode 100644
index 5091d4717a..0000000000
--- a/activerecord/lib/active_record/associations/preloader/belongs_to.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-module ActiveRecord
- module Associations
- class Preloader
- class BelongsTo < SingularAssociation #:nodoc:
-
- def association_key_name
- reflection.options[:primary_key] || klass && klass.primary_key
- end
-
- def owner_key_name
- reflection.foreign_key
- end
-
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/preloader/collection_association.rb b/activerecord/lib/active_record/associations/preloader/collection_association.rb
deleted file mode 100644
index 5adffcd831..0000000000
--- a/activerecord/lib/active_record/associations/preloader/collection_association.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-module ActiveRecord
- module Associations
- class Preloader
- class CollectionAssociation < Association #:nodoc:
-
- private
-
- def build_scope
- super.order(preload_scope.values[:order] || reflection_scope.values[:order])
- end
-
- def preload(preloader)
- associated_records_by_owner(preloader).each do |owner, records|
- association = owner.association(reflection.name)
- association.loaded!
- association.target.concat(records)
- records.each { |record| association.set_inverse_instance(record) }
- end
- end
-
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/preloader/has_many.rb b/activerecord/lib/active_record/associations/preloader/has_many.rb
deleted file mode 100644
index 3ea91a8c11..0000000000
--- a/activerecord/lib/active_record/associations/preloader/has_many.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-module ActiveRecord
- module Associations
- class Preloader
- class HasMany < CollectionAssociation #:nodoc:
-
- def association_key_name
- reflection.foreign_key
- end
-
- def owner_key_name
- reflection.active_record_primary_key
- end
-
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/preloader/has_many_through.rb b/activerecord/lib/active_record/associations/preloader/has_many_through.rb
deleted file mode 100644
index 2029871f39..0000000000
--- a/activerecord/lib/active_record/associations/preloader/has_many_through.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-module ActiveRecord
- module Associations
- class Preloader
- class HasManyThrough < CollectionAssociation #:nodoc:
- include ThroughAssociation
-
- def associated_records_by_owner(preloader)
- records_by_owner = super
-
- if reflection_scope.distinct_value
- records_by_owner.each_value(&:uniq!)
- end
-
- records_by_owner
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/preloader/has_one.rb b/activerecord/lib/active_record/associations/preloader/has_one.rb
deleted file mode 100644
index 24728e9f01..0000000000
--- a/activerecord/lib/active_record/associations/preloader/has_one.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-module ActiveRecord
- module Associations
- class Preloader
- class HasOne < SingularAssociation #:nodoc:
-
- def association_key_name
- reflection.foreign_key
- end
-
- def owner_key_name
- reflection.active_record_primary_key
- end
-
- private
-
- def build_scope
- super.order(preload_scope.values[:order] || reflection_scope.values[:order])
- end
-
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/preloader/has_one_through.rb b/activerecord/lib/active_record/associations/preloader/has_one_through.rb
deleted file mode 100644
index f063f85574..0000000000
--- a/activerecord/lib/active_record/associations/preloader/has_one_through.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-module ActiveRecord
- module Associations
- class Preloader
- class HasOneThrough < SingularAssociation #:nodoc:
- include ThroughAssociation
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/preloader/singular_association.rb b/activerecord/lib/active_record/associations/preloader/singular_association.rb
deleted file mode 100644
index f60647a81e..0000000000
--- a/activerecord/lib/active_record/associations/preloader/singular_association.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-module ActiveRecord
- module Associations
- class Preloader
- class SingularAssociation < Association #:nodoc:
-
- private
-
- def preload(preloader)
- associated_records_by_owner(preloader).each do |owner, associated_records|
- record = associated_records.first
-
- association = owner.association(reflection.name)
- association.target = record
- association.set_inverse_instance(record) if record
- end
- end
-
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb
index 56aa23b173..a6b7ab80a2 100644
--- a/activerecord/lib/active_record/associations/preloader/through_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/through_association.rb
@@ -1,94 +1,106 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Associations
class Preloader
- module ThroughAssociation #:nodoc:
- def through_reflection
- reflection.through_reflection
- end
-
- def source_reflection
- reflection.source_reflection
- end
-
- def associated_records_by_owner(preloader)
- preloader.preload(owners,
- through_reflection.name,
- through_scope)
-
- through_records = owners.map do |owner|
- association = owner.association through_reflection.name
-
- [owner, Array(association.reader)]
- end
-
- reset_association owners, through_reflection.name
-
- middle_records = through_records.flat_map { |(_,rec)| rec }
-
- preloaders = preloader.preload(middle_records,
- source_reflection.name,
- reflection_scope)
-
+ class ThroughAssociation < Association # :nodoc:
+ def run(preloader)
+ already_loaded = owners.first.association(through_reflection.name).loaded?
+ through_scope = through_scope()
+ reflection_scope = target_reflection_scope
+ through_preloaders = preloader.preload(owners, through_reflection.name, through_scope)
+ middle_records = through_preloaders.flat_map(&:preloaded_records)
+ preloaders = preloader.preload(middle_records, source_reflection.name, reflection_scope)
@preloaded_records = preloaders.flat_map(&:preloaded_records)
- middle_to_pl = preloaders.each_with_object({}) do |pl,h|
- pl.owners.each { |middle|
- h[middle] = pl
- }
- end
-
- record_offset = {}
- @preloaded_records.each_with_index do |record,i|
- record_offset[record] = i
- end
-
- through_records.each_with_object({}) { |(lhs,center),records_by_owner|
- pl_to_middle = center.group_by { |record| middle_to_pl[record] }
-
- records_by_owner[lhs] = pl_to_middle.flat_map do |pl, middles|
- rhs_records = middles.flat_map { |r|
- association = r.association source_reflection.name
-
- association.reader
- }.compact
-
- rhs_records.sort_by { |rhs| record_offset[rhs] }
+ owners.each do |owner|
+ through_records = Array(owner.association(through_reflection.name).target)
+ if already_loaded
+ if source_type = reflection.options[:source_type]
+ through_records = through_records.select do |record|
+ record[reflection.foreign_type] == source_type
+ end
+ end
+ else
+ owner.association(through_reflection.name).reset if through_scope
end
- }
+ result = through_records.flat_map do |record|
+ association = record.association(source_reflection.name)
+ target = association.target
+ association.reset if preload_scope
+ target
+ end
+ result.compact!
+ if reflection_scope
+ result.sort_by! { |rhs| preload_index[rhs] } if reflection_scope.order_values.any?
+ result.uniq! if reflection_scope.distinct_value
+ end
+ associate_records_to_owner(owner, result)
+ end
end
private
+ def through_reflection
+ reflection.through_reflection
+ end
- def reset_association(owners, association_name)
- should_reset = (through_scope != through_reflection.klass.unscoped) ||
- (reflection.options[:source_type] && through_reflection.collection?)
-
- # Don't cache the association - we would only be caching a subset
- if should_reset
- owners.each { |owner|
- owner.association(association_name).reset
- }
+ def source_reflection
+ reflection.source_reflection
end
- end
+ def preload_index
+ @preload_index ||= @preloaded_records.each_with_object({}).with_index do |(id, result), index|
+ result[id] = index
+ end
+ end
- def through_scope
- scope = through_reflection.klass.unscoped
+ def through_scope
+ scope = through_reflection.klass.unscoped
+ options = reflection.options
- if options[:source_type]
- scope.where! reflection.foreign_type => options[:source_type]
- else
- unless reflection_scope.where_clause.empty?
- scope.includes_values = Array(reflection_scope.values[:includes] || options[:source])
+ if options[:source_type]
+ scope.where! reflection.foreign_type => options[:source_type]
+ elsif !reflection_scope.where_clause.empty?
scope.where_clause = reflection_scope.where_clause
+ values = reflection_scope.values
+
+ if includes = values[:includes]
+ scope.includes!(source_reflection.name => includes)
+ else
+ scope.includes!(source_reflection.name)
+ end
+
+ if values[:references] && !values[:references].empty?
+ scope.references!(values[:references])
+ else
+ scope.references!(source_reflection.table_name)
+ end
+
+ if joins = values[:joins]
+ scope.joins!(source_reflection.name => joins)
+ end
+
+ if left_outer_joins = values[:left_outer_joins]
+ scope.left_outer_joins!(source_reflection.name => left_outer_joins)
+ end
+
+ if scope.eager_loading? && order_values = values[:order]
+ scope = scope.order(order_values)
+ end
end
- scope.references! reflection_scope.values[:references]
- scope = scope.order reflection_scope.values[:order] if scope.eager_loading?
+ scope unless scope.empty_scope?
end
- scope
- end
+ def target_reflection_scope
+ if preload_scope
+ reflection_scope.merge(preload_scope)
+ elsif reflection.scope
+ reflection_scope
+ else
+ nil
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb
index bec9505bd2..8e50cce102 100644
--- a/activerecord/lib/active_record/associations/singular_association.rb
+++ b/activerecord/lib/active_record/associations/singular_association.rb
@@ -1,11 +1,11 @@
+# frozen_string_literal: true
+
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
- klass.uncached { reload }
- elsif !loaded? || stale_target?
+ def reader
+ if !loaded? || stale_target?
reload
end
@@ -17,51 +17,40 @@ module ActiveRecord
replace(record)
end
- def create(attributes = {}, &block)
- _create_record(attributes, &block)
- end
-
- def create!(attributes = {}, &block)
- _create_record(attributes, true, &block)
- end
-
- def build(attributes = {})
- record = build_record(attributes)
- yield(record) if block_given?
+ def build(attributes = {}, &block)
+ record = build_record(attributes, &block)
set_new_record(record)
record
end
- private
+ # Implements the reload reader method, e.g. foo.reload_bar for
+ # Foo.has_one :bar
+ def force_reload_reader
+ reload(true)
+ target
+ end
- def create_scope
- scope.scope_for_create.stringify_keys.except(klass.primary_key)
+ private
+ def scope_for_create
+ super.except!(klass.primary_key)
end
- def get_records
- if reflection.scope_chain.any?(&:any?) ||
- scope.eager_loading? ||
- klass.scope_attributes?
-
- return scope.limit(1).to_a
- end
+ def find_target
+ scope = self.scope
+ return scope.take if skip_statement_cache?(scope)
conn = klass.connection
- sc = reflection.association_scope_cache(conn, owner) do
- StatementCache.create(conn) { |params|
- as = AssociationScope.create { params.bind }
- target_scope.merge(as.scope(self, conn)).limit(1)
- }
+ sc = reflection.association_scope_cache(conn, owner) do |params|
+ as = AssociationScope.create { params.bind }
+ target_scope.merge!(as.scope(self)).limit(1)
end
binds = AssociationScope.get_bind_values(owner, reflection.chain)
- sc.execute binds, klass, klass.connection
- end
-
- def find_target
- if record = get_records.first
+ sc.execute(binds, conn) do |record|
set_inverse_instance record
- end
+ end.first
+ rescue ::RangeError
+ nil
end
def replace(record)
@@ -72,9 +61,8 @@ module ActiveRecord
replace(record)
end
- def _create_record(attributes, raise_error = false)
- record = build_record(attributes)
- yield(record) if block_given?
+ def _create_record(attributes, raise_error = false, &block)
+ record = build_record(attributes, &block)
saved = record.save
set_new_record(record)
raise RecordInvalid.new(record) if !saved && raise_error
diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb
index 55ee9f04e0..15e6565e69 100644
--- a/activerecord/lib/active_record/associations/through_association.rb
+++ b/activerecord/lib/active_record/associations/through_association.rb
@@ -1,11 +1,27 @@
+# frozen_string_literal: true
+
module ActiveRecord
- # = Active Record Through Association
module Associations
+ # = Active Record Through Association
module ThroughAssociation #:nodoc:
+ delegate :source_reflection, to: :reflection
+
+ private
+ def through_reflection
+ @through_reflection ||= begin
+ refl = reflection.through_reflection
- delegate :source_reflection, :through_reflection, :to => :reflection
+ while refl.through_reflection?
+ refl = refl.through_reflection
+ end
+
+ refl
+ end
+ end
- protected
+ def through_association
+ @through_association ||= owner.association(through_reflection.name)
+ end
# We merge in these scopes for two reasons:
#
@@ -14,7 +30,7 @@ module ActiveRecord
def target_scope
scope = super
reflection.chain.drop(1).each do |reflection|
- relation = reflection.klass.all
+ relation = reflection.klass.scope_for_association
scope.merge!(
relation.except(:select, :create_with, :includes, :preload, :joins, :eager_load)
)
@@ -22,8 +38,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.
#
@@ -39,24 +53,22 @@ module ActiveRecord
def construct_join_attributes(*records)
ensure_mutable
- if source_reflection.association_primary_key(reflection.klass) == reflection.klass.primary_key
+ association_primary_key = source_reflection.association_primary_key(reflection.klass)
+
+ if association_primary_key == reflection.klass.primary_key && !options[:source_type]
join_attributes = { source_reflection.name => records }
else
join_attributes = {
- source_reflection.foreign_key =>
- records.map { |record|
- record.send(source_reflection.association_primary_key(reflection.klass))
- }
+ source_reflection.foreign_key => records.map(&association_primary_key.to_sym)
}
end
if options[:source_type]
- join_attributes[source_reflection.foreign_type] =
- records.map { |record| record.class.base_class.name }
+ join_attributes[source_reflection.foreign_type] = [ options[:source_type] ]
end
if records.count == 1
- Hash[join_attributes.map { |k, v| [k, v.first] }]
+ join_attributes.transform_values!(&:first)
else
join_attributes
end
@@ -76,13 +88,21 @@ module ActiveRecord
def ensure_mutable
unless source_reflection.belongs_to?
- raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection)
+ if reflection.has_one?
+ raise HasOneThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection)
+ else
+ raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection)
+ end
end
end
def ensure_not_nested
if reflection.nested?
- raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection)
+ if reflection.has_one?
+ raise HasOneThroughNestedAssociationsAreReadonly.new(owner, reflection)
+ else
+ raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection)
+ end
end
end
@@ -94,7 +114,7 @@ module ActiveRecord
attributes[inverse.foreign_key] = target.id
end
- super(attributes)
+ super
end
end
end
diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb
deleted file mode 100644
index 73dd3fa041..0000000000
--- a/activerecord/lib/active_record/attribute.rb
+++ /dev/null
@@ -1,174 +0,0 @@
-module ActiveRecord
- class Attribute # :nodoc:
- class << self
- def from_database(name, value, type)
- FromDatabase.new(name, value, type)
- end
-
- def from_user(name, value, type)
- FromUser.new(name, value, type)
- end
-
- def with_cast_value(name, value, type)
- WithCastValue.new(name, value, type)
- end
-
- def null(name)
- Null.new(name)
- end
-
- def uninitialized(name, type)
- Uninitialized.new(name, type)
- end
- end
-
- attr_reader :name, :value_before_type_cast, :type
-
- # This method should not be called directly.
- # Use #from_database or #from_user
- def initialize(name, value_before_type_cast, type)
- @name = name
- @value_before_type_cast = value_before_type_cast
- @type = type
- end
-
- def value
- # `defined?` is cheaper than `||=` when we get back falsy values
- @value = original_value unless defined?(@value)
- @value
- end
-
- def original_value
- type_cast(value_before_type_cast)
- end
-
- def value_for_database
- type.serialize(value)
- end
-
- def changed_from?(old_value)
- type.changed?(old_value, value, value_before_type_cast)
- end
-
- def changed_in_place_from?(old_value)
- has_been_read? && type.changed_in_place?(old_value, value)
- end
-
- def with_value_from_user(value)
- self.class.from_user(name, value, type)
- end
-
- def with_value_from_database(value)
- self.class.from_database(name, value, type)
- end
-
- def with_cast_value(value)
- self.class.with_cast_value(name, value, type)
- end
-
- def with_type(type)
- self.class.new(name, value_before_type_cast, type)
- end
-
- def type_cast(*)
- raise NotImplementedError
- end
-
- def initialized?
- true
- end
-
- def came_from_user?
- false
- end
-
- def has_been_read?
- defined?(@value)
- end
-
- def ==(other)
- self.class == other.class &&
- name == other.name &&
- value_before_type_cast == other.value_before_type_cast &&
- type == other.type
- end
- alias eql? ==
-
- def hash
- [self.class, name, value_before_type_cast, type].hash
- end
-
- protected
-
- def initialize_dup(other)
- if defined?(@value) && @value.duplicable?
- @value = @value.dup
- end
- end
-
- class FromDatabase < Attribute # :nodoc:
- def type_cast(value)
- type.deserialize(value)
- end
- end
-
- class FromUser < Attribute # :nodoc:
- def type_cast(value)
- type.cast(value)
- end
-
- def came_from_user?
- true
- end
- end
-
- class WithCastValue < Attribute # :nodoc:
- def type_cast(value)
- value
- end
-
- def changed_in_place_from?(old_value)
- false
- end
- end
-
- class Null < Attribute # :nodoc:
- def initialize(name)
- super(name, nil, Type::Value.new)
- end
-
- def value
- nil
- end
-
- def with_type(type)
- self.class.with_cast_value(name, nil, type)
- end
-
- def with_value_from_database(value)
- raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`"
- end
- alias_method :with_value_from_user, :with_value_from_database
- end
-
- class Uninitialized < Attribute # :nodoc:
- def initialize(name, type)
- super(name, nil, type)
- end
-
- def value
- if block_given?
- yield name
- end
- end
-
- def value_for_database
- end
-
- def initialized?
- false
- end
- end
- private_constant :FromDatabase, :FromUser, :Null, :Uninitialized, :WithCastValue
- end
-end
diff --git a/activerecord/lib/active_record/attribute/user_provided_default.rb b/activerecord/lib/active_record/attribute/user_provided_default.rb
deleted file mode 100644
index e0bee8c17e..0000000000
--- a/activerecord/lib/active_record/attribute/user_provided_default.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-require 'active_record/attribute'
-
-module ActiveRecord
- class Attribute # :nodoc:
- class UserProvidedDefault < FromUser
- def initialize(name, value, type, database_default)
- super(name, value, type)
- @database_default = database_default
- end
-
- def type_cast(value)
- if value.is_a?(Proc)
- super(value.call)
- else
- super
- end
- end
-
- def changed_in_place_from?(old_value)
- super || changed_from?(database_default.value)
- end
-
- def with_type(type)
- self.class.new(name, value_before_type_cast, type, database_default)
- end
-
- protected
-
- attr_reader :database_default
- end
- end
-end
diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb
index 45fdcaa1cd..b6f0e18764 100644
--- a/activerecord/lib/active_record/attribute_assignment.rb
+++ b/activerecord/lib/active_record/attribute_assignment.rb
@@ -1,99 +1,87 @@
-require 'active_model/forbidden_attributes_protection'
+# frozen_string_literal: true
+
+require "active_model/forbidden_attributes_protection"
module ActiveRecord
module AttributeAssignment
- extend ActiveSupport::Concern
include ActiveModel::AttributeAssignment
- # Alias for `assign_attributes`. See +ActiveModel::AttributeAssignment+.
- def attributes=(attributes)
- assign_attributes(attributes)
- end
-
private
- def _assign_attributes(attributes) # :nodoc:
- multi_parameter_attributes = {}
- nested_parameter_attributes = {}
+ def _assign_attributes(attributes)
+ multi_parameter_attributes = {}
+ nested_parameter_attributes = {}
- attributes.each do |k, v|
- if k.include?("(")
- multi_parameter_attributes[k] = attributes.delete(k)
- elsif v.is_a?(Hash)
- nested_parameter_attributes[k] = attributes.delete(k)
+ attributes.each do |k, v|
+ if k.include?("(")
+ multi_parameter_attributes[k] = attributes.delete(k)
+ elsif v.is_a?(Hash)
+ nested_parameter_attributes[k] = attributes.delete(k)
+ end
end
- end
- super(attributes)
+ super(attributes)
- assign_nested_parameter_attributes(nested_parameter_attributes) unless nested_parameter_attributes.empty?
- assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty?
- end
-
- # Tries to assign given value to given attribute.
- # In case of an error, re-raises with the ActiveRecord constant.
- def _assign_attribute(k, v) # :nodoc:
- super
- rescue ActiveModel::UnknownAttributeError
- raise UnknownAttributeError.new(self, k)
- end
+ assign_nested_parameter_attributes(nested_parameter_attributes) unless nested_parameter_attributes.empty?
+ assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty?
+ end
- # Assign any deferred nested attributes after the base attributes have been set.
- def assign_nested_parameter_attributes(pairs)
- pairs.each { |k, v| _assign_attribute(k, v) }
- end
+ # Assign any deferred nested attributes after the base attributes have been set.
+ def assign_nested_parameter_attributes(pairs)
+ pairs.each { |k, v| _assign_attribute(k, v) }
+ end
- # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done
- # by calling new on the column type or aggregation type (through composed_of) object with these parameters.
- # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
- # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the
- # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum and
- # f for Float. If all the values for a given attribute are empty, the attribute will be set to +nil+.
- def assign_multiparameter_attributes(pairs)
- execute_callstack_for_multiparameter_attributes(
- extract_callstack_for_multiparameter_attributes(pairs)
- )
- end
+ # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done
+ # by calling new on the column type or aggregation type (through composed_of) object with these parameters.
+ # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
+ # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the
+ # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Integer and
+ # f for Float. If all the values for a given attribute are empty, the attribute will be set to +nil+.
+ def assign_multiparameter_attributes(pairs)
+ execute_callstack_for_multiparameter_attributes(
+ extract_callstack_for_multiparameter_attributes(pairs)
+ )
+ end
- def execute_callstack_for_multiparameter_attributes(callstack)
- errors = []
- callstack.each do |name, values_with_empty_parameters|
- begin
- if values_with_empty_parameters.each_value.all?(&:nil?)
- values = nil
- else
- values = values_with_empty_parameters
+ def execute_callstack_for_multiparameter_attributes(callstack)
+ errors = []
+ callstack.each do |name, values_with_empty_parameters|
+ begin
+ if values_with_empty_parameters.each_value.all?(&:nil?)
+ values = nil
+ else
+ values = values_with_empty_parameters
+ end
+ send("#{name}=", values)
+ rescue => ex
+ errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name} (#{ex.message})", ex, name)
end
- send("#{name}=", values)
- rescue => ex
- errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name} (#{ex.message})", ex, name)
+ end
+ unless errors.empty?
+ error_descriptions = errors.map(&:message).join(",")
+ raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes [#{error_descriptions}]"
end
end
- unless errors.empty?
- error_descriptions = errors.map(&:message).join(",")
- raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes [#{error_descriptions}]"
- end
- end
- def extract_callstack_for_multiparameter_attributes(pairs)
- attributes = {}
+ def extract_callstack_for_multiparameter_attributes(pairs)
+ attributes = {}
- pairs.each do |(multiparameter_name, value)|
- attribute_name = multiparameter_name.split("(").first
- attributes[attribute_name] ||= {}
+ pairs.each do |(multiparameter_name, value)|
+ attribute_name = multiparameter_name.split("(").first
+ attributes[attribute_name] ||= {}
- parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value)
- attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value
- end
+ parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value)
+ attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value
+ end
- attributes
- end
+ attributes
+ end
- def type_cast_attribute_value(multiparameter_name, value)
- multiparameter_name =~ /\([0-9]*([if])\)/ ? value.send("to_" + $1) : value
- end
+ def type_cast_attribute_value(multiparameter_name, value)
+ multiparameter_name =~ /\([0-9]*([if])\)/ ? value.send("to_" + $1) : value
+ end
- def find_parameter_position(multiparameter_name)
- multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i
- end
+ def find_parameter_position(multiparameter_name)
+ multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i
+ end
end
end
diff --git a/activerecord/lib/active_record/attribute_decorators.rb b/activerecord/lib/active_record/attribute_decorators.rb
index 7d0ae32411..98b7805c0a 100644
--- a/activerecord/lib/active_record/attribute_decorators.rb
+++ b/activerecord/lib/active_record/attribute_decorators.rb
@@ -1,19 +1,42 @@
+# frozen_string_literal: true
+
module ActiveRecord
module AttributeDecorators # :nodoc:
extend ActiveSupport::Concern
included do
- class_attribute :attribute_type_decorations, instance_accessor: false # :internal:
- self.attribute_type_decorations = TypeDecorator.new
+ class_attribute :attribute_type_decorations, instance_accessor: false, default: TypeDecorator.new # :internal:
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
@@ -24,13 +47,13 @@ module ActiveRecord
private
- def load_schema!
- super
- attribute_types.each do |name, type|
- decorated_type = attribute_type_decorations.apply(name, type)
- define_attribute(name, decorated_type)
+ def load_schema!
+ super
+ attribute_types.each do |name, type|
+ decorated_type = attribute_type_decorations.apply(name, type)
+ define_attribute(name, decorated_type)
+ end
end
- end
end
class TypeDecorator # :nodoc:
@@ -53,15 +76,15 @@ module ActiveRecord
private
- def decorators_for(name, type)
- matching(name, type).map(&:last)
- end
+ def decorators_for(name, type)
+ matching(name, type).map(&:last)
+ end
- def matching(name, type)
- @decorations.values.select do |(matcher, _)|
- matcher.call(name, type)
+ def matching(name, type)
+ @decorations.values.select do |(matcher, _)|
+ matcher.call(name, type)
+ end
end
- end
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index 9d58a19304..fd8c1da842 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -1,7 +1,6 @@
-require 'active_support/core_ext/enumerable'
-require 'active_support/core_ext/string/filters'
-require 'mutex_m'
-require 'thread_safe'
+# frozen_string_literal: true
+
+require "mutex_m"
module ActiveRecord
# = Active Record Attribute Methods
@@ -23,43 +22,12 @@ module ActiveRecord
delegate :column_for_attribute, to: :class
end
- AttrNames = Module.new {
- def self.set_name_cache(name, value)
- const_name = "ATTR_#{name}"
- unless const_defined? const_name
- const_set const_name, value.dup.freeze
- end
- end
- }
-
- BLACKLISTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass)
+ RESTRICTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass)
- class AttributeMethodCache
- def initialize
- @module = Module.new
- @method_cache = ThreadSafe::Cache.new
- end
-
- def [](name)
- @method_cache.compute_if_absent(name) do
- safe_name = name.unpack('h*').first
- temp_method = "__temp__#{safe_name}"
- ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
- @module.module_eval method_body(temp_method, safe_name), __FILE__, __LINE__
- @module.instance_method temp_method
- end
- end
-
- private
-
- # Override this method in the subclasses for method body.
- def method_body(method_name, const_name)
- raise NotImplementedError, "Subclasses must implement a method_body(method_name, const_name) method."
- end
+ class GeneratedAttributeMethods < Module #:nodoc:
+ include Mutex_m
end
- class GeneratedAttributeMethods < Module; end # :nodoc:
-
module ClassMethods
def inherited(child_class) #:nodoc:
child_class.initialize_generated_modules
@@ -67,7 +35,7 @@ module ActiveRecord
end
def initialize_generated_modules # :nodoc:
- @generated_attribute_methods = GeneratedAttributeMethods.new { extend Mutex_m }
+ @generated_attribute_methods = GeneratedAttributeMethods.new
@attribute_methods_generated = false
include @generated_attribute_methods
@@ -82,11 +50,10 @@ module ActiveRecord
# attribute methods.
generated_attribute_methods.synchronize do
return false if @attribute_methods_generated
- superclass.define_attribute_methods unless self == base_class
+ superclass.define_attribute_methods unless base_class?
super(attribute_names)
@attribute_methods_generated = true
end
- true
end
def undefine_attribute_methods # :nodoc:
@@ -96,7 +63,7 @@ module ActiveRecord
end
end
- # Raises a <tt>ActiveRecord::DangerousAttributeError</tt> exception when an
+ # Raises an ActiveRecord::DangerousAttributeError exception when an
# \Active \Record method is defined in the model, otherwise +false+.
#
# class Person < ActiveRecord::Base
@@ -106,7 +73,7 @@ module ActiveRecord
# end
#
# Person.instance_method_already_implemented?(:save)
- # # => ActiveRecord::DangerousAttributeError: save is defined by ActiveRecord
+ # # => ActiveRecord::DangerousAttributeError: save is defined by Active Record. Check to make sure that you don't have an attribute or method with the same name.
#
# Person.instance_method_already_implemented?(:name)
# # => false
@@ -147,7 +114,7 @@ module ActiveRecord
# A class method is 'dangerous' if it is already (re)defined by Active Record, but
# not by any ancestors. (So 'puts' is not dangerous but 'new' is.)
def dangerous_class_method?(method_name)
- BLACKLISTED_CLASS_METHODS.include?(method_name.to_s) || class_method_defined_within?(method_name, Base)
+ RESTRICTED_CLASS_METHODS.include?(method_name.to_s) || class_method_defined_within?(method_name, Base)
end
def class_method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc:
@@ -172,7 +139,7 @@ module ActiveRecord
# Person.attribute_method?(:age=) # => true
# Person.attribute_method?(:nothing) # => false
def attribute_method?(attribute)
- super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, '')))
+ super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, "")))
end
# Returns an array of column names as strings if it's not an abstract class and
@@ -185,10 +152,73 @@ module ActiveRecord
# # => ["id", "created_at", "updated_at", "name", "age"]
def attribute_names
@attribute_names ||= if !abstract_class? && table_exists?
- attribute_types.keys
- else
- []
- end
+ attribute_types.keys
+ else
+ []
+ end
+ end
+
+ # Regexp for column names (with or without a table name prefix). Matches
+ # the following:
+ # "#{table_name}.#{column_name}"
+ # "#{column_name}"
+ COLUMN_NAME = /\A(?:\w+\.)?\w+\z/i
+
+ # Regexp for column names with order (with or without a table name
+ # prefix, with or without various order modifiers). Matches the following:
+ # "#{table_name}.#{column_name}"
+ # "#{table_name}.#{column_name} #{direction}"
+ # "#{table_name}.#{column_name} #{direction} NULLS FIRST"
+ # "#{table_name}.#{column_name} NULLS LAST"
+ # "#{column_name}"
+ # "#{column_name} #{direction}"
+ # "#{column_name} #{direction} NULLS FIRST"
+ # "#{column_name} NULLS LAST"
+ COLUMN_NAME_WITH_ORDER = /
+ \A
+ (?:\w+\.)?
+ \w+
+ (?:\s+asc|\s+desc)?
+ (?:\s+nulls\s+(?:first|last))?
+ \z
+ /ix
+
+ def disallow_raw_sql!(args, permit: COLUMN_NAME) # :nodoc:
+ unexpected = args.reject do |arg|
+ Arel.arel_node?(arg) ||
+ arg.to_s.split(/\s*,\s*/).all? { |part| permit.match?(part) }
+ end
+
+ return if unexpected.none?
+
+ if allow_unsafe_raw_sql == :deprecated
+ ActiveSupport::Deprecation.warn(
+ "Dangerous query method (method whose arguments are used as raw " \
+ "SQL) called with non-attribute argument(s): " \
+ "#{unexpected.map(&:inspect).join(", ")}. Non-attribute " \
+ "arguments will be disallowed in Rails 6.0. This method should " \
+ "not be called with user-provided values, such as request " \
+ "parameters or model attributes. Known-safe values can be passed " \
+ "by wrapping them in Arel.sql()."
+ )
+ else
+ raise(ActiveRecord::UnknownAttributeReference,
+ "Query method called with non-attribute argument(s): " +
+ unexpected.map(&:inspect).join(", ")
+ )
+ end
+ end
+
+ # Returns true if the given attribute exists, otherwise false.
+ #
+ # class Person < ActiveRecord::Base
+ # end
+ #
+ # Person.has_attribute?('name') # => true
+ # Person.has_attribute?(:age) # => true
+ # Person.has_attribute?(:nothing) # => false
+ def has_attribute?(attr_name)
+ attribute_types.key?(attr_name.to_s)
end
# Returns the column object for the named attribute.
@@ -221,26 +251,27 @@ module ActiveRecord
# end
#
# person = Person.new
- # person.respond_to(:name) # => true
- # person.respond_to(:name=) # => true
- # person.respond_to(:name?) # => true
- # person.respond_to('age') # => true
- # person.respond_to('age=') # => true
- # person.respond_to('age?') # => true
- # person.respond_to(:nothing) # => false
+ # person.respond_to?(:name) # => true
+ # person.respond_to?(:name=) # => true
+ # person.respond_to?(:name?) # => true
+ # person.respond_to?('age') # => true
+ # person.respond_to?('age=') # => true
+ # person.respond_to?('age?') # => true
+ # person.respond_to?(:nothing) # => false
def respond_to?(name, include_private = false)
return false unless super
- name = name.to_s
# If the result is true then check for the select case.
# For queries selecting a subset of columns, return false for unselected columns.
# We check defined?(@attributes) not to issue warnings if called on objects that
# have been allocated but not yet initialized.
- if defined?(@attributes) && self.class.column_names.include?(name)
- return has_attribute?(name)
+ if defined?(@attributes)
+ if name = self.class.symbol_column_to_string(name.to_sym)
+ return has_attribute?(name)
+ end
end
- return true
+ true
end
# Returns +true+ if the given attribute is in the attributes hash, otherwise +false+.
@@ -283,9 +314,8 @@ module ActiveRecord
# Returns an <tt>#inspect</tt>-like string for the value of the
# attribute +attr_name+. String attributes are truncated up to 50
# characters, Date and Time attributes are returned in the
- # <tt>:db</tt> format, Array attributes are truncated up to 10 values.
- # Other attributes return the value of <tt>#inspect</tt> without
- # modification.
+ # <tt>:db</tt> format. Other attributes return the value of
+ # <tt>#inspect</tt> without modification.
#
# person = Person.create!(name: 'David Heinemeier Hansson ' * 3)
#
@@ -296,20 +326,10 @@ module ActiveRecord
# # => "\"2012-10-22 00:15:07\""
#
# person.attribute_for_inspect(:tag_ids)
- # # => "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]"
+ # # => "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]"
def attribute_for_inspect(attr_name)
- value = read_attribute(attr_name)
-
- if value.is_a?(String) && value.length > 50
- "#{value[0, 50]}...".inspect
- elsif value.is_a?(Date) || value.is_a?(Time)
- %("#{value.to_s(:db)}")
- elsif value.is_a?(Array) && value.size > 10
- inspected = value.first(10).inspect
- %(#{inspected[0...-1]}, ...])
- else
- value.inspect
- end
+ value = _read_attribute(attr_name)
+ format_for_inspect(value)
end
# Returns +true+ if the specified +attribute+ has been set by the user or by a
@@ -338,8 +358,6 @@ module ActiveRecord
#
# Note: +:id+ is always present.
#
- # Alias for the <tt>read_attribute</tt> method.
- #
# class Person < ActiveRecord::Base
# belongs_to :organization
# end
@@ -356,7 +374,7 @@ module ActiveRecord
end
# Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
- # (Alias for the protected <tt>write_attribute</tt> method).
+ # (Alias for the protected #write_attribute method).
#
# class Person < ActiveRecord::Base
# end
@@ -364,7 +382,7 @@ module ActiveRecord
# person = Person.new
# person[:age] = '22'
# person[:age] # => 22
- # person[:age] # => Fixnum
+ # person[:age].class # => Integer
def []=(attr_name, value)
write_attribute(attr_name, value)
end
@@ -377,92 +395,76 @@ module ActiveRecord
#
# For example:
#
- # class PostsController < ActionController::Base
- # after_action :print_accessed_fields, only: :index
+ # class PostsController < ActionController::Base
+ # after_action :print_accessed_fields, only: :index
#
- # def index
- # @posts = Post.all
- # end
+ # def index
+ # @posts = Post.all
+ # end
#
- # private
+ # private
#
- # def print_accessed_fields
- # p @posts.first.accessed_fields
+ # def print_accessed_fields
+ # p @posts.first.accessed_fields
+ # end
# end
- # end
#
# Which allows you to quickly change your code to:
#
- # class PostsController < ActionController::Base
- # def index
- # @posts = Post.select(:id, :title, :author_id, :updated_at)
+ # class PostsController < ActionController::Base
+ # def index
+ # @posts = Post.select(:id, :title, :author_id, :updated_at)
+ # end
# end
- # end
def accessed_fields
@attributes.accessed
end
- 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
- end
-
- def arel_attributes_with_values_for_create(attribute_names) # :nodoc:
- arel_attributes_with_values(attributes_for_create(attribute_names))
- end
-
- def arel_attributes_with_values_for_update(attribute_names) # :nodoc:
- 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
+ def attribute_method?(attr_name)
+ # We check defined? because Syck calls respond_to? before actually calling initialize.
+ defined?(@attributes) && @attributes.key?(attr_name)
+ end
- # Returns a Hash of the Arel::Attributes and attribute values that have been
- # typecasted for use in an Arel insert/update method.
- def arel_attributes_with_values(attribute_names)
- attrs = {}
- arel_table = self.class.arel_table
-
- attribute_names.each do |name|
- attrs[arel_table[name]] = typecasted_attribute_value(name)
+ def attributes_with_values(attribute_names)
+ attribute_names.each_with_object({}) do |name, attrs|
+ attrs[name] = _read_attribute(name)
+ end
end
- attrs
- end
- # Filters the primary keys and readonly attributes from the attribute names.
- def attributes_for_update(attribute_names)
- attribute_names.reject do |name|
- readonly_attribute?(name)
+ # Filters the primary keys and readonly attributes from the attribute names.
+ def attributes_for_update(attribute_names)
+ attribute_names &= self.class.column_names
+ attribute_names.delete_if do |name|
+ readonly_attribute?(name)
+ end
end
- end
- # Filters out the primary keys, from the attribute names, when the primary
- # key is to be generated (e.g. the id attribute has no value).
- def attributes_for_create(attribute_names)
- attribute_names.reject do |name|
- pk_attribute?(name) && id.nil?
+ # Filters out the primary keys, from the attribute names, when the primary
+ # key is to be generated (e.g. the id attribute has no value).
+ def attributes_for_create(attribute_names)
+ attribute_names &= self.class.column_names
+ attribute_names.delete_if do |name|
+ pk_attribute?(name) && id.nil?
+ end
end
- end
- def readonly_attribute?(name)
- self.class.readonly_attributes.include?(name)
- end
+ def format_for_inspect(value)
+ if value.is_a?(String) && value.length > 50
+ "#{value[0, 50]}...".inspect
+ elsif value.is_a?(Date) || value.is_a?(Time)
+ %("#{value.to_s(:db)}")
+ else
+ value.inspect
+ end
+ end
- def pk_attribute?(name)
- name == self.class.primary_key
- end
+ def readonly_attribute?(name)
+ self.class.readonly_attributes.include?(name)
+ end
- def typecasted_attribute_value(name)
- _read_attribute(name)
- end
+ def pk_attribute?(name)
+ name == self.class.primary_key
+ end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
index 56c1898551..5941f51a1a 100644
--- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
+++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
@@ -1,8 +1,10 @@
+# frozen_string_literal: true
+
module ActiveRecord
module AttributeMethods
# = Active Record Attribute Methods Before Type Cast
#
- # <tt>ActiveRecord::AttributeMethods::BeforeTypeCast</tt> provides a way to
+ # ActiveRecord::AttributeMethods::BeforeTypeCast provides a way to
# read the value of the attributes before typecasting and deserialization.
#
# class Task < ActiveRecord::Base
@@ -63,14 +65,14 @@ module ActiveRecord
private
- # Handle *_before_type_cast for method_missing.
- def attribute_before_type_cast(attribute_name)
- read_attribute_before_type_cast(attribute_name)
- end
+ # Handle *_before_type_cast for method_missing.
+ def attribute_before_type_cast(attribute_name)
+ read_attribute_before_type_cast(attribute_name)
+ end
- def attribute_came_from_user?(attribute_name)
- @attributes[attribute_name].came_from_user?
- end
+ def attribute_came_from_user?(attribute_name)
+ @attributes[attribute_name].came_from_user?
+ end
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
index 0171ef3bdf..ebc2252c50 100644
--- a/activerecord/lib/active_record/attribute_methods/dirty.rb
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -1,8 +1,10 @@
-require 'active_support/core_ext/module/attribute_accessors'
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/attribute_accessors"
module ActiveRecord
module AttributeMethods
- module Dirty # :nodoc:
+ module Dirty
extend ActiveSupport::Concern
include ActiveModel::Dirty
@@ -12,177 +14,171 @@ module ActiveRecord
raise "You cannot include Dirty after Timestamp"
end
- class_attribute :partial_writes, instance_writer: false
- self.partial_writes = true
- end
+ class_attribute :partial_writes, instance_writer: false, default: true
- # Attempts to +save+ the record and clears changed attributes if successful.
- def save(*)
- if status = super
- changes_applied
- end
- status
- end
+ # Attribute methods for "changed in last call to save?"
+ attribute_method_affix(prefix: "saved_change_to_", suffix: "?")
+ attribute_method_prefix("saved_change_to_")
+ attribute_method_suffix("_before_last_save")
- # Attempts to <tt>save!</tt> the record and clears changed attributes if successful.
- def save!(*)
- super.tap do
- changes_applied
- end
+ # Attribute methods for "will change if I call save?"
+ attribute_method_affix(prefix: "will_save_change_to_", suffix: "?")
+ attribute_method_suffix("_change_to_be_saved", "_in_database")
end
# <tt>reload</tt> the record and clears changed attributes.
def reload(*)
super.tap do
- clear_changes_information
+ @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
+ @mutations_before_last_save = nil
+ @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
+ @mutations_from_database = nil
end
end
- def initialize_dup(other) # :nodoc:
- super
- calculate_changes_from_defaults
- end
-
- def changes_applied
- super
- store_original_raw_attributes
- end
-
- def clear_changes_information
- super
- original_raw_attributes.clear
- end
-
- def changed_attributes
- # This should only be set by methods which will call changed_attributes
- # multiple times when it is known that the computed value cannot change.
- if defined?(@cached_changed_attributes)
- @cached_changed_attributes
- else
- super.reverse_merge(attributes_changed_in_place).freeze
- end
- end
-
- def changes
- cache_changed_attributes do
- super
- end
- end
-
- def attribute_changed_in_place?(attr_name)
- old_value = original_raw_attribute(attr_name)
- @attributes[attr_name].changed_in_place_from?(old_value)
+ # Did this attribute change when we last saved?
+ #
+ # This method is useful in after callbacks to determine if an attribute
+ # was changed during the save that triggered the callbacks to run. It can
+ # be invoked as +saved_change_to_name?+ instead of
+ # <tt>saved_change_to_attribute?("name")</tt>.
+ #
+ # ==== Options
+ #
+ # +from+ When passed, this method will return false unless the original
+ # value is equal to the given option
+ #
+ # +to+ When passed, this method will return false unless the value was
+ # changed to the given value
+ def saved_change_to_attribute?(attr_name, **options)
+ mutations_before_last_save.changed?(attr_name, **options)
+ end
+
+ # Returns the change to an attribute during the last save. If the
+ # attribute was changed, the result will be an array containing the
+ # original value and the saved value.
+ #
+ # This method is useful in after callbacks, to see the change in an
+ # attribute during the save that triggered the callbacks to run. It can be
+ # invoked as +saved_change_to_name+ instead of
+ # <tt>saved_change_to_attribute("name")</tt>.
+ def saved_change_to_attribute(attr_name)
+ mutations_before_last_save.change_to_attribute(attr_name)
+ end
+
+ # Returns the original value of an attribute before the last save.
+ #
+ # This method is useful in after callbacks to get the original value of an
+ # attribute before the save that triggered the callbacks to run. It can be
+ # invoked as +name_before_last_save+ instead of
+ # <tt>attribute_before_last_save("name")</tt>.
+ def attribute_before_last_save(attr_name)
+ mutations_before_last_save.original_value(attr_name)
+ end
+
+ # Did the last call to +save+ have any changes to change?
+ def saved_changes?
+ mutations_before_last_save.any_changes?
+ end
+
+ # Returns a hash containing all the changes that were just saved.
+ def saved_changes
+ mutations_before_last_save.changes
+ end
+
+ # Will this attribute change the next time we save?
+ #
+ # This method is useful in validations and before callbacks to determine
+ # if the next call to +save+ will change a particular attribute. It can be
+ # invoked as +will_save_change_to_name?+ instead of
+ # <tt>will_save_change_to_attribute("name")</tt>.
+ #
+ # ==== Options
+ #
+ # +from+ When passed, this method will return false unless the original
+ # value is equal to the given option
+ #
+ # +to+ When passed, this method will return false unless the value will be
+ # changed to the given value
+ def will_save_change_to_attribute?(attr_name, **options)
+ mutations_from_database.changed?(attr_name, **options)
+ end
+
+ # Returns the change to an attribute that will be persisted during the
+ # next save.
+ #
+ # This method is useful in validations and before callbacks, to see the
+ # change to an attribute that will occur when the record is saved. It can
+ # be invoked as +name_change_to_be_saved+ instead of
+ # <tt>attribute_change_to_be_saved("name")</tt>.
+ #
+ # If the attribute will change, the result will be an array containing the
+ # original value and the new value about to be saved.
+ def attribute_change_to_be_saved(attr_name)
+ mutations_from_database.change_to_attribute(attr_name)
+ end
+
+ # Returns the value of an attribute in the database, as opposed to the
+ # in-memory value that will be persisted the next time the record is
+ # saved.
+ #
+ # This method is useful in validations and before callbacks, to see the
+ # original value of an attribute prior to any changes about to be
+ # saved. It can be invoked as +name_in_database+ instead of
+ # <tt>attribute_in_database("name")</tt>.
+ def attribute_in_database(attr_name)
+ mutations_from_database.original_value(attr_name)
+ end
+
+ # Will the next call to +save+ have any changes to persist?
+ def has_changes_to_save?
+ mutations_from_database.any_changes?
+ end
+
+ # Returns a hash containing all the changes that will be persisted during
+ # the next save.
+ def changes_to_save
+ mutations_from_database.changes
+ end
+
+ # Returns an array of the names of any attributes that will change when
+ # the record is next saved.
+ def changed_attribute_names_to_save
+ mutations_from_database.changed_attribute_names
+ end
+
+ # Returns a hash of the attributes that will change when the record is
+ # next saved.
+ #
+ # The hash keys are the attribute names, and the hash values are the
+ # original attribute values in the database (as opposed to the in-memory
+ # values about to be saved).
+ def attributes_in_database
+ mutations_from_database.changed_values
end
private
-
- def changes_include?(attr_name)
- super || attribute_changed_in_place?(attr_name)
- end
-
- def calculate_changes_from_defaults
- @changed_attributes = nil
- self.class.column_defaults.each do |attr, orig_value|
- set_attribute_was(attr, orig_value) if _field_changed?(attr, orig_value)
+ def write_attribute_without_type_cast(attr_name, _)
+ result = super
+ clear_attribute_change(attr_name)
+ result
end
- end
-
- # Wrap write_attribute to remember original attribute value.
- def write_attribute(attr, value)
- attr = attr.to_s
-
- old_value = old_attribute_value(attr)
-
- result = super
- store_original_raw_attribute(attr)
- save_changed_attribute(attr, old_value)
- result
- end
-
- def raw_write_attribute(attr, value)
- attr = attr.to_s
-
- result = super
- original_raw_attributes[attr] = value
- result
- end
- def save_changed_attribute(attr, old_value)
- clear_changed_attributes_cache
- if attribute_changed_by_setter?(attr)
- clear_attribute_changes(attr) unless _field_changed?(attr, old_value)
- else
- set_attribute_was(attr, old_value) if _field_changed?(attr, old_value)
- end
- end
-
- def old_attribute_value(attr)
- if attribute_changed?(attr)
- changed_attributes[attr]
- else
- clone_attribute_value(:_read_attribute, attr)
- end
- end
-
- def _update_record(*)
- partial_writes? ? super(keys_for_partial_write) : super
- end
-
- def _create_record(*)
- partial_writes? ? super(keys_for_partial_write) : super
- end
-
- def keys_for_partial_write
- changed & self.class.column_names
- end
-
- def _field_changed?(attr, old_value)
- @attributes[attr].changed_from?(old_value)
- end
-
- def attributes_changed_in_place
- changed_in_place.each_with_object({}) do |attr_name, h|
- orig = @attributes[attr_name].original_value
- h[attr_name] = orig
- end
- end
-
- def changed_in_place
- self.class.attribute_names.select do |attr_name|
- attribute_changed_in_place?(attr_name)
+ def _update_record(attribute_names = attribute_names_for_partial_writes)
+ affected_rows = super
+ changes_applied
+ affected_rows
end
- end
- def original_raw_attribute(attr_name)
- original_raw_attributes.fetch(attr_name) do
- read_attribute_before_type_cast(attr_name)
+ def _create_record(attribute_names = attribute_names_for_partial_writes)
+ id = super
+ changes_applied
+ id
end
- end
-
- def original_raw_attributes
- @original_raw_attributes ||= {}
- end
- def store_original_raw_attribute(attr_name)
- original_raw_attributes[attr_name] = @attributes[attr_name].value_for_database rescue nil
- end
-
- def store_original_raw_attributes
- attribute_names.each do |attr|
- store_original_raw_attribute(attr)
+ def attribute_names_for_partial_writes
+ partial_writes? ? changed_attribute_names_to_save : attribute_names
end
- end
-
- def cache_changed_attributes
- @cached_changed_attributes = changed_attributes
- yield
- ensure
- clear_changed_attributes_cache
- end
-
- def clear_changed_attributes_cache
- remove_instance_variable(:@cached_changed_attributes) if defined?(@cached_changed_attributes)
- end
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb
index c28374e4ab..9b267bb7c0 100644
--- a/activerecord/lib/active_record/attribute_methods/primary_key.rb
+++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb
@@ -1,30 +1,31 @@
-require 'set'
+# frozen_string_literal: true
+
+require "set"
module ActiveRecord
module AttributeMethods
module PrimaryKey
extend ActiveSupport::Concern
- # Returns this record's primary key value wrapped in an Array if one is
+ # Returns this record's primary key value wrapped in an array if one is
# available.
def to_key
- sync_with_transaction_state
- key = self.id
+ key = id
[key] if key
end
# Returns the primary key value.
def id
- if pk = self.class.primary_key
- sync_with_transaction_state
- _read_attribute(pk)
- end
+ sync_with_transaction_state
+ primary_key = self.class.primary_key
+ _read_attribute(primary_key) if primary_key
end
# Sets the primary key value.
def id=(value)
sync_with_transaction_state
- write_attribute(self.class.primary_key, value) if self.class.primary_key
+ primary_key = self.class.primary_key
+ _write_attribute(primary_key, value) if primary_key
end
# Queries the primary key value.
@@ -45,84 +46,98 @@ module ActiveRecord
attribute_was(self.class.primary_key)
end
- protected
-
- def attribute_method?(attr_name)
- attr_name == 'id' || super
+ def id_in_database
+ sync_with_transaction_state
+ attribute_in_database(self.class.primary_key)
end
- module ClassMethods
- def define_method_attribute(attr_name)
- super
+ private
- if attr_name == primary_key && attr_name != 'id'
- generated_attribute_methods.send(:alias_method, :id, primary_key)
- end
+ def attribute_method?(attr_name)
+ attr_name == "id" || super
end
- ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast id_was).to_set
+ module ClassMethods
+ ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast id_was id_in_database).to_set
- def dangerous_attribute_method?(method_name)
- super && !ID_ATTRIBUTE_METHODS.include?(method_name)
- end
+ def instance_method_already_implemented?(method_name)
+ super || primary_key && ID_ATTRIBUTE_METHODS.include?(method_name)
+ end
- # Defines the primary key field -- can be overridden in subclasses.
- # Overwriting will negate any effect of the +primary_key_prefix_type+
- # setting, though.
- def primary_key
- @primary_key = reset_primary_key unless defined? @primary_key
- @primary_key
- end
+ def dangerous_attribute_method?(method_name)
+ super && !ID_ATTRIBUTE_METHODS.include?(method_name)
+ end
- # Returns a quoted version of the primary key name, used to construct
- # SQL statements.
- def quoted_primary_key
- @quoted_primary_key ||= connection.quote_column_name(primary_key)
- end
+ # Defines the primary key field -- can be overridden in subclasses.
+ # Overwriting will negate any effect of the +primary_key_prefix_type+
+ # setting, though.
+ def primary_key
+ @primary_key = reset_primary_key unless defined? @primary_key
+ @primary_key
+ end
- def reset_primary_key #:nodoc:
- if self == base_class
- self.primary_key = get_primary_key(base_class.name)
- else
- self.primary_key = base_class.primary_key
+ # Returns a quoted version of the primary key name, used to construct
+ # SQL statements.
+ def quoted_primary_key
+ @quoted_primary_key ||= connection.quote_column_name(primary_key)
end
- end
- def get_primary_key(base_name) #:nodoc:
- if base_name && primary_key_prefix_type == :table_name
- base_name.foreign_key(false)
- elsif base_name && primary_key_prefix_type == :table_name_with_underscore
- base_name.foreign_key
- else
- if ActiveRecord::Base != self && table_exists?
- connection.schema_cache.primary_keys(table_name)
+ def reset_primary_key #:nodoc:
+ if base_class?
+ self.primary_key = get_primary_key(base_class.name)
else
- 'id'
+ self.primary_key = base_class.primary_key
end
end
- end
- # Sets the name of the primary key column.
- #
- # class Project < ActiveRecord::Base
- # self.primary_key = 'sysid'
- # end
- #
- # You can also define the +primary_key+ method yourself:
- #
- # class Project < ActiveRecord::Base
- # def self.primary_key
- # 'foo_' + super
- # end
- # end
- #
- # Project.primary_key # => "foo_id"
- def primary_key=(value)
- @primary_key = value && value.to_s
- @quoted_primary_key = nil
- @attributes_builder = nil
+ def get_primary_key(base_name) #:nodoc:
+ if base_name && primary_key_prefix_type == :table_name
+ base_name.foreign_key(false)
+ elsif base_name && primary_key_prefix_type == :table_name_with_underscore
+ base_name.foreign_key
+ else
+ if ActiveRecord::Base != self && table_exists?
+ pk = connection.schema_cache.primary_keys(table_name)
+ suppress_composite_primary_key(pk)
+ else
+ "id"
+ end
+ end
+ end
+
+ # Sets the name of the primary key column.
+ #
+ # class Project < ActiveRecord::Base
+ # self.primary_key = 'sysid'
+ # end
+ #
+ # You can also define the #primary_key method yourself:
+ #
+ # class Project < ActiveRecord::Base
+ # def self.primary_key
+ # 'foo_' + super
+ # end
+ # end
+ #
+ # Project.primary_key # => "foo_id"
+ def primary_key=(value)
+ @primary_key = value && value.to_s
+ @quoted_primary_key = nil
+ @attributes_builder = nil
+ end
+
+ private
+
+ def suppress_composite_primary_key(pk)
+ return pk unless pk.is_a?(Array)
+
+ warn <<~WARNING
+ WARNING: Active Record does not support composite primary key.
+
+ #{table_name} has composite primary key. Composite primary key is ignored.
+ WARNING
+ end
end
- end
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb
index 553122a5fc..6757e9b66a 100644
--- a/activerecord/lib/active_record/attribute_methods/query.rb
+++ b/activerecord/lib/active_record/attribute_methods/query.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module AttributeMethods
module Query
@@ -19,7 +21,7 @@ module ActiveRecord
if Numeric === value || value !~ /[^0-9]/
!value.to_i.zero?
else
- return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value)
+ return false if ActiveModel::Type::Boolean::FALSE_VALUES.include?(value)
!value.blank?
end
elsif value.respond_to?(:zero?)
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index 0d989c2eca..6e1275e990 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -1,69 +1,42 @@
+# frozen_string_literal: true
+
module ActiveRecord
module AttributeMethods
module Read
- ReaderMethodCache = Class.new(AttributeMethodCache) {
- private
- # We want to generate the methods via module_eval rather than
- # define_method, because define_method is slower on dispatch.
- # Evaluating many similar methods may use more memory as the instruction
- # sequences are duplicated and cached (in MRI). define_method may
- # be slower on dispatch, but if you're careful about the closure
- # created, then define_method will consume much less memory.
- #
- # But sometimes the database might return columns with
- # characters that are not allowed in normal method names (like
- # 'my_column(omg)'. So to work around this we first define with
- # the __temp__ identifier, and then use alias method to rename
- # it to what we want.
- #
- # We are also defining a constant to hold the frozen string of
- # the attribute name. Using a constant means that we do not have
- # to allocate an object on each call to the attribute method.
- # Making it frozen means that it doesn't get duped when used to
- # key the @attributes in read_attribute.
- def method_body(method_name, const_name)
- <<-EOMETHOD
- def #{method_name}
- name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{const_name}
- _read_attribute(name) { |n| missing_attribute(n, caller) }
- end
- EOMETHOD
- end
- }.new
-
extend ActiveSupport::Concern
- module ClassMethods
- protected
-
- def define_method_attribute(name)
- safe_name = name.unpack('h*').first
- temp_method = "__temp__#{safe_name}"
-
- ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
+ module ClassMethods # :nodoc:
+ private
- generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
- def #{temp_method}
- name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
- _read_attribute(name) { |n| missing_attribute(n, caller) }
+ def define_method_attribute(name)
+ sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key
+
+ ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
+ generated_attribute_methods, name
+ ) do |temp_method_name, attr_name_expr|
+ generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def #{temp_method_name}
+ #{sync_with_transaction_state}
+ name = #{attr_name_expr}
+ _read_attribute(name) { |n| missing_attribute(n, caller) }
+ end
+ RUBY
end
- STR
-
- generated_attribute_methods.module_eval do
- alias_method name, temp_method
- undef_method temp_method
end
- end
end
- ID = 'id'.freeze
-
# Returns the value of the attribute identified by <tt>attr_name</tt> after
# 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
+ if self.class.attribute_alias?(name)
+ name = self.class.attribute_alias(name)
+ end
+
+ primary_key = self.class.primary_key
+ name = primary_key if name == "id" && primary_key
+ sync_with_transaction_state if name == primary_key
_read_attribute(name, &block)
end
@@ -72,7 +45,7 @@ module ActiveRecord
if defined?(JRUBY_VERSION)
# This form is significantly faster on JRuby, and this is one of our biggest hotspots.
# https://github.com/jruby/jruby/pull/2562
- def _read_attribute(attr_name, &block) # :nodoc
+ def _read_attribute(attr_name, &block) # :nodoc:
@attributes.fetch_value(attr_name.to_s, &block)
end
else
@@ -83,7 +56,6 @@ module ActiveRecord
alias :attribute :_read_attribute
private :attribute
-
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb
index e03bf5945d..6e0e90f39c 100644
--- a/activerecord/lib/active_record/attribute_methods/serialization.rb
+++ b/activerecord/lib/active_record/attribute_methods/serialization.rb
@@ -1,23 +1,35 @@
+# frozen_string_literal: true
+
module ActiveRecord
module AttributeMethods
module Serialization
extend ActiveSupport::Concern
+ class ColumnNotSerializableError < StandardError
+ def initialize(name, type)
+ super <<~EOS
+ Column `#{name}` of type #{type.class} does not support `serialize` feature.
+ Usually it means that you are trying to use `serialize`
+ on a column that already implements serialization natively.
+ EOS
+ end
+ end
+
module ClassMethods
# If you have an attribute that needs to be saved to the database as an
# object, and retrieved as the same object, then specify the name of that
# attribute using this method and it will be handled automatically. The
# serialization is done through YAML. If +class_name+ is specified, the
# serialized object must be of that class on assignment and retrieval.
- # Otherwise <tt>SerializationTypeMismatch</tt> will be raised.
+ # Otherwise SerializationTypeMismatch will be raised.
#
- # Empty objects as +{}+, in the case of +Hash+, or +[]+, in the case of
+ # Empty objects as <tt>{}</tt>, in the case of +Hash+, or <tt>[]</tt>, in the case of
# +Array+, will always be persisted as null.
#
# Keep in mind that database adapters handle certain serialization tasks
# for you. For instance: +json+ and +jsonb+ types in PostgreSQL will be
# converted between JSON object/array syntax and Ruby +Hash+ or +Array+
- # objects transparently. There is no need to use +serialize+ in this
+ # objects transparently. There is no need to use #serialize in this
# case.
#
# For more complex cases, such as conversion to or from your application
@@ -26,7 +38,7 @@ module ActiveRecord
# ==== Parameters
#
# * +attr_name+ - The field name that should be serialized.
- # * +class_name_or_coder+ - Optional, a coder object, which responds to `.load` / `.dump`
+ # * +class_name_or_coder+ - Optional, a coder object, which responds to +.load+ and +.dump+
# or a class name that the object type should be equal to.
#
# ==== Example
@@ -50,17 +62,28 @@ module ActiveRecord
# to ensure special objects (e.g. Active Record models) are dumped correctly
# using the #as_json hook.
coder = if class_name_or_coder == ::JSON
- Coders::JSON
- elsif [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) }
- class_name_or_coder
- else
- Coders::YAMLColumn.new(class_name_or_coder)
- end
+ Coders::JSON
+ elsif [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) }
+ class_name_or_coder
+ else
+ Coders::YAMLColumn.new(attr_name, class_name_or_coder)
+ end
decorate_attribute_type(attr_name, :serialize) do |type|
+ if type_incompatible_with_serialize?(type, class_name_or_coder)
+ raise ColumnNotSerializableError.new(attr_name, type)
+ end
+
Type::Serialized.new(type, coder)
end
end
+
+ private
+
+ def type_incompatible_with_serialize?(type, class_name)
+ type.is_a?(ActiveRecord::Type::Json) && class_name == ::JSON ||
+ type.respond_to?(:type_cast_array, true) && class_name == ::Array
+ end
end
end
end
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 f9beb43e4b..294a3dc32c 100644
--- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
+++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module AttributeMethods
module TimeZoneConversion
@@ -7,93 +9,82 @@ module ActiveRecord
end
def cast(value)
- if value.is_a?(Array)
- value.map { |v| cast(v) }
- elsif value.is_a?(Hash)
+ return if value.nil?
+
+ if value.is_a?(Hash)
set_time_zone_without_conversion(super)
elsif value.respond_to?(:in_time_zone)
begin
- user_input_in_time_zone(value) || super
+ super(user_input_in_time_zone(value)) || super
rescue ArgumentError
nil
end
+ else
+ map_avoiding_infinite_recursion(super) { |v| cast(v) }
end
end
private
- def convert_time_to_time_zone(value)
- if value.is_a?(Array)
- value.map { |v| convert_time_to_time_zone(v) }
- elsif value.acts_like?(:time)
- value.in_time_zone
- else
- value
+ def convert_time_to_time_zone(value)
+ return if value.nil?
+
+ if value.acts_like?(:time)
+ value.in_time_zone
+ elsif value.is_a?(::Float)
+ value
+ else
+ map_avoiding_infinite_recursion(value) { |v| convert_time_to_time_zone(v) }
+ end
end
- end
- def set_time_zone_without_conversion(value)
- ::Time.zone.local_to_utc(value).in_time_zone
- end
+ def set_time_zone_without_conversion(value)
+ ::Time.zone.local_to_utc(value).try(:in_time_zone) if value
+ end
+
+ def map_avoiding_infinite_recursion(value)
+ map(value) do |v|
+ if value.equal?(v)
+ nil
+ else
+ yield(v)
+ end
+ end
+ end
end
extend ActiveSupport::Concern
included do
- mattr_accessor :time_zone_aware_attributes, instance_writer: false
- self.time_zone_aware_attributes = false
-
- class_attribute :skip_time_zone_conversion_for_attributes, instance_writer: false
- self.skip_time_zone_conversion_for_attributes = []
+ mattr_accessor :time_zone_aware_attributes, instance_writer: false, default: false
- class_attribute :time_zone_aware_types, instance_writer: false
- self.time_zone_aware_types = [:datetime, :not_explicitly_configured]
+ class_attribute :skip_time_zone_conversion_for_attributes, instance_writer: false, default: []
+ class_attribute :time_zone_aware_types, instance_writer: false, default: [ :datetime, :time ]
end
- module ClassMethods
+ module ClassMethods # :nodoc:
private
- def inherited(subclass)
- # We need to apply this decorator here, rather than on module inclusion. The closure
- # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the
- # sub class being decorated. As such, changes to `time_zone_aware_attributes`, or
- # `skip_time_zone_conversion_for_attributes` would not be picked up.
- subclass.class_eval do
- matcher = ->(name, type) { create_time_zone_conversion_attribute?(name, type) }
- decorate_matching_attribute_types(matcher, :_time_zone_conversion) do |type|
- TimeZoneConverter.new(type)
+ def inherited(subclass)
+ super
+ # We need to apply this decorator here, rather than on module inclusion. The closure
+ # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the
+ # sub class being decorated. As such, changes to `time_zone_aware_attributes`, or
+ # `skip_time_zone_conversion_for_attributes` would not be picked up.
+ subclass.class_eval do
+ matcher = ->(name, type) { create_time_zone_conversion_attribute?(name, type) }
+ decorate_matching_attribute_types(matcher, "_time_zone_conversion") do |type|
+ TimeZoneConverter.new(type)
+ end
end
end
- super
- end
-
- def create_time_zone_conversion_attribute?(name, cast_type)
- enabled_for_column = time_zone_aware_attributes &&
- !self.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)
- 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`.
+ 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)
- 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 << :time
- MESSAGE
+ enabled_for_column && time_zone_aware_types.include?(cast_type.type)
end
-
- result
- end
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb
index ab017c7b54..455e67e19b 100644
--- a/activerecord/lib/active_record/attribute_methods/write.rb
+++ b/activerecord/lib/active_record/attribute_methods/write.rb
@@ -1,72 +1,67 @@
+# frozen_string_literal: true
+
module ActiveRecord
module AttributeMethods
module Write
- WriterMethodCache = Class.new(AttributeMethodCache) {
- private
-
- def method_body(method_name, const_name)
- <<-EOMETHOD
- def #{method_name}(value)
- name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{const_name}
- write_attribute(name, value)
- end
- EOMETHOD
- end
- }.new
-
extend ActiveSupport::Concern
included do
attribute_method_suffix "="
end
- module ClassMethods
- protected
+ module ClassMethods # :nodoc:
+ private
- def define_method_attribute=(name)
- safe_name = name.unpack('h*').first
- ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
+ def define_method_attribute=(name)
+ sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key
- generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
- def __temp__#{safe_name}=(value)
- name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
- write_attribute(name, value)
+ ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
+ generated_attribute_methods, name, writer: true,
+ ) do |temp_method_name, attr_name_expr|
+ generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def #{temp_method_name}(value)
+ name = #{attr_name_expr}
+ #{sync_with_transaction_state}
+ _write_attribute(name, value)
+ end
+ RUBY
end
- alias_method #{(name + '=').inspect}, :__temp__#{safe_name}=
- undef_method :__temp__#{safe_name}=
- STR
- end
+ end
end
# Updates the attribute identified by <tt>attr_name</tt> with the
- # specified +value+. Empty strings for fixnum and float columns are
+ # 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)
- end
+ name = attr_name.to_s
+ if self.class.attribute_alias?(name)
+ name = self.class.attribute_alias(name)
+ end
- def raw_write_attribute(attr_name, value)
- write_attribute_with_type_cast(attr_name, value, false)
+ primary_key = self.class.primary_key
+ name = primary_key if name == "id" && primary_key
+ sync_with_transaction_state if name == primary_key
+ _write_attribute(name, value)
end
- private
- # Handle *= for method_missing.
- def attribute=(attribute_name, value)
- write_attribute(attribute_name, value)
+ # This method exists to avoid the expensive primary_key check internally, without
+ # breaking compatibility with the write_attribute API
+ def _write_attribute(attr_name, value) # :nodoc:
+ @attributes.write_from_user(attr_name.to_s, value)
+ value
end
- def write_attribute_with_type_cast(attr_name, value, should_type_cast)
- attr_name = attr_name.to_s
- attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key
-
- if should_type_cast
- @attributes.write_from_user(attr_name, value)
- else
- @attributes.write_cast_value(attr_name, value)
+ private
+ def write_attribute_without_type_cast(attr_name, value)
+ name = attr_name.to_s
+ @attributes.write_cast_value(name, value)
+ value
end
- value
- end
+ # Handle *= for method_missing.
+ def attribute=(attribute_name, value)
+ _write_attribute(attribute_name, value)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb
deleted file mode 100644
index 013a7d0e01..0000000000
--- a/activerecord/lib/active_record/attribute_set.rb
+++ /dev/null
@@ -1,93 +0,0 @@
-require 'active_record/attribute_set/builder'
-
-module ActiveRecord
- class AttributeSet # :nodoc:
- def initialize(attributes)
- @attributes = attributes
- end
-
- def [](name)
- attributes[name] || Attribute.null(name)
- end
-
- def []=(name, value)
- attributes[name] = value
- end
-
- def values_before_type_cast
- attributes.transform_values(&:value_before_type_cast)
- end
-
- def to_hash
- initialized_attributes.transform_values(&:value)
- end
- alias_method :to_h, :to_hash
-
- def key?(name)
- attributes.key?(name) && self[name].initialized?
- end
-
- def keys
- attributes.each_key.select { |name| self[name].initialized? }
- end
-
- if defined?(JRUBY_VERSION)
- # This form is significantly faster on JRuby, and this is one of our biggest hotspots.
- # https://github.com/jruby/jruby/pull/2562
- def fetch_value(name, &block)
- self[name].value(&block)
- end
- else
- def fetch_value(name)
- self[name].value { |n| yield n if block_given? }
- end
- end
-
- def write_from_database(name, value)
- attributes[name] = self[name].with_value_from_database(value)
- end
-
- def write_from_user(name, value)
- attributes[name] = self[name].with_value_from_user(value)
- end
-
- def write_cast_value(name, value)
- attributes[name] = self[name].with_cast_value(value)
- end
-
- def freeze
- @attributes.freeze
- super
- end
-
- def initialize_dup(_)
- @attributes = attributes.deep_dup
- super
- end
-
- def initialize_clone(_)
- @attributes = attributes.clone
- super
- end
-
- def reset(key)
- if key?(key)
- write_from_database(key, nil)
- end
- end
-
- def accessed
- attributes.select { |_, attr| attr.has_been_read? }.keys
- end
-
- protected
-
- attr_reader :attributes
-
- private
-
- def initialized_attributes
- attributes.select { |_, attr| attr.initialized? }
- end
- end
-end
diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb
deleted file mode 100644
index e85777c335..0000000000
--- a/activerecord/lib/active_record/attribute_set/builder.rb
+++ /dev/null
@@ -1,92 +0,0 @@
-module ActiveRecord
- class AttributeSet # :nodoc:
- class Builder # :nodoc:
- attr_reader :types, :always_initialized
-
- def initialize(types, always_initialized = nil)
- @types = types
- @always_initialized = always_initialized
- end
-
- def build_from_database(values = {}, additional_types = {})
- if always_initialized && !values.key?(always_initialized)
- values[always_initialized] = nil
- end
-
- attributes = LazyAttributeHash.new(types, values, additional_types)
- AttributeSet.new(attributes)
- end
- end
- end
-
- class LazyAttributeHash # :nodoc:
- delegate :transform_values, :each_key, to: :materialize
-
- def initialize(types, values, additional_types)
- @types = types
- @values = values
- @additional_types = additional_types
- @materialized = false
- @delegate_hash = {}
- end
-
- def key?(key)
- delegate_hash.key?(key) || values.key?(key) || types.key?(key)
- end
-
- def [](key)
- delegate_hash[key] || assign_default_value(key)
- end
-
- def []=(key, value)
- if frozen?
- raise RuntimeError, "Can't modify frozen hash"
- end
- delegate_hash[key] = value
- end
-
- def initialize_dup(_)
- @delegate_hash = delegate_hash.transform_values(&:dup)
- super
- end
-
- def select
- keys = types.keys | values.keys | delegate_hash.keys
- keys.each_with_object({}) do |key, hash|
- attribute = self[key]
- if yield(key, attribute)
- hash[key] = attribute
- end
- end
- end
-
- protected
-
- attr_reader :types, :values, :additional_types, :delegate_hash
-
- private
-
- def assign_default_value(name)
- type = additional_types.fetch(name, types[name])
- value_present = true
- value = values.fetch(name) { value_present = false }
-
- if value_present
- delegate_hash[name] = Attribute.from_database(name, value, type)
- elsif types.key?(name)
- delegate_hash[name] = Attribute.uninitialized(name, type)
- end
- end
-
- def materialize
- unless @materialized
- values.each_key { |key| self[key] }
- types.each_key { |key| self[key] }
- unless frozen?
- @materialized = true
- end
- end
- delegate_hash
- end
- end
-end
diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb
index 8b2c4c7170..35150889d9 100644
--- a/activerecord/lib/active_record/attributes.rb
+++ b/activerecord/lib/active_record/attributes.rb
@@ -1,16 +1,14 @@
-require 'active_record/attribute/user_provided_default'
+# frozen_string_literal: true
+
+require "active_model/attribute/user_provided_default"
module ActiveRecord
# See ActiveRecord::Attributes::ClassMethods for documentation
module Attributes
extend ActiveSupport::Concern
- # :nodoc:
- Type = ActiveRecord::Type
-
included do
- class_attribute :attributes_to_define_after_schema_loads, instance_accessor: false # :internal:
- self.attributes_to_define_after_schema_loads = {}
+ class_attribute :attributes_to_define_after_schema_loads, instance_accessor: false, default: {} # :internal:
end
module ClassMethods
@@ -18,7 +16,7 @@ module ActiveRecord
# type of existing attributes if needed. This allows control over how
# values are converted to and from SQL when assigned to a model. It also
# changes the behavior of values passed to
- # ActiveRecord::QueryMethods#where. This will let you use
+ # {ActiveRecord::Base.where}[rdoc-ref:QueryMethods#where]. This will let you use
# your domain objects across much of Active Record, without having to
# rely on implementation details or monkey patching.
#
@@ -37,10 +35,10 @@ module ActiveRecord
# is not passed, the previous default value (if any) will be used.
# Otherwise, the default will be +nil+.
#
- # +array+ (PG only) specifies that the type should be an array (see the
+ # +array+ (PostgreSQL only) specifies that the type should be an array (see the
# examples below).
#
- # +range+ (PG only) specifies that the type should be a range (see the
+ # +range+ (PostgreSQL only) specifies that the type should be a range (see the
# examples below).
#
# ==== Examples
@@ -59,7 +57,7 @@ module ActiveRecord
# store_listing = StoreListing.new(price_in_cents: '10.1')
#
# # before
- # store_listing.price_in_cents # => BigDecimal.new(10.1)
+ # store_listing.price_in_cents # => BigDecimal(10.1)
#
# class StoreListing < ActiveRecord::Base
# attribute :price_in_cents, :integer
@@ -70,12 +68,14 @@ module ActiveRecord
#
# A default can also be provided.
#
+ # # db/schema.rb
# create_table :store_listings, force: true do |t|
# t.string :my_string, default: "original default"
# end
#
# StoreListing.new.my_string # => "original default"
#
+ # # app/models/store_listing.rb
# class StoreListing < ActiveRecord::Base
# attribute :my_string, :string, default: "new default"
# end
@@ -90,8 +90,9 @@ module ActiveRecord
# sleep 1
# Product.new.my_default_proc # => 2015-05-30 11:04:49 -0600
#
- # Attributes do not need to be backed by a database column.
+ # \Attributes do not need to be backed by a database column.
#
+ # # app/models/my_model.rb
# class MyModel < ActiveRecord::Base
# attribute :my_string, :string
# attribute :my_int_array, :integer, array: true
@@ -116,13 +117,13 @@ module ActiveRecord
# Users may also define their own custom types, as long as they respond
# to the methods defined on the value type. The method +deserialize+ or
# +cast+ will be called on your type object, with raw input from the
- # database or from your controllers. See ActiveRecord::Type::Value for the
+ # database or from your controllers. See ActiveModel::Type::Value for the
# expected API. It is recommended that your type objects inherit from an
# existing type, or from ActiveRecord::Type::Value
#
# class MoneyType < ActiveRecord::Type::Integer
# def cast(value)
- # if value.include?('$')
+ # if !value.kind_of?(Numeric) && value.include?('$')
# price_in_dollars = value.gsub(/\$/, '').to_f
# super(price_in_dollars * 100)
# else
@@ -134,7 +135,7 @@ module ActiveRecord
# # config/initializers/types.rb
# ActiveRecord::Type.register(:money, MoneyType)
#
- # # /app/models/store_listing.rb
+ # # app/models/store_listing.rb
# class StoreListing < ActiveRecord::Base
# attribute :price_in_cents, :money
# end
@@ -143,13 +144,13 @@ module ActiveRecord
# store_listing.price_in_cents # => 1000
#
# For more details on creating custom types, see the documentation for
- # ActiveRecord::Type::Value. For more details on registering your types
+ # ActiveModel::Type::Value. For more details on registering your types
# to be referenced by a symbol, see ActiveRecord::Type.register. You can
# also pass a type object directly, in place of a symbol.
#
- # ==== Querying
+ # ==== \Querying
#
- # When ActiveRecord::QueryMethods#where is called, it will
+ # When {ActiveRecord::Base.where}[rdoc-ref:QueryMethods#where] is called, it will
# use the type defined by the model class to convert the value to SQL,
# calling +serialize+ on your type object. For example:
#
@@ -157,7 +158,7 @@ module ActiveRecord
# end
#
# class MoneyType < Type::Value
- # def initialize(currency_converter)
+ # def initialize(currency_converter:)
# @currency_converter = currency_converter
# end
#
@@ -170,11 +171,13 @@ module ActiveRecord
# end
# end
#
+ # # config/initializers/types.rb
# ActiveRecord::Type.register(:money, MoneyType)
#
+ # # app/models/product.rb
# class Product < ActiveRecord::Base
# currency_converter = ConversionRatesFromTheInternet.new
- # attribute :price_in_bitcoins, :money, currency_converter
+ # attribute :price_in_bitcoins, :money, currency_converter: currency_converter
# end
#
# Product.where(price_in_bitcoins: Money.new(5, "USD"))
@@ -188,8 +191,8 @@ module ActiveRecord
# The type of an attribute is given the opportunity to change how dirty
# tracking is performed. The methods +changed?+ and +changed_in_place?+
# will be called from ActiveModel::Dirty. See the documentation for those
- # methods in ActiveRecord::Type::Value for more details.
- def attribute(name, cast_type, **options)
+ # methods in ActiveModel::Type::Value for more details.
+ def attribute(name, cast_type = Type::Value.new, **options)
name = name.to_s
reload_schema_from_cache
@@ -240,24 +243,24 @@ module ActiveRecord
private
- NO_DEFAULT_PROVIDED = Object.new # :nodoc:
- private_constant :NO_DEFAULT_PROVIDED
+ NO_DEFAULT_PROVIDED = Object.new # :nodoc:
+ private_constant :NO_DEFAULT_PROVIDED
- def define_default_attribute(name, value, type, from_user:)
- if value == NO_DEFAULT_PROVIDED
- default_attribute = _default_attributes[name].with_type(type)
- elsif from_user
- default_attribute = Attribute::UserProvidedDefault.new(
- name,
- value,
- type,
- _default_attributes[name],
- )
- else
- default_attribute = Attribute.from_database(name, value, type)
+ def define_default_attribute(name, value, type, from_user:)
+ if value == NO_DEFAULT_PROVIDED
+ default_attribute = _default_attributes[name].with_type(type)
+ elsif from_user
+ default_attribute = ActiveModel::Attribute::UserProvidedDefault.new(
+ name,
+ value,
+ type,
+ _default_attributes.fetch(name.to_s) { nil },
+ )
+ else
+ default_attribute = ActiveModel::Attribute.from_database(name, value, type)
+ end
+ _default_attributes[name] = default_attribute
end
- _default_attributes[name] = default_attribute
- end
end
end
end
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index 0792d19c3e..d77d76cb1e 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -1,10 +1,12 @@
+# frozen_string_literal: true
+
module ActiveRecord
# = Active Record Autosave Association
#
- # +AutosaveAssociation+ is a module that takes care of automatically saving
+ # AutosaveAssociation is a module that takes care of automatically saving
# associated records when their parent is saved. In addition to saving, it
# also destroys any associated records that were marked for destruction.
- # (See +mark_for_destruction+ and <tt>marked_for_destruction?</tt>).
+ # (See #mark_for_destruction and #marked_for_destruction?).
#
# Saving of the parent, its associations, and the destruction of marked
# associations, all happen inside a transaction. This should never leave the
@@ -22,7 +24,7 @@ module ActiveRecord
#
# == Validation
#
- # Children records are validated unless <tt>:validate</tt> is +false+.
+ # Child records are validated unless <tt>:validate</tt> is +false+.
#
# == Callbacks
#
@@ -125,7 +127,6 @@ module ActiveRecord
# Now it _is_ removed from the database:
#
# Comment.find_by(id: id).nil? # => true
-
module AutosaveAssociation
extend ActiveSupport::Concern
@@ -141,22 +142,23 @@ module ActiveRecord
included do
Associations::Builder::Association.extensions << AssociationBuilderExtension
+ mattr_accessor :index_nested_attribute_errors, instance_writer: false, default: false
end
- module ClassMethods
+ module ClassMethods # :nodoc:
private
def define_non_cyclic_method(name, &block)
- return if method_defined?(name)
+ return if instance_methods(false).include?(name)
define_method(name) do |*args|
result = true; @_already_called ||= {}
# Loop prevention for validation of associations
unless @_already_called[name]
begin
- @_already_called[name]=true
+ @_already_called[name] = true
result = instance_eval(&block)
ensure
- @_already_called[name]=false
+ @_already_called[name] = false
end
end
@@ -180,6 +182,7 @@ module ActiveRecord
if reflection.collection?
before_save :before_save_collection_association
+ after_save :after_save_collection_association
define_non_cyclic_method(save_method) { save_collection_association(reflection) }
# Doesn't use after_save as that would save associations added in after_create/after_update twice
@@ -214,14 +217,9 @@ module ActiveRecord
method = :validate_single_association
end
- define_non_cyclic_method(validation_method) do
- send(method, reflection)
- # TODO: remove the following line as soon as the return value of
- # callbacks is ignored, that is, returning `false` does not
- # display a deprecation warning or halts the callback chain.
- true
- end
+ define_non_cyclic_method(validation_method) { send(method, reflection) }
validate validation_method
+ after_validation :_ensure_no_duplicate_errors
end
end
end
@@ -233,7 +231,7 @@ module ActiveRecord
super
end
- # Marks this record to be destroyed as part of the parents save transaction.
+ # Marks this record to be destroyed as part of the parent's save transaction.
# This does _not_ actually destroy the record instantly, rather child record will be destroyed
# when <tt>parent.save</tt> is called.
#
@@ -242,7 +240,7 @@ module ActiveRecord
@marked_for_destruction = true
end
- # Returns whether or not this record will be destroyed as part of the parents save transaction.
+ # Returns whether or not this record will be destroyed as part of the parent's save transaction.
#
# Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
def marked_for_destruction?
@@ -265,7 +263,7 @@ module ActiveRecord
# Returns whether or not this record has been changed in any way (including whether
# any of its nested autosave associations are likewise changed)
def changed_for_autosave?
- new_record? || changed? || marked_for_destruction? || nested_records_changed_for_autosave?
+ new_record? || has_changes_to_save? || marked_for_destruction? || nested_records_changed_for_autosave?
end
private
@@ -315,7 +313,7 @@ module ActiveRecord
def validate_collection_association(reflection)
if association = association_instance_get(reflection.name)
if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave])
- records.each { |record| association_valid?(reflection, record) }
+ records.each_with_index { |record, index| association_valid?(reflection, record, index) }
end
end
end
@@ -323,17 +321,30 @@ module ActiveRecord
# Returns whether or not the association is valid and applies any errors to
# the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt>
# enabled records if they're marked_for_destruction? or destroyed.
- def association_valid?(reflection, record)
- return true if record.destroyed? || record.marked_for_destruction?
+ def association_valid?(reflection, record, index = nil)
+ return true if record.destroyed? || (reflection.options[:autosave] && record.marked_for_destruction?)
+
+ context = validation_context unless [:create, :update].include?(validation_context)
- validation_context = self.validation_context unless [:create, :update].include?(self.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)
+
record.errors.each do |attribute, message|
- attribute = "#{reflection.name}.#{attribute}"
+ attribute = normalize_reflection_attribute(indexed_attribute, reflection, index, attribute)
errors[attribute] << message
errors[attribute].uniq!
end
+
+ record.errors.details.each_key do |attribute|
+ reflection_attribute =
+ normalize_reflection_attribute(indexed_attribute, reflection, index, attribute).to_sym
+
+ record.errors.details[attribute].each do |error|
+ errors.details[reflection_attribute] << error
+ errors.details[reflection_attribute].uniq!
+ end
+ end
else
errors.add(reflection.name)
end
@@ -341,18 +352,29 @@ module ActiveRecord
valid
end
+ def normalize_reflection_attribute(indexed_attribute, reflection, index, attribute)
+ if indexed_attribute
+ "#{reflection.name}[#{index}].#{attribute}"
+ else
+ "#{reflection.name}.#{attribute}"
+ end
+ end
+
# Is used as a before_save callback to check while saving a collection
# association whether or not the parent was a new record before saving.
def before_save_collection_association
@new_record_before_save = new_record?
- true
+ end
+
+ def after_save_collection_association
+ @new_record_before_save = false
end
# Saves any new associated records, or all loaded autosave associations if
# <tt>:autosave</tt> is enabled on the association.
#
# In addition, it destroys all children that were marked for destruction
- # with mark_for_destruction.
+ # with #mark_for_destruction.
#
# This all happens inside a transaction, _if_ the Transactions module is included into
# ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
@@ -360,6 +382,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 records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
if autosave
records_to_destroy = records.select(&:marked_for_destruction?)
@@ -375,19 +400,21 @@ module ActiveRecord
if autosave != false && (@new_record_before_save || record.new_record?)
if autosave
saved = association.insert_record(record, false)
- else
- association.insert_record(record) unless reflection.nested?
+ elsif !reflection.nested?
+ association_saved = association.insert_record(record)
+
+ if reflection.validate?
+ errors.add(reflection.name) unless association_saved
+ saved = association_saved
+ end
end
elsif autosave
- saved = record.save(:validate => false)
+ saved = record.save(validate: false)
end
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
@@ -395,7 +422,7 @@ module ActiveRecord
# on the association.
#
# In addition, it will destroy the association if it was marked for
- # destruction with mark_for_destruction.
+ # destruction with #mark_for_destruction.
#
# This all happens inside a transaction, _if_ the Transactions module is included into
# ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
@@ -414,9 +441,12 @@ module ActiveRecord
if (autosave && record.changed_for_autosave?) || new_record? || record_changed?(reflection, record, key)
unless reflection.through_reflection
record[reflection.foreign_key] = key
+ if inverse_reflection = reflection.inverse_of
+ record.association(inverse_reflection.name).loaded!
+ end
end
- saved = record.save(:validate => !autosave)
+ saved = record.save(validate: !autosave)
raise ActiveRecord::Rollback if !saved && autosave
saved
end
@@ -428,7 +458,7 @@ module ActiveRecord
def record_changed?(reflection, record, key)
record.new_record? ||
(record.has_attribute?(reflection.foreign_key) && record[reflection.foreign_key] != key) ||
- record.attribute_changed?(reflection.foreign_key)
+ record.will_save_change_to_attribute?(reflection.foreign_key)
end
# Saves the associated record if it's new or <tt>:autosave</tt> is enabled.
@@ -436,7 +466,9 @@ module ActiveRecord
# In addition, it will destroy the association if it was marked for destruction.
def save_belongs_to_association(reflection)
association = association_instance_get(reflection.name)
- record = association && association.load_target
+ return unless association && association.loaded? && !association.stale_target?
+
+ record = association.load_target
if record && !record.destroyed?
autosave = reflection.options[:autosave]
@@ -444,7 +476,7 @@ module ActiveRecord
self[reflection.foreign_key] = nil
record.destroy
elsif autosave != false
- saved = record.save(:validate => !autosave) if record.new_record? || (autosave && record.changed_for_autosave?)
+ saved = record.save(validate: !autosave) if record.new_record? || (autosave && record.changed_for_autosave?)
if association.updated?
association_id = record.send(reflection.options[:primary_key] || :id)
@@ -456,5 +488,11 @@ module ActiveRecord
end
end
end
+
+ def _ensure_no_duplicate_errors
+ errors.messages.each_key do |attribute|
+ errors[attribute].uniq!
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index c918e88590..db097cb930 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -1,27 +1,28 @@
-require 'yaml'
-require 'set'
-require 'active_support/benchmarkable'
-require 'active_support/dependencies'
-require 'active_support/descendants_tracker'
-require 'active_support/time'
-require 'active_support/core_ext/module/attribute_accessors'
-require 'active_support/core_ext/array/extract_options'
-require 'active_support/core_ext/hash/deep_merge'
-require 'active_support/core_ext/hash/slice'
-require 'active_support/core_ext/hash/transform_values'
-require 'active_support/core_ext/string/behavior'
-require 'active_support/core_ext/kernel/singleton_class'
-require 'active_support/core_ext/module/introspection'
-require 'active_support/core_ext/object/duplicable'
-require 'active_support/core_ext/class/subclasses'
-require 'arel'
-require 'active_record/attribute_decorators'
-require 'active_record/errors'
-require 'active_record/log_subscriber'
-require 'active_record/explain_subscriber'
-require 'active_record/relation/delegation'
-require 'active_record/attributes'
-require 'active_record/type_caster'
+# frozen_string_literal: true
+
+require "yaml"
+require "active_support/benchmarkable"
+require "active_support/dependencies"
+require "active_support/descendants_tracker"
+require "active_support/time"
+require "active_support/core_ext/module/attribute_accessors"
+require "active_support/core_ext/array/extract_options"
+require "active_support/core_ext/hash/deep_merge"
+require "active_support/core_ext/hash/slice"
+require "active_support/core_ext/string/behavior"
+require "active_support/core_ext/kernel/singleton_class"
+require "active_support/core_ext/module/introspection"
+require "active_support/core_ext/object/duplicable"
+require "active_support/core_ext/class/subclasses"
+require "active_record/attribute_decorators"
+require "active_record/define_callbacks"
+require "active_record/errors"
+require "active_record/log_subscriber"
+require "active_record/explain_subscriber"
+require "active_record/relation/delegation"
+require "active_record/attributes"
+require "active_record/type_caster"
+require "active_record/database_configurations"
module ActiveRecord #:nodoc:
# = Active Record
@@ -133,9 +134,6 @@ module ActiveRecord #:nodoc:
# end
# end
#
- # You can alternatively use <tt>self[:attribute]=(value)</tt> and <tt>self[:attribute]</tt>
- # or <tt>write_attribute(:attribute, value)</tt> and <tt>read_attribute(:attribute)</tt>.
- #
# == Attribute query methods
#
# In addition to the basic accessors, query methods are also automatically available on the Active Record object.
@@ -171,10 +169,11 @@ module ActiveRecord #:nodoc:
# <tt>Person.find_by_user_name(user_name)</tt>.
#
# It's possible to add an exclamation point (!) on the end of the dynamic finders to get them to raise an
- # <tt>ActiveRecord::RecordNotFound</tt> error if they do not return any records,
+ # ActiveRecord::RecordNotFound error if they do not return any records,
# like <tt>Person.find_by_last_name!</tt>.
#
- # It's also possible to use multiple attributes in the same find by separating them with "_and_".
+ # It's also possible to use multiple attributes in the same <tt>find_by_</tt> by separating them with
+ # "_and_".
#
# Person.find_by(user_name: user_name, password: password)
# Person.find_by_user_name_and_password(user_name, password) # with dynamic finder
@@ -186,7 +185,8 @@ module ActiveRecord #:nodoc:
# == Saving arrays, hashes, and other non-mappable objects in text columns
#
# Active Record can serialize any object in text columns using YAML. To do so, you must
- # specify this with a call to the class method +serialize+.
+ # specify this with a call to the class method
+ # {serialize}[rdoc-ref:AttributeMethods::Serialization::ClassMethods#serialize].
# This makes it possible to store arrays, hashes, and other non-mappable objects without doing
# any additional work.
#
@@ -226,39 +226,47 @@ module ActiveRecord #:nodoc:
#
# == Connection to multiple databases in different models
#
- # Connections are usually created through ActiveRecord::Base.establish_connection and retrieved
+ # Connections are usually created through
+ # {ActiveRecord::Base.establish_connection}[rdoc-ref:ConnectionHandling#establish_connection] and retrieved
# by ActiveRecord::Base.connection. All classes inheriting from ActiveRecord::Base will use this
# connection. But you can also set a class-specific connection. For example, if Course is an
# ActiveRecord::Base, but resides in a different database, you can just say <tt>Course.establish_connection</tt>
# and Course and all of its subclasses will use this connection instead.
#
# This feature is implemented by keeping a connection pool in ActiveRecord::Base that is
- # a Hash indexed by the class. If a connection is requested, the retrieve_connection method
+ # a hash indexed by the class. If a connection is requested, the
+ # {ActiveRecord::Base.retrieve_connection}[rdoc-ref:ConnectionHandling#retrieve_connection] method
# will go up the class-hierarchy until a connection is found in the connection pool.
#
# == Exceptions
#
# * ActiveRecordError - Generic error class and superclass of all other errors raised by Active Record.
- # * AdapterNotSpecified - The configuration hash used in <tt>establish_connection</tt> didn't include an
- # <tt>:adapter</tt> key.
- # * AdapterNotFound - The <tt>:adapter</tt> key used in <tt>establish_connection</tt> specified a
- # non-existent adapter
+ # * AdapterNotSpecified - The configuration hash used in
+ # {ActiveRecord::Base.establish_connection}[rdoc-ref:ConnectionHandling#establish_connection]
+ # didn't include an <tt>:adapter</tt> key.
+ # * AdapterNotFound - The <tt>:adapter</tt> key used in
+ # {ActiveRecord::Base.establish_connection}[rdoc-ref:ConnectionHandling#establish_connection]
+ # specified a non-existent adapter
# (or a bad spelling of an existing one).
# * AssociationTypeMismatch - The object assigned to the association wasn't of the type
# specified in the association definition.
# * AttributeAssignmentError - An error occurred while doing a mass assignment through the
- # <tt>attributes=</tt> method.
+ # {ActiveRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=] method.
# You can inspect the +attribute+ property of the exception object to determine which attribute
# triggered the error.
- # * ConnectionNotEstablished - No connection has been established. Use <tt>establish_connection</tt>
- # before querying.
+ # * ConnectionNotEstablished - No connection has been established.
+ # Use {ActiveRecord::Base.establish_connection}[rdoc-ref:ConnectionHandling#establish_connection] before querying.
# * MultiparameterAssignmentErrors - Collection of errors that occurred during a mass assignment using the
- # <tt>attributes=</tt> method. The +errors+ property of this exception contains an array of
+ # {ActiveRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=] method.
+ # The +errors+ property of this exception contains an array of
# AttributeAssignmentError
# objects that should be inspected to determine which attributes triggered the errors.
- # * RecordInvalid - raised by <tt>save!</tt> and <tt>create!</tt> when the record is invalid.
- # * RecordNotFound - No record responded to the +find+ method. Either the row with the given ID doesn't exist
- # or the row didn't meet the additional restrictions. Some +find+ calls do not raise this exception to signal
+ # * RecordInvalid - raised by {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!] and
+ # {ActiveRecord::Base.create!}[rdoc-ref:Persistence::ClassMethods#create!]
+ # when the record is invalid.
+ # * RecordNotFound - No record responded to the {ActiveRecord::Base.find}[rdoc-ref:FinderMethods#find] method.
+ # Either the row with the given ID doesn't exist or the row didn't meet the additional restrictions.
+ # Some {ActiveRecord::Base.find}[rdoc-ref:FinderMethods#find] calls do not raise this exception to signal
# nothing was found, please check its documentation for further details.
# * SerializationTypeMismatch - The serialized object wasn't of the class specified as the second parameter.
# * StatementInvalid - The database server rejected the SQL statement. The precise error is added in the message.
@@ -280,6 +288,8 @@ module ActiveRecord #:nodoc:
extend Explain
extend Enum
extend Delegation::DelegateCache
+ extend CollectionCacheKey
+ extend Aggregations::ClassMethods
include Core
include Persistence
@@ -297,6 +307,7 @@ module ActiveRecord #:nodoc:
include AttributeDecorators
include Locking::Optimistic
include Locking::Pessimistic
+ include DefineCallbacks
include AttributeMethods
include Callbacks
include Timestamp
@@ -304,10 +315,9 @@ module ActiveRecord #:nodoc:
include ActiveModel::SecurePassword
include AutosaveAssociation
include NestedAttributes
- include Aggregations
include Transactions
- include NoTouching
include TouchLater
+ include NoTouching
include Reflection
include Serialization
include Store
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index 3027ce928e..5407af85ea 100644
--- a/activerecord/lib/active_record/callbacks.rb
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -1,11 +1,13 @@
+# frozen_string_literal: true
+
module ActiveRecord
- # = Active Record Callbacks
+ # = Active Record \Callbacks
#
- # Callbacks are hooks into the life cycle of an Active Record object that allow you to trigger logic
+ # \Callbacks are hooks into the life cycle of an Active Record object that allow you to trigger logic
# before or after an alteration of the object state. This can be used to make sure that associated and
- # dependent objects are deleted when +destroy+ is called (by overwriting +before_destroy+) or to massage attributes
- # before they're validated (by overwriting +before_validation+). As an example of the callbacks initiated, consider
- # the <tt>Base#save</tt> call for a new record:
+ # dependent objects are deleted when {ActiveRecord::Base#destroy}[rdoc-ref:Persistence#destroy] is called (by overwriting +before_destroy+) or
+ # to massage attributes before they're validated (by overwriting +before_validation+).
+ # As an example of the callbacks initiated, consider the {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] call for a new record:
#
# * (-) <tt>save</tt>
# * (-) <tt>valid</tt>
@@ -20,7 +22,7 @@ module ActiveRecord
# * (7) <tt>after_commit</tt>
#
# Also, an <tt>after_rollback</tt> callback can be configured to be triggered whenever a rollback is issued.
- # Check out <tt>ActiveRecord::Transactions</tt> for more details about <tt>after_commit</tt> and
+ # Check out ActiveRecord::Transactions for more details about <tt>after_commit</tt> and
# <tt>after_rollback</tt>.
#
# Additionally, an <tt>after_touch</tt> callback is triggered whenever an
@@ -31,7 +33,7 @@ module ActiveRecord
# are instantiated as well.
#
# There are nineteen callbacks in total, which give you immense power to react and prepare for each state in the
- # Active Record life cycle. The sequence for calling <tt>Base#save</tt> for an existing record is similar,
+ # Active Record life cycle. The sequence for calling {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] for an existing record is similar,
# except that each <tt>_create</tt> callback is replaced by the corresponding <tt>_update</tt> callback.
#
# Examples:
@@ -53,9 +55,9 @@ module ActiveRecord
# end
#
# class Firm < ActiveRecord::Base
- # # Destroys the associated clients and people when the firm is destroyed
- # before_destroy { |record| Person.destroy_all "firm_id = #{record.id}" }
- # before_destroy { |record| Client.destroy_all "client_of = #{record.id}" }
+ # # Disables access to the system, for associated clients and people when the firm is destroyed
+ # before_destroy { |record| Person.where(firm_id: record.id).update_all(access: 'disabled') }
+ # before_destroy { |record| Client.where(client_of: record.id).update_all(access: 'disabled') }
# end
#
# == Inheritable callback queues
@@ -73,21 +75,7 @@ module ActiveRecord
# end
#
# Now, when <tt>Topic#destroy</tt> is run only +destroy_author+ is called. When <tt>Reply#destroy</tt> is
- # run, both +destroy_author+ and +destroy_readers+ are called. Contrast this to the following situation
- # where the +before_destroy+ method is overridden:
- #
- # class Topic < ActiveRecord::Base
- # def before_destroy() destroy_author end
- # end
- #
- # class Reply < Topic
- # def before_destroy() destroy_readers end
- # end
- #
- # In that case, <tt>Reply#destroy</tt> would only run +destroy_readers+ and _not_ +destroy_author+.
- # So, use the callback macros when you want to ensure that a certain callback is called for the entire
- # hierarchy, and use the regular overwritable methods when you want to leave it up to each descendant
- # to decide whether they want to call +super+ and trigger the inherited callbacks.
+ # run, both +destroy_author+ and +destroy_readers+ are called.
#
# *IMPORTANT:* In order for inheritance to work for the callback queues, you must specify the
# callbacks before specifying the associations. Otherwise, you might trigger the loading of a
@@ -96,9 +84,9 @@ module ActiveRecord
# == Types of callbacks
#
# There are four types of callbacks accepted by the callback macros: Method references (symbol), callback objects,
- # inline methods (using a proc), and inline eval methods (using a string). Method references and callback objects
+ # inline methods (using a proc). Method references and callback objects
# are the recommended approaches, inline methods using a proc are sometimes appropriate (such as for
- # creating mix-ins), and inline eval methods are deprecated.
+ # creating mix-ins).
#
# The method reference callbacks work by specifying a protected or private method available in the object, like this:
#
@@ -107,7 +95,7 @@ module ActiveRecord
#
# private
# def delete_parents
- # self.class.delete_all "parent_id = #{id}"
+ # self.class.where(parent_id: id).delete_all
# end
# end
#
@@ -140,7 +128,7 @@ module ActiveRecord
# end
# end
#
- # So you specify the object you want messaged on a given callback. When that callback is triggered, the object has
+ # So you specify the object you want to be messaged on a given callback. When that callback is triggered, the object has
# a method by the name of the callback messaged. You can make these callbacks more flexible by passing in other
# initialization data such as the name of the attribute to work with:
#
@@ -175,26 +163,12 @@ module ActiveRecord
# end
# end
#
- # The callback macros usually accept a symbol for the method they're supposed to run, but you can also
- # pass a "method string", which will then be evaluated within the binding of the callback. Example:
- #
- # class Topic < ActiveRecord::Base
- # before_destroy 'self.class.delete_all "parent_id = #{id}"'
- # end
- #
- # Notice that single quotes (') are used so the <tt>#{id}</tt> part isn't evaluated until the callback
- # is triggered. Also note that these inline callbacks can be stacked just like the regular ones:
- #
- # class Topic < ActiveRecord::Base
- # before_destroy 'self.class.delete_all "parent_id = #{id}"',
- # 'puts "Evaluated after parents are destroyed"'
- # end
- #
# == <tt>before_validation*</tt> returning statements
#
# If the +before_validation+ callback throws +:abort+, the process will be
- # aborted and <tt>Base#save</tt> will return +false+. If Base#save! is called it will raise a
- # <tt>ActiveRecord::RecordInvalid</tt> exception. Nothing will be appended to the errors object.
+ # aborted and {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] will return +false+.
+ # If {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!] is called it will raise an ActiveRecord::RecordInvalid exception.
+ # Nothing will be appended to the errors object.
#
# == Canceling callbacks
#
@@ -206,12 +180,13 @@ module ActiveRecord
# == Ordering callbacks
#
# Sometimes the code needs that the callbacks execute in a specific order. For example, a +before_destroy+
- # callback (+log_children+ in this case) should be executed before the children get destroyed by the +dependent: destroy+ option.
+ # callback (+log_children+ in this case) should be executed before the children get destroyed by the
+ # <tt>dependent: :destroy</tt> option.
#
# Let's look at the code below:
#
# class Topic < ActiveRecord::Base
- # has_many :children, dependent: destroy
+ # has_many :children, dependent: :destroy
#
# before_destroy :log_children
#
@@ -222,10 +197,11 @@ module ActiveRecord
# end
#
# In this case, the problem is that when the +before_destroy+ callback is executed, the children are not available
- # because the +destroy+ callback gets executed first. You can use the +prepend+ option on the +before_destroy+ callback to avoid this.
+ # because the {ActiveRecord::Base#destroy}[rdoc-ref:Persistence#destroy] callback gets executed first.
+ # You can use the +prepend+ option on the +before_destroy+ callback to avoid this.
#
# class Topic < ActiveRecord::Base
- # has_many :children, dependent: destroy
+ # has_many :children, dependent: :destroy
#
# before_destroy :log_children, prepend: true
#
@@ -235,23 +211,72 @@ module ActiveRecord
# end
# end
#
- # This way, the +before_destroy+ gets executed before the <tt>dependent: destroy</tt> is called, and the data is still available.
+ # 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 < ActiveRecord::Base
+ # has_many :children
+ #
+ # after_save :log_children
+ # after_save :do_something_else
+ #
+ # private
+ #
+ # def log_children
+ # # 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.
#
- # == Transactions
+ # In case there are multiple transactional callbacks as seen below, the order
+ # is reversed.
+ #
+ # For example:
+ #
+ # class Topic < ActiveRecord::Base
+ # has_many :children
#
- # The entire callback chain of a +save+, <tt>save!</tt>, or +destroy+ call runs
- # within a transaction. That includes <tt>after_*</tt> hooks. If everything
- # goes fine a COMMIT is executed once the chain has been completed.
+ # after_commit :log_children
+ # after_commit :do_something_else
+ #
+ # private
+ #
+ # def log_children
+ # # 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!],
+ # or {#destroy}[rdoc-ref:Persistence#destroy] call runs within a transaction. That includes <tt>after_*</tt> hooks.
+ # If everything goes fine a COMMIT is executed once the chain has been completed.
#
# If a <tt>before_*</tt> callback cancels the action a ROLLBACK is issued. You
# can also trigger a ROLLBACK raising an exception in any of the callbacks,
# including <tt>after_*</tt> hooks. Note, however, that in that case the client
- # needs to be aware of it because an ordinary +save+ will raise such exception
+ # needs to be aware of it because an ordinary {#save}[rdoc-ref:Persistence#save] will raise such exception
# instead of quietly returning +false+.
#
# == Debugging callbacks
#
- # The callback chain is accessible via the <tt>_*_callbacks</tt> method on an object. ActiveModel Callbacks support
+ # The callback chain is accessible via the <tt>_*_callbacks</tt> method on an object. Active Model \Callbacks support
# <tt>:before</tt>, <tt>:after</tt> and <tt>:around</tt> as values for the <tt>kind</tt> property. The <tt>kind</tt> property
# defines what part of the chain the callback runs in.
#
@@ -277,36 +302,38 @@ module ActiveRecord
:before_destroy, :around_destroy, :after_destroy, :after_commit, :after_rollback
]
- module ClassMethods
- include ActiveModel::Callbacks
- end
-
- included do
- include ActiveModel::Validations::Callbacks
-
- define_model_callbacks :initialize, :find, :touch, :only => :after
- define_model_callbacks :save, :create, :update, :destroy
- end
-
def destroy #:nodoc:
- run_callbacks(:destroy) { super }
+ @_destroy_callback_already_called ||= false
+ return if @_destroy_callback_already_called
+ @_destroy_callback_already_called = true
+ _run_destroy_callbacks { super }
+ rescue RecordNotDestroyed => e
+ @_association_destroy_exception = e
+ false
+ ensure
+ @_destroy_callback_already_called = false
end
def touch(*) #:nodoc:
- run_callbacks(:touch) { super }
+ _run_touch_callbacks { super }
+ end
+
+ def increment!(attribute, by = 1, touch: nil) # :nodoc:
+ touch ? _run_touch_callbacks { super } : super
end
private
- def create_or_update(*) #:nodoc:
- run_callbacks(:save) { super }
+
+ def create_or_update(*)
+ _run_save_callbacks { super }
end
- def _create_record #:nodoc:
- run_callbacks(:create) { super }
+ def _create_record
+ _run_create_callbacks { super }
end
- def _update_record(*) #:nodoc:
- run_callbacks(:update) { super }
+ def _update_record(*)
+ _run_update_callbacks { super }
end
end
end
diff --git a/activerecord/lib/active_record/coders/json.rb b/activerecord/lib/active_record/coders/json.rb
index 75d3bfe625..a69b38487e 100644
--- a/activerecord/lib/active_record/coders/json.rb
+++ b/activerecord/lib/active_record/coders/json.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Coders # :nodoc:
class JSON # :nodoc:
@@ -6,7 +8,7 @@ module ActiveRecord
end
def self.load(json)
- ActiveSupport::JSON.decode(json) unless json.nil?
+ ActiveSupport::JSON.decode(json) unless json.blank?
end
end
end
diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb
index 9ea22ed798..11559141c7 100644
--- a/activerecord/lib/active_record/coders/yaml_column.rb
+++ b/activerecord/lib/active_record/coders/yaml_column.rb
@@ -1,12 +1,14 @@
-require 'yaml'
+# frozen_string_literal: true
+
+require "yaml"
module ActiveRecord
module Coders # :nodoc:
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
@@ -14,36 +16,35 @@ module ActiveRecord
def dump(obj)
return if obj.nil?
- unless obj.is_a?(object_class)
- raise SerializationTypeMismatch,
- "Attribute was supposed to be a #{object_class}, but was a #{obj.class}. -- #{obj.inspect}"
- end
+ assert_valid_value(obj, action: "dump")
YAML.dump obj
end
def load(yaml)
return object_class.new if object_class != Object && yaml.nil?
- return yaml unless yaml.is_a?(String) && yaml =~ /^---/
+ return yaml unless yaml.is_a?(String) && /^---/.match?(yaml)
obj = YAML.load(yaml)
- unless obj.is_a?(object_class) || obj.nil?
- raise SerializationTypeMismatch,
- "Attribute was supposed to be a #{object_class}, but was a #{obj.class}"
- end
+ assert_valid_value(obj, action: "load")
obj ||= object_class.new if object_class != Object
obj
end
+ def assert_valid_value(obj, action:)
+ unless obj.nil? || obj.is_a?(object_class)
+ raise SerializationTypeMismatch,
+ "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
+ def check_arity_of_constructor
load(nil)
rescue ArgumentError
raise ArgumentError, "Cannot serialize #{object_class}. Classes passed to `serialize` must have a 0 argument constructor."
end
- end
end
end
end
diff --git a/activerecord/lib/active_record/collection_cache_key.rb b/activerecord/lib/active_record/collection_cache_key.rb
new file mode 100644
index 0000000000..4b6db8a96c
--- /dev/null
+++ b/activerecord/lib/active_record/collection_cache_key.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module CollectionCacheKey
+ def collection_cache_key(collection = all, timestamp_column = :updated_at) # :nodoc:
+ query_signature = ActiveSupport::Digest.hexdigest(collection.to_sql)
+ key = "#{collection.model_name.cache_key}/query-#{query_signature}"
+
+ if collection.loaded? || collection.distinct_value
+ size = collection.records.size
+ if size > 0
+ timestamp = collection.max_by(&timestamp_column)._read_attribute(timestamp_column)
+ end
+ else
+ if collection.eager_loading?
+ collection = collection.send(:apply_join_dependency)
+ end
+ column_type = type_for_attribute(timestamp_column)
+ column = connection.visitor.compile(collection.arel_attribute(timestamp_column))
+ select_values = "COUNT(*) AS #{connection.quote_column_name("size")}, MAX(%s) AS timestamp"
+
+ if collection.has_limit_or_offset?
+ query = collection.select("#{column} AS collection_cache_key_timestamp")
+ subquery_alias = "subquery_for_cache_key"
+ subquery_column = "#{subquery_alias}.collection_cache_key_timestamp"
+ subquery = query.arel.as(subquery_alias)
+ arel = Arel::SelectManager.new(subquery).project(select_values % subquery_column)
+ else
+ query = collection.unscope(:order)
+ query.select_values = [select_values % column]
+ arel = query.arel
+ end
+
+ result = connection.select_one(arel, nil)
+
+ if result.blank?
+ size = 0
+ timestamp = nil
+ else
+ size = result["size"]
+ timestamp = column_type.deserialize(result["timestamp"])
+ end
+
+ end
+
+ if timestamp
+ "#{key}-#{size}-#{timestamp.utc.to_s(cache_timestamp_format)}"
+ else
+ "#{key}-#{size}"
+ end
+ 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 6535121075..99934a0e31 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -1,6 +1,8 @@
-require 'thread'
-require 'thread_safe'
-require 'monitor'
+# frozen_string_literal: true
+
+require "thread"
+require "concurrent/map"
+require "monitor"
module ActiveRecord
# Raised when a connection could not be obtained within the connection
@@ -10,8 +12,9 @@ module ActiveRecord
end
# Raised when a pool was unable to get ahold of all its connections
- # to perform a "group" action such as +ConnectionPool#disconnect!+
- # or +ConnectionPool#clear_reloadable_connections!+.
+ # to perform a "group" action such as
+ # {ActiveRecord::Base.connection_pool.disconnect!}[rdoc-ref:ConnectionAdapters::ConnectionPool#disconnect!]
+ # or {ActiveRecord::Base.clear_reloadable_connections!}[rdoc-ref:ConnectionAdapters::ConnectionHandler#clear_reloadable_connections!].
class ExclusiveConnectionTimeoutError < ConnectionTimeoutError
end
@@ -37,17 +40,18 @@ module ActiveRecord
# Connections can be obtained and used from a connection pool in several
# ways:
#
- # 1. Simply use ActiveRecord::Base.connection as with Active Record 2.1 and
+ # 1. Simply use {ActiveRecord::Base.connection}[rdoc-ref:ConnectionHandling.connection]
+ # as with Active Record 2.1 and
# earlier (pre-connection-pooling). Eventually, when you're done with
# the connection(s) and wish it to be returned to the pool, you call
- # ActiveRecord::Base.clear_active_connections!. This will be the
- # default behavior for Active Record when used in conjunction with
+ # {ActiveRecord::Base.clear_active_connections!}[rdoc-ref:ConnectionAdapters::ConnectionHandler#clear_active_connections!].
+ # This will be the default behavior for Active Record when used in conjunction with
# Action Pack's request handling cycle.
# 2. Manually check out a connection from the pool with
- # ActiveRecord::Base.connection_pool.checkout. You are responsible for
+ # {ActiveRecord::Base.connection_pool.checkout}[rdoc-ref:#checkout]. You are responsible for
# returning this connection to the pool when finished by calling
- # ActiveRecord::Base.connection_pool.checkin(connection).
- # 3. Use ActiveRecord::Base.connection_pool.with_connection(&block), which
+ # {ActiveRecord::Base.connection_pool.checkin(connection)}[rdoc-ref:#checkin].
+ # 3. Use {ActiveRecord::Base.connection_pool.with_connection(&block)}[rdoc-ref:#with_connection], which
# obtains a connection, yields it as the sole argument to the block,
# and returns it to the pool after the block completes.
#
@@ -59,30 +63,25 @@ module ActiveRecord
# There are several connection-pooling-related options that you can add to
# your database connection configuration:
#
- # * +pool+: number indicating size of connection pool (default 5)
- # * +checkout_timeout+: number of seconds to block and wait for a connection
- # before giving up and raising a timeout error (default 5 seconds).
- # * +reaping_frequency+: frequency in seconds to periodically run the
- # Reaper, which attempts to find and recover connections from dead
- # threads, which can occur if a programmer forgets to close a
- # connection at the end of a thread or a thread dies unexpectedly.
- # Regardless of this setting, the Reaper will be invoked before every
- # blocking wait. (Default nil, which means don't schedule the Reaper).
+ # * +pool+: maximum number of connections the pool may manage (default 5).
+ # * +idle_timeout+: number of seconds that a connection will be kept
+ # unused in the pool before it is automatically disconnected (default
+ # 300 seconds). Set this to zero to keep connections forever.
+ # * +checkout_timeout+: number of seconds to wait for a connection to
+ # become available before giving up and raising a timeout error (default
+ # 5 seconds).
#
#--
# Synchronization policy:
# * all public methods can be called outside +synchronize+
- # * access to these i-vars needs to be in +synchronize+:
+ # * access to these instance variables needs to be in +synchronize+:
# * @connections
# * @now_connecting
# * private methods that require being called in a +synchronize+ blocks
# are now explicitly documented
class ConnectionPool
- # Threadsafe, fair, FIFO queue. Meant to be used by ConnectionPool
- # with which it shares a Monitor. But could be a generic Queue.
- #
- # The Queue in stdlib's 'thread' could replace this class except
- # stdlib's doesn't support waiting with a timeout.
+ # Threadsafe, fair, LIFO queue. Meant to be used by ConnectionPool
+ # with which it shares a Monitor.
class Queue
def initialize(lock = Monitor.new)
@lock = lock
@@ -114,7 +113,7 @@ module ActiveRecord
end
end
- # If +element+ is in the queue, remove and return it, or nil.
+ # If +element+ is in the queue, remove and return it, or +nil+.
def delete(element)
synchronize do
@queue.delete(element)
@@ -133,14 +132,14 @@ module ActiveRecord
# If +timeout+ is not given, remove and return the head the
# queue if the number of available elements is strictly
# greater than the number of threads currently waiting (that
- # is, don't jump ahead in line). Otherwise, return nil.
+ # is, don't jump ahead in line). Otherwise, return +nil+.
#
# If +timeout+ is given, block if there is no element
# available, waiting up to +timeout+ seconds for an element to
# become available.
#
# Raises:
- # - ConnectionTimeoutError if +timeout+ is given and no element
+ # - ActiveRecord::ConnectionTimeoutError if +timeout+ is given and no element
# becomes available within +timeout+ seconds,
def poll(timeout = nil)
synchronize { internal_poll(timeout) }
@@ -148,61 +147,63 @@ module ActiveRecord
private
- def internal_poll(timeout)
- no_wait_poll || (timeout && wait_poll(timeout))
- end
+ def internal_poll(timeout)
+ no_wait_poll || (timeout && wait_poll(timeout))
+ end
- def synchronize(&block)
- @lock.synchronize(&block)
- end
+ def synchronize(&block)
+ @lock.synchronize(&block)
+ end
- # Test if the queue currently contains any elements.
- def any?
- !@queue.empty?
- end
+ # Test if the queue currently contains any elements.
+ def any?
+ !@queue.empty?
+ end
- # A thread can remove an element from the queue without
- # waiting if and only if the number of currently available
- # connections is strictly greater than the number of waiting
- # threads.
- def can_remove_no_wait?
- @queue.size > @num_waiting
- end
+ # A thread can remove an element from the queue without
+ # waiting if and only if the number of currently available
+ # connections is strictly greater than the number of waiting
+ # threads.
+ def can_remove_no_wait?
+ @queue.size > @num_waiting
+ end
- # Removes and returns the head of the queue if possible, or nil.
- def remove
- @queue.shift
- end
+ # Removes and returns the head of the queue if possible, or +nil+.
+ def remove
+ @queue.pop
+ end
- # Remove and return the head the queue if the number of
- # available elements is strictly greater than the number of
- # threads currently waiting. Otherwise, return nil.
- def no_wait_poll
- remove if can_remove_no_wait?
- end
+ # Remove and return the head the queue if the number of
+ # available elements is strictly greater than the number of
+ # threads currently waiting. Otherwise, return +nil+.
+ def no_wait_poll
+ remove if can_remove_no_wait?
+ end
- # Waits on the queue up to +timeout+ seconds, then removes and
- # returns the head of the queue.
- def wait_poll(timeout)
- @num_waiting += 1
+ # Waits on the queue up to +timeout+ seconds, then removes and
+ # returns the head of the queue.
+ def wait_poll(timeout)
+ @num_waiting += 1
- t0 = Time.now
- elapsed = 0
- loop do
- @cond.wait(timeout - elapsed)
+ t0 = Time.now
+ elapsed = 0
+ loop do
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
+ @cond.wait(timeout - elapsed)
+ end
- return remove if any?
+ return remove if any?
- elapsed = Time.now - t0
- if elapsed >= timeout
- msg = 'could not obtain a database connection within %0.3f seconds (waited %0.3f seconds)' %
- [timeout, elapsed]
- raise ConnectionTimeoutError, msg
+ elapsed = Time.now - t0
+ if elapsed >= timeout
+ msg = "could not obtain a connection from the pool within %0.3f seconds (waited %0.3f seconds); all pooled connections were in use" %
+ [timeout, elapsed]
+ raise ConnectionTimeoutError, msg
+ end
end
+ ensure
+ @num_waiting -= 1
end
- ensure
- @num_waiting -= 1
- end
end
# Adds the ability to turn a basic fair FIFO queue into one
@@ -266,25 +267,25 @@ module ActiveRecord
# Connections must be leased while holding the main pool mutex. This is
# an internal subclass that also +.leases+ returned connections while
# still in queue's critical section (queue synchronizes with the same
- # +@lock+ as the main pool) so that a returned connection is already
+ # <tt>@lock</tt> as the main pool) so that a returned connection is already
# leased and there is no need to re-enter synchronized block.
class ConnectionLeasingQueue < Queue # :nodoc:
include BiasableQueue
private
- def internal_poll(timeout)
- conn = super
- conn.lease if conn
- conn
- end
+ def internal_poll(timeout)
+ conn = super
+ conn.lease if conn
+ conn
+ end
end
- # Every +frequency+ seconds, the reaper will call +reap+ on +pool+.
- # A reaper instantiated with a nil frequency will never reap the
- # connection pool.
+ # Every +frequency+ seconds, the reaper will call +reap+ and +flush+ on
+ # +pool+. A reaper instantiated with a zero frequency will never reap
+ # the connection pool.
#
- # Configure the frequency by setting "reaping_frequency" in your
- # database yaml file.
+ # Configure the frequency by setting +reaping_frequency+ in your database
+ # yaml file (default 60 seconds).
class Reaper
attr_reader :pool, :frequency
@@ -294,17 +295,19 @@ module ActiveRecord
end
def run
- return unless frequency
+ return unless frequency && frequency > 0
Thread.new(frequency, pool) { |t, p|
- while true
+ loop do
sleep t
p.reap
+ p.flush
end
}
end
end
include MonitorMixin
+ include QueryCache::ConnectionPoolConfiguration
attr_accessor :automatic_reconnect, :checkout_timeout, :schema_cache
attr_reader :spec, :connections, :size, :reaper
@@ -321,36 +324,53 @@ module ActiveRecord
@spec = spec
@checkout_timeout = (spec.config[:checkout_timeout] && spec.config[:checkout_timeout].to_f) || 5
- @reaper = Reaper.new(self, (spec.config[:reaping_frequency] && spec.config[:reaping_frequency].to_f))
- @reaper.run
+ if @idle_timeout = spec.config.fetch(:idle_timeout, 300)
+ @idle_timeout = @idle_timeout.to_f
+ @idle_timeout = nil if @idle_timeout <= 0
+ end
# default max pool size to 5
@size = (spec.config[:pool] && spec.config[:pool].to_i) || 5
- # The cache of threads mapped to reserved connections, the sole purpose
- # of the cache is to speed-up +connection+ method, it is not the authoritative
- # registry of which thread owns which connection, that is tracked by
- # +connection.owner+ attr on each +connection+ instance.
- # The invariant works like this: if there is mapping of +thread => conn+,
- # then that +thread+ does indeed own that +conn+, however an absence of a such
- # mapping does not mean that the +thread+ doesn't own the said connection, in
+ # This variable tracks the cache of threads mapped to reserved connections, with the
+ # sole purpose of speeding up the +connection+ method. It is not the authoritative
+ # registry of which thread owns which connection. Connection ownership is tracked by
+ # the +connection.owner+ attr on each +connection+ instance.
+ # The invariant works like this: if there is mapping of <tt>thread => conn</tt>,
+ # then that +thread+ does indeed own that +conn+. However, an absence of a such
+ # mapping does not mean that the +thread+ doesn't own the said connection. In
# that case +conn.owner+ attr should be consulted.
- # Access and modification of +@thread_cached_conns+ does not require
+ # Access and modification of <tt>@thread_cached_conns</tt> does not require
# synchronization.
- @thread_cached_conns = ThreadSafe::Cache.new(:initial_capacity => @size)
+ @thread_cached_conns = Concurrent::Map.new(initial_capacity: @size)
@connections = []
@automatic_reconnect = true
- # Connection pool allows for concurrent (outside the main `synchronize` section)
+ # Connection pool allows for concurrent (outside the main +synchronize+ section)
# establishment of new connections. This variable tracks the number of threads
# currently in the process of independently establishing connections to the DB.
@now_connecting = 0
- # A boolean toggle that allows/disallows new connections.
- @new_cons_enabled = true
+ @threads_blocking_new_connections = 0
@available = ConnectionLeasingQueue.new self
+
+ @lock_thread = false
+
+ # +reaping_frequency+ is configurable mostly for historical reasons, but it could
+ # also be useful if someone wants a very low +idle_timeout+.
+ reaping_frequency = spec.config.fetch(:reaping_frequency, 60)
+ @reaper = Reaper.new(self, reaping_frequency && reaping_frequency.to_f)
+ @reaper.run
+ end
+
+ 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
@@ -359,13 +379,13 @@ 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
- # Is there an open connection that is being used for the current thread?
+ # Returns true if there is an open connection being used for the current thread.
#
- # This method only works for connections that have been abtained through
- # #connection or #with_connection methods, connections obtained through
+ # This method only works for connections that have been obtained through
+ # #connection or #with_connection methods. Connections obtained through
# #checkout will not be detected by #active_connection?
def active_connection?
@thread_cached_conns[connection_cache_key(Thread.current)]
@@ -406,14 +426,17 @@ module ActiveRecord
# Disconnects all connections in the pool, and clears the pool.
#
# Raises:
- # - +ExclusiveConnectionTimeoutError+ if unable to gain ownership of all
+ # - ActiveRecord::ExclusiveConnectionTimeoutError if unable to gain ownership of all
# connections in the pool within a timeout interval (default duration is
- # +spec.config[:checkout_timeout] * 2+ seconds).
+ # <tt>spec.config[:checkout_timeout] * 2</tt> seconds).
def disconnect(raise_on_acquisition_timeout = true)
with_exclusively_acquired_all_connections(raise_on_acquisition_timeout) do
synchronize do
@connections.each do |conn|
- checkin conn
+ if conn.in_use?
+ conn.steal!
+ checkin conn
+ end
conn.disconnect!
end
@connections = []
@@ -424,57 +447,58 @@ module ActiveRecord
# Disconnects all connections in the pool, and clears the pool.
#
- # The pool first tries to gain ownership of all connections, if unable to
+ # The pool first tries to gain ownership of all connections. If unable to
# do so within a timeout interval (default duration is
- # +spec.config[:checkout_timeout] * 2+ seconds), the pool is forcefully
- # disconneted wihout any regard for other connection owning threads.
+ # <tt>spec.config[:checkout_timeout] * 2</tt> seconds), then the pool is forcefully
+ # disconnected without any regard for other connection owning threads.
def disconnect!
disconnect(false)
end
+ # Discards all connections in the pool (even if they're currently
+ # leased!), along with the pool itself. Any further interaction with the
+ # pool (except #spec and #schema_cache) is undefined.
+ #
+ # See AbstractAdapter#discard!
+ def discard! # :nodoc:
+ synchronize do
+ return if @connections.nil? # already discarded
+ @connections.each do |conn|
+ conn.discard!
+ end
+ @connections = @available = @thread_cached_conns = nil
+ end
+ end
+
# Clears the cache which maps classes and re-connects connections that
# require reloading.
#
# Raises:
- # - +ExclusiveConnectionTimeoutError+ if unable to gain ownership of all
+ # - ActiveRecord::ExclusiveConnectionTimeoutError if unable to gain ownership of all
# connections in the pool within a timeout interval (default duration is
- # +spec.config[:checkout_timeout] * 2+ seconds).
+ # <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|
- checkin conn
+ if conn.in_use?
+ conn.steal!
+ checkin conn
+ end
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
# require reloading.
#
- # The pool first tries to gain ownership of all connections, if unable to
+ # The pool first tries to gain ownership of all connections. If unable to
# do so within a timeout interval (default duration is
- # +spec.config[:checkout_timeout] * 2+ seconds), the pool forcefully
+ # <tt>spec.config[:checkout_timeout] * 2</tt> seconds), then the pool forcefully
# clears the cache and reloads connections without any regard for other
# connection owning threads.
def clear_reloadable_connections!
@@ -494,7 +518,7 @@ module ActiveRecord
# Returns: an AbstractAdapter object.
#
# Raises:
- # - ConnectionTimeoutError: no connection can be obtained from the pool.
+ # - ActiveRecord::ConnectionTimeoutError no connection can be obtained from the pool.
def checkout(checkout_timeout = @checkout_timeout)
checkout_and_verify(acquire_connection(checkout_timeout))
end
@@ -503,20 +527,22 @@ module ActiveRecord
# no longer need this connection.
#
# +conn+: an AbstractAdapter object, which was obtained by earlier by
- # calling +checkout+ on this pool.
+ # calling #checkout on this pool.
def checkin(conn)
- synchronize do
- remove_connection_from_thread_cache conn
+ conn.lock.synchronize do
+ synchronize do
+ remove_connection_from_thread_cache conn
- conn.run_callbacks :checkin do
- conn.expire
- end
+ conn._run_checkin_callbacks do
+ conn.expire
+ end
- @available.add conn
+ @available.add conn
+ end
end
end
- # Remove a connection from the connection pool. The connection will
+ # Remove a connection from the connection pool. The connection will
# remain open and active but will no longer be managed by this pool.
def remove(conn)
needs_new_connection = false
@@ -528,256 +554,321 @@ module ActiveRecord
@available.delete conn
# @available.any_waiting? => true means that prior to removing this
- # conn, the pool was at its max size (@connections.size == @size)
- # this would mean that any threads stuck waiting in the queue wouldn't
+ # conn, the pool was at its max size (@connections.size == @size).
+ # This would mean that any threads stuck waiting in the queue wouldn't
# know they could checkout_new_connection, so let's do it for them.
# Because condition-wait loop is encapsulated in the Queue class
# (that in turn is oblivious to ConnectionPool implementation), threads
- # that are "stuck" there are helpless, they have no way of creating
+ # that are "stuck" there are helpless. They have no way of creating
# new connections and are completely reliant on us feeding available
# connections into the Queue.
needs_new_connection = @available.any_waiting?
end
# This is intentionally done outside of the synchronized section as we
- # would like not to hold the main mutex while checking out new connections,
- # thus there is some chance that needs_new_connection information is now
+ # would like not to hold the main mutex while checking out new connections.
+ # Thus there is some chance that needs_new_connection information is now
# stale, we can live with that (bulk_make_new_connections will make
# sure not to exceed the pool's @size limit).
bulk_make_new_connections(1) if needs_new_connection
end
- # Recover lost connections for the pool. A lost connection can occur if
+ # Recover lost connections for the pool. A lost connection can occur if
# a programmer forgets to checkin a connection at the end of a thread
# or a thread dies unexpectedly.
def reap
stale_connections = synchronize do
@connections.select do |conn|
conn.in_use? && !conn.owner.alive?
+ end.each do |conn|
+ conn.steal!
end
end
stale_connections.each do |conn|
- synchronize do
- if conn.active?
- conn.reset!
- checkin conn
- else
- remove conn
- end
+ if conn.active?
+ conn.reset!
+ checkin conn
+ else
+ remove conn
end
end
end
+ # Disconnect all connections that have been idle for at least
+ # +minimum_idle+ seconds. Connections currently checked out, or that were
+ # checked in less than +minimum_idle+ seconds ago, are unaffected.
+ def flush(minimum_idle = @idle_timeout)
+ return if minimum_idle.nil?
+
+ idle_connections = synchronize do
+ @connections.select do |conn|
+ !conn.in_use? && conn.seconds_idle >= minimum_idle
+ end.each do |conn|
+ conn.lease
+
+ @available.delete conn
+ @connections.delete conn
+ end
+ end
+
+ idle_connections.each do |conn|
+ conn.disconnect!
+ end
+ end
+
+ # Disconnect all currently idle connections. Connections currently checked
+ # out are unaffected.
+ def flush!
+ reap
+ flush(-1)
+ end
+
def num_waiting_in_queue # :nodoc:
@available.num_waiting
end
- private
- #--
- # this is unfortunately not concurrent
- def bulk_make_new_connections(num_new_conns_needed)
- num_new_conns_needed.times do
- # try_to_checkout_new_connection will not exceed pool's @size limit
- if new_conn = try_to_checkout_new_connection
- # make the new_conn available to the starving threads stuck @available Queue
- checkin(new_conn)
- end
- end
- end
-
- #--
- # From the discussion on Github:
- # https://github.com/rails/rails/pull/14938#commitcomment-6601951
- # This hook-in method allows for easier monkey-patching fixes needed by
- # JRuby users that use Fibers.
- def connection_cache_key(thread)
- thread
- end
-
- # Take control of all existing connections so a "group" action such as
- # reload/disconnect can be performed safely. It is no longer enough to
- # wrap it in +synchronize+ because some pool's actions are allowed
- # to be performed outside of the main +synchronize+ block.
- def with_exclusively_acquired_all_connections(raise_on_acquisition_timeout = true)
- with_new_connections_blocked do
- attempt_to_checkout_all_existing_connections(raise_on_acquisition_timeout)
- yield
+ # Return connection pool's usage statistic
+ # Example:
+ #
+ # ActiveRecord::Base.connection_pool.stat # => { size: 15, connections: 1, busy: 1, dead: 0, idle: 0, waiting: 0, checkout_timeout: 5 }
+ def stat
+ synchronize do
+ {
+ size: size,
+ connections: @connections.size,
+ busy: @connections.count { |c| c.in_use? && c.owner.alive? },
+ dead: @connections.count { |c| c.in_use? && !c.owner.alive? },
+ idle: @connections.count { |c| !c.in_use? },
+ waiting: num_waiting_in_queue,
+ checkout_timeout: checkout_timeout
+ }
end
end
- def attempt_to_checkout_all_existing_connections(raise_on_acquisition_timeout = true)
- collected_conns = synchronize do
- # account for our own connections
- @connections.select {|conn| conn.owner == Thread.current}
+ private
+ #--
+ # this is unfortunately not concurrent
+ def bulk_make_new_connections(num_new_conns_needed)
+ num_new_conns_needed.times do
+ # try_to_checkout_new_connection will not exceed pool's @size limit
+ if new_conn = try_to_checkout_new_connection
+ # make the new_conn available to the starving threads stuck @available Queue
+ checkin(new_conn)
+ end
+ end
end
- newly_checked_out = []
- timeout_time = Time.now + (@checkout_timeout * 2)
+ #--
+ # From the discussion on GitHub:
+ # https://github.com/rails/rails/pull/14938#commitcomment-6601951
+ # This hook-in method allows for easier monkey-patching fixes needed by
+ # JRuby users that use Fibers.
+ def connection_cache_key(thread)
+ thread
+ end
- @available.with_a_bias_for(Thread.current) do
- while true
- synchronize do
- return if collected_conns.size == @connections.size && @now_connecting == 0
- remaining_timeout = timeout_time - Time.now
- remaining_timeout = 0 if remaining_timeout < 0
- conn = checkout_for_exclusive_access(remaining_timeout)
- collected_conns << conn
- newly_checked_out << conn
- end
+ # Take control of all existing connections so a "group" action such as
+ # reload/disconnect can be performed safely. It is no longer enough to
+ # wrap it in +synchronize+ because some pool's actions are allowed
+ # to be performed outside of the main +synchronize+ block.
+ def with_exclusively_acquired_all_connections(raise_on_acquisition_timeout = true)
+ with_new_connections_blocked do
+ attempt_to_checkout_all_existing_connections(raise_on_acquisition_timeout)
+ yield
end
end
- rescue ExclusiveConnectionTimeoutError
- # `raise_on_acquisition_timeout == false` means we are directed to ignore any
- # timeouts and are expected to just give up: we've obtained as many connections
- # as possible, note that in a case like that we don't return any of the
- # `newly_checked_out` connections.
- if raise_on_acquisition_timeout
+ def attempt_to_checkout_all_existing_connections(raise_on_acquisition_timeout = true)
+ collected_conns = synchronize do
+ # account for our own connections
+ @connections.select { |conn| conn.owner == Thread.current }
+ end
+
+ newly_checked_out = []
+ timeout_time = Time.now + (@checkout_timeout * 2)
+
+ @available.with_a_bias_for(Thread.current) do
+ loop do
+ synchronize do
+ return if collected_conns.size == @connections.size && @now_connecting == 0
+ remaining_timeout = timeout_time - Time.now
+ remaining_timeout = 0 if remaining_timeout < 0
+ conn = checkout_for_exclusive_access(remaining_timeout)
+ collected_conns << conn
+ newly_checked_out << conn
+ end
+ end
+ end
+ rescue ExclusiveConnectionTimeoutError
+ # <tt>raise_on_acquisition_timeout == false</tt> means we are directed to ignore any
+ # timeouts and are expected to just give up: we've obtained as many connections
+ # as possible, note that in a case like that we don't return any of the
+ # +newly_checked_out+ connections.
+
+ if raise_on_acquisition_timeout
+ release_newly_checked_out = true
+ raise
+ end
+ rescue Exception # if something else went wrong
+ # this can't be a "naked" rescue, because we have should return conns
+ # even for non-StandardErrors
release_newly_checked_out = true
raise
+ ensure
+ if release_newly_checked_out && newly_checked_out
+ # releasing only those conns that were checked out in this method, conns
+ # checked outside this method (before it was called) are not for us to release
+ newly_checked_out.each { |conn| checkin(conn) }
+ end
end
- rescue Exception # if something else went wrong
- # this can't be a "naked" rescue, because we have should return conns
- # even for non-StandardErrors
- release_newly_checked_out = true
- raise
- ensure
- if release_newly_checked_out && newly_checked_out
- # releasing only those conns that were checked out in this method, conns
- # checked outside this method (before it was called) are not for us to release
- newly_checked_out.each {|conn| checkin(conn)}
- end
- end
- #--
- # Must be called in a synchronize block.
- def checkout_for_exclusive_access(checkout_timeout)
- checkout(checkout_timeout)
- rescue ConnectionTimeoutError
- # this block can't be easily moved into attempt_to_checkout_all_existing_connections's
- # rescue block, because doing so would put it outside of synchronize section, without
- # being in a critical section thread_report might become inaccurate
- msg = "could not obtain ownership of all database connections in #{checkout_timeout} seconds"
-
- thread_report = []
- @connections.each do |conn|
- unless conn.owner == Thread.current
- thread_report << "#{conn} is owned by #{conn.owner}"
+ #--
+ # Must be called in a synchronize block.
+ def checkout_for_exclusive_access(checkout_timeout)
+ checkout(checkout_timeout)
+ rescue ConnectionTimeoutError
+ # this block can't be easily moved into attempt_to_checkout_all_existing_connections's
+ # rescue block, because doing so would put it outside of synchronize section, without
+ # being in a critical section thread_report might become inaccurate
+ msg = +"could not obtain ownership of all database connections in #{checkout_timeout} seconds"
+
+ thread_report = []
+ @connections.each do |conn|
+ unless conn.owner == Thread.current
+ thread_report << "#{conn} is owned by #{conn.owner}"
+ end
end
+
+ msg << " (#{thread_report.join(', ')})" if thread_report.any?
+
+ raise ExclusiveConnectionTimeoutError, msg
end
- msg << " (#{thread_report.join(', ')})" if thread_report.any?
+ def with_new_connections_blocked
+ synchronize do
+ @threads_blocking_new_connections += 1
+ end
- raise ExclusiveConnectionTimeoutError, msg
- end
+ yield
+ ensure
+ num_new_conns_required = 0
- def with_new_connections_blocked
- previous_value = nil
- synchronize do
- previous_value, @new_cons_enabled = @new_cons_enabled, false
- end
- yield
- ensure
- synchronize { @new_cons_enabled = previous_value }
- end
+ synchronize do
+ @threads_blocking_new_connections -= 1
- # Acquire a connection by one of 1) immediately removing one
- # from the queue of available connections, 2) creating a new
- # connection if the pool is not at capacity, 3) waiting on the
- # queue for a connection to become available.
- #
- # Raises:
- # - ConnectionTimeoutError if a connection could not be acquired
- #
- #--
- # Implementation detail: the connection returned by +acquire_connection+
- # will already be "+connection.lease+ -ed" to the current thread.
- def acquire_connection(checkout_timeout)
- # NOTE: we rely on `@available.poll` and `try_to_checkout_new_connection` to
- # `conn.lease` the returned connection (and to do this in a `synchronized`
- # section), this is not the cleanest implementation, as ideally we would
- # `synchronize { conn.lease }` in this method, but by leaving it to `@available.poll`
- # and `try_to_checkout_new_connection` we can piggyback on `synchronize` sections
- # of the said methods and avoid an additional `synchronize` overhead.
- if conn = @available.poll || try_to_checkout_new_connection
- conn
- else
- reap
- @available.poll(checkout_timeout)
+ 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
- end
- #--
- # if owner_thread param is omitted, this must be called in synchronize block
- def remove_connection_from_thread_cache(conn, owner_thread = conn.owner)
- @thread_cached_conns.delete_pair(connection_cache_key(owner_thread), conn)
- end
- alias_method :release, :remove_connection_from_thread_cache
+ # Acquire a connection by one of 1) immediately removing one
+ # from the queue of available connections, 2) creating a new
+ # connection if the pool is not at capacity, 3) waiting on the
+ # queue for a connection to become available.
+ #
+ # Raises:
+ # - ActiveRecord::ConnectionTimeoutError if a connection could not be acquired
+ #
+ #--
+ # Implementation detail: the connection returned by +acquire_connection+
+ # will already be "+connection.lease+ -ed" to the current thread.
+ def acquire_connection(checkout_timeout)
+ # NOTE: we rely on <tt>@available.poll</tt> and +try_to_checkout_new_connection+ to
+ # +conn.lease+ the returned connection (and to do this in a +synchronized+
+ # section). This is not the cleanest implementation, as ideally we would
+ # <tt>synchronize { conn.lease }</tt> in this method, but by leaving it to <tt>@available.poll</tt>
+ # and +try_to_checkout_new_connection+ we can piggyback on +synchronize+ sections
+ # of the said methods and avoid an additional +synchronize+ overhead.
+ if conn = @available.poll || try_to_checkout_new_connection
+ conn
+ else
+ reap
+ @available.poll(checkout_timeout)
+ end
+ end
- def new_connection
- Base.send(spec.adapter_method, spec.config).tap do |conn|
- conn.schema_cache = schema_cache.dup if schema_cache
+ #--
+ # if owner_thread param is omitted, this must be called in synchronize block
+ def remove_connection_from_thread_cache(conn, owner_thread = conn.owner)
+ @thread_cached_conns.delete_pair(connection_cache_key(owner_thread), conn)
end
- end
+ alias_method :release, :remove_connection_from_thread_cache
- # If the pool is not at a +@size+ limit, establish new connection. Connecting
- # to the DB is done outside main synchronized section.
- #--
- # Implementation constraint: a newly established connection returned by this
- # method must be in the +.leased+ state.
- def try_to_checkout_new_connection
- # first in synchronized section check if establishing new conns is allowed
- # and increment @now_connecting, to prevent overstepping this pool's @size
- # constraint
- do_checkout = synchronize do
- if @new_cons_enabled && (@connections.size + @now_connecting) < @size
- @now_connecting += 1
+ def new_connection
+ Base.send(spec.adapter_method, spec.config).tap do |conn|
+ conn.schema_cache = schema_cache.dup if schema_cache
end
end
- if do_checkout
- begin
- # if successfully incremented @now_connecting establish new connection
- # outside of synchronized section
- conn = checkout_new_connection
- ensure
- synchronize do
- if conn
- adopt_connection(conn)
- # returned conn needs to be already leased
- conn.lease
+
+ # If the pool is not at a <tt>@size</tt> limit, establish new connection. Connecting
+ # to the DB is done outside main synchronized section.
+ #--
+ # Implementation constraint: a newly established connection returned by this
+ # method must be in the +.leased+ state.
+ def try_to_checkout_new_connection
+ # first in synchronized section check if establishing new conns is allowed
+ # and increment @now_connecting, to prevent overstepping this pool's @size
+ # constraint
+ do_checkout = synchronize do
+ if @threads_blocking_new_connections.zero? && (@connections.size + @now_connecting) < @size
+ @now_connecting += 1
+ end
+ end
+ if do_checkout
+ begin
+ # if successfully incremented @now_connecting establish new connection
+ # outside of synchronized section
+ conn = checkout_new_connection
+ ensure
+ synchronize do
+ if conn
+ adopt_connection(conn)
+ # returned conn needs to be already leased
+ conn.lease
+ end
+ @now_connecting -= 1
end
- @now_connecting -= 1
end
end
end
- end
- def adopt_connection(conn)
- conn.pool = self
- @connections << conn
- end
+ def adopt_connection(conn)
+ conn.pool = self
+ @connections << conn
+ end
- def checkout_new_connection
- raise ConnectionNotEstablished unless @automatic_reconnect
- new_connection
- end
+ def checkout_new_connection
+ raise ConnectionNotEstablished unless @automatic_reconnect
+ new_connection
+ end
- def checkout_and_verify(c)
- c.run_callbacks :checkout do
- c.verify!
+ def checkout_and_verify(c)
+ c._run_checkout_callbacks do
+ c.verify!
+ end
+ c
+ rescue
+ remove c
+ c.disconnect!
+ raise
end
- c
- rescue
- remove c
- c.disconnect!
- raise
- end
end
# ConnectionHandler is a collection of ConnectionPool objects. It is used
- # for keeping separate connection pools for Active Record models that connect
- # to different databases.
+ # for keeping separate connection pools that connect to different databases.
#
# For example, suppose that you have 5 models, with the following hierarchy:
#
@@ -788,7 +879,7 @@ module ActiveRecord
# end
#
# class Book < ActiveRecord::Base
- # establish_connection "library_db"
+ # establish_connection :library_db
# end
#
# class ScaryBook < Book
@@ -819,28 +910,63 @@ module ActiveRecord
# ConnectionHandler accessible via ActiveRecord::Base.connection_handler.
# All Active Record models use this handler to determine the connection pool that they
# should use.
+ #
+ # The ConnectionHandler class is not coupled with the Active models, as it has no knowledge
+ # about the model. The model needs to pass a specification name to the handler,
+ # in order to look up the correct connection pool.
class ConnectionHandler
- def initialize
- # These caches are keyed by klass.name, NOT klass. Keying them by klass
- # alone would lead to memory leaks in development mode as all previous
- # instances of the class would stay in memory.
- @owner_to_pool = ThreadSafe::Cache.new(:initial_capacity => 2) do |h,k|
- h[k] = ThreadSafe::Cache.new(:initial_capacity => 2)
+ def self.unowned_pool_finalizer(pid_map) # :nodoc:
+ lambda do |_|
+ discard_unowned_pools(pid_map)
end
- @class_to_pool = ThreadSafe::Cache.new(:initial_capacity => 2) do |h,k|
- h[k] = ThreadSafe::Cache.new
+ end
+
+ def self.discard_unowned_pools(pid_map) # :nodoc:
+ pid_map.each do |pid, pools|
+ pools.values.compact.each(&:discard!) unless pid == Process.pid
end
end
+ def initialize
+ # These caches are keyed by spec.name (ConnectionSpecification#name).
+ @owner_to_pool = Concurrent::Map.new(initial_capacity: 2) do |h, k|
+ # Discard the parent's connection pools immediately; we have no need
+ # of them
+ ConnectionHandler.discard_unowned_pools(h)
+
+ h[k] = Concurrent::Map.new(initial_capacity: 2)
+ end
+
+ # Backup finalizer: if the forked child never needed a pool, the above
+ # early discard has not occurred
+ ObjectSpace.define_finalizer self, ConnectionHandler.unowned_pool_finalizer(@owner_to_pool)
+ end
+
def connection_pool_list
owner_to_pool.values.compact
end
alias :connection_pools :connection_pool_list
- def establish_connection(owner, spec)
- @class_to_pool.clear
- raise RuntimeError, "Anonymous class is not allowed." unless owner.name
- owner_to_pool[owner.name] = ConnectionAdapters::ConnectionPool.new(spec)
+ def establish_connection(config)
+ resolver = ConnectionSpecification::Resolver.new(Base.configurations)
+ spec = resolver.spec(config)
+
+ remove_connection(spec.name)
+
+ message_bus = ActiveSupport::Notifications.instrumenter
+ payload = {
+ connection_id: object_id
+ }
+ if spec
+ payload[:spec_name] = spec.name
+ payload[:config] = spec.config
+ end
+
+ message_bus.instrument("!connection.active_record", payload) do
+ owner_to_pool[spec.name] = ConnectionAdapters::ConnectionPool.new(spec)
+ end
+
+ owner_to_pool[spec.name]
end
# Returns true if there are any active connections among the connection
@@ -857,6 +983,8 @@ module ActiveRecord
end
# Clears the cache which maps classes.
+ #
+ # See ConnectionPool#clear_reloadable_connections! for details.
def clear_reloadable_connections!
connection_pool_list.each(&:clear_reloadable_connections!)
end
@@ -865,107 +993,72 @@ module ActiveRecord
connection_pool_list.each(&:disconnect!)
end
+ # Disconnects all currently idle connections.
+ #
+ # See ConnectionPool#flush! for details.
+ def flush_idle_connections!
+ connection_pool_list.each(&:flush!)
+ end
+
# Locate the connection of the nearest super class. This can be an
# active or defined connection: if it is the latter, it will be
# opened and set as the active connection for the class it was defined
# for (not necessarily the current class).
- def retrieve_connection(klass) #:nodoc:
- pool = retrieve_connection_pool(klass)
- raise ConnectionNotEstablished, "No connection pool for #{klass}" unless pool
- conn = pool.connection
- raise ConnectionNotEstablished, "No connection for #{klass} in connection pool" unless conn
- conn
+ def retrieve_connection(spec_name) #:nodoc:
+ pool = retrieve_connection_pool(spec_name)
+ raise ConnectionNotEstablished, "No connection pool with '#{spec_name}' found." unless pool
+ pool.connection
end
# Returns true if a connection that's accessible to this class has
# already been opened.
- def connected?(klass)
- conn = retrieve_connection_pool(klass)
- conn && conn.connected?
+ def connected?(spec_name)
+ pool = retrieve_connection_pool(spec_name)
+ pool && pool.connected?
end
# Remove the connection for this class. This will close the active
# connection and the defined connection (if they exist). The result
- # can be used as an argument for establish_connection, for easily
+ # can be used as an argument for #establish_connection, for easily
# re-establishing the connection.
- def remove_connection(owner)
- if pool = owner_to_pool.delete(owner.name)
- @class_to_pool.clear
+ def remove_connection(spec_name)
+ if pool = owner_to_pool.delete(spec_name)
pool.automatic_reconnect = false
pool.disconnect!
pool.spec.config
end
end
- # Retrieving the connection pool happens a lot so we cache it in @class_to_pool.
+ # Retrieving the connection pool happens a lot, so we cache it in @owner_to_pool.
# This makes retrieving the connection pool O(1) once the process is warm.
# When a connection is established or removed, we invalidate the cache.
- #
- # Ideally we would use #fetch here, as class_to_pool[klass] may sometimes be nil.
- # However, benchmarking (https://gist.github.com/jonleighton/3552829) showed that
- # #fetch is significantly slower than #[]. So in the nil case, no caching will
- # take place, but that's ok since the nil case is not the common one that we wish
- # to optimise for.
- def retrieve_connection_pool(klass)
- class_to_pool[klass.name] ||= begin
- until pool = pool_for(klass)
- klass = klass.superclass
- break unless klass <= Base
- end
-
- class_to_pool[klass.name] = pool
- end
- end
-
- private
-
- def owner_to_pool
- @owner_to_pool[Process.pid]
- end
-
- def class_to_pool
- @class_to_pool[Process.pid]
- end
-
- def pool_for(owner)
- owner_to_pool.fetch(owner.name) {
- if ancestor_pool = pool_from_any_process_for(owner)
+ def retrieve_connection_pool(spec_name)
+ owner_to_pool.fetch(spec_name) do
+ # Check if a connection was previously established in an ancestor process,
+ # which may have been forked.
+ if ancestor_pool = pool_from_any_process_for(spec_name)
# A connection was established in an ancestor process that must have
# subsequently forked. We can't reuse the connection, but we can copy
# the specification and establish a new connection with it.
- establish_connection(owner, ancestor_pool.spec).tap do |pool|
+ establish_connection(ancestor_pool.spec.to_hash).tap do |pool|
pool.schema_cache = ancestor_pool.schema_cache if ancestor_pool.schema_cache
end
else
- owner_to_pool[owner.name] = nil
+ owner_to_pool[spec_name] = nil
end
- }
- end
-
- def pool_from_any_process_for(owner)
- owner_to_pool = @owner_to_pool.values.find { |v| v[owner.name] }
- owner_to_pool && owner_to_pool[owner.name]
- end
- end
-
- class ConnectionManagement
- def initialize(app)
- @app = app
+ end
end
- def call(env)
- testing = env['rack.test']
+ private
- response = @app.call(env)
- response[2] = ::Rack::BodyProxy.new(response[2]) do
- ActiveRecord::Base.clear_active_connections! unless testing
+ def owner_to_pool
+ @owner_to_pool[Process.pid]
end
- response
- rescue Exception
- ActiveRecord::Base.clear_active_connections! unless testing
- raise
- end
+ def pool_from_any_process_for(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
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
index 30b2fca2ca..1305216be2 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
@@ -1,7 +1,10 @@
+# frozen_string_literal: true
+
+require "active_support/deprecation"
+
module ActiveRecord
module ConnectionAdapters # :nodoc:
module DatabaseLimits
-
# Returns the maximum length of a table alias.
def table_alias_length
255
@@ -11,16 +14,18 @@ module ActiveRecord
def column_name_length
64
end
+ deprecate :column_name_length
# Returns the maximum length of a table name.
def table_name_length
64
end
+ deprecate :table_name_length
# Returns the maximum allowed length for an index name. This
# limit is enforced by \Rails and is less than or equal to
- # <tt>index_name_length</tt>. The gap between
- # <tt>index_name_length</tt> is to allow internal \Rails
+ # #index_name_length. The gap between
+ # #index_name_length is to allow internal \Rails
# operations to use prefixes in temporary operations.
def allowed_index_name_length
index_name_length
@@ -35,19 +40,22 @@ module ActiveRecord
def columns_per_table
1024
end
+ deprecate :columns_per_table
# Returns the maximum number of indexes per table.
def indexes_per_table
16
end
+ deprecate :indexes_per_table
# Returns the maximum number of columns in a multicolumn index.
def columns_per_multicolumn_index
16
end
+ deprecate :columns_per_multicolumn_index
# Returns the maximum number of elements in an IN (x,y,z) clause.
- # nil means no limit.
+ # +nil+ means no limit.
def in_clause_length
nil
end
@@ -56,12 +64,18 @@ module ActiveRecord
def sql_query_length
1048575
end
+ deprecate :sql_query_length
# Returns maximum number of joins in a single query.
def joins_per_query
256
end
+ deprecate :joins_per_query
+ private
+ def bind_params_length
+ 65535
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
index 38dd9578fe..0059f0b773 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters # :nodoc:
module DatabaseStatements
@@ -7,29 +9,58 @@ module ActiveRecord
end
# Converts an arel AST to SQL
- def to_sql(arel, binds = [])
- if arel.respond_to?(:ast)
- collected = visitor.accept(arel.ast, collector)
- collected.compile(binds.dup, self)
+ def to_sql(arel_or_sql_string, binds = [])
+ sql, _ = to_sql_and_binds(arel_or_sql_string, binds)
+ sql
+ end
+
+ def to_sql_and_binds(arel_or_sql_string, binds = []) # :nodoc:
+ if arel_or_sql_string.respond_to?(:ast)
+ unless binds.empty?
+ raise "Passing bind parameters with an arel AST is forbidden. " \
+ "The values must be stored on the AST directly"
+ end
+ sql, binds = visitor.compile(arel_or_sql_string.ast, collector)
+ [sql.freeze, binds || []]
else
- arel
+ [arel_or_sql_string.dup.freeze, binds]
end
end
+ private :to_sql_and_binds
# This is used in the StatementCache object. It returns an object that
# can be used to query the database repeatedly.
- def cacheable_query(arel) # :nodoc:
+ def cacheable_query(klass, arel) # :nodoc:
if prepared_statements
- ActiveRecord::StatementCache.query visitor, arel.ast
+ sql, binds = visitor.compile(arel.ast, collector)
+ query = klass.query(sql)
else
- ActiveRecord::StatementCache.partial_query visitor, arel.ast, collector
+ collector = klass.partial_query_collector
+ parts, binds = visitor.compile(arel.ast, collector)
+ query = klass.partial_query(parts)
end
+ [query, binds]
end
# Returns an ActiveRecord::Result instance.
- def select_all(arel, name = nil, binds = [])
- arel, binds = binds_from_relation arel, binds
- select(to_sql(arel, binds), name, binds)
+ def select_all(arel, name = nil, binds = [], preparable: nil)
+ arel = arel_from_relation(arel)
+ sql, binds = to_sql_and_binds(arel, binds)
+
+ if !prepared_statements || (arel.is_a?(String) && preparable.nil?)
+ preparable = false
+ elsif binds.length > bind_params_length
+ sql, binds = unprepared_statement { to_sql_and_binds(arel) }
+ preparable = false
+ else
+ preparable = visitor.preparable
+ end
+
+ if prepared_statements && preparable
+ select_prepared(sql, name, binds)
+ else
+ select(sql, name, binds)
+ end
end
# Returns a record hash with the column names as keys and column values
@@ -40,47 +71,61 @@ 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
- result.first
- end
+ single_value_from_rows(select_rows(arel, name, binds))
end
# 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)
- arel, binds = binds_from_relation arel, []
- select_rows(to_sql(arel, binds), name, binds).map(&:first)
+ def select_values(arel, name = nil, binds = [])
+ 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 = [])
+ def select_rows(arel, name = nil, binds = [])
+ select_all(arel, name, binds).rows
+ end
+
+ def query_value(sql, name = nil) # :nodoc:
+ single_value_from_rows(query(sql, name))
+ end
+
+ def query_values(sql, name = nil) # :nodoc:
+ query(sql, name).map(&:first)
end
- undef_method :select_rows
- # Executes the SQL statement in the context of this connection.
+ def query(sql, name = nil) # :nodoc:
+ exec_query(sql, name).rows
+ end
+
+ # Executes the SQL statement in the context of this connection and returns
+ # the raw result from the connection adapter.
+ # Note: depending on your database connector, the result returned by this
+ # method may be manually memory managed. Consider using the exec_query
+ # wrapper instead.
def execute(sql, name = nil)
+ raise NotImplementedError
end
- undef_method :execute
# Executes +sql+ statement in the context of this connection using
# +binds+ as the bind substitutes. +name+ is logged along with
# the executed +sql+ statement.
- def exec_query(sql, name = 'SQL', binds = [])
+ def exec_query(sql, name = "SQL", binds = [], prepare: false)
+ raise NotImplementedError
end
# Executes insert +sql+ statement in the context of this connection using
# +binds+ as the bind substitutes. +name+ is logged along with
# the executed +sql+ statement.
- def exec_insert(sql, name, binds, pk = nil, sequence_name = nil)
+ def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil)
+ sql, binds = sql_for_insert(sql, pk, nil, sequence_name, binds)
exec_query(sql, name, binds)
end
# Executes delete +sql+ statement in the context of this connection using
# +binds+ as the bind substitutes. +name+ is logged along with
# the executed +sql+ statement.
- def exec_delete(sql, name, binds)
+ def exec_delete(sql, name = nil, binds = [])
exec_query(sql, name, binds)
end
@@ -92,39 +137,43 @@ module ActiveRecord
# Executes update +sql+ statement in the context of this connection using
# +binds+ as the bind substitutes. +name+ is logged along with
# the executed +sql+ statement.
- def exec_update(sql, name, binds)
+ def exec_update(sql, name = nil, binds = [])
exec_query(sql, name, binds)
end
- # Returns the last auto-generated ID from the affected table.
+ # Executes an INSERT query and returns the new record's ID
#
- # +id_value+ will be returned unless the value is nil, in
+ # +id_value+ will be returned unless the value is +nil+, in
# which case the database will attempt to calculate the last inserted
# id and return that value.
#
# If the next id was calculated in advance (as in Oracle), it should be
# passed in as +id_value+.
def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [])
- sql, binds = sql_for_insert(to_sql(arel, binds), pk, id_value, sequence_name, binds)
- value = exec_insert(sql, name, binds, pk, sequence_name)
+ sql, binds = to_sql_and_binds(arel, binds)
+ value = exec_insert(sql, name, binds, pk, sequence_name)
id_value || last_inserted_id(value)
end
+ alias create 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)
+ sql, binds = to_sql_and_binds(arel, binds)
+ exec_update(sql, name, binds)
end
# 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)
+ sql, binds = to_sql_and_binds(arel, binds)
+ exec_delete(sql, name, binds)
end
# Returns +true+ when the connection adapter supports prepared statement
# caching, otherwise returns +false+
- def supports_statement_cache?
- false
+ def supports_statement_cache? # :nodoc:
+ true
end
+ deprecate :supports_statement_cache?
# Runs the given block in a database transaction, and returns the result
# of the block.
@@ -137,7 +186,7 @@ module ActiveRecord
#
# In order to get around this problem, #transaction will emulate the effect
# of nested transactions, by using savepoints:
- # http://dev.mysql.com/doc/refman/5.6/en/savepoint.html
+ # https://dev.mysql.com/doc/refman/5.7/en/savepoint.html
# Savepoints are supported by MySQL and PostgreSQL. SQLite3 version >= '3.6.8'
# supports savepoints.
#
@@ -189,19 +238,17 @@ module ActiveRecord
# You should consult the documentation for your database to understand the
# semantics of these different levels:
#
- # * http://www.postgresql.org/docs/current/static/transaction-iso.html
- # * https://dev.mysql.com/doc/refman/5.6/en/set-transaction.html
+ # * https://www.postgresql.org/docs/current/static/transaction-iso.html
+ # * https://dev.mysql.com/doc/refman/5.7/en/set-transaction.html
#
- # An <tt>ActiveRecord::TransactionIsolationError</tt> will be raised if:
+ # An ActiveRecord::TransactionIsolationError will be raised if:
#
# * The adapter does not support setting the isolation level
# * You are joining an existing open transaction
# * You are creating a nested (savepoint) transaction
#
- # The mysql, mysql2 and postgresql adapters support setting the transaction
- # isolation level. However, support is disabled for MySQL versions below 5,
- # because they are affected by a bug[http://bugs.mysql.com/bug.php?id=39170]
- # which means the isolation level gets persisted outside the transaction.
+ # The mysql2 and postgresql adapters support setting the transaction
+ # isolation level.
def transaction(requires_new: nil, isolation: nil, joinable: true)
if !requires_new && current_transaction.joinable?
if isolation
@@ -217,14 +264,16 @@ module ActiveRecord
attr_reader :transaction_manager #:nodoc:
- delegate :within_new_transaction, :open_transactions, :current_transaction, :begin_transaction, :commit_transaction, :rollback_transaction, to: :transaction_manager
+ delegate :within_new_transaction, :open_transactions, :current_transaction, :begin_transaction,
+ :commit_transaction, :rollback_transaction, :materialize_transactions,
+ :disable_lazy_transactions!, :enable_lazy_transactions!, to: :transaction_manager
def transaction_open?
current_transaction.open?
end
def reset_transaction #:nodoc:
- @transaction_manager = TransactionManager.new(self)
+ @transaction_manager = ConnectionAdapters::TransactionManager.new(self)
end
# Register a record with the current transaction so that its after_commit and after_rollback callbacks
@@ -271,9 +320,6 @@ module ActiveRecord
exec_rollback_to_savepoint(name)
end
- def exec_rollback_to_savepoint(name = nil) #:nodoc:
- end
-
def default_sequence_name(table, column)
nil
end
@@ -285,93 +331,132 @@ module ActiveRecord
# Inserts the given fixture into the table. Overridden in adapters that require
# something beyond a simple insert (eg. Oracle).
+ # Most of adapters should implement `insert_fixtures` that leverages bulk SQL insert.
+ # We keep this method to provide fallback
+ # for databases like sqlite that do not support bulk inserts.
def insert_fixture(fixture, table_name)
- columns = schema_cache.columns_hash(table_name)
+ fixture = fixture.stringify_keys
+ columns = schema_cache.columns_hash(table_name)
binds = fixture.map do |name, value|
- type = lookup_cast_type_from_column(columns[name])
- Relation::QueryAttribute.new(name, value, type)
- end
- key_list = fixture.keys.map { |name| quote_column_name(name) }
- value_list = prepare_binds_for_database(binds).map do |value|
- begin
- quote(value)
- rescue TypeError
- quote(YAML.dump(value))
+ if column = columns[name]
+ type = lookup_cast_type_from_column(column)
+ Relation::QueryAttribute.new(name, value, type)
+ else
+ raise Fixture::FixtureError, %(table "#{table_name}" has no column named #{name.inspect}.)
end
end
- execute "INSERT INTO #{quote_table_name(table_name)} (#{key_list.join(', ')}) VALUES (#{value_list.join(', ')})", 'Fixture Insert'
+ table = Arel::Table.new(table_name)
+
+ values = binds.map do |bind|
+ value = with_yaml_fallback(bind.value_for_database)
+ [table[bind.name], value]
+ end
+
+ manager = Arel::InsertManager.new
+ manager.into(table)
+ manager.insert(values)
+ execute manager.to_sql, "Fixture Insert"
end
- def empty_insert_statement_value
+ # Inserts a set of fixtures into the table. Overridden in adapters that require
+ # something beyond a simple insert (eg. Oracle).
+ def insert_fixtures(fixtures, table_name)
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ `insert_fixtures` is deprecated and will be removed in the next version of Rails.
+ Consider using `insert_fixtures_set` for performance improvement.
+ MSG
+ return if fixtures.empty?
+
+ execute(build_fixture_sql(fixtures, table_name), "Fixtures Insert")
+ end
+
+ def insert_fixtures_set(fixture_set, tables_to_delete = [])
+ fixture_inserts = fixture_set.map do |table_name, fixtures|
+ next if fixtures.empty?
+
+ build_fixture_sql(fixtures, table_name)
+ end.compact
+
+ table_deletes = tables_to_delete.map { |table| +"DELETE FROM #{quote_table_name table}" }
+ total_sql = Array.wrap(combine_multi_statements(table_deletes + fixture_inserts))
+
+ disable_referential_integrity do
+ transaction(requires_new: true) do
+ total_sql.each do |sql|
+ execute sql, "Fixtures Load"
+ yield if block_given?
+ end
+ end
+ end
+ end
+
+ def empty_insert_statement_value(primary_key = nil)
"DEFAULT VALUES"
end
# 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
end
- # The default strategy for an UPDATE with joins is to use a subquery. This doesn't work
- # on MySQL (even when aliasing the tables), but MySQL allows using JOIN directly in
- # an UPDATE statement, so in the MySQL adapters we redefine this to do that.
- def join_to_update(update, select) #:nodoc:
- key = update.key
- subselect = subquery_for(key, select)
-
- update.where key.in(subselect)
- end
+ private
+ def default_insert_value(column)
+ Arel.sql("DEFAULT")
+ end
- def join_to_delete(delete, select, key) #:nodoc:
- subselect = subquery_for(key, select)
+ def build_fixture_sql(fixtures, table_name)
+ columns = schema_cache.columns_hash(table_name)
+
+ values = fixtures.map do |fixture|
+ fixture = fixture.stringify_keys
+
+ unknown_columns = fixture.keys - columns.keys
+ if unknown_columns.any?
+ raise Fixture::FixtureError, %(table "#{table_name}" has no columns named #{unknown_columns.map(&:inspect).join(', ')}.)
+ end
+
+ columns.map do |name, column|
+ if fixture.key?(name)
+ type = lookup_cast_type_from_column(column)
+ bind = Relation::QueryAttribute.new(name, fixture[name], type)
+ with_yaml_fallback(bind.value_for_database)
+ else
+ default_insert_value(column)
+ end
+ end
+ end
- delete.where key.in(subselect)
- end
+ table = Arel::Table.new(table_name)
+ manager = Arel::InsertManager.new
+ manager.into(table)
+ columns.each_key { |column| manager.columns << table[column] }
+ manager.values = manager.create_values_list(values)
- protected
+ manager.to_sql
+ end
- # Returns a subquery for the given key using the join information.
- def subquery_for(key, select)
- subselect = select.clone
- subselect.projections = [key]
- subselect
+ def combine_multi_statements(total_sql)
+ total_sql.join(";\n")
end
# Returns an ActiveRecord::Result instance.
def select(sql, name = nil, binds = [])
- exec_query(sql, name, binds)
+ exec_query(sql, name, binds, prepare: false)
end
-
- # Returns the last auto-generated ID from the affected table.
- def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
- execute(sql, name)
- id_value
- end
-
- # Executes the update statement and returns the number of rows affected.
- def update_sql(sql, name = nil)
- execute(sql, name)
- end
-
- # Executes the delete statement and returns the number of rows affected.
- def delete_sql(sql, name = nil)
- update_sql(sql, name)
+ def select_prepared(sql, name = nil, binds = [])
+ exec_query(sql, name, binds, prepare: true)
end
def sql_for_insert(sql, pk, id_value, sequence_name, binds)
@@ -379,15 +464,31 @@ module ActiveRecord
end
def last_inserted_id(result)
- row = result.rows.first
+ single_value_from_rows(result.rows)
+ end
+
+ def single_value_from_rows(rows)
+ row = rows.first
row && row.first
end
- def binds_from_relation(relation, binds)
- if relation.is_a?(Relation) && binds.empty?
- relation, binds = relation.arel, relation.bound_attributes
+ def arel_from_relation(relation)
+ if relation.is_a?(Relation)
+ relation.arel
+ else
+ relation
+ end
+ end
+
+ # Fixture value is quoted by Arel, however scalar values
+ # are not quotable. In this case we want to convert
+ # the column value to YAML.
+ def with_yaml_fallback(value)
+ if value.is_a?(Hash) || value.is_a?(Array)
+ YAML.dump(value)
+ else
+ value
end
- [relation, binds]
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
index 5e27cfe507..8aeb934ec2 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
@@ -1,9 +1,16 @@
+# frozen_string_literal: true
+
+require "concurrent/map"
+
module ActiveRecord
module ConnectionAdapters # :nodoc:
module QueryCache
class << self
def included(base) #:nodoc:
dirties_query_cache base, :insert, :update, :delete, :rollback_to_savepoint, :rollback_db_transaction
+
+ base.set_callback :checkout, :after, :configure_query_cache!
+ base.set_callback :checkin, :after, :disable_query_cache!
end
def dirties_query_cache(base, *method_names)
@@ -18,11 +25,32 @@ module ActiveRecord
end
end
+ module ConnectionPoolConfiguration
+ def initialize(*)
+ super
+ @query_cache_enabled = Concurrent::Map.new { false }
+ end
+
+ def enable_query_cache!
+ @query_cache_enabled[connection_cache_key(Thread.current)] = true
+ connection.enable_query_cache! if active_connection?
+ end
+
+ def disable_query_cache!
+ @query_cache_enabled.delete connection_cache_key(Thread.current)
+ connection.disable_query_cache! if active_connection?
+ end
+
+ def query_cache_enabled
+ @query_cache_enabled[connection_cache_key(Thread.current)]
+ end
+ end
+
attr_reader :query_cache, :query_cache_enabled
def initialize(*)
super
- @query_cache = Hash.new { |h,sql| h[sql] = {} }
+ @query_cache = Hash.new { |h, sql| h[sql] = {} }
@query_cache_enabled = false
end
@@ -41,6 +69,7 @@ module ActiveRecord
def disable_query_cache!
@query_cache_enabled = false
+ clear_query_cache
end
# Disable the query cache within the block.
@@ -58,14 +87,16 @@ 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 = [])
+ def select_all(arel, name = nil, binds = [], preparable: nil)
if @query_cache_enabled && !locked?(arel)
- arel, binds = binds_from_relation arel, binds
- sql = to_sql(arel, binds)
- cache_sql(sql, binds) { super(sql, name, binds) }
+ arel = arel_from_relation(arel)
+ sql, binds = to_sql_and_binds(arel, binds)
+ cache_sql(sql, name, binds) { super(sql, name, binds, preparable: preparable) }
else
super
end
@@ -73,23 +104,45 @@ module ActiveRecord
private
- def cache_sql(sql, binds)
- result =
- if @query_cache[sql].key?(binds)
- ActiveSupport::Notifications.instrument("sql.active_record",
- :sql => sql, :binds => binds, :name => "CACHE", :connection_id => object_id)
- @query_cache[sql][binds]
- else
- @query_cache[sql][binds] = yield
+ def cache_sql(sql, name, binds)
+ @lock.synchronize do
+ result =
+ if @query_cache[sql].key?(binds)
+ ActiveSupport::Notifications.instrument(
+ "sql.active_record",
+ cache_notification_info(sql, name, binds)
+ )
+ @query_cache[sql][binds]
+ else
+ @query_cache[sql][binds] = yield
+ end
+ result.dup
end
- result.dup
- end
+ end
- # If arel is locked this is a SELECT ... FOR UPDATE or somesuch. Such
- # queries should not be cached.
- def locked?(arel)
- arel.respond_to?(:locked) && arel.locked
- end
+ # Database adapters can override this method to
+ # provide custom cache information.
+ def cache_notification_info(sql, name, binds)
+ {
+ sql: sql,
+ binds: binds,
+ type_casted_binds: -> { type_casted_binds(binds) },
+ name: name,
+ connection_id: object_id,
+ cached: true
+ }
+ end
+
+ # If arel is locked this is a SELECT ... FOR UPDATE or somesuch. Such
+ # queries should not be cached.
+ def locked?(arel)
+ arel = arel.arel if arel.is_a?(Relation)
+ arel.respond_to?(:locked) && arel.locked
+ end
+
+ def configure_query_cache!
+ enable_query_cache! if pool.query_cache_enabled
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
index 2c7409b2dc..07e86afe9a 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'
+# frozen_string_literal: true
+
+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)
+ # {SQL injection attacks}[https://en.wikipedia.org/wiki/SQL_injection].
+ 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?(:value_for_database)
+ value = value.value_for_database
end
_quote(value)
@@ -26,9 +22,7 @@ module ActiveRecord
# SQLite does not understand dates, so this method will convert a Date
# to a String.
def type_cast(value, column = nil)
- if value.respond_to?(:quoted_id) && value.respond_to?(:id)
- return value.id
- end
+ value = id_value_for_database(value) if value.is_a?(Base)
if column
value = type_cast_from_column(column, value)
@@ -43,9 +37,9 @@ module ActiveRecord
# If you are having to call this function, you are likely doing something
# wrong. The column does not have sufficient type information if the user
# provided a custom type on the class level either explicitly (via
- # `attribute`) or implicitly (via `serialize`,
- # `time_zone_aware_attributes`). In almost all cases, the sql type should
- # only be used to change quoting behavior, when the primitive to
+ # Attributes::ClassMethods#attribute) or implicitly (via
+ # AttributeMethods::Serialization::ClassMethods#serialize, +time_zone_aware_attributes+).
+ # In almost all cases, the sql type should only be used to change quoting behavior, when the primitive to
# represent the type doesn't sufficiently reflect the differences
# (varchar vs binary) for example. The type used to get this primitive
# should have been provided before reaching the connection adapter.
@@ -58,31 +52,20 @@ module ActiveRecord
end
end
- # See docs for +type_cast_from_column+
+ # See docs for #type_cast_from_column
def lookup_cast_type_from_column(column) # :nodoc:
lookup_cast_type(column.sql_type)
end
- def fetch_type_metadata(sql_type)
- cast_type = lookup_cast_type(sql_type)
- SqlTypeMetadata.new(
- sql_type: sql_type,
- type: cast_type.type,
- limit: cast_type.limit,
- precision: cast_type.precision,
- scale: cast_type.scale,
- )
- end
-
# Quotes a string, escaping any ' (single quote) and \ (backslash)
# characters.
def quote_string(s)
- s.gsub('\\'.freeze, '\&\&'.freeze).gsub("'".freeze, "''".freeze) # ' (for ruby-mode)
+ s.gsub('\\', '\&\&').gsub("'", "''") # ' (for ruby-mode)
end
# Quotes the column name. Defaults to no quoting.
def quote_column_name(column_name)
- column_name
+ column_name.to_s
end
# Quotes the table name. Defaults to column name quoting.
@@ -93,7 +76,7 @@ module ActiveRecord
# Override to return the quoted table name for assignment. Defaults to
# table quoting.
#
- # This works for mysql and mysql2 where table.column can be used to
+ # This works for mysql2 where table.column can be used to
# resolve ambiguity.
#
# We override this in the sqlite3 and postgresql adapters to use only
@@ -102,25 +85,29 @@ module ActiveRecord
quote_table_name("#{table}.#{attr}")
end
- def quote_default_expression(value, column) #:nodoc:
- value = lookup_cast_type(column.sql_type).serialize(value)
- quote(value)
+ def quote_default_expression(value, column) # :nodoc:
+ if value.is_a?(Proc)
+ value.call
+ else
+ value = lookup_cast_type(column.sql_type).serialize(value)
+ quote(value)
+ end
end
def quoted_true
- "'t'"
+ "TRUE"
end
def unquoted_true
- 't'
+ true
end
def quoted_false
- "'f'"
+ "FALSE"
end
def unquoted_false
- 'f'
+ false
end
# Quote date/time values for use in SQL input. Includes microseconds
@@ -142,47 +129,72 @@ module ActiveRecord
end
end
- def prepare_binds_for_database(binds) # :nodoc:
- binds.map(&:value_for_database)
+ def quoted_time(value) # :nodoc:
+ value = value.change(year: 2000, month: 1, day: 1)
+ quoted_date(value).sub(/\A\d\d\d\d-\d\d-\d\d /, "")
end
- private
+ def quoted_binary(value) # :nodoc:
+ "'#{quote_string(value.to_s)}'"
+ end
- def types_which_need_no_typecasting
- [nil, Numeric, String]
- end
-
- def _quote(value)
- case value
- when String, ActiveSupport::Multibyte::Chars, Type::Binary::Data
- "'#{quote_string(value.to_s)}'"
- when true then quoted_true
- when false then quoted_false
- when nil then "NULL"
- # 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 Date, Time then "'#{quoted_date(value)}'"
- when Symbol then "'#{quote_string(value.to_s)}'"
- when Class then "'#{value}'"
- else raise TypeError, "can't quote #{value.class.name}"
+ 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
- def _type_cast(value)
- case value
- when Symbol, ActiveSupport::Multibyte::Chars, Type::Binary::Data
- value.to_s
- when true then unquoted_true
- when false then unquoted_false
- # BigDecimals need to be put in a non-normalized form and quoted.
- when BigDecimal then value.to_s('F')
- when Date, Time then quoted_date(value)
- when *types_which_need_no_typecasting
- value
- else raise TypeError
+ private
+ def lookup_cast_type(sql_type)
+ type_map.lookup(sql_type)
+ end
+
+ 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]
+ end
+
+ def _quote(value)
+ case value
+ when String, ActiveSupport::Multibyte::Chars
+ "'#{quote_string(value.to_s)}'"
+ when true then quoted_true
+ when false then quoted_false
+ when nil then "NULL"
+ # 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)}'"
+ when Class then "'#{value}'"
+ else raise TypeError, "can't quote #{value.class.name}"
+ end
+ end
+
+ def _type_cast(value)
+ case value
+ when Symbol, ActiveSupport::Multibyte::Chars, Type::Binary::Data
+ value.to_s
+ when true then unquoted_true
+ when false then unquoted_false
+ # BigDecimals need to be put in a non-normalized form and quoted.
+ when BigDecimal then value.to_s("F")
+ when Type::Time::Value then quoted_time(value)
+ when Date, Time then quoted_date(value)
+ when *types_which_need_no_typecasting
+ value
+ else raise TypeError
+ end
end
- end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb
index c0662f8473..52a796b926 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb
@@ -1,8 +1,10 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
- module Savepoints #:nodoc:
- def supports_savepoints?
- true
+ module Savepoints
+ def current_savepoint_name
+ current_transaction.savepoint_name
end
def create_savepoint(name = current_savepoint_name)
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 18d943f452..2cb0a2a4df 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
@@ -1,4 +1,4 @@
-require 'active_support/core_ext/string/strip'
+# frozen_string_literal: true
module ActiveRecord
module ConnectionAdapters
@@ -14,41 +14,59 @@ module ActiveRecord
send m, o
end
- delegate :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql, to: :@conn
- private :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql
+ delegate :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql,
+ :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys_in_create?, :foreign_key_options,
+ to: :@conn, private: true
private
def visit_AlterTable(o)
- sql = "ALTER TABLE #{quote_table_name(o.name)} "
- sql << o.adds.map { |col| accept col }.join(' ')
- sql << o.foreign_key_adds.map { |fk| visit_AddForeignKey fk }.join(' ')
- sql << o.foreign_key_drops.map { |fk| visit_DropForeignKey fk }.join(' ')
+ sql = +"ALTER TABLE #{quote_table_name(o.name)} "
+ sql << o.adds.map { |col| accept col }.join(" ")
+ sql << o.foreign_key_adds.map { |fk| visit_AddForeignKey fk }.join(" ")
+ sql << o.foreign_key_drops.map { |fk| visit_DropForeignKey fk }.join(" ")
end
def visit_ColumnDefinition(o)
- o.sql_type ||= type_to_sql(o.type, o.limit, o.precision, o.scale)
- column_sql = "#{quote_column_name(o.name)} #{o.sql_type}"
+ 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
end
def visit_AddColumnDefinition(o)
- "ADD #{accept(o.column)}"
+ +"ADD #{accept(o.column)}"
end
def visit_TableDefinition(o)
- create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE "
+ create_sql = +"CREATE#{table_modifier_in_create(o)} TABLE "
+ create_sql << "IF NOT EXISTS " if o.if_not_exists
create_sql << "#{quote_table_name(o.name)} "
- create_sql << "(#{o.columns.map { |c| accept c }.join(', ')}) " unless o.as
- create_sql << "#{o.options}"
- create_sql << " AS #{@conn.to_sql(o.as)}" if o.as
+
+ statements = o.columns.map { |c| accept c }
+ statements << accept(o.primary_keys) if o.primary_keys
+
+ if supports_indexes_in_create?
+ statements.concat(o.indexes.map { |column_name, options| index_in_create(o.name, column_name, options) })
+ end
+
+ 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
+
+ create_sql << "(#{statements.join(', ')})" if statements.present?
+ add_table_options!(create_sql, table_options(o))
+ create_sql << " AS #{to_sql(o.as)}" if o.as
create_sql
end
- def visit_AddForeignKey(o)
- sql = <<-SQL.strip_heredoc
- ADD CONSTRAINT #{quote_column_name(o.name)}
+ def visit_PrimaryKeyDefinition(o)
+ "PRIMARY KEY (#{o.name.map { |name| quote_column_name(name) }.join(', ')})"
+ end
+
+ def visit_ForeignKeyDefinition(o)
+ sql = +<<~SQL
+ CONSTRAINT #{quote_column_name(o.name)}
FOREIGN KEY (#{quote_column_name(o.column)})
REFERENCES #{quote_table_name(o.to_table)} (#{quote_column_name(o.primary_key)})
SQL
@@ -57,21 +75,30 @@ module ActiveRecord
sql
end
+ def visit_AddForeignKey(o)
+ "ADD #{accept(o)}"
+ end
+
def visit_DropForeignKey(name)
"DROP CONSTRAINT #{quote_column_name(name)}"
end
+ def table_options(o)
+ table_options = {}
+ table_options[:comment] = o.comment
+ table_options[:options] = o.options
+ table_options
+ end
+
+ def add_table_options!(create_sql, options)
+ if options_sql = options[:options]
+ create_sql << " #{options_sql}"
+ end
+ create_sql
+ 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
+ o.options.merge(column: o)
end
def add_column_options!(sql, options)
@@ -89,8 +116,19 @@ module ActiveRecord
sql
end
- def options_include_default?(options)
- options.include?(:default) && !(options[:null] == false && options[:default].nil?)
+ def to_sql(sql)
+ sql = sql.to_sql if sql.respond_to?(:to_sql)
+ sql
+ end
+
+ # Returns any SQL string to go between CREATE and TABLE. May be nil.
+ def table_modifier_in_create(o)
+ " TEMPORARY" if o.temporary
+ end
+
+ def foreign_key_in_create(from_table, to_table, options)
+ options = foreign_key_options(from_table, to_table, options)
+ accept ForeignKeyDefinition.new(from_table, to_table, options)
end
def action_sql(action, dependency)
@@ -99,7 +137,7 @@ module ActiveRecord
when :cascade then "ON #{action} CASCADE"
when :restrict then "ON #{action} RESTRICT"
else
- raise ArgumentError, <<-MSG.strip_heredoc
+ raise ArgumentError, <<~MSG
'#{dependency}' is not supported for :on_update or :on_delete.
Supported values are: :nullify, :cascade, :restrict
MSG
@@ -107,5 +145,6 @@ module ActiveRecord
end
end
end
+ SchemaCreation = AbstractAdapter::SchemaCreation # :nodoc:
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
index d17e272ed1..db489143af 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -1,29 +1,79 @@
+# frozen_string_literal: true
+
+require "active_support/deprecation"
+
module ActiveRecord
module ConnectionAdapters #:nodoc:
# Abstract representation of an index definition on a table. Instances of
# this type are typically created and returned by methods in database
- # adapters. e.g. ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#indexes
- class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using) #:nodoc:
+ # adapters. e.g. ActiveRecord::ConnectionAdapters::MySQL::SchemaStatements#indexes
+ class IndexDefinition # :nodoc:
+ attr_reader :table, :name, :unique, :columns, :lengths, :orders, :opclasses, :where, :type, :using, :comment
+
+ def initialize(
+ table, name,
+ unique = false,
+ columns = [],
+ lengths: {},
+ orders: {},
+ opclasses: {},
+ where: nil,
+ type: nil,
+ using: nil,
+ comment: nil
+ )
+ @table = table
+ @name = name
+ @unique = unique
+ @columns = columns
+ @lengths = concise_options(lengths)
+ @orders = concise_options(orders)
+ @opclasses = concise_options(opclasses)
+ @where = where
+ @type = type
+ @using = using
+ @comment = comment
+ end
+
+ private
+ def concise_options(options)
+ if columns.size == options.size && options.values.uniq.size == 1
+ options.values.first
+ else
+ options
+ end
+ end
end
# Abstract representation of a column definition. Instances of this type
# are typically created by methods in TableDefinition, and added to the
# +columns+ attribute of said TableDefinition object, in order to be used
# for generating a number of table creation or table changing SQL statements.
- class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :collation, :sql_type) #:nodoc:
-
+ 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 ForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) #:nodoc:
+ AddColumnDefinition = Struct.new(:column) # :nodoc:
+
+ ChangeColumnDefinition = Struct.new(:column, :name) #:nodoc:
+
+ PrimaryKeyDefinition = Struct.new(:name) # :nodoc:
+
+ ForeignKeyDefinition = Struct.new(:from_table, :to_table, :options) do #:nodoc:
def name
options[:name]
end
@@ -48,27 +98,37 @@ module ActiveRecord
options[:primary_key] != default_primary_key
end
- def defined_for?(options_or_to_table = {})
- if options_or_to_table.is_a?(Hash)
- options_or_to_table.all? {|key, value| options[key].to_s == value.to_s }
+ def validate?
+ options.fetch(:validate, true)
+ end
+ alias validated? validate?
+
+ def export_name_on_schema_dump?
+ name !~ ActiveRecord::SchemaDumper.fk_ignore_pattern
+ end
+
+ def defined_for?(to_table_ord = nil, to_table: nil, **options)
+ if to_table_ord
+ self.to_table == to_table_ord.to_s
else
- to_table == options_or_to_table.to_s
+ (to_table.nil? || to_table.to_s == self.to_table) &&
+ options.all? { |k, v| self.options[k].to_s == v.to_s }
end
end
private
- def default_primary_key
- "id"
- end
+ def default_primary_key
+ "id"
+ end
end
class ReferenceDefinition # :nodoc:
def initialize(
name,
polymorphic: false,
- index: false,
+ index: true,
foreign_key: false,
- type: :integer,
+ type: :bigint,
**options
)
@name = name
@@ -97,47 +157,46 @@ module ActiveRecord
end
end
- protected
-
- attr_reader :name, :polymorphic, :index, :foreign_key, :type, :options
-
private
+ attr_reader :name, :polymorphic, :index, :foreign_key, :type, :options
- def as_options(value, default = {})
- if value.is_a?(Hash)
- value
- else
- default
+ def as_options(value)
+ value.is_a?(Hash) ? value : {}
end
- end
- def polymorphic_options
- as_options(polymorphic, options)
- end
+ def polymorphic_options
+ as_options(polymorphic).merge(options.slice(:null, :first, :after))
+ end
- def index_options
- as_options(index)
- end
+ def index_options
+ as_options(index)
+ end
- def foreign_key_options
- as_options(foreign_key)
- end
+ def foreign_key_options
+ as_options(foreign_key).merge(column: column_name)
+ end
- def columns
- result = [["#{name}_id", type, options]]
- if polymorphic
- result.unshift(["#{name}_type", :string, polymorphic_options])
+ def columns
+ result = [[column_name, type, options]]
+ if polymorphic
+ result.unshift(["#{name}_type", :string, polymorphic_options])
+ end
+ result
end
- result
- end
- def column_names
- columns.map(&:first)
- end
+ def column_name
+ "#{name}_id"
+ end
- def foreign_table_name
- Base.pluralize_table_names ? name.to_s.pluralize : name
- end
+ def column_names
+ columns.map(&:first)
+ end
+
+ def foreign_table_name
+ foreign_key_options.fetch(:to_table) do
+ Base.pluralize_table_names ? name.to_s.pluralize : name
+ end
+ end
end
module ColumnMethods
@@ -162,10 +221,12 @@ module ActiveRecord
:decimal,
:float,
:integer,
+ :json,
:string,
:text,
:time,
:timestamp,
+ :virtual,
].each do |column_type|
module_eval <<-CODE, __FILE__, __LINE__ + 1
def #{column_type}(*args, **options)
@@ -173,15 +234,16 @@ module ActiveRecord
end
CODE
end
+ alias_method :numeric, :decimal
end
# Represents the schema of an SQL table in an abstract way. This class
# provides methods for manipulating the schema representation.
#
- # Inside migration files, the +t+ object in +create_table+
+ # Inside migration files, the +t+ object in {create_table}[rdoc-ref:SchemaStatements#create_table]
# is actually of this type:
#
- # class SomeMigration < ActiveRecord::Migration
+ # class SomeMigration < ActiveRecord::Migration[5.0]
# def up
# create_table :foo do |t|
# puts t.class # => "ActiveRecord::ConnectionAdapters::TableDefinition"
@@ -193,27 +255,40 @@ module ActiveRecord
# end
# end
#
- # The table definitions
- # The Columns are stored as a ColumnDefinition in the +columns+ attribute.
class TableDefinition
include ColumnMethods
- # An array of ColumnDefinition objects, representing the column changes
- # that have been defined.
- attr_accessor :indexes
- attr_reader :name, :temporary, :options, :as, :foreign_keys
+ attr_reader :name, :temporary, :if_not_exists, :options, :as, :comment, :indexes, :foreign_keys
+ attr_writer :indexes
+ deprecate :indexes=
- def initialize(types, name, temporary, options, as = nil)
+ def initialize(
+ name,
+ temporary: false,
+ if_not_exists: false,
+ options: nil,
+ as: nil,
+ comment: nil,
+ **
+ )
@columns_hash = {}
- @indexes = {}
- @foreign_keys = {}
- @native = types
+ @indexes = []
+ @foreign_keys = []
+ @primary_keys = nil
@temporary = temporary
+ @if_not_exists = if_not_exists
@options = options
@as = as
@name = name
+ @comment = comment
end
+ def primary_keys(name = nil) # :nodoc:
+ @primary_keys = PrimaryKeyDefinition.new(name) if name
+ @primary_keys
+ end
+
+ # Returns an array of ColumnDefinition objects for the columns of the table.
def columns; @columns_hash.values; end
# Returns a ColumnDefinition for the column with name +name+.
@@ -222,90 +297,23 @@ module ActiveRecord
end
# Instantiates a new column for the table.
- # The +type+ parameter is normally one of the migrations native types,
- # which is one of the following:
- # <tt>:primary_key</tt>, <tt>:string</tt>, <tt>:text</tt>,
- # <tt>:integer</tt>, <tt>:bigint</tt>, <tt>:float</tt>, <tt>:decimal</tt>,
- # <tt>:datetime</tt>, <tt>:time</tt>, <tt>:date</tt>,
- # <tt>:binary</tt>, <tt>:boolean</tt>.
- #
- # You may use a type not in this list as long as it is supported by your
- # database (for example, "polygon" in MySQL), but this will not be database
- # agnostic and should usually be avoided.
- #
- # Available options are (none of these exists by default):
- # * <tt>:limit</tt> -
- # Requests a maximum column length. This is number of characters for <tt>:string</tt> and
- # <tt>:text</tt> columns and number of bytes for <tt>:binary</tt> and <tt>:integer</tt> columns.
- # * <tt>:default</tt> -
- # The column's default value. Use nil for NULL.
- # * <tt>:null</tt> -
- # Allows or disallows +NULL+ values in the column. This option could
- # have been named <tt>:null_allowed</tt>.
- # * <tt>:precision</tt> -
- # Specifies the precision for a <tt>:decimal</tt> column.
- # * <tt>:scale</tt> -
- # Specifies the scale for a <tt>:decimal</tt> column.
+ # See {connection.add_column}[rdoc-ref:ConnectionAdapters::SchemaStatements#add_column]
+ # for available options.
+ #
+ # Additional options are:
# * <tt>:index</tt> -
# Create an index for the column. Can be either <tt>true</tt> or an options hash.
#
- # Note: The precision is the total number of significant digits
- # and the scale is the number of digits that can be stored following
- # the decimal point. For example, the number 123.45 has a precision of 5
- # and a scale of 2. A decimal with a precision of 5 and a scale of 2 can
- # range from -999.99 to 999.99.
- #
- # Please be aware of different RDBMS implementations behavior with
- # <tt>:decimal</tt> columns:
- # * The SQL standard says the default scale should be 0, <tt>:scale</tt> <=
- # <tt>:precision</tt>, and makes no comments about the requirements of
- # <tt>:precision</tt>.
- # * MySQL: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..30].
- # Default is (10,0).
- # * PostgreSQL: <tt>:precision</tt> [1..infinity],
- # <tt>:scale</tt> [0..infinity]. No default.
- # * SQLite2: Any <tt>:precision</tt> and <tt>:scale</tt> may be used.
- # Internal storage as strings. No default.
- # * SQLite3: No restrictions on <tt>:precision</tt> and <tt>:scale</tt>,
- # but the maximum supported <tt>:precision</tt> is 16. No default.
- # * Oracle: <tt>:precision</tt> [1..38], <tt>:scale</tt> [-84..127].
- # Default is (38,0).
- # * DB2: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..62].
- # Default unknown.
- # * SqlServer?: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38].
- # Default (38,0).
- #
# This method returns <tt>self</tt>.
#
# == Examples
- # # Assuming +td+ is an instance of TableDefinition
- # td.column(:granted, :boolean)
- # # granted BOOLEAN
- #
- # td.column(:picture, :binary, limit: 2.megabytes)
- # # => picture BLOB(2097152)
#
- # td.column(:sales_stage, :string, limit: 20, default: 'new', null: false)
- # # => sales_stage VARCHAR(20) DEFAULT 'new' NOT NULL
- #
- # td.column(:bill_gates_money, :decimal, precision: 15, scale: 2)
- # # => bill_gates_money DECIMAL(15,2)
- #
- # td.column(:sensor_reading, :decimal, precision: 30, scale: 20)
- # # => sensor_reading DECIMAL(30,20)
- #
- # # While <tt>:scale</tt> defaults to zero on most databases, it
- # # probably wouldn't hurt to include it.
- # td.column(:huge_integer, :decimal, precision: 30)
- # # => huge_integer DECIMAL(30)
- #
- # # Defines a column with a database-specific type.
- # td.column(:foo, 'polygon')
- # # => foo polygon
+ # # Assuming +td+ is an instance of TableDefinition
+ # td.column(:granted, :boolean, index: true)
#
# == Short-hand examples
#
- # Instead of calling +column+ directly, you can also work with the short-hand definitions for the default types.
+ # Instead of calling #column directly, you can also work with the short-hand definitions for the default types.
# They use the type as the method name instead of as a parameter and allow for multiple columns to be defined
# in a single statement.
#
@@ -337,7 +345,8 @@ module ActiveRecord
# TableDefinition#references will add an appropriately-named _id column, plus a corresponding _type
# column if the <tt>:polymorphic</tt> option is supplied. If <tt>:polymorphic</tt> is a hash of
# options, these will be used when creating the <tt>_type</tt> column. The <tt>:index</tt> option
- # will also create an index, similar to calling <tt>add_index</tt>. So what can be written like this:
+ # will also create an index, similar to calling {add_index}[rdoc-ref:ConnectionAdapters::SchemaStatements#add_index].
+ # So what can be written like this:
#
# create_table :taggings do |t|
# t.integer :tag_id, :tagger_id, :taggable_id
@@ -351,16 +360,20 @@ module ActiveRecord
#
# create_table :taggings do |t|
# t.references :tag, index: { name: 'index_taggings_on_tag_id' }
- # t.references :tagger, polymorphic: true, index: true
- # t.references :taggable, polymorphic: { default: 'Photo' }
+ # t.references :tagger, polymorphic: true
+ # t.references :taggable, polymorphic: { default: 'Photo' }, index: false
# end
def column(name, type, options = {})
name = name.to_s
- type = type.to_sym
+ type = type.to_sym if type
options = options.dup
- if @columns_hash[name] && @columns_hash[name].primary_key?
- raise ArgumentError, "you can't redefine the primary key column '#{name}'. To define a custom primary key, pass { id: false } to create_table."
+ if @columns_hash[name]
+ if @columns_hash[name].primary_key?
+ raise ArgumentError, "you can't redefine the primary key column '#{name}'. To define a custom primary key, pass { id: false } to create_table."
+ else
+ raise ArgumentError, "you can't define an already defined column '#{name}'."
+ end
end
index_options = options.delete(:index)
@@ -369,6 +382,8 @@ module ActiveRecord
self
end
+ # remove the column +name+ from the table.
+ # remove_column(:account_id)
def remove_column(name)
@columns_hash.delete name.to_s
end
@@ -378,20 +393,21 @@ module ActiveRecord
#
# index(:account_id, name: 'index_projects_on_account_id')
def index(column_name, options = {})
- indexes[column_name] = options
+ indexes << [column_name, options]
end
def foreign_key(table_name, options = {}) # :nodoc:
- foreign_keys[table_name] = options
+ table_name_prefix = ActiveRecord::Base.table_name_prefix
+ table_name_suffix = ActiveRecord::Base.table_name_suffix
+ table_name = "#{table_name_prefix}#{table_name}#{table_name_suffix}"
+ foreign_keys.push([table_name, options])
end
# Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and
- # <tt>:updated_at</tt> to the table. See SchemaStatements#add_timestamps
+ # <tt>:updated_at</tt> to the table. See {connection.add_timestamps}[rdoc-ref:SchemaStatements#add_timestamps]
#
# t.timestamps null: false
- def timestamps(*args)
- options = args.extract_options!
-
+ def timestamps(**options)
options[:null] = false if options[:null].nil?
column(:created_at, :datetime, options)
@@ -403,46 +419,40 @@ module ActiveRecord
# t.references(:user)
# t.belongs_to(:supplier, foreign_key: true)
#
- # See SchemaStatements#add_reference for details of the options you can use.
+ # 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:
- type = aliased_types(type.to_s, type)
- column = create_column_definition name, type
- limit = options.fetch(:limit) do
- native[type][:limit] if native[type].is_a?(Hash)
+ def new_column_definition(name, type, **options) # :nodoc:
+ if integer_like_primary_key?(type, options)
+ type = integer_like_primary_key_type(type, options)
end
-
- column.limit = 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
+ type = aliased_types(type.to_s, type)
+ 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
- end
+ def create_column_definition(name, type, options)
+ ColumnDefinition.new(name, type, options)
+ end
- def native
- @native
- end
+ def aliased_types(name, fallback)
+ "timestamp" == name ? :datetime : fallback
+ end
- def aliased_types(name, fallback)
- 'timestamp' == name ? :datetime : fallback
- end
+ def integer_like_primary_key?(type, options)
+ options[:primary_key] && [:integer, :bigint].include?(type) && !options.key?(:default)
+ end
+
+ def integer_like_primary_key_type(type, options)
+ type
+ end
end
class AlterTable # :nodoc:
@@ -475,7 +485,7 @@ module ActiveRecord
end
# Represents an SQL table in an abstract way for updating a table.
- # Also see TableDefinition and SchemaStatements#create_table
+ # Also see TableDefinition and {connection.create_table}[rdoc-ref:SchemaStatements#create_table]
#
# Available transformations are:
#
@@ -496,12 +506,16 @@ module ActiveRecord
# t.bigint
# t.float
# t.decimal
+ # t.numeric
# t.datetime
# t.timestamp
# t.time
# t.date
# t.binary
# t.boolean
+ # t.foreign_key
+ # t.json
+ # t.virtual
# t.remove
# t.remove_references
# t.remove_belongs_to
@@ -525,14 +539,16 @@ module ActiveRecord
#
# See TableDefinition#column for details of the options you can use.
def column(column_name, type, options = {})
+ index_options = options.delete(:index)
@base.add_column(name, column_name, type, options)
+ index(column_name, index_options.is_a?(Hash) ? index_options : {}) if index_options
end
# Checks to see if a column exists.
#
- # t.string(:name) unless t.column_exists?(:name, :string)
+ # t.string(:name) unless t.column_exists?(:name, :string)
#
- # See SchemaStatements#column_exists?
+ # See {connection.column_exists?}[rdoc-ref:SchemaStatements#column_exists?]
def column_exists?(column_name, type = nil, options = {})
@base.column_exists?(name, column_name, type, options)
end
@@ -544,18 +560,18 @@ module ActiveRecord
# t.index([:branch_id, :party_id], unique: true)
# t.index([:branch_id, :party_id], unique: true, name: 'by_branch_party')
#
- # See SchemaStatements#add_index for details of the options you can use.
+ # See {connection.add_index}[rdoc-ref:SchemaStatements#add_index] for details of the options you can use.
def index(column_name, options = {})
@base.add_index(name, column_name, options)
end
# Checks to see if an index exists.
#
- # unless t.index_exists?(:branch_id)
- # t.index(:branch_id)
- # end
+ # unless t.index_exists?(:branch_id)
+ # t.index(:branch_id)
+ # end
#
- # See SchemaStatements#index_exists?
+ # See {connection.index_exists?}[rdoc-ref:SchemaStatements#index_exists?]
def index_exists?(column_name, options = {})
@base.index_exists?(name, column_name, options)
end
@@ -564,7 +580,7 @@ module ActiveRecord
#
# t.rename_index(:user_id, :account_id)
#
- # See SchemaStatements#rename_index
+ # See {connection.rename_index}[rdoc-ref:SchemaStatements#rename_index]
def rename_index(index_name, new_index_name)
@base.rename_index(name, index_name, new_index_name)
end
@@ -573,7 +589,7 @@ module ActiveRecord
#
# t.timestamps(null: false)
#
- # See SchemaStatements#add_timestamps
+ # See {connection.add_timestamps}[rdoc-ref:SchemaStatements#add_timestamps]
def timestamps(options = {})
@base.add_timestamps(name, options)
end
@@ -594,7 +610,7 @@ module ActiveRecord
# t.change_default(:authorized, 1)
# t.change_default(:status, from: nil, to: "draft")
#
- # See SchemaStatements#change_column_default
+ # See {connection.change_column_default}[rdoc-ref:SchemaStatements#change_column_default]
def change_default(column_name, default_or_changes)
@base.change_column_default(name, column_name, default_or_changes)
end
@@ -604,7 +620,7 @@ module ActiveRecord
# t.remove(:qualification)
# t.remove(:qualification, :experience)
#
- # See SchemaStatements#remove_columns
+ # See {connection.remove_columns}[rdoc-ref:SchemaStatements#remove_columns]
def remove(*column_names)
@base.remove_columns(name, *column_names)
end
@@ -615,7 +631,7 @@ module ActiveRecord
# t.remove_index(column: [:branch_id, :party_id])
# t.remove_index(name: :by_branch_party)
#
- # See SchemaStatements#remove_index
+ # See {connection.remove_index}[rdoc-ref:SchemaStatements#remove_index]
def remove_index(options = {})
@base.remove_index(name, options)
end
@@ -624,7 +640,7 @@ module ActiveRecord
#
# t.remove_timestamps
#
- # See SchemaStatements#remove_timestamps
+ # See {connection.remove_timestamps}[rdoc-ref:SchemaStatements#remove_timestamps]
def remove_timestamps(options = {})
@base.remove_timestamps(name, options)
end
@@ -633,7 +649,7 @@ module ActiveRecord
#
# t.rename(:description, :name)
#
- # See SchemaStatements#rename_column
+ # See {connection.rename_column}[rdoc-ref:SchemaStatements#rename_column]
def rename(column_name, new_column_name)
@base.rename_column(name, column_name, new_column_name)
end
@@ -643,9 +659,8 @@ module ActiveRecord
# t.references(:user)
# t.belongs_to(:supplier, foreign_key: true)
#
- # See SchemaStatements#add_reference for details of the options you can use.
- def references(*args)
- options = args.extract_options!
+ # See {connection.add_reference}[rdoc-ref:SchemaStatements#add_reference] for details of the options you can use.
+ def references(*args, **options)
args.each do |ref_name|
@base.add_reference(name, ref_name, options)
end
@@ -657,9 +672,8 @@ module ActiveRecord
# t.remove_references(:user)
# t.remove_belongs_to(:supplier, polymorphic: true)
#
- # See SchemaStatements#remove_reference
- def remove_references(*args)
- options = args.extract_options!
+ # See {connection.remove_reference}[rdoc-ref:SchemaStatements#remove_reference]
+ def remove_references(*args, **options)
args.each do |ref_name|
@base.remove_reference(name, ref_name, options)
end
@@ -668,26 +682,21 @@ module ActiveRecord
# Adds a foreign key.
#
- # t.foreign_key(:authors)
+ # t.foreign_key(:authors)
#
- # See SchemaStatements#add_foreign_key
- def foreign_key(*args) # :nodoc:
+ # See {connection.add_foreign_key}[rdoc-ref:SchemaStatements#add_foreign_key]
+ def foreign_key(*args)
@base.add_foreign_key(name, *args)
end
# Checks to see if a foreign key exists.
#
- # t.foreign_key(:authors) unless t.foreign_key_exists?(:authors)
+ # t.foreign_key(:authors) unless t.foreign_key_exists?(:authors)
#
- # See SchemaStatements#foreign_key_exists?
- def foreign_key_exists?(*args) # :nodoc:
+ # See {connection.foreign_key_exists?}[rdoc-ref:SchemaStatements#foreign_key_exists?]
+ def foreign_key_exists?(*args)
@base.foreign_key_exists?(name, *args)
end
-
- private
- def native
- @base.native_database_types
- end
end
end
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 b944a8631c..622e00fffb 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
@@ -1,89 +1,93 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters # :nodoc:
- # The goal of this module is to move Adapter specific column
- # definitions to the Adapter instead of having it in the schema
- # dumper itself. This code represents the normal case.
- # We can then redefine how certain data types may be handled in the schema dumper on the
- # Adapter level by over-writing this code inside the database specific adapters
- module ColumnDumper
- def column_spec(column)
- spec = prepare_column_options(column)
- (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k}: ")}
- spec
- end
-
- def column_spec_for_primary_key(column)
- return if column.type == :integer
- spec = { id: column.type.inspect }
- spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type].include?(key) })
+ class SchemaDumper < SchemaDumper # :nodoc:
+ def self.create(connection, options)
+ new(connection, options)
end
- # This can be overridden on a Adapter level basis to support other
- # extended datatypes (Example: Adding an array option in the
- # PostgreSQLAdapter)
- def prepare_column_options(column)
- spec = {}
- spec[:name] = column.name.inspect
- spec[:type] = schema_type(column)
- spec[:null] = 'false' unless column.null
-
- if limit = schema_limit(column)
- spec[:limit] = limit
+ private
+ def column_spec(column)
+ [schema_type_with_virtual(column), prepare_column_options(column)]
end
- if precision = schema_precision(column)
- spec[:precision] = precision
+ 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
- if scale = schema_scale(column)
- spec[:scale] = scale
+ def prepare_column_options(column)
+ spec = {}
+ spec[:limit] = schema_limit(column)
+ spec[:precision] = schema_precision(column)
+ spec[:scale] = schema_scale(column)
+ spec[:default] = schema_default(column)
+ spec[:null] = "false" unless column.null
+ spec[:collation] = schema_collation(column)
+ spec[:comment] = column.comment.inspect if column.comment.present?
+ spec.compact!
+ spec
end
- default = schema_default(column) if column.has_default?
- spec[:default] = default unless default.nil?
-
- if collation = schema_collation(column)
- spec[:collation] = collation
+ def default_primary_key?(column)
+ schema_type(column) == :bigint
end
- spec
- end
+ def explicit_primary_key_default?(column)
+ false
+ end
- # Lists the valid migration options
- def migration_keys
- [:name, :limit, :precision, :scale, :default, :null, :collation]
- end
+ def schema_type_with_virtual(column)
+ if @connection.supports_virtual_columns? && column.virtual?
+ :virtual
+ else
+ schema_type(column)
+ end
+ end
- private
+ def schema_type(column)
+ if column.bigint?
+ :bigint
+ else
+ column.type
+ end
+ end
- def schema_type(column)
- column.type.to_s
- end
+ def schema_limit(column)
+ limit = column.limit unless column.bigint?
+ limit.inspect if limit && limit != @connection.native_database_types[column.type][:limit]
+ end
- def schema_limit(column)
- limit = column.limit || native_database_types[column.type][:limit]
- limit.inspect if limit
- end
+ def schema_precision(column)
+ column.precision.inspect if column.precision
+ end
- def schema_precision(column)
- column.precision.inspect if column.precision
- end
+ def schema_scale(column)
+ column.scale.inspect if column.scale
+ end
- def schema_scale(column)
- column.scale.inspect if column.scale
- end
+ def schema_default(column)
+ return unless column.has_default?
+ type = @connection.lookup_cast_type_from_column(column)
+ default = type.deserialize(column.default)
+ if default.nil?
+ schema_expression(column)
+ else
+ type.type_cast_for_schema(default)
+ end
+ end
- def schema_default(column)
- type = lookup_cast_type_from_column(column)
- default = type.deserialize(column.default)
- unless default.nil?
- type.type_cast_for_schema(default)
+ def schema_expression(column)
+ "-> { #{column.default_function.inspect} }" if column.default_function
end
- end
- def schema_collation(column)
- column.collation.inspect if column.collation
- end
+ def schema_collation(column)
+ column.collation.inspect if column.collation
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index e3115abe66..38cfc3a241 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -1,6 +1,8 @@
-require 'active_record/migration/join_table'
-require 'active_support/core_ext/string/access'
-require 'digest'
+# frozen_string_literal: true
+
+require "active_record/migration/join_table"
+require "active_support/core_ext/string/access"
+require "digest/sha2"
module ActiveRecord
module ConnectionAdapters # :nodoc:
@@ -18,9 +20,37 @@ module ActiveRecord
nil
end
+ # Returns the table comment that's stored in database metadata.
+ def table_comment(table_name)
+ nil
+ end
+
# Truncates a table alias according to the limits of the current adapter.
def table_alias_for(table_name)
- table_name[0...table_alias_length].tr('.', '_')
+ table_name[0...table_alias_length].tr(".", "_")
+ end
+
+ # Returns the relation names useable to back Active Record models.
+ # For most adapters this means all #tables and #views.
+ def data_sources
+ query_values(data_source_sql, "SCHEMA")
+ rescue NotImplementedError
+ tables | views
+ end
+
+ # Checks to see if the data source +name+ exists on the database.
+ #
+ # data_source_exists?(:ebooks)
+ #
+ def data_source_exists?(name)
+ query_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
+ query_values(data_source_sql(type: "BASE TABLE"), "SCHEMA")
end
# Checks to see if the table +table_name+ exists on the database.
@@ -28,11 +58,30 @@ module ActiveRecord
# table_exists?(:developers)
#
def table_exists?(table_name)
+ query_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
+ query_values(data_source_sql(type: "VIEW"), "SCHEMA")
+ end
+
+ # Checks to see if the view +view_name+ exists on the database.
+ #
+ # view_exists?(:ebooks)
+ #
+ def view_exists?(view_name)
+ query_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)
+ raise NotImplementedError, "#indexes is not implemented"
+ end
# Checks to see if an index exists on a table for a given index definition.
#
@@ -50,18 +99,21 @@ module ActiveRecord
#
def index_exists?(table_name, column_name, options = {})
column_names = Array(column_name).map(&:to_s)
- index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, column: column_names)
checks = []
- checks << lambda { |i| i.name == index_name }
checks << lambda { |i| i.columns == column_names }
checks << lambda { |i| i.unique } if options[:unique]
+ checks << lambda { |i| i.name == options[:name].to_s } if options[:name]
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.
- def columns(table_name) end
+ # Returns an array of +Column+ objects for the table specified by +table_name+.
+ def columns(table_name)
+ 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.
#
@@ -79,19 +131,27 @@ module ActiveRecord
#
def column_exists?(table_name, column_name, type = nil, options = {})
column_name = column_name.to_s
- columns(table_name).any?{ |c| c.name == column_name &&
- (!type || c.type == type) &&
- (!options.key?(:limit) || c.limit == options[:limit]) &&
- (!options.key?(:precision) || c.precision == options[:precision]) &&
- (!options.key?(:scale) || c.scale == options[:scale]) &&
- (!options.key?(:default) || c.default == options[:default]) &&
- (!options.key?(:null) || c.null == options[:null]) }
+ checks = []
+ checks << lambda { |c| c.name == column_name }
+ checks << lambda { |c| c.type == type } if type
+ column_options_keys.each do |attr|
+ checks << lambda { |c| c.send(attr) == options[attr] } if options.key?(attr)
+ end
+
+ columns(table_name).any? { |c| checks.all? { |check| check[c] } }
+ end
+
+ # Returns just a table's primary key
+ def primary_key(table_name)
+ pk = primary_keys(table_name)
+ pk = pk.first unless pk.size > 1
+ pk
end
# Creates a new table with the name +table_name+. +table_name+ may either
# be a String or a Symbol.
#
- # There are two ways to work with +create_table+. You can use the block
+ # There are two ways to work with #create_table. You can use the block
# form or the regular form, like this:
#
# === Block form
@@ -123,15 +183,18 @@ module ActiveRecord
# The +options+ hash can include the following keys:
# [<tt>:id</tt>]
# Whether to automatically add a primary key column. Defaults to true.
- # Join tables for +has_and_belongs_to_many+ should set it to false.
+ # Join tables for {ActiveRecord::Base.has_and_belongs_to_many}[rdoc-ref:Associations::ClassMethods#has_and_belongs_to_many] should set it to false.
#
# A Symbol can be used to specify the type of the generated primary key column.
# [<tt>:primary_key</tt>]
# The name of the primary key, if one is to be added automatically.
- # Defaults to +id+. If <tt>:id</tt> is false this option is ignored.
+ # Defaults to +id+. If <tt>:id</tt> is false, then this option is ignored.
+ #
+ # If an array is passed, a composite primary key will be created.
#
# Note that Active Record models will automatically detect their
- # primary key. This can be avoided by using +self.primary_key=+ on the model
+ # primary key. This can be avoided by using
+ # {self.primary_key=}[rdoc-ref:AttributeMethods::PrimaryKey::ClassMethods#primary_key=] on the model
# to define the key explicitly.
#
# [<tt>:options</tt>]
@@ -142,19 +205,22 @@ module ActiveRecord
# Set to true to drop the table before creating it.
# Set to +:cascade+ to drop dependent objects as well.
# Defaults to false.
+ # [<tt>:if_not_exists</tt>]
+ # Set to true to avoid raising an error when the table already exists.
+ # Defaults to false.
# [<tt>:as</tt>]
# SQL to use to generate the table. When this option is used, the block is
# ignored, as are the <tt>:id</tt> and <tt>:primary_key</tt> options.
#
# ====== Add a backend specific option to the generated SQL (MySQL)
#
- # create_table(:suppliers, options: 'ENGINE=InnoDB DEFAULT CHARSET=utf8')
+ # create_table(:suppliers, options: 'ENGINE=InnoDB DEFAULT CHARSET=utf8mb4')
#
# generates:
#
# CREATE TABLE suppliers (
- # id int(11) DEFAULT NULL auto_increment PRIMARY KEY
- # ) ENGINE=InnoDB DEFAULT CHARSET=utf8
+ # id bigint auto_increment PRIMARY KEY
+ # ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
#
# ====== Rename the primary key column
#
@@ -165,7 +231,7 @@ module ActiveRecord
# generates:
#
# CREATE TABLE objects (
- # guid int(11) DEFAULT NULL auto_increment PRIMARY KEY,
+ # guid bigint auto_increment PRIMARY KEY,
# name varchar(80)
# )
#
@@ -182,18 +248,35 @@ module ActiveRecord
# label varchar
# )
#
+ # ====== Create a composite primary key
+ #
+ # create_table(:orders, primary_key: [:product_id, :client_id]) do |t|
+ # t.belongs_to :product
+ # t.belongs_to :client
+ # end
+ #
+ # generates:
+ #
+ # CREATE TABLE order (
+ # product_id bigint NOT NULL,
+ # client_id bigint NOT NULL
+ # );
+ #
+ # ALTER TABLE ONLY "orders"
+ # ADD CONSTRAINT orders_pkey PRIMARY KEY (product_id, client_id);
+ #
# ====== Do not add a primary key column
#
# create_table(:categories_suppliers, id: false) do |t|
- # t.column :category_id, :integer
- # t.column :supplier_id, :integer
+ # t.column :category_id, :bigint
+ # t.column :supplier_id, :bigint
# end
#
# generates:
#
# CREATE TABLE categories_suppliers (
- # category_id int,
- # supplier_id int
+ # category_id bigint,
+ # supplier_id bigint
# )
#
# ====== Create a temporary table based on a query
@@ -207,33 +290,43 @@ module ActiveRecord
# SELECT * FROM orders INNER JOIN line_items ON order_id=orders.id
#
# See also TableDefinition#column for details on how to create columns.
- def create_table(table_name, options = {})
- td = create_table_definition table_name, options[:temporary], options[:options], options[:as]
+ def create_table(table_name, **options)
+ td = create_table_definition(table_name, options)
if options[:id] != false && !options[:as]
pk = options.fetch(:primary_key) do
Base.get_primary_key table_name.to_s.singularize
end
- td.primary_key pk, options.fetch(:id, :primary_key), options
+ if pk.is_a?(Array)
+ td.primary_keys pk
+ else
+ td.primary_key pk, options.fetch(:id, :primary_key), options
+ end
end
yield td if block_given?
- if options[:force] && table_exists?(table_name)
- drop_table(table_name, options)
+ if options[:force]
+ drop_table(table_name, options.merge(if_exists: true))
end
result = execute schema_creation.accept td
unless supports_indexes_in_create?
- td.indexes.each_pair do |column_name, index_options|
+ td.indexes.each do |column_name, index_options|
add_index(table_name, column_name, index_options)
end
end
- td.foreign_keys.each_pair do |other_table_name, foreign_key_options|
- add_foreign_key(table_name, other_table_name, foreign_key_options)
+ if supports_comments? && !supports_comments_in_create?
+ if table_comment = options[:comment].presence
+ change_table_comment(table_name, table_comment)
+ end
+
+ td.columns.each do |column|
+ change_column_comment(table_name, column.name, column.comment) if column.comment.present?
+ end
end
result
@@ -245,9 +338,9 @@ module ActiveRecord
# # Creates a table called 'assemblies_parts' with no id.
# create_join_table(:assemblies, :parts)
#
- # You can pass a +options+ hash can include the following keys:
+ # You can pass an +options+ hash which can include the following keys:
# [<tt>:table_name</tt>]
- # Sets the table name overriding the default
+ # Sets the table name, overriding the default.
# [<tt>:column_options</tt>]
# Any extra options you want appended to the columns definition.
# [<tt>:options</tt>]
@@ -258,7 +351,7 @@ module ActiveRecord
# Set to true to drop the table before creating it.
# Defaults to false.
#
- # Note that +create_join_table+ does not create any indices by default; you can use
+ # Note that #create_join_table does not create any indices by default; you can use
# its block form to do so yourself:
#
# create_join_table :products, :categories do |t|
@@ -273,31 +366,30 @@ module ActiveRecord
# generates:
#
# CREATE TABLE assemblies_parts (
- # assembly_id int NOT NULL,
- # part_id int NOT NULL,
+ # assembly_id bigint NOT NULL,
+ # part_id bigint NOT NULL,
# ) ENGINE=InnoDB DEFAULT CHARSET=utf8
#
- def create_join_table(table_1, table_2, 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)
+ 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.integer t1_column, column_options
- td.integer t2_column, column_options
+ td.references t1_ref, column_options
+ td.references t2_ref, column_options
yield td if block_given?
end
end
# Drops the join table specified by the given arguments.
- # See +create_join_table+ for details.
+ # See #create_join_table for details.
#
# Although this command ignores the block if one is given, it can be helpful
# to provide one in a migration's +change+ method so it can be reverted.
- # In that case, the block will be used by create_join_table.
+ # In that case, the block will be used by #create_join_table.
def drop_join_table(table_1, table_2, options = {})
join_table_name = find_join_table_name(table_1, table_2, options)
drop_table(join_table_name)
@@ -315,10 +407,12 @@ module ActiveRecord
# [<tt>:bulk</tt>]
# Set this to true to make this a bulk alter query, such as
#
- # ALTER TABLE `users` ADD COLUMN age INT(11), ADD COLUMN birthdate DATETIME ...
+ # ALTER TABLE `users` ADD COLUMN age INT, ADD COLUMN birthdate DATETIME ...
#
# Defaults to false.
#
+ # Only supported on the MySQL and PostgreSQL adapter, ignored elsewhere.
+ #
# ====== Add a column
#
# change_table(:suppliers) do |t|
@@ -343,7 +437,7 @@ module ActiveRecord
# t.references :company
# end
#
- # Creates a <tt>company_id(integer)</tt> column.
+ # Creates a <tt>company_id(bigint)</tt> column.
#
# ====== Add a polymorphic foreign key column
#
@@ -351,7 +445,7 @@ module ActiveRecord
# t.belongs_to :company, polymorphic: true
# end
#
- # Creates <tt>company_type(varchar)</tt> and <tt>company_id(integer)</tt> columns.
+ # Creates <tt>company_type(varchar)</tt> and <tt>company_id(bigint)</tt> columns.
#
# ====== Remove a column
#
@@ -372,7 +466,7 @@ module ActiveRecord
# t.remove_index :company_id
# end
#
- # See also Table for details on all of the various column transformation.
+ # See also Table for details on all of the various column transformations.
def change_table(table_name, options = {})
if supports_bulk_alter? && options[:bulk]
recorder = ActiveRecord::Migration::CommandRecorder.new(self)
@@ -402,17 +496,93 @@ module ActiveRecord
#
# Although this command ignores most +options+ and the block if one is given,
# it can be helpful to provide these in a migration's +change+ method so it can be reverted.
- # In that case, +options+ and the block will be used by create_table.
+ # In that case, +options+ and the block will be used by #create_table.
def drop_table(table_name, options = {})
execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}"
end
- # Adds a new column to the named table.
- # See TableDefinition#column for details of the options you can use.
- #
- # Note: Not all options will be available, generally this command should
- # ignore most of them. In favor of doing a low-level call to simply
- # create a column.
+ # Add a new +type+ column named +column_name+ to +table_name+.
+ #
+ # The +type+ parameter is normally one of the migrations native types,
+ # which is one of the following:
+ # <tt>:primary_key</tt>, <tt>:string</tt>, <tt>:text</tt>,
+ # <tt>:integer</tt>, <tt>:bigint</tt>, <tt>:float</tt>, <tt>:decimal</tt>, <tt>:numeric</tt>,
+ # <tt>:datetime</tt>, <tt>:time</tt>, <tt>:date</tt>,
+ # <tt>:binary</tt>, <tt>:boolean</tt>.
+ #
+ # You may use a type not in this list as long as it is supported by your
+ # database (for example, "polygon" in MySQL), but this will not be database
+ # agnostic and should usually be avoided.
+ #
+ # Available options are (none of these exists by default):
+ # * <tt>:limit</tt> -
+ # Requests a maximum column length. This is the number of characters for a <tt>:string</tt> column
+ # and number of bytes for <tt>:text</tt>, <tt>:binary</tt> and <tt>:integer</tt> columns.
+ # This option is ignored by some backends.
+ # * <tt>:default</tt> -
+ # The column's default value. Use +nil+ for +NULL+.
+ # * <tt>:null</tt> -
+ # Allows or disallows +NULL+ values in the column.
+ # * <tt>:precision</tt> -
+ # Specifies the precision for the <tt>:decimal</tt> and <tt>:numeric</tt> columns.
+ # * <tt>:scale</tt> -
+ # Specifies the scale for the <tt>:decimal</tt> and <tt>:numeric</tt> columns.
+ # * <tt>:collation</tt> -
+ # Specifies the collation for a <tt>:string</tt> or <tt>:text</tt> column. If not specified, the
+ # column will have the same collation as the table.
+ # * <tt>:comment</tt> -
+ # Specifies the comment for the column. This option is ignored by some backends.
+ #
+ # Note: The precision is the total number of significant digits,
+ # and the scale is the number of digits that can be stored following
+ # the decimal point. For example, the number 123.45 has a precision of 5
+ # and a scale of 2. A decimal with a precision of 5 and a scale of 2 can
+ # range from -999.99 to 999.99.
+ #
+ # Please be aware of different RDBMS implementations behavior with
+ # <tt>:decimal</tt> columns:
+ # * The SQL standard says the default scale should be 0, <tt>:scale</tt> <=
+ # <tt>:precision</tt>, and makes no comments about the requirements of
+ # <tt>:precision</tt>.
+ # * MySQL: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..30].
+ # Default is (10,0).
+ # * PostgreSQL: <tt>:precision</tt> [1..infinity],
+ # <tt>:scale</tt> [0..infinity]. No default.
+ # * SQLite3: No restrictions on <tt>:precision</tt> and <tt>:scale</tt>,
+ # but the maximum supported <tt>:precision</tt> is 16. No default.
+ # * Oracle: <tt>:precision</tt> [1..38], <tt>:scale</tt> [-84..127].
+ # Default is (38,0).
+ # * DB2: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..62].
+ # Default unknown.
+ # * SqlServer: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38].
+ # Default (38,0).
+ #
+ # == Examples
+ #
+ # add_column(:users, :picture, :binary, limit: 2.megabytes)
+ # # ALTER TABLE "users" ADD "picture" blob(2097152)
+ #
+ # add_column(:articles, :status, :string, limit: 20, default: 'draft', null: false)
+ # # ALTER TABLE "articles" ADD "status" varchar(20) DEFAULT 'draft' NOT NULL
+ #
+ # add_column(:answers, :bill_gates_money, :decimal, precision: 15, scale: 2)
+ # # ALTER TABLE "answers" ADD "bill_gates_money" decimal(15,2)
+ #
+ # add_column(:measurements, :sensor_reading, :decimal, precision: 30, scale: 20)
+ # # ALTER TABLE "measurements" ADD "sensor_reading" decimal(30,20)
+ #
+ # # While :scale defaults to zero on most databases, it
+ # # probably wouldn't hurt to include it.
+ # add_column(:measurements, :huge_integer, :decimal, precision: 30)
+ # # ALTER TABLE "measurements" ADD "huge_integer" decimal(30)
+ #
+ # # Defines a column that stores an array of a type.
+ # add_column(:users, :skills, :text, array: true)
+ # # ALTER TABLE "users" ADD "skills" text[]
+ #
+ # # Defines a column with a database-specific type.
+ # add_column(:shapes, :triangle, 'polygon')
+ # # ALTER TABLE "shapes" ADD "triangle" polygon
def add_column(table_name, column_name, type, options = {})
at = create_alter_table table_name
at.add_column(column_name, type, options)
@@ -436,9 +606,10 @@ module ActiveRecord
#
# The +type+ and +options+ parameters will be ignored if present. It can be helpful
# to provide these in a migration's +change+ method so it can be reverted.
- # In that case, +type+ and +options+ will be used by add_column.
+ # In that case, +type+ and +options+ will be used by #add_column.
+ # Indexes on the column are automatically removed.
def remove_column(table_name, column_name, type = nil, options = {})
- execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{quote_column_name(column_name)}"
+ execute "ALTER TABLE #{quote_table_name(table_name)} #{remove_column_for_alter(table_name, column_name, type, options)}"
end
# Changes the column's definition according to the new options.
@@ -469,7 +640,7 @@ module ActiveRecord
raise NotImplementedError, "change_column_default is not implemented"
end
- # Sets or removes a +NOT NULL+ constraint on a column. The +null+ flag
+ # Sets or removes a <tt>NOT NULL</tt> constraint on a column. The +null+ flag
# indicates whether the value can be +NULL+. For example
#
# change_column_null(:users, :nickname, false)
@@ -481,7 +652,7 @@ module ActiveRecord
# allows them to be +NULL+ (drops the constraint).
#
# The method accepts an optional fourth argument to replace existing
- # +NULL+s with some other value. Use that one when enabling the
+ # <tt>NULL</tt>s with some other value. Use that one when enabling the
# constraint if needed, since otherwise those rows would not be valid.
#
# Please note the fourth argument does not set a column's default.
@@ -535,6 +706,8 @@ module ActiveRecord
#
# CREATE INDEX by_name ON accounts(name(10))
#
+ # ====== Creating an index with specific key lengths for multiple keys
+ #
# add_index(:accounts, [:name, :surname], name: 'by_name_surname', length: {name: 10, surname: 15})
#
# generates:
@@ -551,7 +724,7 @@ module ActiveRecord
#
# CREATE INDEX by_branch_desc_party ON accounts(branch_id DESC, party_id ASC, surname)
#
- # Note: MySQL doesn't yet support index order (it accepts the syntax but ignores it).
+ # Note: MySQL only supports index order from 8.0.1 onwards (earlier versions accepted the syntax but ignored it).
#
# ====== Creating a partial index
#
@@ -574,6 +747,19 @@ module ActiveRecord
#
# Note: only supported by PostgreSQL and MySQL
#
+ # ====== Creating an index with a specific operator class
+ #
+ # add_index(:developers, :name, using: 'gist', opclass: :gist_trgm_ops)
+ # # CREATE INDEX developers_on_name ON developers USING gist (name gist_trgm_ops) -- PostgreSQL
+ #
+ # add_index(:developers, [:name, :city], using: 'gist', opclass: { city: :gist_trgm_ops })
+ # # CREATE INDEX developers_on_name_and_city ON developers USING gist (name, city gist_trgm_ops) -- PostgreSQL
+ #
+ # add_index(:developers, [:name, :city], using: 'gist', opclass: :gist_trgm_ops)
+ # # CREATE INDEX developers_on_name_and_city ON developers USING gist (name gist_trgm_ops, city gist_trgm_ops) -- PostgreSQL
+ #
+ # Note: only supported by PostgreSQL
+ #
# ====== Creating an index with a specific type
#
# add_index(:developers, :name, type: :fulltext)
@@ -582,7 +768,7 @@ module ActiveRecord
#
# CREATE FULLTEXT INDEX index_developers_on_name ON developers (name) -- MySQL
#
- # Note: only supported by MySQL. Supported: <tt>:fulltext</tt> and <tt>:spatial</tt> on MyISAM tables.
+ # Note: only supported by MySQL.
def add_index(table_name, column_name, options = {})
index_name, index_type, index_columns, index_options = add_index_options(table_name, column_name, options)
execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options}"
@@ -590,15 +776,15 @@ module ActiveRecord
# Removes the given index from the table.
#
- # Removes the +index_accounts_on_column+ in the +accounts+ table.
+ # Removes the index on +branch_id+ in the +accounts+ table if exactly one such index exists.
#
# remove_index :accounts, :branch_id
#
- # Removes the index named +index_accounts_on_branch_id+ in the +accounts+ table.
+ # Removes the index on +branch_id+ in the +accounts+ table if exactly one such index exists.
#
# remove_index :accounts, column: :branch_id
#
- # Removes the index named +index_accounts_on_branch_id_and_party_id+ in the +accounts+ table.
+ # Removes the index on +branch_id+ and +party_id+ in the +accounts+ table if exactly one such index exists.
#
# remove_index :accounts, column: [:branch_id, :party_id]
#
@@ -607,10 +793,7 @@ module ActiveRecord
# remove_index :accounts, name: :by_branch_party
#
def remove_index(table_name, options = {})
- remove_index!(table_name, index_name_for_remove(table_name, options))
- end
-
- def remove_index!(table_name, index_name) #:nodoc:
+ index_name = index_name_for_remove(table_name, options)
execute "DROP INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}"
end
@@ -623,7 +806,7 @@ module ActiveRecord
def rename_index(table_name, old_name, new_name)
validate_index_length!(table_name, new_name)
- # this is a naive implementation; some DBs may support this more efficiently (Postgres, for instance)
+ # this is a naive implementation; some DBs may support this more efficiently (PostgreSQL, for instance)
old_index_def = indexes(table_name).detect { |i| i.name == old_name }
return unless old_index_def
add_index(table_name, old_index_def.columns, name: new_name, unique: old_index_def.unique)
@@ -640,62 +823,73 @@ 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)
index_name = index_name.to_s
indexes(table_name).detect { |i| i.name == index_name }
end
- # Adds a reference. The reference column is an integer by default,
+ # Adds a reference. The reference column is a bigint by default,
# the <tt>:type</tt> option can be used to specify a different type.
# Optionally adds a +_type+ column, if <tt>:polymorphic</tt> option is provided.
- # <tt>add_reference</tt> and <tt>add_belongs_to</tt> are acceptable.
+ # #add_reference and #add_belongs_to are acceptable.
#
# The +options+ hash can include the following keys:
# [<tt>:type</tt>]
- # The reference column type. Defaults to +:integer+.
+ # The reference column type. Defaults to +:bigint+.
# [<tt>:index</tt>]
- # Add an appropriate index. Defaults to false.
+ # Add an appropriate index. Defaults to true.
+ # See #add_index for usage of this option.
# [<tt>:foreign_key</tt>]
- # Add an appropriate foreign key. Defaults to false.
+ # Add an appropriate foreign key constraint. Defaults to false.
# [<tt>:polymorphic</tt>]
# Whether an additional +_type+ column should be added. Defaults to false.
+ # [<tt>:null</tt>]
+ # Whether the column allows nulls. Defaults to true.
#
- # ====== Create a user_id integer column
+ # ====== Create a user_id bigint column without a index
#
- # add_reference(:products, :user)
+ # add_reference(:products, :user, index: false)
#
# ====== Create a user_id string column
#
# add_reference(:products, :user, type: :string)
#
- # ====== Create supplier_id, supplier_type columns and appropriate index
+ # ====== Create supplier_id, supplier_type columns
+ #
+ # add_reference(:products, :supplier, polymorphic: true)
#
- # add_reference(:products, :supplier, polymorphic: true, index: true)
+ # ====== Create a supplier_id column with a unique index
+ #
+ # add_reference(:products, :supplier, index: { unique: true })
+ #
+ # ====== Create a supplier_id column with a named index
+ #
+ # add_reference(:products, :supplier, index: { name: "my_supplier_index" })
#
# ====== Create a supplier_id column and appropriate foreign key
#
# add_reference(:products, :supplier, foreign_key: true)
#
- def add_reference(table_name, *args)
- ReferenceDefinition.new(*args).add_to(update_table_definition(table_name, self))
+ # ====== Create a supplier_id column and a foreign key to the firms table
+ #
+ # add_reference(:products, :supplier, foreign_key: {to_table: :firms})
+ #
+ 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
# Removes the reference(s). Also removes a +type+ column if one exists.
- # <tt>remove_reference</tt> and <tt>remove_belongs_to</tt> are acceptable.
+ # #remove_reference and #remove_belongs_to are acceptable.
#
# ====== Remove the reference
#
- # remove_reference(:products, :user, index: true)
+ # remove_reference(:products, :user, index: false)
#
# ====== Remove polymorphic reference
#
@@ -703,21 +897,27 @@ module ActiveRecord
#
# ====== Remove the reference with a foreign key
#
- # remove_reference(:products, :user, index: true, foreign_key: true)
+ # remove_reference(:products, :user, foreign_key: true)
#
- def remove_reference(table_name, ref_name, options = {})
- if options[:foreign_key]
+ def remove_reference(table_name, ref_name, foreign_key: false, polymorphic: false, **options)
+ if foreign_key
reference_name = Base.pluralize_table_names ? ref_name.to_s.pluralize : ref_name
- remove_foreign_key(table_name, reference_name)
+ if foreign_key.is_a?(Hash)
+ foreign_key_options = foreign_key
+ 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
remove_column(table_name, "#{ref_name}_id")
- remove_column(table_name, "#{ref_name}_type") if options[:polymorphic]
+ remove_column(table_name, "#{ref_name}_type") if polymorphic
end
alias :remove_belongs_to :remove_reference
# Returns an array of foreign keys for the given table.
- # The foreign keys are represented as +ForeignKeyDefinition+ objects.
+ # The foreign keys are represented as ForeignKeyDefinition objects.
def foreign_keys(table_name)
raise NotImplementedError, "foreign_keys is not implemented"
end
@@ -735,7 +935,7 @@ module ActiveRecord
#
# generates:
#
- # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id")
+ # ALTER TABLE "articles" ADD CONSTRAINT fk_rails_e74ce85cbc FOREIGN KEY ("author_id") REFERENCES "authors" ("id")
#
# ====== Creating a foreign key on a specific column
#
@@ -751,7 +951,7 @@ module ActiveRecord
#
# generates:
#
- # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id") ON DELETE CASCADE
+ # ALTER TABLE "articles" ADD CONSTRAINT fk_rails_e74ce85cbc FOREIGN KEY ("author_id") REFERENCES "authors" ("id") ON DELETE CASCADE
#
# The +options+ hash can include the following keys:
# [<tt>:column</tt>]
@@ -761,21 +961,15 @@ module ActiveRecord
# [<tt>:name</tt>]
# The constraint name. Defaults to <tt>fk_rails_<identifier></tt>.
# [<tt>:on_delete</tt>]
- # Action that happens <tt>ON DELETE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+
+ # Action that happens <tt>ON DELETE</tt>. Valid values are +:nullify+, +:cascade+ and +:restrict+
# [<tt>:on_update</tt>]
- # Action that happens <tt>ON UPDATE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+
+ # Action that happens <tt>ON UPDATE</tt>. Valid values are +:nullify+, +:cascade+ and +:restrict+
+ # [<tt>:validate</tt>]
+ # (Postgres only) Specify whether or not the constraint should be validated. Defaults to +true+.
def add_foreign_key(from_table, to_table, options = {})
return unless supports_foreign_keys?
- options[:column] ||= foreign_key_column_for(to_table)
-
- options = {
- column: options[:column],
- primary_key: options[:primary_key],
- name: foreign_key_name(from_table, options),
- on_delete: options[:on_delete],
- on_update: options[:on_update]
- }
+ options = foreign_key_options(from_table, to_table, options)
at = create_alter_table from_table
at.add_foreign_key to_table, options
@@ -795,11 +989,18 @@ module ActiveRecord
#
# remove_foreign_key :accounts, column: :owner_id
#
+ # Removes the foreign key on +accounts.owner_id+.
+ #
+ # remove_foreign_key :accounts, to_table: :owners
+ #
# Removes the foreign key named +special_fk_name+ on the +accounts+ table.
#
# remove_foreign_key :accounts, name: :special_fk_name
#
- # The +options+ hash accepts the same keys as SchemaStatements#add_foreign_key.
+ # The +options+ hash accepts the same keys as SchemaStatements#add_foreign_key
+ # with an addition of
+ # [<tt>:to_table</tt>]
+ # The name of the table that contains the referenced primary key.
def remove_foreign_key(from_table, options_or_to_table = {})
return unless supports_foreign_keys?
@@ -813,29 +1014,19 @@ module ActiveRecord
# Checks to see if a foreign key exists on a table for a given foreign key definition.
#
- # # Check a foreign key exists
+ # # Checks to see if a foreign key exists.
# foreign_key_exists?(:accounts, :branches)
#
- # # Check a foreign key on a specified column exists
+ # # Checks to see if a foreign key on a specified column exists.
# foreign_key_exists?(:accounts, column: :owner_id)
#
- # # Check a foreign key with a custom name exists
+ # # Checks to see if a foreign key with a custom name exists.
# foreign_key_exists?(:accounts, name: "special_fk_name")
#
def foreign_key_exists?(from_table, options_or_to_table = {})
foreign_key_for(from_table, options_or_to_table).present?
end
- def foreign_key_for(from_table, options_or_to_table = {}) # :nodoc:
- return unless supports_foreign_keys?
- foreign_keys(from_table).detect {|fk| fk.defined_for? options_or_to_table }
- end
-
- def foreign_key_for!(from_table, options_or_to_table = {}) # :nodoc:
- foreign_key_for(from_table, options_or_to_table) or \
- raise ArgumentError, "Table '#{from_table}' has no foreign key for #{options_or_to_table}"
- end
-
def foreign_key_column_for(table_name) # :nodoc:
prefix = Base.table_name_prefix
suffix = Base.table_name_suffix
@@ -843,48 +1034,48 @@ module ActiveRecord
"#{name.singularize}_id"
end
- def dump_schema_information #:nodoc:
- sm_table = ActiveRecord::Migrator.schema_migrations_table_name
+ def foreign_key_options(from_table, to_table, options) # :nodoc:
+ options = options.dup
+ options[:column] ||= foreign_key_column_for(to_table)
+ options[:name] ||= foreign_key_name(from_table, options)
+ options
+ end
- ActiveRecord::SchemaMigration.order('version').map { |sm|
- "INSERT INTO #{sm_table} (version) VALUES ('#{sm.version}');"
- }.join "\n\n"
+ def dump_schema_information #:nodoc:
+ versions = ActiveRecord::SchemaMigration.all_versions
+ insert_versions_sql(versions) if versions.any?
end
- # Should not be called normally, but this operation is non-destructive.
- # The migrations module handles this automatically.
- def initialize_schema_migrations_table
- ActiveRecord::SchemaMigration.create_table
+ def internal_string_options_for_primary_key # :nodoc:
+ { primary_key: true }
end
- def assume_migrated_upto_version(version, migrations_paths = ActiveRecord::Migrator.migrations_paths)
+ 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 = migration_context.migration_files.map do |file|
+ migration_context.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
- inserted = Set.new
- (versions - migrated).each do |v|
- if inserted.include?(v)
- raise "Duplicate migration #{v}. Please renumber your migrations to resolve the conflict."
- elsif v < version
- execute "INSERT INTO #{sm_table} (version) VALUES ('#{v}')"
- inserted << v
+ inserting = (versions - migrated).select { |v| v < version }
+ if inserting.any?
+ 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)
end
end
- def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc:
- if native = native_database_types[type.to_sym]
+ 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
if type == :decimal # ignore limit, use precision and scale
@@ -900,7 +1091,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
@@ -917,18 +1108,19 @@ module ActiveRecord
end
# Given a set of columns and an ORDER BY clause, returns the columns for a SELECT DISTINCT.
- # Both PostgreSQL and Oracle overrides this for custom DISTINCT syntax - they
+ # PostgreSQL, MySQL, and Oracle override this for custom DISTINCT syntax - they
# require the order columns appear in the SELECT.
#
# columns_for_distinct("posts.id", ["posts.created_at desc"])
- def columns_for_distinct(columns, orders) #:nodoc:
+ #
+ def columns_for_distinct(columns, orders) # :nodoc:
columns
end
# Adds timestamps (+created_at+ and +updated_at+) columns to +table_name+.
- # Additional options (like <tt>null: false</tt>) are forwarded to #add_column.
+ # Additional options (like +:null+) are forwarded to #add_column.
#
- # add_timestamps(:suppliers, null: false)
+ # add_timestamps(:suppliers, null: true)
#
def add_timestamps(table_name, options = {})
options[:null] = false if options[:null].nil?
@@ -950,16 +1142,15 @@ module ActiveRecord
Table.new(table_name, base)
end
- def add_index_options(table_name, column_name, options = {}) #:nodoc:
- column_names = Array(column_name)
+ def add_index_options(table_name, column_name, comment: nil, **options) # :nodoc:
+ column_names = index_column_names(column_name)
- options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type)
+ options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type, :opclass)
index_type = options[:type].to_s if options.key?(:type)
index_type ||= options[:unique] ? "UNIQUE" : ""
index_name = options[:name].to_s if options.key?(:name)
- index_name ||= index_name(table_name, column: column_names)
- max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length
+ index_name ||= index_name(table_name, column_names)
if options.key?(:algorithm)
algorithm = index_algorithms.fetch(options[:algorithm]) {
@@ -973,63 +1164,99 @@ module ActiveRecord
index_options = options[:where] ? " WHERE #{options[:where]}" : ""
end
- if index_name.length > max_index_length
- raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{max_index_length} characters"
- end
- if table_exists?(table_name) && index_name_exists?(table_name, index_name, false)
+ validate_index_length!(table_name, index_name, options.fetch(:internal, 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(", ")
- [index_name, index_type, index_columns, index_options, algorithm, using]
+ [index_name, index_type, index_columns, index_options, algorithm, using, comment]
end
- protected
- def add_index_sort_order(option_strings, column_names, options = {})
- if options.is_a?(Hash) && order = options[:order]
- case order
- when Hash
- column_names.each {|name| option_strings[name] += " #{order[name].upcase}" if order.has_key?(name)}
- when String
- column_names.each {|name| option_strings[name] += " #{order.upcase}"}
- end
- end
+ def options_include_default?(options)
+ options.include?(:default) && !(options[:null] == false && options[:default].nil?)
+ end
+
+ # Changes the comment for a table or removes it if +nil+.
+ def change_table_comment(table_name, comment)
+ raise NotImplementedError, "#{self.class} does not support changing table comments"
+ end
+
+ # Changes the comment for a column or removes it if +nil+.
+ def change_column_comment(table_name, column_name, comment)
+ raise NotImplementedError, "#{self.class} does not support changing column comments"
+ end
+
+ def create_schema_dumper(options) # :nodoc:
+ SchemaDumper.create(self, options)
+ end
- return option_strings
+ private
+ def column_options_keys
+ [:limit, :precision, :scale, :default, :null, :collation, :comment]
end
- # Overridden by the MySQL adapter for supporting index lengths
- def quoted_columns_for_index(column_names, options = {})
- option_strings = Hash[column_names.map {|name| [name, '']}]
+ def add_index_sort_order(quoted_columns, **options)
+ orders = options_for_index_columns(options[:order])
+ quoted_columns.each do |name, column|
+ column << " #{orders[name].upcase}" if orders[name].present?
+ end
+ end
- # add index sort order if supported
+ def options_for_index_columns(options)
+ if options.is_a?(Hash)
+ options.symbolize_keys
+ else
+ Hash.new { |hash, column| hash[column] = options }
+ end
+ end
+
+ # Overridden by the MySQL adapter for supporting index lengths and by
+ # the PostgreSQL adapter for supporting operator classes.
+ def add_options_for_index_columns(quoted_columns, **options)
if supports_index_sort_order?
- option_strings = add_index_sort_order(option_strings, column_names, options)
+ quoted_columns = add_index_sort_order(quoted_columns, options)
end
- column_names.map {|name| quote_column_name(name) + option_strings[name]}
+ quoted_columns
end
- def options_include_default?(options)
- options.include?(:default) && !(options[:null] == false && options[:default].nil?)
+ def quoted_columns_for_index(column_names, **options)
+ return [column_names] if column_names.is_a?(String)
+
+ quoted_columns = Hash[column_names.map { |name| [name.to_sym, quote_column_name(name).dup] }]
+ add_options_for_index_columns(quoted_columns, options).values
end
def index_name_for_remove(table_name, options = {})
- index_name = index_name(table_name, options)
+ return options[:name] if can_remove_index_by_name?(options)
- unless index_name_exists?(table_name, index_name, true)
- 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)
+ checks = []
- return index_name_without_column if index_name_exists?(table_name, index_name_without_column, false)
- end
+ if options.is_a?(Hash)
+ checks << lambda { |i| i.name == options[:name].to_s } if options.key?(:name)
+ column_names = index_column_names(options[:column])
+ else
+ column_names = index_column_names(options)
+ end
- raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist"
+ if column_names.present?
+ checks << lambda { |i| index_name(table_name, i.columns) == index_name(table_name, column_names) }
end
- index_name
+ raise ArgumentError, "No name or columns specified" if checks.none?
+
+ matching_indexes = indexes(table_name).select { |i| checks.all? { |check| check[i] } }
+
+ if matching_indexes.count > 1
+ raise ArgumentError, "Multiple indexes found on #{table_name} columns #{column_names}. " \
+ "Specify an index name from #{matching_indexes.map(&:name).join(', ')}"
+ elsif matching_indexes.none?
+ raise ArgumentError, "No indexes found on #{table_name} with the options provided."
+ else
+ matching_indexes.first.name
+ end
end
def rename_table_indexes(table_name, new_name)
@@ -1054,36 +1281,126 @@ module ActiveRecord
end
end
- private
- def create_table_definition(name, temporary = false, options = nil, as = nil)
- TableDefinition.new native_database_types, name, temporary, options, as
- end
+ def schema_creation
+ SchemaCreation.new(self)
+ end
- def create_alter_table(name)
- AlterTable.new create_table_definition(name)
- end
+ def create_table_definition(*args)
+ TableDefinition.new(*args)
+ end
- def foreign_key_name(table_name, options) # :nodoc:
- identifier = "#{table_name}_#{options.fetch(:column)}_fk"
- hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10)
- options.fetch(:name) do
- "fk_rails_#{hashed_identifier}"
+ def create_alter_table(name)
+ AlterTable.new create_table_definition(name)
end
- end
- def validate_index_length!(table_name, new_name) # :nodoc:
- if new_name.length > allowed_index_name_length
- raise ArgumentError, "Index name '#{new_name}' on table '#{table_name}' is too long; the limit is #{allowed_index_name_length} characters"
+ def fetch_type_metadata(sql_type)
+ cast_type = lookup_cast_type(sql_type)
+ SqlTypeMetadata.new(
+ sql_type: sql_type,
+ type: cast_type.type,
+ limit: cast_type.limit,
+ precision: cast_type.precision,
+ scale: cast_type.scale,
+ )
end
- end
- def extract_new_default_value(default_or_changes)
- if default_or_changes.is_a?(Hash) && default_or_changes.has_key?(:from) && default_or_changes.has_key?(:to)
- default_or_changes[:to]
- else
- default_or_changes
+ 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)
+ options.fetch(:name) do
+ identifier = "#{table_name}_#{options.fetch(:column)}_fk"
+ hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10)
+
+ "fk_rails_#{hashed_identifier}"
+ end
+ end
+
+ def foreign_key_for(from_table, options_or_to_table = {})
+ return unless supports_foreign_keys?
+ foreign_keys(from_table).detect { |fk| fk.defined_for? options_or_to_table }
+ end
+
+ def foreign_key_for!(from_table, options_or_to_table = {})
+ foreign_key_for(from_table, options_or_to_table) || \
+ raise(ArgumentError, "Table '#{from_table}' has no foreign key for #{options_or_to_table}")
+ end
+
+ def extract_foreign_key_action(specifier)
+ case specifier
+ when "CASCADE"; :cascade
+ when "SET NULL"; :nullify
+ when "RESTRICT"; :restrict
+ end
+ end
+
+ 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
+ raise ArgumentError, "Index name '#{new_name}' on table '#{table_name}' is too long; the limit is #{allowed_index_name_length} characters"
+ end
+ end
+
+ def extract_new_default_value(default_or_changes)
+ if default_or_changes.is_a?(Hash) && default_or_changes.has_key?(:from) && default_or_changes.has_key?(:to)
+ default_or_changes[:to]
+ else
+ default_or_changes
+ end
+ end
+
+ def can_remove_index_by_name?(options)
+ options.is_a?(Hash) && options.key?(:name) && options.except(:name, :algorithm).empty?
+ end
+
+ def add_column_for_alter(table_name, column_name, type, options = {})
+ td = create_table_definition(table_name)
+ cd = td.new_column_definition(column_name, type, options)
+ schema_creation.accept(AddColumnDefinition.new(cd))
+ end
+
+ def remove_column_for_alter(table_name, column_name, type = nil, options = {})
+ "DROP COLUMN #{quote_column_name(column_name)}"
+ end
+
+ def remove_columns_for_alter(table_name, *column_names)
+ column_names.map { |column_name| remove_column_for_alter(table_name, column_name) }
+ end
+
+ def insert_versions_sql(versions)
+ sm_table = quote_table_name(ActiveRecord::SchemaMigration.table_name)
+
+ if versions.is_a?(Array)
+ sql = +"INSERT INTO #{sm_table} (version) VALUES\n"
+ sql << versions.map { |v| "(#{quote(v)})" }.join(",\n")
+ sql << ";\n\n"
+ sql
+ else
+ "INSERT INTO #{sm_table} (version) VALUES (#{quote(versions)});"
+ end
+ 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
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
index 295a7bed87..0f2b1e85ff 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
@@ -1,10 +1,15 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
class TransactionState
- VALID_STATES = Set.new([:committed, :rolledback, nil])
-
def initialize(state = nil)
@state = state
+ @children = []
+ end
+
+ def add_child(state)
+ @children << state
end
def finalized?
@@ -12,11 +17,23 @@ module ActiveRecord
end
def committed?
- @state == :committed
+ @state == :committed || @state == :fully_committed
+ end
+
+ def fully_committed?
+ @state == :fully_committed
end
def rolledback?
- @state == :rolledback
+ @state == :rolledback || @state == :fully_rolledback
+ end
+
+ def fully_rolledback?
+ @state == :fully_rolledback
+ end
+
+ def fully_completed?
+ completed?
end
def completed?
@@ -24,15 +41,49 @@ module ActiveRecord
end
def set_state(state)
- unless VALID_STATES.include?(state)
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ The set_state method is deprecated and will be removed in
+ Rails 6.0. Please use rollback! or commit! to set transaction
+ state directly.
+ MSG
+ case state
+ when :rolledback
+ rollback!
+ when :committed
+ commit!
+ when nil
+ nullify!
+ else
raise ArgumentError, "Invalid transaction state: #{state}"
end
- @state = state
+ end
+
+ def rollback!
+ @children.each { |c| c.rollback! }
+ @state = :rolledback
+ end
+
+ def full_rollback!
+ @children.each { |c| c.rollback! }
+ @state = :fully_rolledback
+ end
+
+ def commit!
+ @state = :committed
+ end
+
+ def full_commit!
+ @state = :fully_committed
+ end
+
+ def nullify!
+ @state = nil
end
end
class NullTransaction #:nodoc:
def initialize; end
+ def state; end
def closed?; true; end
def open?; false; end
def joinable?; false; end
@@ -40,14 +91,14 @@ module ActiveRecord
end
class Transaction #:nodoc:
-
- attr_reader :connection, :state, :records, :savepoint_name
- attr_writer :joinable
+ attr_reader :connection, :state, :records, :savepoint_name, :isolation_level
def initialize(connection, options, run_commit_callbacks: false)
@connection = connection
@state = TransactionState.new
@records = []
+ @isolation_level = options[:isolation]
+ @materialized = false
@joinable = options.fetch(:joinable, true)
@run_commit_callbacks = run_commit_callbacks
end
@@ -56,8 +107,12 @@ module ActiveRecord
records << record
end
- def rollback
- @state.set_state(:rolledback)
+ def materialize!
+ @materialized = true
+ end
+
+ def materialized?
+ @materialized
end
def rollback_records
@@ -71,10 +126,6 @@ module ActiveRecord
end
end
- def commit
- @state.set_state(:committed)
- end
-
def before_commit_records
records.uniq.each(&:before_committed!) if @run_commit_callbacks
end
@@ -86,7 +137,7 @@ module ActiveRecord
record.committed!
else
# if not running callbacks, only adds the record to the parent transaction
- record.add_to_transaction
+ connection.add_transaction_record(record)
end
end
ensure
@@ -100,47 +151,55 @@ module ActiveRecord
end
class SavepointTransaction < Transaction
+ def initialize(connection, savepoint_name, parent_transaction, *args)
+ super(connection, *args)
+
+ parent_transaction.state.add_child(@state)
- def initialize(connection, savepoint_name, options, *args)
- super(connection, options, *args)
- if options[:isolation]
+ if isolation_level
raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction"
end
- connection.create_savepoint(@savepoint_name = savepoint_name)
+
+ @savepoint_name = savepoint_name
end
- def rollback
- connection.rollback_to_savepoint(savepoint_name)
+ def materialize!
+ connection.create_savepoint(savepoint_name)
super
end
+ def rollback
+ connection.rollback_to_savepoint(savepoint_name) if materialized?
+ @state.rollback!
+ end
+
def commit
- connection.release_savepoint(savepoint_name)
- super
+ connection.release_savepoint(savepoint_name) if materialized?
+ @state.commit!
end
def full_rollback?; false; end
end
class RealTransaction < Transaction
-
- def initialize(connection, options, *args)
- super
- if options[:isolation]
- connection.begin_isolated_db_transaction(options[:isolation])
+ def materialize!
+ if isolation_level
+ connection.begin_isolated_db_transaction(isolation_level)
else
connection.begin_db_transaction
end
+
+ super
end
def rollback
- connection.rollback_db_transaction
- super
+ connection.rollback_db_transaction if materialized?
+ @state.full_rollback!
end
def commit
- connection.commit_db_transaction
- super
+ connection.commit_db_transaction if materialized?
+ @state.full_commit!
end
end
@@ -148,52 +207,103 @@ module ActiveRecord
def initialize(connection)
@stack = []
@connection = connection
+ @has_unmaterialized_transactions = false
+ @materializing_transactions = false
+ @lazy_transactions_enabled = true
end
def begin_transaction(options = {})
- run_commit_callbacks = !current_transaction.joinable?
- transaction =
- if @stack.empty?
- RealTransaction.new(@connection, options, run_commit_callbacks: run_commit_callbacks)
- else
- SavepointTransaction.new(@connection, "active_record_#{@stack.size}", options,
- run_commit_callbacks: run_commit_callbacks)
- end
+ @connection.lock.synchronize do
+ run_commit_callbacks = !current_transaction.joinable?
+ transaction =
+ if @stack.empty?
+ RealTransaction.new(@connection, options, run_commit_callbacks: run_commit_callbacks)
+ else
+ SavepointTransaction.new(@connection, "active_record_#{@stack.size}", @stack.last, options,
+ run_commit_callbacks: run_commit_callbacks)
+ end
+
+ transaction.materialize! unless @connection.supports_lazy_transactions? && lazy_transactions_enabled?
+ @stack.push(transaction)
+ @has_unmaterialized_transactions = true if @connection.supports_lazy_transactions?
+ transaction
+ end
+ end
+
+ def disable_lazy_transactions!
+ materialize_transactions
+ @lazy_transactions_enabled = false
+ end
+
+ def enable_lazy_transactions!
+ @lazy_transactions_enabled = true
+ end
- @stack.push(transaction)
- transaction
+ def lazy_transactions_enabled?
+ @lazy_transactions_enabled
+ end
+
+ def materialize_transactions
+ return if @materializing_transactions
+ return unless @has_unmaterialized_transactions
+
+ @connection.lock.synchronize do
+ begin
+ @materializing_transactions = true
+ @stack.each { |t| t.materialize! unless t.materialized? }
+ ensure
+ @materializing_transactions = false
+ end
+ @has_unmaterialized_transactions = false
+ end
end
def commit_transaction
- transaction = @stack.last
- transaction.before_commit_records
- @stack.pop
- transaction.commit
- transaction.commit_records
+ @connection.lock.synchronize do
+ transaction = @stack.last
+
+ begin
+ transaction.before_commit_records
+ ensure
+ @stack.pop
+ end
+
+ transaction.commit
+ transaction.commit_records
+ end
end
def rollback_transaction(transaction = nil)
- transaction ||= @stack.pop
- transaction.rollback
- transaction.rollback_records
+ @connection.lock.synchronize do
+ transaction ||= @stack.pop
+ transaction.rollback
+ transaction.rollback_records
+ end
end
def within_new_transaction(options = {})
- transaction = begin_transaction options
- yield
- rescue Exception => error
- rollback_transaction if transaction
- raise
- ensure
- unless error
- if Thread.current.status == 'aborting'
- rollback_transaction if transaction
- else
- begin
- commit_transaction
- rescue Exception
- rollback_transaction(transaction) unless transaction.state.completed?
- raise
+ @connection.lock.synchronize do
+ begin
+ transaction = begin_transaction options
+ yield
+ rescue Exception => error
+ if transaction
+ rollback_transaction
+ after_failure_actions(transaction, error)
+ end
+ raise
+ ensure
+ unless error
+ if Thread.current.status == "aborting"
+ rollback_transaction if transaction
+ else
+ begin
+ commit_transaction if transaction
+ rescue Exception
+ rollback_transaction(transaction) unless transaction.state.completed?
+ raise
+ end
+ end
end
end
end
@@ -208,7 +318,15 @@ module ActiveRecord
end
private
+
NULL_TRANSACTION = NullTransaction.new
+
+ # Deallocate invalidated prepared statements outside of the transaction
+ def after_failure_actions(transaction, error)
+ return unless transaction.is_a?(RealTransaction)
+ return unless error.is_a?(ActiveRecord::PreparedStatementCacheExpired)
+ @connection.clear_cache!
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index 6d3a21a3dc..0fe868478c 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -1,11 +1,15 @@
-require 'active_record/type'
-require 'active_support/core_ext/benchmark'
-require 'active_record/connection_adapters/schema_cache'
-require 'active_record/connection_adapters/sql_type_metadata'
-require 'active_record/connection_adapters/abstract/schema_dumper'
-require 'active_record/connection_adapters/abstract/schema_creation'
-require 'arel/collectors/bind'
-require 'arel/collectors/sql_string'
+# frozen_string_literal: true
+
+require "active_record/connection_adapters/determine_if_preparable_visitor"
+require "active_record/connection_adapters/schema_cache"
+require "active_record/connection_adapters/sql_type_metadata"
+require "active_record/connection_adapters/abstract/schema_dumper"
+require "active_record/connection_adapters/abstract/schema_creation"
+require "active_support/concurrency/load_interlock_aware_monitor"
+require "arel/collectors/bind"
+require "arel/collectors/composite"
+require "arel/collectors/sql_string"
+require "arel/collectors/substitute_binds"
module ActiveRecord
module ConnectionAdapters # :nodoc:
@@ -14,7 +18,7 @@ module ActiveRecord
autoload :Column
autoload :ConnectionSpecification
- autoload_at 'active_record/connection_adapters/abstract/schema_definitions' do
+ autoload_at "active_record/connection_adapters/abstract/schema_definitions" do
autoload :IndexDefinition
autoload :ColumnDefinition
autoload :ChangeColumnDefinition
@@ -22,14 +26,14 @@ module ActiveRecord
autoload :TableDefinition
autoload :Table
autoload :AlterTable
+ autoload :ReferenceDefinition
end
- autoload_at 'active_record/connection_adapters/abstract/connection_pool' do
+ autoload_at "active_record/connection_adapters/abstract/connection_pool" do
autoload :ConnectionHandler
- autoload :ConnectionManagement
end
- autoload_under 'abstract' do
+ autoload_under "abstract" do
autoload :SchemaStatements
autoload :DatabaseStatements
autoload :DatabaseLimits
@@ -39,7 +43,7 @@ module ActiveRecord
autoload :Savepoints
end
- autoload_at 'active_record/connection_adapters/abstract/transaction' do
+ autoload_at "active_record/connection_adapters/abstract/transaction" do
autoload :TransactionManager
autoload :NullTransaction
autoload :RealTransaction
@@ -51,33 +55,37 @@ module ActiveRecord
# related classes form the abstraction layer which makes this possible.
# An AbstractAdapter represents a connection to a database, and provides an
# abstract interface for database-specific functionality such as establishing
- # a connection, escaping values, building the right SQL fragments for ':offset'
- # and ':limit' options, etc.
+ # a connection, escaping values, building the right SQL fragments for +:offset+
+ # and +:limit+ options, etc.
#
# All the concrete database adapters follow the interface laid down in this class.
- # ActiveRecord::Base.connection returns an AbstractAdapter object, which
+ # {ActiveRecord::Base.connection}[rdoc-ref:ConnectionHandling#connection] returns an AbstractAdapter object, which
# you can use.
#
# Most of the methods in the adapter are useful during migrations. Most
- # notably, the instance methods provided by SchemaStatement are very useful.
+ # notably, the instance methods provided by SchemaStatements are very useful.
class AbstractAdapter
- ADAPTER_NAME = 'Abstract'.freeze
+ ADAPTER_NAME = "Abstract"
+ include ActiveSupport::Callbacks
+ define_callbacks :checkout, :checkin
+
include Quoting, DatabaseStatements, SchemaStatements
include DatabaseLimits
include QueryCache
- include ActiveSupport::Callbacks
- include ColumnDumper
+ include Savepoints
SIMPLE_INT = /\A\d+\z/
- define_callbacks :checkout, :checkin
-
attr_accessor :visitor, :pool
- attr_reader :schema_cache, :owner, :logger
+ attr_reader :schema_cache, :owner, :logger, :prepared_statements, :lock
alias :in_use? :owner
+ set_callback :checkin, :after, :enable_lazy_transactions!
+
def self.type_cast_config_to_integer(config)
- if config =~ SIMPLE_INT
+ if config.is_a?(Integer)
+ config
+ elsif SIMPLE_INT.match?(config)
config.to_i
else
config
@@ -92,58 +100,75 @@ module ActiveRecord
end
end
- attr_reader :prepared_statements
-
- def initialize(connection, logger = nil, pool = nil) #:nodoc:
+ def initialize(connection, logger = nil, config = {}) # :nodoc:
super()
@connection = connection
@owner = nil
@instrumenter = ActiveSupport::Notifications.instrumenter
@logger = logger
- @pool = pool
+ @config = config
+ @pool = nil
+ @idle_since = Concurrent.monotonic_time
@schema_cache = SchemaCache.new self
- @visitor = nil
- @prepared_statements = false
- end
+ @quoted_column_names, @quoted_table_names = {}, {}
+ @visitor = arel_visitor
+ @lock = ActiveSupport::Concurrency::LoadInterlockAwareMonitor.new
- class BindCollector < Arel::Collectors::Bind
- def compile(bvs, conn)
- casted_binds = conn.prepare_binds_for_database(bvs)
- super(casted_binds.map { |value| conn.quote(value) })
+ if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
+ @prepared_statements = true
+ @visitor.extend(DetermineIfPreparableVisitor)
+ else
+ @prepared_statements = false
end
+
+ @advisory_locks_enabled = self.class.type_cast_config_to_boolean(
+ config.fetch(:advisory_locks, true)
+ )
+
+ check_version
end
- class SQLString < Arel::Collectors::SQLString
- def compile(bvs, conn)
- super(bvs)
- end
+ def replica?
+ @config[:replica] || false
end
- def collector
- if prepared_statements
- SQLString.new
- else
- BindCollector.new
- end
+ def migrations_paths # :nodoc:
+ @config[:migrations_paths] || Migrator.migrations_paths
end
- def valid_type?(type)
- true
+ def migration_context # :nodoc:
+ MigrationContext.new(migrations_paths)
end
- def schema_creation
- SchemaCreation.new self
+ class Version
+ include Comparable
+
+ def initialize(version_string)
+ @version = version_string.split(".").map(&:to_i)
+ end
+
+ def <=>(version_string)
+ @version <=> version_string.split(".").map(&:to_i)
+ end
+
+ def to_s
+ @version.join(".")
+ end
+ end
+
+ def valid_type?(type) # :nodoc:
+ !native_database_types[type].nil?
end
# this method must only be called while holding connection pool's mutex
def lease
if in_use?
- msg = 'Cannot lease connection, '
+ msg = +"Cannot lease connection, "
if @owner == Thread.current
- msg << 'it is already leased by the current thread.'
+ 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
@@ -159,7 +184,37 @@ module ActiveRecord
# this method must only be called while holding connection pool's mutex
def expire
- @owner = nil
+ if in_use?
+ if @owner != Thread.current
+ raise ActiveRecordError, "Cannot expire connection, " \
+ "it is owned by a different thread: #{@owner}. " \
+ "Current thread: #{Thread.current}."
+ end
+
+ @idle_since = Concurrent.monotonic_time
+ @owner = nil
+ else
+ raise ActiveRecordError, "Cannot expire connection, it is not currently leased."
+ end
+ end
+
+ # this method must only be called while holding connection pool's mutex (and a desire for segfaults)
+ def steal! # :nodoc:
+ if in_use?
+ if @owner != Thread.current
+ pool.send :remove_connection_from_thread_cache, self, @owner
+
+ @owner = Thread.current
+ end
+ else
+ raise ActiveRecordError, "Cannot steal connection, it is not currently leased."
+ end
+ end
+
+ # Seconds since this connection was returned to the pool
+ def seconds_idle # :nodoc:
+ return 0 if in_use?
+ Concurrent.monotonic_time - @idle_since
end
def unprepared_statement
@@ -175,17 +230,6 @@ module ActiveRecord
self.class::ADAPTER_NAME
end
- # Does this adapter support migrations?
- def supports_migrations?
- false
- end
-
- # 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
- end
-
# Does this adapter support DDL rollbacks in transactions? That is, would
# CREATE TABLE or ALTER TABLE get rolled back by a transaction?
def supports_ddl_transactions?
@@ -201,6 +245,11 @@ module ActiveRecord
false
end
+ # Does this adapter support application-enforced advisory locking?
+ def supports_advisory_locks?
+ false
+ end
+
# Should primary key values be selected from their corresponding
# sequence before the insert statement? If true, next_sequence_value
# is called before each insert to set the record's primary key.
@@ -218,6 +267,11 @@ module ActiveRecord
false
end
+ # Does this adapter support expression indices?
+ def supports_expression_index?
+ false
+ end
+
# Does this adapter support explain?
def supports_explain?
false
@@ -244,6 +298,17 @@ module ActiveRecord
false
end
+ # Does this adapter support creating invalid constraints?
+ def supports_validate_constraints?
+ false
+ end
+
+ # Does this adapter support creating foreign key constraints
+ # in the same statement as creating the table?
+ def supports_foreign_keys_in_create?
+ supports_foreign_keys?
+ end
+
# Does this adapter support views?
def supports_views?
false
@@ -254,6 +319,41 @@ module ActiveRecord
false
end
+ # Does this adapter support json data type?
+ def supports_json?
+ false
+ end
+
+ # Does this adapter support metadata comments on database objects (tables, columns, indexes)?
+ def supports_comments?
+ false
+ end
+
+ # Can comments for tables, columns, and indexes be specified in create/alter table statements?
+ def supports_comments_in_create?
+ false
+ end
+
+ # Does this adapter support multi-value insert?
+ def supports_multi_insert?
+ true
+ end
+ deprecate :supports_multi_insert?
+
+ # Does this adapter support virtual columns?
+ def supports_virtual_columns?
+ false
+ end
+
+ # Does this adapter support foreign/external tables?
+ def supports_foreign_tables?
+ false
+ end
+
+ def supports_lazy_transactions?
+ false
+ end
+
# This is meant to be implemented by the adapters that support extensions
def disable_extension(name)
end
@@ -262,6 +362,24 @@ module ActiveRecord
def enable_extension(name)
end
+ def advisory_locks_enabled? # :nodoc:
+ supports_advisory_locks? && @advisory_locks_enabled
+ end
+
+ # This is meant to be implemented by the adapters that support advisory
+ # locks
+ #
+ # Return true if we got the lock, otherwise false
+ def get_advisory_lock(lock_id) # :nodoc:
+ end
+
+ # This is meant to be implemented by the adapters that support advisory
+ # locks.
+ #
+ # Return true if we released the lock, otherwise false
+ def release_advisory_lock(lock_id) # :nodoc:
+ end
+
# A list of extensions, to be filled in by adapters that support them.
def extensions
[]
@@ -272,12 +390,6 @@ module ActiveRecord
{}
end
- # Returns a bind substitution value given a bind +column+
- # NOTE: The column param is currently being used by the sqlserver-adapter
- def substitute_at(column, _unused = 0)
- Arel::Nodes::BindParam.new
- end
-
# REFERENTIAL INTEGRITY ====================================
# Override to turn off referential integrity while executing <tt>&block</tt>.
@@ -308,6 +420,19 @@ module ActiveRecord
reset_transaction
end
+ # Immediately forget this connection ever existed. Unlike disconnect!,
+ # this will not communicate with the server.
+ #
+ # After calling this method, the behavior of all other methods becomes
+ # undefined. This is called internally just before a forked process gets
+ # rid of a connection that belonged to its parent.
+ def discard!
+ # This should be overridden by concrete adapters.
+ #
+ # Prevent @connection's finalizer from touching the socket, or
+ # otherwise communicating with its server, when it is collected.
+ end
+
# Reset the state of this connection, directing the DBMS to clear
# transactions and other connection-related server-side state. Usually a
# database-dependent operation.
@@ -331,43 +456,32 @@ module ActiveRecord
end
# Checks whether the connection to the database is still active (i.e. not stale).
- # This is done under the hood by calling <tt>active?</tt>. If the connection
+ # 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)
+ def verify!
reconnect! unless active?
end
# Provides access to the underlying database driver for this adapter. For
- # example, this method returns a Mysql object in case of MysqlAdapter,
- # and a PGconn object in case of PostgreSQLAdapter.
+ # example, this method returns a Mysql2::Client object in case of Mysql2Adapter,
+ # 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.
def raw_connection
+ disable_lazy_transactions!
@connection
end
- def create_savepoint(name = nil)
- end
-
- def release_savepoint(name = nil)
- end
-
- def case_sensitive_modifier(node, table_attribute)
- node
- end
-
- def case_sensitive_comparison(table, attribute, column, value)
- table_attr = table[attribute]
- value = case_sensitive_modifier(value, table_attr) unless value.nil?
- table_attr.eq(value)
+ 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(value))
else
- case_sensitive_comparison(table, attribute, column, value)
+ table[attribute].eq(value)
end
end
@@ -376,143 +490,164 @@ module ActiveRecord
end
private :can_perform_case_insensitive_comparison_for?
- def current_savepoint_name
- current_transaction.savepoint_name
- end
-
# Check the connection back in to the connection pool
def close
pool.checkin self
end
- def type_map # :nodoc:
- @type_map ||= Type::TypeMap.new.tap do |mapping|
- initialize_type_map(mapping)
- end
+ def column_name_for_operation(operation, node) # :nodoc:
+ visitor.compile(node)
end
- def new_column(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil)
- Column.new(name, default, sql_type_metadata, null, default_function, collation)
+ def default_index_type?(index) # :nodoc:
+ index.using.nil?
end
- def lookup_cast_type(sql_type) # :nodoc:
- type_map.lookup(sql_type)
- end
+ private
+ def check_version
+ end
- def column_name_for_operation(operation, node) # :nodoc:
- visitor.accept(node, collector).value
- end
-
- protected
-
- def initialize_type_map(m) # :nodoc:
- 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
- register_class_with_limit m, %r(text)i, Type::Text
- register_class_with_precision m, %r(date)i, Type::Date
- register_class_with_precision m, %r(time)i, Type::Time
- register_class_with_precision m, %r(datetime)i, Type::DateTime
- register_class_with_limit m, %r(float)i, Type::Float
- register_class_with_limit m, %r(int)i, Type::Integer
-
- m.alias_type %r(blob)i, 'binary'
- m.alias_type %r(clob)i, 'text'
- m.alias_type %r(timestamp)i, 'datetime'
- m.alias_type %r(numeric)i, 'decimal'
- m.alias_type %r(number)i, 'decimal'
- m.alias_type %r(double)i, 'float'
-
- m.register_type(%r(decimal)i) do |sql_type|
- scale = extract_scale(sql_type)
- precision = extract_precision(sql_type)
-
- if scale == 0
- # FIXME: Remove this class as well
- Type::DecimalWithoutScale.new(precision: precision)
- else
- Type::Decimal.new(precision: precision, scale: scale)
+ def type_map
+ @type_map ||= Type::TypeMap.new.tap do |mapping|
+ initialize_type_map(mapping)
end
end
- end
- def reload_type_map # :nodoc:
- type_map.clear
- initialize_type_map(type_map)
- end
+ def initialize_type_map(m = type_map)
+ 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
+ register_class_with_limit m, %r(text)i, Type::Text
+ register_class_with_precision m, %r(date)i, Type::Date
+ register_class_with_precision m, %r(time)i, Type::Time
+ register_class_with_precision m, %r(datetime)i, Type::DateTime
+ register_class_with_limit m, %r(float)i, Type::Float
+ register_class_with_limit m, %r(int)i, Type::Integer
+
+ m.alias_type %r(blob)i, "binary"
+ m.alias_type %r(clob)i, "text"
+ m.alias_type %r(timestamp)i, "datetime"
+ m.alias_type %r(numeric)i, "decimal"
+ m.alias_type %r(number)i, "decimal"
+ m.alias_type %r(double)i, "float"
+
+ m.register_type %r(^json)i, Type::Json.new
+
+ m.register_type(%r(decimal)i) do |sql_type|
+ scale = extract_scale(sql_type)
+ precision = extract_precision(sql_type)
+
+ if scale == 0
+ # FIXME: Remove this class as well
+ Type::DecimalWithoutScale.new(precision: precision)
+ else
+ Type::Decimal.new(precision: precision, scale: scale)
+ end
+ end
+ end
- def register_class_with_limit(mapping, key, klass) # :nodoc:
- mapping.register_type(key) do |*args|
- limit = extract_limit(args.last)
- klass.new(limit: limit)
+ def reload_type_map
+ type_map.clear
+ initialize_type_map
end
- end
- def register_class_with_precision(mapping, key, klass) # :nodoc:
- mapping.register_type(key) do |*args|
- precision = extract_precision(args.last)
- klass.new(precision: precision)
+ 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
- end
- def extract_scale(sql_type) # :nodoc:
- case sql_type
+ 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)
+ case sql_type
when /\((\d+)\)/ then 0
when /\((\d+)(,(\d+))\)/ then $3.to_i
+ end
end
- end
- def extract_precision(sql_type) # :nodoc:
- $1.to_i if sql_type =~ /\((\d+)(,\d+)?\)/
- end
+ def extract_precision(sql_type)
+ $1.to_i if sql_type =~ /\((\d+)(,\d+)?\)/
+ end
- def extract_limit(sql_type) # :nodoc:
- case sql_type
- when /^bigint/i
- 8
- when /\((.*)\)/
- $1.to_i
+ def extract_limit(sql_type)
+ $1.to_i if sql_type =~ /\((.*)\)/
end
- end
- def translate_exception_class(e, sql)
- begin
- message = "#{e.class.name}: #{e.message}: #{sql}"
- rescue Encoding::CompatibilityError
- message = "#{e.class.name}: #{e.message.force_encoding sql.encoding}: #{sql}"
+ def translate_exception_class(e, sql)
+ begin
+ message = "#{e.class.name}: #{e.message}: #{sql}"
+ rescue Encoding::CompatibilityError
+ message = "#{e.class.name}: #{e.message.force_encoding sql.encoding}: #{sql}"
+ end
+
+ exception = translate_exception(e, message)
+ exception.set_backtrace e.backtrace
+ exception
end
- exception = translate_exception(e, message)
- exception.set_backtrace e.backtrace
- exception
- end
+ def log(sql, name = "SQL", binds = [], type_casted_binds = [], statement_name = nil) # :doc:
+ @instrumenter.instrument(
+ "sql.active_record",
+ sql: sql,
+ name: name,
+ binds: binds,
+ type_casted_binds: type_casted_binds,
+ statement_name: statement_name,
+ connection_id: object_id) do
+ begin
+ @lock.synchronize do
+ yield
+ end
+ rescue => e
+ raise translate_exception_class(e, sql)
+ end
+ end
+ end
- def log(sql, name = "SQL", binds = [], statement_name = nil)
- @instrumenter.instrument(
- "sql.active_record",
- :sql => sql,
- :name => name,
- :connection_id => object_id,
- :statement_name => statement_name,
- :binds => binds) { yield }
- rescue => e
- raise translate_exception_class(e, sql)
- end
+ def translate_exception(exception, message)
+ # override in derived class
+ case exception
+ when RuntimeError
+ exception
+ else
+ ActiveRecord::StatementInvalid.new(message)
+ end
+ end
- def translate_exception(exception, message)
- # override in derived class
- ActiveRecord::StatementInvalid.new(message, exception)
- end
+ def without_prepared_statement?(binds)
+ !prepared_statements || binds.empty?
+ end
- def without_prepared_statement?(binds)
- !prepared_statements || binds.empty?
- end
+ 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}")
+ end
- def column_for(table_name, column_name) # :nodoc:
- column_name = column_name.to_s
- columns(table_name).detect { |c| c.name == column_name } ||
- raise(ActiveRecordError, "No such column: #{table_name}.#{column_name}")
- end
+ def collector
+ if prepared_statements
+ Arel::Collectors::Composite.new(
+ Arel::Collectors::SQLString.new,
+ Arel::Collectors::Bind.new,
+ )
+ else
+ Arel::Collectors::SubstituteBinds.new(
+ self,
+ Arel::Collectors::SQLString.new,
+ )
+ end
+ end
+
+ def arel_visitor
+ Arel::Visitors::ToSql.new(self)
+ end
end
end
end
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 2027492f29..13c799b64a 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -1,329 +1,135 @@
-require 'active_support/core_ext/string/strip'
+# frozen_string_literal: true
+
+require "active_record/connection_adapters/abstract_adapter"
+require "active_record/connection_adapters/statement_pool"
+require "active_record/connection_adapters/mysql/column"
+require "active_record/connection_adapters/mysql/explain_pretty_printer"
+require "active_record/connection_adapters/mysql/quoting"
+require "active_record/connection_adapters/mysql/schema_creation"
+require "active_record/connection_adapters/mysql/schema_definitions"
+require "active_record/connection_adapters/mysql/schema_dumper"
+require "active_record/connection_adapters/mysql/schema_statements"
+require "active_record/connection_adapters/mysql/type_metadata"
module ActiveRecord
module ConnectionAdapters
class AbstractMysqlAdapter < AbstractAdapter
- include Savepoints
+ include MySQL::Quoting
+ include MySQL::SchemaStatements
- module ColumnMethods
- def primary_key(name, type = :primary_key, **options)
- options[:auto_increment] = true if type == :bigint
- super
- end
- end
-
- class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition
- attr_accessor :charset
- end
-
- class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
- include ColumnMethods
-
- def new_column_definition(name, type, options) # :nodoc:
- column = super
- case column.type
- when :primary_key
- column.type = :integer
- column.auto_increment = true
- end
- column.charset = options[:charset]
- column
- end
-
- private
-
- def create_column_definition(name, type)
- ColumnDefinition.new(name, type)
- end
- end
+ ##
+ # :singleton-method:
+ # By default, the Mysql2Adapter will consider all columns of type <tt>tinyint(1)</tt>
+ # as boolean. If you wish to disable this emulation you can add the following line
+ # to your application.rb file:
+ #
+ # ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans = false
+ class_attribute :emulate_booleans, default: true
- class Table < ActiveRecord::ConnectionAdapters::Table
- include ColumnMethods
- end
+ NATIVE_DATABASE_TYPES = {
+ 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", limit: 24 },
+ decimal: { name: "decimal" },
+ datetime: { name: "datetime" },
+ timestamp: { name: "timestamp" },
+ time: { name: "time" },
+ date: { name: "date" },
+ binary: { name: "blob", limit: 65535 },
+ boolean: { name: "tinyint", limit: 1 },
+ json: { name: "json" },
+ }
- class SchemaCreation < AbstractAdapter::SchemaCreation
+ class StatementPool < ConnectionAdapters::StatementPool # :nodoc:
private
- def visit_DropForeignKey(name)
- "DROP FOREIGN KEY #{name}"
- end
-
- def visit_TableDefinition(o)
- name = o.name
- create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(name)} "
-
- statements = o.columns.map { |c| accept c }
- statements.concat(o.indexes.map { |column_name, options| index_in_create(name, column_name, options) })
-
- create_sql << "(#{statements.join(', ')}) " if statements.present?
- create_sql << "#{o.options}"
- create_sql << " AS #{@conn.to_sql(o.as)}" if o.as
- create_sql
- end
-
- def visit_AddColumnDefinition(o)
- add_column_position!(super, column_options(o.column))
- end
-
- def visit_ChangeColumnDefinition(o)
- change_column_sql = "CHANGE #{quote_column_name(o.name)} #{accept(o.column)}"
- add_column_position!(change_column_sql, column_options(o.column))
- end
-
- def column_options(o)
- column_options = super
- column_options[:charset] = o.charset
- column_options
- end
-
- def add_column_options!(sql, options)
- if options[:charset]
- sql << " CHARACTER SET #{options[:charset]}"
- end
- if options[:collation]
- sql << " COLLATE #{options[:collation]}"
- end
- super
- end
-
- def add_column_position!(sql, options)
- if options[:first]
- sql << " FIRST"
- elsif options[:after]
- sql << " AFTER #{quote_column_name(options[:after])}"
+ def dealloc(stmt)
+ stmt.close
end
- sql
- end
-
- def index_in_create(table_name, column_name, options)
- index_name, index_type, index_columns, _, _, index_using = @conn.add_index_options(table_name, column_name, options)
- "#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns}) "
- end
end
- def update_table_definition(table_name, base) # :nodoc:
- Table.new(table_name, base)
- end
+ def initialize(connection, logger, connection_options, config)
+ super(connection, logger, config)
- def schema_creation
- SchemaCreation.new self
+ @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit]))
end
- def column_spec_for_primary_key(column)
- spec = {}
- if column.auto_increment?
- spec[:id] = ':bigint' if column.bigint?
- return if spec.empty?
- else
- spec[:id] = column.type.inspect
- spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type, :null].include?(key) })
- end
- spec
+ def version #:nodoc:
+ @version ||= Version.new(version_string)
end
- private
-
- def schema_limit(column)
- super unless column.type == :boolean
+ def mariadb? # :nodoc:
+ /mariadb/i.match?(full_version)
end
- def schema_precision(column)
- super unless /time/ === column.sql_type && column.precision == 0
- end
-
- def schema_collation(column)
- if column.collation && table_name = column.instance_variable_get(:@table_name)
- @collation_cache ||= {}
- @collation_cache[table_name] ||= select_one("SHOW TABLE STATUS LIKE '#{table_name}'")["Collation"]
- column.collation.inspect if column.collation != @collation_cache[table_name]
- end
+ def supports_bulk_alter? #:nodoc:
+ true
end
- public
-
- class Column < ConnectionAdapters::Column # :nodoc:
- delegate :strict, :extra, to: :sql_type_metadata, allow_nil: true
-
- def initialize(*)
- super
- assert_valid_default(default)
- extract_default
- end
-
- def extract_default
- if blob_or_text_column?
- @default = null || strict ? nil : ''
- elsif missing_default_forged_as_empty_string?(default)
- @default = nil
- end
- end
-
- def has_default?
- return false if blob_or_text_column? # MySQL forbids defaults on blob and text columns
- super
- end
-
- def blob_or_text_column?
- sql_type =~ /blob/i || type == :text
- end
-
- def case_sensitive?
- collation && !collation.match(/_ci$/)
- end
-
- def auto_increment?
- extra == 'auto_increment'
- end
-
- private
-
- # MySQL misreports NOT NULL column default when none is given.
- # We can't detect this for columns which may have a legitimate ''
- # default (string) but we can for others (integer, datetime, boolean,
- # and the rest).
- #
- # Test whether the column has default '', is not null, and is not
- # a type allowing default ''.
- def missing_default_forged_as_empty_string?(default)
- type != :string && !null && default == ''
- end
-
- def assert_valid_default(default)
- if blob_or_text_column? && default.present?
- raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
- end
- end
+ def supports_index_sort_order?
+ !mariadb? && version >= "8.0.1"
end
- class MysqlTypeMetadata < DelegateClass(SqlTypeMetadata) # :nodoc:
- attr_reader :extra, :strict
-
- def initialize(type_metadata, extra: "", strict: false)
- super(type_metadata)
- @type_metadata = type_metadata
- @extra = extra
- @strict = strict
- end
-
- def ==(other)
- other.is_a?(MysqlTypeMetadata) &&
- attributes_for_hash == other.attributes_for_hash
- end
- alias eql? ==
-
- def hash
- attributes_for_hash.hash
- end
-
- protected
-
- def attributes_for_hash
- [self.class, @type_metadata, extra, strict]
- end
+ def supports_expression_index?
+ !mariadb? && version >= "8.0.13"
end
- ##
- # :singleton-method:
- # By default, the MysqlAdapter will consider all columns of type <tt>tinyint(1)</tt>
- # as boolean. If you wish to disable this emulation (which was the default
- # behavior in versions 0.13.1 and earlier) you can add the following line
- # to your application.rb file:
- #
- # ActiveRecord::ConnectionAdapters::Mysql[2]Adapter.emulate_booleans = false
- class_attribute :emulate_booleans
- self.emulate_booleans = true
-
- LOST_CONNECTION_ERROR_MESSAGES = [
- "Server shutdown in progress",
- "Broken pipe",
- "Lost connection to MySQL server during query",
- "MySQL server has gone away" ]
-
- QUOTED_TRUE, QUOTED_FALSE = '1', '0'
-
- NATIVE_DATABASE_TYPES = {
- :primary_key => "int(11) auto_increment PRIMARY KEY",
- :string => { :name => "varchar", :limit => 255 },
- :text => { :name => "text" },
- :integer => { :name => "int", :limit => 4 },
- :float => { :name => "float" },
- :decimal => { :name => "decimal" },
- :datetime => { :name => "datetime" },
- :time => { :name => "time" },
- :date => { :name => "date" },
- :binary => { :name => "blob" },
- :boolean => { :name => "tinyint", :limit => 1 }
- }
-
- INDEX_TYPES = [:fulltext, :spatial]
- INDEX_USINGS = [:btree, :hash]
-
- # FIXME: Make the first parameter more similar for the two adapters
- def initialize(connection, logger, connection_options, config)
- super(connection, logger)
- @connection_options, @config = connection_options, config
- @quoted_column_names, @quoted_table_names = {}, {}
-
- @visitor = Arel::Visitors::MySQL.new self
-
- if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
- @prepared_statements = true
- else
- @prepared_statements = false
- end
+ def supports_transaction_isolation?
+ true
end
- MAX_INDEX_LENGTH_FOR_CHARSETS_OF_4BYTES_MAXLEN = 191
- CHARSETS_OF_4BYTES_MAXLEN = ['utf8mb4', 'utf16', 'utf16le', 'utf32']
- def initialize_schema_migrations_table
- if CHARSETS_OF_4BYTES_MAXLEN.include?(charset)
- ActiveRecord::SchemaMigration.create_table(MAX_INDEX_LENGTH_FOR_CHARSETS_OF_4BYTES_MAXLEN)
- else
- ActiveRecord::SchemaMigration.create_table
- end
+ def supports_explain?
+ true
end
- # Returns true, since this connection adapter supports migrations.
- def supports_migrations?
+ def supports_indexes_in_create?
true
end
- def supports_primary_key?
+ def supports_foreign_keys?
true
end
- def supports_bulk_alter? #:nodoc:
+ def supports_views?
true
end
- # Technically MySQL allows to create indexes with the sort order syntax
- # but at the moment (5.5) it doesn't yet implement them
- def supports_index_sort_order?
- true
+ def supports_datetime_with_precision?
+ if mariadb?
+ version >= "5.3.0"
+ else
+ version >= "5.6.4"
+ end
end
- # MySQL 4 technically support transaction isolation, but it is affected by a bug
- # where the transaction level gets persisted for the whole session:
- #
- # http://bugs.mysql.com/bug.php?id=39170
- def supports_transaction_isolation?
- version[0] >= 5
+ def supports_virtual_columns?
+ if mariadb?
+ version >= "5.2.0"
+ else
+ version >= "5.7.5"
+ end
end
- def supports_indexes_in_create?
+ def supports_advisory_locks?
true
end
- def supports_foreign_keys?
- true
+ def supports_longer_index_key_prefix?
+ if mariadb?
+ version >= "10.2.2"
+ else
+ version >= "5.7.9"
+ end
end
- def supports_views?
- version[0] >= 5
+ def get_advisory_lock(lock_name, timeout = 0) # :nodoc:
+ query_value("SELECT GET_LOCK(#{quote(lock_name.to_s)}, #{timeout})") == 1
end
- def supports_datetime_with_precision?
- (version[0] == 5 && version[1] >= 6) || version[0] >= 6
+ def release_advisory_lock(lock_name) # :nodoc:
+ query_value("SELECT RELEASE_LOCK(#{quote(lock_name.to_s)})") == 1
end
def native_database_types
@@ -331,7 +137,7 @@ module ActiveRecord
end
def index_algorithms
- { default: 'ALGORITHM = DEFAULT', copy: 'ALGORITHM = COPY', inplace: 'ALGORITHM = INPLACE' }
+ { default: +"ALGORITHM = DEFAULT", copy: +"ALGORITHM = COPY", inplace: +"ALGORITHM = INPLACE" }
end
# HELPER METHODS ===========================================
@@ -342,54 +148,16 @@ module ActiveRecord
raise NotImplementedError
end
- def new_column(field, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) # :nodoc:
- Column.new(field, default, sql_type_metadata, null, default_function, collation)
- end
-
# Must return the MySQL error number from the exception, if the exception has an
# error number.
def error_number(exception) # :nodoc:
raise NotImplementedError
end
- # QUOTING ==================================================
-
- def _quote(value) # :nodoc:
- if value.is_a?(Type::Binary::Data)
- "x'#{value.hex}'"
- else
- super
- end
- end
-
- def quote_column_name(name) #:nodoc:
- @quoted_column_names[name] ||= "`#{name.to_s.gsub('`', '``')}`"
- end
-
- def quote_table_name(name) #:nodoc:
- @quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`')
- end
-
- def quoted_true
- QUOTED_TRUE
- end
-
- def unquoted_true
- 1
- end
-
- def quoted_false
- QUOTED_FALSE
- end
-
- def unquoted_false
- 0
- end
-
# REFERENTIAL INTEGRITY ====================================
def disable_referential_integrity #:nodoc:
- old = select_value("SELECT @@FOREIGN_KEY_CHECKS")
+ old = query_value("SELECT @@FOREIGN_KEY_CHECKS")
begin
update("SET FOREIGN_KEY_CHECKS = 0")
@@ -399,30 +167,43 @@ module ActiveRecord
end
end
+ # CONNECTION MANAGEMENT ====================================
+
+ # Clears the prepared statements cache.
+ def clear_cache!
+ reload_type_map
+ @statements.clear
+ end
+
#--
# DATABASE STATEMENTS ======================================
#++
- def clear_cache!
- super
- reload_type_map
+ def explain(arel, binds = [])
+ sql = "EXPLAIN #{to_sql(arel, binds)}"
+ start = Time.now
+ result = exec_query(sql, "EXPLAIN", binds)
+ elapsed = Time.now - start
+
+ MySQL::ExplainPrettyPrinter.new.pp(result, elapsed)
end
# Executes the SQL statement in the context of this connection.
def execute(sql, name = nil)
- log(sql, name) { @connection.query(sql) }
- end
+ materialize_transactions
- # MysqlAdapter has to free a result after using it, so we use this method to write
- # stuff in an abstract way without concerning ourselves about whether it needs to be
- # explicitly freed or not.
- def execute_and_free(sql, name = nil) #:nodoc:
- yield execute(sql, name)
+ log(sql, name) do
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
+ @connection.query(sql)
+ end
+ end
end
- def update_sql(sql, name = nil) #:nodoc:
- super
- @connection.affected_rows
+ # Mysql2Adapter doesn't have to free a result after using it, but we use this method
+ # to write stuff in an abstract way without concerning ourselves about whether it
+ # needs to be explicitly freed or not.
+ def execute_and_free(sql, name = nil) # :nodoc:
+ yield execute(sql, name)
end
def begin_db_transaction
@@ -442,19 +223,7 @@ module ActiveRecord
execute "ROLLBACK"
end
- # In the simple case, MySQL allows us to place JOINs directly into the UPDATE
- # query. However, this does not allow for LIMIT, OFFSET and ORDER. To support
- # these, we must use a subquery.
- def join_to_update(update, select) #:nodoc:
- if select.limit || select.offset || select.orders.any?
- super
- else
- update.table select.source
- update.wheres = select.constraints
- end
- end
-
- def empty_insert_statement_value
+ def empty_insert_statement_value(primary_key = nil)
"VALUES ()"
end
@@ -470,7 +239,7 @@ module ActiveRecord
end
# Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>.
- # Charset defaults to utf8.
+ # Charset defaults to utf8mb4.
#
# Example:
# create_database 'charset_test', charset: 'latin1', collation: 'latin1_bin'
@@ -478,9 +247,13 @@ module ActiveRecord
# create_database 'matt_development', charset: :big5
def create_database(name, options = {})
if options[:collation]
- execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`"
+ execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT COLLATE #{quote_table_name(options[:collation])}"
+ elsif options[:charset]
+ execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset])}"
+ elsif supports_longer_index_key_prefix?
+ execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET `utf8mb4`"
else
- execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}`"
+ raise "Configure a supported :charset and ensure innodb_large_prefix is enabled to support indexes on varchar(255) string columns."
end
end
@@ -489,97 +262,42 @@ module ActiveRecord
# Example:
# drop_database('sebastian_development')
def drop_database(name) #:nodoc:
- execute "DROP DATABASE IF EXISTS `#{name}`"
+ execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}"
end
def current_database
- select_value 'SELECT DATABASE() as db'
+ query_value("SELECT database()", "SCHEMA")
end
# Returns the database character set.
def charset
- show_variable 'character_set_database'
+ show_variable "character_set_database"
end
# Returns the database collation strategy.
def collation
- show_variable 'collation_database'
- end
-
- def tables(name = nil, database = nil, like = nil) #:nodoc:
- sql = "SHOW TABLES "
- sql << "IN #{quote_table_name(database)} " if database
- sql << "LIKE #{quote(like)}" if like
-
- execute_and_free(sql, 'SCHEMA') do |result|
- result.collect(&:first)
- end
+ show_variable "collation_database"
end
def truncate(table_name, name = nil)
execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name
end
- def table_exists?(name)
- return false unless name.present?
- return true if tables(nil, nil, name).any?
-
- name = name.to_s
- schema, table = name.split('.', 2)
-
- unless table # A table was provided without a schema
- table = schema
- schema = nil
- end
-
- tables(nil, schema, table).any?
- end
-
- # Returns an array of indexes for the given table.
- def indexes(table_name, name = nil) #:nodoc:
- indexes = []
- current_index = nil
- execute_and_free("SHOW KEYS FROM #{quote_table_name(table_name)}", 'SCHEMA') do |result|
- each_hash(result) do |row|
- if current_index != row[:Key_name]
- next if row[:Key_name] == 'PRIMARY' # skip the primary key
- current_index = row[:Key_name]
-
- mysql_index_type = row[:Index_type].downcase.to_sym
- index_type = INDEX_TYPES.include?(mysql_index_type) ? mysql_index_type : nil
- index_using = INDEX_USINGS.include?(mysql_index_type) ? mysql_index_type : nil
- indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, [], [], nil, nil, index_type, index_using)
- end
+ def table_comment(table_name) # :nodoc:
+ scope = quoted_scope(table_name)
- indexes.last.columns << row[:Column_name]
- indexes.last.lengths << row[:Sub_part]
- end
- end
-
- indexes
- end
-
- # Returns an array of +Column+ objects for the table specified by +table_name+.
- def columns(table_name)#:nodoc:
- sql = "SHOW FULL FIELDS FROM #{quote_table_name(table_name)}"
- execute_and_free(sql, 'SCHEMA') do |result|
- each_hash(result).map do |field|
- field_name = set_field_encoding(field[:Field])
- sql_type = field[:Type]
- type_metadata = fetch_type_metadata(sql_type, field[:Extra])
- new_column(field_name, field[:Default], type_metadata, field[:Null] == "YES", nil, field[:Collation])
- end
- end
- end
-
- def create_table(table_name, options = {}) #:nodoc:
- super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB"))
+ query_value(<<~SQL, "SCHEMA").presence
+ SELECT table_comment
+ FROM information_schema.tables
+ WHERE table_schema = #{scope[:schema]}
+ AND table_name = #{scope[:name]}
+ SQL
end
def bulk_change_table(table_name, operations) #:nodoc:
sqls = operations.flat_map do |command, args|
table, arguments = args.shift, args
- method = :"#{command}_sql"
+ method = :"#{command}_for_alter"
if respond_to?(method, true)
send(method, table, *arguments)
@@ -591,6 +309,11 @@ module ActiveRecord
execute("ALTER TABLE #{quote_table_name(table_name)} #{sqls}")
end
+ def change_table_comment(table_name, comment) #:nodoc:
+ comment = "" if comment.nil?
+ execute("ALTER TABLE #{quote_table_name(table_name)} COMMENT #{quote(comment)}")
+ end
+
# Renames a table.
#
# Example:
@@ -631,432 +354,521 @@ module ActiveRecord
def change_column_default(table_name, column_name, default_or_changes) #:nodoc:
default = extract_new_default_value(default_or_changes)
- column = column_for(table_name, column_name)
- change_column table_name, column_name, column.sql_type, :default => default
+ change_column table_name, column_name, nil, default: default
end
def change_column_null(table_name, column_name, null, default = nil) #:nodoc:
- column = column_for(table_name, column_name)
-
unless null || default.nil?
execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
end
- change_column table_name, column_name, column.sql_type, :null => null
+ change_column table_name, column_name, nil, null: null
+ end
+
+ def change_column_comment(table_name, column_name, comment) #:nodoc:
+ change_column table_name, column_name, nil, comment: comment
end
def change_column(table_name, column_name, type, options = {}) #:nodoc:
- execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_sql(table_name, column_name, type, options)}")
+ execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_for_alter(table_name, column_name, type, options)}")
end
def rename_column(table_name, column_name, new_column_name) #:nodoc:
- execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_sql(table_name, column_name, new_column_name)}")
+ execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_for_alter(table_name, column_name, new_column_name)}")
rename_column_indexes(table_name, column_name, new_column_name)
end
def add_index(table_name, column_name, options = {}) #:nodoc:
- index_name, index_type, index_columns, _, index_algorithm, index_using = add_index_options(table_name, column_name, options)
- execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns}) #{index_algorithm}"
+ index_name, index_type, index_columns, _, index_algorithm, index_using, comment = add_index_options(table_name, column_name, options)
+ sql = +"CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns}) #{index_algorithm}"
+ execute add_sql_comment!(sql, comment)
+ end
+
+ def add_sql_comment!(sql, comment) # :nodoc:
+ sql << " COMMENT #{quote(comment)}" if comment.present?
+ sql
end
def foreign_keys(table_name)
- fk_info = select_all <<-SQL.strip_heredoc
- SELECT fk.referenced_table_name as 'to_table'
- ,fk.referenced_column_name as 'primary_key'
- ,fk.column_name as 'column'
- ,fk.constraint_name as 'name'
- FROM information_schema.key_column_usage fk
- WHERE fk.referenced_column_name is not null
- AND fk.table_schema = '#{@config[:database]}'
- AND fk.table_name = '#{table_name}'
+ raise ArgumentError unless table_name.present?
+
+ scope = quoted_scope(table_name)
+
+ fk_info = exec_query(<<~SQL, "SCHEMA")
+ SELECT fk.referenced_table_name AS 'to_table',
+ fk.referenced_column_name AS 'primary_key',
+ fk.column_name AS 'column',
+ fk.constraint_name AS 'name',
+ rc.update_rule AS 'on_update',
+ rc.delete_rule AS 'on_delete'
+ FROM information_schema.referential_constraints rc
+ JOIN information_schema.key_column_usage fk
+ USING (constraint_schema, constraint_name)
+ WHERE fk.referenced_column_name IS NOT NULL
+ AND fk.table_schema = #{scope[:schema]}
+ AND fk.table_name = #{scope[:name]}
+ AND rc.constraint_schema = #{scope[:schema]}
+ AND rc.table_name = #{scope[:name]}
SQL
- create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"]
-
fk_info.map do |row|
options = {
- column: row['column'],
- name: row['name'],
- primary_key: row['primary_key']
+ column: row["column"],
+ name: row["name"],
+ primary_key: row["primary_key"]
}
- options[:on_update] = extract_foreign_key_action(create_table_info, row['name'], "UPDATE")
- options[:on_delete] = extract_foreign_key_action(create_table_info, row['name'], "DELETE")
+ options[:on_update] = extract_foreign_key_action(row["on_update"])
+ options[:on_delete] = extract_foreign_key_action(row["on_delete"])
- ForeignKeyDefinition.new(table_name, row['to_table'], options)
+ ForeignKeyDefinition.new(table_name, row["to_table"], options)
end
end
- def table_options(table_name)
- create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"]
+ def table_options(table_name) # :nodoc:
+ table_options = {}
+
+ create_table_info = create_table_info(table_name)
# strip create_definitions and partition_options
- raw_table_options = create_table_info.sub(/\A.*\n\) /m, '').sub(/\n\/\*!.*\*\/\n\z/m, '').strip
+ raw_table_options = create_table_info.sub(/\A.*\n\) /m, "").sub(/\n\/\*!.*\*\/\n\z/m, "").strip
# strip AUTO_INCREMENT
- raw_table_options.sub(/(ENGINE=\w+)(?: AUTO_INCREMENT=\d+)/, '\1')
- end
+ raw_table_options.sub!(/(ENGINE=\w+)(?: AUTO_INCREMENT=\d+)/, '\1')
- # Maps logical Rails types to MySQL-specific data types.
- def type_to_sql(type, limit = nil, precision = nil, scale = nil)
- case type.to_s
- when 'binary'
- binary_to_sql(limit)
- when 'integer'
- integer_to_sql(limit)
- when 'text'
- text_to_sql(limit)
- else
- super
+ table_options[:options] = raw_table_options
+
+ # strip COMMENT
+ if raw_table_options.sub!(/ COMMENT='.+'/, "")
+ table_options[:comment] = table_comment(table_name)
end
- end
- # SHOW VARIABLES LIKE 'name'
- def show_variable(name)
- variables = select_all("SHOW VARIABLES LIKE '#{name}'", 'SCHEMA')
- variables.first['Value'] unless variables.empty?
+ table_options
end
- # Returns a table's primary key and belonging sequence.
- def pk_and_sequence_for(table)
- execute_and_free("SHOW CREATE TABLE #{quote_table_name(table)}", 'SCHEMA') do |result|
- create_table = each_hash(result).first[:"Create Table"]
- if create_table.to_s =~ /PRIMARY KEY\s+(?:USING\s+\w+\s+)?\((.+)\)/
- keys = $1.split(",").map { |key| key.delete('`"') }
- keys.length == 1 ? [keys.first, nil] : nil
+ # Maps logical Rails types to MySQL-specific data types.
+ def type_to_sql(type, limit: nil, precision: nil, scale: nil, unsigned: nil, **) # :nodoc:
+ sql = \
+ case type.to_s
+ when "integer"
+ integer_to_sql(limit)
+ when "text"
+ text_to_sql(limit)
+ when "blob"
+ binary_to_sql(limit)
+ when "binary"
+ if (0..0xfff) === limit
+ "varbinary(#{limit})"
+ else
+ binary_to_sql(limit)
+ end
else
- nil
+ super
end
- end
+
+ sql = "#{sql} unsigned" if unsigned && type != :primary_key
+ sql
end
- # Returns just a table's primary key
- def primary_key(table)
- pk_and_sequence = pk_and_sequence_for(table)
- pk_and_sequence && pk_and_sequence.first
+ # SHOW VARIABLES LIKE 'name'
+ def show_variable(name)
+ query_value("SELECT @@#{name}", "SCHEMA")
+ rescue ActiveRecord::StatementInvalid
+ nil
end
- def case_sensitive_modifier(node, table_attribute)
- node = Arel::Nodes.build_quoted node, table_attribute
- Arel::Nodes::Bin.new(node)
+ def primary_keys(table_name) # :nodoc:
+ raise ArgumentError unless table_name.present?
+
+ scope = quoted_scope(table_name)
+
+ query_values(<<~SQL, "SCHEMA")
+ SELECT column_name
+ FROM information_schema.key_column_usage
+ WHERE constraint_name = 'PRIMARY'
+ AND table_schema = #{scope[:schema]}
+ AND table_name = #{scope[:name]}
+ ORDER BY ordinal_position
+ SQL
end
- def case_sensitive_comparison(table, attribute, column, value)
- if column.case_sensitive?
- table[attribute].eq(value)
+ def case_sensitive_comparison(table, attribute, column, value) # :nodoc:
+ if column.collation && !column.case_sensitive?
+ table[attribute].eq(Arel::Nodes::Bin.new(value))
else
super
end
end
- def case_insensitive_comparison(table, attribute, column, value)
- if column.case_sensitive?
- super
- else
- table[attribute].eq(value)
- end
+ def can_perform_case_insensitive_comparison_for?(column)
+ column.case_sensitive?
+ end
+ private :can_perform_case_insensitive_comparison_for?
+
+ # In MySQL 5.7.5 and up, ONLY_FULL_GROUP_BY affects handling of queries that use
+ # DISTINCT and ORDER BY. It requires the ORDER BY columns in the select list for
+ # distinct queries, and requires that the ORDER BY include the distinct column.
+ # See https://dev.mysql.com/doc/refman/5.7/en/group-by-handling.html
+ def columns_for_distinct(columns, orders) # :nodoc:
+ order_columns = orders.reject(&:blank?).map { |s|
+ # Convert Arel node to string
+ s = s.to_sql unless s.is_a?(String)
+ # Remove any ASC/DESC modifiers
+ s.gsub(/\s+(?:ASC|DESC)\b/i, "")
+ }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }
+
+ (order_columns << super).join(", ")
end
def strict_mode?
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
-
- def initialize_type_map(m) # :nodoc:
- super
-
- register_class_with_limit m, %r(char)i, MysqlString
-
- m.register_type %r(tinytext)i, Type::Text.new(limit: 2**8 - 1)
- m.register_type %r(tinyblob)i, Type::Binary.new(limit: 2**8 - 1)
- m.register_type %r(text)i, Type::Text.new(limit: 2**16 - 1)
- m.register_type %r(blob)i, Type::Binary.new(limit: 2**16 - 1)
- m.register_type %r(mediumtext)i, Type::Text.new(limit: 2**24 - 1)
- m.register_type %r(mediumblob)i, Type::Binary.new(limit: 2**24 - 1)
- m.register_type %r(longtext)i, Type::Text.new(limit: 2**32 - 1)
- m.register_type %r(longblob)i, Type::Binary.new(limit: 2**32 - 1)
- m.register_type %r(^float)i, Type::Float.new(limit: 24)
- m.register_type %r(^double)i, Type::Float.new(limit: 53)
-
- register_integer_type m, %r(^bigint)i, limit: 8
- register_integer_type m, %r(^int)i, limit: 4
- register_integer_type m, %r(^mediumint)i, limit: 3
- register_integer_type m, %r(^smallint)i, limit: 2
- register_integer_type m, %r(^tinyint)i, limit: 1
-
- m.alias_type %r(tinyint\(1\))i, 'boolean' if emulate_booleans
- m.alias_type %r(set)i, 'varchar'
- m.alias_type %r(year)i, 'integer'
- m.alias_type %r(bit)i, 'binary'
-
- m.register_type(%r(enum)i) do |sql_type|
- limit = sql_type[/^enum\((.+)\)/i, 1]
- .split(',').map{|enum| enum.strip.length - 2}.max
- MysqlString.new(limit: limit)
+ def insert_fixtures_set(fixture_set, tables_to_delete = [])
+ with_multi_statements do
+ super { discard_remaining_results }
end
end
- def register_integer_type(mapping, key, options) # :nodoc:
- mapping.register_type(key) do |sql_type|
- if /unsigned/i =~ sql_type
- Type::UnsignedInteger.new(options)
- else
- Type::Integer.new(options)
+ private
+ def check_version
+ if version < "5.5.8"
+ raise "Your version of MySQL (#{version_string}) is too old. Active Record supports MySQL >= 5.5.8."
end
end
- end
- def extract_precision(sql_type)
- if /time/ === sql_type
- super || 0
- else
- super
+ def combine_multi_statements(total_sql)
+ total_sql.each_with_object([]) do |sql, total_sql_chunks|
+ previous_packet = total_sql_chunks.last
+ sql << ";\n"
+ if max_allowed_packet_reached?(sql, previous_packet) || total_sql_chunks.empty?
+ total_sql_chunks << sql
+ else
+ previous_packet << sql
+ end
+ end
end
- end
- def fetch_type_metadata(sql_type, extra = "")
- MysqlTypeMetadata.new(super(sql_type), extra: extra, strict: strict_mode?)
- end
-
- def add_index_length(option_strings, column_names, options = {})
- if options.is_a?(Hash) && length = options[:length]
- case length
- when Hash
- column_names.each {|name| option_strings[name] += "(#{length[name]})" if length.has_key?(name) && length[name].present?}
- when Fixnum
- column_names.each {|name| option_strings[name] += "(#{length})"}
+ def max_allowed_packet_reached?(current_packet, previous_packet)
+ if current_packet.bytesize > max_allowed_packet
+ raise ActiveRecordError, "Fixtures set is too large #{current_packet.bytesize}. Consider increasing the max_allowed_packet variable."
+ elsif previous_packet.nil?
+ false
+ else
+ (current_packet.bytesize + previous_packet.bytesize) > max_allowed_packet
end
end
- return option_strings
- end
-
- def quoted_columns_for_index(column_names, options = {})
- option_strings = Hash[column_names.map {|name| [name, '']}]
-
- # add index length
- option_strings = add_index_length(option_strings, column_names, options)
+ def max_allowed_packet
+ bytes_margin = 2
+ @max_allowed_packet ||= (show_variable("max_allowed_packet") - bytes_margin)
+ end
- # add index sort order
- option_strings = add_index_sort_order(option_strings, column_names, options)
+ def initialize_type_map(m = type_map)
+ super
- column_names.map {|name| quote_column_name(name) + option_strings[name]}
- end
+ register_class_with_limit m, %r(char)i, MysqlString
+
+ m.register_type %r(tinytext)i, Type::Text.new(limit: 2**8 - 1)
+ m.register_type %r(tinyblob)i, Type::Binary.new(limit: 2**8 - 1)
+ m.register_type %r(text)i, Type::Text.new(limit: 2**16 - 1)
+ m.register_type %r(blob)i, Type::Binary.new(limit: 2**16 - 1)
+ m.register_type %r(mediumtext)i, Type::Text.new(limit: 2**24 - 1)
+ m.register_type %r(mediumblob)i, Type::Binary.new(limit: 2**24 - 1)
+ m.register_type %r(longtext)i, Type::Text.new(limit: 2**32 - 1)
+ m.register_type %r(longblob)i, Type::Binary.new(limit: 2**32 - 1)
+ m.register_type %r(^float)i, Type::Float.new(limit: 24)
+ m.register_type %r(^double)i, Type::Float.new(limit: 53)
+
+ register_integer_type m, %r(^bigint)i, limit: 8
+ register_integer_type m, %r(^int)i, limit: 4
+ register_integer_type m, %r(^mediumint)i, limit: 3
+ register_integer_type m, %r(^smallint)i, limit: 2
+ register_integer_type m, %r(^tinyint)i, limit: 1
+
+ m.register_type %r(^tinyint\(1\))i, Type::Boolean.new if emulate_booleans
+ m.alias_type %r(year)i, "integer"
+ m.alias_type %r(bit)i, "binary"
+
+ m.register_type(%r(enum)i) do |sql_type|
+ limit = sql_type[/^enum\((.+)\)/i, 1]
+ .split(",").map { |enum| enum.strip.length - 2 }.max
+ MysqlString.new(limit: limit)
+ end
- def translate_exception(exception, message)
- case error_number(exception)
- when 1062
- RecordNotUnique.new(message, exception)
- when 1452
- InvalidForeignKey.new(message, exception)
- else
- super
+ m.register_type(%r(^set)i) do |sql_type|
+ limit = sql_type[/^set\((.+)\)/i, 1]
+ .split(",").map { |set| set.strip.length - 1 }.sum - 1
+ MysqlString.new(limit: limit)
+ end
end
- end
-
- def add_column_sql(table_name, column_name, type, options = {})
- td = create_table_definition(table_name)
- cd = td.new_column_definition(column_name, type, options)
- schema_creation.accept(AddColumnDefinition.new(cd))
- end
- def change_column_sql(table_name, column_name, type, options = {})
- column = column_for(table_name, column_name)
+ def register_integer_type(mapping, key, options)
+ mapping.register_type(key) do |sql_type|
+ if /\bunsigned\b/.match?(sql_type)
+ Type::UnsignedInteger.new(options)
+ else
+ Type::Integer.new(options)
+ end
+ end
+ end
- unless options_include_default?(options)
- options[:default] = column.default
+ def extract_precision(sql_type)
+ if /\A(?:date)?time(?:stamp)?\b/.match?(sql_type)
+ super || 0
+ else
+ super
+ end
end
- unless options.has_key?(:null)
- options[:null] = column.null
+ # See https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html
+ ER_DUP_ENTRY = 1062
+ ER_NOT_NULL_VIOLATION = 1048
+ ER_NO_REFERENCED_ROW = 1216
+ ER_ROW_IS_REFERENCED = 1217
+ ER_DO_NOT_HAVE_DEFAULT = 1364
+ ER_ROW_IS_REFERENCED_2 = 1451
+ 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
+ ER_LOCK_WAIT_TIMEOUT = 1205
+ ER_QUERY_INTERRUPTED = 1317
+ ER_QUERY_TIMEOUT = 3024
+
+ def translate_exception(exception, message)
+ case error_number(exception)
+ when ER_DUP_ENTRY
+ RecordNotUnique.new(message)
+ when ER_NO_REFERENCED_ROW, ER_ROW_IS_REFERENCED, ER_ROW_IS_REFERENCED_2, 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)
+ when ER_LOCK_WAIT_TIMEOUT
+ LockWaitTimeout.new(message)
+ when ER_QUERY_TIMEOUT
+ StatementTimeout.new(message)
+ when ER_QUERY_INTERRUPTED
+ QueryCanceled.new(message)
+ else
+ super
+ end
end
- td = create_table_definition(table_name)
- cd = td.new_column_definition(column.name, type, options)
- schema_creation.accept(ChangeColumnDefinition.new(cd, column.name))
- end
+ def change_column_for_alter(table_name, column_name, type, options = {})
+ column = column_for(table_name, column_name)
+ type ||= column.sql_type
- def rename_column_sql(table_name, column_name, new_column_name)
- column = column_for(table_name, column_name)
- options = {
- default: column.default,
- null: column.null,
- auto_increment: column.auto_increment?
- }
-
- current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'", 'SCHEMA')["Type"]
- td = create_table_definition(table_name)
- cd = td.new_column_definition(new_column_name, current_type, options)
- schema_creation.accept(ChangeColumnDefinition.new(cd, column.name))
- end
+ unless options.key?(:default)
+ options[:default] = column.default
+ end
- def remove_column_sql(table_name, column_name, type = nil, options = {})
- "DROP #{quote_column_name(column_name)}"
- end
+ unless options.key?(:null)
+ options[:null] = column.null
+ end
- def remove_columns_sql(table_name, *column_names)
- column_names.map {|column_name| remove_column_sql(table_name, column_name) }
- end
+ unless options.key?(:comment)
+ options[:comment] = column.comment
+ end
- def add_index_sql(table_name, column_name, options = {})
- index_name, index_type, index_columns, _, index_algorithm, index_using = add_index_options(table_name, column_name, options)
- index_algorithm[0, 0] = ", " if index_algorithm.present?
- "ADD #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_algorithm}"
- end
+ td = create_table_definition(table_name)
+ cd = td.new_column_definition(column.name, type, options)
+ schema_creation.accept(ChangeColumnDefinition.new(cd, column.name))
+ end
- def remove_index_sql(table_name, options = {})
- index_name = index_name_for_remove(table_name, options)
- "DROP INDEX #{index_name}"
- end
+ def rename_column_for_alter(table_name, column_name, new_column_name)
+ column = column_for(table_name, column_name)
+ options = {
+ default: column.default,
+ null: column.null,
+ auto_increment: column.auto_increment?
+ }
- def add_timestamps_sql(table_name, options = {})
- [add_column_sql(table_name, :created_at, :datetime, options), add_column_sql(table_name, :updated_at, :datetime, options)]
- end
+ current_type = exec_query("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE #{quote(column_name)}", "SCHEMA").first["Type"]
+ td = create_table_definition(table_name)
+ cd = td.new_column_definition(new_column_name, current_type, options)
+ schema_creation.accept(ChangeColumnDefinition.new(cd, column.name))
+ end
- def remove_timestamps_sql(table_name, options = {})
- [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)]
- end
+ def add_index_for_alter(table_name, column_name, options = {})
+ index_name, index_type, index_columns, _, index_algorithm, index_using = add_index_options(table_name, column_name, options)
+ index_algorithm[0, 0] = ", " if index_algorithm.present?
+ "ADD #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_algorithm}"
+ end
- private
+ def remove_index_for_alter(table_name, options = {})
+ index_name = index_name_for_remove(table_name, options)
+ "DROP INDEX #{quote_column_name(index_name)}"
+ end
- # 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)
- subsubselect = select.clone
- subsubselect.projections = [key]
-
- subselect = Arel::SelectManager.new(select.engine)
- subselect.project Arel.sql(key.name)
- # Materialized subquery by adding distinct
- # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on'
- subselect.from subsubselect.distinct.as('__active_record_temp')
- end
+ def add_timestamps_for_alter(table_name, options = {})
+ [add_column_for_alter(table_name, :created_at, :datetime, options), add_column_for_alter(table_name, :updated_at, :datetime, options)]
+ end
- def version
- @version ||= full_version.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map(&:to_i)
- end
+ def remove_timestamps_for_alter(table_name, options = {})
+ [remove_column_for_alter(table_name, :updated_at), remove_column_for_alter(table_name, :created_at)]
+ end
- def mariadb?
- full_version =~ /mariadb/i
- end
+ def supports_rename_index?
+ mariadb? ? false : version >= "5.7.6"
+ end
- def supports_rename_index?
- mariadb? ? false : (version[0] == 5 && version[1] >= 7) || version[0] >= 6
- end
+ def configure_connection
+ variables = @config.fetch(:variables, {}).stringify_keys
- def configure_connection
- variables = @config.fetch(:variables, {}).stringify_keys
+ # By default, MySQL 'where id is null' selects the last inserted id; Turn this off.
+ variables["sql_auto_is_null"] = 0
- # By default, MySQL 'where id is null' selects the last inserted id; Turn this off.
- variables['sql_auto_is_null'] = 0
+ # Increase timeout so the server doesn't disconnect us.
+ wait_timeout = self.class.type_cast_config_to_integer(@config[:wait_timeout])
+ wait_timeout = 2147483 unless wait_timeout.is_a?(Integer)
+ variables["wait_timeout"] = wait_timeout
- # Increase timeout so the server doesn't disconnect us.
- wait_timeout = @config[:wait_timeout]
- wait_timeout = 2147483 unless wait_timeout.is_a?(Fixnum)
- variables['wait_timeout'] = self.class.type_cast_config_to_integer(wait_timeout)
+ defaults = [":default", :default].to_set
- defaults = [':default', :default].to_set
+ # Make MySQL reject illegal values rather than truncating or blanking them, see
+ # https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_strict_all_tables
+ # If the user has provided another value for sql_mode, don't replace it.
+ if sql_mode = variables.delete("sql_mode")
+ sql_mode = quote(sql_mode)
+ elsif !defaults.include?(strict_mode?)
+ if strict_mode?
+ sql_mode = "CONCAT(@@sql_mode, ',STRICT_ALL_TABLES')"
+ else
+ sql_mode = "REPLACE(@@sql_mode, 'STRICT_TRANS_TABLES', '')"
+ sql_mode = "REPLACE(#{sql_mode}, 'STRICT_ALL_TABLES', '')"
+ sql_mode = "REPLACE(#{sql_mode}, 'TRADITIONAL', '')"
+ end
+ sql_mode = "CONCAT(#{sql_mode}, ',NO_AUTO_VALUE_ON_ZERO')"
+ end
+ sql_mode_assignment = "@@SESSION.sql_mode = #{sql_mode}, " if sql_mode
+
+ # NAMES does not have an equals sign, see
+ # https://dev.mysql.com/doc/refman/5.7/en/set-names.html
+ # (trailing comma because variable_assignments will always have content)
+ if @config[:encoding]
+ encoding = +"NAMES #{@config[:encoding]}"
+ encoding << " COLLATE #{@config[:collation]}" if @config[:collation]
+ encoding << ", "
+ end
- # Make MySQL reject illegal values rather than truncating or blanking them, see
- # http://dev.mysql.com/doc/refman/5.6/en/sql-mode.html#sqlmode_strict_all_tables
- # If the user has provided another value for sql_mode, don't replace it.
- unless variables.has_key?('sql_mode') || defaults.include?(@config[:strict])
- variables['sql_mode'] = strict_mode? ? 'STRICT_ALL_TABLES' : ''
- end
+ # Gather up all of the SET variables...
+ variable_assignments = variables.map do |k, v|
+ if defaults.include?(v)
+ "@@SESSION.#{k} = DEFAULT" # Sets the value to the global or compile default
+ elsif !v.nil?
+ "@@SESSION.#{k} = #{quote(v)}"
+ end
+ # or else nil; compact to clear nils out
+ end.compact.join(", ")
- # NAMES does not have an equals sign, see
- # http://dev.mysql.com/doc/refman/5.6/en/set-statement.html#id944430
- # (trailing comma because variable_assignments will always have content)
- if @config[:encoding]
- encoding = "NAMES #{@config[:encoding]}"
- encoding << " COLLATE #{@config[:collation]}" if @config[:collation]
- encoding << ", "
+ # ...and send them all in one query
+ execute "SET #{encoding} #{sql_mode_assignment} #{variable_assignments}"
end
- # Gather up all of the SET variables...
- variable_assignments = variables.map do |k, v|
- if defaults.include?(v)
- "@@SESSION.#{k} = DEFAULT" # Sets the value to the global or compile default
- elsif !v.nil?
- "@@SESSION.#{k} = #{quote(v)}"
+ def column_definitions(table_name) # :nodoc:
+ execute_and_free("SHOW FULL FIELDS FROM #{quote_table_name(table_name)}", "SCHEMA") do |result|
+ each_hash(result)
end
- # or else nil; compact to clear nils out
- end.compact.join(', ')
-
- # ...and send them all in one query
- @connection.query "SET #{encoding} #{variable_assignments}"
- end
+ end
- def extract_foreign_key_action(structure, name, action) # :nodoc:
- if structure =~ /CONSTRAINT #{quote_column_name(name)} FOREIGN KEY .* REFERENCES .* ON #{action} (CASCADE|SET NULL|RESTRICT)/
- case $1
- when 'CASCADE'; :cascade
- when 'SET NULL'; :nullify
- end
+ def create_table_info(table_name) # :nodoc:
+ exec_query("SHOW CREATE TABLE #{quote_table_name(table_name)}", "SCHEMA").first["Create Table"]
end
- end
- def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc:
- TableDefinition.new(native_database_types, name, temporary, options, as)
- end
+ def arel_visitor
+ Arel::Visitors::MySQL.new(self)
+ end
- def binary_to_sql(limit) # :nodoc:
- case limit
- when 0..0xfff; "varbinary(#{limit})"
- when nil; "blob"
- when 0x1000..0xffffffff; "blob(#{limit})"
- else raise(ActiveRecordError, "No binary type has character length #{limit}")
+ 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
- end
- def integer_to_sql(limit) # :nodoc:
- case limit
- when 1; 'tinyint'
- when 2; 'smallint'
- when 3; 'mediumint'
- when nil, 4, 11; 'int(11)' # compatibility with MySQL default
- when 5..8; 'bigint'
- else raise(ActiveRecordError, "No integer type has byte size #{limit}")
+ def integer_to_sql(limit) # :nodoc:
+ case limit
+ when 1; "tinyint"
+ when 2; "smallint"
+ when 3; "mediumint"
+ when nil, 4; "int"
+ when 5..8; "bigint"
+ else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a decimal with scale 0 instead.")
+ end
end
- end
- def text_to_sql(limit) # :nodoc:
- case limit
- when 0..0xff; 'tinytext'
- when nil, 0x100..0xffff; 'text'
- when 0x10000..0xffffff; 'mediumtext'
- when 0x1000000..0xffffffff; 'longtext'
- else raise(ActiveRecordError, "No text type has character length #{limit}")
+ def text_to_sql(limit) # :nodoc:
+ case limit
+ when 0..0xff; "tinytext"
+ when nil, 0x100..0xffff; "text"
+ when 0x10000..0xffffff; "mediumtext"
+ when 0x1000000..0xffffffff; "longtext"
+ else raise(ActiveRecordError, "No text type has byte length #{limit}")
+ end
end
- end
- class MysqlString < Type::String # :nodoc:
- def serialize(value)
- case value
- when true then "1"
- when false then "0"
- else super
+ def binary_to_sql(limit) # :nodoc:
+ case limit
+ when 0..0xff; "tinyblob"
+ when nil, 0x100..0xffff; "blob"
+ when 0x10000..0xffffff; "mediumblob"
+ when 0x1000000..0xffffffff; "longblob"
+ else raise(ActiveRecordError, "No binary type has byte length #{limit}")
end
end
- private
+ def version_string
+ full_version.match(/^(?:5\.5\.5-)?(\d+\.\d+\.\d+)/)[1]
+ end
- def cast_value(value)
- case value
- when true then "1"
- when false then "0"
- else super
+ class MysqlString < Type::String # :nodoc:
+ def serialize(value)
+ case value
+ when true then "1"
+ when false then "0"
+ else super
+ end
end
+
+ private
+
+ def cast_value(value)
+ case value
+ when true then "1"
+ when false then "0"
+ else super
+ end
+ end
end
- end
- ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql)
- ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql2)
+ ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql2)
+ ActiveRecord::Type.register(:unsigned_integer, Type::UnsignedInteger, adapter: :mysql2)
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb
index 4b95b0681d..5d81de9fe1 100644
--- a/activerecord/lib/active_record/connection_adapters/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -1,35 +1,29 @@
-require 'set'
+# frozen_string_literal: true
module ActiveRecord
# :stopdoc:
module ConnectionAdapters
# An abstract definition of a column in a table.
class Column
- FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].to_set
-
- module Format
- ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/
- ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
- end
-
- attr_reader :name, :null, :sql_type_metadata, :default, :default_function, :collation
+ attr_reader :name, :default, :sql_type_metadata, :null, :table_name, :default_function, :collation, :comment
delegate :precision, :scale, :limit, :type, :sql_type, to: :sql_type_metadata, allow_nil: true
# Instantiates a new column in the table.
#
- # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int(11)</tt>.
+ # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id bigint</tt>.
# +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>.
# +sql_type_metadata+ is various information about the type of the column
# +null+ determines if this column allows +NULL+ values.
- def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil)
- @name = name
+ def initialize(name, default, sql_type_metadata = nil, null = true, table_name = nil, default_function = nil, collation = nil, comment: nil, **)
+ @name = name.freeze
+ @table_name = table_name
@sql_type_metadata = sql_type_metadata
@null = null
@default = default
@default_function = default_function
@collation = collation
- @table_name = nil
+ @comment = comment
end
def has_default?
@@ -37,7 +31,7 @@ module ActiveRecord
end
def bigint?
- /bigint/ === sql_type
+ /\Abigint\b/.match?(sql_type)
end
# Returns the human name of the column name.
@@ -48,6 +42,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
@@ -60,9 +76,9 @@ module ActiveRecord
protected
- def attributes_for_hash
- [self.class, name, default, sql_type_metadata, null, default_function, collation]
- end
+ def attributes_for_hash
+ [self.class, name, default, sql_type_metadata, null, table_name, default_function, collation]
+ end
end
class NullColumn < Column
diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
index 08d46fca96..2e7a78215a 100644
--- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb
+++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
@@ -1,21 +1,26 @@
-require 'uri'
+# frozen_string_literal: true
+
+require "uri"
module ActiveRecord
module ConnectionAdapters
class ConnectionSpecification #:nodoc:
- attr_reader :config, :adapter_method
+ attr_reader :name, :config, :adapter_method
- def initialize(config, adapter_method)
- @config, @adapter_method = config, adapter_method
+ def initialize(name, config, adapter_method)
+ @name, @config, @adapter_method = name, config, adapter_method
end
def initialize_dup(original)
@config = original.config.dup
end
+ def to_hash
+ @config.merge(name: @name)
+ end
+
# Expands a connection string into a hash.
class ConnectionUrlResolver # :nodoc:
-
# == Example
#
# url = "postgresql://foo:bar@localhost:9000/foo_test?pool=5&timeout=3000"
@@ -33,11 +38,11 @@ module ActiveRecord
def initialize(url)
raise "Database URL cannot be empty" if url.blank?
@uri = uri_parser.parse(url)
- @adapter = @uri.scheme.tr('-', '_')
+ @adapter = @uri.scheme && @uri.scheme.tr("-", "_")
@adapter = "postgresql" if @adapter == "postgres"
if @uri.opaque
- @uri.opaque, @query = @uri.opaque.split('?', 2)
+ @uri.opaque, @query = @uri.opaque.split("?", 2)
else
@query = @uri.query
end
@@ -45,65 +50,63 @@ module ActiveRecord
# Converts the given URL to a full connection hash.
def to_hash
- config = raw_config.reject { |_,value| value.blank? }
- config.map { |key,value| config[key] = uri_parser.unescape(value) if value.is_a? String }
+ config = raw_config.reject { |_, value| value.blank? }
+ config.map { |key, value| config[key] = uri_parser.unescape(value) if value.is_a? String }
config
end
private
- def uri
- @uri
- end
+ attr_reader :uri
- def uri_parser
- @uri_parser ||= URI::Parser.new
- end
+ def uri_parser
+ @uri_parser ||= URI::Parser.new
+ end
- # Converts the query parameters of the URI into a hash.
- #
- # "localhost?pool=5&reaping_frequency=2"
- # # => { "pool" => "5", "reaping_frequency" => "2" }
- #
- # returns empty hash if no query present.
- #
- # "localhost"
- # # => {}
- def query_hash
- Hash[(@query || '').split("&").map { |pair| pair.split("=") }]
- end
+ # Converts the query parameters of the URI into a hash.
+ #
+ # "localhost?pool=5&reaping_frequency=2"
+ # # => { "pool" => "5", "reaping_frequency" => "2" }
+ #
+ # returns empty hash if no query present.
+ #
+ # "localhost"
+ # # => {}
+ def query_hash
+ Hash[(@query || "").split("&").map { |pair| pair.split("=") }]
+ end
- def raw_config
- if uri.opaque
- query_hash.merge({
- "adapter" => @adapter,
- "database" => uri.opaque })
- else
- query_hash.merge({
- "adapter" => @adapter,
- "username" => uri.user,
- "password" => uri.password,
- "port" => uri.port,
- "database" => database_from_path,
- "host" => uri.hostname })
+ def raw_config
+ if uri.opaque
+ query_hash.merge(
+ "adapter" => @adapter,
+ "database" => uri.opaque)
+ else
+ query_hash.merge(
+ "adapter" => @adapter,
+ "username" => uri.user,
+ "password" => uri.password,
+ "port" => uri.port,
+ "database" => database_from_path,
+ "host" => uri.hostname)
+ end
end
- end
- # Returns name of the database.
- def database_from_path
- if @adapter == 'sqlite3'
- # 'sqlite3:/foo' is absolute, because that makes sense. The
- # corresponding relative version, 'sqlite3:foo', is handled
- # elsewhere, as an "opaque".
+ # Returns name of the database.
+ def database_from_path
+ if @adapter == "sqlite3"
+ # 'sqlite3:/foo' is absolute, because that makes sense. The
+ # corresponding relative version, 'sqlite3:foo', is handled
+ # elsewhere, as an "opaque".
- uri.path
- else
- # Only SQLite uses a filename as the "database" name; for
- # anything else, a leading slash would be silly.
+ uri.path
+ else
+ # Only SQLite uses a filename as the "database" name; for
+ # anything else, a leading slash would be silly.
- uri.path.sub(%r{^/}, "")
+ uri.path.sub(%r{^/}, "")
+ end
end
- end
end
##
@@ -111,8 +114,7 @@ module ActiveRecord
class Resolver # :nodoc:
attr_reader :configurations
- # Accepts a hash two layers deep, keys on the first layer represent
- # environments such as "production". Keys must be strings.
+ # Accepts a list of db config objects.
def initialize(configurations)
@configurations = configurations
end
@@ -133,25 +135,14 @@ module ActiveRecord
# Resolver.new(configurations).resolve(:production)
# # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" }
#
- def resolve(config)
- if config
- resolve_connection config
- elsif env = ActiveRecord::ConnectionHandling::RAILS_ENV.call
- resolve_symbol_connection env.to_sym
+ def resolve(config_or_env, pool_name = nil)
+ if config_or_env
+ resolve_connection config_or_env, pool_name
else
raise AdapterNotSpecified
end
end
- # Expands each key in @configurations hash into fully resolved hash
- def resolve_all
- config = configurations.dup
- config.each do |key, value|
- config[key] = resolve(value) if value
- end
- config
- end
-
# Returns an instance of ConnectionSpecification for a given adapter.
# Accepts a hash one layer deep that contains all connection information.
#
@@ -165,91 +156,122 @@ module ActiveRecord
# # => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" }
#
def spec(config)
- spec = resolve(config).symbolize_keys
+ pool_name = config if config.is_a?(Symbol)
+
+ spec = resolve(config, pool_name).symbolize_keys
raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter)
+ # Require the adapter itself and give useful feedback about
+ # 1. Missing adapter gems and
+ # 2. Adapter gems' missing dependencies.
path_to_adapter = "active_record/connection_adapters/#{spec[:adapter]}_adapter"
begin
require path_to_adapter
- rescue Gem::LoadError => e
- raise Gem::LoadError, "Specified '#{spec[:adapter]}' for database adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile (and ensure its version is at the minimum required by ActiveRecord)."
rescue LoadError => e
- raise LoadError, "Could not load '#{path_to_adapter}'. Make sure that the adapter in config/database.yml is valid. If you use an adapter other than 'mysql', 'mysql2', 'postgresql' or 'sqlite3' add the necessary adapter gem to the Gemfile.", e.backtrace
+ # We couldn't require the adapter itself. Raise an exception that
+ # points out config typos and missing gems.
+ if e.path == path_to_adapter
+ # We can assume that a non-builtin adapter was specified, so it's
+ # either misspelled or missing from Gemfile.
+ raise e.class, "Could not load the '#{spec[:adapter]}' Active Record adapter. Ensure that the adapter is spelled correctly in config/database.yml and that you've added the necessary adapter gem to your Gemfile.", e.backtrace
+
+ # Bubbled up from the adapter require. Prefix the exception message
+ # with some guidance about how to address it and reraise.
+ else
+ raise e.class, "Error loading the '#{spec[:adapter]}' Active Record adapter. Missing a gem it depends on? #{e.message}", e.backtrace
+ end
end
adapter_method = "#{spec[:adapter]}_connection"
- ConnectionSpecification.new(spec, adapter_method)
+
+ unless ActiveRecord::Base.respond_to?(adapter_method)
+ raise AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter"
+ end
+
+ ConnectionSpecification.new(spec.delete(:name) || "primary", spec, adapter_method)
end
private
-
- # Returns fully resolved connection, accepts hash, string or symbol.
- # Always returns a hash.
- #
- # == Examples
- #
- # Symbol representing current environment.
- #
- # Resolver.new("production" => {}).resolve_connection(:production)
- # # => {}
- #
- # One layer deep hash of connection values.
- #
- # Resolver.new({}).resolve_connection("adapter" => "sqlite3")
- # # => { "adapter" => "sqlite3" }
- #
- # Connection URL.
- #
- # Resolver.new({}).resolve_connection("postgresql://localhost/foo")
- # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" }
- #
- def resolve_connection(spec)
- case spec
- when Symbol
- resolve_symbol_connection spec
- when String
- resolve_url_connection spec
- when Hash
- resolve_hash_connection spec
+ # Returns fully resolved connection, accepts hash, string or symbol.
+ # Always returns a hash.
+ #
+ # == Examples
+ #
+ # Symbol representing current environment.
+ #
+ # Resolver.new("production" => {}).resolve_connection(:production)
+ # # => {}
+ #
+ # One layer deep hash of connection values.
+ #
+ # Resolver.new({}).resolve_connection("adapter" => "sqlite3")
+ # # => { "adapter" => "sqlite3" }
+ #
+ # Connection URL.
+ #
+ # Resolver.new({}).resolve_connection("postgresql://localhost/foo")
+ # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" }
+ #
+ def resolve_connection(config_or_env, pool_name = nil)
+ case config_or_env
+ when Symbol
+ resolve_symbol_connection config_or_env, pool_name
+ when String
+ resolve_url_connection config_or_env
+ when Hash
+ resolve_hash_connection config_or_env
+ else
+ resolve_connection config_or_env
+ end
end
- end
- # Takes the environment such as +:production+ or +:development+.
- # This requires that the @configurations was initialized with a key that
- # matches.
- #
- # Resolver.new("production" => {}).resolve_symbol_connection(:production)
- # # => {}
- #
- def resolve_symbol_connection(spec)
- if config = configurations[spec.to_s]
- resolve_connection(config)
- else
- raise(AdapterNotSpecified, "'#{spec}' database is not configured. Available: #{configurations.keys.inspect}")
+ # Takes the environment such as +:production+ or +:development+ and a
+ # pool name the corresponds to the name given by the connection pool
+ # to the connection. That pool name is merged into the hash with the
+ # name key.
+ #
+ # This requires that the @configurations was initialized with a key that
+ # matches.
+ #
+ # configurations = #<ActiveRecord::DatabaseConfigurations:0x00007fd9fdace3e0
+ # @configurations=[
+ # #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd9fdace250
+ # @env_name="production", @spec_name="primary", @config={"database"=>"my_db"}>
+ # ]>
+ #
+ # Resolver.new(configurations).resolve_symbol_connection(:production, "primary")
+ # # => { "database" => "my_db" }
+ def resolve_symbol_connection(env_name, pool_name)
+ db_config = configurations.find_db_config(env_name)
+
+ if db_config
+ resolve_connection(db_config.config).merge("name" => pool_name.to_s)
+ else
+ raise(AdapterNotSpecified, "'#{env_name}' database is not configured. Available: #{configurations.configurations.map(&:env_name).join(", ")}")
+ end
end
- end
- # Accepts a hash. Expands the "url" key that contains a
- # URL database connection to a full connection
- # hash and merges with the rest of the hash.
- # Connection details inside of the "url" key win any merge conflicts
- def resolve_hash_connection(spec)
- if spec["url"] && spec["url"] !~ /^jdbc:/
- connection_hash = resolve_url_connection(spec.delete("url"))
- spec.merge!(connection_hash)
+ # Accepts a hash. Expands the "url" key that contains a
+ # URL database connection to a full connection
+ # hash and merges with the rest of the hash.
+ # Connection details inside of the "url" key win any merge conflicts
+ def resolve_hash_connection(spec)
+ if spec["url"] && spec["url"] !~ /^jdbc:/
+ connection_hash = resolve_url_connection(spec.delete("url"))
+ spec.merge!(connection_hash)
+ end
+ spec
end
- spec
- end
- # Takes a connection URL.
- #
- # Resolver.new({}).resolve_url_connection("postgresql://localhost/foo")
- # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" }
- #
- def resolve_url_connection(url)
- ConnectionUrlResolver.new(url).to_hash
- end
+ # Takes a connection URL.
+ #
+ # Resolver.new({}).resolve_url_connection("postgresql://localhost/foo")
+ # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" }
+ #
+ def resolve_url_connection(url)
+ ConnectionUrlResolver.new(url).to_hash
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb b/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb
new file mode 100644
index 0000000000..883747b84b
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module ConnectionAdapters
+ module DetermineIfPreparableVisitor
+ attr_reader :preparable
+
+ def accept(*)
+ @preparable = true
+ super
+ end
+
+ def visit_Arel_Nodes_In(o, collector)
+ @preparable = false
+ super
+ end
+
+ def visit_Arel_Nodes_NotIn(o, collector)
+ @preparable = false
+ super
+ end
+
+ def visit_Arel_Nodes_SqlLiteral(*)
+ @preparable = false
+ super
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/column.rb b/activerecord/lib/active_record/connection_adapters/mysql/column.rb
new file mode 100644
index 0000000000..fa1541019d
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/mysql/column.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module ConnectionAdapters
+ module MySQL
+ class Column < ConnectionAdapters::Column # :nodoc:
+ delegate :extra, to: :sql_type_metadata, allow_nil: true
+
+ def unsigned?
+ /\bunsigned(?: zerofill)?\z/.match?(sql_type)
+ end
+
+ def case_sensitive?
+ collation && !/_ci\z/.match?(collation)
+ end
+
+ def auto_increment?
+ extra == "auto_increment"
+ end
+
+ def virtual?
+ /\b(?:VIRTUAL|STORED|PERSISTENT)\b/.match?(extra)
+ end
+ 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
new file mode 100644
index 0000000000..684c7042a7
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
@@ -0,0 +1,141 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module ConnectionAdapters
+ module MySQL
+ module DatabaseStatements
+ # Returns an ActiveRecord::Result instance.
+ def select_all(*) # :nodoc:
+ result = if ExplainRegistry.collect? && prepared_statements
+ unprepared_statement { super }
+ else
+ super
+ end
+ discard_remaining_results
+ result
+ end
+
+ def query(sql, name = nil) # :nodoc:
+ execute(sql, name).to_a
+ end
+
+ # Executes the SQL statement in the context of this connection.
+ def execute(sql, name = nil)
+ # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
+ # made since we established the connection
+ @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone
+
+ super
+ end
+
+ def exec_query(sql, name = "SQL", binds = [], prepare: false)
+ materialize_transactions
+
+ if without_prepared_statement?(binds)
+ execute_and_free(sql, name) do |result|
+ ActiveRecord::Result.new(result.fields, result.to_a) if result
+ end
+ else
+ exec_stmt_and_free(sql, name, binds, cache_stmt: prepare) do |_, result|
+ ActiveRecord::Result.new(result.fields, result.to_a) if result
+ end
+ end
+ end
+
+ def exec_delete(sql, name = nil, binds = [])
+ materialize_transactions
+
+ if without_prepared_statement?(binds)
+ execute_and_free(sql, name) { @connection.affected_rows }
+ else
+ exec_stmt_and_free(sql, name, binds) { |stmt| stmt.affected_rows }
+ end
+ end
+ alias :exec_update :exec_delete
+
+ private
+ def default_insert_value(column)
+ Arel.sql("DEFAULT") unless column.auto_increment?
+ end
+
+ def last_inserted_id(result)
+ @connection.last_id
+ end
+
+ def discard_remaining_results
+ @connection.abandon_results!
+ end
+
+ def supports_set_server_option?
+ @connection.respond_to?(:set_server_option)
+ end
+
+ def multi_statements_enabled?(flags)
+ if flags.is_a?(Array)
+ flags.include?("MULTI_STATEMENTS")
+ else
+ (flags & Mysql2::Client::MULTI_STATEMENTS) != 0
+ end
+ end
+
+ def with_multi_statements
+ previous_flags = @config[:flags]
+
+ unless multi_statements_enabled?(previous_flags)
+ if supports_set_server_option?
+ @connection.set_server_option(Mysql2::Client::OPTION_MULTI_STATEMENTS_ON)
+ else
+ @config[:flags] = Mysql2::Client::MULTI_STATEMENTS
+ reconnect!
+ end
+ end
+
+ yield
+ ensure
+ unless multi_statements_enabled?(previous_flags)
+ if supports_set_server_option?
+ @connection.set_server_option(Mysql2::Client::OPTION_MULTI_STATEMENTS_OFF)
+ else
+ @config[:flags] = previous_flags
+ reconnect!
+ end
+ end
+ end
+
+ def exec_stmt_and_free(sql, name, binds, cache_stmt: false)
+ # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
+ # made since we established the connection
+ @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone
+
+ type_casted_binds = type_casted_binds(binds)
+
+ log(sql, name, binds, type_casted_binds) do
+ if cache_stmt
+ stmt = @statements[sql] ||= @connection.prepare(sql)
+ else
+ stmt = @connection.prepare(sql)
+ end
+
+ begin
+ result = ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
+ stmt.execute(*type_casted_binds)
+ end
+ rescue Mysql2::Error => e
+ if cache_stmt
+ @statements.delete(sql)
+ else
+ stmt.close
+ end
+ raise e
+ end
+
+ ret = yield stmt, result
+ result.free if result
+ stmt.close unless cache_stmt
+ ret
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb b/activerecord/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb
new file mode 100644
index 0000000000..20c3c83664
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module ConnectionAdapters
+ module MySQL
+ class ExplainPrettyPrinter # :nodoc:
+ # Pretty prints the result of an EXPLAIN in a way that resembles the output of the
+ # MySQL shell:
+ #
+ # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
+ # | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+ # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
+ # | 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | |
+ # | 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where |
+ # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
+ # 2 rows in set (0.00 sec)
+ #
+ # This is an exercise in Ruby hyperrealism :).
+ def pp(result, elapsed)
+ widths = compute_column_widths(result)
+ separator = build_separator(widths)
+
+ pp = []
+
+ pp << separator
+ pp << build_cells(result.columns, widths)
+ pp << separator
+
+ result.rows.each do |row|
+ pp << build_cells(row, widths)
+ end
+
+ pp << separator
+ pp << build_footer(result.rows.length, elapsed)
+
+ pp.join("\n") + "\n"
+ end
+
+ private
+
+ def compute_column_widths(result)
+ [].tap do |widths|
+ result.columns.each_with_index do |column, i|
+ cells_in_column = [column] + result.rows.map { |r| r[i].nil? ? "NULL" : r[i].to_s }
+ widths << cells_in_column.map(&:length).max
+ end
+ end
+ end
+
+ def build_separator(widths)
+ padding = 1
+ "+" + widths.map { |w| "-" * (w + (padding * 2)) }.join("+") + "+"
+ end
+
+ def build_cells(items, widths)
+ cells = []
+ items.each_with_index do |item, i|
+ item = "NULL" if item.nil?
+ justifier = item.is_a?(Numeric) ? "rjust" : "ljust"
+ cells << item.to_s.send(justifier, widths[i])
+ end
+ "| " + cells.join(" | ") + " |"
+ end
+
+ def build_footer(nrows, elapsed)
+ rows_label = nrows == 1 ? "row" : "rows"
+ "#{nrows} #{rows_label} in set (%.2f sec)" % elapsed
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb
new file mode 100644
index 0000000000..75564a61d6
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module ConnectionAdapters
+ module MySQL
+ module Quoting # :nodoc:
+ def quote_column_name(name)
+ @quoted_column_names[name] ||= "`#{super.gsub('`', '``')}`"
+ end
+
+ def quote_table_name(name)
+ @quoted_table_names[name] ||= super.gsub(".", "`.`").freeze
+ end
+
+ def unquoted_true
+ 1
+ end
+
+ def unquoted_false
+ 0
+ end
+
+ def quoted_date(value)
+ if supports_datetime_with_precision?
+ super
+ else
+ super.sub(/\.\d{6}\z/, "")
+ end
+ end
+
+ def quoted_binary(value)
+ "x'#{value.hex}'"
+ end
+
+ def _type_cast(value)
+ case value
+ when Date, Time then value
+ else super
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb
new file mode 100644
index 0000000000..82ed320617
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module ConnectionAdapters
+ module MySQL
+ class SchemaCreation < AbstractAdapter::SchemaCreation # :nodoc:
+ delegate :add_sql_comment!, :mariadb?, to: :@conn, private: true
+
+ private
+
+ def visit_DropForeignKey(name)
+ "DROP FOREIGN KEY #{name}"
+ end
+
+ def visit_AddColumnDefinition(o)
+ add_column_position!(super, column_options(o.column))
+ end
+
+ def visit_ChangeColumnDefinition(o)
+ change_column_sql = +"CHANGE #{quote_column_name(o.name)} #{accept(o.column)}"
+ add_column_position!(change_column_sql, column_options(o.column))
+ end
+
+ def add_table_options!(create_sql, options)
+ add_sql_comment!(super, options[:comment])
+ 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 https://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
+
+ if collation = options[:collation]
+ 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
+
+ def add_column_position!(sql, options)
+ if options[:first]
+ sql << " FIRST"
+ elsif options[:after]
+ sql << " AFTER #{quote_column_name(options[:after])}"
+ end
+
+ sql
+ end
+
+ def index_in_create(table_name, column_name, options)
+ index_name, index_type, index_columns, _, _, index_using, comment = @conn.add_index_options(table_name, column_name, options)
+ add_sql_comment!((+"#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})"), comment)
+ end
+ end
+ end
+ end
+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
new file mode 100644
index 0000000000..2ed4ad16ae
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module ConnectionAdapters
+ module MySQL
+ module ColumnMethods
+ def blob(*args, **options)
+ args.each { |name| column(name, :blob, options) }
+ end
+
+ def tinyblob(*args, **options)
+ args.each { |name| column(name, :tinyblob, options) }
+ end
+
+ def mediumblob(*args, **options)
+ args.each { |name| column(name, :mediumblob, options) }
+ end
+
+ def longblob(*args, **options)
+ args.each { |name| column(name, :longblob, options) }
+ end
+
+ def tinytext(*args, **options)
+ args.each { |name| column(name, :tinytext, options) }
+ end
+
+ def mediumtext(*args, **options)
+ args.each { |name| column(name, :mediumtext, options) }
+ end
+
+ def longtext(*args, **options)
+ args.each { |name| column(name, :longtext, options) }
+ end
+
+ def unsigned_integer(*args, **options)
+ args.each { |name| column(name, :unsigned_integer, options) }
+ end
+
+ def unsigned_bigint(*args, **options)
+ args.each { |name| column(name, :unsigned_bigint, options) }
+ end
+
+ def unsigned_float(*args, **options)
+ args.each { |name| column(name, :unsigned_float, options) }
+ end
+
+ def unsigned_decimal(*args, **options)
+ args.each { |name| column(name, :unsigned_decimal, options) }
+ end
+ end
+
+ class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
+ include ColumnMethods
+
+ def new_column_definition(name, type, **options) # :nodoc:
+ case type
+ when :virtual
+ type = options[:type]
+ when :primary_key
+ type = :integer
+ options[:limit] ||= 8
+ options[:primary_key] = true
+ when /\Aunsigned_(?<type>.+)\z/
+ type = $~[:type].to_sym
+ options[:unsigned] = true
+ end
+
+ super
+ end
+
+ private
+ def aliased_types(name, fallback)
+ fallback
+ end
+
+ def integer_like_primary_key_type(type, options)
+ options[:auto_increment] = true
+ type
+ end
+ end
+
+ class Table < ActiveRecord::ConnectionAdapters::Table
+ include ColumnMethods
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
new file mode 100644
index 0000000000..d23178e43c
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module ConnectionAdapters
+ module MySQL
+ class SchemaDumper < ConnectionAdapters::SchemaDumper # :nodoc:
+ private
+ def prepare_column_options(column)
+ spec = super
+ spec[:unsigned] = "true" if column.unsigned?
+ spec[:auto_increment] = "true" if column.auto_increment?
+
+ if @connection.supports_virtual_columns? && column.virtual?
+ spec[:as] = extract_expression_for_virtual_column(column)
+ spec[:stored] = "true" if /\b(?:STORED|PERSISTENT)\b/.match?(column.extra)
+ spec = { type: schema_type(column).inspect }.merge!(spec)
+ end
+
+ spec
+ end
+
+ def column_spec_for_primary_key(column)
+ spec = super
+ spec.delete(:auto_increment) if column.type == :integer && column.auto_increment?
+ spec
+ end
+
+ def default_primary_key?(column)
+ super && column.auto_increment? && !column.unsigned?
+ end
+
+ def explicit_primary_key_default?(column)
+ column.type == :integer && !column.auto_increment?
+ end
+
+ def schema_type(column)
+ case column.sql_type
+ when /\Atimestamp\b/
+ :timestamp
+ when "tinyblob"
+ :blob
+ else
+ super
+ end
+ end
+
+ def schema_precision(column)
+ super unless /\A(?:date)?time(?:stamp)?\b/.match?(column.sql_type) && column.precision == 0
+ end
+
+ def schema_collation(column)
+ if column.collation && table_name = column.table_name
+ @table_collation_cache ||= {}
+ @table_collation_cache[table_name] ||=
+ @connection.exec_query("SHOW TABLE STATUS LIKE #{@connection.quote(table_name)}", "SCHEMA").first["Collation"]
+ column.collation.inspect if column.collation != @table_collation_cache[table_name]
+ end
+ end
+
+ def extract_expression_for_virtual_column(column)
+ if @connection.mariadb? && @connection.version < "10.2.5"
+ create_table_info = @connection.send(:create_table_info, column.table_name)
+ column_name = @connection.quote_column_name(column.name)
+ if %r/#{column_name} #{Regexp.quote(column.sql_type)}(?: COLLATE \w+)? AS \((?<expression>.+?)\) #{column.extra}/ =~ create_table_info
+ $~[:expression].inspect
+ end
+ else
+ scope = @connection.send(:quoted_scope, column.table_name)
+ column_name = @connection.quote(column.name)
+ sql = "SELECT generation_expression FROM information_schema.columns" \
+ " WHERE table_schema = #{scope[:schema]}" \
+ " AND table_name = #{scope[:name]}" \
+ " AND column_name = #{column_name}"
+ @connection.query_value(sql, "SCHEMA").inspect
+ end
+ 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..4894fd1c08
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
@@ -0,0 +1,177 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module ConnectionAdapters
+ module MySQL
+ module SchemaStatements # :nodoc:
+ # Returns an array of indexes for the given table.
+ def indexes(table_name)
+ indexes = []
+ current_index = nil
+ execute_and_free("SHOW KEYS FROM #{quote_table_name(table_name)}", "SCHEMA") do |result|
+ each_hash(result) do |row|
+ if current_index != row[:Key_name]
+ next if row[:Key_name] == "PRIMARY" # skip the primary key
+ current_index = row[:Key_name]
+
+ mysql_index_type = row[:Index_type].downcase.to_sym
+ case mysql_index_type
+ when :fulltext, :spatial
+ index_type = mysql_index_type
+ when :btree, :hash
+ index_using = mysql_index_type
+ end
+
+ indexes << [
+ row[:Table],
+ row[:Key_name],
+ row[:Non_unique].to_i == 0,
+ [],
+ lengths: {},
+ orders: {},
+ type: index_type,
+ using: index_using,
+ comment: row[:Index_comment].presence
+ ]
+ end
+
+ if row[:Expression]
+ expression = row[:Expression]
+ expression = +"(#{expression})" unless expression.start_with?("(")
+ indexes.last[-2] << expression
+ indexes.last[-1][:expressions] ||= {}
+ indexes.last[-1][:expressions][expression] = expression
+ indexes.last[-1][:orders][expression] = :desc if row[:Collation] == "D"
+ else
+ indexes.last[-2] << row[:Column_name]
+ indexes.last[-1][:lengths][row[:Column_name]] = row[:Sub_part].to_i if row[:Sub_part]
+ indexes.last[-1][:orders][row[:Column_name]] = :desc if row[:Collation] == "D"
+ end
+ end
+ end
+
+ indexes.map do |index|
+ options = index.last
+
+ if expressions = options.delete(:expressions)
+ orders = options.delete(:orders)
+ lengths = options.delete(:lengths)
+
+ columns = index[-2].map { |name|
+ [ name.to_sym, expressions[name] || +quote_column_name(name) ]
+ }.to_h
+
+ index[-2] = add_options_for_index_columns(
+ columns, order: orders, length: lengths
+ ).values.join(", ")
+ end
+
+ IndexDefinition.new(*index)
+ end
+ end
+
+ def remove_column(table_name, column_name, type = nil, options = {})
+ if foreign_key_exists?(table_name, column: column_name)
+ remove_foreign_key(table_name, column: column_name)
+ end
+ super
+ end
+
+ def internal_string_options_for_primary_key
+ super.tap do |options|
+ if CHARSETS_OF_4BYTES_MAXLEN.include?(charset) && (mariadb? || version < "8.0.0")
+ options[:collation] = collation.sub(/\A[^_]+/, "utf8")
+ end
+ end
+ end
+
+ def update_table_definition(table_name, base)
+ MySQL::Table.new(table_name, base)
+ end
+
+ def create_schema_dumper(options)
+ MySQL::SchemaDumper.create(self, options)
+ end
+
+ private
+ CHARSETS_OF_4BYTES_MAXLEN = ["utf8mb4", "utf16", "utf16le", "utf32"]
+
+ def schema_creation
+ MySQL::SchemaCreation.new(self)
+ end
+
+ def create_table_definition(*args)
+ MySQL::TableDefinition.new(*args)
+ end
+
+ def new_column_from_field(table_name, field)
+ type_metadata = fetch_type_metadata(field[:Type], field[:Extra])
+ default, default_function = field[:Default], nil
+
+ if type_metadata.type == :datetime && /\ACURRENT_TIMESTAMP(?:\([0-6]?\))?\z/i.match?(default)
+ default, default_function = nil, default
+ elsif type_metadata.extra == "DEFAULT_GENERATED"
+ default = +"(#{default})" unless default.start_with?("(")
+ default, default_function = nil, default
+ end
+
+ MySQL::Column.new(
+ field[:Field],
+ default,
+ type_metadata,
+ field[:Null] == "YES",
+ table_name,
+ default_function,
+ field[:Collation],
+ comment: field[:Comment].presence
+ )
+ end
+
+ def fetch_type_metadata(sql_type, extra = "")
+ MySQL::TypeMetadata.new(super(sql_type), extra: extra)
+ end
+
+ def extract_foreign_key_action(specifier)
+ super unless specifier == "RESTRICT"
+ end
+
+ def add_index_length(quoted_columns, **options)
+ lengths = options_for_index_columns(options[:length])
+ quoted_columns.each do |name, column|
+ column << "(#{lengths[name]})" if lengths[name].present?
+ end
+ end
+
+ def add_options_for_index_columns(quoted_columns, **options)
+ quoted_columns = add_index_length(quoted_columns, options)
+ super
+ end
+
+ def data_source_sql(name = nil, type: nil)
+ scope = quoted_scope(name, type: type)
+
+ 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
new file mode 100644
index 0000000000..7ad0944d51
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+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: "")
+ super(type_metadata)
+ @type_metadata = type_metadata
+ @extra = extra
+ end
+
+ def ==(other)
+ other.is_a?(MySQL::TypeMetadata) &&
+ attributes_for_hash == other.attributes_for_hash
+ end
+ alias eql? ==
+
+ def hash
+ attributes_for_hash.hash
+ end
+
+ protected
+
+ def attributes_for_hash
+ [self.class, @type_metadata, extra]
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index e97e82f056..9bdaa00336 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -1,26 +1,29 @@
-require 'active_record/connection_adapters/abstract_mysql_adapter'
+# frozen_string_literal: true
-gem 'mysql2', '~> 0.3.18'
-require 'mysql2'
+require "active_record/connection_adapters/abstract_mysql_adapter"
+require "active_record/connection_adapters/mysql/database_statements"
+
+gem "mysql2", ">= 0.4.4"
+require "mysql2"
module ActiveRecord
module ConnectionHandling # :nodoc:
# Establishes a connection to the database that's used by all Active Record objects.
def mysql2_connection(config)
config = config.symbolize_keys
+ config[:flags] ||= 0
- config[:username] = 'root' if config[:username].nil?
-
- if Mysql2::Client.const_defined? :FOUND_ROWS
- config[:flags] = Mysql2::Client::FOUND_ROWS
+ if config[:flags].kind_of? Array
+ config[:flags].push "FOUND_ROWS"
+ else
+ config[:flags] |= Mysql2::Client::FOUND_ROWS
end
client = Mysql2::Client.new(config)
- options = [config[:host], config[:username], config[:password], config[:database], config[:port], config[:socket], 0]
- ConnectionAdapters::Mysql2Adapter.new(client, logger, options, config)
+ ConnectionAdapters::Mysql2Adapter.new(client, logger, nil, config)
rescue Mysql2::Error => error
if error.message.include?("Unknown database")
- raise ActiveRecord::NoDatabaseError.new(error.message, error)
+ raise ActiveRecord::NoDatabaseError
else
raise
end
@@ -29,15 +32,33 @@ module ActiveRecord
module ConnectionAdapters
class Mysql2Adapter < AbstractMysqlAdapter
- ADAPTER_NAME = 'Mysql2'.freeze
+ ADAPTER_NAME = "Mysql2"
+
+ include MySQL::DatabaseStatements
def initialize(connection, logger, connection_options, config)
super
- @prepared_statements = false
+ @prepared_statements = false unless config.key?(:prepared_statements)
configure_connection
end
- def supports_explain?
+ def supports_json?
+ !mariadb? && version >= "5.7.8"
+ end
+
+ def supports_comments?
+ true
+ end
+
+ def supports_comments_in_create?
+ true
+ end
+
+ def supports_savepoints?
+ true
+ end
+
+ def supports_lazy_transactions?
true
end
@@ -45,7 +66,7 @@ module ActiveRecord
def each_hash(result) # :nodoc:
if block_given?
- result.each(:as => :hash, :symbolize_keys => true) do |row|
+ result.each(as: :hash, symbolize_keys: true) do |row|
yield row
end
else
@@ -70,7 +91,6 @@ module ActiveRecord
#++
def active?
- return false unless @connection
@connection.ping
end
@@ -85,181 +105,29 @@ module ActiveRecord
# Otherwise, this method does nothing.
def disconnect!
super
- unless @connection.nil?
- @connection.close
- @connection = nil
- end
+ @connection.close
end
- #--
- # DATABASE STATEMENTS ======================================
- #++
-
- def explain(arel, binds = [])
- sql = "EXPLAIN #{to_sql(arel, binds.dup)}"
- start = Time.now
- result = exec_query(sql, 'EXPLAIN', binds)
- elapsed = Time.now - start
-
- ExplainPrettyPrinter.new.pp(result, elapsed)
+ def discard! # :nodoc:
+ @connection.automatic_close = false
+ @connection = nil
end
- class ExplainPrettyPrinter # :nodoc:
- # Pretty prints the result of a EXPLAIN in a way that resembles the output of the
- # MySQL shell:
- #
- # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
- # | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
- # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
- # | 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | |
- # | 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where |
- # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
- # 2 rows in set (0.00 sec)
- #
- # This is an exercise in Ruby hyperrealism :).
- def pp(result, elapsed)
- widths = compute_column_widths(result)
- separator = build_separator(widths)
-
- pp = []
-
- pp << separator
- pp << build_cells(result.columns, widths)
- pp << separator
-
- result.rows.each do |row|
- pp << build_cells(row, widths)
- end
-
- pp << separator
- pp << build_footer(result.rows.length, elapsed)
-
- pp.join("\n") + "\n"
- end
-
- private
-
- def compute_column_widths(result)
- [].tap do |widths|
- result.columns.each_with_index do |column, i|
- cells_in_column = [column] + result.rows.map {|r| r[i].nil? ? 'NULL' : r[i].to_s}
- widths << cells_in_column.map(&:length).max
- end
- end
- end
-
- def build_separator(widths)
- padding = 1
- '+' + widths.map {|w| '-' * (w + (padding*2))}.join('+') + '+'
- end
+ private
- def build_cells(items, widths)
- cells = []
- items.each_with_index do |item, i|
- item = 'NULL' if item.nil?
- justifier = item.is_a?(Numeric) ? 'rjust' : 'ljust'
- cells << item.to_s.send(justifier, widths[i])
- end
- '| ' + cells.join(' | ') + ' |'
+ def connect
+ @connection = Mysql2::Client.new(@config)
+ configure_connection
end
- def build_footer(nrows, elapsed)
- rows_label = nrows == 1 ? 'row' : 'rows'
- "#{nrows} #{rows_label} in set (%.2f sec)" % elapsed
+ def configure_connection
+ @connection.query_options[:as] = :array
+ super
end
- end
- # FIXME: re-enable the following once a "better" query_cache solution is in core
- #
- # The overrides below perform much better than the originals in AbstractAdapter
- # because we're able to take advantage of mysql2's lazy-loading capabilities
- #
- # # Returns a record hash with the column names as keys and column values
- # # as values.
- # def select_one(sql, name = nil)
- # result = execute(sql, name)
- # result.each(as: :hash) do |r|
- # return r
- # end
- # end
- #
- # # Returns a single value from a record
- # def select_value(sql, name = nil)
- # result = execute(sql, name)
- # if first = result.first
- # first.first
- # end
- # end
- #
- # # 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(sql, name = nil)
- # execute(sql, name).map { |row| row.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 = [])
- execute(sql, name).to_a
- end
-
- # Executes the SQL statement in the context of this connection.
- def execute(sql, name = nil)
- if @connection
- # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
- # made since we established the connection
- @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone
+ def full_version
+ @full_version ||= @connection.server_info[:version]
end
-
- super
- end
-
- def exec_query(sql, name = 'SQL', binds = [])
- result = execute(sql, name)
- ActiveRecord::Result.new(result.fields, result.to_a)
- end
-
- alias exec_without_stmt exec_query
-
- def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
- super
- id_value || @connection.last_id
- end
- alias :create :insert_sql
-
- def exec_insert(sql, name, binds, pk = nil, sequence_name = nil)
- execute to_sql(sql, binds), name
- end
-
- def exec_delete(sql, name, binds)
- execute to_sql(sql, binds), name
- @connection.affected_rows
- end
- alias :exec_update :exec_delete
-
- def last_inserted_id(result)
- @connection.last_id
- end
-
- private
-
- def connect
- @connection = Mysql2::Client.new(@config)
- configure_connection
- end
-
- def configure_connection
- @connection.query_options.merge!(:as => :array)
- super
- end
-
- def full_version
- @full_version ||= @connection.info[:version]
- end
-
- def set_field_encoding field_name
- field_name
- end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
deleted file mode 100644
index 2ae462d773..0000000000
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ /dev/null
@@ -1,468 +0,0 @@
-require 'active_record/connection_adapters/abstract_mysql_adapter'
-require 'active_record/connection_adapters/statement_pool'
-require 'active_support/core_ext/hash/keys'
-
-gem 'mysql', '~> 2.9'
-require 'mysql'
-
-class Mysql # :nodoc: all
- class Time
- # Used for casting DateTime fields to a MySQL friendly Time.
- # This was documented in 48498da0dfed5239ea1eafb243ce47d7e3ce9e8e
- def to_date
- Date.new(year, month, day)
- end
- end
- class Stmt; include Enumerable end
- class Result; include Enumerable end
-end
-
-module ActiveRecord
- module ConnectionHandling # :nodoc:
- # Establishes a connection to the database that's used by all Active Record objects.
- def mysql_connection(config)
- config = config.symbolize_keys
- host = config[:host]
- port = config[:port]
- socket = config[:socket]
- username = config[:username] ? config[:username].to_s : 'root'
- password = config[:password].to_s
- database = config[:database]
-
- mysql = Mysql.init
- mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslca] || config[:sslkey]
-
- default_flags = Mysql.const_defined?(:CLIENT_MULTI_RESULTS) ? Mysql::CLIENT_MULTI_RESULTS : 0
- default_flags |= Mysql::CLIENT_FOUND_ROWS if Mysql.const_defined?(:CLIENT_FOUND_ROWS)
- options = [host, username, password, database, port, socket, default_flags]
- ConnectionAdapters::MysqlAdapter.new(mysql, logger, options, config)
- rescue Mysql::Error => error
- if error.message.include?("Unknown database")
- raise ActiveRecord::NoDatabaseError.new(error.message, error)
- else
- raise
- end
- end
- end
-
- module ConnectionAdapters
- # The MySQL adapter will work with both Ruby/MySQL, which is a Ruby-based MySQL adapter that comes bundled with Active Record, and with
- # the faster C-based MySQL/Ruby adapter (available both as a gem and from http://www.tmtm.org/en/mysql/ruby/).
- #
- # Options:
- #
- # * <tt>:host</tt> - Defaults to "localhost".
- # * <tt>:port</tt> - Defaults to 3306.
- # * <tt>:socket</tt> - Defaults to "/tmp/mysql.sock".
- # * <tt>:username</tt> - Defaults to "root"
- # * <tt>:password</tt> - Defaults to nothing.
- # * <tt>:database</tt> - The name of the database. No default, must be provided.
- # * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection.
- # * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.6/en/auto-reconnect.html).
- # * <tt>:strict</tt> - Defaults to true. Enable STRICT_ALL_TABLES. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.6/en/sql-mode.html)
- # * <tt>:variables</tt> - (Optional) A hash session variables to send as <tt>SET @@SESSION.key = value</tt> on each database connection. Use the value +:default+ to set a variable to its DEFAULT value. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.6/en/set-statement.html).
- # * <tt>:sslca</tt> - Necessary to use MySQL with an SSL connection.
- # * <tt>:sslkey</tt> - Necessary to use MySQL with an SSL connection.
- # * <tt>:sslcert</tt> - Necessary to use MySQL with an SSL connection.
- # * <tt>:sslcapath</tt> - Necessary to use MySQL with an SSL connection.
- # * <tt>:sslcipher</tt> - Necessary to use MySQL with an SSL connection.
- #
- class MysqlAdapter < AbstractMysqlAdapter
- ADAPTER_NAME = 'MySQL'.freeze
-
- class StatementPool < ConnectionAdapters::StatementPool
- private
-
- def dealloc(stmt)
- stmt[:stmt].close
- end
- end
-
- def initialize(connection, logger, connection_options, config)
- super
- @statements = StatementPool.new(@connection,
- self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }))
- @client_encoding = nil
- connect
- end
-
- # Returns true, since this connection adapter supports prepared statement
- # caching.
- def supports_statement_cache?
- true
- end
-
- # HELPER METHODS ===========================================
-
- def each_hash(result) # :nodoc:
- if block_given?
- result.each_hash do |row|
- row.symbolize_keys!
- yield row
- end
- else
- to_enum(:each_hash, result)
- end
- end
-
- def error_number(exception) # :nodoc:
- exception.errno if exception.respond_to?(:errno)
- end
-
- # QUOTING ==================================================
-
- def quote_string(string) #:nodoc:
- @connection.quote(string)
- end
-
- #--
- # CONNECTION MANAGEMENT ====================================
- #++
-
- def active?
- if @connection.respond_to?(:stat)
- @connection.stat
- else
- @connection.query 'select 1'
- end
-
- # mysql-ruby doesn't raise an exception when stat fails.
- if @connection.respond_to?(:errno)
- @connection.errno.zero?
- else
- true
- end
- rescue Mysql::Error
- false
- end
-
- def reconnect!
- super
- disconnect!
- connect
- end
-
- # Disconnects from the database if already connected. Otherwise, this
- # method does nothing.
- def disconnect!
- super
- @connection.close rescue nil
- end
-
- def reset!
- if @connection.respond_to?(:change_user)
- # See http://bugs.mysql.com/bug.php?id=33540 -- the workaround way to
- # reset the connection is to change the user to the same user.
- @connection.change_user(@config[:username], @config[:password], @config[:database])
- configure_connection
- end
- end
-
- #--
- # DATABASE STATEMENTS ======================================
- #++
-
- def select_rows(sql, name = nil, binds = [])
- @connection.query_with_result = true
- rows = exec_query(sql, name, binds).rows
- @connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped
- rows
- end
-
- # Clears the prepared statements cache.
- def clear_cache!
- super
- @statements.clear
- end
-
- # Taken from here:
- # https://github.com/tmtm/ruby-mysql/blob/master/lib/mysql/charset.rb
- # Author: TOMITA Masahiro <tommy@tmtm.org>
- ENCODINGS = {
- "armscii8" => nil,
- "ascii" => Encoding::US_ASCII,
- "big5" => Encoding::Big5,
- "binary" => Encoding::ASCII_8BIT,
- "cp1250" => Encoding::Windows_1250,
- "cp1251" => Encoding::Windows_1251,
- "cp1256" => Encoding::Windows_1256,
- "cp1257" => Encoding::Windows_1257,
- "cp850" => Encoding::CP850,
- "cp852" => Encoding::CP852,
- "cp866" => Encoding::IBM866,
- "cp932" => Encoding::Windows_31J,
- "dec8" => nil,
- "eucjpms" => Encoding::EucJP_ms,
- "euckr" => Encoding::EUC_KR,
- "gb2312" => Encoding::EUC_CN,
- "gbk" => Encoding::GBK,
- "geostd8" => nil,
- "greek" => Encoding::ISO_8859_7,
- "hebrew" => Encoding::ISO_8859_8,
- "hp8" => nil,
- "keybcs2" => nil,
- "koi8r" => Encoding::KOI8_R,
- "koi8u" => Encoding::KOI8_U,
- "latin1" => Encoding::ISO_8859_1,
- "latin2" => Encoding::ISO_8859_2,
- "latin5" => Encoding::ISO_8859_9,
- "latin7" => Encoding::ISO_8859_13,
- "macce" => Encoding::MacCentEuro,
- "macroman" => Encoding::MacRoman,
- "sjis" => Encoding::SHIFT_JIS,
- "swe7" => nil,
- "tis620" => Encoding::TIS_620,
- "ucs2" => Encoding::UTF_16BE,
- "ujis" => Encoding::EucJP_ms,
- "utf8" => Encoding::UTF_8,
- "utf8mb4" => Encoding::UTF_8,
- }
-
- # Get the client encoding for this database
- def client_encoding
- return @client_encoding if @client_encoding
-
- result = exec_query(
- "SHOW VARIABLES WHERE Variable_name = 'character_set_client'",
- 'SCHEMA')
- @client_encoding = ENCODINGS[result.rows.last.last]
- end
-
- def exec_query(sql, name = 'SQL', binds = [])
- if without_prepared_statement?(binds)
- result_set, affected_rows = exec_without_stmt(sql, name)
- else
- result_set, affected_rows = exec_stmt(sql, name, binds)
- end
-
- yield affected_rows if block_given?
-
- result_set
- end
-
- def last_inserted_id(result)
- @connection.insert_id
- end
-
- module Fields # :nodoc:
- class DateTime < Type::DateTime # :nodoc:
- def cast_value(value)
- if Mysql::Time === value
- new_time(
- value.year,
- value.month,
- value.day,
- value.hour,
- value.minute,
- value.second,
- value.second_part)
- else
- super
- end
- end
- end
-
- class Time < Type::Time # :nodoc:
- def cast_value(value)
- if Mysql::Time === value
- new_time(
- 2000,
- 01,
- 01,
- value.hour,
- value.minute,
- value.second,
- value.second_part)
- else
- super
- end
- end
- end
-
- class << self
- TYPES = Type::HashLookupTypeMap.new # :nodoc:
-
- delegate :register_type, :alias_type, to: :TYPES
-
- def find_type(field)
- if field.type == Mysql::Field::TYPE_TINY && field.length > 1
- TYPES.lookup(Mysql::Field::TYPE_LONG)
- else
- TYPES.lookup(field.type)
- end
- end
- end
-
- register_type Mysql::Field::TYPE_TINY, Type::Boolean.new
- register_type Mysql::Field::TYPE_LONG, Type::Integer.new
- alias_type Mysql::Field::TYPE_LONGLONG, Mysql::Field::TYPE_LONG
- alias_type Mysql::Field::TYPE_NEWDECIMAL, Mysql::Field::TYPE_LONG
-
- register_type Mysql::Field::TYPE_DATE, Type::Date.new
- register_type Mysql::Field::TYPE_DATETIME, Fields::DateTime.new
- register_type Mysql::Field::TYPE_TIME, Fields::Time.new
- register_type Mysql::Field::TYPE_FLOAT, Type::Float.new
- end
-
- def initialize_type_map(m) # :nodoc:
- super
- register_class_with_precision m, %r(datetime)i, Fields::DateTime
- register_class_with_precision m, %r(time)i, Fields::Time
- end
-
- def exec_without_stmt(sql, name = 'SQL') # :nodoc:
- # Some queries, like SHOW CREATE TABLE don't work through the prepared
- # statement API. For those queries, we need to use this method. :'(
- log(sql, name) do
- result = @connection.query(sql)
- affected_rows = @connection.affected_rows
-
- if result
- types = {}
- fields = []
- result.fetch_fields.each { |field|
- field_name = field.name
- fields << field_name
-
- if field.decimals > 0
- types[field_name] = Type::Decimal.new
- else
- types[field_name] = Fields.find_type field
- end
- }
-
- result_set = ActiveRecord::Result.new(fields, result.to_a, types)
- result.free
- else
- result_set = ActiveRecord::Result.new([], [])
- end
-
- [result_set, affected_rows]
- end
- end
-
- def execute_and_free(sql, name = nil) # :nodoc:
- result = execute(sql, name)
- ret = yield result
- result.free
- ret
- end
-
- def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
- super sql, name
- id_value || @connection.insert_id
- end
- alias :create :insert_sql
-
- def exec_delete(sql, name, binds) # :nodoc:
- affected_rows = 0
-
- exec_query(sql, name, binds) do |n|
- affected_rows = n
- end
-
- affected_rows
- end
- alias :exec_update :exec_delete
-
- def begin_db_transaction #:nodoc:
- exec_query "BEGIN"
- end
-
- private
-
- def exec_stmt(sql, name, binds)
- cache = {}
- type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) }
-
- log(sql, name, binds) do
- if binds.empty?
- stmt = @connection.prepare(sql)
- else
- cache = @statements[sql] ||= {
- :stmt => @connection.prepare(sql)
- }
- stmt = cache[:stmt]
- end
-
- begin
- stmt.execute(*type_casted_binds)
- rescue Mysql::Error => e
- # Older versions of MySQL leave the prepared statement in a bad
- # place when an error occurs. To support older MySQL versions, we
- # need to close the statement and delete the statement from the
- # cache.
- if binds.empty?
- stmt.close
- else
- @statements.delete sql
- end
- raise e
- end
-
- cols = nil
- if metadata = stmt.result_metadata
- cols = cache[:cols] ||= metadata.fetch_fields.map(&:name)
- metadata.free
- end
-
- result_set = ActiveRecord::Result.new(cols, stmt.to_a) if cols
- affected_rows = stmt.affected_rows
-
- stmt.free_result
- stmt.close if binds.empty?
-
- [result_set, affected_rows]
- end
- end
-
- def connect
- encoding = @config[:encoding]
- if encoding
- @connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil
- end
-
- if @config[:sslca] || @config[:sslkey]
- @connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher])
- end
-
- @connection.options(Mysql::OPT_CONNECT_TIMEOUT, @config[:connect_timeout]) if @config[:connect_timeout]
- @connection.options(Mysql::OPT_READ_TIMEOUT, @config[:read_timeout]) if @config[:read_timeout]
- @connection.options(Mysql::OPT_WRITE_TIMEOUT, @config[:write_timeout]) if @config[:write_timeout]
-
- @connection.real_connect(*@connection_options)
-
- # reconnect must be set after real_connect is called, because real_connect sets it to false internally
- @connection.reconnect = !!@config[:reconnect] if @connection.respond_to?(:reconnect=)
-
- configure_connection
- end
-
- # Many Rails applications monkey-patch a replacement of the configure_connection method
- # and don't call 'super', so leave this here even though it looks superfluous.
- def configure_connection
- super
- end
-
- def select(sql, name = nil, binds = [])
- @connection.query_with_result = true
- rows = super
- @connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped
- rows
- end
-
- # Returns the full version of the connected MySQL server.
- def full_version
- @full_version ||= @connection.server_info
- end
-
- def set_field_encoding field_name
- field_name.force_encoding(client_encoding)
- if internal_enc = Encoding.default_internal
- field_name = field_name.encode!(internal_enc)
- end
- field_name
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
index bfa03fa136..3ccc7271ab 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
# PostgreSQL-specific extensions to column definitions in a table.
@@ -5,12 +7,37 @@ module ActiveRecord
delegate :array, :oid, :fmod, to: :sql_type_metadata
alias :array? :array
+ def initialize(*, max_identifier_length: 63, **)
+ super
+ @max_identifier_length = max_identifier_length
+ end
+
def serial?
return unless default_function
- table_name = @table_name || '(?<table_name>.+)'
- %r{\Anextval\('"?#{table_name}_#{name}_seq"?'::regclass\)\z} === default_function
+ if %r{\Anextval\('"?(?<sequence_name>.+_(?<suffix>seq\d*))"?'::regclass\)\z} =~ default_function
+ sequence_name_from_parts(table_name, name, suffix) == sequence_name
+ end
end
+
+ private
+ attr_reader :max_identifier_length
+
+ def sequence_name_from_parts(table_name, column_name, suffix)
+ over_length = [table_name, column_name, suffix].map(&:length).sum + 2 - max_identifier_length
+
+ if over_length > 0
+ column_name_length = [(max_identifier_length - suffix.length - 2) / 2, column_name.length].min
+ over_length -= column_name.length - column_name_length
+ column_name = column_name[0, column_name_length - [over_length, 0].min]
+ end
+
+ if over_length > 0
+ table_name = table_name[0, table_name.length - over_length]
+ end
+
+ "#{table_name}_#{column_name}_#{suffix}"
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
index 11d3f5301a..6bd6b67165 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
@@ -1,97 +1,12 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module DatabaseStatements
def explain(arel, binds = [])
sql = "EXPLAIN #{to_sql(arel, binds)}"
- ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds))
- end
-
- class ExplainPrettyPrinter # :nodoc:
- # Pretty prints the result of a EXPLAIN in a way that resembles the output of the
- # PostgreSQL shell:
- #
- # QUERY PLAN
- # ------------------------------------------------------------------------------
- # Nested Loop Left Join (cost=0.00..37.24 rows=8 width=0)
- # Join Filter: (posts.user_id = users.id)
- # -> Index Scan using users_pkey on users (cost=0.00..8.27 rows=1 width=4)
- # Index Cond: (id = 1)
- # -> Seq Scan on posts (cost=0.00..28.88 rows=8 width=4)
- # Filter: (posts.user_id = 1)
- # (6 rows)
- #
- def pp(result)
- header = result.columns.first
- lines = result.rows.map(&:first)
-
- # We add 2 because there's one char of padding at both sides, note
- # the extra hyphens in the example above.
- width = [header, *lines].map(&:length).max + 2
-
- pp = []
-
- pp << header.center(width).rstrip
- pp << '-' * width
-
- pp += lines.map {|line| " #{line}"}
-
- nrows = result.rows.length
- rows_label = nrows == 1 ? 'row' : 'rows'
- pp << "(#{nrows} #{rows_label})"
-
- pp.join("\n") + "\n"
- end
- 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|
- result.getvalue(0, 0) if result.ntuples > 0 && result.nfields > 0
- end
- end
-
- def select_values(arel, name = nil)
- arel, binds = binds_from_relation arel, []
- sql = to_sql(arel, binds)
- execute_and_clear(sql, name, binds) do |result|
- if result.nfields > 0
- result.column_values(0)
- else
- []
- end
- end
- end
-
- # 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|
- result.values
- end
- end
-
- # Executes an INSERT query and returns the new record's ID
- def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
- unless pk
- # Extract the table from the insert sql. Yuck.
- table_ref = extract_table_ref_from_insert_sql(sql)
- pk = primary_key(table_ref) if table_ref
- end
-
- if pk && use_insert_returning?
- select_value("#{sql} RETURNING #{quote_column_name(pk)}")
- elsif pk
- super
- last_insert_id_value(sequence_name || default_sequence_name(table_ref, pk))
- else
- super
- end
- end
-
- def create
- super.insert
+ PostgreSQL::ExplainPrettyPrinter.new.pp(exec_query(sql, "EXPLAIN", binds))
end
# The internal PostgreSQL identifier of the money data type.
@@ -133,9 +48,9 @@ module ActiveRecord
# (2) $12.345.678,12
case data
when /^-?\D+[\d,]+\.\d{2}$/ # (1)
- data.gsub!(/[^-\d.]/, '')
+ data.gsub!(/[^-\d.]/, "")
when /^-?\D+[\d.]+,\d{2}$/ # (2)
- data.gsub!(/[^-\d,]/, '').sub!(/,/, '.')
+ data.gsub!(/[^-\d,]/, "").sub!(/,/, ".")
end
end
end
@@ -143,21 +58,31 @@ module ActiveRecord
# Queries the database and returns the results in an Array-like object
def query(sql, name = nil) #:nodoc:
+ materialize_transactions
+
log(sql, name) do
- result_as_array @connection.async_exec(sql)
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
+ result_as_array @connection.async_exec(sql)
+ end
end
end
- # Executes an SQL statement, returning a PGresult object on success
- # or raising a PGError exception otherwise.
+ # Executes an SQL statement, returning a PG::Result object on success
+ # or raising a PG::Error exception otherwise.
+ # Note: the PG::Result object is manually memory managed; if you don't
+ # need it specifically, you may want consider the <tt>exec_query</tt> wrapper.
def execute(sql, name = nil)
+ materialize_transactions
+
log(sql, name) do
- @connection.async_exec(sql)
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
+ @connection.async_exec(sql)
+ end
end
end
- def exec_query(sql, name = 'SQL', binds = [])
- execute_and_clear(sql, name, binds) do |result|
+ def exec_query(sql, name = "SQL", binds = [], prepare: false)
+ execute_and_clear(sql, name, binds, prepare: prepare) do |result|
types = {}
fields = result.fields
fields.each_with_index do |fname, i|
@@ -169,44 +94,44 @@ module ActiveRecord
end
end
- def exec_delete(sql, name = 'SQL', binds = [])
- execute_and_clear(sql, name, binds) {|result| result.cmd_tuples }
+ def exec_delete(sql, name = nil, binds = [])
+ execute_and_clear(sql, name, binds) { |result| result.cmd_tuples }
end
alias :exec_update :exec_delete
- def sql_for_insert(sql, pk, id_value, sequence_name, binds)
- unless pk
+ def sql_for_insert(sql, pk, id_value, sequence_name, binds) # :nodoc:
+ if pk.nil?
# Extract the table from the insert sql. Yuck.
table_ref = extract_table_ref_from_insert_sql(sql)
pk = primary_key(table_ref) if table_ref
end
- if pk && use_insert_returning?
+ if pk = suppress_composite_primary_key(pk)
sql = "#{sql} RETURNING #{quote_column_name(pk)}"
end
- [sql, binds]
+ super
end
+ private :sql_for_insert
- def exec_insert(sql, name, binds, pk = nil, sequence_name = nil)
- val = exec_query(sql, name, binds)
- if !use_insert_returning? && pk
+ def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil)
+ if use_insert_returning? || pk == false
+ super
+ else
+ result = exec_query(sql, name, binds)
unless sequence_name
table_ref = extract_table_ref_from_insert_sql(sql)
- sequence_name = default_sequence_name(table_ref, pk)
- return val unless sequence_name
+ if table_ref
+ pk = primary_key(table_ref) if pk.nil?
+ pk = suppress_composite_primary_key(pk)
+ sequence_name = default_sequence_name(table_ref, pk)
+ end
+ return result unless sequence_name
end
last_insert_id_result(sequence_name)
- else
- val
end
end
- # Executes an UPDATE query and returns the number of affected tuples.
- def update_sql(sql, name = nil)
- super.cmd_tuples
- end
-
# Begins a transaction.
def begin_db_transaction
execute "BEGIN"
@@ -226,6 +151,16 @@ module ActiveRecord
def exec_rollback_db_transaction
execute "ROLLBACK"
end
+
+ private
+ # Returns the current ID of a table's sequence.
+ def last_insert_id_result(sequence_name)
+ exec_query("SELECT currval(#{quote(sequence_name)})", "SQL")
+ end
+
+ def suppress_composite_primary_key(pk)
+ pk unless pk.is_a?(Array)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/explain_pretty_printer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/explain_pretty_printer.rb
new file mode 100644
index 0000000000..086a5dcc15
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/explain_pretty_printer.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ class ExplainPrettyPrinter # :nodoc:
+ # Pretty prints the result of an EXPLAIN in a way that resembles the output of the
+ # PostgreSQL shell:
+ #
+ # QUERY PLAN
+ # ------------------------------------------------------------------------------
+ # Nested Loop Left Join (cost=0.00..37.24 rows=8 width=0)
+ # Join Filter: (posts.user_id = users.id)
+ # -> Index Scan using users_pkey on users (cost=0.00..8.27 rows=1 width=4)
+ # Index Cond: (id = 1)
+ # -> Seq Scan on posts (cost=0.00..28.88 rows=8 width=4)
+ # Filter: (posts.user_id = 1)
+ # (6 rows)
+ #
+ def pp(result)
+ header = result.columns.first
+ lines = result.rows.map(&:first)
+
+ # We add 2 because there's one char of padding at both sides, note
+ # the extra hyphens in the example above.
+ width = [header, *lines].map(&:length).max + 2
+
+ pp = []
+
+ pp << header.center(width).rstrip
+ pp << "-" * width
+
+ pp += lines.map { |line| " #{line}" }
+
+ nrows = result.rows.length
+ rows_label = nrows == 1 ? "row" : "rows"
+ pp << "(#{nrows} #{rows_label})"
+
+ pp.join("\n") + "\n"
+ 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 68752cdd80..247a25054e 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
@@ -1,25 +1,28 @@
-require 'active_record/connection_adapters/postgresql/oid/array'
-require 'active_record/connection_adapters/postgresql/oid/bit'
-require 'active_record/connection_adapters/postgresql/oid/bit_varying'
-require 'active_record/connection_adapters/postgresql/oid/bytea'
-require 'active_record/connection_adapters/postgresql/oid/cidr'
-require 'active_record/connection_adapters/postgresql/oid/date_time'
-require 'active_record/connection_adapters/postgresql/oid/decimal'
-require 'active_record/connection_adapters/postgresql/oid/enum'
-require 'active_record/connection_adapters/postgresql/oid/hstore'
-require 'active_record/connection_adapters/postgresql/oid/inet'
-require 'active_record/connection_adapters/postgresql/oid/json'
-require 'active_record/connection_adapters/postgresql/oid/jsonb'
-require 'active_record/connection_adapters/postgresql/oid/money'
-require 'active_record/connection_adapters/postgresql/oid/point'
-require 'active_record/connection_adapters/postgresql/oid/rails_5_1_point'
-require 'active_record/connection_adapters/postgresql/oid/range'
-require 'active_record/connection_adapters/postgresql/oid/specialized_string'
-require 'active_record/connection_adapters/postgresql/oid/uuid'
-require 'active_record/connection_adapters/postgresql/oid/vector'
-require 'active_record/connection_adapters/postgresql/oid/xml'
+# frozen_string_literal: true
-require 'active_record/connection_adapters/postgresql/oid/type_map_initializer'
+require "active_record/connection_adapters/postgresql/oid/array"
+require "active_record/connection_adapters/postgresql/oid/bit"
+require "active_record/connection_adapters/postgresql/oid/bit_varying"
+require "active_record/connection_adapters/postgresql/oid/bytea"
+require "active_record/connection_adapters/postgresql/oid/cidr"
+require "active_record/connection_adapters/postgresql/oid/date"
+require "active_record/connection_adapters/postgresql/oid/date_time"
+require "active_record/connection_adapters/postgresql/oid/decimal"
+require "active_record/connection_adapters/postgresql/oid/enum"
+require "active_record/connection_adapters/postgresql/oid/hstore"
+require "active_record/connection_adapters/postgresql/oid/inet"
+require "active_record/connection_adapters/postgresql/oid/jsonb"
+require "active_record/connection_adapters/postgresql/oid/money"
+require "active_record/connection_adapters/postgresql/oid/oid"
+require "active_record/connection_adapters/postgresql/oid/point"
+require "active_record/connection_adapters/postgresql/oid/legacy_point"
+require "active_record/connection_adapters/postgresql/oid/range"
+require "active_record/connection_adapters/postgresql/oid/specialized_string"
+require "active_record/connection_adapters/postgresql/oid/uuid"
+require "active_record/connection_adapters/postgresql/oid/vector"
+require "active_record/connection_adapters/postgresql/oid/xml"
+
+require "active_record/connection_adapters/postgresql/oid/type_map_initializer"
module ActiveRecord
module ConnectionAdapters
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 25961a9869..6fbeaa2b9e 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
@@ -5,10 +7,12 @@ 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 = ',')
+ def initialize(subtype, delimiter = ",")
@subtype = subtype
@delimiter = delimiter
@@ -17,8 +21,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
@@ -26,14 +33,21 @@ module ActiveRecord
def cast(value)
if value.is_a?(::String)
- value = @pg_decoder.decode(value)
+ value = begin
+ @pg_decoder.decode(value)
+ rescue TypeError
+ # malformed array string is treated as [], will raise in PG 2.0 gem
+ # this keeps a consistent implementation
+ []
+ end
end
type_cast_array(value, :cast)
end
def serialize(value)
if value.is_a?(::Array)
- @pg_encoder.encode(type_cast_array(value, :serialize))
+ casted_values = type_cast_array(value, :serialize)
+ Data.new(@pg_encoder, casted_values)
else
super
end
@@ -50,15 +64,27 @@ module ActiveRecord
"[" + value.map { |v| subtype.type_cast_for_schema(v) }.join(", ") + "]"
end
+ def map(value, &block)
+ value.map(&block)
+ end
+
+ def changed_in_place?(raw_old_value, new_value)
+ deserialize(raw_old_value) != new_value
+ end
+
+ def force_equality?(value)
+ value.is_a?(::Array)
+ end
+
private
- def type_cast_array(value, method)
- if value.is_a?(::Array)
- value.map { |item| type_cast_array(item, method) }
- else
- @subtype.public_send(method, value)
+ def type_cast_array(value, method)
+ if value.is_a?(::Array)
+ value.map { |item| type_cast_array(item, method) }
+ else
+ @subtype.public_send(method, value)
+ end
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 ea0fa2517f..e9a79526f9 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
@@ -7,7 +9,7 @@ module ActiveRecord
:bit
end
- def cast(value)
+ def cast_value(value)
if ::String === value
case value
when /^0x/i
@@ -16,7 +18,7 @@ module ActiveRecord
value # Bit-string notation
end
else
- value
+ value.to_s
end
end
@@ -34,16 +36,15 @@ module ActiveRecord
end
def binary?
- /\A[01]*\Z/ === value
+ /\A[01]*\Z/.match?(value)
end
def hex?
- /\A[0-9A-F]*\Z/i === value
+ /\A[0-9A-F]*\Z/i.match?(value)
end
- protected
-
- attr_reader :value
+ private
+ attr_reader :value
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb
index 4c21097d48..dc7079dda2 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
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..a3c60ecef6 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
@@ -6,7 +8,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/cidr.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb
index eeccb09bdf..66e99d9404 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb
@@ -1,3 +1,7 @@
+# frozen_string_literal: true
+
+require "ipaddr"
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb
new file mode 100644
index 0000000000..24a1daa95a
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Date < Type::Date # :nodoc:
+ def cast_value(value)
+ case value
+ when "infinity" then ::Float::INFINITY
+ when "-infinity" then -::Float::INFINITY
+ when / BC$/
+ astronomical_year = format("%04d", -value[/^\d+/].to_i + 1)
+ super(value.sub(/ BC$/, "").sub(/^\d+/, astronomical_year))
+ else
+ super
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb
index 2c04c46131..cd667422f5 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb
@@ -1,21 +1,19 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
class DateTime < Type::DateTime # :nodoc:
def cast_value(value)
- if value.is_a?(::String)
- case value
- when 'infinity' then ::Float::INFINITY
- when '-infinity' then -::Float::INFINITY
- when / BC$/
- astronomical_year = format("%04d", -value[/^\d+/].to_i + 1)
- super(value.sub(/ BC$/, "").sub(/^\d+/, astronomical_year))
- else
- super
- end
+ case value
+ when "infinity" then ::Float::INFINITY
+ when "-infinity" then -::Float::INFINITY
+ when / BC$/
+ astronomical_year = format("%04d", -value[/^\d+/].to_i + 1)
+ super(value.sub(/ BC$/, "").sub(/^\d+/, astronomical_year))
else
- value
+ super
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb
index 43d22c8daf..e7d33855c4 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb
@@ -1,10 +1,12 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
class Decimal < Type::Decimal # :nodoc:
def infinity(options = {})
- BigDecimal.new("Infinity") * (options[:negative] ? -1 : 1)
+ BigDecimal("Infinity") * (options[:negative] ? -1 : 1)
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb
index 91d339f32c..f70f09ad95 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
@@ -9,9 +11,9 @@ module ActiveRecord
private
- def cast_value(value)
- value.to_s
- end
+ def cast_value(value)
+ value.to_s
+ end
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 9270fc9f21..aabe83b85d 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
@@ -12,8 +14,8 @@ module ActiveRecord
def deserialize(value)
if value.is_a?(::String)
::Hash[value.scan(HstorePair).map { |k, v|
- v = v.upcase == 'NULL' ? nil : v.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1')
- k = k.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1')
+ v = v.upcase == "NULL" ? nil : v.gsub(/\A"(.*)"\Z/m, '\1').gsub(/\\(.)/, '\1')
+ k = k.gsub(/\A"(.*)"\Z/m, '\1').gsub(/\\(.)/, '\1')
[k, v]
}]
else
@@ -23,7 +25,9 @@ module ActiveRecord
def serialize(value)
if value.is_a?(::Hash)
- value.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(', ')
+ value.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(", ")
+ elsif value.respond_to?(:to_unsafe_h)
+ serialize(value.to_unsafe_h)
else
value
end
@@ -33,25 +37,33 @@ 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
- quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/
- unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/
- /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/
- end
+ HstorePair = begin
+ quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/
+ unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/
+ /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/
+ end
- def escape_hstore(value)
- if value.nil?
- 'NULL'
- else
- if value == ""
- '""'
+ def escape_hstore(value)
+ if value.nil?
+ "NULL"
else
- '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1')
+ if value == ""
+ '""'
+ else
+ '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1')
+ end
end
end
- end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb
index 96486fa65b..55be71fd26 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb
deleted file mode 100644
index 8e1256baad..0000000000
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-module ActiveRecord
- module ConnectionAdapters
- module PostgreSQL
- module OID # :nodoc:
- class Json < Type::Value # :nodoc:
- include Type::Helpers::Mutable
-
- def type
- :json
- end
-
- def deserialize(value)
- if value.is_a?(::String)
- ::ActiveSupport::JSON.decode(value) rescue nil
- else
- value
- end
- end
-
- def serialize(value)
- if value.is_a?(::Array) || value.is_a?(::Hash)
- ::ActiveSupport::JSON.encode(value)
- else
- value
- end
- end
-
- def accessor
- ActiveRecord::Store::StringKeyedHashAccessor
- end
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb
index 87391b5dc7..e0216f1089 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb
@@ -1,21 +1,13 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Jsonb < Json # :nodoc:
+ class Jsonb < Type::Json # :nodoc:
def type
:jsonb
end
-
- def changed_in_place?(raw_old_value, new_value)
- # Postgres does not preserve insignificant whitespaces when
- # round-tripping jsonb columns. This causes some false positives for
- # the comparison here. Therefore, we need to parse and re-dump the
- # raw value here to ensure the insignificant whitespaces are
- # consistent with our encoder's output.
- raw_old_value = serialize(deserialize(raw_old_value))
- super(raw_old_value, new_value)
- end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb
new file mode 100644
index 0000000000..7b057a8452
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class LegacyPoint < Type::Value # :nodoc:
+ include Type::Helpers::Mutable
+
+ def type
+ :point
+ end
+
+ def cast(value)
+ case value
+ when ::String
+ if value[0] == "(" && value[-1] == ")"
+ value = value[1...-1]
+ end
+ cast(value.split(","))
+ when ::Array
+ value.map { |v| Float(v) }
+ else
+ value
+ end
+ end
+
+ def serialize(value)
+ if value.is_a?(::Array)
+ "(#{number_for_point(value[0])},#{number_for_point(value[1])})"
+ else
+ super
+ end
+ end
+
+ private
+
+ def number_for_point(number)
+ number.to_s.gsub(/\.0$/, "")
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb
index 2163674019..6434377b57 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb
@@ -1,10 +1,10 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
class Money < Type::Decimal # :nodoc:
- class_attribute :precision
-
def type
:money
end
@@ -24,12 +24,12 @@ module ActiveRecord
# (3) -$2.55
# (4) ($2.55)
- value.sub!(/^\((.+)\)$/, '-\1') # (4)
+ value = value.sub(/^\((.+)\)$/, '-\1') # (4)
case value
when /^-?\D+[\d,]+\.\d{2}$/ # (1)
- value.gsub!(/[^-\d.]/, '')
+ value.gsub!(/[^-\d.]/, "")
when /^-?\D+[\d.]+,\d{2}$/ # (2)
- value.gsub!(/[^-\d,]/, '').sub!(/,/, '.')
+ value.gsub!(/[^-\d,]/, "").sub!(/,/, ".")
end
super(value)
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..d8c044320d
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/oid.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+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/point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
index bf565bcf47..02a9c506f6 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
@@ -1,4 +1,8 @@
+# frozen_string_literal: true
+
module ActiveRecord
+ Point = Struct.new(:x, :y)
+
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
@@ -12,20 +16,34 @@ module ActiveRecord
def cast(value)
case value
when ::String
- if value[0] == '(' && value[-1] == ')'
+ return if value.blank?
+
+ if value[0] == "(" && value[-1] == ")"
value = value[1...-1]
end
- cast(value.split(','))
+ x, y = value.split(",")
+ build_point(x, y)
when ::Array
- value.map { |v| Float(v) }
+ build_point(*value)
else
value
end
end
def serialize(value)
- if value.is_a?(::Array)
- "(#{number_for_point(value[0])},#{number_for_point(value[1])})"
+ case value
+ when ActiveRecord::Point
+ "(#{number_for_point(value.x)},#{number_for_point(value.y)})"
+ when ::Array
+ serialize(build_point(*value))
+ else
+ super
+ end
+ end
+
+ def type_cast_for_schema(value)
+ if ActiveRecord::Point === value
+ [value.x, value.y]
else
super
end
@@ -33,9 +51,13 @@ module ActiveRecord
private
- def number_for_point(number)
- number.to_s.gsub(/\.0$/, '')
- end
+ def number_for_point(number)
+ number.to_s.gsub(/\.0$/, "")
+ end
+
+ def build_point(x, y)
+ ActiveRecord::Point.new(Float(x), Float(y))
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/rails_5_1_point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/rails_5_1_point.rb
deleted file mode 100644
index 7427a25ad5..0000000000
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/rails_5_1_point.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-module ActiveRecord
- Point = Struct.new(:x, :y)
-
- module ConnectionAdapters
- module PostgreSQL
- module OID # :nodoc:
- class Rails51Point < Type::Value # :nodoc:
- include Type::Helpers::Mutable
-
- def type
- :point
- end
-
- def cast(value)
- case value
- when ::String
- if value[0] == '(' && value[-1] == ')'
- value = value[1...-1]
- end
- x, y = value.split(",")
- build_point(x, y)
- when ::Array
- build_point(*value)
- else
- value
- end
- end
-
- def serialize(value)
- if value.is_a?(ActiveRecord::Point)
- "(#{number_for_point(value.x)},#{number_for_point(value.y)})"
- else
- super
- end
- end
-
- private
-
- def number_for_point(number)
- number.to_s.gsub(/\.0$/, '')
- end
-
- def build_point(x, y)
- ActiveRecord::Point.new(Float(x), Float(y))
- 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 fc201f8fb9..d85f9ab3ef 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
@@ -1,4 +1,4 @@
-require 'active_support/core_ext/string/filters'
+# frozen_string_literal: true
module ActiveRecord
module ConnectionAdapters
@@ -6,6 +6,7 @@ module ActiveRecord
module OID # :nodoc:
class Range < Type::Value # :nodoc:
attr_reader :subtype, :type
+ delegate :user_input_in_time_zone, to: :subtype
def initialize(subtype, type = :range)
@subtype = subtype
@@ -13,12 +14,12 @@ module ActiveRecord
end
def type_cast_for_schema(value)
- value.inspect.gsub('Infinity', '::Float::INFINITY')
+ value.inspect.gsub("Infinity", "::Float::INFINITY")
end
def cast_value(value)
- return if value == 'empty'
- return value if value.is_a?(::Range)
+ return if value == "empty"
+ return value unless value.is_a?(::String)
extracted = extract_bounds(value)
from = type_cast_single extracted[:from]
@@ -34,7 +35,7 @@ module ActiveRecord
if value.is_a?(::Range)
from = type_cast_single_for_database(value.begin)
to = type_cast_single_for_database(value.end)
- "[#{from},#{to}#{value.exclude_end? ? ')' : ']'}"
+ ::Range.new(from, to, value.exclude_end?)
else
super
end
@@ -46,39 +47,49 @@ module ActiveRecord
other.type == type
end
- private
-
- def type_cast_single(value)
- infinity?(value) ? value : @subtype.deserialize(value)
+ def map(value) # :nodoc:
+ new_begin = yield(value.begin)
+ new_end = yield(value.end)
+ ::Range.new(new_begin, new_end, value.exclude_end?)
end
- def type_cast_single_for_database(value)
- infinity?(value) ? '' : @subtype.serialize(value)
+ def force_equality?(value)
+ value.is_a?(::Range)
end
- def extract_bounds(value)
- from, to = value[1..-2].split(',')
- {
- from: (value[1] == ',' || from == '-infinity') ? infinity(negative: true) : from,
- to: (value[-2] == ',' || to == 'infinity') ? infinity : to,
- exclude_start: (value[0] == '('),
- exclude_end: (value[-1] == ')')
- }
- end
+ private
- def infinity(negative: false)
- if subtype.respond_to?(:infinity)
- subtype.infinity(negative: negative)
- elsif negative
- -::Float::INFINITY
- else
- ::Float::INFINITY
+ def type_cast_single(value)
+ infinity?(value) ? value : @subtype.deserialize(value)
end
- end
- def infinity?(value)
- value.respond_to?(:infinite?) && value.infinite?
- end
+ def type_cast_single_for_database(value)
+ infinity?(value) ? value : @subtype.serialize(value)
+ end
+
+ def extract_bounds(value)
+ from, to = value[1..-2].split(",")
+ {
+ from: (value[1] == "," || from == "-infinity") ? infinity(negative: true) : from,
+ to: (value[-2] == "," || to == "infinity") ? infinity : to,
+ exclude_start: (value[0] == "("),
+ exclude_end: (value[-1] == ")")
+ }
+ end
+
+ def infinity(negative: false)
+ if subtype.respond_to?(:infinity)
+ subtype.infinity(negative: negative)
+ elsif negative
+ -::Float::INFINITY
+ else
+ ::Float::INFINITY
+ end
+ end
+
+ def infinity?(value)
+ value.respond_to?(:infinite?) && value.infinite?
+ end
end
end
end
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..4ad1344f05 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
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
@@ -5,8 +7,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/oid/type_map_initializer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb
index 191c828e60..83c21ba6ea 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb
@@ -1,3 +1,7 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/array/extract"
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
@@ -13,13 +17,13 @@ module ActiveRecord
end
def run(records)
- nodes = records.reject { |row| @store.key? row['oid'].to_i }
- mapped, nodes = nodes.partition { |row| @store.key? row['typname'] }
- ranges, nodes = nodes.partition { |row| row['typtype'] == 'r'.freeze }
- enums, nodes = nodes.partition { |row| row['typtype'] == 'e'.freeze }
- domains, nodes = nodes.partition { |row| row['typtype'] == 'd'.freeze }
- arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in'.freeze }
- composites, nodes = nodes.partition { |row| row['typelem'].to_i != 0 }
+ nodes = records.reject { |row| @store.key? row["oid"].to_i }
+ mapped = nodes.extract! { |row| @store.key? row["typname"] }
+ ranges = nodes.extract! { |row| row["typtype"] == "r" }
+ enums = nodes.extract! { |row| row["typtype"] == "e" }
+ domains = nodes.extract! { |row| row["typtype"] == "d" }
+ arrays = nodes.extract! { |row| row["typinput"] == "array_in" }
+ composites = nodes.extract! { |row| row["typelem"].to_i != 0 }
mapped.each { |row| register_mapped_type(row) }
enums.each { |row| register_enum_type(row) }
@@ -29,79 +33,79 @@ module ActiveRecord
composites.each { |row| register_composite_type(row) }
end
- def query_conditions_for_initial_load(type_map)
- known_type_names = type_map.keys.map { |n| "'#{n}'" }
+ def query_conditions_for_initial_load
+ known_type_names = @store.keys.map { |n| "'#{n}'" }
known_type_types = %w('r' 'e' 'd')
<<-SQL % [known_type_names.join(", "), known_type_types.join(", ")]
WHERE
t.typname IN (%s)
OR t.typtype IN (%s)
- OR t.typinput::varchar = 'array_in'
+ OR t.typinput = 'array_in(cstring,oid,integer)'::regprocedure
OR t.typelem != 0
SQL
end
private
- def register_mapped_type(row)
- alias_type row['oid'], row['typname']
- end
+ def register_mapped_type(row)
+ alias_type row["oid"], row["typname"]
+ end
- def register_enum_type(row)
- register row['oid'], OID::Enum.new
- end
+ def register_enum_type(row)
+ register row["oid"], OID::Enum.new
+ end
- def register_array_type(row)
- register_with_subtype(row['oid'], row['typelem'].to_i) do |subtype|
- OID::Array.new(subtype, row['typdelim'])
+ def register_array_type(row)
+ register_with_subtype(row["oid"], row["typelem"].to_i) do |subtype|
+ OID::Array.new(subtype, row["typdelim"])
+ end
end
- end
- def register_range_type(row)
- register_with_subtype(row['oid'], row['rngsubtype'].to_i) do |subtype|
- OID::Range.new(subtype, row['typname'].to_sym)
+ def register_range_type(row)
+ register_with_subtype(row["oid"], row["rngsubtype"].to_i) do |subtype|
+ OID::Range.new(subtype, row["typname"].to_sym)
+ end
end
- end
- def register_domain_type(row)
- if base_type = @store.lookup(row["typbasetype"].to_i)
- register row['oid'], base_type
- else
- warn "unknown base type (OID: #{row["typbasetype"]}) for domain #{row["typname"]}."
+ def register_domain_type(row)
+ if base_type = @store.lookup(row["typbasetype"].to_i)
+ register row["oid"], base_type
+ else
+ warn "unknown base type (OID: #{row["typbasetype"]}) for domain #{row["typname"]}."
+ end
end
- end
- def register_composite_type(row)
- if subtype = @store.lookup(row['typelem'].to_i)
- register row['oid'], OID::Vector.new(row['typdelim'], subtype)
+ def register_composite_type(row)
+ if subtype = @store.lookup(row["typelem"].to_i)
+ register row["oid"], OID::Vector.new(row["typdelim"], subtype)
+ end
end
- end
- def register(oid, oid_type = nil, &block)
- oid = assert_valid_registration(oid, oid_type || block)
- if block_given?
- @store.register_type(oid, &block)
- else
- @store.register_type(oid, oid_type)
+ def register(oid, oid_type = nil, &block)
+ oid = assert_valid_registration(oid, oid_type || block)
+ if block_given?
+ @store.register_type(oid, &block)
+ else
+ @store.register_type(oid, oid_type)
+ end
end
- end
- def alias_type(oid, target)
- oid = assert_valid_registration(oid, target)
- @store.alias_type(oid, target)
- end
+ def alias_type(oid, target)
+ oid = assert_valid_registration(oid, target)
+ @store.alias_type(oid, target)
+ end
- def register_with_subtype(oid, target_oid)
- if @store.key?(target_oid)
- register(oid) do |_, *args|
- yield @store.lookup(target_oid, *args)
+ def register_with_subtype(oid, target_oid)
+ if @store.key?(target_oid)
+ register(oid) do |_, *args|
+ yield @store.lookup(target_oid, *args)
+ end
end
end
- end
- def assert_valid_registration(oid, oid_type)
- raise ArgumentError, "can't register nil type for OID #{oid}" if oid_type.nil?
- oid.to_i
- end
+ def assert_valid_registration(oid, oid_type)
+ raise ArgumentError, "can't register nil type for OID #{oid}" if oid_type.nil?
+ oid.to_i
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb
index 5e839228e9..bc9b8dbfcf 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
class Uuid < Type::Value # :nodoc:
- ACCEPTABLE_UUID = %r{\A\{?([a-fA-F0-9]{4}-?){8}\}?\z}x
+ ACCEPTABLE_UUID = %r{\A(\{)?([a-fA-F0-9]{4}-?){8}(?(1)\}|)\z}
alias_method :serialize, :deserialize
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb
index b26e876b54..88ef626a16 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb
index d40d837cee..042f32fdc3 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
index f175730551..0895d06356 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
@@ -27,8 +29,13 @@ module ActiveRecord
# - schema_name."table.name"
# - "schema.name".table_name
# - "schema.name"."table.name"
- def quote_table_name(name)
- Utils.extract_schema_qualified_name(name.to_s).quoted
+ def quote_table_name(name) # :nodoc:
+ @quoted_table_names[name] ||= Utils.extract_schema_qualified_name(name.to_s).quoted.freeze
+ end
+
+ # Quotes schema names for use in SQL queries.
+ def quote_schema_name(name)
+ PG::Connection.quote_ident(name)
end
def quote_table_name_for_assignment(table, attr)
@@ -36,8 +43,8 @@ module ActiveRecord
end
# Quotes column names for use in SQL queries.
- def quote_column_name(name) #:nodoc:
- PGconn.quote_ident(name.to_s)
+ def quote_column_name(name) # :nodoc:
+ @quoted_column_names[name] ||= PG::Connection.quote_ident(super).freeze
end
# Quote date/time values for use in SQL input.
@@ -50,10 +57,15 @@ module ActiveRecord
end
end
- # Does not quote function default values for UUID columns
- def quote_default_expression(value, column) #:nodoc:
- if column.type == :uuid && value =~ /\(\)/
- value
+ 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
+ elsif column.type == :uuid && value.is_a?(String) && /\(\)/.match?(value)
+ value # Does not quote function default values for UUID columns
elsif column.respond_to?(:array?)
value = type_cast_from_column(column, value)
quote(value)
@@ -67,43 +79,89 @@ module ActiveRecord
end
private
+ def lookup_cast_type(sql_type)
+ super(query_value("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").to_i)
+ end
- 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
- if value.binary?
- "B'#{value}'"
- elsif value.hex?
- "X'#{value}'"
+ def _quote(value)
+ case value
+ when OID::Xml::Data
+ "xml '#{quote_string(value.to_s)}'"
+ when OID::Bit::Data
+ if value.binary?
+ "B'#{value}'"
+ elsif value.hex?
+ "X'#{value}'"
+ end
+ when Numeric
+ if value.finite?
+ super
+ else
+ "'#{value}'"
+ end
+ when OID::Array::Data
+ _quote(encode_array(value))
+ when Range
+ _quote(encode_range(value))
+ else
+ super
end
- when Float
- if value.infinite? || value.nan?
- "'#{value}'"
+ end
+
+ def _type_cast(value)
+ case value
+ when Type::Binary::Data
+ # Return a bind param hash with format as binary.
+ # 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)
+ when Range
+ encode_range(value)
else
super
end
- else
- super
end
- end
- def _type_cast(value)
- 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
- # for more information
- { value: value.to_s, format: 1 }
- when OID::Xml::Data, OID::Bit::Data
- value.to_s
- else
- super
+ 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 encode_range(range)
+ "[#{type_cast_range_value(range.first)},#{type_cast_range_value(range.last)}#{range.exclude_end? ? ')' : ']'}"
+ end
+
+ def determine_encoding_of_strings_in_array(value)
+ case value
+ when ::Array then determine_encoding_of_strings_in_array(value.first)
+ 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
+
+ def type_cast_range_value(value)
+ infinity?(value) ? "" : type_cast(value)
+ end
+
+ def infinity?(value)
+ value.respond_to?(:infinite?) && value.infinite?
end
- end
end
end
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..8df91c988b 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
@@ -1,27 +1,24 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module ReferentialIntegrity # :nodoc:
- def supports_disable_referential_integrity? # :nodoc:
- true
- end
-
def disable_referential_integrity # :nodoc:
- if supports_disable_referential_integrity?
- original_exception = nil
+ original_exception = nil
- begin
- transaction(requires_new: true) do
- execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";"))
- end
- rescue ActiveRecord::ActiveRecordError => e
- original_exception = e
+ begin
+ transaction(requires_new: true) do
+ execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";"))
end
+ rescue ActiveRecord::ActiveRecordError => e
+ original_exception = e
+ end
- begin
- yield
- rescue ActiveRecord::InvalidForeignKey => e
- warn <<-WARNING
+ begin
+ yield
+ rescue ActiveRecord::InvalidForeignKey => e
+ warn <<-WARNING
WARNING: Rails was not able to disable referential integrity.
This is most likely caused due to missing permissions.
@@ -30,17 +27,14 @@ Rails needs superuser privileges to disable referential integrity.
cause: #{original_exception.try(:message)}
WARNING
- raise e
- end
+ raise e
+ end
- begin
- transaction(requires_new: true) do
- execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";"))
- end
- rescue ActiveRecord::ActiveRecordError
+ begin
+ transaction(requires_new: true) do
+ execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";"))
end
- else
- yield
+ rescue ActiveRecord::ActiveRecordError
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb
new file mode 100644
index 0000000000..ceb8b40bd9
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ class SchemaCreation < AbstractAdapter::SchemaCreation # :nodoc:
+ private
+ def visit_AlterTable(o)
+ super << o.constraint_validations.map { |fk| visit_ValidateConstraint fk }.join(" ")
+ end
+
+ def visit_AddForeignKey(o)
+ super.dup.tap { |sql| sql << " NOT VALID" unless o.validate? }
+ end
+
+ def visit_ValidateConstraint(name)
+ "VALIDATE CONSTRAINT #{quote_column_name(name)}"
+ end
+
+ def add_column_options!(sql, options)
+ if options[:collation]
+ sql << " COLLATE \"#{options[:collation]}\""
+ end
+ super
+ end
+
+ # Returns any SQL string to go between CREATE and TABLE. May be nil.
+ def table_modifier_in_create(o)
+ # A table cannot be both TEMPORARY and UNLOGGED, since all TEMPORARY
+ # tables are already UNLOGGED.
+ if o.temporary
+ " TEMPORARY"
+ elsif o.unlogged
+ " UNLOGGED"
+ end
+ 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 022dbdfa27..dc4a0bb26e 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
@@ -11,11 +13,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 <tt>gen_random_uuid()</tt> 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 <tt>uuid_generate_v4()</tt> 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 +36,18 @@ 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
+ if type == :uuid
+ options[:default] = options.fetch(:default, "gen_random_uuid()")
+ end
+
super
end
@@ -67,6 +83,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
@@ -75,10 +95,6 @@ module ActiveRecord
args.each { |name| column(name, :int8range, options) }
end
- def json(*args, **options)
- args.each { |name| column(name, :json, options) }
- end
-
def jsonb(*args, **options)
args.each { |name| column(name, :jsonb, options) }
end
@@ -99,10 +115,38 @@ 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
+ def line(*args, **options)
+ args.each { |name| column(name, :line, options) }
+ end
+
+ def lseg(*args, **options)
+ args.each { |name| column(name, :lseg, options) }
+ end
+
+ def box(*args, **options)
+ args.each { |name| column(name, :box, options) }
+ end
+
+ def path(*args, **options)
+ args.each { |name| column(name, :path, options) }
+ end
+
+ def polygon(*args, **options)
+ args.each { |name| column(name, :polygon, options) }
+ end
+
+ def circle(*args, **options)
+ args.each { |name| column(name, :circle, options) }
+ end
+
def serial(*args, **options)
args.each { |name| column(name, :serial, options) }
end
@@ -128,29 +172,42 @@ 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
+ attr_reader :unlogged
+
+ def initialize(*)
+ super
+ @unlogged = ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables
end
private
-
- def create_column_definition(name, type)
- PostgreSQL::ColumnDefinition.new name, type
+ def integer_like_primary_key_type(type, options)
+ if type == :bigint || options[:limit] == 8
+ :bigserial
+ else
+ :serial
+ end
end
end
class Table < ActiveRecord::ConnectionAdapters::Table
include ColumnMethods
end
+
+ class AlterTable < ActiveRecord::ConnectionAdapters::AlterTable
+ attr_reader :constraint_validations
+
+ def initialize(td)
+ super
+ @constraint_validations = []
+ end
+
+ def validate_constraint(name)
+ @constraint_validations << name
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb
new file mode 100644
index 0000000000..84643d20da
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ class SchemaDumper < ConnectionAdapters::SchemaDumper # :nodoc:
+ private
+
+ def extensions(stream)
+ extensions = @connection.extensions
+ if extensions.any?
+ stream.puts " # These are extensions that must be enabled in order to support this database"
+ extensions.sort.each do |extension|
+ stream.puts " enable_extension #{extension.inspect}"
+ end
+ stream.puts
+ end
+ end
+
+ def prepare_column_options(column)
+ spec = super
+ spec[:array] = "true" if column.array?
+ spec
+ end
+
+ def default_primary_key?(column)
+ schema_type(column) == :bigserial
+ end
+
+ def explicit_primary_key_default?(column)
+ column.type == :uuid || (column.type == :integer && !column.serial?)
+ end
+
+ def schema_type(column)
+ return super unless column.serial?
+
+ if column.bigint?
+ :bigserial
+ else
+ :serial
+ end
+ end
+
+ def schema_expression(column)
+ super unless column.serial?
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
index d114cad16b..fae3ddbad4 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -1,22 +1,8 @@
+# frozen_string_literal: true
+
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+.
@@ -34,26 +20,26 @@ module ActiveRecord
# create_database config[:database], config
# create_database 'foo_development', encoding: 'unicode'
def create_database(name, options = {})
- options = { encoding: 'utf8' }.merge!(options.symbolize_keys)
+ options = { encoding: "utf8" }.merge!(options.symbolize_keys)
option_string = options.inject("") do |memo, (key, value)|
memo += case key
- when :owner
- " OWNER = \"#{value}\""
- when :template
- " TEMPLATE = \"#{value}\""
- when :encoding
- " ENCODING = '#{value}'"
- when :collation
- " LC_COLLATE = '#{value}'"
- when :ctype
- " LC_CTYPE = '#{value}'"
- when :tablespace
- " TABLESPACE = \"#{value}\""
- when :connection_limit
- " CONNECTION LIMIT = #{value}"
- else
- ""
+ when :owner
+ " OWNER = \"#{value}\""
+ when :template
+ " TEMPLATE = \"#{value}\""
+ when :encoding
+ " ENCODING = '#{value}'"
+ when :collation
+ " LC_COLLATE = '#{value}'"
+ when :ctype
+ " LC_CTYPE = '#{value}'"
+ when :tablespace
+ " TABLESPACE = \"#{value}\""
+ when :connection_limit
+ " CONNECTION LIMIT = #{value}"
+ else
+ ""
end
end
@@ -68,62 +54,48 @@ module ActiveRecord
execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}"
end
- # Returns the list of all tables in the schema search path or a specified schema.
- def tables(name = nil)
- select_values("SELECT tablename FROM pg_tables WHERE schemaname = ANY(current_schemas(false))", 'SCHEMA')
- 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)
- name = Utils.extract_schema_qualified_name(name.to_s)
- return false unless name.identifier
-
- select_value(<<-SQL, 'SCHEMA').to_i > 0
- SELECT COUNT(*)
- 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 = '#{name.identifier}'
- AND n.nspname = #{name.schema ? "'#{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
+ query_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)
- select_value(<<-SQL, 'SCHEMA').to_i > 0
+ def index_name_exists?(table_name, index_name)
+ table = quoted_scope(table_name)
+ index = quoted_scope(index_name)
+
+ query_value(<<-SQL, "SCHEMA").to_i > 0
SELECT COUNT(*)
FROM pg_class t
INNER JOIN pg_index d ON t.oid = d.indrelid
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_name}'
- AND t.relname = '#{table_name}'
- AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = 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)
- result = query(<<-SQL, 'SCHEMA')
- SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid
- FROM pg_class t
- INNER JOIN pg_index d ON t.oid = d.indrelid
- INNER JOIN pg_class i ON d.indexrelid = i.oid
- WHERE i.relkind = 'i'
- AND d.indisprimary = 'f'
- AND t.relname = '#{table_name}'
- AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) )
+ def indexes(table_name) # :nodoc:
+ scope = quoted_scope(table_name)
+
+ result = query(<<-SQL, "SCHEMA")
+ SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid,
+ pg_catalog.obj_description(i.oid, 'pg_class') AS comment
+ FROM pg_class t
+ INNER JOIN pg_index d ON t.oid = d.indrelid
+ 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 d.indisprimary = 'f'
+ AND t.relname = #{scope[:name]}
+ AND n.nspname = #{scope[:schema]}
ORDER BY i.relname
SQL
@@ -133,73 +105,98 @@ module ActiveRecord
indkey = row[2].split(" ").map(&:to_i)
inddef = row[3]
oid = row[4]
+ comment = row[5]
- columns = Hash[query(<<-SQL, "SCHEMA")]
- SELECT a.attnum, a.attname
- FROM pg_attribute a
- WHERE a.attrelid = #{oid}
- AND a.attnum IN (#{indkey.join(",")})
- SQL
-
- column_names = columns.values_at(*indkey).compact
+ using, expressions, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: WHERE (.+))?\z/m).flatten
- unless column_names.empty?
- # add info on sort order for columns (only desc order is explicitly specified, asc is the default)
- desc_order_columns = inddef.scan(/(\w+) DESC/).flatten
- orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {}
- where = inddef.scan(/WHERE (.+)$/).flatten[0]
- using = inddef.scan(/USING (.+?) /).flatten[0].to_sym
+ orders = {}
+ opclasses = {}
- IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where, nil, using)
+ if indkey.include?(0)
+ columns = expressions
+ else
+ columns = Hash[query(<<~SQL, "SCHEMA")].values_at(*indkey).compact
+ SELECT a.attnum, a.attname
+ FROM pg_attribute a
+ WHERE a.attrelid = #{oid}
+ AND a.attnum IN (#{indkey.join(",")})
+ SQL
+
+ # add info on sort order (only desc order is explicitly specified, asc is the default)
+ # and non-default opclasses
+ expressions.scan(/(?<column>\w+)\s?(?<opclass>\w+_ops)?\s?(?<desc>DESC)?\s?(?<nulls>NULLS (?:FIRST|LAST))?/).each do |column, opclass, desc, nulls|
+ opclasses[column] = opclass.to_sym if opclass
+ if nulls
+ orders[column] = [desc, nulls].compact.join(" ")
+ else
+ orders[column] = :desc if desc
+ end
+ end
end
- end.compact
+
+ IndexDefinition.new(
+ table_name,
+ index_name,
+ unique,
+ columns,
+ orders: orders,
+ opclasses: opclasses,
+ where: where,
+ using: using.to_sym,
+ comment: comment.presence
+ )
+ end
end
- # Returns the list of all column definitions for a table.
- def columns(table_name)
- # Limit, precision, and scale are all handled by the superclass.
- column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod, collation|
- oid = oid.to_i
- fmod = fmod.to_i
- type_metadata = fetch_type_metadata(column_name, type, oid, fmod)
- default_value = extract_value_from_default(default)
- default_function = extract_default_function(default_value, default)
- new_column(column_name, default_value, type_metadata, !notnull, default_function, collation)
+ def table_options(table_name) # :nodoc:
+ if comment = table_comment(table_name)
+ { comment: comment }
end
end
- def new_column(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) # :nodoc:
- PostgreSQLColumn.new(name, default, sql_type_metadata, null, default_function, collation)
+ # Returns a comment stored in database for given table
+ def table_comment(table_name) # :nodoc:
+ scope = quoted_scope(table_name, type: "BASE TABLE")
+ if scope[:name]
+ query_value(<<~SQL, "SCHEMA")
+ SELECT pg_catalog.obj_description(c.oid, 'pg_class')
+ FROM pg_catalog.pg_class c
+ LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
+ 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')
+ query_value("SELECT current_database()", "SCHEMA")
end
# Returns the current schema name.
def current_schema
- select_value('SELECT current_schema', 'SCHEMA')
+ query_value("SELECT current_schema", "SCHEMA")
end
# Returns the current database encoding format.
def encoding
- select_value("SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname LIKE '#{current_database}'", 'SCHEMA')
+ query_value("SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = current_database()", "SCHEMA")
end
# Returns the current database collation.
def collation
- select_value("SELECT datcollate FROM pg_database WHERE datname LIKE '#{current_database}'", 'SCHEMA')
+ query_value("SELECT datcollate FROM pg_database WHERE datname = current_database()", "SCHEMA")
end
# Returns the current database ctype.
def ctype
- select_value("SELECT datctype FROM pg_database WHERE datname LIKE '#{current_database}'", 'SCHEMA')
+ query_value("SELECT datctype FROM pg_database WHERE datname = current_database()", "SCHEMA")
end
# Returns an array of schema names.
def schema_names
- select_values(<<-SQL, 'SCHEMA')
+ query_values(<<-SQL, "SCHEMA")
SELECT nspname
FROM pg_namespace
WHERE nspname !~ '^pg_.*'
@@ -209,53 +206,53 @@ module ActiveRecord
end
# Creates a schema for the given schema name.
- def create_schema schema_name
- execute "CREATE SCHEMA #{schema_name}"
+ def create_schema(schema_name)
+ execute "CREATE SCHEMA #{quote_schema_name(schema_name)}"
end
# Drops the schema for the given schema name.
- def drop_schema schema_name
- execute "DROP SCHEMA #{schema_name} CASCADE"
+ def drop_schema(schema_name, options = {})
+ execute "DROP SCHEMA#{' IF EXISTS' if options[:if_exists]} #{quote_schema_name(schema_name)} CASCADE"
end
# Sets the schema search path to a string of comma-separated schema names.
# Names beginning with $ have to be quoted (e.g. $user => '$user').
- # See: http://www.postgresql.org/docs/current/static/ddl-schemas.html
+ # See: https://www.postgresql.org/docs/current/static/ddl-schemas.html
#
# This should be not be called manually but set in database.yml.
def schema_search_path=(schema_csv)
if schema_csv
- execute("SET search_path TO #{schema_csv}", 'SCHEMA')
+ execute("SET search_path TO #{schema_csv}", "SCHEMA")
@schema_search_path = schema_csv
end
end
# Returns the active schema search path.
def schema_search_path
- @schema_search_path ||= select_value('SHOW search_path', 'SCHEMA')
+ @schema_search_path ||= query_value("SHOW search_path", "SCHEMA")
end
# Returns the current client message level.
def client_min_messages
- select_value('SHOW client_min_messages', 'SCHEMA')
+ query_value("SHOW client_min_messages", "SCHEMA")
end
# Set the client message level.
def client_min_messages=(level)
- execute("SET client_min_messages TO '#{level}'", 'SCHEMA')
+ execute("SET client_min_messages TO '#{level}'", "SCHEMA")
end
# Returns the sequence name for a table's primary key or some other specified key.
- def default_sequence_name(table_name, pk = nil) #:nodoc:
- result = serial_sequence(table_name, pk || 'id')
+ def default_sequence_name(table_name, pk = "id") #:nodoc:
+ result = serial_sequence(table_name, pk)
return nil unless result
Utils.extract_schema_qualified_name(result).to_s
rescue ActiveRecord::StatementInvalid
- PostgreSQL::Name.new(nil, "#{table_name}_#{pk || 'id'}_seq").to_s
+ PostgreSQL::Name.new(nil, "#{table_name}_#{pk}_seq").to_s
end
def serial_sequence(table, column)
- select_value("SELECT pg_get_serial_sequence('#{table}', '#{column}')", 'SCHEMA')
+ query_value("SELECT pg_get_serial_sequence(#{quote(table)}, #{quote(column)})", "SCHEMA")
end
# Sets the sequence of a table's primary key to the specified value.
@@ -266,16 +263,16 @@ module ActiveRecord
if sequence
quoted_sequence = quote_table_name(sequence)
- select_value("SELECT setval('#{quoted_sequence}', #{value})", 'SCHEMA')
+ query_value("SELECT setval(#{quote(quoted_sequence)}, #{value})", "SCHEMA")
else
- @logger.warn "#{table} has primary key #{pk} with no default sequence" if @logger
+ @logger.warn "#{table} has primary key #{pk} with no default sequence." if @logger
end
end
end
# Resets the sequence of a table's primary key to the maximum value.
def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc:
- unless pk and sequence
+ unless pk && sequence
default_pk, default_sequence = pk_and_sequence_for(table)
pk ||= default_pk
@@ -283,15 +280,21 @@ module ActiveRecord
end
if @logger && pk && !sequence
- @logger.warn "#{table} has primary key #{pk} with no default sequence"
+ @logger.warn "#{table} has primary key #{pk} with no default sequence."
end
if pk && sequence
quoted_sequence = quote_table_name(sequence)
+ max_pk = query_value("SELECT MAX(#{quote_column_name pk}) FROM #{quote_table_name(table)}", "SCHEMA")
+ if max_pk.nil?
+ if postgresql_version >= 100000
+ minvalue = query_value("SELECT seqmin FROM pg_sequence WHERE seqrelid = #{quote(quoted_sequence)}::regclass", "SCHEMA")
+ else
+ minvalue = query_value("SELECT min_value FROM #{quoted_sequence}", "SCHEMA")
+ end
+ end
- select_value(<<-end_sql, 'SCHEMA')
- SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false)
- end_sql
+ query_value("SELECT setval(#{quote(quoted_sequence)}, #{max_pk ? max_pk : minvalue}, #{max_pk ? true : false})", "SCHEMA")
end
end
@@ -299,7 +302,7 @@ module ActiveRecord
def pk_and_sequence_for(table) #:nodoc:
# First try looking for a sequence with a dependency on the
# given table's primary key.
- result = query(<<-end_sql, 'SCHEMA')[0]
+ result = query(<<-end_sql, "SCHEMA")[0]
SELECT attr.attname, nsp.nspname, seq.relname
FROM pg_class seq,
pg_attribute attr,
@@ -315,11 +318,11 @@ module ActiveRecord
AND seq.relnamespace = nsp.oid
AND cons.contype = 'p'
AND dep.classid = 'pg_class'::regclass
- AND dep.refobjid = '#{quote_table_name(table)}'::regclass
+ AND dep.refobjid = #{quote(quote_table_name(table))}::regclass
end_sql
- if result.nil? or result.empty?
- result = query(<<-end_sql, 'SCHEMA')[0]
+ if result.nil? || result.empty?
+ result = query(<<-end_sql, "SCHEMA")[0]
SELECT attr.attname, nsp.nspname,
CASE
WHEN pg_get_expr(def.adbin, def.adrelid) !~* 'nextval' THEN NULL
@@ -333,7 +336,7 @@ module ActiveRecord
JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum)
JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1])
JOIN pg_namespace nsp ON (t.relnamespace = nsp.oid)
- WHERE t.oid = '#{quote_table_name(table)}'::regclass
+ WHERE t.oid = #{quote(quote_table_name(table))}::regclass
AND cons.contype = 'p'
AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval|uuid_generate'
end_sql
@@ -349,17 +352,45 @@ module ActiveRecord
nil
end
- # Returns just a table's primary key
- def primary_key(table)
- pks = query(<<-end_sql, 'SCHEMA')
- SELECT attr.attname
- FROM pg_attribute attr
- INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = any(cons.conkey)
- WHERE cons.contype = 'p'
- AND cons.conrelid = '#{quote_table_name(table)}'::regclass
- end_sql
- return nil unless pks.count == 1
- pks[0][0]
+ def primary_keys(table_name) # :nodoc:
+ query_values(<<~SQL, "SCHEMA")
+ SELECT a.attname
+ FROM (
+ SELECT indrelid, indkey, generate_subscripts(indkey, 1) idx
+ FROM pg_index
+ WHERE indrelid = #{quote(quote_table_name(table_name))}::regclass
+ AND indisprimary
+ ) i
+ JOIN pg_attribute a
+ ON a.attrelid = i.indrelid
+ AND a.attnum = i.indkey[i.idx]
+ ORDER BY i.idx
+ SQL
+ end
+
+ def bulk_change_table(table_name, operations)
+ sql_fragments = []
+ non_combinable_operations = []
+
+ operations.each do |command, args|
+ table, arguments = args.shift, args
+ method = :"#{command}_for_alter"
+
+ if respond_to?(method, true)
+ sqls, procs = Array(send(method, table, *arguments)).partition { |v| v.is_a?(String) }
+ sql_fragments << sqls
+ non_combinable_operations.concat(procs)
+ else
+ execute "ALTER TABLE #{quote_table_name(table_name)} #{sql_fragments.join(", ")}" unless sql_fragments.empty?
+ non_combinable_operations.each(&:call)
+ sql_fragments = []
+ non_combinable_operations = []
+ send(command, table, *arguments)
+ end
+ end
+
+ execute "ALTER TABLE #{quote_table_name(table_name)} #{sql_fragments.join(", ")}" unless sql_fragments.empty?
+ non_combinable_operations.each(&:call)
end
# Renames a table.
@@ -372,67 +403,55 @@ module ActiveRecord
clear_cache!
execute "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}"
pk, seq = pk_and_sequence_for(new_name)
- if seq && seq.identifier == "#{table_name}_#{pk}_seq"
- new_seq = "#{new_name}_#{pk}_seq"
+ if pk
idx = "#{table_name}_pkey"
new_idx = "#{new_name}_pkey"
- execute "ALTER TABLE #{quote_table_name(seq)} RENAME TO #{quote_table_name(new_seq)}"
execute "ALTER INDEX #{quote_table_name(idx)} RENAME TO #{quote_table_name(new_idx)}"
+ if seq && seq.identifier == "#{table_name}_#{pk}_seq"
+ new_seq = "#{new_name}_#{pk}_seq"
+ execute "ALTER TABLE #{seq.quoted} RENAME TO #{quote_table_name(new_seq)}"
+ end
end
-
rename_table_indexes(table_name, new_name)
end
def add_column(table_name, column_name, type, options = {}) #:nodoc:
clear_cache!
super
+ change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment)
end
def change_column(table_name, column_name, type, options = {}) #:nodoc:
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 = "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quoted_column_name} TYPE #{sql_type}"
- if options[:collation]
- sql << " COLLATE \"#{options[:collation]}\""
- end
- if options[:using]
- sql << " USING #{options[:using]}"
- elsif options[:cast_as]
- cast_as_type = type_to_sql(options[:cast_as], options[:limit], options[:precision], options[:scale], options[:array])
- 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_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
+ sqls, procs = Array(change_column_for_alter(table_name, column_name, type, options)).partition { |v| v.is_a?(String) }
+ execute "ALTER TABLE #{quote_table_name(table_name)} #{sqls.join(", ")}"
+ procs.each(&:call)
end
# Changes the default value of a table column.
def change_column_default(table_name, column_name, default_or_changes) # :nodoc:
- clear_cache!
- column = column_for(table_name, column_name)
- return unless column
-
- default = extract_new_default_value(default_or_changes)
- alter_column_query = "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} %s"
- if default.nil?
- # <tt>DEFAULT NULL</tt> results in the same behavior as <tt>DROP DEFAULT</tt>. However, PostgreSQL will
- # cast the default to the columns type, which leaves us with a default like "default NULL::character varying".
- execute alter_column_query % "DROP DEFAULT"
- else
- execute alter_column_query % "SET DEFAULT #{quote_default_expression(default, column)}"
- end
+ execute "ALTER TABLE #{quote_table_name(table_name)} #{change_column_default_for_alter(table_name, column_name, default_or_changes)}"
end
def change_column_null(table_name, column_name, null, default = nil) #:nodoc:
clear_cache!
unless null || default.nil?
column = column_for(table_name, column_name)
- execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_expression(default, column)} WHERE #{quote_column_name(column_name)} IS NULL") if column
+ execute "UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_expression(default, column)} WHERE #{quote_column_name(column_name)} IS NULL" if column
end
- execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL")
+ execute "ALTER TABLE #{quote_table_name(table_name)} #{change_column_null_for_alter(table_name, column_name, null, default)}"
+ end
+
+ # Adds comment for given table column or drops it if +comment+ is a +nil+
+ def change_column_comment(table_name, column_name, comment) # :nodoc:
+ clear_cache!
+ execute "COMMENT ON COLUMN #{quote_table_name(table_name)}.#{quote_column_name(column_name)} IS #{quote(comment)}"
+ end
+
+ # Adds comment for given table or drops it if +comment+ is a +nil+
+ def change_table_comment(table_name, comment) # :nodoc:
+ clear_cache!
+ execute "COMMENT ON TABLE #{quote_table_name(table_name)} IS #{quote(comment)}"
end
# Renames a column in a table.
@@ -443,12 +462,34 @@ module ActiveRecord
end
def add_index(table_name, column_name, options = {}) #:nodoc:
- index_name, index_type, index_columns, index_options, index_algorithm, index_using = add_index_options(table_name, column_name, options)
- execute "CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns})#{index_options}"
+ index_name, index_type, index_columns_and_opclasses, index_options, index_algorithm, index_using, comment = add_index_options(table_name, column_name, options)
+ execute("CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns_and_opclasses})#{index_options}").tap do
+ execute "COMMENT ON INDEX #{quote_column_name(index_name)} IS #{quote(comment)}" if comment
+ end
end
- def remove_index!(table_name, index_name) #:nodoc:
- execute "DROP INDEX #{quote_table_name(index_name)}"
+ def remove_index(table_name, options = {}) #:nodoc:
+ table = Utils.extract_schema_qualified_name(table_name.to_s)
+
+ if options.is_a?(Hash) && options.key?(:name)
+ provided_index = Utils.extract_schema_qualified_name(options[:name].to_s)
+
+ options[:name] = provided_index.identifier
+ table = PostgreSQL::Name.new(provided_index.schema, table.identifier) unless table.schema.present?
+
+ if provided_index.schema.present? && table.schema != provided_index.schema
+ raise ArgumentError.new("Index schema '#{provided_index.schema}' does not match table schema '#{table.schema}'")
+ end
+ end
+
+ index_to_remove = PostgreSQL::Name.new(table.schema, index_name_for_remove(table.to_s, options))
+ algorithm =
+ if options.is_a?(Hash) && options.key?(:algorithm)
+ index_algorithms.fetch(options[:algorithm]) do
+ raise ArgumentError.new("Algorithm must be one of the following: #{index_algorithms.keys.map(&:inspect).join(', ')}")
+ end
+ end
+ execute "DROP INDEX #{algorithm} #{quote_table_name(index_to_remove)}"
end
# Renames an index of a table. Raises error if length of new
@@ -460,8 +501,9 @@ module ActiveRecord
end
def foreign_keys(table_name)
- fk_info = select_all <<-SQL.strip_heredoc
- 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
+ scope = quoted_scope(table_name)
+ fk_info = exec_query(<<~SQL, "SCHEMA")
+ SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete, c.convalidated AS valid
FROM pg_constraint c
JOIN pg_class t1 ON c.conrelid = t1.oid
JOIN pg_class t2 ON c.confrelid = t2.oid
@@ -469,94 +511,281 @@ 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
fk_info.map do |row|
options = {
- column: row['column'],
- name: row['name'],
- primary_key: row['primary_key']
+ column: row["column"],
+ name: row["name"],
+ primary_key: row["primary_key"]
}
- options[:on_delete] = extract_foreign_key_action(row['on_delete'])
- options[:on_update] = extract_foreign_key_action(row['on_update'])
+ options[:on_delete] = extract_foreign_key_action(row["on_delete"])
+ options[:on_update] = extract_foreign_key_action(row["on_update"])
+ options[:validate] = row["valid"]
- ForeignKeyDefinition.new(table_name, row['to_table'], options)
+ ForeignKeyDefinition.new(table_name, row["to_table"], options)
end
end
- def extract_foreign_key_action(specifier) # :nodoc:
- case specifier
- when 'c'; :cascade
- when 'n'; :nullify
- when 'r'; :restrict
- end
+ def foreign_tables
+ query_values(data_source_sql(type: "FOREIGN TABLE"), "SCHEMA")
end
- def index_name_length
- 63
+ def foreign_table_exists?(table_name)
+ query_values(data_source_sql(table_name, type: "FOREIGN TABLE"), "SCHEMA").any? if table_name.present?
end
# Maps logical Rails types to PostgreSQL-specific data types.
- def type_to_sql(type, limit = nil, precision = nil, scale = nil, array = nil)
- sql = case type.to_s
- when 'binary'
- # PostgreSQL doesn't support limits on binary (bytea) columns.
- # The hard limit is 1GB, because of a 32-bit size field, and TOAST.
- case limit
- when nil, 0..0x3fffffff; super(type)
- else raise(ActiveRecordError, "No binary type has byte size #{limit}.")
- end
- when 'text'
- # PostgreSQL doesn't support limits on text columns.
- # The hard limit is 1GB, according to section 8.3 in the manual.
- case limit
- when nil, 0..0x3fffffff; super(type)
- else raise(ActiveRecordError, "The limit on text can be at most 1GB - 1byte.")
- end
- when 'integer'
- case limit
- when 1, 2; 'smallint'
- when nil, 3, 4; 'integer'
- when 5..8; 'bigint'
- else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.")
+ def type_to_sql(type, limit: nil, precision: nil, scale: nil, array: nil, **) # :nodoc:
+ sql = \
+ case type.to_s
+ when "binary"
+ # PostgreSQL doesn't support limits on binary (bytea) columns.
+ # The hard limit is 1GB, because of a 32-bit size field, and TOAST.
+ case limit
+ when nil, 0..0x3fffffff; super(type)
+ else raise(ActiveRecordError, "No binary type has byte size #{limit}.")
+ end
+ when "text"
+ # PostgreSQL doesn't support limits on text columns.
+ # The hard limit is 1GB, according to section 8.3 in the manual.
+ case limit
+ when nil, 0..0x3fffffff; super(type)
+ else raise(ActiveRecordError, "The limit on text can be at most 1GB - 1byte.")
+ end
+ when "integer"
+ case limit
+ when 1, 2; "smallint"
+ when nil, 3, 4; "integer"
+ when 5..8; "bigint"
+ else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with scale 0 instead.")
+ end
+ else
+ super
end
- else
- super(type, limit, precision, scale)
- end
- sql << '[]' if array && type != :primary_key
+ sql = "#{sql}[]" if array && type != :primary_key
sql
end
# PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and
# requires that the ORDER BY include the distinct column.
def columns_for_distinct(columns, orders) #:nodoc:
- order_columns = orders.reject(&:blank?).map{ |s|
+ order_columns = orders.reject(&:blank?).map { |s|
# Convert Arel node to string
s = s.to_sql unless s.is_a?(String)
# Remove any ASC/DESC modifiers
- s.gsub(/\s+(?:ASC|DESC)\b/i, '')
- .gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, '')
+ s.gsub(/\s+(?:ASC|DESC)\b/i, "")
+ .gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, "")
}.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }
- [super, *order_columns].join(', ')
+ (order_columns << super).join(", ")
+ end
+
+ def update_table_definition(table_name, base) # :nodoc:
+ PostgreSQL::Table.new(table_name, base)
+ end
+
+ def create_schema_dumper(options) # :nodoc:
+ PostgreSQL::SchemaDumper.create(self, options)
+ end
+
+ # Validates the given constraint.
+ #
+ # Validates the constraint named +constraint_name+ on +accounts+.
+ #
+ # validate_constraint :accounts, :constraint_name
+ def validate_constraint(table_name, constraint_name)
+ return unless supports_validate_constraints?
+
+ at = create_alter_table table_name
+ at.validate_constraint constraint_name
+
+ execute schema_creation.accept(at)
end
- def fetch_type_metadata(column_name, sql_type, oid, fmod)
- cast_type = get_oid_type(oid, fmod, column_name, sql_type)
- simple_type = SqlTypeMetadata.new(
- sql_type: sql_type,
- type: cast_type.type,
- limit: cast_type.limit,
- precision: cast_type.precision,
- scale: cast_type.scale,
- )
- PostgreSQLTypeMetadata.new(simple_type, oid: oid, fmod: fmod)
+ # Validates the given foreign key.
+ #
+ # Validates the foreign key on +accounts.branch_id+.
+ #
+ # validate_foreign_key :accounts, :branches
+ #
+ # Validates the foreign key on +accounts.owner_id+.
+ #
+ # validate_foreign_key :accounts, column: :owner_id
+ #
+ # Validates the foreign key named +special_fk_name+ on the +accounts+ table.
+ #
+ # validate_foreign_key :accounts, name: :special_fk_name
+ #
+ # The +options+ hash accepts the same keys as SchemaStatements#add_foreign_key.
+ def validate_foreign_key(from_table, options_or_to_table = {})
+ return unless supports_validate_constraints?
+
+ fk_name_to_validate = foreign_key_for!(from_table, options_or_to_table).name
+
+ validate_constraint from_table, fk_name_to_validate
end
+
+ private
+ def schema_creation
+ PostgreSQL::SchemaCreation.new(self)
+ end
+
+ def create_table_definition(*args)
+ PostgreSQL::TableDefinition.new(*args)
+ end
+
+ def create_alter_table(name)
+ PostgreSQL::AlterTable.new create_table_definition(name)
+ end
+
+ def new_column_from_field(table_name, field)
+ column_name, type, default, notnull, oid, fmod, collation, comment = field
+ type_metadata = fetch_type_metadata(column_name, type, oid.to_i, fmod.to_i)
+ default_value = extract_value_from_default(default)
+ default_function = extract_default_function(default_value, default)
+
+ PostgreSQLColumn.new(
+ column_name,
+ default_value,
+ type_metadata,
+ !notnull,
+ table_name,
+ default_function,
+ collation,
+ comment: comment.presence,
+ max_identifier_length: max_identifier_length
+ )
+ end
+
+ def fetch_type_metadata(column_name, sql_type, oid, fmod)
+ cast_type = get_oid_type(oid, fmod, column_name, sql_type)
+ simple_type = SqlTypeMetadata.new(
+ sql_type: sql_type,
+ type: cast_type.type,
+ limit: cast_type.limit,
+ precision: cast_type.precision,
+ scale: cast_type.scale,
+ )
+ PostgreSQLTypeMetadata.new(simple_type, oid: oid, fmod: fmod)
+ end
+
+ def extract_foreign_key_action(specifier)
+ case specifier
+ when "c"; :cascade
+ when "n"; :nullify
+ when "r"; :restrict
+ end
+ end
+
+ def change_column_sql(table_name, column_name, type, options = {})
+ quoted_column_name = quote_column_name(column_name)
+ sql_type = type_to_sql(type, options)
+ sql = +"ALTER COLUMN #{quoted_column_name} TYPE #{sql_type}"
+ if options[:collation]
+ sql << " COLLATE \"#{options[:collation]}\""
+ end
+ if options[:using]
+ sql << " USING #{options[:using]}"
+ elsif options[:cast_as]
+ cast_as_type = type_to_sql(options[:cast_as], options)
+ sql << " USING CAST(#{quoted_column_name} AS #{cast_as_type})"
+ end
+
+ sql
+ end
+
+ def add_column_for_alter(table_name, column_name, type, options = {})
+ return super unless options.key?(:comment)
+ [super, Proc.new { change_column_comment(table_name, column_name, options[:comment]) }]
+ end
+
+ def change_column_for_alter(table_name, column_name, type, options = {})
+ sqls = [change_column_sql(table_name, column_name, type, options)]
+ sqls << change_column_default_for_alter(table_name, column_name, options[:default]) if options.key?(:default)
+ sqls << change_column_null_for_alter(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
+ sqls << Proc.new { change_column_comment(table_name, column_name, options[:comment]) } if options.key?(:comment)
+ sqls
+ end
+
+ # Changes the default value of a table column.
+ def change_column_default_for_alter(table_name, column_name, default_or_changes) # :nodoc:
+ column = column_for(table_name, column_name)
+ return unless column
+
+ default = extract_new_default_value(default_or_changes)
+ alter_column_query = "ALTER COLUMN #{quote_column_name(column_name)} %s"
+ if default.nil?
+ # <tt>DEFAULT NULL</tt> results in the same behavior as <tt>DROP DEFAULT</tt>. However, PostgreSQL will
+ # cast the default to the columns type, which leaves us with a default like "default NULL::character varying".
+ alter_column_query % "DROP DEFAULT"
+ else
+ alter_column_query % "SET DEFAULT #{quote_default_expression(default, column)}"
+ end
+ end
+
+ def change_column_null_for_alter(table_name, column_name, null, default = nil) #:nodoc:
+ "ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL"
+ end
+
+ def add_timestamps_for_alter(table_name, options = {})
+ [add_column_for_alter(table_name, :created_at, :datetime, options), add_column_for_alter(table_name, :updated_at, :datetime, options)]
+ end
+
+ def remove_timestamps_for_alter(table_name, options = {})
+ [remove_column_for_alter(table_name, :updated_at), remove_column_for_alter(table_name, :created_at)]
+ end
+
+ def add_index_opclass(quoted_columns, **options)
+ opclasses = options_for_index_columns(options[:opclass])
+ quoted_columns.each do |name, column|
+ column << " #{opclasses[name]}" if opclasses[name].present?
+ end
+ end
+
+ def add_options_for_index_columns(quoted_columns, **options)
+ quoted_columns = add_index_opclass(quoted_columns, options)
+ super
+ end
+
+ def data_source_sql(name = nil, type: nil)
+ scope = quoted_scope(name, type: type)
+ scope[:type] ||= "'r','v','m','p','f'" # (r)elation/table, (v)iew, (m)aterialized view, (p)artitioned table, (f)oreign table
+
+ 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','p'"
+ when "VIEW"
+ "'v','m'"
+ when "FOREIGN TABLE"
+ "'f'"
+ 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 58715978f7..cd69d28139 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,11 @@
+# frozen_string_literal: true
+
module ActiveRecord
+ # :stopdoc:
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)
@@ -8,7 +13,7 @@ module ActiveRecord
@type_metadata = type_metadata
@oid = oid
@fmod = fmod
- @array = /\[\]$/ === type_metadata.sql_type
+ @array = /\[\]$/.match?(type_metadata.sql_type)
end
def sql_type
@@ -27,9 +32,9 @@ module ActiveRecord
protected
- def attributes_for_hash
- [self.class, @type_metadata, oid, fmod]
- end
+ def attributes_for_hash
+ [self.class, @type_metadata, oid, fmod]
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb
index 9a0b80d7d3..bfd300723d 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
@@ -19,9 +21,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 +37,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 +50,6 @@ module ActiveRecord
part
end
end
-
- def parts
- @parts ||= [@schema, @identifier].compact
- end
end
module Utils # :nodoc:
@@ -53,7 +57,7 @@ module ActiveRecord
# Returns an instance of <tt>ActiveRecord::ConnectionAdapters::PostgreSQL::Name</tt>
# extracted from +string+.
- # +schema+ is nil if not specified in +string+.
+ # +schema+ is +nil+ if not specified in +string+.
# +schema+ and +identifier+ exclude surrounding quotes (regardless of whether provided in +string+)
# +string+ supports the range of schema/table references understood by PostgreSQL, for example:
#
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 2c43c46a3d..d2ed699ee2 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -1,29 +1,34 @@
+# frozen_string_literal: true
+
# Make sure we're using pg high enough for type casts and Ruby 2.2+ compatibility
-gem 'pg', '~> 0.18'
-require 'pg'
+gem "pg", ">= 0.18", "< 2.0"
+require "pg"
+
+# Use async_exec instead of exec_params on pg versions before 1.1
+class ::PG::Connection # :nodoc:
+ unless self.public_method_defined?(:async_exec_params)
+ remove_method :exec_params
+ alias exec_params async_exec
+ end
+end
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"
-
-require 'ipaddr'
module ActiveRecord
module ConnectionHandling # :nodoc:
- VALID_CONN_PARAMS = [:host, :hostaddr, :port, :dbname, :user, :password, :connect_timeout,
- :client_encoding, :options, :application_name, :fallback_application_name,
- :keepalives, :keepalives_idle, :keepalives_interval, :keepalives_count,
- :tty, :sslmode, :requiressl, :sslcompression, :sslcert, :sslkey,
- :sslrootcert, :sslcrl, :requirepeer, :krbsrvname, :gsslib, :service]
-
# Establishes a connection to the database that's used by all Active Record objects
def postgresql_connection(config)
conn_params = config.symbolize_keys
@@ -34,12 +39,18 @@ 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.
- conn_params.keep_if { |k, _| VALID_CONN_PARAMS.include?(k) }
+ # 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,
- # so just pass a nil connection object for the time being.
- ConnectionAdapters::PostgreSQLAdapter.new(nil, logger, conn_params, config)
+ conn = PG.connect(conn_params)
+ ConnectionAdapters::PostgreSQLAdapter.new(conn, logger, conn_params, config)
+ rescue ::PG::Error => error
+ if error.message.include?("does not exist")
+ raise ActiveRecord::NoDatabaseError
+ else
+ raise
+ end
end
end
@@ -66,20 +77,32 @@ module ActiveRecord
# defaults to true.
#
# Any further options are used as connection parameters to libpq. See
- # http://www.postgresql.org/docs/current/static/libpq-connect.html for the
+ # https://www.postgresql.org/docs/current/static/libpq-connect.html for the
# list of parameters.
#
# In addition, default connection parameters of libpq can be set per environment variables.
- # See http://www.postgresql.org/docs/current/static/libpq-envars.html .
+ # See https://www.postgresql.org/docs/current/static/libpq-envars.html .
class PostgreSQLAdapter < AbstractAdapter
- ADAPTER_NAME = 'PostgreSQL'.freeze
+ ADAPTER_NAME = "PostgreSQL"
+
+ ##
+ # :singleton-method:
+ # PostgreSQL allows the creation of "unlogged" tables, which do not record
+ # data in the PostgreSQL Write-Ahead Log. This can make the tables faster,
+ # but significantly increases the risk of data loss if the database
+ # crashes. As a result, this should not be used in production
+ # environments. If you would like all created tables to be unlogged in
+ # the test environment you can add the following line to your test.rb
+ # file:
+ #
+ # ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables = true
+ class_attribute :create_unlogged_tables, default: false
NATIVE_DATABASE_TYPES = {
- primary_key: "serial primary key",
- bigserial: "bigserial",
+ primary_key: "bigserial primary key",
string: { name: "character varying" },
text: { name: "text" },
- integer: { name: "integer" },
+ integer: { name: "integer", limit: 4 },
float: { name: "float" },
decimal: { name: "decimal" },
datetime: { name: "timestamp" },
@@ -93,7 +116,6 @@ module ActiveRecord
int8range: { name: "int8range" },
binary: { name: "bytea" },
boolean: { name: "boolean" },
- bigint: { name: "bigint" },
xml: { name: "xml" },
tsvector: { name: "tsvector" },
hstore: { name: "hstore" },
@@ -106,9 +128,17 @@ module ActiveRecord
ltree: { name: "ltree" },
citext: { name: "citext" },
point: { name: "point" },
+ line: { name: "line" },
+ lseg: { name: "lseg" },
+ box: { name: "box" },
+ path: { name: "path" },
+ polygon: { name: "polygon" },
+ circle: { name: "circle" },
bit: { name: "bit" },
bit_varying: { name: "bit varying" },
money: { name: "money" },
+ interval: { name: "interval" },
+ oid: { name: "oid" },
}
OID = PostgreSQL::OID #:nodoc:
@@ -117,97 +147,63 @@ module ActiveRecord
include PostgreSQL::ReferentialIntegrity
include PostgreSQL::SchemaStatements
include PostgreSQL::DatabaseStatements
- include Savepoints
-
- def schema_creation # :nodoc:
- PostgreSQL::SchemaCreation.new self
- end
- def column_spec_for_primary_key(column)
- spec = {}
- if column.serial?
- return unless column.bigint?
- spec[:id] = ':bigserial'
- elsif column.type == :uuid
- spec[:id] = ':uuid'
- spec[:default] = column.default_function.inspect
- else
- spec[:id] = column.type.inspect
- spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type, :null].include?(key) })
- end
- spec
+ def supports_bulk_alter?
+ true
end
- # Adds +:array+ option to the default set provided by the
- # AbstractAdapter
- def prepare_column_options(column) # :nodoc:
- spec = super
- spec[:array] = 'true' if column.array?
- spec
+ def supports_index_sort_order?
+ true
end
- # Adds +:array+ as a valid migration key
- def migration_keys
- super + [:array]
+ def supports_partial_index?
+ true
end
- def schema_type(column)
- return super unless column.serial?
-
- if column.bigint?
- 'bigserial'
- else
- 'serial'
- end
+ def supports_expression_index?
+ true
end
- private :schema_type
- def schema_default(column)
- if column.default_function
- column.default_function.inspect unless column.serial?
- else
- super
- end
+ def supports_transaction_isolation?
+ true
end
- private :schema_default
- # Returns +true+, since this connection adapter supports prepared statement
- # caching.
- def supports_statement_cache?
+ def supports_foreign_keys?
true
end
- def supports_index_sort_order?
+ def supports_validate_constraints?
true
end
- def supports_partial_index?
+ def supports_views?
true
end
- def supports_transaction_isolation?
+ def supports_datetime_with_precision?
true
end
- def supports_foreign_keys?
- true
+ def supports_json?
+ postgresql_version >= 90200
end
- def supports_views?
+ def supports_comments?
true
end
- def supports_datetime_with_precision?
+ def supports_savepoints?
true
end
def index_algorithms
- { concurrently: 'CONCURRENTLY' }
+ { concurrently: "CONCURRENTLY" }
end
- class StatementPool < ConnectionAdapters::StatementPool
+ class StatementPool < ConnectionAdapters::StatementPool # :nodoc:
def initialize(connection, max)
- super
+ super(max)
+ @connection = connection
@counter = 0
end
@@ -220,55 +216,46 @@ module ActiveRecord
end
private
-
def dealloc(key)
@connection.query "DEALLOCATE #{key}" if connection_active?
+ rescue PG::Error
end
def connection_active?
- @connection.status == PGconn::CONNECTION_OK
- rescue PGError
+ @connection.status == PG::CONNECTION_OK
+ rescue PG::Error
false
end
end
# Initializes and connects a PostgreSQL adapter.
def initialize(connection, logger, connection_parameters, config)
- super(connection, logger)
-
- @visitor = Arel::Visitors::PostgreSQL.new self
- if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
- @prepared_statements = true
- else
- @prepared_statements = false
- end
+ super(connection, logger, config)
- @connection_parameters, @config = connection_parameters, config
+ @connection_parameters = connection_parameters
# @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
+ configure_connection
add_pg_encoders
@statements = StatementPool.new @connection,
- self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 })
-
- if postgresql_version < 80200
- raise "Your version of PostgreSQL (#{postgresql_version}) is too old, please upgrade!"
- end
+ self.class.type_cast_config_to_integer(config[:statement_limit])
add_pg_decoders
@type_map = Type::HashLookupTypeMap.new
- initialize_type_map(type_map)
- @local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"]
+ initialize_type_map
+ @local_tz = execute("SHOW TIME ZONE", "SCHEMA").first["TimeZone"]
@use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : true
end
# Clears the prepared statements cache.
def clear_cache!
- @statements.clear
+ @lock.synchronize do
+ @statements.clear
+ end
end
def truncate(table_name, name = nil)
@@ -277,59 +264,62 @@ module ActiveRecord
# Is this connection alive and ready for queries?
def active?
- @connection.query 'SELECT 1'
+ @lock.synchronize do
+ @connection.query "SELECT 1"
+ end
true
- rescue PGError
+ rescue PG::Error
false
end
# Close then reopen the connection.
def reconnect!
- super
- @connection.reset
- configure_connection
+ @lock.synchronize do
+ super
+ @connection.reset
+ configure_connection
+ end
end
def reset!
- clear_cache!
- reset_transaction
- unless @connection.transaction_status == ::PG::PQTRANS_IDLE
- @connection.query 'ROLLBACK'
+ @lock.synchronize do
+ clear_cache!
+ reset_transaction
+ unless @connection.transaction_status == ::PG::PQTRANS_IDLE
+ @connection.query "ROLLBACK"
+ end
+ @connection.query "DISCARD ALL"
+ configure_connection
end
- @connection.query 'DISCARD ALL'
- configure_connection
end
# Disconnects from the database if already connected. Otherwise, this
# method does nothing.
def disconnect!
- super
- @connection.close rescue nil
+ @lock.synchronize do
+ super
+ @connection.close rescue nil
+ end
+ end
+
+ def discard! # :nodoc:
+ @connection.socket_io.reopen(IO::NULL) rescue nil
+ @connection = nil
end
def native_database_types #:nodoc:
NATIVE_DATABASE_TYPES
end
- # Returns true, since this connection adapter supports migrations.
- def supports_migrations?
- true
+ def set_standard_conforming_strings
+ execute("SET standard_conforming_strings = on", "SCHEMA")
end
- # Does PostgreSQL support finding primary key on non-Active Record tables?
- def supports_primary_key? #:nodoc:
+ def supports_ddl_transactions?
true
end
- # Enable standard-conforming strings if available.
- def set_standard_conforming_strings
- old, self.client_min_messages = client_min_messages, 'panic'
- execute('SET standard_conforming_strings = on', 'SCHEMA') rescue nil
- ensure
- self.client_min_messages = old
- end
-
- def supports_ddl_transactions?
+ def supports_advisory_locks?
true
end
@@ -337,13 +327,12 @@ module ActiveRecord
true
end
- # Returns true if pg > 9.1
def supports_extensions?
- postgresql_version >= 90100
+ 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
@@ -351,6 +340,32 @@ module ActiveRecord
postgresql_version >= 90300
end
+ def supports_foreign_tables?
+ postgresql_version >= 90300
+ end
+
+ def supports_pgcrypto_uuid?
+ postgresql_version >= 90400
+ end
+
+ def supports_lazy_transactions?
+ true
+ end
+
+ def get_advisory_lock(lock_id) # :nodoc:
+ unless lock_id.is_a?(Integer) && lock_id.bit_length <= 63
+ raise(ArgumentError, "PostgreSQL requires advisory lock ids to be a signed 64 bit integer")
+ end
+ query_value("SELECT pg_try_advisory_lock(#{lock_id})")
+ end
+
+ def release_advisory_lock(lock_id) # :nodoc:
+ unless lock_id.is_a?(Integer) && lock_id.bit_length <= 63
+ raise(ArgumentError, "PostgreSQL requires advisory lock ids to be a signed 64 bit integer")
+ end
+ query_value("SELECT pg_advisory_unlock(#{lock_id})")
+ end
+
def enable_extension(name)
exec_query("CREATE EXTENSION IF NOT EXISTS \"#{name}\"").tap {
reload_type_map
@@ -364,49 +379,31 @@ module ActiveRecord
end
def extension_enabled?(name)
- if supports_extensions?
- res = exec_query "SELECT EXISTS(SELECT * FROM pg_available_extensions WHERE name = '#{name}' AND installed_version IS NOT NULL) as enabled",
- 'SCHEMA'
- res.cast_values.first
- end
+ res = exec_query("SELECT EXISTS(SELECT * FROM pg_available_extensions WHERE name = '#{name}' AND installed_version IS NOT NULL) as enabled", "SCHEMA")
+ res.cast_values.first
end
def extensions
- if supports_extensions?
- exec_query("SELECT extname from pg_extension", "SCHEMA").cast_values
- else
- super
- end
+ exec_query("SELECT extname FROM pg_extension", "SCHEMA").cast_values
end
# Returns the configured supported identifier length supported by PostgreSQL
- def table_alias_length
- @table_alias_length ||= query('SHOW max_identifier_length', 'SCHEMA')[0][0].to_i
+ def max_identifier_length
+ @max_identifier_length ||= query_value("SHOW max_identifier_length", "SCHEMA").to_i
end
+ alias table_alias_length max_identifier_length
+ alias index_name_length max_identifier_length
# Set the authorized user for this session
def session_auth=(user)
clear_cache!
- exec_query "SET SESSION AUTHORIZATION #{user}"
+ execute("SET SESSION AUTHORIZATION #{user}")
end
def use_insert_returning?
@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
-
- def lookup_cast_type(sql_type) # :nodoc:
- oid = execute("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").first['oid'].to_i
- super(oid)
- end
-
def column_name_for_operation(operation, node) # :nodoc:
OPERATION_ALIASES.fetch(operation) { operation.downcase }
end
@@ -417,91 +414,121 @@ module ActiveRecord
"average" => "avg",
}
- protected
+ # Returns the version of the connected PostgreSQL server.
+ def postgresql_version
+ @connection.server_version
+ end
- # Returns the version of the connected PostgreSQL server.
- def postgresql_version
- @connection.server_version
+ def default_index_type?(index) # :nodoc:
+ index.using == :btree || super
+ end
+
+ private
+ def check_version
+ if postgresql_version < 90100
+ raise "Your version of PostgreSQL (#{postgresql_version}) is too old. Active Record supports PostgreSQL >= 9.1."
+ end
end
- # See http://www.postgresql.org/docs/current/static/errcodes-appendix.html
+ # See https://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"
+ DEADLOCK_DETECTED = "40P01"
+ LOCK_NOT_AVAILABLE = "55P03"
+ QUERY_CANCELED = "57014"
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, exception)
+ RecordNotUnique.new(message)
when FOREIGN_KEY_VIOLATION
- InvalidForeignKey.new(message, exception)
+ 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
+ Deadlocked.new(message)
+ when LOCK_NOT_AVAILABLE
+ LockWaitTimeout.new(message)
+ when QUERY_CANCELED
+ QueryCanceled.new(message)
else
super
end
end
- private
-
- def get_oid_type(oid, fmod, column_name, sql_type = '') # :nodoc:
+ def get_oid_type(oid, fmod, column_name, sql_type = "")
if !type_map.key?(oid)
- load_additional_types(type_map, [oid])
+ load_additional_types([oid])
end
type_map.fetch(oid, fmod, sql_type) {
warn "unknown OID #{oid}: failed to recognize type of '#{column_name}'. It will be treated as String."
- Type::Value.new.tap do |cast_type|
+ Type.default_value.tap do |cast_type|
type_map.register_type(oid, cast_type)
end
}
end
- def initialize_type_map(m) # :nodoc:
- 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 'float4', Type::Float.new
- m.alias_type 'float8', 'float4'
- m.register_type 'text', Type::Text.new
- register_class_with_limit m, 'varchar', Type::String
- m.alias_type 'char', 'varchar'
- m.alias_type 'name', 'varchar'
- m.alias_type 'bpchar', 'varchar'
- m.register_type 'bool', Type::Boolean.new
- register_class_with_limit m, 'bit', OID::Bit
- register_class_with_limit m, 'varbit', OID::BitVarying
- m.alias_type 'timestamptz', 'timestamp'
- m.register_type 'date', Type::Date.new
-
- m.register_type 'money', OID::Money.new
- m.register_type 'bytea', OID::Bytea.new
- m.register_type 'point', OID::Point.new
- m.register_type 'hstore', OID::Hstore.new
- m.register_type 'json', OID::Json.new
- m.register_type 'jsonb', OID::Jsonb.new
- m.register_type 'cidr', OID::Cidr.new
- m.register_type 'inet', OID::Inet.new
- m.register_type 'uuid', OID::Uuid.new
- m.register_type 'xml', OID::Xml.new
- m.register_type 'tsvector', OID::SpecializedString.new(:tsvector)
- m.register_type 'macaddr', OID::SpecializedString.new(:macaddr)
- m.register_type 'citext', OID::SpecializedString.new(:citext)
- m.register_type 'ltree', OID::SpecializedString.new(:ltree)
-
- # FIXME: why are we keeping these types as strings?
- m.alias_type 'interval', 'varchar'
- m.alias_type 'path', 'varchar'
- m.alias_type 'line', 'varchar'
- m.alias_type 'polygon', 'varchar'
- m.alias_type 'circle', 'varchar'
- m.alias_type 'lseg', 'varchar'
- m.alias_type 'box', 'varchar'
-
- register_class_with_precision m, 'time', Type::Time
- register_class_with_precision m, 'timestamp', OID::DateTime
-
- m.register_type 'numeric' do |_, fmod, sql_type|
+ def initialize_type_map(m = type_map)
+ m.register_type "int2", Type::Integer.new(limit: 2)
+ m.register_type "int4", Type::Integer.new(limit: 4)
+ m.register_type "int8", Type::Integer.new(limit: 8)
+ 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
+ register_class_with_limit m, "varchar", Type::String
+ m.alias_type "char", "varchar"
+ m.alias_type "name", "varchar"
+ m.alias_type "bpchar", "varchar"
+ m.register_type "bool", Type::Boolean.new
+ register_class_with_limit m, "bit", OID::Bit
+ register_class_with_limit m, "varbit", OID::BitVarying
+ m.alias_type "timestamptz", "timestamp"
+ m.register_type "date", OID::Date.new
+
+ m.register_type "money", OID::Money.new
+ m.register_type "bytea", OID::Bytea.new
+ m.register_type "point", OID::Point.new
+ m.register_type "hstore", OID::Hstore.new
+ m.register_type "json", Type::Json.new
+ m.register_type "jsonb", OID::Jsonb.new
+ m.register_type "cidr", OID::Cidr.new
+ m.register_type "inet", OID::Inet.new
+ m.register_type "uuid", OID::Uuid.new
+ m.register_type "xml", OID::Xml.new
+ m.register_type "tsvector", OID::SpecializedString.new(:tsvector)
+ m.register_type "macaddr", OID::SpecializedString.new(:macaddr)
+ m.register_type "citext", OID::SpecializedString.new(:citext)
+ m.register_type "ltree", OID::SpecializedString.new(:ltree)
+ m.register_type "line", OID::SpecializedString.new(:line)
+ m.register_type "lseg", OID::SpecializedString.new(:lseg)
+ m.register_type "box", OID::SpecializedString.new(:box)
+ m.register_type "path", OID::SpecializedString.new(:path)
+ m.register_type "polygon", OID::SpecializedString.new(:polygon)
+ m.register_type "circle", OID::SpecializedString.new(:circle)
+
+ 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
+
+ m.register_type "numeric" do |_, fmod, sql_type|
precision = extract_precision(sql_type)
scale = extract_scale(sql_type)
@@ -521,51 +548,45 @@ module ActiveRecord
end
end
- load_additional_types(m)
- end
-
- def extract_limit(sql_type) # :nodoc:
- case sql_type
- when /^bigint/i, /^int8/i
- 8
- when /^smallint/i
- 2
- else
- super
- end
+ load_additional_types
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]?'(.*)'::/m
- $1.gsub("''".freeze, "'".freeze)
+ when /\A[\(B]?'(.*)'.*::"?([\w. ]+)"?(?:\[\])?\z/m
+ # The default 'now'::date is CURRENT_DATE
+ if $1 == "now" && $2 == "date"
+ nil
+ else
+ $1.gsub("''", "'")
+ end
# Boolean types
- when 'true'.freeze, 'false'.freeze
- default
+ when "true", "false"
+ default
# Numeric types
- when /\A\(?(-?\d+(\.\d*)?)\)?(::bigint)?\z/
- $1
+ when /\A\(?(-?\d+(\.\d*)?)\)?(::bigint)?\z/
+ $1
# Object identifier types
- when /\A-?\d+\z/
- $1
- else
- # Anything else is blank, some user type, or some function
- # and we can't know the value of that, so return nil.
- nil
+ when /\A-?\d+\z/
+ $1
+ else
+ # Anything else is blank, some user type, or some function
+ # and we can't know the value of that, so return nil.
+ nil
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:
- !default_value && (%r{\w+\(.*\)} === default)
+ def has_default_function?(default_value, default)
+ !default_value && %r{\w+\(.*\)|\(.*\)::\w+|CURRENT_DATE|CURRENT_TIMESTAMP}.match?(default)
end
- def load_additional_types(type_map, oids = nil) # :nodoc:
+ def load_additional_types(oids = nil)
initializer = OID::TypeMapInitializer.new(type_map)
if supports_ranges?
@@ -584,55 +605,89 @@ module ActiveRecord
if oids
query += "WHERE t.oid::integer IN (%s)" % oids.join(", ")
else
- query += initializer.query_conditions_for_initial_load(type_map)
+ query += initializer.query_conditions_for_initial_load
end
- execute_and_clear(query, 'SCHEMA', []) do |records|
+ execute_and_clear(query, "SCHEMA", []) do |records|
initializer.run(records)
end
end
FEATURE_NOT_SUPPORTED = "0A000" #:nodoc:
- def execute_and_clear(sql, name, binds)
- result = without_prepared_statement?(binds) ? exec_no_cache(sql, name, binds) :
- exec_cache(sql, name, binds)
+ def execute_and_clear(sql, name, binds, prepare: false)
+ if without_prepared_statement?(binds)
+ result = exec_no_cache(sql, name, [])
+ elsif !prepare
+ result = exec_no_cache(sql, name, binds)
+ else
+ result = exec_cache(sql, name, binds)
+ end
ret = yield result
result.clear
ret
end
def exec_no_cache(sql, name, binds)
- log(sql, name, binds) { @connection.async_exec(sql, []) }
+ materialize_transactions
+
+ type_casted_binds = type_casted_binds(binds)
+ log(sql, name, binds, type_casted_binds) do
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
+ @connection.exec_params(sql, type_casted_binds)
+ end
+ end
end
def exec_cache(sql, name, binds)
+ materialize_transactions
+
stmt_key = prepare_statement(sql)
- type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) }
+ type_casted_binds = type_casted_binds(binds)
- log(sql, name, binds, stmt_key) do
- @connection.exec_prepared(stmt_key, type_casted_binds)
+ log(sql, name, binds, type_casted_binds, stmt_key) do
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
+ @connection.exec_prepared(stmt_key, type_casted_binds)
+ end
end
rescue ActiveRecord::StatementInvalid => e
- pgerror = e.original_exception
-
- # Get the PG code for the failure. Annoyingly, the code for
- # prepared statements whose return value may have changed is
- # FEATURE_NOT_SUPPORTED. Check here for more details:
- # http://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573
- begin
- code = pgerror.result.result_error_field(PGresult::PG_DIAG_SQLSTATE)
- rescue
- raise e
- end
- if FEATURE_NOT_SUPPORTED == code
- @statements.delete sql_key(sql)
- retry
+ raise unless is_cached_plan_failure?(e)
+
+ # Nothing we can do if we are in a transaction because all commands
+ # will raise InFailedSQLTransaction
+ if in_transaction?
+ raise ActiveRecord::PreparedStatementCacheExpired.new(e.cause.message)
else
- raise e
+ @lock.synchronize do
+ # outside of transactions we can simply flush this query and retry
+ @statements.delete sql_key(sql)
+ end
+ retry
end
end
+ # Annoyingly, the code for prepared statements whose return value may
+ # have changed is FEATURE_NOT_SUPPORTED.
+ #
+ # This covers various different error types so we need to do additional
+ # work to classify the exception definitively as a
+ # ActiveRecord::PreparedStatementCacheExpired
+ #
+ # Check here for more details:
+ # https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573
+ CACHED_PLAN_HEURISTIC = "cached plan must not change result type"
+ def is_cached_plan_failure?(e)
+ pgerror = e.cause
+ code = pgerror.result.result_error_field(PG::PG_DIAG_SQLSTATE)
+ code == FEATURE_NOT_SUPPORTED && pgerror.message.include?(CACHED_PLAN_HEURISTIC)
+ rescue
+ false
+ end
+
+ def in_transaction?
+ open_transactions > 0
+ end
+
# Returns the statement identifier for the client side cache
# of statements
def sql_key(sql)
@@ -642,38 +697,28 @@ 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)
-
- # Money type has a fixed precision of 10 in PostgreSQL 8.2 and below, and as of
- # PostgreSQL 8.3 it has a fixed precision of 19. PostgreSQLColumn.extract_precision
- # should know about this but can't detect it there, so deal with it here.
- OID::Money.precision = (postgresql_version >= 80300) ? 19 : 10
-
+ @connection = PG.connect(@connection_parameters)
configure_connection
- rescue ::PG::Error => error
- if error.message.include?("does not exist")
- raise ActiveRecord::NoDatabaseError.new(error.message, error)
- else
- raise
- end
end
# Configures the encoding, verbosity, schema search path, and time zone of the connection.
@@ -682,51 +727,40 @@ module ActiveRecord
if @config[:encoding]
@connection.set_client_encoding(@config[:encoding])
end
- self.client_min_messages = @config[:min_messages] || 'warning'
+ self.client_min_messages = @config[:min_messages] || "warning"
self.schema_search_path = @config[:schema_search_path] || @config[:schema_order]
- # Use standard-conforming strings if available so we don't have to do the E'...' dance.
+ # Use standard-conforming strings so we don't have to do the E'...' dance.
set_standard_conforming_strings
+ variables = @config.fetch(:variables, {}).stringify_keys
+
# If using Active Record's time zone support configure the connection to return
# TIMESTAMP WITH ZONE types in UTC.
- # (SET TIME ZONE does not use an equals sign like other SET variables)
- if ActiveRecord::Base.default_timezone == :utc
- execute("SET time zone 'UTC'", 'SCHEMA')
- elsif @local_tz
- execute("SET time zone '#{@local_tz}'", 'SCHEMA')
+ unless variables["timezone"]
+ if ActiveRecord::Base.default_timezone == :utc
+ variables["timezone"] = "UTC"
+ elsif @local_tz
+ variables["timezone"] = @local_tz
+ end
end
# SET statements from :variables config hash
- # http://www.postgresql.org/docs/current/static/sql-set.html
- variables = @config[:variables] || {}
+ # https://www.postgresql.org/docs/current/static/sql-set.html
variables.map do |k, v|
- if v == ':default' || v == :default
+ if v == ":default" || v == :default
# Sets the value to the global or compile default
- execute("SET SESSION #{k} TO DEFAULT", 'SCHEMA')
+ execute("SET SESSION #{k} TO DEFAULT", "SCHEMA")
elsif !v.nil?
- execute("SET SESSION #{k} TO #{quote(v)}", 'SCHEMA')
+ execute("SET SESSION #{k} TO #{quote(v)}", "SCHEMA")
end
end
end
- # Returns the current ID of a table's sequence.
- def last_insert_id(sequence_name) #:nodoc:
- Integer(last_insert_id_value(sequence_name))
- end
-
- def last_insert_id_value(sequence_name)
- last_insert_id_result(sequence_name).rows.first.first
- end
-
- def last_insert_id_result(sequence_name) #:nodoc:
- exec_query("SELECT currval('#{sequence_name}')", 'SQL')
- end
-
# Returns the list of a table's column names, data types, and default values.
#
# The underlying query is roughly:
- # SELECT column.name, column.type, default.value
+ # SELECT column.name, column.type, default.value, column.comment
# FROM column LEFT JOIN default
# ON column.table_id = default.table_id
# AND column.num = default.column_num
@@ -741,27 +775,28 @@ 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:
- query(<<-end_sql, 'SCHEMA')
+ 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)
- FROM pg_attribute a LEFT JOIN pg_attrdef d
- ON a.attrelid = d.adrelid AND a.attnum = d.adnum
- WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass
+ 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:
- sql[/into\s+([^\(]*).*values\s*\(/im]
+ 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(name, temporary = false, options = nil, as = nil) # :nodoc:
- PostgreSQL::TableDefinition.new native_database_types, name, temporary, options, as
+ def arel_visitor
+ Arel::Visitors::PostgreSQL.new(self)
end
def can_perform_case_insensitive_comparison_for?(column)
@@ -770,10 +805,14 @@ module ActiveRecord
sql = <<-end_sql
SELECT exists(
SELECT * FROM pg_proc
+ WHERE proname = 'lower'
+ AND proargtypes = ARRAY[#{quote column.sql_type}::regtype]::oidvector
+ ) OR exists(
+ SELECT * FROM pg_proc
INNER JOIN pg_cast
- ON casttarget::text::oidvector = proargtypes
+ ON ARRAY[casttarget]::oidvector = proargtypes
WHERE proname = 'lower'
- AND castsource = '#{column.sql_type}'::regtype::oid
+ AND castsource = #{quote column.sql_type}::regtype
)
end_sql
execute_and_clear(sql, "SCHEMA", []) do |result|
@@ -787,19 +826,18 @@ 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
def add_pg_decoders
coders_by_name = {
- 'int2' => PG::TextDecoder::Integer,
- 'int4' => PG::TextDecoder::Integer,
- 'int8' => PG::TextDecoder::Integer,
- 'oid' => PG::TextDecoder::Integer,
- 'float4' => PG::TextDecoder::Float,
- 'float8' => PG::TextDecoder::Float,
- 'bool' => PG::TextDecoder::Boolean,
+ "int2" => PG::TextDecoder::Integer,
+ "int4" => PG::TextDecoder::Integer,
+ "int8" => PG::TextDecoder::Integer,
+ "oid" => PG::TextDecoder::Integer,
+ "float4" => PG::TextDecoder::Float,
+ "float8" => PG::TextDecoder::Float,
+ "bool" => PG::TextDecoder::Boolean,
}
known_coder_types = coders_by_name.keys.map { |n| quote(n) }
query = <<-SQL % known_coder_types.join(", ")
@@ -809,7 +847,7 @@ module ActiveRecord
SQL
coders = execute_and_clear(query, "SCHEMA", []) do |result|
result
- .map { |row| construct_coder(row, coders_by_name[row['typname']]) }
+ .map { |row| construct_coder(row, coders_by_name[row["typname"]]) }
.compact
end
@@ -820,7 +858,7 @@ module ActiveRecord
def construct_coder(row, coder_class)
return unless coder_class
- coder_class.new(oid: row['oid'].to_i, name: row['typname'])
+ coder_class.new(oid: row["oid"].to_i, name: row["typname"])
end
ActiveRecord::Type.add_modifier({ array: true }, OID::Array, adapter: :postgresql)
@@ -829,17 +867,16 @@ module ActiveRecord
ActiveRecord::Type.register(:bit_varying, OID::BitVarying, adapter: :postgresql)
ActiveRecord::Type.register(:binary, OID::Bytea, adapter: :postgresql)
ActiveRecord::Type.register(:cidr, OID::Cidr, adapter: :postgresql)
- ActiveRecord::Type.register(:date_time, OID::DateTime, adapter: :postgresql)
+ ActiveRecord::Type.register(:date, OID::Date, adapter: :postgresql)
+ ActiveRecord::Type.register(:datetime, OID::DateTime, adapter: :postgresql)
ActiveRecord::Type.register(:decimal, OID::Decimal, adapter: :postgresql)
ActiveRecord::Type.register(:enum, OID::Enum, adapter: :postgresql)
ActiveRecord::Type.register(:hstore, OID::Hstore, adapter: :postgresql)
ActiveRecord::Type.register(:inet, OID::Inet, adapter: :postgresql)
- ActiveRecord::Type.register(:json, OID::Json, adapter: :postgresql)
ActiveRecord::Type.register(:jsonb, OID::Jsonb, adapter: :postgresql)
ActiveRecord::Type.register(:money, OID::Money, adapter: :postgresql)
ActiveRecord::Type.register(:point, OID::Point, adapter: :postgresql)
- ActiveRecord::Type.register(:legacy_point, OID::Point, adapter: :postgresql)
- ActiveRecord::Type.register(:rails_5_1_point, OID::Rails51Point, adapter: :postgresql)
+ ActiveRecord::Type.register(:legacy_point, OID::LegacyPoint, adapter: :postgresql)
ActiveRecord::Type.register(:uuid, OID::Uuid, adapter: :postgresql)
ActiveRecord::Type.register(:vector, OID::Vector, adapter: :postgresql)
ActiveRecord::Type.register(:xml, OID::Xml, adapter: :postgresql)
diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
index 981d5d7a3c..c29cf1f9a1 100644
--- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
class SchemaCache
@@ -10,7 +12,7 @@ module ActiveRecord
@columns = {}
@columns_hash = {}
@primary_keys = {}
- @tables = {}
+ @data_sources = {}
end
def initialize_dup(other)
@@ -18,32 +20,48 @@ module ActiveRecord
@columns = @columns.dup
@columns_hash = @columns_hash.dup
@primary_keys = @primary_keys.dup
- @tables = @tables.dup
+ @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"] = connection.migration_context.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] ||= table_exists?(table_name) ? connection.primary_key(table_name) : nil
+ @primary_keys[table_name] ||= data_source_exists?(table_name) ? connection.primary_key(table_name) : nil
end
# A cached lookup for table existence.
- def table_exists?(name)
- prepare_tables if @tables.empty?
- return @tables[name] if @tables.key? name
+ def data_source_exists?(name)
+ prepare_data_sources if @data_sources.empty?
+ return @data_sources[name] if @data_sources.key? name
- @tables[name] = connection.table_exists?(name)
+ @data_sources[name] = connection.data_source_exists?(name)
end
# Add internal cache for table with +table_name+.
def add(table_name)
- if table_exists?(table_name)
+ if data_source_exists?(table_name)
primary_keys(table_name)
columns(table_name)
columns_hash(table_name)
end
end
- def tables(name)
- @tables[name]
+ def data_sources(name)
+ @data_sources[name]
end
# Get the columns for a table
@@ -64,36 +82,36 @@ module ActiveRecord
@columns.clear
@columns_hash.clear
@primary_keys.clear
- @tables.clear
+ @data_sources.clear
@version = nil
end
def size
- [@columns, @columns_hash, @primary_keys, @tables].map(&:size).inject :+
+ [@columns, @columns_hash, @primary_keys, @data_sources].map(&:size).inject :+
end
- # Clear out internal caches for table with +table_name+.
- def clear_table_cache!(table_name)
- @columns.delete table_name
- @columns_hash.delete table_name
- @primary_keys.delete table_name
- @tables.delete table_name
+ # Clear out internal caches for the data source +name+.
+ def clear_data_source_cache!(name)
+ @columns.delete name
+ @columns_hash.delete name
+ @primary_keys.delete name
+ @data_sources.delete name
end
def marshal_dump
# if we get current version during initialization, it happens stack over flow.
- @version = ActiveRecord::Migrator.current_version
- [@version, @columns, @columns_hash, @primary_keys, @tables]
+ @version = connection.migration_context.current_version
+ [@version, @columns, @columns_hash, @primary_keys, @data_sources]
end
def marshal_load(array)
- @version, @columns, @columns_hash, @primary_keys, @tables = array
+ @version, @columns, @columns_hash, @primary_keys, @data_sources = array
end
private
- def prepare_tables
- connection.tables.each { |table| @tables[table] = true }
+ def prepare_data_sources
+ connection.data_sources.each { |source| @data_sources[source] = true }
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb b/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb
index ccb7e154ee..8489bcbf1d 100644
--- a/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb
+++ b/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
# :stopdoc:
module ConnectionAdapters
@@ -24,9 +26,9 @@ module ActiveRecord
protected
- def attributes_for_hash
- [self.class, sql_type, type, limit, precision, scale]
- end
+ def attributes_for_hash
+ [self.class, sql_type, type, limit, precision, scale]
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/explain_pretty_printer.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/explain_pretty_printer.rb
new file mode 100644
index 0000000000..832fdfe5c4
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/explain_pretty_printer.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module ConnectionAdapters
+ module SQLite3
+ class ExplainPrettyPrinter # :nodoc:
+ # Pretty prints the result of an EXPLAIN QUERY PLAN in a way that resembles
+ # the output of the SQLite shell:
+ #
+ # 0|0|0|SEARCH TABLE users USING INTEGER PRIMARY KEY (rowid=?) (~1 rows)
+ # 0|1|1|SCAN TABLE posts (~100000 rows)
+ #
+ def pp(result)
+ result.rows.map do |row|
+ row.join("|")
+ end.join("\n") + "\n"
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
new file mode 100644
index 0000000000..b2dcdb5373
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module ConnectionAdapters
+ module SQLite3
+ module Quoting # :nodoc:
+ def quote_string(s)
+ @connection.class.quote(s)
+ end
+
+ def quote_table_name_for_assignment(table, attr)
+ quote_column_name(attr)
+ end
+
+ def quote_column_name(name)
+ @quoted_column_names[name] ||= %Q("#{super.gsub('"', '""')}")
+ end
+
+ def quoted_time(value)
+ value = value.change(year: 2000, month: 1, day: 1)
+ quoted_date(value).sub(/\A\d\d\d\d-\d\d-\d\d /, "2000-01-01 ")
+ end
+
+ def quoted_binary(value)
+ "x'#{value.hex}'"
+ end
+
+ def quoted_true
+ ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? "1" : "'t'"
+ end
+
+ def unquoted_true
+ ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? 1 : "t"
+ end
+
+ def quoted_false
+ ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? "0" : "'f'"
+ end
+
+ def unquoted_false
+ ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? 0 : "f"
+ end
+
+ private
+
+ def _type_cast(value)
+ case value
+ when BigDecimal
+ value.to_f
+ when String
+ if value.encoding == Encoding::ASCII_8BIT
+ super(value.encode(Encoding::UTF_8))
+ else
+ super
+ end
+ else
+ super
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb
index fe1dcbd710..b842561317 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module SQLite3
- class SchemaCreation < AbstractAdapter::SchemaCreation
+ class SchemaCreation < AbstractAdapter::SchemaCreation # :nodoc:
private
def add_column_options!(sql, options)
if 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..c9855019c1
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module ConnectionAdapters
+ module SQLite3
+ class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
+ def references(*args, **options)
+ super(*args, type: :integer, **options)
+ end
+ alias :belongs_to :references
+
+ private
+ def integer_like_primary_key_type(type, options)
+ :primary_key
+ end
+ 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..621678ec65
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module ConnectionAdapters
+ module SQLite3
+ class SchemaDumper < ConnectionAdapters::SchemaDumper # :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..48277f0ae2
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module ConnectionAdapters
+ module SQLite3
+ module SchemaStatements # :nodoc:
+ # Returns an array of indexes for the given table.
+ def indexes(table_name)
+ exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", "SCHEMA").map do |row|
+ # Indexes SQLite creates implicitly for internal use start with "sqlite_".
+ # See https://www.sqlite.org/fileformat2.html#intschema
+ next if row["name"].starts_with?("sqlite_")
+
+ index_sql = query_value(<<-SQL, "SCHEMA")
+ SELECT sql
+ FROM sqlite_master
+ WHERE name = #{quote(row['name'])} AND type = 'index'
+ UNION ALL
+ SELECT sql
+ FROM sqlite_temp_master
+ WHERE name = #{quote(row['name'])} AND type = 'index'
+ SQL
+
+ /\bON\b\s*"?(\w+?)"?\s*\((?<expressions>.+?)\)(?:\s*WHERE\b\s*(?<where>.+))?\z/i =~ index_sql
+
+ columns = exec_query("PRAGMA index_info(#{quote(row['name'])})", "SCHEMA").map do |col|
+ col["name"]
+ end
+
+ orders = {}
+
+ if columns.any?(&:nil?) # index created with an expression
+ columns = expressions
+ else
+ # Add info on sort order for columns (only desc order is explicitly specified,
+ # asc is the default)
+ if index_sql # index_sql can be null in case of primary key indexes
+ index_sql.scan(/"(\w+)" DESC/).flatten.each { |order_column|
+ orders[order_column] = :desc
+ }
+ end
+ end
+
+ IndexDefinition.new(
+ table_name,
+ row["name"],
+ row["unique"] != 0,
+ columns,
+ where: where,
+ orders: orders
+ )
+ end.compact
+ end
+
+ def create_schema_dumper(options)
+ SQLite3::SchemaDumper.create(self, options)
+ end
+
+ private
+ def schema_creation
+ SQLite3::SchemaCreation.new(self)
+ end
+
+ def create_table_definition(*args)
+ SQLite3::TableDefinition.new(*args)
+ end
+
+ def new_column_from_field(table_name, field)
+ default = \
+ case field["dflt_value"]
+ when /^null$/i
+ nil
+ when /^'(.*)'$/m
+ $1.gsub("''", "'")
+ when /^"(.*)"$/m
+ $1.gsub('""', '"')
+ else
+ field["dflt_value"]
+ end
+
+ type_metadata = fetch_type_metadata(field["type"])
+ Column.new(field["name"], default, type_metadata, field["notnull"].to_i == 0, table_name, nil, field["collation"])
+ end
+
+ 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 7c809b088c..3312c3de01 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -1,14 +1,22 @@
-require 'active_record/connection_adapters/abstract_adapter'
-require 'active_record/connection_adapters/statement_pool'
-require 'active_record/connection_adapters/sqlite3/schema_creation'
+# frozen_string_literal: true
-gem 'sqlite3', '~> 1.3.6'
-require 'sqlite3'
+require "active_record/connection_adapters/abstract_adapter"
+require "active_record/connection_adapters/statement_pool"
+require "active_record/connection_adapters/sqlite3/explain_pretty_printer"
+require "active_record/connection_adapters/sqlite3/quoting"
+require "active_record/connection_adapters/sqlite3/schema_creation"
+require "active_record/connection_adapters/sqlite3/schema_definitions"
+require "active_record/connection_adapters/sqlite3/schema_dumper"
+require "active_record/connection_adapters/sqlite3/schema_statements"
+
+gem "sqlite3", "~> 1.3.6"
+require "sqlite3"
module ActiveRecord
module ConnectionHandling # :nodoc:
- # sqlite3 adapter reuses sqlite_connection.
def sqlite3_connection(config)
+ config = config.symbolize_keys
+
# Require database.
unless config[:database]
raise ArgumentError, "No database file specified. Missing argument: database"
@@ -17,7 +25,7 @@ module ActiveRecord
# Allow database path relative to Rails.root, but only if the database
# path is not the special path that tells sqlite to build a database only
# in memory.
- if ':memory:' != config[:database]
+ if ":memory:" != config[:database]
config[:database] = File.expand_path(config[:database], Rails.root) if defined?(Rails.root)
dirname = File.dirname(config[:database])
Dir.mkdir(dirname) unless File.directory?(dirname)
@@ -25,7 +33,7 @@ module ActiveRecord
db = SQLite3::Database.new(
config[:database].to_s,
- :results_as_hash => true
+ config.merge(results_as_hash: true)
)
db.busy_timeout(ConnectionAdapters::SQLite3Adapter.type_cast_config_to_integer(config[:timeout])) if config[:timeout]
@@ -33,7 +41,7 @@ module ActiveRecord
ConnectionAdapters::SQLite3Adapter.new(db, logger, nil, config)
rescue Errno::ENOENT => error
if error.message.include?("No such file or directory")
- raise ActiveRecord::NoDatabaseError.new(error.message, error)
+ raise ActiveRecord::NoDatabaseError
else
raise
end
@@ -48,11 +56,13 @@ module ActiveRecord
#
# * <tt>:database</tt> - Path to the database file.
class SQLite3Adapter < AbstractAdapter
- ADAPTER_NAME = 'SQLite'.freeze
- include Savepoints
+ ADAPTER_NAME = "SQLite"
+
+ include SQLite3::Quoting
+ include SQLite3::SchemaStatements
NATIVE_DATABASE_TYPES = {
- primary_key: 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL',
+ primary_key: "integer PRIMARY KEY AUTOINCREMENT NOT NULL",
string: { name: "varchar" },
text: { name: "text" },
integer: { name: "integer" },
@@ -62,49 +72,40 @@ module ActiveRecord
time: { name: "time" },
date: { name: "date" },
binary: { name: "blob" },
- boolean: { name: "boolean" }
+ boolean: { name: "boolean" },
+ json: { name: "json" },
}
- class Version
- include Comparable
-
- def initialize(version_string)
- @version = version_string.split('.').map(&:to_i)
- end
-
- def <=>(version_string)
- @version <=> version_string.split('.').map(&:to_i)
- end
- end
+ ##
+ # :singleton-method:
+ # Indicates whether boolean values are stored in sqlite3 databases as 1
+ # and 0 or 't' and 'f'. Leaving <tt>ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer</tt>
+ # set to false is deprecated. SQLite databases have used 't' and 'f' to
+ # serialize boolean values and must have old data converted to 1 and 0
+ # (its native boolean serialization) before setting this flag to true.
+ # Conversion can be accomplished by setting up a rake task which runs
+ #
+ # ExampleModel.where("boolean_column = 't'").update_all(boolean_column: 1)
+ # ExampleModel.where("boolean_column = 'f'").update_all(boolean_column: 0)
+ # for all models and all boolean columns, after which the flag must be set
+ # to true by adding the following to your <tt>application.rb</tt> file:
+ #
+ # Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true
+ class_attribute :represent_boolean_as_integer, default: false
- class StatementPool < ConnectionAdapters::StatementPool
+ class StatementPool < ConnectionAdapters::StatementPool # :nodoc:
private
-
- def dealloc(stmt)
- stmt[:stmt].close unless stmt[:stmt].closed?
- end
- end
-
- def schema_creation # :nodoc:
- SQLite3::SchemaCreation.new self
+ def dealloc(stmt)
+ stmt.close unless stmt.closed?
+ end
end
def initialize(connection, logger, connection_options, config)
- super(connection, logger)
-
- @active = nil
- @statements = StatementPool.new(@connection,
- self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }))
- @config = config
+ super(connection, logger, config)
- @visitor = Arel::Visitors::SQLite.new self
- @quoted_column_names = {}
-
- if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
- @prepared_statements = true
- else
- @prepared_statements = false
- end
+ @active = true
+ @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit]))
+ configure_connection
end
def supports_ddl_transactions?
@@ -116,34 +117,35 @@ module ActiveRecord
end
def supports_partial_index?
- sqlite_version >= '3.8.0'
+ true
end
- # Returns true, since this connection adapter supports prepared statement
- # caching.
- def supports_statement_cache?
+ def supports_expression_index?
+ sqlite_version >= "3.9.0"
+ end
+
+ def requires_reloading?
true
end
- # Returns true, since this connection adapter supports migrations.
- def supports_migrations? #:nodoc:
+ def supports_foreign_keys_in_create?
true
end
- def supports_primary_key? #:nodoc:
+ def supports_views?
true
end
- def requires_reloading?
+ def supports_datetime_with_precision?
true
end
- def supports_views?
+ def supports_json?
true
end
def active?
- @active != false
+ @active
end
# Disconnects from the database if already connected. Otherwise, this
@@ -164,7 +166,7 @@ module ActiveRecord
end
# Returns 62. SQLite supports index names up to 64
- # characters. The rest is used by rails internally to perform
+ # characters. The rest is used by Rails internally to perform
# temporary rename operations
def allowed_index_name_length
index_name_length - 2
@@ -183,42 +185,24 @@ module ActiveRecord
true
end
- # QUOTING ==================================================
-
- def _quote(value) # :nodoc:
- case value
- when Type::Binary::Data
- "x'#{value.hex}'"
- else
- super
- end
- end
-
- def _type_cast(value) # :nodoc:
- case value
- when BigDecimal
- value.to_f
- when String
- if value.encoding == Encoding::ASCII_8BIT
- super(value.encode(Encoding::UTF_8))
- else
- super
- end
- else
- super
- end
+ def supports_lazy_transactions?
+ true
end
- def quote_string(s) #:nodoc:
- @connection.class.quote(s)
- end
+ # REFERENTIAL INTEGRITY ====================================
- def quote_table_name_for_assignment(table, attr)
- quote_column_name(attr)
- end
+ def disable_referential_integrity # :nodoc:
+ old_foreign_keys = query_value("PRAGMA foreign_keys")
+ old_defer_foreign_keys = query_value("PRAGMA defer_foreign_keys")
- def quote_column_name(name) #:nodoc:
- @quoted_column_names[name] ||= %Q("#{name.to_s.gsub('"', '""')}")
+ begin
+ execute("PRAGMA defer_foreign_keys = ON")
+ execute("PRAGMA foreign_keys = OFF")
+ yield
+ ensure
+ execute("PRAGMA defer_foreign_keys = #{old_defer_foreign_keys}")
+ execute("PRAGMA foreign_keys = #{old_foreign_keys}")
+ end
end
#--
@@ -227,52 +211,42 @@ module ActiveRecord
def explain(arel, binds = [])
sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}"
- ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', []))
- end
-
- class ExplainPrettyPrinter
- # Pretty prints the result of a EXPLAIN QUERY PLAN in a way that resembles
- # the output of the SQLite shell:
- #
- # 0|0|0|SEARCH TABLE users USING INTEGER PRIMARY KEY (rowid=?) (~1 rows)
- # 0|1|1|SCAN TABLE posts (~100000 rows)
- #
- def pp(result) # :nodoc:
- result.rows.map do |row|
- row.join('|')
- end.join("\n") + "\n"
- end
- end
-
- def exec_query(sql, name = nil, binds = [])
- type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) }
-
- log(sql, name, binds) do
- # Don't cache statements if they are not prepared
- if without_prepared_statement?(binds)
- stmt = @connection.prepare(sql)
- begin
- cols = stmt.columns
+ SQLite3::ExplainPrettyPrinter.new.pp(exec_query(sql, "EXPLAIN", []))
+ end
+
+ def exec_query(sql, name = nil, binds = [], prepare: false)
+ materialize_transactions
+
+ type_casted_binds = type_casted_binds(binds)
+
+ log(sql, name, binds, type_casted_binds) do
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
+ # Don't cache statements if they are not prepared
+ unless prepare
+ stmt = @connection.prepare(sql)
+ begin
+ cols = stmt.columns
+ unless without_prepared_statement?(binds)
+ stmt.bind_params(type_casted_binds)
+ end
+ records = stmt.to_a
+ ensure
+ stmt.close
+ end
+ else
+ stmt = @statements[sql] ||= @connection.prepare(sql)
+ cols = stmt.columns
+ stmt.reset!
+ stmt.bind_params(type_casted_binds)
records = stmt.to_a
- ensure
- stmt.close
end
- stmt = records
- else
- cache = @statements[sql] ||= {
- :stmt => @connection.prepare(sql)
- }
- stmt = cache[:stmt]
- cols = cache[:cols] ||= stmt.columns
- stmt.reset!
- stmt.bind_params type_casted_binds
- end
- ActiveRecord::Result.new(cols, stmt.to_a)
+ ActiveRecord::Result.new(cols, records)
+ end
end
end
- def exec_delete(sql, name = 'SQL', binds = [])
+ def exec_delete(sql, name = "SQL", binds = [])
exec_query(sql, name, binds)
@connection.changes
end
@@ -283,111 +257,36 @@ module ActiveRecord
end
def execute(sql, name = nil) #:nodoc:
- log(sql, name) { @connection.execute(sql) }
- end
-
- def update_sql(sql, name = nil) #:nodoc:
- super
- @connection.changes
- end
+ materialize_transactions
- def delete_sql(sql, name = nil) #:nodoc:
- sql += " WHERE 1=1" unless sql =~ /WHERE/i
- super sql, name
- end
-
- def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
- super
- id_value || @connection.last_insert_row_id
- end
- alias :create :insert_sql
-
- def select_rows(sql, name = nil, binds = [])
- exec_query(sql, name, binds).rows
+ log(sql, name) do
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
+ @connection.execute(sql)
+ end
+ end
end
def begin_db_transaction #:nodoc:
- log('begin transaction',nil) { @connection.transaction }
+ log("begin transaction", nil) { @connection.transaction }
end
def commit_db_transaction #:nodoc:
- log('commit transaction',nil) { @connection.commit }
+ log("commit transaction", nil) { @connection.commit }
end
def exec_rollback_db_transaction #:nodoc:
- log('rollback transaction',nil) { @connection.rollback }
+ log("rollback transaction", nil) { @connection.rollback }
end
# SCHEMA STATEMENTS ========================================
- def tables(name = nil, table_name = nil) #:nodoc:
- sql = <<-SQL
- SELECT name
- FROM sqlite_master
- WHERE (type = 'table' OR type = 'view') AND NOT name = 'sqlite_sequence'
- SQL
- sql << " AND name = #{quote_table_name(table_name)}" if table_name
-
- exec_query(sql, 'SCHEMA').map do |row|
- row['name']
- end
- end
-
- def table_exists?(table_name)
- table_name && tables(nil, table_name).any?
- end
-
- # Returns an array of +Column+ objects for the table specified by +table_name+.
- def columns(table_name) #:nodoc:
- table_structure(table_name).map do |field|
- case field["dflt_value"]
- when /^null$/i
- field["dflt_value"] = nil
- when /^'(.*)'$/m
- field["dflt_value"] = $1.gsub("''", "'")
- when /^"(.*)"$/m
- field["dflt_value"] = $1.gsub('""', '"')
- end
-
- collation = field['collation']
- sql_type = field['type']
- type_metadata = fetch_type_metadata(sql_type)
- new_column(field['name'], field['dflt_value'], type_metadata, field['notnull'].to_i == 0, nil, collation)
- end
- end
-
- # Returns an array of indexes for the given table.
- def indexes(table_name, name = nil) #:nodoc:
- exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", 'SCHEMA').map do |row|
- sql = <<-SQL
- SELECT sql
- FROM sqlite_master
- WHERE name=#{quote(row['name'])} AND type='index'
- UNION ALL
- SELECT sql
- FROM sqlite_temp_master
- WHERE name=#{quote(row['name'])} AND type='index'
- SQL
- index_sql = exec_query(sql).first['sql']
- match = /\sWHERE\s+(.+)$/i.match(index_sql)
- where = match[1] if match
- IndexDefinition.new(
- table_name,
- row['name'],
- row['unique'] != 0,
- exec_query("PRAGMA index_info('#{row['name']}')", "SCHEMA").map { |col|
- col['name']
- }, nil, nil, where)
- end
- end
-
- def primary_key(table_name) #:nodoc:
- pks = table_structure(table_name).select { |f| f['pk'] > 0 }
- return nil unless pks.count == 1
- pks[0]['name']
+ def primary_keys(table_name) # :nodoc:
+ pks = table_structure(table_name).select { |f| f["pk"] > 0 }
+ pks.sort_by { |f| f["pk"] }.map { |f| f["name"] }
end
- def remove_index!(table_name, index_name) #:nodoc:
+ def remove_index(table_name, options = {}) #:nodoc:
+ index_name = index_name_for_remove(table_name, options)
exec_query "DROP INDEX #{quote_column_name(index_name)}"
end
@@ -400,19 +299,18 @@ module ActiveRecord
rename_table_indexes(table_name, new_name)
end
- # See: http://www.sqlite.org/lang_altertable.html
- # SQLite has an additional restriction on the ALTER TABLE statement
- def valid_alter_table_type?(type)
- type.to_sym != :primary_key
+ def valid_alter_table_type?(type, options = {})
+ !invalid_alter_table_type?(type, options)
end
+ deprecate :valid_alter_table_type?
def add_column(table_name, column_name, type, options = {}) #:nodoc:
- if valid_alter_table_type?(type)
- super(table_name, column_name, type, options)
- else
+ if invalid_alter_table_type?(type, options)
alter_table(table_name) do |definition|
definition.column(column_name, type, options)
end
+ else
+ super
end
end
@@ -441,14 +339,13 @@ 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)
+ self.scale = options[:scale] if options.include?(:scale)
self.collation = options[:collation] if options.include?(:collation)
end
end
@@ -456,52 +353,130 @@ module ActiveRecord
def rename_column(table_name, column_name, new_column_name) #:nodoc:
column = column_for(table_name, column_name)
- alter_table(table_name, rename: {column.name => new_column_name.to_s})
+ alter_table(table_name, rename: { column.name => new_column_name.to_s })
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 = exec_query("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 insert_fixtures(rows, table_name)
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ `insert_fixtures` is deprecated and will be removed in the next version of Rails.
+ Consider using `insert_fixtures_set` for performance improvement.
+ MSG
+ insert_fixtures_set(table_name => rows)
+ end
+
+ def insert_fixtures_set(fixture_set, tables_to_delete = [])
+ disable_referential_integrity do
+ transaction(requires_new: true) do
+ tables_to_delete.each { |table| delete "DELETE FROM #{quote_table_name(table)}", "Fixture Delete" }
+
+ fixture_set.each do |table_name, rows|
+ rows.each { |row| insert_fixture(row, table_name) }
+ end
+ end
+ end
+ end
+
+ private
+ # See https://www.sqlite.org/limits.html,
+ # the default value is 999 when not configured.
+ def bind_params_length
+ 999
+ end
+
+ def check_version
+ if sqlite_version < "3.8.0"
+ raise "Your version of SQLite (#{sqlite_version}) is too old. Active Record supports SQLite >= 3.8."
+ end
+ end
+
+ def initialize_type_map(m = type_map)
+ super
+ register_class_with_limit m, %r(int)i, SQLite3Integer
+ end
def table_structure(table_name)
- structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", 'SCHEMA')
+ 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
+
+ # See: https://www.sqlite.org/lang_altertable.html
+ # SQLite has an additional restriction on the ALTER TABLE statement
+ def invalid_alter_table_type?(type, options)
+ type.to_sym == :primary_key || options[:primary_key]
+ end
- def alter_table(table_name, options = {}) #:nodoc:
+ def alter_table(table_name, options = {})
altered_table_name = "a#{table_name}"
- caller = lambda {|definition| yield definition if block_given?}
+ foreign_keys = foreign_keys(table_name)
+
+ caller = lambda do |definition|
+ rename = options[:rename] || {}
+ foreign_keys.each do |fk|
+ if column = rename[fk.options[:column]]
+ fk.options[:column] = column
+ end
+ definition.foreign_key(fk.to_table, fk.options)
+ end
+
+ yield definition if block_given?
+ end
transaction do
- move_table(table_name, altered_table_name,
- options.merge(:temporary => true))
- move_table(altered_table_name, table_name, &caller)
+ disable_referential_integrity do
+ move_table(table_name, altered_table_name, options.merge(temporary: true))
+ move_table(altered_table_name, table_name, &caller)
+ end
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|
@definition = definition
- @definition.primary_key(from_primary_key) if from_primary_key.present?
+ if from_primary_key.is_a?(Array)
+ @definition.primary_keys from_primary_key
+ end
columns(from).each do |column|
column_name = options[:rename] ?
(options[:rename][column.name] ||
options[:rename][column.name.to_sym] ||
column.name) : column.name
- next if column_name == from_primary_key
@definition.column(column_name, column.type,
- :limit => column.limit, :default => column.default,
- :precision => column.precision, :scale => column.scale,
- :null => column.null, collation: column.collation)
+ limit: column.limit, default: column.default,
+ precision: column.precision, scale: column.scale,
+ null: column.null, collation: column.collation,
+ primary_key: column_name == from_primary_key
+ )
end
+
yield @definition if block_given?
end
copy_table_indexes(from, to, options[:rename] || {})
@@ -510,7 +485,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}"
@@ -519,35 +494,39 @@ module ActiveRecord
name = name[1..-1]
end
- to_column_names = columns(to).map(&:name)
- columns = index.columns.map {|c| rename[c] || c }.select do |column|
- to_column_names.include?(column)
+ columns = index.columns
+ if columns.is_a?(Array)
+ to_column_names = columns(to).map(&:name)
+ columns = columns.map { |c| rename[c] || c }.select do |column|
+ to_column_names.include?(column)
+ end
end
unless columns.empty?
# index name can't be the same
opts = { name: name.gsub(/(^|_)(#{from})_/, "\\1#{to}_"), internal: true }
opts[:unique] = true if index.unique
+ opts[:where] = index.where if index.where
add_index(to, columns, opts)
end
end
end
- def copy_table_contents(from, to, columns, rename = {}) #:nodoc:
- column_mappings = Hash[columns.map {|name| [name, name]}]
+ 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)
- columns = columns.find_all{|col| from_columns.include?(column_mappings[col])}
+ columns = columns.find_all { |col| from_columns.include?(column_mappings[col]) }
from_columns_to_copy = columns.map { |col| column_mappings[col] }
- quoted_columns = columns.map { |col| quote_column_name(col) } * ','
- quoted_from_columns = from_columns_to_copy.map { |col| quote_column_name(col) } * ','
+ quoted_columns = columns.map { |col| quote_column_name(col) } * ","
+ quoted_from_columns = from_columns_to_copy.map { |col| quote_column_name(col) } * ","
exec_query("INSERT INTO #{quote_table_name(to)} (#{quoted_columns})
SELECT #{quoted_from_columns} FROM #{quote_table_name(from)}")
end
def sqlite_version
- @sqlite_version ||= SQLite3Adapter::Version.new(select_value('select sqlite_version(*)'))
+ @sqlite_version ||= SQLite3Adapter::Version.new(query_value("SELECT sqlite_version(*)"))
end
def translate_exception(exception, message)
@@ -557,51 +536,76 @@ module ActiveRecord
# Older versions of SQLite return:
# column *column_name* is not unique
when /column(s)? .* (is|are) not unique/, /UNIQUE constraint failed: .*/
- RecordNotUnique.new(message, exception)
+ 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)
collation_hash = {}
- sql = "SELECT sql FROM
- (SELECT * FROM sqlite_master UNION ALL
- SELECT * FROM sqlite_temp_master)
- WHERE type='table' and name='#{ table_name }' \;"
+ sql = <<-SQL
+ SELECT sql FROM
+ (SELECT * FROM sqlite_master UNION ALL
+ SELECT * FROM sqlite_temp_master)
+ WHERE type = 'table' AND name = #{quote(table_name)}
+ SQL
# Result will have following sample string
# CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
# "password_digest" varchar COLLATE "NOCASE");
- result = exec_query(sql, 'SCHEMA').first
+ result = exec_query(sql, "SCHEMA").first
if result
- # Splitting with left parantheses and picking up last will return all
+ # Splitting with left parentheses and picking up last will return all
# columns separated with comma(,).
- columns_string = result["sql"].split('(').last
+ columns_string = result["sql"].split("(").last
- columns_string.split(',').each do |column_string|
+ columns_string.split(",").each do |column_string|
# This regex will match the column name and collation type and will save
# the value in $1 and $2 respectively.
- collation_hash[$1] = $2 if (COLLATE_REGEX =~ column_string)
+ collation_hash[$1] = $2 if COLLATE_REGEX =~ column_string
end
basic_structure.map! do |column|
- column_name = column['name']
+ column_name = column["name"]
if collation_hash.has_key? column_name
- column['collation'] = collation_hash[column_name]
+ column["collation"] = collation_hash[column_name]
end
column
end
else
- basic_structure.to_hash
+ basic_structure.to_a
end
end
+
+ def arel_visitor
+ Arel::Visitors::SQLite.new(self)
+ end
+
+ def configure_connection
+ execute("PRAGMA foreign_keys = ON", "SCHEMA")
+ end
+
+ class SQLite3Integer < Type::Integer # :nodoc:
+ private
+ def _limit
+ # INTEGER storage class can be stored 8 bytes value.
+ # See https://www.sqlite.org/datatype3.html#storage_classes_and_datatypes
+ limit || 8
+ end
+ end
+
+ ActiveRecord::Type.register(:integer, SQLite3Integer, adapter: :sqlite3)
end
+ ActiveSupport.run_load_hooks(:active_record_sqlite3adapter, SQLite3Adapter)
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/statement_pool.rb b/activerecord/lib/active_record/connection_adapters/statement_pool.rb
index 82e9ef3d3d..46bd831da7 100644
--- a/activerecord/lib/active_record/connection_adapters/statement_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/statement_pool.rb
@@ -1,12 +1,15 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
- class StatementPool
+ class StatementPool # :nodoc:
include Enumerable
- def initialize(connection, max = 1000)
- @cache = Hash.new { |h,pid| h[pid] = {} }
- @connection = connection
- @max = max
+ DEFAULT_STATEMENT_LIMIT = 1000
+
+ def initialize(statement_limit = nil)
+ @cache = Hash.new { |h, pid| h[pid] = {} }
+ @statement_limit = statement_limit || DEFAULT_STATEMENT_LIMIT
end
def each(&block)
@@ -26,7 +29,7 @@ module ActiveRecord
end
def []=(sql, stmt)
- while @max <= cache.size
+ while @statement_limit <= cache.size
dealloc(cache.shift.last)
end
cache[sql] = stmt
@@ -46,13 +49,13 @@ module ActiveRecord
private
- def cache
- @cache[Process.pid]
- end
+ def cache
+ @cache[Process.pid]
+ end
- def dealloc(stmt)
- raise NotImplementedError
- end
+ def dealloc(stmt)
+ raise NotImplementedError
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb
index d6b661ff76..3ce9aad5fc 100644
--- a/activerecord/lib/active_record/connection_handling.rb
+++ b/activerecord/lib/active_record/connection_handling.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionHandling
- RAILS_ENV = -> { (Rails.env if defined?(Rails.env)) || ENV["RAILS_ENV"] || ENV["RACK_ENV"] }
+ RAILS_ENV = -> { (Rails.env if defined?(Rails.env)) || ENV["RAILS_ENV"].presence || ENV["RACK_ENV"].presence }
DEFAULT_ENV = -> { RAILS_ENV.call || "default_env" }
# Establishes the connection to the database. Accepts a hash as input where
@@ -8,7 +10,7 @@ module ActiveRecord
# example for regular databases (MySQL, PostgreSQL, etc):
#
# ActiveRecord::Base.establish_connection(
- # adapter: "mysql",
+ # adapter: "mysql2",
# host: "localhost",
# username: "myuser",
# password: "mypass",
@@ -35,49 +37,120 @@ module ActiveRecord
# "postgres://myuser:mypass@localhost/somedatabase"
# )
#
- # In case <tt>ActiveRecord::Base.configurations</tt> is set (Rails
- # automatically loads the contents of config/database.yml into it),
+ # In case {ActiveRecord::Base.configurations}[rdoc-ref:Core.configurations]
+ # is set (Rails automatically loads the contents of config/database.yml into it),
# a symbol can also be given as argument, representing a key in the
# configuration hash:
#
# ActiveRecord::Base.establish_connection(:production)
#
- # The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError
+ # The exceptions AdapterNotSpecified, AdapterNotFound and +ArgumentError+
# may be returned on an error.
- def establish_connection(spec = nil)
- spec ||= DEFAULT_ENV.call.to_sym
- resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new configurations
- spec = resolver.spec(spec)
+ def establish_connection(config_or_env = nil)
+ config_hash = resolve_config_for_connection(config_or_env)
+ connection_handler.establish_connection(config_hash)
+ end
- unless respond_to?(spec.adapter_method)
- raise AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter"
+ # Connects a model to the databases specified. The +database+ keyword
+ # takes a hash consisting of a +role+ and a +database_key+.
+ #
+ # This will create a connection handler for switching between connections,
+ # look up the config hash using the +database_key+ and finally
+ # establishes a connection to that config.
+ #
+ # class AnimalsModel < ApplicationRecord
+ # self.abstract_class = true
+ #
+ # connects_to database: { writing: :primary, reading: :primary_replica }
+ # end
+ #
+ # Returns an array of established connections.
+ def connects_to(database: {})
+ connections = []
+
+ database.each do |role, database_key|
+ config_hash = resolve_config_for_connection(database_key)
+ handler = lookup_connection_handler(role.to_sym)
+
+ connections << handler.establish_connection(config_hash)
end
- remove_connection
- connection_handler.establish_connection self, spec
+ connections
end
- class MergeAndResolveDefaultUrlConfig # :nodoc:
- def initialize(raw_configurations)
- @raw_config = raw_configurations.dup
- @env = DEFAULT_ENV.call.to_s
- end
+ # Connects to a database or role (ex writing, reading, or another
+ # custom role) for the duration of the block.
+ #
+ # If a role is passed, Active Record will look up the connection
+ # based on the requested role:
+ #
+ # ActiveRecord::Base.connected_to(role: :writing) do
+ # Dog.create! # creates dog using dog connection
+ # end
+ #
+ # ActiveRecord::Base.connected_to(role: :reading) do
+ # Dog.create! # throws exception because we're on a replica
+ # end
+ #
+ # ActiveRecord::Base.connected_to(role: :unknown_ode) do
+ # # raises exception due to non-existent role
+ # end
+ #
+ # For cases where you may want to connect to a database outside of the model,
+ # you can use +connected_to+ with a +database+ argument. The +database+ argument
+ # expects a symbol that corresponds to the database key in your config.
+ #
+ # This will connect to a new database for the queries inside the block.
+ #
+ # ActiveRecord::Base.connected_to(database: :animals_slow_replica) do
+ # Dog.run_a_long_query # runs a long query while connected to the +animals_slow_replica+
+ # end
+ def connected_to(database: nil, role: nil, &blk)
+ if database && role
+ raise ArgumentError, "connected_to can only accept a `database` or a `role` argument, but not both arguments."
+ elsif database
+ if database.is_a?(Hash)
+ role, database = database.first
+ role = role.to_sym
+ else
+ role = database.to_sym
+ end
- # Returns fully resolved connection hashes.
- # Merges connection information from `ENV['DATABASE_URL']` if available.
- def resolve
- ConnectionAdapters::ConnectionSpecification::Resolver.new(config).resolve_all
- end
+ config_hash = resolve_config_for_connection(database)
+ handler = lookup_connection_handler(role)
- private
- def config
- @raw_config.dup.tap do |cfg|
- if url = ENV['DATABASE_URL']
- cfg[@env] ||= {}
- cfg[@env]["url"] ||= url
- end
- end
+ with_handler(role) do
+ handler.establish_connection(config_hash)
+ yield
end
+ elsif role
+ with_handler(role.to_sym, &blk)
+ else
+ raise ArgumentError, "must provide a `database` or a `role`."
+ end
+ end
+
+ def lookup_connection_handler(handler_key) # :nodoc:
+ connection_handlers[handler_key] ||= ActiveRecord::ConnectionAdapters::ConnectionHandler.new
+ end
+
+ def with_handler(handler_key, &blk) # :nodoc:
+ handler = lookup_connection_handler(handler_key)
+ swap_connection_handler(handler, &blk)
+ end
+
+ def resolve_config_for_connection(config_or_env) # :nodoc:
+ raise "Anonymous class is not allowed." unless name
+
+ config_or_env ||= DEFAULT_ENV.call.to_sym
+ pool_name = self == Base ? "primary" : name
+ self.connection_specification_name = pool_name
+
+ resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(Base.configurations)
+ config_hash = resolver.resolve(config_or_env, pool_name).symbolize_keys
+ config_hash[:name] = pool_name
+
+ config_hash
end
# Returns the connection currently associated with the class. This can
@@ -87,12 +160,14 @@ module ActiveRecord
retrieve_connection
end
- def connection_id
- ActiveRecord::RuntimeRegistry.connection_id ||= Thread.current.object_id
- end
+ attr_writer :connection_specification_name
- def connection_id=(connection_id)
- ActiveRecord::RuntimeRegistry.connection_id = connection_id
+ # Return the specification name from the current class or its parent.
+ def connection_specification_name
+ if !defined?(@connection_specification_name) || @connection_specification_name.nil?
+ return self == Base ? "primary" : superclass.connection_specification_name
+ end
+ @connection_specification_name
end
# Returns the configuration of the associated connection as a hash:
@@ -106,20 +181,28 @@ module ActiveRecord
end
def connection_pool
- connection_handler.retrieve_connection_pool(self) or raise ConnectionNotEstablished
+ connection_handler.retrieve_connection_pool(connection_specification_name) || raise(ConnectionNotEstablished)
end
def retrieve_connection
- connection_handler.retrieve_connection(self)
+ connection_handler.retrieve_connection(connection_specification_name)
end
# Returns +true+ if Active Record is connected.
def connected?
- connection_handler.connected?(self)
+ connection_handler.connected?(connection_specification_name)
end
- def remove_connection(klass = self)
- connection_handler.remove_connection(klass)
+ def remove_connection(name = nil)
+ name ||= @connection_specification_name if defined?(@connection_specification_name)
+ # if removing a connection that has a pool, we reset the
+ # connection_specification_name so it will use the parent
+ # pool.
+ if connection_handler.retrieve_connection_pool(name)
+ self.connection_specification_name = nil
+ end
+
+ connection_handler.remove_connection(name)
end
def clear_cache! # :nodoc:
@@ -127,6 +210,15 @@ module ActiveRecord
end
delegate :clear_active_connections!, :clear_reloadable_connections!,
- :clear_all_connections!, :to => :connection_handler
+ :clear_all_connections!, :flush_idle_connections!, to: :connection_handler
+
+ private
+
+ def swap_connection_handler(handler, &blk) # :nodoc:
+ old_handler, ActiveRecord::Base.connection_handler = ActiveRecord::Base.connection_handler, handler
+ yield
+ ensure
+ ActiveRecord::Base.connection_handler = old_handler
+ end
end
end
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index 8a014e682e..50f3087c51 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -1,7 +1,9 @@
-require 'thread'
-require 'active_support/core_ext/hash/indifferent_access'
-require 'active_support/core_ext/object/duplicable'
-require 'active_support/core_ext/string/filters'
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/indifferent_access"
+require "active_support/core_ext/string/filters"
+require "active_support/parameter_filter"
+require "concurrent/map"
module ActiveRecord
module Core
@@ -17,8 +19,15 @@ module ActiveRecord
mattr_accessor :logger, instance_writer: false
##
+ # :singleton-method:
+ #
+ # Specifies if the methods calling database queries should be logged below
+ # their relevant queries. Defaults to false.
+ mattr_accessor :verbose_query_logs, instance_writer: false, default: false
+
+ ##
# Contains the database configuration - as is typically stored in config/database.yml -
- # as a Hash.
+ # as an ActiveRecord::DatabaseConfigurations object.
#
# For example, the following database.yml...
#
@@ -32,22 +41,18 @@ module ActiveRecord
#
# ...would result in ActiveRecord::Base.configurations to look like this:
#
- # {
- # 'development' => {
- # 'adapter' => 'sqlite3',
- # 'database' => 'db/development.sqlite3'
- # },
- # 'production' => {
- # 'adapter' => 'sqlite3',
- # 'database' => 'db/production.sqlite3'
- # }
- # }
+ # #<ActiveRecord::DatabaseConfigurations:0x00007fd1acbdf800 @configurations=[
+ # #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbded10 @env_name="development",
+ # @spec_name="primary", @config={"adapter"=>"sqlite3", "database"=>"db/development.sqlite3"}>,
+ # #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbdea90 @env_name="production",
+ # @spec_name="primary", @config={"adapter"=>"mysql2", "database"=>"db/production.sqlite3"}>
+ # ]>
def self.configurations=(config)
- @@configurations = ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig.new(config).resolve
+ @@configurations = ActiveRecord::DatabaseConfigurations.new(config)
end
self.configurations = {}
- # Returns fully resolved configurations hash
+ # Returns fully resolved ActiveRecord::DatabaseConfigurations object
def self.configurations
@@configurations
end
@@ -56,8 +61,7 @@ module ActiveRecord
# :singleton-method:
# Determines whether to use Time.utc (using :utc) or Time.local (using :local) when pulling
# dates and times from the database. This is set to :utc by default.
- mattr_accessor :default_timezone, instance_writer: false
- self.default_timezone = :utc
+ mattr_accessor :default_timezone, instance_writer: false, default: :utc
##
# :singleton-method:
@@ -67,23 +71,35 @@ module ActiveRecord
# ActiveRecord::Schema file which can be loaded into any database that
# supports migrations. Use :ruby if you want to have different database
# adapters for, e.g., your development and test environments.
- mattr_accessor :schema_format, instance_writer: false
- self.schema_format = :ruby
+ mattr_accessor :schema_format, instance_writer: false, default: :ruby
+
+ ##
+ # :singleton-method:
+ # Specifies if an error should be raised if the query has an order being
+ # ignored when doing batch queries. Useful in applications where the
+ # scope being ignored is error-worthy, rather than a warning.
+ mattr_accessor :error_on_ignored_order, instance_writer: false, default: false
+
+ # :singleton-method:
+ # Specify the behavior for unsafe raw query methods. Values are as follows
+ # deprecated - Warnings are logged when unsafe raw SQL is passed to
+ # query methods.
+ # disabled - Unsafe raw SQL passed to query methods results in
+ # UnknownAttributeReference exception.
+ mattr_accessor :allow_unsafe_raw_sql, instance_writer: false, default: :deprecated
##
# :singleton-method:
# Specify whether or not to use timestamps for migration versions
- mattr_accessor :timestamped_migrations, instance_writer: false
- self.timestamped_migrations = true
+ mattr_accessor :timestamped_migrations, instance_writer: false, default: true
##
# :singleton-method:
# Specify whether schema dump should happen at the end of the
- # db:migrate rake task. This is true by default, which is useful for the
+ # db:migrate rails command. This is true by default, which is useful for the
# development environment. This should ideally be false in the production
# environment where dumping schema is rarely needed.
- mattr_accessor :dump_schema_after_migration, instance_writer: false
- self.dump_schema_after_migration = true
+ mattr_accessor :dump_schema_after_migration, instance_writer: false, default: true
##
# :singleton-method:
@@ -92,8 +108,7 @@ module ActiveRecord
# schema_search_path are dumped. Use :all to dump all schemas regardless
# of schema_search_path, or a string of comma separated schemas for a
# custom list.
- mattr_accessor :dump_schemas, instance_writer: false
- self.dump_schemas = :schema_search_path
+ mattr_accessor :dump_schemas, instance_writer: false, default: :schema_search_path
##
# :singleton-method:
@@ -102,14 +117,17 @@ module ActiveRecord
# be used to identify queries which load thousands of records and
# potentially cause memory bloat.
mattr_accessor :warn_on_records_fetched_greater_than, instance_writer: false
- self.warn_on_records_fetched_greater_than = nil
mattr_accessor :maintain_test_schema, instance_accessor: false
mattr_accessor :belongs_to_required_by_default, instance_accessor: false
+ mattr_accessor :connection_handlers, instance_accessor: false, default: {}
+
class_attribute :default_connection_handler, instance_writer: false
+ self.filter_attributes = []
+
def self.connection_handler
ActiveRecord::RuntimeRegistry.connection_handler || default_connection_handler
end
@@ -119,16 +137,12 @@ module ActiveRecord
end
self.default_connection_handler = ConnectionAdapters::ConnectionHandler.new
+ self.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler }
end
module ClassMethods
- def allocate
- define_attribute_methods
- super
- end
-
def initialize_find_by_cache # :nodoc:
- @find_by_statement_cache = {}.extend(Mutex_m)
+ @find_by_statement_cache = { true => Concurrent::Map.new, false => Concurrent::Map.new }
end
def inherited(child_class) # :nodoc:
@@ -143,39 +157,36 @@ module ActiveRecord
return super if block_given? ||
primary_key.nil? ||
scope_attributes? ||
- columns_hash.include?(inheritance_column) ||
- ids.first.kind_of?(Array)
-
- 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
+ columns_hash.include?(inheritance_column)
+
+ id = ids.first
+
+ return super if StatementCache.unsupported_value?(id)
key = primary_key
statement = cached_find_by_statement(key) { |params|
where(key => params.bind).limit(1)
}
- record = statement.execute([id], self, connection).first
+
+ record = statement.execute([id], connection).first
unless record
- raise RecordNotFound, "Couldn't find #{name} with '#{primary_key}'=#{id}"
+ raise RecordNotFound.new("Couldn't find #{name} with '#{primary_key}'=#{id}",
+ name, primary_key, id)
end
record
- rescue RangeError
- raise RecordNotFound, "Couldn't find #{name} with an out of range value for '#{primary_key}'"
+ 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
+ return super if !(Hash === hash) || hash.values.any? { |v|
+ StatementCache.unsupported_value?(v)
}
# We can't cache Post.find_by(author: david) ...yet
@@ -190,32 +201,46 @@ module ActiveRecord
where(wheres).limit(1)
}
begin
- statement.execute(hash.values, self, connection).first
- rescue TypeError => e
- raise ActiveRecord::StatementInvalid.new(e.message, e)
- rescue RangeError
+ statement.execute(hash.values, connection).first
+ rescue TypeError
+ raise ActiveRecord::StatementInvalid
+ rescue ::RangeError
nil
end
end
def find_by!(*args) # :nodoc:
- find_by(*args) or raise RecordNotFound.new("Couldn't find #{name}")
+ find_by(*args) || raise(RecordNotFound.new("Couldn't find #{name}", name))
end
def initialize_generated_modules # :nodoc:
generated_association_methods
end
- def generated_association_methods
+ def generated_association_methods # :nodoc:
@generated_association_methods ||= begin
mod = const_set(:GeneratedAssociationMethods, Module.new)
+ private_constant :GeneratedAssociationMethods
include mod
+
mod
end
end
+ # Returns columns which shouldn't be exposed while calling +#inspect+.
+ def filter_attributes
+ if defined?(@filter_attributes)
+ @filter_attributes
+ else
+ superclass.filter_attributes
+ end
+ end
+
+ # Specifies columns which shouldn't be exposed while calling +#inspect+.
+ attr_writer :filter_attributes
+
# Returns a string like 'Post(id:integer, title:string, body:text)'
- def inspect
+ def inspect # :nodoc:
if self == Base
super
elsif abstract_class?
@@ -223,35 +248,30 @@ module ActiveRecord
elsif !connected?
"#{super} (call '#{super}.connection' to establish a connection)"
elsif table_exists?
- attr_list = attribute_types.map { |name, type| "#{name}: #{type.type}" } * ', '
+ attr_list = attribute_types.map { |name, type| "#{name}: #{type.type}" } * ", "
"#{super}(#{attr_list})"
else
"#{super}(Table doesn't exist)"
end
end
- # Overwrite the default class equality method to provide support for association proxies.
- def ===(object)
+ # Overwrite the default class equality method to provide support for decorated models.
+ def ===(object) # :nodoc:
object.is_a?(self)
end
# Returns an instance of <tt>Arel::Table</tt> loaded with the current table name.
#
# class Post < ActiveRecord::Base
- # scope :published_and_commented, -> { published.and(self.arel_table[:comments_count].gt(0)) }
+ # scope :published_and_commented, -> { published.and(arel_table[:comments_count].gt(0)) }
# end
def arel_table # :nodoc:
@arel_table ||= Arel::Table.new(table_name, type_caster: type_caster)
end
- # Returns the Arel engine.
- def arel_engine # :nodoc:
- @arel_engine ||=
- if Base == self || connection_handler.retrieve_connection_pool(self)
- self
- else
- superclass.arel_engine
- end
+ def arel_attribute(name, table = arel_table) # :nodoc:
+ name = attribute_alias(name) if attribute_alias?(name)
+ table[name]
end
def predicate_builder # :nodoc:
@@ -264,25 +284,25 @@ module ActiveRecord
private
- def cached_find_by_statement(key, &block) # :nodoc:
- @find_by_statement_cache[key] || @find_by_statement_cache.synchronize {
- @find_by_statement_cache[key] ||= StatementCache.create(connection, &block)
- }
- end
+ def cached_find_by_statement(key, &block)
+ cache = @find_by_statement_cache[connection.prepared_statements]
+ cache.compute_if_absent(key) { StatementCache.create(connection, &block) }
+ end
- def relation # :nodoc:
- relation = Relation.create(self, arel_table, predicate_builder)
+ def relation
+ relation = Relation.create(self)
- if finder_needs_type_condition?
- relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name)
- else
- relation
+ if finder_needs_type_condition? && !ignore_default_scope?
+ relation.where!(type_condition)
+ relation.create_with!(inheritance_column.to_s => sti_name)
+ else
+ relation
+ end
end
- end
- def table_metadata # :nodoc:
- TableMetadata.new(self, arel_table)
- end
+ def table_metadata
+ TableMetadata.new(self, arel_table)
+ end
end
# New objects can be instantiated as either empty (pass no construction parameter) or pre-set with
@@ -294,8 +314,8 @@ module ActiveRecord
# # Instantiates a single new object
# User.new(first_name: 'Jamie')
def initialize(attributes = nil)
- @attributes = self.class._default_attributes.dup
self.class.define_attribute_methods
+ @attributes = self.class._default_attributes.deep_dup
init_internals
initialize_internals_callback
@@ -303,12 +323,12 @@ module ActiveRecord
assign_attributes(attributes) if attributes
yield self if block_given?
- run_callbacks :initialize
+ _run_initialize_callbacks
end
# Initialize an empty model object from +coder+. +coder+ should be
# the result of previously encoding an Active Record model, using
- # `encode_with`
+ # #encode_with.
#
# class Post < ActiveRecord::Base
# end
@@ -320,18 +340,30 @@ module ActiveRecord
# post = Post.allocate
# post.init_with(coder)
# post.title # => 'hello world'
- def init_with(coder)
+ def init_with(coder, &block)
coder = LegacyYamlAdapter.convert(self.class, coder)
- @attributes = coder['attributes']
+ attributes = self.class.yaml_encoder.decode(coder)
+ init_with_attributes(attributes, coder["new_record"], &block)
+ end
+ ##
+ # Initialize an empty model object from +attributes+.
+ # +attributes+ should be an attributes object, and unlike the
+ # `initialize` method, no assignment calls are made per attribute.
+ #
+ # :nodoc:
+ def init_with_attributes(attributes, new_record = false)
init_internals
- @new_record = coder['new_record']
+ @new_record = new_record
+ @attributes = attributes
self.class.define_attribute_methods
- run_callbacks :find
- run_callbacks :initialize
+ yield self if block_given?
+
+ _run_find_callbacks
+ _run_initialize_callbacks
self
end
@@ -364,20 +396,22 @@ module ActiveRecord
##
def initialize_dup(other) # :nodoc:
- @attributes = @attributes.dup
+ @attributes = @attributes.deep_dup
@attributes.reset(self.class.primary_key)
- run_callbacks(:initialize)
+ _run_initialize_callbacks
- @new_record = true
- @destroyed = false
+ @new_record = true
+ @destroyed = false
+ @_start_transaction_state = {}
+ @transaction_state = nil
super
end
# Populate +coder+ with attributes about this record that should be
# serialized. The structure of +coder+ defined in this method is
- # guaranteed to match the structure of +coder+ passed to the +init_with+
+ # guaranteed to match the structure of +coder+ passed to the #init_with
# method.
#
# Example:
@@ -388,11 +422,9 @@ module ActiveRecord
# Post.new.encode_with(coder)
# coder # => {"attributes" => {"id" => nil, ... }}
def encode_with(coder)
- # FIXME: Remove this when we better serialize attributes
- coder['raw_attributes'] = attributes_before_type_cast
- coder['attributes'] = @attributes
- coder['new_record'] = new_record?
- coder['active_record_yaml_version'] = 1
+ self.class.yaml_encoder.encode(@attributes, coder)
+ coder["new_record"] = new_record?
+ coder["active_record_yaml_version"] = 2
end
# Returns true if +comparison_object+ is the same exact object, or +comparison_object+
@@ -416,7 +448,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
- id.hash
+ self.class.hash ^ id.hash
else
super
end
@@ -438,7 +470,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
@@ -464,82 +496,100 @@ module ActiveRecord
# We check defined?(@attributes) not to issue warnings if the object is
# allocated but not initialized.
inspection = if defined?(@attributes) && @attributes
- self.class.column_names.collect { |name|
- if has_attribute?(name)
- "#{name}: #{attribute_for_inspect(name)}"
- end
- }.compact.join(", ")
- else
- "not initialized"
- end
+ self.class.attribute_names.collect do |name|
+ if has_attribute?(name)
+ attr = _read_attribute(name)
+ value = if attr.nil?
+ attr.inspect
+ else
+ attr = format_for_inspect(attr)
+ inspection_filter.filter_param(name, attr)
+ end
+ "#{name}: #{value}"
+ end
+ end.compact.join(", ")
+ else
+ "not initialized"
+ end
+
"#<#{self.class} #{inspection}>"
end
- # Takes a PP and prettily prints this record to it, allowing you to get a nice result from `pp record`
+ # Takes a PP and prettily prints this record to it, allowing you to get a nice result from <tt>pp record</tt>
# when pp is required.
def pretty_print(pp)
return super if custom_inspect_method_defined?
pp.object_address_group(self) do
if defined?(@attributes) && @attributes
- column_names = self.class.column_names.select { |name| has_attribute?(name) || new_record? }
- pp.seplist(column_names, proc { pp.text ',' }) do |column_name|
- column_value = read_attribute(column_name)
- pp.breakable ' '
+ attr_names = self.class.attribute_names.select { |name| has_attribute?(name) }
+ pp.seplist(attr_names, proc { pp.text "," }) do |attr_name|
+ pp.breakable " "
pp.group(1) do
- pp.text column_name
- pp.text ':'
+ pp.text attr_name
+ pp.text ":"
pp.breakable
- pp.pp column_value
+ value = _read_attribute(attr_name)
+ value = inspection_filter.filter_param(attr_name, value) unless value.nil?
+ pp.pp value
end
end
else
- pp.breakable ' '
- pp.text 'not initialized'
+ pp.breakable " "
+ pp.text "not initialized"
end
end
end
# Returns a hash of the given methods with their names as keys and returned values as values.
def slice(*methods)
- Hash[methods.map! { |method| [method, public_send(method)] }].with_indifferent_access
+ Hash[methods.flatten.map! { |method| [method, public_send(method)] }].with_indifferent_access
end
private
- # Under Ruby 1.9, Array#flatten will call #to_ary (recursively) on each of the elements
- # of the array, and then rescues from the possible NoMethodError. If those elements are
- # ActiveRecord::Base's, then this triggers the various method_missing's that we have,
- # which significantly impacts upon performance.
- #
- # So we can avoid the method_missing hit by explicitly defining #to_ary as nil here.
- #
- # See also http://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary.html
- def to_ary # :nodoc:
- nil
- end
+ # +Array#flatten+ will call +#to_ary+ (recursively) on each of the elements of
+ # the array, and then rescues from the possible +NoMethodError+. If those elements are
+ # +ActiveRecord::Base+'s, then this triggers the various +method_missing+'s that we have,
+ # which significantly impacts upon performance.
+ #
+ # So we can avoid the +method_missing+ hit by explicitly defining +#to_ary+ as +nil+ here.
+ #
+ # See also https://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary.html
+ def to_ary
+ nil
+ end
- def init_internals
- @readonly = false
- @destroyed = false
- @marked_for_destruction = false
- @destroyed_by_association = nil
- @new_record = true
- @txn = nil
- @_start_transaction_state = {}
- @transaction_state = nil
- end
+ def init_internals
+ @readonly = false
+ @destroyed = false
+ @marked_for_destruction = false
+ @destroyed_by_association = nil
+ @new_record = true
+ @_start_transaction_state = {}
+ @transaction_state = nil
+ end
- def initialize_internals_callback
- end
+ def initialize_internals_callback
+ end
- def thaw
- if frozen?
- @attributes = @attributes.dup
+ def thaw
+ if frozen?
+ @attributes = @attributes.dup
+ end
end
- end
- def custom_inspect_method_defined?
- self.class.instance_method(:inspect).owner != ActiveRecord::Base.instance_method(:inspect).owner
- end
+ def custom_inspect_method_defined?
+ self.class.instance_method(:inspect).owner != ActiveRecord::Base.instance_method(:inspect).owner
+ end
+
+ def inspection_filter
+ @inspection_filter ||= begin
+ mask = DelegateClass(::String).new(ActiveSupport::ParameterFilter::FILTERED)
+ def mask.pretty_print(pp)
+ pp.text __getobj__
+ end
+ ActiveSupport::ParameterFilter.new(self.class.filter_attributes, mask: mask)
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb
index 82596b63df..27c1b7a311 100644
--- a/activerecord/lib/active_record/counter_cache.rb
+++ b/activerecord/lib/active_record/counter_cache.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
# = Active Record Counter Cache
module CounterCache
@@ -12,13 +14,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
@@ -26,7 +36,7 @@ module ActiveRecord
has_many_association = has_many.find { |association| association.counter_cache_column && association.counter_cache_column.to_sym == counter_association.to_sym }
counter_association = has_many_association.plural_name if has_many_association
end
- raise ArgumentError, "'#{self.name}' has no association called '#{counter_association}'" unless has_many_association
+ raise ArgumentError, "'#{name}' has no association called '#{counter_association}'" unless has_many_association
if has_many_association.is_a? ActiveRecord::Reflection::ThroughReflection
has_many_association = has_many_association.through_reflection
@@ -37,24 +47,33 @@ 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 => object.send(counter_association).count(:all) }
+
+ if touch
+ names = touch if touch != true
+ updates.merge!(touch_attributes_with_time(*names))
+ end
+
+ unscoped.where(primary_key => object.id).update_all(updates)
end
- return true
+
+ true
end
# A generic "counter updater" implementation, intended primarily to be
- # used by increment_counter and decrement_counter, but which may also
+ # used by #increment_counter and #decrement_counter, but which may also
# be useful on its own. It simply does a direct SQL update for the record
# with the given ID, altering the given hash of counters by the amount
# given by the corresponding value:
#
# ==== Parameters
#
- # * +id+ - The id of the object you wish to update a counter on or an Array of ids.
+ # * +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.
+ # If attribute names are passed, they are updated along with updated_at/on
+ # attributes.
#
# ==== Examples
#
@@ -73,65 +92,78 @@ 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)
- 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
-
- unscoped.where(primary_key => id).update_all updates.join(', ')
+ unscoped.where!(primary_key => id).update_counters(counters)
end
# Increment a numeric field by one, via a direct SQL update.
#
# This method is used primarily for maintaining counter_cache columns that are
- # used to store aggregate values. For example, a DiscussionBoard may cache
+ # used to store aggregate values. For example, a +DiscussionBoard+ may cache
# posts_count and comments_count to avoid running an SQL query to calculate the
# number of posts and comments there are, each time it is displayed.
#
# ==== Parameters
#
# * +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.
+ # * +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 post_count column for the record with an id of 5
- # DiscussionBoard.increment_counter(:post_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
+ # DiscussionBoard.increment_counter(:posts_count, 5)
+ #
+ # # 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.
#
- # This works the same as increment_counter but reduces the column value by
+ # This works the same as #increment_counter but reduces the column value by
# 1 instead of increasing it.
#
# ==== Parameters
#
# * +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.
+ # * +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 post_count column for the record with an id of 5
- # DiscussionBoard.decrement_counter(:post_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
+ # DiscussionBoard.decrement_counter(:posts_count, 5)
+ #
+ # # 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
end
private
-
- def _create_record(*)
+ def _create_record(attribute_names = self.attribute_names)
id = super
each_counter_cached_associations do |association|
- if send(association.reflection.name)
- association.increment_counters
- @_after_create_counter_called = true
- end
+ association.increment_counters
end
id
@@ -144,9 +176,7 @@ module ActiveRecord
each_counter_cached_associations do |association|
foreign_key = association.reflection.foreign_key.to_sym
unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == foreign_key
- if send(association.reflection.name)
- association.decrement_counters
- end
+ association.decrement_counters
end
end
end
@@ -159,6 +189,5 @@ module ActiveRecord
yield association(name.to_sym) if reflection.belongs_to? && reflection.counter_cache_column
end
end
-
end
end
diff --git a/activerecord/lib/active_record/database_configurations.rb b/activerecord/lib/active_record/database_configurations.rb
new file mode 100644
index 0000000000..30cb0a27e7
--- /dev/null
+++ b/activerecord/lib/active_record/database_configurations.rb
@@ -0,0 +1,186 @@
+# frozen_string_literal: true
+
+require "active_record/database_configurations/database_config"
+require "active_record/database_configurations/hash_config"
+require "active_record/database_configurations/url_config"
+
+module ActiveRecord
+ # ActiveRecord::DatabaseConfigurations returns an array of DatabaseConfig
+ # objects (either a HashConfig or UrlConfig) that are constructed from the
+ # application's database configuration hash or url string.
+ class DatabaseConfigurations
+ attr_reader :configurations
+ delegate :any?, to: :configurations
+
+ def initialize(configurations = {})
+ @configurations = build_configs(configurations)
+ end
+
+ # Collects the configs for the environment and optionally the specification
+ # name passed in. To include replica configurations pass `include_replicas: true`.
+ #
+ # If a spec name is provided a single DatabaseConfig object will be
+ # returned, otherwise an array of DatabaseConfig objects will be
+ # returned that corresponds with the environment and type requested.
+ #
+ # Options:
+ #
+ # <tt>env_name:</tt> The environment name. Defaults to nil which will collect
+ # configs for all environments.
+ # <tt>spec_name:</tt> The specification name (ie primary, animals, etc.). Defaults
+ # to +nil+.
+ # <tt>include_replicas:</tt> Determines whether to include replicas in
+ # the returned list. Most of the time we're only iterating over the write
+ # connection (i.e. migrations don't need to run for the write and read connection).
+ # Defaults to +false+.
+ def configs_for(env_name: nil, spec_name: nil, include_replicas: false)
+ configs = env_with_configs(env_name)
+
+ unless include_replicas
+ configs = configs.select do |db_config|
+ !db_config.replica?
+ end
+ end
+
+ if spec_name
+ configs.find do |db_config|
+ db_config.spec_name == spec_name
+ end
+ else
+ configs
+ end
+ end
+
+ # Returns the config hash that corresponds with the environment
+ #
+ # If the application has multiple databases `default_hash` will
+ # return the first config hash for the environment.
+ #
+ # { database: "my_db", adapter: "mysql2" }
+ def default_hash(env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call.to_s)
+ default = find_db_config(env)
+ default.config if default
+ end
+ alias :[] :default_hash
+
+ # Returns a single DatabaseConfig object based on the requested environment.
+ #
+ # If the application has multiple databases `find_db_config` will return
+ # the first DatabaseConfig for the environment.
+ def find_db_config(env)
+ configurations.find do |db_config|
+ db_config.env_name == env.to_s ||
+ (db_config.for_current_env? && db_config.spec_name == env.to_s)
+ end
+ end
+
+ # Returns the DatabaseConfigurations object as a Hash.
+ def to_h
+ configs = configurations.reverse.inject({}) do |memo, db_config|
+ memo.merge(db_config.to_legacy_hash)
+ end
+
+ Hash[configs.to_a.reverse]
+ end
+
+ # Checks if the application's configurations are empty.
+ #
+ # Aliased to blank?
+ def empty?
+ configurations.empty?
+ end
+ alias :blank? :empty?
+
+ private
+ def env_with_configs(env = nil)
+ if env
+ configurations.select { |db_config| db_config.env_name == env }
+ else
+ configurations
+ end
+ end
+
+ def build_configs(configs)
+ return configs.configurations if configs.is_a?(DatabaseConfigurations)
+
+ build_db_config = configs.each_pair.flat_map do |env_name, config|
+ walk_configs(env_name.to_s, "primary", config)
+ end.compact
+
+ if url = ENV["DATABASE_URL"]
+ build_url_config(url, build_db_config)
+ else
+ build_db_config
+ end
+ end
+
+ def walk_configs(env_name, spec_name, config)
+ case config
+ when String
+ build_db_config_from_string(env_name, spec_name, config)
+ when Hash
+ build_db_config_from_hash(env_name, spec_name, config.stringify_keys)
+ end
+ end
+
+ def build_db_config_from_string(env_name, spec_name, config)
+ begin
+ url = config
+ uri = URI.parse(url)
+ if uri.try(:scheme)
+ ActiveRecord::DatabaseConfigurations::UrlConfig.new(env_name, spec_name, url)
+ end
+ rescue URI::InvalidURIError
+ ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, spec_name, config)
+ end
+ end
+
+ def build_db_config_from_hash(env_name, spec_name, config)
+ if url = config["url"]
+ config_without_url = config.dup
+ config_without_url.delete "url"
+ ActiveRecord::DatabaseConfigurations::UrlConfig.new(env_name, spec_name, url, config_without_url)
+ elsif config["database"] || (config.size == 1 && config.values.all? { |v| v.is_a? String })
+ ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, spec_name, config)
+ else
+ config.each_pair.map do |sub_spec_name, sub_config|
+ walk_configs(env_name, sub_spec_name, sub_config)
+ end
+ end
+ end
+
+ def build_url_config(url, configs)
+ env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call.to_s
+
+ if original_config = configs.find(&:for_current_env?)
+ if original_config.url_config?
+ configs
+ else
+ configs.map do |config|
+ ActiveRecord::DatabaseConfigurations::UrlConfig.new(env, config.spec_name, url, config.config)
+ end
+ end
+ else
+ configs + [ActiveRecord::DatabaseConfigurations::UrlConfig.new(env, "primary", url)]
+ end
+ end
+
+ def method_missing(method, *args, &blk)
+ if Hash.method_defined?(method)
+ ActiveSupport::Deprecation.warn \
+ "Returning a hash from ActiveRecord::Base.configurations is deprecated. Therefore calling `#{method}` on the hash is also deprecated. Please switch to using the `configs_for` method instead to collect and iterate over database configurations."
+ end
+
+ case method
+ when :each, :first
+ configurations.send(method, *args, &blk)
+ when :fetch
+ configs_for(env_name: args.first)
+ when :values
+ configurations.map(&:config)
+ else
+ super
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/database_configurations/database_config.rb b/activerecord/lib/active_record/database_configurations/database_config.rb
new file mode 100644
index 0000000000..adc37cc439
--- /dev/null
+++ b/activerecord/lib/active_record/database_configurations/database_config.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ class DatabaseConfigurations
+ # ActiveRecord::Base.configurations will return either a HashConfig or
+ # UrlConfig respectively. It will never return a DatabaseConfig object,
+ # as this is the parent class for the types of database configuration objects.
+ class DatabaseConfig # :nodoc:
+ attr_reader :env_name, :spec_name
+
+ def initialize(env_name, spec_name)
+ @env_name = env_name
+ @spec_name = spec_name
+ end
+
+ def replica?
+ raise NotImplementedError
+ end
+
+ def migrations_paths
+ raise NotImplementedError
+ end
+
+ def url_config?
+ false
+ end
+
+ def to_legacy_hash
+ { env_name => config }
+ end
+
+ def for_current_env?
+ env_name == ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/database_configurations/hash_config.rb b/activerecord/lib/active_record/database_configurations/hash_config.rb
new file mode 100644
index 0000000000..c176a62458
--- /dev/null
+++ b/activerecord/lib/active_record/database_configurations/hash_config.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ class DatabaseConfigurations
+ # A HashConfig object is created for each database configuration entry that
+ # is created from a hash.
+ #
+ # A hash config:
+ #
+ # { "development" => { "database" => "db_name" } }
+ #
+ # Becomes:
+ #
+ # #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbded10
+ # @env_name="development", @spec_name="primary", @config={"database"=>"db_name"}>
+ #
+ # Options are:
+ #
+ # <tt>:env_name</tt> - The Rails environment, ie "development"
+ # <tt>:spec_name</tt> - The specification name. In a standard two-tier
+ # database configuration this will default to "primary". In a multiple
+ # database three-tier database configuration this corresponds to the name
+ # used in the second tier, for example "primary_readonly".
+ # <tt>:config</tt> - The config hash. This is the hash that contains the
+ # database adapter, name, and other important information for database
+ # connections.
+ class HashConfig < DatabaseConfig
+ attr_reader :config
+
+ def initialize(env_name, spec_name, config)
+ super(env_name, spec_name)
+ @config = config
+ end
+
+ # Determines whether a database configuration is for a replica / readonly
+ # connection. If the `replica` key is present in the config, `replica?` will
+ # return +true+.
+ def replica?
+ config["replica"]
+ end
+
+ # The migrations paths for a database configuration. If the
+ # `migrations_paths` key is present in the config, `migrations_paths`
+ # will return its value.
+ def migrations_paths
+ config["migrations_paths"]
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/database_configurations/url_config.rb b/activerecord/lib/active_record/database_configurations/url_config.rb
new file mode 100644
index 0000000000..81917fc4c1
--- /dev/null
+++ b/activerecord/lib/active_record/database_configurations/url_config.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ class DatabaseConfigurations
+ # A UrlConfig object is created for each database configuration
+ # entry that is created from a URL. This can either be a URL string
+ # or a hash with a URL in place of the config hash.
+ #
+ # A URL config:
+ #
+ # postgres://localhost/foo
+ #
+ # Becomes:
+ #
+ # #<ActiveRecord::DatabaseConfigurations::UrlConfig:0x00007fdc3238f340
+ # @env_name="default_env", @spec_name="primary",
+ # @config={"adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost"},
+ # @url="postgres://localhost/foo">
+ #
+ # Options are:
+ #
+ # <tt>:env_name</tt> - The Rails environment, ie "development"
+ # <tt>:spec_name</tt> - The specification name. In a standard two-tier
+ # database configuration this will default to "primary". In a multiple
+ # database three-tier database configuration this corresponds to the name
+ # used in the second tier, for example "primary_readonly".
+ # <tt>:url</tt> - The database URL.
+ # <tt>:config</tt> - The config hash. This is the hash that contains the
+ # database adapter, name, and other important information for database
+ # connections.
+ class UrlConfig < DatabaseConfig
+ attr_reader :url, :config
+
+ def initialize(env_name, spec_name, url, config = {})
+ super(env_name, spec_name)
+ @config = build_config(config, url)
+ @url = url
+ end
+
+ def url_config? # :nodoc:
+ true
+ end
+
+ # Determines whether a database configuration is for a replica / readonly
+ # connection. If the `replica` key is present in the config, `replica?` will
+ # return +true+.
+ def replica?
+ config["replica"]
+ end
+
+ # The migrations paths for a database configuration. If the
+ # `migrations_paths` key is present in the config, `migrations_paths`
+ # will return its value.
+ def migrations_paths
+ config["migrations_paths"]
+ end
+
+ private
+ def build_config(original_config, url)
+ if /^jdbc:/.match?(url)
+ hash = { "url" => url }
+ else
+ hash = ActiveRecord::ConnectionAdapters::ConnectionSpecification::ConnectionUrlResolver.new(url).to_hash
+ end
+
+ if original_config[env_name]
+ original_config[env_name].merge(hash)
+ else
+ original_config.merge(hash)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/define_callbacks.rb b/activerecord/lib/active_record/define_callbacks.rb
new file mode 100644
index 0000000000..87ecd7cec5
--- /dev/null
+++ b/activerecord/lib/active_record/define_callbacks.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ # This module exists because ActiveRecord::AttributeMethods::Dirty needs to
+ # define callbacks, but continue to have its version of +save+ be the super
+ # method of ActiveRecord::Callbacks. This will be removed when the removal
+ # of deprecated code removes this need.
+ module DefineCallbacks
+ extend ActiveSupport::Concern
+
+ module ClassMethods # :nodoc:
+ include ActiveModel::Callbacks
+ end
+
+ included do
+ include ActiveModel::Validations::Callbacks
+
+ define_model_callbacks :initialize, :find, :touch, only: :after
+ define_model_callbacks :save, :create, :update, :destroy
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb
index b6dd6814db..3bb8c6f4e3 100644
--- a/activerecord/lib/active_record/dynamic_matchers.rb
+++ b/activerecord/lib/active_record/dynamic_matchers.rb
@@ -1,121 +1,122 @@
+# frozen_string_literal: true
+
module ActiveRecord
module DynamicMatchers #:nodoc:
- def respond_to?(name, include_private = false)
- if self == Base
- super
- else
+ private
+ def respond_to_missing?(name, _)
+ if self == Base
+ super
+ else
+ match = Method.match(self, name)
+ match && match.valid? || super
+ end
+ end
+
+ def method_missing(name, *arguments, &block)
match = Method.match(self, name)
- match && match.valid? || super
+
+ if match && match.valid?
+ match.define
+ send(name, *arguments, &block)
+ else
+ super
+ end
end
- end
- private
+ class Method
+ @matchers = []
- def method_missing(name, *arguments, &block)
- match = Method.match(self, name)
+ class << self
+ attr_reader :matchers
- if match && match.valid?
- match.define
- send(name, *arguments, &block)
- else
- super
- end
- end
+ def match(model, name)
+ klass = matchers.find { |k| k.pattern.match?(name) }
+ klass.new(model, name) if klass
+ end
- class Method
- @matchers = []
+ def pattern
+ @pattern ||= /\A#{prefix}_([_a-zA-Z]\w*)#{suffix}\Z/
+ end
- class << self
- attr_reader :matchers
+ def prefix
+ raise NotImplementedError
+ end
- def match(model, name)
- klass = matchers.find { |k| name =~ k.pattern }
- klass.new(model, name) if klass
+ def suffix
+ ""
+ end
end
- def pattern
- @pattern ||= /\A#{prefix}_([_a-zA-Z]\w*)#{suffix}\Z/
- end
+ attr_reader :model, :name, :attribute_names
- def prefix
- raise NotImplementedError
+ def initialize(model, name)
+ @model = model
+ @name = name.to_s
+ @attribute_names = @name.match(self.class.pattern)[1].split("_and_")
+ @attribute_names.map! { |n| @model.attribute_aliases[n] || n }
end
- def suffix
- ''
+ def valid?
+ attribute_names.all? { |name| model.columns_hash[name] || model.reflect_on_aggregation(name.to_sym) }
end
- end
-
- attr_reader :model, :name, :attribute_names
- def initialize(model, name)
- @model = model
- @name = name.to_s
- @attribute_names = @name.match(self.class.pattern)[1].split('_and_')
- @attribute_names.map! { |n| @model.attribute_aliases[n] || n }
- end
+ def define
+ model.class_eval <<-CODE, __FILE__, __LINE__ + 1
+ def self.#{name}(#{signature})
+ #{body}
+ end
+ CODE
+ end
- def valid?
- attribute_names.all? { |name| model.columns_hash[name] || model.reflect_on_aggregation(name.to_sym) }
- end
+ private
- def define
- model.class_eval <<-CODE, __FILE__, __LINE__ + 1
- def self.#{name}(#{signature})
- #{body}
+ def body
+ "#{finder}(#{attributes_hash})"
end
- CODE
- end
-
- private
-
- def body
- "#{finder}(#{attributes_hash})"
- end
- # The parameters in the signature may have reserved Ruby words, in order
- # to prevent errors, we start each param name with `_`.
- def signature
- attribute_names.map { |name| "_#{name}" }.join(', ')
- end
+ # The parameters in the signature may have reserved Ruby words, in order
+ # to prevent errors, we start each param name with `_`.
+ def signature
+ attribute_names.map { |name| "_#{name}" }.join(", ")
+ end
- # Given that the parameters starts with `_`, the finder needs to use the
- # same parameter name.
- def attributes_hash
- "{" + attribute_names.map { |name| ":#{name} => _#{name}" }.join(',') + "}"
- end
+ # Given that the parameters starts with `_`, the finder needs to use the
+ # same parameter name.
+ def attributes_hash
+ "{" + attribute_names.map { |name| ":#{name} => _#{name}" }.join(",") + "}"
+ end
- def finder
- raise NotImplementedError
+ def finder
+ raise NotImplementedError
+ end
end
- end
- class FindBy < Method
- Method.matchers << self
+ class FindBy < Method
+ Method.matchers << self
- def self.prefix
- "find_by"
- end
+ def self.prefix
+ "find_by"
+ end
- def finder
- "find_by"
+ def finder
+ "find_by"
+ end
end
- end
- class FindByBang < Method
- Method.matchers << self
+ class FindByBang < Method
+ Method.matchers << self
- def self.prefix
- "find_by"
- end
+ def self.prefix
+ "find_by"
+ end
- def self.suffix
- "!"
- end
+ def self.suffix
+ "!"
+ end
- def finder
- "find_by!"
+ def finder
+ "find_by!"
+ end
end
- end
end
end
diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb
index c0d9d9c1c8..3a600835e1 100644
--- a/activerecord/lib/active_record/enum.rb
+++ b/activerecord/lib/active_record/enum.rb
@@ -1,4 +1,6 @@
-require 'active_support/core_ext/object/deep_dup'
+# frozen_string_literal: true
+
+require "active_support/core_ext/object/deep_dup"
module ActiveRecord
# Declare an enum attribute where the values map to integers in the database,
@@ -18,10 +20,9 @@ module ActiveRecord
# conversation.archived? # => true
# conversation.status # => "archived"
#
- # # conversation.update! status: 1
+ # # conversation.status = 1
# conversation.status = "archived"
#
- # # conversation.update! status: nil
# conversation.status = nil
# conversation.status.nil? # => true
# conversation.status # => nil
@@ -47,13 +48,13 @@ module ActiveRecord
# Good practice is to let the first declared status be the default.
#
# Finally, it's also possible to explicitly map the relation between attribute and
- # database integer with a +Hash+:
+ # database integer with a hash:
#
# class Conversation < ActiveRecord::Base
# enum status: { active: 0, archived: 1 }
# end
#
- # Note that when an +Array+ is used, the implicit mapping from the values to database
+ # Note that when an array is used, the implicit mapping from the values to database
# integers is derived from the order the values appear in the array. In the example,
# <tt>:active</tt> is mapped to +0+ as it's the first element, and <tt>:archived</tt>
# is mapped to +1+. In general, the +i+-th element is mapped to <tt>i-1</tt> in the
@@ -61,7 +62,7 @@ module ActiveRecord
#
# Therefore, once a value is added to the enum array, its position in the array must
# be maintained, and new values should only be added to the end of the array. To
- # remove unused values, the explicit +Hash+ syntax should be used.
+ # remove unused values, the explicit hash syntax should be used.
#
# In rare circumstances you might need to access the mapping directly.
# The mappings are exposed through a class method with the pluralized attribute
@@ -75,27 +76,28 @@ module ActiveRecord
#
# Conversation.where("status <> ?", Conversation.statuses[:archived])
#
- # You can use the +:enum_prefix+ or +:enum_suffix+ options when you need
- # to define multiple enums with same values. If the passed value is +true+,
- # the methods are prefixed/suffixed with the name of the enum.
+ # You can use the +:_prefix+ or +:_suffix+ options when you need to define
+ # multiple enums with same values. If the passed value is +true+, the methods
+ # are prefixed/suffixed with the name of the enum. It is also possible to
+ # supply a custom value:
#
- # class Invoice < ActiveRecord::Base
- # enum verification: [:done, :fail], enum_prefix: true
+ # class Conversation < ActiveRecord::Base
+ # enum status: [:active, :archived], _suffix: true
+ # enum comments_status: [:active, :inactive], _prefix: :comments
# end
#
- # It is also possible to supply a custom prefix.
+ # With the above example, the bang and predicate methods along with the
+ # associated scopes are now prefixed and/or suffixed accordingly:
#
- # class Invoice < ActiveRecord::Base
- # enum verification: [:done, :fail], enum_prefix: :verification_status
- # end
+ # conversation.active_status!
+ # conversation.archived_status? # => false
#
- # Note that <tt>:enum_prefix</tt>/<tt>:enum_suffix</tt> are reserved keywords
- # and can not be used as an enum name.
+ # conversation.comments_inactive!
+ # conversation.comments_active? # => false
module Enum
def self.extended(base) # :nodoc:
- base.class_attribute(:defined_enums)
- base.defined_enums = {}
+ base.class_attribute(:defined_enums, instance_writer: false, default: {})
end
def inherited(base) # :nodoc:
@@ -103,10 +105,13 @@ module ActiveRecord
super
end
- class EnumType < Type::Value
- def initialize(name, mapping)
+ class EnumType < Type::Value # :nodoc:
+ delegate :type, to: :subtype
+
+ def initialize(name, mapping, subtype)
@name = name
@mapping = mapping
+ @subtype = subtype
end
def cast(value)
@@ -117,45 +122,55 @@ module ActiveRecord
elsif mapping.has_value?(value)
mapping.key(value)
else
- raise ArgumentError, "'#{value}' is not a valid #{name}"
+ assert_valid_value(value)
end
end
def deserialize(value)
return if value.nil?
- mapping.key(value.to_i)
+ mapping.key(subtype.deserialize(value))
end
def serialize(value)
mapping.fetch(value, value)
end
- protected
+ def assert_valid_value(value)
+ unless value.blank? || mapping.has_key?(value) || mapping.has_value?(value)
+ raise ArgumentError, "'#{value}' is not a valid #{name}"
+ end
+ end
- attr_reader :name, :mapping
+ private
+ attr_reader :name, :mapping, :subtype
end
def enum(definitions)
klass = self
- enum_prefix = definitions.delete(:enum_prefix)
- enum_suffix = definitions.delete(:enum_suffix)
+ enum_prefix = definitions.delete(:_prefix)
+ enum_suffix = definitions.delete(:_suffix)
definitions.each do |name, values|
+ assert_valid_enum_definition_values(values)
# statuses = { }
enum_values = ActiveSupport::HashWithIndifferentAccess.new
- name = name.to_sym
+ name = name.to_s
- # def self.statuses statuses end
- detect_enum_conflict!(name, name.to_s.pluralize, true)
- klass.singleton_class.send(:define_method, name.to_s.pluralize) { enum_values }
+ # def self.statuses() statuses end
+ detect_enum_conflict!(name, name.pluralize, true)
+ singleton_class.send(:define_method, name.pluralize) { enum_values }
+ defined_enums[name] = enum_values
detect_enum_conflict!(name, name)
detect_enum_conflict!(name, "#{name}=")
- attribute name, EnumType.new(name, enum_values)
+ attr = attribute_alias?(name) ? attribute_alias(name) : name
+ decorate_attribute_type(attr, :enum) do |subtype|
+ EnumType.new(attr, enum_values, subtype)
+ end
_enum_methods_module.module_eval do
pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index
- pairs.each do |value, i|
+ pairs.each do |label, value|
if enum_prefix == true
prefix = "#{name}_"
elsif enum_prefix
@@ -167,23 +182,23 @@ module ActiveRecord
suffix = "_#{enum_suffix}"
end
- value_method_name = "#{prefix}#{value}#{suffix}"
- enum_values[value] = i
+ value_method_name = "#{prefix}#{label}#{suffix}"
+ enum_values[label] = value
+ label = label.to_s
- # def active?() status == 0 end
+ # def active?() status == "active" end
klass.send(:detect_enum_conflict!, name, "#{value_method_name}?")
- define_method("#{value_method_name}?") { self[name] == value.to_s }
+ define_method("#{value_method_name}?") { self[attr] == label }
- # def active!() update! status: :active end
+ # def active!() update!(status: 0) end
klass.send(:detect_enum_conflict!, name, "#{value_method_name}!")
- define_method("#{value_method_name}!") { update! name => value }
+ define_method("#{value_method_name}!") { update!(attr => value) }
- # scope :active, -> { where status: 0 }
+ # scope :active, -> { where(status: 0) }
klass.send(:detect_enum_conflict!, name, value_method_name, true)
- klass.scope value_method_name, -> { klass.where name => value }
+ klass.scope value_method_name, -> { where(attr => value) }
end
end
- defined_enums[name.to_s] = enum_values
end
end
@@ -196,37 +211,45 @@ module ActiveRecord
end
end
+ def assert_valid_enum_definition_values(values)
+ unless values.is_a?(Hash) || values.all? { |v| v.is_a?(Symbol) } || values.all? { |v| v.is_a?(String) }
+ error_message = <<~MSG
+ Enum values #{values} must be either a hash, an array of symbols, or an array of strings.
+ MSG
+ raise ArgumentError, error_message
+ end
+
+ if values.is_a?(Hash) && values.keys.any?(&:blank?) || values.is_a?(Array) && values.any?(&:blank?)
+ raise ArgumentError, "Enum label name must not be blank."
+ end
+ end
+
ENUM_CONFLICT_MESSAGE = \
"You tried to define an enum named \"%{enum}\" on the model \"%{klass}\", but " \
"this will generate a %{type} method \"%{method}\", which is already defined " \
"by %{source}."
+ private_constant :ENUM_CONFLICT_MESSAGE
def detect_enum_conflict!(enum_name, method_name, klass_method = false)
if klass_method && dangerous_class_method?(method_name)
- raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
- enum: enum_name,
- klass: self.name,
- type: 'class',
- method: method_name,
- source: 'Active Record'
- }
+ raise_conflict_error(enum_name, method_name, type: "class")
+ elsif klass_method && method_defined_within?(method_name, Relation)
+ raise_conflict_error(enum_name, method_name, type: "class", source: Relation.name)
elsif !klass_method && dangerous_attribute_method?(method_name)
- raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
- enum: enum_name,
- klass: self.name,
- type: 'instance',
- method: method_name,
- source: 'Active Record'
- }
+ raise_conflict_error(enum_name, method_name)
elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module)
- raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
- enum: enum_name,
- klass: self.name,
- type: 'instance',
- method: method_name,
- source: 'another enum'
- }
+ raise_conflict_error(enum_name, method_name, source: "another enum")
end
end
+
+ def raise_conflict_error(enum_name, method_name, type: "instance", source: "Active Record")
+ raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
+ enum: enum_name,
+ klass: name,
+ type: type,
+ method: method_name,
+ source: source
+ }
+ end
end
end
diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb
index 0f1759abaa..f61bc7b9e8 100644
--- a/activerecord/lib/active_record/errors.rb
+++ b/activerecord/lib/active_record/errors.rb
@@ -1,5 +1,6 @@
-module ActiveRecord
+# frozen_string_literal: true
+module ActiveRecord
# = Active Record Errors
#
# Generic Active Record exception class.
@@ -7,8 +8,10 @@ module ActiveRecord
end
# Raised when the single-table inheritance mechanism fails to locate the subclass
- # (for example due to improper usage of column that +inheritance_column+ points to).
- class SubclassNotFound < ActiveRecordError #:nodoc:
+ # (for example due to improper usage of column that
+ # {ActiveRecord::Base.inheritance_column}[rdoc-ref:ModelSchema::ClassMethods#inheritance_column]
+ # points to).
+ class SubclassNotFound < ActiveRecordError
end
# Raised when an object assigned to an association has an incorrect type.
@@ -40,27 +43,40 @@ module ActiveRecord
class AdapterNotFound < ActiveRecordError
end
- # Raised when connection to the database could not been established (for
- # example when +connection=+ is given a nil object).
+ # Raised when connection to the database could not been established (for example when
+ # {ActiveRecord::Base.connection=}[rdoc-ref:ConnectionHandling#connection]
+ # is given a +nil+ object).
class ConnectionNotEstablished < ActiveRecordError
end
- # Raised when Active Record cannot find record by given id or set of ids.
+ # Raised when Active Record cannot find a record by given id or set of ids.
class RecordNotFound < ActiveRecordError
+ attr_reader :model, :primary_key, :id
+
+ def initialize(message = nil, model = nil, primary_key = nil, id = nil)
+ @primary_key = primary_key
+ @model = model
+ @id = id
+
+ super(message)
+ end
end
- # Raised by ActiveRecord::Base.save! and ActiveRecord::Base.create! methods when record cannot be
- # saved because record is invalid.
+ # Raised by {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!] and
+ # {ActiveRecord::Base.create!}[rdoc-ref:Persistence::ClassMethods#create!]
+ # methods when a record is invalid and can not be saved.
class RecordNotSaved < ActiveRecordError
attr_reader :record
- def initialize(message, record = nil)
+ def initialize(message = nil, record = nil)
@record = record
super(message)
end
end
- # Raised by ActiveRecord::Base.destroy! when a call to destroy would return false.
+ # Raised by {ActiveRecord::Base#destroy!}[rdoc-ref:Persistence#destroy!]
+ # when a call to {#destroy}[rdoc-ref:Persistence#destroy!]
+ # would return false.
#
# begin
# complex_operation_that_internally_calls_destroy!
@@ -71,7 +87,7 @@ module ActiveRecord
class RecordNotDestroyed < ActiveRecordError
attr_reader :record
- def initialize(message, record = nil)
+ def initialize(message = nil, record = nil)
@record = record
super(message)
end
@@ -79,32 +95,70 @@ module ActiveRecord
# Superclass for all database execution errors.
#
- # Wraps the underlying database error as +original_exception+.
+ # Wraps the underlying database error as +cause+.
class StatementInvalid < ActiveRecordError
- attr_reader :original_exception
-
- def initialize(message, original_exception = nil)
- super(message)
- @original_exception = original_exception
+ def initialize(message = nil)
+ super(message || $!.try(:message))
end
end
# Defunct wrapper class kept for compatibility.
- # +StatementInvalid+ wraps the original exception now.
+ # StatementInvalid wraps the original exception now.
class WrappedDatabaseException < StatementInvalid
end
- # Raised when a record cannot be inserted because it would violate a uniqueness constraint.
+ # Raised when a record cannot be inserted or updated because it would violate a uniqueness constraint.
class RecordNotUnique < WrappedDatabaseException
end
- # Raised when a record cannot be inserted or updated because it references a non-existent record.
+ # Raised when a record cannot be inserted or updated because it references a non-existent record,
+ # or when a record cannot be deleted because a parent record references it.
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
+ 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 +find+ method) does not match number of expected
- # values supplied.
+ # (for example, when using {ActiveRecord::Base.find}[rdoc-ref:FinderMethods#find] method)
+ # does not match number of expected values supplied.
#
# For example, when there are two placeholders with only one value supplied:
#
@@ -116,6 +170,11 @@ module ActiveRecord
class NoDatabaseError < StatementInvalid
end
+ # Raised when PostgreSQL returns 'cached plan must not change result type' and
+ # we cannot retry gracefully (e.g. inside a transaction)
+ class PreparedStatementCacheExpired < StatementInvalid
+ end
+
# Raised on attempt to save stale record. Record is stale when it's being saved in another query after
# instantiation, for example, when two users edit the same wiki page and one starts editing and saves
# the page before the other.
@@ -125,16 +184,21 @@ module ActiveRecord
class StaleObjectError < ActiveRecordError
attr_reader :record, :attempted_action
- def initialize(record, attempted_action)
- super("Attempted to #{attempted_action} a stale object: #{record.class.name}")
- @record = record
- @attempted_action = attempted_action
+ def initialize(record = nil, attempted_action = nil)
+ if record && attempted_action
+ @record = record
+ @attempted_action = attempted_action
+ super("Attempted to #{attempted_action} a stale object: #{record.class.name}.")
+ else
+ super("Stale object error.")
+ end
end
-
end
# Raised when association is being configured improperly or user tries to use
- # offset and limit together with +has_many+ or +has_and_belongs_to_many+
+ # offset and limit together with
+ # {ActiveRecord::Base.has_many}[rdoc-ref:Associations::ClassMethods#has_many] or
+ # {ActiveRecord::Base.has_and_belongs_to_many}[rdoc-ref:Associations::ClassMethods#has_and_belongs_to_many]
# associations.
class ConfigurationError < ActiveRecordError
end
@@ -143,9 +207,10 @@ module ActiveRecord
class ReadOnlyRecord < ActiveRecordError
end
- # ActiveRecord::Transactions::ClassMethods.transaction uses this exception
- # to distinguish a deliberate rollback from other exceptional situations.
- # Normally, raising an exception will cause the +transaction+ method to rollback
+ # {ActiveRecord::Base.transaction}[rdoc-ref:Transactions::ClassMethods#transaction]
+ # uses this exception to distinguish a deliberate rollback from other exceptional situations.
+ # Normally, raising an exception will cause the
+ # {.transaction}[rdoc-ref:Transactions::ClassMethods#transaction] method to rollback
# the database transaction *and* pass on the exception. But if you raise an
# ActiveRecord::Rollback exception, then the database transaction will be rolled back,
# without passing on the exception.
@@ -182,25 +247,26 @@ module ActiveRecord
UnknownAttributeError = ActiveModel::UnknownAttributeError
# Raised when an error occurred while doing a mass assignment to an attribute through the
- # +attributes=+ method. The exception has an +attribute+ property that is the name of the
- # offending attribute.
+ # {ActiveRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=] method.
+ # The exception has an +attribute+ property that is the name of the offending attribute.
class AttributeAssignmentError < ActiveRecordError
attr_reader :exception, :attribute
- def initialize(message, exception, attribute)
+ def initialize(message = nil, exception = nil, attribute = nil)
super(message)
@exception = exception
@attribute = attribute
end
end
- # Raised when there are multiple errors while doing a mass assignment through the +attributes+
+ # Raised when there are multiple errors while doing a mass assignment through the
+ # {ActiveRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=]
# method. The exception has an +errors+ property that contains an array of AttributeAssignmentError
# objects, each corresponding to the error while assigning to an attribute.
class MultiparameterAssignmentErrors < ActiveRecordError
attr_reader :errors
- def initialize(errors)
+ def initialize(errors = nil)
@errors = errors
end
end
@@ -209,11 +275,16 @@ module ActiveRecord
class UnknownPrimaryKey < ActiveRecordError
attr_reader :model
- def initialize(model)
- super("Unknown primary key for table #{model.table_name} in model #{model}.")
- @model = model
+ def initialize(model = nil, description = nil)
+ if model
+ message = "Unknown primary key for table #{model.table_name} in model #{model}."
+ message += "\n#{description}" if description
+ @model = model
+ super(message)
+ else
+ super("Unknown primary key.")
+ end
end
-
end
# Raised when a relation cannot be mutated because it's already loaded.
@@ -236,7 +307,69 @@ module ActiveRecord
# * You are joining an existing open transaction
# * You are creating a nested (savepoint) transaction
#
- # The mysql, mysql2 and postgresql adapters support setting the transaction isolation level.
+ # The mysql2 and postgresql adapters support setting the transaction isolation level.
class TransactionIsolationError < ActiveRecordError
end
+
+ # TransactionRollbackError will be raised when a transaction is rolled
+ # back by the database due to a serialization failure or a deadlock.
+ #
+ # See the following:
+ #
+ # * https://www.postgresql.org/docs/current/static/transaction-iso.html
+ # * https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html#error_er_lock_deadlock
+ class TransactionRollbackError < StatementInvalid
+ end
+
+ # SerializationFailure will be raised when a transaction is rolled
+ # back by the database due to a serialization failure.
+ class SerializationFailure < TransactionRollbackError
+ end
+
+ # Deadlocked will be raised when a transaction is rolled
+ # back by the database when a deadlock is encountered.
+ class Deadlocked < TransactionRollbackError
+ end
+
+ # IrreversibleOrderError is raised when a relation's order is too complex for
+ # +reverse_order+ to automatically reverse.
+ class IrreversibleOrderError < ActiveRecordError
+ end
+
+ # LockWaitTimeout will be raised when lock wait timeout exceeded.
+ class LockWaitTimeout < StatementInvalid
+ end
+
+ # StatementTimeout will be raised when statement timeout exceeded.
+ class StatementTimeout < StatementInvalid
+ end
+
+ # QueryCanceled will be raised when canceling statement due to user request.
+ class QueryCanceled < StatementInvalid
+ end
+
+ # UnknownAttributeReference is raised when an unknown and potentially unsafe
+ # value is passed to a query method when allow_unsafe_raw_sql is set to
+ # :disabled. For example, passing a non column name value to a relation's
+ # #order method might cause this exception.
+ #
+ # When working around this exception, caution should be taken to avoid SQL
+ # injection vulnerabilities when passing user-provided values to query
+ # methods. Known-safe values can be passed to query methods by wrapping them
+ # in Arel.sql.
+ #
+ # For example, with allow_unsafe_raw_sql set to :disabled, the following
+ # code would raise this exception:
+ #
+ # Post.order("length(title)").first
+ #
+ # The desired result can be accomplished by wrapping the known-safe string
+ # in Arel.sql:
+ #
+ # Post.order(Arel.sql("length(title)")).first
+ #
+ # Again, such a workaround should *not* be used when passing user-provided
+ # values, such as request parameters or model attributes to query methods.
+ class UnknownAttributeReference < ActiveRecordError
+ end
end
diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb
index 727a9befc1..919e96cd7a 100644
--- a/activerecord/lib/active_record/explain.rb
+++ b/activerecord/lib/active_record/explain.rb
@@ -1,5 +1,6 @@
-require 'active_support/lazy_load_hooks'
-require 'active_record/explain_registry'
+# frozen_string_literal: true
+
+require "active_record/explain_registry"
module ActiveRecord
module Explain
@@ -16,15 +17,14 @@ module ActiveRecord
# Makes the adapter execute EXPLAIN for the tuples of queries and bindings.
# Returns a formatted string ready to be logged.
def exec_explain(queries) # :nodoc:
- str = queries.map do |sql, bind|
- [].tap do |msg|
- msg << "EXPLAIN for: #{sql}"
- unless bind.empty?
- bind_msg = bind.map {|col, val| [col.name, val]}.inspect
- msg.last << " #{bind_msg}"
- end
- msg << connection.explain(sql, bind)
- end.join("\n")
+ str = queries.map do |sql, binds|
+ msg = +"EXPLAIN for: #{sql}"
+ unless binds.empty?
+ msg << " "
+ msg << binds.map { |attr| render_bind(attr) }.inspect
+ end
+ msg << "\n"
+ msg << connection.explain(sql, binds)
end.join("\n")
# Overriding inspect to be more human readable, especially in the console.
@@ -34,5 +34,17 @@ module ActiveRecord
str
end
+
+ private
+
+ def render_bind(attr)
+ value = if attr.type.binary? && attr.value
+ "<#{attr.value_for_database.to_s.bytesize} bytes of binary data>"
+ else
+ connection.type_cast(attr.value_for_database)
+ end
+
+ [attr.name, value]
+ end
end
end
diff --git a/activerecord/lib/active_record/explain_registry.rb b/activerecord/lib/active_record/explain_registry.rb
index f5cd57e075..7fd078941a 100644
--- a/activerecord/lib/active_record/explain_registry.rb
+++ b/activerecord/lib/active_record/explain_registry.rb
@@ -1,4 +1,6 @@
-require 'active_support/per_thread_registry'
+# frozen_string_literal: true
+
+require "active_support/per_thread_registry"
module ActiveRecord
# This is a thread locals registry for EXPLAIN. For example
@@ -7,7 +9,7 @@ module ActiveRecord
#
# returns the collected queries local to the current thread.
#
- # See the documentation of <tt>ActiveSupport::PerThreadRegistry</tt>
+ # See the documentation of ActiveSupport::PerThreadRegistry
# for further details.
class ExplainRegistry # :nodoc:
extend ActiveSupport::PerThreadRegistry
diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb
index 9adabd7819..a86217abc0 100644
--- a/activerecord/lib/active_record/explain_subscriber.rb
+++ b/activerecord/lib/active_record/explain_subscriber.rb
@@ -1,5 +1,7 @@
-require 'active_support/notifications'
-require 'active_record/explain_registry'
+# frozen_string_literal: true
+
+require "active_support/notifications"
+require "active_record/explain_registry"
module ActiveRecord
class ExplainSubscriber # :nodoc:
@@ -14,14 +16,17 @@ module ActiveRecord
end
# SCHEMA queries cannot be EXPLAINed, also we do not want to run EXPLAIN on
- # our own EXPLAINs now matter how loopingly beautiful that would be.
+ # our own EXPLAINs no matter how loopingly beautiful that would be.
#
# On the other hand, we want to monitor the performance of our real database
# queries, not the performance of the access to the query cache.
- IGNORED_PAYLOADS = %w(SCHEMA EXPLAIN CACHE)
+ IGNORED_PAYLOADS = %w(SCHEMA EXPLAIN)
EXPLAINED_SQLS = /\A\s*(with|select|update|delete|insert)\b/i
def ignore_payload?(payload)
- payload[:exception] || IGNORED_PAYLOADS.include?(payload[:name]) || payload[:sql] !~ EXPLAINED_SQLS
+ payload[:exception] ||
+ payload[:cached] ||
+ IGNORED_PAYLOADS.include?(payload[:name]) ||
+ payload[:sql] !~ EXPLAINED_SQLS
end
ActiveSupport::Notifications.subscribe("sql.active_record", new)
diff --git a/activerecord/lib/active_record/fixture_set/file.rb b/activerecord/lib/active_record/fixture_set/file.rb
index 8132310c91..f1ea0e022f 100644
--- a/activerecord/lib/active_record/fixture_set/file.rb
+++ b/activerecord/lib/active_record/fixture_set/file.rb
@@ -1,5 +1,7 @@
-require 'erb'
-require 'yaml'
+# frozen_string_literal: true
+
+require "erb"
+require "yaml"
module ActiveRecord
class FixtureSet
@@ -17,38 +19,62 @@ module ActiveRecord
def initialize(file)
@file = file
- @rows = nil
end
def each(&block)
rows.each(&block)
end
+ def model_class
+ config_row["model_class"]
+ end
private
def rows
- return @rows if @rows
+ @rows ||= raw_rows.reject { |fixture_name, _| fixture_name == "_fixture" }
+ end
- begin
+ def config_row
+ @config_row ||= begin
+ row = raw_rows.find { |fixture_name, _| fixture_name == "_fixture" }
+ if row
+ row.last
+ else
+ { 'model_class': nil }
+ end
+ end
+ end
+
+ def raw_rows
+ @raw_rows ||= begin
data = YAML.load(render(IO.read(@file)))
+ data ? validate(data).to_a : []
rescue ArgumentError, Psych::SyntaxError => error
raise Fixture::FormatError, "a YAML error occurred parsing #{@file}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n #{error.class}: #{error}", error.backtrace
end
- @rows = data ? validate(data).to_a : []
+ end
+
+ def prepare_erb(content)
+ erb = ERB.new(content)
+ erb.filename = @file
+ erb
end
def render(content)
context = ActiveRecord::FixtureSet::RenderContext.create_subclass.new
- ERB.new(content).result(context.get_binding)
+ prepare_erb(content).result(context.get_binding)
end
# Validate our unmarshalled data.
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/fixture_set/model_metadata.rb b/activerecord/lib/active_record/fixture_set/model_metadata.rb
new file mode 100644
index 0000000000..fb23df6f45
--- /dev/null
+++ b/activerecord/lib/active_record/fixture_set/model_metadata.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ class FixtureSet
+ class ModelMetadata # :nodoc:
+ def initialize(model_class)
+ @model_class = model_class
+ end
+
+ def primary_key_name
+ @primary_key_name ||= @model_class && @model_class.primary_key
+ end
+
+ def primary_key_type
+ @primary_key_type ||= @model_class && @model_class.type_for_attribute(@model_class.primary_key).type
+ end
+
+ def has_primary_key_column?
+ @has_primary_key_column ||= primary_key_name &&
+ @model_class.columns.any? { |col| col.name == primary_key_name }
+ end
+
+ def timestamp_column_names
+ @timestamp_column_names ||=
+ %w(created_at created_on updated_at updated_on) & @model_class.column_names
+ end
+
+ def inheritance_column_name
+ @inheritance_column_name ||= @model_class && @model_class.inheritance_column
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/fixture_set/render_context.rb b/activerecord/lib/active_record/fixture_set/render_context.rb
new file mode 100644
index 0000000000..c90b5343dc
--- /dev/null
+++ b/activerecord/lib/active_record/fixture_set/render_context.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+# NOTE: This class has to be defined in compact style in
+# order for rendering context subclassing to work correctly.
+class ActiveRecord::FixtureSet::RenderContext # :nodoc:
+ def self.create_subclass
+ Class.new(ActiveRecord::FixtureSet.context_class) do
+ def get_binding
+ binding()
+ end
+
+ def binary(path)
+ %(!!binary "#{Base64.strict_encode64(File.read(path))}")
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/fixture_set/table_row.rb b/activerecord/lib/active_record/fixture_set/table_row.rb
new file mode 100644
index 0000000000..cb4726f1ee
--- /dev/null
+++ b/activerecord/lib/active_record/fixture_set/table_row.rb
@@ -0,0 +1,153 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ class FixtureSet
+ class TableRow # :nodoc:
+ class ReflectionProxy # :nodoc:
+ def initialize(association)
+ @association = association
+ end
+
+ def join_table
+ @association.join_table
+ end
+
+ def name
+ @association.name
+ end
+
+ def primary_key_type
+ @association.klass.type_for_attribute(@association.klass.primary_key).type
+ end
+ end
+
+ class HasManyThroughProxy < ReflectionProxy # :nodoc:
+ def rhs_key
+ @association.foreign_key
+ end
+
+ def lhs_key
+ @association.through_reflection.foreign_key
+ end
+
+ def join_table
+ @association.through_reflection.table_name
+ end
+ end
+
+ def initialize(fixture, table_rows:, label:, now:)
+ @table_rows = table_rows
+ @label = label
+ @now = now
+ @row = fixture.to_hash
+ fill_row_model_attributes
+ end
+
+ def to_hash
+ @row
+ end
+
+ private
+
+ def model_metadata
+ @table_rows.model_metadata
+ end
+
+ def model_class
+ @table_rows.model_class
+ end
+
+ def fill_row_model_attributes
+ return unless model_class
+ fill_timestamps
+ interpolate_label
+ generate_primary_key
+ resolve_enums
+ resolve_sti_reflections
+ end
+
+ def reflection_class
+ @reflection_class ||= if @row.include?(model_metadata.inheritance_column_name)
+ @row[model_metadata.inheritance_column_name].constantize rescue model_class
+ else
+ model_class
+ end
+ end
+
+ def fill_timestamps
+ # fill in timestamp columns if they aren't specified and the model is set to record_timestamps
+ if model_class.record_timestamps
+ model_metadata.timestamp_column_names.each do |c_name|
+ @row[c_name] = @now unless @row.key?(c_name)
+ end
+ end
+ end
+
+ def interpolate_label
+ # interpolate the fixture label
+ @row.each do |key, value|
+ @row[key] = value.gsub("$LABEL", @label.to_s) if value.is_a?(String)
+ end
+ end
+
+ def generate_primary_key
+ # generate a primary key if necessary
+ if model_metadata.has_primary_key_column? && !@row.include?(model_metadata.primary_key_name)
+ @row[model_metadata.primary_key_name] = ActiveRecord::FixtureSet.identify(
+ @label, model_metadata.primary_key_type
+ )
+ end
+ end
+
+ def resolve_enums
+ model_class.defined_enums.each do |name, values|
+ if @row.include?(name)
+ @row[name] = values.fetch(@row[name], @row[name])
+ end
+ end
+ end
+
+ def resolve_sti_reflections
+ # If STI is used, find the correct subclass for association reflection
+ reflection_class._reflections.each_value do |association|
+ case association.macro
+ when :belongs_to
+ # Do not replace association name with association foreign key if they are named the same
+ fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s
+
+ if association.name.to_s != fk_name && value = @row.delete(association.name.to_s)
+ if association.polymorphic? && value.sub!(/\s*\(([^\)]*)\)\s*$/, "")
+ # support polymorphic belongs_to as "label (Type)"
+ @row[association.foreign_type] = $1
+ end
+
+ fk_type = reflection_class.type_for_attribute(fk_name).type
+ @row[fk_name] = ActiveRecord::FixtureSet.identify(value, fk_type)
+ end
+ when :has_many
+ if association.options[:through]
+ add_join_records(HasManyThroughProxy.new(association))
+ end
+ end
+ end
+ end
+
+ def add_join_records(association)
+ # This is the case when the join table has no fixtures file
+ if (targets = @row.delete(association.name.to_s))
+ table_name = association.join_table
+ column_type = association.primary_key_type
+ lhs_key = association.lhs_key
+ rhs_key = association.rhs_key
+
+ targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/)
+ joins = targets.map do |target|
+ { lhs_key => @row[model_metadata.primary_key_name],
+ rhs_key => ActiveRecord::FixtureSet.identify(target, column_type) }
+ end
+ @table_rows.tables[table_name].concat(joins)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/fixture_set/table_rows.rb b/activerecord/lib/active_record/fixture_set/table_rows.rb
new file mode 100644
index 0000000000..23814b6cb5
--- /dev/null
+++ b/activerecord/lib/active_record/fixture_set/table_rows.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require "active_record/fixture_set/table_row"
+require "active_record/fixture_set/model_metadata"
+
+module ActiveRecord
+ class FixtureSet
+ class TableRows # :nodoc:
+ def initialize(table_name, model_class:, fixtures:, config:)
+ @model_class = model_class
+
+ # track any join tables we need to insert later
+ @tables = Hash.new { |h, table| h[table] = [] }
+
+ # ensure this table is loaded before any HABTM associations
+ @tables[table_name] = nil
+
+ build_table_rows_from(table_name, fixtures, config)
+ end
+
+ attr_reader :tables, :model_class
+
+ def to_hash
+ @tables.transform_values { |rows| rows.map(&:to_hash) }
+ end
+
+ def model_metadata
+ @model_metadata ||= ModelMetadata.new(model_class)
+ end
+
+ private
+
+ def build_table_rows_from(table_name, fixtures, config)
+ now = config.default_timezone == :utc ? Time.now.utc : Time.now
+
+ @tables[table_name] = fixtures.map do |label, fixture|
+ TableRow.new(
+ fixture,
+ table_rows: self,
+ label: label,
+ now: now,
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index d062dd9e34..1248ed00c5 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -1,11 +1,16 @@
-require 'erb'
-require 'yaml'
-require 'zlib'
-require 'set'
-require 'active_support/dependencies'
-require 'active_support/core_ext/digest/uuid'
-require 'active_record/fixture_set/file'
-require 'active_record/errors'
+# frozen_string_literal: true
+
+require "erb"
+require "yaml"
+require "zlib"
+require "set"
+require "active_support/dependencies"
+require "active_support/core_ext/digest/uuid"
+require "active_record/fixture_set/file"
+require "active_record/fixture_set/render_context"
+require "active_record/fixture_set/table_rows"
+require "active_record/test_fixtures"
+require "active_record/errors"
module ActiveRecord
class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc:
@@ -66,17 +71,36 @@ module ActiveRecord
# By default, +test_helper.rb+ will load all of your fixtures into your test
# database, so this test will succeed.
#
- # The testing environment will automatically load the all fixtures into the database before each
+ # The testing environment will automatically load all the fixtures into the database before each
# test. To ensure consistent data, the environment deletes the fixtures before running the load.
#
# In addition to being available in the database, the fixture's data may also be accessed by
- # using a special dynamic method, which has the same name as the model, and accepts the
- # name of the fixture to instantiate:
+ # using a special dynamic method, which has the same name as the model.
#
- # test "find" do
+ # Passing in a fixture name to this dynamic method returns the fixture matching this name:
+ #
+ # test "find one" do
# assert_equal "Ruby on Rails", web_sites(:rubyonrails).name
# end
#
+ # Passing in multiple fixture names returns all fixtures matching these names:
+ #
+ # test "find all by name" do
+ # assert_equal 2, web_sites(:rubyonrails, :google).length
+ # end
+ #
+ # Passing in no arguments returns all fixtures:
+ #
+ # test "find all" do
+ # assert_equal 2, web_sites.length
+ # end
+ #
+ # Passing in any fixture name that does not exist will raise <tt>StandardError</tt>:
+ #
+ # test "find by name that does not exist" do
+ # assert_raise(StandardError) { web_sites(:reddit) }
+ # end
+ #
# Alternatively, you may enable auto-instantiation of the fixture data. For instance, take the
# following tests:
#
@@ -88,8 +112,8 @@ module ActiveRecord
# assert_equal "Ruby on Rails", @rubyonrails.name
# end
#
- # In order to use these methods to access fixtured data within your testcases, you must specify one of the
- # following in your <tt>ActiveSupport::TestCase</tt>-derived class:
+ # In order to use these methods to access fixtured data within your test cases, you must specify one of the
+ # following in your ActiveSupport::TestCase-derived class:
#
# - to fully enable instantiated fixtures (enable alternate methods #1 and #2 above)
# self.use_instantiated_fixtures = true
@@ -103,7 +127,7 @@ module ActiveRecord
#
# = Dynamic fixtures with ERB
#
- # Some times you don't care about the content of the fixtures as much as you care about the volume.
+ # Sometimes you don't care about the content of the fixtures as much as you care about the volume.
# In these cases, you can mix ERB in with your YAML fixtures to create a bunch of fixtures for load
# testing, like:
#
@@ -124,9 +148,9 @@ module ActiveRecord
#
# Helper methods defined in a fixture will not be available in other fixtures, to prevent against
# unwanted inter-test dependencies. Methods used by multiple fixtures should be defined in a module
- # that is included in <tt>ActiveRecord::FixtureSet.context_class</tt>.
+ # that is included in ActiveRecord::FixtureSet.context_class.
#
- # - define a helper method in `test_helper.rb`
+ # - define a helper method in <tt>test_helper.rb</tt>
# module FixtureFileHelpers
# def file_sha(path)
# Digest::SHA2.hexdigest(File.read(Rails.root.join('test/fixtures', path)))
@@ -148,18 +172,18 @@ module ActiveRecord
# self.use_transactional_tests = true
#
# test "godzilla" do
- # assert !Foo.all.empty?
+ # assert_not_empty Foo.all
# Foo.destroy_all
- # assert Foo.all.empty?
+ # assert_empty Foo.all
# end
#
# test "godzilla aftermath" do
- # assert !Foo.all.empty?
+ # assert_not_empty Foo.all
# end
# end
#
- # If you preload your test database with all fixture data (probably in the rake task) and use
- # transactional tests, then you may omit all fixtures declarations in your test cases since
+ # If you preload your test database with all fixture data (probably by running `rails db:fixtures:load`)
+ # and use transactional tests, then you may omit all fixtures declarations in your test cases since
# all the data's already there and every case rolls back its changes.
#
# In order to use instantiated fixtures with preloaded data, set +self.pre_loaded_fixtures+ to
@@ -395,54 +419,105 @@ module ActiveRecord
# <<: *DEFAULTS
#
# Any fixture labeled "DEFAULTS" is safely ignored.
+ #
+ # == Configure the fixture model class
+ #
+ # It's possible to set the fixture's model class directly in the YAML file.
+ # This is helpful when fixtures are loaded outside tests and
+ # +set_fixture_class+ is not available (e.g.
+ # when running <tt>rails db:fixtures:load</tt>).
+ #
+ # _fixture:
+ # model_class: User
+ # david:
+ # name: David
+ #
+ # Any fixtures labeled "_fixture" are safely ignored.
class FixtureSet
#--
# An instance of FixtureSet is normally stored in a single YAML file and
# possibly in a folder with the same name.
#++
- MAX_ID = 2 ** 30 - 1
+ MAX_ID = 2**30 - 1
- @@all_cached_fixtures = Hash.new { |h,k| h[k] = {} }
+ @@all_cached_fixtures = Hash.new { |h, k| h[k] = {} }
- def self.default_fixture_model_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc:
- config.pluralize_table_names ?
- fixture_set_name.singularize.camelize :
- fixture_set_name.camelize
- end
+ cattr_accessor :all_loaded_fixtures, default: {}
- def self.default_fixture_table_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc:
- "#{ config.table_name_prefix }"\
- "#{ fixture_set_name.tr('/', '_') }"\
- "#{ config.table_name_suffix }".to_sym
- end
+ class ClassCache
+ def initialize(class_names, config)
+ @class_names = class_names.stringify_keys
+ @config = config
- def self.reset_cache
- @@all_cached_fixtures.clear
- end
+ # Remove string values that aren't constants or subclasses of AR
+ @class_names.delete_if do |klass_name, klass|
+ !insert_class(@class_names, klass_name, klass)
+ end
+ end
- def self.cache_for_connection(connection)
- @@all_cached_fixtures[connection]
- end
+ def [](fs_name)
+ @class_names.fetch(fs_name) do
+ klass = default_fixture_model(fs_name, @config).safe_constantize
+ insert_class(@class_names, fs_name, klass)
+ end
+ end
+
+ private
+
+ def insert_class(class_names, name, klass)
+ # We only want to deal with AR objects.
+ if klass && klass < ActiveRecord::Base
+ class_names[name] = klass
+ else
+ class_names[name] = nil
+ end
+ end
- def self.fixture_is_cached?(connection, table_name)
- cache_for_connection(connection)[table_name]
+ def default_fixture_model(fs_name, config)
+ ActiveRecord::FixtureSet.default_fixture_model_name(fs_name, config)
+ end
end
- def self.cached_fixtures(connection, keys_to_fetch = nil)
- if keys_to_fetch
- cache_for_connection(connection).values_at(*keys_to_fetch)
- else
- cache_for_connection(connection).values
+ class << self
+ def default_fixture_model_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc:
+ config.pluralize_table_names ?
+ fixture_set_name.singularize.camelize :
+ fixture_set_name.camelize
end
- end
- def self.cache_fixtures(connection, fixtures_map)
- cache_for_connection(connection).update(fixtures_map)
- end
+ def default_fixture_table_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc:
+ "#{ config.table_name_prefix }"\
+ "#{ fixture_set_name.tr('/', '_') }"\
+ "#{ config.table_name_suffix }".to_sym
+ end
- def self.instantiate_fixtures(object, fixture_set, load_instances = true)
- if load_instances
+ def reset_cache
+ @@all_cached_fixtures.clear
+ end
+
+ def cache_for_connection(connection)
+ @@all_cached_fixtures[connection]
+ end
+
+ def fixture_is_cached?(connection, table_name)
+ cache_for_connection(connection)[table_name]
+ end
+
+ def cached_fixtures(connection, keys_to_fetch = nil)
+ if keys_to_fetch
+ cache_for_connection(connection).values_at(*keys_to_fetch)
+ else
+ cache_for_connection(connection).values
+ end
+ end
+
+ def cache_fixtures(connection, fixtures_map)
+ cache_for_connection(connection).update(fixtures_map)
+ end
+
+ def instantiate_fixtures(object, fixture_set, load_instances = true)
+ return unless load_instances
fixture_set.each do |fixture_name, fixture|
begin
object.instance_variable_set "@#{fixture_name}", fixture.find
@@ -451,155 +526,117 @@ module ActiveRecord
end
end
end
- end
- def self.instantiate_all_loaded_fixtures(object, load_instances = true)
- all_loaded_fixtures.each_value do |fixture_set|
- instantiate_fixtures(object, fixture_set, load_instances)
+ def instantiate_all_loaded_fixtures(object, load_instances = true)
+ all_loaded_fixtures.each_value do |fixture_set|
+ instantiate_fixtures(object, fixture_set, load_instances)
+ end
end
- end
- cattr_accessor :all_loaded_fixtures
- self.all_loaded_fixtures = {}
+ def create_fixtures(fixtures_directory, fixture_set_names, class_names = {}, config = ActiveRecord::Base)
+ fixture_set_names = Array(fixture_set_names).map(&:to_s)
+ class_names = ClassCache.new class_names, config
- class ClassCache
- def initialize(class_names, config)
- @class_names = class_names.stringify_keys
- @config = config
+ # FIXME: Apparently JK uses this.
+ connection = block_given? ? yield : ActiveRecord::Base.connection
- # Remove string values that aren't constants or subclasses of AR
- @class_names.delete_if { |klass_name, klass| !insert_class(@class_names, klass_name, klass) }
- end
+ fixture_files_to_read = fixture_set_names.reject do |fs_name|
+ fixture_is_cached?(connection, fs_name)
+ end
- def [](fs_name)
- @class_names.fetch(fs_name) {
- klass = default_fixture_model(fs_name, @config).safe_constantize
- insert_class(@class_names, fs_name, klass)
- }
+ if fixture_files_to_read.any?
+ fixtures_map = read_and_insert(
+ fixtures_directory,
+ fixture_files_to_read,
+ class_names,
+ connection,
+ )
+ cache_fixtures(connection, fixtures_map)
+ end
+ cached_fixtures(connection, fixture_set_names)
end
- private
-
- def insert_class(class_names, name, klass)
- # We only want to deal with AR objects.
- if klass && klass < ActiveRecord::Base
- class_names[name] = klass
+ # Returns a consistent, platform-independent identifier for +label+.
+ # Integer identifiers are values less than 2^30. UUIDs are RFC 4122 version 5 SHA-1 hashes.
+ def identify(label, column_type = :integer)
+ if column_type == :uuid
+ Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, label.to_s)
else
- class_names[name] = nil
+ Zlib.crc32(label.to_s) % MAX_ID
end
end
- def default_fixture_model(fs_name, config)
- ActiveRecord::FixtureSet.default_fixture_model_name(fs_name, config)
+ # Superclass for the evaluation contexts used by ERB fixtures.
+ def context_class
+ @context_class ||= Class.new
end
- end
-
- def self.create_fixtures(fixtures_directory, fixture_set_names, class_names = {}, config = ActiveRecord::Base)
- fixture_set_names = Array(fixture_set_names).map(&:to_s)
- class_names = ClassCache.new class_names, config
-
- # FIXME: Apparently JK uses this.
- connection = block_given? ? yield : ActiveRecord::Base.connection
- files_to_read = fixture_set_names.reject { |fs_name|
- fixture_is_cached?(connection, fs_name)
- }
-
- unless files_to_read.empty?
- connection.disable_referential_integrity do
- fixtures_map = {}
+ private
- fixture_sets = files_to_read.map do |fs_name|
- klass = class_names[fs_name]
- conn = klass ? klass.connection : connection
- fixtures_map[fs_name] = new( # ActiveRecord::FixtureSet.new
- conn,
- fs_name,
- klass,
- ::File.join(fixtures_directory, fs_name))
- end
+ def read_and_insert(fixtures_directory, fixture_files, class_names, connection) # :nodoc:
+ fixtures_map = {}
+ fixture_sets = fixture_files.map do |fixture_set_name|
+ klass = class_names[fixture_set_name]
+ fixtures_map[fixture_set_name] = new( # ActiveRecord::FixtureSet.new
+ nil,
+ fixture_set_name,
+ klass,
+ ::File.join(fixtures_directory, fixture_set_name)
+ )
+ end
+ update_all_loaded_fixtures(fixtures_map)
- update_all_loaded_fixtures fixtures_map
+ insert(fixture_sets, connection)
- connection.transaction(:requires_new => true) do
- deleted_tables = Set.new
- fixture_sets.each do |fs|
- conn = fs.model_class.respond_to?(:connection) ? fs.model_class.connection : connection
- table_rows = fs.table_rows
+ fixtures_map
+ end
- table_rows.each_key do |table|
- unless deleted_tables.include? table
- conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete'
- end
- deleted_tables << table
- end
+ def insert(fixture_sets, connection) # :nodoc:
+ fixture_sets_by_connection = fixture_sets.group_by do |fixture_set|
+ fixture_set.model_class&.connection || connection
+ end
- table_rows.each do |fixture_set_name, rows|
- rows.each do |row|
- conn.insert_fixture(row, fixture_set_name)
- end
- end
+ fixture_sets_by_connection.each do |conn, set|
+ table_rows_for_connection = Hash.new { |h, k| h[k] = [] }
- # Cap primary key sequences to max(pk).
- if conn.respond_to?(:reset_pk_sequence!)
- conn.reset_pk_sequence!(fs.table_name)
- end
+ set.each do |fixture_set|
+ fixture_set.table_rows.each do |table, rows|
+ table_rows_for_connection[table].unshift(*rows)
end
end
+ conn.insert_fixtures_set(table_rows_for_connection, table_rows_for_connection.keys)
- cache_fixtures(connection, fixtures_map)
+ # Cap primary key sequences to max(pk).
+ if conn.respond_to?(:reset_pk_sequence!)
+ set.each { |fs| conn.reset_pk_sequence!(fs.table_name) }
+ end
end
end
- cached_fixtures(connection, fixture_set_names)
- end
- # Returns a consistent, platform-independent identifier for +label+.
- # Integer identifiers are values less than 2^30. UUIDs are RFC 4122 version 5 SHA-1 hashes.
- def self.identify(label, column_type = :integer)
- if column_type == :uuid
- Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, label.to_s)
- else
- Zlib.crc32(label.to_s) % MAX_ID
+ def update_all_loaded_fixtures(fixtures_map) # :nodoc:
+ all_loaded_fixtures.update(fixtures_map)
end
end
- # Superclass for the evaluation contexts used by ERB fixtures.
- def self.context_class
- @context_class ||= Class.new
- end
-
- def self.update_all_loaded_fixtures(fixtures_map) # :nodoc:
- all_loaded_fixtures.update(fixtures_map)
- end
-
attr_reader :table_name, :name, :fixtures, :model_class, :config
- def initialize(connection, name, class_name, path, config = ActiveRecord::Base)
+ def initialize(_, name, class_name, path, config = ActiveRecord::Base)
@name = name
@path = path
@config = config
- @model_class = nil
- if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any?
- @model_class = class_name
- else
- @model_class = class_name.safe_constantize if class_name
- end
-
- @connection = connection
+ self.model_class = class_name
- @table_name = ( model_class.respond_to?(:table_name) ?
- model_class.table_name :
- self.class.default_fixture_table_name(name, config) )
+ @fixtures = read_fixture_files(path)
- @fixtures = read_fixture_files path, @model_class
+ @table_name = model_class&.table_name || self.class.default_fixture_table_name(name, config)
end
def [](x)
fixtures[x]
end
- def []=(k,v)
+ def []=(k, v)
fixtures[k] = v
end
@@ -614,160 +651,38 @@ module ActiveRecord
# Returns a hash of rows to be inserted. The key is the table, the value is
# a list of rows to insert to that table.
def table_rows
- now = config.default_timezone == :utc ? Time.now.utc : Time.now
-
# allow a standard key to be used for doing defaults in YAML
- fixtures.delete('DEFAULTS')
-
- # track any join tables we need to insert later
- rows = Hash.new { |h,table| h[table] = [] }
-
- rows[table_name] = fixtures.map do |label, fixture|
- row = fixture.to_hash
-
- if model_class
- # fill in timestamp columns if they aren't specified and the model is set to record_timestamps
- if model_class.record_timestamps
- timestamp_column_names.each do |c_name|
- row[c_name] = now unless row.key?(c_name)
- end
- end
-
- # interpolate the fixture label
- row.each do |key, value|
- row[key] = value.gsub("$LABEL", label.to_s) if value.is_a?(String)
- end
-
- # generate a primary key if necessary
- if has_primary_key_column? && !row.include?(primary_key_name)
- row[primary_key_name] = ActiveRecord::FixtureSet.identify(label, primary_key_type)
- end
-
- # Resolve enums
- model_class.defined_enums.each do |name, values|
- if row.include?(name)
- row[name] = values.fetch(row[name], row[name])
- end
- end
-
- # If STI is used, find the correct subclass for association reflection
- reflection_class =
- if row.include?(inheritance_column_name)
- row[inheritance_column_name].constantize rescue model_class
- else
- model_class
- end
-
- reflection_class._reflections.each_value do |association|
- case association.macro
- when :belongs_to
- # Do not replace association name with association foreign key if they are named the same
- fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s
-
- if association.name.to_s != fk_name && value = row.delete(association.name.to_s)
- if association.polymorphic? && value.sub!(/\s*\(([^\)]*)\)\s*$/, "")
- # support polymorphic belongs_to as "label (Type)"
- row[association.foreign_type] = $1
- end
-
- fk_type = reflection_class.type_for_attribute(fk_name).type
- row[fk_name] = ActiveRecord::FixtureSet.identify(value, fk_type)
- end
- when :has_many
- if association.options[:through]
- add_join_records(rows, row, HasManyThroughProxy.new(association))
- end
- end
- end
- end
+ fixtures.delete("DEFAULTS")
- row
- end
- rows
- end
-
- class ReflectionProxy # :nodoc:
- def initialize(association)
- @association = association
- end
-
- def join_table
- @association.join_table
- end
-
- def name
- @association.name
- end
-
- def primary_key_type
- @association.klass.type_for_attribute(@association.klass.primary_key).type
- end
- end
-
- class HasManyThroughProxy < ReflectionProxy # :nodoc:
- def rhs_key
- @association.foreign_key
- end
-
- def lhs_key
- @association.through_reflection.foreign_key
- end
-
- def join_table
- @association.through_reflection.table_name
- end
+ TableRows.new(
+ table_name,
+ model_class: model_class,
+ fixtures: fixtures,
+ config: config,
+ ).to_hash
end
private
- def primary_key_name
- @primary_key_name ||= model_class && model_class.primary_key
- end
-
- def primary_key_type
- @primary_key_type ||= model_class && model_class.type_for_attribute(model_class.primary_key).type
- end
-
- def add_join_records(rows, row, association)
- # This is the case when the join table has no fixtures file
- if (targets = row.delete(association.name.to_s))
- table_name = association.join_table
- column_type = association.primary_key_type
- lhs_key = association.lhs_key
- rhs_key = association.rhs_key
- targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/)
- rows[table_name].concat targets.map { |target|
- { lhs_key => row[primary_key_name],
- rhs_key => ActiveRecord::FixtureSet.identify(target, column_type) }
- }
+ def model_class=(class_name)
+ if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any?
+ @model_class = class_name
+ else
+ @model_class = class_name.safe_constantize if class_name
end
end
- def has_primary_key_column?
- @has_primary_key_column ||= primary_key_name &&
- model_class.columns.any? { |c| c.name == primary_key_name }
- end
-
- def timestamp_column_names
- @timestamp_column_names ||=
- %w(created_at created_on updated_at updated_on) & column_names
- end
-
- def inheritance_column_name
- @inheritance_column_name ||= model_class && model_class.inheritance_column
- end
-
- def column_names
- @column_names ||= @connection.columns(@table_name).collect(&:name)
- end
-
- def read_fixture_files(path, model_class)
+ # Loads the fixtures from the YAML file at +path+.
+ # If the file sets the +model_class+ and current instance value is not set,
+ # it uses the file value.
+ def read_fixture_files(path)
yaml_files = Dir["#{path}/{**,*}/*.yml"].select { |f|
::File.file?(f)
} + [yaml_file_path(path)]
yaml_files.each_with_object({}) do |file, fixtures|
FixtureSet::File.open(file) do |fh|
+ self.model_class ||= fh.model_class if fh.model_class
fh.each do |fixture_name, row|
fixtures[fixture_name] = ActiveRecord::Fixture.new(row, model_class)
end
@@ -778,7 +693,6 @@ module ActiveRecord
def yaml_file_path(path)
"#{path}.yml"
end
-
end
class Fixture #:nodoc:
@@ -812,214 +726,9 @@ module ActiveRecord
alias :to_hash :fixture
def find
- if model_class
- model_class.unscoped do
- model_class.find(fixture[model_class.primary_key])
- end
- else
- raise FixtureClassNotFound, "No class attached to find."
- end
- end
- end
-end
-
-module ActiveRecord
- module TestFixtures
- extend ActiveSupport::Concern
-
- def before_setup
- setup_fixtures
- super
- end
-
- def after_teardown
- super
- teardown_fixtures
- end
-
- included do
- class_attribute :fixture_path, :instance_writer => false
- 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 = Hash.new do |h, fixture_set_name|
- h[fixture_set_name] = ActiveRecord::FixtureSet.default_fixture_model_name(fixture_set_name, self.config)
- end
-
- silence_warnings do
- define_singleton_method :use_transactional_tests do
- if use_transactional_fixtures.nil?
- true
- else
- use_transactional_fixtures
- end
- end
- end
- end
-
- module ClassMethods
- # Sets the model class for a fixture when the class name cannot be inferred from the fixture name.
- #
- # Examples:
- #
- # set_fixture_class some_fixture: SomeModel,
- # 'namespaced/fixture' => Another::Model
- #
- # 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)
- end
-
- def fixtures(*fixture_set_names)
- if fixture_set_names.first == :all
- fixture_set_names = Dir["#{fixture_path}/{**,*}/*.{yml}"]
- fixture_set_names.map! { |f| f[(fixture_path.to_s.size + 1)..-5] }
- else
- fixture_set_names = fixture_set_names.flatten.map(&:to_s)
- end
-
- self.fixture_table_names |= fixture_set_names
- setup_fixture_accessors(fixture_set_names)
- end
-
- def setup_fixture_accessors(fixture_set_names = nil)
- fixture_set_names = Array(fixture_set_names || fixture_table_names)
- methods = Module.new do
- fixture_set_names.each do |fs_name|
- fs_name = fs_name.to_s
- accessor_name = fs_name.tr('/', '_').to_sym
-
- define_method(accessor_name) do |*fixture_names|
- force_reload = fixture_names.pop if fixture_names.last == true || fixture_names.last == :reload
-
- @fixture_cache[fs_name] ||= {}
-
- instances = fixture_names.map do |f_name|
- f_name = f_name.to_s if f_name.is_a?(Symbol)
- @fixture_cache[fs_name].delete(f_name) if force_reload
-
- if @loaded_fixtures[fs_name][f_name]
- @fixture_cache[fs_name][f_name] ||= @loaded_fixtures[fs_name][f_name].find
- else
- raise StandardError, "No fixture named '#{f_name}' found for fixture set '#{fs_name}'"
- end
- end
-
- instances.size == 1 ? instances.first : instances
- end
- private accessor_name
- end
- end
- include methods
- end
-
- def uses_transaction(*methods)
- @uses_transaction = [] unless defined?(@uses_transaction)
- @uses_transaction.concat methods.map(&:to_s)
- end
-
- def uses_transaction?(method)
- @uses_transaction = [] unless defined?(@uses_transaction)
- @uses_transaction.include?(method.to_s)
- end
- end
-
- def run_in_transaction?
- use_transactional_tests &&
- !self.class.uses_transaction?(method_name)
- end
-
- def setup_fixtures(config = ActiveRecord::Base)
- if pre_loaded_fixtures && !use_transactional_tests
- raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_tests'
- end
-
- @fixture_cache = {}
- @fixture_connections = []
- @@already_loaded_fixtures ||= {}
-
- # Load fixtures once and begin transaction.
- if run_in_transaction?
- if @@already_loaded_fixtures[self.class]
- @loaded_fixtures = @@already_loaded_fixtures[self.class]
- else
- @loaded_fixtures = load_fixtures(config)
- @@already_loaded_fixtures[self.class] = @loaded_fixtures
- end
- @fixture_connections = enlist_fixture_connections
- @fixture_connections.each do |connection|
- connection.begin_transaction joinable: false
- end
- # Load fixtures for every test.
- else
- ActiveRecord::FixtureSet.reset_cache
- @@already_loaded_fixtures[self.class] = nil
- @loaded_fixtures = load_fixtures(config)
- end
-
- # Instantiate fixtures for every test if requested.
- instantiate_fixtures if use_instantiated_fixtures
- end
-
- def teardown_fixtures
- # Rollback changes if a transaction is active.
- if run_in_transaction?
- @fixture_connections.each do |connection|
- connection.rollback_transaction if connection.transaction_open?
- end
- @fixture_connections.clear
- else
- ActiveRecord::FixtureSet.reset_cache
- end
-
- ActiveRecord::Base.clear_active_connections!
- end
-
- def enlist_fixture_connections
- ActiveRecord::Base.connection_handler.connection_pool_list.map(&:connection)
- end
-
- private
- def load_fixtures(config)
- fixtures = ActiveRecord::FixtureSet.create_fixtures(fixture_path, fixture_table_names, fixture_class_names, config)
- Hash[fixtures.map { |f| [f.name, f] }]
- end
-
- def instantiate_fixtures
- if pre_loaded_fixtures
- raise RuntimeError, 'Load fixtures before instantiating them.' if ActiveRecord::FixtureSet.all_loaded_fixtures.empty?
- ActiveRecord::FixtureSet.instantiate_all_loaded_fixtures(self, load_instances?)
- else
- raise RuntimeError, 'Load fixtures before instantiating them.' if @loaded_fixtures.nil?
- @loaded_fixtures.each_value do |fixture_set|
- ActiveRecord::FixtureSet.instantiate_fixtures(self, fixture_set, load_instances?)
- end
- end
- end
-
- def load_instances?
- use_instantiated_fixtures != :no_instances
- end
- end
-end
-
-class ActiveRecord::FixtureSet::RenderContext # :nodoc:
- def self.create_subclass
- Class.new ActiveRecord::FixtureSet.context_class do
- def get_binding
- binding()
+ raise FixtureClassNotFound, "No class attached to find." unless model_class
+ model_class.unscoped do
+ model_class.find(fixture[model_class.primary_key])
end
end
end
diff --git a/activerecord/lib/active_record/gem_version.rb b/activerecord/lib/active_record/gem_version.rb
index a388b529c9..72035a986b 100644
--- a/activerecord/lib/active_record/gem_version.rb
+++ b/activerecord/lib/active_record/gem_version.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
# Returns the version of the currently loaded Active Record as a <tt>Gem::Version</tt>
def self.gem_version
@@ -5,7 +7,7 @@ module ActiveRecord
end
module VERSION
- MAJOR = 5
+ MAJOR = 6
MINOR = 0
TINY = 0
PRE = "alpha"
diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb
index e613d157aa..138fd1cf53 100644
--- a/activerecord/lib/active_record/inheritance.rb
+++ b/activerecord/lib/active_record/inheritance.rb
@@ -1,4 +1,6 @@
-require 'active_support/core_ext/hash/indifferent_access'
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/indifferent_access"
module ActiveRecord
# == Single table inheritance
@@ -19,7 +21,7 @@ module ActiveRecord
# Be aware that because the type column is an attribute on the record every new
# subclass will instantly be marked as dirty and the type column will be included
# in the list of changed attributes on the record. This is different from non
- # STI classes:
+ # Single Table Inheritance(STI) classes:
#
# Company.new.changed? # => false
# Firm.new.changed? # => true
@@ -30,33 +32,40 @@ module ActiveRecord
# for differentiating between them or reloading the right type with find.
#
# Note, all the attributes for all the cases are kept in the same table. Read more:
- # http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html
+ # https://www.martinfowler.com/eaaCatalog/singleTableInheritance.html
#
module Inheritance
extend ActiveSupport::Concern
included do
# Determines whether to store the full constant name including namespace when using STI.
- class_attribute :store_full_sti_class, instance_writer: false
- self.store_full_sti_class = true
+ # This is true, by default.
+ class_attribute :store_full_sti_class, instance_writer: false, default: true
end
module ClassMethods
# Determines if one of the attributes passed in is the inheritance column,
# and if the inheritance column is attr accessible, it initializes an
# instance of the given subclass instead of the base class.
- def new(*args, &block)
+ def new(attributes = nil, &block)
if abstract_class? || self == Base
raise NotImplementedError, "#{self} is an abstract class and cannot be instantiated."
end
- attrs = args.first
- if subclass_from_attributes?(attrs)
- subclass = subclass_from_attributes(attrs)
+ if has_attribute?(inheritance_column)
+ subclass = subclass_from_attributes(attributes)
+
+ if subclass.nil? && scope_attributes = current_scope&.scope_for_create
+ subclass = subclass_from_attributes(scope_attributes)
+ end
+
+ if subclass.nil? && base_class?
+ subclass = subclass_from_attributes(column_defaults)
+ end
end
- if subclass
- subclass.new(*args, &block)
+ if subclass && subclass != self
+ subclass.new(attributes, &block)
else
super
end
@@ -82,7 +91,7 @@ module ActiveRecord
# Returns the class descending directly from ActiveRecord::Base, or
# an abstract class, if any, in the inheritance hierarchy.
#
- # If A extends AR::Base, A.base_class will return A. If B descends from A
+ # If A extends ActiveRecord::Base, A.base_class will return A. If B descends from A
# through some arbitrarily deep hierarchy, B.base_class will return A.
#
# If B < A and C < B and if A is an abstract_class then both B.base_class
@@ -99,21 +108,53 @@ module ActiveRecord
end
end
- # Set this to true if this is an abstract class (see <tt>abstract_class?</tt>).
- # If you are using inheritance with ActiveRecord and don't want child classes
- # to utilize the implied STI table name of the parent class, this will need to be true.
- # For example, given the following:
+ # Returns whether the class is a base class.
+ # See #base_class for more information.
+ def base_class?
+ base_class == self
+ end
+
+ # Set this to +true+ if this is an abstract class (see
+ # <tt>abstract_class?</tt>).
+ # If you are using inheritance with Active Record and don't want a class
+ # to be considered as part of the STI hierarchy, you must set this to
+ # true.
+ # +ApplicationRecord+, for example, is generated as an abstract class.
#
- # class SuperClass < ActiveRecord::Base
+ # Consider the following default behaviour:
+ #
+ # Shape = Class.new(ActiveRecord::Base)
+ # Polygon = Class.new(Shape)
+ # Square = Class.new(Polygon)
+ #
+ # Shape.table_name # => "shapes"
+ # Polygon.table_name # => "shapes"
+ # Square.table_name # => "shapes"
+ # Shape.create! # => #<Shape id: 1, type: nil>
+ # Polygon.create! # => #<Polygon id: 2, type: "Polygon">
+ # Square.create! # => #<Square id: 3, type: "Square">
+ #
+ # However, when using <tt>abstract_class</tt>, +Shape+ is omitted from
+ # the hierarchy:
+ #
+ # class Shape < ActiveRecord::Base
# self.abstract_class = true
# end
- # class Child < SuperClass
- # self.table_name = 'the_table_i_really_want'
- # end
- #
+ # Polygon = Class.new(Shape)
+ # Square = Class.new(Polygon)
#
- # <tt>self.abstract_class = true</tt> is required to make <tt>Child<.find,.create, or any Arel method></tt> use <tt>the_table_i_really_want</tt> instead of a table called <tt>super_classes</tt>
+ # Shape.table_name # => nil
+ # Polygon.table_name # => "polygons"
+ # Square.table_name # => "polygons"
+ # Shape.create! # => NotImplementedError: Shape is an abstract class and cannot be instantiated.
+ # Polygon.create! # => #<Polygon id: 1, type: nil>
+ # Square.create! # => #<Square id: 2, type: "Square">
#
+ # Note that in the above example, to disallow the creation of a plain
+ # +Polygon+, you should use <tt>validates :type, presence: true</tt>,
+ # instead of setting it as an abstract class. This way, +Polygon+ will
+ # stay in the hierarchy, and Active Record will continue to correctly
+ # derive the table name.
attr_accessor :abstract_class
# Returns whether this class is an abstract class or not.
@@ -125,91 +166,104 @@ module ActiveRecord
store_full_sti_class ? name : name.demodulize
end
+ def polymorphic_name
+ base_class.name
+ end
+
+ def inherited(subclass)
+ subclass.instance_variable_set(:@_type_candidates_cache, Concurrent::Map.new)
+ super
+ 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 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
- # Build a list of candidates to search for
- candidates = []
- name.scan(/::|$/) { candidates.unshift "#{$`}::#{type_name}" }
- candidates << type_name
-
- candidates.each do |candidate|
- constant = ActiveSupport::Dependencies.safe_constantize(candidate)
- return constant if candidate == constant.to_s
- end
+ # 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.start_with?("::")
+ # 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
- raise NameError.new("uninitialized constant #{candidates.first}", candidates.first)
- end
- end
+ # Build a list of candidates to search for
+ candidates = []
+ name.scan(/::|$/) { candidates.unshift "#{$`}::#{type_name}" }
+ candidates << type_name
- private
+ candidates.each do |candidate|
+ constant = ActiveSupport::Dependencies.safe_constantize(candidate)
+ if candidate == constant.to_s
+ @_type_candidates_cache[type_name] = candidate
+ return constant
+ end
+ end
- # Called by +instantiate+ to decide which class to use for a new
- # record instance. For single-table inheritance, we check the record
- # for a +type+ column and return the corresponding class.
- def discriminate_class_for_record(record)
- if using_single_table_inheritance?(record)
- find_sti_class(record[inheritance_column])
- else
- super
+ raise NameError.new("uninitialized constant #{candidates.first}", candidates.first)
+ end
end
- end
- def using_single_table_inheritance?(record)
- record[inheritance_column].present? && columns_hash.include?(inheritance_column)
- end
+ private
- def find_sti_class(type_name)
- if store_full_sti_class
- ActiveSupport::Dependencies.constantize(type_name)
- else
- compute_type(type_name)
+ # Called by +instantiate+ to decide which class to use for a new
+ # record instance. For single-table inheritance, we check the record
+ # for a +type+ column and return the corresponding class.
+ def discriminate_class_for_record(record)
+ if using_single_table_inheritance?(record)
+ find_sti_class(record[inheritance_column])
+ else
+ super
+ end
end
- rescue NameError
- raise SubclassNotFound,
- "The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " +
- "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " +
- "Please rename this column if you didn't intend it to be used for storing the inheritance class " +
- "or overwrite #{name}.inheritance_column to use another column for that information."
- end
- def type_condition(table = arel_table)
- sti_column = table[inheritance_column]
- sti_names = ([self] + descendants).map(&:sti_name)
+ def using_single_table_inheritance?(record)
+ record[inheritance_column].present? && has_attribute?(inheritance_column)
+ end
- sti_column.in(sti_names)
- end
+ def find_sti_class(type_name)
+ type_name = base_class.type_for_attribute(inheritance_column).cast(type_name)
+ subclass = begin
+ if store_full_sti_class
+ ActiveSupport::Dependencies.constantize(type_name)
+ else
+ compute_type(type_name)
+ end
+ rescue NameError
+ raise SubclassNotFound,
+ "The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " \
+ "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " \
+ "Please rename this column if you didn't intend it to be used for storing the inheritance class " \
+ "or overwrite #{name}.inheritance_column to use another column for that information."
+ end
+ unless subclass == self || descendants.include?(subclass)
+ raise SubclassNotFound, "Invalid single-table inheritance type: #{subclass.name} is not a subclass of #{name}"
+ end
+ subclass
+ end
- # Detect the subclass from the inheritance column of attrs. If the inheritance column value
- # is not self or a valid subclass, raises ActiveRecord::SubclassNotFound
- # If this is a StrongParameters hash, and access to inheritance_column is not permitted,
- # this will ignore the inheritance column and return nil
- def subclass_from_attributes?(attrs)
- attribute_names.include?(inheritance_column) && attrs.is_a?(Hash)
- end
+ def type_condition(table = arel_table)
+ sti_column = arel_attribute(inheritance_column, table)
+ sti_names = ([self] + descendants).map(&:sti_name)
- def subclass_from_attributes(attrs)
- subclass_name = attrs.with_indifferent_access[inheritance_column]
+ sti_column.in(sti_names)
+ end
- if subclass_name.present?
- subclass = find_sti_class(subclass_name)
+ # Detect the subclass from the inheritance column of attrs. If the inheritance column value
+ # is not self or a valid subclass, raises ActiveRecord::SubclassNotFound
+ def subclass_from_attributes(attrs)
+ attrs = attrs.to_h if attrs.respond_to?(:permitted?)
+ if attrs.is_a?(Hash)
+ subclass_name = attrs[inheritance_column] || attrs[inheritance_column.to_sym]
- if subclass.name != self.name
- unless descendants.include?(subclass)
- raise ActiveRecord::SubclassNotFound.new("Invalid single-table inheritance type: #{subclass.name} is not a subclass of #{name}")
+ if subclass_name.present?
+ find_sti_class(subclass_name)
end
-
- subclass
end
end
- end
end
def initialize_dup(other)
@@ -219,21 +273,21 @@ module ActiveRecord
private
- def initialize_internals_callback
- super
- ensure_proper_type
- end
+ def initialize_internals_callback
+ super
+ ensure_proper_type
+ end
- # Sets the attribute used for single table inheritance to this class name if this is not the
- # ActiveRecord::Base descendant.
- # Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to
- # do Reply.new without having to set <tt>Reply[Reply.inheritance_column] = "Reply"</tt> yourself.
- # No such attribute would be set for objects of the Message class in that example.
- def ensure_proper_type
- klass = self.class
- if klass.finder_needs_type_condition?
- write_attribute(klass.inheritance_column, klass.sti_name)
+ # Sets the attribute used for single table inheritance to this class name if this is not the
+ # ActiveRecord::Base descendant.
+ # Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to
+ # do Reply.new without having to set <tt>Reply[Reply.inheritance_column] = "Reply"</tt> yourself.
+ # No such attribute would be set for objects of the Message class in that example.
+ def ensure_proper_type
+ klass = self.class
+ if klass.finder_needs_type_condition?
+ _write_attribute(klass.inheritance_column, klass.sti_name)
+ end
end
- end
end
end
diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb
index 15b2f65dcb..33c4066b89 100644
--- a/activerecord/lib/active_record/integration.rb
+++ b/activerecord/lib/active_record/integration.rb
@@ -1,4 +1,6 @@
-require 'active_support/core_ext/string/filters'
+# frozen_string_literal: true
+
+require "active_support/core_ext/string/filters"
module ActiveRecord
module Integration
@@ -7,17 +9,24 @@ module ActiveRecord
included do
##
# :singleton-method:
- # Indicates the format used to generate the timestamp in the cache key.
- # Accepts any of the symbols in <tt>Time::DATE_FORMATS</tt>.
+ # Indicates the format used to generate the timestamp in the cache key, if
+ # versioning is off. Accepts any of the symbols in <tt>Time::DATE_FORMATS</tt>.
+ #
+ # This is +:usec+, by default.
+ class_attribute :cache_timestamp_format, instance_writer: false, default: :usec
+
+ ##
+ # :singleton-method:
+ # Indicates whether to use a stable #cache_key method that is accompanied
+ # by a changing version in the #cache_version method.
#
- # This is +:nsec+, by default.
- class_attribute :cache_timestamp_format, :instance_writer => false
- self.cache_timestamp_format = :nsec
+ # This is +true+, by default on Rails 5.2 and above.
+ class_attribute :cache_versioning, instance_writer: false, default: false
end
- # Returns a String, which Action Pack uses for constructing an URL to this
- # object. The default implementation returns this record's id as a String,
- # or nil if this record's unsaved.
+ # Returns a +String+, which Action Pack uses for constructing a URL to this
+ # object. The default implementation returns this record's id as a +String+,
+ # or +nil+ if this record's unsaved.
#
# For example, suppose that you have a User model, and that you have a
# <tt>resources :users</tt> route. Normally, +user_path+ will
@@ -42,29 +51,62 @@ module ActiveRecord
id && id.to_s # Be sure to stringify the id for routes
end
- # Returns a cache key that can be used to identify this record.
+ # Returns a stable cache key that can be used to identify this record.
#
# Product.new.cache_key # => "products/new"
- # Product.find(5).cache_key # => "products/5" (updated_at not available)
- # Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available)
+ # Product.find(5).cache_key # => "products/5"
#
- # You can also pass a list of named timestamps, and the newest in the list will be
- # used to generate the key:
+ # If ActiveRecord::Base.cache_versioning is turned off, as it was in Rails 5.1 and earlier,
+ # the cache key will also include a version.
#
- # Person.find(5).cache_key(:updated_at, :last_reviewed_at)
+ # Product.cache_versioning = false
+ # Product.find(5).cache_key # => "products/5-20071224150000" (updated_at available)
def cache_key(*timestamp_names)
- case
- when new_record?
+ if new_record?
"#{model_name.cache_key}/new"
- when timestamp_names.any?
- timestamp = max_updated_column_timestamp(timestamp_names)
- timestamp = timestamp.utc.to_s(cache_timestamp_format)
- "#{model_name.cache_key}/#{id}-#{timestamp}"
- when timestamp = max_updated_column_timestamp
- timestamp = timestamp.utc.to_s(cache_timestamp_format)
- "#{model_name.cache_key}/#{id}-#{timestamp}"
else
- "#{model_name.cache_key}/#{id}"
+ if cache_version && timestamp_names.none?
+ "#{model_name.cache_key}/#{id}"
+ else
+ timestamp = if timestamp_names.any?
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Specifying a timestamp name for #cache_key has been deprecated in favor of
+ the explicit #cache_version method that can be overwritten.
+ MSG
+
+ max_updated_column_timestamp(timestamp_names)
+ else
+ max_updated_column_timestamp
+ end
+
+ if timestamp
+ timestamp = timestamp.utc.to_s(cache_timestamp_format)
+ "#{model_name.cache_key}/#{id}-#{timestamp}"
+ else
+ "#{model_name.cache_key}/#{id}"
+ end
+ end
+ end
+ end
+
+ # Returns a cache version that can be used together with the cache key to form
+ # a recyclable caching scheme. By default, the #updated_at column is used for the
+ # cache_version, but this method can be overwritten to return something else.
+ #
+ # Note, this method will return nil if ActiveRecord::Base.cache_versioning is set to
+ # +false+ (which it is by default until Rails 6.0).
+ def cache_version
+ if cache_versioning && timestamp = try(:updated_at)
+ timestamp.utc.to_s(:usec)
+ end
+ end
+
+ # Returns a cache key along with the version.
+ def cache_key_with_version
+ if version = cache_version
+ "#{cache_key}-#{version}"
+ else
+ cache_key
end
end
@@ -84,9 +126,9 @@ module ActiveRecord
# Values longer than 20 characters will be truncated. The value
# is truncated word by word.
#
- # user = User.find_by(name: 'David HeinemeierHansson')
+ # user = User.find_by(name: 'David Heinemeier Hansson')
# user.id # => 125
- # user_path(user) # => "/users/125-david"
+ # user_path(user) # => "/users/125-david-heinemeier"
#
# Because the generated param begins with the record's +id+, it is
# suitable for passing to +find+. In a controller, for example:
@@ -100,7 +142,7 @@ module ActiveRecord
define_method :to_param do
if (default = super()) &&
(result = send(method_name).to_s).present? &&
- (param = result.squish.truncate(20, separator: /\s/, omission: nil).parameterize).present?
+ (param = result.squish.parameterize.truncate(20, separator: /-/, omission: "")).present?
"#{default}-#{param}"
else
default
diff --git a/activerecord/lib/active_record/internal_metadata.rb b/activerecord/lib/active_record/internal_metadata.rb
new file mode 100644
index 0000000000..3626a13d7c
--- /dev/null
+++ b/activerecord/lib/active_record/internal_metadata.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require "active_record/scoping/default"
+require "active_record/scoping/named"
+
+module ActiveRecord
+ # This class is used to create a table that keeps track of values and keys such
+ # as which environment migrations were run in.
+ class InternalMetadata < ActiveRecord::Base # :nodoc:
+ class << self
+ def primary_key
+ "key"
+ end
+
+ def table_name
+ "#{table_name_prefix}#{ActiveRecord::Base.internal_metadata_table_name}#{table_name_suffix}"
+ end
+
+ def []=(key, value)
+ find_or_initialize_by(key: key).update!(value: value)
+ end
+
+ def [](key)
+ where(key: key).pluck(:value).first
+ end
+
+ def table_exists?
+ connection.table_exists?(table_name)
+ end
+
+ # Creates an internal metadata table with columns +key+ and +value+
+ def create_table
+ unless table_exists?
+ key_options = connection.internal_string_options_for_primary_key
+
+ connection.create_table(table_name, id: false) do |t|
+ t.string :key, key_options
+ t.string :value
+ t.timestamps
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/legacy_yaml_adapter.rb b/activerecord/lib/active_record/legacy_yaml_adapter.rb
index 89dee58423..ffa095dd94 100644
--- a/activerecord/lib/active_record/legacy_yaml_adapter.rb
+++ b/activerecord/lib/active_record/legacy_yaml_adapter.rb
@@ -1,12 +1,14 @@
+# frozen_string_literal: true
+
module ActiveRecord
module LegacyYamlAdapter
def self.convert(klass, coder)
return coder unless coder.is_a?(Psych::Coder)
case coder["active_record_yaml_version"]
- when 1 then coder
+ when 1, 2 then coder
else
- if coder["attributes"].is_a?(AttributeSet)
+ if coder["attributes"].is_a?(ActiveModel::AttributeSet)
Rails420.convert(klass, coder)
else
Rails41.convert(klass, coder)
diff --git a/activerecord/lib/active_record/locale/en.yml b/activerecord/lib/active_record/locale/en.yml
index 8a3c27e6da..0b35027b2b 100644
--- a/activerecord/lib/active_record/locale/en.yml
+++ b/activerecord/lib/active_record/locale/en.yml
@@ -16,8 +16,8 @@ en:
messages:
record_invalid: "Validation failed: %{errors}"
restrict_dependent_destroy:
- one: "Cannot delete record because a dependent %{record} exists"
- many: "Cannot delete record because dependent %{record} exist"
+ has_one: "Cannot delete record because a dependent %{record} exists"
+ has_many: "Cannot delete record because dependent %{record} exist"
# Append your own errors here or at the model/attributes scope.
# You can define own errors for models or model attributes.
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index a09437b4b0..4a3a31fc95 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Locking
# == What is Optimistic Locking
@@ -22,7 +24,7 @@ module ActiveRecord
# p1.save
#
# p2.first_name = "should fail"
- # p2.save # Raises a ActiveRecord::StaleObjectError
+ # p2.save # Raises an ActiveRecord::StaleObjectError
#
# Optimistic locking will also check for stale data when objects are destroyed. Example:
#
@@ -32,7 +34,7 @@ module ActiveRecord
# p1.first_name = "Michael"
# p1.save
#
- # p2.destroy # Raises a ActiveRecord::StaleObjectError
+ # p2.destroy # Raises an ActiveRecord::StaleObjectError
#
# You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging,
# or otherwise apply the business logic needed to resolve the conflict.
@@ -51,8 +53,7 @@ module ActiveRecord
extend ActiveSupport::Concern
included do
- class_attribute :lock_optimistically, instance_writer: false
- self.lock_optimistically = true
+ class_attribute :lock_optimistically, instance_writer: false, default: true
end
def locking_enabled? #:nodoc:
@@ -60,13 +61,7 @@ module ActiveRecord
end
private
- def increment_lock
- lock_col = self.class.locking_column
- previous_lock_value = send(lock_col).to_i
- 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
@@ -75,127 +70,128 @@ module ActiveRecord
super
end
- def _update_record(attribute_names = self.attribute_names) #:nodoc:
+ def _touch_row(attribute_names, time)
+ super
+ ensure
+ clear_attribute_change(self.class.locking_column) if locking_enabled?
+ end
+
+ def _update_row(attribute_names, attempted_action = "update")
return super unless locking_enabled?
- return 0 if attribute_names.empty?
- lock_col = self.class.locking_column
- previous_lock_value = send(lock_col).to_i
- increment_lock
+ begin
+ locking_column = self.class.locking_column
+ previous_lock_value = read_attribute_before_type_cast(locking_column)
+ attribute_names << locking_column
- attribute_names += [lock_col]
- attribute_names.uniq!
+ self[locking_column] += 1
- begin
- relation = self.class.unscoped
-
- affected_rows = relation.where(
- self.class.primary_key => id,
- lock_col => previous_lock_value,
- ).update_all(
- attributes_for_update(attribute_names).map do |name|
- [name, _read_attribute(name)]
- end.to_h
+ affected_rows = self.class._update_record(
+ attributes_with_values(attribute_names),
+ self.class.primary_key => id_in_database,
+ locking_column => previous_lock_value
)
- unless affected_rows == 1
- raise ActiveRecord::StaleObjectError.new(self, "update")
+ if affected_rows != 1
+ raise ActiveRecord::StaleObjectError.new(self, attempted_action)
end
affected_rows
- # If something went wrong, revert the version.
+ # If something went wrong, revert the locking_column value.
rescue Exception
- send(lock_col + '=', previous_lock_value)
+ self[locking_column] = previous_lock_value.to_i
raise
end
end
def destroy_row
- affected_rows = super
+ return super unless locking_enabled?
+
+ locking_column = self.class.locking_column
- if locking_enabled? && affected_rows != 1
+ affected_rows = self.class._delete_record(
+ self.class.primary_key => id_in_database,
+ locking_column => read_attribute_before_type_cast(locking_column)
+ )
+
+ if affected_rows != 1
raise ActiveRecord::StaleObjectError.new(self, "destroy")
end
affected_rows
end
- def relation_for_destroy
- relation = super
+ module ClassMethods
+ DEFAULT_LOCKING_COLUMN = "lock_version"
- if locking_enabled?
- locking_column = self.class.locking_column
- relation = relation.where(locking_column => _read_attribute(locking_column))
+ # Returns true if the +lock_optimistically+ flag is set to true
+ # (which it is, by default) and the table includes the
+ # +locking_column+ column (defaults to +lock_version+).
+ def locking_enabled?
+ lock_optimistically && columns_hash[locking_column]
end
- relation
- end
-
- module ClassMethods
- DEFAULT_LOCKING_COLUMN = 'lock_version'
-
- # Returns true if the +lock_optimistically+ flag is set to true
- # (which it is, by default) and the table includes the
- # +locking_column+ column (defaults to +lock_version+).
- def locking_enabled?
- lock_optimistically && columns_hash[locking_column]
- end
-
- # Set the column to use for optimistic locking. Defaults to +lock_version+.
- def locking_column=(value)
- reload_schema_from_cache
- @locking_column = value.to_s
- end
+ # Set the column to use for optimistic locking. Defaults to +lock_version+.
+ def locking_column=(value)
+ reload_schema_from_cache
+ @locking_column = value.to_s
+ end
- # The version column used for optimistic locking. Defaults to +lock_version+.
- def locking_column
- reset_locking_column unless defined?(@locking_column)
- @locking_column
- end
+ # The version column used for optimistic locking. Defaults to +lock_version+.
+ def locking_column
+ @locking_column = DEFAULT_LOCKING_COLUMN unless defined?(@locking_column)
+ @locking_column
+ end
- # Reset the column used for optimistic locking back to the +lock_version+ default.
- def reset_locking_column
- self.locking_column = DEFAULT_LOCKING_COLUMN
- end
+ # Reset the column used for optimistic locking back to the +lock_version+ default.
+ def reset_locking_column
+ self.locking_column = DEFAULT_LOCKING_COLUMN
+ end
- # Make sure the lock version column gets updated when counters are
- # updated.
- def update_counters(id, counters)
- counters = counters.merge(locking_column => 1) if locking_enabled?
- super
- end
+ # Make sure the lock version column gets updated when counters are
+ # updated.
+ def update_counters(id, counters)
+ counters = counters.merge(locking_column => 1) if locking_enabled?
+ super
+ end
- private
-
- # We need to apply this decorator here, rather than on module inclusion. The closure
- # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the
- # sub class being decorated. As such, changes to `lock_optimistically`, or
- # `locking_column` would not be picked up.
- def inherited(subclass)
- subclass.class_eval do
- is_lock_column = ->(name, _) { lock_optimistically && name == locking_column }
- decorate_matching_attribute_types(is_lock_column, :_optimistic_locking) do |type|
- LockingType.new(type)
+ private
+
+ # We need to apply this decorator here, rather than on module inclusion. The closure
+ # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the
+ # sub class being decorated. As such, changes to `lock_optimistically`, or
+ # `locking_column` would not be picked up.
+ def inherited(subclass)
+ subclass.class_eval do
+ is_lock_column = ->(name, _) { lock_optimistically && name == locking_column }
+ decorate_matching_attribute_types(is_lock_column, "_optimistic_locking") do |type|
+ LockingType.new(type)
+ end
+ end
+ super
end
- end
- super
end
- end
end
+ # In de/serialize we change `nil` to 0, so that we can allow passing
+ # `nil` values to `lock_version`, and not result in `ActiveRecord::StaleObjectError`
+ # during update record.
class LockingType < DelegateClass(Type::Value) # :nodoc:
def deserialize(value)
- # `nil` *should* be changed to 0
+ super.to_i
+ end
+
+ def serialize(value)
super.to_i
end
def init_with(coder)
- __setobj__(coder['subtype'])
+ __setobj__(coder["subtype"])
end
def encode_with(coder)
- coder['subtype'] = __getobj__
+ coder["subtype"] = __getobj__
end
end
end
diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb
index 3d95c54ef3..130ef8a330 100644
--- a/activerecord/lib/active_record/locking/pessimistic.rb
+++ b/activerecord/lib/active_record/locking/pessimistic.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Locking
# Locking::Pessimistic provides support for row-level locking using
@@ -12,9 +14,9 @@ module ActiveRecord
# of your own such as 'LOCK IN SHARE MODE' or 'FOR UPDATE NOWAIT'. Example:
#
# Account.transaction do
- # # select * from accounts where name = 'shugo' limit 1 for update
- # shugo = Account.where("name = 'shugo'").lock(true).first
- # yuko = Account.where("name = 'yuko'").lock(true).first
+ # # select * from accounts where name = 'shugo' limit 1 for update nowait
+ # shugo = Account.lock("FOR UPDATE NOWAIT").find_by(name: "shugo")
+ # yuko = Account.lock("FOR UPDATE NOWAIT").find_by(name: "yuko")
# shugo.balance -= 100
# shugo.save!
# yuko.balance += 100
@@ -51,15 +53,25 @@ module ActiveRecord
# end
#
# Database-specific information on row locking:
- # MySQL: http://dev.mysql.com/doc/refman/5.6/en/innodb-locking-reads.html
- # PostgreSQL: http://www.postgresql.org/docs/current/interactive/sql-select.html#SQL-FOR-UPDATE-SHARE
+ # MySQL: https://dev.mysql.com/doc/refman/5.7/en/innodb-locking-reads.html
+ # PostgreSQL: https://www.postgresql.org/docs/current/interactive/sql-select.html#SQL-FOR-UPDATE-SHARE
module Pessimistic
# Obtain a row lock on this record. Reloads the record to obtain the requested
# lock. Pass an SQL locking clause to append the end of the SELECT statement
# 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 has_changes_to_save?
+ raise(<<-MSG.squish)
+ Locking a record with unpersisted changes is not supported. Use
+ `save` to persist the changes, or `reload` to discard them
+ explicitly.
+ MSG
+ end
+
+ reload(lock: lock)
+ end
self
end
diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb
index 4d597a0ab1..6b84431343 100644
--- a/activerecord/lib/active_record/log_subscriber.rb
+++ b/activerecord/lib/active_record/log_subscriber.rb
@@ -1,7 +1,11 @@
+# frozen_string_literal: true
+
module ActiveRecord
class LogSubscriber < ActiveSupport::LogSubscriber
IGNORE_PAYLOAD_NAMES = ["SCHEMA", "EXPLAIN"]
+ class_attribute :backtrace_cleaner, default: ActiveSupport::BacktraceCleaner.new
+
def self.runtime=(value)
ActiveRecord::RuntimeRegistry.sql_runtime = value
end
@@ -15,58 +19,99 @@ module ActiveRecord
rt
end
- def initialize
- super
- @odd = false
- end
-
- def render_bind(attribute)
- value = if attribute.type.binary? && attribute.value
- "<#{attribute.value.bytesize} bytes of binary data>"
- else
- attribute.value_for_database
- end
-
- [attribute.name, value]
- end
-
def sql(event)
- return unless logger.debug?
-
self.class.runtime += event.duration
+ return unless logger.debug?
payload = event.payload
return if IGNORE_PAYLOAD_NAMES.include?(payload[:name])
name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
+ name = "CACHE #{name}" if payload[:cached]
sql = payload[:sql]
binds = nil
unless (payload[:binds] || []).empty?
- binds = " " + payload[:binds].map { |attr| render_bind(attr) }.inspect
+ casted_params = type_casted_binds(payload[:type_casted_binds])
+ binds = " " + payload[:binds].zip(casted_params).map { |attr, value|
+ render_bind(attr, value)
+ }.inspect
end
- name = color(name, nil, true)
+ name = colorize_payload_name(name, payload[:name])
sql = color(sql, sql_color(sql), true)
debug " #{name} #{sql}#{binds}"
end
- def sql_color(sql)
- case sql
- when /\s*\Ainsert/i then GREEN
- when /\s*\Aselect/i then BLUE
- when /\s*\Aupdate/i then YELLOW
- when /\s*\Adelete/i then RED
- when /transaction\s*\Z/i then CYAN
- else MAGENTA
+ private
+ def type_casted_binds(casted_binds)
+ casted_binds.respond_to?(:call) ? casted_binds.call : casted_binds
end
- end
- def logger
- ActiveRecord::Base.logger
- end
+ 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 && attr.name, value]
+ end
+
+ def colorize_payload_name(name, payload_name)
+ if payload_name.blank? || payload_name == "SQL" # SQL vs Model Load/Exists
+ color(name, MAGENTA, true)
+ else
+ color(name, CYAN, true)
+ end
+ end
+
+ def sql_color(sql)
+ case sql
+ when /\A\s*rollback/mi
+ RED
+ when /select .*for update/mi, /\A\s*lock/mi
+ WHITE
+ when /\A\s*select/i
+ BLUE
+ when /\A\s*insert/i
+ GREEN
+ when /\A\s*update/i
+ YELLOW
+ when /\A\s*delete/i
+ RED
+ when /transaction\s*\Z/i
+ CYAN
+ else
+ MAGENTA
+ end
+ end
+
+ def logger
+ ActiveRecord::Base.logger
+ end
+
+ def debug(progname = nil, &block)
+ return unless super
+
+ if ActiveRecord::Base.verbose_query_logs
+ log_query_source
+ end
+ end
+
+ def log_query_source
+ source = extract_query_source_location(caller)
+
+ if source
+ logger.debug(" ↳ #{source}")
+ end
+ end
+
+ def extract_query_source_location(locations)
+ backtrace_cleaner.clean(locations).first
+ end
end
end
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index 4cfda302ea..6e5a610642 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -1,5 +1,9 @@
+# frozen_string_literal: true
+
+require "benchmark"
+require "set"
+require "zlib"
require "active_support/core_ext/module/attribute_accessors"
-require 'set'
module ActiveRecord
class MigrationError < ActiveRecordError#:nodoc:
@@ -9,40 +13,171 @@ module ActiveRecord
end
end
- # Exception that can be raised to stop migrations from going backwards.
+ # Exception that can be raised to stop migrations from being rolled back.
+ # For example the following migration is not reversible.
+ # Rolling back this migration will raise an ActiveRecord::IrreversibleMigration error.
+ #
+ # class IrreversibleMigrationExample < ActiveRecord::Migration[5.0]
+ # def change
+ # create_table :distributors do |t|
+ # t.string :zipcode
+ # end
+ #
+ # execute <<-SQL
+ # ALTER TABLE distributors
+ # ADD CONSTRAINT zipchk
+ # CHECK (char_length(zipcode) = 5) NO INHERIT;
+ # SQL
+ # end
+ # end
+ #
+ # There are two ways to mitigate this problem.
+ #
+ # 1. Define <tt>#up</tt> and <tt>#down</tt> methods instead of <tt>#change</tt>:
+ #
+ # class ReversibleMigrationExample < ActiveRecord::Migration[5.0]
+ # def up
+ # create_table :distributors do |t|
+ # t.string :zipcode
+ # end
+ #
+ # execute <<-SQL
+ # ALTER TABLE distributors
+ # ADD CONSTRAINT zipchk
+ # CHECK (char_length(zipcode) = 5) NO INHERIT;
+ # SQL
+ # end
+ #
+ # def down
+ # execute <<-SQL
+ # ALTER TABLE distributors
+ # DROP CONSTRAINT zipchk
+ # SQL
+ #
+ # drop_table :distributors
+ # end
+ # end
+ #
+ # 2. Use the #reversible method in <tt>#change</tt> method:
+ #
+ # class ReversibleMigrationExample < ActiveRecord::Migration[5.0]
+ # def change
+ # create_table :distributors do |t|
+ # t.string :zipcode
+ # end
+ #
+ # reversible do |dir|
+ # dir.up do
+ # execute <<-SQL
+ # ALTER TABLE distributors
+ # ADD CONSTRAINT zipchk
+ # CHECK (char_length(zipcode) = 5) NO INHERIT;
+ # SQL
+ # end
+ #
+ # dir.down do
+ # execute <<-SQL
+ # ALTER TABLE distributors
+ # DROP CONSTRAINT zipchk
+ # SQL
+ # end
+ # end
+ # end
+ # end
class IrreversibleMigration < MigrationError
end
class DuplicateMigrationVersionError < MigrationError#:nodoc:
- def initialize(version)
- super("Multiple migrations have the version number #{version}")
+ def initialize(version = nil)
+ if version
+ super("Multiple migrations have the version number #{version}.")
+ else
+ super("Duplicate migration version error.")
+ end
end
end
class DuplicateMigrationNameError < MigrationError#:nodoc:
- def initialize(name)
- super("Multiple migrations have the name #{name}")
+ def initialize(name = nil)
+ if name
+ super("Multiple migrations have the name #{name}.")
+ else
+ super("Duplicate migration name.")
+ end
end
end
class UnknownMigrationVersionError < MigrationError #:nodoc:
- def initialize(version)
- super("No migration with version number #{version}")
+ def initialize(version = nil)
+ if version
+ super("No migration with version number #{version}.")
+ else
+ super("Unknown migration version.")
+ end
end
end
class IllegalMigrationNameError < MigrationError#:nodoc:
- def initialize(name)
- super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed)")
+ def initialize(name = nil)
+ if name
+ super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed).")
+ else
+ super("Illegal name for migration.")
+ end
end
end
class PendingMigrationError < MigrationError#:nodoc:
+ def initialize(message = nil)
+ if !message && defined?(Rails.env)
+ super("Migrations are pending. To resolve this issue, run:\n\n rails db:migrate RAILS_ENV=#{::Rails.env}")
+ elsif !message
+ super("Migrations are pending. To resolve this issue, run:\n\n rails db:migrate")
+ else
+ super
+ end
+ end
+ end
+
+ class ConcurrentMigrationError < MigrationError #:nodoc:
+ DEFAULT_MESSAGE = "Cannot run migrations because another migration process is currently running."
+ RELEASE_LOCK_FAILED_MESSAGE = "Failed to release advisory lock"
+
+ def initialize(message = DEFAULT_MESSAGE)
+ super
+ end
+ end
+
+ class NoEnvironmentInSchemaError < MigrationError #:nodoc:
def initialize
+ msg = "Environment data not found in the schema. To resolve this issue, run: \n\n rails db:environment:set"
+ if defined?(Rails.env)
+ super("#{msg} RAILS_ENV=#{::Rails.env}")
+ else
+ super(msg)
+ end
+ end
+ end
+
+ class ProtectedEnvironmentError < ActiveRecordError #:nodoc:
+ def initialize(env = "production")
+ msg = +"You are attempting to run a destructive action against your '#{env}' database.\n"
+ msg << "If you are sure you want to continue, run the same command with the environment variable:\n"
+ msg << "DISABLE_DATABASE_ENVIRONMENT_CHECK=1"
+ super(msg)
+ end
+ end
+
+ class EnvironmentMismatchError < ActiveRecordError
+ def initialize(current: nil, stored: nil)
+ msg = +"You are attempting to modify a database that was last run in `#{ stored }` environment.\n"
+ msg << "You are running in `#{ current }` environment. "
+ msg << "If you are sure you want to continue, first set the environment using:\n\n"
+ msg << " rails db:environment:set"
if defined?(Rails.env)
- super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate RAILS_ENV=#{::Rails.env}")
+ super("#{msg} RAILS_ENV=#{::Rails.env}\n\n")
else
- super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate")
+ super("#{msg}\n\n")
end
end
end
@@ -59,7 +194,7 @@ module ActiveRecord
#
# Example of a simple migration:
#
- # class AddSsl < ActiveRecord::Migration
+ # class AddSsl < ActiveRecord::Migration[5.0]
# def up
# add_column :accounts, :ssl_enabled, :boolean, default: true
# end
@@ -79,7 +214,7 @@ module ActiveRecord
#
# Example of a more complex migration that also needs to initialize data:
#
- # class AddSystemSettings < ActiveRecord::Migration
+ # class AddSystemSettings < ActiveRecord::Migration[5.0]
# def up
# create_table :system_settings do |t|
# t.string :name
@@ -106,17 +241,18 @@ module ActiveRecord
#
# == Available transformations
#
+ # === Creation
+ #
+ # * <tt>create_join_table(table_1, table_2, options)</tt>: Creates a join
+ # table having its name as the lexical order of the first two
+ # arguments. See
+ # ActiveRecord::ConnectionAdapters::SchemaStatements#create_join_table for
+ # details.
# * <tt>create_table(name, options)</tt>: Creates a table called +name+ and
# makes the table object available to a block that can then add columns to it,
# following the same format as +add_column+. See example above. The options hash
# is for fragments like "DEFAULT CHARSET=UTF-8" that are appended to the create
# table definition.
- # * <tt>drop_table(name)</tt>: Drops the table called +name+.
- # * <tt>change_table(name, options)</tt>: Allows to make column alterations to
- # the table called +name+. It makes the table object available to a block that
- # can then add/remove columns, indexes or foreign keys to it.
- # * <tt>rename_table(old_name, new_name)</tt>: Renames the table called +old_name+
- # to +new_name+.
# * <tt>add_column(table_name, column_name, type, options)</tt>: Adds a new column
# to the table called +table_name+
# named +column_name+ specified to be one of the following types:
@@ -127,24 +263,61 @@ module ActiveRecord
# Other options include <tt>:limit</tt> and <tt>:null</tt> (e.g.
# <tt>{ limit: 50, null: false }</tt>) -- see
# ActiveRecord::ConnectionAdapters::TableDefinition#column for details.
- # * <tt>rename_column(table_name, column_name, new_column_name)</tt>: Renames
- # a column but keeps the type and content.
- # * <tt>change_column(table_name, column_name, type, options)</tt>: Changes
- # the column to a different type using the same parameters as add_column.
- # * <tt>remove_column(table_name, column_name, type, options)</tt>: Removes the column
- # named +column_name+ from the table called +table_name+.
+ # * <tt>add_foreign_key(from_table, to_table, options)</tt>: Adds a new
+ # foreign key. +from_table+ is the table with the key column, +to_table+ contains
+ # the referenced primary key.
# * <tt>add_index(table_name, column_names, options)</tt>: Adds a new index
# with the name of the column. Other options include
# <tt>:name</tt>, <tt>:unique</tt> (e.g.
# <tt>{ name: 'users_name_index', unique: true }</tt>) and <tt>:order</tt>
# (e.g. <tt>{ order: { name: :desc } }</tt>).
+ # * <tt>add_reference(:table_name, :reference_name)</tt>: Adds a new column
+ # +reference_name_id+ by default an integer. See
+ # ActiveRecord::ConnectionAdapters::SchemaStatements#add_reference for details.
+ # * <tt>add_timestamps(table_name, options)</tt>: Adds timestamps (+created_at+
+ # and +updated_at+) columns to +table_name+.
+ #
+ # === Modification
+ #
+ # * <tt>change_column(table_name, column_name, type, options)</tt>: Changes
+ # the column to a different type using the same parameters as add_column.
+ # * <tt>change_column_default(table_name, column_name, default_or_changes)</tt>:
+ # Sets a default value for +column_name+ defined by +default_or_changes+ on
+ # +table_name+. Passing a hash containing <tt>:from</tt> and <tt>:to</tt>
+ # as +default_or_changes+ will make this change reversible in the migration.
+ # * <tt>change_column_null(table_name, column_name, null, default = nil)</tt>:
+ # Sets or removes a +NOT NULL+ constraint on +column_name+. The +null+ flag
+ # indicates whether the value can be +NULL+. See
+ # ActiveRecord::ConnectionAdapters::SchemaStatements#change_column_null for
+ # details.
+ # * <tt>change_table(name, options)</tt>: Allows to make column alterations to
+ # the table called +name+. It makes the table object available to a block that
+ # can then add/remove columns, indexes or foreign keys to it.
+ # * <tt>rename_column(table_name, column_name, new_column_name)</tt>: Renames
+ # a column but keeps the type and content.
+ # * <tt>rename_index(table_name, old_name, new_name)</tt>: Renames an index.
+ # * <tt>rename_table(old_name, new_name)</tt>: Renames the table called +old_name+
+ # to +new_name+.
+ #
+ # === Deletion
+ #
+ # * <tt>drop_table(name)</tt>: Drops the table called +name+.
+ # * <tt>drop_join_table(table_1, table_2, options)</tt>: Drops the join table
+ # specified by the given arguments.
+ # * <tt>remove_column(table_name, column_name, type, options)</tt>: Removes the column
+ # named +column_name+ from the table called +table_name+.
+ # * <tt>remove_columns(table_name, *column_names)</tt>: Removes the given
+ # columns from the table definition.
+ # * <tt>remove_foreign_key(from_table, options_or_to_table)</tt>: Removes the
+ # given foreign key from the table called +table_name+.
# * <tt>remove_index(table_name, column: column_names)</tt>: Removes the index
# specified by +column_names+.
# * <tt>remove_index(table_name, name: index_name)</tt>: Removes the index
# specified by +index_name+.
- # * <tt>add_reference(:table_name, :reference_name)</tt>: Adds a new column
- # +reference_name_id+ by default a integer. See
- # ActiveRecord::ConnectionAdapters::SchemaStatements#add_reference for details.
+ # * <tt>remove_reference(table_name, ref_name, options)</tt>: Removes the
+ # reference(s) on +table_name+ specified by +ref_name+.
+ # * <tt>remove_timestamps(table_name, options)</tt>: Removes the timestamp
+ # columns (+created_at+ and +updated_at+) from the table definition.
#
# == Irreversible transformations
#
@@ -168,24 +341,24 @@ module ActiveRecord
#
# rails generate migration add_fieldname_to_tablename fieldname:string
#
- # This will generate the file <tt>timestamp_add_fieldname_to_tablename</tt>, which will look like this:
- # class AddFieldnameToTablename < ActiveRecord::Migration
+ # This will generate the file <tt>timestamp_add_fieldname_to_tablename.rb</tt>, which will look like this:
+ # class AddFieldnameToTablename < ActiveRecord::Migration[5.0]
# def change
# add_column :tablenames, :fieldname, :string
# end
# end
#
# To run migrations against the currently configured database, use
- # <tt>rake db:migrate</tt>. This will update the database by running all of the
+ # <tt>rails db:migrate</tt>. This will update the database by running all of the
# pending migrations, creating the <tt>schema_migrations</tt> table
# (see "About the schema_migrations table" section below) if missing. It will also
- # invoke the db:schema:dump task, which will update your db/schema.rb file
+ # invoke the db:schema:dump command, which will update your db/schema.rb file
# to match the structure of your database.
#
# To roll the database back to a previous migration version, use
- # <tt>rake db:migrate VERSION=X</tt> where <tt>X</tt> is the version to which
+ # <tt>rails db:rollback VERSION=X</tt> where <tt>X</tt> is the version to which
# you wish to downgrade. Alternatively, you can also use the STEP option if you
- # wish to rollback last few migrations. <tt>rake db:migrate STEP=2</tt> will rollback
+ # wish to rollback last few migrations. <tt>rails db:rollback STEP=2</tt> will rollback
# the latest two migrations.
#
# If any of the migrations throw an <tt>ActiveRecord::IrreversibleMigration</tt> exception,
@@ -200,7 +373,7 @@ module ActiveRecord
#
# Not all migrations change the schema. Some just fix the data:
#
- # class RemoveEmptyTags < ActiveRecord::Migration
+ # class RemoveEmptyTags < ActiveRecord::Migration[5.0]
# def up
# Tag.all.each { |tag| tag.destroy if tag.pages.empty? }
# end
@@ -213,7 +386,7 @@ module ActiveRecord
#
# Others remove columns when they migrate up instead of down:
#
- # class RemoveUnnecessaryItemAttributes < ActiveRecord::Migration
+ # class RemoveUnnecessaryItemAttributes < ActiveRecord::Migration[5.0]
# def up
# remove_column :items, :incomplete_items_count
# remove_column :items, :completed_items_count
@@ -227,7 +400,7 @@ module ActiveRecord
#
# And sometimes you need to do something in SQL not abstracted directly by migrations:
#
- # class MakeJoinUnique < ActiveRecord::Migration
+ # class MakeJoinUnique < ActiveRecord::Migration[5.0]
# def up
# execute "ALTER TABLE `pages_linked_pages` ADD UNIQUE `page_id_linked_page_id` (`page_id`,`linked_page_id`)"
# end
@@ -244,7 +417,7 @@ module ActiveRecord
# <tt>Base#reset_column_information</tt> in order to ensure that the model has the
# latest column data from after the new column was added. Example:
#
- # class AddPeopleSalary < ActiveRecord::Migration
+ # class AddPeopleSalary < ActiveRecord::Migration[5.0]
# def up
# add_column :people, :salary, :integer
# Person.reset_column_information
@@ -302,7 +475,7 @@ module ActiveRecord
# To define a reversible migration, define the +change+ method in your
# migration like this:
#
- # class TenderloveMigration < ActiveRecord::Migration
+ # class TenderloveMigration < ActiveRecord::Migration[5.0]
# def change
# create_table(:horses) do |t|
# t.column :content, :text
@@ -332,7 +505,7 @@ module ActiveRecord
# can't execute inside a transaction though, and for these situations
# you can turn the automatic transactions off.
#
- # class ChangeEnum < ActiveRecord::Migration
+ # class ChangeEnum < ActiveRecord::Migration[5.0]
# disable_ddl_transaction!
#
# def up
@@ -343,11 +516,35 @@ module ActiveRecord
# Remember that you can still open your own transactions, even if you
# are in a Migration with <tt>self.disable_ddl_transaction!</tt>.
class Migration
- autoload :CommandRecorder, 'active_record/migration/command_recorder'
+ autoload :CommandRecorder, "active_record/migration/command_recorder"
+ autoload :Compatibility, "active_record/migration/compatibility"
+
+ # This must be defined before the inherited hook, below
+ class Current < Migration # :nodoc:
+ end
+
+ def self.inherited(subclass) # :nodoc:
+ super
+ if subclass.superclass == Migration
+ 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
+
+ def self.[](version)
+ Compatibility.find(version)
+ end
+ def self.current_version
+ ActiveRecord::VERSION::STRING.to_f
+ end
+
+ MigrationFilenameRegexp = /\A([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/ # :nodoc:
# This class is used to verify that all migrations have been run before
- # loading a web page if config.active_record.migration_error is set to :page_load
+ # loading a web page if <tt>config.active_record.migration_error</tt> is set to :page_load
class CheckPending
def initialize(app)
@app = app
@@ -355,38 +552,42 @@ 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::Base.connection.migration_context.last_migration.mtime.to_i
+ if @last_check < mtime
+ ActiveRecord::Migration.check_pending!(connection)
+ @last_check = mtime
end
@app.call(env)
end
private
- def connection
- ActiveRecord::Base.connection
- end
+ def connection
+ ActiveRecord::Base.connection
+ end
end
class << self
attr_accessor :delegate # :nodoc:
attr_accessor :disable_ddl_transaction # :nodoc:
+ def nearest_delegate # :nodoc:
+ delegate || superclass.nearest_delegate
+ end
+
+ # Raises <tt>ActiveRecord::PendingMigrationError</tt> error if any migrations are pending.
def check_pending!(connection = Base.connection)
- raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration?(connection)
+ raise ActiveRecord::PendingMigrationError if connection.migration_context.needs_migration?
end
def load_schema_if_pending!
- if ActiveRecord::Migrator.needs_migration? || !ActiveRecord::Migrator.any_migrations?
+ if Base.connection.migration_context.needs_migration? || !Base.connection.migration_context.any_migrations?
# Roundtrip to Rake to allow plugins to hook into database initialization.
- FileUtils.cd Rails.root do
+ root = defined?(ENGINE_ROOT) ? ENGINE_ROOT : Rails.root
+ FileUtils.cd(root) do
current_config = Base.connection_config
Base.clear_all_connections!
- system("bin/rake db:test:prepare")
+ system("bin/rails db:test:prepare")
# Establish a new connection, the old database may be gone (db:test:prepare uses purge)
Base.establish_connection(current_config)
end
@@ -401,14 +602,17 @@ module ActiveRecord
end
def method_missing(name, *args, &block) # :nodoc:
- (delegate || superclass.delegate).send(name, *args, &block)
+ nearest_delegate.send(name, *args, &block)
end
def migrate(direction)
new.migrate direction
end
- # Disable DDL transactions for this migration.
+ # Disable the transaction wrapping this migration.
+ # You can still create your own transactions even after calling #disable_ddl_transaction!
+ #
+ # For more details read the {"Transactional Migrations" section above}[rdoc-ref:Migration].
def disable_ddl_transaction!
@disable_ddl_transaction = true
end
@@ -438,7 +642,7 @@ module ActiveRecord
# and create the table 'apples' on the way up, and the reverse
# on the way down.
#
- # class FixTLMigration < ActiveRecord::Migration
+ # class FixTLMigration < ActiveRecord::Migration[5.0]
# def change
# revert do
# create_table(:horses) do |t|
@@ -455,9 +659,9 @@ module ActiveRecord
# Or equivalently, if +TenderloveMigration+ is defined as in the
# documentation for Migration:
#
- # require_relative '2012121212_tenderlove_migration'
+ # require_relative '20121212123456_tenderlove_migration'
#
- # class FixupTLMigration < ActiveRecord::Migration
+ # class FixupTLMigration < ActiveRecord::Migration[5.0]
# def change
# revert TenderloveMigration
#
@@ -471,27 +675,25 @@ module ActiveRecord
def revert(*migration_classes)
run(*migration_classes.reverse, revert: true) unless migration_classes.empty?
if block_given?
- if @connection.respond_to? :revert
- @connection.revert { yield }
+ if connection.respond_to? :revert
+ connection.revert { yield }
else
- recorder = CommandRecorder.new(@connection)
+ recorder = command_recorder
@connection = recorder
suppress_messages do
- @connection.revert { yield }
+ connection.revert { yield }
end
@connection = recorder.delegate
- recorder.commands.each do |cmd, args, block|
- send(cmd, *args, &block)
- end
+ recorder.replay(self)
end
end
end
def reverting?
- @connection.respond_to?(:reverting) && @connection.reverting
+ 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
@@ -510,7 +712,7 @@ module ActiveRecord
# when the three columns 'first_name', 'last_name' and 'full_name' exist,
# even when migrating down:
#
- # class SplitNameMigration < ActiveRecord::Migration
+ # class SplitNameMigration < ActiveRecord::Migration[5.0]
# def change
# add_column :users, :first_name, :string
# add_column :users, :last_name, :string
@@ -529,7 +731,25 @@ module ActiveRecord
# end
def reversible
helper = ReversibleBlockHelper.new(reverting?)
- execute_block{ yield helper }
+ execute_block { yield helper }
+ end
+
+ # Used to specify an operation that is only run when migrating up
+ # (for example, populating a new column with its initial values).
+ #
+ # In the following example, the new column +published+ will be given
+ # the value +true+ for all existing records.
+ #
+ # class AddPublishedToPosts < ActiveRecord::Migration[5.2]
+ # def change
+ # add_column :posts, :published, :boolean, default: false
+ # up_only do
+ # execute "update posts set published = 'true'"
+ # end
+ # end
+ # end
+ def up_only
+ execute_block { yield } unless reverting?
end
# Runs the given migration classes.
@@ -545,7 +765,7 @@ module ActiveRecord
revert { run(*migration_classes, direction: dir, revert: true) }
else
migration_classes.each do |migration_class|
- migration_class.new.exec_migration(@connection, dir)
+ migration_class.new.exec_migration(connection, dir)
end
end
end
@@ -571,7 +791,7 @@ module ActiveRecord
when :down then announce "reverting"
end
- time = nil
+ time = nil
ActiveRecord::Base.connection_pool.with_connection do |conn|
time = Benchmark.measure do
exec_migration(conn, direction)
@@ -599,7 +819,7 @@ module ActiveRecord
@connection = nil
end
- def write(text="")
+ def write(text = "")
puts(text) if verbose
end
@@ -609,10 +829,14 @@ module ActiveRecord
write "== %s %s" % [text, "=" * length]
end
- def say(message, subitem=false)
+ # Takes a message argument and outputs it as is.
+ # A second boolean argument can be passed to specify whether to indent or not.
+ def say(message, subitem = false)
write "#{subitem ? " ->" : "--"} #{message}"
end
+ # Outputs text along with how long it took to run its block.
+ # If the block returns an integer it assumes it is the number of rows affected.
def say_with_time(message)
say(message)
result = nil
@@ -622,6 +846,7 @@ module ActiveRecord
result
end
+ # Takes a block as an argument and suppresses any output generated by the block.
def suppress_messages
save, self.verbose = verbose, false
yield
@@ -634,10 +859,10 @@ module ActiveRecord
end
def method_missing(method, *arguments, &block)
- arg_list = arguments.map(&:inspect) * ', '
+ arg_list = arguments.map(&:inspect) * ", "
say_with_time "#{method}(#{arg_list})" do
- unless @connection.respond_to? :revert
+ unless connection.respond_to? :revert
unless arguments.empty? || [:execute, :enable_extension, :disable_extension].include?(method)
arguments[0] = proper_table_name(arguments.first, table_name_options)
if [:rename_table, :add_foreign_key].include?(method) ||
@@ -656,23 +881,25 @@ module ActiveRecord
FileUtils.mkdir_p(destination) unless File.exist?(destination)
- destination_migrations = ActiveRecord::Migrator.migrations(destination)
+ destination_migrations = ActiveRecord::MigrationContext.new(destination).migrations
last = destination_migrations.last
sources.each do |scope, path|
- source_migrations = ActiveRecord::Migrator.migrations(path)
+ source_migrations = ActiveRecord::MigrationContext.new(path).migrations
source_migrations.each do |migration|
source = File.binread(migration.filename)
inserted_comment = "# This migration comes from #{scope} (originally #{migration.version})\n"
- if /\A#.*\b(?:en)?coding:\s*\S+/ =~ source
+ magic_comments = +""
+ loop do
# If we have a magic comment in the original migration,
# insert our comment after the first newline(end of the magic comment line)
# so the magic keep working.
# Note that magic comments must be at the first line(except sh-bang).
- source[/\n/] = "\n#{inserted_comment}"
- else
- source = "#{inserted_comment}#{source}"
+ source.sub!(/\A(?:#.*\b(?:en)?coding:\s*\S+|#\s*frozen_string_literal:\s*(?:true|false)).*\n/) do |magic_comment|
+ magic_comments << magic_comment; ""
+ end || break
end
+ source = "#{magic_comments}#{inserted_comment}#{source}"
if duplicate = destination_migrations.detect { |m| m.name == migration.name }
if options[:on_skip] && duplicate.scope != scope.to_s
@@ -716,7 +943,9 @@ module ActiveRecord
end
end
- def table_name_options(config = ActiveRecord::Base)
+ # Builds a hash for use in ActiveRecord::Migration#proper_table_name using
+ # the Active Record object's table_name prefix and suffix
+ def table_name_options(config = ActiveRecord::Base) #:nodoc:
{
table_name_prefix: config.table_name_prefix,
table_name_suffix: config.table_name_suffix
@@ -724,19 +953,22 @@ module ActiveRecord
end
private
- def execute_block
- if connection.respond_to? :execute_block
- super # use normal delegation to record the block
- else
- yield
+ def execute_block
+ if connection.respond_to? :execute_block
+ super # use normal delegation to record the block
+ else
+ yield
+ end
+ end
+
+ def command_recorder
+ CommandRecorder.new(connection)
end
- end
end
# 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
@@ -762,7 +994,6 @@ module ActiveRecord
require(File.expand_path(filename))
name.constantize.new(name, version)
end
-
end
class NullMigration < MigrationProxy #:nodoc:
@@ -775,131 +1006,185 @@ module ActiveRecord
end
end
- class Migrator#:nodoc:
- class << self
- attr_writer :migrations_paths
- alias :migrations_path= :migrations_paths=
-
- def migrate(migrations_paths, target_version = nil, &block)
- case
- when target_version.nil?
- up(migrations_paths, target_version, &block)
- when current_version == 0 && target_version == 0
- []
- when current_version > target_version
- down(migrations_paths, target_version, &block)
- else
- up(migrations_paths, target_version, &block)
- end
- end
+ class MigrationContext # :nodoc:
+ attr_reader :migrations_paths
- def rollback(migrations_paths, steps=1)
- move(:down, migrations_paths, steps)
- end
+ def initialize(migrations_paths)
+ @migrations_paths = migrations_paths
+ end
- def forward(migrations_paths, steps=1)
- move(:up, migrations_paths, steps)
+ def migrate(target_version = nil, &block)
+ case
+ when target_version.nil?
+ up(target_version, &block)
+ when current_version == 0 && target_version == 0
+ []
+ when current_version > target_version
+ down(target_version, &block)
+ else
+ up(target_version, &block)
end
+ end
- def up(migrations_paths, target_version = nil)
- migrations = migrations(migrations_paths)
- migrations.select! { |m| yield m } if block_given?
+ def rollback(steps = 1)
+ move(:down, steps)
+ end
+
+ def forward(steps = 1)
+ move(:up, steps)
+ end
- new(:up, migrations, target_version).migrate
+ def up(target_version = nil)
+ selected_migrations = if block_given?
+ migrations.select { |m| yield m }
+ else
+ migrations
end
- def down(migrations_paths, target_version = nil, &block)
- migrations = migrations(migrations_paths)
- migrations.select! { |m| yield m } if block_given?
+ Migrator.new(:up, selected_migrations, target_version).migrate
+ end
- new(:down, migrations, target_version).migrate
+ def down(target_version = nil)
+ selected_migrations = if block_given?
+ migrations.select { |m| yield m }
+ else
+ migrations
end
- def run(direction, migrations_paths, target_version)
- new(direction, migrations(migrations_paths), target_version).run
- end
+ Migrator.new(:down, selected_migrations, target_version).migrate
+ end
- def open(migrations_paths)
- new(:up, migrations(migrations_paths), nil)
- end
+ def run(direction, target_version)
+ Migrator.new(direction, migrations, target_version).run
+ end
- def schema_migrations_table_name
- SchemaMigration.table_name
- end
+ def open
+ Migrator.new(:up, migrations, nil)
+ end
- def get_all_versions(connection = Base.connection)
- if connection.table_exists?(schema_migrations_table_name)
- SchemaMigration.all.map { |x| x.version.to_i }.sort
- else
- []
- end
+ def get_all_versions
+ if SchemaMigration.table_exists?
+ SchemaMigration.all_versions.map(&:to_i)
+ else
+ []
end
+ end
- def current_version(connection = Base.connection)
- get_all_versions(connection).max || 0
- end
+ def current_version
+ get_all_versions.max || 0
+ rescue ActiveRecord::NoDatabaseError
+ end
- def needs_migration?(connection = Base.connection)
- (migrations(migrations_paths).collect(&:version) - get_all_versions(connection)).size > 0
- end
+ def needs_migration?
+ (migrations.collect(&:version) - get_all_versions).size > 0
+ end
- def any_migrations?
- migrations(migrations_paths).any?
- end
+ def any_migrations?
+ migrations.any?
+ end
- def last_version
- last_migration.version
- end
+ def last_migration #:nodoc:
+ migrations.last || NullMigration.new
+ end
- def last_migration #:nodoc:
- migrations(migrations_paths).last || NullMigration.new
- end
+ def parse_migration_filename(filename) # :nodoc:
+ File.basename(filename).scan(Migration::MigrationFilenameRegexp).first
+ end
+
+ def migrations
+ migrations = migration_files.map do |file|
+ version, name, scope = parse_migration_filename(file)
+ raise IllegalMigrationNameError.new(file) unless version
+ version = version.to_i
+ name = name.camelize
- def migrations_paths
- @migrations_paths ||= ['db/migrate']
- # just to not break things if someone uses: migration_path = some_string
- Array(@migrations_paths)
+ MigrationProxy.new(name, version, file, scope)
end
- def migrations_path
- migrations_paths.first
+ migrations.sort_by(&:version)
+ end
+
+ def migrations_status
+ db_list = ActiveRecord::SchemaMigration.normalized_versions
+
+ file_list = migration_files.map do |file|
+ version, name, scope = parse_migration_filename(file)
+ raise IllegalMigrationNameError.new(file) unless version
+ version = ActiveRecord::SchemaMigration.normalize_migration_number(version)
+ status = db_list.delete(version) ? "up" : "down"
+ [status, version, (name + scope).humanize]
+ end.compact
+
+ db_list.map! do |version|
+ ["up", version, "********** NO FILE **********"]
end
- def migrations(paths)
- paths = Array(paths)
+ (db_list + file_list).sort_by { |_, version, _| version }
+ end
- files = Dir[*paths.map { |p| "#{p}/**/[0-9]*_*.rb" }]
+ def migration_files
+ paths = Array(migrations_paths)
+ Dir[*paths.flat_map { |path| "#{path}/**/[0-9]*_*.rb" }]
+ end
+
+ def current_environment
+ ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
+ end
- migrations = files.map do |file|
- version, name, scope = file.scan(/([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/).first
+ def protected_environment?
+ ActiveRecord::Base.protected_environments.include?(last_stored_environment) if last_stored_environment
+ end
+
+ def last_stored_environment
+ return nil if current_version == 0
+ raise NoEnvironmentInSchemaError unless ActiveRecord::InternalMetadata.table_exists?
+
+ environment = ActiveRecord::InternalMetadata[:environment]
+ raise NoEnvironmentInSchemaError unless environment
+ environment
+ end
- raise IllegalMigrationNameError.new(file) unless version
- version = version.to_i
- name = name.camelize
+ private
+ def move(direction, steps)
+ migrator = Migrator.new(direction, migrations)
- MigrationProxy.new(name, version, file, scope)
+ if current_version != 0 && !migrator.current_migration
+ raise UnknownMigrationVersionError.new(current_version)
end
- migrations.sort_by(&:version)
+ start_index =
+ if current_version == 0
+ 0
+ else
+ migrator.migrations.index(migrator.current_migration)
+ end
+
+ finish = migrator.migrations[start_index + steps]
+ version = finish ? finish.version : 0
+ send(direction, version)
end
+ end
- private
+ class Migrator # :nodoc:
+ class << self
+ attr_accessor :migrations_paths
- def move(direction, migrations_paths, steps)
- migrator = new(direction, migrations(migrations_paths))
- start_index = migrator.migrations.index(migrator.current_migration)
+ def migrations_path=(path)
+ ActiveSupport::Deprecation.warn \
+ "`ActiveRecord::Migrator.migrations_path=` is now deprecated and will be removed in Rails 6.0. " \
+ "You can set the `migrations_paths` on the `connection` instead through the `database.yml`."
+ self.migrations_paths = [path]
+ end
- if start_index
- finish = migrator.migrations[start_index + steps]
- version = finish ? finish.version : 0
- send(direction, migrations_paths, version)
- end
+ # For cases where a table doesn't exist like loading from schema cache
+ def current_version
+ MigrationContext.new(migrations_paths).current_version
end
end
- def initialize(direction, migrations, target_version = nil)
- raise StandardError.new("This database does not yet support migrations") unless Base.connection.supports_migrations?
+ self.migrations_paths = ["db/migrate"]
+ def initialize(direction, migrations, target_version = nil)
@direction = direction
@target_version = target_version
@migrated_versions = nil
@@ -907,7 +1192,8 @@ module ActiveRecord
validate(@migrations)
- Base.connection.initialize_schema_migrations_table
+ ActiveRecord::SchemaMigration.create_table
+ ActiveRecord::InternalMetadata.create_table
end
def current_version
@@ -920,32 +1206,18 @@ module ActiveRecord
alias :current :current_migration
def run
- migration = migrations.detect { |m| m.version == @target_version }
- raise UnknownMigrationVersionError.new(@target_version) if migration.nil?
- unless (up? && migrated.include?(migration.version.to_i)) || (down? && !migrated.include?(migration.version.to_i))
- begin
- execute_migration_in_transaction(migration, @direction)
- rescue => e
- canceled_msg = use_transaction?(migration) ? ", this migration was canceled" : ""
- raise StandardError, "An error has occurred#{canceled_msg}:\n\n#{e}", e.backtrace
- end
+ if use_advisory_lock?
+ with_advisory_lock { run_without_lock }
+ else
+ run_without_lock
end
end
def migrate
- if !target && @target_version && @target_version > 0
- raise UnknownMigrationVersionError.new(@target_version)
- end
-
- runnable.each do |migration|
- Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger
-
- begin
- execute_migration_in_transaction(migration, @direction)
- rescue => e
- canceled_msg = use_transaction?(migration) ? "this and " : ""
- raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace
- end
+ if use_advisory_lock?
+ with_advisory_lock { migrate_without_lock }
+ else
+ migrate_without_lock
end
end
@@ -970,70 +1242,145 @@ module ActiveRecord
end
def migrated
- @migrated_versions ||= Set.new(self.class.get_all_versions)
+ @migrated_versions || load_migrated
end
- private
- def ran?(migration)
- migrated.include?(migration.version.to_i)
+ def load_migrated
+ @migrated_versions = Set.new(Base.connection.migration_context.get_all_versions)
end
- def execute_migration_in_transaction(migration, direction)
- ddl_transaction(migration) do
- migration.migrate(direction)
- record_version_state_after_migrating(migration.version)
+ private
+
+ # Used for running a specific migration.
+ def run_without_lock
+ migration = migrations.detect { |m| m.version == @target_version }
+ raise UnknownMigrationVersionError.new(@target_version) if migration.nil?
+ result = execute_migration_in_transaction(migration, @direction)
+
+ record_environment
+ result
end
- end
- def target
- migrations.detect { |m| m.version == @target_version }
- end
+ # Used for running multiple migrations up to or down to a certain value.
+ def migrate_without_lock
+ if invalid_target?
+ raise UnknownMigrationVersionError.new(@target_version)
+ end
- def finish
- migrations.index(target) || migrations.size - 1
- end
+ result = runnable.each do |migration|
+ execute_migration_in_transaction(migration, @direction)
+ end
- def start
- up? ? 0 : (migrations.index(current) || 0)
- end
+ record_environment
+ result
+ end
- def validate(migrations)
- name ,= migrations.group_by(&:name).find { |_,v| v.length > 1 }
- raise DuplicateMigrationNameError.new(name) if name
+ # Stores the current environment in the database.
+ def record_environment
+ return if down?
+ ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Base.connection.migration_context.current_environment
+ end
- version ,= migrations.group_by(&:version).find { |_,v| v.length > 1 }
- raise DuplicateMigrationVersionError.new(version) if version
- end
+ def ran?(migration)
+ migrated.include?(migration.version.to_i)
+ end
- def record_version_state_after_migrating(version)
- if down?
- migrated.delete(version)
- ActiveRecord::SchemaMigration.where(:version => version.to_s).delete_all
- else
- migrated << version
- ActiveRecord::SchemaMigration.create!(:version => version.to_s)
+ # Return true if a valid version is not provided.
+ def invalid_target?
+ @target_version && @target_version != 0 && !target
end
- end
- def up?
- @direction == :up
- end
+ def execute_migration_in_transaction(migration, direction)
+ return if down? && !migrated.include?(migration.version.to_i)
+ return if up? && migrated.include?(migration.version.to_i)
- def down?
- @direction == :down
- end
+ Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger
- # Wrap the migration in a transaction only if supported by the adapter.
- def ddl_transaction(migration)
- if use_transaction?(migration)
- Base.transaction { yield }
- else
+ ddl_transaction(migration) do
+ migration.migrate(direction)
+ record_version_state_after_migrating(migration.version)
+ end
+ rescue => e
+ msg = +"An error has occurred, "
+ msg << "this and " if use_transaction?(migration)
+ msg << "all later migrations canceled:\n\n#{e}"
+ raise StandardError, msg, e.backtrace
+ end
+
+ def target
+ migrations.detect { |m| m.version == @target_version }
+ end
+
+ def finish
+ migrations.index(target) || migrations.size - 1
+ end
+
+ def start
+ up? ? 0 : (migrations.index(current) || 0)
+ end
+
+ def validate(migrations)
+ name, = migrations.group_by(&:name).find { |_, v| v.length > 1 }
+ raise DuplicateMigrationNameError.new(name) if name
+
+ version, = migrations.group_by(&:version).find { |_, v| v.length > 1 }
+ raise DuplicateMigrationVersionError.new(version) if version
+ end
+
+ def record_version_state_after_migrating(version)
+ if down?
+ migrated.delete(version)
+ ActiveRecord::SchemaMigration.where(version: version.to_s).delete_all
+ else
+ migrated << version
+ ActiveRecord::SchemaMigration.create!(version: version.to_s)
+ end
+ end
+
+ def up?
+ @direction == :up
+ end
+
+ def down?
+ @direction == :down
+ end
+
+ # Wrap the migration in a transaction only if supported by the adapter.
+ def ddl_transaction(migration)
+ if use_transaction?(migration)
+ Base.transaction { yield }
+ else
+ yield
+ end
+ end
+
+ def use_transaction?(migration)
+ !migration.disable_ddl_transaction && Base.connection.supports_ddl_transactions?
+ end
+
+ def use_advisory_lock?
+ Base.connection.advisory_locks_enabled?
+ end
+
+ def with_advisory_lock
+ lock_id = generate_migrator_advisory_lock_id
+ connection = Base.connection
+ got_lock = connection.get_advisory_lock(lock_id)
+ raise ConcurrentMigrationError unless got_lock
+ load_migrated # reload schema_migrations to be sure it wasn't changed by another process before we got the lock
yield
+ ensure
+ if got_lock && !connection.release_advisory_lock(lock_id)
+ raise ConcurrentMigrationError.new(
+ ConcurrentMigrationError::RELEASE_LOCK_FAILED_MESSAGE
+ )
+ end
end
- end
- def use_transaction?(migration)
- !migration.disable_ddl_transaction && Base.connection.supports_ddl_transactions?
- end
+ MIGRATOR_SALT = 2053462845
+ def generate_migrator_advisory_lock_id
+ db_name_hash = Zlib.crc32(Base.connection.current_database)
+ MIGRATOR_SALT * db_name_hash
+ end
end
end
diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb
index dcc2362397..82f5121d94 100644
--- a/activerecord/lib/active_record/migration/command_recorder.rb
+++ b/activerecord/lib/active_record/migration/command_recorder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
class Migration
# <tt>ActiveRecord::Migration::CommandRecorder</tt> records commands done during
@@ -5,15 +7,36 @@ module ActiveRecord
# knows how to invert the following commands:
#
# * add_column
+ # * add_foreign_key
# * add_index
+ # * add_reference
# * add_timestamps
- # * create_table
+ # * change_column
+ # * change_column_default (must supply a :from and :to option)
+ # * change_column_null
# * create_join_table
+ # * create_table
+ # * disable_extension
+ # * drop_join_table
+ # * drop_table (must supply a block)
+ # * enable_extension
+ # * remove_column (must supply a type)
+ # * remove_columns (must specify at least one column name or more)
+ # * remove_foreign_key (must supply a second table)
+ # * remove_index
+ # * remove_reference
# * remove_timestamps
# * rename_column
# * rename_index
# * rename_table
class CommandRecorder
+ ReversibleAndIrreversibleMethods = [:create_table, :create_join_table, :rename_table, :add_column, :remove_column,
+ :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps,
+ :change_column_default, :add_reference, :remove_reference, :transaction,
+ :drop_join_table, :drop_table, :execute_block, :enable_extension, :disable_extension,
+ :change_column, :execute, :remove_columns, :change_column_null,
+ :add_foreign_key, :remove_foreign_key
+ ]
include JoinTable
attr_accessor :commands, :delegate, :reverting
@@ -41,7 +64,7 @@ module ActiveRecord
@reverting = !@reverting
end
- # record +command+. +command+ should be a method name and arguments.
+ # Record +command+. +command+ should be a method name and arguments.
# For example:
#
# recorder.record(:method_name, [:arg1, :arg2])
@@ -62,22 +85,16 @@ module ActiveRecord
# invert the +command+.
def inverse_of(command, args, &block)
method = :"invert_#{command}"
- raise IrreversibleMigration unless respond_to?(method, true)
+ raise IrreversibleMigration, <<~MSG unless respond_to?(method, true)
+ This migration uses #{command}, which is not automatically reversible.
+ To make the migration reversible you can either:
+ 1. Define #up and #down methods in place of the #change method.
+ 2. Use the #reversible method to define reversible behavior.
+ MSG
send(method, args, &block)
end
- def respond_to?(*args) # :nodoc:
- super || delegate.respond_to?(*args)
- end
-
- [:create_table, :create_join_table, :rename_table, :add_column, :remove_column,
- :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps,
- :add_reference, :remove_reference, :transaction,
- :drop_join_table, :drop_table, :execute_block, :enable_extension,
- :change_column, :execute, :remove_columns, :change_column_null,
- :add_foreign_key, :remove_foreign_key
- # irreversible methods need to be here too
- ].each do |method|
+ ReversibleAndIrreversibleMethods.each do |method|
class_eval <<-EOV, __FILE__, __LINE__ + 1
def #{method}(*args, &block) # def create_table(*args, &block)
record(:"#{method}", args, &block) # record(:create_table, args, &block)
@@ -91,129 +108,163 @@ module ActiveRecord
yield delegate.update_table_definition(table_name, self)
end
+ def replay(migration)
+ commands.each do |cmd, args, block|
+ migration.send(cmd, *args, &block)
+ end
+ end
+
private
- module StraightReversions
- private
- { transaction: :transaction,
- execute_block: :execute_block,
- create_table: :drop_table,
- create_join_table: :drop_join_table,
- add_column: :remove_column,
- add_timestamps: :remove_timestamps,
- add_reference: :remove_reference,
- enable_extension: :disable_extension
- }.each do |cmd, inv|
- [[inv, cmd], [cmd, inv]].uniq.each do |method, inverse|
- class_eval <<-EOV, __FILE__, __LINE__ + 1
- def invert_#{method}(args, &block) # def invert_create_table(args, &block)
- [:#{inverse}, args, block] # [:drop_table, args, block]
- end # end
- EOV
- end
+ module StraightReversions # :nodoc:
+ private
+ {
+ execute_block: :execute_block,
+ create_table: :drop_table,
+ create_join_table: :drop_join_table,
+ add_column: :remove_column,
+ add_timestamps: :remove_timestamps,
+ add_reference: :remove_reference,
+ enable_extension: :disable_extension
+ }.each do |cmd, inv|
+ [[inv, cmd], [cmd, inv]].uniq.each do |method, inverse|
+ class_eval <<-EOV, __FILE__, __LINE__ + 1
+ def invert_#{method}(args, &block) # def invert_create_table(args, &block)
+ [:#{inverse}, args, block] # [:drop_table, args, block]
+ end # end
+ EOV
+ end
+ end
end
- end
- include StraightReversions
+ include StraightReversions
+
+ def invert_transaction(args)
+ sub_recorder = CommandRecorder.new(delegate)
+ sub_recorder.revert { yield }
+
+ invertions_proc = proc {
+ sub_recorder.replay(self)
+ }
- def invert_drop_table(args, &block)
- if args.size == 1 && block == nil
- raise ActiveRecord::IrreversibleMigration, "To avoid mistakes, drop_table is only reversible if given options or a block (can be empty)."
+ [:transaction, args, invertions_proc]
end
- super
- end
- def invert_rename_table(args)
- [:rename_table, args.reverse]
- end
+ def invert_drop_table(args, &block)
+ if args.size == 1 && block == nil
+ raise ActiveRecord::IrreversibleMigration, "To avoid mistakes, drop_table is only reversible if given options or a block (can be empty)."
+ end
+ super
+ end
- def invert_remove_column(args)
- raise ActiveRecord::IrreversibleMigration, "remove_column is only reversible if given a type." if args.size <= 2
- super
- end
+ def invert_rename_table(args)
+ [:rename_table, args.reverse]
+ end
- def invert_rename_index(args)
- [:rename_index, [args.first] + args.last(2).reverse]
- end
+ def invert_remove_column(args)
+ raise ActiveRecord::IrreversibleMigration, "remove_column is only reversible if given a type." if args.size <= 2
+ super
+ end
- def invert_rename_column(args)
- [:rename_column, [args.first] + args.last(2).reverse]
- end
+ def invert_rename_index(args)
+ [:rename_index, [args.first] + args.last(2).reverse]
+ end
- def invert_add_index(args)
- table, columns, options = *args
- options ||= {}
+ def invert_rename_column(args)
+ [:rename_column, [args.first] + args.last(2).reverse]
+ end
- index_name = options[:name]
- options_hash = index_name ? { name: index_name } : { column: columns }
+ def invert_add_index(args)
+ table, columns, options = *args
+ options ||= {}
- [:remove_index, [table, options_hash]]
- end
+ options_hash = options.slice(:name, :algorithm)
+ options_hash[:column] = columns if !options_hash[:name]
- def invert_remove_index(args)
- table, options_or_column = *args
- if (options = options_or_column).is_a?(Hash)
- unless options[:column]
- raise ActiveRecord::IrreversibleMigration, "remove_index is only reversible if given a :column option."
+ [:remove_index, [table, options_hash]]
+ end
+
+ def invert_remove_index(args)
+ table, options_or_column = *args
+ if (options = options_or_column).is_a?(Hash)
+ unless options[:column]
+ raise ActiveRecord::IrreversibleMigration, "remove_index is only reversible if given a :column option."
+ end
+ options = options.dup
+ [:add_index, [table, options.delete(:column), options]]
+ elsif (column = options_or_column).present?
+ [:add_index, [table, column]]
end
- options = options.dup
- [:add_index, [table, options.delete(:column), options]]
- elsif (column = options_or_column).present?
- [:add_index, [table, column]]
end
- end
- alias :invert_add_belongs_to :invert_add_reference
- alias :invert_remove_belongs_to :invert_remove_reference
+ alias :invert_add_belongs_to :invert_add_reference
+ alias :invert_remove_belongs_to :invert_remove_reference
- def invert_change_column_default(args)
- table, column, options = *args
+ def invert_change_column_default(args)
+ table, column, options = *args
- unless options && options.is_a?(Hash) && options.has_key?(:from) && options.has_key?(:to)
- raise ActiveRecord::IrreversibleMigration, "change_column_default is only reversible if given a :from and :to option."
+ unless options && options.is_a?(Hash) && options.has_key?(:from) && options.has_key?(:to)
+ raise ActiveRecord::IrreversibleMigration, "change_column_default is only reversible if given a :from and :to option."
+ end
+
+ [:change_column_default, [table, column, from: options[:to], to: options[:from]]]
end
- [:change_column_default, [table, column, from: options[:to], to: options[:from]]]
- end
+ def invert_change_column_null(args)
+ args[2] = !args[2]
+ [:change_column_null, args]
+ end
- def invert_change_column_null(args)
- args[2] = !args[2]
- [:change_column_null, args]
- end
+ def invert_add_foreign_key(args)
+ from_table, to_table, add_options = args
+ add_options ||= {}
- def invert_add_foreign_key(args)
- from_table, to_table, add_options = args
- add_options ||= {}
+ if add_options[:name]
+ options = { name: add_options[:name] }
+ elsif add_options[:column]
+ options = { column: add_options[:column] }
+ else
+ options = to_table
+ end
- if add_options[:name]
- options = { name: add_options[:name] }
- elsif add_options[:column]
- options = { column: add_options[:column] }
- else
- options = to_table
+ [:remove_foreign_key, [from_table, options]]
end
- [:remove_foreign_key, [from_table, options]]
- end
+ def invert_remove_foreign_key(args)
+ from_table, options_or_to_table, options_or_nil = args
- def invert_remove_foreign_key(args)
- from_table, to_table, remove_options = args
- raise ActiveRecord::IrreversibleMigration, "remove_foreign_key is only reversible if given a second table" if to_table.nil? || to_table.is_a?(Hash)
+ to_table = if options_or_to_table.is_a?(Hash)
+ options_or_to_table[:to_table]
+ else
+ options_or_to_table
+ end
- reversed_args = [from_table, to_table]
- reversed_args << remove_options if remove_options
+ remove_options = if options_or_to_table.is_a?(Hash)
+ options_or_to_table.except(:to_table)
+ else
+ options_or_nil
+ end
- [:add_foreign_key, reversed_args]
- end
+ raise ActiveRecord::IrreversibleMigration, "remove_foreign_key is only reversible if given a second table" if to_table.nil?
- # Forwards any missing method call to the \target.
- def method_missing(method, *args, &block)
- if @delegate.respond_to?(method)
- @delegate.send(method, *args, &block)
- else
- super
+ reversed_args = [from_table, to_table]
+ reversed_args << remove_options if remove_options.present?
+
+ [:add_foreign_key, reversed_args]
+ end
+
+ def respond_to_missing?(method, _)
+ super || delegate.respond_to?(method)
+ end
+
+ # Forwards any missing method call to the \target.
+ def method_missing(method, *args, &block)
+ if delegate.respond_to?(method)
+ delegate.public_send(method, *args, &block)
+ else
+ super
+ end
end
- end
end
end
end
diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb
new file mode 100644
index 0000000000..8f6fcfcaea
--- /dev/null
+++ b/activerecord/lib/active_record/migration/compatibility.rb
@@ -0,0 +1,235 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ class Migration
+ module Compatibility # :nodoc: all
+ def self.find(version)
+ version = version.to_s
+ name = "V#{version.tr('.', '_')}"
+ unless const_defined?(name)
+ versions = constants.grep(/\AV[0-9_]+\z/).map { |s| s.to_s.delete("V").tr("_", ".").inspect }
+ raise ArgumentError, "Unknown migration version #{version.inspect}; expected one of #{versions.sort.join(', ')}"
+ end
+ const_get(name)
+ end
+
+ V6_0 = Current
+
+ class V5_2 < V6_0
+ module CommandRecorder
+ def invert_transaction(args, &block)
+ [:transaction, args, block]
+ end
+ end
+
+ private
+
+ def command_recorder
+ recorder = super
+ class << recorder
+ prepend CommandRecorder
+ end
+ recorder
+ end
+ end
+
+ class V5_1 < V5_2
+ def change_column(table_name, column_name, type, options = {})
+ if adapter_name == "PostgreSQL"
+ clear_cache!
+ sql = connection.send(:change_column_sql, table_name, column_name, type, options)
+ execute "ALTER TABLE #{quote_table_name(table_name)} #{sql}"
+ change_column_default(table_name, column_name, options[:default]) if options.key?(:default)
+ change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
+ change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment)
+ else
+ super
+ end
+ end
+
+ def create_table(table_name, options = {})
+ if adapter_name == "Mysql2"
+ super(table_name, options: "ENGINE=InnoDB", **options)
+ else
+ super
+ end
+ end
+ end
+
+ class V5_0 < V5_1
+ module TableDefinition
+ def primary_key(name, type = :primary_key, **options)
+ type = :integer if type == :primary_key
+ super
+ end
+
+ def references(*args, **options)
+ super(*args, type: :integer, **options)
+ end
+ 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 PostgreSQL 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 do |t|
+ yield compatible_table_definition(t)
+ end
+ else
+ super
+ end
+ end
+
+ def change_table(table_name, options = {})
+ if block_given?
+ super do |t|
+ yield compatible_table_definition(t)
+ end
+ else
+ super
+ end
+ end
+
+ def create_join_table(table_1, table_2, column_options: {}, **options)
+ column_options.reverse_merge!(type: :integer)
+
+ if block_given?
+ super do |t|
+ yield compatible_table_definition(t)
+ end
+ else
+ super
+ end
+ end
+
+ def add_column(table_name, column_name, type, options = {})
+ if type == :primary_key
+ type = :integer
+ options[:primary_key] = true
+ end
+ super
+ end
+
+ def add_reference(table_name, ref_name, **options)
+ super(table_name, ref_name, type: :integer, **options)
+ end
+ alias :add_belongs_to :add_reference
+
+ private
+ def compatible_table_definition(t)
+ class << t
+ prepend TableDefinition
+ end
+ t
+ end
+ end
+
+ class V4_2 < V5_0
+ module TableDefinition
+ def references(*, **options)
+ options[:index] ||= false
+ super
+ end
+ alias :belongs_to :references
+
+ def timestamps(**options)
+ options[:null] = true if options[:null].nil?
+ super
+ end
+ end
+
+ def create_table(table_name, options = {})
+ if block_given?
+ super do |t|
+ yield compatible_table_definition(t)
+ end
+ else
+ super
+ end
+ end
+
+ def change_table(table_name, options = {})
+ if block_given?
+ super do |t|
+ yield compatible_table_definition(t)
+ end
+ else
+ super
+ end
+ end
+
+ def add_reference(*, **options)
+ options[:index] ||= false
+ super
+ end
+ alias :add_belongs_to :add_reference
+
+ def add_timestamps(_, **options)
+ options[:null] = true if options[:null].nil?
+ super
+ end
+
+ def index_exists?(table_name, column_name, options = {})
+ column_names = Array(column_name).map(&:to_s)
+ options[:name] =
+ if options[:name].present?
+ options[:name].to_s
+ else
+ index_name(table_name, column: column_names)
+ end
+ super
+ end
+
+ def remove_index(table_name, options = {})
+ options = { column: options } unless options.is_a?(Hash)
+ options[:name] = index_name_for_remove(table_name, options)
+ super(table_name, options)
+ end
+
+ private
+ def compatible_table_definition(t)
+ class << t
+ prepend TableDefinition
+ end
+ super
+ end
+
+ def index_name_for_remove(table_name, options = {})
+ index_name = index_name(table_name, options)
+
+ 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)
+ end
+
+ raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist"
+ end
+
+ index_name
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/migration/join_table.rb b/activerecord/lib/active_record/migration/join_table.rb
index 05569fadbd..9abb289bb0 100644
--- a/activerecord/lib/active_record/migration/join_table.rb
+++ b/activerecord/lib/active_record/migration/join_table.rb
@@ -1,15 +1,17 @@
+# frozen_string_literal: true
+
module ActiveRecord
class Migration
module JoinTable #:nodoc:
private
- def find_join_table_name(table_1, table_2, options = {})
- options.delete(:table_name) || join_table_name(table_1, table_2)
- end
+ def find_join_table_name(table_1, table_2, options = {})
+ options.delete(:table_name) || join_table_name(table_1, table_2)
+ end
- def join_table_name(table_1, table_2)
- ModelSchema.derive_join_table_name(table_1, table_2).to_sym
- end
+ def join_table_name(table_1, table_2)
+ ModelSchema.derive_join_table_name(table_1, table_2).to_sym
+ end
end
end
end
diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb
index 5a6f42ba09..c50a420432 100644
--- a/activerecord/lib/active_record/model_schema.rb
+++ b/activerecord/lib/active_record/model_schema.rb
@@ -1,58 +1,123 @@
+# frozen_string_literal: true
+
+require "monitor"
+
module ActiveRecord
module ModelSchema
extend ActiveSupport::Concern
+ ##
+ # :singleton-method: primary_key_prefix_type
+ # :call-seq: primary_key_prefix_type
+ #
+ # The prefix type that will be prepended to every primary key column name.
+ # The options are +:table_name+ and +:table_name_with_underscore+. If the first is specified,
+ # the Product class will look for "productid" instead of "id" as the primary column. If the
+ # latter is specified, the Product class will look for "product_id" instead of "id". Remember
+ # that this is a global setting for all Active Records.
+
+ ##
+ # :singleton-method: primary_key_prefix_type=
+ # :call-seq: primary_key_prefix_type=(prefix_type)
+ #
+ # Sets the prefix type that will be prepended to every primary key column name.
+ # The options are +:table_name+ and +:table_name_with_underscore+. If the first is specified,
+ # the Product class will look for "productid" instead of "id" as the primary column. If the
+ # latter is specified, the Product class will look for "product_id" instead of "id". Remember
+ # that this is a global setting for all Active Records.
+
+ ##
+ # :singleton-method: table_name_prefix
+ # :call-seq: table_name_prefix
+ #
+ # The prefix string to prepend to every table name.
+
+ ##
+ # :singleton-method: table_name_prefix=
+ # :call-seq: table_name_prefix=(prefix)
+ #
+ # Sets the prefix string to prepend to every table name. So if set to "basecamp_", all table
+ # names will be named like "basecamp_projects", "basecamp_people", etc. This is a convenient
+ # way of creating a namespace for tables in a shared database. By default, the prefix is the
+ # empty string.
+ #
+ # If you are organising your models within modules you can add a prefix to the models within
+ # a namespace by defining a singleton method in the parent module called table_name_prefix which
+ # returns your chosen prefix.
+
+ ##
+ # :singleton-method: table_name_suffix
+ # :call-seq: table_name_suffix
+ #
+ # The suffix string to append to every table name.
+
+ ##
+ # :singleton-method: table_name_suffix=
+ # :call-seq: table_name_suffix=(suffix)
+ #
+ # Works like +table_name_prefix=+, but appends instead of prepends (set to "_basecamp" gives "projects_basecamp",
+ # "people_basecamp"). By default, the suffix is the empty string.
+ #
+ # If you are organising your models within modules, you can add a suffix to the models within
+ # a namespace by defining a singleton method in the parent module called table_name_suffix which
+ # returns your chosen suffix.
+
+ ##
+ # :singleton-method: schema_migrations_table_name
+ # :call-seq: schema_migrations_table_name
+ #
+ # The name of the schema migrations table. By default, the value is <tt>"schema_migrations"</tt>.
+
+ ##
+ # :singleton-method: schema_migrations_table_name=
+ # :call-seq: schema_migrations_table_name=(table_name)
+ #
+ # Sets the name of the schema migrations table.
+
+ ##
+ # :singleton-method: internal_metadata_table_name
+ # :call-seq: internal_metadata_table_name
+ #
+ # The name of the internal metadata table. By default, the value is <tt>"ar_internal_metadata"</tt>.
+
+ ##
+ # :singleton-method: internal_metadata_table_name=
+ # :call-seq: internal_metadata_table_name=(table_name)
+ #
+ # Sets the name of the internal metadata table.
+
+ ##
+ # :singleton-method: pluralize_table_names
+ # :call-seq: pluralize_table_names
+ #
+ # Indicates whether table names should be the pluralized versions of the corresponding class names.
+ # If true, the default table name for a Product class will be "products". If false, it would just be "product".
+ # See table_name for the full rules on table/class naming. This is true, by default.
+
+ ##
+ # :singleton-method: pluralize_table_names=
+ # :call-seq: pluralize_table_names=(value)
+ #
+ # Set whether table names should be the pluralized versions of the corresponding class names.
+ # If true, the default table name for a Product class will be "products". If false, it would just be "product".
+ # See table_name for the full rules on table/class naming. This is true, by default.
+
included do
- ##
- # :singleton-method:
- # Accessor for the prefix type that will be prepended to every primary key column name.
- # The options are :table_name and :table_name_with_underscore. If the first is specified,
- # the Product class will look for "productid" instead of "id" as the primary column. If the
- # latter is specified, the Product class will look for "product_id" instead of "id". Remember
- # that this is a global setting for all Active Records.
mattr_accessor :primary_key_prefix_type, instance_writer: false
- ##
- # :singleton-method:
- # Accessor for the name of the prefix string to prepend to every table name. So if set
- # to "basecamp_", all table names will be named like "basecamp_projects", "basecamp_people",
- # etc. This is a convenient way of creating a namespace for tables in a shared database.
- # By default, the prefix is the empty string.
- #
- # If you are organising your models within modules you can add a prefix to the models within
- # a namespace by defining a singleton method in the parent module called table_name_prefix which
- # returns your chosen prefix.
- class_attribute :table_name_prefix, instance_writer: false
- self.table_name_prefix = ""
-
- ##
- # :singleton-method:
- # Works like +table_name_prefix+, but appends instead of prepends (set to "_basecamp" gives "projects_basecamp",
- # "people_basecamp"). By default, the suffix is the empty string.
- #
- # If you are organising your models within modules, you can add a suffix to the models within
- # a namespace by defining a singleton method in the parent module called table_name_suffix which
- # returns your chosen suffix.
- class_attribute :table_name_suffix, instance_writer: false
- self.table_name_suffix = ""
-
- ##
- # :singleton-method:
- # Accessor for the name of the schema migrations table. By default, the value is "schema_migrations"
- class_attribute :schema_migrations_table_name, instance_accessor: false
- self.schema_migrations_table_name = "schema_migrations"
-
- ##
- # :singleton-method:
- # Indicates whether table names should be the pluralized versions of the corresponding class names.
- # If true, the default table name for a Product class will be +products+. If false, it would just be +product+.
- # See table_name for the full rules on table/class naming. This is true, by default.
- class_attribute :pluralize_table_names, instance_writer: false
- self.pluralize_table_names = true
-
- self.inheritance_column = 'type'
+ class_attribute :table_name_prefix, instance_writer: false, default: ""
+ class_attribute :table_name_suffix, instance_writer: false, default: ""
+ class_attribute :schema_migrations_table_name, instance_accessor: false, default: "schema_migrations"
+ class_attribute :internal_metadata_table_name, instance_accessor: false, default: "ar_internal_metadata"
+ class_attribute :pluralize_table_names, instance_writer: false, default: true
+
+ self.protected_environments = ["production"]
+ self.inheritance_column = "type"
+ self.ignored_columns = [].freeze
delegate :type_for_attribute, to: :class
+
+ initialize_load_schema_monitor
end
# Derives the join table name for +first_table+ and +second_table+. The
@@ -153,11 +218,26 @@ module ActiveRecord
end
def full_table_name_prefix #:nodoc:
- (parents.detect{ |p| p.respond_to?(:table_name_prefix) } || self).table_name_prefix
+ (module_parents.detect { |p| p.respond_to?(:table_name_prefix) } || self).table_name_prefix
end
def full_table_name_suffix #:nodoc:
- (parents.detect {|p| p.respond_to?(:table_name_suffix) } || self).table_name_suffix
+ (module_parents.detect { |p| p.respond_to?(:table_name_suffix) } || self).table_name_suffix
+ end
+
+ # The array of names of environments where destructive actions should be prohibited. By default,
+ # the value is <tt>["production"]</tt>.
+ def protected_environments
+ if defined?(@protected_environments)
+ @protected_environments
+ else
+ superclass.protected_environments
+ end
+ end
+
+ # Sets an array of names of environments where destructive actions should be prohibited.
+ def protected_environments=(environments)
+ @protected_environments = environments.map(&:to_s)
end
# Defines the name of the table column which will store the class name on single-table
@@ -179,8 +259,24 @@ module ActiveRecord
@explicit_inheritance_column = true
end
+ # The list of columns names the model should ignore. Ignored columns won't have attribute
+ # accessors defined, and won't be referenced in SQL queries.
+ def ignored_columns
+ if defined?(@ignored_columns)
+ @ignored_columns
+ else
+ superclass.ignored_columns
+ end
+ end
+
+ # Sets the columns names the model should ignore. Ignored columns won't have attribute
+ # accessors defined, and won't be referenced in SQL queries.
+ def ignored_columns=(columns)
+ @ignored_columns = columns.map(&:to_s)
+ end
+
def sequence_name
- if base_class == self
+ if base_class?
@sequence_name ||= reset_sequence_name
else
(@sequence_name ||= nil) || base_class.sequence_name
@@ -193,7 +289,7 @@ module ActiveRecord
end
# Sets the name of the sequence to use when generating ids to the given
- # value, or (if the value is nil or false) to the value returned by the
+ # value, or (if the value is +nil+ or +false+) to the value returned by the
# given block. This is required for Oracle and is useful for any
# database which relies on sequences for primary key generation.
#
@@ -211,13 +307,29 @@ module ActiveRecord
@explicit_sequence_name = true
end
+ # Determines if the primary key values should be selected from their
+ # corresponding sequence before the insert statement.
+ def prefetch_primary_key?
+ connection.prefetch_primary_key?(table_name)
+ end
+
+ # Returns the next value that will be used as the primary key on
+ # an insert statement.
+ def next_sequence_value
+ connection.next_sequence_value(sequence_name)
+ end
+
# Indicates whether the table associated with this class exists
def table_exists?
- connection.schema_cache.table_exists?(table_name)
+ connection.schema_cache.data_source_exists?(table_name)
end
def attributes_builder # :nodoc:
- @attributes_builder ||= AttributeSet::Builder.new(attribute_types, primary_key)
+ unless defined?(@attributes_builder) && @attributes_builder
+ defaults = _default_attributes.except(*(column_names - [primary_key]))
+ @attributes_builder = ActiveModel::AttributeSet::Builder.new(attribute_types, defaults)
+ end
+ @attributes_builder
end
def columns_hash # :nodoc:
@@ -232,22 +344,43 @@ module ActiveRecord
def attribute_types # :nodoc:
load_schema
- @attribute_types ||= Hash.new(Type::Value.new)
+ @attribute_types ||= Hash.new(Type.default_value)
end
- def type_for_attribute(attr_name) # :nodoc:
- attribute_types[attr_name]
+ def yaml_encoder # :nodoc:
+ @yaml_encoder ||= ActiveModel::AttributeSet::YAMLEncoder.new(attribute_types)
+ end
+
+ # Returns the type of the attribute with the given name, after applying
+ # all modifiers. This method is the only valid source of information for
+ # anything related to the types of a model's attributes. This method will
+ # access the database and load the model's schema if it is required.
+ #
+ # The return value of this method will implement the interface described
+ # by ActiveModel::Type::Value (though the object itself may not subclass
+ # it).
+ #
+ # +attr_name+ The name of the attribute to retrieve the type for. Must be
+ # a string or a symbol.
+ def type_for_attribute(attr_name, &block)
+ attr_name = attr_name.to_s
+ if block
+ attribute_types.fetch(attr_name, &block)
+ else
+ attribute_types[attr_name]
+ end
end
# Returns a hash where the keys are column names and the values are
- # default values when instantiating the AR object for this table.
+ # default values when instantiating the Active Record object for this table.
def column_defaults
load_schema
- _default_attributes.to_hash
+ @column_defaults ||= _default_attributes.deep_dup.to_hash
end
def _default_attributes # :nodoc:
- @default_attributes ||= AttributeSet.new({})
+ load_schema
+ @default_attributes ||= ActiveModel::AttributeSet.new({})
end
# Returns an array of column names as strings.
@@ -255,10 +388,20 @@ module ActiveRecord
@column_names ||= columns.map(&:name)
end
+ def symbol_column_to_string(name_symbol) # :nodoc:
+ @symbol_column_to_string_name_hash ||= column_names.index_by(&:to_sym)
+ @symbol_column_to_string_name_hash[name_symbol]
+ end
+
# Returns an array of column objects where the primary id, all columns ending in "_id" or "_count",
# and columns used for single table inheritance have been removed.
def content_columns
- @content_columns ||= columns.reject { |c| c.name == primary_key || c.name =~ /(_id|_count)$/ || c.name == inheritance_column }
+ @content_columns ||= columns.reject do |c|
+ c.name == primary_key ||
+ c.name == inheritance_column ||
+ c.name.end_with?("_id") ||
+ c.name.end_with?("_count")
+ end
end
# Resets all the cached information about columns, which will cause them
@@ -268,7 +411,7 @@ module ActiveRecord
# when just after creating a table you want to populate it with some default
# values, eg:
#
- # class CreateJobLevels < ActiveRecord::Migration
+ # class CreateJobLevels < ActiveRecord::Migration[5.0]
# def up
# create_table :job_levels do |t|
# t.integer :id
@@ -289,96 +432,95 @@ module ActiveRecord
# end
def reset_column_information
connection.clear_cache!
- undefine_attribute_methods
- connection.schema_cache.clear_table_cache!(table_name)
+ ([self] + descendants).each(&:undefine_attribute_methods)
+ connection.schema_cache.clear_data_source_cache!(table_name)
reload_schema_from_cache
+ initialize_find_by_cache
end
- private
+ protected
- def schema_loaded?
- defined?(@columns_hash) && @columns_hash
- end
+ def initialize_load_schema_monitor
+ @load_schema_monitor = Monitor.new
+ end
- def load_schema
- unless schema_loaded?
- load_schema!
+ private
+
+ def inherited(child_class)
+ super
+ child_class.initialize_load_schema_monitor
end
- end
- def load_schema!
- @columns_hash = connection.schema_cache.columns_hash(table_name)
- @columns_hash.each do |name, column|
- warn_if_deprecated_type(column)
- define_attribute(
- name,
- connection.lookup_cast_type_from_column(column),
- default: column.default,
- user_provided_default: false
- )
+ def schema_loaded?
+ defined?(@schema_loaded) && @schema_loaded
end
- end
- def reload_schema_from_cache
- @arel_engine = nil
- @arel_table = nil
- @column_names = nil
- @attribute_types = nil
- @content_columns = nil
- @default_attributes = nil
- @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column
- @attributes_builder = nil
- @columns = nil
- @columns_hash = nil
- @attribute_names = nil
- end
+ def load_schema
+ return if schema_loaded?
+ @load_schema_monitor.synchronize do
+ return if defined?(@columns_hash) && @columns_hash
- # Guesses the table name, but does not decorate it with prefix and suffix information.
- def undecorated_table_name(class_name = base_class.name)
- table_name = class_name.to_s.demodulize.underscore
- pluralize_table_names ? table_name.pluralize : table_name
- end
+ load_schema!
- # Computes and returns a table name according to default conventions.
- def compute_table_name
- base = base_class
- if self == base
- # Nested classes are prefixed with singular parent table name.
- if parent < Base && !parent.abstract_class?
- contained = parent.table_name
- contained = contained.singularize if parent.pluralize_table_names
- contained += '_'
+ @schema_loaded = true
end
-
- "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{full_table_name_suffix}"
- else
- # STI subclasses always use their superclass' table.
- base.table_name
end
- end
- def warn_if_deprecated_type(column)
- return if attributes_to_define_after_schema_loads.key?(column.name)
- if column.respond_to?(:oid) && column.sql_type.start_with?("point")
- if column.array?
- array_arguments = ", array: true"
- else
- array_arguments = ""
+ def load_schema!
+ @columns_hash = connection.schema_cache.columns_hash(table_name).except(*ignored_columns)
+ @columns_hash.each do |name, column|
+ define_attribute(
+ name,
+ connection.lookup_cast_type_from_column(column),
+ default: column.default,
+ user_provided_default: false
+ )
end
- ActiveSupport::Deprecation.warn(<<-WARNING.strip_heredoc)
- The behavior of the `:point` type will be changing in Rails 5.1 to
- return a `Point` object, rather than an `Array`. If you'd like to
- keep the old behavior, you can add this line to #{self.name}:
+ end
- attribute :#{column.name}, :legacy_point#{array_arguments}
+ def reload_schema_from_cache
+ @arel_table = nil
+ @column_names = nil
+ @symbol_column_to_string_name_hash = nil
+ @attribute_types = nil
+ @content_columns = nil
+ @default_attributes = nil
+ @column_defaults = nil
+ @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column
+ @attributes_builder = nil
+ @columns = nil
+ @columns_hash = nil
+ @schema_loaded = false
+ @attribute_names = nil
+ @yaml_encoder = nil
+ direct_descendants.each do |descendant|
+ descendant.send(:reload_schema_from_cache)
+ end
+ end
- If you'd like the new behavior today, you can add this line:
+ # Guesses the table name, but does not decorate it with prefix and suffix information.
+ def undecorated_table_name(class_name = base_class.name)
+ table_name = class_name.to_s.demodulize.underscore
+ pluralize_table_names ? table_name.pluralize : table_name
+ end
- attribute :#{column.name}, :rails_5_1_point#{array_arguments}
- WARNING
+ # Computes and returns a table name according to default conventions.
+ def compute_table_name
+ if base_class?
+ # Nested classes are prefixed with singular parent table name.
+ if module_parent < Base && !module_parent.abstract_class?
+ contained = module_parent.table_name
+ contained = contained.singularize if module_parent.pluralize_table_names
+ contained += "_"
+ end
+
+ "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{full_table_name_suffix}"
+ else
+ # STI subclasses always use their superclass' table.
+ base_class.table_name
+ end
end
- end
end
end
end
diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb
index c942d0e265..8b9098df6c 100644
--- a/activerecord/lib/active_record/nested_attributes.rb
+++ b/activerecord/lib/active_record/nested_attributes.rb
@@ -1,6 +1,9 @@
-require 'active_support/core_ext/hash/except'
-require 'active_support/core_ext/object/try'
-require 'active_support/core_ext/hash/indifferent_access'
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/except"
+require "active_support/core_ext/module/redefine_method"
+require "active_support/core_ext/object/try"
+require "active_support/core_ext/hash/indifferent_access"
module ActiveRecord
module NestedAttributes #:nodoc:
@@ -10,8 +13,7 @@ module ActiveRecord
extend ActiveSupport::Concern
included do
- class_attribute :nested_attributes_options, instance_writer: false
- self.nested_attributes_options = {}
+ class_attribute :nested_attributes_options, instance_writer: false, default: {}
end
# = Active Record Nested Attributes
@@ -61,6 +63,18 @@ module ActiveRecord
# member.update params[:member]
# member.avatar.icon # => 'sad'
#
+ # If you want to update the current avatar without providing the id, you must add <tt>:update_only</tt> option.
+ #
+ # class Member < ActiveRecord::Base
+ # has_one :avatar
+ # accepts_nested_attributes_for :avatar, update_only: true
+ # end
+ #
+ # params = { member: { avatar_attributes: { icon: 'sad' } } }
+ # member.update params[:member]
+ # member.avatar.id # => 2
+ # member.avatar.icon # => 'sad'
+ #
# By default you will only be able to set and update attributes on the
# associated model. If you want to destroy the associated model through the
# attributes hash, you have to enable it first using the
@@ -195,19 +209,27 @@ module ActiveRecord
# Nested attributes for an associated collection can also be passed in
# the form of a hash of hashes instead of an array of hashes:
#
- # Member.create(name: 'joe',
- # posts_attributes: { first: { title: 'Foo' },
- # second: { title: 'Bar' } })
+ # Member.create(
+ # name: 'joe',
+ # posts_attributes: {
+ # first: { title: 'Foo' },
+ # second: { title: 'Bar' }
+ # }
+ # )
#
# has the same effect as
#
- # Member.create(name: 'joe',
- # posts_attributes: [ { title: 'Foo' },
- # { title: 'Bar' } ])
+ # Member.create(
+ # name: 'joe',
+ # posts_attributes: [
+ # { title: 'Foo' },
+ # { title: 'Bar' }
+ # ]
+ # )
#
# The keys of the hash which is the value for +:posts_attributes+ are
# ignored in this case.
- # However, it is not allowed to use +'id'+ or +:id+ for one of
+ # However, it is not allowed to use <tt>'id'</tt> or <tt>:id</tt> for one of
# such keys, otherwise the hash will be wrapped in an array and
# interpreted as an attribute hash for a single post.
#
@@ -259,7 +281,7 @@ module ActiveRecord
# member.avatar_attributes = {icon: 'sad'}
# member.avatar.width # => 200
module ClassMethods
- REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |key, value| key == '_destroy' || value.blank? } }
+ REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |key, value| key == "_destroy" || value.blank? } }
# Defines an attributes writer for the specified association(s).
#
@@ -309,7 +331,7 @@ module ActiveRecord
# # creates avatar_attributes= and posts_attributes=
# accepts_nested_attributes_for :avatar, :posts, allow_destroy: true
def accepts_nested_attributes_for(*attr_names)
- options = { :allow_destroy => false, :update_only => false }
+ options = { allow_destroy: false, update_only: false }
options.update(attr_names.extract_options!)
options.assert_valid_keys(:allow_destroy, :reject_if, :limit, :update_only)
options[:reject_if] = REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank
@@ -333,27 +355,25 @@ module ActiveRecord
private
- # Generates a writer method for this association. Serves as a point for
- # accessing the objects in the association. For example, this method
- # could generate the following:
- #
- # def pirate_attributes=(attributes)
- # assign_nested_attributes_for_one_to_one_association(:pirate, attributes)
- # end
- #
- # This redirects the attempts to write objects in an association through
- # the helper methods defined below. Makes it seem like the nested
- # associations are just regular associations.
- def generate_association_writer(association_name, type)
- generated_association_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1
- if method_defined?(:#{association_name}_attributes=)
- remove_method(:#{association_name}_attributes=)
- end
- def #{association_name}_attributes=(attributes)
- assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes)
- end
- eoruby
- end
+ # Generates a writer method for this association. Serves as a point for
+ # accessing the objects in the association. For example, this method
+ # could generate the following:
+ #
+ # def pirate_attributes=(attributes)
+ # assign_nested_attributes_for_one_to_one_association(:pirate, attributes)
+ # end
+ #
+ # This redirects the attempts to write objects in an association through
+ # the helper methods defined below. Makes it seem like the nested
+ # associations are just regular associations.
+ def generate_association_writer(association_name, type)
+ generated_association_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1
+ silence_redefinition_of_method :#{association_name}_attributes=
+ def #{association_name}_attributes=(attributes)
+ assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes)
+ end
+ eoruby
+ end
end
# Returns ActiveRecord::AutosaveAssociation::marked_for_destruction? It's
@@ -367,192 +387,214 @@ module ActiveRecord
private
- # Attribute hash keys that should not be assigned as normal attributes.
- # These hash keys are nested attributes implementation details.
- UNASSIGNABLE_KEYS = %w( id _destroy )
-
- # Assigns the given attributes to the association.
- #
- # If an associated record does not yet exist, one will be instantiated. If
- # an associated record already exists, the method's behavior depends on
- # the value of the update_only option. If update_only is +false+ and the
- # given attributes include an <tt>:id</tt> that matches the existing record's
- # id, then the existing record will be modified. If no <tt>:id</tt> is provided
- # it will be replaced with a new record. If update_only is +true+ the existing
- # record will be modified regardless of whether an <tt>:id</tt> is provided.
- #
- # If the given attributes include a matching <tt>:id</tt> attribute, or
- # 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]
- attributes = attributes.with_indifferent_access
- existing_record = send(association_name)
-
- if (options[:update_only] || !attributes['id'].blank?) && existing_record &&
- (options[:update_only] || existing_record.id.to_s == attributes['id'].to_s)
- assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) unless call_reject_if(association_name, attributes)
-
- elsif attributes['id'].present?
- raise_nested_attributes_record_not_found!(association_name, attributes['id'])
-
- elsif !reject_new_record?(association_name, attributes)
- assignable_attributes = attributes.except(*UNASSIGNABLE_KEYS)
-
- if existing_record && existing_record.new_record?
- existing_record.assign_attributes(assignable_attributes)
- association(association_name).initialize_attributes(existing_record)
- else
- method = "build_#{association_name}"
- if respond_to?(method)
- send(method, assignable_attributes)
+ # Attribute hash keys that should not be assigned as normal attributes.
+ # These hash keys are nested attributes implementation details.
+ UNASSIGNABLE_KEYS = %w( id _destroy )
+
+ # Assigns the given attributes to the association.
+ #
+ # If an associated record does not yet exist, one will be instantiated. If
+ # an associated record already exists, the method's behavior depends on
+ # the value of the update_only option. If update_only is +false+ and the
+ # given attributes include an <tt>:id</tt> that matches the existing record's
+ # id, then the existing record will be modified. If no <tt>:id</tt> is provided
+ # it will be replaced with a new record. If update_only is +true+ the existing
+ # record will be modified regardless of whether an <tt>:id</tt> is provided.
+ #
+ # If the given attributes include a matching <tt>:id</tt> attribute, or
+ # 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 = nested_attributes_options[association_name]
+ if attributes.respond_to?(:permitted?)
+ attributes = attributes.to_h
+ end
+ attributes = attributes.with_indifferent_access
+ existing_record = send(association_name)
+
+ if (options[:update_only] || !attributes["id"].blank?) && existing_record &&
+ (options[:update_only] || existing_record.id.to_s == attributes["id"].to_s)
+ assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) unless call_reject_if(association_name, attributes)
+
+ elsif attributes["id"].present?
+ raise_nested_attributes_record_not_found!(association_name, attributes["id"])
+
+ elsif !reject_new_record?(association_name, attributes)
+ assignable_attributes = attributes.except(*UNASSIGNABLE_KEYS)
+
+ if existing_record && existing_record.new_record?
+ existing_record.assign_attributes(assignable_attributes)
+ association(association_name).initialize_attributes(existing_record)
else
- raise ArgumentError, "Cannot build association `#{association_name}'. Are you trying to build a polymorphic one-to-one association?"
+ method = :"build_#{association_name}"
+ if respond_to?(method)
+ send(method, assignable_attributes)
+ else
+ raise ArgumentError, "Cannot build association `#{association_name}'. Are you trying to build a polymorphic one-to-one association?"
+ end
end
end
end
- end
- # Assigns the given attributes to the collection association.
- #
- # Hashes with an <tt>:id</tt> value matching an existing associated record
- # will update that record. Hashes without an <tt>:id</tt> value will build
- # a new record for the association. Hashes with a matching <tt>:id</tt>
- # value and a <tt>:_destroy</tt> key set to a truthy value will mark the
- # matched record for destruction.
- #
- # For example:
- #
- # assign_nested_attributes_for_collection_association(:people, {
- # '1' => { id: '1', name: 'Peter' },
- # '2' => { name: 'John' },
- # '3' => { id: '2', _destroy: true }
- # })
- #
- # Will update the name of the Person with ID 1, build a new associated
- # person with the name 'John', and mark the associated Person with ID 2
- # for destruction.
- #
- # Also accepts an Array of attribute hashes:
- #
- # assign_nested_attributes_for_collection_association(:people, [
- # { id: '1', name: 'Peter' },
- # { name: 'John' },
- # { id: '2', _destroy: true }
- # ])
- def assign_nested_attributes_for_collection_association(association_name, attributes_collection)
- options = self.nested_attributes_options[association_name]
+ # Assigns the given attributes to the collection association.
+ #
+ # Hashes with an <tt>:id</tt> value matching an existing associated record
+ # will update that record. Hashes without an <tt>:id</tt> value will build
+ # a new record for the association. Hashes with a matching <tt>:id</tt>
+ # value and a <tt>:_destroy</tt> key set to a truthy value will mark the
+ # matched record for destruction.
+ #
+ # For example:
+ #
+ # assign_nested_attributes_for_collection_association(:people, {
+ # '1' => { id: '1', name: 'Peter' },
+ # '2' => { name: 'John' },
+ # '3' => { id: '2', _destroy: true }
+ # })
+ #
+ # Will update the name of the Person with ID 1, build a new associated
+ # person with the name 'John', and mark the associated Person with ID 2
+ # for destruction.
+ #
+ # Also accepts an Array of attribute hashes:
+ #
+ # assign_nested_attributes_for_collection_association(:people, [
+ # { id: '1', name: 'Peter' },
+ # { name: 'John' },
+ # { id: '2', _destroy: true }
+ # ])
+ def assign_nested_attributes_for_collection_association(association_name, attributes_collection)
+ options = nested_attributes_options[association_name]
+ if attributes_collection.respond_to?(:permitted?)
+ attributes_collection = attributes_collection.to_h
+ end
- unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
- raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
- end
+ unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
+ raise ArgumentError, "Hash or Array expected for attribute `#{association_name}`, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
+ end
- check_record_limit!(options[:limit], attributes_collection)
+ check_record_limit!(options[:limit], attributes_collection)
- if attributes_collection.is_a? Hash
- keys = attributes_collection.keys
- attributes_collection = if keys.include?('id') || keys.include?(:id)
- [attributes_collection]
- else
- attributes_collection.values
+ if attributes_collection.is_a? Hash
+ keys = attributes_collection.keys
+ attributes_collection = if keys.include?("id") || keys.include?(:id)
+ [attributes_collection]
+ else
+ attributes_collection.values
+ end
end
- end
- association = association(association_name)
+ association = association(association_name)
- existing_records = if association.loaded?
- association.target
- else
- attribute_ids = attributes_collection.map {|a| a['id'] || a[:id] }.compact
- attribute_ids.empty? ? [] : association.scope.where(association.klass.primary_key => attribute_ids)
- end
+ existing_records = if association.loaded?
+ association.target
+ else
+ attribute_ids = attributes_collection.map { |a| a["id"] || a[:id] }.compact
+ attribute_ids.empty? ? [] : association.scope.where(association.klass.primary_key => attribute_ids)
+ end
- attributes_collection.each do |attributes|
- attributes = attributes.with_indifferent_access
+ attributes_collection.each do |attributes|
+ if attributes.respond_to?(:permitted?)
+ attributes = attributes.to_h
+ end
+ attributes = attributes.with_indifferent_access
- if attributes['id'].blank?
- unless reject_new_record?(association_name, attributes)
- association.build(attributes.except(*UNASSIGNABLE_KEYS))
+ if attributes["id"].blank?
+ unless reject_new_record?(association_name, attributes)
+ association.reader.build(attributes.except(*UNASSIGNABLE_KEYS))
+ end
+ elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes["id"].to_s }
+ unless call_reject_if(association_name, attributes)
+ # Make sure we are operating on the actual object which is in the association's
+ # proxy_target array (either by finding it, or adding it if not found)
+ # Take into account that the proxy_target may have changed due to callbacks
+ target_record = association.target.detect { |record| record.id.to_s == attributes["id"].to_s }
+ if target_record
+ existing_record = target_record
+ else
+ association.add_to_target(existing_record, :skip_callbacks)
+ end
+
+ assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
+ end
+ else
+ raise_nested_attributes_record_not_found!(association_name, attributes["id"])
end
- elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s }
- unless call_reject_if(association_name, attributes)
- # Make sure we are operating on the actual object which is in the association's
- # proxy_target array (either by finding it, or adding it if not found)
- # Take into account that the proxy_target may have changed due to callbacks
- target_record = association.target.detect { |record| record.id.to_s == attributes['id'].to_s }
- if target_record
- existing_record = target_record
+ end
+ end
+
+ # Takes in a limit and checks if the attributes_collection has too many
+ # records. It accepts limit in the form of symbol, proc, or
+ # number-like object (anything that can be compared with an integer).
+ #
+ # Raises TooManyRecords error if the attributes_collection is
+ # larger than the limit.
+ def check_record_limit!(limit, attributes_collection)
+ if limit
+ limit = \
+ case limit
+ when Symbol
+ send(limit)
+ when Proc
+ limit.call
else
- association.add_to_target(existing_record, :skip_callbacks)
+ limit
end
- assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
+ if limit && attributes_collection.size > limit
+ raise TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead."
end
- else
- raise_nested_attributes_record_not_found!(association_name, attributes['id'])
end
end
- end
- # Takes in a limit and checks if the attributes_collection has too many
- # records. It accepts limit in the form of symbol, proc, or
- # number-like object (anything that can be compared with an integer).
- #
- # Raises TooManyRecords error if the attributes_collection is
- # larger than the limit.
- def check_record_limit!(limit, attributes_collection)
- if limit
- limit = case limit
- when Symbol
- send(limit)
- when Proc
- limit.call
- else
- limit
- end
+ # Updates a record with the +attributes+ or marks it for destruction if
+ # +allow_destroy+ is +true+ and has_destroy_flag? returns +true+.
+ def assign_to_or_mark_for_destruction(record, attributes, allow_destroy)
+ record.assign_attributes(attributes.except(*UNASSIGNABLE_KEYS))
+ record.mark_for_destruction if has_destroy_flag?(attributes) && allow_destroy
+ end
- if limit && attributes_collection.size > limit
- raise TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead."
- end
+ # Determines if a hash contains a truthy _destroy key.
+ def has_destroy_flag?(hash)
+ Type::Boolean.new.cast(hash["_destroy"])
end
- end
- # Updates a record with the +attributes+ or marks it for destruction if
- # +allow_destroy+ is +true+ and has_destroy_flag? returns +true+.
- def assign_to_or_mark_for_destruction(record, attributes, allow_destroy)
- record.assign_attributes(attributes.except(*UNASSIGNABLE_KEYS))
- record.mark_for_destruction if has_destroy_flag?(attributes) && allow_destroy
- end
+ # Determines if a new record should be rejected by checking
+ # has_destroy_flag? or if a <tt>:reject_if</tt> proc exists for this
+ # association and evaluates to +true+.
+ def reject_new_record?(association_name, attributes)
+ will_be_destroyed?(association_name, attributes) || call_reject_if(association_name, attributes)
+ end
- # Determines if a hash contains a truthy _destroy key.
- def has_destroy_flag?(hash)
- Type::Boolean.new.cast(hash['_destroy'])
- end
+ # Determines if a record with the particular +attributes+ should be
+ # rejected by calling the reject_if Symbol or Proc (if defined).
+ # The reject_if option is defined by +accepts_nested_attributes_for+.
+ #
+ # Returns false if there is a +destroy_flag+ on the attributes.
+ def call_reject_if(association_name, attributes)
+ return false if will_be_destroyed?(association_name, attributes)
- # Determines if a new record should be rejected by checking
- # has_destroy_flag? or if a <tt>:reject_if</tt> proc exists for this
- # association and evaluates to +true+.
- def reject_new_record?(association_name, attributes)
- has_destroy_flag?(attributes) || call_reject_if(association_name, attributes)
- end
+ case callback = nested_attributes_options[association_name][:reject_if]
+ when Symbol
+ method(callback).arity == 0 ? send(callback) : send(callback, attributes)
+ when Proc
+ callback.call(attributes)
+ end
+ end
- # Determines if a record with the particular +attributes+ should be
- # rejected by calling the reject_if Symbol or Proc (if defined).
- # The reject_if option is defined by +accepts_nested_attributes_for+.
- #
- # Returns false if there is a +destroy_flag+ on the attributes.
- def call_reject_if(association_name, attributes)
- return false if has_destroy_flag?(attributes)
- case callback = self.nested_attributes_options[association_name][:reject_if]
- when Symbol
- method(callback).arity == 0 ? send(callback) : send(callback, attributes)
- when Proc
- callback.call(attributes)
+ # Only take into account the destroy flag if <tt>:allow_destroy</tt> is true
+ def will_be_destroyed?(association_name, attributes)
+ allow_destroy?(association_name) && has_destroy_flag?(attributes)
end
- end
- def raise_nested_attributes_record_not_found!(association_name, record_id)
- raise RecordNotFound, "Couldn't find #{self.class._reflect_on_association(association_name).klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}"
- end
+ def allow_destroy?(association_name)
+ nested_attributes_options[association_name][:allow_destroy]
+ end
+
+ def raise_nested_attributes_record_not_found!(association_name, record_id)
+ model = self.class._reflect_on_association(association_name).klass.name
+ raise RecordNotFound.new("Couldn't find #{model} with ID=#{record_id} for #{self.class.name} with ID=#{id}",
+ model, "id", record_id)
+ end
end
end
diff --git a/activerecord/lib/active_record/no_touching.rb b/activerecord/lib/active_record/no_touching.rb
index edb5066fa0..697076bdae 100644
--- a/activerecord/lib/active_record/no_touching.rb
+++ b/activerecord/lib/active_record/no_touching.rb
@@ -1,10 +1,12 @@
+# frozen_string_literal: true
+
module ActiveRecord
# = Active Record No Touching
module NoTouching
extend ActiveSupport::Concern
module ClassMethods
- # Lets you selectively disable calls to `touch` for the
+ # Lets you selectively disable calls to +touch+ for the
# duration of a block.
#
# ==== Examples
@@ -41,10 +43,21 @@ module ActiveRecord
end
end
+ # Returns +true+ if the class has +no_touching+ set, +false+ otherwise.
+ #
+ # Project.no_touching do
+ # Project.first.no_touching? # true
+ # Message.first.no_touching? # false
+ # end
+ #
def no_touching?
NoTouching.applied_to?(self.class)
end
+ def touch_later(*) # :nodoc:
+ super unless no_touching?
+ end
+
def touch(*) # :nodoc:
super unless no_touching?
end
diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb
index 74894d0c37..cf0de0fdeb 100644
--- a/activerecord/lib/active_record/null_relation.rb
+++ b/activerecord/lib/active_record/null_relation.rb
@@ -1,16 +1,12 @@
-# -*- coding: utf-8 -*-
+# frozen_string_literal: true
module ActiveRecord
module NullRelation # :nodoc:
- def exec_queries
- @records = []
- end
-
def pluck(*column_names)
[]
end
- def delete_all(_conditions = nil)
+ def delete_all
0
end
@@ -22,10 +18,6 @@ module ActiveRecord
0
end
- def size
- calculate :size, nil
- end
-
def empty?
true
end
@@ -50,33 +42,12 @@ module ActiveRecord
""
end
- def count(*)
- calculate :count, nil
- end
-
- def sum(*)
- calculate :sum, nil
- end
-
- def average(*)
- calculate :average, nil
- end
-
- def minimum(*)
- calculate :minimum, nil
- end
-
- def maximum(*)
- calculate :maximum, nil
- end
-
def calculate(operation, _column_name)
- if [:count, :sum, :size].include? operation
+ case operation
+ when :count, :sum
group_values.any? ? Hash.new : 0
- elsif [:average, :minimum, :maximum].include?(operation) && group_values.any?
- Hash.new
- else
- nil
+ when :average, :minimum, :maximum
+ group_values.any? ? Hash.new : nil
end
end
@@ -87,5 +58,11 @@ module ActiveRecord
def or(other)
other.spawn
end
+
+ private
+
+ def exec_queries
+ @records = [].freeze
+ end
end
end
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index 0a6e4ac0bd..7bf8d568df 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
module ActiveRecord
- # = Active Record Persistence
+ # = Active Record \Persistence
module Persistence
extend ActiveSupport::Concern
@@ -61,15 +63,155 @@ module ActiveRecord
# +instantiate+ instead of +new+, finder methods ensure they get new
# instances of the appropriate class for each record.
#
- # See +ActiveRecord::Inheritance#discriminate_class_for_record+ to see
+ # See <tt>ActiveRecord::Inheritance#discriminate_class_for_record</tt> to see
# how this "single-table" inheritance mapping is implemented.
- def instantiate(attributes, column_types = {})
+ def instantiate(attributes, column_types = {}, &block)
klass = discriminate_class_for_record(attributes)
- attributes = klass.attributes_builder.build_from_database(attributes, column_types)
- klass.allocate.init_with('attributes' => attributes, 'new_record' => false)
+ instantiate_instance_of(klass, attributes, column_types, &block)
+ end
+
+ # Updates an object (or multiple objects) and saves it to the database, if validations pass.
+ # The resulting object is returned whether the object was saved successfully to the database or not.
+ #
+ # ==== Parameters
+ #
+ # * +id+ - This should be the id or an array of ids to be updated.
+ # * +attributes+ - This should be a hash of attributes or an array of hashes.
+ #
+ # ==== Examples
+ #
+ # # Updates one record
+ # Person.update(15, user_name: "Samuel", group: "expert")
+ #
+ # # Updates multiple records
+ # people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } }
+ # Person.update(people.keys, people.values)
+ #
+ # # Updates multiple records from the result of a relation
+ # people = Person.where(group: "expert")
+ # people.update(group: "masters")
+ #
+ # Note: Updating a large number of records will run an UPDATE
+ # query for each record, which may cause a performance issue.
+ # When running callbacks is not needed for each record update,
+ # it is preferred to use {update_all}[rdoc-ref:Relation#update_all]
+ # for updating all records in a single query.
+ def update(id, attributes)
+ if id.is_a?(Array)
+ id.map { |one_id| find(one_id) }.each_with_index { |object, idx|
+ object.update(attributes[idx])
+ }
+ else
+ if ActiveRecord::Base === id
+ raise ArgumentError,
+ "You are passing an instance of ActiveRecord::Base to `update`. " \
+ "Please pass the id of the object by calling `.id`."
+ end
+ object = find(id)
+ object.update(attributes)
+ object
+ end
+ end
+
+ # Destroy an object (or multiple objects) that has the given id. The object is instantiated first,
+ # therefore all callbacks and filters are fired off before the object is deleted. This method is
+ # less efficient than #delete but allows cleanup methods and other actions to be run.
+ #
+ # This essentially finds the object (or multiple objects) with the given id, creates a new object
+ # from the attributes, and then calls destroy on it.
+ #
+ # ==== Parameters
+ #
+ # * +id+ - This should be the id or an array of ids to be destroyed.
+ #
+ # ==== Examples
+ #
+ # # Destroy a single object
+ # Todo.destroy(1)
+ #
+ # # Destroy multiple objects
+ # todos = [1,2,3]
+ # Todo.destroy(todos)
+ def destroy(id)
+ if id.is_a?(Array)
+ find(id).each(&:destroy)
+ else
+ find(id).destroy
+ end
+ end
+
+ # Deletes the row with a primary key matching the +id+ argument, using a
+ # SQL +DELETE+ statement, and returns the number of rows deleted. Active
+ # Record objects are not instantiated, so the object's callbacks are not
+ # executed, including any <tt>:dependent</tt> association options.
+ #
+ # You can delete multiple rows at once by passing an Array of <tt>id</tt>s.
+ #
+ # Note: Although it is often much faster than the alternative, #destroy,
+ # skipping callbacks might bypass business logic in your application
+ # that ensures referential integrity or performs other essential jobs.
+ #
+ # ==== Examples
+ #
+ # # Delete a single row
+ # Todo.delete(1)
+ #
+ # # Delete multiple rows
+ # Todo.delete([2,3,4])
+ def delete(id_or_array)
+ where(primary_key => id_or_array).delete_all
+ end
+
+ def _insert_record(values) # :nodoc:
+ primary_key_value = nil
+
+ if primary_key && Hash === values
+ primary_key_value = values[primary_key]
+
+ if !primary_key_value && prefetch_primary_key?
+ primary_key_value = next_sequence_value
+ values[primary_key] = primary_key_value
+ end
+ end
+
+ if values.empty?
+ im = arel_table.compile_insert(connection.empty_insert_statement_value(primary_key))
+ im.into arel_table
+ else
+ im = arel_table.compile_insert(_substitute_values(values))
+ end
+
+ connection.insert(im, "#{self} Create", primary_key || false, primary_key_value)
+ end
+
+ def _update_record(values, constraints) # :nodoc:
+ constraints = _substitute_values(constraints).map { |attr, bind| attr.eq(bind) }
+
+ um = arel_table.where(
+ constraints.reduce(&:and)
+ ).compile_update(_substitute_values(values), primary_key)
+
+ connection.update(um, "#{self} Update")
+ end
+
+ def _delete_record(constraints) # :nodoc:
+ constraints = _substitute_values(constraints).map { |attr, bind| attr.eq(bind) }
+
+ dm = Arel::DeleteManager.new
+ dm.from(arel_table)
+ dm.wheres = constraints
+
+ connection.delete(dm, "#{self} Destroy")
end
private
+ # Given a class, an attributes hash, +instantiate_instance_of+ returns a
+ # new instance of the class. Accepts only keys as strings.
+ def instantiate_instance_of(klass, attributes, column_types = {}, &block)
+ attributes = klass.attributes_builder.build_from_database(attributes, column_types)
+ klass.allocate.init_with_attributes(attributes, &block)
+ end
+
# Called by +instantiate+ to decide which class to use for a new
# record instance.
#
@@ -78,6 +220,14 @@ module ActiveRecord
def discriminate_class_for_record(record)
self
end
+
+ def _substitute_values(values)
+ values.map do |name, value|
+ attr = arel_attribute(name)
+ bind = predicate_builder.build_bind_attribute(name, value)
+ [attr, bind]
+ end
+ end
end
# Returns true if this object hasn't been saved yet -- that is, a record
@@ -100,14 +250,18 @@ module ActiveRecord
!(@new_record || @destroyed)
end
+ ##
+ # :call-seq:
+ # save(*args)
+ #
# Saves the model.
#
- # If the model is new a record gets created in the database, otherwise
+ # If the model is new, a record gets created in the database, otherwise
# the existing record gets updated.
#
- # By default, save always run validations. If any of them fail the action
- # is cancelled and +save+ returns +false+. However, if you supply
- # validate: false, validations are bypassed altogether. See
+ # By default, save always runs validations. If any of them fail the action
+ # is cancelled and #save returns +false+, and the record won't be saved. However, if you supply
+ # <tt>validate: false</tt>, validations are bypassed altogether. See
# ActiveRecord::Validations for more information.
#
# By default, #save also sets the +updated_at+/+updated_on+ attributes to
@@ -121,20 +275,25 @@ module ActiveRecord
#
# Attributes marked as readonly are silently ignored if the record is
# being updated.
- def save(*args)
- create_or_update(*args)
+ def save(*args, &block)
+ create_or_update(*args, &block)
rescue ActiveRecord::RecordInvalid
false
end
+ ##
+ # :call-seq:
+ # save!(*args)
+ #
# Saves the model.
#
# If the model is new, a record gets created in the database, otherwise
# the existing record gets updated.
#
- # With <tt>save!</tt> validations always run. If any of them fail
- # ActiveRecord::RecordInvalid gets raised. See ActiveRecord::Validations
- # for more information.
+ # By default, #save! always runs validations. If any of them fail
+ # ActiveRecord::RecordInvalid gets raised, and the record won't be saved. However, if you supply
+ # <tt>validate: false</tt>, validations are bypassed altogether. See
+ # ActiveRecord::Validations for more information.
#
# By default, #save! also sets the +updated_at+/+updated_on+ attributes to
# the current time. However, if you supply <tt>touch: false</tt>, these
@@ -147,8 +306,10 @@ module ActiveRecord
#
# Attributes marked as readonly are silently ignored if the record is
# being updated.
- def save!(*args)
- create_or_update(*args) || raise(RecordNotSaved.new("Failed to save the record", self))
+ #
+ # Unless an error is raised, returns true.
+ def save!(*args, &block)
+ create_or_update(*args, &block) || raise(RecordNotSaved.new("Failed to save the record", self))
end
# Deletes the record in the database and freezes this instance to
@@ -158,13 +319,13 @@ module ActiveRecord
# The row is simply removed with an SQL +DELETE+ statement on the
# record's primary key, and no callbacks are executed.
#
- # Note that this will also delete records marked as <tt>readonly?</tt>.
+ # Note that this will also delete records marked as {#readonly?}[rdoc-ref:Core#readonly?].
#
# To enforce the object's +before_destroy+ and +after_destroy+
# callbacks or any <tt>:dependent</tt> association
# options, use <tt>#destroy</tt>.
def delete
- self.class.delete(id) if persisted?
+ _delete_row if persisted?
@destroyed = true
freeze
end
@@ -177,10 +338,14 @@ module ActiveRecord
# and #destroy returns +false+.
# See ActiveRecord::Callbacks for further details.
def destroy
- raise ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly?
+ _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
@@ -193,7 +358,7 @@ module ActiveRecord
# and #destroy! raises ActiveRecord::RecordNotDestroyed.
# See ActiveRecord::Callbacks for further details.
def destroy!
- destroy || raise(RecordNotDestroyed.new("Failed to destroy the record", self))
+ destroy || _raise_record_not_destroyed
end
# Returns an instance of the specified +klass+ with the attributes of the
@@ -207,18 +372,20 @@ module ActiveRecord
# Note: The new instance will share a link to the same attributes as the original class.
# Therefore the sti column value will still be the same.
# Any change to the attributes on either instance will affect both instances.
- # If you want to change the sti column as well, use +becomes!+ instead.
+ # If you want to change the sti column as well, use #becomes! instead.
def becomes(klass)
- became = klass.new
+ became = klass.allocate
+ became.send(:initialize)
became.instance_variable_set("@attributes", @attributes)
+ became.instance_variable_set("@mutations_from_database", @mutations_from_database ||= nil)
became.instance_variable_set("@changed_attributes", attributes_changed_by_setter)
became.instance_variable_set("@new_record", new_record?)
became.instance_variable_set("@destroyed", destroyed?)
- became.instance_variable_set("@errors", errors)
+ became.errors.copy!(errors)
became
end
- # Wrapper around +becomes+ that also changes the instance's sti column value.
+ # Wrapper around #becomes that also changes the instance's sti column value.
# This is especially useful if you want to persist the changed class in your
# database.
#
@@ -238,19 +405,20 @@ module ActiveRecord
# This is especially useful for boolean flags on existing records. Also note that
#
# * Validation is skipped.
- # * Callbacks are invoked.
+ # * \Callbacks are invoked.
# * updated_at/updated_on column is updated if that column is available.
# * Updates all the attributes that are dirty in this object.
#
- # This method raises an +ActiveRecord::ActiveRecordError+ if the
+ # This method raises an ActiveRecord::ActiveRecordError if the
# attribute is marked as readonly.
#
- # See also +update_column+.
+ # Also see #update_column.
def update_attribute(name, value)
name = name.to_s
verify_readonly_attribute(name)
public_send("#{name}=", value)
- save(validate: false) if changed?
+
+ save(validate: false)
end
# Updates the attributes of the model from the passed-in hash and saves the
@@ -266,9 +434,10 @@ module ActiveRecord
end
alias update_attributes update
+ deprecate :update_attributes
- # Updates its receiver just like +update+ but calls <tt>save!</tt> instead
- # of +save+, so an exception is raised if the record is invalid.
+ # Updates its receiver just like #update but calls #save! instead
+ # of +save+, so an exception is raised if the record is invalid and saving will fail.
def update!(attributes)
# The following transaction covers any possible database side-effects of the
# attributes assignment. For example, setting the IDs of a child collection.
@@ -279,6 +448,7 @@ module ActiveRecord
end
alias update_attributes! update!
+ deprecate :update_attributes!
# Equivalent to <code>update_columns(name => value)</code>.
def update_column(name, value)
@@ -294,11 +464,12 @@ module ActiveRecord
# the database, but take into account that in consequence the regular update
# procedures are totally bypassed. In particular:
#
- # * Validations are skipped.
- # * Callbacks are skipped.
+ # * \Validations are skipped.
+ # * \Callbacks are skipped.
# * +updated_at+/+updated_on+ are not updated.
+ # * However, attributes are serialized with the same rules as ActiveRecord::Relation#update_all
#
- # This method raises an +ActiveRecord::ActiveRecordError+ when called on new
+ # This method raises an ActiveRecord::ActiveRecordError when called on new
# objects, or when at least one of the attributes is marked as readonly.
def update_columns(attributes)
raise ActiveRecordError, "cannot update a new record" if new_record?
@@ -308,13 +479,17 @@ module ActiveRecord
verify_readonly_attribute(key.to_s)
end
- updated_count = self.class.unscoped.where(self.class.primary_key => id).update_all(attributes)
-
+ id_in_database = self.id_in_database
attributes.each do |k, v|
- raw_write_attribute(k, v)
+ write_attribute_without_type_cast(k, v)
end
- updated_count == 1
+ affected_rows = self.class._update_record(
+ attributes,
+ self.class.primary_key => id_in_database
+ )
+
+ affected_rows == 1
end
# Initializes +attribute+ to zero if +nil+ and adds the value passed as +by+ (default is 1).
@@ -326,42 +501,56 @@ 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)
- increment(attribute, by).update_attribute(attribute, self[attribute])
+ # 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, touch: touch)
+ clear_attribute_change(attribute) # eww
+ self
end
# Initializes +attribute+ to zero if +nil+ and subtracts the value passed as +by+ (default is 1).
# The decrement is performed directly on the underlying attribute, no setter is invoked.
# Only makes sense for number-based attributes. Returns +self+.
def decrement(attribute, by = 1)
- self[attribute] ||= 0
- self[attribute] -= by
- self
+ increment(attribute, -by)
end
- # Wrapper around +decrement+ 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 decrement!(attribute, by = 1)
- decrement(attribute, by).update_attribute(attribute, self[attribute])
+ # 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
# if the predicate returns +true+ the attribute will become +false+. This
# method toggles directly the underlying value without calling any setter.
# Returns +self+.
+ #
+ # Example:
+ #
+ # user = User.first
+ # user.banned? # => false
+ # user.toggle(:banned)
+ # user.banned? # => true
+ #
def toggle(attribute)
self[attribute] = !public_send("#{attribute}?")
self
end
- # Wrapper around +toggle+ that saves the record. This method differs from
- # its non-bang version in that it passes through the attribute setter.
+ # Wrapper around #toggle 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 toggle!(attribute)
@@ -370,8 +559,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>
@@ -383,7 +572,7 @@ module ActiveRecord
# Attributes are reloaded from the database, and caches busted, in
# particular the associations cache and the QueryCache.
#
- # If the record no longer exists in the database <tt>ActiveRecord::RecordNotFound</tt>
+ # If the record no longer exists in the database ActiveRecord::RecordNotFound
# is raised. Otherwise, in addition to the in-place modification the method
# returns +self+ for convenience.
#
@@ -426,7 +615,7 @@ module ActiveRecord
self.class.unscoped { self.class.find(id) }
end
- @attributes = fresh_object.instance_variable_get('@attributes')
+ @attributes = fresh_object.instance_variable_get("@attributes")
@new_record = false
self
end
@@ -445,8 +634,8 @@ module ActiveRecord
# product.touch(:designed_at) # updates the designed_at attribute and updated_at/on
# product.touch(:started_at, :ended_at) # updates started_at, ended_at and updated_at/on attributes
#
- # If used along with +belongs_to+ then +touch+ will invoke +touch+ method on
- # associated object.
+ # If used along with {belongs_to}[rdoc-ref:Associations::ClassMethods#belongs_to]
+ # then +touch+ will invoke +touch+ method on associated object.
#
# class Brake < ActiveRecord::Base
# belongs_to :car, touch: true
@@ -466,37 +655,19 @@ module ActiveRecord
# ball.touch(:updated_at) # => raises ActiveRecordError
#
def touch(*names, time: nil)
- raise ActiveRecordError, "cannot touch on a new record object" unless persisted?
-
- time ||= current_time_from_proper_timezone
- attributes = timestamp_attributes_for_update_in_model
- attributes.concat(names)
-
- unless attributes.empty?
- changes = {}
-
- attributes.each do |column|
- column = column.to_s
- changes[column] = write_attribute(column, time)
- end
-
- clear_attribute_changes(changes.keys)
- primary_key = self.class.primary_key
- scope = self.class.unscoped.where(primary_key => _read_attribute(primary_key))
-
- if locking_enabled?
- locking_column = self.class.locking_column
- scope = scope.where(locking_column => _read_attribute(locking_column))
- changes[locking_column] = increment_lock
- end
-
- result = scope.update_all(changes) == 1
+ unless persisted?
+ raise ActiveRecordError, <<-MSG.squish
+ cannot touch on a new or destroyed record object. Consider using
+ persisted?, new_record?, or destroyed? before touching
+ MSG
+ end
- if !result && locking_enabled?
- raise ActiveRecord::StaleObjectError.new(self, "touch")
- end
+ attribute_names = timestamp_attributes_for_update_in_model
+ attribute_names |= names.map(&:to_s)
- result
+ unless attribute_names.empty?
+ affected_rows = _touch_row(attribute_names, time)
+ @_trigger_update_callback = affected_rows == 1
else
true
end
@@ -509,44 +680,93 @@ module ActiveRecord
end
def destroy_row
- relation_for_destroy.delete_all
+ _delete_row
end
- def relation_for_destroy
- self.class.unscoped.where(self.class.primary_key => id)
+ def _delete_row
+ self.class._delete_record(self.class.primary_key => id_in_database)
end
- def create_or_update(*args)
- raise ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly?
- result = new_record? ? _create_record : _update_record(*args)
+ def _touch_row(attribute_names, time)
+ time ||= current_time_from_proper_timezone
+
+ attribute_names.each do |attr_name|
+ write_attribute(attr_name, time)
+ clear_attribute_change(attr_name)
+ end
+
+ _update_row(attribute_names, "touch")
+ end
+
+ def _update_row(attribute_names, attempted_action = "update")
+ self.class._update_record(
+ attributes_with_values(attribute_names),
+ self.class.primary_key => id_in_database
+ )
+ end
+
+ def create_or_update(*args, &block)
+ _raise_readonly_record_error if readonly?
+ return false if destroyed?
+ result = new_record? ? _create_record(&block) : _update_record(*args, &block)
result != false
end
# Updates the associated record with values matching those of the instance attributes.
# Returns the number of affected rows.
def _update_record(attribute_names = self.attribute_names)
- attributes_values = arel_attributes_with_values_for_update(attribute_names)
- if attributes_values.empty?
- 0
+ attribute_names = attributes_for_update(attribute_names)
+
+ if attribute_names.empty?
+ affected_rows = 0
+ @_trigger_update_callback = true
else
- self.class.unscoped._update_record attributes_values, id, id_was
+ affected_rows = _update_row(attribute_names)
+ @_trigger_update_callback = affected_rows == 1
end
+
+ yield(self) if block_given?
+
+ affected_rows
end
# Creates a record with values matching those of the instance attributes
# and returns its id.
def _create_record(attribute_names = self.attribute_names)
- attributes_values = arel_attributes_with_values_for_create(attribute_names)
+ attribute_names = attributes_for_create(attribute_names)
+
+ new_id = self.class._insert_record(
+ attributes_with_values(attribute_names)
+ )
- new_id = self.class.unscoped.insert attributes_values
self.id ||= new_id if self.class.primary_key
@new_record = false
+
+ yield(self) if block_given?
+
id
end
def verify_readonly_attribute(name)
raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name)
end
+
+ def _raise_record_not_destroyed
+ @_association_destroy_exception ||= nil
+ raise @_association_destroy_exception || RecordNotDestroyed.new("Failed to destroy the record", self)
+ ensure
+ @_association_destroy_exception = nil
+ end
+
+ # The name of the method used to touch a +belongs_to+ association when the
+ # +:touch+ option is used.
+ def belongs_to_touch_method
+ :touch
+ end
+
+ def _raise_readonly_record_error
+ raise ReadOnlyRecord, "#{self.class} is marked as readonly"
+ end
end
end
diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb
index dcb2bd3d84..28194c7c46 100644
--- a/activerecord/lib/active_record/query_cache.rb
+++ b/activerecord/lib/active_record/query_cache.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
# = Active Record Query Cache
class QueryCache
@@ -5,7 +7,7 @@ module ActiveRecord
# Enable the query cache within the block if Active Record is configured.
# If it's not, it will execute the given block.
def cache(&block)
- if ActiveRecord::Base.connected?
+ if connected? || !configurations.empty?
connection.cache(&block)
else
yield
@@ -15,7 +17,7 @@ module ActiveRecord
# Disable the query cache within the block if Active Record is configured.
# If it's not, it will execute the given block.
def uncached(&block)
- if ActiveRecord::Base.connected?
+ if connected? || !configurations.empty?
connection.uncached(&block)
else
yield
@@ -23,34 +25,21 @@ module ActiveRecord
end
end
- def initialize(app)
- @app = app
+ def self.run
+ ActiveRecord::Base.connection_handler.connection_pool_list.
+ reject { |p| p.query_cache_enabled }.each { |p| p.enable_query_cache! }
end
- def call(env)
- connection = ActiveRecord::Base.connection
- enabled = connection.query_cache_enabled
- connection_id = ActiveRecord::Base.connection_id
- connection.enable_query_cache!
+ def self.complete(pools)
+ pools.each { |pool| pool.disable_query_cache! }
- response = @app.call(env)
- response[2] = Rack::BodyProxy.new(response[2]) do
- restore_query_cache_settings(connection_id, enabled)
+ ActiveRecord::Base.connection_handler.connection_pool_list.each do |pool|
+ pool.release_connection if pool.active_connection? && !pool.connection.transaction_open?
end
-
- response
- rescue Exception => e
- restore_query_cache_settings(connection_id, enabled)
- raise e
end
- private
-
- def restore_query_cache_settings(connection_id, enabled)
- ActiveRecord::Base.connection_id = connection_id
- ActiveRecord::Base.connection.clear_query_cache
- ActiveRecord::Base.connection.disable_query_cache! unless enabled
+ def self.install_executor_hooks(executor = ActiveSupport::Executor)
+ executor.register_hook(self)
end
-
end
end
diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb
index 4e597590e9..c84f3d0fbb 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -1,17 +1,19 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Querying
- delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, to: :all
- delegate :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, to: :all
+ delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :none?, :one?, to: :all
+ delegate :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, :third_to_last, :third_to_last!, :second_to_last, :second_to_last!, to: :all
delegate :first_or_create, :first_or_create!, :first_or_initialize, to: :all
- delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, to: :all
+ delegate :find_or_create_by, :find_or_create_by!, :create_or_find_by, :create_or_find_by!, :find_or_initialize_by, to: :all
delegate :find_by, :find_by!, to: :all
- delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, to: :all
- delegate :find_each, :find_in_batches, to: :all
- delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :or,
- :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly,
- :having, :create_with, :uniq, :distinct, :references, :none, :unscope, to: :all
+ delegate :destroy_all, :delete_all, :update_all, to: :all
+ 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, :extending,
+ :having, :create_with, :distinct, :references, :none, :unscope, :merge, to: :all
delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all
- delegate :pluck, :ids, to: :all
+ delegate :pluck, :pick, :ids, to: :all
# Executes a custom SQL query against your database and returns all the results. The results will
# be returned as an array with columns requested encapsulated as attributes of the model you call
@@ -35,8 +37,8 @@ module ActiveRecord
#
# Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date]
# Post.find_by_sql ["SELECT body FROM comments WHERE author = :user_id OR approved_by = :user_id", { :user_id => user_id }]
- def find_by_sql(sql, binds = [])
- result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds)
+ def find_by_sql(sql, binds = [], preparable: nil, &block)
+ result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds, preparable: preparable)
column_types = result_set.column_types.dup
columns_hash.each_key { |k| column_types.delete k }
message_bus = ActiveSupport::Notifications.instrumenter
@@ -46,8 +48,13 @@ module ActiveRecord
class_name: name
}
- message_bus.instrument('instantiation.active_record', payload) do
- result_set.map { |record| instantiate(record, column_types) }
+ message_bus.instrument("instantiation.active_record", payload) do
+ if result_set.includes_column?(inheritance_column)
+ result_set.map { |record| instantiate(record, column_types, &block) }
+ else
+ # Instantiate a homogeneous set
+ result_set.map { |record| instantiate_instance_of(self, record, column_types, &block) }
+ end
end
end
@@ -62,8 +69,7 @@ module ActiveRecord
#
# * +sql+ - An SQL statement which should return a count query from the database, see the example above.
def count_by_sql(sql)
- sql = sanitize_conditions(sql)
- connection.select_value(sql, "#{name} Count").to_i
+ connection.select_value(sanitize_sql(sql), "#{name} Count").to_i
end
end
end
diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb
index da6b8447d3..538659d6bd 100644
--- a/activerecord/lib/active_record/railtie.rb
+++ b/activerecord/lib/active_record/railtie.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
require "active_record"
require "rails"
require "active_model/railtie"
# For now, action_controller must always be present with
-# rails, so let's make sure that it gets required before
+# Rails, so let's make sure that it gets required before
# here. This is needed for correctly setting up the middleware.
# In the future, this might become an optional require.
require "action_controller/railtie"
@@ -13,26 +15,22 @@ module ActiveRecord
class Railtie < Rails::Railtie # :nodoc:
config.active_record = ActiveSupport::OrderedOptions.new
- config.app_generators.orm :active_record, :migration => true,
- :timestamps => true
-
- config.app_middleware.insert_after "::ActionDispatch::Callbacks",
- "ActiveRecord::QueryCache"
-
- config.app_middleware.insert_after "::ActionDispatch::Callbacks",
- "ActiveRecord::ConnectionAdapters::ConnectionManagement"
+ config.app_generators.orm :active_record, migration: true,
+ timestamps: true
config.action_dispatch.rescue_responses.merge!(
- 'ActiveRecord::RecordNotFound' => :not_found,
- 'ActiveRecord::StaleObjectError' => :conflict,
- 'ActiveRecord::RecordInvalid' => :unprocessable_entity,
- 'ActiveRecord::RecordNotSaved' => :unprocessable_entity
+ "ActiveRecord::RecordNotFound" => :not_found,
+ "ActiveRecord::StaleObjectError" => :conflict,
+ "ActiveRecord::RecordInvalid" => :unprocessable_entity,
+ "ActiveRecord::RecordNotSaved" => :unprocessable_entity
)
-
config.active_record.use_schema_cache_dump = true
config.active_record.maintain_test_schema = true
+ config.active_record.sqlite3 = ActiveSupport::OrderedOptions.new
+ config.active_record.sqlite3.represent_boolean_as_integer = nil
+
config.eager_load_namespaces << ActiveRecord
rake_tasks do
@@ -40,9 +38,9 @@ module ActiveRecord
task :load_config do
ActiveRecord::Tasks::DatabaseTasks.database_configuration = Rails.application.config.database_configuration
- if defined?(ENGINE_PATH) && engine = Rails::Engine.find(ENGINE_PATH)
- if engine.paths['db/migrate'].existent
- ActiveRecord::Tasks::DatabaseTasks.migrations_paths += engine.paths['db/migrate'].to_a
+ if defined?(ENGINE_ROOT) && engine = Rails::Engine.find(ENGINE_ROOT)
+ if engine.paths["db/migrate"].existent
+ ActiveRecord::Tasks::DatabaseTasks.migrations_paths += engine.paths["db/migrate"].to_a
end
end
end
@@ -57,8 +55,11 @@ module ActiveRecord
console do |app|
require "active_record/railties/console_sandbox" if app.sandbox?
require "active_record/base"
- console = ActiveSupport::Logger.new(STDERR)
- Rails.logger.extend ActiveSupport::Logger.broadcast console
+ unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDERR, STDOUT)
+ console = ActiveSupport::Logger.new(STDERR)
+ Rails.logger.extend ActiveSupport::Logger.broadcast console
+ end
+ ActiveRecord::Base.verbose_query_logs = false
end
runner do
@@ -76,10 +77,39 @@ module ActiveRecord
ActiveSupport.on_load(:active_record) { self.logger ||= ::Rails.logger }
end
+ initializer "active_record.backtrace_cleaner" do
+ ActiveSupport.on_load(:active_record) { LogSubscriber.backtrace_cleaner = ::Rails.backtrace_cleaner }
+ end
+
initializer "active_record.migration_error" do
if config.active_record.delete(:migration_error) == :page_load
- config.app_middleware.insert_after "::ActionDispatch::Callbacks",
- "ActiveRecord::Migration::CheckPending"
+ config.app_middleware.insert_after ::ActionDispatch::Callbacks,
+ ActiveRecord::Migration::CheckPending
+ end
+ end
+
+ initializer "Check for cache versioning support" do
+ config.after_initialize do |app|
+ ActiveSupport.on_load(:active_record) do
+ if app.config.active_record.cache_versioning && Rails.cache
+ unless Rails.cache.class.try(:supports_cache_versioning?)
+ raise <<-end_error
+
+You're using a cache store that doesn't support native cache versioning.
+Your best option is to upgrade to a newer version of #{Rails.cache.class}
+that supports cache versioning (#{Rails.cache.class}.supports_cache_versioning? #=> true).
+
+Next best, switch to a different cache store that does support cache versioning:
+https://guides.rubyonrails.org/caching_with_rails.html#cache-stores.
+
+To keep using the current cache store, you can turn off cache versioning entirely:
+
+ config.active_record.cache_versioning = false
+
+end_error
+ end
+ end
+ end
end
end
@@ -87,15 +117,19 @@ 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
- if cache.version == ActiveRecord::Migrator.current_version
- self.connection.schema_cache = cache
- self.connection_pool.schema_cache = cache.dup
+ current_version = ActiveRecord::Migrator.current_version
+
+ next if current_version.nil?
+
+ cache = YAML.load(File.read(filename))
+ if cache.version == current_version
+ 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 #{current_version}, but the one in the cache is #{cache.version}."
end
end
end
@@ -103,17 +137,27 @@ module ActiveRecord
end
end
+ initializer "active_record.define_attribute_methods" do |app|
+ config.after_initialize do
+ ActiveSupport.on_load(:active_record) do
+ descendants.each(&:define_attribute_methods) if app.config.eager_load
+ end
+ end
+ end
+
initializer "active_record.warn_on_records_fetched_greater_than" do
if config.active_record.warn_on_records_fetched_greater_than
ActiveSupport.on_load(:active_record) do
- require 'active_record/relation/record_fetch_warning'
+ require "active_record/relation/record_fetch_warning"
end
end
end
initializer "active_record.set_configs" do |app|
ActiveSupport.on_load(:active_record) do
- app.config.active_record.each do |k,v|
+ configs = app.config.active_record.dup
+ configs.delete(:sqlite3)
+ configs.each do |k, v|
send "#{k}=", v
end
end
@@ -121,24 +165,10 @@ module ActiveRecord
# This sets the database configuration from Configuration#database_configuration
# and then establishes the connection.
- initializer "active_record.initialize_database" do |app|
+ initializer "active_record.initialize_database" do
ActiveSupport.on_load(:active_record) do
self.configurations = Rails.application.config.database_configuration
-
- begin
- establish_connection
- rescue ActiveRecord::NoDatabaseError
- warn <<-end_warning
-Oops - You have a database configured, but it doesn't exist yet!
-
-Here's how to get started:
-
- 1. Configure your database in config/database.yml.
- 2. Run `bin/rake db:create` to create the database.
- 3. Run `bin/rake db:setup` to load your database schema.
-end_warning
- raise
- end
+ establish_connection
end
end
@@ -150,11 +180,16 @@ end_warning
end
end
- initializer "active_record.set_reloader_hooks" do |app|
- hook = app.config.reload_classes_only_on_change ? :to_prepare : :to_cleanup
+ initializer "active_record.collection_cache_association_loading" do
+ require "active_record/railties/collection_cache_association_loading"
+ ActiveSupport.on_load(:action_view) do
+ ActionView::PartialRenderer.prepend(ActiveRecord::Railties::CollectionCacheAssociationLoading)
+ end
+ end
+ initializer "active_record.set_reloader_hooks" do
ActiveSupport.on_load(:active_record) do
- ActionDispatch::Reloader.send(hook) do
+ ActiveSupport::Reloader.before_class_unload do
if ActiveRecord::Base.connected?
ActiveRecord::Base.clear_cache!
ActiveRecord::Base.clear_reloadable_connections!
@@ -163,9 +198,65 @@ end_warning
end
end
+ initializer "active_record.set_executor_hooks" do
+ ActiveRecord::QueryCache.install_executor_hooks
+ end
+
initializer "active_record.add_watchable_files" do |app|
path = app.paths["db"].first
config.watchable_files.concat ["#{path}/schema.rb", "#{path}/structure.sql"]
end
+
+ initializer "active_record.clear_active_connections" do
+ config.after_initialize do
+ ActiveSupport.on_load(:active_record) do
+ # Ideally the application doesn't connect to the database during boot,
+ # but sometimes it does. In case it did, we want to empty out the
+ # connection pools so that a non-database-using process (e.g. a master
+ # process in a forking server model) doesn't retain a needless
+ # connection. If it was needed, the incremental cost of reestablishing
+ # this connection is trivial: the rest of the pool would need to be
+ # populated anyway.
+
+ clear_active_connections!
+ flush_idle_connections!
+ end
+ end
+ end
+
+ initializer "active_record.check_represent_sqlite3_boolean_as_integer" do
+ config.after_initialize do
+ ActiveSupport.on_load(:active_record_sqlite3adapter) do
+ represent_boolean_as_integer = Rails.application.config.active_record.sqlite3.delete(:represent_boolean_as_integer)
+ unless represent_boolean_as_integer.nil?
+ ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = represent_boolean_as_integer
+ end
+
+ unless ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer
+ ActiveSupport::Deprecation.warn <<-MSG
+Leaving `ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer`
+set to false is deprecated. SQLite databases have used 't' and 'f' to serialize
+boolean values and must have old data converted to 1 and 0 (its native boolean
+serialization) before setting this flag to true. Conversion can be accomplished
+by setting up a rake task which runs
+
+ ExampleModel.where("boolean_column = 't'").update_all(boolean_column: 1)
+ ExampleModel.where("boolean_column = 'f'").update_all(boolean_column: 0)
+
+for all models and all boolean columns, after which the flag must be set to
+true by adding the following to your application.rb file:
+
+ Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true
+MSG
+ end
+ end
+ end
+ end
+
+ initializer "active_record.set_filter_attributes" do
+ ActiveSupport.on_load(:active_record) do
+ self.filter_attributes += Rails.application.config.filter_parameters
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/railties/collection_cache_association_loading.rb b/activerecord/lib/active_record/railties/collection_cache_association_loading.rb
new file mode 100644
index 0000000000..b5129e4239
--- /dev/null
+++ b/activerecord/lib/active_record/railties/collection_cache_association_loading.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module Railties # :nodoc:
+ module CollectionCacheAssociationLoading #:nodoc:
+ def setup(context, options, block)
+ @relation = relation_from_options(options)
+
+ super
+ end
+
+ def relation_from_options(cached: nil, partial: nil, collection: nil, **_)
+ return unless cached
+
+ relation = partial if partial.is_a?(ActiveRecord::Relation)
+ relation ||= collection if collection.is_a?(ActiveRecord::Relation)
+
+ if relation && !relation.loaded?
+ relation.skip_preloading!
+ end
+ end
+
+ def collection_without_template
+ @relation.preload_associations(@collection) if @relation
+ super
+ end
+
+ def collection_with_template
+ @relation.preload_associations(@collection) if @relation
+ super
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/railties/console_sandbox.rb b/activerecord/lib/active_record/railties/console_sandbox.rb
index 604a220303..8917638a5d 100644
--- a/activerecord/lib/active_record/railties/console_sandbox.rb
+++ b/activerecord/lib/active_record/railties/console_sandbox.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
ActiveRecord::Base.connection.begin_transaction(joinable: false)
at_exit do
diff --git a/activerecord/lib/active_record/railties/controller_runtime.rb b/activerecord/lib/active_record/railties/controller_runtime.rb
index 8727e46cb3..309441a057 100644
--- a/activerecord/lib/active_record/railties/controller_runtime.rb
+++ b/activerecord/lib/active_record/railties/controller_runtime.rb
@@ -1,50 +1,51 @@
-require 'active_support/core_ext/module/attr_internal'
-require 'active_record/log_subscriber'
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/attr_internal"
+require "active_record/log_subscriber"
module ActiveRecord
module Railties # :nodoc:
module ControllerRuntime #:nodoc:
extend ActiveSupport::Concern
- protected
-
- attr_internal :db_runtime
-
- def process_action(action, *args)
- # We also need to reset the runtime before each action
- # because of queries in middleware or in cases we are streaming
- # and it won't be cleaned up by the method below.
- ActiveRecord::LogSubscriber.reset_runtime
- super
+ module ClassMethods # :nodoc:
+ def log_process_action(payload)
+ messages, db_runtime = super, payload[:db_runtime]
+ messages << ("ActiveRecord: %.1fms" % db_runtime.to_f) if db_runtime
+ messages
+ end
end
- def cleanup_view_runtime
- if logger.info? && ActiveRecord::Base.connected?
- db_rt_before_render = ActiveRecord::LogSubscriber.reset_runtime
- self.db_runtime = (db_runtime || 0) + db_rt_before_render
- runtime = super
- db_rt_after_render = ActiveRecord::LogSubscriber.reset_runtime
- self.db_runtime += db_rt_after_render
- runtime - db_rt_after_render
- else
+ private
+ attr_internal :db_runtime
+
+ def process_action(action, *args)
+ # We also need to reset the runtime before each action
+ # because of queries in middleware or in cases we are streaming
+ # and it won't be cleaned up by the method below.
+ ActiveRecord::LogSubscriber.reset_runtime
super
end
- end
- def append_info_to_payload(payload)
- super
- if ActiveRecord::Base.connected?
- payload[:db_runtime] = (db_runtime || 0) + ActiveRecord::LogSubscriber.reset_runtime
+ def cleanup_view_runtime
+ if logger && logger.info? && ActiveRecord::Base.connected?
+ db_rt_before_render = ActiveRecord::LogSubscriber.reset_runtime
+ self.db_runtime = (db_runtime || 0) + db_rt_before_render
+ runtime = super
+ db_rt_after_render = ActiveRecord::LogSubscriber.reset_runtime
+ self.db_runtime += db_rt_after_render
+ runtime - db_rt_after_render
+ else
+ super
+ end
end
- end
- module ClassMethods # :nodoc:
- def log_process_action(payload)
- messages, db_runtime = super, payload[:db_runtime]
- messages << ("ActiveRecord: %.1fms" % db_runtime.to_f) if db_runtime
- messages
+ def append_info_to_payload(payload)
+ super
+ if ActiveRecord::Base.connected?
+ payload[:db_runtime] = (db_runtime || 0) + ActiveRecord::LogSubscriber.reset_runtime
+ end
end
- end
end
end
end
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index 6a72d528b4..475baa7559 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -1,48 +1,83 @@
-require 'active_record'
+# frozen_string_literal: true
+
+require "active_record"
db_namespace = namespace :db do
- task :load_config do
+ desc "Set the environment value for the database"
+ task "environment:set" => :load_config do
+ ActiveRecord::InternalMetadata.create_table
+ ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Base.connection.migration_context.current_environment
+ end
+
+ task check_protected_environments: :load_config do
+ ActiveRecord::Tasks::DatabaseTasks.check_protected_environments!
+ end
+
+ task load_config: :environment do
ActiveRecord::Base.configurations = ActiveRecord::Tasks::DatabaseTasks.database_configuration || {}
ActiveRecord::Migrator.migrations_paths = ActiveRecord::Tasks::DatabaseTasks.migrations_paths
end
namespace :create do
- task :all => :load_config do
+ task all: :load_config do
ActiveRecord::Tasks::DatabaseTasks.create_all
end
+
+ ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
+ desc "Create #{spec_name} database for current environment"
+ task spec_name => :load_config do
+ db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name)
+ ActiveRecord::Tasks::DatabaseTasks.create(db_config.config)
+ end
+ end
end
- desc 'Creates the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:create:all to create all databases in the config). Without RAILS_ENV, it defaults to creating the development and test databases.'
- task :create => [:load_config] do
+ desc "Creates the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:create:all to create all databases in the config). Without RAILS_ENV or when RAILS_ENV is development, it defaults to creating the development and test databases."
+ task create: [:load_config] do
ActiveRecord::Tasks::DatabaseTasks.create_current
end
namespace :drop do
- task :all => :load_config do
+ task all: [:load_config, :check_protected_environments] do
ActiveRecord::Tasks::DatabaseTasks.drop_all
end
+
+ ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
+ desc "Drop #{spec_name} database for current environment"
+ task spec_name => [:load_config, :check_protected_environments] do
+ db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name)
+ ActiveRecord::Tasks::DatabaseTasks.drop(db_config.config)
+ end
+ end
+ end
+
+ desc "Drops the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV or when RAILS_ENV is development, it defaults to dropping the development and test databases."
+ task drop: [:load_config, :check_protected_environments] do
+ db_namespace["drop:_unsafe"].invoke
end
- desc 'Drops the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV, it defaults to dropping the development and test databases.'
- task :drop => [:load_config] do
+ task "drop:_unsafe" => [:load_config] do
ActiveRecord::Tasks::DatabaseTasks.drop_current
end
namespace :purge do
- task :all => :load_config do
+ task all: [:load_config, :check_protected_environments] do
ActiveRecord::Tasks::DatabaseTasks.purge_all
end
end
- # desc "Empty the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV it defaults to purging the development and test databases."
- task :purge => [:load_config] do
+ # desc "Empty the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:purge:all to purge all databases in the config). Without RAILS_ENV it defaults to purging the development and test databases."
+ task purge: [:load_config, :check_protected_environments] do
ActiveRecord::Tasks::DatabaseTasks.purge_current
end
desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)."
- task :migrate => [:environment, :load_config] do
- ActiveRecord::Tasks::DatabaseTasks.migrate
- db_namespace['_dump'].invoke
+ task migrate: :load_config do
+ ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
+ ActiveRecord::Base.establish_connection(db_config.config)
+ ActiveRecord::Tasks::DatabaseTasks.migrate
+ end
+ db_namespace["_dump"].invoke
end
# IMPORTANT: This task won't dump the schema if ActiveRecord::Base.dump_schema_after_migration is set to false
@@ -57,165 +92,175 @@ db_namespace = namespace :db do
end
# Allow this task to be called as many times as required. An example is the
# migrate:redo task, which calls other two internally that depend on this one.
- db_namespace['_dump'].reenable
+ db_namespace["_dump"].reenable
end
namespace :migrate do
+ ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
+ desc "Migrate #{spec_name} database for current environment"
+ task spec_name => :load_config do
+ db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name)
+ ActiveRecord::Base.establish_connection(db_config.config)
+ ActiveRecord::Tasks::DatabaseTasks.migrate
+ end
+ end
+
# desc 'Rollbacks the database one migration and re migrate up (options: STEP=x, VERSION=x).'
- task :redo => [:environment, :load_config] do
- if ENV['VERSION']
- db_namespace['migrate:down'].invoke
- db_namespace['migrate:up'].invoke
+ task redo: :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
else
- db_namespace['rollback'].invoke
- db_namespace['migrate'].invoke
+ db_namespace["rollback"].invoke
+ db_namespace["migrate"].invoke
end
end
# desc 'Resets your database using your migrations for the current environment'
- task :reset => ['db:drop', 'db:create', 'db:migrate']
+ task reset: ["db:drop", "db:create", "db:migrate"]
# desc 'Runs the "up" for a given migration VERSION.'
- task :up => [:environment, :load_config] do
- version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil
- raise 'VERSION is required' unless version
- ActiveRecord::Migrator.run(:up, ActiveRecord::Migrator.migrations_paths, version)
- db_namespace['_dump'].invoke
+ task up: :load_config do
+ raise "VERSION is required" if !ENV["VERSION"] || ENV["VERSION"].empty?
+
+ ActiveRecord::Tasks::DatabaseTasks.check_target_version
+
+ ActiveRecord::Base.connection.migration_context.run(
+ :up,
+ ActiveRecord::Tasks::DatabaseTasks.target_version
+ )
+ db_namespace["_dump"].invoke
end
# desc 'Runs the "down" for a given migration VERSION.'
- task :down => [:environment, :load_config] do
- 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::Migrator.migrations_paths, version)
- db_namespace['_dump'].invoke
+ task down: :load_config do
+ raise "VERSION is required - To go down one migration, use db:rollback" if !ENV["VERSION"] || ENV["VERSION"].empty?
+
+ ActiveRecord::Tasks::DatabaseTasks.check_target_version
+
+ ActiveRecord::Base.connection.migration_context.run(
+ :down,
+ ActiveRecord::Tasks::DatabaseTasks.target_version
+ )
+ db_namespace["_dump"].invoke
end
- desc 'Display status of migrations'
- task :status => [:environment, :load_config] do
- unless ActiveRecord::SchemaMigration.table_exists?
- abort 'Schema migrations table does not exist yet.'
+ desc "Display status of migrations"
+ task status: :load_config do
+ ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
+ ActiveRecord::Base.establish_connection(db_config.config)
+ ActiveRecord::Tasks::DatabaseTasks.migrate_status
end
- db_list = ActiveRecord::SchemaMigration.normalized_versions
-
- file_list =
- ActiveRecord::Migrator.migrations_paths.flat_map do |path|
- # match "20091231235959_some_name.rb" and "001_some_name.rb" pattern
- Dir.foreach(path).grep(/^(\d{3,})_(.+)\.rb$/) do
- version = ActiveRecord::SchemaMigration.normalize_migration_number($1)
- status = db_list.delete(version) ? 'up' : 'down'
- [status, version, $2.humanize]
- end
- end
+ 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|
- puts "#{status.center(8)} #{version.ljust(14)} #{name}"
+ namespace :status do
+ ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
+ desc "Display status of migrations for #{spec_name} database"
+ task spec_name => :load_config do
+ db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name)
+ ActiveRecord::Base.establish_connection(db_config.config)
+ ActiveRecord::Tasks::DatabaseTasks.migrate_status
+ end
end
- puts
end
end
- desc 'Rolls the schema back to the previous version (specify steps w/ STEP=n).'
- task :rollback => [:environment, :load_config] do
- step = ENV['STEP'] ? ENV['STEP'].to_i : 1
- ActiveRecord::Migrator.rollback(ActiveRecord::Migrator.migrations_paths, step)
- db_namespace['_dump'].invoke
+ desc "Rolls the schema back to the previous version (specify steps w/ STEP=n)."
+ task rollback: :load_config do
+ step = ENV["STEP"] ? ENV["STEP"].to_i : 1
+ ActiveRecord::Base.connection.migration_context.rollback(step)
+ db_namespace["_dump"].invoke
end
# desc 'Pushes the schema to the next version (specify steps w/ STEP=n).'
- task :forward => [:environment, :load_config] do
- step = ENV['STEP'] ? ENV['STEP'].to_i : 1
- ActiveRecord::Migrator.forward(ActiveRecord::Migrator.migrations_paths, step)
- db_namespace['_dump'].invoke
+ task forward: :load_config do
+ step = ENV["STEP"] ? ENV["STEP"].to_i : 1
+ ActiveRecord::Base.connection.migration_context.forward(step)
+ db_namespace["_dump"].invoke
end
# desc 'Drops and recreates the database from db/schema.rb for the current environment and loads the seeds.'
- task :reset => [ 'db:drop', 'db:setup' ]
+ task reset: [ "db:drop", "db:setup" ]
# desc "Retrieves the charset for the current environment's database"
- task :charset => [:environment, :load_config] do
+ task charset: :load_config do
puts ActiveRecord::Tasks::DatabaseTasks.charset_current
end
# desc "Retrieves the collation for the current environment's database"
- task :collation => [:environment, :load_config] do
+ task collation: :load_config do
begin
puts ActiveRecord::Tasks::DatabaseTasks.collation_current
rescue NoMethodError
- $stderr.puts 'Sorry, your database adapter is not supported yet. Feel free to submit a patch.'
+ $stderr.puts "Sorry, your database adapter is not supported yet. Feel free to submit a patch."
end
end
- desc 'Retrieves the current schema version number'
- task :version => [:environment, :load_config] do
- puts "Current version: #{ActiveRecord::Migrator.current_version}"
+ desc "Retrieves the current schema version number"
+ task version: :load_config do
+ puts "Current version: #{ActiveRecord::Base.connection.migration_context.current_version}"
end
# desc "Raises an error if there are pending migrations"
- task :abort_if_pending_migrations => [:environment, :load_config] do
- pending_migrations = ActiveRecord::Migrator.open(ActiveRecord::Migrator.migrations_paths).pending_migrations
+ task abort_if_pending_migrations: :load_config do
+ pending_migrations = ActiveRecord::Base.connection.migration_context.open.pending_migrations
if pending_migrations.any?
puts "You have #{pending_migrations.size} pending #{pending_migrations.size > 1 ? 'migrations:' : 'migration:'}"
pending_migrations.each do |pending_migration|
- puts ' %4d %s' % [pending_migration.version, pending_migration.name]
+ puts " %4d %s" % [pending_migration.version, pending_migration.name]
end
- abort %{Run `rake db:migrate` to update your database then try again.}
+ abort %{Run `rails db:migrate` to update your database then try again.}
end
end
- desc 'Creates the database, loads the schema, and initializes with the seed data (use db:reset to also drop the database first)'
- task :setup => ['db:schema:load_if_ruby', 'db:structure:load_if_sql', :seed]
+ desc "Creates the database, loads the schema, and initializes with the seed data (use db:reset to also drop the database first)"
+ task setup: ["db:schema:load_if_ruby", "db:structure:load_if_sql", :seed]
- desc 'Loads the seed data from db/seeds.rb'
- task :seed do
- db_namespace['abort_if_pending_migrations'].invoke
+ desc "Loads the seed data from db/seeds.rb"
+ task seed: :load_config do
+ db_namespace["abort_if_pending_migrations"].invoke
ActiveRecord::Tasks::DatabaseTasks.load_seed
end
namespace :fixtures do
desc "Loads fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures."
- task :load => [:environment, :load_config] do
- require 'active_record/fixtures'
+ task load: :load_config do
+ require "active_record/fixtures"
base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path
- fixtures_dir = if ENV['FIXTURES_DIR']
- File.join base_dir, ENV['FIXTURES_DIR']
- else
- base_dir
- end
+ fixtures_dir = if ENV["FIXTURES_DIR"]
+ File.join base_dir, ENV["FIXTURES_DIR"]
+ else
+ base_dir
+ end
- fixture_files = if ENV['FIXTURES']
- ENV['FIXTURES'].split(',')
- else
- # The use of String#[] here is to support namespaced fixtures
- Dir["#{fixtures_dir}/**/*.yml"].map {|f| f[(fixtures_dir.size + 1)..-5] }
- end
+ fixture_files = if ENV["FIXTURES"]
+ ENV["FIXTURES"].split(",")
+ else
+ # The use of String#[] here is to support namespaced fixtures.
+ Dir["#{fixtures_dir}/**/*.yml"].map { |f| f[(fixtures_dir.size + 1)..-5] }
+ end
ActiveRecord::FixtureSet.create_fixtures(fixtures_dir, fixture_files)
end
# desc "Search for a fixture given a LABEL or ID. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures."
- task :identify => [:environment, :load_config] do
- require 'active_record/fixtures'
+ task identify: :load_config do
+ require "active_record/fixtures"
- label, id = ENV['LABEL'], ENV['ID']
- raise 'LABEL or ID required' if label.blank? && id.blank?
+ label, id = ENV["LABEL"], ENV["ID"]
+ raise "LABEL or ID required" if label.blank? && id.blank?
puts %Q(The fixture ID for "#{label}" is #{ActiveRecord::FixtureSet.identify(label)}.) if label
base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path
Dir["#{base_dir}/**/*.yml"].each do |file|
- if data = YAML::load(ERB.new(IO.read(file)).result)
+ if data = YAML.load(ERB.new(IO.read(file)).result)
data.each_key do |key|
key_id = ActiveRecord::FixtureSet.identify(key)
@@ -229,134 +274,126 @@ db_namespace = namespace :db do
end
namespace :schema do
- desc 'Creates a db/schema.rb file that is portable against any DB supported by AR'
- task :dump => [:environment, :load_config] do
- require 'active_record/schema_dumper'
- filename = ENV['SCHEMA'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, 'schema.rb')
- File.open(filename, "w:utf-8") do |file|
- ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
+ desc "Creates a db/schema.rb file that is portable against any DB supported by Active Record"
+ task dump: :load_config do
+ require "active_record/schema_dumper"
+ ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
+ filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.spec_name, :ruby)
+ File.open(filename, "w:utf-8") do |file|
+ ActiveRecord::Base.establish_connection(db_config.config)
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
+ end
end
- db_namespace['schema:dump'].reenable
+
+ db_namespace["schema:dump"].reenable
end
- desc 'Loads a schema.rb file into the database'
- task :load => [:environment, :load_config] do
- ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:ruby, ENV['SCHEMA'])
+ desc "Loads a schema.rb file into the database"
+ task load: [:load_config, :check_protected_environments] do
+ ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:ruby, ENV["SCHEMA"])
end
- task :load_if_ruby => ['db:create', :environment] do
+ task load_if_ruby: ["db:create", :environment] do
db_namespace["schema:load"].invoke if ActiveRecord::Base.schema_format == :ruby
end
namespace :cache do
- desc 'Creates a db/schema_cache.dump 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.tables.each { |table| con.schema_cache.add(table) }
- open(filename, 'wb') { |f| f.write(Marshal.dump(con.schema_cache)) }
+ desc "Creates a db/schema_cache.yml file."
+ task dump: :load_config do
+ ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
+ ActiveRecord::Base.establish_connection(db_config.config)
+ filename = ActiveRecord::Tasks::DatabaseTasks.cache_dump_filename(db_config.spec_name)
+ ActiveRecord::Tasks::DatabaseTasks.dump_schema_cache(
+ ActiveRecord::Base.connection,
+ filename,
+ )
+ end
end
- desc 'Clears a db/schema_cache.dump file.'
- task :clear => [:environment, :load_config] do
- filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump")
- FileUtils.rm(filename) if File.exist?(filename)
+ desc "Clears a db/schema_cache.yml file."
+ task clear: :load_config do
+ ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
+ filename = ActiveRecord::Tasks::DatabaseTasks.cache_dump_filename(db_config.spec_name)
+ rm_f filename, verbose: false
+ end
end
end
-
end
namespace :structure do
- desc 'Dumps the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql'
- task :dump => [:environment, :load_config] do
- filename = ENV['SCHEMA'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql")
- current_config = ActiveRecord::Tasks::DatabaseTasks.current_config
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename)
-
- if ActiveRecord::Base.connection.supports_migrations? &&
- ActiveRecord::SchemaMigration.table_exists?
- File.open(filename, "a") do |f|
- f.puts ActiveRecord::Base.connection.dump_schema_information
- f.print "\n"
+ desc "Dumps the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql"
+ task dump: :load_config do
+ ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
+ ActiveRecord::Base.establish_connection(db_config.config)
+ filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.spec_name, :sql)
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(db_config.config, filename)
+ if ActiveRecord::SchemaMigration.table_exists?
+ File.open(filename, "a") do |f|
+ f.puts ActiveRecord::Base.connection.dump_schema_information
+ f.print "\n"
+ end
end
end
- db_namespace['structure:dump'].reenable
+
+ db_namespace["structure:dump"].reenable
end
desc "Recreates the databases from the structure.sql file"
- task :load => [:load_config] do
- ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:sql, ENV['SCHEMA'])
+ task load: [:load_config, :check_protected_environments] do
+ ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:sql, ENV["SCHEMA"])
end
- task :load_if_sql => ['db:create', :environment] do
+ task load_if_sql: ["db:create", :environment] do
db_namespace["structure:load"].invoke if ActiveRecord::Base.schema_format == :sql
end
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
+ task load: %w(db:test:purge) do
case ActiveRecord::Base.schema_format
- when :ruby
- db_namespace["test:load_schema"].invoke
- when :sql
- db_namespace["test:load_structure"].invoke
+ when :ruby
+ db_namespace["test:load_schema"].invoke
+ when :sql
+ db_namespace["test:load_structure"].invoke
end
end
# desc "Recreate the test database from an existent schema.rb file"
- task :load_schema => %w(db:test:purge) do
+ task load_schema: %w(db:test:purge) do
begin
should_reconnect = ActiveRecord::Base.connection_pool.active_connection?
ActiveRecord::Schema.verbose = false
- ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations['test'], :ruby, ENV['SCHEMA']
+ ActiveRecord::Base.configurations.configs_for(env_name: "test").each do |db_config|
+ filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.spec_name, :ruby)
+ ActiveRecord::Tasks::DatabaseTasks.load_schema(db_config.config, :ruby, filename, "test")
+ end
ensure
if should_reconnect
- ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[ActiveRecord::Tasks::DatabaseTasks.env])
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations.default_hash(ActiveRecord::Tasks::DatabaseTasks.env))
end
end
end
# desc "Recreate the test database from an existent structure.sql file"
- task :load_structure => %w(db:test:purge) do
- ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations['test'], :sql, ENV['SCHEMA']
- 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
+ task load_structure: %w(db:test:purge) do
+ ActiveRecord::Base.configurations.configs_for(env_name: "test").each do |db_config|
+ filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.spec_name, :sql)
+ ActiveRecord::Tasks::DatabaseTasks.load_schema(db_config.config, :sql, filename, "test")
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) do
- ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations['test']
+ task purge: %w(load_config check_protected_environments) do
+ ActiveRecord::Base.configurations.configs_for(env_name: "test").each do |db_config|
+ ActiveRecord::Tasks::DatabaseTasks.purge(db_config.config)
+ end
end
- # desc 'Check for pending migrations and load the test schema'
- task :prepare => %w(environment load_config) do
+ # desc 'Load the test schema'
+ task prepare: :load_config do
unless ActiveRecord::Base.configurations.blank?
- db_namespace['test:load'].invoke
+ db_namespace["test:load"].invoke
end
end
end
@@ -365,13 +402,13 @@ end
namespace :railties do
namespace :install do
# desc "Copies missing migrations from Railties (e.g. engines). You can specify Railties to use with FROM=railtie1,railtie2"
- task :migrations => :'db:load_config' do
- to_load = ENV['FROM'].blank? ? :all : ENV['FROM'].split(",").map(&:strip)
+ task migrations: :'db:load_config' do
+ to_load = ENV["FROM"].blank? ? :all : ENV["FROM"].split(",").map(&:strip)
railties = {}
Rails.application.migration_railties.each do |railtie|
next unless to_load == :all || to_load.include?(railtie.railtie_name)
- if railtie.respond_to?(:paths) && (path = railtie.paths['db/migrate'].first)
+ if railtie.respond_to?(:paths) && (path = railtie.paths["db/migrate"].first)
railties[railtie.railtie_name] = path
end
end
@@ -384,8 +421,8 @@ namespace :railties do
puts "Copied migration #{migration.basename} from #{name}"
end
- ActiveRecord::Migration.copy(ActiveRecord::Migrator.migrations_paths.first, railties,
- :on_skip => on_skip, :on_copy => on_copy)
+ ActiveRecord::Migration.copy(ActiveRecord::Tasks::DatabaseTasks.migrations_paths.first, railties,
+ on_skip: on_skip, on_copy: on_copy)
end
end
end
diff --git a/activerecord/lib/active_record/railties/jdbcmysql_error.rb b/activerecord/lib/active_record/railties/jdbcmysql_error.rb
deleted file mode 100644
index 6a38211bff..0000000000
--- a/activerecord/lib/active_record/railties/jdbcmysql_error.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-#FIXME Remove if ArJdbcMysql will give.
-module ArJdbcMySQL #:nodoc:
- class Error < StandardError #:nodoc:
- attr_accessor :error_number, :sql_state
-
- def initialize msg
- super
- @error_number = nil
- @sql_state = nil
- end
-
- # Mysql gem compatibility
- alias_method :errno, :error_number
- alias_method :error, :message
- end
-end
diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb
index ce78f1756d..7bc26993d5 100644
--- a/activerecord/lib/active_record/readonly_attributes.rb
+++ b/activerecord/lib/active_record/readonly_attributes.rb
@@ -1,22 +1,23 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ReadonlyAttributes
extend ActiveSupport::Concern
included do
- class_attribute :_attr_readonly, instance_accessor: false
- self._attr_readonly = []
+ class_attribute :_attr_readonly, instance_accessor: false, default: []
end
module ClassMethods
# 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.
def readonly_attributes
- self._attr_readonly
+ _attr_readonly
end
end
end
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 5360db6a19..b2110f727c 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -1,5 +1,7 @@
-require 'thread'
-require 'active_support/core_ext/string/filters'
+# frozen_string_literal: true
+
+require "active_support/core_ext/string/filters"
+require "concurrent/map"
module ActiveRecord
# = Active Record Reflection
@@ -7,36 +9,41 @@ module ActiveRecord
extend ActiveSupport::Concern
included do
- class_attribute :_reflections
- class_attribute :aggregate_reflections
- self._reflections = {}
- self.aggregate_reflections = {}
+ class_attribute :_reflections, instance_writer: false, default: {}
+ class_attribute :aggregate_reflections, instance_writer: false, default: {}
end
- def self.create(macro, name, scope, options, ar)
- klass = case macro
- when :composed_of
- AggregateReflection
- when :has_many
- HasManyReflection
- when :has_one
- HasOneReflection
- when :belongs_to
- BelongsToReflection
- else
- raise "Unsupported Macro: #{macro}"
- end
-
- reflection = klass.new(name, scope, options, ar)
- options[:through] ? ThroughReflection.new(reflection) : reflection
- end
+ class << self
+ def create(macro, name, scope, options, ar)
+ reflection = reflection_class_for(macro).new(name, scope, options, ar)
+ options[:through] ? ThroughReflection.new(reflection) : reflection
+ end
- def self.add_reflection(ar, name, reflection)
- ar._reflections = ar._reflections.merge(name.to_s => reflection)
- end
+ def add_reflection(ar, name, reflection)
+ ar.clear_reflections_cache
+ name = name.to_s
+ ar._reflections = ar._reflections.except(name).merge!(name => reflection)
+ end
- def self.add_aggregate_reflection(ar, name, reflection)
- ar.aggregate_reflections = ar.aggregate_reflections.merge(name.to_s => reflection)
+ def add_aggregate_reflection(ar, name, reflection)
+ ar.aggregate_reflections = ar.aggregate_reflections.merge(name.to_s => reflection)
+ end
+
+ private
+ def reflection_class_for(macro)
+ case macro
+ when :composed_of
+ AggregateReflection
+ when :has_many
+ HasManyReflection
+ when :has_one
+ HasOneReflection
+ when :belongs_to
+ BelongsToReflection
+ else
+ raise "Unsupported Macro: #{macro}"
+ end
+ end
end
# \Reflection enables the ability to examine the associations and aggregations of
@@ -61,24 +68,27 @@ module ActiveRecord
aggregate_reflections[aggregation.to_s]
end
- # Returns a Hash of name of the reflection as the key and a AssociationReflection as the value.
+ # Returns a Hash of name of the reflection as the key and an AssociationReflection as the value.
#
# Account.reflections # => {"balance" => AggregateReflection}
#
- # @api public
def reflections
- ref = {}
- _reflections.each do |name, reflection|
- parent_reflection = reflection.parent_reflection
+ @__reflections ||= begin
+ ref = {}
- if parent_reflection
- parent_name = parent_reflection.name
- ref[parent_name.to_s] = parent_reflection
- else
- ref[name] = reflection
+ _reflections.each do |name, reflection|
+ parent_reflection = reflection.parent_reflection
+
+ if parent_reflection
+ parent_name = parent_reflection.name
+ ref[parent_name.to_s] = parent_reflection
+ else
+ ref[name] = reflection
+ end
end
+
+ ref
end
- ref
end
# Returns an array of AssociationReflection objects for all the
@@ -91,10 +101,10 @@ module ActiveRecord
# Account.reflect_on_all_associations # returns an array of all associations
# Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations
#
- # @api public
def reflect_on_all_associations(macro = nil)
association_reflections = reflections.values
- macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections
+ association_reflections.select! { |reflection| reflection.macro == macro } if macro
+ association_reflections
end
# Returns the AssociationReflection object for the +association+ (use the symbol).
@@ -102,27 +112,42 @@ module ActiveRecord
# Account.reflect_on_association(:owner) # returns the owner AssociationReflection
# Invoice.reflect_on_association(:line_items).macro # returns :has_many
#
- # @api public
def reflect_on_association(association)
reflections[association.to_s]
end
- # @api private
def _reflect_on_association(association) #:nodoc:
_reflections[association.to_s]
end
# Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled.
- #
- # @api public
def reflect_on_all_autosave_associations
reflections.values.select { |reflection| reflection.options[:autosave] }
end
+
+ def clear_reflections_cache # :nodoc:
+ @__reflections = nil
+ end
end
- # Holds all the methods that are shared between MacroReflection, AssociationReflection
- # and ThroughReflection
+ # Holds all the methods that are shared between MacroReflection and ThroughReflection.
+ #
+ # AbstractReflection
+ # MacroReflection
+ # AggregateReflection
+ # AssociationReflection
+ # HasManyReflection
+ # HasOneReflection
+ # BelongsToReflection
+ # HasAndBelongsToManyReflection
+ # ThroughReflection
+ # PolymorphicReflection
+ # RuntimeReflection
class AbstractReflection # :nodoc:
+ def through_reflection?
+ false
+ end
+
def table_name
klass.table_name
end
@@ -133,14 +158,6 @@ module ActiveRecord
klass.new(attributes, &block)
end
- def quoted_table_name
- klass.quoted_table_name
- end
-
- def primary_key_type
- klass.type_for_attribute(klass.primary_key)
- end
-
# Returns the class name for the macro.
#
# <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>'Money'</tt>
@@ -151,29 +168,165 @@ 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
+ @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 build_join_constraint(table, foreign_table)
+ key = join_keys.key
+ foreign_key = join_keys.foreign_key
+
+ constraint = table[key].eq(foreign_table[foreign_key])
+
+ if klass.finder_needs_type_condition?
+ table.create_and([constraint, klass.send(:type_condition, table)])
+ else
+ constraint
+ end
+ end
+
+ def join_scope(table, foreign_klass)
+ predicate_builder = predicate_builder(table)
+ scope_chain_items = join_scopes(table, predicate_builder)
+ klass_scope = klass_join_scope(table, predicate_builder)
+
+ if type
+ klass_scope.where!(type => foreign_klass.polymorphic_name)
+ end
+
+ scope_chain_items.inject(klass_scope, &:merge!)
+ end
+
+ def join_scopes(table, predicate_builder) # :nodoc:
+ if scope
+ [scope_for(build_scope(table, predicate_builder))]
+ else
+ []
+ end
+ end
+
+ def klass_join_scope(table, predicate_builder) # :nodoc:
+ relation = build_scope(table, predicate_builder)
+ klass.scope_for_association(relation)
end
def constraints
- scope_chain.flatten
+ chain.flat_map(&:scopes)
+ end
+
+ def counter_cache_column
+ if belongs_to?
+ if options[:counter_cache] == true
+ "#{active_record.name.demodulize.underscore.pluralize}_count"
+ elsif options[:counter_cache]
+ options[:counter_cache].to_s
+ end
+ else
+ options[:counter_cache] ? options[:counter_cache].to_s : "#{name}_count"
+ end
+ end
+
+ def inverse_of
+ return unless inverse_name
+
+ @inverse_of ||= klass._reflect_on_association inverse_name
+ end
+
+ def check_validity_of_inverse!
+ unless polymorphic?
+ if has_inverse? && inverse_of.nil?
+ raise InverseOfAssociationNotFoundError.new(self)
+ end
+ end
+ end
+
+ # This shit is nasty. We need to avoid the following situation:
+ #
+ # * An associated record is deleted via record.destroy
+ # * Hence the callbacks run, and they find a belongs_to on the record with a
+ # :counter_cache options which points back at our owner. So they update the
+ # counter cache.
+ # * In which case, we must make sure to *not* update the counter cache, or else
+ # it will be decremented twice.
+ #
+ # Hence this method.
+ def inverse_which_updates_counter_cache
+ return @inverse_which_updates_counter_cache if defined?(@inverse_which_updates_counter_cache)
+ @inverse_which_updates_counter_cache = klass.reflect_on_all_associations(:belongs_to).find do |inverse|
+ inverse.counter_cache_column == counter_cache_column
+ end
+ end
+ alias inverse_updates_counter_cache? inverse_which_updates_counter_cache
+
+ def inverse_updates_counter_in_memory?
+ inverse_of && inverse_which_updates_counter_cache == inverse_of
+ end
+
+ # Returns whether a counter cache should be used for this association.
+ #
+ # The counter_cache option must be given on either the owner or inverse
+ # association, and the column must be present on the owner.
+ def has_cached_counter?
+ options[:counter_cache] ||
+ inverse_which_updates_counter_cache && inverse_which_updates_counter_cache.options[:counter_cache] &&
+ !!active_record.columns_hash[counter_cache_column]
+ end
+
+ def counter_must_be_updated_by_has_many?
+ !inverse_updates_counter_in_memory? && has_cached_counter?
end
def alias_candidate(name)
"#{plural_name}_#{name}"
end
+
+ def chain
+ collect_join_chain
+ end
+
+ def get_join_keys(association_klass)
+ JoinKeys.new(join_primary_key(association_klass), join_foreign_key)
+ end
+
+ def build_scope(table, predicate_builder = predicate_builder(table))
+ Relation.create(
+ klass,
+ table: table,
+ predicate_builder: predicate_builder
+ )
+ end
+
+ def join_primary_key(*)
+ foreign_key
+ end
+
+ def join_foreign_key
+ active_record_primary_key
+ end
+
+ protected
+ def actual_source_reflection # FIXME: this is a horrible name
+ self
+ end
+
+ private
+ def predicate_builder(table)
+ PredicateBuilder.new(TableMetadata.new(klass, table))
+ end
+
+ def primary_key(klass)
+ klass.primary_key || raise(UnknownPrimaryKey.new(klass))
+ end
end
# Base class for AggregateReflection and AssociationReflection. Objects of
# AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
- #
- # MacroReflection
- # AggregateReflection
- # AssociationReflection
- # HasManyReflection
- # HasOneReflection
- # BelongsToReflection
- # ThroughReflection
class MacroReflection < AbstractReflection
# Returns the name of the macro.
#
@@ -204,7 +357,6 @@ module ActiveRecord
end
def autosave=(autosave)
- @automatic_inverse_of = false
@options[:autosave] = autosave
parent_reflection = self.parent_reflection
if parent_reflection
@@ -216,6 +368,17 @@ module ActiveRecord
#
# <tt>composed_of :balance, class_name: 'Money'</tt> returns the Money class
# <tt>has_many :clients</tt> returns the Client class
+ #
+ # class Company < ActiveRecord::Base
+ # has_many :clients
+ # end
+ #
+ # Company.reflect_on_association(:clients).klass
+ # # => Client
+ #
+ # <b>Note:</b> Do not call +klass.new+ or +klass.create+ to instantiate
+ # a new association object. Use +build_association+ or +create_association+
+ # instead. This allows plugins to hook into association object creation.
def klass
@klass ||= compute_class(class_name)
end
@@ -234,14 +397,17 @@ module ActiveRecord
active_record == other_aggregation.active_record
end
+ def scope_for(relation, owner = nil)
+ relation.instance_exec(owner, &scope) || relation
+ end
+
private
def derive_class_name
name.to_s.camelize
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
@@ -250,26 +416,13 @@ 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.
- #
- # class Author < ActiveRecord::Base
- # has_many :books
- # end
- #
- # Author.reflect_on_association(:books).klass
- # # => Book
- #
- # <b>Note:</b> Do not call +klass.new+ or +klass.create+ to instantiate
- # a new association object. Use +build_association+ or +create_association+
- # instead. This allows plugins to hook into association object creation.
- def klass
- @klass ||= compute_class(class_name)
- end
-
def compute_class(name)
+ if polymorphic?
+ raise ArgumentError, "Polymorphic associations do not support computing the class."
+ end
active_record.send(:compute_type, name)
end
@@ -278,22 +431,22 @@ module ActiveRecord
def initialize(name, scope, options, active_record)
super
- @automatic_inverse_of = nil
@type = options[:as] && (options[:foreign_type] || "#{options[:as]}_type")
- @foreign_type = options[:foreign_type] || "#{name}_type"
+ @foreign_type = options[:polymorphic] && (options[:foreign_type] || "#{name}_type")
@constructable = calculate_constructable(macro, options)
- @association_scope_cache = {}
- @scope_lock = Mutex.new
+ @association_scope_cache = Concurrent::Map.new
+
+ if options[:class_name] && options[:class_name].class == Class
+ raise ArgumentError, "A class was passed to `:class_name` but we are expecting a string."
+ end
end
- def association_scope_cache(conn, owner)
+ def association_scope_cache(conn, owner, &block)
key = conn.prepared_statements
if polymorphic?
key = [key, owner._read_attribute(@foreign_type)]
end
- @association_scope_cache[key] ||= @scope_lock.synchronize {
- @association_scope_cache[key] ||= yield
- }
+ @association_scope_cache.compute_if_absent(key) { StatementCache.create(conn, &block) }
end
def constructable? # :nodoc:
@@ -305,7 +458,7 @@ module ActiveRecord
end
def foreign_key
- @foreign_key ||= options[:foreign_key] || derive_foreign_key
+ @foreign_key ||= options[:foreign_key] || derive_foreign_key.freeze
end
def association_foreign_key
@@ -321,26 +474,10 @@ module ActiveRecord
@active_record_primary_key ||= options[:primary_key] || primary_key(active_record)
end
- def counter_cache_column
- if options[:counter_cache] == true
- "#{active_record.name.demodulize.underscore.pluralize}_count"
- elsif options[:counter_cache]
- options[:counter_cache].to_s
- end
- end
-
def check_validity!
check_validity_of_inverse!
end
- def check_validity_of_inverse!
- unless polymorphic?
- if has_inverse? && inverse_of.nil?
- raise InverseOfAssociationNotFoundError.new(self)
- end
- end
- end
-
def check_preloadable!
return unless scope
@@ -355,7 +492,7 @@ module ActiveRecord
alias :check_eager_loadable! :check_preloadable!
def join_id_for(owner) # :nodoc:
- owner[active_record_primary_key]
+ owner[join_foreign_key]
end
def through_reflection
@@ -368,7 +505,7 @@ module ActiveRecord
# A chain of reflections from this one back to the owner. For more see the explanation in
# ThroughReflection.
- def chain
+ def collect_join_chain
[self]
end
@@ -382,22 +519,14 @@ 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]] : [[]]
+ def has_scope?
+ scope
end
def has_inverse?
inverse_name
end
- def inverse_of
- return unless inverse_name
-
- @inverse_of ||= klass._reflect_on_association inverse_name
- end
-
def polymorphic_inverse_of(associated_class)
if has_inverse?
if inverse_relationship = associated_class._reflect_on_association(options[:inverse_of])
@@ -439,73 +568,65 @@ module ActiveRecord
# Returns +true+ if +self+ is a +has_one+ reflection.
def has_one?; false; end
- def association_class
- case macro
- when :belongs_to
- if polymorphic?
- Associations::BelongsToPolymorphicAssociation
- else
- Associations::BelongsToAssociation
- end
- when :has_many
- if options[:through]
- Associations::HasManyThroughAssociation
- else
- Associations::HasManyAssociation
- end
- when :has_one
- if options[:through]
- Associations::HasOneThroughAssociation
- else
- Associations::HasOneAssociation
- end
- end
- end
+ def association_class; raise NotImplementedError; end
def polymorphic?
options[:polymorphic]
end
VALID_AUTOMATIC_INVERSE_MACROS = [:has_many, :has_one, :belongs_to]
- INVALID_AUTOMATIC_INVERSE_OPTIONS = [:conditions, :through, :polymorphic, :foreign_key]
+ INVALID_AUTOMATIC_INVERSE_OPTIONS = [:through, :foreign_key]
- protected
+ def add_as_source(seed)
+ seed
+ end
- def actual_source_reflection # FIXME: this is a horrible name
- self
- end
+ def add_as_polymorphic_through(reflection, seed)
+ seed + [PolymorphicReflection.new(self, reflection)]
+ end
+
+ def add_as_through(seed)
+ seed + [self]
+ end
+
+ def extensions
+ Array(options[:extend])
+ end
private
def calculate_constructable(macro, options)
- case macro
- when :belongs_to
- !polymorphic?
- when :has_one
- !options[:through]
- else
- true
- end
+ true
end
# Attempts to find the inverse association name automatically.
# If it cannot find a suitable inverse association name, it returns
- # nil.
+ # +nil+.
def inverse_name
- options.fetch(:inverse_of) do
- if @automatic_inverse_of == false
- nil
- else
- @automatic_inverse_of ||= automatic_inverse_of
- end
+ unless defined?(@inverse_name)
+ @inverse_name = options.fetch(:inverse_of) { automatic_inverse_of }
end
+
+ @inverse_name
end
- # returns either nil or the inverse association name that it finds.
+ # returns either +nil+ or the inverse association name that it finds.
def automatic_inverse_of
- if can_find_inverse_of_automatically?(self)
- inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name.demodulize).to_sym
+ return unless can_find_inverse_of_automatically?(self)
+
+ inverse_name_candidates =
+ if options[:as]
+ [options[:as]]
+ else
+ active_record_name = active_record.name.demodulize
+ [active_record_name, ActiveSupport::Inflector.pluralize(active_record_name)]
+ end
+ inverse_name_candidates.map! do |candidate|
+ ActiveSupport::Inflector.underscore(candidate).to_sym
+ end
+
+ inverse_name_candidates.detect do |inverse_name|
begin
reflection = klass._reflect_on_association(inverse_name)
rescue NameError
@@ -514,24 +635,17 @@ module ActiveRecord
reflection = false
end
- if valid_inverse_reflection?(reflection)
- return inverse_name
- end
+ valid_inverse_reflection?(reflection)
end
-
- false
end
# Checks if the inverse reflection that is returned from the
# +automatic_inverse_of+ method is a valid reflection. We must
# make sure that the reflection's active_record name matches up
# with the current reflection's klass name.
- #
- # Note: klass will always be valid because when there's a NameError
- # from calling +klass+, +reflection+ will already be set to false.
def valid_inverse_reflection?(reflection)
reflection &&
- klass.name == reflection.active_record.name &&
+ klass <= reflection.active_record &&
can_find_inverse_of_automatically?(reflection)
end
@@ -539,9 +653,8 @@ module ActiveRecord
# us from being able to guess the inverse automatically. First, the
# <tt>inverse_of</tt> option cannot be set to false. Second, we must
# have <tt>has_many</tt>, <tt>has_one</tt>, <tt>belongs_to</tt> associations.
- # Third, we must not have options such as <tt>:polymorphic</tt> or
- # <tt>:foreign_key</tt> which prevent us from correctly guessing the
- # inverse association.
+ # Third, we must not have options such as <tt>:foreign_key</tt>
+ # which prevent us from correctly guessing the inverse association.
#
# Anything with a scope can additionally ruin our attempt at finding an
# inverse, so we exclude reflections with scopes.
@@ -571,56 +684,78 @@ module ActiveRecord
def derive_join_table
ModelSchema.derive_join_table_name active_record.table_name, klass.table_name
end
-
- def primary_key(klass)
- klass.primary_key || raise(UnknownPrimaryKey.new(klass))
- end
end
class HasManyReflection < AssociationReflection # :nodoc:
- def initialize(name, scope, options, active_record)
- super(name, scope, options, active_record)
- end
-
def macro; :has_many; end
def collection?; true; end
- end
- class HasOneReflection < AssociationReflection # :nodoc:
- def initialize(name, scope, options, active_record)
- super(name, scope, options, active_record)
+ def association_class
+ if options[:through]
+ Associations::HasManyThroughAssociation
+ else
+ Associations::HasManyAssociation
+ end
end
+ def association_primary_key(klass = nil)
+ primary_key(klass || self.klass)
+ end
+ end
+
+ class HasOneReflection < AssociationReflection # :nodoc:
def macro; :has_one; end
def has_one?; true; end
- end
- class BelongsToReflection < AssociationReflection # :nodoc:
- def initialize(name, scope, options, active_record)
- super(name, scope, options, active_record)
+ def association_class
+ if options[:through]
+ Associations::HasOneThroughAssociation
+ else
+ Associations::HasOneAssociation
+ end
end
+ private
+
+ def calculate_constructable(macro, options)
+ !options[:through]
+ end
+ end
+
+ class BelongsToReflection < AssociationReflection # :nodoc:
def macro; :belongs_to; end
def belongs_to?; true; end
- def join_keys(association_klass)
- key = polymorphic? ? association_primary_key(association_klass) : association_primary_key
- JoinKeys.new(key, foreign_key)
+ def association_class
+ if polymorphic?
+ Associations::BelongsToPolymorphicAssociation
+ else
+ Associations::BelongsToAssociation
+ end
end
- def join_id_for(owner) # :nodoc:
- owner[foreign_key]
+ def join_primary_key(klass = nil)
+ polymorphic? ? association_primary_key(klass) : association_primary_key
end
- end
- class HasAndBelongsToManyReflection < AssociationReflection # :nodoc:
- def initialize(name, scope, options, active_record)
- super
+ def join_foreign_key
+ foreign_key
end
+ private
+ def can_find_inverse_of_automatically?(_)
+ !polymorphic? && super
+ end
+
+ def calculate_constructable(macro, options)
+ !polymorphic?
+ end
+ end
+
+ class HasAndBelongsToManyReflection < AssociationReflection # :nodoc:
def macro; :has_and_belongs_to_many; end
def collection?
@@ -628,19 +763,22 @@ 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
+ delegate :foreign_key, :foreign_type, :association_foreign_key, :join_id_for,
+ :active_record_primary_key, :type, :get_join_keys, to: :source_reflection
def initialize(delegate_reflection)
@delegate_reflection = delegate_reflection
- @klass = delegate_reflection.options[:anonymous_class]
+ @klass = delegate_reflection.options[:anonymous_class]
@source_reflection_name = delegate_reflection.options[:source]
end
+ def through_reflection?
+ true
+ end
+
def klass
@klass ||= delegate_reflection.compute_class(class_name)
end
@@ -699,78 +837,35 @@ module ActiveRecord
# # => [<ActiveRecord::Reflection::ThroughReflection: @delegate_reflection=#<ActiveRecord::Reflection::HasManyReflection: @name=:tags...>,
# <ActiveRecord::Reflection::HasManyReflection: @name=:taggings, @options={}, @active_record=Post>]
#
- def chain
- @chain ||= begin
- a = source_reflection.chain
- b = through_reflection.chain.map(&:dup)
-
- if options[:source_type]
- b[0] = PolymorphicReflection.new(b[0], self)
- end
-
- chain = a + b
- chain[0] = self # Use self so we don't lose the information from :source_type
- chain
- end
+ def collect_join_chain
+ collect_join_reflections [self]
end
# This is for clearing cache on the reflection. Useful for tests that need to compare
# SQL queries on associations.
def clear_association_scope_cache # :nodoc:
- @chain = nil
delegate_reflection.clear_association_scope_cache
source_reflection.clear_association_scope_cache
through_reflection.clear_association_scope_cache
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)
-
- if options[:source_type]
- type = foreign_type
- source_type = options[:source_type]
- through_scope_chain.first << lambda { |object|
- where(type => source_type)
- }
- end
+ def scopes
+ source_reflection.scopes + super
+ end
- # Recursively fill out the rest of the array from the through reflection
- scope_chain + through_scope_chain
- end
+ def join_scopes(table, predicate_builder) # :nodoc:
+ source_reflection.join_scopes(table, predicate_builder) + super
end
- def join_keys(association_klass)
- source_reflection.join_keys(association_klass)
+ def has_scope?
+ scope || options[:source_type] ||
+ source_reflection.has_scope? ||
+ through_reflection.has_scope?
end
# A through association is nested if there would be more than one join table
def nested?
- chain.length > 2
+ source_reflection.through_reflection? || through_reflection.through_reflection?
end
# We want to use the klass from this reflection, rather than just delegate straight to
@@ -806,15 +901,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
@@ -828,10 +921,6 @@ module ActiveRecord
through_reflection.options
end
- def join_id_for(owner) # :nodoc:
- source_reflection.join_id_for(owner)
- end
-
def check_validity!
if through_reflection.nil?
raise HasManyThroughAssociationNotFoundError.new(active_record.name, self)
@@ -861,6 +950,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
@@ -870,17 +967,37 @@ module ActiveRecord
scope_chain
end
- protected
+ def add_as_source(seed)
+ collect_join_reflections seed
+ end
+
+ def add_as_polymorphic_through(reflection, seed)
+ collect_join_reflections(seed + [PolymorphicReflection.new(self, reflection)])
+ end
+ def add_as_through(seed)
+ collect_join_reflections(seed + [self])
+ end
+
+ protected
def actual_source_reflection # FIXME: this is a horrible name
- source_reflection.send(:actual_source_reflection)
+ source_reflection.actual_source_reflection
end
- def primary_key(klass)
- klass.primary_key || raise(UnknownPrimaryKey.new(klass))
+ private
+ attr_reader :delegate_reflection
+
+ def collect_join_reflections(seed)
+ a = source_reflection.add_as_source seed
+ if options[:source_type]
+ through_reflection.add_as_polymorphic_through self, a
+ else
+ through_reflection.add_as_through a
+ end
end
- private
+ def inverse_name; delegate_reflection.send(:inverse_name); end
+
def derive_class_name
# get the class_name of the belongs_to association of the through reflection
options[:source_type] || source_reflection.class_name
@@ -890,52 +1007,35 @@ module ActiveRecord
public_instance_methods
delegate(*delegate_methods, to: :delegate_reflection)
-
end
- class PolymorphicReflection < ThroughReflection # :nodoc:
+ class PolymorphicReflection < AbstractReflection # :nodoc:
+ delegate :klass, :scope, :plural_name, :type, :get_join_keys, :scope_for, to: :@reflection
+
def initialize(reflection, previous_reflection)
@reflection = reflection
@previous_reflection = previous_reflection
end
- def klass
- @reflection.klass
- end
-
- def scope
- @reflection.scope
- end
-
- def table_name
- @reflection.table_name
- end
-
- def plural_name
- @reflection.plural_name
- end
-
- def join_keys(association_klass)
- @reflection.join_keys(association_klass)
- end
-
- def type
- @reflection.type
+ def join_scopes(table, predicate_builder) # :nodoc:
+ scopes = @previous_reflection.join_scopes(table, predicate_builder) + super
+ scopes << build_scope(table, predicate_builder).instance_exec(nil, &source_type_scope)
end
def constraints
- [source_type_info]
+ @reflection.constraints + [source_type_scope]
end
- def source_type_info
- type = @previous_reflection.foreign_type
- source_type = @previous_reflection.options[:source_type]
- lambda { |object| where(type => source_type) }
- end
+ private
+ def source_type_scope
+ type = @previous_reflection.foreign_type
+ source_type = @previous_reflection.options[:source_type]
+ lambda { |object| where(type => source_type) }
+ end
end
- class RuntimeReflection < PolymorphicReflection # :nodoc:
- attr_accessor :next
+ class RuntimeReflection < AbstractReflection # :nodoc:
+ delegate :scope, :type, :constraints, :get_join_keys, to: :@reflection
def initialize(reflection, association)
@reflection = reflection
@@ -946,24 +1046,8 @@ module ActiveRecord
@association.klass
end
- def table_name
- klass.table_name
- end
-
- def constraints
- @reflection.constraints
- end
-
- def source_type_info
- @reflection.source_type_info
- end
-
- def alias_candidate(name)
- "#{plural_name}_#{name}_join"
- end
-
- def alias_name
- Arel::Table.new(table_name)
+ def aliased_table
+ @aliased_table ||= Arel::Table.new(table_name, type_caster: klass.type_caster)
end
def all_includes; yield; end
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index e4df122af6..d5b6082d13 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -1,17 +1,17 @@
-# -*- coding: utf-8 -*-
-require "arel/collectors/bind"
+# frozen_string_literal: true
module ActiveRecord
- # = Active Record Relation
+ # = Active Record \Relation
class Relation
MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group,
- :order, :joins, :references,
+ :order, :joins, :left_outer_joins, :references,
:extending, :unscope]
SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering,
- :reverse_order, :distinct, :create_with]
+ :reverse_order, :distinct, :create_with, :skip_query_cache]
+
CLAUSE_METHODS = [:where, :having, :from]
- INVALID_METHODS_FOR_DELETE_ALL = [:limit, :distinct, :offset, :group, :having]
+ INVALID_METHODS_FOR_DELETE_ALL = [:distinct, :group, :having]
VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS + CLAUSE_METHODS
@@ -19,97 +19,40 @@ module ActiveRecord
include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation
attr_reader :table, :klass, :loaded, :predicate_builder
+ attr_accessor :skip_preloading_value
alias :model :klass
alias :loaded? :loaded
+ alias :locked? :lock_value
- def initialize(klass, table, predicate_builder, values = {})
+ def initialize(klass, table: klass.arel_table, predicate_builder: klass.predicate_builder, values: {})
@klass = klass
@table = table
@values = values
@offsets = {}
@loaded = false
@predicate_builder = predicate_builder
+ @delegate_to_klass = false
end
def initialize_copy(other)
- # This method is a hot spot, so for now, use Hash[] to dup the hash.
- # https://bugs.ruby-lang.org/issues/7166
- @values = Hash[@values]
+ @values = @values.dup
reset
end
- def insert(values) # :nodoc:
- primary_key_value = nil
-
- if primary_key && Hash === values
- primary_key_value = values[values.keys.find { |k|
- k.name == primary_key
- }]
-
- if !primary_key_value && connection.prefetch_primary_key?(klass.table_name)
- primary_key_value = connection.next_sequence_value(klass.sequence_name)
- values[klass.arel_table[klass.primary_key]] = primary_key_value
- end
- end
-
- im = arel.create_insert
- im.into @table
-
- substitutes, binds = substitute_values values
-
- if values.empty? # empty insert
- im.values = Arel.sql(connection.empty_insert_statement_value)
- else
- im.insert substitutes
- end
-
- @klass.connection.insert(
- im,
- 'SQL',
- primary_key,
- primary_key_value,
- nil,
- binds)
- end
-
- def _update_record(values, id, id_was) # :nodoc:
- substitutes, binds = substitute_values values
-
- scope = @klass.unscoped
-
- if @klass.finder_needs_type_condition?
- scope.unscope!(where: @klass.inheritance_column)
- end
-
- relation = scope.where(@klass.primary_key => (id_was || id))
- bvs = binds + relation.bound_attributes
- um = relation
- .arel
- .compile_update(substitutes, @klass.primary_key)
-
- @klass.connection.update(
- um,
- 'SQL',
- bvs,
- )
+ def arel_attribute(name) # :nodoc:
+ klass.arel_attribute(name, table)
end
- def substitute_values(values) # :nodoc:
- binds = values.map do |arel_attr, value|
- QueryAttribute.new(arel_attr.name, value, klass.type_for_attribute(arel_attr.name))
- end
-
- substitutes = values.map do |(arel_attr, _)|
- [arel_attr, connection.substitute_at(klass.columns_hash[arel_attr.name])]
- end
-
- [substitutes, binds]
+ def bind_attribute(name, value) # :nodoc:
+ attr = arel_attribute(name)
+ bind = predicate_builder.build_bind_attribute(attr.name, value)
+ yield attr, bind
end
# Initializes new record from relation while maintaining the current
# scope.
#
- # Expects arguments in the same format as +Base.new+.
+ # Expects arguments in the same format as {ActiveRecord::Base.new}[rdoc-ref:Core.new].
#
# users = User.where(name: 'DHH')
# user = users.new # => #<User id: nil, name: "DHH", created_at: nil, updated_at: nil>
@@ -118,8 +61,8 @@ module ActiveRecord
#
# user = users.new { |user| user.name = 'Oscar' }
# user.name # => Oscar
- def new(*args, &block)
- scoping { @klass.new(*args, &block) }
+ def new(attributes = nil, &block)
+ scoping { klass.new(attributes, &block) }
end
alias build new
@@ -127,30 +70,34 @@ module ActiveRecord
# Tries to create a new record with the same scoped attributes
# defined in the relation. Returns the initialized object if validation fails.
#
- # Expects arguments in the same format as +Base.create+.
+ # Expects arguments in the same format as
+ # {ActiveRecord::Base.create}[rdoc-ref:Persistence::ClassMethods#create].
#
# ==== Examples
+ #
# users = User.where(name: 'Oscar')
- # users.create # #<User id: 3, name: "oscar", ...>
+ # users.create # => #<User id: 3, name: "Oscar", ...>
#
# users.create(name: 'fxn')
- # users.create # #<User id: 4, name: "fxn", ...>
+ # users.create # => #<User id: 4, name: "fxn", ...>
#
# users.create { |user| user.name = 'tenderlove' }
- # # #<User id: 5, name: "tenderlove", ...>
+ # # => #<User id: 5, name: "tenderlove", ...>
#
# users.create(name: nil) # validation on name
- # # #<User id: nil, name: nil, ...>
- def create(*args, &block)
- scoping { @klass.create(*args, &block) }
+ # # => #<User id: nil, name: nil, ...>
+ def create(attributes = nil, &block)
+ scoping { klass.create(attributes, &block) }
end
- # Similar to #create, but calls +create!+ on the base class. Raises
- # an exception if a validation error occurs.
+ # Similar to #create, but calls
+ # {create!}[rdoc-ref:Persistence::ClassMethods#create!]
+ # on the base class. Raises an exception if a validation error occurs.
#
- # Expects arguments in the same format as <tt>Base.create!</tt>.
- def create!(*args, &block)
- scoping { @klass.create!(*args, &block) }
+ # Expects arguments in the same format as
+ # {ActiveRecord::Base.create!}[rdoc-ref:Persistence::ClassMethods#create!].
+ def create!(attributes = nil, &block)
+ scoping { klass.create!(attributes, &block) }
end
def first_or_create(attributes = nil, &block) # :nodoc:
@@ -182,7 +129,7 @@ module ActiveRecord
# User.create_with(last_name: 'Johansson').find_or_create_by(first_name: 'Scarlett')
# # => #<User id: 2, first_name: "Scarlett", last_name: "Johansson">
#
- # This method accepts a block, which is passed down to +create+. The last example
+ # This method accepts a block, which is passed down to #create. The last example
# above can be alternatively written this way:
#
# # Find the first user named "Scarlett" or create a new one with a
@@ -194,36 +141,68 @@ module ActiveRecord
#
# This method always returns a record, but if creation was attempted and
# failed due to validation errors it won't be persisted, you get what
- # +create+ returns in such situation.
+ # #create returns in such situation.
#
- # Please note *this method is not atomic*, it runs first a SELECT, and if
+ # Please note <b>this method is not atomic</b>, it runs first a SELECT, and if
# there are no results an INSERT is attempted. If there are other threads
# or processes there is a race condition between both calls and it could
# be the case that you end up with two similar records.
#
- # Whether that is a problem or not depends on the logic of the
- # application, but in the particular case in which rows have a UNIQUE
- # constraint an exception may be raised, just retry:
- #
- # begin
- # CreditAccount.transaction(requires_new: true) do
- # CreditAccount.find_or_create_by(user_id: user.id)
- # end
- # rescue ActiveRecord::RecordNotUnique
- # retry
- # end
- #
+ # If this might be a problem for your application, please see #create_or_find_by.
def find_or_create_by(attributes, &block)
find_by(attributes) || create(attributes, &block)
end
- # Like <tt>find_or_create_by</tt>, but calls <tt>create!</tt> so an exception
+ # Like #find_or_create_by, but calls
+ # {create!}[rdoc-ref:Persistence::ClassMethods#create!] so an exception
# is raised if the created record is invalid.
def find_or_create_by!(attributes, &block)
find_by(attributes) || create!(attributes, &block)
end
- # Like <tt>find_or_create_by</tt>, but calls <tt>new</tt> instead of <tt>create</tt>.
+ # Attempts to create a record with the given attributes in a table that has a unique constraint
+ # on one or several of its columns. If a row already exists with one or several of these
+ # unique constraints, the exception such an insertion would normally raise is caught,
+ # and the existing record with those attributes is found using #find_by.
+ #
+ # This is similar to #find_or_create_by, but avoids the problem of stale reads between the SELECT
+ # and the INSERT, as that method needs to first query the table, then attempt to insert a row
+ # if none is found.
+ #
+ # There are several drawbacks to #create_or_find_by, though:
+ #
+ # * The underlying table must have the relevant columns defined with unique constraints.
+ # * A unique constraint violation may be triggered by only one, or at least less than all,
+ # of the given attributes. This means that the subsequent #find_by may fail to find a
+ # matching record, which will then raise an <tt>ActiveRecord::RecordNotFound</tt> exception,
+ # rather than a record with the given attributes.
+ # * While we avoid the race condition between SELECT -> INSERT from #find_or_create_by,
+ # we actually have another race condition between INSERT -> SELECT, which can be triggered
+ # if a DELETE between those two statements is run by another client. But for most applications,
+ # that's a significantly less likely condition to hit.
+ # * It relies on exception handling to handle control flow, which may be marginally slower.
+ #
+ # This method will return a record if all given attributes are covered by unique constraints
+ # (unless the INSERT -> DELETE -> SELECT race condition is triggered), but if creation was attempted
+ # and failed due to validation errors it won't be persisted, you get what #create returns in
+ # such situation.
+ def create_or_find_by(attributes, &block)
+ transaction(requires_new: true) { create(attributes, &block) }
+ rescue ActiveRecord::RecordNotUnique
+ find_by!(attributes)
+ end
+
+ # Like #create_or_find_by, but calls
+ # {create!}[rdoc-ref:Persistence::ClassMethods#create!] so an exception
+ # is raised if the created record is invalid.
+ def create_or_find_by!(attributes, &block)
+ transaction(requires_new: true) { create!(attributes, &block) }
+ rescue ActiveRecord::RecordNotUnique
+ find_by!(attributes)
+ end
+
+ # Like #find_or_create_by, but calls {new}[rdoc-ref:Core#new]
+ # instead of {create}[rdoc-ref:Persistence::ClassMethods#create].
def find_or_initialize_by(attributes, &block)
find_by(attributes) || new(attributes, &block)
end
@@ -236,25 +215,25 @@ module ActiveRecord
# are needed by the next ones when eager loading is going on.
#
# Please see further details in the
- # {Active Record Query Interface guide}[http://guides.rubyonrails.org/active_record_querying.html#running-explain].
+ # {Active Record Query Interface guide}[https://guides.rubyonrails.org/active_record_querying.html#running-explain].
def explain
- #TODO: Fix for binds.
exec_explain(collecting_queries_for_explain { exec_queries })
end
# Converts relation objects to Array.
- def to_a
+ def to_ary
+ records.dup
+ end
+ alias to_a to_ary
+
+ def records # :nodoc:
load
@records
end
# Serializes the relation objects Array.
def encode_with(coder)
- coder.represent_seq(nil, to_a)
- end
-
- def as_json(options = nil) #:nodoc:
- to_a.as_json(options)
+ coder.represent_seq(nil, records)
end
# Returns size of the records.
@@ -265,13 +244,7 @@ module ActiveRecord
# Returns true if there are no records.
def empty?
return @records.empty? if loaded?
-
- if limit_value == 0
- true
- else
- c = count(:all)
- c.respond_to?(:zero?) ? c.zero? : c.empty?
- end
+ !exists?
end
# Returns true if there are no records.
@@ -289,13 +262,39 @@ module ActiveRecord
# Returns true if there is exactly one record.
def one?
return super if block_given?
- limit_value ? to_a.one? : size == 1
+ limit_value ? records.one? : size == 1
end
# Returns true if there is more than one record.
def many?
return super if block_given?
- limit_value ? to_a.many? : size > 1
+ limit_value ? records.many? : size > 1
+ end
+
+ # Returns a cache key that can be used to identify the records fetched by
+ # this query. The cache key is built with a fingerprint of the sql query,
+ # the number of records matched by the query and a timestamp of the last
+ # updated record. When a new record comes to match the query, or any of
+ # the existing records is updated or deleted, the cache key changes.
+ #
+ # Product.where("name like ?", "%Cosmic Encounter%").cache_key
+ # # => "products/query-1850ab3d302391b85b8693e941286659-1-20150714212553907087000"
+ #
+ # If the collection is loaded, the method will iterate through the records
+ # to generate the timestamp, otherwise it will trigger one SQL query like:
+ #
+ # SELECT COUNT(*), MAX("products"."updated_at") FROM "products" WHERE (name like '%Cosmic Encounter%')
+ #
+ # You can also pass a custom timestamp column to fetch the timestamp of the
+ # last updated record.
+ #
+ # Product.where("name like ?", "%Game%").cache_key(:last_reviewed_at)
+ #
+ # You can customize the strategy to generate the key on a per model basis
+ # overriding ActiveRecord::Base#collection_cache_key.
+ def cache_key(timestamp_column = :updated_at)
+ @cache_keys ||= {}
+ @cache_keys[timestamp_column] ||= @klass.collection_cache_key(self, timestamp_column)
end
# Scope all queries to the current scope.
@@ -308,17 +307,20 @@ module ActiveRecord
# Please check unscoped if you want to remove all previous scopes (including
# the default_scope) during the execution of a block.
def scoping
- previous, klass.current_scope = klass.current_scope, self
- yield
+ @delegate_to_klass ? yield : klass._scoping(self) { yield }
+ end
+
+ def _exec_scope(*args, &block) # :nodoc:
+ @delegate_to_klass = true
+ instance_exec(*args, &block) || self
ensure
- klass.current_scope = previous
+ @delegate_to_klass = false
end
# Updates all records in the current relation with details given. This method constructs a single SQL UPDATE
# statement and sends it straight to the database. It does not instantiate the involved models and it does not
- # trigger Active Record callbacks or validations. Values passed to `update_all` will not go through
- # ActiveRecord's type-casting behavior. It should receive only values that can be passed as-is to the SQL
- # database.
+ # trigger Active Record callbacks or validations. However, values passed to #update_all will still go through
+ # Active Record's normal type casting and serialization.
#
# ==== Parameters
#
@@ -334,68 +336,99 @@ module ActiveRecord
#
# # Update all books that match conditions, but limit it to 5 ordered by date
# Book.where('title LIKE ?', '%Rails%').order(:created_at).limit(5).update_all(author: 'David')
+ #
+ # # Update all invoices and set the number column to its id value.
+ # Invoice.update_all('number = id')
def update_all(updates)
raise ArgumentError, "Empty list of attributes to change" if updates.blank?
+ if eager_loading?
+ relation = apply_join_dependency
+ return relation.update_all(updates)
+ end
+
stmt = Arel::UpdateManager.new
+ stmt.table(arel.join_sources.empty? ? table : arel.source)
+ stmt.key = arel_attribute(primary_key)
+ stmt.take(arel.limit)
+ stmt.offset(arel.offset)
+ stmt.order(*arel.orders)
+ stmt.wheres = arel.constraints
+
+ if updates.is_a?(Hash)
+ stmt.set _substitute_values(updates)
+ else
+ stmt.set Arel.sql(klass.sanitize_sql_for_assignment(updates, table.name))
+ end
- stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates))
- stmt.table(table)
- stmt.key = table[primary_key]
+ @klass.connection.update stmt, "#{@klass} Update All"
+ end
- if joins_values.any?
- @klass.connection.join_to_update(stmt, arel)
+ def update(id = :all, attributes) # :nodoc:
+ if id == :all
+ each { |record| record.update(attributes) }
else
- stmt.take(arel.limit)
- stmt.order(*arel.orders)
- stmt.wheres = arel.constraints
+ klass.update(id, attributes)
+ end
+ end
+
+ def update_counters(counters) # :nodoc:
+ touch = counters.delete(:touch)
+
+ updates = {}
+ counters.each do |counter_name, value|
+ attr = arel_attribute(counter_name)
+ bind = predicate_builder.build_bind_attribute(attr.name, value.abs)
+ expr = table.coalesce(Arel::Nodes::UnqualifiedColumn.new(attr), 0)
+ expr = value < 0 ? expr - bind : expr + bind
+ updates[counter_name] = expr.expr
+ end
+
+ if touch
+ names = touch if touch != true
+ touch_updates = klass.touch_attributes_with_time(*names)
+ updates.merge!(touch_updates) unless touch_updates.empty?
end
- @klass.connection.update stmt, 'SQL', bound_attributes
+ update_all updates
end
- # Updates an object (or multiple objects) and saves it to the database, if validations pass.
- # The resulting object is returned whether the object was saved successfully to the database or not.
+ # Touches all records in the current relation without instantiating records first with the updated_at/on attributes
+ # set to the current time or the time specified.
+ # This method can be passed attribute names and an optional time argument.
+ # If attribute names are passed, they are updated along with updated_at/on attributes.
+ # If no time argument is passed, the current time is used as default.
#
- # ==== Parameters
+ # === Examples
#
- # * +id+ - This should be the id or an array of ids to be updated.
- # * +attributes+ - This should be a hash of attributes or an array of hashes.
+ # # Touch all records
+ # Person.all.touch_all
+ # # => "UPDATE \"people\" SET \"updated_at\" = '2018-01-04 22:55:23.132670'"
#
- # ==== Examples
+ # # Touch multiple records with a custom attribute
+ # Person.all.touch_all(:created_at)
+ # # => "UPDATE \"people\" SET \"updated_at\" = '2018-01-04 22:55:23.132670', \"created_at\" = '2018-01-04 22:55:23.132670'"
+ #
+ # # Touch multiple records with a specified time
+ # Person.all.touch_all(time: Time.new(2020, 5, 16, 0, 0, 0))
+ # # => "UPDATE \"people\" SET \"updated_at\" = '2020-05-16 00:00:00'"
#
- # # Updates one record
- # Person.update(15, user_name: 'Samuel', group: 'expert')
- #
- # # Updates multiple records
- # people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } }
- # Person.update(people.keys, people.values)
- #
- # # Updates multiple records from the result of a relation
- # people = Person.where(group: 'expert')
- # people.update(group: 'masters')
- #
- # Note: Updating a large number of records will run a
- # UPDATE query for each record, which may cause a performance
- # issue. So if it is not needed to run callbacks for each update, it is
- # preferred to use <tt>update_all</tt> for updating all records using
- # a single query.
- def update(id = :all, attributes)
- if id.is_a?(Array)
- id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) }
- elsif id == :all
- to_a.each { |record| record.update(attributes) }
+ # # Touch records with scope
+ # Person.where(name: 'David').touch_all
+ # # => "UPDATE \"people\" SET \"updated_at\" = '2018-01-04 22:55:23.132670' WHERE \"people\".\"name\" = 'David'"
+ def touch_all(*names, time: nil)
+ if klass.locking_enabled?
+ names << { time: time }
+ update_counters(klass.locking_column => 1, touch: names)
else
- object = find(id)
- object.update(attributes)
- object
+ update_all klass.touch_attributes_with_time(*names, time: time)
end
end
- # Destroys the records matching +conditions+ by instantiating each
- # record and calling its +destroy+ method. Each object's callbacks are
- # executed (including <tt>:dependent</tt> association options). Returns the
- # collection of objects that were destroyed; each will be frozen, to
+ # Destroys the records by instantiating each
+ # record and calling its {#destroy}[rdoc-ref:Persistence#destroy] method.
+ # Each object's callbacks are executed (including <tt>:dependent</tt> association options).
+ # Returns the collection of objects that were destroyed; each will be frozen, to
# reflect that no changes should be made (since they can't be persisted).
#
# Note: Instantiation, callback execution, and deletion of each
@@ -403,128 +436,59 @@ module ActiveRecord
# once. It generates at least one SQL +DELETE+ query per record (or
# possibly more, to enforce your callbacks). If you want to delete many
# rows quickly, without concern for their associations or callbacks, use
- # +delete_all+ instead.
- #
- # ==== Parameters
- #
- # * +conditions+ - A string, array, or hash that specifies which records
- # to destroy. If omitted, all records are destroyed. See the
- # Conditions section in the introduction to ActiveRecord::Base for
- # more information.
+ # #delete_all instead.
#
# ==== Examples
#
- # Person.destroy_all("last_login < '2004-04-04'")
- # Person.destroy_all(status: "inactive")
# Person.where(age: 0..18).destroy_all
- def destroy_all(conditions = nil)
- if conditions
- where(conditions).destroy_all
- else
- to_a.each(&:destroy).tap { reset }
- end
- end
-
- # Destroy an object (or multiple objects) that has the given id. The object is instantiated first,
- # therefore all callbacks and filters are fired off before the object is deleted. This method is
- # less efficient than ActiveRecord#delete but allows cleanup methods and other actions to be run.
- #
- # This essentially finds the object (or multiple objects) with the given id, creates a new object
- # from the attributes, and then calls destroy on it.
- #
- # ==== Parameters
- #
- # * +id+ - Can be either an Integer or an Array of Integers.
- #
- # ==== Examples
- #
- # # Destroy a single object
- # Todo.destroy(1)
- #
- # # Destroy multiple objects
- # todos = [1,2,3]
- # Todo.destroy(todos)
- def destroy(id)
- if id.is_a?(Array)
- id.map { |one_id| destroy(one_id) }
- else
- find(id).destroy
- end
+ def destroy_all
+ records.each(&:destroy).tap { reset }
end
- # Deletes the records matching +conditions+ without instantiating the records
- # first, and hence not calling the +destroy+ method nor invoking callbacks. This
- # is a single SQL DELETE statement that goes straight to the database, much more
- # efficient than +destroy_all+. Be careful with relations though, in particular
+ # Deletes the records without instantiating the records
+ # first, and hence not calling the {#destroy}[rdoc-ref:Persistence#destroy]
+ # method nor invoking callbacks.
+ # This is a single SQL DELETE statement that goes straight to the database, much more
+ # efficient than #destroy_all. Be careful with relations though, in particular
# <tt>:dependent</tt> rules defined on associations are not honored. Returns the
# number of rows affected.
#
- # Post.delete_all("person_id = 5 AND (category = 'Something' OR category = 'Else')")
- # Post.delete_all(["person_id = ? AND (category = ? OR category = ?)", 5, 'Something', 'Else'])
# Post.where(person_id: 5).where(category: ['Something', 'Else']).delete_all
#
# Both calls delete the affected posts all at once with a single DELETE statement.
# If you need to destroy dependent associations or call your <tt>before_*</tt> or
- # +after_destroy+ callbacks, use the +destroy_all+ method instead.
- #
- # If an invalid method is supplied, +delete_all+ raises an ActiveRecord error:
- #
- # Post.limit(100).delete_all
- # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit
- def delete_all(conditions = nil)
- invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select { |method|
- if MULTI_VALUE_METHODS.include?(method)
- send("#{method}_values").any?
- elsif SINGLE_VALUE_METHODS.include?(method)
- send("#{method}_value")
- elsif CLAUSE_METHODS.include?(method)
- send("#{method}_clause").any?
- end
- }
+ # +after_destroy+ callbacks, use the #destroy_all method instead.
+ #
+ # If an invalid method is supplied, #delete_all raises an ActiveRecordError:
+ #
+ # Post.distinct.delete_all
+ # # => ActiveRecord::ActiveRecordError: delete_all doesn't support distinct
+ def delete_all
+ invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select do |method|
+ value = get_value(method)
+ SINGLE_VALUE_METHODS.include?(method) ? value : value.any?
+ end
if invalid_methods.any?
raise ActiveRecordError.new("delete_all doesn't support #{invalid_methods.join(', ')}")
end
- if conditions
- where(conditions).delete_all
- else
- stmt = Arel::DeleteManager.new
- stmt.from(table)
-
- if joins_values.any?
- @klass.connection.join_to_delete(stmt, arel, table[primary_key])
- else
- stmt.wheres = arel.constraints
- end
+ if eager_loading?
+ relation = apply_join_dependency
+ return relation.delete_all
+ end
- affected = @klass.connection.delete(stmt, 'SQL', bound_attributes)
+ stmt = Arel::DeleteManager.new
+ stmt.from(arel.join_sources.empty? ? table : arel.source)
+ stmt.key = arel_attribute(primary_key)
+ stmt.take(arel.limit)
+ stmt.offset(arel.offset)
+ stmt.order(*arel.orders)
+ stmt.wheres = arel.constraints
- reset
- affected
- end
- end
+ affected = @klass.connection.delete(stmt, "#{@klass} Destroy")
- # Deletes the row with a primary key matching the +id+ argument, using a
- # SQL +DELETE+ statement, and returns the number of rows deleted. Active
- # Record objects are not instantiated, so the object's callbacks are not
- # executed, including any <tt>:dependent</tt> association options.
- #
- # You can delete multiple rows at once by passing an Array of <tt>id</tt>s.
- #
- # Note: Although it is often much faster than the alternative,
- # <tt>#destroy</tt>, skipping callbacks might bypass business logic in
- # your application that ensures referential integrity or performs other
- # essential jobs.
- #
- # ==== Examples
- #
- # # Delete a single row
- # Todo.delete(1)
- #
- # # Delete multiple rows
- # Todo.delete([2,3,4])
- def delete(id_or_array)
- where(primary_key => id_or_array).delete_all
+ reset
+ affected
end
# Causes the records to be loaded from the database if they have not
@@ -533,8 +497,8 @@ module ActiveRecord
# return value is the relation itself, not the records.
#
# Post.where(published: true).load # => #<ActiveRecord::Relation>
- def load
- exec_queries unless loaded?
+ def load(&block)
+ exec_queries(&block) unless loaded?
self
end
@@ -546,9 +510,9 @@ module ActiveRecord
end
def reset
- @last = @to_sql = @order_clause = @scope_for_create = @arel = @loaded = nil
- @should_eager_load = @join_dependency = nil
- @records = []
+ @delegate_to_klass = false
+ @to_sql = @arel = @loaded = @should_eager_load = nil
+ @records = [].freeze
@offsets = {}
self
end
@@ -559,32 +523,28 @@ module ActiveRecord
# # => SELECT "users".* FROM "users" WHERE "users"."name" = 'Oscar'
def to_sql
@to_sql ||= begin
- relation = self
- connection = klass.connection
- visitor = connection.visitor
-
- if eager_loading?
- find_with_associations { |rel| relation = rel }
- end
-
- binds = relation.bound_attributes
- binds = connection.prepare_binds_for_database(binds)
- binds.map! { |value| connection.quote(value) }
- collect = visitor.accept(relation.arel.ast, Arel::Collectors::Bind.new)
- collect.substitute_binds(binds).join
- end
+ if eager_loading?
+ apply_join_dependency do |relation, join_dependency|
+ relation = join_dependency.apply_column_aliases(relation)
+ relation.to_sql
+ end
+ else
+ conn = klass.connection
+ conn.unprepared_statement { conn.to_sql(arel) }
+ end
+ end
end
# Returns a hash of where conditions.
#
# User.where(name: 'Oscar').where_values_hash
# # => {name: "Oscar"}
- def where_values_hash(relation_table_name = table_name)
+ def where_values_hash(relation_table_name = klass.table_name)
where_clause.to_h(relation_table_name)
end
def scope_for_create
- @scope_for_create ||= where_values_hash.merge(create_with_value)
+ where_values_hash.merge!(create_with_value.stringify_keys)
end
# Returns true if relation needs eager loading.
@@ -602,89 +562,144 @@ module ActiveRecord
includes_values & joins_values
end
- # +uniq+ and +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
when Associations::CollectionProxy, AssociationRelation
- self == other.to_a
+ self == other.records
when Relation
other.to_sql == to_sql
when Array
- to_a == other
+ records == other
end
end
def pretty_print(q)
- q.pp(self.to_a)
+ q.pp(records)
end
# Returns true if relation is blank.
def blank?
- to_a.blank?
+ records.blank?
end
def values
- Hash[@values]
+ @values.dup
end
def inspect
- entries = to_a.take([limit_value, 11].compact.min).map!(&:inspect)
- entries[10] = '...' if entries.size == 11
+ subject = loaded? ? records : self
+ entries = subject.take([limit_value, 11].compact.min).map!(&:inspect)
+
+ entries[10] = "..." if entries.size == 11
"#<#{self.class.name} [#{entries.join(', ')}]>"
end
- private
+ def empty_scope? # :nodoc:
+ @values == klass.unscoped.values
+ end
+
+ def has_limit_or_offset? # :nodoc:
+ limit_value || offset_value
+ end
- def exec_queries
- @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bound_attributes)
+ def alias_tracker(joins = [], aliases = nil) # :nodoc:
+ joins += [aliases] if aliases
+ ActiveRecord::Associations::AliasTracker.create(connection, table.name, joins)
+ end
+ def preload_associations(records) # :nodoc:
preload = preload_values
- preload += includes_values unless eager_loading?
- preloader = build_preloader
+ preload += includes_values unless eager_loading?
+ preloader = nil
preload.each do |associations|
- preloader.preload @records, associations
+ preloader ||= build_preloader
+ preloader.preload records, associations
end
+ end
- @records.each(&:readonly!) if readonly_value
+ protected
- @loaded = true
- @records
- end
+ def load_records(records)
+ @records = records.freeze
+ @loaded = true
+ end
- def build_preloader
- ActiveRecord::Associations::Preloader.new
- end
+ private
+ def _substitute_values(values)
+ values.map do |name, value|
+ attr = arel_attribute(name)
+ unless Arel.arel_node?(value)
+ type = klass.type_for_attribute(attr.name)
+ value = predicate_builder.build_bind_attribute(attr.name, type.cast(value))
+ end
+ [attr, value]
+ end
+ end
- def references_eager_loaded_tables?
- joined_tables = arel.join_sources.map do |join|
- if join.is_a?(Arel::Nodes::StringJoin)
- tables_in_string(join.left)
+ def exec_queries(&block)
+ skip_query_cache_if_necessary do
+ @records =
+ if eager_loading?
+ apply_join_dependency do |relation, join_dependency|
+ if ActiveRecord::NullRelation === relation
+ []
+ else
+ relation = join_dependency.apply_column_aliases(relation)
+ rows = connection.select_all(relation.arel, "SQL")
+ join_dependency.instantiate(rows, &block)
+ end.freeze
+ end
+ else
+ klass.find_by_sql(arel, &block).freeze
+ end
+
+ preload_associations(@records) unless skip_preloading_value
+
+ @records.each(&:readonly!) if readonly_value
+
+ @loaded = true
+ @records
+ end
+ end
+
+ def skip_query_cache_if_necessary
+ if skip_query_cache_value
+ uncached do
+ yield
+ end
else
- [join.left.table_name, join.left.table_alias]
+ yield
end
end
- joined_tables += [table.name, table.table_alias]
+ def build_preloader
+ ActiveRecord::Associations::Preloader.new
+ end
- # always convert table names to downcase as in Oracle quoted table names are in uppercase
- joined_tables = joined_tables.flatten.compact.map(&:downcase).uniq
+ def references_eager_loaded_tables?
+ joined_tables = arel.join_sources.map do |join|
+ if join.is_a?(Arel::Nodes::StringJoin)
+ tables_in_string(join.left)
+ else
+ [join.left.table_name, join.left.table_alias]
+ end
+ end
- (references_values - joined_tables).any?
- end
+ joined_tables += [table.name, table.table_alias]
- def tables_in_string(string)
- return [] if string.blank?
- # always convert table names to downcase as in Oracle quoted table names are in uppercase
- # ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries
- string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map(&:downcase).uniq - ['raw_sql_']
- end
+ # always convert table names to downcase as in Oracle quoted table names are in uppercase
+ joined_tables = joined_tables.flatten.compact.map(&:downcase).uniq
+
+ (references_values - joined_tables).any?
+ end
+
+ def tables_in_string(string)
+ return [] if string.blank?
+ # always convert table names to downcase as in Oracle quoted table names are in uppercase
+ # ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries
+ string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map(&:downcase).uniq - ["raw_sql_"]
+ end
end
end
diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb
index e07580a563..9c579843b1 100644
--- a/activerecord/lib/active_record/relation/batches.rb
+++ b/activerecord/lib/active_record/relation/batches.rb
@@ -1,8 +1,14 @@
+# frozen_string_literal: true
+
+require "active_record/relation/batches/batch_enumerator"
+
module ActiveRecord
module Batches
+ ORDER_IGNORE_MESSAGE = "Scoped order is ignored, it's forced to be batch order."
+
# Looping through a collection of records from the database
- # (using the +all+ method, for example) is very inefficient
- # since it will try to instantiate all the objects at once.
+ # (using the Scoping::Named::ClassMethods.all method, for example)
+ # is very inefficient since it will try to instantiate all the objects at once.
#
# In that case, batch processing methods allow you to work
# with the records in batches, thereby greatly reducing memory consumption.
@@ -26,16 +32,28 @@ module ActiveRecord
# end
#
# ==== Options
- # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000.
- # * <tt>:begin_at</tt> - Specifies the primary key value to start from, inclusive of the value.
- # * <tt>:end_at</tt> - Specifies the primary key value to end at, inclusive of the value.
- # This is especially useful if you want multiple workers dealing with
- # the same processing queue. You can make worker 1 handle all the records
- # between id 0 and 10,000 and worker 2 handle from 10,000 and beyond
- # (by setting the +:begin_at+ and +:end_at+ option on each worker).
- #
- # # Let's process for a batch of 2000 records, skipping the first 2000 rows
- # Person.find_each(begin_at: 2000, batch_size: 2000) do |person|
+ # * <tt>:batch_size</tt> - Specifies the size of the batch. Defaults to 1000.
+ # * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value.
+ # * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value.
+ # * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when
+ # an order is present in the relation.
+ #
+ # Limits are honored, and if present there is no requirement for the batch
+ # size: it can be less than, equal to, or greater than the limit.
+ #
+ # The options +start+ and +finish+ are especially useful if you want
+ # multiple workers dealing with the same processing queue. You can make
+ # worker 1 handle all the records between id 1 and 9999 and worker 2
+ # handle from 10000 and beyond by setting the +:start+ and +:finish+
+ # option on each worker.
+ #
+ # # In worker 1, let's process until 9999 records.
+ # Person.find_each(finish: 9_999) do |person|
+ # person.party_all_night!
+ # end
+ #
+ # # In worker 2, let's process from record 10_000 and onwards.
+ # Person.find_each(start: 10_000) do |person|
# person.party_all_night!
# end
#
@@ -44,24 +62,17 @@ module ActiveRecord
# work. This also means that this method only works when the primary key is
# orderable (e.g. an integer or string).
#
- # NOTE: You can't set the limit either, that's used to control
- # the batch sizes.
- def find_each(begin_at: nil, end_at: nil, batch_size: 1000, start: nil)
- if start
- begin_at = start
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Passing `start` value to find_each is deprecated, and will be removed in Rails 5.1.
- Please pass `begin_at` instead.
- MSG
- end
+ # NOTE: By its nature, batch processing is subject to race conditions if
+ # other processes are modifying the database.
+ def find_each(start: nil, finish: nil, batch_size: 1000, error_on_ignore: nil)
if block_given?
- find_in_batches(begin_at: begin_at, end_at: end_at, batch_size: batch_size) do |records|
+ find_in_batches(start: start, finish: finish, batch_size: batch_size, error_on_ignore: error_on_ignore) do |records|
records.each { |record| yield record }
end
else
- enum_for(:find_each, begin_at: begin_at, end_at: end_at, batch_size: batch_size) do
+ enum_for(:find_each, start: start, finish: finish, batch_size: batch_size, error_on_ignore: error_on_ignore) do
relation = self
- apply_limits(relation, begin_at, end_at).size
+ apply_limits(relation, start, finish).size
end
end
end
@@ -85,16 +96,23 @@ module ActiveRecord
# To be yielded each record one by one, use #find_each instead.
#
# ==== Options
- # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000.
- # * <tt>:begin_at</tt> - Specifies the primary key value to start from, inclusive of the value.
- # * <tt>:end_at</tt> - Specifies the primary key value to end at, inclusive of the value.
- # This is especially useful if you want multiple workers dealing with
- # the same processing queue. You can make worker 1 handle all the records
- # between id 0 and 10,000 and worker 2 handle from 10,000 and beyond
- # (by setting the +:begin_at+ and +:end_at+ option on each worker).
- #
- # # Let's process the next 2000 records
- # Person.find_in_batches(begin_at: 2000, batch_size: 2000) do |group|
+ # * <tt>:batch_size</tt> - Specifies the size of the batch. Defaults to 1000.
+ # * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value.
+ # * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value.
+ # * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when
+ # an order is present in the relation.
+ #
+ # Limits are honored, and if present there is no requirement for the batch
+ # size: it can be less than, equal to, or greater than the limit.
+ #
+ # The options +start+ and +finish+ are especially useful if you want
+ # multiple workers dealing with the same processing queue. You can make
+ # worker 1 handle all the records between id 1 and 9999 and worker 2
+ # handle from 10000 and beyond by setting the +:start+ and +:finish+
+ # option on each worker.
+ #
+ # # Let's process from record 10_000 on.
+ # Person.find_in_batches(start: 10_000) do |group|
# group.each { |person| person.party_all_night! }
# end
#
@@ -103,56 +121,170 @@ module ActiveRecord
# work. This also means that this method only works when the primary key is
# orderable (e.g. an integer or string).
#
- # NOTE: You can't set the limit either, that's used to control
- # the batch sizes.
- def find_in_batches(begin_at: nil, end_at: nil, batch_size: 1000, start: nil)
- if start
- begin_at = start
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Passing `start` value to find_in_batches is deprecated, and will be removed in Rails 5.1.
- Please pass `begin_at` instead.
- MSG
- end
-
+ # NOTE: By its nature, batch processing is subject to race conditions if
+ # other processes are modifying the database.
+ def find_in_batches(start: nil, finish: nil, batch_size: 1000, error_on_ignore: nil)
relation = self
unless block_given?
- return to_enum(:find_in_batches, begin_at: begin_at, end_at: end_at, batch_size: batch_size) do
- total = apply_limits(relation, begin_at, end_at).size
+ return to_enum(:find_in_batches, start: start, finish: finish, batch_size: batch_size, error_on_ignore: error_on_ignore) do
+ total = apply_limits(relation, start, finish).size
(total - 1).div(batch_size) + 1
end
end
- if logger && (arel.orders.present? || arel.taken.present?)
- logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size")
+ in_batches(of: batch_size, start: start, finish: finish, load: true, error_on_ignore: error_on_ignore) do |batch|
+ yield batch.to_a
end
+ end
- relation = relation.reorder(batch_order).limit(batch_size)
- relation = apply_limits(relation, begin_at, end_at)
- records = relation.to_a
+ # Yields ActiveRecord::Relation objects to work with a batch of records.
+ #
+ # Person.where("age > 21").in_batches do |relation|
+ # relation.delete_all
+ # sleep(10) # Throttle the delete queries
+ # end
+ #
+ # If you do not provide a block to #in_batches, it will return a
+ # BatchEnumerator which is enumerable.
+ #
+ # Person.in_batches.each_with_index do |relation, batch_index|
+ # puts "Processing relation ##{batch_index}"
+ # relation.delete_all
+ # end
+ #
+ # Examples of calling methods on the returned BatchEnumerator object:
+ #
+ # Person.in_batches.delete_all
+ # Person.in_batches.update_all(awesome: true)
+ # Person.in_batches.each_record(&:party_all_night!)
+ #
+ # ==== Options
+ # * <tt>:of</tt> - Specifies the size of the batch. Defaults to 1000.
+ # * <tt>:load</tt> - Specifies if the relation should be loaded. Defaults to false.
+ # * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value.
+ # * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value.
+ # * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when
+ # an order is present in the relation.
+ #
+ # Limits are honored, and if present there is no requirement for the batch
+ # size, it can be less than, equal, or greater than the limit.
+ #
+ # The options +start+ and +finish+ are especially useful if you want
+ # multiple workers dealing with the same processing queue. You can make
+ # worker 1 handle all the records between id 1 and 9999 and worker 2
+ # handle from 10000 and beyond by setting the +:start+ and +:finish+
+ # option on each worker.
+ #
+ # # Let's process from record 10_000 on.
+ # Person.in_batches(start: 10_000).update_all(awesome: true)
+ #
+ # An example of calling where query method on the relation:
+ #
+ # Person.in_batches.each do |relation|
+ # relation.update_all('age = age + 1')
+ # relation.where('age > 21').update_all(should_party: true)
+ # relation.where('age <= 21').delete_all
+ # end
+ #
+ # NOTE: If you are going to iterate through each record, you should call
+ # #each_record on the yielded BatchEnumerator:
+ #
+ # Person.in_batches.each_record(&:party_all_night!)
+ #
+ # NOTE: It's not possible to set the order. That is automatically set to
+ # ascending on the primary key ("id ASC") to make the batch ordering
+ # consistent. Therefore the primary key must be orderable, e.g. an integer
+ # or a string.
+ #
+ # NOTE: By its nature, batch processing is subject to race conditions if
+ # other processes are modifying the database.
+ def in_batches(of: 1000, start: nil, finish: nil, load: false, error_on_ignore: nil)
+ relation = self
+ unless block_given?
+ return BatchEnumerator.new(of: of, start: start, finish: finish, relation: self)
+ end
+
+ if arel.orders.present?
+ act_on_ignored_order(error_on_ignore)
+ end
+
+ batch_limit = of
+ if limit_value
+ remaining = limit_value
+ batch_limit = remaining if remaining < batch_limit
+ end
+
+ relation = relation.reorder(batch_order).limit(batch_limit)
+ relation = apply_limits(relation, start, finish)
+ relation.skip_query_cache! # Retaining the results in the query cache would undermine the point of batching
+ batch_relation = relation
+
+ loop do
+ if load
+ records = batch_relation.records
+ ids = records.map(&:id)
+ yielded_relation = where(primary_key => ids)
+ yielded_relation.load_records(records)
+ else
+ ids = batch_relation.pluck(primary_key)
+ yielded_relation = where(primary_key => ids)
+ end
+
+ break if ids.empty?
- while records.any?
- records_size = records.size
- primary_key_offset = records.last.id
- raise "Primary key not included in the custom select clause" unless primary_key_offset
+ primary_key_offset = ids.last
+ raise ArgumentError.new("Primary key not included in the custom select clause") unless primary_key_offset
- yield records
+ yield yielded_relation
- break if records_size < batch_size
+ break if ids.length < batch_limit
- records = relation.where(table[primary_key].gt(primary_key_offset)).to_a
+ if limit_value
+ remaining -= ids.length
+
+ if remaining == 0
+ # Saves a useless iteration when the limit is a multiple of the
+ # batch size.
+ break
+ elsif remaining < batch_limit
+ relation = relation.limit(remaining)
+ end
+ end
+
+ batch_relation = relation.where(
+ bind_attribute(primary_key, primary_key_offset) { |attr, bind| attr.gt(bind) }
+ )
end
end
private
- def apply_limits(relation, begin_at, end_at)
- relation = relation.where(table[primary_key].gteq(begin_at)) if begin_at
- relation = relation.where(table[primary_key].lteq(end_at)) if end_at
- relation
- end
+ def apply_limits(relation, start, finish)
+ relation = apply_start_limit(relation, start) if start
+ relation = apply_finish_limit(relation, finish) if finish
+ relation
+ end
- def batch_order
- "#{quoted_table_name}.#{quoted_primary_key} ASC"
- end
+ def apply_start_limit(relation, start)
+ relation.where(bind_attribute(primary_key, start) { |attr, bind| attr.gteq(bind) })
+ end
+
+ def apply_finish_limit(relation, finish)
+ relation.where(bind_attribute(primary_key, finish) { |attr, bind| attr.lteq(bind) })
+ end
+
+ def batch_order
+ arel_attribute(primary_key).asc
+ end
+
+ def act_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)
+ elsif logger
+ logger.warn(ORDER_IGNORE_MESSAGE)
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/relation/batches/batch_enumerator.rb b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb
new file mode 100644
index 0000000000..49697da3bf
--- /dev/null
+++ b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module Batches
+ class BatchEnumerator
+ include Enumerable
+
+ def initialize(of: 1000, start: nil, finish: nil, relation:) #:nodoc:
+ @of = of
+ @relation = relation
+ @start = start
+ @finish = finish
+ end
+
+ # Looping through a collection of records from the database (using the
+ # +all+ method, for example) is very inefficient since it will try to
+ # instantiate all the objects at once.
+ #
+ # In that case, batch processing methods allow you to work with the
+ # records in batches, thereby greatly reducing memory consumption.
+ #
+ # Person.in_batches.each_record do |person|
+ # person.do_awesome_stuff
+ # end
+ #
+ # Person.where("age > 21").in_batches(of: 10).each_record do |person|
+ # person.party_all_night!
+ # end
+ #
+ # If you do not provide a block to #each_record, it will return an Enumerator
+ # for chaining with other methods:
+ #
+ # Person.in_batches.each_record.with_index do |person, index|
+ # person.award_trophy(index + 1)
+ # end
+ def each_record
+ return to_enum(:each_record) unless block_given?
+
+ @relation.to_enum(:in_batches, of: @of, start: @start, finish: @finish, load: true).each do |relation|
+ relation.records.each { |record| yield record }
+ end
+ end
+
+ # Delegates #delete_all, #update_all, #destroy_all methods to each batch.
+ #
+ # People.in_batches.delete_all
+ # People.where('age < 10').in_batches.destroy_all
+ # People.in_batches.update_all('age = age + 1')
+ [:delete_all, :update_all, :destroy_all].each do |method|
+ define_method(method) do |*args, &block|
+ @relation.to_enum(:in_batches, of: @of, start: @start, finish: @finish, load: false).each do |relation|
+ relation.send(method, *args, &block)
+ end
+ end
+ end
+
+ # Yields an ActiveRecord::Relation object for each batch of records.
+ #
+ # Person.in_batches.each do |relation|
+ # relation.update_all(awesome: true)
+ # end
+ def each
+ enum = @relation.to_enum(:in_batches, of: @of, start: @start, finish: @finish, load: false)
+ return enum.each { |relation| yield relation } if block_given?
+ enum
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index 0f6015fa93..0fa5ba2e50 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Calculations
# Count the records.
@@ -14,33 +16,44 @@ module ActiveRecord
# Person.distinct.count(:age)
# # => counts the number of different age values
#
- # If +count+ is used with +group+, it returns a Hash whose keys represent the aggregated column,
+ # If #count is used with {Relation#group}[rdoc-ref:QueryMethods#group],
+ # it returns a Hash whose keys represent the aggregated column,
# and the values are the respective amounts:
#
# Person.group(:city).count
# # => { 'Rome' => 5, 'Paris' => 3 }
#
- # If +count+ is used with +group+ for multiple columns, it returns a Hash whose
+ # If #count is used with {Relation#group}[rdoc-ref:QueryMethods#group] for multiple columns, it returns a Hash whose
# keys are an array containing the individual values of each column and the value
- # of each key would be the +count+.
+ # of each key would be the #count.
#
# Article.group(:status, :category).count
# # => {["draft", "business"]=>10, ["draft", "technology"]=>4,
# ["published", "business"]=>0, ["published", "technology"]=>2}
#
- # If +count+ is used with +select+, it will count the selected columns:
+ # If #count is used with {Relation#select}[rdoc-ref:QueryMethods#select], it will count the selected columns:
#
# Person.select(:age).count
# # => counts the number of different age values
#
- # Note: not all valid +select+ expressions are valid +count+ expressions. The specifics differ
+ # 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?
+ unless column_name.nil?
+ ActiveSupport::Deprecation.warn \
+ "When `count' is called with a block, it ignores other arguments. " \
+ "This behavior is now deprecated and will result in an ArgumentError in Rails 6.0."
+ end
+
+ return super()
+ end
+
calculate(:count, column_name)
end
# Calculates the average value on a given column. Returns +nil+ if there's
- # no row. See +calculate+ for examples with options.
+ # no row. See #calculate for examples with options.
#
# Person.average(:age) # => 35.8
def average(column_name)
@@ -49,7 +62,7 @@ module ActiveRecord
# Calculates the minimum value on a given column. The value is returned
# with the same data type of the column, or +nil+ if there's no row. See
- # +calculate+ for examples with options.
+ # #calculate for examples with options.
#
# Person.minimum(:age) # => 7
def minimum(column_name)
@@ -58,7 +71,7 @@ module ActiveRecord
# Calculates the maximum value on a given column. The value is returned
# with the same data type of the column, or +nil+ if there's no row. See
- # +calculate+ for examples with options.
+ # #calculate for examples with options.
#
# Person.maximum(:age) # => 93
def maximum(column_name)
@@ -66,59 +79,74 @@ module ActiveRecord
end
# Calculates the sum of values on a given column. The value is returned
- # with the same data type of the column, 0 if there's no row. See
- # +calculate+ for examples with options.
+ # with the same data type of the column, +0+ if there's no row. See
+ # #calculate for examples with options.
#
# Person.sum(:age) # => 4562
- def sum(*args)
- return super if block_given?
- calculate(:sum, *args)
+ def sum(column_name = nil)
+ if block_given?
+ unless column_name.nil?
+ ActiveSupport::Deprecation.warn \
+ "When `sum' is called with a block, it ignores other arguments. " \
+ "This behavior is now deprecated and will result in an ArgumentError in Rails 6.0."
+ end
+
+ return super()
+ end
+
+ calculate(:sum, column_name)
end
- # This calculates aggregate values in the given column. Methods for count, sum, average,
- # minimum, and maximum have been added as shortcuts.
+ # This calculates aggregate values in the given column. Methods for #count, #sum, #average,
+ # #minimum, and #maximum have been added as shortcuts.
#
- # There are two basic forms of output:
+ # Person.calculate(:count, :all) # The same as Person.count
+ # Person.average(:age) # SELECT AVG(age) FROM people...
#
- # * Single aggregate value: The single value is type cast to Fixnum for COUNT, Float
- # for AVG, and the given column's type for everything else.
+ # # Selects the minimum age for any family without any minors
+ # Person.group(:last_name).having("min(age) > 17").minimum(:age)
#
- # * Grouped values: This returns an ordered hash of the values and groups them. It
- # takes either a column name, or the name of a belongs_to association.
+ # Person.sum("2 * age")
#
- # values = Person.group('last_name').maximum(:age)
- # puts values["Drake"]
- # # => 43
+ # There are two basic forms of output:
#
- # drake = Family.find_by(last_name: 'Drake')
- # values = Person.group(:family).maximum(:age) # Person belongs_to :family
- # puts values[drake]
- # # => 43
+ # * Single aggregate value: The single value is type cast to Integer for COUNT, Float
+ # for AVG, and the given column's type for everything else.
#
- # values.each do |family, max_age|
- # ...
- # end
+ # * Grouped values: This returns an ordered hash of the values and groups them. It
+ # takes either a column name, or the name of a belongs_to association.
#
- # Person.calculate(:count, :all) # The same as Person.count
- # Person.average(:age) # SELECT AVG(age) FROM people...
+ # values = Person.group('last_name').maximum(:age)
+ # puts values["Drake"]
+ # # => 43
#
- # # Selects the minimum age for any family without any minors
- # Person.group(:last_name).having("min(age) > 17").minimum(:age)
+ # drake = Family.find_by(last_name: 'Drake')
+ # values = Person.group(:family).maximum(:age) # Person belongs_to :family
+ # puts values[drake]
+ # # => 43
#
- # Person.sum("2 * age")
+ # values.each do |family, max_age|
+ # ...
+ # end
def calculate(operation, column_name)
- if column_name.is_a?(Symbol) && attribute_alias?(column_name)
- column_name = attribute_alias(column_name)
- end
-
if has_include?(column_name)
- construct_relation_for_association_calculations.calculate(operation, column_name)
+ relation = apply_join_dependency
+
+ if operation.to_s.downcase == "count"
+ relation.distinct!
+ # PostgreSQL: ORDER BY expressions must appear in SELECT list when using DISTINCT
+ if (column_name == :all || column_name.nil?) && select_values.empty?
+ relation.order_values = []
+ end
+ end
+
+ relation.calculate(operation, column_name)
else
perform_calculation(operation, column_name)
end
end
- # Use <tt>pluck</tt> as a shortcut to select one or more attributes without
+ # Use #pluck as a shortcut to select one or more attributes without
# loading a bunch of records just to grab the attributes you want.
#
# Person.pluck(:name)
@@ -127,7 +155,7 @@ module ActiveRecord
#
# Person.all.map(&:name)
#
- # Pluck returns an <tt>Array</tt> of attribute values type-casted to match
+ # Pluck returns an Array of attribute values type-casted to match
# the plucked column names, if they can be deduced. Plucking an SQL fragment
# returns String values by default.
#
@@ -151,33 +179,45 @@ module ActiveRecord
# # SELECT DATEDIFF(updated_at, created_at) FROM people
# # => ['0', '27761', '173']
#
- # See also +ids+.
+ # See also #ids.
#
def pluck(*column_names)
- column_names.map! do |column_name|
- if column_name.is_a?(Symbol) && attribute_alias?(column_name)
- attribute_alias(column_name)
- else
- column_name.to_s
- end
- end
-
- if loaded? && (column_names - @klass.column_names).empty?
- return @records.pluck(*column_names)
+ if loaded? && (column_names.map(&:to_s) - @klass.attribute_names - @klass.attribute_aliases.keys).empty?
+ return records.pluck(*column_names)
end
if has_include?(column_names.first)
- construct_relation_for_association_calculations.pluck(*column_names)
+ relation = apply_join_dependency
+ relation.pluck(*column_names)
else
+ disallow_raw_sql!(column_names)
relation = spawn
relation.select_values = column_names.map { |cn|
- columns_hash.key?(cn) ? arel_table[cn] : cn
+ @klass.has_attribute?(cn) || @klass.attribute_alias?(cn) ? arel_attribute(cn) : cn
}
- result = klass.connection.select_all(relation.arel, nil, bound_attributes)
+ result = skip_query_cache_if_necessary { klass.connection.select_all(relation.arel, nil) }
result.cast_values(klass.attribute_types)
end
end
+ # Pick the value(s) from the named column(s) in the current relation.
+ # This is short-hand for <tt>relation.limit(1).pluck(*column_names).first</tt>, and is primarily useful
+ # when you have a relation that's already narrowed down to a single row.
+ #
+ # Just like #pluck, #pick will only load the actual value, not the entire record object, so it's also
+ # more efficient. The value is, again like with pluck, typecast by the column type.
+ #
+ # Person.where(id: 1).pick(:name)
+ # # SELECT people.name FROM people WHERE id = 1 LIMIT 1
+ # # => 'David'
+ #
+ # Person.where(id: 1).pick(:name, :email_address)
+ # # SELECT people.name, people.email_address FROM people WHERE id = 1 LIMIT 1
+ # # => [ 'David', 'david@loudthinking.com' ]
+ def pick(*column_names)
+ limit(1).pluck(*column_names).first
+ end
+
# Pluck all the ID's for the relation using the table's primary key
#
# Person.ids # SELECT people.id FROM people
@@ -188,205 +228,205 @@ module ActiveRecord
private
- def has_include?(column_name)
- eager_loading? || (includes_values.present? && column_name && column_name != :all)
- end
+ def has_include?(column_name)
+ eager_loading? || (includes_values.present? && column_name && column_name != :all)
+ end
- def perform_calculation(operation, column_name)
- operation = operation.to_s.downcase
+ def perform_calculation(operation, column_name)
+ operation = operation.to_s.downcase
+
+ # If #count is used with #distinct (i.e. `relation.distinct.count`) it is
+ # considered distinct.
+ distinct = distinct_value
+
+ if operation == "count"
+ column_name ||= select_for_count
+ if column_name == :all
+ if distinct && (group_values.any? || select_values.empty? && order_values.empty?)
+ column_name = primary_key
+ end
+ elsif /\s*DISTINCT[\s(]+/i.match?(column_name.to_s)
+ distinct = nil
+ end
+ end
- # If #count is used with #distinct (i.e. `relation.distinct.count`) it is
- # considered distinct.
- distinct = self.distinct_value
+ if group_values.any?
+ execute_grouped_calculation(operation, column_name, distinct)
+ else
+ execute_simple_calculation(operation, column_name, distinct)
+ end
+ end
- if operation == "count"
- column_name ||= select_for_count
+ def aggregate_column(column_name)
+ return column_name if Arel::Expressions === column_name
- unless arel.ast.grep(Arel::Nodes::OuterJoin).empty?
- distinct = true
+ if @klass.has_attribute?(column_name) || @klass.attribute_alias?(column_name)
+ @klass.arel_attribute(column_name)
+ else
+ Arel.sql(column_name == :all ? "*" : column_name.to_s)
end
-
- column_name = primary_key if column_name == :all && distinct
- distinct = nil if column_name =~ /\s*DISTINCT[\s(]+/i
end
- if group_values.any?
- execute_grouped_calculation(operation, column_name, distinct)
- else
- execute_simple_calculation(operation, column_name, distinct)
+ def operation_over_aggregate_column(column, operation, distinct)
+ operation == "count" ? column.count(distinct) : column.send(operation)
end
- end
- def aggregate_column(column_name)
- if @klass.column_names.include?(column_name.to_s)
- Arel::Attribute.new(@klass.unscoped.table, column_name)
- else
- Arel.sql(column_name == :all ? "*" : column_name.to_s)
- end
- end
+ def execute_simple_calculation(operation, column_name, distinct) #:nodoc:
+ column_alias = column_name
- def operation_over_aggregate_column(column, operation, distinct)
- operation == 'count' ? column.count(distinct) : column.send(operation)
- end
+ if operation == "count" && (column_name == :all && distinct || has_limit_or_offset?)
+ # Shortcut when limit is zero.
+ return 0 if limit_value == 0
- def execute_simple_calculation(operation, column_name, distinct) #:nodoc:
- # PostgreSQL doesn't like ORDER BY when there are no GROUP BY
- relation = unscope(:order)
+ 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).distinct!(false)
- column_alias = column_name
+ column = aggregate_column(column_name)
- if operation == "count" && (relation.limit_value || relation.offset_value)
- # Shortcut when limit is zero.
- return 0 if relation.limit_value == 0
+ select_value = operation_over_aggregate_column(column, operation, distinct)
+ if operation == "sum" && distinct
+ select_value.distinct = true
+ end
- query_builder = build_count_subquery(relation, column_name, distinct)
- else
- column = aggregate_column(column_name)
+ column_alias = select_value.alias
+ column_alias ||= @klass.connection.column_name_for_operation(operation, select_value)
+ relation.select_values = [select_value]
- select_value = operation_over_aggregate_column(column, operation, distinct)
+ query_builder = relation.arel
+ end
- column_alias = select_value.alias
- column_alias ||= @klass.connection.column_name_for_operation(operation, select_value)
- relation.select_values = [select_value]
+ result = skip_query_cache_if_necessary { @klass.connection.select_all(query_builder, nil) }
+ row = result.first
+ value = row && row.values.first
+ type = result.column_types.fetch(column_alias) do
+ type_for(column_name)
+ end
- query_builder = relation.arel
+ type_cast_calculated_value(value, type, operation)
end
- result = @klass.connection.select_all(query_builder, nil, bound_attributes)
- row = result.first
- value = row && row.values.first
- column = result.column_types.fetch(column_alias) do
- type_for(column_name)
- end
+ def execute_grouped_calculation(operation, column_name, distinct) #:nodoc:
+ group_attrs = group_values
- type_cast_calculated_value(value, column, operation)
- end
+ if group_attrs.first.respond_to?(:to_sym)
+ association = @klass._reflect_on_association(group_attrs.first)
+ associated = group_attrs.size == 1 && association && association.belongs_to? # only count belongs_to associations
+ group_fields = Array(associated ? association.foreign_key : group_attrs)
+ else
+ group_fields = group_attrs
+ end
+ group_fields = arel_columns(group_fields)
- def execute_grouped_calculation(operation, column_name, distinct) #:nodoc:
- group_attrs = group_values
+ group_aliases = group_fields.map { |field| column_alias_for(field) }
+ group_columns = group_aliases.zip(group_fields)
- if group_attrs.first.respond_to?(:to_sym)
- association = @klass._reflect_on_association(group_attrs.first)
- associated = group_attrs.size == 1 && association && association.belongs_to? # only count belongs_to associations
- group_fields = Array(associated ? association.foreign_key : group_attrs)
- else
- group_fields = group_attrs
- end
+ if operation == "count" && column_name == :all
+ aggregate_alias = "count_all"
+ else
+ aggregate_alias = column_alias_for([operation, column_name].join(" "))
+ end
- group_aliases = group_fields.map { |field|
- column_alias_for(field)
- }
- group_columns = group_aliases.zip(group_fields).map { |aliaz,field|
- [aliaz, field]
- }
+ select_values = [
+ operation_over_aggregate_column(
+ aggregate_column(column_name),
+ operation,
+ distinct).as(aggregate_alias)
+ ]
+ select_values += self.select_values unless having_clause.empty?
+
+ select_values.concat group_columns.map { |aliaz, field|
+ if field.respond_to?(:as)
+ field.as(aliaz)
+ else
+ "#{field} AS #{aliaz}"
+ end
+ }
- group = group_fields
+ relation = except(:group).distinct!(false)
+ relation.group_values = group_fields
+ relation.select_values = select_values
- if operation == 'count' && column_name == :all
- aggregate_alias = 'count_all'
- else
- aggregate_alias = column_alias_for([operation, column_name].join(' '))
- end
+ calculated_data = skip_query_cache_if_necessary { @klass.connection.select_all(relation.arel, nil) }
- select_values = [
- operation_over_aggregate_column(
- aggregate_column(column_name),
- operation,
- distinct).as(aggregate_alias)
- ]
- select_values += select_values unless having_clause.empty?
-
- select_values.concat group_fields.zip(group_aliases).map { |field,aliaz|
- if field.respond_to?(:as)
- field.as(aliaz)
- else
- "#{field} AS #{aliaz}"
+ if association
+ key_ids = calculated_data.collect { |row| row[group_aliases.first] }
+ key_records = association.klass.base_class.where(association.klass.base_class.primary_key => key_ids)
+ key_records = Hash[key_records.map { |r| [r.id, r] }]
end
- }
-
- relation = except(:group)
- relation.group_values = group
- relation.select_values = select_values
-
- calculated_data = @klass.connection.select_all(relation, nil, relation.bound_attributes)
- if association
- key_ids = calculated_data.collect { |row| row[group_aliases.first] }
- key_records = association.klass.base_class.where(association.klass.base_class.primary_key => key_ids)
- key_records = Hash[key_records.map { |r| [r.id, r] }]
+ Hash[calculated_data.map do |row|
+ key = group_columns.map { |aliaz, col_name|
+ type = type_for(col_name) do
+ calculated_data.column_types.fetch(aliaz, Type.default_value)
+ end
+ type_cast_calculated_value(row[aliaz], type)
+ }
+ key = key.first if key.size == 1
+ key = key_records[key] if associated
+
+ type = calculated_data.column_types.fetch(aggregate_alias) { type_for(column_name) }
+ [key, type_cast_calculated_value(row[aggregate_alias], type, operation)]
+ end]
end
- Hash[calculated_data.map do |row|
- key = group_columns.map { |aliaz, col_name|
- column = calculated_data.column_types.fetch(aliaz) do
- type_for(col_name)
- end
- type_cast_calculated_value(row[aliaz], column)
- }
- key = key.first if key.size == 1
- key = key_records[key] if associated
+ # Converts the given keys to the value that the database adapter returns as
+ # a usable column name:
+ #
+ # column_alias_for("users.id") # => "users_id"
+ # column_alias_for("sum(id)") # => "sum_id"
+ # column_alias_for("count(distinct users.id)") # => "count_distinct_users_id"
+ # column_alias_for("count(*)") # => "count_all"
+ def column_alias_for(keys)
+ if keys.respond_to? :name
+ keys = "#{keys.relation.name}.#{keys.name}"
+ end
- column_type = calculated_data.column_types.fetch(aggregate_alias) { type_for(column_name) }
- [key, type_cast_calculated_value(row[aggregate_alias], column_type, operation)]
- end]
- end
+ table_name = keys.to_s.downcase
+ table_name.gsub!(/\*/, "all")
+ table_name.gsub!(/\W+/, " ")
+ table_name.strip!
+ table_name.gsub!(/ +/, "_")
- # Converts the given keys to the value that the database adapter returns as
- # a usable column name:
- #
- # column_alias_for("users.id") # => "users_id"
- # column_alias_for("sum(id)") # => "sum_id"
- # column_alias_for("count(distinct users.id)") # => "count_distinct_users_id"
- # column_alias_for("count(*)") # => "count_all"
- # column_alias_for("count", "id") # => "count_id"
- def column_alias_for(keys)
- if keys.respond_to? :name
- keys = "#{keys.relation.name}.#{keys.name}"
+ @klass.connection.table_alias_for(table_name)
end
- table_name = keys.to_s.downcase
- table_name.gsub!(/\*/, 'all')
- table_name.gsub!(/\W+/, ' ')
- table_name.strip!
- table_name.gsub!(/ +/, '_')
-
- @klass.connection.table_alias_for(table_name)
- end
-
- def type_for(field)
- field_name = field.respond_to?(:name) ? field.name.to_s : field.to_s.split('.').last
- @klass.type_for_attribute(field_name)
- end
+ def type_for(field, &block)
+ field_name = field.respond_to?(:name) ? field.name.to_s : field.to_s.split(".").last
+ @klass.type_for_attribute(field_name, &block)
+ end
- def type_cast_calculated_value(value, type, operation = nil)
- case operation
- when 'count' then value.to_i
- when 'sum' then type.deserialize(value || 0)
- when 'average' then value.respond_to?(:to_d) ? value.to_d : value
+ def type_cast_calculated_value(value, type, operation = nil)
+ case operation
+ when "count" then value.to_i
+ when "sum" then type.deserialize(value || 0)
+ when "average" then value.respond_to?(:to_d) ? value.to_d : value
else type.deserialize(value)
+ end
end
- end
- # TODO: refactor to allow non-string `select_values` (eg. Arel nodes).
- def select_for_count
- if select_values.present?
- select_values.join(", ")
- else
- :all
+ def select_for_count
+ if select_values.present?
+ return select_values.first if select_values.one?
+ select_values.join(", ")
+ else
+ :all
+ end
end
- end
- def build_count_subquery(relation, column_name, distinct)
- column_alias = Arel.sql('count_column')
- subquery_alias = Arel.sql('subquery_for_count')
+ def build_count_subquery(relation, column_name, distinct)
+ if column_name == :all
+ relation.select_values = [ Arel.sql(FinderMethods::ONE_AS_ONE) ] unless distinct
+ else
+ column_alias = Arel.sql("count_column")
+ relation.select_values = [ aggregate_column(column_name).as(column_alias) ]
+ end
- aliased_column = aggregate_column(column_name == :all ? 1 : column_name).as(column_alias)
- relation.select_values = [aliased_column]
- subquery = relation.arel.as(subquery_alias)
+ subquery = relation.arel.as(Arel.sql("subquery_for_count"))
+ select_value = operation_over_aggregate_column(column_alias || Arel.star, "count", false)
- sm = Arel::SelectManager.new relation.engine
- select_value = operation_over_aggregate_column(column_alias, 'count', distinct)
- sm.project(select_value).from(subquery)
- end
+ Arel::SelectManager.new(subquery).project(select_value)
+ end
end
end
diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb
index 86f2c30168..383dc1bf4b 100644
--- a/activerecord/lib/active_record/relation/delegation.rb
+++ b/activerecord/lib/active_record/relation/delegation.rb
@@ -1,14 +1,13 @@
-require 'set'
-require 'active_support/concern'
+# frozen_string_literal: true
module ActiveRecord
module Delegation # :nodoc:
- module DelegateCache
- def relation_delegate_class(klass) # :nodoc:
+ module DelegateCache # :nodoc:
+ def relation_delegate_class(klass)
@relation_delegate_cache[klass]
end
- def initialize_relation_delegate_cache # :nodoc:
+ def initialize_relation_delegate_cache
@relation_delegate_cache = cache = {}
[
ActiveRecord::Relation,
@@ -18,7 +17,11 @@ module ActiveRecord
delegate = Class.new(klass) {
include ClassSpecificRelation
}
- const_set klass.name.gsub('::', '_'), delegate
+ include_relation_methods(delegate)
+ mangled_name = klass.name.gsub("::", "_")
+ const_set mangled_name, delegate
+ private_constant mangled_name
+
cache[klass] = delegate
end
end
@@ -27,6 +30,35 @@ module ActiveRecord
child_class.initialize_relation_delegate_cache
super
end
+
+ protected
+ def include_relation_methods(delegate)
+ superclass.include_relation_methods(delegate) unless base_class?
+ delegate.include generated_relation_methods
+ end
+
+ private
+ def generated_relation_methods
+ @generated_relation_methods ||= Module.new.tap do |mod|
+ mod_name = "GeneratedRelationMethods"
+ const_set mod_name, mod
+ private_constant mod_name
+ end
+ end
+
+ def generate_relation_method(method)
+ if /\A[a-zA-Z_]\w*[!?]?\z/.match?(method)
+ generated_relation_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def #{method}(*args, &block)
+ scoping { klass.#{method}(*args, &block) }
+ end
+ RUBY
+ else
+ generated_relation_methods.send(:define_method, method) do |*args, &block|
+ scoping { klass.public_send(method, *args, &block) }
+ end
+ end
+ end
end
extend ActiveSupport::Concern
@@ -36,16 +68,12 @@ 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.
- BLACKLISTED_ARRAY_METHODS = [
- :compact!, :flatten!, :reject!, :reverse!, :rotate!, :map!,
- :shuffle!, :slice!, :sort!, :sort_by!, :delete_if,
- :keep_if, :pop, :shift, :delete_at, :select!
- ].to_set # :nodoc:
+ delegate :to_xml, :encode_with, :length, :each, :join,
+ :[], :&, :|, :+, :-, :sample, :reverse, :rotate, :compact, :in_groups, :in_groups_of,
+ :to_sentence, :to_formatted_s, :as_json,
+ :shuffle, :split, :slice, :index, :rindex, to: :records
- delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :join, to: :to_a
-
- delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key,
- :connection, :columns_hash, :to => :klass
+ delegate :primary_key, :connection, to: :klass
module ClassSpecificRelation # :nodoc:
extend ActiveSupport::Concern
@@ -63,7 +91,7 @@ module ActiveRecord
@delegation_mutex.synchronize do
return if method_defined?(method)
- if method.to_s =~ /\A[a-zA-Z_]\w*[!?]?\z/
+ if /\A[a-zA-Z_]\w*[!?]?\z/.match?(method)
module_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{method}(*args, &block)
scoping { @klass.#{method}(*args, &block) }
@@ -76,28 +104,27 @@ module ActiveRecord
end
end
end
-
- def delegate(method, opts = {})
- @delegation_mutex.synchronize do
- return if method_defined?(method)
- super
- end
- end
end
- protected
+ private
- def method_missing(method, *args, &block)
- if @klass.respond_to?(method)
- self.class.delegate_to_scoped_klass(method)
- scoping { @klass.public_send(method, *args, &block) }
- elsif arel.respond_to?(method)
- self.class.delegate method, :to => :arel
- arel.public_send(method, *args, &block)
- else
- super
+ def method_missing(method, *args, &block)
+ if @klass.respond_to?(method)
+ self.class.delegate_to_scoped_klass(method)
+ scoping { @klass.public_send(method, *args, &block) }
+ elsif @delegate_to_klass && @klass.respond_to?(method, true)
+ ActiveSupport::Deprecation.warn \
+ "Delegating missing #{method} method to #{@klass}. " \
+ "Accessibility of private/protected class methods in :scope is deprecated and will be removed in Rails 6.0."
+ @klass.send(method, *args, &block)
+ elsif arel.respond_to?(method)
+ ActiveSupport::Deprecation.warn \
+ "Delegating #{method} to arel is deprecated and will be removed in Rails 6.0."
+ arel.public_send(method, *args, &block)
+ else
+ super
+ end
end
- end
end
module ClassMethods # :nodoc:
@@ -107,33 +134,14 @@ module ActiveRecord
private
- def relation_class_for(klass)
- klass.relation_delegate_class(self)
- end
- end
-
- def respond_to?(method, include_private = false)
- super || @klass.respond_to?(method, include_private) ||
- array_delegable?(method) ||
- arel.respond_to?(method, include_private)
- end
-
- protected
-
- def array_delegable?(method)
- Array.method_defined?(method) && BLACKLISTED_ARRAY_METHODS.exclude?(method)
+ def relation_class_for(klass)
+ klass.relation_delegate_class(self)
+ end
end
- def method_missing(method, *args, &block)
- if @klass.respond_to?(method)
- scoping { @klass.public_send(method, *args, &block) }
- elsif array_delegable?(method)
- to_a.public_send(method, *args, &block)
- elsif arel.respond_to?(method)
- arel.public_send(method, *args, &block)
- else
- super
+ private
+ def respond_to_missing?(method, _)
+ super || @klass.respond_to?(method) || arel.respond_to?(method)
end
- end
end
end
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index 9fef55adea..afaa900442 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -1,8 +1,10 @@
-require 'active_support/core_ext/string/filters'
+# frozen_string_literal: true
+
+require "active_support/core_ext/string/filters"
module ActiveRecord
module FinderMethods
- ONE_AS_ONE = '1 AS one'
+ ONE_AS_ONE = "1 AS one"
# Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]).
# If one or more records can not be found for the requested ids, then RecordNotFound will be raised. If the primary key
@@ -16,9 +18,10 @@ module ActiveRecord
# Person.find([1]) # returns an array for the object with ID = 1
# Person.where("administrator = 1").order("created_on DESC").find(1)
#
- # NOTE: The returned records may not be in the same order as the ids you
- # provide since database rows are unordered. You'd need to provide an explicit <tt>order</tt>
- # option if you want the results are sorted.
+ # NOTE: The returned records are in the same order as the ids you provide.
+ # If you want the results to be sorted by database, you can use ActiveRecord::QueryMethods#where
+ # method and provide an explicit ActiveRecord::QueryMethods#order option.
+ # But ActiveRecord::QueryMethods#where method doesn't raise ActiveRecord::RecordNotFound.
#
# ==== Find with lock
#
@@ -34,7 +37,7 @@ module ActiveRecord
# person.save!
# end
#
- # ==== Variations of +find+
+ # ==== Variations of #find
#
# Person.where(name: 'Spartacus', rating: 4)
# # returns a chainable list (which can be empty).
@@ -42,13 +45,13 @@ module ActiveRecord
# Person.find_by(name: 'Spartacus', rating: 4)
# # returns the first item or nil.
#
- # Person.where(name: 'Spartacus', rating: 4).first_or_initialize
+ # Person.find_or_initialize_by(name: 'Spartacus', rating: 4)
# # returns the first item or returns a new instance (requires you call .save to persist against the database).
#
- # Person.where(name: 'Spartacus', rating: 4).first_or_create
+ # Person.find_or_create_by(name: 'Spartacus', rating: 4)
# # returns the first item or creates it and returns it.
#
- # ==== Alternatives for +find+
+ # ==== Alternatives for #find
#
# Person.where(name: 'Spartacus', rating: 4).exists?(conditions = :none)
# # returns a boolean indicating if any record with the given conditions exist.
@@ -76,16 +79,17 @@ 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
- # Like <tt>find_by</tt>, except that if no record is found, raises
- # an <tt>ActiveRecord::RecordNotFound</tt> error.
+ # Like #find_by, except that if no record is found, raises
+ # an ActiveRecord::RecordNotFound error.
def find_by!(arg, *args)
where(arg, *args).take!
- rescue RangeError
- raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range value"
+ rescue ::RangeError
+ raise RecordNotFound.new("Couldn't find #{@klass.name} with an out of range value",
+ @klass.name, @klass.primary_key)
end
# Gives a record (or N records if a parameter is supplied) without any implied
@@ -96,13 +100,13 @@ module ActiveRecord
# Person.take(5) # returns 5 objects fetched by SELECT * FROM people LIMIT 5
# Person.where(["name LIKE '%?'", name]).take
def take(limit = nil)
- limit ? limit(limit).to_a : find_take
+ limit ? find_take_with_limit(limit) : find_take
end
- # Same as +take+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
- # is found. Note that <tt>take!</tt> accepts no arguments.
+ # Same as #take but raises ActiveRecord::RecordNotFound if no record
+ # is found. Note that #take! accepts no arguments.
def take!
- take or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]")
+ take || raise_record_not_found_exception!
end
# Find the first record (or first N records if a parameter is supplied).
@@ -116,16 +120,16 @@ module ActiveRecord
#
def first(limit = nil)
if limit
- find_nth_with_limit(offset_index, limit)
+ find_nth_with_limit(0, limit)
else
- find_nth(0, offset_index)
+ find_nth 0
end
end
- # Same as +first+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
- # is found. Note that <tt>first!</tt> accepts no arguments.
+ # Same as #first but raises ActiveRecord::RecordNotFound if no record
+ # is found. Note that #first! accepts no arguments.
def first!
- find_nth! 0
+ first || raise_record_not_found_exception!
end
# Find the last record (or last N records if a parameter is supplied).
@@ -144,21 +148,18 @@ module ActiveRecord
#
# [#<Person id:4>, #<Person id:3>, #<Person id:2>]
def last(limit = nil)
- if limit
- if order_values.empty? && primary_key
- order(arel_table[primary_key].desc).limit(limit).reverse
- else
- to_a.last(limit)
- end
- else
- find_last
- end
+ return find_last(limit) if loaded? || has_limit_or_offset?
+
+ result = ordered_relation.limit(limit)
+ result = result.reverse_order!
+
+ limit ? result.reverse : result.first
end
- # Same as +last+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
- # is found. Note that <tt>last!</tt> accepts no arguments.
+ # Same as #last but raises ActiveRecord::RecordNotFound if no record
+ # is found. Note that #last! accepts no arguments.
def last!
- last or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]")
+ last || raise_record_not_found_exception!
end
# Find the second record.
@@ -168,13 +169,13 @@ module ActiveRecord
# Person.offset(3).second # returns the second object from OFFSET 3 (which is OFFSET 4)
# Person.where(["user_name = :u", { u: user_name }]).second
def second
- find_nth(1, offset_index)
+ find_nth 1
end
- # Same as +second+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
+ # Same as #second but raises ActiveRecord::RecordNotFound if no record
# is found.
def second!
- find_nth! 1
+ second || raise_record_not_found_exception!
end
# Find the third record.
@@ -184,13 +185,13 @@ module ActiveRecord
# Person.offset(3).third # returns the third object from OFFSET 3 (which is OFFSET 5)
# Person.where(["user_name = :u", { u: user_name }]).third
def third
- find_nth(2, offset_index)
+ find_nth 2
end
- # Same as +third+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
+ # Same as #third but raises ActiveRecord::RecordNotFound if no record
# is found.
def third!
- find_nth! 2
+ third || raise_record_not_found_exception!
end
# Find the fourth record.
@@ -200,13 +201,13 @@ module ActiveRecord
# Person.offset(3).fourth # returns the fourth object from OFFSET 3 (which is OFFSET 6)
# Person.where(["user_name = :u", { u: user_name }]).fourth
def fourth
- find_nth(3, offset_index)
+ find_nth 3
end
- # Same as +fourth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
+ # Same as #fourth but raises ActiveRecord::RecordNotFound if no record
# is found.
def fourth!
- find_nth! 3
+ fourth || raise_record_not_found_exception!
end
# Find the fifth record.
@@ -216,13 +217,13 @@ module ActiveRecord
# Person.offset(3).fifth # returns the fifth object from OFFSET 3 (which is OFFSET 7)
# Person.where(["user_name = :u", { u: user_name }]).fifth
def fifth
- find_nth(4, offset_index)
+ find_nth 4
end
- # Same as +fifth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
+ # Same as #fifth but raises ActiveRecord::RecordNotFound if no record
# is found.
def fifth!
- find_nth! 4
+ fifth || raise_record_not_found_exception!
end
# Find the forty-second record. Also known as accessing "the reddit".
@@ -232,17 +233,49 @@ module ActiveRecord
# Person.offset(3).forty_two # returns the forty-second object from OFFSET 3 (which is OFFSET 44)
# Person.where(["user_name = :u", { u: user_name }]).forty_two
def forty_two
- find_nth(41, offset_index)
+ find_nth 41
end
- # Same as +forty_two+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
+ # Same as #forty_two but raises ActiveRecord::RecordNotFound if no record
# is found.
def forty_two!
- find_nth! 41
+ forty_two || raise_record_not_found_exception!
+ end
+
+ # Find the third-to-last record.
+ # If no order is defined it will order by primary key.
+ #
+ # Person.third_to_last # returns the third-to-last object fetched by SELECT * FROM people
+ # Person.offset(3).third_to_last # returns the third-to-last object from OFFSET 3
+ # Person.where(["user_name = :u", { u: user_name }]).third_to_last
+ def third_to_last
+ find_nth_from_last 3
end
- # Returns +true+ if a record exists in the table that matches the +id+ or
- # conditions given, or +false+ otherwise. The argument can take six forms:
+ # Same as #third_to_last but raises ActiveRecord::RecordNotFound if no record
+ # is found.
+ def third_to_last!
+ third_to_last || raise_record_not_found_exception!
+ end
+
+ # Find the second-to-last record.
+ # If no order is defined it will order by primary key.
+ #
+ # Person.second_to_last # returns the second-to-last object fetched by SELECT * FROM people
+ # Person.offset(3).second_to_last # returns the second-to-last object from OFFSET 3
+ # Person.where(["user_name = :u", { u: user_name }]).second_to_last
+ def second_to_last
+ find_nth_from_last 2
+ end
+
+ # Same as #second_to_last but raises ActiveRecord::RecordNotFound if no record
+ # is found.
+ def second_to_last!
+ second_to_last || raise_record_not_found_exception!
+ end
+
+ # Returns true if a record exists in the table that matches the +id+ or
+ # conditions given, or false otherwise. The argument can take six forms:
#
# * Integer - Finds the record with this primary key.
# * String - Finds the record with a primary key corresponding to this
@@ -252,10 +285,10 @@ module ActiveRecord
# * Hash - Finds the record that matches these +find+-style conditions
# (such as <tt>{name: 'David'}</tt>).
# * +false+ - Returns always +false+.
- # * No args - Returns +false+ if the table is empty, +true+ otherwise.
+ # * No args - Returns +false+ if the relation is empty, +true+ otherwise.
#
# For more information about specifying conditions as a hash or array,
- # see the Conditions section in the introduction to <tt>ActiveRecord::Base</tt>.
+ # see the Conditions section in the introduction to ActiveRecord::Base.
#
# Note: You can't pass in a condition as a string (like <tt>name =
# 'Jamie'</tt>), since it would be sanitized and then queried against
@@ -268,243 +301,260 @@ module ActiveRecord
# Person.exists?(name: 'David')
# Person.exists?(false)
# Person.exists?
+ # Person.where(name: 'Spartacus', rating: 4).exists?
def exists?(conditions = :none)
if Base === conditions
- 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`
+ Please pass the id of the object by calling `.id`.
MSG
end
- return false if !conditions
+ return false if !conditions || limit_value == 0
- relation = apply_join_dependency(self, construct_join_dependency)
- return false if ActiveRecord::NullRelation === relation
-
- relation = relation.except(:select, :order).select(ONE_AS_ONE).limit(1)
-
- case conditions
- when Array, Hash
- relation = relation.where(conditions)
- else
- unless conditions == :none
- relation = relation.where(primary_key => conditions)
- end
+ if eager_loading?
+ relation = apply_join_dependency(eager_loading: false)
+ return relation.exists?(conditions)
end
- connection.select_value(relation, "#{name} Exists", relation.bound_attributes) ? true : false
+ relation = construct_relation_for_exists(conditions)
+
+ skip_query_cache_if_necessary { connection.select_value(relation.arel, "#{name} Exists") } ? true : false
+ rescue ::RangeError
+ false
end
# This method is called whenever no records are found with either a single
- # id or multiple ids and raises a +ActiveRecord::RecordNotFound+ exception.
+ # id or multiple ids and raises an ActiveRecord::RecordNotFound exception.
#
# The error message is different depending on whether a single id or
# multiple ids are provided. If multiple ids are provided, then the number
# 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, result_size, expected_size) #:nodoc:
- conditions = arel.where_sql(@klass.arel_engine)
+ def raise_record_not_found_exception!(ids = nil, result_size = nil, expected_size = nil, key = primary_key, not_found_ids = nil) # :nodoc:
+ conditions = arel.where_sql(@klass)
conditions = " [#{conditions}]" if conditions
-
- if Array(ids).size == 1
- error = "Couldn't find #{@klass.name} with '#{primary_key}'=#{ids}#{conditions}"
+ name = @klass.name
+
+ if ids.nil?
+ error = +"Couldn't find #{name}"
+ error << " with#{conditions}" if conditions
+ raise RecordNotFound.new(error, name, key)
+ elsif Array(ids).size == 1
+ error = "Couldn't find #{name} with '#{key}'=#{ids}#{conditions}"
+ raise RecordNotFound.new(error, name, key, ids)
else
- error = "Couldn't find all #{@klass.name.pluralize} with '#{primary_key}': "
- error << "(#{ids.join(", ")})#{conditions} (found #{result_size} results, but was looking for #{expected_size})"
+ error = +"Couldn't find all #{name.pluralize} with '#{key}': "
+ error << "(#{ids.join(", ")})#{conditions} (found #{result_size} results, but was looking for #{expected_size})."
+ error << " Couldn't find #{name.pluralize(not_found_ids.size)} with #{key.to_s.pluralize(not_found_ids.size)} #{not_found_ids.join(', ')}." if not_found_ids
+ raise RecordNotFound.new(error, name, key, ids)
end
-
- raise RecordNotFound, error
end
private
- def offset_index
- offset_value || 0
- end
+ def offset_index
+ offset_value || 0
+ end
- def find_with_associations
- # NOTE: the JoinDependency constructed here needs to know about
- # any joins already present in `self`, so pass them in
- #
- # failing to do so means that in cases like activerecord/test/cases/associations/inner_join_association_test.rb:136
- # incorrect SQL is generated. In that case, the join dependency for
- # SpecialCategorizations is constructed without knowledge of the
- # preexisting join in joins_values to categorizations (by way of
- # the `has_many :through` for categories).
- #
- join_dependency = construct_join_dependency(joins_values)
-
- aliases = join_dependency.aliases
- relation = select aliases.columns
- relation = apply_join_dependency(relation, join_dependency)
-
- if block_given?
- yield relation
- else
- if ActiveRecord::NullRelation === relation
- []
+ def construct_relation_for_exists(conditions)
+ relation = except(:select, :distinct, :order)._select!(ONE_AS_ONE).limit!(1)
+
+ case conditions
+ when Array, Hash
+ relation.where!(conditions) unless conditions.empty?
else
- arel = relation.arel
- rows = connection.select_all(arel, 'SQL', relation.bound_attributes)
- join_dependency.instantiate(rows, aliases)
+ relation.where!(primary_key => conditions) unless conditions == :none
end
- end
- end
- def construct_join_dependency(joins = [])
- including = eager_load_values + includes_values
- ActiveRecord::Associations::JoinDependency.new(@klass, including, joins)
- end
+ relation
+ end
- def construct_relation_for_association_calculations
- from = arel.froms.first
- if Arel::Table === from
- apply_join_dependency(self, construct_join_dependency(joins_values))
- else
- # FIXME: as far as I can tell, `from` will always be an Arel::Table.
- # There are no tests that test this branch, but presumably it's
- # possible for `from` to be a list?
- apply_join_dependency(self, construct_join_dependency(from))
+ def construct_join_dependency(associations)
+ ActiveRecord::Associations::JoinDependency.new(
+ klass, table, associations
+ )
end
- end
- def apply_join_dependency(relation, join_dependency)
- relation = relation.except(:includes, :eager_load, :preload)
- relation = relation.joins join_dependency
+ def apply_join_dependency(eager_loading: group_values.empty?)
+ join_dependency = construct_join_dependency(eager_load_values + includes_values)
+ relation = except(:includes, :eager_load, :preload).joins!(join_dependency)
- if using_limitable_reflections?(join_dependency.reflections)
- relation
- else
- if relation.limit_value
- limited_ids = limited_ids_for(relation)
- limited_ids.empty? ? relation.none! : relation.where!(primary_key => limited_ids)
+ if eager_loading && !using_limitable_reflections?(join_dependency.reflections)
+ if has_limit_or_offset?
+ limited_ids = limited_ids_for(relation)
+ limited_ids.empty? ? relation.none! : relation.where!(primary_key => limited_ids)
+ end
+ relation.limit_value = relation.offset_value = nil
+ end
+
+ if block_given?
+ yield relation, join_dependency
+ else
+ relation
end
- relation.except(:limit, :offset)
end
- end
- def limited_ids_for(relation)
- values = @klass.connection.columns_for_distinct(
- "#{quoted_table_name}.#{quoted_primary_key}", relation.order_values)
+ def limited_ids_for(relation)
+ values = @klass.connection.columns_for_distinct(
+ connection.visitor.compile(arel_attribute(primary_key)),
+ relation.order_values
+ )
- relation = relation.except(:select).select(values).distinct!
- arel = relation.arel
+ relation = relation.except(:select).select(values).distinct!
- id_rows = @klass.connection.select_all(arel, 'SQL', relation.bound_attributes)
- id_rows.map {|row| row[primary_key]}
- end
+ id_rows = skip_query_cache_if_necessary { @klass.connection.select_all(relation.arel, "SQL") }
+ id_rows.map { |row| row[primary_key] }
+ end
- def using_limitable_reflections?(reflections)
- reflections.none?(&:collection?)
- end
+ def using_limitable_reflections?(reflections)
+ reflections.none?(&:collection?)
+ end
- protected
+ def find_with_ids(*ids)
+ raise UnknownPrimaryKey.new(@klass) if primary_key.nil?
- def find_with_ids(*ids)
- raise UnknownPrimaryKey.new(@klass) if primary_key.nil?
+ expects_array = ids.first.kind_of?(Array)
+ return [] if expects_array && ids.first.empty?
- expects_array = ids.first.kind_of?(Array)
- return ids.first if expects_array && ids.first.empty?
+ ids = ids.flatten.compact.uniq
- ids = ids.flatten.compact.uniq
+ model_name = @klass.name
- case ids.size
- when 0
- raise RecordNotFound, "Couldn't find #{@klass.name} without an ID"
- when 1
- result = find_one(ids.first)
- expects_array ? [ result ] : result
- else
- find_some(ids)
+ case ids.size
+ when 0
+ error_message = "Couldn't find #{model_name} without an ID"
+ raise RecordNotFound.new(error_message, model_name, primary_key)
+ when 1
+ result = find_one(ids.first)
+ expects_array ? [ result ] : result
+ else
+ find_some(ids)
+ end
+ rescue ::RangeError
+ error_message = "Couldn't find #{model_name} with an out of range ID"
+ raise RecordNotFound.new(error_message, model_name, primary_key, ids)
end
- 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)
- You are passing an instance of ActiveRecord::Base to `find`.
- Please pass the id of the object by calling `.id`
- MSG
+ def find_one(id)
+ if ActiveRecord::Base === id
+ 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
+ end
+
+ relation = where(primary_key => id)
+ record = relation.take
+
+ raise_record_not_found_exception!(id, 0, 1) unless record
+
+ record
end
- relation = where(primary_key => id)
- record = relation.take
+ def find_some(ids)
+ return find_some_ordered(ids) unless order_values.present?
- raise_record_not_found_exception!(id, 0, 1) unless record
+ result = where(primary_key => ids).to_a
- record
- end
+ expected_size =
+ if limit_value && ids.size > limit_value
+ limit_value
+ else
+ ids.size
+ end
- def find_some(ids)
- result = where(primary_key => ids).to_a
+ # 11 ids with limit 3, offset 9 should give 2 results.
+ if offset_value && (ids.size - offset_value < expected_size)
+ expected_size = ids.size - offset_value
+ end
- expected_size =
- if limit_value && ids.size > limit_value
- limit_value
+ if result.size == expected_size
+ result
else
- ids.size
+ raise_record_not_found_exception!(ids, result.size, expected_size)
end
+ end
+
+ def find_some_ordered(ids)
+ ids = ids.slice(offset_value || 0, limit_value || ids.size) || []
+
+ result = except(:limit, :offset).where(primary_key => ids).records
+
+ if result.size == ids.size
+ pk_type = @klass.type_for_attribute(primary_key)
- # 11 ids with limit 3, offset 9 should give 2 results.
- if offset_value && (ids.size - offset_value < expected_size)
- expected_size = ids.size - offset_value
+ records_by_id = result.index_by(&:id)
+ ids.map { |id| records_by_id.fetch(pk_type.cast(id)) }
+ else
+ raise_record_not_found_exception!(ids, result.size, ids.size)
+ end
end
- if result.size == expected_size
- result
- else
- raise_record_not_found_exception!(ids, result.size, expected_size)
+ def find_take
+ if loaded?
+ records.first
+ else
+ @take ||= limit(1).records.first
+ end
end
- end
- def find_take
- if loaded?
- @records.first
- else
- @take ||= limit(1).to_a.first
+ def find_take_with_limit(limit)
+ if loaded?
+ records.take(limit)
+ else
+ limit(limit).to_a
+ end
end
- end
- def find_nth(index, offset)
- if loaded?
- @records[index]
- else
- offset += index
- @offsets[offset] ||= find_nth_with_limit(offset, 1).first
+ def find_nth(index)
+ @offsets[offset_index + index] ||= find_nth_with_limit(index, 1).first
end
- end
- def find_nth!(index)
- find_nth(index, offset_index) or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]")
- end
+ def find_nth_with_limit(index, limit)
+ if loaded?
+ records[index, limit] || []
+ else
+ relation = ordered_relation
- def find_nth_with_limit(offset, limit)
- relation = if order_values.empty? && primary_key
- order(arel_table[primary_key].asc)
- else
- self
- end
+ if limit_value
+ limit = [limit_value - index, limit].min
+ end
- relation = relation.offset(offset) unless offset.zero?
- relation.limit(limit).to_a
- end
+ if limit > 0
+ relation = relation.offset(offset_index + index) unless index.zero?
+ relation.limit(limit).to_a
+ else
+ []
+ end
+ end
+ end
- def find_last
- if loaded?
- @records.last
- else
- @last ||=
- if limit_value
- to_a.last
+ def find_nth_from_last(index)
+ if loaded?
+ records[-index]
+ else
+ relation = ordered_relation
+
+ if equal?(relation) || has_limit_or_offset?
+ relation.records[-index]
else
- reverse_order.limit(1).to_a.first
+ relation.last(index)[-index]
end
+ end
+ end
+
+ def find_last(limit)
+ limit ? records.last(limit) : records.last
+ end
+
+ def ordered_relation
+ if order_values.empty? && primary_key
+ order(arel_attribute(primary_key).asc)
+ else
+ self
+ end
end
- end
end
end
diff --git a/activerecord/lib/active_record/relation/from_clause.rb b/activerecord/lib/active_record/relation/from_clause.rb
index a93952fa30..c53a682aee 100644
--- a/activerecord/lib/active_record/relation/from_clause.rb
+++ b/activerecord/lib/active_record/relation/from_clause.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
module ActiveRecord
class Relation
- class FromClause
+ class FromClause # :nodoc:
attr_reader :value, :name
def initialize(value, name)
@@ -8,14 +10,6 @@ module ActiveRecord
@name = name
end
- def binds
- if value.is_a?(Relation)
- value.bound_attributes
- else
- []
- end
- end
-
def merge(other)
self
end
@@ -25,7 +19,7 @@ module ActiveRecord
end
def self.empty
- new(nil, nil)
+ @empty ||= new(nil, nil)
end
end
end
diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb
index 0b38666ce9..4de7465128 100644
--- a/activerecord/lib/active_record/relation/merger.rb
+++ b/activerecord/lib/active_record/relation/merger.rb
@@ -1,4 +1,6 @@
-require 'active_support/core_ext/hash/keys'
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/keys"
module ActiveRecord
class Relation
@@ -21,7 +23,11 @@ module ActiveRecord
# build a relation to merge in rather than directly merging
# the values.
def other
- other = Relation.create(relation.klass, relation.table, relation.predicate_builder)
+ other = Relation.create(
+ relation.klass,
+ table: relation.table,
+ predicate_builder: relation.predicate_builder
+ )
hash.each { |k, v|
if k == :joins
if Hash === v
@@ -50,7 +56,7 @@ module ActiveRecord
NORMAL_VALUES = Relation::VALUE_METHODS -
Relation::CLAUSE_METHODS -
- [:includes, :preload, :joins, :order, :reverse_order, :lock, :create_with, :reordering] # :nodoc:
+ [:includes, :preload, :joins, :left_outer_joins, :order, :reverse_order, :lock, :create_with, :reordering] # :nodoc:
def normal_values
NORMAL_VALUES
@@ -77,84 +83,107 @@ module ActiveRecord
merge_clauses
merge_preloads
merge_joins
+ merge_outer_joins
relation
end
private
- def merge_preloads
- return if other.preload_values.empty? && other.includes_values.empty?
+ def merge_preloads
+ return if other.preload_values.empty? && other.includes_values.empty?
+
+ if other.klass == relation.klass
+ relation.preload!(*other.preload_values) unless other.preload_values.empty?
+ relation.includes!(other.includes_values) unless other.includes_values.empty?
+ else
+ reflection = relation.klass.reflect_on_all_associations.find do |r|
+ r.class_name == other.klass.name
+ end || return
- if other.klass == relation.klass
- relation.preload!(*other.preload_values) unless other.preload_values.empty?
- relation.includes!(other.includes_values) unless other.includes_values.empty?
- else
- reflection = relation.klass.reflect_on_all_associations.find do |r|
- r.class_name == other.klass.name
- end || return
+ unless other.preload_values.empty?
+ relation.preload! reflection.name => other.preload_values
+ end
- unless other.preload_values.empty?
- relation.preload! reflection.name => other.preload_values
+ unless other.includes_values.empty?
+ relation.includes! reflection.name => other.includes_values
+ end
end
+ end
- unless other.includes_values.empty?
- relation.includes! reflection.name => other.includes_values
+ def merge_joins
+ return if other.joins_values.blank?
+
+ if other.klass == relation.klass
+ relation.joins!(*other.joins_values)
+ else
+ joins_dependency = other.joins_values.map do |join|
+ case join
+ when Hash, Symbol, Array
+ other.send(:construct_join_dependency, join)
+ else
+ join
+ end
+ end
+
+ relation.joins!(*joins_dependency)
end
end
- end
- def merge_joins
- return if other.joins_values.blank?
+ def merge_outer_joins
+ return if other.left_outer_joins_values.blank?
- if other.klass == relation.klass
- relation.joins!(*other.joins_values)
- else
- joins_dependency, rest = other.joins_values.partition do |join|
- case join
- when Hash, Symbol, Array
- true
- else
- false
+ if other.klass == relation.klass
+ relation.left_outer_joins!(*other.left_outer_joins_values)
+ else
+ joins_dependency = other.left_outer_joins_values.map do |join|
+ case join
+ when Hash, Symbol, Array
+ other.send(:construct_join_dependency, join)
+ else
+ join
+ end
end
+
+ relation.left_outer_joins!(*joins_dependency)
end
+ end
- join_dependency = ActiveRecord::Associations::JoinDependency.new(other.klass,
- joins_dependency,
- [])
- relation.joins! rest
+ def merge_multi_values
+ if other.reordering_value
+ # override any order specified in the original relation
+ relation.reorder!(*other.order_values)
+ elsif other.order_values.any?
+ # merge in order_values from relation
+ relation.order!(*other.order_values)
+ end
- @relation = relation.joins join_dependency
+ extensions = other.extensions - relation.extensions
+ relation.extending!(*extensions) if extensions.any?
end
- end
- def merge_multi_values
- if other.reordering_value
- # override any order specified in the original relation
- relation.reorder! other.order_values
- elsif other.order_values
- # merge in order_values from relation
- relation.order! other.order_values
+ def merge_single_values
+ relation.lock_value ||= other.lock_value if other.lock_value
+
+ unless other.create_with_value.blank?
+ relation.create_with_value = (relation.create_with_value || {}).merge(other.create_with_value)
+ end
end
- relation.extend(*other.extending_values) unless other.extending_values.blank?
- end
+ def merge_clauses
+ relation.from_clause = other.from_clause if replace_from_clause?
- def merge_single_values
- relation.lock_value ||= other.lock_value
+ where_clause = relation.where_clause.merge(other.where_clause)
+ relation.where_clause = where_clause unless where_clause.empty?
- unless other.create_with_value.blank?
- relation.create_with_value = (relation.create_with_value || {}).merge(other.create_with_value)
+ having_clause = relation.having_clause.merge(other.having_clause)
+ relation.having_clause = having_clause unless having_clause.empty?
end
- end
- def merge_clauses
- CLAUSE_METHODS.each do |name|
- clause = relation.send("#{name}_clause")
- other_clause = other.send("#{name}_clause")
- relation.send("#{name}_clause=", clause.merge(other_clause))
+ def replace_from_clause?
+ relation.from_clause.empty? && !other.from_clause.empty? &&
+ relation.klass.base_class == other.klass.base_class
end
- end
end
end
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb
index 43e9afe853..b59ff912fe 100644
--- a/activerecord/lib/active_record/relation/predicate_builder.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder.rb
@@ -1,13 +1,7 @@
+# frozen_string_literal: true
+
module ActiveRecord
class PredicateBuilder # :nodoc:
- require 'active_record/relation/predicate_builder/array_handler'
- 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/range_handler'
- require 'active_record/relation/predicate_builder/relation_handler'
-
delegate :resolve_column_aliases, to: :table
def initialize(table)
@@ -15,44 +9,25 @@ module ActiveRecord
@handlers = []
register_handler(BasicObject, BasicObjectHandler.new(self))
- register_handler(Class, ClassHandler.new(self))
register_handler(Base, BaseHandler.new(self))
register_handler(Range, RangeHandler.new(self))
register_handler(Relation, RelationHandler.new)
register_handler(Array, ArrayHandler.new(self))
- register_handler(AssociationQueryValue, AssociationQueryHandler.new(self))
+ register_handler(Set, ArrayHandler.new(self))
end
def build_from_hash(attributes)
- attributes = convert_dot_notation_to_hash(attributes.stringify_keys)
+ attributes = convert_dot_notation_to_hash(attributes)
expand_from_hash(attributes)
end
- def create_binds(attributes)
- attributes = convert_dot_notation_to_hash(attributes.stringify_keys)
- create_binds_for_hash(attributes)
- end
-
- def expand(column, value)
- # 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)
- value = AssociationQueryValue.new(table.associated_table(column), value)
- end
-
- build(table.arel_attribute(column), value)
- end
-
def self.references(attributes)
attributes.map do |key, value|
if value.is_a?(Hash)
key
else
key = key.to_s
- key.split('.').first if key.include?('.')
+ key.split(".").first if key.include?(".")
end
end.compact
end
@@ -67,83 +42,104 @@ module ActiveRecord
# Arel::Nodes::And.new([range.start, range.end])
# )
# end
- # ActiveRecord::PredicateBuilder.register_handler(MyCustomDateRange, handler)
+ # ActiveRecord::PredicateBuilder.new("users").register_handler(MyCustomDateRange, handler)
def register_handler(klass, handler)
@handlers.unshift([klass, handler])
end
def build(attribute, value)
- handler_for(value).call(attribute, value)
- end
-
- protected
-
- attr_reader :table
-
- def expand_from_hash(attributes)
- return ["1=0"] if attributes.empty?
-
- attributes.flat_map do |key, value|
- if value.is_a?(Hash)
- associated_predicate_builder(key).expand_from_hash(value)
- else
- expand(key, value)
- end
+ if table.type(attribute.name).force_equality?(value)
+ bind = build_bind_attribute(attribute.name, value)
+ attribute.eq(bind)
+ else
+ handler_for(value).call(attribute, value)
end
end
+ def build_bind_attribute(column_name, value)
+ attr = Relation::QueryAttribute.new(column_name.to_s, value, table.type(column_name))
+ Arel::Nodes::BindParam.new(attr)
+ end
- def create_binds_for_hash(attributes)
- result = attributes.dup
- binds = []
-
- attributes.each do |column_name, value|
- case value
- when Hash
- attrs, bvs = associated_predicate_builder(column_name).create_binds_for_hash(value)
- result[column_name] = attrs
- binds += bvs
- when Relation
- binds += value.bound_attributes
- else
- if can_be_bound?(column_name, value)
- result[column_name] = Arel::Nodes::BindParam.new
- binds << Relation::QueryAttribute.new(column_name.to_s, value, table.type(column_name))
+ protected
+ def expand_from_hash(attributes)
+ return ["1=0"] if attributes.empty?
+
+ attributes.flat_map do |key, value|
+ if value.is_a?(Hash) && !table.has_column?(key)
+ associated_predicate_builder(key).expand_from_hash(value)
+ elsif table.associated_with?(key)
+ # 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)
+ associated_table = table.associated_table(key)
+ if associated_table.polymorphic_association?
+ case value.is_a?(Array) ? value.first : value
+ when Base, Relation
+ value = [value] unless value.is_a?(Array)
+ klass = PolymorphicArrayValue
+ end
+ end
+
+ klass ||= AssociationQueryValue
+ queries = klass.new(associated_table, value).queries.map do |query|
+ expand_from_hash(query).reduce(&:and)
+ end
+ queries.reduce(&:or)
+ elsif table.aggregated_with?(key)
+ mapping = table.reflect_on_aggregation(key).mapping
+ queries = Array.wrap(value).map do |object|
+ mapping.map do |field_attr, aggregate_attr|
+ if mapping.size == 1 && !object.respond_to?(aggregate_attr)
+ build(table.arel_attribute(field_attr), object)
+ else
+ build(table.arel_attribute(field_attr), object.send(aggregate_attr))
+ end
+ end.reduce(&:and)
+ end
+ queries.reduce(&:or)
+ else
+ build(table.arel_attribute(key), value)
end
end
end
- [result, binds]
- end
-
private
+ attr_reader :table
- def associated_predicate_builder(association_name)
- self.class.new(table.associated_table(association_name))
- end
-
- def convert_dot_notation_to_hash(attributes)
- dot_notation = attributes.keys.select { |s| s.include?(".") }
+ def associated_predicate_builder(association_name)
+ self.class.new(table.associated_table(association_name))
+ end
- dot_notation.each do |key|
- table_name, column_name = key.split(".")
- value = attributes.delete(key)
- attributes[table_name] ||= {}
+ def convert_dot_notation_to_hash(attributes)
+ dot_notation = attributes.select do |k, v|
+ k.include?(".") && !v.is_a?(Hash)
+ end
- attributes[table_name] = attributes[table_name].merge(column_name => value)
- end
+ dot_notation.each_key do |key|
+ table_name, column_name = key.split(".")
+ value = attributes.delete(key)
+ attributes[table_name] ||= {}
- attributes
- end
+ attributes[table_name] = attributes[table_name].merge(column_name => value)
+ end
- def handler_for(object)
- @handlers.detect { |klass, _| klass === object }.last
- end
+ attributes
+ end
- def can_be_bound?(column_name, value)
- !value.nil? &&
- handler_for(value).is_a?(BasicObjectHandler) &&
- !table.associated_with?(column_name)
- end
+ def handler_for(object)
+ @handlers.detect { |klass, _| klass === object }.last
+ end
end
end
+
+require "active_record/relation/predicate_builder/array_handler"
+require "active_record/relation/predicate_builder/base_handler"
+require "active_record/relation/predicate_builder/basic_object_handler"
+require "active_record/relation/predicate_builder/range_handler"
+require "active_record/relation/predicate_builder/relation_handler"
+
+require "active_record/relation/predicate_builder/association_query_value"
+require "active_record/relation/predicate_builder/polymorphic_array_value"
diff --git a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
index 95dbd6a77f..ee2ece1560 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
@@ -1,3 +1,7 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/array/extract"
+
module ActiveRecord
class PredicateBuilder
class ArrayHandler # :nodoc:
@@ -6,18 +10,21 @@ module ActiveRecord
end
def call(attribute, value)
- values = value.map { |x| x.is_a?(Base) ? x.id : x }
- nils, values = values.partition(&:nil?)
-
- return attribute.in([]) if values.empty? && nils.empty?
+ return attribute.in([]) if value.empty?
- ranges, values = values.partition { |v| v.is_a?(Range) }
+ values = value.map { |x| x.is_a?(Base) ? x.id : x }
+ nils = values.extract!(&:nil?)
+ ranges = values.extract! { |v| v.is_a?(Range) }
values_predicate =
case values.length
when 0 then NullPredicate
when 1 then predicate_builder.build(attribute, values.first)
- else attribute.in(values)
+ else
+ values.map! do |v|
+ predicate_builder.build_bind_attribute(attribute.name, v)
+ end
+ values.empty? ? NullPredicate : attribute.in(values)
end
unless nils.empty?
@@ -26,18 +33,17 @@ module ActiveRecord
array_predicates = ranges.map { |range| predicate_builder.build(attribute, range) }
array_predicates.unshift(values_predicate)
- array_predicates.inject { |composite, predicate| composite.or(predicate) }
+ array_predicates.inject(&:or)
end
- protected
-
- attr_reader :predicate_builder
+ private
+ attr_reader :predicate_builder
- module NullPredicate # :nodoc:
- def self.or(other)
- other
+ module NullPredicate # :nodoc:
+ def self.or(other)
+ other
+ end
end
- end
end
end
end
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
deleted file mode 100644
index 159889d3b8..0000000000
--- a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb
+++ /dev/null
@@ -1,78 +0,0 @@
-module ActiveRecord
- class PredicateBuilder
- class AssociationQueryHandler # :nodoc:
- def initialize(predicate_builder)
- @predicate_builder = predicate_builder
- end
-
- def call(attribute, value)
- queries = {}
-
- table = value.associated_table
- if value.base_class
- queries[table.association_foreign_type] = value.base_class.name
- end
-
- queries[table.association_foreign_key] = value.ids
- predicate_builder.build_from_hash(queries)
- end
-
- protected
-
- attr_reader :predicate_builder
- end
-
- class AssociationQueryValue # :nodoc:
- attr_reader :associated_table, :value
-
- def initialize(associated_table, value)
- @associated_table = associated_table
- @value = value
- end
-
- def ids
- case value
- when Relation
- value.select(primary_key)
- when Array
- value.map { |v| convert_to_id(v) }
- else
- convert_to_id(value)
- end
- end
-
- def base_class
- if associated_table.polymorphic_association?
- @base_class ||= polymorphic_base_class_from_value
- end
- end
-
- private
-
- def primary_key
- associated_table.association_primary_key(base_class)
- end
-
- def polymorphic_base_class_from_value
- 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
- end
-
- def convert_to_id(value)
- case value
- when Base
- value._read_attribute(primary_key)
- else
- value
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb b/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb
new file mode 100644
index 0000000000..88cd71cf69
--- /dev/null
+++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ class PredicateBuilder
+ class AssociationQueryValue # :nodoc:
+ def initialize(associated_table, value)
+ @associated_table = associated_table
+ @value = value
+ end
+
+ def queries
+ [associated_table.association_join_foreign_key.to_s => ids]
+ end
+
+ private
+ attr_reader :associated_table, :value
+
+ def ids
+ case value
+ when Relation
+ value.select_values.empty? ? value.select(primary_key) : value
+ when Array
+ value.map { |v| convert_to_id(v) }
+ else
+ convert_to_id(value)
+ end
+ end
+
+ def primary_key
+ associated_table.association_join_primary_key
+ end
+
+ def convert_to_id(value)
+ case value
+ when Base
+ value._read_attribute(primary_key)
+ else
+ value
+ end
+ end
+ end
+ end
+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 6fa5b16f73..10c5c1a66a 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
class PredicateBuilder
class BaseHandler # :nodoc:
@@ -9,9 +11,8 @@ module ActiveRecord
predicate_builder.build(attribute, value.id)
end
- protected
-
- attr_reader :predicate_builder
+ private
+ attr_reader :predicate_builder
end
end
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb
index 6cec75dc0a..e8c9f60860 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
class PredicateBuilder
class BasicObjectHandler # :nodoc:
@@ -6,12 +8,12 @@ module ActiveRecord
end
def call(attribute, value)
- attribute.eq(value)
+ bind = predicate_builder.build_bind_attribute(attribute.name, value)
+ attribute.eq(bind)
end
- protected
-
- attr_reader :predicate_builder
+ private
+ attr_reader :predicate_builder
end
end
end
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 ed313fc9d4..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_value.rb b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb
new file mode 100644
index 0000000000..aae04d9348
--- /dev/null
+++ b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ class PredicateBuilder
+ class PolymorphicArrayValue # :nodoc:
+ def initialize(associated_table, values)
+ @associated_table = associated_table
+ @values = values
+ end
+
+ def queries
+ type_to_ids_mapping.map do |type, ids|
+ {
+ associated_table.association_foreign_type.to_s => type,
+ associated_table.association_foreign_key.to_s => ids
+ }
+ end
+ end
+
+ private
+ attr_reader :associated_table, :values
+
+ def type_to_ids_mapping
+ default_hash = Hash.new { |hsh, key| hsh[key] = [] }
+ values.each_with_object(default_hash) do |value, hash|
+ hash[klass(value).polymorphic_name] << convert_to_id(value)
+ end
+ end
+
+ def primary_key(value)
+ associated_table.association_join_primary_key(klass(value))
+ end
+
+ def klass(value)
+ case value
+ when Base
+ value.class
+ when Relation
+ value.klass
+ end
+ end
+
+ def convert_to_id(value)
+ case value
+ when Base
+ value._read_attribute(primary_key(value))
+ when Relation
+ value.select(primary_key(value))
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb
index 1b3849e3ad..44bb2c7ab6 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb
@@ -1,17 +1,41 @@
+# frozen_string_literal: true
+
module ActiveRecord
class PredicateBuilder
class RangeHandler # :nodoc:
+ class RangeWithBinds < Struct.new(:begin, :end)
+ def exclude_end?
+ false
+ end
+ end
+
def initialize(predicate_builder)
@predicate_builder = predicate_builder
end
def call(attribute, value)
- attribute.between(value)
- end
+ begin_bind = predicate_builder.build_bind_attribute(attribute.name, value.begin)
+ end_bind = predicate_builder.build_bind_attribute(attribute.name, value.end)
- protected
+ if begin_bind.value.infinity?
+ if end_bind.value.infinity?
+ attribute.not_in([])
+ elsif value.exclude_end?
+ attribute.lt(end_bind)
+ else
+ attribute.lteq(end_bind)
+ end
+ elsif end_bind.value.infinity?
+ attribute.gteq(begin_bind)
+ elsif value.exclude_end?
+ attribute.gteq(begin_bind).and(attribute.lt(end_bind))
+ else
+ attribute.between(RangeWithBinds.new(begin_bind, end_bind))
+ end
+ end
- attr_reader :predicate_builder
+ private
+ attr_reader :predicate_builder
end
end
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb
index 063150958a..c8bbfa5051 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb
@@ -1,9 +1,15 @@
+# frozen_string_literal: true
+
module ActiveRecord
class PredicateBuilder
class RelationHandler # :nodoc:
def call(attribute, value)
+ if value.eager_loading?
+ value = value.send(:apply_join_dependency)
+ end
+
if value.select_values.empty?
- value = value.select(value.klass.arel_table[value.klass.primary_key])
+ value = value.select(value.arel_attribute(value.klass.primary_key))
end
attribute.in(value.arel)
diff --git a/activerecord/lib/active_record/relation/query_attribute.rb b/activerecord/lib/active_record/relation/query_attribute.rb
index e69319b4de..f64bd30d38 100644
--- a/activerecord/lib/active_record/relation/query_attribute.rb
+++ b/activerecord/lib/active_record/relation/query_attribute.rb
@@ -1,8 +1,10 @@
-require 'active_record/attribute'
+# frozen_string_literal: true
+
+require "active_model/attribute"
module ActiveRecord
class Relation
- class QueryAttribute < Attribute
+ class QueryAttribute < ActiveModel::Attribute # :nodoc:
def type_cast(value)
value
end
@@ -14,6 +16,28 @@ module ActiveRecord
def with_cast_value(value)
QueryAttribute.new(name, value, type)
end
+
+ def nil?
+ !value_before_type_cast.is_a?(StatementCache::Substitute) &&
+ (value_before_type_cast.nil? || value_for_database.nil?)
+ end
+
+ def boundable?
+ return @_boundable if defined?(@_boundable)
+ nil?
+ @_boundable = true
+ rescue ::RangeError
+ @_boundable = false
+ end
+
+ def infinity?
+ _infinity?(value_before_type_cast) || boundable? && _infinity?(value_for_database)
+ end
+
+ private
+ def _infinity?(value)
+ value.respond_to?(:infinite?) && value.infinite?
+ end
end
end
end
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 706c99c245..eb80aab701 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -1,9 +1,10 @@
+# frozen_string_literal: true
+
require "active_record/relation/from_clause"
require "active_record/relation/query_attribute"
require "active_record/relation/where_clause"
require "active_record/relation/where_clause_factory"
-require 'active_model/forbidden_attributes_protection'
-require 'active_support/core_ext/string/filters'
+require "active_model/forbidden_attributes_protection"
module ActiveRecord
module QueryMethods
@@ -14,6 +15,8 @@ module ActiveRecord
# WhereChain objects act as placeholder for queries in which #where does not have any parameter.
# In this case, #where must be chained with #not to return a new relation.
class WhereChain
+ include ActiveModel::ForbiddenAttributesProtection
+
def initialize(scope)
@scope = scope
end
@@ -21,7 +24,7 @@ module ActiveRecord
# Returns a new relation expressing WHERE + NOT condition according to
# the conditions in the arguments.
#
- # +not+ accepts conditions as a string, array, or hash. See #where for
+ # #not accepts conditions as a string, array, or hash. See QueryMethods#where for
# more details on each format.
#
# User.where.not("name = 'Jon'")
@@ -42,6 +45,8 @@ module ActiveRecord
# User.where.not(name: "Jon", role: "admin")
# # SELECT * FROM users WHERE name != 'Jon' AND role != 'admin'
def not(opts, *rest)
+ opts = sanitize_forbidden_attributes(opts)
+
where_clause = @scope.send(:where_clause_factory).build(opts, rest)
@scope.references!(PredicateBuilder.references(opts)) if Hash === opts
@@ -50,57 +55,27 @@ module ActiveRecord
end
end
- Relation::MULTI_VALUE_METHODS.each do |name|
- class_eval <<-CODE, __FILE__, __LINE__ + 1
- def #{name}_values # def select_values
- @values[:#{name}] || [] # @values[:select] || []
- end # end
- #
- def #{name}_values=(values) # def select_values=(values)
- assert_mutability! # assert_mutability!
- @values[:#{name}] = values # @values[:select] = values
- end # end
- CODE
- end
+ FROZEN_EMPTY_ARRAY = [].freeze
+ FROZEN_EMPTY_HASH = {}.freeze
- (Relation::SINGLE_VALUE_METHODS - [:create_with]).each do |name|
+ Relation::VALUE_METHODS.each do |name|
+ method_name = \
+ case name
+ when *Relation::MULTI_VALUE_METHODS then "#{name}_values"
+ when *Relation::SINGLE_VALUE_METHODS then "#{name}_value"
+ when *Relation::CLAUSE_METHODS then "#{name}_clause"
+ end
class_eval <<-CODE, __FILE__, __LINE__ + 1
- def #{name}_value # def readonly_value
- @values[:#{name}] # @values[:readonly]
+ def #{method_name} # def includes_values
+ get_value(#{name.inspect}) # get_value(:includes)
end # end
- CODE
- end
- Relation::SINGLE_VALUE_METHODS.each do |name|
- class_eval <<-CODE, __FILE__, __LINE__ + 1
- def #{name}_value=(value) # def readonly_value=(value)
- assert_mutability! # assert_mutability!
- @values[:#{name}] = value # @values[:readonly] = value
+ def #{method_name}=(value) # def includes_values=(value)
+ set_value(#{name.inspect}, value) # set_value(:includes, value)
end # end
CODE
end
- Relation::CLAUSE_METHODS.each do |name|
- class_eval <<-CODE, __FILE__, __LINE__ + 1
- def #{name}_clause # def where_clause
- @values[:#{name}] || new_#{name}_clause # @values[:where] || new_where_clause
- end # end
- #
- def #{name}_clause=(value) # def where_clause=(value)
- assert_mutability! # assert_mutability!
- @values[:#{name}] = value # @values[:where] = value
- end # end
- CODE
- end
-
- def bound_attributes
- from_clause.binds + arel.bind_values + where_clause.binds + having_clause.binds
- end
-
- def create_with_value # :nodoc:
- @values[:create_with] || {}
- end
-
alias extensions extending_values
# Specify relationships to be included in the result set. For
@@ -113,7 +88,7 @@ module ActiveRecord
#
# allows you to access the +address+ attribute of the +User+ model without
# firing an additional query. This will often result in a
- # performance improvement over a simple +join+.
+ # performance improvement over a simple join.
#
# You can also specify multiple relationships, like this:
#
@@ -134,7 +109,7 @@ module ActiveRecord
#
# User.includes(:posts).where('posts.name = ?', 'example').references(:posts)
#
- # Note that +includes+ works with association names while +references+ needs
+ # Note that #includes works with association names while #references needs
# the actual table name.
def includes(*args)
check_if_method_has_arguments!(:includes, args)
@@ -152,9 +127,9 @@ module ActiveRecord
# Forces eager loading by performing a LEFT OUTER JOIN on +args+:
#
# User.eager_load(:posts)
- # => SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, ...
- # FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" =
- # "users"."id"
+ # # SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, ...
+ # # FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" =
+ # # "users"."id"
def eager_load(*args)
check_if_method_has_arguments!(:eager_load, args)
spawn.eager_load!(*args)
@@ -165,10 +140,10 @@ module ActiveRecord
self
end
- # Allows preloading of +args+, in the same way that +includes+ does:
+ # Allows preloading of +args+, in the same way that #includes does:
#
# User.preload(:posts)
- # => SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (1, 2, 3)
+ # # SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (1, 2, 3)
def preload(*args)
check_if_method_has_arguments!(:preload, args)
spawn.preload!(*args)
@@ -181,14 +156,14 @@ module ActiveRecord
# Use to indicate that the given +table_names+ are referenced by an SQL string,
# and should therefore be JOINed in any query rather than loaded separately.
- # This method only works in conjunction with +includes+.
+ # This method only works in conjunction with #includes.
# See #includes for more details.
#
# User.includes(:posts).where("posts.name = 'foo'")
- # # => Doesn't JOIN the posts table, resulting in an error.
+ # # Doesn't JOIN the posts table, resulting in an error.
#
# User.includes(:posts).where("posts.name = 'foo'").references(:posts)
- # # => Query now knows the string references posts, so adds a JOIN
+ # # Query now knows the string references posts, so adds a JOIN
def references(*table_names)
check_if_method_has_arguments!(:references, table_names)
spawn.references!(*table_names)
@@ -204,12 +179,13 @@ module ActiveRecord
# Works in two unique ways.
#
- # First: takes a block so it can be used just like Array#select.
+ # First: takes a block so it can be used just like <tt>Array#select</tt>.
#
# Model.all.select { |m| m.field == value }
#
# This will build an array of objects from the database for the scope,
- # converting them into an array and iterating through them using Array#select.
+ # converting them into an array and iterating through them using
+ # <tt>Array#select</tt>.
#
# Second: Modifies the SELECT statement for the query so that only certain
# fields are retrieved:
@@ -237,20 +213,28 @@ module ActiveRecord
# # => "value"
#
# Accessing attributes of an object that do not have fields retrieved by a select
- # except +id+ will throw <tt>ActiveModel::MissingAttributeError</tt>:
+ # except +id+ will throw ActiveModel::MissingAttributeError:
#
# Model.select(:field).first.other_field
# # => ActiveModel::MissingAttributeError: missing attribute: other_field
def select(*fields)
- return super if block_given?
- raise ArgumentError, 'Call this with at least one field' if fields.empty?
+ if block_given?
+ if fields.any?
+ raise ArgumentError, "`select' with block doesn't take arguments."
+ end
+
+ return super()
+ end
+
+ raise ArgumentError, "Call `select' with at least one field" if fields.empty?
spawn._select!(*fields)
end
def _select!(*fields) # :nodoc:
+ fields.reject!(&:blank?)
fields.flatten!
fields.map! do |field|
- klass.attribute_alias?(field) ? klass.attribute_alias(field) : field
+ klass.attribute_alias?(field) ? klass.attribute_alias(field).to_sym : field
end
self.select_values += fields
self
@@ -259,22 +243,23 @@ module ActiveRecord
# Allows to specify a group attribute:
#
# User.group(:name)
- # => SELECT "users".* FROM "users" GROUP BY name
+ # # SELECT "users".* FROM "users" GROUP BY name
#
# Returns an array with distinct records based on the +group+ attribute:
#
# User.select([:id, :name])
- # => [#<User id: 1, name: "Oscar">, #<User id: 2, name: "Oscar">, #<User id: 3, name: "Foo">
+ # # => [#<User id: 1, name: "Oscar">, #<User id: 2, name: "Oscar">, #<User id: 3, name: "Foo">]
#
# User.group(:name)
- # => [#<User id: 3, name: "Foo", ...>, #<User id: 2, name: "Oscar", ...>]
+ # # => [#<User id: 3, name: "Foo", ...>, #<User id: 2, name: "Oscar", ...>]
#
# User.group('name AS grouped_name, age')
- # => [#<User id: 3, name: "Foo", age: 21, ...>, #<User id: 2, name: "Oscar", age: 21, ...>, #<User id: 5, name: "Foo", age: 23, ...>]
+ # # => [#<User id: 3, name: "Foo", age: 21, ...>, #<User id: 2, name: "Oscar", age: 21, ...>, #<User id: 5, name: "Foo", age: 23, ...>]
#
# Passing in an array of attributes to group by is also supported.
+ #
# User.select([:id, :first_name]).group(:id, :first_name).first(3)
- # => [#<User id: 1, first_name: "Bill">, #<User id: 2, first_name: "Earl">, #<User id: 3, first_name: "Beto">]
+ # # => [#<User id: 1, first_name: "Bill">, #<User id: 2, first_name: "Earl">, #<User id: 3, first_name: "Beto">]
def group(*args)
check_if_method_has_arguments!(:group, args)
spawn.group!(*args)
@@ -290,27 +275,28 @@ module ActiveRecord
# Allows to specify an order attribute:
#
# User.order(:name)
- # => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC
+ # # SELECT "users".* FROM "users" ORDER BY "users"."name" ASC
#
# User.order(email: :desc)
- # => SELECT "users".* FROM "users" ORDER BY "users"."email" DESC
+ # # SELECT "users".* FROM "users" ORDER BY "users"."email" DESC
#
# User.order(:name, email: :desc)
- # => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC
+ # # SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC
#
# User.order('name')
- # => SELECT "users".* FROM "users" ORDER BY name
+ # # SELECT "users".* FROM "users" ORDER BY name
#
# User.order('name DESC')
- # => SELECT "users".* FROM "users" ORDER BY name DESC
+ # # SELECT "users".* FROM "users" ORDER BY name DESC
#
# User.order('name DESC, email')
- # => SELECT "users".* FROM "users" ORDER BY name DESC, email
+ # # SELECT "users".* FROM "users" ORDER BY name DESC, email
def order(*args)
check_if_method_has_arguments!(:order, args)
spawn.order!(*args)
end
+ # Same as #order but operates on relation in-place instead of copying.
def order!(*args) # :nodoc:
preprocess_order_args(args)
@@ -332,6 +318,7 @@ module ActiveRecord
spawn.reorder!(*args)
end
+ # Same as #reorder but operates on relation in-place instead of copying.
def reorder!(*args) # :nodoc:
preprocess_order_args(args)
@@ -341,8 +328,8 @@ module ActiveRecord
end
VALID_UNSCOPING_VALUES = Set.new([:where, :select, :group, :order, :lock,
- :limit, :offset, :joins, :includes, :from,
- :readonly, :having])
+ :limit, :offset, :joins, :left_outer_joins,
+ :includes, :from, :readonly, :having])
# Removes an unwanted relation that is already defined on a chain of relations.
# This is useful when passing around chains of relations and would like to
@@ -357,15 +344,15 @@ module ActiveRecord
# User.order('email DESC').select('id').where(name: "John")
# .unscope(:order, :select, :where) == User.all
#
- # One can additionally pass a hash as an argument to unscope specific :where values.
+ # One can additionally pass a hash as an argument to unscope specific +:where+ values.
# This is done by passing a hash with a single key-value pair. The key should be
- # :where and the value should be the where value to unscope. For example:
+ # +:where+ and the value should be the where value to unscope. For example:
#
# User.where(name: "John", active: true).unscope(where: :name)
# == User.where(active: true)
#
- # This method is similar to <tt>except</tt>, but unlike
- # <tt>except</tt>, it persists across merges:
+ # This method is similar to #except, but unlike
+ # #except, it persists across merges:
#
# User.order('email').merge(User.except(:order))
# == User.order('email')
@@ -375,7 +362,7 @@ module ActiveRecord
#
# This means it can be used in association definitions:
#
- # has_many :comments, -> { unscope where: :trashed }
+ # has_many :comments, -> { unscope(where: :trashed) }
#
def unscope(*args)
check_if_method_has_arguments!(:unscope, args)
@@ -389,7 +376,11 @@ module ActiveRecord
args.each do |scope|
case scope
when Symbol
- symbol_unscoping(scope)
+ scope = :left_outer_joins if scope == :left_joins
+ if !VALID_UNSCOPING_VALUES.include?(scope)
+ raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}."
+ end
+ set_value(scope, DEFAULT_VALUES[scope])
when Hash
scope.each do |key, target_value|
if key != :where
@@ -407,15 +398,35 @@ module ActiveRecord
self
end
- # Performs a joins on +args+:
+ # Performs a joins on +args+. The given symbol(s) should match the name of
+ # the association(s).
#
# User.joins(:posts)
- # => SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id"
+ # # SELECT "users".*
+ # # FROM "users"
+ # # INNER JOIN "posts" ON "posts"."user_id" = "users"."id"
+ #
+ # Multiple joins:
+ #
+ # User.joins(:posts, :account)
+ # # SELECT "users".*
+ # # FROM "users"
+ # # INNER JOIN "posts" ON "posts"."user_id" = "users"."id"
+ # # INNER JOIN "accounts" ON "accounts"."id" = "users"."account_id"
+ #
+ # Nested joins:
+ #
+ # User.joins(posts: [:comments])
+ # # SELECT "users".*
+ # # FROM "users"
+ # # INNER JOIN "posts" ON "posts"."user_id" = "users"."id"
+ # # INNER JOIN "comments" "comments_posts"
+ # # ON "comments_posts"."post_id" = "posts"."id"
#
# You can use strings in order to customize your joins:
#
# User.joins("LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id")
- # => SELECT "users".* FROM "users" LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id
+ # # SELECT "users".* FROM "users" LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id
def joins(*args)
check_if_method_has_arguments!(:joins, args)
spawn.joins!(*args)
@@ -428,6 +439,24 @@ module ActiveRecord
self
end
+ # Performs a left outer joins on +args+:
+ #
+ # User.left_outer_joins(:posts)
+ # => SELECT "users".* FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"
+ #
+ def left_outer_joins(*args)
+ check_if_method_has_arguments!(__callee__, args)
+ spawn.left_outer_joins!(*args)
+ end
+ alias :left_joins :left_outer_joins
+
+ def left_outer_joins!(*args) # :nodoc:
+ args.compact!
+ args.flatten!
+ self.left_outer_joins_values += args
+ self
+ end
+
# Returns a new relation, which is the result of filtering the current relation
# according to the conditions in the arguments.
#
@@ -471,7 +500,7 @@ module ActiveRecord
# than the previous methods; you are responsible for ensuring that the values in the template
# are properly quoted. The values are passed to the connector for quoting, but the caller
# is responsible for ensuring they are enclosed in quotes in the resulting SQL. After quoting,
- # the values are inserted using the same escapes as the Ruby core method <tt>Kernel::sprintf</tt>.
+ # the values are inserted using the same escapes as the Ruby core method +Kernel::sprintf+.
#
# User.where(["name = '%s' and email = '%s'", "Joe", "joe@example.com"])
# # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com';
@@ -548,7 +577,7 @@ module ActiveRecord
# If the condition is any blank-ish object, then #where is a no-op and returns
# the current relation.
def where(opts = :chain, *rest)
- if opts == :chain
+ if :chain == opts
WhereChain.new(spawn)
elsif opts.blank?
self
@@ -558,23 +587,25 @@ module ActiveRecord
end
def where!(opts, *rest) # :nodoc:
- if Hash === opts
- opts = sanitize_forbidden_attributes(opts)
- references!(PredicateBuilder.references(opts))
- end
-
+ opts = sanitize_forbidden_attributes(opts)
+ references!(PredicateBuilder.references(opts)) if Hash === opts
self.where_clause += where_clause_factory.build(opts, rest)
self
end
# Allows you to change a previously set where condition for a given attribute, instead of appending to that condition.
#
- # Post.where(trashed: true).where(trashed: false) # => WHERE `trashed` = 1 AND `trashed` = 0
- # Post.where(trashed: true).rewhere(trashed: false) # => WHERE `trashed` = 0
- # Post.where(active: true).where(trashed: true).rewhere(trashed: false) # => WHERE `active` = 1 AND `trashed` = 0
+ # Post.where(trashed: true).where(trashed: false)
+ # # WHERE `trashed` = 1 AND `trashed` = 0
#
- # This is short-hand for unscope(where: conditions.keys).where(conditions). Note that unlike reorder, we're only unscoping
- # the named conditions -- not the entire where statement.
+ # Post.where(trashed: true).rewhere(trashed: false)
+ # # WHERE `trashed` = 0
+ #
+ # Post.where(active: true).where(trashed: true).rewhere(trashed: false)
+ # # WHERE `active` = 1 AND `trashed` = 0
+ #
+ # This is short-hand for <tt>unscope(where: conditions.keys).where(conditions)</tt>.
+ # Note that unlike reorder, we're only unscoping the named conditions -- not the entire where statement.
def rewhere(conditions)
unscope(where: conditions.keys).where(conditions)
end
@@ -583,33 +614,34 @@ module ActiveRecord
# argument.
#
# The two relations must be structurally compatible: they must be scoping the same model, and
- # they must differ only by +where+ (if no +group+ has been defined) or +having+ (if a +group+ is
- # present). Neither relation may have a +limit+, +offset+, or +distinct+ set.
+ # they must differ only by #where (if no #group has been defined) or #having (if a #group is
+ # present). Neither relation may have a #limit, #offset, or #distinct set.
#
- # Post.where("id = 1").or(Post.where("id = 2"))
- # # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR 'id = 2'))
+ # Post.where("id = 1").or(Post.where("author_id = 3"))
+ # # SELECT `posts`.* FROM `posts` WHERE ((id = 1) OR (author_id = 3))
#
def or(other)
+ unless other.is_a? Relation
+ raise ArgumentError, "You have passed #{other.class.name} object to #or. Pass an ActiveRecord::Relation object instead."
+ end
+
spawn.or!(other)
end
def or!(other) # :nodoc:
- unless structurally_compatible_for_or?(other)
- raise ArgumentError, 'Relation passed to #or must be structurally compatible'
+ incompatible_values = structurally_incompatible_values_for_or(other)
+
+ unless incompatible_values.empty?
+ raise ArgumentError, "Relation passed to #or must be structurally compatible. Incompatible values: #{incompatible_values}"
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.references_values += other.references_values
self
end
- private def structurally_compatible_for_or?(other) # :nodoc:
- Relation::SINGLE_VALUE_METHODS.all? { |m| send("#{m}_value") == other.send("#{m}_value") } &&
- (Relation::MULTI_VALUE_METHODS - [:extending]).all? { |m| send("#{m}_values") == other.send("#{m}_values") } &&
- (Relation::CLAUSE_METHODS - [:having, :where]).all? { |m| send("#{m}_clause") != other.send("#{m}_clause") }
- end
-
# Allows to specify a HAVING clause. Note that you can't use HAVING
# without also specifying a GROUP clause.
#
@@ -619,6 +651,7 @@ module ActiveRecord
end
def having!(opts, *rest) # :nodoc:
+ opts = sanitize_forbidden_attributes(opts)
references!(PredicateBuilder.references(opts)) if Hash === opts
self.having_clause += having_clause_factory.build(opts, rest)
@@ -656,7 +689,7 @@ module ActiveRecord
end
# Specifies locking settings (default to +true+). For more information
- # on locking, please see +ActiveRecord::Locking+.
+ # on locking, please see ActiveRecord::Locking.
def lock(locks = true)
spawn.lock!(locks)
end
@@ -687,7 +720,7 @@ module ActiveRecord
# For example:
#
# @posts = current_user.visible_posts.where(name: params[:name])
- # # => the visible_posts method is expected to return a chainable Relation
+ # # the visible_posts method is expected to return a chainable Relation
#
# def visible_posts
# case role
@@ -701,7 +734,7 @@ module ActiveRecord
# end
#
def none
- where("1=0").extending!(NullRelation)
+ spawn.none!
end
def none! # :nodoc:
@@ -713,7 +746,7 @@ module ActiveRecord
#
# users = User.readonly
# users.first.save
- # => ActiveRecord::ReadOnlyRecord: ActiveRecord::ReadOnlyRecord
+ # => ActiveRecord::ReadOnlyRecord: User is marked as readonly
def readonly(value = true)
spawn.readonly!(value)
end
@@ -732,7 +765,7 @@ module ActiveRecord
# users = users.create_with(name: 'DHH')
# users.new.name # => 'DHH'
#
- # You can pass +nil+ to +create_with+ to reset attributes:
+ # You can pass +nil+ to #create_with to reset attributes:
#
# users = users.create_with(nil)
# users.new.name # => 'Oscar'
@@ -745,7 +778,7 @@ module ActiveRecord
value = sanitize_forbidden_attributes(value)
self.create_with_value = create_with_value.merge(value)
else
- self.create_with_value = {}
+ self.create_with_value = FROZEN_EMPTY_HASH
end
self
@@ -754,15 +787,15 @@ module ActiveRecord
# Specifies table from which the records will be fetched. For example:
#
# Topic.select('title').from('posts')
- # # => SELECT title FROM posts
+ # # SELECT title FROM posts
#
# Can accept other relation objects. For example:
#
# Topic.select('title').from(Topic.approved)
- # # => SELECT title FROM (SELECT * FROM topics WHERE approved = 't') subquery
+ # # SELECT title FROM (SELECT * FROM topics WHERE approved = 't') subquery
#
# Topic.select('a.title').from(Topic.approved, :a)
- # # => SELECT a.title FROM (SELECT * FROM topics WHERE approved = 't') a
+ # # SELECT a.title FROM (SELECT * FROM topics WHERE approved = 't') a
#
def from(value, subquery_name = nil)
spawn.from!(value, subquery_name)
@@ -776,26 +809,22 @@ module ActiveRecord
# Specifies whether the records should be unique or not. For example:
#
# User.select(:name)
- # # => Might return two records with the same name
+ # # Might return two records with the same name
#
# User.select(:name).distinct
- # # => Returns 1 record per distinct name
+ # # Returns 1 record per distinct name
#
# User.select(:name).distinct.distinct(false)
- # # => You can also remove the uniqueness
+ # # You can also remove the uniqueness
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.
@@ -865,247 +894,317 @@ module ActiveRecord
self
end
- # Returns the Arel object associated with the relation.
- def arel # :nodoc:
- @arel ||= build_arel
+ def skip_query_cache!(value = true) # :nodoc:
+ self.skip_query_cache_value = value
+ self
end
- private
-
- def assert_mutability!
- raise ImmutableRelation if @loaded
- raise ImmutableRelation if defined?(@arel) && @arel
+ def skip_preloading! # :nodoc:
+ self.skip_preloading_value = true
+ self
end
- def build_arel
- arel = Arel::SelectManager.new(table)
+ # Returns the Arel object associated with the relation.
+ def arel(aliases = nil) # :nodoc:
+ @arel ||= build_arel(aliases)
+ end
- build_joins(arel, joins_values.flatten) unless joins_values.empty?
+ private
+ # Returns a relation value with a given name
+ def get_value(name)
+ @values.fetch(name, DEFAULT_VALUES[name])
+ end
- arel.where(where_clause.ast) unless where_clause.empty?
- arel.having(having_clause.ast) unless having_clause.empty?
- arel.take(connection.sanitize_limit(limit_value)) if limit_value
- arel.skip(offset_value.to_i) if offset_value
- arel.group(*arel_columns(group_values.uniq.reject(&:blank?))) unless group_values.empty?
+ # Sets the relation value with the given name
+ def set_value(name, value)
+ assert_mutability!
+ @values[name] = value
+ end
- build_order(arel)
+ def assert_mutability!
+ raise ImmutableRelation if @loaded
+ raise ImmutableRelation if defined?(@arel) && @arel
+ end
- build_select(arel)
+ def build_arel(aliases)
+ arel = Arel::SelectManager.new(table)
+
+ aliases = build_joins(arel, joins_values.flatten, aliases) unless joins_values.empty?
+ build_left_outer_joins(arel, left_outer_joins_values.flatten, aliases) unless left_outer_joins_values.empty?
+
+ arel.where(where_clause.ast) unless where_clause.empty?
+ arel.having(having_clause.ast) unless having_clause.empty?
+ if limit_value
+ limit_attribute = ActiveModel::Attribute.with_cast_value(
+ "LIMIT",
+ connection.sanitize_limit(limit_value),
+ Type.default_value,
+ )
+ arel.take(Arel::Nodes::BindParam.new(limit_attribute))
+ end
+ if offset_value
+ offset_attribute = ActiveModel::Attribute.with_cast_value(
+ "OFFSET",
+ offset_value.to_i,
+ Type.default_value,
+ )
+ arel.skip(Arel::Nodes::BindParam.new(offset_attribute))
+ end
+ arel.group(*arel_columns(group_values.uniq.reject(&:blank?))) unless group_values.empty?
- arel.distinct(distinct_value)
- arel.from(build_from) unless from_clause.empty?
- arel.lock(lock_value) if lock_value
+ build_order(arel)
- arel
- end
+ build_select(arel)
- def symbol_unscoping(scope)
- if !VALID_UNSCOPING_VALUES.include?(scope)
- raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}."
- end
+ arel.distinct(distinct_value)
+ arel.from(build_from) unless from_clause.empty?
+ arel.lock(lock_value) if lock_value
- clause_method = Relation::CLAUSE_METHODS.include?(scope)
- multi_val_method = Relation::MULTI_VALUE_METHODS.include?(scope)
- if clause_method
- unscope_code = "#{scope}_clause="
- else
- unscope_code = "#{scope}_value#{'s' if multi_val_method}="
+ arel
end
- case scope
- when :order
- result = []
- else
- result = [] if multi_val_method
+ def build_from
+ opts = from_clause.value
+ name = from_clause.name
+ case opts
+ when Relation
+ if opts.eager_loading?
+ opts = opts.send(:apply_join_dependency)
+ end
+ name ||= "subquery"
+ opts.arel.as(name.to_s)
+ else
+ opts
+ end
end
- self.send(unscope_code, result)
- end
-
- def association_for_table(table_name)
- table_name = table_name.to_s
- @klass._reflect_on_association(table_name) ||
- @klass._reflect_on_association(table_name.singularize)
- end
+ def build_left_outer_joins(manager, outer_joins, aliases)
+ buckets = outer_joins.group_by do |join|
+ case join
+ when Hash, Symbol, Array
+ :association_join
+ when ActiveRecord::Associations::JoinDependency
+ :stashed_join
+ else
+ raise ArgumentError, "only Hash, Symbol and Array are allowed"
+ end
+ end
- def build_from
- opts = from_clause.value
- name = from_clause.name
- case opts
- when Relation
- name ||= 'subquery'
- opts.arel.as(name.to_s)
- else
- opts
+ build_join_query(manager, buckets, Arel::Nodes::OuterJoin, aliases)
end
- end
- def build_joins(manager, joins)
- buckets = joins.group_by do |join|
- case join
- when String
- :string_join
- when Hash, Symbol, Array
- :association_join
- when ActiveRecord::Associations::JoinDependency
- :stashed_join
- when Arel::Nodes::Join
- :join_node
- else
- raise 'unknown class: %s' % join.class.name
+ def build_joins(manager, joins, aliases)
+ buckets = joins.group_by do |join|
+ case join
+ when String
+ :string_join
+ when Hash, Symbol, Array
+ :association_join
+ when ActiveRecord::Associations::JoinDependency
+ :stashed_join
+ when Arel::Nodes::Join
+ :join_node
+ else
+ raise "unknown class: %s" % join.class.name
+ end
end
- end
- buckets.default = []
- association_joins = buckets[:association_join]
- stashed_association_joins = buckets[:stashed_join]
- join_nodes = buckets[:join_node].uniq
- string_joins = buckets[:string_join].map(&:strip).uniq
+ build_join_query(manager, buckets, Arel::Nodes::InnerJoin, aliases)
+ end
- join_list = join_nodes + convert_join_strings_to_ast(manager, string_joins)
+ def build_join_query(manager, buckets, join_type, aliases)
+ buckets.default = []
- join_dependency = ActiveRecord::Associations::JoinDependency.new(
- @klass,
- association_joins,
- join_list
- )
+ association_joins = buckets[:association_join]
+ stashed_joins = buckets[:stashed_join]
+ join_nodes = buckets[:join_node].uniq
+ string_joins = buckets[:string_join].map(&:strip).uniq
- join_infos = join_dependency.join_constraints stashed_association_joins
+ join_list = join_nodes + convert_join_strings_to_ast(string_joins)
+ alias_tracker = alias_tracker(join_list, aliases)
- join_infos.each do |info|
- info.joins.each { |join| manager.from(join) }
- manager.bind_values.concat info.binds
- end
+ join_dependency = construct_join_dependency(association_joins)
- manager.join_sources.concat(join_list)
+ joins = join_dependency.join_constraints(stashed_joins, join_type, alias_tracker)
+ joins.each { |join| manager.from(join) }
- manager
- end
+ manager.join_sources.concat(join_list)
- def convert_join_strings_to_ast(table, joins)
- joins
- .flatten
- .reject(&:blank?)
- .map { |join| table.create_string_join(Arel.sql(join)) }
- end
+ alias_tracker.aliases
+ end
- def build_select(arel)
- if select_values.any?
- arel.project(*arel_columns(select_values.uniq))
- else
- arel.project(@klass.arel_table[Arel.star])
+ def convert_join_strings_to_ast(joins)
+ joins
+ .flatten
+ .reject(&:blank?)
+ .map { |join| table.create_string_join(Arel.sql(join)) }
end
- end
- def arel_columns(columns)
- columns.map do |field|
- if (Symbol === field || String === field) && columns_hash.key?(field.to_s) && !from_clause.value
- arel_table[field]
- elsif Symbol === field
- connection.quote_table_name(field.to_s)
+ def build_select(arel)
+ if select_values.any?
+ arel.project(*arel_columns(select_values.uniq))
+ elsif klass.ignored_columns.any?
+ arel.project(*klass.column_names.map { |field| arel_attribute(field) })
else
- field
+ arel.project(table[Arel.star])
end
end
- end
- def reverse_sql_order(order_query)
- order_query = ["#{quoted_table_name}.#{quoted_primary_key} ASC"] if order_query.empty?
-
- order_query.flat_map do |o|
- case o
- when Arel::Nodes::Ordering
- o.reverse
- when String
- o.to_s.split(',').map! do |s|
- s.strip!
- s.gsub!(/\sasc\Z/i, ' DESC') || s.gsub!(/\sdesc\Z/i, ' ASC') || s.concat(' DESC')
+ def arel_columns(columns)
+ columns.flat_map do |field|
+ if (Symbol === field || String === field) && (klass.has_attribute?(field) || klass.attribute_alias?(field)) && !from_clause.value
+ arel_attribute(field)
+ elsif Symbol === field
+ connection.quote_table_name(field.to_s)
+ elsif Proc === field
+ field.call
+ else
+ field
end
- else
- o
end
end
- end
- def build_order(arel)
- orders = order_values.uniq
- orders.reject!(&:blank?)
+ def reverse_sql_order(order_query)
+ if order_query.empty?
+ return [arel_attribute(primary_key).desc] if primary_key
+ raise IrreversibleOrderError,
+ "Relation has no current order and table has no primary key to be used as default order"
+ end
- arel.order(*orders) unless orders.empty?
- end
+ order_query.flat_map do |o|
+ case o
+ when Arel::Attribute
+ o.desc
+ when Arel::Nodes::Ordering
+ o.reverse
+ when String
+ if does_not_support_reverse?(o)
+ raise IrreversibleOrderError, "Order #{o.inspect} can not be reversed automatically"
+ end
+ o.split(",").map! do |s|
+ s.strip!
+ s.gsub!(/\sasc\Z/i, " DESC") || s.gsub!(/\sdesc\Z/i, " ASC") || (s << " DESC")
+ end
+ else
+ o
+ end
+ end
+ end
- VALID_DIRECTIONS = [:asc, :desc, :ASC, :DESC,
- 'asc', 'desc', 'ASC', 'DESC'] # :nodoc:
+ def does_not_support_reverse?(order)
+ # Account for String subclasses like Arel::Nodes::SqlLiteral that
+ # override methods like #count.
+ order = String.new(order) unless order.instance_of?(String)
- def validate_order_args(args)
- args.each do |arg|
- next unless arg.is_a?(Hash)
- arg.each do |_key, value|
- raise ArgumentError, "Direction \"#{value}\" is invalid. Valid " \
- "directions are: #{VALID_DIRECTIONS.inspect}" unless VALID_DIRECTIONS.include?(value)
- end
+ # Uses SQL function with multiple arguments.
+ (order.include?(",") && order.split(",").find { |section| section.count("(") != section.count(")") }) ||
+ # Uses "nulls first" like construction.
+ /nulls (first|last)\Z/i.match?(order)
end
- end
- def preprocess_order_args(order_args)
- order_args.flatten!
- validate_order_args(order_args)
+ def build_order(arel)
+ orders = order_values.uniq
+ orders.reject!(&:blank?)
- references = order_args.grep(String)
- references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact!
- references!(references) if references.any?
+ arel.order(*orders) unless orders.empty?
+ end
- # if a symbol is given we prepend the quoted table name
- order_args.map! do |arg|
- case arg
- when Symbol
- arg = klass.attribute_alias(arg) if klass.attribute_alias?(arg)
- table[arg].asc
- when Hash
- arg.map { |field, dir|
- field = klass.attribute_alias(field) if klass.attribute_alias?(field)
- table[field].send(dir.downcase)
- }
- else
- arg
+ VALID_DIRECTIONS = [:asc, :desc, :ASC, :DESC,
+ "asc", "desc", "ASC", "DESC"].to_set # :nodoc:
+
+ def validate_order_args(args)
+ args.each do |arg|
+ next unless arg.is_a?(Hash)
+ arg.each do |_key, value|
+ unless VALID_DIRECTIONS.include?(value)
+ raise ArgumentError,
+ "Direction \"#{value}\" is invalid. Valid directions are: #{VALID_DIRECTIONS.to_a.inspect}"
+ end
+ end
end
- end.flatten!
- end
+ end
- # Checks to make sure that the arguments are not blank. Note that if some
- # blank-like object were initially passed into the query method, then this
- # method will not raise an error.
- #
- # Example:
- #
- # Post.references() # => raises an error
- # Post.references([]) # => does not raise an error
- #
- # This particular method should be called with a method_name and the args
- # passed into that method as an input. For example:
- #
- # def references(*args)
- # check_if_method_has_arguments!("references", args)
- # ...
- # end
- def check_if_method_has_arguments!(method_name, args)
- if args.blank?
- raise ArgumentError, "The method .#{method_name}() must contain arguments."
+ def preprocess_order_args(order_args)
+ order_args.map! do |arg|
+ klass.sanitize_sql_for_order(arg)
+ end
+ order_args.flatten!
+
+ @klass.disallow_raw_sql!(
+ order_args.flat_map { |a| a.is_a?(Hash) ? a.keys : a },
+ permit: AttributeMethods::ClassMethods::COLUMN_NAME_WITH_ORDER
+ )
+
+ validate_order_args(order_args)
+
+ references = order_args.grep(String)
+ references.map! { |arg| arg =~ /^\W?(\w+)\W?\./ && $1 }.compact!
+ references!(references) if references.any?
+
+ # if a symbol is given we prepend the quoted table name
+ order_args.map! do |arg|
+ case arg
+ when Symbol
+ arel_attribute(arg).asc
+ when Hash
+ arg.map { |field, dir|
+ case field
+ when Arel::Nodes::SqlLiteral
+ field.send(dir.downcase)
+ else
+ arel_attribute(field).send(dir.downcase)
+ end
+ }
+ else
+ arg
+ end
+ end.flatten!
end
- end
- def new_where_clause
- Relation::WhereClause.empty
- end
- alias new_having_clause new_where_clause
+ # Checks to make sure that the arguments are not blank. Note that if some
+ # blank-like object were initially passed into the query method, then this
+ # method will not raise an error.
+ #
+ # Example:
+ #
+ # Post.references() # raises an error
+ # Post.references([]) # does not raise an error
+ #
+ # This particular method should be called with a method_name and the args
+ # passed into that method as an input. For example:
+ #
+ # def references(*args)
+ # check_if_method_has_arguments!("references", args)
+ # ...
+ # end
+ def check_if_method_has_arguments!(method_name, args)
+ if args.blank?
+ raise ArgumentError, "The method .#{method_name}() must contain arguments."
+ end
+ end
- def where_clause_factory
- @where_clause_factory ||= Relation::WhereClauseFactory.new(klass, predicate_builder)
- end
- alias having_clause_factory where_clause_factory
+ STRUCTURAL_OR_METHODS = Relation::VALUE_METHODS - [:extending, :where, :having, :unscope, :references]
+ def structurally_incompatible_values_for_or(other)
+ values = other.values
+ STRUCTURAL_OR_METHODS.reject do |method|
+ get_value(method) == values.fetch(method, DEFAULT_VALUES[method])
+ end
+ end
- def new_from_clause
- Relation::FromClause.empty
- end
+ def where_clause_factory
+ @where_clause_factory ||= Relation::WhereClauseFactory.new(klass, predicate_builder)
+ end
+ alias having_clause_factory where_clause_factory
+
+ DEFAULT_VALUES = {
+ create_with: FROZEN_EMPTY_HASH,
+ where: Relation::WhereClause.empty,
+ having: Relation::WhereClause.empty,
+ from: Relation::FromClause.empty
+ }
+
+ Relation::MULTI_VALUE_METHODS.each do |value|
+ DEFAULT_VALUES[value] ||= FROZEN_EMPTY_ARRAY
+ end
end
end
diff --git a/activerecord/lib/active_record/relation/record_fetch_warning.rb b/activerecord/lib/active_record/relation/record_fetch_warning.rb
index 14e1bf89fa..a7d07d23e1 100644
--- a/activerecord/lib/active_record/relation/record_fetch_warning.rb
+++ b/activerecord/lib/active_record/relation/record_fetch_warning.rb
@@ -1,16 +1,18 @@
+# frozen_string_literal: true
+
module ActiveRecord
class Relation
module RecordFetchWarning
# When this module is prepended to ActiveRecord::Relation and
- # `config.active_record.warn_on_records_fetched_greater_than` is
+ # +config.active_record.warn_on_records_fetched_greater_than+ is
# set to an integer, if the number of records a query returns is
- # greater than the value of `warn_on_records_fetched_greater_than`,
+ # greater than the value of +warn_on_records_fetched_greater_than+,
# a warning is logged. This allows for the detection of queries that
# return a large number of records, which could cause memory bloat.
#
# In most cases, fetching large number of records can be performed
# efficiently using the ActiveRecord::Batches methods.
- # See active_record/lib/relation/batches.rb for more information.
+ # See ActiveRecord::Batches for more information.
def exec_queries
QueryRegistry.reset
@@ -23,23 +25,23 @@ module ActiveRecord
end
end
- ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
- payload = args.last
-
+ # :stopdoc:
+ ActiveSupport::Notifications.subscribe("sql.active_record") do |*, payload|
QueryRegistry.queries << payload[:sql]
end
+ # :startdoc:
class QueryRegistry # :nodoc:
extend ActiveSupport::PerThreadRegistry
- attr_accessor :queries
+ attr_reader :queries
def initialize
- reset
+ @queries = []
end
def reset
- @queries = []
+ @queries.clear
end
end
end
diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb
index 70da37fa84..562e04194c 100644
--- a/activerecord/lib/active_record/relation/spawn_methods.rb
+++ b/activerecord/lib/active_record/relation/spawn_methods.rb
@@ -1,17 +1,19 @@
-require 'active_support/core_ext/hash/except'
-require 'active_support/core_ext/hash/slice'
-require 'active_record/relation/merger'
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/except"
+require "active_support/core_ext/hash/slice"
+require "active_record/relation/merger"
module ActiveRecord
module SpawnMethods
-
# This is overridden by Associations::CollectionProxy
def spawn #:nodoc:
clone
end
- # Merges in the conditions from <tt>other</tt>, if <tt>other</tt> is an <tt>ActiveRecord::Relation</tt>.
+ # Merges in the conditions from <tt>other</tt>, if <tt>other</tt> is an ActiveRecord::Relation.
# Returns an array representing the intersection of the resulting records with <tt>other</tt>, if <tt>other</tt> is an array.
+ #
# Post.where(published: true).joins(:comments).merge( Comment.where(spam: false) )
# # Performs a single join query with both where conditions.
#
@@ -28,7 +30,7 @@ module ActiveRecord
# This is mainly intended for sharing common conditions between multiple associations.
def merge(other)
if other.is_a?(Array)
- to_a & other
+ records & other
elsif other
spawn.merge!(other)
else
@@ -37,11 +39,14 @@ module ActiveRecord
end
def merge!(other) # :nodoc:
- if !other.is_a?(Relation) && other.respond_to?(:to_proc)
+ if other.is_a?(Hash)
+ Relation::HashMerger.new(self, other).merge
+ elsif other.is_a?(Relation)
+ Relation::Merger.new(self, other).merge
+ elsif other.respond_to?(:to_proc)
instance_exec(&other)
else
- klass = other.is_a?(Hash) ? Relation::HashMerger : Relation::Merger
- klass.new(self, other).merge
+ raise ArgumentError, "#{other.inspect} is not an ActiveRecord::Relation"
end
end
@@ -63,8 +68,8 @@ module ActiveRecord
private
- def relation_with(values) # :nodoc:
- result = Relation.create(klass, table, predicate_builder, values)
+ def relation_with(values)
+ result = Relation.create(klass, values: values)
result.extend(*extending_values) if extending_values.any?
result
end
diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb
index 1f000b3f0f..e225628bae 100644
--- a/activerecord/lib/active_record/relation/where_clause.rb
+++ b/activerecord/lib/active_record/relation/where_clause.rb
@@ -1,68 +1,63 @@
+# frozen_string_literal: true
+
module ActiveRecord
class Relation
class WhereClause # :nodoc:
- attr_reader :binds
-
delegate :any?, :empty?, to: :predicates
- def initialize(predicates, binds)
+ def initialize(predicates)
@predicates = predicates
- @binds = binds
end
def +(other)
WhereClause.new(
predicates + other.predicates,
- binds + other.binds,
+ )
+ end
+
+ def -(other)
+ WhereClause.new(
+ predicates - other.predicates,
)
end
def merge(other)
WhereClause.new(
predicates_unreferenced_by(other) + other.predicates,
- non_conflicting_binds(other) + other.binds,
)
end
def except(*columns)
- WhereClause.new(
- predicates_except(columns),
- binds_except(columns),
- )
+ WhereClause.new(except_predicates(columns))
end
def or(other)
- if empty?
- self
- elsif other.empty?
- other
+ left = self - other
+ common = self - left
+ right = other - common
+
+ if left.empty? || right.empty?
+ common
else
- WhereClause.new(
- [ast.or(other.ast)],
- binds + other.binds
+ or_clause = WhereClause.new(
+ [left.ast.or(right.ast)],
)
+ common + or_clause
end
end
def to_h(table_name = nil)
- equalities = predicates.grep(Arel::Nodes::Equality)
+ equalities = equalities(predicates)
if table_name
equalities = equalities.select do |node|
node.left.relation.name == table_name
end
end
- binds = self.binds.map { |attr| [attr.name, attr.value] }.to_h
-
equalities.map { |node|
- name = node.left.name
- [name, binds.fetch(name.to_s) {
- case node.right
- when Array then node.right.map(&:val)
- when Arel::Nodes::Casted, Arel::Nodes::Quoted
- node.right.val
- end
- }]
+ name = node.left.name.to_s
+ value = extract_node_value(node.right)
+ [name, value]
}.to_h
end
@@ -72,102 +67,124 @@ module ActiveRecord
def ==(other)
other.is_a?(WhereClause) &&
- predicates == other.predicates &&
- binds == other.binds
+ predicates == other.predicates
end
def invert
- WhereClause.new(inverted_predicates, binds)
+ WhereClause.new(inverted_predicates)
end
def self.empty
- new([], [])
+ @empty ||= new([])
end
protected
- attr_reader :predicates
+ attr_reader :predicates
- def referenced_columns
- @referenced_columns ||= begin
- equality_nodes = predicates.select { |n| equality_node?(n) }
- Set.new(equality_nodes, &:left)
+ def referenced_columns
+ @referenced_columns ||= begin
+ equality_nodes = predicates.select { |n| equality_node?(n) }
+ Set.new(equality_nodes, &:left)
+ end
end
- end
private
+ def equalities(predicates)
+ equalities = []
+
+ predicates.each do |node|
+ case node
+ when Arel::Nodes::Equality
+ equalities << node
+ when Arel::Nodes::And
+ equalities.concat equalities(node.children)
+ end
+ end
- def predicates_unreferenced_by(other)
- predicates.reject do |n|
- equality_node?(n) && other.referenced_columns.include?(n.left)
+ equalities
end
- end
- def equality_node?(node)
- node.respond_to?(:operator) && node.operator == :==
- end
-
- def non_conflicting_binds(other)
- conflicts = referenced_columns & other.referenced_columns
- conflicts.map! { |node| node.name.to_s }
- binds.reject { |attr| conflicts.include?(attr.name) }
- end
+ def predicates_unreferenced_by(other)
+ predicates.reject do |n|
+ equality_node?(n) && other.referenced_columns.include?(n.left)
+ end
+ end
- def inverted_predicates
- predicates.map { |node| invert_predicate(node) }
- end
+ def equality_node?(node)
+ node.respond_to?(:operator) && node.operator == :==
+ end
- def invert_predicate(node)
- case node
- when NilClass
- raise ArgumentError, 'Invalid argument for .where.not(), got nil.'
- when Arel::Nodes::In
- Arel::Nodes::NotIn.new(node.left, node.right)
- when Arel::Nodes::Equality
- Arel::Nodes::NotEqual.new(node.left, node.right)
- when String
- Arel::Nodes::Not.new(Arel::Nodes::SqlLiteral.new(node))
- else
- Arel::Nodes::Not.new(node)
+ def inverted_predicates
+ predicates.map { |node| invert_predicate(node) }
end
- end
- def predicates_except(columns)
- predicates.reject do |node|
+ def invert_predicate(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)
+ when NilClass
+ raise ArgumentError, "Invalid argument for .where.not(), got nil."
+ when Arel::Nodes::In
+ Arel::Nodes::NotIn.new(node.left, node.right)
+ when Arel::Nodes::IsNotDistinctFrom
+ Arel::Nodes::IsDistinctFrom.new(node.left, node.right)
+ when Arel::Nodes::IsDistinctFrom
+ Arel::Nodes::IsNotDistinctFrom.new(node.left, node.right)
+ when Arel::Nodes::Equality
+ Arel::Nodes::NotEqual.new(node.left, node.right)
+ when String
+ Arel::Nodes::Not.new(Arel::Nodes::SqlLiteral.new(node))
+ else
+ Arel::Nodes::Not.new(node)
end
end
- end
- def binds_except(columns)
- binds.reject do |attr|
- columns.include?(attr.name)
+ def except_predicates(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)
+ end
+ end
end
- end
- def predicates_with_wrapped_sql_literals
- non_empty_predicates.map do |node|
- if Arel::Nodes::Equality === node
- node
- else
- wrap_sql_literal(node)
+ def predicates_with_wrapped_sql_literals
+ non_empty_predicates.map do |node|
+ case node
+ when Arel::Nodes::SqlLiteral, ::String
+ wrap_sql_literal(node)
+ else node
+ end
end
end
- end
- def non_empty_predicates
- predicates - ['']
- end
+ ARRAY_WITH_EMPTY_STRING = [""]
+ def non_empty_predicates
+ predicates - ARRAY_WITH_EMPTY_STRING
+ end
- def wrap_sql_literal(node)
- if ::String === node
- node = Arel.sql(node)
+ def wrap_sql_literal(node)
+ if ::String === node
+ node = Arel.sql(node)
+ end
+ Arel::Nodes::Grouping.new(node)
+ end
+
+ def extract_node_value(node)
+ case node
+ when Array
+ node.map { |v| extract_node_value(v) }
+ when Arel::Nodes::Casted, Arel::Nodes::Quoted
+ node.val
+ when Arel::Nodes::BindParam
+ value = node.value
+ if value.respond_to?(:value_before_type_cast)
+ value.value_before_type_cast
+ else
+ value
+ end
+ end
end
- Arel::Nodes::Grouping.new(node)
- end
end
end
end
diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb
index 0430922be3..c1b3eea9df 100644
--- a/activerecord/lib/active_record/relation/where_clause_factory.rb
+++ b/activerecord/lib/active_record/relation/where_clause_factory.rb
@@ -1,34 +1,33 @@
+# frozen_string_literal: true
+
module ActiveRecord
class Relation
- class WhereClauseFactory
+ class WhereClauseFactory # :nodoc:
def initialize(klass, predicate_builder)
@klass = klass
@predicate_builder = predicate_builder
end
def build(opts, other)
- binds = []
-
case opts
when String, Array
- parts = [klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))]
+ parts = [klass.sanitize_sql(other.empty? ? opts : ([opts] + other))]
when Hash
attributes = predicate_builder.resolve_column_aliases(opts)
- attributes = klass.send(:expand_hash_conditions_for_aggregates, attributes)
-
- attributes, binds = predicate_builder.create_binds(attributes)
+ attributes.stringify_keys!
parts = predicate_builder.build_from_hash(attributes)
- else
+ when Arel::Nodes::Node
parts = [opts]
+ else
+ raise ArgumentError, "Unsupported argument type: #{opts} (#{opts.class})"
end
- WhereClause.new(parts, binds)
+ WhereClause.new(parts)
end
- protected
-
- attr_reader :klass, :predicate_builder
+ private
+ attr_reader :klass, :predicate_builder
end
end
end
diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb
index 500c478e65..da6d10b6ec 100644
--- a/activerecord/lib/active_record/result.rb
+++ b/activerecord/lib/active_record/result.rb
@@ -1,7 +1,10 @@
+# frozen_string_literal: true
+
module ActiveRecord
###
- # This class encapsulates a Result returned from calling +exec_query+ on any
- # database connection adapter. For example:
+ # This class encapsulates a result returned from calling
+ # {#exec_query}[rdoc-ref:ConnectionAdapters::DatabaseStatements#exec_query]
+ # on any database connection adapter. For example:
#
# result = ActiveRecord::Base.connection.exec_query('SELECT id, title, body FROM posts')
# result # => #<ActiveRecord::Result:0xdeadbeef>
@@ -18,7 +21,7 @@ module ActiveRecord
# ]
#
# # Get an array of hashes representing the result (column => value):
- # result.to_hash
+ # result.to_a
# # => [{"id" => 1, "title" => "title_1", "body" => "body_1"},
# {"id" => 2, "title" => "title_2", "body" => "body_2"},
# ...
@@ -31,8 +34,6 @@ module ActiveRecord
class Result
include Enumerable
- IDENTITY_TYPE = Type::Value.new # :nodoc:
-
attr_reader :columns, :rows, :column_types
def initialize(columns, rows, column_types = {})
@@ -42,10 +43,20 @@ module ActiveRecord
@column_types = column_types
end
+ # Returns true if this result set includes the column named +name+
+ def includes_column?(name)
+ @columns.include? name
+ 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 }
@@ -55,36 +66,62 @@ module ActiveRecord
end
def to_hash
- hash_rows
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ `ActiveRecord::Result#to_hash` has been renamed to `to_a`.
+ `to_hash` is deprecated and will be removed in Rails 6.1.
+ MSG
+ to_a
end
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
+ alias :to_a :to_ary
+
def [](idx)
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
- hash_rows.last
+ return nil if @rows.empty?
+ Hash[@columns.zip(@rows.last)]
end
def cast_values(type_overrides = {}) # :nodoc:
- types = columns.map { |name| column_type(name, type_overrides) }
- result = rows.map do |values|
- types.zip(values).map { |type, value| type.deserialize(value) }
- end
+ if columns.one?
+ # Separated to avoid allocating an array per row
+
+ type = column_type(columns.first, type_overrides)
- columns.one? ? result.map!(&:first) : result
+ rows.map do |(value)|
+ type.deserialize(value)
+ end
+ else
+ types = columns.map { |name| column_type(name, type_overrides) }
+
+ rows.map do |values|
+ Array.new(values.size) { |i| types[i].deserialize(values[i]) }
+ end
+ end
end
def initialize_copy(other)
@@ -96,36 +133,36 @@ module ActiveRecord
private
- def column_type(name, type_overrides = {})
- type_overrides.fetch(name) do
- column_types.fetch(name, IDENTITY_TYPE)
+ def column_type(name, type_overrides = {})
+ type_overrides.fetch(name) do
+ column_types.fetch(name, Type.default_value)
+ end
end
- end
- def hash_rows
- @hash_rows ||=
- begin
- # We freeze the strings to prevent them getting duped when
- # used as keys in ActiveRecord::Base's @attributes hash
- columns = @columns.map { |c| c.dup.freeze }
- @rows.map { |row|
- # In the past we used Hash[columns.zip(row)]
- # though elegant, the verbose way is much more efficient
- # both time and memory wise cause it avoids a big array allocation
- # this method is called a lot and needs to be micro optimised
- hash = {}
-
- index = 0
- length = columns.length
-
- while index < length
- hash[columns[index]] = row[index]
- index += 1
- end
-
- hash
- }
- end
- end
+ def hash_rows
+ @hash_rows ||=
+ begin
+ # We freeze the strings to prevent them getting duped when
+ # used as keys in ActiveRecord::Base's @attributes hash
+ columns = @columns.map(&:-@)
+ length = columns.length
+
+ @rows.map { |row|
+ # In the past we used Hash[columns.zip(row)]
+ # though elegant, the verbose way is much more efficient
+ # both time and memory wise cause it avoids a big array allocation
+ # this method is called a lot and needs to be micro optimised
+ hash = {}
+
+ index = 0
+ while index < length
+ hash[columns[index]] = row[index]
+ index += 1
+ end
+
+ hash
+ }
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/runtime_registry.rb b/activerecord/lib/active_record/runtime_registry.rb
index 9d605b826a..4975cb8967 100644
--- a/activerecord/lib/active_record/runtime_registry.rb
+++ b/activerecord/lib/active_record/runtime_registry.rb
@@ -1,4 +1,6 @@
-require 'active_support/per_thread_registry'
+# frozen_string_literal: true
+
+require "active_support/per_thread_registry"
module ActiveRecord
# This is a thread locals registry for Active Record. For example:
@@ -7,14 +9,14 @@ module ActiveRecord
#
# returns the connection handler local to the current thread.
#
- # See the documentation of <tt>ActiveSupport::PerThreadRegistry</tt>
+ # See the documentation of ActiveSupport::PerThreadRegistry
# for further details.
class RuntimeRegistry # :nodoc:
extend ActiveSupport::PerThreadRegistry
- attr_accessor :connection_handler, :sql_runtime, :connection_id
+ attr_accessor :connection_handler, :sql_runtime
- [:connection_handler, :sql_runtime, :connection_id].each do |val|
+ [:connection_handler, :sql_runtime].each do |val|
class_eval %{ def self.#{val}; instance.#{val}; end }, __FILE__, __LINE__
class_eval %{ def self.#{val}=(x); instance.#{val}=x; end }, __FILE__, __LINE__
end
diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb
index ba75ffa5a1..3485d9e557 100644
--- a/activerecord/lib/active_record/sanitization.rb
+++ b/activerecord/lib/active_record/sanitization.rb
@@ -1,21 +1,25 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Sanitization
extend ActiveSupport::Concern
module ClassMethods
- # Used to sanitize objects before they're used in an SQL SELECT statement. Delegates to <tt>connection.quote</tt>.
- def sanitize(object) #:nodoc:
- connection.quote(object)
- end
- alias_method :quote_value, :sanitize
-
- protected
-
# Accepts an array or string of SQL conditions and sanitizes
# them into a valid SQL fragment for a WHERE clause.
- # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
- # "name='foo''bar' and group_id='4'" returns "name='foo''bar' and group_id='4'"
- def sanitize_sql_for_conditions(condition, table_name = self.table_name)
+ #
+ # sanitize_sql_for_conditions(["name=? and group_id=?", "foo'bar", 4])
+ # # => "name='foo''bar' and group_id=4"
+ #
+ # sanitize_sql_for_conditions(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
+ # # => "name='foo''bar' and group_id='4'"
+ #
+ # sanitize_sql_for_conditions(["name='%s' and group_id='%s'", "foo'bar", 4])
+ # # => "name='foo''bar' and group_id='4'"
+ #
+ # sanitize_sql_for_conditions("name='foo''bar' and group_id='4'")
+ # # => "name='foo''bar' and group_id='4'"
+ def sanitize_sql_for_conditions(condition)
return nil if condition.blank?
case condition
@@ -23,13 +27,23 @@ module ActiveRecord
else condition
end
end
- alias_method :sanitize_sql, :sanitize_sql_for_conditions
- alias_method :sanitize_conditions, :sanitize_sql
+ alias :sanitize_sql :sanitize_sql_for_conditions
# Accepts an array, hash, or string of SQL conditions and sanitizes
# them into a valid SQL fragment for a SET clause.
- # { name: nil, group_id: 4 } returns "name = NULL , group_id='4'"
- def sanitize_sql_for_assignment(assignments, default_table_name = self.table_name)
+ #
+ # sanitize_sql_for_assignment(["name=? and group_id=?", nil, 4])
+ # # => "name=NULL and group_id=4"
+ #
+ # sanitize_sql_for_assignment(["name=:name and group_id=:group_id", name: nil, group_id: 4])
+ # # => "name=NULL and group_id=4"
+ #
+ # Post.sanitize_sql_for_assignment({ name: nil, group_id: 4 })
+ # # => "`posts`.`name` = NULL, `posts`.`group_id` = 4"
+ #
+ # sanitize_sql_for_assignment("name=NULL and group_id='4'")
+ # # => "name=NULL and group_id='4'"
+ def sanitize_sql_for_assignment(assignments, default_table_name = table_name)
case assignments
when Array; sanitize_sql_array(assignments)
when Hash; sanitize_sql_hash_for_assignment(assignments, default_table_name)
@@ -37,49 +51,59 @@ module ActiveRecord
end
end
- # Accepts a hash of SQL conditions and replaces those attributes
- # that correspond to a +composed_of+ relationship with their expanded
- # aggregate attribute values.
- # Given:
- # class Person < ActiveRecord::Base
- # composed_of :address, class_name: "Address",
- # mapping: [%w(address_street street), %w(address_city city)]
- # end
- # Then:
- # { address: Address.new("813 abc st.", "chicago") }
- # # => { address_street: "813 abc st.", address_city: "chicago" }
- def expand_hash_conditions_for_aggregates(attrs)
- expanded_attrs = {}
- attrs.each do |attr, value|
- if aggregation = reflect_on_aggregation(attr.to_sym)
- mapping = aggregation.mapping
- mapping.each do |field_attr, aggregate_attr|
- if mapping.size == 1 && !value.respond_to?(aggregate_attr)
- expanded_attrs[field_attr] = value
- else
- expanded_attrs[field_attr] = value.send(aggregate_attr)
- end
- end
- else
- expanded_attrs[attr] = value
+ # Accepts an array, or string of SQL conditions and sanitizes
+ # them into a valid SQL fragment for an ORDER clause.
+ #
+ # sanitize_sql_for_order(["field(id, ?)", [1,3,2]])
+ # # => "field(id, 1,3,2)"
+ #
+ # sanitize_sql_for_order("id ASC")
+ # # => "id ASC"
+ def sanitize_sql_for_order(condition)
+ if condition.is_a?(Array) && condition.first.to_s.include?("?")
+ disallow_raw_sql!([condition.first],
+ permit: AttributeMethods::ClassMethods::COLUMN_NAME_WITH_ORDER
+ )
+
+ # Ensure we aren't dealing with a subclass of String that might
+ # override methods we use (eg. Arel::Nodes::SqlLiteral).
+ if condition.first.kind_of?(String) && !condition.first.instance_of?(String)
+ condition = [String.new(condition.first), *condition[1..-1]]
end
+
+ Arel.sql(sanitize_sql_array(condition))
+ else
+ condition
end
- expanded_attrs
end
# Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause.
- # { status: nil, group_id: 1 }
- # # => "status = NULL , group_id = 1"
+ #
+ # sanitize_sql_hash_for_assignment({ status: nil, group_id: 1 }, "posts")
+ # # => "`posts`.`status` = NULL, `posts`.`group_id` = 1"
def sanitize_sql_hash_for_assignment(attrs, table)
c = connection
attrs.map do |attr, value|
- value = type_for_attribute(attr.to_s).serialize(value)
+ type = type_for_attribute(attr)
+ value = type.serialize(type.cast(value))
"#{c.quote_table_name_for_assignment(table, attr)} = #{c.quote(value)}"
- end.join(', ')
+ end.join(", ")
end
# Sanitizes a +string+ so that it is safe to use within an SQL
- # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%"
+ # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%".
+ #
+ # sanitize_sql_like("100%")
+ # # => "100\\%"
+ #
+ # sanitize_sql_like("snake_cased_string")
+ # # => "snake\\_cased\\_string"
+ #
+ # sanitize_sql_like("100%", "!")
+ # # => "100!%"
+ #
+ # sanitize_sql_like("snake_cased_string", "!")
+ # # => "snake!_cased!_string"
def sanitize_sql_like(string, escape_character = "\\")
pattern = Regexp.union(escape_character, "%", "_")
string.gsub(pattern) { |x| [escape_character, x].join }
@@ -87,12 +111,20 @@ module ActiveRecord
# Accepts an array of conditions. The array has each value
# sanitized and interpolated into the SQL statement.
- # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
+ #
+ # sanitize_sql_array(["name=? and group_id=?", "foo'bar", 4])
+ # # => "name='foo''bar' and group_id=4"
+ #
+ # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
+ # # => "name='foo''bar' and group_id=4"
+ #
+ # sanitize_sql_array(["name='%s' and group_id='%s'", "foo'bar", 4])
+ # # => "name='foo''bar' and group_id='4'"
def sanitize_sql_array(ary)
statement, *values = ary
- if values.first.is_a?(Hash) && statement =~ /:\w+/
+ if values.first.is_a?(Hash) && /:\w+/.match?(statement)
replace_named_bind_variables(statement, values.first)
- elsif statement.include?('?')
+ elsif statement.include?("?")
replace_bind_variables(statement, values)
elsif statement.blank?
statement
@@ -101,57 +133,90 @@ module ActiveRecord
end
end
- def replace_bind_variables(statement, values) #:nodoc:
- raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size)
- bound = values.dup
- c = connection
- statement.gsub(/\?/) do
- replace_bind_variable(bound.shift, c)
+ private
+ # Accepts a hash of SQL conditions and replaces those attributes
+ # that correspond to a {#composed_of}[rdoc-ref:Aggregations::ClassMethods#composed_of]
+ # relationship with their expanded aggregate attribute values.
+ #
+ # Given:
+ #
+ # class Person < ActiveRecord::Base
+ # composed_of :address, class_name: "Address",
+ # mapping: [%w(address_street street), %w(address_city city)]
+ # end
+ #
+ # Then:
+ #
+ # { address: Address.new("813 abc st.", "chicago") }
+ # # => { address_street: "813 abc st.", address_city: "chicago" }
+ def expand_hash_conditions_for_aggregates(attrs) # :doc:
+ expanded_attrs = {}
+ attrs.each do |attr, value|
+ if aggregation = reflect_on_aggregation(attr.to_sym)
+ mapping = aggregation.mapping
+ mapping.each do |field_attr, aggregate_attr|
+ expanded_attrs[field_attr] = if value.is_a?(Array)
+ value.map { |it| it.send(aggregate_attr) }
+ elsif mapping.size == 1 && !value.respond_to?(aggregate_attr)
+ value
+ else
+ value.send(aggregate_attr)
+ end
+ end
+ else
+ expanded_attrs[attr] = value
+ end
+ end
+ expanded_attrs
end
- end
+ deprecate :expand_hash_conditions_for_aggregates
- def replace_bind_variable(value, c = connection) #:nodoc:
- if ActiveRecord::Relation === value
- value.to_sql
- else
- quote_bound_value(value, c)
+ def replace_bind_variables(statement, values)
+ raise_if_bind_arity_mismatch(statement, statement.count("?"), values.size)
+ bound = values.dup
+ c = connection
+ statement.gsub(/\?/) do
+ replace_bind_variable(bound.shift, c)
+ end
end
- end
- def replace_named_bind_variables(statement, bind_vars) #:nodoc:
- statement.gsub(/(:?):([a-zA-Z]\w*)/) do |match|
- if $1 == ':' # skip postgresql casts
- match # return the whole match
- elsif bind_vars.include?(match = $2.to_sym)
- replace_bind_variable(bind_vars[match])
+ def replace_bind_variable(value, c = connection)
+ if ActiveRecord::Relation === value
+ value.to_sql
else
- raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}"
+ quote_bound_value(value, c)
end
end
- end
- def quote_bound_value(value, c = connection) #:nodoc:
- if value.respond_to?(:map) && !value.acts_like?(:string)
- if value.respond_to?(:empty?) && value.empty?
- c.quote(nil)
- else
- value.map { |v| c.quote(v) }.join(',')
+ 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
+ elsif bind_vars.include?(match = $2.to_sym)
+ replace_bind_variable(bind_vars[match])
+ else
+ raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}"
+ end
end
- else
- c.quote(value)
end
- end
- def raise_if_bind_arity_mismatch(statement, expected, provided) #:nodoc:
- unless expected == provided
- raise PreparedStatementInvalid, "wrong number of bind variables (#{provided} for #{expected}) in: #{statement}"
+ 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)
+ else
+ value.map { |v| c.quote(v) }.join(",")
+ end
+ else
+ c.quote(value)
+ end
end
- end
- end
- # TODO: Deprecate this
- def quoted_id
- self.class.quote_value(@attributes[self.class.primary_key].value_for_database)
+ 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
end
end
diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb
index 0a5546a760..216359867c 100644
--- a/activerecord/lib/active_record/schema.rb
+++ b/activerecord/lib/active_record/schema.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
module ActiveRecord
- # = Active Record Schema
+ # = Active Record \Schema
#
# Allows programmers to programmatically define a schema in a portable
# DSL. This means you can define tables, indexes, etc. without using SQL
@@ -27,38 +29,42 @@ module ActiveRecord
#
# ActiveRecord::Schema is only supported by database adapters that also
# support migrations, the two features being very similar.
- class Schema < Migration
-
- # Returns the migrations paths.
- #
- # ActiveRecord::Schema.new.migrations_paths
- # # => ["db/migrate"] # Rails migration path by default.
- def migrations_paths
- ActiveRecord::Migrator.migrations_paths
- end
-
- def define(info, &block) # :nodoc:
- instance_eval(&block)
-
- unless info[:version].blank?
- initialize_schema_migrations_table
- connection.assume_migrated_upto_version(info[:version], migrations_paths)
- end
- end
-
+ class Schema < Migration::Current
# Eval the given block. All methods available to the current connection
# adapter are available within the block, so you can easily use the
- # database definition DSL to build up your schema (+create_table+,
- # +add_index+, etc.).
+ # database definition DSL to build up your schema (
+ # {create_table}[rdoc-ref:ConnectionAdapters::SchemaStatements#create_table],
+ # {add_index}[rdoc-ref:ConnectionAdapters::SchemaStatements#add_index], etc.).
#
# The +info+ hash is optional, and if given is used to define metadata
# about the current schema (currently, only the schema's version):
#
- # ActiveRecord::Schema.define(version: 20380119000001) do
+ # ActiveRecord::Schema.define(version: 2038_01_19_000001) do
# ...
# end
- def self.define(info={}, &block)
+ def self.define(info = {}, &block)
new.define(info, &block)
end
+
+ def define(info, &block) # :nodoc:
+ instance_eval(&block)
+
+ if info[:version].present?
+ ActiveRecord::SchemaMigration.create_table
+ connection.assume_migrated_upto_version(info[:version], migrations_paths)
+ end
+
+ ActiveRecord::InternalMetadata.create_table
+ ActiveRecord::InternalMetadata[:environment] = connection.migration_context.current_environment
+ end
+
+ private
+ # Returns the migrations paths.
+ #
+ # ActiveRecord::Schema.new.migrations_paths
+ # # => ["db/migrate"] # Rails migration path by default.
+ def migrations_paths
+ ActiveRecord::Migrator.migrations_paths
+ end
end
end
diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb
index c5910fa1ad..d475e77444 100644
--- a/activerecord/lib/active_record/schema_dumper.rb
+++ b/activerecord/lib/active_record/schema_dumper.rb
@@ -1,4 +1,6 @@
-require 'stringio'
+# frozen_string_literal: true
+
+require "stringio"
module ActiveRecord
# = Active Record Schema Dumper
@@ -11,14 +13,19 @@ module ActiveRecord
##
# :singleton-method:
# A list of tables which should not be dumped to the schema.
- # Acceptable values are strings as well as regexp.
- # This setting is only used if ActiveRecord::Base.schema_format == :ruby
- cattr_accessor :ignore_tables
- @@ignore_tables = []
+ # Acceptable values are strings as well as regexp if ActiveRecord::Base.schema_format == :ruby.
+ # Only strings are accepted if ActiveRecord::Base.schema_format == :sql.
+ cattr_accessor :ignore_tables, default: []
+
+ ##
+ # :singleton-method:
+ # Specify a custom regular expression matching foreign keys which name
+ # should not be dumped to db/schema.rb.
+ cattr_accessor :fk_ignore_pattern, default: /^fk_rails_[0-9a-f]{10}$/
class << self
- def dump(connection=ActiveRecord::Base.connection, stream=STDOUT, config = ActiveRecord::Base)
- new(connection, generate_options(config)).dump(stream)
+ def dump(connection = ActiveRecord::Base.connection, stream = STDOUT, config = ActiveRecord::Base)
+ connection.create_schema_dumper(generate_options(config)).dump(stream)
stream
end
@@ -43,27 +50,32 @@ module ActiveRecord
def initialize(connection, options = {})
@connection = connection
- @version = Migrator::current_version rescue nil
+ @version = connection.migration_context.current_version rescue nil
@options = options
end
- def header(stream)
- define_params = @version ? "version: #{@version}" : ""
+ # turns 20170404131909 into "2017_04_04_131909"
+ def formatted_version
+ stringified = @version.to_s
+ return stringified unless stringified.length == 14
+ stringified.insert(4, "_").insert(7, "_").insert(10, "_")
+ end
- if stream.respond_to?(:external_encoding) && stream.external_encoding
- stream.puts "# encoding: #{stream.external_encoding.name}"
- end
+ def define_params
+ @version ? "version: #{formatted_version}" : ""
+ end
+ def header(stream)
stream.puts <<HEADER
# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
-# Note that this schema.rb definition is the authoritative source for your
-# database schema. If you need to create the application database on another
-# system, you should be using db:schema:load, not running all the migrations
-# from scratch. The latter is a flawed and unsustainable approach (the more migrations
-# you'll amass, the slower it'll run and the greater likelihood for issues).
+# This file is the source Rails uses to define your schema when running `rails
+# db:schema:load`. When creating a new database, `rails db:schema:load` tends to
+# be faster and is potentially less error prone than running all of your
+# migrations from scratch. Old migrations may fail to apply correctly if those
+# migrations use external dependencies or application code.
#
# It's strongly recommended that you check this file into your version control system.
@@ -76,16 +88,8 @@ HEADER
stream.puts "end"
end
+ # extensions are only supported by PostgreSQL
def extensions(stream)
- return unless @connection.supports_extensions?
- extensions = @connection.extensions
- if extensions.any?
- stream.puts " # These are extensions that must be enabled in order to support this database"
- extensions.each do |extension|
- stream.puts " enable_extension #{extension.inspect}"
- end
- stream.puts
- end
end
def tables(stream)
@@ -104,10 +108,7 @@ HEADER
end
def table(table, stream)
- columns = @connection.columns(table).map do |column|
- column.instance_variable_set(:@table_name, table)
- column
- end
+ columns = @connection.columns(table)
begin
tbl = StringIO.new
@@ -115,63 +116,39 @@ HEADER
pk = @connection.primary_key(table)
tbl.print " create_table #{remove_prefix_and_suffix(table).inspect}"
- pkcol = columns.detect { |c| c.name == pk }
- if pkcol
- if pk != 'id'
- tbl.print %Q(, primary_key: "#{pk}")
- end
- pkcolspec = @connection.column_spec_for_primary_key(pkcol)
- if pkcolspec
- pkcolspec.each do |key, value|
- tbl.print ", #{key}: #{value}"
- end
+
+ case pk
+ when String
+ tbl.print ", primary_key: #{pk.inspect}" unless pk == "id"
+ pkcol = columns.detect { |c| c.name == pk }
+ pkcolspec = column_spec_for_primary_key(pkcol)
+ if pkcolspec.present?
+ tbl.print ", #{format_colspec(pkcolspec)}"
end
+ when Array
+ tbl.print ", primary_key: #{pk.inspect}"
else
tbl.print ", id: false"
end
- tbl.print ", force: :cascade"
table_options = @connection.table_options(table)
- tbl.print ", options: #{table_options.inspect}" unless table_options.blank?
+ if table_options.present?
+ tbl.print ", #{format_options(table_options)}"
+ end
- tbl.puts " do |t|"
+ tbl.puts ", force: :cascade do |t|"
# then dump all non-primary key columns
- column_specs = columns.map do |column|
+ columns.each do |column|
raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" unless @connection.valid_type?(column.type)
next if column.name == pk
- @connection.column_spec(column)
- end.compact
-
- # find all migration keys used in this table
- keys = @connection.migration_keys
-
- # figure out the lengths for each column based on above keys
- lengths = keys.map { |key|
- column_specs.map { |spec|
- spec[key] ? spec[key].length + 2 : 0
- }.max
- }
-
- # the string we're going to sprintf our values against, with standardized column widths
- format_string = lengths.map{ |len| "%-#{len}s" }
-
- # find the max length for the 'type' column, which is special
- type_length = column_specs.map{ |column| column[:type].length }.max
-
- # add column type definition to our format string
- format_string.unshift " t.%-#{type_length}s "
-
- format_string *= ''
-
- column_specs.each do |colspec|
- values = keys.zip(lengths).map{ |key, len| colspec.key?(key) ? colspec[key] + ", " : " " * len }
- values.unshift colspec[:type]
- tbl.print((format_string % values).gsub(/,\s*$/, ''))
+ type, colspec = column_spec(column)
+ tbl.print " t.#{type} #{column.name.inspect}"
+ tbl.print ", #{format_colspec(colspec)}" if colspec.present?
tbl.puts
end
- indexes(table, tbl)
+ indexes_in_create(table, tbl)
tbl.puts " end"
tbl.puts
@@ -183,35 +160,46 @@ HEADER
stream.puts "# #{e.message}"
stream.puts
end
-
- stream
end
+ # Keep it for indexing materialized views
def indexes(table, stream)
if (indexes = @connection.indexes(table)).any?
add_index_statements = indexes.map do |index|
- statement_parts = [
- "t.index #{index.columns.inspect}",
- "name: #{index.name.inspect}",
- ]
- statement_parts << 'unique: true' if index.unique
-
- index_lengths = (index.lengths || []).compact
- statement_parts << "length: #{Hash[index.columns.zip(index.lengths)].inspect}" if index_lengths.any?
-
- index_orders = index.orders || {}
- statement_parts << "order: #{index.orders.inspect}" if index_orders.any?
- statement_parts << "where: #{index.where.inspect}" if index.where
- statement_parts << "using: #{index.using.inspect}" if index.using
- statement_parts << "type: #{index.type.inspect}" if index.type
-
- " #{statement_parts.join(', ')}"
+ table_name = remove_prefix_and_suffix(index.table).inspect
+ " add_index #{([table_name] + index_parts(index)).join(', ')}"
end
stream.puts add_index_statements.sort.join("\n")
+ stream.puts
+ end
+ end
+
+ def indexes_in_create(table, stream)
+ if (indexes = @connection.indexes(table)).any?
+ index_statements = indexes.map do |index|
+ " t.index #{index_parts(index).join(', ')}"
+ end
+ stream.puts index_statements.sort.join("\n")
end
end
+ def index_parts(index)
+ index_parts = [
+ index.columns.inspect,
+ "name: #{index.name.inspect}",
+ ]
+ index_parts << "unique: true" if index.unique
+ index_parts << "length: #{format_index_parts(index.lengths)}" if index.lengths.present?
+ index_parts << "order: #{format_index_parts(index.orders)}" if index.orders.present?
+ index_parts << "opclass: #{format_index_parts(index.opclasses)}" if index.opclasses.present?
+ index_parts << "where: #{index.where.inspect}" if index.where
+ index_parts << "using: #{index.using.inspect}" if !@connection.default_index_type?(index)
+ index_parts << "type: #{index.type.inspect}" if index.type
+ index_parts << "comment: #{index.comment.inspect}" if index.comment
+ index_parts
+ end
+
def foreign_keys(table, stream)
if (foreign_keys = @connection.foreign_keys(table)).any?
add_foreign_key_statements = foreign_keys.map do |foreign_key|
@@ -228,7 +216,7 @@ HEADER
parts << "primary_key: #{foreign_key.primary_key.inspect}"
end
- if foreign_key.name !~ /^fk_rails_[0-9a-f]{10}$/
+ if foreign_key.export_name_on_schema_dump?
parts << "name: #{foreign_key.name.inspect}"
end
@@ -242,12 +230,30 @@ HEADER
end
end
+ def format_colspec(colspec)
+ colspec.map { |key, value| "#{key}: #{value}" }.join(", ")
+ end
+
+ def format_options(options)
+ options.map { |key, value| "#{key}: #{value.inspect}" }.join(", ")
+ end
+
+ def format_index_parts(options)
+ if options.is_a?(Hash)
+ "{ #{format_options(options)} }"
+ else
+ options.inspect
+ end
+ end
+
def remove_prefix_and_suffix(table)
- table.gsub(/^(#{@options[:table_name_prefix]})(.+)(#{@options[:table_name_suffix]})$/, "\\2")
+ prefix = Regexp.escape(@options[:table_name_prefix].to_s)
+ suffix = Regexp.escape(@options[:table_name_suffix].to_s)
+ table.sub(/\A#{prefix}(.+)#{suffix}\z/, "\\1")
end
def ignored?(table_name)
- ['schema_migrations', ignore_tables].flatten.any? do |ignored|
+ [ActiveRecord::Base.schema_migrations_table_name, ActiveRecord::Base.internal_metadata_table_name, ignore_tables].flatten.any? do |ignored|
ignored === remove_prefix_and_suffix(table_name)
end
end
diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb
index cb47bf23f7..f2d8b038fa 100644
--- a/activerecord/lib/active_record/schema_migration.rb
+++ b/activerecord/lib/active_record/schema_migration.rb
@@ -1,42 +1,39 @@
-require 'active_record/scoping/default'
-require 'active_record/scoping/named'
+# frozen_string_literal: true
+
+require "active_record/scoping/default"
+require "active_record/scoping/named"
module ActiveRecord
- class SchemaMigration < ActiveRecord::Base
+ # This class is used to create a table that keeps track of which migrations
+ # have been applied to a given database. When a migration is run, its schema
+ # number is inserted in to the `SchemaMigration.table_name` so it doesn't need
+ # to be executed the next time.
+ class SchemaMigration < ActiveRecord::Base # :nodoc:
class << self
def primary_key
- nil
+ "version"
end
def table_name
"#{table_name_prefix}#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}"
end
- def index_name
- "#{table_name_prefix}unique_#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}"
- end
-
def table_exists?
connection.table_exists?(table_name)
end
- def create_table(limit=nil)
+ def create_table
unless table_exists?
- version_options = {null: false}
- version_options[:limit] = limit if limit
+ version_options = connection.internal_string_options_for_primary_key
connection.create_table(table_name, id: false) do |t|
- t.column :version, :string, version_options
+ t.string :version, version_options
end
- connection.add_index table_name, :version, unique: true, name: index_name
end
end
def drop_table
- if table_exists?
- connection.remove_index table_name, name: index_name
- connection.drop_table(table_name)
- end
+ connection.drop_table table_name, if_exists: true
end
def normalize_migration_number(number)
@@ -44,7 +41,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.rb b/activerecord/lib/active_record/scoping.rb
index f049b658c4..9eba1254a4 100644
--- a/activerecord/lib/active_record/scoping.rb
+++ b/activerecord/lib/active_record/scoping.rb
@@ -1,4 +1,6 @@
-require 'active_support/per_thread_registry'
+# frozen_string_literal: true
+
+require "active_support/per_thread_registry"
module ActiveRecord
module Scoping
@@ -9,36 +11,36 @@ module ActiveRecord
include Named
end
- module ClassMethods
- def current_scope #:nodoc:
- ScopeRegistry.value_for(:current_scope, self.to_s)
- end
-
- def current_scope=(scope) #:nodoc:
- ScopeRegistry.set_value_for(:current_scope, self.to_s, scope)
- end
-
+ module ClassMethods # :nodoc:
# Collects attributes from scopes that should be applied when creating
# an AR instance for the particular class this is called on.
- def scope_attributes # :nodoc:
+ def scope_attributes
all.scope_for_create
end
# Are there attributes associated with this scope?
- def scope_attributes? # :nodoc:
+ def scope_attributes?
current_scope
end
+
+ private
+ def current_scope(skip_inherited_scope = false)
+ ScopeRegistry.value_for(:current_scope, self, skip_inherited_scope)
+ end
+
+ def current_scope=(scope)
+ ScopeRegistry.set_value_for(:current_scope, self, scope)
+ end
end
- def populate_with_current_scope_attributes
+ def populate_with_current_scope_attributes # :nodoc:
return unless self.class.scope_attributes?
- self.class.scope_attributes.each do |att,value|
- send("#{att}=", value) if respond_to?("#{att}=")
- end
+ attributes = self.class.scope_attributes
+ _assign_attributes(attributes) if attributes.any?
end
- def initialize_internals_callback
+ def initialize_internals_callback # :nodoc:
super
populate_with_current_scope_attributes
end
@@ -53,18 +55,18 @@ module ActiveRecord
# following code:
#
# registry = ActiveRecord::Scoping::ScopeRegistry
- # registry.set_value_for(:current_scope, "Board", some_new_scope)
+ # registry.set_value_for(:current_scope, Board, some_new_scope)
#
# Now when you run:
#
- # registry.value_for(:current_scope, "Board")
+ # registry.value_for(:current_scope, Board)
#
- # You will obtain whatever was defined in +some_new_scope+. The +value_for+
- # and +set_value_for+ methods are delegated to the current +ScopeRegistry+
+ # You will obtain whatever was defined in +some_new_scope+. The #value_for
+ # and #set_value_for methods are delegated to the current ScopeRegistry
# object, so the above example code can also be called as:
#
# ActiveRecord::Scoping::ScopeRegistry.set_value_for(:current_scope,
- # "Board", some_new_scope)
+ # Board, some_new_scope)
class ScopeRegistry # :nodoc:
extend ActiveSupport::PerThreadRegistry
@@ -74,25 +76,32 @@ module ActiveRecord
@registry = Hash.new { |hash, key| hash[key] = {} }
end
- # Obtains the value for a given +scope_name+ and +variable_name+.
- def value_for(scope_type, variable_name)
+ # Obtains the value for a given +scope_type+ and +model+.
+ def value_for(scope_type, model, skip_inherited_scope = false)
raise_invalid_scope_type!(scope_type)
- @registry[scope_type][variable_name]
+ return @registry[scope_type][model.name] if skip_inherited_scope
+ klass = model
+ base = model.base_class
+ while klass <= base
+ value = @registry[scope_type][klass.name]
+ return value if value
+ klass = klass.superclass
+ end
end
- # Sets the +value+ for a given +scope_type+ and +variable_name+.
- def set_value_for(scope_type, variable_name, value)
+ # Sets the +value+ for a given +scope_type+ and +model+.
+ def set_value_for(scope_type, model, value)
raise_invalid_scope_type!(scope_type)
- @registry[scope_type][variable_name] = value
+ @registry[scope_type][model.name] = value
end
private
- def raise_invalid_scope_type!(scope_type)
- if !VALID_SCOPE_TYPES.include?(scope_type)
- raise ArgumentError, "Invalid scope type '#{scope_type}' sent to the registry. Scope types must be included in VALID_SCOPE_TYPES"
+ def raise_invalid_scope_type!(scope_type)
+ if !VALID_SCOPE_TYPES.include?(scope_type)
+ raise ArgumentError, "Invalid scope type '#{scope_type}' sent to the registry. Scope types must be included in VALID_SCOPE_TYPES"
+ end
end
- end
end
end
end
diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb
index 3590b8846e..6caf9b3251 100644
--- a/activerecord/lib/active_record/scoping/default.rb
+++ b/activerecord/lib/active_record/scoping/default.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Scoping
module Default
@@ -5,9 +7,8 @@ module ActiveRecord
included do
# Stores the default scope for the class.
- class_attribute :default_scopes, instance_writer: false, instance_predicate: false
-
- self.default_scopes = []
+ class_attribute :default_scopes, instance_writer: false, instance_predicate: false, default: []
+ class_attribute :default_scope_override, instance_writer: false, instance_predicate: false, default: nil
end
module ClassMethods
@@ -15,7 +16,7 @@ module ActiveRecord
#
# class Post < ActiveRecord::Base
# def self.default_scope
- # where published: true
+ # where(published: true)
# end
# end
#
@@ -30,7 +31,14 @@ module ActiveRecord
# Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10"
# }
def unscoped
- block_given? ? relation.scoping { yield } : relation
+ block_given? ? _scoping(relation) { yield } : relation
+ end
+
+ def _scoping(relation) # :nodoc:
+ previous, self.current_scope = current_scope(true), relation
+ yield
+ ensure
+ self.current_scope = previous
end
# Are there attributes associated with this scope?
@@ -42,97 +50,109 @@ 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.
- #
- # class Article < ActiveRecord::Base
- # default_scope { where(published: true) }
- # end
- #
- # Article.all # => SELECT * FROM articles WHERE published = true
- #
- # The +default_scope+ is also applied while creating/building a record.
- # It is not applied while updating a record.
- #
- # Article.new.published # => true
- # Article.create.published # => true
- #
- # (You can also pass any object which responds to +call+ to the
- # +default_scope+ macro, and it will be called when building the
- # default scope.)
- #
- # If you use multiple +default_scope+ declarations in your model then
- # they will be merged together:
- #
- # class Article < ActiveRecord::Base
- # default_scope { where(published: true) }
- # default_scope { where(rating: 'G') }
- # end
- #
- # Article.all # => SELECT * FROM articles WHERE published = true AND rating = 'G'
- #
- # This is also the case with inheritance and module includes where the
- # parent or module defines a +default_scope+ and the child or including
- # class defines a second one.
- #
- # If you need to do more complex things with a default scope, you can
- # alternatively define it as a class method:
- #
- # class Article < ActiveRecord::Base
- # def self.default_scope
- # # Should return a scope, you can call 'super' here etc.
- # end
- # end
- def default_scope(scope = nil)
- scope = Proc.new if block_given?
-
- if scope.is_a?(Relation) || !scope.respond_to?(:call)
- raise ArgumentError,
- "Support for calling #default_scope without a block is removed. For example instead " \
- "of `default_scope where(color: 'red')`, please use " \
- "`default_scope { where(color: 'red') }`. (Alternatively you can just redefine " \
- "self.default_scope.)"
+ # Use this macro in your model to set a default scope for all operations on
+ # the model.
+ #
+ # class Article < ActiveRecord::Base
+ # default_scope { where(published: true) }
+ # end
+ #
+ # Article.all # => SELECT * FROM articles WHERE published = true
+ #
+ # The #default_scope is also applied while creating/building a record.
+ # It is not applied while updating a record.
+ #
+ # Article.new.published # => true
+ # Article.create.published # => true
+ #
+ # (You can also pass any object which responds to +call+ to the
+ # +default_scope+ macro, and it will be called when building the
+ # default scope.)
+ #
+ # If you use multiple #default_scope declarations in your model then
+ # they will be merged together:
+ #
+ # class Article < ActiveRecord::Base
+ # default_scope { where(published: true) }
+ # default_scope { where(rating: 'G') }
+ # end
+ #
+ # Article.all # => SELECT * FROM articles WHERE published = true AND rating = 'G'
+ #
+ # This is also the case with inheritance and module includes where the
+ # parent or module defines a #default_scope and the child or including
+ # class defines a second one.
+ #
+ # If you need to do more complex things with a default scope, you can
+ # alternatively define it as a class method:
+ #
+ # class Article < ActiveRecord::Base
+ # def self.default_scope
+ # # Should return a scope, you can call 'super' here etc.
+ # end
+ # end
+ def default_scope(scope = nil) # :doc:
+ scope = Proc.new if block_given?
+
+ if scope.is_a?(Relation) || !scope.respond_to?(:call)
+ raise ArgumentError,
+ "Support for calling #default_scope without a block is removed. For example instead " \
+ "of `default_scope where(color: 'red')`, please use " \
+ "`default_scope { where(color: 'red') }`. (Alternatively you can just redefine " \
+ "self.default_scope.)"
+ end
+
+ self.default_scopes += [scope]
end
- self.default_scopes += [scope]
- end
+ def build_default_scope(base_rel = nil)
+ return if abstract_class?
+
+ if default_scope_override.nil?
+ self.default_scope_override = !Base.is_a?(method(:default_scope).owner)
+ end
- def build_default_scope(base_rel = relation) # :nodoc:
- if !Base.is_a?(method(:default_scope).owner)
- # The user has defined their own default scope method, so call that
- evaluate_default_scope { default_scope }
- elsif default_scopes.any?
- evaluate_default_scope do
- default_scopes.inject(base_rel) do |default_scope, scope|
- default_scope.merge(base_rel.scoping { scope.call })
+ if default_scope_override
+ # The user has defined their own default scope method, so call that
+ evaluate_default_scope do
+ if scope = default_scope
+ (base_rel ||= relation).merge!(scope)
+ end
+ end
+ elsif default_scopes.any?
+ base_rel ||= relation
+ evaluate_default_scope do
+ default_scopes.inject(base_rel) do |default_scope, scope|
+ scope = scope.respond_to?(:to_proc) ? scope : scope.method(:call)
+ default_scope.merge!(base_rel.instance_exec(&scope))
+ end
end
end
end
- end
- def ignore_default_scope? # :nodoc:
- ScopeRegistry.value_for(:ignore_default_scope, self)
- end
+ def ignore_default_scope?
+ ScopeRegistry.value_for(:ignore_default_scope, base_class)
+ end
- def ignore_default_scope=(ignore) # :nodoc:
- ScopeRegistry.set_value_for(:ignore_default_scope, self, ignore)
- end
+ 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
+ return if ignore_default_scope?
- # 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:
- return if ignore_default_scope?
-
- begin
- self.ignore_default_scope = true
- yield
- ensure
- self.ignore_default_scope = false
+ begin
+ self.ignore_default_scope = true
+ yield
+ ensure
+ self.ignore_default_scope = false
+ end
end
- end
end
end
end
diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb
index 7b62626896..d5cc5db97e 100644
--- a/activerecord/lib/active_record/scoping/named.rb
+++ b/activerecord/lib/active_record/scoping/named.rb
@@ -1,6 +1,8 @@
-require 'active_support/core_ext/array'
-require 'active_support/core_ext/hash/except'
-require 'active_support/core_ext/kernel/singleton_class'
+# frozen_string_literal: true
+
+require "active_support/core_ext/array"
+require "active_support/core_ext/hash/except"
+require "active_support/core_ext/kernel/singleton_class"
module ActiveRecord
# = Active Record \Named \Scopes
@@ -9,7 +11,7 @@ module ActiveRecord
extend ActiveSupport::Concern
module ClassMethods
- # Returns an <tt>ActiveRecord::Relation</tt> scope object.
+ # Returns an ActiveRecord::Relation scope object.
#
# posts = Post.all
# posts.size # Fires "select count(*) from posts" and returns the count
@@ -20,27 +22,48 @@ module ActiveRecord
# fruits = fruits.limit(10) if limited?
#
# You can define a scope that applies to all finders using
- # <tt>ActiveRecord::Base.default_scope</tt>.
+ # {default_scope}[rdoc-ref:Scoping::Default::ClassMethods#default_scope].
def all
- if current_scope
- current_scope.clone
+ scope = current_scope
+
+ if scope
+ if self == scope.klass
+ scope.clone
+ else
+ relation.merge!(scope)
+ end
else
default_scoped
end
end
- def default_scoped # :nodoc:
- scope = build_default_scope
+ def scope_for_association(scope = relation) # :nodoc:
+ if current_scope&.empty_scope?
+ scope
+ else
+ default_scoped(scope)
+ end
+ end
- if scope
- relation.spawn.merge!(scope)
+ def default_scoped(scope = relation) # :nodoc:
+ build_default_scope(scope) || scope
+ end
+
+ def default_extensions # :nodoc:
+ if scope = current_scope || build_default_scope
+ scope.extensions
else
- relation
+ []
end
end
- # Adds a class method for retrieving and querying objects. A \scope
- # represents a narrowing of a database query, such as
+ # Adds a class method for retrieving and querying objects.
+ # The method is intended to return an ActiveRecord::Relation
+ # object, which is composable with other scopes.
+ # If it returns +nil+ or +false+, an
+ # {all}[rdoc-ref:Scoping::Named::ClassMethods#all] scope is returned instead.
+ #
+ # A \scope represents a narrowing of a database query, such as
# <tt>where(color: :red).select('shirts.*').includes(:washing_instructions)</tt>.
#
# class Shirt < ActiveRecord::Base
@@ -48,12 +71,12 @@ module ActiveRecord
# scope :dry_clean_only, -> { joins(:washing_instructions).where('washing_instructions.dry_clean_only = ?', true) }
# end
#
- # The above calls to +scope+ define class methods <tt>Shirt.red</tt> and
+ # The above calls to #scope define class methods <tt>Shirt.red</tt> and
# <tt>Shirt.dry_clean_only</tt>. <tt>Shirt.red</tt>, in effect,
# represents the query <tt>Shirt.where(color: 'red')</tt>.
#
# You should always pass a callable object to the scopes defined
- # with +scope+. This ensures that the scope is re-evaluated each
+ # with #scope. This ensures that the scope is re-evaluated each
# time it is called.
#
# Note that this is simply 'syntactic sugar' for defining an actual
@@ -66,14 +89,15 @@ module ActiveRecord
# end
#
# Unlike <tt>Shirt.find(...)</tt>, however, the object returned by
- # <tt>Shirt.red</tt> is not an Array; it resembles the association object
- # constructed by a +has_many+ declaration. For instance, you can invoke
- # <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>,
+ # <tt>Shirt.red</tt> is not an Array but an ActiveRecord::Relation,
+ # which is composable with other scopes; it resembles the association object
+ # constructed by a {has_many}[rdoc-ref:Associations::ClassMethods#has_many]
+ # declaration. For instance, you can invoke <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>,
# <tt>Shirt.red.where(size: 'small')</tt>. Also, just as with the
# association objects, named \scopes act like an Array, implementing
# Enumerable; <tt>Shirt.red.each(&block)</tt>, <tt>Shirt.red.first</tt>,
# and <tt>Shirt.red.inject(memo, &block)</tt> all behave as if
- # <tt>Shirt.red</tt> really was an Array.
+ # <tt>Shirt.red</tt> really was an array.
#
# These named \scopes are composable. For instance,
# <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are
@@ -84,7 +108,8 @@ module ActiveRecord
#
# All scopes are available as class methods on the ActiveRecord::Base
# descendant upon which the \scopes were defined. But they are also
- # available to +has_many+ associations. If,
+ # available to {has_many}[rdoc-ref:Associations::ClassMethods#has_many]
+ # associations. If,
#
# class Person < ActiveRecord::Base
# has_many :shirts
@@ -93,8 +118,8 @@ module ActiveRecord
# then <tt>elton.shirts.red.dry_clean_only</tt> will return all of
# Elton's red, dry clean only shirts.
#
- # \Named scopes can also have extensions, just as with +has_many+
- # declarations:
+ # \Named scopes can also have extensions, just as with
+ # {has_many}[rdoc-ref:Associations::ClassMethods#has_many] declarations:
#
# class Shirt < ActiveRecord::Base
# scope :red, -> { where(color: 'red') } do
@@ -135,7 +160,7 @@ module ActiveRecord
# Article.featured.titles
def scope(name, body, &block)
unless body.respond_to?(:call)
- raise ArgumentError, 'The scope body needs to be callable.'
+ raise ArgumentError, "The scope body needs to be callable."
end
if dangerous_class_method?(name)
@@ -144,24 +169,40 @@ module ActiveRecord
"a class method with the same name."
end
+ if method_defined_within?(name, Relation)
+ raise ArgumentError, "You tried to define a scope named \"#{name}\" " \
+ "on the model \"#{self.name}\", but ActiveRecord::Relation already defined " \
+ "an instance method with the same name."
+ end
+
+ valid_scope_name?(name)
extension = Module.new(&block) if block
if body.respond_to?(:to_proc)
singleton_class.send(:define_method, name) do |*args|
- scope = all.scoping { instance_exec(*args, &body) }
+ scope = all._exec_scope(*args, &body)
scope = scope.extending(extension) if extension
-
- scope || all
+ scope
end
else
singleton_class.send(:define_method, name) do |*args|
- scope = all.scoping { body.call(*args) }
+ scope = body.call(*args) || all
scope = scope.extending(extension) if extension
-
- scope || all
+ scope
end
end
+
+ generate_relation_method(name)
end
+
+ private
+
+ 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
diff --git a/activerecord/lib/active_record/secure_token.rb b/activerecord/lib/active_record/secure_token.rb
index ca11853da7..bcdb33901b 100644
--- a/activerecord/lib/active_record/secure_token.rb
+++ b/activerecord/lib/active_record/secure_token.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
module ActiveRecord
module SecureToken
extend ActiveSupport::Concern
module ClassMethods
- # Example using has_secure_token
+ # Example using #has_secure_token
#
# # Schema: User(token:string, auth_token:string)
# class User < ActiveRecord::Base
@@ -18,16 +20,16 @@ module ActiveRecord
# user.regenerate_token # => true
# user.regenerate_auth_token # => true
#
- # SecureRandom::base58 is used to generate the 24-character unique token, so collisions are highly unlikely.
+ # <tt>SecureRandom::base58</tt> is used to generate the 24-character unique token, so collisions are highly unlikely.
#
# Note that it's still possible to generate a race condition in the database in the same way that
- # <tt>validates_uniqueness_of</tt> can. You're encouraged to add a unique index in the database to deal
- # with this even more unlikely scenario.
+ # {validates_uniqueness_of}[rdoc-ref:Validations::ClassMethods#validates_uniqueness_of] can.
+ # You're encouraged to add a unique index in the database to deal with this even more unlikely scenario.
def has_secure_token(attribute = :token)
# Load securerandom only when has_secure_token is used.
- require 'active_support/core_ext/securerandom'
+ 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 48c12dcf9f..741fea43ce 100644
--- a/activerecord/lib/active_record/serialization.rb
+++ b/activerecord/lib/active_record/serialization.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
module ActiveRecord #:nodoc:
- # = Active Record Serialization
+ # = Active Record \Serialization
module Serialization
extend ActiveSupport::Concern
include ActiveModel::Serializers::JSON
@@ -9,7 +11,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)
@@ -18,5 +20,3 @@ module ActiveRecord #:nodoc:
end
end
end
-
-require 'active_record/serializers/xml_serializer'
diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb
deleted file mode 100644
index 89b7e0be82..0000000000
--- a/activerecord/lib/active_record/serializers/xml_serializer.rb
+++ /dev/null
@@ -1,193 +0,0 @@
-require 'active_support/core_ext/hash/conversions'
-
-module ActiveRecord #:nodoc:
- module Serialization
- include ActiveModel::Serializers::Xml
-
- # Builds an XML document to represent the model. Some configuration is
- # available through +options+. However more complicated cases should
- # override ActiveRecord::Base#to_xml.
- #
- # By default the generated XML document will include the processing
- # instruction and all the object's attributes. For example:
- #
- # <?xml version="1.0" encoding="UTF-8"?>
- # <topic>
- # <title>The First Topic</title>
- # <author-name>David</author-name>
- # <id type="integer">1</id>
- # <approved type="boolean">false</approved>
- # <replies-count type="integer">0</replies-count>
- # <bonus-time type="dateTime">2000-01-01T08:28:00+12:00</bonus-time>
- # <written-on type="dateTime">2003-07-16T09:28:00+1200</written-on>
- # <content>Have a nice day</content>
- # <author-email-address>david@loudthinking.com</author-email-address>
- # <parent-id></parent-id>
- # <last-read type="date">2004-04-15</last-read>
- # </topic>
- #
- # This behavior can be controlled with <tt>:only</tt>, <tt>:except</tt>,
- # <tt>:skip_instruct</tt>, <tt>:skip_types</tt>, <tt>:dasherize</tt> and <tt>:camelize</tt> .
- # The <tt>:only</tt> and <tt>:except</tt> options are the same as for the
- # +attributes+ method. The default is to dasherize all column names, but you
- # can disable this setting <tt>:dasherize</tt> to +false+. Setting <tt>:camelize</tt>
- # to +true+ will camelize all column names - this also overrides <tt>:dasherize</tt>.
- # To not have the column type included in the XML output set <tt>:skip_types</tt> to +true+.
- #
- # For instance:
- #
- # topic.to_xml(skip_instruct: true, except: [ :id, :bonus_time, :written_on, :replies_count ])
- #
- # <topic>
- # <title>The First Topic</title>
- # <author-name>David</author-name>
- # <approved type="boolean">false</approved>
- # <content>Have a nice day</content>
- # <author-email-address>david@loudthinking.com</author-email-address>
- # <parent-id></parent-id>
- # <last-read type="date">2004-04-15</last-read>
- # </topic>
- #
- # To include first level associations use <tt>:include</tt>:
- #
- # firm.to_xml include: [ :account, :clients ]
- #
- # <?xml version="1.0" encoding="UTF-8"?>
- # <firm>
- # <id type="integer">1</id>
- # <rating type="integer">1</rating>
- # <name>37signals</name>
- # <clients type="array">
- # <client>
- # <rating type="integer">1</rating>
- # <name>Summit</name>
- # </client>
- # <client>
- # <rating type="integer">1</rating>
- # <name>Microsoft</name>
- # </client>
- # </clients>
- # <account>
- # <id type="integer">1</id>
- # <credit-limit type="integer">50</credit-limit>
- # </account>
- # </firm>
- #
- # Additionally, the record being serialized will be passed to a Proc's second
- # parameter. This allows for ad hoc additions to the resultant document that
- # incorporate the context of the record being serialized. And by leveraging the
- # closure created by a Proc, to_xml can be used to add elements that normally fall
- # outside of the scope of the model -- for example, generating and appending URLs
- # associated with models.
- #
- # proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) }
- # firm.to_xml procs: [ proc ]
- #
- # <firm>
- # # ... normal attributes as shown above ...
- # <name-reverse>slangis73</name-reverse>
- # </firm>
- #
- # To include deeper levels of associations pass a hash like this:
- #
- # firm.to_xml include: {account: {}, clients: {include: :address}}
- # <?xml version="1.0" encoding="UTF-8"?>
- # <firm>
- # <id type="integer">1</id>
- # <rating type="integer">1</rating>
- # <name>37signals</name>
- # <clients type="array">
- # <client>
- # <rating type="integer">1</rating>
- # <name>Summit</name>
- # <address>
- # ...
- # </address>
- # </client>
- # <client>
- # <rating type="integer">1</rating>
- # <name>Microsoft</name>
- # <address>
- # ...
- # </address>
- # </client>
- # </clients>
- # <account>
- # <id type="integer">1</id>
- # <credit-limit type="integer">50</credit-limit>
- # </account>
- # </firm>
- #
- # To include any methods on the model being called use <tt>:methods</tt>:
- #
- # firm.to_xml methods: [ :calculated_earnings, :real_earnings ]
- #
- # <firm>
- # # ... normal attributes as shown above ...
- # <calculated-earnings>100000000000000000</calculated-earnings>
- # <real-earnings>5</real-earnings>
- # </firm>
- #
- # To call any additional Procs use <tt>:procs</tt>. The Procs are passed a
- # modified version of the options hash that was given to +to_xml+:
- #
- # proc = Proc.new { |options| options[:builder].tag!('abc', 'def') }
- # firm.to_xml procs: [ proc ]
- #
- # <firm>
- # # ... normal attributes as shown above ...
- # <abc>def</abc>
- # </firm>
- #
- # Alternatively, you can yield the builder object as part of the +to_xml+ call:
- #
- # firm.to_xml do |xml|
- # xml.creator do
- # xml.first_name "David"
- # xml.last_name "Heinemeier Hansson"
- # end
- # end
- #
- # <firm>
- # # ... normal attributes as shown above ...
- # <creator>
- # <first_name>David</first_name>
- # <last_name>Heinemeier Hansson</last_name>
- # </creator>
- # </firm>
- #
- # As noted above, you may override +to_xml+ in your ActiveRecord::Base
- # subclasses to have complete control about what's generated. The general
- # form of doing this is:
- #
- # class IHaveMyOwnXML < ActiveRecord::Base
- # def to_xml(options = {})
- # require 'builder'
- # options[:indent] ||= 2
- # xml = options[:builder] ||= ::Builder::XmlMarkup.new(indent: options[:indent])
- # xml.instruct! unless options[:skip_instruct]
- # xml.level_one do
- # xml.tag!(:second_level, 'content')
- # end
- # end
- # end
- def to_xml(options = {}, &block)
- XmlSerializer.new(self, options).serialize(&block)
- end
- end
-
- class XmlSerializer < ActiveModel::Serializers::Xml::Serializer #:nodoc:
- class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc:
- def compute_type
- klass = @serializable.class
- cast_type = klass.type_for_attribute(name)
-
- type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] || cast_type.type
-
- { :text => :string,
- :time => :datetime }[type] || type
- end
- protected :compute_type
- end
- end
-end
diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb
index 95986c820c..1b1736dcab 100644
--- a/activerecord/lib/active_record/statement_cache.rb
+++ b/activerecord/lib/active_record/statement_cache.rb
@@ -1,5 +1,6 @@
-module ActiveRecord
+# frozen_string_literal: true
+module ActiveRecord
# Statement cache is used to cache a single statement in order to avoid creating the AST again.
# Initializing the cache is done by passing the statement in the create block:
#
@@ -7,12 +8,14 @@ module ActiveRecord
# Book.where(name: "my book").where("author_id > 3")
# end
#
- # The cached statement is executed by using the +execute+ method:
+ # The cached statement is executed by using the
+ # {connection.execute}[rdoc-ref:ConnectionAdapters::DatabaseStatements#execute] method:
#
- # cache.execute([], Book, Book.connection)
+ # cache.execute([], Book.connection)
#
- # The relation returned by the block is cached, and for each +execute+ call the cached relation gets duped.
- # Database is queried when +to_a+ is called on the relation.
+ # The relation returned by the block is cached, and for each
+ # {execute}[rdoc-ref:ConnectionAdapters::DatabaseStatements#execute]
+ # call the cached relation gets duped. Database is queried when +to_a+ is called on the relation.
#
# If you want to cache the statement without the values you can use the +bind+ method of the
# block parameter.
@@ -23,7 +26,7 @@ module ActiveRecord
#
# And pass the bind values as the first argument of +execute+ call.
#
- # cache.execute(["my book"], Book, Book.connection)
+ # cache.execute(["my book"], Book.connection)
class StatementCache # :nodoc:
class Substitute; end # :nodoc:
@@ -38,28 +41,53 @@ module ActiveRecord
end
class PartialQuery < Query # :nodoc:
- def initialize values
+ def initialize(values)
@values = values
- @indexes = values.each_with_index.find_all { |thing,i|
- Arel::Nodes::BindParam === thing
+ @indexes = values.each_with_index.find_all { |thing, i|
+ Substitute === thing
}.map(&:last)
end
def sql_for(binds, connection)
val = @values.dup
- binds = connection.prepare_binds_for_database(binds)
- @indexes.each { |i| val[i] = connection.quote(binds.shift) }
+ casted_binds = binds.map(&:value_for_database)
+ @indexes.each { |i| val[i] = connection.quote(casted_binds.shift) }
val.join
end
end
- def self.query(visitor, ast)
- Query.new visitor.accept(ast, Arel::Collectors::SQLString.new).value
+ class PartialQueryCollector
+ def initialize
+ @parts = []
+ @binds = []
+ end
+
+ def <<(str)
+ @parts << str
+ self
+ end
+
+ def add_bind(obj)
+ @binds << obj
+ @parts << Substitute.new
+ self
+ end
+
+ def value
+ [@parts, @binds]
+ end
+ end
+
+ def self.query(sql)
+ Query.new(sql)
end
- def self.partial_query(visitor, ast, collector)
- collected = visitor.accept(ast, collector).value
- PartialQuery.new collected
+ def self.partial_query(values)
+ PartialQuery.new(values)
+ end
+
+ def self.partial_query_collector
+ PartialQueryCollector.new
end
class Params # :nodoc:
@@ -68,7 +96,7 @@ module ActiveRecord
class BindMap # :nodoc:
def initialize(bound_attributes)
- @indexes = []
+ @indexes = []
@bound_attributes = bound_attributes
bound_attributes.each_with_index do |attr, i|
@@ -80,32 +108,39 @@ module ActiveRecord
def bind(values)
bas = @bound_attributes.dup
- @indexes.each_with_index { |offset,i| bas[offset] = bas[offset].with_cast_value(values[i]) }
+ @indexes.each_with_index { |offset, i| bas[offset] = bas[offset].with_cast_value(values[i]) }
bas
end
end
- attr_reader :bind_map, :query_builder
-
def self.create(connection, block = Proc.new)
- relation = block.call Params.new
- bind_map = BindMap.new relation.bound_attributes
- query_builder = connection.cacheable_query relation.arel
- new query_builder, bind_map
+ relation = block.call Params.new
+ query_builder, binds = connection.cacheable_query(self, relation.arel)
+ bind_map = BindMap.new(binds)
+ new(query_builder, bind_map, relation.klass)
end
- def initialize(query_builder, bind_map)
+ def initialize(query_builder, bind_map, klass)
@query_builder = query_builder
- @bind_map = bind_map
+ @bind_map = bind_map
+ @klass = klass
end
- def execute(params, klass, connection)
+ def execute(params, connection, &block)
bind_values = bind_map.bind params
sql = query_builder.sql_for bind_values, connection
- klass.find_by_sql sql, bind_values
+ klass.find_by_sql(sql, bind_values, preparable: true, &block)
end
- alias :call :execute
+
+ def self.unsupported_value?(value)
+ case value
+ when NilClass, Array, Range, Hash, Relation, Base then true
+ end
+ end
+
+ private
+ attr_reader :query_builder, :bind_map, :klass
end
end
diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb
index 919bc58ba5..3537e2d008 100644
--- a/activerecord/lib/active_record/store.rb
+++ b/activerecord/lib/active_record/store.rb
@@ -1,4 +1,6 @@
-require 'active_support/core_ext/hash/indifferent_access'
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/indifferent_access"
module ActiveRecord
# Store gives you a thin wrapper around serialize for the purpose of storing hashes in a single column.
@@ -15,8 +17,9 @@ module ActiveRecord
# You can set custom coder to encode/decode your serialized attributes to/from different formats.
# JSON, YAML, Marshal are supported out of the box. Generally it can be any wrapper that provides +load+ and +dump+.
#
- # NOTE: If you are using PostgreSQL specific columns like +hstore+ or +json+ there is no need for
- # the serialization provided by +store+. Simply use +store_accessor+ instead to generate
+ # NOTE: If you are using structured database data types (eg. PostgreSQL +hstore+/+json+, or MySQL 5.7+
+ # +json+) there is no need for the serialization provided by {.store}[rdoc-ref:rdoc-ref:ClassMethods#store].
+ # Simply use {.store_accessor}[rdoc-ref:ClassMethods#store_accessor] instead to generate
# the accessor methods. Be aware that these columns use a string keyed hash and do not allow access
# using a symbol.
#
@@ -28,10 +31,18 @@ module ActiveRecord
#
# class User < ActiveRecord::Base
# store :settings, accessors: [ :color, :homepage ], coder: JSON
+ # store :parent, accessors: [ :name ], coder: JSON, prefix: true
+ # store :spouse, accessors: [ :name ], coder: JSON, prefix: :partner
+ # store :settings, accessors: [ :two_factor_auth ], suffix: true
+ # store :settings, accessors: [ :login_retry ], suffix: :config
# end
#
- # u = User.new(color: 'black', homepage: '37signals.com')
+ # u = User.new(color: 'black', homepage: '37signals.com', parent_name: 'Mary', partner_name: 'Lily')
# u.color # Accessor stored attribute
+ # u.parent_name # Accessor stored attribute with prefix
+ # u.partner_name # Accessor stored attribute with custom prefix
+ # u.two_factor_auth_settings # Accessor stored attribute with suffix
+ # u.login_retry_config # Accessor stored attribute with custom suffix
# u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor
#
# # There is no difference between strings and symbols for accessing custom attributes
@@ -41,11 +52,13 @@ module ActiveRecord
# # Add additional accessors to an existing store through store_accessor
# class SuperUser < User
# store_accessor :settings, :privileges, :servants
+ # store_accessor :parent, :birthday, prefix: true
+ # store_accessor :settings, :secret_question, suffix: :config
# end
#
- # The stored attribute names can be retrieved using +stored_attributes+.
+ # The stored attribute names can be retrieved using {.stored_attributes}[rdoc-ref:rdoc-ref:ClassMethods#stored_attributes].
#
- # User.stored_attributes[:settings] # [:color, :homepage]
+ # User.stored_attributes[:settings] # [:color, :homepage, :two_factor_auth, :login_retry]
#
# == Overwriting default accessors
#
@@ -77,20 +90,41 @@ module ActiveRecord
module ClassMethods
def store(store_attribute, options = {})
- serialize store_attribute, IndifferentCoder.new(options[:coder])
- store_accessor(store_attribute, options[:accessors]) if options.has_key? :accessors
+ serialize store_attribute, IndifferentCoder.new(store_attribute, options[:coder])
+ store_accessor(store_attribute, options[:accessors], options.slice(:prefix, :suffix)) if options.has_key? :accessors
end
- def store_accessor(store_attribute, *keys)
+ def store_accessor(store_attribute, *keys, prefix: nil, suffix: nil)
keys = keys.flatten
+ accessor_prefix =
+ case prefix
+ when String, Symbol
+ "#{prefix}_"
+ when TrueClass
+ "#{store_attribute}_"
+ else
+ ""
+ end
+ accessor_suffix =
+ case suffix
+ when String, Symbol
+ "_#{suffix}"
+ when TrueClass
+ "_#{store_attribute}"
+ else
+ ""
+ end
+
_store_accessors_module.module_eval do
keys.each do |key|
- define_method("#{key}=") do |value|
+ accessor_key = "#{accessor_prefix}#{key}#{accessor_suffix}"
+
+ define_method("#{accessor_key}=") do |value|
write_store_attribute(store_attribute, key, value)
end
- define_method(key) do
+ define_method(accessor_key) do
read_store_attribute(store_attribute, key)
end
end
@@ -113,27 +147,26 @@ module ActiveRecord
def stored_attributes
parent = superclass.respond_to?(:stored_attributes) ? superclass.stored_attributes : {}
- if self.local_stored_attributes
- parent.merge!(self.local_stored_attributes) { |k, a, b| a | b }
+ if local_stored_attributes
+ parent.merge!(local_stored_attributes) { |k, a, b| a | b }
end
parent
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
+ type_for_attribute(store_attribute).accessor
end
class HashAccessor # :nodoc:
@@ -176,34 +209,34 @@ module ActiveRecord
end
end
- class IndifferentCoder # :nodoc:
- def initialize(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)
- end
- end
+ class IndifferentCoder # :nodoc:
+ 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(attr_name, coder_or_class_name || Object)
+ end
+ end
- def dump(obj)
- @coder.dump self.class.as_indifferent_hash(obj)
- end
+ def dump(obj)
+ @coder.dump self.class.as_indifferent_hash(obj)
+ end
- def load(yaml)
- self.class.as_indifferent_hash(@coder.load(yaml || ''))
- end
+ def load(yaml)
+ self.class.as_indifferent_hash(@coder.load(yaml || ""))
+ end
- def self.as_indifferent_hash(obj)
- case obj
- when ActiveSupport::HashWithIndifferentAccess
- obj
- when Hash
- obj.with_indifferent_access
- else
- ActiveSupport::HashWithIndifferentAccess.new
+ def self.as_indifferent_hash(obj)
+ case obj
+ when ActiveSupport::HashWithIndifferentAccess
+ obj
+ when Hash
+ obj.with_indifferent_access
+ else
+ ActiveSupport::HashWithIndifferentAccess.new
+ end
end
end
- end
end
end
diff --git a/activerecord/lib/active_record/suppressor.rb b/activerecord/lib/active_record/suppressor.rb
index b3644bf569..8cdb8e0765 100644
--- a/activerecord/lib/active_record/suppressor.rb
+++ b/activerecord/lib/active_record/suppressor.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
# ActiveRecord::Suppressor prevents the receiver from being saved during
# a given block.
@@ -30,14 +32,19 @@ module ActiveRecord
module ClassMethods
def suppress(&block)
+ previous_state = SuppressorRegistry.suppressed[name]
SuppressorRegistry.suppressed[name] = true
yield
ensure
- SuppressorRegistry.suppressed[name] = false
+ SuppressorRegistry.suppressed[name] = previous_state
end
end
- def create_or_update(*args) # :nodoc:
+ def save(*) # :nodoc:
+ SuppressorRegistry.suppressed[self.class.name] ? true : super
+ end
+
+ def save!(*) # :nodoc:
SuppressorRegistry.suppressed[self.class.name] ? true : super
end
end
diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb
index 41f1c55c3c..b67479fb6a 100644
--- a/activerecord/lib/active_record/table_metadata.rb
+++ b/activerecord/lib/active_record/table_metadata.rb
@@ -1,7 +1,8 @@
+# frozen_string_literal: true
+
module ActiveRecord
class TableMetadata # :nodoc:
- delegate :foreign_type, :foreign_key, to: :association, prefix: true
- delegate :association_primary_key, to: :association
+ delegate :foreign_type, :foreign_key, :join_primary_key, :join_foreign_key, to: :association, prefix: true
def initialize(klass, arel_table, association = nil)
@klass = klass
@@ -10,36 +11,45 @@ module ActiveRecord
end
def resolve_column_aliases(hash)
- hash = hash.dup
- hash.keys.grep(Symbol) do |key|
- if klass.attribute_alias? key
- hash[klass.attribute_alias(key)] = hash.delete key
+ new_hash = hash.dup
+ hash.each do |key, _|
+ if (key.is_a?(Symbol)) && klass.attribute_alias?(key)
+ new_hash[klass.attribute_alias(key)] = new_hash.delete(key)
end
end
- hash
+ new_hash
end
def arel_attribute(column_name)
- arel_table[column_name]
+ if klass
+ klass.arel_attribute(column_name, arel_table)
+ else
+ arel_table[column_name]
+ end
end
def type(column_name)
if klass
- klass.type_for_attribute(column_name.to_s)
+ klass.type_for_attribute(column_name)
else
- Type::Value.new
+ Type.default_value
end
end
+ def has_column?(column_name)
+ klass && klass.columns_hash.key?(column_name.to_s)
+ end
+
def associated_with?(association_name)
klass && klass._reflect_on_association(association_name)
end
def associated_table(table_name)
- return self if table_name == arel_table.name
+ association = klass._reflect_on_association(table_name) || klass._reflect_on_association(table_name.to_s.singularize)
- association = klass._reflect_on_association(table_name)
- if association && !association.polymorphic?
+ if !association && table_name == arel_table.name
+ return self
+ elsif association && !association.polymorphic?
association_klass = association.klass
arel_table = association_klass.arel_table.alias(table_name)
else
@@ -55,8 +65,15 @@ module ActiveRecord
association && association.polymorphic?
end
- protected
+ def aggregated_with?(aggregation_name)
+ klass && reflect_on_aggregation(aggregation_name)
+ end
+
+ def reflect_on_aggregation(aggregation_name)
+ klass.reflect_on_aggregation(aggregation_name)
+ end
- attr_reader :klass, :arel_table, :association
+ private
+ attr_reader :klass, :arel_table, :association
end
end
diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb
index 683741768b..27e401a756 100644
--- a/activerecord/lib/active_record/tasks/database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/database_tasks.rb
@@ -1,14 +1,16 @@
-require 'active_support/core_ext/string/filters'
+# frozen_string_literal: true
+
+require "active_record/database_configurations"
module ActiveRecord
module Tasks # :nodoc:
class DatabaseAlreadyExists < StandardError; end # :nodoc:
class DatabaseNotSupported < StandardError; end # :nodoc:
- # <tt>ActiveRecord::Tasks::DatabaseTasks</tt> is a utility class, which encapsulates
+ # ActiveRecord::Tasks::DatabaseTasks is a utility class, which encapsulates
# logic behind common tasks used to manage database and migrations.
#
- # The tasks defined here are used with Rake tasks provided by Active Record.
+ # The tasks defined here are used with Rails commands provided by Active Record.
#
# In order to use DatabaseTasks, a few config values need to be set. All the needed
# config values are set by Rails already, so it's necessary to do it only if you
@@ -18,15 +20,15 @@ module ActiveRecord
#
# The possible config values are:
#
- # * +env+: current environment (like Rails.env).
- # * +database_configuration+: configuration of your databases (as in +config/database.yml+).
- # * +db_dir+: your +db+ directory.
- # * +fixtures_path+: a path to fixtures directory.
- # * +migrations_paths+: a list of paths to directories with migrations.
- # * +seed_loader+: an object which will load seeds, it needs to respond to the +load_seed+ method.
- # * +root+: a path to the root of the application.
+ # * +env+: current environment (like Rails.env).
+ # * +database_configuration+: configuration of your databases (as in +config/database.yml+).
+ # * +db_dir+: your +db+ directory.
+ # * +fixtures_path+: a path to fixtures directory.
+ # * +migrations_paths+: a list of paths to directories with migrations.
+ # * +seed_loader+: an object which will load seeds, it needs to respond to the +load_seed+ method.
+ # * +root+: a path to the root of the application.
#
- # Example usage of +DatabaseTasks+ outside Rails could look as such:
+ # Example usage of DatabaseTasks outside Rails could look as such:
#
# include ActiveRecord::Tasks
# DatabaseTasks.database_configuration = YAML.load_file('my_database_config.yml')
@@ -35,36 +37,62 @@ 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
attr_accessor :database_configuration
- LOCAL_HOSTS = ['127.0.0.1', 'localhost']
+ LOCAL_HOSTS = ["127.0.0.1", "localhost"]
+
+ def check_protected_environments!
+ unless ENV["DISABLE_DATABASE_ENVIRONMENT_CHECK"]
+ current = ActiveRecord::Base.connection.migration_context.current_environment
+ stored = ActiveRecord::Base.connection.migration_context.last_stored_environment
+
+ if ActiveRecord::Base.connection.migration_context.protected_environment?
+ raise ActiveRecord::ProtectedEnvironmentError.new(stored)
+ end
+
+ if stored && stored != current
+ raise ActiveRecord::EnvironmentMismatchError.new(current: current, stored: stored)
+ end
+ end
+ rescue ActiveRecord::NoDatabaseError
+ end
def register_task(pattern, task)
@tasks ||= {}
@tasks[pattern] = task
end
- register_task(/mysql/, ActiveRecord::Tasks::MySQLDatabaseTasks)
- register_task(/postgresql/, ActiveRecord::Tasks::PostgreSQLDatabaseTasks)
- register_task(/sqlite/, ActiveRecord::Tasks::SQLiteDatabaseTasks)
+ register_task(/mysql/, "ActiveRecord::Tasks::MySQLDatabaseTasks")
+ register_task(/postgresql/, "ActiveRecord::Tasks::PostgreSQLDatabaseTasks")
+ register_task(/sqlite/, "ActiveRecord::Tasks::SQLiteDatabaseTasks")
def db_dir
@db_dir ||= Rails.application.config.paths["db"].first
end
def migrations_paths
- @migrations_paths ||= Rails.application.paths['db/migrate'].to_a
+ @migrations_paths ||= Rails.application.paths["db/migrate"].to_a
end
def fixtures_path
- @fixtures_path ||= if ENV['FIXTURES_PATH']
- File.join(root, ENV['FIXTURES_PATH'])
- else
- File.join(root, 'test', 'fixtures')
- end
+ @fixtures_path ||= if ENV["FIXTURES_PATH"]
+ File.join(root, ENV["FIXTURES_PATH"])
+ else
+ File.join(root, "test", "fixtures")
+ end
end
def root
@@ -75,31 +103,54 @@ module ActiveRecord
@env ||= Rails.env
end
+ def spec
+ @spec ||= "primary"
+ end
+
def seed_loader
@seed_loader ||= Rails.application
end
def current_config(options = {})
- options.reverse_merge! :env => env
+ options.reverse_merge! env: env
+ options[:spec] ||= "primary"
if options.has_key?(:config)
@current_config = options[:config]
else
- @current_config ||= ActiveRecord::Base.configurations[options[:env]]
+ @current_config ||= ActiveRecord::Base.configurations.configs_for(env_name: options[:env], spec_name: options[:spec]).config
end
end
def create(*arguments)
configuration = arguments.first
- class_for_adapter(configuration['adapter']).new(*arguments).create
+ class_for_adapter(configuration["adapter"]).new(*arguments).create
+ $stdout.puts "Created database '#{configuration['database']}'" if verbose?
rescue DatabaseAlreadyExists
- $stderr.puts "#{configuration['database']} already exists"
+ $stderr.puts "Database '#{configuration['database']}' already exists" if verbose?
rescue Exception => error
- $stderr.puts error, *(error.backtrace)
- $stderr.puts "Couldn't create database for #{configuration.inspect}"
+ $stderr.puts error
+ $stderr.puts "Couldn't create '#{configuration['database']}' database. Please check your configuration."
+ raise
end
def create_all
+ old_pool = ActiveRecord::Base.connection_handler.retrieve_connection_pool(ActiveRecord::Base.connection_specification_name)
each_local_configuration { |configuration| create configuration }
+ if old_pool
+ ActiveRecord::Base.connection_handler.establish_connection(old_pool.spec.to_hash)
+ end
+ end
+
+ def for_each
+ databases = Rails.application.config.database_configuration
+ database_configs = ActiveRecord::DatabaseConfigurations.new(databases).configs_for(env_name: Rails.env)
+
+ # if this is a single database application we don't want tasks for each primary database
+ return if database_configs.count == 1
+
+ database_configs.each do |db_config|
+ yield db_config.spec_name
+ end
end
def create_current(environment = env)
@@ -111,12 +162,14 @@ module ActiveRecord
def drop(*arguments)
configuration = arguments.first
- class_for_adapter(configuration['adapter']).new(*arguments).drop
+ class_for_adapter(configuration["adapter"]).new(*arguments).drop
+ $stdout.puts "Dropped database '#{configuration['database']}'" if verbose?
rescue ActiveRecord::NoDatabaseError
$stderr.puts "Database '#{configuration['database']}' does not exist"
rescue Exception => error
- $stderr.puts error, *(error.backtrace)
- $stderr.puts "Couldn't drop #{configuration['database']}"
+ $stderr.puts error
+ $stderr.puts "Couldn't drop database '#{configuration['database']}'"
+ raise
end
def drop_all
@@ -130,37 +183,65 @@ module ActiveRecord
end
def migrate
- verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true
- version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil
- scope = ENV['SCOPE']
- verbose_was, Migration.verbose = Migration.verbose, verbose
- Migrator.migrate(Migrator.migrations_paths, version) do |migration|
+ check_target_version
+
+ scope = ENV["SCOPE"]
+ verbose_was, Migration.verbose = Migration.verbose, verbose?
+
+ Base.connection.migration_context.migrate(target_version) do |migration|
scope.blank? || scope == migration.scope
end
+
+ ActiveRecord::Base.clear_cache!
ensure
Migration.verbose = verbose_was
end
- def charset_current(environment = env)
- charset ActiveRecord::Base.configurations[environment]
+ def migrate_status
+ unless ActiveRecord::SchemaMigration.table_exists?
+ Kernel.abort "Schema migrations table does not exist yet."
+ end
+
+ # output
+ puts "\ndatabase: #{ActiveRecord::Base.connection_config[:database]}\n\n"
+ puts "#{'Status'.center(8)} #{'Migration ID'.ljust(14)} Migration Name"
+ puts "-" * 50
+ ActiveRecord::Base.connection.migration_context.migrations_status.each do |status, version, name|
+ puts "#{status.center(8)} #{version.ljust(14)} #{name}"
+ end
+ puts
+ end
+
+ def check_target_version
+ if target_version && !(Migration::MigrationFilenameRegexp.match?(ENV["VERSION"]) || /\A\d+\z/.match?(ENV["VERSION"]))
+ raise "Invalid format of target version: `VERSION=#{ENV['VERSION']}`"
+ end
+ end
+
+ def target_version
+ ENV["VERSION"].to_i if ENV["VERSION"] && !ENV["VERSION"].empty?
+ end
+
+ def charset_current(environment = env, specification_name = spec)
+ charset ActiveRecord::Base.configurations.configs_for(env_name: environment, spec_name: specification_name).config
end
def charset(*arguments)
configuration = arguments.first
- class_for_adapter(configuration['adapter']).new(*arguments).charset
+ class_for_adapter(configuration["adapter"]).new(*arguments).charset
end
- def collation_current(environment = env)
- collation ActiveRecord::Base.configurations[environment]
+ def collation_current(environment = env, specification_name = spec)
+ collation ActiveRecord::Base.configurations.configs_for(env_name: environment, spec_name: specification_name).config
end
def collation(*arguments)
configuration = arguments.first
- class_for_adapter(configuration['adapter']).new(*arguments).collation
+ class_for_adapter(configuration["adapter"]).new(*arguments).collation
end
def purge(configuration)
- class_for_adapter(configuration['adapter']).new(configuration).purge
+ class_for_adapter(configuration["adapter"]).new(configuration).purge
end
def purge_all
@@ -179,65 +260,80 @@ 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:
- file ||= schema_file(format)
+ def load_schema(configuration, format = ActiveRecord::Base.schema_format, file = nil, environment = env, spec_name = "primary") # :nodoc:
+ file ||= dump_filename(spec_name, format)
+
+ verbose_was, Migration.verbose = Migration.verbose, verbose? && ENV["VERBOSE"]
+ check_schema_file(file)
+ ActiveRecord::Base.establish_connection(configuration)
case format
when :ruby
- check_schema_file(file)
- ActiveRecord::Base.establish_connection(configuration)
load(file)
when :sql
- check_schema_file(file)
structure_load(configuration, file)
else
raise ArgumentError, "unknown format #{format.inspect}"
end
+ ActiveRecord::InternalMetadata.create_table
+ ActiveRecord::InternalMetadata[:environment] = environment
+ ensure
+ Migration.verbose = verbose_was
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)
+ def schema_file(format = ActiveRecord::Base.schema_format)
+ File.join(db_dir, schema_file_type(format))
end
- def schema_file(format = ActiveRecord::Base.schema_format)
+ def schema_file_type(format = ActiveRecord::Base.schema_format)
case format
when :ruby
- File.join(db_dir, "schema.rb")
+ "schema.rb"
when :sql
- File.join(db_dir, "structure.sql")
+ "structure.sql"
+ end
+ end
+
+ def dump_filename(namespace, format = ActiveRecord::Base.schema_format)
+ filename = if namespace == "primary"
+ schema_file_type(format)
+ else
+ "#{namespace}_#{schema_file_type(format)}"
end
+
+ ENV["SCHEMA"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, filename)
end
- def load_schema_current_if_exists(format = ActiveRecord::Base.schema_format, file = nil, environment = env)
- if File.exist?(file || schema_file(format))
- load_schema_current(format, file, environment)
+ def cache_dump_filename(namespace)
+ filename = if namespace == "primary"
+ "schema_cache.yml"
+ else
+ "#{namespace}_schema_cache.yml"
end
+
+ ENV["SCHEMA_CACHE"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, filename)
end
def load_schema_current(format = ActiveRecord::Base.schema_format, file = nil, environment = env)
- each_current_configuration(environment) { |configuration|
- load_schema configuration, format, file
+ each_current_configuration(environment) { |configuration, spec_name, env|
+ load_schema(configuration, format, file, env, spec_name)
}
ActiveRecord::Base.establish_connection(environment.to_sym)
end
def check_schema_file(filename)
unless File.exist?(filename)
- message = %{#{filename} doesn't exist yet. Run `rake db:migrate` to create it, then try again.}
- message << %{ If you do not intend to use a database, you should instead alter #{Rails.root}/config/application.rb to limit the frameworks that will be loaded.} if defined?(::Rails)
+ message = +%{#{filename} doesn't exist yet. Run `rails db:migrate` to create it, then try again.}
+ message << %{ If you do not intend to use a database, you should instead alter #{Rails.root}/config/application.rb to limit the frameworks that will be loaded.} if defined?(::Rails.root)
Kernel.abort message
end
end
@@ -246,48 +342,62 @@ 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 verbose?
+ ENV["VERBOSE"] ? ENV["VERBOSE"] != "false" : true
+ end
- def class_for_adapter(adapter)
- key = @tasks.keys.detect { |pattern| adapter[pattern] }
- unless key
- raise DatabaseNotSupported, "Rake tasks not supported by '#{adapter}' adapter"
+ def class_for_adapter(adapter)
+ _key, task = @tasks.each_pair.detect { |pattern, _task| adapter[pattern] }
+ unless task
+ raise DatabaseNotSupported, "Rake tasks not supported by '#{adapter}' adapter"
+ end
+ task.is_a?(String) ? task.constantize : task
end
- @tasks[key]
- end
- def each_current_configuration(environment)
- environments = [environment]
- # add test environment only if no RAILS_ENV was specified.
- environments << 'test' if environment == 'development' && ENV['RAILS_ENV'].nil?
+ def each_current_configuration(environment)
+ environments = [environment]
+ environments << "test" if environment == "development"
- configurations = ActiveRecord::Base.configurations.values_at(*environments)
- configurations.compact.each do |configuration|
- yield configuration unless configuration['database'].blank?
+ environments.each do |env|
+ ActiveRecord::Base.configurations.configs_for(env_name: env).each do |db_config|
+ yield db_config.config, db_config.spec_name, env
+ end
+ end
end
- end
- def each_local_configuration
- ActiveRecord::Base.configurations.each_value do |configuration|
- next unless configuration['database']
+ def each_local_configuration
+ ActiveRecord::Base.configurations.configs_for.each do |db_config|
+ configuration = db_config.config
+ next unless configuration["database"]
- if local_database?(configuration)
- yield configuration
- else
- $stderr.puts "This task only modifies local databases. #{configuration['database']} is on a remote host."
+ if local_database?(configuration)
+ yield configuration
+ else
+ $stderr.puts "This task only modifies local databases. #{configuration['database']} is on a remote host."
+ end
end
end
- end
- def local_database?(configuration)
- configuration['host'].blank? || LOCAL_HOSTS.include?(configuration['host'])
- end
+ def local_database?(configuration)
+ configuration["host"].blank? || LOCAL_HOSTS.include?(configuration["host"])
+ end
end
end
end
diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
index 673386f0d9..1c1b29b5e1 100644
--- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
@@ -1,10 +1,8 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Tasks # :nodoc:
class MySQLDatabaseTasks # :nodoc:
- DEFAULT_CHARSET = ENV['CHARSET'] || 'utf8'
- DEFAULT_COLLATION = ENV['COLLATION'] || 'utf8_unicode_ci'
- ACCESS_DENIED_ERROR = 1045
-
delegate :connection, :establish_connection, to: ActiveRecord::Base
def initialize(configuration)
@@ -13,38 +11,24 @@ module ActiveRecord
def create
establish_connection configuration_without_database
- connection.create_database configuration['database'], creation_options
+ connection.create_database configuration["database"], creation_options
establish_connection configuration
rescue ActiveRecord::StatementInvalid => error
- if /database exists/ === error.message
+ if error.message.include?("database exists")
raise DatabaseAlreadyExists
else
raise
end
- rescue error_class => error
- if error.respond_to?(:errno) && error.errno == ACCESS_DENIED_ERROR
- $stdout.print error.error
- establish_connection root_configuration_without_database
- connection.create_database configuration['database'], creation_options
- if configuration['username'] != 'root'
- connection.execute grant_statement.gsub(/\s+/, ' ').strip
- end
- establish_connection configuration
- else
- $stderr.puts error.inspect
- $stderr.puts "Couldn't create database for #{configuration.inspect}, #{creation_options.inspect}"
- $stderr.puts "(If you set the charset manually, make sure you have a matching collation)" if configuration['encoding']
- end
end
def drop
establish_connection configuration
- connection.drop_database configuration['database']
+ connection.drop_database configuration["database"]
end
def purge
establish_connection configuration
- connection.recreate_database configuration['database'], creation_options
+ connection.recreate_database configuration["database"], creation_options
end
def charset
@@ -55,98 +39,75 @@ module ActiveRecord
connection.collation
end
- def structure_dump(filename)
- args = prepare_command_options('mysqldump')
+ def structure_dump(filename, extra_flags)
+ args = prepare_command_options
args.concat(["--result-file", "#{filename}"])
args.concat(["--no-data"])
args.concat(["--routines"])
- args.concat(["#{configuration['database']}"])
- unless Kernel.system(*args)
- $stderr.puts "Could not dump the database structure. "\
- "Make sure `mysqldump` is in your PATH and check the command output for warnings."
- end
- end
+ args.concat(["--skip-comments"])
- def structure_load(filename)
- args = prepare_command_options('mysql')
- args.concat(['--execute', %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}])
- args.concat(["--database", "#{configuration['database']}"])
- Kernel.system(*args)
- end
+ ignore_tables = ActiveRecord::SchemaDumper.ignore_tables
+ if ignore_tables.any?
+ args += ignore_tables.map { |table| "--ignore-table=#{configuration['database']}.#{table}" }
+ end
- private
+ args.concat(["#{configuration['database']}"])
+ args.unshift(*extra_flags) if extra_flags
- def configuration
- @configuration
+ run_cmd("mysqldump", args, "dumping")
end
- def configuration_without_database
- configuration.merge('database' => nil)
+ 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.unshift(*extra_flags) if extra_flags
+
+ run_cmd("mysql", args, "loading")
end
- def creation_options
- Hash.new.tap do |options|
- options[:charset] = configuration['encoding'] if configuration.include? 'encoding'
- options[:collation] = configuration['collation'] if configuration.include? 'collation'
+ private
- # Set default charset only when collation isn't set.
- options[:charset] ||= DEFAULT_CHARSET unless options[:collation]
+ attr_reader :configuration
- # Set default collation only when charset is also default.
- options[:collation] ||= DEFAULT_COLLATION if options[:charset] == DEFAULT_CHARSET
+ def configuration_without_database
+ configuration.merge("database" => nil)
end
- end
- def error_class
- if configuration['adapter'] =~ /jdbc/
- require 'active_record/railties/jdbcmysql_error'
- ArJdbcMySQL::Error
- elsif defined?(Mysql2)
- Mysql2::Error
- elsif defined?(Mysql)
- Mysql::Error
- else
- StandardError
+ def creation_options
+ Hash.new.tap do |options|
+ options[:charset] = configuration["encoding"] if configuration.include? "encoding"
+ options[:collation] = configuration["collation"] if configuration.include? "collation"
+ end
end
- end
-
- def grant_statement
- <<-SQL
-GRANT ALL PRIVILEGES ON #{configuration['database']}.*
- TO '#{configuration['username']}'@'localhost'
-IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION;
- SQL
- end
- def root_configuration_without_database
- configuration_without_database.merge(
- 'username' => 'root',
- 'password' => root_password
- )
- end
+ def prepare_command_options
+ args = {
+ "host" => "--host",
+ "port" => "--port",
+ "socket" => "--socket",
+ "username" => "--user",
+ "password" => "--password",
+ "encoding" => "--default-character-set",
+ "sslca" => "--ssl-ca",
+ "sslcert" => "--ssl-cert",
+ "sslcapath" => "--ssl-capath",
+ "sslcipher" => "--ssl-cipher",
+ "sslkey" => "--ssl-key"
+ }.map { |opt, arg| "#{arg}=#{configuration[opt]}" if configuration[opt] }.compact
+
+ args
+ end
- def root_password
- $stdout.print "Please provide the root password for your MySQL installation\n>"
- $stdin.gets.strip
- end
+ def run_cmd(cmd, args, action)
+ fail run_cmd_error(cmd, args, action) unless Kernel.system(cmd, *args)
+ end
- def prepare_command_options(command)
- args = {
- 'host' => '--host',
- 'port' => '--port',
- 'socket' => '--socket',
- 'username' => '--user',
- 'password' => '--password',
- 'encoding' => '--default-character-set',
- 'sslca' => '--ssl-ca',
- 'sslcert' => '--ssl-cert',
- 'sslcapath' => '--ssl-capath',
- 'sslcipher' => '--ssh-cipher',
- 'sslkey' => '--ssl-key'
- }.map { |opt, arg| "#{arg}=#{configuration[opt]}" if configuration[opt] }.compact
-
- [command, *args]
- end
+ def run_cmd_error(cmd, args, action)
+ msg = +"failed to execute: `#{cmd}`\n"
+ 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
end
end
end
diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
index d7da95c8a9..8acb11f75f 100644
--- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
@@ -1,9 +1,13 @@
-require 'shellwords'
+# frozen_string_literal: true
+
+require "tempfile"
module ActiveRecord
module Tasks # :nodoc:
class PostgreSQLDatabaseTasks # :nodoc:
- DEFAULT_ENCODING = ENV['CHARSET'] || 'utf8'
+ DEFAULT_ENCODING = ENV["CHARSET"] || "utf8"
+ ON_ERROR_STOP_1 = "ON_ERROR_STOP=1"
+ SQL_COMMENT_BEGIN = "--"
delegate :connection, :establish_connection, :clear_active_connections!,
to: ActiveRecord::Base
@@ -14,11 +18,11 @@ module ActiveRecord
def create(master_established = false)
establish_master_connection unless master_established
- connection.create_database configuration['database'],
- configuration.merge('encoding' => encoding)
+ connection.create_database configuration["database"],
+ configuration.merge("encoding" => encoding)
establish_connection configuration
rescue ActiveRecord::StatementInvalid => error
- if /database .* already exists/ === error.message
+ if error.cause.is_a?(PG::DuplicateDatabase)
raise DatabaseAlreadyExists
else
raise
@@ -27,7 +31,7 @@ module ActiveRecord
def drop
establish_master_connection
- connection.drop_database configuration['database']
+ connection.drop_database configuration["database"]
end
def charset
@@ -44,55 +48,94 @@ module ActiveRecord
create true
end
- def structure_dump(filename)
+ def structure_dump(filename, extra_flags)
set_psql_env
- search_path = case ActiveRecord::Base.dump_schemas
- when :schema_search_path
- configuration['schema_search_path']
- when :all
- nil
- when String
- ActiveRecord::Base.dump_schemas
- end
+ search_path = \
+ case ActiveRecord::Base.dump_schemas
+ when :schema_search_path
+ configuration["schema_search_path"]
+ when :all
+ nil
+ when String
+ ActiveRecord::Base.dump_schemas
+ end
+
+ args = ["-s", "-x", "-O", "-f", filename]
+ args.concat(Array(extra_flags)) if extra_flags
unless search_path.blank?
- search_path = search_path.split(",").map{|search_path_part| "--schema=#{Shellwords.escape(search_path_part.strip)}" }.join(" ")
+ args += search_path.split(",").map do |part|
+ "--schema=#{part.strip}"
+ end
end
- command = "pg_dump -i -s -x -O -f #{Shellwords.escape(filename)} #{search_path} #{Shellwords.escape(configuration['database'])}"
- raise 'Error dumping database' unless Kernel.system(command)
+ ignore_tables = ActiveRecord::SchemaDumper.ignore_tables
+ if ignore_tables.any?
+ args += ignore_tables.flat_map { |table| ["-T", table] }
+ 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
- Kernel.system("psql -X -q -f #{Shellwords.escape(filename)} #{configuration['database']}")
+ args = ["-v", ON_ERROR_STOP_1, "-q", "-X", "-f", filename]
+ args.concat(Array(extra_flags)) if extra_flags
+ args << configuration["database"]
+ run_cmd("psql", args, "loading")
end
private
- def configuration
- @configuration
- end
+ attr_reader :configuration
- def encoding
- configuration['encoding'] || DEFAULT_ENCODING
- end
+ def encoding
+ configuration["encoding"] || DEFAULT_ENCODING
+ end
- def establish_master_connection
- establish_connection configuration.merge(
- 'database' => 'postgres',
- 'schema_search_path' => 'public'
- )
- end
+ def establish_master_connection
+ establish_connection configuration.merge(
+ "database" => "postgres",
+ "schema_search_path" => "public"
+ )
+ end
- def set_psql_env
- ENV['PGHOST'] = configuration['host'] if configuration['host']
- ENV['PGPORT'] = configuration['port'].to_s if configuration['port']
- ENV['PGPASSWORD'] = configuration['password'].to_s if configuration['password']
- ENV['PGUSER'] = configuration['username'].to_s if configuration['username']
- end
+ def set_psql_env
+ ENV["PGHOST"] = configuration["host"] if configuration["host"]
+ ENV["PGPORT"] = configuration["port"].to_s if configuration["port"]
+ ENV["PGPASSWORD"] = configuration["password"].to_s if configuration["password"]
+ ENV["PGUSER"] = configuration["username"].to_s if configuration["username"]
+ end
+
+ def run_cmd(cmd, args, action)
+ fail run_cmd_error(cmd, args, action) unless Kernel.system(cmd, *args)
+ end
+
+ def run_cmd_error(cmd, args, action)
+ msg = +"failed to execute:\n"
+ msg << "#{cmd} #{args.join(' ')}\n\n"
+ 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.cp(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 9ab64d0325..a82cea80ca 100644
--- a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Tasks # :nodoc:
class SQLiteDatabaseTasks # :nodoc:
@@ -8,22 +10,26 @@ module ActiveRecord
end
def create
- raise DatabaseAlreadyExists if File.exist?(configuration['database'])
+ raise DatabaseAlreadyExists if File.exist?(configuration["database"])
establish_connection configuration
connection
end
def drop
- require 'pathname'
- path = Pathname.new configuration['database']
+ require "pathname"
+ path = Pathname.new configuration["database"]
file = path.absolute? ? path.to_s : File.join(root, path)
- FileUtils.rm(file) if File.exist?(file)
+ FileUtils.rm(file)
+ rescue Errno::ENOENT => error
+ raise NoDatabaseError.new(error.message)
end
def purge
drop
+ rescue NoDatabaseError
+ ensure
create
end
@@ -31,25 +37,41 @@ module ActiveRecord
connection.encoding
end
- def structure_dump(filename)
- dbfile = configuration['database']
- `sqlite3 #{dbfile} .schema > #{filename}`
+ def structure_dump(filename, extra_flags)
+ args = []
+ args.concat(Array(extra_flags)) if extra_flags
+ args << configuration["database"]
+
+ ignore_tables = ActiveRecord::SchemaDumper.ignore_tables
+ if ignore_tables.any?
+ condition = ignore_tables.map { |table| connection.quote(table) }.join(", ")
+ args << "SELECT sql FROM sqlite_master WHERE tbl_name NOT IN (#{condition}) ORDER BY tbl_name, type DESC, name"
+ else
+ args << ".schema"
+ end
+ run_cmd("sqlite3", args, filename)
end
- def structure_load(filename)
- dbfile = configuration['database']
- `sqlite3 #{dbfile} < "#{filename}"`
+ def structure_load(filename, extra_flags)
+ dbfile = configuration["database"]
+ flags = extra_flags.join(" ") if extra_flags
+ `sqlite3 #{flags} #{dbfile} < "#{filename}"`
end
private
- def configuration
- @configuration
- end
+ attr_reader :configuration, :root
- def root
- @root
- end
+ def run_cmd(cmd, args, out)
+ fail run_cmd_error(cmd, args) unless Kernel.system(cmd, *args, out: out)
+ end
+
+ def run_cmd_error(cmd, args)
+ msg = +"failed to execute:\n"
+ msg << "#{cmd} #{args.join(' ')}\n\n"
+ 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
end
end
end
diff --git a/activerecord/lib/active_record/test_databases.rb b/activerecord/lib/active_record/test_databases.rb
new file mode 100644
index 0000000000..999830ba79
--- /dev/null
+++ b/activerecord/lib/active_record/test_databases.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require "active_support/testing/parallelization"
+
+module ActiveRecord
+ module TestDatabases # :nodoc:
+ ActiveSupport::Testing::Parallelization.after_fork_hook do |i|
+ create_and_load_schema(i, env_name: Rails.env)
+ end
+
+ ActiveSupport::Testing::Parallelization.run_cleanup_hook do
+ drop(env_name: Rails.env)
+ end
+
+ def self.create_and_load_schema(i, env_name:)
+ old, ENV["VERBOSE"] = ENV["VERBOSE"], "false"
+
+ ActiveRecord::Base.configurations.configs_for(env_name: env_name).each do |db_config|
+ db_config.config["database"] += "-#{i}"
+ ActiveRecord::Tasks::DatabaseTasks.create(db_config.config)
+ ActiveRecord::Tasks::DatabaseTasks.load_schema(db_config.config, ActiveRecord::Base.schema_format, nil, env_name, db_config.spec_name)
+ end
+ ensure
+ ActiveRecord::Base.establish_connection(Rails.env.to_sym)
+ ENV["VERBOSE"] = old
+ end
+
+ def self.drop(env_name:)
+ old, ENV["VERBOSE"] = ENV["VERBOSE"], "false"
+
+ ActiveRecord::Base.configurations.configs_for(env_name: env_name).each do |db_config|
+ ActiveRecord::Tasks::DatabaseTasks.drop(db_config.config)
+ end
+ ensure
+ ENV["VERBOSE"] = old
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/test_fixtures.rb b/activerecord/lib/active_record/test_fixtures.rb
new file mode 100644
index 0000000000..7b7b3f7112
--- /dev/null
+++ b/activerecord/lib/active_record/test_fixtures.rb
@@ -0,0 +1,201 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module TestFixtures
+ extend ActiveSupport::Concern
+
+ def before_setup # :nodoc:
+ setup_fixtures
+ super
+ end
+
+ def after_teardown # :nodoc:
+ super
+ teardown_fixtures
+ end
+
+ included do
+ class_attribute :fixture_path, instance_writer: false
+ class_attribute :fixture_table_names, default: []
+ class_attribute :fixture_class_names, default: {}
+ class_attribute :use_transactional_tests, default: true
+ class_attribute :use_instantiated_fixtures, default: false # true, false, or :no_instances
+ class_attribute :pre_loaded_fixtures, default: false
+ class_attribute :config, default: ActiveRecord::Base
+ class_attribute :lock_threads, default: true
+ end
+
+ module ClassMethods
+ # Sets the model class for a fixture when the class name cannot be inferred from the fixture name.
+ #
+ # Examples:
+ #
+ # set_fixture_class some_fixture: SomeModel,
+ # 'namespaced/fixture' => Another::Model
+ #
+ # 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 = fixture_class_names.merge(class_names.stringify_keys)
+ end
+
+ def fixtures(*fixture_set_names)
+ if fixture_set_names.first == :all
+ raise StandardError, "No fixture path found. Please set `#{self}.fixture_path`." if fixture_path.blank?
+ fixture_set_names = Dir["#{fixture_path}/{**,*}/*.{yml}"].uniq
+ fixture_set_names.map! { |f| f[(fixture_path.to_s.size + 1)..-5] }
+ else
+ fixture_set_names = fixture_set_names.flatten.map(&:to_s)
+ end
+
+ self.fixture_table_names |= fixture_set_names
+ setup_fixture_accessors(fixture_set_names)
+ end
+
+ def setup_fixture_accessors(fixture_set_names = nil)
+ fixture_set_names = Array(fixture_set_names || fixture_table_names)
+ methods = Module.new do
+ fixture_set_names.each do |fs_name|
+ fs_name = fs_name.to_s
+ accessor_name = fs_name.tr("/", "_").to_sym
+
+ define_method(accessor_name) do |*fixture_names|
+ force_reload = fixture_names.pop if fixture_names.last == true || fixture_names.last == :reload
+ return_single_record = fixture_names.size == 1
+ fixture_names = @loaded_fixtures[fs_name].fixtures.keys if fixture_names.empty?
+
+ @fixture_cache[fs_name] ||= {}
+
+ instances = fixture_names.map do |f_name|
+ f_name = f_name.to_s if f_name.is_a?(Symbol)
+ @fixture_cache[fs_name].delete(f_name) if force_reload
+
+ if @loaded_fixtures[fs_name][f_name]
+ @fixture_cache[fs_name][f_name] ||= @loaded_fixtures[fs_name][f_name].find
+ else
+ raise StandardError, "No fixture named '#{f_name}' found for fixture set '#{fs_name}'"
+ end
+ end
+
+ return_single_record ? instances.first : instances
+ end
+ private accessor_name
+ end
+ end
+ include methods
+ end
+
+ def uses_transaction(*methods)
+ @uses_transaction = [] unless defined?(@uses_transaction)
+ @uses_transaction.concat methods.map(&:to_s)
+ end
+
+ def uses_transaction?(method)
+ @uses_transaction = [] unless defined?(@uses_transaction)
+ @uses_transaction.include?(method.to_s)
+ end
+ end
+
+ def run_in_transaction?
+ use_transactional_tests &&
+ !self.class.uses_transaction?(method_name)
+ end
+
+ def setup_fixtures(config = ActiveRecord::Base)
+ if pre_loaded_fixtures && !use_transactional_tests
+ raise RuntimeError, "pre_loaded_fixtures requires use_transactional_tests"
+ end
+
+ @fixture_cache = {}
+ @fixture_connections = []
+ @@already_loaded_fixtures ||= {}
+ @connection_subscriber = nil
+
+ # Load fixtures once and begin transaction.
+ if run_in_transaction?
+ if @@already_loaded_fixtures[self.class]
+ @loaded_fixtures = @@already_loaded_fixtures[self.class]
+ else
+ @loaded_fixtures = load_fixtures(config)
+ @@already_loaded_fixtures[self.class] = @loaded_fixtures
+ end
+
+ # Begin transactions for connections already established
+ @fixture_connections = enlist_fixture_connections
+ @fixture_connections.each do |connection|
+ connection.begin_transaction joinable: false
+ connection.pool.lock_thread = true if lock_threads
+ end
+
+ # When connections are established in the future, begin a transaction too
+ @connection_subscriber = ActiveSupport::Notifications.subscribe("!connection.active_record") do |_, _, _, _, payload|
+ spec_name = payload[:spec_name] if payload.key?(:spec_name)
+
+ if spec_name
+ begin
+ connection = ActiveRecord::Base.connection_handler.retrieve_connection(spec_name)
+ rescue ConnectionNotEstablished
+ connection = nil
+ end
+
+ if connection && !@fixture_connections.include?(connection)
+ connection.begin_transaction joinable: false
+ connection.pool.lock_thread = true if lock_threads
+ @fixture_connections << connection
+ end
+ end
+ end
+
+ # Load fixtures for every test.
+ else
+ ActiveRecord::FixtureSet.reset_cache
+ @@already_loaded_fixtures[self.class] = nil
+ @loaded_fixtures = load_fixtures(config)
+ end
+
+ # Instantiate fixtures for every test if requested.
+ instantiate_fixtures if use_instantiated_fixtures
+ end
+
+ def teardown_fixtures
+ # Rollback changes if a transaction is active.
+ if run_in_transaction?
+ 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
+ ActiveRecord::FixtureSet.reset_cache
+ end
+
+ ActiveRecord::Base.clear_active_connections!
+ end
+
+ def enlist_fixture_connections
+ ActiveRecord::Base.connection_handler.connection_pool_list.map(&:connection)
+ end
+
+ private
+ def load_fixtures(config)
+ fixtures = ActiveRecord::FixtureSet.create_fixtures(fixture_path, fixture_table_names, fixture_class_names, config)
+ Hash[fixtures.map { |f| [f.name, f] }]
+ end
+
+ def instantiate_fixtures
+ if pre_loaded_fixtures
+ raise RuntimeError, "Load fixtures before instantiating them." if ActiveRecord::FixtureSet.all_loaded_fixtures.empty?
+ ActiveRecord::FixtureSet.instantiate_all_loaded_fixtures(self, load_instances?)
+ else
+ raise RuntimeError, "Load fixtures before instantiating them." if @loaded_fixtures.nil?
+ @loaded_fixtures.each_value do |fixture_set|
+ ActiveRecord::FixtureSet.instantiate_fixtures(self, fixture_set, load_instances?)
+ end
+ end
+ end
+
+ def load_instances?
+ use_instantiated_fixtures != :no_instances
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb
index 20e4235788..d32f971ad1 100644
--- a/activerecord/lib/active_record/timestamp.rb
+++ b/activerecord/lib/active_record/timestamp.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
module ActiveRecord
- # = Active Record Timestamp
+ # = Active Record \Timestamp
#
# Active Record automatically timestamps create and update operations if the
# table has fields named <tt>created_at/created_on</tt> or
@@ -15,14 +17,25 @@ module ActiveRecord
#
# == Time Zone aware attributes
#
- # By default, ActiveRecord::Base keeps all the datetime columns time zone aware by executing following code.
+ # Active Record keeps all the <tt>datetime</tt> and <tt>time</tt> columns
+ # timezone aware. By default, these values are stored in the database as UTC
+ # and converted back to the current <tt>Time.zone</tt> when pulled from the database.
+ #
+ # This feature can be turned off completely by setting:
+ #
+ # config.active_record.time_zone_aware_attributes = false
+ #
+ # You can also specify that only <tt>datetime</tt> columns should be time-zone
+ # aware (while <tt>time</tt> should not) by setting:
#
- # config.active_record.time_zone_aware_attributes = true
+ # ActiveRecord::Base.time_zone_aware_types = [:datetime]
#
- # This feature can easily be turned off by assigning value <tt>false</tt> .
+ # You can also add database specific timezone aware types. For example, for PostgreSQL:
#
- # If your attributes are time zone aware and you desire to skip time zone conversion to the current Time.zone
- # when reading certain attributes then you can do following:
+ # ActiveRecord::Base.time_zone_aware_types += [:tsrange, :tstzrange]
+ #
+ # Finally, you can indicate specific attributes of a model for which time zone
+ # conversion should not applied, for instance by setting:
#
# class Topic < ActiveRecord::Base
# self.skip_time_zone_conversion_for_attributes = [:written_on]
@@ -31,8 +44,7 @@ module ActiveRecord
extend ActiveSupport::Concern
included do
- class_attribute :record_timestamps
- self.record_timestamps = true
+ class_attribute :record_timestamps, default: true
end
def initialize_dup(other) # :nodoc:
@@ -40,16 +52,48 @@ module ActiveRecord
clear_timestamp_attributes
end
+ module ClassMethods # :nodoc:
+ def touch_attributes_with_time(*names, time: nil)
+ attribute_names = timestamp_attributes_for_update_in_model
+ attribute_names |= names.map(&:to_s)
+ attribute_names.index_with(time ||= current_time_from_proper_timezone)
+ end
+
+ private
+ def timestamp_attributes_for_create_in_model
+ timestamp_attributes_for_create.select { |c| column_names.include?(c) }
+ end
+
+ def timestamp_attributes_for_update_in_model
+ timestamp_attributes_for_update.select { |c| column_names.include?(c) }
+ end
+
+ def all_timestamp_attributes_in_model
+ timestamp_attributes_for_create_in_model + timestamp_attributes_for_update_in_model
+ end
+
+ 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 self.record_timestamps
+ 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)
- write_attribute(column, current_time)
+ all_timestamp_attributes_in_model.each do |column|
+ if !attribute_present?(column)
+ _write_attribute(column, current_time)
end
end
end
@@ -62,43 +106,34 @@ module ActiveRecord
current_time = current_time_from_proper_timezone
timestamp_attributes_for_update_in_model.each do |column|
- column = column.to_s
- next if attribute_changed?(column)
- write_attribute(column, current_time)
+ next if will_save_change_to_attribute?(column)
+ _write_attribute(column, current_time)
end
end
super(*args)
end
def should_record_timestamps?
- self.record_timestamps && (!partial_writes? || changed?)
+ record_timestamps && (!partial_writes? || has_changes_to_save?)
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 = timestamp_attributes_for_update_in_model)
timestamp_names
.map { |attr| self[attr] }
.compact
@@ -106,10 +141,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/touch_later.rb b/activerecord/lib/active_record/touch_later.rb
index 4352a0ffea..f70b7c50a2 100644
--- a/activerecord/lib/active_record/touch_later.rb
+++ b/activerecord/lib/active_record/touch_later.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
# = Active Record Touch Later
module TouchLater
@@ -8,7 +10,12 @@ module ActiveRecord
end
def touch_later(*names) # :nodoc:
- raise ActiveRecordError, "cannot touch on a new record object" unless persisted?
+ unless persisted?
+ raise ActiveRecordError, <<-MSG.squish
+ cannot touch on a new or destroyed record object. Consider using
+ persisted?, new_record?, or destroyed? before touching
+ MSG
+ end
@_defer_touch_attrs ||= timestamp_attributes_for_update_in_model
@_defer_touch_attrs |= names
@@ -16,6 +23,13 @@ module ActiveRecord
surreptitiously_touch @_defer_touch_attrs
self.class.connection.add_transaction_record self
+
+ # touch the parents as we are not calling the after_save callbacks
+ self.class.reflect_on_all_associations(:belongs_to).each do |r|
+ if touch = r.options[:touch]
+ ActiveRecord::Associations::Builder::BelongsTo.touch_record(self, changes_to_save, r.foreign_key, r.name, touch, :touch_later)
+ end
+ end
end
def touch(*names, time: nil) # :nodoc:
@@ -26,6 +40,7 @@ module ActiveRecord
end
private
+
def surreptitiously_touch(attrs)
attrs.each { |attr| write_attribute attr, @_touch_time }
clear_attribute_changes attrs
@@ -33,9 +48,8 @@ module ActiveRecord
def touch_deferred_attributes
if has_defer_touch_attrs? && persisted?
- @_touching_delayed_records = true
touch(*@_defer_touch_attrs, time: @_touch_time)
- @_touching_delayed_records, @_defer_touch_attrs, @_touch_time = nil, nil, nil
+ @_defer_touch_attrs, @_touch_time = nil, nil
end
end
@@ -43,8 +57,8 @@ module ActiveRecord
defined?(@_defer_touch_attrs) && @_defer_touch_attrs.present?
end
- def touching_delayed_records?
- defined?(@_touching_delayed_records) && @_touching_delayed_records
+ def belongs_to_touch_method
+ :touch_later
end
end
end
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index 6f2def0df1..fe3842b905 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
# See ActiveRecord::Transactions::ClassMethods for documentation.
module Transactions
@@ -16,10 +18,10 @@ module ActiveRecord
# = Active Record Transactions
#
- # Transactions are protective blocks where SQL statements are only permanent
+ # \Transactions are protective blocks where SQL statements are only permanent
# if they can all succeed as one atomic action. The classic example is a
# transfer between two accounts where you can only have a deposit if the
- # withdrawal succeeded and vice versa. Transactions enforce the integrity of
+ # withdrawal succeeded and vice versa. \Transactions enforce the integrity of
# the database and guard the data against program errors or database
# break-downs. So basically you should use transaction blocks whenever you
# have a number of statements that must be executed together or not at all.
@@ -39,20 +41,20 @@ module ActiveRecord
#
# == Different Active Record classes in a single transaction
#
- # Though the transaction class method is called on some Active Record class,
+ # Though the #transaction class method is called on some Active Record class,
# the objects within the transaction block need not all be instances of
# that class. This is because transactions are per-database connection, not
# per-model.
#
# In this example a +balance+ record is transactionally saved even
- # though +transaction+ is called on the +Account+ class:
+ # though #transaction is called on the +Account+ class:
#
# Account.transaction do
# balance.save!
# account.save!
# end
#
- # The +transaction+ method is also available as a model instance method.
+ # The #transaction method is also available as a model instance method.
# For example, you can also do this:
#
# balance.transaction do
@@ -79,7 +81,8 @@ module ActiveRecord
#
# == +save+ and +destroy+ are automatically wrapped in a transaction
#
- # Both +save+ and +destroy+ come wrapped in a transaction that ensures
+ # Both {#save}[rdoc-ref:Persistence#save] and
+ # {#destroy}[rdoc-ref:Persistence#destroy] come wrapped in a transaction that ensures
# that whatever you do in validations or callbacks will happen under its
# protected cover. So you can use validations to check for values that
# the transaction depends on or you can raise exceptions in the callbacks
@@ -88,7 +91,7 @@ module ActiveRecord
# As a consequence changes to the database are not seen outside your connection
# until the operation is complete. For example, if you try to update the index
# of a search engine in +after_save+ the indexer won't see the updated record.
- # The +after_commit+ callback is the only one that is triggered once the update
+ # The #after_commit callback is the only one that is triggered once the update
# is committed. See below.
#
# == Exception handling and rolling back
@@ -97,11 +100,11 @@ module ActiveRecord
# be propagated (after triggering the ROLLBACK), so you should be ready to
# catch those in your application code.
#
- # One exception is the <tt>ActiveRecord::Rollback</tt> exception, which will trigger
+ # One exception is the ActiveRecord::Rollback exception, which will trigger
# a ROLLBACK when raised, but not be re-raised by the transaction block.
#
- # *Warning*: one should not catch <tt>ActiveRecord::StatementInvalid</tt> exceptions
- # inside a transaction block. <tt>ActiveRecord::StatementInvalid</tt> exceptions indicate that an
+ # *Warning*: one should not catch ActiveRecord::StatementInvalid exceptions
+ # inside a transaction block. ActiveRecord::StatementInvalid exceptions indicate that an
# error occurred at the database level, for example when a unique constraint
# is violated. On some database systems, such as PostgreSQL, database errors
# inside a transaction cause the entire transaction to become unusable
@@ -122,16 +125,16 @@ 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
#
# One should restart the entire transaction if an
- # <tt>ActiveRecord::StatementInvalid</tt> occurred.
+ # ActiveRecord::StatementInvalid occurred.
#
# == Nested transactions
#
- # +transaction+ calls can be nested. By default, this makes all database
+ # #transaction calls can be nested. By default, this makes all database
# statements in the nested transaction block become part of the parent
# transaction. For example, the following behavior may be surprising:
#
@@ -143,7 +146,7 @@ module ActiveRecord
# end
# end
#
- # creates both "Kotori" and "Nemu". Reason is the <tt>ActiveRecord::Rollback</tt>
+ # creates both "Kotori" and "Nemu". Reason is the ActiveRecord::Rollback
# exception in the nested block does not issue a ROLLBACK. Since these exceptions
# are captured in transaction blocks, the parent block does not see it and the
# real transaction is committed.
@@ -167,28 +170,28 @@ module ActiveRecord
# writing, the only database that we're aware of that supports true nested
# transactions, is MS-SQL. Because of this, Active Record emulates nested
# transactions by using savepoints on MySQL and PostgreSQL. See
- # http://dev.mysql.com/doc/refman/5.6/en/savepoint.html
+ # https://dev.mysql.com/doc/refman/5.7/en/savepoint.html
# for more information about savepoints.
#
- # === Callbacks
+ # === \Callbacks
#
# There are two types of callbacks associated with committing and rolling back transactions:
- # +after_commit+ and +after_rollback+.
+ # #after_commit and #after_rollback.
#
- # +after_commit+ callbacks are called on every record saved or destroyed within a
- # transaction immediately after the transaction is committed. +after_rollback+ callbacks
+ # #after_commit callbacks are called on every record saved or destroyed within a
+ # transaction immediately after the transaction is committed. #after_rollback callbacks
# are called on every record saved or destroyed within a transaction immediately after the
# transaction or savepoint is rolled back.
#
# These callbacks are useful for interacting with other systems since you will be guaranteed
# that the callback is only executed when the database is in a permanent state. For example,
- # +after_commit+ is a good spot to put in a hook to clearing a cache since clearing it from
+ # #after_commit is a good spot to put in a hook to clearing a cache since clearing it from
# within a transaction could trigger the cache to be regenerated before the database is updated.
#
# === Caveats
#
- # If you're on MySQL, then do not use DDL operations in nested transactions
- # blocks that are emulated with savepoints. That is, do not execute statements
+ # If you're on MySQL, then do not use Data Definition Language (DDL) operations in nested
+ # transactions blocks that are emulated with savepoints. That is, do not execute statements
# like 'CREATE TABLE' inside such blocks. This is because MySQL automatically
# releases all savepoints upon executing a DDL operation. When +transaction+
# is finished and tries to release the savepoint it created earlier, a
@@ -204,9 +207,8 @@ module ActiveRecord
#
# Note that "TRUNCATE" is also a MySQL DDL statement!
module ClassMethods
- # See ActiveRecord::Transactions::ClassMethods for detailed documentation.
+ # See the ConnectionAdapters::DatabaseStatements#transaction API docs.
def transaction(options = {}, &block)
- # See the ConnectionAdapters::DatabaseStatements#transaction API docs.
connection.transaction(options, &block)
end
@@ -232,9 +234,27 @@ module ActiveRecord
set_callback(:commit, :after, *args, &block)
end
+ # Shortcut for <tt>after_commit :hook, on: :create</tt>.
+ def after_create_commit(*args, &block)
+ set_options_for_callbacks!(args, on: :create)
+ set_callback(:commit, :after, *args, &block)
+ end
+
+ # Shortcut for <tt>after_commit :hook, on: :update</tt>.
+ def after_update_commit(*args, &block)
+ set_options_for_callbacks!(args, on: :update)
+ set_callback(:commit, :after, *args, &block)
+ end
+
+ # Shortcut for <tt>after_commit :hook, on: :destroy</tt>.
+ def after_destroy_commit(*args, &block)
+ set_options_for_callbacks!(args, on: :destroy)
+ set_callback(:commit, :after, *args, &block)
+ end
+
# This callback is called after a create, update, or destroy are rolled back.
#
- # Please check the documentation of +after_commit+ for options.
+ # Please check the documentation of #after_commit for options.
def after_rollback(*args, &block)
set_options_for_callbacks!(args)
set_callback(:rollback, :after, *args, &block)
@@ -255,33 +275,25 @@ 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)
- options = args.last
- if options.is_a?(Hash) && options[:on]
- fire_on = Array(options[:on])
- assert_valid_transaction_action(fire_on)
- options[:if] = Array(options[:if])
- options[:if] << "transaction_include_any_action?(#{fire_on})"
+ def set_options_for_callbacks!(args, enforced_options = {})
+ options = args.extract_options!.merge!(enforced_options)
+ args << options
+
+ if options[:on]
+ fire_on = Array(options[:on])
+ assert_valid_transaction_action(fire_on)
+ options[:if] = Array(options[:if])
+ options[:if].unshift(-> { transaction_include_any_action?(fire_on) })
+ end
end
- end
- def assert_valid_transaction_action(actions)
- if (actions - ACTIONS).any?
- raise ArgumentError, ":on conditions for after_commit and after_rollback callbacks have to be one of #{ACTIONS}"
+ def assert_valid_transaction_action(actions)
+ if (actions - ACTIONS).any?
+ raise ArgumentError, ":on conditions for after_commit and after_rollback callbacks have to be one of #{ACTIONS}"
+ end
end
- end
end
# See ActiveRecord::Transactions::ClassMethods for detailed documentation.
@@ -294,9 +306,7 @@ module ActiveRecord
end
def save(*) #:nodoc:
- rollback_active_record_state! do
- with_transaction_returning_status { super }
- end
+ with_transaction_returning_status { super }
end
def save!(*) #:nodoc:
@@ -307,48 +317,39 @@ module ActiveRecord
with_transaction_returning_status { super }
end
- # Reset id and @new_record if the transaction rolls back.
- def rollback_active_record_state!
- remember_transaction_record_state
- yield
- rescue Exception
- restore_transaction_record_state
- raise
- ensure
- clear_transaction_record_state
- end
-
def before_committed! # :nodoc:
- run_callbacks :before_commit_without_transaction_enrollment
- run_callbacks :before_commit
+ _run_before_commit_without_transaction_enrollment_callbacks
+ _run_before_commit_callbacks
end
- # Call the +after_commit+ callbacks.
+ # Call the #after_commit callbacks.
#
# Ensure that it is not called if the object was never persisted (failed create),
# but call it after the commit of a destroyed object.
def committed!(should_run_callbacks: true) #:nodoc:
- if should_run_callbacks && destroyed? || persisted?
- run_callbacks :commit_without_transaction_enrollment
- run_callbacks :commit
+ if should_run_callbacks && (destroyed? || persisted?)
+ @_committed_already_called = true
+ _run_commit_without_transaction_enrollment_callbacks
+ _run_commit_callbacks
end
ensure
+ @_committed_already_called = false
force_clear_transaction_record_state
end
- # Call the +after_rollback+ callbacks. The +force_restore_state+ argument indicates if the record
+ # Call the #after_rollback callbacks. The +force_restore_state+ argument indicates if the record
# state should be rolled back to the beginning or just to the last savepoint.
def rolledback!(force_restore_state: false, should_run_callbacks: true) #:nodoc:
if should_run_callbacks
- run_callbacks :rollback
- run_callbacks :rollback_without_transaction_enrollment
+ _run_rollback_callbacks
+ _run_rollback_without_transaction_enrollment_callbacks
end
ensure
restore_transaction_record_state(force_restore_state)
clear_transaction_record_state
end
- # Add the record to the current transaction so that the +after_rollback+ and +after_commit+ callbacks
+ # Add the record to the current transaction so that the #after_rollback and #after_commit callbacks
# can be called.
def add_to_transaction
if has_transactional_callbacks?
@@ -370,115 +371,113 @@ module ActiveRecord
status = nil
self.class.transaction do
add_to_transaction
- begin
- status = yield
- rescue ActiveRecord::Rollback
- clear_transaction_record_state
- status = nil
- end
-
+ status = yield
raise ActiveRecord::Rollback unless status
end
status
end
- protected
-
- # 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:
- @_start_transaction_state[:id] = id
- @_start_transaction_state.reverse_merge!(
- new_record: @new_record,
- destroyed: @destroyed,
- frozen?: frozen?,
- )
- @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1
- end
+ private
+ attr_reader :_committed_already_called, :_trigger_update_callback, :_trigger_destroy_callback
+
+ # 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
+ @_start_transaction_state.reverse_merge!(
+ id: id,
+ new_record: @new_record,
+ destroyed: @destroyed,
+ frozen?: frozen?,
+ )
+ @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1
+ remember_new_record_before_last_commit
+ end
- # Clear the new record state and id of a record.
- def clear_transaction_record_state #:nodoc:
- @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
- force_clear_transaction_record_state if @_start_transaction_state[:level] < 1
- end
+ def remember_new_record_before_last_commit
+ if _committed_already_called
+ @_new_record_before_last_commit = false
+ else
+ @_new_record_before_last_commit = @_start_transaction_state[:new_record]
+ end
+ end
- # Force to clear the transaction record state.
- def force_clear_transaction_record_state #:nodoc:
- @_start_transaction_state.clear
- end
+ # Clear the new record state and id of a record.
+ 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
+ @_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:
- unless @_start_transaction_state.empty?
- transaction_level = (@_start_transaction_state[:level] || 0) - 1
- if transaction_level < 1 || force
- restore_state = @_start_transaction_state
- thaw
- @new_record = restore_state[:new_record]
- @destroyed = restore_state[:destroyed]
- pk = self.class.primary_key
- if pk && read_attribute(pk) != restore_state[:id]
- write_attribute(pk, restore_state[:id])
+ # 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)
+ unless @_start_transaction_state.empty?
+ transaction_level = (@_start_transaction_state[:level] || 0) - 1
+ if transaction_level < 1 || force
+ restore_state = @_start_transaction_state
+ thaw
+ @new_record = restore_state[:new_record]
+ @destroyed = restore_state[:destroyed]
+ pk = self.class.primary_key
+ if pk && _read_attribute(pk) != restore_state[:id]
+ _write_attribute(pk, restore_state[:id])
+ end
+ freeze if restore_state[:frozen?]
end
- freeze if restore_state[:frozen?]
end
end
- 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:
- @_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:
- actions.any? do |action|
- case action
- when :create
- transaction_record_state(:new_record)
- when :destroy
- destroyed?
- when :update
- !(transaction_record_state(:new_record) || destroyed?)
+ # Determine if a transaction included an action for :create, :update, or :destroy. Used in filtering callbacks.
+ def transaction_include_any_action?(actions)
+ actions.any? do |action|
+ case action
+ when :create
+ persisted? && @_new_record_before_last_commit
+ when :update
+ !(@_new_record_before_last_commit || destroyed?) && _trigger_update_callback
+ when :destroy
+ _trigger_destroy_callback
+ end
end
end
- end
-
- private
- def set_transaction_state(state) # :nodoc:
- @transaction_state = state
- end
+ def set_transaction_state(state)
+ @transaction_state = state
+ end
- def has_transactional_callbacks? # :nodoc:
- !_rollback_callbacks.empty? || !_commit_callbacks.empty? || !_before_commit_callbacks.empty?
- end
+ def has_transactional_callbacks?
+ !_rollback_callbacks.empty? || !_commit_callbacks.empty? || !_before_commit_callbacks.empty?
+ end
- # Updates the attributes on this particular ActiveRecord object so that
- # if it's associated with a transaction, then the state of the ActiveRecord
- # object will be updated to reflect the current state of the transaction
- #
- # The @transaction_state variable stores the states of the associated
- # transaction. This relies on the fact that a transaction can only be in
- # one rollback or commit (otherwise a list of states would be required)
- # Each ActiveRecord object inside of a transaction carries that transaction's
- # TransactionState.
- #
- # This method checks to see if the ActiveRecord object's state reflects
- # the TransactionState, and rolls back or commits the ActiveRecord object
- # as appropriate.
- #
- # Since ActiveRecord objects can be inside multiple transactions, this
- # method recursively goes through the parent of the TransactionState and
- # checks if the ActiveRecord object reflects the state of the object.
- def sync_with_transaction_state
- update_attributes_from_transaction_state(@transaction_state)
- end
+ # Updates the attributes on this particular Active Record object so that
+ # if it's associated with a transaction, then the state of the Active Record
+ # object will be updated to reflect the current state of the transaction.
+ #
+ # The <tt>@transaction_state</tt> variable stores the states of the associated
+ # transaction. This relies on the fact that a transaction can only be in
+ # one rollback or commit (otherwise a list of states would be required).
+ # Each Active Record object inside of a transaction carries that transaction's
+ # TransactionState.
+ #
+ # This method checks to see if the ActiveRecord object's state reflects
+ # the TransactionState, and rolls back or commits the Active Record object
+ # as appropriate.
+ #
+ # Since Active Record objects can be inside multiple transactions, this
+ # method recursively goes through the parent of the TransactionState and
+ # checks if the Active Record object reflects the state of the object.
+ def sync_with_transaction_state
+ update_attributes_from_transaction_state(@transaction_state)
+ end
- def update_attributes_from_transaction_state(transaction_state)
- if transaction_state && transaction_state.finalized?
- restore_transaction_record_state if transaction_state.rolledback?
- clear_transaction_record_state
+ def update_attributes_from_transaction_state(transaction_state)
+ if transaction_state && transaction_state.finalized?
+ restore_transaction_record_state(transaction_state.fully_rolledback?) if transaction_state.rolledback?
+ force_clear_transaction_record_state if transaction_state.fully_committed?
+ clear_transaction_record_state if transaction_state.fully_completed?
+ end
end
- end
end
end
diff --git a/activerecord/lib/active_record/translation.rb b/activerecord/lib/active_record/translation.rb
index ddcb5f2a7a..82661a328a 100644
--- a/activerecord/lib/active_record/translation.rb
+++ b/activerecord/lib/active_record/translation.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Translation
include ActiveModel::Translation
@@ -8,7 +10,7 @@ module ActiveRecord
classes = [klass]
return classes if klass == ActiveRecord::Base
- while klass != klass.base_class
+ while !klass.base_class?
classes << klass = klass.superclass
end
classes
diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb
index 2c0cda69d0..c303186ef2 100644
--- a/activerecord/lib/active_record/type.rb
+++ b/activerecord/lib/active_record/type.rb
@@ -1,24 +1,22 @@
-require 'active_record/type/helpers'
-require 'active_record/type/value'
+# frozen_string_literal: true
-require 'active_record/type/big_integer'
-require 'active_record/type/binary'
-require 'active_record/type/boolean'
-require 'active_record/type/date'
-require 'active_record/type/date_time'
-require 'active_record/type/decimal'
-require 'active_record/type/decimal_without_scale'
-require 'active_record/type/float'
-require 'active_record/type/integer'
-require 'active_record/type/serialized'
-require 'active_record/type/string'
-require 'active_record/type/text'
-require 'active_record/type/time'
-require 'active_record/type/unsigned_integer'
+require "active_model/type"
-require 'active_record/type/adapter_specific_registry'
-require 'active_record/type/type_map'
-require 'active_record/type/hash_lookup_type_map'
+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/json"
+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"
+
+require "active_record/type/type_map"
+require "active_record/type/hash_lookup_type_map"
module ActiveRecord
module Type
@@ -29,13 +27,13 @@ module ActiveRecord
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
+ # symbol by {ActiveRecord::Base.attribute}[rdoc-ref:Attributes::ClassMethods#attribute].
+ # If your type is only meant to be used with a specific database adapter, you can
+ # do so by passing <tt>adapter: :postgresql</tt>. 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.
+ # raised unless you specify an +:override+ option. <tt>override: true</tt> will
+ # cause your type to be used instead of the native type. <tt>override:
+ # false</tt> will cause the native type to be used over yours if one exists.
def register(type_name, klass = nil, **options, &block)
registry.register(type_name, klass, **options, &block)
end
@@ -44,6 +42,10 @@ module ActiveRecord
registry.lookup(*args, adapter: adapter, **kwargs)
end
+ def default_value # :nodoc:
+ @default_value ||= Value.new
+ end
+
private
def current_adapter_name
@@ -51,14 +53,25 @@ module ActiveRecord
end
end
+ Helpers = ActiveModel::Type::Helpers
+ BigInteger = ActiveModel::Type::BigInteger
+ Binary = ActiveModel::Type::Binary
+ Boolean = ActiveModel::Type::Boolean
+ Decimal = ActiveModel::Type::Decimal
+ Float = ActiveModel::Type::Float
+ Integer = ActiveModel::Type::Integer
+ String = ActiveModel::Type::String
+ Value = ActiveModel::Type::Value
+
register(:big_integer, Type::BigInteger, override: false)
register(:binary, Type::Binary, override: false)
register(:boolean, Type::Boolean, override: false)
register(:date, Type::Date, override: false)
- register(:date_time, Type::DateTime, override: false)
+ register(:datetime, Type::DateTime, override: false)
register(:decimal, Type::Decimal, override: false)
register(:float, Type::Float, override: false)
register(:integer, Type::Integer, override: false)
+ register(:json, Type::Json, override: false)
register(:string, Type::String, override: false)
register(:text, Type::Text, override: false)
register(:time, Type::Time, 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 5f71b3cb94..b300fdfa05 100644
--- a/activerecord/lib/active_record/type/adapter_specific_registry.rb
+++ b/activerecord/lib/active_record/type/adapter_specific_registry.rb
@@ -1,35 +1,26 @@
+# frozen_string_literal: true
+
+require "active_model/type/registry"
+
module ActiveRecord
# :stopdoc:
module Type
- class AdapterSpecificRegistry
- def initialize
- @registrations = []
- end
-
- def register(type_name, klass = nil, **options, &block)
- block ||= proc { |_, *args| klass.new(*args) }
- registrations << Registration.new(type_name, block, **options)
- end
-
- def lookup(symbol, *args)
- registration = registrations
- .select { |r| r.matches?(symbol, *args) }
- .max
-
- if registration
- registration.call(self, symbol, *args)
- else
- raise ArgumentError, "Unknown type #{symbol.inspect}"
- end
- end
-
+ class AdapterSpecificRegistry < ActiveModel::Type::Registry
def add_modifier(options, klass, **args)
registrations << DecorationRegistration.new(options, klass, **args)
end
- protected
+ private
- attr_reader :registrations
+ def registration_klass
+ Registration
+ end
+
+ def find_registration(symbol, *args)
+ registrations
+ .select { |registration| registration.matches?(symbol, *args) }
+ .max
+ end
end
class Registration
@@ -63,42 +54,42 @@ module ActiveRecord
protected
- attr_reader :name, :block, :adapter, :override
-
- def priority
- result = 0
- if adapter
- result |= 1
+ attr_reader :name, :block, :adapter, :override
+
+ def priority
+ result = 0
+ if adapter
+ result |= 1
+ end
+ if override
+ result |= 2
+ end
+ result
end
- if override
- result |= 2
- end
- result
- end
- def priority_except_adapter
- priority & 0b111111100
- end
+ def priority_except_adapter
+ priority & 0b111111100
+ end
private
- def matches_adapter?(adapter: nil, **)
- (self.adapter.nil? || adapter == self.adapter)
- end
+ def matches_adapter?(adapter: nil, **)
+ (self.adapter.nil? || adapter == self.adapter)
+ end
- def conflicts_with?(other)
- same_priority_except_adapter?(other) &&
- has_adapter_conflict?(other)
- end
+ def conflicts_with?(other)
+ same_priority_except_adapter?(other) &&
+ has_adapter_conflict?(other)
+ end
- def same_priority_except_adapter?(other)
- priority_except_adapter == other.priority_except_adapter
- end
+ def same_priority_except_adapter?(other)
+ priority_except_adapter == other.priority_except_adapter
+ end
- def has_adapter_conflict?(other)
- (override.nil? && other.adapter) ||
- (adapter && other.override.nil?)
- end
+ def has_adapter_conflict?(other)
+ (override.nil? && other.adapter) ||
+ (adapter && other.override.nil?)
+ end
end
class DecorationRegistration < Registration
@@ -121,22 +112,18 @@ module ActiveRecord
super | 4
end
- protected
-
- attr_reader :options, :klass
-
private
+ attr_reader :options, :klass
- def matches_options?(**kwargs)
- options.all? do |key, value|
- kwargs[key] == value
+ def matches_options?(**kwargs)
+ options.all? do |key, value|
+ kwargs[key] == value
+ end
end
- end
end
end
class TypeConflictError < StandardError
end
-
# :startdoc:
end
diff --git a/activerecord/lib/active_record/type/big_integer.rb b/activerecord/lib/active_record/type/big_integer.rb
deleted file mode 100644
index 0c72d8914f..0000000000
--- a/activerecord/lib/active_record/type/big_integer.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-require 'active_record/type/integer'
-
-module ActiveRecord
- module Type
- class BigInteger < Integer # :nodoc:
- private
-
- def max_value
- ::Float::INFINITY
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/binary.rb b/activerecord/lib/active_record/type/binary.rb
deleted file mode 100644
index 0baf8c63ad..0000000000
--- a/activerecord/lib/active_record/type/binary.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-module ActiveRecord
- module Type
- class Binary < Value # :nodoc:
- def type
- :binary
- end
-
- def binary?
- true
- end
-
- def cast(value)
- if value.is_a?(Data)
- value.to_s
- else
- super
- end
- end
-
- def serialize(value)
- return if value.nil?
- Data.new(super)
- end
-
- def changed_in_place?(raw_old_value, value)
- old_value = deserialize(raw_old_value)
- old_value != value
- end
-
- class Data # :nodoc:
- def initialize(value)
- @value = value.to_s
- end
-
- def to_s
- @value
- end
- alias_method :to_str, :to_s
-
- def hex
- @value.unpack('H*')[0]
- end
-
- def ==(other)
- other == to_s || super
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/boolean.rb b/activerecord/lib/active_record/type/boolean.rb
deleted file mode 100644
index f6a75512fd..0000000000
--- a/activerecord/lib/active_record/type/boolean.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-module ActiveRecord
- module Type
- class Boolean < Value # :nodoc:
- def type
- :boolean
- end
-
- private
-
- def cast_value(value)
- if value == ''
- nil
- else
- !ConnectionAdapters::Column::FALSE_VALUES.include?(value)
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/date.rb b/activerecord/lib/active_record/type/date.rb
index 3ceab59ebb..8177074a20 100644
--- a/activerecord/lib/active_record/type/date.rb
+++ b/activerecord/lib/active_record/type/date.rb
@@ -1,49 +1,9 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Type
- class Date < Value # :nodoc:
- include Helpers::AcceptsMultiparameterTime.new
-
- def type
- :date
- end
-
- def type_cast_for_schema(value)
- "'#{value.to_s(:db)}'"
- end
-
- private
-
- def cast_value(value)
- if value.is_a?(::String)
- return if value.empty?
- fast_string_to_date(value) || fallback_string_to_date(value)
- elsif value.respond_to?(:to_date)
- value.to_date
- else
- value
- end
- end
-
- def fast_string_to_date(string)
- if string =~ ConnectionAdapters::Column::Format::ISO_DATE
- new_date $1.to_i, $2.to_i, $3.to_i
- end
- end
-
- def fallback_string_to_date(string)
- new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday))
- end
-
- def new_date(year, mon, mday)
- if year && year != 0
- ::Date.new(year, mon, mday) rescue nil
- end
- end
-
- def value_from_multiparameter_assignment(*)
- time = super
- time && time.to_date
- end
+ class Date < ActiveModel::Type::Date
+ include Internal::Timezone
end
end
end
diff --git a/activerecord/lib/active_record/type/date_time.rb b/activerecord/lib/active_record/type/date_time.rb
index a5199959b9..4acde6b9f8 100644
--- a/activerecord/lib/active_record/type/date_time.rb
+++ b/activerecord/lib/active_record/type/date_time.rb
@@ -1,44 +1,9 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Type
- class DateTime < Value # :nodoc:
- include Helpers::TimeValue
- include Helpers::AcceptsMultiparameterTime.new(
- defaults: { 4 => 0, 5 => 0 }
- )
-
- def type
- :datetime
- end
-
- private
-
- def cast_value(string)
- return string unless string.is_a?(::String)
- return if string.empty?
-
- fast_string_to_time(string) || fallback_string_to_time(string)
- end
-
- # '0.123456' -> 123456
- # '1.123456' -> 123456
- def microseconds(time)
- time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0
- end
-
- def fallback_string_to_time(string)
- time_hash = ::Date._parse(string)
- time_hash[:sec_fraction] = microseconds(time_hash)
-
- new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset))
- end
-
- def value_from_multiparameter_assignment(values_hash)
- missing_parameter = (1..3).detect { |key| !values_hash.key?(key) }
- if missing_parameter
- raise ArgumentError, missing_parameter
- end
- super
- end
+ class DateTime < ActiveModel::Type::DateTime
+ include Internal::Timezone
end
end
end
diff --git a/activerecord/lib/active_record/type/decimal.rb b/activerecord/lib/active_record/type/decimal.rb
deleted file mode 100644
index f200a92d10..0000000000
--- a/activerecord/lib/active_record/type/decimal.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-module ActiveRecord
- module Type
- class Decimal < Value # :nodoc:
- include Helpers::Numeric
-
- def type
- :decimal
- end
-
- def type_cast_for_schema(value)
- value.to_s.inspect
- end
-
- private
-
- def cast_value(value)
- case value
- when ::Float
- convert_float_to_big_decimal(value)
- when ::Numeric, ::String
- BigDecimal(value, precision.to_i)
- else
- if value.respond_to?(:to_d)
- value.to_d
- else
- cast_value(value.to_s)
- end
- end
- end
-
- def convert_float_to_big_decimal(value)
- if precision
- BigDecimal(value, float_precision)
- else
- value.to_d
- end
- end
-
- def float_precision
- if precision.to_i > ::Float::DIG + 1
- ::Float::DIG + 1
- else
- precision.to_i
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/decimal_without_scale.rb b/activerecord/lib/active_record/type/decimal_without_scale.rb
index ff5559e300..a207940dc7 100644
--- a/activerecord/lib/active_record/type/decimal_without_scale.rb
+++ b/activerecord/lib/active_record/type/decimal_without_scale.rb
@@ -1,11 +1,15 @@
-require 'active_record/type/big_integer'
+# frozen_string_literal: true
module ActiveRecord
module Type
- class DecimalWithoutScale < BigInteger # :nodoc:
+ 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/float.rb b/activerecord/lib/active_record/type/float.rb
deleted file mode 100644
index d88482b85d..0000000000
--- a/activerecord/lib/active_record/type/float.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-module ActiveRecord
- module Type
- class Float < Value # :nodoc:
- include Helpers::Numeric
-
- def type
- :float
- end
-
- alias serialize cast
-
- private
-
- def cast_value(value)
- case value
- when ::Float then value
- when "Infinity" then ::Float::INFINITY
- when "-Infinity" then -::Float::INFINITY
- when "NaN" then ::Float::NAN
- else value.to_f
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/hash_lookup_type_map.rb b/activerecord/lib/active_record/type/hash_lookup_type_map.rb
index 3b01e3f8ca..db9853fbcc 100644
--- a/activerecord/lib/active_record/type/hash_lookup_type_map.rb
+++ b/activerecord/lib/active_record/type/hash_lookup_type_map.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Type
class HashLookupTypeMap < TypeMap # :nodoc:
@@ -15,9 +17,9 @@ module ActiveRecord
private
- def perform_fetch(type, *args, &block)
- @mapping.fetch(type, block).call(type, *args)
- end
+ def perform_fetch(type, *args, &block)
+ @mapping.fetch(type, block).call(type, *args)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/type/helpers.rb b/activerecord/lib/active_record/type/helpers.rb
deleted file mode 100644
index 634d417d13..0000000000
--- a/activerecord/lib/active_record/type/helpers.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-require 'active_record/type/helpers/accepts_multiparameter_time'
-require 'active_record/type/helpers/numeric'
-require 'active_record/type/helpers/mutable'
-require 'active_record/type/helpers/time_value'
diff --git a/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb b/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb
deleted file mode 100644
index be571fc1c7..0000000000
--- a/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-module ActiveRecord
- module Type
- module Helpers
- class AcceptsMultiparameterTime < Module # :nodoc:
- def initialize(defaults: {})
- define_method(:cast) do |value|
- if value.is_a?(Hash)
- value_from_multiparameter_assignment(value)
- else
- super(value)
- end
- end
-
- define_method(:value_from_multiparameter_assignment) do |values_hash|
- defaults.each do |k, v|
- values_hash[k] ||= v
- end
- return unless values_hash[1] && values_hash[2] && values_hash[3]
- values = values_hash.sort.map(&:last)
- ::Time.send(
- ActiveRecord::Base.default_timezone,
- *values
- )
- end
- private :value_from_multiparameter_assignment
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/helpers/mutable.rb b/activerecord/lib/active_record/type/helpers/mutable.rb
deleted file mode 100644
index 88a9099277..0000000000
--- a/activerecord/lib/active_record/type/helpers/mutable.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-module ActiveRecord
- module Type
- module Helpers
- module Mutable # :nodoc:
- def cast(value)
- deserialize(serialize(value))
- end
-
- # +raw_old_value+ will be the `_before_type_cast` version of the
- # value (likely a string). +new_value+ will be the current, type
- # cast value.
- def changed_in_place?(raw_old_value, new_value)
- raw_old_value != serialize(new_value)
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/helpers/numeric.rb b/activerecord/lib/active_record/type/helpers/numeric.rb
deleted file mode 100644
index a755a02a59..0000000000
--- a/activerecord/lib/active_record/type/helpers/numeric.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-module ActiveRecord
- module Type
- module Helpers
- module Numeric # :nodoc:
- def cast(value)
- value = case value
- when true then 1
- when false then 0
- when ::String then value.presence
- else value
- end
- super(value)
- end
-
- def changed?(old_value, _new_value, new_value_before_type_cast) # :nodoc:
- super || number_to_non_number?(old_value, new_value_before_type_cast)
- end
-
- private
-
- def number_to_non_number?(old_value, new_value_before_type_cast)
- old_value != nil && non_numeric_string?(new_value_before_type_cast)
- end
-
- def non_numeric_string?(value)
- # 'wibble'.to_i will give zero, we want to make sure
- # that we aren't marking int zero to string zero as
- # changed.
- value.to_s !~ /\A-?\d+\.?\d*\z/
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/helpers/time_value.rb b/activerecord/lib/active_record/type/helpers/time_value.rb
deleted file mode 100644
index 7eb41557cb..0000000000
--- a/activerecord/lib/active_record/type/helpers/time_value.rb
+++ /dev/null
@@ -1,58 +0,0 @@
-module ActiveRecord
- module Type
- module Helpers
- module TimeValue # :nodoc:
- def serialize(value)
- if precision && value.respond_to?(:usec)
- number_of_insignificant_digits = 6 - precision
- round_power = 10 ** number_of_insignificant_digits
- value = value.change(usec: value.usec / round_power * round_power)
- end
-
- if value.acts_like?(:time)
- zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
-
- if value.respond_to?(zone_conversion_method)
- value = value.send(zone_conversion_method)
- end
- end
-
- value
- end
-
- def type_cast_for_schema(value)
- "'#{value.to_s(:db)}'"
- end
-
- def user_input_in_time_zone(value)
- value.in_time_zone
- end
-
- private
-
- def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil)
- # Treat 0000-00-00 00:00:00 as nil.
- return if year.nil? || (year == 0 && mon == 0 && mday == 0)
-
- if offset
- time = ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil
- return unless time
-
- time -= offset
- Base.default_timezone == :utc ? time : time.getlocal
- else
- ::Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
- end
- end
-
- # Doesn't handle time zones.
- def fast_string_to_time(string)
- if string =~ ConnectionAdapters::Column::Format::ISO_DATETIME
- microsec = ($7.to_r * 1_000_000).to_i
- new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec
- end
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/integer.rb b/activerecord/lib/active_record/type/integer.rb
deleted file mode 100644
index c5040c6d3b..0000000000
--- a/activerecord/lib/active_record/type/integer.rb
+++ /dev/null
@@ -1,66 +0,0 @@
-module ActiveRecord
- module Type
- class Integer < Value # :nodoc:
- include Helpers::Numeric
-
- # Column storage size in bytes.
- # 4 bytes means a MySQL int or Postgres integer as opposed to smallint etc.
- DEFAULT_LIMIT = 4
-
- def initialize(*)
- super
- @range = min_value...max_value
- end
-
- def type
- :integer
- end
-
- def deserialize(value)
- return if value.nil?
- value.to_i
- end
-
- def serialize(value)
- result = cast(value)
- if result
- ensure_in_range(result)
- end
- result
- end
-
- protected
-
- attr_reader :range
-
- private
-
- def cast_value(value)
- case value
- when true then 1
- when false then 0
- else
- value.to_i rescue nil
- end
- end
-
- def ensure_in_range(value)
- unless range.cover?(value)
- raise RangeError, "#{value} is out of range for #{self.class} with limit #{_limit}"
- end
- end
-
- def max_value
- 1 << (_limit * 8 - 1) # 8 bits per byte with one bit for sign
- end
-
- def min_value
- -max_value
- end
-
- def _limit
- self.limit || DEFAULT_LIMIT
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/internal/timezone.rb b/activerecord/lib/active_record/type/internal/timezone.rb
new file mode 100644
index 0000000000..3059755752
--- /dev/null
+++ b/activerecord/lib/active_record/type/internal/timezone.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module Type
+ module Internal
+ module Timezone
+ def is_utc?
+ ActiveRecord::Base.default_timezone == :utc
+ end
+
+ def default_timezone
+ ActiveRecord::Base.default_timezone
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/json.rb b/activerecord/lib/active_record/type/json.rb
new file mode 100644
index 0000000000..3f9ff22796
--- /dev/null
+++ b/activerecord/lib/active_record/type/json.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module Type
+ class Json < ActiveModel::Type::Value
+ include ActiveModel::Type::Helpers::Mutable
+
+ def type
+ :json
+ end
+
+ def deserialize(value)
+ return value unless value.is_a?(::String)
+ ActiveSupport::JSON.decode(value) rescue nil
+ end
+
+ def serialize(value)
+ ActiveSupport::JSON.encode(value) unless value.nil?
+ end
+
+ def changed_in_place?(raw_old_value, new_value)
+ deserialize(raw_old_value) != new_value
+ end
+
+ def accessor
+ ActiveRecord::Store::StringKeyedHashAccessor
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb
index ea3e0d6a45..0a2f6cb9fb 100644
--- a/activerecord/lib/active_record/type/serialized.rb
+++ b/activerecord/lib/active_record/type/serialized.rb
@@ -1,7 +1,11 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Type
- class Serialized < DelegateClass(Type::Value) # :nodoc:
- include Helpers::Mutable
+ class Serialized < DelegateClass(ActiveModel::Type::Value) # :nodoc:
+ undef to_yaml if method_defined?(:to_yaml)
+
+ include ActiveModel::Type::Helpers::Mutable
attr_reader :subtype, :coder
@@ -32,7 +36,7 @@ module ActiveRecord
def changed_in_place?(raw_old_value, value)
return false if value.nil?
- raw_new_value = serialize(value)
+ raw_new_value = encoded(value)
raw_old_value.nil? != raw_new_value.nil? ||
subtype.changed_in_place?(raw_old_value, raw_new_value)
end
@@ -41,11 +45,27 @@ module ActiveRecord
ActiveRecord::Store::IndifferentHashAccessor
end
- private
+ def assert_valid_value(value)
+ if coder.respond_to?(:assert_valid_value)
+ coder.assert_valid_value(value, action: "serialize")
+ end
+ end
- def default_value?(value)
- value == coder.load(nil)
+ def force_equality?(value)
+ coder.respond_to?(:object_class) && value.is_a?(coder.object_class)
end
+
+ private
+
+ def default_value?(value)
+ value == coder.load(nil)
+ end
+
+ def encoded(value)
+ unless default_value?(value)
+ coder.dump(value)
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/type/string.rb b/activerecord/lib/active_record/type/string.rb
deleted file mode 100644
index 2662b7e874..0000000000
--- a/activerecord/lib/active_record/type/string.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-module ActiveRecord
- module Type
- class String < Value # :nodoc:
- def type
- :string
- end
-
- def changed_in_place?(raw_old_value, new_value)
- if new_value.is_a?(::String)
- raw_old_value != new_value
- end
- end
-
- def serialize(value)
- case value
- when ::Numeric, ActiveSupport::Duration then value.to_s
- when ::String then ::String.new(value)
- when true then "t"
- when false then "f"
- else super
- end
- end
-
- private
-
- def cast_value(value)
- case value
- when true then "t"
- when false then "f"
- # String.new is slightly faster than dup
- else ::String.new(value.to_s)
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type/text.rb b/activerecord/lib/active_record/type/text.rb
index 26f980f060..6d19696671 100644
--- a/activerecord/lib/active_record/type/text.rb
+++ b/activerecord/lib/active_record/type/text.rb
@@ -1,8 +1,8 @@
-require 'active_record/type/string'
+# frozen_string_literal: true
module ActiveRecord
module Type
- class Text < String # :nodoc:
+ class Text < ActiveModel::Type::String # :nodoc:
def type
:text
end
diff --git a/activerecord/lib/active_record/type/time.rb b/activerecord/lib/active_record/type/time.rb
index 19a10021bc..f4da1ecf2c 100644
--- a/activerecord/lib/active_record/type/time.rb
+++ b/activerecord/lib/active_record/type/time.rb
@@ -1,40 +1,19 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Type
- class Time < Value # :nodoc:
- include Helpers::TimeValue
- include Helpers::AcceptsMultiparameterTime.new(
- defaults: { 1 => 1970, 2 => 1, 3 => 1, 4 => 0, 5 => 0 }
- )
+ class Time < ActiveModel::Type::Time
+ include Internal::Timezone
- def type
- :time
+ class Value < DelegateClass(::Time) # :nodoc:
end
- def user_input_in_time_zone(value)
- return unless value.present?
-
- case value
- when ::String
- value = "2000-01-01 #{value}"
+ def serialize(value)
+ case value = super
when ::Time
- value = value.change(year: 2000, day: 1, month: 1)
- end
-
- super(value)
- end
-
- private
-
- def cast_value(value)
- return value unless value.is_a?(::String)
- return if value.empty?
-
- dummy_time_value = "2000-01-01 #{value}"
-
- fast_string_to_time(dummy_time_value) || begin
- time_hash = ::Date._parse(dummy_time_value)
- return if time_hash[:hour].nil?
- new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction))
+ Value.new(value)
+ else
+ value
end
end
end
diff --git a/activerecord/lib/active_record/type/type_map.rb b/activerecord/lib/active_record/type/type_map.rb
index 09f5ba6b74..fc40b460f0 100644
--- a/activerecord/lib/active_record/type/type_map.rb
+++ b/activerecord/lib/active_record/type/type_map.rb
@@ -1,17 +1,19 @@
-require 'thread_safe'
+# frozen_string_literal: true
+
+require "concurrent/map"
module ActiveRecord
module Type
class TypeMap # :nodoc:
def initialize
@mapping = {}
- @cache = ThreadSafe::Cache.new do |h, key|
- h.fetch_or_store(key, ThreadSafe::Cache.new)
+ @cache = Concurrent::Map.new do |h, key|
+ h.fetch_or_store(key, Concurrent::Map.new)
end
end
def lookup(lookup_key, *args)
- fetch(lookup_key, *args) { default_value }
+ fetch(lookup_key, *args) { Type.default_value }
end
def fetch(lookup_key, *args, &block)
@@ -44,21 +46,17 @@ module ActiveRecord
private
- def perform_fetch(lookup_key, *args)
- matching_pair = @mapping.reverse_each.detect do |key, _|
- key === lookup_key
- end
+ def perform_fetch(lookup_key, *args)
+ matching_pair = @mapping.reverse_each.detect do |key, _|
+ key === lookup_key
+ end
- if matching_pair
- matching_pair.last.call(lookup_key, *args)
- else
- yield lookup_key, *args
+ if matching_pair
+ matching_pair.last.call(lookup_key, *args)
+ else
+ yield lookup_key, *args
+ end
end
- end
-
- def default_value
- @default_value ||= Value.new
- end
end
end
end
diff --git a/activerecord/lib/active_record/type/unsigned_integer.rb b/activerecord/lib/active_record/type/unsigned_integer.rb
index ed3e527483..4619528f81 100644
--- a/activerecord/lib/active_record/type/unsigned_integer.rb
+++ b/activerecord/lib/active_record/type/unsigned_integer.rb
@@ -1,15 +1,17 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Type
- class UnsignedInteger < Integer # :nodoc:
+ class UnsignedInteger < ActiveModel::Type::Integer # :nodoc:
private
- def max_value
- super * 2
- end
+ def max_value
+ super * 2
+ end
- def min_value
- 0
- end
+ def min_value
+ 0
+ end
end
end
end
diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb
deleted file mode 100644
index 6b9d147ecc..0000000000
--- a/activerecord/lib/active_record/type/value.rb
+++ /dev/null
@@ -1,104 +0,0 @@
-module ActiveRecord
- module Type
- class Value
- attr_reader :precision, :scale, :limit
-
- def initialize(precision: nil, limit: nil, scale: nil)
- @precision = precision
- @scale = scale
- @limit = limit
- end
-
- def type # :nodoc:
- end
-
- # Converts a value from database input to the appropriate ruby type. The
- # return value of this method will be returned from
- # ActiveRecord::AttributeMethods::Read#read_attribute. The default
- # implementation just calls Value#cast.
- #
- # +value+ The raw input, as provided from the database.
- def deserialize(value)
- cast(value)
- end
-
- # Type casts a value from user input (e.g. from a setter). This value may
- # be a string from the form builder, or a ruby object passed to a setter.
- # There is currently no way to differentiate between which source it came
- # from.
- #
- # The return value of this method will be returned from
- # ActiveRecord::AttributeMethods::Read#read_attribute. See also:
- # Value#cast_value.
- #
- # +value+ The raw input, as provided to the attribute setter.
- def cast(value)
- cast_value(value) unless value.nil?
- end
-
- # Casts a value from the ruby type to a type that the database knows how
- # to understand. The returned value from this method should be a
- # +String+, +Numeric+, +Date+, +Time+, +Symbol+, +true+, +false+, or
- # +nil+.
- def serialize(value)
- value
- end
-
- # Type casts a value for schema dumping. This method is private, as we are
- # hoping to remove it entirely.
- def type_cast_for_schema(value) # :nodoc:
- value.inspect
- end
-
- # These predicates are not documented, as I need to look further into
- # their use, and see if they can be removed entirely.
- def binary? # :nodoc:
- false
- end
-
- # Determines whether a value has changed for dirty checking. +old_value+
- # and +new_value+ will always be type-cast. Types should not need to
- # override this method.
- def changed?(old_value, new_value, _new_value_before_type_cast)
- old_value != new_value
- end
-
- # Determines whether the mutable value has been modified since it was
- # read. Returns +false+ by default. If your type returns an object
- # which could be mutated, you should override this method. You will need
- # to either:
- #
- # - pass +new_value+ to Value#serialize and compare it to
- # +raw_old_value+
- #
- # or
- #
- # - pass +raw_old_value+ to Value#deserialize and compare it to
- # +new_value+
- #
- # +raw_old_value+ The original value, before being passed to
- # +deserialize+.
- #
- # +new_value+ The current value, after type casting.
- def changed_in_place?(raw_old_value, new_value)
- false
- end
-
- def ==(other)
- self.class == other.class &&
- precision == other.precision &&
- scale == other.scale &&
- limit == other.limit
- end
-
- private
-
- # Convenience method for types which do not need separate type casting
- # behavior for user and database inputs. Called by Value#cast for
- # values except +nil+.
- def cast_value(value) # :doc:
- value
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/type_caster.rb b/activerecord/lib/active_record/type_caster.rb
index 63ba10c289..2e5f45fa3d 100644
--- a/activerecord/lib/active_record/type_caster.rb
+++ b/activerecord/lib/active_record/type_caster.rb
@@ -1,7 +1,9 @@
-require 'active_record/type_caster/map'
-require 'active_record/type_caster/connection'
+# frozen_string_literal: true
+
+require "active_record/type_caster/map"
+require "active_record/type_caster/connection"
module ActiveRecord
- module TypeCaster
+ module TypeCaster # :nodoc:
end
end
diff --git a/activerecord/lib/active_record/type_caster/connection.rb b/activerecord/lib/active_record/type_caster/connection.rb
index 3878270770..7cf8181d8e 100644
--- a/activerecord/lib/active_record/type_caster/connection.rb
+++ b/activerecord/lib/active_record/type_caster/connection.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
module ActiveRecord
module TypeCaster
- class Connection
+ class Connection # :nodoc:
def initialize(klass, table_name)
@klass = klass
@table_name = table_name
@@ -12,18 +14,15 @@ module ActiveRecord
connection.type_cast_from_column(column, value)
end
- protected
-
- attr_reader :table_name
- delegate :connection, to: :@klass
-
private
+ attr_reader :table_name
+ delegate :connection, to: :@klass
- def column_for(attribute_name)
- if connection.schema_cache.table_exists?(table_name)
- connection.schema_cache.columns_hash(table_name)[attribute_name.to_s]
+ def column_for(attribute_name)
+ if connection.schema_cache.data_source_exists?(table_name)
+ connection.schema_cache.columns_hash(table_name)[attribute_name.to_s]
+ end
end
- end
end
end
end
diff --git a/activerecord/lib/active_record/type_caster/map.rb b/activerecord/lib/active_record/type_caster/map.rb
index 4b1941351c..663cdadb03 100644
--- a/activerecord/lib/active_record/type_caster/map.rb
+++ b/activerecord/lib/active_record/type_caster/map.rb
@@ -1,19 +1,20 @@
+# frozen_string_literal: true
+
module ActiveRecord
module TypeCaster
- class Map
+ class Map # :nodoc:
def initialize(types)
@types = types
end
def type_cast_for_database(attr_name, value)
return value if value.is_a?(Arel::Nodes::BindParam)
- type = types.type_for_attribute(attr_name.to_s)
+ type = types.type_for_attribute(attr_name)
type.serialize(value)
end
- protected
-
- attr_reader :types
+ private
+ attr_reader :types
end
end
end
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
index 34d96b19fe..ca27a3f0ab 100644
--- a/activerecord/lib/active_record/validations.rb
+++ b/activerecord/lib/active_record/validations.rb
@@ -1,8 +1,11 @@
+# frozen_string_literal: true
+
module ActiveRecord
- # = Active Record RecordInvalid
+ # = Active Record \RecordInvalid
#
- # Raised by <tt>save!</tt> and <tt>create!</tt> when the record is invalid. Use the
- # +record+ method to retrieve the record which did not validate.
+ # Raised by {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!] and
+ # {ActiveRecord::Base#create!}[rdoc-ref:Persistence::ClassMethods#create!] when the record is invalid.
+ # Use the #record method to retrieve the record which did not validate.
#
# begin
# complex_operation_that_internally_calls_save!
@@ -12,62 +15,72 @@ module ActiveRecord
class RecordInvalid < ActiveRecordError
attr_reader :record
- def initialize(record)
- @record = record
- errors = @record.errors.full_messages.join(", ")
- super(I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid", :errors => errors, :default => :"errors.messages.record_invalid"))
+ def initialize(record = nil)
+ if record
+ @record = record
+ errors = @record.errors.full_messages.join(", ")
+ message = I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid", errors: errors, default: :"errors.messages.record_invalid")
+ else
+ message = "Record invalid"
+ end
+
+ super(message)
end
end
- # = Active Record Validations
+ # = Active Record \Validations
#
- # Active Record includes the majority of its validations from <tt>ActiveModel::Validations</tt>
+ # Active Record includes the majority of its validations from ActiveModel::Validations
# all of which accept the <tt>:on</tt> argument to define the context where the
# validations are active. Active Record will always supply either the context of
# <tt>:create</tt> or <tt>:update</tt> dependent on whether the model is a
- # <tt>new_record?</tt>.
+ # {new_record?}[rdoc-ref:Persistence#new_record?].
module Validations
extend ActiveSupport::Concern
include ActiveModel::Validations
# The validation process on save can be skipped by passing <tt>validate: false</tt>.
- # The regular Base#save method is replaced with this when the validations
- # module is mixed in, which it is by default.
- def save(options={})
+ # The regular {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] method is replaced
+ # with this when the validations module is mixed in, which it is by default.
+ def save(options = {})
perform_validations(options) ? super : false
end
- # Attempts to save the record just like Base#save but will raise a +RecordInvalid+
- # exception instead of returning +false+ if the record is not valid.
- def save!(options={})
+ # Attempts to save the record just like {ActiveRecord::Base#save}[rdoc-ref:Base#save] but
+ # will raise an ActiveRecord::RecordInvalid exception instead of returning +false+ if the record is not valid.
+ def save!(options = {})
perform_validations(options) ? super : raise_validation_error
end
# Runs all the validations within the specified context. Returns +true+ if
# no errors are found, +false+ otherwise.
#
- # Aliased as validate.
+ # Aliased as #validate.
#
# If the argument is +false+ (default is +nil+), the context is set to <tt>:create</tt> if
- # <tt>new_record?</tt> is +true+, and to <tt>:update</tt> if it is not.
+ # {new_record?}[rdoc-ref:Persistence#new_record?] is +true+, and to <tt>:update</tt> if it is not.
#
- # Validations with no <tt>:on</tt> option will run no matter the context. Validations with
+ # \Validations with no <tt>:on</tt> option will run no matter the context. \Validations with
# some <tt>:on</tt> option will only run in the specified context.
def valid?(context = nil)
- context ||= (new_record? ? :create : :update)
+ context ||= default_validation_context
output = super(context)
errors.empty? && output
end
alias_method :validate, :valid?
- protected
+ private
+
+ def default_validation_context
+ new_record? ? :create : :update
+ end
def raise_validation_error
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/absence.rb b/activerecord/lib/active_record/validations/absence.rb
index 2e19e6dc5c..6afb9eabd2 100644
--- a/activerecord/lib/active_record/validations/absence.rb
+++ b/activerecord/lib/active_record/validations/absence.rb
@@ -1,8 +1,9 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Validations
class AbsenceValidator < ActiveModel::Validations::AbsenceValidator # :nodoc:
def validate_each(record, attribute, association_or_value)
- return unless should_validate?(record)
if record.class._reflect_on_association(attribute)
association_or_value = Array.wrap(association_or_value).reject(&:marked_for_destruction?)
end
diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb
index 47ccef31a5..3538aeec22 100644
--- a/activerecord/lib/active_record/validations/associated.rb
+++ b/activerecord/lib/active_record/validations/associated.rb
@@ -1,11 +1,19 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Validations
class AssociatedValidator < ActiveModel::EachValidator #:nodoc:
def validate_each(record, attribute, value)
- if Array.wrap(value).reject {|r| r.marked_for_destruction? || r.valid?}.any?
- record.errors.add(attribute, :invalid, options.merge(:value => value))
+ if Array(value).reject { |r| valid_object?(r) }.any?
+ record.errors.add(attribute, :invalid, options.merge(value: value))
end
end
+
+ private
+
+ def valid_object?(record)
+ (record.respond_to?(:marked_for_destruction?) && record.marked_for_destruction?) || record.valid?
+ end
end
module ClassMethods
@@ -24,13 +32,14 @@ module ActiveRecord
#
# NOTE: This validation will not fail if the association hasn't been
# assigned. If you want to ensure that the association is both present and
- # guaranteed to be valid, you also need to use +validates_presence_of+.
+ # guaranteed to be valid, you also need to use
+ # {validates_presence_of}[rdoc-ref:Validations::ClassMethods#validates_presence_of].
#
# Configuration options:
#
# * <tt>:message</tt> - A custom error message (default is: "is invalid").
# * <tt>:on</tt> - Specifies the contexts where this validation is active.
- # Runs in all validation contexts by default (nil). You can pass a symbol
+ # Runs in all validation contexts by default +nil+. You can pass a symbol
# or an array of symbols. (e.g. <tt>on: :create</tt> or
# <tt>on: :custom_validation_context</tt> or
# <tt>on: [:create, :custom_validation_context]</tt>)
diff --git a/activerecord/lib/active_record/validations/length.rb b/activerecord/lib/active_record/validations/length.rb
index 69e048eef1..f47b14ae3a 100644
--- a/activerecord/lib/active_record/validations/length.rb
+++ b/activerecord/lib/active_record/validations/length.rb
@@ -1,24 +1,14 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Validations
class LengthValidator < ActiveModel::Validations::LengthValidator # :nodoc:
def validate_each(record, attribute, association_or_value)
- return unless should_validate?(record) || associations_are_dirty?(record)
if association_or_value.respond_to?(:loaded?) && association_or_value.loaded?
association_or_value = association_or_value.target.reject(&:marked_for_destruction?)
end
super
end
-
- def associations_are_dirty?(record)
- attributes.any? do |attribute|
- value = record.read_attribute_for_validation(attribute)
- if value.respond_to?(:loaded?) && value.loaded?
- value.target.any?(&:marked_for_destruction?)
- else
- false
- end
- end
- end
end
module ClassMethods
diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb
index 23a3985d35..75e97e1997 100644
--- a/activerecord/lib/active_record/validations/presence.rb
+++ b/activerecord/lib/active_record/validations/presence.rb
@@ -1,8 +1,9 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Validations
class PresenceValidator < ActiveModel::Validations::PresenceValidator # :nodoc:
def validate_each(record, attribute, association_or_value)
- return unless should_validate?(record)
if record.class._reflect_on_association(attribute)
association_or_value = Array.wrap(association_or_value).reject(&:marked_for_destruction?)
end
@@ -31,7 +32,7 @@ module ActiveRecord
# This is due to the way Object#blank? handles boolean values:
# <tt>false.blank? # => true</tt>.
#
- # This validator defers to the ActiveModel validation for presence, adding the
+ # This validator defers to the Active Model validation for presence, adding the
# check to see that an associated object is not marked for destruction. This
# prevents the parent object from validating successfully and saving, which then
# deletes the associated object, thus putting the parent object into an invalid
@@ -39,12 +40,13 @@ module ActiveRecord
#
# NOTE: This validation will not fail while using it with an association
# if the latter was assigned but not valid. If you want to ensure that
- # it is both present and valid, you also need to use +validates_associated+.
+ # it is both present and valid, you also need to use
+ # {validates_associated}[rdoc-ref:Validations::ClassMethods#validates_associated].
#
# Configuration options:
# * <tt>:message</tt> - A custom error message (default is: "can't be blank").
# * <tt>:on</tt> - Specifies the contexts where this validation is active.
- # Runs in all validation contexts by default (nil). You can pass a symbol
+ # Runs in all validation contexts by default +nil+. You can pass a symbol
# or an array of symbols. (e.g. <tt>on: :create</tt> or
# <tt>on: :custom_validation_context</tt> or
# <tt>on: [:create, :custom_validation_context]</tt>)
@@ -57,7 +59,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 <tt>ActiveModel::Validation#validates!</tt> 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 5106f4e127..5a1dbc8e53 100644
--- a/activerecord/lib/active_record/validations/uniqueness.rb
+++ b/activerecord/lib/active_record/validations/uniqueness.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Validations
class UniquenessValidator < ActiveModel::EachValidator # :nodoc:
@@ -6,19 +8,27 @@ module ActiveRecord
raise ArgumentError, "#{options[:conditions]} was passed as :conditions but is not callable. " \
"Pass a callable instead: `conditions: -> { where(approved: true) }`"
end
+ unless Array(options[:scope]).all? { |scope| scope.respond_to?(:to_sym) }
+ raise ArgumentError, "#{options[:scope]} is not supported format for :scope option. " \
+ "Pass a symbol or an array of symbols instead: `scope: :user_id`"
+ end
super({ case_sensitive: true }.merge!(options))
@klass = options[:class]
end
def validate_each(record, attribute, value)
- return unless should_validate?(record)
finder_class = find_finder_class_for(record)
- table = finder_class.arel_table
value = map_enum_attribute(finder_class, attribute, value)
- relation = build_relation(finder_class, table, attribute, value)
- relation = relation.where.not(finder_class.primary_key => record.id) if record.persisted?
- relation = scope_relation(record, table, relation)
+ relation = build_relation(finder_class, attribute, value)
+ if record.persisted?
+ if finder_class.primary_key
+ relation = relation.where.not(finder_class.primary_key => record.id_in_database)
+ else
+ raise UnknownPrimaryKey.new(finder_class, "Can not validate uniqueness for persisted record without primary key.")
+ end
+ end
+ relation = scope_relation(record, relation)
relation = relation.merge(options[:conditions]) if options[:conditions]
if relation.exists?
@@ -29,13 +39,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
@@ -45,48 +55,42 @@ module ActiveRecord
class_hierarchy.detect { |klass| !klass.abstract_class? }
end
- def build_relation(klass, table, attribute, value) #:nodoc:
+ def build_relation(klass, attribute, value)
if reflection = klass._reflect_on_association(attribute)
attribute = reflection.foreign_key
value = value.attributes[reflection.klass.primary_key] unless value.nil?
end
- attribute_name = attribute.to_s
+ if value.nil?
+ return klass.unscoped.where!(attribute => value)
+ end
# the attribute may be an aliased attribute
- if klass.attribute_aliases[attribute_name]
- attribute = klass.attribute_aliases[attribute_name]
- attribute_name = attribute.to_s
+ if klass.attribute_alias?(attribute)
+ attribute = klass.attribute_alias(attribute)
end
- column = klass.columns_hash[attribute_name]
- cast_type = klass.type_for_attribute(attribute_name)
- value = cast_type.serialize(value)
- value = klass.connection.type_cast(value)
- if value.is_a?(String) && column.limit
- value = value.to_s[0, column.limit]
- end
+ attribute_name = attribute.to_s
+ value = klass.predicate_builder.build_bind_attribute(attribute_name, value)
- value = Arel::Nodes::Quoted.new(value)
+ table = klass.arel_table
+ column = klass.columns_hash[attribute_name]
- comparison = if !options[:case_sensitive] && !value.nil?
+ 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.where(comparison)
- rescue RangeError
- klass.none
+ klass.unscoped.where!(comparison)
end
- def scope_relation(record, table, relation)
+ 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
@@ -166,7 +170,8 @@ module ActiveRecord
#
# === Concurrency and integrity
#
- # Using this validation method in conjunction with ActiveRecord::Base#save
+ # Using this validation method in conjunction with
+ # {ActiveRecord::Base#save}[rdoc-ref:Persistence#save]
# does not guarantee the absence of duplicate record insertions, because
# uniqueness checks on the application level are inherently prone to race
# conditions. For example, suppose that two users try to post a Comment at
@@ -200,21 +205,19 @@ module ActiveRecord
# | # Boom! We now have a duplicate
# | # title!
#
- # This could even happen if you use transactions with the 'serializable'
- # isolation level. The best way to work around this problem is to add a unique
- # index to the database table using
- # ActiveRecord::ConnectionAdapters::SchemaStatements#add_index. In the
- # rare case that a race condition occurs, the database will guarantee
+ # The best way to work around this problem is to add a unique index to the database table using
+ # {connection.add_index}[rdoc-ref:ConnectionAdapters::SchemaStatements#add_index].
+ # In the rare case that a race condition occurs, the database will guarantee
# the field's uniqueness.
#
# When the database catches such a duplicate insertion,
- # ActiveRecord::Base#save will raise an ActiveRecord::StatementInvalid
+ # {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] will raise an ActiveRecord::StatementInvalid
# exception. You can either choose to let this error propagate (which
# will result in the default Rails exception page being shown), or you
# can catch it and restart the transaction (e.g. by telling the user
# that the title already exists, and asking them to re-enter the title).
# This technique is also known as
- # {optimistic concurrency control}[http://en.wikipedia.org/wiki/Optimistic_concurrency_control].
+ # {optimistic concurrency control}[https://en.wikipedia.org/wiki/Optimistic_concurrency_control].
#
# The bundled ActiveRecord::ConnectionAdapters distinguish unique index
# constraint errors from other types of database errors by throwing an
@@ -224,7 +227,6 @@ module ActiveRecord
#
# The following bundled adapters throw the ActiveRecord::RecordNotUnique exception:
#
- # * ActiveRecord::ConnectionAdapters::MysqlAdapter.
# * ActiveRecord::ConnectionAdapters::Mysql2Adapter.
# * ActiveRecord::ConnectionAdapters::SQLite3Adapter.
# * ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.
diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb
index cf76a13b44..6b0d82d8fc 100644
--- a/activerecord/lib/active_record/version.rb
+++ b/activerecord/lib/active_record/version.rb
@@ -1,4 +1,6 @@
-require_relative 'gem_version'
+# frozen_string_literal: true
+
+require_relative "gem_version"
module ActiveRecord
# Returns the version of the currently loaded ActiveRecord as a <tt>Gem::Version</tt>
diff --git a/activerecord/lib/arel.rb b/activerecord/lib/arel.rb
new file mode 100644
index 0000000000..dab785738e
--- /dev/null
+++ b/activerecord/lib/arel.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require "arel/errors"
+
+require "arel/crud"
+require "arel/factory_methods"
+
+require "arel/expressions"
+require "arel/predications"
+require "arel/window_predications"
+require "arel/math"
+require "arel/alias_predication"
+require "arel/order_predications"
+require "arel/table"
+require "arel/attributes"
+require "arel/compatibility/wheres"
+
+require "arel/visitors"
+require "arel/collectors/sql_string"
+
+require "arel/tree_manager"
+require "arel/insert_manager"
+require "arel/select_manager"
+require "arel/update_manager"
+require "arel/delete_manager"
+require "arel/nodes"
+
+module Arel # :nodoc: all
+ VERSION = "10.0.0"
+
+ def self.sql(raw_sql)
+ Arel::Nodes::SqlLiteral.new raw_sql
+ end
+
+ def self.star
+ sql "*"
+ end
+
+ def self.arel_node?(value)
+ value.is_a?(Arel::Node) || value.is_a?(Arel::Attribute) || value.is_a?(Arel::Nodes::SqlLiteral)
+ end
+
+ ## Convenience Alias
+ Node = Arel::Nodes::Node
+end
diff --git a/activerecord/lib/arel/alias_predication.rb b/activerecord/lib/arel/alias_predication.rb
new file mode 100644
index 0000000000..4abbbb7ef6
--- /dev/null
+++ b/activerecord/lib/arel/alias_predication.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module AliasPredication
+ def as(other)
+ Nodes::As.new self, Nodes::SqlLiteral.new(other)
+ end
+ end
+end
diff --git a/activerecord/lib/arel/attributes.rb b/activerecord/lib/arel/attributes.rb
new file mode 100644
index 0000000000..35d586c948
--- /dev/null
+++ b/activerecord/lib/arel/attributes.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require "arel/attributes/attribute"
+
+module Arel # :nodoc: all
+ module Attributes
+ ###
+ # Factory method to wrap a raw database +column+ to an Arel Attribute.
+ def self.for(column)
+ case column.type
+ when :string, :text, :binary then String
+ when :integer then Integer
+ when :float then Float
+ when :decimal then Decimal
+ when :date, :datetime, :timestamp, :time then Time
+ when :boolean then Boolean
+ else
+ Undefined
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/attributes/attribute.rb b/activerecord/lib/arel/attributes/attribute.rb
new file mode 100644
index 0000000000..ecf499a23e
--- /dev/null
+++ b/activerecord/lib/arel/attributes/attribute.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Attributes
+ class Attribute < Struct.new :relation, :name
+ include Arel::Expressions
+ include Arel::Predications
+ include Arel::AliasPredication
+ include Arel::OrderPredications
+ include Arel::Math
+
+ ###
+ # Create a node for lowering this attribute
+ def lower
+ relation.lower self
+ end
+
+ def type_cast_for_database(value)
+ relation.type_cast_for_database(name, value)
+ end
+
+ def able_to_type_cast?
+ relation.able_to_type_cast?
+ end
+ end
+
+ class String < Attribute; end
+ class Time < Attribute; end
+ class Boolean < Attribute; end
+ class Decimal < Attribute; end
+ class Float < Attribute; end
+ class Integer < Attribute; end
+ class Undefined < Attribute; end
+ end
+
+ Attribute = Attributes::Attribute
+end
diff --git a/activerecord/lib/arel/collectors/bind.rb b/activerecord/lib/arel/collectors/bind.rb
new file mode 100644
index 0000000000..6f8912575d
--- /dev/null
+++ b/activerecord/lib/arel/collectors/bind.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Collectors
+ class Bind
+ def initialize
+ @binds = []
+ end
+
+ def <<(str)
+ self
+ end
+
+ def add_bind(bind)
+ @binds << bind
+ self
+ end
+
+ def value
+ @binds
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/collectors/composite.rb b/activerecord/lib/arel/collectors/composite.rb
new file mode 100644
index 0000000000..0533544993
--- /dev/null
+++ b/activerecord/lib/arel/collectors/composite.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Collectors
+ class Composite
+ def initialize(left, right)
+ @left = left
+ @right = right
+ end
+
+ def <<(str)
+ left << str
+ right << str
+ self
+ end
+
+ def add_bind(bind, &block)
+ left.add_bind bind, &block
+ right.add_bind bind, &block
+ self
+ end
+
+ def value
+ [left.value, right.value]
+ end
+
+ private
+ attr_reader :left, :right
+ end
+ end
+end
diff --git a/activerecord/lib/arel/collectors/plain_string.rb b/activerecord/lib/arel/collectors/plain_string.rb
new file mode 100644
index 0000000000..c0e9fff399
--- /dev/null
+++ b/activerecord/lib/arel/collectors/plain_string.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Collectors
+ class PlainString
+ def initialize
+ @str = +""
+ end
+
+ def value
+ @str
+ end
+
+ def <<(str)
+ @str << str
+ self
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/collectors/sql_string.rb b/activerecord/lib/arel/collectors/sql_string.rb
new file mode 100644
index 0000000000..54e1e562c2
--- /dev/null
+++ b/activerecord/lib/arel/collectors/sql_string.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require "arel/collectors/plain_string"
+
+module Arel # :nodoc: all
+ module Collectors
+ class SQLString < PlainString
+ def initialize(*)
+ super
+ @bind_index = 1
+ end
+
+ def add_bind(bind)
+ self << yield(@bind_index)
+ @bind_index += 1
+ self
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/collectors/substitute_binds.rb b/activerecord/lib/arel/collectors/substitute_binds.rb
new file mode 100644
index 0000000000..4b894bc4b1
--- /dev/null
+++ b/activerecord/lib/arel/collectors/substitute_binds.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Collectors
+ class SubstituteBinds
+ def initialize(quoter, delegate_collector)
+ @quoter = quoter
+ @delegate = delegate_collector
+ end
+
+ def <<(str)
+ delegate << str
+ self
+ end
+
+ def add_bind(bind)
+ self << quoter.quote(bind)
+ end
+
+ def value
+ delegate.value
+ end
+
+ private
+ attr_reader :quoter, :delegate
+ end
+ end
+end
diff --git a/activerecord/lib/arel/compatibility/wheres.rb b/activerecord/lib/arel/compatibility/wheres.rb
new file mode 100644
index 0000000000..c8a73f0dae
--- /dev/null
+++ b/activerecord/lib/arel/compatibility/wheres.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Compatibility # :nodoc:
+ class Wheres # :nodoc:
+ include Enumerable
+
+ module Value # :nodoc:
+ attr_accessor :visitor
+ def value
+ visitor.accept self
+ end
+
+ def name
+ super.to_sym
+ end
+ end
+
+ def initialize(engine, collection)
+ @engine = engine
+ @collection = collection
+ end
+
+ def each
+ to_sql = Visitors::ToSql.new @engine
+
+ @collection.each { |c|
+ c.extend(Value)
+ c.visitor = to_sql
+ yield c
+ }
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/crud.rb b/activerecord/lib/arel/crud.rb
new file mode 100644
index 0000000000..e8a563ca4a
--- /dev/null
+++ b/activerecord/lib/arel/crud.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ ###
+ # FIXME hopefully we can remove this
+ module Crud
+ def compile_update(values, pk)
+ um = UpdateManager.new
+
+ if Nodes::SqlLiteral === values
+ relation = @ctx.from
+ else
+ relation = values.first.first.relation
+ end
+ um.key = pk
+ um.table relation
+ um.set values
+ um.take @ast.limit.expr if @ast.limit
+ um.order(*@ast.orders)
+ um.wheres = @ctx.wheres
+ um
+ end
+
+ def compile_insert(values)
+ im = create_insert
+ im.insert values
+ im
+ end
+
+ def create_insert
+ InsertManager.new
+ end
+
+ def compile_delete
+ dm = DeleteManager.new
+ dm.take @ast.limit.expr if @ast.limit
+ dm.wheres = @ctx.wheres
+ dm.from @ctx.froms
+ dm
+ end
+ end
+end
diff --git a/activerecord/lib/arel/delete_manager.rb b/activerecord/lib/arel/delete_manager.rb
new file mode 100644
index 0000000000..fdba937d64
--- /dev/null
+++ b/activerecord/lib/arel/delete_manager.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ class DeleteManager < Arel::TreeManager
+ include TreeManager::StatementMethods
+
+ def initialize
+ super
+ @ast = Nodes::DeleteStatement.new
+ @ctx = @ast
+ end
+
+ def from(relation)
+ @ast.relation = relation
+ self
+ end
+ end
+end
diff --git a/activerecord/lib/arel/errors.rb b/activerecord/lib/arel/errors.rb
new file mode 100644
index 0000000000..2f8d5e3c02
--- /dev/null
+++ b/activerecord/lib/arel/errors.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ class ArelError < StandardError
+ end
+
+ class EmptyJoinError < ArelError
+ end
+end
diff --git a/activerecord/lib/arel/expressions.rb b/activerecord/lib/arel/expressions.rb
new file mode 100644
index 0000000000..da8afb338c
--- /dev/null
+++ b/activerecord/lib/arel/expressions.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Expressions
+ def count(distinct = false)
+ Nodes::Count.new [self], distinct
+ end
+
+ def sum
+ Nodes::Sum.new [self]
+ end
+
+ def maximum
+ Nodes::Max.new [self]
+ end
+
+ def minimum
+ Nodes::Min.new [self]
+ end
+
+ def average
+ Nodes::Avg.new [self]
+ end
+
+ def extract(field)
+ Nodes::Extract.new [self], field
+ end
+ end
+end
diff --git a/activerecord/lib/arel/factory_methods.rb b/activerecord/lib/arel/factory_methods.rb
new file mode 100644
index 0000000000..83ec23e403
--- /dev/null
+++ b/activerecord/lib/arel/factory_methods.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ ###
+ # Methods for creating various nodes
+ module FactoryMethods
+ def create_true
+ Arel::Nodes::True.new
+ end
+
+ def create_false
+ Arel::Nodes::False.new
+ end
+
+ def create_table_alias(relation, name)
+ Nodes::TableAlias.new(relation, name)
+ end
+
+ def create_join(to, constraint = nil, klass = Nodes::InnerJoin)
+ klass.new(to, constraint)
+ end
+
+ def create_string_join(to)
+ create_join to, nil, Nodes::StringJoin
+ end
+
+ def create_and(clauses)
+ Nodes::And.new clauses
+ end
+
+ def create_on(expr)
+ Nodes::On.new expr
+ end
+
+ def grouping(expr)
+ Nodes::Grouping.new expr
+ end
+
+ ###
+ # Create a LOWER() function
+ def lower(column)
+ Nodes::NamedFunction.new "LOWER", [Nodes.build_quoted(column)]
+ end
+
+ def coalesce(*exprs)
+ Nodes::NamedFunction.new "COALESCE", exprs
+ end
+ end
+end
diff --git a/activerecord/lib/arel/insert_manager.rb b/activerecord/lib/arel/insert_manager.rb
new file mode 100644
index 0000000000..c90fc33a48
--- /dev/null
+++ b/activerecord/lib/arel/insert_manager.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ class InsertManager < Arel::TreeManager
+ def initialize
+ super
+ @ast = Nodes::InsertStatement.new
+ end
+
+ def into(table)
+ @ast.relation = table
+ self
+ end
+
+ def columns; @ast.columns end
+ def values=(val); @ast.values = val; end
+
+ def select(select)
+ @ast.select = select
+ end
+
+ def insert(fields)
+ return if fields.empty?
+
+ if String === fields
+ @ast.values = Nodes::SqlLiteral.new(fields)
+ else
+ @ast.relation ||= fields.first.first.relation
+
+ values = []
+
+ fields.each do |column, value|
+ @ast.columns << column
+ values << value
+ end
+ @ast.values = create_values values, @ast.columns
+ end
+ self
+ end
+
+ def create_values(values, columns)
+ Nodes::Values.new values, columns
+ end
+
+ def create_values_list(rows)
+ Nodes::ValuesList.new(rows)
+ end
+ end
+end
diff --git a/activerecord/lib/arel/math.rb b/activerecord/lib/arel/math.rb
new file mode 100644
index 0000000000..2359f13148
--- /dev/null
+++ b/activerecord/lib/arel/math.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Math
+ def *(other)
+ Arel::Nodes::Multiplication.new(self, other)
+ end
+
+ def +(other)
+ Arel::Nodes::Grouping.new(Arel::Nodes::Addition.new(self, other))
+ end
+
+ def -(other)
+ Arel::Nodes::Grouping.new(Arel::Nodes::Subtraction.new(self, other))
+ end
+
+ def /(other)
+ Arel::Nodes::Division.new(self, other)
+ end
+
+ def &(other)
+ Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseAnd.new(self, other))
+ end
+
+ def |(other)
+ Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseOr.new(self, other))
+ end
+
+ def ^(other)
+ Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseXor.new(self, other))
+ end
+
+ def <<(other)
+ Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseShiftLeft.new(self, other))
+ end
+
+ def >>(other)
+ Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseShiftRight.new(self, other))
+ end
+
+ def ~@
+ Arel::Nodes::BitwiseNot.new(self)
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes.rb b/activerecord/lib/arel/nodes.rb
new file mode 100644
index 0000000000..5af0e532e2
--- /dev/null
+++ b/activerecord/lib/arel/nodes.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+# node
+require "arel/nodes/node"
+require "arel/nodes/node_expression"
+require "arel/nodes/select_statement"
+require "arel/nodes/select_core"
+require "arel/nodes/insert_statement"
+require "arel/nodes/update_statement"
+require "arel/nodes/bind_param"
+
+# terminal
+
+require "arel/nodes/terminal"
+require "arel/nodes/true"
+require "arel/nodes/false"
+
+# unary
+require "arel/nodes/unary"
+require "arel/nodes/grouping"
+require "arel/nodes/ascending"
+require "arel/nodes/descending"
+require "arel/nodes/unqualified_column"
+require "arel/nodes/with"
+
+# binary
+require "arel/nodes/binary"
+require "arel/nodes/equality"
+require "arel/nodes/in" # Why is this subclassed from equality?
+require "arel/nodes/join_source"
+require "arel/nodes/delete_statement"
+require "arel/nodes/table_alias"
+require "arel/nodes/infix_operation"
+require "arel/nodes/unary_operation"
+require "arel/nodes/over"
+require "arel/nodes/matches"
+require "arel/nodes/regexp"
+
+# nary
+require "arel/nodes/and"
+
+# function
+# FIXME: Function + Alias can be rewritten as a Function and Alias node.
+# We should make Function a Unary node and deprecate the use of "aliaz"
+require "arel/nodes/function"
+require "arel/nodes/count"
+require "arel/nodes/extract"
+require "arel/nodes/values"
+require "arel/nodes/values_list"
+require "arel/nodes/named_function"
+
+# windows
+require "arel/nodes/window"
+
+# conditional expressions
+require "arel/nodes/case"
+
+# joins
+require "arel/nodes/full_outer_join"
+require "arel/nodes/inner_join"
+require "arel/nodes/outer_join"
+require "arel/nodes/right_outer_join"
+require "arel/nodes/string_join"
+
+require "arel/nodes/sql_literal"
+
+require "arel/nodes/casted"
diff --git a/activerecord/lib/arel/nodes/and.rb b/activerecord/lib/arel/nodes/and.rb
new file mode 100644
index 0000000000..c530a77bfb
--- /dev/null
+++ b/activerecord/lib/arel/nodes/and.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class And < Arel::Nodes::Node
+ attr_reader :children
+
+ def initialize(children)
+ super()
+ @children = children
+ end
+
+ def left
+ children.first
+ end
+
+ def right
+ children[1]
+ end
+
+ def hash
+ children.hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.children == other.children
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/ascending.rb b/activerecord/lib/arel/nodes/ascending.rb
new file mode 100644
index 0000000000..8b617f4df5
--- /dev/null
+++ b/activerecord/lib/arel/nodes/ascending.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Ascending < Ordering
+ def reverse
+ Descending.new(expr)
+ end
+
+ def direction
+ :asc
+ end
+
+ def ascending?
+ true
+ end
+
+ def descending?
+ false
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/binary.rb b/activerecord/lib/arel/nodes/binary.rb
new file mode 100644
index 0000000000..e184e99c73
--- /dev/null
+++ b/activerecord/lib/arel/nodes/binary.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Binary < Arel::Nodes::NodeExpression
+ attr_accessor :left, :right
+
+ def initialize(left, right)
+ super()
+ @left = left
+ @right = right
+ end
+
+ def initialize_copy(other)
+ super
+ @left = @left.clone if @left
+ @right = @right.clone if @right
+ end
+
+ def hash
+ [self.class, @left, @right].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.left == other.left &&
+ self.right == other.right
+ end
+ alias :== :eql?
+ end
+
+ %w{
+ As
+ Assignment
+ Between
+ GreaterThan
+ GreaterThanOrEqual
+ Join
+ LessThan
+ LessThanOrEqual
+ NotEqual
+ NotIn
+ Or
+ Union
+ UnionAll
+ Intersect
+ Except
+ }.each do |name|
+ const_set name, Class.new(Binary)
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/bind_param.rb b/activerecord/lib/arel/nodes/bind_param.rb
new file mode 100644
index 0000000000..ba8340558a
--- /dev/null
+++ b/activerecord/lib/arel/nodes/bind_param.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class BindParam < Node
+ attr_reader :value
+
+ def initialize(value)
+ @value = value
+ super()
+ end
+
+ def hash
+ [self.class, self.value].hash
+ end
+
+ def eql?(other)
+ other.is_a?(BindParam) &&
+ value == other.value
+ end
+ alias :== :eql?
+
+ def nil?
+ value.nil?
+ end
+
+ def boundable?
+ !value.respond_to?(:boundable?) || value.boundable?
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/case.rb b/activerecord/lib/arel/nodes/case.rb
new file mode 100644
index 0000000000..654a54825e
--- /dev/null
+++ b/activerecord/lib/arel/nodes/case.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Case < Arel::Nodes::Node
+ attr_accessor :case, :conditions, :default
+
+ def initialize(expression = nil, default = nil)
+ @case = expression
+ @conditions = []
+ @default = default
+ end
+
+ def when(condition, expression = nil)
+ @conditions << When.new(Nodes.build_quoted(condition), expression)
+ self
+ end
+
+ def then(expression)
+ @conditions.last.right = Nodes.build_quoted(expression)
+ self
+ end
+
+ def else(expression)
+ @default = Else.new Nodes.build_quoted(expression)
+ self
+ end
+
+ def initialize_copy(other)
+ super
+ @case = @case.clone if @case
+ @conditions = @conditions.map { |x| x.clone }
+ @default = @default.clone if @default
+ end
+
+ def hash
+ [@case, @conditions, @default].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.case == other.case &&
+ self.conditions == other.conditions &&
+ self.default == other.default
+ end
+ alias :== :eql?
+ end
+
+ class When < Binary # :nodoc:
+ end
+
+ class Else < Unary # :nodoc:
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/casted.rb b/activerecord/lib/arel/nodes/casted.rb
new file mode 100644
index 0000000000..c1e6e97d6d
--- /dev/null
+++ b/activerecord/lib/arel/nodes/casted.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Casted < Arel::Nodes::NodeExpression # :nodoc:
+ attr_reader :val, :attribute
+ def initialize(val, attribute)
+ @val = val
+ @attribute = attribute
+ super()
+ end
+
+ def nil?; @val.nil?; end
+
+ def hash
+ [self.class, val, attribute].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.val == other.val &&
+ self.attribute == other.attribute
+ end
+ alias :== :eql?
+ end
+
+ class Quoted < Arel::Nodes::Unary # :nodoc:
+ alias :val :value
+ def nil?; val.nil?; end
+ end
+
+ def self.build_quoted(other, attribute = nil)
+ case other
+ when Arel::Nodes::Node, Arel::Attributes::Attribute, Arel::Table, Arel::Nodes::BindParam, Arel::SelectManager, Arel::Nodes::Quoted, Arel::Nodes::SqlLiteral
+ other
+ else
+ case attribute
+ when Arel::Attributes::Attribute
+ Casted.new other, attribute
+ else
+ Quoted.new other
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/count.rb b/activerecord/lib/arel/nodes/count.rb
new file mode 100644
index 0000000000..880464639d
--- /dev/null
+++ b/activerecord/lib/arel/nodes/count.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Count < Arel::Nodes::Function
+ def initialize(expr, distinct = false, aliaz = nil)
+ super(expr, aliaz)
+ @distinct = distinct
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/delete_statement.rb b/activerecord/lib/arel/nodes/delete_statement.rb
new file mode 100644
index 0000000000..a419975335
--- /dev/null
+++ b/activerecord/lib/arel/nodes/delete_statement.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class DeleteStatement < Arel::Nodes::Node
+ attr_accessor :left, :right, :orders, :limit, :offset, :key
+
+ alias :relation :left
+ alias :relation= :left=
+ alias :wheres :right
+ alias :wheres= :right=
+
+ def initialize(relation = nil, wheres = [])
+ super()
+ @left = relation
+ @right = wheres
+ @orders = []
+ @limit = nil
+ @offset = nil
+ @key = nil
+ end
+
+ def initialize_copy(other)
+ super
+ @left = @left.clone if @left
+ @right = @right.clone if @right
+ end
+
+ def hash
+ [self.class, @left, @right, @orders, @limit, @offset, @key].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.left == other.left &&
+ self.right == other.right &&
+ self.orders == other.orders &&
+ self.limit == other.limit &&
+ self.offset == other.offset &&
+ self.key == other.key
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/descending.rb b/activerecord/lib/arel/nodes/descending.rb
new file mode 100644
index 0000000000..f3f6992ca8
--- /dev/null
+++ b/activerecord/lib/arel/nodes/descending.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Descending < Ordering
+ def reverse
+ Ascending.new(expr)
+ end
+
+ def direction
+ :desc
+ end
+
+ def ascending?
+ false
+ end
+
+ def descending?
+ true
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/equality.rb b/activerecord/lib/arel/nodes/equality.rb
new file mode 100644
index 0000000000..551d56c2ff
--- /dev/null
+++ b/activerecord/lib/arel/nodes/equality.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Equality < Arel::Nodes::Binary
+ def operator; :== end
+ alias :operand1 :left
+ alias :operand2 :right
+ end
+
+ %w{
+ IsDistinctFrom
+ IsNotDistinctFrom
+ }.each do |name|
+ const_set name, Class.new(Equality)
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/extract.rb b/activerecord/lib/arel/nodes/extract.rb
new file mode 100644
index 0000000000..5799ee9b8f
--- /dev/null
+++ b/activerecord/lib/arel/nodes/extract.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Extract < Arel::Nodes::Unary
+ attr_accessor :field
+
+ def initialize(expr, field)
+ super(expr)
+ @field = field
+ end
+
+ def hash
+ super ^ @field.hash
+ end
+
+ def eql?(other)
+ super &&
+ self.field == other.field
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/false.rb b/activerecord/lib/arel/nodes/false.rb
new file mode 100644
index 0000000000..1e5bf04be5
--- /dev/null
+++ b/activerecord/lib/arel/nodes/false.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class False < Arel::Nodes::NodeExpression
+ def hash
+ self.class.hash
+ end
+
+ def eql?(other)
+ self.class == other.class
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/full_outer_join.rb b/activerecord/lib/arel/nodes/full_outer_join.rb
new file mode 100644
index 0000000000..91bb81f2e3
--- /dev/null
+++ b/activerecord/lib/arel/nodes/full_outer_join.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class FullOuterJoin < Arel::Nodes::Join
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/function.rb b/activerecord/lib/arel/nodes/function.rb
new file mode 100644
index 0000000000..0a439b39f5
--- /dev/null
+++ b/activerecord/lib/arel/nodes/function.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Function < Arel::Nodes::NodeExpression
+ include Arel::WindowPredications
+ attr_accessor :expressions, :alias, :distinct
+
+ def initialize(expr, aliaz = nil)
+ super()
+ @expressions = expr
+ @alias = aliaz && SqlLiteral.new(aliaz)
+ @distinct = false
+ end
+
+ def as(aliaz)
+ self.alias = SqlLiteral.new(aliaz)
+ self
+ end
+
+ def hash
+ [@expressions, @alias, @distinct].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.expressions == other.expressions &&
+ self.alias == other.alias &&
+ self.distinct == other.distinct
+ end
+ alias :== :eql?
+ end
+
+ %w{
+ Sum
+ Exists
+ Max
+ Min
+ Avg
+ }.each do |name|
+ const_set(name, Class.new(Function))
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/grouping.rb b/activerecord/lib/arel/nodes/grouping.rb
new file mode 100644
index 0000000000..4d0bd69d4d
--- /dev/null
+++ b/activerecord/lib/arel/nodes/grouping.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Grouping < Unary
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/in.rb b/activerecord/lib/arel/nodes/in.rb
new file mode 100644
index 0000000000..2be45d6f99
--- /dev/null
+++ b/activerecord/lib/arel/nodes/in.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class In < Equality
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/infix_operation.rb b/activerecord/lib/arel/nodes/infix_operation.rb
new file mode 100644
index 0000000000..bc7e20dcc6
--- /dev/null
+++ b/activerecord/lib/arel/nodes/infix_operation.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class InfixOperation < Binary
+ include Arel::Expressions
+ include Arel::Predications
+ include Arel::OrderPredications
+ include Arel::AliasPredication
+ include Arel::Math
+
+ attr_reader :operator
+
+ def initialize(operator, left, right)
+ super(left, right)
+ @operator = operator
+ end
+ end
+
+ class Multiplication < InfixOperation
+ def initialize(left, right)
+ super(:*, left, right)
+ end
+ end
+
+ class Division < InfixOperation
+ def initialize(left, right)
+ super(:/, left, right)
+ end
+ end
+
+ class Addition < InfixOperation
+ def initialize(left, right)
+ super(:+, left, right)
+ end
+ end
+
+ class Subtraction < InfixOperation
+ def initialize(left, right)
+ super(:-, left, right)
+ end
+ end
+
+ class Concat < InfixOperation
+ def initialize(left, right)
+ super("||", left, right)
+ end
+ end
+
+ class BitwiseAnd < InfixOperation
+ def initialize(left, right)
+ super(:&, left, right)
+ end
+ end
+
+ class BitwiseOr < InfixOperation
+ def initialize(left, right)
+ super(:|, left, right)
+ end
+ end
+
+ class BitwiseXor < InfixOperation
+ def initialize(left, right)
+ super(:^, left, right)
+ end
+ end
+
+ class BitwiseShiftLeft < InfixOperation
+ def initialize(left, right)
+ super(:<<, left, right)
+ end
+ end
+
+ class BitwiseShiftRight < InfixOperation
+ def initialize(left, right)
+ super(:>>, left, right)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/inner_join.rb b/activerecord/lib/arel/nodes/inner_join.rb
new file mode 100644
index 0000000000..519fafad09
--- /dev/null
+++ b/activerecord/lib/arel/nodes/inner_join.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class InnerJoin < Arel::Nodes::Join
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/insert_statement.rb b/activerecord/lib/arel/nodes/insert_statement.rb
new file mode 100644
index 0000000000..d28fd1f6c8
--- /dev/null
+++ b/activerecord/lib/arel/nodes/insert_statement.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class InsertStatement < Arel::Nodes::Node
+ attr_accessor :relation, :columns, :values, :select
+
+ def initialize
+ super()
+ @relation = nil
+ @columns = []
+ @values = nil
+ @select = nil
+ end
+
+ def initialize_copy(other)
+ super
+ @columns = @columns.clone
+ @values = @values.clone if @values
+ @select = @select.clone if @select
+ end
+
+ def hash
+ [@relation, @columns, @values, @select].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.relation == other.relation &&
+ self.columns == other.columns &&
+ self.select == other.select &&
+ self.values == other.values
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/join_source.rb b/activerecord/lib/arel/nodes/join_source.rb
new file mode 100644
index 0000000000..abf0944623
--- /dev/null
+++ b/activerecord/lib/arel/nodes/join_source.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ ###
+ # Class that represents a join source
+ #
+ # http://www.sqlite.org/syntaxdiagrams.html#join-source
+
+ class JoinSource < Arel::Nodes::Binary
+ def initialize(single_source, joinop = [])
+ super
+ end
+
+ def empty?
+ !left && right.empty?
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/matches.rb b/activerecord/lib/arel/nodes/matches.rb
new file mode 100644
index 0000000000..fd5734f4bd
--- /dev/null
+++ b/activerecord/lib/arel/nodes/matches.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Matches < Binary
+ attr_reader :escape
+ attr_accessor :case_sensitive
+
+ def initialize(left, right, escape = nil, case_sensitive = false)
+ super(left, right)
+ @escape = escape && Nodes.build_quoted(escape)
+ @case_sensitive = case_sensitive
+ end
+ end
+
+ class DoesNotMatch < Matches; end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/named_function.rb b/activerecord/lib/arel/nodes/named_function.rb
new file mode 100644
index 0000000000..126462d6d6
--- /dev/null
+++ b/activerecord/lib/arel/nodes/named_function.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class NamedFunction < Arel::Nodes::Function
+ attr_accessor :name
+
+ def initialize(name, expr, aliaz = nil)
+ super(expr, aliaz)
+ @name = name
+ end
+
+ def hash
+ super ^ @name.hash
+ end
+
+ def eql?(other)
+ super && self.name == other.name
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/node.rb b/activerecord/lib/arel/nodes/node.rb
new file mode 100644
index 0000000000..8086102bde
--- /dev/null
+++ b/activerecord/lib/arel/nodes/node.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ ###
+ # Abstract base class for all AST nodes
+ class Node
+ include Arel::FactoryMethods
+ include Enumerable
+
+ ###
+ # Factory method to create a Nodes::Not node that has the recipient of
+ # the caller as a child.
+ def not
+ Nodes::Not.new self
+ end
+
+ ###
+ # Factory method to create a Nodes::Grouping node that has an Nodes::Or
+ # node as a child.
+ def or(right)
+ Nodes::Grouping.new Nodes::Or.new(self, right)
+ end
+
+ ###
+ # Factory method to create an Nodes::And node.
+ def and(right)
+ Nodes::And.new [self, right]
+ end
+
+ # FIXME: this method should go away. I don't like people calling
+ # to_sql on non-head nodes. This forces us to walk the AST until we
+ # can find a node that has a "relation" member.
+ #
+ # Maybe we should just use `Table.engine`? :'(
+ def to_sql(engine = Table.engine)
+ collector = Arel::Collectors::SQLString.new
+ collector = engine.connection.visitor.accept self, collector
+ collector.value
+ end
+
+ # Iterate through AST, nodes will be yielded depth-first
+ def each(&block)
+ return enum_for(:each) unless block_given?
+
+ ::Arel::Visitors::DepthFirst.new(block).accept self
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/node_expression.rb b/activerecord/lib/arel/nodes/node_expression.rb
new file mode 100644
index 0000000000..cbcfaba37c
--- /dev/null
+++ b/activerecord/lib/arel/nodes/node_expression.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class NodeExpression < Arel::Nodes::Node
+ include Arel::Expressions
+ include Arel::Predications
+ include Arel::AliasPredication
+ include Arel::OrderPredications
+ include Arel::Math
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/outer_join.rb b/activerecord/lib/arel/nodes/outer_join.rb
new file mode 100644
index 0000000000..0a3042be61
--- /dev/null
+++ b/activerecord/lib/arel/nodes/outer_join.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class OuterJoin < Arel::Nodes::Join
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/over.rb b/activerecord/lib/arel/nodes/over.rb
new file mode 100644
index 0000000000..91176764a9
--- /dev/null
+++ b/activerecord/lib/arel/nodes/over.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Over < Binary
+ include Arel::AliasPredication
+
+ def initialize(left, right = nil)
+ super(left, right)
+ end
+
+ def operator; "OVER" end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/regexp.rb b/activerecord/lib/arel/nodes/regexp.rb
new file mode 100644
index 0000000000..7c25095569
--- /dev/null
+++ b/activerecord/lib/arel/nodes/regexp.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Regexp < Binary
+ attr_accessor :case_sensitive
+
+ def initialize(left, right, case_sensitive = true)
+ super(left, right)
+ @case_sensitive = case_sensitive
+ end
+ end
+
+ class NotRegexp < Regexp; end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/right_outer_join.rb b/activerecord/lib/arel/nodes/right_outer_join.rb
new file mode 100644
index 0000000000..04ed4aaa78
--- /dev/null
+++ b/activerecord/lib/arel/nodes/right_outer_join.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class RightOuterJoin < Arel::Nodes::Join
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/select_core.rb b/activerecord/lib/arel/nodes/select_core.rb
new file mode 100644
index 0000000000..73461ff683
--- /dev/null
+++ b/activerecord/lib/arel/nodes/select_core.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class SelectCore < Arel::Nodes::Node
+ attr_accessor :projections, :wheres, :groups, :windows
+ attr_accessor :havings, :source, :set_quantifier
+
+ def initialize
+ super()
+ @source = JoinSource.new nil
+
+ # https://ronsavage.github.io/SQL/sql-92.bnf.html#set%20quantifier
+ @set_quantifier = nil
+ @projections = []
+ @wheres = []
+ @groups = []
+ @havings = []
+ @windows = []
+ end
+
+ def from
+ @source.left
+ end
+
+ def from=(value)
+ @source.left = value
+ end
+
+ alias :froms= :from=
+ alias :froms :from
+
+ def initialize_copy(other)
+ super
+ @source = @source.clone if @source
+ @projections = @projections.clone
+ @wheres = @wheres.clone
+ @groups = @groups.clone
+ @havings = @havings.clone
+ @windows = @windows.clone
+ end
+
+ def hash
+ [
+ @source, @set_quantifier, @projections,
+ @wheres, @groups, @havings, @windows
+ ].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.source == other.source &&
+ self.set_quantifier == other.set_quantifier &&
+ self.projections == other.projections &&
+ self.wheres == other.wheres &&
+ self.groups == other.groups &&
+ self.havings == other.havings &&
+ self.windows == other.windows
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/select_statement.rb b/activerecord/lib/arel/nodes/select_statement.rb
new file mode 100644
index 0000000000..eff5dad939
--- /dev/null
+++ b/activerecord/lib/arel/nodes/select_statement.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class SelectStatement < Arel::Nodes::NodeExpression
+ attr_reader :cores
+ attr_accessor :limit, :orders, :lock, :offset, :with
+
+ def initialize(cores = [SelectCore.new])
+ super()
+ @cores = cores
+ @orders = []
+ @limit = nil
+ @lock = nil
+ @offset = nil
+ @with = nil
+ end
+
+ def initialize_copy(other)
+ super
+ @cores = @cores.map { |x| x.clone }
+ @orders = @orders.map { |x| x.clone }
+ end
+
+ def hash
+ [@cores, @orders, @limit, @lock, @offset, @with].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.cores == other.cores &&
+ self.orders == other.orders &&
+ self.limit == other.limit &&
+ self.lock == other.lock &&
+ self.offset == other.offset &&
+ self.with == other.with
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/sql_literal.rb b/activerecord/lib/arel/nodes/sql_literal.rb
new file mode 100644
index 0000000000..d25a8521b7
--- /dev/null
+++ b/activerecord/lib/arel/nodes/sql_literal.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class SqlLiteral < String
+ include Arel::Expressions
+ include Arel::Predications
+ include Arel::AliasPredication
+ include Arel::OrderPredications
+
+ def encode_with(coder)
+ coder.scalar = self.to_s
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/string_join.rb b/activerecord/lib/arel/nodes/string_join.rb
new file mode 100644
index 0000000000..86027fcab7
--- /dev/null
+++ b/activerecord/lib/arel/nodes/string_join.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class StringJoin < Arel::Nodes::Join
+ def initialize(left, right = nil)
+ super
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/table_alias.rb b/activerecord/lib/arel/nodes/table_alias.rb
new file mode 100644
index 0000000000..f95ca16a3d
--- /dev/null
+++ b/activerecord/lib/arel/nodes/table_alias.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class TableAlias < Arel::Nodes::Binary
+ alias :name :right
+ alias :relation :left
+ alias :table_alias :name
+
+ def [](name)
+ Attribute.new(self, name)
+ end
+
+ def table_name
+ relation.respond_to?(:name) ? relation.name : name
+ end
+
+ def type_cast_for_database(*args)
+ relation.type_cast_for_database(*args)
+ end
+
+ def able_to_type_cast?
+ relation.respond_to?(:able_to_type_cast?) && relation.able_to_type_cast?
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/terminal.rb b/activerecord/lib/arel/nodes/terminal.rb
new file mode 100644
index 0000000000..d84c453f1a
--- /dev/null
+++ b/activerecord/lib/arel/nodes/terminal.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Distinct < Arel::Nodes::NodeExpression
+ def hash
+ self.class.hash
+ end
+
+ def eql?(other)
+ self.class == other.class
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/true.rb b/activerecord/lib/arel/nodes/true.rb
new file mode 100644
index 0000000000..c891012969
--- /dev/null
+++ b/activerecord/lib/arel/nodes/true.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class True < Arel::Nodes::NodeExpression
+ def hash
+ self.class.hash
+ end
+
+ def eql?(other)
+ self.class == other.class
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/unary.rb b/activerecord/lib/arel/nodes/unary.rb
new file mode 100644
index 0000000000..00639304e4
--- /dev/null
+++ b/activerecord/lib/arel/nodes/unary.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Unary < Arel::Nodes::NodeExpression
+ attr_accessor :expr
+ alias :value :expr
+
+ def initialize(expr)
+ super()
+ @expr = expr
+ end
+
+ def hash
+ @expr.hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.expr == other.expr
+ end
+ alias :== :eql?
+ end
+
+ %w{
+ Bin
+ Cube
+ DistinctOn
+ Group
+ GroupingElement
+ GroupingSet
+ Lateral
+ Limit
+ Lock
+ Not
+ Offset
+ On
+ Ordering
+ RollUp
+ }.each do |name|
+ const_set(name, Class.new(Unary))
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/unary_operation.rb b/activerecord/lib/arel/nodes/unary_operation.rb
new file mode 100644
index 0000000000..524282ac84
--- /dev/null
+++ b/activerecord/lib/arel/nodes/unary_operation.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class UnaryOperation < Unary
+ attr_reader :operator
+
+ def initialize(operator, operand)
+ super(operand)
+ @operator = operator
+ end
+ end
+
+ class BitwiseNot < UnaryOperation
+ def initialize(operand)
+ super(:~, operand)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/unqualified_column.rb b/activerecord/lib/arel/nodes/unqualified_column.rb
new file mode 100644
index 0000000000..7c3e0720d7
--- /dev/null
+++ b/activerecord/lib/arel/nodes/unqualified_column.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class UnqualifiedColumn < Arel::Nodes::Unary
+ alias :attribute :expr
+ alias :attribute= :expr=
+
+ def relation
+ @expr.relation
+ end
+
+ def column
+ @expr.column
+ end
+
+ def name
+ @expr.name
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/update_statement.rb b/activerecord/lib/arel/nodes/update_statement.rb
new file mode 100644
index 0000000000..cfaa19e392
--- /dev/null
+++ b/activerecord/lib/arel/nodes/update_statement.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class UpdateStatement < Arel::Nodes::Node
+ attr_accessor :relation, :wheres, :values, :orders, :limit, :offset, :key
+
+ def initialize
+ @relation = nil
+ @wheres = []
+ @values = []
+ @orders = []
+ @limit = nil
+ @offset = nil
+ @key = nil
+ end
+
+ def initialize_copy(other)
+ super
+ @wheres = @wheres.clone
+ @values = @values.clone
+ end
+
+ def hash
+ [@relation, @wheres, @values, @orders, @limit, @offset, @key].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.relation == other.relation &&
+ self.wheres == other.wheres &&
+ self.values == other.values &&
+ self.orders == other.orders &&
+ self.limit == other.limit &&
+ self.offset == other.offset &&
+ self.key == other.key
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/values.rb b/activerecord/lib/arel/nodes/values.rb
new file mode 100644
index 0000000000..650248dc04
--- /dev/null
+++ b/activerecord/lib/arel/nodes/values.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Values < Arel::Nodes::Binary
+ alias :expressions :left
+ alias :expressions= :left=
+ alias :columns :right
+ alias :columns= :right=
+
+ def initialize(exprs, columns = [])
+ super
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/values_list.rb b/activerecord/lib/arel/nodes/values_list.rb
new file mode 100644
index 0000000000..27109848e4
--- /dev/null
+++ b/activerecord/lib/arel/nodes/values_list.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class ValuesList < Node
+ attr_reader :rows
+
+ def initialize(rows)
+ @rows = rows
+ super()
+ end
+
+ def hash
+ @rows.hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.rows == other.rows
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/window.rb b/activerecord/lib/arel/nodes/window.rb
new file mode 100644
index 0000000000..4916fc7fbe
--- /dev/null
+++ b/activerecord/lib/arel/nodes/window.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Window < Arel::Nodes::Node
+ attr_accessor :orders, :framing, :partitions
+
+ def initialize
+ @orders = []
+ @partitions = []
+ @framing = nil
+ end
+
+ def order(*expr)
+ # FIXME: We SHOULD NOT be converting these to SqlLiteral automatically
+ @orders.concat expr.map { |x|
+ String === x || Symbol === x ? Nodes::SqlLiteral.new(x.to_s) : x
+ }
+ self
+ end
+
+ def partition(*expr)
+ # FIXME: We SHOULD NOT be converting these to SqlLiteral automatically
+ @partitions.concat expr.map { |x|
+ String === x || Symbol === x ? Nodes::SqlLiteral.new(x.to_s) : x
+ }
+ self
+ end
+
+ def frame(expr)
+ @framing = expr
+ end
+
+ def rows(expr = nil)
+ if @framing
+ Rows.new(expr)
+ else
+ frame(Rows.new(expr))
+ end
+ end
+
+ def range(expr = nil)
+ if @framing
+ Range.new(expr)
+ else
+ frame(Range.new(expr))
+ end
+ end
+
+ def initialize_copy(other)
+ super
+ @orders = @orders.map { |x| x.clone }
+ end
+
+ def hash
+ [@orders, @framing].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.orders == other.orders &&
+ self.framing == other.framing &&
+ self.partitions == other.partitions
+ end
+ alias :== :eql?
+ end
+
+ class NamedWindow < Window
+ attr_accessor :name
+
+ def initialize(name)
+ super()
+ @name = name
+ end
+
+ def initialize_copy(other)
+ super
+ @name = other.name.clone
+ end
+
+ def hash
+ super ^ @name.hash
+ end
+
+ def eql?(other)
+ super && self.name == other.name
+ end
+ alias :== :eql?
+ end
+
+ class Rows < Unary
+ def initialize(expr = nil)
+ super(expr)
+ end
+ end
+
+ class Range < Unary
+ def initialize(expr = nil)
+ super(expr)
+ end
+ end
+
+ class CurrentRow < Node
+ def hash
+ self.class.hash
+ end
+
+ def eql?(other)
+ self.class == other.class
+ end
+ alias :== :eql?
+ end
+
+ class Preceding < Unary
+ def initialize(expr = nil)
+ super(expr)
+ end
+ end
+
+ class Following < Unary
+ def initialize(expr = nil)
+ super(expr)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/with.rb b/activerecord/lib/arel/nodes/with.rb
new file mode 100644
index 0000000000..157bdcaa08
--- /dev/null
+++ b/activerecord/lib/arel/nodes/with.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class With < Arel::Nodes::Unary
+ alias children expr
+ end
+
+ class WithRecursive < With; end
+ end
+end
diff --git a/activerecord/lib/arel/order_predications.rb b/activerecord/lib/arel/order_predications.rb
new file mode 100644
index 0000000000..d785bbba92
--- /dev/null
+++ b/activerecord/lib/arel/order_predications.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module OrderPredications
+ def asc
+ Nodes::Ascending.new self
+ end
+
+ def desc
+ Nodes::Descending.new self
+ end
+ end
+end
diff --git a/activerecord/lib/arel/predications.rb b/activerecord/lib/arel/predications.rb
new file mode 100644
index 0000000000..77502dd199
--- /dev/null
+++ b/activerecord/lib/arel/predications.rb
@@ -0,0 +1,249 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Predications
+ def not_eq(other)
+ Nodes::NotEqual.new self, quoted_node(other)
+ end
+
+ def not_eq_any(others)
+ grouping_any :not_eq, others
+ end
+
+ def not_eq_all(others)
+ grouping_all :not_eq, others
+ end
+
+ def eq(other)
+ Nodes::Equality.new self, quoted_node(other)
+ end
+
+ def is_not_distinct_from(other)
+ Nodes::IsNotDistinctFrom.new self, quoted_node(other)
+ end
+
+ def is_distinct_from(other)
+ Nodes::IsDistinctFrom.new self, quoted_node(other)
+ end
+
+ def eq_any(others)
+ grouping_any :eq, others
+ end
+
+ def eq_all(others)
+ grouping_all :eq, quoted_array(others)
+ end
+
+ def between(other)
+ if equals_quoted?(other.begin, -Float::INFINITY)
+ if equals_quoted?(other.end, Float::INFINITY)
+ not_in([])
+ elsif other.exclude_end?
+ lt(other.end)
+ else
+ lteq(other.end)
+ end
+ elsif equals_quoted?(other.end, Float::INFINITY)
+ gteq(other.begin)
+ elsif other.exclude_end?
+ gteq(other.begin).and(lt(other.end))
+ else
+ left = quoted_node(other.begin)
+ right = quoted_node(other.end)
+ Nodes::Between.new(self, left.and(right))
+ end
+ end
+
+ def in(other)
+ case other
+ when Arel::SelectManager
+ Arel::Nodes::In.new(self, other.ast)
+ when Range
+ if $VERBOSE
+ warn <<-eowarn
+Passing a range to `#in` is deprecated. Call `#between`, instead.
+ eowarn
+ end
+ between(other)
+ when Enumerable
+ Nodes::In.new self, quoted_array(other)
+ else
+ Nodes::In.new self, quoted_node(other)
+ end
+ end
+
+ def in_any(others)
+ grouping_any :in, others
+ end
+
+ def in_all(others)
+ grouping_all :in, others
+ end
+
+ def not_between(other)
+ if equals_quoted?(other.begin, -Float::INFINITY)
+ if equals_quoted?(other.end, Float::INFINITY)
+ self.in([])
+ elsif other.exclude_end?
+ gteq(other.end)
+ else
+ gt(other.end)
+ end
+ elsif equals_quoted?(other.end, Float::INFINITY)
+ lt(other.begin)
+ else
+ left = lt(other.begin)
+ right = if other.exclude_end?
+ gteq(other.end)
+ else
+ gt(other.end)
+ end
+ left.or(right)
+ end
+ end
+
+ def not_in(other)
+ case other
+ when Arel::SelectManager
+ Arel::Nodes::NotIn.new(self, other.ast)
+ when Range
+ if $VERBOSE
+ warn <<-eowarn
+Passing a range to `#not_in` is deprecated. Call `#not_between`, instead.
+ eowarn
+ end
+ not_between(other)
+ when Enumerable
+ Nodes::NotIn.new self, quoted_array(other)
+ else
+ Nodes::NotIn.new self, quoted_node(other)
+ end
+ end
+
+ def not_in_any(others)
+ grouping_any :not_in, others
+ end
+
+ def not_in_all(others)
+ grouping_all :not_in, others
+ end
+
+ def matches(other, escape = nil, case_sensitive = false)
+ Nodes::Matches.new self, quoted_node(other), escape, case_sensitive
+ end
+
+ def matches_regexp(other, case_sensitive = true)
+ Nodes::Regexp.new self, quoted_node(other), case_sensitive
+ end
+
+ def matches_any(others, escape = nil, case_sensitive = false)
+ grouping_any :matches, others, escape, case_sensitive
+ end
+
+ def matches_all(others, escape = nil, case_sensitive = false)
+ grouping_all :matches, others, escape, case_sensitive
+ end
+
+ def does_not_match(other, escape = nil, case_sensitive = false)
+ Nodes::DoesNotMatch.new self, quoted_node(other), escape, case_sensitive
+ end
+
+ def does_not_match_regexp(other, case_sensitive = true)
+ Nodes::NotRegexp.new self, quoted_node(other), case_sensitive
+ end
+
+ def does_not_match_any(others, escape = nil)
+ grouping_any :does_not_match, others, escape
+ end
+
+ def does_not_match_all(others, escape = nil)
+ grouping_all :does_not_match, others, escape
+ end
+
+ def gteq(right)
+ Nodes::GreaterThanOrEqual.new self, quoted_node(right)
+ end
+
+ def gteq_any(others)
+ grouping_any :gteq, others
+ end
+
+ def gteq_all(others)
+ grouping_all :gteq, others
+ end
+
+ def gt(right)
+ Nodes::GreaterThan.new self, quoted_node(right)
+ end
+
+ def gt_any(others)
+ grouping_any :gt, others
+ end
+
+ def gt_all(others)
+ grouping_all :gt, others
+ end
+
+ def lt(right)
+ Nodes::LessThan.new self, quoted_node(right)
+ end
+
+ def lt_any(others)
+ grouping_any :lt, others
+ end
+
+ def lt_all(others)
+ grouping_all :lt, others
+ end
+
+ def lteq(right)
+ Nodes::LessThanOrEqual.new self, quoted_node(right)
+ end
+
+ def lteq_any(others)
+ grouping_any :lteq, others
+ end
+
+ def lteq_all(others)
+ grouping_all :lteq, others
+ end
+
+ def when(right)
+ Nodes::Case.new(self).when quoted_node(right)
+ end
+
+ def concat(other)
+ Nodes::Concat.new self, other
+ end
+
+ private
+
+ def grouping_any(method_id, others, *extras)
+ nodes = others.map { |expr| send(method_id, expr, *extras) }
+ Nodes::Grouping.new nodes.inject { |memo, node|
+ Nodes::Or.new(memo, node)
+ }
+ end
+
+ def grouping_all(method_id, others, *extras)
+ nodes = others.map { |expr| send(method_id, expr, *extras) }
+ Nodes::Grouping.new Nodes::And.new(nodes)
+ end
+
+ def quoted_node(other)
+ Nodes.build_quoted(other, self)
+ end
+
+ def quoted_array(others)
+ others.map { |v| quoted_node(v) }
+ end
+
+ def equals_quoted?(maybe_quoted, value)
+ if maybe_quoted.is_a?(Nodes::Quoted)
+ maybe_quoted.val == value
+ else
+ maybe_quoted == value
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/select_manager.rb b/activerecord/lib/arel/select_manager.rb
new file mode 100644
index 0000000000..a2b2838a3d
--- /dev/null
+++ b/activerecord/lib/arel/select_manager.rb
@@ -0,0 +1,271 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ class SelectManager < Arel::TreeManager
+ include Arel::Crud
+
+ STRING_OR_SYMBOL_CLASS = [Symbol, String]
+
+ def initialize(table = nil)
+ super()
+ @ast = Nodes::SelectStatement.new
+ @ctx = @ast.cores.last
+ from table
+ end
+
+ def initialize_copy(other)
+ super
+ @ctx = @ast.cores.last
+ end
+
+ def limit
+ @ast.limit && @ast.limit.expr
+ end
+ alias :taken :limit
+
+ def constraints
+ @ctx.wheres
+ end
+
+ def offset
+ @ast.offset && @ast.offset.expr
+ end
+
+ def skip(amount)
+ if amount
+ @ast.offset = Nodes::Offset.new(amount)
+ else
+ @ast.offset = nil
+ end
+ self
+ end
+ alias :offset= :skip
+
+ ###
+ # Produces an Arel::Nodes::Exists node
+ def exists
+ Arel::Nodes::Exists.new @ast
+ end
+
+ def as(other)
+ create_table_alias grouping(@ast), Nodes::SqlLiteral.new(other)
+ end
+
+ def lock(locking = Arel.sql("FOR UPDATE"))
+ case locking
+ when true
+ locking = Arel.sql("FOR UPDATE")
+ when Arel::Nodes::SqlLiteral
+ when String
+ locking = Arel.sql locking
+ end
+
+ @ast.lock = Nodes::Lock.new(locking)
+ self
+ end
+
+ def locked
+ @ast.lock
+ end
+
+ def on(*exprs)
+ @ctx.source.right.last.right = Nodes::On.new(collapse(exprs))
+ self
+ end
+
+ def group(*columns)
+ columns.each do |column|
+ # FIXME: backwards compat
+ column = Nodes::SqlLiteral.new(column) if String === column
+ column = Nodes::SqlLiteral.new(column.to_s) if Symbol === column
+
+ @ctx.groups.push Nodes::Group.new column
+ end
+ self
+ end
+
+ def from(table)
+ table = Nodes::SqlLiteral.new(table) if String === table
+
+ case table
+ when Nodes::Join
+ @ctx.source.right << table
+ else
+ @ctx.source.left = table
+ end
+
+ self
+ end
+
+ def froms
+ @ast.cores.map { |x| x.from }.compact
+ end
+
+ def join(relation, klass = Nodes::InnerJoin)
+ return self unless relation
+
+ case relation
+ when String, Nodes::SqlLiteral
+ raise EmptyJoinError if relation.empty?
+ klass = Nodes::StringJoin
+ end
+
+ @ctx.source.right << create_join(relation, nil, klass)
+ self
+ end
+
+ def outer_join(relation)
+ join(relation, Nodes::OuterJoin)
+ end
+
+ def having(expr)
+ @ctx.havings << expr
+ self
+ end
+
+ def window(name)
+ window = Nodes::NamedWindow.new(name)
+ @ctx.windows.push window
+ window
+ end
+
+ def project(*projections)
+ # FIXME: converting these to SQLLiterals is probably not good, but
+ # rails tests require it.
+ @ctx.projections.concat projections.map { |x|
+ STRING_OR_SYMBOL_CLASS.include?(x.class) ? Nodes::SqlLiteral.new(x.to_s) : x
+ }
+ self
+ end
+
+ def projections
+ @ctx.projections
+ end
+
+ def projections=(projections)
+ @ctx.projections = projections
+ end
+
+ def distinct(value = true)
+ if value
+ @ctx.set_quantifier = Arel::Nodes::Distinct.new
+ else
+ @ctx.set_quantifier = nil
+ end
+ self
+ end
+
+ def distinct_on(value)
+ if value
+ @ctx.set_quantifier = Arel::Nodes::DistinctOn.new(value)
+ else
+ @ctx.set_quantifier = nil
+ end
+ self
+ end
+
+ def order(*expr)
+ # FIXME: We SHOULD NOT be converting these to SqlLiteral automatically
+ @ast.orders.concat expr.map { |x|
+ STRING_OR_SYMBOL_CLASS.include?(x.class) ? Nodes::SqlLiteral.new(x.to_s) : x
+ }
+ self
+ end
+
+ def orders
+ @ast.orders
+ end
+
+ def where_sql(engine = Table.engine)
+ return if @ctx.wheres.empty?
+
+ viz = Visitors::WhereSql.new(engine.connection.visitor, engine.connection)
+ Nodes::SqlLiteral.new viz.accept(@ctx, Collectors::SQLString.new).value
+ end
+
+ def union(operation, other = nil)
+ if other
+ node_class = Nodes.const_get("Union#{operation.to_s.capitalize}")
+ else
+ other = operation
+ node_class = Nodes::Union
+ end
+
+ node_class.new self.ast, other.ast
+ end
+
+ def intersect(other)
+ Nodes::Intersect.new ast, other.ast
+ end
+
+ def except(other)
+ Nodes::Except.new ast, other.ast
+ end
+ alias :minus :except
+
+ def lateral(table_name = nil)
+ base = table_name.nil? ? ast : as(table_name)
+ Nodes::Lateral.new(base)
+ end
+
+ def with(*subqueries)
+ if subqueries.first.is_a? Symbol
+ node_class = Nodes.const_get("With#{subqueries.shift.to_s.capitalize}")
+ else
+ node_class = Nodes::With
+ end
+ @ast.with = node_class.new(subqueries.flatten)
+
+ self
+ end
+
+ def take(limit)
+ if limit
+ @ast.limit = Nodes::Limit.new(limit)
+ else
+ @ast.limit = nil
+ end
+ self
+ end
+ alias limit= take
+
+ def join_sources
+ @ctx.source.right
+ end
+
+ def source
+ @ctx.source
+ end
+
+ class Row < Struct.new(:data) # :nodoc:
+ def id
+ data["id"]
+ end
+
+ def method_missing(name, *args)
+ name = name.to_s
+ return data[name] if data.key?(name)
+ super
+ end
+ end
+
+ private
+ def collapse(exprs)
+ exprs = exprs.compact
+ exprs.map! { |expr|
+ if String === expr
+ # FIXME: Don't do this automatically
+ Arel.sql(expr)
+ else
+ expr
+ end
+ }
+
+ if exprs.length == 1
+ exprs.first
+ else
+ create_and exprs
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/table.rb b/activerecord/lib/arel/table.rb
new file mode 100644
index 0000000000..c40c68715a
--- /dev/null
+++ b/activerecord/lib/arel/table.rb
@@ -0,0 +1,110 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ class Table
+ include Arel::Crud
+ include Arel::FactoryMethods
+
+ @engine = nil
+ class << self; attr_accessor :engine; end
+
+ attr_accessor :name, :table_alias
+
+ # TableAlias and Table both have a #table_name which is the name of the underlying table
+ alias :table_name :name
+
+ def initialize(name, as: nil, type_caster: nil)
+ @name = name.to_s
+ @type_caster = type_caster
+
+ # Sometime AR sends an :as parameter to table, to let the table know
+ # that it is an Alias. We may want to override new, and return a
+ # TableAlias node?
+ if as.to_s == @name
+ as = nil
+ end
+ @table_alias = as
+ end
+
+ def alias(name = "#{self.name}_2")
+ Nodes::TableAlias.new(self, name)
+ end
+
+ def from
+ SelectManager.new(self)
+ end
+
+ def join(relation, klass = Nodes::InnerJoin)
+ return from unless relation
+
+ case relation
+ when String, Nodes::SqlLiteral
+ raise EmptyJoinError if relation.empty?
+ klass = Nodes::StringJoin
+ end
+
+ from.join(relation, klass)
+ end
+
+ def outer_join(relation)
+ join(relation, Nodes::OuterJoin)
+ end
+
+ def group(*columns)
+ from.group(*columns)
+ end
+
+ def order(*expr)
+ from.order(*expr)
+ end
+
+ def where(condition)
+ from.where condition
+ end
+
+ def project(*things)
+ from.project(*things)
+ end
+
+ def take(amount)
+ from.take amount
+ end
+
+ def skip(amount)
+ from.skip amount
+ end
+
+ def having(expr)
+ from.having expr
+ end
+
+ def [](name)
+ ::Arel::Attribute.new self, name
+ end
+
+ def hash
+ # Perf note: aliases and table alias is excluded from the hash
+ # aliases can have a loop back to this table breaking hashes in parent
+ # relations, for the vast majority of cases @name is unique to a query
+ @name.hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.name == other.name &&
+ self.table_alias == other.table_alias
+ end
+ alias :== :eql?
+
+ def type_cast_for_database(attribute_name, value)
+ type_caster.type_cast_for_database(attribute_name, value)
+ end
+
+ def able_to_type_cast?
+ !type_caster.nil?
+ end
+
+ private
+ attr_reader :type_caster
+ end
+end
diff --git a/activerecord/lib/arel/tree_manager.rb b/activerecord/lib/arel/tree_manager.rb
new file mode 100644
index 0000000000..0476399618
--- /dev/null
+++ b/activerecord/lib/arel/tree_manager.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ class TreeManager
+ include Arel::FactoryMethods
+
+ module StatementMethods
+ def take(limit)
+ @ast.limit = Nodes::Limit.new(Nodes.build_quoted(limit)) if limit
+ self
+ end
+
+ def offset(offset)
+ @ast.offset = Nodes::Offset.new(Nodes.build_quoted(offset)) if offset
+ self
+ end
+
+ def order(*expr)
+ @ast.orders = expr
+ self
+ end
+
+ def key=(key)
+ @ast.key = Nodes.build_quoted(key)
+ end
+
+ def key
+ @ast.key
+ end
+
+ def wheres=(exprs)
+ @ast.wheres = exprs
+ end
+
+ def where(expr)
+ @ast.wheres << expr
+ self
+ end
+ end
+
+ attr_reader :ast
+
+ def initialize
+ @ctx = nil
+ end
+
+ def to_dot
+ collector = Arel::Collectors::PlainString.new
+ collector = Visitors::Dot.new.accept @ast, collector
+ collector.value
+ end
+
+ def to_sql(engine = Table.engine)
+ collector = Arel::Collectors::SQLString.new
+ collector = engine.connection.visitor.accept @ast, collector
+ collector.value
+ end
+
+ def initialize_copy(other)
+ super
+ @ast = @ast.clone
+ end
+
+ def where(expr)
+ if Arel::TreeManager === expr
+ expr = expr.ast
+ end
+ @ctx.wheres << expr
+ self
+ end
+ end
+end
diff --git a/activerecord/lib/arel/update_manager.rb b/activerecord/lib/arel/update_manager.rb
new file mode 100644
index 0000000000..a809dbb307
--- /dev/null
+++ b/activerecord/lib/arel/update_manager.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ class UpdateManager < Arel::TreeManager
+ include TreeManager::StatementMethods
+
+ def initialize
+ super
+ @ast = Nodes::UpdateStatement.new
+ @ctx = @ast
+ end
+
+ ###
+ # UPDATE +table+
+ def table(table)
+ @ast.relation = table
+ self
+ end
+
+ def set(values)
+ if String === values
+ @ast.values = [values]
+ else
+ @ast.values = values.map { |column, value|
+ Nodes::Assignment.new(
+ Nodes::UnqualifiedColumn.new(column),
+ value
+ )
+ }
+ end
+ self
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors.rb b/activerecord/lib/arel/visitors.rb
new file mode 100644
index 0000000000..e350f52e65
--- /dev/null
+++ b/activerecord/lib/arel/visitors.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require "arel/visitors/visitor"
+require "arel/visitors/depth_first"
+require "arel/visitors/to_sql"
+require "arel/visitors/sqlite"
+require "arel/visitors/postgresql"
+require "arel/visitors/mysql"
+require "arel/visitors/mssql"
+require "arel/visitors/oracle"
+require "arel/visitors/oracle12"
+require "arel/visitors/where_sql"
+require "arel/visitors/dot"
+require "arel/visitors/ibm_db"
+require "arel/visitors/informix"
+
+module Arel # :nodoc: all
+ module Visitors
+ end
+end
diff --git a/activerecord/lib/arel/visitors/depth_first.rb b/activerecord/lib/arel/visitors/depth_first.rb
new file mode 100644
index 0000000000..92d309453c
--- /dev/null
+++ b/activerecord/lib/arel/visitors/depth_first.rb
@@ -0,0 +1,199 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class DepthFirst < Arel::Visitors::Visitor
+ def initialize(block = nil)
+ @block = block || Proc.new
+ super()
+ end
+
+ private
+
+ def visit(o)
+ super
+ @block.call o
+ end
+
+ def unary(o)
+ visit o.expr
+ end
+ alias :visit_Arel_Nodes_Else :unary
+ alias :visit_Arel_Nodes_Group :unary
+ alias :visit_Arel_Nodes_Cube :unary
+ alias :visit_Arel_Nodes_RollUp :unary
+ alias :visit_Arel_Nodes_GroupingSet :unary
+ alias :visit_Arel_Nodes_GroupingElement :unary
+ alias :visit_Arel_Nodes_Grouping :unary
+ alias :visit_Arel_Nodes_Having :unary
+ alias :visit_Arel_Nodes_Lateral :unary
+ alias :visit_Arel_Nodes_Limit :unary
+ alias :visit_Arel_Nodes_Not :unary
+ alias :visit_Arel_Nodes_Offset :unary
+ alias :visit_Arel_Nodes_On :unary
+ alias :visit_Arel_Nodes_Ordering :unary
+ alias :visit_Arel_Nodes_Ascending :unary
+ alias :visit_Arel_Nodes_Descending :unary
+ alias :visit_Arel_Nodes_UnqualifiedColumn :unary
+
+ def function(o)
+ visit o.expressions
+ visit o.alias
+ visit o.distinct
+ end
+ alias :visit_Arel_Nodes_Avg :function
+ alias :visit_Arel_Nodes_Exists :function
+ alias :visit_Arel_Nodes_Max :function
+ alias :visit_Arel_Nodes_Min :function
+ alias :visit_Arel_Nodes_Sum :function
+
+ def visit_Arel_Nodes_NamedFunction(o)
+ visit o.name
+ visit o.expressions
+ visit o.distinct
+ visit o.alias
+ end
+
+ def visit_Arel_Nodes_Count(o)
+ visit o.expressions
+ visit o.alias
+ visit o.distinct
+ end
+
+ def visit_Arel_Nodes_Case(o)
+ visit o.case
+ visit o.conditions
+ visit o.default
+ end
+
+ def nary(o)
+ o.children.each { |child| visit child }
+ end
+ alias :visit_Arel_Nodes_And :nary
+
+ def binary(o)
+ visit o.left
+ visit o.right
+ end
+ alias :visit_Arel_Nodes_As :binary
+ alias :visit_Arel_Nodes_Assignment :binary
+ alias :visit_Arel_Nodes_Between :binary
+ alias :visit_Arel_Nodes_Concat :binary
+ alias :visit_Arel_Nodes_DeleteStatement :binary
+ alias :visit_Arel_Nodes_DoesNotMatch :binary
+ alias :visit_Arel_Nodes_Equality :binary
+ alias :visit_Arel_Nodes_FullOuterJoin :binary
+ alias :visit_Arel_Nodes_GreaterThan :binary
+ alias :visit_Arel_Nodes_GreaterThanOrEqual :binary
+ alias :visit_Arel_Nodes_In :binary
+ alias :visit_Arel_Nodes_InfixOperation :binary
+ alias :visit_Arel_Nodes_JoinSource :binary
+ alias :visit_Arel_Nodes_InnerJoin :binary
+ alias :visit_Arel_Nodes_LessThan :binary
+ alias :visit_Arel_Nodes_LessThanOrEqual :binary
+ alias :visit_Arel_Nodes_Matches :binary
+ alias :visit_Arel_Nodes_NotEqual :binary
+ alias :visit_Arel_Nodes_NotIn :binary
+ alias :visit_Arel_Nodes_NotRegexp :binary
+ alias :visit_Arel_Nodes_IsNotDistinctFrom :binary
+ alias :visit_Arel_Nodes_IsDistinctFrom :binary
+ alias :visit_Arel_Nodes_Or :binary
+ alias :visit_Arel_Nodes_OuterJoin :binary
+ alias :visit_Arel_Nodes_Regexp :binary
+ alias :visit_Arel_Nodes_RightOuterJoin :binary
+ alias :visit_Arel_Nodes_TableAlias :binary
+ alias :visit_Arel_Nodes_Values :binary
+ alias :visit_Arel_Nodes_When :binary
+
+ def visit_Arel_Nodes_StringJoin(o)
+ visit o.left
+ end
+
+ def visit_Arel_Attribute(o)
+ visit o.relation
+ visit o.name
+ end
+ alias :visit_Arel_Attributes_Integer :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_Float :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_String :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_Time :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_Boolean :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_Attribute :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_Decimal :visit_Arel_Attribute
+
+ def visit_Arel_Table(o)
+ visit o.name
+ end
+
+ def terminal(o)
+ end
+ alias :visit_ActiveSupport_Multibyte_Chars :terminal
+ alias :visit_ActiveSupport_StringInquirer :terminal
+ alias :visit_Arel_Nodes_Lock :terminal
+ alias :visit_Arel_Nodes_Node :terminal
+ alias :visit_Arel_Nodes_SqlLiteral :terminal
+ alias :visit_Arel_Nodes_BindParam :terminal
+ alias :visit_Arel_Nodes_Window :terminal
+ alias :visit_Arel_Nodes_True :terminal
+ alias :visit_Arel_Nodes_False :terminal
+ alias :visit_BigDecimal :terminal
+ alias :visit_Class :terminal
+ alias :visit_Date :terminal
+ alias :visit_DateTime :terminal
+ alias :visit_FalseClass :terminal
+ alias :visit_Float :terminal
+ alias :visit_Integer :terminal
+ alias :visit_NilClass :terminal
+ alias :visit_String :terminal
+ alias :visit_Symbol :terminal
+ alias :visit_Time :terminal
+ alias :visit_TrueClass :terminal
+
+ def visit_Arel_Nodes_InsertStatement(o)
+ visit o.relation
+ visit o.columns
+ visit o.values
+ end
+
+ def visit_Arel_Nodes_SelectCore(o)
+ visit o.projections
+ visit o.source
+ visit o.wheres
+ visit o.groups
+ visit o.windows
+ visit o.havings
+ end
+
+ def visit_Arel_Nodes_SelectStatement(o)
+ visit o.cores
+ visit o.orders
+ visit o.limit
+ visit o.lock
+ visit o.offset
+ end
+
+ def visit_Arel_Nodes_UpdateStatement(o)
+ visit o.relation
+ visit o.values
+ visit o.wheres
+ visit o.orders
+ visit o.limit
+ end
+
+ def visit_Array(o)
+ o.each { |i| visit i }
+ end
+ alias :visit_Set :visit_Array
+
+ def visit_Hash(o)
+ o.each { |k, v| visit(k); visit(v) }
+ end
+
+ DISPATCH = dispatch_cache
+
+ def get_dispatch_cache
+ DISPATCH
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/dot.rb b/activerecord/lib/arel/visitors/dot.rb
new file mode 100644
index 0000000000..6389c875cb
--- /dev/null
+++ b/activerecord/lib/arel/visitors/dot.rb
@@ -0,0 +1,292 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class Dot < Arel::Visitors::Visitor
+ class Node # :nodoc:
+ attr_accessor :name, :id, :fields
+
+ def initialize(name, id, fields = [])
+ @name = name
+ @id = id
+ @fields = fields
+ end
+ end
+
+ class Edge < Struct.new :name, :from, :to # :nodoc:
+ end
+
+ def initialize
+ super()
+ @nodes = []
+ @edges = []
+ @node_stack = []
+ @edge_stack = []
+ @seen = {}
+ end
+
+ def accept(object, collector)
+ visit object
+ collector << to_dot
+ end
+
+ private
+
+ def visit_Arel_Nodes_Ordering(o)
+ visit_edge o, "expr"
+ end
+
+ def visit_Arel_Nodes_TableAlias(o)
+ visit_edge o, "name"
+ visit_edge o, "relation"
+ end
+
+ def visit_Arel_Nodes_Count(o)
+ visit_edge o, "expressions"
+ visit_edge o, "distinct"
+ end
+
+ def visit_Arel_Nodes_Values(o)
+ visit_edge o, "expressions"
+ end
+
+ def visit_Arel_Nodes_StringJoin(o)
+ visit_edge o, "left"
+ end
+
+ def visit_Arel_Nodes_InnerJoin(o)
+ visit_edge o, "left"
+ visit_edge o, "right"
+ end
+ alias :visit_Arel_Nodes_FullOuterJoin :visit_Arel_Nodes_InnerJoin
+ alias :visit_Arel_Nodes_OuterJoin :visit_Arel_Nodes_InnerJoin
+ alias :visit_Arel_Nodes_RightOuterJoin :visit_Arel_Nodes_InnerJoin
+
+ def visit_Arel_Nodes_DeleteStatement(o)
+ visit_edge o, "relation"
+ visit_edge o, "wheres"
+ end
+
+ def unary(o)
+ visit_edge o, "expr"
+ end
+ alias :visit_Arel_Nodes_Group :unary
+ alias :visit_Arel_Nodes_Cube :unary
+ alias :visit_Arel_Nodes_RollUp :unary
+ alias :visit_Arel_Nodes_GroupingSet :unary
+ alias :visit_Arel_Nodes_GroupingElement :unary
+ alias :visit_Arel_Nodes_Grouping :unary
+ alias :visit_Arel_Nodes_Having :unary
+ alias :visit_Arel_Nodes_Limit :unary
+ alias :visit_Arel_Nodes_Not :unary
+ alias :visit_Arel_Nodes_Offset :unary
+ alias :visit_Arel_Nodes_On :unary
+ alias :visit_Arel_Nodes_UnqualifiedColumn :unary
+ alias :visit_Arel_Nodes_Preceding :unary
+ alias :visit_Arel_Nodes_Following :unary
+ alias :visit_Arel_Nodes_Rows :unary
+ alias :visit_Arel_Nodes_Range :unary
+
+ def window(o)
+ visit_edge o, "partitions"
+ visit_edge o, "orders"
+ visit_edge o, "framing"
+ end
+ alias :visit_Arel_Nodes_Window :window
+
+ def named_window(o)
+ visit_edge o, "partitions"
+ visit_edge o, "orders"
+ visit_edge o, "framing"
+ visit_edge o, "name"
+ end
+ alias :visit_Arel_Nodes_NamedWindow :named_window
+
+ def function(o)
+ visit_edge o, "expressions"
+ visit_edge o, "distinct"
+ visit_edge o, "alias"
+ end
+ alias :visit_Arel_Nodes_Exists :function
+ alias :visit_Arel_Nodes_Min :function
+ alias :visit_Arel_Nodes_Max :function
+ alias :visit_Arel_Nodes_Avg :function
+ alias :visit_Arel_Nodes_Sum :function
+
+ def extract(o)
+ visit_edge o, "expressions"
+ visit_edge o, "alias"
+ end
+ alias :visit_Arel_Nodes_Extract :extract
+
+ def visit_Arel_Nodes_NamedFunction(o)
+ visit_edge o, "name"
+ visit_edge o, "expressions"
+ visit_edge o, "distinct"
+ visit_edge o, "alias"
+ end
+
+ def visit_Arel_Nodes_InsertStatement(o)
+ visit_edge o, "relation"
+ visit_edge o, "columns"
+ visit_edge o, "values"
+ end
+
+ def visit_Arel_Nodes_SelectCore(o)
+ visit_edge o, "source"
+ visit_edge o, "projections"
+ visit_edge o, "wheres"
+ visit_edge o, "windows"
+ end
+
+ def visit_Arel_Nodes_SelectStatement(o)
+ visit_edge o, "cores"
+ visit_edge o, "limit"
+ visit_edge o, "orders"
+ visit_edge o, "offset"
+ end
+
+ def visit_Arel_Nodes_UpdateStatement(o)
+ visit_edge o, "relation"
+ visit_edge o, "wheres"
+ visit_edge o, "values"
+ end
+
+ def visit_Arel_Table(o)
+ visit_edge o, "name"
+ end
+
+ def visit_Arel_Nodes_Casted(o)
+ visit_edge o, "val"
+ visit_edge o, "attribute"
+ end
+
+ def visit_Arel_Attribute(o)
+ visit_edge o, "relation"
+ visit_edge o, "name"
+ end
+ alias :visit_Arel_Attributes_Integer :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_Float :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_String :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_Time :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_Boolean :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_Attribute :visit_Arel_Attribute
+
+ def nary(o)
+ o.children.each_with_index do |x, i|
+ edge(i) { visit x }
+ end
+ end
+ alias :visit_Arel_Nodes_And :nary
+
+ def binary(o)
+ visit_edge o, "left"
+ visit_edge o, "right"
+ end
+ alias :visit_Arel_Nodes_As :binary
+ alias :visit_Arel_Nodes_Assignment :binary
+ alias :visit_Arel_Nodes_Between :binary
+ alias :visit_Arel_Nodes_Concat :binary
+ alias :visit_Arel_Nodes_DoesNotMatch :binary
+ alias :visit_Arel_Nodes_Equality :binary
+ alias :visit_Arel_Nodes_GreaterThan :binary
+ alias :visit_Arel_Nodes_GreaterThanOrEqual :binary
+ alias :visit_Arel_Nodes_In :binary
+ alias :visit_Arel_Nodes_JoinSource :binary
+ alias :visit_Arel_Nodes_LessThan :binary
+ alias :visit_Arel_Nodes_LessThanOrEqual :binary
+ alias :visit_Arel_Nodes_IsNotDistinctFrom :binary
+ alias :visit_Arel_Nodes_IsDistinctFrom :binary
+ alias :visit_Arel_Nodes_Matches :binary
+ alias :visit_Arel_Nodes_NotEqual :binary
+ alias :visit_Arel_Nodes_NotIn :binary
+ alias :visit_Arel_Nodes_Or :binary
+ alias :visit_Arel_Nodes_Over :binary
+
+ def visit_String(o)
+ @node_stack.last.fields << o
+ end
+ alias :visit_Time :visit_String
+ alias :visit_Date :visit_String
+ alias :visit_DateTime :visit_String
+ alias :visit_NilClass :visit_String
+ alias :visit_TrueClass :visit_String
+ alias :visit_FalseClass :visit_String
+ alias :visit_Integer :visit_String
+ alias :visit_BigDecimal :visit_String
+ alias :visit_Float :visit_String
+ alias :visit_Symbol :visit_String
+ alias :visit_Arel_Nodes_SqlLiteral :visit_String
+
+ def visit_Arel_Nodes_BindParam(o); end
+
+ def visit_Hash(o)
+ o.each_with_index do |pair, i|
+ edge("pair_#{i}") { visit pair }
+ end
+ end
+
+ def visit_Array(o)
+ o.each_with_index do |x, i|
+ edge(i) { visit x }
+ end
+ end
+ alias :visit_Set :visit_Array
+
+ def visit_edge(o, method)
+ edge(method) { visit o.send(method) }
+ end
+
+ def visit(o)
+ if node = @seen[o.object_id]
+ @edge_stack.last.to = node
+ return
+ end
+
+ node = Node.new(o.class.name, o.object_id)
+ @seen[node.id] = node
+ @nodes << node
+ with_node node do
+ super
+ end
+ end
+
+ def edge(name)
+ edge = Edge.new(name, @node_stack.last)
+ @edge_stack.push edge
+ @edges << edge
+ yield
+ @edge_stack.pop
+ end
+
+ def with_node(node)
+ if edge = @edge_stack.last
+ edge.to = node
+ end
+
+ @node_stack.push node
+ yield
+ @node_stack.pop
+ end
+
+ def quote(string)
+ string.to_s.gsub('"', '\"')
+ end
+
+ def to_dot
+ "digraph \"Arel\" {\nnode [width=0.375,height=0.25,shape=record];\n" +
+ @nodes.map { |node|
+ label = "<f0>#{node.name}"
+
+ node.fields.each_with_index do |field, i|
+ label += "|<f#{i + 1}>#{quote field}"
+ end
+
+ "#{node.id} [label=\"#{label}\"];"
+ }.join("\n") + "\n" + @edges.map { |edge|
+ "#{edge.from.id} -> #{edge.to.id} [label=\"#{edge.name}\"];"
+ }.join("\n") + "\n}"
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/ibm_db.rb b/activerecord/lib/arel/visitors/ibm_db.rb
new file mode 100644
index 0000000000..73166054da
--- /dev/null
+++ b/activerecord/lib/arel/visitors/ibm_db.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class IBM_DB < Arel::Visitors::ToSql
+ private
+
+ def visit_Arel_Nodes_Limit(o, collector)
+ collector << "FETCH FIRST "
+ collector = visit o.expr, collector
+ collector << " ROWS ONLY"
+ end
+
+ def is_distinct_from(o, collector)
+ collector << "DECODE("
+ collector = visit [o.left, o.right, 0, 1], collector
+ collector << ")"
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/informix.rb b/activerecord/lib/arel/visitors/informix.rb
new file mode 100644
index 0000000000..0a9713794e
--- /dev/null
+++ b/activerecord/lib/arel/visitors/informix.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class Informix < Arel::Visitors::ToSql
+ private
+ def visit_Arel_Nodes_SelectStatement(o, collector)
+ collector << "SELECT "
+ collector = maybe_visit o.offset, collector
+ collector = maybe_visit o.limit, collector
+ collector = o.cores.inject(collector) { |c, x|
+ visit_Arel_Nodes_SelectCore x, c
+ }
+ if o.orders.any?
+ collector << "ORDER BY "
+ collector = inject_join o.orders, collector, ", "
+ end
+ collector = maybe_visit o.lock, collector
+ end
+ def visit_Arel_Nodes_SelectCore(o, collector)
+ collector = inject_join o.projections, collector, ", "
+ if o.source && !o.source.empty?
+ collector << " FROM "
+ collector = visit o.source, collector
+ end
+
+ if o.wheres.any?
+ collector << " WHERE "
+ collector = inject_join o.wheres, collector, " AND "
+ end
+
+ if o.groups.any?
+ collector << "GROUP BY "
+ collector = inject_join o.groups, collector, ", "
+ end
+
+ if o.havings.any?
+ collector << " HAVING "
+ collector = inject_join o.havings, collector, " AND "
+ end
+ collector
+ end
+
+ def visit_Arel_Nodes_Offset(o, collector)
+ collector << "SKIP "
+ visit o.expr, collector
+ end
+ def visit_Arel_Nodes_Limit(o, collector)
+ collector << "FIRST "
+ visit o.expr, collector
+ collector << " "
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/mssql.rb b/activerecord/lib/arel/visitors/mssql.rb
new file mode 100644
index 0000000000..fdd864b40d
--- /dev/null
+++ b/activerecord/lib/arel/visitors/mssql.rb
@@ -0,0 +1,143 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class MSSQL < Arel::Visitors::ToSql
+ RowNumber = Struct.new :children
+
+ def initialize(*)
+ @primary_keys = {}
+ super
+ end
+
+ private
+
+ def visit_Arel_Nodes_IsNotDistinctFrom(o, collector)
+ right = o.right
+
+ if right.nil?
+ collector = visit o.left, collector
+ collector << " IS NULL"
+ else
+ collector << "EXISTS (VALUES ("
+ collector = visit o.left, collector
+ collector << ") INTERSECT VALUES ("
+ collector = visit right, collector
+ collector << "))"
+ end
+ end
+
+ def visit_Arel_Nodes_IsDistinctFrom(o, collector)
+ if o.right.nil?
+ collector = visit o.left, collector
+ collector << " IS NOT NULL"
+ else
+ collector << "NOT "
+ visit_Arel_Nodes_IsNotDistinctFrom o, collector
+ end
+ end
+
+ def visit_Arel_Visitors_MSSQL_RowNumber(o, collector)
+ collector << "ROW_NUMBER() OVER (ORDER BY "
+ inject_join(o.children, collector, ", ") << ") as _row_num"
+ end
+
+ def visit_Arel_Nodes_SelectStatement(o, collector)
+ if !o.limit && !o.offset
+ return super
+ end
+
+ is_select_count = false
+ o.cores.each { |x|
+ core_order_by = row_num_literal determine_order_by(o.orders, x)
+ if select_count? x
+ x.projections = [core_order_by]
+ is_select_count = true
+ else
+ x.projections << core_order_by
+ end
+ }
+
+ if is_select_count
+ # fixme count distinct wouldn't work with limit or offset
+ collector << "SELECT COUNT(1) as count_id FROM ("
+ end
+
+ collector << "SELECT _t.* FROM ("
+ collector = o.cores.inject(collector) { |c, x|
+ visit_Arel_Nodes_SelectCore x, c
+ }
+ collector << ") as _t WHERE #{get_offset_limit_clause(o)}"
+
+ if is_select_count
+ collector << ") AS subquery"
+ else
+ collector
+ end
+ end
+
+ def get_offset_limit_clause(o)
+ first_row = o.offset ? o.offset.expr.to_i + 1 : 1
+ last_row = o.limit ? o.limit.expr.to_i - 1 + first_row : nil
+ if last_row
+ " _row_num BETWEEN #{first_row} AND #{last_row}"
+ else
+ " _row_num >= #{first_row}"
+ end
+ end
+
+ def visit_Arel_Nodes_DeleteStatement(o, collector)
+ collector << "DELETE "
+ if o.limit
+ collector << "TOP ("
+ visit o.limit.expr, collector
+ collector << ") "
+ end
+ collector << "FROM "
+ collector = visit o.relation, collector
+ if o.wheres.any?
+ collector << " WHERE "
+ inject_join o.wheres, collector, AND
+ else
+ collector
+ end
+ end
+
+ def determine_order_by(orders, x)
+ if orders.any?
+ orders
+ elsif x.groups.any?
+ x.groups
+ else
+ pk = find_left_table_pk(x.froms)
+ pk ? [pk] : []
+ end
+ end
+
+ def row_num_literal(order_by)
+ RowNumber.new order_by
+ end
+
+ def select_count?(x)
+ x.projections.length == 1 && Arel::Nodes::Count === x.projections.first
+ end
+
+ # FIXME raise exception of there is no pk?
+ def find_left_table_pk(o)
+ if o.kind_of?(Arel::Nodes::Join)
+ find_left_table_pk(o.left)
+ elsif o.instance_of?(Arel::Table)
+ find_primary_key(o)
+ end
+ end
+
+ def find_primary_key(o)
+ @primary_keys[o.name] ||= begin
+ primary_key_name = @connection.primary_key(o.name)
+ # some tables might be without primary key
+ primary_key_name && o[primary_key_name]
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/mysql.rb b/activerecord/lib/arel/visitors/mysql.rb
new file mode 100644
index 0000000000..dd77cfdf66
--- /dev/null
+++ b/activerecord/lib/arel/visitors/mysql.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class MySQL < Arel::Visitors::ToSql
+ private
+ def visit_Arel_Nodes_Bin(o, collector)
+ collector << "BINARY "
+ visit o.expr, collector
+ end
+
+ def visit_Arel_Nodes_UnqualifiedColumn(o, collector)
+ visit o.expr, collector
+ end
+
+ ###
+ # :'(
+ # http://dev.mysql.com/doc/refman/5.0/en/select.html#id3482214
+ def visit_Arel_Nodes_SelectStatement(o, collector)
+ if o.offset && !o.limit
+ o.limit = Arel::Nodes::Limit.new(18446744073709551615)
+ end
+ super
+ end
+
+ def visit_Arel_Nodes_SelectCore(o, collector)
+ o.froms ||= Arel.sql("DUAL")
+ super
+ end
+
+ def visit_Arel_Nodes_Concat(o, collector)
+ collector << " CONCAT("
+ visit o.left, collector
+ collector << ", "
+ visit o.right, collector
+ collector << ") "
+ collector
+ end
+
+ def visit_Arel_Nodes_IsNotDistinctFrom(o, collector)
+ collector = visit o.left, collector
+ collector << " <=> "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_IsDistinctFrom(o, collector)
+ collector << "NOT "
+ visit_Arel_Nodes_IsNotDistinctFrom o, collector
+ end
+
+ # In the simple case, MySQL allows us to place JOINs directly into the UPDATE
+ # query. However, this does not allow for LIMIT, OFFSET and ORDER. To support
+ # these, we must use a subquery.
+ def prepare_update_statement(o)
+ if o.offset || has_join_sources?(o) && has_limit_or_offset_or_orders?(o)
+ super
+ else
+ o
+ end
+ end
+ alias :prepare_delete_statement :prepare_update_statement
+
+ # 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.
+ def build_subselect(key, o)
+ subselect = super
+
+ # Materialize subquery by adding distinct
+ # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on'
+ unless has_limit_or_offset_or_orders?(subselect)
+ core = subselect.cores.last
+ core.set_quantifier = Arel::Nodes::Distinct.new
+ end
+
+ Nodes::SelectStatement.new.tap do |stmt|
+ core = stmt.cores.last
+ core.froms = Nodes::Grouping.new(subselect).as("__active_record_temp")
+ core.projections = [Arel.sql(quote_column_name(key.name))]
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/oracle.rb b/activerecord/lib/arel/visitors/oracle.rb
new file mode 100644
index 0000000000..f96bf65ee5
--- /dev/null
+++ b/activerecord/lib/arel/visitors/oracle.rb
@@ -0,0 +1,159 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class Oracle < Arel::Visitors::ToSql
+ private
+
+ def visit_Arel_Nodes_SelectStatement(o, collector)
+ o = order_hacks(o)
+
+ # if need to select first records without ORDER BY and GROUP BY and without DISTINCT
+ # then can use simple ROWNUM in WHERE clause
+ if o.limit && o.orders.empty? && o.cores.first.groups.empty? && !o.offset && o.cores.first.set_quantifier.class.to_s !~ /Distinct/
+ o.cores.last.wheres.push Nodes::LessThanOrEqual.new(
+ Nodes::SqlLiteral.new("ROWNUM"), o.limit.expr
+ )
+ return super
+ end
+
+ if o.limit && o.offset
+ o = o.dup
+ limit = o.limit.expr
+ offset = o.offset
+ o.offset = nil
+ collector << "
+ SELECT * FROM (
+ SELECT raw_sql_.*, rownum raw_rnum_
+ FROM ("
+
+ collector = super(o, collector)
+
+ if offset.expr.is_a? Nodes::BindParam
+ collector << ") raw_sql_ WHERE rownum <= ("
+ collector = visit offset.expr, collector
+ collector << " + "
+ collector = visit limit, collector
+ collector << ") ) WHERE raw_rnum_ > "
+ collector = visit offset.expr, collector
+ return collector
+ else
+ collector << ") raw_sql_
+ WHERE rownum <= #{offset.expr.to_i + limit}
+ )
+ WHERE "
+ return visit(offset, collector)
+ end
+ end
+
+ if o.limit
+ o = o.dup
+ limit = o.limit.expr
+ collector << "SELECT * FROM ("
+ collector = super(o, collector)
+ collector << ") WHERE ROWNUM <= "
+ return visit limit, collector
+ end
+
+ if o.offset
+ o = o.dup
+ offset = o.offset
+ o.offset = nil
+ collector << "SELECT * FROM (
+ SELECT raw_sql_.*, rownum raw_rnum_
+ FROM ("
+ collector = super(o, collector)
+ collector << ") raw_sql_
+ )
+ WHERE "
+ return visit offset, collector
+ end
+
+ super
+ end
+
+ def visit_Arel_Nodes_Limit(o, collector)
+ collector
+ end
+
+ def visit_Arel_Nodes_Offset(o, collector)
+ collector << "raw_rnum_ > "
+ visit o.expr, collector
+ end
+
+ def visit_Arel_Nodes_Except(o, collector)
+ collector << "( "
+ collector = infix_value o, collector, " MINUS "
+ collector << " )"
+ end
+
+ def visit_Arel_Nodes_UpdateStatement(o, collector)
+ # Oracle does not allow ORDER BY/LIMIT in UPDATEs.
+ if o.orders.any? && o.limit.nil?
+ # However, there is no harm in silently eating the ORDER BY clause if no LIMIT has been provided,
+ # otherwise let the user deal with the error
+ o = o.dup
+ o.orders = []
+ end
+
+ super
+ end
+
+ ###
+ # Hacks for the order clauses specific to Oracle
+ def order_hacks(o)
+ return o if o.orders.empty?
+ return o unless o.cores.any? do |core|
+ core.projections.any? do |projection|
+ /FIRST_VALUE/ === projection
+ end
+ end
+ # Previous version with join and split broke ORDER BY clause
+ # if it contained functions with several arguments (separated by ',').
+ #
+ # orders = o.orders.map { |x| visit x }.join(', ').split(',')
+ orders = o.orders.map do |x|
+ string = visit(x, Arel::Collectors::SQLString.new).value
+ if string.include?(",")
+ split_order_string(string)
+ else
+ string
+ end
+ end.flatten
+ o.orders = []
+ orders.each_with_index do |order, i|
+ o.orders <<
+ Nodes::SqlLiteral.new("alias_#{i}__#{' DESC' if /\bdesc$/i === order}")
+ end
+ o
+ end
+
+ # Split string by commas but count opening and closing brackets
+ # and ignore commas inside brackets.
+ def split_order_string(string)
+ array = []
+ i = 0
+ string.split(",").each do |part|
+ if array[i]
+ array[i] << "," << part
+ else
+ # to ensure that array[i] will be String and not Arel::Nodes::SqlLiteral
+ array[i] = part.to_s
+ end
+ i += 1 if array[i].count("(") == array[i].count(")")
+ end
+ array
+ end
+
+ def visit_Arel_Nodes_BindParam(o, collector)
+ collector.add_bind(o.value) { |i| ":a#{i}" }
+ end
+
+ def is_distinct_from(o, collector)
+ collector << "DECODE("
+ collector = visit [o.left, o.right, 0, 1], collector
+ collector << ")"
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/oracle12.rb b/activerecord/lib/arel/visitors/oracle12.rb
new file mode 100644
index 0000000000..b092aa95e0
--- /dev/null
+++ b/activerecord/lib/arel/visitors/oracle12.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class Oracle12 < Arel::Visitors::ToSql
+ private
+
+ def visit_Arel_Nodes_SelectStatement(o, collector)
+ # Oracle does not allow LIMIT clause with select for update
+ if o.limit && o.lock
+ raise ArgumentError, <<-MSG
+ 'Combination of limit and lock is not supported.
+ because generated SQL statements
+ `SELECT FOR UPDATE and FETCH FIRST n ROWS` generates ORA-02014.`
+ MSG
+ end
+ super
+ end
+
+ def visit_Arel_Nodes_SelectOptions(o, collector)
+ collector = maybe_visit o.offset, collector
+ collector = maybe_visit o.limit, collector
+ collector = maybe_visit o.lock, collector
+ end
+
+ def visit_Arel_Nodes_Limit(o, collector)
+ collector << "FETCH FIRST "
+ collector = visit o.expr, collector
+ collector << " ROWS ONLY"
+ end
+
+ def visit_Arel_Nodes_Offset(o, collector)
+ collector << "OFFSET "
+ visit o.expr, collector
+ collector << " ROWS"
+ end
+
+ def visit_Arel_Nodes_Except(o, collector)
+ collector << "( "
+ collector = infix_value o, collector, " MINUS "
+ collector << " )"
+ end
+
+ def visit_Arel_Nodes_UpdateStatement(o, collector)
+ # Oracle does not allow ORDER BY/LIMIT in UPDATEs.
+ if o.orders.any? && o.limit.nil?
+ # However, there is no harm in silently eating the ORDER BY clause if no LIMIT has been provided,
+ # otherwise let the user deal with the error
+ o = o.dup
+ o.orders = []
+ end
+
+ super
+ end
+
+ def visit_Arel_Nodes_BindParam(o, collector)
+ collector.add_bind(o.value) { |i| ":a#{i}" }
+ end
+
+ def is_distinct_from(o, collector)
+ collector << "DECODE("
+ collector = visit [o.left, o.right, 0, 1], collector
+ collector << ")"
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/postgresql.rb b/activerecord/lib/arel/visitors/postgresql.rb
new file mode 100644
index 0000000000..920776b4dc
--- /dev/null
+++ b/activerecord/lib/arel/visitors/postgresql.rb
@@ -0,0 +1,116 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class PostgreSQL < Arel::Visitors::ToSql
+ CUBE = "CUBE"
+ ROLLUP = "ROLLUP"
+ GROUPING_SETS = "GROUPING SETS"
+ LATERAL = "LATERAL"
+
+ private
+
+ def visit_Arel_Nodes_Matches(o, collector)
+ op = o.case_sensitive ? " LIKE " : " ILIKE "
+ collector = infix_value o, collector, op
+ if o.escape
+ collector << " ESCAPE "
+ visit o.escape, collector
+ else
+ collector
+ end
+ end
+
+ def visit_Arel_Nodes_DoesNotMatch(o, collector)
+ op = o.case_sensitive ? " NOT LIKE " : " NOT ILIKE "
+ collector = infix_value o, collector, op
+ if o.escape
+ collector << " ESCAPE "
+ visit o.escape, collector
+ else
+ collector
+ end
+ end
+
+ def visit_Arel_Nodes_Regexp(o, collector)
+ op = o.case_sensitive ? " ~ " : " ~* "
+ infix_value o, collector, op
+ end
+
+ def visit_Arel_Nodes_NotRegexp(o, collector)
+ op = o.case_sensitive ? " !~ " : " !~* "
+ infix_value o, collector, op
+ end
+
+ def visit_Arel_Nodes_DistinctOn(o, collector)
+ collector << "DISTINCT ON ( "
+ visit(o.expr, collector) << " )"
+ end
+
+ def visit_Arel_Nodes_BindParam(o, collector)
+ collector.add_bind(o.value) { |i| "$#{i}" }
+ end
+
+ def visit_Arel_Nodes_GroupingElement(o, collector)
+ collector << "( "
+ visit(o.expr, collector) << " )"
+ end
+
+ def visit_Arel_Nodes_Cube(o, collector)
+ collector << CUBE
+ grouping_array_or_grouping_element o, collector
+ end
+
+ def visit_Arel_Nodes_RollUp(o, collector)
+ collector << ROLLUP
+ grouping_array_or_grouping_element o, collector
+ end
+
+ def visit_Arel_Nodes_GroupingSet(o, collector)
+ collector << GROUPING_SETS
+ grouping_array_or_grouping_element o, collector
+ end
+
+ def visit_Arel_Nodes_Lateral(o, collector)
+ collector << LATERAL
+ collector << SPACE
+ grouping_parentheses o, collector
+ end
+
+ def visit_Arel_Nodes_IsNotDistinctFrom(o, collector)
+ collector = visit o.left, collector
+ collector << " IS NOT DISTINCT FROM "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_IsDistinctFrom(o, collector)
+ collector = visit o.left, collector
+ collector << " IS DISTINCT FROM "
+ visit o.right, collector
+ end
+
+ # Used by Lateral visitor to enclose select queries in parentheses
+ def grouping_parentheses(o, collector)
+ if o.expr.is_a? Nodes::SelectStatement
+ collector << "("
+ visit o.expr, collector
+ collector << ")"
+ else
+ visit o.expr, collector
+ end
+ end
+
+ # Utilized by GroupingSet, Cube & RollUp visitors to
+ # handle grouping aggregation semantics
+ def grouping_array_or_grouping_element(o, collector)
+ if o.expr.is_a? Array
+ collector << "( "
+ visit o.expr, collector
+ collector << " )"
+ else
+ visit o.expr, collector
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/sqlite.rb b/activerecord/lib/arel/visitors/sqlite.rb
new file mode 100644
index 0000000000..af6f7e856a
--- /dev/null
+++ b/activerecord/lib/arel/visitors/sqlite.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class SQLite < Arel::Visitors::ToSql
+ private
+
+ # Locks are not supported in SQLite
+ def visit_Arel_Nodes_Lock(o, collector)
+ collector
+ end
+
+ def visit_Arel_Nodes_SelectStatement(o, collector)
+ o.limit = Arel::Nodes::Limit.new(-1) if o.offset && !o.limit
+ super
+ end
+
+ def visit_Arel_Nodes_True(o, collector)
+ collector << "1"
+ end
+
+ def visit_Arel_Nodes_False(o, collector)
+ collector << "0"
+ end
+
+ def visit_Arel_Nodes_IsNotDistinctFrom(o, collector)
+ collector = visit o.left, collector
+ collector << " IS "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_IsDistinctFrom(o, collector)
+ collector = visit o.left, collector
+ collector << " IS NOT "
+ visit o.right, collector
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/to_sql.rb b/activerecord/lib/arel/visitors/to_sql.rb
new file mode 100644
index 0000000000..f9fe4404eb
--- /dev/null
+++ b/activerecord/lib/arel/visitors/to_sql.rb
@@ -0,0 +1,911 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class UnsupportedVisitError < StandardError
+ def initialize(object)
+ super "Unsupported argument type: #{object.class.name}. Construct an Arel node instead."
+ end
+ end
+
+ class ToSql < Arel::Visitors::Visitor
+ ##
+ # This is some roflscale crazy stuff. I'm roflscaling this because
+ # building SQL queries is a hotspot. I will explain the roflscale so that
+ # others will not rm this code.
+ #
+ # In YARV, string literals in a method body will get duped when the byte
+ # code is executed. Let's take a look:
+ #
+ # > puts RubyVM::InstructionSequence.new('def foo; "bar"; end').disasm
+ #
+ # == disasm: <RubyVM::InstructionSequence:foo@<compiled>>=====
+ # 0000 trace 8
+ # 0002 trace 1
+ # 0004 putstring "bar"
+ # 0006 trace 16
+ # 0008 leave
+ #
+ # The `putstring` bytecode will dup the string and push it on the stack.
+ # In many cases in our SQL visitor, that string is never mutated, so there
+ # is no need to dup the literal.
+ #
+ # If we change to a constant lookup, the string will not be duped, and we
+ # can reduce the objects in our system:
+ #
+ # > puts RubyVM::InstructionSequence.new('BAR = "bar"; def foo; BAR; end').disasm
+ #
+ # == disasm: <RubyVM::InstructionSequence:foo@<compiled>>========
+ # 0000 trace 8
+ # 0002 trace 1
+ # 0004 getinlinecache 11, <ic:0>
+ # 0007 getconstant :BAR
+ # 0009 setinlinecache <ic:0>
+ # 0011 trace 16
+ # 0013 leave
+ #
+ # `getconstant` should be a hash lookup, and no object is duped when the
+ # value of the constant is pushed on the stack. Hence the crazy
+ # constants below.
+ #
+ # `matches` and `doesNotMatch` operate case-insensitively via Visitor subclasses
+ # specialized for specific databases when necessary.
+ #
+
+ WHERE = " WHERE " # :nodoc:
+ SPACE = " " # :nodoc:
+ COMMA = ", " # :nodoc:
+ GROUP_BY = " GROUP BY " # :nodoc:
+ ORDER_BY = " ORDER BY " # :nodoc:
+ WINDOW = " WINDOW " # :nodoc:
+ AND = " AND " # :nodoc:
+
+ DISTINCT = "DISTINCT" # :nodoc:
+
+ def initialize(connection)
+ super()
+ @connection = connection
+ end
+
+ def compile(node, collector = Arel::Collectors::SQLString.new)
+ accept(node, collector).value
+ end
+
+ private
+
+ def visit_Arel_Nodes_DeleteStatement(o, collector)
+ o = prepare_delete_statement(o)
+
+ if has_join_sources?(o)
+ collector << "DELETE "
+ visit o.relation.left, collector
+ collector << " FROM "
+ else
+ collector << "DELETE FROM "
+ end
+ collector = visit o.relation, collector
+
+ collect_nodes_for o.wheres, collector, " WHERE ", " AND "
+ collect_nodes_for o.orders, collector, " ORDER BY "
+ maybe_visit o.limit, collector
+ end
+
+ def visit_Arel_Nodes_UpdateStatement(o, collector)
+ o = prepare_update_statement(o)
+
+ collector << "UPDATE "
+ collector = visit o.relation, collector
+ collect_nodes_for o.values, collector, " SET "
+
+ collect_nodes_for o.wheres, collector, " WHERE ", " AND "
+ collect_nodes_for o.orders, collector, " ORDER BY "
+ maybe_visit o.limit, collector
+ end
+
+ def visit_Arel_Nodes_InsertStatement(o, collector)
+ collector << "INSERT INTO "
+ collector = visit o.relation, collector
+ if o.columns.any?
+ collector << " (#{o.columns.map { |x|
+ quote_column_name x.name
+ }.join ', '})"
+ end
+
+ if o.values
+ maybe_visit o.values, collector
+ elsif o.select
+ maybe_visit o.select, collector
+ else
+ collector
+ end
+ end
+
+ def visit_Arel_Nodes_Exists(o, collector)
+ collector << "EXISTS ("
+ collector = visit(o.expressions, collector) << ")"
+ if o.alias
+ collector << " AS "
+ visit o.alias, collector
+ else
+ collector
+ end
+ end
+
+ def visit_Arel_Nodes_Casted(o, collector)
+ collector << quoted(o.val, o.attribute).to_s
+ end
+
+ def visit_Arel_Nodes_Quoted(o, collector)
+ collector << quoted(o.expr, nil).to_s
+ end
+
+ def visit_Arel_Nodes_True(o, collector)
+ collector << "TRUE"
+ end
+
+ def visit_Arel_Nodes_False(o, collector)
+ collector << "FALSE"
+ end
+
+ def visit_Arel_Nodes_ValuesList(o, collector)
+ collector << "VALUES "
+
+ len = o.rows.length - 1
+ o.rows.each_with_index { |row, i|
+ collector << "("
+ row_len = row.length - 1
+ row.each_with_index do |value, k|
+ case value
+ when Nodes::SqlLiteral, Nodes::BindParam
+ collector = visit(value, collector)
+ else
+ collector << quote(value)
+ end
+ collector << COMMA unless k == row_len
+ end
+ collector << ")"
+ collector << COMMA unless i == len
+ }
+ collector
+ end
+
+ def visit_Arel_Nodes_Values(o, collector)
+ collector << "VALUES ("
+
+ len = o.expressions.length - 1
+ o.expressions.each_with_index { |value, i|
+ case value
+ when Nodes::SqlLiteral, Nodes::BindParam
+ collector = visit value, collector
+ else
+ collector << quote(value).to_s
+ end
+ unless i == len
+ collector << COMMA
+ end
+ }
+
+ collector << ")"
+ end
+
+ def visit_Arel_Nodes_SelectStatement(o, collector)
+ if o.with
+ collector = visit o.with, collector
+ collector << SPACE
+ end
+
+ collector = o.cores.inject(collector) { |c, x|
+ visit_Arel_Nodes_SelectCore(x, c)
+ }
+
+ unless o.orders.empty?
+ collector << ORDER_BY
+ len = o.orders.length - 1
+ o.orders.each_with_index { |x, i|
+ collector = visit(x, collector)
+ collector << COMMA unless len == i
+ }
+ end
+
+ visit_Arel_Nodes_SelectOptions(o, collector)
+
+ collector
+ end
+
+ def visit_Arel_Nodes_SelectOptions(o, collector)
+ collector = maybe_visit o.limit, collector
+ collector = maybe_visit o.offset, collector
+ collector = maybe_visit o.lock, collector
+ end
+
+ def visit_Arel_Nodes_SelectCore(o, collector)
+ collector << "SELECT"
+
+ collector = maybe_visit o.set_quantifier, collector
+
+ collect_nodes_for o.projections, collector, SPACE
+
+ if o.source && !o.source.empty?
+ collector << " FROM "
+ collector = visit o.source, collector
+ end
+
+ collect_nodes_for o.wheres, collector, WHERE, AND
+ collect_nodes_for o.groups, collector, GROUP_BY
+ collect_nodes_for o.havings, collector, " HAVING ", AND
+ collect_nodes_for o.windows, collector, WINDOW
+
+ collector
+ end
+
+ def collect_nodes_for(nodes, collector, spacer, connector = COMMA)
+ unless nodes.empty?
+ collector << spacer
+ inject_join nodes, collector, connector
+ end
+ end
+
+ def visit_Arel_Nodes_Bin(o, collector)
+ visit o.expr, collector
+ end
+
+ def visit_Arel_Nodes_Distinct(o, collector)
+ collector << DISTINCT
+ end
+
+ def visit_Arel_Nodes_DistinctOn(o, collector)
+ raise NotImplementedError, "DISTINCT ON not implemented for this db"
+ end
+
+ def visit_Arel_Nodes_With(o, collector)
+ collector << "WITH "
+ inject_join o.children, collector, COMMA
+ end
+
+ def visit_Arel_Nodes_WithRecursive(o, collector)
+ collector << "WITH RECURSIVE "
+ inject_join o.children, collector, COMMA
+ end
+
+ def visit_Arel_Nodes_Union(o, collector)
+ infix_value_with_paren(o, collector, " UNION ")
+ end
+
+ def visit_Arel_Nodes_UnionAll(o, collector)
+ infix_value_with_paren(o, collector, " UNION ALL ")
+ end
+
+ def visit_Arel_Nodes_Intersect(o, collector)
+ collector << "( "
+ infix_value(o, collector, " INTERSECT ") << " )"
+ end
+
+ def visit_Arel_Nodes_Except(o, collector)
+ collector << "( "
+ infix_value(o, collector, " EXCEPT ") << " )"
+ end
+
+ def visit_Arel_Nodes_NamedWindow(o, collector)
+ collector << quote_column_name(o.name)
+ collector << " AS "
+ visit_Arel_Nodes_Window o, collector
+ end
+
+ def visit_Arel_Nodes_Window(o, collector)
+ collector << "("
+
+ collect_nodes_for o.partitions, collector, "PARTITION BY "
+
+ if o.orders.any?
+ collector << SPACE if o.partitions.any?
+ collector << "ORDER BY "
+ collector = inject_join o.orders, collector, ", "
+ end
+
+ if o.framing
+ collector << SPACE if o.partitions.any? || o.orders.any?
+ collector = visit o.framing, collector
+ end
+
+ collector << ")"
+ end
+
+ def visit_Arel_Nodes_Rows(o, collector)
+ if o.expr
+ collector << "ROWS "
+ visit o.expr, collector
+ else
+ collector << "ROWS"
+ end
+ end
+
+ def visit_Arel_Nodes_Range(o, collector)
+ if o.expr
+ collector << "RANGE "
+ visit o.expr, collector
+ else
+ collector << "RANGE"
+ end
+ end
+
+ def visit_Arel_Nodes_Preceding(o, collector)
+ collector = if o.expr
+ visit o.expr, collector
+ else
+ collector << "UNBOUNDED"
+ end
+
+ collector << " PRECEDING"
+ end
+
+ def visit_Arel_Nodes_Following(o, collector)
+ collector = if o.expr
+ visit o.expr, collector
+ else
+ collector << "UNBOUNDED"
+ end
+
+ collector << " FOLLOWING"
+ end
+
+ def visit_Arel_Nodes_CurrentRow(o, collector)
+ collector << "CURRENT ROW"
+ end
+
+ def visit_Arel_Nodes_Over(o, collector)
+ case o.right
+ when nil
+ visit(o.left, collector) << " OVER ()"
+ when Arel::Nodes::SqlLiteral
+ infix_value o, collector, " OVER "
+ when String, Symbol
+ visit(o.left, collector) << " OVER #{quote_column_name o.right.to_s}"
+ else
+ infix_value o, collector, " OVER "
+ end
+ end
+
+ def visit_Arel_Nodes_Offset(o, collector)
+ collector << "OFFSET "
+ visit o.expr, collector
+ end
+
+ def visit_Arel_Nodes_Limit(o, collector)
+ collector << "LIMIT "
+ visit o.expr, collector
+ end
+
+ def visit_Arel_Nodes_Lock(o, collector)
+ visit o.expr, collector
+ end
+
+ def visit_Arel_Nodes_Grouping(o, collector)
+ if o.expr.is_a? Nodes::Grouping
+ visit(o.expr, collector)
+ else
+ collector << "("
+ visit(o.expr, collector) << ")"
+ end
+ end
+
+ def visit_Arel_SelectManager(o, collector)
+ collector << "("
+ visit(o.ast, collector) << ")"
+ end
+
+ def visit_Arel_Nodes_Ascending(o, collector)
+ visit(o.expr, collector) << " ASC"
+ end
+
+ def visit_Arel_Nodes_Descending(o, collector)
+ visit(o.expr, collector) << " DESC"
+ end
+
+ def visit_Arel_Nodes_Group(o, collector)
+ visit o.expr, collector
+ end
+
+ def visit_Arel_Nodes_NamedFunction(o, collector)
+ collector << o.name
+ collector << "("
+ collector << "DISTINCT " if o.distinct
+ collector = inject_join(o.expressions, collector, ", ") << ")"
+ if o.alias
+ collector << " AS "
+ visit o.alias, collector
+ else
+ collector
+ end
+ end
+
+ def visit_Arel_Nodes_Extract(o, collector)
+ collector << "EXTRACT(#{o.field.to_s.upcase} FROM "
+ visit(o.expr, collector) << ")"
+ end
+
+ def visit_Arel_Nodes_Count(o, collector)
+ aggregate "COUNT", o, collector
+ end
+
+ def visit_Arel_Nodes_Sum(o, collector)
+ aggregate "SUM", o, collector
+ end
+
+ def visit_Arel_Nodes_Max(o, collector)
+ aggregate "MAX", o, collector
+ end
+
+ def visit_Arel_Nodes_Min(o, collector)
+ aggregate "MIN", o, collector
+ end
+
+ def visit_Arel_Nodes_Avg(o, collector)
+ aggregate "AVG", o, collector
+ end
+
+ def visit_Arel_Nodes_TableAlias(o, collector)
+ collector = visit o.relation, collector
+ collector << " "
+ collector << quote_table_name(o.name)
+ end
+
+ def visit_Arel_Nodes_Between(o, collector)
+ collector = visit o.left, collector
+ collector << " BETWEEN "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_GreaterThanOrEqual(o, collector)
+ collector = visit o.left, collector
+ collector << " >= "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_GreaterThan(o, collector)
+ collector = visit o.left, collector
+ collector << " > "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_LessThanOrEqual(o, collector)
+ collector = visit o.left, collector
+ collector << " <= "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_LessThan(o, collector)
+ collector = visit o.left, collector
+ collector << " < "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_Matches(o, collector)
+ collector = visit o.left, collector
+ collector << " LIKE "
+ collector = visit o.right, collector
+ if o.escape
+ collector << " ESCAPE "
+ visit o.escape, collector
+ else
+ collector
+ end
+ end
+
+ def visit_Arel_Nodes_DoesNotMatch(o, collector)
+ collector = visit o.left, collector
+ collector << " NOT LIKE "
+ collector = visit o.right, collector
+ if o.escape
+ collector << " ESCAPE "
+ visit o.escape, collector
+ else
+ collector
+ end
+ end
+
+ def visit_Arel_Nodes_JoinSource(o, collector)
+ if o.left
+ collector = visit o.left, collector
+ end
+ if o.right.any?
+ collector << SPACE if o.left
+ collector = inject_join o.right, collector, SPACE
+ end
+ collector
+ end
+
+ def visit_Arel_Nodes_Regexp(o, collector)
+ raise NotImplementedError, "~ not implemented for this db"
+ end
+
+ def visit_Arel_Nodes_NotRegexp(o, collector)
+ raise NotImplementedError, "!~ not implemented for this db"
+ end
+
+ def visit_Arel_Nodes_StringJoin(o, collector)
+ visit o.left, collector
+ end
+
+ def visit_Arel_Nodes_FullOuterJoin(o, collector)
+ collector << "FULL OUTER JOIN "
+ collector = visit o.left, collector
+ collector << SPACE
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_OuterJoin(o, collector)
+ collector << "LEFT OUTER JOIN "
+ collector = visit o.left, collector
+ collector << " "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_RightOuterJoin(o, collector)
+ collector << "RIGHT OUTER JOIN "
+ collector = visit o.left, collector
+ collector << SPACE
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_InnerJoin(o, collector)
+ collector << "INNER JOIN "
+ collector = visit o.left, collector
+ if o.right
+ collector << SPACE
+ visit(o.right, collector)
+ else
+ collector
+ end
+ end
+
+ def visit_Arel_Nodes_On(o, collector)
+ collector << "ON "
+ visit o.expr, collector
+ end
+
+ def visit_Arel_Nodes_Not(o, collector)
+ collector << "NOT ("
+ visit(o.expr, collector) << ")"
+ end
+
+ def visit_Arel_Table(o, collector)
+ if o.table_alias
+ collector << "#{quote_table_name o.name} #{quote_table_name o.table_alias}"
+ else
+ collector << quote_table_name(o.name)
+ end
+ end
+
+ def visit_Arel_Nodes_In(o, collector)
+ if Array === o.right && !o.right.empty?
+ o.right.keep_if { |value| boundable?(value) }
+ end
+
+ if Array === o.right && o.right.empty?
+ collector << "1=0"
+ else
+ collector = visit o.left, collector
+ collector << " IN ("
+ visit(o.right, collector) << ")"
+ end
+ end
+
+ def visit_Arel_Nodes_NotIn(o, collector)
+ if Array === o.right && !o.right.empty?
+ o.right.keep_if { |value| boundable?(value) }
+ end
+
+ if Array === o.right && o.right.empty?
+ collector << "1=1"
+ else
+ collector = visit o.left, collector
+ collector << " NOT IN ("
+ collector = visit o.right, collector
+ collector << ")"
+ end
+ end
+
+ def visit_Arel_Nodes_And(o, collector)
+ inject_join o.children, collector, " AND "
+ end
+
+ def visit_Arel_Nodes_Or(o, collector)
+ collector = visit o.left, collector
+ collector << " OR "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_Assignment(o, collector)
+ case o.right
+ when Arel::Nodes::Node, Arel::Attributes::Attribute
+ collector = visit o.left, collector
+ collector << " = "
+ visit o.right, collector
+ else
+ collector = visit o.left, collector
+ collector << " = "
+ collector << quote(o.right).to_s
+ end
+ end
+
+ def visit_Arel_Nodes_Equality(o, collector)
+ right = o.right
+
+ collector = visit o.left, collector
+
+ if right.nil?
+ collector << " IS NULL"
+ else
+ collector << " = "
+ visit right, collector
+ end
+ end
+
+ def visit_Arel_Nodes_IsNotDistinctFrom(o, collector)
+ if o.right.nil?
+ collector = visit o.left, collector
+ collector << " IS NULL"
+ else
+ collector = is_distinct_from(o, collector)
+ collector << " = 0"
+ end
+ end
+
+ def visit_Arel_Nodes_IsDistinctFrom(o, collector)
+ if o.right.nil?
+ collector = visit o.left, collector
+ collector << " IS NOT NULL"
+ else
+ collector = is_distinct_from(o, collector)
+ collector << " = 1"
+ end
+ end
+
+ def visit_Arel_Nodes_NotEqual(o, collector)
+ right = o.right
+
+ collector = visit o.left, collector
+
+ if right.nil?
+ collector << " IS NOT NULL"
+ else
+ collector << " != "
+ visit right, collector
+ end
+ end
+
+ def visit_Arel_Nodes_As(o, collector)
+ collector = visit o.left, collector
+ collector << " AS "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_Case(o, collector)
+ collector << "CASE "
+ if o.case
+ visit o.case, collector
+ collector << " "
+ end
+ o.conditions.each do |condition|
+ visit condition, collector
+ collector << " "
+ end
+ if o.default
+ visit o.default, collector
+ collector << " "
+ end
+ collector << "END"
+ end
+
+ def visit_Arel_Nodes_When(o, collector)
+ collector << "WHEN "
+ visit o.left, collector
+ collector << " THEN "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_Else(o, collector)
+ collector << "ELSE "
+ visit o.expr, collector
+ end
+
+ def visit_Arel_Nodes_UnqualifiedColumn(o, collector)
+ collector << "#{quote_column_name o.name}"
+ collector
+ end
+
+ def visit_Arel_Attributes_Attribute(o, collector)
+ join_name = o.relation.table_alias || o.relation.name
+ collector << "#{quote_table_name join_name}.#{quote_column_name o.name}"
+ end
+ alias :visit_Arel_Attributes_Integer :visit_Arel_Attributes_Attribute
+ alias :visit_Arel_Attributes_Float :visit_Arel_Attributes_Attribute
+ alias :visit_Arel_Attributes_Decimal :visit_Arel_Attributes_Attribute
+ alias :visit_Arel_Attributes_String :visit_Arel_Attributes_Attribute
+ alias :visit_Arel_Attributes_Time :visit_Arel_Attributes_Attribute
+ alias :visit_Arel_Attributes_Boolean :visit_Arel_Attributes_Attribute
+
+ def literal(o, collector); collector << o.to_s; end
+
+ def visit_Arel_Nodes_BindParam(o, collector)
+ collector.add_bind(o.value) { "?" }
+ end
+
+ alias :visit_Arel_Nodes_SqlLiteral :literal
+ alias :visit_Integer :literal
+
+ def quoted(o, a)
+ if a && a.able_to_type_cast?
+ quote(a.type_cast_for_database(o))
+ else
+ quote(o)
+ end
+ end
+
+ def unsupported(o, collector)
+ raise UnsupportedVisitError.new(o)
+ end
+
+ alias :visit_ActiveSupport_Multibyte_Chars :unsupported
+ alias :visit_ActiveSupport_StringInquirer :unsupported
+ alias :visit_BigDecimal :unsupported
+ alias :visit_Class :unsupported
+ alias :visit_Date :unsupported
+ alias :visit_DateTime :unsupported
+ alias :visit_FalseClass :unsupported
+ alias :visit_Float :unsupported
+ alias :visit_Hash :unsupported
+ alias :visit_NilClass :unsupported
+ alias :visit_String :unsupported
+ alias :visit_Symbol :unsupported
+ alias :visit_Time :unsupported
+ alias :visit_TrueClass :unsupported
+
+ def visit_Arel_Nodes_InfixOperation(o, collector)
+ collector = visit o.left, collector
+ collector << " #{o.operator} "
+ visit o.right, collector
+ end
+
+ alias :visit_Arel_Nodes_Addition :visit_Arel_Nodes_InfixOperation
+ alias :visit_Arel_Nodes_Subtraction :visit_Arel_Nodes_InfixOperation
+ alias :visit_Arel_Nodes_Multiplication :visit_Arel_Nodes_InfixOperation
+ alias :visit_Arel_Nodes_Division :visit_Arel_Nodes_InfixOperation
+
+ def visit_Arel_Nodes_UnaryOperation(o, collector)
+ collector << " #{o.operator} "
+ visit o.expr, collector
+ end
+
+ def visit_Array(o, collector)
+ inject_join o, collector, ", "
+ end
+ alias :visit_Set :visit_Array
+
+ def quote(value)
+ return value if Arel::Nodes::SqlLiteral === value
+ @connection.quote value
+ end
+
+ def quote_table_name(name)
+ return name if Arel::Nodes::SqlLiteral === name
+ @connection.quote_table_name(name)
+ end
+
+ def quote_column_name(name)
+ return name if Arel::Nodes::SqlLiteral === name
+ @connection.quote_column_name(name)
+ end
+
+ def maybe_visit(thing, collector)
+ return collector unless thing
+ collector << " "
+ visit thing, collector
+ end
+
+ def inject_join(list, collector, join_str)
+ len = list.length - 1
+ list.each_with_index.inject(collector) { |c, (x, i)|
+ if i == len
+ visit x, c
+ else
+ visit(x, c) << join_str
+ end
+ }
+ end
+
+ def boundable?(value)
+ !value.respond_to?(:boundable?) || value.boundable?
+ end
+
+ def has_join_sources?(o)
+ o.relation.is_a?(Nodes::JoinSource) && !o.relation.right.empty?
+ end
+
+ def has_limit_or_offset_or_orders?(o)
+ o.limit || o.offset || !o.orders.empty?
+ end
+
+ # The default strategy for an UPDATE with joins is to use a subquery. This doesn't work
+ # on MySQL (even when aliasing the tables), but MySQL allows using JOIN directly in
+ # an UPDATE statement, so in the MySQL visitor we redefine this to do that.
+ def prepare_update_statement(o)
+ if o.key && (has_limit_or_offset_or_orders?(o) || has_join_sources?(o))
+ stmt = o.clone
+ stmt.limit = nil
+ stmt.offset = nil
+ stmt.orders = []
+ stmt.wheres = [Nodes::In.new(o.key, [build_subselect(o.key, o)])]
+ stmt.relation = o.relation.left if has_join_sources?(o)
+ stmt
+ else
+ o
+ end
+ end
+ alias :prepare_delete_statement :prepare_update_statement
+
+ # FIXME: we should probably have a 2-pass visitor for this
+ def build_subselect(key, o)
+ stmt = Nodes::SelectStatement.new
+ core = stmt.cores.first
+ core.froms = o.relation
+ core.wheres = o.wheres
+ core.projections = [key]
+ stmt.limit = o.limit
+ stmt.offset = o.offset
+ stmt.orders = o.orders
+ stmt
+ end
+
+ def infix_value(o, collector, value)
+ collector = visit o.left, collector
+ collector << value
+ visit o.right, collector
+ end
+
+ def infix_value_with_paren(o, collector, value, suppress_parens = false)
+ collector << "( " unless suppress_parens
+ collector = if o.left.class == o.class
+ infix_value_with_paren(o.left, collector, value, true)
+ else
+ visit o.left, collector
+ end
+ collector << value
+ collector = if o.right.class == o.class
+ infix_value_with_paren(o.right, collector, value, true)
+ else
+ visit o.right, collector
+ end
+ collector << " )" unless suppress_parens
+ collector
+ end
+
+ def aggregate(name, o, collector)
+ collector << "#{name}("
+ if o.distinct
+ collector << "DISTINCT "
+ end
+ collector = inject_join(o.expressions, collector, ", ") << ")"
+ if o.alias
+ collector << " AS "
+ visit o.alias, collector
+ else
+ collector
+ end
+ end
+
+ def is_distinct_from(o, collector)
+ collector << "CASE WHEN "
+ collector = visit o.left, collector
+ collector << " = "
+ collector = visit o.right, collector
+ collector << " OR ("
+ collector = visit o.left, collector
+ collector << " IS NULL AND "
+ collector = visit o.right, collector
+ collector << " IS NULL)"
+ collector << " THEN 0 ELSE 1 END"
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/visitor.rb b/activerecord/lib/arel/visitors/visitor.rb
new file mode 100644
index 0000000000..1c17184e86
--- /dev/null
+++ b/activerecord/lib/arel/visitors/visitor.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class Visitor
+ def initialize
+ @dispatch = get_dispatch_cache
+ end
+
+ def accept(object, *args)
+ visit object, *args
+ end
+
+ private
+
+ attr_reader :dispatch
+
+ def self.dispatch_cache
+ Hash.new do |hash, klass|
+ hash[klass] = "visit_#{(klass.name || '').gsub('::', '_')}"
+ end
+ end
+
+ def get_dispatch_cache
+ self.class.dispatch_cache
+ end
+
+ def visit(object, *args)
+ dispatch_method = dispatch[object.class]
+ send dispatch_method, object, *args
+ rescue NoMethodError => e
+ raise e if respond_to?(dispatch_method, true)
+ superklass = object.class.ancestors.find { |klass|
+ respond_to?(dispatch[klass], true)
+ }
+ raise(TypeError, "Cannot visit #{object.class}") unless superklass
+ dispatch[object.class] = dispatch[superklass]
+ retry
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/where_sql.rb b/activerecord/lib/arel/visitors/where_sql.rb
new file mode 100644
index 0000000000..c6caf5e7c9
--- /dev/null
+++ b/activerecord/lib/arel/visitors/where_sql.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class WhereSql < Arel::Visitors::ToSql
+ def initialize(inner_visitor, *args, &block)
+ @inner_visitor = inner_visitor
+ super(*args, &block)
+ end
+
+ private
+
+ def visit_Arel_Nodes_SelectCore(o, collector)
+ collector << "WHERE "
+ wheres = o.wheres.map do |where|
+ Nodes::SqlLiteral.new(@inner_visitor.accept(where, collector.class.new).value)
+ end
+
+ inject_join wheres, collector, " AND "
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/window_predications.rb b/activerecord/lib/arel/window_predications.rb
new file mode 100644
index 0000000000..3a8ee41f8a
--- /dev/null
+++ b/activerecord/lib/arel/window_predications.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module WindowPredications
+ def over(expr = nil)
+ Nodes::Over.new(self, expr)
+ end
+ end
+end
diff --git a/activerecord/lib/rails/generators/active_record.rb b/activerecord/lib/rails/generators/active_record.rb
index dc29213235..a7e5e373a7 100644
--- a/activerecord/lib/rails/generators/active_record.rb
+++ b/activerecord/lib/rails/generators/active_record.rb
@@ -1,7 +1,9 @@
-require 'rails/generators/named_base'
-require 'rails/generators/active_model'
-require 'rails/generators/active_record/migration'
-require 'active_record'
+# frozen_string_literal: true
+
+require "rails/generators/named_base"
+require "rails/generators/active_model"
+require "rails/generators/active_record/migration"
+require "active_record"
module ActiveRecord
module Generators # :nodoc:
@@ -10,7 +12,7 @@ module ActiveRecord
# Set the current directory as base for the inherited generators.
def self.base_root
- File.dirname(__FILE__)
+ __dir__
end
end
end
diff --git a/activerecord/lib/rails/generators/active_record/application_record/application_record_generator.rb b/activerecord/lib/rails/generators/active_record/application_record/application_record_generator.rb
new file mode 100644
index 0000000000..35d5664400
--- /dev/null
+++ b/activerecord/lib/rails/generators/active_record/application_record/application_record_generator.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require "rails/generators/active_record"
+
+module ActiveRecord
+ module Generators # :nodoc:
+ class ApplicationRecordGenerator < ::Rails::Generators::Base # :nodoc:
+ source_root File.expand_path("templates", __dir__)
+
+ # FIXME: Change this file to a symlink once RubyGems 2.5.0 is required.
+ def create_application_record
+ template "application_record.rb", application_record_file_name
+ end
+
+ private
+
+ def application_record_file_name
+ @application_record_file_name ||=
+ if namespaced?
+ "app/models/#{namespaced_path}/application_record.rb"
+ else
+ "app/models/application_record.rb"
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt b/activerecord/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt
new file mode 100644
index 0000000000..60050e0bf8
--- /dev/null
+++ b/activerecord/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt
@@ -0,0 +1,5 @@
+<% module_namespacing do -%>
+class ApplicationRecord < ActiveRecord::Base
+ self.abstract_class = true
+end
+<% end -%>
diff --git a/activerecord/lib/rails/generators/active_record/migration.rb b/activerecord/lib/rails/generators/active_record/migration.rb
index b7418cf42f..cbb88d571d 100644
--- a/activerecord/lib/rails/generators/active_record/migration.rb
+++ b/activerecord/lib/rails/generators/active_record/migration.rb
@@ -1,4 +1,6 @@
-require 'rails/generators/migration'
+# frozen_string_literal: true
+
+require "rails/generators/migration"
module ActiveRecord
module Generators # :nodoc:
@@ -13,6 +15,34 @@ module ActiveRecord
ActiveRecord::Migration.next_migration_number(next_migration_number)
end
end
+
+ private
+
+ def primary_key_type
+ key_type = options[:primary_key_type]
+ ", id: :#{key_type}" if key_type
+ end
+
+ def db_migrate_path
+ if defined?(Rails.application) && Rails.application
+ configured_migrate_path || default_migrate_path
+ else
+ "db/migrate"
+ end
+ end
+
+ def default_migrate_path
+ Rails.application.config.paths["db/migrate"].to_ary.first
+ end
+
+ def configured_migrate_path
+ return unless database = options[:database]
+ config = ActiveRecord::Base.configurations.configs_for(
+ env_name: Rails.env,
+ spec_name: database,
+ )
+ config&.migrations_paths
+ 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 0d57de4d65..dd79bcf542 100644
--- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
+++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
@@ -1,56 +1,60 @@
-require 'rails/generators/active_record'
+# frozen_string_literal: true
+
+require "rails/generators/active_record"
module ActiveRecord
module Generators # :nodoc:
class MigrationGenerator < Base # :nodoc:
- argument :attributes, :type => :array, :default => [], :banner => "field[:type][:index] field[:type][:index]"
+ argument :attributes, type: :array, default: [], banner: "field[:type][:index] field[:type][:index]"
+
+ class_option :primary_key_type, type: :string, desc: "The type for primary key"
+ class_option :database, type: :string, aliases: %i(db), desc: "The database for your migration. By default, the current environment's primary database is used."
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
- protected
- attr_reader :migration_action, :join_tables
+ private
+ attr_reader :migration_action, :join_tables
- # Sets the default migration template that is being used for the generation of the migration.
- # Depending on command line arguments, the migration template and the table name instance
- # variables are set up.
- def set_local_assigns!
- @migration_template = "migration.rb"
- case file_name
- when /^(add|remove)_.*_(?:to|from)_(.*)/
- @migration_action = $1
- @table_name = normalize_table_name($2)
- when /join_table/
- if attributes.length == 2
- @migration_action = 'join'
- @join_tables = pluralize_table_names? ? attributes.map(&:plural_name) : attributes.map(&:singular_name)
+ # 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.
+ def set_local_assigns!
+ @migration_template = "migration.rb"
+ case file_name
+ when /^(add)_.*_to_(.*)/, /^(remove)_.*?_from_(.*)/
+ @migration_action = $1
+ @table_name = normalize_table_name($2)
+ when /join_table/
+ if attributes.length == 2
+ @migration_action = "join"
+ @join_tables = pluralize_table_names? ? attributes.map(&:plural_name) : attributes.map(&:singular_name)
- set_index_names
+ set_index_names
+ end
+ when /^create_(.+)/
+ @table_name = normalize_table_name($1)
+ @migration_template = "create_table_migration.rb"
end
- when /^create_(.+)/
- @table_name = normalize_table_name($1)
- @migration_template = "create_table_migration.rb"
end
- end
- def set_index_names
- attributes.each_with_index do |attr, i|
- attr.index_name = [attr, attributes[i - 1]].map{ |a| index_name_for(a) }
+ def set_index_names
+ attributes.each_with_index do |attr, i|
+ attr.index_name = [attr, attributes[i - 1]].map { |a| index_name_for(a) }
+ end
end
- end
- def index_name_for(attribute)
- if attribute.foreign_key?
- attribute.name
- else
- attribute.name.singularize.foreign_key
- end.to_sym
- end
+ def index_name_for(attribute)
+ if attribute.foreign_key?
+ attribute.name
+ else
+ attribute.name.singularize.foreign_key
+ end.to_sym
+ end
- private
def attributes_with_index
attributes.select { |a| !a.reference? && a.has_index? }
end
@@ -58,7 +62,7 @@ module ActiveRecord
# A migration file name can only contain underscores (_), lowercase characters,
# and numbers 0-9. Any other file name will raise an IllegalMigrationNameError.
def validate_file_name!
- unless file_name =~ /^[_a-z0-9]+$/
+ unless /^[_a-z0-9]+$/.match?(file_name)
raise IllegalMigrationNameError.new(file_name)
end
end
diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb
deleted file mode 100644
index 5b3e57dcf6..0000000000
--- a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-class <%= migration_class_name %> < ActiveRecord::Migration
- def change
- create_table :<%= table_name %> do |t|
-<% attributes.each do |attribute| -%>
-<% if attribute.password_digest? -%>
- t.string :password_digest<%= attribute.inject_options %>
-<% elsif attribute.token? -%>
- t.string :<%= attribute.name %><%= attribute.inject_options %>
-<% else -%>
- t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %>
-<% end -%>
-<% end -%>
-<% if options[:timestamps] %>
- t.timestamps
-<% end -%>
- end
-<% attributes.select(&:token?).each do |attribute| -%>
- add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>, unique: true
-<% end -%>
-<% attributes_with_index.each do |attribute| -%>
- add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
-<% end -%>
- end
-end
diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt
new file mode 100644
index 0000000000..5f7201cfe1
--- /dev/null
+++ b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt
@@ -0,0 +1,24 @@
+class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
+ def change
+ create_table :<%= table_name %><%= primary_key_type %> do |t|
+<% attributes.each do |attribute| -%>
+<% if attribute.password_digest? -%>
+ t.string :password_digest<%= attribute.inject_options %>
+<% elsif attribute.token? -%>
+ t.string :<%= attribute.name %><%= attribute.inject_options %>
+<% else -%>
+ t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %>
+<% end -%>
+<% end -%>
+<% if options[:timestamps] %>
+ t.timestamps
+<% end -%>
+ end
+<% attributes.select(&:token?).each do |attribute| -%>
+ add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>, unique: true
+<% end -%>
+<% attributes_with_index.each do |attribute| -%>
+ add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
+<% end -%>
+ end
+end
diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb
deleted file mode 100644
index 23a377db6a..0000000000
--- a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-class <%= migration_class_name %> < ActiveRecord::Migration
-<%- if migration_action == 'add' -%>
- def change
-<% attributes.each do |attribute| -%>
- <%- if attribute.reference? -%>
- add_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %>
- <%- elsif attribute.token? -%>
- add_column :<%= table_name %>, :<%= attribute.name %>, :string<%= attribute.inject_options %>
- add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>, unique: true
- <%- else -%>
- add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %>
- <%- if attribute.has_index? -%>
- add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
- <%- end -%>
- <%- end -%>
-<%- end -%>
- end
-<%- elsif migration_action == 'join' -%>
- def change
- create_join_table :<%= join_tables.first %>, :<%= join_tables.second %> do |t|
- <%- attributes.each do |attribute| -%>
- <%= '# ' unless attribute.has_index? -%>t.index <%= attribute.index_name %><%= attribute.inject_index_options %>
- <%- end -%>
- end
- end
-<%- else -%>
- def change
-<% attributes.each do |attribute| -%>
-<%- if migration_action -%>
- <%- if attribute.reference? -%>
- remove_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %>
- <%- else -%>
- <%- if attribute.has_index? -%>
- remove_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
- <%- end -%>
- remove_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %>
- <%- end -%>
-<%- end -%>
-<%- end -%>
- end
-<%- end -%>
-end
diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb.tt b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb.tt
new file mode 100644
index 0000000000..481c70201b
--- /dev/null
+++ b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb.tt
@@ -0,0 +1,46 @@
+class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
+<%- if migration_action == 'add' -%>
+ def change
+<% attributes.each do |attribute| -%>
+ <%- if attribute.reference? -%>
+ add_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %>
+ <%- elsif attribute.token? -%>
+ add_column :<%= table_name %>, :<%= attribute.name %>, :string<%= attribute.inject_options %>
+ add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>, unique: true
+ <%- else -%>
+ add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %>
+ <%- if attribute.has_index? -%>
+ add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
+ <%- end -%>
+ <%- end -%>
+<%- end -%>
+ end
+<%- elsif migration_action == 'join' -%>
+ def change
+ create_join_table :<%= join_tables.first %>, :<%= join_tables.second %> do |t|
+ <%- attributes.each do |attribute| -%>
+ <%- if attribute.reference? -%>
+ t.references :<%= attribute.name %><%= attribute.inject_options %>
+ <%- else -%>
+ <%= '# ' unless attribute.has_index? -%>t.index <%= attribute.index_name %><%= attribute.inject_index_options %>
+ <%- end -%>
+ <%- end -%>
+ end
+ end
+<%- else -%>
+ def change
+<% attributes.each do |attribute| -%>
+<%- if migration_action -%>
+ <%- if attribute.reference? -%>
+ remove_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %>
+ <%- else -%>
+ <%- if attribute.has_index? -%>
+ remove_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
+ <%- end -%>
+ remove_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %>
+ <%- end -%>
+<%- end -%>
+<%- end -%>
+ end
+<%- end -%>
+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 7e8d68ce69..eac504f9f1 100644
--- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb
+++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb
@@ -1,52 +1,49 @@
-require 'rails/generators/active_record'
+# frozen_string_literal: true
+
+require "rails/generators/active_record"
module ActiveRecord
module Generators # :nodoc:
class ModelGenerator < Base # :nodoc:
- argument :attributes, :type => :array, :default => [], :banner => "field[:type][:index] field[:type][:index]"
+ argument :attributes, type: :array, default: [], banner: "field[:type][:index] field[:type][:index]"
check_class_collision
- class_option :migration, :type => :boolean
- class_option :timestamps, :type => :boolean
- class_option :parent, :type => :string, :desc => "The parent class for the generated model"
- class_option :indexes, :type => :boolean, :default => true, :desc => "Add indexes for references and belongs_to columns"
+ class_option :migration, type: :boolean
+ class_option :timestamps, type: :boolean
+ class_option :parent, type: :string, desc: "The parent class for the generated model"
+ class_option :indexes, type: :boolean, default: true, desc: "Add indexes for references and belongs_to columns"
+ class_option :primary_key_type, type: :string, desc: "The type for primary key"
+ class_option :database, type: :string, aliases: %i(db), desc: "The database for your model's migration. By default, the current environment's primary database is used."
-
# creates the migration file for the model.
-
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
- template 'model.rb', File.join('app/models', class_path, "#{file_name}.rb")
+ template "model.rb", File.join("app/models", class_path, "#{file_name}.rb")
end
def create_module_file
return if regular_class_path.empty?
- template 'module.rb', File.join('app/models', "#{class_path.join('/')}.rb") if behavior == :invoke
- end
-
- def attributes_with_index
- attributes.select { |a| !a.reference? && a.has_index? }
- end
-
- def accessible_attributes
- attributes.reject(&:reference?)
+ template "module.rb", File.join("app/models", "#{class_path.join('/')}.rb") if behavior == :invoke
end
hook_for :test_framework
- protected
+ private
+
+ def attributes_with_index
+ attributes.select { |a| !a.reference? && a.has_index? }
+ end
# Used by the migration template to determine the parent name of the model
def parent_class_name
- options[:parent] || "ActiveRecord::Base"
+ options[:parent] || "ApplicationRecord"
end
-
end
end
end
diff --git a/activerecord/lib/rails/generators/active_record/model/templates/model.rb b/activerecord/lib/rails/generators/active_record/model/templates/model.rb.tt
index 55dc65c8ad..55dc65c8ad 100644
--- a/activerecord/lib/rails/generators/active_record/model/templates/model.rb
+++ b/activerecord/lib/rails/generators/active_record/model/templates/model.rb.tt
diff --git a/activerecord/lib/rails/generators/active_record/model/templates/module.rb b/activerecord/lib/rails/generators/active_record/model/templates/module.rb.tt
index a3bf1c37b6..a3bf1c37b6 100644
--- a/activerecord/lib/rails/generators/active_record/model/templates/module.rb
+++ b/activerecord/lib/rails/generators/active_record/model/templates/module.rb.tt
diff --git a/activerecord/test/.gitignore b/activerecord/test/.gitignore
deleted file mode 100644
index a0ec5967dd..0000000000
--- a/activerecord/test/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/config.yml
diff --git a/activerecord/test/active_record/connection_adapters/fake_adapter.rb b/activerecord/test/active_record/connection_adapters/fake_adapter.rb
index 49a68fb94c..f977b2997b 100644
--- a/activerecord/test/active_record/connection_adapters/fake_adapter.rb
+++ b/activerecord/test/active_record/connection_adapters/fake_adapter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionHandling
def fake_connection(config)
@@ -7,16 +9,16 @@ module ActiveRecord
module ConnectionAdapters
class FakeAdapter < AbstractAdapter
- attr_accessor :tables, :primary_keys
+ attr_accessor :data_sources, :primary_keys
- @columns = Hash.new { |h,k| h[k] = [] }
+ @columns = Hash.new { |h, k| h[k] = [] }
class << self
attr_reader :columns
end
def initialize(connection, logger)
super
- @tables = []
+ @data_sources = []
@primary_keys = {}
@columns = self.class.columns
end
@@ -37,7 +39,7 @@ module ActiveRecord
@columns[table_name]
end
- def table_exists?(*)
+ def data_source_exists?(*)
true
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 1712ff0ac6..64c2b51f83 100644
--- a/activerecord/test/cases/adapter_test.rb
+++ b/activerecord/test/cases/adapter_test.rb
@@ -1,89 +1,135 @@
+# frozen_string_literal: true
+
require "cases/helper"
+require "support/connection_helper"
require "models/book"
require "models/post"
require "models/author"
+require "models/event"
module ActiveRecord
class AdapterTest < ActiveRecord::TestCase
def setup
@connection = ActiveRecord::Base.connection
+ @connection.materialize_transactions
end
##
# PostgreSQL does not support null bytes in strings
- unless current_adapter?(:PostgreSQLAdapter)
+ unless current_adapter?(:PostgreSQLAdapter) ||
+ (current_adapter?(:SQLite3Adapter) && !ActiveRecord::Base.connection.prepared_statements)
def test_update_prepared_statement
b = Book.create(name: "my \x00 book")
b.reload
assert_equal "my \x00 book", b.name
- b.update_attributes(name: "my other \x00 book")
+ b.update(name: "my other \x00 book")
b.reload
assert_equal "my other \x00 book", b.name
end
end
+ def test_create_record_with_pk_as_zero
+ Book.create(id: 0)
+ assert_equal 0, Book.find(0).id
+ assert_nothing_raised { Book.destroy(0) }
+ end
+
+ def test_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 = @connection.tables
- assert tables.include?("accounts")
- assert tables.include?("authors")
- assert tables.include?("tasks")
- assert tables.include?("topics")
+ assert_includes tables, "accounts"
+ assert_includes tables, "authors"
+ assert_includes tables, "tasks"
+ assert_includes tables, "topics"
end
def test_table_exists?
assert @connection.table_exists?("accounts")
- assert !@connection.table_exists?("nonexistingtable")
- assert !@connection.table_exists?(nil)
+ 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
+ data_sources = @connection.data_sources
+ assert_includes data_sources, "accounts"
+ assert_includes data_sources, "authors"
+ assert_includes data_sources, "tasks"
+ assert_includes data_sources, "topics"
+ end
+
+ def test_data_source_exists?
+ assert @connection.data_source_exists?("accounts")
+ assert @connection.data_source_exists?(:accounts)
+ assert_not @connection.data_source_exists?("nonexistingtable")
+ assert_not @connection.data_source_exists?("'")
+ assert_not @connection.data_source_exists?(nil)
end
def test_indexes
idx_name = "accounts_idx"
- if @connection.respond_to?(:indexes)
- indexes = @connection.indexes("accounts")
- assert indexes.empty?
-
- @connection.add_index :accounts, :firm_id, :name => idx_name
- indexes = @connection.indexes("accounts")
- assert_equal "accounts", indexes.first.table
- assert_equal idx_name, indexes.first.name
- assert !indexes.first.unique
- assert_equal ["firm_id"], indexes.first.columns
- else
- warn "#{@connection.class} does not respond to #indexes"
- end
+ indexes = @connection.indexes("accounts")
+ assert_empty indexes
+ @connection.add_index :accounts, :firm_id, name: idx_name
+ indexes = @connection.indexes("accounts")
+ assert_equal "accounts", indexes.first.table
+ assert_equal idx_name, indexes.first.name
+ assert_not indexes.first.unique
+ assert_equal ["firm_id"], indexes.first.columns
ensure
- @connection.remove_index(:accounts, :name => idx_name) rescue nil
+ @connection.remove_index(:accounts, name: idx_name) rescue nil
+ end
+
+ def test_remove_index_when_name_and_wrong_column_name_specified
+ index_name = "accounts_idx"
+
+ @connection.add_index :accounts, :firm_id, name: index_name
+ assert_raises ArgumentError do
+ @connection.remove_index :accounts, name: index_name, column: :wrong_column_name
+ end
+ ensure
+ @connection.remove_index(:accounts, name: index_name)
end
def test_current_database
if @connection.respond_to?(:current_database)
- assert_equal ARTest.connection_config['arunit']['database'], @connection.current_database
+ assert_equal ARTest.connection_config["arunit"]["database"], @connection.current_database
end
end
- if current_adapter?(:MysqlAdapter)
+ if current_adapter?(:Mysql2Adapter)
def test_charset
assert_not_nil @connection.charset
- assert_not_equal 'character_set_database', @connection.charset
- assert_equal @connection.show_variable('character_set_database'), @connection.charset
+ assert_not_equal "character_set_database", @connection.charset
+ assert_equal @connection.show_variable("character_set_database"), @connection.charset
end
def test_collation
assert_not_nil @connection.collation
- assert_not_equal 'collation_database', @connection.collation
- assert_equal @connection.show_variable('collation_database'), @connection.collation
+ assert_not_equal "collation_database", @connection.collation
+ assert_equal @connection.show_variable("collation_database"), @connection.collation
end
def test_show_nonexistent_variable_returns_nil
- assert_nil @connection.show_variable('foo_bar_baz')
+ assert_nil @connection.show_variable("foo_bar_baz")
end
def test_not_specifying_database_name_for_cross_database_selects
begin
assert_nothing_raised do
- ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['arunit'].except(:database))
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations["arunit"].except(:database))
config = ARTest.connection_config
ActiveRecord::Base.connection.execute(
@@ -104,9 +150,9 @@ module ActiveRecord
alias_method :table_alias_length, :test_table_alias_length
end
- assert_equal 'posts', @connection.table_alias_for('posts')
- assert_equal 'posts_comm', @connection.table_alias_for('posts_comments')
- assert_equal 'dbo_posts', @connection.table_alias_for('dbo.posts')
+ assert_equal "posts", @connection.table_alias_for("posts")
+ assert_equal "posts_comm", @connection.table_alias_for("posts_comments")
+ assert_equal "dbo_posts", @connection.table_alias_for("dbo.posts")
class << @connection
remove_method :table_alias_length
@@ -114,74 +160,61 @@ module ActiveRecord
end
end
- # test resetting sequences in odd tables in PostgreSQL
- if ActiveRecord::Base.connection.respond_to?(:reset_pk_sequence!)
- require 'models/movie'
- require 'models/subscriber'
-
- def test_reset_empty_table_with_custom_pk
- Movie.delete_all
- Movie.connection.reset_pk_sequence! 'movies'
- assert_equal 1, Movie.create(:name => 'fight club').id
+ def test_uniqueness_violations_are_translated_to_specific_exception
+ @connection.execute "INSERT INTO subscribers(nick) VALUES('me')"
+ error = assert_raises(ActiveRecord::RecordNotUnique) do
+ @connection.execute "INSERT INTO subscribers(nick) VALUES('me')"
end
- def test_reset_table_with_non_integer_pk
- Subscriber.delete_all
- Subscriber.connection.reset_pk_sequence! 'subscribers'
- sub = Subscriber.new(:name => 'robert drake')
- sub.id = 'bob drake'
- assert_nothing_raised { sub.save! }
- end
+ assert_not_nil error.cause
end
- def test_uniqueness_violations_are_translated_to_specific_exception
- @connection.execute "INSERT INTO subscribers(nick) VALUES('me')"
- assert_raises(ActiveRecord::RecordNotUnique) do
- @connection.execute "INSERT INTO subscribers(nick) VALUES('me')"
+ def test_not_null_violations_are_translated_to_specific_exception
+ error = assert_raises(ActiveRecord::NotNullViolation) do
+ Post.create
end
+
+ assert_not_nil error.cause
end
unless current_adapter?(:SQLite3Adapter)
- def test_foreign_key_violations_are_translated_to_specific_exception
- 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
+ def test_value_limit_violations_are_translated_to_specific_exception
+ error = assert_raises(ActiveRecord::ValueTooLong) do
+ Event.create(title: "abcdefgh")
end
+
+ assert_not_nil error.cause
end
- 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'
+ 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_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
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"
- end
+ def test_exceptions_from_notifications_are_not_translated
+ original_error = StandardError.new("This StandardError shouldn't get translated")
+ subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") { raise original_error }
+ actual_error = assert_raises(StandardError) do
+ @connection.execute("SELECT * FROM posts")
end
+
+ assert_equal original_error, actual_error
+
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
+ end
+
+ def test_database_related_exceptions_are_translated_to_statement_invalid
+ error = assert_raises(ActiveRecord::StatementInvalid) do
+ @connection.execute("This is a syntax error")
+ end
+
+ assert_instance_of ActiveRecord::StatementInvalid, error
+ assert_kind_of Exception, error.cause
end
def test_select_all_always_return_activerecord_result
@@ -189,22 +222,61 @@ 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(nil).to_sql}", nil, [[nil, post.id]])
+ assert_equal expected.to_a, result.to_a
+ end
+
+ def test_insert_update_delete_with_legacy_binds
+ binds = [[nil, 1]]
+ bind_param = Arel::Nodes::BindParam.new(nil)
+
+ id = @connection.insert("INSERT INTO events(id) VALUES (#{bind_param.to_sql})", nil, nil, nil, nil, binds)
+ assert_equal 1, id
+
+ @connection.update("UPDATE events SET title = 'foo' WHERE id = #{bind_param.to_sql}", nil, binds)
+ result = @connection.select_all("SELECT * FROM events WHERE id = #{bind_param.to_sql}", nil, binds)
+ assert_equal({ "id" => 1, "title" => "foo" }, result.first)
+
+ @connection.delete("DELETE FROM events WHERE id = #{bind_param.to_sql}", nil, binds)
+ result = @connection.select_all("SELECT * FROM events WHERE id = #{bind_param.to_sql}", nil, binds)
+ assert_nil result.first
+ end
+
+ def test_insert_update_delete_with_binds
+ binds = [Relation::QueryAttribute.new("id", 1, Type.default_value)]
+ bind_param = Arel::Nodes::BindParam.new(nil)
+
+ id = @connection.insert("INSERT INTO events(id) VALUES (#{bind_param.to_sql})", nil, nil, nil, nil, binds)
+ assert_equal 1, id
+
+ @connection.update("UPDATE events SET title = 'foo' WHERE id = #{bind_param.to_sql}", nil, binds)
+ result = @connection.select_all("SELECT * FROM events WHERE id = #{bind_param.to_sql}", nil, binds)
+ assert_equal({ "id" => 1, "title" => "foo" }, result.first)
+
+ @connection.delete("DELETE FROM events WHERE id = #{bind_param.to_sql}", nil, binds)
+ result = @connection.select_all("SELECT * FROM events WHERE id = #{bind_param.to_sql}", nil, binds)
+ assert_nil result.first
+ end
+ end
+
def test_select_methods_passing_a_association_relation
- author = Author.create!(name: 'john')
- Post.create!(author: author, title: 'foo', body: 'bar')
- query = author.posts.where(title: 'foo').select(:title)
- assert_equal({"title" => "foo"}, @connection.select_one(query.arel, nil, query.bound_attributes))
- assert_equal({"title" => "foo"}, @connection.select_one(query))
+ author = Author.create!(name: "john")
+ Post.create!(author: author, title: "foo", body: "bar")
+ query = author.posts.where(title: "foo").select(:title)
+ assert_equal({ "title" => "foo" }, @connection.select_one(query))
assert @connection.select_all(query).is_a?(ActiveRecord::Result)
assert_equal "foo", @connection.select_value(query)
assert_equal ["foo"], @connection.select_values(query)
end
def test_select_methods_passing_a_relation
- Post.create!(title: 'foo', body: 'bar')
- query = Post.where(title: 'foo').select(:title)
- assert_equal({"title" => "foo"}, @connection.select_one(query.arel, nil, query.bound_attributes))
- assert_equal({"title" => "foo"}, @connection.select_one(query))
+ Post.create!(title: "foo", body: "bar")
+ query = Post.where(title: "foo").select(:title)
+ assert_equal({ "title" => "foo" }, @connection.select_one(query))
assert @connection.select_all(query).is_a?(ActiveRecord::Result)
assert_equal "foo", @connection.select_value(query)
assert_equal ["foo"], @connection.select_values(query)
@@ -216,13 +288,111 @@ module ActiveRecord
unless current_adapter?(:PostgreSQLAdapter)
def test_log_invalid_encoding
- assert_raise ActiveRecord::StatementInvalid do
+ error = assert_raises RuntimeError do
@connection.send :log, "SELECT 'ы' FROM DUAL" do
- raise 'ы'.force_encoding(Encoding::ASCII_8BIT)
+ raise (+"ы").force_encoding(Encoding::ASCII_8BIT)
end
end
+
+ assert_equal "ы", error.message
+ end
+ end
+
+ def test_supports_multi_insert_is_deprecated
+ assert_deprecated { @connection.supports_multi_insert? }
+ end
+
+ def test_column_name_length_is_deprecated
+ assert_deprecated { @connection.column_name_length }
+ end
+
+ def test_table_name_length_is_deprecated
+ assert_deprecated { @connection.table_name_length }
+ end
+
+ def test_columns_per_table_is_deprecated
+ assert_deprecated { @connection.columns_per_table }
+ end
+
+ def test_indexes_per_table_is_deprecated
+ assert_deprecated { @connection.indexes_per_table }
+ end
+
+ def test_columns_per_multicolumn_index_is_deprecated
+ assert_deprecated { @connection.columns_per_multicolumn_index }
+ end
+
+ def test_sql_query_length_is_deprecated
+ assert_deprecated { @connection.sql_query_length }
+ end
+
+ def test_joins_per_query_is_deprecated
+ assert_deprecated { @connection.joins_per_query }
+ end
+ end
+
+ class AdapterForeignKeyTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
+
+ fixtures :fk_test_has_pk
+
+ 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
+
+ def test_foreign_key_violations_on_insert_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_foreign_key_violations_on_delete_are_translated_to_specific_exception
+ insert_into_fk_test_has_fk fk_id: 1
+
+ error = assert_raises(ActiveRecord::InvalidForeignKey) do
+ @connection.execute "DELETE FROM fk_test_has_pk WHERE pk_id = 1"
+ end
+
+ assert_not_nil error.cause
+ end
+
+ 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(fk_id: 0)
+ # 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},#{fk_id})"
+ else
+ @connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (#{fk_id})"
+ end
+ end
end
class AdapterTestWithoutTransaction < ActiveRecord::TestCase
@@ -243,16 +413,60 @@ module ActiveRecord
unless in_memory_db?
test "transaction state is reset after a reconnect" do
@connection.begin_transaction
- assert @connection.transaction_open?
+ assert_predicate @connection, :transaction_open?
@connection.reconnect!
- assert !@connection.transaction_open?
+ assert_not_predicate @connection, :transaction_open?
end
test "transaction state is reset after a disconnect" do
@connection.begin_transaction
- assert @connection.transaction_open?
+ assert_predicate @connection, :transaction_open?
@connection.disconnect!
- assert !@connection.transaction_open?
+ assert_not_predicate @connection, :transaction_open?
+ end
+ end
+
+ # test resetting sequences in odd tables in PostgreSQL
+ if ActiveRecord::Base.connection.respond_to?(:reset_pk_sequence!)
+ require "models/movie"
+ require "models/subscriber"
+
+ def test_reset_empty_table_with_custom_pk
+ Movie.delete_all
+ Movie.connection.reset_pk_sequence! "movies"
+ assert_equal 1, Movie.create(name: "fight club").id
+ end
+
+ def test_reset_table_with_non_integer_pk
+ Subscriber.delete_all
+ Subscriber.connection.reset_pk_sequence! "subscribers"
+ sub = Subscriber.new(name: "robert drake")
+ sub.id = "bob drake"
+ assert_nothing_raised { sub.save! }
+ end
+ end
+ end
+end
+
+if ActiveRecord::Base.connection.supports_advisory_locks?
+ class AdvisoryLocksEnabledTest < ActiveRecord::TestCase
+ include ConnectionHelper
+
+ def test_advisory_locks_enabled?
+ assert ActiveRecord::Base.connection.advisory_locks_enabled?
+
+ run_without_connection do |orig_connection|
+ ActiveRecord::Base.establish_connection(
+ orig_connection.merge(advisory_locks: false)
+ )
+
+ assert_not ActiveRecord::Base.connection.advisory_locks_enabled?
+
+ ActiveRecord::Base.establish_connection(
+ orig_connection.merge(advisory_locks: true)
+ )
+
+ assert ActiveRecord::Base.connection.advisory_locks_enabled?
end
end
end
diff --git a/activerecord/test/cases/adapters/mysql/active_schema_test.rb b/activerecord/test/cases/adapters/mysql/active_schema_test.rb
deleted file mode 100644
index f0fd95ac16..0000000000
--- a/activerecord/test/cases/adapters/mysql/active_schema_test.rb
+++ /dev/null
@@ -1,192 +0,0 @@
-require "cases/helper"
-require 'support/connection_helper'
-
-class MysqlActiveSchemaTest < ActiveRecord::MysqlTestCase
- include ConnectionHelper
-
- def setup
- ActiveRecord::Base.connection.singleton_class.class_eval do
- alias_method :execute_without_stub, :execute
- def execute(sql, name = nil) return sql end
- end
- end
-
- teardown do
- reset_connection
- end
-
- def test_add_index
- # add_index calls table_exists? and index_name_exists? which can't work since execute is stubbed
- def (ActiveRecord::Base.connection).table_exists?(*); true; end
- def (ActiveRecord::Base.connection).index_name_exists?(*); false; end
-
- expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`) "
- assert_equal expected, add_index(:people, :last_name, :length => nil)
-
- expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`(10)) "
- assert_equal expected, add_index(:people, :last_name, :length => 10)
-
- 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)
-
- 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})
-
- 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})
-
- %w(SPATIAL FULLTEXT UNIQUE).each do |type|
- expected = "CREATE #{type} INDEX `index_people_on_last_name` ON `people` (`last_name`) "
- assert_equal expected, add_index(:people, :last_name, :type => type)
- end
-
- %w(btree hash).each do |using|
- expected = "CREATE INDEX `index_people_on_last_name` USING #{using} ON `people` (`last_name`) "
- assert_equal expected, add_index(:people, :last_name, :using => using)
- end
-
- expected = "CREATE INDEX `index_people_on_last_name` USING btree ON `people` (`last_name`(10)) "
- assert_equal expected, add_index(:people, :last_name, :length => 10, :using => :btree)
-
- expected = "CREATE INDEX `index_people_on_last_name` USING btree ON `people` (`last_name`(10)) ALGORITHM = COPY"
- assert_equal expected, add_index(:people, :last_name, :length => 10, using: :btree, algorithm: :copy)
-
- assert_raise ArgumentError do
- add_index(:people, :last_name, algorithm: :coyp)
- end
-
- expected = "CREATE INDEX `index_people_on_last_name_and_first_name` USING btree ON `people` (`last_name`(15), `first_name`(15)) "
- assert_equal expected, add_index(:people, [:last_name, :first_name], :length => 15, :using => :btree)
- end
-
- def test_index_in_create
- def (ActiveRecord::Base.connection).table_exists?(*); false; end
-
- %w(SPATIAL FULLTEXT UNIQUE).each do |type|
- expected = "CREATE TABLE `people` (#{type} INDEX `index_people_on_last_name` (`last_name`) ) ENGINE=InnoDB"
- actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t|
- t.index :last_name, type: type
- end
- assert_equal expected, actual
- end
-
- expected = "CREATE TABLE `people` ( INDEX `index_people_on_last_name` USING btree (`last_name`(10)) ) ENGINE=InnoDB"
- actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t|
- t.index :last_name, length: 10, using: :btree
- end
- assert_equal expected, actual
- end
-
- def test_index_in_bulk_change
- def (ActiveRecord::Base.connection).table_exists?(*); true; end
- def (ActiveRecord::Base.connection).index_name_exists?(*); false; end
-
- %w(SPATIAL FULLTEXT UNIQUE).each do |type|
- expected = "ALTER TABLE `people` ADD #{type} INDEX `index_people_on_last_name` (`last_name`)"
- actual = ActiveRecord::Base.connection.change_table(:people, bulk: true) do |t|
- t.index :last_name, type: type
- end
- 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|
- t.index :last_name, length: 10, using: :btree, algorithm: :copy
- end
- assert_equal expected, actual
- end
-
- def test_drop_table
- assert_equal "DROP TABLE `people`", drop_table(:people)
- end
-
- if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
- def test_create_mysql_database_with_encoding
- assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8`", create_database(:matt)
- assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, {:charset => 'latin1'})
- assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT CHARACTER SET `big5` COLLATE `big5_chinese_ci`", create_database(:matt_aimonetti, {:charset => :big5, :collation => :big5_chinese_ci})
- end
-
- def test_recreate_mysql_database_with_encoding
- create_database(:luca, {:charset => 'latin1'})
- assert_equal "CREATE DATABASE `luca` DEFAULT CHARACTER SET `latin1`", recreate_database(:luca, {:charset => 'latin1'})
- end
- end
-
- def test_add_column
- assert_equal "ALTER TABLE `people` ADD `last_name` varchar(255)", add_column(:people, :last_name, :string)
- end
-
- def test_add_column_with_limit
- assert_equal "ALTER TABLE `people` ADD `key` varchar(32)", add_column(:people, :key, :string, :limit => 32)
- end
-
- def test_drop_table_with_specific_database
- assert_equal "DROP TABLE `otherdb`.`people`", drop_table('otherdb.people')
- end
-
- def test_add_timestamps
- with_real_execute do
- begin
- ActiveRecord::Base.connection.create_table :delete_me
- ActiveRecord::Base.connection.add_timestamps :delete_me, null: true
- assert column_present?('delete_me', 'updated_at', 'datetime')
- assert column_present?('delete_me', 'created_at', 'datetime')
- ensure
- ActiveRecord::Base.connection.drop_table :delete_me rescue nil
- end
- end
- end
-
- def test_remove_timestamps
- with_real_execute do
- begin
- ActiveRecord::Base.connection.create_table :delete_me do |t|
- t.timestamps null: true
- end
- ActiveRecord::Base.connection.remove_timestamps :delete_me, { null: true }
- assert !column_present?('delete_me', 'updated_at', 'datetime')
- assert !column_present?('delete_me', 'created_at', 'datetime')
- ensure
- ActiveRecord::Base.connection.drop_table :delete_me rescue nil
- end
- end
- end
-
- def test_indexes_in_create
- ActiveRecord::Base.connection.stubs(:table_exists?).with(:temp).returns(false)
- ActiveRecord::Base.connection.stubs(:index_name_exists?).with(:index_temp_on_zip).returns(false)
-
- expected = "CREATE TEMPORARY TABLE `temp` ( INDEX `index_temp_on_zip` (`zip`) ) ENGINE=InnoDB AS SELECT id, name, zip FROM a_really_complicated_query"
- actual = ActiveRecord::Base.connection.create_table(:temp, temporary: true, as: "SELECT id, name, zip FROM a_really_complicated_query") do |t|
- t.index :zip
- end
-
- assert_equal expected, actual
- end
-
- private
- def with_real_execute
- ActiveRecord::Base.connection.singleton_class.class_eval do
- alias_method :execute_with_stub, :execute
- remove_method :execute
- alias_method :execute, :execute_without_stub
- end
-
- yield
- ensure
- ActiveRecord::Base.connection.singleton_class.class_eval do
- remove_method :execute
- alias_method :execute, :execute_with_stub
- end
- end
-
- def method_missing(method_symbol, *arguments)
- ActiveRecord::Base.connection.send(method_symbol, *arguments)
- end
-
- def column_present?(table_name, column_name, type)
- results = ActiveRecord::Base.connection.select_all("SHOW FIELDS FROM #{table_name} LIKE '#{column_name}'")
- results.first && results.first['Type'] == type
- end
-end
diff --git a/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb
deleted file mode 100644
index 98d44315dd..0000000000
--- a/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb
+++ /dev/null
@@ -1,54 +0,0 @@
-require "cases/helper"
-
-class MysqlCaseSensitivityTest < ActiveRecord::MysqlTestCase
- class CollationTest < ActiveRecord::Base
- end
-
- repair_validations(CollationTest)
-
- def test_columns_include_collation_different_from_table
- assert_equal 'utf8_bin', CollationTest.columns_hash['string_cs_column'].collation
- assert_equal 'utf8_general_ci', CollationTest.columns_hash['string_ci_column'].collation
- end
-
- def test_case_sensitive
- assert !CollationTest.columns_hash['string_ci_column'].case_sensitive?
- assert CollationTest.columns_hash['string_cs_column'].case_sensitive?
- end
-
- def test_case_insensitive_comparison_for_ci_column
- CollationTest.validates_uniqueness_of(:string_ci_column, :case_sensitive => false)
- CollationTest.create!(:string_ci_column => 'A')
- invalid = CollationTest.new(:string_ci_column => 'a')
- queries = assert_sql { invalid.save }
- ci_uniqueness_query = queries.detect { |q| q.match(/string_ci_column/) }
- assert_no_match(/lower/i, ci_uniqueness_query)
- end
-
- def test_case_insensitive_comparison_for_cs_column
- CollationTest.validates_uniqueness_of(:string_cs_column, :case_sensitive => false)
- CollationTest.create!(:string_cs_column => 'A')
- invalid = CollationTest.new(:string_cs_column => 'a')
- queries = assert_sql { invalid.save }
- cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/) }
- assert_match(/lower/i, cs_uniqueness_query)
- end
-
- def test_case_sensitive_comparison_for_ci_column
- CollationTest.validates_uniqueness_of(:string_ci_column, :case_sensitive => true)
- CollationTest.create!(:string_ci_column => 'A')
- invalid = CollationTest.new(:string_ci_column => 'A')
- queries = assert_sql { invalid.save }
- ci_uniqueness_query = queries.detect { |q| q.match(/string_ci_column/) }
- assert_match(/binary/i, ci_uniqueness_query)
- end
-
- def test_case_sensitive_comparison_for_cs_column
- CollationTest.validates_uniqueness_of(:string_cs_column, :case_sensitive => true)
- CollationTest.create!(:string_cs_column => 'A')
- invalid = CollationTest.new(:string_cs_column => 'A')
- queries = assert_sql { invalid.save }
- cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/) }
- assert_no_match(/binary/i, cs_uniqueness_query)
- end
-end
diff --git a/activerecord/test/cases/adapters/mysql/charset_collation_test.rb b/activerecord/test/cases/adapters/mysql/charset_collation_test.rb
deleted file mode 100644
index f2117a97e6..0000000000
--- a/activerecord/test/cases/adapters/mysql/charset_collation_test.rb
+++ /dev/null
@@ -1,54 +0,0 @@
-require "cases/helper"
-require 'support/schema_dumping_helper'
-
-class MysqlCharsetCollationTest < ActiveRecord::MysqlTestCase
- include SchemaDumpingHelper
- self.use_transactional_tests = false
-
- setup do
- @connection = ActiveRecord::Base.connection
- @connection.create_table :charset_collations, force: true do |t|
- t.string :string_ascii_bin, charset: 'ascii', collation: 'ascii_bin'
- t.text :text_ucs2_unicode_ci, charset: 'ucs2', collation: 'ucs2_unicode_ci'
- end
- end
-
- teardown do
- @connection.drop_table :charset_collations, if_exists: true
- end
-
- test "string column with charset and collation" do
- column = @connection.columns(:charset_collations).find { |c| c.name == 'string_ascii_bin' }
- assert_equal :string, column.type
- assert_equal 'ascii_bin', column.collation
- end
-
- test "text column with charset and collation" do
- column = @connection.columns(:charset_collations).find { |c| c.name == 'text_ucs2_unicode_ci' }
- assert_equal :text, column.type
- assert_equal 'ucs2_unicode_ci', column.collation
- end
-
- test "add column with charset and collation" do
- @connection.add_column :charset_collations, :title, :string, charset: 'utf8', collation: 'utf8_bin'
-
- column = @connection.columns(:charset_collations).find { |c| c.name == 'title' }
- assert_equal :string, column.type
- assert_equal 'utf8_bin', column.collation
- end
-
- test "change column with charset and collation" do
- @connection.add_column :charset_collations, :description, :string, charset: 'utf8', collation: 'utf8_unicode_ci'
- @connection.change_column :charset_collations, :description, :text, charset: 'utf8', collation: 'utf8_general_ci'
-
- column = @connection.columns(:charset_collations).find { |c| c.name == 'description' }
- assert_equal :text, column.type
- assert_equal 'utf8_general_ci', column.collation
- end
-
- test "schema dump includes collation" do
- output = dump_table_schema("charset_collations")
- assert_match %r{t.string\s+"string_ascii_bin",\s+limit: 255,\s+collation: "ascii_bin"$}, output
- assert_match %r{t.text\s+"text_ucs2_unicode_ci",\s+limit: 65535,\s+collation: "ucs2_unicode_ci"$}, output
- end
-end
diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb
deleted file mode 100644
index ddbc007b87..0000000000
--- a/activerecord/test/cases/adapters/mysql/connection_test.rb
+++ /dev/null
@@ -1,191 +0,0 @@
-require "cases/helper"
-require 'support/connection_helper'
-require 'support/ddl_helper'
-
-class MysqlConnectionTest < ActiveRecord::MysqlTestCase
- include ConnectionHelper
- include DdlHelper
-
- class Klass < ActiveRecord::Base
- end
-
- def setup
- super
- @connection = ActiveRecord::Base.connection
- end
-
- def test_mysql_reconnect_attribute_after_connection_with_reconnect_true
- run_without_connection do |orig_connection|
- ActiveRecord::Base.establish_connection(orig_connection.merge({:reconnect => true}))
- assert ActiveRecord::Base.connection.raw_connection.reconnect
- end
- end
-
- unless ARTest.connection_config['arunit']['socket']
- def test_connect_with_url
- run_without_connection do
- ar_config = ARTest.connection_config['arunit']
-
- url = "mysql://#{ar_config["username"]}@localhost/#{ar_config["database"]}"
- Klass.establish_connection(url)
- assert_equal ar_config['database'], Klass.connection.current_database
- end
- end
- end
-
- def test_mysql_reconnect_attribute_after_connection_with_reconnect_false
- run_without_connection do |orig_connection|
- ActiveRecord::Base.establish_connection(orig_connection.merge({:reconnect => false}))
- assert !ActiveRecord::Base.connection.raw_connection.reconnect
- end
- end
-
- def test_no_automatic_reconnection_after_timeout
- assert @connection.active?
- @connection.update('set @@wait_timeout=1')
- sleep 2
- assert !@connection.active?
-
- # Repair all fixture connections so other tests won't break.
- @fixture_connections.each(&:verify!)
- end
-
- def test_successful_reconnection_after_timeout_with_manual_reconnect
- assert @connection.active?
- @connection.update('set @@wait_timeout=1')
- sleep 2
- @connection.reconnect!
- assert @connection.active?
- end
-
- def test_successful_reconnection_after_timeout_with_verify
- assert @connection.active?
- @connection.update('set @@wait_timeout=1')
- sleep 2
- @connection.verify!
- assert @connection.active?
- end
-
- def test_bind_value_substitute
- bind_param = @connection.substitute_at('foo')
- assert_equal Arel.sql('?'), bind_param.to_sql
- end
-
- def test_exec_no_binds
- with_example_table do
- result = @connection.exec_query('SELECT id, data FROM ex')
- assert_equal 0, result.rows.length
- assert_equal 2, result.columns.length
- assert_equal %w{ id data }, result.columns
-
- @connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
-
- # if there are no bind parameters, it will return a string (due to
- # the libmysql api)
- result = @connection.exec_query('SELECT id, data FROM ex')
- assert_equal 1, result.rows.length
- assert_equal 2, result.columns.length
-
- assert_equal [['1', 'foo']], result.rows
- end
- end
-
- def test_exec_with_binds
- with_example_table do
- @connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
- result = @connection.exec_query(
- 'SELECT id, data FROM ex WHERE id = ?', nil, [ActiveRecord::Relation::QueryAttribute.new("id", 1, ActiveRecord::Type::Value.new)])
-
- assert_equal 1, result.rows.length
- assert_equal 2, result.columns.length
-
- assert_equal [[1, 'foo']], result.rows
- end
- end
-
- def test_exec_typecasts_bind_vals
- with_example_table do
- @connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
- bind = ActiveRecord::Relation::QueryAttribute.new("id", "1-fuu", ActiveRecord::Type::Integer.new)
-
- result = @connection.exec_query(
- 'SELECT id, data FROM ex WHERE id = ?', nil, [bind])
-
- assert_equal 1, result.rows.length
- assert_equal 2, result.columns.length
-
- assert_equal [[1, 'foo']], result.rows
- end
- end
-
- # Test that MySQL allows multiple results for stored procedures
- if defined?(Mysql) && Mysql.const_defined?(:CLIENT_MULTI_RESULTS)
- def test_multi_results
- rows = ActiveRecord::Base.connection.select_rows('CALL ten();')
- assert_equal 10, rows[0][0].to_i, "ten() did not return 10 as expected: #{rows.inspect}"
- assert @connection.active?, "Bad connection use by 'MysqlAdapter.select_rows'"
- 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')
- end
-
- def test_mysql_default_in_strict_mode
- result = @connection.exec_query "SELECT @@SESSION.sql_mode"
- assert_equal [["STRICT_ALL_TABLES"]], result.rows
- end
-
- def test_mysql_strict_mode_disabled
- run_without_connection do |orig_connection|
- ActiveRecord::Base.establish_connection(orig_connection.merge({:strict => false}))
- result = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode"
- assert_equal [['']], result.rows
- end
- end
-
- def test_mysql_strict_mode_specified_default
- run_without_connection do |orig_connection|
- ActiveRecord::Base.establish_connection(orig_connection.merge({strict: :default}))
- global_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@GLOBAL.sql_mode"
- session_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode"
- assert_equal global_sql_mode.rows, session_sql_mode.rows
- end
- end
-
- def test_mysql_set_session_variable
- run_without_connection do |orig_connection|
- ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:default_week_format => 3}}))
- session_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.DEFAULT_WEEK_FORMAT"
- assert_equal 3, session_mode.rows.first.first.to_i
- end
- end
-
- def test_mysql_sql_mode_variable_overrides_strict_mode
- run_without_connection do |orig_connection|
- ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { 'sql_mode' => 'ansi' }))
- result = ActiveRecord::Base.connection.exec_query 'SELECT @@SESSION.sql_mode'
- assert_not_equal [['STRICT_ALL_TABLES']], result.rows
- end
- end
-
- def test_mysql_set_session_variable_to_default
- run_without_connection do |orig_connection|
- ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:default_week_format => :default}}))
- global_mode = ActiveRecord::Base.connection.exec_query "SELECT @@GLOBAL.DEFAULT_WEEK_FORMAT"
- session_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.DEFAULT_WEEK_FORMAT"
- assert_equal global_mode.rows, session_mode.rows
- end
- end
-
- private
-
- def with_example_table(&block)
- definition ||= <<-SQL
- `id` int(11) auto_increment PRIMARY KEY,
- `data` varchar(255)
- SQL
- super(@connection, 'ex', definition, &block)
- end
-end
diff --git a/activerecord/test/cases/adapters/mysql/consistency_test.rb b/activerecord/test/cases/adapters/mysql/consistency_test.rb
deleted file mode 100644
index 743f6436e4..0000000000
--- a/activerecord/test/cases/adapters/mysql/consistency_test.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-require "cases/helper"
-
-class MysqlConsistencyTest < ActiveRecord::MysqlTestCase
- self.use_transactional_tests = false
-
- class Consistency < ActiveRecord::Base
- self.table_name = "mysql_consistency"
- end
-
- setup do
- @old_emulate_booleans = ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans
- ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = false
-
- @connection = ActiveRecord::Base.connection
- @connection.clear_cache!
- @connection.create_table("mysql_consistency") do |t|
- t.boolean "a_bool"
- t.string "a_string"
- end
- Consistency.reset_column_information
- Consistency.create!
- end
-
- teardown do
- ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = @old_emulate_booleans
- @connection.drop_table "mysql_consistency"
- end
-
- test "boolean columns with random value type cast to 0 when emulate_booleans is false" do
- with_new = Consistency.new
- with_last = Consistency.last
- with_new.a_bool = 'wibble'
- with_last.a_bool = 'wibble'
-
- assert_equal 0, with_new.a_bool
- assert_equal 0, with_last.a_bool
- end
-
- test "string columns call #to_s" do
- with_new = Consistency.new
- with_last = Consistency.last
- thing = Object.new
- with_new.a_string = thing
- with_last.a_string = thing
-
- assert_equal thing.to_s, with_new.a_string
- assert_equal thing.to_s, with_last.a_string
- end
-end
diff --git a/activerecord/test/cases/adapters/mysql/enum_test.rb b/activerecord/test/cases/adapters/mysql/enum_test.rb
deleted file mode 100644
index ef8ee0a6e3..0000000000
--- a/activerecord/test/cases/adapters/mysql/enum_test.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-require "cases/helper"
-
-class MysqlEnumTest < ActiveRecord::MysqlTestCase
- class EnumTest < ActiveRecord::Base
- end
-
- def test_enum_limit
- assert_equal 6, EnumTest.columns.first.limit
- end
-end
diff --git a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
deleted file mode 100644
index b804cb45b9..0000000000
--- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
+++ /dev/null
@@ -1,152 +0,0 @@
-
-require "cases/helper"
-require 'support/ddl_helper'
-
-module ActiveRecord
- module ConnectionAdapters
- class MysqlAdapterTest < ActiveRecord::MysqlTestCase
- include DdlHelper
-
- def setup
- @conn = ActiveRecord::Base.connection
- end
-
- def test_bad_connection_mysql
- assert_raise ActiveRecord::NoDatabaseError do
- configuration = ActiveRecord::Base.configurations['arunit'].merge(database: 'inexistent_activerecord_unittest')
- connection = ActiveRecord::Base.mysql_connection(configuration)
- connection.drop_table 'ex', if_exists: true
- 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_client_encoding
- assert_equal Encoding::UTF_8, @conn.client_encoding
- end
-
- def test_exec_insert_number
- with_example_table do
- insert(@conn, 'number' => 10)
-
- result = @conn.exec_query('SELECT number FROM ex WHERE number = 10')
-
- assert_equal 1, result.rows.length
- # if there are no bind parameters, it will return a string (due to
- # the libmysql api)
- assert_equal '10', result.rows.last.last
- end
- end
-
- def test_exec_insert_string
- with_example_table do
- str = 'いただきます!'
- insert(@conn, 'number' => 10, 'data' => str)
-
- result = @conn.exec_query('SELECT number, data FROM ex WHERE number = 10')
-
- value = result.rows.last.last
-
- # FIXME: this should probably be inside the mysql AR adapter?
- value.force_encoding(@conn.client_encoding)
-
- # The strings in this file are utf-8, so transcode to utf-8
- value.encode!(Encoding::UTF_8)
-
- assert_equal str, value
- end
- end
-
- def test_tables_quoting
- @conn.tables(nil, "foo-bar", nil)
- flunk
- rescue => e
- # assertion for *quoted* database properly
- assert_match(/database 'foo-bar'/, e.inspect)
- end
-
- def test_pk_and_sequence_for
- with_example_table do
- pk, seq = @conn.pk_and_sequence_for('ex')
- assert_equal 'id', pk
- assert_equal @conn.default_sequence_name('ex', 'id'), seq
- end
- end
-
- def test_pk_and_sequence_for_with_non_standard_primary_key
- with_example_table '`code` INT(11) auto_increment, PRIMARY KEY (`code`)' do
- pk, seq = @conn.pk_and_sequence_for('ex')
- assert_equal 'code', pk
- assert_equal @conn.default_sequence_name('ex', 'code'), seq
- end
- end
-
- def test_pk_and_sequence_for_with_custom_index_type_pk
- with_example_table '`id` INT(11) auto_increment, PRIMARY KEY USING BTREE (`id`)' do
- pk, seq = @conn.pk_and_sequence_for('ex')
- assert_equal 'id', pk
- assert_equal @conn.default_sequence_name('ex', 'id'), seq
- end
- end
-
- def test_composite_primary_key
- with_example_table '`id` INT(11), `number` INT(11), foo INT(11), PRIMARY KEY (`id`, `number`)' do
- assert_nil @conn.primary_key('ex')
- end
- end
-
- def test_tinyint_integer_typecasting
- with_example_table '`status` TINYINT(4)' do
- insert(@conn, { 'status' => 2 }, 'ex')
-
- result = @conn.exec_query('SELECT status FROM ex')
-
- assert_equal 2, result.column_types['status'].deserialize(result.last['status'])
- end
- end
-
- def test_supports_extensions
- assert_not @conn.supports_extensions?, 'does not support extensions'
- end
-
- def test_respond_to_enable_extension
- assert @conn.respond_to?(:enable_extension)
- end
-
- def test_respond_to_disable_extension
- assert @conn.respond_to?(:disable_extension)
- end
-
- private
- def insert(ctx, data, table='ex')
- binds = data.map { |name, value|
- Relation::QueryAttribute.new(name, value, Type::Value.new)
- }
- columns = binds.map(&:name)
-
- sql = "INSERT INTO #{table} (#{columns.join(", ")})
- VALUES (#{(['?'] * columns.length).join(', ')})"
-
- ctx.exec_insert(sql, 'SQL', binds)
- end
-
- def with_example_table(definition = nil, &block)
- definition ||= <<-SQL
- `id` int(11) auto_increment PRIMARY KEY,
- `number` integer,
- `data` varchar(255)
- SQL
- super(@conn, 'ex', definition, &block)
- end
- end
- end
-end
diff --git a/activerecord/test/cases/adapters/mysql/quoting_test.rb b/activerecord/test/cases/adapters/mysql/quoting_test.rb
deleted file mode 100644
index a296cf9d31..0000000000
--- a/activerecord/test/cases/adapters/mysql/quoting_test.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-require "cases/helper"
-
-class MysqlQuotingTest < ActiveRecord::MysqlTestCase
- def setup
- @conn = ActiveRecord::Base.connection
- end
-
- def test_type_cast_true
- assert_equal 1, @conn.type_cast(true)
- end
-
- def test_type_cast_false
- assert_equal 0, @conn.type_cast(false)
- end
-end
diff --git a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb
deleted file mode 100644
index 4ea1d9ad36..0000000000
--- a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb
+++ /dev/null
@@ -1,153 +0,0 @@
-require "cases/helper"
-
-# a suite of tests to ensure the ConnectionAdapters#MysqlAdapter can handle tables with
-# reserved word names (ie: group, order, values, etc...)
-class MysqlReservedWordTest < ActiveRecord::MysqlTestCase
- class Group < ActiveRecord::Base
- Group.table_name = 'group'
- belongs_to :select
- has_one :values
- end
-
- class Select < ActiveRecord::Base
- Select.table_name = 'select'
- has_many :groups
- end
-
- class Values < ActiveRecord::Base
- Values.table_name = 'values'
- end
-
- class Distinct < ActiveRecord::Base
- Distinct.table_name = 'distinct'
- has_and_belongs_to_many :selects
- has_many :values, :through => :groups
- end
-
- def setup
- @connection = ActiveRecord::Base.connection
-
- # we call execute directly here (and do similar below) because ActiveRecord::Base#create_table()
- # will fail with these table names if these test cases fail
-
- create_tables_directly 'group'=>'id int auto_increment primary key, `order` varchar(255), select_id int',
- 'select'=>'id int auto_increment primary key',
- 'values'=>'id int auto_increment primary key, group_id int',
- 'distinct'=>'id int auto_increment primary key',
- 'distinct_select'=>'distinct_id int, select_id int'
- end
-
- teardown do
- drop_tables_directly ['group', 'select', 'values', 'distinct', 'distinct_select', 'order']
- end
-
- # create tables with reserved-word names and columns
- def test_create_tables
- assert_nothing_raised {
- @connection.create_table :order do |t|
- t.column :group, :string
- end
- }
- end
-
- # rename tables with reserved-word names
- def test_rename_tables
- assert_nothing_raised { @connection.rename_table(:group, :order) }
- end
-
- # alter column with a reserved-word name in a table with a reserved-word name
- def test_change_columns
- assert_nothing_raised { @connection.change_column_default(:group, :order, 'whatever') }
- #the quoting here will reveal any double quoting issues in change_column's interaction with the column method in the adapter
- assert_nothing_raised { @connection.change_column('group', 'order', :Int, :default => 0) }
- assert_nothing_raised { @connection.rename_column(:group, :order, :values) }
- end
-
- # introspect table with reserved word name
- def test_introspect
- assert_nothing_raised { @connection.columns(:group) }
- assert_nothing_raised { @connection.indexes(:group) }
- end
-
- #fixtures
- self.use_instantiated_fixtures = true
- self.use_transactional_tests = false
-
- #activerecord model class with reserved-word table name
- def test_activerecord_model
- create_test_fixtures :select, :distinct, :group, :values, :distinct_select
- x = nil
- assert_nothing_raised { x = Group.new }
- x.order = 'x'
- assert_nothing_raised { x.save }
- x.order = 'y'
- assert_nothing_raised { x.save }
- assert_nothing_raised { Group.find_by_order('y') }
- assert_nothing_raised { Group.find(1) }
- Group.find(1)
- end
-
- # has_one association with reserved-word table name
- def test_has_one_associations
- create_test_fixtures :select, :distinct, :group, :values, :distinct_select
- v = nil
- assert_nothing_raised { v = Group.find(1).values }
- assert_equal 2, v.id
- end
-
- # belongs_to association with reserved-word table name
- def test_belongs_to_associations
- create_test_fixtures :select, :distinct, :group, :values, :distinct_select
- gs = nil
- assert_nothing_raised { gs = Select.find(2).groups }
- assert_equal gs.length, 2
- assert(gs.collect(&:id).sort == [2, 3])
- end
-
- # has_and_belongs_to_many with reserved-word table name
- def test_has_and_belongs_to_many
- create_test_fixtures :select, :distinct, :group, :values, :distinct_select
- s = nil
- assert_nothing_raised { s = Distinct.find(1).selects }
- assert_equal s.length, 2
- assert(s.collect(&:id).sort == [1, 2])
- end
-
- # activerecord model introspection with reserved-word table and column names
- def test_activerecord_introspection
- assert_nothing_raised { Group.table_exists? }
- assert_nothing_raised { Group.columns }
- end
-
- # Calculations
- def test_calculations_work_with_reserved_words
- assert_nothing_raised { Group.count }
- end
-
- def test_associations_work_with_reserved_words
- assert_nothing_raised { Select.all.merge!(:includes => [:groups]).to_a }
- end
-
- #the following functions were added to DRY test cases
-
- private
- # custom fixture loader, uses FixtureSet#create_fixtures and appends base_path to the current file's path
- def create_test_fixtures(*fixture_names)
- ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT + "/reserved_words", fixture_names)
- end
-
- # custom drop table, uses execute on connection to drop a table if it exists. note: escapes table_name
- def drop_tables_directly(table_names, connection = @connection)
- table_names.each do |name|
- connection.drop_table name, if_exists: true
- end
- 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)
- tables.each do |table_name, column_properties|
- connection.execute("CREATE TABLE `#{table_name}` ( #{column_properties} )")
- end
- end
-
-end
diff --git a/activerecord/test/cases/adapters/mysql/schema_test.rb b/activerecord/test/cases/adapters/mysql/schema_test.rb
deleted file mode 100644
index 2e18f609fd..0000000000
--- a/activerecord/test/cases/adapters/mysql/schema_test.rb
+++ /dev/null
@@ -1,100 +0,0 @@
-require "cases/helper"
-require 'models/post'
-require 'models/comment'
-
-module ActiveRecord
- module ConnectionAdapters
- class MysqlSchemaTest < ActiveRecord::MysqlTestCase
- fixtures :posts
-
- def setup
- @connection = ActiveRecord::Base.connection
- db = Post.connection_pool.spec.config[:database]
- table = Post.table_name
- @db_name = db
-
- @omgpost = Class.new(ActiveRecord::Base) do
- self.table_name = "#{db}.#{table}"
- def self.name; 'Post'; end
- end
-
- @connection.create_table "mysql_doubles"
- end
-
- teardown do
- @connection.drop_table "mysql_doubles", if_exists: true
- end
-
- class MysqlDouble < ActiveRecord::Base
- self.table_name = "mysql_doubles"
- end
-
- def test_float_limits
- @connection.add_column :mysql_doubles, :float_no_limit, :float
- @connection.add_column :mysql_doubles, :float_short, :float, limit: 5
- @connection.add_column :mysql_doubles, :float_long, :float, limit: 53
-
- @connection.add_column :mysql_doubles, :float_23, :float, limit: 23
- @connection.add_column :mysql_doubles, :float_24, :float, limit: 24
- @connection.add_column :mysql_doubles, :float_25, :float, limit: 25
- MysqlDouble.reset_column_information
-
- column_no_limit = MysqlDouble.columns.find { |c| c.name == 'float_no_limit' }
- column_short = MysqlDouble.columns.find { |c| c.name == 'float_short' }
- column_long = MysqlDouble.columns.find { |c| c.name == 'float_long' }
-
- column_23 = MysqlDouble.columns.find { |c| c.name == 'float_23' }
- column_24 = MysqlDouble.columns.find { |c| c.name == 'float_24' }
- column_25 = MysqlDouble.columns.find { |c| c.name == 'float_25' }
-
- # Mysql floats are precision 0..24, Mysql doubles are precision 25..53
- assert_equal 24, column_no_limit.limit
- assert_equal 24, column_short.limit
- assert_equal 53, column_long.limit
-
- assert_equal 24, column_23.limit
- assert_equal 24, column_24.limit
- assert_equal 53, column_25.limit
- end
-
- def test_schema
- assert @omgpost.first
- end
-
- def test_primary_key
- assert_equal 'id', @omgpost.primary_key
- end
-
- def test_table_exists?
- name = @omgpost.table_name
- assert @connection.table_exists?(name), "#{name} table should exist"
- end
-
- def test_table_exists_wrong_schema
- assert(!@connection.table_exists?("#{@db_name}.zomg"), "table should not exist")
- end
-
- def test_dump_indexes
- index_a_name = 'index_key_tests_on_snack'
- index_b_name = 'index_key_tests_on_pizza'
- index_c_name = 'index_key_tests_on_awesome'
-
- table = 'key_tests'
-
- indexes = @connection.indexes(table).sort_by(&:name)
- assert_equal 3,indexes.size
-
- index_a = indexes.select{|i| i.name == index_a_name}[0]
- index_b = indexes.select{|i| i.name == index_b_name}[0]
- index_c = indexes.select{|i| i.name == index_c_name}[0]
- assert_equal :btree, index_a.using
- assert_nil index_a.type
- assert_equal :btree, index_b.using
- assert_nil index_b.type
-
- assert_nil index_c.using
- assert_equal :fulltext, index_c.type
- end
- end
- end
-end
diff --git a/activerecord/test/cases/adapters/mysql/sp_test.rb b/activerecord/test/cases/adapters/mysql/sp_test.rb
deleted file mode 100644
index a3d5110032..0000000000
--- a/activerecord/test/cases/adapters/mysql/sp_test.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-require "cases/helper"
-require 'models/topic'
-
-class StoredProcedureTest < ActiveRecord::MysqlTestCase
- fixtures :topics
-
- # Test that MySQL allows multiple results for stored procedures
- if defined?(Mysql) && Mysql.const_defined?(:CLIENT_MULTI_RESULTS)
- def test_multi_results_from_find_by_sql
- topics = Topic.find_by_sql 'CALL topics();'
- assert_equal 1, topics.size
- assert ActiveRecord::Base.connection.active?, "Bad connection use by 'MysqlAdapter.select'"
- end
- end
-end
diff --git a/activerecord/test/cases/adapters/mysql/sql_types_test.rb b/activerecord/test/cases/adapters/mysql/sql_types_test.rb
deleted file mode 100644
index 25b28de7f0..0000000000
--- a/activerecord/test/cases/adapters/mysql/sql_types_test.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-require "cases/helper"
-
-class MysqlSqlTypesTest < ActiveRecord::MysqlTestCase
- def test_binary_types
- assert_equal 'varbinary(64)', type_to_sql(:binary, 64)
- assert_equal 'varbinary(4095)', type_to_sql(:binary, 4095)
- assert_equal 'blob(4096)', type_to_sql(:binary, 4096)
- assert_equal 'blob', type_to_sql(:binary)
- end
-
- def type_to_sql(*args)
- ActiveRecord::Base.connection.type_to_sql(*args)
- end
-end
diff --git a/activerecord/test/cases/adapters/mysql/statement_pool_test.rb b/activerecord/test/cases/adapters/mysql/statement_pool_test.rb
deleted file mode 100644
index 6be36566de..0000000000
--- a/activerecord/test/cases/adapters/mysql/statement_pool_test.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-require 'cases/helper'
-
-class MysqlStatementPoolTest < ActiveRecord::MysqlTestCase
- if Process.respond_to?(:fork)
- def test_cache_is_per_pid
- cache = ActiveRecord::ConnectionAdapters::MysqlAdapter::StatementPool.new nil, 10
- cache['foo'] = 'bar'
- assert_equal 'bar', cache['foo']
-
- pid = fork {
- lookup = cache['foo'];
- exit!(!lookup)
- }
-
- Process.waitpid pid
- assert $?.success?, 'process should exit successfully'
- end
- end
-end
diff --git a/activerecord/test/cases/adapters/mysql/table_options_test.rb b/activerecord/test/cases/adapters/mysql/table_options_test.rb
deleted file mode 100644
index 99df6d6cba..0000000000
--- a/activerecord/test/cases/adapters/mysql/table_options_test.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-require "cases/helper"
-require 'support/schema_dumping_helper'
-
-class MysqlTableOptionsTest < ActiveRecord::MysqlTestCase
- include SchemaDumpingHelper
-
- def setup
- @connection = ActiveRecord::Base.connection
- end
-
- def teardown
- @connection.drop_table "mysql_table_options", if_exists: true
- end
-
- test "table options with ENGINE" do
- @connection.create_table "mysql_table_options", force: true, options: "ENGINE=MyISAM"
- output = dump_table_schema("mysql_table_options")
- options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options]
- assert_match %r{ENGINE=MyISAM}, options
- end
-
- test "table options with ROW_FORMAT" do
- @connection.create_table "mysql_table_options", force: true, options: "ROW_FORMAT=REDUNDANT"
- output = dump_table_schema("mysql_table_options")
- options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options]
- assert_match %r{ROW_FORMAT=REDUNDANT}, options
- end
-
- test "table options with CHARSET" do
- @connection.create_table "mysql_table_options", force: true, options: "CHARSET=utf8mb4"
- output = dump_table_schema("mysql_table_options")
- options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options]
- assert_match %r{CHARSET=utf8mb4}, options
- end
-
- test "table options with COLLATE" do
- @connection.create_table "mysql_table_options", force: true, options: "COLLATE=utf8mb4_bin"
- output = dump_table_schema("mysql_table_options")
- options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options]
- assert_match %r{COLLATE=utf8mb4_bin}, options
- end
-end
diff --git a/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb b/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb
deleted file mode 100644
index ed9398a918..0000000000
--- a/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-require "cases/helper"
-
-class MysqlUnsignedTypeTest < ActiveRecord::MysqlTestCase
- self.use_transactional_tests = false
-
- class UnsignedType < ActiveRecord::Base
- end
-
- setup do
- @connection = ActiveRecord::Base.connection
- @connection.create_table("unsigned_types", force: true) do |t|
- t.column :unsigned_integer, "int unsigned"
- end
- end
-
- teardown do
- @connection.drop_table "unsigned_types"
- end
-
- test "unsigned int max value is in range" do
- assert expected = UnsignedType.create(unsigned_integer: 4294967295)
- assert_equal expected, UnsignedType.find_by(unsigned_integer: 4294967295)
- end
-
- test "minus value is out of range" do
- assert_raise(RangeError) do
- UnsignedType.create(unsigned_integer: -10)
- end
- end
-end
diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
index 6558d60aa1..261fee13eb 100644
--- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'support/connection_helper'
+require "support/connection_helper"
class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
include ConnectionHelper
@@ -7,7 +9,7 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
def setup
ActiveRecord::Base.connection.singleton_class.class_eval do
alias_method :execute_without_stub, :execute
- def execute(sql, name = nil) return sql end
+ def execute(sql, name = nil) sql end
end
end
@@ -16,61 +18,64 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
end
def test_add_index
- # add_index calls table_exists? and index_name_exists? which can't work since execute is stubbed
- def (ActiveRecord::Base.connection).table_exists?(*); true; end
+ # add_index calls data_source_exists? and index_name_exists? which can't work since execute is stubbed
+ def (ActiveRecord::Base.connection).data_source_exists?(*); true; end
def (ActiveRecord::Base.connection).index_name_exists?(*); false; end
expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`) "
- assert_equal expected, add_index(:people, :last_name, :length => nil)
+ assert_equal expected, add_index(:people, :last_name, length: nil)
expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`(10)) "
- assert_equal expected, add_index(:people, :last_name, :length => 10)
+ assert_equal expected, add_index(:people, :last_name, length: 10)
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)
+ 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 })
+ 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 })
+ 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`) "
- assert_equal expected, add_index(:people, :last_name, :type => type)
+ assert_equal expected, add_index(:people, :last_name, type: type)
end
%w(btree hash).each do |using|
expected = "CREATE INDEX `index_people_on_last_name` USING #{using} ON `people` (`last_name`) "
- assert_equal expected, add_index(:people, :last_name, :using => using)
+ assert_equal expected, add_index(:people, :last_name, using: using)
end
expected = "CREATE INDEX `index_people_on_last_name` USING btree ON `people` (`last_name`(10)) "
- assert_equal expected, add_index(:people, :last_name, :length => 10, :using => :btree)
+ assert_equal expected, add_index(:people, :last_name, length: 10, using: :btree)
expected = "CREATE INDEX `index_people_on_last_name` USING btree ON `people` (`last_name`(10)) ALGORITHM = COPY"
- assert_equal expected, add_index(:people, :last_name, :length => 10, using: :btree, algorithm: :copy)
+ assert_equal expected, add_index(:people, :last_name, length: 10, using: :btree, algorithm: :copy)
assert_raise ArgumentError do
add_index(:people, :last_name, algorithm: :coyp)
end
expected = "CREATE INDEX `index_people_on_last_name_and_first_name` USING btree ON `people` (`last_name`(15), `first_name`(15)) "
- assert_equal expected, add_index(:people, [:last_name, :first_name], :length => 15, :using => :btree)
+ assert_equal expected, add_index(:people, [:last_name, :first_name], length: 15, using: :btree)
end
def test_index_in_create
- def (ActiveRecord::Base.connection).table_exists?(*); false; end
+ def (ActiveRecord::Base.connection).data_source_exists?(*); false; end
%w(SPATIAL FULLTEXT UNIQUE).each do |type|
- expected = "CREATE TABLE `people` (#{type} INDEX `index_people_on_last_name` (`last_name`) ) ENGINE=InnoDB"
+ expected = "CREATE TABLE `people` (#{type} INDEX `index_people_on_last_name` (`last_name`))"
actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t|
t.index :last_name, type: type
end
assert_equal expected, actual
end
- expected = "CREATE TABLE `people` ( INDEX `index_people_on_last_name` USING btree (`last_name`(10)) ) ENGINE=InnoDB"
+ expected = "CREATE TABLE `people` ( INDEX `index_people_on_last_name` USING btree (`last_name`(10)))"
actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t|
t.index :last_name, length: 10, using: :btree
end
@@ -78,7 +83,7 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
end
def test_index_in_bulk_change
- def (ActiveRecord::Base.connection).table_exists?(*); true; end
+ def (ActiveRecord::Base.connection).data_source_exists?(*); true; end
def (ActiveRecord::Base.connection).index_name_exists?(*); false; end
%w(SPATIAL FULLTEXT UNIQUE).each do |type|
@@ -89,8 +94,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
@@ -100,17 +105,15 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
assert_equal "DROP TABLE `people`", drop_table(:people)
end
- if current_adapter?(:Mysql2Adapter)
- def test_create_mysql_database_with_encoding
- assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8`", create_database(:matt)
- assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, {:charset => 'latin1'})
- assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT CHARACTER SET `big5` COLLATE `big5_chinese_ci`", create_database(:matt_aimonetti, {:charset => :big5, :collation => :big5_chinese_ci})
- end
+ def test_create_mysql_database_with_encoding
+ assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8mb4`", create_database(:matt)
+ assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, charset: "latin1")
+ assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT COLLATE `utf8mb4_bin`", create_database(:matt_aimonetti, collation: "utf8mb4_bin")
+ end
- def test_recreate_mysql_database_with_encoding
- create_database(:luca, {:charset => 'latin1'})
- assert_equal "CREATE DATABASE `luca` DEFAULT CHARACTER SET `latin1`", recreate_database(:luca, {:charset => 'latin1'})
- end
+ def test_recreate_mysql_database_with_encoding
+ create_database(:luca, charset: "latin1")
+ assert_equal "CREATE DATABASE `luca` DEFAULT CHARACTER SET `latin1`", recreate_database(:luca, charset: "latin1")
end
def test_add_column
@@ -118,11 +121,11 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
end
def test_add_column_with_limit
- assert_equal "ALTER TABLE `people` ADD `key` varchar(32)", add_column(:people, :key, :string, :limit => 32)
+ assert_equal "ALTER TABLE `people` ADD `key` varchar(32)", add_column(:people, :key, :string, limit: 32)
end
def test_drop_table_with_specific_database
- assert_equal "DROP TABLE `otherdb`.`people`", drop_table('otherdb.people')
+ assert_equal "DROP TABLE `otherdb`.`people`", drop_table("otherdb.people")
end
def test_add_timestamps
@@ -130,8 +133,8 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
begin
ActiveRecord::Base.connection.create_table :delete_me
ActiveRecord::Base.connection.add_timestamps :delete_me, null: true
- assert column_present?('delete_me', 'updated_at', 'datetime')
- assert column_present?('delete_me', 'created_at', 'datetime')
+ assert column_present?("delete_me", "updated_at", "datetime")
+ assert column_present?("delete_me", "created_at", "datetime")
ensure
ActiveRecord::Base.connection.drop_table :delete_me rescue nil
end
@@ -144,9 +147,9 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
ActiveRecord::Base.connection.create_table :delete_me do |t|
t.timestamps null: true
end
- ActiveRecord::Base.connection.remove_timestamps :delete_me, { null: true }
- assert !column_present?('delete_me', 'updated_at', 'datetime')
- assert !column_present?('delete_me', 'created_at', 'datetime')
+ ActiveRecord::Base.connection.remove_timestamps :delete_me, null: true
+ assert_not column_present?("delete_me", "updated_at", "datetime")
+ assert_not column_present?("delete_me", "created_at", "datetime")
ensure
ActiveRecord::Base.connection.drop_table :delete_me rescue nil
end
@@ -154,15 +157,19 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
end
def test_indexes_in_create
- ActiveRecord::Base.connection.stubs(:table_exists?).with(:temp).returns(false)
- ActiveRecord::Base.connection.stubs(:index_name_exists?).with(:index_temp_on_zip).returns(false)
+ assert_called_with(
+ ActiveRecord::Base.connection,
+ :data_source_exists?,
+ [:temp],
+ returns: false
+ ) do
+ expected = "CREATE TEMPORARY TABLE `temp` ( INDEX `index_temp_on_zip` (`zip`)) AS SELECT id, name, zip FROM a_really_complicated_query"
+ actual = ActiveRecord::Base.connection.create_table(:temp, temporary: true, as: "SELECT id, name, zip FROM a_really_complicated_query") do |t|
+ t.index :zip
+ end
- expected = "CREATE TEMPORARY TABLE `temp` ( INDEX `index_temp_on_zip` (`zip`) ) ENGINE=InnoDB AS SELECT id, name, zip FROM a_really_complicated_query"
- actual = ActiveRecord::Base.connection.create_table(:temp, temporary: true, as: "SELECT id, name, zip FROM a_really_complicated_query") do |t|
- t.index :zip
+ assert_equal expected, actual
end
-
- assert_equal expected, actual
end
private
@@ -187,6 +194,6 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
def column_present?(table_name, column_name, type)
results = ActiveRecord::Base.connection.select_all("SHOW FIELDS FROM #{table_name} LIKE '#{column_name}'")
- results.first && results.first['Type'] == type
+ results.first && results.first["Type"] == type
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/auto_increment_test.rb b/activerecord/test/cases/adapters/mysql2/auto_increment_test.rb
new file mode 100644
index 0000000000..4c67633946
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql2/auto_increment_test.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "support/schema_dumping_helper"
+
+class Mysql2AutoIncrementTest < ActiveRecord::Mysql2TestCase
+ include SchemaDumpingHelper
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def teardown
+ @connection.drop_table :auto_increments, if_exists: true
+ end
+
+ def test_auto_increment_without_primary_key
+ @connection.create_table :auto_increments, id: false, force: true do |t|
+ t.integer :id, null: false, auto_increment: true
+ t.index :id
+ end
+ output = dump_table_schema("auto_increments")
+ assert_match(/t\.integer\s+"id",\s+null: false,\s+auto_increment: true$/, output)
+ end
+
+ def test_auto_increment_with_composite_primary_key
+ @connection.create_table :auto_increments, primary_key: [:id, :created_at], force: true do |t|
+ t.integer :id, null: false, auto_increment: true
+ t.datetime :created_at, null: false
+ end
+ output = dump_table_schema("auto_increments")
+ assert_match(/t\.integer\s+"id",\s+null: false,\s+auto_increment: true$/, output)
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb b/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb
index abdf3dbf5b..825bddfb73 100644
--- a/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/topic'
+require "models/topic"
module ActiveRecord
module ConnectionAdapters
@@ -20,7 +22,7 @@ module ActiveRecord
def test_create_question_marks
str = "foo?bar"
- x = Topic.create!(:title => str, :content => str)
+ x = Topic.create!(title: str, content: str)
x.reload
assert_equal str, x.title
assert_equal str, x.content
@@ -39,7 +41,7 @@ module ActiveRecord
def test_create_null_bytes
str = "foo\0bar"
- x = Topic.create!(:title => str, :content => str)
+ x = Topic.create!(title: str, content: str)
x.reload
assert_equal str, x.title
assert_equal str, x.content
diff --git a/activerecord/test/cases/adapters/mysql2/boolean_test.rb b/activerecord/test/cases/adapters/mysql2/boolean_test.rb
index 8575df9e43..db09b30361 100644
--- a/activerecord/test/cases/adapters/mysql2/boolean_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/boolean_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
class Mysql2BooleanTest < ActiveRecord::Mysql2TestCase
@@ -38,28 +40,38 @@ class Mysql2BooleanTest < ActiveRecord::Mysql2TestCase
assert_equal :string, string_column.type
end
- test "test type casting with emulated booleans" do
+ test "type casting with emulated booleans" do
emulate_booleans true
boolean = BooleanType.create!(archived: true, published: true)
attributes = boolean.reload.attributes_before_type_cast
-
assert_equal 1, attributes["archived"]
assert_equal "1", attributes["published"]
+ boolean = BooleanType.create!(archived: false, published: false)
+ attributes = boolean.reload.attributes_before_type_cast
+ assert_equal 0, attributes["archived"]
+ assert_equal "0", attributes["published"]
+
assert_equal 1, @connection.type_cast(true)
+ assert_equal 0, @connection.type_cast(false)
end
- test "test type casting without emulated booleans" do
+ test "type casting without emulated booleans" do
emulate_booleans false
boolean = BooleanType.create!(archived: true, published: true)
attributes = boolean.reload.attributes_before_type_cast
-
assert_equal 1, attributes["archived"]
assert_equal "1", attributes["published"]
+ boolean = BooleanType.create!(archived: false, published: false)
+ attributes = boolean.reload.attributes_before_type_cast
+ assert_equal 0, attributes["archived"]
+ assert_equal "0", attributes["published"]
+
assert_equal 1, @connection.type_cast(true)
+ assert_equal 0, @connection.type_cast(false)
end
test "with booleans stored as 1 and 0" do
@@ -76,11 +88,11 @@ class Mysql2BooleanTest < ActiveRecord::Mysql2TestCase
end
def boolean_column
- BooleanType.columns.find { |c| c.name == 'archived' }
+ BooleanType.columns.find { |c| c.name == "archived" }
end
def string_column
- BooleanType.columns.find { |c| c.name == 'published' }
+ BooleanType.columns.find { |c| c.name == "published" }
end
def emulate_booleans(value)
diff --git a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb
index 963116f08a..c32475c683 100644
--- a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
class Mysql2CaseSensitivityTest < ActiveRecord::Mysql2TestCase
@@ -7,48 +9,57 @@ class Mysql2CaseSensitivityTest < ActiveRecord::Mysql2TestCase
repair_validations(CollationTest)
def test_columns_include_collation_different_from_table
- assert_equal 'utf8_bin', CollationTest.columns_hash['string_cs_column'].collation
- assert_equal 'utf8_general_ci', CollationTest.columns_hash['string_ci_column'].collation
+ assert_equal "utf8mb4_bin", CollationTest.columns_hash["string_cs_column"].collation
+ assert_equal "utf8mb4_general_ci", CollationTest.columns_hash["string_ci_column"].collation
end
def test_case_sensitive
- assert !CollationTest.columns_hash['string_ci_column'].case_sensitive?
- assert CollationTest.columns_hash['string_cs_column'].case_sensitive?
+ assert_not_predicate CollationTest.columns_hash["string_ci_column"], :case_sensitive?
+ assert_predicate CollationTest.columns_hash["string_cs_column"], :case_sensitive?
end
def test_case_insensitive_comparison_for_ci_column
- CollationTest.validates_uniqueness_of(:string_ci_column, :case_sensitive => false)
- CollationTest.create!(:string_ci_column => 'A')
- invalid = CollationTest.new(:string_ci_column => 'a')
+ CollationTest.validates_uniqueness_of(:string_ci_column, case_sensitive: false)
+ CollationTest.create!(string_ci_column: "A")
+ invalid = CollationTest.new(string_ci_column: "a")
queries = assert_sql { invalid.save }
ci_uniqueness_query = queries.detect { |q| q.match(/string_ci_column/) }
assert_no_match(/lower/i, ci_uniqueness_query)
end
def test_case_insensitive_comparison_for_cs_column
- CollationTest.validates_uniqueness_of(:string_cs_column, :case_sensitive => false)
- CollationTest.create!(:string_cs_column => 'A')
- invalid = CollationTest.new(:string_cs_column => 'a')
+ CollationTest.validates_uniqueness_of(:string_cs_column, case_sensitive: false)
+ CollationTest.create!(string_cs_column: "A")
+ invalid = CollationTest.new(string_cs_column: "a")
queries = assert_sql { invalid.save }
- cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/)}
+ cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/) }
assert_match(/lower/i, cs_uniqueness_query)
end
def test_case_sensitive_comparison_for_ci_column
- CollationTest.validates_uniqueness_of(:string_ci_column, :case_sensitive => true)
- CollationTest.create!(:string_ci_column => 'A')
- invalid = CollationTest.new(:string_ci_column => 'A')
+ CollationTest.validates_uniqueness_of(:string_ci_column, case_sensitive: true)
+ CollationTest.create!(string_ci_column: "A")
+ invalid = CollationTest.new(string_ci_column: "A")
queries = assert_sql { invalid.save }
ci_uniqueness_query = queries.detect { |q| q.match(/string_ci_column/) }
assert_match(/binary/i, ci_uniqueness_query)
end
def test_case_sensitive_comparison_for_cs_column
- CollationTest.validates_uniqueness_of(:string_cs_column, :case_sensitive => true)
- CollationTest.create!(:string_cs_column => 'A')
- invalid = CollationTest.new(:string_cs_column => 'A')
+ CollationTest.validates_uniqueness_of(:string_cs_column, case_sensitive: true)
+ CollationTest.create!(string_cs_column: "A")
+ invalid = CollationTest.new(string_cs_column: "A")
queries = assert_sql { invalid.save }
cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/) }
assert_no_match(/binary/i, cs_uniqueness_query)
end
+
+ def test_case_sensitive_comparison_for_binary_column
+ CollationTest.validates_uniqueness_of(:binary_column, case_sensitive: true)
+ CollationTest.create!(binary_column: "A")
+ invalid = CollationTest.new(binary_column: "A")
+ queries = assert_sql { invalid.save }
+ bin_uniqueness_query = queries.detect { |q| q.match(/binary_column/) }
+ assert_no_match(/\bBINARY\b/, bin_uniqueness_query)
+ end
end
diff --git a/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb b/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb
index 4fd34def15..0bdbefdfb9 100644
--- a/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'support/schema_dumping_helper'
+require "support/schema_dumping_helper"
class Mysql2CharsetCollationTest < ActiveRecord::Mysql2TestCase
include SchemaDumpingHelper
@@ -8,8 +10,8 @@ class Mysql2CharsetCollationTest < ActiveRecord::Mysql2TestCase
setup do
@connection = ActiveRecord::Base.connection
@connection.create_table :charset_collations, force: true do |t|
- t.string :string_ascii_bin, charset: 'ascii', collation: 'ascii_bin'
- t.text :text_ucs2_unicode_ci, charset: 'ucs2', collation: 'ucs2_unicode_ci'
+ t.string :string_ascii_bin, charset: "ascii", collation: "ascii_bin"
+ t.text :text_ucs2_unicode_ci, charset: "ucs2", collation: "ucs2_unicode_ci"
end
end
@@ -18,37 +20,37 @@ class Mysql2CharsetCollationTest < ActiveRecord::Mysql2TestCase
end
test "string column with charset and collation" do
- column = @connection.columns(:charset_collations).find { |c| c.name == 'string_ascii_bin' }
+ column = @connection.columns(:charset_collations).find { |c| c.name == "string_ascii_bin" }
assert_equal :string, column.type
- assert_equal 'ascii_bin', column.collation
+ assert_equal "ascii_bin", column.collation
end
test "text column with charset and collation" do
- column = @connection.columns(:charset_collations).find { |c| c.name == 'text_ucs2_unicode_ci' }
+ column = @connection.columns(:charset_collations).find { |c| c.name == "text_ucs2_unicode_ci" }
assert_equal :text, column.type
- assert_equal 'ucs2_unicode_ci', column.collation
+ assert_equal "ucs2_unicode_ci", column.collation
end
test "add column with charset and collation" do
- @connection.add_column :charset_collations, :title, :string, charset: 'utf8', collation: 'utf8_bin'
+ @connection.add_column :charset_collations, :title, :string, charset: "utf8mb4", collation: "utf8mb4_bin"
- column = @connection.columns(:charset_collations).find { |c| c.name == 'title' }
+ column = @connection.columns(:charset_collations).find { |c| c.name == "title" }
assert_equal :string, column.type
- assert_equal 'utf8_bin', column.collation
+ assert_equal "utf8mb4_bin", column.collation
end
test "change column with charset and collation" do
- @connection.add_column :charset_collations, :description, :string, charset: 'utf8', collation: 'utf8_unicode_ci'
- @connection.change_column :charset_collations, :description, :text, charset: 'utf8', collation: 'utf8_general_ci'
+ @connection.add_column :charset_collations, :description, :string, charset: "utf8mb4", collation: "utf8mb4_unicode_ci"
+ @connection.change_column :charset_collations, :description, :text, charset: "utf8mb4", collation: "utf8mb4_general_ci"
- column = @connection.columns(:charset_collations).find { |c| c.name == 'description' }
+ column = @connection.columns(:charset_collations).find { |c| c.name == "description" }
assert_equal :text, column.type
- assert_equal 'utf8_general_ci', column.collation
+ assert_equal "utf8mb4_general_ci", column.collation
end
test "schema dump includes collation" do
output = dump_table_schema("charset_collations")
- assert_match %r{t.string\s+"string_ascii_bin",\s+limit: 255,\s+collation: "ascii_bin"$}, output
- assert_match %r{t.text\s+"text_ucs2_unicode_ci",\s+limit: 65535,\s+collation: "ucs2_unicode_ci"$}, output
+ assert_match %r{t\.string\s+"string_ascii_bin",\s+collation: "ascii_bin"$}, output
+ assert_match %r{t\.text\s+"text_ucs2_unicode_ci",\s+collation: "ucs2_unicode_ci"$}, output
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb
index 000bcadebe..3103589186 100644
--- a/activerecord/test/cases/adapters/mysql2/connection_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'support/connection_helper'
+require "support/connection_helper"
class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase
include ConnectionHelper
@@ -9,7 +11,7 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase
def setup
super
@subscriber = SQLSubscriber.new
- @subscription = ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber)
+ @subscription = ActiveSupport::Notifications.subscribe("sql.active_record", @subscriber)
@connection = ActiveRecord::Base.connection
end
@@ -20,9 +22,9 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase
def test_bad_connection
assert_raise ActiveRecord::NoDatabaseError do
- configuration = ActiveRecord::Base.configurations['arunit'].merge(database: 'inexistent_activerecord_unittest')
+ configuration = ActiveRecord::Base.configurations["arunit"].merge(database: "inexistent_activerecord_unittest")
connection = ActiveRecord::Base.mysql2_connection(configuration)
- connection.drop_table 'ex', if_exists: true
+ connection.drop_table "ex", if_exists: true
end
end
@@ -38,80 +40,129 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase
end
def test_no_automatic_reconnection_after_timeout
- assert @connection.active?
- @connection.update('set @@wait_timeout=1')
+ assert_predicate @connection, :active?
+ @connection.update("set @@wait_timeout=1")
sleep 2
- assert !@connection.active?
-
+ assert_not_predicate @connection, :active?
+ ensure
# Repair all fixture connections so other tests won't break.
@fixture_connections.each(&:verify!)
end
def test_successful_reconnection_after_timeout_with_manual_reconnect
- assert @connection.active?
- @connection.update('set @@wait_timeout=1')
+ assert_predicate @connection, :active?
+ @connection.update("set @@wait_timeout=1")
sleep 2
@connection.reconnect!
- assert @connection.active?
+ assert_predicate @connection, :active?
end
def test_successful_reconnection_after_timeout_with_verify
- assert @connection.active?
- @connection.update('set @@wait_timeout=1')
+ assert_predicate @connection, :active?
+ @connection.update("set @@wait_timeout=1")
sleep 2
@connection.verify!
- assert @connection.active?
+ assert_predicate @connection, :active?
+ end
+
+ def test_execute_after_disconnect
+ @connection.disconnect!
+
+ error = assert_raise(ActiveRecord::StatementInvalid) do
+ @connection.execute("SELECT 1")
+ end
+ assert_kind_of Mysql2::Error, error.cause
+ end
+
+ def test_quote_after_disconnect
+ @connection.disconnect!
+
+ assert_raise(Mysql2::Error) do
+ @connection.quote("string")
+ end
+ end
+
+ def test_active_after_disconnect
+ @connection.disconnect!
+ 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')
+ assert_equal "utf8mb4_unicode_ci", @connection.show_variable("collation_connection")
+ assert_equal "utf8mb4_general_ci", ARUnit2Model.connection.show_variable("collation_connection")
end
- # TODO: Below is a straight up copy/paste from mysql/connection_test.rb
- # I'm not sure what the correct way is to share these tests between
- # adapters in minitest.
def test_mysql_default_in_strict_mode
- result = @connection.exec_query "SELECT @@SESSION.sql_mode"
- assert_equal [["STRICT_ALL_TABLES"]], result.rows
+ result = @connection.select_value("SELECT @@SESSION.sql_mode")
+ assert_match %r(STRICT_ALL_TABLES), result
end
def test_mysql_strict_mode_disabled
run_without_connection do |orig_connection|
- ActiveRecord::Base.establish_connection(orig_connection.merge({:strict => false}))
- result = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode"
- assert_equal [['']], result.rows
+ ActiveRecord::Base.establish_connection(orig_connection.merge(strict: false))
+ result = ActiveRecord::Base.connection.select_value("SELECT @@SESSION.sql_mode")
+ assert_no_match %r(STRICT_ALL_TABLES), result
end
end
def test_mysql_strict_mode_specified_default
run_without_connection do |orig_connection|
- ActiveRecord::Base.establish_connection(orig_connection.merge({strict: :default}))
- global_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@GLOBAL.sql_mode"
- session_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode"
- assert_equal global_sql_mode.rows, session_sql_mode.rows
+ ActiveRecord::Base.establish_connection(orig_connection.merge(strict: :default))
+ global_sql_mode = ActiveRecord::Base.connection.select_value("SELECT @@GLOBAL.sql_mode")
+ session_sql_mode = ActiveRecord::Base.connection.select_value("SELECT @@SESSION.sql_mode")
+ assert_equal global_sql_mode, session_sql_mode
end
end
- def test_mysql_set_session_variable
+ def test_mysql_sql_mode_variable_overrides_strict_mode
run_without_connection do |orig_connection|
- ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:default_week_format => 3}}))
- session_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.DEFAULT_WEEK_FORMAT"
- assert_equal 3, session_mode.rows.first.first.to_i
+ ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { "sql_mode" => "ansi" }))
+ result = ActiveRecord::Base.connection.select_value("SELECT @@SESSION.sql_mode")
+ assert_no_match %r(STRICT_ALL_TABLES), result
end
end
- def test_mysql_sql_mode_variable_overrides_strict_mode
+ 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]
+ end
+ end
+
+ def test_passing_flags_by_array_to_adapter
+ run_without_connection do |orig_connection|
+ ActiveRecord::Base.establish_connection(orig_connection.merge(flags: ["COMPRESS"]))
+ assert_equal ["COMPRESS", "FOUND_ROWS"], ActiveRecord::Base.connection.raw_connection.query_options[:flags]
+ end
+ end
+
+ def test_mysql_set_session_variable
run_without_connection do |orig_connection|
- ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { 'sql_mode' => 'ansi' }))
- result = ActiveRecord::Base.connection.exec_query 'SELECT @@SESSION.sql_mode'
- assert_not_equal [['STRICT_ALL_TABLES']], result.rows
+ ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { default_week_format: 3 }))
+ session_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.DEFAULT_WEEK_FORMAT"
+ assert_equal 3, session_mode.rows.first.first.to_i
end
end
def test_mysql_set_session_variable_to_default
run_without_connection do |orig_connection|
- ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:default_week_format => :default}}))
+ ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { default_week_format: :default }))
global_mode = ActiveRecord::Base.connection.exec_query "SELECT @@GLOBAL.DEFAULT_WEEK_FORMAT"
session_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.DEFAULT_WEEK_FORMAT"
assert_equal global_mode.rows, session_mode.rows
@@ -119,16 +170,46 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase
end
def test_logs_name_show_variable
- @connection.show_variable 'foo'
+ ActiveRecord::Base.connection.materialize_transactions
+ @subscriber.logged.clear
+ @connection.show_variable "foo"
assert_equal "SCHEMA", @subscriber.logged[0][1]
end
- def test_logs_name_rename_column_sql
+ def test_logs_name_rename_column_for_alter
@connection.execute "CREATE TABLE `bar_baz` (`foo` varchar(255))"
@subscriber.logged.clear
- @connection.send(:rename_column_sql, 'bar_baz', 'foo', 'foo2')
+ @connection.send(:rename_column_for_alter, "bar_baz", "foo", "foo2")
assert_equal "SCHEMA", @subscriber.logged[0][1]
ensure
@connection.execute "DROP TABLE `bar_baz`"
end
+
+ def test_get_and_release_advisory_lock
+ lock_name = "test lock'n'name"
+
+ got_lock = @connection.get_advisory_lock(lock_name)
+ assert got_lock, "get_advisory_lock should have returned true but it didn't"
+
+ assert_equal test_lock_free(lock_name), false,
+ "expected the test advisory lock to be held but it wasn't"
+
+ released_lock = @connection.release_advisory_lock(lock_name)
+ assert released_lock, "expected release_advisory_lock to return true but it didn't"
+
+ assert test_lock_free(lock_name), "expected the test lock to be available after releasing"
+ end
+
+ def test_release_non_existent_advisory_lock
+ lock_name = "fake lock'n'name"
+ released_non_existent_lock = @connection.release_advisory_lock(lock_name)
+ assert_equal released_non_existent_lock, false,
+ "expected release_advisory_lock to return false when there was no lock to release"
+ end
+
+ private
+
+ def test_lock_free(lock_name)
+ @connection.select_value("SELECT IS_FREE_LOCK(#{@connection.quote(lock_name)})") == 1
+ end
end
diff --git a/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb b/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb
new file mode 100644
index 0000000000..00a075e063
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+
+class Mysql2DatetimePrecisionQuotingTest < ActiveRecord::Mysql2TestCase
+ setup do
+ @connection = ActiveRecord::Base.connection
+ end
+
+ test "microsecond precision for MySQL gte 5.6.4" do
+ stub_version "5.6.4" do
+ assert_microsecond_precision
+ end
+ end
+
+ test "no microsecond precision for MySQL lt 5.6.4" do
+ 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" do
+ assert_microsecond_precision
+ end
+ end
+
+ test "no microsecond precision for MariaDB lt 5.3.0" do
+ stub_version "5.2.9-MariaDB" do
+ assert_no_microsecond_precision
+ end
+ end
+
+ private
+ def assert_microsecond_precision
+ assert_match_quoted_microsecond_datetime(/\.000001\z/)
+ end
+
+ def assert_no_microsecond_precision
+ assert_match_quoted_microsecond_datetime(/\d\z/)
+ end
+
+ def assert_match_quoted_microsecond_datetime(match)
+ assert_match match, @connection.quoted_date(Time.now.change(usec: 1))
+ end
+
+ def stub_version(full_version_string)
+ @connection.stub(:full_version, full_version_string) do
+ @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version)
+ yield
+ end
+ ensure
+ @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version)
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql2/enum_test.rb b/activerecord/test/cases/adapters/mysql2/enum_test.rb
index bd732b5eca..832f5d61d1 100644
--- a/activerecord/test/cases/adapters/mysql2/enum_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/enum_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
class Mysql2EnumTest < ActiveRecord::Mysql2TestCase
@@ -5,6 +7,17 @@ class Mysql2EnumTest < ActiveRecord::Mysql2TestCase
end
def test_enum_limit
- assert_equal 6, EnumTest.columns.first.limit
+ column = EnumTest.columns_hash["enum_column"]
+ assert_equal 8, column.limit
+ end
+
+ def test_should_not_be_unsigned
+ column = EnumTest.columns_hash["enum_column"]
+ assert_not_predicate column, :unsigned?
+ end
+
+ def test_should_not_be_bigint
+ column = EnumTest.columns_hash["enum_column"]
+ assert_not_predicate column, :bigint?
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/explain_test.rb b/activerecord/test/cases/adapters/mysql2/explain_test.rb
index 4fc7414b18..b8e778f0b0 100644
--- a/activerecord/test/cases/adapters/mysql2/explain_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/explain_test.rb
@@ -1,27 +1,23 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/developer'
-require 'models/computer'
+require "models/author"
+require "models/post"
-module ActiveRecord
- module ConnectionAdapters
- class Mysql2Adapter
- class ExplainTest < ActiveRecord::Mysql2TestCase
- fixtures :developers
+class Mysql2ExplainTest < ActiveRecord::Mysql2TestCase
+ fixtures :authors
- def test_explain_for_one_query
- explain = Developer.where(:id => 1).explain
- assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain
- assert_match %r(developers |.* const), explain
- end
+ def test_explain_for_one_query
+ explain = Author.where(id: 1).explain
+ assert_match %(EXPLAIN for: SELECT `authors`.* FROM `authors` WHERE `authors`.`id` = 1), explain
+ assert_match %r(authors |.* const), explain
+ end
- def test_explain_with_eager_loading
- explain = Developer.where(:id => 1).includes(:audit_logs).explain
- assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain
- assert_match %r(developers |.* const), explain
- assert_match %(EXPLAIN for: SELECT `audit_logs`.* FROM `audit_logs` WHERE `audit_logs`.`developer_id` = 1), explain
- assert_match %r(audit_logs |.* ALL), explain
- end
- end
- end
+ def test_explain_with_eager_loading
+ explain = Author.where(id: 1).includes(:posts).explain
+ assert_match %(EXPLAIN for: SELECT `authors`.* FROM `authors` WHERE `authors`.`id` = 1), explain
+ assert_match %r(authors |.* const), explain
+ assert_match %(EXPLAIN for: SELECT `posts`.* FROM `posts` WHERE `posts`.`author_id` = 1), explain
+ assert_match %r(posts |.* ALL), explain
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/json_test.rb b/activerecord/test/cases/adapters/mysql2/json_test.rb
new file mode 100644
index 0000000000..de78ba91f5
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql2/json_test.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "cases/json_shared_test_cases"
+
+if ActiveRecord::Base.connection.supports_json?
+ class Mysql2JSONTest < ActiveRecord::Mysql2TestCase
+ include JSONSharedTestCases
+ self.use_transactional_tests = false
+
+ def setup
+ super
+ @connection.create_table("json_data_type") do |t|
+ t.json "payload"
+ t.json "settings"
+ end
+ end
+
+ private
+ def column_type
+ :json
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb
new file mode 100644
index 0000000000..0719baaa23
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "support/ddl_helper"
+
+class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase
+ include DdlHelper
+
+ def setup
+ @conn = ActiveRecord::Base.connection
+ end
+
+ def test_exec_query_nothing_raises_with_no_result_queries
+ assert_nothing_raised do
+ with_example_table do
+ @conn.exec_query("INSERT INTO ex (number) VALUES (1)")
+ @conn.exec_query("DELETE FROM ex WHERE number = 1")
+ end
+ end
+ end
+
+ def test_columns_for_distinct_zero_orders
+ assert_equal "posts.id",
+ @conn.columns_for_distinct("posts.id", [])
+ end
+
+ def test_columns_for_distinct_one_order
+ assert_equal "posts.created_at AS alias_0, posts.id",
+ @conn.columns_for_distinct("posts.id", ["posts.created_at desc"])
+ end
+
+ def test_columns_for_distinct_few_orders
+ assert_equal "posts.created_at AS alias_0, posts.position AS alias_1, posts.id",
+ @conn.columns_for_distinct("posts.id", ["posts.created_at desc", "posts.position asc"])
+ end
+
+ def test_columns_for_distinct_with_case
+ assert_equal(
+ "CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END AS alias_0, posts.id",
+ @conn.columns_for_distinct("posts.id",
+ ["CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END"])
+ )
+ end
+
+ def test_columns_for_distinct_blank_not_nil_orders
+ assert_equal "posts.created_at AS alias_0, posts.id",
+ @conn.columns_for_distinct("posts.id", ["posts.created_at desc", "", " "])
+ end
+
+ def test_columns_for_distinct_with_arel_order
+ order = Object.new
+ def order.to_sql
+ "posts.created_at desc"
+ end
+ assert_equal "posts.created_at AS alias_0, posts.id",
+ @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)
+ super(@conn, "ex", definition, &block)
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb
deleted file mode 100644
index ffb4e2c5cf..0000000000
--- a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb
+++ /dev/null
@@ -1,152 +0,0 @@
-require "cases/helper"
-
-# a suite of tests to ensure the ConnectionAdapters#MysqlAdapter can handle tables with
-# reserved word names (ie: group, order, values, etc...)
-class Mysql2ReservedWordTest < ActiveRecord::Mysql2TestCase
- class Group < ActiveRecord::Base
- Group.table_name = 'group'
- belongs_to :select
- has_one :values
- end
-
- class Select < ActiveRecord::Base
- Select.table_name = 'select'
- has_many :groups
- end
-
- class Values < ActiveRecord::Base
- Values.table_name = 'values'
- end
-
- class Distinct < ActiveRecord::Base
- Distinct.table_name = 'distinct'
- has_and_belongs_to_many :selects
- has_many :values, :through => :groups
- end
-
- def setup
- @connection = ActiveRecord::Base.connection
-
- # we call execute directly here (and do similar below) because ActiveRecord::Base#create_table()
- # will fail with these table names if these test cases fail
-
- create_tables_directly 'group'=>'id int auto_increment primary key, `order` varchar(255), select_id int',
- 'select'=>'id int auto_increment primary key',
- 'values'=>'id int auto_increment primary key, group_id int',
- 'distinct'=>'id int auto_increment primary key',
- 'distinct_select'=>'distinct_id int, select_id int'
- end
-
- teardown do
- drop_tables_directly ['group', 'select', 'values', 'distinct', 'distinct_select', 'order']
- end
-
- # create tables with reserved-word names and columns
- def test_create_tables
- assert_nothing_raised {
- @connection.create_table :order do |t|
- t.column :group, :string
- end
- }
- end
-
- # rename tables with reserved-word names
- def test_rename_tables
- assert_nothing_raised { @connection.rename_table(:group, :order) }
- end
-
- # alter column with a reserved-word name in a table with a reserved-word name
- def test_change_columns
- assert_nothing_raised { @connection.change_column_default(:group, :order, 'whatever') }
- #the quoting here will reveal any double quoting issues in change_column's interaction with the column method in the adapter
- assert_nothing_raised { @connection.change_column('group', 'order', :Int, :default => 0) }
- assert_nothing_raised { @connection.rename_column(:group, :order, :values) }
- end
-
- # introspect table with reserved word name
- def test_introspect
- assert_nothing_raised { @connection.columns(:group) }
- assert_nothing_raised { @connection.indexes(:group) }
- end
-
- #fixtures
- self.use_instantiated_fixtures = true
- self.use_transactional_tests = false
-
- #activerecord model class with reserved-word table name
- def test_activerecord_model
- create_test_fixtures :select, :distinct, :group, :values, :distinct_select
- x = nil
- assert_nothing_raised { x = Group.new }
- x.order = 'x'
- assert_nothing_raised { x.save }
- x.order = 'y'
- assert_nothing_raised { x.save }
- assert_nothing_raised { Group.find_by_order('y') }
- assert_nothing_raised { Group.find(1) }
- end
-
- # has_one association with reserved-word table name
- def test_has_one_associations
- create_test_fixtures :select, :distinct, :group, :values, :distinct_select
- v = nil
- assert_nothing_raised { v = Group.find(1).values }
- assert_equal 2, v.id
- end
-
- # belongs_to association with reserved-word table name
- def test_belongs_to_associations
- create_test_fixtures :select, :distinct, :group, :values, :distinct_select
- gs = nil
- assert_nothing_raised { gs = Select.find(2).groups }
- assert_equal gs.length, 2
- assert(gs.collect(&:id).sort == [2, 3])
- end
-
- # has_and_belongs_to_many with reserved-word table name
- def test_has_and_belongs_to_many
- create_test_fixtures :select, :distinct, :group, :values, :distinct_select
- s = nil
- assert_nothing_raised { s = Distinct.find(1).selects }
- assert_equal s.length, 2
- assert(s.collect(&:id).sort == [1, 2])
- end
-
- # activerecord model introspection with reserved-word table and column names
- def test_activerecord_introspection
- assert_nothing_raised { Group.table_exists? }
- assert_nothing_raised { Group.columns }
- end
-
- # Calculations
- def test_calculations_work_with_reserved_words
- assert_nothing_raised { Group.count }
- end
-
- def test_associations_work_with_reserved_words
- assert_nothing_raised { Select.all.merge!(:includes => [:groups]).to_a }
- end
-
- #the following functions were added to DRY test cases
-
- private
- # custom fixture loader, uses FixtureSet#create_fixtures and appends base_path to the current file's path
- def create_test_fixtures(*fixture_names)
- ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT + "/reserved_words", fixture_names)
- end
-
- # custom drop table, uses execute on connection to drop a table if it exists. note: escapes table_name
- def drop_tables_directly(table_names, connection = @connection)
- table_names.each do |name|
- connection.drop_table name, if_exists: true
- end
- 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)
- tables.each do |table_name, column_properties|
- connection.execute("CREATE TABLE `#{table_name}` ( #{column_properties} )")
- end
- end
-
-end
diff --git a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb
index 396f235e77..d7d9a2d732 100644
--- a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb
@@ -1,6 +1,10 @@
+# frozen_string_literal: true
+
require "cases/helper"
class SchemaMigrationsTest < ActiveRecord::Mysql2TestCase
+ self.use_transactional_tests = false
+
def test_renaming_index_on_foreign_key
connection.add_index "engines", "car_id"
connection.add_foreign_key :engines, :cars, name: "fk_engines_cars"
@@ -12,31 +16,50 @@ class SchemaMigrationsTest < ActiveRecord::Mysql2TestCase
end
def test_initializes_schema_migrations_for_encoding_utf8mb4
- smtn = ActiveRecord::Migrator.schema_migrations_table_name
- connection.drop_table smtn, if_exists: true
+ with_encoding_utf8mb4 do
+ table_name = ActiveRecord::SchemaMigration.table_name
+ connection.drop_table table_name, if_exists: true
- database_name = connection.current_database
- database_info = connection.select_one("SELECT * FROM information_schema.schemata WHERE schema_name = '#{database_name}'")
+ ActiveRecord::SchemaMigration.create_table
- original_charset = database_info["DEFAULT_CHARACTER_SET_NAME"]
- original_collation = database_info["DEFAULT_COLLATION_NAME"]
+ assert connection.column_exists?(table_name, :version, :string)
+ end
+ end
- execute("ALTER DATABASE #{database_name} DEFAULT CHARACTER SET utf8mb4")
+ def test_initializes_internal_metadata_for_encoding_utf8mb4
+ with_encoding_utf8mb4 do
+ table_name = ActiveRecord::InternalMetadata.table_name
+ connection.drop_table table_name, if_exists: true
- connection.initialize_schema_migrations_table
+ ActiveRecord::InternalMetadata.create_table
- limit = ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::MAX_INDEX_LENGTH_FOR_CHARSETS_OF_4BYTES_MAXLEN
- assert connection.column_exists?(smtn, :version, :string, limit: limit)
+ assert connection.column_exists?(table_name, :key, :string)
+ end
ensure
- execute("ALTER DATABASE #{database_name} DEFAULT CHARACTER SET #{original_charset} COLLATE #{original_collation}")
+ ActiveRecord::InternalMetadata[:environment] = connection.migration_context.current_environment
end
private
- def connection
- @connection ||= ActiveRecord::Base.connection
- end
- def execute(sql)
- connection.execute(sql)
- end
+ def with_encoding_utf8mb4
+ database_name = connection.current_database
+ database_info = connection.select_one("SELECT * FROM information_schema.schemata WHERE schema_name = '#{database_name}'")
+
+ original_charset = database_info["DEFAULT_CHARACTER_SET_NAME"]
+ original_collation = database_info["DEFAULT_COLLATION_NAME"]
+
+ execute("ALTER DATABASE #{database_name} DEFAULT CHARACTER SET utf8mb4")
+
+ yield
+ ensure
+ execute("ALTER DATABASE #{database_name} DEFAULT CHARACTER SET #{original_charset} COLLATE #{original_collation}")
+ end
+
+ def connection
+ @connection ||= ActiveRecord::Base.connection
+ end
+
+ def execute(sql)
+ connection.execute(sql)
+ end
end
diff --git a/activerecord/test/cases/adapters/mysql2/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb
index 880a2123d2..1283b0642c 100644
--- a/activerecord/test/cases/adapters/mysql2/schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/post'
-require 'models/comment'
+require "models/post"
+require "models/comment"
module ActiveRecord
module ConnectionAdapters
@@ -14,9 +16,41 @@ module ActiveRecord
@db_name = db
@omgpost = Class.new(ActiveRecord::Base) do
+ self.inheritance_column = :disabled
self.table_name = "#{db}.#{table}"
- def self.name; 'Post'; end
+ def self.name; "Post"; end
+ end
+ end
+
+ def test_float_limits
+ @connection.create_table :mysql_doubles do |t|
+ t.float :float_no_limit
+ t.float :float_short, limit: 5
+ t.float :float_long, limit: 53
+
+ t.float :float_23, limit: 23
+ t.float :float_24, limit: 24
+ t.float :float_25, limit: 25
end
+
+ column_no_limit = @connection.columns(:mysql_doubles).find { |c| c.name == "float_no_limit" }
+ column_short = @connection.columns(:mysql_doubles).find { |c| c.name == "float_short" }
+ column_long = @connection.columns(:mysql_doubles).find { |c| c.name == "float_long" }
+
+ column_23 = @connection.columns(:mysql_doubles).find { |c| c.name == "float_23" }
+ column_24 = @connection.columns(:mysql_doubles).find { |c| c.name == "float_24" }
+ column_25 = @connection.columns(:mysql_doubles).find { |c| c.name == "float_25" }
+
+ # Mysql floats are precision 0..24, Mysql doubles are precision 25..53
+ assert_equal 24, column_no_limit.limit
+ assert_equal 24, column_short.limit
+ assert_equal 53, column_long.limit
+
+ assert_equal 24, column_23.limit
+ assert_equal 24, column_24.limit
+ assert_equal 53, column_25.limit
+ ensure
+ @connection.drop_table "mysql_doubles", if_exists: true
end
def test_schema
@@ -24,39 +58,31 @@ module ActiveRecord
end
def test_primary_key
- assert_equal 'id', @omgpost.primary_key
+ assert_equal "id", @omgpost.primary_key
end
- def test_table_exists?
+ def test_data_source_exists?
name = @omgpost.table_name
- assert @connection.table_exists?(name), "#{name} table should exist"
- end
-
- def test_table_exists_wrong_schema
- assert(!@connection.table_exists?("#{@db_name}.zomg"), "table should not exist")
+ assert @connection.data_source_exists?(name), "#{name} data_source should exist"
end
- def test_tables_quoting
- @connection.tables(nil, "foo-bar", nil)
- flunk
- rescue => e
- # assertion for *quoted* database properly
- assert_match(/database 'foo-bar'/, e.inspect)
+ def test_data_source_exists_wrong_schema
+ assert_not(@connection.data_source_exists?("#{@db_name}.zomg"), "data_source should not exist")
end
def test_dump_indexes
- index_a_name = 'index_key_tests_on_snack'
- index_b_name = 'index_key_tests_on_pizza'
- index_c_name = 'index_key_tests_on_awesome'
+ index_a_name = "index_key_tests_on_snack"
+ index_b_name = "index_key_tests_on_pizza"
+ index_c_name = "index_key_tests_on_awesome"
- table = 'key_tests'
+ table = "key_tests"
indexes = @connection.indexes(table).sort_by(&:name)
- assert_equal 3,indexes.size
+ assert_equal 3, indexes.size
- index_a = indexes.select{|i| i.name == index_a_name}[0]
- index_b = indexes.select{|i| i.name == index_b_name}[0]
- index_c = indexes.select{|i| i.name == index_c_name}[0]
+ index_a = indexes.select { |i| i.name == index_a_name }[0]
+ index_b = indexes.select { |i| i.name == index_b_name }[0]
+ index_c = indexes.select { |i| i.name == index_c_name }[0]
assert_equal :btree, index_a.using
assert_nil index_a.type
assert_equal :btree, index_b.using
@@ -79,3 +105,24 @@ module ActiveRecord
end
end
end
+
+class Mysql2AnsiQuotesTest < ActiveRecord::Mysql2TestCase
+ def setup
+ @connection = ActiveRecord::Base.connection
+ @connection.execute("SET SESSION sql_mode='ANSI_QUOTES'")
+ end
+
+ def teardown
+ @connection.reconnect!
+ end
+
+ def test_primary_key_method_with_ansi_quotes
+ assert_equal "id", @connection.primary_key("topics")
+ end
+
+ def test_foreign_keys_method_with_ansi_quotes
+ fks = @connection.foreign_keys("lessons_students")
+ assert_equal([["lessons_students", "students", :cascade]],
+ fks.map { |fk| [fk.from_table, fk.to_table, fk.on_delete] })
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql2/sp_test.rb b/activerecord/test/cases/adapters/mysql2/sp_test.rb
new file mode 100644
index 0000000000..7b6dce71e9
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql2/sp_test.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/topic"
+require "models/reply"
+
+class Mysql2StoredProcedureTest < ActiveRecord::Mysql2TestCase
+ fixtures :topics
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ unless ActiveRecord::Base.connection.version >= "5.6.0"
+ skip("no stored procedure support")
+ end
+ end
+
+ # Test that MySQL allows multiple results for stored procedures
+ #
+ # In MySQL 5.6, CLIENT_MULTI_RESULTS is enabled by default.
+ # https://dev.mysql.com/doc/refman/5.6/en/call.html
+ def test_multi_results
+ rows = @connection.select_rows("CALL ten();")
+ assert_equal 10, rows[0][0].to_i, "ten() did not return 10 as expected: #{rows.inspect}"
+ assert @connection.active?, "Bad connection use by 'Mysql2Adapter.select_rows'"
+ end
+
+ def test_multi_results_from_select_one
+ row = @connection.select_one("CALL topics(1);")
+ assert_equal "David", row["author_name"]
+ assert @connection.active?, "Bad connection use by 'Mysql2Adapter.select_one'"
+ end
+
+ def test_multi_results_from_find_by_sql
+ topics = Topic.find_by_sql "CALL topics(3);"
+ assert_equal 3, topics.size
+ assert @connection.active?, "Bad connection use by 'Mysql2Adapter.select'"
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql2/sql_types_test.rb b/activerecord/test/cases/adapters/mysql2/sql_types_test.rb
index ae505d29c9..e10642cbb4 100644
--- a/activerecord/test/cases/adapters/mysql2/sql_types_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/sql_types_test.rb
@@ -1,14 +1,16 @@
+# frozen_string_literal: true
+
require "cases/helper"
class Mysql2SqlTypesTest < ActiveRecord::Mysql2TestCase
def test_binary_types
- assert_equal 'varbinary(64)', type_to_sql(:binary, 64)
- assert_equal 'varbinary(4095)', type_to_sql(:binary, 4095)
- assert_equal 'blob(4096)', type_to_sql(:binary, 4096)
- assert_equal 'blob', type_to_sql(:binary)
+ assert_equal "varbinary(64)", type_to_sql(:binary, 64)
+ assert_equal "varbinary(4095)", type_to_sql(:binary, 4095)
+ assert_equal "blob", type_to_sql(:binary, 4096)
+ 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/table_options_test.rb b/activerecord/test/cases/adapters/mysql2/table_options_test.rb
index af121ee7d9..1c92df940f 100644
--- a/activerecord/test/cases/adapters/mysql2/table_options_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/table_options_test.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'support/schema_dumping_helper'
+require "support/schema_dumping_helper"
class Mysql2TableOptionsTest < ActiveRecord::Mysql2TestCase
include SchemaDumpingHelper
@@ -15,28 +17,103 @@ class Mysql2TableOptionsTest < ActiveRecord::Mysql2TestCase
test "table options with ENGINE" do
@connection.create_table "mysql_table_options", force: true, options: "ENGINE=MyISAM"
output = dump_table_schema("mysql_table_options")
- options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options]
+ options = %r{create_table "mysql_table_options", options: "(?<options>.*)"}.match(output)[:options]
assert_match %r{ENGINE=MyISAM}, options
end
test "table options with ROW_FORMAT" do
@connection.create_table "mysql_table_options", force: true, options: "ROW_FORMAT=REDUNDANT"
output = dump_table_schema("mysql_table_options")
- options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options]
+ options = %r{create_table "mysql_table_options", options: "(?<options>.*)"}.match(output)[:options]
assert_match %r{ROW_FORMAT=REDUNDANT}, options
end
test "table options with CHARSET" do
@connection.create_table "mysql_table_options", force: true, options: "CHARSET=utf8mb4"
output = dump_table_schema("mysql_table_options")
- options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options]
+ options = %r{create_table "mysql_table_options", options: "(?<options>.*)"}.match(output)[:options]
assert_match %r{CHARSET=utf8mb4}, options
end
test "table options with COLLATE" do
@connection.create_table "mysql_table_options", force: true, options: "COLLATE=utf8mb4_bin"
output = dump_table_schema("mysql_table_options")
- options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options]
+ options = %r{create_table "mysql_table_options", options: "(?<options>.*)"}.match(output)[:options]
assert_match %r{COLLATE=utf8mb4_bin}, options
end
end
+
+class Mysql2DefaultEngineOptionSchemaDumpTest < ActiveRecord::Mysql2TestCase
+ include SchemaDumpingHelper
+ self.use_transactional_tests = false
+
+ def setup
+ @verbose_was = ActiveRecord::Migration.verbose
+ ActiveRecord::Migration.verbose = false
+ end
+
+ def teardown
+ ActiveRecord::Base.connection.drop_table "mysql_table_options", if_exists: true
+ ActiveRecord::Migration.verbose = @verbose_was
+ ActiveRecord::SchemaMigration.delete_all rescue nil
+ end
+
+ test "schema dump includes ENGINE=InnoDB if not provided" do
+ ActiveRecord::Base.connection.create_table "mysql_table_options", force: true
+
+ output = dump_table_schema("mysql_table_options")
+ options = %r{create_table "mysql_table_options", options: "(?<options>.*)"}.match(output)[:options]
+ assert_match %r{ENGINE=InnoDB}, options
+ end
+
+ test "schema dump includes ENGINE=InnoDB in legacy migrations" do
+ migration = Class.new(ActiveRecord::Migration[5.1]) do
+ def migrate(x)
+ create_table "mysql_table_options", force: true
+ end
+ end.new
+
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+
+ output = dump_table_schema("mysql_table_options")
+ options = %r{create_table "mysql_table_options", options: "(?<options>.*)"}.match(output)[:options]
+ assert_match %r{ENGINE=InnoDB}, options
+ end
+end
+
+class Mysql2DefaultEngineOptionSqlOutputTest < ActiveRecord::Mysql2TestCase
+ self.use_transactional_tests = false
+
+ def setup
+ @logger_was = ActiveRecord::Base.logger
+ @log = StringIO.new
+ @verbose_was = ActiveRecord::Migration.verbose
+ ActiveRecord::Base.logger = ActiveSupport::Logger.new(@log)
+ ActiveRecord::Migration.verbose = false
+ end
+
+ def teardown
+ ActiveRecord::Base.logger = @logger_was
+ ActiveRecord::Migration.verbose = @verbose_was
+ ActiveRecord::Base.connection.drop_table "mysql_table_options", if_exists: true
+ ActiveRecord::SchemaMigration.delete_all rescue nil
+ end
+
+ test "new migrations do not contain default ENGINE=InnoDB option" do
+ ActiveRecord::Base.connection.create_table "mysql_table_options", force: true
+
+ assert_no_match %r{ENGINE=InnoDB}, @log.string
+ end
+
+ test "legacy migrations contain default ENGINE=InnoDB option" do
+ migration = Class.new(ActiveRecord::Migration[5.1]) do
+ def migrate(x)
+ create_table "mysql_table_options", force: true
+ end
+ end.new
+
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+
+ assert_match %r{ENGINE=InnoDB}, @log.string
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql2/transaction_test.rb b/activerecord/test/cases/adapters/mysql2/transaction_test.rb
new file mode 100644
index 0000000000..52e283f247
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql2/transaction_test.rb
@@ -0,0 +1,149 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "support/connection_helper"
+
+module ActiveRecord
+ class Mysql2TransactionTest < ActiveRecord::Mysql2TestCase
+ self.use_transactional_tests = false
+
+ class Sample < ActiveRecord::Base
+ self.table_name = "samples"
+ end
+
+ setup do
+ @abort, Thread.abort_on_exception = Thread.abort_on_exception, false
+ Thread.report_on_exception, @original_report_on_exception = false, Thread.report_on_exception
+
+ @connection = ActiveRecord::Base.connection
+ @connection.clear_cache!
+
+ @connection.transaction do
+ @connection.drop_table "samples", if_exists: true
+ @connection.create_table("samples") do |t|
+ t.integer "value"
+ end
+ end
+
+ Sample.reset_column_information
+ end
+
+ teardown do
+ @connection.drop_table "samples", if_exists: true
+
+ Thread.abort_on_exception = @abort
+ Thread.report_on_exception = @original_report_on_exception
+ end
+
+ test "raises Deadlocked when a deadlock is encountered" do
+ 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!
+ barrier.wait
+ s2.update value: 1
+ end
+ end
+
+ begin
+ Sample.transaction do
+ s2.lock!
+ barrier.wait
+ s1.update value: 2
+ end
+ ensure
+ thread.join
+ end
+ end
+ end
+
+ test "raises LockWaitTimeout when lock wait timeout exceeded" do
+ assert_raises(ActiveRecord::LockWaitTimeout) do
+ s = Sample.create!(value: 1)
+ latch1 = Concurrent::CountDownLatch.new
+ latch2 = Concurrent::CountDownLatch.new
+
+ thread = Thread.new do
+ Sample.transaction do
+ Sample.lock.find(s.id)
+ latch1.count_down
+ latch2.wait
+ end
+ end
+
+ begin
+ Sample.transaction do
+ latch1.wait
+ Sample.connection.execute("SET innodb_lock_wait_timeout = 1")
+ Sample.lock.find(s.id)
+ end
+ ensure
+ Sample.connection.execute("SET innodb_lock_wait_timeout = DEFAULT")
+ latch2.count_down
+ thread.join
+ end
+ end
+ end
+
+ test "raises StatementTimeout when statement timeout exceeded" do
+ skip unless ActiveRecord::Base.connection.show_variable("max_execution_time")
+ assert_raises(ActiveRecord::StatementTimeout) do
+ s = Sample.create!(value: 1)
+ latch1 = Concurrent::CountDownLatch.new
+ latch2 = Concurrent::CountDownLatch.new
+
+ thread = Thread.new do
+ Sample.transaction do
+ Sample.lock.find(s.id)
+ latch1.count_down
+ latch2.wait
+ end
+ end
+
+ begin
+ Sample.transaction do
+ latch1.wait
+ Sample.connection.execute("SET max_execution_time = 1")
+ Sample.lock.find(s.id)
+ end
+ ensure
+ Sample.connection.execute("SET max_execution_time = DEFAULT")
+ latch2.count_down
+ thread.join
+ end
+ end
+ end
+
+ test "raises QueryCanceled when canceling statement due to user request" do
+ assert_raises(ActiveRecord::QueryCanceled) do
+ s = Sample.create!(value: 1)
+ latch = Concurrent::CountDownLatch.new
+
+ thread = Thread.new do
+ Sample.transaction do
+ Sample.lock.find(s.id)
+ latch.count_down
+ sleep(0.5)
+ conn = Sample.connection
+ pid = conn.query_value("SELECT id FROM information_schema.processlist WHERE info LIKE '% FOR UPDATE'")
+ conn.execute("KILL QUERY #{pid}")
+ end
+ end
+
+ begin
+ Sample.transaction do
+ latch.wait
+ Sample.lock.find(s.id)
+ end
+ ensure
+ thread.join
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb
index 9e06db2519..97da96003d 100644
--- a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb
@@ -1,6 +1,10 @@
+# frozen_string_literal: true
+
require "cases/helper"
+require "support/schema_dumping_helper"
class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase
+ include SchemaDumpingHelper
self.use_transactional_tests = false
class UnsignedType < ActiveRecord::Base
@@ -9,12 +13,16 @@ class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase
setup do
@connection = ActiveRecord::Base.connection
@connection.create_table("unsigned_types", force: true) do |t|
- t.column :unsigned_integer, "int unsigned"
+ t.integer :unsigned_integer, unsigned: true
+ 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
teardown do
- @connection.drop_table "unsigned_types"
+ @connection.drop_table "unsigned_types", if_exists: true
end
test "unsigned int max value is in range" do
@@ -23,8 +31,38 @@ class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase
end
test "minus value is out of range" do
- assert_raise(RangeError) do
+ assert_raise(ActiveModel::RangeError) do
UnsignedType.create(unsigned_integer: -10)
end
+ assert_raise(ActiveModel::RangeError) do
+ UnsignedType.create(unsigned_bigint: -10)
+ end
+ assert_raise(ActiveRecord::RangeError) do
+ UnsignedType.create(unsigned_float: -10.0)
+ end
+ assert_raise(ActiveRecord::RangeError) do
+ UnsignedType.create(unsigned_decimal: -10.0)
+ end
+ end
+
+ test "schema definition can use unsigned as the type" do
+ @connection.change_table("unsigned_types") do |t|
+ t.unsigned_integer :unsigned_integer_t
+ t.unsigned_bigint :unsigned_bigint_t
+ t.unsigned_float :unsigned_float_t
+ t.unsigned_decimal :unsigned_decimal_t, precision: 10, scale: 2
+ end
+
+ @connection.columns("unsigned_types").select { |c| /^unsigned_/.match?(c.name) }.each do |column|
+ assert_predicate column, :unsigned?
+ end
+ end
+
+ test "schema dump includes unsigned option" do
+ schema = dump_table_schema "unsigned_types"
+ assert_match %r{t\.integer\s+"unsigned_integer",\s+unsigned: true$}, schema
+ assert_match %r{t\.bigint\s+"unsigned_bigint",\s+unsigned: true$}, schema
+ assert_match %r{t\.float\s+"unsigned_float",\s+unsigned: true$}, schema
+ assert_match %r{t\.decimal\s+"unsigned_decimal",\s+precision: 10,\s+scale: 2,\s+unsigned: true$}, schema
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..8494acee3b
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql2/virtual_column_test.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+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
+ t.virtual :name_octet_length, type: :integer, as: "OCTET_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|UCASE)\(`name`\)"$/i, output)
+ assert_match(/t\.virtual\s+"name_length",\s+type: :integer,\s+as: "(?:octet_length|length)\(`name`\)",\s+stored: true$/i, output)
+ assert_match(/t\.virtual\s+"name_octet_length",\s+type: :integer,\s+as: "(?:octet_length|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 dc7ba314c6..afd422881b 100644
--- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb
@@ -1,7 +1,11 @@
-require 'cases/helper'
+# frozen_string_literal: true
+
+require "cases/helper"
class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase
def setup
+ ActiveRecord::Base.connection.materialize_transactions
+
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do
def execute(sql, name = nil) sql end
end
@@ -15,40 +19,89 @@ class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase
def test_create_database_with_encoding
assert_equal %(CREATE DATABASE "matt" ENCODING = 'utf8'), create_database(:matt)
- assert_equal %(CREATE DATABASE "aimonetti" ENCODING = 'latin1'), create_database(:aimonetti, :encoding => :latin1)
- assert_equal %(CREATE DATABASE "aimonetti" ENCODING = 'latin1'), create_database(:aimonetti, 'encoding' => :latin1)
+ assert_equal %(CREATE DATABASE "aimonetti" ENCODING = 'latin1'), create_database(:aimonetti, encoding: :latin1)
+ assert_equal %(CREATE DATABASE "aimonetti" ENCODING = 'latin1'), create_database(:aimonetti, "encoding" => :latin1)
end
def test_create_database_with_collation_and_ctype
- assert_equal %(CREATE DATABASE "aimonetti" ENCODING = 'UTF8' LC_COLLATE = 'ja_JP.UTF8' LC_CTYPE = 'ja_JP.UTF8'), create_database(:aimonetti, :encoding => :"UTF8", :collation => :"ja_JP.UTF8", :ctype => :"ja_JP.UTF8")
+ assert_equal %(CREATE DATABASE "aimonetti" ENCODING = 'UTF8' LC_COLLATE = 'ja_JP.UTF8' LC_CTYPE = 'ja_JP.UTF8'), create_database(:aimonetti, encoding: :"UTF8", collation: :"ja_JP.UTF8", ctype: :"ja_JP.UTF8")
end
def test_add_index
# add_index calls index_name_exists? which can't work since execute is stubbed
- ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.stubs(:index_name_exists?).returns(false)
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send(:define_method, :index_name_exists?) { |*| false }
expected = %(CREATE UNIQUE INDEX "index_people_on_last_name" ON "people" ("last_name") WHERE state = 'active')
- assert_equal expected, add_index(:people, :last_name, :unique => true, :where => "state = 'active'")
+ assert_equal expected, add_index(:people, :last_name, unique: true, where: "state = 'active'")
+
+ expected = %(CREATE UNIQUE INDEX "index_people_on_lower_last_name" ON "people" (lower(last_name)))
+ assert_equal expected, add_index(:people, "lower(last_name)", unique: true)
+
+ expected = %(CREATE UNIQUE INDEX "index_people_on_last_name_varchar_pattern_ops" ON "people" (last_name varchar_pattern_ops))
+ assert_equal expected, add_index(:people, "last_name varchar_pattern_ops", unique: true)
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)
expected = %(CREATE INDEX CONCURRENTLY "index_people_on_last_name" ON "people" USING #{type} ("last_name"))
assert_equal expected, add_index(:people, :last_name, using: type, algorithm: :concurrently)
+
+ expected = %(CREATE UNIQUE INDEX "index_people_on_last_name" ON "people" USING #{type} ("last_name") WHERE state = 'active')
+ assert_equal expected, add_index(:people, :last_name, using: type, unique: true, where: "state = 'active'")
+
+ expected = %(CREATE UNIQUE INDEX "index_people_on_lower_last_name" ON "people" USING #{type} (lower(last_name)))
+ assert_equal expected, add_index(:people, "lower(last_name)", using: type, unique: true)
+ end
+
+ expected = %(CREATE INDEX "index_people_on_last_name" ON "people" USING gist ("last_name" bpchar_pattern_ops))
+ assert_equal expected, add_index(:people, :last_name, using: :gist, opclass: { last_name: :bpchar_pattern_ops })
+
+ expected = %(CREATE INDEX "index_people_on_last_name_and_first_name" ON "people" ("last_name" DESC NULLS LAST, "first_name" ASC))
+ assert_equal expected, add_index(:people, [:last_name, :first_name], order: { last_name: "DESC NULLS LAST", first_name: :asc })
+
+ expected = %(CREATE INDEX "index_people_on_last_name" ON "people" ("last_name" NULLS FIRST))
+ assert_equal expected, add_index(:people, :last_name, order: "NULLS FIRST")
+
+ assert_raise ArgumentError do
+ add_index(:people, :last_name, algorithm: :copy)
+ end
+
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send :remove_method, :index_name_exists?
+ end
+
+ def test_remove_index
+ # remove_index calls index_name_for_remove which can't work since execute is stubbed
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send(:define_method, :index_name_for_remove) do |*|
+ "index_people_on_last_name"
end
+ expected = %(DROP INDEX CONCURRENTLY "index_people_on_last_name")
+ assert_equal expected, remove_index(:people, name: "index_people_on_last_name", algorithm: :concurrently)
+
assert_raise ArgumentError do
add_index(:people, :last_name, algorithm: :copy)
end
- expected = %(CREATE UNIQUE INDEX "index_people_on_last_name" ON "people" USING gist ("last_name"))
- assert_equal expected, add_index(:people, :last_name, :unique => true, :using => :gist)
- expected = %(CREATE UNIQUE INDEX "index_people_on_last_name" ON "people" USING gist ("last_name") WHERE state = 'active')
- assert_equal expected, add_index(:people, :last_name, :unique => true, :where => "state = 'active'", :using => :gist)
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send :remove_method, :index_name_for_remove
+ end
+
+ def test_remove_index_when_name_is_specified
+ expected = %(DROP INDEX CONCURRENTLY "index_people_on_last_name")
+ assert_equal expected, remove_index(:people, name: "index_people_on_last_name", algorithm: :concurrently)
+ end
+
+ def test_remove_index_with_wrong_option
+ assert_raises ArgumentError do
+ remove_index(:people, coulmn: :last_name)
+ end
end
private
diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb
index 380a90d765..42618c2ec3 100644
--- a/activerecord/test/cases/adapters/postgresql/array_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/array_test.rb
@@ -1,63 +1,100 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'support/schema_dumping_helper'
+require "support/schema_dumping_helper"
class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
include SchemaDumpingHelper
include InTimeZone
class PgArray < ActiveRecord::Base
- self.table_name = 'pg_arrays'
+ self.table_name = "pg_arrays"
end
def setup
@connection = ActiveRecord::Base.connection
- enable_extension!('hstore', @connection)
+ enable_extension!("hstore", @connection)
@connection.transaction do
- @connection.create_table('pg_arrays') do |t|
- t.string 'tags', array: true
- t.integer 'ratings', array: true
+ @connection.create_table("pg_arrays") do |t|
+ 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
- @column = PgArray.columns_hash['tags']
+ @column = PgArray.columns_hash["tags"]
@type = PgArray.type_for_attribute("tags")
end
teardown do
- @connection.drop_table 'pg_arrays', if_exists: true
- disable_extension!('hstore', @connection)
+ @connection.drop_table "pg_arrays", if_exists: true
+ disable_extension!("hstore", @connection)
end
def test_column
assert_equal :string, @column.type
- assert_equal "character varying", @column.sql_type
- assert @column.array?
- assert_not @type.binary?
+ assert_equal "character varying(255)", @column.sql_type
+ assert_predicate @column, :array?
+ assert_not_predicate @type, :binary?
- ratings_column = PgArray.columns_hash['ratings']
+ ratings_column = PgArray.columns_hash["ratings"]
assert_equal :integer, ratings_column.type
- assert ratings_column.array?
+ assert_predicate ratings_column, :array?
+ end
+
+ def test_not_compatible_with_serialize_array
+ new_klass = Class.new(PgArray) do
+ serialize :tags, Array
+ end
+ assert_raises(ActiveRecord::AttributeMethods::Serialization::ColumnNotSerializableError) do
+ new_klass.new
+ end
+ end
+
+ class MyTags
+ def initialize(tags); @tags = tags end
+ def to_a; @tags end
+ def self.load(tags); new(tags) end
+ def self.dump(object); object.to_a end
+ end
+
+ def test_array_with_serialized_attributes
+ new_klass = Class.new(PgArray) do
+ serialize :tags, MyTags
+ end
+
+ new_klass.create!(tags: MyTags.new(["one", "two"]))
+ record = new_klass.first
+
+ assert_instance_of MyTags, record.tags
+ assert_equal ["one", "two"], record.tags.to_a
+
+ record.tags = MyTags.new(["three", "four"])
+ record.save!
+
+ assert_equal ["three", "four"], record.reload.tags.to_a
end
def test_default
- @connection.add_column 'pg_arrays', 'score', :integer, array: true, default: [4, 4, 2]
+ @connection.add_column "pg_arrays", "score", :integer, array: true, default: [4, 4, 2]
PgArray.reset_column_information
- assert_equal([4, 4, 2], PgArray.column_defaults['score'])
+ assert_equal([4, 4, 2], PgArray.column_defaults["score"])
assert_equal([4, 4, 2], PgArray.new.score)
ensure
PgArray.reset_column_information
end
def test_default_strings
- @connection.add_column 'pg_arrays', 'names', :string, array: true, default: ["foo", "bar"]
+ @connection.add_column "pg_arrays", "names", :string, array: true, default: ["foo", "bar"]
PgArray.reset_column_information
- assert_equal(["foo", "bar"], PgArray.column_defaults['names'])
+ assert_equal(["foo", "bar"], PgArray.column_defaults["names"])
assert_equal(["foo", "bar"], PgArray.new.names)
ensure
PgArray.reset_column_information
@@ -68,11 +105,11 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
@connection.change_column :pg_arrays, :snippets, :text, array: true, default: []
PgArray.reset_column_information
- column = PgArray.columns_hash['snippets']
+ column = PgArray.columns_hash["snippets"]
assert_equal :text, column.type
- assert_equal [], PgArray.column_defaults['snippets']
- assert column.array?
+ assert_equal [], PgArray.column_defaults["snippets"]
+ assert_predicate column, :array?
end
def test_change_column_cant_make_non_array_column_to_array
@@ -88,17 +125,17 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
@connection.change_column_default :pg_arrays, :tags, []
PgArray.reset_column_information
- assert_equal [], PgArray.column_defaults['tags']
+ assert_equal [], PgArray.column_defaults["tags"]
end
def test_type_cast_array
- assert_equal(['1', '2', '3'], @type.deserialize('{1,2,3}'))
- assert_equal([], @type.deserialize('{}'))
- assert_equal([nil], @type.deserialize('{NULL}'))
+ assert_equal(["1", "2", "3"], @type.deserialize("{1,2,3}"))
+ assert_equal([], @type.deserialize("{}"))
+ assert_equal([nil], @type.deserialize("{NULL}"))
end
def test_type_cast_integers
- x = PgArray.new(ratings: ['1', '2'])
+ x = PgArray.new(ratings: ["1", "2"])
assert_equal([1, 2], x.ratings)
@@ -110,22 +147,23 @@ 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
@connection.execute "insert into pg_arrays (tags) VALUES ('{1,2,3}')"
x = PgArray.first
- assert_equal(['1','2','3'], x.tags)
+ assert_equal(["1", "2", "3"], x.tags)
end
def test_rewrite_with_strings
@connection.execute "insert into pg_arrays (tags) VALUES ('{1,2,3}')"
x = PgArray.first
- x.tags = ['1','2','3','4']
+ x.tags = ["1", "2", "3", "4"]
x.save!
- assert_equal ['1','2','3','4'], x.reload.tags
+ assert_equal ["1", "2", "3", "4"], x.reload.tags
end
def test_select_with_integers
@@ -137,25 +175,25 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
def test_rewrite_with_integers
@connection.execute "insert into pg_arrays (ratings) VALUES ('{1,2,3}')"
x = PgArray.first
- x.ratings = [2, '3', 4]
+ x.ratings = [2, "3", 4]
x.save!
assert_equal [2, 3, 4], x.reload.ratings
end
def test_multi_dimensional_with_strings
- assert_cycle(:tags, [[['1'], ['2']], [['2'], ['3']]])
+ assert_cycle(:tags, [[["1"], ["2"]], [["2"], ["3"]]])
end
def test_with_empty_strings
- assert_cycle(:tags, [ '1', '2', '', '4', '', '5' ])
+ assert_cycle(:tags, [ "1", "2", "", "4", "", "5" ])
end
def test_with_multi_dimensional_empty_strings
- assert_cycle(:tags, [[['1', '2'], ['', '4'], ['', '5']]])
+ assert_cycle(:tags, [[["1", "2"], ["", "4"], ["", "5"]]])
end
def test_with_arbitrary_whitespace
- assert_cycle(:tags, [[['1', '2'], [' ', '4'], [' ', '5']]])
+ assert_cycle(:tags, [[["1", "2"], [" ", "4"], [" ", "5"]]])
end
def test_multi_dimensional_with_integers
@@ -163,34 +201,47 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
end
def test_strings_with_quotes
- assert_cycle(:tags, ['this has','some "s that need to be escaped"'])
+ assert_cycle(:tags, ["this has", 'some "s that need to be escaped"'])
end
def test_strings_with_commas
- assert_cycle(:tags, ['this,has','many,values'])
+ assert_cycle(:tags, ["this,has", "many,values"])
end
def test_strings_with_array_delimiters
- assert_cycle(:tags, ['{','}'])
+ assert_cycle(:tags, ["{", "}"])
end
def test_strings_with_null_strings
- assert_cycle(:tags, ['NULL','NULL'])
+ assert_cycle(:tags, ["NULL", "NULL"])
end
def test_contains_nils
- assert_cycle(:tags, ['1',nil,nil])
+ assert_cycle(:tags, ["1", nil, nil])
end
def test_insert_fixture
tag_values = ["val1", "val2", "val3_with_'_multiple_quote_'_chars"]
- @connection.insert_fixture({"tags" => tag_values}, "pg_arrays" )
+ @connection.insert_fixture({ "tags" => tag_values }, "pg_arrays")
+ assert_equal(PgArray.last.tags, tag_values)
+ end
+
+ def test_insert_fixtures
+ tag_values = ["val1", "val2", "val3_with_'_multiple_quote_'_chars"]
+ assert_deprecated do
+ @connection.insert_fixtures([{ "tags" => tag_values }], "pg_arrays")
+ end
assert_equal(PgArray.last.tags, tag_values)
end
def test_attribute_for_inspect_for_array_field
+ record = PgArray.new { |a| a.ratings = (1..10).to_a }
+ assert_equal("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]", record.attribute_for_inspect(:ratings))
+ end
+
+ def test_attribute_for_inspect_for_array_field_for_large_array
record = PgArray.new { |a| a.ratings = (1..11).to_a }
- assert_equal("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]", record.attribute_for_inspect(:ratings))
+ assert_equal("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]", record.attribute_for_inspect(:ratings))
end
def test_escaping
@@ -206,17 +257,18 @@ 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)
+ assert_not_predicate x, :changed?
end
def test_quoting_non_standard_delimiters
strings = ["hello,", "world;"]
oid = ActiveRecord::ConnectionAdapters::PostgreSQL::OID
- comma_delim = oid::Array.new(ActiveRecord::Type::String.new, ',')
- semicolon_delim = oid::Array.new(ActiveRecord::Type::String.new, ';')
+ 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
@@ -227,18 +279,18 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
x.reload
assert_equal %w(one two three), x.tags
- assert_not x.changed?
+ assert_not_predicate x, :changed?
end
def test_mutate_value_in_array
- x = PgArray.create!(hstores: [{ a: 'a' }, { b: 'b' }])
+ x = PgArray.create!(hstores: [{ a: "a" }, { b: "b" }])
- x.hstores.first['a'] = 'c'
+ x.hstores.first["a"] = "c"
x.save!
x.reload
- assert_equal [{ 'a' => 'c' }, { 'b' => 'b' }], x.hstores
- assert_not x.changed?
+ assert_equal [{ "a" => "c" }, { "b" => "b" }], x.hstores
+ assert_not_predicate x, :changed?
end
def test_datetime_with_timezone_awareness
@@ -285,6 +337,12 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
assert_equal record.tags, record.reload.tags
end
+ def test_where_by_attribute_with_array
+ tags = ["black", "blue"]
+ record = PgArray.create!(tags: tags)
+ assert_equal record, PgArray.where(tags: tags).take
+ end
+
def test_uniqueness_validation
klass = Class.new(PgArray) do
validates_uniqueness_of :tags
@@ -295,23 +353,38 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
assert e1.persisted?, "Saving e1"
e2 = klass.create("tags" => ["black", "blue"])
- assert !e2.persisted?, "e2 shouldn't be valid"
+ assert_not e2.persisted?, "e2 shouldn't be valid"
assert e2.errors[:tags].any?, "Should have errors for tags"
assert_equal ["has already been taken"], e2.errors[:tags], "Should have uniqueness message for tags"
end
- private
- def assert_cycle field, array
- # test creation
- x = PgArray.create!(field => array)
- x.reload
- assert_equal(array, x.public_send(field))
+ def test_encoding_arrays_of_utf8_strings
+ arrays_of_utf8_strings = %w(nový ファイル)
+ assert_equal arrays_of_utf8_strings, @type.deserialize(@type.serialize(arrays_of_utf8_strings))
+ assert_equal [arrays_of_utf8_strings], @type.deserialize(@type.serialize([arrays_of_utf8_strings]))
+ end
- # test updating
- x = PgArray.create!(field => [])
- x.public_send("#{field}=", array)
- x.save!
- x.reload
- assert_equal(array, x.public_send(field))
+ 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
+ x = PgArray.create!(field => array)
+ x.reload
+ assert_equal(array, x.public_send(field))
+
+ # test updating
+ x = PgArray.create!(field => [])
+ x.public_send("#{field}=", array)
+ x.save!
+ x.reload
+ assert_equal(array, x.public_send(field))
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb
index 6f72fa6e0f..c8e728bbb6 100644
--- a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'support/connection_helper'
-require 'support/schema_dumping_helper'
+require "support/connection_helper"
+require "support/schema_dumping_helper"
class PostgresqlBitStringTest < ActiveRecord::PostgreSQLTestCase
include ConnectionHelper
@@ -10,7 +12,7 @@ class PostgresqlBitStringTest < ActiveRecord::PostgreSQLTestCase
def setup
@connection = ActiveRecord::Base.connection
- @connection.create_table('postgresql_bit_strings', :force => true) do |t|
+ @connection.create_table("postgresql_bit_strings", force: true) do |t|
t.bit :a_bit, default: "00000011", limit: 8
t.bit_varying :a_bit_varying, default: "0011", limit: 4
t.bit :another_bit
@@ -20,34 +22,34 @@ class PostgresqlBitStringTest < ActiveRecord::PostgreSQLTestCase
def teardown
return unless @connection
- @connection.drop_table 'postgresql_bit_strings', if_exists: true
+ @connection.drop_table "postgresql_bit_strings", if_exists: true
end
def test_bit_string_column
column = PostgresqlBitString.columns_hash["a_bit"]
assert_equal :bit, column.type
assert_equal "bit(8)", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = PostgresqlBitString.type_for_attribute("a_bit")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_bit_string_varying_column
column = PostgresqlBitString.columns_hash["a_bit_varying"]
assert_equal :bit_varying, column.type
assert_equal "bit varying(4)", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = PostgresqlBitString.type_for_attribute("a_bit_varying")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_default
- assert_equal "00000011", PostgresqlBitString.column_defaults['a_bit']
+ assert_equal "00000011", PostgresqlBitString.column_defaults["a_bit"]
assert_equal "00000011", PostgresqlBitString.new.a_bit
- assert_equal "0011", PostgresqlBitString.column_defaults['a_bit_varying']
+ assert_equal "0011", PostgresqlBitString.column_defaults["a_bit_varying"]
assert_equal "0011", PostgresqlBitString.new.a_bit_varying
end
@@ -57,16 +59,19 @@ class PostgresqlBitStringTest < ActiveRecord::PostgreSQLTestCase
assert_match %r{t\.bit_varying\s+"a_bit_varying",\s+limit: 4,\s+default: "0011"$}, output
end
- def test_assigning_invalid_hex_string_raises_exception
- assert_raises(ActiveRecord::StatementInvalid) { PostgresqlBitString.create! a_bit: "FF" }
- assert_raises(ActiveRecord::StatementInvalid) { PostgresqlBitString.create! a_bit_varying: "FF" }
+ if ActiveRecord::Base.connection.prepared_statements
+ def test_assigning_invalid_hex_string_raises_exception
+ assert_raises(ActiveRecord::StatementInvalid) { PostgresqlBitString.create! a_bit: "FF" }
+ assert_raises(ActiveRecord::StatementInvalid) { PostgresqlBitString.create! a_bit_varying: "F" }
+ end
end
def test_roundtrip
- PostgresqlBitString.create! a_bit: "00001010", a_bit_varying: "0101"
- record = PostgresqlBitString.first
+ record = PostgresqlBitString.create!(a_bit: "00001010", a_bit_varying: "0101")
assert_equal "00001010", record.a_bit
assert_equal "0101", record.a_bit_varying
+ assert_nil record.another_bit
+ assert_nil record.another_bit_varying
record.a_bit = "11111111"
record.a_bit_varying = "0xF"
diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb
index b6bb1929e6..3988c2adca 100644
--- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb
@@ -1,26 +1,31 @@
+# frozen_string_literal: true
+
require "cases/helper"
+require "support/schema_dumping_helper"
class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase
+ include SchemaDumpingHelper
+
class ByteaDataType < ActiveRecord::Base
- self.table_name = 'bytea_data_type'
+ self.table_name = "bytea_data_type"
end
def setup
@connection = ActiveRecord::Base.connection
begin
@connection.transaction do
- @connection.create_table('bytea_data_type') do |t|
- t.binary 'payload'
- t.binary 'serialized'
+ @connection.create_table("bytea_data_type") do |t|
+ t.binary "payload"
+ t.binary "serialized"
end
end
end
- @column = ByteaDataType.columns_hash['payload']
+ @column = ByteaDataType.columns_hash["payload"]
@type = ByteaDataType.type_for_attribute("payload")
end
teardown do
- @connection.drop_table 'bytea_data_type', if_exists: true
+ @connection.drop_table "bytea_data_type", if_exists: true
end
def test_column
@@ -29,9 +34,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
@@ -39,17 +44,17 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase
assert @column
data = "\u001F\x8B"
- assert_equal('UTF-8', data.encoding.name)
- assert_equal('ASCII-8BIT', @type.deserialize(data).encoding.name)
+ assert_equal("UTF-8", data.encoding.name)
+ assert_equal("ASCII-8BIT", @type.deserialize(data).encoding.name)
end
def test_type_cast_binary_value
- data = "\u001F\x8B".force_encoding("BINARY")
+ data = (+"\u001F\x8B").force_encoding("BINARY")
assert_equal(data, @type.deserialize(data))
end
def test_type_case_nil
- assert_equal(nil, @type.deserialize(nil))
+ assert_nil(@type.deserialize(nil))
end
def test_read_value
@@ -63,14 +68,14 @@ 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
def test_write_value
data = "\u001F"
record = ByteaDataType.create(payload: data)
- assert_not record.new_record?
+ assert_not_predicate record, :new_record?
assert_equal(data, record.payload)
end
@@ -85,26 +90,27 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase
def test_via_to_sql_with_complicating_connection
Thread.new do
other_conn = ActiveRecord::Base.connection
- other_conn.execute('SET standard_conforming_strings = off')
+ other_conn.execute("SET standard_conforming_strings = off")
+ other_conn.execute("SET escape_string_warning = off")
end.join
test_via_to_sql
end
def test_write_binary
- data = File.read(File.join(File.dirname(__FILE__), '..', '..', '..', 'assets', 'example.log'))
+ data = File.read(File.join(__dir__, "..", "..", "..", "assets", "example.log"))
assert(data.size > 1)
record = ByteaDataType.create(payload: data)
- assert_not record.new_record?
+ assert_not_predicate record, :new_record?
assert_equal(data, record.payload)
assert_equal(data, ByteaDataType.where(id: record.id).first.payload)
end
def test_write_nil
record = ByteaDataType.create(payload: nil)
- assert_not record.new_record?
- assert_equal(nil, record.payload)
- assert_equal(nil, ByteaDataType.where(id: record.id).first.payload)
+ assert_not_predicate record, :new_record?
+ assert_nil(record.payload)
+ assert_nil(ByteaDataType.where(id: record.id).first.payload)
end
class Serializer
@@ -122,4 +128,10 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase
obj.reload
assert_equal "hello world", obj.serialized
end
+
+ def test_schema_dumping
+ output = dump_table_schema("bytea_data_type")
+ assert_match %r{t\.binary\s+"payload"$}, output
+ assert_match %r{t\.binary\s+"serialized"$}, output
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/case_insensitive_test.rb b/activerecord/test/cases/adapters/postgresql/case_insensitive_test.rb
new file mode 100644
index 0000000000..305e033642
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/case_insensitive_test.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+
+class PostgresqlCaseInsensitiveTest < ActiveRecord::PostgreSQLTestCase
+ class Default < ActiveRecord::Base; end
+
+ def test_case_insensitiveness
+ connection = ActiveRecord::Base.connection
+ table = Default.arel_table
+
+ column = Default.columns_hash["char1"]
+ comparison = connection.case_insensitive_comparison table, :char1, column, nil
+ assert_match(/lower/i, comparison.to_sql)
+
+ column = Default.columns_hash["char2"]
+ comparison = connection.case_insensitive_comparison table, :char2, column, nil
+ assert_match(/lower/i, comparison.to_sql)
+
+ column = Default.columns_hash["char3"]
+ comparison = connection.case_insensitive_comparison table, :char3, column, nil
+ assert_match(/lower/i, comparison.to_sql)
+
+ column = Default.columns_hash["multiline_default"]
+ comparison = connection.case_insensitive_comparison table, :multiline_default, column, nil
+ assert_match(/lower/i, comparison.to_sql)
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/change_schema_test.rb b/activerecord/test/cases/adapters/postgresql/change_schema_test.rb
index bc12df668d..6dba4f3e14 100644
--- a/activerecord/test/cases/adapters/postgresql/change_schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/change_schema_test.rb
@@ -1,4 +1,6 @@
-require 'cases/helper'
+# frozen_string_literal: true
+
+require "cases/helper"
module ActiveRecord
class Migration
@@ -19,19 +21,19 @@ module ActiveRecord
def test_change_string_to_date
connection.change_column :strings, :somedate, :timestamp, using: 'CAST("somedate" AS timestamp)'
- assert_equal :datetime, connection.columns(:strings).find { |c| c.name == 'somedate' }.type
+ assert_equal :datetime, connection.columns(:strings).find { |c| c.name == "somedate" }.type
end
def test_change_type_with_symbol
connection.change_column :strings, :somedate, :timestamp, cast_as: :timestamp
- assert_equal :datetime, connection.columns(:strings).find { |c| c.name == 'somedate' }.type
+ assert_equal :datetime, connection.columns(:strings).find { |c| c.name == "somedate" }.type
end
def test_change_type_with_array
connection.change_column :strings, :somedate, :timestamp, array: true, cast_as: :timestamp
- column = connection.columns(:strings).find { |c| c.name == 'somedate' }
+ column = connection.columns(:strings).find { |c| c.name == "somedate" }
assert_equal :datetime, column.type
- assert column.array?
+ assert_predicate column, :array?
end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/cidr_test.rb b/activerecord/test/cases/adapters/postgresql/cidr_test.rb
index 52f2a0096c..f20958fbd2 100644
--- a/activerecord/test/cases/adapters/postgresql/cidr_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/cidr_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "ipaddr"
diff --git a/activerecord/test/cases/adapters/postgresql/citext_test.rb b/activerecord/test/cases/adapters/postgresql/citext_test.rb
index bd62041e79..9eb0b7d99c 100644
--- a/activerecord/test/cases/adapters/postgresql/citext_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/citext_test.rb
@@ -1,78 +1,78 @@
-require 'cases/helper'
-require 'support/schema_dumping_helper'
-
-if ActiveRecord::Base.connection.supports_extensions?
- class PostgresqlCitextTest < ActiveRecord::PostgreSQLTestCase
- include SchemaDumpingHelper
- class Citext < ActiveRecord::Base
- self.table_name = 'citexts'
- end
+# frozen_string_literal: true
- def setup
- @connection = ActiveRecord::Base.connection
+require "cases/helper"
+require "support/schema_dumping_helper"
- enable_extension!('citext', @connection)
+class PostgresqlCitextTest < ActiveRecord::PostgreSQLTestCase
+ include SchemaDumpingHelper
+ class Citext < ActiveRecord::Base
+ self.table_name = "citexts"
+ end
- @connection.create_table('citexts') do |t|
- t.citext 'cival'
- end
- end
+ def setup
+ @connection = ActiveRecord::Base.connection
- teardown do
- @connection.drop_table 'citexts', if_exists: true
- disable_extension!('citext', @connection)
- end
+ enable_extension!("citext", @connection)
- def test_citext_enabled
- assert @connection.extension_enabled?('citext')
+ @connection.create_table("citexts") do |t|
+ t.citext "cival"
end
+ end
- def test_column
- column = Citext.columns_hash['cival']
- assert_equal :citext, column.type
- assert_equal 'citext', column.sql_type
- assert_not column.array?
+ teardown do
+ @connection.drop_table "citexts", if_exists: true
+ disable_extension!("citext", @connection)
+ end
- type = Citext.type_for_attribute('cival')
- assert_not type.binary?
- end
+ def test_citext_enabled
+ assert @connection.extension_enabled?("citext")
+ end
- def test_change_table_supports_json
- @connection.transaction do
- @connection.change_table('citexts') do |t|
- t.citext 'username'
- end
- Citext.reset_column_information
- column = Citext.columns_hash['username']
- assert_equal :citext, column.type
+ def test_column
+ column = Citext.columns_hash["cival"]
+ assert_equal :citext, column.type
+ assert_equal "citext", column.sql_type
+ assert_not_predicate column, :array?
- raise ActiveRecord::Rollback # reset the schema change
+ type = Citext.type_for_attribute("cival")
+ assert_not_predicate type, :binary?
+ end
+
+ def test_change_table_supports_json
+ @connection.transaction do
+ @connection.change_table("citexts") do |t|
+ t.citext "username"
end
- ensure
Citext.reset_column_information
+ column = Citext.columns_hash["username"]
+ assert_equal :citext, column.type
+
+ raise ActiveRecord::Rollback # reset the schema change
end
+ ensure
+ Citext.reset_column_information
+ end
- def test_write
- x = Citext.new(cival: 'Some CI Text')
- x.save!
- citext = Citext.first
- assert_equal "Some CI Text", citext.cival
+ def test_write
+ x = Citext.new(cival: "Some CI Text")
+ x.save!
+ citext = Citext.first
+ assert_equal "Some CI Text", citext.cival
- citext.cival = "Some NEW CI Text"
- citext.save!
+ citext.cival = "Some NEW CI Text"
+ citext.save!
- assert_equal "Some NEW CI Text", citext.reload.cival
- end
+ assert_equal "Some NEW CI Text", citext.reload.cival
+ end
- def test_select_case_insensitive
- @connection.execute "insert into citexts (cival) values('Cased Text')"
- x = Citext.where(cival: 'cased text').first
- assert_equal 'Cased Text', x.cival
- end
+ def test_select_case_insensitive
+ @connection.execute "insert into citexts (cival) values('Cased Text')"
+ x = Citext.where(cival: "cased text").first
+ assert_equal "Cased Text", x.cival
+ end
- def test_schema_dump_with_shorthand
- output = dump_table_schema("citexts")
- assert_match %r[t\.citext "cival"], output
- end
+ def test_schema_dump_with_shorthand
+ output = dump_table_schema("citexts")
+ assert_match %r[t\.citext "cival"], output
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/collation_test.rb b/activerecord/test/cases/adapters/postgresql/collation_test.rb
index 8470329c35..7468f4c4f8 100644
--- a/activerecord/test/cases/adapters/postgresql/collation_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/collation_test.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'support/schema_dumping_helper'
+require "support/schema_dumping_helper"
class PostgresqlCollationTest < ActiveRecord::PostgreSQLTestCase
include SchemaDumpingHelper
@@ -7,8 +9,8 @@ class PostgresqlCollationTest < ActiveRecord::PostgreSQLTestCase
def setup
@connection = ActiveRecord::Base.connection
@connection.create_table :postgresql_collations, force: true do |t|
- t.string :string_c, collation: 'C'
- t.text :text_posix, collation: 'POSIX'
+ t.string :string_c, collation: "C"
+ t.text :text_posix, collation: "POSIX"
end
end
@@ -17,37 +19,37 @@ class PostgresqlCollationTest < ActiveRecord::PostgreSQLTestCase
end
test "string column with collation" do
- column = @connection.columns(:postgresql_collations).find { |c| c.name == 'string_c' }
+ column = @connection.columns(:postgresql_collations).find { |c| c.name == "string_c" }
assert_equal :string, column.type
- assert_equal 'C', column.collation
+ assert_equal "C", column.collation
end
test "text column with collation" do
- column = @connection.columns(:postgresql_collations).find { |c| c.name == 'text_posix' }
+ column = @connection.columns(:postgresql_collations).find { |c| c.name == "text_posix" }
assert_equal :text, column.type
- assert_equal 'POSIX', column.collation
+ assert_equal "POSIX", column.collation
end
test "add column with collation" do
- @connection.add_column :postgresql_collations, :title, :string, collation: 'C'
+ @connection.add_column :postgresql_collations, :title, :string, collation: "C"
- column = @connection.columns(:postgresql_collations).find { |c| c.name == 'title' }
+ column = @connection.columns(:postgresql_collations).find { |c| c.name == "title" }
assert_equal :string, column.type
- assert_equal 'C', column.collation
+ assert_equal "C", column.collation
end
test "change column with collation" do
@connection.add_column :postgresql_collations, :description, :string
- @connection.change_column :postgresql_collations, :description, :text, collation: 'POSIX'
+ @connection.change_column :postgresql_collations, :description, :text, collation: "POSIX"
- column = @connection.columns(:postgresql_collations).find { |c| c.name == 'description' }
+ column = @connection.columns(:postgresql_collations).find { |c| c.name == "description" }
assert_equal :text, column.type
- assert_equal 'POSIX', column.collation
+ assert_equal "POSIX", column.collation
end
test "schema dump includes collation" do
output = dump_table_schema("postgresql_collations")
- assert_match %r{t.string\s+"string_c",\s+collation: "C"$}, output
- assert_match %r{t.text\s+"text_posix",\s+collation: "POSIX"$}, output
+ assert_match %r{t\.string\s+"string_c",\s+collation: "C"$}, output
+ assert_match %r{t\.text\s+"text_posix",\s+collation: "POSIX"$}, output
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/composite_test.rb b/activerecord/test/cases/adapters/postgresql/composite_test.rb
index 1de87e5f01..b0ce2694a3 100644
--- a/activerecord/test/cases/adapters/postgresql/composite_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'support/connection_helper'
+require "support/connection_helper"
module PostgresqlCompositeBehavior
include ConnectionHelper
@@ -20,7 +22,7 @@ module PostgresqlCompositeBehavior
street VARCHAR(90)
);
SQL
- @connection.create_table('postgresql_composites') do |t|
+ @connection.create_table("postgresql_composites") do |t|
t.column :address, :full_address
end
end
@@ -29,8 +31,8 @@ module PostgresqlCompositeBehavior
def teardown
super
- @connection.drop_table 'postgresql_composites', if_exists: true
- @connection.execute 'DROP TYPE IF EXISTS full_address'
+ @connection.drop_table "postgresql_composites", if_exists: true
+ @connection.execute "DROP TYPE IF EXISTS full_address"
reset_connection
PostgresqlComposite.reset_column_information
end
@@ -49,10 +51,10 @@ class PostgresqlCompositeTest < ActiveRecord::PostgreSQLTestCase
column = PostgresqlComposite.columns_hash["address"]
assert_nil column.type
assert_equal "full_address", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = PostgresqlComposite.type_for_attribute("address")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_composite_mapping
@@ -69,12 +71,12 @@ class PostgresqlCompositeTest < ActiveRecord::PostgreSQLTestCase
end
private
- def ensure_warning_is_issued
- warning = capture(:stderr) do
- PostgresqlComposite.columns_hash
+ def ensure_warning_is_issued
+ warning = capture(:stderr) do
+ PostgresqlComposite.columns_hash
+ end
+ assert_match(/unknown OID \d+: failed to recognize type of 'address'\. It will be treated as String\./, warning)
end
- assert_match(/unknown OID \d+: failed to recognize type of 'address'\. It will be treated as String\./, warning)
- end
end
class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::PostgreSQLTestCase
@@ -104,17 +106,17 @@ class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::PostgreSQLTestCase
def setup
super
- @connection.type_map.register_type "full_address", FullAddressType.new
+ @connection.send(:type_map).register_type "full_address", FullAddressType.new
end
def test_column
column = PostgresqlComposite.columns_hash["address"]
assert_equal :full_address, column.type
assert_equal "full_address", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = PostgresqlComposite.type_for_attribute("address")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_composite_mapping
@@ -126,7 +128,7 @@ class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::PostgreSQLTestCase
composite.address = FullAddress.new("Paris", "Rue Basse")
composite.save!
- assert_equal 'Paris', composite.reload.address.city
- assert_equal 'Rue Basse', composite.reload.address.street
+ assert_equal "Paris", composite.reload.address.city
+ assert_equal "Rue Basse", composite.reload.address.street
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb
index 820d41e13b..70aa189893 100644
--- a/activerecord/test/cases/adapters/postgresql/connection_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'support/connection_helper'
+require "support/connection_helper"
module ActiveRecord
class PostgresqlConnectionTest < ActiveRecord::PostgreSQLTestCase
@@ -13,8 +15,9 @@ module ActiveRecord
def setup
super
@subscriber = SQLSubscriber.new
- @subscription = ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber)
@connection = ActiveRecord::Base.connection
+ @connection.materialize_transactions
+ @subscription = ActiveSupport::Notifications.subscribe("sql.active_record", @subscriber)
end
def teardown
@@ -23,23 +26,29 @@ module ActiveRecord
end
def test_truncate
- count = ActiveRecord::Base.connection.execute("select count(*) from comments").first['count'].to_i
+ count = ActiveRecord::Base.connection.execute("select count(*) from comments").first["count"].to_i
assert_operator count, :>, 0
ActiveRecord::Base.connection.truncate("comments")
- count = ActiveRecord::Base.connection.execute("select count(*) from comments").first['count'].to_i
+ count = ActiveRecord::Base.connection.execute("select count(*) from comments").first["count"].to_i
assert_equal 0, count
end
def test_encoding
- assert_not_nil @connection.encoding
+ assert_queries(1) do
+ assert_not_nil @connection.encoding
+ end
end
def test_collation
- assert_not_nil @connection.collation
+ assert_queries(1) do
+ assert_not_nil @connection.collation
+ end
end
def test_ctype
- assert_not_nil @connection.ctype
+ assert_queries(1) do
+ assert_not_nil @connection.ctype
+ end
end
def test_default_client_min_messages
@@ -54,85 +63,87 @@ module ActiveRecord
NonExistentTable.establish_connection(params)
# Verify the connection param has been applied.
- expect = NonExistentTable.connection.query('show geqo').first.first
- assert_equal 'off', expect
+ expect = NonExistentTable.connection.query("show geqo").first.first
+ assert_equal "off", expect
end
def test_reset
- @connection.query('ROLLBACK')
- @connection.query('SET geqo TO off')
+ @connection.query("ROLLBACK")
+ @connection.query("SET geqo TO off")
# Verify the setting has been applied.
- expect = @connection.query('show geqo').first.first
- assert_equal 'off', expect
+ expect = @connection.query("show geqo").first.first
+ assert_equal "off", expect
@connection.reset!
# Verify the setting has been cleared.
- expect = @connection.query('show geqo').first.first
- assert_equal 'on', expect
+ expect = @connection.query("show geqo").first.first
+ assert_equal "on", expect
end
def test_reset_with_transaction
- @connection.query('ROLLBACK')
- @connection.query('SET geqo TO off')
+ @connection.query("ROLLBACK")
+ @connection.query("SET geqo TO off")
# Verify the setting has been applied.
- expect = @connection.query('show geqo').first.first
- assert_equal 'off', expect
+ expect = @connection.query("show geqo").first.first
+ assert_equal "off", expect
- @connection.query('BEGIN')
+ @connection.query("BEGIN")
@connection.reset!
# Verify the setting has been cleared.
- expect = @connection.query('show geqo').first.first
- assert_equal 'on', expect
+ expect = @connection.query("show geqo").first.first
+ assert_equal "on", expect
end
def test_tables_logs_name
- @connection.tables('hello')
- assert_equal 'SCHEMA', @subscriber.logged[0][1]
+ @connection.tables
+ assert_equal "SCHEMA", @subscriber.logged[0][1]
end
def test_indexes_logs_name
- @connection.indexes('items', 'hello')
- assert_equal 'SCHEMA', @subscriber.logged[0][1]
+ @connection.indexes("items")
+ assert_equal "SCHEMA", @subscriber.logged[0][1]
end
def test_table_exists_logs_name
- @connection.table_exists?('items')
- assert_equal 'SCHEMA', @subscriber.logged[0][1]
+ @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]
+ assert_equal "SCHEMA", @subscriber.logged[0][1]
end
def test_current_database_logs_name
@connection.current_database
- assert_equal 'SCHEMA', @subscriber.logged[0][1]
+ assert_equal "SCHEMA", @subscriber.logged[0][1]
end
def test_encoding_logs_name
@connection.encoding
- assert_equal 'SCHEMA', @subscriber.logged[0][1]
+ assert_equal "SCHEMA", @subscriber.logged[0][1]
end
def test_schema_names_logs_name
@connection.schema_names
- assert_equal 'SCHEMA', @subscriber.logged[0][1]
+ assert_equal "SCHEMA", @subscriber.logged[0][1]
end
- def test_statement_key_is_logged
- bind = Relation::QueryAttribute.new(nil, 1, Type::Value.new)
- @connection.exec_query('SELECT $1::integer', 'SQL', [bind])
- name = @subscriber.payloads.last[:statement_name]
- assert name
- res = @connection.exec_query("EXPLAIN (FORMAT JSON) EXECUTE #{name}(1)")
- plan = res.column_types['QUERY PLAN'].deserialize res.rows.first.first
- assert_operator plan.length, :>, 0
+ if ActiveRecord::Base.connection.prepared_statements
+ def test_statement_key_is_logged
+ bind = Relation::QueryAttribute.new(nil, 1, Type::Value.new)
+ @connection.exec_query("SELECT $1::integer", "SQL", [bind], prepare: true)
+ name = @subscriber.payloads.last[:statement_name]
+ assert name
+ res = @connection.exec_query("EXPLAIN (FORMAT JSON) EXECUTE #{name}(1)")
+ plan = res.column_types["QUERY PLAN"].deserialize res.rows.first.first
+ assert_operator plan.length, :>, 0
+ end
end
# Must have PostgreSQL >= 9.2, or with_manual_interventions set to
@@ -141,20 +152,20 @@ module ActiveRecord
# When prompted, restart the PostgreSQL server with the
# "-m fast" option or kill the individual connection assuming
# you know the incantation to do that.
- # To restart PostgreSQL 9.1 on OS X, installed via MacPorts, ...
+ # To restart PostgreSQL 9.1 on macOS, installed via MacPorts, ...
# sudo su postgres -c "pg_ctl restart -D /opt/local/var/db/postgresql91/defaultdb/ -m fast"
def test_reconnection_after_actual_disconnection_with_verify
- original_connection_pid = @connection.query('select pg_backend_pid()')
+ original_connection_pid = @connection.query("select pg_backend_pid()")
# Sanity check.
- assert @connection.active?
+ assert_predicate @connection, :active?
if @connection.send(:postgresql_version) >= 90200
secondary_connection = ActiveRecord::Base.connection_pool.checkout
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 ' +
+ elsif ARTest.config["with_manual_interventions"]
+ puts "Kill the connection now (e.g. by restarting the PostgreSQL " \
'server with the "-m fast" option) and then press enter.'
$stdin.gets
else
@@ -166,23 +177,23 @@ module ActiveRecord
@connection.verify!
- assert @connection.active?
+ assert_predicate @connection, :active?
# If we get no exception here, then either we re-connected successfully, or
# we never actually got disconnected.
- new_connection_pid = @connection.query('select pg_backend_pid()')
+ 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
def test_set_session_variable_true
run_without_connection do |orig_connection|
- ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:debug_print_plan => true}}))
+ ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { debug_print_plan: true }))
set_true = ActiveRecord::Base.connection.exec_query "SHOW DEBUG_PRINT_PLAN"
assert_equal set_true.rows, [["on"]]
end
@@ -190,7 +201,7 @@ module ActiveRecord
def test_set_session_variable_false
run_without_connection do |orig_connection|
- ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:debug_print_plan => false}}))
+ ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { debug_print_plan: false }))
set_false = ActiveRecord::Base.connection.exec_query "SHOW DEBUG_PRINT_PLAN"
assert_equal set_false.rows, [["off"]]
end
@@ -199,15 +210,64 @@ module ActiveRecord
def test_set_session_variable_nil
run_without_connection do |orig_connection|
# This should be a no-op that does not raise an error
- ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:debug_print_plan => nil}}))
+ ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { debug_print_plan: nil }))
end
end
def test_set_session_variable_default
run_without_connection do |orig_connection|
# This should execute a query that does not raise an error
- ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:debug_print_plan => :default}}))
+ ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { debug_print_plan: :default }))
+ end
+ end
+
+ def test_set_session_timezone
+ run_without_connection do |orig_connection|
+ ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { timezone: "America/New_York" }))
+ assert_equal "America/New_York", ActiveRecord::Base.connection.query_value("SHOW TIME ZONE")
+ end
+ end
+
+ def test_get_and_release_advisory_lock
+ lock_id = 5295901941911233559
+ list_advisory_locks = <<-SQL
+ SELECT locktype,
+ (classid::bigint << 32) | objid::bigint AS lock_id
+ FROM pg_locks
+ WHERE locktype = 'advisory'
+ SQL
+
+ got_lock = @connection.get_advisory_lock(lock_id)
+ assert got_lock, "get_advisory_lock should have returned true but it didn't"
+
+ advisory_lock = @connection.query(list_advisory_locks).find { |l| l[1] == lock_id }
+ assert advisory_lock,
+ "expected to find an advisory lock with lock_id #{lock_id} but there wasn't one"
+
+ released_lock = @connection.release_advisory_lock(lock_id)
+ assert released_lock, "expected release_advisory_lock to return true but it didn't"
+
+ advisory_locks = @connection.query(list_advisory_locks).select { |l| l[1] == lock_id }
+ assert_empty advisory_locks,
+ "expected to have released advisory lock with lock_id #{lock_id} but it was still held"
+ end
+
+ def test_release_non_existent_advisory_lock
+ fake_lock_id = 2940075057017742022
+ with_warning_suppression do
+ released_non_existent_lock = @connection.release_advisory_lock(fake_lock_id)
+ assert_equal released_non_existent_lock, false,
+ "expected release_advisory_lock to return false when there was no lock to release"
end
end
+
+ private
+
+ def with_warning_suppression
+ log_level = @connection.client_min_messages
+ @connection.client_min_messages = "error"
+ yield
+ @connection.client_min_messages = log_level
+ end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/create_unlogged_tables_test.rb b/activerecord/test/cases/adapters/postgresql/create_unlogged_tables_test.rb
new file mode 100644
index 0000000000..a02bae1453
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/create_unlogged_tables_test.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "support/schema_dumping_helper"
+
+class UnloggedTablesTest < ActiveRecord::PostgreSQLTestCase
+ include SchemaDumpingHelper
+
+ TABLE_NAME = "things"
+ LOGGED_FIELD = "relpersistence"
+ LOGGED_QUERY = "SELECT #{LOGGED_FIELD} FROM pg_class WHERE relname = '#{TABLE_NAME}'"
+ LOGGED = "p"
+ UNLOGGED = "u"
+ TEMPORARY = "t"
+
+ class Thing < ActiveRecord::Base
+ self.table_name = TABLE_NAME
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables = false
+ end
+
+ teardown do
+ @connection.drop_table TABLE_NAME, if_exists: true
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables = false
+ end
+
+ def test_logged_by_default
+ @connection.create_table(TABLE_NAME) do |t|
+ end
+ assert_equal @connection.execute(LOGGED_QUERY).first[LOGGED_FIELD], LOGGED
+ end
+
+ def test_unlogged_in_test_environment_when_unlogged_setting_enabled
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables = true
+
+ @connection.create_table(TABLE_NAME) do |t|
+ end
+ assert_equal @connection.execute(LOGGED_QUERY).first[LOGGED_FIELD], UNLOGGED
+ end
+
+ def test_not_included_in_schema_dump
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables = true
+
+ @connection.create_table(TABLE_NAME) do |t|
+ end
+ assert_no_match(/unlogged/i, dump_table_schema(TABLE_NAME))
+ end
+
+ def test_not_changed_in_change_table
+ @connection.create_table(TABLE_NAME) do |t|
+ end
+
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables = true
+
+ @connection.change_table(TABLE_NAME) do |t|
+ t.column :name, :string
+ end
+ assert_equal @connection.execute(LOGGED_QUERY).first[LOGGED_FIELD], LOGGED
+ end
+
+ def test_gracefully_handles_temporary_tables
+ @connection.create_table(TABLE_NAME, temporary: true) do |t|
+ end
+
+ # Temporary tables are already unlogged, though this query results in a
+ # different result ("t" vs. "u"). This test is really just checking that we
+ # didn't try to run `CREATE TEMPORARY UNLOGGED TABLE`, which would result in
+ # a PostgreSQL error.
+ assert_equal @connection.execute(LOGGED_QUERY).first[LOGGED_FIELD], TEMPORARY
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb
index 232c25cb3b..b7535d5c9a 100644
--- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb
@@ -1,6 +1,7 @@
-require "cases/helper"
-require 'support/ddl_helper'
+# frozen_string_literal: true
+require "cases/helper"
+require "support/ddl_helper"
class PostgresqlTime < ActiveRecord::Base
end
@@ -29,17 +30,17 @@ 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
- assert_equal '-1 years -2 days', @first_time.time_interval
- assert_equal '-21 days', @first_time.scaled_time_interval
+ assert_equal "-1 years -2 days", @first_time.time_interval
+ assert_equal "-21 days", @first_time.scaled_time_interval
end
def test_oid_values
@@ -47,10 +48,10 @@ class PostgresqlDataTypeTest < ActiveRecord::PostgreSQLTestCase
end
def test_update_time
- @first_time.time_interval = '2 years 3 minutes'
+ @first_time.time_interval = "2 years 3 minutes"
assert @first_time.save
assert @first_time.reload
- assert_equal '2 years 00:03:00', @first_time.time_interval
+ assert_equal "2 years 00:03:00", @first_time.time_interval
end
def test_update_oid
@@ -62,9 +63,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
@@ -77,15 +78,15 @@ class PostgresqlInternalDataTypeTest < ActiveRecord::PostgreSQLTestCase
end
def test_name_column_type
- with_example_table @connection, 'ex', 'data name' do
- column = @connection.columns('ex').find { |col| col.name == 'data' }
+ with_example_table @connection, "ex", "data name" do
+ column = @connection.columns("ex").find { |col| col.name == "data" }
assert_equal :string, column.type
end
end
def test_char_column_type
- with_example_table @connection, 'ex', 'data "char"' do
- column = @connection.columns('ex').find { |col| col.name == 'data' }
+ with_example_table @connection, "ex", 'data "char"' do
+ column = @connection.columns("ex").find { |col| col.name == "data" }
assert_equal :string, column.type
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/date_test.rb b/activerecord/test/cases/adapters/postgresql/date_test.rb
new file mode 100644
index 0000000000..a86abac2be
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/date_test.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/topic"
+
+class PostgresqlDateTest < ActiveRecord::PostgreSQLTestCase
+ def test_load_infinity_and_beyond
+ topic = Topic.find_by_sql("SELECT 'infinity'::date AS last_read").first
+ assert topic.last_read.infinite?, "timestamp should be infinite"
+ assert_operator topic.last_read, :>, 0
+
+ topic = Topic.find_by_sql("SELECT '-infinity'::date AS last_read").first
+ assert topic.last_read.infinite?, "timestamp should be infinite"
+ assert_operator topic.last_read, :<, 0
+ end
+
+ def test_save_infinity_and_beyond
+ topic = Topic.create!(last_read: 1.0 / 0.0)
+ assert_equal(1.0 / 0.0, topic.last_read)
+
+ topic = Topic.create!(last_read: -1.0 / 0.0)
+ assert_equal(-1.0 / 0.0, topic.last_read)
+ end
+
+ def test_bc_date
+ date = Date.new(0) - 1.week
+ topic = Topic.create!(last_read: date)
+ assert_equal date, Topic.find(topic.id).last_read
+ end
+
+ def test_bc_date_leap_year
+ date = Time.utc(-4, 2, 29).to_date
+ topic = Topic.create!(last_read: date)
+ assert_equal date, Topic.find(topic.id).last_read
+ end
+
+ def test_bc_date_year_zero
+ date = Time.utc(0, 4, 7).to_date
+ topic = Topic.create!(last_read: date)
+ assert_equal date, Topic.find(topic.id).last_read
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/domain_test.rb b/activerecord/test/cases/adapters/postgresql/domain_test.rb
index 6102ddacd1..eeaad94c27 100644
--- a/activerecord/test/cases/adapters/postgresql/domain_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/domain_test.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'support/connection_helper'
+require "support/connection_helper"
class PostgresqlDomainTest < ActiveRecord::PostgreSQLTestCase
include ConnectionHelper
@@ -12,15 +14,15 @@ class PostgresqlDomainTest < ActiveRecord::PostgreSQLTestCase
@connection = ActiveRecord::Base.connection
@connection.transaction do
@connection.execute "CREATE DOMAIN custom_money as numeric(8,2)"
- @connection.create_table('postgresql_domains') do |t|
+ @connection.create_table("postgresql_domains") do |t|
t.column :price, :custom_money
end
end
end
teardown do
- @connection.drop_table 'postgresql_domains', if_exists: true
- @connection.execute 'DROP DOMAIN IF EXISTS custom_money'
+ @connection.drop_table "postgresql_domains", if_exists: true
+ @connection.execute "DROP DOMAIN IF EXISTS custom_money"
reset_connection
end
@@ -28,10 +30,10 @@ class PostgresqlDomainTest < ActiveRecord::PostgreSQLTestCase
column = PostgresqlDomain.columns_hash["price"]
assert_equal :decimal, column.type
assert_equal "custom_money", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = PostgresqlDomain.type_for_attribute("price")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_domain_acts_like_basetype
@@ -42,6 +44,6 @@ class PostgresqlDomainTest < ActiveRecord::PostgreSQLTestCase
record.price = "34.15"
record.save!
- assert_equal BigDecimal.new("34.15"), record.reload.price
+ assert_equal BigDecimal("34.15"), record.reload.price
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/enum_test.rb b/activerecord/test/cases/adapters/postgresql/enum_test.rb
index 6816a6514b..6789ff63e7 100644
--- a/activerecord/test/cases/adapters/postgresql/enum_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/enum_test.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'support/connection_helper'
+require "support/connection_helper"
class PostgresqlEnumTest < ActiveRecord::PostgreSQLTestCase
include ConnectionHelper
@@ -14,15 +16,15 @@ class PostgresqlEnumTest < ActiveRecord::PostgreSQLTestCase
@connection.execute <<-SQL
CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy');
SQL
- @connection.create_table('postgresql_enums') do |t|
+ @connection.create_table("postgresql_enums") do |t|
t.column :current_mood, :mood
end
end
end
teardown do
- @connection.drop_table 'postgresql_enums', if_exists: true
- @connection.execute 'DROP TYPE IF EXISTS mood'
+ @connection.drop_table "postgresql_enums", if_exists: true
+ @connection.execute "DROP TYPE IF EXISTS mood"
reset_connection
end
@@ -30,17 +32,17 @@ class PostgresqlEnumTest < ActiveRecord::PostgreSQLTestCase
column = PostgresqlEnum.columns_hash["current_mood"]
assert_equal :enum, column.type
assert_equal "mood", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = PostgresqlEnum.type_for_attribute("current_mood")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_enum_defaults
- @connection.add_column 'postgresql_enums', 'good_mood', :mood, default: 'happy'
+ @connection.add_column "postgresql_enums", "good_mood", :mood, default: "happy"
PostgresqlEnum.reset_column_information
- assert_equal "happy", PostgresqlEnum.column_defaults['good_mood']
+ assert_equal "happy", PostgresqlEnum.column_defaults["good_mood"]
assert_equal "happy", PostgresqlEnum.new.good_mood
ensure
PostgresqlEnum.reset_column_information
@@ -71,7 +73,7 @@ class PostgresqlEnumTest < ActiveRecord::PostgreSQLTestCase
@connection.execute "INSERT INTO postgresql_enums VALUES (1, 'sad');"
stderr_output = capture(:stderr) { PostgresqlEnum.first }
- assert stderr_output.blank?
+ assert_predicate stderr_output, :blank?
end
def test_enum_type_cast
diff --git a/activerecord/test/cases/adapters/postgresql/explain_test.rb b/activerecord/test/cases/adapters/postgresql/explain_test.rb
index 4d0fd640aa..be525383e9 100644
--- a/activerecord/test/cases/adapters/postgresql/explain_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/explain_test.rb
@@ -1,20 +1,22 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/developer'
-require 'models/computer'
+require "models/author"
+require "models/post"
class PostgreSQLExplainTest < ActiveRecord::PostgreSQLTestCase
- fixtures :developers
+ fixtures :authors
def test_explain_for_one_query
- explain = Developer.where(:id => 1).explain
- assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = $1), explain
+ explain = Author.where(id: 1).explain
+ assert_match %r(EXPLAIN for: SELECT "authors"\.\* FROM "authors" WHERE "authors"\."id" = (?:\$1 \[\["id", 1\]\]|1)), explain
assert_match %(QUERY PLAN), explain
end
def test_explain_with_eager_loading
- explain = Developer.where(:id => 1).includes(:audit_logs).explain
+ explain = Author.where(id: 1).includes(:posts).explain
assert_match %(QUERY PLAN), explain
- assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = $1), explain
- assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" = 1), explain
+ assert_match %r(EXPLAIN for: SELECT "authors"\.\* FROM "authors" WHERE "authors"\."id" = (?:\$1 \[\["id", 1\]\]|1)), explain
+ assert_match %r(EXPLAIN for: SELECT "posts"\.\* FROM "posts" WHERE "posts"\."author_id" = (?:\$1 \[\["author_id", 1\]\]|1)), explain
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb
index 9cfc133308..df97ab11e7 100644
--- a/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb
@@ -1,15 +1,17 @@
+# frozen_string_literal: true
+
require "cases/helper"
class PostgresqlExtensionMigrationTest < ActiveRecord::PostgreSQLTestCase
self.use_transactional_tests = false
- class EnableHstore < ActiveRecord::Migration
+ class EnableHstore < ActiveRecord::Migration::Current
def change
enable_extension "hstore"
end
end
- class DisableHstore < ActiveRecord::Migration
+ class DisableHstore < ActiveRecord::Migration::Current
def change
disable_extension "hstore"
end
@@ -20,13 +22,9 @@ class PostgresqlExtensionMigrationTest < ActiveRecord::PostgreSQLTestCase
@connection = ActiveRecord::Base.connection
- unless @connection.supports_extensions?
- return skip("no extension support")
- end
-
- @old_schema_migration_tabel_name = ActiveRecord::SchemaMigration.table_name
- @old_tabel_name_prefix = ActiveRecord::Base.table_name_prefix
- @old_tabel_name_suffix = ActiveRecord::Base.table_name_suffix
+ @old_schema_migration_table_name = ActiveRecord::SchemaMigration.table_name
+ @old_table_name_prefix = ActiveRecord::Base.table_name_prefix
+ @old_table_name_suffix = ActiveRecord::Base.table_name_suffix
ActiveRecord::Base.table_name_prefix = "p_"
ActiveRecord::Base.table_name_suffix = "_s"
@@ -36,11 +34,11 @@ class PostgresqlExtensionMigrationTest < ActiveRecord::PostgreSQLTestCase
end
def teardown
- ActiveRecord::Base.table_name_prefix = @old_tabel_name_prefix
- ActiveRecord::Base.table_name_suffix = @old_tabel_name_suffix
+ ActiveRecord::Base.table_name_prefix = @old_table_name_prefix
+ ActiveRecord::Base.table_name_suffix = @old_table_name_suffix
ActiveRecord::SchemaMigration.delete_all rescue nil
ActiveRecord::Migration.verbose = true
- ActiveRecord::SchemaMigration.table_name = @old_schema_migration_tabel_name
+ ActiveRecord::SchemaMigration.table_name = @old_schema_migration_table_name
super
end
diff --git a/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb b/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb
new file mode 100644
index 0000000000..4fa315ad23
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/professor"
+
+if ActiveRecord::Base.connection.supports_foreign_tables?
+ class ForeignTableTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
+
+ class ForeignProfessor < ActiveRecord::Base
+ self.table_name = "foreign_professors"
+ end
+
+ class ForeignProfessorWithPk < ForeignProfessor
+ self.primary_key = "id"
+ end
+
+ def setup
+ @professor = Professor.create(name: "Nicola")
+
+ @connection = ActiveRecord::Base.connection
+ enable_extension!("postgres_fdw", @connection)
+
+ foreign_db_config = ARTest.connection_config["arunit2"]
+ @connection.execute <<-SQL
+ CREATE SERVER foreign_server
+ FOREIGN DATA WRAPPER postgres_fdw
+ OPTIONS (dbname '#{foreign_db_config["database"]}')
+ SQL
+
+ @connection.execute <<-SQL
+ CREATE USER MAPPING FOR CURRENT_USER
+ SERVER foreign_server
+ SQL
+
+ @connection.execute <<-SQL
+ CREATE FOREIGN TABLE foreign_professors (
+ id int,
+ name character varying NOT NULL
+ ) SERVER foreign_server OPTIONS (
+ table_name 'professors'
+ )
+ SQL
+ end
+
+ def teardown
+ disable_extension!("postgres_fdw", @connection)
+ @connection.execute <<-SQL
+ DROP SERVER IF EXISTS foreign_server CASCADE
+ SQL
+ end
+
+ def test_table_exists
+ table_name = ForeignProfessor.table_name
+ assert_not ActiveRecord::Base.connection.table_exists?(table_name)
+ end
+
+ def test_foreign_tables_are_valid_data_sources
+ table_name = ForeignProfessor.table_name
+ assert @connection.data_source_exists?(table_name), "'#{table_name}' should be a data source"
+ end
+
+ def test_foreign_tables
+ assert_equal ["foreign_professors"], @connection.foreign_tables
+ end
+
+ def test_foreign_table_exists
+ assert @connection.foreign_table_exists?("foreign_professors")
+ assert @connection.foreign_table_exists?(:foreign_professors)
+ assert_not @connection.foreign_table_exists?("nonexistingtable")
+ assert_not @connection.foreign_table_exists?("'")
+ assert_not @connection.foreign_table_exists?(nil)
+ end
+
+ def test_attribute_names
+ assert_equal ["id", "name"], ForeignProfessor.attribute_names
+ end
+
+ def test_attributes
+ professor = ForeignProfessorWithPk.find(@professor.id)
+ assert_equal @professor.attributes, professor.attributes
+ end
+
+ def test_does_not_have_a_primary_key
+ assert_nil ForeignProfessor.primary_key
+ end
+
+ def test_insert_record
+ # Explicit `id` here to avoid complex configurations to implicitly work with remote table
+ ForeignProfessorWithPk.create!(id: 100, name: "Leonardo")
+
+ professor = ForeignProfessorWithPk.last
+ assert_equal "Leonardo", professor.name
+ end
+
+ def test_update_record
+ professor = ForeignProfessorWithPk.find(@professor.id)
+ professor.name = "Albert"
+ professor.save!
+ professor.reload
+ assert_equal "Albert", professor.name
+ end
+
+ def test_delete_record
+ professor = ForeignProfessorWithPk.find(@professor.id)
+ assert_difference("ForeignProfessor.count", -1) { professor.destroy }
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/full_text_test.rb b/activerecord/test/cases/adapters/postgresql/full_text_test.rb
index bde7513339..95dee3bf44 100644
--- a/activerecord/test/cases/adapters/postgresql/full_text_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/full_text_test.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'support/schema_dumping_helper'
+require "support/schema_dumping_helper"
class PostgresqlFullTextTest < ActiveRecord::PostgreSQLTestCase
include SchemaDumpingHelper
@@ -7,23 +9,23 @@ class PostgresqlFullTextTest < ActiveRecord::PostgreSQLTestCase
setup do
@connection = ActiveRecord::Base.connection
- @connection.create_table('tsvectors') do |t|
- t.tsvector 'text_vector'
+ @connection.create_table("tsvectors") do |t|
+ t.tsvector "text_vector"
end
end
teardown do
- @connection.drop_table 'tsvectors', if_exists: true
+ @connection.drop_table "tsvectors", if_exists: true
end
def test_tsvector_column
column = Tsvector.columns_hash["text_vector"]
assert_equal :tsvector, column.type
assert_equal "tsvector", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = Tsvector.type_for_attribute("text_vector")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_update_tsvector
diff --git a/activerecord/test/cases/adapters/postgresql/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb
index 0baf985654..8c6f046553 100644
--- a/activerecord/test/cases/adapters/postgresql/geometric_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb
@@ -1,16 +1,18 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'support/connection_helper'
-require 'support/schema_dumping_helper'
+require "support/connection_helper"
+require "support/schema_dumping_helper"
class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase
include ConnectionHelper
include SchemaDumpingHelper
class PostgresqlPoint < ActiveRecord::Base
- attribute :x, :rails_5_1_point
- attribute :y, :rails_5_1_point
- attribute :z, :rails_5_1_point
- attribute :array_of_points, :rails_5_1_point, array: true
+ attribute :x, :point
+ attribute :y, :point
+ attribute :z, :point
+ attribute :array_of_points, :point, array: true
attribute :legacy_x, :legacy_point
attribute :legacy_y, :legacy_point
attribute :legacy_z, :legacy_point
@@ -18,7 +20,7 @@ class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase
def setup
@connection = ActiveRecord::Base.connection
- @connection.create_table('postgresql_points') do |t|
+ @connection.create_table("postgresql_points") do |t|
t.point :x
t.point :y, default: [12.2, 13.3]
t.point :z, default: "(14.4,15.5)"
@@ -27,39 +29,27 @@ class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase
t.point :legacy_y, default: [12.2, 13.3]
t.point :legacy_z, default: "(14.4,15.5)"
end
- @connection.create_table('deprecated_points') do |t|
- t.point :x
- end
end
teardown do
- @connection.drop_table 'postgresql_points', if_exists: true
- @connection.drop_table 'deprecated_points', if_exists: true
- end
-
- class DeprecatedPoint < ActiveRecord::Base; end
-
- def test_deprecated_legacy_type
- assert_deprecated do
- DeprecatedPoint.new
- end
+ @connection.drop_table "postgresql_points", if_exists: true
end
def test_column
column = PostgresqlPoint.columns_hash["x"]
assert_equal :point, column.type
assert_equal "point", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = PostgresqlPoint.type_for_attribute("x")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_default
- assert_equal ActiveRecord::Point.new(12.2, 13.3), PostgresqlPoint.column_defaults['y']
+ assert_equal ActiveRecord::Point.new(12.2, 13.3), PostgresqlPoint.column_defaults["y"]
assert_equal ActiveRecord::Point.new(12.2, 13.3), PostgresqlPoint.new.y
- assert_equal ActiveRecord::Point.new(14.4, 15.5), PostgresqlPoint.column_defaults['z']
+ assert_equal ActiveRecord::Point.new(14.4, 15.5), PostgresqlPoint.column_defaults["z"]
assert_equal ActiveRecord::Point.new(14.4, 15.5), PostgresqlPoint.new.z
end
@@ -89,7 +79,7 @@ class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase
p.reload
assert_equal ActiveRecord::Point.new(10.0, 25.0), p.x
- assert_not p.changed?
+ assert_not_predicate p, :changed?
end
def test_array_assignment
@@ -104,6 +94,11 @@ class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase
assert_equal ActiveRecord::Point.new(1, 2), p.x
end
+ def test_empty_string_assignment
+ p = PostgresqlPoint.new(x: "")
+ assert_nil p.x
+ end
+
def test_array_of_points_round_trip
expected_value = [
ActiveRecord::Point.new(1, 2),
@@ -122,17 +117,17 @@ class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase
column = PostgresqlPoint.columns_hash["legacy_x"]
assert_equal :point, column.type
assert_equal "point", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = PostgresqlPoint.type_for_attribute("legacy_x")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_legacy_default
- assert_equal [12.2, 13.3], PostgresqlPoint.column_defaults['legacy_y']
+ assert_equal [12.2, 13.3], PostgresqlPoint.column_defaults["legacy_y"]
assert_equal [12.2, 13.3], PostgresqlPoint.new.legacy_y
- assert_equal [14.4, 15.5], PostgresqlPoint.column_defaults['legacy_z']
+ assert_equal [14.4, 15.5], PostgresqlPoint.column_defaults["legacy_z"]
assert_equal [14.4, 15.5], PostgresqlPoint.new.legacy_z
end
@@ -162,70 +157,72 @@ class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase
p.reload
assert_equal [10.0, 25.0], p.legacy_x
- assert_not p.changed?
+ assert_not_predicate p, :changed?
end
end
class PostgresqlGeometricTest < ActiveRecord::PostgreSQLTestCase
+ include SchemaDumpingHelper
+
class PostgresqlGeometric < ActiveRecord::Base; end
setup do
@connection = ActiveRecord::Base.connection
@connection.create_table("postgresql_geometrics") do |t|
- t.column :a_line_segment, :lseg
- t.column :a_box, :box
- t.column :a_path, :path
- t.column :a_polygon, :polygon
- t.column :a_circle, :circle
+ t.lseg :a_line_segment
+ t.box :a_box
+ t.path :a_path
+ t.polygon :a_polygon
+ t.circle :a_circle
end
end
teardown do
- @connection.drop_table 'postgresql_geometrics', if_exists: true
+ @connection.drop_table "postgresql_geometrics", if_exists: true
end
def test_geometric_types
g = PostgresqlGeometric.new(
- :a_line_segment => '(2.0, 3), (5.5, 7.0)',
- :a_box => '2.0, 3, 5.5, 7.0',
- :a_path => '[(2.0, 3), (5.5, 7.0), (8.5, 11.0)]',
- :a_polygon => '((2.0, 3), (5.5, 7.0), (8.5, 11.0))',
- :a_circle => '<(5.3, 10.4), 2>'
+ a_line_segment: "(2.0, 3), (5.5, 7.0)",
+ a_box: "2.0, 3, 5.5, 7.0",
+ a_path: "[(2.0, 3), (5.5, 7.0), (8.5, 11.0)]",
+ a_polygon: "((2.0, 3), (5.5, 7.0), (8.5, 11.0))",
+ a_circle: "<(5.3, 10.4), 2>"
)
g.save!
h = PostgresqlGeometric.find(g.id)
- assert_equal '[(2,3),(5.5,7)]', h.a_line_segment
- assert_equal '(5.5,7),(2,3)', h.a_box # reordered to store upper right corner then bottom left corner
- assert_equal '[(2,3),(5.5,7),(8.5,11)]', h.a_path
- assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_polygon
- assert_equal '<(5.3,10.4),2>', h.a_circle
+ assert_equal "[(2,3),(5.5,7)]", h.a_line_segment
+ assert_equal "(5.5,7),(2,3)", h.a_box # reordered to store upper right corner then bottom left corner
+ assert_equal "[(2,3),(5.5,7),(8.5,11)]", h.a_path
+ assert_equal "((2,3),(5.5,7),(8.5,11))", h.a_polygon
+ assert_equal "<(5.3,10.4),2>", h.a_circle
end
def test_alternative_format
g = PostgresqlGeometric.new(
- :a_line_segment => '((2.0, 3), (5.5, 7.0))',
- :a_box => '(2.0, 3), (5.5, 7.0)',
- :a_path => '((2.0, 3), (5.5, 7.0), (8.5, 11.0))',
- :a_polygon => '2.0, 3, 5.5, 7.0, 8.5, 11.0',
- :a_circle => '((5.3, 10.4), 2)'
+ a_line_segment: "((2.0, 3), (5.5, 7.0))",
+ a_box: "(2.0, 3), (5.5, 7.0)",
+ a_path: "((2.0, 3), (5.5, 7.0), (8.5, 11.0))",
+ a_polygon: "2.0, 3, 5.5, 7.0, 8.5, 11.0",
+ a_circle: "((5.3, 10.4), 2)"
)
g.save!
h = PostgresqlGeometric.find(g.id)
- assert_equal '[(2,3),(5.5,7)]', h.a_line_segment
- assert_equal '(5.5,7),(2,3)', h.a_box # reordered to store upper right corner then bottom left corner
- assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_path
- assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_polygon
- assert_equal '<(5.3,10.4),2>', h.a_circle
+ assert_equal "[(2,3),(5.5,7)]", h.a_line_segment
+ assert_equal "(5.5,7),(2,3)", h.a_box # reordered to store upper right corner then bottom left corner
+ assert_equal "((2,3),(5.5,7),(8.5,11))", h.a_path
+ assert_equal "((2,3),(5.5,7),(8.5,11))", h.a_polygon
+ assert_equal "<(5.3,10.4),2>", h.a_circle
end
def test_geometric_function
- PostgresqlGeometric.create! a_path: '[(2.0, 3), (5.5, 7.0), (8.5, 11.0)]' # [ ] is an open path
- PostgresqlGeometric.create! a_path: '((2.0, 3), (5.5, 7.0), (8.5, 11.0))' # ( ) is a closed path
+ PostgresqlGeometric.create! a_path: "[(2.0, 3), (5.5, 7.0), (8.5, 11.0)]" # [ ] is an open path
+ PostgresqlGeometric.create! a_path: "((2.0, 3), (5.5, 7.0), (8.5, 11.0))" # ( ) is a closed path
objs = PostgresqlGeometric.find_by_sql "SELECT isopen(a_path) FROM postgresql_geometrics ORDER BY id ASC"
assert_equal [true, false], objs.map(&:isopen)
@@ -233,4 +230,144 @@ class PostgresqlGeometricTest < ActiveRecord::PostgreSQLTestCase
objs = PostgresqlGeometric.find_by_sql "SELECT isclosed(a_path) FROM postgresql_geometrics ORDER BY id ASC"
assert_equal [false, true], objs.map(&:isclosed)
end
+
+ def test_schema_dumping
+ output = dump_table_schema("postgresql_geometrics")
+ assert_match %r{t\.lseg\s+"a_line_segment"$}, output
+ assert_match %r{t\.box\s+"a_box"$}, output
+ assert_match %r{t\.path\s+"a_path"$}, output
+ assert_match %r{t\.polygon\s+"a_polygon"$}, output
+ assert_match %r{t\.circle\s+"a_circle"$}, output
+ end
+end
+
+class PostgreSQLGeometricLineTest < ActiveRecord::PostgreSQLTestCase
+ include SchemaDumpingHelper
+
+ class PostgresqlLine < ActiveRecord::Base; end
+
+ setup do
+ unless ActiveRecord::Base.connection.send(:postgresql_version) >= 90400
+ skip("line type is not fully implemented")
+ end
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table("postgresql_lines") do |t|
+ t.line :a_line
+ end
+ end
+
+ teardown do
+ if defined?(@connection)
+ @connection.drop_table "postgresql_lines", if_exists: true
+ end
+ end
+
+ def test_geometric_line_type
+ g = PostgresqlLine.new(
+ a_line: "{2.0, 3, 5.5}"
+ )
+ g.save!
+
+ h = PostgresqlLine.find(g.id)
+ assert_equal "{2,3,5.5}", h.a_line
+ end
+
+ def test_alternative_format_line_type
+ g = PostgresqlLine.new(
+ a_line: "(2.0, 3), (4.0, 6.0)"
+ )
+ g.save!
+
+ h = PostgresqlLine.find(g.id)
+ assert_equal "{1.5,-1,0}", h.a_line
+ end
+
+ def test_schema_dumping_for_line_type
+ output = dump_table_schema("postgresql_lines")
+ assert_match %r{t\.line\s+"a_line"$}, output
+ end
+end
+
+class PostgreSQLGeometricTypesTest < ActiveRecord::PostgreSQLTestCase
+ attr_reader :connection, :table_name
+
+ def setup
+ super
+ @connection = ActiveRecord::Base.connection
+ @table_name = :testings
+ end
+
+ def test_creating_column_with_point_type
+ connection.create_table(table_name) do |t|
+ t.point :foo_point
+ end
+
+ assert_column_exists(:foo_point)
+ assert_type_correct(:foo_point, :point)
+ end
+
+ def test_creating_column_with_line_type
+ connection.create_table(table_name) do |t|
+ t.line :foo_line
+ end
+
+ assert_column_exists(:foo_line)
+ assert_type_correct(:foo_line, :line)
+ end
+
+ def test_creating_column_with_lseg_type
+ connection.create_table(table_name) do |t|
+ t.lseg :foo_lseg
+ end
+
+ assert_column_exists(:foo_lseg)
+ assert_type_correct(:foo_lseg, :lseg)
+ end
+
+ def test_creating_column_with_box_type
+ connection.create_table(table_name) do |t|
+ t.box :foo_box
+ end
+
+ assert_column_exists(:foo_box)
+ assert_type_correct(:foo_box, :box)
+ end
+
+ def test_creating_column_with_path_type
+ connection.create_table(table_name) do |t|
+ t.path :foo_path
+ end
+
+ assert_column_exists(:foo_path)
+ assert_type_correct(:foo_path, :path)
+ end
+
+ def test_creating_column_with_polygon_type
+ connection.create_table(table_name) do |t|
+ t.polygon :foo_polygon
+ end
+
+ assert_column_exists(:foo_polygon)
+ assert_type_correct(:foo_polygon, :polygon)
+ end
+
+ def test_creating_column_with_circle_type
+ connection.create_table(table_name) do |t|
+ t.circle :foo_circle
+ end
+
+ assert_column_exists(:foo_circle)
+ assert_type_correct(:foo_circle, :circle)
+ end
+
+ private
+
+ def assert_column_exists(column_name)
+ assert connection.column_exists?(table_name, column_name)
+ end
+
+ def assert_type_correct(column_name, type)
+ column = connection.columns(table_name).find { |c| c.name == column_name.to_s }
+ assert_equal type, column.type
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
index 6a2d501646..4b061a9375 100644
--- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
@@ -1,327 +1,353 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'support/schema_dumping_helper'
+require "support/schema_dumping_helper"
-if ActiveRecord::Base.connection.supports_extensions?
- class PostgresqlHstoreTest < ActiveRecord::PostgreSQLTestCase
- include SchemaDumpingHelper
- class Hstore < ActiveRecord::Base
- self.table_name = 'hstores'
+class PostgresqlHstoreTest < ActiveRecord::PostgreSQLTestCase
+ include SchemaDumpingHelper
+ class Hstore < ActiveRecord::Base
+ self.table_name = "hstores"
- store_accessor :settings, :language, :timezone
- end
+ store_accessor :settings, :language, :timezone
+ end
- def setup
- @connection = ActiveRecord::Base.connection
+ class FakeParameters
+ def to_unsafe_h
+ { "hi" => "hi" }
+ end
+ end
- unless @connection.extension_enabled?('hstore')
- @connection.enable_extension 'hstore'
- @connection.commit_db_transaction
- end
+ def setup
+ @connection = ActiveRecord::Base.connection
- @connection.reconnect!
+ enable_extension!("hstore", @connection)
- @connection.transaction do
- @connection.create_table('hstores') do |t|
- t.hstore 'tags', :default => ''
- t.hstore 'payload', array: true
- t.hstore 'settings'
- end
+ @connection.transaction do
+ @connection.create_table("hstores") do |t|
+ t.hstore "tags", default: ""
+ t.hstore "payload", array: true
+ t.hstore "settings"
end
- Hstore.reset_column_information
- @column = Hstore.columns_hash['tags']
- @type = Hstore.type_for_attribute("tags")
- end
-
- teardown do
- @connection.drop_table 'hstores', if_exists: true
end
+ Hstore.reset_column_information
+ @column = Hstore.columns_hash["tags"]
+ @type = Hstore.type_for_attribute("tags")
+ end
- def test_hstore_included_in_extensions
- assert @connection.respond_to?(:extensions), "connection should have a list of extensions"
- assert @connection.extensions.include?('hstore'), "extension list should include hstore"
- end
+ teardown do
+ @connection.drop_table "hstores", if_exists: true
+ disable_extension!("hstore", @connection)
+ end
- def test_disable_enable_hstore
- assert @connection.extension_enabled?('hstore')
- @connection.disable_extension 'hstore'
- assert_not @connection.extension_enabled?('hstore')
- @connection.enable_extension 'hstore'
- assert @connection.extension_enabled?('hstore')
- ensure
- # Restore column(s) dropped by `drop extension hstore cascade;`
- load_schema
- end
+ def test_hstore_included_in_extensions
+ assert_respond_to @connection, :extensions
+ assert_includes @connection.extensions, "hstore", "extension list should include hstore"
+ end
- def test_column
- assert_equal :hstore, @column.type
- assert_equal "hstore", @column.sql_type
- assert_not @column.array?
+ def test_disable_enable_hstore
+ assert @connection.extension_enabled?("hstore")
+ @connection.disable_extension "hstore"
+ assert_not @connection.extension_enabled?("hstore")
+ @connection.enable_extension "hstore"
+ assert @connection.extension_enabled?("hstore")
+ ensure
+ # Restore column(s) dropped by `drop extension hstore cascade;`
+ load_schema
+ end
- assert_not @type.binary?
- end
+ def test_column
+ assert_equal :hstore, @column.type
+ assert_equal "hstore", @column.sql_type
+ assert_not_predicate @column, :array?
- def test_default
- @connection.add_column 'hstores', 'permissions', :hstore, default: '"users"=>"read", "articles"=>"write"'
- Hstore.reset_column_information
+ assert_not_predicate @type, :binary?
+ end
- assert_equal({"users"=>"read", "articles"=>"write"}, Hstore.column_defaults['permissions'])
- assert_equal({"users"=>"read", "articles"=>"write"}, Hstore.new.permissions)
- ensure
- Hstore.reset_column_information
- end
+ def test_default
+ @connection.add_column "hstores", "permissions", :hstore, default: '"users"=>"read", "articles"=>"write"'
+ Hstore.reset_column_information
- def test_change_table_supports_hstore
- @connection.transaction do
- @connection.change_table('hstores') do |t|
- t.hstore 'users', default: ''
- end
- Hstore.reset_column_information
- column = Hstore.columns_hash['users']
- assert_equal :hstore, column.type
+ assert_equal({ "users" => "read", "articles" => "write" }, Hstore.column_defaults["permissions"])
+ assert_equal({ "users" => "read", "articles" => "write" }, Hstore.new.permissions)
+ ensure
+ Hstore.reset_column_information
+ end
- raise ActiveRecord::Rollback # reset the schema change
+ def test_change_table_supports_hstore
+ @connection.transaction do
+ @connection.change_table("hstores") do |t|
+ t.hstore "users", default: ""
end
- ensure
Hstore.reset_column_information
+ column = Hstore.columns_hash["users"]
+ assert_equal :hstore, column.type
+
+ raise ActiveRecord::Rollback # reset the schema change
end
+ ensure
+ Hstore.reset_column_information
+ end
- def test_hstore_migration
- hstore_migration = Class.new(ActiveRecord::Migration) do
- def change
- change_table("hstores") do |t|
- t.hstore :keys
- end
+ def test_hstore_migration
+ hstore_migration = Class.new(ActiveRecord::Migration::Current) do
+ def change
+ change_table("hstores") do |t|
+ t.hstore :keys
end
end
-
- hstore_migration.new.suppress_messages do
- hstore_migration.migrate(:up)
- assert_includes @connection.columns(:hstores).map(&:name), "keys"
- hstore_migration.migrate(:down)
- assert_not_includes @connection.columns(:hstores).map(&:name), "keys"
- end
end
- def test_cast_value_on_write
- x = Hstore.new tags: {"bool" => true, "number" => 5}
- assert_equal({"bool" => true, "number" => 5}, x.tags_before_type_cast)
- assert_equal({"bool" => "true", "number" => "5"}, x.tags)
- x.save
- assert_equal({"bool" => "true", "number" => "5"}, x.reload.tags)
+ hstore_migration.new.suppress_messages do
+ hstore_migration.migrate(:up)
+ assert_includes @connection.columns(:hstores).map(&:name), "keys"
+ hstore_migration.migrate(:down)
+ assert_not_includes @connection.columns(:hstores).map(&:name), "keys"
end
+ end
- def test_type_cast_hstore
- assert_equal({'1' => '2'}, @type.deserialize("\"1\"=>\"2\""))
- assert_equal({}, @type.deserialize(""))
- assert_equal({'key'=>nil}, @type.deserialize('key => NULL'))
- assert_equal({'c'=>'}','"a"'=>'b "a b'}, @type.deserialize(%q(c=>"}", "\"a\""=>"b \"a b")))
- end
+ def test_cast_value_on_write
+ x = Hstore.new tags: { "bool" => true, "number" => 5 }
+ assert_equal({ "bool" => true, "number" => 5 }, x.tags_before_type_cast)
+ assert_equal({ "bool" => "true", "number" => "5" }, x.tags)
+ x.save
+ assert_equal({ "bool" => "true", "number" => "5" }, x.reload.tags)
+ end
- def test_with_store_accessors
- x = Hstore.new(language: "fr", timezone: "GMT")
- assert_equal "fr", x.language
- assert_equal "GMT", x.timezone
+ def test_type_cast_hstore
+ assert_equal({ "1" => "2" }, @type.deserialize("\"1\"=>\"2\""))
+ assert_equal({}, @type.deserialize(""))
+ assert_equal({ "key" => nil }, @type.deserialize("key => NULL"))
+ assert_equal({ "c" => "}", '"a"' => 'b "a b' }, @type.deserialize(%q(c=>"}", "\"a\""=>"b \"a b")))
+ end
- x.save!
- x = Hstore.first
- assert_equal "fr", x.language
- assert_equal "GMT", x.timezone
+ def test_with_store_accessors
+ x = Hstore.new(language: "fr", timezone: "GMT")
+ assert_equal "fr", x.language
+ assert_equal "GMT", x.timezone
- x.language = "de"
- x.save!
+ x.save!
+ x = Hstore.first
+ assert_equal "fr", x.language
+ assert_equal "GMT", x.timezone
- x = Hstore.first
- assert_equal "de", x.language
- assert_equal "GMT", x.timezone
- end
+ x.language = "de"
+ x.save!
- def test_duplication_with_store_accessors
- x = Hstore.new(language: "fr", timezone: "GMT")
- assert_equal "fr", x.language
- assert_equal "GMT", x.timezone
+ x = Hstore.first
+ assert_equal "de", x.language
+ assert_equal "GMT", x.timezone
+ end
- y = x.dup
- assert_equal "fr", y.language
- assert_equal "GMT", y.timezone
- end
+ def test_duplication_with_store_accessors
+ x = Hstore.new(language: "fr", timezone: "GMT")
+ assert_equal "fr", x.language
+ assert_equal "GMT", x.timezone
- def test_yaml_round_trip_with_store_accessors
- x = Hstore.new(language: "fr", timezone: "GMT")
- assert_equal "fr", x.language
- assert_equal "GMT", x.timezone
+ y = x.dup
+ assert_equal "fr", y.language
+ assert_equal "GMT", y.timezone
+ end
- y = YAML.load(YAML.dump(x))
- assert_equal "fr", y.language
- assert_equal "GMT", y.timezone
- end
+ def test_yaml_round_trip_with_store_accessors
+ x = Hstore.new(language: "fr", timezone: "GMT")
+ assert_equal "fr", x.language
+ assert_equal "GMT", x.timezone
- def test_changes_in_place
- hstore = Hstore.create!(settings: { 'one' => 'two' })
- hstore.settings['three'] = 'four'
- hstore.save!
- hstore.reload
+ y = YAML.load(YAML.dump(x))
+ assert_equal "fr", y.language
+ assert_equal "GMT", y.timezone
+ end
- assert_equal 'four', hstore.settings['three']
- assert_not hstore.changed?
- end
+ def test_changes_in_place
+ hstore = Hstore.create!(settings: { "one" => "two" })
+ hstore.settings["three"] = "four"
+ hstore.save!
+ hstore.reload
- def test_gen1
- assert_equal(%q(" "=>""), @type.serialize({' '=>''}))
- end
+ assert_equal "four", hstore.settings["three"]
+ assert_not_predicate hstore, :changed?
+ end
- def test_gen2
- assert_equal(%q(","=>""), @type.serialize({','=>''}))
- end
+ def test_dirty_from_user_equal
+ settings = { "alongkey" => "anything", "key" => "value" }
+ hstore = Hstore.create!(settings: settings)
- def test_gen3
- assert_equal(%q("="=>""), @type.serialize({'='=>''}))
- end
+ hstore.settings = { "key" => "value", "alongkey" => "anything" }
+ assert_equal settings, hstore.settings
+ assert_not_predicate hstore, :changed?
+ end
- def test_gen4
- assert_equal(%q(">"=>""), @type.serialize({'>'=>''}))
- end
+ def test_hstore_dirty_from_database_equal
+ settings = { "alongkey" => "anything", "key" => "value" }
+ hstore = Hstore.create!(settings: settings)
+ hstore.reload
- def test_parse1
- assert_equal({'a'=>nil,'b'=>nil,'c'=>'NuLl','null'=>'c'}, @type.deserialize('a=>null,b=>NuLl,c=>"NuLl",null=>c'))
- end
+ assert_equal settings, hstore.settings
+ hstore.settings = settings
+ assert_not_predicate hstore, :changed?
+ end
- def test_parse2
- assert_equal({" " => " "}, @type.deserialize("\\ =>\\ "))
- end
+ def test_gen1
+ assert_equal('" "=>""', @type.serialize(" " => ""))
+ end
- def test_parse3
- assert_equal({"=" => ">"}, @type.deserialize("==>>"))
- end
+ def test_gen2
+ assert_equal('","=>""', @type.serialize("," => ""))
+ end
- def test_parse4
- assert_equal({"=a"=>"q=w"}, @type.deserialize('\=a=>q=w'))
- end
+ def test_gen3
+ assert_equal('"="=>""', @type.serialize("=" => ""))
+ end
- def test_parse5
- assert_equal({"=a"=>"q=w"}, @type.deserialize('"=a"=>q\=w'))
- end
+ def test_gen4
+ assert_equal('">"=>""', @type.serialize(">" => ""))
+ end
- def test_parse6
- assert_equal({"\"a"=>"q>w"}, @type.deserialize('"\"a"=>q>w'))
- end
+ def test_parse1
+ assert_equal({ "a" => nil, "b" => nil, "c" => "NuLl", "null" => "c" }, @type.deserialize('a=>null,b=>NuLl,c=>"NuLl",null=>c'))
+ end
- def test_parse7
- assert_equal({"\"a"=>"q\"w"}, @type.deserialize('\"a=>q"w'))
- end
+ def test_parse2
+ assert_equal({ " " => " " }, @type.deserialize("\\ =>\\ "))
+ end
- def test_rewrite
- @connection.execute "insert into hstores (tags) VALUES ('1=>2')"
- x = Hstore.first
- x.tags = { '"a\'' => 'b' }
- assert x.save!
- end
+ def test_parse3
+ assert_equal({ "=" => ">" }, @type.deserialize("==>>"))
+ end
- def test_select
- @connection.execute "insert into hstores (tags) VALUES ('1=>2')"
- x = Hstore.first
- assert_equal({'1' => '2'}, x.tags)
- end
+ def test_parse4
+ assert_equal({ "=a" => "q=w" }, @type.deserialize('\=a=>q=w'))
+ end
- def test_array_cycle
- assert_array_cycle([{"AA" => "BB", "CC" => "DD"}, {"AA" => nil}])
- end
+ def test_parse5
+ assert_equal({ "=a" => "q=w" }, @type.deserialize('"=a"=>q\=w'))
+ end
- def test_array_strings_with_quotes
- assert_array_cycle([{'this has' => 'some "s that need to be escaped"'}])
- end
+ def test_parse6
+ assert_equal({ "\"a" => "q>w" }, @type.deserialize('"\"a"=>q>w'))
+ end
- def test_array_strings_with_commas
- assert_array_cycle([{'this,has' => 'many,values'}])
- end
+ def test_parse7
+ assert_equal({ "\"a" => "q\"w" }, @type.deserialize('\"a=>q"w'))
+ end
- def test_array_strings_with_array_delimiters
- assert_array_cycle(['{' => '}'])
- end
+ def test_rewrite
+ @connection.execute "insert into hstores (tags) VALUES ('1=>2')"
+ x = Hstore.first
+ x.tags = { '"a\'' => "b" }
+ assert x.save!
+ end
- def test_array_strings_with_null_strings
- assert_array_cycle([{'NULL' => 'NULL'}])
- end
+ def test_select
+ @connection.execute "insert into hstores (tags) VALUES ('1=>2')"
+ x = Hstore.first
+ assert_equal({ "1" => "2" }, x.tags)
+ end
- def test_contains_nils
- assert_array_cycle([{'NULL' => nil}])
- end
+ def test_array_cycle
+ assert_array_cycle([{ "AA" => "BB", "CC" => "DD" }, { "AA" => nil }])
+ end
- def test_select_multikey
- @connection.execute "insert into hstores (tags) VALUES ('1=>2,2=>3')"
- x = Hstore.first
- assert_equal({'1' => '2', '2' => '3'}, x.tags)
- end
+ def test_array_strings_with_quotes
+ assert_array_cycle([{ "this has" => 'some "s that need to be escaped"' }])
+ end
- def test_create
- assert_cycle('a' => 'b', '1' => '2')
- end
+ def test_array_strings_with_commas
+ assert_array_cycle([{ "this,has" => "many,values" }])
+ end
- def test_nil
- assert_cycle('a' => nil)
- end
+ def test_array_strings_with_array_delimiters
+ assert_array_cycle(["{" => "}"])
+ end
- def test_quotes
- assert_cycle('a' => 'b"ar', '1"foo' => '2')
- end
+ def test_array_strings_with_null_strings
+ assert_array_cycle([{ "NULL" => "NULL" }])
+ end
- def test_whitespace
- assert_cycle('a b' => 'b ar', '1"foo' => '2')
- end
+ def test_contains_nils
+ assert_array_cycle([{ "NULL" => nil }])
+ end
- def test_backslash
- assert_cycle('a\\b' => 'b\\ar', '1"foo' => '2')
- end
+ def test_select_multikey
+ @connection.execute "insert into hstores (tags) VALUES ('1=>2,2=>3')"
+ x = Hstore.first
+ assert_equal({ "1" => "2", "2" => "3" }, x.tags)
+ end
- def test_comma
- assert_cycle('a, b' => 'bar', '1"foo' => '2')
- end
+ def test_create
+ assert_cycle("a" => "b", "1" => "2")
+ end
- def test_arrow
- assert_cycle('a=>b' => 'bar', '1"foo' => '2')
- end
+ def test_nil
+ assert_cycle("a" => nil)
+ end
- def test_quoting_special_characters
- assert_cycle('ca' => 'cà', 'ac' => 'àc')
- end
+ def test_quotes
+ assert_cycle("a" => 'b"ar', '1"foo' => "2")
+ end
- def test_multiline
- assert_cycle("a\nb" => "c\nd")
- end
+ def test_whitespace
+ assert_cycle("a b" => "b ar", '1"foo' => "2")
+ end
- class TagCollection
- def initialize(hash); @hash = hash end
- def to_hash; @hash end
- def self.load(hash); new(hash) end
- def self.dump(object); object.to_hash end
- end
+ def test_backslash
+ assert_cycle('a\\b' => 'b\\ar', '1"foo' => "2")
+ end
- class HstoreWithSerialize < Hstore
- serialize :tags, TagCollection
- end
+ def test_comma
+ assert_cycle("a, b" => "bar", '1"foo' => "2")
+ end
- def test_hstore_with_serialized_attributes
- HstoreWithSerialize.create! tags: TagCollection.new({"one" => "two"})
- record = HstoreWithSerialize.first
- assert_instance_of TagCollection, record.tags
- assert_equal({"one" => "two"}, record.tags.to_hash)
- record.tags = TagCollection.new("three" => "four")
- record.save!
- assert_equal({"three" => "four"}, HstoreWithSerialize.first.tags.to_hash)
- end
+ def test_arrow
+ assert_cycle("a=>b" => "bar", '1"foo' => "2")
+ end
- def test_clone_hstore_with_serialized_attributes
- HstoreWithSerialize.create! tags: TagCollection.new({"one" => "two"})
- record = HstoreWithSerialize.first
- dupe = record.dup
- assert_equal({"one" => "two"}, dupe.tags.to_hash)
- end
+ def test_quoting_special_characters
+ assert_cycle("ca" => "cà", "ac" => "àc")
+ end
- def test_schema_dump_with_shorthand
- output = dump_table_schema("hstores")
- assert_match %r[t\.hstore "tags",\s+default: {}], output
- end
+ def test_multiline
+ assert_cycle("a\nb" => "c\nd")
+ end
+
+ class TagCollection
+ def initialize(hash); @hash = hash end
+ def to_hash; @hash end
+ def self.load(hash); new(hash) end
+ def self.dump(object); object.to_hash end
+ end
+
+ class HstoreWithSerialize < Hstore
+ serialize :tags, TagCollection
+ end
+
+ def test_hstore_with_serialized_attributes
+ HstoreWithSerialize.create! tags: TagCollection.new("one" => "two")
+ record = HstoreWithSerialize.first
+ assert_instance_of TagCollection, record.tags
+ assert_equal({ "one" => "two" }, record.tags.to_hash)
+ record.tags = TagCollection.new("three" => "four")
+ record.save!
+ assert_equal({ "three" => "four" }, HstoreWithSerialize.first.tags.to_hash)
+ end
- private
+ def test_clone_hstore_with_serialized_attributes
+ HstoreWithSerialize.create! tags: TagCollection.new("one" => "two")
+ record = HstoreWithSerialize.first
+ dupe = record.dup
+ assert_equal({ "one" => "two" }, dupe.tags.to_hash)
+ end
+
+ def test_schema_dump_with_shorthand
+ output = dump_table_schema("hstores")
+ assert_match %r[t\.hstore "tags",\s+default: {}], output
+ end
+
+ def test_supports_to_unsafe_h_values
+ assert_equal("\"hi\"=>\"hi\"", @type.serialize(FakeParameters.new))
+ end
+
+ private
def assert_array_cycle(array)
# test creation
x = Hstore.create!(payload: array)
@@ -338,16 +364,15 @@ if ActiveRecord::Base.connection.supports_extensions?
def assert_cycle(hash)
# test creation
- x = Hstore.create!(:tags => hash)
+ x = Hstore.create!(tags: hash)
x.reload
assert_equal(hash, x.tags)
# test updating
- x = Hstore.create!(:tags => {})
+ x = Hstore.create!(tags: {})
x.tags = hash
x.save!
x.reload
assert_equal(hash, x.tags)
end
- end
end
diff --git a/activerecord/test/cases/adapters/postgresql/infinity_test.rb b/activerecord/test/cases/adapters/postgresql/infinity_test.rb
index bfda933fa4..5e56ce8427 100644
--- a/activerecord/test/cases/adapters/postgresql/infinity_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/infinity_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
class PostgresqlInfinityTest < ActiveRecord::PostgreSQLTestCase
@@ -11,11 +13,12 @@ class PostgresqlInfinityTest < ActiveRecord::PostgreSQLTestCase
@connection.create_table(:postgresql_infinities) do |t|
t.float :float
t.datetime :datetime
+ t.date :date
end
end
teardown do
- @connection.drop_table 'postgresql_infinities', if_exists: true
+ @connection.drop_table "postgresql_infinities", if_exists: true
end
test "type casting infinity on a float column" do
@@ -25,12 +28,12 @@ class PostgresqlInfinityTest < ActiveRecord::PostgreSQLTestCase
end
test "type casting string on a float column" do
- record = PostgresqlInfinity.new(float: 'Infinity')
+ record = PostgresqlInfinity.new(float: "Infinity")
assert_equal Float::INFINITY, record.float
- record = PostgresqlInfinity.new(float: '-Infinity')
+ record = PostgresqlInfinity.new(float: "-Infinity")
assert_equal(-Float::INFINITY, record.float)
- record = PostgresqlInfinity.new(float: 'NaN')
- assert_send [record.float, :nan?]
+ record = PostgresqlInfinity.new(float: "NaN")
+ assert record.float.nan?, "Expected #{record.float} to be NaN"
end
test "update_all with infinity on a float column" do
@@ -41,11 +44,25 @@ class PostgresqlInfinityTest < ActiveRecord::PostgreSQLTestCase
end
test "type casting infinity on a datetime column" do
+ record = PostgresqlInfinity.create!(datetime: "infinity")
+ record.reload
+ assert_equal Float::INFINITY, record.datetime
+
record = PostgresqlInfinity.create!(datetime: Float::INFINITY)
record.reload
assert_equal Float::INFINITY, record.datetime
end
+ test "type casting infinity on a date column" do
+ record = PostgresqlInfinity.create!(date: "infinity")
+ record.reload
+ assert_equal Float::INFINITY, record.date
+
+ record = PostgresqlInfinity.create!(date: Float::INFINITY)
+ record.reload
+ assert_equal Float::INFINITY, record.date
+ end
+
test "update_all with infinity on a datetime column" do
record = PostgresqlInfinity.create!
PostgresqlInfinity.update_all(datetime: Float::INFINITY)
@@ -66,4 +83,28 @@ class PostgresqlInfinityTest < ActiveRecord::PostgreSQLTestCase
PostgresqlInfinity.reset_column_information
end
end
+
+ test "where clause with infinite range on a datetime column" do
+ record = PostgresqlInfinity.create!(datetime: Time.current)
+
+ string = PostgresqlInfinity.where(datetime: "-infinity".."infinity")
+ assert_equal record, string.take
+
+ infinity = PostgresqlInfinity.where(datetime: -::Float::INFINITY..::Float::INFINITY)
+ assert_equal record, infinity.take
+
+ assert_equal infinity.to_sql, string.to_sql
+ end
+
+ test "where clause with infinite range on a date column" do
+ record = PostgresqlInfinity.create!(date: Date.current)
+
+ string = PostgresqlInfinity.where(date: "-infinity".."infinity")
+ assert_equal record, string.take
+
+ infinity = PostgresqlInfinity.where(date: -::Float::INFINITY..::Float::INFINITY)
+ assert_equal record, infinity.take
+
+ assert_equal infinity.to_sql, string.to_sql
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/integer_test.rb b/activerecord/test/cases/adapters/postgresql/integer_test.rb
index b4e55964b9..3e45b057ff 100644
--- a/activerecord/test/cases/adapters/postgresql/integer_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/integer_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "active_support/core_ext/numeric/bytes"
diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb
index f242f32496..ee08841eb3 100644
--- a/activerecord/test/cases/adapters/postgresql/json_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/json_test.rb
@@ -1,190 +1,37 @@
-# -*- coding: utf-8 -*-
+# frozen_string_literal: true
+
require "cases/helper"
-require 'support/schema_dumping_helper'
+require "cases/json_shared_test_cases"
module PostgresqlJSONSharedTestCases
- include SchemaDumpingHelper
-
- class JsonDataType < ActiveRecord::Base
- self.table_name = 'json_data_type'
-
- store_accessor :settings, :resolution
- end
+ include JSONSharedTestCases
def setup
- @connection = ActiveRecord::Base.connection
- begin
- @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'
- end
- rescue ActiveRecord::StatementInvalid
- skip "do not test on PostgreSQL without #{column_type} type."
+ super
+ @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
- end
-
- def teardown
- @connection.drop_table :json_data_type, if_exists: true
- JsonDataType.reset_column_information
- end
-
- def test_column
- column = JsonDataType.columns_hash["payload"]
- assert_equal column_type, column.type
- assert_equal column_type.to_s, column.sql_type
- assert_not column.array?
-
- type = JsonDataType.type_for_attribute("payload")
- assert_not type.binary?
+ rescue ActiveRecord::StatementInvalid
+ skip "do not test on PostgreSQL without #{column_type} type."
end
def test_default
- @connection.add_column 'json_data_type', 'permissions', column_type, default: '{"users": "read", "posts": ["read", "write"]}'
- JsonDataType.reset_column_information
-
- assert_equal({"users"=>"read", "posts"=>["read", "write"]}, JsonDataType.column_defaults['permissions'])
- assert_equal({"users"=>"read", "posts"=>["read", "write"]}, JsonDataType.new.permissions)
- ensure
- JsonDataType.reset_column_information
- end
-
- def test_change_table_supports_json
- @connection.transaction do
- @connection.change_table('json_data_type') do |t|
- t.public_send column_type, 'users', default: '{}' # t.json 'users', default: '{}'
- end
- JsonDataType.reset_column_information
- column = JsonDataType.columns_hash['users']
- assert_equal column_type, column.type
-
- raise ActiveRecord::Rollback # reset the schema change
- end
- ensure
- JsonDataType.reset_column_information
- end
-
- def test_schema_dumping
- output = dump_table_schema("json_data_type")
- assert_match(/t\.#{column_type.to_s}\s+"payload",\s+default: {}/, output)
- end
-
- def test_cast_value_on_write
- x = JsonDataType.new payload: {"string" => "foo", :symbol => :bar}
- assert_equal({"string" => "foo", :symbol => :bar}, x.payload_before_type_cast)
- assert_equal({"string" => "foo", "symbol" => "bar"}, x.payload)
- x.save
- assert_equal({"string" => "foo", "symbol" => "bar"}, x.reload.payload)
- end
-
- def test_type_cast_json
- type = JsonDataType.type_for_attribute("payload")
-
- data = "{\"a_key\":\"a_value\"}"
- hash = type.deserialize(data)
- assert_equal({'a_key' => 'a_value'}, hash)
- assert_equal({'a_key' => 'a_value'}, type.deserialize(data))
-
- assert_equal({}, type.deserialize("{}"))
- assert_equal({'key'=>nil}, type.deserialize('{"key": null}'))
- assert_equal({'c'=>'}','"a"'=>'b "a b'}, type.deserialize(%q({"c":"}", "\"a\"":"b \"a b"})))
- end
-
- def test_rewrite
- @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')"
- x = JsonDataType.first
- x.payload = { '"a\'' => 'b' }
- assert x.save!
- end
-
- def test_select
- @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')"
- x = JsonDataType.first
- assert_equal({'k' => 'v'}, x.payload)
- end
+ @connection.add_column "json_data_type", "permissions", column_type, default: { "users": "read", "posts": ["read", "write"] }
+ klass.reset_column_information
- def test_select_multikey
- @connection.execute %q|insert into json_data_type (payload) VALUES ('{"k1":"v1", "k2":"v2", "k3":[1,2,3]}')|
- x = JsonDataType.first
- assert_equal({'k1' => 'v1', 'k2' => 'v2', 'k3' => [1,2,3]}, x.payload)
+ assert_equal({ "users" => "read", "posts" => ["read", "write"] }, klass.column_defaults["permissions"])
+ assert_equal({ "users" => "read", "posts" => ["read", "write"] }, klass.new.permissions)
end
- def test_null_json
- @connection.execute %q|insert into json_data_type (payload) VALUES(null)|
- x = JsonDataType.first
- assert_equal(nil, x.payload)
- end
-
- def test_select_array_json_value
- @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')|
- x = JsonDataType.first
- assert_equal(['v0', {'k1' => 'v1'}], x.payload)
- end
-
- def test_rewrite_array_json_value
- @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')|
- x = JsonDataType.first
- x.payload = ['v1', {'k2' => 'v2'}, 'v3']
- assert x.save!
- end
-
- def test_with_store_accessors
- x = JsonDataType.new(resolution: "320×480")
- assert_equal "320×480", x.resolution
-
+ def test_deserialize_with_array
+ x = klass.new(objects: ["foo" => "bar"])
+ assert_equal ["foo" => "bar"], x.objects
x.save!
- x = JsonDataType.first
- assert_equal "320×480", x.resolution
-
- x.resolution = "640×1136"
- x.save!
-
- x = JsonDataType.first
- assert_equal "640×1136", x.resolution
- end
-
- def test_duplication_with_store_accessors
- x = JsonDataType.new(resolution: "320×480")
- assert_equal "320×480", x.resolution
-
- y = x.dup
- assert_equal "320×480", y.resolution
- end
-
- def test_yaml_round_trip_with_store_accessors
- x = JsonDataType.new(resolution: "320×480")
- assert_equal "320×480", x.resolution
-
- y = YAML.load(YAML.dump(x))
- assert_equal "320×480", y.resolution
- end
-
- def test_changes_in_place
- json = JsonDataType.new
- assert_not json.changed?
-
- json.payload = { 'one' => 'two' }
- assert json.changed?
- assert json.payload_changed?
-
- json.save!
- assert_not json.changed?
-
- json.payload['three'] = 'four'
- assert json.payload_changed?
-
- json.save!
- json.reload
-
- assert_equal({ 'one' => 'two', 'three' => 'four' }, json.payload)
- assert_not json.changed?
- end
-
- def test_assigning_invalid_json
- json = JsonDataType.new
-
- json.payload = 'foo'
-
- assert_nil json.payload
+ assert_equal ["foo" => "bar"], x.objects
+ x.reload
+ assert_equal ["foo" => "bar"], x.objects
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/ltree_test.rb b/activerecord/test/cases/adapters/postgresql/ltree_test.rb
index 56516c82b4..8349ee6ee2 100644
--- a/activerecord/test/cases/adapters/postgresql/ltree_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/ltree_test.rb
@@ -1,20 +1,22 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'support/schema_dumping_helper'
+require "support/schema_dumping_helper"
class PostgresqlLtreeTest < ActiveRecord::PostgreSQLTestCase
include SchemaDumpingHelper
class Ltree < ActiveRecord::Base
- self.table_name = 'ltrees'
+ self.table_name = "ltrees"
end
def setup
@connection = ActiveRecord::Base.connection
- enable_extension!('ltree', @connection)
+ enable_extension!("ltree", @connection)
@connection.transaction do
- @connection.create_table('ltrees') do |t|
- t.ltree 'path'
+ @connection.create_table("ltrees") do |t|
+ t.ltree "path"
end
end
rescue ActiveRecord::StatementInvalid
@@ -22,28 +24,28 @@ class PostgresqlLtreeTest < ActiveRecord::PostgreSQLTestCase
end
teardown do
- @connection.drop_table 'ltrees', if_exists: true
+ @connection.drop_table "ltrees", if_exists: true
end
def test_column
- column = Ltree.columns_hash['path']
+ column = Ltree.columns_hash["path"]
assert_equal :ltree, column.type
assert_equal "ltree", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
- type = Ltree.type_for_attribute('path')
- assert_not type.binary?
+ type = Ltree.type_for_attribute("path")
+ assert_not_predicate type, :binary?
end
def test_write
- ltree = Ltree.new(path: '1.2.3.4')
+ ltree = Ltree.new(path: "1.2.3.4")
assert ltree.save!
end
def test_select
@connection.execute "insert into ltrees (path) VALUES ('1.2.3')"
ltree = Ltree.first
- assert_equal '1.2.3', ltree.path
+ assert_equal "1.2.3", ltree.path
end
def test_schema_dump_with_shorthand
diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb
index c031178479..1aa0348879 100644
--- a/activerecord/test/cases/adapters/postgresql/money_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/money_test.rb
@@ -1,22 +1,26 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'support/schema_dumping_helper'
+require "support/schema_dumping_helper"
class PostgresqlMoneyTest < ActiveRecord::PostgreSQLTestCase
include SchemaDumpingHelper
- class PostgresqlMoney < ActiveRecord::Base; end
+ class PostgresqlMoney < ActiveRecord::Base
+ validates :depth, numericality: true
+ end
setup do
@connection = ActiveRecord::Base.connection
@connection.execute("set lc_monetary = 'C'")
- @connection.create_table('postgresql_moneys', force: true) do |t|
+ @connection.create_table("postgresql_moneys", force: true) do |t|
t.money "wealth"
t.money "depth", default: "150.55"
end
end
teardown do
- @connection.drop_table 'postgresql_moneys', if_exists: true
+ @connection.drop_table "postgresql_moneys", if_exists: true
end
def test_column
@@ -24,15 +28,16 @@ class PostgresqlMoneyTest < ActiveRecord::PostgreSQLTestCase
assert_equal :money, column.type
assert_equal "money", column.sql_type
assert_equal 2, column.scale
- assert_not column.array?
+ assert_not_predicate column, :array?
type = PostgresqlMoney.type_for_attribute("wealth")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_default
- assert_equal BigDecimal.new("150.55"), PostgresqlMoney.column_defaults['depth']
- assert_equal BigDecimal.new("150.55"), PostgresqlMoney.new.depth
+ assert_equal BigDecimal("150.55"), PostgresqlMoney.column_defaults["depth"]
+ assert_equal BigDecimal("150.55"), PostgresqlMoney.new.depth
+ assert_equal "150.55", PostgresqlMoney.new.depth_before_type_cast
end
def test_money_values
@@ -46,11 +51,11 @@ class PostgresqlMoneyTest < ActiveRecord::PostgreSQLTestCase
end
def test_money_type_cast
- type = PostgresqlMoney.type_for_attribute('wealth')
- assert_equal(12345678.12, type.cast("$12,345,678.12"))
- assert_equal(12345678.12, type.cast("$12.345.678,12"))
- assert_equal(-1.15, type.cast("-$1.15"))
- assert_equal(-2.25, type.cast("($2.25)"))
+ type = PostgresqlMoney.type_for_attribute("wealth")
+ assert_equal(12345678.12, type.cast(+"$12,345,678.12"))
+ assert_equal(12345678.12, type.cast(+"$12.345.678,12"))
+ assert_equal(-1.15, type.cast(+"-$1.15"))
+ assert_equal(-2.25, type.cast(+"($2.25)"))
end
def test_schema_dumping
@@ -60,10 +65,10 @@ class PostgresqlMoneyTest < ActiveRecord::PostgreSQLTestCase
end
def test_create_and_update_money
- money = PostgresqlMoney.create(wealth: "987.65")
+ money = PostgresqlMoney.create(wealth: +"987.65")
assert_equal 987.65, money.wealth
- new_value = BigDecimal.new('123.45')
+ new_value = BigDecimal("123.45")
money.wealth = new_value
money.save!
money.reload
@@ -80,7 +85,7 @@ class PostgresqlMoneyTest < ActiveRecord::PostgreSQLTestCase
def test_update_all_with_money_big_decimal
money = PostgresqlMoney.create!
- PostgresqlMoney.update_all(wealth: '123.45'.to_d)
+ PostgresqlMoney.update_all(wealth: "123.45".to_d)
money.reload
assert_equal 123.45, money.wealth
diff --git a/activerecord/test/cases/adapters/postgresql/network_test.rb b/activerecord/test/cases/adapters/postgresql/network_test.rb
index fe6ee4e2d9..736570451b 100644
--- a/activerecord/test/cases/adapters/postgresql/network_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/network_test.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'support/schema_dumping_helper'
+require "support/schema_dumping_helper"
class PostgresqlNetworkTest < ActiveRecord::PostgreSQLTestCase
include SchemaDumpingHelper
@@ -7,75 +9,75 @@ class PostgresqlNetworkTest < ActiveRecord::PostgreSQLTestCase
setup do
@connection = ActiveRecord::Base.connection
- @connection.create_table('postgresql_network_addresses', force: true) do |t|
- t.inet 'inet_address', default: "192.168.1.1"
- t.cidr 'cidr_address', default: "192.168.1.0/24"
- t.macaddr 'mac_address', default: "ff:ff:ff:ff:ff:ff"
+ @connection.create_table("postgresql_network_addresses", force: true) do |t|
+ t.inet "inet_address", default: "192.168.1.1"
+ t.cidr "cidr_address", default: "192.168.1.0/24"
+ t.macaddr "mac_address", default: "ff:ff:ff:ff:ff:ff"
end
end
teardown do
- @connection.drop_table 'postgresql_network_addresses', if_exists: true
+ @connection.drop_table "postgresql_network_addresses", if_exists: true
end
def test_cidr_column
column = PostgresqlNetworkAddress.columns_hash["cidr_address"]
assert_equal :cidr, column.type
assert_equal "cidr", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = PostgresqlNetworkAddress.type_for_attribute("cidr_address")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_inet_column
column = PostgresqlNetworkAddress.columns_hash["inet_address"]
assert_equal :inet, column.type
assert_equal "inet", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = PostgresqlNetworkAddress.type_for_attribute("inet_address")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_macaddr_column
column = PostgresqlNetworkAddress.columns_hash["mac_address"]
assert_equal :macaddr, column.type
assert_equal "macaddr", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = PostgresqlNetworkAddress.type_for_attribute("mac_address")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_network_types
- PostgresqlNetworkAddress.create(cidr_address: '192.168.0.0/24',
- inet_address: '172.16.1.254/32',
- mac_address: '01:23:45:67:89:0a')
+ PostgresqlNetworkAddress.create(cidr_address: "192.168.0.0/24",
+ inet_address: "172.16.1.254/32",
+ mac_address: "01:23:45:67:89:0a")
address = PostgresqlNetworkAddress.first
- assert_equal IPAddr.new('192.168.0.0/24'), address.cidr_address
- assert_equal IPAddr.new('172.16.1.254'), address.inet_address
- assert_equal '01:23:45:67:89:0a', address.mac_address
+ assert_equal IPAddr.new("192.168.0.0/24"), address.cidr_address
+ assert_equal IPAddr.new("172.16.1.254"), address.inet_address
+ assert_equal "01:23:45:67:89:0a", address.mac_address
- address.cidr_address = '10.1.2.3/32'
- address.inet_address = '10.0.0.0/8'
- address.mac_address = 'bc:de:f0:12:34:56'
+ address.cidr_address = "10.1.2.3/32"
+ address.inet_address = "10.0.0.0/8"
+ address.mac_address = "bc:de:f0:12:34:56"
address.save!
assert address.reload
- assert_equal IPAddr.new('10.1.2.3/32'), address.cidr_address
- assert_equal IPAddr.new('10.0.0.0/8'), address.inet_address
- assert_equal 'bc:de:f0:12:34:56', address.mac_address
+ assert_equal IPAddr.new("10.1.2.3/32"), address.cidr_address
+ assert_equal IPAddr.new("10.0.0.0/8"), address.inet_address
+ assert_equal "bc:de:f0:12:34:56", address.mac_address
end
def test_invalid_network_address
- invalid_address = PostgresqlNetworkAddress.new(cidr_address: 'invalid addr',
- inet_address: 'invalid addr')
+ invalid_address = PostgresqlNetworkAddress.new(cidr_address: "invalid addr",
+ inet_address: "invalid addr")
assert_nil invalid_address.cidr_address
assert_nil invalid_address.inet_address
- assert_equal 'invalid addr', invalid_address.cidr_address_before_type_cast
- assert_equal 'invalid addr', invalid_address.inet_address_before_type_cast
+ assert_equal "invalid addr", invalid_address.cidr_address_before_type_cast
+ assert_equal "invalid addr", invalid_address.inet_address_before_type_cast
assert invalid_address.save
invalid_address.reload
diff --git a/activerecord/test/cases/adapters/postgresql/numbers_test.rb b/activerecord/test/cases/adapters/postgresql/numbers_test.rb
index ba7e7dc9a3..b53a12254d 100644
--- a/activerecord/test/cases/adapters/postgresql/numbers_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/numbers_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
class PostgresqlNumberTest < ActiveRecord::PostgreSQLTestCase
@@ -5,14 +7,14 @@ class PostgresqlNumberTest < ActiveRecord::PostgreSQLTestCase
setup do
@connection = ActiveRecord::Base.connection
- @connection.create_table('postgresql_numbers', force: true) do |t|
- t.column 'single', 'REAL'
- t.column 'double', 'DOUBLE PRECISION'
+ @connection.create_table("postgresql_numbers", force: true) do |t|
+ t.column "single", "REAL"
+ t.column "double", "DOUBLE PRECISION"
end
end
teardown do
- @connection.drop_table 'postgresql_numbers', if_exists: true
+ @connection.drop_table "postgresql_numbers", if_exists: true
end
def test_data_type
@@ -31,7 +33,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/partitions_test.rb b/activerecord/test/cases/adapters/postgresql/partitions_test.rb
new file mode 100644
index 0000000000..0ac9ca1200
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/partitions_test.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+
+class PostgreSQLPartitionsTest < ActiveRecord::PostgreSQLTestCase
+ def setup
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def teardown
+ @connection.drop_table "partitioned_events", if_exists: true
+ end
+
+ def test_partitions_table_exists
+ skip unless ActiveRecord::Base.connection.postgresql_version >= 100000
+ @connection.create_table :partitioned_events, force: true, id: false,
+ options: "partition by range (issued_at)" do |t|
+ t.timestamp :issued_at
+ end
+ assert @connection.table_exists?("partitioned_events")
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
index 6e6850c4a9..cbb6cd42b5 100644
--- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
@@ -1,10 +1,13 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'support/ddl_helper'
-require 'support/connection_helper'
+require "support/ddl_helper"
+require "support/connection_helper"
module ActiveRecord
module ConnectionAdapters
class PostgreSQLAdapterTest < ActiveRecord::PostgreSQLTestCase
+ self.use_transactional_tests = false
include DdlHelper
include ConnectionHelper
@@ -14,199 +17,140 @@ module ActiveRecord
def test_bad_connection
assert_raise ActiveRecord::NoDatabaseError do
- configuration = ActiveRecord::Base.configurations['arunit'].merge(database: 'should_not_exist-cinco-dog-db')
+ configuration = ActiveRecord::Base.configurations["arunit"].merge(database: "should_not_exist-cinco-dog-db")
connection = ActiveRecord::Base.postgresql_connection(configuration)
- connection.exec_query('SELECT 1')
- 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)
+ connection.exec_query("SELECT 1")
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')
+ assert_equal "id", @connection.primary_key("ex")
end
end
def test_primary_key_works_tables_containing_capital_letters
- assert_equal 'id', @connection.primary_key('CamelCase')
+ assert_equal "id", @connection.primary_key("CamelCase")
end
def test_non_standard_primary_key
- with_example_table 'data character varying(255) primary key' do
- assert_equal 'data', @connection.primary_key('ex')
+ with_example_table "data character varying(255) primary key" do
+ assert_equal "data", @connection.primary_key("ex")
end
end
def test_primary_key_returns_nil_for_no_pk
- with_example_table 'id integer' do
- assert_nil @connection.primary_key('ex')
- end
- end
-
- def test_composite_primary_key
- with_example_table 'id serial, number serial, PRIMARY KEY (id, number)' do
- assert_nil @connection.primary_key('ex')
- 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_insert_sql_with_proprietary_returning_clause
- with_example_table do
- id = @connection.insert_sql("insert into ex (number) values(5150)", nil, "number")
- assert_equal 5150, id
- end
- end
-
- def test_insert_sql_with_quoted_schema_and_table_name
- with_example_table do
- id = @connection.insert_sql('insert into "public"."ex" (number) values(5150)')
- expect = @connection.query('select max(id) from ex').first.first
- assert_equal expect, id
+ with_example_table "id integer" do
+ assert_nil @connection.primary_key("ex")
end
end
- def test_insert_sql_with_no_space_after_table_name
- with_example_table do
- id = @connection.insert_sql("insert into ex(number) values(5150)")
- expect = @connection.query('select max(id) from ex').first.first
- assert_equal expect, id
- end
- end
-
- def test_multiline_insert_sql
- with_example_table do
- id = @connection.insert_sql(<<-SQL)
- insert into ex(
- number)
- values(
- 5152
- )
- SQL
- expect = @connection.query('select max(id) from ex').first.first
- assert_equal expect, id
- end
- end
-
- def test_insert_sql_with_returning_disabled
+ def test_exec_insert_with_returning_disabled
connection = connection_without_insert_returning
- id = connection.insert_sql("insert into postgresql_partitioned_table_parent (number) VALUES (1)")
- expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first
- assert_equal expect.to_i, id
+ result = connection.exec_insert("insert into postgresql_partitioned_table_parent (number) VALUES (1)", nil, [], "id", "postgresql_partitioned_table_parent_id_seq")
+ expect = connection.query("select max(id) from postgresql_partitioned_table_parent").first.first
+ assert_equal expect.to_i, result.rows.first.first
end
- def test_exec_insert_with_returning_disabled
+ def test_exec_insert_with_returning_disabled_and_no_sequence_name_given
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')
- expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first
+ result = connection.exec_insert("insert into postgresql_partitioned_table_parent (number) VALUES (1)", nil, [], "id")
+ expect = connection.query("select max(id) from postgresql_partitioned_table_parent").first.first
assert_equal expect.to_i, result.rows.first.first
end
- def test_exec_insert_with_returning_disabled_and_no_sequence_name_given
+ def test_exec_insert_default_values_with_returning_disabled_and_no_sequence_name_given
connection = connection_without_insert_returning
- result = connection.exec_insert("insert into postgresql_partitioned_table_parent (number) VALUES (1)", nil, [], 'id')
- expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first
+ result = connection.exec_insert("insert into postgresql_partitioned_table_parent DEFAULT VALUES", nil, [], "id")
+ expect = connection.query("select max(id) from postgresql_partitioned_table_parent").first.first
assert_equal expect.to_i, result.rows.first.first
end
- def test_sql_for_insert_with_returning_disabled
+ def test_exec_insert_default_values_quoted_schema_with_returning_disabled_and_no_sequence_name_given
connection = connection_without_insert_returning
- result = connection.sql_for_insert('sql', nil, nil, nil, 'binds')
- assert_equal ['sql', 'binds'], result
+ result = connection.exec_insert('insert into "public"."postgresql_partitioned_table_parent" DEFAULT VALUES', nil, [], "id")
+ expect = connection.query("select max(id) from postgresql_partitioned_table_parent").first.first
+ assert_equal expect.to_i, result.rows.first.first
end
def test_serial_sequence
- assert_equal 'public.accounts_id_seq',
- @connection.serial_sequence('accounts', 'id')
+ assert_equal "public.accounts_id_seq",
+ @connection.serial_sequence("accounts", "id")
assert_raises(ActiveRecord::StatementInvalid) do
- @connection.serial_sequence('zomg', 'id')
+ @connection.serial_sequence("zomg", "id")
end
end
def test_default_sequence_name
- assert_equal 'public.accounts_id_seq',
- @connection.default_sequence_name('accounts', 'id')
+ assert_equal "public.accounts_id_seq",
+ @connection.default_sequence_name("accounts", "id")
- assert_equal 'public.accounts_id_seq',
- @connection.default_sequence_name('accounts')
+ assert_equal "public.accounts_id_seq",
+ @connection.default_sequence_name("accounts")
end
def test_default_sequence_name_bad_table
- assert_equal 'zomg_id_seq',
- @connection.default_sequence_name('zomg', 'id')
+ assert_equal "zomg_id_seq",
+ @connection.default_sequence_name("zomg", "id")
- assert_equal 'zomg_id_seq',
- @connection.default_sequence_name('zomg')
+ assert_equal "zomg_id_seq",
+ @connection.default_sequence_name("zomg")
end
def test_pk_and_sequence_for
with_example_table do
- pk, seq = @connection.pk_and_sequence_for('ex')
- assert_equal 'id', pk
- assert_equal @connection.default_sequence_name('ex', 'id'), seq.to_s
+ pk, seq = @connection.pk_and_sequence_for("ex")
+ assert_equal "id", pk
+ assert_equal @connection.default_sequence_name("ex", "id"), seq.to_s
end
end
def test_pk_and_sequence_for_with_non_standard_primary_key
- with_example_table 'code serial primary key' do
- pk, seq = @connection.pk_and_sequence_for('ex')
- assert_equal 'code', pk
- assert_equal @connection.default_sequence_name('ex', 'code'), seq.to_s
+ with_example_table "code serial primary key" do
+ pk, seq = @connection.pk_and_sequence_for("ex")
+ assert_equal "code", pk
+ assert_equal @connection.default_sequence_name("ex", "code"), seq.to_s
end
end
def test_pk_and_sequence_for_returns_nil_if_no_seq
- with_example_table 'id integer primary key' do
- assert_nil @connection.pk_and_sequence_for('ex')
+ with_example_table "id integer primary key" do
+ assert_nil @connection.pk_and_sequence_for("ex")
end
end
def test_pk_and_sequence_for_returns_nil_if_no_pk
- with_example_table 'id integer' do
- assert_nil @connection.pk_and_sequence_for('ex')
+ with_example_table "id integer" do
+ assert_nil @connection.pk_and_sequence_for("ex")
end
end
def test_pk_and_sequence_for_returns_nil_if_table_not_found
- assert_nil @connection.pk_and_sequence_for('unobtainium')
+ assert_nil @connection.pk_and_sequence_for("unobtainium")
end
def test_pk_and_sequence_for_with_collision_pg_class_oid
- @connection.exec_query('create table ex(id serial primary key)')
- @connection.exec_query('create table ex2(id serial primary key)')
+ @connection.exec_query("create table ex(id serial primary key)")
+ @connection.exec_query("create table ex2(id serial primary key)")
correct_depend_record = [
"'pg_class'::regclass",
"'ex_id_seq'::regclass",
- '0',
+ "0",
"'pg_class'::regclass",
"'ex'::regclass",
- '1',
+ "1",
"'a'"
]
collision_depend_record = [
"'pg_attrdef'::regclass",
"'ex2_id_seq'::regclass",
- '0',
+ "0",
"'pg_class'::regclass",
"'ex'::regclass",
- '1',
+ "1",
"'a'"
]
@@ -220,39 +164,15 @@ module ActiveRecord
"INSERT INTO pg_depend VALUES(#{correct_depend_record.join(',')})"
)
- seq = @connection.pk_and_sequence_for('ex').last
+ seq = @connection.pk_and_sequence_for("ex").last
assert_equal PostgreSQL::Name.new("public", "ex_id_seq"), seq
@connection.exec_query(
"DELETE FROM pg_depend WHERE objid = 'ex2_id_seq'::regclass AND refobjid = 'ex'::regclass AND deptype = 'a'"
)
ensure
- @connection.drop_table 'ex', if_exists: true
- @connection.drop_table 'ex2', if_exists: true
- end
-
- def test_exec_insert_number
- with_example_table do
- insert(@connection, 'number' => 10)
-
- result = @connection.exec_query('SELECT number FROM ex WHERE number = 10')
-
- assert_equal 1, result.rows.length
- assert_equal 10, result.rows.last.last
- end
- end
-
- def test_exec_insert_string
- with_example_table do
- str = 'いただきます!'
- insert(@connection, 'number' => 10, 'data' => str)
-
- result = @connection.exec_query('SELECT number, data FROM ex WHERE number = 10')
-
- value = result.rows.last.last
-
- assert_equal str, value
- end
+ @connection.drop_table "ex", if_exists: true
+ @connection.drop_table "ex2", if_exists: true
end
def test_table_alias_length
@@ -263,89 +183,105 @@ module ActiveRecord
def test_exec_no_binds
with_example_table do
- result = @connection.exec_query('SELECT id, data FROM ex')
+ result = @connection.exec_query("SELECT id, data FROM ex")
assert_equal 0, result.rows.length
assert_equal 2, result.columns.length
assert_equal %w{ id data }, result.columns
- string = @connection.quote('foo')
+ string = @connection.quote("foo")
@connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})")
- result = @connection.exec_query('SELECT id, data FROM ex')
+ result = @connection.exec_query("SELECT id, data FROM ex")
assert_equal 1, result.rows.length
assert_equal 2, result.columns.length
- assert_equal [[1, 'foo']], result.rows
+ assert_equal [[1, "foo"]], result.rows
end
end
- def test_exec_with_binds
- with_example_table do
- string = @connection.quote('foo')
- @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})")
- result = @connection.exec_query(
- 'SELECT id, data FROM ex WHERE id = $1', nil, [bind_param(1)])
+ if ActiveRecord::Base.connection.prepared_statements
+ def test_exec_with_binds
+ with_example_table do
+ string = @connection.quote("foo")
+ @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})")
- assert_equal 1, result.rows.length
- assert_equal 2, result.columns.length
+ bind = Relation::QueryAttribute.new("id", 1, Type::Value.new)
+ result = @connection.exec_query("SELECT id, data FROM ex WHERE id = $1", nil, [bind])
+
+ assert_equal 1, result.rows.length
+ assert_equal 2, result.columns.length
- assert_equal [[1, 'foo']], result.rows
+ assert_equal [[1, "foo"]], result.rows
+ end
end
- end
- def test_exec_typecasts_bind_vals
- with_example_table do
- string = @connection.quote('foo')
- @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})")
+ def test_exec_typecasts_bind_vals
+ with_example_table do
+ string = @connection.quote("foo")
+ @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})")
- bind = ActiveRecord::Relation::QueryAttribute.new("id", "1-fuu", ActiveRecord::Type::Integer.new)
- result = @connection.exec_query(
- 'SELECT id, data FROM ex WHERE id = $1', nil, [bind])
+ bind = Relation::QueryAttribute.new("id", "1-fuu", Type::Integer.new)
+ result = @connection.exec_query("SELECT id, data FROM ex WHERE id = $1", nil, [bind])
- assert_equal 1, result.rows.length
- assert_equal 2, result.columns.length
+ assert_equal 1, result.rows.length
+ assert_equal 2, result.columns.length
- assert_equal [[1, 'foo']], result.rows
+ assert_equal [[1, "foo"]], result.rows
+ end
end
end
- def test_substitute_at
- bind = @connection.substitute_at(nil)
- assert_equal Arel.sql('$1'), bind.to_sql
- end
-
def test_partial_index
with_example_table do
- @connection.add_index 'ex', %w{ id number }, :name => 'partial', :where => "number > 100"
- index = @connection.indexes('ex').find { |idx| idx.name == 'partial' }
+ @connection.add_index "ex", %w{ id number }, name: "partial", where: "number > 100"
+ index = @connection.indexes("ex").find { |idx| idx.name == "partial" }
assert_equal "(number > 100)", index.where
end
end
+ def test_expression_index
+ with_example_table do
+ @connection.add_index "ex", "mod(id, 10), abs(number)", name: "expression"
+ index = @connection.indexes("ex").find { |idx| idx.name == "expression" }
+ assert_equal "mod(id, 10), abs(number)", index.columns
+ end
+ end
+
+ def test_index_with_opclass
+ with_example_table do
+ @connection.add_index "ex", "data", opclass: "varchar_pattern_ops"
+ index = @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data" }
+ assert_equal ["data"], index.columns
+
+ @connection.remove_index "ex", "data"
+ assert_not @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data" }
+ end
+ end
+
def test_columns_for_distinct_zero_orders
assert_equal "posts.id",
@connection.columns_for_distinct("posts.id", [])
end
def test_columns_for_distinct_one_order
- assert_equal "posts.id, posts.created_at AS alias_0",
+ assert_equal "posts.created_at AS alias_0, posts.id",
@connection.columns_for_distinct("posts.id", ["posts.created_at desc"])
end
def test_columns_for_distinct_few_orders
- assert_equal "posts.id, posts.created_at AS alias_0, posts.position AS alias_1",
+ assert_equal "posts.created_at AS alias_0, posts.position AS alias_1, posts.id",
@connection.columns_for_distinct("posts.id", ["posts.created_at desc", "posts.position asc"])
end
def test_columns_for_distinct_with_case
assert_equal(
- 'posts.id, CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END AS alias_0',
- @connection.columns_for_distinct('posts.id',
+ "CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END AS alias_0, posts.id",
+ @connection.columns_for_distinct("posts.id",
["CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END"])
)
end
def test_columns_for_distinct_blank_not_nil_orders
- assert_equal "posts.id, posts.created_at AS alias_0",
+ assert_equal "posts.created_at AS alias_0, posts.id",
@connection.columns_for_distinct("posts.id", ["posts.created_at desc", "", " "])
end
@@ -354,23 +290,23 @@ module ActiveRecord
def order.to_sql
"posts.created_at desc"
end
- assert_equal "posts.id, posts.created_at AS alias_0",
+ assert_equal "posts.created_at AS alias_0, posts.id",
@connection.columns_for_distinct("posts.id", [order])
end
def test_columns_for_distinct_with_nulls
- assert_equal "posts.title, posts.updater_id AS alias_0", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls first"])
- assert_equal "posts.title, posts.updater_id AS alias_0", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls last"])
+ assert_equal "posts.updater_id AS alias_0, posts.title", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls first"])
+ assert_equal "posts.updater_id AS alias_0, posts.title", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls last"])
end
def test_columns_for_distinct_without_order_specifiers
- assert_equal "posts.title, posts.updater_id AS alias_0",
+ assert_equal "posts.updater_id AS alias_0, posts.title",
@connection.columns_for_distinct("posts.title", ["posts.updater_id"])
- assert_equal "posts.title, posts.updater_id AS alias_0",
+ assert_equal "posts.updater_id AS alias_0, posts.title",
@connection.columns_for_distinct("posts.title", ["posts.updater_id nulls last"])
- assert_equal "posts.title, posts.updater_id AS alias_0",
+ assert_equal "posts.updater_id AS alias_0, posts.title",
@connection.columns_for_distinct("posts.title", ["posts.updater_id nulls first"])
end
@@ -390,29 +326,35 @@ module ActiveRecord
reset_connection
end
- def test_only_reload_type_map_once_for_every_unknown_type
+ def test_only_reload_type_map_once_for_every_unrecognized_type
+ reset_connection
+ connection = ActiveRecord::Base.connection
+
silence_warnings do
assert_queries 2, ignore_none: true do
- @connection.select_all "SELECT NULL::anyelement"
+ connection.select_all "select 'pg_catalog.pg_class'::regclass"
end
assert_queries 1, ignore_none: true do
- @connection.select_all "SELECT NULL::anyelement"
+ connection.select_all "select 'pg_catalog.pg_class'::regclass"
end
assert_queries 2, ignore_none: true do
- @connection.select_all "SELECT NULL::anyarray"
+ connection.select_all "SELECT NULL::anyarray"
end
end
ensure
reset_connection
end
- def test_only_warn_on_first_encounter_of_unknown_oid
+ def test_only_warn_on_first_encounter_of_unrecognized_oid
+ reset_connection
+ connection = ActiveRecord::Base.connection
+
warning = capture(:stderr) {
- @connection.select_all "SELECT NULL::anyelement"
- @connection.select_all "SELECT NULL::anyelement"
- @connection.select_all "SELECT NULL::anyelement"
+ connection.select_all "select 'pg_catalog.pg_class'::regclass"
+ connection.select_all "select 'pg_catalog.pg_class'::regclass"
+ connection.select_all "select 'pg_catalog.pg_class'::regclass"
}
- assert_match(/\Aunknown OID \d+: failed to recognize type of 'anyelement'. It will be treated as String.\n\z/, warning)
+ assert_match(/\Aunknown OID \d+: failed to recognize type of 'regclass'\. It will be treated as String\.\n\z/, warning)
ensure
reset_connection
end
@@ -420,7 +362,7 @@ module ActiveRecord
def test_unparsed_defaults_are_at_least_set_when_saving
with_example_table "id SERIAL PRIMARY KEY, number INTEGER NOT NULL DEFAULT (4 + 4) * 2 / 4" do
number_klass = Class.new(ActiveRecord::Base) do
- self.table_name = 'ex'
+ self.table_name = "ex"
end
column = number_klass.columns_hash["number"]
assert_nil column.default
@@ -435,31 +377,14 @@ module ActiveRecord
end
private
- def insert(ctx, data)
- binds = data.map { |name, value|
- bind_param(value, name)
- }
- columns = binds.map(&:name)
-
- bind_subs = columns.length.times.map { |x| "$#{x + 1}" }
-
- sql = "INSERT INTO ex (#{columns.join(", ")})
- VALUES (#{bind_subs.join(', ')})"
- ctx.exec_insert(sql, 'SQL', binds)
- end
-
- def with_example_table(definition = 'id serial primary key, number integer, data character varying(255)', &block)
- super(@connection, 'ex', definition, &block)
- end
-
- def connection_without_insert_returning
- ActiveRecord::Base.postgresql_connection(ActiveRecord::Base.configurations['arunit'].merge(:insert_returning => false))
- end
+ def with_example_table(definition = "id serial primary key, number integer, data character varying(255)", &block)
+ super(@connection, "ex", definition, &block)
+ end
- def bind_param(value, name = nil)
- ActiveRecord::Relation::QueryAttribute.new(name, value, ActiveRecord::Type::Value.new)
- end
+ def connection_without_insert_returning
+ ActiveRecord::Base.postgresql_connection(ActiveRecord::Base.configurations["arunit"].merge(insert_returning: false))
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/prepared_statements_disabled_test.rb b/activerecord/test/cases/adapters/postgresql/prepared_statements_disabled_test.rb
new file mode 100644
index 0000000000..f7478b50c3
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/prepared_statements_disabled_test.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/computer"
+require "models/developer"
+
+class PreparedStatementsDisabledTest < ActiveRecord::PostgreSQLTestCase
+ fixtures :developers
+
+ def setup
+ @conn = ActiveRecord::Base.establish_connection :arunit_without_prepared_statements
+ end
+
+ def teardown
+ @conn.release_connection
+ ActiveRecord::Base.establish_connection :arunit
+ end
+
+ def test_select_query_works_even_when_prepared_statements_are_disabled
+ assert_not Developer.connection.prepared_statements
+
+ david = developers(:david)
+
+ assert_equal david, Developer.where(name: "David").last # With Binds
+ assert_operator Developer.count, :>, 0 # Without Binds
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb
index 5e6f4dbbb8..d50dc49276 100644
--- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb
@@ -1,5 +1,6 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'ipaddr'
module ActiveRecord
module ConnectionAdapters
@@ -10,20 +11,20 @@ module ActiveRecord
end
def test_type_cast_true
- assert_equal 't', @conn.type_cast(true)
+ assert_equal true, @conn.type_cast(true)
end
def test_type_cast_false
- assert_equal 'f', @conn.type_cast(false)
+ assert_equal false, @conn.type_cast(false)
end
def test_quote_float_nan
- nan = 0.0/0
+ nan = 0.0 / 0
assert_equal "'NaN'", @conn.quote(nan)
end
def test_quote_float_infinity
- infinity = 1.0/0
+ infinity = 1.0 / 0
assert_equal "'Infinity'", @conn.quote(infinity)
end
@@ -36,7 +37,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/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb
index 02b1083430..433598500d 100644
--- a/activerecord/test/cases/adapters/postgresql/range_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/range_test.rb
@@ -1,14 +1,18 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'support/connection_helper'
+require "support/connection_helper"
if ActiveRecord::Base.connection.respond_to?(:supports_ranges?) && ActiveRecord::Base.connection.supports_ranges?
class PostgresqlRange < ActiveRecord::Base
self.table_name = "postgresql_ranges"
+ self.time_zone_aware_types += [:tsrange, :tstzrange]
end
class PostgresqlRangeTest < ActiveRecord::PostgreSQLTestCase
self.use_transactional_tests = false
include ConnectionHelper
+ include InTimeZone
def setup
@connection = PostgresqlRange.connection
@@ -21,7 +25,7 @@ if ActiveRecord::Base.connection.respond_to?(:supports_ranges?) && ActiveRecord:
);
_SQL
- @connection.create_table('postgresql_ranges') do |t|
+ @connection.create_table("postgresql_ranges") do |t|
t.daterange :date_range
t.numrange :num_range
t.tsrange :ts_range
@@ -30,7 +34,7 @@ _SQL
t.int8range :int8_range
end
- @connection.add_column 'postgresql_ranges', 'float_range', 'floatrange'
+ @connection.add_column "postgresql_ranges", "float_range", "floatrange"
end
PostgresqlRange.reset_column_information
rescue ActiveRecord::StatementInvalid
@@ -91,8 +95,8 @@ _SQL
end
teardown do
- @connection.drop_table 'postgresql_ranges', if_exists: true
- @connection.execute 'DROP TYPE IF EXISTS floatrange'
+ @connection.drop_table "postgresql_ranges", if_exists: true
+ @connection.execute "DROP TYPE IF EXISTS floatrange"
reset_connection
end
@@ -130,10 +134,10 @@ _SQL
end
def test_numrange_values
- assert_equal BigDecimal.new('0.1')..BigDecimal.new('0.2'), @first_range.num_range
- assert_equal BigDecimal.new('0.1')...BigDecimal.new('0.2'), @second_range.num_range
- assert_equal BigDecimal.new('0.1')...BigDecimal.new('Infinity'), @third_range.num_range
- assert_equal BigDecimal.new('-Infinity')...BigDecimal.new('Infinity'), @fourth_range.num_range
+ assert_equal BigDecimal("0.1")..BigDecimal("0.2"), @first_range.num_range
+ assert_equal BigDecimal("0.1")...BigDecimal("0.2"), @second_range.num_range
+ assert_equal BigDecimal("0.1")...BigDecimal("Infinity"), @third_range.num_range
+ assert_equal BigDecimal("-Infinity")...BigDecimal("Infinity"), @fourth_range.num_range
assert_nil @empty_range.num_range
end
@@ -146,8 +150,8 @@ _SQL
end
def test_tstzrange_values
- assert_equal Time.parse('2010-01-01 09:30:00 UTC')..Time.parse('2011-01-01 17:30:00 UTC'), @first_range.tstz_range
- assert_equal Time.parse('2010-01-01 09:30:00 UTC')...Time.parse('2011-01-01 17:30:00 UTC'), @second_range.tstz_range
+ assert_equal Time.parse("2010-01-01 09:30:00 UTC")..Time.parse("2011-01-01 17:30:00 UTC"), @first_range.tstz_range
+ assert_equal Time.parse("2010-01-01 09:30:00 UTC")...Time.parse("2011-01-01 17:30:00 UTC"), @second_range.tstz_range
assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.tstz_range)
assert_nil @empty_range.tstz_range
end
@@ -160,18 +164,38 @@ _SQL
assert_nil @empty_range.float_range
end
+ def test_timezone_awareness_tzrange
+ tz = "Pacific Time (US & Canada)"
+
+ in_time_zone tz do
+ PostgresqlRange.reset_column_information
+ time_string = Time.current.to_s
+ time = Time.zone.parse(time_string)
+
+ record = PostgresqlRange.new(tstz_range: time_string..time_string)
+ assert_equal time..time, record.tstz_range
+ assert_equal ActiveSupport::TimeZone[tz], record.tstz_range.begin.time_zone
+
+ record.save!
+ record.reload
+
+ assert_equal time..time, record.tstz_range
+ assert_equal ActiveSupport::TimeZone[tz], record.tstz_range.begin.time_zone
+ end
+ end
+
def test_create_tstzrange
- tstzrange = Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2011-02-02 14:30:00 CDT')
+ tstzrange = Time.parse("2010-01-01 14:30:00 +0100")...Time.parse("2011-02-02 14:30:00 CDT")
round_trip(@new_range, :tstz_range, tstzrange)
assert_equal @new_range.tstz_range, tstzrange
- assert_equal @new_range.tstz_range, Time.parse('2010-01-01 13:30:00 UTC')...Time.parse('2011-02-02 19:30:00 UTC')
+ assert_equal @new_range.tstz_range, Time.parse("2010-01-01 13:30:00 UTC")...Time.parse("2011-02-02 19:30:00 UTC")
end
def test_update_tstzrange
assert_equal_round_trip(@first_range, :tstz_range,
- Time.parse('2010-01-01 14:30:00 CDT')...Time.parse('2011-02-02 14:30:00 CET'))
+ Time.parse("2010-01-01 14:30:00 CDT")...Time.parse("2011-02-02 14:30:00 CET"))
assert_nil_round_trip(@first_range, :tstz_range,
- Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2010-01-01 13:30:00 +0000'))
+ Time.parse("2010-01-01 14:30:00 +0100")...Time.parse("2010-01-01 13:30:00 +0000"))
end
def test_create_tsrange
@@ -188,16 +212,87 @@ _SQL
Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2010, 1, 1, 14, 30, 0))
end
+ def test_timezone_awareness_tsrange
+ tz = "Pacific Time (US & Canada)"
+
+ in_time_zone tz do
+ PostgresqlRange.reset_column_information
+ time_string = Time.current.to_s
+ time = Time.zone.parse(time_string)
+
+ record = PostgresqlRange.new(ts_range: time_string..time_string)
+ assert_equal time..time, record.ts_range
+ assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone
+
+ record.save!
+ record.reload
+
+ assert_equal time..time, record.ts_range
+ assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone
+ end
+ end
+
+ def test_create_tstzrange_preserve_usec
+ tstzrange = Time.parse("2010-01-01 14:30:00.670277 +0100")...Time.parse("2011-02-02 14:30:00.745125 CDT")
+ round_trip(@new_range, :tstz_range, tstzrange)
+ assert_equal @new_range.tstz_range, tstzrange
+ assert_equal @new_range.tstz_range, Time.parse("2010-01-01 13:30:00.670277 UTC")...Time.parse("2011-02-02 19:30:00.745125 UTC")
+ end
+
+ def test_update_tstzrange_preserve_usec
+ assert_equal_round_trip(@first_range, :tstz_range,
+ Time.parse("2010-01-01 14:30:00.245124 CDT")...Time.parse("2011-02-02 14:30:00.451274 CET"))
+ assert_nil_round_trip(@first_range, :tstz_range,
+ Time.parse("2010-01-01 14:30:00.245124 +0100")...Time.parse("2010-01-01 13:30:00.245124 +0000"))
+ end
+
+ def test_create_tsrange_preseve_usec
+ tz = ::ActiveRecord::Base.default_timezone
+ assert_equal_round_trip(@new_range, :ts_range,
+ Time.send(tz, 2010, 1, 1, 14, 30, 0, 125435)...Time.send(tz, 2011, 2, 2, 14, 30, 0, 225435))
+ end
+
+ def test_update_tsrange_preserve_usec
+ tz = ::ActiveRecord::Base.default_timezone
+ assert_equal_round_trip(@first_range, :ts_range,
+ Time.send(tz, 2010, 1, 1, 14, 30, 0, 142432)...Time.send(tz, 2011, 2, 2, 14, 30, 0, 224242))
+ assert_nil_round_trip(@first_range, :ts_range,
+ Time.send(tz, 2010, 1, 1, 14, 30, 0, 142432)...Time.send(tz, 2010, 1, 1, 14, 30, 0, 142432))
+ end
+
+ def test_timezone_awareness_tsrange_preserve_usec
+ tz = "Pacific Time (US & Canada)"
+
+ in_time_zone tz do
+ PostgresqlRange.reset_column_information
+ time_string = "2017-09-26 07:30:59.132451 -0700"
+ time = Time.zone.parse(time_string)
+ assert time.usec > 0
+
+ record = PostgresqlRange.new(ts_range: time_string..time_string)
+ assert_equal time..time, record.ts_range
+ assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone
+ assert_equal time.usec, record.ts_range.begin.usec
+
+ record.save!
+ record.reload
+
+ assert_equal time..time, record.ts_range
+ assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone
+ assert_equal time.usec, record.ts_range.begin.usec
+ end
+ end
+
def test_create_numrange
assert_equal_round_trip(@new_range, :num_range,
- BigDecimal.new('0.5')...BigDecimal.new('1'))
+ BigDecimal("0.5")...BigDecimal("1"))
end
def test_update_numrange
assert_equal_round_trip(@first_range, :num_range,
- BigDecimal.new('0.5')...BigDecimal.new('1'))
+ BigDecimal("0.5")...BigDecimal("1"))
assert_nil_round_trip(@first_range, :num_range,
- BigDecimal.new('0.5')...BigDecimal.new('0.5'))
+ BigDecimal("0.5")...BigDecimal("0.5"))
end
def test_create_daterange
@@ -240,6 +335,18 @@ _SQL
assert_raises(ArgumentError) { PostgresqlRange.create!(tstz_range: "(''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']") }
end
+ def test_where_by_attribute_with_range
+ range = 1..100
+ record = PostgresqlRange.create!(int4_range: range)
+ assert_equal record, PostgresqlRange.where(int4_range: range).take
+ end
+
+ def test_where_by_attribute_with_range_in_array
+ range = 1..100
+ record = PostgresqlRange.create!(int4_range: range)
+ assert_equal record, PostgresqlRange.where(int4_range: [range]).take
+ end
+
def test_update_all_with_ranges
PostgresqlRange.create!
@@ -257,6 +364,18 @@ _SQL
end
end
+ def test_infinity_values
+ PostgresqlRange.create!(int4_range: 1..Float::INFINITY,
+ int8_range: -Float::INFINITY..0,
+ float_range: -Float::INFINITY..Float::INFINITY)
+
+ record = PostgresqlRange.first
+
+ assert_equal(1...Float::INFINITY, record.int4_range)
+ assert_equal(-Float::INFINITY...1, record.int8_range)
+ assert_equal(-Float::INFINITY...Float::INFINITY, record.float_range)
+ end
+
private
def assert_equal_round_trip(range, attribute, value)
round_trip(range, attribute, value)
diff --git a/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb
index c895ab9db5..ba477c63f4 100644
--- a/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb
@@ -1,5 +1,7 @@
-require 'cases/helper'
-require 'support/connection_helper'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "support/connection_helper"
class PostgreSQLReferentialIntegrityTest < ActiveRecord::PostgreSQLTestCase
self.use_transactional_tests = false
@@ -14,7 +16,7 @@ class PostgreSQLReferentialIntegrityTest < ActiveRecord::PostgreSQLTestCase
def execute(sql)
if IS_REFERENTIAL_INTEGRITY_SQL.call(sql)
super "BROKEN;" rescue nil # put transaction in broken state
- raise ActiveRecord::StatementInvalid, 'PG::InsufficientPrivilege'
+ raise ActiveRecord::StatementInvalid, "PG::InsufficientPrivilege"
else
super
end
@@ -24,7 +26,7 @@ class PostgreSQLReferentialIntegrityTest < ActiveRecord::PostgreSQLTestCase
module ProgrammerMistake
def execute(sql)
if IS_REFERENTIAL_INTEGRITY_SQL.call(sql)
- raise ArgumentError, 'something is not right.'
+ raise ArgumentError, "something is not right."
else
super
end
@@ -48,10 +50,10 @@ class PostgreSQLReferentialIntegrityTest < ActiveRecord::PostgreSQLTestCase
warning = capture(:stderr) do
e = assert_raises(ActiveRecord::InvalidForeignKey) do
@connection.disable_referential_integrity do
- raise ActiveRecord::InvalidForeignKey, 'Should be re-raised'
+ raise ActiveRecord::InvalidForeignKey, "Should be re-raised"
end
end
- assert_equal 'Should be re-raised', e.message
+ 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
@@ -63,10 +65,10 @@ class PostgreSQLReferentialIntegrityTest < ActiveRecord::PostgreSQLTestCase
warning = capture(:stderr) do
e = assert_raises(ActiveRecord::StatementInvalid) do
@connection.disable_referential_integrity do
- raise ActiveRecord::StatementInvalid, 'Should be re-raised'
+ raise ActiveRecord::StatementInvalid, "Should be re-raised"
end
end
- assert_equal 'Should be re-raised', e.message
+ assert_equal "Should be re-raised", e.message
end
assert warning.blank?, "expected no warnings but got:\n#{warning}"
end
@@ -99,13 +101,13 @@ class PostgreSQLReferentialIntegrityTest < ActiveRecord::PostgreSQLTestCase
@connection.extend ProgrammerMistake
assert_raises ArgumentError do
- @connection.disable_referential_integrity {}
+ @connection.disable_referential_integrity { }
end
end
private
- def assert_transaction_is_not_broken
- assert_equal 1, @connection.select_value("SELECT 1")
- end
+ def assert_transaction_is_not_broken
+ assert_equal 1, @connection.select_value("SELECT 1")
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/rename_table_test.rb b/activerecord/test/cases/adapters/postgresql/rename_table_test.rb
index bd64bae308..100d247113 100644
--- a/activerecord/test/cases/adapters/postgresql/rename_table_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/rename_table_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
class PostgresqlRenameTableTest < ActiveRecord::PostgreSQLTestCase
@@ -24,11 +26,11 @@ class PostgresqlRenameTableTest < ActiveRecord::PostgreSQLTestCase
private
- def num_indices_named(name)
- @connection.execute(<<-SQL).values.length
- SELECT 1 FROM "pg_index"
- JOIN "pg_class" ON "pg_index"."indexrelid" = "pg_class"."oid"
- WHERE "pg_class"."relname" = '#{name}'
- SQL
- end
+ def num_indices_named(name)
+ @connection.execute(<<-SQL).values.length
+ SELECT 1 FROM "pg_index"
+ JOIN "pg_class" ON "pg_index"."indexrelid" = "pg_class"."oid"
+ WHERE "pg_class"."relname" = '#{name}'
+ SQL
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb
index fa6584eae5..fcb0aec81b 100644
--- a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
class SchemaThing < ActiveRecord::Base
@@ -6,12 +8,12 @@ end
class SchemaAuthorizationTest < ActiveRecord::PostgreSQLTestCase
self.use_transactional_tests = false
- TABLE_NAME = 'schema_things'
+ TABLE_NAME = "schema_things"
COLUMNS = [
- 'id serial primary key',
- 'name character varying(50)'
+ "id serial primary key",
+ "name character varying(50)"
]
- USERS = ['rails_pg_schema_user1', 'rails_pg_schema_user2']
+ USERS = ["rails_pg_schema_user1", "rails_pg_schema_user2"]
def setup
@connection = ActiveRecord::Base.connection
@@ -31,7 +33,7 @@ class SchemaAuthorizationTest < ActiveRecord::PostgreSQLTestCase
set_session_auth
@connection.execute "RESET search_path"
USERS.each do |u|
- @connection.execute "DROP SCHEMA #{u} CASCADE"
+ @connection.drop_schema u
@connection.execute "DROP USER #{u}"
end
end
@@ -45,7 +47,7 @@ class SchemaAuthorizationTest < ActiveRecord::PostgreSQLTestCase
def test_session_auth=
assert_raise(ActiveRecord::StatementInvalid) do
- @connection.session_auth = 'DEFAULT'
+ @connection.session_auth = "DEFAULT"
@connection.execute "SELECT * FROM #{TABLE_NAME}"
end
end
@@ -55,31 +57,22 @@ class SchemaAuthorizationTest < ActiveRecord::PostgreSQLTestCase
set_session_auth
USERS.each do |u|
set_session_auth u
- assert_equal u, @connection.exec_query("SELECT name FROM #{TABLE_NAME} WHERE id = $1", 'SQL', [bind_param(1)]).first['name']
- set_session_auth
- end
- end
- end
-
- def test_auth_with_bind
- assert_nothing_raised do
- set_session_auth
- USERS.each do |u|
- @connection.clear_cache!
- set_session_auth u
- assert_equal u, @connection.exec_query("SELECT name FROM #{TABLE_NAME} WHERE id = $1", 'SQL', [bind_param(1)]).first['name']
+ assert_equal u, @connection.select_value("SELECT name FROM #{TABLE_NAME} WHERE id = 1")
set_session_auth
end
end
end
- def test_schema_uniqueness
- assert_nothing_raised do
- set_session_auth
- USERS.each do |u|
- set_session_auth u
- assert_equal u, @connection.select_value("SELECT name FROM #{TABLE_NAME} WHERE id = 1")
+ if ActiveRecord::Base.connection.prepared_statements
+ def test_auth_with_bind
+ assert_nothing_raised do
set_session_auth
+ USERS.each do |u|
+ @connection.clear_cache!
+ set_session_auth u
+ assert_equal u, @connection.select_value("SELECT name FROM #{TABLE_NAME} WHERE id = $1", "SQL", [bind_param(1)])
+ set_session_auth
+ end
end
end
end
@@ -88,9 +81,9 @@ class SchemaAuthorizationTest < ActiveRecord::PostgreSQLTestCase
assert_nothing_raised do
USERS.each do |u|
set_session_auth u
- st = SchemaThing.new :name => 'TEST1'
+ st = SchemaThing.new name: "TEST1"
st.save!
- st = SchemaThing.new :id => 5, :name => 'TEST2'
+ st = SchemaThing.new id: 5, name: "TEST2"
st.save!
set_session_auth
end
@@ -98,17 +91,17 @@ class SchemaAuthorizationTest < ActiveRecord::PostgreSQLTestCase
end
def test_tables_in_current_schemas
- assert !@connection.tables.include?(TABLE_NAME)
+ assert_not_includes @connection.tables, TABLE_NAME
USERS.each do |u|
set_session_auth u
- assert @connection.tables.include?(TABLE_NAME)
+ assert_includes @connection.tables, TABLE_NAME
set_session_auth
end
end
private
- def set_session_auth auth = nil
- @connection.session_auth = auth || 'default'
+ def set_session_auth(auth = nil)
+ @connection.session_auth = auth || "default"
end
def bind_param(value)
diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb
index 35d5581aa7..a36d066c80 100644
--- a/activerecord/test/cases/adapters/postgresql/schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb
@@ -1,36 +1,50 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/default'
-require 'support/schema_dumping_helper'
+require "models/default"
+require "support/schema_dumping_helper"
+
+module PGSchemaHelper
+ def with_schema_search_path(schema_search_path)
+ @connection.schema_search_path = schema_search_path
+ @connection.schema_cache.clear!
+ yield if block_given?
+ ensure
+ @connection.schema_search_path = "'$user', public"
+ @connection.schema_cache.clear!
+ end
+end
class SchemaTest < ActiveRecord::PostgreSQLTestCase
+ include PGSchemaHelper
self.use_transactional_tests = false
- SCHEMA_NAME = 'test_schema'
- SCHEMA2_NAME = 'test_schema2'
- TABLE_NAME = 'things'
- CAPITALIZED_TABLE_NAME = 'Things'
- INDEX_A_NAME = 'a_index_things_on_name'
- INDEX_B_NAME = 'b_index_things_on_different_columns_in_each_schema'
- INDEX_C_NAME = 'c_index_full_text_search'
- INDEX_D_NAME = 'd_index_things_on_description_desc'
- INDEX_E_NAME = 'e_index_things_on_name_vector'
- INDEX_A_COLUMN = 'name'
- INDEX_B_COLUMN_S1 = 'email'
- INDEX_B_COLUMN_S2 = 'moment'
- INDEX_C_COLUMN = %q{(to_tsvector('english', coalesce(things.name, '')))}
- INDEX_D_COLUMN = 'description'
- INDEX_E_COLUMN = 'name_vector'
+ SCHEMA_NAME = "test_schema"
+ SCHEMA2_NAME = "test_schema2"
+ TABLE_NAME = "things"
+ CAPITALIZED_TABLE_NAME = "Things"
+ INDEX_A_NAME = "a_index_things_on_name"
+ INDEX_B_NAME = "b_index_things_on_different_columns_in_each_schema"
+ INDEX_C_NAME = "c_index_full_text_search"
+ INDEX_D_NAME = "d_index_things_on_description_desc"
+ INDEX_E_NAME = "e_index_things_on_name_vector"
+ INDEX_A_COLUMN = "name"
+ INDEX_B_COLUMN_S1 = "email"
+ INDEX_B_COLUMN_S2 = "moment"
+ INDEX_C_COLUMN = "(to_tsvector('english', coalesce(things.name, '')))"
+ INDEX_D_COLUMN = "description"
+ INDEX_E_COLUMN = "name_vector"
COLUMNS = [
- 'id integer',
- 'name character varying(50)',
- 'email character varying(50)',
- 'description character varying(100)',
- 'name_vector tsvector',
- 'moment timestamp without time zone default now()'
+ "id integer",
+ "name character varying(50)",
+ "email character varying(50)",
+ "description character varying(100)",
+ "name_vector tsvector",
+ "moment timestamp without time zone default now()"
]
- PK_TABLE_NAME = 'table_with_pk'
- UNMATCHED_SEQUENCE_NAME = 'unmatched_primary_key_default_value_seq'
- UNMATCHED_PK_TABLE_NAME = 'table_with_unmatched_sequence_for_pk'
+ PK_TABLE_NAME = "table_with_pk"
+ UNMATCHED_SEQUENCE_NAME = "unmatched_primary_key_default_value_seq"
+ UNMATCHED_PK_TABLE_NAME = "table_with_unmatched_sequence_for_pk"
class Thing1 < ActiveRecord::Base
self.table_name = "test_schema.things"
@@ -49,7 +63,7 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase
end
class Thing5 < ActiveRecord::Base
- self.table_name = 'things'
+ self.table_name = "things"
end
class Song < ActiveRecord::Base
@@ -79,13 +93,14 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase
@connection.execute "CREATE INDEX #{INDEX_E_NAME} ON #{SCHEMA_NAME}.#{TABLE_NAME} USING gin (#{INDEX_E_COLUMN});"
@connection.execute "CREATE INDEX #{INDEX_E_NAME} ON #{SCHEMA2_NAME}.#{TABLE_NAME} USING gin (#{INDEX_E_COLUMN});"
@connection.execute "CREATE TABLE #{SCHEMA_NAME}.#{PK_TABLE_NAME} (id serial primary key)"
+ @connection.execute "CREATE TABLE #{SCHEMA2_NAME}.#{PK_TABLE_NAME} (id serial primary key)"
@connection.execute "CREATE SEQUENCE #{SCHEMA_NAME}.#{UNMATCHED_SEQUENCE_NAME}"
@connection.execute "CREATE TABLE #{SCHEMA_NAME}.#{UNMATCHED_PK_TABLE_NAME} (id integer NOT NULL DEFAULT nextval('#{SCHEMA_NAME}.#{UNMATCHED_SEQUENCE_NAME}'::regclass), CONSTRAINT unmatched_pkey PRIMARY KEY (id))"
end
teardown do
- @connection.execute "DROP SCHEMA #{SCHEMA2_NAME} CASCADE"
- @connection.execute "DROP SCHEMA #{SCHEMA_NAME} CASCADE"
+ @connection.drop_schema SCHEMA2_NAME, if_exists: true
+ @connection.drop_schema SCHEMA_NAME, if_exists: true
end
def test_schema_names
@@ -118,13 +133,20 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase
ensure
@connection.drop_schema "test_schema3"
end
- assert !@connection.schema_names.include?("test_schema3")
+ assert_not_includes @connection.schema_names, "test_schema3"
+ end
+
+ def test_drop_schema_if_exists
+ @connection.create_schema "some_schema"
+ assert_includes @connection.schema_names, "some_schema"
+ @connection.drop_schema "some_schema", if_exists: true
+ assert_not_includes @connection.schema_names, "some_schema"
end
def test_habtm_table_name_with_schema
+ ActiveRecord::Base.connection.drop_schema "music", if_exists: true
+ ActiveRecord::Base.connection.create_schema "music"
ActiveRecord::Base.connection.execute <<-SQL
- DROP SCHEMA IF EXISTS music CASCADE;
- CREATE SCHEMA music;
CREATE TABLE music.albums (id serial primary key);
CREATE TABLE music.songs (id serial primary key);
CREATE TABLE music.albums_songs (album_id integer, song_id integer);
@@ -134,69 +156,75 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase
Album.create
assert_equal song, Song.includes(:albums).references(:albums).first
ensure
- ActiveRecord::Base.connection.execute "DROP SCHEMA music CASCADE;"
+ ActiveRecord::Base.connection.drop_schema "music", if_exists: true
end
- def test_raise_drop_schema_with_nonexisting_schema
+ def test_drop_schema_with_nonexisting_schema
assert_raises(ActiveRecord::StatementInvalid) do
- @connection.drop_schema "test_schema3"
+ @connection.drop_schema "idontexist"
+ end
+
+ assert_nothing_raised do
+ @connection.drop_schema "idontexist", if_exists: true
end
end
- def test_raise_wraped_exception_on_bad_prepare
+ def test_raise_wrapped_exception_on_bad_prepare
assert_raises(ActiveRecord::StatementInvalid) do
- @connection.exec_query "select * from developers where id = ?", 'sql', [[nil, 1]]
+ @connection.exec_query "select * from developers where id = ?", "sql", [bind_param(1)]
end
end
- def test_schema_change_with_prepared_stmt
- altered = false
- @connection.exec_query "select * from developers where id = $1", 'sql', [bind_param(1)]
- @connection.exec_query "alter table developers add column zomg int", 'sql', []
- altered = true
- @connection.exec_query "select * from developers where id = $1", 'sql', [bind_param(1)]
- ensure
- # We are not using DROP COLUMN IF EXISTS because that syntax is only
- # supported by pg 9.X
- @connection.exec_query("alter table developers drop column zomg", 'sql', []) if altered
+ if ActiveRecord::Base.connection.prepared_statements
+ def test_schema_change_with_prepared_stmt
+ altered = false
+ @connection.exec_query "select * from developers where id = $1", "sql", [bind_param(1)]
+ @connection.exec_query "alter table developers add column zomg int", "sql", []
+ altered = true
+ @connection.exec_query "select * from developers where id = $1", "sql", [bind_param(1)]
+ ensure
+ # We are not using DROP COLUMN IF EXISTS because that syntax is only
+ # supported by pg 9.X
+ @connection.exec_query("alter table developers drop column zomg", "sql", []) if altered
+ end
end
- def test_table_exists?
+ def test_data_source_exists?
[Thing1, Thing2, Thing3, Thing4].each do |klass|
name = klass.table_name
- assert @connection.table_exists?(name), "'#{name}' table should exist"
+ assert @connection.data_source_exists?(name), "'#{name}' data_source should exist"
end
end
- def test_table_exists_when_on_schema_search_path
+ def test_data_source_exists_when_on_schema_search_path
with_schema_search_path(SCHEMA_NAME) do
- assert(@connection.table_exists?(TABLE_NAME), "table should exist and be found")
+ assert(@connection.data_source_exists?(TABLE_NAME), "data_source should exist and be found")
end
end
- def test_table_exists_when_not_on_schema_search_path
- with_schema_search_path('PUBLIC') do
- assert(!@connection.table_exists?(TABLE_NAME), "table exists but should not be found")
+ def test_data_source_exists_when_not_on_schema_search_path
+ with_schema_search_path("PUBLIC") do
+ assert_not(@connection.data_source_exists?(TABLE_NAME), "data_source exists but should not be found")
end
end
- def test_table_exists_wrong_schema
- assert(!@connection.table_exists?("foo.things"), "table should not exist")
+ def test_data_source_exists_wrong_schema
+ assert_not(@connection.data_source_exists?("foo.things"), "data_source should not exist")
end
- def test_table_exists_quoted_names
+ def test_data_source_exists_quoted_names
[ %("#{SCHEMA_NAME}"."#{TABLE_NAME}"), %(#{SCHEMA_NAME}."#{TABLE_NAME}"), %(#{SCHEMA_NAME}."#{TABLE_NAME}")].each do |given|
- assert(@connection.table_exists?(given), "table should exist when specified as #{given}")
+ assert(@connection.data_source_exists?(given), "data_source should exist when specified as #{given}")
end
with_schema_search_path(SCHEMA_NAME) do
given = %("#{TABLE_NAME}")
- assert(@connection.table_exists?(given), "table should exist when specified as #{given}")
+ assert(@connection.data_source_exists?(given), "data_source should exist when specified as #{given}")
end
end
- def test_table_exists_quoted_table
+ def test_data_source_exists_quoted_table
with_schema_search_path(SCHEMA_NAME) do
- assert(@connection.table_exists?('"things.table"'), "table should exist")
+ assert(@connection.data_source_exists?('"things.table"'), "data_source should exist")
end
end
@@ -221,9 +249,9 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase
end
def test_proper_encoding_of_table_name
- assert_equal '"table_name"', @connection.quote_table_name('table_name')
+ assert_equal '"table_name"', @connection.quote_table_name("table_name")
assert_equal '"table.name"', @connection.quote_table_name('"table.name"')
- assert_equal '"schema_name"."table_name"', @connection.quote_table_name('schema_name.table_name')
+ assert_equal '"schema_name"."table_name"', @connection.quote_table_name("schema_name.table_name")
assert_equal '"schema_name"."table.name"', @connection.quote_table_name('schema_name."table.name"')
assert_equal '"schema.name"."table_name"', @connection.quote_table_name('"schema.name".table_name')
assert_equal '"schema.name"."table.name"', @connection.quote_table_name('"schema.name"."table.name"')
@@ -235,25 +263,25 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase
assert_equal 0, Thing3.count
assert_equal 0, Thing4.count
- Thing1.create(:id => 1, :name => "thing1", :email => "thing1@localhost", :moment => Time.now)
+ Thing1.create(id: 1, name: "thing1", email: "thing1@localhost", moment: Time.now)
assert_equal 1, Thing1.count
assert_equal 0, Thing2.count
assert_equal 0, Thing3.count
assert_equal 0, Thing4.count
- Thing2.create(:id => 1, :name => "thing1", :email => "thing1@localhost", :moment => Time.now)
+ Thing2.create(id: 1, name: "thing1", email: "thing1@localhost", moment: Time.now)
assert_equal 1, Thing1.count
assert_equal 1, Thing2.count
assert_equal 0, Thing3.count
assert_equal 0, Thing4.count
- Thing3.create(:id => 1, :name => "thing1", :email => "thing1@localhost", :moment => Time.now)
+ Thing3.create(id: 1, name: "thing1", email: "thing1@localhost", moment: Time.now)
assert_equal 1, Thing1.count
assert_equal 1, Thing2.count
assert_equal 1, Thing3.count
assert_equal 0, Thing4.count
- Thing4.create(:id => 1, :name => "thing1", :email => "thing1@localhost", :moment => Time.now)
+ Thing4.create(id: 1, name: "thing1", email: "thing1@localhost", moment: Time.now)
assert_equal 1, Thing1.count
assert_equal 1, Thing2.count
assert_equal 1, Thing3.count
@@ -262,7 +290,7 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase
def test_raise_on_unquoted_schema_name
assert_raises(ActiveRecord::StatementInvalid) do
- with_schema_search_path '$user,public'
+ with_schema_search_path "$user,public"
end
end
@@ -276,13 +304,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
@@ -298,37 +326,46 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase
do_dump_index_tests_for_schema("public, #{SCHEMA_NAME}", INDEX_A_COLUMN, INDEX_B_COLUMN_S1, INDEX_D_COLUMN, INDEX_E_COLUMN)
end
+ def test_dump_indexes_for_table_with_scheme_specified_in_name
+ indexes = @connection.indexes("#{SCHEMA_NAME}.#{TABLE_NAME}")
+ assert_equal 5, indexes.size
+ end
+
def test_with_uppercase_index_name
@connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)"
- assert_nothing_raised { @connection.remove_index! "things", "#{SCHEMA_NAME}.things_Index"}
- @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)"
with_schema_search_path SCHEMA_NAME do
- assert_nothing_raised { @connection.remove_index! "things", "things_Index"}
+ assert_nothing_raised { @connection.remove_index "things", name: "things_Index" }
end
end
+ def test_remove_index_when_schema_specified
+ @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)"
+ assert_nothing_raised { @connection.remove_index "things", name: "#{SCHEMA_NAME}.things_Index" }
+
+ @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)"
+ assert_nothing_raised { @connection.remove_index "#{SCHEMA_NAME}.things", name: "things_Index" }
+
+ @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)"
+ assert_nothing_raised { @connection.remove_index "#{SCHEMA_NAME}.things", name: "#{SCHEMA_NAME}.things_Index" }
+
+ @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)"
+ assert_raises(ArgumentError) { @connection.remove_index "#{SCHEMA2_NAME}.things", name: "#{SCHEMA_NAME}.things_Index" }
+ end
+
def test_primary_key_with_schema_specified
[
%("#{SCHEMA_NAME}"."#{PK_TABLE_NAME}"),
%(#{SCHEMA_NAME}."#{PK_TABLE_NAME}"),
%(#{SCHEMA_NAME}.#{PK_TABLE_NAME})
].each do |given|
- assert_equal 'id', @connection.primary_key(given), "primary key should be found when table referenced as #{given}"
+ assert_equal "id", @connection.primary_key(given), "primary key should be found when table referenced as #{given}"
end
end
def test_primary_key_assuming_schema_search_path
- with_schema_search_path(SCHEMA_NAME) do
- assert_equal 'id', @connection.primary_key(PK_TABLE_NAME), "primary key should be found"
- 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
+ with_schema_search_path("#{SCHEMA_NAME}, #{SCHEMA2_NAME}") do
+ assert_equal "id", @connection.primary_key(PK_TABLE_NAME), "primary key should be found"
end
end
@@ -339,19 +376,19 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase
%("#{SCHEMA_NAME}"."#{UNMATCHED_PK_TABLE_NAME}")
].each do |given|
pk, seq = @connection.pk_and_sequence_for(given)
- assert_equal 'id', pk, "primary key should be found when table referenced as #{given}"
+ assert_equal "id", pk, "primary key should be found when table referenced as #{given}"
assert_equal pg_name.new(SCHEMA_NAME, "#{PK_TABLE_NAME}_id_seq"), seq, "sequence name should be found when table referenced as #{given}" if given == %("#{SCHEMA_NAME}"."#{PK_TABLE_NAME}")
- assert_equal pg_name.new(SCHEMA_NAME, UNMATCHED_SEQUENCE_NAME), seq, "sequence name should be found when table referenced as #{given}" if given == %("#{SCHEMA_NAME}"."#{UNMATCHED_PK_TABLE_NAME}")
+ assert_equal pg_name.new(SCHEMA_NAME, UNMATCHED_SEQUENCE_NAME), seq, "sequence name should be found when table referenced as #{given}" if given == %("#{SCHEMA_NAME}"."#{UNMATCHED_PK_TABLE_NAME}")
end
end
def test_current_schema
{
- %('$user',public) => 'public',
+ %('$user',public) => "public",
SCHEMA_NAME => SCHEMA_NAME,
%(#{SCHEMA2_NAME},#{SCHEMA_NAME},public) => SCHEMA2_NAME,
- %(public,#{SCHEMA2_NAME},#{SCHEMA_NAME}) => 'public'
- }.each do |given,expect|
+ %(public,#{SCHEMA2_NAME},#{SCHEMA_NAME}) => "public"
+ }.each do |given, expect|
with_schema_search_path(given) { assert_equal expect, @connection.current_schema }
end
end
@@ -359,7 +396,7 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase
def test_prepared_statements_with_multiple_schemas
[SCHEMA_NAME, SCHEMA2_NAME].each do |schema_name|
with_schema_search_path schema_name do
- Thing5.create(:id => 1, :name => "thing inside #{SCHEMA_NAME}", :email => "thing1@localhost", :moment => Time.now)
+ Thing5.create(id: 1, name: "thing inside #{SCHEMA_NAME}", email: "thing1@localhost", moment: Time.now)
end
end
@@ -372,11 +409,11 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase
def test_schema_exists?
{
- 'public' => true,
+ "public" => true,
SCHEMA_NAME => true,
SCHEMA2_NAME => true,
- 'darkside' => false
- }.each do |given,expect|
+ "darkside" => false
+ }.each do |given, expect|
assert_equal expect, @connection.schema_exists?(given)
end
end
@@ -400,32 +437,29 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase
private
def columns(table_name)
@connection.send(:column_definitions, table_name).map do |name, type, default|
- "#{name} #{type}" + (default ? " default #{default}" : '')
+ "#{name} #{type}" + (default ? " default #{default}" : "")
end
end
- def with_schema_search_path(schema_search_path)
- @connection.schema_search_path = schema_search_path
- yield if block_given?
- ensure
- @connection.schema_search_path = "'$user', public"
- end
-
def do_dump_index_tests_for_schema(this_schema_name, first_index_column_name, second_index_column_name, third_index_column_name, fourth_index_column_name)
with_schema_search_path(this_schema_name) do
indexes = @connection.indexes(TABLE_NAME).sort_by(&:name)
- assert_equal 4,indexes.size
-
- do_dump_index_assertions_for_one_index(indexes[0], INDEX_A_NAME, first_index_column_name)
- do_dump_index_assertions_for_one_index(indexes[1], INDEX_B_NAME, second_index_column_name)
- do_dump_index_assertions_for_one_index(indexes[2], INDEX_D_NAME, third_index_column_name)
- do_dump_index_assertions_for_one_index(indexes[3], INDEX_E_NAME, fourth_index_column_name)
-
- indexes.select{|i| i.name != INDEX_E_NAME}.each do |index|
- assert_equal :btree, index.using
- end
- assert_equal :gin, indexes.select{|i| i.name == INDEX_E_NAME}[0].using
- assert_equal :desc, indexes.select{|i| i.name == INDEX_D_NAME}[0].orders[INDEX_D_COLUMN]
+ assert_equal 5, indexes.size
+
+ index_a, index_b, index_c, index_d, index_e = indexes
+
+ do_dump_index_assertions_for_one_index(index_a, INDEX_A_NAME, first_index_column_name)
+ do_dump_index_assertions_for_one_index(index_b, INDEX_B_NAME, second_index_column_name)
+ do_dump_index_assertions_for_one_index(index_d, INDEX_D_NAME, third_index_column_name)
+ do_dump_index_assertions_for_one_index(index_e, INDEX_E_NAME, fourth_index_column_name)
+
+ assert_equal :btree, index_a.using
+ assert_equal :btree, index_b.using
+ assert_equal :gin, index_c.using
+ assert_equal :btree, index_d.using
+ assert_equal :gin, index_e.using
+
+ assert_equal :desc, index_d.orders
end
end
@@ -462,14 +496,74 @@ class SchemaForeignKeyTest < ActiveRecord::PostgreSQLTestCase
ensure
@connection.drop_table "wagons", if_exists: true
@connection.drop_table "my_schema.trains", if_exists: true
- @connection.execute "DROP SCHEMA IF EXISTS my_schema"
+ @connection.drop_schema "my_schema", if_exists: true
+ end
+end
+
+class SchemaIndexOpclassTest < ActiveRecord::PostgreSQLTestCase
+ include SchemaDumpingHelper
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table "trains" do |t|
+ t.string :name
+ t.text :description
+ end
+ end
+
+ teardown do
+ @connection.drop_table "trains", if_exists: true
+ end
+
+ def test_string_opclass_is_dumped
+ @connection.execute "CREATE INDEX trains_name_and_description ON trains USING btree(name text_pattern_ops, description text_pattern_ops)"
+
+ output = dump_table_schema "trains"
+
+ assert_match(/opclass: :text_pattern_ops/, output)
+ end
+
+ def test_non_default_opclass_is_dumped
+ @connection.execute "CREATE INDEX trains_name_and_description ON trains USING btree(name, description text_pattern_ops)"
+
+ output = dump_table_schema "trains"
+
+ assert_match(/opclass: \{ description: :text_pattern_ops \}/, output)
+ end
+end
+
+class SchemaIndexNullsOrderTest < ActiveRecord::PostgreSQLTestCase
+ include SchemaDumpingHelper
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table "trains" do |t|
+ t.string :name
+ t.text :description
+ end
+ end
+
+ teardown do
+ @connection.drop_table "trains", if_exists: true
+ end
+
+ def test_nulls_order_is_dumped
+ @connection.execute "CREATE INDEX trains_name_and_description ON trains USING btree(name NULLS FIRST, description)"
+ output = dump_table_schema "trains"
+ assert_match(/order: \{ name: "NULLS FIRST" \}/, output)
+ end
+
+ def test_non_default_order_with_nulls_is_dumped
+ @connection.execute "CREATE INDEX trains_name_and_desc ON trains USING btree(name DESC NULLS LAST, description)"
+ output = dump_table_schema "trains"
+ assert_match(/order: \{ name: "DESC NULLS LAST" \}/, output)
end
end
class DefaultsUsingMultipleSchemasAndDomainTest < ActiveRecord::PostgreSQLTestCase
setup do
@connection = ActiveRecord::Base.connection
- @connection.execute "DROP SCHEMA IF EXISTS schema_1 CASCADE"
+ @connection.drop_schema "schema_1", if_exists: true
@connection.execute "CREATE SCHEMA schema_1"
@connection.execute "CREATE DOMAIN schema_1.text AS text"
@connection.execute "CREATE DOMAIN schema_1.varchar AS varchar"
@@ -487,7 +581,7 @@ class DefaultsUsingMultipleSchemasAndDomainTest < ActiveRecord::PostgreSQLTestCa
teardown do
@connection.schema_search_path = @old_search_path
- @connection.execute "DROP SCHEMA IF EXISTS schema_1 CASCADE"
+ @connection.drop_schema "schema_1", if_exists: true
Default.reset_column_information
end
@@ -500,7 +594,7 @@ class DefaultsUsingMultipleSchemasAndDomainTest < ActiveRecord::PostgreSQLTestCa
end
def test_decimal_defaults_in_new_schema_when_overriding_domain
- assert_equal BigDecimal.new("3.14159265358979323846"), Default.new.decimal_col, "Default of decimal column was not correctly parsed"
+ assert_equal BigDecimal("3.14159265358979323846"), Default.new.decimal_col, "Default of decimal column was not correctly parsed"
end
def test_bpchar_defaults_in_new_schema_when_overriding_domain
@@ -519,3 +613,40 @@ class DefaultsUsingMultipleSchemasAndDomainTest < ActiveRecord::PostgreSQLTestCa
assert_equal "foo'::bar", Default.new.string_col
end
end
+
+class SchemaWithDotsTest < ActiveRecord::PostgreSQLTestCase
+ include PGSchemaHelper
+ self.use_transactional_tests = false
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_schema "my.schema"
+ end
+
+ teardown do
+ @connection.drop_schema "my.schema", if_exists: true
+ end
+
+ test "rename_table" do
+ with_schema_search_path('"my.schema"') do
+ @connection.create_table :posts
+ @connection.rename_table :posts, :articles
+ assert_equal ["articles"], @connection.tables
+ end
+ end
+
+ test "Active Record basics" do
+ with_schema_search_path('"my.schema"') do
+ @connection.create_table :articles do |t|
+ t.string :title
+ end
+ article_class = Class.new(ActiveRecord::Base) do
+ self.table_name = '"my.schema".articles'
+ end
+
+ article_class.create!(title: "zOMG, welcome to my blorgh!")
+ welcome_article = article_class.last
+ assert_equal "zOMG, welcome to my blorgh!", welcome_article.title
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/serial_test.rb b/activerecord/test/cases/adapters/postgresql/serial_test.rb
index 7d30db247b..83ea86be6d 100644
--- a/activerecord/test/cases/adapters/postgresql/serial_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/serial_test.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'support/schema_dumping_helper'
+require "support/schema_dumping_helper"
class PostgresqlSerialTest < ActiveRecord::PostgreSQLTestCase
include SchemaDumpingHelper
@@ -10,6 +12,7 @@ class PostgresqlSerialTest < ActiveRecord::PostgreSQLTestCase
@connection = ActiveRecord::Base.connection
@connection.create_table "postgresql_serials", force: true do |t|
t.serial :seq
+ t.integer :serials_id, default: -> { "nextval('postgresql_serials_id_seq')" }
end
end
@@ -21,12 +24,24 @@ class PostgresqlSerialTest < ActiveRecord::PostgreSQLTestCase
column = PostgresqlSerial.columns_hash["seq"]
assert_equal :integer, column.type
assert_equal "integer", column.sql_type
- assert column.serial?
+ assert_predicate column, :serial?
+ end
+
+ def test_not_serial_column
+ column = PostgresqlSerial.columns_hash["serials_id"]
+ assert_equal :integer, column.type
+ assert_equal "integer", column.sql_type
+ assert_not_predicate column, :serial?
end
def test_schema_dump_with_shorthand
output = dump_table_schema "postgresql_serials"
- assert_match %r{t\.serial\s+"seq"}, output
+ assert_match %r{t\.serial\s+"seq",\s+null: false$}, output
+ end
+
+ def test_schema_dump_with_not_serial
+ output = dump_table_schema "postgresql_serials"
+ assert_match %r{t\.integer\s+"serials_id",\s+default: -> \{ "nextval\('postgresql_serials_id_seq'::regclass\)" \}$}, output
end
end
@@ -39,6 +54,7 @@ class PostgresqlBigSerialTest < ActiveRecord::PostgreSQLTestCase
@connection = ActiveRecord::Base.connection
@connection.create_table "postgresql_big_serials", force: true do |t|
t.bigserial :seq
+ t.bigint :serials_id, default: -> { "nextval('postgresql_big_serials_id_seq')" }
end
end
@@ -50,11 +66,91 @@ class PostgresqlBigSerialTest < ActiveRecord::PostgreSQLTestCase
column = PostgresqlBigSerial.columns_hash["seq"]
assert_equal :integer, column.type
assert_equal "bigint", column.sql_type
- assert column.serial?
+ assert_predicate column, :serial?
+ end
+
+ def test_not_bigserial_column
+ column = PostgresqlBigSerial.columns_hash["serials_id"]
+ assert_equal :integer, column.type
+ assert_equal "bigint", column.sql_type
+ assert_not_predicate column, :serial?
end
def test_schema_dump_with_shorthand
output = dump_table_schema "postgresql_big_serials"
- assert_match %r{t\.bigserial\s+"seq"}, output
+ assert_match %r{t\.bigserial\s+"seq",\s+null: false$}, output
+ end
+
+ def test_schema_dump_with_not_bigserial
+ output = dump_table_schema "postgresql_big_serials"
+ assert_match %r{t\.bigint\s+"serials_id",\s+default: -> \{ "nextval\('postgresql_big_serials_id_seq'::regclass\)" \}$}, output
+ end
+end
+
+module SequenceNameDetectionTestCases
+ class CollidedSequenceNameTest < ActiveRecord::PostgreSQLTestCase
+ include SchemaDumpingHelper
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table :foo_bar, force: true do |t|
+ t.serial :baz_id
+ end
+ @connection.create_table :foo, force: true do |t|
+ t.serial :bar_id
+ t.bigserial :bar_baz_id
+ end
+ end
+
+ def teardown
+ @connection.drop_table :foo_bar, if_exists: true
+ @connection.drop_table :foo, if_exists: true
+ end
+
+ def test_serial_columns
+ columns = @connection.columns(:foo)
+ columns.each do |column|
+ assert_equal :integer, column.type
+ assert_predicate column, :serial?
+ end
+ end
+
+ def test_schema_dump_with_collided_sequence_name
+ output = dump_table_schema "foo"
+ assert_match %r{t\.serial\s+"bar_id",\s+null: false$}, output
+ assert_match %r{t\.bigserial\s+"bar_baz_id",\s+null: false$}, output
+ end
+ end
+
+ class LongerSequenceNameDetectionTest < ActiveRecord::PostgreSQLTestCase
+ include SchemaDumpingHelper
+
+ def setup
+ @table_name = "long_table_name_to_test_sequence_name_detection_for_serial_cols"
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table @table_name, force: true do |t|
+ t.serial :seq
+ t.bigserial :bigseq
+ end
+ end
+
+ def teardown
+ @connection.drop_table @table_name, if_exists: true
+ end
+
+ def test_serial_columns
+ columns = @connection.columns(@table_name)
+ columns.each do |column|
+ assert_equal :integer, column.type
+ assert_predicate column, :serial?
+ end
+ end
+
+ def test_schema_dump_with_long_table_name
+ output = dump_table_schema @table_name
+ assert_match %r{create_table "#{@table_name}", force: :cascade}, output
+ assert_match %r{t\.serial\s+"seq",\s+null: false$}, output
+ assert_match %r{t\.bigserial\s+"bigseq",\s+null: false$}, output
+ end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb
index 5aab246c99..fef4b02b04 100644
--- a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb
@@ -1,15 +1,17 @@
-require 'cases/helper'
+# frozen_string_literal: true
+
+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
@@ -17,22 +19,22 @@ module ActiveRecord
if Process.respond_to?(:fork)
def test_cache_is_per_pid
cache = StatementPool.new nil, 10
- cache['foo'] = 'bar'
- assert_equal 'bar', cache['foo']
+ cache["foo"] = "bar"
+ assert_equal "bar", cache["foo"]
pid = fork {
- lookup = cache['foo'];
+ lookup = cache["foo"]
exit!(!lookup)
}
Process.waitpid pid
- assert $?.success?, 'process should exit successfully'
+ assert $?.success?, "process should exit successfully"
end
end
def test_dealloc_does_not_raise_on_inactive_connection
- cache = StatementPool.new InactivePGconn.new, 10
- cache['foo'] = 'bar'
+ cache = StatementPool.new InactivePgConnection.new, 10
+ cache["foo"] = "bar"
assert_nothing_raised { cache.clear }
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb
index 4c4866b46b..b7f213efc8 100644
--- a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb
@@ -1,6 +1,8 @@
-require 'cases/helper'
-require 'models/developer'
-require 'models/topic'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/developer"
+require "models/topic"
class PostgresqlTimestampTest < ActiveRecord::PostgreSQLTestCase
class PostgresqlTimestampWithZone < ActiveRecord::Base; end
@@ -21,7 +23,7 @@ class PostgresqlTimestampTest < ActiveRecord::PostgreSQLTestCase
@connection.reconnect!
timestamp = PostgresqlTimestampWithZone.find(1)
- assert_equal Time.utc(2010,1,1, 11,0,0), timestamp.time
+ assert_equal Time.utc(2010, 1, 1, 11, 0, 0), timestamp.time
assert_instance_of Time, timestamp.time
end
ensure
@@ -32,10 +34,10 @@ class PostgresqlTimestampTest < ActiveRecord::PostgreSQLTestCase
with_timezone_config default: :local, aware_attributes: false do
@connection.reconnect!
# make sure to use a non-UTC time zone
- @connection.execute("SET time zone 'America/Jamaica'", 'SCHEMA')
+ @connection.execute("SET time zone 'America/Jamaica'", "SCHEMA")
timestamp = PostgresqlTimestampWithZone.find(1)
- assert_equal Time.utc(2010,1,1, 11,0,0), timestamp.time
+ assert_equal Time.utc(2010, 1, 1, 11, 0, 0), timestamp.time
assert_instance_of Time, timestamp.time
end
ensure
@@ -54,37 +56,37 @@ class PostgresqlTimestampFixtureTest < ActiveRecord::PostgreSQLTestCase
def test_load_infinity_and_beyond
d = Developer.find_by_sql("select 'infinity'::timestamp as updated_at")
- assert d.first.updated_at.infinite?, 'timestamp should be infinite'
+ assert d.first.updated_at.infinite?, "timestamp should be infinite"
d = Developer.find_by_sql("select '-infinity'::timestamp as updated_at")
time = d.first.updated_at
- assert time.infinite?, 'timestamp should be infinite'
+ assert time.infinite?, "timestamp should be infinite"
assert_operator time, :<, 0
end
def test_save_infinity_and_beyond
- d = Developer.create!(:name => 'aaron', :updated_at => 1.0 / 0.0)
+ d = Developer.create!(name: "aaron", updated_at: 1.0 / 0.0)
assert_equal(1.0 / 0.0, d.updated_at)
- d = Developer.create!(:name => 'aaron', :updated_at => -1.0 / 0.0)
+ d = Developer.create!(name: "aaron", updated_at: -1.0 / 0.0)
assert_equal(-1.0 / 0.0, d.updated_at)
end
def test_bc_timestamp
date = Date.new(0) - 1.week
- Developer.create!(:name => "aaron", :updated_at => date)
+ Developer.create!(name: "aaron", updated_at: date)
assert_equal date, Developer.find_by_name("aaron").updated_at
end
def test_bc_timestamp_leap_year
date = Time.utc(-4, 2, 29)
- Developer.create!(:name => "taihou", :updated_at => date)
+ Developer.create!(name: "taihou", updated_at: date)
assert_equal date, Developer.find_by_name("taihou").updated_at
end
def test_bc_timestamp_year_zero
date = Time.utc(0, 4, 7)
- Developer.create!(:name => "yahagi", :updated_at => date)
+ Developer.create!(name: "yahagi", updated_at: date)
assert_equal date, Developer.find_by_name("yahagi").updated_at
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/transaction_test.rb b/activerecord/test/cases/adapters/postgresql/transaction_test.rb
new file mode 100644
index 0000000000..984b2f5ea4
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/transaction_test.rb
@@ -0,0 +1,190 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "support/connection_helper"
+require "concurrent/atomic/cyclic_barrier"
+
+module ActiveRecord
+ class PostgresqlTransactionTest < ActiveRecord::PostgreSQLTestCase
+ self.use_transactional_tests = false
+
+ class Sample < ActiveRecord::Base
+ self.table_name = "samples"
+ end
+
+ setup do
+ @abort, Thread.abort_on_exception = Thread.abort_on_exception, false
+ Thread.report_on_exception, @original_report_on_exception = false, Thread.report_on_exception
+
+ @connection = ActiveRecord::Base.connection
+
+ @connection.transaction do
+ @connection.drop_table "samples", if_exists: true
+ @connection.create_table("samples") do |t|
+ t.integer "value"
+ end
+ end
+
+ Sample.reset_column_information
+ end
+
+ teardown do
+ @connection.drop_table "samples", if_exists: true
+
+ Thread.abort_on_exception = @abort
+ Thread.report_on_exception = @original_report_on_exception
+ end
+
+ test "raises SerializationFailure when a serialization failure occurs" do
+ assert_raises(ActiveRecord::SerializationFailure) do
+ before = Concurrent::CyclicBarrier.new(2)
+ after = Concurrent::CyclicBarrier.new(2)
+
+ thread = Thread.new do
+ with_warning_suppression do
+ Sample.transaction isolation: :serializable do
+ before.wait
+ Sample.create value: Sample.sum(:value)
+ after.wait
+ end
+ end
+ end
+
+ begin
+ with_warning_suppression do
+ Sample.transaction isolation: :serializable do
+ before.wait
+ Sample.create value: Sample.sum(:value)
+ after.wait
+ end
+ end
+ ensure
+ thread.join
+ end
+ end
+ end
+
+ test "raises Deadlocked when a deadlock is encountered" do
+ with_warning_suppression 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!
+ barrier.wait
+ s2.update value: 1
+ end
+ end
+
+ begin
+ Sample.transaction do
+ s2.lock!
+ barrier.wait
+ s1.update value: 2
+ end
+ ensure
+ thread.join
+ end
+ end
+ end
+ end
+
+ test "raises LockWaitTimeout when lock wait timeout exceeded" do
+ skip unless ActiveRecord::Base.connection.postgresql_version >= 90300
+ assert_raises(ActiveRecord::LockWaitTimeout) do
+ s = Sample.create!(value: 1)
+ latch1 = Concurrent::CountDownLatch.new
+ latch2 = Concurrent::CountDownLatch.new
+
+ thread = Thread.new do
+ Sample.transaction do
+ Sample.lock.find(s.id)
+ latch1.count_down
+ latch2.wait
+ end
+ end
+
+ begin
+ Sample.transaction do
+ latch1.wait
+ Sample.connection.execute("SET lock_timeout = 1")
+ Sample.lock.find(s.id)
+ end
+ ensure
+ Sample.connection.execute("SET lock_timeout = DEFAULT")
+ latch2.count_down
+ thread.join
+ end
+ end
+ end
+
+ test "raises QueryCanceled when statement timeout exceeded" do
+ assert_raises(ActiveRecord::QueryCanceled) do
+ s = Sample.create!(value: 1)
+ latch1 = Concurrent::CountDownLatch.new
+ latch2 = Concurrent::CountDownLatch.new
+
+ thread = Thread.new do
+ Sample.transaction do
+ Sample.lock.find(s.id)
+ latch1.count_down
+ latch2.wait
+ end
+ end
+
+ begin
+ Sample.transaction do
+ latch1.wait
+ Sample.connection.execute("SET statement_timeout = 1")
+ Sample.lock.find(s.id)
+ end
+ ensure
+ Sample.connection.execute("SET statement_timeout = DEFAULT")
+ latch2.count_down
+ thread.join
+ end
+ end
+ end
+
+ test "raises QueryCanceled when canceling statement due to user request" do
+ assert_raises(ActiveRecord::QueryCanceled) do
+ s = Sample.create!(value: 1)
+ latch = Concurrent::CountDownLatch.new
+
+ thread = Thread.new do
+ Sample.transaction do
+ Sample.lock.find(s.id)
+ latch.count_down
+ sleep(0.5)
+ conn = Sample.connection
+ pid = conn.query_value("SELECT pid FROM pg_stat_activity WHERE query LIKE '% FOR UPDATE'")
+ conn.execute("SELECT pg_cancel_backend(#{pid})")
+ end
+ end
+
+ begin
+ Sample.transaction do
+ latch.wait
+ Sample.lock.find(s.id)
+ end
+ ensure
+ thread.join
+ end
+ end
+ end
+
+ private
+
+ def with_warning_suppression
+ log_level = ActiveRecord::Base.connection.client_min_messages
+ ActiveRecord::Base.connection.client_min_messages = "error"
+ yield
+ ensure
+ ActiveRecord::Base.connection.client_min_messages = log_level
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb
index 77a99ca778..8212ed4263 100644
--- a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb
@@ -1,4 +1,6 @@
-require 'cases/helper'
+# frozen_string_literal: true
+
+require "cases/helper"
class PostgresqlTypeLookupTest < ActiveRecord::PostgreSQLTestCase
setup do
@@ -6,28 +8,28 @@ class PostgresqlTypeLookupTest < ActiveRecord::PostgreSQLTestCase
end
test "array delimiters are looked up correctly" do
- box_array = @connection.type_map.lookup(1020)
- int_array = @connection.type_map.lookup(1007)
+ box_array = @connection.send(:type_map).lookup(1020)
+ int_array = @connection.send(:type_map).lookup(1007)
- assert_equal ';', box_array.delimiter
- assert_equal ',', int_array.delimiter
+ assert_equal ";", box_array.delimiter
+ assert_equal ",", int_array.delimiter
end
test "array types correctly respect registration of subtypes" do
- int_array = @connection.type_map.lookup(1007, -1, "integer[]")
- bigint_array = @connection.type_map.lookup(1016, -1, "bigint[]")
+ int_array = @connection.send(:type_map).lookup(1007, -1, "integer[]")
+ bigint_array = @connection.send(:type_map).lookup(1016, -1, "bigint[]")
big_array = [123456789123456789]
- assert_raises(RangeError) { int_array.serialize(big_array) }
- assert_equal "{123456789123456789}", bigint_array.serialize(big_array)
+ assert_raises(ActiveModel::RangeError) { int_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
- int_range = @connection.type_map.lookup(3904, -1, "int4range")
- bigint_range = @connection.type_map.lookup(3926, -1, "int8range")
+ int_range = @connection.send(:type_map).lookup(3904, -1, "int4range")
+ bigint_range = @connection.send(:type_map).lookup(3926, -1, "int8range")
big_range = 0..123456789123456789
- assert_raises(RangeError) { int_range.serialize(big_range) }
- assert_equal "[0,123456789123456789]", bigint_range.serialize(big_range)
+ assert_raises(ActiveModel::RangeError) { int_range.serialize(big_range) }
+ assert_equal "[0,123456789123456789]", @connection.type_cast(bigint_range.serialize(big_range))
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/utils_test.rb b/activerecord/test/cases/adapters/postgresql/utils_test.rb
index 095c1826e5..c91884f384 100644
--- a/activerecord/test/cases/adapters/postgresql/utils_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/utils_test.rb
@@ -1,5 +1,7 @@
-require 'cases/helper'
-require 'active_record/connection_adapters/postgresql/utils'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "active_record/connection_adapters/postgresql/utils"
class PostgreSQLUtilsTest < ActiveRecord::PostgreSQLTestCase
Name = ActiveRecord::ConnectionAdapters::PostgreSQL::Name
@@ -7,14 +9,14 @@ class PostgreSQLUtilsTest < ActiveRecord::PostgreSQLTestCase
def test_extract_schema_qualified_name
{
- %(table_name) => [nil,'table_name'],
- %("table.name") => [nil,'table.name'],
+ %(table_name) => [nil, "table_name"],
+ %("table.name") => [nil, "table.name"],
%(schema.table_name) => %w{schema table_name},
%("schema".table_name) => %w{schema table_name},
%(schema."table_name") => %w{schema table_name},
%("schema"."table_name") => %w{schema table_name},
- %("even spaces".table) => ['even spaces','table'],
- %(schema."table.name") => ['schema', 'table.name']
+ %("even spaces".table) => ["even spaces", "table"],
+ %(schema."table.name") => ["schema", "table.name"]
}.each do |given, expect|
assert_equal Name.new(*expect), extract_schema_qualified_name(given)
end
@@ -54,9 +56,9 @@ class PostgreSQLNameTest < ActiveRecord::PostgreSQLTestCase
end
test "can be used as hash key" do
- hash = {Name.new("schema", "article_seq") => "success"}
+ 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 7127d69e9e..71d07e2f4c 100644
--- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'support/schema_dumping_helper'
+require "support/schema_dumping_helper"
module PostgresqlUUIDHelper
def connection
@@ -9,6 +11,14 @@ 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
+
+ def uuid_default
+ connection.supports_pgcrypto_uuid? ? {} : { default: uuid_function }
+ end
end
class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase
@@ -20,8 +30,11 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase
end
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'
+ t.uuid "guid"
end
end
@@ -29,72 +42,106 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase
drop_table "uuid_data_type"
end
+ if ActiveRecord::Base.connection.respond_to?(:supports_pgcrypto_uuid?) &&
+ 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']
+ 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']
+ column = UUIDType.columns_hash["thingy"]
assert_equal "uuid_generate_v4()", column.default_function
ensure
UUIDType.reset_column_information
end
+ def test_add_column_with_null_true_and_default_nil
+ connection.add_column :uuid_data_type, :thingy, :uuid, null: true, default: nil
+
+ UUIDType.reset_column_information
+ column = UUIDType.columns_hash["thingy"]
+
+ assert column.null
+ assert_nil column.default
+ end
+
+ def test_add_column_with_default_array
+ connection.add_column :uuid_data_type, :thingy, :uuid, array: true, default: []
+
+ UUIDType.reset_column_information
+ column = UUIDType.columns_hash["thingy"]
+
+ assert_predicate column, :array?
+ assert_equal "{}", column.default
+
+ schema = dump_table_schema "uuid_data_type"
+ assert_match %r{t\.uuid "thingy", default: \[\], array: true$}, schema
+ end
+
def test_data_type_of_uuid_types
column = UUIDType.columns_hash["guid"]
assert_equal :uuid, column.type
assert_equal "uuid", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = UUIDType.type_for_attribute("guid")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_treat_blank_uuid_as_nil
- UUIDType.create! guid: ''
- assert_equal(nil, UUIDType.last.guid)
+ UUIDType.create! guid: ""
+ assert_nil(UUIDType.last.guid)
end
def test_treat_invalid_uuid_as_nil
- uuid = UUIDType.create! guid: 'foobar'
- assert_equal(nil, uuid.guid)
+ uuid = UUIDType.create! guid: "foobar"
+ assert_nil(uuid.guid)
end
def test_invalid_uuid_dont_modify_before_type_cast
- uuid = UUIDType.new guid: 'foobar'
- assert_equal 'foobar', uuid.guid_before_type_cast
+ uuid = UUIDType.new guid: "foobar"
+ assert_equal "foobar", uuid.guid_before_type_cast
end
def test_acceptable_uuid_regex
# Valid uuids
- ['A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11',
- '{a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11}',
- 'a0eebc999c0b4ef8bb6d6bb9bd380a11',
- 'a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11',
- '{a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11}',
+ ["A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11",
+ "{a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11}",
+ "a0eebc999c0b4ef8bb6d6bb9bd380a11",
+ "a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11",
+ "{a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11}",
# The following is not a valid RFC 4122 UUID, but PG doesn't seem to care,
# so we shouldn't block it either. (Pay attention to "fb6d" – the "f" here
# is invalid – it must be one of 8, 9, A, B, a, b according to the spec.)
- '{a0eebc99-9c0b-4ef8-fb6d-6bb9bd380a11}',
+ "{a0eebc99-9c0b-4ef8-fb6d-6bb9bd380a11}",
].each do |valid_uuid|
uuid = UUIDType.new guid: valid_uuid
assert_not_nil uuid.guid
end
# Invalid uuids
- [['A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11'],
+ [["A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11"],
Hash.new,
0,
0.0,
true,
- 'Z0000C99-9C0B-4EF8-BB6D-6BB9BD380A11',
- 'a0eebc999r0b4ef8ab6d6bb9bd380a11',
- 'a0ee-bc99------4ef8-bb6d-6bb9-bd38-0a11',
- '{a0eebc99-bb6d6bb9-bd380a11}'].each do |invalid_uuid|
+ "Z0000C99-9C0B-4EF8-BB6D-6BB9BD380A11",
+ "a0eebc999r0b4ef8ab6d6bb9bd380a11",
+ "a0ee-bc99------4ef8-bb6d-6bb9-bd38-0a11",
+ "{a0eebc99-bb6d6bb9-bd380a11}",
+ "{a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11",
+ "a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11}"].each do |invalid_uuid|
uuid = UUIDType.new guid: invalid_uuid
assert_nil uuid.guid
end
@@ -131,7 +178,7 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase
duplicate = klass.new(guid: record.guid)
assert record.guid.present? # Ensure we actually are testing a UUID
- assert_not duplicate.valid?
+ assert_not_predicate duplicate, :valid?
end
end
@@ -140,74 +187,102 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::PostgreSQLTestCase
include SchemaDumpingHelper
class UUID < ActiveRecord::Base
- self.table_name = 'pg_uuids'
+ self.table_name = "pg_uuids"
end
setup do
- enable_extension!('uuid-ossp', connection)
-
- connection.create_table('pg_uuids', id: :uuid, default: 'uuid_generate_v1()') do |t|
- t.string 'name'
- t.uuid 'other_uuid', default: 'uuid_generate_v4()'
+ connection.create_table("pg_uuids", id: :uuid, default: "uuid_generate_v1()") do |t|
+ t.string "name"
+ t.uuid "other_uuid", default: "uuid_generate_v4()"
end
# Create custom PostgreSQL function to generate UUIDs
# 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
# Create such a table with custom function as default value generator
- connection.create_table('pg_uuids_2', id: :uuid, default: 'my_uuid_generator()') do |t|
- t.string 'name'
- t.uuid 'other_uuid_2', default: 'my_uuid_generator()'
+ connection.create_table("pg_uuids_2", id: :uuid, default: "my_uuid_generator()") do |t|
+ t.string "name"
+ t.uuid "other_uuid_2", default: "my_uuid_generator()"
+ end
+
+ connection.create_table("pg_uuids_3", id: :uuid, **uuid_default) do |t|
+ t.string "name"
end
end
teardown do
drop_table "pg_uuids"
- drop_table 'pg_uuids_2'
- connection.execute 'DROP FUNCTION IF EXISTS my_uuid_generator();'
- disable_extension!('uuid-ossp', connection)
+ drop_table "pg_uuids_2"
+ drop_table "pg_uuids_3"
+ connection.execute "DROP FUNCTION IF EXISTS my_uuid_generator();"
end
- if ActiveRecord::Base.connection.supports_extensions?
- def test_id_is_uuid
- assert_equal :uuid, UUID.columns_hash['id'].type
- assert UUID.primary_key
- end
+ def test_id_is_uuid
+ assert_equal :uuid, UUID.columns_hash["id"].type
+ assert UUID.primary_key
+ end
- def test_id_has_a_default
- u = UUID.create
- assert_not_nil u.id
- end
+ def test_id_has_a_default
+ u = UUID.create
+ assert_not_nil u.id
+ end
- def test_auto_create_uuid
- u = UUID.create
- u.reload
- assert_not_nil u.other_uuid
- end
+ def test_auto_create_uuid
+ u = UUID.create
+ u.reload
+ assert_not_nil u.other_uuid
+ end
- def test_pk_and_sequence_for_uuid_primary_key
- pk, seq = connection.pk_and_sequence_for('pg_uuids')
- assert_equal 'id', pk
- assert_equal nil, seq
- end
+ def test_pk_and_sequence_for_uuid_primary_key
+ pk, seq = connection.pk_and_sequence_for("pg_uuids")
+ assert_equal "id", pk
+ assert_nil seq
+ end
- def test_schema_dumper_for_uuid_primary_key
- schema = dump_table_schema "pg_uuids"
- assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: "uuid_generate_v1\(\)"/, schema)
- assert_match(/t\.uuid "other_uuid", default: "uuid_generate_v4\(\)"/, schema)
- end
+ def test_schema_dumper_for_uuid_primary_key
+ schema = dump_table_schema "pg_uuids"
+ assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: -> { "uuid_generate_v1\(\)" }/, schema)
+ assert_match(/t\.uuid "other_uuid", default: -> { "uuid_generate_v4\(\)" }/, schema)
+ end
+
+ def test_schema_dumper_for_uuid_primary_key_with_custom_default
+ schema = dump_table_schema "pg_uuids_2"
+ assert_match(/\bcreate_table "pg_uuids_2", id: :uuid, default: -> { "my_uuid_generator\(\)" }/, schema)
+ assert_match(/t\.uuid "other_uuid_2", default: -> { "my_uuid_generator\(\)" }/, schema)
+ end
- def test_schema_dumper_for_uuid_primary_key_with_custom_default
- schema = dump_table_schema "pg_uuids_2"
- assert_match(/\bcreate_table "pg_uuids_2", id: :uuid, default: "my_uuid_generator\(\)"/, schema)
- assert_match(/t\.uuid "other_uuid_2", default: "my_uuid_generator\(\)"/, schema)
+ def test_schema_dumper_for_uuid_primary_key_default
+ schema = dump_table_schema "pg_uuids_3"
+ if connection.supports_pgcrypto_uuid?
+ assert_match(/\bcreate_table "pg_uuids_3", id: :uuid, default: -> { "gen_random_uuid\(\)" }/, schema)
+ else
+ assert_match(/\bcreate_table "pg_uuids_3", id: :uuid, default: -> { "uuid_generate_v4\(\)" }/, schema)
end
end
+
+ def test_schema_dumper_for_uuid_primary_key_default_in_legacy_migration
+ @verbose_was = ActiveRecord::Migration.verbose
+ ActiveRecord::Migration.verbose = false
+
+ 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
class PostgresqlUUIDTestNilDefault < ActiveRecord::PostgreSQLTestCase
@@ -215,32 +290,46 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::PostgreSQLTestCase
include SchemaDumpingHelper
setup do
- enable_extension!('uuid-ossp', connection)
-
- connection.create_table('pg_uuids', id: false) do |t|
+ connection.create_table("pg_uuids", id: false) do |t|
t.primary_key :id, :uuid, default: nil
- t.string 'name'
+ t.string "name"
end
end
teardown do
drop_table "pg_uuids"
- disable_extension!('uuid-ossp', connection)
end
- if ActiveRecord::Base.connection.supports_extensions?
- def test_id_allows_default_override_via_nil
- col_desc = connection.execute("SELECT pg_get_expr(d.adbin, d.adrelid) as default
- FROM pg_attribute a
- LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum
- WHERE a.attname='id' AND a.attrelid = 'pg_uuids'::regclass").first
- assert_nil col_desc["default"]
- end
+ def test_id_allows_default_override_via_nil
+ col_desc = connection.execute("SELECT pg_get_expr(d.adbin, d.adrelid) as default
+ FROM pg_attribute a
+ LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum
+ WHERE a.attname='id' AND a.attrelid = 'pg_uuids'::regclass").first
+ assert_nil col_desc["default"]
+ end
- def test_schema_dumper_for_uuid_primary_key_with_default_override_via_nil
- schema = dump_table_schema "pg_uuids"
- assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: nil/, schema)
- end
+ def test_schema_dumper_for_uuid_primary_key_with_default_override_via_nil
+ schema = dump_table_schema "pg_uuids"
+ assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: nil/, schema)
+ end
+
+ def test_schema_dumper_for_uuid_primary_key_with_default_nil_in_legacy_migration
+ @verbose_was = ActiveRecord::Migration.verbose
+ ActiveRecord::Migration.verbose = false
+
+ migration = Class.new(ActiveRecord::Migration[5.0]) do
+ def version; 101 end
+ def migrate(x)
+ create_table("pg_uuids_4", id: :uuid, default: nil)
+ end
+ end.new
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+
+ schema = dump_table_schema "pg_uuids_4"
+ assert_match(/\bcreate_table "pg_uuids_4", id: :uuid, default: nil/, schema)
+ ensure
+ drop_table "pg_uuids_4"
+ ActiveRecord::Migration.verbose = @verbose_was
end
end
@@ -248,54 +337,47 @@ class PostgresqlUUIDTestInverseOf < ActiveRecord::PostgreSQLTestCase
include PostgresqlUUIDHelper
class UuidPost < ActiveRecord::Base
- self.table_name = 'pg_uuid_posts'
+ self.table_name = "pg_uuid_posts"
has_many :uuid_comments, inverse_of: :uuid_post
end
class UuidComment < ActiveRecord::Base
- self.table_name = 'pg_uuid_comments'
+ self.table_name = "pg_uuid_comments"
belongs_to :uuid_post
end
setup do
- enable_extension!('uuid-ossp', connection)
-
connection.transaction do
- connection.create_table('pg_uuid_posts', id: :uuid) do |t|
- t.string 'title'
+ connection.create_table("pg_uuid_posts", id: :uuid, **uuid_default) do |t|
+ t.string "title"
end
- connection.create_table('pg_uuid_comments', id: :uuid) do |t|
+ connection.create_table("pg_uuid_comments", id: :uuid, **uuid_default) do |t|
t.references :uuid_post, type: :uuid
- t.string 'content'
+ t.string "content"
end
end
end
teardown do
- drop_table "pg_uuid_comments"
- drop_table "pg_uuid_posts"
- disable_extension!('uuid-ossp', connection)
+ drop_table "pg_uuid_comments"
+ drop_table "pg_uuid_posts"
end
- if ActiveRecord::Base.connection.supports_extensions?
- def test_collection_association_with_uuid
- post = UuidPost.create!
- comment = post.uuid_comments.create!
- assert post.uuid_comments.find(comment.id)
- end
-
- def test_find_with_uuid
- UuidPost.create!
- assert_raise ActiveRecord::RecordNotFound do
- UuidPost.find(123456)
- end
-
- end
+ def test_collection_association_with_uuid
+ post = UuidPost.create!
+ comment = post.uuid_comments.create!
+ assert post.uuid_comments.find(comment.id)
+ end
- def test_find_by_with_uuid
- UuidPost.create!
- assert_nil UuidPost.find_by(id: 789)
+ def test_find_with_uuid
+ UuidPost.create!
+ assert_raise ActiveRecord::RecordNotFound do
+ UuidPost.find(123456)
end
end
+ def test_find_by_with_uuid
+ UuidPost.create!
+ assert_nil UuidPost.find_by(id: 789)
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/view_test.rb b/activerecord/test/cases/adapters/postgresql/view_test.rb
deleted file mode 100644
index 2dd6ec5fe6..0000000000
--- a/activerecord/test/cases/adapters/postgresql/view_test.rb
+++ /dev/null
@@ -1,64 +0,0 @@
-require "cases/helper"
-require "cases/view_test"
-
-class UpdateableViewTest < ActiveRecord::PostgreSQLTestCase
- fixtures :books
-
- class PrintedBook < ActiveRecord::Base
- self.primary_key = "id"
- end
-
- setup do
- @connection = ActiveRecord::Base.connection
- @connection.execute <<-SQL
- CREATE VIEW printed_books
- AS SELECT id, name, status, format FROM books WHERE format = 'paperback'
- SQL
- end
-
- teardown do
- @connection.execute "DROP VIEW printed_books" if @connection.table_exists? "printed_books"
- end
-
- def test_update_record
- book = PrintedBook.first
- book.name = "AWDwR"
- book.save!
- book.reload
- assert_equal "AWDwR", book.name
- end
-
- def test_insert_record
- PrintedBook.create! name: "Rails in Action", status: 0, format: "paperback"
-
- new_book = PrintedBook.last
- assert_equal "Rails in Action", new_book.name
- end
-
- def test_update_record_to_fail_view_conditions
- book = PrintedBook.first
- book.format = "ebook"
- book.save!
-
- assert_raises ActiveRecord::RecordNotFound do
- book.reload
- end
- end
-end
-
-if ActiveRecord::Base.connection.respond_to?(:supports_materialized_views?) &&
- ActiveRecord::Base.connection.supports_materialized_views?
-class MaterializedViewTest < ActiveRecord::PostgreSQLTestCase
- include ViewBehavior
-
- private
- def create_view(name, query)
- @connection.execute "CREATE MATERIALIZED VIEW #{name} AS #{query}"
- end
-
- def drop_view(name)
- @connection.execute "DROP MATERIALIZED VIEW #{name}" if @connection.table_exists? name
-
- end
-end
-end
diff --git a/activerecord/test/cases/adapters/postgresql/xml_test.rb b/activerecord/test/cases/adapters/postgresql/xml_test.rb
index add32699fa..71ead6f7f3 100644
--- a/activerecord/test/cases/adapters/postgresql/xml_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/xml_test.rb
@@ -1,28 +1,30 @@
-require 'cases/helper'
-require 'support/schema_dumping_helper'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "support/schema_dumping_helper"
class PostgresqlXMLTest < ActiveRecord::PostgreSQLTestCase
include SchemaDumpingHelper
class XmlDataType < ActiveRecord::Base
- self.table_name = 'xml_data_type'
+ self.table_name = "xml_data_type"
end
def setup
@connection = ActiveRecord::Base.connection
begin
@connection.transaction do
- @connection.create_table('xml_data_type') do |t|
- t.xml 'payload'
+ @connection.create_table("xml_data_type") do |t|
+ t.xml "payload"
end
end
rescue ActiveRecord::StatementInvalid
skip "do not test on PG without xml"
end
- @column = XmlDataType.columns_hash['payload']
+ @column = XmlDataType.columns_hash["payload"]
end
teardown do
- @connection.drop_table 'xml_data_type', if_exists: true
+ @connection.drop_table "xml_data_type", if_exists: true
end
def test_column
@@ -30,7 +32,7 @@ class PostgresqlXMLTest < ActiveRecord::PostgreSQLTestCase
end
def test_null_xml
- @connection.execute %q|insert into xml_data_type (payload) VALUES(null)|
+ @connection.execute "insert into xml_data_type (payload) VALUES(null)"
assert_nil XmlDataType.first.payload
end
diff --git a/activerecord/test/cases/adapters/sqlite3/bind_parameter_test.rb b/activerecord/test/cases/adapters/sqlite3/bind_parameter_test.rb
new file mode 100644
index 0000000000..93a7dafebd
--- /dev/null
+++ b/activerecord/test/cases/adapters/sqlite3/bind_parameter_test.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/topic"
+
+module ActiveRecord
+ module ConnectionAdapters
+ class SQLite3Adapter
+ class BindParameterTest < ActiveRecord::SQLite3TestCase
+ def test_too_many_binds
+ topics = Topic.where(id: (1..999).to_a << 2**63)
+ assert_equal Topic.count, topics.count
+
+ topics = Topic.where.not(id: (1..999).to_a << 2**63)
+ assert_equal 0, topics.count
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/sqlite3/collation_test.rb b/activerecord/test/cases/adapters/sqlite3/collation_test.rb
index 58a9469ce5..76c8f7d8dd 100644
--- a/activerecord/test/cases/adapters/sqlite3/collation_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/collation_test.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'support/schema_dumping_helper'
+require "support/schema_dumping_helper"
class SQLite3CollationTest < ActiveRecord::SQLite3TestCase
include SchemaDumpingHelper
@@ -7,8 +9,8 @@ class SQLite3CollationTest < ActiveRecord::SQLite3TestCase
def setup
@connection = ActiveRecord::Base.connection
@connection.create_table :collation_table_sqlite3, force: true do |t|
- t.string :string_nocase, collation: 'NOCASE'
- t.text :text_rtrim, collation: 'RTRIM'
+ t.string :string_nocase, collation: "NOCASE"
+ t.text :text_rtrim, collation: "RTRIM"
end
end
@@ -17,37 +19,37 @@ class SQLite3CollationTest < ActiveRecord::SQLite3TestCase
end
test "string column with collation" do
- column = @connection.columns(:collation_table_sqlite3).find { |c| c.name == 'string_nocase' }
+ column = @connection.columns(:collation_table_sqlite3).find { |c| c.name == "string_nocase" }
assert_equal :string, column.type
- assert_equal 'NOCASE', column.collation
+ assert_equal "NOCASE", column.collation
end
test "text column with collation" do
- column = @connection.columns(:collation_table_sqlite3).find { |c| c.name == 'text_rtrim' }
+ column = @connection.columns(:collation_table_sqlite3).find { |c| c.name == "text_rtrim" }
assert_equal :text, column.type
- assert_equal 'RTRIM', column.collation
+ assert_equal "RTRIM", column.collation
end
test "add column with collation" do
- @connection.add_column :collation_table_sqlite3, :title, :string, collation: 'RTRIM'
+ @connection.add_column :collation_table_sqlite3, :title, :string, collation: "RTRIM"
- column = @connection.columns(:collation_table_sqlite3).find { |c| c.name == 'title' }
+ column = @connection.columns(:collation_table_sqlite3).find { |c| c.name == "title" }
assert_equal :string, column.type
- assert_equal 'RTRIM', column.collation
+ assert_equal "RTRIM", column.collation
end
test "change column with collation" do
@connection.add_column :collation_table_sqlite3, :description, :string
- @connection.change_column :collation_table_sqlite3, :description, :text, collation: 'RTRIM'
+ @connection.change_column :collation_table_sqlite3, :description, :text, collation: "RTRIM"
- column = @connection.columns(:collation_table_sqlite3).find { |c| c.name == 'description' }
+ column = @connection.columns(:collation_table_sqlite3).find { |c| c.name == "description" }
assert_equal :text, column.type
- assert_equal 'RTRIM', column.collation
+ assert_equal "RTRIM", column.collation
end
test "schema dump includes collation" do
output = dump_table_schema("collation_table_sqlite3")
- assert_match %r{t.string\s+"string_nocase",\s+collation: "NOCASE"$}, output
- assert_match %r{t.text\s+"text_rtrim",\s+collation: "RTRIM"$}, output
+ assert_match %r{t\.string\s+"string_nocase",\s+collation: "NOCASE"$}, output
+ assert_match %r{t\.text\s+"text_rtrim",\s+collation: "RTRIM"$}, output
end
end
diff --git a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb
index 34e3b2e023..ffb1d6afce 100644
--- a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
class CopyTableTest < ActiveRecord::SQLite3TestCase
@@ -10,8 +12,8 @@ class CopyTableTest < ActiveRecord::SQLite3TestCase
end
end
- def test_copy_table(from = 'customers', to = 'customers2', options = {})
- assert_nothing_raised {copy_table(from, to, options)}
+ def test_copy_table(from = "customers", to = "customers2", options = {})
+ assert_nothing_raised { copy_table(from, to, options) }
assert_equal row_count(from), row_count(to)
if block_given?
@@ -24,68 +26,69 @@ class CopyTableTest < ActiveRecord::SQLite3TestCase
end
def test_copy_table_renaming_column
- test_copy_table('customers', 'customers2',
- :rename => {'name' => 'person_name'}) do |from, to, options|
- expected = column_values(from, 'name')
- assert_equal expected, column_values(to, 'person_name')
+ test_copy_table("customers", "customers2",
+ rename: { "name" => "person_name" }) do |from, to, options|
+ expected = column_values(from, "name")
+ assert_equal expected, column_values(to, "person_name")
assert expected.any?, "No values in table: #{expected.inspect}"
end
end
def test_copy_table_allows_to_pass_options_to_create_table
- @connection.create_table('blocker_table')
- test_copy_table('customers', 'blocker_table', force: true)
+ @connection.create_table("blocker_table")
+ test_copy_table("customers", "blocker_table", force: true)
end
def test_copy_table_with_index
- 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')
+ 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_nil table_indexes_without_name("comments_with_index")
+ assert_nil table_indexes_without_name("comments_with_index2")
end
end
end
def test_copy_table_without_primary_key
- test_copy_table('developers_projects', 'programmers_projects') do
- assert_nil @connection.primary_key('programmers_projects')
+ test_copy_table("developers_projects", "programmers_projects") do
+ assert_nil @connection.primary_key("programmers_projects")
end
end
def test_copy_table_with_id_col_that_is_not_primary_key
- test_copy_table('goofy_string_id', 'goofy_string_id2') do
- original_id = @connection.columns('goofy_string_id').detect{|col| col.name == 'id' }
- copied_id = @connection.columns('goofy_string_id2').detect{|col| col.name == 'id' }
+ test_copy_table("goofy_string_id", "goofy_string_id2") do
+ original_id = @connection.columns("goofy_string_id").detect { |col| col.name == "id" }
+ 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
def test_copy_table_with_unconventional_primary_key
- test_copy_table('owners', 'owners_unconventional') do
- original_pk = @connection.primary_key('owners')
- copied_pk = @connection.primary_key('owners_unconventional')
+ test_copy_table("owners", "owners_unconventional") do
+ original_pk = @connection.primary_key("owners")
+ copied_pk = @connection.primary_key("owners_unconventional")
assert_equal original_pk, copied_pk
end
end
def test_copy_table_with_binary_column
- test_copy_table 'binaries', 'binaries2'
+ test_copy_table "binaries", "binaries2"
end
-protected
+private
def copy_table(from, to, options = {})
- @connection.copy_table(from, to, {:temporary => true}.merge(options))
+ @connection.copy_table(from, to, { temporary: true }.merge(options))
end
def column_names(table)
- @connection.table_structure(table).map {|column| column['name']}
+ @connection.table_structure(table).map { |column| column["name"] }
end
def column_values(table, column)
- @connection.select_all("SELECT #{column} FROM #{table} ORDER BY id").map {|row| row[column]}
+ @connection.select_all("SELECT #{column} FROM #{table} ORDER BY id").map { |row| row[column] }
end
def table_indexes_without_name(table)
@@ -93,6 +96,6 @@ protected
end
def row_count(table)
- @connection.select_one("SELECT COUNT(*) AS count FROM #{table}")['count']
+ @connection.select_one("SELECT COUNT(*) AS count FROM #{table}")["count"]
end
end
diff --git a/activerecord/test/cases/adapters/sqlite3/explain_test.rb b/activerecord/test/cases/adapters/sqlite3/explain_test.rb
index 2aec322582..b6d2ccdb53 100644
--- a/activerecord/test/cases/adapters/sqlite3/explain_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/explain_test.rb
@@ -1,27 +1,23 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/developer'
-require 'models/computer'
+require "models/author"
+require "models/post"
-module ActiveRecord
- module ConnectionAdapters
- class SQLite3Adapter
- class ExplainTest < ActiveRecord::SQLite3TestCase
- fixtures :developers
+class SQLite3ExplainTest < ActiveRecord::SQLite3TestCase
+ fixtures :authors
- def test_explain_for_one_query
- explain = Developer.where(:id => 1).explain
- assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = ?), explain
- assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain)
- end
+ def test_explain_for_one_query
+ explain = Author.where(id: 1).explain
+ assert_match %r(EXPLAIN for: SELECT "authors"\.\* FROM "authors" WHERE "authors"\."id" = (?:\? \[\["id", 1\]\]|1)), explain
+ assert_match(/(SEARCH )?TABLE authors USING (INTEGER )?PRIMARY KEY/, explain)
+ end
- def test_explain_with_eager_loading
- explain = Developer.where(:id => 1).includes(:audit_logs).explain
- assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = ?), explain
- assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain)
- assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" = 1), explain
- assert_match(/(SCAN )?TABLE audit_logs/, explain)
- end
- end
- end
+ def test_explain_with_eager_loading
+ explain = Author.where(id: 1).includes(:posts).explain
+ assert_match %r(EXPLAIN for: SELECT "authors"\.\* FROM "authors" WHERE "authors"\."id" = (?:\? \[\["id", 1\]\]|1)), explain
+ assert_match(/(SEARCH )?TABLE authors USING (INTEGER )?PRIMARY KEY/, explain)
+ assert_match %r(EXPLAIN for: SELECT "posts"\.\* FROM "posts" WHERE "posts"\."author_id" = (?:\? \[\["author_id", 1\]\]|1)), explain
+ assert_match(/(SCAN )?TABLE posts/, explain)
end
end
diff --git a/activerecord/test/cases/adapters/sqlite3/json_test.rb b/activerecord/test/cases/adapters/sqlite3/json_test.rb
new file mode 100644
index 0000000000..6f247fcd22
--- /dev/null
+++ b/activerecord/test/cases/adapters/sqlite3/json_test.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "cases/json_shared_test_cases"
+
+class SQLite3JSONTest < ActiveRecord::SQLite3TestCase
+ include JSONSharedTestCases
+
+ def setup
+ super
+ @connection.create_table("json_data_type") do |t|
+ t.json "payload", default: {}
+ t.json "settings"
+ end
+ end
+
+ def test_default
+ @connection.add_column "json_data_type", "permissions", column_type, default: { "users": "read", "posts": ["read", "write"] }
+ klass.reset_column_information
+
+ assert_equal({ "users" => "read", "posts" => ["read", "write"] }, klass.column_defaults["permissions"])
+ assert_equal({ "users" => "read", "posts" => ["read", "write"] }, klass.new.permissions)
+ end
+
+ private
+ def column_type
+ :json
+ end
+end
diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
index 87a892db37..40b58e86bf 100644
--- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
@@ -1,94 +1,90 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'bigdecimal'
-require 'yaml'
-require 'securerandom'
-
-module ActiveRecord
- module ConnectionAdapters
- class SQLite3Adapter
- class QuotingTest < ActiveRecord::SQLite3TestCase
- def setup
- @conn = Base.sqlite3_connection :database => ':memory:',
- :adapter => 'sqlite3',
- :timeout => 100
- end
-
- def test_type_cast_binary_encoding_without_logger
- @conn.extend(Module.new { def logger; end })
- binary = SecureRandom.hex
- expected = binary.dup.encode!(Encoding::UTF_8)
- 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
-
- def test_type_cast_false
- assert_equal 'f', @conn.type_cast(false)
- end
-
- def test_type_cast_bigdecimal
- bd = BigDecimal.new '10.0'
- 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 = Type::String.new
-
- assert_equal "'hello'", @conn.quote(type.serialize(value))
- end
+require "bigdecimal"
+require "securerandom"
+
+class SQLite3QuotingTest < ActiveRecord::SQLite3TestCase
+ def setup
+ @conn = ActiveRecord::Base.connection
+ @initial_represent_boolean_as_integer = ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer
+ end
+
+ def teardown
+ ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = @initial_represent_boolean_as_integer
+ end
+
+ def test_type_cast_binary_encoding_without_logger
+ @conn.extend(Module.new { def logger; end })
+ binary = SecureRandom.hex
+ expected = binary.dup.encode!(Encoding::UTF_8)
+ assert_equal expected, @conn.type_cast(binary)
+ end
+
+ def test_type_cast_true
+ ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = false
+ assert_equal "t", @conn.type_cast(true)
+
+ ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = true
+ assert_equal 1, @conn.type_cast(true)
+ end
+
+ def test_type_cast_false
+ ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = false
+ assert_equal "f", @conn.type_cast(false)
+
+ ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = true
+ assert_equal 0, @conn.type_cast(false)
+ end
+
+ def test_type_cast_bigdecimal
+ bd = BigDecimal "10.0"
+ assert_equal bd.to_f, @conn.type_cast(bd)
+ end
+
+ def test_quoting_binary_strings
+ value = "hello".encode("ascii-8bit")
+ type = ActiveRecord::Type::String.new
+
+ assert_equal "'hello'", @conn.quote(type.serialize(value))
+ end
+
+ def test_quoted_time_returns_date_qualified_time
+ value = ::Time.utc(2000, 1, 1, 12, 30, 0, 999999)
+ type = ActiveRecord::Type::Time.new
+
+ assert_equal "'2000-01-01 12:30:00.999999'", @conn.quote(type.serialize(value))
+ end
+
+ def test_quoted_time_normalizes_date_qualified_time
+ value = ::Time.utc(2018, 3, 11, 12, 30, 0, 999999)
+ type = ActiveRecord::Type::Time.new
+
+ assert_equal "'2000-01-01 12:30:00.999999'", @conn.quote(type.serialize(value))
+ end
+
+ def test_quoted_time_dst_utc
+ with_env_tz "America/New_York" do
+ with_timezone_config default: :utc do
+ t = Time.new(2000, 7, 1, 0, 0, 0, "+04:30")
+
+ expected = t.change(year: 2000, month: 1, day: 1)
+ expected = expected.getutc.to_s(:db).sub(/\A\d\d\d\d-\d\d-\d\d /, "2000-01-01 ")
+
+ assert_equal expected, @conn.quoted_time(t)
+ end
+ end
+ end
+
+ def test_quoted_time_dst_local
+ with_env_tz "America/New_York" do
+ with_timezone_config default: :local do
+ t = Time.new(2000, 7, 1, 0, 0, 0, "+04:30")
+
+ expected = t.change(year: 2000, month: 1, day: 1)
+ expected = expected.getlocal.to_s(:db).sub(/\A\d\d\d\d-\d\d-\d\d /, "2000-01-01 ")
+
+ assert_equal expected, @conn.quoted_time(t)
end
end
end
diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
index 7996e7ad50..89052019f8 100644
--- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/owner'
-require 'tempfile'
-require 'support/ddl_helper'
+require "models/owner"
+require "tempfile"
+require "support/ddl_helper"
module ActiveRecord
module ConnectionAdapters
@@ -14,22 +16,22 @@ module ActiveRecord
end
def setup
- @conn = Base.sqlite3_connection database: ':memory:',
- adapter: 'sqlite3',
+ @conn = Base.sqlite3_connection database: ":memory:",
+ adapter: "sqlite3",
timeout: 100
end
def test_bad_connection
assert_raise ActiveRecord::NoDatabaseError do
connection = ActiveRecord::Base.sqlite3_connection(adapter: "sqlite3", database: "/tmp/should/_not/_exist/-cinco-dog.db")
- connection.drop_table 'ex', if_exists: true
+ connection.drop_table "ex", if_exists: true
end
end
unless in_memory_db?
def test_connect_with_url
original_connection = ActiveRecord::Base.remove_connection
- tf = Tempfile.open 'whatever'
+ tf = Tempfile.open "whatever"
url = "sqlite3:#{tf.path}"
ActiveRecord::Base.establish_connection(url)
assert ActiveRecord::Base.connection
@@ -49,33 +51,17 @@ 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 = Owner.create!(name: "hello".encode("ascii-8bit"))
owner.reload
- select = Owner.columns.map { |c| "typeof(#{c.name})" }.join ', '
+ select = Owner.columns.map { |c| "typeof(#{c.name})" }.join ", "
result = Owner.connection.exec_query <<-esql
SELECT #{select}
FROM #{Owner.table_name}
WHERE #{Owner.primary_key} = #{owner.id}
esql
- assert(!result.rows.first.include?("blob"), "should not store blobs")
+ assert_not(result.rows.first.include?("blob"), "should not store blobs")
ensure
owner.delete
end
@@ -83,10 +69,10 @@ module ActiveRecord
def test_exec_insert
with_example_table do
vals = [Relation::QueryAttribute.new("number", 10, Type::Value.new)]
- @conn.exec_insert('insert into ex (number) VALUES (?)', 'SQL', vals)
+ @conn.exec_insert("insert into ex (number) VALUES (?)", "SQL", vals)
result = @conn.exec_query(
- 'select number from ex where number = ?', 'SQL', vals)
+ "select number from ex where number = ?", "SQL", vals)
assert_equal 1, result.rows.length
assert_equal 10, result.rows.first.first
@@ -94,87 +80,82 @@ module ActiveRecord
end
def test_primary_key_returns_nil_for_no_pk
- with_example_table 'id int, data string' do
- assert_nil @conn.primary_key('ex')
+ with_example_table "id int, data string" do
+ assert_nil @conn.primary_key("ex")
end
end
def test_connection_no_db
assert_raises(ArgumentError) do
- Base.sqlite3_connection {}
+ Base.sqlite3_connection { }
end
end
def test_bad_timeout
assert_raises(TypeError) do
- Base.sqlite3_connection database: ':memory:',
- adapter: 'sqlite3',
- timeout: 'usa'
+ Base.sqlite3_connection database: ":memory:",
+ adapter: "sqlite3",
+ timeout: "usa"
end
end
# connection is OK with a nil timeout
def test_nil_timeout
- conn = Base.sqlite3_connection database: ':memory:',
- adapter: 'sqlite3',
+ conn = Base.sqlite3_connection database: ":memory:",
+ adapter: "sqlite3",
timeout: nil
- assert conn, 'made a connection'
+ assert conn, "made a connection"
end
def test_connect
- assert @conn, 'should have connection'
+ assert @conn, "should have connection"
end
# sqlite3 defaults to UTF-8 encoding
def test_encoding
- assert_equal 'UTF-8', @conn.encoding
- end
-
- def test_bind_value_substitute
- bind_param = @conn.substitute_at('foo')
- assert_equal Arel.sql('?'), bind_param.to_sql
+ assert_equal "UTF-8", @conn.encoding
end
def test_exec_no_binds
- with_example_table 'id int, data string' do
- result = @conn.exec_query('SELECT id, data FROM ex')
+ with_example_table "id int, data string" do
+ result = @conn.exec_query("SELECT id, data FROM ex")
assert_equal 0, result.rows.length
assert_equal 2, result.columns.length
assert_equal %w{ id data }, result.columns
@conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
- result = @conn.exec_query('SELECT id, data FROM ex')
+ result = @conn.exec_query("SELECT id, data FROM ex")
assert_equal 1, result.rows.length
assert_equal 2, result.columns.length
- assert_equal [[1, 'foo']], result.rows
+ assert_equal [[1, "foo"]], result.rows
end
end
def test_exec_query_with_binds
- with_example_table 'id int, data string' do
+ with_example_table "id int, data string" do
@conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
result = @conn.exec_query(
- 'SELECT id, data FROM ex WHERE id = ?', nil, [Relation::QueryAttribute.new(nil, 1, Type::Value.new)])
+ "SELECT id, data FROM ex WHERE id = ?", nil, [Relation::QueryAttribute.new(nil, 1, Type::Value.new)])
assert_equal 1, result.rows.length
assert_equal 2, result.columns.length
- assert_equal [[1, 'foo']], result.rows
+ assert_equal [[1, "foo"]], result.rows
end
end
def test_exec_query_typecasts_bind_vals
- with_example_table 'id int, data string' do
+ with_example_table "id int, data string" do
@conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
result = @conn.exec_query(
- 'SELECT id, data FROM ex WHERE id = ?', nil, [Relation::QueryAttribute.new("id", "1-fuu", Type::Integer.new)])
+ "SELECT id, data FROM ex WHERE id = ?", nil, [Relation::QueryAttribute.new("id", "1-fuu", Type::Integer.new)])
assert_equal 1, result.rows.length
assert_equal 2, result.columns.length
- assert_equal [[1, 'foo']], result.rows
+ assert_equal [[1, "foo"]], result.rows
end
end
@@ -186,16 +167,16 @@ module ActiveRecord
data binary
)
eosql
- str = "\x80".force_encoding("ASCII-8BIT")
- binary = DualEncoding.new name: 'いただきます!', data: str
+ str = (+"\x80").force_encoding("ASCII-8BIT")
+ binary = DualEncoding.new name: "いただきます!", data: str
binary.save!
assert_equal str, binary.data
ensure
- DualEncoding.connection.drop_table 'dual_encodings', if_exists: true
+ DualEncoding.connection.drop_table "dual_encodings", if_exists: true
end
def test_type_cast_should_not_mutate_encoding
- name = 'hello'.force_encoding(Encoding::ASCII_8BIT)
+ name = (+"hello").force_encoding(Encoding::ASCII_8BIT)
Owner.create(name: name)
assert_equal Encoding::ASCII_8BIT, name.encoding
ensure
@@ -209,8 +190,8 @@ module ActiveRecord
assert_equal 1, records.length
record = records.first
- assert_equal 10, record['number']
- assert_equal 1, record['id']
+ assert_equal 10, record["number"]
+ assert_equal 1, record["id"]
end
end
@@ -218,24 +199,12 @@ module ActiveRecord
assert_equal "''", @conn.quote_string("'")
end
- def test_insert_sql
- with_example_table do
- 2.times do |i|
- rv = @conn.insert_sql "INSERT INTO ex (number) VALUES (#{i})"
- assert_equal(i + 1, rv)
- end
-
- records = @conn.execute "SELECT * FROM ex"
- assert_equal 2, records.length
- end
- end
-
- def test_insert_sql_logged
+ def test_insert_logged
with_example_table do
sql = "INSERT INTO ex (number) VALUES (10)"
name = "foo"
assert_logged [[sql, name, []]] do
- @conn.insert_sql sql, name
+ @conn.insert(sql, name)
end
end
end
@@ -243,8 +212,8 @@ module ActiveRecord
def test_insert_id_value_returned
with_example_table do
sql = "INSERT INTO ex (number) VALUES (10)"
- idval = 'vuvuzela'
- id = @conn.insert_sql sql, nil, nil, idval
+ idval = "vuvuzela"
+ id = @conn.insert(sql, nil, nil, idval)
assert_equal idval, id
end
end
@@ -254,7 +223,7 @@ module ActiveRecord
2.times do |i|
@conn.create "INSERT INTO ex (number) VALUES (#{i})"
end
- rows = @conn.select_rows 'select number, id from ex'
+ rows = @conn.select_rows "select number, id from ex"
assert_equal [[0, 1], [1, 2]], rows
end
end
@@ -271,7 +240,7 @@ module ActiveRecord
def test_transaction
with_example_table do
- count_sql = 'select count(*) from ex'
+ count_sql = "select count(*) from ex"
@conn.begin_db_transaction
@conn.create "INSERT INTO ex (number) VALUES (10)"
@@ -285,7 +254,7 @@ module ActiveRecord
def test_tables
with_example_table do
assert_equal %w{ ex }, @conn.tables
- with_example_table 'id integer PRIMARY KEY AUTOINCREMENT, number integer', 'people' do
+ with_example_table "id integer PRIMARY KEY AUTOINCREMENT, number integer", "people" do
assert_equal %w{ ex people }.sort, @conn.tables.sort
end
end
@@ -293,38 +262,27 @@ module ActiveRecord
def test_tables_logs_name
sql = <<-SQL
- SELECT name FROM sqlite_master
- WHERE (type = 'table' OR type = 'view') AND NOT name = 'sqlite_sequence'
+ SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence' AND type IN ('table')
SQL
- assert_logged [[sql.squish, 'SCHEMA', []]] do
- @conn.tables('hello')
- end
- end
-
- def test_indexes_logs_name
- with_example_table do
- assert_logged [["PRAGMA index_list(\"ex\")", 'SCHEMA', []]] do
- @conn.indexes('ex', 'hello')
- end
+ assert_logged [[sql.squish, "SCHEMA", []]] do
+ @conn.tables
end
end
def test_table_exists_logs_name
with_example_table do
sql = <<-SQL
- SELECT name FROM sqlite_master
- WHERE (type = 'table' OR type = 'view')
- AND NOT 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
- assert @conn.table_exists?('ex')
+ assert_logged [[sql.squish, "SCHEMA", []]] do
+ assert @conn.table_exists?("ex")
end
end
end
def test_columns
with_example_table do
- columns = @conn.columns('ex').sort_by(&:name)
+ columns = @conn.columns("ex").sort_by(&:name)
assert_equal 2, columns.length
assert_equal %w{ id number }.sort, columns.map(&:name)
assert_equal [nil, nil], columns.map(&:default)
@@ -333,17 +291,17 @@ module ActiveRecord
end
def test_columns_with_default
- with_example_table 'id integer PRIMARY KEY AUTOINCREMENT, number integer default 10' do
- column = @conn.columns('ex').find { |x|
- x.name == 'number'
+ with_example_table "id integer PRIMARY KEY AUTOINCREMENT, number integer default 10" do
+ column = @conn.columns("ex").find { |x|
+ x.name == "number"
}
- assert_equal '10', column.default
+ assert_equal "10", column.default
end
end
def test_columns_with_not_null
- with_example_table 'id integer PRIMARY KEY AUTOINCREMENT, number integer not null' do
- column = @conn.columns('ex').find { |x| x.name == 'number' }
+ with_example_table "id integer PRIMARY KEY AUTOINCREMENT, number integer not null" do
+ column = @conn.columns("ex").find { |x| x.name == "number" }
assert_not column.null, "column should not be null"
end
end
@@ -351,108 +309,288 @@ module ActiveRecord
def test_indexes_logs
with_example_table do
assert_logged [["PRAGMA index_list(\"ex\")", "SCHEMA", []]] do
- @conn.indexes('ex')
+ @conn.indexes("ex")
end
end
end
def test_no_indexes
- assert_equal [], @conn.indexes('items')
+ assert_equal [], @conn.indexes("items")
end
def test_index
with_example_table do
- @conn.add_index 'ex', 'id', unique: true, name: 'fun'
- index = @conn.indexes('ex').find { |idx| idx.name == 'fun' }
+ @conn.add_index "ex", "id", unique: true, name: "fun"
+ index = @conn.indexes("ex").find { |idx| idx.name == "fun" }
- assert_equal 'ex', index.table
- assert index.unique, 'index is unique'
- assert_equal ['id'], index.columns
+ assert_equal "ex", index.table
+ assert index.unique, "index is unique"
+ assert_equal ["id"], index.columns
end
end
def test_non_unique_index
with_example_table do
- @conn.add_index 'ex', 'id', name: 'fun'
- index = @conn.indexes('ex').find { |idx| idx.name == 'fun' }
- assert_not index.unique, 'index is not unique'
+ @conn.add_index "ex", "id", name: "fun"
+ index = @conn.indexes("ex").find { |idx| idx.name == "fun" }
+ assert_not index.unique, "index is not unique"
end
end
def test_compound_index
with_example_table do
- @conn.add_index 'ex', %w{ id number }, name: 'fun'
- index = @conn.indexes('ex').find { |idx| idx.name == 'fun' }
+ @conn.add_index "ex", %w{ id number }, name: "fun"
+ index = @conn.indexes("ex").find { |idx| idx.name == "fun" }
assert_equal %w{ id number }.sort, index.columns.sort
end
end
+ if ActiveRecord::Base.connection.supports_expression_index?
+ def test_expression_index
+ with_example_table do
+ @conn.add_index "ex", "max(id, number)", name: "expression"
+ index = @conn.indexes("ex").find { |idx| idx.name == "expression" }
+ assert_equal "max(id, number)", index.columns
+ end
+ end
+
+ def test_expression_index_with_where
+ with_example_table do
+ @conn.add_index "ex", "id % 10, max(id, number)", name: "expression", where: "id > 1000"
+ index = @conn.indexes("ex").find { |idx| idx.name == "expression" }
+ assert_equal "id % 10, max(id, number)", index.columns
+ assert_equal "id > 1000", index.where
+ end
+ end
+
+ def test_complicated_expression
+ with_example_table do
+ @conn.execute "CREATE INDEX expression ON ex (id % 10, (CASE WHEN number > 0 THEN max(id, number) END))WHERE(id > 1000)"
+ index = @conn.indexes("ex").find { |idx| idx.name == "expression" }
+ assert_equal "id % 10, (CASE WHEN number > 0 THEN max(id, number) END)", index.columns
+ assert_equal "(id > 1000)", index.where
+ end
+ end
+
+ def test_not_everything_an_expression
+ with_example_table do
+ @conn.add_index "ex", "id, max(id, number)", name: "expression"
+ index = @conn.indexes("ex").find { |idx| idx.name == "expression" }
+ assert_equal "id, max(id, number)", index.columns
+ end
+ end
+ end
+
def test_primary_key
with_example_table do
- assert_equal 'id', @conn.primary_key('ex')
- with_example_table 'internet integer PRIMARY KEY AUTOINCREMENT, number integer not null', 'foos' do
- assert_equal 'internet', @conn.primary_key('foos')
+ assert_equal "id", @conn.primary_key("ex")
+ with_example_table "internet integer PRIMARY KEY AUTOINCREMENT, number integer not null", "foos" do
+ assert_equal "internet", @conn.primary_key("foos")
end
end
end
def test_no_primary_key
- with_example_table 'number integer not null' do
- assert_nil @conn.primary_key('ex')
+ with_example_table "number integer not null" do
+ assert_nil @conn.primary_key("ex")
end
end
- def test_composite_primary_key
- with_example_table 'id integer, number integer, foo integer, PRIMARY KEY (id, number)' do
- assert_nil @conn.primary_key('ex')
+ class Barcode < ActiveRecord::Base
+ self.primary_key = "code"
+ end
+
+ def test_copy_table_with_existing_records_have_custom_primary_key
+ connection = Barcode.connection
+ connection.create_table(:barcodes, primary_key: "code", id: :string, limit: 42, force: true) do |t|
+ t.text :other_attr
end
+ code = "214fe0c2-dd47-46df-b53b-66090b3c1d40"
+ Barcode.create!(code: code, other_attr: "xxx")
+
+ connection.remove_column("barcodes", "other_attr")
+
+ assert_equal code, Barcode.first.id
+ ensure
+ Barcode.reset_column_information
+ end
+
+ def test_copy_table_with_composite_primary_keys
+ connection = Barcode.connection
+ connection.create_table(:barcodes, primary_key: ["region", "code"], force: true) do |t|
+ t.string :region
+ t.string :code
+ t.text :other_attr
+ end
+ region = "US"
+ code = "214fe0c2-dd47-46df-b53b-66090b3c1d40"
+ Barcode.create!(region: region, code: code, other_attr: "xxx")
+
+ connection.remove_column("barcodes", "other_attr")
+
+ assert_equal ["region", "code"], connection.primary_keys("barcodes")
+
+ barcode = Barcode.first
+ assert_equal region, barcode.region
+ assert_equal code, barcode.code
+ ensure
+ Barcode.reset_column_information
+ end
+
+ def test_custom_primary_key_in_create_table
+ connection = Barcode.connection
+ connection.create_table :barcodes, id: false, force: true do |t|
+ t.primary_key :id, :string
+ end
+
+ assert_equal "id", connection.primary_key("barcodes")
+
+ custom_pk = Barcode.columns_hash["id"]
+
+ assert_equal :string, custom_pk.type
+ assert_not custom_pk.null
+ ensure
+ Barcode.reset_column_information
+ end
+
+ def test_custom_primary_key_in_change_table
+ connection = Barcode.connection
+ connection.create_table :barcodes, id: false, force: true do |t|
+ t.integer :dummy
+ end
+ connection.change_table :barcodes do |t|
+ t.primary_key :id, :string
+ end
+
+ assert_equal "id", connection.primary_key("barcodes")
+
+ custom_pk = Barcode.columns_hash["id"]
+
+ assert_equal :string, custom_pk.type
+ assert_not custom_pk.null
+ ensure
+ Barcode.reset_column_information
+ end
+
+ def test_add_column_with_custom_primary_key
+ connection = Barcode.connection
+ connection.create_table :barcodes, id: false, force: true do |t|
+ t.integer :dummy
+ end
+ connection.add_column :barcodes, :id, :string, primary_key: true
+
+ assert_equal "id", connection.primary_key("barcodes")
+
+ custom_pk = Barcode.columns_hash["id"]
+
+ assert_equal :string, custom_pk.type
+ assert_not custom_pk.null
+ ensure
+ Barcode.reset_column_information
+ end
+
+ def test_remove_column_preserves_partial_indexes
+ connection = Barcode.connection
+ connection.create_table :barcodes, force: true do |t|
+ t.string :code
+ t.string :region
+ t.boolean :bool_attr
+
+ t.index :code, unique: true, where: :bool_attr, name: "partial"
+ end
+ connection.remove_column :barcodes, :region
+
+ index = connection.indexes("barcodes").find { |idx| idx.name == "partial" }
+ assert_equal "bool_attr", index.where
+ ensure
+ Barcode.reset_column_information
end
def test_supports_extensions
- assert_not @conn.supports_extensions?, 'does not support extensions'
+ assert_not @conn.supports_extensions?, "does not support extensions"
end
def test_respond_to_enable_extension
- assert @conn.respond_to?(:enable_extension)
+ assert_respond_to @conn, :enable_extension
end
def test_respond_to_disable_extension
- assert @conn.respond_to?(:disable_extension)
+ assert_respond_to @conn, :disable_extension
end
def test_statement_closed
db = ::SQLite3::Database.new(ActiveRecord::Base.
- configurations['arunit']['database'])
+ configurations["arunit"]["database"])
statement = ::SQLite3::Statement.new(db,
- 'CREATE TABLE statement_test (number integer not null)')
- statement.stubs(:step).raises(::SQLite3::BusyException, 'busy')
- statement.stubs(:columns).once.returns([])
- statement.expects(:close).once
- ::SQLite3::Statement.stubs(:new).returns(statement)
-
- assert_raises ActiveRecord::StatementInvalid do
- @conn.exec_query 'select * from statement_test'
+ "CREATE TABLE statement_test (number integer not null)")
+ statement.stub(:step, -> { raise ::SQLite3::BusyException.new("busy") }) do
+ assert_called(statement, :columns, returns: []) do
+ assert_called(statement, :close) do
+ ::SQLite3::Statement.stub(:new, statement) do
+ assert_raises ActiveRecord::StatementInvalid do
+ @conn.exec_query "select * from statement_test"
+ end
+ end
+ end
+ end
end
end
- private
+ def test_deprecate_valid_alter_table_type
+ assert_deprecated { @conn.valid_alter_table_type?(:string) }
+ end
- def assert_logged logs
- subscriber = SQLSubscriber.new
- subscription = ActiveSupport::Notifications.subscribe('sql.active_record', subscriber)
- yield
- assert_equal logs, subscriber.logged
- ensure
- ActiveSupport::Notifications.unsubscribe(subscription)
+ def test_db_is_not_readonly_when_readonly_option_is_false
+ conn = Base.sqlite3_connection database: ":memory:",
+ adapter: "sqlite3",
+ readonly: false
+
+ assert_not_predicate conn.raw_connection, :readonly?
end
- def with_example_table(definition = nil, table_name = 'ex', &block)
- definition ||= <<-SQL
- id integer PRIMARY KEY AUTOINCREMENT,
- number integer
- SQL
- super(@conn, table_name, definition, &block)
+ def test_db_is_not_readonly_when_readonly_option_is_unspecified
+ conn = Base.sqlite3_connection database: ":memory:",
+ adapter: "sqlite3"
+
+ assert_not_predicate conn.raw_connection, :readonly?
+ end
+
+ def test_db_is_readonly_when_readonly_option_is_true
+ conn = Base.sqlite3_connection database: ":memory:",
+ adapter: "sqlite3",
+ readonly: true
+
+ assert_predicate conn.raw_connection, :readonly?
+ end
+
+ def test_writes_are_not_permitted_to_readonly_databases
+ conn = Base.sqlite3_connection database: ":memory:",
+ adapter: "sqlite3",
+ readonly: true
+
+ assert_raises(ActiveRecord::StatementInvalid, /SQLite3::ReadOnlyException/) do
+ conn.execute("CREATE TABLE test(id integer)")
+ end
end
+
+ private
+
+ def assert_logged(logs)
+ subscriber = SQLSubscriber.new
+ subscription = ActiveSupport::Notifications.subscribe("sql.active_record", subscriber)
+ yield
+ assert_equal logs, subscriber.logged
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscription)
+ end
+
+ def with_example_table(definition = nil, table_name = "ex", &block)
+ definition ||= <<-SQL
+ id integer PRIMARY KEY AUTOINCREMENT,
+ number integer
+ SQL
+ super(@conn, table_name, definition, &block)
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb
index 887dcfc96c..d70486605f 100644
--- a/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb
@@ -1,18 +1,24 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/owner'
+require "models/owner"
module ActiveRecord
module ConnectionAdapters
class SQLite3CreateFolder < ActiveRecord::SQLite3TestCase
def test_sqlite_creates_directory
Dir.mktmpdir do |dir|
- dir = Pathname.new(dir)
- @conn = Base.sqlite3_connection :database => dir.join("db/foo.sqlite3"),
- :adapter => 'sqlite3',
- :timeout => 100
+ begin
+ dir = Pathname.new(dir)
+ @conn = Base.sqlite3_connection database: dir.join("db/foo.sqlite3"),
+ adapter: "sqlite3",
+ timeout: 100
- assert Dir.exist? dir.join('db')
- assert File.exist? dir.join('db/foo.sqlite3')
+ assert Dir.exist? dir.join("db")
+ assert File.exist? dir.join("db/foo.sqlite3")
+ ensure
+ @conn.disconnect! if @conn
+ end
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 ef324183a7..61002435a4 100644
--- a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb
@@ -1,24 +1,21 @@
-require 'cases/helper'
+# frozen_string_literal: true
-module ActiveRecord::ConnectionAdapters
- class SQLite3Adapter
- class StatementPoolTest < ActiveRecord::SQLite3TestCase
- if Process.respond_to?(:fork)
- def test_cache_is_per_pid
+require "cases/helper"
- cache = StatementPool.new nil, 10
- cache['foo'] = 'bar'
- assert_equal 'bar', cache['foo']
+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"]
- pid = fork {
- lookup = cache['foo'];
- exit!(!lookup)
- }
+ pid = fork {
+ lookup = cache["foo"]
+ exit!(!lookup)
+ }
- Process.waitpid pid
- assert $?.success?, 'process should exit successfully'
- end
- end
+ Process.waitpid pid
+ assert $?.success?, "process should exit successfully"
end
end
end
diff --git a/activerecord/test/cases/aggregations_test.rb b/activerecord/test/cases/aggregations_test.rb
index 5536702f58..fbdf2ada4b 100644
--- a/activerecord/test/cases/aggregations_test.rb
+++ b/activerecord/test/cases/aggregations_test.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/customer'
+require "models/customer"
class AggregationsTest < ActiveRecord::TestCase
fixtures :customers
@@ -25,7 +27,7 @@ class AggregationsTest < ActiveRecord::TestCase
def test_immutable_value_objects
customers(:david).balance = Money.new(100)
- assert_raise(RuntimeError) { customers(:david).balance.instance_eval { @amount = 20 } }
+ assert_raise(frozen_error_class) { customers(:david).balance.instance_eval { @amount = 20 } }
end
def test_inferred_mapping
@@ -51,17 +53,17 @@ class AggregationsTest < ActiveRecord::TestCase
Customer.update_all("gps_location = '24x113'")
customers(:david).reload
- assert_equal '24x113', customers(:david)['gps_location']
+ assert_equal "24x113", customers(:david)["gps_location"]
- assert_equal GpsLocation.new('24x113'), customers(:david).gps_location
+ assert_equal GpsLocation.new("24x113"), customers(:david).gps_location
end
def test_gps_equality
- assert_equal GpsLocation.new('39x110'), GpsLocation.new('39x110')
+ assert_equal GpsLocation.new("39x110"), GpsLocation.new("39x110")
end
def test_gps_inequality
- assert_not_equal GpsLocation.new('39x110'), GpsLocation.new('39x111')
+ assert_not_equal GpsLocation.new("39x110"), GpsLocation.new("39x111")
end
def test_allow_nil_gps_is_nil
@@ -102,7 +104,7 @@ class AggregationsTest < ActiveRecord::TestCase
end
def test_nil_assignment_results_in_nil
- customers(:david).gps_location = GpsLocation.new('39x111')
+ customers(:david).gps_location = GpsLocation.new("39x111")
assert_not_nil customers(:david).gps_location
customers(:david).gps_location = nil
assert_nil customers(:david).gps_location
@@ -129,26 +131,36 @@ class AggregationsTest < ActiveRecord::TestCase
end
def test_custom_constructor
- assert_equal 'Barney GUMBLE', customers(:barney).fullname.to_s
+ assert_equal "Barney GUMBLE", customers(:barney).fullname.to_s
assert_kind_of Fullname, customers(:barney).fullname
end
def test_custom_converter
- customers(:barney).fullname = 'Barnoit Gumbleau'
- assert_equal 'Barnoit GUMBLEAU', customers(:barney).fullname.to_s
+ customers(:barney).fullname = "Barnoit Gumbleau"
+ assert_equal "Barnoit GUMBLEAU", customers(:barney).fullname.to_s
assert_kind_of Fullname, customers(:barney).fullname
end
+
+ def test_assigning_hash_to_custom_converter
+ customers(:barney).fullname = { first: "Barney", last: "Stinson" }
+ assert_equal "Barney STINSON", customers(:barney).name
+ end
+
+ def test_assigning_hash_without_custom_converter
+ customers(:barney).fullname_no_converter = { first: "Barney", last: "Stinson" }
+ assert_equal({ first: "Barney", last: "Stinson" }.to_s, customers(:barney).name)
+ end
end
class OverridingAggregationsTest < ActiveRecord::TestCase
class DifferentName; end
class Person < ActiveRecord::Base
- composed_of :composed_of, :mapping => %w(person_first_name first_name)
+ composed_of :composed_of, mapping: %w(person_first_name first_name)
end
class DifferentPerson < Person
- composed_of :composed_of, :class_name => 'DifferentName', :mapping => %w(different_person_first_name first_name)
+ composed_of :composed_of, class_name: "DifferentName", mapping: %w(different_person_first_name first_name)
end
def test_composed_of_aggregation_redefinition_reflections_should_differ_and_not_inherited
diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb
index 9d5327bf35..f05dcac7dd 100644
--- a/activerecord/test/cases/ar_schema_test.rb
+++ b/activerecord/test/cases/ar_schema_test.rb
@@ -1,130 +1,145 @@
+# frozen_string_literal: true
+
require "cases/helper"
-if ActiveRecord::Base.connection.supports_migrations?
+class ActiveRecordSchemaTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
- 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
- setup do
- @original_verbose = ActiveRecord::Migration.verbose
- ActiveRecord::Migration.verbose = false
- @connection = ActiveRecord::Base.connection
- ActiveRecord::SchemaMigration.drop_table
- end
+ 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
- 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
- ActiveRecord::SchemaMigration.delete_all rescue nil
- ActiveRecord::Migration.verbose = @original_verbose
- 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
- def test_has_no_primary_key
- old_primary_key_prefix_type = ActiveRecord::Base.primary_key_prefix_type
- ActiveRecord::Base.primary_key_prefix_type = :table_name_with_underscore
- assert_nil ActiveRecord::SchemaMigration.primary_key
+ 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
- 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, @connection.migration_context.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
+ 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
end
+ assert_equal 7, @connection.migration_context.current_version
+ ensure
+ ActiveRecord::Base.table_name_prefix = old_table_name_prefix
+ ActiveRecord::SchemaMigration.table_name = table_name
+ 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
+ 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
- assert_equal 7, ActiveRecord::Migrator::current_version
- ensure
- ActiveRecord::Base.table_name_prefix = old_table_name_prefix
- ActiveRecord::SchemaMigration.table_name = table_name
end
+ 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
+ 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_schema_subclass
- Class.new(ActiveRecord::Schema).define(:version => 9) do
- create_table :fruits
+ 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
- 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
+ indexes = @connection.indexes("multiple_indexes")
- 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
- end
+ assert_equal 2, indexes.length
+ assert_equal ["multiple_indexes_foo_1", "multiple_indexes_foo_2"], indexes.collect(&:name).sort
+ 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_create_table
+ ActiveRecord::Schema.define do
+ create_table :has_timestamps do |t|
+ t.timestamps
+ end
end
- def test_timestamps_without_null_set_null_to_false_on_change_table
- ActiveRecord::Schema.define do
- create_table :has_timestamps
+ assert_not @connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null
+ assert_not @connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null
+ end
- 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_not @connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null
+ assert_not @connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null
+ end
- 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_not @connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null
+ assert_not @connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null
end
end
diff --git a/activerecord/test/cases/arel/attributes/attribute_test.rb b/activerecord/test/cases/arel/attributes/attribute_test.rb
new file mode 100644
index 0000000000..671e273543
--- /dev/null
+++ b/activerecord/test/cases/arel/attributes/attribute_test.rb
@@ -0,0 +1,1015 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+require "ostruct"
+
+module Arel
+ module Attributes
+ class AttributeTest < Arel::Spec
+ describe "#not_eq" do
+ it "should create a NotEqual node" do
+ relation = Table.new(:users)
+ relation[:id].not_eq(10).must_be_kind_of Nodes::NotEqual
+ end
+
+ it "should generate != in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].not_eq(10)
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."id" != 10
+ }
+ end
+
+ it "should handle nil" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].not_eq(nil)
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."id" IS NOT NULL
+ }
+ end
+ end
+
+ describe "#not_eq_any" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].not_eq_any([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ORs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].not_eq_any([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" != 1 OR "users"."id" != 2)
+ }
+ end
+ end
+
+ describe "#not_eq_all" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].not_eq_all([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ANDs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].not_eq_all([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" != 1 AND "users"."id" != 2)
+ }
+ end
+ end
+
+ describe "#gt" do
+ it "should create a GreaterThan node" do
+ relation = Table.new(:users)
+ relation[:id].gt(10).must_be_kind_of Nodes::GreaterThan
+ end
+
+ it "should generate > in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].gt(10)
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."id" > 10
+ }
+ end
+
+ it "should handle comparing with a subquery" do
+ users = Table.new(:users)
+
+ avg = users.project(users[:karma].average)
+ mgr = users.project(Arel.star).where(users[:karma].gt(avg))
+
+ mgr.to_sql.must_be_like %{
+ SELECT * FROM "users" WHERE "users"."karma" > (SELECT AVG("users"."karma") FROM "users")
+ }
+ end
+
+ it "should accept various data types." do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:name].gt("fake_name")
+ mgr.to_sql.must_match %{"users"."name" > 'fake_name'}
+
+ current_time = ::Time.now
+ mgr.where relation[:created_at].gt(current_time)
+ mgr.to_sql.must_match %{"users"."created_at" > '#{current_time}'}
+ end
+ end
+
+ describe "#gt_any" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].gt_any([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ORs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].gt_any([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" > 1 OR "users"."id" > 2)
+ }
+ end
+ end
+
+ describe "#gt_all" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].gt_all([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ANDs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].gt_all([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" > 1 AND "users"."id" > 2)
+ }
+ end
+ end
+
+ describe "#gteq" do
+ it "should create a GreaterThanOrEqual node" do
+ relation = Table.new(:users)
+ relation[:id].gteq(10).must_be_kind_of Nodes::GreaterThanOrEqual
+ end
+
+ it "should generate >= in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].gteq(10)
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."id" >= 10
+ }
+ end
+
+ it "should accept various data types." do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:name].gteq("fake_name")
+ mgr.to_sql.must_match %{"users"."name" >= 'fake_name'}
+
+ current_time = ::Time.now
+ mgr.where relation[:created_at].gteq(current_time)
+ mgr.to_sql.must_match %{"users"."created_at" >= '#{current_time}'}
+ end
+ end
+
+ describe "#gteq_any" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].gteq_any([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ORs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].gteq_any([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" >= 1 OR "users"."id" >= 2)
+ }
+ end
+ end
+
+ describe "#gteq_all" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].gteq_all([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ANDs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].gteq_all([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" >= 1 AND "users"."id" >= 2)
+ }
+ end
+ end
+
+ describe "#lt" do
+ it "should create a LessThan node" do
+ relation = Table.new(:users)
+ relation[:id].lt(10).must_be_kind_of Nodes::LessThan
+ end
+
+ it "should generate < in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].lt(10)
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."id" < 10
+ }
+ end
+
+ it "should accept various data types." do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:name].lt("fake_name")
+ mgr.to_sql.must_match %{"users"."name" < 'fake_name'}
+
+ current_time = ::Time.now
+ mgr.where relation[:created_at].lt(current_time)
+ mgr.to_sql.must_match %{"users"."created_at" < '#{current_time}'}
+ end
+ end
+
+ describe "#lt_any" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].lt_any([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ORs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].lt_any([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" < 1 OR "users"."id" < 2)
+ }
+ end
+ end
+
+ describe "#lt_all" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].lt_all([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ANDs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].lt_all([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" < 1 AND "users"."id" < 2)
+ }
+ end
+ end
+
+ describe "#lteq" do
+ it "should create a LessThanOrEqual node" do
+ relation = Table.new(:users)
+ relation[:id].lteq(10).must_be_kind_of Nodes::LessThanOrEqual
+ end
+
+ it "should generate <= in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].lteq(10)
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."id" <= 10
+ }
+ end
+
+ it "should accept various data types." do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:name].lteq("fake_name")
+ mgr.to_sql.must_match %{"users"."name" <= 'fake_name'}
+
+ current_time = ::Time.now
+ mgr.where relation[:created_at].lteq(current_time)
+ mgr.to_sql.must_match %{"users"."created_at" <= '#{current_time}'}
+ end
+ end
+
+ describe "#lteq_any" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].lteq_any([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ORs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].lteq_any([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" <= 1 OR "users"."id" <= 2)
+ }
+ end
+ end
+
+ describe "#lteq_all" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].lteq_all([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ANDs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].lteq_all([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" <= 1 AND "users"."id" <= 2)
+ }
+ end
+ end
+
+ describe "#average" do
+ it "should create a AVG node" do
+ relation = Table.new(:users)
+ relation[:id].average.must_be_kind_of Nodes::Avg
+ end
+
+ it "should generate the proper SQL" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id].average
+ mgr.to_sql.must_be_like %{
+ SELECT AVG("users"."id")
+ FROM "users"
+ }
+ end
+ end
+
+ describe "#maximum" do
+ it "should create a MAX node" do
+ relation = Table.new(:users)
+ relation[:id].maximum.must_be_kind_of Nodes::Max
+ end
+
+ it "should generate proper SQL" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id].maximum
+ mgr.to_sql.must_be_like %{
+ SELECT MAX("users"."id")
+ FROM "users"
+ }
+ end
+ end
+
+ describe "#minimum" do
+ it "should create a Min node" do
+ relation = Table.new(:users)
+ relation[:id].minimum.must_be_kind_of Nodes::Min
+ end
+
+ it "should generate proper SQL" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id].minimum
+ mgr.to_sql.must_be_like %{
+ SELECT MIN("users"."id")
+ FROM "users"
+ }
+ end
+ end
+
+ describe "#sum" do
+ it "should create a SUM node" do
+ relation = Table.new(:users)
+ relation[:id].sum.must_be_kind_of Nodes::Sum
+ end
+
+ it "should generate the proper SQL" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id].sum
+ mgr.to_sql.must_be_like %{
+ SELECT SUM("users"."id")
+ FROM "users"
+ }
+ end
+ end
+
+ describe "#count" do
+ it "should return a count node" do
+ relation = Table.new(:users)
+ relation[:id].count.must_be_kind_of Nodes::Count
+ end
+
+ it "should take a distinct param" do
+ relation = Table.new(:users)
+ count = relation[:id].count(nil)
+ count.must_be_kind_of Nodes::Count
+ count.distinct.must_be_nil
+ end
+ end
+
+ describe "#eq" do
+ it "should return an equality node" do
+ attribute = Attribute.new nil, nil
+ equality = attribute.eq 1
+ equality.left.must_equal attribute
+ equality.right.val.must_equal 1
+ equality.must_be_kind_of Nodes::Equality
+ end
+
+ it "should generate = in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].eq(10)
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."id" = 10
+ }
+ end
+
+ it "should handle nil" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].eq(nil)
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."id" IS NULL
+ }
+ end
+ end
+
+ describe "#eq_any" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].eq_any([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ORs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].eq_any([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" = 1 OR "users"."id" = 2)
+ }
+ end
+
+ it "should not eat input" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ values = [1, 2]
+ mgr.where relation[:id].eq_any(values)
+ values.must_equal [1, 2]
+ end
+ end
+
+ describe "#eq_all" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].eq_all([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ANDs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].eq_all([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" = 1 AND "users"."id" = 2)
+ }
+ end
+
+ it "should not eat input" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ values = [1, 2]
+ mgr.where relation[:id].eq_all(values)
+ values.must_equal [1, 2]
+ end
+ end
+
+ describe "#matches" do
+ it "should create a Matches node" do
+ relation = Table.new(:users)
+ relation[:name].matches("%bacon%").must_be_kind_of Nodes::Matches
+ end
+
+ it "should generate LIKE in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:name].matches("%bacon%")
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."name" LIKE '%bacon%'
+ }
+ end
+ end
+
+ describe "#matches_any" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:name].matches_any(["%chunky%", "%bacon%"]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ORs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:name].matches_any(["%chunky%", "%bacon%"])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."name" LIKE '%chunky%' OR "users"."name" LIKE '%bacon%')
+ }
+ end
+ end
+
+ describe "#matches_all" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:name].matches_all(["%chunky%", "%bacon%"]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ANDs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:name].matches_all(["%chunky%", "%bacon%"])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."name" LIKE '%chunky%' AND "users"."name" LIKE '%bacon%')
+ }
+ end
+ end
+
+ describe "#does_not_match" do
+ it "should create a DoesNotMatch node" do
+ relation = Table.new(:users)
+ relation[:name].does_not_match("%bacon%").must_be_kind_of Nodes::DoesNotMatch
+ end
+
+ it "should generate NOT LIKE in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:name].does_not_match("%bacon%")
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."name" NOT LIKE '%bacon%'
+ }
+ end
+ end
+
+ describe "#does_not_match_any" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:name].does_not_match_any(["%chunky%", "%bacon%"]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ORs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:name].does_not_match_any(["%chunky%", "%bacon%"])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."name" NOT LIKE '%chunky%' OR "users"."name" NOT LIKE '%bacon%')
+ }
+ end
+ end
+
+ describe "#does_not_match_all" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:name].does_not_match_all(["%chunky%", "%bacon%"]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ANDs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:name].does_not_match_all(["%chunky%", "%bacon%"])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."name" NOT LIKE '%chunky%' AND "users"."name" NOT LIKE '%bacon%')
+ }
+ end
+ end
+
+ describe "with a range" do
+ it "can be constructed with a standard range" do
+ attribute = Attribute.new nil, nil
+ node = attribute.between(1..3)
+
+ node.must_equal Nodes::Between.new(
+ attribute,
+ Nodes::And.new([
+ Nodes::Casted.new(1, attribute),
+ Nodes::Casted.new(3, attribute)
+ ])
+ )
+ end
+
+ it "can be constructed with a range starting from -Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.between(-::Float::INFINITY..3)
+
+ node.must_equal Nodes::LessThanOrEqual.new(
+ attribute,
+ Nodes::Casted.new(3, attribute)
+ )
+ end
+
+ it "can be constructed with a quoted range starting from -Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.between(quoted_range(-::Float::INFINITY, 3, false))
+
+ node.must_equal Nodes::LessThanOrEqual.new(
+ attribute,
+ Nodes::Quoted.new(3)
+ )
+ end
+
+ it "can be constructed with an exclusive range starting from -Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.between(-::Float::INFINITY...3)
+
+ node.must_equal Nodes::LessThan.new(
+ attribute,
+ Nodes::Casted.new(3, attribute)
+ )
+ end
+
+ it "can be constructed with a quoted exclusive range starting from -Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.between(quoted_range(-::Float::INFINITY, 3, true))
+
+ node.must_equal Nodes::LessThan.new(
+ attribute,
+ Nodes::Quoted.new(3)
+ )
+ end
+
+ it "can be constructed with an infinite range" do
+ attribute = Attribute.new nil, nil
+ node = attribute.between(-::Float::INFINITY..::Float::INFINITY)
+
+ node.must_equal Nodes::NotIn.new(attribute, [])
+ end
+
+ it "can be constructed with a quoted infinite range" do
+ attribute = Attribute.new nil, nil
+ node = attribute.between(quoted_range(-::Float::INFINITY, ::Float::INFINITY, false))
+
+ node.must_equal Nodes::NotIn.new(attribute, [])
+ end
+
+
+ it "can be constructed with a range ending at Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.between(0..::Float::INFINITY)
+
+ node.must_equal Nodes::GreaterThanOrEqual.new(
+ attribute,
+ Nodes::Casted.new(0, attribute)
+ )
+ end
+
+ it "can be constructed with a quoted range ending at Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.between(quoted_range(0, ::Float::INFINITY, false))
+
+ node.must_equal Nodes::GreaterThanOrEqual.new(
+ attribute,
+ Nodes::Quoted.new(0)
+ )
+ end
+
+ it "can be constructed with an exclusive range" do
+ attribute = Attribute.new nil, nil
+ node = attribute.between(0...3)
+
+ node.must_equal Nodes::And.new([
+ Nodes::GreaterThanOrEqual.new(
+ attribute,
+ Nodes::Casted.new(0, attribute)
+ ),
+ Nodes::LessThan.new(
+ attribute,
+ Nodes::Casted.new(3, attribute)
+ )
+ ])
+ end
+
+ def quoted_range(begin_val, end_val, exclude)
+ OpenStruct.new(
+ begin: Nodes::Quoted.new(begin_val),
+ end: Nodes::Quoted.new(end_val),
+ exclude_end?: exclude,
+ )
+ end
+ end
+
+ describe "#in" do
+ it "can be constructed with a subquery" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:name].does_not_match_all(["%chunky%", "%bacon%"])
+ attribute = Attribute.new nil, nil
+
+ node = attribute.in(mgr)
+
+ node.must_equal Nodes::In.new(attribute, mgr.ast)
+ end
+
+ it "can be constructed with a list" do
+ attribute = Attribute.new nil, nil
+ node = attribute.in([1, 2, 3])
+
+ node.must_equal Nodes::In.new(
+ attribute,
+ [
+ Nodes::Casted.new(1, attribute),
+ Nodes::Casted.new(2, attribute),
+ Nodes::Casted.new(3, attribute),
+ ]
+ )
+ end
+
+ it "can be constructed with a random object" do
+ attribute = Attribute.new nil, nil
+ random_object = Object.new
+ node = attribute.in(random_object)
+
+ node.must_equal Nodes::In.new(
+ attribute,
+ Nodes::Casted.new(random_object, attribute)
+ )
+ end
+
+ it "should generate IN in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].in([1, 2, 3])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."id" IN (1, 2, 3)
+ }
+ end
+ end
+
+ describe "#in_any" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].in_any([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ORs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].in_any([[1, 2], [3, 4]])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" IN (1, 2) OR "users"."id" IN (3, 4))
+ }
+ end
+ end
+
+ describe "#in_all" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].in_all([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ANDs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].in_all([[1, 2], [3, 4]])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" IN (1, 2) AND "users"."id" IN (3, 4))
+ }
+ end
+ end
+
+ describe "with a range" do
+ it "can be constructed with a standard range" do
+ attribute = Attribute.new nil, nil
+ node = attribute.not_between(1..3)
+
+ node.must_equal Nodes::Grouping.new(Nodes::Or.new(
+ Nodes::LessThan.new(
+ attribute,
+ Nodes::Casted.new(1, attribute)
+ ),
+ Nodes::GreaterThan.new(
+ attribute,
+ Nodes::Casted.new(3, attribute)
+ )
+ ))
+ end
+
+ it "can be constructed with a range starting from -Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.not_between(-::Float::INFINITY..3)
+
+ node.must_equal Nodes::GreaterThan.new(
+ attribute,
+ Nodes::Casted.new(3, attribute)
+ )
+ end
+
+ it "can be constructed with an exclusive range starting from -Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.not_between(-::Float::INFINITY...3)
+
+ node.must_equal Nodes::GreaterThanOrEqual.new(
+ attribute,
+ Nodes::Casted.new(3, attribute)
+ )
+ end
+
+ it "can be constructed with an infinite range" do
+ attribute = Attribute.new nil, nil
+ node = attribute.not_between(-::Float::INFINITY..::Float::INFINITY)
+
+ node.must_equal Nodes::In.new(attribute, [])
+ end
+
+ it "can be constructed with a range ending at Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.not_between(0..::Float::INFINITY)
+
+ node.must_equal Nodes::LessThan.new(
+ attribute,
+ Nodes::Casted.new(0, attribute)
+ )
+ end
+
+ it "can be constructed with an exclusive range" do
+ attribute = Attribute.new nil, nil
+ node = attribute.not_between(0...3)
+
+ node.must_equal Nodes::Grouping.new(Nodes::Or.new(
+ Nodes::LessThan.new(
+ attribute,
+ Nodes::Casted.new(0, attribute)
+ ),
+ Nodes::GreaterThanOrEqual.new(
+ attribute,
+ Nodes::Casted.new(3, attribute)
+ )
+ ))
+ end
+ end
+
+ describe "#not_in" do
+ it "can be constructed with a subquery" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:name].does_not_match_all(["%chunky%", "%bacon%"])
+ attribute = Attribute.new nil, nil
+
+ node = attribute.not_in(mgr)
+
+ node.must_equal Nodes::NotIn.new(attribute, mgr.ast)
+ end
+
+ it "can be constructed with a Union" do
+ relation = Table.new(:users)
+ mgr1 = relation.project(relation[:id])
+ mgr2 = relation.project(relation[:id])
+
+ union = mgr1.union(mgr2)
+ node = relation[:id].in(union)
+ node.to_sql.must_be_like %{
+ "users"."id" IN (( SELECT "users"."id" FROM "users" UNION SELECT "users"."id" FROM "users" ))
+ }
+ end
+
+ it "can be constructed with a list" do
+ attribute = Attribute.new nil, nil
+ node = attribute.not_in([1, 2, 3])
+
+ node.must_equal Nodes::NotIn.new(
+ attribute,
+ [
+ Nodes::Casted.new(1, attribute),
+ Nodes::Casted.new(2, attribute),
+ Nodes::Casted.new(3, attribute),
+ ]
+ )
+ end
+
+ it "can be constructed with a random object" do
+ attribute = Attribute.new nil, nil
+ random_object = Object.new
+ node = attribute.not_in(random_object)
+
+ node.must_equal Nodes::NotIn.new(
+ attribute,
+ Nodes::Casted.new(random_object, attribute)
+ )
+ end
+
+ it "should generate NOT IN in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].not_in([1, 2, 3])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."id" NOT IN (1, 2, 3)
+ }
+ end
+ end
+
+ describe "#not_in_any" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].not_in_any([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ORs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].not_in_any([[1, 2], [3, 4]])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" NOT IN (1, 2) OR "users"."id" NOT IN (3, 4))
+ }
+ end
+ end
+
+ describe "#not_in_all" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].not_in_all([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ANDs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].not_in_all([[1, 2], [3, 4]])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" NOT IN (1, 2) AND "users"."id" NOT IN (3, 4))
+ }
+ end
+ end
+
+ describe "#eq_all" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].eq_all([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ANDs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].eq_all([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" = 1 AND "users"."id" = 2)
+ }
+ end
+ end
+
+ describe "#asc" do
+ it "should create an Ascending node" do
+ relation = Table.new(:users)
+ relation[:id].asc.must_be_kind_of Nodes::Ascending
+ end
+
+ it "should generate ASC in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.order relation[:id].asc
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" ORDER BY "users"."id" ASC
+ }
+ end
+ end
+
+ describe "#desc" do
+ it "should create a Descending node" do
+ relation = Table.new(:users)
+ relation[:id].desc.must_be_kind_of Nodes::Descending
+ end
+
+ it "should generate DESC in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.order relation[:id].desc
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" ORDER BY "users"."id" DESC
+ }
+ end
+ end
+
+ describe "equality" do
+ describe "#to_sql" do
+ it "should produce sql" do
+ table = Table.new :users
+ condition = table["id"].eq 1
+ condition.to_sql.must_equal '"users"."id" = 1'
+ end
+ end
+ end
+
+ describe "type casting" do
+ it "does not type cast by default" do
+ table = Table.new(:foo)
+ condition = table["id"].eq("1")
+
+ assert_not table.able_to_type_cast?
+ condition.to_sql.must_equal %("foo"."id" = '1')
+ end
+
+ it "type casts when given an explicit caster" do
+ fake_caster = Object.new
+ def fake_caster.type_cast_for_database(attr_name, value)
+ if attr_name == "id"
+ value.to_i
+ else
+ value
+ end
+ end
+ table = Table.new(:foo, type_caster: fake_caster)
+ condition = table["id"].eq("1").and(table["other_id"].eq("2"))
+
+ assert table.able_to_type_cast?
+ condition.to_sql.must_equal %("foo"."id" = 1 AND "foo"."other_id" = '2')
+ end
+
+ it "does not type cast SqlLiteral nodes" do
+ fake_caster = Object.new
+ def fake_caster.type_cast_for_database(attr_name, value)
+ value.to_i
+ end
+ table = Table.new(:foo, type_caster: fake_caster)
+ condition = table["id"].eq(Arel.sql("(select 1)"))
+
+ assert table.able_to_type_cast?
+ condition.to_sql.must_equal %("foo"."id" = (select 1))
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/attributes/math_test.rb b/activerecord/test/cases/arel/attributes/math_test.rb
new file mode 100644
index 0000000000..41eea217c0
--- /dev/null
+++ b/activerecord/test/cases/arel/attributes/math_test.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Attributes
+ class MathTest < Arel::Spec
+ %i[* /].each do |math_operator|
+ it "average should be compatible with #{math_operator}" do
+ table = Arel::Table.new :users
+ (table[:id].average.public_send(math_operator, 2)).to_sql.must_be_like %{
+ AVG("users"."id") #{math_operator} 2
+ }
+ end
+
+ it "count should be compatible with #{math_operator}" do
+ table = Arel::Table.new :users
+ (table[:id].count.public_send(math_operator, 2)).to_sql.must_be_like %{
+ COUNT("users"."id") #{math_operator} 2
+ }
+ end
+
+ it "maximum should be compatible with #{math_operator}" do
+ table = Arel::Table.new :users
+ (table[:id].maximum.public_send(math_operator, 2)).to_sql.must_be_like %{
+ MAX("users"."id") #{math_operator} 2
+ }
+ end
+
+ it "minimum should be compatible with #{math_operator}" do
+ table = Arel::Table.new :users
+ (table[:id].minimum.public_send(math_operator, 2)).to_sql.must_be_like %{
+ MIN("users"."id") #{math_operator} 2
+ }
+ end
+
+ it "attribute node should be compatible with #{math_operator}" do
+ table = Arel::Table.new :users
+ (table[:id].public_send(math_operator, 2)).to_sql.must_be_like %{
+ "users"."id" #{math_operator} 2
+ }
+ end
+ end
+
+ %i[+ - & | ^ << >>].each do |math_operator|
+ it "average should be compatible with #{math_operator}" do
+ table = Arel::Table.new :users
+ (table[:id].average.public_send(math_operator, 2)).to_sql.must_be_like %{
+ (AVG("users"."id") #{math_operator} 2)
+ }
+ end
+
+ it "count should be compatible with #{math_operator}" do
+ table = Arel::Table.new :users
+ (table[:id].count.public_send(math_operator, 2)).to_sql.must_be_like %{
+ (COUNT("users"."id") #{math_operator} 2)
+ }
+ end
+
+ it "maximum should be compatible with #{math_operator}" do
+ table = Arel::Table.new :users
+ (table[:id].maximum.public_send(math_operator, 2)).to_sql.must_be_like %{
+ (MAX("users"."id") #{math_operator} 2)
+ }
+ end
+
+ it "minimum should be compatible with #{math_operator}" do
+ table = Arel::Table.new :users
+ (table[:id].minimum.public_send(math_operator, 2)).to_sql.must_be_like %{
+ (MIN("users"."id") #{math_operator} 2)
+ }
+ end
+
+ it "attribute node should be compatible with #{math_operator}" do
+ table = Arel::Table.new :users
+ (table[:id].public_send(math_operator, 2)).to_sql.must_be_like %{
+ ("users"."id" #{math_operator} 2)
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/attributes_test.rb b/activerecord/test/cases/arel/attributes_test.rb
new file mode 100644
index 0000000000..b00af4bd29
--- /dev/null
+++ b/activerecord/test/cases/arel/attributes_test.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require_relative "helper"
+
+module Arel
+ describe "Attributes" do
+ it "responds to lower" do
+ relation = Table.new(:users)
+ attribute = relation[:foo]
+ node = attribute.lower
+ assert_equal "LOWER", node.name
+ assert_equal [attribute], node.expressions
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ array = [Attribute.new("foo", "bar"), Attribute.new("foo", "bar")]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ array = [Attribute.new("foo", "bar"), Attribute.new("foo", "baz")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+
+ describe "for" do
+ it "deals with unknown column types" do
+ column = Struct.new(:type).new :crazy
+ Attributes.for(column).must_equal Attributes::Undefined
+ end
+
+ it "returns the correct constant for strings" do
+ [:string, :text, :binary].each do |type|
+ column = Struct.new(:type).new type
+ Attributes.for(column).must_equal Attributes::String
+ end
+ end
+
+ it "returns the correct constant for ints" do
+ column = Struct.new(:type).new :integer
+ Attributes.for(column).must_equal Attributes::Integer
+ end
+
+ it "returns the correct constant for floats" do
+ column = Struct.new(:type).new :float
+ Attributes.for(column).must_equal Attributes::Float
+ end
+
+ it "returns the correct constant for decimals" do
+ column = Struct.new(:type).new :decimal
+ Attributes.for(column).must_equal Attributes::Decimal
+ end
+
+ it "returns the correct constant for boolean" do
+ column = Struct.new(:type).new :boolean
+ Attributes.for(column).must_equal Attributes::Boolean
+ end
+
+ it "returns the correct constant for time" do
+ [:date, :datetime, :timestamp, :time].each do |type|
+ column = Struct.new(:type).new type
+ Attributes.for(column).must_equal Attributes::Time
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/collectors/bind_test.rb b/activerecord/test/cases/arel/collectors/bind_test.rb
new file mode 100644
index 0000000000..ffa9b15f66
--- /dev/null
+++ b/activerecord/test/cases/arel/collectors/bind_test.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+require "arel/collectors/bind"
+
+module Arel
+ module Collectors
+ class TestBind < Arel::Test
+ def setup
+ @conn = FakeRecord::Base.new
+ @visitor = Visitors::ToSql.new @conn.connection
+ super
+ end
+
+ def collect(node)
+ @visitor.accept(node, Collectors::Bind.new)
+ end
+
+ def compile(node)
+ collect(node).value
+ end
+
+ def ast_with_binds(bvs)
+ table = Table.new(:users)
+ manager = Arel::SelectManager.new table
+ manager.where(table[:age].eq(Nodes::BindParam.new(bvs.shift)))
+ manager.where(table[:name].eq(Nodes::BindParam.new(bvs.shift)))
+ manager.ast
+ end
+
+ def test_compile_gathers_all_bind_params
+ binds = compile(ast_with_binds(["hello", "world"]))
+ assert_equal ["hello", "world"], binds
+
+ binds = compile(ast_with_binds(["hello2", "world3"]))
+ assert_equal ["hello2", "world3"], binds
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/collectors/composite_test.rb b/activerecord/test/cases/arel/collectors/composite_test.rb
new file mode 100644
index 0000000000..545637496f
--- /dev/null
+++ b/activerecord/test/cases/arel/collectors/composite_test.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+require "arel/collectors/bind"
+require "arel/collectors/composite"
+
+module Arel
+ module Collectors
+ class TestComposite < Arel::Test
+ def setup
+ @conn = FakeRecord::Base.new
+ @visitor = Visitors::ToSql.new @conn.connection
+ super
+ end
+
+ def collect(node)
+ sql_collector = Collectors::SQLString.new
+ bind_collector = Collectors::Bind.new
+ collector = Collectors::Composite.new(sql_collector, bind_collector)
+ @visitor.accept(node, collector)
+ end
+
+ def compile(node)
+ collect(node).value
+ end
+
+ def ast_with_binds(bvs)
+ table = Table.new(:users)
+ manager = Arel::SelectManager.new table
+ manager.where(table[:age].eq(Nodes::BindParam.new(bvs.shift)))
+ manager.where(table[:name].eq(Nodes::BindParam.new(bvs.shift)))
+ manager.ast
+ end
+
+ def test_composite_collector_performs_multiple_collections_at_once
+ sql, binds = compile(ast_with_binds(["hello", "world"]))
+ assert_equal 'SELECT FROM "users" WHERE "users"."age" = ? AND "users"."name" = ?', sql
+ assert_equal ["hello", "world"], binds
+
+ sql, binds = compile(ast_with_binds(["hello2", "world3"]))
+ assert_equal 'SELECT FROM "users" WHERE "users"."age" = ? AND "users"."name" = ?', sql
+ assert_equal ["hello2", "world3"], binds
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/collectors/sql_string_test.rb b/activerecord/test/cases/arel/collectors/sql_string_test.rb
new file mode 100644
index 0000000000..443c7eb54b
--- /dev/null
+++ b/activerecord/test/cases/arel/collectors/sql_string_test.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Collectors
+ class TestSqlString < Arel::Test
+ def setup
+ @conn = FakeRecord::Base.new
+ @visitor = Visitors::ToSql.new @conn.connection
+ super
+ end
+
+ def collect(node)
+ @visitor.accept(node, Collectors::SQLString.new)
+ end
+
+ def compile(node)
+ collect(node).value
+ end
+
+ def ast_with_binds
+ table = Table.new(:users)
+ manager = Arel::SelectManager.new table
+ manager.where(table[:age].eq(Nodes::BindParam.new("hello")))
+ manager.where(table[:name].eq(Nodes::BindParam.new("world")))
+ manager.ast
+ end
+
+ def test_compile
+ sql = compile(ast_with_binds)
+ assert_equal 'SELECT FROM "users" WHERE "users"."age" = ? AND "users"."name" = ?', sql
+ end
+
+ def test_returned_sql_uses_utf8_encoding
+ sql = compile(ast_with_binds)
+ assert_equal sql.encoding, Encoding::UTF_8
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/collectors/substitute_bind_collector_test.rb b/activerecord/test/cases/arel/collectors/substitute_bind_collector_test.rb
new file mode 100644
index 0000000000..255c8e79e9
--- /dev/null
+++ b/activerecord/test/cases/arel/collectors/substitute_bind_collector_test.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+require "arel/collectors/substitute_binds"
+require "arel/collectors/sql_string"
+
+module Arel
+ module Collectors
+ class TestSubstituteBindCollector < Arel::Test
+ def setup
+ @conn = FakeRecord::Base.new
+ @visitor = Visitors::ToSql.new @conn.connection
+ super
+ end
+
+ def ast_with_binds
+ table = Table.new(:users)
+ manager = Arel::SelectManager.new table
+ manager.where(table[:age].eq(Nodes::BindParam.new("hello")))
+ manager.where(table[:name].eq(Nodes::BindParam.new("world")))
+ manager.ast
+ end
+
+ def compile(node, quoter)
+ collector = Collectors::SubstituteBinds.new(quoter, Collectors::SQLString.new)
+ @visitor.accept(node, collector).value
+ end
+
+ def test_compile
+ quoter = Object.new
+ def quoter.quote(val)
+ val.to_s
+ end
+ sql = compile(ast_with_binds, quoter)
+ assert_equal 'SELECT FROM "users" WHERE "users"."age" = hello AND "users"."name" = world', sql
+ end
+
+ def test_quoting_is_delegated_to_quoter
+ quoter = Object.new
+ def quoter.quote(val)
+ val.inspect
+ end
+ sql = compile(ast_with_binds, quoter)
+ assert_equal 'SELECT FROM "users" WHERE "users"."age" = "hello" AND "users"."name" = "world"', sql
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/crud_test.rb b/activerecord/test/cases/arel/crud_test.rb
new file mode 100644
index 0000000000..f3cdd8927f
--- /dev/null
+++ b/activerecord/test/cases/arel/crud_test.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require_relative "helper"
+
+module Arel
+ class FakeCrudder < SelectManager
+ class FakeEngine
+ attr_reader :calls, :connection_pool, :spec, :config
+
+ def initialize
+ @calls = []
+ @connection_pool = self
+ @spec = self
+ @config = { adapter: "sqlite3" }
+ end
+
+ def connection; self end
+
+ def method_missing(name, *args)
+ @calls << [name, args]
+ end
+ end
+
+ include Crud
+
+ attr_reader :engine
+ attr_accessor :ctx
+
+ def initialize(engine = FakeEngine.new)
+ super
+ end
+ end
+
+ describe "crud" do
+ describe "insert" do
+ it "should call insert on the connection" do
+ table = Table.new :users
+ fc = FakeCrudder.new
+ fc.from table
+ im = fc.compile_insert [[table[:id], "foo"]]
+ assert_instance_of Arel::InsertManager, im
+ end
+ end
+
+ describe "update" do
+ it "should call update on the connection" do
+ table = Table.new :users
+ fc = FakeCrudder.new
+ fc.from table
+ stmt = fc.compile_update [[table[:id], "foo"]], Arel::Attributes::Attribute.new(table, "id")
+ assert_instance_of Arel::UpdateManager, stmt
+ end
+ end
+
+ describe "delete" do
+ it "should call delete on the connection" do
+ table = Table.new :users
+ fc = FakeCrudder.new
+ fc.from table
+ stmt = fc.compile_delete
+ assert_instance_of Arel::DeleteManager, stmt
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/delete_manager_test.rb b/activerecord/test/cases/arel/delete_manager_test.rb
new file mode 100644
index 0000000000..0bad02f4d2
--- /dev/null
+++ b/activerecord/test/cases/arel/delete_manager_test.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require_relative "helper"
+
+module Arel
+ class DeleteManagerTest < Arel::Spec
+ describe "new" do
+ it "takes an engine" do
+ Arel::DeleteManager.new
+ end
+ end
+
+ it "handles limit properly" do
+ table = Table.new(:users)
+ dm = Arel::DeleteManager.new
+ dm.take 10
+ dm.from table
+ dm.key = table[:id]
+ assert_match(/LIMIT 10/, dm.to_sql)
+ end
+
+ describe "from" do
+ it "uses from" do
+ table = Table.new(:users)
+ dm = Arel::DeleteManager.new
+ dm.from table
+ dm.to_sql.must_be_like %{ DELETE FROM "users" }
+ end
+
+ it "chains" do
+ table = Table.new(:users)
+ dm = Arel::DeleteManager.new
+ dm.from(table).must_equal dm
+ end
+ end
+
+ describe "where" do
+ it "uses where values" do
+ table = Table.new(:users)
+ dm = Arel::DeleteManager.new
+ dm.from table
+ dm.where table[:id].eq(10)
+ dm.to_sql.must_be_like %{ DELETE FROM "users" WHERE "users"."id" = 10}
+ end
+
+ it "chains" do
+ table = Table.new(:users)
+ dm = Arel::DeleteManager.new
+ dm.where(table[:id].eq(10)).must_equal dm
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/factory_methods_test.rb b/activerecord/test/cases/arel/factory_methods_test.rb
new file mode 100644
index 0000000000..26d2cdd08d
--- /dev/null
+++ b/activerecord/test/cases/arel/factory_methods_test.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require_relative "helper"
+
+module Arel
+ module FactoryMethods
+ class TestFactoryMethods < Arel::Test
+ class Factory
+ include Arel::FactoryMethods
+ end
+
+ def setup
+ @factory = Factory.new
+ end
+
+ def test_create_join
+ join = @factory.create_join :one, :two
+ assert_kind_of Nodes::Join, join
+ assert_equal :two, join.right
+ end
+
+ def test_create_on
+ on = @factory.create_on :one
+ assert_instance_of Nodes::On, on
+ assert_equal :one, on.expr
+ end
+
+ def test_create_true
+ true_node = @factory.create_true
+ assert_instance_of Nodes::True, true_node
+ end
+
+ def test_create_false
+ false_node = @factory.create_false
+ assert_instance_of Nodes::False, false_node
+ end
+
+ def test_lower
+ lower = @factory.lower :one
+ assert_instance_of Nodes::NamedFunction, lower
+ assert_equal "LOWER", lower.name
+ assert_equal [:one], lower.expressions.map(&:expr)
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/helper.rb b/activerecord/test/cases/arel/helper.rb
new file mode 100644
index 0000000000..f8ce658440
--- /dev/null
+++ b/activerecord/test/cases/arel/helper.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require "active_support"
+require "minitest/autorun"
+require "arel"
+
+require_relative "support/fake_record"
+
+class Object
+ def must_be_like(other)
+ gsub(/\s+/, " ").strip.must_equal other.gsub(/\s+/, " ").strip
+ end
+end
+
+module Arel
+ class Test < ActiveSupport::TestCase
+ def setup
+ super
+ @arel_engine = Arel::Table.engine
+ Arel::Table.engine = FakeRecord::Base.new
+ end
+
+ def teardown
+ Arel::Table.engine = @arel_engine if defined? @arel_engine
+ super
+ end
+ end
+
+ class Spec < Minitest::Spec
+ before do
+ @arel_engine = Arel::Table.engine
+ Arel::Table.engine = FakeRecord::Base.new
+ end
+
+ after do
+ Arel::Table.engine = @arel_engine if defined? @arel_engine
+ end
+ include ActiveSupport::Testing::Assertions
+
+ # test/unit backwards compatibility methods
+ alias :assert_no_match :refute_match
+ alias :assert_not_equal :refute_equal
+ alias :assert_not_same :refute_same
+ end
+end
diff --git a/activerecord/test/cases/arel/insert_manager_test.rb b/activerecord/test/cases/arel/insert_manager_test.rb
new file mode 100644
index 0000000000..2376ad8d37
--- /dev/null
+++ b/activerecord/test/cases/arel/insert_manager_test.rb
@@ -0,0 +1,242 @@
+# frozen_string_literal: true
+
+require_relative "helper"
+
+module Arel
+ class InsertManagerTest < Arel::Spec
+ describe "new" do
+ it "takes an engine" do
+ Arel::InsertManager.new
+ end
+ end
+
+ describe "insert" do
+ it "can create a Values node" do
+ manager = Arel::InsertManager.new
+ values = manager.create_values %w{ a b }, %w{ c d }
+
+ assert_kind_of Arel::Nodes::Values, values
+ assert_equal %w{ a b }, values.left
+ assert_equal %w{ c d }, values.right
+ end
+
+ it "allows sql literals" do
+ manager = Arel::InsertManager.new
+ manager.into Table.new(:users)
+ manager.values = manager.create_values [Arel.sql("*")], %w{ a }
+ manager.to_sql.must_be_like %{
+ INSERT INTO \"users\" VALUES (*)
+ }
+ end
+
+ it "works with multiple values" do
+ table = Table.new(:users)
+ manager = Arel::InsertManager.new
+ manager.into table
+
+ manager.columns << table[:id]
+ manager.columns << table[:name]
+
+ manager.values = manager.create_values_list([
+ %w{1 david},
+ %w{2 kir},
+ ["3", Arel.sql("DEFAULT")],
+ ])
+
+ manager.to_sql.must_be_like %{
+ INSERT INTO \"users\" (\"id\", \"name\") VALUES ('1', 'david'), ('2', 'kir'), ('3', DEFAULT)
+ }
+ end
+
+ it "literals in multiple values are not escaped" do
+ table = Table.new(:users)
+ manager = Arel::InsertManager.new
+ manager.into table
+
+ manager.columns << table[:name]
+
+ manager.values = manager.create_values_list([
+ [Arel.sql("*")],
+ [Arel.sql("DEFAULT")],
+ ])
+
+ manager.to_sql.must_be_like %{
+ INSERT INTO \"users\" (\"name\") VALUES (*), (DEFAULT)
+ }
+ end
+
+ it "works with multiple single values" do
+ table = Table.new(:users)
+ manager = Arel::InsertManager.new
+ manager.into table
+
+ manager.columns << table[:name]
+
+ manager.values = manager.create_values_list([
+ %w{david},
+ %w{kir},
+ [Arel.sql("DEFAULT")],
+ ])
+
+ manager.to_sql.must_be_like %{
+ INSERT INTO \"users\" (\"name\") VALUES ('david'), ('kir'), (DEFAULT)
+ }
+ end
+
+ it "inserts false" do
+ table = Table.new(:users)
+ manager = Arel::InsertManager.new
+
+ manager.insert [[table[:bool], false]]
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users" ("bool") VALUES ('f')
+ }
+ end
+
+ it "inserts null" do
+ table = Table.new(:users)
+ manager = Arel::InsertManager.new
+ manager.insert [[table[:id], nil]]
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users" ("id") VALUES (NULL)
+ }
+ end
+
+ it "inserts time" do
+ table = Table.new(:users)
+ manager = Arel::InsertManager.new
+
+ time = Time.now
+ attribute = table[:created_at]
+
+ manager.insert [[attribute, time]]
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users" ("created_at") VALUES (#{Table.engine.connection.quote time})
+ }
+ end
+
+ it "takes a list of lists" do
+ table = Table.new(:users)
+ manager = Arel::InsertManager.new
+ manager.into table
+ manager.insert [[table[:id], 1], [table[:name], "aaron"]]
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users" ("id", "name") VALUES (1, 'aaron')
+ }
+ end
+
+ it "defaults the table" do
+ table = Table.new(:users)
+ manager = Arel::InsertManager.new
+ manager.insert [[table[:id], 1], [table[:name], "aaron"]]
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users" ("id", "name") VALUES (1, 'aaron')
+ }
+ end
+
+ it "noop for empty list" do
+ table = Table.new(:users)
+ manager = Arel::InsertManager.new
+ manager.insert [[table[:id], 1]]
+ manager.insert []
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users" ("id") VALUES (1)
+ }
+ end
+
+ it "is chainable" do
+ table = Table.new(:users)
+ manager = Arel::InsertManager.new
+ insert_result = manager.insert [[table[:id], 1]]
+ assert_equal manager, insert_result
+ end
+ end
+
+ describe "into" do
+ it "takes a Table and chains" do
+ manager = Arel::InsertManager.new
+ manager.into(Table.new(:users)).must_equal manager
+ end
+
+ it "converts to sql" do
+ table = Table.new :users
+ manager = Arel::InsertManager.new
+ manager.into table
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users"
+ }
+ end
+ end
+
+ describe "columns" do
+ it "converts to sql" do
+ table = Table.new :users
+ manager = Arel::InsertManager.new
+ manager.into table
+ manager.columns << table[:id]
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users" ("id")
+ }
+ end
+ end
+
+ describe "values" do
+ it "converts to sql" do
+ table = Table.new :users
+ manager = Arel::InsertManager.new
+ manager.into table
+
+ manager.values = Nodes::Values.new [1]
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users" VALUES (1)
+ }
+ end
+
+ it "accepts sql literals" do
+ table = Table.new :users
+ manager = Arel::InsertManager.new
+ manager.into table
+
+ manager.values = Arel.sql("DEFAULT VALUES")
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users" DEFAULT VALUES
+ }
+ end
+ end
+
+ describe "combo" do
+ it "combines columns and values list in order" do
+ table = Table.new :users
+ manager = Arel::InsertManager.new
+ manager.into table
+
+ manager.values = Nodes::Values.new [1, "aaron"]
+ manager.columns << table[:id]
+ manager.columns << table[:name]
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users" ("id", "name") VALUES (1, 'aaron')
+ }
+ end
+ end
+
+ describe "select" do
+ it "accepts a select query in place of a VALUES clause" do
+ table = Table.new :users
+
+ manager = Arel::InsertManager.new
+ manager.into table
+
+ select = Arel::SelectManager.new
+ select.project Arel.sql("1")
+ select.project Arel.sql('"aaron"')
+
+ manager.select select
+ manager.columns << table[:id]
+ manager.columns << table[:name]
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users" ("id", "name") (SELECT 1, "aaron")
+ }
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/and_test.rb b/activerecord/test/cases/arel/nodes/and_test.rb
new file mode 100644
index 0000000000..eff54abd91
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/and_test.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe "And" do
+ describe "equality" do
+ it "is equal with equal ivars" do
+ array = [And.new(["foo", "bar"]), And.new(["foo", "bar"])]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ array = [And.new(["foo", "bar"]), And.new(["foo", "baz"])]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/as_test.rb b/activerecord/test/cases/arel/nodes/as_test.rb
new file mode 100644
index 0000000000..1169ea11c9
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/as_test.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe "As" do
+ describe "#as" do
+ it "makes an AS node" do
+ attr = Table.new(:users)[:id]
+ as = attr.as(Arel.sql("foo"))
+ assert_equal attr, as.left
+ assert_equal "foo", as.right
+ end
+
+ it "converts right to SqlLiteral if a string" do
+ attr = Table.new(:users)[:id]
+ as = attr.as("foo")
+ assert_kind_of Arel::Nodes::SqlLiteral, as.right
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ array = [As.new("foo", "bar"), As.new("foo", "bar")]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ array = [As.new("foo", "bar"), As.new("foo", "baz")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/ascending_test.rb b/activerecord/test/cases/arel/nodes/ascending_test.rb
new file mode 100644
index 0000000000..4811e6ff5b
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/ascending_test.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ class TestAscending < Arel::Test
+ def test_construct
+ ascending = Ascending.new "zomg"
+ assert_equal "zomg", ascending.expr
+ end
+
+ def test_reverse
+ ascending = Ascending.new "zomg"
+ descending = ascending.reverse
+ assert_kind_of Descending, descending
+ assert_equal ascending.expr, descending.expr
+ end
+
+ def test_direction
+ ascending = Ascending.new "zomg"
+ assert_equal :asc, ascending.direction
+ end
+
+ def test_ascending?
+ ascending = Ascending.new "zomg"
+ assert ascending.ascending?
+ end
+
+ def test_descending?
+ ascending = Ascending.new "zomg"
+ assert_not ascending.descending?
+ end
+
+ def test_equality_with_same_ivars
+ array = [Ascending.new("zomg"), Ascending.new("zomg")]
+ assert_equal 1, array.uniq.size
+ end
+
+ def test_inequality_with_different_ivars
+ array = [Ascending.new("zomg"), Ascending.new("zomg!")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/bin_test.rb b/activerecord/test/cases/arel/nodes/bin_test.rb
new file mode 100644
index 0000000000..ee2ec3cf2f
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/bin_test.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ class TestBin < Arel::Test
+ def test_new
+ assert Arel::Nodes::Bin.new("zomg")
+ end
+
+ def test_default_to_sql
+ viz = Arel::Visitors::ToSql.new Table.engine.connection_pool
+ node = Arel::Nodes::Bin.new(Arel.sql("zomg"))
+ assert_equal "zomg", viz.accept(node, Collectors::SQLString.new).value
+ end
+
+ def test_mysql_to_sql
+ viz = Arel::Visitors::MySQL.new Table.engine.connection_pool
+ node = Arel::Nodes::Bin.new(Arel.sql("zomg"))
+ assert_equal "BINARY zomg", viz.accept(node, Collectors::SQLString.new).value
+ end
+
+ def test_equality_with_same_ivars
+ array = [Bin.new("zomg"), Bin.new("zomg")]
+ assert_equal 1, array.uniq.size
+ end
+
+ def test_inequality_with_different_ivars
+ array = [Bin.new("zomg"), Bin.new("zomg!")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/binary_test.rb b/activerecord/test/cases/arel/nodes/binary_test.rb
new file mode 100644
index 0000000000..d160e7cd9d
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/binary_test.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ class NodesTest < Arel::Spec
+ describe "Binary" do
+ describe "#hash" do
+ it "generates a hash based on its value" do
+ eq = Equality.new("foo", "bar")
+ eq2 = Equality.new("foo", "bar")
+ eq3 = Equality.new("bar", "baz")
+
+ assert_equal eq.hash, eq2.hash
+ assert_not_equal eq.hash, eq3.hash
+ end
+
+ it "generates a hash specific to its class" do
+ eq = Equality.new("foo", "bar")
+ neq = NotEqual.new("foo", "bar")
+
+ assert_not_equal eq.hash, neq.hash
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/bind_param_test.rb b/activerecord/test/cases/arel/nodes/bind_param_test.rb
new file mode 100644
index 0000000000..37a362ece4
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/bind_param_test.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe "BindParam" do
+ it "is equal to other bind params with the same value" do
+ BindParam.new(1).must_equal(BindParam.new(1))
+ BindParam.new("foo").must_equal(BindParam.new("foo"))
+ end
+
+ it "is not equal to other nodes" do
+ BindParam.new(nil).wont_equal(Node.new)
+ end
+
+ it "is not equal to bind params with different values" do
+ BindParam.new(1).wont_equal(BindParam.new(2))
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/case_test.rb b/activerecord/test/cases/arel/nodes/case_test.rb
new file mode 100644
index 0000000000..89861488df
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/case_test.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ class NodesTest < Arel::Spec
+ describe "Case" do
+ describe "#initialize" do
+ it "sets case expression from first argument" do
+ node = Case.new "foo"
+
+ assert_equal "foo", node.case
+ end
+
+ it "sets default case from second argument" do
+ node = Case.new nil, "bar"
+
+ assert_equal "bar", node.default
+ end
+ end
+
+ describe "#clone" do
+ it "clones case, conditions and default" do
+ foo = Nodes.build_quoted "foo"
+
+ node = Case.new
+ node.case = foo
+ node.conditions = [When.new(foo, foo)]
+ node.default = foo
+
+ dolly = node.clone
+
+ assert_equal dolly.case, node.case
+ assert_not_same dolly.case, node.case
+
+ assert_equal dolly.conditions, node.conditions
+ assert_not_same dolly.conditions, node.conditions
+
+ assert_equal dolly.default, node.default
+ assert_not_same dolly.default, node.default
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ foo = Nodes.build_quoted "foo"
+ one = Nodes.build_quoted 1
+ zero = Nodes.build_quoted 0
+
+ case1 = Case.new foo
+ case1.conditions = [When.new(foo, one)]
+ case1.default = Else.new zero
+
+ case2 = Case.new foo
+ case2.conditions = [When.new(foo, one)]
+ case2.default = Else.new zero
+
+ array = [case1, case2]
+
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ foo = Nodes.build_quoted "foo"
+ bar = Nodes.build_quoted "bar"
+ one = Nodes.build_quoted 1
+ zero = Nodes.build_quoted 0
+
+ case1 = Case.new foo
+ case1.conditions = [When.new(foo, one)]
+ case1.default = Else.new zero
+
+ case2 = Case.new foo
+ case2.conditions = [When.new(bar, one)]
+ case2.default = Else.new zero
+
+ array = [case1, case2]
+
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/casted_test.rb b/activerecord/test/cases/arel/nodes/casted_test.rb
new file mode 100644
index 0000000000..e27f58a4e2
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/casted_test.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe Casted do
+ describe "#hash" do
+ it "is equal when eql? returns true" do
+ one = Casted.new 1, 2
+ also_one = Casted.new 1, 2
+
+ assert_equal one.hash, also_one.hash
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/count_test.rb b/activerecord/test/cases/arel/nodes/count_test.rb
new file mode 100644
index 0000000000..daabea6c4c
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/count_test.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+class Arel::Nodes::CountTest < Arel::Spec
+ describe "as" do
+ it "should alias the count" do
+ table = Arel::Table.new :users
+ table[:id].count.as("foo").to_sql.must_be_like %{
+ COUNT("users"."id") AS foo
+ }
+ end
+ end
+
+ describe "eq" do
+ it "should compare the count" do
+ table = Arel::Table.new :users
+ table[:id].count.eq(2).to_sql.must_be_like %{
+ COUNT("users"."id") = 2
+ }
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ array = [Arel::Nodes::Count.new("foo"), Arel::Nodes::Count.new("foo")]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ array = [Arel::Nodes::Count.new("foo"), Arel::Nodes::Count.new("foo!")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/delete_statement_test.rb b/activerecord/test/cases/arel/nodes/delete_statement_test.rb
new file mode 100644
index 0000000000..3f078063a4
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/delete_statement_test.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+describe Arel::Nodes::DeleteStatement do
+ describe "#clone" do
+ it "clones wheres" do
+ statement = Arel::Nodes::DeleteStatement.new
+ statement.wheres = %w[a b c]
+
+ dolly = statement.clone
+ dolly.wheres.must_equal statement.wheres
+ dolly.wheres.wont_be_same_as statement.wheres
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ statement1 = Arel::Nodes::DeleteStatement.new
+ statement1.wheres = %w[a b c]
+ statement2 = Arel::Nodes::DeleteStatement.new
+ statement2.wheres = %w[a b c]
+ array = [statement1, statement2]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ statement1 = Arel::Nodes::DeleteStatement.new
+ statement1.wheres = %w[a b c]
+ statement2 = Arel::Nodes::DeleteStatement.new
+ statement2.wheres = %w[1 2 3]
+ array = [statement1, statement2]
+ assert_equal 2, array.uniq.size
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/descending_test.rb b/activerecord/test/cases/arel/nodes/descending_test.rb
new file mode 100644
index 0000000000..5f1747e1da
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/descending_test.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ class TestDescending < Arel::Test
+ def test_construct
+ descending = Descending.new "zomg"
+ assert_equal "zomg", descending.expr
+ end
+
+ def test_reverse
+ descending = Descending.new "zomg"
+ ascending = descending.reverse
+ assert_kind_of Ascending, ascending
+ assert_equal descending.expr, ascending.expr
+ end
+
+ def test_direction
+ descending = Descending.new "zomg"
+ assert_equal :desc, descending.direction
+ end
+
+ def test_ascending?
+ descending = Descending.new "zomg"
+ assert_not descending.ascending?
+ end
+
+ def test_descending?
+ descending = Descending.new "zomg"
+ assert descending.descending?
+ end
+
+ def test_equality_with_same_ivars
+ array = [Descending.new("zomg"), Descending.new("zomg")]
+ assert_equal 1, array.uniq.size
+ end
+
+ def test_inequality_with_different_ivars
+ array = [Descending.new("zomg"), Descending.new("zomg!")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/distinct_test.rb b/activerecord/test/cases/arel/nodes/distinct_test.rb
new file mode 100644
index 0000000000..de5f0ee588
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/distinct_test.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe "Distinct" do
+ describe "equality" do
+ it "is equal to other distinct nodes" do
+ array = [Distinct.new, Distinct.new]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with other nodes" do
+ array = [Distinct.new, Node.new]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/equality_test.rb b/activerecord/test/cases/arel/nodes/equality_test.rb
new file mode 100644
index 0000000000..e173720e86
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/equality_test.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe "equality" do
+ # FIXME: backwards compat
+ describe "backwards compat" do
+ describe "operator" do
+ it "returns :==" do
+ attr = Table.new(:users)[:id]
+ left = attr.eq(10)
+ left.operator.must_equal :==
+ end
+ end
+
+ describe "operand1" do
+ it "should equal left" do
+ attr = Table.new(:users)[:id]
+ left = attr.eq(10)
+ left.left.must_equal left.operand1
+ end
+ end
+
+ describe "operand2" do
+ it "should equal right" do
+ attr = Table.new(:users)[:id]
+ left = attr.eq(10)
+ left.right.must_equal left.operand2
+ end
+ end
+
+ describe "to_sql" do
+ it "takes an engine" do
+ engine = FakeRecord::Base.new
+ engine.connection.extend Module.new {
+ attr_accessor :quote_count
+ def quote(*args) @quote_count += 1; super; end
+ def quote_column_name(*args) @quote_count += 1; super; end
+ def quote_table_name(*args) @quote_count += 1; super; end
+ }
+ engine.connection.quote_count = 0
+
+ attr = Table.new(:users)[:id]
+ test = attr.eq(10)
+ test.to_sql engine
+ engine.connection.quote_count.must_equal 3
+ end
+ end
+ end
+
+ describe "or" do
+ it "makes an OR node" do
+ attr = Table.new(:users)[:id]
+ left = attr.eq(10)
+ right = attr.eq(11)
+ node = left.or right
+ node.expr.left.must_equal left
+ node.expr.right.must_equal right
+ end
+ end
+
+ describe "and" do
+ it "makes and AND node" do
+ attr = Table.new(:users)[:id]
+ left = attr.eq(10)
+ right = attr.eq(11)
+ node = left.and right
+ node.left.must_equal left
+ node.right.must_equal right
+ end
+ end
+
+ it "is equal with equal ivars" do
+ array = [Equality.new("foo", "bar"), Equality.new("foo", "bar")]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ array = [Equality.new("foo", "bar"), Equality.new("foo", "baz")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/extract_test.rb b/activerecord/test/cases/arel/nodes/extract_test.rb
new file mode 100644
index 0000000000..8fc1e04d67
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/extract_test.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+class Arel::Nodes::ExtractTest < Arel::Spec
+ it "should extract field" do
+ table = Arel::Table.new :users
+ table[:timestamp].extract("date").to_sql.must_be_like %{
+ EXTRACT(DATE FROM "users"."timestamp")
+ }
+ end
+
+ describe "as" do
+ it "should alias the extract" do
+ table = Arel::Table.new :users
+ table[:timestamp].extract("date").as("foo").to_sql.must_be_like %{
+ EXTRACT(DATE FROM "users"."timestamp") AS foo
+ }
+ end
+
+ it "should not mutate the extract" do
+ table = Arel::Table.new :users
+ extract = table[:timestamp].extract("date")
+ before = extract.dup
+ extract.as("foo")
+ assert_equal extract, before
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ table = Arel::Table.new :users
+ array = [table[:attr].extract("foo"), table[:attr].extract("foo")]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ table = Arel::Table.new :users
+ array = [table[:attr].extract("foo"), table[:attr].extract("bar")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/false_test.rb b/activerecord/test/cases/arel/nodes/false_test.rb
new file mode 100644
index 0000000000..4ecf8e332e
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/false_test.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe "False" do
+ describe "equality" do
+ it "is equal to other false nodes" do
+ array = [False.new, False.new]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with other nodes" do
+ array = [False.new, Node.new]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/grouping_test.rb b/activerecord/test/cases/arel/nodes/grouping_test.rb
new file mode 100644
index 0000000000..03d5c142d5
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/grouping_test.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ class GroupingTest < Arel::Spec
+ it "should create Equality nodes" do
+ grouping = Grouping.new(Nodes.build_quoted("foo"))
+ grouping.eq("foo").to_sql.must_be_like "('foo') = 'foo'"
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ array = [Grouping.new("foo"), Grouping.new("foo")]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ array = [Grouping.new("foo"), Grouping.new("bar")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/infix_operation_test.rb b/activerecord/test/cases/arel/nodes/infix_operation_test.rb
new file mode 100644
index 0000000000..dcf2200c12
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/infix_operation_test.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ class TestInfixOperation < Arel::Test
+ def test_construct
+ operation = InfixOperation.new :+, 1, 2
+ assert_equal :+, operation.operator
+ assert_equal 1, operation.left
+ assert_equal 2, operation.right
+ end
+
+ def test_operation_alias
+ operation = InfixOperation.new :+, 1, 2
+ aliaz = operation.as("zomg")
+ assert_kind_of As, aliaz
+ assert_equal operation, aliaz.left
+ assert_equal "zomg", aliaz.right
+ end
+
+ def test_operation_ordering
+ operation = InfixOperation.new :+, 1, 2
+ ordering = operation.desc
+ assert_kind_of Descending, ordering
+ assert_equal operation, ordering.expr
+ assert ordering.descending?
+ end
+
+ def test_equality_with_same_ivars
+ array = [InfixOperation.new(:+, 1, 2), InfixOperation.new(:+, 1, 2)]
+ assert_equal 1, array.uniq.size
+ end
+
+ def test_inequality_with_different_ivars
+ array = [InfixOperation.new(:+, 1, 2), InfixOperation.new(:+, 1, 3)]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/insert_statement_test.rb b/activerecord/test/cases/arel/nodes/insert_statement_test.rb
new file mode 100644
index 0000000000..252a0d0d0b
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/insert_statement_test.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+describe Arel::Nodes::InsertStatement do
+ describe "#clone" do
+ it "clones columns and values" do
+ statement = Arel::Nodes::InsertStatement.new
+ statement.columns = %w[a b c]
+ statement.values = %w[x y z]
+
+ dolly = statement.clone
+ dolly.columns.must_equal statement.columns
+ dolly.values.must_equal statement.values
+
+ dolly.columns.wont_be_same_as statement.columns
+ dolly.values.wont_be_same_as statement.values
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ statement1 = Arel::Nodes::InsertStatement.new
+ statement1.columns = %w[a b c]
+ statement1.values = %w[x y z]
+ statement2 = Arel::Nodes::InsertStatement.new
+ statement2.columns = %w[a b c]
+ statement2.values = %w[x y z]
+ array = [statement1, statement2]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ statement1 = Arel::Nodes::InsertStatement.new
+ statement1.columns = %w[a b c]
+ statement1.values = %w[x y z]
+ statement2 = Arel::Nodes::InsertStatement.new
+ statement2.columns = %w[a b c]
+ statement2.values = %w[1 2 3]
+ array = [statement1, statement2]
+ assert_equal 2, array.uniq.size
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/named_function_test.rb b/activerecord/test/cases/arel/nodes/named_function_test.rb
new file mode 100644
index 0000000000..dbd7ae43be
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/named_function_test.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ class TestNamedFunction < Arel::Test
+ def test_construct
+ function = NamedFunction.new "omg", "zomg"
+ assert_equal "omg", function.name
+ assert_equal "zomg", function.expressions
+ end
+
+ def test_function_alias
+ function = NamedFunction.new "omg", "zomg"
+ function = function.as("wth")
+ assert_equal "omg", function.name
+ assert_equal "zomg", function.expressions
+ assert_kind_of SqlLiteral, function.alias
+ assert_equal "wth", function.alias
+ end
+
+ def test_construct_with_alias
+ function = NamedFunction.new "omg", "zomg", "wth"
+ assert_equal "omg", function.name
+ assert_equal "zomg", function.expressions
+ assert_kind_of SqlLiteral, function.alias
+ assert_equal "wth", function.alias
+ end
+
+ def test_equality_with_same_ivars
+ array = [
+ NamedFunction.new("omg", "zomg", "wth"),
+ NamedFunction.new("omg", "zomg", "wth")
+ ]
+ assert_equal 1, array.uniq.size
+ end
+
+ def test_inequality_with_different_ivars
+ array = [
+ NamedFunction.new("omg", "zomg", "wth"),
+ NamedFunction.new("zomg", "zomg", "wth")
+ ]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/node_test.rb b/activerecord/test/cases/arel/nodes/node_test.rb
new file mode 100644
index 0000000000..f4f07ef2c5
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/node_test.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ class TestNode < Arel::Test
+ def test_includes_factory_methods
+ assert Node.new.respond_to?(:create_join)
+ end
+
+ def test_all_nodes_are_nodes
+ Nodes.constants.map { |k|
+ Nodes.const_get(k)
+ }.grep(Class).each do |klass|
+ next if Nodes::SqlLiteral == klass
+ next if Nodes::BindParam == klass
+ next if klass.name =~ /^Arel::Nodes::(?:Test|.*Test$)/
+ assert klass.ancestors.include?(Nodes::Node), klass.name
+ end
+ end
+
+ def test_each
+ list = []
+ node = Nodes::Node.new
+ node.each { |n| list << n }
+ assert_equal [node], list
+ end
+
+ def test_generator
+ list = []
+ node = Nodes::Node.new
+ node.each.each { |n| list << n }
+ assert_equal [node], list
+ end
+
+ def test_enumerable
+ node = Nodes::Node.new
+ assert_kind_of Enumerable, node
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/not_test.rb b/activerecord/test/cases/arel/nodes/not_test.rb
new file mode 100644
index 0000000000..481e678700
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/not_test.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe "not" do
+ describe "#not" do
+ it "makes a NOT node" do
+ attr = Table.new(:users)[:id]
+ expr = attr.eq(10)
+ node = expr.not
+ node.must_be_kind_of Not
+ node.expr.must_equal expr
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ array = [Not.new("foo"), Not.new("foo")]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ array = [Not.new("foo"), Not.new("baz")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/or_test.rb b/activerecord/test/cases/arel/nodes/or_test.rb
new file mode 100644
index 0000000000..93f826740d
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/or_test.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe "or" do
+ describe "#or" do
+ it "makes an OR node" do
+ attr = Table.new(:users)[:id]
+ left = attr.eq(10)
+ right = attr.eq(11)
+ node = left.or right
+ node.expr.left.must_equal left
+ node.expr.right.must_equal right
+
+ oror = node.or(right)
+ oror.expr.left.must_equal node
+ oror.expr.right.must_equal right
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ array = [Or.new("foo", "bar"), Or.new("foo", "bar")]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ array = [Or.new("foo", "bar"), Or.new("foo", "baz")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/over_test.rb b/activerecord/test/cases/arel/nodes/over_test.rb
new file mode 100644
index 0000000000..981ec2e34b
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/over_test.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+class Arel::Nodes::OverTest < Arel::Spec
+ describe "as" do
+ it "should alias the expression" do
+ table = Arel::Table.new :users
+ table[:id].count.over.as("foo").to_sql.must_be_like %{
+ COUNT("users"."id") OVER () AS foo
+ }
+ end
+ end
+
+ describe "with literal" do
+ it "should reference the window definition by name" do
+ table = Arel::Table.new :users
+ table[:id].count.over("foo").to_sql.must_be_like %{
+ COUNT("users"."id") OVER "foo"
+ }
+ end
+ end
+
+ describe "with SQL literal" do
+ it "should reference the window definition by name" do
+ table = Arel::Table.new :users
+ table[:id].count.over(Arel.sql("foo")).to_sql.must_be_like %{
+ COUNT("users"."id") OVER foo
+ }
+ end
+ end
+
+ describe "with no expression" do
+ it "should use empty definition" do
+ table = Arel::Table.new :users
+ table[:id].count.over.to_sql.must_be_like %{
+ COUNT("users"."id") OVER ()
+ }
+ end
+ end
+
+ describe "with expression" do
+ it "should use definition in sub-expression" do
+ table = Arel::Table.new :users
+ window = Arel::Nodes::Window.new.order(table["foo"])
+ table[:id].count.over(window).to_sql.must_be_like %{
+ COUNT("users"."id") OVER (ORDER BY \"users\".\"foo\")
+ }
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ array = [
+ Arel::Nodes::Over.new("foo", "bar"),
+ Arel::Nodes::Over.new("foo", "bar")
+ ]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ array = [
+ Arel::Nodes::Over.new("foo", "bar"),
+ Arel::Nodes::Over.new("foo", "baz")
+ ]
+ assert_equal 2, array.uniq.size
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/select_core_test.rb b/activerecord/test/cases/arel/nodes/select_core_test.rb
new file mode 100644
index 0000000000..0b698205ff
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/select_core_test.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ class TestSelectCore < Arel::Test
+ def test_clone
+ core = Arel::Nodes::SelectCore.new
+ core.froms = %w[a b c]
+ core.projections = %w[d e f]
+ core.wheres = %w[g h i]
+
+ dolly = core.clone
+
+ assert_equal core.froms, dolly.froms
+ assert_equal core.projections, dolly.projections
+ assert_equal core.wheres, dolly.wheres
+
+ assert_not_same core.froms, dolly.froms
+ assert_not_same core.projections, dolly.projections
+ assert_not_same core.wheres, dolly.wheres
+ end
+
+ def test_set_quantifier
+ core = Arel::Nodes::SelectCore.new
+ core.set_quantifier = Arel::Nodes::Distinct.new
+ viz = Arel::Visitors::ToSql.new Table.engine.connection_pool
+ assert_match "DISTINCT", viz.accept(core, Collectors::SQLString.new).value
+ end
+
+ def test_equality_with_same_ivars
+ core1 = SelectCore.new
+ core1.froms = %w[a b c]
+ core1.projections = %w[d e f]
+ core1.wheres = %w[g h i]
+ core1.groups = %w[j k l]
+ core1.windows = %w[m n o]
+ core1.havings = %w[p q r]
+ core2 = SelectCore.new
+ core2.froms = %w[a b c]
+ core2.projections = %w[d e f]
+ core2.wheres = %w[g h i]
+ core2.groups = %w[j k l]
+ core2.windows = %w[m n o]
+ core2.havings = %w[p q r]
+ array = [core1, core2]
+ assert_equal 1, array.uniq.size
+ end
+
+ def test_inequality_with_different_ivars
+ core1 = SelectCore.new
+ core1.froms = %w[a b c]
+ core1.projections = %w[d e f]
+ core1.wheres = %w[g h i]
+ core1.groups = %w[j k l]
+ core1.windows = %w[m n o]
+ core1.havings = %w[p q r]
+ core2 = SelectCore.new
+ core2.froms = %w[a b c]
+ core2.projections = %w[d e f]
+ core2.wheres = %w[g h i]
+ core2.groups = %w[j k l]
+ core2.windows = %w[m n o]
+ core2.havings = %w[l o l]
+ array = [core1, core2]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/select_statement_test.rb b/activerecord/test/cases/arel/nodes/select_statement_test.rb
new file mode 100644
index 0000000000..a91605de3e
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/select_statement_test.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+describe Arel::Nodes::SelectStatement do
+ describe "#clone" do
+ it "clones cores" do
+ statement = Arel::Nodes::SelectStatement.new %w[a b c]
+
+ dolly = statement.clone
+ dolly.cores.must_equal statement.cores
+ dolly.cores.wont_be_same_as statement.cores
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ statement1 = Arel::Nodes::SelectStatement.new %w[a b c]
+ statement1.offset = 1
+ statement1.limit = 2
+ statement1.lock = false
+ statement1.orders = %w[x y z]
+ statement1.with = "zomg"
+ statement2 = Arel::Nodes::SelectStatement.new %w[a b c]
+ statement2.offset = 1
+ statement2.limit = 2
+ statement2.lock = false
+ statement2.orders = %w[x y z]
+ statement2.with = "zomg"
+ array = [statement1, statement2]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ statement1 = Arel::Nodes::SelectStatement.new %w[a b c]
+ statement1.offset = 1
+ statement1.limit = 2
+ statement1.lock = false
+ statement1.orders = %w[x y z]
+ statement1.with = "zomg"
+ statement2 = Arel::Nodes::SelectStatement.new %w[a b c]
+ statement2.offset = 1
+ statement2.limit = 2
+ statement2.lock = false
+ statement2.orders = %w[x y z]
+ statement2.with = "wth"
+ array = [statement1, statement2]
+ assert_equal 2, array.uniq.size
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/sql_literal_test.rb b/activerecord/test/cases/arel/nodes/sql_literal_test.rb
new file mode 100644
index 0000000000..3b95fed1f4
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/sql_literal_test.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+require "yaml"
+
+module Arel
+ module Nodes
+ class SqlLiteralTest < Arel::Spec
+ before do
+ @visitor = Visitors::ToSql.new Table.engine.connection
+ end
+
+ def compile(node)
+ @visitor.accept(node, Collectors::SQLString.new).value
+ end
+
+ describe "sql" do
+ it "makes a sql literal node" do
+ sql = Arel.sql "foo"
+ sql.must_be_kind_of Arel::Nodes::SqlLiteral
+ end
+ end
+
+ describe "count" do
+ it "makes a count node" do
+ node = SqlLiteral.new("*").count
+ compile(node).must_be_like %{ COUNT(*) }
+ end
+
+ it "makes a distinct node" do
+ node = SqlLiteral.new("*").count true
+ compile(node).must_be_like %{ COUNT(DISTINCT *) }
+ end
+ end
+
+ describe "equality" do
+ it "makes an equality node" do
+ node = SqlLiteral.new("foo").eq(1)
+ compile(node).must_be_like %{ foo = 1 }
+ end
+
+ it "is equal with equal contents" do
+ array = [SqlLiteral.new("foo"), SqlLiteral.new("foo")]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different contents" do
+ array = [SqlLiteral.new("foo"), SqlLiteral.new("bar")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+
+ describe 'grouped "or" equality' do
+ it "makes a grouping node with an or node" do
+ node = SqlLiteral.new("foo").eq_any([1, 2])
+ compile(node).must_be_like %{ (foo = 1 OR foo = 2) }
+ end
+ end
+
+ describe 'grouped "and" equality' do
+ it "makes a grouping node with an and node" do
+ node = SqlLiteral.new("foo").eq_all([1, 2])
+ compile(node).must_be_like %{ (foo = 1 AND foo = 2) }
+ end
+ end
+
+ describe "serialization" do
+ it "serializes into YAML" do
+ yaml_literal = SqlLiteral.new("foo").to_yaml
+ assert_equal("foo", YAML.load(yaml_literal))
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/sum_test.rb b/activerecord/test/cases/arel/nodes/sum_test.rb
new file mode 100644
index 0000000000..5015964951
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/sum_test.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+class Arel::Nodes::SumTest < Arel::Spec
+ describe "as" do
+ it "should alias the sum" do
+ table = Arel::Table.new :users
+ table[:id].sum.as("foo").to_sql.must_be_like %{
+ SUM("users"."id") AS foo
+ }
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ array = [Arel::Nodes::Sum.new("foo"), Arel::Nodes::Sum.new("foo")]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ array = [Arel::Nodes::Sum.new("foo"), Arel::Nodes::Sum.new("foo!")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+
+ describe "order" do
+ it "should order the sum" do
+ table = Arel::Table.new :users
+ table[:id].sum.desc.to_sql.must_be_like %{
+ SUM("users"."id") DESC
+ }
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/table_alias_test.rb b/activerecord/test/cases/arel/nodes/table_alias_test.rb
new file mode 100644
index 0000000000..c661b6771e
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/table_alias_test.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe "table alias" do
+ describe "equality" do
+ it "is equal with equal ivars" do
+ relation1 = Table.new(:users)
+ node1 = TableAlias.new relation1, :foo
+ relation2 = Table.new(:users)
+ node2 = TableAlias.new relation2, :foo
+ array = [node1, node2]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ relation1 = Table.new(:users)
+ node1 = TableAlias.new relation1, :foo
+ relation2 = Table.new(:users)
+ node2 = TableAlias.new relation2, :bar
+ array = [node1, node2]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/true_test.rb b/activerecord/test/cases/arel/nodes/true_test.rb
new file mode 100644
index 0000000000..1e85fe7d48
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/true_test.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe "True" do
+ describe "equality" do
+ it "is equal to other true nodes" do
+ array = [True.new, True.new]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with other nodes" do
+ array = [True.new, Node.new]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/unary_operation_test.rb b/activerecord/test/cases/arel/nodes/unary_operation_test.rb
new file mode 100644
index 0000000000..f0dd0c625c
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/unary_operation_test.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ class TestUnaryOperation < Arel::Test
+ def test_construct
+ operation = UnaryOperation.new :-, 1
+ assert_equal :-, operation.operator
+ assert_equal 1, operation.expr
+ end
+
+ def test_operation_alias
+ operation = UnaryOperation.new :-, 1
+ aliaz = operation.as("zomg")
+ assert_kind_of As, aliaz
+ assert_equal operation, aliaz.left
+ assert_equal "zomg", aliaz.right
+ end
+
+ def test_operation_ordering
+ operation = UnaryOperation.new :-, 1
+ ordering = operation.desc
+ assert_kind_of Descending, ordering
+ assert_equal operation, ordering.expr
+ assert ordering.descending?
+ end
+
+ def test_equality_with_same_ivars
+ array = [UnaryOperation.new(:-, 1), UnaryOperation.new(:-, 1)]
+ assert_equal 1, array.uniq.size
+ end
+
+ def test_inequality_with_different_ivars
+ array = [UnaryOperation.new(:-, 1), UnaryOperation.new(:-, 2)]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/update_statement_test.rb b/activerecord/test/cases/arel/nodes/update_statement_test.rb
new file mode 100644
index 0000000000..a83ce32f68
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/update_statement_test.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+describe Arel::Nodes::UpdateStatement do
+ describe "#clone" do
+ it "clones wheres and values" do
+ statement = Arel::Nodes::UpdateStatement.new
+ statement.wheres = %w[a b c]
+ statement.values = %w[x y z]
+
+ dolly = statement.clone
+ dolly.wheres.must_equal statement.wheres
+ dolly.wheres.wont_be_same_as statement.wheres
+
+ dolly.values.must_equal statement.values
+ dolly.values.wont_be_same_as statement.values
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ statement1 = Arel::Nodes::UpdateStatement.new
+ statement1.relation = "zomg"
+ statement1.wheres = 2
+ statement1.values = false
+ statement1.orders = %w[x y z]
+ statement1.limit = 42
+ statement1.key = "zomg"
+ statement2 = Arel::Nodes::UpdateStatement.new
+ statement2.relation = "zomg"
+ statement2.wheres = 2
+ statement2.values = false
+ statement2.orders = %w[x y z]
+ statement2.limit = 42
+ statement2.key = "zomg"
+ array = [statement1, statement2]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ statement1 = Arel::Nodes::UpdateStatement.new
+ statement1.relation = "zomg"
+ statement1.wheres = 2
+ statement1.values = false
+ statement1.orders = %w[x y z]
+ statement1.limit = 42
+ statement1.key = "zomg"
+ statement2 = Arel::Nodes::UpdateStatement.new
+ statement2.relation = "zomg"
+ statement2.wheres = 2
+ statement2.values = false
+ statement2.orders = %w[x y z]
+ statement2.limit = 42
+ statement2.key = "wth"
+ array = [statement1, statement2]
+ assert_equal 2, array.uniq.size
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/window_test.rb b/activerecord/test/cases/arel/nodes/window_test.rb
new file mode 100644
index 0000000000..729b0556a4
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/window_test.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe "Window" do
+ describe "equality" do
+ it "is equal with equal ivars" do
+ window1 = Window.new
+ window1.orders = [1, 2]
+ window1.partitions = [1]
+ window1.frame 3
+ window2 = Window.new
+ window2.orders = [1, 2]
+ window2.partitions = [1]
+ window2.frame 3
+ array = [window1, window2]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ window1 = Window.new
+ window1.orders = [1, 2]
+ window1.partitions = [1]
+ window1.frame 3
+ window2 = Window.new
+ window2.orders = [1, 2]
+ window1.partitions = [1]
+ window2.frame 4
+ array = [window1, window2]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+
+ describe "NamedWindow" do
+ describe "equality" do
+ it "is equal with equal ivars" do
+ window1 = NamedWindow.new "foo"
+ window1.orders = [1, 2]
+ window1.partitions = [1]
+ window1.frame 3
+ window2 = NamedWindow.new "foo"
+ window2.orders = [1, 2]
+ window2.partitions = [1]
+ window2.frame 3
+ array = [window1, window2]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ window1 = NamedWindow.new "foo"
+ window1.orders = [1, 2]
+ window1.partitions = [1]
+ window1.frame 3
+ window2 = NamedWindow.new "bar"
+ window2.orders = [1, 2]
+ window2.partitions = [1]
+ window2.frame 3
+ array = [window1, window2]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+
+ describe "CurrentRow" do
+ describe "equality" do
+ it "is equal to other current row nodes" do
+ array = [CurrentRow.new, CurrentRow.new]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with other nodes" do
+ array = [CurrentRow.new, Node.new]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes_test.rb b/activerecord/test/cases/arel/nodes_test.rb
new file mode 100644
index 0000000000..9021de0d20
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes_test.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require_relative "helper"
+
+module Arel
+ module Nodes
+ class TestNodes < Arel::Test
+ def test_every_arel_nodes_have_hash_eql_eqeq_from_same_class
+ # #descendants code from activesupport
+ node_descendants = []
+ ObjectSpace.each_object(Arel::Nodes::Node.singleton_class) do |k|
+ next if k.respond_to?(:singleton_class?) && k.singleton_class?
+ node_descendants.unshift k unless k == self
+ end
+ node_descendants.delete(Arel::Nodes::Node)
+ node_descendants.delete(Arel::Nodes::NodeExpression)
+
+ bad_node_descendants = node_descendants.reject do |subnode|
+ eqeq_owner = subnode.instance_method(:==).owner
+ eql_owner = subnode.instance_method(:eql?).owner
+ hash_owner = subnode.instance_method(:hash).owner
+
+ eqeq_owner < Arel::Nodes::Node &&
+ eqeq_owner == eql_owner &&
+ eqeq_owner == hash_owner
+ end
+
+ problem_msg = "Some subclasses of Arel::Nodes::Node do not have a" \
+ " #== or #eql? or #hash defined from the same class as the others"
+ assert_empty bad_node_descendants, problem_msg
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/select_manager_test.rb b/activerecord/test/cases/arel/select_manager_test.rb
new file mode 100644
index 0000000000..5220950905
--- /dev/null
+++ b/activerecord/test/cases/arel/select_manager_test.rb
@@ -0,0 +1,1225 @@
+# frozen_string_literal: true
+
+require_relative "helper"
+
+module Arel
+ class SelectManagerTest < Arel::Spec
+ def test_join_sources
+ manager = Arel::SelectManager.new
+ manager.join_sources << Arel::Nodes::StringJoin.new(Nodes.build_quoted("foo"))
+ assert_equal "SELECT FROM 'foo'", manager.to_sql
+ end
+
+ describe "backwards compatibility" do
+ describe "project" do
+ it "accepts symbols as sql literals" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.project :id
+ manager.from table
+ manager.to_sql.must_be_like %{
+ SELECT id FROM "users"
+ }
+ end
+ end
+
+ describe "order" do
+ it "accepts symbols" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.project Nodes::SqlLiteral.new "*"
+ manager.from table
+ manager.order :foo
+ manager.to_sql.must_be_like %{ SELECT * FROM "users" ORDER BY foo }
+ end
+ end
+
+ describe "group" do
+ it "takes a symbol" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.group :foo
+ manager.to_sql.must_be_like %{ SELECT FROM "users" GROUP BY foo }
+ end
+ end
+
+ describe "as" do
+ it "makes an AS node by grouping the AST" do
+ manager = Arel::SelectManager.new
+ as = manager.as(Arel.sql("foo"))
+ assert_kind_of Arel::Nodes::Grouping, as.left
+ assert_equal manager.ast, as.left.expr
+ assert_equal "foo", as.right
+ end
+
+ it "converts right to SqlLiteral if a string" do
+ manager = Arel::SelectManager.new
+ as = manager.as("foo")
+ assert_kind_of Arel::Nodes::SqlLiteral, as.right
+ end
+
+ it "can make a subselect" do
+ manager = Arel::SelectManager.new
+ manager.project Arel.star
+ manager.from Arel.sql("zomg")
+ as = manager.as(Arel.sql("foo"))
+
+ manager = Arel::SelectManager.new
+ manager.project Arel.sql("name")
+ manager.from as
+ manager.to_sql.must_be_like "SELECT name FROM (SELECT * FROM zomg) foo"
+ end
+ end
+
+ describe "from" do
+ it "ignores strings when table of same name exists" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+
+ manager.from table
+ manager.from "users"
+ manager.project table["id"]
+ manager.to_sql.must_be_like 'SELECT "users"."id" FROM users'
+ end
+
+ it "should support any ast" do
+ table = Table.new :users
+ manager1 = Arel::SelectManager.new
+
+ manager2 = Arel::SelectManager.new
+ manager2.project(Arel.sql("*"))
+ manager2.from table
+
+ manager1.project Arel.sql("lol")
+ as = manager2.as Arel.sql("omg")
+ manager1.from(as)
+
+ manager1.to_sql.must_be_like %{
+ SELECT lol FROM (SELECT * FROM "users") omg
+ }
+ end
+ end
+
+ describe "having" do
+ it "converts strings to SQLLiterals" do
+ table = Table.new :users
+ mgr = table.from
+ mgr.having Arel.sql("foo")
+ mgr.to_sql.must_be_like %{ SELECT FROM "users" HAVING foo }
+ end
+
+ it "can have multiple items specified separately" do
+ table = Table.new :users
+ mgr = table.from
+ mgr.having Arel.sql("foo")
+ mgr.having Arel.sql("bar")
+ mgr.to_sql.must_be_like %{ SELECT FROM "users" HAVING foo AND bar }
+ end
+
+ it "can receive any node" do
+ table = Table.new :users
+ mgr = table.from
+ mgr.having Arel::Nodes::And.new([Arel.sql("foo"), Arel.sql("bar")])
+ mgr.to_sql.must_be_like %{ SELECT FROM "users" HAVING foo AND bar }
+ end
+ end
+
+ describe "on" do
+ it "converts to sqlliterals" do
+ table = Table.new :users
+ right = table.alias
+ mgr = table.from
+ mgr.join(right).on("omg")
+ mgr.to_sql.must_be_like %{ SELECT FROM "users" INNER JOIN "users" "users_2" ON omg }
+ end
+
+ it "converts to sqlliterals with multiple items" do
+ table = Table.new :users
+ right = table.alias
+ mgr = table.from
+ mgr.join(right).on("omg", "123")
+ mgr.to_sql.must_be_like %{ SELECT FROM "users" INNER JOIN "users" "users_2" ON omg AND 123 }
+ end
+ end
+ end
+
+ describe "clone" do
+ it "creates new cores" do
+ table = Table.new :users, as: "foo"
+ mgr = table.from
+ m2 = mgr.clone
+ m2.project "foo"
+ mgr.to_sql.wont_equal m2.to_sql
+ end
+
+ it "makes updates to the correct copy" do
+ table = Table.new :users, as: "foo"
+ mgr = table.from
+ m2 = mgr.clone
+ m3 = m2.clone
+ m2.project "foo"
+ mgr.to_sql.wont_equal m2.to_sql
+ m3.to_sql.must_equal mgr.to_sql
+ end
+ end
+
+ describe "initialize" do
+ it "uses alias in sql" do
+ table = Table.new :users, as: "foo"
+ mgr = table.from
+ mgr.skip 10
+ mgr.to_sql.must_be_like %{ SELECT FROM "users" "foo" OFFSET 10 }
+ end
+ end
+
+ describe "skip" do
+ it "should add an offset" do
+ table = Table.new :users
+ mgr = table.from
+ mgr.skip 10
+ mgr.to_sql.must_be_like %{ SELECT FROM "users" OFFSET 10 }
+ end
+
+ it "should chain" do
+ table = Table.new :users
+ mgr = table.from
+ mgr.skip(10).to_sql.must_be_like %{ SELECT FROM "users" OFFSET 10 }
+ end
+ end
+
+ describe "offset" do
+ it "should add an offset" do
+ table = Table.new :users
+ mgr = table.from
+ mgr.offset = 10
+ mgr.to_sql.must_be_like %{ SELECT FROM "users" OFFSET 10 }
+ end
+
+ it "should remove an offset" do
+ table = Table.new :users
+ mgr = table.from
+ mgr.offset = 10
+ mgr.to_sql.must_be_like %{ SELECT FROM "users" OFFSET 10 }
+
+ mgr.offset = nil
+ mgr.to_sql.must_be_like %{ SELECT FROM "users" }
+ end
+
+ it "should return the offset" do
+ table = Table.new :users
+ mgr = table.from
+ mgr.offset = 10
+ assert_equal 10, mgr.offset
+ end
+ end
+
+ describe "exists" do
+ it "should create an exists clause" do
+ table = Table.new(:users)
+ manager = Arel::SelectManager.new table
+ manager.project Nodes::SqlLiteral.new "*"
+ m2 = Arel::SelectManager.new
+ m2.project manager.exists
+ m2.to_sql.must_be_like %{ SELECT EXISTS (#{manager.to_sql}) }
+ end
+
+ it "can be aliased" do
+ table = Table.new(:users)
+ manager = Arel::SelectManager.new table
+ manager.project Nodes::SqlLiteral.new "*"
+ m2 = Arel::SelectManager.new
+ m2.project manager.exists.as("foo")
+ m2.to_sql.must_be_like %{ SELECT EXISTS (#{manager.to_sql}) AS foo }
+ end
+ end
+
+ describe "union" do
+ before do
+ table = Table.new :users
+ @m1 = Arel::SelectManager.new table
+ @m1.project Arel.star
+ @m1.where(table[:age].lt(18))
+
+ @m2 = Arel::SelectManager.new table
+ @m2.project Arel.star
+ @m2.where(table[:age].gt(99))
+ end
+
+ it "should union two managers" do
+ # FIXME should this union "managers" or "statements" ?
+ # FIXME this probably shouldn't return a node
+ node = @m1.union @m2
+
+ # maybe FIXME: decide when wrapper parens are needed
+ node.to_sql.must_be_like %{
+ ( SELECT * FROM "users" WHERE "users"."age" < 18 UNION SELECT * FROM "users" WHERE "users"."age" > 99 )
+ }
+ end
+
+ it "should union all" do
+ node = @m1.union :all, @m2
+
+ node.to_sql.must_be_like %{
+ ( SELECT * FROM "users" WHERE "users"."age" < 18 UNION ALL SELECT * FROM "users" WHERE "users"."age" > 99 )
+ }
+ end
+ end
+
+ describe "intersect" do
+ before do
+ table = Table.new :users
+ @m1 = Arel::SelectManager.new table
+ @m1.project Arel.star
+ @m1.where(table[:age].gt(18))
+
+ @m2 = Arel::SelectManager.new table
+ @m2.project Arel.star
+ @m2.where(table[:age].lt(99))
+ end
+
+ it "should intersect two managers" do
+ # FIXME should this intersect "managers" or "statements" ?
+ # FIXME this probably shouldn't return a node
+ node = @m1.intersect @m2
+
+ # maybe FIXME: decide when wrapper parens are needed
+ node.to_sql.must_be_like %{
+ ( SELECT * FROM "users" WHERE "users"."age" > 18 INTERSECT SELECT * FROM "users" WHERE "users"."age" < 99 )
+ }
+ end
+ end
+
+ describe "except" do
+ before do
+ table = Table.new :users
+ @m1 = Arel::SelectManager.new table
+ @m1.project Arel.star
+ @m1.where(table[:age].between(18..60))
+
+ @m2 = Arel::SelectManager.new table
+ @m2.project Arel.star
+ @m2.where(table[:age].between(40..99))
+ end
+
+ it "should except two managers" do
+ # FIXME should this except "managers" or "statements" ?
+ # FIXME this probably shouldn't return a node
+ node = @m1.except @m2
+
+ # maybe FIXME: decide when wrapper parens are needed
+ node.to_sql.must_be_like %{
+ ( SELECT * FROM "users" WHERE "users"."age" BETWEEN 18 AND 60 EXCEPT SELECT * FROM "users" WHERE "users"."age" BETWEEN 40 AND 99 )
+ }
+ end
+ end
+
+ describe "with" do
+ it "should support basic WITH" do
+ users = Table.new(:users)
+ users_top = Table.new(:users_top)
+ comments = Table.new(:comments)
+
+ top = users.project(users[:id]).where(users[:karma].gt(100))
+ users_as = Arel::Nodes::As.new(users_top, top)
+ select_manager = comments.project(Arel.star).with(users_as)
+ .where(comments[:author_id].in(users_top.project(users_top[:id])))
+
+ select_manager.to_sql.must_be_like %{
+ WITH "users_top" AS (SELECT "users"."id" FROM "users" WHERE "users"."karma" > 100) SELECT * FROM "comments" WHERE "comments"."author_id" IN (SELECT "users_top"."id" FROM "users_top")
+ }
+ end
+
+ it "should support WITH RECURSIVE" do
+ comments = Table.new(:comments)
+ comments_id = comments[:id]
+ comments_parent_id = comments[:parent_id]
+
+ replies = Table.new(:replies)
+ replies_id = replies[:id]
+
+ recursive_term = Arel::SelectManager.new
+ recursive_term.from(comments).project(comments_id, comments_parent_id).where(comments_id.eq 42)
+
+ non_recursive_term = Arel::SelectManager.new
+ non_recursive_term.from(comments).project(comments_id, comments_parent_id).join(replies).on(comments_parent_id.eq replies_id)
+
+ union = recursive_term.union(non_recursive_term)
+
+ as_statement = Arel::Nodes::As.new replies, union
+
+ manager = Arel::SelectManager.new
+ manager.with(:recursive, as_statement).from(replies).project(Arel.star)
+
+ sql = manager.to_sql
+ sql.must_be_like %{
+ WITH RECURSIVE "replies" AS (
+ SELECT "comments"."id", "comments"."parent_id" FROM "comments" WHERE "comments"."id" = 42
+ UNION
+ SELECT "comments"."id", "comments"."parent_id" FROM "comments" INNER JOIN "replies" ON "comments"."parent_id" = "replies"."id"
+ )
+ SELECT * FROM "replies"
+ }
+ end
+ end
+
+ describe "ast" do
+ it "should return the ast" do
+ table = Table.new :users
+ mgr = table.from
+ assert mgr.ast
+ end
+
+ it "should allow orders to work when the ast is grepped" do
+ table = Table.new :users
+ mgr = table.from
+ mgr.project Arel.sql "*"
+ mgr.from table
+ mgr.orders << Arel::Nodes::Ascending.new(Arel.sql("foo"))
+ mgr.ast.grep(Arel::Nodes::OuterJoin)
+ mgr.to_sql.must_be_like %{ SELECT * FROM "users" ORDER BY foo ASC }
+ end
+ end
+
+ describe "taken" do
+ it "should return limit" do
+ manager = Arel::SelectManager.new
+ manager.take 10
+ manager.taken.must_equal 10
+ end
+ end
+
+ describe "lock" do
+ # This should fail on other databases
+ it "adds a lock node" do
+ table = Table.new :users
+ mgr = table.from
+ mgr.lock.to_sql.must_be_like %{ SELECT FROM "users" FOR UPDATE }
+ end
+ end
+
+ describe "orders" do
+ it "returns order clauses" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ order = table[:id]
+ manager.order table[:id]
+ manager.orders.must_equal [order]
+ end
+ end
+
+ describe "order" do
+ it "generates order clauses" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.project Nodes::SqlLiteral.new "*"
+ manager.from table
+ manager.order table[:id]
+ manager.to_sql.must_be_like %{
+ SELECT * FROM "users" ORDER BY "users"."id"
+ }
+ end
+
+ # FIXME: I would like to deprecate this
+ it "takes *args" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.project Nodes::SqlLiteral.new "*"
+ manager.from table
+ manager.order table[:id], table[:name]
+ manager.to_sql.must_be_like %{
+ SELECT * FROM "users" ORDER BY "users"."id", "users"."name"
+ }
+ end
+
+ it "chains" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.order(table[:id]).must_equal manager
+ end
+
+ it "has order attributes" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.project Nodes::SqlLiteral.new "*"
+ manager.from table
+ manager.order table[:id].desc
+ manager.to_sql.must_be_like %{
+ SELECT * FROM "users" ORDER BY "users"."id" DESC
+ }
+ end
+ end
+
+ describe "on" do
+ it "takes two params" do
+ left = Table.new :users
+ right = left.alias
+ predicate = left[:id].eq(right[:id])
+ manager = Arel::SelectManager.new
+
+ manager.from left
+ manager.join(right).on(predicate, predicate)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users"
+ INNER JOIN "users" "users_2"
+ ON "users"."id" = "users_2"."id" AND
+ "users"."id" = "users_2"."id"
+ }
+ end
+
+ it "takes three params" do
+ left = Table.new :users
+ right = left.alias
+ predicate = left[:id].eq(right[:id])
+ manager = Arel::SelectManager.new
+
+ manager.from left
+ manager.join(right).on(
+ predicate,
+ predicate,
+ left[:name].eq(right[:name])
+ )
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users"
+ INNER JOIN "users" "users_2"
+ ON "users"."id" = "users_2"."id" AND
+ "users"."id" = "users_2"."id" AND
+ "users"."name" = "users_2"."name"
+ }
+ end
+ end
+
+ it "should hand back froms" do
+ relation = Arel::SelectManager.new
+ assert_equal [], relation.froms
+ end
+
+ it "should create and nodes" do
+ relation = Arel::SelectManager.new
+ children = ["foo", "bar", "baz"]
+ clause = relation.create_and children
+ assert_kind_of Arel::Nodes::And, clause
+ assert_equal children, clause.children
+ end
+
+ it "should create insert managers" do
+ relation = Arel::SelectManager.new
+ insert = relation.create_insert
+ assert_kind_of Arel::InsertManager, insert
+ end
+
+ it "should create join nodes" do
+ relation = Arel::SelectManager.new
+ join = relation.create_join "foo", "bar"
+ assert_kind_of Arel::Nodes::InnerJoin, join
+ assert_equal "foo", join.left
+ assert_equal "bar", join.right
+ end
+
+ it "should create join nodes with a full outer join klass" do
+ relation = Arel::SelectManager.new
+ join = relation.create_join "foo", "bar", Arel::Nodes::FullOuterJoin
+ assert_kind_of Arel::Nodes::FullOuterJoin, join
+ assert_equal "foo", join.left
+ assert_equal "bar", join.right
+ end
+
+ it "should create join nodes with a outer join klass" do
+ relation = Arel::SelectManager.new
+ join = relation.create_join "foo", "bar", Arel::Nodes::OuterJoin
+ assert_kind_of Arel::Nodes::OuterJoin, join
+ assert_equal "foo", join.left
+ assert_equal "bar", join.right
+ end
+
+ it "should create join nodes with a right outer join klass" do
+ relation = Arel::SelectManager.new
+ join = relation.create_join "foo", "bar", Arel::Nodes::RightOuterJoin
+ assert_kind_of Arel::Nodes::RightOuterJoin, join
+ assert_equal "foo", join.left
+ assert_equal "bar", join.right
+ end
+
+ describe "join" do
+ it "responds to join" do
+ left = Table.new :users
+ right = left.alias
+ predicate = left[:id].eq(right[:id])
+ manager = Arel::SelectManager.new
+
+ manager.from left
+ manager.join(right).on(predicate)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users"
+ INNER JOIN "users" "users_2"
+ ON "users"."id" = "users_2"."id"
+ }
+ end
+
+ it "takes a class" do
+ left = Table.new :users
+ right = left.alias
+ predicate = left[:id].eq(right[:id])
+ manager = Arel::SelectManager.new
+
+ manager.from left
+ manager.join(right, Nodes::OuterJoin).on(predicate)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users"
+ LEFT OUTER JOIN "users" "users_2"
+ ON "users"."id" = "users_2"."id"
+ }
+ end
+
+ it "takes the full outer join class" do
+ left = Table.new :users
+ right = left.alias
+ predicate = left[:id].eq(right[:id])
+ manager = Arel::SelectManager.new
+
+ manager.from left
+ manager.join(right, Nodes::FullOuterJoin).on(predicate)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users"
+ FULL OUTER JOIN "users" "users_2"
+ ON "users"."id" = "users_2"."id"
+ }
+ end
+
+ it "takes the right outer join class" do
+ left = Table.new :users
+ right = left.alias
+ predicate = left[:id].eq(right[:id])
+ manager = Arel::SelectManager.new
+
+ manager.from left
+ manager.join(right, Nodes::RightOuterJoin).on(predicate)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users"
+ RIGHT OUTER JOIN "users" "users_2"
+ ON "users"."id" = "users_2"."id"
+ }
+ end
+
+ it "noops on nil" do
+ manager = Arel::SelectManager.new
+ manager.join(nil).must_equal manager
+ end
+
+ it "raises EmptyJoinError on empty" do
+ left = Table.new :users
+ manager = Arel::SelectManager.new
+
+ manager.from left
+ assert_raises(EmptyJoinError) do
+ manager.join("")
+ end
+ end
+ end
+
+ describe "outer join" do
+ it "responds to join" do
+ left = Table.new :users
+ right = left.alias
+ predicate = left[:id].eq(right[:id])
+ manager = Arel::SelectManager.new
+
+ manager.from left
+ manager.outer_join(right).on(predicate)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users"
+ LEFT OUTER JOIN "users" "users_2"
+ ON "users"."id" = "users_2"."id"
+ }
+ end
+
+ it "noops on nil" do
+ manager = Arel::SelectManager.new
+ manager.outer_join(nil).must_equal manager
+ end
+ end
+
+ describe "joins" do
+ it "returns inner join sql" do
+ table = Table.new :users
+ aliaz = table.alias
+ manager = Arel::SelectManager.new
+ manager.from Nodes::InnerJoin.new(aliaz, table[:id].eq(aliaz[:id]))
+ assert_match 'INNER JOIN "users" "users_2" "users"."id" = "users_2"."id"',
+ manager.to_sql
+ end
+
+ it "returns outer join sql" do
+ table = Table.new :users
+ aliaz = table.alias
+ manager = Arel::SelectManager.new
+ manager.from Nodes::OuterJoin.new(aliaz, table[:id].eq(aliaz[:id]))
+ assert_match 'LEFT OUTER JOIN "users" "users_2" "users"."id" = "users_2"."id"',
+ manager.to_sql
+ end
+
+ it "can have a non-table alias as relation name" do
+ users = Table.new :users
+ comments = Table.new :comments
+
+ counts = comments.from.
+ group(comments[:user_id]).
+ project(
+ comments[:user_id].as("user_id"),
+ comments[:user_id].count.as("count")
+ ).as("counts")
+
+ joins = users.join(counts).on(counts[:user_id].eq(10))
+ joins.to_sql.must_be_like %{
+ SELECT FROM "users" INNER JOIN (SELECT "comments"."user_id" AS user_id, COUNT("comments"."user_id") AS count FROM "comments" GROUP BY "comments"."user_id") counts ON counts."user_id" = 10
+ }
+ end
+
+ it "joins itself" do
+ left = Table.new :users
+ right = left.alias
+ predicate = left[:id].eq(right[:id])
+
+ mgr = left.join(right)
+ mgr.project Nodes::SqlLiteral.new("*")
+ mgr.on(predicate).must_equal mgr
+
+ mgr.to_sql.must_be_like %{
+ SELECT * FROM "users"
+ INNER JOIN "users" "users_2"
+ ON "users"."id" = "users_2"."id"
+ }
+ end
+
+ it "returns string join sql" do
+ manager = Arel::SelectManager.new
+ manager.from Nodes::StringJoin.new(Nodes.build_quoted("hello"))
+ assert_match "'hello'", manager.to_sql
+ end
+ end
+
+ describe "group" do
+ it "takes an attribute" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.group table[:id]
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" GROUP BY "users"."id"
+ }
+ end
+
+ it "chains" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.group(table[:id]).must_equal manager
+ end
+
+ it "takes multiple args" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.group table[:id], table[:name]
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" GROUP BY "users"."id", "users"."name"
+ }
+ end
+
+ # FIXME: backwards compat
+ it "makes strings literals" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.group "foo"
+ manager.to_sql.must_be_like %{ SELECT FROM "users" GROUP BY foo }
+ end
+ end
+
+ describe "window definition" do
+ it "can be empty" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window")
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS ()
+ }
+ end
+
+ it "takes an order" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").order(table["foo"].asc)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (ORDER BY "users"."foo" ASC)
+ }
+ end
+
+ it "takes an order with multiple columns" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").order(table["foo"].asc, table["bar"].desc)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (ORDER BY "users"."foo" ASC, "users"."bar" DESC)
+ }
+ end
+
+ it "takes a partition" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").partition(table["bar"])
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (PARTITION BY "users"."bar")
+ }
+ end
+
+ it "takes a partition and an order" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").partition(table["foo"]).order(table["foo"].asc)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (PARTITION BY "users"."foo"
+ ORDER BY "users"."foo" ASC)
+ }
+ end
+
+ it "takes a partition with multiple columns" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").partition(table["bar"], table["baz"])
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (PARTITION BY "users"."bar", "users"."baz")
+ }
+ end
+
+ it "takes a rows frame, unbounded preceding" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").rows(Arel::Nodes::Preceding.new)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (ROWS UNBOUNDED PRECEDING)
+ }
+ end
+
+ it "takes a rows frame, bounded preceding" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").rows(Arel::Nodes::Preceding.new(5))
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (ROWS 5 PRECEDING)
+ }
+ end
+
+ it "takes a rows frame, unbounded following" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").rows(Arel::Nodes::Following.new)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (ROWS UNBOUNDED FOLLOWING)
+ }
+ end
+
+ it "takes a rows frame, bounded following" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").rows(Arel::Nodes::Following.new(5))
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (ROWS 5 FOLLOWING)
+ }
+ end
+
+ it "takes a rows frame, current row" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").rows(Arel::Nodes::CurrentRow.new)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (ROWS CURRENT ROW)
+ }
+ end
+
+ it "takes a rows frame, between two delimiters" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ window = manager.window("a_window")
+ window.frame(
+ Arel::Nodes::Between.new(
+ window.rows,
+ Nodes::And.new([
+ Arel::Nodes::Preceding.new,
+ Arel::Nodes::CurrentRow.new
+ ])))
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
+ }
+ end
+
+ it "takes a range frame, unbounded preceding" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").range(Arel::Nodes::Preceding.new)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (RANGE UNBOUNDED PRECEDING)
+ }
+ end
+
+ it "takes a range frame, bounded preceding" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").range(Arel::Nodes::Preceding.new(5))
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (RANGE 5 PRECEDING)
+ }
+ end
+
+ it "takes a range frame, unbounded following" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").range(Arel::Nodes::Following.new)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (RANGE UNBOUNDED FOLLOWING)
+ }
+ end
+
+ it "takes a range frame, bounded following" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").range(Arel::Nodes::Following.new(5))
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (RANGE 5 FOLLOWING)
+ }
+ end
+
+ it "takes a range frame, current row" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").range(Arel::Nodes::CurrentRow.new)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (RANGE CURRENT ROW)
+ }
+ end
+
+ it "takes a range frame, between two delimiters" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ window = manager.window("a_window")
+ window.frame(
+ Arel::Nodes::Between.new(
+ window.range,
+ Nodes::And.new([
+ Arel::Nodes::Preceding.new,
+ Arel::Nodes::CurrentRow.new
+ ])))
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
+ }
+ end
+ end
+
+ describe "delete" do
+ it "copies from" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ stmt = manager.compile_delete
+
+ stmt.to_sql.must_be_like %{ DELETE FROM "users" }
+ end
+
+ it "copies where" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.where table[:id].eq 10
+ stmt = manager.compile_delete
+
+ stmt.to_sql.must_be_like %{
+ DELETE FROM "users" WHERE "users"."id" = 10
+ }
+ end
+ end
+
+ describe "where_sql" do
+ it "gives me back the where sql" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.where table[:id].eq 10
+ manager.where_sql.must_be_like %{ WHERE "users"."id" = 10 }
+ end
+
+ it "joins wheres with AND" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.where table[:id].eq 10
+ manager.where table[:id].eq 11
+ manager.where_sql.must_be_like %{ WHERE "users"."id" = 10 AND "users"."id" = 11}
+ end
+
+ it "handles database specific statements" do
+ old_visitor = Table.engine.connection.visitor
+ Table.engine.connection.visitor = Visitors::PostgreSQL.new Table.engine.connection
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.where table[:id].eq 10
+ manager.where table[:name].matches "foo%"
+ manager.where_sql.must_be_like %{ WHERE "users"."id" = 10 AND "users"."name" ILIKE 'foo%' }
+ Table.engine.connection.visitor = old_visitor
+ end
+
+ it "returns nil when there are no wheres" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.where_sql.must_be_nil
+ end
+ end
+
+ describe "update" do
+ it "creates an update statement" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ stmt = manager.compile_update({ table[:id] => 1 }, Arel::Attributes::Attribute.new(table, "id"))
+
+ stmt.to_sql.must_be_like %{
+ UPDATE "users" SET "id" = 1
+ }
+ end
+
+ it "takes a string" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ stmt = manager.compile_update(Nodes::SqlLiteral.new("foo = bar"), Arel::Attributes::Attribute.new(table, "id"))
+
+ stmt.to_sql.must_be_like %{ UPDATE "users" SET foo = bar }
+ end
+
+ it "copies limits" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.take 1
+ stmt = manager.compile_update(Nodes::SqlLiteral.new("foo = bar"), Arel::Attributes::Attribute.new(table, "id"))
+ stmt.key = table["id"]
+
+ stmt.to_sql.must_be_like %{
+ UPDATE "users" SET foo = bar
+ WHERE "users"."id" IN (SELECT "users"."id" FROM "users" LIMIT 1)
+ }
+ end
+
+ it "copies order" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.order :foo
+ stmt = manager.compile_update(Nodes::SqlLiteral.new("foo = bar"), Arel::Attributes::Attribute.new(table, "id"))
+ stmt.key = table["id"]
+
+ stmt.to_sql.must_be_like %{
+ UPDATE "users" SET foo = bar
+ WHERE "users"."id" IN (SELECT "users"."id" FROM "users" ORDER BY foo)
+ }
+ end
+
+ it "copies where clauses" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.where table[:id].eq 10
+ manager.from table
+ stmt = manager.compile_update({ table[:id] => 1 }, Arel::Attributes::Attribute.new(table, "id"))
+
+ stmt.to_sql.must_be_like %{
+ UPDATE "users" SET "id" = 1 WHERE "users"."id" = 10
+ }
+ end
+
+ it "copies where clauses when nesting is triggered" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.where table[:foo].eq 10
+ manager.take 42
+ manager.from table
+ stmt = manager.compile_update({ table[:id] => 1 }, Arel::Attributes::Attribute.new(table, "id"))
+
+ stmt.to_sql.must_be_like %{
+ UPDATE "users" SET "id" = 1 WHERE "users"."id" IN (SELECT "users"."id" FROM "users" WHERE "users"."foo" = 10 LIMIT 42)
+ }
+ end
+ end
+
+ describe "project" do
+ it "takes sql literals" do
+ manager = Arel::SelectManager.new
+ manager.project Nodes::SqlLiteral.new "*"
+ manager.to_sql.must_be_like %{ SELECT * }
+ end
+
+ it "takes multiple args" do
+ manager = Arel::SelectManager.new
+ manager.project Nodes::SqlLiteral.new("foo"),
+ Nodes::SqlLiteral.new("bar")
+ manager.to_sql.must_be_like %{ SELECT foo, bar }
+ end
+
+ it "takes strings" do
+ manager = Arel::SelectManager.new
+ manager.project "*"
+ manager.to_sql.must_be_like %{ SELECT * }
+ end
+ end
+
+ describe "projections" do
+ it "reads projections" do
+ manager = Arel::SelectManager.new
+ manager.project Arel.sql("foo"), Arel.sql("bar")
+ manager.projections.must_equal [Arel.sql("foo"), Arel.sql("bar")]
+ end
+ end
+
+ describe "projections=" do
+ it "overwrites projections" do
+ manager = Arel::SelectManager.new
+ manager.project Arel.sql("foo")
+ manager.projections = [Arel.sql("bar")]
+ manager.to_sql.must_be_like %{ SELECT bar }
+ end
+ end
+
+ describe "take" do
+ it "knows take" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from(table).project(table["id"])
+ manager.where(table["id"].eq(1))
+ manager.take 1
+
+ manager.to_sql.must_be_like %{
+ SELECT "users"."id"
+ FROM "users"
+ WHERE "users"."id" = 1
+ LIMIT 1
+ }
+ end
+
+ it "chains" do
+ manager = Arel::SelectManager.new
+ manager.take(1).must_equal manager
+ end
+
+ it "removes LIMIT when nil is passed" do
+ manager = Arel::SelectManager.new
+ manager.limit = 10
+ assert_match("LIMIT", manager.to_sql)
+
+ manager.limit = nil
+ assert_no_match("LIMIT", manager.to_sql)
+ end
+ end
+
+ describe "where" do
+ it "knows where" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from(table).project(table["id"])
+ manager.where(table["id"].eq(1))
+ manager.to_sql.must_be_like %{
+ SELECT "users"."id"
+ FROM "users"
+ WHERE "users"."id" = 1
+ }
+ end
+
+ it "chains" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from(table)
+ manager.project(table["id"]).where(table["id"].eq 1).must_equal manager
+ end
+ end
+
+ describe "from" do
+ it "makes sql" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+
+ manager.from table
+ manager.project table["id"]
+ manager.to_sql.must_be_like 'SELECT "users"."id" FROM "users"'
+ end
+
+ it "chains" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from(table).project(table["id"]).must_equal manager
+ manager.to_sql.must_be_like 'SELECT "users"."id" FROM "users"'
+ end
+ end
+
+ describe "source" do
+ it "returns the join source of the select core" do
+ manager = Arel::SelectManager.new
+ manager.source.must_equal manager.ast.cores.last.source
+ end
+ end
+
+ describe "distinct" do
+ it "sets the quantifier" do
+ manager = Arel::SelectManager.new
+
+ manager.distinct
+ manager.ast.cores.last.set_quantifier.class.must_equal Arel::Nodes::Distinct
+
+ manager.distinct(false)
+ manager.ast.cores.last.set_quantifier.must_be_nil
+ end
+
+ it "chains" do
+ manager = Arel::SelectManager.new
+ manager.distinct.must_equal manager
+ manager.distinct(false).must_equal manager
+ end
+ end
+
+ describe "distinct_on" do
+ it "sets the quantifier" do
+ manager = Arel::SelectManager.new
+ table = Table.new :users
+
+ manager.distinct_on(table["id"])
+ manager.ast.cores.last.set_quantifier.must_equal Arel::Nodes::DistinctOn.new(table["id"])
+
+ manager.distinct_on(false)
+ manager.ast.cores.last.set_quantifier.must_be_nil
+ end
+
+ it "chains" do
+ manager = Arel::SelectManager.new
+ table = Table.new :users
+
+ manager.distinct_on(table["id"]).must_equal manager
+ manager.distinct_on(false).must_equal manager
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/support/fake_record.rb b/activerecord/test/cases/arel/support/fake_record.rb
new file mode 100644
index 0000000000..559ff5d4e6
--- /dev/null
+++ b/activerecord/test/cases/arel/support/fake_record.rb
@@ -0,0 +1,129 @@
+# frozen_string_literal: true
+
+require "date"
+module FakeRecord
+ class Column < Struct.new(:name, :type)
+ end
+
+ class Connection
+ attr_reader :tables
+ attr_accessor :visitor
+
+ def initialize(visitor = nil)
+ @tables = %w{ users photos developers products}
+ @columns = {
+ "users" => [
+ Column.new("id", :integer),
+ Column.new("name", :string),
+ Column.new("bool", :boolean),
+ Column.new("created_at", :date)
+ ],
+ "products" => [
+ Column.new("id", :integer),
+ Column.new("price", :decimal)
+ ]
+ }
+ @columns_hash = {
+ "users" => Hash[@columns["users"].map { |x| [x.name, x] }],
+ "products" => Hash[@columns["products"].map { |x| [x.name, x] }]
+ }
+ @primary_keys = {
+ "users" => "id",
+ "products" => "id"
+ }
+ @visitor = visitor
+ end
+
+ def columns_hash(table_name)
+ @columns_hash[table_name]
+ end
+
+ def primary_key(name)
+ @primary_keys[name.to_s]
+ end
+
+ def data_source_exists?(name)
+ @tables.include? name.to_s
+ end
+
+ def columns(name, message = nil)
+ @columns[name.to_s]
+ end
+
+ def quote_table_name(name)
+ "\"#{name}\""
+ end
+
+ def quote_column_name(name)
+ "\"#{name}\""
+ end
+
+ def schema_cache
+ self
+ end
+
+ def quote(thing)
+ case thing
+ when DateTime
+ "'#{thing.strftime("%Y-%m-%d %H:%M:%S")}'"
+ when Date
+ "'#{thing.strftime("%Y-%m-%d")}'"
+ when true
+ "'t'"
+ when false
+ "'f'"
+ when nil
+ "NULL"
+ when Numeric
+ thing
+ else
+ "'#{thing.to_s.gsub("'", "\\\\'")}'"
+ end
+ end
+ end
+
+ class ConnectionPool
+ class Spec < Struct.new(:config)
+ end
+
+ attr_reader :spec, :connection
+
+ def initialize
+ @spec = Spec.new(adapter: "america")
+ @connection = Connection.new
+ @connection.visitor = Arel::Visitors::ToSql.new(connection)
+ end
+
+ def with_connection
+ yield connection
+ end
+
+ def table_exists?(name)
+ connection.tables.include? name.to_s
+ end
+
+ def columns_hash
+ connection.columns_hash
+ end
+
+ def schema_cache
+ connection
+ end
+
+ def quote(thing)
+ connection.quote thing
+ end
+ end
+
+ class Base
+ attr_accessor :connection_pool
+
+ def initialize
+ @connection_pool = ConnectionPool.new
+ end
+
+ def connection
+ connection_pool.connection
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/table_test.rb b/activerecord/test/cases/arel/table_test.rb
new file mode 100644
index 0000000000..91b7a5a480
--- /dev/null
+++ b/activerecord/test/cases/arel/table_test.rb
@@ -0,0 +1,216 @@
+# frozen_string_literal: true
+
+require_relative "helper"
+
+module Arel
+ class TableTest < Arel::Spec
+ before do
+ @relation = Table.new(:users)
+ end
+
+ it "should create join nodes" do
+ join = @relation.create_string_join "foo"
+ assert_kind_of Arel::Nodes::StringJoin, join
+ assert_equal "foo", join.left
+ end
+
+ it "should create join nodes" do
+ join = @relation.create_join "foo", "bar"
+ assert_kind_of Arel::Nodes::InnerJoin, join
+ assert_equal "foo", join.left
+ assert_equal "bar", join.right
+ end
+
+ it "should create join nodes with a klass" do
+ join = @relation.create_join "foo", "bar", Arel::Nodes::FullOuterJoin
+ assert_kind_of Arel::Nodes::FullOuterJoin, join
+ assert_equal "foo", join.left
+ assert_equal "bar", join.right
+ end
+
+ it "should create join nodes with a klass" do
+ join = @relation.create_join "foo", "bar", Arel::Nodes::OuterJoin
+ assert_kind_of Arel::Nodes::OuterJoin, join
+ assert_equal "foo", join.left
+ assert_equal "bar", join.right
+ end
+
+ it "should create join nodes with a klass" do
+ join = @relation.create_join "foo", "bar", Arel::Nodes::RightOuterJoin
+ assert_kind_of Arel::Nodes::RightOuterJoin, join
+ assert_equal "foo", join.left
+ assert_equal "bar", join.right
+ end
+
+ it "should return an insert manager" do
+ im = @relation.compile_insert "VALUES(NULL)"
+ assert_kind_of Arel::InsertManager, im
+ im.into Table.new(:users)
+ assert_equal "INSERT INTO \"users\" VALUES(NULL)", im.to_sql
+ end
+
+ describe "skip" do
+ it "should add an offset" do
+ sm = @relation.skip 2
+ sm.to_sql.must_be_like "SELECT FROM \"users\" OFFSET 2"
+ end
+ end
+
+ describe "having" do
+ it "adds a having clause" do
+ mgr = @relation.having @relation[:id].eq(10)
+ mgr.to_sql.must_be_like %{
+ SELECT FROM "users" HAVING "users"."id" = 10
+ }
+ end
+ end
+
+ describe "backwards compat" do
+ describe "join" do
+ it "noops on nil" do
+ mgr = @relation.join nil
+
+ mgr.to_sql.must_be_like %{ SELECT FROM "users" }
+ end
+
+ it "raises EmptyJoinError on empty" do
+ assert_raises(EmptyJoinError) do
+ @relation.join ""
+ end
+ end
+
+ it "takes a second argument for join type" do
+ right = @relation.alias
+ predicate = @relation[:id].eq(right[:id])
+ mgr = @relation.join(right, Nodes::OuterJoin).on(predicate)
+
+ mgr.to_sql.must_be_like %{
+ SELECT FROM "users"
+ LEFT OUTER JOIN "users" "users_2"
+ ON "users"."id" = "users_2"."id"
+ }
+ end
+ end
+
+ describe "join" do
+ it "creates an outer join" do
+ right = @relation.alias
+ predicate = @relation[:id].eq(right[:id])
+ mgr = @relation.outer_join(right).on(predicate)
+
+ mgr.to_sql.must_be_like %{
+ SELECT FROM "users"
+ LEFT OUTER JOIN "users" "users_2"
+ ON "users"."id" = "users_2"."id"
+ }
+ end
+ end
+ end
+
+ describe "group" do
+ it "should create a group" do
+ manager = @relation.group @relation[:id]
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" GROUP BY "users"."id"
+ }
+ end
+ end
+
+ describe "alias" do
+ it "should create a node that proxies to a table" do
+ node = @relation.alias
+ node.name.must_equal "users_2"
+ node[:id].relation.must_equal node
+ end
+ end
+
+ describe "new" do
+ it "should accept a hash" do
+ rel = Table.new :users, as: "foo"
+ rel.table_alias.must_equal "foo"
+ end
+
+ it "ignores as if it equals name" do
+ rel = Table.new :users, as: "users"
+ rel.table_alias.must_be_nil
+ end
+ end
+
+ describe "order" do
+ it "should take an order" do
+ manager = @relation.order "foo"
+ manager.to_sql.must_be_like %{ SELECT FROM "users" ORDER BY foo }
+ end
+ end
+
+ describe "take" do
+ it "should add a limit" do
+ manager = @relation.take 1
+ manager.project Nodes::SqlLiteral.new "*"
+ manager.to_sql.must_be_like %{ SELECT * FROM "users" LIMIT 1 }
+ end
+ end
+
+ describe "project" do
+ it "can project" do
+ manager = @relation.project Nodes::SqlLiteral.new "*"
+ manager.to_sql.must_be_like %{ SELECT * FROM "users" }
+ end
+
+ it "takes multiple parameters" do
+ manager = @relation.project Nodes::SqlLiteral.new("*"), Nodes::SqlLiteral.new("*")
+ manager.to_sql.must_be_like %{ SELECT *, * FROM "users" }
+ end
+ end
+
+ describe "where" do
+ it "returns a tree manager" do
+ manager = @relation.where @relation[:id].eq 1
+ manager.project @relation[:id]
+ manager.must_be_kind_of TreeManager
+ manager.to_sql.must_be_like %{
+ SELECT "users"."id"
+ FROM "users"
+ WHERE "users"."id" = 1
+ }
+ end
+ end
+
+ it "should have a name" do
+ @relation.name.must_equal "users"
+ end
+
+ it "should have a table name" do
+ @relation.table_name.must_equal "users"
+ end
+
+ describe "[]" do
+ describe "when given a Symbol" do
+ it "manufactures an attribute if the symbol names an attribute within the relation" do
+ column = @relation[:id]
+ column.name.must_equal :id
+ end
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ relation1 = Table.new(:users)
+ relation1.table_alias = "zomg"
+ relation2 = Table.new(:users)
+ relation2.table_alias = "zomg"
+ array = [relation1, relation2]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ relation1 = Table.new(:users)
+ relation1.table_alias = "zomg"
+ relation2 = Table.new(:users)
+ relation2.table_alias = "zomg2"
+ array = [relation1, relation2]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/update_manager_test.rb b/activerecord/test/cases/arel/update_manager_test.rb
new file mode 100644
index 0000000000..cc1b9ac5b3
--- /dev/null
+++ b/activerecord/test/cases/arel/update_manager_test.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+
+require_relative "helper"
+
+module Arel
+ class UpdateManagerTest < Arel::Spec
+ describe "new" do
+ it "takes an engine" do
+ Arel::UpdateManager.new
+ end
+ end
+
+ it "should not quote sql literals" do
+ table = Table.new(:users)
+ um = Arel::UpdateManager.new
+ um.table table
+ um.set [[table[:name], Arel::Nodes::BindParam.new(1)]]
+ um.to_sql.must_be_like %{ UPDATE "users" SET "name" = ? }
+ end
+
+ it "handles limit properly" do
+ table = Table.new(:users)
+ um = Arel::UpdateManager.new
+ um.key = "id"
+ um.take 10
+ um.table table
+ um.set [[table[:name], nil]]
+ assert_match(/LIMIT 10/, um.to_sql)
+ end
+
+ describe "set" do
+ it "updates with null" do
+ table = Table.new(:users)
+ um = Arel::UpdateManager.new
+ um.table table
+ um.set [[table[:name], nil]]
+ um.to_sql.must_be_like %{ UPDATE "users" SET "name" = NULL }
+ end
+
+ it "takes a string" do
+ table = Table.new(:users)
+ um = Arel::UpdateManager.new
+ um.table table
+ um.set Nodes::SqlLiteral.new "foo = bar"
+ um.to_sql.must_be_like %{ UPDATE "users" SET foo = bar }
+ end
+
+ it "takes a list of lists" do
+ table = Table.new(:users)
+ um = Arel::UpdateManager.new
+ um.table table
+ um.set [[table[:id], 1], [table[:name], "hello"]]
+ um.to_sql.must_be_like %{
+ UPDATE "users" SET "id" = 1, "name" = 'hello'
+ }
+ end
+
+ it "chains" do
+ table = Table.new(:users)
+ um = Arel::UpdateManager.new
+ um.set([[table[:id], 1], [table[:name], "hello"]]).must_equal um
+ end
+ end
+
+ describe "table" do
+ it "generates an update statement" do
+ um = Arel::UpdateManager.new
+ um.table Table.new(:users)
+ um.to_sql.must_be_like %{ UPDATE "users" }
+ end
+
+ it "chains" do
+ um = Arel::UpdateManager.new
+ um.table(Table.new(:users)).must_equal um
+ end
+
+ it "generates an update statement with joins" do
+ um = Arel::UpdateManager.new
+
+ table = Table.new(:users)
+ join_source = Arel::Nodes::JoinSource.new(
+ table,
+ [table.create_join(Table.new(:posts))]
+ )
+
+ um.table join_source
+ um.to_sql.must_be_like %{ UPDATE "users" INNER JOIN "posts" }
+ end
+ end
+
+ describe "where" do
+ it "generates a where clause" do
+ table = Table.new :users
+ um = Arel::UpdateManager.new
+ um.table table
+ um.where table[:id].eq(1)
+ um.to_sql.must_be_like %{
+ UPDATE "users" WHERE "users"."id" = 1
+ }
+ end
+
+ it "chains" do
+ table = Table.new :users
+ um = Arel::UpdateManager.new
+ um.table table
+ um.where(table[:id].eq(1)).must_equal um
+ end
+ end
+
+ describe "key" do
+ before do
+ @table = Table.new :users
+ @um = Arel::UpdateManager.new
+ @um.key = @table[:foo]
+ end
+
+ it "can be set" do
+ @um.ast.key.must_equal @table[:foo]
+ end
+
+ it "can be accessed" do
+ @um.key.must_equal @table[:foo]
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/visitors/depth_first_test.rb b/activerecord/test/cases/arel/visitors/depth_first_test.rb
new file mode 100644
index 0000000000..f94ad521d7
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/depth_first_test.rb
@@ -0,0 +1,270 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Visitors
+ class TestDepthFirst < Arel::Test
+ Collector = Struct.new(:calls) do
+ def call(object)
+ calls << object
+ end
+ end
+
+ def setup
+ @collector = Collector.new []
+ @visitor = Visitors::DepthFirst.new @collector
+ end
+
+ def test_raises_with_object
+ assert_raises(TypeError) do
+ @visitor.accept(Object.new)
+ end
+ end
+
+
+ # unary ops
+ [
+ Arel::Nodes::Not,
+ Arel::Nodes::Group,
+ Arel::Nodes::On,
+ Arel::Nodes::Grouping,
+ Arel::Nodes::Offset,
+ Arel::Nodes::Ordering,
+ Arel::Nodes::StringJoin,
+ Arel::Nodes::UnqualifiedColumn,
+ Arel::Nodes::Limit,
+ Arel::Nodes::Else,
+ ].each do |klass|
+ define_method("test_#{klass.name.gsub('::', '_')}") do
+ op = klass.new(:a)
+ @visitor.accept op
+ assert_equal [:a, op], @collector.calls
+ end
+ end
+
+ # functions
+ [
+ Arel::Nodes::Exists,
+ Arel::Nodes::Avg,
+ Arel::Nodes::Min,
+ Arel::Nodes::Max,
+ Arel::Nodes::Sum,
+ ].each do |klass|
+ define_method("test_#{klass.name.gsub('::', '_')}") do
+ func = klass.new(:a, "b")
+ @visitor.accept func
+ assert_equal [:a, "b", false, func], @collector.calls
+ end
+ end
+
+ def test_named_function
+ func = Arel::Nodes::NamedFunction.new(:a, :b, "c")
+ @visitor.accept func
+ assert_equal [:a, :b, false, "c", func], @collector.calls
+ end
+
+ def test_lock
+ lock = Nodes::Lock.new true
+ @visitor.accept lock
+ assert_equal [lock], @collector.calls
+ end
+
+ def test_count
+ count = Nodes::Count.new :a, :b, "c"
+ @visitor.accept count
+ assert_equal [:a, "c", :b, count], @collector.calls
+ end
+
+ def test_inner_join
+ join = Nodes::InnerJoin.new :a, :b
+ @visitor.accept join
+ assert_equal [:a, :b, join], @collector.calls
+ end
+
+ def test_full_outer_join
+ join = Nodes::FullOuterJoin.new :a, :b
+ @visitor.accept join
+ assert_equal [:a, :b, join], @collector.calls
+ end
+
+ def test_outer_join
+ join = Nodes::OuterJoin.new :a, :b
+ @visitor.accept join
+ assert_equal [:a, :b, join], @collector.calls
+ end
+
+ def test_right_outer_join
+ join = Nodes::RightOuterJoin.new :a, :b
+ @visitor.accept join
+ assert_equal [:a, :b, join], @collector.calls
+ end
+
+ [
+ Arel::Nodes::Assignment,
+ Arel::Nodes::Between,
+ Arel::Nodes::Concat,
+ Arel::Nodes::DoesNotMatch,
+ Arel::Nodes::Equality,
+ Arel::Nodes::GreaterThan,
+ Arel::Nodes::GreaterThanOrEqual,
+ Arel::Nodes::In,
+ Arel::Nodes::LessThan,
+ Arel::Nodes::LessThanOrEqual,
+ Arel::Nodes::Matches,
+ Arel::Nodes::NotEqual,
+ Arel::Nodes::NotIn,
+ Arel::Nodes::Or,
+ Arel::Nodes::TableAlias,
+ Arel::Nodes::Values,
+ Arel::Nodes::As,
+ Arel::Nodes::DeleteStatement,
+ Arel::Nodes::JoinSource,
+ Arel::Nodes::When,
+ ].each do |klass|
+ define_method("test_#{klass.name.gsub('::', '_')}") do
+ binary = klass.new(:a, :b)
+ @visitor.accept binary
+ assert_equal [:a, :b, binary], @collector.calls
+ end
+ end
+
+ def test_Arel_Nodes_InfixOperation
+ binary = Arel::Nodes::InfixOperation.new(:o, :a, :b)
+ @visitor.accept binary
+ assert_equal [:a, :b, binary], @collector.calls
+ end
+
+ # N-ary
+ [
+ Arel::Nodes::And,
+ ].each do |klass|
+ define_method("test_#{klass.name.gsub('::', '_')}") do
+ binary = klass.new([:a, :b, :c])
+ @visitor.accept binary
+ assert_equal [:a, :b, :c, binary], @collector.calls
+ end
+ end
+
+ [
+ Arel::Attributes::Integer,
+ Arel::Attributes::Float,
+ Arel::Attributes::String,
+ Arel::Attributes::Time,
+ Arel::Attributes::Boolean,
+ Arel::Attributes::Attribute
+ ].each do |klass|
+ define_method("test_#{klass.name.gsub('::', '_')}") do
+ binary = klass.new(:a, :b)
+ @visitor.accept binary
+ assert_equal [:a, :b, binary], @collector.calls
+ end
+ end
+
+ def test_table
+ relation = Arel::Table.new(:users)
+ @visitor.accept relation
+ assert_equal ["users", relation], @collector.calls
+ end
+
+ def test_array
+ node = Nodes::Or.new(:a, :b)
+ list = [node]
+ @visitor.accept list
+ assert_equal [:a, :b, node, list], @collector.calls
+ end
+
+ def test_set
+ node = Nodes::Or.new(:a, :b)
+ set = Set.new([node])
+ @visitor.accept set
+ assert_equal [:a, :b, node, set], @collector.calls
+ end
+
+ def test_hash
+ node = Nodes::Or.new(:a, :b)
+ hash = { node => node }
+ @visitor.accept hash
+ assert_equal [:a, :b, node, :a, :b, node, hash], @collector.calls
+ end
+
+ def test_update_statement
+ stmt = Nodes::UpdateStatement.new
+ stmt.relation = :a
+ stmt.values << :b
+ stmt.wheres << :c
+ stmt.orders << :d
+ stmt.limit = :e
+
+ @visitor.accept stmt
+ assert_equal [:a, :b, stmt.values, :c, stmt.wheres, :d, stmt.orders,
+ :e, stmt], @collector.calls
+ end
+
+ def test_select_core
+ core = Nodes::SelectCore.new
+ core.projections << :a
+ core.froms = :b
+ core.wheres << :c
+ core.groups << :d
+ core.windows << :e
+ core.havings << :f
+
+ @visitor.accept core
+ assert_equal [
+ :a, core.projections,
+ :b, [],
+ core.source,
+ :c, core.wheres,
+ :d, core.groups,
+ :e, core.windows,
+ :f, core.havings,
+ core], @collector.calls
+ end
+
+ def test_select_statement
+ ss = Nodes::SelectStatement.new
+ ss.cores.replace [:a]
+ ss.orders << :b
+ ss.limit = :c
+ ss.lock = :d
+ ss.offset = :e
+
+ @visitor.accept ss
+ assert_equal [
+ :a, ss.cores,
+ :b, ss.orders,
+ :c,
+ :d,
+ :e,
+ ss], @collector.calls
+ end
+
+ def test_insert_statement
+ stmt = Nodes::InsertStatement.new
+ stmt.relation = :a
+ stmt.columns << :b
+ stmt.values = :c
+
+ @visitor.accept stmt
+ assert_equal [:a, :b, stmt.columns, :c, stmt], @collector.calls
+ end
+
+ def test_case
+ node = Arel::Nodes::Case.new
+ node.case = :a
+ node.conditions << :b
+ node.default = :c
+
+ @visitor.accept node
+ assert_equal [:a, :b, node.conditions, :c, node], @collector.calls
+ end
+
+ def test_node
+ node = Nodes::Node.new
+ @visitor.accept node
+ assert_equal [node], @collector.calls
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/visitors/dispatch_contamination_test.rb b/activerecord/test/cases/arel/visitors/dispatch_contamination_test.rb
new file mode 100644
index 0000000000..a07a1a050a
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/dispatch_contamination_test.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+require "concurrent"
+
+module Arel
+ module Visitors
+ class DummyVisitor < Visitor
+ def initialize
+ super
+ @barrier = Concurrent::CyclicBarrier.new(2)
+ end
+
+ def visit_Arel_Visitors_DummySuperNode(node)
+ 42
+ end
+
+ # This is terrible, but it's the only way to reliably reproduce
+ # the possible race where two threads attempt to correct the
+ # dispatch hash at the same time.
+ def send(*args)
+ super
+ rescue
+ # Both threads try (and fail) to dispatch to the subclass's name
+ @barrier.wait
+ raise
+ ensure
+ # Then one thread successfully completes (updating the dispatch
+ # table in the process) before the other finishes raising its
+ # exception.
+ Thread.current[:delay].wait if Thread.current[:delay]
+ end
+ end
+
+ class DummySuperNode
+ end
+
+ class DummySubNode < DummySuperNode
+ end
+
+ class DispatchContaminationTest < Arel::Spec
+ before do
+ @connection = Table.engine.connection
+ @table = Table.new(:users)
+ end
+
+ it "dispatches properly after failing upwards" do
+ node = Nodes::Union.new(Nodes::True.new, Nodes::False.new)
+ assert_equal "( TRUE UNION FALSE )", node.to_sql
+
+ node.first # from Nodes::Node's Enumerable mixin
+
+ assert_equal "( TRUE UNION FALSE )", node.to_sql
+ end
+
+ it "is threadsafe when implementing superclass fallback" do
+ visitor = DummyVisitor.new
+ main_thread_finished = Concurrent::Event.new
+
+ racing_thread = Thread.new do
+ Thread.current[:delay] = main_thread_finished
+ visitor.accept DummySubNode.new
+ end
+
+ assert_equal 42, visitor.accept(DummySubNode.new)
+ main_thread_finished.set
+
+ assert_equal 42, racing_thread.value
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/visitors/dot_test.rb b/activerecord/test/cases/arel/visitors/dot_test.rb
new file mode 100644
index 0000000000..6b3c132f83
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/dot_test.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Visitors
+ class TestDot < Arel::Test
+ def setup
+ @visitor = Visitors::Dot.new
+ end
+
+ # functions
+ [
+ Nodes::Sum,
+ Nodes::Exists,
+ Nodes::Max,
+ Nodes::Min,
+ Nodes::Avg,
+ ].each do |klass|
+ define_method("test_#{klass.name.gsub('::', '_')}") do
+ op = klass.new(:a, "z")
+ @visitor.accept op, Collectors::PlainString.new
+ end
+ end
+
+ def test_named_function
+ func = Nodes::NamedFunction.new "omg", "omg"
+ @visitor.accept func, Collectors::PlainString.new
+ end
+
+ # unary ops
+ [
+ Arel::Nodes::Not,
+ Arel::Nodes::Group,
+ Arel::Nodes::On,
+ Arel::Nodes::Grouping,
+ Arel::Nodes::Offset,
+ Arel::Nodes::Ordering,
+ Arel::Nodes::UnqualifiedColumn,
+ Arel::Nodes::Limit,
+ ].each do |klass|
+ define_method("test_#{klass.name.gsub('::', '_')}") do
+ op = klass.new(:a)
+ @visitor.accept op, Collectors::PlainString.new
+ end
+ end
+
+ # binary ops
+ [
+ Arel::Nodes::Assignment,
+ Arel::Nodes::Between,
+ Arel::Nodes::DoesNotMatch,
+ Arel::Nodes::Equality,
+ Arel::Nodes::GreaterThan,
+ Arel::Nodes::GreaterThanOrEqual,
+ Arel::Nodes::In,
+ Arel::Nodes::LessThan,
+ Arel::Nodes::LessThanOrEqual,
+ Arel::Nodes::Matches,
+ Arel::Nodes::NotEqual,
+ Arel::Nodes::NotIn,
+ Arel::Nodes::Or,
+ Arel::Nodes::TableAlias,
+ Arel::Nodes::Values,
+ Arel::Nodes::As,
+ Arel::Nodes::DeleteStatement,
+ Arel::Nodes::JoinSource,
+ Arel::Nodes::Casted,
+ ].each do |klass|
+ define_method("test_#{klass.name.gsub('::', '_')}") do
+ binary = klass.new(:a, :b)
+ @visitor.accept binary, Collectors::PlainString.new
+ end
+ end
+
+ def test_Arel_Nodes_BindParam
+ node = Arel::Nodes::BindParam.new(1)
+ collector = Collectors::PlainString.new
+ assert_match '[label="<f0>Arel::Nodes::BindParam"]', @visitor.accept(node, collector).value
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/visitors/ibm_db_test.rb b/activerecord/test/cases/arel/visitors/ibm_db_test.rb
new file mode 100644
index 0000000000..2ddbec3266
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/ibm_db_test.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Visitors
+ class IbmDbTest < Arel::Spec
+ before do
+ @visitor = IBM_DB.new Table.engine.connection
+ end
+
+ def compile(node)
+ @visitor.accept(node, Collectors::SQLString.new).value
+ end
+
+ it "uses FETCH FIRST n ROWS to limit results" do
+ stmt = Nodes::SelectStatement.new
+ stmt.limit = Nodes::Limit.new(1)
+ sql = compile(stmt)
+ sql.must_be_like "SELECT FETCH FIRST 1 ROWS ONLY"
+ end
+
+ it "uses FETCH FIRST n ROWS in updates with a limit" do
+ table = Table.new(:users)
+ stmt = Nodes::UpdateStatement.new
+ stmt.relation = table
+ stmt.limit = Nodes::Limit.new(Nodes.build_quoted(1))
+ stmt.key = table[:id]
+ sql = compile(stmt)
+ sql.must_be_like "UPDATE \"users\" WHERE \"users\".\"id\" IN (SELECT \"users\".\"id\" FROM \"users\" FETCH FIRST 1 ROWS ONLY)"
+ end
+
+ describe "Nodes::IsNotDistinctFrom" do
+ it "should construct a valid generic SQL statement" do
+ test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson"
+ compile(test).must_be_like %{
+ DECODE("users"."name", 'Aaron Patterson', 0, 1) = 0
+ }
+ end
+
+ it "should handle column names on both sides" do
+ test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name]
+ compile(test).must_be_like %{
+ DECODE("users"."first_name", "users"."last_name", 0, 1) = 0
+ }
+ end
+
+ it "should handle nil" do
+ @table = Table.new(:users)
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" IS NULL }
+ end
+ end
+
+ describe "Nodes::IsDistinctFrom" do
+ it "should handle column names on both sides" do
+ test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name]
+ compile(test).must_be_like %{
+ DECODE("users"."first_name", "users"."last_name", 0, 1) = 1
+ }
+ end
+
+ it "should handle nil" do
+ @table = Table.new(:users)
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::IsDistinctFrom.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" IS NOT NULL }
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/visitors/informix_test.rb b/activerecord/test/cases/arel/visitors/informix_test.rb
new file mode 100644
index 0000000000..b6c2dd6ae7
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/informix_test.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Visitors
+ class InformixTest < Arel::Spec
+ before do
+ @visitor = Informix.new Table.engine.connection
+ end
+
+ def compile(node)
+ @visitor.accept(node, Collectors::SQLString.new).value
+ end
+
+ it "uses FIRST n to limit results" do
+ stmt = Nodes::SelectStatement.new
+ stmt.limit = Nodes::Limit.new(1)
+ sql = compile(stmt)
+ sql.must_be_like "SELECT FIRST 1"
+ end
+
+ it "uses FIRST n in updates with a limit" do
+ table = Table.new(:users)
+ stmt = Nodes::UpdateStatement.new
+ stmt.relation = table
+ stmt.limit = Nodes::Limit.new(Nodes.build_quoted(1))
+ stmt.key = table[:id]
+ sql = compile(stmt)
+ sql.must_be_like "UPDATE \"users\" WHERE \"users\".\"id\" IN (SELECT FIRST 1 \"users\".\"id\" FROM \"users\")"
+ end
+
+ it "uses SKIP n to jump results" do
+ stmt = Nodes::SelectStatement.new
+ stmt.offset = Nodes::Offset.new(10)
+ sql = compile(stmt)
+ sql.must_be_like "SELECT SKIP 10"
+ end
+
+ it "uses SKIP before FIRST" do
+ stmt = Nodes::SelectStatement.new
+ stmt.limit = Nodes::Limit.new(1)
+ stmt.offset = Nodes::Offset.new(1)
+ sql = compile(stmt)
+ sql.must_be_like "SELECT SKIP 1 FIRST 1"
+ end
+
+ it "uses INNER JOIN to perform joins" do
+ core = Nodes::SelectCore.new
+ table = Table.new(:posts)
+ core.source = Nodes::JoinSource.new(table, [table.create_join(Table.new(:comments))])
+
+ stmt = Nodes::SelectStatement.new([core])
+ sql = compile(stmt)
+ sql.must_be_like 'SELECT FROM "posts" INNER JOIN "comments"'
+ end
+
+ describe "Nodes::IsNotDistinctFrom" do
+ it "should construct a valid generic SQL statement" do
+ test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson"
+ compile(test).must_be_like %{
+ CASE WHEN "users"."name" = 'Aaron Patterson' OR ("users"."name" IS NULL AND 'Aaron Patterson' IS NULL) THEN 0 ELSE 1 END = 0
+ }
+ end
+
+ it "should handle column names on both sides" do
+ test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name]
+ compile(test).must_be_like %{
+ CASE WHEN "users"."first_name" = "users"."last_name" OR ("users"."first_name" IS NULL AND "users"."last_name" IS NULL) THEN 0 ELSE 1 END = 0
+ }
+ end
+
+ it "should handle nil" do
+ @table = Table.new(:users)
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" IS NULL }
+ end
+ end
+
+ describe "Nodes::IsDistinctFrom" do
+ it "should handle column names on both sides" do
+ test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name]
+ compile(test).must_be_like %{
+ CASE WHEN "users"."first_name" = "users"."last_name" OR ("users"."first_name" IS NULL AND "users"."last_name" IS NULL) THEN 0 ELSE 1 END = 1
+ }
+ end
+
+ it "should handle nil" do
+ @table = Table.new(:users)
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::IsDistinctFrom.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" IS NOT NULL }
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/visitors/mssql_test.rb b/activerecord/test/cases/arel/visitors/mssql_test.rb
new file mode 100644
index 0000000000..74f34b4dad
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/mssql_test.rb
@@ -0,0 +1,138 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Visitors
+ class MssqlTest < Arel::Spec
+ before do
+ @visitor = MSSQL.new Table.engine.connection
+ @table = Arel::Table.new "users"
+ end
+
+ def compile(node)
+ @visitor.accept(node, Collectors::SQLString.new).value
+ end
+
+ it "should not modify query if no offset or limit" do
+ stmt = Nodes::SelectStatement.new
+ sql = compile(stmt)
+ sql.must_be_like "SELECT"
+ end
+
+ it "should go over table PK if no .order() or .group()" do
+ stmt = Nodes::SelectStatement.new
+ stmt.cores.first.from = @table
+ stmt.limit = Nodes::Limit.new(10)
+ sql = compile(stmt)
+ sql.must_be_like "SELECT _t.* FROM (SELECT ROW_NUMBER() OVER (ORDER BY \"users\".\"id\") as _row_num FROM \"users\") as _t WHERE _row_num BETWEEN 1 AND 10"
+ end
+
+ it "caches the PK lookup for order" do
+ connection = Minitest::Mock.new
+ connection.expect(:primary_key, ["id"], ["users"])
+
+ # We don't care how many times these methods are called
+ def connection.quote_table_name(*); ""; end
+ def connection.quote_column_name(*); ""; end
+
+ @visitor = MSSQL.new(connection)
+ stmt = Nodes::SelectStatement.new
+ stmt.cores.first.from = @table
+ stmt.limit = Nodes::Limit.new(10)
+
+ compile(stmt)
+ compile(stmt)
+
+ connection.verify
+ end
+
+ it "should use TOP for limited deletes" do
+ stmt = Nodes::DeleteStatement.new
+ stmt.relation = @table
+ stmt.limit = Nodes::Limit.new(10)
+ sql = compile(stmt)
+
+ sql.must_be_like "DELETE TOP (10) FROM \"users\""
+ end
+
+ it "should go over query ORDER BY if .order()" do
+ stmt = Nodes::SelectStatement.new
+ stmt.limit = Nodes::Limit.new(10)
+ stmt.orders << Nodes::SqlLiteral.new("order_by")
+ sql = compile(stmt)
+ sql.must_be_like "SELECT _t.* FROM (SELECT ROW_NUMBER() OVER (ORDER BY order_by) as _row_num) as _t WHERE _row_num BETWEEN 1 AND 10"
+ end
+
+ it "should go over query GROUP BY if no .order() and there is .group()" do
+ stmt = Nodes::SelectStatement.new
+ stmt.cores.first.groups << Nodes::SqlLiteral.new("group_by")
+ stmt.limit = Nodes::Limit.new(10)
+ sql = compile(stmt)
+ sql.must_be_like "SELECT _t.* FROM (SELECT ROW_NUMBER() OVER (ORDER BY group_by) as _row_num GROUP BY group_by) as _t WHERE _row_num BETWEEN 1 AND 10"
+ end
+
+ it "should use BETWEEN if both .limit() and .offset" do
+ stmt = Nodes::SelectStatement.new
+ stmt.limit = Nodes::Limit.new(10)
+ stmt.offset = Nodes::Offset.new(20)
+ sql = compile(stmt)
+ sql.must_be_like "SELECT _t.* FROM (SELECT ROW_NUMBER() OVER (ORDER BY ) as _row_num) as _t WHERE _row_num BETWEEN 21 AND 30"
+ end
+
+ it "should use >= if only .offset" do
+ stmt = Nodes::SelectStatement.new
+ stmt.offset = Nodes::Offset.new(20)
+ sql = compile(stmt)
+ sql.must_be_like "SELECT _t.* FROM (SELECT ROW_NUMBER() OVER (ORDER BY ) as _row_num) as _t WHERE _row_num >= 21"
+ end
+
+ it "should generate subquery for .count" do
+ stmt = Nodes::SelectStatement.new
+ stmt.limit = Nodes::Limit.new(10)
+ stmt.cores.first.projections << Nodes::Count.new("*")
+ sql = compile(stmt)
+ sql.must_be_like "SELECT COUNT(1) as count_id FROM (SELECT _t.* FROM (SELECT ROW_NUMBER() OVER (ORDER BY ) as _row_num) as _t WHERE _row_num BETWEEN 1 AND 10) AS subquery"
+ end
+
+ describe "Nodes::IsNotDistinctFrom" do
+ it "should construct a valid generic SQL statement" do
+ test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson"
+ compile(test).must_be_like %{
+ EXISTS (VALUES ("users"."name") INTERSECT VALUES ('Aaron Patterson'))
+ }
+ end
+
+ it "should handle column names on both sides" do
+ test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name]
+ compile(test).must_be_like %{
+ EXISTS (VALUES ("users"."first_name") INTERSECT VALUES ("users"."last_name"))
+ }
+ end
+
+ it "should handle nil" do
+ @table = Table.new(:users)
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" IS NULL }
+ end
+ end
+
+ describe "Nodes::IsDistinctFrom" do
+ it "should handle column names on both sides" do
+ test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name]
+ compile(test).must_be_like %{
+ NOT EXISTS (VALUES ("users"."first_name") INTERSECT VALUES ("users"."last_name"))
+ }
+ end
+
+ it "should handle nil" do
+ @table = Table.new(:users)
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::IsDistinctFrom.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" IS NOT NULL }
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/visitors/mysql_test.rb b/activerecord/test/cases/arel/visitors/mysql_test.rb
new file mode 100644
index 0000000000..5f37587957
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/mysql_test.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Visitors
+ class MysqlTest < Arel::Spec
+ before do
+ @visitor = MySQL.new Table.engine.connection
+ end
+
+ def compile(node)
+ @visitor.accept(node, Collectors::SQLString.new).value
+ end
+
+ ###
+ # :'(
+ # http://dev.mysql.com/doc/refman/5.0/en/select.html#id3482214
+ it "defaults limit to 18446744073709551615" do
+ stmt = Nodes::SelectStatement.new
+ stmt.offset = Nodes::Offset.new(1)
+ sql = compile(stmt)
+ sql.must_be_like "SELECT FROM DUAL LIMIT 18446744073709551615 OFFSET 1"
+ end
+
+ it "should escape LIMIT" do
+ sc = Arel::Nodes::UpdateStatement.new
+ sc.relation = Table.new(:users)
+ sc.limit = Nodes::Limit.new(Nodes.build_quoted("omg"))
+ assert_equal("UPDATE \"users\" LIMIT 'omg'", compile(sc))
+ end
+
+ it "uses DUAL for empty from" do
+ stmt = Nodes::SelectStatement.new
+ sql = compile(stmt)
+ sql.must_be_like "SELECT FROM DUAL"
+ end
+
+ describe "locking" do
+ it "defaults to FOR UPDATE when locking" do
+ node = Nodes::Lock.new(Arel.sql("FOR UPDATE"))
+ compile(node).must_be_like "FOR UPDATE"
+ end
+
+ it "allows a custom string to be used as a lock" do
+ node = Nodes::Lock.new(Arel.sql("LOCK IN SHARE MODE"))
+ compile(node).must_be_like "LOCK IN SHARE MODE"
+ end
+ end
+
+ describe "concat" do
+ it "concats columns" do
+ @table = Table.new(:users)
+ query = @table[:name].concat(@table[:name])
+ compile(query).must_be_like %{
+ CONCAT("users"."name", "users"."name")
+ }
+ end
+
+ it "concats a string" do
+ @table = Table.new(:users)
+ query = @table[:name].concat(Nodes.build_quoted("abc"))
+ compile(query).must_be_like %{
+ CONCAT("users"."name", 'abc')
+ }
+ end
+ end
+
+ describe "Nodes::IsNotDistinctFrom" do
+ it "should construct a valid generic SQL statement" do
+ test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson"
+ compile(test).must_be_like %{
+ "users"."name" <=> 'Aaron Patterson'
+ }
+ end
+
+ it "should handle column names on both sides" do
+ test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name]
+ compile(test).must_be_like %{
+ "users"."first_name" <=> "users"."last_name"
+ }
+ end
+
+ it "should handle nil" do
+ @table = Table.new(:users)
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" <=> NULL }
+ end
+ end
+
+ describe "Nodes::IsDistinctFrom" do
+ it "should handle column names on both sides" do
+ test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name]
+ compile(test).must_be_like %{
+ NOT "users"."first_name" <=> "users"."last_name"
+ }
+ end
+
+ it "should handle nil" do
+ @table = Table.new(:users)
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::IsDistinctFrom.new(@table[:name], val)
+ sql.must_be_like %{ NOT "users"."name" <=> NULL }
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/visitors/oracle12_test.rb b/activerecord/test/cases/arel/visitors/oracle12_test.rb
new file mode 100644
index 0000000000..4ce5cab4db
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/oracle12_test.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Visitors
+ class Oracle12Test < Arel::Spec
+ before do
+ @visitor = Oracle12.new Table.engine.connection
+ @table = Table.new(:users)
+ end
+
+ def compile(node)
+ @visitor.accept(node, Collectors::SQLString.new).value
+ end
+
+ it "modified except to be minus" do
+ left = Nodes::SqlLiteral.new("SELECT * FROM users WHERE age > 10")
+ right = Nodes::SqlLiteral.new("SELECT * FROM users WHERE age > 20")
+ sql = compile Nodes::Except.new(left, right)
+ sql.must_be_like %{
+ ( SELECT * FROM users WHERE age > 10 MINUS SELECT * FROM users WHERE age > 20 )
+ }
+ end
+
+ it "generates select options offset then limit" do
+ stmt = Nodes::SelectStatement.new
+ stmt.offset = Nodes::Offset.new(1)
+ stmt.limit = Nodes::Limit.new(10)
+ sql = compile(stmt)
+ sql.must_be_like "SELECT OFFSET 1 ROWS FETCH FIRST 10 ROWS ONLY"
+ end
+
+ describe "locking" do
+ it "generates ArgumentError if limit and lock are used" do
+ stmt = Nodes::SelectStatement.new
+ stmt.limit = Nodes::Limit.new(10)
+ stmt.lock = Nodes::Lock.new(Arel.sql("FOR UPDATE"))
+ assert_raises ArgumentError do
+ compile(stmt)
+ end
+ end
+
+ it "defaults to FOR UPDATE when locking" do
+ node = Nodes::Lock.new(Arel.sql("FOR UPDATE"))
+ compile(node).must_be_like "FOR UPDATE"
+ end
+ end
+
+ describe "Nodes::BindParam" do
+ it "increments each bind param" do
+ query = @table[:name].eq(Arel::Nodes::BindParam.new(1))
+ .and(@table[:id].eq(Arel::Nodes::BindParam.new(1)))
+ compile(query).must_be_like %{
+ "users"."name" = :a1 AND "users"."id" = :a2
+ }
+ end
+ end
+
+ describe "Nodes::IsNotDistinctFrom" do
+ it "should construct a valid generic SQL statement" do
+ test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson"
+ compile(test).must_be_like %{
+ DECODE("users"."name", 'Aaron Patterson', 0, 1) = 0
+ }
+ end
+
+ it "should handle column names on both sides" do
+ test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name]
+ compile(test).must_be_like %{
+ DECODE("users"."first_name", "users"."last_name", 0, 1) = 0
+ }
+ end
+
+ it "should handle nil" do
+ @table = Table.new(:users)
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" IS NULL }
+ end
+ end
+
+ describe "Nodes::IsDistinctFrom" do
+ it "should handle column names on both sides" do
+ test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name]
+ compile(test).must_be_like %{
+ DECODE("users"."first_name", "users"."last_name", 0, 1) = 1
+ }
+ end
+
+ it "should handle nil" do
+ @table = Table.new(:users)
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::IsDistinctFrom.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" IS NOT NULL }
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/visitors/oracle_test.rb b/activerecord/test/cases/arel/visitors/oracle_test.rb
new file mode 100644
index 0000000000..893edc7f74
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/oracle_test.rb
@@ -0,0 +1,236 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Visitors
+ class OracleTest < Arel::Spec
+ before do
+ @visitor = Oracle.new Table.engine.connection
+ @table = Table.new(:users)
+ end
+
+ def compile(node)
+ @visitor.accept(node, Collectors::SQLString.new).value
+ end
+
+ it "modifies order when there is distinct and first value" do
+ # *sigh*
+ select = "DISTINCT foo.id, FIRST_VALUE(projects.name) OVER (foo) AS alias_0__"
+ stmt = Nodes::SelectStatement.new
+ stmt.cores.first.projections << Nodes::SqlLiteral.new(select)
+ stmt.orders << Nodes::SqlLiteral.new("foo")
+ sql = compile(stmt)
+ sql.must_be_like %{
+ SELECT #{select} ORDER BY alias_0__
+ }
+ end
+
+ it "is idempotent with crazy query" do
+ # *sigh*
+ select = "DISTINCT foo.id, FIRST_VALUE(projects.name) OVER (foo) AS alias_0__"
+ stmt = Nodes::SelectStatement.new
+ stmt.cores.first.projections << Nodes::SqlLiteral.new(select)
+ stmt.orders << Nodes::SqlLiteral.new("foo")
+
+ sql = compile(stmt)
+ sql2 = compile(stmt)
+ sql.must_equal sql2
+ end
+
+ it "splits orders with commas" do
+ # *sigh*
+ select = "DISTINCT foo.id, FIRST_VALUE(projects.name) OVER (foo) AS alias_0__"
+ stmt = Nodes::SelectStatement.new
+ stmt.cores.first.projections << Nodes::SqlLiteral.new(select)
+ stmt.orders << Nodes::SqlLiteral.new("foo, bar")
+ sql = compile(stmt)
+ sql.must_be_like %{
+ SELECT #{select} ORDER BY alias_0__, alias_1__
+ }
+ end
+
+ it "splits orders with commas and function calls" do
+ # *sigh*
+ select = "DISTINCT foo.id, FIRST_VALUE(projects.name) OVER (foo) AS alias_0__"
+ stmt = Nodes::SelectStatement.new
+ stmt.cores.first.projections << Nodes::SqlLiteral.new(select)
+ stmt.orders << Nodes::SqlLiteral.new("NVL(LOWER(bar, foo), foo) DESC, UPPER(baz)")
+ sql = compile(stmt)
+ sql.must_be_like %{
+ SELECT #{select} ORDER BY alias_0__ DESC, alias_1__
+ }
+ end
+
+ describe "Nodes::SelectStatement" do
+ describe "limit" do
+ it "adds a rownum clause" do
+ stmt = Nodes::SelectStatement.new
+ stmt.limit = Nodes::Limit.new(10)
+ sql = compile stmt
+ sql.must_be_like %{ SELECT WHERE ROWNUM <= 10 }
+ end
+
+ it "is idempotent" do
+ stmt = Nodes::SelectStatement.new
+ stmt.orders << Nodes::SqlLiteral.new("foo")
+ stmt.limit = Nodes::Limit.new(10)
+ sql = compile stmt
+ sql2 = compile stmt
+ sql.must_equal sql2
+ end
+
+ it "creates a subquery when there is order_by" do
+ stmt = Nodes::SelectStatement.new
+ stmt.orders << Nodes::SqlLiteral.new("foo")
+ stmt.limit = Nodes::Limit.new(10)
+ sql = compile stmt
+ sql.must_be_like %{
+ SELECT * FROM (SELECT ORDER BY foo ) WHERE ROWNUM <= 10
+ }
+ end
+
+ it "creates a subquery when there is group by" do
+ stmt = Nodes::SelectStatement.new
+ stmt.cores.first.groups << Nodes::SqlLiteral.new("foo")
+ stmt.limit = Nodes::Limit.new(10)
+ sql = compile stmt
+ sql.must_be_like %{
+ SELECT * FROM (SELECT GROUP BY foo ) WHERE ROWNUM <= 10
+ }
+ end
+
+ it "creates a subquery when there is DISTINCT" do
+ stmt = Nodes::SelectStatement.new
+ stmt.cores.first.set_quantifier = Arel::Nodes::Distinct.new
+ stmt.cores.first.projections << Nodes::SqlLiteral.new("id")
+ stmt.limit = Arel::Nodes::Limit.new(10)
+ sql = compile stmt
+ sql.must_be_like %{
+ SELECT * FROM (SELECT DISTINCT id ) WHERE ROWNUM <= 10
+ }
+ end
+
+ it "creates a different subquery when there is an offset" do
+ stmt = Nodes::SelectStatement.new
+ stmt.limit = Nodes::Limit.new(10)
+ stmt.offset = Nodes::Offset.new(10)
+ sql = compile stmt
+ sql.must_be_like %{
+ SELECT * FROM (
+ SELECT raw_sql_.*, rownum raw_rnum_
+ FROM (SELECT ) raw_sql_
+ WHERE rownum <= 20
+ )
+ WHERE raw_rnum_ > 10
+ }
+ end
+
+ it "creates a subquery when there is limit and offset with BindParams" do
+ stmt = Nodes::SelectStatement.new
+ stmt.limit = Nodes::Limit.new(Nodes::BindParam.new(1))
+ stmt.offset = Nodes::Offset.new(Nodes::BindParam.new(1))
+ sql = compile stmt
+ sql.must_be_like %{
+ SELECT * FROM (
+ SELECT raw_sql_.*, rownum raw_rnum_
+ FROM (SELECT ) raw_sql_
+ WHERE rownum <= (:a1 + :a2)
+ )
+ WHERE raw_rnum_ > :a3
+ }
+ end
+
+ it "is idempotent with different subquery" do
+ stmt = Nodes::SelectStatement.new
+ stmt.limit = Nodes::Limit.new(10)
+ stmt.offset = Nodes::Offset.new(10)
+ sql = compile stmt
+ sql2 = compile stmt
+ sql.must_equal sql2
+ end
+ end
+
+ describe "only offset" do
+ it "creates a select from subquery with rownum condition" do
+ stmt = Nodes::SelectStatement.new
+ stmt.offset = Nodes::Offset.new(10)
+ sql = compile stmt
+ sql.must_be_like %{
+ SELECT * FROM (
+ SELECT raw_sql_.*, rownum raw_rnum_
+ FROM (SELECT) raw_sql_
+ )
+ WHERE raw_rnum_ > 10
+ }
+ end
+ end
+ end
+
+ it "modified except to be minus" do
+ left = Nodes::SqlLiteral.new("SELECT * FROM users WHERE age > 10")
+ right = Nodes::SqlLiteral.new("SELECT * FROM users WHERE age > 20")
+ sql = compile Nodes::Except.new(left, right)
+ sql.must_be_like %{
+ ( SELECT * FROM users WHERE age > 10 MINUS SELECT * FROM users WHERE age > 20 )
+ }
+ end
+
+ describe "locking" do
+ it "defaults to FOR UPDATE when locking" do
+ node = Nodes::Lock.new(Arel.sql("FOR UPDATE"))
+ compile(node).must_be_like "FOR UPDATE"
+ end
+ end
+
+ describe "Nodes::BindParam" do
+ it "increments each bind param" do
+ query = @table[:name].eq(Arel::Nodes::BindParam.new(1))
+ .and(@table[:id].eq(Arel::Nodes::BindParam.new(1)))
+ compile(query).must_be_like %{
+ "users"."name" = :a1 AND "users"."id" = :a2
+ }
+ end
+ end
+
+ describe "Nodes::IsNotDistinctFrom" do
+ it "should construct a valid generic SQL statement" do
+ test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson"
+ compile(test).must_be_like %{
+ DECODE("users"."name", 'Aaron Patterson', 0, 1) = 0
+ }
+ end
+
+ it "should handle column names on both sides" do
+ test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name]
+ compile(test).must_be_like %{
+ DECODE("users"."first_name", "users"."last_name", 0, 1) = 0
+ }
+ end
+
+ it "should handle nil" do
+ @table = Table.new(:users)
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" IS NULL }
+ end
+ end
+
+ describe "Nodes::IsDistinctFrom" do
+ it "should handle column names on both sides" do
+ test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name]
+ compile(test).must_be_like %{
+ DECODE("users"."first_name", "users"."last_name", 0, 1) = 1
+ }
+ end
+
+ it "should handle nil" do
+ @table = Table.new(:users)
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::IsDistinctFrom.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" IS NOT NULL }
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/visitors/postgres_test.rb b/activerecord/test/cases/arel/visitors/postgres_test.rb
new file mode 100644
index 0000000000..0f9efb20b4
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/postgres_test.rb
@@ -0,0 +1,320 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Visitors
+ class PostgresTest < Arel::Spec
+ before do
+ @visitor = PostgreSQL.new Table.engine.connection
+ @table = Table.new(:users)
+ @attr = @table[:id]
+ end
+
+ def compile(node)
+ @visitor.accept(node, Collectors::SQLString.new).value
+ end
+
+ describe "locking" do
+ it "defaults to FOR UPDATE" do
+ compile(Nodes::Lock.new(Arel.sql("FOR UPDATE"))).must_be_like %{
+ FOR UPDATE
+ }
+ end
+
+ it "allows a custom string to be used as a lock" do
+ node = Nodes::Lock.new(Arel.sql("FOR SHARE"))
+ compile(node).must_be_like %{
+ FOR SHARE
+ }
+ end
+ end
+
+ it "should escape LIMIT" do
+ sc = Arel::Nodes::SelectStatement.new
+ sc.limit = Nodes::Limit.new(Nodes.build_quoted("omg"))
+ sc.cores.first.projections << Arel.sql("DISTINCT ON")
+ sc.orders << Arel.sql("xyz")
+ sql = compile(sc)
+ assert_match(/LIMIT 'omg'/, sql)
+ assert_equal 1, sql.scan(/LIMIT/).length, "should have one limit"
+ end
+
+ it "should support DISTINCT ON" do
+ core = Arel::Nodes::SelectCore.new
+ core.set_quantifier = Arel::Nodes::DistinctOn.new(Arel.sql("aaron"))
+ assert_match "DISTINCT ON ( aaron )", compile(core)
+ end
+
+ it "should support DISTINCT" do
+ core = Arel::Nodes::SelectCore.new
+ core.set_quantifier = Arel::Nodes::Distinct.new
+ assert_equal "SELECT DISTINCT", compile(core)
+ end
+
+ it "encloses LATERAL queries in parens" do
+ subquery = @table.project(:id).where(@table[:name].matches("foo%"))
+ compile(subquery.lateral).must_be_like %{
+ LATERAL (SELECT id FROM "users" WHERE "users"."name" ILIKE 'foo%')
+ }
+ end
+
+ it "produces LATERAL queries with alias" do
+ subquery = @table.project(:id).where(@table[:name].matches("foo%"))
+ compile(subquery.lateral("bar")).must_be_like %{
+ LATERAL (SELECT id FROM "users" WHERE "users"."name" ILIKE 'foo%') bar
+ }
+ end
+
+ describe "Nodes::Matches" do
+ it "should know how to visit" do
+ node = @table[:name].matches("foo%")
+ node.must_be_kind_of Nodes::Matches
+ node.case_sensitive.must_equal(false)
+ compile(node).must_be_like %{
+ "users"."name" ILIKE 'foo%'
+ }
+ end
+
+ it "should know how to visit case sensitive" do
+ node = @table[:name].matches("foo%", nil, true)
+ node.case_sensitive.must_equal(true)
+ compile(node).must_be_like %{
+ "users"."name" LIKE 'foo%'
+ }
+ end
+
+ it "can handle ESCAPE" do
+ node = @table[:name].matches("foo!%", "!")
+ compile(node).must_be_like %{
+ "users"."name" ILIKE 'foo!%' ESCAPE '!'
+ }
+ end
+
+ it "can handle subqueries" do
+ subquery = @table.project(:id).where(@table[:name].matches("foo%"))
+ node = @attr.in subquery
+ compile(node).must_be_like %{
+ "users"."id" IN (SELECT id FROM "users" WHERE "users"."name" ILIKE 'foo%')
+ }
+ end
+ end
+
+ describe "Nodes::DoesNotMatch" do
+ it "should know how to visit" do
+ node = @table[:name].does_not_match("foo%")
+ node.must_be_kind_of Nodes::DoesNotMatch
+ node.case_sensitive.must_equal(false)
+ compile(node).must_be_like %{
+ "users"."name" NOT ILIKE 'foo%'
+ }
+ end
+
+ it "should know how to visit case sensitive" do
+ node = @table[:name].does_not_match("foo%", nil, true)
+ node.case_sensitive.must_equal(true)
+ compile(node).must_be_like %{
+ "users"."name" NOT LIKE 'foo%'
+ }
+ end
+
+ it "can handle ESCAPE" do
+ node = @table[:name].does_not_match("foo!%", "!")
+ compile(node).must_be_like %{
+ "users"."name" NOT ILIKE 'foo!%' ESCAPE '!'
+ }
+ end
+
+ it "can handle subqueries" do
+ subquery = @table.project(:id).where(@table[:name].does_not_match("foo%"))
+ node = @attr.in subquery
+ compile(node).must_be_like %{
+ "users"."id" IN (SELECT id FROM "users" WHERE "users"."name" NOT ILIKE 'foo%')
+ }
+ end
+ end
+
+ describe "Nodes::Regexp" do
+ it "should know how to visit" do
+ node = @table[:name].matches_regexp("foo.*")
+ node.must_be_kind_of Nodes::Regexp
+ node.case_sensitive.must_equal(true)
+ compile(node).must_be_like %{
+ "users"."name" ~ 'foo.*'
+ }
+ end
+
+ it "can handle case insensitive" do
+ node = @table[:name].matches_regexp("foo.*", false)
+ node.must_be_kind_of Nodes::Regexp
+ node.case_sensitive.must_equal(false)
+ compile(node).must_be_like %{
+ "users"."name" ~* 'foo.*'
+ }
+ end
+
+ it "can handle subqueries" do
+ subquery = @table.project(:id).where(@table[:name].matches_regexp("foo.*"))
+ node = @attr.in subquery
+ compile(node).must_be_like %{
+ "users"."id" IN (SELECT id FROM "users" WHERE "users"."name" ~ 'foo.*')
+ }
+ end
+ end
+
+ describe "Nodes::NotRegexp" do
+ it "should know how to visit" do
+ node = @table[:name].does_not_match_regexp("foo.*")
+ node.must_be_kind_of Nodes::NotRegexp
+ node.case_sensitive.must_equal(true)
+ compile(node).must_be_like %{
+ "users"."name" !~ 'foo.*'
+ }
+ end
+
+ it "can handle case insensitive" do
+ node = @table[:name].does_not_match_regexp("foo.*", false)
+ node.case_sensitive.must_equal(false)
+ compile(node).must_be_like %{
+ "users"."name" !~* 'foo.*'
+ }
+ end
+
+ it "can handle subqueries" do
+ subquery = @table.project(:id).where(@table[:name].does_not_match_regexp("foo.*"))
+ node = @attr.in subquery
+ compile(node).must_be_like %{
+ "users"."id" IN (SELECT id FROM "users" WHERE "users"."name" !~ 'foo.*')
+ }
+ end
+ end
+
+ describe "Nodes::BindParam" do
+ it "increments each bind param" do
+ query = @table[:name].eq(Arel::Nodes::BindParam.new(1))
+ .and(@table[:id].eq(Arel::Nodes::BindParam.new(1)))
+ compile(query).must_be_like %{
+ "users"."name" = $1 AND "users"."id" = $2
+ }
+ end
+ end
+
+ describe "Nodes::Cube" do
+ it "should know how to visit with array arguments" do
+ node = Arel::Nodes::Cube.new([@table[:name], @table[:bool]])
+ compile(node).must_be_like %{
+ CUBE( "users"."name", "users"."bool" )
+ }
+ end
+
+ it "should know how to visit with CubeDimension Argument" do
+ dimensions = Arel::Nodes::GroupingElement.new([@table[:name], @table[:bool]])
+ node = Arel::Nodes::Cube.new(dimensions)
+ compile(node).must_be_like %{
+ CUBE( "users"."name", "users"."bool" )
+ }
+ end
+
+ it "should know how to generate parenthesis when supplied with many Dimensions" do
+ dim1 = Arel::Nodes::GroupingElement.new(@table[:name])
+ dim2 = Arel::Nodes::GroupingElement.new([@table[:bool], @table[:created_at]])
+ node = Arel::Nodes::Cube.new([dim1, dim2])
+ compile(node).must_be_like %{
+ CUBE( ( "users"."name" ), ( "users"."bool", "users"."created_at" ) )
+ }
+ end
+ end
+
+ describe "Nodes::GroupingSet" do
+ it "should know how to visit with array arguments" do
+ node = Arel::Nodes::GroupingSet.new([@table[:name], @table[:bool]])
+ compile(node).must_be_like %{
+ GROUPING SETS( "users"."name", "users"."bool" )
+ }
+ end
+
+ it "should know how to visit with CubeDimension Argument" do
+ group = Arel::Nodes::GroupingElement.new([@table[:name], @table[:bool]])
+ node = Arel::Nodes::GroupingSet.new(group)
+ compile(node).must_be_like %{
+ GROUPING SETS( "users"."name", "users"."bool" )
+ }
+ end
+
+ it "should know how to generate parenthesis when supplied with many Dimensions" do
+ group1 = Arel::Nodes::GroupingElement.new(@table[:name])
+ group2 = Arel::Nodes::GroupingElement.new([@table[:bool], @table[:created_at]])
+ node = Arel::Nodes::GroupingSet.new([group1, group2])
+ compile(node).must_be_like %{
+ GROUPING SETS( ( "users"."name" ), ( "users"."bool", "users"."created_at" ) )
+ }
+ end
+ end
+
+ describe "Nodes::RollUp" do
+ it "should know how to visit with array arguments" do
+ node = Arel::Nodes::RollUp.new([@table[:name], @table[:bool]])
+ compile(node).must_be_like %{
+ ROLLUP( "users"."name", "users"."bool" )
+ }
+ end
+
+ it "should know how to visit with CubeDimension Argument" do
+ group = Arel::Nodes::GroupingElement.new([@table[:name], @table[:bool]])
+ node = Arel::Nodes::RollUp.new(group)
+ compile(node).must_be_like %{
+ ROLLUP( "users"."name", "users"."bool" )
+ }
+ end
+
+ it "should know how to generate parenthesis when supplied with many Dimensions" do
+ group1 = Arel::Nodes::GroupingElement.new(@table[:name])
+ group2 = Arel::Nodes::GroupingElement.new([@table[:bool], @table[:created_at]])
+ node = Arel::Nodes::RollUp.new([group1, group2])
+ compile(node).must_be_like %{
+ ROLLUP( ( "users"."name" ), ( "users"."bool", "users"."created_at" ) )
+ }
+ end
+ end
+
+ describe "Nodes::IsNotDistinctFrom" do
+ it "should construct a valid generic SQL statement" do
+ test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson"
+ compile(test).must_be_like %{
+ "users"."name" IS NOT DISTINCT FROM 'Aaron Patterson'
+ }
+ end
+
+ it "should handle column names on both sides" do
+ test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name]
+ compile(test).must_be_like %{
+ "users"."first_name" IS NOT DISTINCT FROM "users"."last_name"
+ }
+ end
+
+ it "should handle nil" do
+ @table = Table.new(:users)
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" IS NOT DISTINCT FROM NULL }
+ end
+ end
+
+ describe "Nodes::IsDistinctFrom" do
+ it "should handle column names on both sides" do
+ test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name]
+ compile(test).must_be_like %{
+ "users"."first_name" IS DISTINCT FROM "users"."last_name"
+ }
+ end
+
+ it "should handle nil" do
+ @table = Table.new(:users)
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::IsDistinctFrom.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" IS DISTINCT FROM NULL }
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/visitors/sqlite_test.rb b/activerecord/test/cases/arel/visitors/sqlite_test.rb
new file mode 100644
index 0000000000..ee4e07a675
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/sqlite_test.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Visitors
+ class SqliteTest < Arel::Spec
+ before do
+ @visitor = SQLite.new Table.engine.connection
+ end
+
+ def compile(node)
+ @visitor.accept(node, Collectors::SQLString.new).value
+ end
+
+ it "defaults limit to -1" do
+ stmt = Nodes::SelectStatement.new
+ stmt.offset = Nodes::Offset.new(1)
+ sql = @visitor.accept(stmt, Collectors::SQLString.new).value
+ sql.must_be_like "SELECT LIMIT -1 OFFSET 1"
+ end
+
+ it "does not support locking" do
+ node = Nodes::Lock.new(Arel.sql("FOR UPDATE"))
+ assert_equal "", @visitor.accept(node, Collectors::SQLString.new).value
+ end
+
+ it "does not support boolean" do
+ node = Nodes::True.new()
+ assert_equal "1", @visitor.accept(node, Collectors::SQLString.new).value
+ node = Nodes::False.new()
+ assert_equal "0", @visitor.accept(node, Collectors::SQLString.new).value
+ end
+
+ describe "Nodes::IsNotDistinctFrom" do
+ it "should construct a valid generic SQL statement" do
+ test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson"
+ compile(test).must_be_like %{
+ "users"."name" IS 'Aaron Patterson'
+ }
+ end
+
+ it "should handle column names on both sides" do
+ test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name]
+ compile(test).must_be_like %{
+ "users"."first_name" IS "users"."last_name"
+ }
+ end
+
+ it "should handle nil" do
+ @table = Table.new(:users)
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" IS NULL }
+ end
+ end
+
+ describe "Nodes::IsDistinctFrom" do
+ it "should handle column names on both sides" do
+ test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name]
+ compile(test).must_be_like %{
+ "users"."first_name" IS NOT "users"."last_name"
+ }
+ end
+
+ it "should handle nil" do
+ @table = Table.new(:users)
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::IsDistinctFrom.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" IS NOT NULL }
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/visitors/to_sql_test.rb b/activerecord/test/cases/arel/visitors/to_sql_test.rb
new file mode 100644
index 0000000000..4bfa799a96
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/to_sql_test.rb
@@ -0,0 +1,713 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+require "bigdecimal"
+
+module Arel
+ module Visitors
+ describe "the to_sql visitor" do
+ before do
+ @conn = FakeRecord::Base.new
+ @visitor = ToSql.new @conn.connection
+ @table = Table.new(:users)
+ @attr = @table[:id]
+ end
+
+ def compile(node)
+ @visitor.accept(node, Collectors::SQLString.new).value
+ end
+
+ it "works with BindParams" do
+ node = Nodes::BindParam.new(1)
+ sql = compile node
+ sql.must_be_like "?"
+ end
+
+ it "does not quote BindParams used as part of a Values" do
+ bp = Nodes::BindParam.new(1)
+ values = Nodes::Values.new([bp])
+ sql = compile values
+ sql.must_be_like "VALUES (?)"
+ end
+
+ it "can define a dispatch method" do
+ visited = false
+ viz = Class.new(Arel::Visitors::Visitor) {
+ define_method(:hello) do |node, c|
+ visited = true
+ end
+
+ def dispatch
+ { Arel::Table => "hello" }
+ end
+ }.new
+
+ viz.accept(@table, Collectors::SQLString.new)
+ assert visited, "hello method was called"
+ end
+
+ it "should not quote sql literals" do
+ node = @table[Arel.star]
+ sql = compile node
+ sql.must_be_like '"users".*'
+ end
+
+ it "should visit named functions" do
+ function = Nodes::NamedFunction.new("omg", [Arel.star])
+ assert_equal "omg(*)", compile(function)
+ end
+
+ it "should chain predications on named functions" do
+ function = Nodes::NamedFunction.new("omg", [Arel.star])
+ sql = compile(function.eq(2))
+ sql.must_be_like %{ omg(*) = 2 }
+ end
+
+ it "should handle nil with named functions" do
+ function = Nodes::NamedFunction.new("omg", [Arel.star])
+ sql = compile(function.eq(nil))
+ sql.must_be_like %{ omg(*) IS NULL }
+ end
+
+ it "should visit built-in functions" do
+ function = Nodes::Count.new([Arel.star])
+ assert_equal "COUNT(*)", compile(function)
+
+ function = Nodes::Sum.new([Arel.star])
+ assert_equal "SUM(*)", compile(function)
+
+ function = Nodes::Max.new([Arel.star])
+ assert_equal "MAX(*)", compile(function)
+
+ function = Nodes::Min.new([Arel.star])
+ assert_equal "MIN(*)", compile(function)
+
+ function = Nodes::Avg.new([Arel.star])
+ assert_equal "AVG(*)", compile(function)
+ end
+
+ it "should visit built-in functions operating on distinct values" do
+ function = Nodes::Count.new([Arel.star])
+ function.distinct = true
+ assert_equal "COUNT(DISTINCT *)", compile(function)
+
+ function = Nodes::Sum.new([Arel.star])
+ function.distinct = true
+ assert_equal "SUM(DISTINCT *)", compile(function)
+
+ function = Nodes::Max.new([Arel.star])
+ function.distinct = true
+ assert_equal "MAX(DISTINCT *)", compile(function)
+
+ function = Nodes::Min.new([Arel.star])
+ function.distinct = true
+ assert_equal "MIN(DISTINCT *)", compile(function)
+
+ function = Nodes::Avg.new([Arel.star])
+ function.distinct = true
+ assert_equal "AVG(DISTINCT *)", compile(function)
+ end
+
+ it "works with lists" do
+ function = Nodes::NamedFunction.new("omg", [Arel.star, Arel.star])
+ assert_equal "omg(*, *)", compile(function)
+ end
+
+ describe "Nodes::Equality" do
+ it "should escape strings" do
+ test = Table.new(:users)[:name].eq "Aaron Patterson"
+ compile(test).must_be_like %{
+ "users"."name" = 'Aaron Patterson'
+ }
+ end
+
+ it "should handle false" do
+ table = Table.new(:users)
+ val = Nodes.build_quoted(false, table[:active])
+ sql = compile Nodes::Equality.new(val, val)
+ sql.must_be_like %{ 'f' = 'f' }
+ end
+
+ it "should handle nil" do
+ sql = compile Nodes::Equality.new(@table[:name], nil)
+ sql.must_be_like %{ "users"."name" IS NULL }
+ end
+ end
+
+ describe "Nodes::Grouping" do
+ it "wraps nested groupings in brackets only once" do
+ sql = compile Nodes::Grouping.new(Nodes::Grouping.new(Nodes.build_quoted("foo")))
+ sql.must_equal "('foo')"
+ end
+ end
+
+ describe "Nodes::NotEqual" do
+ it "should handle false" do
+ val = Nodes.build_quoted(false, @table[:active])
+ sql = compile Nodes::NotEqual.new(@table[:active], val)
+ sql.must_be_like %{ "users"."active" != 'f' }
+ end
+
+ it "should handle nil" do
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::NotEqual.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" IS NOT NULL }
+ end
+ end
+
+ describe "Nodes::IsNotDistinctFrom" do
+ it "should construct a valid generic SQL statement" do
+ test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson"
+ compile(test).must_be_like %{
+ CASE WHEN "users"."name" = 'Aaron Patterson' OR ("users"."name" IS NULL AND 'Aaron Patterson' IS NULL) THEN 0 ELSE 1 END = 0
+ }
+ end
+
+ it "should handle column names on both sides" do
+ test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name]
+ compile(test).must_be_like %{
+ CASE WHEN "users"."first_name" = "users"."last_name" OR ("users"."first_name" IS NULL AND "users"."last_name" IS NULL) THEN 0 ELSE 1 END = 0
+ }
+ end
+
+ it "should handle nil" do
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" IS NULL }
+ end
+ end
+
+ describe "Nodes::IsDistinctFrom" do
+ it "should handle column names on both sides" do
+ test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name]
+ compile(test).must_be_like %{
+ CASE WHEN "users"."first_name" = "users"."last_name" OR ("users"."first_name" IS NULL AND "users"."last_name" IS NULL) THEN 0 ELSE 1 END = 1
+ }
+ end
+
+ it "should handle nil" do
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::IsDistinctFrom.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" IS NOT NULL }
+ end
+ end
+
+ it "should visit string subclass" do
+ [
+ Class.new(String).new(":'("),
+ Class.new(Class.new(String)).new(":'("),
+ ].each do |obj|
+ val = Nodes.build_quoted(obj, @table[:active])
+ sql = compile Nodes::NotEqual.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" != ':\\'(' }
+ end
+ end
+
+ it "should visit_Class" do
+ compile(Nodes.build_quoted(DateTime)).must_equal "'DateTime'"
+ end
+
+ it "should escape LIMIT" do
+ sc = Arel::Nodes::SelectStatement.new
+ sc.limit = Arel::Nodes::Limit.new(Nodes.build_quoted("omg"))
+ assert_match(/LIMIT 'omg'/, compile(sc))
+ end
+
+ it "should contain a single space before ORDER BY" do
+ table = Table.new(:users)
+ test = table.order(table[:name])
+ sql = compile test
+ assert_match(/"users" ORDER BY/, sql)
+ end
+
+ it "should quote LIMIT without column type coercion" do
+ table = Table.new(:users)
+ sc = table.where(table[:name].eq(0)).take(1).ast
+ assert_match(/WHERE "users"."name" = 0 LIMIT 1/, compile(sc))
+ end
+
+ it "should visit_DateTime" do
+ dt = DateTime.now
+ table = Table.new(:users)
+ test = table[:created_at].eq dt
+ sql = compile test
+
+ sql.must_be_like %{"users"."created_at" = '#{dt.strftime("%Y-%m-%d %H:%M:%S")}'}
+ end
+
+ it "should visit_Float" do
+ test = Table.new(:products)[:price].eq 2.14
+ sql = compile test
+ sql.must_be_like %{"products"."price" = 2.14}
+ end
+
+ it "should visit_Not" do
+ sql = compile Nodes::Not.new(Arel.sql("foo"))
+ sql.must_be_like "NOT (foo)"
+ end
+
+ it "should apply Not to the whole expression" do
+ node = Nodes::And.new [@attr.eq(10), @attr.eq(11)]
+ sql = compile Nodes::Not.new(node)
+ sql.must_be_like %{NOT ("users"."id" = 10 AND "users"."id" = 11)}
+ end
+
+ it "should visit_As" do
+ as = Nodes::As.new(Arel.sql("foo"), Arel.sql("bar"))
+ sql = compile as
+ sql.must_be_like "foo AS bar"
+ end
+
+ it "should visit_Integer" do
+ compile 8787878092
+ end
+
+ it "should visit_Hash" do
+ compile(Nodes.build_quoted(a: 1))
+ end
+
+ it "should visit_Set" do
+ compile Nodes.build_quoted(Set.new([1, 2]))
+ end
+
+ it "should visit_BigDecimal" do
+ compile Nodes.build_quoted(BigDecimal("2.14"))
+ end
+
+ it "should visit_Date" do
+ dt = Date.today
+ table = Table.new(:users)
+ test = table[:created_at].eq dt
+ sql = compile test
+
+ sql.must_be_like %{"users"."created_at" = '#{dt.strftime("%Y-%m-%d")}'}
+ end
+
+ it "should visit_NilClass" do
+ compile(Nodes.build_quoted(nil)).must_be_like "NULL"
+ end
+
+ it "unsupported input should raise UnsupportedVisitError" do
+ error = assert_raises(UnsupportedVisitError) { compile(nil) }
+ assert_match(/\AUnsupported/, error.message)
+ end
+
+ it "should visit_Arel_SelectManager, which is a subquery" do
+ mgr = Table.new(:foo).project(:bar)
+ compile(mgr).must_be_like '(SELECT bar FROM "foo")'
+ end
+
+ it "should visit_Arel_Nodes_And" do
+ node = Nodes::And.new [@attr.eq(10), @attr.eq(11)]
+ compile(node).must_be_like %{
+ "users"."id" = 10 AND "users"."id" = 11
+ }
+ end
+
+ it "should visit_Arel_Nodes_Or" do
+ node = Nodes::Or.new @attr.eq(10), @attr.eq(11)
+ compile(node).must_be_like %{
+ "users"."id" = 10 OR "users"."id" = 11
+ }
+ end
+
+ it "should visit_Arel_Nodes_Assignment" do
+ column = @table["id"]
+ node = Nodes::Assignment.new(
+ Nodes::UnqualifiedColumn.new(column),
+ Nodes::UnqualifiedColumn.new(column)
+ )
+ compile(node).must_be_like %{
+ "id" = "id"
+ }
+ end
+
+ it "should visit visit_Arel_Attributes_Time" do
+ attr = Attributes::Time.new(@attr.relation, @attr.name)
+ compile attr
+ end
+
+ it "should visit_TrueClass" do
+ test = Table.new(:users)[:bool].eq(true)
+ compile(test).must_be_like %{ "users"."bool" = 't' }
+ end
+
+ describe "Nodes::Matches" do
+ it "should know how to visit" do
+ node = @table[:name].matches("foo%")
+ compile(node).must_be_like %{
+ "users"."name" LIKE 'foo%'
+ }
+ end
+
+ it "can handle ESCAPE" do
+ node = @table[:name].matches("foo!%", "!")
+ compile(node).must_be_like %{
+ "users"."name" LIKE 'foo!%' ESCAPE '!'
+ }
+ end
+
+ it "can handle subqueries" do
+ subquery = @table.project(:id).where(@table[:name].matches("foo%"))
+ node = @attr.in subquery
+ compile(node).must_be_like %{
+ "users"."id" IN (SELECT id FROM "users" WHERE "users"."name" LIKE 'foo%')
+ }
+ end
+ end
+
+ describe "Nodes::DoesNotMatch" do
+ it "should know how to visit" do
+ node = @table[:name].does_not_match("foo%")
+ compile(node).must_be_like %{
+ "users"."name" NOT LIKE 'foo%'
+ }
+ end
+
+ it "can handle ESCAPE" do
+ node = @table[:name].does_not_match("foo!%", "!")
+ compile(node).must_be_like %{
+ "users"."name" NOT LIKE 'foo!%' ESCAPE '!'
+ }
+ end
+
+ it "can handle subqueries" do
+ subquery = @table.project(:id).where(@table[:name].does_not_match("foo%"))
+ node = @attr.in subquery
+ compile(node).must_be_like %{
+ "users"."id" IN (SELECT id FROM "users" WHERE "users"."name" NOT LIKE 'foo%')
+ }
+ end
+ end
+
+ describe "Nodes::Ordering" do
+ it "should know how to visit" do
+ node = @attr.desc
+ compile(node).must_be_like %{
+ "users"."id" DESC
+ }
+ end
+ end
+
+ describe "Nodes::In" do
+ it "should know how to visit" do
+ node = @attr.in [1, 2, 3]
+ compile(node).must_be_like %{
+ "users"."id" IN (1, 2, 3)
+ }
+ end
+
+ it "should return 1=0 when empty right which is always false" do
+ node = @attr.in []
+ compile(node).must_equal "1=0"
+ end
+
+ it "can handle two dot ranges" do
+ node = @attr.between 1..3
+ compile(node).must_be_like %{
+ "users"."id" BETWEEN 1 AND 3
+ }
+ end
+
+ it "can handle three dot ranges" do
+ node = @attr.between 1...3
+ compile(node).must_be_like %{
+ "users"."id" >= 1 AND "users"."id" < 3
+ }
+ end
+
+ it "can handle ranges bounded by infinity" do
+ node = @attr.between 1..Float::INFINITY
+ compile(node).must_be_like %{
+ "users"."id" >= 1
+ }
+ node = @attr.between(-Float::INFINITY..3)
+ compile(node).must_be_like %{
+ "users"."id" <= 3
+ }
+ node = @attr.between(-Float::INFINITY...3)
+ compile(node).must_be_like %{
+ "users"."id" < 3
+ }
+ node = @attr.between(-Float::INFINITY..Float::INFINITY)
+ compile(node).must_be_like %{1=1}
+ end
+
+ it "can handle subqueries" do
+ table = Table.new(:users)
+ subquery = table.project(:id).where(table[:name].eq("Aaron"))
+ node = @attr.in subquery
+ compile(node).must_be_like %{
+ "users"."id" IN (SELECT id FROM "users" WHERE "users"."name" = 'Aaron')
+ }
+ end
+ end
+
+ describe "Nodes::InfixOperation" do
+ it "should handle Multiplication" do
+ node = Arel::Attributes::Decimal.new(Table.new(:products), :price) * Arel::Attributes::Decimal.new(Table.new(:currency_rates), :rate)
+ compile(node).must_equal %("products"."price" * "currency_rates"."rate")
+ end
+
+ it "should handle Division" do
+ node = Arel::Attributes::Decimal.new(Table.new(:products), :price) / 5
+ compile(node).must_equal %("products"."price" / 5)
+ end
+
+ it "should handle Addition" do
+ node = Arel::Attributes::Decimal.new(Table.new(:products), :price) + 6
+ compile(node).must_equal %(("products"."price" + 6))
+ end
+
+ it "should handle Subtraction" do
+ node = Arel::Attributes::Decimal.new(Table.new(:products), :price) - 7
+ compile(node).must_equal %(("products"."price" - 7))
+ end
+
+ it "should handle Concatenation" do
+ table = Table.new(:users)
+ node = table[:name].concat(table[:name])
+ compile(node).must_equal %("users"."name" || "users"."name")
+ end
+
+ it "should handle BitwiseAnd" do
+ node = Arel::Attributes::Integer.new(Table.new(:products), :bitmap) & 16
+ compile(node).must_equal %(("products"."bitmap" & 16))
+ end
+
+ it "should handle BitwiseOr" do
+ node = Arel::Attributes::Integer.new(Table.new(:products), :bitmap) | 16
+ compile(node).must_equal %(("products"."bitmap" | 16))
+ end
+
+ it "should handle BitwiseXor" do
+ node = Arel::Attributes::Integer.new(Table.new(:products), :bitmap) ^ 16
+ compile(node).must_equal %(("products"."bitmap" ^ 16))
+ end
+
+ it "should handle BitwiseShiftLeft" do
+ node = Arel::Attributes::Integer.new(Table.new(:products), :bitmap) << 4
+ compile(node).must_equal %(("products"."bitmap" << 4))
+ end
+
+ it "should handle BitwiseShiftRight" do
+ node = Arel::Attributes::Integer.new(Table.new(:products), :bitmap) >> 4
+ compile(node).must_equal %(("products"."bitmap" >> 4))
+ end
+
+ it "should handle arbitrary operators" do
+ node = Arel::Nodes::InfixOperation.new(
+ "&&",
+ Arel::Attributes::String.new(Table.new(:products), :name),
+ Arel::Attributes::String.new(Table.new(:products), :name)
+ )
+ compile(node).must_equal %("products"."name" && "products"."name")
+ end
+ end
+
+ describe "Nodes::UnaryOperation" do
+ it "should handle BitwiseNot" do
+ node = ~ Arel::Attributes::Integer.new(Table.new(:products), :bitmap)
+ compile(node).must_equal %( ~ "products"."bitmap")
+ end
+
+ it "should handle arbitrary operators" do
+ node = Arel::Nodes::UnaryOperation.new("!", Arel::Attributes::String.new(Table.new(:products), :active))
+ compile(node).must_equal %( ! "products"."active")
+ end
+ end
+
+ describe "Nodes::Union" do
+ it "squashes parenthesis on multiple unions" do
+ subnode = Nodes::Union.new Arel.sql("left"), Arel.sql("right")
+ node = Nodes::Union.new subnode, Arel.sql("topright")
+ assert_equal("( left UNION right UNION topright )", compile(node))
+ subnode = Nodes::Union.new Arel.sql("left"), Arel.sql("right")
+ node = Nodes::Union.new Arel.sql("topleft"), subnode
+ assert_equal("( topleft UNION left UNION right )", compile(node))
+ end
+ end
+
+ describe "Nodes::UnionAll" do
+ it "squashes parenthesis on multiple union alls" do
+ subnode = Nodes::UnionAll.new Arel.sql("left"), Arel.sql("right")
+ node = Nodes::UnionAll.new subnode, Arel.sql("topright")
+ assert_equal("( left UNION ALL right UNION ALL topright )", compile(node))
+ subnode = Nodes::UnionAll.new Arel.sql("left"), Arel.sql("right")
+ node = Nodes::UnionAll.new Arel.sql("topleft"), subnode
+ assert_equal("( topleft UNION ALL left UNION ALL right )", compile(node))
+ end
+ end
+
+ describe "Nodes::NotIn" do
+ it "should know how to visit" do
+ node = @attr.not_in [1, 2, 3]
+ compile(node).must_be_like %{
+ "users"."id" NOT IN (1, 2, 3)
+ }
+ end
+
+ it "should return 1=1 when empty right which is always true" do
+ node = @attr.not_in []
+ compile(node).must_equal "1=1"
+ end
+
+ it "can handle two dot ranges" do
+ node = @attr.not_between 1..3
+ compile(node).must_equal(
+ %{("users"."id" < 1 OR "users"."id" > 3)}
+ )
+ end
+
+ it "can handle three dot ranges" do
+ node = @attr.not_between 1...3
+ compile(node).must_equal(
+ %{("users"."id" < 1 OR "users"."id" >= 3)}
+ )
+ end
+
+ it "can handle ranges bounded by infinity" do
+ node = @attr.not_between 1..Float::INFINITY
+ compile(node).must_be_like %{
+ "users"."id" < 1
+ }
+ node = @attr.not_between(-Float::INFINITY..3)
+ compile(node).must_be_like %{
+ "users"."id" > 3
+ }
+ node = @attr.not_between(-Float::INFINITY...3)
+ compile(node).must_be_like %{
+ "users"."id" >= 3
+ }
+ node = @attr.not_between(-Float::INFINITY..Float::INFINITY)
+ compile(node).must_be_like %{1=0}
+ end
+
+ it "can handle subqueries" do
+ table = Table.new(:users)
+ subquery = table.project(:id).where(table[:name].eq("Aaron"))
+ node = @attr.not_in subquery
+ compile(node).must_be_like %{
+ "users"."id" NOT IN (SELECT id FROM "users" WHERE "users"."name" = 'Aaron')
+ }
+ end
+ end
+
+ describe "Constants" do
+ it "should handle true" do
+ test = Table.new(:users).create_true
+ compile(test).must_be_like %{
+ TRUE
+ }
+ end
+
+ it "should handle false" do
+ test = Table.new(:users).create_false
+ compile(test).must_be_like %{
+ FALSE
+ }
+ end
+ end
+
+ describe "TableAlias" do
+ it "should use the underlying table for checking columns" do
+ test = Table.new(:users).alias("zomgusers")[:id].eq "3"
+ compile(test).must_be_like %{
+ "zomgusers"."id" = '3'
+ }
+ end
+ end
+
+ describe "distinct on" do
+ it "raises not implemented error" do
+ core = Arel::Nodes::SelectCore.new
+ core.set_quantifier = Arel::Nodes::DistinctOn.new(Arel.sql("aaron"))
+
+ assert_raises(NotImplementedError) do
+ compile(core)
+ end
+ end
+ end
+
+ describe "Nodes::Regexp" do
+ it "raises not implemented error" do
+ node = Arel::Nodes::Regexp.new(@table[:name], Nodes.build_quoted("foo%"))
+
+ assert_raises(NotImplementedError) do
+ compile(node)
+ end
+ end
+ end
+
+ describe "Nodes::NotRegexp" do
+ it "raises not implemented error" do
+ node = Arel::Nodes::NotRegexp.new(@table[:name], Nodes.build_quoted("foo%"))
+
+ assert_raises(NotImplementedError) do
+ compile(node)
+ end
+ end
+ end
+
+ describe "Nodes::Case" do
+ it "supports simple case expressions" do
+ node = Arel::Nodes::Case.new(@table[:name])
+ .when("foo").then(1)
+ .else(0)
+
+ compile(node).must_be_like %{
+ CASE "users"."name" WHEN 'foo' THEN 1 ELSE 0 END
+ }
+ end
+
+ it "supports extended case expressions" do
+ node = Arel::Nodes::Case.new
+ .when(@table[:name].in(%w(foo bar))).then(1)
+ .else(0)
+
+ compile(node).must_be_like %{
+ CASE WHEN "users"."name" IN ('foo', 'bar') THEN 1 ELSE 0 END
+ }
+ end
+
+ it "works without default branch" do
+ node = Arel::Nodes::Case.new(@table[:name])
+ .when("foo").then(1)
+
+ compile(node).must_be_like %{
+ CASE "users"."name" WHEN 'foo' THEN 1 END
+ }
+ end
+
+ it "allows chaining multiple conditions" do
+ node = Arel::Nodes::Case.new(@table[:name])
+ .when("foo").then(1)
+ .when("bar").then(2)
+ .else(0)
+
+ compile(node).must_be_like %{
+ CASE "users"."name" WHEN 'foo' THEN 1 WHEN 'bar' THEN 2 ELSE 0 END
+ }
+ end
+
+ it "supports #when with two arguments and no #then" do
+ node = Arel::Nodes::Case.new @table[:name]
+
+ { foo: 1, bar: 0 }.reduce(node) { |_node, pair| _node.when(*pair) }
+
+ compile(node).must_be_like %{
+ CASE "users"."name" WHEN 'foo' THEN 1 WHEN 'bar' THEN 0 END
+ }
+ end
+
+ it "can be chained as a predicate" do
+ node = @table[:name].when("foo").then("bar").else("baz")
+
+ compile(node).must_be_like %{
+ CASE "users"."name" WHEN 'foo' THEN 'bar' ELSE 'baz' END
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/associations/association_scope_test.rb b/activerecord/test/cases/associations/association_scope_test.rb
deleted file mode 100644
index 472e270f8c..0000000000
--- a/activerecord/test/cases/associations/association_scope_test.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-require 'cases/helper'
-require 'models/post'
-require 'models/author'
-
-module ActiveRecord
- module Associations
- class AssociationScopeTest < ActiveRecord::TestCase
- test 'does not duplicate conditions' do
- scope = AssociationScope.scope(Author.new.association(:welcome_posts),
- Author.connection)
- binds = scope.where_clause.binds.map(&:value)
- assert_equal binds.uniq, binds
- end
- end
- end
-end
diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb
index ebe08c618f..93dd427951 100644
--- a/activerecord/test/cases/associations/belongs_to_associations_test.rb
+++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb
@@ -1,26 +1,30 @@
-require 'cases/helper'
-require 'models/developer'
-require 'models/computer'
-require 'models/project'
-require 'models/company'
-require 'models/topic'
-require 'models/reply'
-require 'models/computer'
-require 'models/post'
-require 'models/author'
-require 'models/tag'
-require 'models/tagging'
-require 'models/comment'
-require 'models/sponsor'
-require 'models/member'
-require 'models/essay'
-require 'models/toy'
-require 'models/invoice'
-require 'models/line_item'
-require 'models/column'
-require 'models/record'
-require 'models/admin'
-require 'models/admin/user'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/developer"
+require "models/project"
+require "models/company"
+require "models/topic"
+require "models/reply"
+require "models/computer"
+require "models/post"
+require "models/author"
+require "models/tag"
+require "models/tagging"
+require "models/comment"
+require "models/sponsor"
+require "models/member"
+require "models/essay"
+require "models/toy"
+require "models/invoice"
+require "models/line_item"
+require "models/column"
+require "models/record"
+require "models/admin"
+require "models/admin/user"
+require "models/ship"
+require "models/treasure"
+require "models/parrot"
class BelongsToAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :developers, :projects, :topics,
@@ -33,6 +37,21 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal companies(:first_firm).name, firm.name
end
+ def test_assigning_belongs_to_on_destroyed_object
+ client = Client.create!(name: "Client")
+ client.destroy!
+ assert_raise(frozen_error_class) { client.firm = nil }
+ assert_raise(frozen_error_class) { client.firm = Firm.new(name: "Firm") }
+ end
+
+ def test_eager_loading_wont_mutate_owner_record
+ client = Client.eager_load(:firm_with_basic_id).first
+ assert_not_predicate client, :firm_id_came_from_user?
+
+ client = Client.preload(:firm_with_basic_id).first
+ assert_not_predicate client, :firm_id_came_from_user?
+ end
+
def test_missing_attribute_error_is_raised_when_no_foreign_key_attribute
assert_raises(ActiveModel::MissingAttributeError) { Client.select(:id).first.firm }
end
@@ -41,17 +60,17 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
ActiveRecord::SQLCounter.clear_log
Client.find(3).firm
ensure
- assert ActiveRecord::SQLCounter.log_all.all? { |sql| /order by/i !~ sql }, 'ORDER BY was used in the query'
+ assert ActiveRecord::SQLCounter.log_all.all? { |sql| /order by/i !~ sql }, "ORDER BY was used in the query"
end
def test_belongs_to_with_primary_key
- client = Client.create(:name => "Primary key client", :firm_name => companies(:first_firm).name)
+ client = Client.create(name: "Primary key client", firm_name: companies(:first_firm).name)
assert_equal companies(:first_firm).name, client.firm_with_primary_key.name
end
def test_belongs_to_with_primary_key_joins_on_correct_column
sql = Client.joins(:firm_with_primary_key).to_sql
- if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
+ if current_adapter?(:Mysql2Adapter)
assert_no_match(/`firm_with_primary_keys_companies`\.`id`/, sql)
assert_match(/`firm_with_primary_keys_companies`\.`name`/, sql)
elsif current_adapter?(:OracleAdapter)
@@ -75,7 +94,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
account = model.new
- assert account.valid?
+ assert_predicate account, :valid?
ensure
ActiveRecord::Base.belongs_to_required_by_default = original_value
end
@@ -91,8 +110,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
account = model.new
- refute account.valid?
- assert_equal [{error: :blank}], account.errors.details[:company]
+ assert_not_predicate account, :valid?
+ assert_equal [{ error: :blank }], account.errors.details[:company]
ensure
ActiveRecord::Base.belongs_to_required_by_default = original_value
end
@@ -108,31 +127,69 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
account = model.new
- refute account.valid?
- assert_equal [{error: :blank}], account.errors.details[:company]
+ assert_not_predicate account, :valid?
+ assert_equal [{ error: :blank }], account.errors.details[:company]
ensure
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_with_lambda
+ model = Class.new(ActiveRecord::Base) do
+ self.table_name = "ships"
+ def self.name; "Temp"; end
+ belongs_to :developer, default: -> { default_developer }
+
+ def default_developer
+ Developer.first
+ end
+ end
+
+ ship = model.create!
+ assert_equal developers(:david), ship.developer
+
+ ship = model.create!(developer: developers(:jamis))
+ assert_equal developers(:jamis), ship.developer
+ end
+
def test_default_scope_on_relations_is_not_cached
counter = 0
comments = Class.new(ActiveRecord::Base) {
- self.table_name = 'comments'
- self.inheritance_column = 'not_there'
+ self.table_name = "comments"
+ self.inheritance_column = "not_there"
posts = Class.new(ActiveRecord::Base) {
- self.table_name = 'posts'
- self.inheritance_column = 'not_there'
+ self.table_name = "posts"
+ self.inheritance_column = "not_there"
default_scope -> {
counter += 1
- where("id = :inc", :inc => counter)
+ where("id = :inc", inc: counter)
}
- has_many :comments, :anonymous_class => comments
+ has_many :comments, anonymous_class: comments
}
- belongs_to :post, :anonymous_class => posts, :inverse_of => false
+ belongs_to :post, anonymous_class: posts, inverse_of: false
}
assert_equal 0, counter
@@ -164,9 +221,9 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
Admin.const_set "Region", Class.new(ActiveRecord::Base)
e = assert_raise(ActiveRecord::AssociationTypeMismatch) {
- Admin::RegionalUser.new(region: 'wrong value')
+ Admin::RegionalUser.new(region: "wrong value")
}
- assert_match(/^Region\([^)]+\) expected, got String\([^)]+\)$/, e.message)
+ assert_match(/^Region\([^)]+\) expected, got "wrong value" which is an instance of String\([^)]+\)$/, e.message)
ensure
Admin.send :remove_const, "Region" if Admin.const_defined?("Region")
Admin.send :remove_const, "RegionalUser" if Admin.const_defined?("RegionalUser")
@@ -175,6 +232,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
remove_column :admin_users, :region_id if column_exists?(:admin_users, :region_id)
drop_table :admin_regions, if_exists: true
end
+
+ Admin::User.reset_column_information
end
def test_natural_assignment
@@ -201,15 +260,15 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
def test_eager_loading_with_primary_key
Firm.create("name" => "Apple")
Client.create("name" => "Citibank", :firm_name => "Apple")
- citibank_result = Client.all.merge!(:where => {:name => "Citibank"}, :includes => :firm_with_primary_key).first
- assert citibank_result.association(:firm_with_primary_key).loaded?
+ citibank_result = Client.all.merge!(where: { name: "Citibank" }, includes: :firm_with_primary_key).first
+ assert_predicate citibank_result.association(:firm_with_primary_key), :loaded?
end
def test_eager_loading_with_primary_key_as_symbol
Firm.create("name" => "Apple")
Client.create("name" => "Citibank", :firm_name => "Apple")
- citibank_result = Client.all.merge!(:where => {:name => "Citibank"}, :includes => :firm_with_primary_key_symbols).first
- assert citibank_result.association(:firm_with_primary_key_symbols).loaded?
+ citibank_result = Client.all.merge!(where: { name: "Citibank" }, includes: :firm_with_primary_key_symbols).first
+ assert_predicate citibank_result.association(:firm_with_primary_key_symbols), :loaded?
end
def test_creating_the_belonging_object
@@ -221,8 +280,17 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal apple, citibank.firm
end
+ def test_creating_the_belonging_object_from_new_record
+ citibank = Account.new("credit_limit" => 10)
+ apple = citibank.create_firm("name" => "Apple")
+ assert_equal apple, citibank.firm
+ citibank.save
+ citibank.reload
+ assert_equal apple, citibank.firm
+ end
+
def test_creating_the_belonging_object_with_primary_key
- client = Client.create(:name => "Primary key client")
+ client = Client.create(name: "Primary key client")
apple = client.create_firm_with_primary_key("name" => "Apple")
assert_equal apple, client.firm_with_primary_key
client.save
@@ -245,63 +313,99 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
def test_building_the_belonging_object_with_explicit_sti_base_class
account = Account.new
- company = account.build_firm(:type => "Company")
+ company = account.build_firm(type: "Company")
assert_kind_of Company, company, "Expected #{company.class} to be a Company"
end
def test_building_the_belonging_object_with_sti_subclass
account = Account.new
- company = account.build_firm(:type => "Firm")
+ company = account.build_firm(type: "Firm")
assert_kind_of Firm, company, "Expected #{company.class} to be a Firm"
end
def test_building_the_belonging_object_with_an_invalid_type
account = Account.new
- assert_raise(ActiveRecord::SubclassNotFound) { account.build_firm(:type => "InvalidType") }
+ assert_raise(ActiveRecord::SubclassNotFound) { account.build_firm(type: "InvalidType") }
end
def test_building_the_belonging_object_with_an_unrelated_type
account = Account.new
- assert_raise(ActiveRecord::SubclassNotFound) { account.build_firm(:type => "Account") }
+ assert_raise(ActiveRecord::SubclassNotFound) { account.build_firm(type: "Account") }
end
def test_building_the_belonging_object_with_primary_key
- client = Client.create(:name => "Primary key client")
+ client = Client.create(name: "Primary key client")
apple = client.build_firm_with_primary_key("name" => "Apple")
client.save
assert_equal apple.name, client.firm_name
end
def test_create!
- client = Client.create!(:name => "Jimmy")
- account = client.create_account!(:credit_limit => 10)
+ client = Client.create!(name: "Jimmy")
+ account = client.create_account!(credit_limit: 10)
assert_equal account, client.account
- assert account.persisted?
+ assert_predicate account, :persisted?
client.save
client.reload
assert_equal account, client.account
end
def test_failing_create!
- client = Client.create!(:name => "Jimmy")
+ client = Client.create!(name: "Jimmy")
assert_raise(ActiveRecord::RecordInvalid) { client.create_account! }
assert_not_nil client.account
- assert client.account.new_record?
+ assert_predicate client.account, :new_record?
+ end
+
+ def test_reloading_the_belonging_object
+ 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_reload_the_belonging_object_with_query_cache
+ odegy_account_id = accounts(:odegy_account).id
+
+ connection = ActiveRecord::Base.connection
+ connection.enable_query_cache!
+ connection.clear_query_cache
+
+ # Populate the cache with a query
+ odegy_account = Account.find(odegy_account_id)
+
+ # Populate the cache with a second query
+ odegy_account.firm
+
+ assert_equal 2, connection.query_cache.size
+
+ # Clear the cache and fetch the firm again, populating the cache with a query
+ assert_queries(1) { odegy_account.reload_firm }
+
+ # This query is not cached anymore, so it should make a real SQL query
+ assert_queries(1) { Account.find(odegy_account_id) }
+ ensure
+ ActiveRecord::Base.connection.disable_query_cache!
end
def test_natural_assignment_to_nil
client = Client.find(3)
client.firm = nil
client.save
- assert_nil client.firm(true)
+ client.association(:firm).reload
+ assert_nil client.firm
assert_nil client.client_of
end
def test_natural_assignment_to_nil_with_primary_key
- client = Client.create(:name => "Primary key client", :firm_name => companies(:first_firm).name)
+ client = Client.create(name: "Primary key client", firm_name: companies(:first_firm).name)
client.firm_with_primary_key = nil
client.save
- assert_nil client.firm_with_primary_key(true)
+ client.association(:firm_with_primary_key).reload
+ assert_nil client.firm_with_primary_key
assert_nil client.client_of
end
@@ -318,20 +422,21 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
def test_polymorphic_association_class
sponsor = Sponsor.new
assert_nil sponsor.association(:sponsorable).send(:klass)
- assert_nil sponsor.sponsorable(force_reload: true)
+ sponsor.association(:sponsorable).reload
+ assert_nil sponsor.sponsorable
- sponsor.sponsorable_type = '' # the column doesn't have to be declared NOT NULL
+ sponsor.sponsorable_type = "" # the column doesn't have to be declared NOT NULL
assert_nil sponsor.association(:sponsorable).send(:klass)
- assert_nil sponsor.sponsorable(force_reload: true)
+ sponsor.association(:sponsorable).reload
+ assert_nil sponsor.sponsorable
- sponsor.sponsorable = Member.new :name => "Bert"
+ sponsor.sponsorable = Member.new name: "Bert"
assert_equal Member, sponsor.association(:sponsorable).send(:klass)
- assert_equal "members", sponsor.association(:sponsorable).aliased_table_name
end
def test_with_polymorphic_and_condition
sponsor = Sponsor.create
- member = Member.create :name => "Bert"
+ member = Member.create name: "Bert"
sponsor.sponsorable = member
assert_equal member, sponsor.sponsorable
@@ -340,7 +445,23 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
def test_with_select
assert_equal 1, Company.find(2).firm_with_select.attributes.size
- assert_equal 1, Company.all.merge!(:includes => :firm_with_select ).find(2).firm_with_select.attributes.size
+ assert_equal 1, Company.all.merge!(includes: :firm_with_select).find(2).firm_with_select.attributes.size
+ end
+
+ def test_belongs_to_without_counter_cache_option
+ # Ship has a conventionally named `treasures_count` column, but the counter_cache
+ # option is not given on the association.
+ ship = Ship.create(name: "Countless")
+
+ assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed unless counter_cache is given on the relation" do
+ treasure = Treasure.new(name: "Gold", ship: ship)
+ treasure.save
+ end
+
+ assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed unless counter_cache is given on the relation" do
+ treasure = ship.treasures.first
+ treasure.destroy
+ end
end
def test_belongs_to_counter
@@ -355,31 +476,70 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
def test_belongs_to_counter_with_assigning_nil
- post = Post.find(1)
- comment = Comment.find(1)
+ topic = Topic.create!(title: "debate")
+ reply = Reply.create!(title: "blah!", content: "world around!", topic: topic)
+
+ assert_equal topic.id, reply.parent_id
+ assert_equal 1, topic.reload.replies.size
+
+ reply.topic = nil
+ reply.reload
+
+ assert_equal topic.id, reply.parent_id
+ assert_equal 1, topic.reload.replies.size
+
+ reply.topic = nil
+ reply.save!
+
+ assert_equal 0, topic.reload.replies.size
+ end
+
+ def test_belongs_to_counter_with_assigning_new_object
+ topic = Topic.create!(title: "debate")
+ reply = Reply.create!(title: "blah!", content: "world around!", topic: topic)
- assert_equal post.id, comment.post_id
- assert_equal 2, Post.find(post.id).comments.size
+ assert_equal topic.id, reply.parent_id
+ assert_equal 1, topic.reload.replies_count
- comment.post = nil
+ topic2 = reply.build_topic(title: "debate2")
+ reply.save!
- assert_equal 1, Post.find(post.id).comments.size
+ assert_not_equal topic.id, reply.parent_id
+ assert_equal topic2.id, reply.parent_id
+
+ assert_equal 0, topic.reload.replies_count
+ assert_equal 1, topic2.reload.replies_count
end
def test_belongs_to_with_primary_key_counter
debate = Topic.create("title" => "debate")
debate2 = Topic.create("title" => "debate2")
- reply = Reply.create("title" => "blah!", "content" => "world around!", "parent_title" => "debate")
+ reply = Reply.create("title" => "blah!", "content" => "world around!", "parent_title" => "debate2")
+
+ assert_equal 0, debate.reload.replies_count
+ assert_equal 1, debate2.reload.replies_count
+
+ reply.parent_title = "debate"
+ reply.save!
+
+ assert_equal 1, debate.reload.replies_count
+ assert_equal 0, debate2.reload.replies_count
+
+ assert_no_queries do
+ reply.topic_with_primary_key = debate
+ end
assert_equal 1, debate.reload.replies_count
assert_equal 0, debate2.reload.replies_count
reply.topic_with_primary_key = debate2
+ reply.save!
assert_equal 0, debate.reload.replies_count
assert_equal 1, debate2.reload.replies_count
reply.topic_with_primary_key = nil
+ reply.save!
assert_equal 0, debate.reload.replies_count
assert_equal 0, debate2.reload.replies_count
@@ -406,11 +566,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal 1, Topic.find(topic2.id).replies.size
reply1.topic = nil
+ reply1.save!
assert_equal 0, Topic.find(topic1.id).replies.size
assert_equal 0, Topic.find(topic2.id).replies.size
reply1.topic = topic1
+ reply1.save!
assert_equal 1, Topic.find(topic1.id).replies.size
assert_equal 0, Topic.find(topic2.id).replies.size
@@ -439,14 +601,65 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
def test_belongs_to_counter_after_save
- topic = Topic.create!(:title => "monday night")
- topic.replies.create!(:title => "re: monday night", :content => "football")
+ topic = Topic.create!(title: "monday night")
+
+ assert_queries(2) do
+ topic.replies.create!(title: "re: monday night", content: "football")
+ end
+
assert_equal 1, Topic.find(topic.id)[:replies_count]
topic.save!
assert_equal 1, Topic.find(topic.id)[:replies_count]
end
+ def test_belongs_to_counter_after_touch
+ topic = Topic.create!(title: "topic")
+
+ assert_equal 0, topic.replies_count
+ assert_equal 0, topic.after_touch_called
+
+ reply = Reply.create!(title: "blah!", content: "world around!", topic_with_primary_key: topic)
+
+ assert_equal 1, topic.replies_count
+ assert_equal 1, topic.after_touch_called
+
+ reply.destroy!
+
+ assert_equal 0, topic.replies_count
+ assert_equal 2, topic.after_touch_called
+ end
+
+ def test_belongs_to_touch_with_reassigning
+ debate = Topic.create!(title: "debate")
+ debate2 = Topic.create!(title: "debate2")
+ reply = Reply.create!(title: "blah!", content: "world around!", parent_title: "debate2")
+
+ time = 1.day.ago
+
+ debate.touch(time: time)
+ debate2.touch(time: time)
+
+ assert_queries(3) do
+ reply.parent_title = "debate"
+ reply.save!
+ end
+
+ assert_operator debate.reload.updated_at, :>, time
+ assert_operator debate2.reload.updated_at, :>, time
+
+ debate.touch(time: time)
+ debate2.touch(time: time)
+
+ assert_queries(3) do
+ reply.topic_with_primary_key = debate2
+ reply.save!
+ end
+
+ assert_operator debate.reload.updated_at, :>, time
+ assert_operator debate2.reload.updated_at, :>, time
+ end
+
def test_belongs_to_with_touch_option_on_touch
line_item = LineItem.create!
Invoice.create!(line_items: [line_item])
@@ -478,7 +691,9 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
line_item = LineItem.create!
invoice = Invoice.create!(line_items: [line_item])
initial = invoice.updated_at
- line_item.touch
+ travel(1.second) do
+ line_item.touch
+ end
assert_not_equal initial, invoice.reload.updated_at
end
@@ -503,7 +718,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
line_item = LineItem.create!
Invoice.create!(line_items: [line_item])
- assert_queries(0) { line_item.save }
+ assert_no_queries { line_item.save }
end
def test_belongs_to_with_touch_option_on_destroy
@@ -540,8 +755,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
def test_belongs_to_counter_when_update_columns
- topic = Topic.create!(:title => "37s")
- topic.replies.create!(:title => "re: 37s", :content => "rails")
+ topic = Topic.create!(title: "37s")
+ topic.replies.create!(title: "re: 37s", content: "rails")
assert_equal 1, Topic.find(topic.id)[:replies_count]
topic.update_columns(content: "rails is wonderful")
@@ -552,30 +767,31 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
final_cut = Client.new("name" => "Final Cut")
firm = Firm.find(1)
final_cut.firm = firm
- assert !final_cut.persisted?
+ assert_not_predicate final_cut, :persisted?
assert final_cut.save
- assert final_cut.persisted?
- assert firm.persisted?
+ assert_predicate final_cut, :persisted?
+ assert_predicate firm, :persisted?
+ assert_equal firm, final_cut.firm
+ final_cut.association(:firm).reload
assert_equal firm, final_cut.firm
- assert_equal firm, final_cut.firm(true)
end
def test_assignment_before_child_saved_with_primary_key
final_cut = Client.new("name" => "Final Cut")
firm = Firm.find(1)
final_cut.firm_with_primary_key = firm
- assert !final_cut.persisted?
+ assert_not_predicate final_cut, :persisted?
assert final_cut.save
- assert final_cut.persisted?
- assert firm.persisted?
+ assert_predicate final_cut, :persisted?
+ assert_predicate firm, :persisted?
+ assert_equal firm, final_cut.firm_with_primary_key
+ final_cut.association(:firm_with_primary_key).reload
assert_equal firm, final_cut.firm_with_primary_key
- assert_equal firm, final_cut.firm_with_primary_key(true)
end
def test_new_record_with_foreign_key_but_no_object
client = Client.new("firm_id" => 1)
- # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first
- assert_equal Firm.all.merge!(:order => "id").first, client.firm_with_basic_id
+ assert_equal Firm.first, client.firm_with_basic_id
end
def test_setting_foreign_key_after_nil_target_loaded
@@ -597,7 +813,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
def test_dont_find_target_when_foreign_key_is_null
tagging = taggings(:thinking_general)
- assert_queries(0) { tagging.super_tag }
+ assert_no_queries { tagging.super_tag }
+ end
+
+ def test_dont_find_target_when_saving_foreign_key_after_stale_association_loaded
+ client = Client.create!(name: "Test client", firm_with_basic_id: Firm.find(1))
+ client.firm_id = Firm.create!(name: "Test firm").id
+ assert_queries(1) { client.save! }
end
def test_field_name_same_as_foreign_key
@@ -606,11 +828,12 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
def test_counter_cache
- topic = Topic.create :title => "Zoom-zoom-zoom"
+ topic = Topic.create title: "Zoom-zoom-zoom"
assert_equal 0, topic[:replies_count]
- reply = Reply.create(:title => "re: zoom", :content => "speedy quick!")
+ reply = Reply.create(title: "re: zoom", content: "speedy quick!")
reply.topic = topic
+ reply.save!
assert_equal 1, topic.reload[:replies_count]
assert_equal 1, topic.replies.size
@@ -620,10 +843,10 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
def test_counter_cache_double_destroy
- topic = Topic.create :title => "Zoom-zoom-zoom"
+ topic = Topic.create title: "Zoom-zoom-zoom"
5.times do
- topic.replies.create(:title => "re: zoom", :content => "speedy quick!")
+ topic.replies.create(title: "re: zoom", content: "speedy quick!")
end
assert_equal 5, topic.reload[:replies_count]
@@ -640,10 +863,10 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
def test_concurrent_counter_cache_double_destroy
- topic = Topic.create :title => "Zoom-zoom-zoom"
+ topic = Topic.create title: "Zoom-zoom-zoom"
5.times do
- topic.replies.create(:title => "re: zoom", :content => "speedy quick!")
+ topic.replies.create(title: "re: zoom", content: "speedy quick!")
end
assert_equal 5, topic.reload[:replies_count]
@@ -661,11 +884,12 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
def test_custom_counter_cache
- reply = Reply.create(:title => "re: zoom", :content => "speedy quick!")
+ reply = Reply.create(title: "re: zoom", content: "speedy quick!")
assert_equal 0, reply[:replies_count]
- silly = SillyReply.create(:title => "gaga", :content => "boo-boo")
+ silly = SillyReply.create(title: "gaga", content: "boo-boo")
silly.reply = reply
+ silly.save!
assert_equal 1, reply.reload[:replies_count]
assert_equal 1, reply.replies.size
@@ -674,10 +898,21 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal 17, reply.replies.size
end
+ def test_replace_counter_cache
+ topic = Topic.create(title: "Zoom-zoom-zoom")
+ reply = Reply.create(title: "re: zoom", content: "speedy quick!")
+
+ reply.topic = topic
+ reply.save
+ topic.reload
+
+ assert_equal 1, topic.replies_count
+ end
+
def test_association_assignment_sticks
post = Post.first
- author1, author2 = Author.all.merge!(:limit => 2).to_a
+ author1, author2 = Author.all.merge!(limit: 2).to_a
assert_not_nil author1
assert_not_nil author2
@@ -697,10 +932,10 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
def test_cant_save_readonly_association
assert_raise(ActiveRecord::ReadOnlyRecord) { companies(:first_client).readonly_firm.save! }
- assert companies(:first_client).readonly_firm.readonly?
+ assert_predicate companies(:first_client).readonly_firm, :readonly?
end
- def test_test_polymorphic_assignment_foreign_key_type_string
+ def test_polymorphic_assignment_foreign_key_type_string
comment = Comment.first
comment.author = Author.first
comment.resource = Member.first
@@ -730,7 +965,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
def test_polymorphic_assignment_with_primary_key_foreign_type_field_updating
# should update when assigning a saved record
essay = Essay.new
- writer = Author.create(:name => "David")
+ writer = Author.create(name: "David")
essay.writer = writer
assert_equal "Author", essay.writer_type
@@ -755,7 +990,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
def test_assignment_updates_foreign_id_field_for_new_and_saved_records
client = Client.new
- saved_firm = Firm.create :name => "Saved"
+ saved_firm = Firm.create name: "Saved"
new_firm = Firm.new
client.firm = saved_firm
@@ -767,7 +1002,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
def test_polymorphic_assignment_with_primary_key_updates_foreign_id_field_for_new_and_saved_records
essay = Essay.new
- saved_writer = Author.create(:name => "David")
+ saved_writer = Author.create(name: "David")
new_writer = Author.new
essay.writer = saved_writer
@@ -783,7 +1018,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_nil essay.writer_type
essay.writer_id = 1
- essay.writer_type = 'Author'
+ essay.writer_type = "Author"
essay.writer = nil
assert_nil essay.writer_id
@@ -805,14 +1040,14 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_nothing_raised do
Account.find(@account.id).save!
- Account.all.merge!(:includes => :firm).find(@account.id).save!
+ Account.all.merge!(includes: :firm).find(@account.id).save!
end
@account.firm.delete
assert_nothing_raised do
Account.find(@account.id).save!
- Account.all.merge!(:includes => :firm).find(@account.id).save!
+ Account.all.merge!(includes: :firm).find(@account.id).save!
end
end
@@ -833,18 +1068,42 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
def test_belongs_to_invalid_dependent_option_raises_exception
error = assert_raise ArgumentError do
- Class.new(Author).belongs_to :special_author_address, :dependent => :nullify
+ Class.new(Author).belongs_to :special_author_address, dependent: :nullify
+ end
+ assert_equal error.message, "The :dependent option must be one of [:destroy, :delete], but is :nullify"
+ end
+
+ class DestroyableBook < ActiveRecord::Base
+ self.table_name = "books"
+ belongs_to :author, class_name: "UndestroyableAuthor", dependent: :destroy
+ end
+
+ class UndestroyableAuthor < ActiveRecord::Base
+ self.table_name = "authors"
+ has_one :book, class_name: "DestroyableBook", foreign_key: "author_id"
+ before_destroy :dont
+
+ def dont
+ throw(:abort)
+ end
+ end
+
+ def test_dependency_should_halt_parent_destruction
+ author = UndestroyableAuthor.create!(name: "Test")
+ book = DestroyableBook.create!(author: author)
+
+ assert_no_difference ["UndestroyableAuthor.count", "DestroyableBook.count"] do
+ assert_not book.destroy
end
- assert_equal error.message, 'The :dependent option must be one of [:destroy, :delete], but is :nullify'
end
def test_attributes_are_being_set_when_initialized_from_belongs_to_association_with_where_clause
- new_firm = accounts(:signals37).build_firm(:name => 'Apple')
+ new_firm = accounts(:signals37).build_firm(name: "Apple")
assert_equal new_firm.name, "Apple"
end
def test_attributes_are_set_without_error_when_initialized_from_belongs_to_association_with_array_in_where_clause
- new_account = Account.where(:credit_limit => [ 50, 60 ]).new
+ new_account = Account.where(credit_limit: [ 50, 60 ]).new
assert_nil new_account.credit_limit
end
@@ -856,15 +1115,15 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
firm_proxy = client.send(:association_instance_get, :firm)
firm_with_condition_proxy = client.send(:association_instance_get, :firm_with_condition)
- assert !firm_proxy.stale_target?
- assert !firm_with_condition_proxy.stale_target?
+ assert_not_predicate firm_proxy, :stale_target?
+ assert_not_predicate firm_with_condition_proxy, :stale_target?
assert_equal companies(:first_firm), client.firm
assert_equal companies(:first_firm), client.firm_with_condition
client.client_of = companies(:another_firm).id
- assert firm_proxy.stale_target?
- assert firm_with_condition_proxy.stale_target?
+ assert_predicate firm_proxy, :stale_target?
+ assert_predicate firm_with_condition_proxy, :stale_target?
assert_equal companies(:another_firm), client.firm
assert_equal companies(:another_firm), client.firm_with_condition
end
@@ -875,12 +1134,12 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
sponsor.sponsorable
proxy = sponsor.send(:association_instance_get, :sponsorable)
- assert !proxy.stale_target?
+ assert_not_predicate proxy, :stale_target?
assert_equal members(:groucho), sponsor.sponsorable
sponsor.sponsorable_id = members(:some_other_guy).id
- assert proxy.stale_target?
+ assert_predicate proxy, :stale_target?
assert_equal members(:some_other_guy), sponsor.sponsorable
end
@@ -890,12 +1149,12 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
sponsor.sponsorable
proxy = sponsor.send(:association_instance_get, :sponsorable)
- assert !proxy.stale_target?
+ assert_not_predicate proxy, :stale_target?
assert_equal members(:groucho), sponsor.sponsorable
- sponsor.sponsorable_type = 'Firm'
+ sponsor.sponsorable_type = "Firm"
- assert proxy.stale_target?
+ assert_predicate proxy, :stale_target?
assert_equal companies(:first_firm), sponsor.sponsorable
end
@@ -917,9 +1176,20 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
post = posts(:welcome)
comment = comments(:greetings)
- assert_difference lambda { post.reload.tags_count }, -1 do
- assert_difference 'comment.reload.tags_count', +1 do
+ assert_equal post.id, comment.id
+
+ assert_difference "post.reload.tags_count", -1 do
+ assert_difference "comment.reload.tags_count", +1 do
tagging.taggable = comment
+ tagging.save!
+ end
+ end
+
+ assert_difference "comment.reload.tags_count", -1 do
+ assert_difference "post.reload.tags_count", +1 do
+ tagging.taggable_type = post.class.polymorphic_name
+ tagging.taggable_id = post.id
+ tagging.save!
end
end
end
@@ -965,46 +1235,46 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
def test_build_with_block
- client = Client.create(:name => 'Client Company')
+ client = Client.create(name: "Client Company")
- firm = client.build_firm{ |f| f.name = 'Agency Company' }
- assert_equal 'Agency Company', firm.name
+ firm = client.build_firm { |f| f.name = "Agency Company" }
+ assert_equal "Agency Company", firm.name
end
def test_create_with_block
- client = Client.create(:name => 'Client Company')
+ client = Client.create(name: "Client Company")
- firm = client.create_firm{ |f| f.name = 'Agency Company' }
- assert_equal 'Agency Company', firm.name
+ firm = client.create_firm { |f| f.name = "Agency Company" }
+ assert_equal "Agency Company", firm.name
end
def test_create_bang_with_block
- client = Client.create(:name => 'Client Company')
+ client = Client.create(name: "Client Company")
- firm = client.create_firm!{ |f| f.name = 'Agency Company' }
- assert_equal 'Agency Company', firm.name
+ firm = client.create_firm! { |f| f.name = "Agency Company" }
+ assert_equal "Agency Company", firm.name
end
def test_should_set_foreign_key_on_create_association
- client = Client.create! :name => "fuu"
+ client = Client.create! name: "fuu"
- firm = client.create_firm :name => "baa"
+ firm = client.create_firm name: "baa"
assert_equal firm.id, client.client_of
end
def test_should_set_foreign_key_on_create_association!
- client = Client.create! :name => "fuu"
+ client = Client.create! name: "fuu"
- firm = client.create_firm! :name => "baa"
+ firm = client.create_firm! name: "baa"
assert_equal firm.id, client.client_of
end
def test_self_referential_belongs_to_with_counter_cache_assigning_nil
- comment = Comment.create! :post => posts(:thinking), :body => "fuu"
+ comment = Comment.create! post: posts(:thinking), body: "fuu"
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
@@ -1019,9 +1289,23 @@ 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_predicate 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)
+ sponsor = Sponsor.create!(sponsorable: toy)
assert_equal toy, sponsor.reload.sponsorable
end
@@ -1035,12 +1319,12 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
citibank.firm_id = apple.id.to_s
- assert !citibank.association(:firm).stale_target?
+ assert_not_predicate citibank.association(:firm), :stale_target?
end
def test_reflect_the_most_recent_change
author1, author2 = Author.limit(2)
- post = Post.new(:title => "foo", :body=> "bar")
+ post = Post.new(title: "foo", body: "bar")
post.author = author1
post.author_id = author2.id
@@ -1049,8 +1333,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal post.author_id, author2.id
end
- test 'dangerous association name raises ArgumentError' do
- [:errors, 'errors', :save, 'save'].each do |name|
+ test "dangerous association name raises ArgumentError" do
+ [:errors, "errors", :save, "save"].each do |name|
assert_raises(ArgumentError, "Association #{name} should not be allowed") do
Class.new(ActiveRecord::Base) do
belongs_to name
@@ -1059,11 +1343,22 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
end
- test 'belongs_to works with model called Record' do
+ test "belongs_to works with model called Record" do
record = Record.create!
Column.create! record: record
assert_equal 1, Column.count
end
+
+ def test_multiple_counter_cache_with_after_create_update
+ post = posts(:welcome)
+ parent = comments(:greetings)
+
+ assert_difference "parent.reload.children_count", +1 do
+ assert_difference "post.reload.comments_count", +1 do
+ CommentWithAfterCreateUpdate.create(body: "foo", post: post, parent: parent)
+ end
+ end
+ end
end
class BelongsToWithForeignKeyTest < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/associations/bidirectional_destroy_dependencies_test.rb b/activerecord/test/cases/associations/bidirectional_destroy_dependencies_test.rb
new file mode 100644
index 0000000000..88221b012e
--- /dev/null
+++ b/activerecord/test/cases/associations/bidirectional_destroy_dependencies_test.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/content"
+
+class BidirectionalDestroyDependenciesTest < ActiveRecord::TestCase
+ fixtures :content, :content_positions
+
+ def setup
+ Content.destroyed_ids.clear
+ ContentPosition.destroyed_ids.clear
+ end
+
+ def test_bidirectional_dependence_when_destroying_item_with_belongs_to_association
+ content_position = ContentPosition.find(1)
+ content = content_position.content
+ assert_not_nil content
+
+ content_position.destroy
+
+ assert_equal [content_position.id], ContentPosition.destroyed_ids
+ assert_equal [content.id], Content.destroyed_ids
+ end
+
+ def test_bidirectional_dependence_when_destroying_item_with_has_one_association
+ content = Content.find(1)
+ content_position = content.content_position
+ assert_not_nil content_position
+
+ content.destroy
+
+ assert_equal [content.id], Content.destroyed_ids
+ assert_equal [content_position.id], ContentPosition.destroyed_ids
+ end
+
+ def test_bidirectional_dependence_when_destroying_item_with_has_one_association_fails_first_time
+ content = ContentWhichRequiresTwoDestroyCalls.find(1)
+
+ 2.times { content.destroy }
+
+ assert_equal content.destroyed?, true
+ end
+end
diff --git a/activerecord/test/cases/associations/callbacks_test.rb b/activerecord/test/cases/associations/callbacks_test.rb
index a531e0e02c..25d55dc4c9 100644
--- a/activerecord/test/cases/associations/callbacks_test.rb
+++ b/activerecord/test/cases/associations/callbacks_test.rb
@@ -1,19 +1,21 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/post'
-require 'models/author'
-require 'models/project'
-require 'models/developer'
-require 'models/computer'
-require 'models/company'
+require "models/post"
+require "models/author"
+require "models/project"
+require "models/developer"
+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)
@thinking = posts(:thinking)
@authorless = posts(:authorless)
- assert @david.post_log.empty?
+ assert_empty @david.post_log
end
def test_adding_macro_callbacks
@@ -61,20 +63,20 @@ class AssociationCallbacksTest < ActiveRecord::TestCase
end
def test_has_many_callbacks_with_create
- morten = Author.create :name => "Morten"
- post = morten.posts_with_proc_callbacks.create! :title => "Hello", :body => "How are you doing?"
+ morten = Author.create name: "Morten"
+ post = morten.posts_with_proc_callbacks.create! title: "Hello", body: "How are you doing?"
assert_equal ["before_adding<new>", "after_adding#{post.id}"], morten.post_log
end
def test_has_many_callbacks_with_create!
- morten = Author.create! :name => "Morten"
- post = morten.posts_with_proc_callbacks.create :title => "Hello", :body => "How are you doing?"
+ morten = Author.create! name: "Morten"
+ post = morten.posts_with_proc_callbacks.create title: "Hello", body: "How are you doing?"
assert_equal ["before_adding<new>", "after_adding#{post.id}"], morten.post_log
end
def test_has_many_callbacks_for_save_on_parent
- jack = Author.new :name => "Jack"
- jack.posts_with_callbacks.build :title => "Call me back!", :body => "Before you wake up and after you sleep"
+ jack = Author.new name: "Jack"
+ jack.posts_with_callbacks.build title: "Call me back!", body: "Before you wake up and after you sleep"
callback_log = ["before_adding<new>", "after_adding#{jack.posts_with_callbacks.first.id}"]
assert_equal callback_log, jack.post_log
@@ -84,8 +86,8 @@ class AssociationCallbacksTest < ActiveRecord::TestCase
end
def test_has_many_callbacks_for_destroy_on_parent
- firm = Firm.create! :name => "Firm"
- client = firm.clients.create! :name => "Client"
+ firm = Firm.create! name: "Firm"
+ client = firm.clients.create! name: "Client"
firm.destroy
assert_equal ["before_remove#{client.id}", "after_remove#{client.id}"], firm.log
@@ -94,7 +96,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase
def test_has_and_belongs_to_many_add_callback
david = developers(:david)
ar = projects(:active_record)
- assert ar.developers_log.empty?
+ assert_empty ar.developers_log
ar.developers_with_callbacks << david
assert_equal ["before_adding#{david.id}", "after_adding#{david.id}"], ar.developers_log
ar.developers_with_callbacks << david
@@ -108,41 +110,40 @@ class AssociationCallbacksTest < ActiveRecord::TestCase
klass = Class.new(Project) do
def self.name; Project.name; end
has_and_belongs_to_many :developers_with_callbacks,
- :class_name => "Developer",
- :before_add => lambda { |o,r|
+ class_name: "Developer",
+ before_add: lambda { |o, r|
dev = r
new_dev = r.new_record?
}
end
rec = klass.create!
- alice = Developer.new(:name => 'alice')
+ alice = Developer.new(name: "alice")
rec.developers_with_callbacks << alice
assert_equal alice, dev
assert_not_nil new_dev
assert new_dev, "record should not have been saved"
- assert_not alice.new_record?
+ assert_not_predicate alice, :new_record?
end
def test_has_and_belongs_to_many_after_add_called_after_save
ar = projects(:active_record)
- assert ar.developers_log.empty?
- alice = Developer.new(:name => 'alice')
+ assert_empty ar.developers_log
+ alice = Developer.new(name: "alice")
ar.developers_with_callbacks << alice
- assert_equal"after_adding#{alice.id}", ar.developers_log.last
+ assert_equal "after_adding#{alice.id}", ar.developers_log.last
- bob = ar.developers_with_callbacks.create(:name => 'bob')
+ bob = ar.developers_with_callbacks.create(name: "bob")
assert_equal "after_adding#{bob.id}", ar.developers_log.last
- ar.developers_with_callbacks.build(:name => 'charlie')
+ ar.developers_with_callbacks.build(name: "charlie")
assert_equal "after_adding<new>", ar.developers_log.last
end
-
def test_has_and_belongs_to_many_remove_callback
david = developers(:david)
jamis = developers(:jamis)
activerecord = projects(:active_record)
- assert activerecord.developers_log.empty?
+ assert_empty activerecord.developers_log
activerecord.developers_with_callbacks.delete(david)
assert_equal ["before_removing#{david.id}", "after_removing#{david.id}"], activerecord.developers_log
@@ -153,21 +154,21 @@ class AssociationCallbacksTest < ActiveRecord::TestCase
def test_has_and_belongs_to_many_does_not_fire_callbacks_on_clear
activerecord = projects(:active_record)
- assert activerecord.developers_log.empty?
+ assert_empty activerecord.developers_log
if activerecord.developers_with_callbacks.size == 0
activerecord.developers << developers(:david)
activerecord.developers << developers(:jamis)
activerecord.reload
assert activerecord.developers_with_callbacks.size == 2
end
- activerecord.developers_with_callbacks.flat_map {|d| ["before_removing#{d.id}","after_removing#{d.id}"]}.sort
+ activerecord.developers_with_callbacks.flat_map { |d| ["before_removing#{d.id}", "after_removing#{d.id}"] }.sort
assert activerecord.developers_with_callbacks.clear
- assert_predicate activerecord.developers_log, :empty?
+ assert_empty activerecord.developers_log
end
def test_has_many_and_belongs_to_many_callbacks_for_save_on_parent
- project = Project.new :name => "Callbacks"
- project.developers_with_callbacks.build :name => "Jack", :salary => 95000
+ project = Project.new name: "Callbacks"
+ project.developers_with_callbacks.build name: "Jack", salary: 95000
callback_log = ["before_adding<new>", "after_adding<new>"]
assert_equal callback_log, project.developers_log
@@ -177,14 +178,14 @@ class AssociationCallbacksTest < ActiveRecord::TestCase
end
def test_dont_add_if_before_callback_raises_exception
- assert !@david.unchangable_posts.include?(@authorless)
+ assert_not_includes @david.unchangeable_posts, @authorless
begin
- @david.unchangable_posts << @authorless
+ @david.unchangeable_posts << @authorless
rescue Exception
end
- assert @david.post_log.empty?
- assert !@david.unchangable_posts.include?(@authorless)
+ assert_empty @david.post_log
+ assert_not_includes @david.unchangeable_posts, @authorless
@david.reload
- assert !@david.unchangable_posts.include?(@authorless)
+ assert_not_includes @david.unchangeable_posts, @authorless
end
end
diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
index 51d8e0523e..a9e22c7643 100644
--- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
+++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
@@ -1,56 +1,52 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/post'
-require 'models/comment'
-require 'models/author'
-require 'models/categorization'
-require 'models/category'
-require 'models/company'
-require 'models/topic'
-require 'models/reply'
-require 'models/person'
-require 'models/vertex'
-require 'models/edge'
+require "models/post"
+require "models/comment"
+require "models/author"
+require "models/categorization"
+require "models/category"
+require "models/company"
+require "models/topic"
+require "models/reply"
+require "models/person"
+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
- authors = Author.all.merge!(:includes=>{:posts=>:comments}, :order=>"authors.id").to_a
+ authors = Author.all.merge!(includes: { posts: :comments }, order: "authors.id").to_a
assert_equal 3, authors.size
assert_equal 5, authors[0].posts.size
assert_equal 3, authors[1].posts.size
- assert_equal 10, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i}
+ assert_equal 10, authors[0].posts.collect { |post| post.comments.size }.inject(0) { |sum, i| sum + i }
end
def test_eager_association_loading_with_cascaded_two_levels_and_one_level
- authors = Author.all.merge!(:includes=>[{:posts=>:comments}, :categorizations], :order=>"authors.id").to_a
+ authors = Author.all.merge!(includes: [{ posts: :comments }, :categorizations], order: "authors.id").to_a
assert_equal 3, authors.size
assert_equal 5, authors[0].posts.size
assert_equal 3, authors[1].posts.size
- assert_equal 10, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i}
+ assert_equal 10, authors[0].posts.collect { |post| post.comments.size }.inject(0) { |sum, i| sum + i }
assert_equal 1, authors[0].categorizations.size
assert_equal 2, authors[1].categorizations.size
end
def test_eager_association_loading_with_hmt_does_not_table_name_collide_when_joining_associations
- assert_nothing_raised do
- Author.joins(:posts).eager_load(:comments).where(:posts => {:tags_count => 1}).to_a
- end
- authors = Author.joins(:posts).eager_load(:comments).where(:posts => {:tags_count => 1}).to_a
- assert_equal 1, assert_no_queries { authors.size }
+ authors = Author.joins(:posts).eager_load(:comments).where(posts: { tags_count: 1 }).to_a
+ assert_equal 3, assert_no_queries { authors.size }
assert_equal 10, assert_no_queries { authors[0].comments.size }
end
def test_eager_association_loading_grafts_stashed_associations_to_correct_parent
- assert_nothing_raised do
- Person.eager_load(:primary_contact => :primary_contact).where('primary_contacts_people_2.first_name = ?', 'Susan').order('people.id').to_a
- end
- assert_equal people(:michael), Person.eager_load(:primary_contact => :primary_contact).where('primary_contacts_people_2.first_name = ?', 'Susan').order('people.id').first
+ assert_equal people(:michael), Person.eager_load(primary_contact: :primary_contact).where("primary_contacts_people_2.first_name = ?", "Susan").order("people.id").first
end
def test_cascaded_eager_association_loading_with_join_for_count
- categories = Category.joins(:categorizations).includes([{:posts=>:comments}, :authors])
+ categories = Category.joins(:categorizations).includes([{ posts: :comments }, :authors])
assert_equal 4, categories.count
assert_equal 4, categories.to_a.count
@@ -59,7 +55,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
end
def test_cascaded_eager_association_loading_with_duplicated_includes
- categories = Category.includes(:categorizations).includes(:categorizations => :author).where("categorizations.id is not null").references(:categorizations)
+ categories = Category.includes(:categorizations).includes(categorizations: :author).where("categorizations.id is not null").references(:categorizations)
assert_nothing_raised do
assert_equal 3, categories.count
assert_equal 3, categories.to_a.size
@@ -67,7 +63,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
end
def test_cascaded_eager_association_loading_with_twice_includes_edge_cases
- categories = Category.includes(:categorizations => :author).includes(:categorizations => :post).where("posts.id is not null").references(:posts)
+ categories = Category.includes(categorizations: :author).includes(categorizations: :post).where("posts.id is not null").references(:posts)
assert_nothing_raised do
assert_equal 3, categories.count
assert_equal 3, categories.to_a.size
@@ -82,29 +78,29 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
end
def test_eager_association_loading_with_cascaded_two_levels_with_two_has_many_associations
- authors = Author.all.merge!(:includes=>{:posts=>[:comments, :categorizations]}, :order=>"authors.id").to_a
+ authors = Author.all.merge!(includes: { posts: [:comments, :categorizations] }, order: "authors.id").to_a
assert_equal 3, authors.size
assert_equal 5, authors[0].posts.size
assert_equal 3, authors[1].posts.size
- assert_equal 10, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i}
+ assert_equal 10, authors[0].posts.collect { |post| post.comments.size }.inject(0) { |sum, i| sum + i }
end
def test_eager_association_loading_with_cascaded_two_levels_and_self_table_reference
- authors = Author.all.merge!(:includes=>{:posts=>[:comments, :author]}, :order=>"authors.id").to_a
+ authors = Author.all.merge!(includes: { posts: [:comments, :author] }, order: "authors.id").to_a
assert_equal 3, authors.size
assert_equal 5, authors[0].posts.size
assert_equal authors(:david).name, authors[0].name
- assert_equal [authors(:david).name], authors[0].posts.collect{|post| post.author.name}.uniq
+ assert_equal [authors(:david).name], authors[0].posts.collect { |post| post.author.name }.uniq
end
def test_eager_association_loading_with_cascaded_two_levels_with_condition
- authors = Author.all.merge!(:includes=>{:posts=>:comments}, :where=>"authors.id=1", :order=>"authors.id").to_a
+ authors = Author.all.merge!(includes: { posts: :comments }, where: "authors.id=1", order: "authors.id").to_a
assert_equal 1, authors.size
assert_equal 5, authors[0].posts.size
end
def test_eager_association_loading_with_cascaded_three_levels_by_ping_pong
- firms = Firm.all.merge!(:includes=>{:account=>{:firm=>:account}}, :order=>"companies.id").to_a
+ firms = Firm.all.merge!(includes: { account: { firm: :account } }, order: "companies.id").to_a
assert_equal 2, firms.size
assert_equal firms.first.account, firms.first.account.firm.account
assert_equal companies(:first_firm).account, assert_no_queries { firms.first.account.firm.account }
@@ -112,7 +108,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
end
def test_eager_association_loading_with_has_many_sti
- topics = Topic.all.merge!(:includes => :replies, :order => 'topics.id').to_a
+ topics = Topic.all.merge!(includes: :replies, order: "topics.id").to_a
first, second, = topics(:first).replies.size, topics(:second).replies.size
assert_no_queries do
assert_equal first, topics[0].replies.size
@@ -121,11 +117,10 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
end
def test_eager_association_loading_with_has_many_sti_and_subclasses
- silly = SillyReply.new(:title => "gaga", :content => "boo-boo", :parent_id => 1)
- silly.parent_id = 1
- assert silly.save
+ reply = Reply.new(title: "gaga", content: "boo-boo", parent_id: 1)
+ assert reply.save
- topics = Topic.all.merge!(:includes => :replies, :order => ['topics.id', 'replies_topics.id']).to_a
+ topics = Topic.all.merge!(includes: :replies, order: ["topics.id", "replies_topics.id"]).to_a
assert_no_queries do
assert_equal 2, topics[0].replies.size
assert_equal 0, topics[1].replies.size
@@ -133,14 +128,14 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
end
def test_eager_association_loading_with_belongs_to_sti
- replies = Reply.all.merge!(:includes => :topic, :order => 'topics.id').to_a
- assert replies.include?(topics(:second))
- assert !replies.include?(topics(:first))
+ replies = Reply.all.merge!(includes: :topic, order: "topics.id").to_a
+ assert_includes replies, topics(:second)
+ assert_not_includes replies, topics(:first)
assert_equal topics(:first), assert_no_queries { replies.first.topic }
end
def test_eager_association_loading_with_multiple_stis_and_order
- author = Author.all.merge!(:includes => { :posts => [ :special_comments , :very_special_comment ] }, :order => ['authors.name', 'comments.body', 'very_special_comments_posts.body'], :where => 'posts.id = 4').first
+ author = Author.all.merge!(includes: { posts: [ :special_comments, :very_special_comment ] }, order: ["authors.name", "comments.body", "very_special_comments_posts.body"], where: "posts.id = 4").first
assert_equal authors(:david), author
assert_no_queries do
author.posts.first.special_comments
@@ -149,7 +144,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
end
def test_eager_association_loading_of_stis_with_multiple_references
- authors = Author.all.merge!(:includes => { :posts => { :special_comments => { :post => [ :special_comments, :very_special_comment ] } } }, :order => 'comments.body, very_special_comments_posts.body', :where => 'posts.id = 4').to_a
+ authors = Author.all.merge!(includes: { posts: { special_comments: { post: [ :special_comments, :very_special_comment ] } } }, order: "comments.body, very_special_comments_posts.body", where: "posts.id = 4").to_a
assert_equal [authors(:david)], authors
assert_no_queries do
authors.first.posts.first.special_comments.first.post.special_comments
@@ -158,20 +153,30 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
end
def test_eager_association_loading_where_first_level_returns_nil
- authors = Author.all.merge!(:includes => {:post_about_thinking => :comments}, :order => 'authors.id DESC').to_a
+ authors = Author.all.merge!(includes: { post_about_thinking: :comments }, order: "authors.id DESC").to_a
assert_equal [authors(:bob), authors(:mary), authors(:david)], authors
assert_no_queries do
authors[2].post_about_thinking.comments.first
end
end
+ def test_preload_through_missing_records
+ post = Post.where.not(author_id: Author.select(:id)).preload(author: { comments: :post }).first!
+ assert_no_queries { assert_nil post.author }
+ end
+
+ def test_eager_association_loading_with_missing_first_record
+ posts = Post.where(id: 3).preload(author: { comments: :post }).to_a
+ assert_equal posts.size, 1
+ end
+
def test_eager_association_loading_with_recursive_cascading_four_levels_has_many_through
- source = Vertex.all.merge!(:includes=>{:sinks=>{:sinks=>{:sinks=>:sinks}}}, :order => 'vertices.id').first
+ source = Vertex.all.merge!(includes: { sinks: { sinks: { sinks: :sinks } } }, order: "vertices.id").first
assert_equal vertices(:vertex_4), assert_no_queries { source.sinks.first.sinks.first.sinks.first }
end
def test_eager_association_loading_with_recursive_cascading_four_levels_has_and_belongs_to_many
- sink = Vertex.all.merge!(:includes=>{:sources=>{:sources=>{:sources=>:sources}}}, :order => 'vertices.id DESC').first
+ sink = Vertex.all.merge!(includes: { sources: { sources: { sources: :sources } } }, order: "vertices.id DESC").first
assert_equal vertices(:vertex_1), assert_no_queries { sink.sources.first.sources.first.sources.first.sources.first }
end
@@ -183,6 +188,6 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
assert_equal 1, authors[1].comments.size
assert_equal 5, authors[0].posts.size
assert_equal 3, authors[1].posts.size
- assert_equal 3, authors[0].posts.collect { |post| post.categorizations.size }.inject(0) { |sum, i| sum+i }
+ assert_equal 3, authors[0].posts.collect { |post| post.categorizations.size }.inject(0) { |sum, i| sum + i }
end
end
diff --git a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb
index 75a6295350..5fca972aee 100644
--- a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb
+++ b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb
@@ -1,36 +1,88 @@
-require 'cases/helper'
-require 'models/post'
-require 'models/tagging'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/post"
+require "models/tagging"
module Namespaced
class Post < ActiveRecord::Base
- self.table_name = 'posts'
- has_one :tagging, :as => :taggable, :class_name => 'Tagging'
+ self.table_name = "posts"
+ has_one :tagging, as: :taggable, class_name: "Tagging"
+
+ def self.polymorphic_name
+ sti_name
+ end
end
end
-class EagerLoadIncludeFullStiClassNamesTest < ActiveRecord::TestCase
-
+module PolymorphicFullStiClassNamesSharedTest
def setup
- generate_test_objects
+ @old_store_full_sti_class = ActiveRecord::Base.store_full_sti_class
+ ActiveRecord::Base.store_full_sti_class = store_full_sti_class
+
+ post = Namespaced::Post.create(title: "Great stuff", body: "This is not", author_id: 1)
+ @tagging = Tagging.create(taggable: post)
end
- def generate_test_objects
- post = Namespaced::Post.create( :title => 'Great stuff', :body => 'This is not', :author_id => 1 )
- Tagging.create( :taggable => post )
+ def teardown
+ ActiveRecord::Base.store_full_sti_class = @old_store_full_sti_class
end
def test_class_names
- old = ActiveRecord::Base.store_full_sti_class
+ ActiveRecord::Base.store_full_sti_class = !store_full_sti_class
+ post = Namespaced::Post.find_by_title("Great stuff")
+ assert_nil post.tagging
+
+ ActiveRecord::Base.store_full_sti_class = store_full_sti_class
+ post = Namespaced::Post.find_by_title("Great stuff")
+ assert_equal @tagging, post.tagging
+ end
+
+ def test_class_names_with_includes
+ ActiveRecord::Base.store_full_sti_class = !store_full_sti_class
+ post = Namespaced::Post.includes(:tagging).find_by_title("Great stuff")
+ assert_nil post.tagging
+
+ ActiveRecord::Base.store_full_sti_class = store_full_sti_class
+ post = Namespaced::Post.includes(:tagging).find_by_title("Great stuff")
+ assert_equal @tagging, post.tagging
+ end
- ActiveRecord::Base.store_full_sti_class = false
- post = Namespaced::Post.includes(:tagging).find_by_title('Great stuff')
+ def test_class_names_with_eager_load
+ ActiveRecord::Base.store_full_sti_class = !store_full_sti_class
+ post = Namespaced::Post.eager_load(:tagging).find_by_title("Great stuff")
assert_nil post.tagging
- ActiveRecord::Base.store_full_sti_class = true
- post = Namespaced::Post.includes(:tagging).find_by_title('Great stuff')
- assert_instance_of Tagging, post.tagging
- ensure
- ActiveRecord::Base.store_full_sti_class = old
+ ActiveRecord::Base.store_full_sti_class = store_full_sti_class
+ post = Namespaced::Post.eager_load(:tagging).find_by_title("Great stuff")
+ assert_equal @tagging, post.tagging
end
+
+ def test_class_names_with_find_by
+ post = Namespaced::Post.find_by_title("Great stuff")
+
+ ActiveRecord::Base.store_full_sti_class = !store_full_sti_class
+ assert_nil Tagging.find_by(taggable: post)
+
+ ActiveRecord::Base.store_full_sti_class = store_full_sti_class
+ assert_equal @tagging, Tagging.find_by(taggable: post)
+ end
+end
+
+class PolymorphicFullStiClassNamesTest < ActiveRecord::TestCase
+ include PolymorphicFullStiClassNamesSharedTest
+
+ private
+ def store_full_sti_class
+ true
+ end
+end
+
+class PolymorphicNonFullStiClassNamesTest < ActiveRecord::TestCase
+ include PolymorphicFullStiClassNamesSharedTest
+
+ private
+ def store_full_sti_class
+ false
+ end
end
diff --git a/activerecord/test/cases/associations/eager_load_nested_include_test.rb b/activerecord/test/cases/associations/eager_load_nested_include_test.rb
index f571198079..525ad3197a 100644
--- a/activerecord/test/cases/associations/eager_load_nested_include_test.rb
+++ b/activerecord/test/cases/associations/eager_load_nested_include_test.rb
@@ -1,18 +1,20 @@
-require 'cases/helper'
-require 'models/post'
-require 'models/tag'
-require 'models/author'
-require 'models/comment'
-require 'models/category'
-require 'models/categorization'
-require 'models/tagging'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/post"
+require "models/tag"
+require "models/author"
+require "models/comment"
+require "models/category"
+require "models/categorization"
+require "models/tagging"
module Remembered
extend ActiveSupport::Concern
included do
after_create :remember
- protected
+ private
def remember; self.class.remembered << self; end
end
@@ -23,30 +25,30 @@ module Remembered
end
class ShapeExpression < ActiveRecord::Base
- belongs_to :shape, :polymorphic => true
- belongs_to :paint, :polymorphic => true
+ belongs_to :shape, polymorphic: true
+ belongs_to :paint, polymorphic: true
end
class Circle < ActiveRecord::Base
- has_many :shape_expressions, :as => :shape
+ has_many :shape_expressions, as: :shape
include Remembered
end
class Square < ActiveRecord::Base
- has_many :shape_expressions, :as => :shape
+ has_many :shape_expressions, as: :shape
include Remembered
end
class Triangle < ActiveRecord::Base
- has_many :shape_expressions, :as => :shape
+ has_many :shape_expressions, as: :shape
include Remembered
end
-class PaintColor < ActiveRecord::Base
- has_many :shape_expressions, :as => :paint
- belongs_to :non_poly, :foreign_key => "non_poly_one_id", :class_name => "NonPolyOne"
+class PaintColor < ActiveRecord::Base
+ has_many :shape_expressions, as: :paint
+ belongs_to :non_poly, foreign_key: "non_poly_one_id", class_name: "NonPolyOne"
include Remembered
end
class PaintTexture < ActiveRecord::Base
- has_many :shape_expressions, :as => :paint
- belongs_to :non_poly, :foreign_key => "non_poly_two_id", :class_name => "NonPolyTwo"
+ has_many :shape_expressions, as: :paint
+ belongs_to :non_poly, foreign_key: "non_poly_two_id", class_name: "NonPolyTwo"
include Remembered
end
class NonPolyOne < ActiveRecord::Base
@@ -58,8 +60,6 @@ class NonPolyTwo < ActiveRecord::Base
include Remembered
end
-
-
class EagerLoadPolyAssocsTest < ActiveRecord::TestCase
NUM_SIMPLE_OBJS = 50
NUM_SHAPE_EXPRESSIONS = 100
@@ -78,21 +78,21 @@ class EagerLoadPolyAssocsTest < ActiveRecord::TestCase
[Circle, Square, Triangle, NonPolyOne, NonPolyTwo].map(&:create!)
end
1.upto(NUM_SIMPLE_OBJS) do
- PaintColor.create!(:non_poly_one_id => NonPolyOne.sample.id)
- PaintTexture.create!(:non_poly_two_id => NonPolyTwo.sample.id)
+ PaintColor.create!(non_poly_one_id: NonPolyOne.sample.id)
+ PaintTexture.create!(non_poly_two_id: NonPolyTwo.sample.id)
end
1.upto(NUM_SHAPE_EXPRESSIONS) do
shape_type = [Circle, Square, Triangle].sample
paint_type = [PaintColor, PaintTexture].sample
- ShapeExpression.create!(:shape_type => shape_type.to_s, :shape_id => shape_type.sample.id,
- :paint_type => paint_type.to_s, :paint_id => paint_type.sample.id)
+ ShapeExpression.create!(shape_type: shape_type.to_s, shape_id: shape_type.sample.id,
+ paint_type: paint_type.to_s, paint_id: paint_type.sample.id)
end
end
def test_include_query
- res = ShapeExpression.all.merge!(:includes => [ :shape, { :paint => :non_poly } ]).to_a
+ res = ShapeExpression.all.merge!(includes: [ :shape, { paint: :non_poly } ]).to_a
assert_equal NUM_SHAPE_EXPRESSIONS, res.size
- assert_queries(0) do
+ assert_no_queries do
res.each do |se|
assert_not_nil se.paint.non_poly, "this is the association that was loading incorrectly before the change"
assert_not_nil se.shape, "just making sure other associations still work"
@@ -103,10 +103,10 @@ end
class EagerLoadNestedIncludeWithMissingDataTest < ActiveRecord::TestCase
def setup
- @davey_mcdave = Author.create(:name => 'Davey McDave')
- @first_post = @davey_mcdave.posts.create(:title => 'Davey Speaks', :body => 'Expressive wordage')
- @first_comment = @first_post.comments.create(:body => 'Inflamatory doublespeak')
- @first_categorization = @davey_mcdave.categorizations.create(:category => Category.first, :post => @first_post)
+ @davey_mcdave = Author.create(name: "Davey McDave")
+ @first_post = @davey_mcdave.posts.create(title: "Davey Speaks", body: "Expressive wordage")
+ @first_comment = @first_post.comments.create(body: "Inflamatory doublespeak")
+ @first_categorization = @davey_mcdave.categorizations.create(category: Category.first, post: @first_post)
end
teardown do
@@ -119,8 +119,8 @@ class EagerLoadNestedIncludeWithMissingDataTest < ActiveRecord::TestCase
def test_missing_data_in_a_nested_include_should_not_cause_errors_when_constructing_objects
assert_nothing_raised do
# @davey_mcdave doesn't have any author_favorites
- includes = {:posts => :comments, :categorizations => :category, :author_favorites => :favorite_author }
- Author.all.merge!(:includes => includes, :where => {:authors => {:name => @davey_mcdave.name}}, :order => 'categories.name').to_a
+ includes = { posts: :comments, categorizations: :category, author_favorites: :favorite_author }
+ Author.all.merge!(includes: includes, where: { authors: { name: @davey_mcdave.name } }, order: "categories.name").to_a
end
end
end
diff --git a/activerecord/test/cases/associations/eager_singularization_test.rb b/activerecord/test/cases/associations/eager_singularization_test.rb
index a61a070331..420a5a805b 100644
--- a/activerecord/test/cases/associations/eager_singularization_test.rb
+++ b/activerecord/test/cases/associations/eager_singularization_test.rb
@@ -1,7 +1,7 @@
-require "cases/helper"
+# frozen_string_literal: true
+require "cases/helper"
-if ActiveRecord::Base.connection.supports_migrations?
class EagerSingularizationTest < ActiveRecord::TestCase
class Virus < ActiveRecord::Base
belongs_to :octopus
@@ -25,10 +25,10 @@ class EagerSingularizationTest < ActiveRecord::TestCase
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
+ 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
@@ -37,8 +37,8 @@ class EagerSingularizationTest < ActiveRecord::TestCase
end
class Success < ActiveRecord::Base
- has_many :analyses, :dependent => :destroy
- has_many :crises, :through => :analyses
+ has_many :analyses, dependent: :destroy
+ has_many :crises, through: :analyses
end
class Dress < ActiveRecord::Base
@@ -65,7 +65,7 @@ class EagerSingularizationTest < ActiveRecord::TestCase
connection.create_table :buses do |t|
t.column :name, :string
end
- connection.create_table :crises_messes, :id => false do |t|
+ connection.create_table :crises_messes, id: false do |t|
t.column :crisis_id, :integer
t.column :mess_id, :integer
end
@@ -104,45 +104,45 @@ class EagerSingularizationTest < ActiveRecord::TestCase
connection.drop_table :compresses
end
- def connection
- ActiveRecord::Base.connection
- end
-
def test_eager_no_extra_singularization_belongs_to
assert_nothing_raised do
- Virus.all.merge!(:includes => :octopus).to_a
+ 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
+ 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
+ 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
+ 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
+ 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
+ Crisis.all.merge!(includes: :compresses).to_a
end
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 ffbf60e390..b37e59038e 100644
--- a/activerecord/test/cases/associations/eager_test.rb
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -1,29 +1,46 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/post'
-require 'models/tagging'
-require 'models/tag'
-require 'models/comment'
-require 'models/author'
-require 'models/essay'
-require 'models/category'
-require 'models/company'
-require 'models/person'
-require 'models/reader'
-require 'models/owner'
-require 'models/pet'
-require 'models/reference'
-require 'models/job'
-require 'models/subscriber'
-require 'models/subscription'
-require 'models/book'
-require 'models/developer'
-require 'models/computer'
-require 'models/project'
-require 'models/member'
-require 'models/membership'
-require 'models/club'
-require 'models/categorization'
-require 'models/sponsor'
+require "models/post"
+require "models/tagging"
+require "models/tag"
+require "models/comment"
+require "models/author"
+require "models/essay"
+require "models/category"
+require "models/company"
+require "models/person"
+require "models/reader"
+require "models/owner"
+require "models/pet"
+require "models/reference"
+require "models/job"
+require "models/subscriber"
+require "models/subscription"
+require "models/book"
+require "models/citation"
+require "models/developer"
+require "models/computer"
+require "models/project"
+require "models/member"
+require "models/membership"
+require "models/club"
+require "models/categorization"
+require "models/sponsor"
+require "models/mentor"
+require "models/contract"
+
+class EagerLoadingTooManyIdsTest < ActiveRecord::TestCase
+ fixtures :citations
+
+ def test_preloading_too_many_ids
+ assert_equal Citation.count, Citation.preload(:reference_of).to_a.size
+ end
+
+ def test_eager_loading_too_may_ids
+ assert_equal Citation.count, Citation.eager_load(:citations).offset(0).size
+ end
+end
class EagerAssociationTest < ActiveRecord::TestCase
fixtures :posts, :comments, :authors, :essays, :author_addresses, :categories, :categories_posts,
@@ -32,42 +49,113 @@ class EagerAssociationTest < ActiveRecord::TestCase
:developers, :projects, :developers_projects, :members, :memberships, :clubs, :sponsors
def test_eager_with_has_one_through_join_model_with_conditions_on_the_through
- member = Member.all.merge!(:includes => :favourite_club).find(members(:some_other_guy).id)
+ member = Member.all.merge!(includes: :favourite_club).find(members(:some_other_guy).id)
assert_nil member.favourite_club
end
+ def test_should_work_inverse_of_with_eager_load
+ author = authors(:david)
+ assert_same author, author.posts.first.author
+ assert_same author, author.posts.eager_load(:comments).first.author
+ end
+
def test_loading_with_one_association
- posts = Post.all.merge!(:includes => :comments).to_a
+ posts = Post.all.merge!(includes: :comments).to_a
post = posts.find { |p| p.id == 1 }
assert_equal 2, post.comments.size
- assert post.comments.include?(comments(:greetings))
+ assert_includes post.comments, comments(:greetings)
- post = Post.all.merge!(:includes => :comments, :where => "posts.title = 'Welcome to the weblog'").first
+ post = Post.all.merge!(includes: :comments, where: "posts.title = 'Welcome to the weblog'").first
assert_equal 2, post.comments.size
- assert post.comments.include?(comments(:greetings))
+ assert_includes post.comments, comments(:greetings)
- posts = Post.all.merge!(:includes => :last_comment).to_a
+ posts = Post.all.merge!(includes: :last_comment).to_a
post = posts.find { |p| p.id == 1 }
assert_equal Post.find(1).last_comment, post.last_comment
end
def test_loading_with_one_association_with_non_preload
- posts = Post.all.merge!(:includes => :last_comment, :order => 'comments.id DESC').to_a
+ posts = Post.all.merge!(includes: :last_comment, order: "comments.id DESC").to_a
post = posts.find { |p| p.id == 1 }
assert_equal Post.find(1).last_comment, post.last_comment
end
def test_loading_conditions_with_or
posts = authors(:david).posts.references(:comments).merge(
- :includes => :comments,
- :where => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE} = 'SpecialComment'"
+ includes: :comments,
+ where: "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE} = 'SpecialComment'"
).to_a
assert_nil posts.detect { |p| p.author_id != authors(:david).id },
"expected to find only david's posts"
end
+ def test_loading_with_scope_including_joins
+ member = Member.first
+ assert_equal members(:groucho), member
+ assert_equal clubs(:boring_club), member.general_club
+
+ member = Member.preload(:general_club).first
+ assert_equal members(:groucho), member
+ assert_equal clubs(:boring_club), member.general_club
+
+ member = Member.eager_load(:general_club).first
+ assert_equal members(:groucho), member
+ assert_equal clubs(:boring_club), member.general_club
+ end
+
+ def test_loading_association_with_same_table_joins
+ super_memberships = [memberships(:super_membership_of_boring_club)]
+
+ member = Member.joins(:favourite_memberships).first
+ assert_equal members(:groucho), member
+ assert_equal super_memberships, member.super_memberships
+
+ member = Member.joins(:favourite_memberships).preload(:super_memberships).first
+ assert_equal members(:groucho), member
+ assert_equal super_memberships, member.super_memberships
+
+ member = Member.joins(:favourite_memberships).eager_load(:super_memberships).first
+ assert_equal members(:groucho), member
+ assert_equal super_memberships, member.super_memberships
+ end
+
+ def test_loading_association_with_intersection_joins
+ member = Member.joins(:current_membership).first
+ assert_equal members(:groucho), member
+ assert_equal clubs(:boring_club), member.club
+ assert_equal memberships(:membership_of_boring_club), member.current_membership
+
+ member = Member.joins(:current_membership).preload(:club, :current_membership).first
+ assert_equal members(:groucho), member
+ assert_equal clubs(:boring_club), member.club
+ assert_equal memberships(:membership_of_boring_club), member.current_membership
+
+ member = Member.joins(:current_membership).eager_load(:club, :current_membership).first
+ assert_equal members(:groucho), member
+ assert_equal clubs(:boring_club), member.club
+ assert_equal memberships(:membership_of_boring_club), member.current_membership
+ end
+
+ def test_loading_associations_dont_leak_instance_state
+ assertions = ->(firm) {
+ assert_equal companies(:first_firm), firm
+
+ assert_predicate firm.association(:readonly_account), :loaded?
+ assert_predicate firm.association(:accounts), :loaded?
+
+ assert_equal accounts(:signals37), firm.readonly_account
+ assert_equal [accounts(:signals37)], firm.accounts
+
+ assert_predicate firm.readonly_account, :readonly?
+ assert firm.accounts.none?(&:readonly?)
+ }
+
+ assertions.call(Firm.preload(:readonly_account, :accounts).first)
+ assertions.call(Firm.eager_load(:readonly_account, :accounts).first)
+ end
+
def test_with_ordering
- list = Post.all.merge!(:includes => :comments, :order => "posts.id DESC").to_a
+ list = Post.all.merge!(includes: :comments, order: "posts.id DESC").to_a
[:other_by_mary, :other_by_bob, :misc_by_mary, :misc_by_bob, :eager_other,
:sti_habtm, :sti_post_and_comments, :sti_comments, :authorless, :thinking, :welcome
].each_with_index do |post, index|
@@ -88,115 +176,123 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_no_queries { authors.map(&:post) }
end
+ def test_calculate_with_string_in_from_and_eager_loading
+ assert_equal 10, Post.from("authors, posts").eager_load(:comments).where("posts.author_id = authors.id").count
+ end
+
def test_with_two_tables_in_from_without_getting_double_quoted
posts = Post.select("posts.*").from("authors, posts").eager_load(:comments).where("posts.author_id = authors.id").order("posts.id").to_a
assert_equal 2, posts.first.comments.size
end
def test_loading_with_multiple_associations
- posts = Post.all.merge!(:includes => [ :comments, :author, :categories ], :order => "posts.id").to_a
+ posts = Post.all.merge!(includes: [ :comments, :author, :categories ], order: "posts.id").to_a
assert_equal 2, posts.first.comments.size
assert_equal 2, posts.first.categories.size
- assert posts.first.comments.include?(comments(:greetings))
+ assert_includes posts.first.comments, comments(:greetings)
end
def test_duplicate_middle_objects
- comments = Comment.all.merge!(:where => 'post_id = 1', :includes => [:post => :author]).to_a
+ comments = Comment.all.merge!(where: "post_id = 1", includes: [post: :author]).to_a
assert_no_queries do
- comments.each {|comment| comment.post.author.name}
+ comments.each { |comment| comment.post.author.name }
end
end
def test_preloading_has_many_in_multiple_queries_with_more_ids_than_database_can_handle
- Comment.connection.expects(:in_clause_length).at_least_once.returns(5)
- posts = Post.all.merge!(:includes=>:comments).to_a
- assert_equal 11, posts.size
+ assert_called(Comment.connection, :in_clause_length, returns: 5) do
+ posts = Post.all.merge!(includes: :comments).to_a
+ assert_equal 11, posts.size
+ end
end
def test_preloading_has_many_in_one_queries_when_database_has_no_limit_on_ids_it_can_handle
- Comment.connection.expects(:in_clause_length).at_least_once.returns(nil)
- posts = Post.all.merge!(:includes=>:comments).to_a
- assert_equal 11, posts.size
+ assert_called(Comment.connection, :in_clause_length, returns: nil) do
+ posts = Post.all.merge!(includes: :comments).to_a
+ assert_equal 11, posts.size
+ end
end
def test_preloading_habtm_in_multiple_queries_with_more_ids_than_database_can_handle
- Comment.connection.expects(:in_clause_length).at_least_once.returns(5)
- posts = Post.all.merge!(:includes=>:categories).to_a
- assert_equal 11, posts.size
+ assert_called(Comment.connection, :in_clause_length, times: 2, returns: 5) do
+ posts = Post.all.merge!(includes: :categories).to_a
+ assert_equal 11, posts.size
+ end
end
def test_preloading_habtm_in_one_queries_when_database_has_no_limit_on_ids_it_can_handle
- Comment.connection.expects(:in_clause_length).at_least_once.returns(nil)
- posts = Post.all.merge!(:includes=>:categories).to_a
- assert_equal 11, posts.size
+ assert_called(Comment.connection, :in_clause_length, times: 2, returns: nil) do
+ posts = Post.all.merge!(includes: :categories).to_a
+ assert_equal 11, posts.size
+ end
end
def test_load_associated_records_in_one_query_when_adapter_has_no_limit
- Comment.connection.expects(:in_clause_length).at_least_once.returns(nil)
-
- post = posts(:welcome)
- assert_queries(2) do
- Post.includes(:comments).where(:id => post.id).to_a
+ assert_called(Comment.connection, :in_clause_length, returns: nil) do
+ post = posts(:welcome)
+ assert_queries(2) do
+ Post.includes(:comments).where(id: post.id).to_a
+ end
end
end
def test_load_associated_records_in_several_queries_when_many_ids_passed
- Comment.connection.expects(:in_clause_length).at_least_once.returns(1)
-
- post1, post2 = posts(:welcome), posts(:thinking)
- assert_queries(3) do
- Post.includes(:comments).where(:id => [post1.id, post2.id]).to_a
+ assert_called(Comment.connection, :in_clause_length, returns: 1) do
+ post1, post2 = posts(:welcome), posts(:thinking)
+ assert_queries(3) do
+ Post.includes(:comments).where(id: [post1.id, post2.id]).to_a
+ end
end
end
def test_load_associated_records_in_one_query_when_a_few_ids_passed
- Comment.connection.expects(:in_clause_length).at_least_once.returns(3)
-
- post = posts(:welcome)
- assert_queries(2) do
- Post.includes(:comments).where(:id => post.id).to_a
+ assert_called(Comment.connection, :in_clause_length, returns: 3) do
+ post = posts(:welcome)
+ assert_queries(2) do
+ Post.includes(:comments).where(id: post.id).to_a
+ end
end
end
def test_including_duplicate_objects_from_belongs_to
- popular_post = Post.create!(:title => 'foo', :body => "I like cars!")
- comment = popular_post.comments.create!(:body => "lol")
- popular_post.readers.create!(:person => people(:michael))
- popular_post.readers.create!(:person => people(:david))
+ popular_post = Post.create!(title: "foo", body: "I like cars!")
+ comment = popular_post.comments.create!(body: "lol")
+ popular_post.readers.create!(person: people(:michael))
+ popular_post.readers.create!(person: people(:david))
- readers = Reader.all.merge!(:where => ["post_id = ?", popular_post.id],
- :includes => {:post => :comments}).to_a
+ readers = Reader.all.merge!(where: ["post_id = ?", popular_post.id],
+ includes: { post: :comments }).to_a
readers.each do |reader|
assert_equal [comment], reader.post.comments
end
end
def test_including_duplicate_objects_from_has_many
- car_post = Post.create!(:title => 'foo', :body => "I like cars!")
+ car_post = Post.create!(title: "foo", body: "I like cars!")
car_post.categories << categories(:general)
car_post.categories << categories(:technology)
- comment = car_post.comments.create!(:body => "hmm")
- categories = Category.all.merge!(:where => { 'posts.id' => car_post.id },
- :includes => {:posts => :comments}).to_a
+ comment = car_post.comments.create!(body: "hmm")
+ categories = Category.all.merge!(where: { "posts.id" => car_post.id },
+ includes: { posts: :comments }).to_a
categories.each do |category|
assert_equal [comment], category.posts[0].comments
end
end
def test_associations_loaded_for_all_records
- post = Post.create!(:title => 'foo', :body => "I like cars!")
- SpecialComment.create!(:body => 'Come on!', :post => post)
- first_category = Category.create! :name => 'First!', :posts => [post]
- second_category = Category.create! :name => 'Second!', :posts => [post]
+ post = Post.create!(title: "foo", body: "I like cars!")
+ SpecialComment.create!(body: "Come on!", post: post)
+ first_category = Category.create! name: "First!", posts: [post]
+ second_category = Category.create! name: "Second!", posts: [post]
- categories = Category.where(:id => [first_category.id, second_category.id]).includes(:posts => :special_comments)
+ categories = Category.where(id: [first_category.id, second_category.id]).includes(posts: :special_comments)
assert_equal categories.map { |category| category.posts.first.special_comments.loaded? }, [true, true]
end
def test_finding_with_includes_on_has_many_association_with_same_include_includes_only_once
author_id = authors(:david).id
- author = assert_queries(3) { Author.all.merge!(:includes => {:posts_with_comments => :comments}).find(author_id) } # find the author, then find the posts, then find the comments
+ author = assert_queries(3) { Author.all.merge!(includes: { posts_with_comments: :comments }).find(author_id) } # find the author, then find the posts, then find the comments
author.posts_with_comments.each do |post_with_comments|
assert_equal post_with_comments.comments.length, post_with_comments.comments.count
assert_nil post_with_comments.comments.to_a.uniq!
@@ -207,7 +303,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
author = authors(:david)
post = author.post_about_thinking_with_last_comment
last_comment = post.last_comment
- author = assert_queries(3) { Author.all.merge!(:includes => {:post_about_thinking_with_last_comment => :last_comment}).find(author.id)} # find the author, then find the posts, then find the comments
+ author = assert_queries(3) { Author.all.merge!(includes: { post_about_thinking_with_last_comment: :last_comment }).find(author.id) } # find the author, then find the posts, then find the comments
assert_no_queries do
assert_equal post, author.post_about_thinking_with_last_comment
assert_equal last_comment, author.post_about_thinking_with_last_comment.last_comment
@@ -218,7 +314,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
post = posts(:welcome)
author = post.author
author_address = author.author_address
- post = assert_queries(3) { Post.all.merge!(:includes => {:author_with_address => :author_address}).find(post.id) } # find the post, then find the author, then find the address
+ post = assert_queries(3) { Post.all.merge!(includes: { author_with_address: :author_address }).find(post.id) } # find the post, then find the author, then find the address
assert_no_queries do
assert_equal author, post.author_with_address
assert_equal author_address, post.author_with_address.author_address
@@ -228,53 +324,50 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_finding_with_includes_on_null_belongs_to_association_with_same_include_includes_only_once
post = posts(:welcome)
post.update!(author: nil)
- post = assert_queries(1) { Post.all.merge!(includes: {author_with_address: :author_address}).find(post.id) }
+ 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
def test_finding_with_includes_on_null_belongs_to_polymorphic_association
sponsor = sponsors(:moustache_club_sponsor_for_groucho)
sponsor.update!(sponsorable: nil)
- sponsor = assert_queries(1) { Sponsor.all.merge!(:includes => :sponsorable).find(sponsor.id) }
+ 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
def test_finding_with_includes_on_empty_polymorphic_type_column
sponsor = sponsors(:moustache_club_sponsor_for_groucho)
- sponsor.update!(sponsorable_type: '', sponsorable_id: nil) # sponsorable_type column might be declared NOT NULL
+ sponsor.update!(sponsorable_type: "", sponsorable_id: nil) # sponsorable_type column might be declared NOT NULL
sponsor = assert_queries(1) do
- assert_nothing_raised { Sponsor.all.merge!(:includes => :sponsorable).find(sponsor.id) }
+ 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
def test_loading_from_an_association
- posts = authors(:david).posts.merge(:includes => :comments, :order => "posts.id").to_a
+ posts = authors(:david).posts.merge(includes: :comments, order: "posts.id").to_a
assert_equal 2, posts.first.comments.size
end
def test_loading_from_an_association_that_has_a_hash_of_conditions
- assert_nothing_raised do
- Author.all.merge!(:includes => :hello_posts_with_hash_conditions).to_a
- end
- assert !Author.all.merge!(:includes => :hello_posts_with_hash_conditions).find(authors(:david).id).hello_posts.empty?
+ assert_not_empty Author.all.merge!(includes: :hello_posts_with_hash_conditions).find(authors(:david).id).hello_posts
end
def test_loading_with_no_associations
- assert_nil Post.all.merge!(:includes => :author).find(posts(:authorless).id).author
+ assert_nil Post.all.merge!(includes: :author).find(posts(:authorless).id).author
end
# Regression test for 21c75e5
def test_nested_loading_does_not_raise_exception_when_association_does_not_exist
assert_nothing_raised do
- Post.all.merge!(:includes => {:author => :author_addresss}).find(posts(:authorless).id)
+ Post.all.merge!(includes: { author: :author_addresss }).find(posts(:authorless).id)
end
end
@@ -282,117 +375,117 @@ class EagerAssociationTest < ActiveRecord::TestCase
post_id = Comment.where(author_id: nil).where.not(post_id: nil).first.post_id
assert_nothing_raised do
- Post.preload(:comments => [{:author => :essays}]).find(post_id)
+ Post.preload(comments: [{ author: :essays }]).find(post_id)
end
end
def test_nested_loading_through_has_one_association
- aa = AuthorAddress.all.merge!(:includes => {:author => :posts}).find(author_addresses(:david_address).id)
+ aa = AuthorAddress.all.merge!(includes: { author: :posts }).find(author_addresses(:david_address).id)
assert_equal aa.author.posts.count, aa.author.posts.length
end
def test_nested_loading_through_has_one_association_with_order
- aa = AuthorAddress.all.merge!(:includes => {:author => :posts}, :order => 'author_addresses.id').find(author_addresses(:david_address).id)
+ aa = AuthorAddress.all.merge!(includes: { author: :posts }, order: "author_addresses.id").find(author_addresses(:david_address).id)
assert_equal aa.author.posts.count, aa.author.posts.length
end
def test_nested_loading_through_has_one_association_with_order_on_association
- aa = AuthorAddress.all.merge!(:includes => {:author => :posts}, :order => 'authors.id').find(author_addresses(:david_address).id)
+ aa = AuthorAddress.all.merge!(includes: { author: :posts }, order: "authors.id").find(author_addresses(:david_address).id)
assert_equal aa.author.posts.count, aa.author.posts.length
end
def test_nested_loading_through_has_one_association_with_order_on_nested_association
- aa = AuthorAddress.all.merge!(:includes => {:author => :posts}, :order => 'posts.id').find(author_addresses(:david_address).id)
+ aa = AuthorAddress.all.merge!(includes: { author: :posts }, order: "posts.id").find(author_addresses(:david_address).id)
assert_equal aa.author.posts.count, aa.author.posts.length
end
def test_nested_loading_through_has_one_association_with_conditions
aa = AuthorAddress.references(:author_addresses).merge(
- :includes => {:author => :posts},
- :where => "author_addresses.id > 0"
+ includes: { author: :posts },
+ where: "author_addresses.id > 0"
).find author_addresses(:david_address).id
assert_equal aa.author.posts.count, aa.author.posts.length
end
def test_nested_loading_through_has_one_association_with_conditions_on_association
aa = AuthorAddress.references(:authors).merge(
- :includes => {:author => :posts},
- :where => "authors.id > 0"
+ includes: { author: :posts },
+ where: "authors.id > 0"
).find author_addresses(:david_address).id
assert_equal aa.author.posts.count, aa.author.posts.length
end
def test_nested_loading_through_has_one_association_with_conditions_on_nested_association
aa = AuthorAddress.references(:posts).merge(
- :includes => {:author => :posts},
- :where => "posts.id > 0"
+ includes: { author: :posts },
+ where: "posts.id > 0"
).find author_addresses(:david_address).id
assert_equal aa.author.posts.count, aa.author.posts.length
end
def test_eager_association_loading_with_belongs_to_and_foreign_keys
- pets = Pet.all.merge!(:includes => :owner).to_a
+ pets = Pet.all.merge!(includes: :owner).to_a
assert_equal 4, pets.length
end
def test_eager_association_loading_with_belongs_to
- comments = Comment.all.merge!(:includes => :post).to_a
+ comments = Comment.all.merge!(includes: :post).to_a
assert_equal 11, comments.length
titles = comments.map { |c| c.post.title }
- assert titles.include?(posts(:welcome).title)
- assert titles.include?(posts(:sti_post_and_comments).title)
+ assert_includes titles, posts(:welcome).title
+ assert_includes titles, posts(:sti_post_and_comments).title
end
def test_eager_association_loading_with_belongs_to_and_limit
- comments = Comment.all.merge!(:includes => :post, :limit => 5, :order => 'comments.id').to_a
+ comments = Comment.all.merge!(includes: :post, limit: 5, order: "comments.id").to_a
assert_equal 5, comments.length
- assert_equal [1,2,3,5,6], comments.collect(&:id)
+ assert_equal [1, 2, 3, 5, 6], comments.collect(&:id)
end
def test_eager_association_loading_with_belongs_to_and_limit_and_conditions
- comments = Comment.all.merge!(:includes => :post, :where => 'post_id = 4', :limit => 3, :order => 'comments.id').to_a
+ comments = Comment.all.merge!(includes: :post, where: "post_id = 4", limit: 3, order: "comments.id").to_a
assert_equal 3, comments.length
- assert_equal [5,6,7], comments.collect(&:id)
+ assert_equal [5, 6, 7], comments.collect(&:id)
end
def test_eager_association_loading_with_belongs_to_and_limit_and_offset
- comments = Comment.all.merge!(:includes => :post, :limit => 3, :offset => 2, :order => 'comments.id').to_a
+ comments = Comment.all.merge!(includes: :post, limit: 3, offset: 2, order: "comments.id").to_a
assert_equal 3, comments.length
- assert_equal [3,5,6], comments.collect(&:id)
+ assert_equal [3, 5, 6], comments.collect(&:id)
end
def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_conditions
- comments = Comment.all.merge!(:includes => :post, :where => 'post_id = 4', :limit => 3, :offset => 1, :order => 'comments.id').to_a
+ comments = Comment.all.merge!(includes: :post, where: "post_id = 4", limit: 3, offset: 1, order: "comments.id").to_a
assert_equal 3, comments.length
- assert_equal [6,7,8], comments.collect(&:id)
+ assert_equal [6, 7, 8], comments.collect(&:id)
end
def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_conditions_array
- comments = Comment.all.merge!(:includes => :post, :where => ['post_id = ?',4], :limit => 3, :offset => 1, :order => 'comments.id').to_a
+ comments = Comment.all.merge!(includes: :post, where: ["post_id = ?", 4], limit: 3, offset: 1, order: "comments.id").to_a
assert_equal 3, comments.length
- assert_equal [6,7,8], comments.collect(&:id)
+ assert_equal [6, 7, 8], comments.collect(&:id)
end
def test_eager_association_loading_with_belongs_to_and_conditions_string_with_unquoted_table_name
assert_nothing_raised do
- Comment.includes(:post).references(:posts).where('posts.id = ?', 4)
+ Comment.includes(:post).references(:posts).where("posts.id = ?", 4)
end
end
def test_eager_association_loading_with_belongs_to_and_conditions_hash
comments = []
assert_nothing_raised do
- comments = Comment.all.merge!(:includes => :post, :where => {:posts => {:id => 4}}, :limit => 3, :order => 'comments.id').to_a
+ comments = Comment.all.merge!(includes: :post, where: { posts: { id: 4 } }, limit: 3, order: "comments.id").to_a
end
assert_equal 3, comments.length
- assert_equal [5,6,7], comments.collect(&:id)
+ assert_equal [5, 6, 7], comments.collect(&:id)
assert_no_queries do
comments.first.post
end
end
def test_eager_association_loading_with_belongs_to_and_conditions_string_with_quoted_table_name
- quoted_posts_id= Comment.connection.quote_table_name('posts') + '.' + Comment.connection.quote_column_name('id')
+ quoted_posts_id = Comment.connection.quote_table_name("posts") + "." + Comment.connection.quote_column_name("id")
assert_nothing_raised do
Comment.includes(:post).references(:posts).where("#{quoted_posts_id} = ?", 4)
end
@@ -400,61 +493,61 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_eager_association_loading_with_belongs_to_and_order_string_with_unquoted_table_name
assert_nothing_raised do
- Comment.all.merge!(:includes => :post, :order => 'posts.id').to_a
+ Comment.all.merge!(includes: :post, order: "posts.id").to_a
end
end
def test_eager_association_loading_with_belongs_to_and_order_string_with_quoted_table_name
- quoted_posts_id= Comment.connection.quote_table_name('posts') + '.' + Comment.connection.quote_column_name('id')
+ quoted_posts_id = Comment.connection.quote_table_name("posts") + "." + Comment.connection.quote_column_name("id")
assert_nothing_raised do
- Comment.includes(:post).references(:posts).order(quoted_posts_id)
+ Comment.includes(:post).references(:posts).order(Arel.sql(quoted_posts_id))
end
end
def test_eager_association_loading_with_belongs_to_and_limit_and_multiple_associations
- posts = Post.all.merge!(:includes => [:author, :very_special_comment], :limit => 1, :order => 'posts.id').to_a
+ posts = Post.all.merge!(includes: [:author, :very_special_comment], limit: 1, order: "posts.id").to_a
assert_equal 1, posts.length
assert_equal [1], posts.collect(&:id)
end
def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_multiple_associations
- posts = Post.all.merge!(:includes => [:author, :very_special_comment], :limit => 1, :offset => 1, :order => 'posts.id').to_a
+ posts = Post.all.merge!(includes: [:author, :very_special_comment], limit: 1, offset: 1, order: "posts.id").to_a
assert_equal 1, posts.length
assert_equal [2], posts.collect(&:id)
end
def test_eager_association_loading_with_belongs_to_inferred_foreign_key_from_association_name
- author_favorite = AuthorFavorite.all.merge!(:includes => :favorite_author).first
+ author_favorite = AuthorFavorite.all.merge!(includes: :favorite_author).first
assert_equal authors(:mary), assert_no_queries { author_favorite.favorite_author }
end
def test_eager_load_belongs_to_quotes_table_and_column_names
job = Job.includes(:ideal_reference).find jobs(:unicyclist).id
references(:michael_unicyclist)
- assert_no_queries{ assert_equal references(:michael_unicyclist), job.ideal_reference}
+ assert_no_queries { assert_equal references(:michael_unicyclist), job.ideal_reference }
end
def test_eager_load_has_one_quotes_table_and_column_names
- michael = Person.all.merge!(:includes => :favourite_reference).find(people(:michael).id)
+ michael = Person.all.merge!(includes: :favourite_reference).find(people(:michael).id)
references(:michael_unicyclist)
- assert_no_queries{ assert_equal references(:michael_unicyclist), michael.favourite_reference}
+ assert_no_queries { assert_equal references(:michael_unicyclist), michael.favourite_reference }
end
def test_eager_load_has_many_quotes_table_and_column_names
- michael = Person.all.merge!(:includes => :references).find(people(:michael).id)
- references(:michael_magician,:michael_unicyclist)
- assert_no_queries{ assert_equal references(:michael_magician,:michael_unicyclist), michael.references.sort_by(&:id) }
+ michael = Person.all.merge!(includes: :references).find(people(:michael).id)
+ references(:michael_magician, :michael_unicyclist)
+ assert_no_queries { assert_equal references(:michael_magician, :michael_unicyclist), michael.references.sort_by(&:id) }
end
def test_eager_load_has_many_through_quotes_table_and_column_names
- michael = Person.all.merge!(:includes => :jobs).find(people(:michael).id)
+ michael = Person.all.merge!(includes: :jobs).find(people(:michael).id)
jobs(:magician, :unicyclist)
- assert_no_queries{ assert_equal jobs(:unicyclist, :magician), michael.jobs.sort_by(&:id) }
+ assert_no_queries { assert_equal jobs(:unicyclist, :magician), michael.jobs.sort_by(&:id) }
end
def test_eager_load_has_many_with_string_keys
subscriptions = subscriptions(:webster_awdr, :webster_rfr)
- subscriber =Subscriber.all.merge!(:includes => :subscriptions).find(subscribers(:second).id)
+ subscriber = Subscriber.all.merge!(includes: :subscriptions).find(subscribers(:second).id)
assert_equal subscriptions, subscriber.subscriptions.sort_by(&:id)
end
@@ -465,32 +558,32 @@ class EagerAssociationTest < ActiveRecord::TestCase
b = Book.create!
- Subscription.create!(:subscriber_id => "PL", :book_id => b.id)
+ Subscription.create!(subscriber_id: "PL", book_id: b.id)
s.reload
s.book_ids = s.book_ids
end
def test_eager_load_has_many_through_with_string_keys
books = books(:awdr, :rfr)
- subscriber = Subscriber.all.merge!(:includes => :books).find(subscribers(:second).id)
+ subscriber = Subscriber.all.merge!(includes: :books).find(subscribers(:second).id)
assert_equal books, subscriber.books.sort_by(&:id)
end
def test_eager_load_belongs_to_with_string_keys
subscriber = subscribers(:second)
- subscription = Subscription.all.merge!(:includes => :subscriber).find(subscriptions(:webster_awdr).id)
+ subscription = Subscription.all.merge!(includes: :subscriber).find(subscriptions(:webster_awdr).id)
assert_equal subscriber, subscription.subscriber
end
def test_eager_association_loading_with_explicit_join
- posts = Post.all.merge!(:includes => :comments, :joins => "INNER JOIN authors ON posts.author_id = authors.id AND authors.name = 'Mary'", :limit => 1, :order => 'author_id').to_a
+ posts = Post.all.merge!(includes: :comments, joins: "INNER JOIN authors ON posts.author_id = authors.id AND authors.name = 'Mary'", limit: 1, order: "author_id").to_a
assert_equal 1, posts.length
end
def test_eager_with_has_many_through
- posts_with_comments = people(:michael).posts.merge(:includes => :comments, :order => 'posts.id').to_a
- posts_with_author = people(:michael).posts.merge(:includes => :author, :order => 'posts.id').to_a
- posts_with_comments_and_author = people(:michael).posts.merge(:includes => [ :comments, :author ], :order => 'posts.id').to_a
+ posts_with_comments = people(:michael).posts.merge(includes: :comments, order: "posts.id").to_a
+ posts_with_author = people(:michael).posts.merge(includes: :author, order: "posts.id").to_a
+ posts_with_comments_and_author = people(:michael).posts.merge(includes: [ :comments, :author ], order: "posts.id").to_a
assert_equal 2, posts_with_comments.inject(0) { |sum, post| sum + post.comments.size }
assert_equal authors(:david), assert_no_queries { posts_with_author.first.author }
assert_equal authors(:david), assert_no_queries { posts_with_comments_and_author.first.author }
@@ -498,35 +591,43 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_eager_with_has_many_through_a_belongs_to_association
author = authors(:mary)
- Post.create!(:author => author, :title => "TITLE", :body => "BODY")
- author.author_favorites.create(:favorite_author_id => 1)
- author.author_favorites.create(:favorite_author_id => 2)
- posts_with_author_favorites = author.posts.merge(:includes => :author_favorites).to_a
+ Post.create!(author: author, title: "TITLE", body: "BODY")
+ author.author_favorites.create(favorite_author_id: 1)
+ author.author_favorites.create(favorite_author_id: 2)
+ posts_with_author_favorites = author.posts.merge(includes: :author_favorites).to_a
assert_no_queries { posts_with_author_favorites.first.author_favorites.first.author_id }
end
def test_eager_with_has_many_through_an_sti_join_model
- author = Author.all.merge!(:includes => :special_post_comments, :order => 'authors.id').first
+ author = Author.all.merge!(includes: :special_post_comments, order: "authors.id").first
assert_equal [comments(:does_it_hurt)], assert_no_queries { author.special_post_comments }
end
+ def test_preloading_has_many_through_with_implicit_source
+ authors = Author.includes(:very_special_comments).to_a
+ assert_no_queries do
+ special_comment_authors = authors.map { |author| [author.name, author.very_special_comments.size] }
+ assert_equal [["David", 1], ["Mary", 0], ["Bob", 0]], special_comment_authors
+ end
+ end
+
def test_eager_with_has_many_through_an_sti_join_model_with_conditions_on_both
- author = Author.all.merge!(:includes => :special_nonexistent_post_comments, :order => 'authors.id').first
+ author = Author.all.merge!(includes: :special_nonexistent_post_comments, order: "authors.id").first
assert_equal [], author.special_nonexistent_post_comments
end
def test_eager_with_has_many_through_join_model_with_conditions
- assert_equal Author.all.merge!(:includes => :hello_post_comments,
- :order => 'authors.id').first.hello_post_comments.sort_by(&:id),
- Author.all.merge!(:order => 'authors.id').first.hello_post_comments.sort_by(&:id)
+ assert_equal Author.all.merge!(includes: :hello_post_comments,
+ order: "authors.id").first.hello_post_comments.sort_by(&:id),
+ Author.all.merge!(order: "authors.id").first.hello_post_comments.sort_by(&:id)
end
def test_eager_with_has_many_through_join_model_with_conditions_on_top_level
- assert_equal comments(:more_greetings), Author.all.merge!(:includes => :comments_with_order_and_conditions).find(authors(:david).id).comments_with_order_and_conditions.first
+ assert_equal comments(:more_greetings), Author.all.merge!(includes: :comments_with_order_and_conditions).find(authors(:david).id).comments_with_order_and_conditions.first
end
def test_eager_with_has_many_through_join_model_with_include
- author_comments = Author.all.merge!(:includes => :comments_with_include).find(authors(:david).id).comments_with_include.to_a
+ author_comments = Author.all.merge!(includes: :comments_with_include).find(authors(:david).id).comments_with_include.to_a
assert_no_queries do
author_comments.first.post.title
end
@@ -534,7 +635,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_eager_with_has_many_through_with_conditions_join_model_with_include
post_tags = Post.find(posts(:welcome).id).misc_tags
- eager_post_tags = Post.all.merge!(:includes => :misc_tags).find(1).misc_tags
+ eager_post_tags = Post.all.merge!(includes: :misc_tags).find(1).misc_tags
assert_equal post_tags, eager_post_tags
end
@@ -545,67 +646,67 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_eager_with_has_many_and_limit
- posts = Post.all.merge!(:order => 'posts.id asc', :includes => [ :author, :comments ], :limit => 2).to_a
+ posts = Post.all.merge!(order: "posts.id asc", includes: [ :author, :comments ], limit: 2).to_a
assert_equal 2, posts.size
assert_equal 3, posts.inject(0) { |sum, post| sum + post.comments.size }
end
def test_eager_with_has_many_and_limit_and_conditions
- posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => "posts.body = 'hello'", :order => "posts.id").to_a
+ posts = Post.all.merge!(includes: [ :author, :comments ], limit: 2, where: "posts.body = 'hello'", order: "posts.id").to_a
assert_equal 2, posts.size
- assert_equal [4,5], posts.collect(&:id)
+ assert_equal [4, 5], posts.collect(&:id)
end
def test_eager_with_has_many_and_limit_and_conditions_array
- posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => [ "posts.body = ?", 'hello' ], :order => "posts.id").to_a
+ posts = Post.all.merge!(includes: [ :author, :comments ], limit: 2, where: [ "posts.body = ?", "hello" ], order: "posts.id").to_a
assert_equal 2, posts.size
- assert_equal [4,5], posts.collect(&:id)
+ assert_equal [4, 5], posts.collect(&:id)
end
def test_eager_with_has_many_and_limit_and_conditions_array_on_the_eagers
- posts = Post.includes(:author, :comments).limit(2).references(:author).where("authors.name = ?", 'David')
+ posts = Post.includes(:author, :comments).limit(2).references(:author).where("authors.name = ?", "David")
assert_equal 2, posts.size
- count = Post.includes(:author, :comments).limit(2).references(:author).where("authors.name = ?", 'David').count
+ count = Post.includes(:author, :comments).limit(2).references(:author).where("authors.name = ?", "David").count
assert_equal posts.size, count
end
def test_eager_with_has_many_and_limit_and_high_offset
- posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :offset => 10, :where => { 'authors.name' => 'David' }).to_a
+ posts = Post.all.merge!(includes: [ :author, :comments ], limit: 2, offset: 10, where: { "authors.name" => "David" }).to_a
assert_equal 0, posts.size
end
def test_eager_with_has_many_and_limit_and_high_offset_and_multiple_array_conditions
assert_queries(1) do
posts = Post.references(:authors, :comments).
- merge(:includes => [ :author, :comments ], :limit => 2, :offset => 10,
- :where => [ "authors.name = ? and comments.body = ?", 'David', 'go crazy' ]).to_a
+ merge(includes: [ :author, :comments ], limit: 2, offset: 10,
+ where: [ "authors.name = ? and comments.body = ?", "David", "go crazy" ]).to_a
assert_equal 0, posts.size
end
end
def test_eager_with_has_many_and_limit_and_high_offset_and_multiple_hash_conditions
assert_queries(1) do
- posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :offset => 10,
- :where => { 'authors.name' => 'David', 'comments.body' => 'go crazy' }).to_a
+ posts = Post.all.merge!(includes: [ :author, :comments ], limit: 2, offset: 10,
+ where: { "authors.name" => "David", "comments.body" => "go crazy" }).to_a
assert_equal 0, posts.size
end
end
def test_count_eager_with_has_many_and_limit_and_high_offset
- posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :offset => 10, :where => { 'authors.name' => 'David' }).count(:all)
+ posts = Post.all.merge!(includes: [ :author, :comments ], limit: 2, offset: 10, where: { "authors.name" => "David" }).count(:all)
assert_equal 0, posts
end
def test_eager_with_has_many_and_limit_with_no_results
- posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => "posts.title = 'magic forest'").to_a
+ posts = Post.all.merge!(includes: [ :author, :comments ], limit: 2, where: "posts.title = 'magic forest'").to_a
assert_equal 0, posts.size
end
def test_eager_count_performed_on_a_has_many_association_with_multi_table_conditional
author = authors(:david)
author_posts_without_comments = author.posts.select { |post| post.comments.blank? }
- assert_equal author_posts_without_comments.size, author.posts.includes(:comments).where('comments.id is null').references(:comments).count
+ assert_equal author_posts_without_comments.size, author.posts.includes(:comments).where("comments.id is null").references(:comments).count
end
def test_eager_count_performed_on_a_has_many_through_association_with_multi_table_conditional
@@ -615,13 +716,13 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_eager_with_has_and_belongs_to_many_and_limit
- posts = Post.all.merge!(:includes => :categories, :order => "posts.id", :limit => 3).to_a
+ posts = Post.all.merge!(includes: :categories, order: "posts.id", limit: 3).to_a
assert_equal 3, posts.size
assert_equal 2, posts[0].categories.size
assert_equal 1, posts[1].categories.size
assert_equal 0, posts[2].categories.size
- assert posts[0].categories.include?(categories(:technology))
- assert posts[1].categories.include?(categories(:general))
+ assert_includes posts[0].categories, categories(:technology)
+ assert_includes posts[1].categories, categories(:general)
end
# Since the preloader for habtm gets raw row hashes from the database and then
@@ -681,32 +782,32 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_eager_association_loading_with_habtm
- posts = Post.all.merge!(:includes => :categories, :order => "posts.id").to_a
+ posts = Post.all.merge!(includes: :categories, order: "posts.id").to_a
assert_equal 2, posts[0].categories.size
assert_equal 1, posts[1].categories.size
assert_equal 0, posts[2].categories.size
- assert posts[0].categories.include?(categories(:technology))
- assert posts[1].categories.include?(categories(:general))
+ assert_includes posts[0].categories, categories(:technology)
+ assert_includes posts[1].categories, categories(:general)
end
def test_eager_with_inheritance
- SpecialPost.all.merge!(:includes => [ :comments ]).to_a
+ SpecialPost.all.merge!(includes: [ :comments ]).to_a
end
def test_eager_has_one_with_association_inheritance
- post = Post.all.merge!(:includes => [ :very_special_comment ]).find(4)
+ post = Post.all.merge!(includes: [ :very_special_comment ]).find(4)
assert_equal "VerySpecialComment", post.very_special_comment.class.to_s
end
def test_eager_has_many_with_association_inheritance
- post = Post.all.merge!(:includes => [ :special_comments ]).find(4)
+ post = Post.all.merge!(includes: [ :special_comments ]).find(4)
post.special_comments.each do |special_comment|
assert special_comment.is_a?(SpecialComment)
end
end
def test_eager_habtm_with_association_inheritance
- post = Post.all.merge!(:includes => [ :special_categories ]).find(6)
+ post = Post.all.merge!(includes: [ :special_categories ]).find(6)
assert_equal 1, post.special_categories.size
post.special_categories.each do |special_category|
assert_equal "SpecialCategory", special_category.class.to_s
@@ -715,8 +816,8 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_eager_with_has_one_dependent_does_not_destroy_dependent
assert_not_nil companies(:first_firm).account
- f = Firm.all.merge!(:includes => :account,
- :where => ["companies.name = ?", "37signals"]).first
+ f = Firm.all.merge!(includes: :account,
+ where: ["companies.name = ?", "37signals"]).first
assert_not_nil f.account
assert_equal companies(:first_firm, :reload).account, f.account
end
@@ -729,22 +830,61 @@ 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") {
- Post.all.merge!(:includes=> :monkeys ).find(6)
+ 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") {
- Post.all.merge!(:includes=>[ :monkeys ]).find(6)
+ 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") {
- Post.all.merge!(:includes=>[ 'monkeys' ]).find(6)
+ 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") {
- Post.all.merge!(:includes=>[ :monkeys, :elephants ]).find(6)
+ 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
+ tag = OrderedTag.create(name: "Foo")
+ post1 = Post.create!(title: "Beaches", body: "I like beaches!")
+ post2 = Post.create!(title: "Pools", body: "I like pools!")
+
+ Tagging.create!(taggable_type: "Post", taggable_id: post1.id, tag: tag)
+ Tagging.create!(taggable_type: "Post", taggable_id: post2.id, tag: tag)
+
+ tag_with_includes = OrderedTag.includes(:tagged_posts).find(tag.id)
+ assert_equal tag_with_includes.ordered_taggings.map(&:taggable).map(&:title), tag_with_includes.tagged_posts.map(&:title)
+ end
+
+ def test_eager_has_many_through_multiple_with_order
+ tag1 = OrderedTag.create!(name: "Bar")
+ tag2 = OrderedTag.create!(name: "Foo")
+
+ post1 = Post.create!(title: "Beaches", body: "I like beaches!")
+ post2 = Post.create!(title: "Pools", body: "I like pools!")
+
+ Tagging.create!(taggable: post1, tag: tag1)
+ Tagging.create!(taggable: post2, tag: tag1)
+ Tagging.create!(taggable: post2, tag: tag2)
+ Tagging.create!(taggable: post1, tag: tag2)
+
+ tags_with_includes = OrderedTag.where(id: [tag1, tag2].map(&:id)).includes(:tagged_posts).order(:id).to_a
+ tag1_with_includes = tags_with_includes.first
+ tag2_with_includes = tags_with_includes.last
+
+ assert_equal([post2, post1].map(&:title), tag1_with_includes.tagged_posts.map(&:title))
+ assert_equal([post1, post2].map(&:title), tag2_with_includes.tagged_posts.map(&:title))
end
def test_eager_with_default_scope
- developer = EagerDeveloperWithDefaultScope.where(:name => 'David').first
+ developer = EagerDeveloperWithDefaultScope.where(name: "David").first
projects = Project.order(:id).to_a
assert_no_queries do
assert_equal(projects, developer.projects)
@@ -752,7 +892,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_eager_with_default_scope_as_class_method
- developer = EagerDeveloperWithClassMethodDefaultScope.where(:name => 'David').first
+ developer = EagerDeveloperWithClassMethodDefaultScope.where(name: "David").first
projects = Project.order(:id).to_a
assert_no_queries do
assert_equal(projects, developer.projects)
@@ -769,7 +909,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_eager_with_default_scope_as_class_method_using_find_by_method
- developer = EagerDeveloperWithClassMethodDefaultScope.find_by(name: 'David')
+ developer = EagerDeveloperWithClassMethodDefaultScope.find_by(name: "David")
projects = Project.order(:id).to_a
assert_no_queries do
assert_equal(projects, developer.projects)
@@ -777,7 +917,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_eager_with_default_scope_as_lambda
- developer = EagerDeveloperWithLambdaDefaultScope.where(:name => 'David').first
+ developer = EagerDeveloperWithLambdaDefaultScope.where(name: "David").first
projects = Project.order(:id).to_a
assert_no_queries do
assert_equal(projects, developer.projects)
@@ -786,8 +926,8 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_eager_with_default_scope_as_block
# warm up the habtm cache
- EagerDeveloperWithBlockDefaultScope.where(:name => 'David').first.projects
- developer = EagerDeveloperWithBlockDefaultScope.where(:name => 'David').first
+ EagerDeveloperWithBlockDefaultScope.where(name: "David").first.projects
+ developer = EagerDeveloperWithBlockDefaultScope.where(name: "David").first
projects = Project.order(:id).to_a
assert_no_queries do
assert_equal(projects, developer.projects)
@@ -795,30 +935,26 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_eager_with_default_scope_as_callable
- developer = EagerDeveloperWithCallableDefaultScope.where(:name => 'David').first
+ developer = EagerDeveloperWithCallableDefaultScope.where(name: "David").first
projects = Project.order(:id).to_a
assert_no_queries do
assert_equal(projects, developer.projects)
end
end
- def find_all_ordered(className, include=nil)
- className.all.merge!(:order=>"#{className.table_name}.#{className.primary_key}", :includes=>include).to_a
- end
-
def test_limited_eager_with_order
assert_equal(
posts(:thinking, :sti_comments),
Post.all.merge!(
- :includes => [:author, :comments], :where => { 'authors.name' => 'David' },
- :order => 'UPPER(posts.title)', :limit => 2, :offset => 1
+ includes: [:author, :comments], where: { "authors.name" => "David" },
+ order: Arel.sql("UPPER(posts.title)"), limit: 2, offset: 1
).to_a
)
assert_equal(
posts(:sti_post_and_comments, :sti_comments),
Post.all.merge!(
- :includes => [:author, :comments], :where => { 'authors.name' => 'David' },
- :order => 'UPPER(posts.title) DESC', :limit => 2, :offset => 1
+ includes: [:author, :comments], where: { "authors.name" => "David" },
+ order: Arel.sql("UPPER(posts.title) DESC"), limit: 2, offset: 1
).to_a
)
end
@@ -827,15 +963,15 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_equal(
posts(:thinking, :sti_comments),
Post.all.merge!(
- :includes => [:author, :comments], :where => { 'authors.name' => 'David' },
- :order => ['UPPER(posts.title)', 'posts.id'], :limit => 2, :offset => 1
+ includes: [:author, :comments], where: { "authors.name" => "David" },
+ order: [Arel.sql("UPPER(posts.title)"), "posts.id"], limit: 2, offset: 1
).to_a
)
assert_equal(
posts(:sti_post_and_comments, :sti_comments),
Post.all.merge!(
- :includes => [:author, :comments], :where => { 'authors.name' => 'David' },
- :order => ['UPPER(posts.title) DESC', 'posts.id'], :limit => 2, :offset => 1
+ includes: [:author, :comments], where: { "authors.name" => "David" },
+ order: [Arel.sql("UPPER(posts.title) DESC"), "posts.id"], limit: 2, offset: 1
).to_a
)
end
@@ -844,25 +980,25 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_equal(
people(:david, :susan),
Person.references(:number1_fans_people).merge(
- :includes => [:readers, :primary_contact, :number1_fan],
- :where => "number1_fans_people.first_name like 'M%'",
- :order => 'people.id', :limit => 2, :offset => 0
+ includes: [:readers, :primary_contact, :number1_fan],
+ where: "number1_fans_people.first_name like 'M%'",
+ order: "people.id", limit: 2, offset: 0
).to_a
)
end
def test_polymorphic_type_condition
- post = Post.all.merge!(:includes => :taggings).find(posts(:thinking).id)
- assert post.taggings.include?(taggings(:thinking_general))
- post = SpecialPost.all.merge!(:includes => :taggings).find(posts(:thinking).id)
- assert post.taggings.include?(taggings(:thinking_general))
+ post = Post.all.merge!(includes: :taggings).find(posts(:thinking).id)
+ assert_includes post.taggings, taggings(:thinking_general)
+ post = SpecialPost.all.merge!(includes: :taggings).find(posts(:thinking).id)
+ assert_includes post.taggings, taggings(:thinking_general)
end
def test_eager_with_multiple_associations_with_same_table_has_many_and_habtm
# Eager includes of has many and habtm associations aren't necessarily sorted in the same way
def assert_equal_after_sort(item1, item2, item3 = nil)
- assert_equal(item1.sort{|a,b| a.id <=> b.id}, item2.sort{|a,b| a.id <=> b.id})
- assert_equal(item3.sort{|a,b| a.id <=> b.id}, item2.sort{|a,b| a.id <=> b.id}) if item3
+ assert_equal(item1.sort { |a, b| a.id <=> b.id }, item2.sort { |a, b| a.id <=> b.id })
+ assert_equal(item3.sort { |a, b| a.id <=> b.id }, item2.sort { |a, b| a.id <=> b.id }) if item3
end
# Test regular association, association with conditions, association with
# STI, and association with conditions assured not to be true
@@ -891,7 +1027,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
@@ -901,28 +1041,34 @@ 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
- assert_nothing_raised { Post.all.merge!(:includes => 'comments').to_a }
+ assert_nothing_raised { Post.all.merge!(includes: "comments").to_a }
end
def test_eager_with_floating_point_numbers
assert_queries(2) do
# Before changes, the floating point numbers will be interpreted as table names and will cause this to run in one query
- Comment.all.merge!(:where => "123.456 = 123.456", :includes => :post).to_a
+ Comment.all.merge!(where: "123.456 = 123.456", includes: :post).to_a
end
end
def test_preconfigured_includes_with_belongs_to
author = posts(:welcome).author_with_posts
- assert_no_queries {assert_equal 5, author.posts.size}
+ assert_no_queries { assert_equal 5, author.posts.size }
end
def test_preconfigured_includes_with_has_one
comment = posts(:sti_comments).very_special_comment_with_post
- assert_no_queries {assert_equal posts(:sti_comments), comment.post}
+ assert_no_queries { assert_equal posts(:sti_comments), comment.post }
end
def test_eager_association_with_scope_with_joins
@@ -964,13 +1110,13 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_association_loading_notification
- notifications = messages_for('instantiation.active_record') do
- Developer.all.merge!(:includes => 'projects', :where => { 'developers_projects.access_level' => 1 }, :limit => 5).to_a.size
+ notifications = messages_for("instantiation.active_record") do
+ Developer.all.merge!(includes: "projects", where: { "developers_projects.access_level" => 1 }, limit: 5).to_a.size
end
message = notifications.first
payload = message.last
- count = Developer.all.merge!(:includes => 'projects', :where => { 'developers_projects.access_level' => 1 }, :limit => 5).to_a.size
+ count = Developer.all.merge!(includes: "projects", where: { "developers_projects.access_level" => 1 }, limit: 5).to_a.size
# eagerloaded row count should be greater than just developer count
assert_operator payload[:record_count], :>, count
@@ -978,7 +1124,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_base_messages
- notifications = messages_for('instantiation.active_record') do
+ notifications = messages_for("instantiation.active_record") do
Developer.all.to_a
end
message = notifications.first
@@ -1000,61 +1146,55 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_load_with_sti_sharing_association
- assert_queries(2) do #should not do 1 query per subclass
+ assert_queries(2) do # should not do 1 query per subclass
Comment.includes(:post).to_a
end
end
def test_conditions_on_join_table_with_include_and_limit
- assert_equal 3, Developer.all.merge!(:includes => 'projects', :where => { 'developers_projects.access_level' => 1 }, :limit => 5).to_a.size
+ assert_equal 3, Developer.all.merge!(includes: "projects", where: { "developers_projects.access_level" => 1 }, limit: 5).to_a.size
end
def test_dont_create_temporary_active_record_instances
Developer.instance_count = 0
- developers = Developer.all.merge!(:includes => 'projects', :where => { 'developers_projects.access_level' => 1 }, :limit => 5).to_a
+ developers = Developer.all.merge!(includes: "projects", where: { "developers_projects.access_level" => 1 }, limit: 5).to_a
assert_equal developers.count, Developer.instance_count
end
def test_order_on_join_table_with_include_and_limit
- assert_equal 5, Developer.all.merge!(:includes => 'projects', :order => 'developers_projects.joined_on DESC', :limit => 5).to_a.size
+ assert_equal 5, Developer.all.merge!(includes: "projects", order: "developers_projects.joined_on DESC", limit: 5).to_a.size
end
def test_eager_loading_with_order_on_joined_table_preloads
posts = assert_queries(2) do
- Post.all.merge!(:joins => :comments, :includes => :author, :order => 'comments.id DESC').to_a
+ Post.all.merge!(joins: :comments, includes: :author, order: "comments.id DESC").to_a
end
assert_equal posts(:eager_other), posts[1]
- assert_equal authors(:mary), assert_no_queries { posts[1].author}
+ assert_equal authors(:mary), assert_no_queries { posts[1].author }
end
def test_eager_loading_with_conditions_on_joined_table_preloads
posts = assert_queries(2) do
- Post.all.merge!(:select => 'distinct posts.*', :includes => :author, :joins => [:comments], :where => "comments.body like 'Thank you%'", :order => 'posts.id').to_a
- end
- assert_equal [posts(:welcome)], posts
- assert_equal authors(:david), assert_no_queries { posts[0].author}
-
- posts = assert_queries(2) do
- Post.all.merge!(:select => 'distinct posts.*', :includes => :author, :joins => [:comments], :where => "comments.body like 'Thank you%'", :order => 'posts.id').to_a
+ Post.all.merge!(select: "distinct posts.*", includes: :author, joins: [:comments], where: "comments.body like 'Thank you%'", order: "posts.id").to_a
end
assert_equal [posts(:welcome)], posts
- assert_equal authors(:david), assert_no_queries { posts[0].author}
+ assert_equal authors(:david), assert_no_queries { posts[0].author }
posts = assert_queries(2) do
- Post.all.merge!(:includes => :author, :joins => {:taggings => :tag}, :where => "tags.name = 'General'", :order => 'posts.id').to_a
+ Post.all.merge!(includes: :author, joins: { taggings: :tag }, where: "tags.name = 'General'", order: "posts.id").to_a
end
assert_equal posts(:welcome, :thinking), posts
posts = assert_queries(2) do
- Post.all.merge!(:includes => :author, :joins => {:taggings => {:tag => :taggings}}, :where => "taggings_tags.super_tag_id=2", :order => 'posts.id').to_a
+ Post.all.merge!(includes: :author, joins: { taggings: { tag: :taggings } }, where: "taggings_tags.super_tag_id=2", order: "posts.id").to_a
end
assert_equal posts(:welcome, :thinking), posts
end
def test_preload_has_many_with_association_condition_and_default_scope
- post = Post.create!(:title => 'Beaches', :body => "I like beaches!")
- Reader.create! :person => people(:david), :post => post
- LazyReader.create! :person => people(:susan), :post => post
+ post = Post.create!(title: "Beaches", body: "I like beaches!")
+ Reader.create! person: people(:david), post: post
+ LazyReader.create! person: people(:susan), post: post
assert_equal 1, post.lazy_readers.to_a.size
assert_equal 2, post.lazy_readers_skimmers_or_not.to_a.size
@@ -1065,39 +1205,39 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_eager_loading_with_conditions_on_string_joined_table_preloads
posts = assert_queries(2) do
- Post.all.merge!(:select => 'distinct posts.*', :includes => :author, :joins => "INNER JOIN comments on comments.post_id = posts.id", :where => "comments.body like 'Thank you%'", :order => 'posts.id').to_a
+ Post.all.merge!(select: "distinct posts.*", includes: :author, joins: "INNER JOIN comments on comments.post_id = posts.id", where: "comments.body like 'Thank you%'", order: "posts.id").to_a
end
assert_equal [posts(:welcome)], posts
- assert_equal authors(:david), assert_no_queries { posts[0].author}
+ assert_equal authors(:david), assert_no_queries { posts[0].author }
posts = assert_queries(2) do
- Post.all.merge!(:select => 'distinct posts.*', :includes => :author, :joins => ["INNER JOIN comments on comments.post_id = posts.id"], :where => "comments.body like 'Thank you%'", :order => 'posts.id').to_a
+ Post.all.merge!(select: "distinct posts.*", includes: :author, joins: ["INNER JOIN comments on comments.post_id = posts.id"], where: "comments.body like 'Thank you%'", order: "posts.id").to_a
end
assert_equal [posts(:welcome)], posts
- assert_equal authors(:david), assert_no_queries { posts[0].author}
+ assert_equal authors(:david), assert_no_queries { posts[0].author }
end
def test_eager_loading_with_select_on_joined_table_preloads
posts = assert_queries(2) do
- Post.all.merge!(:select => 'posts.*, authors.name as author_name', :includes => :comments, :joins => :author, :order => 'posts.id').to_a
+ Post.all.merge!(select: "posts.*, authors.name as author_name", includes: :comments, joins: :author, order: "posts.id").to_a
end
- assert_equal 'David', posts[0].author_name
- assert_equal posts(:welcome).comments, assert_no_queries { posts[0].comments}
+ assert_equal "David", posts[0].author_name
+ assert_equal posts(:welcome).comments, assert_no_queries { posts[0].comments }
end
def test_eager_loading_with_conditions_on_join_model_preloads
authors = assert_queries(2) do
- Author.all.merge!(:includes => :author_address, :joins => :comments, :where => "posts.title like 'Welcome%'").to_a
+ Author.all.merge!(includes: :author_address, joins: :comments, where: "posts.title like 'Welcome%'").to_a
end
assert_equal authors(:david), authors[0]
assert_equal author_addresses(:david_address), authors[0].author_address
end
def test_preload_belongs_to_uses_exclusive_scope
- people = Person.males.merge(:includes => :primary_contact).to_a
+ people = Person.males.merge(includes: :primary_contact).to_a
assert_not_equal people.length, 0
people.each do |person|
- assert_no_queries {assert_not_nil person.primary_contact}
+ assert_no_queries { assert_not_nil person.primary_contact }
assert_equal Person.find(person.id).primary_contact, person.primary_contact
end
end
@@ -1121,9 +1261,9 @@ class EagerAssociationTest < ActiveRecord::TestCase
expected = Firm.find(1).clients_using_primary_key.sort_by(&:name)
# Oracle adapter truncates alias to 30 characters
if current_adapter?(:OracleAdapter)
- firm = Firm.all.merge!(:includes => :clients_using_primary_key, :order => 'clients_using_primary_keys_companies'[0,30]+'.name').find(1)
+ firm = Firm.all.merge!(includes: :clients_using_primary_key, order: "clients_using_primary_keys_companies"[0, 30] + ".name").find(1)
else
- firm = Firm.all.merge!(:includes => :clients_using_primary_key, :order => 'clients_using_primary_keys_companies.name').find(1)
+ firm = Firm.all.merge!(includes: :clients_using_primary_key, order: "clients_using_primary_keys_companies.name").find(1)
end
assert_no_queries do
assert_equal expected, firm.clients_using_primary_key
@@ -1132,7 +1272,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_preload_has_one_using_primary_key
expected = accounts(:signals37)
- firm = Firm.all.merge!(:includes => :account_using_primary_key, :order => 'companies.id').first
+ firm = Firm.all.merge!(includes: :account_using_primary_key, order: "companies.id").first
assert_no_queries do
assert_equal expected, firm.account_using_primary_key
end
@@ -1140,94 +1280,123 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_include_has_one_using_primary_key
expected = accounts(:signals37)
- firm = Firm.all.merge!(:includes => :account_using_primary_key, :order => 'accounts.id').to_a.detect {|f| f.id == 1}
+ firm = Firm.all.merge!(includes: :account_using_primary_key, order: "accounts.id").to_a.detect { |f| f.id == 1 }
assert_no_queries do
assert_equal expected, firm.account_using_primary_key
end
end
def test_preloading_empty_belongs_to
- c = Client.create!(:name => 'Foo', :client_of => Company.maximum(:id) + 1)
+ c = Client.create!(name: "Foo", client_of: Company.maximum(:id) + 1)
client = assert_queries(2) { Client.preload(:firm).find(c.id) }
assert_no_queries { assert_nil client.firm }
+ assert_equal c.client_of, client.client_of
end
def test_preloading_empty_belongs_to_polymorphic
- t = Tagging.create!(:taggable_type => 'Post', :taggable_id => Post.maximum(:id) + 1, :tag => tags(:general))
+ t = Tagging.create!(taggable_type: "Post", taggable_id: Post.maximum(:id) + 1, tag: tags(:general))
tagging = assert_queries(2) { Tagging.preload(:taggable).find(t.id) }
assert_no_queries { assert_nil tagging.taggable }
+ assert_equal t.taggable_id, tagging.taggable_id
end
def test_preloading_through_empty_belongs_to
- c = Client.create!(:name => 'Foo', :client_of => Company.maximum(:id) + 1)
+ c = Client.create!(name: "Foo", client_of: Company.maximum(:id) + 1)
client = assert_queries(2) { Client.preload(:accounts).find(c.id) }
assert_no_queries { assert client.accounts.empty? }
end
def test_preloading_has_many_through_with_distinct
- mary = Author.includes(:unique_categorized_posts).where(:id => authors(:mary).id).first
+ mary = Author.includes(:unique_categorized_posts).where(id: authors(:mary).id).first
assert_equal 1, mary.unique_categorized_posts.length
assert_equal 1, mary.unique_categorized_post_ids.length
end
+ def test_preloading_has_one_using_reorder
+ klass = Class.new(ActiveRecord::Base) do
+ def self.name; "TempAuthor"; end
+ self.table_name = "authors"
+ has_one :post, class_name: "PostWithDefaultScope", foreign_key: :author_id
+ has_one :reorderd_post, -> { reorder(title: :desc) }, class_name: "PostWithDefaultScope", foreign_key: :author_id
+ end
+
+ author = klass.first
+ # PRECONDITION: make sure ordering results in different results
+ assert_not_equal author.post, author.reorderd_post
+
+ preloaded_reorderd_post = klass.preload(:reorderd_post).first.reorderd_post
+
+ assert_equal author.reorderd_post, preloaded_reorderd_post
+ assert_equal Post.order(title: :desc).first.title, preloaded_reorderd_post.title
+ end
+
def test_preloading_polymorphic_with_custom_foreign_type
sponsor = sponsors(:moustache_club_sponsor_for_groucho)
groucho = members(:groucho)
sponsor = assert_queries(2) {
- Sponsor.includes(:thing).where(:id => sponsor.id).first
+ Sponsor.includes(:thing).where(id: sponsor.id).first
}
assert_no_queries { assert_equal groucho, sponsor.thing }
end
def test_joins_with_includes_should_preload_via_joins
- post = assert_queries(1) { Post.includes(:comments).joins(:comments).order('posts.id desc').to_a.first }
+ post = assert_queries(1) { Post.includes(:comments).joins(:comments).order("posts.id desc").to_a.first }
- assert_queries(0) do
+ assert_no_queries do
assert_not_equal 0, post.comments.to_a.count
end
end
def test_join_eager_with_empty_order_should_generate_valid_sql
- assert_nothing_raised(ActiveRecord::StatementInvalid) do
- Post.includes(:comments).order("").where(:comments => {:body => "Thank you for the welcome"}).first
- end
- end
-
- def test_join_eager_with_nil_order_should_generate_valid_sql
- assert_nothing_raised(ActiveRecord::StatementInvalid) do
- Post.includes(:comments).order(nil).where(:comments => {:body => "Thank you for the welcome"}).first
+ assert_nothing_raised do
+ Post.includes(:comments).order("").where(comments: { body: "Thank you for the welcome" }).first
end
end
def test_deep_including_through_habtm
# warm up habtm cache
- posts = Post.all.merge!(:includes => {:categories => :categorizations}, :order => "posts.id").to_a
+ posts = Post.all.merge!(includes: { categories: :categorizations }, order: "posts.id").to_a
posts[0].categories[0].categorizations.length
- posts = Post.all.merge!(:includes => {:categories => :categorizations}, :order => "posts.id").to_a
+ posts = Post.all.merge!(includes: { categories: :categorizations }, order: "posts.id").to_a
assert_no_queries { assert_equal 2, posts[0].categories[0].categorizations.length }
assert_no_queries { assert_equal 1, posts[0].categories[1].categorizations.length }
assert_no_queries { assert_equal 2, posts[1].categories[0].categorizations.length }
end
+ def test_eager_load_multiple_associations_with_references
+ mentor = Mentor.create!(name: "Barış Can DAYLIK")
+ developer = Developer.create!(name: "Mehmet Emin İNAÇ", mentor: mentor)
+ Contract.create!(developer: developer)
+ project = Project.create!(name: "VNGRS", mentor: mentor)
+ project.developers << developer
+ projects = Project.references(:mentors).includes(mentor: { developers: :contracts }, developers: :contracts)
+ assert_equal projects.last.mentor.developers.first.contracts, projects.last.developers.last.contracts
+ end
+
+ def test_preloading_has_many_through_with_custom_scope
+ project = Project.includes(:developers_named_david_with_hash_conditions).find(projects(:active_record).id)
+ assert_equal [developers(:david)], project.developers_named_david_with_hash_conditions
+ end
+
test "scoping with a circular preload" do
- assert_equal Comment.find(1), Comment.preload(:post => :comments).scoping { Comment.find(1) }
+ assert_equal Comment.find(1), Comment.preload(post: :comments).scoping { Comment.find(1) }
end
test "circular preload does not modify unscoped" do
expected = FirstPost.unscoped.find(2)
- FirstPost.preload(:comments => :first_post).find(1)
+ FirstPost.preload(comments: :first_post).find(1)
assert_equal expected, FirstPost.unscoped.find(2)
end
test "preload ignores the scoping" do
assert_equal(
Comment.find(1).post,
- Post.where('1 = 0').scoping { Comment.preload(:post).find(1).post }
+ Post.where("1 = 0").scoping { Comment.preload(:post).find(1).post }
)
end
@@ -1252,10 +1421,10 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
test "works in combination with order(:symbol) and reorder(:symbol)" do
- author = Author.includes(:posts).references(:posts).order(:name).find_by('posts.title IS NOT NULL')
+ author = Author.includes(:posts).references(:posts).order(:name).find_by("posts.title IS NOT NULL")
assert_equal authors(:bob), author
- author = Author.includes(:posts).references(:posts).reorder(:name).find_by('posts.title IS NOT NULL')
+ author = Author.includes(:posts).references(:posts).reorder(:name).find_by("posts.title IS NOT NULL")
assert_equal authors(:bob), author
end
@@ -1281,6 +1450,8 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_nothing_raised do
authors(:david).essays.includes(:writer).any?
+ authors(:david).essays.includes(:writer).exists?
+ authors(:david).essays.includes(:owner).where("name IS NOT NULL").exists?
end
end
@@ -1295,7 +1466,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
test "including associations with where.not adds implicit references" do
author = assert_queries(2) {
- Author.includes(:posts).where.not(posts: { title: 'Welcome to the weblog'} ).last
+ Author.includes(:posts).where.not(posts: { title: "Welcome to the weblog" }).last
}
assert_no_queries {
@@ -1325,45 +1496,130 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_match message, error.message
end
+ test "preload with invalid argument" do
+ exception = assert_raises(ArgumentError) do
+ Author.preload(10).to_a
+ end
+ assert_equal("10 was not recognized for preload", exception.message)
+ end
+
+ test "associations with extensions are not instance dependent" do
+ assert_nothing_raised do
+ Author.includes(:posts_with_extension).to_a
+ end
+ end
+
+ test "including associations with extensions and an instance dependent scope is not supported" do
+ e = assert_raises(ArgumentError) do
+ Author.includes(:posts_with_extension_and_instance).to_a
+ end
+ assert_match(/Preloading instance dependent scopes is not supported/, e.message)
+ end
+
test "preloading readonly association" do
# has-one
firm = Firm.where(id: "1").preload(:readonly_account).first!
- assert firm.readonly_account.readonly?
+ assert_predicate firm.readonly_account, :readonly?
# has_and_belongs_to_many
project = Project.where(id: "2").preload(:readonly_developers).first!
- assert project.readonly_developers.first.readonly?
+ assert_predicate project.readonly_developers.first, :readonly?
# has-many :through
david = Author.where(id: "1").preload(:readonly_comments).first!
- assert david.readonly_comments.first.readonly?
+ assert_predicate david.readonly_comments.first, :readonly?
+ end
+
+ test "eager-loading non-readonly association" do
+ # has_one
+ firm = Firm.where(id: "1").eager_load(:account).first!
+ assert_not_predicate firm.account, :readonly?
+
+ # has_and_belongs_to_many
+ project = Project.where(id: "2").eager_load(:developers).first!
+ assert_not_predicate project.developers.first, :readonly?
+
+ # has_many :through
+ david = Author.where(id: "1").eager_load(:comments).first!
+ assert_not_predicate david.comments.first, :readonly?
+
+ # belongs_to
+ post = Post.where(id: "1").eager_load(:author).first!
+ assert_not_predicate post.author, :readonly?
end
test "eager-loading readonly association" do
# has-one
firm = Firm.where(id: "1").eager_load(:readonly_account).first!
- assert firm.readonly_account.readonly?
+ assert_predicate firm.readonly_account, :readonly?
# has_and_belongs_to_many
project = Project.where(id: "2").eager_load(:readonly_developers).first!
- assert project.readonly_developers.first.readonly?
+ assert_predicate project.readonly_developers.first, :readonly?
# has-many :through
david = Author.where(id: "1").eager_load(:readonly_comments).first!
- assert david.readonly_comments.first.readonly?
+ assert_predicate david.readonly_comments.first, :readonly?
# belongs_to
- post = Post.where(id: "1").eager_load(:author).first!
- assert post.author.readonly?
+ post = Post.where(id: "1").eager_load(:readonly_author).first!
+ assert_predicate post.readonly_author, :readonly?
end
test "preloading a polymorphic association with references to the associated table" do
- post = Post.includes(:tags).references(:tags).where('tags.name = ?', 'General').first
+ post = Post.includes(:tags).references(:tags).where("tags.name = ?", "General").first
assert_equal posts(:welcome), post
end
test "eager-loading a polymorphic association with references to the associated table" do
- post = Post.eager_load(:tags).where('tags.name = ?', 'General').first
+ post = Post.eager_load(:tags).where("tags.name = ?", "General").first
assert_equal posts(:welcome), post
end
+
+ test "eager-loading with a polymorphic association won't work consistently" do
+ assert_raise(ActiveRecord::EagerLoadPolymorphicError) { authors(:david).essays.eager_load(:writer).to_a }
+ assert_raise(ActiveRecord::EagerLoadPolymorphicError) { authors(:david).essays.eager_load(:writer).count }
+ assert_raise(ActiveRecord::EagerLoadPolymorphicError) { authors(:david).essays.eager_load(:writer).exists? }
+ end
+
+ # CollectionProxy#reader is expensive, so the preloader avoids calling it.
+ test "preloading has_many_through association avoids calling association.reader" do
+ assert_not_called_on_instance_of(ActiveRecord::Associations::HasManyAssociation, :reader) do
+ Author.preload(:readonly_comments).first!
+ end
+ end
+
+ test "preloading through a polymorphic association doesn't require the association to exist" do
+ sponsors = []
+ assert_queries 5 do
+ sponsors = Sponsor.where(sponsorable_id: 1).preload(sponsorable: [:post, :membership]).to_a
+ end
+ # check the preload worked
+ assert_queries 0 do
+ sponsors.map(&:sponsorable).map { |s| s.respond_to?(:posts) ? s.post.author : s.membership }
+ end
+ end
+
+ test "preloading a regular association through a polymorphic association doesn't require the association to exist on all types" do
+ sponsors = []
+ assert_queries 6 do
+ sponsors = Sponsor.where(sponsorable_id: 1).preload(sponsorable: [{ post: :first_comment }, :membership]).to_a
+ end
+ # check the preload worked
+ assert_queries 0 do
+ sponsors.map(&:sponsorable).map { |s| s.respond_to?(:posts) ? s.post.author : s.membership }
+ end
+ end
+
+ test "preloading a regular association with a typo through a polymorphic association still raises" do
+ # this test contains an intentional typo of first -> fist
+ assert_raises(ActiveRecord::AssociationNotFoundError) do
+ Sponsor.where(sponsorable_id: 1).preload(sponsorable: [{ post: :fist_comment }, :membership]).to_a
+ end
+ end
+
+ private
+ def find_all_ordered(klass, include = nil)
+ klass.order("#{klass.table_name}.#{klass.primary_key}").includes(include).to_a
+ end
end
diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb
index b161cde335..aef8f31112 100644
--- a/activerecord/test/cases/associations/extension_test.rb
+++ b/activerecord/test/cases/associations/extension_test.rb
@@ -1,10 +1,12 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/post'
-require 'models/comment'
-require 'models/project'
-require 'models/developer'
-require 'models/computer'
-require 'models/company_in_module'
+require "models/post"
+require "models/comment"
+require "models/project"
+require "models/developer"
+require "models/computer"
+require "models/company_in_module"
class AssociationsExtensionsTest < ActiveRecord::TestCase
fixtures :projects, :developers, :developers_projects, :comments, :posts
@@ -36,6 +38,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
@@ -45,7 +52,7 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase
# Marshaling an association shouldn't make it unusable by wiping its reflection.
assert_not_nil david.association(:projects).reflection
- david_too = Marshal.load(marshalled)
+ david_too = Marshal.load(marshalled)
assert_equal projects(:action_controller), david_too.projects.find_most_recent
end
@@ -63,14 +70,20 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase
extend!(Developer)
extend!(MyApplication::Business::Developer)
- assert Object.const_get 'DeveloperAssociationNameAssociationExtension'
- assert MyApplication::Business.const_get 'DeveloperAssociationNameAssociationExtension'
+ assert Object.const_get "DeveloperAssociationNameAssociationExtension"
+ assert MyApplication::Business.const_get "DeveloperAssociationNameAssociationExtension"
end
def test_proxy_association_after_scoped
post = posts(:welcome)
assert_equal post.association(:comments), post.comments.the_association
- assert_equal post.association(:comments), post.comments.where('1=1').the_association
+ assert_equal post.association(:comments), post.comments.where("1=1").the_association
+ end
+
+ def test_association_with_default_scope
+ assert_raises OopsError do
+ posts(:welcome).comments.destroy_all
+ end
end
private
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 e584c94ad8..515eb65d37 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
@@ -1,82 +1,89 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/developer'
-require 'models/computer'
-require 'models/project'
-require 'models/company'
-require 'models/customer'
-require 'models/order'
-require 'models/categorization'
-require 'models/category'
-require 'models/post'
-require 'models/author'
-require 'models/tag'
-require 'models/tagging'
-require 'models/parrot'
-require 'models/person'
-require 'models/pirate'
-require 'models/treasure'
-require 'models/price_estimate'
-require 'models/club'
-require 'models/member'
-require 'models/membership'
-require 'models/sponsor'
-require 'models/country'
-require 'models/treaty'
-require 'models/vertex'
-require 'models/publisher'
-require 'models/publisher/article'
-require 'models/publisher/magazine'
-require 'active_support/core_ext/string/conversions'
+require "models/developer"
+require "models/computer"
+require "models/project"
+require "models/company"
+require "models/course"
+require "models/customer"
+require "models/order"
+require "models/categorization"
+require "models/category"
+require "models/post"
+require "models/author"
+require "models/tag"
+require "models/tagging"
+require "models/parrot"
+require "models/person"
+require "models/pirate"
+require "models/professor"
+require "models/treasure"
+require "models/price_estimate"
+require "models/club"
+require "models/user"
+require "models/member"
+require "models/membership"
+require "models/sponsor"
+require "models/lesson"
+require "models/student"
+require "models/country"
+require "models/treaty"
+require "models/vertex"
+require "models/publisher"
+require "models/publisher/article"
+require "models/publisher/magazine"
+require "active_support/core_ext/string/conversions"
class ProjectWithAfterCreateHook < ActiveRecord::Base
- self.table_name = 'projects'
+ self.table_name = "projects"
has_and_belongs_to_many :developers,
- :class_name => "DeveloperForProjectWithAfterCreateHook",
- :join_table => "developers_projects",
- :foreign_key => "project_id",
- :association_foreign_key => "developer_id"
+ class_name: "DeveloperForProjectWithAfterCreateHook",
+ join_table: "developers_projects",
+ foreign_key: "project_id",
+ association_foreign_key: "developer_id"
after_create :add_david
def add_david
- david = DeveloperForProjectWithAfterCreateHook.find_by_name('David')
+ david = DeveloperForProjectWithAfterCreateHook.find_by_name("David")
david.projects << self
end
end
class DeveloperForProjectWithAfterCreateHook < ActiveRecord::Base
- self.table_name = 'developers'
+ self.table_name = "developers"
has_and_belongs_to_many :projects,
- :class_name => "ProjectWithAfterCreateHook",
- :join_table => "developers_projects",
- :association_foreign_key => "project_id",
- :foreign_key => "developer_id"
+ class_name: "ProjectWithAfterCreateHook",
+ join_table: "developers_projects",
+ association_foreign_key: "project_id",
+ foreign_key: "developer_id"
end
class ProjectWithSymbolsForKeys < ActiveRecord::Base
- self.table_name = 'projects'
+ self.table_name = "projects"
has_and_belongs_to_many :developers,
- :class_name => "DeveloperWithSymbolsForKeys",
- :join_table => :developers_projects,
- :foreign_key => :project_id,
- :association_foreign_key => "developer_id"
+ class_name: "DeveloperWithSymbolsForKeys",
+ join_table: :developers_projects,
+ foreign_key: :project_id,
+ association_foreign_key: "developer_id"
end
class DeveloperWithSymbolsForKeys < ActiveRecord::Base
- self.table_name = 'developers'
+ self.table_name = "developers"
has_and_belongs_to_many :projects,
- :class_name => "ProjectWithSymbolsForKeys",
- :join_table => :developers_projects,
- :association_foreign_key => :project_id,
- :foreign_key => "developer_id"
+ class_name: "ProjectWithSymbolsForKeys",
+ join_table: :developers_projects,
+ association_foreign_key: :project_id,
+ foreign_key: "developer_id"
end
class SubDeveloper < Developer
- self.table_name = 'developers'
+ self.table_name = "developers"
has_and_belongs_to_many :special_projects,
- :join_table => 'developers_projects',
- :foreign_key => "project_id",
- :association_foreign_key => "developer_id"
+ join_table: "developers_projects",
+ foreign_key: "project_id",
+ association_foreign_key: "developer_id"
end
class DeveloperWithSymbolClassName < Developer
@@ -86,26 +93,50 @@ end
class DeveloperWithExtendOption < Developer
module NamedExtension
def category
- 'sns'
+ "sns"
end
end
has_and_belongs_to_many :projects, extend: NamedExtension
end
+class ProjectUnscopingDavidDefaultScope < ActiveRecord::Base
+ self.table_name = "projects"
+ has_and_belongs_to_many :developers, -> { unscope(where: "name") },
+ class_name: "LazyBlockDeveloperCalledDavid",
+ join_table: "developers_projects",
+ foreign_key: "project_id",
+ association_foreign_key: "developer_id"
+end
+
+class Kitchen < ActiveRecord::Base
+ has_one :sink
+end
+
+class Sink < ActiveRecord::Base
+ has_and_belongs_to_many :sources, join_table: :edges
+ belongs_to :kitchen
+ accepts_nested_attributes_for :kitchen
+end
+
+class Source < ActiveRecord::Base
+ self.table_name = "men"
+ has_and_belongs_to_many :sinks, join_table: :edges
+end
+
class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects,
:parrots, :pirates, :parrots_pirates, :treasures, :price_estimates, :tags, :taggings, :computers
def setup_data_for_habtm_case
- ActiveRecord::Base.connection.execute('delete from countries_treaties')
+ ActiveRecord::Base.connection.execute("delete from countries_treaties")
- country = Country.new(:name => 'India')
- country.country_id = 'c1'
+ country = Country.new(name: "India")
+ country.country_id = "c1"
country.save!
- treaty = Treaty.new(:name => 'peace')
- treaty.treaty_id = 't1'
+ treaty = Treaty.new(name: "peace")
+ treaty.treaty_id = "t1"
country.treaties << treaty
end
@@ -119,32 +150,45 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
setup_data_for_habtm_case
con = ActiveRecord::Base.connection
- sql = 'select * from countries_treaties'
+ sql = "select * from countries_treaties"
record = con.select_rows(sql).last
- assert_equal 'c1', record[0]
- assert_equal 't1', record[1]
+ assert_equal "c1", record[0]
+ assert_equal "t1", record[1]
end
def test_proper_usage_of_primary_keys_and_join_table
setup_data_for_habtm_case
- assert_equal 'country_id', Country.primary_key
- assert_equal 'treaty_id', Treaty.primary_key
+ assert_equal "country_id", Country.primary_key
+ assert_equal "treaty_id", Treaty.primary_key
country = Country.first
assert_equal 1, country.treaties.count
end
+ def test_join_table_composite_primary_key_should_not_warn
+ country = Country.new(name: "India")
+ country.country_id = "c1"
+ country.save!
+
+ treaty = Treaty.new(name: "peace")
+ treaty.treaty_id = "t1"
+ warning = capture(:stderr) do
+ country.treaties << treaty
+ end
+ assert_no_match(/WARNING: Active Record does not support composite primary key\./, warning)
+ end
+
def test_has_and_belongs_to_many
david = Developer.find(1)
- assert !david.projects.empty?
+ assert_not_empty david.projects
assert_equal 2, david.projects.size
active_record = Project.find(1)
- assert !active_record.developers.empty?
+ assert_not_empty active_record.developers
assert_equal 3, active_record.developers.size
- assert active_record.developers.include?(david)
+ assert_includes active_record.developers, david
end
def test_adding_single
@@ -157,8 +201,8 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
jamis.projects << action_controller
assert_equal 2, jamis.projects.size
- assert_equal 2, jamis.projects(true).size
- assert_equal 2, action_controller.developers(true).size
+ assert_equal 2, jamis.projects.reload.size
+ assert_equal 2, action_controller.developers.reload.size
end
def test_adding_type_mismatch
@@ -176,9 +220,9 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
action_controller.developers << jamis
- assert_equal 2, jamis.projects(true).size
+ assert_equal 2, jamis.projects.reload.size
assert_equal 2, action_controller.developers.size
- assert_equal 2, action_controller.developers(true).size
+ assert_equal 2, action_controller.developers.reload.size
end
def test_adding_from_the_project_fixed_timestamp
@@ -192,9 +236,9 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
action_controller.developers << jamis
assert_equal updated_at, jamis.updated_at
- assert_equal 2, jamis.projects(true).size
+ assert_equal 2, jamis.projects.reload.size
assert_equal 2, action_controller.developers.size
- assert_equal 2, action_controller.developers(true).size
+ assert_equal 2, action_controller.developers.reload.size
end
def test_adding_multiple
@@ -203,7 +247,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
aredridel.projects.reload
aredridel.projects.push(Project.find(1), Project.find(2))
assert_equal 2, aredridel.projects.size
- assert_equal 2, aredridel.projects(true).size
+ assert_equal 2, aredridel.projects.reload.size
end
def test_adding_a_collection
@@ -212,7 +256,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
aredridel.projects.reload
aredridel.projects.concat([Project.find(1), Project.find(2)])
assert_equal 2, aredridel.projects.size
- assert_equal 2, aredridel.projects(true).size
+ assert_equal 2, aredridel.projects.reload.size
end
def test_habtm_adding_before_save
@@ -220,20 +264,20 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
no_of_projects = Project.count
aredridel = Developer.new("name" => "Aredridel")
aredridel.projects.concat([Project.find(1), p = Project.new("name" => "Projekt")])
- assert !aredridel.persisted?
- assert !p.persisted?
+ assert_not_predicate aredridel, :persisted?
+ assert_not_predicate p, :persisted?
assert aredridel.save
- assert aredridel.persisted?
- assert_equal no_of_devels+1, Developer.count
- assert_equal no_of_projects+1, Project.count
+ assert_predicate aredridel, :persisted?
+ assert_equal no_of_devels + 1, Developer.count
+ assert_equal no_of_projects + 1, Project.count
assert_equal 2, aredridel.projects.size
- assert_equal 2, aredridel.projects(true).size
+ assert_equal 2, aredridel.projects.reload.size
end
def test_habtm_saving_multiple_relationships
new_project = Project.new("name" => "Grimetime")
amount_of_developers = 4
- developers = (0...amount_of_developers).collect {|i| Developer.create(:name => "JME #{i}") }.reverse
+ developers = (0...amount_of_developers).reverse_each.map { |i| Developer.create(name: "JME #{i}") }
new_project.developer_ids = [developers[0].id, developers[1].id]
new_project.developers_with_callback_ids = [developers[2].id, developers[3].id]
@@ -258,54 +302,61 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
def test_habtm_collection_size_from_params
- devel = Developer.new({
+ devel = Developer.new(
projects_attributes: {
- '0' => {}
- }
- })
+ "0" => {}
+ })
assert_equal 1, devel.projects.size
end
def test_build
devel = Developer.find(1)
- proj = assert_no_queries(ignore_none: false) { devel.projects.build("name" => "Projekt") }
- assert !devel.projects.loaded?
+
+ # Load schema information so we don't query below if running just this test.
+ Project.define_attribute_methods
+
+ proj = assert_no_queries { devel.projects.build("name" => "Projekt") }
+ assert_not_predicate devel.projects, :loaded?
assert_equal devel.projects.last, proj
- assert devel.projects.loaded?
+ assert_predicate devel.projects, :loaded?
- assert !proj.persisted?
+ assert_not_predicate proj, :persisted?
devel.save
- assert proj.persisted?
+ assert_predicate proj, :persisted?
assert_equal devel.projects.last, proj
assert_equal Developer.find(1).projects.sort_by(&:id).last, proj # prove join table is updated
end
def test_new_aliased_to_build
devel = Developer.find(1)
- proj = assert_no_queries(ignore_none: false) { devel.projects.new("name" => "Projekt") }
- assert !devel.projects.loaded?
+
+ # Load schema information so we don't query below if running just this test.
+ Project.define_attribute_methods
+
+ proj = assert_no_queries { devel.projects.new("name" => "Projekt") }
+ assert_not_predicate devel.projects, :loaded?
assert_equal devel.projects.last, proj
- assert devel.projects.loaded?
+ assert_predicate devel.projects, :loaded?
- assert !proj.persisted?
+ assert_not_predicate proj, :persisted?
devel.save
- assert proj.persisted?
+ assert_predicate proj, :persisted?
assert_equal devel.projects.last, proj
assert_equal Developer.find(1).projects.sort_by(&:id).last, proj # prove join table is updated
end
def test_build_by_new_record
- devel = Developer.new(:name => "Marcel", :salary => 75000)
- devel.projects.build(:name => "Make bed")
- proj2 = devel.projects.build(:name => "Lie in it")
+ devel = Developer.new(name: "Marcel", salary: 75000)
+ devel.projects.build(name: "Make bed")
+ proj2 = devel.projects.build(name: "Lie in it")
assert_equal devel.projects.last, proj2
- assert !proj2.persisted?
+ assert_not_predicate proj2, :persisted?
devel.save
- assert devel.persisted?
- assert proj2.persisted?
+ assert_predicate devel, :persisted?
+ assert_predicate proj2, :persisted?
assert_equal devel.projects.last, proj2
assert_equal Developer.find_by_name("Marcel").projects.last, proj2 # prove join table is updated
end
@@ -313,40 +364,27 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_create
devel = Developer.find(1)
proj = devel.projects.create("name" => "Projekt")
- assert !devel.projects.loaded?
+ assert_not_predicate devel.projects, :loaded?
assert_equal devel.projects.last, proj
- assert !devel.projects.loaded?
+ assert_not_predicate devel.projects, :loaded?
- assert proj.persisted?
+ assert_predicate proj, :persisted?
assert_equal Developer.find(1).projects.sort_by(&:id).last, proj # prove join table is updated
end
- def test_create_by_new_record
- devel = Developer.new(:name => "Marcel", :salary => 75000)
- devel.projects.build(:name => "Make bed")
- proj2 = devel.projects.build(:name => "Lie in it")
- assert_equal devel.projects.last, proj2
- assert !proj2.persisted?
- devel.save
- assert devel.persisted?
- assert proj2.persisted?
- assert_equal devel.projects.last, proj2
- assert_equal Developer.find_by_name("Marcel").projects.last, proj2 # prove join table is updated
- end
-
def test_creation_respects_hash_condition
# in Oracle '' is saved as null therefore need to save ' ' in not null column
- post = categories(:general).post_with_conditions.build(:body => ' ')
+ post = categories(:general).post_with_conditions.build(body: " ")
- assert post.save
- assert_equal 'Yet Another Testing Title', post.title
+ assert post.save
+ assert_equal "Yet Another Testing Title", post.title
# in Oracle '' is saved as null therefore need to save ' ' in not null column
- another_post = categories(:general).post_with_conditions.create(:body => ' ')
+ another_post = categories(:general).post_with_conditions.create(body: " ")
- assert another_post.persisted?
- assert_equal 'Yet Another Testing Title', another_post.title
+ assert_predicate another_post, :persisted?
+ assert_equal "Yet Another Testing Title", another_post.title
end
def test_distinct_after_the_fact
@@ -355,7 +393,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
dev.projects << projects(:active_record)
assert_equal 3, dev.projects.size
- assert_equal 1, dev.projects.distinct.size
+ assert_equal 1, dev.projects.uniq.size
end
def test_distinct_before_the_fact
@@ -391,8 +429,8 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
david.projects.delete(active_record)
assert_equal 1, david.projects.size
- assert_equal 1, david.projects(true).size
- assert_equal 2, active_record.developers(true).size
+ assert_equal 1, david.projects.reload.size
+ assert_equal 2, active_record.developers.reload.size
end
def test_deleting_array
@@ -400,7 +438,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
david.projects.reload
david.projects.delete(Project.all.to_a)
assert_equal 0, david.projects.size
- assert_equal 0, david.projects(true).size
+ assert_equal 0, david.projects.reload.size
end
def test_deleting_all
@@ -408,15 +446,15 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
david.projects.reload
david.projects.clear
assert_equal 0, david.projects.size
- assert_equal 0, david.projects(true).size
+ assert_equal 0, david.projects.reload.size
end
def test_removing_associations_on_destroy
david = DeveloperWithBeforeDestroyRaise.find(1)
- assert !david.projects.empty?
+ assert_not_empty david.projects
david.destroy
- assert david.projects.empty?
- assert DeveloperWithBeforeDestroyRaise.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = 1").empty?
+ assert_empty david.projects
+ assert_empty DeveloperWithBeforeDestroyRaise.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = 1")
end
def test_destroying
@@ -431,10 +469,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
join_records = Developer.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = #{david.id} AND project_id = #{project.id}")
- assert join_records.empty?
+ assert_empty join_records
assert_equal 1, david.reload.projects.size
- assert_equal 1, david.projects(true).size
+ assert_equal 1, david.projects.reload.size
end
def test_destroying_many
@@ -447,32 +485,32 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
join_records = Developer.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = #{david.id}")
- assert join_records.empty?
+ assert_empty join_records
assert_equal 0, david.reload.projects.size
- assert_equal 0, david.projects(true).size
+ assert_equal 0, david.projects.reload.size
end
def test_destroy_all
david = Developer.find(1)
david.projects.reload
- assert !david.projects.empty?
+ assert_not_empty david.projects
assert_no_difference "Project.count" do
david.projects.destroy_all
end
join_records = Developer.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = #{david.id}")
- assert join_records.empty?
+ assert_empty join_records
- assert david.projects.empty?
- assert david.projects(true).empty?
+ assert_empty david.projects
+ assert_empty david.projects.reload
end
def test_destroy_associations_destroys_multiple_associations
george = parrots(:george)
- assert !george.pirates.empty?
- assert !george.treasures.empty?
+ assert_not_empty george.pirates
+ assert_not_empty george.treasures
assert_no_difference "Pirate.count" do
assert_no_difference "Treasure.count" do
@@ -481,12 +519,12 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
join_records = Parrot.connection.select_all("SELECT * FROM parrots_pirates WHERE parrot_id = #{george.id}")
- assert join_records.empty?
- assert george.pirates(true).empty?
+ assert_empty join_records
+ assert_empty george.pirates.reload
join_records = Parrot.connection.select_all("SELECT * FROM parrots_treasures WHERE parrot_id = #{george.id}")
- assert join_records.empty?
- assert george.treasures(true).empty?
+ assert_empty join_records
+ assert_empty george.treasures.reload
end
def test_associations_with_conditions
@@ -518,9 +556,9 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
developer = project.developers.first
- assert_no_queries(ignore_none: false) do
- assert project.developers.loaded?
- assert project.developers.include?(developer)
+ assert_no_queries do
+ assert_predicate project.developers, :loaded?
+ assert_includes project.developers, developer
end
end
@@ -529,19 +567,19 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
developer = project.developers.first
project.reload
- assert ! project.developers.loaded?
+ assert_not_predicate project.developers, :loaded?
assert_queries(1) do
- assert project.developers.include?(developer)
+ assert_includes project.developers, developer
end
- assert ! project.developers.loaded?
+ assert_not_predicate project.developers, :loaded?
end
def test_include_returns_false_for_non_matching_record_to_verify_scoping
project = projects(:active_record)
- developer = Developer.create :name => "Bryan", :salary => 50_000
+ developer = Developer.create name: "Bryan", salary: 50_000
- assert ! project.developers.loaded?
- assert ! project.developers.include?(developer)
+ assert_not_predicate project.developers, :loaded?
+ assert_not project.developers.include?(developer)
end
def test_find_with_merged_options
@@ -552,32 +590,32 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_dynamic_find_should_respect_association_order
# Developers are ordered 'name DESC, id DESC'
- high_id_jamis = projects(:active_record).developers.create(:name => 'Jamis')
+ high_id_jamis = projects(:active_record).developers.create(name: "Jamis")
- assert_equal high_id_jamis, projects(:active_record).developers.merge(:where => "name = 'Jamis'").first
- assert_equal high_id_jamis, projects(:active_record).developers.find_by_name('Jamis')
+ assert_equal high_id_jamis, projects(:active_record).developers.merge(where: "name = 'Jamis'").first
+ assert_equal high_id_jamis, projects(:active_record).developers.find_by_name("Jamis")
end
def test_find_should_append_to_association_order
- ordered_developers = projects(:active_record).developers.order('projects.id')
- assert_equal ['developers.name desc, developers.id desc', 'projects.id'], ordered_developers.order_values
+ ordered_developers = projects(:active_record).developers.order("projects.id")
+ assert_equal ["developers.name desc, developers.id desc", "projects.id"], ordered_developers.order_values
end
def test_dynamic_find_all_should_respect_readonly_access
- projects(:active_record).readonly_developers.each { |d| assert_raise(ActiveRecord::ReadOnlyRecord) { d.save! } if d.valid?}
+ projects(:active_record).readonly_developers.each { |d| assert_raise(ActiveRecord::ReadOnlyRecord) { d.save! } if d.valid? }
projects(:active_record).readonly_developers.each(&:readonly?)
end
def test_new_with_values_in_collection
- jamis = DeveloperForProjectWithAfterCreateHook.find_by_name('Jamis')
- david = DeveloperForProjectWithAfterCreateHook.find_by_name('David')
- project = ProjectWithAfterCreateHook.new(:name => "Cooking with Bertie")
+ jamis = DeveloperForProjectWithAfterCreateHook.find_by_name("Jamis")
+ david = DeveloperForProjectWithAfterCreateHook.find_by_name("David")
+ project = ProjectWithAfterCreateHook.new(name: "Cooking with Bertie")
project.developers << jamis
project.save!
project.reload
- assert project.developers.include?(jamis)
- assert project.developers.include?(david)
+ assert_includes project.developers, jamis
+ assert_includes project.developers, david
end
def test_find_in_association_with_options
@@ -588,8 +626,8 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
def test_association_with_extend_option
- eponine = DeveloperWithExtendOption.create(name: 'Eponine')
- assert_equal 'sns', eponine.projects.category
+ eponine = DeveloperWithExtendOption.create(name: "Eponine")
+ assert_equal "sns", eponine.projects.category
end
def test_replace_with_less
@@ -604,7 +642,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
david.projects = [projects(:action_controller), Project.new("name" => "ActionWebSearch")]
david.save
assert_equal 2, david.projects.length
- assert !david.projects.include?(projects(:active_record))
+ assert_not_includes david.projects, projects(:active_record)
end
def test_replace_on_new_object
@@ -622,9 +660,9 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
developer.special_projects << special_project
developer.reload
- assert developer.projects.include?(special_project)
- assert developer.special_projects.include?(special_project)
- assert !developer.special_projects.include?(other_project)
+ assert_includes developer.projects, special_project
+ assert_includes developer.special_projects, special_project
+ assert_not_includes developer.special_projects, other_project
end
def test_symbol_join_table
@@ -634,7 +672,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_includes developer.sym_special_projects, sp
end
- def test_update_attributes_after_push_without_duplicate_join_table_rows
+ def test_update_columns_after_push_without_duplicate_join_table_rows
developer = Developer.new("name" => "Kano")
project = SpecialProject.create("name" => "Special Project")
assert developer.save
@@ -654,7 +692,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
def test_habtm_respects_select
- categories(:technology).select_testing_posts(true).each do |o|
+ categories(:technology).select_testing_posts.reload.each do |o|
assert_respond_to o, :correctness_marker
end
assert_respond_to categories(:technology).select_testing_posts.first, :correctness_marker
@@ -665,28 +703,17 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
def test_habtm_respects_select_query_method
- assert_equal ['id'], developers(:david).projects.select(:id).first.attributes.keys
+ assert_equal ["id"], developers(:david).projects.select(:id).first.attributes.keys
end
def test_join_table_alias
- # FIXME: `references` has no impact on the aliases generated for the join
- # query. The fact that we pass `:developers_projects_join` to `references`
- # and that the SQL string contains `developers_projects_join` is merely a
- # coincidence.
assert_equal(
3,
- Developer.references(:developers_projects_join).merge(
- :includes => {:projects => :developers},
- :where => 'projects_developers_projects_join.joined_on IS NOT NULL'
- ).to_a.size
+ Developer.includes(projects: :developers).where.not("projects_developers_projects_join.joined_on": nil).to_a.size
)
end
def test_join_with_group
- # FIXME: `references` has no impact on the aliases generated for the join
- # query. The fact that we pass `:developers_projects_join` to `references`
- # and that the SQL string contains `developers_projects_join` is merely a
- # coincidence.
group = Developer.columns.inject([]) do |g, c|
g << "developers.#{c.name}"
g << "developers_projects_2.#{c.name}"
@@ -695,16 +722,13 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal(
3,
- Developer.references(:developers_projects_join).merge(
- :includes => {:projects => :developers}, :where => 'projects_developers_projects_join.joined_on IS NOT NULL',
- :group => group.join(",")
- ).to_a.size
+ Developer.includes(projects: :developers).where.not("projects_developers_projects_join.joined_on": nil).group(group.join(",")).to_a.size
)
end
def test_find_grouped
- all_posts_from_category1 = Post.all.merge!(:where => "category_id = 1", :joins => :categories).to_a
- grouped_posts_of_category1 = Post.all.merge!(:where => "category_id = 1", :group => "author_id", :select => 'count(posts.id) as posts_count', :joins => :categories).to_a
+ all_posts_from_category1 = Post.all.merge!(where: "category_id = 1", joins: :categories).to_a
+ grouped_posts_of_category1 = Post.all.merge!(where: "category_id = 1", group: "author_id", select: "count(posts.id) as posts_count", joins: :categories).to_a
assert_equal 5, all_posts_from_category1.size
assert_equal 2, grouped_posts_of_category1.size
end
@@ -715,8 +739,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
@@ -726,8 +750,8 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_get_ids_for_loaded_associations
developer = developers(:david)
- developer.projects(true)
- assert_queries(0) do
+ developer.projects.reload
+ assert_no_queries do
developer.project_ids
developer.project_ids
end
@@ -735,9 +759,9 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_get_ids_for_unloaded_associations_does_not_load_them
developer = developers(:david)
- assert !developer.projects.loaded?
+ assert_not_predicate developer.projects, :loaded?
assert_equal projects(:active_record, :action_controller).map(&:id).sort, developer.project_ids.sort
- assert !developer.projects.loaded?
+ assert_not_predicate developer.projects, :loaded?
end
def test_assign_ids
@@ -751,13 +775,23 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_assign_ids_ignoring_blanks
developer = Developer.new("name" => "Joe")
- developer.project_ids = [projects(:active_record).id, nil, projects(:action_controller).id, '']
+ developer.project_ids = [projects(:active_record).id, nil, projects(:action_controller).id, ""]
developer.save
developer.reload
assert_equal 2, developer.projects.length
assert_equal [projects(:active_record), projects(:action_controller)].map(&:id).sort, developer.project_ids.sort
end
+ def test_singular_ids_are_reloaded_after_collection_concat
+ student = Student.create(name: "Alberto Almagro")
+ student.lesson_ids
+
+ lesson = Lesson.create(name: "DSI")
+ student.lessons << lesson
+
+ assert_includes student.lesson_ids, lesson.id
+ end
+
def test_scoped_find_on_through_association_doesnt_return_read_only_records
tag = Post.find(1).tags.find_by_name("General")
@@ -767,12 +801,12 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
def test_has_many_through_polymorphic_has_manys_works
- assert_equal [10, 20].to_set, pirates(:redbeard).treasure_estimates.map(&:price).to_set
+ assert_equal ["$10.00", "$20.00"].to_set, pirates(:redbeard).treasure_estimates.map(&:price).to_set
end
def test_symbols_as_keys
- developer = DeveloperWithSymbolsForKeys.new(:name => 'David')
- project = ProjectWithSymbolsForKeys.new(:name => 'Rails Testing')
+ developer = DeveloperWithSymbolsForKeys.new(name: "David")
+ project = ProjectWithSymbolsForKeys.new(name: "Rails Testing")
project.developers << developer
project.save!
@@ -785,7 +819,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_dynamic_find_should_respect_association_include
# SQL error in sort clause if :include is not included
# due to Unknown column 'authors.id'
- assert Category.find(1).posts_with_authors_sorted_by_author_id.find_by_title('Welcome to the weblog')
+ assert Category.find(1).posts_with_authors_sorted_by_author_id.find_by_title("Welcome to the weblog")
end
def test_count
@@ -794,9 +828,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
def test_association_proxy_transaction_method_starts_transaction_in_association_class
- Post.expects(:transaction)
- Category.first.posts.transaction do
- # nothing
+ assert_called(Post, :transaction) do
+ Category.first.posts.transaction do
+ # nothing
+ end
end
end
@@ -815,13 +850,13 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_no_queries { david.projects.columns }
end
- def test_attributes_are_being_set_when_initialized_from_habm_association_with_where_clause
- new_developer = projects(:action_controller).developers.where(:name => "Marcelo").build
+ def test_attributes_are_being_set_when_initialized_from_habtm_association_with_where_clause
+ new_developer = projects(:action_controller).developers.where(name: "Marcelo").build
assert_equal new_developer.name, "Marcelo"
end
- def test_attributes_are_being_set_when_initialized_from_habm_association_with_multiple_where_clauses
- new_developer = projects(:action_controller).developers.where(:name => "Marcelo").where(:salary => 90_000).build
+ def test_attributes_are_being_set_when_initialized_from_habtm_association_with_multiple_where_clauses
+ new_developer = projects(:action_controller).developers.where(name: "Marcelo").where(salary: 90_000).build
assert_equal new_developer.name, "Marcelo"
assert_equal new_developer.salary, 90_000
end
@@ -829,7 +864,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_include_method_in_has_and_belongs_to_many_association_should_return_true_for_instance_added_with_build
project = Project.new
developer = project.developers.build
- assert project.developers.include?(developer)
+ assert_includes project.developers, developer
end
def test_destruction_does_not_error_without_primary_key
@@ -844,9 +879,9 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_has_and_belongs_to_many_associations_on_new_records_use_null_relations
projects = Developer.new.projects
- assert_no_queries(ignore_none: false) do
+ assert_no_queries do
assert_equal [], projects
- assert_equal [], projects.where(title: 'omg')
+ assert_equal [], projects.where(title: "omg")
assert_equal [], projects.pluck(:title)
assert_equal 0, projects.count
end
@@ -860,7 +895,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
treasure.valid?
assert_equal 1, treasure.rich_people.size
- assert_nil rich_person.first_name, 'should not run associated person validation on create when validate: false'
+ assert_nil rich_person.first_name, "should not run associated person validation on create when validate: false"
end
def test_association_with_validate_false_does_not_run_associated_validation_callbacks_on_update
@@ -873,11 +908,11 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
treasure.valid?
assert_equal 1, treasure.rich_people.size
- assert_equal person_first_name, rich_person.first_name, 'should not run associated person validation on update when validate: false'
+ assert_equal person_first_name, rich_person.first_name, "should not run associated person validation on update when validate: false"
end
def test_custom_join_table
- assert_equal 'edges', Vertex.reflect_on_association(:sources).join_table
+ assert_equal "edges", Vertex.reflect_on_association(:sources).join_table
end
def test_has_and_belongs_to_many_in_a_namespaced_model_pointing_to_a_namespaced_model
@@ -901,20 +936,91 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_redefine_habtm
child = SubDeveloper.new("name" => "Aredridel")
child.special_projects << SpecialProject.new("name" => "Special Project")
- assert child.save, 'child object should be saved'
+ assert child.save, "child object should be saved"
end
def test_habtm_with_reflection_using_class_name_and_fixtures
- assert_not_nil Developer._reflections['shared_computers']
+ assert_not_nil Developer._reflections["shared_computers"]
# Checking the fixture for named association is important here, because it's the only way
# we've been able to reproduce this bug
- assert_not_nil File.read(File.expand_path("../../../fixtures/developers.yml", __FILE__)).index("shared_computers")
+ assert_not_nil File.read(File.expand_path("../../fixtures/developers.yml", __dir__)).index("shared_computers")
assert_equal developers(:david).shared_computers.first, computers(:laptop)
end
def test_with_symbol_class_name
- assert_nothing_raised NoMethodError do
- DeveloperWithSymbolClassName.new
+ assert_nothing_raised do
+ developer = DeveloperWithSymbolClassName.new
+ developer.projects
+ end
+ end
+
+ def test_alternate_database
+ professor = Professor.create(name: "Plum")
+ course = Course.create(name: "Forensics")
+ assert_equal 0, professor.courses.count
+ assert_nothing_raised do
+ professor.courses << course
+ end
+ assert_equal 1, professor.courses.count
+ end
+
+ def test_habtm_scope_can_unscope
+ project = ProjectUnscopingDavidDefaultScope.new
+ project.save!
+
+ developer = LazyBlockDeveloperCalledDavid.new(name: "Not David")
+ developer.save!
+ project.developers << developer
+
+ projects = ProjectUnscopingDavidDefaultScope.includes(:developers).where(id: project.id)
+ assert_equal 1, projects.first.developers.size
+ end
+
+ def test_preloaded_associations_size
+ assert_equal Project.first.salaried_developers.size,
+ Project.preload(:salaried_developers).first.salaried_developers.size
+
+ assert_equal Project.includes(:salaried_developers).references(:salaried_developers).first.salaried_developers.size,
+ Project.preload(:salaried_developers).first.salaried_developers.size
+
+ # Nested HATBM
+ first_project = Developer.first.projects.first
+ preloaded_first_project =
+ Developer.preload(projects: :salaried_developers).
+ first.
+ projects.
+ detect { |p| p.id == first_project.id }
+
+ assert preloaded_first_project.salaried_developers.loaded?, true
+ assert_equal first_project.salaried_developers.size, preloaded_first_project.salaried_developers.size
+ end
+
+ def test_has_and_belongs_to_many_is_useable_with_belongs_to_required_by_default
+ assert_difference "Project.first.developers_required_by_default.size", 1 do
+ Project.first.developers_required_by_default.create!(name: "Sean", salary: 50000)
+ end
+ end
+
+ def test_association_name_is_the_same_as_join_table_name
+ user = User.create!
+ assert_nothing_raised { user.jobs_pool.clear }
+ end
+
+ def test_has_and_belongs_to_many_while_partial_writes_false
+ begin
+ original_partial_writes = ActiveRecord::Base.partial_writes
+ ActiveRecord::Base.partial_writes = false
+ developer = Developer.new(name: "Mehmet Emin İNAÇ")
+ developer.projects << Project.new(name: "Bounty")
+
+ assert developer.save
+ ensure
+ ActiveRecord::Base.partial_writes = original_partial_writes
end
end
+
+ def test_has_and_belongs_to_many_with_belongs_to
+ sink = Sink.create! kitchen: Kitchen.new, sources: [Source.new]
+ assert_equal 1, sink.sources.count
+ end
end
diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb
index 2c4e2a875c..d13e1a86e9 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -1,94 +1,108 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/developer'
-require 'models/computer'
-require 'models/project'
-require 'models/company'
-require 'models/contract'
-require 'models/topic'
-require 'models/reply'
-require 'models/category'
-require 'models/image'
-require 'models/post'
-require 'models/author'
-require 'models/essay'
-require 'models/comment'
-require 'models/person'
-require 'models/reader'
-require 'models/tagging'
-require 'models/tag'
-require 'models/invoice'
-require 'models/line_item'
-require 'models/car'
-require 'models/bulb'
-require 'models/engine'
-require 'models/categorization'
-require 'models/minivan'
-require 'models/speedometer'
-require 'models/reference'
-require 'models/job'
-require 'models/college'
-require 'models/student'
-require 'models/pirate'
-require 'models/ship'
-require 'models/ship_part'
-require 'models/tyre'
-require 'models/subscriber'
-require 'models/subscription'
-require 'models/zine'
-require 'models/interest'
+require "models/developer"
+require "models/computer"
+require "models/project"
+require "models/company"
+require "models/contract"
+require "models/topic"
+require "models/reply"
+require "models/category"
+require "models/image"
+require "models/post"
+require "models/author"
+require "models/essay"
+require "models/comment"
+require "models/person"
+require "models/reader"
+require "models/tagging"
+require "models/tag"
+require "models/invoice"
+require "models/line_item"
+require "models/car"
+require "models/bulb"
+require "models/engine"
+require "models/categorization"
+require "models/minivan"
+require "models/speedometer"
+require "models/reference"
+require "models/college"
+require "models/student"
+require "models/pirate"
+require "models/ship"
+require "models/ship_part"
+require "models/treasure"
+require "models/parrot"
+require "models/tyre"
+require "models/subscriber"
+require "models/subscription"
+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)
# this can fail on adapters which require ORDER BY expressions to be included in the SELECT expression
# if the reorder clauses are not correctly handled
- assert author.posts_with_comments_sorted_by_comment_id.where('comments.id > 0').reorder('posts.comments_count DESC', 'posts.tags_count DESC').last
+ assert author.posts_with_comments_sorted_by_comment_id.where("comments.id > 0").reorder("posts.comments_count DESC", "posts.tags_count DESC").last
end
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')
- assert !subscriber.subscriptions.loaded?
+ subscriber = Subscriber.new(nick: "webster132")
+ assert_not_predicate subscriber.subscriptions, :loaded?
assert_queries 1 do
assert_equal 2, subscriber.subscriptions.size
end
- assert_equal subscriber.subscriptions, Subscription.where(subscriber_id: 'webster132')
+ assert_equal Subscription.where(subscriber_id: "webster132"), subscriber.subscriptions
end
def test_association_primary_key_on_new_record_should_fetch_with_query
- author = Author.new(:name => "David")
- assert !author.essays.loaded?
+ author = Author.new(name: "David")
+ assert_not_predicate author.essays, :loaded?
assert_queries 1 do
assert_equal 1, author.essays.size
end
- assert_equal author.essays, Essay.where(writer_id: "David")
+ assert_equal Essay.where(writer_id: "David"), author.essays
end
def test_has_many_custom_primary_key
david = authors(:david)
- assert_equal david.essays, Essay.where(writer_id: "David")
+ assert_equal Essay.where(writer_id: "David"), david.essays
+ end
+
+ def test_ids_on_unloaded_association_with_custom_primary_key
+ david = people(:david)
+ assert_equal Essay.where(writer_id: "David").pluck(:id), david.essay_ids
+ end
+
+ def test_ids_on_loaded_association_with_custom_primary_key
+ david = people(:david)
+ david.essays.load
+ assert_equal Essay.where(writer_id: "David").pluck(:id), david.essay_ids
end
def test_has_many_assignment_with_custom_primary_key
david = people(:david)
assert_equal ["A Modest Proposal"], david.essays.map(&:name)
- david.essays = [Essay.create!(name: "Remote Work" )]
+ david.essays = [Essay.create!(name: "Remote Work")]
assert_equal ["Remote Work"], david.essays.map(&:name)
end
def test_blank_custom_primary_key_on_new_record_should_not_run_queries
author = Author.new
- assert !author.essays.loaded?
+ assert_not_predicate author.essays, :loaded?
assert_queries 0 do
assert_equal 0, author.essays.size
@@ -98,8 +112,8 @@ end
class HasManyAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :categories, :companies, :developers, :projects,
- :developers_projects, :topics, :authors, :comments,
- :posts, :readers, :taggings, :cars, :jobs, :tags,
+ :developers_projects, :topics, :authors, :author_addresses, :comments,
+ :posts, :readers, :taggings, :cars, :tags,
:categorizations, :zines, :interests
def setup
@@ -114,14 +128,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_anonymous_has_many
developer = Class.new(ActiveRecord::Base) {
- self.table_name = 'developers'
+ self.table_name = "developers"
dev = self
developer_project = Class.new(ActiveRecord::Base) {
- self.table_name = 'developers_projects'
- belongs_to :developer, :anonymous_class => dev
+ self.table_name = "developers_projects"
+ belongs_to :developer, anonymous_class: dev
}
- has_many :developer_projects, :anonymous_class => developer_project, :foreign_key => 'developer_id'
+ has_many :developer_projects, anonymous_class: developer_project, foreign_key: "developer_id"
}
dev = developer.first
named = Developer.find(dev.id)
@@ -133,20 +147,20 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_default_scope_on_relations_is_not_cached
counter = 0
posts = Class.new(ActiveRecord::Base) {
- self.table_name = 'posts'
- self.inheritance_column = 'not_there'
+ self.table_name = "posts"
+ self.inheritance_column = "not_there"
post = self
comments = Class.new(ActiveRecord::Base) {
- self.table_name = 'comments'
- self.inheritance_column = 'not_there'
- belongs_to :post, :anonymous_class => post
+ self.table_name = "comments"
+ self.inheritance_column = "not_there"
+ belongs_to :post, anonymous_class: post
default_scope -> {
counter += 1
- where("id = :inc", :inc => counter)
+ where("id = :inc", inc: counter)
}
}
- has_many :comments, :anonymous_class => comments, :foreign_key => 'post_id'
+ has_many :comments, anonymous_class: comments, foreign_key: "post_id"
}
assert_equal 0, counter
post = posts.first
@@ -157,18 +171,20 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_has_many_build_with_options
- college = College.create(name: 'UFMT')
- Student.create(active: true, college_id: college.id, name: 'Sarah')
+ college = College.create(name: "UFMT")
+ Student.create(active: true, college_id: college.id, name: "Sarah")
assert_equal college.students, Student.where(active: true, college_id: college.id)
end
def test_add_record_to_collection_should_change_its_updated_at
- ship = Ship.create(name: 'dauntless')
- part = ShipPart.create(name: 'cockpit')
+ ship = Ship.create(name: "dauntless")
+ part = ShipPart.create(name: "cockpit")
updated_at = part.updated_at
- ship.parts << part
+ travel(1.second) do
+ ship.parts << part
+ end
assert_equal part.ship, ship
assert_not_equal part.updated_at, updated_at
@@ -177,67 +193,88 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_clear_collection_should_not_change_updated_at
# GH#17161: .clear calls delete_all (and returns the association),
# which is intended to not touch associated objects's updated_at field
- ship = Ship.create(name: 'dauntless')
- part = ShipPart.create(name: 'cockpit', ship_id: ship.id)
+ ship = Ship.create(name: "dauntless")
+ part = ShipPart.create(name: "cockpit", ship_id: ship.id)
ship.parts.clear
part.reload
- assert_equal nil, part.ship
- assert !part.updated_at_changed?
+ assert_nil part.ship
+ assert_not_predicate part, :updated_at_changed?
end
def test_create_from_association_should_respect_default_scope
- car = Car.create(:name => 'honda')
- assert_equal 'honda', car.name
+ car = Car.create(name: "honda")
+ assert_equal "honda", car.name
bulb = Bulb.create
- assert_equal 'defaulty', bulb.name
+ assert_equal "defaulty", bulb.name
bulb = car.bulbs.build
- assert_equal 'defaulty', bulb.name
+ assert_equal "defaulty", bulb.name
bulb = car.bulbs.create
- assert_equal 'defaulty', bulb.name
+ assert_equal "defaulty", bulb.name
+ end
+
+ def test_build_and_create_from_association_should_respect_passed_attributes_over_default_scope
+ car = Car.create(name: "honda")
+
+ bulb = car.bulbs.build(name: "exotic")
+ assert_equal "exotic", bulb.name
- bulb = car.bulbs.create(:name => 'exotic')
- assert_equal 'exotic', bulb.name
+ bulb = car.bulbs.create(name: "exotic")
+ assert_equal "exotic", bulb.name
+
+ bulb = car.awesome_bulbs.build(frickinawesome: false)
+ assert_equal false, bulb.frickinawesome
+
+ bulb = car.awesome_bulbs.create(frickinawesome: false)
+ assert_equal false, bulb.frickinawesome
end
def test_build_from_association_should_respect_scope
author = Author.new
post = author.thinking_posts.build
- assert_equal 'So I was thinking', post.title
+ assert_equal "So I was thinking", post.title
end
def test_create_from_association_with_nil_values_should_work
- car = Car.create(:name => 'honda')
+ car = Car.create(name: "honda")
bulb = car.bulbs.new(nil)
- assert_equal 'defaulty', bulb.name
+ assert_equal "defaulty", bulb.name
bulb = car.bulbs.build(nil)
- assert_equal 'defaulty', bulb.name
+ assert_equal "defaulty", bulb.name
bulb = car.bulbs.create(nil)
- assert_equal 'defaulty', bulb.name
+ assert_equal "defaulty", bulb.name
+ end
+
+ def test_build_from_association_sets_inverse_instance
+ car = Car.new(name: "honda")
+
+ bulb = car.bulbs.build
+ assert_equal car, bulb.car
end
def test_do_not_call_callbacks_for_delete_all
- car = Car.create(:name => 'honda')
+ car = Car.create(name: "honda")
car.funky_bulbs.create!
+ assert_equal 1, car.funky_bulbs.count
assert_nothing_raised { car.reload.funky_bulbs.delete_all }
- assert_equal 0, Bulb.count, "bulbs should have been deleted using :delete_all strategy"
+ assert_equal 0, car.funky_bulbs.count, "bulbs should have been deleted using :delete_all strategy"
end
def test_delete_all_on_association_is_the_same_as_not_loaded
author = authors :david
- author.thinking_posts.create!(:body => "test")
+ author.thinking_posts.create!(body: "test")
author.reload
expected_sql = capture_sql { author.thinking_posts.delete_all }
- author.thinking_posts.create!(:body => "test")
+ author.thinking_posts.create!(body: "test")
author.reload
author.thinking_posts.inspect
loaded_sql = capture_sql { author.thinking_posts.delete_all }
@@ -246,11 +283,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_delete_all_on_association_with_nil_dependency_is_the_same_as_not_loaded
author = authors :david
- author.posts.create!(:title => "test", :body => "body")
+ author.posts.create!(title: "test", body: "body")
author.reload
expected_sql = capture_sql { author.posts.delete_all }
- author.posts.create!(:title => "test", :body => "body")
+ author.posts.create!(title: "test", body: "body")
author.reload
author.posts.to_a
loaded_sql = capture_sql { author.posts.delete_all }
@@ -265,29 +302,29 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_building_the_associated_object_with_explicit_sti_base_class
firm = DependentFirm.new
- company = firm.companies.build(:type => "Company")
+ company = firm.companies.build(type: "Company")
assert_kind_of Company, company, "Expected #{company.class} to be a Company"
end
def test_building_the_associated_object_with_sti_subclass
firm = DependentFirm.new
- company = firm.companies.build(:type => "Client")
+ company = firm.companies.build(type: "Client")
assert_kind_of Client, company, "Expected #{company.class} to be a Client"
end
def test_building_the_associated_object_with_an_invalid_type
firm = DependentFirm.new
- assert_raise(ActiveRecord::SubclassNotFound) { firm.companies.build(:type => "Invalid") }
+ assert_raise(ActiveRecord::SubclassNotFound) { firm.companies.build(type: "Invalid") }
end
def test_building_the_associated_object_with_an_unrelated_type
firm = DependentFirm.new
- assert_raise(ActiveRecord::SubclassNotFound) { firm.companies.build(:type => "Account") }
+ assert_raise(ActiveRecord::SubclassNotFound) { firm.companies.build(type: "Account") }
end
test "building the association with an array" do
speedometer = Speedometer.new(speedometer_id: "a")
- data = [{name: "first"}, {name: "second"}]
+ data = [{ name: "first" }, { name: "second" }]
speedometer.minivans.build(data)
assert_equal 2, speedometer.minivans.size
@@ -296,24 +333,24 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_association_keys_bypass_attribute_protection
- car = Car.create(:name => 'honda')
+ car = Car.create(name: "honda")
bulb = car.bulbs.new
assert_equal car.id, bulb.car_id
- bulb = car.bulbs.new :car_id => car.id + 1
+ bulb = car.bulbs.new car_id: car.id + 1
assert_equal car.id, bulb.car_id
bulb = car.bulbs.build
assert_equal car.id, bulb.car_id
- bulb = car.bulbs.build :car_id => car.id + 1
+ bulb = car.bulbs.build car_id: car.id + 1
assert_equal car.id, bulb.car_id
bulb = car.bulbs.create
assert_equal car.id, bulb.car_id
- bulb = car.bulbs.create :car_id => car.id + 1
+ bulb = car.bulbs.create car_id: car.id + 1
assert_equal car.id, bulb.car_id
end
@@ -323,22 +360,43 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
line_item = invoice.line_items.new
assert_equal invoice.id, line_item.invoice_id
- line_item = invoice.line_items.new :invoice_id => invoice.id + 1
+ line_item = invoice.line_items.new invoice_id: invoice.id + 1
assert_equal invoice.id, line_item.invoice_id
line_item = invoice.line_items.build
assert_equal invoice.id, line_item.invoice_id
- line_item = invoice.line_items.build :invoice_id => invoice.id + 1
+ line_item = invoice.line_items.build invoice_id: invoice.id + 1
assert_equal invoice.id, line_item.invoice_id
line_item = invoice.line_items.create
assert_equal invoice.id, line_item.invoice_id
- line_item = invoice.line_items.create :invoice_id => invoice.id + 1
+ line_item = invoice.line_items.create invoice_id: invoice.id + 1
assert_equal invoice.id, line_item.invoice_id
end
+ class SpecialAuthor < ActiveRecord::Base
+ self.table_name = "authors"
+ has_many :books, class_name: "SpecialBook", foreign_key: :author_id
+ end
+
+ class SpecialBook < ActiveRecord::Base
+ self.table_name = "books"
+
+ belongs_to :author
+ enum read_status: { unread: 0, reading: 2, read: 3, forgotten: nil }
+ end
+
+ def test_association_enum_works_properly
+ author = SpecialAuthor.create!(name: "Test")
+ book = SpecialBook.create!(read_status: "reading")
+ author.books << book
+
+ assert_equal "reading", book.read_status
+ assert_not_equal 0, SpecialAuthor.joins(:books).where(books: { read_status: "reading" }).count
+ end
+
# When creating objects on the association, we must not do it within a scope (even though it
# would be convenient), because this would cause that scope to be applied to any callbacks etc.
def test_build_and_create_should_not_happen_within_scope
@@ -356,54 +414,106 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_no_sql_should_be_fired_if_association_already_loaded
- Car.create(:name => 'honda')
+ Car.create(name: "honda")
bulbs = Car.first.bulbs
bulbs.to_a # to load all instances of bulbs
assert_no_queries do
bulbs.first()
- bulbs.first({})
end
assert_no_queries do
bulbs.second()
- bulbs.second({})
end
assert_no_queries do
bulbs.third()
- bulbs.third({})
end
assert_no_queries do
bulbs.fourth()
- bulbs.fourth({})
end
assert_no_queries do
bulbs.fifth()
- bulbs.fifth({})
end
assert_no_queries do
bulbs.forty_two()
- bulbs.forty_two({})
+ end
+
+ assert_no_queries do
+ bulbs.third_to_last()
+ end
+
+ assert_no_queries do
+ bulbs.second_to_last()
end
assert_no_queries do
bulbs.last()
- bulbs.last({})
+ end
+ end
+
+ def test_finder_method_with_dirty_target
+ company = companies(:first_firm)
+ new_clients = []
+
+ # Load schema information so we don't query below if running just this test.
+ Client.define_attribute_methods
+
+ assert_no_queries do
+ new_clients << company.clients_of_firm.build(name: "Another Client")
+ new_clients << company.clients_of_firm.build(name: "Another Client II")
+ new_clients << company.clients_of_firm.build(name: "Another Client III")
+ end
+
+ assert_not_predicate company.clients_of_firm, :loaded?
+ assert_queries(1) do
+ assert_same new_clients[0], company.clients_of_firm.third
+ assert_same new_clients[1], company.clients_of_firm.fourth
+ assert_same new_clients[2], company.clients_of_firm.fifth
+ assert_same new_clients[0], company.clients_of_firm.third_to_last
+ assert_same new_clients[1], company.clients_of_firm.second_to_last
+ assert_same new_clients[2], company.clients_of_firm.last
+ end
+ end
+
+ def test_finder_bang_method_with_dirty_target
+ company = companies(:first_firm)
+ new_clients = []
+
+ # Load schema information so we don't query below if running just this test.
+ Client.define_attribute_methods
+
+ assert_no_queries do
+ new_clients << company.clients_of_firm.build(name: "Another Client")
+ new_clients << company.clients_of_firm.build(name: "Another Client II")
+ new_clients << company.clients_of_firm.build(name: "Another Client III")
+ end
+
+ assert_not_predicate company.clients_of_firm, :loaded?
+ assert_queries(1) do
+ assert_same new_clients[0], company.clients_of_firm.third!
+ assert_same new_clients[1], company.clients_of_firm.fourth!
+ assert_same new_clients[2], company.clients_of_firm.fifth!
+ assert_same new_clients[0], company.clients_of_firm.third_to_last!
+ assert_same new_clients[1], company.clients_of_firm.second_to_last!
+ assert_same new_clients[2], company.clients_of_firm.last!
end
end
def test_create_resets_cached_counters
- person = Person.create!(:first_name => 'tenderlove')
+ Reader.delete_all
+
+ person = Person.create!(first_name: "tenderlove")
+
post = Post.first
assert_equal [], person.readers
assert_nil person.readers.find_by_post_id(post.id)
- person.readers.create(:post_id => post.id)
+ person.readers.create(post_id: post.id)
assert_equal 1, person.readers.count
assert_equal 1, person.readers.length
@@ -411,25 +521,36 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal person, person.readers.first.person
end
- def force_signal37_to_load_all_clients_of_firm
- companies(:first_firm).clients_of_firm.each {|f| }
+ def test_update_all_respects_association_scope
+ person = Person.new
+ person.first_name = "Naruto"
+ person.references << Reference.new
+ person.save!
+ assert_equal 1, person.references.update_all(favourite: true)
+ end
+
+ def test_exists_respects_association_scope
+ person = Person.new
+ person.first_name = "Sasuke"
+ person.references << Reference.new
+ person.save!
+ assert_predicate person.references, :exists?
end
- # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first
def test_counting_with_counter_sql
- assert_equal 3, Firm.all.merge!(:order => "id").first.clients.count
+ assert_equal 3, Firm.first.clients.count
end
def test_counting
- assert_equal 3, Firm.all.merge!(:order => "id").first.plain_clients.count
+ assert_equal 3, Firm.first.plain_clients.count
end
def test_counting_with_single_hash
- assert_equal 1, Firm.all.merge!(:order => "id").first.plain_clients.where(:name => "Microsoft").count
+ assert_equal 1, Firm.first.plain_clients.where(name: "Microsoft").count
end
def test_counting_with_column_name_and_hash
- assert_equal 3, Firm.all.merge!(:order => "id").first.plain_clients.count(:name)
+ assert_equal 3, Firm.first.plain_clients.count(:name)
end
def test_counting_with_association_limit
@@ -439,11 +560,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_finding
- assert_equal 3, Firm.all.merge!(:order => "id").first.clients.length
+ assert_equal 3, Firm.first.clients.length
end
def test_finding_array_compatibility
- assert_equal 3, Firm.order(:id).find{|f| f.id > 0}.clients.length
+ assert_equal 3, Firm.order(:id).find { |f| f.id > 0 }.clients.length
end
def test_find_many_with_merged_options
@@ -453,13 +574,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_find_should_append_to_association_order
- ordered_clients = companies(:first_firm).clients_sorted_desc.order('companies.id')
- assert_equal ['id DESC', 'companies.id'], ordered_clients.order_values
+ ordered_clients = companies(:first_firm).clients_sorted_desc.order("companies.id")
+ assert_equal ["id DESC", "companies.id"], ordered_clients.order_values
end
def test_dynamic_find_should_respect_association_order
assert_equal companies(:another_first_firm_client), companies(:first_firm).clients_sorted_desc.where("type = 'Client'").first
- assert_equal companies(:another_first_firm_client), companies(:first_firm).clients_sorted_desc.find_by_type('Client')
+ assert_equal companies(:another_first_firm_client), companies(:first_firm).clients_sorted_desc.find_by_type("Client")
end
def test_taking
@@ -479,16 +600,28 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_taking_with_a_number
+ klass = Class.new(Author) do
+ has_many :posts, -> { order(:id) }
+
+ def self.name
+ "Author"
+ end
+ end
+
# taking from unloaded Relation
- bob = Author.find(authors(:bob).id)
+ bob = klass.find(authors(:bob).id)
+ new_post = bob.posts.build
+ assert_not_predicate bob.posts, :loaded?
assert_equal [posts(:misc_by_bob)], bob.posts.take(1)
- bob = Author.find(authors(:bob).id)
assert_equal [posts(:misc_by_bob), posts(:other_by_bob)], bob.posts.take(2)
+ assert_equal [posts(:misc_by_bob), posts(:other_by_bob), new_post], bob.posts.take(3)
# taking from loaded Relation
- bob.posts.to_a
- assert_equal [posts(:misc_by_bob)], authors(:bob).posts.take(1)
- assert_equal [posts(:misc_by_bob), posts(:other_by_bob)], authors(:bob).posts.take(2)
+ bob.posts.load
+ assert_predicate bob.posts, :loaded?
+ assert_equal [posts(:misc_by_bob)], bob.posts.take(1)
+ assert_equal [posts(:misc_by_bob), posts(:other_by_bob)], bob.posts.take(2)
+ assert_equal [posts(:misc_by_bob), posts(:other_by_bob), new_post], bob.posts.take(3)
end
def test_taking_with_inverse_of
@@ -507,46 +640,41 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_finding_default_orders
- assert_equal "Summit", Firm.all.merge!(:order => "id").first.clients.first.name
+ assert_equal "Summit", Firm.first.clients.first.name
end
def test_finding_with_different_class_name_and_order
- assert_equal "Apex", Firm.all.merge!(:order => "id").first.clients_sorted_desc.first.name
+ assert_equal "Apex", Firm.first.clients_sorted_desc.first.name
end
def test_finding_with_foreign_key
- assert_equal "Microsoft", Firm.all.merge!(:order => "id").first.clients_of_firm.first.name
+ assert_equal "Microsoft", Firm.first.clients_of_firm.first.name
end
def test_finding_with_condition
- assert_equal "Microsoft", Firm.all.merge!(:order => "id").first.clients_like_ms.first.name
+ assert_equal "Microsoft", Firm.first.clients_like_ms.first.name
end
def test_finding_with_condition_hash
- assert_equal "Microsoft", Firm.all.merge!(:order => "id").first.clients_like_ms_with_hash_conditions.first.name
+ assert_equal "Microsoft", Firm.first.clients_like_ms_with_hash_conditions.first.name
end
def test_finding_using_primary_key
- assert_equal "Summit", Firm.all.merge!(:order => "id").first.clients_using_primary_key.first.name
+ assert_equal "Summit", Firm.first.clients_using_primary_key.first.name
end
def test_update_all_on_association_accessed_before_save
- firm = Firm.new(name: 'Firm')
- clients_proxy_id = firm.clients.object_id
+ firm = Firm.new(name: "Firm")
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
+ assert_equal firm.clients.count, firm.clients.update_all(description: "Great!")
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 = Firm.new(name: "Firm", id: 100)
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
+ assert_equal firm.clients.count, firm.clients.update_all(description: "Great!")
end
def test_belongs_to_sanity
@@ -555,7 +683,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_find_ids
- firm = Firm.all.merge!(:order => "id").first
+ firm = Firm.first
assert_raise(ActiveRecord::RecordNotFound) { firm.clients.find }
@@ -574,9 +702,23 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::RecordNotFound) { firm.clients.find(2, 99) }
end
+ def test_find_one_message_on_primary_key
+ firm = Firm.first
+
+ e = assert_raises(ActiveRecord::RecordNotFound) do
+ firm.clients.find(0)
+ end
+ assert_equal 0, e.id
+ assert_equal "id", e.primary_key
+ assert_equal "Client", e.model
+ assert_match (/\ACouldn't find Client with 'id'=0/), e.message
+ end
+
def test_find_ids_and_inverse_of
force_signal37_to_load_all_clients_of_firm
+ assert_predicate companies(:first_firm).clients_of_firm, :loaded?
+
firm = companies(:first_firm)
client = firm.clients_of_firm.find(3)
assert_kind_of Client, client
@@ -587,7 +729,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_find_all
- firm = Firm.all.merge!(:order => "id").first
+ firm = Firm.first
assert_equal 3, firm.clients.where("#{QUOTED_TYPE} = 'Client'").to_a.length
assert_equal 1, firm.clients.where("name = 'Summit'").to_a.length
end
@@ -595,67 +737,143 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_find_each
firm = companies(:first_firm)
- assert ! firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
assert_queries(4) do
- firm.clients.find_each(:batch_size => 1) {|c| assert_equal firm.id, c.firm_id }
+ firm.clients.find_each(batch_size: 1) { |c| assert_equal firm.id, c.firm_id }
end
- assert ! firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
end
def test_find_each_with_conditions
firm = companies(:first_firm)
assert_queries(2) do
- firm.clients.where(name: 'Microsoft').find_each(batch_size: 1) do |c|
+ firm.clients.where(name: "Microsoft").find_each(batch_size: 1) do |c|
assert_equal firm.id, c.firm_id
assert_equal "Microsoft", c.name
end
end
- assert ! firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
end
def test_find_in_batches
firm = companies(:first_firm)
- assert ! firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
assert_queries(2) do
- firm.clients.find_in_batches(:batch_size => 2) do |clients|
- clients.each {|c| assert_equal firm.id, c.firm_id }
+ firm.clients.find_in_batches(batch_size: 2) do |clients|
+ clients.each { |c| assert_equal firm.id, c.firm_id }
end
end
- assert ! firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
end
def test_find_all_sanitized
- # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first
- firm = Firm.all.merge!(:order => "id").first
+ firm = Firm.first
summit = firm.clients.where("name = 'Summit'").to_a
assert_equal summit, firm.clients.where("name = ?", "Summit").to_a
- assert_equal summit, firm.clients.where("name = :name", { :name => "Summit" }).to_a
+ assert_equal summit, firm.clients.where("name = :name", name: "Summit").to_a
end
def test_find_first
- firm = Firm.all.merge!(:order => "id").first
+ firm = Firm.first
client2 = Client.find(2)
assert_equal firm.clients.first, firm.clients.order("id").first
assert_equal client2, firm.clients.where("#{QUOTED_TYPE} = 'Client'").order("id").first
end
def test_find_first_sanitized
- firm = Firm.all.merge!(:order => "id").first
+ firm = Firm.first
client2 = Client.find(2)
- assert_equal client2, firm.clients.merge!(:where => ["#{QUOTED_TYPE} = ?", 'Client'], :order => "id").first
- assert_equal client2, firm.clients.merge!(:where => ["#{QUOTED_TYPE} = :type", { :type => 'Client' }], :order => "id").first
+ assert_equal client2, firm.clients.where("#{QUOTED_TYPE} = ?", "Client").first
+ assert_equal client2, firm.clients.where("#{QUOTED_TYPE} = :type", type: "Client").first
+ end
+
+ def test_find_first_after_reset_scope
+ firm = Firm.first
+ collection = firm.clients
+
+ original_object = collection.first
+ assert_same original_object, collection.first, "Expected second call to #first to cache the same object"
+
+ # It should return a different object, since the association has been reloaded
+ assert_not_same original_object, firm.clients.first, "Expected #first to return a new object"
+ end
+
+ def test_find_first_after_reset
+ firm = Firm.first
+ collection = firm.clients
+
+ original_object = collection.first
+ assert_same original_object, collection.first, "Expected second call to #first to cache the same object"
+ collection.reset
+
+ # It should return a different object, since the association has been reloaded
+ assert_not_same original_object, collection.first, "Expected #first after #reset to return a new object"
+ end
+
+ def test_find_first_after_reload
+ firm = Firm.first
+ collection = firm.clients
+
+ original_object = collection.first
+ assert_same original_object, collection.first, "Expected second call to #first to cache the same object"
+ collection.reload
+
+ # It should return a different object, since the association has been reloaded
+ assert_not_same original_object, collection.first, "Expected #first after #reload to return a new object"
+ end
+
+ def test_reload_with_query_cache
+ connection = ActiveRecord::Base.connection
+ connection.enable_query_cache!
+ connection.clear_query_cache
+
+ # Populate the cache with a query
+ firm = Firm.first
+ # Populate the cache with a second query
+ firm.clients.load
+
+ assert_equal 2, connection.query_cache.size
+
+ # Clear the cache and fetch the clients again, populating the cache with a query
+ assert_queries(1) { firm.clients.reload }
+ # This query is cached, so it shouldn't make a real SQL query
+ assert_queries(0) { firm.clients.load }
+
+ assert_equal 1, connection.query_cache.size
+ ensure
+ ActiveRecord::Base.connection.disable_query_cache!
+ end
+
+ def test_reloading_unloaded_associations_with_query_cache
+ connection = ActiveRecord::Base.connection
+ connection.enable_query_cache!
+ connection.clear_query_cache
+
+ firm = Firm.create!(name: "firm name")
+ client = firm.clients.create!(name: "client name")
+ firm.clients.to_a # add request to cache
+
+ connection.uncached do
+ client.update!(name: "new client name")
+ end
+
+ firm = Firm.find(firm.id)
+
+ assert_equal [client.name], firm.clients.reload.map(&:name)
+ ensure
+ ActiveRecord::Base.connection.disable_query_cache!
end
def test_find_all_with_include_and_conditions
assert_nothing_raised do
- Developer.all.merge!(:joins => :audit_logs, :where => {'audit_logs.message' => nil, :name => 'Smith'}).to_a
+ Developer.all.merge!(joins: :audit_logs, where: { "audit_logs.message" => nil, :name => "Smith" }).to_a
end
end
@@ -665,8 +883,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_find_grouped
- all_clients_of_firm1 = Client.all.merge!(:where => "firm_id = 1").to_a
- grouped_clients_of_firm1 = Client.all.merge!(:where => "firm_id = 1", :group => "firm_id", :select => 'firm_id, count(id) as clients_count').to_a
+ all_clients_of_firm1 = Client.all.merge!(where: "firm_id = 1").to_a
+ grouped_clients_of_firm1 = Client.all.merge!(where: "firm_id = 1", group: "firm_id", select: "firm_id, count(id) as clients_count").to_a
assert_equal 3, all_clients_of_firm1.size
assert_equal 1, grouped_clients_of_firm1.size
end
@@ -679,7 +897,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_find_scoped_grouped_having
- assert_equal 1, authors(:david).popular_grouped_posts.length
+ assert_equal 2, authors(:david).popular_grouped_posts.length
assert_equal 0, authors(:mary).popular_grouped_posts.length
end
@@ -688,30 +906,39 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_select_query_method
- assert_equal ['id', 'body'], posts(:welcome).comments.select(:id, :body).first.attributes.keys
+ assert_equal ["id", "body"], posts(:welcome).comments.select(:id, :body).first.attributes.keys
end
def test_select_with_block
assert_equal [1], posts(:welcome).comments.select { |c| c.id == 1 }.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
assert_equal companies(:first_firm).accounts.first.credit_limit, companies(:first_firm).accounts.select(:credit_limit).first.credit_limit
end
def test_adding
force_signal37_to_load_all_clients_of_firm
+
+ assert_predicate companies(:first_firm).clients_of_firm, :loaded?
+
natural = Client.new("name" => "Natural Company")
companies(:first_firm).clients_of_firm << natural
assert_equal 3, companies(:first_firm).clients_of_firm.size # checking via the collection
- assert_equal 3, companies(:first_firm).clients_of_firm(true).size # checking using the db
+ assert_equal 3, companies(:first_firm).clients_of_firm.reload.size # checking using the db
assert_equal natural, companies(:first_firm).clients_of_firm.last
end
def test_adding_using_create
first_firm = companies(:first_firm)
assert_equal 3, first_firm.plain_clients.size
- first_firm.plain_clients.create(:name => "Natural Company")
+ first_firm.plain_clients.create(name: "Natural Company")
assert_equal 4, first_firm.plain_clients.length
assert_equal 4, first_firm.plain_clients.size
end
@@ -719,7 +946,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_create_with_bang_on_has_many_when_parent_is_new_raises
error = assert_raise(ActiveRecord::RecordNotSaved) do
firm = Firm.new
- firm.plain_clients.create! :name=>"Whoever"
+ firm.plain_clients.create! name: "Whoever"
end
assert_equal "You cannot call create unless the parent is saved", error.message
@@ -728,7 +955,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_regular_create_on_has_many_when_parent_is_new_raises
error = assert_raise(ActiveRecord::RecordNotSaved) do
firm = Firm.new
- firm.plain_clients.create :name=>"Whoever"
+ firm.plain_clients.create name: "Whoever"
end
assert_equal "You cannot call create unless the parent is saved", error.message
@@ -736,7 +963,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_create_with_bang_on_has_many_raises_when_record_not_saved
assert_raise(ActiveRecord::RecordInvalid) do
- firm = Firm.all.merge!(:order => "id").first
+ firm = Firm.first
firm.plain_clients.create!
end
end
@@ -757,26 +984,32 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_adding_a_collection
force_signal37_to_load_all_clients_of_firm
+
+ assert_predicate companies(:first_firm).clients_of_firm, :loaded?
+
companies(:first_firm).clients_of_firm.concat([Client.new("name" => "Natural Company"), Client.new("name" => "Apple")])
assert_equal 4, companies(:first_firm).clients_of_firm.size
- assert_equal 4, companies(:first_firm).clients_of_firm(true).size
+ assert_equal 4, companies(:first_firm).clients_of_firm.reload.size
end
def test_transactions_when_adding_to_persisted
- good = Client.new(:name => "Good")
- bad = Client.new(:name => "Bad", :raise_on_save => true)
+ good = Client.new(name: "Good")
+ bad = Client.new(name: "Bad", raise_on_save: true)
begin
companies(:first_firm).clients_of_firm.concat(good, bad)
rescue Client::RaisedOnSave
end
- assert !companies(:first_firm).clients_of_firm(true).include?(good)
+ assert_not_includes companies(:first_firm).clients_of_firm.reload, good
end
def test_transactions_when_adding_to_new_record
- assert_no_queries(ignore_none: false) do
- firm = Firm.new
+ # Load schema information so we don't query below if running just this test.
+ Client.define_attribute_methods
+
+ firm = Firm.new
+ assert_no_queries do
firm.clients_of_firm.concat(Client.new("name" => "Natural Company"))
end
end
@@ -790,21 +1023,29 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_new_aliased_to_build
company = companies(:first_firm)
- new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.new("name" => "Another Client") }
- assert !company.clients_of_firm.loaded?
+
+ # Load schema information so we don't query below if running just this test.
+ Client.define_attribute_methods
+
+ new_client = assert_no_queries { company.clients_of_firm.new("name" => "Another Client") }
+ assert_not_predicate company.clients_of_firm, :loaded?
assert_equal "Another Client", new_client.name
- assert !new_client.persisted?
+ assert_not_predicate new_client, :persisted?
assert_equal new_client, company.clients_of_firm.last
end
def test_build
company = companies(:first_firm)
- new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build("name" => "Another Client") }
- assert !company.clients_of_firm.loaded?
+
+ # Load schema information so we don't query below if running just this test.
+ Client.define_attribute_methods
+
+ new_client = assert_no_queries { company.clients_of_firm.build("name" => "Another Client") }
+ assert_not_predicate company.clients_of_firm, :loaded?
assert_equal "Another Client", new_client.name
- assert !new_client.persisted?
+ assert_not_predicate new_client, :persisted?
assert_equal new_client, company.clients_of_firm.last
end
@@ -813,13 +1054,34 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
company.clients_of_firm.build("name" => "Another Client")
company.clients_of_firm.build("name" => "Yet Another Client")
assert_equal 4, company.clients_of_firm.size
+ assert_equal 4, company.clients_of_firm.uniq.size
end
def test_collection_not_empty_after_building
company = companies(:first_firm)
- assert_predicate company.contracts, :empty?
+ assert_empty company.contracts
company.contracts.build
- assert_not_predicate company.contracts, :empty?
+ assert_not_empty company.contracts
+ end
+
+ def test_collection_size_with_dirty_target
+ post = posts(:thinking)
+ assert_equal [], post.reader_ids
+ assert_equal 0, post.readers.size
+ post.readers.reset
+ post.readers.build
+ assert_equal [nil], post.reader_ids
+ assert_equal 1, post.readers.size
+ end
+
+ def test_collection_empty_with_dirty_target
+ post = posts(:thinking)
+ assert_equal [], post.reader_ids
+ assert_empty post.readers
+ post.readers.reset
+ post.readers.build
+ assert_equal [nil], post.reader_ids
+ assert_not_empty post.readers
end
def test_collection_size_twice_for_regressions
@@ -836,24 +1098,30 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_build_many
company = companies(:first_firm)
- new_clients = assert_no_queries(ignore_none: false) { company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) }
+
+ # Load schema information so we don't query below if running just this test.
+ Client.define_attribute_methods
+
+ new_clients = assert_no_queries { company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) }
assert_equal 2, new_clients.size
end
def test_build_followed_by_save_does_not_load_target
companies(:first_firm).clients_of_firm.build("name" => "Another Client")
assert companies(:first_firm).save
- assert !companies(:first_firm).clients_of_firm.loaded?
+ assert_not_predicate companies(:first_firm).clients_of_firm, :loaded?
end
def test_build_without_loading_association
first_topic = topics(:first)
- Reply.column_names
assert_equal 1, first_topic.replies.length
+ # Load schema information so we don't query below if running just this test.
+ Reply.define_attribute_methods
+
assert_no_queries do
- first_topic.replies.build(:title => "Not saved", :content => "Superstars")
+ first_topic.replies.build(title: "Not saved", content: "Superstars")
assert_equal 2, first_topic.replies.size
end
@@ -862,18 +1130,26 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_build_via_block
company = companies(:first_firm)
- new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build {|client| client.name = "Another Client" } }
- assert !company.clients_of_firm.loaded?
+
+ # Load schema information so we don't query below if running just this test.
+ Client.define_attribute_methods
+
+ new_client = assert_no_queries { company.clients_of_firm.build { |client| client.name = "Another Client" } }
+ assert_not_predicate company.clients_of_firm, :loaded?
assert_equal "Another Client", new_client.name
- assert !new_client.persisted?
+ assert_not_predicate new_client, :persisted?
assert_equal new_client, company.clients_of_firm.last
end
def test_build_many_via_block
company = companies(:first_firm)
- new_clients = assert_no_queries(ignore_none: false) do
- company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) do |client|
+
+ # Load schema information so we don't query below if running just this test.
+ Client.define_attribute_methods
+
+ new_clients = assert_no_queries do
+ company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) do |client|
client.name = "changed"
end
end
@@ -884,15 +1160,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_create_without_loading_association
- first_firm = companies(:first_firm)
- Firm.column_names
- Client.column_names
+ first_firm = companies(:first_firm)
assert_equal 2, first_firm.clients_of_firm.size
first_firm.clients_of_firm.reset
assert_queries(1) do
- first_firm.clients_of_firm.create(:name => "Superstars")
+ first_firm.clients_of_firm.create(name: "Superstars")
end
assert_equal 3, first_firm.clients_of_firm.size
@@ -900,28 +1174,34 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_create
force_signal37_to_load_all_clients_of_firm
+
+ assert_predicate companies(:first_firm).clients_of_firm, :loaded?
+
new_client = companies(:first_firm).clients_of_firm.create("name" => "Another Client")
- assert new_client.persisted?
+ assert_predicate new_client, :persisted?
assert_equal new_client, companies(:first_firm).clients_of_firm.last
- assert_equal new_client, companies(:first_firm).clients_of_firm(true).last
+ assert_equal new_client, companies(:first_firm).clients_of_firm.reload.last
end
def test_create_many
- companies(:first_firm).clients_of_firm.create([{"name" => "Another Client"}, {"name" => "Another Client II"}])
- assert_equal 4, companies(:first_firm).clients_of_firm(true).size
+ companies(:first_firm).clients_of_firm.create([{ "name" => "Another Client" }, { "name" => "Another Client II" }])
+ assert_equal 4, companies(:first_firm).clients_of_firm.reload.size
end
def test_create_followed_by_save_does_not_load_target
companies(:first_firm).clients_of_firm.create("name" => "Another Client")
assert companies(:first_firm).save
- assert !companies(:first_firm).clients_of_firm.loaded?
+ assert_not_predicate companies(:first_firm).clients_of_firm, :loaded?
end
def test_deleting
force_signal37_to_load_all_clients_of_firm
+
+ assert_predicate companies(:first_firm).clients_of_firm, :loaded?
+
companies(:first_firm).clients_of_firm.delete(companies(:first_firm).clients_of_firm.first)
assert_equal 1, companies(:first_firm).clients_of_firm.size
- assert_equal 1, companies(:first_firm).clients_of_firm(true).size
+ assert_equal 1, companies(:first_firm).clients_of_firm.reload.size
end
def test_deleting_before_save
@@ -932,6 +1212,25 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 0, new_firm.clients_of_firm.size
end
+ def test_has_many_without_counter_cache_option
+ # Ship has a conventionally named `treasures_count` column, but the counter_cache
+ # option is not given on the association.
+ ship = Ship.create(name: "Countless", treasures_count: 10)
+
+ assert_not_predicate Ship.reflect_on_association(:treasures), :has_cached_counter?
+
+ # Count should come from sql count() of treasures rather than treasures_count attribute
+ assert_equal ship.treasures.size, 0
+
+ assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed" do
+ ship.treasures.create(name: "Gold")
+ end
+
+ assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed" do
+ ship.treasures.destroy_all
+ end
+ end
+
def test_deleting_updates_counter_cache
topic = Topic.order("id ASC").first
assert_equal topic.replies.to_a.size, topic.replies_count
@@ -971,6 +1270,38 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 2, topic.reload.replies.size
end
+ def test_counter_cache_updates_in_memory_after_update_with_inverse_of_disabled
+ topic = Topic.create!(title: "Zoom-zoom-zoom")
+
+ assert_equal 0, topic.replies_count
+
+ reply1 = Reply.create!(title: "re: zoom", content: "speedy quick!")
+ reply2 = Reply.create!(title: "re: zoom 2", content: "OMG lol!")
+
+ assert_queries(4) do
+ topic.replies << [reply1, reply2]
+ end
+
+ assert_equal 2, topic.replies_count
+ assert_equal 2, topic.reload.replies_count
+ end
+
+ def test_counter_cache_updates_in_memory_after_update_with_inverse_of_enabled
+ category = Category.create!(name: "Counter Cache")
+
+ assert_nil category.categorizations_count
+
+ categorization1 = Categorization.create!
+ categorization2 = Categorization.create!
+
+ assert_queries(4) do
+ category.categorizations << [categorization1, categorization2]
+ end
+
+ assert_equal 2, category.categorizations_count
+ assert_equal 2, category.reload.categorizations_count
+ end
+
def test_pushing_association_updates_counter_cache
topic = Topic.order("id ASC").first
reply = Reply.create!
@@ -1008,8 +1339,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_calling_empty_with_counter_cache
post = posts(:welcome)
- assert_queries(0) do
- assert_not post.comments.empty?
+ assert_no_queries do
+ assert_not_empty post.comments
end
end
@@ -1021,20 +1352,20 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
end
- def test_calling_update_attributes_on_id_changes_the_counter_cache
+ def test_calling_update_on_id_changes_the_counter_cache
topic = Topic.order("id ASC").first
original_count = topic.replies.to_a.size
assert_equal original_count, topic.replies_count
first_reply = topic.replies.first
- first_reply.update_attributes(:parent_id => nil)
+ first_reply.update(parent_id: nil)
assert_equal original_count - 1, topic.reload.replies_count
- first_reply.update_attributes(:parent_id => topic.id)
+ first_reply.update(parent_id: topic.id)
assert_equal original_count, topic.reload.replies_count
end
- def test_calling_update_attributes_changing_ids_doesnt_change_counter_cache
+ def test_calling_update_changing_ids_doesnt_change_counter_cache
topic1 = Topic.find(1)
topic2 = Topic.find(3)
original_count1 = topic1.replies.to_a.size
@@ -1043,26 +1374,32 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
reply1 = topic1.replies.first
reply2 = topic2.replies.first
- reply1.update_attributes(:parent_id => topic2.id)
+ reply1.update(parent_id: topic2.id)
assert_equal original_count1 - 1, topic1.reload.replies_count
assert_equal original_count2 + 1, topic2.reload.replies_count
- reply2.update_attributes(:parent_id => topic1.id)
+ reply2.update(parent_id: topic1.id)
assert_equal original_count1, topic1.reload.replies_count
assert_equal original_count2, topic2.reload.replies_count
end
def test_deleting_a_collection
force_signal37_to_load_all_clients_of_firm
+
+ assert_predicate companies(:first_firm).clients_of_firm, :loaded?
+
companies(:first_firm).clients_of_firm.create("name" => "Another Client")
assert_equal 3, companies(:first_firm).clients_of_firm.size
companies(:first_firm).clients_of_firm.delete([companies(:first_firm).clients_of_firm[0], companies(:first_firm).clients_of_firm[1], companies(:first_firm).clients_of_firm[2]])
assert_equal 0, companies(:first_firm).clients_of_firm.size
- assert_equal 0, companies(:first_firm).clients_of_firm(true).size
+ assert_equal 0, companies(:first_firm).clients_of_firm.reload.size
end
def test_delete_all
force_signal37_to_load_all_clients_of_firm
+
+ assert_predicate companies(:first_firm).clients_of_firm, :loaded?
+
companies(:first_firm).dependent_clients_of_firm.create("name" => "Another Client")
clients = companies(:first_firm).dependent_clients_of_firm.to_a
assert_equal 3, clients.count
@@ -1074,17 +1411,20 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_delete_all_with_not_yet_loaded_association_collection
force_signal37_to_load_all_clients_of_firm
+
+ assert_predicate companies(:first_firm).clients_of_firm, :loaded?
+
companies(:first_firm).clients_of_firm.create("name" => "Another Client")
assert_equal 3, companies(:first_firm).clients_of_firm.size
companies(:first_firm).clients_of_firm.reset
companies(:first_firm).clients_of_firm.delete_all
assert_equal 0, companies(:first_firm).clients_of_firm.size
- assert_equal 0, companies(:first_firm).clients_of_firm(true).size
+ assert_equal 0, companies(:first_firm).clients_of_firm.reload.size
end
def test_transaction_when_deleting_persisted
- good = Client.new(:name => "Good")
- bad = Client.new(:name => "Bad", :raise_on_destroy => true)
+ good = Client.new(name: "Good")
+ bad = Client.new(name: "Bad", raise_on_destroy: true)
companies(:first_firm).clients_of_firm = [good, bad]
@@ -1093,12 +1433,15 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
rescue Client::RaisedOnDestroy
end
- assert_equal [good, bad], companies(:first_firm).clients_of_firm(true)
+ assert_equal [good, bad], companies(:first_firm).clients_of_firm.reload
end
def test_transaction_when_deleting_new_record
- assert_no_queries(ignore_none: false) do
- firm = Firm.new
+ # Load schema information so we don't query below if running just this test.
+ Client.define_attribute_methods
+
+ firm = Firm.new
+ assert_no_queries do
client = Client.new("name" => "New Client")
firm.clients_of_firm << client
firm.clients_of_firm.destroy(client)
@@ -1113,7 +1456,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm.clients_of_firm.clear
assert_equal 0, firm.clients_of_firm.size
- assert_equal 0, firm.clients_of_firm(true).size
+ assert_equal 0, firm.clients_of_firm.reload.size
assert_equal [], Client.destroyed_client_ids[firm.id]
# Should not be destroyed since the association is not dependent.
@@ -1125,7 +1468,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_clearing_updates_counter_cache
topic = Topic.first
- assert_difference 'topic.reload.replies_count', -1 do
+ assert_difference "topic.reload.replies_count", -1 do
topic.replies.clear
end
end
@@ -1134,7 +1477,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
car = Car.first
car.engines.create!
- assert_difference 'car.reload.engines_count', -1 do
+ assert_difference "car.reload.engines_count", -1 do
car.engines.clear
end
end
@@ -1149,7 +1492,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm.dependent_clients_of_firm.clear
assert_equal 0, firm.dependent_clients_of_firm.size
- assert_equal 0, firm.dependent_clients_of_firm(true).size
+ assert_equal 0, firm.dependent_clients_of_firm.reload.size
assert_equal [], Client.destroyed_client_ids[firm.id]
# Should be destroyed since the association is dependent.
@@ -1182,7 +1525,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm.exclusively_dependent_clients_of_firm.clear
assert_equal 0, firm.exclusively_dependent_clients_of_firm.size
- assert_equal 0, firm.exclusively_dependent_clients_of_firm(true).size
+ assert_equal 0, firm.exclusively_dependent_clients_of_firm.reload.size
# no destroy-filters should have been called
assert_equal [], Client.destroyed_client_ids[firm.id]
@@ -1192,8 +1535,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_dependent_association_respects_optional_conditions_on_delete
firm = companies(:odegy)
- Client.create(:client_of => firm.id, :name => "BigShot Inc.")
- Client.create(:client_of => firm.id, :name => "SmallTime Inc.")
+ Client.create(client_of: firm.id, name: "BigShot Inc.")
+ Client.create(client_of: firm.id, name: "SmallTime Inc.")
# only one of two clients is included in the association due to the :conditions key
assert_equal 2, Client.where(client_of: firm.id).size
assert_equal 1, firm.dependent_conditional_clients_of_firm.size
@@ -1204,8 +1547,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_dependent_association_respects_optional_sanitized_conditions_on_delete
firm = companies(:odegy)
- Client.create(:client_of => firm.id, :name => "BigShot Inc.")
- Client.create(:client_of => firm.id, :name => "SmallTime Inc.")
+ Client.create(client_of: firm.id, name: "BigShot Inc.")
+ Client.create(client_of: firm.id, name: "SmallTime Inc.")
# only one of two clients is included in the association due to the :conditions key
assert_equal 2, Client.where(client_of: firm.id).size
assert_equal 1, firm.dependent_sanitized_conditional_clients_of_firm.size
@@ -1216,11 +1559,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_dependent_association_respects_optional_hash_conditions_on_delete
firm = companies(:odegy)
- Client.create(:client_of => firm.id, :name => "BigShot Inc.")
- Client.create(:client_of => firm.id, :name => "SmallTime Inc.")
+ Client.create(client_of: firm.id, name: "BigShot Inc.")
+ Client.create(client_of: firm.id, name: "SmallTime Inc.")
# only one of two clients is included in the association due to the :conditions key
assert_equal 2, Client.where(client_of: firm.id).size
- assert_equal 1, firm.dependent_sanitized_conditional_clients_of_firm.size
+ assert_equal 1, firm.dependent_hash_conditional_clients_of_firm.size
firm.destroy
# only the correctly associated client should have been deleted
assert_equal 1, Client.where(client_of: firm.id).size
@@ -1231,7 +1574,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
# break the vanilla firm_id foreign key
assert_equal 3, firm.clients.count
firm.clients.first.update_columns(firm_id: nil)
- assert_equal 2, firm.clients(true).count
+ assert_equal 2, firm.clients.reload.count
assert_equal 2, firm.clients_using_primary_key_with_delete_all.count
old_record = firm.clients_using_primary_key_with_delete_all.first
firm = Firm.first
@@ -1242,13 +1585,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_creation_respects_hash_condition
ms_client = companies(:first_firm).clients_like_ms_with_hash_conditions.build
- assert ms_client.save
- assert_equal 'Microsoft', ms_client.name
+ assert ms_client.save
+ assert_equal "Microsoft", ms_client.name
another_ms_client = companies(:first_firm).clients_like_ms_with_hash_conditions.create
- assert another_ms_client.persisted?
- assert_equal 'Microsoft', another_ms_client.name
+ assert_predicate another_ms_client, :persisted?
+ assert_equal "Microsoft", another_ms_client.name
end
def test_clearing_without_initial_access
@@ -1257,22 +1600,25 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm.clients_of_firm.clear
assert_equal 0, firm.clients_of_firm.size
- assert_equal 0, firm.clients_of_firm(true).size
+ assert_equal 0, firm.clients_of_firm.reload.size
end
def test_deleting_a_item_which_is_not_in_the_collection
force_signal37_to_load_all_clients_of_firm
- summit = Client.find_by_name('Summit')
+
+ assert_predicate companies(:first_firm).clients_of_firm, :loaded?
+
+ summit = Client.find_by_name("Summit")
companies(:first_firm).clients_of_firm.delete(summit)
assert_equal 2, companies(:first_firm).clients_of_firm.size
- assert_equal 2, companies(:first_firm).clients_of_firm(true).size
+ assert_equal 2, companies(:first_firm).clients_of_firm.reload.size
assert_equal 2, summit.client_of
end
- def test_deleting_by_fixnum_id
+ def test_deleting_by_integer_id
david = Developer.find(1)
- assert_difference 'david.projects.count', -1 do
+ assert_difference "david.projects.count", -1 do
assert_equal 1, david.projects.delete(1).size
end
@@ -1282,8 +1628,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_deleting_by_string_id
david = Developer.find(1)
- assert_difference 'david.projects.count', -1 do
- assert_equal 1, david.projects.delete('1').size
+ assert_difference "david.projects.count", -1 do
+ assert_equal 1, david.projects.delete("1").size
end
assert_equal 1, david.projects.size
@@ -1298,38 +1644,47 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_destroying
force_signal37_to_load_all_clients_of_firm
+ assert_predicate companies(:first_firm).clients_of_firm, :loaded?
+
assert_difference "Client.count", -1 do
companies(:first_firm).clients_of_firm.destroy(companies(:first_firm).clients_of_firm.first)
end
assert_equal 1, companies(:first_firm).reload.clients_of_firm.size
- assert_equal 1, companies(:first_firm).clients_of_firm(true).size
+ assert_equal 1, companies(:first_firm).clients_of_firm.reload.size
end
- def test_destroying_by_fixnum_id
+ def test_destroying_by_integer_id
force_signal37_to_load_all_clients_of_firm
+ assert_predicate companies(:first_firm).clients_of_firm, :loaded?
+
assert_difference "Client.count", -1 do
companies(:first_firm).clients_of_firm.destroy(companies(:first_firm).clients_of_firm.first.id)
end
assert_equal 1, companies(:first_firm).reload.clients_of_firm.size
- assert_equal 1, companies(:first_firm).clients_of_firm(true).size
+ assert_equal 1, companies(:first_firm).clients_of_firm.reload.size
end
def test_destroying_by_string_id
force_signal37_to_load_all_clients_of_firm
+ assert_predicate companies(:first_firm).clients_of_firm, :loaded?
+
assert_difference "Client.count", -1 do
companies(:first_firm).clients_of_firm.destroy(companies(:first_firm).clients_of_firm.first.id.to_s)
end
assert_equal 1, companies(:first_firm).reload.clients_of_firm.size
- assert_equal 1, companies(:first_firm).clients_of_firm(true).size
+ assert_equal 1, companies(:first_firm).clients_of_firm.reload.size
end
def test_destroying_a_collection
force_signal37_to_load_all_clients_of_firm
+
+ assert_predicate companies(:first_firm).clients_of_firm, :loaded?
+
companies(:first_firm).clients_of_firm.create("name" => "Another Client")
assert_equal 3, companies(:first_firm).clients_of_firm.size
@@ -1338,35 +1693,37 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
assert_equal 1, companies(:first_firm).reload.clients_of_firm.size
- assert_equal 1, companies(:first_firm).clients_of_firm(true).size
+ assert_equal 1, companies(:first_firm).clients_of_firm.reload.size
end
def test_destroy_all
force_signal37_to_load_all_clients_of_firm
+
+ assert_predicate companies(:first_firm).clients_of_firm, :loaded?
+
clients = companies(:first_firm).clients_of_firm.to_a
- assert !clients.empty?, "37signals has clients after load"
+ assert_not clients.empty?, "37signals has clients after load"
destroyed = companies(:first_firm).clients_of_firm.destroy_all
assert_equal clients.sort_by(&:id), destroyed.sort_by(&:id)
assert destroyed.all?(&:frozen?), "destroyed clients should be frozen"
assert companies(:first_firm).clients_of_firm.empty?, "37signals has no clients after destroy all"
- assert companies(:first_firm).clients_of_firm(true).empty?, "37signals has no clients after destroy all and refresh"
+ assert companies(:first_firm).clients_of_firm.reload.empty?, "37signals has no clients after destroy all and refresh"
end
def test_dependence
firm = companies(:first_firm)
assert_equal 3, firm.clients.size
firm.destroy
- assert Client.all.merge!(:where => "firm_id=#{firm.id}").to_a.empty?
+ assert_empty Client.all.merge!(where: "firm_id=#{firm.id}").to_a
end
def test_dependence_for_associations_with_hash_condition
david = authors(:david)
- assert_difference('Post.count', -1) { assert david.destroy }
+ assert_difference("Post.count", -1) { assert david.destroy }
end
def test_destroy_dependent_when_deleted_from_association
- # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first
- firm = Firm.all.merge!(:order => "id").first
+ firm = Firm.first
assert_equal 3, firm.clients.size
client = firm.clients.first
@@ -1393,7 +1750,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm.destroy rescue "do nothing"
- assert_equal 3, Client.all.merge!(:where => "firm_id=#{firm.id}").to_a.size
+ assert_equal 3, Client.all.merge!(where: "firm_id=#{firm.id}").to_a.size
end
def test_dependence_on_account
@@ -1417,28 +1774,47 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_restrict_with_exception
- firm = RestrictedWithExceptionFirm.create!(:name => 'restrict')
- firm.companies.create(:name => 'child')
+ firm = RestrictedWithExceptionFirm.create!(name: "restrict")
+ firm.companies.create(name: "child")
- assert !firm.companies.empty?
+ assert_not_empty firm.companies
assert_raise(ActiveRecord::DeleteRestrictionError) { firm.destroy }
- assert RestrictedWithExceptionFirm.exists?(:name => 'restrict')
- assert firm.companies.exists?(:name => 'child')
+ assert RestrictedWithExceptionFirm.exists?(name: "restrict")
+ assert firm.companies.exists?(name: "child")
end
def test_restrict_with_error
- firm = RestrictedWithErrorFirm.create!(:name => 'restrict')
- firm.companies.create(:name => 'child')
+ firm = RestrictedWithErrorFirm.create!(name: "restrict")
+ firm.companies.create(name: "child")
- assert !firm.companies.empty?
+ assert_not_empty firm.companies
firm.destroy
- assert !firm.errors.empty?
+ assert_not_empty firm.errors
assert_equal "Cannot delete record because dependent companies exist", firm.errors[:base].first
- assert RestrictedWithErrorFirm.exists?(:name => 'restrict')
- assert firm.companies.exists?(:name => 'child')
+ assert RestrictedWithErrorFirm.exists?(name: "restrict")
+ assert firm.companies.exists?(name: "child")
+ end
+
+ def test_restrict_with_error_with_locale
+ I18n.backend = I18n::Backend::Simple.new
+ I18n.backend.store_translations "en", activerecord: { attributes: { restricted_with_error_firm: { companies: "client companies" } } }
+ firm = RestrictedWithErrorFirm.create!(name: "restrict")
+ firm.companies.create(name: "child")
+
+ assert_not_empty firm.companies
+
+ firm.destroy
+
+ assert_not_empty firm.errors
+
+ assert_equal "Cannot delete record because dependent client companies exist", firm.errors[:base].first
+ assert RestrictedWithErrorFirm.exists?(name: "restrict")
+ assert firm.companies.exists?(name: "child")
+ ensure
+ I18n.backend.reload!
end
def test_included_in_collection
@@ -1446,10 +1822,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_included_in_collection_for_new_records
- client = Client.create(:name => 'Persisted')
+ client = Client.create(name: "Persisted")
assert_nil client.client_of
assert_equal false, Firm.new.clients_of_firm.include?(client),
- 'includes a client that does not belong to any firm'
+ "includes a client that does not belong to any firm"
end
def test_adding_array_and_collection
@@ -1457,7 +1833,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_replace_with_less
- firm = Firm.all.merge!(:order => "id").first
+ firm = Firm.first
firm.clients = [companies(:first_client)]
assert firm.save, "Could not save firm"
firm.reload
@@ -1471,7 +1847,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_replace_with_new
- firm = Firm.all.merge!(:order => "id").first
+ firm = Firm.first
firm.clients = [companies(:second_client), Client.new("name" => "New Client")]
firm.save
firm.reload
@@ -1484,8 +1860,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
account = Account.new
orig_accounts = firm.accounts.to_a
- assert !account.valid?
- assert !orig_accounts.empty?
+ assert_not_predicate account, :valid?
+ assert_not_empty orig_accounts
error = assert_raise ActiveRecord::RecordNotSaved do
firm.accounts = [account]
end
@@ -1500,16 +1876,16 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm.clients = []
firm.save
- assert_queries(0, ignore_none: true) do
+ assert_no_queries do
firm.clients = []
end
- assert_equal [], firm.send('clients=', [])
+ assert_equal [], firm.send("clients=", [])
end
def test_transactions_when_replacing_on_persisted
- good = Client.new(:name => "Good")
- bad = Client.new(:name => "Bad", :raise_on_save => true)
+ good = Client.new(name: "Good")
+ bad = Client.new(name: "Bad", raise_on_save: true)
companies(:first_firm).clients_of_firm = [good]
@@ -1518,12 +1894,15 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
rescue Client::RaisedOnSave
end
- assert_equal [good], companies(:first_firm).clients_of_firm(true)
+ assert_equal [good], companies(:first_firm).clients_of_firm.reload
end
def test_transactions_when_replacing_on_new_record
- assert_no_queries(ignore_none: false) do
- firm = Firm.new
+ # Load schema information so we don't query below if running just this test.
+ Client.define_attribute_methods
+
+ firm = Firm.new
+ assert_no_queries do
firm.clients_of_firm = [Client.new("name" => "New Client")]
end
end
@@ -1534,8 +1913,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_get_ids_for_loaded_associations
company = companies(:first_firm)
- company.clients(true)
- assert_queries(0) do
+ company.clients.reload
+ assert_no_queries do
company.client_ids
company.client_ids
end
@@ -1543,9 +1922,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_get_ids_for_unloaded_associations_does_not_load_them
company = companies(:first_firm)
- assert !company.clients.loaded?
+ assert_not_predicate company.clients, :loaded?
assert_equal [companies(:first_client).id, companies(:second_client).id, companies(:another_first_firm_client).id], company.client_ids
- assert !company.clients.loaded?
+ assert_not_predicate company.clients, :loaded?
+ end
+
+ def test_counter_cache_on_unloaded_association
+ car = Car.create(name: "My AppliCar")
+ assert_equal car.engines.size, 0
end
def test_get_ids_ignores_include_option
@@ -1557,11 +1941,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_get_ids_for_association_on_new_record_does_not_try_to_find_records
- Company.columns # Load schema information so we don't query below
- Contract.columns # if running just this test.
+ # Load schema information so we don't query below if running just this test.
+ companies(:first_client).contract_ids
company = Company.new
- assert_queries(0) do
+ assert_no_queries do
company.contract_ids
end
@@ -1572,7 +1956,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
contract_a = Contract.create!
contract_b = Contract.create!
Contract.create! # another contract
- company = Company.new(:name => "Some Company")
+ company = Company.new(name: "Some Company")
company.contract_ids = [contract_a.id, contract_b.id]
assert_equal [contract_a.id, contract_b.id], company.contract_ids
@@ -1584,11 +1968,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_assign_ids_ignoring_blanks
- firm = Firm.create!(:name => 'Apple')
- firm.client_ids = [companies(:first_client).id, nil, companies(:second_client).id, '']
+ firm = Firm.create!(name: "Apple")
+ firm.client_ids = [companies(:first_client).id, nil, companies(:second_client).id, ""]
firm.save!
- assert_equal 2, firm.clients(true).size
+ assert_equal 2, firm.clients.reload.size
assert_equal true, firm.clients.include?(companies(:second_client))
end
@@ -1600,14 +1984,21 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
[
lambda { authors(:mary).comment_ids = [comments(:greetings).id, comments(:more_greetings).id] },
lambda { authors(:mary).comments = [comments(:greetings), comments(:more_greetings)] },
- lambda { authors(:mary).comments << Comment.create!(:body => "Yay", :post_id => 424242) },
+ lambda { authors(:mary).comments << Comment.create!(body: "Yay", post_id: 424242) },
lambda { authors(:mary).comments.delete(authors(:mary).comments.first) },
- ].each {|block| assert_raise(ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection, &block) }
+ ].each { |block| assert_raise(ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection, &block) }
+ end
+
+ def test_associations_order_should_be_priority_over_throughs_order
+ david = authors(:david)
+ expected = [12, 10, 9, 8, 7, 6, 5, 3, 2, 1]
+ assert_equal expected, david.comments_desc.map(&:id)
+ assert_equal expected, Author.includes(:comments_desc).find(david.id).comments_desc.map(&:id)
end
def test_dynamic_find_should_respect_association_order_for_through
assert_equal Comment.find(10), authors(:david).comments_desc.where("comments.type = 'SpecialComment'").first
- assert_equal Comment.find(10), authors(:david).comments_desc.find_by_type('SpecialComment')
+ assert_equal Comment.find(10), authors(:david).comments_desc.find_by_type("SpecialComment")
end
def test_has_many_through_respects_hash_conditions
@@ -1622,7 +2013,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
client = firm.clients.first
assert_no_queries do
- assert firm.clients.loaded?
+ assert_predicate firm.clients, :loaded?
assert_equal true, firm.clients.include?(client)
end
end
@@ -1632,18 +2023,18 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
client = firm.clients.first
firm.reload
- assert ! firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
assert_queries(1) do
assert_equal true, firm.clients.include?(client)
end
- assert ! firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
end
def test_include_returns_false_for_non_matching_record_to_verify_scoping
firm = companies(:first_firm)
- client = Client.create!(:name => 'Not Associated')
+ client = Client.create!(name: "Not Associated")
- assert ! firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
assert_equal false, firm.clients.include?(client)
end
@@ -1652,15 +2043,15 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm.clients.first
firm.clients.second
firm.clients.last
- assert !firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
end
def test_calling_first_or_last_on_loaded_association_should_not_fetch_with_query
firm = companies(:first_firm)
firm.clients.load_target
- assert firm.clients.loaded?
+ assert_predicate firm.clients, :loaded?
- assert_no_queries(ignore_none: false) do
+ assert_no_queries do
firm.clients.first
assert_equal 2, firm.clients.first(2).size
firm.clients.last
@@ -1670,8 +2061,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_calling_first_or_last_on_existing_record_with_build_should_load_association
firm = companies(:first_firm)
- firm.clients.build(:name => 'Foo')
- assert !firm.clients.loaded?
+ firm.clients.build(name: "Foo")
+ assert_not_predicate firm.clients, :loaded?
assert_queries 1 do
firm.clients.first
@@ -1679,13 +2070,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm.clients.last
end
- assert firm.clients.loaded?
+ assert_predicate firm.clients, :loaded?
end
def test_calling_first_nth_or_last_on_existing_record_with_create_should_not_load_association
firm = companies(:first_firm)
- firm.clients.create(:name => 'Foo')
- assert !firm.clients.loaded?
+ firm.clients.create(name: "Foo")
+ assert_not_predicate firm.clients, :loaded?
assert_queries 3 do
firm.clients.first
@@ -1693,7 +2084,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm.clients.last
end
- assert !firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
end
def test_calling_first_nth_or_last_on_new_record_should_not_run_queries
@@ -1708,15 +2099,15 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_calling_first_or_last_with_integer_on_association_should_not_load_association
firm = companies(:first_firm)
- firm.clients.create(:name => 'Foo')
- assert !firm.clients.loaded?
+ firm.clients.create(name: "Foo")
+ assert_not_predicate firm.clients, :loaded?
assert_queries 2 do
firm.clients.first(2)
firm.clients.last(2)
end
- assert !firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
end
def test_calling_many_should_count_instead_of_loading_association
@@ -1724,37 +2115,38 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_queries(1) do
firm.clients.many? # use count query
end
- assert !firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
end
def test_calling_many_on_loaded_association_should_not_use_query
firm = companies(:first_firm)
- firm.clients.collect # force load
+ firm.clients.load # force load
assert_no_queries { assert firm.clients.many? }
end
def test_calling_many_should_defer_to_collection_if_using_a_block
firm = companies(:first_firm)
assert_queries(1) do
- firm.clients.expects(:size).never
- firm.clients.many? { true }
+ assert_not_called(firm.clients, :size) do
+ firm.clients.many? { true }
+ end
end
- assert firm.clients.loaded?
+ assert_predicate firm.clients, :loaded?
end
def test_calling_many_should_return_false_if_none_or_one
firm = companies(:another_firm)
- assert !firm.clients_like_ms.many?
+ assert_not_predicate firm.clients_like_ms, :many?
assert_equal 0, firm.clients_like_ms.size
firm = companies(:first_firm)
- assert !firm.limited_clients.many?
+ assert_not_predicate firm.limited_clients, :many?
assert_equal 1, firm.limited_clients.size
end
def test_calling_many_should_return_true_if_more_than_one
firm = companies(:first_firm)
- assert firm.clients.many?
+ assert_predicate firm.clients, :many?
assert_equal 3, firm.clients.size
end
@@ -1763,33 +2155,34 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_queries(1) do
firm.clients.none? # use count query
end
- assert !firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
end
def test_calling_none_on_loaded_association_should_not_use_query
firm = companies(:first_firm)
- firm.clients.collect # force load
- assert_no_queries { assert ! firm.clients.none? }
+ firm.clients.load # force load
+ assert_no_queries { assert_not firm.clients.none? }
end
def test_calling_none_should_defer_to_collection_if_using_a_block
firm = companies(:first_firm)
assert_queries(1) do
- firm.clients.expects(:size).never
- firm.clients.none? { true }
+ assert_not_called(firm.clients, :size) do
+ firm.clients.none? { true }
+ end
end
- assert firm.clients.loaded?
+ assert_predicate firm.clients, :loaded?
end
def test_calling_none_should_return_true_if_none
firm = companies(:another_firm)
- assert firm.clients_like_ms.none?
+ assert_predicate firm.clients_like_ms, :none?
assert_equal 0, firm.clients_like_ms.size
end
def test_calling_none_should_return_false_if_any
firm = companies(:first_firm)
- assert !firm.limited_clients.none?
+ assert_not_predicate firm.limited_clients, :none?
assert_equal 1, firm.limited_clients.size
end
@@ -1798,39 +2191,40 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_queries(1) do
firm.clients.one? # use count query
end
- assert !firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
end
def test_calling_one_on_loaded_association_should_not_use_query
firm = companies(:first_firm)
- firm.clients.collect # force load
- assert_no_queries { assert ! firm.clients.one? }
+ firm.clients.load # force load
+ assert_no_queries { assert_not firm.clients.one? }
end
def test_calling_one_should_defer_to_collection_if_using_a_block
firm = companies(:first_firm)
assert_queries(1) do
- firm.clients.expects(:size).never
- firm.clients.one? { true }
+ assert_not_called(firm.clients, :size) do
+ firm.clients.one? { true }
+ end
end
- assert firm.clients.loaded?
+ assert_predicate firm.clients, :loaded?
end
def test_calling_one_should_return_false_if_zero
firm = companies(:another_firm)
- assert ! firm.clients_like_ms.one?
+ assert_not_predicate firm.clients_like_ms, :one?
assert_equal 0, firm.clients_like_ms.size
end
def test_calling_one_should_return_true_if_one
firm = companies(:first_firm)
- assert firm.limited_clients.one?
+ assert_predicate firm.limited_clients, :one?
assert_equal 1, firm.limited_clients.size
end
def test_calling_one_should_return_false_if_more_than_one
firm = companies(:first_firm)
- assert ! firm.clients.one?
+ assert_not_predicate firm.clients, :one?
assert_equal 3, firm.clients.size
end
@@ -1838,13 +2232,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
old = ActiveRecord::Base.store_full_sti_class
ActiveRecord::Base.store_full_sti_class = true
- firm = Namespaced::Firm.create({ :name => 'Some Company' })
- firm.clients.create({ :name => 'Some Client' })
+ firm = Namespaced::Firm.create(name: "Some Company")
+ firm.clients.create(name: "Some Client")
stats = Namespaced::Firm.all.merge!(
- :select => "#{Namespaced::Firm.table_name}.id, COUNT(#{Namespaced::Client.table_name}.id) AS num_clients",
- :joins => :clients,
- :group => "#{Namespaced::Firm.table_name}.id"
+ select: "#{Namespaced::Firm.table_name}.id, COUNT(#{Namespaced::Client.table_name}.id) AS num_clients",
+ joins: :clients,
+ group: "#{Namespaced::Firm.table_name}.id"
).find firm.id
assert_equal 1, stats.num_clients.to_i
ensure
@@ -1852,9 +2246,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_association_proxy_transaction_method_starts_transaction_in_association_class
- Comment.expects(:transaction)
- Post.first.comments.transaction do
- # nothing
+ assert_called(Comment, :transaction) do
+ Post.first.comments.transaction do
+ # nothing
+ end
end
end
@@ -1863,43 +2258,45 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal client_association.new.attributes, client_association.send(:new).attributes
end
- def test_respond_to_private_class_methods
- client_association = companies(:first_firm).clients
- assert !client_association.respond_to?(:private_method)
- assert client_association.respond_to?(:private_method, true)
- end
-
def test_creating_using_primary_key
- firm = Firm.all.merge!(:order => "id").first
- client = firm.clients_using_primary_key.create!(:name => 'test')
+ firm = Firm.first
+ client = firm.clients_using_primary_key.create!(name: "test")
assert_equal firm.name, client.firm_name
end
def test_defining_has_many_association_with_delete_all_dependency_lazily_evaluates_target_class
- ActiveRecord::Reflection::AssociationReflection.any_instance.expects(:class_name).never
- class_eval(<<-EOF, __FILE__, __LINE__ + 1)
- class DeleteAllModel < ActiveRecord::Base
- has_many :nonentities, :dependent => :delete_all
- end
- EOF
+ assert_not_called_on_instance_of(
+ ActiveRecord::Reflection::AssociationReflection,
+ :class_name,
+ ) do
+ class_eval(<<-EOF, __FILE__, __LINE__ + 1)
+ class DeleteAllModel < ActiveRecord::Base
+ has_many :nonentities, :dependent => :delete_all
+ end
+ EOF
+ end
end
def test_defining_has_many_association_with_nullify_dependency_lazily_evaluates_target_class
- ActiveRecord::Reflection::AssociationReflection.any_instance.expects(:class_name).never
- class_eval(<<-EOF, __FILE__, __LINE__ + 1)
- class NullifyModel < ActiveRecord::Base
- has_many :nonentities, :dependent => :nullify
- end
- EOF
+ assert_not_called_on_instance_of(
+ ActiveRecord::Reflection::AssociationReflection,
+ :class_name,
+ ) do
+ class_eval(<<-EOF, __FILE__, __LINE__ + 1)
+ class NullifyModel < ActiveRecord::Base
+ has_many :nonentities, :dependent => :nullify
+ end
+ EOF
+ end
end
def test_attributes_are_being_set_when_initialized_from_has_many_association_with_where_clause
- new_comment = posts(:welcome).comments.where(:body => "Some content").build
+ new_comment = posts(:welcome).comments.where(body: "Some content").build
assert_equal new_comment.body, "Some content"
end
def test_attributes_are_being_set_when_initialized_from_has_many_association_with_multiple_where_clauses
- new_comment = posts(:welcome).comments.where(:body => "Some content").where(:type => 'SpecialComment').build
+ new_comment = posts(:welcome).comments.where(body: "Some content").where(type: "SpecialComment").build
assert_equal new_comment.body, "Some content"
assert_equal new_comment.type, "SpecialComment"
assert_equal new_comment.post_id, posts(:welcome).id
@@ -1913,7 +2310,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_load_target_respects_protected_attributes
topic = Topic.create!
- reply = topic.replies.create(:title => "reply 1")
+ reply = topic.replies.create(title: "reply 1")
reply.approved = false
reply.save!
@@ -1940,7 +2337,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_merging_with_custom_attribute_writer
- bulb = Bulb.new(:color => "red")
+ bulb = Bulb.new(color: "red")
assert_equal "RED!", bulb.color
car = Car.create!
@@ -1950,13 +2347,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_abstract_class_with_polymorphic_has_many
- post = SubStiPost.create! :title => "fooo", :body => "baa"
- tagging = Tagging.create! :taggable => post
+ post = SubStiPost.create! title: "fooo", body: "baa"
+ tagging = Tagging.create! taggable: post
assert_equal [tagging], post.taggings
end
def test_with_polymorphic_has_many_with_custom_columns_name
- post = Post.create! :title => 'foo', :body => 'bar'
+ post = Post.create! title: "foo", body: "bar"
image = Image.create!
post.images << image
@@ -1966,10 +2363,17 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_build_with_polymorphic_has_many_does_not_allow_to_override_type_and_id
welcome = posts(:welcome)
- tagging = welcome.taggings.build(:taggable_id => 99, :taggable_type => 'ShouldNotChange')
+ tagging = welcome.taggings.build(taggable_id: 99, taggable_type: "ShouldNotChange")
assert_equal welcome.id, tagging.taggable_id
- assert_equal 'Post', tagging.taggable_type
+ assert_equal "Post", tagging.taggable_type
+ end
+
+ def test_build_from_polymorphic_association_sets_inverse_instance
+ post = Post.new
+ tagging = post.taggings.build
+
+ assert_equal post, tagging.taggable
end
def test_dont_call_save_callbacks_twice_on_has_many
@@ -1981,30 +2385,30 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_association_attributes_are_available_to_after_initialize
- car = Car.create(:name => 'honda')
+ car = Car.create(name: "honda")
bulb = car.bulbs.build
- assert_equal car.id, bulb.attributes_after_initialize['car_id']
+ assert_equal car.id, bulb.attributes_after_initialize["car_id"]
end
def test_attributes_are_set_when_initialized_from_has_many_null_relationship
- car = Car.new name: 'honda'
- bulb = car.bulbs.where(name: 'headlight').first_or_initialize
- assert_equal 'headlight', bulb.name
+ car = Car.new name: "honda"
+ bulb = car.bulbs.where(name: "headlight").first_or_initialize
+ assert_equal "headlight", bulb.name
end
def test_attributes_are_set_when_initialized_from_polymorphic_has_many_null_relationship
- post = Post.new title: 'title', body: 'bar'
- tag = Tag.create!(name: 'foo')
+ post = Post.new title: "title", body: "bar"
+ tag = Tag.create!(name: "foo")
tagging = post.taggings.where(tag: tag).first_or_initialize
assert_equal tag.id, tagging.tag_id
- assert_equal 'Post', tagging.taggable_type
+ assert_equal "Post", tagging.taggable_type
end
def test_replace
- car = Car.create(:name => 'honda')
+ car = Car.create(name: "honda")
bulb1 = car.bulbs.create
bulb2 = Bulb.create
@@ -2015,7 +2419,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_replace_returns_target
- car = Car.create(:name => 'honda')
+ car = Car.create(name: "honda")
bulb1 = car.bulbs.create
bulb2 = car.bulbs.create
bulb3 = Bulb.create
@@ -2029,18 +2433,31 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_collection_association_with_private_kernel_method
firm = companies(:first_firm)
assert_equal [accounts(:signals37)], firm.accounts.open
+ assert_equal [accounts(:signals37)], firm.accounts.available
+ end
+
+ def test_association_with_or_doesnt_set_inverse_instance_key
+ firm = companies(:first_firm)
+ accounts = firm.accounts.or(Account.where(firm_id: nil)).order(:id)
+ assert_equal [firm.id, nil], accounts.map(&:firm_id)
+ end
+
+ def test_association_with_rewhere_doesnt_set_inverse_instance_key
+ firm = companies(:first_firm)
+ accounts = firm.accounts.rewhere(firm_id: [firm.id, nil]).order(:id)
+ assert_equal [firm.id, nil], accounts.map(&:firm_id)
end
test "first_or_initialize adds the record to the association" do
- firm = Firm.create! name: 'omg'
+ firm = Firm.create! name: "omg"
client = firm.clients_of_firm.first_or_initialize
assert_equal [client], firm.clients_of_firm
end
test "first_or_create adds the record to the association" do
- firm = Firm.create! name: 'omg'
+ firm = Firm.create! name: "omg"
firm.clients_of_firm.load_target
- client = firm.clients_of_firm.first_or_create name: 'lol'
+ client = firm.clients_of_firm.first_or_create name: "lol"
assert_equal [client], firm.clients_of_firm
assert_equal [client], firm.reload.clients_of_firm
end
@@ -2049,7 +2466,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
post = posts(:welcome)
assert post.taggings_with_delete_all.count > 0
- assert !post.taggings_with_delete_all.loaded?
+ assert_not_predicate post.taggings_with_delete_all, :loaded?
# 2 queries: one DELETE and another to update the counter cache
assert_queries(2) do
@@ -2060,9 +2477,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
test "has many associations on new records use null relations" do
post = Post.new
- assert_no_queries(ignore_none: false) do
+ assert_no_queries do
assert_equal [], post.comments
- assert_equal [], post.comments.where(body: 'omg')
+ assert_equal [], post.comments.where(body: "omg")
assert_equal [], post.comments.pluck(:body)
assert_equal 0, post.comments.sum(:id)
assert_equal 0, post.comments.count
@@ -2071,7 +2488,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
test "collection proxy respects default scope" do
author = authors(:mary)
- assert !author.first_posts.exists?
+ assert_not_predicate author.first_posts, :exists?
end
test "association with extend option" do
@@ -2083,14 +2500,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
test "association with extend option with multiple extensions" do
post = posts(:welcome)
assert_equal "lifo", post.comments_with_extend_2.author
- assert_equal "hello", post.comments_with_extend_2.greeting
+ assert_equal "hullo", post.comments_with_extend_2.greeting
+ end
+
+ test "extend option affects per association" do
+ post = posts(:welcome)
+ assert_equal "lifo", post.comments_with_extend.author
+ assert_equal "lifo", post.comments_with_extend_2.author
+ assert_equal "hello", post.comments_with_extend.greeting
+ assert_equal "hullo", post.comments_with_extend_2.greeting
end
test "delete record with complex joins" do
david = authors(:david)
post = david.posts.first
- post.type = 'PostWithSpecialCategorization'
+ post.type = "PostWithSpecialCategorization"
post.save
categorization = post.categorizations.first
@@ -2103,8 +2528,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
test "does not duplicate associations when used with natural primary keys" do
- speedometer = Speedometer.create!(id: '4')
- speedometer.minivans.create!(minivan_id: 'a-van-red' ,name: 'a van', color: 'red')
+ speedometer = Speedometer.create!(id: "4")
+ speedometer.minivans.create!(minivan_id: "a-van-red", name: "a van", color: "red")
assert_equal 1, speedometer.minivans.to_a.size, "Only one association should be present:\n#{speedometer.minivans.to_a}"
assert_equal 1, speedometer.reload.minivans.to_a.size
@@ -2119,12 +2544,33 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal [bulb1, bulb2], car.all_bulbs.sort_by(&:id)
end
- test 'unscopes the default scope of associated model when used with include' do
+ test "can unscope and where the default scope of the associated model" do
+ Car.has_many :other_bulbs, -> { unscope(where: [:name]).where(name: "other") }, class_name: "Bulb"
+ car = Car.create!
+ bulb1 = Bulb.create! name: "defaulty", car: car
+ bulb2 = Bulb.create! name: "other", car: car
+
+ assert_equal [bulb1], car.bulbs
+ assert_equal [bulb2], car.other_bulbs
+ end
+
+ test "can rewhere the default scope of the associated model" do
+ Car.has_many :old_bulbs, -> { rewhere(name: "old") }, class_name: "Bulb"
+ car = Car.create!
+ bulb1 = Bulb.create! name: "defaulty", car: car
+ bulb2 = Bulb.create! name: "old", car: car
+
+ assert_equal [bulb1], car.bulbs
+ assert_equal [bulb2], car.old_bulbs
+ end
+
+ test "unscopes the default scope of associated model when used with include" do
car = Car.create!
bulb = Bulb.create! name: "other", car: car
- assert_equal bulb, Car.find(car.id).all_bulbs.first
- assert_equal bulb, Car.includes(:all_bulbs).find(car.id).all_bulbs.first
+ assert_equal [bulb], Car.find(car.id).all_bulbs
+ assert_equal [bulb], Car.includes(:all_bulbs).find(car.id).all_bulbs
+ assert_equal [bulb], Car.eager_load(:all_bulbs).find(car.id).all_bulbs
end
test "raises RecordNotDestroyed when replaced child can't be destroyed" do
@@ -2139,7 +2585,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal "Failed to destroy the record", error.message
end
- test 'updates counter cache when default scope is given' do
+ test "updates counter cache when default scope is given" do
topic = DefaultRejectedTopic.create approved: true
assert_difference "topic.reload.replies_count", 1 do
@@ -2147,8 +2593,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
end
- test 'dangerous association name raises ArgumentError' do
- [:errors, 'errors', :save, 'save'].each do |name|
+ test "dangerous association name raises ArgumentError" do
+ [:errors, "errors", :save, "save"].each do |name|
assert_raises(ArgumentError, "Association #{name} should not be allowed") do
Class.new(ActiveRecord::Base) do
has_many name
@@ -2157,16 +2603,16 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
end
- test 'passes custom context validation to validate children' do
+ test "passes custom context validation to validate children" do
pirate = FamousPirate.new
pirate.famous_ships << ship = FamousShip.new
- assert pirate.valid?
+ assert_predicate pirate, :valid?
assert_not pirate.valid?(:conference)
assert_equal "can't be blank", ship.errors[:name].first
end
- test 'association with instance dependent scope' do
+ test "association with instance dependent scope" do
bob = authors(:bob)
Post.create!(title: "signed post by bob", body: "stuff", author: authors(:bob))
Post.create!(title: "anonymous post", body: "more stuff", author: authors(:bob))
@@ -2176,7 +2622,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal [], authors(:david).posts_with_signature.map(&:title)
end
- test 'associations autosaves when object is already persited' do
+ test "associations autosaves when object is already persisted" do
bulb = Bulb.create!
tyre = Tyre.create!
@@ -2189,7 +2635,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 1, car.tyres.count
end
- test 'associations replace in memory when records have the same id' do
+ test "associations replace in memory when records have the same id" do
bulb = Bulb.create!
car = Car.create!(bulbs: [bulb])
@@ -2200,7 +2646,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal "foo", car.bulbs.first.name
end
- test 'in memory replacement executes no queries' do
+ test "in memory replacement executes no queries" do
bulb = Bulb.create!
car = Car.create!(bulbs: [bulb])
@@ -2211,7 +2657,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
end
- test 'in memory replacements do not execute callbacks' do
+ test "in memory replacements do not execute callbacks" do
raise_after_add = false
klass = Class.new(ActiveRecord::Base) do
self.table_name = :cars
@@ -2232,7 +2678,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
end
- test 'in memory replacements sets inverse instance' do
+ test "in memory replacements sets inverse instance" do
bulb = Bulb.create!
car = Car.create!(bulbs: [bulb])
@@ -2242,7 +2688,16 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_same car, new_bulb.car
end
- test 'in memory replacement maintains order' do
+ test "reattach to new objects replaces inverse association and foreign key" do
+ bulb = Bulb.create!(car: Car.create!)
+ assert bulb.car_id
+ car = Car.new
+ car.bulbs << bulb
+ assert_equal car, bulb.car
+ assert_nil bulb.car_id
+ end
+
+ test "in memory replacement maintains order" do
first_bulb = Bulb.create!
second_bulb = Bulb.create!
car = Car.create!(bulbs: [first_bulb, second_bulb])
@@ -2252,4 +2707,184 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal [first_bulb, second_bulb], car.bulbs
end
+
+ test "association size calculation works with default scoped selects when not previously fetched" do
+ firm = Firm.create!(name: "Firm")
+ 5.times { firm.developers_with_select << Developer.create!(name: "Developer") }
+
+ same_firm = Firm.find(firm.id)
+ assert_equal 5, same_firm.developers_with_select.size
+ end
+
+ test "prevent double insertion of new object when the parent association loaded in the after save callback" do
+ reset_callbacks(:save, Bulb) do
+ Bulb.after_save { |record| record.car.bulbs.load }
+
+ car = Car.create!
+ car.bulbs << Bulb.new
+
+ assert_equal 1, car.bulbs.size
+ end
+ end
+
+ test "prevent double firing the before save callback of new object when the parent association saved in the callback" do
+ reset_callbacks(:save, Bulb) do
+ count = 0
+ Bulb.before_save { |record| record.car.save && count += 1 }
+
+ car = Car.create!
+ car.bulbs.create!
+
+ assert_equal 1, count
+ end
+ end
+
+ test "calling size on an association that has not been loaded performs a query" do
+ car = Car.create!
+ Bulb.create(car_id: car.id)
+
+ car_two = Car.create!
+
+ assert_queries(1) do
+ assert_equal 1, car.bulbs.size
+ end
+
+ assert_queries(1) do
+ assert_equal 0, car_two.bulbs.size
+ end
+ end
+
+ test "calling size on an association that has been loaded does not perform query" do
+ car = Car.create!
+ Bulb.create(car_id: car.id)
+ car.bulb_ids
+
+ car_two = Car.create!
+ car_two.bulb_ids
+
+ assert_no_queries do
+ assert_equal 1, car.bulbs.size
+ end
+
+ assert_no_queries do
+ assert_equal 0, car_two.bulbs.size
+ end
+ end
+
+ test "calling empty on an association that has not been loaded performs a query" do
+ car = Car.create!
+ Bulb.create(car_id: car.id)
+
+ car_two = Car.create!
+
+ assert_queries(1) do
+ assert_not_empty car.bulbs
+ end
+
+ assert_queries(1) do
+ assert_empty car_two.bulbs
+ end
+ end
+
+ test "calling empty on an association that has been loaded does not performs query" do
+ car = Car.create!
+ Bulb.create(car_id: car.id)
+ car.bulb_ids
+
+ car_two = Car.create!
+ car_two.bulb_ids
+
+ assert_no_queries do
+ assert_not_empty car.bulbs
+ end
+
+ assert_no_queries do
+ assert_empty car_two.bulbs
+ end
+ end
+
+ class AuthorWithErrorDestroyingAssociation < ActiveRecord::Base
+ self.table_name = "authors"
+ has_many :posts_with_error_destroying,
+ class_name: "PostWithErrorDestroying",
+ foreign_key: :author_id,
+ dependent: :destroy
+ end
+
+ class PostWithErrorDestroying < ActiveRecord::Base
+ self.table_name = "posts"
+ self.inheritance_column = nil
+ before_destroy -> { throw :abort }
+ end
+
+ def test_destroy_does_not_raise_when_association_errors_on_destroy
+ assert_no_difference "AuthorWithErrorDestroyingAssociation.count" do
+ author = AuthorWithErrorDestroyingAssociation.first
+
+ assert_not author.destroy
+ end
+ end
+
+ def test_destroy_with_bang_bubbles_errors_from_associations
+ error = assert_raises ActiveRecord::RecordNotDestroyed do
+ AuthorWithErrorDestroyingAssociation.first.destroy!
+ end
+
+ assert_instance_of PostWithErrorDestroying, error.record
+ end
+
+ def test_ids_reader_memoization
+ car = Car.create!(name: "Tofaş")
+ bulb = Bulb.create!(car: car)
+
+ assert_equal [bulb.id], car.bulb_ids
+ assert_no_queries { car.bulb_ids }
+
+ bulb2 = car.bulbs.create!
+
+ assert_equal [bulb.id, bulb2.id], car.bulb_ids
+ assert_no_queries { car.bulb_ids }
+ end
+
+ def test_loading_association_in_validate_callback_doesnt_affect_persistence
+ reset_callbacks(:validation, Bulb) do
+ Bulb.after_validation { |record| record.car.bulbs.load }
+
+ car = Car.create!(name: "Car")
+ bulb = car.bulbs.create!
+
+ assert_equal [bulb], car.bulbs
+ end
+ end
+
+ def test_create_children_could_be_rolled_back_by_after_save
+ firm = Firm.create!(name: "A New Firm, Inc")
+ assert_no_difference "Client.count" do
+ client = firm.clients.create(name: "New Client") do |cli|
+ cli.rollback_on_save = true
+ assert_not cli.rollback_on_create_called
+ end
+ assert client.rollback_on_create_called
+ end
+ end
+
+ private
+
+ def force_signal37_to_load_all_clients_of_firm
+ companies(:first_firm).clients_of_firm.load_target
+ end
+
+ def reset_callbacks(kind, klass)
+ old_callbacks = {}
+ old_callbacks[klass] = klass.send("_#{kind}_callbacks").dup
+ klass.subclasses.each do |subclass|
+ old_callbacks[subclass] = subclass.send("_#{kind}_callbacks").dup
+ end
+ yield
+ ensure
+ klass.send("_#{kind}_callbacks=", old_callbacks[klass])
+ klass.subclasses.each do |subclass|
+ subclass.send("_#{kind}_callbacks=", old_callbacks[subclass])
+ end
+ end
end
diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb
index c34387f06d..7b405c74c4 100644
--- a/activerecord/test/cases/associations/has_many_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -1,31 +1,38 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/post'
-require 'models/person'
-require 'models/reference'
-require 'models/job'
-require 'models/reader'
-require 'models/comment'
-require 'models/rating'
-require 'models/tag'
-require 'models/tagging'
-require 'models/author'
-require 'models/owner'
-require 'models/pet'
-require 'models/toy'
-require 'models/contract'
-require 'models/company'
-require 'models/developer'
-require 'models/computer'
-require 'models/subscriber'
-require 'models/book'
-require 'models/subscription'
-require 'models/essay'
-require 'models/category'
-require 'models/categorization'
-require 'models/member'
-require 'models/membership'
-require 'models/club'
-require 'models/organization'
+require "models/post"
+require "models/person"
+require "models/reference"
+require "models/job"
+require "models/reader"
+require "models/comment"
+require "models/rating"
+require "models/tag"
+require "models/tagging"
+require "models/author"
+require "models/owner"
+require "models/pet"
+require "models/pet_treasure"
+require "models/toy"
+require "models/treasure"
+require "models/contract"
+require "models/company"
+require "models/developer"
+require "models/computer"
+require "models/subscriber"
+require "models/book"
+require "models/subscription"
+require "models/essay"
+require "models/category"
+require "models/categorization"
+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,
@@ -35,8 +42,13 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
# Dummies to force column loads so query counts are clean.
def setup
- Person.create :first_name => 'gummy'
- Reader.create :person_id => 0, :post_id => 0
+ Person.create first_name: "gummy"
+ Reader.create person_id: 0, post_id: 0
+ end
+
+ def test_marshal_dump
+ preloaded = Post.includes(:first_blue_tags).first
+ assert_equal preloaded, Marshal.load(Marshal.dump(preloaded))
end
def test_preload_sti_rhs_class
@@ -47,9 +59,9 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
def test_preload_sti_middle_relation
- club = Club.create!(name: 'Aaron cool banana club')
- member1 = Member.create!(name: 'Aaron')
- member2 = Member.create!(name: 'Cat')
+ club = Club.create!(name: "Aaron cool banana club")
+ member1 = Member.create!(name: "Aaron")
+ member2 = Member.create!(name: "Cat")
SuperMembership.create! club: club, member: member1
CurrentMembership.create! club: club, member: member2
@@ -59,21 +71,26 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
club1.members.sort_by(&:id)
end
- def make_model(name)
- Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } }
+ def test_preload_multiple_instances_of_the_same_record
+ club = Club.create!(name: "Aaron cool banana club")
+ Membership.create! club: club, member: Member.create!(name: "Aaron")
+ Membership.create! club: club, member: Member.create!(name: "Bob")
+
+ preloaded_clubs = Club.joins(:memberships).preload(:membership).to_a
+ assert_no_queries { preloaded_clubs.each(&:membership) }
end
- def test_ordered_habtm
+ def test_ordered_has_many_through
person_prime = Class.new(ActiveRecord::Base) do
- def self.name; 'Person'; end
+ def self.name; "Person"; end
has_many :readers
- has_many :posts, -> { order('posts.id DESC') }, :through => :readers
+ has_many :posts, -> { order("posts.id DESC") }, through: :readers
end
posts = person_prime.includes(:posts).first.posts
assert_operator posts.length, :>, 1
- posts.each_cons(2) do |left,right|
+ posts.each_cons(2) do |left, right|
assert_operator left.id, :>, right.id
end
end
@@ -83,7 +100,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
subscription = make_model "Subscription"
subscriber = make_model "Subscriber"
- subscriber.primary_key = 'nick'
+ subscriber.primary_key = "nick"
subscription.belongs_to :book, anonymous_class: book
subscription.belongs_to :subscriber, anonymous_class: subscriber
@@ -104,8 +121,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
def test_no_pk_join_table_append
lesson, _, student = make_no_pk_hm_t
- sicp = lesson.new(:name => "SICP")
- ben = student.new(:name => "Ben Bitdiddle")
+ sicp = lesson.new(name: "SICP")
+ ben = student.new(name: "Ben Bitdiddle")
sicp.students << ben
assert sicp.save!
end
@@ -113,17 +130,17 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
def test_no_pk_join_table_delete
lesson, lesson_student, student = make_no_pk_hm_t
- sicp = lesson.new(:name => "SICP")
- ben = student.new(:name => "Ben Bitdiddle")
- louis = student.new(:name => "Louis Reasoner")
+ sicp = lesson.new(name: "SICP")
+ ben = student.new(name: "Ben Bitdiddle")
+ louis = student.new(name: "Louis Reasoner")
sicp.students << ben
sicp.students << louis
assert sicp.save!
sicp.students.reload
assert_operator lesson_student.count, :>=, 2
- assert_no_difference('student.count') do
- assert_difference('lesson_student.count', -2) do
+ assert_no_difference("student.count") do
+ assert_difference("lesson_student.count", -2) do
sicp.students.destroy(*student.all.to_a)
end
end
@@ -137,8 +154,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
after_destroy_called = true
end
- sicp = lesson.new(:name => "SICP")
- ben = student.new(:name => "Ben Bitdiddle")
+ sicp = lesson.new(name: "SICP")
+ ben = student.new(name: "Ben Bitdiddle")
sicp.students << ben
assert sicp.save!
@@ -147,20 +164,6 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert after_destroy_called, "after destroy should be called"
end
- def make_no_pk_hm_t
- lesson = make_model 'Lesson'
- student = make_model 'Student'
-
- lesson_student = make_model 'LessonStudent'
- lesson_student.table_name = 'lessons_students'
-
- lesson_student.belongs_to :lesson, :anonymous_class => lesson
- lesson_student.belongs_to :student, :anonymous_class => student
- lesson.has_many :lesson_students, :anonymous_class => lesson_student
- lesson.has_many :students, :through => :lesson_students, :anonymous_class => student
- [lesson, lesson_student, student]
- end
-
def test_pk_is_not_required_for_join
post = Post.includes(:scategories).first
post2 = Post.includes(:categories).first
@@ -173,7 +176,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
person = Person.new
post = Post.new
person.posts << post
- assert person.posts.include?(post)
+ assert_includes person.posts, post
end
def test_associate_existing
@@ -185,18 +188,18 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
assert_queries(1) do
- assert post.people.include?(person)
+ assert_includes post.people, person
end
- assert post.reload.people(true).include?(person)
+ assert_includes post.reload.people.reload, person
end
def test_delete_all_for_with_dependent_option_destroy
person = people(:david)
assert_equal 1, person.jobs_with_dependent_destroy.count
- assert_no_difference 'Job.count' do
- assert_difference 'Reference.count', -1 do
+ assert_no_difference "Job.count" do
+ assert_difference "Reference.count", -1 do
person.reload.jobs_with_dependent_destroy.delete_all
end
end
@@ -206,8 +209,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
person = people(:david)
assert_equal 1, person.jobs_with_dependent_nullify.count
- assert_no_difference 'Job.count' do
- assert_no_difference 'Reference.count' do
+ assert_no_difference "Job.count" do
+ assert_no_difference "Reference.count" do
person.reload.jobs_with_dependent_nullify.delete_all
end
end
@@ -217,8 +220,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
person = people(:david)
assert_equal 1, person.jobs_with_dependent_delete_all.count
- assert_no_difference 'Job.count' do
- assert_difference 'Reference.count', -1 do
+ assert_no_difference "Job.count" do
+ assert_difference "Reference.count", -1 do
person.reload.jobs_with_dependent_delete_all.delete_all
end
end
@@ -229,14 +232,14 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
post = posts(:thinking)
post.people.concat [person]
assert_equal 1, post.people.size
- assert_equal 1, post.people(true).size
+ assert_equal 1, post.people.reload.size
end
def test_associate_existing_record_twice_should_add_to_target_twice
post = posts(:thinking)
person = people(:david)
- assert_difference 'post.people.to_a.count', 2 do
+ assert_difference "post.people.to_a.count", 2 do
post.people << person
post.people << person
end
@@ -246,7 +249,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
post = posts(:thinking)
person = people(:david)
- assert_difference 'post.people.count', 2 do
+ assert_difference "post.people.count", 2 do
post.people << person
post.people << person
end
@@ -259,20 +262,23 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
post.people << person
post.people << person
- counts = ['post.people.count', 'post.people.to_a.count', 'post.readers.count', 'post.readers.to_a.count']
+ counts = ["post.people.count", "post.people.to_a.count", "post.readers.count", "post.readers.to_a.count"]
assert_difference counts, -2 do
post.people.delete(person)
end
- assert !post.people.reload.include?(person)
+ assert_not_includes post.people.reload, person
end
def test_associating_new
assert_queries(1) { posts(:thinking) }
new_person = nil # so block binding catches it
- assert_queries(0) do
- new_person = Person.new :first_name => 'bob'
+ # Load schema information so we don't query below if running just this test.
+ Person.define_attribute_methods
+
+ assert_no_queries do
+ new_person = Person.new first_name: "bob"
end
# Associating new records always saves them
@@ -282,59 +288,73 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
assert_queries(1) do
- assert posts(:thinking).people.include?(new_person)
+ assert_includes posts(:thinking).people, new_person
end
- assert posts(:thinking).reload.people(true).include?(new_person)
+ assert_includes posts(:thinking).reload.people.reload, new_person
end
def test_associate_new_by_building
assert_queries(1) { posts(:thinking) }
- assert_queries(0) do
- posts(:thinking).people.build(:first_name => "Bob")
- posts(:thinking).people.new(:first_name => "Ted")
+ # Load schema information so we don't query below if running just this test.
+ Person.define_attribute_methods
+
+ assert_no_queries do
+ posts(:thinking).people.build(first_name: "Bob")
+ posts(:thinking).people.new(first_name: "Ted")
end
# Should only need to load the association once
assert_queries(1) do
- assert posts(:thinking).people.collect(&:first_name).include?("Bob")
- assert posts(:thinking).people.collect(&:first_name).include?("Ted")
+ assert_includes posts(:thinking).people.collect(&:first_name), "Bob"
+ assert_includes posts(:thinking).people.collect(&:first_name), "Ted"
end
# 2 queries for each new record (1 to save the record itself, 1 for the join model)
# * 2 new records = 4
# + 1 query to save the actual post = 5
assert_queries(5) do
- posts(:thinking).body += '-changed'
+ posts(:thinking).body += "-changed"
posts(:thinking).save
end
- assert posts(:thinking).reload.people(true).collect(&:first_name).include?("Bob")
- assert posts(:thinking).reload.people(true).collect(&:first_name).include?("Ted")
+ assert_includes posts(:thinking).reload.people.reload.collect(&:first_name), "Bob"
+ assert_includes posts(:thinking).reload.people.reload.collect(&:first_name), "Ted"
end
def test_build_then_save_with_has_many_inverse
post = posts(:thinking)
- person = post.people.build(:first_name => "Bob")
+ person = post.people.build(first_name: "Bob")
person.save
post.reload
- assert post.people.include?(person)
+ assert_includes post.people, person
end
def test_build_then_save_with_has_one_inverse
post = posts(:thinking)
- person = post.single_people.build(:first_name => "Bob")
+ person = post.single_people.build(first_name: "Bob")
person.save
post.reload
- assert post.single_people.include?(person)
+ assert_includes post.single_people, person
+ end
+
+ def test_build_then_remove_then_save
+ post = posts(:thinking)
+ post.people.build(first_name: "Bob")
+ ted = post.people.build(first_name: "Ted")
+ post.people.delete(ted)
+ post.save!
+ post.reload
+
+ assert_equal ["Bob"], post.people.collect(&:first_name)
end
def test_both_parent_ids_set_when_saving_new
- post = Post.new(title: 'Hello', body: 'world')
- person = Person.new(first_name: 'Sean')
+ post = Post.new(title: "Hello", body: "world")
+ person = Person.new(first_name: "Sean")
post.people = [person]
post.save
@@ -346,17 +366,17 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
def test_delete_association
- assert_queries(2){posts(:welcome);people(:michael); }
+ assert_queries(2) { posts(:welcome);people(:michael); }
assert_queries(1) do
posts(:welcome).people.delete(people(:michael))
end
assert_queries(1) do
- assert posts(:welcome).people.empty?
+ assert_empty posts(:welcome).people
end
- assert posts(:welcome).reload.people(true).empty?
+ assert_empty posts(:welcome).reload.people.reload
end
def test_destroy_association
@@ -366,8 +386,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
end
- assert posts(:welcome).reload.people.empty?
- assert posts(:welcome).people(true).empty?
+ assert_empty posts(:welcome).reload.people
+ assert_empty posts(:welcome).people.reload
end
def test_destroy_all
@@ -377,8 +397,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
end
- assert posts(:welcome).reload.people.empty?
- assert posts(:welcome).people(true).empty?
+ assert_empty posts(:welcome).reload.people
+ assert_empty posts(:welcome).people.reload
end
def test_should_raise_exception_for_destroying_mismatching_records
@@ -392,15 +412,15 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
person = people(:michael)
job = jobs(:magician)
- reference = Reference.where(:job_id => job.id, :person_id => person.id).first
+ reference = Reference.where(job_id: job.id, person_id: person.id).first
- assert_no_difference ['Job.count', 'Reference.count'] do
- assert_difference 'person.jobs.count', -1 do
+ assert_no_difference ["Job.count", "Reference.count"] do
+ assert_difference "person.jobs.count", -1 do
person.jobs_with_dependent_nullify.delete(job)
end
end
- assert_equal nil, reference.reload.job_id
+ assert_nil reference.reload.job_id
ensure
Reference.make_comments = false
end
@@ -414,14 +434,14 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
# Make sure we're not deleting everything
assert person.jobs.count >= 2
- assert_no_difference 'Job.count' do
- assert_difference ['person.jobs.count', 'Reference.count'], -1 do
+ assert_no_difference "Job.count" do
+ assert_difference ["person.jobs.count", "Reference.count"], -1 do
person.jobs_with_dependent_delete_all.delete(job)
end
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
@@ -435,8 +455,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
# Make sure we're not deleting everything
assert person.jobs.count >= 2
- assert_no_difference 'Job.count' do
- assert_difference ['person.jobs.count', 'Reference.count'], -1 do
+ assert_no_difference "Job.count" do
+ assert_difference ["person.jobs.count", "Reference.count"], -1 do
person.jobs_with_dependent_destroy.delete(job)
end
end
@@ -453,8 +473,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
# Create a reference which is not linked to a job. This should not be destroyed.
person.references.create!
- assert_no_difference 'Job.count' do
- assert_difference 'Reference.count', -person.jobs.count do
+ assert_no_difference "Job.count" do
+ assert_difference "Reference.count", -person.jobs.count do
person.destroy
end
end
@@ -466,8 +486,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
# Create a reference which is not linked to a job. This should not be destroyed.
person.references.create!
- assert_no_difference 'Job.count' do
- assert_difference 'Reference.count', -person.jobs.count do
+ assert_no_difference "Job.count" do
+ assert_difference "Reference.count", -person.jobs.count do
person.destroy
end
end
@@ -478,41 +498,41 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
references = person.references.to_a
- assert_no_difference ['Reference.count', 'Job.count'] do
+ assert_no_difference ["Reference.count", "Job.count"] do
person.destroy
end
references.each do |reference|
- assert_equal nil, reference.reload.job_id
+ assert_nil reference.reload.job_id
end
end
def test_update_counter_caches_on_delete
post = posts(:welcome)
- tag = post.tags.create!(:name => 'doomed')
+ tag = post.tags.create!(name: "doomed")
- assert_difference ['post.reload.tags_count'], -1 do
+ assert_difference ["post.reload.tags_count"], -1 do
posts(:welcome).tags.delete(tag)
end
end
def test_update_counter_caches_on_delete_with_dependent_destroy
post = posts(:welcome)
- tag = post.tags.create!(:name => 'doomed')
+ tag = post.tags.create!(name: "doomed")
post.update_columns(tags_with_destroy_count: post.tags.count)
- assert_difference ['post.reload.tags_with_destroy_count'], -1 do
+ assert_difference ["post.reload.tags_with_destroy_count"], -1 do
posts(:welcome).tags_with_destroy.delete(tag)
end
end
def test_update_counter_caches_on_delete_with_dependent_nullify
post = posts(:welcome)
- tag = post.tags.create!(:name => 'doomed')
+ tag = post.tags.create!(name: "doomed")
post.update_columns(tags_with_nullify_count: post.tags.count)
- assert_no_difference 'post.reload.tags_count' do
- assert_difference 'post.reload.tags_with_nullify_count', -1 do
+ assert_no_difference "post.reload.tags_count" do
+ assert_difference "post.reload.tags_with_nullify_count", -1 do
posts(:welcome).tags_with_nullify.delete(tag)
end
end
@@ -520,7 +540,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
def test_update_counter_caches_on_replace_association
post = posts(:welcome)
- tag = post.tags.create!(:name => 'doomed')
+ tag = post.tags.create!(name: "doomed")
tag.tagged_posts << posts(:thinking)
tag.tagged_posts = []
@@ -531,15 +551,25 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
def test_update_counter_caches_on_destroy
post = posts(:welcome)
- tag = post.tags.create!(name: 'doomed')
+ tag = post.tags.create!(name: "doomed")
- assert_difference 'post.reload.tags_count', -1 do
+ assert_difference "post.reload.tags_count", -1 do
tag.tagged_posts.destroy(post)
end
end
+ def test_update_counter_caches_on_destroy_with_indestructible_through_record
+ post = posts(:welcome)
+ tag = post.indestructible_tags.create!(name: "doomed")
+ post.update_columns(indestructible_tags_count: post.indestructible_tags.count)
+
+ assert_no_difference "post.reload.indestructible_tags_count" do
+ posts(:welcome).indestructible_tags.destroy(tag)
+ end
+ end
+
def test_replace_association
- assert_queries(4){posts(:welcome);people(:david);people(:michael); posts(:welcome).people(true)}
+ assert_queries(4) { posts(:welcome);people(:david);people(:michael); posts(:welcome).people.reload }
# 1 query to delete the existing reader (michael)
# 1 query to associate the new reader (david)
@@ -547,35 +577,35 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
posts(:welcome).people = [people(:david)]
end
- assert_queries(0){
- assert posts(:welcome).people.include?(people(:david))
- assert !posts(:welcome).people.include?(people(:michael))
- }
+ assert_no_queries do
+ assert_includes posts(:welcome).people, people(:david)
+ assert_not_includes posts(:welcome).people, people(:michael)
+ end
- assert posts(:welcome).reload.people(true).include?(people(:david))
- assert !posts(:welcome).reload.people(true).include?(people(:michael))
+ assert_includes posts(:welcome).reload.people.reload, people(:david)
+ assert_not_includes posts(:welcome).reload.people.reload, people(:michael)
end
def test_replace_order_is_preserved
posts(:welcome).people.clear
posts(:welcome).people = [people(:david), people(:michael)]
- assert_equal [people(:david).id, people(:michael).id], posts(:welcome).readers.order('id').map(&:person_id)
+ assert_equal [people(:david).id, people(:michael).id], posts(:welcome).readers.order("id").map(&:person_id)
# Test the inverse order in case the first success was a coincidence
posts(:welcome).people.clear
posts(:welcome).people = [people(:michael), people(:david)]
- assert_equal [people(:michael).id, people(:david).id], posts(:welcome).readers.order('id').map(&:person_id)
+ assert_equal [people(:michael).id, people(:david).id], posts(:welcome).readers.order("id").map(&:person_id)
end
def test_replace_by_id_order_is_preserved
posts(:welcome).people.clear
posts(:welcome).person_ids = [people(:david).id, people(:michael).id]
- assert_equal [people(:david).id, people(:michael).id], posts(:welcome).readers.order('id').map(&:person_id)
+ assert_equal [people(:david).id, people(:michael).id], posts(:welcome).readers.order("id").map(&:person_id)
# Test the inverse order in case the first success was a coincidence
posts(:welcome).people.clear
posts(:welcome).person_ids = [people(:michael).id, people(:david).id]
- assert_equal [people(:michael).id, people(:david).id], posts(:welcome).readers.order('id').map(&:person_id)
+ assert_equal [people(:michael).id, people(:david).id], posts(:welcome).readers.order("id").map(&:person_id)
end
def test_associate_with_create
@@ -584,15 +614,15 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
# 1 query for the new record, 1 for the join table record
# No need to update the actual collection yet!
assert_queries(2) do
- posts(:thinking).people.create(:first_name=>"Jeb")
+ posts(:thinking).people.create(first_name: "Jeb")
end
# *Now* we actually need the collection so it's loaded
assert_queries(1) do
- assert posts(:thinking).people.collect(&:first_name).include?("Jeb")
+ assert_includes posts(:thinking).people.collect(&:first_name), "Jeb"
end
- assert posts(:thinking).reload.people(true).collect(&:first_name).include?("Jeb")
+ assert_includes posts(:thinking).reload.people.reload.collect(&:first_name), "Jeb"
end
def test_through_record_is_built_when_created_with_where
@@ -603,82 +633,82 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
def test_associate_with_create_and_no_options
peeps = posts(:thinking).people.count
- posts(:thinking).people.create(:first_name => 'foo')
+ posts(:thinking).people.create(first_name: "foo")
assert_equal peeps + 1, posts(:thinking).people.count
end
def test_associate_with_create_with_through_having_conditions
impatient_people = posts(:thinking).impatient_people.count
- posts(:thinking).impatient_people.create!(:first_name => 'foo')
+ posts(:thinking).impatient_people.create!(first_name: "foo")
assert_equal impatient_people + 1, posts(:thinking).impatient_people.count
end
def test_associate_with_create_exclamation_and_no_options
peeps = posts(:thinking).people.count
- posts(:thinking).people.create!(:first_name => 'foo')
+ posts(:thinking).people.create!(first_name: "foo")
assert_equal peeps + 1, posts(:thinking).people.count
end
def test_create_on_new_record
p = Post.new
- error = assert_raises(ActiveRecord::RecordNotSaved) { p.people.create(:first_name => "mew") }
+ error = assert_raises(ActiveRecord::RecordNotSaved) { p.people.create(first_name: "mew") }
assert_equal "You cannot call create unless the parent is saved", error.message
- error = assert_raises(ActiveRecord::RecordNotSaved) { p.people.create!(:first_name => "snow") }
+ error = assert_raises(ActiveRecord::RecordNotSaved) { p.people.create!(first_name: "snow") }
assert_equal "You cannot call create unless the parent is saved", error.message
end
def test_associate_with_create_and_invalid_options
firm = companies(:first_firm)
- assert_no_difference('firm.developers.count') { assert_nothing_raised { firm.developers.create(:name => '0') } }
+ assert_no_difference("firm.developers.count") { assert_nothing_raised { firm.developers.create(name: "0") } }
end
def test_associate_with_create_and_valid_options
firm = companies(:first_firm)
- assert_difference('firm.developers.count', 1) { firm.developers.create(:name => 'developer') }
+ assert_difference("firm.developers.count", 1) { firm.developers.create(name: "developer") }
end
def test_associate_with_create_bang_and_invalid_options
firm = companies(:first_firm)
- assert_no_difference('firm.developers.count') { assert_raises(ActiveRecord::RecordInvalid) { firm.developers.create!(:name => '0') } }
+ assert_no_difference("firm.developers.count") { assert_raises(ActiveRecord::RecordInvalid) { firm.developers.create!(name: "0") } }
end
def test_associate_with_create_bang_and_valid_options
firm = companies(:first_firm)
- assert_difference('firm.developers.count', 1) { firm.developers.create!(:name => 'developer') }
+ assert_difference("firm.developers.count", 1) { firm.developers.create!(name: "developer") }
end
def test_push_with_invalid_record
firm = companies(:first_firm)
- assert_raises(ActiveRecord::RecordInvalid) { firm.developers << Developer.new(:name => '0') }
+ assert_raises(ActiveRecord::RecordInvalid) { firm.developers << Developer.new(name: "0") }
end
def test_push_with_invalid_join_record
repair_validations(Contract) do
- Contract.validate {|r| r.errors[:base] << 'Invalid Contract' }
+ Contract.validate { |r| r.errors[:base] << "Invalid Contract" }
firm = companies(:first_firm)
- lifo = Developer.new(:name => 'lifo')
+ lifo = Developer.new(name: "lifo")
assert_raises(ActiveRecord::RecordInvalid) { firm.developers << lifo }
- lifo = Developer.create!(:name => 'lifo')
+ lifo = Developer.create!(name: "lifo")
assert_raises(ActiveRecord::RecordInvalid) { firm.developers << lifo }
end
end
def test_clear_associations
- assert_queries(2) { posts(:welcome);posts(:welcome).people(true) }
+ assert_queries(2) { posts(:welcome);posts(:welcome).people.reload }
assert_queries(1) do
posts(:welcome).people.clear
end
- assert_queries(0) do
- assert posts(:welcome).people.empty?
+ assert_no_queries do
+ assert_empty posts(:welcome).people
end
- assert posts(:welcome).reload.people(true).empty?
+ assert_empty posts(:welcome).reload.people.reload
end
def test_association_callback_ordering
@@ -692,7 +722,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
[:added, :after, "Michael"]
], log.last(2)
- post.people_with_callbacks.push(people(:david), Person.create!(:first_name => "Bob"), Person.new(:first_name => "Lary"))
+ post.people_with_callbacks.push(people(:david), Person.create!(first_name: "Bob"), Person.new(first_name: "Lary"))
assert_equal [
[:added, :before, "David"],
[:added, :after, "David"],
@@ -700,21 +730,21 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
[:added, :after, "Bob"],
[:added, :before, "Lary"],
[:added, :after, "Lary"]
- ],log.last(6)
+ ], log.last(6)
- post.people_with_callbacks.build(:first_name => "Ted")
+ post.people_with_callbacks.build(first_name: "Ted")
assert_equal [
[:added, :before, "Ted"],
[:added, :after, "Ted"]
], log.last(2)
- post.people_with_callbacks.create(:first_name => "Sam")
+ post.people_with_callbacks.create(first_name: "Sam")
assert_equal [
[:added, :before, "Sam"],
[:added, :after, "Sam"]
], log.last(2)
- post.people_with_callbacks = [people(:michael),people(:david), Person.new(:first_name => "Julian"), Person.create!(:first_name => "Roger")]
+ post.people_with_callbacks = [people(:michael), people(:david), Person.new(first_name: "Julian"), Person.create!(first_name: "Roger")]
assert_equal((%w(Ted Bob Sam Lary) * 2).sort, log[-12..-5].collect(&:last).sort)
assert_equal [
[:added, :before, "Julian"],
@@ -722,12 +752,24 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
[:added, :before, "Roger"],
[:added, :after, "Roger"]
], log.last(4)
+
+ post.people_with_callbacks.build { |person| person.first_name = "Ted" }
+ assert_equal [
+ [:added, :before, "Ted"],
+ [:added, :after, "Ted"]
+ ], log.last(2)
+
+ post.people_with_callbacks.create { |person| person.first_name = "Sam" }
+ assert_equal [
+ [:added, :before, "Sam"],
+ [:added, :after, "Sam"]
+ ], log.last(2)
end
def test_dynamic_find_should_respect_association_include
# SQL error in sort clause if :include is not included
# due to Unknown column 'comments.id'
- assert Person.find(1).posts_with_comments_sorted_by_comment_id.find_by_title('Welcome to the weblog')
+ assert Person.find(1).posts_with_comments_sorted_by_comment_id.find_by_title("Welcome to the weblog")
end
def test_count_with_include_should_alias_join_table
@@ -743,15 +785,16 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
def test_get_ids_for_has_many_through_with_conditions_should_not_preload
- Tagging.create!(:taggable_type => 'Post', :taggable_id => posts(:welcome).id, :tag => tags(:misc))
- ActiveRecord::Associations::Preloader.expects(:new).never
- posts(:welcome).misc_tag_ids
+ Tagging.create!(taggable_type: "Post", taggable_id: posts(:welcome).id, tag: tags(:misc))
+ assert_not_called(ActiveRecord::Associations::Preloader, :new) do
+ posts(:welcome).misc_tag_ids
+ end
end
def test_get_ids_for_loaded_associations
person = people(:michael)
- person.posts(true)
- assert_queries(0) do
+ person.posts.reload
+ assert_no_queries do
person.post_ids
person.post_ids
end
@@ -759,29 +802,30 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
def test_get_ids_for_unloaded_associations_does_not_load_them
person = people(:michael)
- assert !person.posts.loaded?
+ assert_not_predicate person.posts, :loaded?
assert_equal [posts(:welcome).id, posts(:authorless).id].sort, person.post_ids.sort
- assert !person.posts.loaded?
+ assert_not_predicate person.posts, :loaded?
end
def test_association_proxy_transaction_method_starts_transaction_in_association_class
- Tag.expects(:transaction)
- Post.first.tags.transaction do
- # nothing
+ assert_called(Tag, :transaction) do
+ Post.first.tags.transaction do
+ # nothing
+ end
end
end
def test_has_many_association_through_a_belongs_to_association_where_the_association_doesnt_exist
- post = Post.create!(:title => "TITLE", :body => "BODY")
+ post = Post.create!(title: "TITLE", body: "BODY")
assert_equal [], post.author_favorites
end
def test_has_many_association_through_a_belongs_to_association
author = authors(:mary)
- post = Post.create!(:author => author, :title => "TITLE", :body => "BODY")
- author.author_favorites.create(:favorite_author_id => 1)
- author.author_favorites.create(:favorite_author_id => 2)
- author.author_favorites.create(:favorite_author_id => 3)
+ post = Post.create!(author: author, title: "TITLE", body: "BODY")
+ author.author_favorites.create(favorite_author_id: 1)
+ author.author_favorites.create(favorite_author_id: 2)
+ author.author_favorites.create(favorite_author_id: 3)
assert_equal post.author.author_favorites, post.author_favorites
end
@@ -805,37 +849,37 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
def test_modifying_has_many_through_has_one_reflection_should_raise
[
- lambda { authors(:david).very_special_comments = [VerySpecialComment.create!(:body => "Gorp!", :post_id => 1011), VerySpecialComment.create!(:body => "Eep!", :post_id => 1012)] },
- lambda { authors(:david).very_special_comments << VerySpecialComment.create!(:body => "Hoohah!", :post_id => 1013) },
+ lambda { authors(:david).very_special_comments = [VerySpecialComment.create!(body: "Gorp!", post_id: 1011), VerySpecialComment.create!(body: "Eep!", post_id: 1012)] },
+ lambda { authors(:david).very_special_comments << VerySpecialComment.create!(body: "Hoohah!", post_id: 1013) },
lambda { authors(:david).very_special_comments.delete(authors(:david).very_special_comments.first) },
- ].each {|block| assert_raise(ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection, &block) }
+ ].each { |block| assert_raise(ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection, &block) }
end
def test_has_many_association_through_a_has_many_association_to_self
- sarah = Person.create!(:first_name => 'Sarah', :primary_contact_id => people(:susan).id, :gender => 'F', :number1_fan_id => 1)
- john = Person.create!(:first_name => 'John', :primary_contact_id => sarah.id, :gender => 'M', :number1_fan_id => 1)
+ sarah = Person.create!(first_name: "Sarah", primary_contact_id: people(:susan).id, gender: "F", number1_fan_id: 1)
+ john = Person.create!(first_name: "John", primary_contact_id: sarah.id, gender: "M", number1_fan_id: 1)
assert_equal sarah.agents, [john]
- assert_equal people(:susan).agents.flat_map(&:agents), people(:susan).agents_of_agents
+ assert_equal people(:susan).agents.flat_map(&:agents).sort, people(:susan).agents_of_agents.sort
end
def test_associate_existing_with_nonstandard_primary_key_on_belongs_to
- Categorization.create(:author => authors(:mary), :named_category_name => categories(:general).name)
+ Categorization.create(author: authors(:mary), named_category_name: categories(:general).name)
assert_equal categories(:general), authors(:mary).named_categories.first
end
def test_collection_build_with_nonstandard_primary_key_on_belongs_to
author = authors(:mary)
- category = author.named_categories.build(:name => "Primary")
+ category = author.named_categories.build(name: "Primary")
author.save
- assert Categorization.exists?(:author_id => author.id, :named_category_name => category.name)
- assert author.named_categories(true).include?(category)
+ assert Categorization.exists?(author_id: author.id, named_category_name: category.name)
+ assert_includes author.named_categories.reload, category
end
def test_collection_create_with_nonstandard_primary_key_on_belongs_to
author = authors(:mary)
- category = author.named_categories.create(:name => "Primary")
- assert Categorization.exists?(:author_id => author.id, :named_category_name => category.name)
- assert author.named_categories(true).include?(category)
+ category = author.named_categories.create(name: "Primary")
+ assert Categorization.exists?(author_id: author.id, named_category_name: category.name)
+ assert_includes author.named_categories.reload, category
end
def test_collection_exists
@@ -847,10 +891,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
def test_collection_delete_with_nonstandard_primary_key_on_belongs_to
author = authors(:mary)
- category = author.named_categories.create(:name => "Primary")
+ category = author.named_categories.create(name: "Primary")
author.named_categories.delete(category)
- assert !Categorization.exists?(:author_id => author.id, :named_category_name => category.name)
- assert author.named_categories(true).empty?
+ assert_not Categorization.exists?(author_id: author.id, named_category_name: category.name)
+ assert_empty author.named_categories.reload
end
def test_collection_singular_ids_getter_with_string_primary_keys
@@ -867,35 +911,52 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_equal [dev], company.developers
end
+ def test_collection_singular_ids_setter_with_required_type_cast
+ company = companies(:rails_core)
+ dev = Developer.first
+
+ company.developer_ids = [dev.id.to_s]
+ assert_equal [dev], company.developers
+ end
+
def test_collection_singular_ids_setter_with_string_primary_keys
assert_nothing_raised do
book = books(:awdr)
book.subscriber_ids = [subscribers(:second).nick]
- assert_equal [subscribers(:second)], book.subscribers(true)
+ assert_equal [subscribers(:second)], book.subscribers.reload
book.subscriber_ids = []
- assert_equal [], book.subscribers(true)
+ assert_equal [], book.subscribers.reload
end
-
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::RecordNotFound) {company.developer_ids= ids}
+ ids = [Developer.first.id, -9999]
+ e = assert_raises(ActiveRecord::RecordNotFound) { company.developer_ids = ids }
+ msg = "Couldn't find all Developers with 'id': (1, -9999) (found 1 results, but was looking for 2). Couldn't find Developer with id -9999."
+ assert_equal(msg, 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 }
+ msg = "Couldn't find all Categories with 'name': (General, Unknown) (found 1 results, but was looking for 2). Couldn't find Category with name Unknown."
+ assert_equal msg, e.message
end
def test_build_a_model_from_hm_through_association_with_where_clause
- assert_nothing_raised { books(:awdr).subscribers.where(:nick => "marklazz").build }
+ assert_nothing_raised { books(:awdr).subscribers.where(nick: "marklazz").build }
end
def test_attributes_are_being_set_when_initialized_from_hm_through_association_with_where_clause
- new_subscriber = books(:awdr).subscribers.where(:nick => "marklazz").build
+ new_subscriber = books(:awdr).subscribers.where(nick: "marklazz").build
assert_equal new_subscriber.nick, "marklazz"
end
def test_attributes_are_being_set_when_initialized_from_hm_through_association_with_multiple_where_clauses
- new_subscriber = books(:awdr).subscribers.where(:nick => "marklazz").where(:name => 'Marcelo Giorgi').build
+ new_subscriber = books(:awdr).subscribers.where(nick: "marklazz").where(name: "Marcelo Giorgi").build
assert_equal new_subscriber.nick, "marklazz"
assert_equal new_subscriber.name, "Marcelo Giorgi"
end
@@ -904,19 +965,19 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
person = Person.new
reference = person.references.build
job = reference.build_job
- assert person.jobs.include?(job)
+ assert_includes person.jobs, job
end
def test_include_method_in_association_through_should_return_true_for_instance_added_with_nested_builds
author = Author.new
post = author.posts.build
comment = post.comments.build
- assert author.comments.include?(comment)
+ assert_includes author.comments, comment
end
def test_through_association_readonly_should_be_false
- assert !people(:michael).posts.first.readonly?
- assert !people(:michael).posts.to_a.first.readonly?
+ assert_not_predicate people(:michael).posts.first, :readonly?
+ assert_not_predicate people(:michael).posts.to_a.first, :readonly?
end
def test_can_update_through_association
@@ -925,10 +986,17 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
end
+ def test_has_many_through_polymorphic_with_rewhere
+ post = TaggedPost.create!(title: "Tagged", body: "Post")
+ tag = post.tags.create!(name: "Tag")
+ assert_equal [tag], TaggedPost.preload(:tags).last.tags
+ assert_equal [tag], TaggedPost.eager_load(:tags).last.tags
+ end
+
def test_has_many_through_polymorphic_with_primary_key_option
assert_equal [categories(:general)], authors(:david).essay_categories
- authors = Author.joins(:essay_categories).where('categories.id' => categories(:general).id)
+ authors = Author.joins(:essay_categories).where("categories.id" => categories(:general).id)
assert_equal authors(:david), authors.first
assert_equal [owners(:blackbeard)], authors(:david).essay_owners
@@ -940,7 +1008,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
def test_has_many_through_with_primary_key_option
assert_equal [categories(:general)], authors(:david).essay_categories_2
- authors = Author.joins(:essay_categories_2).where('categories.id' => categories(:general).id)
+ authors = Author.joins(:essay_categories_2).where("categories.id" => categories(:general).id)
assert_equal authors(:david), authors.first
end
@@ -952,30 +1020,30 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
def test_has_many_through_with_default_scope_on_join_model
- assert_equal posts(:welcome).comments.order('id').to_a, authors(:david).comments_on_first_posts
+ assert_equal posts(:welcome).comments.order("id").to_a, authors(:david).comments_on_first_posts
end
def test_create_has_many_through_with_default_scope_on_join_model
- category = authors(:david).special_categories.create(:name => "Foo")
- assert_equal 1, category.categorizations.where(:special => true).count
+ category = authors(:david).special_categories.create(name: "Foo")
+ assert_equal 1, category.categorizations.where(special: true).count
end
def test_joining_has_many_through_with_distinct
- mary = Author.joins(:unique_categorized_posts).where(:id => authors(:mary).id).first
+ mary = Author.joins(:unique_categorized_posts).where(id: authors(:mary).id).first
assert_equal 1, mary.unique_categorized_posts.length
assert_equal 1, mary.unique_categorized_post_ids.length
end
def test_joining_has_many_through_belongs_to
- posts = Post.joins(:author_categorizations).order('posts.id').
- where('categorizations.id' => categorizations(:mary_thinking_sti).id)
+ posts = Post.joins(:author_categorizations).order("posts.id").
+ where("categorizations.id" => categorizations(:mary_thinking_sti).id)
assert_equal [posts(:eager_other), posts(:misc_by_mary), posts(:other_by_mary)], posts
end
def test_select_chosen_fields_only
author = authors(:david)
- assert_equal ['body', 'id'].sort, author.comments.select('comments.body').first.attributes.keys.sort
+ assert_equal ["body", "id"].sort, author.comments.select("comments.body").first.attributes.keys.sort
end
def test_get_has_many_through_belongs_to_ids_with_conditions
@@ -998,12 +1066,12 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
post.author_categorizations
proxy = post.send(:association_instance_get, :author_categorizations)
- assert !proxy.stale_target?
+ assert_not_predicate proxy, :stale_target?
assert_equal authors(:mary).categorizations.sort_by(&:id), post.author_categorizations.sort_by(&:id)
post.author_id = authors(:david).id
- assert proxy.stale_target?
+ assert_predicate proxy, :stale_target?
assert_equal authors(:david).categorizations.sort_by(&:id), post.author_categorizations.sort_by(&:id)
end
@@ -1018,15 +1086,15 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
post = posts(:welcome)
address = author_addresses(:david_address)
- assert post.author_addresses.include?(address)
+ assert_includes post.author_addresses, address
post.author_addresses.delete(address)
- assert post[:author_count].nil?
+ assert_predicate post[:author_count], :nil?
end
def test_primary_key_option_on_source
post = posts(:welcome)
category = categories(:general)
- Categorization.create!(:post_id => post.id, :named_category_name => category.name)
+ Categorization.create!(post_id: post.id, named_category_name: category.name)
assert_equal [category], post.named_categories
assert_equal [category.name], post.named_category_ids # checks when target loaded
@@ -1035,29 +1103,29 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
def test_create_should_not_raise_exception_when_join_record_has_errors
repair_validations(Categorization) do
- Categorization.validate { |r| r.errors[:base] << 'Invalid Categorization' }
- Category.create(:name => 'Fishing', :authors => [Author.first])
+ Categorization.validate { |r| r.errors[:base] << "Invalid Categorization" }
+ Category.create(name: "Fishing", authors: [Author.first])
end
end
def test_assign_array_to_new_record_builds_join_records
- c = Category.new(:name => 'Fishing', :authors => [Author.first])
+ c = Category.new(name: "Fishing", authors: [Author.first])
assert_equal 1, c.categorizations.size
end
def test_create_bang_should_raise_exception_when_join_record_has_errors
repair_validations(Categorization) do
- Categorization.validate { |r| r.errors[:base] << 'Invalid Categorization' }
+ Categorization.validate { |r| r.errors[:base] << "Invalid Categorization" }
assert_raises(ActiveRecord::RecordInvalid) do
- Category.create!(:name => 'Fishing', :authors => [Author.first])
+ Category.create!(name: "Fishing", authors: [Author.first])
end
end
end
def test_save_bang_should_raise_exception_when_join_record_has_errors
repair_validations(Categorization) do
- Categorization.validate { |r| r.errors[:base] << 'Invalid Categorization' }
- c = Category.new(:name => 'Fishing', :authors => [Author.first])
+ Categorization.validate { |r| r.errors[:base] << "Invalid Categorization" }
+ c = Category.new(name: "Fishing", authors: [Author.first])
assert_raises(ActiveRecord::RecordInvalid) do
c.save!
end
@@ -1066,41 +1134,79 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
def test_save_returns_falsy_when_join_record_has_errors
repair_validations(Categorization) do
- Categorization.validate { |r| r.errors[:base] << 'Invalid Categorization' }
- c = Category.new(:name => 'Fishing', :authors => [Author.first])
+ Categorization.validate { |r| r.errors[:base] << "Invalid Categorization" }
+ c = Category.new(name: "Fishing", authors: [Author.first])
assert_not c.save
end
end
def test_preloading_empty_through_association_via_joins
- person = Person.create!(:first_name => "Gaga")
- person = Person.where(:id => person.id).where('readers.id = 1 or 1=1').references(:readers).includes(:posts).to_a.first
+ person = Person.create!(first_name: "Gaga")
+ person = Person.where(id: person.id).where("readers.id = 1 or 1=1").references(:readers).includes(:posts).to_a.first
- assert person.posts.loaded?, 'person.posts should be loaded'
+ assert person.posts.loaded?, "person.posts should be loaded"
assert_equal [], person.posts
end
+ def test_preloading_empty_through_with_polymorphic_source_association
+ owner = Owner.create!(name: "Rainbow Unicat")
+ pet = Pet.create!(owner: owner)
+ person = Person.create!(first_name: "Gaga")
+ treasure = Treasure.create!(looter: person)
+ non_looted_treasure = Treasure.create!()
+ PetTreasure.create!(pet: pet, treasure: treasure, rainbow_color: "Ultra violet indigo")
+ PetTreasure.create!(pet: pet, treasure: non_looted_treasure, rainbow_color: "Ultra violet indigo")
+
+ assert_equal [person], Owner.where(name: "Rainbow Unicat").includes(pets: :persons).first.persons.to_a
+ end
+
def test_explicitly_joining_join_table
assert_equal owners(:blackbeard).toys, owners(:blackbeard).toys.with_pet
end
def test_has_many_through_with_polymorphic_source
- post = tags(:general).tagged_posts.create! :title => "foo", :body => "bar"
+ post = tags(:general).tagged_posts.create! title: "foo", body: "bar"
assert_equal [tags(:general)], post.reload.tags
end
def test_has_many_through_obeys_order_on_through_association
owner = owners(:blackbeard)
- assert owner.toys.to_sql.include?("pets.name desc")
+ assert_includes owner.toys.to_sql, "pets.name desc"
assert_equal ["parrot", "bulbul"], owner.toys.map { |r| r.pet.name }
end
+ def test_has_many_through_associations_sum_on_columns
+ post1 = Post.create(title: "active", body: "sample")
+ post2 = Post.create(title: "inactive", body: "sample")
+
+ person1 = Person.create(first_name: "aaron", followers_count: 1)
+ person2 = Person.create(first_name: "schmit", followers_count: 2)
+ person3 = Person.create(first_name: "bill", followers_count: 3)
+ person4 = Person.create(first_name: "cal", followers_count: 4)
+
+ Reader.create(post_id: post1.id, person_id: person1.id)
+ Reader.create(post_id: post1.id, person_id: person2.id)
+ Reader.create(post_id: post1.id, person_id: person3.id)
+ Reader.create(post_id: post1.id, person_id: person4.id)
+
+ Reader.create(post_id: post2.id, person_id: person1.id)
+ Reader.create(post_id: post2.id, person_id: person2.id)
+ Reader.create(post_id: post2.id, person_id: person3.id)
+ Reader.create(post_id: post2.id, person_id: person4.id)
+
+ active_persons = Person.joins(:readers).joins(:posts).distinct(true).where("posts.title" => "active")
+
+ assert_equal active_persons.map(&:followers_count).reduce(:+), 10
+ assert_equal active_persons.sum(:followers_count), 10
+ assert_equal active_persons.sum(:followers_count), active_persons.map(&:followers_count).reduce(:+)
+ end
+
def test_has_many_through_associations_on_new_records_use_null_relations
person = Person.new
- assert_no_queries(ignore_none: false) do
+ assert_no_queries do
assert_equal [], person.posts
- assert_equal [], person.posts.where(body: 'omg')
+ assert_equal [], person.posts.where(body: "omg")
assert_equal [], person.posts.pluck(:body)
assert_equal 0, person.posts.sum(:tags_count)
assert_equal 0, person.posts.count
@@ -1109,10 +1215,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
def test_has_many_through_with_default_scope_on_the_target
person = people(:michael)
- assert_equal [posts(:thinking)], person.first_posts
+ assert_equal [posts(:thinking).id], person.first_posts.map(&:id)
readers(:michael_authorless).update(first_post_id: 1)
- assert_equal [posts(:thinking)], person.reload.first_posts
+ assert_equal [posts(:thinking).id], person.reload.first_posts.map(&:id)
end
def test_has_many_through_with_includes_in_through_association_scope
@@ -1132,9 +1238,9 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
def test_has_many_through_unscope_default_scope
- post = Post.create!(:title => 'Beaches', :body => "I like beaches!")
- Reader.create! :person => people(:david), :post => post
- LazyReader.create! :person => people(:susan), :post => post
+ post = Post.create!(title: "Beaches", body: "I like beaches!")
+ Reader.create! person: people(:david), post: post
+ LazyReader.create! person: people(:susan), post: post
assert_equal 2, post.people.to_a.size
assert_equal 1, post.lazy_people.to_a.size
@@ -1144,8 +1250,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
def test_has_many_through_add_with_sti_middle_relation
- club = SuperClub.create!(name: 'Fight Club')
- member = Member.create!(name: 'Tyler Durden')
+ club = SuperClub.create!(name: "Fight Club")
+ member = Member.create!(name: "Tyler Durden")
club.members << member
assert_equal 1, SuperMembership.where(member_id: member.id, club_id: club.id).count
@@ -1165,4 +1271,169 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_nil Club.new.special_favourites.distinct_value
end
+
+ def test_has_many_through_do_not_cache_association_reader_if_the_though_method_has_default_scopes
+ member = Member.create!
+ club = Club.create!
+ TenantMembership.create!(
+ member: member,
+ club: club
+ )
+
+ TenantMembership.current_member = member
+
+ tenant_clubs = member.tenant_clubs
+ assert_equal [club], tenant_clubs
+
+ TenantMembership.current_member = nil
+
+ other_member = Member.create!
+ other_club = Club.create!
+ TenantMembership.create!(
+ member: other_member,
+ club: other_club
+ )
+
+ tenant_clubs = other_member.tenant_clubs
+ assert_equal [other_club], tenant_clubs
+ ensure
+ TenantMembership.current_member = nil
+ end
+
+ def test_has_many_through_with_scope_that_has_joined_same_table_with_parent_relation
+ assert_equal authors(:david), Author.joins(:comments_for_first_author).take
+ end
+
+ def test_has_many_through_with_left_joined_same_table_with_through_table
+ assert_equal [comments(:eager_other_comment1)], authors(:mary).comments.left_joins(:post)
+ end
+
+ def test_has_many_through_with_unscope_should_affect_to_through_scope
+ assert_equal [comments(:eager_other_comment1)], authors(:mary).unordered_comments
+ end
+
+ def test_has_many_through_with_scope_should_accept_string_and_hash_join
+ assert_equal authors(:david), Author.joins({ comments_for_first_author: :post }, "inner join posts posts_alias on authors.id = posts_alias.author_id").eager_load(:categories).take
+ end
+
+ def test_has_many_through_with_scope_should_respect_table_alias
+ family = Family.create!
+ users = 3.times.map { User.create! }
+ 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_through_scope_is_affected_by_unscoping
+ author = authors(:david)
+
+ expected = author.comments.to_a
+ FirstPost.unscoped do
+ assert_equal expected.sort_by(&:id), author.comments_on_first_posts.sort_by(&:id)
+ end
+ end
+
+ def test_through_scope_isnt_affected_by_scoping
+ author = authors(:david)
+
+ expected = author.comments_on_first_posts.to_a
+ FirstPost.where(id: 2).scoping do
+ author.comments_on_first_posts.reset
+ assert_equal expected.sort_by(&:id), author.comments_on_first_posts.sort_by(&:id)
+ end
+ end
+
+ def test_incorrectly_ordered_through_associations
+ assert_raises(ActiveRecord::HasManyThroughOrderError) do
+ DeveloperWithIncorrectlyOrderedHasManyThrough.create(
+ companies: [Company.create]
+ )
+ end
+ end
+
+ def test_has_many_through_update_ids_with_conditions
+ author = Author.create!(name: "Bill")
+ category = categories(:general)
+
+ author.update(
+ special_categories_with_condition_ids: [category.id],
+ nonspecial_categories_with_condition_ids: [category.id]
+ )
+
+ assert_equal [category.id], author.special_categories_with_condition_ids
+ assert_equal [category.id], author.nonspecial_categories_with_condition_ids
+
+ author.update(nonspecial_categories_with_condition_ids: [])
+ author.reload
+
+ assert_equal [category.id], author.special_categories_with_condition_ids
+ assert_equal [], author.nonspecial_categories_with_condition_ids
+ end
+
+ def test_single_has_many_through_association_with_unpersisted_parent_instance
+ post_with_single_has_many_through = Class.new(Post) do
+ def self.name; "PostWithSingleHasManyThrough"; end
+ has_many :subscriptions, through: :author
+ end
+ post = post_with_single_has_many_through.new
+
+ post.author = authors(:mary)
+ book1 = Book.create!(name: "essays on single has many through associations 1")
+ post.author.books << book1
+ subscription1 = Subscription.first
+ book1.subscriptions << subscription1
+ assert_equal [subscription1], post.subscriptions.to_a
+
+ post.author = authors(:bob)
+ book2 = Book.create!(name: "essays on single has many through associations 2")
+ post.author.books << book2
+ subscription2 = Subscription.second
+ book2.subscriptions << subscription2
+ assert_equal [subscription2], post.subscriptions.to_a
+ end
+
+ def test_nested_has_many_through_association_with_unpersisted_parent_instance
+ post_with_nested_has_many_through = Class.new(Post) do
+ def self.name; "PostWithNestedHasManyThrough"; end
+ has_many :books, through: :author
+ has_many :subscriptions, through: :books
+ end
+ post = post_with_nested_has_many_through.new
+
+ post.author = authors(:mary)
+ book1 = Book.create!(name: "essays on nested has many through associations 1")
+ post.author.books << book1
+ subscription1 = Subscription.first
+ book1.subscriptions << subscription1
+ assert_equal [subscription1], post.subscriptions.to_a
+
+ post.author = authors(:bob)
+ book2 = Book.create!(name: "essays on nested has many through associations 2")
+ post.author.books << book2
+ subscription2 = Subscription.second
+ book2.subscriptions << subscription2
+ assert_equal [subscription2], post.subscriptions.to_a
+ end
+
+ private
+ def make_model(name)
+ Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } }
+ end
+
+ def make_no_pk_hm_t
+ lesson = make_model "Lesson"
+ student = make_model "Student"
+
+ lesson_student = make_model "LessonStudent"
+ lesson_student.table_name = "lessons_students"
+
+ lesson_student.belongs_to :lesson, anonymous_class: lesson
+ lesson_student.belongs_to :student, anonymous_class: student
+ lesson.has_many :lesson_students, anonymous_class: lesson_student
+ lesson.has_many :students, through: :lesson_students, anonymous_class: student
+ [lesson, lesson_student, student]
+ 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 5c2e5e7b43..adfb3ce072 100644
--- a/activerecord/test/cases/associations/has_one_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_associations_test.rb
@@ -1,19 +1,21 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/developer'
-require 'models/computer'
-require 'models/project'
-require 'models/company'
-require 'models/ship'
-require 'models/pirate'
-require 'models/car'
-require 'models/bulb'
-require 'models/author'
-require 'models/image'
-require 'models/post'
+require "models/developer"
+require "models/computer"
+require "models/project"
+require "models/company"
+require "models/ship"
+require "models/pirate"
+require "models/car"
+require "models/bulb"
+require "models/author"
+require "models/image"
+require "models/post"
class HasOneAssociationsTest < ActiveRecord::TestCase
self.use_transactional_tests = false unless supports_savepoints?
- fixtures :accounts, :companies, :developers, :projects, :developers_projects, :ships, :pirates
+ fixtures :accounts, :companies, :developers, :projects, :developers_projects, :ships, :pirates, :authors, :author_addresses
def setup
Account.destroyed_account_ids.clear
@@ -28,21 +30,22 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
ActiveRecord::SQLCounter.clear_log
companies(:first_firm).account
ensure
- assert ActiveRecord::SQLCounter.log_all.all? { |sql| /order by/i !~ sql }, 'ORDER BY was used in the query'
+ log_all = ActiveRecord::SQLCounter.log_all
+ assert log_all.all? { |sql| /order by/i !~ sql }, "ORDER BY was used in the query: #{log_all}"
end
def test_has_one_cache_nils
firm = companies(:another_firm)
assert_queries(1) { assert_nil firm.account }
- assert_queries(0) { assert_nil firm.account }
+ assert_no_queries { assert_nil firm.account }
- firms = Firm.all.merge!(:includes => :account).to_a
- assert_queries(0) { firms.each(&:account) }
+ firms = Firm.includes(:account).to_a
+ assert_no_queries { firms.each(&:account) }
end
def test_with_select
assert_equal Firm.find(1).account_with_select.attributes.size, 2
- assert_equal Firm.all.merge!(:includes => :account_with_select).find(1).account_with_select.attributes.size, 2
+ assert_equal Firm.all.merge!(includes: :account_with_select).find(1).account_with_select.attributes.size, 2
end
def test_finding_using_primary_key
@@ -102,11 +105,19 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
def test_nullification_on_association_change
firm = companies(:rails_core)
old_account_id = firm.account.id
- firm.account = Account.new(:credit_limit => 5)
+ firm.account = Account.new(credit_limit: 5)
# account is dependent with nullify, therefore its firm_id should be nil
assert_nil Account.find(old_account_id).firm_id
end
+ def test_nullification_on_destroyed_association
+ developer = Developer.create!(name: "Someone")
+ ship = Ship.create!(name: "Planet Caravan", developer: developer)
+ ship.destroy
+ assert_not_predicate ship, :persisted?
+ assert_not_predicate developer, :persisted?
+ end
+
def test_natural_assignment_to_nil_after_destroy
firm = companies(:rails_core)
old_account_id = firm.account.id
@@ -117,12 +128,12 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
end
def test_association_change_calls_delete
- companies(:first_firm).deletable_account = Account.new(:credit_limit => 5)
+ companies(:first_firm).deletable_account = Account.new(credit_limit: 5)
assert_equal [], Account.destroyed_account_ids[companies(:first_firm).id]
end
def test_association_change_calls_destroy
- companies(:first_firm).account = Account.new(:credit_limit => 5)
+ companies(:first_firm).account = Account.new(credit_limit: 5)
assert_equal [companies(:first_firm).id], Account.destroyed_account_ids[companies(:first_firm).id]
end
@@ -162,34 +173,52 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
end
def test_dependence_with_nil_associate
- firm = DependentFirm.new(:name => 'nullify')
+ firm = DependentFirm.new(name: "nullify")
firm.save!
assert_nothing_raised { firm.destroy }
end
def test_restrict_with_exception
- firm = RestrictedWithExceptionFirm.create!(:name => 'restrict')
- firm.create_account(:credit_limit => 10)
+ firm = RestrictedWithExceptionFirm.create!(name: "restrict")
+ firm.create_account(credit_limit: 10)
assert_not_nil firm.account
assert_raise(ActiveRecord::DeleteRestrictionError) { firm.destroy }
- assert RestrictedWithExceptionFirm.exists?(:name => 'restrict')
- assert firm.account.present?
+ assert RestrictedWithExceptionFirm.exists?(name: "restrict")
+ assert_predicate firm.account, :present?
end
def test_restrict_with_error
- firm = RestrictedWithErrorFirm.create!(:name => 'restrict')
- firm.create_account(:credit_limit => 10)
+ firm = RestrictedWithErrorFirm.create!(name: "restrict")
+ firm.create_account(credit_limit: 10)
assert_not_nil firm.account
firm.destroy
- assert !firm.errors.empty?
+ assert_not_empty firm.errors
assert_equal "Cannot delete record because a dependent account exists", firm.errors[:base].first
- assert RestrictedWithErrorFirm.exists?(:name => 'restrict')
- assert firm.account.present?
+ assert RestrictedWithErrorFirm.exists?(name: "restrict")
+ assert_predicate firm.account, :present?
+ end
+
+ def test_restrict_with_error_with_locale
+ I18n.backend = I18n::Backend::Simple.new
+ I18n.backend.store_translations "en", activerecord: { attributes: { restricted_with_error_firm: { account: "firm account" } } }
+ firm = RestrictedWithErrorFirm.create!(name: "restrict")
+ firm.create_account(credit_limit: 10)
+
+ assert_not_nil firm.account
+
+ firm.destroy
+
+ assert_not_empty firm.errors
+ assert_equal "Cannot delete record because a dependent firm account exists", firm.errors[:base].first
+ assert RestrictedWithErrorFirm.exists?(name: "restrict")
+ assert_predicate firm.account, :present?
+ ensure
+ I18n.backend.reload!
end
def test_successful_build_association
@@ -202,9 +231,13 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
end
def test_build_association_dont_create_transaction
- assert_no_queries(ignore_none: false) {
- Firm.new.build_account
- }
+ # Load schema information so we don't query below if running just this test.
+ Account.define_attribute_methods
+
+ firm = Firm.new
+ assert_no_queries do
+ firm.build_account
+ end
end
def test_building_the_associated_object_with_implicit_sti_base_class
@@ -215,24 +248,24 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
def test_building_the_associated_object_with_explicit_sti_base_class
firm = DependentFirm.new
- company = firm.build_company(:type => "Company")
+ company = firm.build_company(type: "Company")
assert_kind_of Company, company, "Expected #{company.class} to be a Company"
end
def test_building_the_associated_object_with_sti_subclass
firm = DependentFirm.new
- company = firm.build_company(:type => "Client")
+ company = firm.build_company(type: "Client")
assert_kind_of Client, company, "Expected #{company.class} to be a Client"
end
def test_building_the_associated_object_with_an_invalid_type
firm = DependentFirm.new
- assert_raise(ActiveRecord::SubclassNotFound) { firm.build_company(:type => "Invalid") }
+ assert_raise(ActiveRecord::SubclassNotFound) { firm.build_company(type: "Invalid") }
end
def test_building_the_associated_object_with_an_unrelated_type
firm = DependentFirm.new
- assert_raise(ActiveRecord::SubclassNotFound) { firm.build_company(:type => "Account") }
+ assert_raise(ActiveRecord::SubclassNotFound) { firm.build_company(type: "Account") }
end
def test_build_and_create_should_not_happen_within_scope
@@ -250,19 +283,19 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
end
def test_create_association
- firm = Firm.create(:name => "GlobalMegaCorp")
- account = firm.create_account(:credit_limit => 1000)
+ firm = Firm.create(name: "GlobalMegaCorp")
+ account = firm.create_account(credit_limit: 1000)
assert_equal account, firm.reload.account
end
def test_create_association_with_bang
- firm = Firm.create(:name => "GlobalMegaCorp")
- account = firm.create_account!(:credit_limit => 1000)
+ firm = Firm.create(name: "GlobalMegaCorp")
+ account = firm.create_account!(credit_limit: 1000)
assert_equal account, firm.reload.account
end
def test_create_association_with_bang_failing
- firm = Firm.create(:name => "GlobalMegaCorp")
+ firm = Firm.create(name: "GlobalMegaCorp")
assert_raise ActiveRecord::RecordInvalid do
firm.create_account!
end
@@ -274,13 +307,55 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
end
def test_create_with_inexistent_foreign_key_failing
- firm = Firm.create(name: 'GlobalMegaCorp')
+ firm = Firm.create(name: "GlobalMegaCorp")
assert_raises(ActiveRecord::UnknownAttributeError) do
firm.create_account_with_inexistent_foreign_key
end
end
+ def test_create_when_parent_is_new_raises
+ firm = Firm.new
+ error = assert_raise(ActiveRecord::RecordNotSaved) do
+ firm.create_account
+ end
+
+ assert_equal "You cannot call create unless the parent is saved", error.message
+ 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_reload_association_with_query_cache
+ odegy_id = companies(:odegy).id
+
+ connection = ActiveRecord::Base.connection
+ connection.enable_query_cache!
+ connection.clear_query_cache
+
+ # Populate the cache with a query
+ odegy = Company.find(odegy_id)
+ # Populate the cache with a second query
+ odegy.account
+
+ assert_equal 2, connection.query_cache.size
+
+ # Clear the cache and fetch the account again, populating the cache with a query
+ assert_queries(1) { odegy.reload_account }
+
+ # This query is not cached anymore, so it should make a real SQL query
+ assert_queries(1) { Company.find(odegy_id) }
+ ensure
+ ActiveRecord::Base.connection.disable_query_cache!
+ end
+
def test_build
firm = Firm.new("name" => "GlobalMegaCorp")
firm.save
@@ -320,7 +395,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
def test_finding_with_interpolated_condition
firm = Firm.first
- superior = firm.clients.create(:name => 'SuperiorCo')
+ superior = firm.clients.create(name: "SuperiorCo")
superior.rating = 10
superior.save
assert_equal 10, firm.clients_with_interpolated_conditions.first.rating
@@ -329,14 +404,15 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
def test_assignment_before_child_saved
firm = Firm.find(1)
firm.account = a = Account.new("credit_limit" => 1000)
- assert a.persisted?
+ assert_predicate a, :persisted?
+ assert_equal a, firm.account
assert_equal a, firm.account
+ firm.association(:account).reload
assert_equal a, firm.account
- assert_equal a, firm.account(true)
end
def test_save_still_works_after_accessing_nil_has_one
- jp = Company.new :name => 'Jaded Pixel'
+ jp = Company.new name: "Jaded Pixel"
jp.dummy_account.nil?
assert_nothing_raised do
@@ -346,7 +422,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
def test_cant_save_readonly_association
assert_raise(ActiveRecord::ReadOnlyRecord) { companies(:first_firm).readonly_account.save! }
- assert companies(:first_firm).readonly_account.readonly?
+ assert_predicate companies(:first_firm).readonly_account, :readonly?
end
def test_has_one_proxy_should_not_respond_to_private_methods
@@ -365,14 +441,14 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_nothing_raised do
Firm.find(@firm.id).save!
- Firm.all.merge!(:includes => :account).find(@firm.id).save!
+ Firm.all.merge!(includes: :account).find(@firm.id).save!
end
@firm.account.destroy
assert_nothing_raised do
Firm.find(@firm.id).save!
- Firm.all.merge!(:includes => :account).find(@firm.id).save!
+ Firm.all.merge!(includes: :account).find(@firm.id).save!
end
end
@@ -384,12 +460,12 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
def test_create_respects_hash_condition
account = companies(:first_firm).create_account_limit_500_with_hash_conditions
- assert account.persisted?
+ assert_predicate account, :persisted?
assert_equal 500, account.credit_limit
end
def test_attributes_are_being_set_when_initialized_from_has_one_association_with_where_clause
- new_account = companies(:first_firm).build_account(:firm_name => 'Account')
+ new_account = companies(:first_firm).build_account(firm_name: "Account")
assert_equal new_account.firm_name, "Account"
end
@@ -401,9 +477,9 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
new_ship = pirate.create_ship
assert_not_equal ships(:black_pearl), new_ship
assert_equal new_ship, pirate.ship
- assert new_ship.new_record?
+ assert_predicate new_ship, :new_record?
assert_nil orig_ship.pirate_id
- assert !orig_ship.changed? # check it was saved
+ assert_not orig_ship.changed? # check it was saved
end
def test_creation_failure_with_dependent_option
@@ -411,8 +487,8 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
orig_ship = pirate.dependent_ship
new_ship = pirate.create_dependent_ship
- assert new_ship.new_record?
- assert orig_ship.destroyed?
+ assert_predicate new_ship, :new_record?
+ assert_predicate orig_ship, :destroyed?
end
def test_creation_failure_due_to_new_record_should_raise_error
@@ -432,14 +508,14 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
pirate = pirates(:blackbeard)
pirate.ship.name = nil
- assert !pirate.ship.valid?
+ assert_not_predicate pirate.ship, :valid?
error = assert_raise(ActiveRecord::RecordNotSaved) do
pirate.ship = ships(:interceptor)
end
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
@@ -459,63 +535,63 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
end
def test_association_keys_bypass_attribute_protection
- car = Car.create(:name => 'honda')
+ car = Car.create(name: "honda")
bulb = car.build_bulb
assert_equal car.id, bulb.car_id
- bulb = car.build_bulb :car_id => car.id + 1
+ bulb = car.build_bulb car_id: car.id + 1
assert_equal car.id, bulb.car_id
bulb = car.create_bulb
assert_equal car.id, bulb.car_id
- bulb = car.create_bulb :car_id => car.id + 1
+ bulb = car.create_bulb car_id: car.id + 1
assert_equal car.id, bulb.car_id
end
def test_association_protect_foreign_key
- pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
+ pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?")
ship = pirate.build_ship
assert_equal pirate.id, ship.pirate_id
- ship = pirate.build_ship :pirate_id => pirate.id + 1
+ ship = pirate.build_ship pirate_id: pirate.id + 1
assert_equal pirate.id, ship.pirate_id
ship = pirate.create_ship
assert_equal pirate.id, ship.pirate_id
- ship = pirate.create_ship :pirate_id => pirate.id + 1
+ ship = pirate.create_ship pirate_id: pirate.id + 1
assert_equal pirate.id, ship.pirate_id
end
def test_build_with_block
- car = Car.create(:name => 'honda')
+ car = Car.create(name: "honda")
- bulb = car.build_bulb{ |b| b.color = 'Red' }
- assert_equal 'RED!', bulb.color
+ bulb = car.build_bulb { |b| b.color = "Red" }
+ assert_equal "RED!", bulb.color
end
def test_create_with_block
- car = Car.create(:name => 'honda')
+ car = Car.create(name: "honda")
- bulb = car.create_bulb{ |b| b.color = 'Red' }
- assert_equal 'RED!', bulb.color
+ bulb = car.create_bulb { |b| b.color = "Red" }
+ assert_equal "RED!", bulb.color
end
def test_create_bang_with_block
- car = Car.create(:name => 'honda')
+ car = Car.create(name: "honda")
- bulb = car.create_bulb!{ |b| b.color = 'Red' }
- assert_equal 'RED!', bulb.color
+ bulb = car.create_bulb! { |b| b.color = "Red" }
+ assert_equal "RED!", bulb.color
end
def test_association_attributes_are_available_to_after_initialize
- car = Car.create(:name => 'honda')
+ car = Car.create(name: "honda")
bulb = car.create_bulb
- assert_equal car.id, bulb.attributes_after_initialize['car_id']
+ assert_equal car.id, bulb.attributes_after_initialize["car_id"]
end
def test_has_one_transaction
@@ -535,36 +611,36 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
def test_has_one_assignment_dont_trigger_save_on_change_of_same_object
pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?")
- ship = pirate.build_ship(name: 'old name')
+ ship = pirate.build_ship(name: "old name")
ship.save!
- ship.name = 'new name'
- assert ship.changed?
+ ship.name = "new name"
+ assert_predicate ship, :changed?
assert_queries(1) do
# One query for updating name, not triggering query for updating pirate_id
pirate.ship = ship
end
- assert_equal 'new name', pirate.ship.reload.name
+ assert_equal "new name", pirate.ship.reload.name
end
def test_has_one_assignment_triggers_save_on_change_on_replacing_object
pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?")
- ship = pirate.build_ship(name: 'old name')
+ ship = pirate.build_ship(name: "old name")
ship.save!
- new_ship = Ship.create(name: 'new name')
+ new_ship = Ship.create(name: "new name")
assert_queries(2) do
- # One query for updating name and second query for updating pirate_id
+ # One query to nullify the old ship, one query to update the new ship
pirate.ship = new_ship
end
- assert_equal 'new name', pirate.ship.reload.name
+ assert_equal "new name", pirate.ship.reload.name
end
def test_has_one_autosave_with_primary_key_manually_set
- post = Post.create(id: 1234, title: "Some title", body: 'Some content')
- author = Author.new(id: 33, name: 'Hank Moody')
+ post = Post.create(id: 1234, title: "Some title", body: "Some content")
+ author = Author.new(id: 33, name: "Hank Moody")
author.post = post
author.save
@@ -575,7 +651,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
end
def test_has_one_loading_for_new_record
- post = Post.create!(author_id: 42, title: 'foo', body: 'bar')
+ post = Post.create!(author_id: 42, title: "foo", body: "bar")
author = Author.new(id: 42)
assert_equal post, author.post
end
@@ -589,7 +665,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
end
def test_with_polymorphic_has_one_with_custom_columns_name
- post = Post.create! :title => 'foo', :body => 'bar'
+ post = Post.create! title: "foo", body: "bar"
image = Image.create!
post.main_image = image
@@ -598,8 +674,8 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_equal image, post.main_image
end
- test 'dangerous association name raises ArgumentError' do
- [:errors, 'errors', :save, 'save'].each do |name|
+ test "dangerous association name raises ArgumentError" do
+ [:errors, "errors", :save, "save"].each do |name|
assert_raises(ArgumentError, "Association #{name} should not be allowed") do
Class.new(ActiveRecord::Base) do
has_one name
@@ -607,4 +683,100 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
end
end
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"
+
+ enum status: [:proposed, :written, :published]
+ end
+
+ class SpecialAuthor < ActiveRecord::Base
+ self.table_name = "authors"
+ has_one :book, class_name: "SpecialBook", foreign_key: "author_id"
+ end
+
+ 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
+
+ assert_equal "published", book.status
+ assert_not_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
+
+ class DestroyByParentBook < ActiveRecord::Base
+ self.table_name = "books"
+ belongs_to :author, class_name: "DestroyByParentAuthor"
+ before_destroy :dont, unless: :destroyed_by_association
+
+ def dont
+ throw(:abort)
+ end
+ end
+
+ class DestroyByParentAuthor < ActiveRecord::Base
+ self.table_name = "authors"
+ has_one :book, class_name: "DestroyByParentBook", foreign_key: "author_id", dependent: :destroy
+ end
+
+ test "destroyed_by_association set in child destroy callback on parent destroy" do
+ author = DestroyByParentAuthor.create!(name: "Test")
+ book = DestroyByParentBook.create!(author: author)
+
+ author.destroy
+
+ assert_not DestroyByParentBook.exists?(book.id)
+ end
+
+ test "destroyed_by_association set in child destroy callback on replace" do
+ author = DestroyByParentAuthor.create!(name: "Test")
+ book = DestroyByParentBook.create!(author: author)
+
+ author.book = DestroyByParentBook.create!
+ author.save!
+
+ assert_not DestroyByParentBook.exists?(book.id)
+ end
+
+ class UndestroyableBook < ActiveRecord::Base
+ self.table_name = "books"
+ belongs_to :author, class_name: "DestroyableAuthor"
+ before_destroy :dont
+
+ def dont
+ throw(:abort)
+ end
+ end
+
+ class DestroyableAuthor < ActiveRecord::Base
+ self.table_name = "authors"
+ has_one :book, class_name: "UndestroyableBook", foreign_key: "author_id", dependent: :destroy
+ end
+
+ def test_dependency_should_halt_parent_destruction
+ author = DestroyableAuthor.create!(name: "Test")
+ UndestroyableBook.create!(author: author)
+
+ assert_no_difference ["DestroyableAuthor.count", "UndestroyableBook.count"] do
+ assert_not author.destroy
+ end
+ end
end
diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb
index f8772547a2..0309663943 100644
--- a/activerecord/test/cases/associations/has_one_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb
@@ -1,25 +1,31 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/club'
-require 'models/member_type'
-require 'models/member'
-require 'models/membership'
-require 'models/sponsor'
-require 'models/organization'
-require 'models/member_detail'
-require 'models/minivan'
-require 'models/dashboard'
-require 'models/speedometer'
-require 'models/category'
-require 'models/author'
-require 'models/essay'
-require 'models/owner'
-require 'models/post'
-require 'models/comment'
-require 'models/categorization'
+require "models/club"
+require "models/member_type"
+require "models/member"
+require "models/membership"
+require "models/sponsor"
+require "models/organization"
+require "models/member_detail"
+require "models/minivan"
+require "models/dashboard"
+require "models/speedometer"
+require "models/category"
+require "models/author"
+require "models/essay"
+require "models/owner"
+require "models/post"
+require "models/comment"
+require "models/categorization"
+require "models/customer"
+require "models/carrier"
+require "models/shop_account"
+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)
@@ -30,14 +36,26 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
end
def test_creating_association_creates_through_record
- new_member = Member.create(:name => "Chris")
- new_member.club = Club.create(:name => "LRUG")
+ new_member = Member.create(name: "Chris")
+ new_member.club = Club.create(name: "LRUG")
assert_not_nil new_member.current_membership
assert_not_nil new_member.club
end
+ def test_creating_association_builds_through_record
+ new_member = Member.create(name: "Chris")
+ new_club = new_member.association(:club).build
+ assert new_member.current_membership
+ assert_equal new_club, new_member.club
+ assert_predicate new_club, :new_record?
+ assert_predicate new_member.current_membership, :new_record?
+ assert new_member.save
+ assert_predicate new_club, :persisted?
+ assert_predicate new_member.current_membership, :persisted?
+ end
+
def test_creating_association_builds_through_record_for_new
- new_member = Member.new(:name => "Jane")
+ new_member = Member.new(name: "Jane")
new_member.club = clubs(:moustache_club)
assert new_member.current_membership
assert_equal clubs(:moustache_club), new_member.current_membership.club
@@ -46,9 +64,27 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
assert_equal clubs(:moustache_club), new_member.club
end
+ def test_building_multiple_associations_builds_through_record
+ member_type = MemberType.create!
+ member = Member.create!
+ member_detail_with_one_association = MemberDetail.new(member_type: member_type)
+ assert_predicate member_detail_with_one_association.member, :new_record?
+ member_detail_with_two_associations = MemberDetail.new(member_type: member_type, admittable: member)
+ assert_predicate member_detail_with_two_associations.member, :new_record?
+ end
+
+ def test_creating_multiple_associations_creates_through_record
+ member_type = MemberType.create!
+ member = Member.create!
+ member_detail_with_one_association = MemberDetail.create!(member_type: member_type)
+ assert_not_predicate member_detail_with_one_association.member, :new_record?
+ member_detail_with_two_associations = MemberDetail.create!(member_type: member_type, admittable: member)
+ assert_not_predicate member_detail_with_two_associations.member, :new_record?
+ end
+
def test_creating_association_sets_both_parent_ids_for_new
- member = Member.new(name: 'Sean Griffin')
- club = Club.new(name: 'Da Club')
+ member = Member.new(name: "Sean Griffin")
+ club = Club.new(name: "Da Club")
member.club = club
@@ -61,7 +97,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
end
def test_replace_target_record
- new_club = Club.create(:name => "Marx Bros")
+ new_club = Club.create(name: "Marx Bros")
@member.club = new_club
@member.reload
assert_equal new_club, @member.club
@@ -69,7 +105,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
def test_replacing_target_record_deletes_old_association
assert_no_difference "Membership.count" do
- new_club = Club.create(:name => "Bananarama")
+ new_club = Club.create(name: "Bananarama")
@member.club = new_club
@member.reload
end
@@ -78,40 +114,47 @@ 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
def test_has_one_through_eager_loading
- members = assert_queries(3) do #base table, through table, clubs table
- Member.all.merge!(:includes => :club, :where => ["name = ?", "Groucho Marx"]).to_a
+ members = assert_queries(3) do # base table, through table, clubs table
+ Member.all.merge!(includes: :club, where: ["name = ?", "Groucho Marx"]).to_a
end
assert_equal 1, members.size
- assert_not_nil assert_no_queries {members[0].club}
+ assert_not_nil assert_no_queries { members[0].club }
end
def test_has_one_through_eager_loading_through_polymorphic
- members = assert_queries(3) do #base table, through table, clubs table
- Member.all.merge!(:includes => :sponsor_club, :where => ["name = ?", "Groucho Marx"]).to_a
+ members = assert_queries(3) do # base table, through table, clubs table
+ Member.all.merge!(includes: :sponsor_club, where: ["name = ?", "Groucho Marx"]).to_a
end
assert_equal 1, members.size
- assert_not_nil assert_no_queries {members[0].sponsor_club}
+ assert_not_nil assert_no_queries { members[0].sponsor_club }
end
def test_has_one_through_with_conditions_eager_loading
# conditions on the through table
- assert_equal clubs(:moustache_club), Member.all.merge!(:includes => :favourite_club).find(@member.id).favourite_club
+ 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
+ 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
@@ -119,31 +162,31 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
end
def test_eager_has_one_through_polymorphic_with_source_type
- clubs = Club.all.merge!(:includes => :sponsored_member, :where => ["name = ?","Moustache and Eyebrow Fancier Club"]).to_a
+ clubs = Club.all.merge!(includes: :sponsored_member, where: ["name = ?", "Moustache and Eyebrow Fancier Club"]).to_a
# Only the eyebrow fanciers club has a sponsored_member
- assert_not_nil assert_no_queries {clubs[0].sponsored_member}
+ assert_not_nil assert_no_queries { clubs[0].sponsored_member }
end
def test_has_one_through_nonpreload_eagerloading
members = assert_queries(1) do
- Member.all.merge!(:includes => :club, :where => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name').to_a #force fallback
+ Member.all.merge!(includes: :club, where: ["members.name = ?", "Groucho Marx"], order: "clubs.name").to_a # force fallback
end
assert_equal 1, members.size
- assert_not_nil assert_no_queries {members[0].club}
+ assert_not_nil assert_no_queries { members[0].club }
end
def test_has_one_through_nonpreload_eager_loading_through_polymorphic
members = assert_queries(1) do
- Member.all.merge!(:includes => :sponsor_club, :where => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name').to_a #force fallback
+ Member.all.merge!(includes: :sponsor_club, where: ["members.name = ?", "Groucho Marx"], order: "clubs.name").to_a # force fallback
end
assert_equal 1, members.size
- assert_not_nil assert_no_queries {members[0].sponsor_club}
+ assert_not_nil assert_no_queries { members[0].sponsor_club }
end
def test_has_one_through_nonpreload_eager_loading_through_polymorphic_with_more_than_one_through_record
- Sponsor.new(:sponsor_club => clubs(:crazy_club), :sponsorable => members(:groucho)).save!
+ Sponsor.new(sponsor_club: clubs(:crazy_club), sponsorable: members(:groucho)).save!
members = assert_queries(1) do
- Member.all.merge!(:includes => :sponsor_club, :where => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name DESC').to_a #force fallback
+ Member.all.merge!(includes: :sponsor_club, where: ["members.name = ?", "Groucho Marx"], order: "clubs.name DESC").to_a # force fallback
end
assert_equal 1, members.size
assert_not_nil assert_no_queries { members[0].sponsor_club }
@@ -155,8 +198,8 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
end
def test_assigning_association_correctly_assigns_target
- new_member = Member.create(:name => "Chris")
- new_member.club = new_club = Club.create(:name => "LRUG")
+ new_member = Member.create(name: "Chris")
+ new_member.club = new_club = Club.create(name: "LRUG")
assert_equal new_club, new_member.association(:club).target
end
@@ -172,37 +215,37 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
def test_assigning_to_has_one_through_preserves_decorated_join_record
@organization = organizations(:nsa)
- assert_difference 'MemberDetail.count', 1 do
- @member_detail = MemberDetail.new(:extra_data => 'Extra')
+ assert_difference "MemberDetail.count", 1 do
+ @member_detail = MemberDetail.new(extra_data: "Extra")
@member.member_detail = @member_detail
@member.organization = @organization
end
assert_equal @organization, @member.organization
- assert @organization.members.include?(@member)
- assert_equal 'Extra', @member.member_detail.extra_data
+ assert_includes @organization.members, @member
+ assert_equal "Extra", @member.member_detail.extra_data
end
def test_reassigning_has_one_through
@organization = organizations(:nsa)
@new_organization = organizations(:discordians)
- assert_difference 'MemberDetail.count', 1 do
- @member_detail = MemberDetail.new(:extra_data => 'Extra')
+ assert_difference "MemberDetail.count", 1 do
+ @member_detail = MemberDetail.new(extra_data: "Extra")
@member.member_detail = @member_detail
@member.organization = @organization
end
assert_equal @organization, @member.organization
- assert_equal 'Extra', @member.member_detail.extra_data
- assert @organization.members.include?(@member)
- assert !@new_organization.members.include?(@member)
+ assert_equal "Extra", @member.member_detail.extra_data
+ assert_includes @organization.members, @member
+ assert_not_includes @new_organization.members, @member
- assert_no_difference 'MemberDetail.count' do
+ assert_no_difference "MemberDetail.count" do
@member.organization = @new_organization
end
assert_equal @new_organization, @member.organization
- assert_equal 'Extra', @member.member_detail.extra_data
- assert !@organization.members.include?(@member)
- assert @new_organization.members.include?(@member)
+ assert_equal "Extra", @member.member_detail.extra_data
+ assert_not_includes @organization.members, @member
+ assert_includes @new_organization.members, @member
end
def test_preloading_has_one_through_on_belongs_to
@@ -213,10 +256,10 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
@member.member_detail = @member_detail
@member.organization = @organization
@member_details = assert_queries(3) do
- MemberDetail.all.merge!(:includes => :member_type).to_a
+ MemberDetail.all.merge!(includes: :member_type).to_a
end
@new_detail = @member_details[0]
- assert @new_detail.send(:association, :member_type).loaded?
+ assert_predicate @new_detail.send(:association, :member_type), :loaded?
assert_no_queries { @new_detail.member_type }
end
@@ -226,36 +269,38 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
assert_nothing_raised do
Club.find(@club.id).save!
- Club.all.merge!(:includes => :sponsored_member).find(@club.id).save!
+ Club.all.merge!(includes: :sponsored_member).find(@club.id).save!
end
@club.sponsor.destroy
assert_nothing_raised do
Club.find(@club.id).save!
- Club.all.merge!(:includes => :sponsored_member).find(@club.id).save!
+ Club.all.merge!(includes: :sponsored_member).find(@club.id).save!
end
end
def test_through_belongs_to_after_destroy
- @member_detail = MemberDetail.new(:extra_data => 'Extra')
+ @member_detail = MemberDetail.new(extra_data: "Extra")
@member.member_detail = @member_detail
@member.save!
assert_not_nil @member_detail.member_type
@member_detail.destroy
assert_queries(1) do
- assert_not_nil @member_detail.member_type(true)
+ @member_detail.association(:member_type).reload
+ assert_not_nil @member_detail.member_type
end
@member_detail.member.destroy
assert_queries(1) do
- assert_nil @member_detail.member_type(true)
+ @member_detail.association(:member_type).reload
+ assert_nil @member_detail.member_type
end
end
def test_value_is_properly_quoted
- minivan = Minivan.find('m1')
+ minivan = Minivan.find("m1")
assert_nothing_raised do
minivan.dashboard
end
@@ -264,7 +309,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
def test_has_one_through_polymorphic_with_primary_key_option
assert_equal categories(:general), authors(:david).essay_category
- authors = Author.joins(:essay_category).where('categories.id' => categories(:general).id)
+ authors = Author.joins(:essay_category).where("categories.id" => categories(:general).id)
assert_equal authors(:david), authors.first
assert_equal owners(:blackbeard), authors(:david).essay_owner
@@ -276,12 +321,12 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
def test_has_one_through_with_primary_key_option
assert_equal categories(:general), authors(:david).essay_category_2
- authors = Author.joins(:essay_category_2).where('categories.id' => categories(:general).id)
+ authors = Author.joins(:essay_category_2).where("categories.id" => categories(:general).id)
assert_equal authors(:david), authors.first
end
def test_has_one_through_with_default_scope_on_join_model
- assert_equal posts(:welcome).comments.order('id').first, authors(:david).comment_on_first_post
+ assert_equal posts(:welcome).comments.order("id").first, authors(:david).comment_on_first_post
end
def test_has_one_through_many_raises_exception
@@ -302,12 +347,12 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
minivan.dashboard
proxy = minivan.send(:association_instance_get, :dashboard)
- assert !proxy.stale_target?
+ assert_not_predicate proxy, :stale_target?
assert_equal dashboards(:cool_first), minivan.dashboard
minivan.speedometer_id = speedometers(:second).id
- assert proxy.stale_target?
+ assert_predicate proxy, :stale_target?
assert_equal dashboards(:second), minivan.dashboard
end
@@ -319,7 +364,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
minivan.speedometer_id = speedometers(:second).id
- assert proxy.stale_target?
+ assert_predicate proxy, :stale_target?
assert_equal dashboards(:second), minivan.dashboard
end
@@ -344,4 +389,34 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
end
end
end
+
+ def test_has_one_through_do_not_cache_association_reader_if_the_though_method_has_default_scopes
+ customer = Customer.create!
+ carrier = Carrier.create!
+ customer_carrier = CustomerCarrier.create!(
+ customer: customer,
+ carrier: carrier,
+ )
+ account = ShopAccount.create!(customer_carrier: customer_carrier)
+
+ CustomerCarrier.current_customer = customer
+
+ account_carrier = account.carrier
+ assert_equal carrier, account_carrier
+
+ CustomerCarrier.current_customer = nil
+
+ other_carrier = Carrier.create!
+ other_customer = Customer.create!
+ other_customer_carrier = CustomerCarrier.create!(
+ customer: other_customer,
+ carrier: other_carrier,
+ )
+ other_account = ShopAccount.create!(customer_carrier: other_customer_carrier)
+
+ account_carrier = other_account.carrier
+ assert_equal other_carrier, account_carrier
+ ensure
+ CustomerCarrier.current_customer = nil
+ end
end
diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb
index b3fe759ad9..c33dcdee61 100644
--- a/activerecord/test/cases/associations/inner_join_association_test.rb
+++ b/activerecord/test/cases/associations/inner_join_association_test.rb
@@ -1,16 +1,18 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/post'
-require 'models/comment'
-require 'models/author'
-require 'models/essay'
-require 'models/category'
-require 'models/categorization'
-require 'models/person'
-require 'models/tagging'
-require 'models/tag'
+require "models/post"
+require "models/comment"
+require "models/author"
+require "models/essay"
+require "models/category"
+require "models/categorization"
+require "models/person"
+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
@@ -20,11 +22,29 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase
def test_construct_finder_sql_does_not_table_name_collide_on_duplicate_associations
assert_nothing_raised do
- sql = Person.joins(:agents => {:agents => :agents}).joins(:agents => {:agents => {:primary_contact => :agents}}).to_sql
+ sql = Person.joins(agents: { agents: :agents }).joins(agents: { agents: { primary_contact: :agents } }).to_sql
assert_match(/agents_people_4/i, sql)
end
end
+ def test_construct_finder_sql_does_not_table_name_collide_on_duplicate_associations_with_left_outer_joins
+ sql = Person.joins(agents: :agents).left_outer_joins(agents: :agents).to_sql
+ assert_match(/agents_people_4/i, sql)
+ end
+
+ def test_construct_finder_sql_does_not_table_name_collide_with_string_joins
+ sql = Person.joins(:agents).joins("JOIN people agents_people ON agents_people.primary_contact_id = people.id").to_sql
+ assert_match(/agents_people_2/i, sql)
+ end
+
+ def test_construct_finder_sql_does_not_table_name_collide_with_aliased_joins
+ people = Person.arel_table
+ agents = people.alias("agents_people")
+ constraint = agents[:primary_contact_id].eq(people[:id])
+ sql = Person.joins(:agents).joins(agents.create_join(agents, agents.create_on(constraint))).to_sql
+ assert_match(/agents_people_2/i, sql)
+ end
+
def test_construct_finder_sql_ignores_empty_joins_hash
sql = Author.joins({}).to_sql
assert_no_match(/JOIN/i, sql)
@@ -47,7 +67,7 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase
end
def test_join_conditions_allow_nil_associations
- authors = Author.includes(:essays).where(:essays => {:id => nil})
+ authors = Author.includes(:essays).where(essays: { id: nil })
assert_equal 2, authors.count
end
@@ -58,56 +78,56 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase
end
def test_find_with_implicit_inner_joins_honors_readonly_with_select
- authors = Author.joins(:posts).select('authors.*').to_a
- assert !authors.empty?, "expected authors to be non-empty"
- assert authors.all? {|a| !a.readonly? }, "expected no authors to be readonly"
+ authors = Author.joins(:posts).select("authors.*").to_a
+ assert_not authors.empty?, "expected authors to be non-empty"
+ assert authors.all? { |a| !a.readonly? }, "expected no authors to be readonly"
end
def test_find_with_implicit_inner_joins_honors_readonly_false
authors = Author.joins(:posts).readonly(false).to_a
- assert !authors.empty?, "expected authors to be non-empty"
- assert authors.all? {|a| !a.readonly? }, "expected no authors to be readonly"
+ assert_not authors.empty?, "expected authors to be non-empty"
+ assert authors.all? { |a| !a.readonly? }, "expected no authors to be readonly"
end
def test_find_with_implicit_inner_joins_does_not_set_associations
- authors = Author.joins(:posts).select('authors.*').to_a
- assert !authors.empty?, "expected authors to be non-empty"
+ authors = Author.joins(:posts).select("authors.*").to_a
+ assert_not authors.empty?, "expected authors to be non-empty"
assert authors.all? { |a| !a.instance_variable_defined?(:@posts) }, "expected no authors to have the @posts association loaded"
end
def test_count_honors_implicit_inner_joins
- real_count = Author.all.to_a.sum{|a| a.posts.count }
+ real_count = Author.all.to_a.sum { |a| a.posts.count }
assert_equal real_count, Author.joins(:posts).count, "plain inner join count should match the number of referenced posts records"
end
def test_calculate_honors_implicit_inner_joins
- real_count = Author.all.to_a.sum{|a| a.posts.count }
- assert_equal real_count, Author.joins(:posts).calculate(:count, 'authors.id'), "plain inner join count should match the number of referenced posts records"
+ real_count = Author.all.to_a.sum { |a| a.posts.count }
+ assert_equal real_count, Author.joins(:posts).calculate(:count, "authors.id"), "plain inner join count should match the number of referenced posts records"
end
def test_calculate_honors_implicit_inner_joins_and_distinct_and_conditions
- real_count = Author.all.to_a.select {|a| a.posts.any? {|p| p.title =~ /^Welcome/} }.length
- authors_with_welcoming_post_titles = Author.all.merge!(joins: :posts, where: "posts.title like 'Welcome%'").distinct.calculate(:count, 'authors.id')
+ real_count = Author.all.to_a.select { |a| a.posts.any? { |p| p.title.start_with?("Welcome") } }.length
+ authors_with_welcoming_post_titles = Author.all.merge!(joins: :posts, where: "posts.title like 'Welcome%'").distinct.calculate(:count, "authors.id")
assert_equal real_count, authors_with_welcoming_post_titles, "inner join and conditions should have only returned authors posting titles starting with 'Welcome'"
end
def test_find_with_sti_join
- scope = Post.joins(:special_comments).where(:id => posts(:sti_comments).id)
+ scope = Post.joins(:special_comments).where(id: posts(:sti_comments).id)
# The join should match SpecialComment and its subclasses only
- assert scope.where("comments.type" => "Comment").empty?
- assert !scope.where("comments.type" => "SpecialComment").empty?
- assert !scope.where("comments.type" => "SubSpecialComment").empty?
+ assert_empty scope.where("comments.type" => "Comment")
+ assert_not_empty scope.where("comments.type" => "SpecialComment")
+ assert_not_empty scope.where("comments.type" => "SubSpecialComment")
end
def test_find_with_conditions_on_reflection
- assert !posts(:welcome).comments.empty?
- assert Post.joins(:nonexistent_comments).where(:id => posts(:welcome).id).empty? # [sic!]
+ assert_not_empty posts(:welcome).comments
+ assert Post.joins(:nonexistent_comments).where(id: posts(:welcome).id).empty? # [sic!]
end
def test_find_with_conditions_on_through_reflection
- assert !posts(:welcome).tags.empty?
- assert Post.joins(:misc_tags).where(:id => posts(:welcome).id).empty?
+ assert_not_empty posts(:welcome).tags
+ assert_empty Post.joins(:misc_tags).where(id: posts(:welcome).id)
end
test "the default scope of the target is applied when joining associations" do
@@ -120,8 +140,8 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase
test "the default scope of the target is correctly aliased when joining associations" do
author = Author.create! name: "Jon"
- author.categories.create! name: 'Not Special'
- author.special_categories.create! name: 'Special'
+ author.categories.create! name: "Not Special"
+ author.special_categories.create! name: "Special"
categories = author.categories.includes(:special_categorizations).references(:special_categorizations).to_a
assert_equal 2, categories.size
@@ -129,8 +149,8 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase
test "the correct records are loaded when including an aliased association" do
author = Author.create! name: "Jon"
- author.categories.create! name: 'Not Special'
- author.special_categories.create! name: 'Special'
+ author.categories.create! name: "Not Special"
+ author.special_categories.create! name: "Special"
categories = author.categories.eager_load(:special_categorizations).order(:name).to_a
assert_equal 0, categories.first.special_categorizations.size
diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb
index 423b8238b1..eb4dc73423 100644
--- a/activerecord/test/cases/associations/inverse_associations_test.rb
+++ b/activerecord/test/cases/associations/inverse_associations_test.rb
@@ -1,18 +1,27 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/man'
-require 'models/face'
-require 'models/interest'
-require 'models/zine'
-require 'models/club'
-require 'models/sponsor'
-require 'models/rating'
-require 'models/comment'
-require 'models/car'
-require 'models/bulb'
-require 'models/mixed_case_monkey'
-require 'models/admin'
-require 'models/admin/account'
-require 'models/admin/user'
+require "models/man"
+require "models/face"
+require "models/interest"
+require "models/zine"
+require "models/club"
+require "models/sponsor"
+require "models/rating"
+require "models/comment"
+require "models/car"
+require "models/bulb"
+require "models/mixed_case_monkey"
+require "models/admin"
+require "models/admin/account"
+require "models/admin/user"
+require "models/developer"
+require "models/company"
+require "models/project"
+require "models/author"
+require "models/post"
+require "models/department"
+require "models/hotel"
class AutomaticInverseFindingTests < ActiveRecord::TestCase
fixtures :ratings, :comments, :cars
@@ -21,11 +30,9 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase
monkey_reflection = MixedCaseMonkey.reflect_on_association(:man)
man_reflection = Man.reflect_on_association(:mixed_case_monkey)
- assert_respond_to monkey_reflection, :has_inverse?
assert monkey_reflection.has_inverse?, "The monkey reflection should have an inverse"
assert_equal man_reflection, monkey_reflection.inverse_of, "The monkey reflection's inverse should be the man reflection"
- assert_respond_to man_reflection, :has_inverse?
assert man_reflection.has_inverse?, "The man reflection should have an inverse"
assert_equal monkey_reflection, man_reflection.inverse_of, "The man reflection's inverse should be the monkey reflection"
end
@@ -34,7 +41,6 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase
account_reflection = Admin::Account.reflect_on_association(:users)
user_reflection = Admin::User.reflect_on_association(:account)
- assert_respond_to account_reflection, :has_inverse?
assert account_reflection.has_inverse?, "The Admin::Account reflection should have an inverse"
assert_equal user_reflection, account_reflection.inverse_of, "The Admin::Account reflection's inverse should be the Admin::User reflection"
end
@@ -43,11 +49,9 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase
car_reflection = Car.reflect_on_association(:bulb)
bulb_reflection = Bulb.reflect_on_association(:car)
- assert_respond_to car_reflection, :has_inverse?
assert car_reflection.has_inverse?, "The Car reflection should have an inverse"
assert_equal bulb_reflection, car_reflection.inverse_of, "The Car reflection's inverse should be the Bulb reflection"
- assert_respond_to bulb_reflection, :has_inverse?
assert bulb_reflection.has_inverse?, "The Bulb reflection should have an inverse"
assert_equal car_reflection, bulb_reflection.inverse_of, "The Bulb reflection's inverse should be the Car reflection"
end
@@ -56,11 +60,24 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase
comment_reflection = Comment.reflect_on_association(:ratings)
rating_reflection = Rating.reflect_on_association(:comment)
- assert_respond_to comment_reflection, :has_inverse?
assert comment_reflection.has_inverse?, "The Comment reflection should have an inverse"
assert_equal rating_reflection, comment_reflection.inverse_of, "The Comment reflection's inverse should be the Rating reflection"
end
+ def test_has_many_and_belongs_to_should_find_inverse_automatically_for_sti
+ author_reflection = Author.reflect_on_association(:posts)
+ author_child_reflection = Author.reflect_on_association(:special_posts)
+ post_reflection = Post.reflect_on_association(:author)
+
+ assert_respond_to author_reflection, :has_inverse?
+ assert author_reflection.has_inverse?, "The Author reflection should have an inverse"
+ assert_equal post_reflection, author_reflection.inverse_of, "The Author reflection's inverse should be the Post reflection"
+
+ assert_respond_to author_child_reflection, :has_inverse?
+ assert author_child_reflection.has_inverse?, "The Author reflection should have an inverse"
+ assert_equal post_reflection, author_child_reflection.inverse_of, "The Author reflection's inverse should be the Post reflection"
+ end
+
def test_has_one_and_belongs_to_automatic_inverse_shares_objects
car = Car.first
bulb = Bulb.create!(car: car)
@@ -80,10 +97,10 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase
assert_equal rating.comment, comment, "The Rating's comment should be the original Comment"
- rating.comment.body = "Brogramming is the act of programming, like a bro."
+ rating.comment.body = "Fennec foxes are the smallest of the foxes."
assert_equal rating.comment.body, comment.body, "Changing the Comment's body on the association should change the original Comment's body"
- comment.body = "Broseiden is the king of the sea of bros."
+ comment.body = "Kittens are adorable."
assert_equal comment.body, rating.comment.body, "Changing the original Comment's body should change the Comment's body on the association"
end
@@ -94,87 +111,63 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase
assert_equal rating.comment, comment, "The Rating's comment should be the original Comment"
- rating.comment.body = "Brogramming is the act of programming, like a bro."
+ rating.comment.body = "Fennec foxes are the smallest of the foxes."
assert_equal rating.comment.body, comment.body, "Changing the Comment's body on the association should change the original Comment's body"
- comment.body = "Broseiden is the king of the sea of bros."
+ comment.body = "Kittens are adorable."
assert_equal comment.body, rating.comment.body, "Changing the original Comment's body should change the Comment's body on the association"
end
def test_polymorphic_and_has_many_through_relationships_should_not_have_inverses
sponsor_reflection = Sponsor.reflect_on_association(:sponsorable)
- assert_respond_to sponsor_reflection, :has_inverse?
- assert !sponsor_reflection.has_inverse?, "A polymorphic association should not find an inverse automatically"
+ assert_not sponsor_reflection.has_inverse?, "A polymorphic association should not find an inverse automatically"
club_reflection = Club.reflect_on_association(:members)
- assert_respond_to club_reflection, :has_inverse?
- assert !club_reflection.has_inverse?, "A has_many_through association should not find an inverse automatically"
+ assert_not club_reflection.has_inverse?, "A has_many_through association should not find an inverse automatically"
end
- def test_polymorphic_relationships_should_still_not_have_inverses_when_non_polymorphic_relationship_has_the_same_name
+ def test_polymorphic_has_one_should_find_inverse_automatically
man_reflection = Man.reflect_on_association(:polymorphic_face_without_inverse)
- face_reflection = Face.reflect_on_association(:man)
-
- assert_respond_to face_reflection, :has_inverse?
- assert face_reflection.has_inverse?, "For this test, the non-polymorphic association must have an inverse"
- assert_respond_to man_reflection, :has_inverse?
- assert !man_reflection.has_inverse?, "The target of a polymorphic association should not find an inverse automatically"
+ assert_predicate man_reflection, :has_inverse?
end
end
class InverseAssociationTests < ActiveRecord::TestCase
def test_should_allow_for_inverse_of_options_in_associations
- assert_nothing_raised(ArgumentError, 'ActiveRecord should allow the inverse_of options on has_many') do
- Class.new(ActiveRecord::Base).has_many(:wheels, :inverse_of => :car)
+ assert_nothing_raised do
+ Class.new(ActiveRecord::Base).has_many(:wheels, inverse_of: :car)
end
- assert_nothing_raised(ArgumentError, 'ActiveRecord should allow the inverse_of options on has_one') do
- Class.new(ActiveRecord::Base).has_one(:engine, :inverse_of => :car)
+ assert_nothing_raised do
+ Class.new(ActiveRecord::Base).has_one(:engine, inverse_of: :car)
end
- assert_nothing_raised(ArgumentError, 'ActiveRecord should allow the inverse_of options on belongs_to') do
- Class.new(ActiveRecord::Base).belongs_to(:car, :inverse_of => :driver)
+ assert_nothing_raised do
+ Class.new(ActiveRecord::Base).belongs_to(:car, inverse_of: :driver)
end
end
def test_should_be_able_to_ask_a_reflection_if_it_has_an_inverse
has_one_with_inverse_ref = Man.reflect_on_association(:face)
- assert_respond_to has_one_with_inverse_ref, :has_inverse?
- assert has_one_with_inverse_ref.has_inverse?
+ assert_predicate has_one_with_inverse_ref, :has_inverse?
has_many_with_inverse_ref = Man.reflect_on_association(:interests)
- assert_respond_to has_many_with_inverse_ref, :has_inverse?
- assert has_many_with_inverse_ref.has_inverse?
+ assert_predicate has_many_with_inverse_ref, :has_inverse?
belongs_to_with_inverse_ref = Face.reflect_on_association(:man)
- assert_respond_to belongs_to_with_inverse_ref, :has_inverse?
- assert belongs_to_with_inverse_ref.has_inverse?
+ assert_predicate belongs_to_with_inverse_ref, :has_inverse?
has_one_without_inverse_ref = Club.reflect_on_association(:sponsor)
- assert_respond_to has_one_without_inverse_ref, :has_inverse?
- assert !has_one_without_inverse_ref.has_inverse?
+ assert_not_predicate has_one_without_inverse_ref, :has_inverse?
has_many_without_inverse_ref = Club.reflect_on_association(:memberships)
- assert_respond_to has_many_without_inverse_ref, :has_inverse?
- assert !has_many_without_inverse_ref.has_inverse?
+ assert_not_predicate has_many_without_inverse_ref, :has_inverse?
belongs_to_without_inverse_ref = Sponsor.reflect_on_association(:sponsor_club)
- assert_respond_to belongs_to_without_inverse_ref, :has_inverse?
- assert !belongs_to_without_inverse_ref.has_inverse?
- end
-
- def test_should_be_able_to_ask_a_reflection_what_it_is_the_inverse_of
- has_one_ref = Man.reflect_on_association(:face)
- assert_respond_to has_one_ref, :inverse_of
-
- has_many_ref = Man.reflect_on_association(:interests)
- assert_respond_to has_many_ref, :inverse_of
-
- belongs_to_ref = Face.reflect_on_association(:man)
- assert_respond_to belongs_to_ref, :inverse_of
+ assert_not_predicate belongs_to_without_inverse_ref, :has_inverse?
end
def test_inverse_of_method_should_supply_the_actual_reflection_instance_it_is_the_inverse_of
@@ -198,6 +191,26 @@ class InverseAssociationTests < ActiveRecord::TestCase
belongs_to_ref = Sponsor.reflect_on_association(:sponsor_club)
assert_nil belongs_to_ref.inverse_of
end
+
+ def test_polymorphic_associations_dont_attempt_to_find_inverse_of
+ belongs_to_ref = Sponsor.reflect_on_association(:sponsor)
+ assert_raise(ArgumentError) { belongs_to_ref.klass }
+ assert_nil belongs_to_ref.inverse_of
+
+ belongs_to_ref = Face.reflect_on_association(:human)
+ assert_raise(ArgumentError) { belongs_to_ref.klass }
+ assert_nil belongs_to_ref.inverse_of
+ end
+
+ def test_this_inverse_stuff
+ firm = Firm.create!(name: "Adequate Holdings")
+ Project.create!(name: "Project 1", firm: firm)
+ Developer.create!(name: "Gorbypuff", firm: firm)
+
+ new_project = Project.last
+ assert Project.reflect_on_association(:lead_developer).inverse_of.present?, "Expected inverse of to be present"
+ assert new_project.lead_developer.present?, "Expected lead developer to be present on the project"
+ end
end
class InverseHasOneTests < ActiveRecord::TestCase
@@ -207,73 +220,72 @@ class InverseHasOneTests < ActiveRecord::TestCase
m = men(:gordon)
f = m.face
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
- m.name = 'Bongo'
+ m.name = "Bongo"
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
- f.man.name = 'Mungo'
+ f.man.name = "Mungo"
assert_equal m.name, f.man.name, "Name of man should be the same after changes to child-owned instance"
end
-
def test_parent_instance_should_be_shared_with_eager_loaded_child_on_find
- m = Man.all.merge!(:where => {:name => 'Gordon'}, :includes => :face).first
+ m = Man.all.merge!(where: { name: "Gordon" }, includes: :face).first
f = m.face
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
- m.name = 'Bongo'
+ m.name = "Bongo"
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
- f.man.name = 'Mungo'
+ f.man.name = "Mungo"
assert_equal m.name, f.man.name, "Name of man should be the same after changes to child-owned instance"
- m = Man.all.merge!(:where => {:name => 'Gordon'}, :includes => :face, :order => 'faces.id').first
+ m = Man.all.merge!(where: { name: "Gordon" }, includes: :face, order: "faces.id").first
f = m.face
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
- m.name = 'Bongo'
+ m.name = "Bongo"
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
- f.man.name = 'Mungo'
+ f.man.name = "Mungo"
assert_equal m.name, f.man.name, "Name of man should be the same after changes to child-owned instance"
end
def test_parent_instance_should_be_shared_with_newly_built_child
m = Man.first
- f = m.build_face(:description => 'haunted')
+ f = m.build_face(description: "haunted")
assert_not_nil f.man
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
- m.name = 'Bongo'
+ m.name = "Bongo"
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
- f.man.name = 'Mungo'
+ f.man.name = "Mungo"
assert_equal m.name, f.man.name, "Name of man should be the same after changes to just-built-child-owned instance"
end
def test_parent_instance_should_be_shared_with_newly_created_child
m = Man.first
- f = m.create_face(:description => 'haunted')
+ f = m.create_face(description: "haunted")
assert_not_nil f.man
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
- m.name = 'Bongo'
+ m.name = "Bongo"
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
- f.man.name = 'Mungo'
+ f.man.name = "Mungo"
assert_equal m.name, f.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
end
def test_parent_instance_should_be_shared_with_newly_created_child_via_bang_method
m = Man.first
- f = m.create_face!(:description => 'haunted')
+ f = m.create_face!(description: "haunted")
assert_not_nil f.man
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
- m.name = 'Bongo'
+ m.name = "Bongo"
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
- f.man.name = 'Mungo'
+ f.man.name = "Mungo"
assert_equal m.name, f.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
end
def test_parent_instance_should_be_shared_with_replaced_via_accessor_child
m = Man.first
- f = Face.new(:description => 'haunted')
+ f = Face.new(description: "haunted")
m.face = f
assert_not_nil f.man
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
- m.name = 'Bongo'
+ m.name = "Bongo"
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
- f.man.name = 'Mungo'
+ f.man.name = "Mungo"
assert_equal m.name, f.man.name, "Name of man should be the same after changes to replaced-child-owned instance"
end
@@ -283,74 +295,95 @@ class InverseHasOneTests < ActiveRecord::TestCase
end
class InverseHasManyTests < ActiveRecord::TestCase
- fixtures :men, :interests
+ fixtures :men, :interests, :posts, :authors, :author_addresses
def test_parent_instance_should_be_shared_with_every_child_on_find
m = men(:gordon)
is = m.interests
is.each do |i|
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
- m.name = 'Bongo'
+ m.name = "Bongo"
assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
- i.man.name = 'Mungo'
+ i.man.name = "Mungo"
assert_equal m.name, i.man.name, "Name of man should be the same after changes to child-owned instance"
end
end
+ def test_parent_instance_should_be_shared_with_every_child_on_find_for_sti
+ a = authors(:david)
+ ps = a.posts
+ ps.each do |p|
+ assert_equal a.name, p.author.name, "Name of man should be the same before changes to parent instance"
+ a.name = "Bongo"
+ assert_equal a.name, p.author.name, "Name of man should be the same after changes to parent instance"
+ p.author.name = "Mungo"
+ assert_equal a.name, p.author.name, "Name of man should be the same after changes to child-owned instance"
+ end
+
+ sps = a.special_posts
+ sps.each do |sp|
+ assert_equal a.name, sp.author.name, "Name of man should be the same before changes to parent instance"
+ a.name = "Bongo"
+ assert_equal a.name, sp.author.name, "Name of man should be the same after changes to parent instance"
+ sp.author.name = "Mungo"
+ assert_equal a.name, sp.author.name, "Name of man should be the same after changes to child-owned instance"
+ end
+ end
+
def test_parent_instance_should_be_shared_with_eager_loaded_children
- m = Man.all.merge!(:where => {:name => 'Gordon'}, :includes => :interests).first
+ m = Man.all.merge!(where: { name: "Gordon" }, includes: :interests).first
is = m.interests
is.each do |i|
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
- m.name = 'Bongo'
+ m.name = "Bongo"
assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
- i.man.name = 'Mungo'
+ i.man.name = "Mungo"
assert_equal m.name, i.man.name, "Name of man should be the same after changes to child-owned instance"
end
- m = Man.all.merge!(:where => {:name => 'Gordon'}, :includes => :interests, :order => 'interests.id').first
+ m = Man.all.merge!(where: { name: "Gordon" }, includes: :interests, order: "interests.id").first
is = m.interests
is.each do |i|
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
- m.name = 'Bongo'
+ m.name = "Bongo"
assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
- i.man.name = 'Mungo'
+ i.man.name = "Mungo"
assert_equal m.name, i.man.name, "Name of man should be the same after changes to child-owned instance"
end
end
def test_parent_instance_should_be_shared_with_newly_block_style_built_child
m = Man.first
- i = m.interests.build {|ii| ii.topic = 'Industrial Revolution Re-enactment'}
+ i = m.interests.build { |ii| ii.topic = "Industrial Revolution Re-enactment" }
assert_not_nil i.topic, "Child attributes supplied to build via blocks should be populated"
assert_not_nil i.man
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
- m.name = 'Bongo'
+ m.name = "Bongo"
assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
- i.man.name = 'Mungo'
+ i.man.name = "Mungo"
assert_equal m.name, i.man.name, "Name of man should be the same after changes to just-built-child-owned instance"
end
def test_parent_instance_should_be_shared_with_newly_created_via_bang_method_child
m = Man.first
- i = m.interests.create!(:topic => 'Industrial Revolution Re-enactment')
+ i = m.interests.create!(topic: "Industrial Revolution Re-enactment")
assert_not_nil i.man
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
- m.name = 'Bongo'
+ m.name = "Bongo"
assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
- i.man.name = 'Mungo'
+ i.man.name = "Mungo"
assert_equal m.name, i.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
end
def test_parent_instance_should_be_shared_with_newly_block_style_created_child
m = Man.first
- i = m.interests.create {|ii| ii.topic = 'Industrial Revolution Re-enactment'}
+ i = m.interests.create { |ii| ii.topic = "Industrial Revolution Re-enactment" }
assert_not_nil i.topic, "Child attributes supplied to create via blocks should be populated"
assert_not_nil i.man
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
- m.name = 'Bongo'
+ m.name = "Bongo"
assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
- i.man.name = 'Mungo'
+ i.man.name = "Mungo"
assert_equal m.name, i.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
end
@@ -372,25 +405,25 @@ class InverseHasManyTests < ActiveRecord::TestCase
def test_parent_instance_should_be_shared_with_poked_in_child
m = men(:gordon)
- i = Interest.create(:topic => 'Industrial Revolution Re-enactment')
+ i = Interest.create(topic: "Industrial Revolution Re-enactment")
m.interests << i
assert_not_nil i.man
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
- m.name = 'Bongo'
+ m.name = "Bongo"
assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
- i.man.name = 'Mungo'
+ i.man.name = "Mungo"
assert_equal m.name, i.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
end
def test_parent_instance_should_be_shared_with_replaced_via_accessor_children
m = Man.first
- i = Interest.new(:topic => 'Industrial Revolution Re-enactment')
+ i = Interest.new(topic: "Industrial Revolution Re-enactment")
m.interests = [i]
assert_not_nil i.man
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
- m.name = 'Bongo'
+ m.name = "Bongo"
assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
- i.man.name = 'Mungo'
+ i.man.name = "Mungo"
assert_equal m.name, i.man.name, "Name of man should be the same after changes to replaced-child-owned instance"
end
@@ -431,7 +464,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"
@@ -443,7 +476,7 @@ class InverseHasManyTests < ActiveRecord::TestCase
interest = Interest.create!(man: man)
man.interests.find(interest.id)
- assert_not man.interests.loaded?
+ assert_not_predicate man.interests, :loaded?
end
def test_raise_record_not_found_error_when_invalid_ids_are_passed
@@ -463,7 +496,10 @@ class InverseHasManyTests < ActiveRecord::TestCase
def test_raise_record_not_found_error_when_no_ids_are_passed
man = Man.create!
- assert_raise(ActiveRecord::RecordNotFound) { man.interests.find() }
+ exception = assert_raise(ActiveRecord::RecordNotFound) { man.interests.load.find() }
+
+ assert_equal exception.model, "Interest"
+ assert_equal exception.primary_key, "id"
end
def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error
@@ -472,7 +508,7 @@ class InverseHasManyTests < ActiveRecord::TestCase
def test_child_instance_should_point_to_parent_without_saving
man = Man.new
- i = Interest.create(:topic => 'Industrial Revolution Re-enactment')
+ i = Interest.create(topic: "Industrial Revolution Re-enactment")
man.interests << i
assert_not_nil i.man
@@ -480,7 +516,34 @@ class InverseHasManyTests < ActiveRecord::TestCase
i.man.name = "Charles"
assert_equal i.man.name, man.name
- assert !man.persisted?
+ assert_not_predicate man, :persisted?
+ end
+
+ def test_inverse_instance_should_be_set_before_find_callbacks_are_run
+ reset_callbacks(Interest, :find) do
+ Interest.after_find { raise unless association(:man).loaded? && man.present? }
+
+ assert_predicate Man.first.interests.reload, :any?
+ assert_predicate Man.includes(:interests).first.interests, :any?
+ assert_predicate Man.joins(:interests).includes(:interests).first.interests, :any?
+ end
+ end
+
+ def test_inverse_instance_should_be_set_before_initialize_callbacks_are_run
+ reset_callbacks(Interest, :initialize) do
+ Interest.after_initialize { raise unless association(:man).loaded? && man.present? }
+
+ assert_predicate Man.first.interests.reload, :any?
+ assert_predicate Man.includes(:interests).first.interests, :any?
+ assert_predicate Man.joins(:interests).includes(:interests).first.interests, :any?
+ end
+ end
+
+ def reset_callbacks(target, type)
+ old_callbacks = target.send(:get_callbacks, type).deep_dup
+ yield
+ ensure
+ target.send(:set_callbacks, type, old_callbacks) if old_callbacks
end
end
@@ -491,49 +554,49 @@ class InverseBelongsToTests < ActiveRecord::TestCase
f = faces(:trusting)
m = f.man
assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
- f.description = 'gormless'
+ f.description = "gormless"
assert_equal f.description, m.face.description, "Description of face should be the same after changes to child instance"
- m.face.description = 'pleasing'
+ m.face.description = "pleasing"
assert_equal f.description, m.face.description, "Description of face should be the same after changes to parent-owned instance"
end
def test_eager_loaded_child_instance_should_be_shared_with_parent_on_find
- f = Face.all.merge!(:includes => :man, :where => {:description => 'trusting'}).first
+ f = Face.all.merge!(includes: :man, where: { description: "trusting" }).first
m = f.man
assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
- f.description = 'gormless'
+ f.description = "gormless"
assert_equal f.description, m.face.description, "Description of face should be the same after changes to child instance"
- m.face.description = 'pleasing'
+ m.face.description = "pleasing"
assert_equal f.description, m.face.description, "Description of face should be the same after changes to parent-owned instance"
- f = Face.all.merge!(:includes => :man, :order => 'men.id', :where => {:description => 'trusting'}).first
+ f = Face.all.merge!(includes: :man, order: "men.id", where: { description: "trusting" }).first
m = f.man
assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
- f.description = 'gormless'
+ f.description = "gormless"
assert_equal f.description, m.face.description, "Description of face should be the same after changes to child instance"
- m.face.description = 'pleasing'
+ m.face.description = "pleasing"
assert_equal f.description, m.face.description, "Description of face should be the same after changes to parent-owned instance"
end
def test_child_instance_should_be_shared_with_newly_built_parent
f = faces(:trusting)
- m = f.build_man(:name => 'Charles')
+ m = f.build_man(name: "Charles")
assert_not_nil m.face
assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
- f.description = 'gormless'
+ f.description = "gormless"
assert_equal f.description, m.face.description, "Description of face should be the same after changes to child instance"
- m.face.description = 'pleasing'
+ m.face.description = "pleasing"
assert_equal f.description, m.face.description, "Description of face should be the same after changes to just-built-parent-owned instance"
end
def test_child_instance_should_be_shared_with_newly_created_parent
f = faces(:trusting)
- m = f.create_man(:name => 'Charles')
+ m = f.create_man(name: "Charles")
assert_not_nil m.face
assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
- f.description = 'gormless'
+ f.description = "gormless"
assert_equal f.description, m.face.description, "Description of face should be the same after changes to child instance"
- m.face.description = 'pleasing'
+ m.face.description = "pleasing"
assert_equal f.description, m.face.description, "Description of face should be the same after changes to newly-created-parent-owned instance"
end
@@ -541,24 +604,24 @@ class InverseBelongsToTests < ActiveRecord::TestCase
i = interests(:trainspotting)
m = i.man
assert_not_nil m.interests
- iz = m.interests.detect { |_iz| _iz.id == i.id}
+ iz = m.interests.detect { |_iz| _iz.id == i.id }
assert_not_nil iz
assert_equal i.topic, iz.topic, "Interest topics should be the same before changes to child"
- i.topic = 'Eating cheese with a spoon'
+ i.topic = "Eating cheese with a spoon"
assert_not_equal i.topic, iz.topic, "Interest topics should not be the same after changes to child"
- iz.topic = 'Cow tipping'
+ iz.topic = "Cow tipping"
assert_not_equal i.topic, iz.topic, "Interest topics should not be the same after changes to parent-owned instance"
end
def test_child_instance_should_be_shared_with_replaced_via_accessor_parent
f = Face.first
- m = Man.new(:name => 'Charles')
+ m = Man.new(name: "Charles")
f.man = m
assert_not_nil m.face
assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
- f.description = 'gormless'
+ f.description = "gormless"
assert_equal f.description, m.face.description, "Description of face should be the same after changes to child instance"
- m.face.description = 'pleasing'
+ m.face.description = "pleasing"
assert_equal f.description, m.face.description, "Description of face should be the same after changes to replaced-parent-owned instance"
end
@@ -571,30 +634,30 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase
fixtures :men, :faces, :interests
def test_child_instance_should_be_shared_with_parent_on_find
- f = Face.all.merge!(:where => {:description => 'confused'}).first
+ f = Face.all.merge!(where: { description: "confused" }).first
m = f.polymorphic_man
assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same before changes to child instance"
- f.description = 'gormless'
+ f.description = "gormless"
assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to child instance"
- m.polymorphic_face.description = 'pleasing'
+ m.polymorphic_face.description = "pleasing"
assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to parent-owned instance"
end
def test_eager_loaded_child_instance_should_be_shared_with_parent_on_find
- f = Face.all.merge!(:where => {:description => 'confused'}, :includes => :man).first
+ f = Face.all.merge!(where: { description: "confused" }, includes: :man).first
m = f.polymorphic_man
assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same before changes to child instance"
- f.description = 'gormless'
+ f.description = "gormless"
assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to child instance"
- m.polymorphic_face.description = 'pleasing'
+ m.polymorphic_face.description = "pleasing"
assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to parent-owned instance"
- f = Face.all.merge!(:where => {:description => 'confused'}, :includes => :man, :order => 'men.id').first
+ f = Face.all.merge!(where: { description: "confused" }, includes: :man, order: "men.id").first
m = f.polymorphic_man
assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same before changes to child instance"
- f.description = 'gormless'
+ f.description = "gormless"
assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to child instance"
- m.polymorphic_face.description = 'pleasing'
+ m.polymorphic_face.description = "pleasing"
assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to parent-owned instance"
end
@@ -606,23 +669,9 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase
face.polymorphic_man = new_man
assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same before changes to parent instance"
- face.description = 'Bongo'
- assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to parent instance"
- new_man.polymorphic_face.description = 'Mungo'
- assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to replaced-parent-owned instance"
- end
-
- def test_child_instance_should_be_shared_with_replaced_via_method_parent
- face = faces(:confused)
- new_man = Man.new
-
- assert_not_nil face.polymorphic_man
- face.polymorphic_man = new_man
-
- assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same before changes to parent instance"
- face.description = 'Bongo'
+ face.description = "Bongo"
assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to parent instance"
- new_man.polymorphic_face.description = 'Mungo'
+ new_man.polymorphic_face.description = "Mungo"
assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to replaced-parent-owned instance"
end
@@ -638,22 +687,32 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase
assert_equal old_inversed_man.object_id, new_inversed_man.object_id
end
+ def test_inversed_instance_should_not_be_reloaded_after_stale_state_changed_with_validation
+ face = Face.new man: Man.new
+
+ old_inversed_man = face.man
+ face.save!
+ new_inversed_man = face.man
+
+ assert_equal old_inversed_man.object_id, new_inversed_man.object_id
+ end
+
def test_should_not_try_to_set_inverse_instances_when_the_inverse_is_a_has_many
i = interests(:llama_wrangling)
m = i.polymorphic_man
assert_not_nil m.polymorphic_interests
- iz = m.polymorphic_interests.detect { |_iz| _iz.id == i.id}
+ iz = m.polymorphic_interests.detect { |_iz| _iz.id == i.id }
assert_not_nil iz
assert_equal i.topic, iz.topic, "Interest topics should be the same before changes to child"
- i.topic = 'Eating cheese with a spoon'
+ i.topic = "Eating cheese with a spoon"
assert_not_equal i.topic, iz.topic, "Interest topics should not be the same after changes to child"
- iz.topic = 'Cow tipping'
+ iz.topic = "Cow tipping"
assert_not_equal i.topic, iz.topic, "Interest topics should not be the same after changes to parent-owned instance"
end
def test_trying_to_access_inverses_that_dont_exist_shouldnt_raise_an_error
# Ideally this would, if only for symmetry's sake with other association types
- assert_nothing_raised(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.horrible_polymorphic_man }
+ assert_nothing_raised { Face.first.horrible_polymorphic_man }
end
def test_trying_to_set_polymorphic_inverses_that_dont_exist_at_all_should_raise_an_error
@@ -663,10 +722,20 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase
def test_trying_to_set_polymorphic_inverses_that_dont_exist_on_the_instance_being_set_should_raise_an_error
# passes because Man does have the correct inverse_of
- assert_nothing_raised(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.polymorphic_man = Man.first }
+ assert_nothing_raised { Face.first.polymorphic_man = Man.first }
# fails because Interest does have the correct inverse_of
assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.polymorphic_man = Interest.first }
end
+
+ def test_favors_has_one_associations_for_inverse_of
+ inverse_name = Post.reflect_on_association(:author).inverse_of.name
+ assert_equal :post, inverse_name
+ end
+
+ def test_finds_inverse_of_for_plural_associations
+ inverse_name = Department.reflect_on_association(:hotel).inverse_of.name
+ assert_equal :departments, inverse_name
+ end
end
# NOTE - these tests might not be meaningful, ripped as they were from the parental_control plugin
@@ -675,7 +744,7 @@ class InverseMultipleHasManyInversesForSameModel < ActiveRecord::TestCase
fixtures :men, :interests, :zines
def test_that_we_can_load_associations_that_have_the_same_reciprocal_name_from_different_models
- assert_nothing_raised(ActiveRecord::AssociationTypeMismatch) do
+ assert_nothing_raised do
i = Interest.first
i.zine
i.man
@@ -683,10 +752,10 @@ class InverseMultipleHasManyInversesForSameModel < ActiveRecord::TestCase
end
def test_that_we_can_create_associations_that_have_the_same_reciprocal_name_from_different_models
- assert_nothing_raised(ActiveRecord::AssociationTypeMismatch) do
+ assert_nothing_raised do
i = Interest.first
- i.build_zine(:title => 'Get Some in Winter! 2008')
- i.build_man(:name => 'Gordon')
+ i.build_zine(title: "Get Some in Winter! 2008")
+ i.build_man(name: "Gordon")
i.save!
end
end
diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb
index 5575419c35..9d1c73c33b 100644
--- a/activerecord/test/cases/associations/join_model_test.rb
+++ b/activerecord/test/cases/associations/join_model_test.rb
@@ -1,38 +1,40 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/tag'
-require 'models/tagging'
-require 'models/post'
-require 'models/rating'
-require 'models/item'
-require 'models/comment'
-require 'models/author'
-require 'models/category'
-require 'models/categorization'
-require 'models/vertex'
-require 'models/edge'
-require 'models/book'
-require 'models/citation'
-require 'models/aircraft'
-require 'models/engine'
-require 'models/car'
+require "models/tag"
+require "models/tagging"
+require "models/post"
+require "models/rating"
+require "models/item"
+require "models/comment"
+require "models/author"
+require "models/category"
+require "models/categorization"
+require "models/vertex"
+require "models/edge"
+require "models/book"
+require "models/citation"
+require "models/aircraft"
+require "models/engine"
+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
def test_has_many
- assert authors(:david).categories.include?(categories(:general))
+ assert_includes authors(:david).categories, categories(:general)
end
def test_has_many_inherited
- assert authors(:mary).categories.include?(categories(:sti_test))
+ assert_includes authors(:mary).categories, categories(:sti_test)
end
def test_inherited_has_many
- assert categories(:sti_test).authors.include?(authors(:mary))
+ assert_includes categories(:sti_test).authors, authors(:mary)
end
def test_has_many_distinct_through_join_model
@@ -42,11 +44,11 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_has_many_distinct_through_count
author = authors(:mary)
- assert !authors(:mary).unique_categorized_posts.loaded?
+ assert_not_predicate authors(:mary).unique_categorized_posts, :loaded?
assert_queries(1) { assert_equal 1, author.unique_categorized_posts.count }
assert_queries(1) { assert_equal 1, author.unique_categorized_posts.count(:title) }
assert_queries(1) { assert_equal 0, author.unique_categorized_posts.where(title: nil).count(:title) }
- assert !authors(:mary).unique_categorized_posts.loaded?
+ assert_not_predicate authors(:mary).unique_categorized_posts, :loaded?
end
def test_has_many_distinct_through_find
@@ -88,7 +90,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_polymorphic_has_many_going_through_join_model_with_custom_select_and_joins
assert_equal tags(:general), tag = posts(:welcome).tags.add_joins_and_select.first
- assert_nothing_raised(NoMethodError) { tag.author_id }
+ assert_nothing_raised { tag.author_id }
end
def test_polymorphic_has_many_going_through_join_model_with_custom_foreign_key
@@ -97,11 +99,11 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
end
def test_polymorphic_has_many_create_model_with_inheritance_and_custom_base_class
- post = SubStiPost.create :title => 'SubStiPost', :body => 'SubStiPost body'
- assert_instance_of SubStiPost, post
+ post = SubAbstractStiPost.create title: "SubAbstractStiPost", body: "SubAbstractStiPost body"
+ assert_instance_of SubAbstractStiPost, post
- tagging = tags(:misc).taggings.create(:taggable => post)
- assert_equal "SubStiPost", tagging.taggable_type
+ tagging = tags(:misc).taggings.create(taggable: post)
+ assert_equal "SubAbstractStiPost", tagging.taggable_type
end
def test_polymorphic_has_many_going_through_join_model_with_inheritance
@@ -116,12 +118,12 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
post = posts(:thinking)
assert_instance_of SpecialPost, post
- tagging = tags(:misc).taggings.create(:taggable => post)
+ tagging = tags(:misc).taggings.create(taggable: post)
assert_equal "Post", tagging.taggable_type
end
def test_polymorphic_has_one_create_model_with_inheritance
- tagging = tags(:misc).create_tagging(:taggable => posts(:thinking))
+ tagging = tags(:misc).create_tagging(taggable: posts(:thinking))
assert_equal "Post", tagging.taggable_type
end
@@ -142,7 +144,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_set_polymorphic_has_one_on_new_record
tagging = tags(:misc).taggings.create
- post = Post.new :title => "foo", :body => "bar"
+ post = Post.new title: "foo", body: "bar"
post.tagging = tagging
post.save!
@@ -153,50 +155,50 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_create_polymorphic_has_many_with_scope
old_count = posts(:welcome).taggings.count
- tagging = posts(:welcome).taggings.create(:tag => tags(:misc))
+ tagging = posts(:welcome).taggings.create(tag: tags(:misc))
assert_equal "Post", tagging.taggable_type
- assert_equal old_count+1, posts(:welcome).taggings.count
+ assert_equal old_count + 1, posts(:welcome).taggings.count
end
def test_create_bang_polymorphic_with_has_many_scope
old_count = posts(:welcome).taggings.count
- tagging = posts(:welcome).taggings.create!(:tag => tags(:misc))
+ tagging = posts(:welcome).taggings.create!(tag: tags(:misc))
assert_equal "Post", tagging.taggable_type
- assert_equal old_count+1, posts(:welcome).taggings.count
+ assert_equal old_count + 1, posts(:welcome).taggings.count
end
def test_create_polymorphic_has_one_with_scope
old_count = Tagging.count
- tagging = posts(:welcome).create_tagging(:tag => tags(:misc))
+ tagging = posts(:welcome).create_tagging(tag: tags(:misc))
assert_equal "Post", tagging.taggable_type
- assert_equal old_count+1, Tagging.count
+ assert_equal old_count + 1, Tagging.count
end
def test_delete_polymorphic_has_many_with_delete_all
assert_equal 1, posts(:welcome).taggings.count
- posts(:welcome).taggings.first.update_columns taggable_type: 'PostWithHasManyDeleteAll'
+ posts(:welcome).taggings.first.update_columns taggable_type: "PostWithHasManyDeleteAll"
post = find_post_with_dependency(1, :has_many, :taggings, :delete_all)
old_count = Tagging.count
post.destroy
- assert_equal old_count-1, Tagging.count
+ assert_equal old_count - 1, Tagging.count
assert_equal 0, posts(:welcome).taggings.count
end
def test_delete_polymorphic_has_many_with_destroy
assert_equal 1, posts(:welcome).taggings.count
- posts(:welcome).taggings.first.update_columns taggable_type: 'PostWithHasManyDestroy'
+ posts(:welcome).taggings.first.update_columns taggable_type: "PostWithHasManyDestroy"
post = find_post_with_dependency(1, :has_many, :taggings, :destroy)
old_count = Tagging.count
post.destroy
- assert_equal old_count-1, Tagging.count
+ assert_equal old_count - 1, Tagging.count
assert_equal 0, posts(:welcome).taggings.count
end
def test_delete_polymorphic_has_many_with_nullify
assert_equal 1, posts(:welcome).taggings.count
- posts(:welcome).taggings.first.update_columns taggable_type: 'PostWithHasManyNullify'
+ posts(:welcome).taggings.first.update_columns taggable_type: "PostWithHasManyNullify"
post = find_post_with_dependency(1, :has_many, :taggings, :nullify)
old_count = Tagging.count
@@ -207,24 +209,26 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_delete_polymorphic_has_one_with_destroy
assert posts(:welcome).tagging
- posts(:welcome).tagging.update_columns taggable_type: 'PostWithHasOneDestroy'
+ posts(:welcome).tagging.update_columns taggable_type: "PostWithHasOneDestroy"
post = find_post_with_dependency(1, :has_one, :tagging, :destroy)
old_count = Tagging.count
post.destroy
- assert_equal old_count-1, Tagging.count
- assert_nil posts(:welcome).tagging(true)
+ assert_equal old_count - 1, Tagging.count
+ posts(:welcome).association(:tagging).reload
+ assert_nil posts(:welcome).tagging
end
def test_delete_polymorphic_has_one_with_nullify
assert posts(:welcome).tagging
- posts(:welcome).tagging.update_columns taggable_type: 'PostWithHasOneNullify'
+ posts(:welcome).tagging.update_columns taggable_type: "PostWithHasOneNullify"
post = find_post_with_dependency(1, :has_one, :tagging, :nullify)
old_count = Tagging.count
post.destroy
assert_equal old_count, Tagging.count
- assert_nil posts(:welcome).tagging(true)
+ posts(:welcome).association(:tagging).reload
+ assert_nil posts(:welcome).tagging
end
def test_has_many_with_piggyback
@@ -233,15 +237,15 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_create_through_has_many_with_piggyback
category = categories(:sti_test)
- ernie = category.authors_with_select.create(:name => 'Ernie')
+ ernie = category.authors_with_select.create(name: "Ernie")
assert_nothing_raised do
- assert_equal ernie, category.authors_with_select.detect {|a| a.name == 'Ernie'}
+ assert_equal ernie, category.authors_with_select.detect { |a| a.name == "Ernie" }
end
end
def test_include_has_many_through
- posts = Post.all.merge!(:order => 'posts.id').to_a
- posts_with_authors = Post.all.merge!(:includes => :authors, :order => 'posts.id').to_a
+ posts = Post.all.merge!(order: "posts.id").to_a
+ posts_with_authors = Post.all.merge!(includes: :authors, order: "posts.id").to_a
assert_equal posts.length, posts_with_authors.length
posts.length.times do |i|
assert_equal posts[i].authors.length, assert_no_queries { posts_with_authors[i].authors.length }
@@ -265,8 +269,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
end
def test_include_polymorphic_has_many_through
- posts = Post.all.merge!(:order => 'posts.id').to_a
- posts_with_tags = Post.all.merge!(:includes => :tags, :order => 'posts.id').to_a
+ posts = Post.all.merge!(order: "posts.id").to_a
+ posts_with_tags = Post.all.merge!(includes: :tags, order: "posts.id").to_a
assert_equal posts.length, posts_with_tags.length
posts.length.times do |i|
assert_equal posts[i].tags.length, assert_no_queries { posts_with_tags[i].tags.length }
@@ -274,8 +278,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
end
def test_include_polymorphic_has_many
- posts = Post.all.merge!(:order => 'posts.id').to_a
- posts_with_taggings = Post.all.merge!(:includes => :taggings, :order => 'posts.id').to_a
+ posts = Post.all.merge!(order: "posts.id").to_a
+ posts_with_taggings = Post.all.merge!(includes: :taggings, order: "posts.id").to_a
assert_equal posts.length, posts_with_taggings.length
posts.length.times do |i|
assert_equal posts[i].taggings.length, assert_no_queries { posts_with_taggings[i].taggings.length }
@@ -300,7 +304,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
end
def test_has_many_array_methods_called_by_method_missing
- assert authors(:david).categories.any? { |category| category.name == 'General' }
+ assert authors(:david).categories.any? { |category| category.name == "General" }
assert_nothing_raised { authors(:david).categories.sort }
end
@@ -322,12 +326,12 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
end
def test_has_many_through_with_custom_primary_key_on_has_many_source
- assert_equal [authors(:david), authors(:bob)], posts(:thinking).authors_using_custom_pk.order('authors.id')
+ assert_equal [authors(:david), authors(:bob)], posts(:thinking).authors_using_custom_pk.order("authors.id")
end
def test_belongs_to_polymorphic_with_counter_cache
assert_equal 1, posts(:welcome)[:tags_count]
- tagging = posts(:welcome).taggings.create(:tag => tags(:general))
+ tagging = posts(:welcome).taggings.create(tag: tags(:general))
assert_equal 2, posts(:welcome, :reload)[:tags_count]
tagging.destroy
assert_equal 1, posts(:welcome, :reload)[:tags_count]
@@ -352,7 +356,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
end
assert_raise ActiveRecord::EagerLoadPolymorphicError do
- tags(:general).taggings.includes(:taggable).where('bogus_table.column = 1').references(:bogus_table).to_a
+ tags(:general).taggings.includes(:taggable).where("bogus_table.column = 1").references(:bogus_table).to_a
end
end
@@ -361,8 +365,15 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
assert_equal posts(:welcome, :thinking).sort_by(&:id), tags(:general).tagged_posts.sort_by(&:id)
end
+ def test_has_many_polymorphic_associations_merges_through_scope
+ Tag.has_many :null_taggings, -> { none }, class_name: :Tagging
+ Tag.has_many :null_tagged_posts, through: :null_taggings, source: "taggable", source_type: "Post"
+ assert_equal [], tags(:general).null_tagged_posts
+ assert_not_equal [], tags(:general).tagged_posts
+ end
+
def test_eager_has_many_polymorphic_with_source_type
- tag_with_include = Tag.all.merge!(:includes => :tagged_posts).find(tags(:general).id)
+ tag_with_include = Tag.all.merge!(includes: :tagged_posts).find(tags(:general).id)
desired = posts(:welcome, :thinking)
assert_no_queries do
# added sort by ID as otherwise test using JRuby was failing as array elements were in different order
@@ -372,19 +383,19 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
end
def test_has_many_through_has_many_find_all
- assert_equal comments(:greetings), authors(:david).comments.order('comments.id').to_a.first
+ assert_equal comments(:greetings), authors(:david).comments.order("comments.id").to_a.first
end
def test_has_many_through_has_many_find_all_with_custom_class
- assert_equal comments(:greetings), authors(:david).funky_comments.order('comments.id').to_a.first
+ assert_equal comments(:greetings), authors(:david).funky_comments.order("comments.id").to_a.first
end
def test_has_many_through_has_many_find_first
- assert_equal comments(:greetings), authors(:david).comments.order('comments.id').first
+ assert_equal comments(:greetings), authors(:david).comments.order("comments.id").first
end
def test_has_many_through_has_many_find_conditions
- options = { :where => "comments.#{QUOTED_TYPE}='SpecialComment'", :order => 'comments.id' }
+ options = { where: "comments.#{QUOTED_TYPE}='SpecialComment'", order: "comments.id" }
assert_equal comments(:does_it_hurt), authors(:david).comments.merge(options).first
end
@@ -393,7 +404,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
end
def test_has_many_through_polymorphic_has_one
- assert_equal Tagging.find(1,2).sort_by(&:id), authors(:david).taggings_2
+ assert_equal Tagging.find(1, 2).sort_by(&:id), authors(:david).taggings_2.sort_by(&:id)
end
def test_has_many_through_polymorphic_has_many
@@ -404,20 +415,20 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
author = Author.includes(:taggings).find authors(:david).id
expected_taggings = taggings(:welcome_general, :thinking_general)
assert_no_queries do
- assert_equal expected_taggings, author.taggings.distinct.sort_by(&:id)
+ assert_equal expected_taggings, author.taggings.uniq.sort_by(&:id)
end
end
def test_eager_load_has_many_through_has_many
- author = Author.all.merge!(:where => ['name = ?', 'David'], :includes => :comments, :order => 'comments.id').first
+ author = Author.all.merge!(where: ["name = ?", "David"], includes: :comments, order: "comments.id").first
SpecialComment.new; VerySpecialComment.new
assert_no_queries do
- assert_equal [1,2,3,5,6,7,8,9,10,12], author.comments.collect(&:id)
+ assert_equal [1, 2, 3, 5, 6, 7, 8, 9, 10, 12], author.comments.collect(&:id)
end
end
def test_eager_load_has_many_through_has_many_with_conditions
- post = Post.all.merge!(:includes => :invalid_tags).first
+ post = Post.all.merge!(includes: :invalid_tags).first
assert_no_queries do
post.invalid_tags
end
@@ -425,8 +436,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_eager_belongs_to_and_has_one_not_singularized
assert_nothing_raised do
- Author.all.merge!(:includes => :author_address).first
- AuthorAddress.all.merge!(:includes => :author).first
+ Author.all.merge!(includes: :author_address).first
+ AuthorAddress.all.merge!(includes: :author).first
end
end
@@ -436,15 +447,15 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
end
def test_add_to_self_referential_has_many_through
- new_author = Author.create(:name => "Bob")
- authors(:david).author_favorites.create :favorite_author => new_author
+ new_author = Author.create(name: "Bob")
+ authors(:david).author_favorites.create favorite_author: new_author
assert_equal new_author, authors(:david).reload.favorite_authors.first
end
def test_has_many_through_uses_conditions_specified_on_the_has_many_association
author = Author.first
- assert author.comments.present?
- assert author.nonexistent_comments.blank?
+ assert_predicate author.comments, :present?
+ assert_predicate author.nonexistent_comments, :blank?
end
def test_has_many_through_uses_correct_attributes
@@ -453,60 +464,59 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_associating_unsaved_records_with_has_many_through
saved_post = posts(:thinking)
- new_tag = Tag.new(:name => "new")
+ new_tag = Tag.new(name: "new")
saved_post.tags << new_tag
- assert new_tag.persisted? #consistent with habtm!
- assert saved_post.persisted?
- assert saved_post.tags.include?(new_tag)
-
- assert new_tag.persisted?
- assert saved_post.reload.tags(true).include?(new_tag)
+ assert new_tag.persisted? # consistent with habtm!
+ assert_predicate saved_post, :persisted?
+ assert_includes saved_post.tags, new_tag
+ assert_predicate new_tag, :persisted?
+ assert_includes saved_post.reload.tags.reload, new_tag
- new_post = Post.new(:title => "Association replacement works!", :body => "You best believe it.")
+ new_post = Post.new(title: "Association replacement works!", body: "You best believe it.")
saved_tag = tags(:general)
new_post.tags << saved_tag
- assert !new_post.persisted?
- assert saved_tag.persisted?
- assert new_post.tags.include?(saved_tag)
+ assert_not_predicate new_post, :persisted?
+ assert_predicate saved_tag, :persisted?
+ assert_includes new_post.tags, saved_tag
new_post.save!
- assert new_post.persisted?
- assert new_post.reload.tags(true).include?(saved_tag)
+ assert_predicate new_post, :persisted?
+ assert_includes new_post.reload.tags.reload, saved_tag
- assert !posts(:thinking).tags.build.persisted?
- assert !posts(:thinking).tags.new.persisted?
+ assert_not_predicate posts(:thinking).tags.build, :persisted?
+ assert_not_predicate posts(:thinking).tags.new, :persisted?
end
def test_create_associate_when_adding_to_has_many_through
count = posts(:thinking).tags.count
- push = Tag.create!(:name => 'pushme')
+ push = Tag.create!(name: "pushme")
post_thinking = posts(:thinking)
assert_nothing_raised { post_thinking.tags << push }
- assert_nil( wrong = post_thinking.tags.detect { |t| t.class != Tag },
- message = "Expected a Tag in tags collection, got #{wrong.class}.")
- assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging },
- message = "Expected a Tagging in taggings collection, got #{wrong.class}.")
+ assert_nil(wrong = post_thinking.tags.detect { |t| t.class != Tag },
+ "Expected a Tag in tags collection, got #{wrong.class}.")
+ assert_nil(wrong = post_thinking.taggings.detect { |t| t.class != Tagging },
+ "Expected a Tagging in taggings collection, got #{wrong.class}.")
assert_equal(count + 1, post_thinking.reload.tags.size)
- assert_equal(count + 1, post_thinking.tags(true).size)
+ assert_equal(count + 1, post_thinking.tags.reload.size)
- assert_kind_of Tag, post_thinking.tags.create!(:name => 'foo')
- assert_nil( wrong = post_thinking.tags.detect { |t| t.class != Tag },
- message = "Expected a Tag in tags collection, got #{wrong.class}.")
- assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging },
- message = "Expected a Tagging in taggings collection, got #{wrong.class}.")
+ assert_kind_of Tag, post_thinking.tags.create!(name: "foo")
+ assert_nil(wrong = post_thinking.tags.detect { |t| t.class != Tag },
+ "Expected a Tag in tags collection, got #{wrong.class}.")
+ assert_nil(wrong = post_thinking.taggings.detect { |t| t.class != Tagging },
+ "Expected a Tagging in taggings collection, got #{wrong.class}.")
assert_equal(count + 2, post_thinking.reload.tags.size)
- assert_equal(count + 2, post_thinking.tags(true).size)
+ assert_equal(count + 2, post_thinking.tags.reload.size)
- assert_nothing_raised { post_thinking.tags.concat(Tag.create!(:name => 'abc'), Tag.create!(:name => 'def')) }
- assert_nil( wrong = post_thinking.tags.detect { |t| t.class != Tag },
- message = "Expected a Tag in tags collection, got #{wrong.class}.")
- assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging },
- message = "Expected a Tagging in taggings collection, got #{wrong.class}.")
+ assert_nothing_raised { post_thinking.tags.concat(Tag.create!(name: "abc"), Tag.create!(name: "def")) }
+ assert_nil(wrong = post_thinking.tags.detect { |t| t.class != Tag },
+ "Expected a Tag in tags collection, got #{wrong.class}.")
+ assert_nil(wrong = post_thinking.taggings.detect { |t| t.class != Tagging },
+ "Expected a Tagging in taggings collection, got #{wrong.class}.")
assert_equal(count + 4, post_thinking.reload.tags.size)
- assert_equal(count + 4, post_thinking.tags(true).size)
+ assert_equal(count + 4, post_thinking.tags.reload.size)
# Raises if the wrong reflection name is used to set the Edge belongs_to
assert_nothing_raised { vertices(:vertex_1).sinks << vertices(:vertex_5) }
@@ -519,14 +529,14 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_has_many_through_collection_size_doesnt_load_target_if_not_loaded
author = authors(:david)
assert_equal 10, author.comments.size
- assert !author.comments.loaded?
+ assert_not_predicate author.comments, :loaded?
end
def test_has_many_through_collection_size_uses_counter_cache_if_it_exists
c = categories(:general)
c.categorizations_count = 100
assert_equal 100, c.categorizations.size
- assert !c.categorizations.loaded?
+ assert_not_predicate c.categorizations, :loaded?
end
def test_adding_junk_to_has_many_through_should_raise_type_mismatch
@@ -541,47 +551,47 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_delete_associate_when_deleting_from_has_many_through_with_nonstandard_id
count = books(:awdr).references.count
references_before = books(:awdr).references
- book = Book.create!(:name => 'Getting Real')
+ book = Book.create!(name: "Getting Real")
book_awdr = books(:awdr)
book_awdr.references << book
- assert_equal(count + 1, book_awdr.references(true).size)
+ assert_equal(count + 1, book_awdr.references.reload.size)
assert_nothing_raised { book_awdr.references.delete(book) }
assert_equal(count, book_awdr.references.size)
- assert_equal(count, book_awdr.references(true).size)
+ assert_equal(count, book_awdr.references.reload.size)
assert_equal(references_before.sort, book_awdr.references.sort)
end
def test_delete_associate_when_deleting_from_has_many_through
count = posts(:thinking).tags.count
tags_before = posts(:thinking).tags.sort
- tag = Tag.create!(:name => 'doomed')
+ tag = Tag.create!(name: "doomed")
post_thinking = posts(:thinking)
post_thinking.tags << tag
- assert_equal(count + 1, post_thinking.taggings(true).size)
- assert_equal(count + 1, post_thinking.reload.tags(true).size)
+ assert_equal(count + 1, post_thinking.taggings.reload.size)
+ assert_equal(count + 1, post_thinking.reload.tags.reload.size)
assert_not_equal(tags_before, post_thinking.tags.sort)
assert_nothing_raised { post_thinking.tags.delete(tag) }
assert_equal(count, post_thinking.tags.size)
- assert_equal(count, post_thinking.tags(true).size)
- assert_equal(count, post_thinking.taggings(true).size)
+ assert_equal(count, post_thinking.tags.reload.size)
+ assert_equal(count, post_thinking.taggings.reload.size)
assert_equal(tags_before, post_thinking.tags.sort)
end
def test_delete_associate_when_deleting_from_has_many_through_with_multiple_tags
count = posts(:thinking).tags.count
tags_before = posts(:thinking).tags.sort
- doomed = Tag.create!(:name => 'doomed')
- doomed2 = Tag.create!(:name => 'doomed2')
- quaked = Tag.create!(:name => 'quaked')
+ doomed = Tag.create!(name: "doomed")
+ doomed2 = Tag.create!(name: "doomed2")
+ quaked = Tag.create!(name: "quaked")
post_thinking = posts(:thinking)
post_thinking.tags << doomed << doomed2
- assert_equal(count + 2, post_thinking.reload.tags(true).size)
+ assert_equal(count + 2, post_thinking.reload.tags.reload.size)
assert_nothing_raised { post_thinking.tags.delete(doomed, doomed2, quaked) }
assert_equal(count, post_thinking.tags.size)
- assert_equal(count, post_thinking.tags(true).size)
+ assert_equal(count, post_thinking.tags.reload.size)
assert_equal(tags_before, post_thinking.tags.sort)
end
@@ -589,10 +599,10 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::AssociationTypeMismatch) { posts(:thinking).tags.delete(Object.new) }
end
- def test_deleting_by_fixnum_id_from_has_many_through
+ def test_deleting_by_integer_id_from_has_many_through
post = posts(:thinking)
- assert_difference 'post.tags.count', -1 do
+ assert_difference "post.tags.count", -1 do
assert_equal 1, post.tags.delete(1).size
end
@@ -602,8 +612,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_deleting_by_string_id_from_has_many_through
post = posts(:thinking)
- assert_difference 'post.tags.count', -1 do
- assert_equal 1, post.tags.delete('1').size
+ assert_difference "post.tags.count", -1 do
+ assert_equal 1, post.tags.delete("1").size
end
assert_equal 0, post.tags.size
@@ -633,26 +643,26 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_polymorphic_has_many
expected = taggings(:welcome_general)
- p = Post.all.merge!(:includes => :taggings).find(posts(:welcome).id)
- assert_no_queries {assert p.taggings.include?(expected)}
- assert posts(:welcome).taggings.include?(taggings(:welcome_general))
+ p = Post.all.merge!(includes: :taggings).find(posts(:welcome).id)
+ assert_no_queries { assert_includes p.taggings, expected }
+ assert_includes posts(:welcome).taggings, taggings(:welcome_general)
end
def test_polymorphic_has_one
expected = posts(:welcome)
- tagging = Tagging.all.merge!(:includes => :taggable).find(taggings(:welcome_general).id)
- assert_no_queries { assert_equal expected, tagging.taggable}
+ tagging = Tagging.all.merge!(includes: :taggable).find(taggings(:welcome_general).id)
+ assert_no_queries { assert_equal expected, tagging.taggable }
end
def test_polymorphic_belongs_to
- p = Post.all.merge!(:includes => {:taggings => :taggable}).find(posts(:welcome).id)
- assert_no_queries {assert_equal posts(:welcome), p.taggings.first.taggable}
+ p = Post.all.merge!(includes: { taggings: :taggable }).find(posts(:welcome).id)
+ assert_no_queries { assert_equal posts(:welcome), p.taggings.first.taggable }
end
def test_preload_polymorphic_has_many_through
- posts = Post.all.merge!(:order => 'posts.id').to_a
- posts_with_tags = Post.all.merge!(:includes => :tags, :order => 'posts.id').to_a
+ posts = Post.all.merge!(order: "posts.id").to_a
+ posts_with_tags = Post.all.merge!(includes: :tags, order: "posts.id").to_a
assert_equal posts.length, posts_with_tags.length
posts.length.times do |i|
assert_equal posts[i].tags.length, assert_no_queries { posts_with_tags[i].tags.length }
@@ -660,26 +670,26 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
end
def test_preload_polymorph_many_types
- taggings = Tagging.all.merge!(:includes => :taggable, :where => ['taggable_type != ?', 'FakeModel']).to_a
+ taggings = Tagging.all.merge!(includes: :taggable, where: ["taggable_type != ?", "FakeModel"]).to_a
assert_no_queries do
taggings.first.taggable.id
taggings[1].taggable.id
end
taggables = taggings.map(&:taggable)
- assert taggables.include?(items(:dvd))
- assert taggables.include?(posts(:welcome))
+ assert_includes taggables, items(:dvd)
+ assert_includes taggables, posts(:welcome)
end
def test_preload_nil_polymorphic_belongs_to
assert_nothing_raised do
- Tagging.all.merge!(:includes => :taggable, :where => ['taggable_type IS NULL']).to_a
+ Tagging.all.merge!(includes: :taggable, where: ["taggable_type IS NULL"]).to_a
end
end
def test_preload_polymorphic_has_many
- posts = Post.all.merge!(:order => 'posts.id').to_a
- posts_with_taggings = Post.all.merge!(:includes => :taggings, :order => 'posts.id').to_a
+ posts = Post.all.merge!(order: "posts.id").to_a
+ posts_with_taggings = Post.all.merge!(includes: :taggings, order: "posts.id").to_a
assert_equal posts.length, posts_with_taggings.length
posts.length.times do |i|
assert_equal posts[i].taggings.length, assert_no_queries { posts_with_taggings[i].taggings.length }
@@ -687,7 +697,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
end
def test_belongs_to_shared_parent
- comments = Comment.all.merge!(:includes => :post, :where => 'post_id = 1').to_a
+ comments = Comment.all.merge!(includes: :post, where: "post_id = 1").to_a
assert_no_queries do
assert_equal comments.first.post, comments[1].post
end
@@ -700,8 +710,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
category = david.categories.first
assert_no_queries do
- assert david.categories.loaded?
- assert david.categories.include?(category)
+ assert_predicate david.categories, :loaded?
+ assert_includes david.categories, category
end
end
@@ -710,42 +720,59 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
category = david.categories.first
david.reload
- assert ! david.categories.loaded?
+ assert_not_predicate david.categories, :loaded?
assert_queries(1) do
- assert david.categories.include?(category)
+ assert_includes david.categories, category
end
- assert ! david.categories.loaded?
+ assert_not_predicate david.categories, :loaded?
end
def test_has_many_through_include_returns_false_for_non_matching_record_to_verify_scoping
david = authors(:david)
- category = Category.create!(:name => 'Not Associated')
+ category = Category.create!(name: "Not Associated")
- assert ! david.categories.loaded?
- assert ! david.categories.include?(category)
+ assert_not_predicate david.categories, :loaded?
+ assert_not david.categories.include?(category)
end
def test_has_many_through_goes_through_all_sti_classes
- sub_sti_post = SubStiPost.create!(:title => 'test', :body => 'test', :author_id => 1)
- new_comment = sub_sti_post.comments.create(:body => 'test')
+ sub_sti_post = SubStiPost.create!(title: "test", body: "test", author_id: 1)
+ new_comment = sub_sti_post.comments.create(body: "test")
assert_equal [9, 10, new_comment.id], authors(:david).sti_post_comments.map(&:id).sort
end
def test_has_many_with_pluralize_table_names_false
- aircraft = Aircraft.create!(:name => "Airbus 380")
- engine = Engine.create!(:car_id => aircraft.id)
+ aircraft = Aircraft.create!(name: "Airbus 380")
+ engine = Engine.create!(car_id: aircraft.id)
assert_equal aircraft.engines, [engine]
end
+ def test_proper_error_message_for_eager_load_and_includes_association_errors
+ includes_error = assert_raises(ActiveRecord::ConfigurationError) {
+ Post.includes(:nonexistent_relation).where(nonexistent_relation: { name: "Rochester" }).find(1)
+ }
+ assert_equal("Can't join 'Post' to association named 'nonexistent_relation'; perhaps you misspelled it?", includes_error.message)
+
+ eager_load_error = assert_raises(ActiveRecord::ConfigurationError) {
+ Post.eager_load(:nonexistent_relation).where(nonexistent_relation: { name: "Rochester" }).find(1)
+ }
+ assert_equal("Can't join 'Post' to association named 'nonexistent_relation'; perhaps you misspelled it?", eager_load_error.message)
+
+ includes_and_eager_load_error = assert_raises(ActiveRecord::ConfigurationError) {
+ Post.eager_load(:nonexistent_relation).includes(:nonexistent_relation).where(nonexistent_relation: { name: "Rochester" }).find(1)
+ }
+ assert_equal("Can't join 'Post' to association named 'nonexistent_relation'; perhaps you misspelled it?", includes_and_eager_load_error.message)
+ end
+
private
# create dynamic Post models to allow different dependency options
def find_post_with_dependency(post_id, association, association_name, dependency)
class_name = "PostWith#{association.to_s.classify}#{dependency.to_s.classify}"
Post.find(post_id).update_columns type: class_name
klass = Object.const_set(class_name, Class.new(ActiveRecord::Base))
- klass.table_name = 'posts'
- klass.send(association, association_name, :as => :taggable, :dependent => dependency)
+ klass.table_name = "posts"
+ klass.send(association, association_name, as: :taggable, dependent: dependency)
klass.find(post_id)
end
end
diff --git a/activerecord/test/cases/associations/left_outer_join_association_test.rb b/activerecord/test/cases/associations/left_outer_join_association_test.rb
new file mode 100644
index 0000000000..0e54e8c1b0
--- /dev/null
+++ b/activerecord/test/cases/associations/left_outer_join_association_test.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/post"
+require "models/comment"
+require "models/author"
+require "models/essay"
+require "models/category"
+require "models/categorization"
+require "models/person"
+
+class LeftOuterJoinAssociationTest < ActiveRecord::TestCase
+ fixtures :authors, :author_addresses, :essays, :posts, :comments, :categorizations, :people
+
+ def test_construct_finder_sql_applies_aliases_tables_on_association_conditions
+ result = Author.left_outer_joins(:thinking_posts, :welcome_posts).to_a
+ assert_equal authors(:david), result.first
+ end
+
+ def test_construct_finder_sql_does_not_table_name_collide_on_duplicate_associations
+ assert_nothing_raised do
+ queries = capture_sql do
+ Person.left_outer_joins(agents: { agents: :agents })
+ .left_outer_joins(agents: { agents: { primary_contact: :agents } }).to_a
+ end
+ assert queries.any? { |sql| /agents_people_4/i.match?(sql) }
+ end
+ end
+
+ def test_left_outer_joins_count_is_same_as_size_of_loaded_results
+ assert_equal 17, Post.left_outer_joins(:comments).to_a.size
+ assert_equal 17, Post.left_outer_joins(:comments).count
+ end
+
+ def test_left_joins_aliases_left_outer_joins
+ assert_equal Post.left_outer_joins(:comments).to_sql, Post.left_joins(:comments).to_sql
+ end
+
+ def test_left_outer_joins_return_has_value_for_every_comment
+ all_post_ids = Post.pluck(:id)
+ assert_equal all_post_ids, all_post_ids & Post.left_outer_joins(:comments).pluck(:id)
+ end
+
+ def test_left_outer_joins_actually_does_a_left_outer_join
+ queries = capture_sql { Author.left_outer_joins(:posts).to_a }
+ assert queries.any? { |sql| /LEFT OUTER JOIN/i.match?(sql) }
+ end
+
+ def test_construct_finder_sql_ignores_empty_left_outer_joins_hash
+ queries = capture_sql { Author.left_outer_joins({}).to_a }
+ assert queries.none? { |sql| /LEFT OUTER JOIN/i.match?(sql) }
+ end
+
+ def test_construct_finder_sql_ignores_empty_left_outer_joins_array
+ queries = capture_sql { Author.left_outer_joins([]).to_a }
+ assert queries.none? { |sql| /LEFT OUTER JOIN/i.match?(sql) }
+ end
+
+ def test_left_outer_joins_forbids_to_use_string_as_argument
+ assert_raise(ArgumentError) { Author.left_outer_joins('LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"').to_a }
+ end
+
+ def test_join_conditions_added_to_join_clause
+ queries = capture_sql { Author.left_outer_joins(:essays).to_a }
+ assert queries.any? { |sql| /writer_type.*?=.*?(Author|\?|\$1|\:a1)/i.match?(sql) }
+ assert queries.none? { |sql| /WHERE/i.match?(sql) }
+ end
+
+ def test_find_with_sti_join
+ scope = Post.left_outer_joins(:special_comments).where(id: posts(:sti_comments).id)
+
+ # The join should match SpecialComment and its subclasses only
+ assert_empty scope.where("comments.type" => "Comment")
+ assert_not_empty scope.where("comments.type" => "SpecialComment")
+ assert_not_empty scope.where("comments.type" => "SubSpecialComment")
+ end
+
+ def test_does_not_override_select
+ authors = Author.select("authors.name, #{%{(authors.author_address_id || ' ' || authors.author_address_extra_id) as addr_id}}").left_outer_joins(:posts)
+ assert_predicate authors, :any?
+ assert_respond_to authors.first, :addr_id
+ end
+
+ test "the default scope of the target is applied when joining associations" do
+ author = Author.create! name: "Jon"
+ author.categorizations.create!
+ author.categorizations.create! special: true
+
+ assert_equal [author], Author.where(id: author).left_outer_joins(:special_categorizations)
+ end
+end
diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb
index 31b68c940e..5821744530 100644
--- a/activerecord/test/cases/associations/nested_through_associations_test.rb
+++ b/activerecord/test/cases/associations/nested_through_associations_test.rb
@@ -1,30 +1,37 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/author'
-require 'models/post'
-require 'models/person'
-require 'models/reference'
-require 'models/job'
-require 'models/reader'
-require 'models/comment'
-require 'models/tag'
-require 'models/tagging'
-require 'models/subscriber'
-require 'models/book'
-require 'models/subscription'
-require 'models/rating'
-require 'models/member'
-require 'models/member_detail'
-require 'models/member_type'
-require 'models/sponsor'
-require 'models/club'
-require 'models/organization'
-require 'models/category'
-require 'models/categorization'
-require 'models/membership'
-require 'models/essay'
+require "models/author"
+require "models/post"
+require "models/person"
+require "models/reference"
+require "models/job"
+require "models/reader"
+require "models/comment"
+require "models/tag"
+require "models/tagging"
+require "models/subscriber"
+require "models/book"
+require "models/subscription"
+require "models/rating"
+require "models/member"
+require "models/member_detail"
+require "models/member_type"
+require "models/sponsor"
+require "models/club"
+require "models/organization"
+require "models/category"
+require "models/categorization"
+require "models/membership"
+require "models/essay"
+require "models/hotel"
+require "models/department"
+require "models/chef"
+require "models/cake_designer"
+require "models/drink_designer"
class NestedThroughAssociationsTest < ActiveRecord::TestCase
- fixtures :authors, :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
@@ -65,13 +72,13 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
def test_has_many_through_has_many_with_has_many_through_source_reflection_preload_via_joins
assert_includes_and_joins_equal(
- Author.where('tags.id' => tags(:general).id),
+ Author.where("tags.id" => tags(:general).id),
[authors(:david)], :tags
)
# This ensures that the polymorphism of taggings is being observed correctly
- authors = Author.joins(:tags).where('taggings.taggable_type' => 'FakeModel')
- assert authors.empty?
+ authors = Author.joins(:tags).where("taggings.taggable_type" => "FakeModel")
+ assert_empty authors
end
# has_many through
@@ -79,7 +86,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
# Through: has_many through
def test_has_many_through_has_many_through_with_has_many_source_reflection
luke, david = subscribers(:first), subscribers(:second)
- assert_equal [luke, david, david], authors(:david).subscribers.order('subscribers.nick')
+ assert_equal [luke, david, david], authors(:david).subscribers.order("subscribers.nick")
end
def test_has_many_through_has_many_through_with_has_many_source_reflection_preload
@@ -93,7 +100,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
def test_has_many_through_has_many_through_with_has_many_source_reflection_preload_via_joins
# All authors with subscribers where one of the subscribers' nick is 'alterself'
assert_includes_and_joins_equal(
- Author.where('subscribers.nick' => 'alterself'),
+ Author.where("subscribers.nick" => "alterself"),
[authors(:david)], :subscribers
)
end
@@ -115,7 +122,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
def test_has_many_through_has_one_with_has_one_through_source_reflection_preload_via_joins
assert_includes_and_joins_equal(
- Member.where('member_types.id' => member_types(:founding).id),
+ Member.where("member_types.id" => member_types(:founding).id),
[members(:groucho)], :nested_member_types
)
end
@@ -130,14 +137,14 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
def test_has_many_through_has_one_through_with_has_one_source_reflection_preload
members = assert_queries(4) { Member.includes(:nested_sponsors).to_a }
mustache = sponsors(:moustache_club_sponsor_for_groucho)
- assert_no_queries(ignore_none: false) do
+ assert_no_queries do
assert_equal [mustache], members.first.nested_sponsors
end
end
def test_has_many_through_has_one_through_with_has_one_source_reflection_preload_via_joins
assert_includes_and_joins_equal(
- Member.where('sponsors.id' => sponsors(:moustache_club_sponsor_for_groucho).id),
+ Member.where("sponsors.id" => sponsors(:moustache_club_sponsor_for_groucho).id),
[members(:groucho)], :nested_sponsors
)
end
@@ -149,7 +156,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
groucho_details, other_details = member_details(:groucho), member_details(:some_other_guy)
assert_equal [groucho_details, other_details],
- members(:groucho).organization_member_details.order('member_details.id')
+ members(:groucho).organization_member_details.order("member_details.id")
end
def test_has_many_through_has_one_with_has_many_through_source_reflection_preload
@@ -164,13 +171,13 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
def test_has_many_through_has_one_with_has_many_through_source_reflection_preload_via_joins
assert_includes_and_joins_equal(
- Member.where('member_details.id' => member_details(:groucho).id).order('member_details.id'),
+ Member.where("member_details.id" => member_details(:groucho).id).order("member_details.id"),
[members(:groucho), members(:some_other_guy)], :organization_member_details
)
members = Member.joins(:organization_member_details).
- where('member_details.id' => 9)
- assert members.empty?
+ where("member_details.id" => 9)
+ assert_empty members
end
# has_many through
@@ -180,7 +187,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
groucho_details, other_details = member_details(:groucho), member_details(:some_other_guy)
assert_equal [groucho_details, other_details],
- members(:groucho).organization_member_details_2.order('member_details.id')
+ members(:groucho).organization_member_details_2.order("member_details.id")
end
def test_has_many_through_has_one_through_with_has_many_source_reflection_preload
@@ -189,20 +196,20 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
# postgresql test if randomly executed then executes "SHOW max_identifier_length". Hence
# the need to ignore certain predefined sqls that deal with system calls.
- assert_no_queries(ignore_none: false) do
+ assert_no_queries do
assert_equal [groucho_details, other_details], members.first.organization_member_details_2.sort_by(&:id)
end
end
def test_has_many_through_has_one_through_with_has_many_source_reflection_preload_via_joins
assert_includes_and_joins_equal(
- Member.where('member_details.id' => member_details(:groucho).id).order('member_details.id'),
+ Member.where("member_details.id" => member_details(:groucho).id).order("member_details.id"),
[members(:groucho), members(:some_other_guy)], :organization_member_details_2
)
members = Member.joins(:organization_member_details_2).
- where('member_details.id' => 9)
- assert members.empty?
+ where("member_details.id" => 9)
+ assert_empty members
end
# has_many through
@@ -211,7 +218,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
def test_has_many_through_has_many_with_has_and_belongs_to_many_source_reflection
general, cooking = categories(:general), categories(:cooking)
- assert_equal [general, cooking], authors(:bob).post_categories.order('categories.id')
+ assert_equal [general, cooking], authors(:bob).post_categories.order("categories.id")
end
def test_has_many_through_has_many_with_has_and_belongs_to_many_source_reflection_preload
@@ -228,7 +235,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
Author.joins(:post_categories).first
assert_includes_and_joins_equal(
- Author.where('categories.id' => categories(:cooking).id),
+ Author.where("categories.id" => categories(:cooking).id),
[authors(:bob)], :post_categories
)
end
@@ -239,7 +246,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
def test_has_many_through_has_and_belongs_to_many_with_has_many_source_reflection
greetings, more = comments(:greetings), comments(:more_greetings)
- assert_equal [greetings, more], categories(:technology).post_comments.order('comments.id')
+ assert_equal [greetings, more], categories(:technology).post_comments.order("comments.id")
end
def test_has_many_through_has_and_belongs_to_many_with_has_many_source_reflection_preload
@@ -257,7 +264,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
Category.joins(:post_comments).first
assert_includes_and_joins_equal(
- Category.where('comments.id' => comments(:more_greetings).id).order('categories.id'),
+ Category.where("comments.id" => comments(:more_greetings).id).order("categories.id"),
[categories(:general), categories(:technology)], :post_comments
)
end
@@ -268,7 +275,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
def test_has_many_through_has_many_with_has_many_through_habtm_source_reflection
greetings, more = comments(:greetings), comments(:more_greetings)
- assert_equal [greetings, more], authors(:bob).category_post_comments.order('comments.id')
+ assert_equal [greetings, more], authors(:bob).category_post_comments.order("comments.id")
end
def test_has_many_through_has_many_with_has_many_through_habtm_source_reflection_preload
@@ -285,7 +292,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
Author.joins(:category_post_comments).first
assert_includes_and_joins_equal(
- Author.where('comments.id' => comments(:does_it_hurt).id).order('authors.id'),
+ Author.where("comments.id" => comments(:does_it_hurt).id).order("authors.id"),
[authors(:david), authors(:mary)], :category_post_comments
)
end
@@ -308,7 +315,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
def test_has_many_through_has_many_through_with_belongs_to_source_reflection_preload_via_joins
assert_includes_and_joins_equal(
- Author.where('tags.id' => tags(:general).id),
+ Author.where("tags.id" => tags(:general).id),
[authors(:david)], :tagging_tags
)
end
@@ -320,7 +327,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
welcome_general, thinking_general = taggings(:welcome_general), taggings(:thinking_general)
assert_equal [welcome_general, thinking_general],
- categorizations(:david_welcome_general).post_taggings.order('taggings.id')
+ categorizations(:david_welcome_general).post_taggings.order("taggings.id")
end
def test_has_many_through_belongs_to_with_has_many_through_source_reflection_preload
@@ -334,7 +341,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
def test_has_many_through_belongs_to_with_has_many_through_source_reflection_preload_via_joins
assert_includes_and_joins_equal(
- Categorization.where('taggings.id' => taggings(:welcome_general).id).order('taggings.id'),
+ Categorization.where("taggings.id" => taggings(:welcome_general).id).order("taggings.id"),
[categorizations(:david_welcome_general)], :post_taggings
)
end
@@ -357,7 +364,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
def test_has_one_through_has_one_with_has_one_through_source_reflection_preload_via_joins
assert_includes_and_joins_equal(
- Member.where('member_types.id' => member_types(:founding).id),
+ Member.where("member_types.id" => member_types(:founding).id),
[members(:groucho)], :nested_member_type
)
end
@@ -391,7 +398,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
def test_has_one_through_has_one_through_with_belongs_to_source_reflection_preload_via_joins
assert_includes_and_joins_equal(
- Member.where('categories.id' => categories(:technology).id),
+ Member.where("categories.id" => categories(:technology).id),
[members(:blarpy_winkup)], :club_category
)
end
@@ -404,7 +411,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
def test_distinct_has_many_through_a_has_many_through_association_on_through_reflection
author = authors(:david)
assert_equal [subscribers(:first), subscribers(:second)],
- author.distinct_subscribers.order('subscribers.nick')
+ author.distinct_subscribers.order("subscribers.nick")
end
def test_nested_has_many_through_with_a_table_referenced_multiple_times
@@ -413,26 +420,31 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
author.similar_posts.sort_by(&:id)
# Mary and Bob both have posts in misc, but they are the only ones.
- authors = Author.joins(:similar_posts).where('posts.id' => posts(:misc_by_bob).id)
+ authors = Author.joins(:similar_posts).where("posts.id" => posts(:misc_by_bob).id)
assert_equal [authors(:mary), authors(:bob)], authors.distinct.sort_by(&:id)
# Check the polymorphism of taggings is being observed correctly (in both joins)
- authors = Author.joins(:similar_posts).where('taggings.taggable_type' => 'FakeModel')
- assert authors.empty?
- authors = Author.joins(:similar_posts).where('taggings_authors_join.taggable_type' => 'FakeModel')
- assert authors.empty?
+ authors = Author.joins(:similar_posts).where("taggings.taggable_type" => "FakeModel")
+ assert_empty authors
+ authors = Author.joins(:similar_posts).where("taggings_authors_join.taggable_type" => "FakeModel")
+ assert_empty authors
+ end
+
+ def test_nested_has_many_through_with_scope_on_polymorphic_reflection
+ authors = Author.joins(:ordered_posts).where("posts.id" => posts(:misc_by_bob).id)
+ assert_equal [authors(:mary), authors(:bob)], authors.distinct.sort_by(&:id)
end
def test_has_many_through_with_foreign_key_option_on_through_reflection
- assert_equal [posts(:welcome), posts(:authorless)], people(:david).agents_posts.order('posts.id')
+ assert_equal [posts(:welcome), posts(:authorless)], people(:david).agents_posts.order("posts.id")
assert_equal [authors(:david)], references(:david_unicyclist).agents_posts_authors
- references = Reference.joins(:agents_posts_authors).where('authors.id' => authors(:david).id)
+ references = Reference.joins(:agents_posts_authors).where("authors.id" => authors(:david).id)
assert_equal [references(:david_unicyclist)], references
end
def test_has_many_through_with_foreign_key_option_on_source_reflection
- assert_equal [people(:michael), people(:susan)], jobs(:unicyclist).agents.order('people.id')
+ assert_equal [people(:michael), people(:susan)], jobs(:unicyclist).agents.order("people.id")
jobs = Job.joins(:agents)
assert_equal [jobs(:unicyclist), jobs(:unicyclist)], jobs
@@ -443,19 +455,19 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
assert_equal [ratings(:special_comment_rating), ratings(:sub_special_comment_rating)], ratings
# Ensure STI is respected in the join
- scope = Post.joins(:special_comments_ratings).where(:id => posts(:sti_comments).id)
- assert scope.where("comments.type" => "Comment").empty?
- assert !scope.where("comments.type" => "SpecialComment").empty?
- assert !scope.where("comments.type" => "SubSpecialComment").empty?
+ scope = Post.joins(:special_comments_ratings).where(id: posts(:sti_comments).id)
+ assert_empty scope.where("comments.type" => "Comment")
+ assert_not_empty scope.where("comments.type" => "SpecialComment")
+ assert_not_empty scope.where("comments.type" => "SubSpecialComment")
end
def test_has_many_through_with_sti_on_nested_through_reflection
taggings = posts(:sti_comments).special_comments_ratings_taggings
assert_equal [taggings(:special_comment_rating)], taggings
- scope = Post.joins(:special_comments_ratings_taggings).where(:id => posts(:sti_comments).id)
- assert scope.where("comments.type" => "Comment").empty?
- assert !scope.where("comments.type" => "SpecialComment").empty?
+ scope = Post.joins(:special_comments_ratings_taggings).where(id: posts(:sti_comments).id)
+ assert_empty scope.where("comments.type" => "Comment")
+ assert_not_empty scope.where("comments.type" => "SpecialComment")
end
def test_nested_has_many_through_writers_should_raise_error
@@ -495,7 +507,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
groucho = members(:groucho)
founding = member_types(:founding)
- assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do
+ assert_raises(ActiveRecord::HasOneThroughNestedAssociationsAreReadonly) do
groucho.nested_member_type = founding
end
end
@@ -505,7 +517,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
end
def test_nested_has_many_through_with_conditions_on_through_associations_preload
- assert Author.where('tags.id' => 100).joins(:misc_post_first_blue_tags).empty?
+ assert_empty Author.where("tags.id" => 100).joins(:misc_post_first_blue_tags)
authors = assert_queries(3) { Author.includes(:misc_post_first_blue_tags).to_a.sort_by(&:id) }
blue = tags(:blue)
@@ -518,7 +530,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
def test_nested_has_many_through_with_conditions_on_through_associations_preload_via_joins
# Pointless condition to force single-query loading
assert_includes_and_joins_equal(
- Author.where('tags.id = tags.id').references(:tags),
+ Author.where("tags.id = tags.id").references(:tags),
[authors(:bob)], :misc_post_first_blue_tags
)
end
@@ -539,7 +551,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
def test_nested_has_many_through_with_conditions_on_source_associations_preload_via_joins
# Pointless condition to force single-query loading
assert_includes_and_joins_equal(
- Author.where('tags.id = tags.id').references(:tags),
+ Author.where("tags.id = tags.id").references(:tags),
[authors(:bob)], :misc_post_first_blue_tags_2
)
end
@@ -548,13 +560,13 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
assert_equal [categories(:general)], organizations(:nsa).author_essay_categories
organizations = Organization.joins(:author_essay_categories).
- where('categories.id' => categories(:general).id)
+ where("categories.id" => categories(:general).id)
assert_equal [organizations(:nsa)], organizations
assert_equal categories(:general), organizations(:nsa).author_owned_essay_category
organizations = Organization.joins(:author_owned_essay_category).
- where('categories.id' => categories(:general).id)
+ where("categories.id" => categories(:general).id)
assert_equal [organizations(:nsa)], organizations
end
@@ -562,9 +574,40 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
c = Categorization.new
c.author = authors(:david)
c.post_taggings.to_a
- assert !c.post_taggings.empty?
+ assert_not_empty c.post_taggings
c.save
- assert !c.post_taggings.empty?
+ assert_not_empty c.post_taggings
+ end
+
+ def test_polymorphic_has_many_through_when_through_association_has_not_loaded
+ cake_designer = CakeDesigner.create!(chef: Chef.new)
+ drink_designer = DrinkDesigner.create!(chef: Chef.new)
+ department = Department.create!(chefs: [cake_designer.chef, drink_designer.chef])
+ Hotel.create!(departments: [department])
+ hotel = Hotel.includes(:cake_designers, :drink_designers).take
+
+ assert_equal [cake_designer], hotel.cake_designers
+ assert_equal [drink_designer], hotel.drink_designers
+ end
+
+ def test_polymorphic_has_many_through_when_through_association_has_already_loaded
+ cake_designer = CakeDesigner.create!(chef: Chef.new)
+ drink_designer = DrinkDesigner.create!(chef: Chef.new)
+ department = Department.create!(chefs: [cake_designer.chef, drink_designer.chef])
+ Hotel.create!(departments: [department])
+ hotel = Hotel.includes(:chefs, :cake_designers, :drink_designers).take
+
+ assert_equal [cake_designer], hotel.cake_designers
+ assert_equal [drink_designer], hotel.drink_designers
+ end
+
+ def test_polymorphic_has_many_through_joined_different_table_twice
+ cake_designer = CakeDesigner.create!(chef: Chef.new)
+ drink_designer = DrinkDesigner.create!(chef: Chef.new)
+ department = Department.create!(chefs: [cake_designer.chef, drink_designer.chef])
+ hotel = Hotel.create!(departments: [department])
+
+ assert_equal hotel, Hotel.joins(:cake_designers, :drink_designers).take
end
private
diff --git a/activerecord/test/cases/associations/required_test.rb b/activerecord/test/cases/associations/required_test.rb
index 3e5494e897..65a3bb5efe 100644
--- a/activerecord/test/cases/associations/required_test.rb
+++ b/activerecord/test/cases/associations/required_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
class RequiredAssociationsTest < ActiveRecord::TestCase
@@ -18,18 +20,25 @@ class RequiredAssociationsTest < ActiveRecord::TestCase
end
teardown do
- @connection.drop_table 'parents', if_exists: true
- @connection.drop_table 'children', if_exists: true
+ @connection.drop_table "parents", if_exists: true
+ @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
- assert model.new.save
- assert model.new(parent: Parent.new).save
+ 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
+ ensure
+ ActiveRecord::Base.belongs_to_required_by_default = original_value
+ end
end
test "required belongs_to associations have presence validated" do
@@ -46,6 +55,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,
@@ -92,11 +122,11 @@ class RequiredAssociationsTest < ActiveRecord::TestCase
private
- def subclass_of(klass, &block)
- subclass = Class.new(klass, &block)
- def subclass.name
- superclass.name
+ def subclass_of(klass, &block)
+ subclass = Class.new(klass, &block)
+ def subclass.name
+ superclass.name
+ end
+ subclass
end
- subclass
- end
end
diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb
index 3d202a5527..081da95df7 100644
--- a/activerecord/test/cases/associations_test.rb
+++ b/activerecord/test/cases/associations_test.rb
@@ -1,89 +1,86 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/computer'
-require 'models/developer'
-require 'models/computer'
-require 'models/project'
-require 'models/company'
-require 'models/categorization'
-require 'models/category'
-require 'models/post'
-require 'models/author'
-require 'models/comment'
-require 'models/tag'
-require 'models/tagging'
-require 'models/person'
-require 'models/reader'
-require 'models/parrot'
-require 'models/ship_part'
-require 'models/ship'
-require 'models/liquid'
-require 'models/molecule'
-require 'models/electron'
-require 'models/man'
-require 'models/interest'
+require "models/computer"
+require "models/developer"
+require "models/project"
+require "models/company"
+require "models/categorization"
+require "models/category"
+require "models/post"
+require "models/author"
+require "models/comment"
+require "models/tag"
+require "models/tagging"
+require "models/person"
+require "models/reader"
+require "models/ship_part"
+require "models/ship"
+require "models/liquid"
+require "models/molecule"
+require "models/electron"
+require "models/man"
+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')
- molecule = liquid.molecules.create(:name => 'molecule_1')
- molecule.electrons.create(:name => 'electron_1')
- molecule.electrons.create(:name => 'electron_2')
+ liquid = Liquid.create(name: "salty")
+ molecule = liquid.molecules.create(name: "molecule_1")
+ molecule.electrons.create(name: "electron_1")
+ molecule.electrons.create(name: "electron_2")
- liquids = Liquid.includes(:molecules => :electrons).references(:molecules).where('molecules.id is not null')
+ liquids = Liquid.includes(molecules: :electrons).references(:molecules).where("molecules.id is not null")
assert_equal 1, liquids[0].molecules.length
end
def test_subselect
author = authors :david
favs = author.author_favorites
- fav2 = author.author_favorites.where(:author => Author.where(id: author.id)).to_a
+ fav2 = author.author_favorites.where(author: Author.where(id: author.id)).to_a
assert_equal favs, fav2
end
def test_loading_the_association_target_should_keep_child_records_marked_for_destruction
- ship = Ship.create!(:name => "The good ship Dollypop")
- part = ship.parts.create!(:name => "Mast")
+ ship = Ship.create!(name: "The good ship Dollypop")
+ part = ship.parts.create!(name: "Mast")
part.mark_for_destruction
- ship.parts.send(:load_target)
- assert ship.parts[0].marked_for_destruction?
+ assert_predicate ship.parts[0], :marked_for_destruction?
end
def test_loading_the_association_target_should_load_most_recent_attributes_for_child_records_marked_for_destruction
- ship = Ship.create!(:name => "The good ship Dollypop")
- part = ship.parts.create!(:name => "Mast")
+ ship = Ship.create!(name: "The good ship Dollypop")
+ part = ship.parts.create!(name: "Mast")
part.mark_for_destruction
- ShipPart.find(part.id).update_columns(name: 'Deck')
- ship.parts.send(:load_target)
- assert_equal 'Deck', ship.parts[0].name
+ ShipPart.find(part.id).update_columns(name: "Deck")
+ assert_equal "Deck", ship.parts[0].name
end
-
def test_include_with_order_works
- assert_nothing_raised {Account.all.merge!(:order => 'id', :includes => :firm).first}
- assert_nothing_raised {Account.all.merge!(:order => :id, :includes => :firm).first}
+ assert_nothing_raised { Account.all.merge!(order: "id", includes: :firm).first }
+ assert_nothing_raised { Account.all.merge!(order: :id, includes: :firm).first }
end
def test_bad_collection_keys
- assert_raise(ArgumentError, 'ActiveRecord should have barked on bad collection keys') do
- Class.new(ActiveRecord::Base).has_many(:wheels, :name => 'wheels')
+ assert_raise(ArgumentError, "ActiveRecord should have barked on bad collection keys") do
+ Class.new(ActiveRecord::Base).has_many(:wheels, name: "wheels")
end
end
def test_should_construct_new_finder_sql_after_create
- person = Person.new :first_name => 'clark'
+ person = Person.new first_name: "clark"
assert_equal [], person.readers.to_a
person.save!
- reader = Reader.create! :person => person, :post => Post.new(:title => "foo", :body => "bar")
+ reader = Reader.create! person: person, post: Post.new(title: "foo", body: "bar")
assert person.readers.find(reader.id)
end
def test_force_reload
firm = Firm.new("name" => "A New Firm, Inc")
firm.save
- firm.clients.each {} # forcing to load all clients
+ firm.clients.each { } # forcing to load all clients
assert firm.clients.empty?, "New firm shouldn't have client objects"
assert_equal 0, firm.clients.size, "New firm should have 0 clients"
@@ -93,8 +90,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"
- 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"
+ firm.clients.reload
+
+ assert_not 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
@@ -103,102 +102,99 @@ class AssociationsTest < ActiveRecord::TestCase
has_many_reflections = [Tag.reflect_on_association(:taggings), Developer.reflect_on_association(:projects)]
mixed_reflections = (belongs_to_reflections + has_many_reflections).uniq
assert using_limitable_reflections.call(belongs_to_reflections), "Belong to associations are limitable"
- assert !using_limitable_reflections.call(has_many_reflections), "All has many style associations are not limitable"
- 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)
- 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
+ assert_not using_limitable_reflections.call(has_many_reflections), "All has many style associations are not limitable"
+ assert_not using_limitable_reflections.call(mixed_reflections), "No collection associations (has many style) should pass"
end
def test_association_with_references
firm = companies(:first_firm)
- assert_includes firm.association_with_references.references_values, 'foo'
+ assert_includes firm.association_with_references.references_values, "foo"
end
-
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)
- david.posts << (post = Post.new(:title => "New on Edge", :body => "More cool stuff!"))
- assert !david.posts.loaded?
- assert david.posts.include?(post)
+ david.posts << (post = Post.new(title: "New on Edge", body: "More cool stuff!"))
+ assert_not_predicate david.posts, :loaded?
+ assert_includes david.posts, post
end
def test_push_has_many_through_does_not_load_target
david = authors(:david)
david.categories << categories(:technology)
- assert !david.categories.loaded?
- assert david.categories.include?(categories(:technology))
+ assert_not_predicate david.categories, :loaded?
+ assert_includes david.categories, categories(:technology)
end
def test_push_followed_by_save_does_not_load_target
david = authors(:david)
- david.posts << (post = Post.new(:title => "New on Edge", :body => "More cool stuff!"))
- assert !david.posts.loaded?
+ david.posts << (post = Post.new(title: "New on Edge", body: "More cool stuff!"))
+ assert_not_predicate david.posts, :loaded?
david.save
- assert !david.posts.loaded?
- assert david.posts.include?(post)
+ assert_not_predicate david.posts, :loaded?
+ assert_includes david.posts, post
end
def test_push_does_not_lose_additions_to_new_record
- josh = Author.new(:name => "Josh")
- josh.posts << Post.new(:title => "New on Edge", :body => "More cool stuff!")
- assert josh.posts.loaded?
+ josh = Author.new(name: "Josh")
+ josh.posts << Post.new(title: "New on Edge", body: "More cool stuff!")
+ assert_predicate josh.posts, :loaded?
assert_equal 1, josh.posts.size
end
def test_append_behaves_like_push
- josh = Author.new(:name => "Josh")
- josh.posts.append Post.new(:title => "New on Edge", :body => "More cool stuff!")
- assert josh.posts.loaded?
+ josh = Author.new(name: "Josh")
+ josh.posts.append Post.new(title: "New on Edge", body: "More cool stuff!")
+ assert_predicate josh.posts, :loaded?
assert_equal 1, josh.posts.size
end
def test_prepend_is_not_defined
- josh = Author.new(:name => "Josh")
+ josh = Author.new(name: "Josh")
assert_raises(NoMethodError) { josh.posts.prepend Post.new }
end
def test_save_on_parent_does_not_load_target
david = developers(:david)
- assert !david.projects.loaded?
+ assert_not_predicate david.projects, :loaded?
david.update_columns(created_at: Time.now)
- assert !david.projects.loaded?
+ assert_not_predicate david.projects, :loaded?
+ end
+
+ def test_load_does_load_target
+ david = developers(:david)
+
+ assert_not_predicate david.projects, :loaded?
+ david.projects.load
+ assert_predicate david.projects, :loaded?
end
def test_inspect_does_not_reload_a_not_yet_loaded_target
- andreas = Developer.new :name => 'Andreas', :log => 'new developer added'
- assert !andreas.audit_logs.loaded?
+ andreas = Developer.new name: "Andreas", log: "new developer added"
+ assert_not_predicate andreas.audit_logs, :loaded?
assert_match(/message: "new developer added"/, andreas.audit_logs.inspect)
end
def test_save_on_parent_saves_children
- developer = Developer.create :name => "Bryan", :salary => 50_000
+ developer = Developer.create name: "Bryan", salary: 50_000
assert_equal 1, developer.reload.audit_logs.size
end
def test_create_via_association_with_block
- post = authors(:david).posts.create(:title => "New on Edge") {|p| p.body = "More cool stuff!"}
+ post = authors(:david).posts.create(title: "New on Edge") { |p| p.body = "More cool stuff!" }
assert_equal post.title, "New on Edge"
assert_equal post.body, "More cool stuff!"
end
def test_create_with_bang_via_association_with_block
- post = authors(:david).posts.create!(:title => "New on Edge") {|p| p.body = "More cool stuff!"}
+ post = authors(:david).posts.create!(title: "New on Edge") { |p| p.body = "More cool stuff!" }
assert_equal post.title, "New on Edge"
assert_equal post.body, "More cool stuff!"
end
@@ -216,7 +212,7 @@ class AssociationProxyTest < ActiveRecord::TestCase
end
def test_scoped_allows_conditions
- assert developers(:david).projects.merge(where: 'foo').to_sql.include?('foo')
+ assert developers(:david).projects.merge(where: "foo").to_sql.include?("foo")
end
test "getting a scope from an association" do
@@ -228,7 +224,14 @@ class AssociationProxyTest < ActiveRecord::TestCase
test "proxy object is cached" do
david = developers(:david)
- assert david.projects.equal?(david.projects)
+ assert_same david.projects, david.projects
+ end
+
+ test "proxy object can be stubbed" do
+ david = developers(:david)
+ david.projects.define_singleton_method(:extra_method) { 42 }
+
+ assert_equal 42, david.projects.extra_method
end
test "inverses get set of subsets of the association" do
@@ -244,16 +247,25 @@ class AssociationProxyTest < ActiveRecord::TestCase
test "first! works on loaded associations" do
david = authors(:david)
- assert_equal david.posts.first, david.posts.reload.first!
+ assert_equal david.first_posts.first, david.first_posts.reload.first!
+ assert_predicate david.first_posts, :loaded?
+ assert_no_queries { david.first_posts.first! }
+ end
+
+ def test_pluck_uses_loaded_target
+ david = authors(:david)
+ assert_equal david.first_posts.pluck(:title), david.first_posts.load.pluck(:title)
+ assert_predicate david.first_posts, :loaded?
+ assert_no_queries { david.first_posts.pluck(:title) }
end
def test_reset_unloads_target
david = authors(:david)
david.posts.reload
- assert david.posts.loaded?
+ assert_predicate david.posts, :loaded?
david.posts.reset
- assert !david.posts.loaded?
+ assert_not_predicate david.posts, :loaded?
end
end
@@ -261,18 +273,18 @@ class OverridingAssociationsTest < ActiveRecord::TestCase
class DifferentPerson < ActiveRecord::Base; end
class PeopleList < ActiveRecord::Base
- has_and_belongs_to_many :has_and_belongs_to_many, :before_add => :enlist
- has_many :has_many, :before_add => :enlist
+ has_and_belongs_to_many :has_and_belongs_to_many, before_add: :enlist
+ has_many :has_many, before_add: :enlist
belongs_to :belongs_to
has_one :has_one
end
class DifferentPeopleList < PeopleList
# Different association with the same name, callbacks should be omitted here.
- has_and_belongs_to_many :has_and_belongs_to_many, :class_name => 'DifferentPerson'
- has_many :has_many, :class_name => 'DifferentPerson'
- belongs_to :belongs_to, :class_name => 'DifferentPerson'
- has_one :has_one, :class_name => 'DifferentPerson'
+ has_and_belongs_to_many :has_and_belongs_to_many, class_name: "DifferentPerson"
+ has_many :has_many, class_name: "DifferentPerson"
+ belongs_to :belongs_to, class_name: "DifferentPerson"
+ has_one :has_one, class_name: "DifferentPerson"
end
def test_habtm_association_redefinition_callbacks_should_differ_and_not_inherited
diff --git a/activerecord/test/cases/attribute_decorators_test.rb b/activerecord/test/cases/attribute_decorators_test.rb
index 2aeb2601c2..42eca233ce 100644
--- a/activerecord/test/cases/attribute_decorators_test.rb
+++ b/activerecord/test/cases/attribute_decorators_test.rb
@@ -1,9 +1,11 @@
-require 'cases/helper'
+# frozen_string_literal: true
+
+require "cases/helper"
module ActiveRecord
class AttributeDecoratorsTest < ActiveRecord::TestCase
class Model < ActiveRecord::Base
- self.table_name = 'attribute_decorators_model'
+ self.table_name = "attribute_decorators_model"
end
class StringDecorator < SimpleDelegator
@@ -28,19 +30,19 @@ module ActiveRecord
teardown do
return unless @connection
- @connection.drop_table 'attribute_decorators_model', if_exists: true
+ @connection.drop_table "attribute_decorators_model", if_exists: true
Model.attribute_type_decorations.clear
Model.reset_column_information
end
test "attributes can be decorated" do
- model = Model.new(a_string: 'Hello')
- assert_equal 'Hello', model.a_string
+ model = Model.new(a_string: "Hello")
+ assert_equal "Hello", model.a_string
Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
- model = Model.new(a_string: 'Hello')
- assert_equal 'Hello decorated!', model.a_string
+ model = Model.new(a_string: "Hello")
+ assert_equal "Hello decorated!", model.a_string
end
test "decoration does not eagerly load existing columns" do
@@ -51,54 +53,54 @@ module ActiveRecord
end
test "undecorated columns are not touched" do
- Model.attribute :another_string, :string, default: 'something or other'
+ Model.attribute :another_string, :string, default: "something or other"
Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
- assert_equal 'something or other', Model.new.another_string
+ assert_equal "something or other", Model.new.another_string
end
test "decorators can be chained" do
Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
Model.decorate_attribute_type(:a_string, :other) { |t| StringDecorator.new(t) }
- model = Model.new(a_string: 'Hello!')
+ model = Model.new(a_string: "Hello!")
- assert_equal 'Hello! decorated! decorated!', model.a_string
+ assert_equal "Hello! decorated! decorated!", model.a_string
end
test "decoration of the same type multiple times is idempotent" do
Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
- model = Model.new(a_string: 'Hello')
- assert_equal 'Hello decorated!', model.a_string
+ model = Model.new(a_string: "Hello")
+ assert_equal "Hello decorated!", model.a_string
end
test "decorations occur in order of declaration" do
Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
Model.decorate_attribute_type(:a_string, :other) do |type|
- StringDecorator.new(type, 'decorated again!')
+ StringDecorator.new(type, "decorated again!")
end
- model = Model.new(a_string: 'Hello!')
+ model = Model.new(a_string: "Hello!")
- assert_equal 'Hello! decorated! decorated again!', model.a_string
+ assert_equal "Hello! decorated! decorated again!", model.a_string
end
test "decorating attributes does not modify parent classes" do
- Model.attribute :another_string, :string, default: 'whatever'
+ Model.attribute :another_string, :string, default: "whatever"
Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
child_class = Class.new(Model)
child_class.decorate_attribute_type(:another_string, :test) { |t| StringDecorator.new(t) }
child_class.decorate_attribute_type(:a_string, :other) { |t| StringDecorator.new(t) }
- model = Model.new(a_string: 'Hello!')
- child = child_class.new(a_string: 'Hello!')
+ model = Model.new(a_string: "Hello!")
+ child = child_class.new(a_string: "Hello!")
- assert_equal 'Hello! decorated!', model.a_string
- assert_equal 'whatever', model.another_string
- assert_equal 'Hello! decorated! decorated!', child.a_string
- assert_equal 'whatever decorated!', child.another_string
+ assert_equal "Hello! decorated!", model.a_string
+ assert_equal "whatever", model.another_string
+ assert_equal "Hello! decorated! decorated!", child.a_string
+ assert_equal "whatever decorated!", child.another_string
end
class Multiplier < SimpleDelegator
@@ -116,9 +118,9 @@ module ActiveRecord
Multiplier.new(type)
end
- model = Model.new(a_string: 'whatever', an_int: 1)
+ model = Model.new(a_string: "whatever", an_int: 1)
- assert_equal 'whatever', model.a_string
+ assert_equal "whatever", model.a_string
assert_equal 2, model.an_int
end
end
diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb
index 74e556211b..54512068ee 100644
--- a/activerecord/test/cases/attribute_methods/read_test.rb
+++ b/activerecord/test/cases/attribute_methods/read_test.rb
@@ -1,20 +1,21 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'thread'
module ActiveRecord
module AttributeMethods
class ReadTest < ActiveRecord::TestCase
- class FakeColumn < Struct.new(:name)
+ FakeColumn = Struct.new(:name) do
def type; :integer; end
end
def setup
- @klass = Class.new do
+ @klass = Class.new(Class.new { def self.initialize_generated_modules; end }) do
def self.superclass; Base; end
- def self.base_class; self; end
+ def self.base_class?; true; end
def self.decorate_matching_attribute_types(*); end
- def self.initialize_generated_modules; end
+ include ActiveRecord::DefineCallbacks
include ActiveRecord::AttributeMethods
def self.attribute_names
@@ -40,13 +41,13 @@ module ActiveRecord
instance = @klass.new
@klass.attribute_names.each do |name|
- assert !instance.methods.map(&:to_s).include?(name)
+ assert_not_includes instance.methods.map(&:to_s), name
end
@klass.define_attribute_methods
@klass.attribute_names.each do |name|
- assert instance.methods.map(&:to_s).include?(name), "#{name} is not defined"
+ assert_includes instance.methods.map(&:to_s), name, "#{name} is not defined"
end
end
diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb
index ea2b94cbf4..0dbdd56ae6 100644
--- a/activerecord/test/cases/attribute_methods_test.rb
+++ b/activerecord/test/cases/attribute_methods_test.rb
@@ -1,16 +1,17 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/minimalistic'
-require 'models/developer'
-require 'models/computer'
-require 'models/auto_id'
-require 'models/boolean'
-require 'models/computer'
-require 'models/topic'
-require 'models/company'
-require 'models/category'
-require 'models/reply'
-require 'models/contact'
-require 'models/keyboard'
+require "models/minimalistic"
+require "models/developer"
+require "models/auto_id"
+require "models/boolean"
+require "models/computer"
+require "models/topic"
+require "models/company"
+require "models/category"
+require "models/reply"
+require "models/contact"
+require "models/keyboard"
class AttributeMethodsTest < ActiveRecord::TestCase
include InTimeZone
@@ -20,7 +21,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
def setup
@old_matchers = ActiveRecord::Base.send(:attribute_method_matchers).dup
@target = Class.new(ActiveRecord::Base)
- @target.table_name = 'topics'
+ @target.table_name = "topics"
end
teardown do
@@ -28,26 +29,52 @@ class AttributeMethodsTest < ActiveRecord::TestCase
ActiveRecord::Base.send(:attribute_method_matchers).concat(@old_matchers)
end
- def test_attribute_for_inspect
+ test "attribute_for_inspect with a string" do
t = topics(:first)
t.title = "The First Topic Now Has A Title With\nNewlines And More Than 50 Characters"
- assert_equal %("#{t.written_on.to_s(:db)}"), t.attribute_for_inspect(:written_on)
assert_equal '"The First Topic Now Has A Title With\nNewlines And ..."', t.attribute_for_inspect(:title)
end
- def test_attribute_present
+ test "attribute_for_inspect with a date" do
+ t = topics(:first)
+
+ assert_equal %("#{t.written_on.to_s(:db)}"), t.attribute_for_inspect(:written_on)
+ end
+
+ test "attribute_for_inspect with an array" do
+ t = topics(:first)
+ t.content = [Object.new]
+
+ assert_match %r(\[#<Object:0x[0-9a-f]+>\]), t.attribute_for_inspect(:content)
+ end
+
+ test "attribute_for_inspect with a long array" do
+ t = topics(:first)
+ t.content = (1..11).to_a
+
+ assert_equal "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]", t.attribute_for_inspect(:content)
+ end
+
+ test "attribute_for_inspect with a non-primary key id attribute" do
+ t = topics(:first).becomes(TitlePrimaryKeyTopic)
+ t.title = "The First Topic Now Has A Title With\nNewlines And More Than 50 Characters"
+
+ assert_equal "1", t.attribute_for_inspect(:id)
+ end
+
+ test "attribute_present" do
t = Topic.new
t.title = "hello there!"
t.written_on = Time.now
t.author_name = ""
assert t.attribute_present?("title")
assert t.attribute_present?("written_on")
- assert !t.attribute_present?("content")
- assert !t.attribute_present?("author_name")
+ assert_not t.attribute_present?("content")
+ assert_not t.attribute_present?("author_name")
end
- def test_attribute_present_with_booleans
+ test "attribute_present with booleans" do
b1 = Boolean.new
b1.value = false
assert b1.attribute_present?(:value)
@@ -57,7 +84,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert b2.attribute_present?(:value)
b3 = Boolean.new
- assert !b3.attribute_present?(:value)
+ assert_not b3.attribute_present?(:value)
b4 = Boolean.new
b4.value = false
@@ -65,43 +92,44 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert Boolean.find(b4.id).attribute_present?(:value)
end
- def test_caching_nil_primary_key
+ test "caching a nil primary key" do
klass = Class.new(Minimalistic)
- klass.expects(:reset_primary_key).returns(nil).once
- 2.times { klass.primary_key }
+ assert_called(klass, :reset_primary_key, returns: nil) do
+ 2.times { klass.primary_key }
+ end
end
- def test_attribute_keys_on_new_instance
+ 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
- def test_boolean_attributes
- assert !Topic.find(1).approved?
- assert Topic.find(2).approved?
+ test "boolean attributes" do
+ assert_not_predicate Topic.find(1), :approved?
+ assert_predicate Topic.find(2), :approved?
end
- def test_set_attributes
+ test "set attributes" do
topic = Topic.find(1)
- topic.attributes = { "title" => "Budget", "author_name" => "Jason" }
+ topic.attributes = { title: "Budget", author_name: "Jason" }
topic.save
assert_equal("Budget", topic.title)
assert_equal("Jason", topic.author_name)
assert_equal(topics(:first).author_email_address, Topic.find(1).author_email_address)
end
- def test_set_attributes_without_hash
+ test "set attributes without a hash" do
topic = Topic.new
- assert_raise(ArgumentError) { topic.attributes = '' }
+ assert_raise(ArgumentError) { topic.attributes = "" }
end
- def test_integers_as_nil
- test = AutoId.create('value' => '')
+ test "integers as nil" do
+ test = AutoId.create(value: "")
assert_nil AutoId.find(test.id).value
end
- def test_set_attributes_with_block
+ test "set attributes with a block" do
topic = Topic.new do |t|
t.title = "Budget"
t.author_name = "Jason"
@@ -111,7 +139,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal("Jason", topic.author_name)
end
- def test_respond_to?
+ test "respond_to?" do
topic = Topic.find(1)
assert_respond_to topic, "title"
assert_respond_to topic, "title?"
@@ -121,79 +149,62 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_respond_to topic, :title=
assert_respond_to topic, "author_name"
assert_respond_to topic, "attribute_names"
- assert !topic.respond_to?("nothingness")
- assert !topic.respond_to?(:nothingness)
+ assert_not_respond_to topic, "nothingness"
+ assert_not_respond_to topic, :nothingness
end
- def test_respond_to_with_custom_primary_key
+ test "respond_to? with a custom primary key" do
keyboard = Keyboard.create
assert_not_nil keyboard.key_number
assert_equal keyboard.key_number, keyboard.id
- assert keyboard.respond_to?('key_number')
- assert keyboard.respond_to?('id')
+ assert_respond_to keyboard, "key_number"
+ assert_respond_to keyboard, "id"
end
- def test_id_before_type_cast_with_custom_primary_key
+ test "id_before_type_cast with a custom primary key" do
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_equal '10', keyboard.read_attribute_before_type_cast('key_number')
- assert_equal '10', keyboard.read_attribute_before_type_cast(:key_number)
- end
-
- # Syck calls respond_to? before actually calling initialize
- def test_respond_to_with_allocated_object
- klass = Class.new(ActiveRecord::Base) do
- self.table_name = 'topics'
- end
-
- topic = klass.allocate
- assert !topic.respond_to?("nothingness")
- assert !topic.respond_to?(:nothingness)
- assert_respond_to topic, "title"
- assert_respond_to topic, :title
+ keyboard.key_number = "10"
+ assert_equal "10", keyboard.id_before_type_cast
+ 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
- # IRB inspects the return value of "MyModel.allocate".
- def test_allocated_object_can_be_inspected
+ # IRB inspects the return value of MyModel.allocate.
+ test "allocated objects can be inspected" do
topic = Topic.allocate
assert_equal "#<Topic not initialized>", topic.inspect
end
- def test_array_content
+ test "array content" do
+ content = %w( one two three )
topic = Topic.new
- topic.content = %w( one two three )
+ topic.content = content
topic.save
- assert_equal(%w( one two three ), Topic.find(topic.id).content)
+ assert_equal content, Topic.find(topic.id).content
end
- def test_read_attributes_before_type_cast
- category = Category.new({:name=>"Test category", :type => nil})
- category_attrs = {"name"=>"Test category", "id" => nil, "type" => nil, "categorizations_count" => nil}
- assert_equal category_attrs , category.attributes_before_type_cast
+ test "read attributes_before_type_cast" do
+ category = Category.new(name: "Test category", type: nil)
+ category_attrs = { "name" => "Test category", "id" => nil, "type" => nil, "categorizations_count" => nil }
+ assert_equal category_attrs, category.attributes_before_type_cast
end
- if current_adapter?(:MysqlAdapter)
- def test_read_attributes_before_type_cast_on_boolean
- bool = Boolean.create({ "value" => false })
- if RUBY_PLATFORM =~ /java/
- # JRuby will return the value before typecast as string
- assert_equal "0", bool.reload.attributes_before_type_cast["value"]
- else
- assert_equal 0, bool.reload.attributes_before_type_cast["value"]
- end
+ if current_adapter?(:Mysql2Adapter)
+ test "read attributes_before_type_cast on a boolean" do
+ bool = Boolean.create!("value" => false)
+ assert_equal 0, bool.reload.attributes_before_type_cast["value"]
end
end
- def test_read_attributes_before_type_cast_on_datetime
+ test "read attributes_before_type_cast on a datetime" do
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new
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
@@ -202,7 +213,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
end
- def test_read_attributes_after_type_cast_on_datetime
+ test "read attributes_after_type_cast on a date" do
tz = "Pacific Time (US & Canada)"
in_time_zone tz do
@@ -223,7 +234,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
end
- def test_hash_content
+ test "hash content" do
topic = Topic.new
topic.content = { "one" => 1, "two" => 2 }
topic.save
@@ -237,7 +248,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal 3, Topic.find(topic.id).content["three"]
end
- def test_update_array_content
+ test "update array content" do
topic = Topic.new
topic.content = %w( one two three )
@@ -251,40 +262,40 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal(%w( one two three four five ), topic.content)
end
- def test_case_sensitive_attributes_hash
- # DB2 is not case-sensitive
+ test "case-sensitive attributes hash" do
+ # DB2 is not case-sensitive.
return true if current_adapter?(:DB2Adapter)
- assert_equal @loaded_fixtures['computers']['workstation'].to_hash, Computer.first.attributes
+ assert_equal @loaded_fixtures["computers"]["workstation"].to_hash, Computer.first.attributes
end
- def test_attributes_without_primary_key
+ test "attributes without primary key" do
klass = Class.new(ActiveRecord::Base) do
- self.table_name = 'developers_projects'
+ self.table_name = "developers_projects"
end
assert_equal klass.column_names, klass.new.attributes.keys
- assert_not klass.new.has_attribute?('id')
+ assert_not klass.new.has_attribute?("id")
end
- def test_hashes_not_mangled
- new_topic = { :title => "New Topic" }
- new_topic_values = { :title => "AnotherTopic" }
+ test "hashes are not mangled" do
+ new_topic = { title: "New Topic" }
+ new_topic_values = { title: "AnotherTopic" }
topic = Topic.new(new_topic)
assert_equal new_topic[:title], topic.title
- topic.attributes= new_topic_values
+ topic.attributes = new_topic_values
assert_equal new_topic_values[:title], topic.title
end
- def test_create_through_factory
- topic = Topic.create("title" => "New Topic")
+ test "create through factory" do
+ topic = Topic.create(title: "New Topic")
topicReloaded = Topic.find(topic.id)
assert_equal(topic, topicReloaded)
end
- def test_write_attribute
+ test "write_attribute" do
topic = Topic.new
topic.send(:write_attribute, :title, "Still another topic")
assert_equal "Still another topic", topic.title
@@ -299,7 +310,20 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal "Still another topic: part 4", topic.title
end
- def test_read_attribute
+ 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 "write_attribute raises ActiveModel::MissingAttributeError when the attribute does not exist" do
+ topic = Topic.first
+ assert_raises(ActiveModel::MissingAttributeError) { topic.update_columns(no_column_exists: "Hello!") }
+ assert_raises(ActiveModel::UnknownAttributeError) { topic.update(no_column_exists: "Hello!") }
+ end
+
+ test "read_attribute" do
topic = Topic.new
topic.title = "Don't change the topic"
assert_equal "Don't change the topic", topic.read_attribute("title")
@@ -309,23 +333,33 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal "Don't change the topic", topic[:title]
end
- def test_read_attribute_raises_missing_attribute_error_when_not_exists
- computer = Computer.select('id').first
+ 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] }
assert_raises(ActiveModel::MissingAttributeError) { computer[:extendedWarranty] }
- assert_raises(ActiveModel::MissingAttributeError) { computer[:no_column_exists] = 'Hello!' }
- assert_nothing_raised { computer[:developer] = 'Hello!' }
+ assert_raises(ActiveModel::MissingAttributeError) { computer[:no_column_exists] = "Hello!" }
+ assert_nothing_raised { computer[:developer] = "Hello!" }
end
- def test_read_attribute_when_false
+ test "read_attribute when false" do
topic = topics(:first)
topic.approved = false
- assert !topic.approved?, "approved should be false"
+ assert_not topic.approved?, "approved should be false"
topic.approved = "false"
- assert !topic.approved?, "approved should be false"
+ assert_not topic.approved?, "approved should be false"
end
- def test_read_attribute_when_true
+ test "read_attribute when true" do
topic = topics(:first)
topic.approved = true
assert topic.approved?, "approved should be true"
@@ -333,13 +367,13 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert topic.approved?, "approved should be true"
end
- def test_read_write_boolean_attribute
+ test "boolean attributes writing and reading" do
topic = Topic.new
topic.approved = "false"
- assert !topic.approved?, "approved should be false"
+ assert_not topic.approved?, "approved should be false"
topic.approved = "false"
- assert !topic.approved?, "approved should be false"
+ assert_not topic.approved?, "approved should be false"
topic.approved = "true"
assert topic.approved?, "approved should be true"
@@ -348,7 +382,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert topic.approved?, "approved should be true"
end
- def test_overridden_write_attribute
+ test "overridden write_attribute" do
topic = Topic.new
def topic.write_attribute(attr_name, value)
super(attr_name, value.downcase)
@@ -367,7 +401,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal "yet another topic: part 4", topic.title
end
- def test_overridden_read_attribute
+ test "overridden read_attribute" do
topic = Topic.new
topic.title = "Stop changing the topic"
def topic.read_attribute(attr_name)
@@ -381,40 +415,40 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal "STOP CHANGING THE TOPIC", topic[:title]
end
- def test_read_overridden_attribute
- topic = Topic.new(:title => 'a')
- def topic.title() 'b' end
- assert_equal 'a', topic[:title]
+ test "read overridden attribute" do
+ topic = Topic.new(title: "a")
+ def topic.title() "b" end
+ assert_equal "a", topic[:title]
end
- def test_query_attribute_string
+ test "string attribute predicate" do
[nil, "", " "].each do |value|
- assert_equal false, Topic.new(:author_name => value).author_name?
+ assert_equal false, Topic.new(author_name: value).author_name?
end
- assert_equal true, Topic.new(:author_name => "Name").author_name?
+ assert_equal true, Topic.new(author_name: "Name").author_name?
end
- def test_query_attribute_number
+ test "number attribute predicate" do
[nil, 0, "0"].each do |value|
- assert_equal false, Developer.new(:salary => value).salary?
+ assert_equal false, Developer.new(salary: value).salary?
end
- assert_equal true, Developer.new(:salary => 1).salary?
- assert_equal true, Developer.new(:salary => "1").salary?
+ assert_equal true, Developer.new(salary: 1).salary?
+ assert_equal true, Developer.new(salary: "1").salary?
end
- def test_query_attribute_boolean
+ test "boolean attribute predicate" do
[nil, "", false, "false", "f", 0].each do |value|
- assert_equal false, Topic.new(:approved => value).approved?
+ assert_equal false, Topic.new(approved: value).approved?
end
[true, "true", "1", 1].each do |value|
- assert_equal true, Topic.new(:approved => value).approved?
+ assert_equal true, Topic.new(approved: value).approved?
end
end
- def test_query_attribute_with_custom_fields
+ test "custom field attribute predicate" do
object = Company.find_by_sql(<<-SQL).first
SELECT c1.*, c2.type as string_value, c2.rating as int_value
FROM companies c1, companies c2
@@ -423,107 +457,107 @@ class AttributeMethodsTest < ActiveRecord::TestCase
SQL
assert_equal "Firm", object.string_value
- assert object.string_value?
+ assert_predicate object, :string_value?
object.string_value = " "
- assert !object.string_value?
+ assert_not_predicate object, :string_value?
assert_equal 1, object.int_value.to_i
- assert object.int_value?
+ assert_predicate object, :int_value?
object.int_value = "0"
- assert !object.int_value?
+ assert_not_predicate object, :int_value?
end
- def test_non_attribute_access_and_assignment
+ test "non-attribute read and write" do
topic = Topic.new
- assert !topic.respond_to?("mumbo")
+ assert_not_respond_to topic, "mumbo"
assert_raise(NoMethodError) { topic.mumbo }
assert_raise(NoMethodError) { topic.mumbo = 5 }
end
- def test_undeclared_attribute_method_does_not_affect_respond_to_and_method_missing
- topic = @target.new(:title => 'Budget')
- assert topic.respond_to?('title')
- assert_equal 'Budget', topic.title
- assert !topic.respond_to?('title_hello_world')
+ test "undeclared attribute method does not affect respond_to? and method_missing" do
+ topic = @target.new(title: "Budget")
+ assert_respond_to topic, "title"
+ assert_equal "Budget", topic.title
+ assert_not_respond_to topic, "title_hello_world"
assert_raise(NoMethodError) { topic.title_hello_world }
end
- def test_declared_prefixed_attribute_method_affects_respond_to_and_method_missing
- topic = @target.new(:title => 'Budget')
+ test "declared prefixed attribute method affects respond_to? and method_missing" do
+ topic = @target.new(title: "Budget")
%w(default_ title_).each do |prefix|
@target.class_eval "def #{prefix}attribute(*args) args end"
@target.attribute_method_prefix prefix
meth = "#{prefix}title"
- assert topic.respond_to?(meth)
- assert_equal ['title'], topic.send(meth)
- assert_equal ['title', 'a'], topic.send(meth, 'a')
- assert_equal ['title', 1, 2, 3], topic.send(meth, 1, 2, 3)
+ assert_respond_to topic, meth
+ assert_equal ["title"], topic.send(meth)
+ assert_equal ["title", "a"], topic.send(meth, "a")
+ assert_equal ["title", 1, 2, 3], topic.send(meth, 1, 2, 3)
end
end
- def test_declared_suffixed_attribute_method_affects_respond_to_and_method_missing
+ test "declared suffixed attribute method affects respond_to? and method_missing" do
%w(_default _title_default _it! _candidate= able?).each do |suffix|
@target.class_eval "def attribute#{suffix}(*args) args end"
@target.attribute_method_suffix suffix
- topic = @target.new(:title => 'Budget')
+ topic = @target.new(title: "Budget")
meth = "title#{suffix}"
- assert topic.respond_to?(meth)
- assert_equal ['title'], topic.send(meth)
- assert_equal ['title', 'a'], topic.send(meth, 'a')
- assert_equal ['title', 1, 2, 3], topic.send(meth, 1, 2, 3)
+ assert_respond_to topic, meth
+ assert_equal ["title"], topic.send(meth)
+ assert_equal ["title", "a"], topic.send(meth, "a")
+ assert_equal ["title", 1, 2, 3], topic.send(meth, 1, 2, 3)
end
end
- def test_declared_affixed_attribute_method_affects_respond_to_and_method_missing
- [['mark_', '_for_update'], ['reset_', '!'], ['default_', '_value?']].each do |prefix, suffix|
+ test "declared affixed attribute method affects respond_to? and method_missing" do
+ [["mark_", "_for_update"], ["reset_", "!"], ["default_", "_value?"]].each do |prefix, suffix|
@target.class_eval "def #{prefix}attribute#{suffix}(*args) args end"
- @target.attribute_method_affix({ :prefix => prefix, :suffix => suffix })
- topic = @target.new(:title => 'Budget')
+ @target.attribute_method_affix(prefix: prefix, suffix: suffix)
+ topic = @target.new(title: "Budget")
meth = "#{prefix}title#{suffix}"
- assert topic.respond_to?(meth)
- assert_equal ['title'], topic.send(meth)
- assert_equal ['title', 'a'], topic.send(meth, 'a')
- assert_equal ['title', 1, 2, 3], topic.send(meth, 1, 2, 3)
+ assert_respond_to topic, meth
+ assert_equal ["title"], topic.send(meth)
+ assert_equal ["title", "a"], topic.send(meth, "a")
+ assert_equal ["title", 1, 2, 3], topic.send(meth, 1, 2, 3)
end
end
- def test_should_unserialize_attributes_for_frozen_records
- myobj = {:value1 => :value2}
- topic = Topic.create("content" => myobj)
+ test "should unserialize attributes for frozen records" do
+ myobj = { value1: :value2 }
+ topic = Topic.create(content: myobj)
topic.freeze
assert_equal myobj, topic.content
end
- def test_typecast_attribute_from_select_to_false
- Topic.create(:title => 'Budget')
- # Oracle does not support boolean expressions in SELECT
+ test "typecast attribute from select to false" do
+ Topic.create(title: "Budget")
+ # Oracle does not support boolean expressions in SELECT.
if current_adapter?(:OracleAdapter, :FbAdapter)
- topic = Topic.all.merge!(:select => "topics.*, 0 as is_test").first
+ topic = Topic.all.merge!(select: "topics.*, 0 as is_test").first
else
- topic = Topic.all.merge!(:select => "topics.*, 1=2 as is_test").first
+ topic = Topic.all.merge!(select: "topics.*, 1=2 as is_test").first
end
- assert !topic.is_test?
+ assert_not_predicate topic, :is_test?
end
- def test_typecast_attribute_from_select_to_true
- Topic.create(:title => 'Budget')
- # Oracle does not support boolean expressions in SELECT
+ test "typecast attribute from select to true" do
+ Topic.create(title: "Budget")
+ # Oracle does not support boolean expressions in SELECT.
if current_adapter?(:OracleAdapter, :FbAdapter)
- topic = Topic.all.merge!(:select => "topics.*, 1 as is_test").first
+ topic = Topic.all.merge!(select: "topics.*, 1 as is_test").first
else
- topic = Topic.all.merge!(:select => "topics.*, 2=2 as is_test").first
+ topic = Topic.all.merge!(select: "topics.*, 2=2 as is_test").first
end
- assert topic.is_test?
+ assert_predicate topic, :is_test?
end
- def test_raises_dangerous_attribute_error_when_defining_activerecord_method_in_model
+ test "raises ActiveRecord::DangerousAttributeError when defining an AR method in a model" do
%w(save create_or_update).each do |method|
- klass = Class.new ActiveRecord::Base
+ klass = Class.new(ActiveRecord::Base)
klass.class_eval "def #{method}() 'defined #{method}' end"
assert_raise ActiveRecord::DangerousAttributeError do
klass.instance_method_already_implemented?(method)
@@ -531,7 +565,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
end
- def test_converted_values_are_returned_after_assignment
+ test "converted values are returned after assignment" do
developer = Developer.new(name: 1337, salary: "50000")
assert_equal "50000", developer.salary_before_type_cast
@@ -542,14 +576,11 @@ class AttributeMethodsTest < ActiveRecord::TestCase
developer.save!
- assert_equal "50000", developer.salary_before_type_cast
- assert_equal 1337, developer.name_before_type_cast
-
assert_equal 50000, developer.salary
assert_equal "1337", developer.name
end
- def test_write_nil_to_time_attributes
+ test "write nil to time attribute" do
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new
record.written_on = nil
@@ -557,7 +588,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
end
- def test_write_time_to_date_attributes
+ test "write time to date attribute" do
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new
record.last_read = Time.utc(2010, 1, 1, 10)
@@ -565,7 +596,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
end
- def test_time_attributes_are_retrieved_in_current_time_zone
+ test "time attributes are retrieved in the current time zone" do
in_time_zone "Pacific Time (US & Canada)" do
utc_time = Time.utc(2008, 1, 1)
record = @target.new
@@ -577,7 +608,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
end
- def test_setting_time_zone_aware_attribute_to_utc
+ test "setting a time zone-aware attribute to UTC" do
in_time_zone "Pacific Time (US & Canada)" do
utc_time = Time.utc(2008, 1, 1)
record = @target.new
@@ -588,11 +619,11 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
end
- def test_setting_time_zone_aware_attribute_in_other_time_zone
+ test "setting time zone-aware attribute in other time zone" do
utc_time = Time.utc(2008, 1, 1)
cst_time = utc_time.in_time_zone("Central Time (US & Canada)")
in_time_zone "Pacific Time (US & Canada)" do
- record = @target.new
+ record = @target.new
record.written_on = cst_time
assert_equal utc_time, record.written_on
assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone
@@ -600,23 +631,23 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
end
- def test_setting_time_zone_aware_read_attribute
+ test "setting time zone-aware read attribute" do
utc_time = Time.utc(2008, 1, 1)
cst_time = utc_time.in_time_zone("Central Time (US & Canada)")
in_time_zone "Pacific Time (US & Canada)" do
- record = @target.create(:written_on => cst_time).reload
+ record = @target.create(written_on: cst_time).reload
assert_equal utc_time, record[:written_on]
assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record[:written_on].time_zone
assert_equal Time.utc(2007, 12, 31, 16), record[:written_on].time
end
end
- def test_setting_time_zone_aware_attribute_with_string
+ test "setting time zone-aware attribute with a string" do
utc_time = Time.utc(2008, 1, 1)
(-11..13).each do |timezone_offset|
time_string = utc_time.in_time_zone(timezone_offset).to_s
in_time_zone "Pacific Time (US & Canada)" do
- record = @target.new
+ record = @target.new
record.written_on = time_string
assert_equal Time.zone.parse(time_string), record.written_on
assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone
@@ -625,30 +656,30 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
end
- def test_time_zone_aware_attribute_saved
+ test "time zone-aware attribute saved" do
in_time_zone 1 do
- record = @target.create(:written_on => '2012-02-20 10:00')
+ record = @target.create(written_on: "2012-02-20 10:00")
- record.written_on = '2012-02-20 09:00'
+ record.written_on = "2012-02-20 09:00"
record.save
assert_equal Time.zone.local(2012, 02, 20, 9), record.reload.written_on
end
end
- def test_setting_time_zone_aware_attribute_to_blank_string_returns_nil
+ test "setting a time zone-aware attribute to a blank string returns nil" do
in_time_zone "Pacific Time (US & Canada)" do
- record = @target.new
- record.written_on = ' '
+ record = @target.new
+ record.written_on = " "
assert_nil record.written_on
assert_nil record[:written_on]
end
end
- def test_setting_time_zone_aware_attribute_interprets_time_zone_unaware_string_in_time_zone
- time_string = 'Tue Jan 01 00:00:00 2008'
+ test "setting a time zone-aware attribute interprets time zone-unaware string in time zone" do
+ time_string = "Tue Jan 01 00:00:00 2008"
(-11..13).each do |timezone_offset|
in_time_zone timezone_offset do
- record = @target.new
+ record = @target.new
record.written_on = time_string
assert_equal Time.zone.parse(time_string), record.written_on
assert_equal ActiveSupport::TimeZone[timezone_offset], record.written_on.time_zone
@@ -657,10 +688,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
end
- def test_setting_time_zone_aware_datetime_in_current_time_zone
+ test "setting a time zone-aware datetime in the current time zone" do
utc_time = Time.utc(2008, 1, 1)
in_time_zone "Pacific Time (US & Canada)" do
- record = @target.new
+ record = @target.new
record.written_on = utc_time.in_time_zone
assert_equal utc_time, record.written_on
assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone
@@ -668,7 +699,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
end
- def test_yaml_dumping_record_with_time_zone_aware_attribute
+ test "YAML dumping a record with time zone-aware attribute" do
in_time_zone "Pacific Time (US & Canada)" do
record = Topic.new(id: 1)
record.written_on = "Jan 01 00:00:00 2014"
@@ -676,7 +707,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
end
- def test_setting_time_zone_aware_time_in_current_time_zone
+ test "setting a time zone-aware time in the current time zone" do
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new
time_string = "10:00:00"
@@ -686,12 +717,12 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal expected_time, record.bonus_time
assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.bonus_time.time_zone
- record.bonus_time = ''
+ record.bonus_time = ""
assert_nil record.bonus_time
end
end
- def test_setting_time_zone_aware_time_with_dst
+ test "setting a time zone-aware time with DST" do
in_time_zone "Pacific Time (US & Canada)" do
current_time = Time.zone.local(2014, 06, 15, 10)
record = @target.new(bonus_time: current_time)
@@ -705,19 +736,36 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
end
- def test_removing_time_zone_aware_types
+ test "setting invalid string to a zone-aware time attribute" do
+ in_time_zone "Pacific Time (US & Canada)" do
+ record = @target.new
+ time_string = "ABC"
+
+ record.bonus_time = time_string
+ assert_nil record.bonus_time
+ end
+ end
+
+ test "removing time zone-aware types" do
with_time_zone_aware_types(:datetime) do
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new(bonus_time: "10:00:00")
expected_time = Time.utc(2000, 01, 01, 10)
assert_equal expected_time, record.bonus_time
- assert record.bonus_time.utc?
+ assert_predicate record.bonus_time, :utc?
end
end
end
- def test_setting_time_zone_conversion_for_attributes_should_write_value_on_class_variable
+ 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_nil record.bonus_time
+ end
+ end
+
+ test "setting a time_zone_conversion_for_attributes should write the value on a class variable" do
Topic.skip_time_zone_conversion_for_attributes = [:field_a]
Minimalistic.skip_time_zone_conversion_for_attributes = [:field_b]
@@ -725,44 +773,44 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal [:field_b], Minimalistic.skip_time_zone_conversion_for_attributes
end
- def test_read_attributes_respect_access_control
+ test "attribute readers respect access control" do
privatize("title")
- topic = @target.new(:title => "The pros and cons of programming naked.")
- assert !topic.respond_to?(:title)
+ topic = @target.new(title: "The pros and cons of programming naked.")
+ assert_not_respond_to topic, :title
exception = assert_raise(NoMethodError) { topic.title }
- assert exception.message.include?("private method")
+ assert_includes exception.message, "private method"
assert_equal "I'm private", topic.send(:title)
end
- def test_write_attributes_respect_access_control
+ test "attribute writers respect access control" do
privatize("title=(value)")
topic = @target.new
- assert !topic.respond_to?(:title=)
- exception = assert_raise(NoMethodError) { topic.title = "Pants"}
- assert exception.message.include?("private method")
+ assert_not_respond_to topic, :title=
+ exception = assert_raise(NoMethodError) { topic.title = "Pants" }
+ assert_includes exception.message, "private method"
topic.send(:title=, "Very large pants")
end
- def test_question_attributes_respect_access_control
+ test "attribute predicates respect access control" do
privatize("title?")
- topic = @target.new(:title => "Isaac Newton's pants")
- assert !topic.respond_to?(:title?)
+ topic = @target.new(title: "Isaac Newton's pants")
+ assert_not_respond_to topic, :title?
exception = assert_raise(NoMethodError) { topic.title? }
- assert exception.message.include?("private method")
+ assert_includes exception.message, "private method"
assert topic.send(:title?)
end
- def test_bulk_update_respects_access_control
+ test "bulk updates respect access control" do
privatize("title=(value)")
- assert_raise(ActiveRecord::UnknownAttributeError) { @target.new(:title => "Rants about pants") }
- assert_raise(ActiveRecord::UnknownAttributeError) { @target.new.attributes = { :title => "Ants in pants" } }
+ assert_raise(ActiveRecord::UnknownAttributeError) { @target.new(title: "Rants about pants") }
+ assert_raise(ActiveRecord::UnknownAttributeError) { @target.new.attributes = { title: "Ants in pants" } }
end
- def test_bulk_update_raise_unknown_attribute_error
+ test "bulk update raises ActiveRecord::UnknownAttributeError" do
error = assert_raises(ActiveRecord::UnknownAttributeError) {
Topic.new(hello: "world")
}
@@ -771,43 +819,45 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal "unknown attribute 'hello' for Topic.", error.message
end
- def test_methods_override_in_multi_level_subclass
+ test "method overrides in multi-level subclasses" do
klass = Class.new(Developer) do
def name
"dev:#{read_attribute(:name)}"
end
end
- 2.times { klass = Class.new klass }
- dev = klass.new(name: 'arthurnn')
+ 2.times { klass = Class.new(klass) }
+ dev = klass.new(name: "arthurnn")
dev.save!
- assert_equal 'dev:arthurnn', dev.reload.name
+ assert_equal "dev:arthurnn", dev.reload.name
end
- def test_global_methods_are_overwritten
+ test "global methods are overwritten" do
klass = Class.new(ActiveRecord::Base) do
- self.table_name = 'computers'
+ self.table_name = "computers"
end
- assert !klass.instance_method_already_implemented?(:system)
+ assert_not klass.instance_method_already_implemented?(:system)
computer = klass.new
assert_nil computer.system
end
- def test_global_methods_are_overwritte_when_subclassing
- klass = Class.new(ActiveRecord::Base) { self.abstract_class = true }
+ test "global methods are overwritten when subclassing" do
+ klass = Class.new(ActiveRecord::Base) do
+ self.abstract_class = true
+ end
subklass = Class.new(klass) do
- self.table_name = 'computers'
+ self.table_name = "computers"
end
- assert !klass.instance_method_already_implemented?(:system)
- assert !subklass.instance_method_already_implemented?(:system)
+ assert_not klass.instance_method_already_implemented?(:system)
+ assert_not subklass.instance_method_already_implemented?(:system)
computer = subklass.new
assert_nil computer.system
end
- def test_instance_method_should_be_defined_on_the_base_class
+ test "instance methods should be defined on the base class" do
subklass = Class.new(Topic)
Topic.define_attribute_methods
@@ -823,14 +873,21 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert subklass.method_defined?(:id), "subklass is missing id method"
end
- def test_read_attribute_with_nil_should_not_asplode
- assert_equal nil, Topic.new.read_attribute(nil)
+ 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
# If B < A, and A defines an accessor for 'foo', we don't want to override
# that by defining a 'foo' method in the generated methods module for B.
# (That module will be inserted between the two, e.g. [B, <GeneratedAttributes>, A].)
- def test_inherited_custom_accessors
+ test "inherited custom accessors" do
klass = new_topic_like_ar_class do
self.abstract_class = true
def title; "omg"; end
@@ -846,9 +903,9 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal "lol", topic.author_name
end
- def test_inherited_custom_accessors_with_reserved_names
+ test "inherited custom accessors with reserved names" do
klass = Class.new(ActiveRecord::Base) do
- self.table_name = 'computers'
+ self.table_name = "computers"
self.abstract_class = true
def system; "omg"; end
def system=(val); self.developer = val; end
@@ -864,18 +921,18 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal 99, computer.developer
end
- def test_on_the_fly_super_invokable_generated_attribute_methods_via_method_missing
+ test "on_the_fly_super_invokable_generated_attribute_methods_via_method_missing" do
klass = new_topic_like_ar_class do
def title
- super + '!'
+ super + "!"
end
end
real_topic = topics(:first)
- assert_equal real_topic.title + '!', klass.find(real_topic.id).title
+ assert_equal real_topic.title + "!", klass.find(real_topic.id).title
end
- def test_on_the_fly_super_invokable_generated_predicate_attribute_methods_via_method_missing
+ test "on-the-fly super-invokable generated attribute predicates via method_missing" do
klass = new_topic_like_ar_class do
def title?
!super
@@ -886,7 +943,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal !real_topic.title?, klass.find(real_topic.id).title?
end
- def test_calling_super_when_parent_does_not_define_method_raises_error
+ test "calling super when the parent does not define method raises NoMethodError" do
klass = new_topic_like_ar_class do
def some_method_that_is_not_on_super
super
@@ -898,46 +955,46 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
end
- def test_attribute_method?
+ test "attribute_method?" do
assert @target.attribute_method?(:title)
assert @target.attribute_method?(:title=)
assert_not @target.attribute_method?(:wibble)
end
- def test_attribute_method_returns_false_if_table_does_not_exist
- @target.table_name = 'wibble'
+ test "attribute_method? returns false if the table does not exist" do
+ @target.table_name = "wibble"
assert_not @target.attribute_method?(:title)
end
- def test_attribute_names_on_new_record
+ test "attribute_names on a new record" do
model = @target.new
assert_equal @target.column_names, model.attribute_names
end
- def test_attribute_names_on_queried_record
+ test "attribute_names on a queried record" do
model = @target.last!
assert_equal @target.column_names, model.attribute_names
end
- def test_attribute_names_with_custom_select
- model = @target.select('id').last!
+ test "attribute_names with a custom select" do
+ model = @target.select("id").last!
- assert_equal ['id'], model.attribute_names
- # Sanity check, make sure other columns exist
- assert_not_equal ['id'], @target.column_names
+ assert_equal ["id"], model.attribute_names
+ # Sanity check, make sure other columns exist.
+ assert_not_equal ["id"], @target.column_names
end
- def test_came_from_user
+ test "came_from_user?" do
model = @target.first
- assert_not model.id_came_from_user?
+ assert_not_predicate model, :id_came_from_user?
model.id = "omg"
- assert model.id_came_from_user?
+ assert_predicate model, :id_came_from_user?
end
- def test_accessed_fields
+ test "accessed_fields" do
model = @target.first
assert_equal [], model.accessed_fields
@@ -947,40 +1004,37 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal ["title"], model.accessed_fields
end
- private
-
- def new_topic_like_ar_class(&block)
- klass = Class.new(ActiveRecord::Base) do
- self.table_name = 'topics'
- class_eval(&block)
- end
-
- assert_empty klass.generated_attribute_methods.instance_methods(false)
- klass
+ test "generated attribute methods ancestors have correct class" do
+ mod = Topic.send(:generated_attribute_methods)
+ assert_match %r(GeneratedAttributeMethods), mod.inspect
end
- def with_time_zone_aware_types(*types)
- old_types = ActiveRecord::Base.time_zone_aware_types
- ActiveRecord::Base.time_zone_aware_types = types
- yield
- ensure
- ActiveRecord::Base.time_zone_aware_types = old_types
- end
+ private
- def cached_columns
- Topic.columns.map(&:name)
- end
+ def new_topic_like_ar_class(&block)
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "topics"
+ class_eval(&block)
+ end
- def time_related_columns_on_topic
- Topic.columns.select { |c| [:time, :date, :datetime, :timestamp].include?(c.type) }
- end
+ assert_empty klass.send(:generated_attribute_methods).instance_methods(false)
+ klass
+ end
- def privatize(method_signature)
- @target.class_eval(<<-private_method, __FILE__, __LINE__ + 1)
- private
- def #{method_signature}
- "I'm private"
- end
- private_method
- end
+ def with_time_zone_aware_types(*types)
+ old_types = ActiveRecord::Base.time_zone_aware_types
+ ActiveRecord::Base.time_zone_aware_types = types
+ yield
+ ensure
+ ActiveRecord::Base.time_zone_aware_types = old_types
+ end
+
+ def privatize(method_signature)
+ @target.class_eval(<<-private_method, __FILE__, __LINE__ + 1)
+ private
+ def #{method_signature}
+ "I'm private"
+ end
+ private_method
+ end
end
diff --git a/activerecord/test/cases/attribute_set_test.rb b/activerecord/test/cases/attribute_set_test.rb
deleted file mode 100644
index 9d927481ec..0000000000
--- a/activerecord/test/cases/attribute_set_test.rb
+++ /dev/null
@@ -1,211 +0,0 @@
-require 'cases/helper'
-
-module ActiveRecord
- class AttributeSetTest < ActiveRecord::TestCase
- test "building a new set from raw attributes" do
- builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
- attributes = builder.build_from_database(foo: '1.1', bar: '2.2')
-
- assert_equal 1, attributes[:foo].value
- assert_equal 2.2, attributes[:bar].value
- assert_equal :foo, attributes[:foo].name
- assert_equal :bar, attributes[:bar].name
- end
-
- test "building with custom types" do
- builder = AttributeSet::Builder.new(foo: Type::Float.new)
- attributes = builder.build_from_database({ foo: '3.3', bar: '4.4' }, { bar: Type::Integer.new })
-
- assert_equal 3.3, attributes[:foo].value
- assert_equal 4, attributes[:bar].value
- end
-
- test "[] returns a null object" do
- builder = AttributeSet::Builder.new(foo: Type::Float.new)
- 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_equal :bar, attributes[:bar].name
- end
-
- test "duping creates a new hash and dups each attribute" do
- builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new)
- attributes = builder.build_from_database(foo: 1, bar: 'foo')
-
- # Ensure the type cast value is cached
- attributes[:foo].value
- attributes[:bar].value
-
- duped = attributes.dup
- duped.write_from_database(:foo, 2)
- duped[:bar].value << 'bar'
-
- assert_equal 1, attributes[:foo].value
- assert_equal 2, duped[:foo].value
- assert_equal 'foo', attributes[:bar].value
- assert_equal 'foobar', duped[:bar].value
- end
-
- test "freezing cloned set does not freeze original" do
- attributes = AttributeSet.new({})
- clone = attributes.clone
-
- clone.freeze
-
- assert clone.frozen?
- assert_not attributes.frozen?
- end
-
- test "to_hash returns a hash of the type cast values" do
- builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
- attributes = builder.build_from_database(foo: '1.1', bar: '2.2')
-
- assert_equal({ foo: 1, bar: 2.2 }, attributes.to_hash)
- assert_equal({ foo: 1, bar: 2.2 }, attributes.to_h)
- end
-
- test "to_hash maintains order" do
- builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
- attributes = builder.build_from_database(foo: '2.2', bar: '3.3')
-
- attributes[:bar]
- hash = attributes.to_h
-
- assert_equal [[:foo, 2], [:bar, 3.3]], hash.to_a
- end
-
- test "values_before_type_cast" do
- builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new)
- attributes = builder.build_from_database(foo: '1.1', bar: '2.2')
-
- assert_equal({ foo: '1.1', bar: '2.2' }, attributes.values_before_type_cast)
- end
-
- test "known columns are built with uninitialized attributes" do
- attributes = attributes_with_uninitialized_key
- assert attributes[:foo].initialized?
- assert_not attributes[:bar].initialized?
- end
-
- test "uninitialized attributes are not included in the attributes hash" do
- attributes = attributes_with_uninitialized_key
- assert_equal({ foo: 1 }, attributes.to_hash)
- end
-
- test "uninitialized attributes are not included in keys" do
- attributes = attributes_with_uninitialized_key
- assert_equal [:foo], attributes.keys
- end
-
- test "uninitialized attributes return false for key?" do
- attributes = attributes_with_uninitialized_key
- assert attributes.key?(:foo)
- assert_not attributes.key?(:bar)
- end
-
- test "unknown attributes return false for key?" do
- attributes = attributes_with_uninitialized_key
- assert_not attributes.key?(:wibble)
- end
-
- test "fetch_value returns the value for the given initialized attribute" do
- builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
- attributes = builder.build_from_database(foo: '1.1', bar: '2.2')
-
- assert_equal 1, attributes.fetch_value(:foo)
- assert_equal 2.2, attributes.fetch_value(:bar)
- end
-
- test "fetch_value returns nil for unknown attributes" do
- attributes = attributes_with_uninitialized_key
- assert_nil attributes.fetch_value(:wibble) { "hello" }
- end
-
- test "fetch_value returns nil for unknown attributes when types has a default" do
- types = Hash.new(Type::Value.new)
- builder = AttributeSet::Builder.new(types)
- attributes = builder.build_from_database
-
- assert_nil attributes.fetch_value(:wibble) { "hello" }
- end
-
- test "fetch_value uses the given block for uninitialized attributes" do
- attributes = attributes_with_uninitialized_key
- value = attributes.fetch_value(:bar) { |n| n.to_s + '!' }
- assert_equal 'bar!', value
- end
-
- test "fetch_value returns nil for uninitialized attributes if no block is given" do
- attributes = attributes_with_uninitialized_key
- assert_nil attributes.fetch_value(:bar)
- end
-
- test "the primary_key is always initialized" do
- builder = AttributeSet::Builder.new({ foo: Type::Integer.new }, :foo)
- attributes = builder.build_from_database
-
- assert attributes.key?(:foo)
- assert_equal [:foo], attributes.keys
- assert attributes[:foo].initialized?
- end
-
- class MyType
- def cast(value)
- return if value.nil?
- value + " from user"
- end
-
- def deserialize(value)
- return if value.nil?
- value + " from database"
- end
- end
-
- test "write_from_database sets the attribute with database typecasting" do
- builder = AttributeSet::Builder.new(foo: MyType.new)
- attributes = builder.build_from_database
-
- assert_nil attributes.fetch_value(:foo)
-
- attributes.write_from_database(:foo, "value")
-
- assert_equal "value from database", attributes.fetch_value(:foo)
- end
-
- test "write_from_user sets the attribute with user typecasting" do
- builder = AttributeSet::Builder.new(foo: MyType.new)
- attributes = builder.build_from_database
-
- assert_nil attributes.fetch_value(:foo)
-
- attributes.write_from_user(:foo, "value")
-
- assert_equal "value from user", attributes.fetch_value(:foo)
- end
-
- def attributes_with_uninitialized_key
- builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
- builder.build_from_database(foo: '1.1')
- end
-
- test "freezing doesn't prevent the set from materializing" do
- builder = AttributeSet::Builder.new(foo: Type::String.new)
- attributes = builder.build_from_database(foo: "1")
-
- attributes.freeze
- assert_equal({ foo: "1" }, attributes.to_hash)
- end
-
- test "#accessed_attributes returns only attributes which have been read" do
- builder = AttributeSet::Builder.new(foo: Type::Value.new, bar: Type::Value.new)
- attributes = builder.build_from_database(foo: "1", bar: "2")
-
- assert_equal [], attributes.accessed
-
- attributes.fetch_value(:foo)
-
- assert_equal [:foo], attributes.accessed
- end
- end
-end
diff --git a/activerecord/test/cases/attribute_test.rb b/activerecord/test/cases/attribute_test.rb
deleted file mode 100644
index aa419c7a67..0000000000
--- a/activerecord/test/cases/attribute_test.rb
+++ /dev/null
@@ -1,192 +0,0 @@
-require 'cases/helper'
-require 'minitest/mock'
-
-module ActiveRecord
- class AttributeTest < ActiveRecord::TestCase
- setup do
- @type = Minitest::Mock.new
- @type.expect(:==, false, [false])
- end
-
- teardown do
- assert @type.verify
- end
-
- test "from_database + read type casts from database" do
- @type.expect(:deserialize, 'type cast from database', ['a value'])
- attribute = Attribute.from_database(nil, 'a value', @type)
-
- type_cast_value = attribute.value
-
- assert_equal 'type cast from database', type_cast_value
- end
-
- test "from_user + read type casts from user" do
- @type.expect(:cast, 'type cast from user', ['a value'])
- attribute = Attribute.from_user(nil, 'a value', @type)
-
- type_cast_value = attribute.value
-
- assert_equal 'type cast from user', type_cast_value
- end
-
- test "reading memoizes the value" do
- @type.expect(:deserialize, 'from the database', ['whatever'])
- attribute = Attribute.from_database(nil, 'whatever', @type)
-
- type_cast_value = attribute.value
- second_read = attribute.value
-
- assert_equal 'from the database', type_cast_value
- assert_same type_cast_value, second_read
- end
-
- test "reading memoizes falsy values" do
- @type.expect(:deserialize, false, ['whatever'])
- attribute = Attribute.from_database(nil, 'whatever', @type)
-
- attribute.value
- attribute.value
- end
-
- test "read_before_typecast returns the given value" do
- attribute = Attribute.from_database(nil, 'raw value', @type)
-
- raw_value = attribute.value_before_type_cast
-
- assert_equal 'raw value', raw_value
- end
-
- test "from_database + read_for_database type casts to and from database" do
- @type.expect(:deserialize, 'read from database', ['whatever'])
- @type.expect(:serialize, 'ready for database', ['read from database'])
- attribute = Attribute.from_database(nil, 'whatever', @type)
-
- serialize = attribute.value_for_database
-
- assert_equal 'ready for database', serialize
- end
-
- test "from_user + read_for_database type casts from the user to the database" do
- @type.expect(:cast, 'read from user', ['whatever'])
- @type.expect(:serialize, 'ready for database', ['read from user'])
- attribute = Attribute.from_user(nil, 'whatever', @type)
-
- serialize = attribute.value_for_database
-
- assert_equal 'ready for database', serialize
- end
-
- test "duping dups the value" do
- @type.expect(:deserialize, 'type cast', ['a value'])
- attribute = Attribute.from_database(nil, 'a value', @type)
-
- value_from_orig = attribute.value
- value_from_clone = attribute.dup.value
- value_from_orig << ' foo'
-
- assert_equal 'type cast foo', value_from_orig
- assert_equal 'type cast', value_from_clone
- end
-
- test "duping does not dup the value if it is not dupable" do
- @type.expect(:deserialize, false, ['a value'])
- attribute = Attribute.from_database(nil, 'a value', @type)
-
- assert_same attribute.value, attribute.dup.value
- end
-
- test "duping does not eagerly type cast if we have not yet type cast" do
- attribute = Attribute.from_database(nil, 'a value', @type)
- attribute.dup
- end
-
- class MyType
- def cast(value)
- value + " from user"
- end
-
- def deserialize(value)
- value + " from database"
- end
- end
-
- test "with_value_from_user returns a new attribute with the value from the user" do
- old = Attribute.from_database(nil, "old", MyType.new)
- new = old.with_value_from_user("new")
-
- assert_equal "old from database", old.value
- assert_equal "new from user", new.value
- end
-
- test "with_value_from_database returns a new attribute with the value from the database" do
- old = Attribute.from_user(nil, "old", MyType.new)
- new = old.with_value_from_database("new")
-
- assert_equal "old from user", old.value
- assert_equal "new from database", new.value
- end
-
- test "uninitialized attributes yield their name if a block is given to value" do
- block = proc { |name| name.to_s + "!" }
- foo = Attribute.uninitialized(:foo, nil)
- bar = Attribute.uninitialized(:bar, nil)
-
- assert_equal "foo!", foo.value(&block)
- assert_equal "bar!", bar.value(&block)
- end
-
- test "uninitialized attributes have no value" do
- assert_nil Attribute.uninitialized(:foo, nil).value
- end
-
- test "attributes equal other attributes with the same constructor arguments" do
- first = Attribute.from_database(:foo, 1, Type::Integer.new)
- second = Attribute.from_database(:foo, 1, Type::Integer.new)
- assert_equal first, second
- end
-
- test "attributes do not equal attributes with different names" do
- first = Attribute.from_database(:foo, 1, Type::Integer.new)
- second = Attribute.from_database(:bar, 1, Type::Integer.new)
- assert_not_equal first, second
- end
-
- test "attributes do not equal attributes with different types" do
- first = Attribute.from_database(:foo, 1, Type::Integer.new)
- second = Attribute.from_database(:foo, 1, Type::Float.new)
- assert_not_equal first, second
- end
-
- test "attributes do not equal attributes with different values" do
- first = Attribute.from_database(:foo, 1, Type::Integer.new)
- second = Attribute.from_database(:foo, 2, Type::Integer.new)
- assert_not_equal first, second
- end
-
- test "attributes do not equal attributes of other classes" do
- first = Attribute.from_database(:foo, 1, Type::Integer.new)
- second = Attribute.from_user(:foo, 1, Type::Integer.new)
- assert_not_equal first, second
- end
-
- test "an attribute has not been read by default" do
- attribute = Attribute.from_database(:foo, 1, Type::Value.new)
- assert_not attribute.has_been_read?
- end
-
- test "an attribute has been read when its value is calculated" do
- attribute = Attribute.from_database(:foo, 1, Type::Value.new)
- attribute.value
- assert attribute.has_been_read?
- end
-
- test "an attribute can not be mutated if it has not been read,
- and skips expensive calculations" do
- type_which_raises_from_all_methods = Object.new
- attribute = Attribute.from_database(:foo, "bar", type_which_raises_from_all_methods)
-
- assert_not attribute.changed_in_place_from?("bar")
- end
- end
-end
diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb
index eeda9335ad..2632aec7ab 100644
--- a/activerecord/test/cases/attributes_test.rb
+++ b/activerecord/test/cases/attributes_test.rb
@@ -1,10 +1,12 @@
-require 'cases/helper'
+# frozen_string_literal: true
+
+require "cases/helper"
class OverloadedType < ActiveRecord::Base
attribute :overloaded_float, :integer
attribute :overloaded_string_with_limit, :string, limit: 50
attribute :non_existent_decimal, :decimal
- attribute :string_with_default, :string, default: 'the overloaded default'
+ attribute :string_with_default, :string, default: "the overloaded default"
end
class ChildOfOverloadedType < OverloadedType
@@ -15,7 +17,7 @@ class GrandchildOfOverloadedType < ChildOfOverloadedType
end
class UnoverloadedType < ActiveRecord::Base
- self.table_name = 'overloaded_types'
+ self.table_name = "overloaded_types"
end
module ActiveRecord
@@ -38,51 +40,60 @@ module ActiveRecord
data.reload
assert_equal 2, data.overloaded_float
- assert_kind_of Fixnum, OverloadedType.last.overloaded_float
+ assert_kind_of Integer, OverloadedType.last.overloaded_float
assert_equal 2.0, UnoverloadedType.last.overloaded_float
assert_kind_of Float, UnoverloadedType.last.overloaded_float
end
test "properties assigned in constructor" do
- data = OverloadedType.new(overloaded_float: '3.3')
+ data = OverloadedType.new(overloaded_float: "3.3")
assert_equal 3, data.overloaded_float
end
test "overloaded properties with limit" do
- assert_equal 50, OverloadedType.type_for_attribute('overloaded_string_with_limit').limit
- assert_equal 255, UnoverloadedType.type_for_attribute('overloaded_string_with_limit').limit
+ assert_equal 50, OverloadedType.type_for_attribute("overloaded_string_with_limit").limit
+ assert_equal 255, UnoverloadedType.type_for_attribute("overloaded_string_with_limit").limit
end
test "nonexistent attribute" do
data = OverloadedType.new(non_existent_decimal: 1)
- assert_equal BigDecimal.new(1), data.non_existent_decimal
+ assert_equal BigDecimal(1), data.non_existent_decimal
assert_raise ActiveRecord::UnknownAttributeError do
UnoverloadedType.new(non_existent_decimal: 1)
end
end
+ test "model with nonexistent attribute with default value can be saved" do
+ klass = Class.new(OverloadedType) do
+ attribute :non_existent_string_with_default, :string, default: "nonexistent"
+ end
+
+ model = klass.new
+ assert model.save
+ end
+
test "changing defaults" do
data = OverloadedType.new
unoverloaded_data = UnoverloadedType.new
- assert_equal 'the overloaded default', data.string_with_default
- assert_equal 'the original default', unoverloaded_data.string_with_default
+ assert_equal "the overloaded default", data.string_with_default
+ assert_equal "the original default", unoverloaded_data.string_with_default
end
test "defaults are not touched on the columns" do
- assert_equal 'the original default', OverloadedType.columns_hash['string_with_default'].default
+ assert_equal "the original default", OverloadedType.columns_hash["string_with_default"].default
end
test "children inherit custom properties" do
- data = ChildOfOverloadedType.new(overloaded_float: '4.4')
+ data = ChildOfOverloadedType.new(overloaded_float: "4.4")
assert_equal 4, data.overloaded_float
end
test "children can override parents" do
- data = GrandchildOfOverloadedType.new(overloaded_float: '4.4')
+ data = GrandchildOfOverloadedType.new(overloaded_float: "4.4")
assert_equal 4.4, data.overloaded_float
end
@@ -97,13 +108,15 @@ module ActiveRecord
assert_equal 6, klass.attribute_types.length
assert_equal 6, klass.column_defaults.length
- assert_not klass.attribute_types.include?('wibble')
+ assert_equal 6, klass.attribute_names.length
+ assert_not klass.attribute_types.include?("wibble")
klass.attribute :wibble, Type::Value.new
assert_equal 7, klass.attribute_types.length
assert_equal 7, klass.column_defaults.length
- assert klass.attribute_types.include?('wibble')
+ assert_equal 7, klass.attribute_names.length
+ assert_includes klass.attribute_types, "wibble"
end
test "the given default value is cast from user" do
@@ -135,6 +148,31 @@ module ActiveRecord
assert_equal 2, klass.new.counter
end
+ test "procs for default values are evaluated even after column_defaults is called" do
+ klass = Class.new(OverloadedType) do
+ @@counter = 0
+ attribute :counter, :integer, default: -> { @@counter += 1 }
+ end
+
+ assert_equal 1, klass.new.counter
+
+ # column_defaults will increment the counter since the proc is called
+ klass.column_defaults
+
+ assert_equal 3, klass.new.counter
+ end
+
+ test "procs are memoized before type casting" do
+ klass = Class.new(OverloadedType) do
+ @@counter = 0
+ attribute :counter, :integer, default: -> { @@counter += 1 }
+ end
+
+ model = klass.new
+ assert_equal 1, model.counter_before_type_cast
+ assert_equal 1, model.counter_before_type_cast
+ end
+
test "user provided defaults are persisted even if unchanged" do
model = OverloadedType.create!
@@ -142,7 +180,7 @@ module ActiveRecord
end
if current_adapter?(:PostgreSQLAdapter)
- test "arrays types can be specified" do
+ test "array types can be specified" do
klass = Class.new(OverloadedType) do
attribute :my_array, :string, limit: 50, array: true
attribute :my_int_array, :integer, array: true
@@ -152,7 +190,7 @@ module ActiveRecord
Type::String.new(limit: 50))
int_array = ConnectionAdapters::PostgreSQL::OID::Array.new(
Type::Integer.new)
- refute_equal string_array, int_array
+ assert_not_equal string_array, int_array
assert_equal string_array, klass.type_for_attribute("my_array")
assert_equal int_array, klass.type_for_attribute("my_int_array")
end
@@ -167,10 +205,81 @@ module ActiveRecord
Type::String.new(limit: 50))
int_range = ConnectionAdapters::PostgreSQL::OID::Range.new(
Type::Integer.new)
- refute_equal string_range, int_range
+ assert_not_equal string_range, int_range
assert_equal string_range, klass.type_for_attribute("my_range")
assert_equal int_range, klass.type_for_attribute("my_int_range")
end
end
+
+ test "attributes added after subclasses load are inherited" do
+ parent = Class.new(ActiveRecord::Base) do
+ self.table_name = "topics"
+ end
+
+ child = Class.new(parent)
+ child.new # => force a schema load
+
+ parent.attribute(:foo, Type::Value.new)
+
+ assert_equal(:bar, child.new(foo: :bar).foo)
+ end
+
+ test "attributes not backed by database columns are not dirty when unchanged" do
+ assert_not_predicate OverloadedType.new, :non_existent_decimal_changed?
+ end
+
+ test "attributes not backed by database columns are always initialized" do
+ OverloadedType.create!
+ model = OverloadedType.first
+
+ assert_nil model.non_existent_decimal
+ model.non_existent_decimal = "123"
+ assert_equal 123, model.non_existent_decimal
+ end
+
+ test "attributes not backed by database columns return the default on models loaded from database" do
+ child = Class.new(OverloadedType) do
+ attribute :non_existent_decimal, :decimal, default: 123
+ end
+ child.create!
+ model = child.first
+
+ assert_equal 123, model.non_existent_decimal
+ end
+
+ test "attributes not backed by database columns properly interact with mutation and dirty" do
+ child = Class.new(ActiveRecord::Base) do
+ self.table_name = "topics"
+ attribute :foo, :string, default: "lol"
+ end
+ child.create!
+ model = child.first
+
+ assert_equal "lol", model.foo
+
+ model.foo << "asdf"
+ assert_equal "lolasdf", model.foo
+ assert_predicate model, :foo_changed?
+
+ model.reload
+ assert_equal "lol", model.foo
+
+ model.foo = "lol"
+ assert_not_predicate model, :changed?
+ end
+
+ test "attributes not backed by database columns appear in inspect" do
+ inspection = OverloadedType.new.inspect
+
+ 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 80b5a0004d..88df0eed55 100644
--- a/activerecord/test/cases/autosave_association_test.rb
+++ b/activerecord/test/cases/autosave_association_test.rb
@@ -1,54 +1,59 @@
-require 'cases/helper'
-require 'models/bird'
-require 'models/comment'
-require 'models/company'
-require 'models/customer'
-require 'models/developer'
-require 'models/computer'
-require 'models/invoice'
-require 'models/line_item'
-require 'models/order'
-require 'models/parrot'
-require 'models/person'
-require 'models/pirate'
-require 'models/post'
-require 'models/reader'
-require 'models/ship'
-require 'models/ship_part'
-require 'models/tag'
-require 'models/tagging'
-require 'models/treasure'
-require 'models/eye'
-require 'models/electron'
-require 'models/molecule'
-require 'models/member'
-require 'models/member_detail'
-require 'models/organization'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/bird"
+require "models/post"
+require "models/comment"
+require "models/company"
+require "models/contract"
+require "models/customer"
+require "models/developer"
+require "models/computer"
+require "models/invoice"
+require "models/line_item"
+require "models/order"
+require "models/parrot"
+require "models/pirate"
+require "models/project"
+require "models/ship"
+require "models/ship_part"
+require "models/tag"
+require "models/tagging"
+require "models/treasure"
+require "models/eye"
+require "models/electron"
+require "models/molecule"
+require "models/member"
+require "models/member_detail"
+require "models/organization"
+require "models/guitar"
+require "models/tuning_peg"
+require "models/reply"
class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase
def test_autosave_validation
person = Class.new(ActiveRecord::Base) {
- self.table_name = 'people'
- validate :should_be_cool, :on => :create
- def self.name; 'Person'; end
+ self.table_name = "people"
+ validate :should_be_cool, on: :create
+ def self.name; "Person"; end
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"
- def self.name; 'Reference'; end
+ def self.name; "Reference"; end
belongs_to :person, autosave: true, anonymous_class: person
}
- u = person.create!(first_name: 'cool')
- u.update_attributes!(first_name: 'nah') # still valid because validation only applies on 'create'
- assert reference.create!(person: u).persisted?
+ u = person.create!(first_name: "cool")
+ u.update!(first_name: "nah") # still valid because validation only applies on 'create'
+ assert_predicate reference.create!(person: u), :persisted?
end
def test_should_not_add_the_same_callbacks_multiple_times_for_has_one
@@ -67,56 +72,64 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase
assert_no_difference_when_adding_callbacks_twice_for Pirate, :parrots
end
+ def test_cyclic_autosaves_do_not_add_multiple_validations
+ ship = ShipWithoutNestedAttributes.new
+ ship.prisoners.build
+
+ assert_not_predicate ship, :valid?
+ assert_equal 1, ship.errors[:name].length
+ end
+
private
- def assert_no_difference_when_adding_callbacks_twice_for(model, association_name)
- reflection = model.reflect_on_association(association_name)
- assert_no_difference "callbacks_for_model(#{model.name}).length" do
- model.send(:add_autosave_association_callbacks, reflection)
+ def assert_no_difference_when_adding_callbacks_twice_for(model, association_name)
+ reflection = model.reflect_on_association(association_name)
+ assert_no_difference "callbacks_for_model(#{model.name}).length" do
+ model.send(:add_autosave_association_callbacks, reflection)
+ end
end
- end
- def callbacks_for_model(model)
- model.instance_variables.grep(/_callbacks$/).flat_map do |ivar|
- model.instance_variable_get(ivar)
+ def callbacks_for_model(model)
+ model.instance_variables.grep(/_callbacks$/).flat_map do |ivar|
+ model.instance_variable_get(ivar)
+ end
end
- end
end
class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
fixtures :companies, :accounts
def test_should_save_parent_but_not_invalid_child
- firm = Firm.new(:name => 'GlobalMegaCorp')
- assert firm.valid?
+ firm = Firm.new(name: "GlobalMegaCorp")
+ assert_predicate firm, :valid?
firm.build_account_using_primary_key
- assert !firm.build_account_using_primary_key.valid?
+ assert_not_predicate firm.build_account_using_primary_key, :valid?
assert firm.save
- assert !firm.account_using_primary_key.persisted?
+ assert_not_predicate firm.account_using_primary_key, :persisted?
end
def test_save_fails_for_invalid_has_one
firm = Firm.first
- assert firm.valid?
+ assert_predicate firm, :valid?
firm.build_account
- assert !firm.account.valid?
- assert !firm.valid?
- assert !firm.save
+ assert_not_predicate firm.account, :valid?
+ assert_not_predicate firm, :valid?
+ assert_not firm.save
assert_equal ["is invalid"], firm.errors["account"]
end
def test_save_succeeds_for_invalid_has_one_with_validate_false
firm = Firm.first
- assert firm.valid?
+ assert_predicate firm, :valid?
firm.build_unvalidated_account
- assert !firm.unvalidated_account.valid?
- assert firm.valid?
+ assert_not_predicate firm.unvalidated_account, :valid?
+ assert_predicate firm, :valid?
assert firm.save
end
@@ -125,10 +138,10 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas
account = firm.build_account("credit_limit" => 1000)
assert_equal account, firm.account
- assert !account.persisted?
+ assert_not_predicate account, :persisted?
assert firm.save
assert_equal account, firm.account
- assert account.persisted?
+ assert_predicate account, :persisted?
end
def test_build_before_either_saved
@@ -136,38 +149,40 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas
firm.account = account = Account.new("credit_limit" => 1000)
assert_equal account, firm.account
- assert !account.persisted?
+ assert_not_predicate account, :persisted?
assert firm.save
assert_equal account, firm.account
- assert account.persisted?
+ assert_predicate account, :persisted?
end
def test_assignment_before_parent_saved
firm = Firm.new("name" => "GlobalMegaCorp")
firm.account = a = Account.find(1)
- assert !firm.persisted?
+ assert_not_predicate firm, :persisted?
assert_equal a, firm.account
assert firm.save
assert_equal a, firm.account
- assert_equal a, firm.account(true)
+ firm.association(:account).reload
+ assert_equal a, firm.account
end
def test_assignment_before_either_saved
firm = Firm.new("name" => "GlobalMegaCorp")
firm.account = a = Account.new("credit_limit" => 1000)
- assert !firm.persisted?
- assert !a.persisted?
+ assert_not_predicate firm, :persisted?
+ assert_not_predicate a, :persisted?
assert_equal a, firm.account
assert firm.save
- assert firm.persisted?
- assert a.persisted?
+ assert_predicate firm, :persisted?
+ assert_predicate a, :persisted?
+ assert_equal a, firm.account
+ firm.association(:account).reload
assert_equal a, firm.account
- assert_equal a, firm.account(true)
end
def test_not_resaved_when_unchanged
- firm = Firm.all.merge!(:includes => :account).first
- firm.name += '-changed'
+ firm = Firm.all.merge!(includes: :account).first
+ firm.name += "-changed"
assert_queries(1) { firm.save! }
firm = Firm.first
@@ -184,21 +199,21 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas
end
def test_callbacks_firing_order_on_create
- eye = Eye.create(:iris_attributes => {:color => 'honey'})
+ eye = Eye.create(iris_attributes: { color: "honey" })
assert_equal [true, false], eye.after_create_callbacks_stack
end
def test_callbacks_firing_order_on_update
- eye = Eye.create(iris_attributes: {color: 'honey'})
- eye.update(iris_attributes: {color: 'green'})
+ eye = Eye.create(iris_attributes: { color: "honey" })
+ eye.update(iris_attributes: { color: "green" })
assert_equal [true, false], eye.after_update_callbacks_stack
end
def test_callbacks_firing_order_on_save
- eye = Eye.create(iris_attributes: {color: 'honey'})
+ eye = Eye.create(iris_attributes: { color: "honey" })
assert_equal [false, false], eye.after_save_callbacks_stack
- eye.update(iris_attributes: {color: 'blue'})
+ eye.update(iris_attributes: { color: "blue" })
assert_equal [false, false, false, false], eye.after_save_callbacks_stack
end
end
@@ -207,34 +222,34 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test
fixtures :companies, :posts, :tags, :taggings
def test_should_save_parent_but_not_invalid_child
- client = Client.new(:name => 'Joe (the Plumber)')
- assert client.valid?
+ client = Client.new(name: "Joe (the Plumber)")
+ assert_predicate client, :valid?
client.build_firm
- assert !client.firm.valid?
+ assert_not_predicate client.firm, :valid?
assert client.save
- assert !client.firm.persisted?
+ assert_not_predicate client.firm, :persisted?
end
def test_save_fails_for_invalid_belongs_to
# Oracle saves empty string as NULL therefore :message changed to one space
- assert log = AuditLog.create(:developer_id => 0, :message => " ")
+ assert log = AuditLog.create(developer_id: 0, message: " ")
log.developer = Developer.new
- assert !log.developer.valid?
- assert !log.valid?
- assert !log.save
+ assert_not_predicate log.developer, :valid?
+ assert_not_predicate log, :valid?
+ assert_not log.save
assert_equal ["is invalid"], log.errors["developer"]
end
def test_save_succeeds_for_invalid_belongs_to_with_validate_false
# Oracle saves empty string as NULL therefore :message changed to one space
- assert log = AuditLog.create(:developer_id => 0, :message=> " ")
+ assert log = AuditLog.create(developer_id: 0, message: " ")
log.unvalidated_developer = Developer.new
- assert !log.unvalidated_developer.valid?
- assert log.valid?
+ assert_not_predicate log.unvalidated_developer, :valid?
+ assert_predicate log, :valid?
assert log.save
end
@@ -243,25 +258,27 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test
apple = Firm.new("name" => "Apple")
client.firm = apple
assert_equal apple, client.firm
- assert !apple.persisted?
+ assert_not_predicate apple, :persisted?
assert client.save
assert apple.save
- assert apple.persisted?
+ assert_predicate apple, :persisted?
+ assert_equal apple, client.firm
+ client.association(:firm).reload
assert_equal apple, client.firm
- assert_equal apple, client.firm(true)
end
def test_assignment_before_either_saved
final_cut = Client.new("name" => "Final Cut")
apple = Firm.new("name" => "Apple")
final_cut.firm = apple
- assert !final_cut.persisted?
- assert !apple.persisted?
+ assert_not_predicate final_cut, :persisted?
+ assert_not_predicate apple, :persisted?
assert final_cut.save
- assert final_cut.persisted?
- assert apple.persisted?
+ assert_predicate final_cut, :persisted?
+ assert_predicate apple, :persisted?
+ assert_equal apple, final_cut.firm
+ final_cut.association(:firm).reload
assert_equal apple, final_cut.firm
- assert_equal apple, final_cut.firm(true)
end
def test_store_two_association_with_one_save
@@ -348,77 +365,170 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test
def test_store_association_with_a_polymorphic_relationship
num_tagging = Tagging.count
- tags(:misc).create_tagging(:taggable => posts(:thinking))
+ tags(:misc).create_tagging(taggable: posts(:thinking))
assert_equal num_tagging + 1, Tagging.count
end
def test_build_and_then_save_parent_should_not_reload_target
client = Client.first
- apple = client.build_firm(:name => "Apple")
+ apple = client.build_firm(name: "Apple")
client.save!
assert_no_queries { assert_equal apple, client.firm }
end
def test_validation_does_not_validate_stale_association_target
- valid_developer = Developer.create!(:name => "Dude", :salary => 50_000)
+ valid_developer = Developer.create!(name: "Dude", salary: 50_000)
invalid_developer = Developer.new()
- auditlog = AuditLog.new(:message => "foo")
+ auditlog = AuditLog.new(message: "foo")
auditlog.developer = invalid_developer
auditlog.developer_id = valid_developer.id
- assert auditlog.valid?
+ assert_predicate auditlog, :valid?
end
end
class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttributes < ActiveRecord::TestCase
def test_invalid_adding_with_nested_attributes
molecule = Molecule.new
- valid_electron = Electron.new(name: 'electron')
+ valid_electron = Electron.new(name: "electron")
invalid_electron = Electron.new
molecule.electrons = [valid_electron, invalid_electron]
molecule.save
- assert_not invalid_electron.valid?
- assert valid_electron.valid?
- assert_not molecule.persisted?, 'Molecule should not be persisted when its electrons are invalid'
+ assert_not_predicate invalid_electron, :valid?
+ assert_predicate valid_electron, :valid?
+ assert_not molecule.persisted?, "Molecule should not be persisted when its electrons are invalid"
+ end
+
+ def test_errors_should_be_indexed_when_passed_as_array
+ guitar = Guitar.new
+ tuning_peg_valid = TuningPeg.new
+ tuning_peg_valid.pitch = 440.0
+ tuning_peg_invalid = TuningPeg.new
+
+ guitar.tuning_pegs = [tuning_peg_valid, tuning_peg_invalid]
+
+ assert_not_predicate tuning_peg_invalid, :valid?
+ assert_predicate tuning_peg_valid, :valid?
+ assert_not_predicate guitar, :valid?
+ assert_equal ["is not a number"], guitar.errors["tuning_pegs[1].pitch"]
+ assert_not_equal ["is not a number"], guitar.errors["tuning_pegs.pitch"]
+ end
+
+ def test_errors_should_be_indexed_when_global_flag_is_set
+ old_attribute_config = ActiveRecord::Base.index_nested_attribute_errors
+ ActiveRecord::Base.index_nested_attribute_errors = true
+
+ molecule = Molecule.new
+ valid_electron = Electron.new(name: "electron")
+ invalid_electron = Electron.new
+
+ molecule.electrons = [valid_electron, invalid_electron]
+
+ assert_not_predicate invalid_electron, :valid?
+ assert_predicate valid_electron, :valid?
+ assert_not_predicate molecule, :valid?
+ assert_equal ["can't be blank"], molecule.errors["electrons[1].name"]
+ assert_not_equal ["can't be blank"], molecule.errors["electrons.name"]
+ ensure
+ ActiveRecord::Base.index_nested_attribute_errors = old_attribute_config
+ end
+
+ def test_errors_details_should_be_set
+ molecule = Molecule.new
+ valid_electron = Electron.new(name: "electron")
+ invalid_electron = Electron.new
+
+ molecule.electrons = [valid_electron, invalid_electron]
+
+ assert_not_predicate invalid_electron, :valid?
+ assert_predicate valid_electron, :valid?
+ assert_not_predicate molecule, :valid?
+ assert_equal [{ error: :blank }], molecule.errors.details[:"electrons.name"]
+ end
+
+ def test_errors_details_should_be_indexed_when_passed_as_array
+ guitar = Guitar.new
+ tuning_peg_valid = TuningPeg.new
+ tuning_peg_valid.pitch = 440.0
+ tuning_peg_invalid = TuningPeg.new
+
+ guitar.tuning_pegs = [tuning_peg_valid, tuning_peg_invalid]
+
+ assert_not_predicate tuning_peg_invalid, :valid?
+ assert_predicate tuning_peg_valid, :valid?
+ assert_not_predicate guitar, :valid?
+ assert_equal [{ error: :not_a_number, value: nil }], guitar.errors.details[:"tuning_pegs[1].pitch"]
+ assert_equal [], guitar.errors.details[:"tuning_pegs.pitch"]
+ end
+
+ def test_errors_details_should_be_indexed_when_global_flag_is_set
+ old_attribute_config = ActiveRecord::Base.index_nested_attribute_errors
+ ActiveRecord::Base.index_nested_attribute_errors = true
+
+ molecule = Molecule.new
+ valid_electron = Electron.new(name: "electron")
+ invalid_electron = Electron.new
+
+ molecule.electrons = [valid_electron, invalid_electron]
+
+ assert_not_predicate invalid_electron, :valid?
+ assert_predicate valid_electron, :valid?
+ assert_not_predicate molecule, :valid?
+ assert_equal [{ error: :blank }], molecule.errors.details[:"electrons[1].name"]
+ assert_equal [], molecule.errors.details[:"electrons.name"]
+ ensure
+ ActiveRecord::Base.index_nested_attribute_errors = old_attribute_config
end
def test_valid_adding_with_nested_attributes
molecule = Molecule.new
- valid_electron = Electron.new(name: 'electron')
+ valid_electron = Electron.new(name: "electron")
molecule.electrons = [valid_electron]
molecule.save
- assert valid_electron.valid?
- assert molecule.persisted?
+ assert_predicate valid_electron, :valid?
+ assert_predicate molecule, :persisted?
assert_equal 1, molecule.electrons.count
end
end
class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCase
- fixtures :companies, :people
+ fixtures :companies, :developers
def test_invalid_adding
firm = Firm.find(1)
- assert !(firm.clients_of_firm << c = Client.new)
- assert !c.persisted?
- assert !firm.valid?
- assert !firm.save
- assert !c.persisted?
+ assert_not (firm.clients_of_firm << c = Client.new)
+ assert_not_predicate c, :persisted?
+ assert_not_predicate firm, :valid?
+ assert_not firm.save
+ assert_not_predicate c, :persisted?
end
def test_invalid_adding_before_save
new_firm = Firm.new("name" => "A New Firm, Inc")
new_firm.clients_of_firm.concat([c = Client.new, Client.new("name" => "Apple")])
- assert !c.persisted?
- assert !c.valid?
- assert !new_firm.valid?
- assert !new_firm.save
- assert !c.persisted?
- assert !new_firm.persisted?
+ assert_not_predicate c, :persisted?
+ assert_not_predicate c, :valid?
+ assert_not_predicate new_firm, :valid?
+ assert_not new_firm.save
+ assert_not_predicate c, :persisted?
+ assert_not_predicate new_firm, :persisted?
+ end
+
+ def test_adding_unsavable_association
+ new_firm = Firm.new("name" => "A New Firm, Inc")
+ client = new_firm.clients.new("name" => "Apple")
+ client.throw_on_save = true
+
+ assert_predicate client, :valid?
+ assert_predicate new_firm, :valid?
+ assert_not new_firm.save
+ assert_not_predicate new_firm, :persisted?
+ assert_not_predicate client, :persisted?
end
def test_invalid_adding_with_validate_false
@@ -426,10 +536,10 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
client = Client.new
firm.unvalidated_clients_of_firm << client
- assert firm.valid?
- assert !client.valid?
+ assert_predicate firm, :valid?
+ assert_not_predicate client, :valid?
assert firm.save
- assert !client.persisted?
+ assert_not_predicate client, :persisted?
end
def test_valid_adding_with_validate_false
@@ -438,25 +548,53 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
firm = Firm.first
client = Client.new("name" => "Apple")
- assert firm.valid?
- assert client.valid?
- assert !client.persisted?
+ assert_predicate firm, :valid?
+ assert_predicate client, :valid?
+ assert_not_predicate client, :persisted?
firm.unvalidated_clients_of_firm << client
assert firm.save
- assert client.persisted?
+ assert_predicate client, :persisted?
assert_equal no_of_clients + 1, Client.count
end
+ def test_parent_should_save_children_record_with_foreign_key_validation_set_in_before_save_callback
+ company = NewlyContractedCompany.new(name: "test")
+
+ assert company.save
+ assert_not_empty company.reload.new_contracts
+ end
+
+ def test_parent_should_not_get_saved_with_duplicate_children_records
+ assert_no_difference "Reply.count" do
+ assert_no_difference "SillyUniqueReply.count" do
+ reply = Reply.new
+ reply.silly_unique_replies.build([
+ { content: "Best content" },
+ { content: "Best content" }
+ ])
+
+ assert_not reply.save
+ assert_equal ["is invalid"], reply.errors[:silly_unique_replies]
+ assert_empty reply.silly_unique_replies.first.errors
+
+ assert_equal(
+ ["has already been taken"],
+ reply.silly_unique_replies.last.errors[:content]
+ )
+ end
+ end
+ end
+
def test_invalid_build
new_client = companies(:first_firm).clients_of_firm.build
- assert !new_client.persisted?
- assert !new_client.valid?
+ assert_not_predicate new_client, :persisted?
+ assert_not_predicate new_client, :valid?
assert_equal new_client, companies(:first_firm).clients_of_firm.last
- assert !companies(:first_firm).save
- assert !new_client.persisted?
- assert_equal 2, companies(:first_firm).clients_of_firm(true).size
+ assert_not companies(:first_firm).save
+ assert_not_predicate new_client, :persisted?
+ assert_equal 2, companies(:first_firm).clients_of_firm.reload.size
end
def test_adding_before_save
@@ -474,14 +612,14 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
assert_equal no_of_firms, Firm.count # Firm was not saved to database.
assert_equal no_of_clients, Client.count # Clients were not saved to database.
assert new_firm.save
- assert new_firm.persisted?
- assert c.persisted?
+ assert_predicate new_firm, :persisted?
+ assert_predicate c, :persisted?
assert_equal new_firm, c.firm
assert_equal no_of_firms + 1, Firm.count # Firm was saved to database.
assert_equal no_of_clients + 2, Client.count # Clients were saved to database.
assert_equal 2, new_firm.clients_of_firm.size
- assert_equal 2, new_firm.clients_of_firm(true).size
+ assert_equal 2, new_firm.clients_of_firm.reload.size
end
def test_assign_ids
@@ -490,60 +628,76 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
firm.save
firm.reload
assert_equal 2, firm.clients.length
- assert firm.clients.include?(companies(:second_client))
+ assert_includes firm.clients, companies(:second_client)
end
def test_assign_ids_for_through_a_belongs_to
- post = Post.new(:title => "Assigning IDs works!", :body => "You heard it here first, folks!")
- post.person_ids = [people(:david).id, people(:michael).id]
- post.save
- post.reload
- assert_equal 2, post.people.length
- assert post.people.include?(people(:david))
+ firm = Firm.new("name" => "Apple")
+ firm.developer_ids = [developers(:david).id, developers(:jamis).id]
+ firm.save
+ firm.reload
+ assert_equal 2, firm.developers.length
+ assert_includes firm.developers, developers(:david)
end
def test_build_before_save
company = companies(:first_firm)
- new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build("name" => "Another Client") }
- assert !company.clients_of_firm.loaded?
- company.name += '-changed'
+ # Load schema information so we don't query below if running just this test.
+ Client.define_attribute_methods
+
+ new_client = assert_no_queries { company.clients_of_firm.build("name" => "Another Client") }
+ assert_not_predicate company.clients_of_firm, :loaded?
+
+ company.name += "-changed"
assert_queries(2) { assert company.save }
- assert new_client.persisted?
- assert_equal 3, company.clients_of_firm(true).size
+ assert_predicate new_client, :persisted?
+ assert_equal 3, company.clients_of_firm.reload.size
end
def test_build_many_before_save
company = companies(:first_firm)
- assert_no_queries(ignore_none: false) { company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) }
- company.name += '-changed'
+ # Load schema information so we don't query below if running just this test.
+ Client.define_attribute_methods
+
+ assert_no_queries { company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) }
+
+ company.name += "-changed"
assert_queries(3) { assert company.save }
- assert_equal 4, company.clients_of_firm(true).size
+ assert_equal 4, company.clients_of_firm.reload.size
end
def test_build_via_block_before_save
company = companies(:first_firm)
- new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build {|client| client.name = "Another Client" } }
- assert !company.clients_of_firm.loaded?
- company.name += '-changed'
+ # Load schema information so we don't query below if running just this test.
+ Client.define_attribute_methods
+
+ new_client = assert_no_queries { company.clients_of_firm.build { |client| client.name = "Another Client" } }
+ assert_not_predicate company.clients_of_firm, :loaded?
+
+ company.name += "-changed"
assert_queries(2) { assert company.save }
- assert new_client.persisted?
- assert_equal 3, company.clients_of_firm(true).size
+ assert_predicate new_client, :persisted?
+ assert_equal 3, company.clients_of_firm.reload.size
end
def test_build_many_via_block_before_save
company = companies(:first_firm)
- assert_no_queries(ignore_none: false) do
- company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) do |client|
+
+ # Load schema information so we don't query below if running just this test.
+ Client.define_attribute_methods
+
+ assert_no_queries do
+ company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) do |client|
client.name = "changed"
end
end
- company.name += '-changed'
+ company.name += "-changed"
assert_queries(3) { assert company.save }
- assert_equal 4, company.clients_of_firm(true).size
+ assert_equal 4, company.clients_of_firm.reload.size
end
def test_replace_on_new_object
@@ -552,7 +706,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
assert firm.save
firm.reload
assert_equal 2, firm.clients.length
- assert firm.clients.include?(Client.find_by_name("New Client"))
+ assert_includes firm.clients, Client.find_by_name("New Client")
end
end
@@ -561,67 +715,67 @@ class TestDefaultAutosaveAssociationOnNewRecord < ActiveRecord::TestCase
new_account = Account.new("credit_limit" => 1000)
new_firm = Firm.new("name" => "some firm")
- assert !new_firm.persisted?
+ assert_not_predicate new_firm, :persisted?
new_account.firm = new_firm
new_account.save!
- assert new_firm.persisted?
+ assert_predicate new_firm, :persisted?
new_account = Account.new("credit_limit" => 1000)
new_autosaved_firm = Firm.new("name" => "some firm")
- assert !new_autosaved_firm.persisted?
+ assert_not_predicate new_autosaved_firm, :persisted?
new_account.unautosaved_firm = new_autosaved_firm
new_account.save!
- assert !new_autosaved_firm.persisted?
+ assert_not_predicate new_autosaved_firm, :persisted?
end
def test_autosave_new_record_on_has_one_can_be_disabled_per_relationship
firm = Firm.new("name" => "some firm")
account = Account.new("credit_limit" => 1000)
- assert !account.persisted?
+ assert_not_predicate account, :persisted?
firm.account = account
firm.save!
- assert account.persisted?
+ assert_predicate account, :persisted?
firm = Firm.new("name" => "some firm")
account = Account.new("credit_limit" => 1000)
firm.unautosaved_account = account
- assert !account.persisted?
+ assert_not_predicate account, :persisted?
firm.unautosaved_account = account
firm.save!
- assert !account.persisted?
+ assert_not_predicate account, :persisted?
end
def test_autosave_new_record_on_has_many_can_be_disabled_per_relationship
firm = Firm.new("name" => "some firm")
account = Account.new("credit_limit" => 1000)
- assert !account.persisted?
+ assert_not_predicate account, :persisted?
firm.accounts << account
firm.save!
- assert account.persisted?
+ assert_predicate account, :persisted?
firm = Firm.new("name" => "some firm")
account = Account.new("credit_limit" => 1000)
- assert !account.persisted?
+ assert_not_predicate account, :persisted?
firm.unautosaved_accounts << account
firm.save!
- assert !account.persisted?
+ assert_not_predicate account, :persisted?
end
def test_autosave_new_record_with_after_create_callback
- post = PostWithAfterCreateCallback.new(title: 'Captain Murphy', body: 'is back')
- post.comments.build(body: 'foo')
+ post = PostWithAfterCreateCallback.new(title: "Captain Murphy", body: "is back")
+ post.comments.build(body: "foo")
post.save!
assert_not_nil post.author_id
@@ -632,8 +786,8 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
self.use_transactional_tests = false
setup do
- @pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?")
- @ship = @pirate.create_ship(:name => 'Nights Dirty Lightning')
+ @pirate = Pirate.create(catchphrase: "Don' botharrr talkin' like one, savvy?")
+ @ship = @pirate.create_ship(name: "Nights Dirty Lightning")
end
teardown do
@@ -649,18 +803,18 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
@pirate.mark_for_destruction
@pirate.ship.mark_for_destruction
- assert !@pirate.reload.marked_for_destruction?
- assert !@pirate.ship.reload.marked_for_destruction?
+ assert_not_predicate @pirate.reload, :marked_for_destruction?
+ assert_not_predicate @pirate.ship.reload, :marked_for_destruction?
end
# has_one
- def test_should_destroy_a_child_association_as_part_of_the_save_transaction_if_it_was_marked_for_destroyal
- assert !@pirate.ship.marked_for_destruction?
+ def test_should_destroy_a_child_association_as_part_of_the_save_transaction_if_it_was_marked_for_destruction
+ assert_not_predicate @pirate.ship, :marked_for_destruction?
@pirate.ship.mark_for_destruction
id = @pirate.ship.id
- assert @pirate.ship.marked_for_destruction?
+ assert_predicate @pirate.ship, :marked_for_destruction?
assert Ship.find_by_id(id)
@pirate.save
@@ -669,12 +823,13 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
end
def test_should_skip_validation_on_a_child_association_if_marked_for_destruction
- @pirate.ship.name = ''
- assert !@pirate.valid?
+ @pirate.ship.name = ""
+ assert_not_predicate @pirate, :valid?
@pirate.ship.mark_for_destruction
- @pirate.ship.expects(:valid?).never
- assert_difference('Ship.count', -1) { @pirate.save! }
+ assert_not_called(@pirate.ship, :valid?) do
+ assert_difference("Ship.count", -1) { @pirate.save! }
+ end
end
def test_a_child_marked_for_destruction_should_not_be_destroyed_twice
@@ -692,13 +847,14 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
def save(*args)
super
destroy
- raise 'Oh noes!'
+ raise "Oh noes!"
end
end
@ship.pirate.catchphrase = "Changed Catchphrase"
+ @ship.name_will_change!
- assert_raise(RuntimeError) { assert !@pirate.save }
+ assert_raise(RuntimeError) { assert_not @pirate.save }
assert_not_nil @pirate.reload.ship
end
@@ -709,18 +865,19 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
end
def test_should_not_save_changed_has_one_unchanged_object_if_child_is_saved
- @pirate.ship.expects(:save).never
- assert @pirate.save
+ assert_not_called(@pirate.ship, :save) do
+ assert @pirate.save
+ end
end
# belongs_to
- def test_should_destroy_a_parent_association_as_part_of_the_save_transaction_if_it_was_marked_for_destroyal
- assert !@ship.pirate.marked_for_destruction?
+ def test_should_destroy_a_parent_association_as_part_of_the_save_transaction_if_it_was_marked_for_destruction
+ assert_not_predicate @ship.pirate, :marked_for_destruction?
@ship.pirate.mark_for_destruction
id = @ship.pirate.id
- assert @ship.pirate.marked_for_destruction?
+ assert_predicate @ship.pirate, :marked_for_destruction?
assert Pirate.find_by_id(id)
@ship.save
@@ -729,12 +886,13 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
end
def test_should_skip_validation_on_a_parent_association_if_marked_for_destruction
- @ship.pirate.catchphrase = ''
- assert !@ship.valid?
+ @ship.pirate.catchphrase = ""
+ assert_not_predicate @ship, :valid?
@ship.pirate.mark_for_destruction
- @ship.pirate.expects(:valid?).never
- assert_difference('Pirate.count', -1) { @ship.save! }
+ assert_not_called(@ship.pirate, :valid?) do
+ assert_difference("Pirate.count", -1) { @ship.save! }
+ end
end
def test_a_parent_marked_for_destruction_should_not_be_destroyed_twice
@@ -752,29 +910,29 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
def save(*args)
super
destroy
- raise 'Oh noes!'
+ raise "Oh noes!"
end
end
@ship.pirate.catchphrase = "Changed Catchphrase"
- assert_raise(RuntimeError) { assert !@ship.save }
+ assert_raise(RuntimeError) { assert_not @ship.save }
assert_not_nil @ship.reload.pirate
end
def test_should_save_changed_child_objects_if_parent_is_saved
- @pirate = @ship.create_pirate(:catchphrase => "Don' botharrr talkin' like one, savvy?")
- @parrot = @pirate.parrots.create!(:name => 'Posideons Killer')
+ @pirate = @ship.create_pirate(catchphrase: "Don' botharrr talkin' like one, savvy?")
+ @parrot = @pirate.parrots.create!(name: "Posideons Killer")
@parrot.name = "NewName"
@ship.save
- assert_equal 'NewName', @parrot.reload.name
+ assert_equal "NewName", @parrot.reload.name
end
def test_should_destroy_has_many_as_part_of_the_save_transaction_if_they_were_marked_for_destruction
- 2.times { |i| @pirate.birds.create!(:name => "birds_#{i}") }
+ 2.times { |i| @pirate.birds.create!(name: "birds_#{i}") }
- assert !@pirate.birds.any?(&:marked_for_destruction?)
+ assert_not @pirate.birds.any?(&:marked_for_destruction?)
@pirate.birds.each(&:mark_for_destruction)
klass = @pirate.birds.first.class
@@ -784,7 +942,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
ids.each { |id| assert klass.find_by_id(id) }
@pirate.save
- assert @pirate.reload.birds.empty?
+ assert_empty @pirate.reload.birds
ids.each { |id| assert_nil klass.find_by_id(id) }
end
@@ -792,62 +950,67 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
@pirate.birds.create!(name: :parrot)
@pirate.birds.first.destroy
@pirate.save!
- assert @pirate.reload.birds.empty?
+ assert_empty @pirate.reload.birds
end
def test_should_skip_validation_on_has_many_if_marked_for_destruction
- 2.times { |i| @pirate.birds.create!(:name => "birds_#{i}") }
+ 2.times { |i| @pirate.birds.create!(name: "birds_#{i}") }
- @pirate.birds.each { |bird| bird.name = '' }
- assert !@pirate.valid?
+ @pirate.birds.each { |bird| bird.name = "" }
+ assert_not_predicate @pirate, :valid?
- @pirate.birds.each do |bird|
- bird.mark_for_destruction
- bird.expects(:valid?).never
+ @pirate.birds.each(&:mark_for_destruction)
+
+ assert_not_called(@pirate.birds.first, :valid?) do
+ assert_not_called(@pirate.birds.last, :valid?) do
+ assert_difference("Bird.count", -2) { @pirate.save! }
+ end
end
- assert_difference("Bird.count", -2) { @pirate.save! }
end
def test_should_skip_validation_on_has_many_if_destroyed
- @pirate.birds.create!(:name => "birds_1")
+ @pirate.birds.create!(name: "birds_1")
- @pirate.birds.each { |bird| bird.name = '' }
- assert !@pirate.valid?
+ @pirate.birds.each { |bird| bird.name = "" }
+ assert_not_predicate @pirate, :valid?
@pirate.birds.each(&:destroy)
- assert @pirate.valid?
+ assert_predicate @pirate, :valid?
end
def test_a_child_marked_for_destruction_should_not_be_destroyed_twice_while_saving_has_many
- @pirate.birds.create!(:name => "birds_1")
+ @pirate.birds.create!(name: "birds_1")
@pirate.birds.each(&:mark_for_destruction)
assert @pirate.save
- @pirate.birds.each { |bird| bird.expects(:destroy).never }
- assert @pirate.save
+ @pirate.birds.each do |bird|
+ assert_not_called(bird, :destroy) do
+ assert @pirate.save
+ end
+ end
end
def test_should_rollback_destructions_if_an_exception_occurred_while_saving_has_many
- 2.times { |i| @pirate.birds.create!(:name => "birds_#{i}") }
+ 2.times { |i| @pirate.birds.create!(name: "birds_#{i}") }
before = @pirate.birds.map { |c| c.mark_for_destruction ; c }
# Stub the destroy method of the second child to raise an exception
class << before.last
def destroy(*args)
super
- raise 'Oh noes!'
+ raise "Oh noes!"
end
end
- assert_raise(RuntimeError) { assert !@pirate.save }
+ assert_raise(RuntimeError) { assert_not @pirate.save }
assert_equal before, @pirate.reload.birds
end
def test_when_new_record_a_child_marked_for_destruction_should_not_affect_other_records_from_saving
- @pirate = @ship.build_pirate(:catchphrase => "Arr' now I shall keep me eye on you matey!") # new record
+ @pirate = @ship.build_pirate(catchphrase: "Arr' now I shall keep me eye on you matey!") # new record
- 3.times { |i| @pirate.birds.build(:name => "birds_#{i}") }
+ 3.times { |i| @pirate.birds.build(name: "birds_#{i}") }
@pirate.birds[1].mark_for_destruction
@pirate.save!
@@ -873,8 +1036,8 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
define_method("test_should_run_add_callback_#{callback_type}s_for_has_many") do
association_name_with_callbacks = "birds_with_#{callback_type}_callbacks"
- pirate = Pirate.new(:catchphrase => "Arr")
- pirate.send(association_name_with_callbacks).build(:name => "Crowe the One-Eyed")
+ pirate = Pirate.new(catchphrase: "Arr")
+ pirate.send(association_name_with_callbacks).build(name: "Crowe the One-Eyed")
expected = [
"before_adding_#{callback_type}_bird_<new>",
@@ -887,7 +1050,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
define_method("test_should_run_remove_callback_#{callback_type}s_for_has_many") do
association_name_with_callbacks = "birds_with_#{callback_type}_callbacks"
- @pirate.send(association_name_with_callbacks).create!(:name => "Crowe the One-Eyed")
+ @pirate.send(association_name_with_callbacks).create!(name: "Crowe the One-Eyed")
@pirate.send(association_name_with_callbacks).each(&:mark_for_destruction)
child_id = @pirate.send(association_name_with_callbacks).first.id
@@ -904,71 +1067,73 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
end
def test_should_destroy_habtm_as_part_of_the_save_transaction_if_they_were_marked_for_destruction
- 2.times { |i| @pirate.parrots.create!(:name => "parrots_#{i}") }
+ 2.times { |i| @pirate.parrots.create!(name: "parrots_#{i}") }
- assert !@pirate.parrots.any?(&:marked_for_destruction?)
+ assert_not @pirate.parrots.any?(&:marked_for_destruction?)
@pirate.parrots.each(&:mark_for_destruction)
assert_no_difference "Parrot.count" do
@pirate.save
end
- assert @pirate.reload.parrots.empty?
+ assert_empty @pirate.reload.parrots
join_records = Pirate.connection.select_all("SELECT * FROM parrots_pirates WHERE pirate_id = #{@pirate.id}")
- assert join_records.empty?
+ assert_empty join_records
end
def test_should_skip_validation_on_habtm_if_marked_for_destruction
- 2.times { |i| @pirate.parrots.create!(:name => "parrots_#{i}") }
+ 2.times { |i| @pirate.parrots.create!(name: "parrots_#{i}") }
+
+ @pirate.parrots.each { |parrot| parrot.name = "" }
+ assert_not_predicate @pirate, :valid?
- @pirate.parrots.each { |parrot| parrot.name = '' }
- assert !@pirate.valid?
+ @pirate.parrots.each { |parrot| parrot.mark_for_destruction }
- @pirate.parrots.each do |parrot|
- parrot.mark_for_destruction
- parrot.expects(:valid?).never
+ assert_not_called(@pirate.parrots.first, :valid?) do
+ assert_not_called(@pirate.parrots.last, :valid?) do
+ @pirate.save!
+ end
end
- @pirate.save!
- assert @pirate.reload.parrots.empty?
+ assert_empty @pirate.reload.parrots
end
def test_should_skip_validation_on_habtm_if_destroyed
- @pirate.parrots.create!(:name => "parrots_1")
+ @pirate.parrots.create!(name: "parrots_1")
- @pirate.parrots.each { |parrot| parrot.name = '' }
- assert !@pirate.valid?
+ @pirate.parrots.each { |parrot| parrot.name = "" }
+ assert_not_predicate @pirate, :valid?
@pirate.parrots.each(&:destroy)
- assert @pirate.valid?
+ assert_predicate @pirate, :valid?
end
def test_a_child_marked_for_destruction_should_not_be_destroyed_twice_while_saving_habtm
- @pirate.parrots.create!(:name => "parrots_1")
+ @pirate.parrots.create!(name: "parrots_1")
@pirate.parrots.each(&:mark_for_destruction)
assert @pirate.save
Pirate.transaction do
- assert_queries(0) do
+ assert_no_queries do
assert @pirate.save
end
end
end
def test_should_rollback_destructions_if_an_exception_occurred_while_saving_habtm
- 2.times { |i| @pirate.parrots.create!(:name => "parrots_#{i}") }
+ 2.times { |i| @pirate.parrots.create!(name: "parrots_#{i}") }
before = @pirate.parrots.map { |c| c.mark_for_destruction ; c }
class << @pirate.association(:parrots)
def destroy(*args)
super
- raise 'Oh noes!'
+ raise "Oh noes!"
end
end
- assert_raise(RuntimeError) { assert !@pirate.save }
+ assert_raise(RuntimeError) { assert_not @pirate.save }
assert_equal before, @pirate.reload.parrots
end
@@ -977,8 +1142,8 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
define_method("test_should_run_add_callback_#{callback_type}s_for_habtm") do
association_name_with_callbacks = "parrots_with_#{callback_type}_callbacks"
- pirate = Pirate.new(:catchphrase => "Arr")
- pirate.send(association_name_with_callbacks).build(:name => "Crowe the One-Eyed")
+ pirate = Pirate.new(catchphrase: "Arr")
+ pirate.send(association_name_with_callbacks).build(name: "Crowe the One-Eyed")
expected = [
"before_adding_#{callback_type}_parrot_<new>",
@@ -991,7 +1156,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
define_method("test_should_run_remove_callback_#{callback_type}s_for_habtm") do
association_name_with_callbacks = "parrots_with_#{callback_type}_callbacks"
- @pirate.send(association_name_with_callbacks).create!(:name => "Crowe the One-Eyed")
+ @pirate.send(association_name_with_callbacks).create!(name: "Crowe the One-Eyed")
@pirate.send(association_name_with_callbacks).each(&:mark_for_destruction)
child_id = @pirate.send(association_name_with_callbacks).first.id
@@ -1013,60 +1178,60 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
def setup
super
- @pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?")
- @ship = @pirate.create_ship(:name => 'Nights Dirty Lightning')
+ @pirate = Pirate.create(catchphrase: "Don' botharrr talkin' like one, savvy?")
+ @ship = @pirate.create_ship(name: "Nights Dirty Lightning")
end
def test_should_still_work_without_an_associated_model
@ship.destroy
@pirate.reload.catchphrase = "Arr"
@pirate.save
- assert_equal 'Arr', @pirate.reload.catchphrase
+ assert_equal "Arr", @pirate.reload.catchphrase
end
def test_should_automatically_save_the_associated_model
- @pirate.ship.name = 'The Vile Insanity'
+ @pirate.ship.name = "The Vile Insanity"
@pirate.save
- assert_equal 'The Vile Insanity', @pirate.reload.ship.name
+ assert_equal "The Vile Insanity", @pirate.reload.ship.name
end
def test_changed_for_autosave_should_handle_cycles
@ship.pirate = @pirate
- assert_queries(0) { @ship.save! }
+ assert_no_queries { @ship.save! }
@parrot = @pirate.parrots.create(name: "some_name")
- @parrot.name="changed_name"
+ @parrot.name = "changed_name"
assert_queries(1) { @ship.save! }
- assert_queries(0) { @ship.save! }
+ assert_no_queries { @ship.save! }
end
def test_should_automatically_save_bang_the_associated_model
- @pirate.ship.name = 'The Vile Insanity'
+ @pirate.ship.name = "The Vile Insanity"
@pirate.save!
- assert_equal 'The Vile Insanity', @pirate.reload.ship.name
+ assert_equal "The Vile Insanity", @pirate.reload.ship.name
end
def test_should_automatically_validate_the_associated_model
- @pirate.ship.name = ''
- assert @pirate.invalid?
- assert @pirate.errors[:"ship.name"].any?
+ @pirate.ship.name = ""
+ assert_predicate @pirate, :invalid?
+ assert_predicate @pirate.errors[:"ship.name"], :any?
end
def test_should_merge_errors_on_the_associated_models_onto_the_parent_even_if_it_is_not_valid
@pirate.ship.name = nil
@pirate.catchphrase = nil
- assert @pirate.invalid?
- assert @pirate.errors[:"ship.name"].any?
- assert @pirate.errors[:catchphrase].any?
+ assert_predicate @pirate, :invalid?
+ assert_predicate @pirate.errors[:"ship.name"], :any?
+ assert_predicate @pirate.errors[:catchphrase], :any?
end
def test_should_not_ignore_different_error_messages_on_the_same_attribute
old_validators = Ship._validators.deep_dup
old_callbacks = Ship._validate_callbacks.deep_dup
- Ship.validates_format_of :name, :with => /\w/
+ Ship.validates_format_of :name, with: /\w/
@pirate.ship.name = ""
@pirate.catchphrase = nil
- assert @pirate.invalid?
+ assert_predicate @pirate, :invalid?
assert_equal ["can't be blank", "is invalid"], @pirate.errors[:"ship.name"]
ensure
Ship._validators = old_validators if old_validators
@@ -1074,49 +1239,49 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
end
def test_should_still_allow_to_bypass_validations_on_the_associated_model
- @pirate.catchphrase = ''
- @pirate.ship.name = ''
- @pirate.save(:validate => false)
+ @pirate.catchphrase = ""
+ @pirate.ship.name = ""
+ @pirate.save(validate: false)
# Oracle saves empty string as NULL
if current_adapter?(:OracleAdapter)
assert_equal [nil, nil], [@pirate.reload.catchphrase, @pirate.ship.name]
else
- assert_equal ['', ''], [@pirate.reload.catchphrase, @pirate.ship.name]
+ assert_equal ["", ""], [@pirate.reload.catchphrase, @pirate.ship.name]
end
end
def test_should_allow_to_bypass_validations_on_associated_models_at_any_depth
- 2.times { |i| @pirate.ship.parts.create!(:name => "part #{i}") }
+ 2.times { |i| @pirate.ship.parts.create!(name: "part #{i}") }
- @pirate.catchphrase = ''
- @pirate.ship.name = ''
- @pirate.ship.parts.each { |part| part.name = '' }
- @pirate.save(:validate => false)
+ @pirate.catchphrase = ""
+ @pirate.ship.name = ""
+ @pirate.ship.parts.each { |part| part.name = "" }
+ @pirate.save(validate: false)
values = [@pirate.reload.catchphrase, @pirate.ship.name, *@pirate.ship.parts.map(&:name)]
# Oracle saves empty string as NULL
if current_adapter?(:OracleAdapter)
assert_equal [nil, nil, nil, nil], values
else
- assert_equal ['', '', '', ''], values
+ assert_equal ["", "", "", ""], values
end
end
def test_should_still_raise_an_ActiveRecordRecord_Invalid_exception_if_we_want_that
- @pirate.ship.name = ''
+ @pirate.ship.name = ""
assert_raise(ActiveRecord::RecordInvalid) do
@pirate.save!
end
end
def test_should_not_save_and_return_false_if_a_callback_cancelled_saving
- pirate = Pirate.new(:catchphrase => 'Arr')
- ship = pirate.build_ship(:name => 'The Vile Insanity')
+ pirate = Pirate.new(catchphrase: "Arr")
+ ship = pirate.build_ship(name: "The Vile Insanity")
ship.cancel_save_from_callback = true
- assert_no_difference 'Pirate.count' do
- assert_no_difference 'Ship.count' do
- assert !pirate.save
+ assert_no_difference "Pirate.count" do
+ assert_no_difference "Ship.count" do
+ assert_not pirate.save
end
end
end
@@ -1124,23 +1289,30 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
def test_should_rollback_any_changes_if_an_exception_occurred_while_saving
before = [@pirate.catchphrase, @pirate.ship.name]
- @pirate.catchphrase = 'Arr'
- @pirate.ship.name = 'The Vile Insanity'
+ @pirate.catchphrase = "Arr"
+ @pirate.ship.name = "The Vile Insanity"
# Stub the save method of the @pirate.ship instance to raise an exception
class << @pirate.ship
def save(*args)
super
- raise 'Oh noes!'
+ raise "Oh noes!"
end
end
- assert_raise(RuntimeError) { assert !@pirate.save }
+ assert_raise(RuntimeError) { assert_not @pirate.save }
assert_equal before, [@pirate.reload.catchphrase, @pirate.ship.name]
end
def test_should_not_load_the_associated_model
- assert_queries(1) { @pirate.catchphrase = 'Arr'; @pirate.save! }
+ assert_queries(1) { @pirate.catchphrase = "Arr"; @pirate.save! }
+ end
+
+ def test_mark_for_destruction_is_ignored_without_autosave_true
+ ship = ShipWithoutNestedAttributes.new(name: "The Black Flag")
+ ship.parts.build.mark_for_destruction
+
+ assert_not_predicate ship, :valid?
end
end
@@ -1158,7 +1330,7 @@ class TestAutosaveAssociationOnAHasOneThroughAssociation < ActiveRecord::TestCas
class << @member.organization
def save(*args)
super
- raise 'Oh noes!'
+ raise "Oh noes!"
end
end
assert_nothing_raised { @member.save }
@@ -1170,70 +1342,70 @@ class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase
def setup
super
- @ship = Ship.create(:name => 'Nights Dirty Lightning')
- @pirate = @ship.create_pirate(:catchphrase => "Don' botharrr talkin' like one, savvy?")
+ @ship = Ship.create(name: "Nights Dirty Lightning")
+ @pirate = @ship.create_pirate(catchphrase: "Don' botharrr talkin' like one, savvy?")
end
def test_should_still_work_without_an_associated_model
@pirate.destroy
@ship.reload.name = "The Vile Insanity"
@ship.save
- assert_equal 'The Vile Insanity', @ship.reload.name
+ assert_equal "The Vile Insanity", @ship.reload.name
end
def test_should_automatically_save_the_associated_model
- @ship.pirate.catchphrase = 'Arr'
+ @ship.pirate.catchphrase = "Arr"
@ship.save
- assert_equal 'Arr', @ship.reload.pirate.catchphrase
+ assert_equal "Arr", @ship.reload.pirate.catchphrase
end
def test_should_automatically_save_bang_the_associated_model
- @ship.pirate.catchphrase = 'Arr'
+ @ship.pirate.catchphrase = "Arr"
@ship.save!
- assert_equal 'Arr', @ship.reload.pirate.catchphrase
+ assert_equal "Arr", @ship.reload.pirate.catchphrase
end
def test_should_automatically_validate_the_associated_model
- @ship.pirate.catchphrase = ''
- assert @ship.invalid?
- assert @ship.errors[:"pirate.catchphrase"].any?
+ @ship.pirate.catchphrase = ""
+ assert_predicate @ship, :invalid?
+ assert_predicate @ship.errors[:"pirate.catchphrase"], :any?
end
def test_should_merge_errors_on_the_associated_model_onto_the_parent_even_if_it_is_not_valid
@ship.name = nil
@ship.pirate.catchphrase = nil
- assert @ship.invalid?
- assert @ship.errors[:name].any?
- assert @ship.errors[:"pirate.catchphrase"].any?
+ assert_predicate @ship, :invalid?
+ assert_predicate @ship.errors[:name], :any?
+ assert_predicate @ship.errors[:"pirate.catchphrase"], :any?
end
def test_should_still_allow_to_bypass_validations_on_the_associated_model
- @ship.pirate.catchphrase = ''
- @ship.name = ''
- @ship.save(:validate => false)
+ @ship.pirate.catchphrase = ""
+ @ship.name = ""
+ @ship.save(validate: false)
# Oracle saves empty string as NULL
if current_adapter?(:OracleAdapter)
assert_equal [nil, nil], [@ship.reload.name, @ship.pirate.catchphrase]
else
- assert_equal ['', ''], [@ship.reload.name, @ship.pirate.catchphrase]
+ assert_equal ["", ""], [@ship.reload.name, @ship.pirate.catchphrase]
end
end
def test_should_still_raise_an_ActiveRecordRecord_Invalid_exception_if_we_want_that
- @ship.pirate.catchphrase = ''
+ @ship.pirate.catchphrase = ""
assert_raise(ActiveRecord::RecordInvalid) do
@ship.save!
end
end
def test_should_not_save_and_return_false_if_a_callback_cancelled_saving
- ship = Ship.new(:name => 'The Vile Insanity')
- pirate = ship.build_pirate(:catchphrase => 'Arr')
+ ship = Ship.new(name: "The Vile Insanity")
+ pirate = ship.build_pirate(catchphrase: "Arr")
pirate.cancel_save_from_callback = true
- assert_no_difference 'Ship.count' do
- assert_no_difference 'Pirate.count' do
- assert !ship.save
+ assert_no_difference "Ship.count" do
+ assert_no_difference "Pirate.count" do
+ assert_not ship.save
end
end
end
@@ -1241,41 +1413,41 @@ class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase
def test_should_rollback_any_changes_if_an_exception_occurred_while_saving
before = [@ship.pirate.catchphrase, @ship.name]
- @ship.pirate.catchphrase = 'Arr'
- @ship.name = 'The Vile Insanity'
+ @ship.pirate.catchphrase = "Arr"
+ @ship.name = "The Vile Insanity"
# Stub the save method of the @ship.pirate instance to raise an exception
class << @ship.pirate
def save(*args)
super
- raise 'Oh noes!'
+ raise "Oh noes!"
end
end
- assert_raise(RuntimeError) { assert !@ship.save }
+ assert_raise(RuntimeError) { assert_not @ship.save }
assert_equal before, [@ship.pirate.reload.catchphrase, @ship.reload.name]
end
def test_should_not_load_the_associated_model
- assert_queries(1) { @ship.name = 'The Vile Insanity'; @ship.save! }
+ assert_queries(1) { @ship.name = "The Vile Insanity"; @ship.save! }
end
end
module AutosaveAssociationOnACollectionAssociationTests
def test_should_automatically_save_the_associated_models
- new_names = ['Grace OMalley', 'Privateers Greed']
+ new_names = ["Grace OMalley", "Privateers Greed"]
@pirate.send(@association_name).each_with_index { |child, i| child.name = new_names[i] }
@pirate.save
- assert_equal new_names, @pirate.reload.send(@association_name).map(&:name)
+ assert_equal new_names.sort, @pirate.reload.send(@association_name).map(&:name).sort
end
def test_should_automatically_save_bang_the_associated_models
- new_names = ['Grace OMalley', 'Privateers Greed']
+ new_names = ["Grace OMalley", "Privateers Greed"]
@pirate.send(@association_name).each_with_index { |child, i| child.name = new_names[i] }
@pirate.save!
- assert_equal new_names, @pirate.reload.send(@association_name).map(&:name)
+ assert_equal new_names.sort, @pirate.reload.send(@association_name).map(&:name).sort
end
def test_should_update_children_when_autosave_is_true_and_parent_is_new_but_child_is_not
@@ -1288,51 +1460,59 @@ 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 = '' }
+ @pirate.send(@association_name).each { |child| child.name = "" }
- assert !@pirate.valid?
+ assert_not_predicate @pirate, :valid?
assert_equal ["can't be blank"], @pirate.errors["#{@association_name}.name"]
- assert @pirate.errors[@association_name].empty?
+ assert_empty @pirate.errors[@association_name]
end
def test_should_not_use_default_invalid_error_on_associated_models
- @pirate.send(@association_name).build(:name => '')
+ @pirate.send(@association_name).build(name: "")
- assert !@pirate.valid?
+ assert_not_predicate @pirate, :valid?
assert_equal ["can't be blank"], @pirate.errors["#{@association_name}.name"]
- assert @pirate.errors[@association_name].empty?
+ assert_empty @pirate.errors[@association_name]
end
def test_should_default_invalid_error_from_i18n
- I18n.backend.store_translations(:en, activerecord: {errors: { models:
+ I18n.backend.store_translations(:en, activerecord: { errors: { models:
{ @associated_model_name.to_s.to_sym => { blank: "cannot be blank" } }
- }})
+ } })
- @pirate.send(@association_name).build(name: '')
+ @pirate.send(@association_name).build(name: "")
- assert !@pirate.valid?
+ assert_not_predicate @pirate, :valid?
assert_equal ["cannot be blank"], @pirate.errors["#{@association_name}.name"]
assert_equal ["#{@association_name.to_s.humanize} name cannot be blank"], @pirate.errors.full_messages
- assert @pirate.errors[@association_name].empty?
+ assert_empty @pirate.errors[@association_name]
ensure
I18n.backend = I18n::Backend::Simple.new
end
def test_should_merge_errors_on_the_associated_models_onto_the_parent_even_if_it_is_not_valid
- @pirate.send(@association_name).each { |child| child.name = '' }
+ @pirate.send(@association_name).each { |child| child.name = "" }
@pirate.catchphrase = nil
- assert !@pirate.valid?
+ assert_not_predicate @pirate, :valid?
assert_equal ["can't be blank"], @pirate.errors["#{@association_name}.name"]
- assert @pirate.errors[:catchphrase].any?
+ assert_predicate @pirate.errors[:catchphrase], :any?
end
def test_should_allow_to_bypass_validations_on_the_associated_models_on_update
- @pirate.catchphrase = ''
- @pirate.send(@association_name).each { |child| child.name = '' }
+ @pirate.catchphrase = ""
+ @pirate.send(@association_name).each { |child| child.name = "" }
- assert @pirate.save(:validate => false)
+ assert @pirate.save(validate: false)
# Oracle saves empty string as NULL
if current_adapter?(:OracleAdapter)
assert_equal [nil, nil, nil], [
@@ -1341,7 +1521,7 @@ module AutosaveAssociationOnACollectionAssociationTests
@pirate.send(@association_name).last.name
]
else
- assert_equal ['', '', ''], [
+ assert_equal ["", "", ""], [
@pirate.reload.catchphrase,
@pirate.send(@association_name).first.name,
@pirate.send(@association_name).last.name
@@ -1359,64 +1539,64 @@ module AutosaveAssociationOnACollectionAssociationTests
def test_should_allow_to_bypass_validations_on_the_associated_models_on_create
assert_difference("#{ @association_name == :birds ? 'Bird' : 'Parrot' }.count", 2) do
2.times { @pirate.send(@association_name).build }
- @pirate.save(:validate => false)
+ @pirate.save(validate: false)
end
end
def test_should_not_save_and_return_false_if_a_callback_cancelled_saving_in_either_create_or_update
- @pirate.catchphrase = 'Changed'
- @child_1.name = 'Changed'
+ @pirate.catchphrase = "Changed"
+ @child_1.name = "Changed"
@child_1.cancel_save_from_callback = true
- assert !@pirate.save
+ assert_not @pirate.save
assert_equal "Don' botharrr talkin' like one, savvy?", @pirate.reload.catchphrase
assert_equal "Posideons Killer", @child_1.reload.name
- new_pirate = Pirate.new(:catchphrase => 'Arr')
- new_child = new_pirate.send(@association_name).build(:name => 'Grace OMalley')
+ new_pirate = Pirate.new(catchphrase: "Arr")
+ new_child = new_pirate.send(@association_name).build(name: "Grace OMalley")
new_child.cancel_save_from_callback = true
- assert_no_difference 'Pirate.count' do
+ assert_no_difference "Pirate.count" do
assert_no_difference "#{new_child.class.name}.count" do
- assert !new_pirate.save
+ assert_not new_pirate.save
end
end
end
def test_should_rollback_any_changes_if_an_exception_occurred_while_saving
before = [@pirate.catchphrase, *@pirate.send(@association_name).map(&:name)]
- new_names = ['Grace OMalley', 'Privateers Greed']
+ new_names = ["Grace OMalley", "Privateers Greed"]
- @pirate.catchphrase = 'Arr'
+ @pirate.catchphrase = "Arr"
@pirate.send(@association_name).each_with_index { |child, i| child.name = new_names[i] }
# Stub the save method of the first child instance to raise an exception
class << @pirate.send(@association_name).first
def save(*args)
super
- raise 'Oh noes!'
+ raise "Oh noes!"
end
end
- assert_raise(RuntimeError) { assert !@pirate.save }
+ assert_raise(RuntimeError) { assert_not @pirate.save }
assert_equal before, [@pirate.reload.catchphrase, *@pirate.send(@association_name).map(&:name)]
end
def test_should_still_raise_an_ActiveRecordRecord_Invalid_exception_if_we_want_that
- @pirate.send(@association_name).each { |child| child.name = '' }
+ @pirate.send(@association_name).each { |child| child.name = "" }
assert_raise(ActiveRecord::RecordInvalid) do
@pirate.save!
end
end
def test_should_not_load_the_associated_models_if_they_were_not_loaded_yet
- assert_queries(1) { @pirate.catchphrase = 'Arr'; @pirate.save! }
+ assert_queries(1) { @pirate.catchphrase = "Arr"; @pirate.save! }
@pirate.send(@association_name).load_target
assert_queries(3) do
- @pirate.catchphrase = 'Yarr'
- new_names = ['Grace OMalley', 'Privateers Greed']
+ @pirate.catchphrase = "Yarr"
+ new_names = ["Grace OMalley", "Privateers Greed"]
@pirate.send(@association_name).each_with_index { |child, i| child.name = new_names[i] }
@pirate.save!
end
@@ -1431,9 +1611,9 @@ class TestAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCase
@association_name = :birds
@associated_model_name = :bird
- @pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?")
- @child_1 = @pirate.birds.create(:name => 'Posideons Killer')
- @child_2 = @pirate.birds.create(:name => 'Killer bandita Dionne')
+ @pirate = Pirate.create(catchphrase: "Don' botharrr talkin' like one, savvy?")
+ @child_1 = @pirate.birds.create(name: "Posideons Killer")
+ @child_2 = @pirate.birds.create(name: "Killer bandita Dionne")
end
include AutosaveAssociationOnACollectionAssociationTests
@@ -1449,8 +1629,8 @@ class TestAutosaveAssociationOnAHasAndBelongsToManyAssociation < ActiveRecord::T
@habtm = true
@pirate = Pirate.create(catchphrase: "Don' botharrr talkin' like one, savvy?")
- @child_1 = @pirate.parrots.create(name: 'Posideons Killer')
- @child_2 = @pirate.parrots.create(name: 'Killer bandita Dionne')
+ @child_1 = @pirate.parrots.create(name: "Posideons Killer")
+ @child_2 = @pirate.parrots.create(name: "Killer bandita Dionne")
end
include AutosaveAssociationOnACollectionAssociationTests
@@ -1466,8 +1646,8 @@ class TestAutosaveAssociationOnAHasAndBelongsToManyAssociationWithAcceptsNestedA
@habtm = true
@pirate = Pirate.create(catchphrase: "Don' botharrr talkin' like one, savvy?")
- @child_1 = @pirate.parrots.create(name: 'Posideons Killer')
- @child_2 = @pirate.parrots.create(name: 'Killer bandita Dionne')
+ @child_1 = @pirate.parrots.create(name: "Posideons Killer")
+ @child_2 = @pirate.parrots.create(name: "Killer bandita Dionne")
end
include AutosaveAssociationOnACollectionAssociationTests
@@ -1478,15 +1658,15 @@ class TestAutosaveAssociationValidationsOnAHasManyAssociation < ActiveRecord::Te
def setup
super
- @pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?")
- @pirate.birds.create(:name => 'cookoo')
+ @pirate = Pirate.create(catchphrase: "Don' botharrr talkin' like one, savvy?")
+ @pirate.birds.create(name: "cookoo")
end
test "should automatically validate associations" do
- assert @pirate.valid?
- @pirate.birds.each { |bird| bird.name = '' }
+ assert_predicate @pirate, :valid?
+ @pirate.birds.each { |bird| bird.name = "" }
- assert !@pirate.valid?
+ assert_not_predicate @pirate, :valid?
end
end
@@ -1495,21 +1675,21 @@ class TestAutosaveAssociationValidationsOnAHasOneAssociation < ActiveRecord::Tes
def setup
super
- @pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?")
- @pirate.create_ship(:name => 'titanic')
+ @pirate = Pirate.create(catchphrase: "Don' botharrr talkin' like one, savvy?")
+ @pirate.create_ship(name: "titanic")
super
end
test "should automatically validate associations with :validate => true" do
- assert @pirate.valid?
- @pirate.ship.name = ''
- assert !@pirate.valid?
+ assert_predicate @pirate, :valid?
+ @pirate.ship.name = ""
+ assert_not_predicate @pirate, :valid?
end
test "should not automatically add validate associations without :validate => true" do
- assert @pirate.valid?
- @pirate.non_validated_ship.name = ''
- assert @pirate.valid?
+ assert_predicate @pirate, :valid?
+ @pirate.non_validated_ship.name = ""
+ assert_predicate @pirate, :valid?
end
end
@@ -1518,19 +1698,19 @@ class TestAutosaveAssociationValidationsOnABelongsToAssociation < ActiveRecord::
def setup
super
- @pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?")
+ @pirate = Pirate.create(catchphrase: "Don' botharrr talkin' like one, savvy?")
end
test "should automatically validate associations with :validate => true" do
- assert @pirate.valid?
- @pirate.parrot = Parrot.new(:name => '')
- assert !@pirate.valid?
+ assert_predicate @pirate, :valid?
+ @pirate.parrot = Parrot.new(name: "")
+ assert_not_predicate @pirate, :valid?
end
test "should not automatically validate associations without :validate => true" do
- assert @pirate.valid?
- @pirate.non_validated_parrot = Parrot.new(:name => '')
- assert @pirate.valid?
+ assert_predicate @pirate, :valid?
+ @pirate.non_validated_parrot = Parrot.new(name: "")
+ assert_predicate @pirate, :valid?
end
end
@@ -1539,21 +1719,21 @@ class TestAutosaveAssociationValidationsOnAHABTMAssociation < ActiveRecord::Test
def setup
super
- @pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?")
+ @pirate = Pirate.create(catchphrase: "Don' botharrr talkin' like one, savvy?")
end
test "should automatically validate associations with :validate => true" do
- assert @pirate.valid?
- @pirate.parrots = [ Parrot.new(:name => 'popuga') ]
- @pirate.parrots.each { |parrot| parrot.name = '' }
- assert !@pirate.valid?
+ assert_predicate @pirate, :valid?
+ @pirate.parrots = [ Parrot.new(name: "popuga") ]
+ @pirate.parrots.each { |parrot| parrot.name = "" }
+ assert_not_predicate @pirate, :valid?
end
test "should not automatically validate associations without :validate => true" do
- assert @pirate.valid?
- @pirate.non_validated_parrots = [ Parrot.new(:name => 'popuga') ]
- @pirate.non_validated_parrots.each { |parrot| parrot.name = '' }
- assert @pirate.valid?
+ assert_predicate @pirate, :valid?
+ @pirate.non_validated_parrots = [ Parrot.new(name: "popuga") ]
+ @pirate.non_validated_parrots.each { |parrot| parrot.name = "" }
+ assert_predicate @pirate, :valid?
end
end
@@ -1574,7 +1754,7 @@ class TestAutosaveAssociationValidationMethodsGeneration < ActiveRecord::TestCas
end
test "should not generate validation methods for has_one associations without :validate => true" do
- assert !@pirate.respond_to?(:validate_associated_records_for_non_validated_ship)
+ assert_not_respond_to @pirate, :validate_associated_records_for_non_validated_ship
end
test "should generate validation methods for belongs_to associations with :validate => true" do
@@ -1582,7 +1762,7 @@ class TestAutosaveAssociationValidationMethodsGeneration < ActiveRecord::TestCas
end
test "should not generate validation methods for belongs_to associations without :validate => true" do
- assert !@pirate.respond_to?(:validate_associated_records_for_non_validated_parrot)
+ assert_not_respond_to @pirate, :validate_associated_records_for_non_validated_parrot
end
test "should generate validation methods for HABTM associations with :validate => true" do
@@ -1593,6 +1773,52 @@ end
class TestAutosaveAssociationWithTouch < ActiveRecord::TestCase
def test_autosave_with_touch_should_not_raise_system_stack_error
invoice = Invoice.create
- assert_nothing_raised { invoice.line_items.create(:amount => 10) }
+ 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 setup
+ Comment.delete_all
+ 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
+
+class TestAutosaveAssociationOnAHasManyAssociationDefinedInSubclassWithAcceptsNestedAttributes < ActiveRecord::TestCase
+ def test_should_update_children_when_asssociation_redefined_in_subclass
+ agency = Agency.create!(name: "Agency")
+ valid_project = Project.create!(firm: agency, name: "Initial")
+ agency.update!(
+ "projects_attributes" => {
+ "0" => {
+ "name" => "Updated",
+ "id" => valid_project.id
+ }
+ }
+ )
+ valid_project.reload
+
+ assert_equal "Updated", valid_project.name
end
end
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index 31c31e4329..09e5517449 100644
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -1,34 +1,32 @@
-# -*- coding: utf-8 -*-
+# frozen_string_literal: true
require "cases/helper"
-require 'active_support/concurrency/latch'
-require 'models/post'
-require 'models/author'
-require 'models/topic'
-require 'models/reply'
-require 'models/category'
-require 'models/company'
-require 'models/customer'
-require 'models/developer'
-require 'models/computer'
-require 'models/project'
-require 'models/default'
-require 'models/auto_id'
-require 'models/boolean'
-require 'models/column_name'
-require 'models/subscriber'
-require 'models/keyboard'
-require 'models/comment'
-require 'models/minimalistic'
-require 'models/warehouse_thing'
-require 'models/parrot'
-require 'models/person'
-require 'models/edge'
-require 'models/joke'
-require 'models/bird'
-require 'models/car'
-require 'models/bulb'
-require 'rexml/document'
+require "models/post"
+require "models/author"
+require "models/topic"
+require "models/reply"
+require "models/category"
+require "models/categorization"
+require "models/company"
+require "models/customer"
+require "models/developer"
+require "models/computer"
+require "models/project"
+require "models/default"
+require "models/auto_id"
+require "models/column_name"
+require "models/subscriber"
+require "models/comment"
+require "models/minimalistic"
+require "models/warehouse_thing"
+require "models/parrot"
+require "models/person"
+require "models/edge"
+require "models/joke"
+require "models/bird"
+require "models/car"
+require "models/bulb"
+require "concurrent/atomic/count_down_latch"
class FirstAbstractClass < ActiveRecord::Base
self.abstract_class = true
@@ -37,8 +35,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
@@ -49,8 +45,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
@@ -60,12 +54,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
@@ -77,18 +65,17 @@ 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
classname = conn.class.name[/[^:]*$/]
badchar = {
- 'SQLite3Adapter' => '"',
- 'MysqlAdapter' => '`',
- 'Mysql2Adapter' => '`',
- 'PostgreSQLAdapter' => '"',
- 'OracleAdapter' => '"',
- 'FbAdapter' => '"'
+ "SQLite3Adapter" => '"',
+ "Mysql2Adapter" => "`",
+ "PostgreSQLAdapter" => '"',
+ "OracleAdapter" => '"',
+ "FbAdapter" => '"'
}.fetch(classname) {
raise "need a bad char for #{classname}"
}
@@ -105,17 +92,25 @@ class BasicsTest < ActiveRecord::TestCase
def test_columns_should_obey_set_primary_key
pk = Subscriber.columns_hash[Subscriber.primary_key]
- assert_equal 'nick', pk.name, 'nick should be primary key'
+ assert_equal "nick", pk.name, "nick should be primary key"
end
def test_primary_key_with_no_id
assert_nil Edge.primary_key
end
- unless current_adapter?(:PostgreSQLAdapter, :OracleAdapter, :SQLServerAdapter, :FbAdapter)
- def test_limit_with_comma
- assert Topic.limit("1,2").to_a
- 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.sql_type, ref.sql_type
+ end
+
+ def test_many_mutations
+ car = Car.new name: "<3<3<3"
+ car.engines_count = 0
+ 20_000.times { car.engines_count += 1 }
+ assert car.save
end
def test_limit_without_comma
@@ -145,20 +140,14 @@ class BasicsTest < ActiveRecord::TestCase
end
end
- unless current_adapter?(:MysqlAdapter, :Mysql2Adapter)
- def test_limit_should_allow_sql_literal
- assert_equal 1, Topic.limit(Arel.sql('2-1')).to_a.length
- end
- end
-
def test_select_symbol
topic_ids = Topic.select(:id).map(&:id).sort
assert_equal Topic.pluck(:id).sort, topic_ids
end
def test_table_exists
- assert !NonExistentTable.table_exists?
- assert Topic.table_exists?
+ assert_not_predicate NonExistentTable, :table_exists?
+ assert_predicate Topic, :table_exists?
end
def test_preserving_date_objects
@@ -171,17 +160,17 @@ class BasicsTest < ActiveRecord::TestCase
def test_previously_changed
topic = Topic.first
- topic.title = '<3<3<3'
+ topic.title = "<3<3<3"
assert_equal({}, topic.previous_changes)
topic.save!
expected = ["The First Topic", "<3<3<3"]
- assert_equal(expected, topic.previous_changes['title'])
+ assert_equal(expected, topic.previous_changes["title"])
end
def test_previously_changed_dup
topic = Topic.first
- topic.title = '<3<3<3'
+ topic.title = "<3<3<3"
topic.save!
t2 = topic.dup
@@ -206,7 +195,7 @@ class BasicsTest < ActiveRecord::TestCase
)
# For adapters which support microsecond resolution.
- if current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter) || mysql_56?
+ if subsecond_precision_supported?
assert_equal 11, Topic.find(1).written_on.sec
assert_equal 223300, Topic.find(1).written_on.usec
assert_equal 9900, Topic.find(2).written_on.usec
@@ -215,10 +204,10 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_preserving_time_objects_with_local_time_conversion_to_default_timezone_utc
- with_env_tz 'America/New_York' do
+ with_env_tz eastern_time_zone do
with_timezone_config default: :utc do
time = Time.local(2000)
- topic = Topic.create('written_on' => time)
+ topic = Topic.create("written_on" => time)
saved_time = Topic.find(topic.id).reload.written_on
assert_equal time, saved_time
assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "EST"], time.to_a
@@ -228,11 +217,11 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_preserving_time_objects_with_time_with_zone_conversion_to_default_timezone_utc
- with_env_tz 'America/New_York' do
+ with_env_tz eastern_time_zone do
with_timezone_config default: :utc do
- Time.use_zone 'Central Time (US & Canada)' do
+ Time.use_zone "Central Time (US & Canada)" do
time = Time.zone.local(2000)
- topic = Topic.create('written_on' => time)
+ topic = Topic.create("written_on" => time)
saved_time = Topic.find(topic.id).reload.written_on
assert_equal time, saved_time
assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "CST"], time.to_a
@@ -243,10 +232,10 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_preserving_time_objects_with_utc_time_conversion_to_default_timezone_local
- with_env_tz 'America/New_York' do
+ with_env_tz eastern_time_zone do
with_timezone_config default: :local do
time = Time.utc(2000)
- topic = Topic.create('written_on' => time)
+ topic = Topic.create("written_on" => time)
saved_time = Topic.find(topic.id).reload.written_on
assert_equal time, saved_time
assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "UTC"], time.to_a
@@ -256,11 +245,11 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_preserving_time_objects_with_time_with_zone_conversion_to_default_timezone_local
- with_env_tz 'America/New_York' do
+ with_env_tz eastern_time_zone do
with_timezone_config default: :local do
- Time.use_zone 'Central Time (US & Canada)' do
+ Time.use_zone "Central Time (US & Canada)" do
time = Time.zone.local(2000)
- topic = Topic.create('written_on' => time)
+ topic = Topic.create("written_on" => time)
saved_time = Topic.find(topic.id).reload.written_on
assert_equal time, saved_time
assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "CST"], time.to_a
@@ -270,6 +259,14 @@ class BasicsTest < ActiveRecord::TestCase
end
end
+ def eastern_time_zone
+ if Gem.win_platform?
+ "EST5EDT"
+ else
+ "America/New_York"
+ end
+ end
+
def test_custom_mutator
topic = Topic.find(1)
# This mutator is protected in the class definition
@@ -278,49 +275,50 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_initialize_with_attributes
- topic = Topic.new({
- "title" => "initialized from attributes", "written_on" => "2003-12-12 23:23"
- })
+ topic = Topic.new(
+ "title" => "initialized from attributes", "written_on" => "2003-12-12 23:23")
assert_equal("initialized from attributes", topic.title)
end
def test_initialize_with_invalid_attribute
- Topic.new({ "title" => "test",
- "last_read(1i)" => "2005", "last_read(2i)" => "2", "last_read(3i)" => "31"})
- rescue ActiveRecord::MultiparameterAssignmentErrors => ex
+ ex = assert_raise(ActiveRecord::MultiparameterAssignmentErrors) do
+ Topic.new("title" => "test",
+ "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00")
+ end
+
assert_equal(1, ex.errors.size)
- assert_equal("last_read", ex.errors[0].attribute)
+ assert_equal("written_on", ex.errors[0].attribute)
end
def test_create_after_initialize_without_block
- cb = CustomBulb.create(:name => 'Dude')
- assert_equal('Dude', cb.name)
+ cb = CustomBulb.create(name: "Dude")
+ assert_equal("Dude", cb.name)
assert_equal(true, cb.frickinawesome)
end
def test_create_after_initialize_with_block
- cb = CustomBulb.create {|c| c.name = 'Dude' }
- assert_equal('Dude', cb.name)
+ cb = CustomBulb.create { |c| c.name = "Dude" }
+ assert_equal("Dude", cb.name)
assert_equal(true, cb.frickinawesome)
end
def test_create_after_initialize_with_array_param
- cbs = CustomBulb.create([{ name: 'Dude' }, { name: 'Bob' }])
- assert_equal 'Dude', cbs[0].name
- assert_equal 'Bob', cbs[1].name
+ cbs = CustomBulb.create([{ name: "Dude" }, { name: "Bob" }])
+ assert_equal "Dude", cbs[0].name
+ assert_equal "Bob", cbs[1].name
assert cbs[0].frickinawesome
- assert !cbs[1].frickinawesome
+ assert_not cbs[1].frickinawesome
end
def test_load
- topics = Topic.all.merge!(:order => 'id').to_a
+ topics = Topic.all.merge!(order: "id").to_a
assert_equal(5, topics.size)
assert_equal(topics(:first).title, topics.first.title)
end
def test_load_with_condition
- topics = Topic.all.merge!(:where => "author_name = 'Mary'").to_a
+ topics = Topic.all.merge!(where: "author_name = 'Mary'").to_a
assert_equal(1, topics.size)
assert_equal(topics(:second).title, topics.first.title)
@@ -440,9 +438,9 @@ class BasicsTest < ActiveRecord::TestCase
Post.reset_table_name
end
- if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
+ if current_adapter?(:Mysql2Adapter)
def test_update_all_with_order_and_limit
- assert_equal 1, Topic.limit(1).order('id DESC').update_all(:content => 'bulk updated!')
+ assert_equal 1, Topic.limit(1).order("id DESC").update_all(content: "bulk updated!")
end
end
@@ -453,7 +451,7 @@ class BasicsTest < ActiveRecord::TestCase
def test_default_values
topic = Topic.new
- assert topic.approved?
+ assert_predicate topic, :approved?
assert_nil topic.written_on
assert_nil topic.bonus_time
assert_nil topic.last_read
@@ -461,7 +459,7 @@ class BasicsTest < ActiveRecord::TestCase
topic.save
topic = Topic.find(topic.id)
- assert topic.approved?
+ assert_predicate topic, :approved?
assert_nil topic.last_read
# Oracle has some funky default handling, so it requires a bit of
@@ -487,12 +485,12 @@ class BasicsTest < ActiveRecord::TestCase
def test_utc_as_time_zone_and_new
with_timezone_config default: :utc do
- attributes = { "bonus_time(1i)"=>"2000",
- "bonus_time(2i)"=>"1",
- "bonus_time(3i)"=>"1",
- "bonus_time(4i)"=>"10",
- "bonus_time(5i)"=>"35",
- "bonus_time(6i)"=>"50" }
+ attributes = { "bonus_time(1i)" => "2000",
+ "bonus_time(2i)" => "1",
+ "bonus_time(3i)" => "1",
+ "bonus_time(4i)" => "10",
+ "bonus_time(5i)" => "35",
+ "bonus_time(6i)" => "50" }
topic = Topic.new(attributes)
assert_equal Time.utc(2000, 1, 1, 10, 35, 50), topic.bonus_time
end
@@ -517,15 +515,16 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_find_by_slug
- assert_equal Topic.find('1-meowmeow'), Topic.find(1)
+ assert_equal Topic.find("1-meowmeow"), Topic.find(1)
end
def test_find_by_slug_with_array
- assert_equal Topic.find(['1-meowmeow', '2-hello']), Topic.find([1, 2])
+ assert_equal Topic.find([1, 2]), Topic.find(["1-meowmeow", "2-hello"])
+ assert_equal "The Second Topic of the day", Topic.find(["2-hello", "1-meowmeow"]).first.title
end
def test_find_by_slug_with_range
- assert_equal Topic.where(id: '1-meowmeow'..'2-hello'), Topic.where(id: 1..2)
+ assert_equal Topic.where(id: "1-meowmeow".."2-hello"), Topic.where(id: 1..2)
end
def test_equality_of_new_records
@@ -534,7 +533,7 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_equality_of_destroyed_records
- topic_1 = Topic.new(:title => 'test_1')
+ topic_1 = Topic.new(title: "test_1")
topic_1.save
topic_2 = Topic.find(topic_1.id)
topic_1.destroy
@@ -543,8 +542,8 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_equality_with_blank_ids
- one = Subscriber.new(:id => '')
- two = Subscriber.new(:id => '')
+ one = Subscriber.new(id: "")
+ two = Subscriber.new(id: "")
assert_equal one, two
end
@@ -553,8 +552,8 @@ class BasicsTest < ActiveRecord::TestCase
car.bulbs.build
car.save
- assert car.bulbs == Bulb.where(car_id: car.id), 'CollectionProxy should be comparable with Relation'
- assert Bulb.where(car_id: car.id) == car.bulbs, 'Relation should be comparable with CollectionProxy'
+ assert car.bulbs == Bulb.where(car_id: car.id), "CollectionProxy should be comparable with Relation"
+ assert Bulb.where(car_id: car.id) == car.bulbs, "Relation should be comparable with CollectionProxy"
end
def test_equality_of_relation_and_array
@@ -562,7 +561,7 @@ class BasicsTest < ActiveRecord::TestCase
car.bulbs.build
car.save
- assert Bulb.where(car_id: car.id) == car.bulbs.to_a, 'Relation should be comparable with Array'
+ assert Bulb.where(car_id: car.id) == car.bulbs.to_a, "Relation should be comparable with Array"
end
def test_equality_of_relation_and_association_relation
@@ -570,8 +569,8 @@ class BasicsTest < ActiveRecord::TestCase
car.bulbs.build
car.save
- assert_equal Bulb.where(car_id: car.id), car.bulbs.includes(:car), 'Relation should be comparable with AssociationRelation'
- assert_equal car.bulbs.includes(:car), Bulb.where(car_id: car.id), 'AssociationRelation should be comparable with Relation'
+ assert_equal Bulb.where(car_id: car.id), car.bulbs.includes(:car), "Relation should be comparable with AssociationRelation"
+ assert_equal car.bulbs.includes(:car), Bulb.where(car_id: car.id), "AssociationRelation should be comparable with Relation"
end
def test_equality_of_collection_proxy_and_association_relation
@@ -579,8 +578,8 @@ class BasicsTest < ActiveRecord::TestCase
car.bulbs.build
car.save
- assert_equal car.bulbs, car.bulbs.includes(:car), 'CollectionProxy should be comparable with AssociationRelation'
- assert_equal car.bulbs.includes(:car), car.bulbs, 'AssociationRelation should be comparable with CollectionProxy'
+ assert_equal car.bulbs, car.bulbs.includes(:car), "CollectionProxy should be comparable with AssociationRelation"
+ assert_equal car.bulbs.includes(:car), car.bulbs, "AssociationRelation should be comparable with CollectionProxy"
end
def test_hashing
@@ -602,24 +601,24 @@ class BasicsTest < ActiveRecord::TestCase
def test_create_without_prepared_statement
topic = Topic.connection.unprepared_statement do
- Topic.create(:title => 'foo')
+ Topic.create(title: "foo")
end
assert_equal topic, Topic.find(topic.id)
end
def test_destroy_without_prepared_statement
- topic = Topic.create(title: 'foo')
+ topic = Topic.create(title: "foo")
Topic.connection.unprepared_statement do
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
topic = Topic.create
- category = Category.create(:name => "comparison")
+ category = Category.create(name: "comparison")
assert_nil topic <=> category
end
@@ -631,9 +630,9 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_readonly_attributes
- assert_equal Set.new([ 'title' , 'comments_count' ]), ReadonlyTitlePost.readonly_attributes
+ assert_equal Set.new([ "title", "comments_count" ]), ReadonlyTitlePost.readonly_attributes
- post = ReadonlyTitlePost.create(:title => "cannot change this", :body => "changeable")
+ post = ReadonlyTitlePost.create(title: "cannot change this", body: "changeable")
post.reload
assert_equal "cannot change this", post.title
@@ -645,8 +644,8 @@ class BasicsTest < ActiveRecord::TestCase
def test_unicode_column_name
Weird.reset_column_information
- weird = Weird.create(:なまえ => 'たこ焼き仮面')
- assert_equal 'たこ焼き仮面', weird.なまえ
+ weird = Weird.create(なまえ: "たこ焼き仮面")
+ assert_equal "たこ焼き仮面", weird.なまえ
end
unless current_adapter?(:PostgreSQLAdapter)
@@ -656,7 +655,7 @@ class BasicsTest < ActiveRecord::TestCase
Weird.reset_column_information
- assert_equal ["EUC-JP"], Weird.columns.map {|c| c.name.encoding.name }.uniq
+ assert_equal ["EUC-JP"], Weird.columns.map { |c| c.name.encoding.name }.uniq
ensure
silence_warnings { Encoding.default_internal = old_default_internal }
Weird.reset_column_information
@@ -664,21 +663,21 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_non_valid_identifier_column_name
- weird = Weird.create('a$b' => 'value')
+ weird = Weird.create("a$b" => "value")
weird.reload
- assert_equal 'value', weird.send('a$b')
- assert_equal 'value', weird.read_attribute('a$b')
+ assert_equal "value", weird.send("a$b")
+ assert_equal "value", weird.read_attribute("a$b")
- weird.update_columns('a$b' => 'value2')
+ weird.update_columns("a$b" => "value2")
weird.reload
- assert_equal 'value2', weird.send('a$b')
- assert_equal 'value2', weird.read_attribute('a$b')
+ assert_equal "value2", weird.send("a$b")
+ assert_equal "value2", weird.read_attribute("a$b")
end
def test_group_weirds_by_from
- Weird.create('a$b' => 'value', :from => 'aaron')
+ Weird.create("a$b" => "value", :from => "aaron")
count = Weird.group(Weird.arel_table[:from]).count
- assert_equal 1, count['aaron']
+ assert_equal 1, count["aaron"]
end
def test_attributes_on_dummy_time
@@ -707,46 +706,15 @@ class BasicsTest < ActiveRecord::TestCase
assert_nil topic.bonus_time
end
- def test_boolean
- b_nil = Boolean.create({ "value" => nil })
- nil_id = b_nil.id
- b_false = Boolean.create({ "value" => false })
- false_id = b_false.id
- b_true = Boolean.create({ "value" => true })
- true_id = b_true.id
-
- b_nil = Boolean.find(nil_id)
- assert_nil b_nil.value
- b_false = Boolean.find(false_id)
- assert !b_false.value?
- b_true = Boolean.find(true_id)
- assert b_true.value?
- end
-
- def test_boolean_without_questionmark
- b_true = Boolean.create({ "value" => true })
- true_id = b_true.id
+ def test_attributes
+ category = Category.new(name: "Ruby")
- subclass = Class.new(Boolean).find true_id
- superclass = Boolean.find true_id
+ expected_attributes = category.attribute_names.map do |attribute_name|
+ [attribute_name, category.public_send(attribute_name)]
+ end.to_h
- assert_equal superclass.read_attribute(:has_fun), subclass.read_attribute(:has_fun)
- end
-
- def test_boolean_cast_from_string
- b_blank = Boolean.create({ "value" => "" })
- blank_id = b_blank.id
- b_false = Boolean.create({ "value" => "0" })
- false_id = b_false.id
- b_true = Boolean.create({ "value" => "1" })
- true_id = b_true.id
-
- b_blank = Boolean.find(blank_id)
- assert_nil b_blank.value
- b_false = Boolean.find(false_id)
- assert !b_false.value?
- b_true = Boolean.find(true_id)
- assert b_true.value?
+ assert_instance_of Hash, category.attributes
+ assert_equal expected_attributes, category.attributes
end
def test_new_record_returns_boolean
@@ -759,7 +727,7 @@ class BasicsTest < ActiveRecord::TestCase
duped_topic = nil
assert_nothing_raised { duped_topic = topic.dup }
assert_equal topic.title, duped_topic.title
- assert !duped_topic.persisted?
+ assert_not_predicate duped_topic, :persisted?
# test if the attributes have been duped
topic.title = "a"
@@ -777,7 +745,7 @@ class BasicsTest < ActiveRecord::TestCase
# test if saved clone object differs from original
duped_topic.save
- assert duped_topic.persisted?
+ assert_predicate duped_topic, :persisted?
assert_not_equal duped_topic.id, topic.id
duped_topic.reload
@@ -787,8 +755,8 @@ class BasicsTest < ActiveRecord::TestCase
DeveloperSalary = Struct.new(:amount)
def test_dup_with_aggregate_of_same_name_as_attribute
developer_with_aggregate = Class.new(ActiveRecord::Base) do
- self.table_name = 'developers'
- composed_of :salary, :class_name => 'BasicsTest::DeveloperSalary', :mapping => [%w(salary amount)]
+ self.table_name = "developers"
+ composed_of :salary, class_name: "BasicsTest::DeveloperSalary", mapping: [%w(salary amount)]
end
dev = developer_with_aggregate.find(1)
@@ -798,15 +766,15 @@ class BasicsTest < ActiveRecord::TestCase
assert_nothing_raised { dup = dev.dup }
assert_kind_of DeveloperSalary, dup.salary
assert_equal dev.salary.amount, dup.salary.amount
- assert !dup.persisted?
+ assert_not_predicate dup, :persisted?
- # test if the attributes have been dupd
+ # test if the attributes have been duped
original_amount = dup.salary.amount
dev.salary.amount = 1
assert_equal original_amount, dup.salary.amount
assert dup.save
- assert dup.persisted?
+ assert_predicate dup, :persisted?
assert_not_equal dup.id, dev.id
end
@@ -826,126 +794,87 @@ class BasicsTest < ActiveRecord::TestCase
def test_clone_of_new_object_with_defaults
developer = Developer.new
- assert !developer.name_changed?
- assert !developer.salary_changed?
+ assert_not_predicate developer, :name_changed?
+ assert_not_predicate developer, :salary_changed?
cloned_developer = developer.clone
- assert !cloned_developer.name_changed?
- assert !cloned_developer.salary_changed?
+ assert_not_predicate cloned_developer, :name_changed?
+ assert_not_predicate cloned_developer, :salary_changed?
end
def test_clone_of_new_object_marks_attributes_as_dirty
- developer = Developer.new :name => 'Bjorn', :salary => 100000
- assert developer.name_changed?
- assert developer.salary_changed?
+ developer = Developer.new name: "Bjorn", salary: 100000
+ assert_predicate developer, :name_changed?
+ assert_predicate developer, :salary_changed?
cloned_developer = developer.clone
- assert cloned_developer.name_changed?
- assert cloned_developer.salary_changed?
+ assert_predicate cloned_developer, :name_changed?
+ assert_predicate cloned_developer, :salary_changed?
end
def test_clone_of_new_object_marks_as_dirty_only_changed_attributes
- developer = Developer.new :name => 'Bjorn'
+ developer = Developer.new name: "Bjorn"
assert developer.name_changed? # obviously
- assert !developer.salary_changed? # attribute has non-nil default value, so treated as not changed
+ assert_not developer.salary_changed? # attribute has non-nil default value, so treated as not changed
cloned_developer = developer.clone
- assert cloned_developer.name_changed?
- assert !cloned_developer.salary_changed? # ... and cloned instance should behave same
+ assert_predicate cloned_developer, :name_changed?
+ assert_not cloned_developer.salary_changed? # ... and cloned instance should behave same
end
def test_dup_of_saved_object_marks_attributes_as_dirty
- developer = Developer.create! :name => 'Bjorn', :salary => 100000
- assert !developer.name_changed?
- assert !developer.salary_changed?
+ developer = Developer.create! name: "Bjorn", salary: 100000
+ assert_not_predicate developer, :name_changed?
+ assert_not_predicate developer, :salary_changed?
cloned_developer = developer.dup
assert cloned_developer.name_changed? # both attributes differ from defaults
- assert cloned_developer.salary_changed?
+ assert_predicate cloned_developer, :salary_changed?
end
def test_dup_of_saved_object_marks_as_dirty_only_changed_attributes
- developer = Developer.create! :name => 'Bjorn'
- assert !developer.name_changed? # both attributes of saved object should be treated as not changed
- assert !developer.salary_changed?
+ developer = Developer.create! name: "Bjorn"
+ assert_not developer.name_changed? # both attributes of saved object should be treated as not changed
+ assert_not_predicate developer, :salary_changed?
cloned_developer = developer.dup
assert cloned_developer.name_changed? # ... but on cloned object should be
- assert !cloned_developer.salary_changed? # ... BUT salary has non-nil default which should be treated as not changed on cloned instance
+ assert_not cloned_developer.salary_changed? # ... BUT salary has non-nil default which should be treated as not changed on cloned instance
end
def test_bignum
company = Company.find(1)
- company.rating = 2147483647
+ company.rating = 2147483648
company.save
company = Company.find(1)
- assert_equal 2147483647, company.rating
+ assert_equal 2147483648, company.rating
end
- # TODO: extend defaults tests to other databases!
- if current_adapter?(:PostgreSQLAdapter)
+ def test_bignum_pk
+ company = Company.create!(id: 2147483648, name: "foo")
+ assert_equal company, Company.find(company.id)
+ end
+
+ if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter, :SQLite3Adapter)
def test_default
with_timezone_config default: :local do
default = Default.new
# fixed dates / times
assert_equal Date.new(2004, 1, 1), default.fixed_date
- assert_equal Time.local(2004, 1,1,0,0,0,0), default.fixed_time
+ assert_equal Time.local(2004, 1, 1, 0, 0, 0, 0), default.fixed_time
# char types
- assert_equal 'Y', default.char1
- assert_equal 'a varchar field', default.char2
- assert_equal 'a text field', default.char3
+ assert_equal "Y", default.char1
+ assert_equal "a varchar field", default.char2
+ # Mysql text type can't have default value
+ unless current_adapter?(:Mysql2Adapter)
+ assert_equal "a text field", default.char3
+ end
end
end
end
- class NumericData < ActiveRecord::Base
- self.table_name = 'numeric_data'
-
- attribute :my_house_population, :integer
- attribute :atoms_in_universe, :integer
- end
-
- def test_big_decimal_conditions
- m = NumericData.new(
- :bank_balance => 1586.43,
- :big_bank_balance => BigDecimal("1000234000567.95"),
- :world_population => 6000000000,
- :my_house_population => 3
- )
- assert m.save
- assert_equal 0, NumericData.where("bank_balance > ?", 2000.0).count
- end
-
- def test_numeric_fields
- m = NumericData.new(
- :bank_balance => 1586.43,
- :big_bank_balance => BigDecimal("1000234000567.95"),
- :world_population => 6000000000,
- :my_house_population => 3
- )
- assert m.save
-
- m1 = NumericData.find(m.id)
- assert_not_nil m1
-
- # As with migration_test.rb, we should make world_population >= 2**62
- # to cover 64-bit platforms and test it is a Bignum, but the main thing
- # is that it's an Integer.
- assert_kind_of Integer, m1.world_population
- assert_equal 6000000000, m1.world_population
-
- assert_kind_of Fixnum, m1.my_house_population
- assert_equal 3, m1.my_house_population
-
- assert_kind_of BigDecimal, m1.bank_balance
- assert_equal BigDecimal("1586.43"), m1.bank_balance
-
- assert_kind_of BigDecimal, m1.big_bank_balance
- assert_equal BigDecimal("1000234000567.95"), m1.big_bank_balance
- end
-
def test_auto_id
auto = AutoId.new
auto.save
@@ -969,28 +898,28 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_quoting_arrays
- replies = Reply.all.merge!(:where => [ "id IN (?)", topics(:first).replies.collect(&:id) ]).to_a
+ replies = Reply.all.merge!(where: [ "id IN (?)", topics(:first).replies.collect(&:id) ]).to_a
assert_equal topics(:first).replies.size, replies.size
- replies = Reply.all.merge!(:where => [ "id IN (?)", [] ]).to_a
+ replies = Reply.all.merge!(where: [ "id IN (?)", [] ]).to_a
assert_equal 0, replies.size
end
def test_quote
author_name = "\\ \001 ' \n \\n \""
- topic = Topic.create('author_name' => author_name)
+ topic = Topic.create("author_name" => author_name)
assert_equal author_name, Topic.find(topic.id).author_name
end
def test_toggle_attribute
- assert !topics(:first).approved?
+ assert_not_predicate topics(:first), :approved?
topics(:first).toggle!(:approved)
- assert topics(:first).approved?
+ assert_predicate topics(:first), :approved?
topic = topics(:first)
topic.toggle(:approved)
- assert !topic.approved?
+ assert_not_predicate topic, :approved?
topic.reload
- assert topic.approved?
+ assert_predicate topic, :approved?
end
def test_reload
@@ -1002,12 +931,6 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal t1.title, t2.title
end
- def test_reload_with_exclusive_scope
- dev = DeveloperCalledDavid.first
- dev.update!(name: "NotDavid" )
- assert_equal dev, dev.reload
- end
-
def test_switching_between_table_name
k = Class.new(Joke)
@@ -1020,7 +943,7 @@ class BasicsTest < ActiveRecord::TestCase
end
end
- def test_clear_cash_when_setting_table_name
+ def test_clear_cache_when_setting_table_name
original_table_name = Joke.table_name
Joke.table_name = "funny_jokes"
@@ -1063,7 +986,7 @@ class BasicsTest < ActiveRecord::TestCase
def test_set_table_name_symbol_converted_to_string
k = Class.new(Joke)
k.table_name = :cold_jokes
- assert_equal 'cold_jokes', k.table_name
+ assert_equal "cold_jokes", k.table_name
end
def test_quoted_table_name_after_set_table_name
@@ -1079,7 +1002,7 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_set_table_name_with_inheritance
- k = Class.new( ActiveRecord::Base )
+ k = Class.new(ActiveRecord::Base)
def k.name; "Foo"; end
def k.table_name; super + "ks"; end
assert_equal "foosks", k.table_name
@@ -1097,45 +1020,31 @@ class BasicsTest < ActiveRecord::TestCase
def test_count_with_join
res = Post.count_by_sql "SELECT COUNT(*) FROM posts LEFT JOIN comments ON posts.id=comments.post_id WHERE posts.#{QUOTED_TYPE} = 'Post'"
-
res2 = Post.where("posts.#{QUOTED_TYPE} = 'Post'").joins("LEFT JOIN comments ON posts.id=comments.post_id").count
assert_equal res, res2
- res3 = nil
- assert_nothing_raised do
- res3 = Post.where("posts.#{QUOTED_TYPE} = 'Post'").joins("LEFT JOIN comments ON posts.id=comments.post_id").count
- end
- assert_equal res, res3
-
res4 = Post.count_by_sql "SELECT COUNT(p.id) FROM posts p, comments co WHERE p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id"
- res5 = nil
- assert_nothing_raised do
- res5 = Post.where("p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id").joins("p, comments co").select("p.id").count
- end
-
+ res5 = Post.where("p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id").joins("p, comments co").select("p.id").count
assert_equal res4, res5
res6 = Post.count_by_sql "SELECT COUNT(DISTINCT p.id) FROM posts p, comments co WHERE p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id"
- res7 = nil
- assert_nothing_raised do
- res7 = Post.where("p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id").joins("p, comments co").select("p.id").distinct.count
- end
+ res7 = Post.where("p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id").joins("p, comments co").select("p.id").distinct.count
assert_equal res6, res7
end
def test_no_limit_offset
assert_nothing_raised do
- Developer.all.merge!(:offset => 2).to_a
+ Developer.all.merge!(offset: 2).to_a
end
end
def test_find_last
- last = Developer.last
- assert_equal last, Developer.all.merge!(:order => 'id desc').first
+ last = Developer.last
+ assert_equal last, Developer.all.merge!(order: "id desc").first
end
def test_last
- assert_equal Developer.all.merge!(:order => 'id desc').first, Developer.last
+ assert_equal Developer.all.merge!(order: "id desc").first, Developer.last
end
def test_all
@@ -1145,80 +1054,48 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_all_with_conditions
- assert_equal Developer.all.merge!(:order => 'id desc').to_a, Developer.order('id desc').to_a
+ assert_equal Developer.all.merge!(order: "id desc").to_a, Developer.order("id desc").to_a
end
def test_find_ordered_last
- last = Developer.all.merge!(:order => 'developers.salary ASC').last
- assert_equal last, Developer.all.merge!(:order => 'developers.salary ASC').to_a.last
+ last = Developer.all.merge!(order: "developers.salary ASC").last
+ assert_equal last, Developer.all.merge!(order: "developers.salary ASC").to_a.last
end
def test_find_reverse_ordered_last
- last = Developer.all.merge!(:order => 'developers.salary DESC').last
- assert_equal last, Developer.all.merge!(:order => 'developers.salary DESC').to_a.last
+ last = Developer.all.merge!(order: "developers.salary DESC").last
+ assert_equal last, Developer.all.merge!(order: "developers.salary DESC").to_a.last
end
def test_find_multiple_ordered_last
- last = Developer.all.merge!(:order => 'developers.name, developers.salary DESC').last
- assert_equal last, Developer.all.merge!(:order => 'developers.name, developers.salary DESC').to_a.last
+ last = Developer.all.merge!(order: "developers.name, developers.salary DESC").last
+ assert_equal last, Developer.all.merge!(order: "developers.name, developers.salary DESC").to_a.last
end
def test_find_keeps_multiple_order_values
- combined = Developer.all.merge!(:order => 'developers.name, developers.salary').to_a
- assert_equal combined, Developer.all.merge!(:order => ['developers.name', 'developers.salary']).to_a
+ combined = Developer.all.merge!(order: "developers.name, developers.salary").to_a
+ assert_equal combined, Developer.all.merge!(order: ["developers.name", "developers.salary"]).to_a
end
def test_find_keeps_multiple_group_values
- combined = Developer.all.merge!(:group => 'developers.name, developers.salary, developers.id, developers.created_at, developers.updated_at, developers.created_on, developers.updated_on').to_a
- assert_equal combined, Developer.all.merge!(:group => ['developers.name', 'developers.salary', 'developers.id', 'developers.created_at', 'developers.updated_at', 'developers.created_on', 'developers.updated_on']).to_a
+ combined = Developer.all.merge!(group: "developers.name, developers.salary, developers.id, developers.created_at, developers.updated_at, developers.created_on, developers.updated_on").to_a
+ assert_equal combined, Developer.all.merge!(group: ["developers.name", "developers.salary", "developers.id", "developers.created_at", "developers.updated_at", "developers.created_on", "developers.updated_on"]).to_a
end
def test_find_symbol_ordered_last
- last = Developer.all.merge!(:order => :salary).last
- assert_equal last, Developer.all.merge!(:order => :salary).to_a.last
- end
-
- def test_abstract_class
- assert !ActiveRecord::Base.abstract_class?
- assert LoosePerson.abstract_class?
- assert !LooseDescendant.abstract_class?
+ last = Developer.all.merge!(order: :salary).last
+ assert_equal last, Developer.all.merge!(order: :salary).to_a.last
end
def test_abstract_class_table_name
assert_nil AbstractCompany.table_name
end
- def test_descends_from_active_record
- assert !ActiveRecord::Base.descends_from_active_record?
-
- # Abstract subclass of AR::Base.
- assert LoosePerson.descends_from_active_record?
-
- # Concrete subclass of an abstract class.
- assert LooseDescendant.descends_from_active_record?
-
- # Concrete subclass of AR::Base.
- assert TightPerson.descends_from_active_record?
-
- # Concrete subclass of a concrete class but has no type column.
- assert TightDescendant.descends_from_active_record?
-
- # Concrete subclass of AR::Base.
- assert Post.descends_from_active_record?
-
- # Abstract subclass of a concrete class which has a type column.
- # This is pathological, as you'll never have Sub < Abstract < Concrete.
- assert !StiPost.descends_from_active_record?
-
- # Concrete subclasses an abstract class which has a type column.
- assert !SubStiPost.descends_from_active_record?
- end
-
def test_find_on_abstract_base_class_doesnt_use_type_condition
old_class = LooseDescendant
Object.send :remove_const, :LooseDescendant
- descendant = old_class.create! :first_name => 'bob'
+ descendant = old_class.create! first_name: "bob"
assert_not_nil LoosePerson.find(descendant.id), "Should have found instance of LooseDescendant when finding abstract LoosePerson: #{descendant.inspect}"
ensure
unless Object.const_defined?(:LooseDescendant)
@@ -1227,7 +1104,7 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_assert_queries
- query = lambda { ActiveRecord::Base.connection.execute 'select count(*) from developers' }
+ query = lambda { ActiveRecord::Base.connection.execute "select count(*) from developers" }
assert_queries(2) { 2.times { query.call } }
assert_queries 1, &query
assert_no_queries { assert true }
@@ -1238,9 +1115,9 @@ class BasicsTest < ActiveRecord::TestCase
log = StringIO.new
ActiveRecord::Base.logger = ActiveSupport::Logger.new(log)
ActiveRecord::Base.logger.level = Logger::WARN
- ActiveRecord::Base.benchmark("Debug Topic Count", :level => :debug) { Topic.count }
- ActiveRecord::Base.benchmark("Warn Topic Count", :level => :warn) { Topic.count }
- ActiveRecord::Base.benchmark("Error Topic Count", :level => :error) { Topic.count }
+ ActiveRecord::Base.benchmark("Debug Topic Count", level: :debug) { Topic.count }
+ ActiveRecord::Base.benchmark("Warn Topic Count", level: :warn) { Topic.count }
+ ActiveRecord::Base.benchmark("Error Topic Count", level: :error) { Topic.count }
assert_no_match(/Debug Topic Count/, log.string)
assert_match(/Warn Topic Count/, log.string)
assert_match(/Error Topic Count/, log.string)
@@ -1252,61 +1129,18 @@ class BasicsTest < ActiveRecord::TestCase
original_logger = ActiveRecord::Base.logger
log = StringIO.new
ActiveRecord::Base.logger = ActiveSupport::Logger.new(log)
- ActiveRecord::Base.benchmark("Logging", :level => :debug, :silence => false) { ActiveRecord::Base.logger.debug "Quiet" }
+ ActiveRecord::Base.logger.level = Logger::DEBUG
+ ActiveRecord::Base.benchmark("Logging", level: :debug, silence: false) { ActiveRecord::Base.logger.debug "Quiet" }
assert_match(/Quiet/, log.string)
ensure
ActiveRecord::Base.logger = original_logger
end
- def test_compute_type_success
- assert_equal Author, ActiveRecord::Base.send(:compute_type, 'Author')
- end
-
- def test_compute_type_nonexistent_constant
- e = assert_raises NameError do
- ActiveRecord::Base.send :compute_type, 'NonexistentModel'
- end
- assert_equal 'uninitialized constant ActiveRecord::Base::NonexistentModel', e.message
- assert_equal 'ActiveRecord::Base::NonexistentModel', e.name
- end
-
- def test_compute_type_no_method_error
- ActiveSupport::Dependencies.stubs(:safe_constantize).raises(NoMethodError)
- assert_raises NoMethodError do
- ActiveRecord::Base.send :compute_type, 'InvalidModel'
- end
- end
-
- def test_compute_type_on_undefined_method
- error = nil
- begin
- Class.new(Author) do
- alias_method :foo, :bar
- end
- rescue => e
- error = e
- end
-
- ActiveSupport::Dependencies.stubs(:safe_constantize).raises(e)
-
- exception = assert_raises NameError do
- ActiveRecord::Base.send :compute_type, 'InvalidModel'
- end
- assert_equal error.message, exception.message
- end
-
- def test_compute_type_argument_error
- ActiveSupport::Dependencies.stubs(:safe_constantize).raises(ArgumentError)
- assert_raises ArgumentError do
- ActiveRecord::Base.send :compute_type, 'InvalidModel'
- end
- end
-
def test_clear_cache!
# preheat cache
- c1 = Post.connection.schema_cache.columns('posts')
+ c1 = Post.connection.schema_cache.columns("posts")
ActiveRecord::Base.clear_cache!
- c2 = Post.connection.schema_cache.columns('posts')
+ c2 = Post.connection.schema_cache.columns("posts")
c1.each_with_index do |v, i|
assert_not_same v, c2[i]
end
@@ -1318,17 +1152,18 @@ class BasicsTest < ActiveRecord::TestCase
UnloadablePost.send(:current_scope=, UnloadablePost.all)
UnloadablePost.unloadable
- assert_not_nil ActiveRecord::Scoping::ScopeRegistry.value_for(:current_scope, "UnloadablePost")
+ klass = UnloadablePost
+ assert_not_nil ActiveRecord::Scoping::ScopeRegistry.value_for(:current_scope, klass)
ActiveSupport::Dependencies.remove_unloadable_constants!
- assert_nil ActiveRecord::Scoping::ScopeRegistry.value_for(:current_scope, "UnloadablePost")
+ assert_nil ActiveRecord::Scoping::ScopeRegistry.value_for(:current_scope, klass)
ensure
- Object.class_eval{ remove_const :UnloadablePost } if defined?(UnloadablePost)
+ Object.class_eval { remove_const :UnloadablePost } if defined?(UnloadablePost)
end
def test_marshal_round_trip
expected = posts(:welcome)
marshalled = Marshal.dump(expected)
- actual = Marshal.load(marshalled)
+ actual = Marshal.load(marshalled)
assert_equal expected.attributes, actual.attributes
end
@@ -1395,6 +1230,19 @@ class BasicsTest < ActiveRecord::TestCase
Company.attribute_names
end
+ def test_has_attribute
+ assert Company.has_attribute?("id")
+ assert Company.has_attribute?("type")
+ assert Company.has_attribute?("name")
+ assert_not Company.has_attribute?("lastname")
+ assert_not Company.has_attribute?("age")
+ end
+
+ def test_has_attribute_with_symbol
+ assert Company.has_attribute?(:id)
+ assert_not Company.has_attribute?(:age)
+ end
+
def test_attribute_names_on_table_not_exists
assert_equal [], NonExistentTable.attribute_names
end
@@ -1404,18 +1252,12 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_touch_should_raise_error_on_a_new_object
- company = Company.new(:rating => 1, :name => "37signals", :firm_name => "37signals")
+ company = Company.new(rating: 1, name: "37signals", firm_name: "37signals")
assert_raises(ActiveRecord::ActiveRecordError) do
company.touch :updated_at
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
@@ -1426,37 +1268,47 @@ class BasicsTest < ActiveRecord::TestCase
def test_column_types_typecast
topic = Topic.first
- assert_not_equal 't.lo', topic.author_name
+ assert_not_equal "t.lo", topic.author_name
attrs = topic.attributes.dup
- attrs.delete 'id'
+ attrs.delete "id"
typecast = Class.new(ActiveRecord::Type::Value) {
- def cast value
+ def cast(value)
"t.lo"
end
}
- types = { 'author_name' => typecast.new }
+ types = { "author_name" => typecast.new }
topic = Topic.instantiate(attrs, types)
- assert_equal 't.lo', topic.author_name
+ assert_equal "t.lo", topic.author_name
end
def test_typecasting_aliases
- assert_equal 10, Topic.select('10 as tenderlove').first.tenderlove
+ assert_equal 10, Topic.select("10 as tenderlove").first.tenderlove
end
def test_slice
- company = Company.new(:rating => 1, :name => "37signals", :firm_name => "37signals")
+ company = Company.new(rating: 1, name: "37signals", firm_name: "37signals")
hash = company.slice(:name, :rating, "arbitrary_method")
assert_equal hash[:name], company.name
- assert_equal hash['name'], company.name
+ assert_equal hash["name"], company.name
assert_equal hash[:rating], company.rating
- assert_equal hash['arbitrary_method'], company.arbitrary_method
+ assert_equal hash["arbitrary_method"], company.arbitrary_method
assert_equal hash[:arbitrary_method], company.arbitrary_method
assert_nil hash[:firm_name]
- assert_nil hash['firm_name']
+ assert_nil hash["firm_name"]
+ end
+
+ def test_slice_accepts_array_argument
+ attrs = {
+ title: "slice",
+ author_name: "@Cohen-Carlisle",
+ content: "accept arrays so I don't have to splat"
+ }.with_indifferent_access
+ topic = Topic.new(attrs)
+ assert_equal attrs, topic.slice(attrs.keys)
end
def test_default_values_are_deeply_dupped
@@ -1467,7 +1319,7 @@ class BasicsTest < ActiveRecord::TestCase
test "scoped can take a values hash" do
klass = Class.new(ActiveRecord::Base)
- assert_equal ['foo'], klass.all.merge!(select: 'foo').select_values
+ assert_equal ["foo"], klass.all.merge!(select: "foo").select_values
end
test "connection_handler can be overridden" do
@@ -1506,20 +1358,20 @@ class BasicsTest < ActiveRecord::TestCase
orig_handler = klass.connection_handler
new_handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
after_handler = nil
- latch1 = ActiveSupport::Concurrency::Latch.new
- latch2 = ActiveSupport::Concurrency::Latch.new
+ latch1 = Concurrent::CountDownLatch.new
+ latch2 = Concurrent::CountDownLatch.new
t = Thread.new do
klass.connection_handler = new_handler
- latch1.release
- latch2.await
+ latch1.count_down
+ latch2.wait
after_handler = klass.connection_handler
end
- latch1.await
+ latch1.wait
klass.connection_handler = orig_handler
- latch2.release
+ latch2.count_down
t.join
assert_equal after_handler, new_handler
@@ -1533,13 +1385,99 @@ class BasicsTest < ActiveRecord::TestCase
assert_not_equal Post.new.hash, Post.new.hash
end
+ test "records of different classes have different hashes" do
+ assert_not_equal Post.new(id: 1).hash, Developer.new(id: 1).hash
+ end
+
test "resetting column information doesn't remove attribute methods" do
topic = topics(:first)
- assert_not topic.id_changed?
+ assert_not_predicate topic, :id_changed?
Topic.reset_column_information
- assert_not topic.id_changed?
+ assert_not_predicate topic, :id_changed?
+ end
+
+ test "ignored columns are not present in columns_hash" do
+ cache_columns = Developer.connection.schema_cache.columns_hash(Developer.table_name)
+ assert_includes cache_columns.keys, "first_name"
+ assert_not_includes Developer.columns_hash.keys, "first_name"
+ assert_not_includes SubDeveloper.columns_hash.keys, "first_name"
+ assert_not_includes SymbolIgnoredDeveloper.columns_hash.keys, "first_name"
+ end
+
+ test "ignored columns have no attribute methods" do
+ assert_not_respond_to Developer.new, :first_name
+ assert_not_respond_to Developer.new, :first_name=
+ assert_not_respond_to Developer.new, :first_name?
+ assert_not_respond_to SubDeveloper.new, :first_name
+ assert_not_respond_to SubDeveloper.new, :first_name=
+ assert_not_respond_to SubDeveloper.new, :first_name?
+ assert_not_respond_to SymbolIgnoredDeveloper.new, :first_name
+ assert_not_respond_to SymbolIgnoredDeveloper.new, :first_name=
+ assert_not_respond_to SymbolIgnoredDeveloper.new, :first_name?
+ end
+
+ test "ignored columns don't prevent explicit declaration of attribute methods" do
+ assert_respond_to Developer.new, :last_name
+ assert_respond_to Developer.new, :last_name=
+ assert_respond_to Developer.new, :last_name?
+ assert_respond_to SubDeveloper.new, :last_name
+ assert_respond_to SubDeveloper.new, :last_name=
+ assert_respond_to SubDeveloper.new, :last_name?
+ assert_respond_to SymbolIgnoredDeveloper.new, :last_name
+ assert_respond_to SymbolIgnoredDeveloper.new, :last_name=
+ assert_respond_to SymbolIgnoredDeveloper.new, :last_name?
+ end
+
+ test "ignored columns are stored as an array of string" do
+ assert_equal(%w(first_name last_name), Developer.ignored_columns)
+ assert_equal(%w(first_name last_name), SymbolIgnoredDeveloper.ignored_columns)
+ end
+
+ test "when #reload called, ignored columns' attribute methods are not defined" do
+ developer = Developer.create!(name: "Developer")
+ assert_not_respond_to developer, :first_name
+ assert_not_respond_to developer, :first_name=
+
+ developer.reload
+
+ assert_not_respond_to developer, :first_name
+ assert_not_respond_to developer, :first_name=
+ end
+
+ test "ignored columns not included in SELECT" do
+ query = Developer.all.to_sql.downcase
+
+ # ignored column
+ assert_not query.include?("first_name")
+
+ # regular column
+ assert query.include?("name")
+ end
+
+ test "column names are quoted when using #from clause and model has ignored columns" do
+ assert_not_empty Developer.ignored_columns
+ query = Developer.from("developers").to_sql
+ quoted_id = "#{Developer.quoted_table_name}.#{Developer.quoted_primary_key}"
+
+ assert_match(/SELECT #{Regexp.escape(quoted_id)}.* FROM developers/, query)
+ end
+
+ test "using table name qualified column names unless having SELECT list explicitly" do
+ assert_equal developers(:david), Developer.from("developers").joins(:shared_computers).take
+ end
+
+ test "protected environments by default is an array with production" do
+ assert_equal ["production"], ActiveRecord::Base.protected_environments
+ end
+
+ def test_protected_environments_are_stored_as_an_array_of_string
+ previous_protected_environments = ActiveRecord::Base.protected_environments
+ ActiveRecord::Base.protected_environments = [:staging, "production"]
+ assert_equal ["staging", "production"], ActiveRecord::Base.protected_environments
+ ensure
+ ActiveRecord::Base.protected_environments = previous_protected_environments
end
end
diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb
index 0791dde1f2..d21218a997 100644
--- a/activerecord/test/cases/batches_test.rb
+++ b/activerecord/test/cases/batches_test.rb
@@ -1,6 +1,9 @@
-require 'cases/helper'
-require 'models/post'
-require 'models/subscriber'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/comment"
+require "models/post"
+require "models/subscriber"
class EachTest < ActiveRecord::TestCase
fixtures :posts, :subscribers
@@ -8,12 +11,12 @@ class EachTest < ActiveRecord::TestCase
def setup
@posts = Post.order("id asc")
@total = Post.count
- Post.count('id') # preheat arel's table cache
+ Post.count("id") # preheat arel's table cache
end
def test_each_should_execute_one_query_per_batch
assert_queries(@total + 1) do
- Post.find_each(:batch_size => 1) do |post|
+ Post.find_each(batch_size: 1) do |post|
assert_kind_of Post, post
end
end
@@ -21,31 +24,29 @@ class EachTest < ActiveRecord::TestCase
def test_each_should_not_return_query_chain_and_execute_only_one_query
assert_queries(1) do
- result = Post.find_each(:batch_size => 100000){ }
+ result = Post.find_each(batch_size: 100000) { }
assert_nil result
end
end
def test_each_should_return_an_enumerator_if_no_block_is_present
assert_queries(1) do
- Post.find_each(:batch_size => 100000).with_index do |post, index|
+ Post.find_each(batch_size: 100000).with_index do |post, index|
assert_kind_of Post, post
assert_kind_of Integer, index
end
end
end
- if Enumerator.method_defined? :size
- def test_each_should_return_a_sized_enumerator
- assert_equal 11, Post.find_each(batch_size: 1).size
- assert_equal 5, Post.find_each(batch_size: 2, begin_at: 7).size
- assert_equal 11, Post.find_each(batch_size: 10_000).size
- end
+ def test_each_should_return_a_sized_enumerator
+ assert_equal 11, Post.find_each(batch_size: 1).size
+ assert_equal 5, Post.find_each(batch_size: 2, start: 7).size
+ assert_equal 11, Post.find_each(batch_size: 10_000).size
end
def test_each_enumerator_should_execute_one_query_per_batch
assert_queries(@total + 1) do
- Post.find_each(:batch_size => 1).with_index do |post, index|
+ Post.find_each(batch_size: 1).with_index do |post, index|
assert_kind_of Post, post
assert_kind_of Integer, index
end
@@ -53,7 +54,7 @@ class EachTest < ActiveRecord::TestCase
end
def test_each_should_raise_if_select_is_set_without_id
- assert_raise(RuntimeError) do
+ assert_raise(ArgumentError) do
Post.select(:title).find_each(batch_size: 1) { |post|
flunk "should not call this block"
}
@@ -62,27 +63,45 @@ class EachTest < ActiveRecord::TestCase
def test_each_should_execute_if_id_is_in_select
assert_queries(6) do
- Post.select("id, title, type").find_each(:batch_size => 2) do |post|
+ Post.select("id, title, type").find_each(batch_size: 2) do |post|
assert_kind_of Post, post
end
end
end
- def test_warn_if_limit_scope_is_set
- ActiveRecord::Base.logger.expects(:warn)
- Post.limit(1).find_each { |post| post }
+ test "find_each should honor limit if passed a block" do
+ limit = @total - 1
+ total = 0
+
+ Post.limit(limit).find_each do |post|
+ total += 1
+ end
+
+ assert_equal limit, total
+ end
+
+ test "find_each should honor limit if no block is passed" do
+ limit = @total - 1
+ total = 0
+
+ Post.limit(limit).find_each.each do |post|
+ total += 1
+ end
+
+ assert_equal limit, total
end
def test_warn_if_order_scope_is_set
- ActiveRecord::Base.logger.expects(:warn)
- Post.order("title").find_each { |post| post }
+ assert_called(ActiveRecord::Base.logger, :warn) do
+ Post.order("title").find_each { |post| post }
+ end
end
def test_logger_not_required
previous_logger = ActiveRecord::Base.logger
ActiveRecord::Base.logger = nil
assert_nothing_raised do
- Post.limit(1).find_each { |post| post }
+ Post.order("comments_count DESC").find_each { |post| post }
end
ensure
ActiveRecord::Base.logger = previous_logger
@@ -90,7 +109,7 @@ class EachTest < ActiveRecord::TestCase
def test_find_in_batches_should_return_batches
assert_queries(@total + 1) do
- Post.find_in_batches(:batch_size => 1) do |batch|
+ Post.find_in_batches(batch_size: 1) do |batch|
assert_kind_of Array, batch
assert_kind_of Post, batch.first
end
@@ -99,16 +118,16 @@ class EachTest < ActiveRecord::TestCase
def test_find_in_batches_should_start_from_the_start_option
assert_queries(@total) do
- Post.find_in_batches(batch_size: 1, begin_at: 2) do |batch|
+ Post.find_in_batches(batch_size: 1, start: 2) do |batch|
assert_kind_of Array, batch
assert_kind_of Post, batch.first
end
end
end
- def test_find_in_batches_should_end_at_the_end_option
+ def test_find_in_batches_should_end_at_the_finish_option
assert_queries(6) do
- Post.find_in_batches(batch_size: 1, end_at: 5) do |batch|
+ Post.find_in_batches(batch_size: 1, finish: 5) do |batch|
assert_kind_of Array, batch
assert_kind_of Post, batch.first
end
@@ -117,18 +136,18 @@ class EachTest < ActiveRecord::TestCase
def test_find_in_batches_shouldnt_execute_query_unless_needed
assert_queries(2) do
- Post.find_in_batches(:batch_size => @total) {|batch| assert_kind_of Array, batch }
+ Post.find_in_batches(batch_size: @total) { |batch| assert_kind_of Array, batch }
end
assert_queries(1) do
- Post.find_in_batches(:batch_size => @total + 1) {|batch| assert_kind_of Array, batch }
+ Post.find_in_batches(batch_size: @total + 1) { |batch| assert_kind_of Array, batch }
end
end
def test_find_in_batches_should_quote_batch_order
c = Post.connection
- assert_sql(/ORDER BY #{c.quote_table_name('posts')}.#{c.quote_column_name('id')}/) do
- Post.find_in_batches(:batch_size => 1) do |batch|
+ assert_sql(/ORDER BY #{c.quote_table_name('posts')}\.#{c.quote_column_name('id')}/) do
+ Post.find_in_batches(batch_size: 1) do |batch|
assert_kind_of Array, batch
assert_kind_of Post, batch.first
end
@@ -136,15 +155,16 @@ class EachTest < ActiveRecord::TestCase
end
def test_find_in_batches_should_not_use_records_after_yielding_them_in_case_original_array_is_modified
- not_a_post = "not a post"
- not_a_post.stubs(:id).raises(StandardError, "not_a_post had #id called on it")
-
- assert_nothing_raised do
- Post.find_in_batches(:batch_size => 1) do |batch|
- assert_kind_of Array, batch
- assert_kind_of Post, batch.first
+ not_a_post = +"not a post"
+ def not_a_post.id; end
+ not_a_post.stub(:id, -> { raise StandardError.new("not_a_post had #id called on it") }) do
+ assert_nothing_raised do
+ Post.find_in_batches(batch_size: 1) do |batch|
+ assert_kind_of Array, batch
+ assert_kind_of Post, batch.first
- batch.map! { not_a_post }
+ batch.map! { not_a_post }
+ end
end
end
end
@@ -158,7 +178,43 @@ class EachTest < ActiveRecord::TestCase
end
# posts.first will be ordered using id only. Title order scope should not apply here
assert_not_equal first_post, posts.first
- assert_equal posts(:welcome), posts.first
+ assert_equal posts(:welcome).id, posts.first.id
+ end
+
+ def test_find_in_batches_should_error_on_ignore_the_order
+ assert_raise(ArgumentError) do
+ PostWithDefaultScope.find_in_batches(error_on_ignore: true) { }
+ end
+ end
+
+ def test_find_in_batches_should_not_error_if_config_overridden
+ # Set the config option which will be overridden
+ prev = ActiveRecord::Base.error_on_ignored_order
+ ActiveRecord::Base.error_on_ignored_order = true
+ assert_nothing_raised do
+ PostWithDefaultScope.find_in_batches(error_on_ignore: false) { }
+ end
+ ensure
+ # Set back to default
+ ActiveRecord::Base.error_on_ignored_order = prev
+ end
+
+ def test_find_in_batches_should_error_on_config_specified_to_error
+ # Set the config option
+ prev = ActiveRecord::Base.error_on_ignored_order
+ ActiveRecord::Base.error_on_ignored_order = true
+ assert_raise(ArgumentError) do
+ PostWithDefaultScope.find_in_batches() { }
+ end
+ ensure
+ # Set back to default
+ ActiveRecord::Base.error_on_ignored_order = prev
+ end
+
+ def test_find_in_batches_should_not_error_by_default
+ assert_nothing_raised do
+ PostWithDefaultScope.find_in_batches() { }
+ end
end
def test_find_in_batches_should_not_ignore_the_default_scope_if_it_is_other_then_order
@@ -172,16 +228,16 @@ class EachTest < ActiveRecord::TestCase
def test_find_in_batches_should_not_modify_passed_options
assert_nothing_raised do
- Post.find_in_batches({ batch_size: 42, begin_at: 1 }.freeze){}
+ Post.find_in_batches({ batch_size: 42, start: 1 }.freeze) { }
end
end
def test_find_in_batches_should_use_any_column_as_primary_key
- nick_order_subscribers = Subscriber.order('nick asc')
+ nick_order_subscribers = Subscriber.order("nick asc")
start_nick = nick_order_subscribers.second.nick
subscribers = []
- Subscriber.find_in_batches(batch_size: 1, begin_at: start_nick) do |batch|
+ Subscriber.find_in_batches(batch_size: 1, start: start_nick) do |batch|
subscribers.concat(batch)
end
@@ -199,8 +255,8 @@ class EachTest < ActiveRecord::TestCase
def test_find_in_batches_should_return_an_enumerator
enum = nil
- assert_queries(0) do
- enum = Post.find_in_batches(:batch_size => 1)
+ assert_no_queries do
+ enum = Post.find_in_batches(batch_size: 1)
end
assert_queries(4) do
enum.first(4) do |batch|
@@ -210,34 +266,398 @@ class EachTest < ActiveRecord::TestCase
end
end
- def test_find_in_batches_start_deprecated
- assert_deprecated do
- assert_queries(@total) do
- Post.find_in_batches(batch_size: 1, start: 2) do |batch|
- assert_kind_of Array, batch
- assert_kind_of Post, batch.first
+ test "find_in_batches should honor limit if passed a block" do
+ limit = @total - 1
+ total = 0
+
+ Post.limit(limit).find_in_batches do |batch|
+ total += batch.size
+ end
+
+ assert_equal limit, total
+ end
+
+ test "find_in_batches should honor limit if no block is passed" do
+ limit = @total - 1
+ total = 0
+
+ Post.limit(limit).find_in_batches.each do |batch|
+ total += batch.size
+ end
+
+ assert_equal limit, total
+ end
+
+ def test_in_batches_should_not_execute_any_query
+ assert_no_queries do
+ assert_kind_of ActiveRecord::Batches::BatchEnumerator, Post.in_batches(of: 2)
+ end
+ end
+
+ def test_in_batches_should_yield_relation_if_block_given
+ assert_queries(6) do
+ Post.in_batches(of: 2) do |relation|
+ assert_kind_of ActiveRecord::Relation, relation
+ end
+ end
+ end
+
+ def test_in_batches_should_be_enumerable_if_no_block_given
+ assert_queries(6) do
+ Post.in_batches(of: 2).each do |relation|
+ assert_kind_of ActiveRecord::Relation, relation
+ end
+ end
+ end
+
+ def test_in_batches_each_record_should_yield_record_if_block_is_given
+ assert_queries(6) do
+ Post.in_batches(of: 2).each_record do |post|
+ assert_predicate post.title, :present?
+ assert_kind_of Post, post
+ end
+ end
+ end
+
+ def test_in_batches_each_record_should_return_enumerator_if_no_block_given
+ assert_queries(6) do
+ Post.in_batches(of: 2).each_record.with_index do |post, i|
+ assert_predicate post.title, :present?
+ assert_kind_of Post, post
+ end
+ end
+ end
+
+ def test_in_batches_each_record_should_be_ordered_by_id
+ ids = Post.order("id ASC").pluck(:id)
+ assert_queries(6) do
+ Post.in_batches(of: 2).each_record.with_index do |post, i|
+ assert_equal ids[i], post.id
+ end
+ end
+ end
+
+ def test_in_batches_update_all_affect_all_records
+ assert_queries(6 + 6) do # 6 selects, 6 updates
+ Post.in_batches(of: 2).update_all(title: "updated-title")
+ end
+ assert_equal Post.all.pluck(:title), ["updated-title"] * Post.count
+ end
+
+ def test_in_batches_delete_all_should_not_delete_records_in_other_batches
+ not_deleted_count = Post.where("id <= 2").count
+ Post.where("id > 2").in_batches(of: 2).delete_all
+ assert_equal 0, Post.where("id > 2").count
+ assert_equal not_deleted_count, Post.count
+ end
+
+ def test_in_batches_should_not_be_loaded
+ Post.in_batches(of: 1) do |relation|
+ assert_not_predicate relation, :loaded?
+ end
+
+ Post.in_batches(of: 1, load: false) do |relation|
+ assert_not_predicate relation, :loaded?
+ end
+ end
+
+ def test_in_batches_should_be_loaded
+ Post.in_batches(of: 1, load: true) do |relation|
+ assert_predicate relation, :loaded?
+ end
+ end
+
+ def test_in_batches_if_not_loaded_executes_more_queries
+ assert_queries(@total + 1) do
+ Post.in_batches(of: 1, load: false) do |relation|
+ assert_not_predicate relation, :loaded?
+ end
+ end
+ end
+
+ def test_in_batches_should_return_relations
+ assert_queries(@total + 1) do
+ Post.in_batches(of: 1) do |relation|
+ assert_kind_of ActiveRecord::Relation, relation
+ end
+ end
+ end
+
+ def test_in_batches_should_start_from_the_start_option
+ post = Post.order("id ASC").where("id >= ?", 2).first
+ assert_queries(2) do
+ relation = Post.in_batches(of: 1, start: 2).first
+ assert_equal post, relation.first
+ end
+ end
+
+ def test_in_batches_should_end_at_the_finish_option
+ post = Post.order("id DESC").where("id <= ?", 5).first
+ assert_queries(7) do
+ relation = Post.in_batches(of: 1, finish: 5, load: true).reverse_each.first
+ assert_equal post, relation.last
+ end
+ end
+
+ def test_in_batches_shouldnt_execute_query_unless_needed
+ assert_queries(2) do
+ Post.in_batches(of: @total) { |relation| assert_kind_of ActiveRecord::Relation, relation }
+ end
+
+ assert_queries(1) do
+ Post.in_batches(of: @total + 1) { |relation| assert_kind_of ActiveRecord::Relation, relation }
+ end
+ end
+
+ def test_in_batches_should_quote_batch_order
+ c = Post.connection
+ assert_sql(/ORDER BY #{c.quote_table_name('posts')}\.#{c.quote_column_name('id')}/) do
+ Post.in_batches(of: 1) do |relation|
+ assert_kind_of ActiveRecord::Relation, relation
+ assert_kind_of Post, relation.first
+ end
+ end
+ end
+
+ def test_in_batches_should_not_use_records_after_yielding_them_in_case_original_array_is_modified
+ not_a_post = +"not a post"
+ def not_a_post.id
+ raise StandardError.new("not_a_post had #id called on it")
+ end
+
+ assert_nothing_raised do
+ Post.in_batches(of: 1) do |relation|
+ assert_kind_of ActiveRecord::Relation, relation
+ assert_kind_of Post, relation.first
+
+ relation = [not_a_post] * relation.count
+ end
+ end
+ end
+
+ def test_in_batches_should_not_ignore_default_scope_without_order_statements
+ special_posts_ids = SpecialPostWithDefaultScope.all.map(&:id).sort
+ posts = []
+ SpecialPostWithDefaultScope.in_batches do |relation|
+ posts.concat(relation)
+ end
+ assert_equal special_posts_ids, posts.map(&:id)
+ end
+
+ def test_in_batches_should_not_modify_passed_options
+ assert_nothing_raised do
+ Post.in_batches({ of: 42, start: 1 }.freeze) { }
+ end
+ end
+
+ def test_in_batches_should_use_any_column_as_primary_key
+ nick_order_subscribers = Subscriber.order("nick asc")
+ start_nick = nick_order_subscribers.second.nick
+
+ subscribers = []
+ Subscriber.in_batches(of: 1, start: start_nick) do |relation|
+ subscribers.concat(relation)
+ end
+
+ assert_equal nick_order_subscribers[1..-1].map(&:id), subscribers.map(&:id)
+ end
+
+ def test_in_batches_should_use_any_column_as_primary_key_when_start_is_not_specified
+ assert_queries(Subscriber.count + 1) do
+ Subscriber.in_batches(of: 1, load: true) do |relation|
+ assert_kind_of ActiveRecord::Relation, relation
+ assert_kind_of Subscriber, relation.first
+ end
+ end
+ end
+
+ def test_in_batches_should_return_an_enumerator
+ enum = nil
+ assert_no_queries do
+ enum = Post.in_batches(of: 1)
+ end
+ assert_queries(4) do
+ enum.first(4) do |relation|
+ assert_kind_of ActiveRecord::Relation, relation
+ assert_kind_of Post, relation.first
+ end
+ end
+ end
+
+ def test_in_batches_relations_should_not_overlap_with_each_other
+ seen_posts = []
+ Post.in_batches(of: 2, load: true) do |relation|
+ relation.to_a.each do |post|
+ assert_not seen_posts.include?(post)
+ seen_posts << post
+ end
+ end
+ end
+
+ def test_in_batches_relations_with_condition_should_not_overlap_with_each_other
+ seen_posts = []
+ author_id = Post.first.author_id
+ posts_by_author = Post.where(author_id: author_id)
+ Post.in_batches(of: 2) do |batch|
+ seen_posts += batch.where(author_id: author_id)
+ end
+
+ assert_equal posts_by_author.pluck(:id).sort, seen_posts.map(&:id).sort
+ end
+
+ def test_in_batches_relations_update_all_should_not_affect_matching_records_in_other_batches
+ Post.update_all(author_id: 0)
+ person = Post.last
+ person.update(author_id: 1)
+
+ Post.in_batches(of: 2) do |batch|
+ batch.where("author_id >= 1").update_all("author_id = author_id + 1")
+ end
+ assert_equal 2, person.reload.author_id # incremented only once
+ end
+
+ def test_find_in_batches_should_return_a_sized_enumerator
+ assert_equal 11, Post.find_in_batches(batch_size: 1).size
+ assert_equal 6, Post.find_in_batches(batch_size: 2).size
+ assert_equal 4, Post.find_in_batches(batch_size: 2, start: 4).size
+ assert_equal 4, Post.find_in_batches(batch_size: 3).size
+ assert_equal 1, Post.find_in_batches(batch_size: 10_000).size
+ end
+
+ [true, false].each do |load|
+ test "in_batches should return limit records when limit is less than batch size and load is #{load}" do
+ limit = 3
+ batch_size = 5
+ total = 0
+
+ Post.limit(limit).in_batches(of: batch_size, load: load) do |batch|
+ total += batch.count
+ end
+
+ assert_equal limit, total
+ end
+
+ test "in_batches should return limit records when limit is greater than batch size and load is #{load}" do
+ limit = 5
+ batch_size = 3
+ total = 0
+
+ Post.limit(limit).in_batches(of: batch_size, load: load) do |batch|
+ total += batch.count
+ end
+
+ assert_equal limit, total
+ end
+
+ test "in_batches should return limit records when limit is a multiple of the batch size and load is #{load}" do
+ limit = 6
+ batch_size = 3
+ total = 0
+
+ Post.limit(limit).in_batches(of: batch_size, load: load) do |batch|
+ total += batch.count
+ end
+
+ assert_equal limit, total
+ end
+
+ test "in_batches should return no records if the limit is 0 and load is #{load}" do
+ limit = 0
+ batch_size = 1
+ total = 0
+
+ Post.limit(limit).in_batches(of: batch_size, load: load) do |batch|
+ total += batch.count
+ end
+
+ assert_equal limit, total
+ end
+
+ test "in_batches should return all if the limit is greater than the number of records when load is #{load}" do
+ limit = @total + 1
+ batch_size = 1
+ total = 0
+
+ Post.limit(limit).in_batches(of: batch_size, load: load) do |batch|
+ total += batch.count
+ end
+
+ assert_equal @total, total
+ end
+ end
+
+ test ".find_each respects table alias" do
+ assert_queries(1) do
+ table_alias = Post.arel_table.alias("omg_posts")
+ table_metadata = ActiveRecord::TableMetadata.new(Post, table_alias)
+ predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata)
+
+ posts = ActiveRecord::Relation.create(
+ Post,
+ table: table_alias,
+ predicate_builder: predicate_builder
+ )
+ posts.find_each { }
+ end
+ end
+
+ test ".find_each bypasses the query cache for its own queries" do
+ Post.cache do
+ assert_queries(2) do
+ Post.find_each { }
+ Post.find_each { }
+ end
+ end
+ end
+
+ test ".find_each does not disable the query cache inside the given block" do
+ Post.cache do
+ Post.find_each(start: 1, finish: 1) do |post|
+ assert_queries(1) do
+ post.comments.count
+ post.comments.count
end
end
end
end
- def test_find_each_start_deprecated
- assert_deprecated do
- assert_queries(@total) do
- Post.find_each(batch_size: 1, start: 2) do |post|
- assert_kind_of Post, post
+ test ".find_in_batches bypasses the query cache for its own queries" do
+ Post.cache do
+ assert_queries(2) do
+ Post.find_in_batches { }
+ Post.find_in_batches { }
+ end
+ end
+ end
+
+ test ".find_in_batches does not disable the query cache inside the given block" do
+ Post.cache do
+ Post.find_in_batches(start: 1, finish: 1) do |batch|
+ assert_queries(1) do
+ batch.first.comments.count
+ batch.first.comments.count
end
end
end
end
- if Enumerator.method_defined? :size
- def test_find_in_batches_should_return_a_sized_enumerator
- assert_equal 11, Post.find_in_batches(:batch_size => 1).size
- assert_equal 6, Post.find_in_batches(:batch_size => 2).size
- assert_equal 4, Post.find_in_batches(batch_size: 2, begin_at: 4).size
- assert_equal 4, Post.find_in_batches(:batch_size => 3).size
- assert_equal 1, Post.find_in_batches(:batch_size => 10_000).size
+ test ".in_batches bypasses the query cache for its own queries" do
+ Post.cache do
+ assert_queries(2) do
+ Post.in_batches { }
+ Post.in_batches { }
+ end
+ end
+ end
+
+ test ".in_batches does not disable the query cache inside the given block" do
+ Post.cache do
+ Post.in_batches(start: 1, finish: 1) do |relation|
+ assert_queries(1) do
+ relation.count
+ relation.count
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/binary_test.rb b/activerecord/test/cases/binary_test.rb
index 86dee929bf..58abdb47f7 100644
--- a/activerecord/test/cases/binary_test.rb
+++ b/activerecord/test/cases/binary_test.rb
@@ -1,30 +1,28 @@
+# frozen_string_literal: true
+
require "cases/helper"
# Without using prepared statements, it makes no sense to test
# BLOB data with DB2, because the length of a statement
# is limited to 32KB.
unless current_adapter?(:DB2Adapter)
- require 'models/binary'
+ require "models/binary"
class BinaryTest < ActiveRecord::TestCase
FIXTURES = %w(flowers.jpg example.log test.txt)
def test_mixed_encoding
- str = "\x80"
- str.force_encoding('ASCII-8BIT')
+ str = +"\x80"
+ str.force_encoding("ASCII-8BIT")
- binary = Binary.new :name => 'いただきます!', :data => str
+ binary = Binary.new name: "いただきます!", data: str
binary.save!
binary.reload
assert_equal str, binary.data
name = binary.name
- # MySQL adapter doesn't properly encode things, so we have to do it
- if current_adapter?(:MysqlAdapter)
- name.force_encoding(Encoding::UTF_8)
- end
- assert_equal 'いただきます!', name
+ assert_equal "いただきます!", name
end
def test_load_save
@@ -32,16 +30,16 @@ unless current_adapter?(:DB2Adapter)
FIXTURES.each do |filename|
data = File.read(ASSETS_ROOT + "/#{filename}")
- data.force_encoding('ASCII-8BIT')
+ data.force_encoding("ASCII-8BIT")
data.freeze
- bin = Binary.new(:data => data)
- assert_equal data, bin.data, 'Newly assigned data differs from original'
+ bin = Binary.new(data: data)
+ assert_equal data, bin.data, "Newly assigned data differs from original"
bin.save!
- assert_equal data, bin.data, 'Data differs from original after save'
+ assert_equal data, bin.data, "Data differs from original after save"
- assert_equal data, bin.reload.data, 'Reloaded data differs from original'
+ assert_equal data, bin.reload.data, "Reloaded data differs from original"
end
end
end
diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb
index 1e38b97c4a..bd5f157ca1 100644
--- a/activerecord/test/cases/bind_parameter_test.rb
+++ b/activerecord/test/cases/bind_parameter_test.rb
@@ -1,49 +1,61 @@
-require 'cases/helper'
-require 'models/topic'
-require 'models/author'
-require 'models/post'
+# frozen_string_literal: true
-module ActiveRecord
- class BindParameterTest < ActiveRecord::TestCase
- fixtures :topics, :authors, :posts
+require "cases/helper"
+require "models/topic"
+require "models/author"
+require "models/post"
- class LogListener
- attr_accessor :calls
+if ActiveRecord::Base.connection.prepared_statements
+ module ActiveRecord
+ class BindParameterTest < ActiveRecord::TestCase
+ fixtures :topics, :authors, :author_addresses, :posts
- def initialize
- @calls = []
+ 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
+ def teardown
+ ActiveSupport::Notifications.unsubscribe(@subscription)
+ end
- teardown do
- ActiveSupport::Notifications.unsubscribe(@subscription)
- end
+ def test_too_many_binds
+ bind_params_length = @connection.send(:bind_params_length)
+
+ topics = Topic.where(id: (1 .. bind_params_length).to_a << 2**63)
+ assert_equal Topic.count, topics.count
+
+ topics = Topic.where.not(id: (1 .. bind_params_length).to_a << 2**63)
+ assert_equal 0, topics.count
+ end
- if ActiveRecord::Base.connection.supports_statement_cache?
def test_bind_from_join_in_subquery
- subquery = Author.joins(:thinking_posts).where(name: 'David')
- scope = Author.from(subquery, 'authors').where(id: 1)
+ subquery = Author.joins(:thinking_posts).where(name: "David")
+ scope = Author.from(subquery, "authors").where(id: 1)
assert_equal 1, scope.count
end
def test_binds_are_logged
- sub = @connection.substitute_at(@pk)
+ sub = Arel::Nodes::BindParam.new(1)
binds = [Relation::QueryAttribute.new("id", 1, Type::Value.new)]
sql = "select * from topics where id = #{sub.to_sql}"
- @connection.exec_query(sql, 'SQL', binds)
+ @connection.exec_query(sql, "SQL", binds)
message = @subscriber.calls.find { |args| args[4][:sql] == sql }
assert_equal binds, message[4][:binds]
@@ -52,37 +64,55 @@ module ActiveRecord
def test_find_one_uses_binds
Topic.find(1)
message = @subscriber.calls.find { |args| args[4][:binds].any? { |attr| attr.value == 1 } }
- assert message, 'expected a message with binds'
+ assert message, "expected a message with binds"
end
- def test_logs_bind_vars_after_type_cast
- payload = {
- :name => 'SQL',
- :sql => 'select * from topics where id = ?',
- :binds => [Relation::QueryAttribute.new("id", "10", Type::Integer.new)]
- }
- 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)
+ def test_logs_binds_after_type_cast
+ binds = [Relation::QueryAttribute.new("id", "10", Type::Integer.new)]
+ assert_logs_binds(binds)
end
+
+ def test_logs_legacy_binds_after_type_cast
+ binds = [[@pk, "10"]]
+ assert_logs_binds(binds)
+ end
+
+ def test_deprecate_supports_statement_cache
+ assert_deprecated { ActiveRecord::Base.connection.supports_statement_cache? }
+ 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/boolean_test.rb b/activerecord/test/cases/boolean_test.rb
new file mode 100644
index 0000000000..ab9f974e2c
--- /dev/null
+++ b/activerecord/test/cases/boolean_test.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/boolean"
+
+class BooleanTest < ActiveRecord::TestCase
+ def test_boolean
+ b_nil = Boolean.create!(value: nil)
+ b_false = Boolean.create!(value: false)
+ b_true = Boolean.create!(value: true)
+
+ assert_nil Boolean.find(b_nil.id).value
+ assert_not_predicate Boolean.find(b_false.id), :value?
+ assert_predicate Boolean.find(b_true.id), :value?
+ end
+
+ def test_boolean_without_questionmark
+ b_true = Boolean.create!(value: true)
+
+ subclass = Class.new(Boolean).find(b_true.id)
+ superclass = Boolean.find(b_true.id)
+
+ assert_equal superclass.read_attribute(:has_fun), subclass.read_attribute(:has_fun)
+ end
+
+ def test_boolean_cast_from_string
+ b_blank = Boolean.create!(value: "")
+ b_false = Boolean.create!(value: "0")
+ b_true = Boolean.create!(value: "1")
+
+ assert_nil Boolean.find(b_blank.id).value
+ assert_not_predicate Boolean.find(b_false.id), :value?
+ assert_predicate Boolean.find(b_true.id), :value?
+ end
+
+ def test_find_by_boolean_string
+ b_false = Boolean.create!(value: "false")
+ b_true = Boolean.create!(value: "true")
+
+ assert_equal b_false, Boolean.find_by(value: "false")
+ assert_equal b_true, Boolean.find_by(value: "true")
+ end
+end
diff --git a/activerecord/test/cases/cache_key_test.rb b/activerecord/test/cases/cache_key_test.rb
new file mode 100644
index 0000000000..3a569f226e
--- /dev/null
+++ b/activerecord/test/cases/cache_key_test.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+
+module ActiveRecord
+ class CacheKeyTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
+
+ class CacheMe < ActiveRecord::Base
+ self.cache_versioning = false
+ end
+
+ class CacheMeWithVersion < ActiveRecord::Base
+ self.cache_versioning = true
+ end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table(:cache_mes, force: true) { |t| t.timestamps }
+ @connection.create_table(:cache_me_with_versions, force: true) { |t| t.timestamps }
+ end
+
+ teardown do
+ @connection.drop_table :cache_mes, if_exists: true
+ @connection.drop_table :cache_me_with_versions, if_exists: true
+ end
+
+ test "cache_key format is not too precise" do
+ record = CacheMe.create
+ key = record.cache_key
+
+ assert_equal key, record.reload.cache_key
+ end
+
+ test "cache_key has no version when versioning is on" do
+ record = CacheMeWithVersion.create
+ assert_equal "active_record/cache_key_test/cache_me_with_versions/#{record.id}", record.cache_key
+ end
+
+ test "cache_version is only there when versioning is on" do
+ assert_predicate CacheMeWithVersion.create.cache_version, :present?
+ assert_not_predicate CacheMe.create.cache_version, :present?
+ end
+
+ test "cache_key_with_version always has both key and version" do
+ r1 = CacheMeWithVersion.create
+ assert_equal "active_record/cache_key_test/cache_me_with_versions/#{r1.id}-#{r1.updated_at.to_s(:usec)}", r1.cache_key_with_version
+
+ r2 = CacheMe.create
+ assert_equal "active_record/cache_key_test/cache_mes/#{r2.id}-#{r2.updated_at.to_s(:usec)}", r2.cache_key_with_version
+ end
+ end
+end
diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb
index aa10817527..5c9ed42173 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -1,41 +1,46 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/club'
-require 'models/company'
+require "models/book"
+require "models/club"
+require "models/company"
require "models/contract"
-require 'models/edge'
-require 'models/organization'
-require 'models/possession'
-require 'models/topic'
-require 'models/reply'
-require 'models/minivan'
-require 'models/speedometer'
-require 'models/ship_part'
-require 'models/treasure'
-require 'models/developer'
-require 'models/comment'
-require 'models/rating'
-require 'models/post'
-
-class NumericData < ActiveRecord::Base
- self.table_name = 'numeric_data'
-
- attribute :world_population, :integer
- attribute :my_house_population, :integer
- attribute :atoms_in_universe, :integer
-end
+require "models/edge"
+require "models/organization"
+require "models/possession"
+require "models/topic"
+require "models/reply"
+require "models/numeric_data"
+require "models/minivan"
+require "models/speedometer"
+require "models/ship_part"
+require "models/treasure"
+require "models/developer"
+require "models/post"
+require "models/comment"
+require "models/rating"
class CalculationsTest < ActiveRecord::TestCase
- fixtures :companies, :accounts, :topics, :speedometers, :minivans
+ fixtures :companies, :accounts, :topics, :speedometers, :minivans, :books, :posts, :comments
def test_should_sum_field
assert_equal 318, Account.sum(:credit_limit)
end
+ def test_should_sum_arel_attribute
+ assert_equal 318, Account.sum(Account.arel_table[:credit_limit])
+ end
+
def test_should_average_field
value = Account.average(:credit_limit)
assert_equal 53.0, value
end
+ def test_should_average_arel_attribute
+ value = Account.average(Account.arel_table[:credit_limit])
+ assert_equal 53.0, value
+ end
+
def test_should_resolve_aliased_attributes
assert_equal 318, Account.sum(:available_credit)
end
@@ -47,7 +52,7 @@ class CalculationsTest < ActiveRecord::TestCase
def test_should_return_integer_average_if_db_returns_such
ShipPart.delete_all
- ShipPart.create!(:id => 3, :name => 'foo')
+ ShipPart.create!(id: 3, name: "foo")
value = ShipPart.average(:id)
assert_equal 3, value
end
@@ -60,35 +65,47 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal 60, Account.maximum(:credit_limit)
end
+ def test_should_get_maximum_of_arel_attribute
+ assert_equal 60, Account.maximum(Account.arel_table[:credit_limit])
+ end
+
def test_should_get_maximum_of_field_with_include
assert_equal 55, Account.where("companies.name != 'Summit'").references(:companies).includes(:firm).maximum(:credit_limit)
end
+ def test_should_get_maximum_of_arel_attribute_with_include
+ assert_equal 55, Account.where("companies.name != 'Summit'").references(:companies).includes(:firm).maximum(Account.arel_table[:credit_limit])
+ end
+
def test_should_get_minimum_of_field
assert_equal 50, Account.minimum(:credit_limit)
end
+ def test_should_get_minimum_of_arel_attribute
+ assert_equal 50, Account.minimum(Account.arel_table[:credit_limit])
+ end
+
def test_should_group_by_field
c = Account.group(:firm_id).sum(:credit_limit)
- [1,6,2].each do |firm_id|
- assert c.keys.include?(firm_id), "Group #{c.inspect} does not contain firm_id #{firm_id}"
+ [1, 6, 2].each do |firm_id|
+ assert_includes c.keys, firm_id, "Group #{c.inspect} does not contain firm_id #{firm_id}"
end
end
def test_should_group_by_arel_attribute
c = Account.group(Account.arel_table[:firm_id]).sum(:credit_limit)
- [1,6,2].each do |firm_id|
- assert c.keys.include?(firm_id), "Group #{c.inspect} does not contain firm_id #{firm_id}"
+ [1, 6, 2].each do |firm_id|
+ assert_includes c.keys, firm_id, "Group #{c.inspect} does not contain firm_id #{firm_id}"
end
end
def test_should_group_by_multiple_fields
- c = Account.group('firm_id', :credit_limit).count(:all)
- [ [nil, 50], [1, 50], [6, 50], [6, 55], [9, 53], [2, 60] ].each { |firm_and_limit| assert c.keys.include?(firm_and_limit) }
+ c = Account.group("firm_id", :credit_limit).count(:all)
+ [ [nil, 50], [1, 50], [6, 50], [6, 55], [9, 53], [2, 60] ].each { |firm_and_limit| assert_includes c.keys, firm_and_limit }
end
def test_should_group_by_multiple_fields_having_functions
- c = Topic.group(:author_name, 'COALESCE(type, title)').count(:all)
+ c = Topic.group(:author_name, "COALESCE(type, title)").count(:all)
assert_equal 1, c[["Carl", "The Third Topic of the day"]]
assert_equal 1, c[["Mary", "Reply"]]
assert_equal 1, c[["David", "The First Topic"]]
@@ -102,6 +119,25 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal 60, c[2]
end
+ def test_should_generate_valid_sql_with_joins_and_group
+ assert_nothing_raised do
+ AuditLog.joins(:developer).group(:id).count
+ end
+ end
+
+ def test_should_calculate_against_given_relation
+ developer = Developer.create!(name: "developer")
+ developer.audit_logs.create!(message: "first log")
+ developer.audit_logs.create!(message: "second log")
+
+ c = developer.audit_logs.joins(:developer).group(:id).count
+
+ assert_equal developer.audit_logs.count, c.size
+ developer.audit_logs.each do |log|
+ assert_equal 1, c[log.id]
+ end
+ end
+
def test_should_order_by_grouped_field
c = Account.group(:firm_id).order("firm_id").sum(:credit_limit)
assert_equal [1, 2, 6, 9], c.keys.compact
@@ -125,12 +161,20 @@ 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(4)
+
+ firm_id_attribute = Account.arel_table[:firm_id]
+ assert_equal 3, accounts.count(firm_id_attribute)
+ assert_equal 3, accounts.select(firm_id_attribute).count
+ end
+
def test_count_should_shortcut_with_limit_zero
accounts = Account.limit(0)
@@ -178,15 +222,76 @@ 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_count_with_eager_loading_and_custom_order
+ posts = Post.includes(:comments).order("comments.id")
+ assert_queries(1) { assert_equal 11, posts.count }
+ assert_queries(1) { assert_equal 11, posts.count(:all) }
+ end
+
+ def test_count_with_eager_loading_and_custom_order_and_distinct
+ posts = Post.includes(:comments).order("comments.id").distinct
+ assert_queries(1) { assert_equal 11, posts.count }
+ assert_queries(1) { assert_equal 11, posts.count(:all) }
+ end
+
+ def test_distinct_count_all_with_custom_select_and_order
+ accounts = Account.distinct.select("credit_limit % 10").order(Arel.sql("credit_limit % 10"))
+ assert_queries(1) { assert_equal 3, accounts.count(:all) }
+ assert_queries(1) { assert_equal 3, accounts.load.size }
+ end
+
+ def test_distinct_count_with_order_and_limit
+ assert_equal 4, Account.distinct.order(:firm_id).limit(4).count
+ end
+
+ def test_distinct_count_with_order_and_offset
+ assert_equal 4, Account.distinct.order(:firm_id).offset(2).count
+ end
+
+ def test_distinct_count_with_order_and_limit_and_offset
+ assert_equal 4, Account.distinct.order(:firm_id).limit(4).offset(2).count
+ end
+
+ def test_distinct_joins_count_with_order_and_limit
+ assert_equal 3, Account.joins(:firm).distinct.order(:firm_id).limit(3).count
+ end
+
+ def test_distinct_joins_count_with_order_and_offset
+ assert_equal 3, Account.joins(:firm).distinct.order(:firm_id).offset(2).count
+ end
+
+ def test_distinct_joins_count_with_order_and_limit_and_offset
+ assert_equal 3, Account.joins(:firm).distinct.order(:firm_id).limit(3).offset(2).count
+ end
+
+ def test_distinct_count_with_group_by_and_order_and_limit
+ assert_equal({ 6 => 2 }, Account.group(:firm_id).distinct.order("1 DESC").limit(1).count)
+ end
+
def test_should_group_by_summed_field_having_condition
- c = Account.group(:firm_id).having('sum(credit_limit) > 50').sum(:credit_limit)
+ c = Account.group(:firm_id).having("sum(credit_limit) > 50").sum(:credit_limit)
assert_nil c[1]
assert_equal 105, c[6]
assert_equal 60, c[2]
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]
@@ -200,52 +305,52 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_should_sum_field_with_conditions
- assert_equal 105, Account.where('firm_id = 6').sum(:credit_limit)
+ assert_equal 105, Account.where("firm_id = 6").sum(:credit_limit)
end
def test_should_return_zero_if_sum_conditions_return_nothing
- assert_equal 0, Account.where('1 = 2').sum(:credit_limit)
- assert_equal 0, companies(:rails_core).companies.where('1 = 2').sum(:id)
+ assert_equal 0, Account.where("1 = 2").sum(:credit_limit)
+ assert_equal 0, companies(:rails_core).companies.where("1 = 2").sum(:id)
end
def test_sum_should_return_valid_values_for_decimals
- NumericData.create(:bank_balance => 19.83)
+ NumericData.create(bank_balance: 19.83)
assert_equal 19.83, NumericData.sum(:bank_balance)
end
def test_should_return_type_casted_values_with_group_and_expression
- assert_equal 0.5, Account.group(:firm_name).sum('0.01 * credit_limit')['37signals']
+ assert_equal 0.5, Account.group(:firm_name).sum("0.01 * credit_limit")["37signals"]
end
def test_should_group_by_summed_field_with_conditions
- c = Account.where('firm_id > 1').group(:firm_id).sum(:credit_limit)
+ c = Account.where("firm_id > 1").group(:firm_id).sum(:credit_limit)
assert_nil c[1]
assert_equal 105, c[6]
assert_equal 60, c[2]
end
def test_should_group_by_summed_field_with_conditions_and_having
- c = Account.where('firm_id > 1').group(:firm_id).
- having('sum(credit_limit) > 60').sum(:credit_limit)
+ c = Account.where("firm_id > 1").group(:firm_id).
+ having("sum(credit_limit) > 60").sum(:credit_limit)
assert_nil c[1]
assert_equal 105, c[6]
assert_nil c[2]
end
def test_should_group_by_fields_with_table_alias
- c = Account.group('accounts.firm_id').sum(:credit_limit)
+ c = Account.group("accounts.firm_id").sum(:credit_limit)
assert_equal 50, c[1]
assert_equal 105, c[6]
assert_equal 60, c[2]
end
def test_should_calculate_with_invalid_field
- assert_equal 6, Account.calculate(:count, '*')
+ assert_equal 6, Account.calculate(:count, "*")
assert_equal 6, Account.calculate(:count, :all)
end
def test_should_calculate_grouped_with_invalid_field
- c = Account.group('accounts.firm_id').count(:all)
+ c = Account.group("accounts.firm_id").count(:all)
assert_equal 1, c[1]
assert_equal 2, c[6]
assert_equal 1, c[2]
@@ -259,8 +364,8 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_should_group_by_association_with_non_numeric_foreign_key
- Speedometer.create! id: 'ABC'
- Minivan.create! id: 'OMG', speedometer_id: 'ABC'
+ Speedometer.create! id: "ABC"
+ Minivan.create! id: "OMG", speedometer_id: "ABC"
c = Minivan.group(:speedometer).count(:all)
first_key = c.keys.first
@@ -269,7 +374,7 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_should_calculate_grouped_association_with_foreign_key_option
- Account.belongs_to :another_firm, :class_name => 'Firm', :foreign_key => 'firm_id'
+ Account.belongs_to :another_firm, class_name: "Firm", foreign_key: "firm_id"
c = Account.group(:another_firm).count(:all)
assert_equal 1, c[companies(:first_firm)]
assert_equal 2, c[companies(:rails_core)]
@@ -279,17 +384,17 @@ class CalculationsTest < ActiveRecord::TestCase
def test_should_calculate_grouped_by_function
c = Company.group("UPPER(#{QUOTED_TYPE})").count(:all)
assert_equal 2, c[nil]
- assert_equal 1, c['DEPENDENTFIRM']
- assert_equal 5, c['CLIENT']
- assert_equal 2, c['FIRM']
+ assert_equal 1, c["DEPENDENTFIRM"]
+ assert_equal 5, c["CLIENT"]
+ assert_equal 2, c["FIRM"]
end
def test_should_calculate_grouped_by_function_with_table_alias
c = Company.group("UPPER(companies.#{QUOTED_TYPE})").count(:all)
assert_equal 2, c[nil]
- assert_equal 1, c['DEPENDENTFIRM']
- assert_equal 5, c['CLIENT']
- assert_equal 2, c['FIRM']
+ assert_equal 1, c["DEPENDENTFIRM"]
+ assert_equal 5, c["CLIENT"]
+ assert_equal 2, c["FIRM"]
end
def test_should_not_overshadow_enumerable_sum
@@ -305,19 +410,19 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_should_sum_scoped_field_with_conditions
- assert_equal 8, companies(:rails_core).companies.where('id > 7').sum(:id)
+ assert_equal 8, companies(:rails_core).companies.where("id > 7").sum(:id)
end
def test_should_group_by_scoped_field
c = companies(:rails_core).companies.group(:name).sum(:id)
- assert_equal 7, c['Leetsoft']
- assert_equal 8, c['Jadedpixel']
+ assert_equal 7, c["Leetsoft"]
+ assert_equal 8, c["Jadedpixel"]
end
def test_should_group_by_summed_field_through_association_and_having
- c = companies(:rails_core).companies.group(:name).having('sum(id) > 7').sum(:id)
- assert_nil c['Leetsoft']
- assert_equal 8, c['Jadedpixel']
+ c = companies(:rails_core).companies.group(:name).having("sum(id) > 7").sum(:id)
+ assert_nil c["Leetsoft"]
+ assert_equal 8, c["Jadedpixel"]
end
def test_should_count_selected_field_with_include
@@ -332,7 +437,7 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_should_perform_joined_include_when_referencing_included_tables
- joined_count = Account.includes(:firm).where(:companies => {:name => '37signals'}).count
+ joined_count = Account.includes(:firm).where(companies: { name: "37signals" }).count
assert_equal 1, joined_count
end
@@ -343,26 +448,35 @@ class CalculationsTest < ActiveRecord::TestCase
def test_should_count_scoped_select_with_options
Account.update_all("credit_limit = NULL")
- Account.last.update_columns('credit_limit' => 49)
- Account.first.update_columns('credit_limit' => 51)
+ Account.last.update_columns("credit_limit" => 49)
+ Account.first.update_columns("credit_limit" => 51)
- assert_equal 1, Account.select("credit_limit").where('credit_limit >= 50').count
+ assert_equal 1, Account.select("credit_limit").where("credit_limit >= 50").count
end
def test_should_count_manual_select_with_include
assert_equal 6, Account.select("DISTINCT accounts.id").includes(:firm).count
end
+ def test_count_selected_arel_attribute
+ assert_equal 5, Account.select(Account.arel_table[:firm_id]).count
+ assert_equal 4, Account.distinct.select(Account.arel_table[:firm_id]).count
+ end
+
def test_count_with_column_parameter
assert_equal 5, Account.count(:firm_id)
end
+ def test_count_with_arel_attribute
+ assert_equal 5, Account.count(Account.arel_table[:firm_id])
+ end
+
+ def test_count_with_arel_star
+ assert_equal 6, Account.count(Arel.star)
+ end
+
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
@@ -374,14 +488,29 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_should_count_field_in_joined_table
- assert_equal 5, Account.joins(:firm).count('companies.id')
- assert_equal 4, Account.joins(:firm).distinct.count('companies.id')
+ assert_equal 5, Account.joins(:firm).count("companies.id")
+ assert_equal 4, Account.joins(:firm).distinct.count("companies.id")
+ end
+
+ def test_count_arel_attribute_in_joined_table_with
+ assert_equal 5, Account.joins(:firm).count(Company.arel_table[:id])
+ assert_equal 4, Account.joins(:firm).distinct.count(Company.arel_table[:id])
+ end
+
+ def test_count_selected_arel_attribute_in_joined_table
+ assert_equal 5, Account.joins(:firm).select(Company.arel_table[:id]).count
+ assert_equal 4, Account.joins(:firm).distinct.select(Company.arel_table[:id]).count
end
def test_should_count_field_in_joined_table_with_group_by
- c = Account.group('accounts.firm_id').joins(:firm).count('companies.id')
+ c = Account.group("accounts.firm_id").joins(:firm).count("companies.id")
+
+ [1, 6, 2, 9].each { |firm_id| assert_includes c.keys, firm_id }
+ end
- [1,6,2,9].each { |firm_id| assert c.keys.include?(firm_id) }
+ def test_should_count_field_of_root_table_with_conflicting_group_by_column
+ assert_equal({ 1 => 1 }, Firm.joins(:accounts).group(:firm_id).count)
+ assert_equal({ 1 => 1 }, Firm.joins(:accounts).group("accounts.firm_id").count)
end
def test_count_with_no_parameters_isnt_deprecated
@@ -401,14 +530,17 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_count_with_where_and_order
- assert_equal 1, Account.where(firm_name: '37signals').count
- assert_equal 1, Account.where(firm_name: '37signals').order(:firm_name).count
- assert_equal 1, Account.where(firm_name: '37signals').order(:firm_name).reverse_order.count
+ assert_equal 1, Account.where(firm_name: "37signals").count
+ assert_equal 1, Account.where(firm_name: "37signals").order(:firm_name).count
+ assert_equal 1, Account.where(firm_name: "37signals").order(:firm_name).reverse_order.count
+ end
+
+ def test_count_with_block
+ assert_equal 4, Account.count { |account| account.credit_limit.modulo(10).zero? }
end
def test_should_sum_expression
- # Oracle adapter returns floating point value 636.0 after SUM
- if current_adapter?(:OracleAdapter)
+ if current_adapter?(:SQLite3Adapter, :Mysql2Adapter, :PostgreSQLAdapter, :OracleAdapter)
assert_equal 636, Account.sum("2 * credit_limit")
else
assert_equal 636, Account.sum("2 * credit_limit").to_i
@@ -416,69 +548,69 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_sum_expression_returns_zero_when_no_records_to_sum
- assert_equal 0, Account.where('1 = 2').sum("2 * credit_limit")
+ assert_equal 0, Account.where("1 = 2").sum("2 * credit_limit")
end
def test_count_with_from_option
- assert_equal Company.count(:all), Company.from('companies').count(:all)
+ assert_equal Company.count(:all), Company.from("companies").count(:all)
assert_equal Account.where("credit_limit = 50").count(:all),
- Account.from('accounts').where("credit_limit = 50").count(:all)
- assert_equal Company.where(:type => "Firm").count(:type),
- Company.where(:type => "Firm").from('companies').count(:type)
+ Account.from("accounts").where("credit_limit = 50").count(:all)
+ assert_equal Company.where(type: "Firm").count(:type),
+ Company.where(type: "Firm").from("companies").count(:type)
end
def test_sum_with_from_option
- assert_equal Account.sum(:credit_limit), Account.from('accounts').sum(:credit_limit)
+ assert_equal Account.sum(:credit_limit), Account.from("accounts").sum(:credit_limit)
assert_equal Account.where("credit_limit > 50").sum(:credit_limit),
- Account.where("credit_limit > 50").from('accounts').sum(:credit_limit)
+ Account.where("credit_limit > 50").from("accounts").sum(:credit_limit)
end
def test_average_with_from_option
- assert_equal Account.average(:credit_limit), Account.from('accounts').average(:credit_limit)
+ assert_equal Account.average(:credit_limit), Account.from("accounts").average(:credit_limit)
assert_equal Account.where("credit_limit > 50").average(:credit_limit),
- Account.where("credit_limit > 50").from('accounts').average(:credit_limit)
+ Account.where("credit_limit > 50").from("accounts").average(:credit_limit)
end
def test_minimum_with_from_option
- assert_equal Account.minimum(:credit_limit), Account.from('accounts').minimum(:credit_limit)
+ assert_equal Account.minimum(:credit_limit), Account.from("accounts").minimum(:credit_limit)
assert_equal Account.where("credit_limit > 50").minimum(:credit_limit),
- Account.where("credit_limit > 50").from('accounts').minimum(:credit_limit)
+ Account.where("credit_limit > 50").from("accounts").minimum(:credit_limit)
end
def test_maximum_with_from_option
- assert_equal Account.maximum(:credit_limit), Account.from('accounts').maximum(:credit_limit)
+ assert_equal Account.maximum(:credit_limit), Account.from("accounts").maximum(:credit_limit)
assert_equal Account.where("credit_limit > 50").maximum(:credit_limit),
- Account.where("credit_limit > 50").from('accounts').maximum(:credit_limit)
+ Account.where("credit_limit > 50").from("accounts").maximum(:credit_limit)
end
def test_maximum_with_not_auto_table_name_prefix_if_column_included
- Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)])
+ Company.create!(name: "test", contracts: [Contract.new(developer_id: 7)])
assert_equal 7, Company.includes(:contracts).maximum(:developer_id)
end
def test_minimum_with_not_auto_table_name_prefix_if_column_included
- Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)])
+ Company.create!(name: "test", contracts: [Contract.new(developer_id: 7)])
assert_equal 7, Company.includes(:contracts).minimum(:developer_id)
end
def test_sum_with_not_auto_table_name_prefix_if_column_included
- Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)])
+ Company.create!(name: "test", contracts: [Contract.new(developer_id: 7)])
assert_equal 7, Company.includes(:contracts).sum(:developer_id)
end
- def test_from_option_with_specified_index
- if Edge.connection.adapter_name == 'MySQL' or Edge.connection.adapter_name == 'Mysql2'
- assert_equal Edge.count(:all), Edge.from('edges USE INDEX(unique_edge_index)').count(:all)
- assert_equal Edge.where('sink_id < 5').count(:all),
- Edge.from('edges USE INDEX(unique_edge_index)').where('sink_id < 5').count(:all)
+ if current_adapter?(:Mysql2Adapter)
+ def test_from_option_with_specified_index
+ assert_equal Edge.count(:all), Edge.from("edges USE INDEX(unique_edge_index)").count(:all)
+ assert_equal Edge.where("sink_id < 5").count(:all),
+ Edge.from("edges USE INDEX(unique_edge_index)").where("sink_id < 5").count(:all)
end
end
def test_from_option_with_table_different_than_class
- assert_equal Account.count(:all), Company.from('accounts').count(:all)
+ assert_equal Account.count(:all), Company.from("accounts").count(:all)
end
def test_distinct_is_honored_when_used_with_count_operation_after_group
@@ -491,22 +623,37 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_pluck
- assert_equal [1,2,3,4,5], Topic.order(:id).pluck(:id)
+ assert_equal [1, 2, 3, 4, 5], Topic.order(:id).pluck(:id)
end
def test_pluck_without_column_names
- assert_equal [[1, "Firm", 1, nil, "37signals", nil, 1, nil, ""]],
- Company.order(:id).limit(1).pluck
+ if current_adapter?(:OracleAdapter)
+ assert_equal [[1, "Firm", 1, nil, "37signals", nil, 1, nil, nil]], Company.order(:id).limit(1).pluck
+ else
+ assert_equal [[1, "Firm", 1, nil, "37signals", nil, 1, nil, ""]], Company.order(:id).limit(1).pluck
+ end
end
def test_pluck_type_cast
topic = topics(:first)
- relation = Topic.where(:id => topic.id)
+ relation = Topic.where(id: topic.id)
assert_equal [ topic.approved ], relation.pluck(:approved)
assert_equal [ topic.last_read ], relation.pluck(:last_read)
assert_equal [ topic.written_on ], relation.pluck(:written_on)
end
+ def test_pluck_with_type_cast_does_not_corrupt_the_query_cache
+ topic = topics(:first)
+ relation = Topic.where(id: topic.id)
+ assert_queries 1 do
+ Topic.cache do
+ kind = relation.select(:written_on).load.first.read_attribute_before_type_cast(:written_on).class
+ relation.pluck(:written_on)
+ assert_kind_of kind, relation.select(:written_on).load.first.read_attribute_before_type_cast(:written_on)
+ end
+ end
+ end
+
def test_pluck_and_distinct
assert_equal [50, 53, 55, 60], Account.order(:credit_limit).distinct.pluck(:credit_limit)
end
@@ -518,42 +665,42 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_pluck_on_aliased_attribute
- assert_equal 'The First Topic', Topic.order(:id).pluck(:heading).first
+ assert_equal "The First Topic", Topic.order(:id).pluck(:heading).first
end
def test_pluck_with_serialization
- t = Topic.create!(:content => { :foo => :bar })
- assert_equal [{:foo => :bar}], Topic.where(:id => t.id).pluck(:content)
+ t = Topic.create!(content: { foo: :bar })
+ assert_equal [{ foo: :bar }], Topic.where(id: t.id).pluck(:content)
end
def test_pluck_with_qualified_column_name
- assert_equal [1,2,3,4,5], Topic.order(:id).pluck("topics.id")
+ assert_equal [1, 2, 3, 4, 5], Topic.order(:id).pluck("topics.id")
end
def test_pluck_auto_table_name_prefix
- c = Company.create!(:name => "test", :contracts => [Contract.new])
+ c = Company.create!(name: "test", contracts: [Contract.new])
assert_equal [c.id], Company.joins(:contracts).pluck(:id)
end
def test_pluck_if_table_included
- c = Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)])
+ c = Company.create!(name: "test", contracts: [Contract.new(developer_id: 7)])
assert_equal [c.id], Company.includes(:contracts).where("contracts.id" => c.contracts.first).pluck(:id)
end
def test_pluck_not_auto_table_name_prefix_if_column_joined
- Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)])
+ Company.create!(name: "test", contracts: [Contract.new(developer_id: 7)])
assert_equal [7], Company.joins(:contracts).pluck(:developer_id)
end
def test_pluck_with_selection_clause
- assert_equal [50, 53, 55, 60], Account.pluck('DISTINCT credit_limit').sort
- assert_equal [50, 53, 55, 60], Account.pluck('DISTINCT accounts.credit_limit').sort
- assert_equal [50, 53, 55, 60], Account.pluck('DISTINCT(credit_limit)').sort
+ assert_equal [50, 53, 55, 60], Account.pluck(Arel.sql("DISTINCT credit_limit")).sort
+ assert_equal [50, 53, 55, 60], Account.pluck(Arel.sql("DISTINCT accounts.credit_limit")).sort
+ assert_equal [50, 53, 55, 60], Account.pluck(Arel.sql("DISTINCT(credit_limit)")).sort
# MySQL returns "SUM(DISTINCT(credit_limit))" as the column name unless
# an alias is provided. Without the alias, the column cannot be found
# and properly typecast.
- assert_equal [50 + 53 + 55 + 60], Account.pluck('SUM(DISTINCT(credit_limit)) as credit_limit')
+ assert_equal [50 + 53 + 55 + 60], Account.pluck(Arel.sql("SUM(DISTINCT(credit_limit)) as credit_limit"))
end
def test_plucks_with_ids
@@ -562,11 +709,34 @@ class CalculationsTest < ActiveRecord::TestCase
def test_pluck_with_includes_limit_and_empty_result
assert_equal [], Topic.includes(:replies).limit(0).pluck(:id)
- assert_equal [], Topic.includes(:replies).limit(1).where('0 = 1').pluck(:id)
+ assert_equal [], Topic.includes(:replies).limit(1).where("0 = 1").pluck(:id)
+ end
+
+ def test_pluck_with_includes_offset
+ assert_equal [5], Topic.includes(:replies).order(:id).offset(4).pluck(:id)
+ assert_equal [], Topic.includes(:replies).order(:id).offset(5).pluck(:id)
+ end
+
+ def test_group_by_with_limit
+ expected = { "Post" => 8, "SpecialPost" => 1 }
+ actual = Post.includes(:comments).group(:type).order(:type).limit(2).count("comments.id")
+ assert_equal expected, actual
+ end
+
+ def test_group_by_with_offset
+ expected = { "SpecialPost" => 1, "StiPost" => 2 }
+ actual = Post.includes(:comments).group(:type).order(:type).offset(1).count("comments.id")
+ assert_equal expected, actual
+ end
+
+ def test_group_by_with_limit_and_offset
+ expected = { "SpecialPost" => 1 }
+ actual = Post.includes(:comments).group(:type).order(:type).offset(1).limit(1).count("comments.id")
+ assert_equal expected, actual
end
def test_pluck_not_auto_table_name_prefix_if_column_included
- Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)])
+ Company.create!(name: "test", contracts: [Contract.new(developer_id: 7)])
ids = Company.includes(:contracts).pluck(:developer_id)
assert_equal Company.count, ids.length
assert_equal [7], ids.compact
@@ -587,12 +757,12 @@ class CalculationsTest < ActiveRecord::TestCase
def test_pluck_with_multiple_columns_and_selection_clause
assert_equal [[1, 50], [2, 50], [3, 50], [4, 60], [5, 55], [6, 53]],
- Account.pluck('id, credit_limit')
+ Account.pluck("id, credit_limit")
end
def test_pluck_with_multiple_columns_and_includes
- Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)])
- companies_and_developers = Company.order('companies.id').includes(:contracts).pluck(:name, :developer_id)
+ Company.create!(name: "test", contracts: [Contract.new(developer_id: 7)])
+ companies_and_developers = Company.order("companies.id").includes(:contracts).pluck(:name, :developer_id)
assert_equal Company.count, companies_and_developers.length
assert_equal ["37signals", nil], companies_and_developers.first
@@ -600,21 +770,21 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_pluck_with_reserved_words
- Possession.create!(:where => "Over There")
+ Possession.create!(where: "Over There")
assert_equal ["Over There"], Possession.pluck(:where)
end
def test_pluck_replaces_select_clause
taks_relation = Topic.select(:approved, :id).order(:id)
- assert_equal [1,2,3,4,5], taks_relation.pluck(:id)
+ assert_equal [1, 2, 3, 4, 5], taks_relation.pluck(:id)
assert_equal [false, true, true, true, true], taks_relation.pluck(:approved)
end
def test_pluck_columns_with_same_name
expected = [["The First Topic", "The Second Topic of the day"], ["The Third Topic of the day", "The Fourth Topic of the day"]]
actual = Topic.joins(:replies)
- .pluck('topics.title', 'replies_topics.title')
+ .pluck("topics.title", "replies_topics.title")
assert_equal expected, actual
end
@@ -633,26 +803,49 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_pluck_loaded_relation
+ Company.attribute_names # Load schema information so we don't query below
companies = Company.order(:id).limit(3).load
+
assert_no_queries do
- assert_equal ['37signals', 'Summit', 'Microsoft'], companies.pluck(:name)
+ assert_equal ["37signals", "Summit", "Microsoft"], companies.pluck(:name)
end
end
def test_pluck_loaded_relation_multiple_columns
+ Company.attribute_names # Load schema information so we don't query below
companies = Company.order(:id).limit(3).load
+
assert_no_queries do
- assert_equal [[1, '37signals'], [2, 'Summit'], [3, 'Microsoft']], companies.pluck(:id, :name)
+ assert_equal [[1, "37signals"], [2, "Summit"], [3, "Microsoft"]], companies.pluck(:id, :name)
end
end
def test_pluck_loaded_relation_sql_fragment
+ Company.attribute_names # Load schema information so we don't query below
companies = Company.order(:name).limit(3).load
+
assert_queries 1 do
- assert_equal ['37signals', 'Apex', 'Ex Nihilo'], companies.pluck('DISTINCT name')
+ assert_equal ["37signals", "Apex", "Ex Nihilo"], companies.pluck(Arel.sql("DISTINCT name"))
end
end
+ def test_pick_one
+ assert_equal "The First Topic", Topic.order(:id).pick(:heading)
+ assert_nil Topic.none.pick(:heading)
+ assert_nil Topic.where("1=0").pick(:heading)
+ end
+
+ def test_pick_two
+ assert_equal ["David", "david@loudthinking.com"], Topic.order(:id).pick(:author_name, :author_email_address)
+ assert_nil Topic.none.pick(:author_name, :author_email_address)
+ assert_nil Topic.where("1=0").pick(:author_name, :author_email_address)
+ end
+
+ def test_pick_delegate_to_all
+ cool_first = minivans(:cool_first)
+ assert_equal cool_first.color, Minivan.pick(:color)
+ end
+
def test_grouped_calculation_with_polymorphic_relation
part = ShipPart.create!(name: "has trinket")
part.trinkets.create!
@@ -666,8 +859,8 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_should_reference_correct_aliases_while_joining_tables_of_has_many_through_association
- assert_nothing_raised ActiveRecord::StatementInvalid do
- developer = Developer.create!(name: 'developer')
+ assert_nothing_raised do
+ developer = Developer.create!(name: "developer")
developer.ratings.includes(comment: :post).where(posts: { id: 1 }).count
end
end
@@ -681,4 +874,94 @@ class CalculationsTest < ActiveRecord::TestCase
end
assert block_called
end
+
+ def test_having_with_strong_parameters
+ protected_params = Class.new do
+ attr_reader :permitted
+ alias :permitted? :permitted
+
+ def initialize(parameters)
+ @parameters = parameters
+ @permitted = false
+ end
+
+ def to_h
+ @parameters
+ end
+
+ def permit!
+ @permitted = true
+ self
+ end
+ end
+
+ params = protected_params.new(credit_limit: "50")
+
+ assert_raises(ActiveModel::ForbiddenAttributesError) do
+ Account.group(:id).having(params)
+ end
+
+ result = Account.group(:id).having(params.permit!)
+ assert_equal 50, result[0].credit_limit
+ assert_equal 50, result[1].credit_limit
+ assert_equal 50, result[2].credit_limit
+ end
+
+ def test_group_by_attribute_with_custom_type
+ assert_equal({ "proposed" => 2, "published" => 2 }, Book.group(:status).count)
+ end
+
+ def test_deprecate_count_with_block_and_column_name
+ assert_deprecated do
+ assert_equal 6, Account.count(:firm_id) { true }
+ end
+ end
+
+ def test_deprecate_sum_with_block_and_column_name
+ assert_deprecated do
+ assert_equal 6, Account.sum(:firm_id) { 1 }
+ end
+ end
+
+ test "#skip_query_cache! for #pluck" do
+ Account.cache do
+ assert_queries(1) do
+ Account.pluck(:credit_limit)
+ Account.pluck(:credit_limit)
+ end
+
+ assert_queries(2) do
+ Account.all.skip_query_cache!.pluck(:credit_limit)
+ Account.all.skip_query_cache!.pluck(:credit_limit)
+ end
+ end
+ end
+
+ test "#skip_query_cache! for a simple calculation" do
+ Account.cache do
+ assert_queries(1) do
+ Account.calculate(:sum, :credit_limit)
+ Account.calculate(:sum, :credit_limit)
+ end
+
+ assert_queries(2) do
+ Account.all.skip_query_cache!.calculate(:sum, :credit_limit)
+ Account.all.skip_query_cache!.calculate(:sum, :credit_limit)
+ end
+ end
+ end
+
+ test "#skip_query_cache! for a grouped calculation" do
+ Account.cache do
+ assert_queries(1) do
+ Account.group(:firm_id).calculate(:sum, :credit_limit)
+ Account.group(:firm_id).calculate(:sum, :credit_limit)
+ end
+
+ assert_queries(2) do
+ Account.all.skip_query_cache!.group(:firm_id).calculate(:sum, :credit_limit)
+ Account.all.skip_query_cache!.group(:firm_id).calculate(:sum, :credit_limit)
+ end
+ end
+ end
end
diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb
index 73ac30e547..0ea3fb86a6 100644
--- a/activerecord/test/cases/callbacks_test.rb
+++ b/activerecord/test/cases/callbacks_test.rb
@@ -1,22 +1,20 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/developer'
-require 'models/computer'
+require "models/developer"
+require "models/computer"
class CallbackDeveloper < ActiveRecord::Base
- self.table_name = 'developers'
+ 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
def define_callback_method(callback_method)
define_method(callback_method) do
- self.history << [callback_method, :method]
+ history << [callback_method, :method]
end
send(callback_method, :"#{callback_method}")
end
@@ -31,9 +29,8 @@ class CallbackDeveloper < ActiveRecord::Base
end
ActiveRecord::Callbacks::CALLBACKS.each do |callback_method|
- next if callback_method.to_s =~ /^around_/
+ next if callback_method.to_s.start_with?("around_")
define_callback_method(callback_method)
- 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,30 +41,24 @@ 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] }
end
class ParentDeveloper < ActiveRecord::Base
- self.table_name = 'developers'
+ self.table_name = "developers"
attr_accessor :after_save_called
- before_validation {|record| record.after_save_called = true}
+ before_validation { |record| record.after_save_called = true }
end
class ChildDeveloper < ParentDeveloper
-
end
class ImmutableDeveloper < ActiveRecord::Base
- self.table_name = 'developers'
+ self.table_name = "developers"
- validates_inclusion_of :salary, :in => 50000..200000
+ validates_inclusion_of :salary, in: 50000..200000
before_save :cancel
before_destroy :cancel
@@ -79,7 +70,7 @@ class ImmutableDeveloper < ActiveRecord::Base
end
class DeveloperWithCanceledCallbacks < ActiveRecord::Base
- self.table_name = 'developers'
+ self.table_name = "developers"
validates_inclusion_of :salary, in: 50000..200000
@@ -93,19 +84,19 @@ class DeveloperWithCanceledCallbacks < ActiveRecord::Base
end
class OnCallbacksDeveloper < ActiveRecord::Base
- self.table_name = 'developers'
+ self.table_name = "developers"
before_validation { history << :before_validation }
- before_validation(:on => :create){ history << :before_validation_on_create }
- before_validation(:on => :update){ history << :before_validation_on_update }
+ before_validation(on: :create) { history << :before_validation_on_create }
+ before_validation(on: :update) { history << :before_validation_on_update }
validate do
history << :validate
end
after_validation { history << :after_validation }
- after_validation(:on => :create){ history << :after_validation_on_create }
- after_validation(:on => :update){ history << :after_validation_on_update }
+ after_validation(on: :create) { history << :after_validation_on_create }
+ after_validation(on: :update) { history << :after_validation_on_update }
def history
@history ||= []
@@ -113,24 +104,24 @@ class OnCallbacksDeveloper < ActiveRecord::Base
end
class ContextualCallbacksDeveloper < ActiveRecord::Base
- self.table_name = 'developers'
+ self.table_name = "developers"
before_validation { history << :before_validation }
- before_validation :before_validation_on_create_and_update, :on => [ :create, :update ]
+ before_validation :before_validation_on_create_and_update, on: [ :create, :update ]
validate do
history << :validate
end
after_validation { history << :after_validation }
- after_validation :after_validation_on_create_and_update, :on => [ :create, :update ]
+ 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
@@ -138,25 +129,8 @@ 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'
+ 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
@@ -179,7 +153,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 ],
@@ -190,12 +163,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 ],
@@ -207,17 +178,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 ],
@@ -229,22 +197,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 ],
@@ -252,53 +216,45 @@ class CallbacksTest < ActiveRecord::TestCase
end
def test_create
- david = CallbackDeveloper.create('name' => 'David', 'salary' => 1000000)
+ 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
def test_validate_on_create
- david = OnCallbacksDeveloper.create('name' => 'David', 'salary' => 1000000)
+ david = OnCallbacksDeveloper.create("name" => "David", "salary" => 1000000)
assert_equal [
:before_validation,
:before_validation_on_create,
@@ -309,7 +265,7 @@ class CallbacksTest < ActiveRecord::TestCase
end
def test_validate_on_contextual_create
- david = ContextualCallbacksDeveloper.create('name' => 'David', 'salary' => 1000000)
+ david = ContextualCallbacksDeveloper.create("name" => "David", "salary" => 1000000)
assert_equal [
:before_validation,
:before_validation_on_create,
@@ -324,49 +280,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
@@ -400,29 +347,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
@@ -432,165 +374,71 @@ 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
- assert !someone.after_update_called
+ assert_not someone.after_save_called
+ assert_not someone.after_create_called
+ assert_not someone.after_update_called
end
private :assert_save_callbacks_not_called
def test_before_create_throwing_abort
someone = CallbackHaltedDeveloper.new
someone.cancel_before_create = true
- assert someone.valid?
- assert !someone.save
+ assert_predicate someone, :valid?
+ assert_not someone.save
assert_save_callbacks_not_called(someone)
end
def test_before_save_throwing_abort
david = DeveloperWithCanceledCallbacks.find(1)
- assert david.valid?
- assert !david.save
+ assert_predicate david, :valid?
+ assert_not david.save
exc = assert_raise(ActiveRecord::RecordNotSaved) { david.save! }
- assert_equal exc.record, david
+ assert_equal david, exc.record
david = DeveloperWithCanceledCallbacks.find(1)
david.salary = 10_000_000
- assert !david.valid?
- assert !david.save
+ assert_not_predicate david, :valid?
+ assert_not david.save
assert_raise(ActiveRecord::RecordInvalid) { david.save! }
someone = CallbackHaltedDeveloper.find(1)
someone.cancel_before_save = true
- assert someone.valid?
- assert !someone.save
+ assert_predicate someone, :valid?
+ assert_not someone.save
assert_save_callbacks_not_called(someone)
end
def test_before_update_throwing_abort
someone = CallbackHaltedDeveloper.find(1)
someone.cancel_before_update = true
- assert someone.valid?
- assert !someone.save
+ assert_predicate someone, :valid?
+ assert_not someone.save
assert_save_callbacks_not_called(someone)
end
def test_before_destroy_throwing_abort
david = DeveloperWithCanceledCallbacks.find(1)
- assert !david.destroy
+ assert_not david.destroy
exc = assert_raise(ActiveRecord::RecordNotDestroyed) { david.destroy! }
- assert_equal exc.record, david
+ assert_equal david, exc.record
assert_not_nil ImmutableDeveloper.find_by_id(1)
someone = CallbackHaltedDeveloper.find(1)
someone.cancel_before_destroy = true
- assert !someone.destroy
+ assert_not someone.destroy
assert_raise(ActiveRecord::RecordNotDestroyed) { someone.destroy! }
- assert !someone.after_destroy_called
- 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
+ assert_not someone.after_destroy_called
end
def test_callback_throwing_abort
@@ -598,17 +446,14 @@ 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 ],
@@ -616,21 +461,46 @@ class CallbacksTest < ActiveRecord::TestCase
[ :after_rollback, :block ],
[ :after_rollback, :object ],
[ :after_rollback, :proc ],
- [ :after_rollback, :string ],
[ :after_rollback, :method ],
], david.history
end
def test_inheritance_of_callbacks
parent = ParentDeveloper.new
- assert !parent.after_save_called
+ assert_not parent.after_save_called
parent.save
assert parent.after_save_called
child = ChildDeveloper.new
- assert !child.after_save_called
+ assert_not child.after_save_called
child.save
assert child.after_save_called
end
+ def test_before_save_doesnt_allow_on_option
+ exception = assert_raises ArgumentError do
+ Class.new(ActiveRecord::Base) do
+ before_save(on: :create) { }
+ end
+ end
+ assert_equal "Unknown key: :on. Valid keys are: :if, :unless, :prepend", exception.message
+ end
+
+ def test_around_save_doesnt_allow_on_option
+ exception = assert_raises ArgumentError do
+ Class.new(ActiveRecord::Base) do
+ around_save(on: :create) { }
+ end
+ end
+ assert_equal "Unknown key: :on. Valid keys are: :if, :unless, :prepend", exception.message
+ end
+
+ def test_after_save_doesnt_allow_on_option
+ exception = assert_raises ArgumentError do
+ Class.new(ActiveRecord::Base) do
+ after_save(on: :create) { }
+ end
+ end
+ assert_equal "Unknown key: :on. Valid keys are: :if, :unless, :prepend", exception.message
+ end
end
diff --git a/activerecord/test/cases/clone_test.rb b/activerecord/test/cases/clone_test.rb
index 5e43082c33..eea36ee736 100644
--- a/activerecord/test/cases/clone_test.rb
+++ b/activerecord/test/cases/clone_test.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/topic'
+require "models/topic"
module ActiveRecord
class CloneTest < ActiveRecord::TestCase
@@ -8,9 +10,9 @@ module ActiveRecord
def test_persisted
topic = Topic.first
cloned = topic.clone
- assert topic.persisted?, 'topic persisted'
- assert cloned.persisted?, 'topic persisted'
- assert !cloned.new_record?, 'topic is not new'
+ assert topic.persisted?, "topic persisted"
+ assert cloned.persisted?, "topic persisted"
+ assert_not cloned.new_record?, "topic is not new"
end
def test_stays_frozen
@@ -18,23 +20,23 @@ module ActiveRecord
topic.freeze
cloned = topic.clone
- assert cloned.persisted?, 'topic persisted'
- assert !cloned.new_record?, 'topic is not new'
- assert cloned.frozen?, 'topic should be frozen'
+ assert cloned.persisted?, "topic persisted"
+ assert_not cloned.new_record?, "topic is not new"
+ assert cloned.frozen?, "topic should be frozen"
end
def test_shallow
topic = Topic.first
cloned = topic.clone
- topic.author_name = 'Aaron'
- assert_equal 'Aaron', cloned.author_name
+ topic.author_name = "Aaron"
+ assert_equal "Aaron", cloned.author_name
end
def test_freezing_a_cloned_model_does_not_freeze_clone
cloned = Topic.new
clone = cloned.clone
cloned.freeze
- assert_not clone.frozen?
+ assert_not_predicate clone, :frozen?
end
end
end
diff --git a/activerecord/test/cases/coders/json_test.rb b/activerecord/test/cases/coders/json_test.rb
new file mode 100644
index 0000000000..e40d576b39
--- /dev/null
+++ b/activerecord/test/cases/coders/json_test.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+
+module ActiveRecord
+ module Coders
+ class JSONTest < ActiveRecord::TestCase
+ def test_returns_nil_if_empty_string_given
+ assert_nil JSON.load("")
+ end
+
+ def test_returns_nil_if_nil_given
+ assert_nil JSON.load(nil)
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/coders/yaml_column_test.rb b/activerecord/test/cases/coders/yaml_column_test.rb
index b72c54f97b..4a5559c62f 100644
--- a/activerecord/test/cases/coders/yaml_column_test.rb
+++ b/activerecord/test/cases/coders/yaml_column_test.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
require "cases/helper"
@@ -5,54 +6,56 @@ 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
- assert_equal 'foo', coder.load("foo")
+ 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
- bad_yaml = '--- {'
+ coder = YAMLColumn.new("attr_name")
+ bad_yaml = "--- {"
assert_raises(Psych::SyntaxError) do
coder.load(bad_yaml)
end
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/collection_cache_key_test.rb b/activerecord/test/cases/collection_cache_key_test.rb
new file mode 100644
index 0000000000..483383257b
--- /dev/null
+++ b/activerecord/test/cases/collection_cache_key_test.rb
@@ -0,0 +1,175 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/computer"
+require "models/developer"
+require "models/project"
+require "models/topic"
+require "models/post"
+require "models/comment"
+
+module ActiveRecord
+ class CollectionCacheKeyTest < ActiveRecord::TestCase
+ fixtures :developers, :projects, :developers_projects, :topics, :comments, :posts
+
+ test "collection_cache_key on model" do
+ assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, Developer.collection_cache_key)
+ end
+
+ test "cache_key for relation" do
+ developers = Developer.where(salary: 100000).order(updated_at: :desc)
+ last_developer_timestamp = developers.first.updated_at
+
+ assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key)
+
+ /\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/ =~ developers.cache_key
+
+ assert_equal ActiveSupport::Digest.hexdigest(developers.to_sql), $1
+ assert_equal developers.count.to_s, $2
+ assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3
+ end
+
+ test "cache_key for relation with limit" do
+ developers = Developer.where(salary: 100000).order(updated_at: :desc).limit(5)
+ last_developer_timestamp = developers.first.updated_at
+
+ assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key)
+
+ /\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/ =~ developers.cache_key
+
+ assert_equal ActiveSupport::Digest.hexdigest(developers.to_sql), $1
+ assert_equal developers.count.to_s, $2
+ assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3
+ end
+
+ test "cache_key for relation with custom select and limit" do
+ developers = Developer.where(salary: 100000).order(updated_at: :desc).limit(5)
+ developers_with_select = developers.select("developers.*")
+ last_developer_timestamp = developers.first.updated_at
+
+ assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers_with_select.cache_key)
+
+ /\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/ =~ developers_with_select.cache_key
+
+ assert_equal ActiveSupport::Digest.hexdigest(developers_with_select.to_sql), $1
+ assert_equal developers.count.to_s, $2
+ assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3
+ end
+
+ test "cache_key for loaded relation" do
+ developers = Developer.where(salary: 100000).order(updated_at: :desc).limit(5).load
+ last_developer_timestamp = developers.first.updated_at
+
+ assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key)
+
+ /\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/ =~ developers.cache_key
+
+ assert_equal ActiveSupport::Digest.hexdigest(developers.to_sql), $1
+ assert_equal developers.count.to_s, $2
+ assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3
+ end
+
+ test "cache_key for relation with table alias" do
+ table_alias = Developer.arel_table.alias("omg_developers")
+ table_metadata = ActiveRecord::TableMetadata.new(Developer, table_alias)
+ predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata)
+
+ developers = ActiveRecord::Relation.create(
+ Developer,
+ table: table_alias,
+ predicate_builder: predicate_builder
+ )
+ developers = developers.where(salary: 100000).order(updated_at: :desc)
+ last_developer_timestamp = developers.first.updated_at
+
+ assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key)
+
+ /\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/ =~ developers.cache_key
+
+ assert_equal ActiveSupport::Digest.hexdigest(developers.to_sql), $1
+ assert_equal developers.count.to_s, $2
+ assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3
+ end
+
+ test "cache_key for relation with includes" do
+ comments = Comment.includes(:post).where("posts.type": "Post")
+ assert_match(/\Acomments\/query-(\h+)-(\d+)-(\d+)\z/, comments.cache_key)
+ end
+
+ test "cache_key for loaded relation with includes" do
+ comments = Comment.includes(:post).where("posts.type": "Post").load
+ assert_match(/\Acomments\/query-(\h+)-(\d+)-(\d+)\z/, comments.cache_key)
+ end
+
+ test "it triggers at most one query" do
+ developers = Developer.where(name: "David")
+
+ assert_queries(1) { developers.cache_key }
+ assert_no_queries { developers.cache_key }
+ end
+
+ test "it doesn't trigger any query if the relation is already loaded" do
+ developers = Developer.where(name: "David").load
+ assert_no_queries { developers.cache_key }
+ end
+
+ test "relation cache_key changes when the sql query changes" do
+ developers = Developer.where(name: "David")
+ other_relation = Developer.where(name: "David").where("1 = 1")
+
+ assert_not_equal developers.cache_key, other_relation.cache_key
+ end
+
+ test "cache_key for empty relation" do
+ developers = Developer.where(name: "Non Existent Developer")
+ assert_match(/\Adevelopers\/query-(\h+)-0\z/, developers.cache_key)
+ end
+
+ test "cache_key with custom timestamp column" do
+ topics = Topic.where("title like ?", "%Topic%")
+ last_topic_timestamp = topics(:fifth).written_on.utc.to_s(:usec)
+ assert_match(last_topic_timestamp, topics.cache_key(:written_on))
+ end
+
+ test "cache_key with unknown timestamp column" do
+ topics = Topic.where("title like ?", "%Topic%")
+ assert_raises(ActiveRecord::StatementInvalid) { topics.cache_key(:published_at) }
+ end
+
+ test "collection proxy provides a cache_key" do
+ developers = projects(:active_record).developers
+ assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key)
+ end
+
+ test "cache_key for loaded collection with zero size" do
+ Comment.delete_all
+ posts = Post.includes(:comments)
+ empty_loaded_collection = posts.first.comments
+
+ assert_match(/\Acomments\/query-(\h+)-0\z/, empty_loaded_collection.cache_key)
+ end
+
+ test "cache_key for queries with offset which return 0 rows" do
+ developers = Developer.offset(20)
+ assert_match(/\Adevelopers\/query-(\h+)-0\z/, developers.cache_key)
+ end
+
+ test "cache_key with a relation having selected columns" do
+ developers = Developer.select(:salary)
+
+ assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key)
+ end
+
+ test "cache_key with a relation having distinct and order" do
+ developers = Developer.distinct.order(:salary).limit(5)
+
+ assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key)
+ end
+
+ test "cache_key with a relation having custom select and order" do
+ developers = Developer.select("name AS dev_name").order("dev_name DESC").limit(5)
+
+ assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key)
+ end
+ end
+end
diff --git a/activerecord/test/cases/column_alias_test.rb b/activerecord/test/cases/column_alias_test.rb
index 40707d9cb2..a883d21fb8 100644
--- a/activerecord/test/cases/column_alias_test.rb
+++ b/activerecord/test/cases/column_alias_test.rb
@@ -1,17 +1,19 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/topic'
+require "models/topic"
class TestColumnAlias < ActiveRecord::TestCase
fixtures :topics
- QUERY = if 'Oracle' == ActiveRecord::Base.connection.adapter_name
- 'SELECT id AS pk FROM topics WHERE ROWNUM < 2'
- else
- 'SELECT id AS pk FROM topics'
- end
+ QUERY = if "Oracle" == ActiveRecord::Base.connection.adapter_name
+ "SELECT id AS pk FROM topics WHERE ROWNUM < 2"
+ else
+ "SELECT id AS pk FROM topics"
+ end
def test_column_alias
records = Topic.connection.select_all(QUERY)
- assert_equal 'pk', records[0].keys[0]
+ assert_equal "pk", records[0].keys[0]
end
end
diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb
index 14b95ecab1..cbd2b44589 100644
--- a/activerecord/test/cases/column_definition_test.rb
+++ b/activerecord/test/cases/column_definition_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
module ActiveRecord
@@ -6,76 +8,26 @@ module ActiveRecord
def setup
@adapter = AbstractAdapter.new(nil)
def @adapter.native_database_types
- {:string => "varchar"}
+ { string: "varchar" }
end
- @viz = @adapter.schema_creation
+ @viz = @adapter.send(:schema_creation)
end
# 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)
- assert_equal "title varchar(20)", @viz.accept(column_def)
+ 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)
- assert_equal %Q{title varchar(20) DEFAULT 'Hello'}, @viz.accept(column_def)
+ 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)
- assert_equal %Q{title varchar(20) DEFAULT 'Hello' NOT NULL}, @viz.accept(column_def)
- end
-
- if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
- def test_should_set_default_for_mysql_binary_data_types
- type = SqlTypeMetadata.new(type: :binary, sql_type: "binary(1)")
- binary_column = AbstractMysqlAdapter::Column.new("title", "a", type)
- assert_equal "a", binary_column.default
-
- type = SqlTypeMetadata.new(type: :binary, sql_type: "varbinary")
- varbinary_column = AbstractMysqlAdapter::Column.new("title", "a", type)
- assert_equal "a", varbinary_column.default
- end
-
- def test_should_not_set_default_for_blob_and_text_data_types
- assert_raise ArgumentError do
- AbstractMysqlAdapter::Column.new("title", "a", SqlTypeMetadata.new(sql_type: "blob"))
- end
-
- text_type = AbstractMysqlAdapter::MysqlTypeMetadata.new(
- SqlTypeMetadata.new(type: :text))
- assert_raise ArgumentError do
- AbstractMysqlAdapter::Column.new("title", "Hello", text_type)
- end
-
- text_column = AbstractMysqlAdapter::Column.new("title", nil, text_type)
- assert_equal nil, text_column.default
-
- not_null_text_column = AbstractMysqlAdapter::Column.new("title", nil, text_type, false)
- assert_equal "", 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 = AbstractMysqlAdapter::Column.new("title", nil, binary_type)
- assert !blob_column.has_default?
-
- text_type = SqlTypeMetadata.new(type: :text)
- text_column = AbstractMysqlAdapter::Column.new("title", nil, text_type)
- assert !text_column.has_default?
- end
+ 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
end
end
diff --git a/activerecord/test/cases/comment_test.rb b/activerecord/test/cases/comment_test.rb
new file mode 100644
index 0000000000..584e03d196
--- /dev/null
+++ b/activerecord/test/cases/comment_test.rb
@@ -0,0 +1,168 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "support/schema_dumping_helper"
+
+if ActiveRecord::Base.connection.supports_comments?
+ class CommentTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
+
+ class Commented < ActiveRecord::Base
+ self.table_name = "commenteds"
+ end
+
+ class BlankComment < ActiveRecord::Base
+ end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+
+ @connection.create_table("commenteds", comment: "A table with comment", force: true) do |t|
+ t.string "name", comment: "Comment should help clarify the column purpose"
+ t.boolean "obvious", comment: "Question is: should you comment obviously named objects?"
+ t.string "content"
+ t.index "name", comment: %Q["Very important" index that powers all the performance.\nAnd it's fun!]
+ end
+
+ @connection.create_table("blank_comments", comment: " ", force: true) do |t|
+ t.string :space_comment, comment: " "
+ t.string :empty_comment, comment: ""
+ t.string :nil_comment, comment: nil
+ t.string :absent_comment
+ t.index :space_comment, comment: " "
+ t.index :empty_comment, comment: ""
+ t.index :nil_comment, comment: nil
+ t.index :absent_comment
+ end
+
+ Commented.reset_column_information
+ BlankComment.reset_column_information
+ end
+
+ teardown do
+ @connection.drop_table "commenteds", if_exists: true
+ @connection.drop_table "blank_comments", if_exists: true
+ end
+
+ def test_column_created_in_block
+ column = Commented.columns_hash["name"]
+ assert_equal :string, column.type
+ assert_equal "Comment should help clarify the column purpose", column.comment
+ end
+
+ def test_blank_columns_created_in_block
+ %w[ space_comment empty_comment nil_comment absent_comment ].each do |field|
+ column = BlankComment.columns_hash[field]
+ assert_equal :string, column.type
+ assert_nil column.comment
+ end
+ end
+
+ def test_blank_indexes_created_in_block
+ @connection.indexes("blank_comments").each do |index|
+ assert_nil index.comment
+ end
+ end
+
+ def test_add_column_with_comment_later
+ @connection.add_column :commenteds, :rating, :integer, comment: "I am running out of imagination"
+ Commented.reset_column_information
+ column = Commented.columns_hash["rating"]
+
+ assert_equal :integer, column.type
+ assert_equal "I am running out of imagination", column.comment
+ end
+
+ def test_add_index_with_comment_later
+ unless current_adapter?(:OracleAdapter)
+ @connection.add_index :commenteds, :obvious, name: "idx_obvious", comment: "We need to see obvious comments"
+ index = @connection.indexes("commenteds").find { |idef| idef.name == "idx_obvious" }
+ assert_equal "We need to see obvious comments", index.comment
+ end
+ end
+
+ def test_add_comment_to_column
+ @connection.change_column :commenteds, :content, :string, comment: "Whoa, content describes itself!"
+
+ Commented.reset_column_information
+ column = Commented.columns_hash["content"]
+
+ assert_equal :string, column.type
+ assert_equal "Whoa, content describes itself!", column.comment
+ end
+
+ def test_remove_comment_from_column
+ @connection.change_column :commenteds, :obvious, :string, comment: nil
+
+ Commented.reset_column_information
+ column = Commented.columns_hash["obvious"]
+
+ assert_equal :string, column.type
+ assert_nil column.comment
+ end
+
+ def test_schema_dump_with_comments
+ # Do all the stuff from other tests
+ @connection.add_column :commenteds, :rating, :integer, comment: "I am running out of imagination"
+ @connection.change_column :commenteds, :content, :string, comment: "Whoa, content describes itself!"
+ @connection.change_column :commenteds, :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"
+
+ # And check that these changes are reflected in dump
+ output = dump_table_schema "commenteds"
+ assert_match %r[create_table "commenteds",.*\s+comment: "A table with comment"], output
+ assert_match %r[t\.string\s+"name",\s+comment: "Comment should help clarify the column purpose"], output
+ assert_match %r[t\.string\s+"obvious"\n], output
+ assert_match %r[t\.string\s+"content",\s+comment: "Whoa, content describes itself!"], output
+ if current_adapter?(:OracleAdapter)
+ assert_match %r[t\.integer\s+"rating",\s+precision: 38,\s+comment: "I am running out of imagination"], output
+ else
+ 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
+ end
+ end
+
+ def test_schema_dump_omits_blank_comments
+ output = dump_table_schema "blank_comments"
+
+ assert_match %r[create_table "blank_comments"], output
+ assert_no_match %r[create_table "blank_comments",.+comment:], output
+
+ assert_match %r[t\.string\s+"space_comment"\n], output
+ assert_no_match %r[t\.string\s+"space_comment", comment:\n], output
+
+ assert_match %r[t\.string\s+"empty_comment"\n], output
+ assert_no_match %r[t\.string\s+"empty_comment", comment:\n], output
+
+ assert_match %r[t\.string\s+"nil_comment"\n], output
+ assert_no_match %r[t\.string\s+"nil_comment", comment:\n], output
+
+ assert_match %r[t\.string\s+"absent_comment"\n], output
+ assert_no_match %r[t\.string\s+"absent_comment", comment:\n], output
+ end
+
+ def test_change_table_comment
+ @connection.change_table_comment :commenteds, "Edited table comment"
+ assert_equal "Edited table comment", @connection.table_comment("commenteds")
+ end
+
+ def test_change_table_comment_to_nil
+ @connection.change_table_comment :commenteds, nil
+ assert_nil @connection.table_comment("commenteds")
+ end
+
+ def test_change_column_comment
+ @connection.change_column_comment :commenteds, :name, "Edited column comment"
+ column = Commented.columns_hash["name"]
+ assert_equal "Edited column comment", column.comment
+ end
+
+ def test_change_column_comment_to_nil
+ @connection.change_column_comment :commenteds, :name, nil
+ column = Commented.columns_hash["name"]
+ assert_nil column.comment
+ end
+ end
+end
diff --git a/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb b/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb
index 580568c8ac..72838ff56b 100644
--- a/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb
+++ b/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
module ActiveRecord
@@ -17,37 +19,37 @@ module ActiveRecord
end
def test_in_use?
- assert_not @adapter.in_use?, 'adapter is not in use'
- assert @adapter.lease, 'lease adapter'
- assert @adapter.in_use?, 'adapter is in use'
+ assert_not @adapter.in_use?, "adapter is not in use"
+ assert @adapter.lease, "lease adapter"
+ assert @adapter.in_use?, "adapter is in use"
end
def test_lease_twice
- assert @adapter.lease, 'should lease adapter'
+ assert @adapter.lease, "should lease adapter"
assert_raises(ActiveRecordError) do
@adapter.lease
end
end
def test_expire_mutates_in_use
- assert @adapter.lease, 'lease adapter'
- assert @adapter.in_use?, 'adapter is in use'
+ assert @adapter.lease, "lease adapter"
+ assert @adapter.in_use?, "adapter is in use"
@adapter.expire
- assert_not @adapter.in_use?, 'adapter is in use'
+ assert_not @adapter.in_use?, "adapter is in use"
end
def test_close
- pool = Pool.new(ConnectionSpecification.new({}, nil))
+ pool = Pool.new(ConnectionSpecification.new("primary", {}, nil))
pool.insert_connection_for_test! @adapter
@adapter.pool = pool
# Make sure the pool marks the connection in use
assert_equal @adapter, pool.connection
- assert @adapter.in_use?
+ assert_predicate @adapter, :in_use?
# Close should put the adapter back in the pool
@adapter.close
- assert_not @adapter.in_use?
+ assert_not_predicate @adapter, :in_use?
assert_equal @adapter, pool.connection
end
diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb
index 9b1865e8bb..51d0cc3d12 100644
--- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb
+++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb
@@ -1,46 +1,203 @@
+# frozen_string_literal: true
+
require "cases/helper"
+require "models/person"
module ActiveRecord
module ConnectionAdapters
class ConnectionHandlerTest < ActiveRecord::TestCase
- def setup
- @klass = Class.new(Base) { def self.name; 'klass'; end }
- @subklass = Class.new(@klass) { def self.name; 'subklass'; end }
+ self.use_transactional_tests = false
+ fixtures :people
+
+ def setup
@handler = ConnectionHandler.new
- @pool = @handler.establish_connection(@klass, Base.connection_pool.spec)
+ @spec_name = "primary"
+ @pool = @handler.establish_connection(ActiveRecord::Base.configurations["arunit"])
end
- def test_retrieve_connection
- assert @handler.retrieve_connection(@klass)
+ def test_default_env_fall_back_to_default_env_when_rails_env_or_rack_env_is_empty_string
+ original_rails_env = ENV["RAILS_ENV"]
+ original_rack_env = ENV["RACK_ENV"]
+ ENV["RAILS_ENV"] = ENV["RACK_ENV"] = ""
+
+ assert_equal "default_env", ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
+ ensure
+ ENV["RAILS_ENV"] = original_rails_env
+ ENV["RACK_ENV"] = original_rack_env
end
- def test_active_connections?
- assert !@handler.active_connections?
- assert @handler.retrieve_connection(@klass)
- assert @handler.active_connections?
- @handler.clear_active_connections!
- assert !@handler.active_connections?
+ def test_establish_connection_uses_spec_name
+ old_config = ActiveRecord::Base.configurations
+ config = { "readonly" => { "adapter" => "sqlite3" } }
+ ActiveRecord::Base.configurations = config
+ resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(ActiveRecord::Base.configurations)
+ spec = resolver.spec(:readonly)
+ @handler.establish_connection(spec.to_hash)
+
+ assert_not_nil @handler.retrieve_connection_pool("readonly")
+ ensure
+ ActiveRecord::Base.configurations = old_config
+ @handler.remove_connection("readonly")
end
- def test_retrieve_connection_pool_with_ar_base
- assert_nil @handler.retrieve_connection_pool(ActiveRecord::Base)
+ 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_retrieve_connection_pool
- assert_not_nil @handler.retrieve_connection_pool(@klass)
+ unless in_memory_db?
+ def test_establish_connection_using_3_level_config_defaults_to_default_env_primary_db
+ previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env"
+
+ config = {
+ "default_env" => {
+ "primary" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" },
+ "readonly" => { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" }
+ },
+ "another_env" => {
+ "primary" => { "adapter" => "sqlite3", "database" => "db/another-primary.sqlite3" },
+ "readonly" => { "adapter" => "sqlite3", "database" => "db/another-readonly.sqlite3" }
+ }
+ }
+ @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config
+
+ ActiveRecord::Base.establish_connection
+
+ assert_match "db/primary.sqlite3", ActiveRecord::Base.connection.pool.spec.config[:database]
+ ensure
+ ActiveRecord::Base.configurations = @prev_configs
+ ENV["RAILS_ENV"] = previous_env
+ ActiveRecord::Base.establish_connection(:arunit)
+ FileUtils.rm_rf "db"
+ end
+
+ def test_establish_connection_using_2_level_config_defaults_to_default_env_primary_db
+ previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env"
+
+ config = {
+ "default_env" => {
+ "adapter" => "sqlite3", "database" => "db/primary.sqlite3"
+ },
+ "another_env" => {
+ "adapter" => "sqlite3", "database" => "db/bad-primary.sqlite3"
+ }
+ }
+ @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config
+
+ ActiveRecord::Base.establish_connection
+
+ assert_match "db/primary.sqlite3", ActiveRecord::Base.connection.pool.spec.config[:database]
+ ensure
+ ActiveRecord::Base.configurations = @prev_configs
+ ENV["RAILS_ENV"] = previous_env
+ ActiveRecord::Base.establish_connection(:arunit)
+ FileUtils.rm_rf "db"
+ end
end
- def test_retrieve_connection_pool_uses_superclass_when_no_subclass_connection
- assert_not_nil @handler.retrieve_connection_pool(@subklass)
+ 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_symbolized_configurations_assignment
+ @prev_configs = ActiveRecord::Base.configurations
+ config = {
+ development: {
+ primary: {
+ adapter: "sqlite3",
+ database: "db/development.sqlite3",
+ },
+ },
+ test: {
+ primary: {
+ adapter: "sqlite3",
+ database: "db/test.sqlite3",
+ },
+ },
+ }
+ ActiveRecord::Base.configurations = config
+ ActiveRecord::Base.configurations.configs_for.each do |db_config|
+ assert_instance_of ActiveRecord::DatabaseConfigurations::HashConfig, db_config
+ assert_instance_of String, db_config.env_name
+ assert_instance_of String, db_config.spec_name
+ db_config.config.keys.each do |key|
+ assert_instance_of String, key
+ end
+ end
+ ensure
+ ActiveRecord::Base.configurations = @prev_configs
+ end
+
+ def test_retrieve_connection
+ assert @handler.retrieve_connection(@spec_name)
+ end
+
+ def test_active_connections?
+ assert_not_predicate @handler, :active_connections?
+ assert @handler.retrieve_connection(@spec_name)
+ assert_predicate @handler, :active_connections?
+ @handler.clear_active_connections!
+ assert_not_predicate @handler, :active_connections?
end
- def test_retrieve_connection_pool_uses_superclass_pool_after_subclass_establish_and_remove
- sub_pool = @handler.establish_connection(@subklass, Base.connection_pool.spec)
- assert_same sub_pool, @handler.retrieve_connection_pool(@subklass)
+ def test_retrieve_connection_pool
+ assert_not_nil @handler.retrieve_connection_pool(@spec_name)
+ end
- @handler.remove_connection @subklass
- assert_same @pool, @handler.retrieve_connection_pool(@subklass)
+ def test_retrieve_connection_pool_with_invalid_id
+ assert_nil @handler.retrieve_connection_pool("foo")
end
def test_connection_pools
@@ -69,9 +226,78 @@ module ActiveRecord
rd.close
end
+ def test_forked_child_doesnt_mangle_parent_connection
+ object_id = ActiveRecord::Base.connection.object_id
+ assert_predicate ActiveRecord::Base.connection, :active?
+
+ rd, wr = IO.pipe
+ rd.binmode
+ wr.binmode
+
+ pid = fork {
+ rd.close
+ if ActiveRecord::Base.connection.active?
+ wr.write Marshal.dump ActiveRecord::Base.connection.object_id
+ end
+ wr.close
+
+ exit # allow finalizers to run
+ }
+
+ wr.close
+
+ Process.waitpid pid
+ assert_not_equal object_id, Marshal.load(rd.read)
+ rd.close
+
+ assert_equal 3, ActiveRecord::Base.connection.select_value("SELECT COUNT(*) FROM people")
+ end
+
+ unless in_memory_db?
+ def test_forked_child_recovers_from_disconnected_parent
+ object_id = ActiveRecord::Base.connection.object_id
+ assert_predicate ActiveRecord::Base.connection, :active?
+
+ rd, wr = IO.pipe
+ rd.binmode
+ wr.binmode
+
+ outer_pid = fork {
+ ActiveRecord::Base.connection.disconnect!
+
+ pid = fork {
+ rd.close
+ if ActiveRecord::Base.connection.active?
+ pair = [ActiveRecord::Base.connection.object_id,
+ ActiveRecord::Base.connection.select_value("SELECT COUNT(*) FROM people")]
+ wr.write Marshal.dump pair
+ end
+ wr.close
+
+ exit # allow finalizers to run
+ }
+
+ Process.waitpid pid
+ }
+
+ wr.close
+
+ Process.waitpid outer_pid
+ child_id, child_count = Marshal.load(rd.read)
+
+ assert_not_equal object_id, child_id
+ rd.close
+
+ assert_equal 3, child_count
+
+ # Outer connection is unaffected
+ assert_equal 6, ActiveRecord::Base.connection.select_value("SELECT 2 * COUNT(*) FROM people")
+ end
+ end
+
def test_retrieve_connection_pool_copies_schema_cache_from_ancestor_pool
@pool.schema_cache = @pool.connection.schema_cache
- @pool.schema_cache.add('posts')
+ @pool.schema_cache.add("posts")
rd, wr = IO.pipe
rd.binmode
@@ -79,7 +305,7 @@ module ActiveRecord
pid = fork {
rd.close
- pool = @handler.retrieve_connection_pool(@klass)
+ pool = @handler.retrieve_connection_pool(@spec_name)
wr.write Marshal.dump pool.schema_cache.size
wr.close
exit!
@@ -91,6 +317,71 @@ module ActiveRecord
assert_equal @pool.schema_cache.size, Marshal.load(rd.read)
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 }
+
+ assert_same klass2.connection, ActiveRecord::Base.connection
+
+ pool = klass2.establish_connection(ActiveRecord::Base.connection_pool.spec.config)
+ assert_same klass2.connection, pool.connection
+ assert_not_same klass2.connection, ActiveRecord::Base.connection
+
+ klass2.remove_connection
+
+ assert_same klass2.connection, ActiveRecord::Base.connection
+ end
+
+ def test_connection_specification_name_should_fallback_to_parent
+ klassA = Class.new(Base)
+ klassB = Class.new(klassA)
+
+ assert_equal klassB.connection_specification_name, klassA.connection_specification_name
+ klassA.connection_specification_name = "readonly"
+ assert_equal "readonly", klassB.connection_specification_name
+ end
+
+ def test_remove_connection_should_not_remove_parent
+ klass2 = Class.new(Base) { def self.name; "klass2"; end }
+ klass2.remove_connection
+ assert_not_nil ActiveRecord::Base.connection
+ assert_same klass2.connection, ActiveRecord::Base.connection
+ end
end
end
end
diff --git a/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb b/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb
new file mode 100644
index 0000000000..a394430dfe
--- /dev/null
+++ b/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb
@@ -0,0 +1,285 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/person"
+
+module ActiveRecord
+ module ConnectionAdapters
+ class ConnectionHandlersMultiDbTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
+
+ fixtures :people
+
+ def setup
+ @handlers = { writing: ConnectionHandler.new, reading: ConnectionHandler.new }
+ @rw_handler = @handlers[:writing]
+ @ro_handler = @handlers[:reading]
+ @spec_name = "primary"
+ @rw_pool = @handlers[:writing].establish_connection(ActiveRecord::Base.configurations["arunit"])
+ @ro_pool = @handlers[:reading].establish_connection(ActiveRecord::Base.configurations["arunit"])
+ end
+
+ def teardown
+ ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler }
+ end
+
+ class MultiConnectionTestModel < ActiveRecord::Base
+ end
+
+ def test_multiple_connection_handlers_works_in_a_threaded_environment
+ tf_writing = Tempfile.open "test_writing"
+ tf_reading = Tempfile.open "test_reading"
+
+ MultiConnectionTestModel.connects_to database: { writing: { database: tf_writing.path, adapter: "sqlite3" }, reading: { database: tf_reading.path, adapter: "sqlite3" } }
+
+ MultiConnectionTestModel.connection.execute("CREATE TABLE `test_1` (connection_role VARCHAR (255))")
+ MultiConnectionTestModel.connection.execute("INSERT INTO test_1 VALUES ('writing')")
+
+ ActiveRecord::Base.connected_to(role: :reading) do
+ MultiConnectionTestModel.connection.execute("CREATE TABLE `test_1` (connection_role VARCHAR (255))")
+ MultiConnectionTestModel.connection.execute("INSERT INTO test_1 VALUES ('reading')")
+ end
+
+ read_latch = Concurrent::CountDownLatch.new
+ write_latch = Concurrent::CountDownLatch.new
+
+ MultiConnectionTestModel.connection
+
+ thread = Thread.new do
+ MultiConnectionTestModel.connection
+
+ write_latch.wait
+ assert_equal "writing", MultiConnectionTestModel.connection.select_value("SELECT connection_role from test_1")
+ read_latch.count_down
+ end
+
+ ActiveRecord::Base.connected_to(role: :reading) do
+ write_latch.count_down
+ assert_equal "reading", MultiConnectionTestModel.connection.select_value("SELECT connection_role from test_1")
+ read_latch.wait
+ end
+
+ thread.join
+ ensure
+ tf_reading.close
+ tf_reading.unlink
+ tf_writing.close
+ tf_writing.unlink
+ end
+
+ unless in_memory_db?
+ 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" }
+ }
+ }
+ @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config
+
+ ActiveRecord::Base.connects_to(database: { writing: :primary, reading: :readonly })
+
+ assert_not_nil pool = ActiveRecord::Base.connection_handlers[:writing].retrieve_connection_pool("primary")
+ assert_equal "db/primary.sqlite3", pool.spec.config[:database]
+
+ assert_not_nil pool = ActiveRecord::Base.connection_handlers[:reading].retrieve_connection_pool("primary")
+ assert_equal "db/readonly.sqlite3", pool.spec.config[:database]
+ ensure
+ ActiveRecord::Base.configurations = @prev_configs
+ ActiveRecord::Base.establish_connection(:arunit)
+ ENV["RAILS_ENV"] = previous_env
+ end
+
+ def test_switching_connections_via_handler
+ previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env"
+
+ config = {
+ "default_env" => {
+ "readonly" => { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" },
+ "primary" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" }
+ }
+ }
+ @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config
+
+ ActiveRecord::Base.connects_to(database: { writing: :primary, reading: :readonly })
+
+ ActiveRecord::Base.connected_to(role: :reading) do
+ @ro_handler = ActiveRecord::Base.connection_handler
+ assert_equal ActiveRecord::Base.connection_handler, ActiveRecord::Base.connection_handlers[:reading]
+ end
+
+ ActiveRecord::Base.connected_to(role: :writing) do
+ assert_equal ActiveRecord::Base.connection_handler, ActiveRecord::Base.connection_handlers[:writing]
+ assert_not_equal @ro_handler, ActiveRecord::Base.connection_handler
+ end
+ ensure
+ ActiveRecord::Base.configurations = @prev_configs
+ ActiveRecord::Base.establish_connection(:arunit)
+ ENV["RAILS_ENV"] = previous_env
+ end
+
+ def test_switching_connections_with_database_url
+ previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env"
+ previous_url, ENV["DATABASE_URL"] = ENV["DATABASE_URL"], "postgres://localhost/foo"
+
+ ActiveRecord::Base.connected_to(database: { writing: "postgres://localhost/bar" }) do
+ handler = ActiveRecord::Base.connection_handler
+ assert_equal handler, ActiveRecord::Base.connection_handlers[:writing]
+
+ assert_not_nil pool = handler.retrieve_connection_pool("primary")
+ assert_equal({ adapter: "postgresql", database: "bar", host: "localhost" }, pool.spec.config)
+ end
+ ensure
+ ActiveRecord::Base.establish_connection(:arunit)
+ ENV["RAILS_ENV"] = previous_env
+ ENV["DATABASE_URL"] = previous_url
+ end
+
+ def test_switching_connections_with_database_config_hash
+ previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env"
+ config = { adapter: "sqlite3", database: "db/readonly.sqlite3" }
+
+ ActiveRecord::Base.connected_to(database: { writing: config }) do
+ handler = ActiveRecord::Base.connection_handler
+ assert_equal handler, ActiveRecord::Base.connection_handlers[:writing]
+
+ assert_not_nil pool = handler.retrieve_connection_pool("primary")
+ assert_equal(config, pool.spec.config)
+ end
+ ensure
+ ActiveRecord::Base.establish_connection(:arunit)
+ ENV["RAILS_ENV"] = previous_env
+ end
+
+ def test_switching_connections_with_database_and_role_raises
+ error = assert_raises(ArgumentError) do
+ ActiveRecord::Base.connected_to(database: :readonly, role: :writing) { }
+ end
+ assert_equal "connected_to can only accept a `database` or a `role` argument, but not both arguments.", error.message
+ end
+
+ def test_switching_connections_without_database_and_role_raises
+ error = assert_raises(ArgumentError) do
+ ActiveRecord::Base.connected_to { }
+ end
+ assert_equal "must provide a `database` or a `role`.", error.message
+ end
+
+ def test_switching_connections_with_database_symbol
+ previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env"
+
+ config = {
+ "default_env" => {
+ "readonly" => { adapter: "sqlite3", database: "db/readonly.sqlite3" },
+ "primary" => { adapter: "sqlite3", database: "db/primary.sqlite3" }
+ }
+ }
+ @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config
+
+ ActiveRecord::Base.connected_to(database: :readonly) do
+ handler = ActiveRecord::Base.connection_handler
+ assert_equal handler, ActiveRecord::Base.connection_handlers[:readonly]
+
+ assert_not_nil pool = handler.retrieve_connection_pool("primary")
+ assert_equal(config["default_env"]["readonly"], pool.spec.config)
+ end
+ ensure
+ ActiveRecord::Base.configurations = @prev_configs
+ ActiveRecord::Base.establish_connection(:arunit)
+ ENV["RAILS_ENV"] = previous_env
+ end
+
+ def test_connects_to_with_single_configuration
+ config = {
+ "development" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" },
+ }
+ @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config
+
+ ActiveRecord::Base.connects_to database: { writing: :development }
+
+ assert_equal 1, ActiveRecord::Base.connection_handlers.size
+ assert_equal ActiveRecord::Base.connection_handler, ActiveRecord::Base.connection_handlers[:writing]
+ ensure
+ ActiveRecord::Base.configurations = @prev_configs
+ ActiveRecord::Base.establish_connection(:arunit)
+ end
+
+ def test_connects_to_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
+
+ ActiveRecord::Base.connects_to database: { writing: :development, reading: :development_readonly }
+
+ assert_not_nil pool = ActiveRecord::Base.connection_handlers[:reading].retrieve_connection_pool("primary")
+ assert_equal "db/readonly.sqlite3", pool.spec.config[:database]
+ ensure
+ ActiveRecord::Base.configurations = @prev_configs
+ ActiveRecord::Base.establish_connection(:arunit)
+ end
+
+ def test_connects_to_returns_array_of_established_connections
+ 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
+
+ result = ActiveRecord::Base.connects_to database: { writing: :development, reading: :development_readonly }
+
+ assert_equal(
+ [
+ ActiveRecord::Base.connection_handlers[:writing].retrieve_connection_pool("primary"),
+ ActiveRecord::Base.connection_handlers[:reading].retrieve_connection_pool("primary")
+ ],
+ result
+ )
+ ensure
+ ActiveRecord::Base.configurations = @prev_configs
+ ActiveRecord::Base.establish_connection(:arunit)
+ end
+ end
+
+ def test_connection_pools
+ assert_equal([@rw_pool], @handlers[:writing].connection_pools)
+ assert_equal([@ro_pool], @handlers[:reading].connection_pools)
+ end
+
+ def test_retrieve_connection
+ assert @rw_handler.retrieve_connection(@spec_name)
+ assert @ro_handler.retrieve_connection(@spec_name)
+ end
+
+ def test_active_connections?
+ assert_not_predicate @rw_handler, :active_connections?
+ assert_not_predicate @ro_handler, :active_connections?
+
+ assert @rw_handler.retrieve_connection(@spec_name)
+ assert @ro_handler.retrieve_connection(@spec_name)
+
+ assert_predicate @rw_handler, :active_connections?
+ assert_predicate @ro_handler, :active_connections?
+
+ @rw_handler.clear_active_connections!
+ assert_not_predicate @rw_handler, :active_connections?
+
+ @ro_handler.clear_active_connections!
+ assert_not_predicate @ro_handler, :active_connections?
+ end
+
+ def test_retrieve_connection_pool
+ assert_not_nil @rw_handler.retrieve_connection_pool(@spec_name)
+ assert_not_nil @ro_handler.retrieve_connection_pool(@spec_name)
+ end
+
+ def test_retrieve_connection_pool_with_invalid_id
+ assert_nil @rw_handler.retrieve_connection_pool("foo")
+ assert_nil @ro_handler.retrieve_connection_pool("foo")
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/connection_adapters/connection_specification_test.rb b/activerecord/test/cases/connection_adapters/connection_specification_test.rb
index ea2196cda2..f81b73c344 100644
--- a/activerecord/test/cases/connection_adapters/connection_specification_test.rb
+++ b/activerecord/test/cases/connection_adapters/connection_specification_test.rb
@@ -1,10 +1,12 @@
+# frozen_string_literal: true
+
require "cases/helper"
module ActiveRecord
module ConnectionAdapters
class ConnectionSpecificationTest < ActiveRecord::TestCase
def test_dup_deep_copy_config
- spec = ConnectionSpecification.new({ :a => :b }, "bar")
+ spec = ConnectionSpecification.new("primary", { a: :b }, "bar")
assert_not_equal(spec.config.object_id, spec.dup.config.object_id)
end
end
diff --git a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb
index 9ee92a3cd2..06c1c51724 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
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
module ActiveRecord
@@ -16,62 +18,65 @@ module ActiveRecord
end
def resolve_config(config)
- ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig.new(config).resolve
+ configs = ActiveRecord::DatabaseConfigurations.new(config)
+ configs.to_h
end
def resolve_spec(spec, config)
- ConnectionSpecification::Resolver.new(resolve_config(config)).resolve(spec)
+ configs = ActiveRecord::DatabaseConfigurations.new(config)
+ resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(configs)
+ resolver.resolve(spec, spec)
end
def test_resolver_with_database_uri_and_current_env_symbol_key
- ENV['DATABASE_URL'] = "postgres://localhost/foo"
+ ENV["DATABASE_URL"] = "postgres://localhost/foo"
config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } }
actual = resolve_spec(:default_env, config)
- expected = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" }
+ expected = { "adapter" => "postgresql", "database" => "foo", "host" => "localhost", "name" => "default_env" }
assert_equal expected, actual
end
def test_resolver_with_database_uri_and_current_env_symbol_key_and_rails_env
- ENV['DATABASE_URL'] = "postgres://localhost/foo"
- ENV['RAILS_ENV'] = "foo"
+ ENV["DATABASE_URL"] = "postgres://localhost/foo"
+ ENV["RAILS_ENV"] = "foo"
config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } }
actual = resolve_spec(:foo, config)
- expected = { "adapter" => "postgresql", "database" => "foo", "host" => "localhost" }
+ expected = { "adapter" => "postgresql", "database" => "foo", "host" => "localhost", "name" => "foo" }
assert_equal expected, actual
end
def test_resolver_with_database_uri_and_current_env_symbol_key_and_rack_env
- ENV['DATABASE_URL'] = "postgres://localhost/foo"
- ENV['RACK_ENV'] = "foo"
+ ENV["DATABASE_URL"] = "postgres://localhost/foo"
+ ENV["RACK_ENV"] = "foo"
config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } }
actual = resolve_spec(:foo, config)
- expected = { "adapter" => "postgresql", "database" => "foo", "host" => "localhost" }
+ expected = { "adapter" => "postgresql", "database" => "foo", "host" => "localhost", "name" => "foo" }
assert_equal expected, actual
end
def test_resolver_with_database_uri_and_known_key
- ENV['DATABASE_URL'] = "postgres://localhost/foo"
+ ENV["DATABASE_URL"] = "postgres://localhost/foo"
config = { "production" => { "adapter" => "not_postgres", "database" => "not_foo", "host" => "localhost" } }
actual = resolve_spec(:production, config)
- expected = { "adapter"=>"not_postgres", "database"=>"not_foo", "host"=>"localhost" }
+ expected = { "adapter" => "not_postgres", "database" => "not_foo", "host" => "localhost", "name" => "production" }
assert_equal expected, actual
end
def test_resolver_with_database_uri_and_unknown_symbol_key
- ENV['DATABASE_URL'] = "postgres://localhost/foo"
- config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } }
+ ENV["DATABASE_URL"] = "postgres://localhost/foo"
+ config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } }
assert_raises AdapterNotSpecified do
resolve_spec(:production, config)
end
end
def test_resolver_with_database_uri_and_supplied_url
- ENV['DATABASE_URL'] = "not-postgres://not-localhost/not_foo"
+ ENV["DATABASE_URL"] = "not-postgres://not-localhost/not_foo"
config = { "production" => { "adapter" => "also_not_postgres", "database" => "also_not_foo" } }
actual = resolve_spec("postgres://localhost/foo", config)
- expected = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" }
+ expected = { "adapter" => "postgresql", "database" => "foo", "host" => "localhost" }
assert_equal expected, actual
end
@@ -82,18 +87,18 @@ module ActiveRecord
end
def test_environment_does_not_exist_in_config_url_does_exist
- ENV['DATABASE_URL'] = "postgres://localhost/foo"
+ ENV["DATABASE_URL"] = "postgres://localhost/foo"
config = { "not_default_env" => { "adapter" => "not_postgres", "database" => "not_foo" } }
actual = resolve_config(config)
- expect_prod = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" }
+ expect_prod = { "adapter" => "postgresql", "database" => "foo", "host" => "localhost" }
assert_equal expect_prod, actual["default_env"]
end
def test_url_with_hyphenated_scheme
- ENV['DATABASE_URL'] = "ibm-db://localhost/foo"
+ ENV["DATABASE_URL"] = "ibm-db://localhost/foo"
config = { "default_env" => { "adapter" => "not_postgres", "database" => "not_foo", "host" => "localhost" } }
actual = resolve_spec(:default_env, config)
- expected = { "adapter"=>"ibm_db", "database"=>"foo", "host"=>"localhost" }
+ expected = { "adapter" => "ibm_db", "database" => "foo", "host" => "localhost", "name" => "default_env" }
assert_equal expected, actual
end
@@ -134,7 +139,7 @@ module ActiveRecord
end
def test_blank_with_database_url
- ENV['DATABASE_URL'] = "postgres://localhost/foo"
+ ENV["DATABASE_URL"] = "postgres://localhost/foo"
config = {}
actual = resolve_config(config)
@@ -142,18 +147,18 @@ 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
- ENV['RAILS_ENV'] = "not_production"
- ENV['DATABASE_URL'] = "postgres://localhost/foo"
+ ENV["RAILS_ENV"] = "not_production"
+ ENV["DATABASE_URL"] = "postgres://localhost/foo"
config = {}
actual = resolve_config(config)
@@ -162,20 +167,20 @@ 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
- ENV['RACK_ENV'] = "not_production"
- ENV['DATABASE_URL'] = "postgres://localhost/foo"
+ ENV["RACK_ENV"] = "not_production"
+ ENV["DATABASE_URL"] = "postgres://localhost/foo"
config = {}
actual = resolve_config(config)
@@ -184,19 +189,19 @@ 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
- ENV['DATABASE_URL'] = "postgres://[::1]:5454/foo"
+ ENV["DATABASE_URL"] = "postgres://[::1]:5454/foo"
config = {}
actual = resolve_config(config)
@@ -208,12 +213,12 @@ module ActiveRecord
end
def test_url_sub_key_with_database_url
- ENV['DATABASE_URL'] = "NOT-POSTGRES://localhost/NOT_FOO"
+ ENV["DATABASE_URL"] = "NOT-POSTGRES://localhost/NOT_FOO"
config = { "default_env" => { "url" => "postgres://localhost/foo" } }
actual = resolve_config(config)
expected = { "default_env" =>
- { "adapter" => "postgresql",
+ { "adapter" => "postgresql",
"database" => "foo",
"host" => "localhost"
}
@@ -222,9 +227,9 @@ module ActiveRecord
end
def test_merge_no_conflicts_with_database_url
- ENV['DATABASE_URL'] = "postgres://localhost/foo"
+ ENV["DATABASE_URL"] = "postgres://localhost/foo"
- config = {"default_env" => { "pool" => "5" } }
+ config = { "default_env" => { "pool" => "5" } }
actual = resolve_config(config)
expected = { "default_env" =>
{ "adapter" => "postgresql",
@@ -237,9 +242,9 @@ module ActiveRecord
end
def test_merge_conflicts_with_database_url
- ENV['DATABASE_URL'] = "postgres://localhost/foo"
+ ENV["DATABASE_URL"] = "postgres://localhost/foo"
- config = {"default_env" => { "adapter" => "NOT-POSTGRES", "database" => "NOT-FOO", "pool" => "5" } }
+ config = { "default_env" => { "adapter" => "NOT-POSTGRES", "database" => "NOT-FOO", "pool" => "5" } }
actual = resolve_config(config)
expected = { "default_env" =>
{ "adapter" => "postgresql",
diff --git a/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb
index 80244d1439..02e76ce146 100644
--- a/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb
+++ b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb
@@ -1,65 +1,78 @@
+# frozen_string_literal: true
+
require "cases/helper"
+require "support/connection_helper"
-if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
-module ActiveRecord
- module ConnectionAdapters
- class MysqlTypeLookupTest < ActiveRecord::TestCase
- setup do
- @connection = ActiveRecord::Base.connection
- end
+if current_adapter?(:Mysql2Adapter)
+ module ActiveRecord
+ module ConnectionAdapters
+ class MysqlTypeLookupTest < ActiveRecord::TestCase
+ include ConnectionHelper
- def test_boolean_types
- emulate_booleans(true) do
- assert_lookup_type :boolean, 'tinyint(1)'
- assert_lookup_type :boolean, 'TINYINT(1)'
+ setup do
+ @connection = ActiveRecord::Base.connection
end
- end
- def test_string_types
- assert_lookup_type :string, "enum('one', 'two', 'three')"
- assert_lookup_type :string, "ENUM('one', 'two', 'three')"
- assert_lookup_type :string, "set('one', 'two', 'three')"
- assert_lookup_type :string, "SET('one', 'two', 'three')"
- end
+ def teardown
+ reset_connection
+ end
- def test_enum_type_with_value_matching_other_type
- assert_lookup_type :string, "ENUM('unicode', '8bit', 'none')"
- end
+ def test_boolean_types
+ emulate_booleans(true) do
+ assert_lookup_type :boolean, "tinyint(1)"
+ assert_lookup_type :boolean, "TINYINT(1)"
+ end
+ end
- def test_binary_types
- assert_lookup_type :binary, 'bit'
- assert_lookup_type :binary, 'BIT'
- end
+ def test_string_types
+ assert_lookup_type :string, "enum('one', 'two', 'three')"
+ assert_lookup_type :string, "ENUM('one', 'two', 'three')"
+ assert_lookup_type :string, "set('one', 'two', 'three')"
+ assert_lookup_type :string, "SET('one', 'two', 'three')"
+ end
- def test_integer_types
- emulate_booleans(false) do
- assert_lookup_type :integer, 'tinyint(1)'
- assert_lookup_type :integer, 'TINYINT(1)'
- assert_lookup_type :integer, 'year'
- assert_lookup_type :integer, 'YEAR'
+ def test_set_type_with_value_matching_other_type
+ assert_lookup_type :string, "SET('unicode', '8bit', 'none', 'time')"
end
- end
- private
+ def test_enum_type_with_value_matching_other_type
+ assert_lookup_type :string, "ENUM('unicode', '8bit', 'none')"
+ end
- def assert_lookup_type(type, lookup)
- cast_type = @connection.type_map.lookup(lookup)
- assert_equal type, cast_type.type
- end
+ def test_binary_types
+ assert_lookup_type :binary, "bit"
+ assert_lookup_type :binary, "BIT"
+ end
- def emulate_booleans(value)
- old_emulate_booleans = @connection.emulate_booleans
- change_emulate_booleans(value)
- yield
- ensure
- change_emulate_booleans(old_emulate_booleans)
- end
+ def test_integer_types
+ emulate_booleans(false) do
+ assert_lookup_type :integer, "tinyint(1)"
+ assert_lookup_type :integer, "TINYINT(1)"
+ assert_lookup_type :integer, "year"
+ assert_lookup_type :integer, "YEAR"
+ end
+ end
- def change_emulate_booleans(value)
- @connection.emulate_booleans = value
- @connection.clear_cache!
+ private
+
+ def assert_lookup_type(type, lookup)
+ cast_type = @connection.send(:type_map).lookup(lookup)
+ assert_equal type, cast_type.type
+ end
+
+ def emulate_booleans(value)
+ old_emulate_booleans = @connection.emulate_booleans
+ change_emulate_booleans(value)
+ yield
+ ensure
+ change_emulate_booleans(old_emulate_booleans)
+ end
+
+ def change_emulate_booleans(value)
+ @connection.emulate_booleans = value
+ @connection.clear_cache!
+ end
end
end
end
end
-end
diff --git a/activerecord/test/cases/connection_adapters/quoting_test.rb b/activerecord/test/cases/connection_adapters/quoting_test.rb
deleted file mode 100644
index 59dcb96ebc..0000000000
--- a/activerecord/test/cases/connection_adapters/quoting_test.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-require "cases/helper"
-
-module ActiveRecord
- module ConnectionAdapters
- module Quoting
- class QuotingTest < ActiveRecord::TestCase
- def test_quoting_classes
- assert_equal "'Object'", AbstractAdapter.new(nil).quote(Object)
- end
- end
- end
- end
-end
diff --git a/activerecord/test/cases/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb
index c7531f5418..67496381d1 100644
--- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb
+++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
module ActiveRecord
@@ -9,28 +11,55 @@ module ActiveRecord
end
def test_primary_key
- assert_equal 'id', @cache.primary_keys('posts')
+ 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 12, new_cache.columns("posts").size
+ assert_equal 12, new_cache.columns_hash("posts").size
+ assert new_cache.data_sources("posts")
+ assert_equal "id", new_cache.primary_keys("posts")
+ end
+ 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')
+ assert_nil @cache.primary_keys("omgponies")
end
def test_caches_columns
- columns = @cache.columns('posts')
- assert_equal columns, @cache.columns('posts')
+ columns = @cache.columns("posts")
+ assert_equal columns, @cache.columns("posts")
end
def test_caches_columns_hash
- columns_hash = @cache.columns_hash('posts')
- assert_equal columns_hash, @cache.columns_hash('posts')
+ columns_hash = @cache.columns_hash("posts")
+ assert_equal columns_hash, @cache.columns_hash("posts")
end
def test_clearing
- @cache.columns('posts')
- @cache.columns_hash('posts')
- @cache.tables('posts')
- @cache.primary_keys('posts')
+ @cache.columns("posts")
+ @cache.columns_hash("posts")
+ @cache.data_sources("posts")
+ @cache.primary_keys("posts")
@cache.clear!
@@ -38,19 +67,35 @@ module ActiveRecord
end
def test_dump_and_load
- @cache.columns('posts')
- @cache.columns_hash('posts')
- @cache.tables('posts')
- @cache.primary_keys('posts')
+ @cache.columns("posts")
+ @cache.columns_hash("posts")
+ @cache.data_sources("posts")
+ @cache.primary_keys("posts")
@cache = Marshal.load(Marshal.dump(@cache))
- assert_equal 11, @cache.columns('posts').size
- assert_equal 11, @cache.columns_hash('posts').size
- assert @cache.tables('posts')
- assert_equal 'id', @cache.primary_keys('posts')
+ assert_no_queries do
+ assert_equal 12, @cache.columns("posts").size
+ assert_equal 12, @cache.columns_hash("posts").size
+ assert @cache.data_sources("posts")
+ assert_equal "id", @cache.primary_keys("posts")
+ end
+ end
+
+ 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 7566863653..1c79d776f0 100644
--- a/activerecord/test/cases/connection_adapters/type_lookup_test.rb
+++ b/activerecord/test/cases/connection_adapters/type_lookup_test.rb
@@ -1,110 +1,120 @@
-require "cases/helper"
+# frozen_string_literal: true
-unless current_adapter?(:PostgreSQLAdapter) # PostgreSQL does not use type strigns for lookup
-module ActiveRecord
- module ConnectionAdapters
- class TypeLookupTest < ActiveRecord::TestCase
- setup do
- @connection = ActiveRecord::Base.connection
- end
+require "cases/helper"
- def test_boolean_types
- assert_lookup_type :boolean, 'boolean'
- assert_lookup_type :boolean, 'BOOLEAN'
- end
+unless current_adapter?(:PostgreSQLAdapter) # PostgreSQL does not use type strings for lookup
+ module ActiveRecord
+ module ConnectionAdapters
+ class TypeLookupTest < ActiveRecord::TestCase
+ setup do
+ @connection = ActiveRecord::Base.connection
+ end
- def test_string_types
- assert_lookup_type :string, 'char'
- assert_lookup_type :string, 'varchar'
- assert_lookup_type :string, 'VARCHAR'
- assert_lookup_type :string, 'varchar(255)'
- assert_lookup_type :string, 'character varying'
- end
+ def test_boolean_types
+ assert_lookup_type :boolean, "boolean"
+ assert_lookup_type :boolean, "BOOLEAN"
+ end
- def test_binary_types
- assert_lookup_type :binary, 'binary'
- assert_lookup_type :binary, 'BINARY'
- assert_lookup_type :binary, 'blob'
- assert_lookup_type :binary, 'BLOB'
- end
+ def test_string_types
+ assert_lookup_type :string, "char"
+ assert_lookup_type :string, "varchar"
+ assert_lookup_type :string, "VARCHAR"
+ assert_lookup_type :string, "varchar(255)"
+ assert_lookup_type :string, "character varying"
+ end
- def test_text_types
- assert_lookup_type :text, 'text'
- assert_lookup_type :text, 'TEXT'
- assert_lookup_type :text, 'clob'
- assert_lookup_type :text, 'CLOB'
- end
+ def test_binary_types
+ assert_lookup_type :binary, "binary"
+ assert_lookup_type :binary, "BINARY"
+ assert_lookup_type :binary, "blob"
+ assert_lookup_type :binary, "BLOB"
+ end
- def test_date_types
- assert_lookup_type :date, 'date'
- assert_lookup_type :date, 'DATE'
- end
+ def test_text_types
+ assert_lookup_type :text, "text"
+ assert_lookup_type :text, "TEXT"
+ assert_lookup_type :text, "clob"
+ assert_lookup_type :text, "CLOB"
+ end
- def test_time_types
- assert_lookup_type :time, 'time'
- assert_lookup_type :time, 'TIME'
- end
+ def test_date_types
+ assert_lookup_type :date, "date"
+ assert_lookup_type :date, "DATE"
+ end
- def test_datetime_types
- assert_lookup_type :datetime, 'datetime'
- assert_lookup_type :datetime, 'DATETIME'
- assert_lookup_type :datetime, 'timestamp'
- assert_lookup_type :datetime, 'TIMESTAMP'
- end
+ def test_time_types
+ assert_lookup_type :time, "time"
+ assert_lookup_type :time, "TIME"
+ end
- def test_decimal_types
- assert_lookup_type :decimal, 'decimal'
- assert_lookup_type :decimal, 'decimal(2,8)'
- assert_lookup_type :decimal, 'DECIMAL'
- assert_lookup_type :decimal, 'numeric'
- assert_lookup_type :decimal, 'numeric(2,8)'
- assert_lookup_type :decimal, 'NUMERIC'
- assert_lookup_type :decimal, 'number'
- assert_lookup_type :decimal, 'number(2,8)'
- assert_lookup_type :decimal, 'NUMBER'
- end
+ def test_datetime_types
+ assert_lookup_type :datetime, "datetime"
+ assert_lookup_type :datetime, "DATETIME"
+ assert_lookup_type :datetime, "timestamp"
+ assert_lookup_type :datetime, "TIMESTAMP"
+ end
- def test_float_types
- assert_lookup_type :float, 'float'
- assert_lookup_type :float, 'FLOAT'
- assert_lookup_type :float, 'double'
- assert_lookup_type :float, 'DOUBLE'
- end
+ def test_decimal_types
+ assert_lookup_type :decimal, "decimal"
+ assert_lookup_type :decimal, "decimal(2,8)"
+ assert_lookup_type :decimal, "DECIMAL"
+ assert_lookup_type :decimal, "numeric"
+ assert_lookup_type :decimal, "numeric(2,8)"
+ assert_lookup_type :decimal, "NUMERIC"
+ assert_lookup_type :decimal, "number"
+ assert_lookup_type :decimal, "number(2,8)"
+ assert_lookup_type :decimal, "NUMBER"
+ end
- def test_integer_types
- assert_lookup_type :integer, 'integer'
- assert_lookup_type :integer, 'INTEGER'
- assert_lookup_type :integer, 'tinyint'
- assert_lookup_type :integer, 'smallint'
- assert_lookup_type :integer, 'bigint'
- end
+ def test_float_types
+ assert_lookup_type :float, "float"
+ assert_lookup_type :float, "FLOAT"
+ assert_lookup_type :float, "double"
+ assert_lookup_type :float, "DOUBLE"
+ end
- def test_bigint_limit
- cast_type = @connection.type_map.lookup("bigint")
- if current_adapter?(:OracleAdapter)
- assert_equal 19, cast_type.limit
- else
- assert_equal 8, cast_type.limit
+ def test_integer_types
+ assert_lookup_type :integer, "integer"
+ assert_lookup_type :integer, "INTEGER"
+ assert_lookup_type :integer, "tinyint"
+ assert_lookup_type :integer, "smallint"
+ assert_lookup_type :integer, "bigint"
end
- 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)
+ def test_bigint_limit
+ limit = @connection.send(:type_map).lookup("bigint").send(:_limit)
+ if current_adapter?(:OracleAdapter)
+ assert_equal 19, limit
+ else
+ assert_equal 8, limit
+ end
+ end
- assert_equal :decimal, cast_type.type
- assert_equal 2, cast_type.cast(2.1)
+ def test_decimal_without_scale
+ 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.send(:type_map).lookup(type)
+
+ assert_equal expected_type, cast_type.type
+ assert_equal 2, cast_type.cast(2.1)
+ end
+ end
end
- end
- private
+ private
- def assert_lookup_type(type, lookup)
- cast_type = @connection.type_map.lookup(lookup)
- assert_equal type, cast_type.type
+ def assert_lookup_type(type, lookup)
+ cast_type = @connection.send(:type_map).lookup(lookup)
+ assert_equal type, cast_type.type
+ end
end
end
end
end
-end
diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb
index dff6ea0fb0..b9b5cc0e28 100644
--- a/activerecord/test/cases/connection_management_test.rb
+++ b/activerecord/test/cases/connection_management_test.rb
@@ -1,9 +1,13 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "rack"
module ActiveRecord
module ConnectionAdapters
class ConnectionManagementTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
+
class App
attr_reader :calls
def initialize
@@ -12,88 +16,99 @@ module ActiveRecord
def call(env)
@calls << env
- [200, {}, ['hi mom']]
+ [200, {}, ["hi mom"]]
end
end
def setup
@env = {}
@app = App.new
- @management = ConnectionManagement.new(@app)
+ @management = middleware(@app)
# make sure we have an active connection
assert ActiveRecord::Base.connection
- assert ActiveRecord::Base.connection_handler.active_connections?
+ assert_predicate ActiveRecord::Base.connection_handler, :active_connections?
end
def test_app_delegation
- manager = ConnectionManagement.new(@app)
+ manager = middleware(@app)
manager.call @env
assert_equal [@env], @app.calls
end
- def test_connections_are_active_after_call
- @management.call(@env)
- assert ActiveRecord::Base.connection_handler.active_connections?
- end
-
def test_body_responds_to_each
_, _, body = @management.call(@env)
bits = []
body.each { |bit| bits << bit }
- assert_equal ['hi mom'], bits
+ assert_equal ["hi mom"], bits
end
def test_connections_are_cleared_after_body_close
_, _, body = @management.call(@env)
body.close
- assert !ActiveRecord::Base.connection_handler.active_connections?
+ assert_not_predicate ActiveRecord::Base.connection_handler, :active_connections?
end
- def test_active_connections_are_not_cleared_on_body_close_during_test
- @env['rack.test'] = true
- _, _, body = @management.call(@env)
- body.close
- assert ActiveRecord::Base.connection_handler.active_connections?
+ def test_active_connections_are_not_cleared_on_body_close_during_transaction
+ ActiveRecord::Base.transaction do
+ _, _, body = @management.call(@env)
+ body.close
+ assert_predicate ActiveRecord::Base.connection_handler, :active_connections?
+ end
end
def test_connections_closed_if_exception
app = Class.new(App) { def call(env); raise NotImplementedError; end }.new
- explosive = ConnectionManagement.new(app)
+ explosive = middleware(app)
assert_raises(NotImplementedError) { explosive.call(@env) }
- assert !ActiveRecord::Base.connection_handler.active_connections?
+ assert_not_predicate ActiveRecord::Base.connection_handler, :active_connections?
end
- def test_connections_not_closed_if_exception_and_test
- @env['rack.test'] = true
- app = Class.new(App) { def call(env); raise; end }.new
- explosive = ConnectionManagement.new(app)
- assert_raises(RuntimeError) { explosive.call(@env) }
- assert ActiveRecord::Base.connection_handler.active_connections?
- end
-
- def test_connections_closed_if_exception_and_explicitly_not_test
- @env['rack.test'] = false
- app = Class.new(App) { def call(env); raise NotImplementedError; end }.new
- explosive = ConnectionManagement.new(app)
- assert_raises(NotImplementedError) { explosive.call(@env) }
- assert !ActiveRecord::Base.connection_handler.active_connections?
+ def test_connections_not_closed_if_exception_inside_transaction
+ ActiveRecord::Base.transaction do
+ app = Class.new(App) { def call(env); raise RuntimeError; end }.new
+ explosive = middleware(app)
+ assert_raises(RuntimeError) { explosive.call(@env) }
+ assert_predicate ActiveRecord::Base.connection_handler, :active_connections?
+ end
end
test "doesn't clear active connections when running in a test case" do
- @env['rack.test'] = true
- @management.call(@env)
- assert ActiveRecord::Base.connection_handler.active_connections?
+ executor.wrap do
+ @management.call(@env)
+ assert_predicate ActiveRecord::Base.connection_handler, :active_connections?
+ end
end
test "proxy is polite to its body and responds to it" do
body = Class.new(String) { def to_path; "/path"; end }.new
app = lambda { |_| [200, {}, body] }
- response_body = ConnectionManagement.new(app).call(@env)[2]
- assert response_body.respond_to?(:to_path)
- assert_equal response_body.to_path, "/path"
+ response_body = middleware(app).call(@env)[2]
+ assert_respond_to response_body, :to_path
+ assert_equal "/path", response_body.to_path
+ end
+
+ test "doesn't mutate the original response" do
+ original_response = [200, {}, "hi"]
+ app = lambda { |_| original_response }
+ middleware(app).call(@env)[2]
+ assert_equal "hi", original_response.last
end
+
+ private
+ def executor
+ @executor ||= Class.new(ActiveSupport::Executor).tap do |exe|
+ ActiveRecord::QueryCache.install_executor_hooks(exe)
+ end
+ end
+
+ def middleware(app)
+ lambda do |env|
+ a, b, c = executor.wrap { app.call(env) }
+ [a, b, Rack::BodyProxy.new(c) { }]
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb
index c905772193..633d56e479 100644
--- a/activerecord/test/cases/connection_pool_test.rb
+++ b/activerecord/test/cases/connection_pool_test.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'active_support/concurrency/latch'
+require "concurrent/atomic/count_down_latch"
module ActiveRecord
module ConnectionAdapters
@@ -33,12 +35,12 @@ module ActiveRecord
def test_checkout_after_close
connection = pool.connection
- assert connection.in_use?
+ assert_predicate connection, :in_use?
connection.close
- assert !connection.in_use?
+ assert_not_predicate connection, :in_use?
- assert pool.connection.in_use?
+ assert_predicate pool.connection, :in_use?
end
def test_released_connection_moves_between_threads
@@ -78,18 +80,20 @@ module ActiveRecord
end
def test_active_connection_in_use
- assert !pool.active_connection?
+ assert_not_predicate pool, :active_connection?
main_thread = pool.connection
- assert pool.active_connection?
+ assert_predicate pool, :active_connection?
main_thread.close
- assert !pool.active_connection?
+ assert_not_predicate pool, :active_connection?
end
def test_full_pool_exception
- @pool.size.times { @pool.checkout }
+ @pool.checkout_timeout = 0.001 # no need to delay test suite by waiting the whole full default timeout
+ @pool.size.times { assert @pool.checkout }
+
assert_raises(ConnectionTimeoutError) do
@pool.checkout
end
@@ -107,6 +111,44 @@ module ActiveRecord
assert_equal connection, t.join.value
end
+ def test_full_pool_blocking_shares_load_interlock
+ @pool.instance_variable_set(:@size, 1)
+
+ load_interlock_latch = Concurrent::CountDownLatch.new
+ connection_latch = Concurrent::CountDownLatch.new
+
+ able_to_get_connection = false
+ able_to_load = false
+
+ thread_with_load_interlock = Thread.new do
+ ActiveSupport::Dependencies.interlock.running do
+ load_interlock_latch.count_down
+ connection_latch.wait
+
+ @pool.with_connection do
+ able_to_get_connection = true
+ end
+ end
+ end
+
+ thread_with_last_connection = Thread.new do
+ @pool.with_connection do
+ connection_latch.count_down
+ load_interlock_latch.wait
+
+ ActiveSupport::Dependencies.interlock.loading do
+ able_to_load = true
+ end
+ end
+ end
+
+ thread_with_load_interlock.join
+ thread_with_last_connection.join
+
+ assert able_to_get_connection
+ assert able_to_load
+ end
+
def test_removing_releases_latch
cs = @pool.size.times.map { @pool.checkout }
t = Thread.new { @pool.checkout }
@@ -133,15 +175,15 @@ module ActiveRecord
end
def test_reap_inactive
- ready = ActiveSupport::Concurrency::Latch.new
+ ready = Concurrent::CountDownLatch.new
@pool.checkout
child = Thread.new do
@pool.checkout
@pool.checkout
- ready.release
+ ready.count_down
Thread.stop
end
- ready.await
+ ready.wait
assert_equal 3, active_connections(@pool).size
@@ -151,16 +193,106 @@ module ActiveRecord
assert_equal 1, active_connections(@pool).size
ensure
- @pool.connections.each(&:close)
+ @pool.connections.each { |conn| conn.close if conn.in_use? }
+ end
+
+ def test_idle_timeout_configuration
+ @pool.disconnect!
+ spec = ActiveRecord::Base.connection_pool.spec
+ spec.config.merge!(idle_timeout: "0.02")
+ @pool = ConnectionPool.new(spec)
+ idle_conn = @pool.checkout
+ @pool.checkin(idle_conn)
+
+ idle_conn.instance_variable_set(
+ :@idle_since,
+ Concurrent.monotonic_time - 0.01
+ )
+
+ @pool.flush
+ assert_equal 1, @pool.connections.length
+
+ idle_conn.instance_variable_set(
+ :@idle_since,
+ Concurrent.monotonic_time - 0.02
+ )
+
+ @pool.flush
+ assert_equal 0, @pool.connections.length
+ end
+
+ def test_disable_flush
+ @pool.disconnect!
+ spec = ActiveRecord::Base.connection_pool.spec
+ spec.config.merge!(idle_timeout: -5)
+ @pool = ConnectionPool.new(spec)
+ idle_conn = @pool.checkout
+ @pool.checkin(idle_conn)
+
+ idle_conn.instance_variable_set(
+ :@idle_since,
+ Concurrent.monotonic_time - 1
+ )
+
+ @pool.flush
+ assert_equal 1, @pool.connections.length
+ end
+
+ def test_flush
+ idle_conn = @pool.checkout
+ recent_conn = @pool.checkout
+ active_conn = @pool.checkout
+
+ @pool.checkin idle_conn
+ @pool.checkin recent_conn
+
+ assert_equal 3, @pool.connections.length
+
+ idle_conn.instance_variable_set(
+ :@idle_since,
+ Concurrent.monotonic_time - 1000
+ )
+
+ @pool.flush(30)
+
+ assert_equal 2, @pool.connections.length
+
+ assert_equal [recent_conn, active_conn].sort_by(&:__id__), @pool.connections.sort_by(&:__id__)
+ ensure
+ @pool.checkin active_conn
+ end
+
+ def test_flush_bang
+ idle_conn = @pool.checkout
+ recent_conn = @pool.checkout
+ active_conn = @pool.checkout
+ _dead_conn = Thread.new { @pool.checkout }.join
+
+ @pool.checkin idle_conn
+ @pool.checkin recent_conn
+
+ assert_equal 4, @pool.connections.length
+
+ def idle_conn.seconds_idle
+ 1000
+ end
+
+ @pool.flush!
+
+ assert_equal 1, @pool.connections.length
+
+ assert_equal [active_conn].sort_by(&:__id__), @pool.connections.sort_by(&:__id__)
+ ensure
+ @pool.checkin active_conn
end
def test_remove_connection
conn = @pool.checkout
- assert conn.in_use?
+ assert_predicate conn, :in_use?
length = @pool.connections.length
@pool.remove conn
- assert conn.in_use?
+ assert_predicate conn, :in_use?
assert_equal(length - 1, @pool.connections.length)
ensure
conn.close
@@ -175,23 +307,23 @@ module ActiveRecord
end
def test_active_connection?
- assert !@pool.active_connection?
+ assert_not_predicate @pool, :active_connection?
assert @pool.connection
- assert @pool.active_connection?
+ assert_predicate @pool, :active_connection?
@pool.release_connection
- assert !@pool.active_connection?
+ assert_not_predicate @pool, :active_connection?
end
def test_checkout_behaviour
pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec
- connection = pool.connection
- assert_not_nil connection
+ main_connection = pool.connection
+ assert_not_nil main_connection
threads = []
4.times do |i|
threads << Thread.new(i) do
- connection = pool.connection
- assert_not_nil connection
- connection.close
+ thread_connection = pool.connection
+ assert_not_nil thread_connection
+ thread_connection.close
end
end
@@ -203,6 +335,14 @@ module ActiveRecord
end.join
end
+ def test_checkout_order_is_lifo
+ conn1 = @pool.checkout
+ conn2 = @pool.checkout
+ @pool.checkin conn1
+ @pool.checkin conn2
+ assert_equal [conn2, conn1], 2.times.map { @pool.checkout }
+ end
+
# The connection pool is "fair" if threads waiting for
# connections receive them in the order in which they began
# waiting. This ensures that we don't timeout one HTTP request
@@ -307,14 +447,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
@@ -335,11 +478,26 @@ module ActiveRecord
# is called with an anonymous class
def test_anonymous_class_exception
anonymous = Class.new(ActiveRecord::Base)
- handler = ActiveRecord::Base.connection_handler
- assert_raises(RuntimeError) {
- handler.establish_connection anonymous, nil
- }
+ assert_raises(RuntimeError) do
+ anonymous.establish_connection
+ 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
+ ConnectionTestModel.establish_connection :arunit
+
+ assert_equal [:config, :connection_id, :spec_name], payloads[0].keys.sort
+ assert_equal "ActiveRecord::ConnectionAdapters::ConnectionPoolTest::ConnectionTestModel", payloads[0][:spec_name]
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscription) if subscription
end
def test_pool_sets_connection_schema_cache
@@ -360,13 +518,13 @@ module ActiveRecord
def test_concurrent_connection_establishment
assert_operator @pool.connections.size, :<=, 1
- all_threads_in_new_connection = ActiveSupport::Concurrency::Latch.new(@pool.size - @pool.connections.size)
- all_go = ActiveSupport::Concurrency::Latch.new
+ all_threads_in_new_connection = Concurrent::CountDownLatch.new(@pool.size - @pool.connections.size)
+ all_go = Concurrent::CountDownLatch.new
@pool.singleton_class.class_eval do
define_method(:new_connection) do
- all_threads_in_new_connection.release
- all_go.await
+ all_threads_in_new_connection.count_down
+ all_go.wait
super()
end
end
@@ -381,19 +539,20 @@ module ActiveRecord
# the kernel of the whole test is here, everything else is just scaffolding,
# this latch will not be released unless conn. pool allows for concurrent
# connection creation
- all_threads_in_new_connection.await
+ all_threads_in_new_connection.wait
end
rescue Timeout::Error
- flunk 'pool unable to establish connections concurrently or implementation has ' <<
- 'changed, this test then needs to patch a different :new_connection method'
+ 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
- all_go.release
+ all_go.count_down
connecting_threads.map(&:join)
end
end
def test_non_bang_disconnect_and_clear_reloadable_connections_throw_exception_if_threads_dont_return_their_conns
+ Thread.report_on_exception, original_report_on_exception = false, Thread.report_on_exception
@pool.checkout_timeout = 0.001 # no need to delay test suite by waiting the whole full default timeout
[:disconnect, :clear_reloadable_connections].each do |group_action_method|
@pool.with_connection do |connection|
@@ -402,6 +561,8 @@ module ActiveRecord
end
end
end
+ ensure
+ Thread.report_on_exception = original_report_on_exception
end
def test_disconnect_and_clear_reloadable_connections_attempt_to_wait_for_threads_to_return_their_conns
@@ -418,7 +579,7 @@ module ActiveRecord
assert_nil timed_join_result
# assert that since this is within default timeout our connection hasn't been forcefully taken away from us
- assert @pool.active_connection?
+ assert_predicate @pool, :active_connection?
end
ensure
thread.join if thread && !timed_join_result # clean up the other thread
@@ -426,13 +587,16 @@ 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|
Thread.new { @pool.send(group_action_method) }.join
# assert connection has been forcefully taken away from us
- assert_not @pool.active_connection?
+ assert_not_predicate @pool, :active_connection?
+
+ # make a new connection for with_connection to clean up
+ @pool.connection
end
end
end
@@ -441,49 +605,50 @@ 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 = ActiveSupport::Concurrency::Latch.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.await }
- end
+ second_thread_done = Concurrent::Event.new
- # 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.release
- 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.release 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
+
+ first_thread.join(2)
+ second_thread.join(2)
+ 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
@@ -496,7 +661,7 @@ module ActiveRecord
end
stuck_thread = Thread.new do
- pool.with_connection {}
+ pool.with_connection { }
end
# wait for stuck_thread to get in queue
@@ -505,21 +670,41 @@ module ActiveRecord
pool.clear_reloadable_connections
unless stuck_thread.join(2)
- flunk 'clear_reloadable_connections must not let other connection waiting threads get stuck in queue'
+ flunk "clear_reloadable_connections must not let other connection waiting threads get stuck in queue"
end
assert_equal 0, pool.num_waiting_in_queue
end
end
- private
- def with_single_connection_pool
- one_conn_spec = ActiveRecord::Base.connection_pool.spec.dup
- one_conn_spec.config[:pool] = 1 # this is safe to do, because .dupped ConnectionSpecification also auto-dups its config
- yield(pool = ConnectionPool.new(one_conn_spec))
- ensure
- pool.disconnect! if pool
+ def test_connection_pool_stat
+ with_single_connection_pool do |pool|
+ pool.with_connection do |connection|
+ stats = pool.stat
+ assert_equal({ size: 1, connections: 1, busy: 1, dead: 0, idle: 0, waiting: 0, checkout_timeout: 5 }, stats)
+ end
+
+ stats = pool.stat
+ assert_equal({ size: 1, connections: 1, busy: 0, dead: 0, idle: 1, waiting: 0, checkout_timeout: 5 }, stats)
+
+ Thread.new do
+ pool.checkout
+ Thread.current.kill
+ end.join
+
+ stats = pool.stat
+ assert_equal({ size: 1, connections: 1, busy: 0, dead: 1, idle: 0, waiting: 0, checkout_timeout: 5 }, stats)
+ end
end
+
+ private
+ def with_single_connection_pool
+ one_conn_spec = ActiveRecord::Base.connection_pool.spec.dup
+ one_conn_spec.config[:pool] = 1 # this is safe to do, because .dupped ConnectionSpecification also auto-dups its config
+ yield(pool = ConnectionPool.new(one_conn_spec))
+ ensure
+ pool.disconnect! if pool
+ end
end
end
end
diff --git a/activerecord/test/cases/connection_specification/resolver_test.rb b/activerecord/test/cases/connection_specification/resolver_test.rb
index 3c2f5d4219..72be14f507 100644
--- a/activerecord/test/cases/connection_specification/resolver_test.rb
+++ b/activerecord/test/cases/connection_specification/resolver_test.rb
@@ -1,64 +1,79 @@
+# frozen_string_literal: true
+
require "cases/helper"
module ActiveRecord
module ConnectionAdapters
class ConnectionSpecification
class ResolverTest < ActiveRecord::TestCase
- def resolve(spec, config={})
- Resolver.new(config).resolve(spec)
+ def resolve(spec, config = {})
+ configs = ActiveRecord::DatabaseConfigurations.new(config)
+ resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(configs)
+ resolver.resolve(spec, spec)
end
- def spec(spec, config={})
- Resolver.new(config).spec(spec)
+ def spec(spec, config = {})
+ configs = ActiveRecord::DatabaseConfigurations.new(config)
+ resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(configs)
+ resolver.spec(spec)
end
def test_url_invalid_adapter
error = assert_raises(LoadError) do
- spec 'ridiculous://foo?encoding=utf8'
+ spec "ridiculous://foo?encoding=utf8"
end
- assert_match "Could not load 'active_record/connection_adapters/ridiculous_adapter'", error.message
+ assert_match "Could not load the 'ridiculous' Active Record adapter. Ensure that the adapter is spelled correctly in config/database.yml and that you've added the necessary adapter gem to your Gemfile.", error.message
end
# The abstract adapter is used simply to bypass the bit of code that
# checks that the adapter file can be required in.
def test_url_from_environment
- spec = resolve :production, 'production' => 'abstract://foo?encoding=utf8'
+ spec = resolve :production, "production" => "abstract://foo?encoding=utf8"
assert_equal({
"adapter" => "abstract",
"host" => "foo",
- "encoding" => "utf8" }, spec)
+ "encoding" => "utf8",
+ "name" => "production" }, spec)
end
def test_url_sub_key
- spec = resolve :production, 'production' => {"url" => 'abstract://foo?encoding=utf8'}
+ spec = resolve :production, "production" => { "url" => "abstract://foo?encoding=utf8" }
assert_equal({
"adapter" => "abstract",
"host" => "foo",
- "encoding" => "utf8" }, spec)
+ "encoding" => "utf8",
+ "name" => "production" }, spec)
end
def test_url_sub_key_merges_correctly
- hash = {"url" => 'abstract://foo?encoding=utf8&', "adapter" => "sqlite3", "host" => "bar", "pool" => "3"}
- spec = resolve :production, 'production' => hash
+ hash = { "url" => "abstract://foo?encoding=utf8&", "adapter" => "sqlite3", "host" => "bar", "pool" => "3" }
+ spec = resolve :production, "production" => hash
assert_equal({
"adapter" => "abstract",
"host" => "foo",
"encoding" => "utf8",
- "pool" => "3" }, spec)
+ "pool" => "3",
+ "name" => "production" }, spec)
end
def test_url_host_no_db
- spec = resolve 'abstract://foo?encoding=utf8'
+ spec = resolve "abstract://foo?encoding=utf8"
assert_equal({
"adapter" => "abstract",
"host" => "foo",
"encoding" => "utf8" }, spec)
end
+ def test_url_missing_scheme
+ spec = resolve "foo"
+ assert_equal({
+ "database" => "foo" }, spec)
+ end
+
def test_url_host_db
- spec = resolve 'abstract://foo/bar?encoding=utf8'
+ spec = resolve "abstract://foo/bar?encoding=utf8"
assert_equal({
"adapter" => "abstract",
"database" => "bar",
@@ -67,7 +82,7 @@ module ActiveRecord
end
def test_url_port
- spec = resolve 'abstract://foo:123?encoding=utf8'
+ spec = resolve "abstract://foo:123?encoding=utf8"
assert_equal({
"adapter" => "abstract",
"port" => 123,
@@ -76,40 +91,50 @@ module ActiveRecord
end
def test_encoded_password
- password = 'am@z1ng_p@ssw0rd#!'
+ password = "am@z1ng_p@ssw0rd#!"
encoded_password = URI.encode_www_form_component(password)
spec = resolve "abstract://foo:#{encoded_password}@localhost/bar"
assert_equal password, spec["password"]
end
def test_url_with_authority_for_sqlite3
- spec = resolve 'sqlite3:///foo_test'
- assert_equal('/foo_test', spec["database"])
+ spec = resolve "sqlite3:///foo_test"
+ assert_equal("/foo_test", spec["database"])
end
def test_url_absolute_path_for_sqlite3
- spec = resolve 'sqlite3:/foo_test'
- assert_equal('/foo_test', spec["database"])
+ spec = resolve "sqlite3:/foo_test"
+ assert_equal("/foo_test", spec["database"])
end
def test_url_relative_path_for_sqlite3
- spec = resolve 'sqlite3:foo_test'
- assert_equal('foo_test', spec["database"])
+ spec = resolve "sqlite3:foo_test"
+ assert_equal("foo_test", spec["database"])
end
def test_url_memory_db_for_sqlite3
- spec = resolve 'sqlite3::memory:'
- assert_equal(':memory:', spec["database"])
+ spec = resolve "sqlite3::memory:"
+ assert_equal(":memory:", spec["database"])
end
def test_url_sub_key_for_sqlite3
- spec = resolve :production, 'production' => {"url" => 'sqlite3:foo?encoding=utf8'}
+ spec = resolve :production, "production" => { "url" => "sqlite3:foo?encoding=utf8" }
assert_equal({
"adapter" => "sqlite3",
"database" => "foo",
- "encoding" => "utf8" }, spec)
+ "encoding" => "utf8",
+ "name" => "production" }, spec)
end
+ def test_spec_name_on_key_lookup
+ spec = spec(:readonly, "readonly" => { "adapter" => "sqlite3" })
+ assert_equal "readonly", spec.name
+ end
+
+ def test_spec_name_with_inline_config
+ spec = spec("adapter" => "sqlite3")
+ assert_equal "primary", spec.name, "should default to primary id"
+ end
end
end
end
diff --git a/activerecord/test/cases/core_test.rb b/activerecord/test/cases/core_test.rb
index 3cb98832c5..36e3d543cd 100644
--- a/activerecord/test/cases/core_test.rb
+++ b/activerecord/test/cases/core_test.rb
@@ -1,8 +1,9 @@
-require 'cases/helper'
-require 'models/person'
-require 'models/topic'
-require 'pp'
-require 'active_support/core_ext/string/strip'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/person"
+require "models/topic"
+require "pp"
class NonExistentTable < ActiveRecord::Base; end
@@ -10,8 +11,8 @@ class CoreTest < ActiveRecord::TestCase
fixtures :topics
def test_inspect_class
- assert_equal 'ActiveRecord::Base', ActiveRecord::Base.inspect
- assert_equal 'LoosePerson(abstract)', LoosePerson.inspect
+ assert_equal "ActiveRecord::Base", ActiveRecord::Base.inspect
+ assert_equal "LoosePerson(abstract)", LoosePerson.inspect
assert_match(/^Topic\(id: integer, title: string/, Topic.inspect)
end
@@ -25,8 +26,13 @@ class CoreTest < ActiveRecord::TestCase
end
def test_inspect_limited_select_instance
- assert_equal %(#<Topic id: 1>), Topic.all.merge!(:select => 'id', :where => 'id = 1').first.inspect
- assert_equal %(#<Topic id: 1, title: "The First Topic">), Topic.all.merge!(:select => 'id, title', :where => 'id = 1').first.inspect
+ assert_equal %(#<Topic id: 1>), Topic.all.merge!(select: "id", where: "id = 1").first.inspect
+ assert_equal %(#<Topic id: 1, title: "The First Topic">), Topic.all.merge!(select: "id, title", where: "id = 1").first.inspect
+ end
+
+ def test_inspect_instance_with_non_primary_key_id_attribute
+ topic = topics(:first).becomes(TitlePrimaryKeyTopic)
+ assert_match(/id: 1/, topic.inspect)
end
def test_inspect_class_without_table
@@ -35,68 +41,68 @@ class CoreTest < ActiveRecord::TestCase
def test_pretty_print_new
topic = Topic.new
- actual = ''
+ actual = +""
PP.pp(topic, StringIO.new(actual))
- expected = <<-PRETTY.strip_heredoc
- #<Topic:0xXXXXXX
- id: nil,
- title: nil,
- author_name: nil,
- author_email_address: "test@test.com",
- written_on: nil,
- bonus_time: nil,
- last_read: nil,
- content: nil,
- important: nil,
- approved: true,
- replies_count: 0,
- unique_replies_count: 0,
- parent_id: nil,
- parent_title: nil,
- type: nil,
- group: nil,
- created_at: nil,
- updated_at: nil>
+ expected = <<~PRETTY
+ #<Topic:0xXXXXXX
+ id: nil,
+ title: nil,
+ author_name: nil,
+ author_email_address: "test@test.com",
+ written_on: nil,
+ bonus_time: nil,
+ last_read: nil,
+ content: nil,
+ important: nil,
+ approved: true,
+ replies_count: 0,
+ unique_replies_count: 0,
+ parent_id: nil,
+ parent_title: nil,
+ type: nil,
+ group: nil,
+ created_at: nil,
+ updated_at: nil>
PRETTY
- assert actual.start_with?(expected.split('XXXXXX').first)
- assert actual.end_with?(expected.split('XXXXXX').last)
+ assert actual.start_with?(expected.split("XXXXXX").first)
+ assert actual.end_with?(expected.split("XXXXXX").last)
end
def test_pretty_print_persisted
topic = topics(:first)
- actual = ''
+ actual = +""
PP.pp(topic, StringIO.new(actual))
- expected = <<-PRETTY.strip_heredoc
- #<Topic:0x\\w+
- id: 1,
- title: "The First Topic",
- author_name: "David",
- author_email_address: "david@loudthinking.com",
- written_on: 2003-07-16 14:28:11 UTC,
- bonus_time: 2000-01-01 14:28:00 UTC,
- last_read: Thu, 15 Apr 2004,
- content: "Have a nice day",
- important: nil,
- approved: false,
- replies_count: 1,
- unique_replies_count: 0,
- parent_id: nil,
- parent_title: nil,
- type: nil,
- group: nil,
- created_at: [^,]+,
- updated_at: [^,>]+>
+ expected = <<~PRETTY
+ #<Topic:0x\\w+
+ id: 1,
+ title: "The First Topic",
+ author_name: "David",
+ author_email_address: "david@loudthinking.com",
+ written_on: 2003-07-16 14:28:11 UTC,
+ bonus_time: 2000-01-01 14:28:00 UTC,
+ last_read: Thu, 15 Apr 2004,
+ content: "Have a nice day",
+ important: nil,
+ approved: false,
+ replies_count: 1,
+ unique_replies_count: 0,
+ parent_id: nil,
+ parent_title: nil,
+ type: nil,
+ group: nil,
+ created_at: [^,]+,
+ updated_at: [^,>]+>
PRETTY
assert_match(/\A#{expected}\z/, actual)
end
def test_pretty_print_uninitialized
topic = Topic.allocate
- actual = ''
+ actual = +""
PP.pp(topic, StringIO.new(actual))
expected = "#<Topic:XXXXXX not initialized>\n"
- assert actual.start_with?(expected.split('XXXXXX').first)
- assert actual.end_with?(expected.split('XXXXXX').last)
+ assert actual.start_with?(expected.split("XXXXXX").first)
+ assert actual.end_with?(expected.split("XXXXXX").last)
end
def test_pretty_print_overridden_by_inspect
@@ -105,8 +111,15 @@ class CoreTest < ActiveRecord::TestCase
"inspecting topic"
end
end
- actual = ''
+ actual = +""
PP.pp(subtopic.new, StringIO.new(actual))
assert_equal "inspecting topic\n", actual
end
+
+ def test_pretty_print_with_non_primary_key_id_attribute
+ topic = topics(:first).becomes(TitlePrimaryKeyTopic)
+ actual = +""
+ PP.pp(topic, StringIO.new(actual))
+ assert_match(/id: 1/, actual)
+ end
end
diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb
index 1f5055b2a2..99d286dc52 100644
--- a/activerecord/test/cases/counter_cache_test.rb
+++ b/activerecord/test/cases/counter_cache_test.rb
@@ -1,29 +1,32 @@
-require 'cases/helper'
-require 'models/topic'
-require 'models/car'
-require 'models/wheel'
-require 'models/engine'
-require 'models/reply'
-require 'models/category'
-require 'models/categorization'
-require 'models/dog'
-require 'models/dog_lover'
-require 'models/person'
-require 'models/friendship'
-require 'models/subscriber'
-require 'models/subscription'
-require 'models/book'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/topic"
+require "models/car"
+require "models/aircraft"
+require "models/wheel"
+require "models/engine"
+require "models/reply"
+require "models/category"
+require "models/categorization"
+require "models/dog"
+require "models/dog_lover"
+require "models/person"
+require "models/friendship"
+require "models/subscriber"
+require "models/subscription"
+require "models/book"
class CounterCacheTest < ActiveRecord::TestCase
fixtures :topics, :categories, :categorizations, :cars, :dogs, :dog_lovers, :people, :friendships, :subscribers, :subscriptions, :books
class ::SpecialTopic < ::Topic
- has_many :special_replies, :foreign_key => 'parent_id'
- has_many :lightweight_special_replies, -> { select('topics.id, topics.title') }, :foreign_key => 'parent_id', :class_name => 'SpecialReply'
+ has_many :special_replies, foreign_key: "parent_id"
+ has_many :lightweight_special_replies, -> { select("topics.id, topics.title") }, foreign_key: "parent_id", class_name: "SpecialReply"
end
class ::SpecialReply < ::Reply
- belongs_to :special_topic, :foreign_key => 'parent_id', :counter_cache => 'replies_count'
+ belongs_to :special_topic, foreign_key: "parent_id", counter_cache: "replies_count"
end
setup do
@@ -31,13 +34,13 @@ class CounterCacheTest < ActiveRecord::TestCase
end
test "increment counter" do
- assert_difference '@topic.reload.replies_count' do
+ assert_difference "@topic.reload.replies_count" do
Topic.increment_counter(:replies_count, @topic.id)
end
end
test "decrement counter" do
- assert_difference '@topic.reload.replies_count', -1 do
+ assert_difference "@topic.reload.replies_count", -1 do
Topic.decrement_counter(:replies_count, @topic.id)
end
end
@@ -47,7 +50,7 @@ class CounterCacheTest < ActiveRecord::TestCase
Topic.increment_counter(:replies_count, @topic.id)
# check that it gets reset
- assert_difference '@topic.reload.replies_count', -1 do
+ assert_difference "@topic.reload.replies_count", -1 do
Topic.reset_counters(@topic.id, :replies)
end
end
@@ -57,31 +60,31 @@ class CounterCacheTest < ActiveRecord::TestCase
Topic.increment_counter(:replies_count, @topic.id)
# check that it gets reset
- assert_difference '@topic.reload.replies_count', -1 do
+ assert_difference "@topic.reload.replies_count", -1 do
Topic.reset_counters(@topic.id, :replies_count)
end
end
- test 'reset multiple counters' do
+ test "reset multiple counters" do
Topic.update_counters @topic.id, replies_count: 1, unique_replies_count: 1
- assert_difference ['@topic.reload.replies_count', '@topic.reload.unique_replies_count'], -1 do
+ assert_difference ["@topic.reload.replies_count", "@topic.reload.unique_replies_count"], -1 do
Topic.reset_counters(@topic.id, :replies, :unique_replies)
end
end
test "reset counters with string argument" do
- Topic.increment_counter('replies_count', @topic.id)
+ Topic.increment_counter("replies_count", @topic.id)
- assert_difference '@topic.reload.replies_count', -1 do
- Topic.reset_counters(@topic.id, 'replies')
+ assert_difference "@topic.reload.replies_count", -1 do
+ Topic.reset_counters(@topic.id, "replies")
end
end
test "reset counters with modularized and camelized classnames" do
- special = SpecialTopic.create!(:title => 'Special')
+ special = SpecialTopic.create!(title: "Special")
SpecialTopic.increment_counter(:replies_count, special.id)
- assert_difference 'special.reload.replies_count', -1 do
+ assert_difference "special.reload.replies_count", -1 do
SpecialTopic.reset_counters(special.id, :special_replies)
end
end
@@ -102,10 +105,10 @@ class CounterCacheTest < ActiveRecord::TestCase
DogLover.increment_counter(:bred_dogs_count, david.id)
DogLover.increment_counter(:trained_dogs_count, david.id)
- assert_difference 'david.reload.bred_dogs_count', -1 do
+ assert_difference "david.reload.bred_dogs_count", -1 do
DogLover.reset_counters(david.id, :bred_dogs)
end
- assert_difference 'david.reload.trained_dogs_count', -1 do
+ assert_difference "david.reload.trained_dogs_count", -1 do
DogLover.reset_counters(david.id, :trained_dogs)
end
end
@@ -115,26 +118,26 @@ class CounterCacheTest < ActiveRecord::TestCase
assert_equal 2, category.categorizations.count
assert_nil category.categorizations_count
- Category.update_counters(category.id, :categorizations_count => category.categorizations.count)
+ Category.update_counters(category.id, categorizations_count: category.categorizations.count)
assert_equal 2, category.reload.categorizations_count
end
test "update counter for decrement" do
- assert_difference '@topic.reload.replies_count', -3 do
- Topic.update_counters(@topic.id, :replies_count => -3)
+ assert_difference "@topic.reload.replies_count", -3 do
+ Topic.update_counters(@topic.id, replies_count: -3)
end
end
test "update counters of multiple records" do
t1, t2 = topics(:first, :second)
- assert_difference ['t1.reload.replies_count', 't2.reload.replies_count'], 2 do
- Topic.update_counters([t1.id, t2.id], :replies_count => 2)
+ assert_difference ["t1.reload.replies_count", "t2.reload.replies_count"], 2 do
+ Topic.update_counters([t1.id, t2.id], replies_count: 2)
end
end
- test 'update multiple counters' do
- assert_difference ['@topic.reload.replies_count', '@topic.reload.unique_replies_count'], 2 do
+ test "update multiple counters" do
+ assert_difference ["@topic.reload.replies_count", "@topic.reload.unique_replies_count"], 2 do
Topic.update_counters @topic.id, replies_count: 2, unique_replies_count: 2
end
end
@@ -143,25 +146,25 @@ class CounterCacheTest < ActiveRecord::TestCase
david, joanna = dog_lovers(:david, :joanna)
joanna = joanna # squelch a warning
- assert_difference 'joanna.reload.dogs_count', -1 do
+ assert_difference "joanna.reload.dogs_count", -1 do
david.destroy
end
end
test "reset the right counter if two have the same foreign key" do
michael = people(:michael)
- assert_nothing_raised(ActiveRecord::StatementInvalid) do
+ assert_nothing_raised do
Person.reset_counters(michael.id, :friends_too)
end
end
test "reset counter of has_many :through association" do
- subscriber = subscribers('second')
- Subscriber.reset_counters(subscriber.id, 'books')
- Subscriber.increment_counter('books_count', subscriber.id)
+ subscriber = subscribers("second")
+ Subscriber.reset_counters(subscriber.id, "books")
+ Subscriber.increment_counter("books_count", subscriber.id)
- assert_difference 'subscriber.reload.books_count', -1 do
- Subscriber.reset_counters(subscriber.id, 'books')
+ assert_difference "subscriber.reload.books_count", -1 do
+ Subscriber.reset_counters(subscriber.id, "books")
end
end
@@ -173,10 +176,10 @@ class CounterCacheTest < ActiveRecord::TestCase
end
test "reset counter works with select declared on association" do
- special = SpecialTopic.create!(:title => 'Special')
+ special = SpecialTopic.create!(title: "Special")
SpecialTopic.increment_counter(:replies_count, special.id)
- assert_difference 'special.reload.replies_count', -1 do
+ assert_difference "special.reload.replies_count", -1 do
SpecialTopic.reset_counters(special.id, :lightweight_special_replies)
end
end
@@ -198,4 +201,167 @@ class CounterCacheTest < ActiveRecord::TestCase
assert_equal 2, car.engines_count
assert_equal 2, car.reload.engines_count
end
+
+ test "update counters in a polymorphic relationship" do
+ aircraft = Aircraft.create!
+
+ assert_difference "aircraft.reload.wheels_count" do
+ aircraft.wheels << Wheel.create!
+ end
+
+ assert_difference "aircraft.reload.wheels_count", -1 do
+ 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, :updated_at, :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, :updated_at, :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, :updated_at, :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, :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: :written_on)
+ end
+ end
+
+ test "increment counters with touch: :written_on" do
+ assert_touching @topic, :updated_at, :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, :updated_at, :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/custom_locking_test.rb b/activerecord/test/cases/custom_locking_test.rb
index e8290297e3..f52b26e9ec 100644
--- a/activerecord/test/cases/custom_locking_test.rb
+++ b/activerecord/test/cases/custom_locking_test.rb
@@ -1,15 +1,17 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/person'
+require "models/person"
module ActiveRecord
class CustomLockingTest < ActiveRecord::TestCase
fixtures :people
def test_custom_lock
- if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
- assert_match 'SHARE MODE', Person.lock('LOCK IN SHARE MODE').to_sql
+ if current_adapter?(:Mysql2Adapter)
+ assert_match "SHARE MODE", Person.lock("LOCK IN SHARE MODE").to_sql
assert_sql(/LOCK IN SHARE MODE/) do
- Person.all.merge!(:lock => 'LOCK IN SHARE MODE').find(1)
+ Person.all.merge!(lock: "LOCK IN SHARE MODE").find(1)
end
end
end
diff --git a/activerecord/test/cases/database_statements_test.rb b/activerecord/test/cases/database_statements_test.rb
index c689e97d83..1c934602ec 100644
--- a/activerecord/test/cases/database_statements_test.rb
+++ b/activerecord/test/cases/database_statements_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
class DatabaseStatementsTest < ActiveRecord::TestCase
@@ -5,15 +7,31 @@ class DatabaseStatementsTest < ActiveRecord::TestCase
@connection = ActiveRecord::Base.connection
end
- def test_insert_should_return_the_inserted_id
- # Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method
- if current_adapter?(:OracleAdapter)
- sequence_name = "accounts_seq"
- id_value = @connection.next_sequence_value(sequence_name)
- id = @connection.insert("INSERT INTO accounts (id, firm_id,credit_limit) VALUES (accounts_seq.nextval,42,5000)", nil, :id, id_value, sequence_name)
- else
- id = @connection.insert("INSERT INTO accounts (firm_id,credit_limit) VALUES (42,5000)")
+ unless current_adapter?(:OracleAdapter)
+ def test_exec_insert
+ result = @connection.exec_insert("INSERT INTO accounts (firm_id,credit_limit) VALUES (42,5000)", nil, [])
+ assert_not_nil @connection.send(:last_inserted_id, result)
end
- assert_not_nil id
end
+
+ def test_insert_should_return_the_inserted_id
+ assert_not_nil return_the_inserted_id(method: :insert)
+ end
+
+ def test_create_should_return_the_inserted_id
+ assert_not_nil return_the_inserted_id(method: :create)
+ end
+
+ private
+
+ def return_the_inserted_id(method:)
+ # Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method
+ if current_adapter?(:OracleAdapter)
+ sequence_name = "accounts_seq"
+ id_value = @connection.next_sequence_value(sequence_name)
+ @connection.send(method, "INSERT INTO accounts (id, firm_id,credit_limit) VALUES (accounts_seq.nextval,42,5000)", nil, :id, id_value, sequence_name)
+ else
+ @connection.send(method, "INSERT INTO accounts (firm_id,credit_limit) VALUES (42,5000)")
+ end
+ end
end
diff --git a/activerecord/test/cases/date_test.rb b/activerecord/test/cases/date_test.rb
new file mode 100644
index 0000000000..9f412cdb63
--- /dev/null
+++ b/activerecord/test/cases/date_test.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/topic"
+
+class DateTest < ActiveRecord::TestCase
+ def test_date_with_time_value
+ time_value = Time.new(2016, 05, 11, 19, 0, 0)
+ topic = Topic.create(last_read: time_value)
+ assert_equal topic, Topic.find_by(last_read: time_value)
+ end
+
+ def test_date_with_string_value
+ string_value = "2016-05-11 19:00:00"
+ topic = Topic.create(last_read: string_value)
+ assert_equal topic, Topic.find_by(last_read: string_value)
+ end
+
+ def test_assign_valid_dates
+ valid_dates = [[2007, 11, 30], [1993, 2, 28], [2008, 2, 29]]
+
+ invalid_dates = [[2007, 11, 31], [1993, 2, 29], [2007, 2, 29]]
+
+ valid_dates.each do |date_src|
+ topic = Topic.new("last_read(1i)" => date_src[0].to_s, "last_read(2i)" => date_src[1].to_s, "last_read(3i)" => date_src[2].to_s)
+ # Oracle DATE columns are datetime columns and Oracle adapter returns Time value
+ if current_adapter?(:OracleAdapter)
+ assert_equal(topic.last_read.to_date, Date.new(*date_src))
+ else
+ assert_equal(topic.last_read, Date.new(*date_src))
+ end
+ end
+
+ invalid_dates.each do |date_src|
+ assert_nothing_raised do
+ topic = Topic.new("last_read(1i)" => date_src[0].to_s, "last_read(2i)" => date_src[1].to_s, "last_read(3i)" => date_src[2].to_s)
+ # Oracle DATE columns are datetime columns and Oracle adapter returns Time value
+ if current_adapter?(:OracleAdapter)
+ assert_equal(topic.last_read.to_date, Time.local(*date_src).to_date, "The date should be modified according to the behavior of the Time object")
+ else
+ assert_equal(topic.last_read, Time.local(*date_src).to_date, "The date should be modified according to the behavior of the Time object")
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/date_time_precision_test.rb b/activerecord/test/cases/date_time_precision_test.rb
index 698f1b852e..e64a8372d0 100644
--- a/activerecord/test/cases/date_time_precision_test.rb
+++ b/activerecord/test/cases/date_time_precision_test.rb
@@ -1,111 +1,107 @@
-require 'cases/helper'
-require 'support/schema_dumping_helper'
+# frozen_string_literal: true
-if ActiveRecord::Base.connection.supports_datetime_with_precision?
-class DateTimePrecisionTest < ActiveRecord::TestCase
- include SchemaDumpingHelper
- self.use_transactional_tests = false
+require "cases/helper"
+require "support/schema_dumping_helper"
- class Foo < ActiveRecord::Base; end
+if subsecond_precision_supported?
+ class DateTimePrecisionTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
+ self.use_transactional_tests = false
- setup do
- @connection = ActiveRecord::Base.connection
- end
+ class Foo < ActiveRecord::Base; end
- teardown do
- @connection.drop_table :foos, if_exists: true
- end
+ setup do
+ @connection = ActiveRecord::Base.connection
+ Foo.reset_column_information
+ end
- def test_datetime_data_type_with_precision
- @connection.create_table(:foos, force: true)
- @connection.add_column :foos, :created_at, :datetime, precision: 0
- @connection.add_column :foos, :updated_at, :datetime, precision: 5
- assert_equal 0, activerecord_column_option('foos', 'created_at', 'precision')
- assert_equal 5, activerecord_column_option('foos', 'updated_at', 'precision')
- end
+ teardown do
+ @connection.drop_table :foos, if_exists: true
+ end
- def test_timestamps_helper_with_custom_precision
- @connection.create_table(:foos, force: true) do |t|
- t.timestamps precision: 4
+ def test_datetime_data_type_with_precision
+ @connection.create_table(:foos, force: true)
+ @connection.add_column :foos, :created_at, :datetime, precision: 0
+ @connection.add_column :foos, :updated_at, :datetime, precision: 5
+ assert_equal 0, Foo.columns_hash["created_at"].precision
+ assert_equal 5, Foo.columns_hash["updated_at"].precision
end
- assert_equal 4, activerecord_column_option('foos', 'created_at', 'precision')
- assert_equal 4, activerecord_column_option('foos', 'updated_at', 'precision')
- end
- def test_passing_precision_to_datetime_does_not_set_limit
- @connection.create_table(:foos, force: true) do |t|
- t.timestamps precision: 4
+ def test_datetime_precision_is_truncated_on_assignment
+ @connection.create_table(:foos, force: true)
+ @connection.add_column :foos, :created_at, :datetime, precision: 0
+ @connection.add_column :foos, :updated_at, :datetime, precision: 6
+
+ time = ::Time.now.change(nsec: 123456789)
+ foo = Foo.new(created_at: time, updated_at: time)
+
+ assert_equal 0, foo.created_at.nsec
+ assert_equal 123456000, foo.updated_at.nsec
+
+ foo.save!
+ foo.reload
+
+ assert_equal 0, foo.created_at.nsec
+ assert_equal 123456000, foo.updated_at.nsec
end
- assert_nil activerecord_column_option('foos', 'created_at', 'limit')
- assert_nil activerecord_column_option('foos', 'updated_at', 'limit')
- end
- def test_invalid_datetime_precision_raises_error
- assert_raises ActiveRecord::ActiveRecordError do
+ def test_timestamps_helper_with_custom_precision
@connection.create_table(:foos, force: true) do |t|
- t.timestamps precision: 7
+ t.timestamps precision: 4
end
+ assert_equal 4, Foo.columns_hash["created_at"].precision
+ assert_equal 4, Foo.columns_hash["updated_at"].precision
end
- end
- def test_database_agrees_with_activerecord_about_precision
- @connection.create_table(:foos, force: true) do |t|
- t.timestamps precision: 4
+ def test_passing_precision_to_datetime_does_not_set_limit
+ @connection.create_table(:foos, force: true) do |t|
+ t.timestamps precision: 4
+ end
+ assert_nil Foo.columns_hash["created_at"].limit
+ assert_nil Foo.columns_hash["updated_at"].limit
end
- assert_equal 4, database_datetime_precision('foos', 'created_at')
- assert_equal 4, database_datetime_precision('foos', 'updated_at')
- end
- def test_formatting_datetime_according_to_precision
- @connection.create_table(:foos, force: true) do |t|
- t.datetime :created_at, precision: 0
- t.datetime :updated_at, precision: 4
+ def test_invalid_datetime_precision_raises_error
+ assert_raises ActiveRecord::ActiveRecordError do
+ @connection.create_table(:foos, force: true) do |t|
+ t.timestamps precision: 7
+ end
+ end
end
- date = ::Time.utc(2014, 8, 17, 12, 30, 0, 999999)
- Foo.create!(created_at: date, updated_at: date)
- assert foo = Foo.find_by(created_at: date)
- assert_equal 1, Foo.where(updated_at: date).count
- assert_equal date.to_s, foo.created_at.to_s
- assert_equal date.to_s, foo.updated_at.to_s
- assert_equal 000000, foo.created_at.usec
- assert_equal 999900, foo.updated_at.usec
- end
- def test_schema_dump_includes_datetime_precision
- @connection.create_table(:foos, force: true) do |t|
- t.timestamps precision: 6
+ def test_formatting_datetime_according_to_precision
+ @connection.create_table(:foos, force: true) do |t|
+ t.datetime :created_at, precision: 0
+ t.datetime :updated_at, precision: 4
+ end
+ date = ::Time.utc(2014, 8, 17, 12, 30, 0, 999999)
+ Foo.create!(created_at: date, updated_at: date)
+ assert foo = Foo.find_by(created_at: date)
+ assert_equal 1, Foo.where(updated_at: date).count
+ assert_equal date.to_s, foo.created_at.to_s
+ assert_equal date.to_s, foo.updated_at.to_s
+ assert_equal 000000, foo.created_at.usec
+ assert_equal 999900, foo.updated_at.usec
end
- output = dump_table_schema("foos")
- assert_match %r{t\.datetime\s+"created_at",\s+precision: 6,\s+null: false$}, output
- assert_match %r{t\.datetime\s+"updated_at",\s+precision: 6,\s+null: false$}, output
- end
- if current_adapter?(:PostgreSQLAdapter)
- def test_datetime_precision_with_zero_should_be_dumped
+ def test_schema_dump_includes_datetime_precision
@connection.create_table(:foos, force: true) do |t|
- t.timestamps precision: 0
+ t.timestamps precision: 6
end
output = dump_table_schema("foos")
- assert_match %r{t\.datetime\s+"created_at",\s+precision: 0,\s+null: false$}, output
- assert_match %r{t\.datetime\s+"updated_at",\s+precision: 0,\s+null: false$}, output
+ assert_match %r{t\.datetime\s+"created_at",\s+precision: 6,\s+null: false$}, output
+ assert_match %r{t\.datetime\s+"updated_at",\s+precision: 6,\s+null: false$}, output
end
- end
- private
-
- def database_datetime_precision(table_name, column_name)
- results = @connection.exec_query("SELECT column_name, datetime_precision FROM information_schema.columns WHERE table_name = '#{table_name}'")
- result = results.find do |result_hash|
- result_hash["column_name"] == column_name
- end
- result && result["datetime_precision"].to_i
- end
-
- def activerecord_column_option(tablename, column_name, option)
- result = @connection.columns(tablename).find do |column|
- column.name == column_name
+ 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
+ end
+ output = dump_table_schema("foos")
+ assert_match %r{t\.datetime\s+"created_at",\s+precision: 0,\s+null: false$}, output
+ assert_match %r{t\.datetime\s+"updated_at",\s+precision: 0,\s+null: false$}, output
+ end
end
- result && result.send(option)
end
end
-end
diff --git a/activerecord/test/cases/date_time_test.rb b/activerecord/test/cases/date_time_test.rb
index 4cbff564aa..b5f35aff0e 100644
--- a/activerecord/test/cases/date_time_test.rb
+++ b/activerecord/test/cases/date_time_test.rb
@@ -1,12 +1,14 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/topic'
-require 'models/task'
+require "models/topic"
+require "models/task"
class DateTimeTest < ActiveRecord::TestCase
include InTimeZone
def test_saves_both_date_and_time
- with_env_tz 'America/New_York' do
+ with_env_tz "America/New_York" do
with_timezone_config default: :utc do
time_values = [1807, 2, 10, 15, 30, 45]
# create DateTime value with local time zone offset
@@ -25,7 +27,7 @@ class DateTimeTest < ActiveRecord::TestCase
def test_assign_empty_date_time
task = Task.new
- task.starting = ''
+ task.starting = ""
task.ending = nil
assert_nil task.starting
assert_nil task.ending
@@ -34,28 +36,41 @@ class DateTimeTest < ActiveRecord::TestCase
def test_assign_bad_date_time_with_timezone
in_time_zone "Pacific Time (US & Canada)" do
task = Task.new
- task.starting = '2014-07-01T24:59:59GMT'
+ task.starting = "2014-07-01T24:59:59GMT"
assert_nil task.starting
end
end
def test_assign_empty_date
topic = Topic.new
- topic.last_read = ''
+ topic.last_read = ""
assert_nil topic.last_read
end
def test_assign_empty_time
topic = Topic.new
- topic.bonus_time = ''
+ topic.bonus_time = ""
assert_nil topic.bonus_time
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
end
end
+
+ def test_date_time_with_string_value_with_subsecond_precision
+ skip unless subsecond_precision_supported?
+ string_value = "2017-07-04 14:19:00.5"
+ topic = Topic.create(written_on: string_value)
+ assert_equal topic, Topic.find_by(written_on: string_value)
+ end
+
+ def test_date_time_with_string_value_with_non_iso_format
+ string_value = "04/07/2017 2:19pm"
+ topic = Topic.create(written_on: string_value)
+ assert_equal topic, Topic.find_by(written_on: string_value)
+ end
end
diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb
index 67fddebf45..5d02e59ef6 100644
--- a/activerecord/test/cases/defaults_test.rb
+++ b/activerecord/test/cases/defaults_test.rb
@@ -1,20 +1,16 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/default'
-require 'models/entrant'
+require "support/schema_dumping_helper"
+require "models/default"
+require "models/entrant"
class DefaultTest < ActiveRecord::TestCase
def test_nil_defaults_for_not_null_columns
- column_defaults =
- if current_adapter?(:MysqlAdapter) && (Mysql.client_version < 50051 || (50100..50122).include?(Mysql.client_version))
- { 'id' => nil, 'name' => '', 'course_id' => nil }
- else
- { 'id' => nil, 'name' => nil, 'course_id' => nil }
- end
-
- column_defaults.each do |name, default|
+ %w(id name course_id).each do |name|
column = Entrant.columns_hash[name]
- assert !column.null, "#{name} column should be NOT NULL"
- assert_equal default, column.default, "#{name} column should be DEFAULT #{default.inspect}"
+ assert_not column.null, "#{name} column should be NOT NULL"
+ assert_not column.default, "#{name} column should be DEFAULT 'nil'"
end
end
@@ -57,7 +53,7 @@ class DefaultNumbersTest < ActiveRecord::TestCase
def test_default_decimal_number
record = DefaultNumber.new
- assert_equal BigDecimal.new("2.78"), record.decimal_number
+ assert_equal BigDecimal("2.78"), record.decimal_number
assert_equal "2.78", record.decimal_number_before_type_cast
end
end
@@ -87,7 +83,64 @@ class DefaultStringsTest < ActiveRecord::TestCase
end
end
-if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
+if current_adapter?(:PostgreSQLAdapter)
+ class PostgresqlDefaultExpressionTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
+
+ test "schema dump includes default expression" do
+ output = dump_table_schema("defaults")
+ if ActiveRecord::Base.connection.postgresql_version >= 100000
+ assert_match %r/t\.date\s+"modified_date",\s+default: -> { "CURRENT_DATE" }/, output
+ assert_match %r/t\.datetime\s+"modified_time",\s+default: -> { "CURRENT_TIMESTAMP" }/, output
+ else
+ assert_match %r/t\.date\s+"modified_date",\s+default: -> { "\('now'::text\)::date" }/, output
+ assert_match %r/t\.datetime\s+"modified_time",\s+default: -> { "now\(\)" }/, output
+ end
+ assert_match %r/t\.date\s+"modified_date_function",\s+default: -> { "now\(\)" }/, output
+ assert_match %r/t\.datetime\s+"modified_time_function",\s+default: -> { "now\(\)" }/, output
+ end
+ end
+end
+
+if current_adapter?(:Mysql2Adapter)
+ class MysqlDefaultExpressionTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
+
+ if supports_default_expression?
+ test "schema dump includes default expression" do
+ output = dump_table_schema("defaults")
+ assert_match %r/t\.binary\s+"uuid",\s+limit: 36,\s+default: -> { "\(uuid\(\)\)" }/i, output
+ end
+ end
+
+ if subsecond_precision_supported?
+ 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(?:\(\))?" }/i, output
+ end
+
+ test "schema dump datetime includes precise default expression" do
+ output = dump_table_schema("datetime_defaults")
+ assert_match %r/t\.datetime\s+"precise_datetime",.+default: -> { "CURRENT_TIMESTAMP\(6\)" }/i, output
+ 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(?:\(\))?" }/i, output
+ end
+
+ test "schema dump timestamp includes precise default expression" do
+ output = dump_table_schema("timestamp_defaults")
+ assert_match %r/t\.timestamp\s+"precise_timestamp",.+default: -> { "CURRENT_TIMESTAMP\(6\)" }/i, 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
+ end
+
class DefaultsTestWithoutTransactionalFixtures < ActiveRecord::TestCase
# ActiveRecord::Base#create! (and #save and other related methods) will
# open a new transaction. When in transactional tests mode, this will
@@ -108,93 +161,66 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
ActiveRecord::Base.establish_connection connection
end
- # MySQL cannot have defaults on text/blob columns. It reports the
- # default value as null.
- #
- # Despite this, in non-strict mode, MySQL will use an empty string
- # as the default value of the field, if no other value is
- # specified.
+ # Strict mode controls how MySQL handles invalid or missing values
+ # in data-change statements such as INSERT or UPDATE. A value can be
+ # invalid for several reasons. For example, it might have the wrong
+ # data type for the column, or it might be out of range. A value is
+ # missing when a new row to be inserted does not contain a value for
+ # a non-NULL column that has no explicit DEFAULT clause in its definition.
+ # (For a NULL column, NULL is inserted if the value is missing.)
#
- # Therefore, in non-strict mode, we want column.default to report
- # an empty string as its default, to be consistent with that.
+ # If strict mode is not in effect, MySQL inserts adjusted values for
+ # invalid or missing values and produces warnings. In strict mode,
+ # you can produce this behavior by using INSERT IGNORE or UPDATE IGNORE.
#
- # In strict mode, column.default should be nil.
- def test_mysql_text_not_null_defaults_non_strict
+ # https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sql-mode-strict
+ def test_mysql_not_null_defaults_non_strict
using_strict(false) do
- with_text_blob_not_null_table do |klass|
+ with_mysql_not_null_table do |klass|
record = klass.new
- assert_equal '', record.non_null_blob
- assert_equal '', record.non_null_text
-
- assert_nil record.null_blob
- assert_nil record.null_text
+ 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 '', record.non_null_text
- assert_equal '', record.non_null_blob
-
- assert_nil record.null_text
- assert_nil record.null_blob
+ 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
- def test_mysql_text_not_null_defaults_strict
+ def test_mysql_not_null_defaults_strict
using_strict(true) do
- with_text_blob_not_null_table do |klass|
+ with_mysql_not_null_table do |klass|
record = klass.new
- assert_nil record.non_null_blob
+ assert_nil record.non_null_integer
+ assert_nil record.non_null_string
assert_nil record.non_null_text
- assert_nil record.null_blob
- assert_nil record.null_text
+ assert_nil record.non_null_blob
- assert_raises(ActiveRecord::StatementInvalid) { klass.create }
+ assert_raises(ActiveRecord::NotNullViolation) { klass.create }
end
end
end
- def with_text_blob_not_null_table
+ def with_mysql_not_null_table
klass = Class.new(ActiveRecord::Base)
- klass.table_name = 'test_mysql_text_not_null_defaults'
+ klass.table_name = "test_mysql_not_null_defaults"
klass.connection.create_table klass.table_name do |t|
- t.column :non_null_text, :text, :null => false
- t.column :non_null_blob, :blob, :null => false
- t.column :null_text, :text, :null => true
- t.column :null_blob, :blob, :null => true
+ t.integer :non_null_integer, null: false
+ t.string :non_null_string, null: false
+ t.text :non_null_text, null: false
+ t.blob :non_null_blob, null: false
end
yield klass
ensure
klass.connection.drop_table(klass.table_name) rescue nil
end
-
- # MySQL uses an implicit default 0 rather than NULL unless in strict mode.
- # We use an implicit NULL so schema.rb is compatible with other databases.
- def test_mysql_integer_not_null_defaults
- klass = Class.new(ActiveRecord::Base)
- klass.table_name = 'test_integer_not_null_default_zero'
- klass.connection.create_table klass.table_name do |t|
- t.column :zero, :integer, :null => false, :default => 0
- t.column :omit, :integer, :null => false
- end
-
- assert_equal '0', klass.columns_hash['zero'].default
- assert !klass.columns_hash['zero'].null
- # 0 in MySQL 4, nil in 5.
- assert [0, nil].include?(klass.columns_hash['omit'].default)
- assert !klass.columns_hash['omit'].null
-
- assert_raise(ActiveRecord::StatementInvalid) { klass.create! }
-
- assert_nothing_raised do
- instance = klass.create!(:omit => 1)
- assert_equal 0, instance.zero
- assert_equal 1, instance.omit
- end
- ensure
- klass.connection.drop_table(klass.table_name) rescue nil
- end
end
end
diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb
index f5aaf22e13..dfd74bfcb4 100644
--- a/activerecord/test/cases/dirty_test.rb
+++ b/activerecord/test/cases/dirty_test.rb
@@ -1,122 +1,104 @@
-require 'cases/helper'
-require 'models/topic' # For booleans
-require 'models/pirate' # For timestamps
-require 'models/parrot'
-require 'models/person' # For optimistic locking
-require 'models/aircraft'
+# frozen_string_literal: true
-class Pirate # Just reopening it, not defining it
- attr_accessor :detected_changes_in_after_update # Boolean for if changes are detected
- attr_accessor :changes_detected_in_after_update # Actual changes
-
- after_update :check_changes
-
-private
- # after_save/update and the model itself
- # can end up checking dirty status and acting on the results
- def check_changes
- if self.changed?
- self.detected_changes_in_after_update = true
- self.changes_detected_in_after_update = self.changes
- end
- end
-end
-
-class NumericData < ActiveRecord::Base
- self.table_name = 'numeric_data'
-end
+require "cases/helper"
+require "models/topic" # For booleans
+require "models/pirate" # For timestamps
+require "models/parrot"
+require "models/person" # For optimistic locking
+require "models/aircraft"
+require "models/numeric_data"
class DirtyTest < ActiveRecord::TestCase
include InTimeZone
# Dummy to force column loads so query counts are clean.
def setup
- Person.create :first_name => 'foo'
+ Person.create first_name: "foo"
end
def test_attribute_changes
# New record - no changes.
pirate = Pirate.new
- assert !pirate.catchphrase_changed?
- assert_nil pirate.catchphrase_change
+ assert_equal false, pirate.catchphrase_changed?
+ assert_equal false, pirate.non_validated_parrot_id_changed?
# Change catchphrase.
- pirate.catchphrase = 'arrr'
- assert pirate.catchphrase_changed?
+ pirate.catchphrase = "arrr"
+ assert_predicate pirate, :catchphrase_changed?
assert_nil pirate.catchphrase_was
- assert_equal [nil, 'arrr'], pirate.catchphrase_change
+ assert_equal [nil, "arrr"], pirate.catchphrase_change
# Saved - no changes.
pirate.save!
- assert !pirate.catchphrase_changed?
+ assert_not_predicate pirate, :catchphrase_changed?
assert_nil pirate.catchphrase_change
# Same value - no changes.
- pirate.catchphrase = 'arrr'
- assert !pirate.catchphrase_changed?
+ pirate.catchphrase = "arrr"
+ assert_not_predicate pirate, :catchphrase_changed?
assert_nil pirate.catchphrase_change
end
def test_time_attributes_changes_with_time_zone
- in_time_zone 'Paris' do
+ in_time_zone "Paris" do
target = Class.new(ActiveRecord::Base)
- target.table_name = 'pirates'
+ target.table_name = "pirates"
# New record - no changes.
pirate = target.new
- assert !pirate.created_on_changed?
+ assert_not_predicate pirate, :created_on_changed?
assert_nil pirate.created_on_change
# Saved - no changes.
- pirate.catchphrase = 'arrrr, time zone!!'
+ pirate.catchphrase = "arrrr, time zone!!"
pirate.save!
- assert !pirate.created_on_changed?
+ assert_not_predicate pirate, :created_on_changed?
assert_nil pirate.created_on_change
# Change created_on.
old_created_on = pirate.created_on
pirate.created_on = Time.now - 1.day
- assert pirate.created_on_changed?
+ assert_predicate pirate, :created_on_changed?
assert_kind_of ActiveSupport::TimeWithZone, pirate.created_on_was
assert_equal old_created_on, pirate.created_on_was
pirate.created_on = old_created_on
- assert !pirate.created_on_changed?
+ assert_not_predicate pirate, :created_on_changed?
end
end
def test_setting_time_attributes_with_time_zone_field_to_itself_should_not_be_marked_as_a_change
- in_time_zone 'Paris' do
+ in_time_zone "Paris" do
target = Class.new(ActiveRecord::Base)
- target.table_name = 'pirates'
+ target.table_name = "pirates"
- pirate = target.create
+ pirate = target.create!
pirate.created_on = pirate.created_on
- assert !pirate.created_on_changed?
+ assert_not_predicate pirate, :created_on_changed?
end
end
def test_time_attributes_changes_without_time_zone_by_skip
- in_time_zone 'Paris' do
+ in_time_zone "Paris" do
target = Class.new(ActiveRecord::Base)
- target.table_name = 'pirates'
+ target.table_name = "pirates"
target.skip_time_zone_conversion_for_attributes = [:created_on]
# New record - no changes.
pirate = target.new
- assert !pirate.created_on_changed?
+ assert_not_predicate pirate, :created_on_changed?
assert_nil pirate.created_on_change
# Saved - no changes.
- pirate.catchphrase = 'arrrr, time zone!!'
+ pirate.catchphrase = "arrrr, time zone!!"
pirate.save!
- assert !pirate.created_on_changed?
+ assert_not_predicate pirate, :created_on_changed?
assert_nil pirate.created_on_change
# Change created_on.
old_created_on = pirate.created_on
pirate.created_on = Time.now + 1.day
- assert pirate.created_on_changed?
+ assert_predicate pirate, :created_on_changed?
# kind_of does not work because
# ActiveSupport::TimeWithZone.name == 'Time'
assert_instance_of Time, pirate.created_on_was
@@ -127,23 +109,23 @@ class DirtyTest < ActiveRecord::TestCase
def test_time_attributes_changes_without_time_zone
with_timezone_config aware_attributes: false do
target = Class.new(ActiveRecord::Base)
- target.table_name = 'pirates'
+ target.table_name = "pirates"
# New record - no changes.
pirate = target.new
- assert !pirate.created_on_changed?
+ assert_not_predicate pirate, :created_on_changed?
assert_nil pirate.created_on_change
# Saved - no changes.
- pirate.catchphrase = 'arrrr, time zone!!'
+ pirate.catchphrase = "arrrr, time zone!!"
pirate.save!
- assert !pirate.created_on_changed?
+ assert_not_predicate pirate, :created_on_changed?
assert_nil pirate.created_on_change
# Change created_on.
old_created_on = pirate.created_on
pirate.created_on = Time.now + 1.day
- assert pirate.created_on_changed?
+ assert_predicate pirate, :created_on_changed?
# kind_of does not work because
# ActiveSupport::TimeWithZone.name == 'Time'
assert_instance_of Time, pirate.created_on_was
@@ -151,28 +133,27 @@ class DirtyTest < ActiveRecord::TestCase
end
end
-
def test_aliased_attribute_changes
# the actual attribute here is name, title is an
# alias setup via alias_attribute
parrot = Parrot.new
- assert !parrot.title_changed?
+ assert_not_predicate parrot, :title_changed?
assert_nil parrot.title_change
- parrot.name = 'Sam'
- assert parrot.title_changed?
+ parrot.name = "Sam"
+ assert_predicate parrot, :title_changed?
assert_nil parrot.title_was
assert_equal parrot.name_change, parrot.title_change
end
def test_restore_attribute!
- pirate = Pirate.create!(:catchphrase => 'Yar!')
- pirate.catchphrase = 'Ahoy!'
+ pirate = Pirate.create!(catchphrase: "Yar!")
+ pirate.catchphrase = "Ahoy!"
pirate.restore_catchphrase!
assert_equal "Yar!", pirate.catchphrase
assert_equal Hash.new, pirate.changes
- assert !pirate.catchphrase_changed?
+ assert_not_predicate pirate, :catchphrase_changed?
end
def test_nullable_number_not_marked_as_changed_if_new_value_is_blank
@@ -180,7 +161,7 @@ class DirtyTest < ActiveRecord::TestCase
["", nil].each do |value|
pirate.parrot_id = value
- assert !pirate.parrot_id_changed?
+ assert_not_predicate pirate, :parrot_id_changed?
assert_nil pirate.parrot_id_change
end
end
@@ -190,7 +171,7 @@ class DirtyTest < ActiveRecord::TestCase
["", nil].each do |value|
numeric_data.bank_balance = value
- assert !numeric_data.bank_balance_changed?
+ assert_not_predicate numeric_data, :bank_balance_changed?
assert_nil numeric_data.bank_balance_change
end
end
@@ -200,15 +181,15 @@ class DirtyTest < ActiveRecord::TestCase
["", nil].each do |value|
numeric_data.temperature = value
- assert !numeric_data.temperature_changed?
+ assert_not_predicate numeric_data, :temperature_changed?
assert_nil numeric_data.temperature_change
end
end
def test_nullable_datetime_not_marked_as_changed_if_new_value_is_blank
- in_time_zone 'Edinburgh' do
+ in_time_zone "Edinburgh" do
target = Class.new(ActiveRecord::Base)
- target.table_name = 'topics'
+ target.table_name = "topics"
topic = target.create
assert_nil topic.written_on
@@ -216,7 +197,7 @@ class DirtyTest < ActiveRecord::TestCase
["", nil].each do |value|
topic.written_on = value
assert_nil topic.written_on
- assert !topic.written_on_changed?
+ assert_not_predicate topic, :written_on_changed?
end
end
end
@@ -224,40 +205,40 @@ class DirtyTest < ActiveRecord::TestCase
def test_integer_zero_to_string_zero_not_marked_as_changed
pirate = Pirate.new
pirate.parrot_id = 0
- pirate.catchphrase = 'arrr'
+ pirate.catchphrase = "arrr"
assert pirate.save!
- assert !pirate.changed?
+ assert_not_predicate pirate, :changed?
- pirate.parrot_id = '0'
- assert !pirate.changed?
+ pirate.parrot_id = "0"
+ assert_not_predicate pirate, :changed?
end
def test_integer_zero_to_integer_zero_not_marked_as_changed
pirate = Pirate.new
pirate.parrot_id = 0
- pirate.catchphrase = 'arrr'
+ pirate.catchphrase = "arrr"
assert pirate.save!
- assert !pirate.changed?
+ assert_not_predicate pirate, :changed?
pirate.parrot_id = 0
- assert !pirate.changed?
+ assert_not_predicate pirate, :changed?
end
def test_float_zero_to_string_zero_not_marked_as_changed
- data = NumericData.new :temperature => 0.0
+ data = NumericData.new temperature: 0.0
data.save!
- assert_not data.changed?
+ assert_not_predicate data, :changed?
- data.temperature = '0'
+ data.temperature = "0"
assert_empty data.changes
- data.temperature = '0.0'
+ data.temperature = "0.0"
assert_empty data.changes
- data.temperature = '0.00'
+ data.temperature = "0.00"
assert_empty data.changes
end
@@ -269,108 +250,115 @@ class DirtyTest < ActiveRecord::TestCase
# check the change from 1 to ''
pirate = Pirate.find_by_catchphrase("Yarrrr, me hearties")
- pirate.parrot_id = ''
- assert pirate.parrot_id_changed?
+ pirate.parrot_id = ""
+ assert_predicate pirate, :parrot_id_changed?
assert_equal([1, nil], pirate.parrot_id_change)
pirate.save
# check the change from nil to 0
pirate = Pirate.find_by_catchphrase("Yarrrr, me hearties")
pirate.parrot_id = 0
- assert pirate.parrot_id_changed?
+ assert_predicate pirate, :parrot_id_changed?
assert_equal([nil, 0], pirate.parrot_id_change)
pirate.save
# check the change from 0 to ''
pirate = Pirate.find_by_catchphrase("Yarrrr, me hearties")
- pirate.parrot_id = ''
- assert pirate.parrot_id_changed?
+ pirate.parrot_id = ""
+ assert_predicate pirate, :parrot_id_changed?
assert_equal([0, nil], pirate.parrot_id_change)
end
def test_object_should_be_changed_if_any_attribute_is_changed
pirate = Pirate.new
- assert !pirate.changed?
+ assert_not_predicate pirate, :changed?
assert_equal [], pirate.changed
assert_equal Hash.new, pirate.changes
- pirate.catchphrase = 'arrr'
- assert pirate.changed?
+ pirate.catchphrase = "arrr"
+ assert_predicate pirate, :changed?
assert_nil pirate.catchphrase_was
assert_equal %w(catchphrase), pirate.changed
- assert_equal({'catchphrase' => [nil, 'arrr']}, pirate.changes)
+ assert_equal({ "catchphrase" => [nil, "arrr"] }, pirate.changes)
pirate.save
- assert !pirate.changed?
+ assert_not_predicate pirate, :changed?
assert_equal [], pirate.changed
assert_equal Hash.new, pirate.changes
end
def test_attribute_will_change!
- pirate = Pirate.create!(:catchphrase => 'arr')
+ pirate = Pirate.create!(catchphrase: "arr")
- assert !pirate.catchphrase_changed?
+ assert_not_predicate pirate, :catchphrase_changed?
assert pirate.catchphrase_will_change!
- assert pirate.catchphrase_changed?
- assert_equal ['arr', 'arr'], pirate.catchphrase_change
+ assert_predicate pirate, :catchphrase_changed?
+ assert_equal ["arr", "arr"], pirate.catchphrase_change
- pirate.catchphrase << ' matey!'
- assert pirate.catchphrase_changed?
- assert_equal ['arr', 'arr matey!'], pirate.catchphrase_change
+ pirate.catchphrase << " matey!"
+ assert_predicate pirate, :catchphrase_changed?
+ assert_equal ["arr", "arr matey!"], pirate.catchphrase_change
+ end
+
+ def test_virtual_attribute_will_change
+ parrot = Parrot.create!(name: "Ruby")
+ parrot.send(:attribute_will_change!, :cancel_save_from_callback)
+ assert_predicate parrot, :has_changes_to_save?
end
def test_association_assignment_changes_foreign_key
- pirate = Pirate.create!(:catchphrase => 'jarl')
- pirate.parrot = Parrot.create!(:name => 'Lorre')
- assert pirate.changed?
+ pirate = Pirate.create!(catchphrase: "jarl")
+ pirate.parrot = Parrot.create!(name: "Lorre")
+ assert_predicate pirate, :changed?
assert_equal %w(parrot_id), pirate.changed
end
def test_attribute_should_be_compared_with_type_cast
topic = Topic.new
- assert topic.approved?
- assert !topic.approved_changed?
+ assert_predicate topic, :approved?
+ assert_not_predicate topic, :approved_changed?
# Coming from web form.
- params = {:topic => {:approved => 1}}
+ params = { topic: { approved: 1 } }
# In the controller.
topic.attributes = params[:topic]
- assert topic.approved?
- assert !topic.approved_changed?
+ assert_predicate topic, :approved?
+ assert_not_predicate topic, :approved_changed?
end
def test_partial_update
- pirate = Pirate.new(:catchphrase => 'foo')
+ pirate = Pirate.new(catchphrase: "foo")
old_updated_on = 1.hour.ago.beginning_of_day
with_partial_writes Pirate, false do
assert_queries(2) { 2.times { pirate.save! } }
- Pirate.where(id: pirate.id).update_all(:updated_on => old_updated_on)
+ Pirate.where(id: pirate.id).update_all(updated_on: old_updated_on)
end
with_partial_writes Pirate, true do
- assert_queries(0) { 2.times { pirate.save! } }
+ assert_no_queries { 2.times { pirate.save! } }
assert_equal old_updated_on, pirate.reload.updated_on
- assert_queries(1) { pirate.catchphrase = 'bar'; pirate.save! }
+ assert_queries(1) { pirate.catchphrase = "bar"; pirate.save! }
assert_not_equal old_updated_on, pirate.reload.updated_on
end
end
def test_partial_update_with_optimistic_locking
- person = Person.new(:first_name => 'foo')
- old_lock_version = 1
+ person = Person.new(first_name: "foo")
with_partial_writes Person, false do
assert_queries(2) { 2.times { person.save! } }
- Person.where(id: person.id).update_all(:first_name => 'baz')
+ 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_no_queries { 2.times { person.save! } }
assert_equal old_lock_version, person.reload.lock_version
- assert_queries(1) { person.first_name = 'bar'; person.save! }
+ assert_queries(1) { person.first_name = "bar"; person.save! }
assert_not_equal old_lock_version, person.reload.lock_version
end
end
@@ -378,7 +366,7 @@ class DirtyTest < ActiveRecord::TestCase
def test_changed_attributes_should_be_preserved_if_save_failure
pirate = Pirate.new
pirate.parrot_id = 1
- assert !pirate.save
+ assert_not pirate.save
check_pirate_after_save_failure(pirate)
pirate = Pirate.new
@@ -388,71 +376,70 @@ class DirtyTest < ActiveRecord::TestCase
end
def test_reload_should_clear_changed_attributes
- pirate = Pirate.create!(:catchphrase => "shiver me timbers")
+ pirate = Pirate.create!(catchphrase: "shiver me timbers")
pirate.catchphrase = "*hic*"
- assert pirate.changed?
+ assert_predicate pirate, :changed?
pirate.reload
- assert !pirate.changed?
+ assert_not_predicate pirate, :changed?
end
def test_dup_objects_should_not_copy_dirty_flag_from_creator
- pirate = Pirate.create!(:catchphrase => "shiver me timbers")
+ pirate = Pirate.create!(catchphrase: "shiver me timbers")
pirate_dup = pirate.dup
pirate_dup.restore_catchphrase!
pirate.catchphrase = "I love Rum"
- assert pirate.catchphrase_changed?
- assert !pirate_dup.catchphrase_changed?
+ assert_predicate pirate, :catchphrase_changed?
+ assert_not_predicate pirate_dup, :catchphrase_changed?
end
def test_reverted_changes_are_not_dirty
phrase = "shiver me timbers"
- pirate = Pirate.create!(:catchphrase => phrase)
+ pirate = Pirate.create!(catchphrase: phrase)
pirate.catchphrase = "*hic*"
- assert pirate.changed?
+ assert_predicate pirate, :changed?
pirate.catchphrase = phrase
- assert !pirate.changed?
+ assert_not_predicate pirate, :changed?
end
def test_reverted_changes_are_not_dirty_after_multiple_changes
phrase = "shiver me timbers"
- pirate = Pirate.create!(:catchphrase => phrase)
+ pirate = Pirate.create!(catchphrase: phrase)
10.times do |i|
pirate.catchphrase = "*hic*" * i
- assert pirate.changed?
+ assert_predicate pirate, :changed?
end
- assert pirate.changed?
+ assert_predicate pirate, :changed?
pirate.catchphrase = phrase
- assert !pirate.changed?
+ assert_not_predicate pirate, :changed?
end
-
def test_reverted_changes_are_not_dirty_going_from_nil_to_value_and_back
- pirate = Pirate.create!(:catchphrase => "Yar!")
+ pirate = Pirate.create!(catchphrase: "Yar!")
pirate.parrot_id = 1
- assert pirate.changed?
- assert pirate.parrot_id_changed?
- assert !pirate.catchphrase_changed?
+ assert_predicate pirate, :changed?
+ assert_predicate pirate, :parrot_id_changed?
+ assert_not_predicate pirate, :catchphrase_changed?
pirate.parrot_id = nil
- assert !pirate.changed?
- assert !pirate.parrot_id_changed?
- assert !pirate.catchphrase_changed?
+ assert_not_predicate pirate, :changed?
+ assert_not_predicate pirate, :parrot_id_changed?
+ assert_not_predicate pirate, :catchphrase_changed?
end
def test_save_should_store_serialized_attributes_even_with_partial_writes
with_partial_writes(Topic) do
- topic = Topic.create!(:content => {:a => "a"})
+ topic = Topic.create!(content: { a: "a" })
- assert_not topic.changed?
+ assert_not_predicate topic, :changed?
topic.content[:b] = "b"
- assert topic.changed?
+ assert_predicate topic, :changed?
topic.save!
- assert_not topic.changed?
+ assert_not_predicate topic, :changed?
assert_equal "b", topic.content[:b]
topic.reload
@@ -463,28 +450,37 @@ class DirtyTest < ActiveRecord::TestCase
def test_save_always_should_update_timestamps_when_serialized_attributes_are_present
with_partial_writes(Topic) do
- topic = Topic.create!(:content => {:a => "a"})
+ topic = Topic.create!(content: { a: "a" })
topic.save!
updated_at = topic.updated_at
- topic.content[:hello] = 'world'
- topic.save!
+ travel(1.second) do
+ topic.content[:hello] = "world"
+ topic.save!
+ end
assert_not_equal updated_at, topic.updated_at
- assert_equal 'world', topic.content[:hello]
+ assert_equal "world", topic.content[:hello]
end
end
def test_save_should_not_save_serialized_attribute_with_partial_writes_if_not_present
with_partial_writes(Topic) do
- Topic.create!(:author_name => 'Bill', :content => {:a => "a"})
- topic = Topic.select('id, author_name').first
- topic.update_columns author_name: 'John'
- topic = Topic.first
- assert_not_nil topic.content
+ topic = Topic.create!(author_name: "Bill", content: { a: "a" })
+ topic = Topic.select("id, author_name").find(topic.id)
+ topic.update_columns author_name: "John"
+ assert_not_nil topic.reload.content
end
end
+ def test_changes_to_save_should_not_mutate_array_of_hashes
+ topic = Topic.new(author_name: "Bill", content: [{ a: "a" }])
+
+ topic.changes_to_save
+
+ assert_equal [{ a: "a" }], topic.content
+ end
+
def test_previous_changes
# original values should be in previous_changes
pirate = Pirate.new
@@ -494,13 +490,13 @@ class DirtyTest < ActiveRecord::TestCase
pirate.save!
assert_equal 4, pirate.previous_changes.size
- assert_equal [nil, "arrr"], pirate.previous_changes['catchphrase']
- assert_equal [nil, pirate.id], pirate.previous_changes['id']
- assert_nil pirate.previous_changes['updated_on'][0]
- assert_not_nil pirate.previous_changes['updated_on'][1]
- assert_nil pirate.previous_changes['created_on'][0]
- assert_not_nil pirate.previous_changes['created_on'][1]
- assert !pirate.previous_changes.key?('parrot_id')
+ assert_equal [nil, "arrr"], pirate.previous_changes["catchphrase"]
+ assert_equal [nil, pirate.id], pirate.previous_changes["id"]
+ assert_nil pirate.previous_changes["updated_on"][0]
+ assert_not_nil pirate.previous_changes["updated_on"][1]
+ assert_nil pirate.previous_changes["created_on"][0]
+ assert_not_nil pirate.previous_changes["created_on"][1]
+ assert_not pirate.previous_changes.key?("parrot_id")
# original values should be in previous_changes
pirate = Pirate.new
@@ -510,106 +506,116 @@ class DirtyTest < ActiveRecord::TestCase
pirate.save
assert_equal 4, pirate.previous_changes.size
- assert_equal [nil, "arrr"], pirate.previous_changes['catchphrase']
- assert_equal [nil, pirate.id], pirate.previous_changes['id']
- assert pirate.previous_changes.include?('updated_on')
- assert pirate.previous_changes.include?('created_on')
- assert !pirate.previous_changes.key?('parrot_id')
+ assert_equal [nil, "arrr"], pirate.previous_changes["catchphrase"]
+ assert_equal [nil, pirate.id], pirate.previous_changes["id"]
+ assert_includes pirate.previous_changes, "updated_on"
+ assert_includes pirate.previous_changes, "created_on"
+ assert_not pirate.previous_changes.key?("parrot_id")
pirate.catchphrase = "Yar!!"
pirate.reload
assert_equal Hash.new, pirate.previous_changes
pirate = Pirate.find_by_catchphrase("arrr")
+
+ travel(1.second)
+
pirate.catchphrase = "Me Maties!"
pirate.save!
assert_equal 2, pirate.previous_changes.size
- assert_equal ["arrr", "Me Maties!"], pirate.previous_changes['catchphrase']
- assert_not_nil pirate.previous_changes['updated_on'][0]
- assert_not_nil pirate.previous_changes['updated_on'][1]
- assert !pirate.previous_changes.key?('parrot_id')
- assert !pirate.previous_changes.key?('created_on')
+ assert_equal ["arrr", "Me Maties!"], pirate.previous_changes["catchphrase"]
+ assert_not_nil pirate.previous_changes["updated_on"][0]
+ assert_not_nil pirate.previous_changes["updated_on"][1]
+ assert_not pirate.previous_changes.key?("parrot_id")
+ assert_not pirate.previous_changes.key?("created_on")
pirate = Pirate.find_by_catchphrase("Me Maties!")
+
+ travel(1.second)
+
pirate.catchphrase = "Thar She Blows!"
pirate.save
assert_equal 2, pirate.previous_changes.size
- assert_equal ["Me Maties!", "Thar She Blows!"], pirate.previous_changes['catchphrase']
- assert_not_nil pirate.previous_changes['updated_on'][0]
- assert_not_nil pirate.previous_changes['updated_on'][1]
- assert !pirate.previous_changes.key?('parrot_id')
- assert !pirate.previous_changes.key?('created_on')
+ assert_equal ["Me Maties!", "Thar She Blows!"], pirate.previous_changes["catchphrase"]
+ assert_not_nil pirate.previous_changes["updated_on"][0]
+ assert_not_nil pirate.previous_changes["updated_on"][1]
+ assert_not pirate.previous_changes.key?("parrot_id")
+ assert_not pirate.previous_changes.key?("created_on")
+
+ travel(1.second)
pirate = Pirate.find_by_catchphrase("Thar She Blows!")
pirate.update(catchphrase: "Ahoy!")
assert_equal 2, pirate.previous_changes.size
- assert_equal ["Thar She Blows!", "Ahoy!"], pirate.previous_changes['catchphrase']
- assert_not_nil pirate.previous_changes['updated_on'][0]
- assert_not_nil pirate.previous_changes['updated_on'][1]
- assert !pirate.previous_changes.key?('parrot_id')
- assert !pirate.previous_changes.key?('created_on')
+ assert_equal ["Thar She Blows!", "Ahoy!"], pirate.previous_changes["catchphrase"]
+ assert_not_nil pirate.previous_changes["updated_on"][0]
+ assert_not_nil pirate.previous_changes["updated_on"][1]
+ assert_not pirate.previous_changes.key?("parrot_id")
+ assert_not pirate.previous_changes.key?("created_on")
+
+ travel(1.second)
pirate = Pirate.find_by_catchphrase("Ahoy!")
pirate.update_attribute(:catchphrase, "Ninjas suck!")
assert_equal 2, pirate.previous_changes.size
- assert_equal ["Ahoy!", "Ninjas suck!"], pirate.previous_changes['catchphrase']
- assert_not_nil pirate.previous_changes['updated_on'][0]
- assert_not_nil pirate.previous_changes['updated_on'][1]
- assert !pirate.previous_changes.key?('parrot_id')
- assert !pirate.previous_changes.key?('created_on')
- 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
+ assert_equal ["Ahoy!", "Ninjas suck!"], pirate.previous_changes["catchphrase"]
+ assert_not_nil pirate.previous_changes["updated_on"][0]
+ assert_not_nil pirate.previous_changes["updated_on"][1]
+ assert_not pirate.previous_changes.key?("parrot_id")
+ assert_not pirate.previous_changes.key?("created_on")
+ end
+
+ 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
- in_time_zone 'Paris' do
+ skip "Fractional seconds are not supported" unless subsecond_precision_supported?
+ in_time_zone "Paris" do
target = Class.new(ActiveRecord::Base)
- target.table_name = 'topics'
+ target.table_name = "topics"
- written_on = Time.utc(2012, 12, 1, 12, 0, 0).in_time_zone('Paris')
+ written_on = Time.utc(2012, 12, 1, 12, 0, 0).in_time_zone("Paris")
- topic = target.create(:written_on => written_on)
+ topic = target.create(written_on: written_on)
topic.written_on += 0.3
- assert topic.written_on_changed?, 'Fractional second update not detected'
+ assert topic.written_on_changed?, "Fractional second update not detected"
end
end
def test_datetime_attribute_doesnt_change_if_zone_is_modified_in_string
- time_in_paris = Time.utc(2014, 1, 1, 12, 0, 0).in_time_zone('Paris')
- pirate = Pirate.create!(:catchphrase => 'rrrr', :created_on => time_in_paris)
+ time_in_paris = Time.utc(2014, 1, 1, 12, 0, 0).in_time_zone("Paris")
+ pirate = Pirate.create!(catchphrase: "rrrr", created_on: time_in_paris)
- pirate.created_on = pirate.created_on.in_time_zone('Tokyo').to_s
- assert !pirate.created_on_changed?
+ pirate.created_on = pirate.created_on.in_time_zone("Tokyo").to_s
+ assert_not_predicate pirate, :created_on_changed?
end
test "partial insert" do
with_partial_writes Person do
jon = nil
assert_sql(/first_name/i) do
- jon = Person.create! first_name: 'Jon'
+ jon = Person.create! first_name: "Jon"
end
- assert ActiveRecord::SQLCounter.log_all.none? { |sql| sql =~ /followers_count/ }
+ assert ActiveRecord::SQLCounter.log_all.none? { |sql| sql.include?("followers_count") }
jon.reload
- assert_equal 'Jon', jon.first_name
+ assert_equal "Jon", jon.first_name
assert_equal 0, jon.followers_count
assert_not_nil jon.id
end
@@ -627,7 +633,7 @@ class DirtyTest < ActiveRecord::TestCase
pirate = Pirate.create!(catchphrase: "arrrr")
pirate.catchphrase << " matey!"
- assert pirate.catchphrase_changed?
+ assert_predicate pirate, :catchphrase_changed?
expected_changes = {
"catchphrase" => ["arrrr", "arrrr matey!"]
}
@@ -635,13 +641,13 @@ class DirtyTest < ActiveRecord::TestCase
assert_equal("arrrr", pirate.catchphrase_was)
assert pirate.catchphrase_changed?(from: "arrrr")
assert_not pirate.catchphrase_changed?(from: "anything else")
- assert pirate.changed_attributes.include?(:catchphrase)
+ assert_includes pirate.changed_attributes, :catchphrase
pirate.save!
pirate.reload
assert_equal "arrrr matey!", pirate.catchphrase
- assert_not pirate.changed?
+ assert_not_predicate pirate, :changed?
end
test "in place mutation for binary" do
@@ -652,19 +658,60 @@ class DirtyTest < ActiveRecord::TestCase
binary = klass.create!(data: "\\\\foo")
- assert_not binary.changed?
+ assert_not_predicate binary, :changed?
binary.data = binary.data.dup
- assert_not binary.changed?
+ assert_not_predicate binary, :changed?
binary = klass.last
- assert_not binary.changed?
+ assert_not_predicate binary, :changed?
binary.data << "bar"
- assert binary.changed?
+ assert_predicate binary, :changed?
+ end
+
+ test "changes is correct for subclass" do
+ foo = Class.new(Pirate) do
+ def catchphrase
+ super.upcase
+ end
+ end
+
+ pirate = foo.create!(catchphrase: "arrrr")
+
+ new_catchphrase = "arrrr matey!"
+
+ pirate.catchphrase = new_catchphrase
+ assert_predicate pirate, :catchphrase_changed?
+
+ expected_changes = {
+ "catchphrase" => ["arrrr", new_catchphrase]
+ }
+
+ assert_equal new_catchphrase.upcase, pirate.catchphrase
+ assert_equal expected_changes, pirate.changes
+ end
+
+ test "changes is correct if override attribute reader" do
+ pirate = Pirate.create!(catchphrase: "arrrr")
+ def pirate.catchphrase
+ super.upcase
+ end
+
+ new_catchphrase = "arrrr matey!"
+
+ pirate.catchphrase = new_catchphrase
+ assert_predicate pirate, :catchphrase_changed?
+
+ expected_changes = {
+ "catchphrase" => ["arrrr", new_catchphrase]
+ }
+
+ assert_equal new_catchphrase.upcase, pirate.catchphrase
+ assert_equal expected_changes, pirate.changes
end
test "attribute_changed? doesn't compute in-place changes for unrelated attributes" do
@@ -674,27 +721,45 @@ class DirtyTest < ActiveRecord::TestCase
end
end
klass = Class.new(ActiveRecord::Base) do
- self.table_name = 'people'
+ self.table_name = "people"
attribute :foo, test_type_class.new
end
model = klass.new(first_name: "Jim")
- assert model.first_name_changed?
+ assert_predicate model, :first_name_changed?
end
test "attribute_will_change! doesn't try to save non-persistable attributes" do
klass = Class.new(ActiveRecord::Base) do
- self.table_name = 'people'
+ self.table_name = "people"
attribute :non_persisted_attribute, :string
end
record = klass.new(first_name: "Sean")
record.non_persisted_attribute_will_change!
- assert record.non_persisted_attribute_changed?
+ assert_predicate record, :non_persisted_attribute_changed?
assert record.save
end
+ test "virtual attributes are not written with partial_writes off" do
+ with_partial_writes(ActiveRecord::Base, false) do
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "people"
+ attribute :non_persisted_attribute, :string
+ end
+
+ record = klass.new(first_name: "Sean")
+ record.non_persisted_attribute_will_change!
+
+ assert record.save
+
+ record.non_persisted_attribute_will_change!
+
+ assert record.save
+ end
+ end
+
test "mutating and then assigning doesn't remove the change" do
pirate = Pirate.create!(catchphrase: "arrrr")
pirate.catchphrase << " matey!"
@@ -719,6 +784,126 @@ class DirtyTest < ActiveRecord::TestCase
assert_equal "arr", pirate.catchphrase
end
+ test "attributes assigned but not selected are dirty" do
+ person = Person.select(:id).first
+ assert_not_predicate person, :changed?
+
+ person.first_name = "Sean"
+ assert_predicate person, :changed?
+
+ person.first_name = nil
+ assert_predicate person, :changed?
+ end
+
+ test "attributes not selected are still missing after save" do
+ person = Person.select(:id).first
+ assert_raises(ActiveModel::MissingAttributeError) { person.first_name }
+ assert person.save # calls forget_attribute_assignments
+ assert_raises(ActiveModel::MissingAttributeError) { person.first_name }
+ end
+
+ test "saved_change_to_attribute? returns whether a change occurred in the last save" do
+ person = Person.create!(first_name: "Sean")
+
+ assert_predicate person, :saved_change_to_first_name?
+ assert_not_predicate person, :saved_change_to_gender?
+ assert person.saved_change_to_first_name?(from: nil, to: "Sean")
+ assert person.saved_change_to_first_name?(from: nil)
+ assert person.saved_change_to_first_name?(to: "Sean")
+ assert_not person.saved_change_to_first_name?(from: "Jim", to: "Sean")
+ assert_not person.saved_change_to_first_name?(from: "Jim")
+ assert_not person.saved_change_to_first_name?(to: "Jim")
+ end
+
+ test "saved_change_to_attribute returns the change that occurred in the last save" do
+ person = Person.create!(first_name: "Sean", gender: "M")
+
+ assert_equal [nil, "Sean"], person.saved_change_to_first_name
+ assert_equal [nil, "M"], person.saved_change_to_gender
+
+ person.update(first_name: "Jim")
+
+ assert_equal ["Sean", "Jim"], person.saved_change_to_first_name
+ assert_nil person.saved_change_to_gender
+ end
+
+ test "attribute_before_last_save returns the original value before saving" do
+ person = Person.create!(first_name: "Sean", gender: "M")
+
+ assert_nil person.first_name_before_last_save
+ assert_nil person.gender_before_last_save
+
+ person.first_name = "Jim"
+
+ assert_nil person.first_name_before_last_save
+ assert_nil person.gender_before_last_save
+
+ person.save
+
+ assert_equal "Sean", person.first_name_before_last_save
+ assert_equal "M", person.gender_before_last_save
+ end
+
+ test "saved_changes? returns whether the last call to save changed anything" do
+ person = Person.create!(first_name: "Sean")
+
+ assert_predicate person, :saved_changes?
+
+ person.save
+
+ assert_not_predicate person, :saved_changes?
+ end
+
+ test "saved_changes returns a hash of all the changes that occurred" do
+ person = Person.create!(first_name: "Sean", gender: "M")
+
+ assert_equal [nil, "Sean"], person.saved_changes[:first_name]
+ assert_equal [nil, "M"], person.saved_changes[:gender]
+ assert_equal %w(id first_name gender created_at updated_at).sort, person.saved_changes.keys.sort
+
+ travel(1.second) do
+ person.update(first_name: "Jim")
+ end
+
+ assert_equal ["Sean", "Jim"], person.saved_changes[:first_name]
+ assert_equal %w(first_name lock_version updated_at).sort, person.saved_changes.keys.sort
+ end
+
+ test "changed? in after callbacks returns false" do
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "people"
+
+ after_save do
+ raise "changed? should be false" if changed?
+ raise "has_changes_to_save? should be false" if has_changes_to_save?
+ raise "saved_changes? should be true" unless saved_changes?
+ raise "id_in_database should not be nil" if id_in_database.nil?
+ end
+ end
+
+ person = klass.create!(first_name: "Sean")
+ assert_not_predicate person, :changed?
+ end
+
+ test "changed? in around callbacks after yield returns false" do
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "people"
+
+ around_create :check_around
+
+ def check_around
+ yield
+ raise "changed? should be false" if changed?
+ raise "has_changes_to_save? should be false" if has_changes_to_save?
+ raise "saved_changes? should be true" unless saved_changes?
+ raise "id_in_database should not be nil" if id_in_database.nil?
+ end
+ end
+
+ person = klass.create!(first_name: "Sean")
+ assert_not_predicate person, :changed?
+ end
+
private
def with_partial_writes(klass, on = true)
old = klass.partial_writes?
@@ -729,8 +914,8 @@ class DirtyTest < ActiveRecord::TestCase
end
def check_pirate_after_save_failure(pirate)
- assert pirate.changed?
- assert pirate.parrot_id_changed?
+ assert_predicate pirate, :changed?
+ assert_predicate pirate, :parrot_id_changed?
assert_equal %w(parrot_id), pirate.changed
assert_nil pirate.parrot_id_was
end
diff --git a/activerecord/test/cases/disconnected_test.rb b/activerecord/test/cases/disconnected_test.rb
index c25089a420..533665d0f4 100644
--- a/activerecord/test/cases/disconnected_test.rb
+++ b/activerecord/test/cases/disconnected_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
class TestRecord < ActiveRecord::Base
diff --git a/activerecord/test/cases/dup_test.rb b/activerecord/test/cases/dup_test.rb
index 638cffe0e6..a2efbf89f9 100644
--- a/activerecord/test/cases/dup_test.rb
+++ b/activerecord/test/cases/dup_test.rb
@@ -1,20 +1,23 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/reply'
-require 'models/topic'
+require "models/reply"
+require "models/topic"
+require "models/movie"
module ActiveRecord
class DupTest < ActiveRecord::TestCase
fixtures :topics
def test_dup
- assert !Topic.new.freeze.dup.frozen?
+ assert_not_predicate Topic.new.freeze.dup, :frozen?
end
def test_not_readonly
topic = Topic.first
duped = topic.dup
- assert !duped.readonly?, 'should not be readonly'
+ assert_not duped.readonly?, "should not be readonly"
end
def test_is_readonly
@@ -22,15 +25,15 @@ module ActiveRecord
topic.readonly!
duped = topic.dup
- assert duped.readonly?, 'should be readonly'
+ assert duped.readonly?, "should be readonly"
end
def test_dup_not_persisted
topic = Topic.first
duped = topic.dup
- assert !duped.persisted?, 'topic not persisted'
- assert duped.new_record?, 'topic is new'
+ assert_not duped.persisted?, "topic not persisted"
+ assert duped.new_record?, "topic is new"
end
def test_dup_not_destroyed
@@ -38,7 +41,7 @@ module ActiveRecord
topic.destroy
duped = topic.dup
- assert_not duped.destroyed?
+ assert_not_predicate duped, :destroyed?
end
def test_dup_has_no_id
@@ -49,9 +52,9 @@ module ActiveRecord
def test_dup_with_modified_attributes
topic = Topic.first
- topic.author_name = 'Aaron'
+ topic.author_name = "Aaron"
duped = topic.dup
- assert_equal 'Aaron', duped.author_name
+ assert_equal "Aaron", duped.author_name
end
def test_dup_with_changes
@@ -60,10 +63,10 @@ module ActiveRecord
topic.attributes = dbtopic.attributes.except("id")
- #duped has no timestamp values
+ # duped has no timestamp values
duped = dbtopic.dup
- #clear topic timestamp values
+ # clear topic timestamp values
topic.send(:clear_timestamp_attributes)
assert_equal topic.changes, duped.changes
@@ -71,10 +74,10 @@ module ActiveRecord
def test_dup_topics_are_independent
topic = Topic.first
- topic.author_name = 'Aaron'
+ topic.author_name = "Aaron"
duped = topic.dup
- duped.author_name = 'meow'
+ duped.author_name = "meow"
assert_not_equal topic.changes, duped.changes
end
@@ -83,11 +86,11 @@ module ActiveRecord
topic = Topic.first
duped = topic.dup
- duped.author_name = 'meow'
- topic.author_name = 'Aaron'
+ duped.author_name = "meow"
+ topic.author_name = "Aaron"
- assert_equal 'Aaron', topic.author_name
- assert_equal 'meow', duped.author_name
+ assert_equal "Aaron", topic.author_name
+ assert_equal "meow", duped.author_name
end
def test_dup_timestamps_are_cleared
@@ -98,7 +101,7 @@ module ActiveRecord
# temporary change to the topic object
topic.updated_at -= 3.days
- #dup should not preserve the timestamps if present
+ # dup should not preserve the timestamps if present
new_topic = topic.dup
assert_nil new_topic.updated_at
assert_nil new_topic.created_at
@@ -124,27 +127,29 @@ module ActiveRecord
duped = topic.dup
duped.title = nil
- assert duped.invalid?
+ assert_predicate duped, :invalid?
topic.title = nil
- duped.title = 'Mathematics'
- assert topic.invalid?
- assert duped.valid?
+ duped.title = "Mathematics"
+ assert_predicate topic, :invalid?
+ assert_predicate duped, :valid?
end
end
def test_dup_with_default_scope
prev_default_scopes = Topic.default_scopes
- Topic.default_scopes = [proc { Topic.where(:approved => true) }]
- topic = Topic.new(:approved => false)
- assert !topic.dup.approved?, "should not be overridden by default scopes"
+ Topic.default_scopes = [proc { Topic.where(approved: true) }]
+ topic = Topic.new(approved: false)
+ assert_not topic.dup.approved?, "should not be overridden by default scopes"
ensure
Topic.default_scopes = prev_default_scopes
end
def test_dup_without_primary_key
+ skip if current_adapter?(:OracleAdapter)
+
klass = Class.new(ActiveRecord::Base) do
- self.table_name = 'parrots_pirates'
+ self.table_name = "parrots_pirates"
end
record = klass.create!
@@ -153,5 +158,20 @@ module ActiveRecord
record.dup
end
end
+
+ def test_dup_record_not_persisted_after_rollback_transaction
+ movie = Movie.new(name: "test")
+
+ assert_raises(ActiveRecord::RecordInvalid) do
+ Movie.transaction do
+ movie.save!
+ duped = movie.dup
+ duped.name = nil
+ duped.save!
+ end
+ end
+
+ assert_not movie.persisted?
+ end
end
end
diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb
index ba23049a92..867ce7082b 100644
--- a/activerecord/test/cases/enum_test.rb
+++ b/activerecord/test/cases/enum_test.rb
@@ -1,23 +1,27 @@
-require 'cases/helper'
-require 'models/book'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/author"
+require "models/book"
class EnumTest < ActiveRecord::TestCase
- fixtures :books
+ fixtures :books, :authors, :author_addresses
setup do
@book = books(:awdr)
end
test "query state by predicate" do
- assert @book.published?
- assert_not @book.written?
- assert_not @book.proposed?
+ assert_predicate @book, :published?
+ assert_not_predicate @book, :written?
+ assert_not_predicate @book, :proposed?
- assert @book.read?
- assert @book.in_english?
- assert @book.author_visibility_visible?
- assert @book.illustrator_visibility_visible?
- assert @book.with_medium_font_size?
+ assert_predicate @book, :read?
+ assert_predicate @book, :in_english?
+ assert_predicate @book, :author_visibility_visible?
+ assert_predicate @book, :illustrator_visibility_visible?
+ assert_predicate @book, :with_medium_font_size?
+ assert_predicate @book, :medium_to_read?
end
test "query state with strings" do
@@ -26,6 +30,7 @@ class EnumTest < ActiveRecord::TestCase
assert_equal "english", @book.language
assert_equal "visible", @book.author_visibility
assert_equal "visible", @book.illustrator_visibility
+ assert_equal "medium", @book.difficulty
end
test "find via scope" do
@@ -34,78 +39,83 @@ class EnumTest < ActiveRecord::TestCase
assert_equal @book, Book.in_english.first
assert_equal @book, Book.author_visibility_visible.first
assert_equal @book, Book.illustrator_visibility_visible.first
+ assert_equal @book, Book.medium_to_read.first
+ assert_equal books(:ddd), Book.forgotten.first
+ assert_equal books(:rfr), authors(:david).unpublished_books.first
end
test "find via where with values" do
published, written = Book.statuses[:published], Book.statuses[:written]
assert_equal @book, Book.where(status: published).first
- refute_equal @book, Book.where(status: written).first
+ assert_not_equal @book, Book.where(status: written).first
assert_equal @book, Book.where(status: [published]).first
- refute_equal @book, Book.where(status: [written]).first
- refute_equal @book, Book.where("status <> ?", published).first
+ assert_not_equal @book, Book.where(status: [written]).first
+ assert_not_equal @book, Book.where("status <> ?", published).first
assert_equal @book, Book.where("status <> ?", written).first
end
test "find via where with symbols" do
assert_equal @book, Book.where(status: :published).first
- refute_equal @book, Book.where(status: :written).first
+ assert_not_equal @book, Book.where(status: :written).first
assert_equal @book, Book.where(status: [:published]).first
- refute_equal @book, Book.where(status: [:written]).first
- refute_equal @book, Book.where.not(status: :published).first
+ assert_not_equal @book, Book.where(status: [:written]).first
+ assert_not_equal @book, Book.where.not(status: :published).first
assert_equal @book, Book.where.not(status: :written).first
+ assert_equal books(:ddd), Book.where(read_status: :forgotten).first
end
test "find via where with strings" do
assert_equal @book, Book.where(status: "published").first
- refute_equal @book, Book.where(status: "written").first
+ assert_not_equal @book, Book.where(status: "written").first
assert_equal @book, Book.where(status: ["published"]).first
- refute_equal @book, Book.where(status: ["written"]).first
- refute_equal @book, Book.where.not(status: "published").first
+ assert_not_equal @book, Book.where(status: ["written"]).first
+ assert_not_equal @book, Book.where.not(status: "published").first
assert_equal @book, Book.where.not(status: "written").first
+ assert_equal books(:ddd), Book.where(read_status: "forgotten").first
end
test "build from scope" do
- assert Book.written.build.written?
- refute Book.written.build.proposed?
+ assert_predicate Book.written.build, :written?
+ assert_not_predicate Book.written.build, :proposed?
end
test "build from where" do
- assert Book.where(status: Book.statuses[:written]).build.written?
- refute Book.where(status: Book.statuses[:written]).build.proposed?
- assert Book.where(status: :written).build.written?
- refute Book.where(status: :written).build.proposed?
- assert Book.where(status: "written").build.written?
- refute Book.where(status: "written").build.proposed?
+ assert_predicate Book.where(status: Book.statuses[:written]).build, :written?
+ assert_not_predicate Book.where(status: Book.statuses[:written]).build, :proposed?
+ assert_predicate Book.where(status: :written).build, :written?
+ assert_not_predicate Book.where(status: :written).build, :proposed?
+ assert_predicate Book.where(status: "written").build, :written?
+ assert_not_predicate Book.where(status: "written").build, :proposed?
end
test "update by declaration" do
@book.written!
- assert @book.written?
+ assert_predicate @book, :written?
@book.in_english!
- assert @book.in_english?
+ assert_predicate @book, :in_english?
@book.author_visibility_visible!
- assert @book.author_visibility_visible?
+ assert_predicate @book, :author_visibility_visible?
end
test "update by setter" do
@book.update! status: :written
- assert @book.written?
+ assert_predicate @book, :written?
end
test "enum methods are overwritable" do
assert_equal "do publish work...", @book.published!
- assert @book.published?
+ assert_predicate @book, :published?
end
test "direct assignment" do
@book.status = :written
- assert @book.written?
+ assert_predicate @book, :written?
end
test "assign string value" do
@book.status = "written"
- assert @book.written?
+ assert_predicate @book, :written?
end
test "enum changed attributes" do
@@ -122,8 +132,8 @@ class EnumTest < ActiveRecord::TestCase
old_language = @book.language
@book.status = :proposed
@book.language = :spanish
- assert_equal [old_status, 'proposed'], @book.changes[:status]
- assert_equal [old_language, 'spanish'], @book.changes[:language]
+ assert_equal [old_status, "proposed"], @book.changes[:status]
+ assert_equal [old_language, "spanish"], @book.changes[:language]
end
test "enum attribute was" do
@@ -145,8 +155,8 @@ class EnumTest < ActiveRecord::TestCase
test "enum attribute changed to" do
@book.status = :proposed
@book.language = :french
- assert @book.attribute_changed?(:status, to: 'proposed')
- assert @book.attribute_changed?(:language, to: 'french')
+ assert @book.attribute_changed?(:status, to: "proposed")
+ assert @book.attribute_changed?(:language, to: "french")
end
test "enum attribute changed from" do
@@ -163,8 +173,8 @@ class EnumTest < ActiveRecord::TestCase
old_language = @book.language
@book.status = :proposed
@book.language = :french
- assert @book.attribute_changed?(:status, from: old_status, to: 'proposed')
- assert @book.attribute_changed?(:language, from: old_language, to: 'french')
+ assert @book.attribute_changed?(:status, from: old_status, to: "proposed")
+ assert @book.attribute_changed?(:language, from: old_language, to: "french")
end
test "enum didn't change" do
@@ -216,12 +226,12 @@ class EnumTest < ActiveRecord::TestCase
end
test "assign empty string value" do
- @book.status = ''
+ @book.status = ""
assert_nil @book.status
end
test "assign long empty string value" do
- @book.status = ' '
+ @book.status = " "
assert_nil @book.status
end
@@ -232,25 +242,56 @@ class EnumTest < ActiveRecord::TestCase
end
test "building new objects with enum scopes" do
- assert Book.written.build.written?
- assert Book.read.build.read?
- assert Book.in_spanish.build.in_spanish?
- assert Book.illustrator_visibility_invisible.build.illustrator_visibility_invisible?
+ assert_predicate Book.written.build, :written?
+ assert_predicate Book.read.build, :read?
+ assert_predicate Book.in_spanish.build, :in_spanish?
+ assert_predicate Book.illustrator_visibility_invisible.build, :illustrator_visibility_invisible?
end
test "creating new objects with enum scopes" do
- assert Book.written.create.written?
- assert Book.read.create.read?
- assert Book.in_spanish.create.in_spanish?
- assert Book.illustrator_visibility_invisible.create.illustrator_visibility_invisible?
+ assert_predicate Book.written.create, :written?
+ assert_predicate Book.read.create, :read?
+ assert_predicate Book.in_spanish.create, :in_spanish?
+ assert_predicate Book.illustrator_visibility_invisible.create, :illustrator_visibility_invisible?
+ end
+
+ test "_before_type_cast" do
+ assert_equal 2, @book.status_before_type_cast
+ assert_equal "published", @book.status
+
+ @book.status = "published"
+
+ assert_equal "published", @book.status_before_type_cast
+ assert_equal "published", @book.status
end
- test "_before_type_cast returns the enum label (required for form fields)" do
- if @book.status_came_from_user?
- assert_equal "published", @book.status_before_type_cast
- else
- assert_equal "published", @book.status
+ test "invalid definition values raise an ArgumentError" do
+ e = assert_raises(ArgumentError) do
+ Class.new(ActiveRecord::Base) do
+ self.table_name = "books"
+ enum status: [proposed: 1, written: 2, published: 3]
+ end
end
+
+ assert_match(/must be either a hash, an array of symbols, or an array of strings./, e.message)
+
+ e = assert_raises(ArgumentError) do
+ Class.new(ActiveRecord::Base) do
+ self.table_name = "books"
+ enum status: { "" => 1, "active" => 2 }
+ end
+ end
+
+ assert_match(/Enum label name must not be blank/, e.message)
+
+ e = assert_raises(ArgumentError) do
+ Class.new(ActiveRecord::Base) do
+ self.table_name = "books"
+ enum status: ["active", ""]
+ end
+ end
+
+ assert_match(/Enum label name must not be blank/, e.message)
end
test "reserved enum names" do
@@ -296,6 +337,24 @@ class EnumTest < ActiveRecord::TestCase
end
end
+ test "reserved enum values for relation" do
+ relation_method_samples = [
+ :records,
+ :to_ary,
+ :scope_for_create
+ ]
+
+ relation_method_samples.each do |value|
+ e = assert_raises(ArgumentError, "enum value `#{value}` should not be allowed") do
+ Class.new(ActiveRecord::Base) do
+ self.table_name = "books"
+ enum category: [:other, value]
+ end
+ end
+ assert_match(/You tried to define an enum named .* on the model/, e.message)
+ end
+ end
+
test "overriding enum method should not raise" do
assert_nothing_raised do
Class.new(ActiveRecord::Base) do
@@ -318,29 +377,29 @@ class EnumTest < ActiveRecord::TestCase
test "validate uniqueness" do
klass = Class.new(ActiveRecord::Base) do
- def self.name; 'Book'; end
+ def self.name; "Book"; end
enum status: [:proposed, :written]
validates_uniqueness_of :status
end
klass.delete_all
klass.create!(status: "proposed")
book = klass.new(status: "written")
- assert book.valid?
+ assert_predicate book, :valid?
book.status = "proposed"
- assert_not book.valid?
+ assert_not_predicate book, :valid?
end
test "validate inclusion of value in array" do
klass = Class.new(ActiveRecord::Base) do
- def self.name; 'Book'; end
+ def self.name; "Book"; end
enum status: [:proposed, :written]
validates_inclusion_of :status, in: ["written"]
end
klass.delete_all
invalid_book = klass.new(status: "proposed")
- assert_not invalid_book.valid?
+ assert_not_predicate invalid_book, :valid?
valid_book = klass.new(status: "written")
- assert valid_book.valid?
+ assert_predicate valid_book, :valid?
end
test "enums are distinct per class" do
@@ -356,11 +415,11 @@ class EnumTest < ActiveRecord::TestCase
book1 = klass1.proposed.create!
book1.status = :written
- assert_equal ['proposed', 'written'], book1.status_change
+ assert_equal ["proposed", "written"], book1.status_change
book2 = klass2.drafted.create!
book2.status = :uploaded
- assert_equal ['drafted', 'uploaded'], book2.status_change
+ assert_equal ["drafted", "uploaded"], book2.status_change
end
test "enums are inheritable" do
@@ -372,11 +431,11 @@ class EnumTest < ActiveRecord::TestCase
book1 = subklass1.proposed.create!
book1.status = :written
- assert_equal ['proposed', 'written'], book1.status_change
+ assert_equal ["proposed", "written"], book1.status_change
book2 = subklass2.drafted.create!
book2.status = :uploaded
- assert_equal ['drafted', 'uploaded'], book2.status_change
+ assert_equal ["drafted", "uploaded"], book2.status_change
end
test "declare multiple enums at a time" do
@@ -387,23 +446,76 @@ class EnumTest < ActiveRecord::TestCase
end
book1 = klass.proposed.create!
- assert book1.proposed?
+ assert_predicate book1, :proposed?
book2 = klass.single.create!
- assert book2.single?
+ assert_predicate book2, :single?
+ end
+
+ test "enum with alias_attribute" do
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "books"
+ alias_attribute :aliased_status, :status
+ enum aliased_status: [:proposed, :written, :published]
+ end
+
+ book = klass.proposed.create!
+ assert_predicate book, :proposed?
+ assert_equal "proposed", book.aliased_status
+
+ book = klass.find(book.id)
+ assert_predicate book, :proposed?
+ assert_equal "proposed", book.aliased_status
end
test "query state by predicate with prefix" do
- assert @book.author_visibility_visible?
- assert_not @book.author_visibility_invisible?
- assert @book.illustrator_visibility_visible?
- assert_not @book.illustrator_visibility_invisible?
+ assert_predicate @book, :author_visibility_visible?
+ assert_not_predicate @book, :author_visibility_invisible?
+ assert_predicate @book, :illustrator_visibility_visible?
+ assert_not_predicate @book, :illustrator_visibility_invisible?
end
test "query state by predicate with custom prefix" do
- assert @book.in_english?
- assert_not @book.in_spanish?
- assert_not @book.in_french?
+ assert_predicate @book, :in_english?
+ assert_not_predicate @book, :in_spanish?
+ assert_not_predicate @book, :in_french?
+ end
+
+ test "query state by predicate with custom suffix" do
+ assert_predicate @book, :medium_to_read?
+ assert_not_predicate @book, :easy_to_read?
+ assert_not_predicate @book, :hard_to_read?
+ end
+
+ test "enum methods with custom suffix defined" do
+ assert_respond_to @book.class, :easy_to_read
+ assert_respond_to @book.class, :medium_to_read
+ assert_respond_to @book.class, :hard_to_read
+
+ assert_respond_to @book, :easy_to_read?
+ assert_respond_to @book, :medium_to_read?
+ assert_respond_to @book, :hard_to_read?
+
+ assert_respond_to @book, :easy_to_read!
+ assert_respond_to @book, :medium_to_read!
+ assert_respond_to @book, :hard_to_read!
+ end
+
+ test "update enum attributes with custom suffix" do
+ @book.medium_to_read!
+ assert_not_predicate @book, :easy_to_read?
+ assert_predicate @book, :medium_to_read?
+ assert_not_predicate @book, :hard_to_read?
+
+ @book.easy_to_read!
+ assert_predicate @book, :easy_to_read?
+ assert_not_predicate @book, :medium_to_read?
+ assert_not_predicate @book, :hard_to_read?
+
+ @book.hard_to_read!
+ assert_not_predicate @book, :easy_to_read?
+ assert_not_predicate @book, :medium_to_read?
+ assert_predicate @book, :hard_to_read?
end
test "uses default status when no status is provided in fixtures" do
@@ -411,4 +523,18 @@ class EnumTest < ActiveRecord::TestCase
assert book.proposed?, "expected fixture to default to proposed status"
assert book.in_english?, "expected fixture to default to english language"
end
+
+ test "uses default value from database on initialization" do
+ book = Book.new
+ assert_predicate book, :proposed?
+ end
+
+ test "uses default value from database on initialization when using custom mapping" do
+ book = Book.new
+ assert_predicate book, :hard?
+ end
+
+ test "data type of Enum type" do
+ assert_equal :integer, Book.type_for_attribute("status").type
+ end
end
diff --git a/activerecord/test/cases/errors_test.rb b/activerecord/test/cases/errors_test.rb
new file mode 100644
index 0000000000..b90e6a66c5
--- /dev/null
+++ b/activerecord/test/cases/errors_test.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+
+class ErrorsTest < ActiveRecord::TestCase
+ def test_can_be_instantiated_with_no_args
+ base = ActiveRecord::ActiveRecordError
+ error_klasses = ObjectSpace.each_object(Class).select { |klass| klass < base }
+
+ (error_klasses - [ActiveRecord::AmbiguousSourceReflectionForThroughAssociation]).each do |error_klass|
+ begin
+ error_klass.new.inspect
+ rescue ArgumentError
+ raise "Instance of #{error_klass} can't be initialized with no arguments"
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/explain_subscriber_test.rb b/activerecord/test/cases/explain_subscriber_test.rb
index 2dee8a26a5..79a0630193 100644
--- a/activerecord/test/cases/explain_subscriber_test.rb
+++ b/activerecord/test/cases/explain_subscriber_test.rb
@@ -1,6 +1,8 @@
-require 'cases/helper'
-require 'active_record/explain_subscriber'
-require 'active_record/explain_registry'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "active_record/explain_subscriber"
+require "active_record/explain_registry"
if ActiveRecord::Base.connection.supports_explain?
class ExplainSubscriberTest < ActiveRecord::TestCase
@@ -13,43 +15,43 @@ if ActiveRecord::Base.connection.supports_explain?
def test_collects_nothing_if_the_payload_has_an_exception
SUBSCRIBER.finish(nil, nil, exception: Exception.new)
- assert queries.empty?
+ assert_empty queries
end
def test_collects_nothing_for_ignored_payloads
ActiveRecord::ExplainSubscriber::IGNORED_PAYLOADS.each do |ip|
SUBSCRIBER.finish(nil, nil, name: ip)
end
- assert queries.empty?
+ assert_empty queries
end
def test_collects_nothing_if_collect_is_false
ActiveRecord::ExplainRegistry.collect = false
- SUBSCRIBER.finish(nil, nil, name: 'SQL', sql: 'select 1 from users', binds: [1, 2])
- assert queries.empty?
+ SUBSCRIBER.finish(nil, nil, name: "SQL", sql: "select 1 from users", binds: [1, 2])
+ assert_empty queries
end
def test_collects_pairs_of_queries_and_binds
- sql = 'select 1 from users'
+ sql = "select 1 from users"
binds = [1, 2]
- SUBSCRIBER.finish(nil, nil, name: 'SQL', sql: sql, binds: binds)
+ SUBSCRIBER.finish(nil, nil, name: "SQL", sql: sql, binds: binds)
assert_equal 1, queries.size
assert_equal sql, queries[0][0]
assert_equal binds, queries[0][1]
end
- def test_collects_nothing_if_the_statement_is_not_whitelisted
- SUBSCRIBER.finish(nil, nil, name: 'SQL', sql: 'SHOW max_identifier_length')
- assert queries.empty?
+ def test_collects_nothing_if_the_statement_is_not_explainable
+ SUBSCRIBER.finish(nil, nil, name: "SQL", sql: "SHOW max_identifier_length")
+ assert_empty queries
end
def test_collects_nothing_if_the_statement_is_only_partially_matched
- SUBSCRIBER.finish(nil, nil, name: 'SQL', sql: 'select_db yo_mama')
- assert queries.empty?
+ SUBSCRIBER.finish(nil, nil, name: "SQL", sql: "select_db yo_mama")
+ assert_empty queries
end
def test_collects_cte_queries
- SUBSCRIBER.finish(nil, nil, name: 'SQL', sql: 'with s as (values(3)) select 1 from s')
+ SUBSCRIBER.finish(nil, nil, name: "SQL", sql: "with s as (values(3)) select 1 from s")
assert_equal 1, queries.size
end
diff --git a/activerecord/test/cases/explain_test.rb b/activerecord/test/cases/explain_test.rb
index f1d5511bb8..a0e75f4e89 100644
--- a/activerecord/test/cases/explain_test.rb
+++ b/activerecord/test/cases/explain_test.rb
@@ -1,6 +1,7 @@
-require 'cases/helper'
-require 'models/car'
-require 'active_support/core_ext/string/strip'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/car"
if ActiveRecord::Base.connection.supports_explain?
class ExplainTest < ActiveRecord::TestCase
@@ -15,13 +16,13 @@ if ActiveRecord::Base.connection.supports_explain?
end
def test_relation_explain
- message = Car.where(:name => 'honda').explain
+ message = Car.where(name: "honda").explain
assert_match(/^EXPLAIN for:/, message)
end
def test_collecting_queries_for_explain
queries = ActiveRecord::Base.collecting_queries_for_explain do
- Car.where(:name => 'honda').to_a
+ Car.where(name: "honda").to_a
end
sql, binds = queries[0]
@@ -30,7 +31,7 @@ if ActiveRecord::Base.connection.supports_explain?
assert_equal 1, binds.length
assert_equal "honda", binds.last.value
else
- assert_match 'honda', sql
+ assert_match "honda", sql
end
end
@@ -39,38 +40,49 @@ if ActiveRecord::Base.connection.supports_explain?
binds = [[], []]
queries = sqls.zip(binds)
- connection.stubs(:explain).returns('query plan foo', 'query plan bar')
- expected = sqls.map {|sql| "EXPLAIN for: #{sql}\nquery plan #{sql}"}.join("\n")
- assert_equal expected, base.exec_explain(queries)
+ stub_explain_for_query_plans do
+ expected = sqls.map { |sql| "EXPLAIN for: #{sql}\nquery plan #{sql}" }.join("\n")
+ assert_equal expected, base.exec_explain(queries)
+ end
end
def test_exec_explain_with_binds
- cols = [Object.new, Object.new]
- cols[0].expects(:name).returns('wadus')
- cols[1].expects(:name).returns('chaflan')
-
sqls = %w(foo bar)
- binds = [[[cols[0], 1]], [[cols[1], 2]]]
+ binds = [[bind_param("wadus", 1)], [bind_param("chaflan", 2)]]
queries = sqls.zip(binds)
- connection.stubs(:explain).returns("query plan foo\n", "query plan bar\n")
- expected = <<-SQL.strip_heredoc
- EXPLAIN for: #{sqls[0]} [["wadus", 1]]
- query plan foo
+ stub_explain_for_query_plans(["query plan foo\n", "query plan bar\n"]) do
+ expected = <<~SQL
+ EXPLAIN for: #{sqls[0]} [["wadus", 1]]
+ query plan foo
- EXPLAIN for: #{sqls[1]} [["chaflan", 2]]
- query plan bar
- SQL
- assert_equal expected, base.exec_explain(queries)
+ EXPLAIN for: #{sqls[1]} [["chaflan", 2]]
+ query plan bar
+ SQL
+ assert_equal expected, base.exec_explain(queries)
+ end
end
def test_unsupported_connection_adapter
- connection.stubs(:supports_explain?).returns(false)
+ connection.stub(:supports_explain?, false) do
+ assert_not_called(base.logger, :warn) do
+ Car.where(name: "honda").to_a
+ end
+ end
+ end
- base.logger.expects(:warn).never
+ private
- Car.where(:name => 'honda').to_a
- end
+ def stub_explain_for_query_plans(query_plans = ["query plan foo", "query plan bar"])
+ explain_called = 0
+ connection.stub(:explain, proc { explain_called += 1; query_plans[explain_called - 1] }) do
+ yield
+ end
+ end
+
+ def bind_param(name, value)
+ ActiveRecord::Relation::QueryAttribute.new(name, value, ActiveRecord::Type::Value.new)
+ end
end
end
diff --git a/activerecord/test/cases/filter_attributes_test.rb b/activerecord/test/cases/filter_attributes_test.rb
new file mode 100644
index 0000000000..47161a547a
--- /dev/null
+++ b/activerecord/test/cases/filter_attributes_test.rb
@@ -0,0 +1,136 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/admin"
+require "models/admin/user"
+require "models/admin/account"
+require "models/user"
+require "pp"
+
+class FilterAttributesTest < ActiveRecord::TestCase
+ fixtures :"admin/users", :"admin/accounts"
+
+ setup do
+ @previous_filter_attributes = ActiveRecord::Base.filter_attributes
+ ActiveRecord::Base.filter_attributes = [:name]
+ end
+
+ teardown do
+ ActiveRecord::Base.filter_attributes = @previous_filter_attributes
+ end
+
+ test "filter_attributes" do
+ Admin::User.all.each do |user|
+ assert_includes user.inspect, "name: [FILTERED]"
+ assert_equal 1, user.inspect.scan("[FILTERED]").length
+ end
+
+ Admin::Account.all.each do |account|
+ assert_includes account.inspect, "name: [FILTERED]"
+ assert_equal 1, account.inspect.scan("[FILTERED]").length
+ end
+ end
+
+ test "string filter_attributes perform pertial match" do
+ ActiveRecord::Base.filter_attributes = ["n"]
+ Admin::Account.all.each do |account|
+ assert_includes account.inspect, "name: [FILTERED]"
+ assert_equal 1, account.inspect.scan("[FILTERED]").length
+ end
+ end
+
+ test "regex filter_attributes are accepted" do
+ ActiveRecord::Base.filter_attributes = [/\An\z/]
+ account = Admin::Account.find_by(name: "37signals")
+ assert_includes account.inspect, 'name: "37signals"'
+ assert_equal 0, account.inspect.scan("[FILTERED]").length
+
+ ActiveRecord::Base.filter_attributes = [/\An/]
+ account = Admin::Account.find_by(name: "37signals")
+ assert_includes account.reload.inspect, "name: [FILTERED]"
+ assert_equal 1, account.inspect.scan("[FILTERED]").length
+ end
+
+ test "proc filter_attributes are accepted" do
+ ActiveRecord::Base.filter_attributes = [ lambda { |key, value| value.reverse! if key == "name" } ]
+ account = Admin::Account.find_by(name: "37signals")
+ assert_includes account.inspect, 'name: "slangis73"'
+ end
+
+ test "filter_attributes could be overwritten by models" do
+ Admin::Account.all.each do |account|
+ assert_includes account.inspect, "name: [FILTERED]"
+ assert_equal 1, account.inspect.scan("[FILTERED]").length
+ end
+
+ begin
+ Admin::Account.filter_attributes = []
+
+ # Above changes should not impact other models
+ Admin::User.all.each do |user|
+ assert_includes user.inspect, "name: [FILTERED]"
+ assert_equal 1, user.inspect.scan("[FILTERED]").length
+ end
+
+ Admin::Account.all.each do |account|
+ assert_not_includes account.inspect, "name: [FILTERED]"
+ assert_equal 0, account.inspect.scan("[FILTERED]").length
+ end
+ ensure
+ Admin::Account.remove_instance_variable(:@filter_attributes)
+ end
+ end
+
+ test "filter_attributes should not filter nil value" do
+ account = Admin::Account.new
+
+ assert_includes account.inspect, "name: nil"
+ assert_not_includes account.inspect, "name: [FILTERED]"
+ assert_equal 0, account.inspect.scan("[FILTERED]").length
+ end
+
+ test "filter_attributes should handle [FILTERED] value properly" do
+ begin
+ User.filter_attributes = ["auth"]
+ user = User.new(token: "[FILTERED]", auth_token: "[FILTERED]")
+
+ assert_includes user.inspect, "auth_token: [FILTERED]"
+ assert_includes user.inspect, 'token: "[FILTERED]"'
+ ensure
+ User.remove_instance_variable(:@filter_attributes)
+ end
+ end
+
+ test "filter_attributes on pretty_print" do
+ user = admin_users(:david)
+ actual = "".dup
+ PP.pp(user, StringIO.new(actual))
+
+ assert_includes actual, "name: [FILTERED]"
+ assert_equal 1, actual.scan("[FILTERED]").length
+ end
+
+ test "filter_attributes on pretty_print should not filter nil value" do
+ user = Admin::User.new
+ actual = "".dup
+ PP.pp(user, StringIO.new(actual))
+
+ assert_includes actual, "name: nil"
+ assert_not_includes actual, "name: [FILTERED]"
+ assert_equal 0, actual.scan("[FILTERED]").length
+ end
+
+ test "filter_attributes on pretty_print should handle [FILTERED] value properly" do
+ begin
+ User.filter_attributes = ["auth"]
+ user = User.new(token: "[FILTERED]", auth_token: "[FILTERED]")
+ actual = "".dup
+ PP.pp(user, StringIO.new(actual))
+
+ assert_includes actual, "auth_token: [FILTERED]"
+ assert_includes actual, 'token: "[FILTERED]"'
+ ensure
+ User.remove_instance_variable(:@filter_attributes)
+ end
+ end
+end
diff --git a/activerecord/test/cases/finder_respond_to_test.rb b/activerecord/test/cases/finder_respond_to_test.rb
index 6ab2657c44..e0acd30c22 100644
--- a/activerecord/test/cases/finder_respond_to_test.rb
+++ b/activerecord/test/cases/finder_respond_to_test.rb
@@ -1,13 +1,14 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/topic'
+require "models/topic"
class FinderRespondToTest < ActiveRecord::TestCase
-
fixtures :topics
def test_should_preserve_normal_respond_to_behaviour_on_base
assert_respond_to ActiveRecord::Base, :new
- assert !ActiveRecord::Base.respond_to?(:find_by_something)
+ assert_not_respond_to ActiveRecord::Base, :find_by_something
end
def test_should_preserve_normal_respond_to_behaviour_and_respond_to_newly_added_method
@@ -42,19 +43,19 @@ class FinderRespondToTest < ActiveRecord::TestCase
end
def test_should_not_respond_to_find_by_one_missing_attribute
- assert !Topic.respond_to?(:find_by_undertitle)
+ assert_not_respond_to Topic, :find_by_undertitle
end
def test_should_not_respond_to_find_by_invalid_method_syntax
- assert !Topic.respond_to?(:fail_to_find_by_title)
- assert !Topic.respond_to?(:find_by_title?)
- assert !Topic.respond_to?(:fail_to_find_or_create_by_title)
- assert !Topic.respond_to?(:find_or_create_by_title?)
+ assert_not_respond_to Topic, :fail_to_find_by_title
+ assert_not_respond_to Topic, :find_by_title?
+ assert_not_respond_to Topic, :fail_to_find_or_create_by_title
+ assert_not_respond_to Topic, :find_or_create_by_title?
end
private
- def ensure_topic_method_is_not_cached(method_id)
- class << Topic; self; end.send(:remove_method, method_id) if Topic.public_methods.include? method_id
- end
+ def ensure_topic_method_is_not_cached(method_id)
+ class << Topic; self; end.send(:remove_method, method_id) if Topic.public_methods.include? method_id
+ end
end
diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb
index 4b819a82e8..52fd9291b2 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -1,35 +1,39 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/post'
-require 'models/author'
-require 'models/categorization'
-require 'models/comment'
-require 'models/company'
-require 'models/tagging'
-require 'models/topic'
-require 'models/reply'
-require 'models/entrant'
-require 'models/project'
-require 'models/developer'
-require 'models/computer'
-require 'models/customer'
-require 'models/toy'
-require 'models/matey'
-require 'models/dog'
-require 'models/car'
-require 'models/tyre'
+require "models/post"
+require "models/author"
+require "models/categorization"
+require "models/comment"
+require "models/company"
+require "models/tagging"
+require "models/topic"
+require "models/reply"
+require "models/rating"
+require "models/entrant"
+require "models/project"
+require "models/developer"
+require "models/computer"
+require "models/customer"
+require "models/toy"
+require "models/matey"
+require "models/dog"
+require "models/car"
+require "models/tyre"
+require "models/subscriber"
class FinderTest < ActiveRecord::TestCase
- fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :customers, :categories, :categorizations, :cars
+ fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :author_addresses, :customers, :categories, :categorizations, :cars
def test_find_by_id_with_hash
- assert_raises(ActiveRecord::StatementInvalid) do
- Post.find_by_id(:limit => 1)
+ assert_nothing_raised do
+ Post.find_by_id(limit: 1)
end
end
def test_find_by_title_and_id_with_hash
- assert_raises(ActiveRecord::StatementInvalid) do
- Post.find_by_title_and_id('foo', :limit => 1)
+ assert_nothing_raised do
+ Post.find_by_title_and_id("foo", limit: 1)
end
end
@@ -43,13 +47,97 @@ class FinderTest < ActiveRecord::TestCase
end
assert_equal "should happen", exception.message
- assert_nothing_raised(RuntimeError) do
+ assert_nothing_raised do
Topic.all.find(-> { raise "should not happen" }) { |e| e.title == topics(:first).title }
end
end
- def test_find_passing_active_record_object_is_deprecated
- assert_deprecated do
+ def test_find_with_ids_returning_ordered
+ records = Topic.find([4, 2, 5])
+ assert_equal "The Fourth Topic of the day", records[0].title
+ assert_equal "The Second Topic of the day", records[1].title
+ assert_equal "The Fifth Topic of the day", records[2].title
+
+ records = Topic.find(4, 2, 5)
+ assert_equal "The Fourth Topic of the day", records[0].title
+ assert_equal "The Second Topic of the day", records[1].title
+ assert_equal "The Fifth Topic of the day", records[2].title
+
+ records = Topic.find(["4", "2", "5"])
+ assert_equal "The Fourth Topic of the day", records[0].title
+ assert_equal "The Second Topic of the day", records[1].title
+ assert_equal "The Fifth Topic of the day", records[2].title
+
+ records = Topic.find("4", "2", "5")
+ assert_equal "The Fourth Topic of the day", records[0].title
+ assert_equal "The Second Topic of the day", records[1].title
+ assert_equal "The Fifth Topic of the day", records[2].title
+ end
+
+ def test_find_with_ids_and_order_clause
+ # The order clause takes precedence over the informed ids
+ records = Topic.order(:author_name).find([5, 3, 1])
+ assert_equal "The Third Topic of the day", records[0].title
+ assert_equal "The First Topic", records[1].title
+ assert_equal "The Fifth Topic of the day", records[2].title
+
+ records = Topic.order(:id).find([5, 3, 1])
+ assert_equal "The First Topic", records[0].title
+ assert_equal "The Third Topic of the day", records[1].title
+ assert_equal "The Fifth Topic of the day", records[2].title
+ end
+
+ def test_find_with_ids_with_limit_and_order_clause
+ # The order clause takes precedence over the informed ids
+ records = Topic.limit(2).order(:id).find([5, 3, 1])
+ assert_equal 2, records.size
+ assert_equal "The First Topic", records[0].title
+ assert_equal "The Third Topic of the day", records[1].title
+ end
+
+ def test_find_with_ids_and_limit
+ records = Topic.limit(3).find([3, 2, 5, 1, 4])
+ assert_equal 3, records.size
+ assert_equal "The Third Topic of the day", records[0].title
+ assert_equal "The Second Topic of the day", records[1].title
+ assert_equal "The Fifth Topic of the day", records[2].title
+ end
+
+ def test_find_with_ids_where_and_limit
+ # Please note that Topic 1 is the only not approved so
+ # if it were among the first 3 it would raise an ActiveRecord::RecordNotFound
+ records = Topic.where(approved: true).limit(3).find([3, 2, 5, 1, 4])
+ assert_equal 3, records.size
+ assert_equal "The Third Topic of the day", records[0].title
+ assert_equal "The Second Topic of the day", records[1].title
+ assert_equal "The Fifth Topic of the day", records[2].title
+ end
+
+ def test_find_with_ids_and_offset
+ records = Topic.offset(2).find([3, 2, 5, 1, 4])
+ assert_equal 3, records.size
+ assert_equal "The Fifth Topic of the day", records[0].title
+ assert_equal "The First Topic", records[1].title
+ assert_equal "The Fourth Topic of the day", records[2].title
+ end
+
+ def test_find_with_ids_with_no_id_passed
+ exception = assert_raises(ActiveRecord::RecordNotFound) { Topic.find }
+ assert_equal exception.model, "Topic"
+ assert_equal exception.primary_key, "id"
+ end
+
+ def test_find_with_ids_with_id_out_of_range
+ exception = assert_raises(ActiveRecord::RecordNotFound) do
+ Topic.find("9999999999999999999999999999999")
+ end
+
+ assert_equal exception.model, "Topic"
+ assert_equal exception.primary_key, "id"
+ end
+
+ def test_find_passing_active_record_object_is_not_permitted
+ assert_raises(ArgumentError) do
Topic.find(Topic.last)
end
end
@@ -58,7 +146,7 @@ class FinderTest < ActiveRecord::TestCase
gc_disabled = GC.disable
Post.where("author_id" => nil) # warm up
x = Symbol.all_symbols.count
- Post.where("title" => {"xxxqqqq" => "bar"})
+ Post.where("title" => { "xxxqqqq" => "bar" })
assert_equal x, Symbol.all_symbols.count
ensure
GC.enable if gc_disabled == false
@@ -67,7 +155,7 @@ class FinderTest < ActiveRecord::TestCase
# find should handle strings that come from URLs
# (example: Category.find(params[:id]))
def test_find_with_string
- assert_equal(Topic.find(1).title,Topic.find("1").title)
+ assert_equal(Topic.find(1).title, Topic.find("1").title)
end
def test_exists
@@ -75,22 +163,49 @@ class FinderTest < ActiveRecord::TestCase
assert_equal true, Topic.exists?("1")
assert_equal true, Topic.exists?(title: "The First Topic")
assert_equal true, Topic.exists?(heading: "The First Topic")
- assert_equal true, Topic.exists?(:author_name => "Mary", :approved => true)
+ assert_equal true, Topic.exists?(author_name: "Mary", approved: true)
assert_equal true, Topic.exists?(["parent_id = ?", 1])
assert_equal true, Topic.exists?(id: [1, 9999])
assert_equal false, Topic.exists?(45)
+ assert_equal false, Topic.exists?(9999999999999999999999999999999)
assert_equal false, Topic.exists?(Topic.new.id)
- assert_raise(NoMethodError) { Topic.exists?([1,2]) }
+ assert_raise(NoMethodError) { Topic.exists?([1, 2]) }
+ end
+
+ def test_exists_with_scope
+ davids = Author.where(name: "David")
+ assert_equal true, davids.exists?
+ assert_equal true, davids.exists?(authors(:david).id)
+ assert_equal false, davids.exists?(authors(:mary).id)
+ assert_equal false, davids.exists?("42")
+ assert_equal false, davids.exists?(42)
+ assert_equal false, davids.exists?(davids.new.id)
+
+ fake = Author.where(name: "fake author")
+ assert_equal false, fake.exists?
+ assert_equal false, fake.exists?(authors(:david).id)
+ end
+
+ def test_exists_uses_existing_scope
+ post = authors(:david).posts.first
+ authors = Author.includes(:posts).where(name: "David", posts: { id: post.id })
+ assert_equal true, authors.exists?(authors(:david).id)
+ end
+
+ def test_any_with_scope_on_hash_includes
+ post = authors(:david).posts.first
+ categories = Categorization.includes(author: :posts).where(posts: { id: post.id })
+ assert_equal true, categories.exists?
end
def test_exists_with_polymorphic_relation
- post = Post.create!(title: 'Post', body: 'default', taggings: [Tagging.new(comment: 'tagging comment')])
- relation = Post.tagged_with_comment('tagging comment')
+ post = Post.create!(title: "Post", body: "default", taggings: [Tagging.new(comment: "tagging comment")])
+ relation = Post.tagged_with_comment("tagging comment")
- assert_equal true, relation.exists?(title: ['Post'])
- assert_equal true, relation.exists?(['title LIKE ?', 'Post%'])
+ assert_equal true, relation.exists?(title: ["Post"])
+ assert_equal true, relation.exists?(["title LIKE ?", "Post%"])
assert_equal true, relation.exists?
assert_equal true, relation.exists?(post.id)
assert_equal true, relation.exists?(post.id.to_s)
@@ -98,17 +213,21 @@ class FinderTest < ActiveRecord::TestCase
assert_equal false, relation.exists?(false)
end
- def test_exists_passing_active_record_object_is_deprecated
- assert_deprecated do
- Topic.exists?(Topic.new)
- end
+ def test_exists_with_string
+ assert_equal false, Subscriber.exists?("foo")
+ assert_equal false, Subscriber.exists?(" ")
+
+ Subscriber.create!(id: "foo")
+ Subscriber.create!(id: " ")
+
+ assert_equal true, Subscriber.exists?("foo")
+ assert_equal true, Subscriber.exists?(" ")
end
- def test_exists_fails_when_parameter_has_invalid_type
- assert_raises(RangeError) do
- assert_equal false, Topic.exists?(("9"*53).to_i) # number that's bigger than int
+ def test_exists_passing_active_record_object_is_not_permitted
+ assert_raises(ArgumentError) do
+ Topic.exists?(Topic.new)
end
- assert_equal false, Topic.exists?("foo")
end
def test_exists_does_not_select_columns_without_alias
@@ -135,26 +254,55 @@ class FinderTest < ActiveRecord::TestCase
assert_equal true, Topic.first.replies.exists?
end
- # ensures +exists?+ runs valid SQL by excluding order value
- def test_exists_with_order
+ def test_exists_with_empty_hash_arg
+ assert_equal true, Topic.exists?({})
+ end
+
+ # Ensure +exists?+ runs without an error by excluding distinct value.
+ # See https://github.com/rails/rails/pull/26981.
+ def test_exists_with_order_and_distinct
assert_equal true, Topic.order(:id).distinct.exists?
end
+ # Ensure +exists?+ runs without an error by excluding order value.
+ def test_exists_with_order
+ assert_equal true, Topic.order(Arel.sql("invalid sql here")).exists?
+ end
+
+ def test_exists_with_joins
+ assert_equal true, Topic.joins(:replies).where(replies_topics: { approved: true }).order("replies_topics.created_at DESC").exists?
+ end
+
+ def test_exists_with_left_joins
+ assert_equal true, Topic.left_joins(:replies).where(replies_topics: { approved: true }).order("replies_topics.created_at DESC").exists?
+ end
+
+ def test_exists_with_eager_load
+ assert_equal true, Topic.eager_load(:replies).where(replies_topics: { approved: true }).order("replies_topics.created_at DESC").exists?
+ end
+
def test_exists_with_includes_limit_and_empty_result
- assert_equal false, Topic.includes(:replies).limit(0).exists?
- assert_equal false, Topic.includes(:replies).limit(1).where('0 = 1').exists?
+ assert_no_queries { assert_equal false, Topic.includes(:replies).limit(0).exists? }
+ assert_queries(1) { assert_equal false, Topic.includes(:replies).limit(1).where("0 = 1").exists? }
end
def test_exists_with_distinct_association_includes_and_limit
author = Author.first
- assert_equal false, author.unique_categorized_posts.includes(:special_comments).limit(0).exists?
- assert_equal true, author.unique_categorized_posts.includes(:special_comments).limit(1).exists?
+ unique_categorized_posts = author.unique_categorized_posts.includes(:special_comments)
+ assert_no_queries { assert_equal false, unique_categorized_posts.limit(0).exists? }
+ assert_queries(1) { assert_equal true, unique_categorized_posts.limit(1).exists? }
end
def test_exists_with_distinct_association_includes_limit_and_order
author = Author.first
- assert_equal false, author.unique_categorized_posts.includes(:special_comments).order('comments.tags_count DESC').limit(0).exists?
- assert_equal true, author.unique_categorized_posts.includes(:special_comments).order('comments.tags_count DESC').limit(1).exists?
+ unique_categorized_posts = author.unique_categorized_posts.includes(:special_comments).order("comments.tags_count DESC")
+ assert_no_queries { assert_equal false, unique_categorized_posts.limit(0).exists? }
+ assert_queries(1) { assert_equal true, unique_categorized_posts.limit(1).exists? }
+ end
+
+ def test_exists_should_reference_correct_aliases_while_joining_tables_of_has_many_through_association
+ ratings = developers(:david).ratings.includes(comment: :post).where(posts: { id: 1 })
+ assert_queries(1) { assert_not_predicate ratings.limit(1), :exists? }
end
def test_exists_with_empty_table_and_no_args_given
@@ -164,22 +312,20 @@ class FinderTest < ActiveRecord::TestCase
def test_exists_with_aggregate_having_three_mappings
existing_address = customers(:david).address
- assert_equal true, Customer.exists?(:address => existing_address)
+ assert_equal true, Customer.exists?(address: existing_address)
end
def test_exists_with_aggregate_having_three_mappings_with_one_difference
existing_address = customers(:david).address
- assert_equal false, Customer.exists?(:address =>
- Address.new(existing_address.street, existing_address.city, existing_address.country + "1"))
- assert_equal false, Customer.exists?(:address =>
- Address.new(existing_address.street, existing_address.city + "1", existing_address.country))
- assert_equal false, Customer.exists?(:address =>
- Address.new(existing_address.street + "1", existing_address.city, existing_address.country))
+ assert_equal false, Customer.exists?(address: Address.new(existing_address.street, existing_address.city, existing_address.country + "1"))
+ assert_equal false, Customer.exists?(address: Address.new(existing_address.street, existing_address.city + "1", existing_address.country))
+ assert_equal false, Customer.exists?(address: Address.new(existing_address.street + "1", existing_address.city, existing_address.country))
end
def test_exists_does_not_instantiate_records
- Developer.expects(:instantiate).never
- Developer.exists?
+ assert_not_called(Developer, :instantiate) do
+ Developer.exists?
+ end
end
def test_find_by_array_of_one_id
@@ -193,8 +339,10 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_by_ids_with_limit_and_offset
- assert_equal 2, Entrant.limit(2).find([1,3,2]).size
- assert_equal 1, Entrant.limit(3).offset(2).find([1,3,2]).size
+ assert_equal 2, Entrant.limit(2).find([1, 3, 2]).size
+ entrants = Entrant.limit(3).offset(2).find([1, 3, 2])
+ assert_equal 1, entrants.size
+ assert_equal "Ruby Guru", entrants.first.name
# Also test an edge case: If you have 11 results, and you set a
# limit of 3 and offset of 9, then you should find that there
@@ -202,32 +350,43 @@ class FinderTest < ActiveRecord::TestCase
devs = Developer.all
last_devs = Developer.limit(3).offset(9).find devs.map(&:id)
assert_equal 2, last_devs.size
+ assert_equal "fixture_10", last_devs[0].name
+ assert_equal "Jamis", last_devs[1].name
end
def test_find_with_large_number
- assert_raises(ActiveRecord::RecordNotFound) { Topic.find('9999999999999999999999999999999') }
+ assert_raises(ActiveRecord::RecordNotFound) { Topic.find("9999999999999999999999999999999") }
end
def test_find_by_with_large_number
- assert_nil Topic.find_by(id: '9999999999999999999999999999999')
+ assert_nil Topic.find_by(id: "9999999999999999999999999999999")
end
def test_find_by_id_with_large_number
- assert_nil Topic.find_by_id('9999999999999999999999999999999')
+ assert_nil Topic.find_by_id("9999999999999999999999999999999")
end
def test_find_on_relation_with_large_number
- assert_nil Topic.where('1=1').find_by(id: 9999999999999999999999999999999)
+ assert_raises(ActiveRecord::RecordNotFound) do
+ Topic.where("1=1").find(9999999999999999999999999999999)
+ end
+ end
+
+ def test_find_by_on_relation_with_large_number
+ assert_nil Topic.where("1=1").find_by(id: 9999999999999999999999999999999)
end
def test_find_by_bang_on_relation_with_large_number
assert_raises(ActiveRecord::RecordNotFound) do
- Topic.where('1=1').find_by!(id: 9999999999999999999999999999999)
+ Topic.where("1=1").find_by!(id: 9999999999999999999999999999999)
end
end
def test_find_an_empty_array
- assert_equal [], Topic.find([])
+ empty_array = []
+ result = Topic.find(empty_array)
+ assert_equal [], result
+ assert_not_same empty_array, result
end
def test_find_doesnt_have_implicit_ordering
@@ -239,7 +398,7 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_with_group_and_sanitized_having_method
- developers = Developer.group(:salary).having("sum(salary) > ?", 10000).select('salary').to_a
+ developers = Developer.group(:salary).having("sum(salary) > ?", 10000).select("salary").to_a
assert_equal 3, developers.size
assert_equal 3, developers.map(&:salary).uniq.size
assert developers.all? { |developer| developer.salary > 10000 }
@@ -264,8 +423,19 @@ class FinderTest < ActiveRecord::TestCase
assert_equal [Account], accounts.collect(&:class).uniq
end
+ def test_find_by_association_subquery
+ author = authors(:david)
+ assert_equal author.post, Post.find_by(author: Author.where(id: author))
+ 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
+ assert_equal topics(:first), Topic.where("title = 'The First Topic'").take
end
def test_take_failing
@@ -308,6 +478,7 @@ class FinderTest < ActiveRecord::TestCase
expected = topics(:first)
expected.touch # PostgreSQL changes the default order if no order clause is used
assert_equal expected, Topic.first
+ assert_equal expected, Topic.limit(5).first
end
def test_model_class_responds_to_first_bang
@@ -330,6 +501,7 @@ class FinderTest < ActiveRecord::TestCase
expected = topics(:second)
expected.touch # PostgreSQL changes the default order if no order clause is used
assert_equal expected, Topic.second
+ assert_equal expected, Topic.limit(5).second
end
def test_model_class_responds_to_second_bang
@@ -352,6 +524,7 @@ class FinderTest < ActiveRecord::TestCase
expected = topics(:third)
expected.touch # PostgreSQL changes the default order if no order clause is used
assert_equal expected, Topic.third
+ assert_equal expected, Topic.limit(5).third
end
def test_model_class_responds_to_third_bang
@@ -374,6 +547,7 @@ class FinderTest < ActiveRecord::TestCase
expected = topics(:fourth)
expected.touch # PostgreSQL changes the default order if no order clause is used
assert_equal expected, Topic.fourth
+ assert_equal expected, Topic.limit(5).fourth
end
def test_model_class_responds_to_fourth_bang
@@ -396,6 +570,7 @@ class FinderTest < ActiveRecord::TestCase
expected = topics(:fifth)
expected.touch # PostgreSQL changes the default order if no order clause is used
assert_equal expected, Topic.fifth
+ assert_equal expected, Topic.limit(5).fifth
end
def test_model_class_responds_to_fifth_bang
@@ -406,6 +581,66 @@ class FinderTest < ActiveRecord::TestCase
end
end
+ def test_second_to_last
+ assert_equal topics(:fourth).title, Topic.second_to_last.title
+
+ # test with offset
+ assert_equal topics(:fourth), Topic.offset(1).second_to_last
+ assert_equal topics(:fourth), Topic.offset(2).second_to_last
+ assert_equal topics(:fourth), Topic.offset(3).second_to_last
+ assert_nil Topic.offset(4).second_to_last
+ assert_nil Topic.offset(5).second_to_last
+
+ # test with limit
+ 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
+ expected = topics(:fourth)
+ expected.touch # PostgreSQL changes the default order if no order clause is used
+ assert_equal expected, Topic.second_to_last
+ end
+
+ def test_model_class_responds_to_second_to_last_bang
+ assert Topic.second_to_last!
+ Topic.delete_all
+ assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do
+ Topic.second_to_last!
+ end
+ end
+
+ def test_third_to_last
+ assert_equal topics(:third).title, Topic.third_to_last.title
+
+ # test with offset
+ assert_equal topics(:third), Topic.offset(1).third_to_last
+ assert_equal topics(:third), Topic.offset(2).third_to_last
+ assert_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_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
+ expected = topics(:third)
+ expected.touch # PostgreSQL changes the default order if no order clause is used
+ assert_equal expected, Topic.third_to_last
+ end
+
+ def test_model_class_responds_to_third_to_last_bang
+ assert Topic.third_to_last!
+ Topic.delete_all
+ assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do
+ Topic.third_to_last!
+ end
+ end
+
def test_last_bang_present
assert_nothing_raised do
assert_equal topics(:second), Topic.where("title = 'The Second Topic of the day'").last!
@@ -427,25 +662,83 @@ class FinderTest < ActiveRecord::TestCase
end
def test_take_and_first_and_last_with_integer_should_use_sql_limit
- assert_sql(/LIMIT 3|ROWNUM <= 3/) { Topic.take(3).entries }
- assert_sql(/LIMIT 2|ROWNUM <= 2/) { Topic.first(2).entries }
- assert_sql(/LIMIT 5|ROWNUM <= 5/) { Topic.last(5).entries }
+ assert_sql(/LIMIT|ROWNUM <=|FETCH FIRST/) { Topic.take(3).entries }
+ assert_sql(/LIMIT|ROWNUM <=|FETCH FIRST/) { Topic.first(2).entries }
+ assert_sql(/LIMIT|ROWNUM <=|FETCH FIRST/) { Topic.last(5).entries }
end
def test_last_with_integer_and_order_should_keep_the_order
assert_equal Topic.order("title").to_a.last(2), Topic.order("title").last(2)
end
- def test_last_with_integer_and_order_should_not_use_sql_limit
- query = assert_sql { Topic.order("title").last(5).entries }
- assert_equal 1, query.length
- assert_no_match(/LIMIT/, query.first)
+ def test_last_with_integer_and_order_should_use_sql_limit
+ relation = Topic.order("title")
+ assert_queries(1) { relation.last(5) }
+ assert_not_predicate relation, :loaded?
+ end
+
+ def test_last_with_integer_and_reorder_should_use_sql_limit
+ relation = Topic.reorder("title")
+ assert_queries(1) { relation.last(5) }
+ assert_not_predicate relation, :loaded?
end
- def test_last_with_integer_and_reorder_should_not_use_sql_limit
- query = assert_sql { Topic.reorder("title").last(5).entries }
- assert_equal 1, query.length
- assert_no_match(/LIMIT/, query.first)
+ def test_last_on_loaded_relation_should_not_use_sql
+ relation = Topic.limit(10).load
+ assert_no_queries do
+ relation.last
+ relation.last(2)
+ end
+ end
+
+ def test_last_with_irreversible_order
+ assert_raises(ActiveRecord::IrreversibleOrderError) do
+ Topic.order(Arel.sql("coalesce(author_name, title)")).last
+ end
+ end
+
+ def test_last_on_relation_with_limit_and_offset
+ post = posts("sti_comments")
+
+ comments = post.comments.order(id: :asc)
+ assert_equal comments.limit(2).to_a.last, comments.limit(2).last
+ assert_equal comments.limit(2).to_a.last(2), comments.limit(2).last(2)
+ assert_equal comments.limit(2).to_a.last(3), comments.limit(2).last(3)
+
+ assert_equal comments.offset(2).to_a.last, comments.offset(2).last
+ assert_equal comments.offset(2).to_a.last(2), comments.offset(2).last(2)
+ assert_equal comments.offset(2).to_a.last(3), comments.offset(2).last(3)
+
+ comments = comments.offset(1)
+ assert_equal comments.limit(2).to_a.last, comments.limit(2).last
+ assert_equal comments.limit(2).to_a.last(2), comments.limit(2).last(2)
+ assert_equal comments.limit(2).to_a.last(3), comments.limit(2).last(3)
+ end
+
+ def test_first_on_relation_with_limit_and_offset
+ post = posts("sti_comments")
+
+ comments = post.comments.order(id: :asc)
+ assert_equal comments.limit(2).to_a.first, comments.limit(2).first
+ assert_equal comments.limit(2).to_a.first(2), comments.limit(2).first(2)
+ assert_equal comments.limit(2).to_a.first(3), comments.limit(2).first(3)
+
+ assert_equal comments.offset(2).to_a.first, comments.offset(2).first
+ assert_equal comments.offset(2).to_a.first(2), comments.offset(2).first(2)
+ assert_equal comments.offset(2).to_a.first(3), comments.offset(2).first(3)
+
+ comments = comments.offset(1)
+ assert_equal comments.limit(2).to_a.first, comments.limit(2).first
+ assert_equal comments.limit(2).to_a.first(2), comments.limit(2).first(2)
+ assert_equal comments.limit(2).to_a.first(3), comments.limit(2).first(3)
+ end
+
+ def test_first_have_determined_order_by_default
+ expected = [companies(:second_client), companies(:another_client)]
+ clients = Client.where(name: expected.map(&:name))
+
+ assert_equal expected, clients.first(2)
+ assert_equal expected, clients.limit(5).first(2)
end
def test_take_and_first_and_last_with_integer_should_return_an_array
@@ -464,12 +757,12 @@ class FinderTest < ActiveRecord::TestCase
def test_find_only_some_columns
topic = Topic.select("author_name").find(1)
- assert_raise(ActiveModel::MissingAttributeError) {topic.title}
- assert_raise(ActiveModel::MissingAttributeError) {topic.title?}
+ assert_raise(ActiveModel::MissingAttributeError) { topic.title }
+ assert_raise(ActiveModel::MissingAttributeError) { topic.title? }
assert_nil topic.read_attribute("title")
assert_equal "David", topic.author_name
- assert !topic.attribute_present?("title")
- assert !topic.attribute_present?(:title)
+ assert_not topic.attribute_present?("title")
+ assert_not topic.attribute_present?(:title)
assert topic.attribute_present?("author_name")
assert_respond_to topic, "author_name"
end
@@ -484,9 +777,14 @@ class FinderTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::RecordNotFound) { Topic.where(approved: true).find(1) }
end
- def test_find_on_hash_conditions_with_explicit_table_name
- assert Topic.where('topics.approved' => false).find(1)
- assert_raise(ActiveRecord::RecordNotFound) { Topic.where('topics.approved' => true).find(1) }
+ def test_find_on_hash_conditions_with_qualified_attribute_dot_notation_string
+ assert Topic.where("topics.approved" => false).find(1)
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.where("topics.approved" => true).find(1) }
+ end
+
+ def test_find_on_hash_conditions_with_qualified_attribute_dot_notation_symbol
+ assert Topic.where('topics.approved': false).find(1)
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.where('topics.approved': true).find(1) }
end
def test_find_on_hash_conditions_with_hashed_table_name
@@ -495,28 +793,28 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_on_combined_explicit_and_hashed_table_names
- assert Topic.where('topics.approved' => false, topics: { author_name: "David" }).find(1)
- assert_raise(ActiveRecord::RecordNotFound) { Topic.where('topics.approved' => true, topics: { author_name: "David" }).find(1) }
- assert_raise(ActiveRecord::RecordNotFound) { Topic.where('topics.approved' => false, topics: { author_name: "Melanie" }).find(1) }
+ assert Topic.where("topics.approved" => false, topics: { author_name: "David" }).find(1)
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.where("topics.approved" => true, topics: { author_name: "David" }).find(1) }
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.where("topics.approved" => false, topics: { author_name: "Melanie" }).find(1) }
end
def test_find_with_hash_conditions_on_joined_table
- firms = Firm.joins(:account).where(:accounts => { :credit_limit => 50 })
+ firms = Firm.joins(:account).where(accounts: { credit_limit: 50 })
assert_equal 1, firms.size
assert_equal companies(:first_firm), firms.first
end
def test_find_with_hash_conditions_on_joined_table_and_with_range
- firms = DependentFirm.joins(:account).where(name: 'RailsCore', accounts: { credit_limit: 55..60 })
+ firms = DependentFirm.joins(:account).where(name: "RailsCore", accounts: { credit_limit: 55..60 })
assert_equal 1, firms.size
assert_equal companies(:rails_core), firms.first
end
def test_find_on_hash_conditions_with_explicit_table_name_and_aggregate
david = customers(:david)
- assert Customer.where('customers.name' => david.name, :address => david.address).find(david.id)
+ assert Customer.where("customers.name" => david.name, :address => david.address).find(david.id)
assert_raise(ActiveRecord::RecordNotFound) {
- Customer.where('customers.name' => david.name + "1", :address => david.address).find(david.id)
+ Customer.where("customers.name" => david.name + "1", :address => david.address).find(david.id)
}
end
@@ -525,34 +823,42 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_on_hash_conditions_with_range
- assert_equal [1,2], Topic.where(id: 1..2).to_a.map(&:id).sort
+ assert_equal [1, 2], Topic.where(id: 1..2).to_a.map(&:id).sort
assert_raise(ActiveRecord::RecordNotFound) { Topic.where(id: 2..3).find(1) }
end
def test_find_on_hash_conditions_with_end_exclusive_range
- assert_equal [1,2,3], Topic.where(id: 1..3).to_a.map(&:id).sort
- assert_equal [1,2], Topic.where(id: 1...3).to_a.map(&:id).sort
+ assert_equal [1, 2, 3], Topic.where(id: 1..3).to_a.map(&:id).sort
+ assert_equal [1, 2], Topic.where(id: 1...3).to_a.map(&:id).sort
assert_raise(ActiveRecord::RecordNotFound) { Topic.where(id: 2...3).find(3) }
end
def test_find_on_hash_conditions_with_multiple_ranges
- assert_equal [1,2,3], Comment.where(id: 1..3, post_id: 1..2).to_a.map(&:id).sort
+ assert_equal [1, 2, 3], Comment.where(id: 1..3, post_id: 1..2).to_a.map(&:id).sort
assert_equal [1], Comment.where(id: 1..1, post_id: 1..10).to_a.map(&:id).sort
end
def test_find_on_hash_conditions_with_array_of_integers_and_ranges
- assert_equal [1,2,3,5,6,7,8,9], Comment.where(id: [1..2, 3, 5, 6..8, 9]).to_a.map(&:id).sort
+ assert_equal [1, 2, 3, 5, 6, 7, 8, 9], Comment.where(id: [1..2, 3, 5, 6..8, 9]).to_a.map(&:id).sort
end
def test_find_on_hash_conditions_with_array_of_ranges
- assert_equal [1,2,6,7,8], Comment.where(id: [1..2, 6..8]).to_a.map(&:id).sort
+ assert_equal [1, 2, 6, 7, 8], Comment.where(id: [1..2, 6..8]).to_a.map(&:id).sort
+ end
+
+ def test_find_on_hash_conditions_with_open_ended_range
+ assert_equal [1, 2, 3], Comment.where(id: Float::INFINITY..3).to_a.map(&:id).sort
+ end
+
+ def test_find_on_hash_conditions_with_numeric_range_for_string
+ topic = Topic.create!(title: "12 Factor App")
+ assert_equal [topic], Topic.where(title: 10..2).to_a
end
def test_find_on_multiple_hash_conditions
assert Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: false).find(1)
assert_raise(ActiveRecord::RecordNotFound) { Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: true).find(1) }
assert_raise(ActiveRecord::RecordNotFound) { Topic.where(author_name: "David", title: "HHC", replies_count: 1, approved: false).find(1) }
- assert_raise(ActiveRecord::RecordNotFound) { Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: true).find(1) }
end
def test_condition_interpolation
@@ -587,9 +893,9 @@ class FinderTest < ActiveRecord::TestCase
end
def test_hash_condition_find_with_array
- p1, p2 = Post.limit(2).order('id asc').to_a
- assert_equal [p1, p2], Post.where(id: [p1, p2]).order('id asc').to_a
- assert_equal [p1, p2], Post.where(id: [p1, p2.id]).order('id asc').to_a
+ p1, p2 = Post.limit(2).order("id asc").to_a
+ assert_equal [p1, p2], Post.where(id: [p1, p2]).order("id asc").to_a
+ assert_equal [p1, p2], Post.where(id: [p1, p2.id]).order("id asc").to_a
end
def test_hash_condition_find_with_nil
@@ -601,56 +907,75 @@ class FinderTest < ActiveRecord::TestCase
def test_hash_condition_find_with_aggregate_having_one_mapping
balance = customers(:david).balance
assert_kind_of Money, balance
- found_customer = Customer.where(:balance => balance).first
+ found_customer = Customer.where(balance: balance).first
assert_equal customers(:david), found_customer
end
+ def test_hash_condition_find_with_aggregate_having_three_mappings_array
+ david_address = customers(:david).address
+ zaphod_address = customers(:zaphod).address
+ barney_address = customers(:barney).address
+ assert_kind_of Address, david_address
+ assert_kind_of Address, zaphod_address
+ found_customers = Customer.where(address: [david_address, zaphod_address, barney_address])
+ assert_equal [customers(:david), customers(:zaphod), customers(:barney)], found_customers.sort_by(&:id)
+ end
+
+ def test_hash_condition_find_with_aggregate_having_one_mapping_array
+ david_balance = customers(:david).balance
+ zaphod_balance = customers(:zaphod).balance
+ assert_kind_of Money, david_balance
+ assert_kind_of Money, zaphod_balance
+ found_customers = Customer.where(balance: [david_balance, zaphod_balance])
+ assert_equal [customers(:david), customers(:zaphod)], found_customers.sort_by(&:id)
+ end
+
def test_hash_condition_find_with_aggregate_attribute_having_same_name_as_field_and_key_value_being_aggregate
gps_location = customers(:david).gps_location
assert_kind_of GpsLocation, gps_location
- found_customer = Customer.where(:gps_location => gps_location).first
+ found_customer = Customer.where(gps_location: gps_location).first
assert_equal customers(:david), found_customer
end
def test_hash_condition_find_with_aggregate_having_one_mapping_and_key_value_being_attribute_value
balance = customers(:david).balance
assert_kind_of Money, balance
- found_customer = Customer.where(:balance => balance.amount).first
+ found_customer = Customer.where(balance: balance.amount).first
assert_equal customers(:david), found_customer
end
def test_hash_condition_find_with_aggregate_attribute_having_same_name_as_field_and_key_value_being_attribute_value
gps_location = customers(:david).gps_location
assert_kind_of GpsLocation, gps_location
- found_customer = Customer.where(:gps_location => gps_location.gps_location).first
+ found_customer = Customer.where(gps_location: gps_location.gps_location).first
assert_equal customers(:david), found_customer
end
def test_hash_condition_find_with_aggregate_having_three_mappings
address = customers(:david).address
assert_kind_of Address, address
- found_customer = Customer.where(:address => address).first
+ found_customer = Customer.where(address: address).first
assert_equal customers(:david), found_customer
end
def test_hash_condition_find_with_one_condition_being_aggregate_and_another_not
address = customers(:david).address
assert_kind_of Address, address
- found_customer = Customer.where(:address => address, :name => customers(:david).name).first
+ found_customer = Customer.where(address: address, name: customers(:david).name).first
assert_equal customers(:david), found_customer
end
def test_condition_utc_time_interpolation_with_default_timezone_local
- with_env_tz 'America/New_York' do
+ with_env_tz "America/New_York" do
with_timezone_config default: :local do
topic = Topic.first
- assert_equal topic, Topic.where(['written_on = ?', topic.written_on.getutc]).first
+ assert_equal topic, Topic.where(["written_on = ?", topic.written_on.getutc]).first
end
end
end
def test_hash_condition_utc_time_interpolation_with_default_timezone_local
- with_env_tz 'America/New_York' do
+ with_env_tz "America/New_York" do
with_timezone_config default: :local do
topic = Topic.first
assert_equal topic, Topic.where(written_on: topic.written_on.getutc).first
@@ -659,16 +984,16 @@ class FinderTest < ActiveRecord::TestCase
end
def test_condition_local_time_interpolation_with_default_timezone_utc
- with_env_tz 'America/New_York' do
+ with_env_tz "America/New_York" do
with_timezone_config default: :utc do
topic = Topic.first
- assert_equal topic, Topic.where(['written_on = ?', topic.written_on.getlocal]).first
+ assert_equal topic, Topic.where(["written_on = ?", topic.written_on.getlocal]).first
end
end
end
def test_hash_condition_local_time_interpolation_with_default_timezone_utc
- with_env_tz 'America/New_York' do
+ with_env_tz "America/New_York" do
with_timezone_config default: :utc do
topic = Topic.first
assert_equal topic, Topic.where(written_on: topic.written_on.getlocal).first
@@ -685,109 +1010,27 @@ class FinderTest < ActiveRecord::TestCase
Company.where(["id=? AND name = ?", 2]).first
}
assert_raise(ActiveRecord::PreparedStatementInvalid) {
- Company.where(["id=?", 2, 3, 4]).first
+ Company.where(["id=?", 2, 3, 4]).first
}
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
- end
-
- def test_bind_arity
- assert_nothing_raised { bind '' }
- assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '', 1 }
-
- assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '?' }
- assert_nothing_raised { bind '?', 1 }
- assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '?', 1, 1 }
+ Company.create("name" => "37signals' go'es against")
+ assert Company.where(["name = :name", { name: "37signals' go'es against" }]).first
end
def test_named_bind_variables
- assert_equal '1', bind(':a', :a => 1) # ' ruby-mode
- assert_equal '1 1', bind(':a :a', :a => 1) # ' ruby-mode
-
- assert_nothing_raised { bind("'+00:00'", :foo => "bar") }
-
assert_kind_of Firm, Company.where(["name = :name", { name: "37signals" }]).first
assert_nil Company.where(["name = :name", { name: "37signals!" }]).first
assert_nil Company.where(["name = :name", { name: "37signals!' OR 1=1" }]).first
assert_kind_of Time, Topic.where(["id = :id", { id: 1 }]).first.written_on
end
- class SimpleEnumerable
- include Enumerable
-
- def initialize(ary)
- @ary = ary
- end
-
- def each(&b)
- @ary.each(&b)
- end
- end
-
- def test_bind_enumerable
- quoted_abc = %(#{ActiveRecord::Base.connection.quote('a')},#{ActiveRecord::Base.connection.quote('b')},#{ActiveRecord::Base.connection.quote('c')})
-
- assert_equal '1,2,3', bind('?', [1, 2, 3])
- assert_equal quoted_abc, bind('?', %w(a b c))
-
- assert_equal '1,2,3', bind(':a', :a => [1, 2, 3])
- assert_equal quoted_abc, bind(':a', :a => %w(a b c)) # '
-
- assert_equal '1,2,3', bind('?', SimpleEnumerable.new([1, 2, 3]))
- assert_equal quoted_abc, bind('?', SimpleEnumerable.new(%w(a b c)))
-
- assert_equal '1,2,3', bind(':a', :a => SimpleEnumerable.new([1, 2, 3]))
- assert_equal quoted_abc, bind(':a', :a => SimpleEnumerable.new(%w(a b c))) # '
- end
-
- def test_bind_empty_enumerable
- quoted_nil = ActiveRecord::Base.connection.quote(nil)
- assert_equal quoted_nil, bind('?', [])
- assert_equal " in (#{quoted_nil})", bind(' in (?)', [])
- assert_equal "foo in (#{quoted_nil})", bind('foo in (?)', [])
- end
-
- def test_bind_empty_string
- quoted_empty = ActiveRecord::Base.connection.quote('')
- assert_equal quoted_empty, bind('?', '')
- end
-
- def test_bind_chars
- quoted_bambi = ActiveRecord::Base.connection.quote("Bambi")
- quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote("Bambi\nand\nThumper")
- assert_equal "name=#{quoted_bambi}", bind('name=?', "Bambi")
- assert_equal "name=#{quoted_bambi_and_thumper}", bind('name=?', "Bambi\nand\nThumper")
- assert_equal "name=#{quoted_bambi}", bind('name=?', "Bambi".mb_chars)
- assert_equal "name=#{quoted_bambi_and_thumper}", bind('name=?', "Bambi\nand\nThumper".mb_chars)
- end
-
- def test_bind_record
- o = Struct.new(:quoted_id).new(1)
- assert_equal '1', bind('?', o)
-
- os = [o] * 3
- assert_equal '1,1,1', bind('?', os)
- end
-
- def test_named_bind_with_postgresql_type_casts
- l = Proc.new { bind(":a::integer '2009-01-01'::date", :a => '10') }
- assert_nothing_raised(&l)
- assert_equal "#{ActiveRecord::Base.connection.quote('10')}::integer '2009-01-01'::date", l.call
- end
-
- def test_string_sanitation
- assert_not_equal "'something ' 1=1'", ActiveRecord::Base.sanitize("something ' 1=1")
- assert_equal "'something; select table'", ActiveRecord::Base.sanitize("something; select table")
- end
-
def test_count_by_sql
assert_equal(0, Entrant.count_by_sql("SELECT COUNT(*) FROM entrants WHERE id > 3"))
assert_equal(1, Entrant.count_by_sql(["SELECT COUNT(*) FROM entrants WHERE id > ?", 2]))
@@ -807,7 +1050,7 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_by_on_attribute_that_is_a_reserved_word
- dog_alias = 'Dog'
+ dog_alias = "Dog"
dog = Dog.create(alias: dog_alias)
assert_equal dog, Dog.find_by_alias(dog_alias)
@@ -824,7 +1067,7 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_by_one_attribute_with_conditions
- assert_equal accounts(:rails_core_account), Account.where('firm_id = ?', 6).find_by_credit_limit(50)
+ assert_equal accounts(:rails_core_account), Account.where("firm_id = ?", 6).find_by_credit_limit(50)
end
def test_find_by_one_attribute_that_is_an_aggregate
@@ -864,12 +1107,12 @@ class FinderTest < ActiveRecord::TestCase
def test_dynamic_finder_on_one_attribute_with_conditions_returns_same_results_after_caching
# ensure this test can run independently of order
class << Account; self; end.send(:remove_method, :find_by_credit_limit) if Account.public_methods.include?(:find_by_credit_limit)
- a = Account.where('firm_id = ?', 6).find_by_credit_limit(50)
- assert_equal a, Account.where('firm_id = ?', 6).find_by_credit_limit(50) # find_by_credit_limit has been cached
+ a = Account.where("firm_id = ?", 6).find_by_credit_limit(50)
+ assert_equal a, Account.where("firm_id = ?", 6).find_by_credit_limit(50) # find_by_credit_limit has been cached
end
def test_find_by_one_attribute_with_several_options
- assert_equal accounts(:unknown), Account.order('id DESC').where('id != ?', 3).find_by_credit_limit(50)
+ assert_equal accounts(:unknown), Account.order("id DESC").where("id != ?", 3).find_by_credit_limit(50)
end
def test_find_by_one_missing_attribute
@@ -892,15 +1135,6 @@ class FinderTest < ActiveRecord::TestCase
assert_raise(ArgumentError) { Topic.find_by_title_and_author_name("The First Topic") }
end
- def test_find_last_with_offset
- devs = Developer.order('id')
-
- assert_equal devs[2], Developer.offset(2).first
- assert_equal devs[-3], Developer.offset(2).last
- assert_equal devs[-3], Developer.offset(2).last
- assert_equal devs[-3], Developer.offset(2).order('id DESC').first
- end
-
def test_find_by_nil_attribute
topic = Topic.find_by_last_read nil
assert_not_nil topic
@@ -916,20 +1150,10 @@ class FinderTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::StatementInvalid) { Topic.find_by_sql "select 1 from badtable" }
end
- def test_find_all_with_join
- developers_on_project_one = Developer.
- joins('LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id').
- where('project_id=1').to_a
- assert_equal 3, developers_on_project_one.length
- developer_names = developers_on_project_one.map(&:name)
- assert developer_names.include?('David')
- assert developer_names.include?('Jamis')
- end
-
def test_joins_dont_clobber_id
first = Firm.
- joins('INNER JOIN companies clients ON clients.firm_id = companies.id').
- where('companies.id = 1').first
+ joins("INNER JOIN companies clients ON clients.firm_id = companies.id").
+ where("companies.id = 1").first
assert_equal 1, first.id
end
@@ -943,12 +1167,12 @@ class FinderTest < ActiveRecord::TestCase
def test_find_by_id_with_conditions_with_or
assert_nothing_raised do
- Post.where("posts.id <= 3 OR posts.#{QUOTED_TYPE} = 'Post'").find([1,2,3])
+ Post.where("posts.id <= 3 OR posts.#{QUOTED_TYPE} = 'Post'").find([1, 2, 3])
end
end
def test_find_ignores_previously_inserted_record
- Post.create!(:title => 'test', :body => 'it out')
+ Post.create!(title: "test", body: "it out")
assert_equal [], Post.where(id: nil)
end
@@ -957,13 +1181,13 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_by_empty_in_condition
- assert_equal [], Post.where('id in (?)', [])
+ assert_equal [], Post.where("id in (?)", [])
end
def test_find_by_records
- p1, p2 = Post.limit(2).order('id asc').to_a
- assert_equal [p1, p2], Post.where(['id in (?)', [p1, p2]]).order('id asc')
- assert_equal [p1, p2], Post.where(['id in (?)', [p1, p2.id]]).order('id asc')
+ p1, p2 = Post.limit(2).order("id asc").to_a
+ assert_equal [p1, p2], Post.where(["id in (?)", [p1, p2]]).order("id asc")
+ assert_equal [p1, p2], Post.where(["id in (?)", [p1, p2.id]]).order("id asc")
end
def test_select_value
@@ -975,8 +1199,8 @@ class FinderTest < ActiveRecord::TestCase
end
def test_select_values
- assert_equal ["1","2","3","4","5","6","7","8","9", "10", "11"], Company.connection.select_values("SELECT id FROM companies ORDER BY id").map!(&:to_s)
- assert_equal ["37signals","Summit","Microsoft", "Flamboyant Software", "Ex Nihilo", "RailsCore", "Leetsoft", "Jadedpixel", "Odegy", "Ex Nihilo Part Deux", "Apex"], Company.connection.select_values("SELECT name FROM companies ORDER BY id")
+ assert_equal ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"], Company.connection.select_values("SELECT id FROM companies ORDER BY id").map!(&:to_s)
+ assert_equal ["37signals", "Summit", "Microsoft", "Flamboyant Software", "Ex Nihilo", "RailsCore", "Leetsoft", "Jadedpixel", "Odegy", "Ex Nihilo Part Deux", "Apex"], Company.connection.select_values("SELECT name FROM companies ORDER BY id")
end
def test_select_rows
@@ -984,33 +1208,41 @@ class FinderTest < ActiveRecord::TestCase
[["1", "1", nil, "37signals"],
["2", "1", "2", "Summit"],
["3", "1", "1", "Microsoft"]],
- Company.connection.select_rows("SELECT id, firm_id, client_of, name FROM companies WHERE id IN (1,2,3) ORDER BY id").map! {|i| i.map! {|j| j.to_s unless j.nil?}})
+ Company.connection.select_rows("SELECT id, firm_id, client_of, name FROM companies WHERE id IN (1,2,3) ORDER BY id").map! { |i| i.map! { |j| j.to_s unless j.nil? } })
assert_equal [["1", "37signals"], ["2", "Summit"], ["3", "Microsoft"]],
- Company.connection.select_rows("SELECT id, name FROM companies WHERE id IN (1,2,3) ORDER BY id").map! {|i| i.map! {|j| j.to_s unless j.nil?}}
+ Company.connection.select_rows("SELECT id, name FROM companies WHERE id IN (1,2,3) ORDER BY id").map! { |i| i.map! { |j| j.to_s unless j.nil? } }
end
def test_find_with_order_on_included_associations_with_construct_finder_sql_for_association_limiting_and_is_distinct
- assert_equal 2, Post.includes(authors: :author_address).order('author_addresses.id DESC ').limit(2).to_a.size
+ assert_equal 2, Post.includes(authors: :author_address).
+ where.not(author_addresses: { id: nil }).
+ order("author_addresses.id DESC").limit(2).to_a.size
assert_equal 3, Post.includes(author: :author_address, authors: :author_address).
- order('author_addresses_authors.id DESC ').limit(3).to_a.size
+ where.not(author_addresses_authors: { id: nil }).
+ order("author_addresses_authors.id DESC").limit(3).to_a.size
+ end
+
+ def test_find_with_eager_loading_collection_and_ordering_by_collection_primary_key
+ assert_equal Post.first, Post.eager_load(comments: :ratings).
+ order("posts.id, ratings.id, comments.id").first
end
def test_find_with_nil_inside_set_passed_for_one_attribute
client_of = Company.
where(client_of: [2, 1, nil],
- name: ['37signals', 'Summit', 'Microsoft']).
- order('client_of DESC').
+ name: ["37signals", "Summit", "Microsoft"]).
+ order("client_of DESC").
map(&:client_of)
- assert client_of.include?(nil)
+ assert_includes client_of, nil
assert_equal [2, 1].sort, client_of.compact.sort
end
def test_find_with_nil_inside_set_passed_for_attribute
client_of = Company.
where(client_of: [nil]).
- order('client_of DESC').
+ order("client_of DESC").
map(&:client_of)
assert_equal [], client_of.compact
@@ -1018,20 +1250,30 @@ class FinderTest < ActiveRecord::TestCase
def test_with_limiting_with_custom_select
posts = Post.references(:authors).merge(
- :includes => :author, :select => 'posts.*, authors.id as "author_id"',
- :limit => 3, :order => 'posts.id'
+ includes: :author, select: 'posts.*, authors.id as "author_id"',
+ limit: 3, order: "posts.id"
).to_a
assert_equal 3, posts.size
assert_equal [0, 1, 1], posts.map(&:author_id).sort
end
+ def test_find_one_message_on_primary_key
+ e = assert_raises(ActiveRecord::RecordNotFound) do
+ Car.find(0)
+ end
+ assert_equal 0, e.id
+ assert_equal "id", e.primary_key
+ assert_equal "Car", e.model
+ assert_equal "Couldn't find Car with 'id'=0", e.message
+ end
+
def test_find_one_message_with_custom_primary_key
table_with_custom_primary_key do |model|
model.primary_key = :name
e = assert_raises(ActiveRecord::RecordNotFound) do
- model.find 'Hello World!'
+ model.find "Hello World!"
end
- assert_equal %Q{Couldn't find MercedesCar with 'name'=Hello World!}, e.message
+ assert_equal "Couldn't find MercedesCar with 'name'=Hello World!", e.message
end
end
@@ -1039,9 +1281,9 @@ class FinderTest < ActiveRecord::TestCase
table_with_custom_primary_key do |model|
model.primary_key = :name
e = assert_raises(ActiveRecord::RecordNotFound) do
- model.find 'Hello', 'World!'
+ model.find "Hello", "World!"
end
- assert_equal %Q{Couldn't find all MercedesCars with 'name': (Hello, World!) (found 0 results, but was looking for 2)}, e.message
+ assert_equal "Couldn't find all MercedesCars with 'name': (Hello, World!) (found 0 results, but was looking for 2).", e.message
end
end
@@ -1052,7 +1294,7 @@ class FinderTest < ActiveRecord::TestCase
end
def test_finder_with_offset_string
- assert_nothing_raised(ActiveRecord::StatementInvalid) { Topic.offset("3").to_a }
+ assert_nothing_raised { Topic.offset("3").to_a }
end
test "find_by with hash conditions returns the first matching record" do
@@ -1064,16 +1306,20 @@ class FinderTest < ActiveRecord::TestCase
end
test "find_by with multi-arg conditions returns the first matching record" do
- assert_equal posts(:eager_other), Post.find_by('id = ?', posts(:eager_other).id)
+ assert_equal posts(:eager_other), Post.find_by("id = ?", posts(:eager_other).id)
+ end
+
+ test "find_by with range conditions returns the first matching record" do
+ assert_equal posts(:eager_other), Post.find_by(id: posts(:eager_other).id...posts(:misc_by_bob).id)
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
assert_equal authors(:david), Post.find_by(author: authors(:david)).author
- assert_equal authors(:mary) , Post.find_by(author: authors(:mary) ).author
+ assert_equal authors(:mary), Post.find_by(author: authors(:mary)).author
end
test "find_by doesn't have implicit ordering" do
@@ -1089,7 +1335,7 @@ class FinderTest < ActiveRecord::TestCase
end
test "find_by! with multi-arg conditions returns the first matching record" do
- assert_equal posts(:eager_other), Post.find_by!('id = ?', posts(:eager_other).id)
+ assert_equal posts(:eager_other), Post.find_by!("id = ?", posts(:eager_other).id)
end
test "find_by! doesn't have implicit ordering" do
@@ -1122,19 +1368,39 @@ class FinderTest < ActiveRecord::TestCase
assert_equal tyre2, zyke.tyres.custom_find_by(id: tyre2.id)
end
- protected
- def bind(statement, *vars)
- if vars.first.is_a?(Hash)
- ActiveRecord::Base.send(:replace_named_bind_variables, statement, vars.first)
- else
- ActiveRecord::Base.send(:replace_bind_variables, statement, vars)
+ test "#skip_query_cache! for #exists?" do
+ Topic.cache do
+ assert_queries(1) do
+ Topic.exists?
+ Topic.exists?
+ end
+
+ assert_queries(2) do
+ Topic.all.skip_query_cache!.exists?
+ Topic.all.skip_query_cache!.exists?
+ end
+ end
+ end
+
+ test "#skip_query_cache! for #exists? with a limited eager load" do
+ Topic.cache do
+ assert_queries(1) do
+ Topic.eager_load(:replies).limit(1).exists?
+ Topic.eager_load(:replies).limit(1).exists?
+ end
+
+ assert_queries(2) do
+ Topic.eager_load(:replies).limit(1).skip_query_cache!.exists?
+ Topic.eager_load(:replies).limit(1).skip_query_cache!.exists?
end
end
+ end
+ private
def table_with_custom_primary_key
yield(Class.new(Toy) do
def self.name
- 'MercedesCar'
+ "MercedesCar"
end
end)
end
@@ -1143,5 +1409,4 @@ class FinderTest < ActiveRecord::TestCase
err = assert_raises(exception_class) { block.call }
assert_match message, err.message
end
-
end
diff --git a/activerecord/test/cases/fixture_set/file_test.rb b/activerecord/test/cases/fixture_set/file_test.rb
index 92efa8aca7..ff99988cb5 100644
--- a/activerecord/test/cases/fixture_set/file_test.rb
+++ b/activerecord/test/cases/fixture_set/file_test.rb
@@ -1,5 +1,7 @@
-require 'cases/helper'
-require 'tempfile'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "tempfile"
module ActiveRecord
class FixtureSet
@@ -15,7 +17,7 @@ module ActiveRecord
called = true
assert_equal 6, fh.to_a.length
end
- assert called, 'block called'
+ assert called, "block called"
end
def test_names
@@ -31,8 +33,8 @@ module ActiveRecord
def test_values
File.open(::File.join(FIXTURES_ROOT, "accounts.yml")) do |fh|
- assert_equal [1,2,3,4,5,6].sort, fh.to_a.map(&:last).map { |x|
- x['id']
+ assert_equal [1, 2, 3, 4, 5, 6].sort, fh.to_a.map(&:last).map { |x|
+ x["id"]
}.sort
end
end
@@ -45,7 +47,7 @@ module ActiveRecord
end
def test_empty_file
- tmp_yaml ['empty', 'yml'], '' do |t|
+ tmp_yaml ["empty", "yml"], "" do |t|
assert_equal [], File.open(t.path) { |fh| fh.to_a }
end
end
@@ -53,7 +55,7 @@ module ActiveRecord
# A valid YAML file is not necessarily a value Fixture file. Make sure
# an exception is raised if the format is not valid Fixture format.
def test_wrong_fixture_format_string
- tmp_yaml ['empty', 'yml'], 'qwerty' do |t|
+ tmp_yaml ["empty", "yml"], "qwerty" do |t|
assert_raises(ActiveRecord::Fixture::FormatError) do
File.open(t.path) { |fh| fh.to_a }
end
@@ -61,7 +63,7 @@ module ActiveRecord
end
def test_wrong_fixture_format_nested
- tmp_yaml ['empty', 'yml'], 'one: two' do |t|
+ tmp_yaml ["empty", "yml"], "one: two" do |t|
assert_raises(ActiveRecord::Fixture::FormatError) do
File.open(t.path) { |fh| fh.to_a }
end
@@ -75,9 +77,9 @@ module ActiveRecord
end
end
yaml = "one:\n name: <%= fixture_helper %>\n"
- tmp_yaml ['curious', 'yml'], yaml do |t|
+ tmp_yaml ["curious", "yml"], yaml do |t|
golden =
- [["one", {"name" => "Fixture helper"}]]
+ [["one", { "name" => "Fixture helper" }]]
assert_equal golden, File.open(t.path) { |fh| fh.to_a }
end
ActiveRecord::FixtureSet.context_class.class_eval do
@@ -95,15 +97,15 @@ one:
File: <%= File.name %>
END
- golden = [['one', {
- 'ActiveRecord' => 'constant',
- 'ActiveRecord_FixtureSet' => 'constant',
- 'FixtureSet' => nil,
- 'ActiveRecord_FixtureSet_File' => 'constant',
- 'File' => 'File'
+ golden = [["one", {
+ "ActiveRecord" => "constant",
+ "ActiveRecord_FixtureSet" => "constant",
+ "FixtureSet" => nil,
+ "ActiveRecord_FixtureSet_File" => "constant",
+ "File" => "File"
}]]
- tmp_yaml ['curious', 'yml'], yaml do |t|
+ tmp_yaml ["curious", "yml"], yaml do |t|
assert_equal golden, File.open(t.path) { |fh| fh.to_a }
end
end
@@ -113,8 +115,8 @@ END
def test_independent_render_contexts
yaml1 = "<% def leaked_method; 'leak'; end %>\n"
yaml2 = "one:\n name: <%= leaked_method %>\n"
- tmp_yaml ['leaky', 'yml'], yaml1 do |t1|
- tmp_yaml ['curious', 'yml'], yaml2 do |t2|
+ tmp_yaml ["leaky", "yml"], yaml1 do |t1|
+ tmp_yaml ["curious", "yml"], yaml2 do |t2|
File.open(t1.path) { |fh| fh.to_a }
assert_raises(NameError) do
File.open(t2.path) { |fh| fh.to_a }
@@ -123,16 +125,34 @@ END
end
end
- private
- def tmp_yaml(name, contents)
- t = Tempfile.new name
- t.binmode
- t.write contents
- t.close
- yield t
- ensure
- t.close true
+ def test_removes_fixture_config_row
+ File.open(::File.join(FIXTURES_ROOT, "other_posts.yml")) do |fh|
+ assert_equal(["second_welcome"], fh.each.map { |name, _| name })
+ end
+ end
+
+ def test_extracts_model_class_from_config_row
+ File.open(::File.join(FIXTURES_ROOT, "other_posts.yml")) do |fh|
+ assert_equal "Post", fh.model_class
+ end
end
+
+ def test_erb_filename
+ filename = "filename.yaml"
+ erb = File.new(filename).send(:prepare_erb, "<% Rails.env %>\n")
+ assert_equal erb.filename, filename
+ end
+
+ private
+ def tmp_yaml(name, contents)
+ t = Tempfile.new name
+ t.binmode
+ t.write contents
+ t.close
+ yield t
+ ensure
+ t.close true
+ end
end
end
end
diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb
index 03a187ae92..a5592fc86a 100644
--- a/activerecord/test/cases/fixtures_test.rb
+++ b/activerecord/test/cases/fixtures_test.rb
@@ -1,33 +1,40 @@
-require 'cases/helper'
-require 'models/admin'
-require 'models/admin/account'
-require 'models/admin/randomly_named_c1'
-require 'models/admin/user'
-require 'models/binary'
-require 'models/book'
-require 'models/bulb'
-require 'models/category'
-require 'models/company'
-require 'models/computer'
-require 'models/course'
-require 'models/developer'
-require 'models/computer'
-require 'models/joke'
-require 'models/matey'
-require 'models/parrot'
-require 'models/pirate'
-require 'models/doubloon'
-require 'models/post'
-require 'models/randomly_named_c1'
-require 'models/reply'
-require 'models/ship'
-require 'models/task'
-require 'models/topic'
-require 'models/traffic_light'
-require 'models/treasure'
-require 'tempfile'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "support/connection_helper"
+require "models/admin"
+require "models/admin/account"
+require "models/admin/randomly_named_c1"
+require "models/admin/user"
+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/randomly_named_c1"
+require "models/reply"
+require "models/ship"
+require "models/task"
+require "models/topic"
+require "models/traffic_light"
+require "models/treasure"
+require "tempfile"
class FixturesTest < ActiveRecord::TestCase
+ include ConnectionHelper
+
self.use_instantiated_fixtures = true
self.use_transactional_tests = false
@@ -52,13 +59,271 @@ class FixturesTest < ActiveRecord::TestCase
end
end
+ class InsertQuerySubscriber
+ attr_reader :events
+
+ def initialize
+ @events = []
+ end
+
+ def call(_, _, _, _, values)
+ @events << values[:sql] if values[:sql] =~ /INSERT/
+ end
+ end
+
+ if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)
+ def test_bulk_insert
+ begin
+ subscriber = InsertQuerySubscriber.new
+ subscription = ActiveSupport::Notifications.subscribe("sql.active_record", subscriber)
+ create_fixtures("bulbs")
+ assert_equal 1, subscriber.events.size, "It takes one INSERT query to insert two fixtures"
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscription)
+ end
+ end
+
+ def test_bulk_insert_multiple_table_with_a_multi_statement_query
+ subscriber = InsertQuerySubscriber.new
+ subscription = ActiveSupport::Notifications.subscribe("sql.active_record", subscriber)
+
+ create_fixtures("bulbs", "authors", "computers")
+
+ expected_sql = <<~EOS.chop
+ INSERT INTO #{ActiveRecord::Base.connection.quote_table_name("bulbs")} .*
+ INSERT INTO #{ActiveRecord::Base.connection.quote_table_name("authors")} .*
+ INSERT INTO #{ActiveRecord::Base.connection.quote_table_name("computers")} .*
+ EOS
+ assert_equal 1, subscriber.events.size
+ assert_match(/#{expected_sql}/, subscriber.events.first)
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscription)
+ end
+
+ def test_bulk_insert_with_a_multi_statement_query_raises_an_exception_when_any_insert_fails
+ require "models/aircraft"
+
+ assert_equal false, Aircraft.columns_hash["wheels_count"].null
+ fixtures = {
+ "aircraft" => [
+ { "name" => "working_aircrafts", "wheels_count" => 2 },
+ { "name" => "broken_aircrafts", "wheels_count" => nil },
+ ]
+ }
+
+ assert_no_difference "Aircraft.count" do
+ assert_raises(ActiveRecord::NotNullViolation) do
+ ActiveRecord::Base.connection.insert_fixtures_set(fixtures)
+ end
+ end
+ end
+
+ def test_bulk_insert_with_a_multi_statement_query_in_a_nested_transaction
+ fixtures = {
+ "traffic_lights" => [
+ { "location" => "US", "state" => ["NY"], "long_state" => ["a"] },
+ ]
+ }
+
+ assert_difference "TrafficLight.count" do
+ ActiveRecord::Base.transaction do
+ conn = ActiveRecord::Base.connection
+ assert_equal 1, conn.open_transactions
+ conn.insert_fixtures_set(fixtures)
+ assert_equal 1, conn.open_transactions
+ end
+ end
+ end
+ end
+
+ if current_adapter?(:Mysql2Adapter)
+ def test_bulk_insert_with_multi_statements_enabled
+ run_without_connection do |orig_connection|
+ ActiveRecord::Base.establish_connection(
+ orig_connection.merge(flags: %w[MULTI_STATEMENTS])
+ )
+
+ fixtures = {
+ "traffic_lights" => [
+ { "location" => "US", "state" => ["NY"], "long_state" => ["a"] },
+ ]
+ }
+
+ ActiveRecord::Base.connection.stub(:supports_set_server_option?, false) do
+ assert_nothing_raised do
+ conn = ActiveRecord::Base.connection
+ conn.execute("SELECT 1; SELECT 2;")
+ conn.raw_connection.abandon_results!
+ end
+
+ assert_difference "TrafficLight.count" do
+ ActiveRecord::Base.transaction do
+ conn = ActiveRecord::Base.connection
+ assert_equal 1, conn.open_transactions
+ conn.insert_fixtures_set(fixtures)
+ assert_equal 1, conn.open_transactions
+ end
+ end
+
+ assert_nothing_raised do
+ conn = ActiveRecord::Base.connection
+ conn.execute("SELECT 1; SELECT 2;")
+ conn.raw_connection.abandon_results!
+ end
+ end
+ end
+ end
+
+ def test_bulk_insert_with_multi_statements_disabled
+ run_without_connection do |orig_connection|
+ ActiveRecord::Base.establish_connection(
+ orig_connection.merge(flags: [])
+ )
+
+ fixtures = {
+ "traffic_lights" => [
+ { "location" => "US", "state" => ["NY"], "long_state" => ["a"] },
+ ]
+ }
+
+ ActiveRecord::Base.connection.stub(:supports_set_server_option?, false) do
+ assert_raises(ActiveRecord::StatementInvalid) do
+ conn = ActiveRecord::Base.connection
+ conn.execute("SELECT 1; SELECT 2;")
+ conn.raw_connection.abandon_results!
+ end
+
+ assert_difference "TrafficLight.count" do
+ conn = ActiveRecord::Base.connection
+ conn.insert_fixtures_set(fixtures)
+ end
+
+ assert_raises(ActiveRecord::StatementInvalid) do
+ conn = ActiveRecord::Base.connection
+ conn.execute("SELECT 1; SELECT 2;")
+ conn.raw_connection.abandon_results!
+ end
+ end
+ end
+ end
+
+ def test_insert_fixtures_set_raises_an_error_when_max_allowed_packet_is_smaller_than_fixtures_set_size
+ conn = ActiveRecord::Base.connection
+ mysql_margin = 2
+ packet_size = 1024
+ bytes_needed_to_have_a_1024_bytes_fixture = 858
+ fixtures = {
+ "traffic_lights" => [
+ { "location" => "US", "state" => ["NY"], "long_state" => ["a" * bytes_needed_to_have_a_1024_bytes_fixture] },
+ ]
+ }
+
+ conn.stub(:max_allowed_packet, packet_size - mysql_margin) do
+ error = assert_raises(ActiveRecord::ActiveRecordError) { conn.insert_fixtures_set(fixtures) }
+ assert_match(/Fixtures set is too large #{packet_size}\./, error.message)
+ end
+ end
+
+ def test_insert_fixture_set_when_max_allowed_packet_is_bigger_than_fixtures_set_size
+ conn = ActiveRecord::Base.connection
+ packet_size = 1024
+ fixtures = {
+ "traffic_lights" => [
+ { "location" => "US", "state" => ["NY"], "long_state" => ["a" * 51] },
+ ]
+ }
+
+ conn.stub(:max_allowed_packet, packet_size) do
+ assert_difference "TrafficLight.count" do
+ conn.insert_fixtures_set(fixtures)
+ end
+ end
+ end
+
+ def test_insert_fixtures_set_split_the_total_sql_into_two_chunks_smaller_than_max_allowed_packet
+ subscriber = InsertQuerySubscriber.new
+ subscription = ActiveSupport::Notifications.subscribe("sql.active_record", subscriber)
+ conn = ActiveRecord::Base.connection
+ packet_size = 1024
+ fixtures = {
+ "traffic_lights" => [
+ { "location" => "US", "state" => ["NY"], "long_state" => ["a" * 450] },
+ ],
+ "comments" => [
+ { "post_id" => 1, "body" => "a" * 450 },
+ ]
+ }
+
+ conn.stub(:max_allowed_packet, packet_size) do
+ conn.insert_fixtures_set(fixtures)
+
+ assert_equal 2, subscriber.events.size
+ assert_operator subscriber.events.first.bytesize, :<, packet_size
+ assert_operator subscriber.events.second.bytesize, :<, packet_size
+ end
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscription)
+ end
+
+ def test_insert_fixtures_set_concat_total_sql_into_a_single_packet_smaller_than_max_allowed_packet
+ subscriber = InsertQuerySubscriber.new
+ subscription = ActiveSupport::Notifications.subscribe("sql.active_record", subscriber)
+ conn = ActiveRecord::Base.connection
+ packet_size = 1024
+ fixtures = {
+ "traffic_lights" => [
+ { "location" => "US", "state" => ["NY"], "long_state" => ["a" * 200] },
+ ],
+ "comments" => [
+ { "post_id" => 1, "body" => "a" * 200 },
+ ]
+ }
+
+ conn.stub(:max_allowed_packet, packet_size) do
+ assert_difference ["TrafficLight.count", "Comment.count"], +1 do
+ conn.insert_fixtures_set(fixtures)
+ end
+ end
+ assert_equal 1, subscriber.events.size
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscription)
+ end
+ end
+
+ def test_auto_value_on_primary_key
+ fixtures = [
+ { "name" => "first", "wheels_count" => 2 },
+ { "name" => "second", "wheels_count" => 3 }
+ ]
+ conn = ActiveRecord::Base.connection
+ assert_nothing_raised do
+ conn.insert_fixtures_set({ "aircraft" => fixtures }, ["aircraft"])
+ end
+ result = conn.select_all("SELECT name, wheels_count FROM aircraft ORDER BY id")
+ assert_equal fixtures, result.to_a
+ end
+
+ def test_deprecated_insert_fixtures
+ fixtures = [
+ { "name" => "first", "wheels_count" => 2 },
+ { "name" => "second", "wheels_count" => 3 }
+ ]
+ conn = ActiveRecord::Base.connection
+ conn.delete("DELETE FROM aircraft")
+ assert_deprecated do
+ conn.insert_fixtures(fixtures, "aircraft")
+ end
+ result = conn.select_all("SELECT name, wheels_count FROM aircraft ORDER BY id")
+ assert_equal fixtures, result.to_a
+ end
+
def test_broken_yaml_exception
- badyaml = Tempfile.new ['foo', '.yml']
- badyaml.write 'a: : '
+ badyaml = Tempfile.new ["foo", ".yml"]
+ badyaml.write "a: : "
badyaml.flush
dir = File.dirname badyaml.path
- name = File.basename badyaml.path, '.yml'
+ name = File.basename badyaml.path, ".yml"
assert_raises(ActiveRecord::Fixture::FormatError) do
ActiveRecord::FixtureSet.create_fixtures(dir, name)
end
@@ -69,8 +334,8 @@ class FixturesTest < ActiveRecord::TestCase
def test_create_fixtures
fixtures = ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT, "parrots")
- assert Parrot.find_by_name('Curious George'), 'George is not in the database'
- assert fixtures.detect { |f| f.name == 'parrots' }, "no fixtures named 'parrots' in #{fixtures.map(&:name).inspect}"
+ assert Parrot.find_by_name("Curious George"), "George is not in the database"
+ assert fixtures.detect { |f| f.name == "parrots" }, "no fixtures named 'parrots' in #{fixtures.map(&:name).inspect}"
end
def test_multiple_clean_fixtures
@@ -81,10 +346,10 @@ class FixturesTest < ActiveRecord::TestCase
end
def test_create_symbol_fixtures
- fixtures = ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT, :collections, :collections => Course) { Course.connection }
+ fixtures = ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT, :collections, collections: Course) { Course.connection }
- assert Course.find_by_name('Collection'), 'course is not in the database'
- assert fixtures.detect { |f| f.name == 'collections' }, "no fixtures named 'collections' in #{fixtures.map(&:name).inspect}"
+ assert Course.find_by_name("Collection"), "course is not in the database"
+ assert fixtures.detect { |f| f.name == "collections" }, "no fixtures named 'collections' in #{fixtures.map(&:name).inspect}"
end
def test_attributes
@@ -93,6 +358,24 @@ class FixturesTest < ActiveRecord::TestCase
assert_nil(topics["second"]["author_email_address"])
end
+ def test_no_args_returns_all
+ all_topics = topics
+ assert_equal 5, all_topics.length
+ assert_equal "The First Topic", all_topics.first["title"]
+ assert_equal 5, all_topics.last.id
+ end
+
+ def test_no_args_record_returns_all_without_array
+ all_binaries = binaries
+ assert_kind_of(Array, all_binaries)
+ assert_equal 2, binaries.length
+ end
+
+ def test_nil_raises
+ assert_raise(StandardError) { topics(nil) }
+ assert_raise(StandardError) { topics([nil]) }
+ end
+
def test_inserts
create_fixtures("topics")
first_row = ActiveRecord::Base.connection.select_one("SELECT * FROM topics WHERE author_name = 'David'")
@@ -102,64 +385,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
@@ -170,7 +451,7 @@ class FixturesTest < ActiveRecord::TestCase
def test_logger_level_invariant
level = ActiveRecord::Base.logger.level
- create_fixtures('topics')
+ create_fixtures("topics")
assert_equal level, ActiveRecord::Base.logger.level
end
@@ -184,7 +465,6 @@ class FixturesTest < ActiveRecord::TestCase
end
def test_fixtures_from_root_yml_with_instantiation
- # assert_equal 2, @accounts.size
assert_equal 50, @unknown.credit_limit
end
@@ -193,37 +473,63 @@ class FixturesTest < ActiveRecord::TestCase
end
def test_empty_yaml_fixture
- assert_not_nil ActiveRecord::FixtureSet.new( Account.connection, "accounts", Account, FIXTURES_ROOT + "/naked/yml/accounts")
+ assert_not_nil ActiveRecord::FixtureSet.new(nil, "accounts", Account, FIXTURES_ROOT + "/naked/yml/accounts")
end
def test_empty_yaml_fixture_with_a_comment_in_it
- assert_not_nil ActiveRecord::FixtureSet.new( Account.connection, "companies", Company, FIXTURES_ROOT + "/naked/yml/companies")
+ assert_not_nil ActiveRecord::FixtureSet.new(nil, "companies", Company, FIXTURES_ROOT + "/naked/yml/companies")
end
def test_nonexistent_fixture_file
nonexistent_fixture_path = FIXTURES_ROOT + "/imnothere"
- #sanity check to make sure that this file never exists
- assert Dir[nonexistent_fixture_path+"*"].empty?
+ # sanity check to make sure that this file never exists
+ assert_empty Dir[nonexistent_fixture_path + "*"]
assert_raise(Errno::ENOENT) do
- ActiveRecord::FixtureSet.new( Account.connection, "companies", Company, nonexistent_fixture_path)
+ ActiveRecord::FixtureSet.new(nil, "companies", Company, nonexistent_fixture_path)
end
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(nil, "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(nil, "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
+ e = assert_raise(ActiveRecord::Fixture::FixtureError) do
+ ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT + "/naked/yml", "parrots")
+ end
+
+ if current_adapter?(:SQLite3Adapter)
+ assert_equal(%(table "parrots" has no column named "arrr".), e.message)
+ else
+ assert_equal(%(table "parrots" has no columns named "arrr", "foobar".), e.message)
end
end
+ def test_yaml_file_with_symbol_columns
+ ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT + "/naked/yml", "trees")
+ end
+
def test_omap_fixtures
assert_nothing_raised do
- fixtures = ActiveRecord::FixtureSet.new(Account.connection, 'categories', Category, FIXTURES_ROOT + "/categories_ordered")
+ fixtures = ActiveRecord::FixtureSet.new(nil, "categories", Category, FIXTURES_ROOT + "/categories_ordered")
fixtures.each.with_index do |(name, fixture), i|
assert_equal "fixture_no_#{i}", name
- assert_equal "Category #{i}", fixture['name']
+ assert_equal "Category #{i}", fixture["name"]
end
end
end
@@ -239,10 +545,11 @@ class FixturesTest < ActiveRecord::TestCase
end
def test_binary_in_fixtures
- data = File.open(ASSETS_ROOT + "/flowers.jpg", 'rb') { |f| f.read }
- data.force_encoding('ASCII-8BIT')
+ data = File.open(ASSETS_ROOT + "/flowers.jpg", "rb") { |f| f.read }
+ data.force_encoding("ASCII-8BIT")
data.freeze
assert_equal data, @flowers.data
+ assert_equal data, @binary_helper.data
end
def test_serialized_fixtures
@@ -250,26 +557,27 @@ class FixturesTest < ActiveRecord::TestCase
end
def test_fixtures_are_set_up_with_database_env_variable
- db_url_tmp = ENV['DATABASE_URL']
- ENV['DATABASE_URL'] = "sqlite3::memory:"
- ActiveRecord::Base.stubs(:configurations).returns({})
- test_case = Class.new(ActiveRecord::TestCase) do
- fixtures :accounts
-
- def test_fixtures
- assert accounts(:signals37)
+ db_url_tmp = ENV["DATABASE_URL"]
+ ENV["DATABASE_URL"] = "sqlite3::memory:"
+ ActiveRecord::Base.stub(:configurations, {}) do
+ test_case = Class.new(ActiveRecord::TestCase) do
+ fixtures :accounts
+
+ def test_fixtures
+ assert accounts(:signals37)
+ end
end
- end
- result = test_case.new(:test_fixtures).run
+ result = test_case.new(:test_fixtures).run
- assert result.passed?, "Expected #{result.name} to pass:\n#{result}"
+ assert result.passed?, "Expected #{result.name} to pass:\n#{result}"
+ end
ensure
- ENV['DATABASE_URL'] = db_url_tmp
+ ENV["DATABASE_URL"] = db_url_tmp
end
end
-class HasManyThroughFixture < ActiveSupport::TestCase
+class HasManyThroughFixture < ActiveRecord::TestCase
def make_model(name)
Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } }
end
@@ -280,17 +588,17 @@ class HasManyThroughFixture < ActiveSupport::TestCase
treasure = make_model "Treasure"
pt.table_name = "parrots_treasures"
- pt.belongs_to :parrot, :anonymous_class => parrot
- pt.belongs_to :treasure, :anonymous_class => treasure
+ pt.belongs_to :parrot, anonymous_class: parrot
+ pt.belongs_to :treasure, anonymous_class: treasure
- parrot.has_many :parrot_treasures, :anonymous_class => pt
- parrot.has_many :treasures, :through => :parrot_treasures
+ parrot.has_many :parrot_treasures, anonymous_class: pt
+ parrot.has_many :treasures, through: :parrot_treasures
- parrots = File.join FIXTURES_ROOT, 'parrots'
+ parrots = File.join FIXTURES_ROOT, "parrots"
- fs = ActiveRecord::FixtureSet.new parrot.connection, "parrots", parrot, parrots
+ fs = ActiveRecord::FixtureSet.new(nil, "parrots", parrot, parrots)
rows = fs.table_rows
- assert_equal load_has_and_belongs_to_many['parrots_treasures'], rows['parrots_treasures']
+ assert_equal load_has_and_belongs_to_many["parrots_treasures"], rows["parrots_treasures"]
end
def test_has_many_through_with_renamed_table
@@ -298,26 +606,30 @@ class HasManyThroughFixture < ActiveSupport::TestCase
parrot = make_model "Parrot"
treasure = make_model "Treasure"
- pt.belongs_to :parrot, :anonymous_class => parrot
- pt.belongs_to :treasure, :anonymous_class => treasure
+ pt.belongs_to :parrot, anonymous_class: parrot
+ pt.belongs_to :treasure, anonymous_class: treasure
- parrot.has_many :parrot_treasures, :anonymous_class => pt
- parrot.has_many :treasures, :through => :parrot_treasures
+ parrot.has_many :parrot_treasures, anonymous_class: pt
+ parrot.has_many :treasures, through: :parrot_treasures
- parrots = File.join FIXTURES_ROOT, 'parrots'
+ parrots = File.join FIXTURES_ROOT, "parrots"
- fs = ActiveRecord::FixtureSet.new parrot.connection, "parrots", parrot, parrots
+ fs = ActiveRecord::FixtureSet.new(nil, "parrots", parrot, parrots)
rows = fs.table_rows
- assert_equal load_has_and_belongs_to_many['parrots_treasures'], rows['parrot_treasures']
+ assert_equal load_has_and_belongs_to_many["parrots_treasures"], rows["parrot_treasures"]
+ end
+
+ def test_has_and_belongs_to_many_order
+ assert_equal ["parrots", "parrots_treasures"], load_has_and_belongs_to_many.keys
end
def load_has_and_belongs_to_many
parrot = make_model "Parrot"
parrot.has_and_belongs_to_many :treasures
- parrots = File.join FIXTURES_ROOT, 'parrots'
+ parrots = File.join FIXTURES_ROOT, "parrots"
- fs = ActiveRecord::FixtureSet.new parrot.connection, "parrots", parrot, parrots
+ fs = ActiveRecord::FixtureSet.new(nil, "parrots", parrot, parrots)
fs.table_rows
end
end
@@ -326,9 +638,10 @@ if Account.connection.respond_to?(:reset_pk_sequence!)
class FixturesResetPkSequenceTest < ActiveRecord::TestCase
fixtures :accounts
fixtures :companies
+ self.use_transactional_tests = false
def setup
- @instances = [Account.new(:credit_limit => 50), Company.new(:name => 'RoR Consulting'), Course.new(name: 'Test')]
+ @instances = [Account.new(credit_limit: 50), Company.new(name: "RoR Consulting"), Course.new(name: "Test")]
ActiveRecord::FixtureSet.reset_cache # make sure tables get reinitialized
end
@@ -357,7 +670,7 @@ if Account.connection.respond_to?(:reset_pk_sequence!)
def test_create_fixtures_resets_sequences_when_not_cached
@instances.each do |instance|
max_id = create_fixtures(instance.class.table_name).first.fixtures.inject(0) do |_max_id, (_, fixture)|
- fixture_id = fixture['id'].to_i
+ fixture_id = fixture["id"].to_i
fixture_id > _max_id ? fixture_id : _max_id
end
@@ -374,14 +687,14 @@ class FixturesWithoutInstantiationTest < ActiveRecord::TestCase
fixtures :topics, :developers, :accounts
def test_without_complete_instantiation
- assert !defined?(@first)
- assert !defined?(@topics)
- assert !defined?(@developers)
- assert !defined?(@accounts)
+ assert_not defined?(@first)
+ assert_not defined?(@topics)
+ assert_not defined?(@developers)
+ assert_not defined?(@accounts)
end
def test_fixtures_from_root_yml_without_instantiation
- assert !defined?(@unknown), "@unknown is not defined"
+ assert_not defined?(@unknown), "@unknown is not defined"
end
def test_visibility_of_accessor_method
@@ -401,9 +714,11 @@ class FixturesWithoutInstantiationTest < ActiveRecord::TestCase
end
def test_reloading_fixtures_through_accessor_methods
+ topic = Struct.new(:title)
assert_equal "The First Topic", topics(:first).title
- @loaded_fixtures['topics']['first'].expects(:find).returns(stub(:title => "Fresh Topic!"))
- assert_equal "Fresh Topic!", topics(:first, true).title
+ assert_called(@loaded_fixtures["topics"]["first"], :find, returns: topic.new("Fresh Topic!")) do
+ assert_equal "Fresh Topic!", topics(:first, true).title
+ end
end
end
@@ -414,7 +729,7 @@ class FixturesWithoutInstanceInstantiationTest < ActiveRecord::TestCase
fixtures :topics, :developers, :accounts
def test_without_instance_instantiation
- assert !defined?(@first), "@first is not defined"
+ assert_not defined?(@first), "@first is not defined"
end
end
@@ -466,7 +781,6 @@ class SetupSubclassTest < SetupTest
end
end
-
class OverlappingFixturesTest < ActiveRecord::TestCase
fixtures :topics, :developers
fixtures :developers, :accounts
@@ -497,18 +811,50 @@ class OverRideFixtureMethodTest < ActiveRecord::TestCase
def topics(name)
topic = super
- topic.title = 'omg'
+ topic.title = "omg"
topic
end
def test_fixture_methods_can_be_overridden
x = topics :first
- assert_equal 'omg', x.title
+ assert_equal "omg", x.title
+ end
+end
+
+class FixtureWithSetModelClassTest < ActiveRecord::TestCase
+ fixtures :other_posts, :other_comments
+
+ # Set to false to blow away fixtures cache and ensure our fixtures are loaded
+ # and thus takes into account the +set_model_class+.
+ self.use_transactional_tests = false
+
+ def test_uses_fixture_class_defined_in_yaml
+ assert_kind_of Post, other_posts(:second_welcome)
+ end
+
+ def test_loads_the_associations_to_fixtures_with_set_model_class
+ post = other_posts(:second_welcome)
+ comment = other_comments(:second_greetings)
+ assert_equal [comment], post.comments
+ assert_equal post, comment.post
+ end
+end
+
+class SetFixtureClassPrevailsTest < ActiveRecord::TestCase
+ set_fixture_class bad_posts: Post
+ fixtures :bad_posts
+
+ # Set to false to blow away fixtures cache and ensure our fixtures are loaded
+ # and thus takes into account the +set_model_class+.
+ self.use_transactional_tests = false
+
+ def test_uses_set_fixture_class
+ assert_kind_of Post, bad_posts(:bad_welcome)
end
end
class CheckSetTableNameFixturesTest < ActiveRecord::TestCase
- set_fixture_class :funny_jokes => Joke
+ set_fixture_class funny_jokes: Joke
fixtures :funny_jokes
# Set to false to blow away fixtures cache and ensure our fixtures are loaded
# and thus takes into account our set_fixture_class
@@ -520,7 +866,7 @@ class CheckSetTableNameFixturesTest < ActiveRecord::TestCase
end
class FixtureNameIsNotTableNameFixturesTest < ActiveRecord::TestCase
- set_fixture_class :items => Book
+ set_fixture_class items: Book
fixtures :items
# Set to false to blow away fixtures cache and ensure our fixtures are loaded
# and thus takes into account our set_fixture_class
@@ -532,7 +878,7 @@ class FixtureNameIsNotTableNameFixturesTest < ActiveRecord::TestCase
end
class FixtureNameIsNotTableNameMultipleFixturesTest < ActiveRecord::TestCase
- set_fixture_class :items => Book, :funny_jokes => Joke
+ set_fixture_class items: Book, funny_jokes: Joke
fixtures :items, :funny_jokes
# Set to false to blow away fixtures cache and ensure our fixtures are loaded
# and thus takes into account our set_fixture_class
@@ -548,7 +894,7 @@ class FixtureNameIsNotTableNameMultipleFixturesTest < ActiveRecord::TestCase
end
class CustomConnectionFixturesTest < ActiveRecord::TestCase
- set_fixture_class :courses => Course
+ set_fixture_class courses: Course
fixtures :courses
self.use_transactional_tests = false
@@ -563,7 +909,7 @@ class CustomConnectionFixturesTest < ActiveRecord::TestCase
end
class TransactionalFixturesOnCustomConnectionTest < ActiveRecord::TestCase
- set_fixture_class :courses => Course
+ set_fixture_class courses: Course
fixtures :courses
self.use_transactional_tests = true
@@ -577,6 +923,66 @@ class TransactionalFixturesOnCustomConnectionTest < ActiveRecord::TestCase
end
end
+class TransactionalFixturesOnConnectionNotification < ActiveRecord::TestCase
+ self.use_transactional_tests = true
+ self.use_instantiated_fixtures = false
+
+ def test_transaction_created_on_connection_notification
+ connection = Class.new do
+ attr_accessor :pool
+
+ def transaction_open?; end
+ def begin_transaction(*args); end
+ def rollback_transaction(*args); end
+ end.new
+
+ connection.pool = Class.new do
+ def lock_thread=(lock_thread); end
+ end.new
+
+ assert_called_with(connection, :begin_transaction, [joinable: false]) do
+ fire_connection_notification(connection)
+ end
+ end
+
+ def test_notification_established_transactions_are_rolled_back
+ 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); 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")
+ end
+
+ private
+
+ def fire_connection_notification(connection)
+ assert_called_with(ActiveRecord::Base.connection_handler, :retrieve_connection, ["book"], returns: connection) do
+ message_bus = ActiveSupport::Notifications.instrumenter
+ payload = {
+ spec_name: "book",
+ config: nil,
+ connection_id: connection.object_id
+ }
+
+ message_bus.instrument("!connection.active_record", payload) { }
+ end
+ end
+end
+
class InvalidTableNameFixturesTest < ActiveRecord::TestCase
fixtures :funny_jokes
# Set to false to blow away fixtures cache and ensure our fixtures are loaded
@@ -591,7 +997,7 @@ class InvalidTableNameFixturesTest < ActiveRecord::TestCase
end
class CheckEscapedYamlFixturesTest < ActiveRecord::TestCase
- set_fixture_class :funny_jokes => Joke
+ set_fixture_class funny_jokes: Joke
fixtures :funny_jokes
# Set to false to blow away fixtures cache and ensure our fixtures are loaded
# and thus takes into account our set_fixture_class
@@ -634,7 +1040,7 @@ class FixturesBrokenRollbackTest < ActiveRecord::TestCase
private
def load_fixtures(config)
- raise 'argh'
+ raise "argh"
end
end
@@ -644,7 +1050,7 @@ class LoadAllFixturesTest < ActiveRecord::TestCase
self.class.fixtures :all
if File.symlink? FIXTURES_ROOT + "/all/admin"
- assert_equal %w(admin/accounts admin/users developers people tasks), fixture_table_names.sort
+ assert_equal %w(admin/accounts admin/users developers namespaced/accounts people tasks), fixture_table_names.sort
end
ensure
ActiveRecord::FixtureSet.reset_cache
@@ -653,11 +1059,11 @@ end
class LoadAllFixturesWithPathnameTest < ActiveRecord::TestCase
def test_all_there
- self.class.fixture_path = Pathname.new(FIXTURES_ROOT).join('all')
+ self.class.fixture_path = Pathname.new(FIXTURES_ROOT).join("all")
self.class.fixtures :all
if File.symlink? FIXTURES_ROOT + "/all/admin"
- assert_equal %w(admin/accounts admin/users developers people tasks), fixture_table_names.sort
+ assert_equal %w(admin/accounts admin/users developers namespaced/accounts people tasks), fixture_table_names.sort
end
ensure
ActiveRecord::FixtureSet.reset_cache
@@ -666,7 +1072,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
@@ -675,28 +1081,30 @@ class FasterFixturesTest < ActiveRecord::TestCase
end
def test_cache
- assert ActiveRecord::FixtureSet.fixture_is_cached?(ActiveRecord::Base.connection, 'categories')
- assert ActiveRecord::FixtureSet.fixture_is_cached?(ActiveRecord::Base.connection, 'authors')
+ assert ActiveRecord::FixtureSet.fixture_is_cached?(ActiveRecord::Base.connection, "categories")
+ assert ActiveRecord::FixtureSet.fixture_is_cached?(ActiveRecord::Base.connection, "authors")
assert_no_queries do
- create_fixtures('categories')
- create_fixtures('authors')
+ create_fixtures("categories")
+ create_fixtures("authors")
end
- load_extra_fixture('posts')
- assert ActiveRecord::FixtureSet.fixture_is_cached?(ActiveRecord::Base.connection, 'posts')
+ load_extra_fixture("posts")
+ assert ActiveRecord::FixtureSet.fixture_is_cached?(ActiveRecord::Base.connection, "posts")
self.class.setup_fixture_accessors :posts
- assert_equal 'Welcome to the weblog', posts(:welcome).title
+ assert_equal "Welcome to the weblog", posts(:welcome).title
end
end
class FoxyFixturesTest < ActiveRecord::TestCase
+ # Set to false to blow away fixtures cache and ensure our fixtures are loaded
+ self.use_transactional_tests = false
fixtures :parrots, :parrots_pirates, :pirates, :treasures, :mateys, :ships, :computers,
:developers, :"admin/accounts", :"admin/users", :live_parrots, :dead_parrots, :books
- if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
- require 'models/uuid_parent'
- require 'models/uuid_child'
+ if ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
+ require "models/uuid_parent"
+ require "models/uuid_child"
fixtures :uuid_parents, :uuid_children
end
@@ -713,8 +1121,8 @@ class FoxyFixturesTest < ActiveRecord::TestCase
assert_equal 207281424, ActiveRecord::FixtureSet.identify(:ruby)
assert_equal 1066363776, ActiveRecord::FixtureSet.identify(:sapphire_2)
- assert_equal 'f92b6bda-0d0d-5fe1-9124-502b18badded', ActiveRecord::FixtureSet.identify(:daddy, :uuid)
- assert_equal 'b4b10018-ad47-595d-b42f-d8bdaa6d01bf', ActiveRecord::FixtureSet.identify(:sonny, :uuid)
+ assert_equal "f92b6bda-0d0d-5fe1-9124-502b18badded", ActiveRecord::FixtureSet.identify(:daddy, :uuid)
+ assert_equal "b4b10018-ad47-595d-b42f-d8bdaa6d01bf", ActiveRecord::FixtureSet.identify(:sonny, :uuid)
end
TIMESTAMP_COLUMNS = %w(created_at created_on updated_at updated_on)
@@ -783,13 +1191,13 @@ class FoxyFixturesTest < ActiveRecord::TestCase
def test_supports_inline_habtm
assert(parrots(:george).treasures.include?(treasures(:diamond)))
assert(parrots(:george).treasures.include?(treasures(:sapphire)))
- assert(!parrots(:george).treasures.include?(treasures(:ruby)))
+ assert_not(parrots(:george).treasures.include?(treasures(:ruby)))
end
def test_supports_inline_habtm_with_specified_id
assert(parrots(:polly).treasures.include?(treasures(:ruby)))
assert(parrots(:polly).treasures.include?(treasures(:sapphire)))
- assert(!parrots(:polly).treasures.include?(treasures(:diamond)))
+ assert_not(parrots(:polly).treasures.include?(treasures(:diamond)))
end
def test_supports_yaml_arrays
@@ -812,7 +1220,7 @@ class FoxyFixturesTest < ActiveRecord::TestCase
assert_equal("X marks the spot!", pirates(:mark).catchphrase)
end
- def test_supports_label_interpolation_for_fixnum_label
+ def test_supports_label_interpolation_for_integer_label
assert_equal("#1 pirate!", pirates(1).catchphrase)
end
@@ -839,15 +1247,15 @@ class FoxyFixturesTest < ActiveRecord::TestCase
end
def test_namespaced_models
- assert admin_accounts(:signals37).users.include?(admin_users(:david))
+ assert_includes admin_accounts(:signals37).users, admin_users(:david)
assert_equal 2, admin_accounts(:signals37).users.size
end
def test_resolves_enums
- assert books(:awdr).published?
- assert books(:awdr).read?
- assert books(:rfr).proposed?
- assert books(:ddd).published?
+ assert_predicate books(:awdr), :published?
+ assert_predicate books(:awdr), :read?
+ assert_predicate books(:rfr), :proposed?
+ assert_predicate books(:ddd), :published?
end
end
@@ -864,14 +1272,14 @@ end
class CustomNameForFixtureOrModelTest < ActiveRecord::TestCase
ActiveRecord::FixtureSet.reset_cache
- set_fixture_class :randomly_named_a9 =>
+ set_fixture_class :randomly_named_a9 =>
ClassNameThatDoesNotFollowCONVENTIONS,
:'admin/randomly_named_a9' =>
Admin::ClassNameThatDoesNotFollowCONVENTIONS1,
- 'admin/randomly_named_b0' =>
+ "admin/randomly_named_b0" =>
Admin::ClassNameThatDoesNotFollowCONVENTIONS2
- fixtures :randomly_named_a9, 'admin/randomly_named_a9',
+ fixtures :randomly_named_a9, "admin/randomly_named_a9",
:'admin/randomly_named_b0'
def test_named_accessor_for_randomly_named_fixture_and_class
@@ -887,8 +1295,8 @@ class CustomNameForFixtureOrModelTest < ActiveRecord::TestCase
end
def test_table_name_is_defined_in_the_model
- assert_equal 'randomly_named_table2', ActiveRecord::FixtureSet::all_loaded_fixtures["admin/randomly_named_a9"].table_name
- assert_equal 'randomly_named_table2', Admin::ClassNameThatDoesNotFollowCONVENTIONS1.table_name
+ assert_equal "randomly_named_table2", ActiveRecord::FixtureSet.all_loaded_fixtures["admin/randomly_named_a9"].table_name
+ assert_equal "randomly_named_table2", Admin::ClassNameThatDoesNotFollowCONVENTIONS1.table_name
end
end
@@ -913,3 +1321,46 @@ class FixturesWithAbstractBelongsTo < ActiveRecord::TestCase
assert_equal pirates(:blackbeard), doubloons(:blackbeards_doubloon).pirate
end
end
+
+class FixtureClassNamesTest < ActiveRecord::TestCase
+ def setup
+ @saved_cache = fixture_class_names.dup
+ end
+
+ def teardown
+ fixture_class_names.replace(@saved_cache)
+ end
+
+ test "fixture_class_names returns nil for unregistered identifier" do
+ 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
+
+class NilFixturePathTest < ActiveRecord::TestCase
+ test "raises an error when all fixtures loaded" do
+ error = assert_raises(StandardError) do
+ TestCase = Class.new(ActiveRecord::TestCase)
+ TestCase.class_eval do
+ self.fixture_path = nil
+ fixtures :all
+ end
+ end
+ assert_equal <<~MSG.squish, error.message
+ No fixture path found.
+ Please set `NilFixturePathTest::TestCase.fixture_path`.
+ MSG
+ end
+end
diff --git a/activerecord/test/cases/forbidden_attributes_protection_test.rb b/activerecord/test/cases/forbidden_attributes_protection_test.rb
index f4e7646f03..101fa118c8 100644
--- a/activerecord/test/cases/forbidden_attributes_protection_test.rb
+++ b/activerecord/test/cases/forbidden_attributes_protection_test.rb
@@ -1,14 +1,22 @@
-require 'cases/helper'
-require 'active_support/core_ext/hash/indifferent_access'
-require 'models/person'
-require 'models/company'
+# frozen_string_literal: true
-class ProtectedParams < ActiveSupport::HashWithIndifferentAccess
+require "cases/helper"
+require "active_support/core_ext/hash/indifferent_access"
+
+require "models/company"
+require "models/person"
+require "models/ship"
+require "models/ship_part"
+require "models/treasure"
+
+class ProtectedParams
attr_accessor :permitted
alias :permitted? :permitted
+ delegate :keys, :key?, :has_key?, :empty?, to: :@parameters
+
def initialize(attributes)
- super(attributes)
+ @parameters = attributes.with_indifferent_access
@permitted = false
end
@@ -17,6 +25,18 @@ class ProtectedParams < ActiveSupport::HashWithIndifferentAccess
self
end
+ def [](key)
+ @parameters[key]
+ end
+
+ def to_h
+ @parameters
+ end
+
+ def stringify_keys
+ dup
+ end
+
def dup
super.tap do |duplicate|
duplicate.instance_variable_set :@permitted, @permitted
@@ -26,40 +46,40 @@ end
class ForbiddenAttributesProtectionTest < ActiveRecord::TestCase
def test_forbidden_attributes_cannot_be_used_for_mass_assignment
- params = ProtectedParams.new(first_name: 'Guille', gender: 'm')
+ params = ProtectedParams.new(first_name: "Guille", gender: "m")
assert_raises(ActiveModel::ForbiddenAttributesError) do
Person.new(params)
end
end
def test_permitted_attributes_can_be_used_for_mass_assignment
- params = ProtectedParams.new(first_name: 'Guille', gender: 'm')
+ params = ProtectedParams.new(first_name: "Guille", gender: "m")
params.permit!
person = Person.new(params)
- assert_equal 'Guille', person.first_name
- assert_equal 'm', person.gender
+ assert_equal "Guille", person.first_name
+ assert_equal "m", person.gender
end
def test_forbidden_attributes_cannot_be_used_for_sti_inheritance_column
- params = ProtectedParams.new(type: 'Client')
+ params = ProtectedParams.new(type: "Client")
assert_raises(ActiveModel::ForbiddenAttributesError) do
Company.new(params)
end
end
def test_permitted_attributes_can_be_used_for_sti_inheritance_column
- params = ProtectedParams.new(type: 'Client')
+ params = ProtectedParams.new(type: "Client")
params.permit!
person = Company.new(params)
assert_equal person.class, Client
end
def test_regular_hash_should_still_be_used_for_mass_assignment
- person = Person.new(first_name: 'Guille', gender: 'm')
+ person = Person.new(first_name: "Guille", gender: "m")
- assert_equal 'Guille', person.first_name
- assert_equal 'm', person.gender
+ assert_equal "Guille", person.first_name
+ assert_equal "m", person.gender
end
def test_blank_attributes_should_not_raise
@@ -68,32 +88,79 @@ class ForbiddenAttributesProtectionTest < ActiveRecord::TestCase
end
def test_create_with_checks_permitted
- params = ProtectedParams.new(first_name: 'Guille', gender: 'm')
+ params = ProtectedParams.new(first_name: "Guille", gender: "m")
assert_raises(ActiveModel::ForbiddenAttributesError) do
Person.create_with(params).create!
end
end
+ def test_create_with_works_with_permitted_params
+ params = ProtectedParams.new(first_name: "Guille").permit!
+
+ person = Person.create_with(params).create!
+ assert_equal "Guille", person.first_name
+ end
+
def test_create_with_works_with_params_values
- params = ProtectedParams.new(first_name: 'Guille')
+ params = ProtectedParams.new(first_name: "Guille")
person = Person.create_with(first_name: params[:first_name]).create!
- assert_equal 'Guille', person.first_name
+ assert_equal "Guille", person.first_name
end
def test_where_checks_permitted
- params = ProtectedParams.new(first_name: 'Guille', gender: 'm')
+ params = ProtectedParams.new(first_name: "Guille", gender: "m")
assert_raises(ActiveModel::ForbiddenAttributesError) do
Person.where(params).create!
end
end
+ def test_where_works_with_permitted_params
+ params = ProtectedParams.new(first_name: "Guille").permit!
+
+ person = Person.where(params).create!
+ assert_equal "Guille", person.first_name
+ end
+
def test_where_works_with_params_values
- params = ProtectedParams.new(first_name: 'Guille')
+ params = ProtectedParams.new(first_name: "Guille")
person = Person.where(first_name: params[:first_name]).create!
- assert_equal 'Guille', person.first_name
+ assert_equal "Guille", person.first_name
+ end
+
+ def test_where_not_checks_permitted
+ params = ProtectedParams.new(first_name: "Guille", gender: "m")
+
+ assert_raises(ActiveModel::ForbiddenAttributesError) do
+ Person.where().not(params)
+ end
+ end
+
+ def test_where_not_works_with_permitted_params
+ params = ProtectedParams.new(first_name: "Guille").permit!
+ Person.create!(params)
+ assert_empty Person.where.not(params).select { |p| p.first_name == "Guille" }
+ end
+
+ def test_strong_params_style_objects_work_with_singular_associations
+ params = ProtectedParams.new(name: "Stern", ship_attributes: ProtectedParams.new(name: "The Black Rock").permit!).permit!
+ part = ShipPart.new(params)
+
+ assert_equal "Stern", part.name
+ assert_equal "The Black Rock", part.ship.name
+ end
+
+ def test_strong_params_style_objects_work_with_collection_associations
+ params = ProtectedParams.new(
+ trinkets_attributes: ProtectedParams.new(
+ "0" => ProtectedParams.new(name: "Necklace").permit!,
+ "1" => ProtectedParams.new(name: "Spoon").permit!)).permit!
+ part = ShipPart.new(params)
+
+ assert_equal "Necklace", part.trinkets[0].name
+ assert_equal "Spoon", part.trinkets[1].name
end
end
diff --git a/activerecord/test/cases/habtm_destroy_order_test.rb b/activerecord/test/cases/habtm_destroy_order_test.rb
index 2ce0de360e..b15e1b48c4 100644
--- a/activerecord/test/cases/habtm_destroy_order_test.rb
+++ b/activerecord/test/cases/habtm_destroy_order_test.rb
@@ -1,24 +1,26 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "models/lesson"
require "models/student"
class HabtmDestroyOrderTest < ActiveRecord::TestCase
test "may not delete a lesson with students" do
- sicp = Lesson.new(:name => "SICP")
- ben = Student.new(:name => "Ben Bitdiddle")
+ sicp = Lesson.new(name: "SICP")
+ ben = Student.new(name: "Ben Bitdiddle")
sicp.students << ben
sicp.save!
assert_raises LessonError do
- assert_no_difference('Lesson.count') do
+ assert_no_difference("Lesson.count") do
sicp.destroy
end
end
- assert !sicp.destroyed?
+ assert_not_predicate sicp, :destroyed?
end
- test 'should not raise error if have foreign key in the join table' do
- student = Student.new(:name => "Ben Bitdiddle")
- lesson = Lesson.new(:name => "SICP")
+ test "should not raise error if have foreign key in the join table" do
+ student = Student.new(name: "Ben Bitdiddle")
+ lesson = Lesson.new(name: "SICP")
lesson.students << student
lesson.save!
assert_nothing_raised do
@@ -29,8 +31,8 @@ class HabtmDestroyOrderTest < ActiveRecord::TestCase
test "not destroying a student with lessons leaves student<=>lesson association intact" do
# test a normal before_destroy doesn't destroy the habtm joins
begin
- sicp = Lesson.new(:name => "SICP")
- ben = Student.new(:name => "Ben Bitdiddle")
+ sicp = Lesson.new(name: "SICP")
+ ben = Student.new(name: "Ben Bitdiddle")
# add a before destroy to student
Student.class_eval do
before_destroy do
@@ -40,7 +42,7 @@ class HabtmDestroyOrderTest < ActiveRecord::TestCase
ben.lessons << sicp
ben.save!
ben.destroy
- assert !ben.reload.lessons.empty?
+ assert_not_empty ben.reload.lessons
ensure
# get rid of it so Student is still like it was
Student.reset_callbacks(:destroy)
@@ -49,13 +51,13 @@ class HabtmDestroyOrderTest < ActiveRecord::TestCase
test "not destroying a lesson with students leaves student<=>lesson association intact" do
# test a more aggressive before_destroy doesn't destroy the habtm joins and still throws the exception
- sicp = Lesson.new(:name => "SICP")
- ben = Student.new(:name => "Ben Bitdiddle")
+ sicp = Lesson.new(name: "SICP")
+ ben = Student.new(name: "Ben Bitdiddle")
sicp.students << ben
sicp.save!
assert_raises LessonError do
sicp.destroy
end
- assert !sicp.reload.students.empty?
+ assert_not_empty sicp.reload.students
end
end
diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb
index 12c793c408..730cd663a2 100644
--- a/activerecord/test/cases/helper.rb
+++ b/activerecord/test/cases/helper.rb
@@ -1,18 +1,16 @@
-require File.expand_path('../../../../load_paths', __FILE__)
+# frozen_string_literal: true
-require 'config'
+require "config"
-require 'active_support/testing/autorun'
-require 'stringio'
+require "stringio"
-require 'active_record'
-require 'cases/test_case'
-require 'active_support/dependencies'
-require 'active_support/logger'
-require 'active_support/core_ext/string/strip'
+require "active_record"
+require "cases/test_case"
+require "active_support/dependencies"
+require "active_support/logger"
-require 'support/config'
-require 'support/connection'
+require "support/config"
+require "support/connection"
# TODO: Move all these random hacks into the ARTest namespace and into the support/ dir
@@ -28,10 +26,7 @@ I18n.enforce_available_locales = false
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
+QUOTED_TYPE = ActiveRecord::Base.connection.quote_column_name("type")
def current_adapter?(*types)
types.any? do |type|
@@ -45,24 +40,32 @@ def in_memory_db?
ActiveRecord::Base.connection_pool.spec.config[:database] == ":memory:"
end
-def mysql_56?
- current_adapter?(:MysqlAdapter, :Mysql2Adapter) &&
- ActiveRecord::Base.connection.send(:version).join(".") >= "5.6.0"
+def subsecond_precision_supported?
+ ActiveRecord::Base.connection.supports_datetime_with_precision?
end
def mysql_enforcing_gtid_consistency?
- current_adapter?(:MysqlAdapter, :Mysql2Adapter) && 'ON' == ActiveRecord::Base.connection.show_variable('enforce_gtid_consistency')
+ current_adapter?(:Mysql2Adapter) && "ON" == ActiveRecord::Base.connection.show_variable("enforce_gtid_consistency")
+end
+
+def supports_default_expression?
+ if current_adapter?(:PostgreSQLAdapter)
+ true
+ elsif current_adapter?(:Mysql2Adapter)
+ conn = ActiveRecord::Base.connection
+ !conn.mariadb? && conn.version >= "8.0.13"
+ end
end
def supports_savepoints?
ActiveRecord::Base.connection.supports_savepoints?
end
-def with_env_tz(new_tz = 'US/Eastern')
- old_tz, ENV['TZ'] = ENV['TZ'], new_tz
+def with_env_tz(new_tz = "US/Eastern")
+ old_tz, ENV["TZ"] = ENV["TZ"], new_tz
yield
ensure
- old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
+ old_tz ? ENV["TZ"] = old_tz : ENV.delete("TZ")
end
def with_timezone_config(cfg)
@@ -136,20 +139,6 @@ def disable_extension!(extension, connection)
connection.reconnect!
end
-require "cases/validations_repair_helper"
-class ActiveSupport::TestCase
- include ActiveRecord::TestFixtures
- include ActiveRecord::ValidationsRepairHelper
-
- self.fixture_path = FIXTURES_ROOT
- self.use_instantiated_fixtures = false
- self.use_transactional_tests = true
-
- def create_fixtures(*fixture_set_names, &block)
- ActiveRecord::FixtureSet.create_fixtures(ActiveSupport::TestCase.fixture_path, fixture_set_names, fixture_class_names, &block)
- end
-end
-
def load_schema
# silence verbose schema loading
original_stdout = $stdout
@@ -163,6 +152,8 @@ def load_schema
if File.exist?(adapter_specific_schema_file)
load adapter_specific_schema_file
end
+
+ ActiveRecord::FixtureSet.reset_cache
ensure
$stdout = original_stdout
end
@@ -189,17 +180,15 @@ end
module InTimeZone
private
- def in_time_zone(zone)
- old_zone = Time.zone
- old_tz = ActiveRecord::Base.time_zone_aware_attributes
-
- Time.zone = zone ? ActiveSupport::TimeZone[zone] : nil
- ActiveRecord::Base.time_zone_aware_attributes = !zone.nil?
- yield
- ensure
- Time.zone = old_zone
- ActiveRecord::Base.time_zone_aware_attributes = old_tz
- end
+ def in_time_zone(zone)
+ old_zone = Time.zone
+ old_tz = ActiveRecord::Base.time_zone_aware_attributes
+
+ Time.zone = zone ? ActiveSupport::TimeZone[zone] : nil
+ ActiveRecord::Base.time_zone_aware_attributes = !zone.nil?
+ yield
+ ensure
+ Time.zone = old_zone
+ ActiveRecord::Base.time_zone_aware_attributes = old_tz
+ end
end
-
-require 'mocha/setup' # FIXME: stop using mocha
diff --git a/activerecord/test/cases/hot_compatibility_test.rb b/activerecord/test/cases/hot_compatibility_test.rb
index 5ba9a1029a..7b388ebc5e 100644
--- a/activerecord/test/cases/hot_compatibility_test.rb
+++ b/activerecord/test/cases/hot_compatibility_test.rb
@@ -1,7 +1,11 @@
-require 'cases/helper'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "support/connection_helper"
class HotCompatibilityTest < ActiveRecord::TestCase
self.use_transactional_tests = false
+ include ConnectionHelper
setup do
@klass = Class.new(ActiveRecord::Base) do
@@ -10,7 +14,7 @@ class HotCompatibilityTest < ActiveRecord::TestCase
t.string :bar
end
- def self.name; 'HotCompatibility'; end
+ def self.name; "HotCompatibility"; end
end
end
@@ -33,22 +37,108 @@ class HotCompatibilityTest < ActiveRecord::TestCase
# but we can successfully create a record so long as we don't
# reference the removed column
- record = @klass.create! foo: 'foo'
+ record = @klass.create! foo: "foo"
record.reload
- assert_equal 'foo', record.foo
+ assert_equal "foo", record.foo
end
test "update after remove_column" do
- record = @klass.create! foo: 'foo'
+ record = @klass.create! foo: "foo"
assert_equal 3, @klass.columns.length
@klass.connection.remove_column :hot_compatibilities, :bar
assert_equal 3, @klass.columns.length
record.reload
- assert_equal 'foo', record.foo
- record.foo = 'bar'
+ assert_equal "foo", record.foo
+ record.foo = "bar"
record.save!
record.reload
- assert_equal 'bar', record.foo
+ assert_equal "bar", record.foo
+ end
+
+ if current_adapter?(:PostgreSQLAdapter) && ActiveRecord::Base.connection.prepared_statements
+ test "cleans up after prepared statement failure in a transaction" do
+ with_two_connections do |original_connection, ddl_connection|
+ record = @klass.create! bar: "bar"
+
+ # prepare the reload statement in a transaction
+ @klass.transaction do
+ record.reload
+ end
+
+ assert get_prepared_statement_cache(@klass.connection).any?,
+ "expected prepared statement cache to have something in it"
+
+ # add a new column
+ ddl_connection.add_column :hot_compatibilities, :baz, :string
+
+ assert_raise(ActiveRecord::PreparedStatementCacheExpired) do
+ @klass.transaction do
+ record.reload
+ end
+ end
+
+ assert_empty get_prepared_statement_cache(@klass.connection),
+ "expected prepared statement cache to be empty but it wasn't"
+ end
+ end
+
+ test "cleans up after prepared statement failure in nested transactions" do
+ with_two_connections do |original_connection, ddl_connection|
+ record = @klass.create! bar: "bar"
+
+ # prepare the reload statement in a transaction
+ @klass.transaction do
+ record.reload
+ end
+
+ assert get_prepared_statement_cache(@klass.connection).any?,
+ "expected prepared statement cache to have something in it"
+
+ # add a new column
+ ddl_connection.add_column :hot_compatibilities, :baz, :string
+
+ assert_raise(ActiveRecord::PreparedStatementCacheExpired) do
+ @klass.transaction do
+ @klass.transaction do
+ @klass.transaction do
+ record.reload
+ end
+ end
+ end
+ end
+
+ assert_empty get_prepared_statement_cache(@klass.connection),
+ "expected prepared statement cache to be empty but it wasn't"
+ end
+ end
end
+
+ private
+
+ def get_prepared_statement_cache(connection)
+ connection.instance_variable_get(:@statements)
+ .instance_variable_get(:@cache)[Process.pid]
+ end
+
+ # Rails will automatically clear the prepared statements on the connection
+ # that runs the migration, so we use two connections to simulate what would
+ # actually happen on a production system; we'd have one connection running the
+ # migration from the rake task ("ddl_connection" here), and we'd have another
+ # connection in a web worker.
+ def with_two_connections
+ run_without_connection do |original_connection|
+ ActiveRecord::Base.establish_connection(original_connection.merge(pool_size: 2))
+ begin
+ ddl_connection = ActiveRecord::Base.connection_pool.checkout
+ begin
+ yield original_connection, ddl_connection
+ ensure
+ ActiveRecord::Base.connection_pool.checkin ddl_connection
+ end
+ ensure
+ ActiveRecord::Base.clear_all_connections!
+ end
+ end
+ end
end
diff --git a/activerecord/test/cases/i18n_test.rb b/activerecord/test/cases/i18n_test.rb
index a428f1d87b..22981c142a 100644
--- a/activerecord/test/cases/i18n_test.rb
+++ b/activerecord/test/cases/i18n_test.rb
@@ -1,45 +1,46 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/topic'
-require 'models/reply'
+require "models/topic"
+require "models/reply"
class ActiveRecordI18nTests < ActiveRecord::TestCase
-
def setup
I18n.backend = I18n::Backend::Simple.new
end
def test_translated_model_attributes
- I18n.backend.store_translations 'en', :activerecord => {:attributes => {:topic => {:title => 'topic title attribute'} } }
- assert_equal 'topic title attribute', Topic.human_attribute_name('title')
+ I18n.backend.store_translations "en", activerecord: { attributes: { topic: { title: "topic title attribute" } } }
+ assert_equal "topic title attribute", Topic.human_attribute_name("title")
end
def test_translated_model_attributes_with_symbols
- I18n.backend.store_translations 'en', :activerecord => {:attributes => {:topic => {:title => 'topic title attribute'} } }
- assert_equal 'topic title attribute', Topic.human_attribute_name(:title)
+ I18n.backend.store_translations "en", activerecord: { attributes: { topic: { title: "topic title attribute" } } }
+ assert_equal "topic title attribute", Topic.human_attribute_name(:title)
end
def test_translated_model_attributes_with_sti
- I18n.backend.store_translations 'en', :activerecord => {:attributes => {:reply => {:title => 'reply title attribute'} } }
- assert_equal 'reply title attribute', Reply.human_attribute_name('title')
+ I18n.backend.store_translations "en", activerecord: { attributes: { reply: { title: "reply title attribute" } } }
+ assert_equal "reply title attribute", Reply.human_attribute_name("title")
end
def test_translated_model_attributes_with_sti_fallback
- I18n.backend.store_translations 'en', :activerecord => {:attributes => {:topic => {:title => 'topic title attribute'} } }
- assert_equal 'topic title attribute', Reply.human_attribute_name('title')
+ I18n.backend.store_translations "en", activerecord: { attributes: { topic: { title: "topic title attribute" } } }
+ assert_equal "topic title attribute", Reply.human_attribute_name("title")
end
def test_translated_model_names
- I18n.backend.store_translations 'en', :activerecord => {:models => {:topic => 'topic model'} }
- assert_equal 'topic model', Topic.model_name.human
+ I18n.backend.store_translations "en", activerecord: { models: { topic: "topic model" } }
+ assert_equal "topic model", Topic.model_name.human
end
def test_translated_model_names_with_sti
- I18n.backend.store_translations 'en', :activerecord => {:models => {:reply => 'reply model'} }
- assert_equal 'reply model', Reply.model_name.human
+ I18n.backend.store_translations "en", activerecord: { models: { reply: "reply model" } }
+ assert_equal "reply model", Reply.model_name.human
end
def test_translated_model_names_with_sti_fallback
- I18n.backend.store_translations 'en', :activerecord => {:models => {:topic => 'topic model'} }
- assert_equal 'topic model', Reply.model_name.human
+ I18n.backend.store_translations "en", activerecord: { models: { topic: "topic model" } }
+ assert_equal "topic model", Reply.model_name.human
end
end
diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb
index f67d85603a..3d3189900f 100644
--- a/activerecord/test/cases/inheritance_test.rb
+++ b/activerecord/test/cases/inheritance_test.rb
@@ -1,11 +1,16 @@
-require 'cases/helper'
-require 'models/company'
-require 'models/person'
-require 'models/post'
-require 'models/project'
-require 'models/subscriber'
-require 'models/vegetables'
-require 'models/shop'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/author"
+require "models/company"
+require "models/membership"
+require "models/person"
+require "models/post"
+require "models/project"
+require "models/subscriber"
+require "models/vegetables"
+require "models/shop"
+require "models/sponsor"
module InheritanceTestHelper
def with_store_full_sti_class(&block)
@@ -27,11 +32,11 @@ end
class InheritanceTest < ActiveRecord::TestCase
include InheritanceTestHelper
- fixtures :companies, :projects, :subscribers, :accounts, :vegetables
+ fixtures :companies, :projects, :subscribers, :accounts, :vegetables, :memberships
def test_class_with_store_full_sti_class_returns_full_name
with_store_full_sti_class do
- assert_equal 'Namespaced::Company', Namespaced::Company.sti_name
+ assert_equal "Namespaced::Company", Namespaced::Company.sti_name
end
end
@@ -40,32 +45,78 @@ class InheritanceTest < ActiveRecord::TestCase
company = company.dup
company.extend(Module.new {
def _read_attribute(name)
- return ' ' if name == 'type'
+ return " " if name == "type"
super
end
})
company.save!
company = Company.all.to_a.find { |x| x.id == company.id }
- assert_equal ' ', company.type
+ assert_equal " ", company.type
end
def test_class_without_store_full_sti_class_returns_demodulized_name
without_store_full_sti_class do
- assert_equal 'Company', Namespaced::Company.sti_name
+ assert_equal "Company", Namespaced::Company.sti_name
+ end
+ end
+
+ def test_compute_type_success
+ assert_equal Author, Company.send(:compute_type, "Author")
+ end
+
+ def test_compute_type_nonexistent_constant
+ e = assert_raises NameError do
+ Company.send :compute_type, "NonexistentModel"
+ end
+ 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
+ Company.send :compute_type, "InvalidModel"
+ end
+ end
+ end
+
+ def test_compute_type_on_undefined_method
+ error = nil
+ begin
+ Class.new(Author) do
+ alias_method :foo, :bar
+ end
+ rescue => e
+ error = e
+ end
+
+ ActiveSupport::Dependencies.stub(:safe_constantize, proc { raise e }) do
+ exception = assert_raises NameError do
+ Company.send :compute_type, "InvalidModel"
+ end
+ assert_equal error.message, exception.message
+ end
+ end
+
+ def test_compute_type_argument_error
+ ActiveSupport::Dependencies.stub(:safe_constantize, proc { raise ArgumentError }) do
+ assert_raises ArgumentError do
+ Company.send :compute_type, "InvalidModel"
+ end
end
end
def test_should_store_demodulized_class_name_with_store_full_sti_class_option_disabled
without_store_full_sti_class do
item = Namespaced::Company.new
- assert_equal 'Company', item[:type]
+ assert_equal "Company", item[:type]
end
end
def test_should_store_full_class_name_with_store_full_sti_class_option_enabled
with_store_full_sti_class do
item = Namespaced::Company.new
- assert_equal 'Namespaced::Company', item[:type]
+ assert_equal "Namespaced::Company", item[:type]
end
end
@@ -77,25 +128,71 @@ class InheritanceTest < ActiveRecord::TestCase
end
end
+ def test_descends_from_active_record
+ assert_not_predicate ActiveRecord::Base, :descends_from_active_record?
+
+ # Abstract subclass of AR::Base.
+ assert_predicate LoosePerson, :descends_from_active_record?
+
+ # Concrete subclass of an abstract class.
+ assert_predicate LooseDescendant, :descends_from_active_record?
+
+ # Concrete subclass of AR::Base.
+ assert_predicate TightPerson, :descends_from_active_record?
+
+ # Concrete subclass of a concrete class but has no type column.
+ assert_predicate TightDescendant, :descends_from_active_record?
+
+ # Concrete subclass of AR::Base.
+ assert_predicate Post, :descends_from_active_record?
+
+ # Concrete subclasses of a concrete class which has a type column.
+ assert_not_predicate StiPost, :descends_from_active_record?
+ assert_not_predicate SubStiPost, :descends_from_active_record?
+
+ # Abstract subclass of a concrete class which has a type column.
+ # This is pathological, as you'll never have Sub < Abstract < Concrete.
+ assert_not_predicate AbstractStiPost, :descends_from_active_record?
+
+ # Concrete subclass of an abstract class which has a type column.
+ assert_not_predicate SubAbstractStiPost, :descends_from_active_record?
+ end
+
def test_company_descends_from_active_record
- assert !ActiveRecord::Base.descends_from_active_record?
- assert AbstractCompany.descends_from_active_record?, 'AbstractCompany should descend from ActiveRecord::Base'
- assert Company.descends_from_active_record?, 'Company should descend from ActiveRecord::Base'
- assert !Class.new(Company).descends_from_active_record?, 'Company subclass should not descend from ActiveRecord::Base'
+ assert_not_predicate ActiveRecord::Base, :descends_from_active_record?
+ assert AbstractCompany.descends_from_active_record?, "AbstractCompany should descend from ActiveRecord::Base"
+ assert Company.descends_from_active_record?, "Company should descend from ActiveRecord::Base"
+ assert_not Class.new(Company).descends_from_active_record?, "Company subclass should not descend from ActiveRecord::Base"
+ end
+
+ def test_abstract_class
+ assert_not_predicate ActiveRecord::Base, :abstract_class?
+ assert_predicate LoosePerson, :abstract_class?
+ assert_not_predicate LooseDescendant, :abstract_class?
end
def test_inheritance_base_class
assert_equal Post, Post.base_class
+ assert_predicate Post, :base_class?
assert_equal Post, SpecialPost.base_class
+ assert_not_predicate SpecialPost, :base_class?
assert_equal Post, StiPost.base_class
- assert_equal SubStiPost, SubStiPost.base_class
+ assert_not_predicate StiPost, :base_class?
+ assert_equal Post, SubStiPost.base_class
+ assert_not_predicate SubStiPost, :base_class?
+ assert_equal SubAbstractStiPost, SubAbstractStiPost.base_class
+ assert_predicate SubAbstractStiPost, :base_class?
end
def test_abstract_inheritance_base_class
assert_equal LoosePerson, LoosePerson.base_class
+ assert_predicate LoosePerson, :base_class?
assert_equal LooseDescendant, LooseDescendant.base_class
+ assert_predicate LooseDescendant, :base_class?
assert_equal TightPerson, TightPerson.base_class
+ assert_predicate TightPerson, :base_class?
assert_equal TightPerson, TightDescendant.base_class
+ assert_not_predicate TightDescendant, :base_class?
end
def test_base_class_activerecord_error
@@ -133,7 +230,7 @@ class InheritanceTest < ActiveRecord::TestCase
def test_becomes_and_change_tracking_for_inheritance_columns
cucumber = Vegetable.find(1)
cabbage = cucumber.becomes!(Cabbage)
- assert_equal ['Cucumber', 'Cabbage'], cabbage.custom_type_change
+ assert_equal ["Cucumber", "Cabbage"], cabbage.custom_type_change
end
def test_alt_becomes_bang_resets_inheritance_type_column
@@ -148,13 +245,13 @@ class InheritanceTest < ActiveRecord::TestCase
end
def test_inheritance_find_all
- companies = Company.all.merge!(:order => 'id').to_a
+ companies = Company.all.merge!(order: "id").to_a
assert_kind_of Firm, companies[0], "37signals should be a firm"
assert_kind_of Client, companies[1], "Summit should be a client"
end
def test_alt_inheritance_find_all
- companies = Vegetable.all.merge!(:order => 'id').to_a
+ companies = Vegetable.all.merge!(order: "id").to_a
assert_kind_of Cucumber, companies[0]
assert_kind_of Cabbage, companies[1]
end
@@ -169,7 +266,7 @@ class InheritanceTest < ActiveRecord::TestCase
end
def test_alt_inheritance_save
- cabbage = Cabbage.new(:name => 'Savoy')
+ cabbage = Cabbage.new(name: "Savoy")
cabbage.save!
savoy = Vegetable.find(cabbage.id)
@@ -182,12 +279,27 @@ class InheritanceTest < ActiveRecord::TestCase
end
def test_inheritance_new_with_base_class
- company = Company.new(:type => 'Company')
+ company = Company.new(type: "Company")
assert_equal Company, company.class
end
def test_inheritance_new_with_subclass
- firm = Company.new(:type => 'Firm')
+ firm = Company.new(type: "Firm")
+ assert_equal Firm, firm.class
+ end
+
+ def test_where_new_with_subclass
+ firm = Company.where(type: "Firm").new
+ assert_equal Firm, firm.class
+ end
+
+ def test_where_create_with_subclass
+ firm = Company.where(type: "Firm").create(name: "Basecamp")
+ assert_equal Firm, firm.class
+ end
+
+ def test_where_create_bang_with_subclass
+ firm = Company.where(type: "Firm").create!(name: "Basecamp")
assert_equal Firm, firm.class
end
@@ -206,40 +318,63 @@ class InheritanceTest < ActiveRecord::TestCase
end
def test_new_with_invalid_type
- assert_raise(ActiveRecord::SubclassNotFound) { Company.new(:type => 'InvalidType') }
+ assert_raise(ActiveRecord::SubclassNotFound) { Company.new(type: "InvalidType") }
end
def test_new_with_unrelated_type
- assert_raise(ActiveRecord::SubclassNotFound) { Company.new(:type => 'Account') }
+ assert_raise(ActiveRecord::SubclassNotFound) { Company.new(type: "Account") }
+ end
+
+ def test_where_new_with_invalid_type
+ assert_raise(ActiveRecord::SubclassNotFound) { Company.where(type: "InvalidType").new }
+ end
+
+ def test_where_new_with_unrelated_type
+ assert_raise(ActiveRecord::SubclassNotFound) { Company.where(type: "Account").new }
+ end
+
+ def test_where_create_with_invalid_type
+ assert_raise(ActiveRecord::SubclassNotFound) { Company.where(type: "InvalidType").create }
+ end
+
+ def test_where_create_with_unrelated_type
+ assert_raise(ActiveRecord::SubclassNotFound) { Company.where(type: "Account").create }
+ end
+
+ def test_where_create_bang_with_invalid_type
+ assert_raise(ActiveRecord::SubclassNotFound) { Company.where(type: "InvalidType").create! }
+ end
+
+ def test_where_create_bang_with_unrelated_type
+ assert_raise(ActiveRecord::SubclassNotFound) { Company.where(type: "Account").create! }
end
def test_new_with_unrelated_namespaced_type
without_store_full_sti_class do
e = assert_raises ActiveRecord::SubclassNotFound do
- Namespaced::Company.new(type: 'Firm')
+ Namespaced::Company.new(type: "Firm")
end
assert_equal "Invalid single-table inheritance type: Namespaced::Firm is not a subclass of Namespaced::Company", e.message
end
end
-
def test_new_with_complex_inheritance
- assert_nothing_raised { Client.new(type: 'VerySpecialClient') }
+ assert_nothing_raised { Client.new(type: "VerySpecialClient") }
end
def test_new_without_storing_full_sti_class
without_store_full_sti_class do
- item = Company.new(type: 'SpecialCo')
+ item = Company.new(type: "SpecialCo")
assert_instance_of Company::SpecialCo, item
end
end
def test_new_with_autoload_paths
- path = File.expand_path('../../models/autoloadable', __FILE__)
+ path = File.expand_path("../models/autoloadable", __dir__)
ActiveSupport::Dependencies.autoload_paths << path
- firm = Company.new(:type => 'ExtraFirm')
+ firm = Company.new(type: "ExtraFirm")
assert_equal ExtraFirm, firm.class
ensure
ActiveSupport::Dependencies.autoload_paths.reject! { |p| p == path }
@@ -272,7 +407,7 @@ class InheritanceTest < ActiveRecord::TestCase
Client.update_all "name = 'I am a client'"
assert_equal "I am a client", Client.first.name
# Order by added as otherwise Oracle tests were failing because of different order of results
- assert_equal "37signals", Firm.all.merge!(:order => "id").to_a.first.name
+ assert_equal "37signals", Firm.all.merge!(order: "id").to_a.first.name
end
def test_alt_update_all_within_inheritance
@@ -294,51 +429,52 @@ class InheritanceTest < ActiveRecord::TestCase
end
def test_find_first_within_inheritance
- assert_kind_of Firm, Company.all.merge!(:where => "name = '37signals'").first
- assert_kind_of Firm, Firm.all.merge!(:where => "name = '37signals'").first
- assert_nil Client.all.merge!(:where => "name = '37signals'").first
+ assert_kind_of Firm, Company.all.merge!(where: "name = '37signals'").first
+ assert_kind_of Firm, Firm.all.merge!(where: "name = '37signals'").first
+ assert_nil Client.all.merge!(where: "name = '37signals'").first
end
def test_alt_find_first_within_inheritance
- assert_kind_of Cabbage, Vegetable.all.merge!(:where => "name = 'his cabbage'").first
- assert_kind_of Cabbage, Cabbage.all.merge!(:where => "name = 'his cabbage'").first
- assert_nil Cucumber.all.merge!(:where => "name = 'his cabbage'").first
+ assert_kind_of Cabbage, Vegetable.all.merge!(where: "name = 'his cabbage'").first
+ assert_kind_of Cabbage, Cabbage.all.merge!(where: "name = 'his cabbage'").first
+ assert_nil Cucumber.all.merge!(where: "name = 'his cabbage'").first
end
def test_complex_inheritance
very_special_client = VerySpecialClient.create("name" => "veryspecial")
assert_equal very_special_client, VerySpecialClient.where("name = 'veryspecial'").first
- assert_equal very_special_client, SpecialClient.all.merge!(:where => "name = 'veryspecial'").first
- assert_equal very_special_client, Company.all.merge!(:where => "name = 'veryspecial'").first
- assert_equal very_special_client, Client.all.merge!(:where => "name = 'veryspecial'").first
- assert_equal 1, Client.all.merge!(:where => "name = 'Summit'").to_a.size
+ assert_equal very_special_client, SpecialClient.all.merge!(where: "name = 'veryspecial'").first
+ assert_equal very_special_client, Company.all.merge!(where: "name = 'veryspecial'").first
+ assert_equal very_special_client, Client.all.merge!(where: "name = 'veryspecial'").first
+ assert_equal 1, Client.all.merge!(where: "name = 'Summit'").to_a.size
assert_equal very_special_client, Client.find(very_special_client.id)
end
def test_alt_complex_inheritance
king_cole = KingCole.create("name" => "uniform heads")
assert_equal king_cole, KingCole.where("name = 'uniform heads'").first
- assert_equal king_cole, GreenCabbage.all.merge!(:where => "name = 'uniform heads'").first
- assert_equal king_cole, Cabbage.all.merge!(:where => "name = 'uniform heads'").first
- assert_equal king_cole, Vegetable.all.merge!(:where => "name = 'uniform heads'").first
- assert_equal 1, Cabbage.all.merge!(:where => "name = 'his cabbage'").to_a.size
+ assert_equal king_cole, GreenCabbage.all.merge!(where: "name = 'uniform heads'").first
+ assert_equal king_cole, Cabbage.all.merge!(where: "name = 'uniform heads'").first
+ assert_equal king_cole, Vegetable.all.merge!(where: "name = 'uniform heads'").first
+ assert_equal 1, Cabbage.all.merge!(where: "name = 'his cabbage'").to_a.size
assert_equal king_cole, Cabbage.find(king_cole.id)
end
def test_eager_load_belongs_to_something_inherited
- account = Account.all.merge!(:includes => :firm).find(1)
+ account = Account.all.merge!(includes: :firm).find(1)
assert account.association(:firm).loaded?, "association was not eager loaded"
end
def test_alt_eager_loading
- cabbage = RedCabbage.all.merge!(:includes => :seller).find(4)
+ cabbage = RedCabbage.all.merge!(includes: :seller).find(4)
assert cabbage.association(:seller).loaded?, "association was not eager loaded"
end
def test_eager_load_belongs_to_primary_key_quoting
con = Account.connection
- assert_sql(/#{con.quote_table_name('companies')}.#{con.quote_column_name('id')} = 1/) do
- Account.all.merge!(:includes => :firm).find(1)
+ bind_param = Arel::Nodes::BindParam.new(nil)
+ assert_sql(/#{con.quote_table_name('companies')}\.#{con.quote_column_name('id')} = (?:#{Regexp.quote(bind_param.to_sql)}|1)/) do
+ Account.all.merge!(includes: :firm).find(1)
end
end
@@ -348,25 +484,24 @@ class InheritanceTest < ActiveRecord::TestCase
def test_inheritance_without_mapping
assert_kind_of SpecialSubscriber, SpecialSubscriber.find("webster132")
- assert_nothing_raised { s = SpecialSubscriber.new("name" => "And breaaaaathe!"); s.id = 'roger'; s.save }
+ assert_nothing_raised { s = SpecialSubscriber.new("name" => "And breaaaaathe!"); s.id = "roger"; s.save }
end
def test_scope_inherited_properly
assert_nothing_raised { Company.of_first_firm }
assert_nothing_raised { Client.of_first_firm }
end
+
+ def test_inheritance_with_default_scope
+ assert_equal 1, SelectedMembership.count(:all)
+ end
end
class InheritanceComputeTypeTest < ActiveRecord::TestCase
include InheritanceTestHelper
fixtures :companies
- def setup
- ActiveSupport::Dependencies.log_activity = true
- end
-
teardown do
- ActiveSupport::Dependencies.log_activity = false
self.class.const_remove :FirmOnTheFly rescue nil
Firm.const_remove :FirmOnTheFly rescue nil
end
@@ -374,7 +509,7 @@ class InheritanceComputeTypeTest < ActiveRecord::TestCase
def test_instantiation_doesnt_try_to_require_corresponding_file
without_store_full_sti_class do
foo = Firm.first.clone
- foo.type = 'FirmOnTheFly'
+ foo.type = "FirmOnTheFly"
foo.save!
# Should fail without FirmOnTheFly in the type condition.
@@ -395,8 +530,131 @@ class InheritanceComputeTypeTest < ActiveRecord::TestCase
end
def test_sti_type_from_attributes_disabled_in_non_sti_class
- phone = Shop::Product::Type.new(name: 'Phone')
- product = Shop::Product.new(:type => phone)
+ phone = Shop::Product::Type.new(name: "Phone")
+ product = Shop::Product.new(type: phone)
assert product.save
end
+
+ def test_inheritance_new_with_subclass_as_default
+ original_type = Company.columns_hash["type"].default
+ ActiveRecord::Base.connection.change_column_default :companies, :type, "Firm"
+ Company.reset_column_information
+
+ firm = Company.new # without arguments
+ assert_equal "Firm", firm.type
+ assert_instance_of Firm, firm
+
+ firm = Company.new(firm_name: "Shri Hans Plastic") # with arguments
+ assert_equal "Firm", firm.type
+ assert_instance_of Firm, firm
+
+ client = Client.new
+ assert_equal "Client", client.type
+ assert_instance_of Client, client
+
+ firm = Company.new(type: "Client") # overwrite the default type
+ assert_equal "Client", firm.type
+ assert_instance_of Client, firm
+ ensure
+ ActiveRecord::Base.connection.change_column_default :companies, :type, original_type
+ Company.reset_column_information
+ end
+end
+
+class InheritanceAttributeTest < ActiveRecord::TestCase
+ class Company < ActiveRecord::Base
+ self.table_name = "companies"
+ attribute :type, :string, default: "InheritanceAttributeTest::Startup"
+ end
+
+ class Startup < Company
+ end
+
+ class Empire < Company
+ end
+
+ def test_inheritance_new_with_subclass_as_default
+ startup = Company.new # without arguments
+ assert_equal "InheritanceAttributeTest::Startup", startup.type
+ assert_instance_of Startup, startup
+
+ empire = Company.new(type: "InheritanceAttributeTest::Empire") # without arguments
+ assert_equal "InheritanceAttributeTest::Empire", empire.type
+ assert_instance_of Empire, empire
+ end
+end
+
+class InheritanceAttributeMappingTest < ActiveRecord::TestCase
+ setup do
+ @old_registry = ActiveRecord::Type.registry
+ ActiveRecord::Type.registry = ActiveRecord::Type::AdapterSpecificRegistry.new
+ ActiveRecord::Type.register :omg_sti, InheritanceAttributeMappingTest::OmgStiType
+ Company.delete_all
+ Sponsor.delete_all
+ end
+
+ teardown do
+ ActiveRecord::Type.registry = @old_registry
+ end
+
+ class OmgStiType < ActiveRecord::Type::String
+ def cast_value(value)
+ if value =~ /\Aomg_(.+)\z/
+ $1.classify
+ else
+ value
+ end
+ end
+
+ def serialize(value)
+ if value
+ "omg_%s" % value.underscore
+ end
+ end
+ end
+
+ class Company < ActiveRecord::Base
+ self.table_name = "companies"
+ attribute :type, :omg_sti
+ end
+
+ class Startup < Company; end
+ class Empire < Company; end
+
+ class Sponsor < ActiveRecord::Base
+ self.table_name = "sponsors"
+ attribute :sponsorable_type, :omg_sti
+
+ belongs_to :sponsorable, polymorphic: true
+ end
+
+ def test_sti_with_custom_type
+ Startup.create! name: "a Startup"
+ Empire.create! name: "an Empire"
+
+ assert_equal [["a Startup", "omg_inheritance_attribute_mapping_test/startup"],
+ ["an Empire", "omg_inheritance_attribute_mapping_test/empire"]], ActiveRecord::Base.connection.select_rows("SELECT name, type FROM companies").sort
+ assert_equal [["a Startup", "InheritanceAttributeMappingTest::Startup"],
+ ["an Empire", "InheritanceAttributeMappingTest::Empire"]], Company.all.map { |a| [a.name, a.type] }.sort
+
+ startup = Startup.first
+ startup.becomes! Empire
+ startup.save!
+
+ assert_equal [["a Startup", "omg_inheritance_attribute_mapping_test/empire"],
+ ["an Empire", "omg_inheritance_attribute_mapping_test/empire"]], ActiveRecord::Base.connection.select_rows("SELECT name, type FROM companies").sort
+
+ assert_equal [["a Startup", "InheritanceAttributeMappingTest::Empire"],
+ ["an Empire", "InheritanceAttributeMappingTest::Empire"]], Company.all.map { |a| [a.name, a.type] }.sort
+ end
+
+ def test_polymorphic_associations_custom_type
+ startup = Startup.create! name: "a Startup"
+ sponsor = Sponsor.create! sponsorable: startup
+
+ assert_equal ["omg_inheritance_attribute_mapping_test/company"], ActiveRecord::Base.connection.select_values("SELECT sponsorable_type FROM sponsors")
+
+ sponsor = Sponsor.first
+ assert_equal startup, sponsor.sponsorable
+ end
end
diff --git a/activerecord/test/cases/instrumentation_test.rb b/activerecord/test/cases/instrumentation_test.rb
new file mode 100644
index 0000000000..e6e8468757
--- /dev/null
+++ b/activerecord/test/cases/instrumentation_test.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/book"
+
+module ActiveRecord
+ class InstrumentationTest < ActiveRecord::TestCase
+ def test_payload_name_on_load
+ Book.create(name: "test book")
+ subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
+ event = ActiveSupport::Notifications::Event.new(*args)
+ if event.payload[:sql].match "SELECT"
+ assert_equal "Book Load", event.payload[:name]
+ end
+ end
+ Book.first
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
+ end
+
+ def test_payload_name_on_create
+ subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
+ event = ActiveSupport::Notifications::Event.new(*args)
+ if event.payload[:sql].match "INSERT"
+ assert_equal "Book Create", event.payload[:name]
+ end
+ end
+ Book.create(name: "test book")
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
+ end
+
+ def test_payload_name_on_update
+ subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
+ event = ActiveSupport::Notifications::Event.new(*args)
+ if event.payload[:sql].match "UPDATE"
+ assert_equal "Book Update", event.payload[:name]
+ end
+ end
+ book = Book.create(name: "test book")
+ book.update_attribute(:name, "new name")
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
+ end
+
+ def test_payload_name_on_update_all
+ subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
+ event = ActiveSupport::Notifications::Event.new(*args)
+ if event.payload[:sql].match "UPDATE"
+ assert_equal "Book Update All", event.payload[:name]
+ end
+ end
+ Book.create(name: "test book")
+ Book.update_all(name: "new name")
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
+ end
+
+ def test_payload_name_on_destroy
+ subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
+ event = ActiveSupport::Notifications::Event.new(*args)
+ if event.payload[:sql].match "DELETE"
+ assert_equal "Book Destroy", event.payload[:name]
+ end
+ end
+ book = Book.create(name: "test book")
+ book.destroy
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
+ end
+ end
+end
diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb
index 018b7b0d8f..5687afbc71 100644
--- a/activerecord/test/cases/integration_test.rb
+++ b/activerecord/test/cases/integration_test.rb
@@ -1,10 +1,11 @@
+# frozen_string_literal: true
-require 'cases/helper'
-require 'models/company'
-require 'models/developer'
-require 'models/computer'
-require 'models/owner'
-require 'models/pet'
+require "cases/helper"
+require "models/company"
+require "models/developer"
+require "models/computer"
+require "models/owner"
+require "models/pet"
class IntegrationTest < ActiveRecord::TestCase
fixtures :companies, :developers, :owners, :pets
@@ -15,59 +16,85 @@ 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
client = Client.new
client.id = 1
- assert_equal '1', client.to_param
+ assert_equal "1", client.to_param
end
def test_to_param_class_method
firm = Firm.find(4)
- assert_equal '4-flamboyant-software', firm.to_param
+ assert_equal "4-flamboyant-software", firm.to_param
+ end
+
+ def test_to_param_class_method_truncates_words_properly
+ firm = Firm.find(4)
+ firm.name << ", Inc."
+ assert_equal "4-flamboyant-software", firm.to_param
+ end
+
+ def test_to_param_class_method_truncates_after_parameterize
+ firm = Firm.find(4)
+ firm.name = "Huey, Dewey, & Louie LLC"
+ # 123456789T123456789v
+ assert_equal "4-huey-dewey-louie-llc", firm.to_param
+ end
+
+ def test_to_param_class_method_truncates_after_parameterize_with_hyphens
+ firm = Firm.find(4)
+ firm.name = "Door-to-Door Wash-n-Fold Service"
+ # 123456789T123456789v
+ assert_equal "4-door-to-door-wash-n", firm.to_param
end
def test_to_param_class_method_truncates
firm = Firm.find(4)
- firm.name = 'a ' * 100
- assert_equal '4-a-a-a-a-a-a-a-a-a', firm.to_param
+ firm.name = "a " * 100
+ assert_equal "4-a-a-a-a-a-a-a-a-a-a", firm.to_param
end
def test_to_param_class_method_truncates_edge_case
firm = Firm.find(4)
- firm.name = 'David HeinemeierHansson'
- assert_equal '4-david', firm.to_param
+ firm.name = "David HeinemeierHansson"
+ assert_equal "4-david", firm.to_param
+ end
+
+ def test_to_param_class_method_truncates_case_shown_in_doc
+ firm = Firm.find(4)
+ firm.name = "David Heinemeier Hansson"
+ assert_equal "4-david-heinemeier", firm.to_param
end
def test_to_param_class_method_squishes
firm = Firm.find(4)
firm.name = "ab \n" * 100
- assert_equal '4-ab-ab-ab-ab-ab-ab', firm.to_param
+ assert_equal "4-ab-ab-ab-ab-ab-ab-ab", firm.to_param
end
def test_to_param_class_method_multibyte_character
firm = Firm.find(4)
firm.name = "戦場ヶ原 ひたぎ"
- assert_equal '4', firm.to_param
+ assert_equal "4", firm.to_param
end
def test_to_param_class_method_uses_default_if_blank
firm = Firm.find(4)
firm.name = nil
- assert_equal '4', firm.to_param
- firm.name = ' '
- assert_equal '4', firm.to_param
+ assert_equal "4", firm.to_param
+ firm.name = " "
+ assert_equal "4", firm.to_param
end
def test_to_param_class_method_uses_default_if_not_persisted
- firm = Firm.new(name: 'Fancy Shirts')
- assert_equal nil, firm.to_param
+ firm = Firm.new(name: "Fancy Shirts")
+ assert_nil firm.to_param
end
def test_to_param_with_no_arguments
- assert_equal 'Firm', Firm.to_param
+ assert_equal "Firm", Firm.to_param
end
def test_cache_key_for_existing_record_is_not_timezone_dependent
@@ -81,7 +108,7 @@ class IntegrationTest < ActiveRecord::TestCase
def test_cache_key_format_for_existing_record_with_updated_at
dev = Developer.first
- assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:nsec)}", dev.cache_key
+ assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:usec)}", dev.cache_key
end
def test_cache_key_format_for_existing_record_with_updated_at_and_custom_cache_timestamp_format
@@ -96,7 +123,9 @@ class IntegrationTest < ActiveRecord::TestCase
owner.update_column :updated_at, Time.current
key = owner.cache_key
- assert pet.touch
+ travel(1.second) do
+ assert pet.touch
+ end
assert_not_equal key, owner.reload.cache_key
end
@@ -109,30 +138,121 @@ class IntegrationTest < ActiveRecord::TestCase
def test_cache_key_for_updated_on
dev = Developer.first
dev.updated_at = nil
- assert_equal "developers/#{dev.id}-#{dev.updated_on.utc.to_s(:nsec)}", dev.cache_key
+ assert_equal "developers/#{dev.id}-#{dev.updated_on.utc.to_s(:usec)}", dev.cache_key
end
def test_cache_key_for_newer_updated_at
dev = Developer.first
dev.updated_at += 3600
- assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:nsec)}", dev.cache_key
+ assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:usec)}", dev.cache_key
end
def test_cache_key_for_newer_updated_on
dev = Developer.first
dev.updated_on += 3600
- assert_equal "developers/#{dev.id}-#{dev.updated_on.utc.to_s(:nsec)}", dev.cache_key
+ assert_equal "developers/#{dev.id}-#{dev.updated_on.utc.to_s(:usec)}", dev.cache_key
end
def test_cache_key_format_is_precise_enough
+ skip("Subsecond precision is not supported") unless subsecond_precision_supported?
dev = Developer.first
key = dev.cache_key
- dev.touch
+ travel_to dev.updated_at + 0.000001 do
+ dev.touch
+ end
assert_not_equal key, dev.cache_key
end
+ def test_cache_key_format_is_not_too_precise
+ dev = Developer.first
+ dev.touch
+ key = dev.cache_key
+ assert_equal key, dev.reload.cache_key
+ end
+
+ def test_cache_version_format_is_precise_enough
+ skip("Subsecond precision is not supported") unless subsecond_precision_supported?
+ with_cache_versioning do
+ dev = Developer.first
+ version = dev.cache_version.to_param
+ travel_to Developer.first.updated_at + 0.000001 do
+ dev.touch
+ end
+ assert_not_equal version, dev.cache_version.to_param
+ end
+ end
+
+ def test_cache_version_format_is_not_too_precise
+ with_cache_versioning do
+ dev = Developer.first
+ dev.touch
+ key = dev.cache_version.to_param
+ assert_equal key, dev.reload.cache_version.to_param
+ end
+ end
+
def test_named_timestamps_for_cache_key
- owner = owners(:blackbeard)
- assert_equal "owners/#{owner.id}-#{owner.happy_at.utc.to_s(:nsec)}", owner.cache_key(:updated_at, :happy_at)
+ assert_deprecated do
+ owner = owners(:blackbeard)
+ assert_equal "owners/#{owner.id}-#{owner.happy_at.utc.to_s(:usec)}", owner.cache_key(:updated_at, :happy_at)
+ end
+ end
+
+ def test_cache_key_when_named_timestamp_is_nil
+ assert_deprecated do
+ owner = owners(:blackbeard)
+ owner.happy_at = nil
+ assert_equal "owners/#{owner.id}", owner.cache_key(:happy_at)
+ end
+ end
+
+ def test_cache_key_is_stable_with_versioning_on
+ with_cache_versioning do
+ developer = Developer.first
+ first_key = developer.cache_key
+
+ developer.touch
+ second_key = developer.cache_key
+
+ assert_equal first_key, second_key
+ end
+ end
+
+ def test_cache_version_changes_with_versioning_on
+ with_cache_versioning do
+ developer = Developer.first
+ first_version = developer.cache_version
+
+ travel 10.seconds do
+ developer.touch
+ end
+
+ second_version = developer.cache_version
+
+ assert_not_equal first_version, second_version
+ end
+ end
+
+ def test_cache_key_retains_version_when_custom_timestamp_is_used
+ with_cache_versioning do
+ developer = Developer.first
+ first_key = developer.cache_key_with_version
+
+ travel 10.seconds do
+ developer.touch
+ end
+
+ second_key = developer.cache_key_with_version
+
+ assert_not_equal first_key, second_key
+ end
+ end
+
+ def with_cache_versioning(value = true)
+ @old_cache_versioning = ActiveRecord::Base.cache_versioning
+ ActiveRecord::Base.cache_versioning = value
+ yield
+ ensure
+ ActiveRecord::Base.cache_versioning = @old_cache_versioning
end
end
diff --git a/activerecord/test/cases/invalid_connection_test.rb b/activerecord/test/cases/invalid_connection_test.rb
index 6523fc29fd..a1be9c2780 100644
--- a/activerecord/test/cases/invalid_connection_test.rb
+++ b/activerecord/test/cases/invalid_connection_test.rb
@@ -1,22 +1,26 @@
+# frozen_string_literal: true
+
require "cases/helper"
-class TestAdapterWithInvalidConnection < ActiveRecord::TestCase
- self.use_transactional_tests = false
+if current_adapter?(:Mysql2Adapter)
+ class TestAdapterWithInvalidConnection < ActiveRecord::TestCase
+ self.use_transactional_tests = false
- class Bird < ActiveRecord::Base
- end
+ class Bird < ActiveRecord::Base
+ end
- def setup
- # Can't just use current adapter; sqlite3 will create a database
- # file on the fly.
- Bird.establish_connection adapter: 'mysql', database: 'i_do_not_exist'
- end
+ def setup
+ # Can't just use current adapter; sqlite3 will create a database
+ # file on the fly.
+ Bird.establish_connection adapter: "mysql2", database: "i_do_not_exist"
+ end
- teardown do
- Bird.remove_connection
- end
+ teardown do
+ Bird.remove_connection
+ end
- test "inspect on Model class does not raise" do
- assert_equal "#{Bird.name} (call '#{Bird.name}.connection' to establish a connection)", Bird.inspect
+ test "inspect on Model class does not raise" do
+ assert_equal "#{Bird.name} (call '#{Bird.name}.connection' to establish a connection)", Bird.inspect
+ end
end
end
diff --git a/activerecord/test/cases/invalid_date_test.rb b/activerecord/test/cases/invalid_date_test.rb
deleted file mode 100644
index 426a350379..0000000000
--- a/activerecord/test/cases/invalid_date_test.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-require 'cases/helper'
-require 'models/topic'
-
-class InvalidDateTest < ActiveRecord::TestCase
- def test_assign_valid_dates
- valid_dates = [[2007, 11, 30], [1993, 2, 28], [2008, 2, 29]]
-
- invalid_dates = [[2007, 11, 31], [1993, 2, 29], [2007, 2, 29]]
-
- valid_dates.each do |date_src|
- topic = Topic.new("last_read(1i)" => date_src[0].to_s, "last_read(2i)" => date_src[1].to_s, "last_read(3i)" => date_src[2].to_s)
- # Oracle DATE columns are datetime columns and Oracle adapter returns Time value
- if current_adapter?(:OracleAdapter)
- assert_equal(topic.last_read.to_date, Date.new(*date_src))
- else
- assert_equal(topic.last_read, Date.new(*date_src))
- end
- end
-
- invalid_dates.each do |date_src|
- assert_nothing_raised do
- topic = Topic.new({"last_read(1i)" => date_src[0].to_s, "last_read(2i)" => date_src[1].to_s, "last_read(3i)" => date_src[2].to_s})
- # Oracle DATE columns are datetime columns and Oracle adapter returns Time value
- if current_adapter?(:OracleAdapter)
- assert_equal(topic.last_read.to_date, Time.local(*date_src).to_date, "The date should be modified according to the behavior of the Time object")
- else
- assert_equal(topic.last_read, Time.local(*date_src).to_date, "The date should be modified according to the behavior of the Time object")
- end
- end
- end
- end
-end
diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb
index 99230aa3d5..6cf17ac15d 100644
--- a/activerecord/test/cases/invertible_migration_test.rb
+++ b/activerecord/test/cases/invertible_migration_test.rb
@@ -1,9 +1,14 @@
+# frozen_string_literal: true
+
require "cases/helper"
+class Horse < ActiveRecord::Base
+end
+
module ActiveRecord
class InvertibleMigrationTest < ActiveRecord::TestCase
- class SilentMigration < ActiveRecord::Migration
- def write(text = '')
+ class SilentMigration < ActiveRecord::Migration::Current
+ def write(text = "")
# sssshhhhh!!
end
end
@@ -17,6 +22,14 @@ module ActiveRecord
end
end
+ class InvertibleTransactionMigration < InvertibleMigration
+ def change
+ transaction do
+ super
+ end
+ end
+ end
+
class InvertibleRevertMigration < SilentMigration
def change
revert do
@@ -76,7 +89,33 @@ module ActiveRecord
end
end
- class LegacyMigration < ActiveRecord::Migration
+ class ChangeColumnDefault1 < SilentMigration
+ def change
+ create_table("horses") do |t|
+ t.column :name, :string, default: "Sekitoba"
+ end
+ end
+ end
+
+ class ChangeColumnDefault2 < SilentMigration
+ def change
+ change_column_default :horses, :name, from: "Sekitoba", to: "Diomed"
+ end
+ end
+
+ class DisableExtension1 < SilentMigration
+ def change
+ enable_extension "hstore"
+ end
+ end
+
+ class DisableExtension2 < SilentMigration
+ def change
+ disable_extension "hstore"
+ end
+ end
+
+ class LegacyMigration < ActiveRecord::Migration::Current
def self.up
create_table("horses") do |t|
t.column :content, :text
@@ -122,6 +161,23 @@ module ActiveRecord
end
end
+ class RevertCustomForeignKeyTable < SilentMigration
+ def change
+ change_table(:horses) do |t|
+ t.references :owner, foreign_key: { to_table: :developers }
+ end
+ end
+ end
+
+ class UpOnlyMigration < SilentMigration
+ def change
+ add_column :horses, :oldie, :integer, default: 0
+ up_only { execute "update horses set oldie = 1" }
+ end
+ end
+
+ self.use_transactional_tests = false
+
setup do
@verbose_was, ActiveRecord::Migration.verbose = ActiveRecord::Migration.verbose, false
end
@@ -167,7 +223,7 @@ module ActiveRecord
migration = InvertibleMigration.new
migration.migrate :up
migration.migrate :down
- assert !migration.connection.table_exists?("horses")
+ assert_not migration.connection.table_exists?("horses")
end
def test_migrate_revert
@@ -175,30 +231,30 @@ module ActiveRecord
revert = InvertibleRevertMigration.new
migration.migrate :up
revert.migrate :up
- assert !migration.connection.table_exists?("horses")
+ assert_not migration.connection.table_exists?("horses")
revert.migrate :down
assert migration.connection.table_exists?("horses")
migration.migrate :down
- assert !migration.connection.table_exists?("horses")
+ assert_not migration.connection.table_exists?("horses")
end
def test_migrate_revert_by_part
InvertibleMigration.new.migrate :up
received = []
migration = InvertibleByPartsMigration.new
- migration.test = ->(dir){
+ migration.test = ->(dir) {
assert migration.connection.table_exists?("horses")
assert migration.connection.table_exists?("new_horses")
received << dir
}
migration.migrate :up
assert_equal [:both, :up], received
- assert !migration.connection.table_exists?("horses")
+ assert_not migration.connection.table_exists?("horses")
assert migration.connection.table_exists?("new_horses")
migration.migrate :down
assert_equal [:both, :up, :both, :down], received
assert migration.connection.table_exists?("horses")
- assert !migration.connection.table_exists?("new_horses")
+ assert_not migration.connection.table_exists?("new_horses")
end
def test_migrate_revert_whole_migration
@@ -207,11 +263,11 @@ module ActiveRecord
revert = RevertWholeMigration.new(klass)
migration.migrate :up
revert.migrate :up
- assert !migration.connection.table_exists?("horses")
+ assert_not migration.connection.table_exists?("horses")
revert.migrate :down
assert migration.connection.table_exists?("horses")
migration.migrate :down
- assert !migration.connection.table_exists?("horses")
+ assert_not migration.connection.table_exists?("horses")
end
end
@@ -220,11 +276,57 @@ module ActiveRecord
revert.migrate :down
assert revert.connection.table_exists?("horses")
revert.migrate :up
- assert !revert.connection.table_exists?("horses")
+ assert_not revert.connection.table_exists?("horses")
+ end
+
+ def test_migrate_revert_transaction
+ migration = InvertibleTransactionMigration.new
+ migration.migrate :up
+ assert migration.connection.table_exists?("horses")
+ migration.migrate :down
+ assert_not migration.connection.table_exists?("horses")
+ end
+
+ def test_migrate_revert_change_column_default
+ migration1 = ChangeColumnDefault1.new
+ migration1.migrate(:up)
+ assert_equal "Sekitoba", Horse.new.name
+
+ migration2 = ChangeColumnDefault2.new
+ migration2.migrate(:up)
+ Horse.reset_column_information
+ assert_equal "Diomed", Horse.new.name
+
+ migration2.migrate(:down)
+ Horse.reset_column_information
+ assert_equal "Sekitoba", Horse.new.name
+ end
+
+ if current_adapter?(:PostgreSQLAdapter)
+ def test_migrate_enable_and_disable_extension
+ migration1 = InvertibleMigration.new
+ migration2 = DisableExtension1.new
+ migration3 = DisableExtension2.new
+
+ migration1.migrate(:up)
+ migration2.migrate(:up)
+ assert_equal true, Horse.connection.extension_enabled?("hstore")
+
+ migration3.migrate(:up)
+ assert_equal false, Horse.connection.extension_enabled?("hstore")
+
+ migration3.migrate(:down)
+ assert_equal true, Horse.connection.extension_enabled?("hstore")
+
+ migration2.migrate(:down)
+ assert_equal false, Horse.connection.extension_enabled?("hstore")
+ ensure
+ enable_extension!("hstore", ActiveRecord::Base.connection)
+ end
end
def test_revert_order
- block = Proc.new{|t| t.string :name }
+ block = Proc.new { |t| t.string :name }
recorder = ActiveRecord::Migration::CommandRecorder.new(ActiveRecord::Base.connection)
recorder.instance_eval do
create_table("apples", &block)
@@ -255,7 +357,7 @@ module ActiveRecord
def test_legacy_down
LegacyMigration.migrate :up
LegacyMigration.migrate :down
- assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist"
+ assert_not ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist"
end
def test_up
@@ -266,22 +368,29 @@ module ActiveRecord
def test_down
LegacyMigration.up
LegacyMigration.down
- assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist"
+ assert_not ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist"
end
def test_migrate_down_with_table_name_prefix
- ActiveRecord::Base.table_name_prefix = 'p_'
- ActiveRecord::Base.table_name_suffix = '_s'
+ ActiveRecord::Base.table_name_prefix = "p_"
+ ActiveRecord::Base.table_name_suffix = "_s"
migration = InvertibleMigration.new
migration.migrate(:up)
assert_nothing_raised { migration.migrate(:down) }
- assert !ActiveRecord::Base.connection.table_exists?("p_horses_s"), "p_horses_s should not exist"
+ assert_not 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 = ''
+ ActiveRecord::Base.table_name_prefix = ActiveRecord::Base.table_name_suffix = ""
+ end
+
+ def test_migrations_can_handle_foreign_keys_to_specific_tables
+ migration = RevertCustomForeignKeyTable.new
+ InvertibleMigration.migrate(:up)
+ migration.migrate(:up)
+ migration.migrate(:down)
end
# MySQL 5.7 and Oracle do not allow to create duplicate indexes on the same columns
- unless current_adapter?(:MysqlAdapter, :Mysql2Adapter, :OracleAdapter)
+ unless current_adapter?(:Mysql2Adapter, :OracleAdapter)
def test_migrate_revert_add_index_with_name
RevertNamedIndexMigration1.new.migrate(:up)
RevertNamedIndexMigration2.new.migrate(:up)
@@ -290,10 +399,27 @@ module ActiveRecord
connection = ActiveRecord::Base.connection
assert connection.index_exists?(:horses, :content),
"index on content should exist"
- assert !connection.index_exists?(:horses, :content, name: "horses_index_named"),
+ assert_not connection.index_exists?(:horses, :content, name: "horses_index_named"),
"horses_index_named index should not exist"
end
end
+ def test_up_only
+ InvertibleMigration.new.migrate(:up)
+ horse1 = Horse.create
+ # populates existing horses with oldie = 1 but new ones have default 0
+ UpOnlyMigration.new.migrate(:up)
+ Horse.reset_column_information
+ horse1.reload
+ horse2 = Horse.create
+
+ assert 1, horse1.oldie # created before migration
+ assert 0, horse2.oldie # created after migration
+
+ UpOnlyMigration.new.migrate(:down) # should be no error
+ connection = ActiveRecord::Base.connection
+ assert_not connection.column_exists?(:horses, :oldie)
+ Horse.reset_column_information
+ end
end
end
diff --git a/activerecord/test/cases/json_attribute_test.rb b/activerecord/test/cases/json_attribute_test.rb
new file mode 100644
index 0000000000..afc39d0420
--- /dev/null
+++ b/activerecord/test/cases/json_attribute_test.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "cases/json_shared_test_cases"
+
+class JsonAttributeTest < ActiveRecord::TestCase
+ include JSONSharedTestCases
+ self.use_transactional_tests = false
+
+ class JsonDataTypeOnText < ActiveRecord::Base
+ self.table_name = "json_data_type"
+
+ attribute :payload, :json
+ attribute :settings, :json
+
+ store_accessor :settings, :resolution
+ end
+
+ def setup
+ super
+ @connection.create_table("json_data_type") do |t|
+ t.string "payload"
+ t.string "settings"
+ end
+ end
+
+ private
+ def column_type
+ :string
+ end
+
+ def klass
+ JsonDataTypeOnText
+ end
+end
diff --git a/activerecord/test/cases/json_serialization_test.rb b/activerecord/test/cases/json_serialization_test.rb
index a222675918..82cf281cff 100644
--- a/activerecord/test/cases/json_serialization_test.rb
+++ b/activerecord/test/cases/json_serialization_test.rb
@@ -1,21 +1,23 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/contact'
-require 'models/post'
-require 'models/author'
-require 'models/tagging'
-require 'models/tag'
-require 'models/comment'
+require "models/contact"
+require "models/post"
+require "models/author"
+require "models/tagging"
+require "models/tag"
+require "models/comment"
module JsonSerializationHelpers
private
- def set_include_root_in_json(value)
- original_root_in_json = ActiveRecord::Base.include_root_in_json
- ActiveRecord::Base.include_root_in_json = value
- yield
- ensure
- ActiveRecord::Base.include_root_in_json = original_root_in_json
- end
+ def set_include_root_in_json(value)
+ original_root_in_json = ActiveRecord::Base.include_root_in_json
+ ActiveRecord::Base.include_root_in_json = value
+ yield
+ ensure
+ ActiveRecord::Base.include_root_in_json = original_root_in_json
+ end
end
class JsonSerializationTest < ActiveRecord::TestCase
@@ -27,18 +29,18 @@ class JsonSerializationTest < ActiveRecord::TestCase
def setup
@contact = Contact.new(
- :name => 'Konata Izumi',
- :age => 16,
- :avatar => 'binarydata',
- :created_at => Time.utc(2006, 8, 1),
- :awesome => true,
- :preferences => { :shows => 'anime' }
+ name: "Konata Izumi",
+ age: 16,
+ avatar: "binarydata",
+ created_at: Time.utc(2006, 8, 1),
+ awesome: true,
+ preferences: { shows: "anime" }
)
end
def test_should_demodulize_root_in_json
set_include_root_in_json(true) do
- @contact = NamespacedContact.new name: 'whatever'
+ @contact = NamespacedContact.new name: "whatever"
json = @contact.to_json
assert_match %r{^\{"namespaced_contact":\{}, json
end
@@ -51,7 +53,7 @@ class JsonSerializationTest < ActiveRecord::TestCase
assert_match %r{^\{"contact":\{}, json
assert_match %r{"name":"Konata Izumi"}, json
assert_match %r{"age":16}, json
- assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}))
+ assert_includes json, %("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))})
assert_match %r{"awesome":true}, json
assert_match %r{"preferences":\{"shows":"anime"\}}, json
end
@@ -62,28 +64,28 @@ class JsonSerializationTest < ActiveRecord::TestCase
assert_match %r{"name":"Konata Izumi"}, json
assert_match %r{"age":16}, json
- assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}))
+ assert_includes json, %("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))})
assert_match %r{"awesome":true}, json
assert_match %r{"preferences":\{"shows":"anime"\}}, json
end
def test_should_allow_attribute_filtering_with_only
- json = @contact.to_json(:only => [:name, :age])
+ json = @contact.to_json(only: [:name, :age])
assert_match %r{"name":"Konata Izumi"}, json
assert_match %r{"age":16}, json
assert_no_match %r{"awesome":true}, json
- assert !json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}))
+ assert_not_includes json, %("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))})
assert_no_match %r{"preferences":\{"shows":"anime"\}}, json
end
def test_should_allow_attribute_filtering_with_except
- json = @contact.to_json(:except => [:name, :age])
+ json = @contact.to_json(except: [:name, :age])
assert_no_match %r{"name":"Konata Izumi"}, json
assert_no_match %r{"age":16}, json
assert_match %r{"awesome":true}, json
- assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}))
+ assert_includes json, %("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))})
assert_match %r{"preferences":\{"shows":"anime"\}}, json
end
@@ -93,16 +95,27 @@ class JsonSerializationTest < ActiveRecord::TestCase
def @contact.favorite_quote; "Constraints are liberating"; end
# Single method.
- assert_match %r{"label":"Has cheezburger"}, @contact.to_json(:only => :name, :methods => :label)
+ assert_match %r{"label":"Has cheezburger"}, @contact.to_json(only: :name, methods: :label)
# Both methods.
- methods_json = @contact.to_json(:only => :name, :methods => [:label, :favorite_quote])
+ methods_json = @contact.to_json(only: :name, methods: [:label, :favorite_quote])
assert_match %r{"label":"Has cheezburger"}, methods_json
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)
+ def @contact.serializable_hash(options = nil)
super(only: %w(name))
end
@@ -113,7 +126,7 @@ class JsonSerializationTest < ActiveRecord::TestCase
end
def test_uses_serializable_hash_with_except_option
- def @contact.serializable_hash(options=nil)
+ def @contact.serializable_hash(options = nil)
super(except: %w(age))
end
@@ -125,7 +138,7 @@ class JsonSerializationTest < ActiveRecord::TestCase
def test_does_not_include_inheritance_column_from_sti
@contact = ContactSti.new(@contact.attributes)
- assert_equal 'ContactSti', @contact.type
+ assert_equal "ContactSti", @contact.type
json = @contact.to_json
assert_match %r{"name":"Konata Izumi"}, json
@@ -135,9 +148,9 @@ class JsonSerializationTest < ActiveRecord::TestCase
def test_serializable_hash_with_default_except_option_and_excluding_inheritance_column_from_sti
@contact = ContactSti.new(@contact.attributes)
- assert_equal 'ContactSti', @contact.type
+ assert_equal "ContactSti", @contact.type
- def @contact.serializable_hash(options={})
+ def @contact.serializable_hash(options = {})
super({ except: %w(age) }.merge!(options))
end
@@ -149,15 +162,13 @@ class JsonSerializationTest < ActiveRecord::TestCase
end
def test_serializable_hash_should_not_modify_options_in_argument
- options = { :only => :name }
- @contact.serializable_hash(options)
-
- assert_nil options[:except]
+ options = { only: :name }.freeze
+ assert_nothing_raised { @contact.serializable_hash(options) }
end
end
class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase
- fixtures :authors, :posts, :comments, :tags, :taggings
+ fixtures :authors, :author_addresses, :posts, :comments, :tags, :taggings
include JsonSerializationHelpers
@@ -167,7 +178,7 @@ class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase
end
def test_includes_uses_association_name
- json = @david.to_json(:include => :posts)
+ json = @david.to_json(include: :posts)
assert_match %r{"posts":\[}, json
@@ -183,7 +194,7 @@ class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase
end
def test_includes_uses_association_name_and_applies_attribute_filters
- json = @david.to_json(:include => { :posts => { :only => :title } })
+ json = @david.to_json(include: { posts: { only: :title } })
assert_match %r{"name":"David"}, json
assert_match %r{"posts":\[}, json
@@ -196,7 +207,7 @@ class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase
end
def test_includes_fetches_second_level_associations
- json = @david.to_json(:include => { :posts => { :include => { :comments => { :only => :body } } } })
+ json = @david.to_json(include: { posts: { include: { comments: { only: :body } } } })
assert_match %r{"name":"David"}, json
assert_match %r{"posts":\[}, json
@@ -209,12 +220,12 @@ class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase
def test_includes_fetches_nth_level_associations
json = @david.to_json(
- :include => {
- :posts => {
- :include => {
- :taggings => {
- :include => {
- :tag => { :only => :name }
+ include: {
+ posts: {
+ include: {
+ taggings: {
+ include: {
+ tag: { only: :name }
}
}
}
@@ -230,8 +241,8 @@ class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase
def test_includes_doesnt_merge_opts_from_base
json = @david.to_json(
- :only => :id,
- :include => :posts
+ only: :id,
+ include: :posts
)
assert_match %{"title":"Welcome to the weblog"}, json
@@ -239,11 +250,11 @@ class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase
def test_should_not_call_methods_on_associations_that_dont_respond
def @david.favorite_quote; "Constraints are liberating"; end
- json = @david.to_json(:include => :posts, :methods => :favorite_quote)
+ json = @david.to_json(include: :posts, methods: :favorite_quote)
- assert !@david.posts.first.respond_to?(:favorite_quote)
+ assert_not_respond_to @david.posts.first, :favorite_quote
assert_match %r{"favorite_quote":"Constraints are liberating"}, json
- assert_equal %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
@@ -267,15 +278,15 @@ class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase
def test_should_allow_includes_for_list_of_authors
authors = [@david, @mary]
json = ActiveSupport::JSON.encode(authors,
- :only => :name,
- :include => {
- :posts => { :only => :id }
+ only: :name,
+ include: {
+ posts: { only: :id }
}
)
['"name":"David"', '"posts":[', '{"id":1}', '{"id":2}', '{"id":4}',
'{"id":5}', '{"id":6}', '"name":"Mary"', '"posts":[', '{"id":7}', '{"id":9}'].each do |fragment|
- assert json.include?(fragment), json
+ assert_includes json, fragment, json
end
end
diff --git a/activerecord/test/cases/json_shared_test_cases.rb b/activerecord/test/cases/json_shared_test_cases.rb
new file mode 100644
index 0000000000..9b79803503
--- /dev/null
+++ b/activerecord/test/cases/json_shared_test_cases.rb
@@ -0,0 +1,269 @@
+# frozen_string_literal: true
+
+require "support/schema_dumping_helper"
+
+module JSONSharedTestCases
+ include SchemaDumpingHelper
+
+ class JsonDataType < ActiveRecord::Base
+ self.table_name = "json_data_type"
+
+ store_accessor :settings, :resolution
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def teardown
+ @connection.drop_table :json_data_type, if_exists: true
+ klass.reset_column_information
+ end
+
+ def test_column
+ column = klass.columns_hash["payload"]
+ assert_equal column_type, column.type
+ assert_type_match column_type, column.sql_type
+
+ type = klass.type_for_attribute("payload")
+ assert_not_predicate type, :binary?
+ end
+
+ def test_change_table_supports_json
+ @connection.change_table("json_data_type") do |t|
+ t.public_send column_type, "users"
+ end
+ klass.reset_column_information
+ column = klass.columns_hash["users"]
+ assert_equal column_type, column.type
+ assert_type_match column_type, column.sql_type
+ end
+
+ def test_schema_dumping
+ output = dump_table_schema("json_data_type")
+ assert_match(/t\.#{column_type}\s+"settings"/, output)
+ end
+
+ def test_cast_value_on_write
+ x = klass.new(payload: { "string" => "foo", :symbol => :bar })
+ assert_equal({ "string" => "foo", :symbol => :bar }, x.payload_before_type_cast)
+ assert_equal({ "string" => "foo", "symbol" => "bar" }, x.payload)
+ x.save!
+ assert_equal({ "string" => "foo", "symbol" => "bar" }, x.reload.payload)
+ end
+
+ def test_type_cast_json
+ type = klass.type_for_attribute("payload")
+
+ data = '{"a_key":"a_value"}'
+ hash = type.deserialize(data)
+ assert_equal({ "a_key" => "a_value" }, hash)
+ assert_equal({ "a_key" => "a_value" }, type.deserialize(data))
+
+ assert_equal({}, type.deserialize("{}"))
+ assert_equal({ "key" => nil }, type.deserialize('{"key": null}'))
+ assert_equal({ "c" => "}", '"a"' => 'b "a b' }, type.deserialize(%q({"c":"}", "\"a\"":"b \"a b"})))
+ end
+
+ def test_rewrite
+ @connection.execute(insert_statement_per_database('{"k":"v"}'))
+ x = klass.first
+ x.payload = { '"a\'' => "b" }
+ assert x.save!
+ end
+
+ def test_select
+ @connection.execute(insert_statement_per_database('{"k":"v"}'))
+ x = klass.first
+ assert_equal({ "k" => "v" }, x.payload)
+ end
+
+ def test_select_multikey
+ @connection.execute(insert_statement_per_database('{"k1":"v1", "k2":"v2", "k3":[1,2,3]}'))
+ x = klass.first
+ assert_equal({ "k1" => "v1", "k2" => "v2", "k3" => [1, 2, 3] }, x.payload)
+ end
+
+ def test_null_json
+ @connection.execute(insert_statement_per_database("null"))
+ x = klass.first
+ assert_nil(x.payload)
+ end
+
+ def test_select_nil_json_after_create
+ json = klass.create!(payload: nil)
+ x = klass.where(payload: nil).first
+ assert_equal(json, x)
+ end
+
+ def test_select_nil_json_after_update
+ json = klass.create!(payload: "foo")
+ x = klass.where(payload: nil).first
+ assert_nil(x)
+
+ json.update(payload: nil)
+ x = klass.where(payload: nil).first
+ assert_equal(json.reload, x)
+ end
+
+ def test_select_array_json_value
+ @connection.execute(insert_statement_per_database('["v0",{"k1":"v1"}]'))
+ x = klass.first
+ assert_equal(["v0", { "k1" => "v1" }], x.payload)
+ end
+
+ def test_rewrite_array_json_value
+ @connection.execute(insert_statement_per_database('["v0",{"k1":"v1"}]'))
+ x = klass.first
+ x.payload = ["v1", { "k2" => "v2" }, "v3"]
+ assert x.save!
+ end
+
+ def test_with_store_accessors
+ x = klass.new(resolution: "320×480")
+ assert_equal "320×480", x.resolution
+
+ x.save!
+ x = klass.first
+ assert_equal "320×480", x.resolution
+
+ x.resolution = "640×1136"
+ x.save!
+
+ x = klass.first
+ assert_equal "640×1136", x.resolution
+ end
+
+ def test_duplication_with_store_accessors
+ x = klass.new(resolution: "320×480")
+ assert_equal "320×480", x.resolution
+
+ y = x.dup
+ assert_equal "320×480", y.resolution
+ end
+
+ def test_yaml_round_trip_with_store_accessors
+ x = klass.new(resolution: "320×480")
+ assert_equal "320×480", x.resolution
+
+ y = YAML.load(YAML.dump(x))
+ assert_equal "320×480", y.resolution
+ end
+
+ def test_changes_in_place
+ json = klass.new
+ assert_not_predicate json, :changed?
+
+ json.payload = { "one" => "two" }
+ assert_predicate json, :changed?
+ assert_predicate json, :payload_changed?
+
+ json.save!
+ assert_not_predicate json, :changed?
+
+ json.payload["three"] = "four"
+ assert_predicate json, :payload_changed?
+
+ json.save!
+ json.reload
+
+ assert_equal({ "one" => "two", "three" => "four" }, json.payload)
+ assert_not_predicate json, :changed?
+ end
+
+ def test_changes_in_place_ignores_key_order
+ json = klass.new
+ assert_not_predicate json, :changed?
+
+ json.payload = { "three" => "four", "one" => "two" }
+ json.save!
+ json.reload
+
+ json.payload = { "three" => "four", "one" => "two" }
+ assert_not_predicate json, :changed?
+
+ json.payload = [{ "three" => "four", "one" => "two" }, { "seven" => "eight", "five" => "six" }]
+ json.save!
+ json.reload
+
+ json.payload = [{ "three" => "four", "one" => "two" }, { "seven" => "eight", "five" => "six" }]
+ assert_not_predicate json, :changed?
+ end
+
+ def test_changes_in_place_with_ruby_object
+ time = Time.now.utc
+ json = klass.create!(payload: time)
+
+ json.reload
+ assert_not_predicate json, :changed?
+
+ json.payload = time
+ assert_not_predicate json, :changed?
+ end
+
+ def test_assigning_string_literal
+ json = klass.create!(payload: "foo")
+ assert_equal "foo", json.payload
+ end
+
+ def test_assigning_number
+ json = klass.create!(payload: 1.234)
+ assert_equal 1.234, json.payload
+ end
+
+ def test_assigning_boolean
+ json = klass.create!(payload: true)
+ assert_equal true, json.payload
+ end
+
+ def test_not_compatible_with_serialize_json
+ new_klass = Class.new(klass) do
+ serialize :payload, JSON
+ end
+ assert_raises(ActiveRecord::AttributeMethods::Serialization::ColumnNotSerializableError) do
+ new_klass.new
+ end
+ end
+
+ class MySettings
+ def initialize(hash); @hash = hash end
+ def to_hash; @hash end
+ def self.load(hash); new(hash) end
+ def self.dump(object); object.to_hash end
+ end
+
+ def test_json_with_serialized_attributes
+ new_klass = Class.new(klass) do
+ serialize :settings, MySettings
+ end
+
+ new_klass.create!(settings: MySettings.new("one" => "two"))
+ record = new_klass.first
+
+ assert_instance_of MySettings, record.settings
+ assert_equal({ "one" => "two" }, record.settings.to_hash)
+
+ record.settings = MySettings.new("three" => "four")
+ record.save!
+
+ assert_equal({ "three" => "four" }, record.reload.settings.to_hash)
+ end
+
+ private
+ def klass
+ JsonDataType
+ end
+
+ def assert_type_match(type, sql_type)
+ native_type = ActiveRecord::Base.connection.native_database_types[type][:name]
+ assert_match %r(\A#{native_type}\b), sql_type
+ end
+
+ def insert_statement_per_database(values)
+ if current_adapter?(:OracleAdapter)
+ "insert into json_data_type (id, payload) VALUES (json_data_type_seq.nextval, '#{values}')"
+ else
+ "insert into json_data_type (payload) VALUES ('#{values}')"
+ end
+ end
+end
diff --git a/activerecord/test/cases/legacy_configurations_test.rb b/activerecord/test/cases/legacy_configurations_test.rb
new file mode 100644
index 0000000000..c36feb5116
--- /dev/null
+++ b/activerecord/test/cases/legacy_configurations_test.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+
+module ActiveRecord
+ class LegacyConfigurationsTest < ActiveRecord::TestCase
+ def test_can_turn_configurations_into_a_hash
+ assert ActiveRecord::Base.configurations.to_h.is_a?(Hash), "expected to be a hash but was not."
+ assert_equal ["arunit", "arunit2", "arunit_without_prepared_statements"].sort, ActiveRecord::Base.configurations.to_h.keys.sort
+ end
+
+ def test_each_is_deprecated
+ assert_deprecated do
+ ActiveRecord::Base.configurations.each do |db_config|
+ assert_equal "primary", db_config.spec_name
+ end
+ end
+ end
+
+ def test_first_is_deprecated
+ assert_deprecated do
+ db_config = ActiveRecord::Base.configurations.first
+ assert_equal "arunit", db_config.env_name
+ assert_equal "primary", db_config.spec_name
+ end
+ end
+
+ def test_fetch_is_deprecated
+ assert_deprecated do
+ db_config = ActiveRecord::Base.configurations.fetch("arunit").first
+ assert_equal "arunit", db_config.env_name
+ assert_equal "primary", db_config.spec_name
+ end
+ end
+
+ def test_values_are_deprecated
+ config_hashes = ActiveRecord::Base.configurations.configurations.map(&:config)
+ assert_deprecated do
+ assert_equal config_hashes, ActiveRecord::Base.configurations.values
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb
index dbdcc84b7d..33bd74e114 100644
--- a/activerecord/test/cases/locking_test.rb
+++ b/activerecord/test/cases/locking_test.rb
@@ -1,24 +1,27 @@
-require 'thread'
+# frozen_string_literal: true
+
+require "thread"
require "cases/helper"
-require 'models/person'
-require 'models/job'
-require 'models/reader'
-require 'models/ship'
-require 'models/legacy_thing'
-require 'models/personal_legacy_thing'
-require 'models/reference'
-require 'models/string_key_object'
-require 'models/car'
-require 'models/bulb'
-require 'models/engine'
-require 'models/wheel'
-require 'models/treasure'
+require "models/person"
+require "models/job"
+require "models/reader"
+require "models/ship"
+require "models/legacy_thing"
+require "models/personal_legacy_thing"
+require "models/reference"
+require "models/string_key_object"
+require "models/car"
+require "models/bulb"
+require "models/engine"
+require "models/wheel"
+require "models/treasure"
+require "models/frog"
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
@@ -33,7 +36,7 @@ class OptimisticLockingTest < ActiveRecord::TestCase
p1 = Person.find(1)
assert_equal 0, p1.lock_version
- p1.first_name = 'anika2'
+ p1.first_name = "anika2"
p1.save!
assert_equal 1, p1.lock_version
@@ -45,12 +48,12 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert_equal 0, s1.lock_version
assert_equal 0, s2.lock_version
- s1.name = 'updated record'
+ s1.name = "updated record"
s1.save!
assert_equal 1, s1.lock_version
assert_equal 0, s2.lock_version
- s2.name = 'doubly updated record'
+ s2.name = "doubly updated record"
assert_raise(ActiveRecord::StaleObjectError) { s2.save! }
end
@@ -60,15 +63,15 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert_equal 0, s1.lock_version
assert_equal 0, s2.lock_version
- s1.name = 'updated record'
+ s1.name = "updated record"
s1.save!
assert_equal 1, s1.lock_version
assert_equal 0, s2.lock_version
assert_raise(ActiveRecord::StaleObjectError) { s2.destroy }
assert s1.destroy
- assert s1.frozen?
- assert s1.destroyed?
+ assert_predicate s1, :frozen?
+ assert_predicate s1, :destroyed?
assert_raises(ActiveRecord::RecordNotFound) { StringKeyObject.find("record1") }
end
@@ -78,12 +81,12 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert_equal 0, p1.lock_version
assert_equal 0, p2.lock_version
- p1.first_name = 'stu'
+ p1.first_name = "stu"
p1.save!
assert_equal 1, p1.lock_version
assert_equal 0, p2.lock_version
- p2.first_name = 'sue'
+ p2.first_name = "sue"
assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
end
@@ -94,7 +97,7 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert_equal 0, p1.lock_version
assert_equal 0, p2.lock_version
- p1.first_name = 'stu'
+ p1.first_name = "stu"
p1.save!
assert_equal 1, p1.lock_version
assert_equal 0, p2.lock_version
@@ -102,8 +105,8 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert_raises(ActiveRecord::StaleObjectError) { p2.destroy }
assert p1.destroy
- assert p1.frozen?
- assert p1.destroyed?
+ assert_predicate p1, :frozen?
+ assert_predicate p1, :destroyed?
assert_raises(ActiveRecord::RecordNotFound) { Person.find(1) }
end
@@ -113,60 +116,64 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert_equal 0, p1.lock_version
assert_equal 0, p2.lock_version
- p1.first_name = 'stu'
+ p1.first_name = "stu"
p1.save!
assert_equal 1, p1.lock_version
assert_equal 0, p2.lock_version
- p2.first_name = 'sue'
+ p2.first_name = "sue"
assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
- p2.first_name = 'sue2'
+ p2.first_name = "sue2"
assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
end
def test_lock_new
- p1 = Person.new(:first_name => 'anika')
+ p1 = Person.new(first_name: "anika")
assert_equal 0, p1.lock_version
- p1.first_name = 'anika2'
+ p1.first_name = "anika2"
p1.save!
p2 = Person.find(p1.id)
assert_equal 0, p1.lock_version
assert_equal 0, p2.lock_version
- p1.first_name = 'anika3'
+ p1.first_name = "anika3"
p1.save!
assert_equal 1, p1.lock_version
assert_equal 0, p2.lock_version
- p2.first_name = 'sue'
+ p2.first_name = "sue"
assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
end
def test_lock_exception_record
- p1 = Person.new(:first_name => 'mira')
+ p1 = Person.new(first_name: "mira")
assert_equal 0, p1.lock_version
- p1.first_name = 'mira2'
+ p1.first_name = "mira2"
p1.save!
p2 = Person.find(p1.id)
assert_equal 0, p1.lock_version
assert_equal 0, p2.lock_version
- p1.first_name = 'mira3'
+ p1.first_name = "mira3"
p1.save!
- p2.first_name = 'sue'
+ p2.first_name = "sue"
error = assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
assert_equal(error.record.object_id, p2.object_id)
end
- def test_lock_new_with_nil
- p1 = Person.new(:first_name => 'anika')
+ def test_lock_new_when_explicitly_passing_nil
+ p1 = Person.new(first_name: "anika", lock_version: nil)
p1.save!
- p1.lock_version = nil # simulate bad fixture or column with no default
+ assert_equal 0, p1.lock_version
+ end
+
+ def test_lock_new_when_explicitly_passing_value
+ p1 = Person.new(first_name: "Douglas Adams", lock_version: 42)
p1.save!
- assert_equal 1, p1.lock_version
+ assert_equal 42, p1.lock_version
end
def test_touch_existing_lock
@@ -175,18 +182,71 @@ class OptimisticLockingTest < ActiveRecord::TestCase
p1.touch
assert_equal 1, p1.lock_version
+ assert_not p1.changed?, "Changes should have been cleared"
end
def test_touch_stale_object
- person = Person.create!(first_name: 'Mehmet Emin')
+ person = Person.create!(first_name: "Mehmet Emin")
stale_person = Person.find(person.id)
- person.update_attribute(:gender, 'M')
+ person.update_attribute(:gender, "M")
assert_raises(ActiveRecord::StaleObjectError) do
stale_person.touch
end
end
+ def test_update_with_dirty_primary_key
+ assert_raises(ActiveRecord::RecordNotUnique) do
+ person = Person.find(1)
+ person.id = 2
+ person.save!
+ end
+
+ person = Person.find(1)
+ person.id = 42
+ person.save!
+
+ assert Person.find(42)
+ assert_raises(ActiveRecord::RecordNotFound) do
+ Person.find(1)
+ end
+ end
+
+ def test_delete_with_dirty_primary_key
+ person = Person.find(1)
+ person.id = 2
+ person.delete
+
+ assert Person.find(2)
+ assert_raises(ActiveRecord::RecordNotFound) do
+ Person.find(1)
+ end
+ end
+
+ def test_destroy_with_dirty_primary_key
+ person = Person.find(1)
+ person.id = 2
+ person.destroy
+
+ assert Person.find(2)
+ assert_raises(ActiveRecord::RecordNotFound) do
+ Person.find(1)
+ end
+ end
+
+ def test_explicit_update_lock_column_raise_error
+ person = Person.find(1)
+
+ assert_raises(ActiveRecord::StaleObjectError) do
+ person.first_name = "Douglas Adams"
+ person.lock_version = 42
+
+ assert_predicate person, :lock_version_changed?
+
+ person.save
+ end
+ end
+
def test_lock_column_name_existing
t1 = LegacyThing.find(1)
t2 = LegacyThing.find(1)
@@ -203,11 +263,11 @@ class OptimisticLockingTest < ActiveRecord::TestCase
end
def test_lock_column_is_mass_assignable
- p1 = Person.create(:first_name => 'bianca')
+ p1 = Person.create(first_name: "bianca")
assert_equal 0, p1.lock_version
assert_equal p1.lock_version, Person.new(p1.attributes).lock_version
- p1.first_name = 'bianca2'
+ p1.first_name = "bianca2"
p1.save!
assert_equal 1, p1.lock_version
assert_equal p1.lock_version, Person.new(p1.attributes).lock_version
@@ -215,28 +275,144 @@ class OptimisticLockingTest < ActiveRecord::TestCase
def test_lock_without_default_sets_version_to_zero
t1 = LockWithoutDefault.new
+
assert_equal 0, t1.lock_version
+ assert_nil t1.lock_version_before_type_cast
+
+ t1.save!
+ t1.reload
- t1.save
- t1 = LockWithoutDefault.find(t1.id)
assert_equal 0, t1.lock_version
+ assert_equal 0, t1.lock_version_before_type_cast
+ end
+
+ def test_touch_existing_lock_without_default_should_work_with_null_in_the_database
+ ActiveRecord::Base.connection.execute("INSERT INTO lock_without_defaults(title) VALUES('title1')")
+ t1 = LockWithoutDefault.last
+
+ assert_equal 0, t1.lock_version
+ assert_nil t1.lock_version_before_type_cast
+
+ t1.touch
+
+ assert_equal 1, t1.lock_version
+ end
+
+ def test_touch_stale_object_with_lock_without_default
+ t1 = LockWithoutDefault.create!(title: "title1")
+ stale_object = LockWithoutDefault.find(t1.id)
+
+ t1.update!(title: "title2")
+
+ assert_raises(ActiveRecord::StaleObjectError) do
+ stale_object.touch
+ end
+ end
+
+ def test_lock_without_default_should_work_with_null_in_the_database
+ ActiveRecord::Base.connection.execute("INSERT INTO lock_without_defaults(title) VALUES('title1')")
+ t1 = LockWithoutDefault.last
+ t2 = LockWithoutDefault.find(t1.id)
+
+ assert_equal 0, t1.lock_version
+ assert_nil t1.lock_version_before_type_cast
+ assert_equal 0, t2.lock_version
+ assert_nil t2.lock_version_before_type_cast
+
+ t1.title = "new title1"
+ t2.title = "new title2"
+
+ assert_nothing_raised { t1.save! }
+ assert_equal 1, t1.lock_version
+ assert_equal "new title1", t1.title
+
+ assert_raise(ActiveRecord::StaleObjectError) { t2.save! }
+ assert_equal 0, t2.lock_version
+ assert_equal "new title2", t2.title
+ end
+
+ def test_lock_without_default_queries_count
+ t1 = LockWithoutDefault.create(title: "title1")
+
+ assert_equal "title1", t1.title
+ assert_equal 0, t1.lock_version
+
+ assert_queries(1) { t1.update(title: "title2") }
+
+ t1.reload
+ assert_equal "title2", t1.title
+ assert_equal 1, t1.lock_version
+
+ t2 = LockWithoutDefault.new(title: "title1")
+
+ assert_queries(1) { t2.save! }
+
+ t2.reload
+ assert_equal "title1", t2.title
+ assert_equal 0, t2.lock_version
end
def test_lock_with_custom_column_without_default_sets_version_to_zero
t1 = LockWithCustomColumnWithoutDefault.new
+
assert_equal 0, t1.custom_lock_version
assert_nil t1.custom_lock_version_before_type_cast
t1.save!
t1.reload
+
assert_equal 0, t1.custom_lock_version
- assert [0, "0"].include?(t1.custom_lock_version_before_type_cast)
+ assert_equal 0, t1.custom_lock_version_before_type_cast
+ end
+
+ def test_lock_with_custom_column_without_default_should_work_with_null_in_the_database
+ ActiveRecord::Base.connection.execute("INSERT INTO lock_without_defaults_cust(title) VALUES('title1')")
+
+ t1 = LockWithCustomColumnWithoutDefault.last
+ t2 = LockWithCustomColumnWithoutDefault.find(t1.id)
+
+ assert_equal 0, t1.custom_lock_version
+ assert_nil t1.custom_lock_version_before_type_cast
+ assert_equal 0, t2.custom_lock_version
+ assert_nil t2.custom_lock_version_before_type_cast
+
+ t1.title = "new title1"
+ t2.title = "new title2"
+
+ assert_nothing_raised { t1.save! }
+ assert_equal 1, t1.custom_lock_version
+ assert_equal "new title1", t1.title
+
+ assert_raise(ActiveRecord::StaleObjectError) { t2.save! }
+ assert_equal 0, t2.custom_lock_version
+ assert_equal "new title2", t2.title
+ end
+
+ def test_lock_with_custom_column_without_default_queries_count
+ t1 = LockWithCustomColumnWithoutDefault.create(title: "title1")
+
+ assert_equal "title1", t1.title
+ assert_equal 0, t1.custom_lock_version
+
+ assert_queries(1) { t1.update(title: "title2") }
+
+ t1.reload
+ assert_equal "title2", t1.title
+ assert_equal 1, t1.custom_lock_version
+
+ t2 = LockWithCustomColumnWithoutDefault.new(title: "title1")
+
+ assert_queries(1) { t2.save! }
+
+ t2.reload
+ assert_equal "title1", t2.title
+ assert_equal 0, t2.custom_lock_version
end
def test_readonly_attributes
- assert_equal Set.new([ 'name' ]), ReadonlyNameShip.readonly_attributes
+ assert_equal Set.new([ "name" ]), ReadonlyNameShip.readonly_attributes
- s = ReadonlyNameShip.create(:name => "unchangeable name")
+ s = ReadonlyNameShip.create(name: "unchangeable name")
s.reload
assert_equal "unchangeable name", s.name
@@ -255,7 +431,7 @@ class OptimisticLockingTest < ActiveRecord::TestCase
# is nothing else being updated.
def test_update_without_attributes_does_not_only_update_lock_version
assert_nothing_raised do
- p1 = Person.create!(:first_name => 'anika')
+ p1 = Person.create!(first_name: "anika")
lock_version = p1.lock_version
p1.save
p1.reload
@@ -263,25 +439,65 @@ class OptimisticLockingTest < ActiveRecord::TestCase
end
end
+ def test_counter_cache_with_touch_and_lock_version
+ car = Car.create!
+
+ assert_equal 0, car.wheels_count
+ assert_equal 0, car.lock_version
+
+ previously_updated_at = car.updated_at
+ previously_wheels_owned_at = car.wheels_owned_at
+ travel(1.second) do
+ Wheel.create!(wheelable: car)
+ end
+
+ assert_equal 1, car.reload.wheels_count
+ assert_equal 1, car.lock_version
+ assert_operator previously_updated_at, :<, car.updated_at
+ assert_operator previously_wheels_owned_at, :<, car.wheels_owned_at
+
+ previously_updated_at = car.updated_at
+ previously_wheels_owned_at = car.wheels_owned_at
+ travel(2.second) do
+ car.wheels.first.update(size: 42)
+ end
+
+ assert_equal 1, car.reload.wheels_count
+ assert_equal 2, car.lock_version
+ assert_operator previously_updated_at, :<, car.updated_at
+ assert_operator previously_wheels_owned_at, :<, car.wheels_owned_at
+
+ previously_updated_at = car.updated_at
+ previously_wheels_owned_at = car.wheels_owned_at
+ travel(3.second) do
+ car.wheels.first.destroy!
+ end
+
+ assert_equal 0, car.reload.wheels_count
+ assert_equal 3, car.lock_version
+ assert_operator previously_updated_at, :<, car.updated_at
+ assert_operator previously_wheels_owned_at, :<, car.wheels_owned_at
+ end
+
def test_polymorphic_destroy_with_dependencies_and_lock_version
car = Car.create!
- assert_difference 'car.wheels.count' do
- car.wheels << Wheel.create!
+ assert_difference "car.wheels.count" do
+ car.wheels.create
end
- assert_difference 'car.wheels.count', -1 do
- car.destroy
+ assert_difference "car.wheels.count", -1 do
+ car.reload.destroy
end
- assert car.destroyed?
+ assert_predicate car, :destroyed?
end
def test_removing_has_and_belongs_to_many_associations_upon_destroy
- p = RichPerson.create! first_name: 'Jon'
+ p = RichPerson.create! first_name: "Jon"
p.treasures.create!
- assert !p.treasures.empty?
+ assert_not_empty p.treasures
p.destroy
- assert p.treasures.empty?
- assert RichPerson.connection.select_all("SELECT * FROM peoples_treasures WHERE rich_person_id = 1").empty?
+ assert_empty p.treasures
+ assert_empty RichPerson.connection.select_all("SELECT * FROM peoples_treasures WHERE rich_person_id = 1")
end
def test_yaml_dumping_with_lock_column
@@ -300,7 +516,7 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase
# of a test (see test_increment_counter_*).
self.use_transactional_tests = false
- { :lock_version => Person, :custom_lock_version => LegacyThing }.each do |name, model|
+ { lock_version: Person, custom_lock_version: LegacyThing }.each do |name, model|
define_method("test_increment_counter_updates_#{name}") do
counter_test model, 1 do |id|
model.increment_counter :test_count, id
@@ -315,7 +531,7 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase
define_method("test_update_counters_updates_#{name}") do
counter_test model, 1 do |id|
- model.update_counters id, :test_count => 1
+ model.update_counters id, test_count: 1
end
end
end
@@ -323,13 +539,13 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase
# See Lighthouse ticket #1966
def test_destroy_dependents
# Establish dependent relationship between Person and PersonalLegacyThing
- add_counter_column_to(Person, 'personal_legacy_things_count')
+ add_counter_column_to(Person, "personal_legacy_things_count")
PersonalLegacyThing.reset_column_information
# Make sure that counter incrementing doesn't cause problems
- p1 = Person.new(:first_name => 'fjord')
+ p1 = Person.new(first_name: "fjord")
p1.save!
- t = PersonalLegacyThing.new(:person => p1)
+ t = PersonalLegacyThing.new(person: p1)
t.save!
p1.reload
assert_equal 1, p1.personal_legacy_things_count
@@ -338,14 +554,39 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase
assert_raises(ActiveRecord::RecordNotFound) { Person.find(p1.id) }
assert_raises(ActiveRecord::RecordNotFound) { PersonalLegacyThing.find(t.id) }
ensure
- remove_counter_column_from(Person, 'personal_legacy_things_count')
+ remove_counter_column_from(Person, "personal_legacy_things_count")
PersonalLegacyThing.reset_column_information
end
+ def test_destroy_existing_object_with_locking_column_value_null_in_the_database
+ ActiveRecord::Base.connection.execute("INSERT INTO lock_without_defaults(title) VALUES('title1')")
+ t1 = LockWithoutDefault.last
+
+ assert_equal 0, t1.lock_version
+ assert_nil t1.lock_version_before_type_cast
+
+ t1.destroy
+
+ assert_predicate t1, :destroyed?
+ end
+
+ def test_destroy_stale_object
+ t1 = LockWithoutDefault.create!(title: "title1")
+ stale_object = LockWithoutDefault.find(t1.id)
+
+ t1.update!(title: "title2")
+
+ assert_raises(ActiveRecord::StaleObjectError) do
+ stale_object.destroy!
+ end
+
+ assert_not_predicate stale_object, :destroyed?
+ end
+
private
- def add_counter_column_to(model, col='test_count')
- model.connection.add_column model.table_name, col, :integer, :null => false, :default => 0
+ def add_counter_column_to(model, col = "test_count")
+ model.connection.add_column model.table_name, col, :integer, null: false, default: 0
model.reset_column_information
end
@@ -368,7 +609,6 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase
end
end
-
# TODO: test against the generated SQL since testing locking behavior itself
# is so cumbersome. Will deadlock Ruby threads if the underlying db.execute
# blocks, so separate script called by Kernel#system is needed.
@@ -405,14 +645,27 @@ unless in_memory_db?
end
end
- # Locking a record reloads it.
- def test_sane_lock_method
+ def test_lock_does_not_raise_when_the_object_is_not_dirty
+ person = Person.find 1
assert_nothing_raised do
- Person.transaction do
- person = Person.find 1
- old, person.first_name = person.first_name, 'fooman'
- person.lock!
- assert_equal old, person.first_name
+ person.lock!
+ end
+ end
+
+ def test_lock_raises_when_the_record_is_dirty
+ person = Person.find 1
+ person.first_name = "fooman"
+ assert_raises(RuntimeError) do
+ person.lock!
+ end
+ end
+
+ def test_locking_in_after_save_callback
+ assert_nothing_raised do
+ frog = ::Frog.create(name: "Old Frog")
+ frog.name = "New Frog"
+ assert_not_deprecated do
+ frog.save!
end
end
end
@@ -420,19 +673,19 @@ unless in_memory_db?
def test_with_lock_commits_transaction
person = Person.find 1
person.with_lock do
- person.first_name = 'fooman'
+ person.first_name = "fooman"
person.save!
end
- assert_equal 'fooman', person.reload.first_name
+ assert_equal "fooman", person.reload.first_name
end
def test_with_lock_rolls_back_transaction
person = Person.find 1
old = person.first_name
person.with_lock do
- person.first_name = 'fooman'
+ person.first_name = "fooman"
person.save!
- raise 'oops'
+ raise "oops"
end rescue nil
assert_equal old, person.reload.first_name
end
@@ -441,47 +694,45 @@ unless in_memory_db?
def test_lock_sending_custom_lock_statement
Person.transaction do
person = Person.find(1)
- assert_sql(/LIMIT 1 FOR SHARE NOWAIT/) do
- person.lock!('FOR SHARE NOWAIT')
+ assert_sql(/LIMIT \$?\d FOR SHARE NOWAIT/) do
+ person.lock!("FOR SHARE NOWAIT")
end
end
end
end
- if current_adapter?(:PostgreSQLAdapter, :OracleAdapter)
- def test_no_locks_no_wait
- first, second = duel { Person.find 1 }
- assert first.end > second.end
- end
+ def test_no_locks_no_wait
+ first, second = duel { Person.find 1 }
+ assert first.end > second.end
+ end
- protected
- def duel(zzz = 5)
- t0, t1, t2, t3 = nil, nil, nil, nil
-
- a = Thread.new do
- t0 = Time.now
- Person.transaction do
- yield
- sleep zzz # block thread 2 for zzz seconds
- end
- t1 = Time.now
- end
+ private
+ def duel(zzz = 5)
+ t0, t1, t2, t3 = nil, nil, nil, nil
- b = Thread.new do
- sleep zzz / 2.0 # ensure thread 1 tx starts first
- t2 = Time.now
- Person.transaction { yield }
- t3 = Time.now
+ a = Thread.new do
+ t0 = Time.now
+ Person.transaction do
+ yield
+ sleep zzz # block thread 2 for zzz seconds
end
+ t1 = Time.now
+ end
- a.join
- b.join
-
- assert t1 > t0 + zzz
- assert t2 > t0
- assert t3 > t2
- [t0.to_f..t1.to_f, t2.to_f..t3.to_f]
+ b = Thread.new do
+ sleep zzz / 2.0 # ensure thread 1 tx starts first
+ t2 = Time.now
+ Person.transaction { yield }
+ t3 = Time.now
end
- end
+
+ a.join
+ b.join
+
+ assert t1 > t0 + zzz
+ assert t2 > t0
+ assert t3 > t2
+ [t0.to_f..t1.to_f, t2.to_f..t3.to_f]
+ end
end
end
diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb
index 4192d12ff4..ae2597adc8 100644
--- a/activerecord/test/cases/log_subscriber_test.rb
+++ b/activerecord/test/cases/log_subscriber_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "models/binary"
require "models/developer"
@@ -7,6 +9,21 @@ require "active_support/log_subscriber/test_helper"
class LogSubscriberTest < ActiveRecord::TestCase
include ActiveSupport::LogSubscriber::TestHelper
include ActiveSupport::Logger::Severity
+ REGEXP_CLEAR = Regexp.escape(ActiveRecord::LogSubscriber::CLEAR)
+ REGEXP_BOLD = Regexp.escape(ActiveRecord::LogSubscriber::BOLD)
+ REGEXP_MAGENTA = Regexp.escape(ActiveRecord::LogSubscriber::MAGENTA)
+ REGEXP_CYAN = Regexp.escape(ActiveRecord::LogSubscriber::CYAN)
+ SQL_COLORINGS = {
+ SELECT: Regexp.escape(ActiveRecord::LogSubscriber::BLUE),
+ INSERT: Regexp.escape(ActiveRecord::LogSubscriber::GREEN),
+ UPDATE: Regexp.escape(ActiveRecord::LogSubscriber::YELLOW),
+ DELETE: Regexp.escape(ActiveRecord::LogSubscriber::RED),
+ LOCK: Regexp.escape(ActiveRecord::LogSubscriber::WHITE),
+ ROLLBACK: Regexp.escape(ActiveRecord::LogSubscriber::RED),
+ TRANSACTION: REGEXP_CYAN,
+ OTHER: REGEXP_MAGENTA
+ }
+ Event = Struct.new(:duration, :payload)
class TestDebugLogSubscriber < ActiveRecord::LogSubscriber
attr_reader :debugs
@@ -16,8 +33,9 @@ class LogSubscriberTest < ActiveRecord::TestCase
super
end
- def debug message
- @debugs << message
+ def debug(progname = nil, &block)
+ @debugs << progname
+ super
end
end
@@ -26,6 +44,7 @@ class LogSubscriberTest < ActiveRecord::TestCase
def setup
@old_logger = ActiveRecord::Base.logger
Developer.primary_key
+ ActiveRecord::Base.connection.materialize_transactions
super
ActiveRecord::LogSubscriber.attach_to(:active_record)
end
@@ -41,25 +60,22 @@ class LogSubscriberTest < ActiveRecord::TestCase
end
def test_schema_statements_are_ignored
- event = Struct.new(:duration, :payload)
-
logger = TestDebugLogSubscriber.new
assert_equal 0, logger.debugs.length
- logger.sql(event.new(0, sql: 'hi mom!'))
+ logger.sql(Event.new(0.9, sql: "hi mom!"))
assert_equal 1, logger.debugs.length
- logger.sql(event.new(0, sql: 'hi mom!', name: 'foo'))
+ logger.sql(Event.new(0.9, sql: "hi mom!", name: "foo"))
assert_equal 2, logger.debugs.length
- logger.sql(event.new(0, sql: 'hi mom!', name: 'SCHEMA'))
+ logger.sql(Event.new(0.9, sql: "hi mom!", name: "SCHEMA"))
assert_equal 2, logger.debugs.length
end
def test_sql_statements_are_not_squeezed
- event = Struct.new(:duration, :payload)
logger = TestDebugLogSubscriber.new
- logger.sql(event.new(0, sql: 'ruby rails'))
+ logger.sql(Event.new(0.9, sql: "ruby rails"))
assert_match(/ruby rails/, logger.debugs.first)
end
@@ -71,6 +87,84 @@ class LogSubscriberTest < ActiveRecord::TestCase
assert_match(/SELECT .*?FROM .?developers.?/i, @logger.logged(:debug).last)
end
+ def test_basic_query_logging_coloration
+ logger = TestDebugLogSubscriber.new
+ logger.colorize_logging = true
+ SQL_COLORINGS.each do |verb, color_regex|
+ logger.sql(Event.new(0.9, sql: verb.to_s))
+ assert_match(/#{REGEXP_BOLD}#{color_regex}#{verb}#{REGEXP_CLEAR}/i, logger.debugs.last)
+ end
+ end
+
+ def test_basic_payload_name_logging_coloration_generic_sql
+ logger = TestDebugLogSubscriber.new
+ logger.colorize_logging = true
+ SQL_COLORINGS.each do |verb, _|
+ logger.sql(Event.new(0.9, sql: verb.to_s))
+ assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0\.9ms\)#{REGEXP_CLEAR}/i, logger.debugs.last)
+
+ logger.sql(Event.new(0.9, sql: verb.to_s, name: "SQL"))
+ assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA}SQL \(0\.9ms\)#{REGEXP_CLEAR}/i, logger.debugs.last)
+ end
+ end
+
+ def test_basic_payload_name_logging_coloration_named_sql
+ logger = TestDebugLogSubscriber.new
+ logger.colorize_logging = true
+ SQL_COLORINGS.each do |verb, _|
+ logger.sql(Event.new(0.9, sql: verb.to_s, name: "Model Load"))
+ assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}Model Load \(0\.9ms\)#{REGEXP_CLEAR}/i, logger.debugs.last)
+
+ logger.sql(Event.new(0.9, sql: verb.to_s, name: "Model Exists"))
+ assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}Model Exists \(0\.9ms\)#{REGEXP_CLEAR}/i, logger.debugs.last)
+
+ logger.sql(Event.new(0.9, sql: verb.to_s, name: "ANY SPECIFIC NAME"))
+ assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}ANY SPECIFIC NAME \(0\.9ms\)#{REGEXP_CLEAR}/i, logger.debugs.last)
+ end
+ end
+
+ def test_query_logging_coloration_with_nested_select
+ logger = TestDebugLogSubscriber.new
+ logger.colorize_logging = true
+ SQL_COLORINGS.slice(:SELECT, :INSERT, :UPDATE, :DELETE).each do |verb, color_regex|
+ logger.sql(Event.new(0.9, sql: "#{verb} WHERE ID IN SELECT"))
+ assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0\.9ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{color_regex}#{verb} WHERE ID IN SELECT#{REGEXP_CLEAR}/i, logger.debugs.last)
+ end
+ end
+
+ def test_query_logging_coloration_with_multi_line_nested_select
+ logger = TestDebugLogSubscriber.new
+ logger.colorize_logging = true
+ SQL_COLORINGS.slice(:SELECT, :INSERT, :UPDATE, :DELETE).each do |verb, color_regex|
+ sql = <<-EOS
+ #{verb}
+ WHERE ID IN (
+ SELECT ID FROM THINGS
+ )
+ EOS
+ logger.sql(Event.new(0.9, sql: sql))
+ assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0\.9ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{color_regex}.*#{verb}.*#{REGEXP_CLEAR}/mi, logger.debugs.last)
+ end
+ end
+
+ def test_query_logging_coloration_with_lock
+ logger = TestDebugLogSubscriber.new
+ logger.colorize_logging = true
+ sql = <<-EOS
+ SELECT * FROM
+ (SELECT * FROM mytable FOR UPDATE) ss
+ WHERE col1 = 5;
+ EOS
+ logger.sql(Event.new(0.9, sql: sql))
+ assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0\.9ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{SQL_COLORINGS[:LOCK]}.*FOR UPDATE.*#{REGEXP_CLEAR}/mi, logger.debugs.last)
+
+ sql = <<-EOS
+ LOCK TABLE films IN SHARE MODE;
+ EOS
+ logger.sql(Event.new(0.9, sql: sql))
+ assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0\.9ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{SQL_COLORINGS[:LOCK]}.*LOCK TABLE.*#{REGEXP_CLEAR}/mi, logger.debugs.last)
+ end
+
def test_exists_query_logging
Developer.exists? 1
wait
@@ -79,6 +173,36 @@ class LogSubscriberTest < ActiveRecord::TestCase
assert_match(/SELECT .*?FROM .?developers.?/i, @logger.logged(:debug).last)
end
+ def test_vebose_query_logs
+ ActiveRecord::Base.verbose_query_logs = true
+
+ logger = TestDebugLogSubscriber.new
+ logger.sql(Event.new(0, sql: "hi mom!"))
+ assert_equal 2, @logger.logged(:debug).size
+ assert_match(/↳/, @logger.logged(:debug).last)
+ ensure
+ ActiveRecord::Base.verbose_query_logs = false
+ end
+
+ def test_verbose_query_with_ignored_callstack
+ ActiveRecord::Base.verbose_query_logs = true
+
+ logger = TestDebugLogSubscriber.new
+ def logger.extract_query_source_location(*); nil; end
+
+ logger.sql(Event.new(0, sql: "hi mom!"))
+ assert_equal 1, @logger.logged(:debug).size
+ assert_no_match(/↳/, @logger.logged(:debug).last)
+ ensure
+ ActiveRecord::Base.verbose_query_logs = false
+ end
+
+ def test_verbose_query_logs_disabled_by_default
+ logger = TestDebugLogSubscriber.new
+ logger.sql(Event.new(0, sql: "hi mom!"))
+ assert_no_match(/↳/, @logger.logged(:debug).last)
+ end
+
def test_cached_queries
ActiveRecord::Base.cache do
Developer.all.load
@@ -111,11 +235,17 @@ class LogSubscriberTest < ActiveRecord::TestCase
Thread.new { assert_equal 0, ActiveRecord::LogSubscriber.runtime }.join
end
- unless current_adapter?(:Mysql2Adapter)
+ if ActiveRecord::Base.connection.prepared_statements
def test_binary_data_is_not_logged
- Binary.create(data: 'some binary data')
+ Binary.create(data: "some binary data")
wait
assert_match(/<16 bytes of binary data>/, @logger.logged(:debug).join)
end
+
+ def test_binary_data_hash
+ Binary.create(data: { a: 1 })
+ wait
+ assert_match(/<7 bytes of binary data>/, @logger.logged(:debug).join)
+ end
end
end
diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb
index 83e50048ec..7777508349 100644
--- a/activerecord/test/cases/migration/change_schema_test.rb
+++ b/activerecord/test/cases/migration/change_schema_test.rb
@@ -1,4 +1,6 @@
-require 'cases/helper'
+# frozen_string_literal: true
+
+require "cases/helper"
module ActiveRecord
class Migration
@@ -40,24 +42,24 @@ module ActiveRecord
def test_create_table_with_not_null_column
connection.create_table :testings do |t|
- t.column :foo, :string, :null => false
+ 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
def test_create_table_with_defaults
# MySQL doesn't allow defaults on TEXT or BLOB columns.
- mysql = current_adapter?(:MysqlAdapter, :Mysql2Adapter)
+ mysql = current_adapter?(:Mysql2Adapter)
connection.create_table :testings do |t|
- t.column :one, :string, :default => "hello"
- t.column :two, :boolean, :default => true
- t.column :three, :boolean, :default => false
- t.column :four, :integer, :default => 1
- t.column :five, :text, :default => "hello" unless mysql
+ t.column :one, :string, default: "hello"
+ t.column :two, :boolean, default: true
+ t.column :three, :boolean, default: false
+ t.column :four, :integer, default: 1
+ t.column :five, :text, default: "hello" unless mysql
end
columns = connection.columns(:testings)
@@ -70,30 +72,30 @@ module ActiveRecord
assert_equal "hello", one.default
assert_equal true, connection.lookup_cast_type_from_column(two).deserialize(two.default)
assert_equal false, connection.lookup_cast_type_from_column(three).deserialize(three.default)
- assert_equal '1', four.default
+ assert_equal "1", four.default
assert_equal "hello", five.default unless mysql
end
if current_adapter?(:PostgreSQLAdapter)
def test_add_column_with_array
connection.create_table :testings
- connection.add_column :testings, :foo, :string, :array => true
+ connection.add_column :testings, :foo, :string, array: true
columns = connection.columns(:testings)
array_column = columns.detect { |c| c.name == "foo" }
- assert array_column.array?
+ assert_predicate array_column, :array?
end
def test_create_table_with_array_column
connection.create_table :testings do |t|
- t.string :foo, :array => true
+ t.string :foo, array: true
end
columns = connection.columns(:testings)
array_column = columns.detect { |c| c.name == "foo" }
- assert array_column.array?
+ assert_predicate array_column, :array?
end
end
@@ -105,9 +107,9 @@ module ActiveRecord
eight = columns.detect { |c| c.name == "eight_int" }
if current_adapter?(:OracleAdapter)
- assert_equal 'NUMBER(19)', eight.sql_type
+ assert_equal "NUMBER(19)", eight.sql_type
elsif current_adapter?(:SQLite3Adapter)
- assert_equal 'bigint', eight.sql_type
+ assert_equal "bigint", eight.sql_type
else
assert_equal :integer, eight.type
assert_equal 8, eight.limit
@@ -118,13 +120,13 @@ module ActiveRecord
def test_create_table_with_limits
connection.create_table :testings do |t|
- t.column :foo, :string, :limit => 255
+ t.column :foo, :string, limit: 255
t.column :default_int, :integer
- t.column :one_int, :integer, :limit => 1
- t.column :four_int, :integer, :limit => 4
- t.column :eight_int, :integer, :limit => 8
+ t.column :one_int, :integer, limit: 1
+ t.column :four_int, :integer, limit: 4
+ t.column :eight_int, :integer, limit: 8
end
columns = connection.columns(:testings)
@@ -137,20 +139,20 @@ module ActiveRecord
eight = columns.detect { |c| c.name == "eight_int" }
if current_adapter?(:PostgreSQLAdapter)
- assert_equal 'integer', default.sql_type
- assert_equal 'smallint', one.sql_type
- assert_equal 'integer', four.sql_type
- assert_equal 'bigint', eight.sql_type
- elsif current_adapter?(:MysqlAdapter, :Mysql2Adapter)
- assert_match 'int(11)', default.sql_type
- assert_match 'tinyint', one.sql_type
- assert_match 'int', four.sql_type
- assert_match 'bigint', eight.sql_type
+ assert_equal "integer", default.sql_type
+ assert_equal "smallint", one.sql_type
+ assert_equal "integer", four.sql_type
+ assert_equal "bigint", eight.sql_type
+ elsif current_adapter?(:Mysql2Adapter)
+ assert_match "int(11)", default.sql_type
+ assert_match "tinyint", one.sql_type
+ assert_match "int", four.sql_type
+ assert_match "bigint", eight.sql_type
elsif current_adapter?(:OracleAdapter)
- assert_equal 'NUMBER(38)', default.sql_type
- assert_equal 'NUMBER(1)', one.sql_type
- assert_equal 'NUMBER(4)', four.sql_type
- assert_equal 'NUMBER(8)', eight.sql_type
+ assert_equal "NUMBER(38)", default.sql_type
+ assert_equal "NUMBER(1)", one.sql_type
+ assert_equal "NUMBER(4)", four.sql_type
+ assert_equal "NUMBER(8)", eight.sql_type
end
end
@@ -194,17 +196,28 @@ module ActiveRecord
assert_equal "you can't redefine the primary key column 'testing_id'. To define a custom primary key, pass { id: false } to create_table.", error.message
end
+ def test_create_table_raises_when_defining_existing_column
+ error = assert_raise(ArgumentError) do
+ connection.create_table :testings do |t|
+ t.column :testing_column, :string
+ t.column :testing_column, :integer
+ end
+ end
+
+ assert_equal "you can't define an already defined column 'testing_column'.", error.message
+ end
+
def test_create_table_with_timestamps_should_create_datetime_columns
connection.create_table table_name do |t|
t.timestamps
end
created_columns = connection.columns(table_name)
- created_at_column = created_columns.detect {|c| c.name == 'created_at' }
- updated_at_column = created_columns.detect {|c| c.name == 'updated_at' }
+ created_at_column = created_columns.detect { |c| c.name == "created_at" }
+ updated_at_column = created_columns.detect { |c| c.name == "updated_at" }
- assert !created_at_column.null
- assert !updated_at_column.null
+ assert_not created_at_column.null
+ assert_not updated_at_column.null
end
def test_create_table_with_timestamps_should_create_datetime_columns_with_options
@@ -213,8 +226,8 @@ module ActiveRecord
end
created_columns = connection.columns(table_name)
- created_at_column = created_columns.detect {|c| c.name == 'created_at' }
- updated_at_column = created_columns.detect {|c| c.name == 'updated_at' }
+ created_at_column = created_columns.detect { |c| c.name == "created_at" }
+ updated_at_column = created_columns.detect { |c| c.name == "updated_at" }
assert created_at_column.null
assert updated_at_column.null
@@ -231,9 +244,9 @@ module ActiveRecord
connection.create_table :testings do |t|
t.column :foo, :string
end
- connection.add_column :testings, :bar, :string, :null => false
+ 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 +257,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
@@ -258,15 +275,18 @@ module ActiveRecord
t.column :foo, :timestamp
end
- klass = Class.new(ActiveRecord::Base)
- klass.table_name = 'testings'
+ column = connection.columns(:testings).find { |c| c.name == "foo" }
- assert_equal :datetime, klass.columns_hash['foo'].type
+ assert_equal :datetime, column.type
if current_adapter?(:PostgreSQLAdapter)
- assert_equal 'timestamp without time zone', klass.columns_hash['foo'].sql_type
+ assert_equal "timestamp without time zone", column.sql_type
+ elsif current_adapter?(:Mysql2Adapter)
+ assert_equal "timestamp", column.sql_type
+ elsif current_adapter?(:OracleAdapter)
+ assert_equal "TIMESTAMP(6)", column.sql_type
else
- assert_equal klass.connection.type_to_sql('datetime'), klass.columns_hash['foo'].sql_type
+ assert_equal connection.type_to_sql("datetime"), column.sql_type
end
end
@@ -275,7 +295,7 @@ module ActiveRecord
t.column :select, :string
end
- connection.change_column :testings, :select, :string, :limit => 10
+ connection.change_column :testings, :select, :string, limit: 10
# Oracle needs primary key value from sequence
if current_adapter?(:OracleAdapter)
@@ -290,17 +310,17 @@ module ActiveRecord
t.column :title, :string
end
person_klass = Class.new(ActiveRecord::Base)
- person_klass.table_name = 'testings'
+ person_klass.table_name = "testings"
- person_klass.connection.add_column "testings", "wealth", :integer, :null => false, :default => 99
+ person_klass.connection.add_column "testings", "wealth", :integer, null: false, default: 99
person_klass.reset_column_information
assert_equal 99, person_klass.column_defaults["wealth"]
assert_equal false, person_klass.columns_hash["wealth"].null
# Oracle needs primary key value from sequence
if current_adapter?(:OracleAdapter)
- assert_nothing_raised {person_klass.connection.execute("insert into testings (id, title) values (testings_seq.nextval, 'tester')")}
+ assert_nothing_raised { person_klass.connection.execute("insert into testings (id, title) values (testings_seq.nextval, 'tester')") }
else
- assert_nothing_raised {person_klass.connection.execute("insert into testings (title) values ('tester')")}
+ assert_nothing_raised { person_klass.connection.execute("insert into testings (title) values ('tester')") }
end
# change column default to see that column doesn't lose its not null definition
@@ -317,19 +337,19 @@ module ActiveRecord
assert_equal false, person_klass.columns_hash["money"].null
# change column
- person_klass.connection.change_column "testings", "money", :integer, :null => false, :default => 1000
+ person_klass.connection.change_column "testings", "money", :integer, null: false, default: 1000
person_klass.reset_column_information
assert_equal 1000, person_klass.column_defaults["money"]
assert_equal false, person_klass.columns_hash["money"].null
# change column, make it nullable and clear default
- person_klass.connection.change_column "testings", "money", :integer, :null => true, :default => nil
+ person_klass.connection.change_column "testings", "money", :integer, null: true, default: nil
person_klass.reset_column_information
assert_nil person_klass.columns_hash["money"].default
assert_equal true, person_klass.columns_hash["money"].null
# change_column_null, make it not nullable and set null values to a default value
- person_klass.connection.execute('UPDATE testings SET money = NULL')
+ person_klass.connection.execute("UPDATE testings SET money = NULL")
person_klass.connection.change_column_null "testings", "money", false, 2000
person_klass.reset_column_information
assert_nil person_klass.columns_hash["money"].default
@@ -339,16 +359,16 @@ module ActiveRecord
def test_change_column_null
testing_table_with_only_foo_attribute do
- notnull_migration = Class.new(ActiveRecord::Migration) do
+ notnull_migration = Class.new(ActiveRecord::Migration::Current) do
def change
change_column_null :testings, :foo, false
end
end
notnull_migration.new.suppress_messages do
notnull_migration.migrate(:up)
- assert_equal false, connection.columns(:testings).find{ |c| c.name == "foo"}.null
+ assert_equal false, connection.columns(:testings).find { |c| c.name == "foo" }.null
notnull_migration.migrate(:down)
- assert connection.columns(:testings).find{ |c| c.name == "foo"}.null
+ assert connection.columns(:testings).find { |c| c.name == "foo" }.null
end
end
end
@@ -365,7 +385,7 @@ module ActiveRecord
def test_column_exists_with_type
connection.create_table :testings do |t|
t.column :foo, :string
- t.column :bar, :decimal, :precision => 8, :scale => 2
+ t.column :bar, :decimal, precision: 8, scale: 2
end
assert connection.column_exists?(:testings, :foo, :string)
@@ -380,7 +400,7 @@ module ActiveRecord
t.column :foo, :string, limit: 100
t.column :bar, :decimal, precision: 8, scale: 2
t.column :taggable_id, :integer, null: false
- t.column :taggable_type, :string, default: 'Photo'
+ t.column :taggable_type, :string, default: "Photo"
end
assert connection.column_exists?(:testings, :foo, :string, limit: 100)
@@ -389,7 +409,7 @@ module ActiveRecord
assert_not connection.column_exists?(:testings, :bar, :decimal, precision: nil, scale: nil)
assert connection.column_exists?(:testings, :taggable_id, :integer, null: false)
assert_not connection.column_exists?(:testings, :taggable_id, :integer, null: true)
- assert connection.column_exists?(:testings, :taggable_type, :string, default: 'Photo')
+ assert connection.column_exists?(:testings, :taggable_type, :string, default: "Photo")
assert_not connection.column_exists?(:testings, :taggable_type, :string, default: nil)
end
@@ -399,7 +419,7 @@ module ActiveRecord
end
connection.change_table :testings do |t|
assert t.column_exists?(:foo)
- assert !(t.column_exists?(:bar))
+ assert_not (t.column_exists?(:bar))
end
end
@@ -415,13 +435,13 @@ module ActiveRecord
end
private
- def testing_table_with_only_foo_attribute
- connection.create_table :testings, :id => false do |t|
- t.column :foo, :string
- end
+ def testing_table_with_only_foo_attribute
+ connection.create_table :testings, id: false do |t|
+ t.column :foo, :string
+ end
- yield
- end
+ yield
+ end
end
if ActiveRecord::Base.connection.supports_foreign_keys?
@@ -442,7 +462,7 @@ module ActiveRecord
end
def test_create_table_with_force_cascade_drops_dependent_objects
- skip "MySQL > 5.5 does not drop dependent objects with DROP TABLE CASCADE" if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
+ skip "MySQL > 5.5 does not drop dependent objects with DROP TABLE CASCADE" if current_adapter?(:Mysql2Adapter)
# can't re-create table referenced by foreign key
assert_raises(ActiveRecord::StatementInvalid) do
@connection.create_table :trains, force: true
diff --git a/activerecord/test/cases/migration/change_table_test.rb b/activerecord/test/cases/migration/change_table_test.rb
index 2ffe7a1b0d..c108d372d1 100644
--- a/activerecord/test/cases/migration/change_table_test.rb
+++ b/activerecord/test/cases/migration/change_table_test.rb
@@ -1,5 +1,6 @@
+# frozen_string_literal: true
+
require "cases/migration/helper"
-require "minitest/mock"
module ActiveRecord
class Migration
@@ -96,7 +97,7 @@ module ActiveRecord
def test_remove_timestamps_creates_updated_at_and_created_at
with_change_table do |t|
@connection.expect :remove_timestamps, nil, [:delete_me, { null: true }]
- t.remove_timestamps({ null: true })
+ t.remove_timestamps(null: true)
end
end
@@ -158,8 +159,16 @@ module ActiveRecord
def test_column_creates_column_with_options
with_change_table do |t|
- @connection.expect :add_column, nil, [:delete_me, :bar, :integer, {:null => false}]
- t.column :bar, :integer, :null => false
+ @connection.expect :add_column, nil, [:delete_me, :bar, :integer, { null: false }]
+ t.column :bar, :integer, null: false
+ end
+ end
+
+ def test_column_creates_column_with_index
+ with_change_table do |t|
+ @connection.expect :add_column, nil, [:delete_me, :bar, :integer, {}]
+ @connection.expect :add_index, nil, [:delete_me, :bar, {}]
+ t.column :bar, :integer, index: true
end
end
@@ -172,8 +181,8 @@ module ActiveRecord
def test_index_creates_index_with_options
with_change_table do |t|
- @connection.expect :add_index, nil, [:delete_me, :bar, {:unique => true}]
- t.index :bar, :unique => true
+ @connection.expect :add_index, nil, [:delete_me, :bar, { unique: true }]
+ t.index :bar, unique: true
end
end
@@ -186,8 +195,8 @@ module ActiveRecord
def test_index_exists_with_options
with_change_table do |t|
- @connection.expect :index_exists?, nil, [:delete_me, :bar, {:unique => true}]
- t.index_exists?(:bar, :unique => true)
+ @connection.expect :index_exists?, nil, [:delete_me, :bar, { unique: true }]
+ t.index_exists?(:bar, unique: true)
end
end
@@ -207,8 +216,8 @@ module ActiveRecord
def test_change_changes_column_with_options
with_change_table do |t|
- @connection.expect :change_column, nil, [:delete_me, :bar, :string, {:null => true}]
- t.change :bar, :string, :null => true
+ @connection.expect :change_column, nil, [:delete_me, :bar, :string, { null: true }]
+ t.change :bar, :string, null: true
end
end
@@ -235,8 +244,8 @@ module ActiveRecord
def test_remove_index_removes_index_with_options
with_change_table do |t|
- @connection.expect :remove_index, nil, [:delete_me, {:unique => true}]
- t.remove_index :unique => true
+ @connection.expect :remove_index, nil, [:delete_me, { unique: true }]
+ t.remove_index unique: true
end
end
diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb
index 8d8e661aa5..3022121f4c 100644
--- a/activerecord/test/cases/migration/column_attributes_test.rb
+++ b/activerecord/test/cases/migration/column_attributes_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/migration/helper"
module ActiveRecord
@@ -9,7 +11,7 @@ module ActiveRecord
def test_add_column_newline_default
string = "foo\nbar"
- add_column 'test_models', 'command', :string, :default => string
+ add_column "test_models", "command", :string, default: string
TestModel.reset_column_information
assert_equal string, TestModel.new.command
@@ -18,10 +20,10 @@ module ActiveRecord
def test_add_remove_single_field_using_string_arguments
assert_no_column TestModel, :last_name
- add_column 'test_models', 'last_name', :string
+ add_column "test_models", "last_name", :string
assert_column TestModel, :last_name
- remove_column 'test_models', 'last_name'
+ remove_column "test_models", "last_name"
assert_no_column TestModel, :last_name
end
@@ -37,17 +39,17 @@ module ActiveRecord
def test_add_column_without_limit
# TODO: limit: nil should work with all adapters.
- skip "MySQL wrongly enforces a limit of 255" if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
+ skip "MySQL wrongly enforces a limit of 255" if current_adapter?(:Mysql2Adapter)
add_column :test_models, :description, :string, limit: nil
TestModel.reset_column_information
assert_nil TestModel.columns_hash["description"].limit
end
- if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
+ if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)
def test_unabstracted_database_dependent_types
- add_column :test_models, :intelligence_quotient, :tinyint
+ add_column :test_models, :intelligence_quotient, :smallint
TestModel.reset_column_information
- assert_match(/tinyint/, TestModel.columns_hash['intelligence_quotient'].sql_type)
+ assert_match(/smallint/, TestModel.columns_hash["intelligence_quotient"].sql_type)
end
end
@@ -56,17 +58,13 @@ module ActiveRecord
# functionality. This allows us to more easily catch INSERT being broken,
# but SELECT actually working fine.
def test_native_decimal_insert_manual_vs_automatic
- correct_value = '0012345678901234567890.0123456789'.to_d
+ correct_value = "0012345678901234567890.0123456789".to_d
- connection.add_column "test_models", "wealth", :decimal, :precision => '30', :scale => '10'
+ connection.add_column "test_models", "wealth", :decimal, precision: "30", scale: "10"
# Do a manual insertion
if current_adapter?(:OracleAdapter)
connection.execute "insert into test_models (id, wealth) values (people_seq.nextval, 12345678901234567890.0123456789)"
- elsif current_adapter?(:MysqlAdapter) && Mysql.client_version < 50003 #before MySQL 5.0.3 decimals stored as strings
- connection.execute "insert into test_models (wealth) values ('12345678901234567890.0123456789')"
- elsif current_adapter?(:PostgreSQLAdapter)
- connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)"
else
connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)"
end
@@ -76,15 +74,13 @@ module ActiveRecord
assert_kind_of BigDecimal, row.wealth
# If this assert fails, that means the SELECT is broken!
- unless current_adapter?(:SQLite3Adapter)
- assert_equal correct_value, row.wealth
- end
+ assert_equal correct_value, row.wealth
# Reset to old state
TestModel.delete_all
# Now use the Rails insertion
- TestModel.create :wealth => BigDecimal.new("12345678901234567890.0123456789")
+ TestModel.create wealth: BigDecimal("12345678901234567890.0123456789")
# SELECT
row = TestModel.first
@@ -96,26 +92,40 @@ module ActiveRecord
end
def test_add_column_with_precision_and_scale
- connection.add_column 'test_models', 'wealth', :decimal, :precision => 9, :scale => 7
+ connection.add_column "test_models", "wealth", :decimal, precision: 9, scale: 7
- wealth_column = TestModel.columns_hash['wealth']
+ wealth_column = TestModel.columns_hash["wealth"]
assert_equal 9, wealth_column.precision
assert_equal 7, wealth_column.scale
end
+ # Test SQLite3 adapter specifically for decimal types with precision and scale
+ # attributes, since these need to be maintained in schema but aren't actually
+ # used in SQLite3 itself
if current_adapter?(:SQLite3Adapter)
+ def test_change_column_with_new_precision_and_scale
+ connection.add_column "test_models", "wealth", :decimal, precision: 9, scale: 7
+
+ connection.change_column "test_models", "wealth", :decimal, precision: 12, scale: 8
+ TestModel.reset_column_information
+
+ wealth_column = TestModel.columns_hash["wealth"]
+ assert_equal 12, wealth_column.precision
+ assert_equal 8, wealth_column.scale
+ end
+
def test_change_column_preserve_other_column_precision_and_scale
- connection.add_column 'test_models', 'last_name', :string
- connection.add_column 'test_models', 'wealth', :decimal, :precision => 9, :scale => 7
+ connection.add_column "test_models", "last_name", :string
+ connection.add_column "test_models", "wealth", :decimal, precision: 9, scale: 7
- wealth_column = TestModel.columns_hash['wealth']
+ wealth_column = TestModel.columns_hash["wealth"]
assert_equal 9, wealth_column.precision
assert_equal 7, wealth_column.scale
- connection.change_column 'test_models', 'last_name', :string, :null => false
+ connection.change_column "test_models", "last_name", :string, null: false
TestModel.reset_column_information
- wealth_column = TestModel.columns_hash['wealth']
+ wealth_column = TestModel.columns_hash["wealth"]
assert_equal 9, wealth_column.precision
assert_equal 7, wealth_column.scale
end
@@ -128,55 +138,48 @@ module ActiveRecord
add_column "test_models", "bio", :text
add_column "test_models", "age", :integer
add_column "test_models", "height", :float
- add_column "test_models", "wealth", :decimal, :precision => '30', :scale => '10'
+ add_column "test_models", "wealth", :decimal, precision: "30", scale: "10"
add_column "test_models", "birthday", :datetime
add_column "test_models", "favorite_day", :date
add_column "test_models", "moment_of_truth", :datetime
add_column "test_models", "male", :boolean
- TestModel.create :first_name => 'bob', :last_name => 'bobsen',
- :bio => "I was born ....", :age => 18, :height => 1.78,
- :wealth => BigDecimal.new("12345678901234567890.0123456789"),
- :birthday => 18.years.ago, :favorite_day => 10.days.ago,
- :moment_of_truth => "1782-10-10 21:40:18", :male => true
+ TestModel.create first_name: "bob", last_name: "bobsen",
+ bio: "I was born ....", age: 18, height: 1.78,
+ wealth: BigDecimal("12345678901234567890.0123456789"),
+ birthday: 18.years.ago, favorite_day: 10.days.ago,
+ moment_of_truth: "1782-10-10 21:40:18", male: true
bob = TestModel.first
- assert_equal 'bob', bob.first_name
- assert_equal 'bobsen', bob.last_name
+ assert_equal "bob", bob.first_name
+ assert_equal "bobsen", bob.last_name
assert_equal "I was born ....", bob.bio
assert_equal 18, bob.age
# Test for 30 significant digits (beyond the 16 of float), 10 of them
# after the decimal place.
- assert_equal BigDecimal.new("0012345678901234567890.0123456789"), bob.wealth
+ assert_equal BigDecimal("0012345678901234567890.0123456789"), bob.wealth
assert_equal true, bob.male?
assert_equal String, bob.first_name.class
assert_equal String, bob.last_name.class
assert_equal String, bob.bio.class
- assert_equal Fixnum, bob.age.class
+ assert_kind_of Integer, bob.age
assert_equal Time, bob.birthday.class
-
- if current_adapter?(:OracleAdapter)
- # Oracle doesn't differentiate between date/time
- assert_equal Time, bob.favorite_day.class
- else
- assert_equal Date, bob.favorite_day.class
- end
-
+ assert_equal Date, bob.favorite_day.class
assert_instance_of TrueClass, bob.male?
assert_kind_of BigDecimal, bob.wealth
end
end
- if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter)
+ if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)
def test_out_of_range_limit_should_raise
- assert_raise(ActiveRecordError) { add_column :test_models, :integer_too_big, :integer, :limit => 10 }
+ assert_raise(ActiveRecordError) { add_column :test_models, :integer_too_big, :integer, limit: 10 }
unless current_adapter?(:PostgreSQLAdapter)
- assert_raise(ActiveRecordError) { add_column :test_models, :text_too_big, :integer, :limit => 0xfffffffff }
+ assert_raise(ActiveRecordError) { add_column :test_models, :text_too_big, :text, limit: 0xfffffffff }
end
end
end
diff --git a/activerecord/test/cases/migration/column_positioning_test.rb b/activerecord/test/cases/migration/column_positioning_test.rb
index 4637970ce0..1c62a68cf9 100644
--- a/activerecord/test/cases/migration/column_positioning_test.rb
+++ b/activerecord/test/cases/migration/column_positioning_test.rb
@@ -1,4 +1,6 @@
-require 'cases/helper'
+# frozen_string_literal: true
+
+require "cases/helper"
module ActiveRecord
class Migration
@@ -11,7 +13,7 @@ module ActiveRecord
@connection = ActiveRecord::Base.connection
- connection.create_table :testings, :id => false do |t|
+ connection.create_table :testings, id: false do |t|
t.column :first, :integer
t.column :second, :integer
t.column :third, :integer
@@ -23,7 +25,7 @@ module ActiveRecord
ActiveRecord::Base.primary_key_prefix_type = nil
end
- if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
+ if current_adapter?(:Mysql2Adapter)
def test_column_positioning
assert_equal %w(first second third), conn.columns(:testings).map(&:name)
end
@@ -34,22 +36,32 @@ module ActiveRecord
end
def test_add_column_with_positioning_first
- conn.add_column :testings, :new_col, :integer, :first => true
+ conn.add_column :testings, :new_col, :integer, first: true
assert_equal %w(new_col first second third), conn.columns(:testings).map(&:name)
end
def test_add_column_with_positioning_after
- conn.add_column :testings, :new_col, :integer, :after => :first
+ conn.add_column :testings, :new_col, :integer, after: :first
assert_equal %w(first new_col second third), conn.columns(:testings).map(&:name)
end
def test_change_column_with_positioning
- conn.change_column :testings, :second, :integer, :first => true
+ conn.change_column :testings, :second, :integer, first: true
assert_equal %w(second first third), conn.columns(:testings).map(&:name)
- conn.change_column :testings, :second, :integer, :after => :third
+ conn.change_column :testings, :second, :integer, after: :third
assert_equal %w(first third second), conn.columns(:testings).map(&:name)
end
+
+ def test_add_reference_with_positioning_first
+ conn.add_reference :testings, :new, polymorphic: true, first: true
+ assert_equal %w(new_id new_type first second third), conn.columns(:testings).map(&:name)
+ end
+
+ def test_add_reference_with_positioning_after
+ conn.add_reference :testings, :new, polymorphic: true, after: :first
+ assert_equal %w(first new_id new_type second third), conn.columns(:testings).map(&:name)
+ end
end
end
end
diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb
index ab3f584350..cedd9c44e3 100644
--- a/activerecord/test/cases/migration/columns_test.rb
+++ b/activerecord/test/cases/migration/columns_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/migration/helper"
module ActiveRecord
@@ -13,7 +15,7 @@ module ActiveRecord
add_column "test_models", "girlfriend", :string
TestModel.reset_column_information
- TestModel.create :girlfriend => 'bobette'
+ TestModel.create girlfriend: "bobette"
rename_column "test_models", "girlfriend", "exgirlfriend"
@@ -28,12 +30,12 @@ module ActiveRecord
def test_rename_column_using_symbol_arguments
add_column :test_models, :first_name, :string
- TestModel.create :first_name => 'foo'
+ TestModel.create first_name: "foo"
rename_column :test_models, :first_name, :nick_name
TestModel.reset_column_information
- assert TestModel.column_names.include?("nick_name")
- assert_equal ['foo'], TestModel.all.map(&:nick_name)
+ assert_includes TestModel.column_names, "nick_name"
+ assert_equal ["foo"], TestModel.all.map(&:nick_name)
end
# FIXME: another integration test. We should decouple this from the
@@ -41,31 +43,31 @@ module ActiveRecord
def test_rename_column
add_column "test_models", "first_name", "string"
- TestModel.create :first_name => 'foo'
+ TestModel.create first_name: "foo"
rename_column "test_models", "first_name", "nick_name"
TestModel.reset_column_information
- assert TestModel.column_names.include?("nick_name")
- assert_equal ['foo'], TestModel.all.map(&:nick_name)
+ assert_includes TestModel.column_names, "nick_name"
+ assert_equal ["foo"], TestModel.all.map(&:nick_name)
end
def test_rename_column_preserves_default_value_not_null
- add_column 'test_models', 'salary', :integer, :default => 70000
+ add_column "test_models", "salary", :integer, default: 70000
default_before = connection.columns("test_models").find { |c| c.name == "salary" }.default
- assert_equal '70000', default_before
+ assert_equal "70000", default_before
rename_column "test_models", "salary", "annual_salary"
- assert TestModel.column_names.include?("annual_salary")
+ assert_includes TestModel.column_names, "annual_salary"
default_after = connection.columns("test_models").find { |c| c.name == "annual_salary" }.default
- assert_equal '70000', default_after
+ assert_equal "70000", default_after
end
- if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
+ if current_adapter?(:Mysql2Adapter)
def test_mysql_rename_column_preserves_auto_increment
rename_column "test_models", "id", "id_test"
- assert connection.columns("test_models").find { |c| c.name == "id_test" }.auto_increment?
+ assert_predicate connection.columns("test_models").find { |c| c.name == "id_test" }, :auto_increment?
TestModel.reset_column_information
ensure
rename_column "test_models", "id_test", "id"
@@ -74,30 +76,31 @@ module ActiveRecord
def test_rename_nonexistent_column
exception = if current_adapter?(:PostgreSQLAdapter, :OracleAdapter)
- ActiveRecord::StatementInvalid
- else
- ActiveRecord::ActiveRecordError
- end
+ ActiveRecord::StatementInvalid
+ else
+ ActiveRecord::ActiveRecordError
+ end
+
assert_raise(exception) do
rename_column "test_models", "nonexistent", "should_fail"
end
end
def test_rename_column_with_sql_reserved_word
- add_column 'test_models', 'first_name', :string
+ add_column "test_models", "first_name", :string
rename_column "test_models", "first_name", "group"
- assert TestModel.column_names.include?("group")
+ assert_includes TestModel.column_names, "group"
end
def test_rename_column_with_an_index
add_column "test_models", :hat_name, :string
add_index :test_models, :hat_name
- assert_equal 1, connection.indexes('test_models').size
+ assert_equal 1, connection.indexes("test_models").size
rename_column "test_models", "hat_name", "name"
- assert_equal ['index_test_models_on_name'], connection.indexes('test_models').map(&:name)
+ assert_equal ["index_test_models_on_name"], connection.indexes("test_models").map(&:name)
end
def test_rename_column_with_multi_column_index
@@ -105,153 +108,167 @@ module ActiveRecord
add_column "test_models", :hat_style, :string, limit: 100
add_index "test_models", ["hat_style", "hat_size"], unique: true
- rename_column "test_models", "hat_size", 'size'
+ rename_column "test_models", "hat_size", "size"
if current_adapter? :OracleAdapter
- assert_equal ['i_test_models_hat_style_size'], connection.indexes('test_models').map(&:name)
+ assert_equal ["i_test_models_hat_style_size"], connection.indexes("test_models").map(&:name)
else
- assert_equal ['index_test_models_on_hat_style_and_size'], connection.indexes('test_models').map(&:name)
+ assert_equal ["index_test_models_on_hat_style_and_size"], connection.indexes("test_models").map(&:name)
end
- rename_column "test_models", "hat_style", 'style'
+ rename_column "test_models", "hat_style", "style"
if current_adapter? :OracleAdapter
- assert_equal ['i_test_models_style_size'], connection.indexes('test_models').map(&:name)
+ assert_equal ["i_test_models_style_size"], connection.indexes("test_models").map(&:name)
else
- assert_equal ['index_test_models_on_style_and_size'], connection.indexes('test_models').map(&:name)
+ assert_equal ["index_test_models_on_style_and_size"], connection.indexes("test_models").map(&:name)
end
end
def test_rename_column_does_not_rename_custom_named_index
add_column "test_models", :hat_name, :string
- add_index :test_models, :hat_name, :name => 'idx_hat_name'
+ add_index :test_models, :hat_name, name: "idx_hat_name"
- assert_equal 1, connection.indexes('test_models').size
+ assert_equal 1, connection.indexes("test_models").size
rename_column "test_models", "hat_name", "name"
- assert_equal ['idx_hat_name'], connection.indexes('test_models').map(&:name)
+ assert_equal ["idx_hat_name"], connection.indexes("test_models").map(&:name)
end
def test_remove_column_with_index
add_column "test_models", :hat_name, :string
add_index :test_models, :hat_name
- assert_equal 1, connection.indexes('test_models').size
+ assert_equal 1, connection.indexes("test_models").size
remove_column("test_models", "hat_name")
- assert_equal 0, connection.indexes('test_models').size
+ assert_equal 0, connection.indexes("test_models").size
end
def test_remove_column_with_multi_column_index
+ # MariaDB starting with 10.2.8
+ # Dropping a column that is part of a multi-column UNIQUE constraint is not permitted.
+ skip if current_adapter?(:Mysql2Adapter) && connection.mariadb? && connection.version >= "10.2.8"
+
add_column "test_models", :hat_size, :integer
- add_column "test_models", :hat_style, :string, :limit => 100
- add_index "test_models", ["hat_style", "hat_size"], :unique => true
+ add_column "test_models", :hat_style, :string, limit: 100
+ add_index "test_models", ["hat_style", "hat_size"], unique: true
- assert_equal 1, connection.indexes('test_models').size
+ assert_equal 1, connection.indexes("test_models").size
remove_column("test_models", "hat_size")
# Every database and/or database adapter has their own behavior
# if it drops the multi-column index when any of the indexed columns dropped by remove_column.
if current_adapter?(:PostgreSQLAdapter, :OracleAdapter)
- assert_equal [], connection.indexes('test_models').map(&:name)
+ assert_equal [], connection.indexes("test_models").map(&:name)
else
- assert_equal ['index_test_models_on_hat_style_and_hat_size'], connection.indexes('test_models').map(&:name)
+ assert_equal ["index_test_models_on_hat_style_and_hat_size"], connection.indexes("test_models").map(&:name)
end
end
def test_change_type_of_not_null_column
- change_column "test_models", "updated_at", :datetime, :null => false
- change_column "test_models", "updated_at", :datetime, :null => false
+ change_column "test_models", "updated_at", :datetime, null: false
+ change_column "test_models", "updated_at", :datetime, null: false
TestModel.reset_column_information
- assert_equal false, TestModel.columns_hash['updated_at'].null
+ assert_equal false, TestModel.columns_hash["updated_at"].null
ensure
- change_column "test_models", "updated_at", :datetime, :null => true
+ change_column "test_models", "updated_at", :datetime, null: true
end
def test_change_column_nullability
add_column "test_models", "funny", :boolean
assert TestModel.columns_hash["funny"].null, "Column 'funny' must initially allow nulls"
- change_column "test_models", "funny", :boolean, :null => false, :default => true
+ change_column "test_models", "funny", :boolean, null: false, default: true
TestModel.reset_column_information
assert_not TestModel.columns_hash["funny"].null, "Column 'funny' must *not* allow nulls at this point"
- change_column "test_models", "funny", :boolean, :null => true
+ change_column "test_models", "funny", :boolean, null: true
TestModel.reset_column_information
assert TestModel.columns_hash["funny"].null, "Column 'funny' must allow nulls again at this point"
end
def test_change_column
- add_column 'test_models', 'age', :integer
- add_column 'test_models', 'approved', :boolean, :default => true
+ add_column "test_models", "age", :integer
+ add_column "test_models", "approved", :boolean, default: true
old_columns = connection.columns(TestModel.table_name)
- assert old_columns.find { |c| c.name == 'age' && c.type == :integer }
+ assert old_columns.find { |c| c.name == "age" && c.type == :integer }
change_column "test_models", "age", :string
new_columns = connection.columns(TestModel.table_name)
- assert_not new_columns.find { |c| c.name == 'age' and c.type == :integer }
- assert new_columns.find { |c| c.name == 'age' and c.type == :string }
+ assert_not new_columns.find { |c| c.name == "age" && c.type == :integer }
+ assert new_columns.find { |c| c.name == "age" && c.type == :string }
old_columns = connection.columns(TestModel.table_name)
assert old_columns.find { |c|
default = connection.lookup_cast_type_from_column(c).deserialize(c.default)
- c.name == 'approved' && c.type == :boolean && default == true
+ c.name == "approved" && c.type == :boolean && default == true
}
- change_column :test_models, :approved, :boolean, :default => false
+ change_column :test_models, :approved, :boolean, default: false
new_columns = connection.columns(TestModel.table_name)
assert_not new_columns.find { |c|
default = connection.lookup_cast_type_from_column(c).deserialize(c.default)
- c.name == 'approved' and c.type == :boolean and default == true
+ c.name == "approved" && c.type == :boolean && default == true
}
assert new_columns.find { |c|
default = connection.lookup_cast_type_from_column(c).deserialize(c.default)
- c.name == 'approved' and c.type == :boolean and default == false
+ c.name == "approved" && c.type == :boolean && default == false
}
- change_column :test_models, :approved, :boolean, :default => true
+ change_column :test_models, :approved, :boolean, default: true
end
def test_change_column_with_nil_default
- add_column "test_models", "contributor", :boolean, :default => true
- assert TestModel.new.contributor?
+ add_column "test_models", "contributor", :boolean, default: true
+ assert_predicate TestModel.new, :contributor?
+
+ change_column "test_models", "contributor", :boolean, default: nil
+ TestModel.reset_column_information
+ assert_not_predicate TestModel.new, :contributor?
+ assert_nil TestModel.new.contributor
+ end
+
+ def test_change_column_to_drop_default_with_null_false
+ add_column "test_models", "contributor", :boolean, default: true, null: false
+ assert_predicate TestModel.new, :contributor?
- change_column "test_models", "contributor", :boolean, :default => nil
+ change_column "test_models", "contributor", :boolean, default: nil, null: false
TestModel.reset_column_information
- assert_not TestModel.new.contributor?
+ assert_not_predicate TestModel.new, :contributor?
assert_nil TestModel.new.contributor
end
def test_change_column_with_new_default
- add_column "test_models", "administrator", :boolean, :default => true
- assert TestModel.new.administrator?
+ add_column "test_models", "administrator", :boolean, default: true
+ assert_predicate TestModel.new, :administrator?
- change_column "test_models", "administrator", :boolean, :default => false
+ change_column "test_models", "administrator", :boolean, default: false
TestModel.reset_column_information
- assert_not TestModel.new.administrator?
+ assert_not_predicate TestModel.new, :administrator?
end
def test_change_column_with_custom_index_name
add_column "test_models", "category", :string
- add_index :test_models, :category, name: 'test_models_categories_idx'
+ add_index :test_models, :category, name: "test_models_categories_idx"
- assert_equal ['test_models_categories_idx'], connection.indexes('test_models').map(&:name)
- change_column "test_models", "category", :string, null: false, default: 'article'
+ assert_equal ["test_models_categories_idx"], connection.indexes("test_models").map(&:name)
+ change_column "test_models", "category", :string, null: false, default: "article"
- assert_equal ['test_models_categories_idx'], connection.indexes('test_models').map(&:name)
+ assert_equal ["test_models_categories_idx"], connection.indexes("test_models").map(&:name)
end
def test_change_column_with_long_index_name
- table_name_prefix = 'test_models_'
- long_index_name = table_name_prefix + ('x' * (connection.allowed_index_name_length - table_name_prefix.length))
+ table_name_prefix = "test_models_"
+ long_index_name = table_name_prefix + ("x" * (connection.allowed_index_name_length - table_name_prefix.length))
add_column "test_models", "category", :string
add_index :test_models, :category, name: long_index_name
- change_column "test_models", "category", :string, null: false, default: 'article'
+ change_column "test_models", "category", :string, null: false, default: "article"
- assert_equal [long_index_name], connection.indexes('test_models').map(&:name)
+ assert_equal [long_index_name], connection.indexes("test_models").map(&:name)
end
def test_change_column_default
@@ -287,7 +304,7 @@ module ActiveRecord
remove_column("my_table", "col_two")
rename_column("my_table", "col_one", "col_three")
- assert_equal 'my_table_id', connection.primary_key('my_table')
+ assert_equal "my_table_id", connection.primary_key("my_table")
ensure
connection.drop_table(:my_table) rescue nil
end
diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb
index 99f1dc65b0..01f8628fc5 100644
--- a/activerecord/test/cases/migration/command_recorder_test.rb
+++ b/activerecord/test/cases/migration/command_recorder_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
module ActiveRecord
@@ -12,7 +14,7 @@ module ActiveRecord
recorder = CommandRecorder.new(Class.new {
def america; end
}.new)
- assert recorder.respond_to?(:america)
+ assert_respond_to recorder, :america
end
def test_send_calls_super
@@ -25,25 +27,26 @@ module ActiveRecord
recorder = CommandRecorder.new(Class.new {
def create_table(name); end
}.new)
- assert recorder.respond_to?(:create_table), 'respond_to? create_table'
+ assert_respond_to recorder, :create_table
recorder.send(:create_table, :horses)
assert_equal [[:create_table, [:horses], nil]], recorder.commands
end
def test_unknown_commands_delegate
- recorder = CommandRecorder.new(stub(:foo => 'bar'))
- assert_equal 'bar', recorder.foo
+ recorder = Struct.new(:foo)
+ recorder = CommandRecorder.new(recorder.new("bar"))
+ assert_equal "bar", recorder.foo
end
def test_inverse_of_raise_exception_on_unknown_commands
assert_raises(ActiveRecord::IrreversibleMigration) do
- @recorder.inverse_of :execute, ['some sql']
+ @recorder.inverse_of :execute, ["some sql"]
end
end
def test_irreversible_commands_raise_exception
assert_raises(ActiveRecord::IrreversibleMigration) do
- @recorder.revert{ @recorder.execute 'some sql' }
+ @recorder.revert { @recorder.execute "some sql" }
end
end
@@ -57,12 +60,12 @@ module ActiveRecord
@recorder.record :create_table, [:hello]
@recorder.record :create_table, [:world]
end
- tables = @recorder.commands.map{|_cmd, args, _block| args}
+ tables = @recorder.commands.map { |_cmd, args, _block| args }
assert_equal [[:world], [:hello]], tables
end
def test_revert_order
- block = Proc.new{|t| t.string :name }
+ block = Proc.new { |t| t.string :name }
@recorder.instance_eval do
create_table("apples", &block)
revert do
@@ -114,13 +117,13 @@ module ActiveRecord
end
def test_invert_create_table_with_options_and_block
- block = Proc.new{}
+ block = Proc.new { }
drop_table = @recorder.inverse_of :create_table, [:people_reminders, id: false], &block
assert_equal [:drop_table, [:people_reminders, id: false], block], drop_table
end
def test_invert_drop_table
- block = Proc.new{}
+ block = Proc.new { }
create_table = @recorder.inverse_of :drop_table, [:people_reminders, id: false], &block
assert_equal [:create_table, [:people_reminders, id: false], block], create_table
end
@@ -142,7 +145,7 @@ module ActiveRecord
end
def test_invert_drop_join_table
- block = Proc.new{}
+ block = Proc.new { }
create_join_table = @recorder.inverse_of :drop_join_table, [:musics, :artists, table_name: :catalog], &block
assert_equal [:create_join_table, [:musics, :artists, table_name: :catalog], block], create_join_table
end
@@ -165,7 +168,7 @@ module ActiveRecord
def test_invert_change_column_default
assert_raises(ActiveRecord::IrreversibleMigration) do
- @recorder.inverse_of :change_column_default, [:table, :column, 'default_value']
+ @recorder.inverse_of :change_column_default, [:table, :column, "default_value"]
end
end
@@ -202,17 +205,17 @@ module ActiveRecord
def test_invert_add_index
remove = @recorder.inverse_of :add_index, [:table, [:one, :two]]
- assert_equal [:remove_index, [:table, {column: [:one, :two]}]], remove
+ assert_equal [:remove_index, [:table, { column: [:one, :two] }]], remove
end
def test_invert_add_index_with_name
remove = @recorder.inverse_of :add_index, [:table, [:one, :two], name: "new_index"]
- assert_equal [:remove_index, [:table, {name: "new_index"}]], remove
+ assert_equal [:remove_index, [:table, { name: "new_index" }]], remove
end
- def test_invert_add_index_with_no_options
- remove = @recorder.inverse_of :add_index, [:table, [:one, :two]]
- assert_equal [:remove_index, [:table, {column: [:one, :two]}]], remove
+ def test_invert_add_index_with_algorithm_option
+ remove = @recorder.inverse_of :add_index, [:table, :one, algorithm: :concurrently]
+ assert_equal [:remove_index, [:table, { column: :one, algorithm: :concurrently }]], remove
end
def test_invert_remove_index
@@ -221,17 +224,17 @@ module ActiveRecord
end
def test_invert_remove_index_with_column
- add = @recorder.inverse_of :remove_index, [:table, {column: [:one, :two], options: true}]
+ add = @recorder.inverse_of :remove_index, [:table, { column: [:one, :two], options: true }]
assert_equal [:add_index, [:table, [:one, :two], options: true]], add
end
def test_invert_remove_index_with_name
- add = @recorder.inverse_of :remove_index, [:table, {column: [:one, :two], name: "new_index"}]
+ add = @recorder.inverse_of :remove_index, [:table, { column: [:one, :two], name: "new_index" }]
assert_equal [:add_index, [:table, [:one, :two], name: "new_index"]], add
end
def test_invert_remove_index_with_no_special_options
- add = @recorder.inverse_of :remove_index, [:table, {column: [:one, :two]}]
+ add = @recorder.inverse_of :remove_index, [:table, { column: [:one, :two] }]
assert_equal [:add_index, [:table, [:one, :two], {}]], add
end
@@ -253,7 +256,7 @@ module ActiveRecord
def test_invert_remove_timestamps
add = @recorder.inverse_of :remove_timestamps, [:table, { null: true }]
- assert_equal [:add_timestamps, [:table, {null: true }], nil], add
+ assert_equal [:add_timestamps, [:table, { null: true }], nil], add
end
def test_invert_add_reference
@@ -282,13 +285,13 @@ module ActiveRecord
end
def test_invert_enable_extension
- disable = @recorder.inverse_of :enable_extension, ['uuid-ossp']
- assert_equal [:disable_extension, ['uuid-ossp'], nil], disable
+ disable = @recorder.inverse_of :enable_extension, ["uuid-ossp"]
+ assert_equal [:disable_extension, ["uuid-ossp"], nil], disable
end
def test_invert_disable_extension
- enable = @recorder.inverse_of :disable_extension, ['uuid-ossp']
- assert_equal [:enable_extension, ['uuid-ossp'], nil], enable
+ enable = @recorder.inverse_of :disable_extension, ["uuid-ossp"]
+ assert_equal [:enable_extension, ["uuid-ossp"], nil], enable
end
def test_invert_add_foreign_key
@@ -326,11 +329,24 @@ module ActiveRecord
assert_equal [:add_foreign_key, [:dogs, :people, primary_key: "person_id"]], enable
end
+ def test_invert_remove_foreign_key_with_primary_key_and_to_table_in_options
+ enable = @recorder.inverse_of :remove_foreign_key, [:dogs, to_table: :people, primary_key: "uuid"]
+ assert_equal [:add_foreign_key, [:dogs, :people, primary_key: "uuid"]], enable
+ end
+
def test_invert_remove_foreign_key_with_on_delete_on_update
enable = @recorder.inverse_of :remove_foreign_key, [:dogs, :people, on_delete: :nullify, on_update: :cascade]
assert_equal [:add_foreign_key, [:dogs, :people, on_delete: :nullify, on_update: :cascade]], enable
end
+ def test_invert_remove_foreign_key_with_to_table_in_options
+ enable = @recorder.inverse_of :remove_foreign_key, [:dogs, to_table: :people]
+ assert_equal [:add_foreign_key, [:dogs, :people]], enable
+
+ enable = @recorder.inverse_of :remove_foreign_key, [:dogs, to_table: :people, column: :owner_id]
+ assert_equal [:add_foreign_key, [:dogs, :people, column: :owner_id]], enable
+ end
+
def test_invert_remove_foreign_key_is_irreversible_without_to_table
assert_raises ActiveRecord::IrreversibleMigration do
@recorder.inverse_of :remove_foreign_key, [:dogs, column: "owner_id"]
@@ -344,6 +360,16 @@ module ActiveRecord
@recorder.inverse_of :remove_foreign_key, [:dogs]
end
end
+
+ def test_invert_transaction_with_irreversible_inside_is_irreversible
+ assert_raises(ActiveRecord::IrreversibleMigration) do
+ @recorder.revert do
+ @recorder.transaction do
+ @recorder.execute "some sql"
+ end
+ end
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/migration/compatibility_test.rb b/activerecord/test/cases/migration/compatibility_test.rb
new file mode 100644
index 0000000000..017ee7951e
--- /dev/null
+++ b/activerecord/test/cases/migration/compatibility_test.rb
@@ -0,0 +1,383 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "support/schema_dumping_helper"
+
+module ActiveRecord
+ class Migration
+ class CompatibilityTest < ActiveRecord::TestCase
+ attr_reader :connection
+ self.use_transactional_tests = false
+
+ def setup
+ super
+ @connection = ActiveRecord::Base.connection
+ @verbose_was = ActiveRecord::Migration.verbose
+ ActiveRecord::Migration.verbose = false
+
+ connection.create_table :testings do |t|
+ t.column :foo, :string, limit: 5
+ t.column :bar, :string, limit: 100
+ end
+ end
+
+ teardown do
+ connection.drop_table :testings rescue nil
+ ActiveRecord::Migration.verbose = @verbose_was
+ ActiveRecord::SchemaMigration.delete_all rescue nil
+ end
+
+ def test_migration_doesnt_remove_named_index
+ connection.add_index :testings, :foo, name: "custom_index_name"
+
+ migration = Class.new(ActiveRecord::Migration[4.2]) {
+ def version; 101 end
+ def migrate(x)
+ remove_index :testings, :foo
+ end
+ }.new
+
+ assert connection.index_exists?(:testings, :foo, name: "custom_index_name")
+ assert_raise(StandardError) { ActiveRecord::Migrator.new(:up, [migration]).migrate }
+ assert connection.index_exists?(:testings, :foo, name: "custom_index_name")
+ end
+
+ def test_migration_does_remove_unnamed_index
+ connection.add_index :testings, :bar
+
+ migration = Class.new(ActiveRecord::Migration[4.2]) {
+ def version; 101 end
+ def migrate(x)
+ remove_index :testings, :bar
+ end
+ }.new
+
+ assert connection.index_exists?(:testings, :bar)
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+ assert_not connection.index_exists?(:testings, :bar)
+ end
+
+ def test_references_does_not_add_index_by_default
+ migration = Class.new(ActiveRecord::Migration[4.2]) {
+ def migrate(x)
+ create_table :more_testings do |t|
+ t.references :foo
+ t.belongs_to :bar, index: false
+ end
+ end
+ }.new
+
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+
+ assert_not connection.index_exists?(:more_testings, :foo_id)
+ assert_not connection.index_exists?(:more_testings, :bar_id)
+ ensure
+ connection.drop_table :more_testings rescue nil
+ end
+
+ def test_timestamps_have_null_constraints_if_not_present_in_migration_of_create_table
+ migration = Class.new(ActiveRecord::Migration[4.2]) {
+ def migrate(x)
+ create_table :more_testings do |t|
+ t.timestamps
+ end
+ end
+ }.new
+
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+
+ assert connection.columns(:more_testings).find { |c| c.name == "created_at" }.null
+ assert connection.columns(:more_testings).find { |c| c.name == "updated_at" }.null
+ ensure
+ connection.drop_table :more_testings rescue nil
+ end
+
+ def test_timestamps_have_null_constraints_if_not_present_in_migration_of_change_table
+ migration = Class.new(ActiveRecord::Migration[4.2]) {
+ def migrate(x)
+ change_table :testings do |t|
+ t.timestamps
+ end
+ end
+ }.new
+
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+
+ assert connection.columns(:testings).find { |c| c.name == "created_at" }.null
+ assert connection.columns(:testings).find { |c| c.name == "updated_at" }.null
+ end
+
+ def test_timestamps_have_null_constraints_if_not_present_in_migration_for_adding_timestamps_to_existing_table
+ migration = Class.new(ActiveRecord::Migration[4.2]) {
+ def migrate(x)
+ add_timestamps :testings
+ end
+ }.new
+
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+
+ assert connection.columns(:testings).find { |c| c.name == "created_at" }.null
+ assert connection.columns(:testings).find { |c| c.name == "updated_at" }.null
+ 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
+
+ def test_legacy_migrations_not_raise_exception_on_reverting_transaction
+ migration = Class.new(ActiveRecord::Migration[5.2]) {
+ def change
+ transaction do
+ execute "select 1"
+ end
+ end
+ }.new
+
+ assert_nothing_raised do
+ migration.migrate(:down)
+ end
+ end
+
+ if current_adapter?(:PostgreSQLAdapter)
+ class Testing < ActiveRecord::Base
+ end
+
+ def test_legacy_change_column_with_null_executes_update
+ migration = Class.new(ActiveRecord::Migration[5.1]) {
+ def migrate(x)
+ change_column :testings, :foo, :string, limit: 10, null: false, default: "foobar"
+ end
+ }.new
+
+ Testing.create!
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+ assert_equal ["foobar"], Testing.all.map(&:foo)
+ ensure
+ ActiveRecord::Base.clear_cache!
+ end
+ end
+ end
+ end
+end
+
+module LegacyPrimaryKeyTestCases
+ include SchemaDumpingHelper
+
+ 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(migration_class) {
+ def change
+ create_table :legacy_primary_keys do |t|
+ t.references :legacy_ref
+ end
+ end
+ }.new
+
+ @migration.migrate(:up)
+
+ assert_legacy_primary_key
+
+ legacy_ref = LegacyPrimaryKey.columns_hash["legacy_ref_id"]
+ assert_not_predicate 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(migration_class) {
+ 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
+
+ def test_legacy_primary_key_in_create_table_should_be_integer
+ @migration = Class.new(migration_class) {
+ def change
+ create_table :legacy_primary_keys, id: false do |t|
+ t.primary_key :id
+ end
+ end
+ }.new
+
+ @migration.migrate(:up)
+
+ assert_legacy_primary_key
+ end
+
+ def test_legacy_primary_key_in_change_table_should_be_integer
+ @migration = Class.new(migration_class) {
+ def change
+ create_table :legacy_primary_keys, id: false do |t|
+ t.integer :dummy
+ end
+ change_table :legacy_primary_keys do |t|
+ t.primary_key :id
+ end
+ end
+ }.new
+
+ @migration.migrate(:up)
+
+ assert_legacy_primary_key
+ end
+
+ def test_add_column_with_legacy_primary_key_should_be_integer
+ @migration = Class.new(migration_class) {
+ def change
+ create_table :legacy_primary_keys, id: false do |t|
+ t.integer :dummy
+ end
+ add_column :legacy_primary_keys, :id, :primary_key
+ end
+ }.new
+
+ @migration.migrate(:up)
+
+ assert_legacy_primary_key
+ end
+
+ def test_legacy_join_table_foreign_keys_should_be_integer
+ @migration = Class.new(migration_class) {
+ def change
+ create_join_table :apples, :bananas do |t|
+ end
+ end
+ }.new
+
+ @migration.migrate(:up)
+
+ schema = dump_table_schema "apples_bananas"
+ assert_match %r{integer "apple_id", null: false}, schema
+ assert_match %r{integer "banana_id", null: false}, schema
+ end
+
+ def test_legacy_join_table_column_options_should_be_overwritten
+ @migration = Class.new(migration_class) {
+ def change
+ create_join_table :apples, :bananas, column_options: { type: :bigint } do |t|
+ end
+ end
+ }.new
+
+ @migration.migrate(:up)
+
+ schema = dump_table_schema "apples_bananas"
+ assert_match %r{bigint "apple_id", null: false}, schema
+ assert_match %r{bigint "banana_id", null: false}, schema
+ end
+
+ if current_adapter?(:Mysql2Adapter)
+ def test_legacy_bigint_primary_key_should_be_auto_incremented
+ @migration = Class.new(migration_class) {
+ def change
+ create_table :legacy_primary_keys, id: :bigint
+ end
+ }.new
+
+ @migration.migrate(:up)
+
+ legacy_pk = LegacyPrimaryKey.columns_hash["id"]
+ assert_predicate legacy_pk, :bigint?
+ assert_predicate legacy_pk, :auto_increment?
+
+ schema = dump_table_schema "legacy_primary_keys"
+ assert_match %r{create_table "legacy_primary_keys", (?!id: :bigint, default: nil)}, schema
+ end
+ else
+ def test_legacy_bigint_primary_key_should_not_be_auto_incremented
+ @migration = Class.new(migration_class) {
+ 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
+
+ private
+ def assert_legacy_primary_key
+ assert_equal "id", LegacyPrimaryKey.primary_key
+
+ legacy_pk = LegacyPrimaryKey.columns_hash["id"]
+
+ assert_equal :integer, legacy_pk.type
+ assert_not_predicate legacy_pk, :bigint?
+ assert_not legacy_pk.null
+
+ if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)
+ schema = dump_table_schema "legacy_primary_keys"
+ assert_match %r{create_table "legacy_primary_keys", id: :(?:integer|serial), (?!default: nil)}, schema
+ end
+ end
+end
+
+module LegacyPrimaryKeyTest
+ class V5_0 < ActiveRecord::TestCase
+ include LegacyPrimaryKeyTestCases
+
+ self.use_transactional_tests = false
+
+ private
+ def migration_class
+ ActiveRecord::Migration[5.0]
+ end
+ end
+
+ class V4_2 < ActiveRecord::TestCase
+ include LegacyPrimaryKeyTestCases
+
+ self.use_transactional_tests = false
+
+ private
+ def migration_class
+ ActiveRecord::Migration[4.2]
+ end
+ end
+end
diff --git a/activerecord/test/cases/migration/create_join_table_test.rb b/activerecord/test/cases/migration/create_join_table_test.rb
index 8fd08fe4ce..e0cbb29dcf 100644
--- a/activerecord/test/cases/migration/create_join_table_test.rb
+++ b/activerecord/test/cases/migration/create_join_table_test.rb
@@ -1,4 +1,6 @@
-require 'cases/helper'
+# frozen_string_literal: true
+
+require "cases/helper"
module ActiveRecord
class Migration
@@ -12,7 +14,7 @@ module ActiveRecord
teardown do
%w(artists_musics musics_videos catalog).each do |table_name|
- connection.drop_table table_name if connection.tables.include?(table_name)
+ connection.drop_table table_name, if_exists: true
end
end
@@ -29,13 +31,13 @@ module ActiveRecord
end
def test_create_join_table_with_strings
- connection.create_join_table 'artists', 'musics'
+ connection.create_join_table "artists", "musics"
assert_equal %w(artist_id music_id), connection.columns(:artists_musics).map(&:name).sort
end
def test_create_join_table_with_symbol_and_string
- connection.create_join_table :artists, 'musics'
+ connection.create_join_table :artists, "musics"
assert_equal %w(artist_id music_id), connection.columns(:artists_musics).map(&:name).sort
end
@@ -53,13 +55,13 @@ module ActiveRecord
end
def test_create_join_table_with_the_table_name_as_string
- connection.create_join_table :artists, :musics, table_name: 'catalog'
+ connection.create_join_table :artists, :musics, table_name: "catalog"
assert_equal %w(artist_id music_id), connection.columns(:catalog).map(&:name).sort
end
def test_create_join_table_with_column_options
- connection.create_join_table :artists, :musics, column_options: {null: true}
+ connection.create_join_table :artists, :musics, column_options: { null: true }
assert_equal [true, true], connection.columns(:artists_musics).map(&:null)
end
@@ -67,7 +69,7 @@ module ActiveRecord
def test_create_join_table_without_indexes
connection.create_join_table :artists, :musics
- assert connection.indexes(:artists_musics).blank?
+ assert_predicate connection.indexes(:artists_musics), :blank?
end
def test_create_join_table_with_index
@@ -78,66 +80,84 @@ 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
- assert !connection.tables.include?('artists_musics')
+ assert_not connection.table_exists?("artists_musics")
end
def test_drop_join_table_with_strings
connection.create_join_table :artists, :musics
- connection.drop_join_table 'artists', 'musics'
+ connection.drop_join_table "artists", "musics"
- assert !connection.tables.include?('artists_musics')
+ assert_not connection.table_exists?("artists_musics")
end
def test_drop_join_table_with_the_proper_order
connection.create_join_table :videos, :musics
connection.drop_join_table :videos, :musics
- assert !connection.tables.include?('musics_videos')
+ assert_not connection.table_exists?("musics_videos")
end
def test_drop_join_table_with_the_table_name
connection.create_join_table :artists, :musics, table_name: :catalog
connection.drop_join_table :artists, :musics, table_name: :catalog
- assert !connection.tables.include?('catalog')
+ assert_not connection.table_exists?("catalog")
end
def test_drop_join_table_with_the_table_name_as_string
- connection.create_join_table :artists, :musics, table_name: 'catalog'
- connection.drop_join_table :artists, :musics, table_name: 'catalog'
+ connection.create_join_table :artists, :musics, table_name: "catalog"
+ connection.drop_join_table :artists, :musics, table_name: "catalog"
- assert !connection.tables.include?('catalog')
+ assert_not connection.table_exists?("catalog")
end
def test_drop_join_table_with_column_options
- connection.create_join_table :artists, :musics, column_options: {null: true}
- connection.drop_join_table :artists, :musics, column_options: {null: true}
+ connection.create_join_table :artists, :musics, column_options: { null: true }
+ connection.drop_join_table :artists, :musics, column_options: { null: true }
- assert !connection.tables.include?('artists_musics')
+ assert_not 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'
- assert_includes connection.tables, 'audio_artists_musics'
+ connection.create_join_table "audio_artists", "audio_musics"
+ assert connection.table_exists?("audio_artists_musics")
+
+ connection.drop_join_table "audio_artists", "audio_musics"
+ assert_not connection.table_exists?("audio_artists_musics"), "Should have dropped join table, but didn't"
+ end
+ end
- connection.drop_join_table 'audio_artists', 'audio_musics'
- assert !connection.tables.include?('audio_artists_musics'), "Should have dropped join table, but didn't"
+ if current_adapter?(:PostgreSQLAdapter)
+ def test_create_join_table_with_uuid
+ connection.create_join_table :artists, :musics, column_options: { type: :uuid }
+ assert_equal [:uuid, :uuid], connection.columns(:artists_musics).map(&:type)
end
end
private
def with_table_cleanup
- tables_before = connection.tables
+ tables_before = connection.data_sources
yield
ensure
- tables_after = connection.tables - tables_before
+ tables_after = connection.data_sources - tables_before
tables_after.each do |table|
connection.drop_table table
diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb
index 72f2fa95f1..bb233fbf74 100644
--- a/activerecord/test/cases/migration/foreign_key_test.rb
+++ b/activerecord/test/cases/migration/foreign_key_test.rb
@@ -1,84 +1,184 @@
-require 'cases/helper'
-require 'support/ddl_helper'
-require 'support/schema_dumping_helper'
+# frozen_string_literal: true
-if ActiveRecord::Base.connection.supports_foreign_keys?
-module ActiveRecord
- class Migration
- class ForeignKeyTest < ActiveRecord::TestCase
- include DdlHelper
- include SchemaDumpingHelper
- include ActiveSupport::Testing::Stream
-
- class Rocket < ActiveRecord::Base
- end
+require "cases/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
- class Astronaut < ActiveRecord::Base
+ 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
- setup do
- @connection = ActiveRecord::Base.connection
- @connection.create_table "rockets", force: true do |t|
- t.string :name
+ class ForeignKeyChangeColumnTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
+
+ class Rocket < ActiveRecord::Base
+ has_many :astronauts
end
- @connection.create_table "astronauts", force: true do |t|
- t.string :name
- t.references :rocket
+ class Astronaut < ActiveRecord::Base
+ belongs_to :rocket
+ end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table "rockets", force: true do |t|
+ t.string :name
+ end
+
+ @connection.create_table "astronauts", force: true do |t|
+ t.string :name
+ t.references :rocket, foreign_key: true
+ end
+ Rocket.reset_column_information
+ Astronaut.reset_column_information
end
- end
- teardown do
- if defined?(@connection)
+ teardown do
@connection.drop_table "astronauts", if_exists: true
@connection.drop_table "rockets", if_exists: true
+ Rocket.reset_column_information
+ Astronaut.reset_column_information
end
- end
- def test_foreign_keys
- foreign_keys = @connection.foreign_keys("fk_test_has_fk")
- assert_equal 1, foreign_keys.size
+ def test_change_column_of_parent_table
+ rocket = Rocket.create!(name: "myrocket")
+ rocket.astronauts << Astronaut.create!
- 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
- end
+ @connection.change_column_null :rockets, :name, false
- def test_add_foreign_key_inferes_column
- @connection.add_foreign_key :astronauts, :rockets
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
- foreign_keys = @connection.foreign_keys("astronauts")
- assert_equal 1, foreign_keys.size
+ fk = foreign_keys.first
+ assert_equal "myrocket", Rocket.first.name
+ assert_equal "astronauts", fk.from_table
+ assert_equal "rockets", fk.to_table
+ end
- fk = foreign_keys.first
- assert_equal "astronauts", fk.from_table
- assert_equal "rockets", fk.to_table
- assert_equal "rocket_id", fk.column
- assert_equal "id", fk.primary_key
- assert_equal("fk_rails_78146ddd2e", fk.name)
- end
+ def test_rename_column_of_child_table
+ rocket = Rocket.create!(name: "myrocket")
+ rocket.astronauts << Astronaut.create!
+
+ @connection.rename_column :astronauts, :name, :astronaut_name
- def test_add_foreign_key_with_column
- @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id"
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal "myrocket", Rocket.first.name
+ assert_equal "astronauts", fk.from_table
+ assert_equal "rockets", fk.to_table
+ end
- foreign_keys = @connection.foreign_keys("astronauts")
- assert_equal 1, foreign_keys.size
+ def test_rename_reference_column_of_child_table
+ rocket = Rocket.create!(name: "myrocket")
+ rocket.astronauts << Astronaut.create!
- fk = foreign_keys.first
- assert_equal "astronauts", fk.from_table
- assert_equal "rockets", fk.to_table
- assert_equal "rocket_id", fk.column
- assert_equal "id", fk.primary_key
- assert_equal("fk_rails_78146ddd2e", fk.name)
+ @connection.rename_column :astronauts, :rocket_id, :new_rocket_id
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal "myrocket", Rocket.first.name
+ assert_equal "astronauts", fk.from_table
+ assert_equal "rockets", fk.to_table
+ assert_equal "new_rocket_id", fk.options[:column]
+ end
end
+ end
+ end
+end
+
+if ActiveRecord::Base.connection.supports_foreign_keys?
+ module ActiveRecord
+ class Migration
+ class ForeignKeyTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
+ include ActiveSupport::Testing::Stream
+
+ class Rocket < ActiveRecord::Base
+ end
+
+ class Astronaut < ActiveRecord::Base
+ end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table "rockets", force: true do |t|
+ t.string :name
+ end
+
+ @connection.create_table "astronauts", force: true do |t|
+ t.string :name
+ t.references :rocket
+ end
+ end
+
+ teardown do
+ @connection.drop_table "astronauts", if_exists: true
+ @connection.drop_table "rockets", if_exists: true
+ end
+
+ def test_foreign_keys
+ foreign_keys = @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
+ end
+
+ def test_add_foreign_key_inferes_column
+ @connection.add_foreign_key :astronauts, :rockets
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal "astronauts", fk.from_table
+ assert_equal "rockets", fk.to_table
+ assert_equal "rocket_id", fk.column
+ assert_equal "id", fk.primary_key
+ assert_equal("fk_rails_78146ddd2e", fk.name)
+ end
+
+ def test_add_foreign_key_with_column
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id"
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal "astronauts", fk.from_table
+ assert_equal "rockets", fk.to_table
+ assert_equal "rocket_id", fk.column
+ assert_equal "id", fk.primary_key
+ assert_equal("fk_rails_78146ddd2e", fk.name)
+ end
+
+ def test_add_foreign_key_with_non_standard_primary_key
+ @connection.create_table :space_shuttles, id: false, force: true do |t|
+ t.bigint :pk, primary_key: true
+ 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")
+ column: "rocket_id", primary_key: "pk", name: "custom_pk")
foreign_keys = @connection.foreign_keys("astronauts")
assert_equal 1, foreign_keys.size
@@ -87,218 +187,311 @@ module ActiveRecord
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
- end
- def test_add_on_delete_restrict_foreign_key
- @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :restrict
+ def test_add_on_delete_restrict_foreign_key
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :restrict
- foreign_keys = @connection.foreign_keys("astronauts")
- assert_equal 1, foreign_keys.size
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
- fk = foreign_keys.first
- if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
- # ON DELETE RESTRICT is the default on MySQL
- assert_equal nil, fk.on_delete
- else
- assert_equal :restrict, fk.on_delete
+ fk = foreign_keys.first
+ if current_adapter?(:Mysql2Adapter)
+ # ON DELETE RESTRICT is the default on MySQL
+ assert_nil fk.on_delete
+ else
+ assert_equal :restrict, fk.on_delete
+ end
end
- end
- def test_add_on_delete_cascade_foreign_key
- @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :cascade
+ def test_add_on_delete_cascade_foreign_key
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :cascade
- foreign_keys = @connection.foreign_keys("astronauts")
- assert_equal 1, foreign_keys.size
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
- fk = foreign_keys.first
- assert_equal :cascade, fk.on_delete
- end
+ fk = foreign_keys.first
+ assert_equal :cascade, fk.on_delete
+ end
- def test_add_on_delete_nullify_foreign_key
- @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :nullify
+ def test_add_on_delete_nullify_foreign_key
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :nullify
- foreign_keys = @connection.foreign_keys("astronauts")
- assert_equal 1, foreign_keys.size
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
- fk = foreign_keys.first
- assert_equal :nullify, fk.on_delete
- end
+ fk = foreign_keys.first
+ assert_equal :nullify, fk.on_delete
+ end
- def test_on_update_and_on_delete_raises_with_invalid_values
- assert_raises ArgumentError do
- @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :invalid
+ def test_on_update_and_on_delete_raises_with_invalid_values
+ assert_raises ArgumentError do
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :invalid
+ end
+
+ assert_raises ArgumentError do
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_update: :invalid
+ end
end
- assert_raises ArgumentError do
- @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_update: :invalid
+ def test_add_foreign_key_with_on_update
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_update: :nullify
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal :nullify, fk.on_update
end
- end
- def test_add_foreign_key_with_on_update
- @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_update: :nullify
+ def test_foreign_key_exists
+ @connection.add_foreign_key :astronauts, :rockets
- foreign_keys = @connection.foreign_keys("astronauts")
- assert_equal 1, foreign_keys.size
+ assert @connection.foreign_key_exists?(:astronauts, :rockets)
+ assert_not @connection.foreign_key_exists?(:astronauts, :stars)
+ end
- fk = foreign_keys.first
- assert_equal :nullify, fk.on_update
- end
+ def test_foreign_key_exists_by_column
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id"
- def test_foreign_key_exists
- @connection.add_foreign_key :astronauts, :rockets
+ assert @connection.foreign_key_exists?(:astronauts, column: "rocket_id")
+ assert_not @connection.foreign_key_exists?(:astronauts, column: "star_id")
+ end
- assert @connection.foreign_key_exists?(:astronauts, :rockets)
- assert_not @connection.foreign_key_exists?(:astronauts, :stars)
- end
+ def test_foreign_key_exists_by_name
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk"
- def test_foreign_key_exists_by_column
- @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id"
+ assert @connection.foreign_key_exists?(:astronauts, name: "fancy_named_fk")
+ assert_not @connection.foreign_key_exists?(:astronauts, name: "other_fancy_named_fk")
+ end
- assert @connection.foreign_key_exists?(:astronauts, column: "rocket_id")
- assert_not @connection.foreign_key_exists?(:astronauts, column: "star_id")
- end
+ def test_remove_foreign_key_inferes_column
+ @connection.add_foreign_key :astronauts, :rockets
+
+ assert_equal 1, @connection.foreign_keys("astronauts").size
+ @connection.remove_foreign_key :astronauts, :rockets
+ assert_equal [], @connection.foreign_keys("astronauts")
+ end
- def test_foreign_key_exists_by_name
- @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk"
+ def test_remove_foreign_key_by_column
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id"
- assert @connection.foreign_key_exists?(:astronauts, name: "fancy_named_fk")
- assert_not @connection.foreign_key_exists?(:astronauts, name: "other_fancy_named_fk")
- end
+ assert_equal 1, @connection.foreign_keys("astronauts").size
+ @connection.remove_foreign_key :astronauts, column: "rocket_id"
+ assert_equal [], @connection.foreign_keys("astronauts")
+ end
- def test_remove_foreign_key_inferes_column
- @connection.add_foreign_key :astronauts, :rockets
+ def test_remove_foreign_key_by_symbol_column
+ @connection.add_foreign_key :astronauts, :rockets, column: :rocket_id
- assert_equal 1, @connection.foreign_keys("astronauts").size
- @connection.remove_foreign_key :astronauts, :rockets
- assert_equal [], @connection.foreign_keys("astronauts")
- end
+ assert_equal 1, @connection.foreign_keys("astronauts").size
+ @connection.remove_foreign_key :astronauts, column: :rocket_id
+ assert_equal [], @connection.foreign_keys("astronauts")
+ end
- def test_remove_foreign_key_by_column
- @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id"
+ def test_remove_foreign_key_by_name
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk"
- assert_equal 1, @connection.foreign_keys("astronauts").size
- @connection.remove_foreign_key :astronauts, column: "rocket_id"
- assert_equal [], @connection.foreign_keys("astronauts")
- end
+ assert_equal 1, @connection.foreign_keys("astronauts").size
+ @connection.remove_foreign_key :astronauts, name: "fancy_named_fk"
+ assert_equal [], @connection.foreign_keys("astronauts")
+ end
- def test_remove_foreign_key_by_symbol_column
- @connection.add_foreign_key :astronauts, :rockets, column: :rocket_id
+ def test_remove_foreign_non_existing_foreign_key_raises
+ assert_raises ArgumentError do
+ @connection.remove_foreign_key :astronauts, :rockets
+ end
+ end
- assert_equal 1, @connection.foreign_keys("astronauts").size
- @connection.remove_foreign_key :astronauts, column: :rocket_id
- assert_equal [], @connection.foreign_keys("astronauts")
- end
+ if ActiveRecord::Base.connection.supports_validate_constraints?
+ def test_add_invalid_foreign_key
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", validate: false
- def test_remove_foreign_key_by_name
- @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk"
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
- assert_equal 1, @connection.foreign_keys("astronauts").size
- @connection.remove_foreign_key :astronauts, name: "fancy_named_fk"
- assert_equal [], @connection.foreign_keys("astronauts")
- end
+ fk = foreign_keys.first
+ assert_not_predicate fk, :validated?
+ end
- def test_remove_foreign_non_existing_foreign_key_raises
- assert_raises ArgumentError do
- @connection.remove_foreign_key :astronauts, :rockets
+ def test_validate_foreign_key_infers_column
+ @connection.add_foreign_key :astronauts, :rockets, validate: false
+ assert_not_predicate @connection.foreign_keys("astronauts").first, :validated?
+
+ @connection.validate_foreign_key :astronauts, :rockets
+ assert_predicate @connection.foreign_keys("astronauts").first, :validated?
+ end
+
+ def test_validate_foreign_key_by_column
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", validate: false
+ assert_not_predicate @connection.foreign_keys("astronauts").first, :validated?
+
+ @connection.validate_foreign_key :astronauts, column: "rocket_id"
+ assert_predicate @connection.foreign_keys("astronauts").first, :validated?
+ end
+
+ def test_validate_foreign_key_by_symbol_column
+ @connection.add_foreign_key :astronauts, :rockets, column: :rocket_id, validate: false
+ assert_not_predicate @connection.foreign_keys("astronauts").first, :validated?
+
+ @connection.validate_foreign_key :astronauts, column: :rocket_id
+ assert_predicate @connection.foreign_keys("astronauts").first, :validated?
+ end
+
+ def test_validate_foreign_key_by_name
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk", validate: false
+ assert_not_predicate @connection.foreign_keys("astronauts").first, :validated?
+
+ @connection.validate_foreign_key :astronauts, name: "fancy_named_fk"
+ assert_predicate @connection.foreign_keys("astronauts").first, :validated?
+ end
+
+ def test_validate_foreign_non_existing_foreign_key_raises
+ assert_raises ArgumentError do
+ @connection.validate_foreign_key :astronauts, :rockets
+ end
+ end
+
+ def test_validate_constraint_by_name
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk", validate: false
+
+ @connection.validate_constraint :astronauts, "fancy_named_fk"
+ assert_predicate @connection.foreign_keys("astronauts").first, :validated?
+ end
+ else
+ # Foreign key should still be created, but should not be invalid
+ def test_add_invalid_foreign_key
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", validate: false
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_predicate fk, :validated?
+ end
end
- end
- def test_schema_dumping
- @connection.add_foreign_key :astronauts, :rockets
- output = dump_table_schema "astronauts"
- assert_match %r{\s+add_foreign_key "astronauts", "rockets"$}, output
- end
+ def test_schema_dumping
+ @connection.add_foreign_key :astronauts, :rockets
+ output = dump_table_schema "astronauts"
+ assert_match %r{\s+add_foreign_key "astronauts", "rockets"$}, output
+ end
- def test_schema_dumping_with_options
- output = dump_table_schema "fk_test_has_fk"
- assert_match %r{\s+add_foreign_key "fk_test_has_fk", "fk_test_has_pk", column: "fk_id", primary_key: "pk_id", name: "fk_name"$}, output
- end
+ def test_schema_dumping_with_options
+ output = dump_table_schema "fk_test_has_fk"
+ assert_match %r{\s+add_foreign_key "fk_test_has_fk", "fk_test_has_pk", column: "fk_id", primary_key: "pk_id", name: "fk_name"$}, output
+ end
- def test_schema_dumping_on_delete_and_on_update_options
- @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :nullify, on_update: :cascade
+ def test_schema_dumping_with_custom_fk_ignore_pattern
+ original_pattern = ActiveRecord::SchemaDumper.fk_ignore_pattern
+ ActiveRecord::SchemaDumper.fk_ignore_pattern = /^ignored_/
+ @connection.add_foreign_key :astronauts, :rockets, name: :ignored_fk_astronauts_rockets
- output = dump_table_schema "astronauts"
- assert_match %r{\s+add_foreign_key "astronauts",.+on_update: :cascade,.+on_delete: :nullify$}, output
- end
+ output = dump_table_schema "astronauts"
+ assert_match %r{\s+add_foreign_key "astronauts", "rockets"$}, output
+
+ ActiveRecord::SchemaDumper.fk_ignore_pattern = original_pattern
+ end
- class CreateCitiesAndHousesMigration < ActiveRecord::Migration
- def change
- create_table("cities") { |t| }
+ def test_schema_dumping_on_delete_and_on_update_options
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :nullify, on_update: :cascade
+
+ output = dump_table_schema "astronauts"
+ assert_match %r{\s+add_foreign_key "astronauts",.+on_update: :cascade,.+on_delete: :nullify$}, output
+ end
- create_table("houses") do |t|
- t.column :city_id, :integer
+ class CreateCitiesAndHousesMigration < ActiveRecord::Migration::Current
+ def change
+ create_table("cities") { |t| }
+
+ create_table("houses") do |t|
+ t.references :city
+ end
+ add_foreign_key :houses, :cities, column: "city_id"
+
+ # remove and re-add to test that schema is updated and not accidentally cached
+ remove_foreign_key :houses, :cities
+ add_foreign_key :houses, :cities, column: "city_id", on_delete: :cascade
end
- add_foreign_key :houses, :cities, column: "city_id"
end
- end
- def test_add_foreign_key_is_reversible
- migration = CreateCitiesAndHousesMigration.new
- silence_stream($stdout) { migration.migrate(:up) }
- assert_equal 1, @connection.foreign_keys("houses").size
- ensure
- silence_stream($stdout) { migration.migrate(:down) }
- end
+ def test_add_foreign_key_is_reversible
+ migration = CreateCitiesAndHousesMigration.new
+ silence_stream($stdout) { migration.migrate(:up) }
+ assert_equal 1, @connection.foreign_keys("houses").size
+ ensure
+ silence_stream($stdout) { migration.migrate(:down) }
+ end
- class CreateSchoolsAndClassesMigration < ActiveRecord::Migration
- def change
- create_table(:schools)
+ def test_foreign_key_constraint_is_not_cached_incorrectly
+ migration = CreateCitiesAndHousesMigration.new
+ silence_stream($stdout) { migration.migrate(:up) }
+ output = dump_table_schema "houses"
+ assert_match %r{\s+add_foreign_key "houses",.+on_delete: :cascade$}, output
+ ensure
+ silence_stream($stdout) { migration.migrate(:down) }
+ end
- create_table(:classes) do |t|
- t.column :school_id, :integer
+ class CreateSchoolsAndClassesMigration < ActiveRecord::Migration::Current
+ def change
+ create_table(:schools)
+
+ create_table(:classes) do |t|
+ t.references :school
+ end
+ add_foreign_key :classes, :schools
end
- add_foreign_key :classes, :schools
end
- end
- def test_add_foreign_key_with_prefix
- ActiveRecord::Base.table_name_prefix = 'p_'
- migration = CreateSchoolsAndClassesMigration.new
- silence_stream($stdout) { migration.migrate(:up) }
- assert_equal 1, @connection.foreign_keys("p_classes").size
- ensure
- silence_stream($stdout) { migration.migrate(:down) }
- ActiveRecord::Base.table_name_prefix = nil
- end
+ def test_add_foreign_key_with_prefix
+ ActiveRecord::Base.table_name_prefix = "p_"
+ migration = CreateSchoolsAndClassesMigration.new
+ silence_stream($stdout) { migration.migrate(:up) }
+ assert_equal 1, @connection.foreign_keys("p_classes").size
+ ensure
+ silence_stream($stdout) { migration.migrate(:down) }
+ ActiveRecord::Base.table_name_prefix = nil
+ end
- def test_add_foreign_key_with_suffix
- ActiveRecord::Base.table_name_suffix = '_s'
- migration = CreateSchoolsAndClassesMigration.new
- silence_stream($stdout) { migration.migrate(:up) }
- assert_equal 1, @connection.foreign_keys("classes_s").size
- ensure
- silence_stream($stdout) { migration.migrate(:down) }
- ActiveRecord::Base.table_name_suffix = nil
+ def test_add_foreign_key_with_suffix
+ ActiveRecord::Base.table_name_suffix = "_s"
+ migration = CreateSchoolsAndClassesMigration.new
+ silence_stream($stdout) { migration.migrate(:up) }
+ assert_equal 1, @connection.foreign_keys("classes_s").size
+ ensure
+ silence_stream($stdout) { migration.migrate(:down) }
+ ActiveRecord::Base.table_name_suffix = nil
+ end
end
-
end
end
-end
else
-module ActiveRecord
- class Migration
- class NoForeignKeySupportTest < ActiveRecord::TestCase
- setup do
- @connection = ActiveRecord::Base.connection
- end
+ module ActiveRecord
+ class Migration
+ class NoForeignKeySupportTest < ActiveRecord::TestCase
+ setup do
+ @connection = ActiveRecord::Base.connection
+ end
- def test_add_foreign_key_should_be_noop
- @connection.add_foreign_key :clubs, :categories
- end
+ def test_add_foreign_key_should_be_noop
+ @connection.add_foreign_key :clubs, :categories
+ end
- def test_remove_foreign_key_should_be_noop
- @connection.remove_foreign_key :clubs, :categories
- end
+ def test_remove_foreign_key_should_be_noop
+ @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
end
end
end
-end
diff --git a/activerecord/test/cases/migration/helper.rb b/activerecord/test/cases/migration/helper.rb
index 5bc0898f33..c056199140 100644
--- a/activerecord/test/cases/migration/helper.rb
+++ b/activerecord/test/cases/migration/helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
module ActiveRecord
@@ -28,12 +30,12 @@ module ActiveRecord
super
TestModel.reset_table_name
TestModel.reset_sequence_name
- connection.drop_table :test_models rescue nil
+ connection.drop_table :test_models, if_exists: true
end
private
- delegate(*CONNECTION_METHODS, to: :connection)
+ delegate(*CONNECTION_METHODS, to: :connection)
end
end
end
diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb
index b23b9a679f..f8fecc83cd 100644
--- a/activerecord/test/cases/migration/index_test.rb
+++ b/activerecord/test/cases/migration/index_test.rb
@@ -1,4 +1,6 @@
-require 'cases/helper'
+# frozen_string_literal: true
+
+require "cases/helper"
module ActiveRecord
class Migration
@@ -11,12 +13,12 @@ module ActiveRecord
@table_name = :testings
connection.create_table table_name do |t|
- t.column :foo, :string, :limit => 100
- t.column :bar, :string, :limit => 100
+ t.column :foo, :string, limit: 100
+ t.column :bar, :string, limit: 100
t.string :first_name
- t.string :last_name, :limit => 100
- t.string :key, :limit => 100
+ t.string :last_name, limit: 100
+ t.string :key, limit: 100
t.boolean :administrator
end
end
@@ -28,32 +30,29 @@ module ActiveRecord
def test_rename_index
# keep the names short to make Oracle and similar behave
- connection.add_index(table_name, [:foo], :name => 'old_idx')
- connection.rename_index(table_name, 'old_idx', 'new_idx')
+ 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_not connection.index_name_exists?(table_name, "old_idx")
+ assert connection.index_name_exists?(table_name, "new_idx")
end
def test_rename_index_too_long
- too_long_index_name = good_index_name + 'x'
+ too_long_index_name = good_index_name + "x"
# keep the names short to make Oracle and similar behave
- connection.add_index(table_name, [:foo], :name => 'old_idx')
+ connection.add_index(table_name, [:foo], name: "old_idx")
e = assert_raises(ArgumentError) {
- connection.rename_index(table_name, 'old_idx', too_long_index_name)
+ connection.rename_index(table_name, "old_idx", too_long_index_name)
}
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
- connection.add_index(table_name, [:foo], :name => 'some_idx')
+ connection.add_index(table_name, [:foo], name: "some_idx")
assert_raises(ArgumentError) {
- connection.add_index(table_name, [:foo], :name => 'some_idx')
+ connection.add_index(table_name, [:foo], name: "some_idx")
}
end
@@ -64,43 +63,43 @@ 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
def test_add_index_does_not_accept_too_long_index_names
- too_long_index_name = good_index_name + 'x'
+ too_long_index_name = good_index_name + "x"
e = assert_raises(ArgumentError) {
connection.add_index(table_name, "foo", name: too_long_index_name)
}
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)
- connection.add_index(table_name, "foo", :name => good_index_name)
+ assert_not connection.index_name_exists?(table_name, too_long_index_name)
+ connection.add_index(table_name, "foo", name: good_index_name)
end
def test_internal_index_with_name_matching_database_limit
- good_index_name = 'x' * connection.index_name_length
+ 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
def test_index_symbol_names
- connection.add_index table_name, :foo, :name => :symbol_index_name
- assert connection.index_exists?(table_name, :foo, :name => :symbol_index_name)
+ connection.add_index table_name, :foo, name: :symbol_index_name
+ assert connection.index_exists?(table_name, :foo, name: :symbol_index_name)
- connection.remove_index table_name, :name => :symbol_index_name
- assert_not connection.index_exists?(table_name, :foo, :name => :symbol_index_name)
+ connection.remove_index table_name, name: :symbol_index_name
+ assert_not connection.index_exists?(table_name, :foo, name: :symbol_index_name)
end
def test_index_exists
connection.add_index :testings, :foo
assert connection.index_exists?(:testings, :foo)
- assert !connection.index_exists?(:testings, :bar)
+ assert_not connection.index_exists?(:testings, :bar)
end
def test_index_exists_on_multiple_columns
@@ -122,19 +121,32 @@ module ActiveRecord
end
def test_unique_index_exists
- connection.add_index :testings, :foo, :unique => true
+ connection.add_index :testings, :foo, unique: true
- assert connection.index_exists?(:testings, :foo, :unique => true)
+ assert connection.index_exists?(:testings, :foo, unique: true)
end
def test_named_index_exists
- connection.add_index :testings, :foo, :name => "custom_index_name"
+ connection.add_index :testings, :foo, name: "custom_index_name"
- assert connection.index_exists?(:testings, :foo, :name => "custom_index_name")
+ assert connection.index_exists?(:testings, :foo)
+ assert connection.index_exists?(:testings, :foo, name: "custom_index_name")
+ assert_not connection.index_exists?(:testings, :foo, name: "other_index_name")
+ end
+
+ def test_remove_named_index
+ connection.add_index :testings, :foo, name: "index_testings_on_custom_index_name"
+
+ assert connection.index_exists?(:testings, :foo)
+
+ assert_raise(ArgumentError) { connection.remove_index(:testings, "custom_index_name") }
+
+ connection.remove_index :testings, :foo
+ assert_not connection.index_exists?(:testings, :foo)
end
def test_add_index_attribute_length_limit
- connection.add_index :testings, [:foo, :bar], :length => {:foo => 10, :bar => nil}
+ connection.add_index :testings, [:foo, :bar], length: { foo: 10, bar: nil }
assert connection.index_exists?(:testings, [:foo, :bar])
end
@@ -144,65 +156,64 @@ module ActiveRecord
connection.remove_index("testings", "last_name")
connection.add_index("testings", ["last_name", "first_name"])
- connection.remove_index("testings", :column => ["last_name", "first_name"])
+ connection.remove_index("testings", column: ["last_name", "first_name"])
# Oracle adapter cannot have specified index name larger than 30 characters
# Oracle adapter is shortening index name when just column list is given
unless current_adapter?(:OracleAdapter)
connection.add_index("testings", ["last_name", "first_name"])
- connection.remove_index("testings", :name => :index_testings_on_last_name_and_first_name)
+ connection.remove_index("testings", name: :index_testings_on_last_name_and_first_name)
connection.add_index("testings", ["last_name", "first_name"])
connection.remove_index("testings", "last_name_and_first_name")
end
connection.add_index("testings", ["last_name", "first_name"])
connection.remove_index("testings", ["last_name", "first_name"])
- connection.add_index("testings", ["last_name"], :length => 10)
+ connection.add_index("testings", ["last_name"], length: 10)
connection.remove_index("testings", "last_name")
- connection.add_index("testings", ["last_name"], :length => {:last_name => 10})
+ connection.add_index("testings", ["last_name"], length: { last_name: 10 })
connection.remove_index("testings", ["last_name"])
- connection.add_index("testings", ["last_name", "first_name"], :length => 10)
+ connection.add_index("testings", ["last_name", "first_name"], length: 10)
connection.remove_index("testings", ["last_name", "first_name"])
- connection.add_index("testings", ["last_name", "first_name"], :length => {:last_name => 10, :first_name => 20})
+ connection.add_index("testings", ["last_name", "first_name"], length: { last_name: 10, first_name: 20 })
connection.remove_index("testings", ["last_name", "first_name"])
- connection.add_index("testings", ["key"], :name => "key_idx", :unique => true)
- connection.remove_index("testings", :name => "key_idx", :unique => true)
+ connection.add_index("testings", ["key"], name: "key_idx", unique: true)
+ connection.remove_index("testings", name: "key_idx", unique: true)
- connection.add_index("testings", %w(last_name first_name administrator), :name => "named_admin")
- connection.remove_index("testings", :name => "named_admin")
+ connection.add_index("testings", %w(last_name first_name administrator), name: "named_admin")
+ connection.remove_index("testings", name: "named_admin")
# Selected adapters support index sort order
- if current_adapter?(:SQLite3Adapter, :MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter)
- connection.add_index("testings", ["last_name"], :order => {:last_name => :desc})
+ if current_adapter?(:SQLite3Adapter, :Mysql2Adapter, :PostgreSQLAdapter)
+ connection.add_index("testings", ["last_name"], order: { last_name: :desc })
connection.remove_index("testings", ["last_name"])
- connection.add_index("testings", ["last_name", "first_name"], :order => {:last_name => :desc})
+ connection.add_index("testings", ["last_name", "first_name"], order: { last_name: :desc })
connection.remove_index("testings", ["last_name", "first_name"])
- connection.add_index("testings", ["last_name", "first_name"], :order => {:last_name => :desc, :first_name => :asc})
+ connection.add_index("testings", ["last_name", "first_name"], order: { last_name: :desc, first_name: :asc })
connection.remove_index("testings", ["last_name", "first_name"])
- connection.add_index("testings", ["last_name", "first_name"], :order => :desc)
+ connection.add_index("testings", ["last_name", "first_name"], order: :desc)
connection.remove_index("testings", ["last_name", "first_name"])
end
end
if current_adapter?(:PostgreSQLAdapter)
def test_add_partial_index
- connection.add_index("testings", "last_name", :where => "first_name = 'john doe'")
+ connection.add_index("testings", "last_name", where: "first_name = 'john doe'")
assert connection.index_exists?("testings", "last_name")
connection.remove_index("testings", "last_name")
- assert !connection.index_exists?("testings", "last_name")
+ assert_not connection.index_exists?("testings", "last_name")
end
end
private
def good_index_name
- 'x' * connection.allowed_index_name_length
+ "x" * connection.allowed_index_name_length
end
-
end
end
end
diff --git a/activerecord/test/cases/migration/logger_test.rb b/activerecord/test/cases/migration/logger_test.rb
index bf6e684887..28f4cc124b 100644
--- a/activerecord/test/cases/migration/logger_test.rb
+++ b/activerecord/test/cases/migration/logger_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
module ActiveRecord
@@ -8,7 +10,7 @@ module ActiveRecord
Migration = Struct.new(:name, :version) do
def disable_ddl_transaction; false end
- def migrate direction
+ def migrate(direction)
# do nothing
end
end
@@ -26,7 +28,7 @@ module ActiveRecord
def test_migration_should_be_run_without_logger
previous_logger = ActiveRecord::Base.logger
ActiveRecord::Base.logger = nil
- migrations = [Migration.new('a', 1), Migration.new('b', 2), Migration.new('c', 3)]
+ migrations = [Migration.new("a", 1), Migration.new("b", 2), Migration.new("c", 3)]
ActiveRecord::Migrator.new(:up, migrations).migrate
ensure
ActiveRecord::Base.logger = previous_logger
diff --git a/activerecord/test/cases/migration/pending_migrations_test.rb b/activerecord/test/cases/migration/pending_migrations_test.rb
index 7afac83bd2..119bfd372a 100644
--- a/activerecord/test/cases/migration/pending_migrations_test.rb
+++ b/activerecord/test/cases/migration/pending_migrations_test.rb
@@ -1,51 +1,40 @@
-require 'cases/helper'
-require "minitest/mock"
+# frozen_string_literal: true
+
+require "cases/helper"
module ActiveRecord
class Migration
- class PendingMigrationsTest < ActiveRecord::TestCase
- def setup
- super
- @connection = Minitest::Mock.new
- @app = Minitest::Mock.new
- conn = @connection
- @pending = Class.new(CheckPending) {
- define_method(:connection) { conn }
- }.new(@app)
- @pending.instance_variable_set :@last_check, -1 # Force checking
- end
-
- def teardown
- assert @connection.verify
- assert @app.verify
- super
- end
-
- def test_errors_if_pending
- @connection.expect :supports_migrations?, true
+ if current_adapter?(:SQLite3Adapter) && !in_memory_db?
+ class PendingMigrationsTest < ActiveRecord::TestCase
+ setup do
+ file = ActiveRecord::Base.connection.raw_connection.filename
+ @conn = ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:", migrations_paths: MIGRATIONS_ROOT + "/valid"
+ source_db = SQLite3::Database.new file
+ dest_db = ActiveRecord::Base.connection.raw_connection
+ backup = SQLite3::Backup.new(dest_db, "main", source_db, "main")
+ backup.step(-1)
+ backup.finish
+ end
- ActiveRecord::Migrator.stub :needs_migration?, true do
- assert_raise ActiveRecord::PendingMigrationError do
- @pending.call(nil)
- end
+ teardown do
+ @conn.release_connection if @conn
+ ActiveRecord::Base.establish_connection :arunit
end
- end
- def test_checks_if_supported
- @connection.expect :supports_migrations?, true
- @app.expect :call, nil, [:foo]
+ def test_errors_if_pending
+ ActiveRecord::Base.connection.drop_table "schema_migrations", if_exists: true
- ActiveRecord::Migrator.stub :needs_migration?, false do
- @pending.call(:foo)
+ assert_raises ActiveRecord::PendingMigrationError do
+ CheckPending.new(Proc.new { }).call({})
+ end
end
- end
- def test_doesnt_check_if_unsupported
- @connection.expect :supports_migrations?, false
- @app.expect :call, nil, [:foo]
+ def test_checks_if_supported
+ ActiveRecord::SchemaMigration.create_table
+ migrator = Base.connection.migration_context
+ capture(:stdout) { migrator.migrate }
- ActiveRecord::Migrator.stub :needs_migration?, true do
- @pending.call(:foo)
+ assert_nil CheckPending.new(Proc.new { }).call({})
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 1594f99852..7a092103c7 100644
--- a/activerecord/test/cases/migration/references_foreign_key_test.rb
+++ b/activerecord/test/cases/migration/references_foreign_key_test.rb
@@ -1,153 +1,257 @@
-require 'cases/helper'
+# frozen_string_literal: true
-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
+require "cases/helper"
- teardown do
- @connection.drop_table "testings", if_exists: true
- @connection.drop_table "testing_parents", if_exists: true
- end
+if ActiveRecord::Base.connection.supports_foreign_keys_in_create?
+ module ActiveRecord
+ class Migration
+ class ReferencesForeignKeyInCreateTest < ActiveRecord::TestCase
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table(:testing_parents, force: true)
+ end
- test "foreign keys can be created with the table" do
- @connection.create_table :testings do |t|
- t.references :testing_parent, foreign_key: true
+ teardown do
+ @connection.drop_table "testings", if_exists: true
+ @connection.drop_table "testing_parents", if_exists: true
end
- fk = @connection.foreign_keys("testings").first
- assert_equal "testings", fk.from_table
- assert_equal "testing_parents", fk.to_table
- end
+ test "foreign keys can be created with the table" do
+ @connection.create_table :testings do |t|
+ t.references :testing_parent, foreign_key: true
+ end
- test "no foreign key is created by default" do
- @connection.create_table :testings do |t|
- t.references :testing_parent
+ fk = @connection.foreign_keys("testings").first
+ assert_equal "testings", fk.from_table
+ assert_equal "testing_parents", fk.to_table
end
- assert_equal [], @connection.foreign_keys("testings")
- end
+ test "no foreign key is created by default" do
+ @connection.create_table :testings do |t|
+ t.references :testing_parent
+ end
- test "options hash can be passed" do
- @connection.change_table :testing_parents do |t|
- t.integer :other_id
- t.index :other_id, unique: true
+ assert_equal [], @connection.foreign_keys("testings")
end
- @connection.create_table :testings do |t|
- t.references :testing_parent, foreign_key: { primary_key: :other_id }
+
+ test "foreign keys can be created in one query when index is not added" do
+ assert_queries(1) do
+ @connection.create_table :testings do |t|
+ t.references :testing_parent, foreign_key: true, index: false
+ end
+ end
end
- fk = @connection.foreign_keys("testings").find { |k| k.to_table == "testing_parents" }
- assert_equal "other_id", fk.primary_key
- end
+ test "options hash can be passed" do
+ @connection.change_table :testing_parents do |t|
+ t.references :other, index: { unique: true }
+ end
+ @connection.create_table :testings do |t|
+ t.references :testing_parent, foreign_key: { primary_key: :other_id }
+ end
- test "foreign keys cannot be added to polymorphic relations when creating the table" do
- @connection.create_table :testings do |t|
- assert_raises(ArgumentError) do
- t.references :testing_parent, polymorphic: true, foreign_key: true
+ fk = @connection.foreign_keys("testings").find { |k| k.to_table == "testing_parents" }
+ assert_equal "other_id", fk.primary_key
+ end
+
+ test "to_table option can be passed" do
+ @connection.create_table :testings do |t|
+ t.references :parent, foreign_key: { to_table: :testing_parents }
end
+ fks = @connection.foreign_keys("testings")
+ assert_equal([["testings", "testing_parents", "parent_id"]],
+ fks.map { |fk| [fk.from_table, fk.to_table, fk.column] })
end
end
+ end
+ end
+end
- test "foreign keys can be created while changing the table" do
- @connection.create_table :testings
- @connection.change_table :testings do |t|
- t.references :testing_parent, foreign_key: true
+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
- fk = @connection.foreign_keys("testings").first
- assert_equal "testings", fk.from_table
- assert_equal "testing_parents", fk.to_table
- end
+ teardown do
+ @connection.drop_table "testings", if_exists: true
+ @connection.drop_table "testing_parents", if_exists: true
+ end
- test "foreign keys are not added by default when changing the table" do
- @connection.create_table :testings
- @connection.change_table :testings do |t|
- t.references :testing_parent
+ test "foreign keys cannot be added to polymorphic relations when creating the table" do
+ @connection.create_table :testings do |t|
+ assert_raises(ArgumentError) do
+ t.references :testing_parent, polymorphic: true, foreign_key: true
+ end
+ end
end
- assert_equal [], @connection.foreign_keys("testings")
- end
+ test "foreign keys can be created while changing the table" do
+ @connection.create_table :testings
+ @connection.change_table :testings do |t|
+ t.references :testing_parent, foreign_key: true
+ end
- 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
+ fk = @connection.foreign_keys("testings").first
+ assert_equal "testings", fk.from_table
+ assert_equal "testing_parents", fk.to_table
end
- @connection.create_table :testings
- @connection.change_table :testings do |t|
- t.references :testing_parent, foreign_key: { primary_key: :other_id }
+
+ test "foreign keys are not added by default when changing the table" do
+ @connection.create_table :testings
+ @connection.change_table :testings do |t|
+ t.references :testing_parent
+ end
+
+ assert_equal [], @connection.foreign_keys("testings")
end
- fk = @connection.foreign_keys("testings").find { |k| k.to_table == "testing_parents" }
- assert_equal "other_id", fk.primary_key
- end
+ test "foreign keys accept options when changing the table" do
+ @connection.change_table :testing_parents do |t|
+ t.references :other, index: { unique: true }
+ end
+ @connection.create_table :testings
+ @connection.change_table :testings do |t|
+ t.references :testing_parent, foreign_key: { primary_key: :other_id }
+ end
- test "foreign keys cannot be added to polymorphic relations when changing the table" do
- @connection.create_table :testings
- @connection.change_table :testings do |t|
- assert_raises(ArgumentError) do
- t.references :testing_parent, polymorphic: true, foreign_key: true
+ fk = @connection.foreign_keys("testings").find { |k| k.to_table == "testing_parents" }
+ assert_equal "other_id", fk.primary_key
+ end
+
+ test "foreign keys cannot be added to polymorphic relations when changing the table" do
+ @connection.create_table :testings
+ @connection.change_table :testings do |t|
+ assert_raises(ArgumentError) do
+ t.references :testing_parent, polymorphic: true, foreign_key: true
+ end
end
end
- end
- test "foreign key column can be removed" do
- @connection.create_table :testings do |t|
- t.references :testing_parent, index: true, foreign_key: true
+ test "foreign key column can be removed" do
+ @connection.create_table :testings do |t|
+ t.references :testing_parent, index: true, foreign_key: true
+ end
+
+ assert_difference "@connection.foreign_keys('testings').size", -1 do
+ @connection.remove_reference :testings, :testing_parent, foreign_key: true
+ end
end
- assert_difference "@connection.foreign_keys('testings').size", -1 do
- @connection.remove_reference :testings, :testing_parent, foreign_key: true
+ test "removing column removes foreign key" do
+ @connection.create_table :testings do |t|
+ t.references :testing_parent, index: true, foreign_key: true
+ end
+
+ assert_difference "@connection.foreign_keys('testings').size", -1 do
+ @connection.remove_column :testings, :testing_parent_id
+ end
end
- end
- test "foreign key methods respect pluralize_table_names" do
- begin
- original_pluralize_table_names = ActiveRecord::Base.pluralize_table_names
- ActiveRecord::Base.pluralize_table_names = false
- @connection.create_table :testing
- @connection.change_table :testing_parents do |t|
- t.references :testing, foreign_key: true
+ test "foreign key methods respect pluralize_table_names" do
+ begin
+ original_pluralize_table_names = ActiveRecord::Base.pluralize_table_names
+ ActiveRecord::Base.pluralize_table_names = false
+ @connection.create_table :testing
+ @connection.change_table :testing_parents do |t|
+ t.references :testing, foreign_key: true
+ end
+
+ fk = @connection.foreign_keys("testing_parents").first
+ assert_equal "testing_parents", fk.from_table
+ assert_equal "testing", fk.to_table
+
+ assert_difference "@connection.foreign_keys('testing_parents').size", -1 do
+ @connection.remove_reference :testing_parents, :testing, foreign_key: true
+ end
+ ensure
+ ActiveRecord::Base.pluralize_table_names = original_pluralize_table_names
+ @connection.drop_table "testing", if_exists: true
end
+ end
- fk = @connection.foreign_keys("testing_parents").first
- assert_equal "testing_parents", fk.from_table
- assert_equal "testing", fk.to_table
+ class CreateDogsMigration < ActiveRecord::Migration::Current
+ def change
+ create_table :dog_owners
- assert_difference "@connection.foreign_keys('testing_parents').size", -1 do
- @connection.remove_reference :testing_parents, :testing, foreign_key: true
+ create_table :dogs do |t|
+ t.references :dog_owner, foreign_key: true
+ end
end
+ end
+
+ def test_references_foreign_key_with_prefix
+ ActiveRecord::Base.table_name_prefix = "p_"
+ migration = CreateDogsMigration.new
+ silence_stream($stdout) { migration.migrate(:up) }
+ assert_equal 1, @connection.foreign_keys("p_dogs").size
ensure
- ActiveRecord::Base.pluralize_table_names = original_pluralize_table_names
- @connection.drop_table "testing", if_exists: true
+ silence_stream($stdout) { migration.migrate(:down) }
+ ActiveRecord::Base.table_name_prefix = nil
+ end
+
+ def test_references_foreign_key_with_suffix
+ ActiveRecord::Base.table_name_suffix = "_s"
+ migration = CreateDogsMigration.new
+ silence_stream($stdout) { migration.migrate(:up) }
+ assert_equal 1, @connection.foreign_keys("dogs_s").size
+ ensure
+ silence_stream($stdout) { migration.migrate(:down) }
+ ActiveRecord::Base.table_name_suffix = nil
+ end
+
+ test "multiple foreign keys can be added to the same table" do
+ @connection.create_table :testings do |t|
+ t.references :parent1, foreign_key: { to_table: :testing_parents }
+ t.references :parent2, 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", "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
+
+ 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", "parent2_id"]], fk_definitions)
end
end
end
end
-end
else
-class ReferencesWithoutForeignKeySupportTest < 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
+ class ReferencesWithoutForeignKeySupportTest < ActiveRecord::TestCase
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table(:testing_parents, force: true)
+ end
- test "ignores foreign keys defined with the table" do
- @connection.create_table :testings do |t|
- t.references :testing_parent, foreign_key: true
+ teardown do
+ @connection.drop_table("testings", if_exists: true)
+ @connection.drop_table("testing_parents", if_exists: true)
end
- assert_includes @connection.tables, "testings"
+ test "ignores foreign keys defined with the table" do
+ @connection.create_table :testings do |t|
+ t.references :testing_parent, foreign_key: true
+ end
+
+ assert_includes @connection.data_sources, "testings"
+ end
end
end
-end
diff --git a/activerecord/test/cases/migration/references_index_test.rb b/activerecord/test/cases/migration/references_index_test.rb
index ad6b828d0b..e41377d817 100644
--- a/activerecord/test/cases/migration/references_index_test.rb
+++ b/activerecord/test/cases/migration/references_index_test.rb
@@ -1,4 +1,6 @@
-require 'cases/helper'
+# frozen_string_literal: true
+
+require "cases/helper"
module ActiveRecord
class Migration
@@ -17,42 +19,42 @@ module ActiveRecord
def test_creates_index
connection.create_table table_name do |t|
- t.references :foo, :index => true
+ t.references :foo, index: true
end
- assert connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id)
+ assert connection.index_exists?(table_name, :foo_id, name: :index_testings_on_foo_id)
end
- def test_does_not_create_index
+ def test_creates_index_by_default_even_if_index_option_is_not_passed
connection.create_table table_name do |t|
t.references :foo
end
- assert_not connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id)
+ assert connection.index_exists?(table_name, :foo_id, name: :index_testings_on_foo_id)
end
def test_does_not_create_index_explicit
connection.create_table table_name do |t|
- t.references :foo, :index => false
+ t.references :foo, index: false
end
- assert_not connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id)
+ assert_not connection.index_exists?(table_name, :foo_id, name: :index_testings_on_foo_id)
end
def test_creates_index_with_options
connection.create_table table_name do |t|
- t.references :foo, :index => {:name => :index_testings_on_yo_momma}
- t.references :bar, :index => {:unique => true}
+ t.references :foo, index: { name: :index_testings_on_yo_momma }
+ t.references :bar, index: { unique: true }
end
- assert connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_yo_momma)
- assert connection.index_exists?(table_name, :bar_id, :name => :index_testings_on_bar_id, :unique => true)
+ assert connection.index_exists?(table_name, :foo_id, name: :index_testings_on_yo_momma)
+ assert connection.index_exists?(table_name, :bar_id, name: :index_testings_on_bar_id, unique: true)
end
unless current_adapter? :OracleAdapter
def test_creates_polymorphic_index
connection.create_table table_name do |t|
- t.references :foo, :polymorphic => true, :index => true
+ t.references :foo, polymorphic: true, index: true
end
assert connection.index_exists?(table_name, [:foo_type, :foo_id], name: :index_testings_on_foo_type_and_foo_id)
@@ -62,35 +64,35 @@ module ActiveRecord
def test_creates_index_for_existing_table
connection.create_table table_name
connection.change_table table_name do |t|
- t.references :foo, :index => true
+ t.references :foo, index: true
end
- assert connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id)
+ assert connection.index_exists?(table_name, :foo_id, name: :index_testings_on_foo_id)
end
- def test_does_not_create_index_for_existing_table
+ def test_creates_index_for_existing_table_even_if_index_option_is_not_passed
connection.create_table table_name
connection.change_table table_name do |t|
t.references :foo
end
- assert_not connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id)
+ assert connection.index_exists?(table_name, :foo_id, name: :index_testings_on_foo_id)
end
def test_does_not_create_index_for_existing_table_explicit
connection.create_table table_name
connection.change_table table_name do |t|
- t.references :foo, :index => false
+ t.references :foo, index: false
end
- assert_not connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id)
+ assert_not connection.index_exists?(table_name, :foo_id, name: :index_testings_on_foo_id)
end
unless current_adapter? :OracleAdapter
def test_creates_polymorphic_index_for_existing_table
connection.create_table table_name
connection.change_table table_name do |t|
- t.references :foo, :polymorphic => true, :index => true
+ t.references :foo, polymorphic: true, index: true
end
assert connection.index_exists?(table_name, [:foo_type, :foo_id], name: :index_testings_on_foo_type_and_foo_id)
diff --git a/activerecord/test/cases/migration/references_statements_test.rb b/activerecord/test/cases/migration/references_statements_test.rb
index f613fd66c3..769241ba12 100644
--- a/activerecord/test/cases/migration/references_statements_test.rb
+++ b/activerecord/test/cases/migration/references_statements_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/migration/helper"
module ActiveRecord
@@ -30,14 +32,14 @@ module ActiveRecord
assert column_exists?(table_name, :taggable_type, :string)
end
- def test_creates_reference_id_index
- add_reference table_name, :user, index: true
- assert index_exists?(table_name, :user_id)
+ def test_does_not_create_reference_id_index_if_index_is_false
+ add_reference table_name, :user, index: false
+ assert_not index_exists?(table_name, :user_id)
end
- def test_does_not_create_reference_id_index
+ def test_create_reference_id_index_even_if_index_option_is_not_passed
add_reference table_name, :user
- assert_not index_exists?(table_name, :user_id)
+ assert index_exists?(table_name, :user_id)
end
def test_creates_polymorphic_index
@@ -46,13 +48,33 @@ module ActiveRecord
end
def test_creates_reference_type_column_with_default
- add_reference table_name, :taggable, polymorphic: { default: 'Photo' }, index: true
- assert column_exists?(table_name, :taggable_type, :string, default: 'Photo')
+ add_reference table_name, :taggable, polymorphic: { default: "Photo" }, index: true
+ assert column_exists?(table_name, :taggable_type, :string, default: "Photo")
+ end
+
+ def test_creates_reference_type_column_with_not_null
+ connection.create_table table_name, force: true do |t|
+ t.references :taggable, null: false, polymorphic: true
+ end
+ assert column_exists?(table_name, :taggable_id, :integer, null: false)
+ assert column_exists?(table_name, :taggable_type, :string, null: false)
+ 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')
+ 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")
+ end
+
+ def test_creates_named_unique_index
+ add_reference table_name, :tag, index: { name: "index_taggings_on_tag_id", unique: true }
+ assert index_exists?(table_name, :tag_id, name: "index_taggings_on_tag_id", unique: true)
end
def test_creates_reference_id_with_specified_type
@@ -105,12 +127,12 @@ module ActiveRecord
private
- def with_polymorphic_column
- add_column table_name, :supplier_type, :string
- add_index table_name, [:supplier_id, :supplier_type]
+ def with_polymorphic_column
+ add_column table_name, :supplier_type, :string
+ add_index table_name, [:supplier_id, :supplier_type]
- yield
- end
+ yield
+ end
end
end
end
diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb
index 6d742d3f2f..a9deb92585 100644
--- a/activerecord/test/cases/migration/rename_table_test.rb
+++ b/activerecord/test/cases/migration/rename_table_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/migration/helper"
module ActiveRecord
@@ -9,9 +11,9 @@ module ActiveRecord
def setup
super
- add_column 'test_models', :url, :string
- remove_column 'test_models', :created_at
- remove_column 'test_models', :updated_at
+ add_column "test_models", :url, :string
+ remove_column "test_models", :created_at
+ remove_column "test_models", :updated_at
end
def teardown
@@ -31,7 +33,7 @@ module ActiveRecord
# Using explicit id in insert for compatibility across all databases
connection.execute "INSERT INTO 'references' (url, created_at, updated_at) VALUES ('http://rubyonrails.com', 0, 0)"
- assert_equal 'http://rubyonrails.com', connection.select_value("SELECT url FROM 'references' WHERE id=1")
+ assert_equal "http://rubyonrails.com", connection.select_value("SELECT url FROM 'references' WHERE id=1")
ensure
return unless renamed
connection.rename_table :references, :test_models
@@ -45,7 +47,7 @@ module ActiveRecord
connection.execute "INSERT INTO octopi (#{connection.quote_column_name('id')}, #{connection.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')"
- assert_equal 'http://www.foreverflying.com/octopus-black7.jpg', connection.select_value("SELECT url FROM octopi WHERE id=1")
+ assert_equal "http://www.foreverflying.com/octopus-black7.jpg", connection.select_value("SELECT url FROM octopi WHERE id=1")
end
def test_rename_table_with_an_index
@@ -55,18 +57,18 @@ module ActiveRecord
connection.execute "INSERT INTO octopi (#{connection.quote_column_name('id')}, #{connection.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')"
- assert_equal 'http://www.foreverflying.com/octopus-black7.jpg', connection.select_value("SELECT url FROM octopi WHERE id=1")
+ assert_equal "http://www.foreverflying.com/octopus-black7.jpg", connection.select_value("SELECT url FROM octopi WHERE id=1")
index = connection.indexes(:octopi).first
- assert index.columns.include?("url")
- assert_equal 'index_octopi_on_url', index.name
+ assert_includes index.columns, "url"
+ assert_equal "index_octopi_on_url", index.name
end
def test_rename_table_does_not_rename_custom_named_index
- add_index :test_models, :url, name: 'special_url_idx'
+ add_index :test_models, :url, name: "special_url_idx"
rename_table :test_models, :octopi
- assert_equal ['special_url_idx'], connection.indexes(:octopi).map(&:name)
+ assert_equal ["special_url_idx"], connection.indexes(:octopi).map(&:name)
end
end
@@ -74,18 +76,39 @@ module ActiveRecord
def test_rename_table_for_postgresql_should_also_rename_default_sequence
rename_table :test_models, :octopi
- pk, seq = connection.pk_and_sequence_for('octopi')
+ pk, seq = connection.pk_and_sequence_for("octopi")
assert_equal ConnectionAdapters::PostgreSQL::Name.new("public", "octopi_#{pk}_seq"), seq
end
+ def test_renaming_table_renames_primary_key
+ connection.create_table :cats, id: :uuid, default: "uuid_generate_v4()"
+ rename_table :cats, :felines
+
+ assert connection.table_exists? :felines
+ assert_not connection.table_exists? :cats
+
+ primary_key_name = connection.select_values(<<~SQL, "SCHEMA")[0]
+ SELECT c.relname
+ FROM pg_class c
+ JOIN pg_index i
+ ON c.oid = i.indexrelid
+ WHERE i.indisprimary
+ AND i.indrelid = 'felines'::regclass
+ SQL
+
+ assert_equal "felines_pkey", primary_key_name
+ ensure
+ connection.drop_table :cats, if_exists: true
+ connection.drop_table :felines, if_exists: true
+ end
+
def test_renaming_table_doesnt_attempt_to_rename_non_existent_sequences
- enable_extension!('uuid-ossp', connection)
- connection.create_table :cats, id: :uuid
+ connection.create_table :cats, id: :uuid, default: "uuid_generate_v4()"
assert_nothing_raised { rename_table :cats, :felines }
assert connection.table_exists? :felines
+ assert_not connection.table_exists? :cats
ensure
- disable_extension!('uuid-ossp', connection)
connection.drop_table :cats, if_exists: true
connection.drop_table :felines, if_exists: true
end
diff --git a/activerecord/test/cases/migration/table_and_index_test.rb b/activerecord/test/cases/migration/table_and_index_test.rb
deleted file mode 100644
index 24cba84a09..0000000000
--- a/activerecord/test/cases/migration/table_and_index_test.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-require "cases/helper"
-
-module ActiveRecord
- class Migration
- class TableAndIndexTest < ActiveRecord::TestCase
- def test_add_schema_info_respects_prefix_and_suffix
- conn = ActiveRecord::Base.connection
-
- conn.drop_table(ActiveRecord::Migrator.schema_migrations_table_name, if_exists: true)
- # Use shorter prefix and suffix as in Oracle database identifier cannot be larger than 30 characters
- ActiveRecord::Base.table_name_prefix = 'p_'
- ActiveRecord::Base.table_name_suffix = '_s'
- conn.drop_table(ActiveRecord::Migrator.schema_migrations_table_name, if_exists: true)
-
- conn.initialize_schema_migrations_table
-
- assert_equal "p_unique_schema_migrations_s", conn.indexes(ActiveRecord::Migrator.schema_migrations_table_name)[0][:name]
- ensure
- ActiveRecord::Base.table_name_prefix = ""
- ActiveRecord::Base.table_name_suffix = ""
- end
- end
- end
-end
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index b2f209fe97..661163b4a1 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -1,11 +1,14 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "cases/migration/helper"
-require 'bigdecimal/util'
+require "bigdecimal/util"
+require "concurrent/atomic/count_down_latch"
-require 'models/person'
-require 'models/topic'
-require 'models/developer'
-require 'models/computer'
+require "models/person"
+require "models/topic"
+require "models/developer"
+require "models/computer"
require MIGRATIONS_ROOT + "/valid/2_we_need_reminders"
require MIGRATIONS_ROOT + "/rename/1_we_need_things"
@@ -42,10 +45,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
@@ -58,7 +61,7 @@ class MigrationTest < ActiveRecord::TestCase
%w(last_name key bio age height wealth birthday favorite_day
moment_of_truth male administrator funny).each do |column|
- Person.connection.remove_column('people', column) rescue nil
+ Person.connection.remove_column("people", column) rescue nil
end
Person.connection.remove_column("people", "first_name") rescue nil
Person.connection.remove_column("people", "middle_name") rescue nil
@@ -68,54 +71,90 @@ class MigrationTest < ActiveRecord::TestCase
ActiveRecord::Migration.verbose = @verbose_was
end
+ def test_migrator_migrations_path_is_deprecated
+ assert_deprecated do
+ ActiveRecord::Migrator.migrations_path = "/whatever"
+ end
+ ensure
+ assert_deprecated do
+ ActiveRecord::Migrator.migrations_path = "db/migrate"
+ end
+ end
+
+ def test_migration_version_matches_component_version
+ assert_equal ActiveRecord::VERSION::STRING.to_f, ActiveRecord::Migration.current_version
+ end
+
def test_migrator_versions
migrations_path = MIGRATIONS_ROOT + "/valid"
- old_path = ActiveRecord::Migrator.migrations_paths
- ActiveRecord::Migrator.migrations_paths = migrations_path
+ migrator = ActiveRecord::MigrationContext.new(migrations_path)
- ActiveRecord::Migrator.up(migrations_path)
- assert_equal 3, ActiveRecord::Migrator.current_version
- assert_equal 3, ActiveRecord::Migrator.last_version
- assert_equal false, ActiveRecord::Migrator.needs_migration?
+ migrator.up
+ assert_equal 3, migrator.current_version
+ assert_equal false, migrator.needs_migration?
- ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid")
- assert_equal 0, ActiveRecord::Migrator.current_version
- assert_equal 3, ActiveRecord::Migrator.last_version
- assert_equal true, ActiveRecord::Migrator.needs_migration?
+ migrator.down
+ assert_equal 0, migrator.current_version
+ assert_equal true, migrator.needs_migration?
- ActiveRecord::SchemaMigration.create!(:version => ActiveRecord::Migrator.last_version)
- assert_equal true, ActiveRecord::Migrator.needs_migration?
- ensure
- ActiveRecord::Migrator.migrations_paths = old_path
+ ActiveRecord::SchemaMigration.create!(version: 3)
+ assert_equal true, migrator.needs_migration?
end
def test_migration_detection_without_schema_migration_table
- ActiveRecord::Base.connection.drop_table 'schema_migrations', if_exists: true
+ ActiveRecord::Base.connection.drop_table "schema_migrations", if_exists: true
migrations_path = MIGRATIONS_ROOT + "/valid"
- old_path = ActiveRecord::Migrator.migrations_paths
- ActiveRecord::Migrator.migrations_paths = migrations_path
+ migrator = ActiveRecord::MigrationContext.new(migrations_path)
- assert_equal true, ActiveRecord::Migrator.needs_migration?
- ensure
- ActiveRecord::Migrator.migrations_paths = old_path
+ assert_equal true, migrator.needs_migration?
end
def test_any_migrations
- old_path = ActiveRecord::Migrator.migrations_paths
- ActiveRecord::Migrator.migrations_paths = MIGRATIONS_ROOT + "/valid"
+ migrator = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid")
- assert ActiveRecord::Migrator.any_migrations?
+ assert_predicate migrator, :any_migrations?
- ActiveRecord::Migrator.migrations_paths = MIGRATIONS_ROOT + "/empty"
+ migrator_empty = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/empty")
- assert_not ActiveRecord::Migrator.any_migrations?
- ensure
- ActiveRecord::Migrator.migrations_paths = old_path
+ assert_not_predicate migrator_empty, :any_migrations?
end
def test_migration_version
- ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT + "/version_check", 20131219224947)
+ migrator = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/version_check")
+ assert_equal 0, migrator.current_version
+ migrator.up(20131219224947)
+ assert_equal 20131219224947, migrator.current_version
+ end
+
+ def test_create_table_raises_if_already_exists
+ connection = Person.connection
+ connection.create_table :testings, force: true do |t|
+ t.string :foo
+ end
+
+ assert_raise(ActiveRecord::StatementInvalid) do
+ connection.create_table :testings do |t|
+ t.string :foo
+ end
+ end
+ ensure
+ connection.drop_table :testings, if_exists: true
+ end
+
+ def test_create_table_with_if_not_exists_true
+ connection = Person.connection
+ connection.create_table :testings, force: true do |t|
+ t.string :foo
+ end
+
+ assert_nothing_raised do
+ connection.create_table :testings, if_not_exists: true do |t|
+ t.string :foo
+ end
+ end
+ ensure
+ connection.drop_table :testings, if_exists: true
end
def test_create_table_with_force_true_does_not_drop_nonexisting_table
@@ -125,24 +164,20 @@ class MigrationTest < ActiveRecord::TestCase
assert_not_equal temp_conn, Person.connection
- temp_conn.create_table :testings2, :force => true do |t|
+ temp_conn.create_table :testings2, force: true do |t|
t.column :foo, :string
end
ensure
Person.connection.drop_table :testings2, if_exists: true
end
- def connection
- ActiveRecord::Base.connection
- end
-
def test_migration_instance_has_connection
- migration = Class.new(ActiveRecord::Migration).new
- assert_equal connection, migration.connection
+ migration = Class.new(ActiveRecord::Migration::Current).new
+ assert_equal ActiveRecord::Base.connection, migration.connection
end
def test_method_missing_delegates_to_connection
- migration = Class.new(ActiveRecord::Migration) {
+ migration = Class.new(ActiveRecord::Migration::Current) {
def connection
Class.new {
def create_table; "hi mom!"; end
@@ -156,16 +191,16 @@ class MigrationTest < ActiveRecord::TestCase
def test_add_table_with_decimals
Person.connection.drop_table :big_numbers rescue nil
- assert !BigNumber.table_exists?
+ assert_not_predicate BigNumber, :table_exists?
GiveMeBigNumbers.up
BigNumber.reset_column_information
assert BigNumber.create(
- :bank_balance => 1586.43,
- :big_bank_balance => BigDecimal("1000234000567.95"),
- :world_population => 6000000000,
- :my_house_population => 3,
- :value_of_e => BigDecimal("2.7182818284590452353602875")
+ bank_balance: 1586.43,
+ big_bank_balance: BigDecimal("1000234000567.95"),
+ world_population: 2**62,
+ my_house_population: 3,
+ value_of_e: BigDecimal("2.7182818284590452353602875")
)
b = BigNumber.first
@@ -177,11 +212,9 @@ class MigrationTest < ActiveRecord::TestCase
assert_not_nil b.my_house_population
assert_not_nil b.value_of_e
- # TODO: set world_population >= 2**62 to cover 64-bit platforms and test
- # is_a?(Bignum)
assert_kind_of Integer, b.world_population
- assert_equal 6000000000, b.world_population
- assert_kind_of Fixnum, b.my_house_population
+ assert_equal 2**62, b.world_population
+ assert_kind_of Integer, b.my_house_population
assert_equal 3, b.my_house_population
assert_kind_of BigDecimal, b.bank_balance
assert_equal BigDecimal("1586.43"), b.bank_balance
@@ -197,8 +230,6 @@ class MigrationTest < ActiveRecord::TestCase
# of 0, they take on the compile-time limit for precision and scale,
# so the following should succeed unless you have used really wacky
# compilation options
- # - SQLite2 has the default behavior of preserving all data sent in,
- # so this happens there too
assert_kind_of BigDecimal, b.value_of_e
assert_equal BigDecimal("2.7182818284590452353602875"), b.value_of_e
elsif current_adapter?(:SQLite3Adapter)
@@ -207,7 +238,7 @@ class MigrationTest < ActiveRecord::TestCase
assert_in_delta BigDecimal("2.71828182845905"), b.value_of_e, 0.00000000000001
else
# - SQL standard is an integer
- assert_kind_of Fixnum, b.value_of_e
+ assert_kind_of Integer, b.value_of_e
assert_equal 2, b.value_of_e
end
@@ -217,21 +248,22 @@ class MigrationTest < ActiveRecord::TestCase
def test_filtering_migrations
assert_no_column Person, :last_name
- assert !Reminder.table_exists?
+ assert_not_predicate Reminder, :table_exists?
name_filter = lambda { |migration| migration.name == "ValidPeopleHaveLastNames" }
- ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", &name_filter)
+ migrator = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid")
+ migrator.up(&name_filter)
assert_column Person, :last_name
assert_raise(ActiveRecord::StatementInvalid) { Reminder.first }
- ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid", &name_filter)
+ migrator.down(&name_filter)
assert_no_column Person, :last_name
assert_raise(ActiveRecord::StatementInvalid) { Reminder.first }
end
- class MockMigration < ActiveRecord::Migration
+ class MockMigration < ActiveRecord::Migration::Current
attr_reader :went_up, :went_down
def initialize
@went_up = false
@@ -251,33 +283,33 @@ class MigrationTest < ActiveRecord::TestCase
def test_instance_based_migration_up
migration = MockMigration.new
- assert !migration.went_up, 'have not gone up'
- assert !migration.went_down, 'have not gone down'
+ assert_not migration.went_up, "have not gone up"
+ assert_not migration.went_down, "have not gone down"
migration.migrate :up
- assert migration.went_up, 'have gone up'
- assert !migration.went_down, 'have not gone down'
+ assert migration.went_up, "have gone up"
+ assert_not migration.went_down, "have not gone down"
end
def test_instance_based_migration_down
migration = MockMigration.new
- assert !migration.went_up, 'have not gone up'
- assert !migration.went_down, 'have not gone down'
+ assert_not migration.went_up, "have not gone up"
+ assert_not migration.went_down, "have not gone down"
migration.migrate :down
- assert !migration.went_up, 'have gone up'
- assert migration.went_down, 'have not gone down'
+ assert_not migration.went_up, "have gone up"
+ assert migration.went_down, "have not gone down"
end
if ActiveRecord::Base.connection.supports_ddl_transactions?
def test_migrator_one_up_with_exception_and_rollback
assert_no_column Person, :last_name
- migration = Class.new(ActiveRecord::Migration) {
+ migration = Class.new(ActiveRecord::Migration::Current) {
def version; 100 end
def migrate(x)
add_column "people", "last_name", :string
- raise 'Something broke'
+ raise "Something broke"
end
}.new
@@ -294,11 +326,11 @@ class MigrationTest < ActiveRecord::TestCase
def test_migrator_one_up_with_exception_and_rollback_using_run
assert_no_column Person, :last_name
- migration = Class.new(ActiveRecord::Migration) {
+ migration = Class.new(ActiveRecord::Migration::Current) {
def version; 100 end
def migrate(x)
add_column "people", "last_name", :string
- raise 'Something broke'
+ raise "Something broke"
end
}.new
@@ -306,7 +338,7 @@ class MigrationTest < ActiveRecord::TestCase
e = assert_raise(StandardError) { migrator.run }
- assert_equal "An error has occurred, this migration was canceled:\n\nSomething broke", e.message
+ assert_equal "An error has occurred, this and all later migrations canceled:\n\nSomething broke", e.message
assert_no_column Person, :last_name,
"On error, the Migrator should revert schema changes but it did not."
@@ -315,13 +347,13 @@ class MigrationTest < ActiveRecord::TestCase
def test_migration_without_transaction
assert_no_column Person, :last_name
- migration = Class.new(ActiveRecord::Migration) {
- self.disable_ddl_transaction!
+ migration = Class.new(ActiveRecord::Migration::Current) {
+ disable_ddl_transaction!
def version; 101 end
def migrate(x)
add_column "people", "last_name", :string
- raise 'Something broke'
+ raise "Something broke"
end
}.new
@@ -333,36 +365,94 @@ class MigrationTest < ActiveRecord::TestCase
"without ddl transactions, the Migrator should not rollback on error but it did."
ensure
Person.reset_column_information
- if Person.column_names.include?('last_name')
- Person.connection.remove_column('people', 'last_name')
+ if Person.column_names.include?("last_name")
+ Person.connection.remove_column("people", "last_name")
end
end
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
end
+ def test_internal_metadata_table_name
+ original_internal_metadata_table_name = ActiveRecord::Base.internal_metadata_table_name
+
+ assert_equal "ar_internal_metadata", ActiveRecord::InternalMetadata.table_name
+ ActiveRecord::Base.table_name_prefix = "p_"
+ ActiveRecord::Base.table_name_suffix = "_s"
+ Reminder.reset_table_name
+ assert_equal "p_ar_internal_metadata_s", ActiveRecord::InternalMetadata.table_name
+ ActiveRecord::Base.internal_metadata_table_name = "changed"
+ Reminder.reset_table_name
+ assert_equal "p_changed_s", ActiveRecord::InternalMetadata.table_name
+ ActiveRecord::Base.table_name_prefix = ""
+ ActiveRecord::Base.table_name_suffix = ""
+ Reminder.reset_table_name
+ assert_equal "changed", ActiveRecord::InternalMetadata.table_name
+ ensure
+ ActiveRecord::Base.internal_metadata_table_name = original_internal_metadata_table_name
+ Reminder.reset_table_name
+ end
+
+ def test_internal_metadata_stores_environment
+ current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
+ migrations_path = MIGRATIONS_ROOT + "/valid"
+ migrator = ActiveRecord::MigrationContext.new(migrations_path)
+
+ migrator.up
+ assert_equal current_env, ActiveRecord::InternalMetadata[:environment]
+
+ original_rails_env = ENV["RAILS_ENV"]
+ original_rack_env = ENV["RACK_ENV"]
+ ENV["RAILS_ENV"] = ENV["RACK_ENV"] = "foofoo"
+ new_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
+
+ assert_not_equal current_env, new_env
+
+ sleep 1 # mysql by default does not store fractional seconds in the database
+ migrator.up
+ assert_equal new_env, ActiveRecord::InternalMetadata[:environment]
+ ensure
+ ENV["RAILS_ENV"] = original_rails_env
+ ENV["RACK_ENV"] = original_rack_env
+ migrator.up
+ end
+
+ def test_internal_metadata_stores_environment_when_other_data_exists
+ ActiveRecord::InternalMetadata.delete_all
+ ActiveRecord::InternalMetadata[:foo] = "bar"
+
+ current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
+ migrations_path = MIGRATIONS_ROOT + "/valid"
+
+ current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
+ migrator = ActiveRecord::MigrationContext.new(migrations_path)
+ migrator.up
+ assert_equal current_env, ActiveRecord::InternalMetadata[:environment]
+ assert_equal "bar", ActiveRecord::InternalMetadata[:foo]
+ end
+
def test_proper_table_name_on_migration
reminder_class = new_isolated_reminder_class
migration = ActiveRecord::Migration.new
- assert_equal "table", migration.proper_table_name('table')
+ assert_equal "table", migration.proper_table_name("table")
assert_equal "table", migration.proper_table_name(:table)
assert_equal "reminders", migration.proper_table_name(reminder_class)
reminder_class.reset_table_name
@@ -371,26 +461,26 @@ class MigrationTest < ActiveRecord::TestCase
# Use the model's own prefix/suffix if a model is given
ActiveRecord::Base.table_name_prefix = "ARprefix_"
ActiveRecord::Base.table_name_suffix = "_ARsuffix"
- reminder_class.table_name_prefix = 'prefix_'
- reminder_class.table_name_suffix = '_suffix'
+ reminder_class.table_name_prefix = "prefix_"
+ reminder_class.table_name_suffix = "_suffix"
reminder_class.reset_table_name
assert_equal "prefix_reminders_suffix", migration.proper_table_name(reminder_class)
- reminder_class.table_name_prefix = ''
- reminder_class.table_name_suffix = ''
+ reminder_class.table_name_prefix = ""
+ reminder_class.table_name_suffix = ""
reminder_class.reset_table_name
# Use AR::Base's prefix/suffix if string or symbol is given
ActiveRecord::Base.table_name_prefix = "prefix_"
ActiveRecord::Base.table_name_suffix = "_suffix"
reminder_class.reset_table_name
- assert_equal "prefix_table_suffix", migration.proper_table_name('table', migration.table_name_options)
+ assert_equal "prefix_table_suffix", migration.proper_table_name("table", migration.table_name_options)
assert_equal "prefix_table_suffix", migration.proper_table_name(:table, migration.table_name_options)
end
def test_rename_table_with_prefix_and_suffix
- assert !Thing.table_exists?
- ActiveRecord::Base.table_name_prefix = 'p_'
- ActiveRecord::Base.table_name_suffix = '_s'
+ assert_not_predicate Thing, :table_exists?
+ ActiveRecord::Base.table_name_prefix = "p_"
+ ActiveRecord::Base.table_name_suffix = "_s"
Thing.reset_table_name
Thing.reset_sequence_name
WeNeedThings.up
@@ -409,9 +499,9 @@ class MigrationTest < ActiveRecord::TestCase
end
def test_add_drop_table_with_prefix_and_suffix
- assert !Reminder.table_exists?
- ActiveRecord::Base.table_name_prefix = 'prefix_'
- ActiveRecord::Base.table_name_suffix = '_suffix'
+ assert_not_predicate Reminder, :table_exists?
+ ActiveRecord::Base.table_name_prefix = "prefix_"
+ ActiveRecord::Base.table_name_suffix = "_suffix"
Reminder.reset_table_name
Reminder.reset_sequence_name
Reminder.reset_column_information
@@ -428,7 +518,7 @@ class MigrationTest < ActiveRecord::TestCase
def test_create_table_with_binary_column
assert_nothing_raised {
Person.connection.create_table :binary_testings do |t|
- t.column "data", :binary, :null => false
+ t.column "data", :binary, null: false
end
}
@@ -436,38 +526,51 @@ class MigrationTest < ActiveRecord::TestCase
data_column = columns.detect { |c| c.name == "data" }
assert_nil data_column.default
-
+ ensure
Person.connection.drop_table :binary_testings, if_exists: true
end
unless mysql_enforcing_gtid_consistency?
def test_create_table_with_query
- Person.connection.drop_table :table_from_query_testings rescue nil
- Person.connection.create_table(:person, force: true)
-
- Person.connection.create_table :table_from_query_testings, as: "SELECT id FROM person"
+ Person.connection.create_table :table_from_query_testings, as: "SELECT id FROM people WHERE id = 1"
columns = Person.connection.columns(:table_from_query_testings)
+ assert_equal [1], Person.connection.select_values("SELECT * FROM table_from_query_testings")
assert_equal 1, columns.length
assert_equal "id", columns.first.name
-
+ ensure
Person.connection.drop_table :table_from_query_testings rescue nil
end
def test_create_table_with_query_from_relation
- Person.connection.drop_table :table_from_query_testings rescue nil
- Person.connection.create_table(:person, force: true)
-
- Person.connection.create_table :table_from_query_testings, as: Person.select(:id)
+ Person.connection.create_table :table_from_query_testings, as: Person.select(:id).where(id: 1)
columns = Person.connection.columns(:table_from_query_testings)
+ assert_equal [1], Person.connection.select_values("SELECT * FROM table_from_query_testings")
assert_equal 1, columns.length
assert_equal "id", columns.first.name
-
+ ensure
Person.connection.drop_table :table_from_query_testings rescue nil
end
end
+ if current_adapter?(:SQLite3Adapter)
+ def test_allows_sqlite3_rollback_on_invalid_column_type
+ Person.connection.create_table :something, force: true do |t|
+ t.column :number, :integer
+ t.column :name, :string
+ t.column :foo, :bar
+ end
+ assert Person.connection.column_exists?(:something, :foo)
+ assert_nothing_raised { Person.connection.remove_column :something, :foo, :bar }
+ assert_not Person.connection.column_exists?(:something, :foo)
+ assert Person.connection.column_exists?(:something, :name)
+ assert Person.connection.column_exists?(:something, :number)
+ ensure
+ Person.connection.drop_table :something, if_exists: true
+ end
+ end
+
if current_adapter? :OracleAdapter
def test_create_table_with_custom_sequence_name
# table name is 29 chars, the standard sequence name will
@@ -475,7 +578,7 @@ class MigrationTest < ActiveRecord::TestCase
assert_nothing_raised do
begin
Person.connection.create_table :table_with_name_thats_just_ok do |t|
- t.column :foo, :string, :null => false
+ t.column :foo, :string, null: false
end
ensure
Person.connection.drop_table :table_with_name_thats_just_ok rescue nil
@@ -486,15 +589,15 @@ class MigrationTest < ActiveRecord::TestCase
assert_nothing_raised do
begin
Person.connection.create_table :table_with_name_thats_just_ok,
- :sequence_name => 'suitably_short_seq' do |t|
- t.column :foo, :string, :null => false
+ sequence_name: "suitably_short_seq" do |t|
+ t.column :foo, :string, null: false
end
Person.connection.execute("select suitably_short_seq.nextval from dual")
ensure
Person.connection.drop_table :table_with_name_thats_just_ok,
- :sequence_name => 'suitably_short_seq' rescue nil
+ sequence_name: "suitably_short_seq" rescue nil
end
end
@@ -505,30 +608,126 @@ class MigrationTest < ActiveRecord::TestCase
end
end
- if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter)
- def test_out_of_range_limit_should_raise
- Person.connection.drop_table :test_limits rescue nil
+ if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)
+ def test_out_of_range_integer_limit_should_raise
e = assert_raise(ActiveRecord::ActiveRecordError, "integer limit didn't raise") do
- Person.connection.create_table :test_integer_limits, :force => true do |t|
- t.column :bigone, :integer, :limit => 10
+ Person.connection.create_table :test_integer_limits, force: true do |t|
+ t.column :bigone, :integer, limit: 10
end
end
assert_match(/No integer type has byte size 10/, e.message)
+ ensure
+ Person.connection.drop_table :test_integer_limits, if_exists: true
+ end
+ end
+
+ if current_adapter?(:Mysql2Adapter)
+ def test_out_of_range_text_limit_should_raise
+ e = assert_raise(ActiveRecord::ActiveRecordError, "text limit didn't raise") do
+ Person.connection.create_table :test_text_limits, force: true do |t|
+ t.text :bigtext, limit: 0xfffffffff
+ end
+ end
+
+ assert_match(/No text type has byte length #{0xfffffffff}/, e.message)
+ ensure
+ Person.connection.drop_table :test_text_limits, if_exists: true
+ end
+ end
- unless current_adapter?(:PostgreSQLAdapter)
- assert_raise(ActiveRecord::ActiveRecordError, "text limit didn't raise") do
- Person.connection.create_table :test_text_limits, :force => true do |t|
- t.column :bigtext, :text, :limit => 0xfffffffff
+ if ActiveRecord::Base.connection.supports_advisory_locks?
+ def test_migrator_generates_valid_lock_id
+ migration = Class.new(ActiveRecord::Migration::Current).new
+ migrator = ActiveRecord::Migrator.new(:up, [migration], 100)
+
+ lock_id = migrator.send(:generate_migrator_advisory_lock_id)
+
+ assert ActiveRecord::Base.connection.get_advisory_lock(lock_id),
+ "the Migrator should have generated a valid lock id, but it didn't"
+ assert ActiveRecord::Base.connection.release_advisory_lock(lock_id),
+ "the Migrator should have generated a valid lock id, but it didn't"
+ end
+
+ def test_generate_migrator_advisory_lock_id
+ # It is important we are consistent with how we generate this so that
+ # exclusive locking works across migrator versions
+ migration = Class.new(ActiveRecord::Migration::Current).new
+ migrator = ActiveRecord::Migrator.new(:up, [migration], 100)
+
+ lock_id = migrator.send(:generate_migrator_advisory_lock_id)
+
+ current_database = ActiveRecord::Base.connection.current_database
+ salt = ActiveRecord::Migrator::MIGRATOR_SALT
+ expected_id = Zlib.crc32(current_database) * salt
+
+ assert lock_id == expected_id, "expected lock id generated by the migrator to be #{expected_id}, but it was #{lock_id} instead"
+ assert lock_id.bit_length <= 63, "lock id must be a signed integer of max 63 bits magnitude"
+ end
+
+ def test_migrator_one_up_with_unavailable_lock
+ assert_no_column Person, :last_name
+
+ migration = Class.new(ActiveRecord::Migration::Current) {
+ def version; 100 end
+ def migrate(x)
+ add_column "people", "last_name", :string
+ end
+ }.new
+
+ migrator = ActiveRecord::Migrator.new(:up, [migration], 100)
+ lock_id = migrator.send(:generate_migrator_advisory_lock_id)
+
+ with_another_process_holding_lock(lock_id) do
+ assert_raise(ActiveRecord::ConcurrentMigrationError) { migrator.migrate }
+ end
+
+ assert_no_column Person, :last_name,
+ "without an advisory lock, the Migrator should not make any changes, but it did."
+ end
+
+ def test_migrator_one_up_with_unavailable_lock_using_run
+ assert_no_column Person, :last_name
+
+ migration = Class.new(ActiveRecord::Migration::Current) {
+ def version; 100 end
+ def migrate(x)
+ add_column "people", "last_name", :string
+ end
+ }.new
+
+ migrator = ActiveRecord::Migrator.new(:up, [migration], 100)
+ lock_id = migrator.send(:generate_migrator_advisory_lock_id)
+
+ with_another_process_holding_lock(lock_id) do
+ assert_raise(ActiveRecord::ConcurrentMigrationError) { migrator.run }
+ end
+
+ assert_no_column Person, :last_name,
+ "without an advisory lock, the Migrator should not make any changes, but it did."
+ end
+
+ def test_with_advisory_lock_raises_the_right_error_when_it_fails_to_release_lock
+ migration = Class.new(ActiveRecord::Migration::Current).new
+ migrator = ActiveRecord::Migrator.new(:up, [migration], 100)
+ lock_id = migrator.send(:generate_migrator_advisory_lock_id)
+
+ e = assert_raises(ActiveRecord::ConcurrentMigrationError) do
+ silence_stream($stderr) do
+ migrator.send(:with_advisory_lock) do
+ ActiveRecord::Base.connection.release_advisory_lock(lock_id)
end
end
end
- Person.connection.drop_table :test_limits rescue nil
+ assert_match(
+ /#{ActiveRecord::ConcurrentMigrationError::RELEASE_LOCK_FAILED_MESSAGE}/,
+ e.message
+ )
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
@@ -537,20 +736,44 @@ class MigrationTest < ActiveRecord::TestCase
def self.base_class; self; end
}
end
+
+ def with_another_process_holding_lock(lock_id)
+ thread_lock = Concurrent::CountDownLatch.new
+ test_terminated = Concurrent::CountDownLatch.new
+
+ other_process = Thread.new do
+ begin
+ conn = ActiveRecord::Base.connection_pool.checkout
+ conn.get_advisory_lock(lock_id)
+ thread_lock.count_down
+ test_terminated.wait # hold the lock open until we tested everything
+ ensure
+ conn.release_advisory_lock(lock_id)
+ ActiveRecord::Base.connection_pool.checkin(conn)
+ end
+ end
+
+ thread_lock.wait # wait until the 'other process' has the lock
+
+ yield
+
+ test_terminated.count_down
+ other_process.join
+ end
end
class ReservedWordsMigrationTest < ActiveRecord::TestCase
def test_drop_index_from_table_named_values
connection = Person.connection
- connection.create_table :values, :force => true do |t|
+ connection.create_table :values, force: true do |t|
t.integer :value
end
assert_nothing_raised do
connection.add_index :values, :value
- connection.remove_index :values, :column => :value
+ connection.remove_index :values, column: :value
end
-
+ ensure
connection.drop_table :values rescue nil
end
end
@@ -562,11 +785,11 @@ class ExplicitlyNamedIndexMigrationTest < ActiveRecord::TestCase
t.integer :value
end
- assert_nothing_raised ArgumentError do
- connection.add_index :values, :value, name: 'a_different_name'
- connection.remove_index :values, column: :value, name: 'a_different_name'
+ assert_nothing_raised do
+ connection.add_index :values, :value, name: "a_different_name"
+ connection.remove_index :values, column: :value, name: "a_different_name"
end
-
+ ensure
connection.drop_table :values rescue nil
end
end
@@ -575,7 +798,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter?
class BulkAlterTableMigrationsTest < ActiveRecord::TestCase
def setup
@connection = Person.connection
- @connection.create_table(:delete_me, :force => true) {|t| }
+ @connection.create_table(:delete_me, force: true) { |t| }
Person.reset_column_information
Person.reset_sequence_name
end
@@ -585,19 +808,28 @@ if ActiveRecord::Base.connection.supports_bulk_alter?
end
def test_adding_multiple_columns
- assert_queries(1) do
+ classname = ActiveRecord::Base.connection.class.name[/[^:]*$/]
+ expected_query_count = {
+ "Mysql2Adapter" => 1,
+ "PostgreSQLAdapter" => 2, # one for bulk change, one for comment
+ }.fetch(classname) {
+ raise "need an expected query count for #{classname}"
+ }
+
+ assert_queries(expected_query_count) do
with_bulk_change_table do |t|
t.column :name, :string
t.string :qualification, :experience
- t.integer :age, :default => 0
- t.date :birthdate
+ t.integer :age, default: 0
+ t.date :birthdate, comment: "This is a comment"
t.timestamps null: true
end
end
assert_equal 8, columns.size
- [:name, :qualification, :experience].each {|s| assert_equal :string, column(s).type }
- assert_equal '0', column(:age).default
+ [:name, :qualification, :experience].each { |s| assert_equal :string, column(s).type }
+ assert_equal "0", column(:age).default
+ assert_equal "This is a comment", column(:birthdate).comment
end
def test_removing_columns
@@ -605,7 +837,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter?
t.string :qualification, :experience
end
- [:qualification, :experience].each {|c| assert column(c) }
+ [:qualification, :experience].each { |c| assert column(c) }
assert_queries(1) do
with_bulk_change_table do |t|
@@ -614,7 +846,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter?
end
end
- [:qualification, :experience].each {|c| assert ! column(c) }
+ [:qualification, :experience].each { |c| assert_not column(c) }
assert column(:qualification_experience)
end
@@ -625,10 +857,17 @@ if ActiveRecord::Base.connection.supports_bulk_alter?
t.integer :age
end
- # Adding an index fires a query every time to check if an index already exists or not
- assert_queries(3) do
+ classname = ActiveRecord::Base.connection.class.name[/[^:]*$/]
+ expected_query_count = {
+ "Mysql2Adapter" => 3, # Adding an index fires a query every time to check if an index already exists or not
+ "PostgreSQLAdapter" => 2,
+ }.fetch(classname) {
+ raise "need an expected query count for #{classname}"
+ }
+
+ assert_queries(expected_query_count) do
with_bulk_change_table do |t|
- t.index :username, :unique => true, :name => :awesome_username_index
+ t.index :username, unique: true, name: :awesome_username_index
t.index [:name, :age]
end
end
@@ -636,8 +875,8 @@ if ActiveRecord::Base.connection.supports_bulk_alter?
assert_equal 2, indexes.size
name_age_index = index(:index_delete_me_on_name_and_age)
- assert_equal ['name', 'age'].sort, name_age_index.columns.sort
- assert ! name_age_index.unique
+ assert_equal ["name", "age"].sort, name_age_index.columns.sort
+ assert_not name_age_index.unique
assert index(:awesome_username_index).unique
end
@@ -650,14 +889,22 @@ if ActiveRecord::Base.connection.supports_bulk_alter?
assert index(:index_delete_me_on_name)
- assert_queries(3) do
+ classname = ActiveRecord::Base.connection.class.name[/[^:]*$/]
+ expected_query_count = {
+ "Mysql2Adapter" => 3, # Adding an index fires a query every time to check if an index already exists or not
+ "PostgreSQLAdapter" => 2,
+ }.fetch(classname) {
+ raise "need an expected query count for #{classname}"
+ }
+
+ assert_queries(expected_query_count) do
with_bulk_change_table do |t|
t.remove_index :name
- t.index :name, :name => :new_name_index, :unique => true
+ t.index :name, name: :new_name_index, unique: true
end
end
- assert ! index(:index_delete_me_on_name)
+ assert_not index(:index_delete_me_on_name)
new_name_index = index(:new_name_index)
assert new_name_index.unique
@@ -669,51 +916,56 @@ if ActiveRecord::Base.connection.supports_bulk_alter?
t.date :birthdate
end
- assert ! column(:name).default
+ assert_not column(:name).default
assert_equal :date, column(:birthdate).type
- # One query for columns (delete_me table)
- # One query for primary key (delete_me table)
- # One query to do the bulk change
- assert_queries(3, :ignore_none => true) do
+ classname = ActiveRecord::Base.connection.class.name[/[^:]*$/]
+ expected_query_count = {
+ "Mysql2Adapter" => 3, # one query for columns, one query for primary key, one query to do the bulk change
+ "PostgreSQLAdapter" => 3, # one query for columns, one for bulk change, one for comment
+ }.fetch(classname) {
+ raise "need an expected query count for #{classname}"
+ }
+
+ assert_queries(expected_query_count, ignore_none: true) do
with_bulk_change_table do |t|
- t.change :name, :string, :default => 'NONAME'
- t.change :birthdate, :datetime
+ t.change :name, :string, default: "NONAME"
+ t.change :birthdate, :datetime, comment: "This is a comment"
end
end
- assert_equal 'NONAME', column(:name).default
+ assert_equal "NONAME", column(:name).default
assert_equal :datetime, column(:birthdate).type
+ assert_equal "This is a comment", column(:birthdate).comment
end
- protected
+ private
- def with_bulk_change_table
- # Reset columns/indexes cache as we're changing the table
- @columns = @indexes = nil
+ def with_bulk_change_table
+ # Reset columns/indexes cache as we're changing the table
+ @columns = @indexes = nil
- Person.connection.change_table(:delete_me, :bulk => true) do |t|
- yield t
+ Person.connection.change_table(:delete_me, bulk: true) do |t|
+ yield t
+ end
end
- end
- def column(name)
- columns.detect {|c| c.name == name.to_s }
- end
+ def column(name)
+ columns.detect { |c| c.name == name.to_s }
+ end
- def columns
- @columns ||= Person.connection.columns('delete_me')
- end
+ def columns
+ @columns ||= Person.connection.columns("delete_me")
+ end
- def index(name)
- indexes.detect {|i| i.name == name.to_s }
- end
+ def index(name)
+ indexes.detect { |i| i.name == name.to_s }
+ end
- def indexes
- @indexes ||= Person.connection.indexes('delete_me')
- end
+ def indexes
+ @indexes ||= Person.connection.indexes("delete_me")
+ end
end # AlterTableMigrationsTest
-
end
class CopyMigrationsTest < ActiveRecord::TestCase
@@ -733,18 +985,18 @@ class CopyMigrationsTest < ActiveRecord::TestCase
@migrations_path = MIGRATIONS_ROOT + "/valid"
@existing_migrations = Dir[@migrations_path + "/*.rb"]
- copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy"})
+ copied = ActiveRecord::Migration.copy(@migrations_path, bukkits: MIGRATIONS_ROOT + "/to_copy")
assert File.exist?(@migrations_path + "/4_people_have_hobbies.bukkits.rb")
assert File.exist?(@migrations_path + "/5_people_have_descriptions.bukkits.rb")
assert_equal [@migrations_path + "/4_people_have_hobbies.bukkits.rb", @migrations_path + "/5_people_have_descriptions.bukkits.rb"], copied.map(&:filename)
expected = "# This migration comes from bukkits (originally 1)"
- assert_equal expected, IO.readlines(@migrations_path + "/4_people_have_hobbies.bukkits.rb")[0].chomp
+ assert_equal expected, IO.readlines(@migrations_path + "/4_people_have_hobbies.bukkits.rb")[1].chomp
files_count = Dir[@migrations_path + "/*.rb"].length
- copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy"})
+ copied = ActiveRecord::Migration.copy(@migrations_path, bukkits: MIGRATIONS_ROOT + "/to_copy")
assert_equal files_count, Dir[@migrations_path + "/*.rb"].length
- assert copied.empty?
+ assert_empty copied
ensure
clear
end
@@ -775,7 +1027,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase
@existing_migrations = Dir[@migrations_path + "/*.rb"]
travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
- copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
+ copied = ActiveRecord::Migration.copy(@migrations_path, bukkits: MIGRATIONS_ROOT + "/to_copy_with_timestamps")
assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
expected = [@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb",
@@ -783,9 +1035,9 @@ class CopyMigrationsTest < ActiveRecord::TestCase
assert_equal expected, copied.map(&:filename)
files_count = Dir[@migrations_path + "/*.rb"].length
- copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
+ copied = ActiveRecord::Migration.copy(@migrations_path, bukkits: MIGRATIONS_ROOT + "/to_copy_with_timestamps")
assert_equal files_count, Dir[@migrations_path + "/*.rb"].length
- assert copied.empty?
+ assert_empty copied
end
ensure
clear
@@ -820,14 +1072,14 @@ class CopyMigrationsTest < ActiveRecord::TestCase
@existing_migrations = Dir[@migrations_path + "/*.rb"]
travel_to(Time.utc(2010, 2, 20, 10, 10, 10)) do
- ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
+ ActiveRecord::Migration.copy(@migrations_path, bukkits: MIGRATIONS_ROOT + "/to_copy_with_timestamps")
assert File.exist?(@migrations_path + "/20100301010102_people_have_hobbies.bukkits.rb")
assert File.exist?(@migrations_path + "/20100301010103_people_have_descriptions.bukkits.rb")
files_count = Dir[@migrations_path + "/*.rb"].length
- copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
+ copied = ActiveRecord::Migration.copy(@migrations_path, bukkits: MIGRATIONS_ROOT + "/to_copy_with_timestamps")
assert_equal files_count, Dir[@migrations_path + "/*.rb"].length
- assert copied.empty?
+ assert_empty copied
end
ensure
clear
@@ -838,17 +1090,17 @@ class CopyMigrationsTest < ActiveRecord::TestCase
@migrations_path = MIGRATIONS_ROOT + "/valid"
@existing_migrations = Dir[@migrations_path + "/*.rb"]
- copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/magic"})
+ copied = ActiveRecord::Migration.copy(@migrations_path, bukkits: MIGRATIONS_ROOT + "/magic")
assert File.exist?(@migrations_path + "/4_currencies_have_symbols.bukkits.rb")
assert_equal [@migrations_path + "/4_currencies_have_symbols.bukkits.rb"], copied.map(&:filename)
- expected = "# coding: ISO-8859-15\n# This migration comes from bukkits (originally 1)"
- assert_equal expected, IO.readlines(@migrations_path + "/4_currencies_have_symbols.bukkits.rb")[0..1].join.chomp
+ expected = "# frozen_string_literal: true\n# coding: ISO-8859-15\n# This migration comes from bukkits (originally 1)"
+ assert_equal expected, IO.readlines(@migrations_path + "/4_currencies_have_symbols.bukkits.rb")[0..2].join.chomp
files_count = Dir[@migrations_path + "/*.rb"].length
- copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/magic"})
+ copied = ActiveRecord::Migration.copy(@migrations_path, bukkits: MIGRATIONS_ROOT + "/magic")
assert_equal files_count, Dir[@migrations_path + "/*.rb"].length
- assert copied.empty?
+ assert_empty copied
ensure
clear
end
@@ -863,7 +1115,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase
skipped = []
on_skip = Proc.new { |name, migration| skipped << "#{name} #{migration.name}" }
- copied = ActiveRecord::Migration.copy(@migrations_path, sources, :on_skip => on_skip)
+ copied = ActiveRecord::Migration.copy(@migrations_path, sources, on_skip: on_skip)
assert_equal 2, copied.length
assert_equal 1, skipped.length
@@ -881,8 +1133,8 @@ class CopyMigrationsTest < ActiveRecord::TestCase
skipped = []
on_skip = Proc.new { |name, migration| skipped << "#{name} #{migration.name}" }
- copied = ActiveRecord::Migration.copy(@migrations_path, sources, :on_skip => on_skip)
- ActiveRecord::Migration.copy(@migrations_path, sources, :on_skip => on_skip)
+ copied = ActiveRecord::Migration.copy(@migrations_path, sources, on_skip: on_skip)
+ ActiveRecord::Migration.copy(@migrations_path, sources, on_skip: on_skip)
assert_equal 2, copied.length
assert_equal 0, skipped.length
@@ -895,7 +1147,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase
@existing_migrations = []
travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
- copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
+ copied = ActiveRecord::Migration.copy(@migrations_path, bukkits: MIGRATIONS_ROOT + "/to_copy_with_timestamps")
assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
assert_equal 2, copied.length
@@ -910,7 +1162,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase
@existing_migrations = []
travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
- copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
+ copied = ActiveRecord::Migration.copy(@migrations_path, bukkits: MIGRATIONS_ROOT + "/to_copy_with_timestamps")
assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
assert_equal 2, copied.length
@@ -922,10 +1174,13 @@ class CopyMigrationsTest < ActiveRecord::TestCase
def test_check_pending_with_stdlib_logger
old, ActiveRecord::Base.logger = ActiveRecord::Base.logger, ::Logger.new($stdout)
quietly do
- assert_nothing_raised { ActiveRecord::Migration::CheckPending.new(Proc.new {}).call({}) }
+ assert_nothing_raised { ActiveRecord::Migration::CheckPending.new(Proc.new { }).call({}) }
end
ensure
ActiveRecord::Base.logger = old
end
+ def test_unknown_migration_version_should_raise_an_argument_error
+ assert_raise(ArgumentError) { ActiveRecord::Migration[1.0] }
+ end
end
diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb
index 2ff6938e7b..30e199f1c5 100644
--- a/activerecord/test/cases/migrator_test.rb
+++ b/activerecord/test/cases/migrator_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "cases/migration/helper"
@@ -6,12 +8,12 @@ class MigratorTest < ActiveRecord::TestCase
# Use this class to sense if migrations have gone
# up or down.
- class Sensor < ActiveRecord::Migration
+ class Sensor < ActiveRecord::Migration::Current
attr_reader :went_up, :went_down
- def initialize name = self.class.name, version = nil
+ def initialize(name = self.class.name, version = nil)
super
- @went_up = false
+ @went_up = false
@went_down = false
end
@@ -45,100 +47,182 @@ class MigratorTest < ActiveRecord::TestCase
end
def test_migrator_with_duplicate_names
- assert_raises(ActiveRecord::DuplicateMigrationNameError, "Multiple migrations have the name Chunky") do
- list = [ActiveRecord::Migration.new('Chunky'), ActiveRecord::Migration.new('Chunky')]
+ 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
assert_raises(ActiveRecord::DuplicateMigrationVersionError) do
- list = [ActiveRecord::Migration.new('Foo', 1), ActiveRecord::Migration.new('Bar', 1)]
+ list = [ActiveRecord::Migration.new("Foo", 1), ActiveRecord::Migration.new("Bar", 1)]
ActiveRecord::Migrator.new(:up, list)
end
end
def test_migrator_with_missing_version_numbers
assert_raises(ActiveRecord::UnknownMigrationVersionError) do
- list = [ActiveRecord::Migration.new('Foo', 1), ActiveRecord::Migration.new('Bar', 2)]
+ list = [ActiveRecord::Migration.new("Foo", 1), ActiveRecord::Migration.new("Bar", 2)]
ActiveRecord::Migrator.new(:up, list, 3).run
end
+
+ assert_raises(ActiveRecord::UnknownMigrationVersionError) do
+ list = [ActiveRecord::Migration.new("Foo", 1), ActiveRecord::Migration.new("Bar", 2)]
+ ActiveRecord::Migrator.new(:up, list, -1).run
+ end
+
+ assert_raises(ActiveRecord::UnknownMigrationVersionError) do
+ list = [ActiveRecord::Migration.new("Foo", 1), ActiveRecord::Migration.new("Bar", 2)]
+ ActiveRecord::Migrator.new(:up, list, 0).run
+ end
+
+ assert_raises(ActiveRecord::UnknownMigrationVersionError) do
+ list = [ActiveRecord::Migration.new("Foo", 1), ActiveRecord::Migration.new("Bar", 2)]
+ ActiveRecord::Migrator.new(:up, list, 3).migrate
+ end
+
+ assert_raises(ActiveRecord::UnknownMigrationVersionError) do
+ list = [ActiveRecord::Migration.new("Foo", 1), ActiveRecord::Migration.new("Bar", 2)]
+ ActiveRecord::Migrator.new(:up, list, -1).migrate
+ end
end
def test_finds_migrations
- migrations = ActiveRecord::Migrator.migrations(MIGRATIONS_ROOT + "/valid")
+ migrations = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid").migrations
- [[1, 'ValidPeopleHaveLastNames'], [2, 'WeNeedReminders'], [3, 'InnocentJointable']].each_with_index do |pair, i|
+ [[1, "ValidPeopleHaveLastNames"], [2, "WeNeedReminders"], [3, "InnocentJointable"]].each_with_index do |pair, i|
assert_equal migrations[i].version, pair.first
assert_equal migrations[i].name, pair.last
end
end
def test_finds_migrations_in_subdirectories
- migrations = ActiveRecord::Migrator.migrations(MIGRATIONS_ROOT + "/valid_with_subdirectories")
+ migrations = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid_with_subdirectories").migrations
- [[1, 'ValidPeopleHaveLastNames'], [2, 'WeNeedReminders'], [3, 'InnocentJointable']].each_with_index do |pair, i|
+ [[1, "ValidPeopleHaveLastNames"], [2, "WeNeedReminders"], [3, "InnocentJointable"]].each_with_index do |pair, i|
assert_equal migrations[i].version, pair.first
assert_equal migrations[i].name, pair.last
end
end
def test_finds_migrations_from_two_directories
- directories = [MIGRATIONS_ROOT + '/valid_with_timestamps', MIGRATIONS_ROOT + '/to_copy_with_timestamps']
- migrations = ActiveRecord::Migrator.migrations directories
+ directories = [MIGRATIONS_ROOT + "/valid_with_timestamps", MIGRATIONS_ROOT + "/to_copy_with_timestamps"]
+ migrations = ActiveRecord::MigrationContext.new(directories).migrations
[[20090101010101, "PeopleHaveHobbies"],
[20090101010202, "PeopleHaveDescriptions"],
[20100101010101, "ValidWithTimestampsPeopleHaveLastNames"],
[20100201010101, "ValidWithTimestampsWeNeedReminders"],
[20100301010101, "ValidWithTimestampsInnocentJointable"]].each_with_index do |pair, i|
- assert_equal pair.first, migrations[i].version
- assert_equal pair.last, migrations[i].name
+ assert_equal pair.first, migrations[i].version
+ assert_equal pair.last, migrations[i].name
end
end
def test_finds_migrations_in_numbered_directory
- migrations = ActiveRecord::Migrator.migrations [MIGRATIONS_ROOT + '/10_urban']
+ migrations = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/10_urban").migrations
assert_equal 9, migrations[0].version
- assert_equal 'AddExpressions', migrations[0].name
+ assert_equal "AddExpressions", migrations[0].name
end
def test_relative_migrations
list = Dir.chdir(MIGRATIONS_ROOT) do
- ActiveRecord::Migrator.migrations("valid")
+ ActiveRecord::MigrationContext.new("valid").migrations
end
migration_proxy = list.find { |item|
- item.name == 'ValidPeopleHaveLastNames'
+ item.name == "ValidPeopleHaveLastNames"
}
- assert migration_proxy, 'should find pending migration'
+ assert migration_proxy, "should find pending migration"
end
def test_finds_pending_migrations
- ActiveRecord::SchemaMigration.create!(:version => '1')
- migration_list = [ActiveRecord::Migration.new('foo', 1), ActiveRecord::Migration.new('bar', 3)]
+ ActiveRecord::SchemaMigration.create!(version: "1")
+ migration_list = [ActiveRecord::Migration.new("foo", 1), ActiveRecord::Migration.new("bar", 3)]
migrations = ActiveRecord::Migrator.new(:up, migration_list).pending_migrations
assert_equal 1, migrations.size
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::MigrationContext.new(path).migrations_status
+ 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::MigrationContext.new(path).migrations_status
+ 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::MigrationContext.new(path).migrations_status
+ 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::MigrationContext.new(paths).migrations_status
+ end
+
def test_migrator_interleaved_migrations
- pass_one = [Sensor.new('One', 1)]
+ pass_one = [Sensor.new("One", 1)]
ActiveRecord::Migrator.new(:up, pass_one).migrate
assert pass_one.first.went_up
assert_not pass_one.first.went_down
- pass_two = [Sensor.new('One', 1), Sensor.new('Three', 3)]
+ pass_two = [Sensor.new("One", 1), Sensor.new("Three", 3)]
ActiveRecord::Migrator.new(:up, pass_two).migrate
assert_not pass_two[0].went_up
assert pass_two[1].went_up
assert pass_two.all? { |x| !x.went_down }
- pass_three = [Sensor.new('One', 1),
- Sensor.new('Two', 2),
- Sensor.new('Three', 3)]
+ pass_three = [Sensor.new("One", 1),
+ Sensor.new("Two", 2),
+ Sensor.new("Three", 3)]
ActiveRecord::Migrator.new(:down, pass_three).migrate
assert pass_three[0].went_down
@@ -148,25 +232,28 @@ class MigratorTest < ActiveRecord::TestCase
def test_up_calls_up
migrations = [Sensor.new(nil, 0), Sensor.new(nil, 1), Sensor.new(nil, 2)]
- ActiveRecord::Migrator.new(:up, migrations).migrate
+ migrator = ActiveRecord::Migrator.new(:up, migrations)
+ migrator.migrate
assert migrations.all?(&:went_up)
assert migrations.all? { |m| !m.went_down }
- assert_equal 2, ActiveRecord::Migrator.current_version
+ assert_equal 2, migrator.current_version
end
def test_down_calls_down
test_up_calls_up
migrations = [Sensor.new(nil, 0), Sensor.new(nil, 1), Sensor.new(nil, 2)]
- ActiveRecord::Migrator.new(:down, migrations).migrate
+ migrator = ActiveRecord::Migrator.new(:down, migrations)
+ migrator.migrate
assert migrations.all? { |m| !m.went_up }
assert migrations.all?(&:went_down)
- assert_equal 0, ActiveRecord::Migrator.current_version
+ assert_equal 0, migrator.current_version
end
def test_current_version
- ActiveRecord::SchemaMigration.create!(:version => '1000')
- assert_equal 1000, ActiveRecord::Migrator.current_version
+ ActiveRecord::SchemaMigration.create!(version: "1000")
+ migrator = ActiveRecord::MigrationContext.new("db/migrate")
+ assert_equal 1000, migrator.current_version
end
def test_migrator_one_up
@@ -205,38 +292,42 @@ class MigratorTest < ActiveRecord::TestCase
def test_migrator_double_up
calls, migrations = sensors(3)
- assert_equal(0, ActiveRecord::Migrator.current_version)
+ migrator = ActiveRecord::Migrator.new(:up, migrations, 1)
+ assert_equal(0, migrator.current_version)
- ActiveRecord::Migrator.new(:up, migrations, 1).migrate
+ migrator.migrate
assert_equal [[:up, 1]], calls
calls.clear
- ActiveRecord::Migrator.new(:up, migrations, 1).migrate
+ migrator.migrate
assert_equal [], calls
end
def test_migrator_double_down
calls, migrations = sensors(3)
+ migrator = ActiveRecord::Migrator.new(:up, migrations, 1)
- assert_equal(0, ActiveRecord::Migrator.current_version)
+ assert_equal 0, migrator.current_version
- ActiveRecord::Migrator.new(:up, migrations, 1).run
+ migrator.run
assert_equal [[:up, 1]], calls
calls.clear
- ActiveRecord::Migrator.new(:down, migrations, 1).run
+ migrator = ActiveRecord::Migrator.new(:down, migrations, 1)
+ migrator.run
assert_equal [[:down, 1]], calls
calls.clear
- ActiveRecord::Migrator.new(:down, migrations, 1).run
+ migrator.run
assert_equal [], calls
- assert_equal(0, ActiveRecord::Migrator.current_version)
+ assert_equal 0, migrator.current_version
end
def test_migrator_verbosity
_, migrations = sensors(3)
+ ActiveRecord::Migration.verbose = true
ActiveRecord::Migrator.new(:up, migrations, 1).migrate
assert_not_equal 0, ActiveRecord::Migration.message_count
@@ -249,7 +340,6 @@ class MigratorTest < ActiveRecord::TestCase
def test_migrator_verbosity_off
_, migrations = sensors(3)
- ActiveRecord::Migration.message_count = 0
ActiveRecord::Migration.verbose = false
ActiveRecord::Migrator.new(:up, migrations, 1).migrate
assert_equal 0, ActiveRecord::Migration.message_count
@@ -277,112 +367,142 @@ class MigratorTest < ActiveRecord::TestCase
def test_migrator_going_down_due_to_version_target
calls, migrator = migrator_class(3)
+ migrator = migrator.new("valid")
- migrator.up("valid", 1)
+ migrator.up(1)
assert_equal [[:up, 1]], calls
calls.clear
- migrator.migrate("valid", 0)
+ migrator.migrate(0)
assert_equal [[:down, 1]], calls
calls.clear
- migrator.migrate("valid")
+ migrator.migrate
assert_equal [[:up, 1], [:up, 2], [:up, 3]], calls
end
+ def test_migrator_output_when_running_multiple_migrations
+ _, migrator = migrator_class(3)
+ migrator = migrator.new("valid")
+
+ result = migrator.migrate
+ assert_equal(3, result.count)
+
+ # Nothing migrated from duplicate run
+ result = migrator.migrate
+ assert_equal(0, result.count)
+
+ result = migrator.rollback
+ assert_equal(1, result.count)
+ end
+
+ def test_migrator_output_when_running_single_migration
+ _, migrator = migrator_class(1)
+ migrator = migrator.new("valid")
+
+ result = migrator.run(:up, 1)
+
+ assert_equal(1, result.version)
+ end
+
def test_migrator_rollback
_, migrator = migrator_class(3)
+ migrator = migrator.new("valid")
- migrator.migrate("valid")
- assert_equal(3, ActiveRecord::Migrator.current_version)
+ migrator.migrate
+ assert_equal(3, migrator.current_version)
- migrator.rollback("valid")
- assert_equal(2, ActiveRecord::Migrator.current_version)
+ migrator.rollback
+ assert_equal(2, migrator.current_version)
- migrator.rollback("valid")
- assert_equal(1, ActiveRecord::Migrator.current_version)
+ migrator.rollback
+ assert_equal(1, migrator.current_version)
- migrator.rollback("valid")
- assert_equal(0, ActiveRecord::Migrator.current_version)
+ migrator.rollback
+ assert_equal(0, migrator.current_version)
- migrator.rollback("valid")
- assert_equal(0, ActiveRecord::Migrator.current_version)
+ migrator.rollback
+ assert_equal(0, migrator.current_version)
end
def test_migrator_db_has_no_schema_migrations_table
_, migrator = migrator_class(3)
+ migrator = migrator.new("valid")
ActiveRecord::Base.connection.drop_table "schema_migrations", if_exists: true
- assert_not ActiveRecord::Base.connection.table_exists?('schema_migrations')
- migrator.migrate("valid", 1)
- assert ActiveRecord::Base.connection.table_exists?('schema_migrations')
+ assert_not ActiveRecord::Base.connection.table_exists?("schema_migrations")
+ migrator.migrate(1)
+ assert ActiveRecord::Base.connection.table_exists?("schema_migrations")
end
def test_migrator_forward
_, migrator = migrator_class(3)
- migrator.migrate("/valid", 1)
- assert_equal(1, ActiveRecord::Migrator.current_version)
+ migrator = migrator.new("/valid")
+ migrator.migrate(1)
+ assert_equal(1, migrator.current_version)
- migrator.forward("/valid", 2)
- assert_equal(3, ActiveRecord::Migrator.current_version)
+ migrator.forward(2)
+ assert_equal(3, migrator.current_version)
- migrator.forward("/valid")
- assert_equal(3, ActiveRecord::Migrator.current_version)
+ migrator.forward
+ assert_equal(3, migrator.current_version)
end
def test_only_loads_pending_migrations
# migrate up to 1
- ActiveRecord::SchemaMigration.create!(:version => '1')
+ ActiveRecord::SchemaMigration.create!(version: "1")
calls, migrator = migrator_class(3)
- migrator.migrate("valid", nil)
+ migrator = migrator.new("valid")
+ migrator.migrate
assert_equal [[:up, 2], [:up, 3]], calls
end
def test_get_all_versions
_, migrator = migrator_class(3)
+ migrator = migrator.new("valid")
- migrator.migrate("valid")
- assert_equal([1,2,3], ActiveRecord::Migrator.get_all_versions)
+ migrator.migrate
+ assert_equal([1, 2, 3], migrator.get_all_versions)
- migrator.rollback("valid")
- assert_equal([1,2], ActiveRecord::Migrator.get_all_versions)
+ migrator.rollback
+ assert_equal([1, 2], migrator.get_all_versions)
- migrator.rollback("valid")
- assert_equal([1], ActiveRecord::Migrator.get_all_versions)
+ migrator.rollback
+ assert_equal([1], migrator.get_all_versions)
- migrator.rollback("valid")
- assert_equal([], ActiveRecord::Migrator.get_all_versions)
+ migrator.rollback
+ assert_equal([], migrator.get_all_versions)
end
private
- def m(name, version)
- x = Sensor.new name, version
- x.extend(Module.new {
- define_method(:up) { yield(:up, x); super() }
- define_method(:down) { yield(:down, x); super() }
- }) if block_given?
- end
-
- def sensors(count)
- calls = []
- migrations = count.times.map { |i|
- m(nil, i + 1) { |c,migration|
- calls << [c, migration.version]
+ def m(name, version)
+ x = Sensor.new name, version
+ x.extend(Module.new {
+ define_method(:up) { yield(:up, x); super() }
+ define_method(:down) { yield(:down, x); super() }
+ }) if block_given?
+ end
+
+ def sensors(count)
+ calls = []
+ migrations = count.times.map { |i|
+ m(nil, i + 1) { |c, migration|
+ calls << [c, migration.version]
+ }
}
- }
- [calls, migrations]
- end
+ [calls, migrations]
+ end
- def migrator_class(count)
- calls, migrations = sensors(count)
+ def migrator_class(count)
+ calls, migrations = sensors(count)
- migrator = Class.new(ActiveRecord::Migrator).extend(Module.new {
- define_method(:migrations) { |paths|
- migrations
+ migrator = Class.new(ActiveRecord::MigrationContext) {
+ define_method(:migrations) { |*|
+ migrations
+ }
}
- })
- [calls, migrator]
- end
+ [calls, migrator]
+ end
end
diff --git a/activerecord/test/cases/mixin_test.rb b/activerecord/test/cases/mixin_test.rb
index 7ebdcac711..fdb8ac6ab3 100644
--- a/activerecord/test/cases/mixin_test.rb
+++ b/activerecord/test/cases/mixin_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
class Mixin < ActiveRecord::Base
@@ -10,10 +12,6 @@ class TouchTest < ActiveRecord::TestCase
travel_to Time.now
end
- teardown do
- travel_back
- end
-
def test_update
stamped = Mixin.new
@@ -41,13 +39,12 @@ class TouchTest < ActiveRecord::TestCase
old_updated_at = stamped.updated_at
- travel 5.minutes do
- stamped.lft_will_change!
- stamped.save
+ travel 5.minutes
+ stamped.lft_will_change!
+ stamped.save
- assert_equal Time.now, stamped.updated_at
- assert_equal old_updated_at, stamped.created_at
- end
+ assert_equal Time.now, stamped.updated_at
+ assert_equal old_updated_at, stamped.created_at
end
def test_create_turned_off
@@ -64,5 +61,4 @@ class TouchTest < ActiveRecord::TestCase
ensure
Mixin.record_timestamps = true
end
-
end
diff --git a/activerecord/test/cases/modules_test.rb b/activerecord/test/cases/modules_test.rb
index 7f31325f47..87455e4fcb 100644
--- a/activerecord/test/cases/modules_test.rb
+++ b/activerecord/test/cases/modules_test.rb
@@ -1,8 +1,10 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/company_in_module'
-require 'models/shop'
-require 'models/developer'
-require 'models/computer'
+require "models/company_in_module"
+require "models/shop"
+require "models/developer"
+require "models/computer"
class ModulesTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :projects, :developers, :collections, :products, :variants
@@ -30,8 +32,8 @@ class ModulesTest < ActiveRecord::TestCase
def test_module_spanning_associations
firm = MyApplication::Business::Firm.first
- assert !firm.clients.empty?, "Firm should have clients"
- assert_nil firm.class.table_name.match('::'), "Firm shouldn't have the module appear in its table name"
+ assert_not firm.clients.empty?, "Firm should have clients"
+ assert_nil firm.class.table_name.match("::"), "Firm shouldn't have the module appear in its table name"
end
def test_module_spanning_has_and_belongs_to_many_associations
@@ -41,7 +43,7 @@ class ModulesTest < ActiveRecord::TestCase
end
def test_associations_spanning_cross_modules
- account = MyApplication::Billing::Account.all.merge!(:order => 'id').first
+ account = MyApplication::Billing::Account.all.merge!(order: "id").first
assert_kind_of MyApplication::Business::Firm, account.firm
assert_kind_of MyApplication::Billing::Firm, account.qualified_billing_firm
assert_kind_of MyApplication::Billing::Firm, account.unqualified_billing_firm
@@ -50,20 +52,20 @@ class ModulesTest < ActiveRecord::TestCase
end
def test_find_account_and_include_company
- account = MyApplication::Billing::Account.all.merge!(:includes => :firm).find(1)
+ account = MyApplication::Billing::Account.all.merge!(includes: :firm).find(1)
assert_kind_of MyApplication::Business::Firm, account.firm
end
def test_table_name
- assert_equal 'accounts', MyApplication::Billing::Account.table_name, 'table_name for ActiveRecord model in module'
- assert_equal 'companies', MyApplication::Business::Client.table_name, 'table_name for ActiveRecord model subclass'
- assert_equal 'company_contacts', MyApplication::Business::Client::Contact.table_name, 'table_name for ActiveRecord model enclosed by another ActiveRecord model'
+ assert_equal "accounts", MyApplication::Billing::Account.table_name, "table_name for ActiveRecord model in module"
+ assert_equal "companies", MyApplication::Business::Client.table_name, "table_name for ActiveRecord model subclass"
+ assert_equal "company_contacts", MyApplication::Business::Client::Contact.table_name, "table_name for ActiveRecord model enclosed by another ActiveRecord model"
end
def test_assign_ids
firm = MyApplication::Business::Firm.first
- assert_nothing_raised NameError, "Should be able to resolve all class constants via reflection" do
+ assert_nothing_raised do
firm.client_ids = [MyApplication::Business::Client.first.id]
end
end
@@ -72,9 +74,9 @@ class ModulesTest < ActiveRecord::TestCase
def test_eager_loading_in_modules
clients = []
- assert_nothing_raised NameError, "Should be able to resolve all class constants via reflection" do
- clients << MyApplication::Business::Client.references(:accounts).merge!(:includes => {:firm => :account}, :where => 'accounts.id IS NOT NULL').find(3)
- clients << MyApplication::Business::Client.includes(:firm => :account).find(3)
+ assert_nothing_raised do
+ clients << MyApplication::Business::Client.references(:accounts).merge!(includes: { firm: :account }, where: "accounts.id IS NOT NULL").find(3)
+ clients << MyApplication::Business::Client.includes(firm: :account).find(3)
end
clients.each do |client|
@@ -85,9 +87,9 @@ class ModulesTest < ActiveRecord::TestCase
end
def test_module_table_name_prefix
- assert_equal 'prefixed_companies', MyApplication::Business::Prefixed::Company.table_name, 'inferred table_name for ActiveRecord model in module with table_name_prefix'
- assert_equal 'prefixed_companies', MyApplication::Business::Prefixed::Nested::Company.table_name, 'table_name for ActiveRecord model in nested module with a parent table_name_prefix'
- assert_equal 'companies', MyApplication::Business::Prefixed::Firm.table_name, 'explicit table_name for ActiveRecord model in module with table_name_prefix should not be prefixed'
+ assert_equal "prefixed_companies", MyApplication::Business::Prefixed::Company.table_name, "inferred table_name for ActiveRecord model in module with table_name_prefix"
+ assert_equal "prefixed_companies", MyApplication::Business::Prefixed::Nested::Company.table_name, "table_name for ActiveRecord model in nested module with a parent table_name_prefix"
+ assert_equal "companies", MyApplication::Business::Prefixed::Firm.table_name, "explicit table_name for ActiveRecord model in module with table_name_prefix should not be prefixed"
end
def test_module_table_name_prefix_with_global_prefix
@@ -101,21 +103,21 @@ class ModulesTest < ActiveRecord::TestCase
MyApplication::Business::Prefixed::Nested::Company,
MyApplication::Billing::Account ]
- ActiveRecord::Base.table_name_prefix = 'global_'
+ ActiveRecord::Base.table_name_prefix = "global_"
classes.each(&:reset_table_name)
- assert_equal 'global_companies', MyApplication::Business::Company.table_name, 'inferred table_name for ActiveRecord model in module without table_name_prefix'
- assert_equal 'prefixed_companies', MyApplication::Business::Prefixed::Company.table_name, 'inferred table_name for ActiveRecord model in module with table_name_prefix'
- assert_equal 'prefixed_companies', MyApplication::Business::Prefixed::Nested::Company.table_name, 'table_name for ActiveRecord model in nested module with a parent table_name_prefix'
- assert_equal 'companies', MyApplication::Business::Prefixed::Firm.table_name, 'explicit table_name for ActiveRecord model in module with table_name_prefix should not be prefixed'
+ assert_equal "global_companies", MyApplication::Business::Company.table_name, "inferred table_name for ActiveRecord model in module without table_name_prefix"
+ assert_equal "prefixed_companies", MyApplication::Business::Prefixed::Company.table_name, "inferred table_name for ActiveRecord model in module with table_name_prefix"
+ assert_equal "prefixed_companies", MyApplication::Business::Prefixed::Nested::Company.table_name, "table_name for ActiveRecord model in nested module with a parent table_name_prefix"
+ assert_equal "companies", MyApplication::Business::Prefixed::Firm.table_name, "explicit table_name for ActiveRecord model in module with table_name_prefix should not be prefixed"
ensure
- ActiveRecord::Base.table_name_prefix = ''
+ ActiveRecord::Base.table_name_prefix = ""
classes.each(&:reset_table_name)
end
def test_module_table_name_suffix
- assert_equal 'companies_suffixed', MyApplication::Business::Suffixed::Company.table_name, 'inferred table_name for ActiveRecord model in module with table_name_suffix'
- assert_equal 'companies_suffixed', MyApplication::Business::Suffixed::Nested::Company.table_name, 'table_name for ActiveRecord model in nested module with a parent table_name_suffix'
- assert_equal 'companies', MyApplication::Business::Suffixed::Firm.table_name, 'explicit table_name for ActiveRecord model in module with table_name_suffix should not be suffixed'
+ assert_equal "companies_suffixed", MyApplication::Business::Suffixed::Company.table_name, "inferred table_name for ActiveRecord model in module with table_name_suffix"
+ assert_equal "companies_suffixed", MyApplication::Business::Suffixed::Nested::Company.table_name, "table_name for ActiveRecord model in nested module with a parent table_name_suffix"
+ assert_equal "companies", MyApplication::Business::Suffixed::Firm.table_name, "explicit table_name for ActiveRecord model in module with table_name_suffix should not be suffixed"
end
def test_module_table_name_suffix_with_global_suffix
@@ -129,14 +131,14 @@ class ModulesTest < ActiveRecord::TestCase
MyApplication::Business::Suffixed::Nested::Company,
MyApplication::Billing::Account ]
- ActiveRecord::Base.table_name_suffix = '_global'
+ ActiveRecord::Base.table_name_suffix = "_global"
classes.each(&:reset_table_name)
- assert_equal 'companies_global', MyApplication::Business::Company.table_name, 'inferred table_name for ActiveRecord model in module without table_name_suffix'
- assert_equal 'companies_suffixed', MyApplication::Business::Suffixed::Company.table_name, 'inferred table_name for ActiveRecord model in module with table_name_suffix'
- assert_equal 'companies_suffixed', MyApplication::Business::Suffixed::Nested::Company.table_name, 'table_name for ActiveRecord model in nested module with a parent table_name_suffix'
- assert_equal 'companies', MyApplication::Business::Suffixed::Firm.table_name, 'explicit table_name for ActiveRecord model in module with table_name_suffix should not be suffixed'
+ assert_equal "companies_global", MyApplication::Business::Company.table_name, "inferred table_name for ActiveRecord model in module without table_name_suffix"
+ assert_equal "companies_suffixed", MyApplication::Business::Suffixed::Company.table_name, "inferred table_name for ActiveRecord model in module with table_name_suffix"
+ assert_equal "companies_suffixed", MyApplication::Business::Suffixed::Nested::Company.table_name, "table_name for ActiveRecord model in nested module with a parent table_name_suffix"
+ assert_equal "companies", MyApplication::Business::Suffixed::Firm.table_name, "explicit table_name for ActiveRecord model in module with table_name_suffix should not be suffixed"
ensure
- ActiveRecord::Base.table_name_suffix = ''
+ ActiveRecord::Base.table_name_suffix = ""
classes.each(&:reset_table_name)
end
@@ -153,7 +155,7 @@ class ModulesTest < ActiveRecord::TestCase
ActiveRecord::Base.store_full_sti_class = true
collection = Shop::Collection.first
- assert !collection.products.empty?, "Collection should have products"
+ assert_not collection.products.empty?, "Collection should have products"
assert_nothing_raised { collection.destroy }
ensure
ActiveRecord::Base.store_full_sti_class = old
@@ -164,7 +166,7 @@ class ModulesTest < ActiveRecord::TestCase
ActiveRecord::Base.store_full_sti_class = true
product = Shop::Product.first
- assert !product.variants.empty?, "Product should have variants"
+ assert_not product.variants.empty?, "Product should have variants"
assert_nothing_raised { product.destroy }
ensure
ActiveRecord::Base.store_full_sti_class = old
diff --git a/activerecord/test/cases/multiparameter_attributes_test.rb b/activerecord/test/cases/multiparameter_attributes_test.rb
index ae18573126..6f3903eed4 100644
--- a/activerecord/test/cases/multiparameter_attributes_test.rb
+++ b/activerecord/test/cases/multiparameter_attributes_test.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/topic'
-require 'models/customer'
+require "models/topic"
+require "models/customer"
class MultiParameterAttributeTest < ActiveRecord::TestCase
fixtures :topics
@@ -11,15 +13,13 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
topic.attributes = attributes
# note that extra #to_date call allows test to pass for Oracle, which
# treats dates/times the same
- assert_date_from_db Date.new(2004, 6, 24), topic.last_read.to_date
+ assert_equal Date.new(2004, 6, 24), topic.last_read.to_date
end
def test_multiparameter_attributes_on_date_with_empty_year
attributes = { "last_read(1i)" => "", "last_read(2i)" => "6", "last_read(3i)" => "24" }
topic = Topic.find(1)
topic.attributes = attributes
- # note that extra #to_date call allows test to pass for Oracle, which
- # treats dates/times the same
assert_nil topic.last_read
end
@@ -27,8 +27,6 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "", "last_read(3i)" => "24" }
topic = Topic.find(1)
topic.attributes = attributes
- # note that extra #to_date call allows test to pass for Oracle, which
- # treats dates/times the same
assert_nil topic.last_read
end
@@ -36,8 +34,6 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "6", "last_read(3i)" => "" }
topic = Topic.find(1)
topic.attributes = attributes
- # note that extra #to_date call allows test to pass for Oracle, which
- # treats dates/times the same
assert_nil topic.last_read
end
@@ -45,8 +41,6 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
attributes = { "last_read(1i)" => "", "last_read(2i)" => "6", "last_read(3i)" => "" }
topic = Topic.find(1)
topic.attributes = attributes
- # note that extra #to_date call allows test to pass for Oracle, which
- # treats dates/times the same
assert_nil topic.last_read
end
@@ -54,8 +48,6 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "", "last_read(3i)" => "" }
topic = Topic.find(1)
topic.attributes = attributes
- # note that extra #to_date call allows test to pass for Oracle, which
- # treats dates/times the same
assert_nil topic.last_read
end
@@ -63,8 +55,6 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
attributes = { "last_read(1i)" => "", "last_read(2i)" => "", "last_read(3i)" => "24" }
topic = Topic.find(1)
topic.attributes = attributes
- # note that extra #to_date call allows test to pass for Oracle, which
- # treats dates/times the same
assert_nil topic.last_read
end
@@ -214,6 +204,20 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
Topic.reset_column_information
end
+ def test_multiparameter_attributes_on_time_with_time_zone_aware_attributes_and_invalid_time_params
+ with_timezone_config aware_attributes: true do
+ Topic.reset_column_information
+ attributes = {
+ "written_on(1i)" => "2004", "written_on(2i)" => "", "written_on(3i)" => ""
+ }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_nil topic.written_on
+ end
+ ensure
+ Topic.reset_column_information
+ end
+
def test_multiparameter_attributes_on_time_with_time_zone_aware_attributes_false
with_timezone_config default: :local, aware_attributes: false, zone: -28800 do
attributes = {
@@ -223,7 +227,7 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
topic = Topic.find(1)
topic.attributes = attributes
assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on
- assert_equal false, topic.written_on.respond_to?(:time_zone)
+ assert_not_respond_to topic.written_on, :time_zone
end
end
@@ -238,7 +242,7 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
topic = Topic.find(1)
topic.attributes = attributes
assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on
- assert_equal false, topic.written_on.respond_to?(:time_zone)
+ assert_not_respond_to topic.written_on, :time_zone
end
ensure
Topic.skip_time_zone_conversion_for_attributes = []
@@ -257,11 +261,24 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
topic = Topic.find(1)
topic.attributes = attributes
assert_equal Time.zone.local(2000, 1, 1, 16, 24, 0), topic.bonus_time
- assert_not topic.bonus_time.utc?
+ assert_not_predicate topic.bonus_time, :utc?
+
+ attributes = {
+ "written_on(1i)" => "2000", "written_on(2i)" => "", "written_on(3i)" => "",
+ "written_on(4i)" => "", "written_on(5i)" => ""
+ }
+ topic.attributes = attributes
+ assert_nil topic.written_on
end
ensure
Topic.reset_column_information
end
+
+ def test_multiparameter_attributes_setting_time_attribute
+ topic = Topic.new("bonus_time(4i)" => "01", "bonus_time(5i)" => "05")
+ assert_equal 1, topic.bonus_time.hour
+ assert_equal 5, topic.bonus_time.min
+ end
end
def test_multiparameter_attributes_on_time_with_empty_seconds
@@ -276,16 +293,15 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
end
end
- unless current_adapter? :OracleAdapter
- def test_multiparameter_attributes_setting_time_attribute
- topic = Topic.new( "bonus_time(4i)"=> "01", "bonus_time(5i)" => "05" )
- assert_equal 1, topic.bonus_time.hour
- assert_equal 5, topic.bonus_time.min
- end
+ def test_multiparameter_attributes_setting_date_attribute
+ topic = Topic.new("written_on(1i)" => "1952", "written_on(2i)" => "3", "written_on(3i)" => "11")
+ assert_equal 1952, topic.written_on.year
+ assert_equal 3, topic.written_on.month
+ assert_equal 11, topic.written_on.day
end
- def test_multiparameter_attributes_setting_date_attribute
- topic = Topic.new( "written_on(1i)" => "1952", "written_on(2i)" => "3", "written_on(3i)" => "11" )
+ def test_create_with_multiparameter_attributes_setting_date_attribute
+ topic = Topic.create_with("written_on(1i)" => "1952", "written_on(2i)" => "3", "written_on(3i)" => "11").new
assert_equal 1952, topic.written_on.year
assert_equal 3, topic.written_on.month
assert_equal 11, topic.written_on.day
@@ -293,11 +309,25 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
def test_multiparameter_attributes_setting_date_and_time_attribute
topic = Topic.new(
- "written_on(1i)" => "1952",
- "written_on(2i)" => "3",
- "written_on(3i)" => "11",
- "written_on(4i)" => "13",
- "written_on(5i)" => "55")
+ "written_on(1i)" => "1952",
+ "written_on(2i)" => "3",
+ "written_on(3i)" => "11",
+ "written_on(4i)" => "13",
+ "written_on(5i)" => "55")
+ assert_equal 1952, topic.written_on.year
+ assert_equal 3, topic.written_on.month
+ assert_equal 11, topic.written_on.day
+ assert_equal 13, topic.written_on.hour
+ assert_equal 55, topic.written_on.min
+ end
+
+ def test_create_with_multiparameter_attributes_setting_date_and_time_attribute
+ topic = Topic.create_with(
+ "written_on(1i)" => "1952",
+ "written_on(2i)" => "3",
+ "written_on(3i)" => "11",
+ "written_on(4i)" => "13",
+ "written_on(5i)" => "55").new
assert_equal 1952, topic.written_on.year
assert_equal 3, topic.written_on.month
assert_equal 11, topic.written_on.day
@@ -306,8 +336,8 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
end
def test_multiparameter_attributes_setting_time_but_not_date_on_date_field
- assert_raise( ActiveRecord::MultiparameterAssignmentErrors ) do
- Topic.new( "written_on(4i)" => "13", "written_on(5i)" => "55" )
+ assert_raise(ActiveRecord::MultiparameterAssignmentErrors) do
+ Topic.new("written_on(4i)" => "13", "written_on(5i)" => "55")
end
end
@@ -355,4 +385,15 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
assert_equal("address", ex.errors[0].attribute)
end
+
+ def test_multiparameter_assigned_attributes_did_not_come_from_user
+ topic = Topic.new(
+ "written_on(1i)" => "1952",
+ "written_on(2i)" => "3",
+ "written_on(3i)" => "11",
+ "written_on(4i)" => "13",
+ "written_on(5i)" => "55",
+ )
+ assert_not_predicate topic, :written_on_came_from_user?
+ end
end
diff --git a/activerecord/test/cases/multiple_db_test.rb b/activerecord/test/cases/multiple_db_test.rb
index 39cdcf5403..192d2f5251 100644
--- a/activerecord/test/cases/multiple_db_test.rb
+++ b/activerecord/test/cases/multiple_db_test.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/entrant'
-require 'models/bird'
-require 'models/course'
+require "models/entrant"
+require "models/bird"
+require "models/course"
class MultipleDbTest < ActiveRecord::TestCase
self.use_transactional_tests = false
@@ -24,6 +26,13 @@ class MultipleDbTest < ActiveRecord::TestCase
assert_equal(ActiveRecord::Base.connection, Entrant.connection)
end
+ def test_swapping_the_connection
+ old_spec_name, Course.connection_specification_name = Course.connection_specification_name, "primary"
+ assert_equal(Entrant.connection, Course.connection)
+ ensure
+ Course.connection_specification_name = old_spec_name
+ end
+
def test_find
c1 = Course.find(1)
assert_equal "Ruby Development", c1.name
@@ -53,7 +62,7 @@ class MultipleDbTest < ActiveRecord::TestCase
ActiveSupport::Dependencies.clear
Object.send(:remove_const, :Course)
- require_dependency 'models/course'
+ require_dependency "models/course"
assert Course.connection
end
@@ -83,14 +92,9 @@ class MultipleDbTest < ActiveRecord::TestCase
assert_equal "Ruby Developer", Entrant.find(1).name
end
- def test_arel_table_engines
- assert_not_equal Entrant.arel_engine, Bird.arel_engine
- assert_not_equal Entrant.arel_engine, Course.arel_engine
- end
-
def test_connection
- assert_equal Entrant.arel_engine.connection, Bird.arel_engine.connection
- assert_not_equal Entrant.arel_engine.connection, Course.arel_engine.connection
+ assert_same Entrant.connection, Bird.connection
+ assert_not_same Entrant.connection, Course.connection
end
unless in_memory_db?
@@ -104,7 +108,7 @@ class MultipleDbTest < ActiveRecord::TestCase
def test_associations_should_work_when_model_has_no_connection
begin
ActiveRecord::Base.remove_connection
- assert_nothing_raised ActiveRecord::ConnectionNotEstablished do
+ assert_nothing_raised do
College.first.courses.first
end
ensure
diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb
index d72225f3d3..bb1c1ea17d 100644
--- a/activerecord/test/cases/nested_attributes_test.rb
+++ b/activerecord/test/cases/nested_attributes_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "models/pirate"
require "models/ship"
@@ -9,11 +11,11 @@ require "models/man"
require "models/interest"
require "models/owner"
require "models/pet"
-require 'active_support/hash_with_indifferent_access'
+require "active_support/hash_with_indifferent_access"
class TestNestedAttributesInGeneral < ActiveRecord::TestCase
teardown do
- Pirate.accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc(&:empty?)
+ Pirate.accepts_nested_attributes_for :ship, allow_destroy: true, reject_if: proc(&:empty?)
end
def test_base_should_have_an_empty_nested_attributes_options
@@ -30,28 +32,28 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
end
def test_should_not_build_a_new_record_using_reject_all_even_if_destroy_is_given
- pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
- pirate.birds_with_reject_all_blank_attributes = [{:name => '', :color => '', :_destroy => '0'}]
+ pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?")
+ pirate.birds_with_reject_all_blank_attributes = [{ name: "", color: "", _destroy: "0" }]
pirate.save!
- assert pirate.birds_with_reject_all_blank.empty?
+ assert_empty pirate.birds_with_reject_all_blank
end
def test_should_not_build_a_new_record_if_reject_all_blank_returns_false
- pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
- pirate.birds_with_reject_all_blank_attributes = [{:name => '', :color => ''}]
+ pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?")
+ pirate.birds_with_reject_all_blank_attributes = [{ name: "", color: "" }]
pirate.save!
- assert pirate.birds_with_reject_all_blank.empty?
+ assert_empty pirate.birds_with_reject_all_blank
end
def test_should_build_a_new_record_if_reject_all_blank_does_not_return_false
- pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
- pirate.birds_with_reject_all_blank_attributes = [{:name => 'Tweetie', :color => ''}]
+ pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?")
+ pirate.birds_with_reject_all_blank_attributes = [{ name: "Tweetie", color: "" }]
pirate.save!
assert_equal 1, pirate.birds_with_reject_all_blank.count
- assert_equal 'Tweetie', pirate.birds_with_reject_all_blank.first.name
+ assert_equal "Tweetie", pirate.birds_with_reject_all_blank.first.name
end
def test_should_raise_an_ArgumentError_for_non_existing_associations
@@ -61,114 +63,134 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
assert_equal "No association found for name `honesty'. Has it been defined yet?", exception.message
end
+ def test_should_raise_an_UnknownAttributeError_for_non_existing_nested_attributes
+ exception = assert_raise ActiveModel::UnknownAttributeError do
+ Pirate.new(ship_attributes: { sail: true })
+ end
+ assert_equal "unknown attribute 'sail' for Ship.", exception.message
+ end
+
def test_should_disable_allow_destroy_by_default
Pirate.accepts_nested_attributes_for :ship
pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?")
- ship = pirate.create_ship(name: 'Nights Dirty Lightning')
+ ship = pirate.create_ship(name: "Nights Dirty Lightning")
- pirate.update(ship_attributes: { '_destroy' => true, :id => ship.id })
+ pirate.update(ship_attributes: { "_destroy" => true, :id => ship.id })
- assert_nothing_raised(ActiveRecord::RecordNotFound) { pirate.ship.reload }
+ assert_nothing_raised { pirate.ship.reload }
end
def test_a_model_should_respond_to_underscore_destroy_and_return_if_it_is_marked_for_destruction
- ship = Ship.create!(:name => 'Nights Dirty Lightning')
- assert !ship._destroy
+ ship = Ship.create!(name: "Nights Dirty Lightning")
+ assert_not ship._destroy
ship.mark_for_destruction
assert ship._destroy
end
def test_reject_if_method_without_arguments
- Pirate.accepts_nested_attributes_for :ship, :reject_if => :new_record?
+ Pirate.accepts_nested_attributes_for :ship, reject_if: :new_record?
- pirate = Pirate.new(:catchphrase => "Stop wastin' me time")
- pirate.ship_attributes = { :name => 'Black Pearl' }
- assert_no_difference('Ship.count') { pirate.save! }
+ pirate = Pirate.new(catchphrase: "Stop wastin' me time")
+ pirate.ship_attributes = { name: "Black Pearl" }
+ assert_no_difference("Ship.count") { pirate.save! }
end
def test_reject_if_method_with_arguments
- Pirate.accepts_nested_attributes_for :ship, :reject_if => :reject_empty_ships_on_create
+ Pirate.accepts_nested_attributes_for :ship, reject_if: :reject_empty_ships_on_create
- pirate = Pirate.new(:catchphrase => "Stop wastin' me time")
- pirate.ship_attributes = { :name => 'Red Pearl', :_reject_me_if_new => true }
- assert_no_difference('Ship.count') { pirate.save! }
+ pirate = Pirate.new(catchphrase: "Stop wastin' me time")
+ pirate.ship_attributes = { name: "Red Pearl", _reject_me_if_new: true }
+ assert_no_difference("Ship.count") { pirate.save! }
# pirate.reject_empty_ships_on_create returns false for saved pirate records
# in the previous step note that pirate gets saved but ship fails
- pirate.ship_attributes = { :name => 'Red Pearl', :_reject_me_if_new => true }
- assert_difference('Ship.count') { pirate.save! }
+ pirate.ship_attributes = { name: "Red Pearl", _reject_me_if_new: true }
+ assert_difference("Ship.count") { pirate.save! }
end
def test_reject_if_with_indifferent_keys
- Pirate.accepts_nested_attributes_for :ship, :reject_if => proc {|attributes| attributes[:name].blank? }
+ Pirate.accepts_nested_attributes_for :ship, reject_if: proc { |attributes| attributes[:name].blank? }
- pirate = Pirate.new(:catchphrase => "Stop wastin' me time")
- pirate.ship_attributes = { :name => 'Hello Pearl' }
- assert_difference('Ship.count') { pirate.save! }
+ pirate = Pirate.new(catchphrase: "Stop wastin' me time")
+ pirate.ship_attributes = { name: "Hello Pearl" }
+ assert_difference("Ship.count") { pirate.save! }
end
def test_reject_if_with_a_proc_which_returns_true_always_for_has_one
- Pirate.accepts_nested_attributes_for :ship, :reject_if => proc {|attributes| true }
- pirate = Pirate.new(catchphrase: "Stop wastin' me time")
- ship = pirate.create_ship(name: 's1')
- pirate.update({ship_attributes: { name: 's2', id: ship.id } })
- assert_equal 's1', ship.reload.name
+ Pirate.accepts_nested_attributes_for :ship, reject_if: proc { |attributes| true }
+ pirate = Pirate.create(catchphrase: "Stop wastin' me time")
+ ship = pirate.create_ship(name: "s1")
+ pirate.update(ship_attributes: { name: "s2", id: ship.id })
+ assert_equal "s1", ship.reload.name
end
def test_reuse_already_built_new_record
pirate = Pirate.new
ship_built_first = pirate.build_ship
- pirate.ship_attributes = { name: 'Ship 1' }
+ pirate.ship_attributes = { name: "Ship 1" }
assert_equal ship_built_first.object_id, pirate.ship.object_id
end
def test_do_not_allow_assigning_foreign_key_when_reusing_existing_new_record
pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?")
pirate.build_ship
- pirate.ship_attributes = { name: 'Ship 1', pirate_id: pirate.id + 1 }
+ pirate.ship_attributes = { name: "Ship 1", pirate_id: pirate.id + 1 }
assert_equal pirate.id, pirate.ship.pirate_id
end
def test_reject_if_with_a_proc_which_returns_true_always_for_has_many
- Man.accepts_nested_attributes_for :interests, :reject_if => proc {|attributes| true }
+ Man.accepts_nested_attributes_for :interests, reject_if: proc { |attributes| true }
man = Man.create(name: "John")
- interest = man.interests.create(topic: 'photography')
- man.update({interests_attributes: { topic: 'gardening', id: interest.id } })
- assert_equal 'photography', interest.reload.topic
+ interest = man.interests.create(topic: "photography")
+ man.update(interests_attributes: { topic: "gardening", id: interest.id })
+ assert_equal "photography", interest.reload.topic
end
def test_destroy_works_independent_of_reject_if
- Man.accepts_nested_attributes_for :interests, :reject_if => proc {|attributes| true }, :allow_destroy => true
+ Man.accepts_nested_attributes_for :interests, reject_if: proc { |attributes| true }, allow_destroy: true
man = Man.create(name: "Jon")
- interest = man.interests.create(topic: 'the ladies')
- man.update({interests_attributes: { _destroy: "1", id: interest.id } })
- assert man.reload.interests.empty?
+ interest = man.interests.create(topic: "the ladies")
+ man.update(interests_attributes: { _destroy: "1", id: interest.id })
+ assert_empty man.reload.interests
+ end
+
+ def test_reject_if_is_not_short_circuited_if_allow_destroy_is_false
+ Pirate.accepts_nested_attributes_for :ship, reject_if: ->(a) { a[:name] == "The Golden Hind" }, allow_destroy: false
+
+ pirate = Pirate.create!(catchphrase: "Stop wastin' me time", ship_attributes: { name: "White Pearl", _destroy: "1" })
+ assert_equal "White Pearl", pirate.reload.ship.name
+
+ pirate.update!(ship_attributes: { id: pirate.ship.id, name: "The Golden Hind", _destroy: "1" })
+ assert_equal "White Pearl", pirate.reload.ship.name
+
+ pirate.update!(ship_attributes: { id: pirate.ship.id, name: "Black Pearl", _destroy: "1" })
+ assert_equal "Black Pearl", pirate.reload.ship.name
end
def test_has_many_association_updating_a_single_record
Man.accepts_nested_attributes_for(:interests)
- man = Man.create(name: 'John')
- interest = man.interests.create(topic: 'photography')
- man.update({interests_attributes: {topic: 'gardening', id: interest.id}})
- assert_equal 'gardening', interest.reload.topic
+ man = Man.create(name: "John")
+ interest = man.interests.create(topic: "photography")
+ man.update(interests_attributes: { topic: "gardening", id: interest.id })
+ assert_equal "gardening", interest.reload.topic
end
def test_reject_if_with_blank_nested_attributes_id
# When using a select list to choose an existing 'ship' id, with include_blank: true
- Pirate.accepts_nested_attributes_for :ship, :reject_if => proc {|attributes| attributes[:id].blank? }
+ Pirate.accepts_nested_attributes_for :ship, reject_if: proc { |attributes| attributes[:id].blank? }
- pirate = Pirate.new(:catchphrase => "Stop wastin' me time")
- pirate.ship_attributes = { :id => "" }
- assert_nothing_raised(ActiveRecord::RecordNotFound) { pirate.save! }
+ pirate = Pirate.new(catchphrase: "Stop wastin' me time")
+ pirate.ship_attributes = { id: "" }
+ assert_nothing_raised { pirate.save! }
end
def test_first_and_array_index_zero_methods_return_the_same_value_when_nested_attributes_are_set_to_update_existing_record
Man.accepts_nested_attributes_for(:interests)
- man = Man.create(:name => "John")
- interest = man.interests.create :topic => 'gardening'
+ man = Man.create(name: "John")
+ interest = man.interests.create topic: "gardening"
man = Man.find man.id
- man.interests_attributes = [{:id => interest.id, :topic => 'gardening'}]
+ man.interests_attributes = [{ id: interest.id, topic: "gardening" }]
assert_equal man.interests.first.topic, man.interests[0].topic
end
@@ -176,11 +198,11 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
mean_pirate_class = Class.new(Pirate) do
accepts_nested_attributes_for :parrot
def parrot_attributes=(attrs)
- super(attrs.merge(:color => "blue"))
+ super(attrs.merge(color: "blue"))
end
end
mean_pirate = mean_pirate_class.new
- mean_pirate.parrot_attributes = { :name => "James" }
+ mean_pirate.parrot_attributes = { name: "James" }
assert_equal "James", mean_pirate.parrot.name
assert_equal "blue", mean_pirate.parrot.color
end
@@ -192,20 +214,32 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
accepts_nested_attributes_for :parrot
end
mean_pirate = mean_pirate_class.new
- mean_pirate.parrot_attributes = { :name => "James" }
+ mean_pirate.parrot_attributes = { name: "James" }
assert_equal "James", mean_pirate.parrot.name
end
+
+ def test_should_not_create_duplicates_with_create_with
+ Man.accepts_nested_attributes_for(:interests)
+
+ assert_difference("Interest.count", 1) do
+ Man.create_with(
+ interests_attributes: [{ topic: "Pirate king" }]
+ ).find_or_create_by!(
+ name: "Monkey D. Luffy"
+ )
+ end
+ end
end
class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
def setup
- @pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
- @ship = @pirate.create_ship(:name => 'Nights Dirty Lightning')
+ @pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?")
+ @ship = @pirate.create_ship(name: "Nights Dirty Lightning")
end
def test_should_raise_argument_error_if_trying_to_build_polymorphic_belongs_to
exception = assert_raise ArgumentError do
- Treasure.new(:name => 'pearl', :looter_attributes => {:catchphrase => "Arrr"})
+ Treasure.new(name: "pearl", looter_attributes: { catchphrase: "Arrr" })
end
assert_equal "Cannot build association `looter'. Are you trying to build a polymorphic one-to-one association?", exception.message
end
@@ -216,15 +250,15 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
def test_should_build_a_new_record_if_there_is_no_id
@ship.destroy
- @pirate.reload.ship_attributes = { :name => 'Davy Jones Gold Dagger' }
+ @pirate.reload.ship_attributes = { name: "Davy Jones Gold Dagger" }
- assert !@pirate.ship.persisted?
- assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name
+ assert_not_predicate @pirate.ship, :persisted?
+ assert_equal "Davy Jones Gold Dagger", @pirate.ship.name
end
def test_should_not_build_a_new_record_if_there_is_no_id_and_destroy_is_truthy
@ship.destroy
- @pirate.reload.ship_attributes = { :name => 'Davy Jones Gold Dagger', :_destroy => '1' }
+ @pirate.reload.ship_attributes = { name: "Davy Jones Gold Dagger", _destroy: "1" }
assert_nil @pirate.ship
end
@@ -237,53 +271,54 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
end
def test_should_replace_an_existing_record_if_there_is_no_id
- @pirate.reload.ship_attributes = { :name => 'Davy Jones Gold Dagger' }
+ @pirate.reload.ship_attributes = { name: "Davy Jones Gold Dagger" }
- assert !@pirate.ship.persisted?
- assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name
- assert_equal 'Nights Dirty Lightning', @ship.name
+ assert_not_predicate @pirate.ship, :persisted?
+ assert_equal "Davy Jones Gold Dagger", @pirate.ship.name
+ assert_equal "Nights Dirty Lightning", @ship.name
end
def test_should_not_replace_an_existing_record_if_there_is_no_id_and_destroy_is_truthy
- @pirate.reload.ship_attributes = { :name => 'Davy Jones Gold Dagger', :_destroy => '1' }
+ @pirate.reload.ship_attributes = { name: "Davy Jones Gold Dagger", _destroy: "1" }
assert_equal @ship, @pirate.ship
- assert_equal 'Nights Dirty Lightning', @pirate.ship.name
+ assert_equal "Nights Dirty Lightning", @pirate.ship.name
end
def test_should_modify_an_existing_record_if_there_is_a_matching_id
- @pirate.reload.ship_attributes = { :id => @ship.id, :name => 'Davy Jones Gold Dagger' }
+ @pirate.reload.ship_attributes = { id: @ship.id, name: "Davy Jones Gold Dagger" }
assert_equal @ship, @pirate.ship
- assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name
+ assert_equal "Davy Jones Gold Dagger", @pirate.ship.name
end
def test_should_raise_RecordNotFound_if_an_id_is_given_but_doesnt_return_a_record
exception = assert_raise ActiveRecord::RecordNotFound do
- @pirate.ship_attributes = { :id => 1234567890 }
+ @pirate.ship_attributes = { id: 1234567890 }
end
assert_equal "Couldn't find Ship with ID=1234567890 for Pirate with ID=#{@pirate.id}", exception.message
end
def test_should_take_a_hash_with_string_keys_and_update_the_associated_model
- @pirate.reload.ship_attributes = { 'id' => @ship.id, 'name' => 'Davy Jones Gold Dagger' }
+ @pirate.reload.ship_attributes = { "id" => @ship.id, "name" => "Davy Jones Gold Dagger" }
assert_equal @ship, @pirate.ship
- assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name
+ assert_equal "Davy Jones Gold Dagger", @pirate.ship.name
end
def test_should_modify_an_existing_record_if_there_is_a_matching_composite_id
- @ship.stubs(:id).returns('ABC1X')
- @pirate.ship_attributes = { :id => @ship.id, :name => 'Davy Jones Gold Dagger' }
+ @ship.stub(:id, "ABC1X") do
+ @pirate.ship_attributes = { id: @ship.id, name: "Davy Jones Gold Dagger" }
- assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name
+ assert_equal "Davy Jones Gold Dagger", @pirate.ship.name
+ end
end
def test_should_destroy_an_existing_record_if_there_is_a_matching_id_and_destroy_is_truthy
@pirate.ship.destroy
- [1, '1', true, 'true'].each do |truth|
- ship = @pirate.reload.create_ship(name: 'Mister Pablo')
+ [1, "1", true, "true"].each do |truth|
+ ship = @pirate.reload.create_ship(name: "Mister Pablo")
@pirate.update(ship_attributes: { id: ship.id, _destroy: truth })
assert_nil @pirate.reload.ship
@@ -292,7 +327,7 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
end
def test_should_not_destroy_an_existing_record_if_destroy_is_not_truthy
- [nil, '0', 0, 'false', false].each do |not_truth|
+ [nil, "0", 0, "false", false].each do |not_truth|
@pirate.update(ship_attributes: { id: @pirate.ship.id, _destroy: not_truth })
assert_equal @ship, @pirate.reload.ship
@@ -300,39 +335,39 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
end
def test_should_not_destroy_an_existing_record_if_allow_destroy_is_false
- Pirate.accepts_nested_attributes_for :ship, :allow_destroy => false, :reject_if => proc(&:empty?)
+ Pirate.accepts_nested_attributes_for :ship, allow_destroy: false, reject_if: proc(&:empty?)
- @pirate.update(ship_attributes: { id: @pirate.ship.id, _destroy: '1' })
+ @pirate.update(ship_attributes: { id: @pirate.ship.id, _destroy: "1" })
assert_equal @ship, @pirate.reload.ship
- Pirate.accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc(&:empty?)
+ Pirate.accepts_nested_attributes_for :ship, allow_destroy: true, reject_if: proc(&:empty?)
end
def test_should_also_work_with_a_HashWithIndifferentAccess
- @pirate.ship_attributes = ActiveSupport::HashWithIndifferentAccess.new(:id => @ship.id, :name => 'Davy Jones Gold Dagger')
+ @pirate.ship_attributes = ActiveSupport::HashWithIndifferentAccess.new(id: @ship.id, name: "Davy Jones Gold Dagger")
- assert @pirate.ship.persisted?
- assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name
+ assert_predicate @pirate.ship, :persisted?
+ assert_equal "Davy Jones Gold Dagger", @pirate.ship.name
end
def test_should_work_with_update_as_well
- @pirate.update({ catchphrase: 'Arr', ship_attributes: { id: @ship.id, name: 'Mister Pablo' } })
+ @pirate.update(catchphrase: "Arr", ship_attributes: { id: @ship.id, name: "Mister Pablo" })
@pirate.reload
- assert_equal 'Arr', @pirate.catchphrase
- assert_equal 'Mister Pablo', @pirate.ship.name
+ assert_equal "Arr", @pirate.catchphrase
+ assert_equal "Mister Pablo", @pirate.ship.name
end
def test_should_not_destroy_the_associated_model_until_the_parent_is_saved
- @pirate.attributes = { :ship_attributes => { :id => @ship.id, :_destroy => '1' } }
+ @pirate.attributes = { ship_attributes: { id: @ship.id, _destroy: "1" } }
- assert !@pirate.ship.destroyed?
- assert @pirate.ship.marked_for_destruction?
+ assert_not_predicate @pirate.ship, :destroyed?
+ assert_predicate @pirate.ship, :marked_for_destruction?
@pirate.save
- assert @pirate.ship.destroyed?
+ assert_predicate @pirate.ship, :destroyed?
assert_nil @pirate.reload.ship
end
@@ -341,56 +376,55 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
end
def test_should_accept_update_only_option
- @pirate.update(update_only_ship_attributes: { id: @pirate.ship.id, name: 'Mayflower' })
+ @pirate.update(update_only_ship_attributes: { id: @pirate.ship.id, name: "Mayflower" })
end
def test_should_create_new_model_when_nothing_is_there_and_update_only_is_true
@ship.delete
- @pirate.reload.update(update_only_ship_attributes: { name: 'Mayflower' })
+ @pirate.reload.update(update_only_ship_attributes: { name: "Mayflower" })
assert_not_nil @pirate.ship
end
def test_should_update_existing_when_update_only_is_true_and_no_id_is_given
@ship.delete
- @ship = @pirate.create_update_only_ship(name: 'Nights Dirty Lightning')
+ @ship = @pirate.create_update_only_ship(name: "Nights Dirty Lightning")
- @pirate.update(update_only_ship_attributes: { name: 'Mayflower' })
+ @pirate.update(update_only_ship_attributes: { name: "Mayflower" })
- assert_equal 'Mayflower', @ship.reload.name
+ assert_equal "Mayflower", @ship.reload.name
assert_equal @ship, @pirate.reload.ship
end
def test_should_update_existing_when_update_only_is_true_and_id_is_given
@ship.delete
- @ship = @pirate.create_update_only_ship(name: 'Nights Dirty Lightning')
+ @ship = @pirate.create_update_only_ship(name: "Nights Dirty Lightning")
- @pirate.update(update_only_ship_attributes: { name: 'Mayflower', id: @ship.id })
+ @pirate.update(update_only_ship_attributes: { name: "Mayflower", id: @ship.id })
- assert_equal 'Mayflower', @ship.reload.name
+ assert_equal "Mayflower", @ship.reload.name
assert_equal @ship, @pirate.reload.ship
end
def test_should_destroy_existing_when_update_only_is_true_and_id_is_given_and_is_marked_for_destruction
- Pirate.accepts_nested_attributes_for :update_only_ship, :update_only => true, :allow_destroy => true
+ Pirate.accepts_nested_attributes_for :update_only_ship, update_only: true, allow_destroy: true
@ship.delete
- @ship = @pirate.create_update_only_ship(name: 'Nights Dirty Lightning')
+ @ship = @pirate.create_update_only_ship(name: "Nights Dirty Lightning")
- @pirate.update(update_only_ship_attributes: { name: 'Mayflower', id: @ship.id, _destroy: true })
+ @pirate.update(update_only_ship_attributes: { name: "Mayflower", id: @ship.id, _destroy: true })
assert_nil @pirate.reload.ship
assert_raise(ActiveRecord::RecordNotFound) { Ship.find(@ship.id) }
- Pirate.accepts_nested_attributes_for :update_only_ship, :update_only => true, :allow_destroy => false
+ Pirate.accepts_nested_attributes_for :update_only_ship, update_only: true, allow_destroy: false
end
-
end
class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
def setup
- @ship = Ship.new(:name => 'Nights Dirty Lightning')
- @pirate = @ship.build_pirate(:catchphrase => 'Aye')
+ @ship = Ship.new(name: "Nights Dirty Lightning")
+ @pirate = @ship.build_pirate(catchphrase: "Aye")
@ship.save!
end
@@ -400,15 +434,15 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
def test_should_build_a_new_record_if_there_is_no_id
@pirate.destroy
- @ship.reload.pirate_attributes = { :catchphrase => 'Arr' }
+ @ship.reload.pirate_attributes = { catchphrase: "Arr" }
- assert !@ship.pirate.persisted?
- assert_equal 'Arr', @ship.pirate.catchphrase
+ assert_not_predicate @ship.pirate, :persisted?
+ assert_equal "Arr", @ship.pirate.catchphrase
end
def test_should_not_build_a_new_record_if_there_is_no_id_and_destroy_is_truthy
@pirate.destroy
- @ship.reload.pirate_attributes = { :catchphrase => 'Arr', :_destroy => '1' }
+ @ship.reload.pirate_attributes = { catchphrase: "Arr", _destroy: "1" }
assert_nil @ship.pirate
end
@@ -421,52 +455,53 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
end
def test_should_replace_an_existing_record_if_there_is_no_id
- @ship.reload.pirate_attributes = { :catchphrase => 'Arr' }
+ @ship.reload.pirate_attributes = { catchphrase: "Arr" }
- assert !@ship.pirate.persisted?
- assert_equal 'Arr', @ship.pirate.catchphrase
- assert_equal 'Aye', @pirate.catchphrase
+ assert_not_predicate @ship.pirate, :persisted?
+ assert_equal "Arr", @ship.pirate.catchphrase
+ assert_equal "Aye", @pirate.catchphrase
end
def test_should_not_replace_an_existing_record_if_there_is_no_id_and_destroy_is_truthy
- @ship.reload.pirate_attributes = { :catchphrase => 'Arr', :_destroy => '1' }
+ @ship.reload.pirate_attributes = { catchphrase: "Arr", _destroy: "1" }
assert_equal @pirate, @ship.pirate
- assert_equal 'Aye', @ship.pirate.catchphrase
+ assert_equal "Aye", @ship.pirate.catchphrase
end
def test_should_modify_an_existing_record_if_there_is_a_matching_id
- @ship.reload.pirate_attributes = { :id => @pirate.id, :catchphrase => 'Arr' }
+ @ship.reload.pirate_attributes = { id: @pirate.id, catchphrase: "Arr" }
assert_equal @pirate, @ship.pirate
- assert_equal 'Arr', @ship.pirate.catchphrase
+ assert_equal "Arr", @ship.pirate.catchphrase
end
def test_should_raise_RecordNotFound_if_an_id_is_given_but_doesnt_return_a_record
exception = assert_raise ActiveRecord::RecordNotFound do
- @ship.pirate_attributes = { :id => 1234567890 }
+ @ship.pirate_attributes = { id: 1234567890 }
end
assert_equal "Couldn't find Pirate with ID=1234567890 for Ship with ID=#{@ship.id}", exception.message
end
def test_should_take_a_hash_with_string_keys_and_update_the_associated_model
- @ship.reload.pirate_attributes = { 'id' => @pirate.id, 'catchphrase' => 'Arr' }
+ @ship.reload.pirate_attributes = { "id" => @pirate.id, "catchphrase" => "Arr" }
assert_equal @pirate, @ship.pirate
- assert_equal 'Arr', @ship.pirate.catchphrase
+ assert_equal "Arr", @ship.pirate.catchphrase
end
def test_should_modify_an_existing_record_if_there_is_a_matching_composite_id
- @pirate.stubs(:id).returns('ABC1X')
- @ship.pirate_attributes = { :id => @pirate.id, :catchphrase => 'Arr' }
+ @pirate.stub(:id, "ABC1X") do
+ @ship.pirate_attributes = { id: @pirate.id, catchphrase: "Arr" }
- assert_equal 'Arr', @ship.pirate.catchphrase
+ assert_equal "Arr", @ship.pirate.catchphrase
+ end
end
def test_should_destroy_an_existing_record_if_there_is_a_matching_id_and_destroy_is_truthy
@ship.pirate.destroy
- [1, '1', true, 'true'].each do |truth|
- pirate = @ship.reload.create_pirate(catchphrase: 'Arr')
+ [1, "1", true, "true"].each do |truth|
+ pirate = @ship.reload.create_pirate(catchphrase: "Arr")
@ship.update(pirate_attributes: { id: pirate.id, _destroy: truth })
assert_raise(ActiveRecord::RecordNotFound) { pirate.reload }
end
@@ -487,34 +522,34 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
end
def test_should_not_destroy_an_existing_record_if_destroy_is_not_truthy
- [nil, '0', 0, 'false', false].each do |not_truth|
+ [nil, "0", 0, "false", false].each do |not_truth|
@ship.update(pirate_attributes: { id: @ship.pirate.id, _destroy: not_truth })
- assert_nothing_raised(ActiveRecord::RecordNotFound) { @ship.pirate.reload }
+ assert_nothing_raised { @ship.pirate.reload }
end
end
def test_should_not_destroy_an_existing_record_if_allow_destroy_is_false
- Ship.accepts_nested_attributes_for :pirate, :allow_destroy => false, :reject_if => proc(&:empty?)
+ Ship.accepts_nested_attributes_for :pirate, allow_destroy: false, reject_if: proc(&:empty?)
- @ship.update(pirate_attributes: { id: @ship.pirate.id, _destroy: '1' })
- assert_nothing_raised(ActiveRecord::RecordNotFound) { @ship.pirate.reload }
+ @ship.update(pirate_attributes: { id: @ship.pirate.id, _destroy: "1" })
+ assert_nothing_raised { @ship.pirate.reload }
ensure
- Ship.accepts_nested_attributes_for :pirate, :allow_destroy => true, :reject_if => proc(&:empty?)
+ Ship.accepts_nested_attributes_for :pirate, allow_destroy: true, reject_if: proc(&:empty?)
end
def test_should_work_with_update_as_well
- @ship.update({ name: 'Mister Pablo', pirate_attributes: { catchphrase: 'Arr' } })
+ @ship.update(name: "Mister Pablo", pirate_attributes: { catchphrase: "Arr" })
@ship.reload
- assert_equal 'Mister Pablo', @ship.name
- assert_equal 'Arr', @ship.pirate.catchphrase
+ assert_equal "Mister Pablo", @ship.name
+ assert_equal "Arr", @ship.pirate.catchphrase
end
def test_should_not_destroy_the_associated_model_until_the_parent_is_saved
pirate = @ship.pirate
- @ship.attributes = { :pirate_attributes => { :id => pirate.id, '_destroy' => true } }
- assert_nothing_raised(ActiveRecord::RecordNotFound) { Pirate.find(pirate.id) }
+ @ship.attributes = { pirate_attributes: { :id => pirate.id, "_destroy" => true } }
+ assert_nothing_raised { Pirate.find(pirate.id) }
@ship.save
assert_raise(ActiveRecord::RecordNotFound) { Pirate.find(pirate.id) }
end
@@ -525,40 +560,40 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
def test_should_create_new_model_when_nothing_is_there_and_update_only_is_true
@pirate.delete
- @ship.reload.attributes = { :update_only_pirate_attributes => { :catchphrase => 'Arr' } }
+ @ship.reload.attributes = { update_only_pirate_attributes: { catchphrase: "Arr" } }
- assert !@ship.update_only_pirate.persisted?
+ assert_not_predicate @ship.update_only_pirate, :persisted?
end
def test_should_update_existing_when_update_only_is_true_and_no_id_is_given
@pirate.delete
- @pirate = @ship.create_update_only_pirate(catchphrase: 'Aye')
+ @pirate = @ship.create_update_only_pirate(catchphrase: "Aye")
- @ship.update(update_only_pirate_attributes: { catchphrase: 'Arr' })
- assert_equal 'Arr', @pirate.reload.catchphrase
+ @ship.update(update_only_pirate_attributes: { catchphrase: "Arr" })
+ assert_equal "Arr", @pirate.reload.catchphrase
assert_equal @pirate, @ship.reload.update_only_pirate
end
def test_should_update_existing_when_update_only_is_true_and_id_is_given
@pirate.delete
- @pirate = @ship.create_update_only_pirate(catchphrase: 'Aye')
+ @pirate = @ship.create_update_only_pirate(catchphrase: "Aye")
- @ship.update(update_only_pirate_attributes: { catchphrase: 'Arr', id: @pirate.id })
+ @ship.update(update_only_pirate_attributes: { catchphrase: "Arr", id: @pirate.id })
- assert_equal 'Arr', @pirate.reload.catchphrase
+ assert_equal "Arr", @pirate.reload.catchphrase
assert_equal @pirate, @ship.reload.update_only_pirate
end
def test_should_destroy_existing_when_update_only_is_true_and_id_is_given_and_is_marked_for_destruction
- Ship.accepts_nested_attributes_for :update_only_pirate, :update_only => true, :allow_destroy => true
+ Ship.accepts_nested_attributes_for :update_only_pirate, update_only: true, allow_destroy: true
@pirate.delete
- @pirate = @ship.create_update_only_pirate(catchphrase: 'Aye')
+ @pirate = @ship.create_update_only_pirate(catchphrase: "Aye")
- @ship.update(update_only_pirate_attributes: { catchphrase: 'Arr', id: @pirate.id, _destroy: true })
+ @ship.update(update_only_pirate_attributes: { catchphrase: "Arr", id: @pirate.id, _destroy: true })
assert_raise(ActiveRecord::RecordNotFound) { @pirate.reload }
- Ship.accepts_nested_attributes_for :update_only_pirate, :update_only => true, :allow_destroy => false
+ Ship.accepts_nested_attributes_for :update_only_pirate, update_only: true, allow_destroy: false
end
end
@@ -567,11 +602,17 @@ module NestedAttributesOnACollectionAssociationTests
assert_respond_to @pirate, association_setter
end
+ def test_should_raise_an_UnknownAttributeError_for_non_existing_nested_attributes_for_has_many
+ exception = assert_raise ActiveModel::UnknownAttributeError do
+ @pirate.parrots_attributes = [{ peg_leg: true }]
+ end
+ assert_equal "unknown attribute 'peg_leg' for Parrot.", exception.message
+ end
+
def test_should_save_only_one_association_on_create
- pirate = Pirate.create!({
- :catchphrase => 'Arr',
- association_getter => { 'foo' => { :name => 'Grace OMalley' } }
- })
+ pirate = Pirate.create!(
+ :catchphrase => "Arr",
+ association_getter => { "foo" => { name: "Grace OMalley" } })
assert_equal 1, pirate.reload.send(@association_name).count
end
@@ -579,88 +620,89 @@ module NestedAttributesOnACollectionAssociationTests
def test_should_take_a_hash_with_string_keys_and_assign_the_attributes_to_the_associated_models
@alternate_params[association_getter].stringify_keys!
@pirate.update @alternate_params
- assert_equal ['Grace OMalley', 'Privateers Greed'], [@child_1.reload.name, @child_2.reload.name]
+ assert_equal ["Grace OMalley", "Privateers Greed"], [@child_1.reload.name, @child_2.reload.name]
end
def test_should_take_an_array_and_assign_the_attributes_to_the_associated_models
@pirate.send(association_setter, @alternate_params[association_getter].values)
@pirate.save
- assert_equal ['Grace OMalley', 'Privateers Greed'], [@child_1.reload.name, @child_2.reload.name]
+ assert_equal ["Grace OMalley", "Privateers Greed"], [@child_1.reload.name, @child_2.reload.name]
end
def test_should_also_work_with_a_HashWithIndifferentAccess
- @pirate.send(association_setter, ActiveSupport::HashWithIndifferentAccess.new('foo' => ActiveSupport::HashWithIndifferentAccess.new(:id => @child_1.id, :name => 'Grace OMalley')))
+ @pirate.send(association_setter, ActiveSupport::HashWithIndifferentAccess.new("foo" => ActiveSupport::HashWithIndifferentAccess.new(id: @child_1.id, name: "Grace OMalley")))
@pirate.save
- assert_equal 'Grace OMalley', @child_1.reload.name
+ assert_equal "Grace OMalley", @child_1.reload.name
end
def test_should_take_a_hash_and_assign_the_attributes_to_the_associated_models
@pirate.attributes = @alternate_params
- assert_equal 'Grace OMalley', @pirate.send(@association_name).first.name
- assert_equal 'Privateers Greed', @pirate.send(@association_name).last.name
+ assert_equal "Grace OMalley", @pirate.send(@association_name).first.name
+ assert_equal "Privateers Greed", @pirate.send(@association_name).last.name
end
def test_should_not_load_association_when_updating_existing_records
@pirate.reload
- @pirate.send(association_setter, [{ :id => @child_1.id, :name => 'Grace OMalley' }])
- assert ! @pirate.send(@association_name).loaded?
+ @pirate.send(association_setter, [{ id: @child_1.id, name: "Grace OMalley" }])
+ assert_not_predicate @pirate.send(@association_name), :loaded?
@pirate.save
- assert ! @pirate.send(@association_name).loaded?
- assert_equal 'Grace OMalley', @child_1.reload.name
+ assert_not_predicate @pirate.send(@association_name), :loaded?
+ assert_equal "Grace OMalley", @child_1.reload.name
end
def test_should_not_overwrite_unsaved_updates_when_loading_association
@pirate.reload
- @pirate.send(association_setter, [{ :id => @child_1.id, :name => 'Grace OMalley' }])
- assert_equal 'Grace OMalley', @pirate.send(@association_name).send(:load_target).find { |r| r.id == @child_1.id }.name
+ @pirate.send(association_setter, [{ id: @child_1.id, name: "Grace OMalley" }])
+ assert_equal "Grace OMalley", @pirate.send(@association_name).load_target.find { |r| r.id == @child_1.id }.name
end
def test_should_preserve_order_when_not_overwriting_unsaved_updates
@pirate.reload
- @pirate.send(association_setter, [{ :id => @child_1.id, :name => 'Grace OMalley' }])
- assert_equal @child_1.id, @pirate.send(@association_name).send(:load_target).first.id
+ @pirate.send(association_setter, [{ id: @child_1.id, name: "Grace OMalley" }])
+ assert_equal @child_1.id, @pirate.send(@association_name).load_target.first.id
end
def test_should_refresh_saved_records_when_not_overwriting_unsaved_updates
@pirate.reload
- record = @pirate.class.reflect_on_association(@association_name).klass.new(name: 'Grace OMalley')
+ record = @pirate.class.reflect_on_association(@association_name).klass.new(name: "Grace OMalley")
@pirate.send(@association_name) << record
record.save!
- @pirate.send(@association_name).last.update!(name: 'Polly')
- assert_equal 'Polly', @pirate.send(@association_name).send(:load_target).last.name
+ @pirate.send(@association_name).last.update!(name: "Polly")
+ assert_equal "Polly", @pirate.send(@association_name).load_target.last.name
end
def test_should_not_remove_scheduled_destroys_when_loading_association
@pirate.reload
- @pirate.send(association_setter, [{ :id => @child_1.id, :_destroy => '1' }])
- assert @pirate.send(@association_name).send(:load_target).find { |r| r.id == @child_1.id }.marked_for_destruction?
+ @pirate.send(association_setter, [{ id: @child_1.id, _destroy: "1" }])
+ assert_predicate @pirate.send(@association_name).load_target.find { |r| r.id == @child_1.id }, :marked_for_destruction?
end
def test_should_take_a_hash_with_composite_id_keys_and_assign_the_attributes_to_the_associated_models
- @child_1.stubs(:id).returns('ABC1X')
- @child_2.stubs(:id).returns('ABC2X')
-
- @pirate.attributes = {
- association_getter => [
- { :id => @child_1.id, :name => 'Grace OMalley' },
- { :id => @child_2.id, :name => 'Privateers Greed' }
- ]
- }
-
- assert_equal ['Grace OMalley', 'Privateers Greed'], [@child_1.name, @child_2.name]
+ @child_1.stub(:id, "ABC1X") do
+ @child_2.stub(:id, "ABC2X") do
+ @pirate.attributes = {
+ association_getter => [
+ { id: @child_1.id, name: "Grace OMalley" },
+ { id: @child_2.id, name: "Privateers Greed" }
+ ]
+ }
+
+ assert_equal ["Grace OMalley", "Privateers Greed"], [@child_1.name, @child_2.name]
+ end
+ end
end
def test_should_raise_RecordNotFound_if_an_id_is_given_but_doesnt_return_a_record
exception = assert_raise ActiveRecord::RecordNotFound do
- @pirate.attributes = { association_getter => [{ :id => 1234567890 }] }
+ @pirate.attributes = { association_getter => [{ id: 1234567890 }] }
end
assert_equal "Couldn't find #{@child_1.class.name} with ID=1234567890 for Pirate with ID=#{@pirate.id}", exception.message
end
def test_should_raise_RecordNotFound_if_an_id_belonging_to_a_different_record_is_given
- other_pirate = Pirate.create! catchphrase: 'Ahoy!'
- other_child = other_pirate.send(@association_name).create! name: 'Buccaneers Servant'
+ other_pirate = Pirate.create! catchphrase: "Ahoy!"
+ other_child = other_pirate.send(@association_name).create! name: "Buccaneers Servant"
exception = assert_raise ActiveRecord::RecordNotFound do
@pirate.attributes = { association_getter => [{ id: other_child.id }] }
@@ -671,19 +713,19 @@ module NestedAttributesOnACollectionAssociationTests
def test_should_automatically_build_new_associated_models_for_each_entry_in_a_hash_where_the_id_is_missing
@pirate.send(@association_name).destroy_all
@pirate.reload.attributes = {
- association_getter => { 'foo' => { :name => 'Grace OMalley' }, 'bar' => { :name => 'Privateers Greed' }}
+ association_getter => { "foo" => { name: "Grace OMalley" }, "bar" => { name: "Privateers Greed" } }
}
- assert !@pirate.send(@association_name).first.persisted?
- assert_equal 'Grace OMalley', @pirate.send(@association_name).first.name
+ assert_not_predicate @pirate.send(@association_name).first, :persisted?
+ assert_equal "Grace OMalley", @pirate.send(@association_name).first.name
- assert !@pirate.send(@association_name).last.persisted?
- assert_equal 'Privateers Greed', @pirate.send(@association_name).last.name
+ assert_not_predicate @pirate.send(@association_name).last, :persisted?
+ assert_equal "Privateers Greed", @pirate.send(@association_name).last.name
end
def test_should_not_assign_destroy_key_to_a_record
- assert_nothing_raised ActiveRecord::UnknownAttributeError do
- @pirate.send(association_setter, { 'foo' => { '_destroy' => '0' }})
+ assert_nothing_raised do
+ @pirate.send(association_setter, "foo" => { "_destroy" => "0" })
end
end
@@ -691,17 +733,17 @@ module NestedAttributesOnACollectionAssociationTests
@pirate.send(@association_name).destroy_all
@pirate.reload.attributes = {
association_getter => {
- 'foo' => { :name => 'Grace OMalley' },
- 'bar' => { :name => 'Privateers Greed', '_destroy' => '1' }
+ "foo" => { name: "Grace OMalley" },
+ "bar" => { :name => "Privateers Greed", "_destroy" => "1" }
}
}
assert_equal 1, @pirate.send(@association_name).length
- assert_equal 'Grace OMalley', @pirate.send(@association_name).first.name
+ assert_equal "Grace OMalley", @pirate.send(@association_name).first.name
end
def test_should_ignore_new_associated_records_if_a_reject_if_proc_returns_false
- @alternate_params[association_getter]['baz'] = {}
+ @alternate_params[association_getter]["baz"] = {}
assert_no_difference("@pirate.send(@association_name).count") do
@pirate.attributes = @alternate_params
end
@@ -709,65 +751,65 @@ module NestedAttributesOnACollectionAssociationTests
def test_should_sort_the_hash_by_the_keys_before_building_new_associated_models
attributes = {}
- attributes['123726353'] = { :name => 'Grace OMalley' }
- attributes['2'] = { :name => 'Privateers Greed' } # 2 is lower then 123726353
+ attributes["123726353"] = { name: "Grace OMalley" }
+ attributes["2"] = { name: "Privateers Greed" } # 2 is lower then 123726353
@pirate.send(association_setter, attributes)
- assert_equal ['Posideons Killer', 'Killer bandita Dionne', 'Privateers Greed', 'Grace OMalley'].to_set, @pirate.send(@association_name).map(&:name).to_set
+ assert_equal ["Posideons Killer", "Killer bandita Dionne", "Privateers Greed", "Grace OMalley"].to_set, @pirate.send(@association_name).map(&:name).to_set
end
def test_should_raise_an_argument_error_if_something_else_than_a_hash_is_passed
- assert_nothing_raised(ArgumentError) { @pirate.send(association_setter, {}) }
- assert_nothing_raised(ArgumentError) { @pirate.send(association_setter, Hash.new) }
+ assert_nothing_raised { @pirate.send(association_setter, {}) }
+ assert_nothing_raised { @pirate.send(association_setter, Hash.new) }
exception = assert_raise ArgumentError do
@pirate.send(association_setter, "foo")
end
- assert_equal 'Hash or Array expected, got String ("foo")', exception.message
+ assert_equal %{Hash or Array expected for attribute `#{@association_name}`, got String ("foo")}, exception.message
end
def test_should_work_with_update_as_well
- @pirate.update(catchphrase: 'Arr',
- association_getter => { 'foo' => { :id => @child_1.id, :name => 'Grace OMalley' }})
+ @pirate.update(catchphrase: "Arr",
+ association_getter => { "foo" => { id: @child_1.id, name: "Grace OMalley" } })
- assert_equal 'Grace OMalley', @child_1.reload.name
+ assert_equal "Grace OMalley", @child_1.reload.name
end
def test_should_update_existing_records_and_add_new_ones_that_have_no_id
- @alternate_params[association_getter]['baz'] = { name: 'Buccaneers Servant' }
- assert_difference('@pirate.send(@association_name).count', +1) do
+ @alternate_params[association_getter]["baz"] = { name: "Buccaneers Servant" }
+ assert_difference("@pirate.send(@association_name).count", +1) do
@pirate.update @alternate_params
end
- assert_equal ['Grace OMalley', 'Privateers Greed', 'Buccaneers Servant'].to_set, @pirate.reload.send(@association_name).map(&:name).to_set
+ assert_equal ["Grace OMalley", "Privateers Greed", "Buccaneers Servant"].to_set, @pirate.reload.send(@association_name).map(&:name).to_set
end
def test_should_be_possible_to_destroy_a_record
- ['1', 1, 'true', true].each do |true_variable|
- record = @pirate.reload.send(@association_name).create!(:name => 'Grace OMalley')
+ ["1", 1, "true", true].each do |true_variable|
+ record = @pirate.reload.send(@association_name).create!(name: "Grace OMalley")
@pirate.send(association_setter,
- @alternate_params[association_getter].merge('baz' => { :id => record.id, '_destroy' => true_variable })
+ @alternate_params[association_getter].merge("baz" => { :id => record.id, "_destroy" => true_variable })
)
- assert_difference('@pirate.send(@association_name).count', -1) do
+ assert_difference("@pirate.send(@association_name).count", -1) do
@pirate.save
end
end
end
def test_should_not_destroy_the_associated_model_with_a_non_truthy_argument
- [nil, '', '0', 0, 'false', false].each do |false_variable|
- @alternate_params[association_getter]['foo']['_destroy'] = false_variable
- assert_no_difference('@pirate.send(@association_name).count') do
+ [nil, "", "0", 0, "false", false].each do |false_variable|
+ @alternate_params[association_getter]["foo"]["_destroy"] = false_variable
+ assert_no_difference("@pirate.send(@association_name).count") do
@pirate.update(@alternate_params)
end
end
end
def test_should_not_destroy_the_associated_model_until_the_parent_is_saved
- assert_no_difference('@pirate.send(@association_name).count') do
- @pirate.send(association_setter, @alternate_params[association_getter].merge('baz' => { :id => @child_1.id, '_destroy' => true }))
+ assert_no_difference("@pirate.send(@association_name).count") do
+ @pirate.send(association_setter, @alternate_params[association_getter].merge("baz" => { :id => @child_1.id, "_destroy" => true }))
end
- assert_difference('@pirate.send(@association_name).count', -1) { @pirate.save }
+ assert_difference("@pirate.send(@association_name).count", -1) { @pirate.save }
end
def test_should_automatically_enable_autosave_on_the_association
@@ -781,10 +823,10 @@ module NestedAttributesOnACollectionAssociationTests
repair_validations(Interest) do
Interest.validates_presence_of(:man)
- assert_difference 'Man.count' do
- assert_difference 'Interest.count', 2 do
- man = Man.create!(:name => 'John',
- :interests_attributes => [{:topic=>'Cars'}, {:topic=>'Sports'}])
+ assert_difference "Man.count" do
+ assert_difference "Interest.count", 2 do
+ man = Man.create!(name: "John",
+ interests_attributes: [{ topic: "Cars" }, { topic: "Sports" }])
assert_equal 2, man.interests.count
end
end
@@ -792,8 +834,8 @@ module NestedAttributesOnACollectionAssociationTests
end
def test_can_use_symbols_as_object_identifier
- @pirate.attributes = { :parrots_attributes => { :foo => { :name => 'Lovely Day' }, :bar => { :name => 'Blown Away' } } }
- assert_nothing_raised(NoMethodError) { @pirate.save! }
+ @pirate.attributes = { parrots_attributes: { foo: { name: "Lovely Day" }, bar: { name: "Blown Away" } } }
+ assert_nothing_raised { @pirate.save! }
end
def test_numeric_column_changes_from_zero_to_no_empty_string
@@ -801,22 +843,22 @@ module NestedAttributesOnACollectionAssociationTests
repair_validations(Interest) do
Interest.validates_numericality_of(:zine_id)
- man = Man.create(name: 'John')
- interest = man.interests.create(topic: 'bar', zine_id: 0)
+ man = Man.create(name: "John")
+ interest = man.interests.create(topic: "bar", zine_id: 0)
assert interest.save
- assert !man.update({interests_attributes: { id: interest.id, zine_id: 'foo' }})
+ assert_not man.update(interests_attributes: { id: interest.id, zine_id: "foo" })
end
end
private
- def association_setter
- @association_setter ||= "#{@association_name}_attributes=".to_sym
- end
+ def association_setter
+ @association_setter ||= "#{@association_name}_attributes=".to_sym
+ end
- def association_getter
- @association_getter ||= "#{@association_name}_attributes".to_sym
- end
+ def association_getter
+ @association_getter ||= "#{@association_name}_attributes".to_sym
+ end
end
class TestNestedAttributesOnAHasManyAssociation < ActiveRecord::TestCase
@@ -824,16 +866,16 @@ class TestNestedAttributesOnAHasManyAssociation < ActiveRecord::TestCase
@association_type = :has_many
@association_name = :birds
- @pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
- @pirate.birds.create!(:name => 'Posideons Killer')
- @pirate.birds.create!(:name => 'Killer bandita Dionne')
+ @pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?")
+ @pirate.birds.create!(name: "Posideons Killer")
+ @pirate.birds.create!(name: "Killer bandita Dionne")
@child_1, @child_2 = @pirate.birds
@alternate_params = {
- :birds_attributes => {
- 'foo' => { :id => @child_1.id, :name => 'Grace OMalley' },
- 'bar' => { :id => @child_2.id, :name => 'Privateers Greed' }
+ birds_attributes: {
+ "foo" => { id: @child_1.id, name: "Grace OMalley" },
+ "bar" => { id: @child_2.id, name: "Privateers Greed" }
}
}
end
@@ -846,16 +888,16 @@ class TestNestedAttributesOnAHasAndBelongsToManyAssociation < ActiveRecord::Test
@association_type = :has_and_belongs_to_many
@association_name = :parrots
- @pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
- @pirate.parrots.create!(:name => 'Posideons Killer')
- @pirate.parrots.create!(:name => 'Killer bandita Dionne')
+ @pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?")
+ @pirate.parrots.create!(name: "Posideons Killer")
+ @pirate.parrots.create!(name: "Killer bandita Dionne")
@child_1, @child_2 = @pirate.parrots
@alternate_params = {
- :parrots_attributes => {
- 'foo' => { :id => @child_1.id, :name => 'Grace OMalley' },
- 'bar' => { :id => @child_2.id, :name => 'Privateers Greed' }
+ parrots_attributes: {
+ "foo" => { id: @child_1.id, name: "Grace OMalley" },
+ "bar" => { id: @child_2.id, name: "Privateers Greed" }
}
}
end
@@ -865,33 +907,33 @@ end
module NestedAttributesLimitTests
def teardown
- Pirate.accepts_nested_attributes_for :parrots, :allow_destroy => true, :reject_if => proc(&:empty?)
+ Pirate.accepts_nested_attributes_for :parrots, allow_destroy: true, reject_if: proc(&:empty?)
end
def test_limit_with_less_records
- @pirate.attributes = { :parrots_attributes => { 'foo' => { :name => 'Big Big Love' } } }
- assert_difference('Parrot.count') { @pirate.save! }
+ @pirate.attributes = { parrots_attributes: { "foo" => { name: "Big Big Love" } } }
+ assert_difference("Parrot.count") { @pirate.save! }
end
def test_limit_with_number_exact_records
- @pirate.attributes = { :parrots_attributes => { 'foo' => { :name => 'Lovely Day' }, 'bar' => { :name => 'Blown Away' } } }
- assert_difference('Parrot.count', 2) { @pirate.save! }
+ @pirate.attributes = { parrots_attributes: { "foo" => { name: "Lovely Day" }, "bar" => { name: "Blown Away" } } }
+ assert_difference("Parrot.count", 2) { @pirate.save! }
end
def test_limit_with_exceeding_records
assert_raises(ActiveRecord::NestedAttributes::TooManyRecords) do
- @pirate.attributes = { :parrots_attributes => { 'foo' => { :name => 'Lovely Day' },
- 'bar' => { :name => 'Blown Away' },
- 'car' => { :name => 'The Happening' }} }
+ @pirate.attributes = { parrots_attributes: { "foo" => { name: "Lovely Day" },
+ "bar" => { name: "Blown Away" },
+ "car" => { name: "The Happening" } } }
end
end
end
class TestNestedAttributesLimitNumeric < ActiveRecord::TestCase
def setup
- Pirate.accepts_nested_attributes_for :parrots, :limit => 2
+ Pirate.accepts_nested_attributes_for :parrots, limit: 2
- @pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
+ @pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?")
end
include NestedAttributesLimitTests
@@ -899,9 +941,9 @@ end
class TestNestedAttributesLimitSymbol < ActiveRecord::TestCase
def setup
- Pirate.accepts_nested_attributes_for :parrots, :limit => :parrots_limit
+ Pirate.accepts_nested_attributes_for :parrots, limit: :parrots_limit
- @pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?", :parrots_limit => 2)
+ @pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?", parrots_limit: 2)
end
include NestedAttributesLimitTests
@@ -909,9 +951,9 @@ end
class TestNestedAttributesLimitProc < ActiveRecord::TestCase
def setup
- Pirate.accepts_nested_attributes_for :parrots, :limit => proc { 2 }
+ Pirate.accepts_nested_attributes_for :parrots, limit: proc { 2 }
- @pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
+ @pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?")
end
include NestedAttributesLimitTests
@@ -921,45 +963,44 @@ class TestNestedAttributesWithNonStandardPrimaryKeys < ActiveRecord::TestCase
fixtures :owners, :pets
def setup
- Owner.accepts_nested_attributes_for :pets, :allow_destroy => true
+ Owner.accepts_nested_attributes_for :pets, allow_destroy: true
@owner = owners(:ashley)
@pet1, @pet2 = pets(:chew), pets(:mochi)
@params = {
- :pets_attributes => {
- '0' => { :id => @pet1.id, :name => 'Foo' },
- '1' => { :id => @pet2.id, :name => 'Bar' }
+ pets_attributes: {
+ "0" => { id: @pet1.id, name: "Foo" },
+ "1" => { id: @pet2.id, name: "Bar" }
}
}
end
def test_should_update_existing_records_with_non_standard_primary_key
@owner.update(@params)
- assert_equal ['Foo', 'Bar'], @owner.pets.map(&:name)
+ assert_equal ["Foo", "Bar"], @owner.pets.map(&:name)
end
def test_attr_accessor_of_child_should_be_value_provided_during_update
@owner = owners(:ashley)
@pet1 = pets(:chew)
- attributes = {:pets_attributes => { "1"=> { :id => @pet1.id,
- :name => "Foo2",
- :current_user => "John",
- :_destroy=>true }}}
+ attributes = { pets_attributes: { "1" => { id: @pet1.id,
+ name: "Foo2",
+ current_user: "John",
+ _destroy: true } } }
@owner.update(attributes)
- assert_equal 'John', Pet.after_destroy_output
+ assert_equal "John", Pet.after_destroy_output
end
-
end
class TestHasOneAutosaveAssociationWhichItselfHasAutosaveAssociations < ActiveRecord::TestCase
self.use_transactional_tests = false unless supports_savepoints?
def setup
- @pirate = Pirate.create!(:catchphrase => "My baby takes tha mornin' train!")
- @ship = @pirate.create_ship(:name => "The good ship Dollypop")
- @part = @ship.parts.create!(:name => "Mast")
- @trinket = @part.trinkets.create!(:name => "Necklace")
+ @pirate = Pirate.create!(catchphrase: "My baby takes tha mornin' train!")
+ @ship = @pirate.create_ship(name: "The good ship Dollypop")
+ @part = @ship.parts.create!(name: "Mast")
+ @trinket = @part.trinkets.create!(name: "Necklace")
end
test "when great-grandchild changed in memory, saving parent should save great-grandchild" do
@@ -969,25 +1010,25 @@ class TestHasOneAutosaveAssociationWhichItselfHasAutosaveAssociations < ActiveRe
end
test "when great-grandchild changed via attributes, saving parent should save great-grandchild" do
- @pirate.attributes = {:ship_attributes => {:id => @ship.id, :parts_attributes => [{:id => @part.id, :trinkets_attributes => [{:id => @trinket.id, :name => "changed"}]}]}}
+ @pirate.attributes = { ship_attributes: { id: @ship.id, parts_attributes: [{ id: @part.id, trinkets_attributes: [{ id: @trinket.id, name: "changed" }] }] } }
@pirate.save
assert_equal "changed", @trinket.reload.name
end
test "when great-grandchild marked_for_destruction via attributes, saving parent should destroy great-grandchild" do
- @pirate.attributes = {:ship_attributes => {:id => @ship.id, :parts_attributes => [{:id => @part.id, :trinkets_attributes => [{:id => @trinket.id, :_destroy => true}]}]}}
- assert_difference('@part.trinkets.count', -1) { @pirate.save }
+ @pirate.attributes = { ship_attributes: { id: @ship.id, parts_attributes: [{ id: @part.id, trinkets_attributes: [{ id: @trinket.id, _destroy: true }] }] } }
+ assert_difference("@part.trinkets.count", -1) { @pirate.save }
end
test "when great-grandchild added via attributes, saving parent should create great-grandchild" do
- @pirate.attributes = {:ship_attributes => {:id => @ship.id, :parts_attributes => [{:id => @part.id, :trinkets_attributes => [{:name => "created"}]}]}}
- assert_difference('@part.trinkets.count', 1) { @pirate.save }
+ @pirate.attributes = { ship_attributes: { id: @ship.id, parts_attributes: [{ id: @part.id, trinkets_attributes: [{ name: "created" }] }] } }
+ assert_difference("@part.trinkets.count", 1) { @pirate.save }
end
test "when extra records exist for associations, validate (which calls nested_records_changed_for_autosave?) should not load them up" do
@trinket.name = "changed"
- Ship.create!(:pirate => @pirate, :name => "The Black Rock")
- ShipPart.create!(:ship => @ship, :name => "Stern")
+ Ship.create!(pirate: @pirate, name: "The Black Rock")
+ ShipPart.create!(ship: @ship, name: "Stern")
assert_no_queries { @pirate.valid? }
end
end
@@ -996,27 +1037,27 @@ class TestHasManyAutosaveAssociationWhichItselfHasAutosaveAssociations < ActiveR
self.use_transactional_tests = false unless supports_savepoints?
def setup
- @ship = Ship.create!(:name => "The good ship Dollypop")
- @part = @ship.parts.create!(:name => "Mast")
- @trinket = @part.trinkets.create!(:name => "Necklace")
+ @ship = Ship.create!(name: "The good ship Dollypop")
+ @part = @ship.parts.create!(name: "Mast")
+ @trinket = @part.trinkets.create!(name: "Necklace")
end
test "if association is not loaded and association record is saved and then in memory record attributes should be saved" do
- @ship.parts_attributes=[{:id => @part.id,:name =>'Deck'}]
+ @ship.parts_attributes = [{ id: @part.id, name: "Deck" }]
assert_equal 1, @ship.association(:parts).target.size
- assert_equal 'Deck', @ship.parts[0].name
+ assert_equal "Deck", @ship.parts[0].name
end
test "if association is not loaded and child doesn't change and I am saving a grandchild then in memory record should be used" do
- @ship.parts_attributes=[{:id => @part.id,:trinkets_attributes =>[{:id => @trinket.id, :name => 'Ruby'}]}]
+ @ship.parts_attributes = [{ id: @part.id, trinkets_attributes: [{ id: @trinket.id, name: "Ruby" }] }]
assert_equal 1, @ship.association(:parts).target.size
- assert_equal 'Mast', @ship.parts[0].name
+ assert_equal "Mast", @ship.parts[0].name
assert_no_difference("@ship.parts[0].association(:trinkets).target.size") do
@ship.parts[0].association(:trinkets).target.size
end
- assert_equal 'Ruby', @ship.parts[0].trinkets[0].name
+ assert_equal "Ruby", @ship.parts[0].trinkets[0].name
@ship.save
- assert_equal 'Ruby', @ship.parts[0].trinkets[0].name
+ assert_equal "Ruby", @ship.parts[0].trinkets[0].name
end
test "when grandchild changed in memory, saving parent should save grandchild" do
@@ -1026,25 +1067,25 @@ class TestHasManyAutosaveAssociationWhichItselfHasAutosaveAssociations < ActiveR
end
test "when grandchild changed via attributes, saving parent should save grandchild" do
- @ship.attributes = {:parts_attributes => [{:id => @part.id, :trinkets_attributes => [{:id => @trinket.id, :name => "changed"}]}]}
+ @ship.attributes = { parts_attributes: [{ id: @part.id, trinkets_attributes: [{ id: @trinket.id, name: "changed" }] }] }
@ship.save
assert_equal "changed", @trinket.reload.name
end
test "when grandchild marked_for_destruction via attributes, saving parent should destroy grandchild" do
- @ship.attributes = {:parts_attributes => [{:id => @part.id, :trinkets_attributes => [{:id => @trinket.id, :_destroy => true}]}]}
- assert_difference('@part.trinkets.count', -1) { @ship.save }
+ @ship.attributes = { parts_attributes: [{ id: @part.id, trinkets_attributes: [{ id: @trinket.id, _destroy: true }] }] }
+ assert_difference("@part.trinkets.count", -1) { @ship.save }
end
test "when grandchild added via attributes, saving parent should create grandchild" do
- @ship.attributes = {:parts_attributes => [{:id => @part.id, :trinkets_attributes => [{:name => "created"}]}]}
- assert_difference('@part.trinkets.count', 1) { @ship.save }
+ @ship.attributes = { parts_attributes: [{ id: @part.id, trinkets_attributes: [{ name: "created" }] }] }
+ assert_difference("@part.trinkets.count", 1) { @ship.save }
end
test "when extra records exist for associations, validate (which calls nested_records_changed_for_autosave?) should not load them up" do
@trinket.name = "changed"
- Ship.create!(:name => "The Black Rock")
- ShipPart.create!(:ship => @ship, :name => "Stern")
+ Ship.create!(name: "The Black Rock")
+ ShipPart.create!(ship: @ship, name: "Stern")
assert_no_queries { @ship.valid? }
end
@@ -1061,7 +1102,19 @@ class TestHasManyAutosaveAssociationWhichItselfHasAutosaveAssociations < ActiveR
test "nested singular associations are validated" do
part = ShipPart.new(name: "Stern", ship_attributes: { name: nil })
- assert_not part.valid?
+ assert_not_predicate part, :valid?
assert_equal ["Ship name can't be blank"], part.errors.full_messages
end
end
+
+class TestNestedAttributesWithExtend < ActiveRecord::TestCase
+ setup do
+ Pirate.accepts_nested_attributes_for :treasures
+ end
+
+ def test_extend_affects_nested_attributes
+ pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?")
+ pirate.treasures_attributes = [{ id: nil }]
+ assert_equal "from extension", pirate.treasures[0].name
+ end
+end
diff --git a/activerecord/test/cases/nested_attributes_with_callbacks_test.rb b/activerecord/test/cases/nested_attributes_with_callbacks_test.rb
index 43a69928b6..1d26057fdc 100644
--- a/activerecord/test/cases/nested_attributes_with_callbacks_test.rb
+++ b/activerecord/test/cases/nested_attributes_with_callbacks_test.rb
@@ -1,27 +1,29 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "models/pirate"
require "models/bird"
class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase
Pirate.has_many(:birds_with_add_load,
- :class_name => "Bird",
- :before_add => proc { |p,b|
+ class_name: "Bird",
+ before_add: proc { |p, b|
@@add_callback_called << b
p.birds_with_add_load.to_a
})
Pirate.has_many(:birds_with_add,
- :class_name => "Bird",
- :before_add => proc { |p,b| @@add_callback_called << b })
+ class_name: "Bird",
+ before_add: proc { |p, b| @@add_callback_called << b })
Pirate.accepts_nested_attributes_for(:birds_with_add_load,
:birds_with_add,
- :allow_destroy => true)
+ allow_destroy: true)
def setup
@@add_callback_called = []
@pirate = Pirate.new.tap do |pirate|
pirate.catchphrase = "Don't call me!"
- pirate.birds_attributes = [{:name => 'Bird1'},{:name => 'Bird2'}]
+ pirate.birds_attributes = [{ name: "Bird1" }, { name: "Bird2" }]
pirate.save!
end
@birds = @pirate.birds.to_a
@@ -37,7 +39,7 @@ class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase
def existing_birds_attributes
@birds.map do |bird|
- bird.attributes.slice("id","name")
+ bird.attributes.slice("id", "name")
end
end
@@ -46,22 +48,22 @@ class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase
end
def new_bird_attributes
- [{'name' => "New Bird"}]
+ [{ "name" => "New Bird" }]
end
def destroy_bird_attributes
- [{'id' => bird_to_destroy.id.to_s, "_destroy" => true}]
+ [{ "id" => bird_to_destroy.id.to_s, "_destroy" => true }]
end
def update_new_and_destroy_bird_attributes
- [{'id' => @birds[0].id.to_s, 'name' => 'New Name'},
- {'name' => "New Bird"},
- {'id' => bird_to_destroy.id.to_s, "_destroy" => true}]
+ [{ "id" => @birds[0].id.to_s, "name" => "New Name" },
+ { "name" => "New Bird" },
+ { "id" => bird_to_destroy.id.to_s, "_destroy" => true }]
end
# Characterizing when :before_add callback is called
test ":before_add called for new bird when not loaded" do
- assert_not @pirate.birds_with_add.loaded?
+ assert_not_predicate @pirate.birds_with_add, :loaded?
@pirate.birds_with_add_attributes = new_bird_attributes
assert_new_bird_with_callback_called
end
@@ -78,7 +80,7 @@ class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase
end
test ":before_add not called for identical assignment when not loaded" do
- assert_not @pirate.birds_with_add.loaded?
+ assert_not_predicate @pirate.birds_with_add, :loaded?
@pirate.birds_with_add_attributes = existing_birds_attributes
assert_callbacks_not_called
end
@@ -90,7 +92,7 @@ class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase
end
test ":before_add not called for destroy assignment when not loaded" do
- assert_not @pirate.birds_with_add.loaded?
+ assert_not_predicate @pirate.birds_with_add, :loaded?
@pirate.birds_with_add_attributes = destroy_bird_attributes
assert_callbacks_not_called
end
@@ -109,7 +111,7 @@ class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase
# Ensuring that the records in the association target are updated,
# whether the association is loaded before or not
test "Assignment updates records in target when not loaded" do
- assert_not @pirate.birds_with_add.loaded?
+ assert_not_predicate @pirate.birds_with_add, :loaded?
@pirate.birds_with_add_attributes = update_new_and_destroy_bird_attributes
assert_assignment_affects_records_in_target(:birds_with_add)
end
@@ -120,14 +122,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?
+ assert_not_predicate @pirate.birds_with_add_load, :loaded?
@pirate.birds_with_add_load_attributes = update_new_and_destroy_bird_attributes
assert_assignment_affects_records_in_target(:birds_with_add_load)
end
- 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
@@ -136,9 +138,9 @@ class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase
def assert_assignment_affects_records_in_target(association_name)
association = @pirate.send(association_name)
- assert association.detect {|b| b == bird_to_update }.name_changed?,
- 'Update record not updated'
- assert association.detect {|b| b == bird_to_destroy }.marked_for_destruction?,
- 'Destroy record not marked for destruction'
+ assert association.detect { |b| b == bird_to_update }.name_changed?,
+ "Update record not updated"
+ assert association.detect { |b| b == bird_to_destroy }.marked_for_destruction?,
+ "Destroy record not marked for destruction"
end
end
diff --git a/activerecord/test/cases/null_relation_test.rb b/activerecord/test/cases/null_relation_test.rb
new file mode 100644
index 0000000000..ee96ea1af6
--- /dev/null
+++ b/activerecord/test/cases/null_relation_test.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/developer"
+require "models/comment"
+require "models/post"
+require "models/topic"
+
+class NullRelationTest < ActiveRecord::TestCase
+ fixtures :posts, :comments
+
+ def test_none
+ assert_no_queries do
+ assert_equal [], Developer.none
+ assert_equal [], Developer.all.none
+ end
+ end
+
+ def test_none_chainable
+ Developer.send(:load_schema)
+ assert_no_queries do
+ assert_equal [], Developer.none.where(name: "David")
+ end
+ end
+
+ def test_none_chainable_to_existing_scope_extension_method
+ assert_no_queries do
+ assert_equal 1, Topic.anonymous_extension.none.one
+ end
+ end
+
+ def test_none_chained_to_methods_firing_queries_straight_to_db
+ assert_no_queries do
+ assert_equal [], Developer.none.pluck(:id, :name)
+ assert_equal 0, Developer.none.delete_all
+ assert_equal 0, Developer.none.update_all(name: "David")
+ assert_equal 0, Developer.none.delete(1)
+ assert_equal false, Developer.none.exists?(1)
+ end
+ end
+
+ def test_null_relation_content_size_methods
+ assert_no_queries do
+ assert_equal 0, Developer.none.size
+ assert_equal 0, Developer.none.count
+ assert_equal true, Developer.none.empty?
+ assert_equal true, Developer.none.none?
+ assert_equal false, Developer.none.any?
+ assert_equal false, Developer.none.one?
+ assert_equal false, Developer.none.many?
+ end
+ end
+
+ def test_null_relation_metadata_methods
+ assert_equal "", Developer.none.to_sql
+ assert_equal({}, Developer.none.where_values_hash)
+ end
+
+ def test_null_relation_where_values_hash
+ assert_equal({ "salary" => 100_000 }, Developer.none.where(salary: 100_000).where_values_hash)
+ end
+
+ [:count, :sum].each do |method|
+ define_method "test_null_relation_#{method}" do
+ assert_no_queries do
+ assert_equal 0, Comment.none.public_send(method, :id)
+ assert_equal Hash.new, Comment.none.group(:post_id).public_send(method, :id)
+ end
+ end
+ end
+
+ [:average, :minimum, :maximum].each do |method|
+ define_method "test_null_relation_#{method}" do
+ assert_no_queries do
+ assert_nil Comment.none.public_send(method, :id)
+ assert_equal Hash.new, Comment.none.group(:post_id).public_send(method, :id)
+ end
+ end
+ end
+
+ def test_null_relation_in_where_condition
+ assert_operator Comment.count, :>, 0 # precondition, make sure there are comments.
+ assert_equal 0, Comment.where(post_id: Post.none).count
+ end
+end
diff --git a/activerecord/test/cases/numeric_data_test.rb b/activerecord/test/cases/numeric_data_test.rb
new file mode 100644
index 0000000000..079e664ee4
--- /dev/null
+++ b/activerecord/test/cases/numeric_data_test.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/numeric_data"
+
+class NumericDataTest < ActiveRecord::TestCase
+ def test_big_decimal_conditions
+ m = NumericData.new(
+ bank_balance: 1586.43,
+ big_bank_balance: BigDecimal("1000234000567.95"),
+ world_population: 6000000000,
+ my_house_population: 3
+ )
+ assert m.save
+ assert_equal 0, NumericData.where("bank_balance > ?", 2000.0).count
+ end
+
+ def test_numeric_fields
+ m = NumericData.new(
+ bank_balance: 1586.43,
+ big_bank_balance: BigDecimal("1000234000567.95"),
+ world_population: 2**62,
+ my_house_population: 3
+ )
+ assert m.save
+
+ m1 = NumericData.find_by(
+ bank_balance: 1586.43,
+ big_bank_balance: BigDecimal("1000234000567.95")
+ )
+
+ assert_kind_of Integer, m1.world_population
+ assert_equal 2**62, m1.world_population
+
+ assert_kind_of Integer, m1.my_house_population
+ assert_equal 3, m1.my_house_population
+
+ assert_kind_of BigDecimal, m1.bank_balance
+ assert_equal BigDecimal("1586.43"), m1.bank_balance
+
+ assert_kind_of BigDecimal, m1.big_bank_balance
+ assert_equal BigDecimal("1000234000567.95"), m1.big_bank_balance
+ end
+
+ def test_numeric_fields_with_scale
+ m = NumericData.new(
+ bank_balance: 1586.43122334,
+ big_bank_balance: BigDecimal("234000567.952344"),
+ world_population: 2**62,
+ my_house_population: 3
+ )
+ assert m.save
+
+ m1 = NumericData.find_by(
+ bank_balance: 1586.43122334,
+ big_bank_balance: BigDecimal("234000567.952344")
+ )
+
+ assert_kind_of Integer, m1.world_population
+ assert_equal 2**62, m1.world_population
+
+ assert_kind_of Integer, m1.my_house_population
+ assert_equal 3, m1.my_house_population
+
+ assert_kind_of BigDecimal, m1.bank_balance
+ assert_equal BigDecimal("1586.43"), m1.bank_balance
+
+ assert_kind_of BigDecimal, m1.big_bank_balance
+ assert_equal BigDecimal("234000567.95"), m1.big_bank_balance
+ end
+
+ if current_adapter?(:PostgreSQLAdapter)
+ def test_numeric_fields_with_nan
+ m = NumericData.new(
+ bank_balance: BigDecimal("NaN"),
+ big_bank_balance: BigDecimal("NaN"),
+ world_population: 2**62,
+ my_house_population: 3
+ )
+ assert_predicate m.bank_balance, :nan?
+ assert_predicate m.big_bank_balance, :nan?
+ assert m.save
+
+ m1 = NumericData.find_by(
+ bank_balance: BigDecimal("NaN"),
+ big_bank_balance: BigDecimal("NaN")
+ )
+
+ assert_predicate m1.bank_balance, :nan?
+ assert_predicate m1.big_bank_balance, :nan?
+ end
+ end
+end
diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb
index 42e7507631..4830ff2b5f 100644
--- a/activerecord/test/cases/persistence_test.rb
+++ b/activerecord/test/cases/persistence_test.rb
@@ -1,98 +1,73 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/aircraft'
-require 'models/post'
-require 'models/comment'
-require 'models/author'
-require 'models/topic'
-require 'models/reply'
-require 'models/category'
-require 'models/company'
-require 'models/developer'
-require 'models/computer'
-require 'models/project'
-require 'models/minimalistic'
-require 'models/warehouse_thing'
-require 'models/parrot'
-require 'models/minivan'
-require 'models/owner'
-require 'models/person'
-require 'models/pet'
-require 'models/toy'
-require 'rexml/document'
+require "models/aircraft"
+require "models/post"
+require "models/comment"
+require "models/author"
+require "models/topic"
+require "models/reply"
+require "models/category"
+require "models/company"
+require "models/developer"
+require "models/computer"
+require "models/project"
+require "models/minimalistic"
+require "models/parrot"
+require "models/minivan"
+require "models/person"
+require "models/ship"
+require "models/admin"
+require "models/admin/user"
class PersistenceTest < ActiveRecord::TestCase
- fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :author_addresses, :categorizations, :categories, :posts, :minivans, :pets, :toys
-
- # Oracle UPDATE does not support ORDER BY
- unless current_adapter?(:OracleAdapter)
- def test_update_all_ignores_order_without_limit_from_association
- author = authors(:david)
- assert_nothing_raised do
- assert_equal author.posts_with_comments_and_categories.length, author.posts_with_comments_and_categories.update_all([ "body = ?", "bulk update!" ])
- end
- end
-
- def test_update_all_doesnt_ignore_order
- assert_equal authors(:david).id + 1, authors(:mary).id # make sure there is going to be a duplicate PK error
- test_update_with_order_succeeds = lambda do |order|
- begin
- Author.order(order).update_all('id = id + 1')
- rescue ActiveRecord::ActiveRecordError
- false
- end
- end
-
- if test_update_with_order_succeeds.call('id DESC')
- assert !test_update_with_order_succeeds.call('id ASC') # test that this wasn't a fluke and using an incorrect order results in an exception
- else
- # test that we're failing because the current Arel's engine doesn't support UPDATE ORDER BY queries is using subselects instead
- assert_sql(/\AUPDATE .+ \(SELECT .* ORDER BY id DESC\)\Z/i) do
- test_update_with_order_succeeds.call('id DESC')
- end
- end
- end
-
- def test_update_all_with_order_and_limit_updates_subset_only
- author = authors(:david)
- assert_nothing_raised do
- assert_equal 1, author.posts_sorted_by_id_limited.size
- assert_equal 2, author.posts_sorted_by_id_limited.limit(2).to_a.size
- assert_equal 1, author.posts_sorted_by_id_limited.update_all([ "body = ?", "bulk update!" ])
- assert_equal "bulk update!", posts(:welcome).body
- assert_not_equal "bulk update!", posts(:thinking).body
- end
- end
- end
+ fixtures :topics, :companies, :developers, :accounts, :minimalistics, :authors, :author_addresses, :posts, :minivans
def test_update_many
topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" } }
updated = Topic.update(topic_data.keys, topic_data.values)
- assert_equal 2, updated.size
+ assert_equal [1, 2], updated.map(&:id)
assert_equal "1 updated", Topic.find(1).content
assert_equal "2 updated", Topic.find(2).content
end
- def test_delete_all
- assert Topic.count > 0
+ def test_update_many_with_duplicated_ids
+ updated = Topic.update([1, 1, 2], [
+ { "content" => "1 duplicated" }, { "content" => "1 updated" }, { "content" => "2 updated" }
+ ])
- assert_equal Topic.count, Topic.delete_all
+ assert_equal [1, 1, 2], updated.map(&:id)
+ assert_equal "1 updated", Topic.find(1).content
+ assert_equal "2 updated", Topic.find(2).content
+ end
+
+ def test_update_many_with_invalid_id
+ topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" }, 99999 => {} }
+
+ assert_raise(ActiveRecord::RecordNotFound) do
+ Topic.update(topic_data.keys, topic_data.values)
+ end
+
+ assert_not_equal "1 updated", Topic.find(1).content
+ assert_not_equal "2 updated", Topic.find(2).content
end
- def test_delete_all_with_joins_and_where_part_is_hash
- where_args = {:toys => {:name => 'Bone'}}
- count = Pet.joins(:toys).where(where_args).count
+ def test_class_level_update_is_affected_by_scoping
+ topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" } }
- assert_equal count, 1
- assert_equal count, Pet.joins(:toys).where(where_args).delete_all
+ assert_raise(ActiveRecord::RecordNotFound) do
+ Topic.where("1=0").scoping { Topic.update(topic_data.keys, topic_data.values) }
+ end
+
+ assert_not_equal "1 updated", Topic.find(1).content
+ assert_not_equal "2 updated", Topic.find(2).content
end
- def test_delete_all_with_joins_and_where_part_is_not_hash
- where_args = ['toys.name = ?', 'Bone']
- count = Pet.joins(:toys).where(where_args).count
+ def test_delete_all
+ assert Topic.count > 0
- assert_equal count, 1
- assert_equal count, Pet.joins(:toys).where(where_args).delete_all
+ assert_equal Topic.count, Topic.delete_all
end
def test_increment_attribute
@@ -119,48 +94,119 @@ class PersistenceTest < ActiveRecord::TestCase
assert_equal 59, accounts(:signals37, :reload).credit_limit
end
- def test_destroy_all
- conditions = "author_name = 'Mary'"
- topics_by_mary = Topic.all.merge!(:where => conditions, :order => 'id').to_a
- assert ! topics_by_mary.empty?
+ def test_increment_updates_counter_in_db_using_offset
+ a1 = accounts(:signals37)
+ initial_credit = a1.credit_limit
+ a2 = Account.find(accounts(:signals37).id)
+ a1.increment!(:credit_limit)
+ a2.increment!(:credit_limit)
+ assert_equal initial_credit + 2, a1.reload.credit_limit
+ end
+
+ def test_increment_with_touch_updates_timestamps
+ topic = topics(:first)
+ assert_equal 1, topic.replies_count
+ previously_updated_at = topic.updated_at
+ travel(1.second) do
+ topic.increment!(:replies_count, touch: true)
+ end
+ assert_equal 2, topic.reload.replies_count
+ assert_operator previously_updated_at, :<, topic.updated_at
+ end
- assert_difference('Topic.count', -topics_by_mary.size) do
- destroyed = Topic.destroy_all(conditions).sort_by(&:id)
- assert_equal topics_by_mary, destroyed
- assert destroyed.all?(&:frozen?), "destroyed topics should be frozen"
+ def test_increment_with_touch_an_attribute_updates_timestamps
+ topic = topics(:first)
+ assert_equal 1, topic.replies_count
+ previously_updated_at = topic.updated_at
+ previously_written_on = topic.written_on
+ travel(1.second) do
+ topic.increment!(:replies_count, touch: :written_on)
end
+ assert_equal 2, topic.reload.replies_count
+ assert_operator previously_updated_at, :<, topic.updated_at
+ assert_operator previously_written_on, :<, topic.written_on
+ end
+
+ def test_increment_with_no_arg
+ topic = topics(:first)
+ assert_raises(ArgumentError) { topic.increment! }
end
def test_destroy_many
- clients = Client.all.merge!(:order => 'id').find([2, 3])
+ clients = Client.find([2, 3])
- assert_difference('Client.count', -2) do
- destroyed = Client.destroy([2, 3]).sort_by(&:id)
+ assert_difference("Client.count", -2) do
+ destroyed = Client.destroy([2, 3])
assert_equal clients, destroyed
assert destroyed.all?(&:frozen?), "destroyed clients should be frozen"
end
end
+ def test_destroy_many_with_invalid_id
+ clients = Client.find([2, 3])
+
+ assert_raise(ActiveRecord::RecordNotFound) do
+ Client.destroy([2, 3, 99999])
+ end
+
+ assert_equal clients, Client.find([2, 3])
+ end
+
def test_becomes
assert_kind_of Reply, topics(:first).becomes(Reply)
assert_equal "The First Topic", topics(:first).becomes(Reply).title
end
+ def test_becomes_after_reload_schema_from_cache
+ Reply.define_attribute_methods
+ Reply.serialize(:content) # invoke reload_schema_from_cache
+ assert_kind_of Reply, topics(:first).becomes(Reply)
+ assert_equal "The First Topic", topics(:first).becomes(Reply).title
+ end
+
def test_becomes_includes_errors
- company = Company.new(:name => nil)
- assert !company.valid?
+ company = Company.new(name: nil)
+ assert_not_predicate company, :valid?
original_errors = company.errors
client = company.becomes(Client)
- assert_equal original_errors, client.errors
+ assert_equal original_errors.keys, client.errors.keys
+ end
+
+ def test_becomes_errors_base
+ child_class = Class.new(Admin::User) do
+ store_accessor :settings, :foo
+
+ def self.name; "Admin::ChildUser"; end
+ end
+
+ admin = Admin::User.new
+ admin.errors.add :token, :invalid
+ child = admin.becomes(child_class)
+
+ assert_equal [:token], child.errors.keys
+ assert_nothing_raised do
+ child.errors.add :foo, :invalid
+ end
end
- def test_dupd_becomes_persists_changes_from_the_original
+ def test_duped_becomes_persists_changes_from_the_original
original = topics(:first)
copy = original.dup.becomes(Reply)
copy.save!
assert_equal "The First Topic", Topic.find(copy.id).title
end
+ def test_becomes_wont_break_mutation_tracking
+ topic = topics(:first)
+ reply = topic.becomes(Reply)
+
+ assert_equal 1, topic.id_in_database
+ assert_empty topic.attributes_in_database
+
+ assert_equal 1, reply.id_in_database
+ assert_empty reply.attributes_in_database
+ end
+
def test_becomes_includes_changed_attributes
company = Company.new(name: "37signals")
client = company.becomes(Client)
@@ -193,6 +239,30 @@ class PersistenceTest < ActiveRecord::TestCase
assert_equal 41, accounts(:signals37, :reload).credit_limit
end
+ def test_decrement_with_touch_updates_timestamps
+ topic = topics(:first)
+ assert_equal 1, topic.replies_count
+ previously_updated_at = topic.updated_at
+ travel(1.second) do
+ topic.decrement!(:replies_count, touch: true)
+ end
+ assert_equal 0, topic.reload.replies_count
+ assert_operator previously_updated_at, :<, topic.updated_at
+ end
+
+ def test_decrement_with_touch_an_attribute_updates_timestamps
+ topic = topics(:first)
+ assert_equal 1, topic.replies_count
+ previously_updated_at = topic.updated_at
+ previously_written_on = topic.written_on
+ travel(1.second) do
+ topic.decrement!(:replies_count, touch: :written_on)
+ end
+ assert_equal 0, topic.reload.replies_count
+ assert_operator previously_updated_at, :<, topic.updated_at
+ assert_operator previously_written_on, :<, topic.written_on
+ end
+
def test_create
topic = Topic.new
topic.title = "New Topic"
@@ -202,7 +272,7 @@ class PersistenceTest < ActiveRecord::TestCase
end
def test_save!
- topic = Topic.new(:title => "New Topic")
+ topic = Topic.new(title: "New Topic")
assert topic.save!
reply = WrongReply.new
@@ -232,7 +302,7 @@ class PersistenceTest < ActiveRecord::TestCase
end
def test_save_for_record_with_only_primary_key_that_is_provided
- assert_nothing_raised { Minimalistic.create!(:id => 2) }
+ assert_nothing_raised { Minimalistic.create!(id: 2) }
end
def test_save_with_duping_of_destroyed_object
@@ -240,8 +310,8 @@ class PersistenceTest < ActiveRecord::TestCase
developer.destroy
new_developer = developer.dup
new_developer.save
- assert new_developer.persisted?
- assert_not new_developer.destroyed?
+ assert_predicate new_developer, :persisted?
+ assert_not_predicate new_developer, :destroyed?
end
def test_create_many
@@ -252,12 +322,13 @@ class PersistenceTest < ActiveRecord::TestCase
def test_create_columns_not_equal_attributes
topic = Topic.instantiate(
- 'attributes' => {
- 'title' => 'Another New Topic',
- 'does_not_exist' => 'test'
- }
+ "title" => "Another New Topic",
+ "does_not_exist" => "test"
)
+ topic = topic.dup # reset @new_record
assert_nothing_raised { topic.save }
+ assert_predicate topic, :persisted?
+ assert_equal "Another New Topic", topic.reload.title
end
def test_create_through_factory_with_block
@@ -301,9 +372,11 @@ class PersistenceTest < ActiveRecord::TestCase
topic.title = "Still another topic"
topic.save
- topic_reloaded = Topic.instantiate(topic.attributes.merge('does_not_exist' => 'test'))
- topic_reloaded.title = 'A New Topic'
+ topic_reloaded = Topic.instantiate(topic.attributes.merge("does_not_exist" => "test"))
+ topic_reloaded.title = "A New Topic"
assert_nothing_raised { topic_reloaded.save }
+ assert_predicate topic_reloaded, :persisted?
+ assert_equal "A New Topic", topic_reloaded.reload.title
end
def test_update_for_record_with_only_primary_key
@@ -340,9 +413,25 @@ class PersistenceTest < ActiveRecord::TestCase
assert_instance_of Reply, Reply.find(reply.id)
end
+ def test_becomes_default_sti_subclass
+ original_type = Topic.columns_hash["type"].default
+ ActiveRecord::Base.connection.change_column_default :topics, :type, "Reply"
+ Topic.reset_column_information
+
+ reply = topics(:second)
+ assert_instance_of Reply, reply
+
+ topic = reply.becomes(Topic)
+ assert_instance_of Topic, topic
+
+ ensure
+ ActiveRecord::Base.connection.change_column_default :topics, :type, original_type
+ Topic.reset_column_information
+ end
+
def test_update_after_create
klass = Class.new(Topic) do
- def self.name; 'Topic'; end
+ def self.name; "Topic"; end
after_create do
update_attribute("author_name", "David")
end
@@ -357,25 +446,23 @@ class PersistenceTest < ActiveRecord::TestCase
end
def test_update_attribute_does_not_run_sql_if_attribute_is_not_changed
- klass = Class.new(Topic) do
- def self.name; 'Topic'; end
- end
- topic = klass.create(title: 'Another New Topic')
- assert_queries(0) do
- topic.update_attribute(:title, 'Another New Topic')
+ topic = Topic.create(title: "Another New Topic")
+ assert_no_queries do
+ assert topic.update_attribute(:title, "Another New Topic")
end
end
def test_update_does_not_run_sql_if_record_has_not_changed
- topic = Topic.create(title: 'Another New Topic')
- assert_queries(0) { topic.update(title: 'Another New Topic') }
- assert_queries(0) { topic.update_attributes(title: 'Another New Topic') }
+ topic = Topic.create(title: "Another New Topic")
+ assert_no_queries do
+ assert topic.update(title: "Another New Topic")
+ end
end
def test_delete
topic = Topic.find(1)
- assert_equal topic, topic.delete, 'topic.delete did not return self'
- assert topic.frozen?, 'topic not frozen after delete'
+ assert_equal topic, topic.delete, "topic.delete did not return self"
+ assert topic.frozen?, "topic not frozen after delete"
assert_raise(ActiveRecord::RecordNotFound) { Topic.find(topic.id) }
end
@@ -384,103 +471,138 @@ class PersistenceTest < ActiveRecord::TestCase
assert_not_nil Topic.find(2)
end
+ def test_delete_isnt_affected_by_scoping
+ topic = Topic.find(1)
+ assert_difference("Topic.count", -1) do
+ Topic.where("1=0").scoping { topic.delete }
+ end
+ end
+
def test_destroy
topic = Topic.find(1)
- assert_equal topic, topic.destroy, 'topic.destroy did not return self'
- assert topic.frozen?, 'topic not frozen after destroy'
+ assert_equal topic, topic.destroy, "topic.destroy did not return self"
+ assert topic.frozen?, "topic not frozen after destroy"
assert_raise(ActiveRecord::RecordNotFound) { Topic.find(topic.id) }
end
def test_destroy!
topic = Topic.find(1)
- assert_equal topic, topic.destroy!, 'topic.destroy! did not return self'
- assert topic.frozen?, 'topic not frozen after destroy!'
+ assert_equal topic, topic.destroy!, "topic.destroy! did not return self"
+ assert topic.frozen?, "topic not frozen after destroy!"
assert_raise(ActiveRecord::RecordNotFound) { Topic.find(topic.id) }
end
- def test_record_not_found_exception
+ def test_find_raises_record_not_found_exception
assert_raise(ActiveRecord::RecordNotFound) { Topic.find(99999) }
end
+ def test_update_raises_record_not_found_exception
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.update(99999, approved: true) }
+ end
+
+ def test_destroy_raises_record_not_found_exception
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.destroy(99999) }
+ end
+
def test_update_all
assert_equal Topic.count, Topic.update_all("content = 'bulk updated!'")
assert_equal "bulk updated!", Topic.find(1).content
assert_equal "bulk updated!", Topic.find(2).content
- assert_equal Topic.count, Topic.update_all(['content = ?', 'bulk updated again!'])
+ assert_equal Topic.count, Topic.update_all(["content = ?", "bulk updated again!"])
assert_equal "bulk updated again!", Topic.find(1).content
assert_equal "bulk updated again!", Topic.find(2).content
- assert_equal Topic.count, Topic.update_all(['content = ?', nil])
+ assert_equal Topic.count, Topic.update_all(["content = ?", nil])
assert_nil Topic.find(1).content
end
def test_update_all_with_hash
assert_not_nil Topic.find(1).last_read
- assert_equal Topic.count, Topic.update_all(:content => 'bulk updated with hash!', :last_read => nil)
+ assert_equal Topic.count, Topic.update_all(content: "bulk updated with hash!", last_read: nil)
assert_equal "bulk updated with hash!", Topic.find(1).content
assert_equal "bulk updated with hash!", Topic.find(2).content
assert_nil Topic.find(1).last_read
assert_nil Topic.find(2).last_read
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
- end
-
def test_delete_new_record
- client = Client.new
+ client = Client.new(name: "37signals")
client.delete
- assert client.frozen?
+ assert_predicate client, :frozen?
+
+ assert_not client.save
+ assert_raise(ActiveRecord::RecordNotSaved) { client.save! }
+
+ assert_predicate client, :frozen?
+ assert_raise(RuntimeError) { client.name = "something else" }
end
def test_delete_record_with_associations
client = Client.find(3)
client.delete
- assert client.frozen?
+ assert_predicate client, :frozen?
assert_kind_of Firm, client.firm
+
+ assert_not client.save
+ assert_raise(ActiveRecord::RecordNotSaved) { client.save! }
+
+ assert_predicate client, :frozen?
assert_raise(RuntimeError) { client.name = "something else" }
end
def test_destroy_new_record
- client = Client.new
+ client = Client.new(name: "37signals")
client.destroy
- assert client.frozen?
+ assert_predicate client, :frozen?
+
+ assert_not client.save
+ assert_raise(ActiveRecord::RecordNotSaved) { client.save! }
+
+ assert_predicate client, :frozen?
+ assert_raise(RuntimeError) { client.name = "something else" }
end
def test_destroy_record_with_associations
client = Client.find(3)
client.destroy
- assert client.frozen?
+ assert_predicate client, :frozen?
assert_kind_of Firm, client.firm
+
+ assert_not client.save
+ assert_raise(ActiveRecord::RecordNotSaved) { client.save! }
+
+ assert_predicate client, :frozen?
assert_raise(RuntimeError) { client.name = "something else" }
end
def test_update_attribute
- assert !Topic.find(1).approved?
+ assert_not_predicate Topic.find(1), :approved?
Topic.find(1).update_attribute("approved", true)
- assert Topic.find(1).approved?
+ assert_predicate Topic.find(1), :approved?
Topic.find(1).update_attribute(:approved, false)
- assert !Topic.find(1).approved?
+ assert_not_predicate Topic.find(1), :approved?
+
+ Topic.find(1).update_attribute(:change_approved_before_save, true)
+ assert_predicate Topic.find(1), :approved?
end
def test_update_attribute_for_readonly_attribute
- minivan = Minivan.find('m1')
- assert_raises(ActiveRecord::ActiveRecordError) { minivan.update_attribute(:color, 'black') }
+ minivan = Minivan.find("m1")
+ assert_raises(ActiveRecord::ActiveRecordError) { minivan.update_attribute(:color, "black") }
end
def test_update_attribute_with_one_updated
t = Topic.first
- t.update_attribute(:title, 'super_title')
- assert_equal 'super_title', t.title
- assert !t.changed?, "topic should not have changed"
- assert !t.title_changed?, "title should not have changed"
- assert_nil t.title_change, 'title change should be nil'
+ t.update_attribute(:title, "super_title")
+ assert_equal "super_title", t.title
+ assert_not t.changed?, "topic should not have changed"
+ assert_not t.title_changed?, "title should not have changed"
+ assert_nil t.title_change, "title change should be nil"
t.reload
- assert_equal 'super_title', t.title
+ assert_equal "super_title", t.title
end
def test_update_attribute_for_updated_at_on
@@ -500,14 +622,14 @@ class PersistenceTest < ActiveRecord::TestCase
def test_update_column
topic = Topic.find(1)
topic.update_column("approved", true)
- assert topic.approved?
+ assert_predicate topic, :approved?
topic.reload
- assert topic.approved?
+ assert_predicate topic, :approved?
topic.update_column(:approved, false)
- assert !topic.approved?
+ assert_not_predicate topic, :approved?
topic.reload
- assert !topic.approved?
+ assert_not_predicate topic, :approved?
end
def test_update_column_should_not_use_setter_method
@@ -540,17 +662,17 @@ class PersistenceTest < ActiveRecord::TestCase
end
def test_update_column_with_model_having_primary_key_other_than_id
- minivan = Minivan.find('m1')
- new_name = 'sebavan'
+ minivan = Minivan.find("m1")
+ new_name = "sebavan"
minivan.update_column(:name, new_name)
assert_equal new_name, minivan.name
end
def test_update_column_for_readonly_attribute
- minivan = Minivan.find('m1')
+ minivan = Minivan.find("m1")
prev_color = minivan.color
- assert_raises(ActiveRecord::ActiveRecordError) { minivan.update_column(:color, 'black') }
+ assert_raises(ActiveRecord::ActiveRecordError) { minivan.update_column(:color, "black") }
assert_equal prev_color, minivan.color
end
@@ -569,35 +691,35 @@ class PersistenceTest < ActiveRecord::TestCase
end
def test_update_column_with_one_changed_and_one_updated
- t = Topic.order('id').limit(1).first
+ t = Topic.order("id").limit(1).first
author_name = t.author_name
- t.author_name = 'John'
- t.update_column(:title, 'super_title')
- assert_equal 'John', t.author_name
- assert_equal 'super_title', t.title
+ t.author_name = "John"
+ t.update_column(:title, "super_title")
+ assert_equal "John", t.author_name
+ assert_equal "super_title", t.title
assert t.changed?, "topic should have changed"
assert t.author_name_changed?, "author_name should have changed"
t.reload
assert_equal author_name, t.author_name
- assert_equal 'super_title', t.title
+ assert_equal "super_title", t.title
end
def test_update_column_with_default_scope
developer = DeveloperCalledDavid.first
- developer.name = 'John'
+ developer.name = "John"
developer.save!
- assert developer.update_column(:name, 'Will'), 'did not update record due to default scope'
+ assert developer.update_column(:name, "Will"), "did not update record due to default scope"
end
def test_update_columns
topic = Topic.find(1)
- topic.update_columns({ "approved" => true, title: "Sebastian Topic" })
- assert topic.approved?
+ topic.update_columns("approved" => true, title: "Sebastian Topic")
+ assert_predicate topic, :approved?
assert_equal "Sebastian Topic", topic.title
topic.reload
- assert topic.approved?
+ assert_predicate topic, :approved?
assert_equal "Sebastian Topic", topic.title
end
@@ -614,35 +736,35 @@ class PersistenceTest < ActiveRecord::TestCase
def test_update_columns_should_raise_exception_if_new_record
topic = Topic.new
- assert_raises(ActiveRecord::ActiveRecordError) { topic.update_columns({ approved: false }) }
+ assert_raises(ActiveRecord::ActiveRecordError) { topic.update_columns(approved: false) }
end
def test_update_columns_should_not_leave_the_object_dirty
topic = Topic.find(1)
- topic.update({ "content" => "--- Have a nice day\n...\n", :author_name => "Jose" })
+ topic.update("content" => "--- Have a nice day\n...\n", :author_name => "Jose")
topic.reload
- topic.update_columns({ content: "--- You too\n...\n", "author_name" => "Sebastian" })
+ topic.update_columns(content: "--- You too\n...\n", "author_name" => "Sebastian")
assert_equal [], topic.changed
topic.reload
- topic.update_columns({ content: "--- Have a nice day\n...\n", author_name: "Jose" })
+ topic.update_columns(content: "--- Have a nice day\n...\n", author_name: "Jose")
assert_equal [], topic.changed
end
def test_update_columns_with_model_having_primary_key_other_than_id
- minivan = Minivan.find('m1')
- new_name = 'sebavan'
+ minivan = Minivan.find("m1")
+ new_name = "sebavan"
minivan.update_columns(name: new_name)
assert_equal new_name, minivan.name
end
def test_update_columns_with_one_readonly_attribute
- minivan = Minivan.find('m1')
+ minivan = Minivan.find("m1")
prev_color = minivan.color
prev_name = minivan.name
- assert_raises(ActiveRecord::ActiveRecordError) { minivan.update_columns({ name: "My old minivan", color: 'black' }) }
+ assert_raises(ActiveRecord::ActiveRecordError) { minivan.update_columns(name: "My old minivan", color: "black") }
assert_equal prev_color, minivan.color
assert_equal prev_name, minivan.name
@@ -668,18 +790,18 @@ class PersistenceTest < ActiveRecord::TestCase
end
def test_update_columns_with_one_changed_and_one_updated
- t = Topic.order('id').limit(1).first
+ t = Topic.order("id").limit(1).first
author_name = t.author_name
- t.author_name = 'John'
- t.update_columns(title: 'super_title')
- assert_equal 'John', t.author_name
- assert_equal 'super_title', t.title
+ t.author_name = "John"
+ t.update_columns(title: "super_title")
+ assert_equal "John", t.author_name
+ assert_equal "super_title", t.title
assert t.changed?, "topic should have changed"
assert t.author_name_changed?, "author_name should have changed"
t.reload
assert_equal author_name, t.author_name
- assert_equal 'super_title', t.title
+ assert_equal "super_title", t.title
end
def test_update_columns_changing_id
@@ -697,61 +819,53 @@ class PersistenceTest < ActiveRecord::TestCase
def test_update_columns_with_default_scope
developer = DeveloperCalledDavid.first
- developer.name = 'John'
+ developer.name = "John"
developer.save!
- assert developer.update_columns(name: 'Will'), 'did not update record due to default scope'
+ assert developer.update_columns(name: "Will"), "did not update record due to default scope"
end
def test_update
topic = Topic.find(1)
- assert !topic.approved?
+ assert_not_predicate topic, :approved?
assert_equal "The First Topic", topic.title
topic.update("approved" => true, "title" => "The First Topic Updated")
topic.reload
- assert topic.approved?
+ assert_predicate topic, :approved?
assert_equal "The First Topic Updated", topic.title
topic.update(approved: false, title: "The First Topic")
topic.reload
- assert !topic.approved?
- assert_equal "The First Topic", topic.title
- end
-
- def test_update_attributes
- topic = Topic.find(1)
- assert !topic.approved?
- assert_equal "The First Topic", topic.title
-
- topic.update_attributes("approved" => true, "title" => "The First Topic Updated")
- topic.reload
- assert topic.approved?
- assert_equal "The First Topic Updated", topic.title
-
- topic.update_attributes(approved: false, title: "The First Topic")
- topic.reload
- assert !topic.approved?
+ assert_not_predicate topic, :approved?
assert_equal "The First Topic", topic.title
- assert_raise(ActiveRecord::RecordNotUnique, ActiveRecord::StatementInvalid) do
- topic.update_attributes(id: 3, title: "Hm is it possible?")
+ error = assert_raise(ActiveRecord::RecordNotUnique, ActiveRecord::StatementInvalid) do
+ topic.update(id: 3, title: "Hm is it possible?")
end
+ assert_not_nil error.cause
assert_not_equal "Hm is it possible?", Topic.find(3).title
- topic.update_attributes(id: 1234)
+ topic.update(id: 1234)
assert_nothing_raised { topic.reload }
assert_equal topic.title, Topic.find(1234).title
end
- def test_update_attributes_parameters
+ def test_update_attributes
+ topic = Topic.find(1)
+ assert_deprecated do
+ topic.update_attributes("title" => "The First Topic Updated")
+ end
+ end
+
+ def test_update_parameters
topic = Topic.find(1)
assert_nothing_raised do
- topic.update_attributes({})
+ topic.update({})
end
assert_raises(ArgumentError) do
- topic.update_attributes(nil)
+ topic.update(nil)
end
end
@@ -777,24 +891,10 @@ class PersistenceTest < ActiveRecord::TestCase
end
def test_update_attributes!
- Reply.validates_presence_of(:title)
reply = Reply.find(2)
- assert_equal "The Second Topic of the day", reply.title
- assert_equal "Have a nice day", reply.content
-
- reply.update_attributes!("title" => "The Second Topic of the day updated", "content" => "Have a nice evening")
- reply.reload
- assert_equal "The Second Topic of the day updated", reply.title
- assert_equal "Have a nice evening", reply.content
-
- reply.update_attributes!(title: "The Second Topic of the day", content: "Have a nice day")
- reply.reload
- assert_equal "The Second Topic of the day", reply.title
- assert_equal "Have a nice day", reply.content
-
- assert_raise(ActiveRecord::RecordInvalid) { reply.update_attributes!(title: nil, content: "Have a nice evening") }
- ensure
- Reply.clear_validators!
+ assert_deprecated do
+ reply.update_attributes!("title" => "The Second Topic of the day updated")
+ end
end
def test_destroyed_returns_boolean
@@ -810,7 +910,7 @@ class PersistenceTest < ActiveRecord::TestCase
end
def test_persisted_returns_boolean
- developer = Developer.new(:name => "Jose")
+ developer = Developer.new(name: "Jose")
assert_equal false, developer.persisted?
developer.save!
assert_equal true, developer.persisted?
@@ -830,18 +930,41 @@ class PersistenceTest < ActiveRecord::TestCase
should_be_destroyed_reply = Reply.create("title" => "hello", "content" => "world")
Topic.find(1).replies << should_be_destroyed_reply
- Topic.destroy(1)
+ topic = Topic.destroy(1)
+ assert_predicate topic, :destroyed?
+
assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1) }
assert_raise(ActiveRecord::RecordNotFound) { Reply.find(should_be_destroyed_reply.id) }
end
+ def test_class_level_destroy_is_affected_by_scoping
+ should_not_be_destroyed_reply = Reply.create("title" => "hello", "content" => "world")
+ Topic.find(1).replies << should_not_be_destroyed_reply
+
+ assert_raise(ActiveRecord::RecordNotFound) do
+ Topic.where("1=0").scoping { Topic.destroy(1) }
+ end
+
+ assert_nothing_raised { Topic.find(1) }
+ assert_nothing_raised { Reply.find(should_not_be_destroyed_reply.id) }
+ end
+
def test_class_level_delete
- should_be_destroyed_reply = Reply.create("title" => "hello", "content" => "world")
- Topic.find(1).replies << should_be_destroyed_reply
+ should_not_be_destroyed_reply = Reply.create("title" => "hello", "content" => "world")
+ Topic.find(1).replies << should_not_be_destroyed_reply
Topic.delete(1)
assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1) }
- assert_nothing_raised { Reply.find(should_be_destroyed_reply.id) }
+ assert_nothing_raised { Reply.find(should_not_be_destroyed_reply.id) }
+ end
+
+ def test_class_level_delete_is_affected_by_scoping
+ should_not_be_destroyed_reply = Reply.create("title" => "hello", "content" => "world")
+ Topic.find(1).replies << should_not_be_destroyed_reply
+
+ Topic.where("1=0").scoping { Topic.delete(1) }
+ assert_nothing_raised { Topic.find(1) }
+ assert_nothing_raised { Reply.find(should_not_be_destroyed_reply.id) }
end
def test_create_with_custom_timestamps
@@ -879,7 +1002,7 @@ class PersistenceTest < ActiveRecord::TestCase
end
def test_reload_removes_custom_selects
- post = Post.select('posts.*, 1 as wibble').last!
+ post = Post.select("posts.*, 1 as wibble").last!
assert_equal 1, post[:wibble]
assert_nil post.reload[:wibble]
@@ -888,20 +1011,20 @@ class PersistenceTest < ActiveRecord::TestCase
def test_find_via_reload
post = Post.new
- assert post.new_record?
+ assert_predicate post, :new_record?
post.id = 1
post.reload
assert_equal "Welcome to the weblog", post.title
- assert_not post.new_record?
+ assert_not_predicate post, :new_record?
end
def test_reload_via_querycache
ActiveRecord::Base.connection.enable_query_cache!
ActiveRecord::Base.connection.clear_query_cache
- assert ActiveRecord::Base.connection.query_cache_enabled, 'cache should be on'
- parrot = Parrot.create(:name => 'Shane')
+ assert ActiveRecord::Base.connection.query_cache_enabled, "cache should be on"
+ parrot = Parrot.create(name: "Shane")
# populate the cache with the SELECT result
found_parrot = Parrot.find(parrot.id)
@@ -910,49 +1033,50 @@ class PersistenceTest < ActiveRecord::TestCase
# Manually update the 'name' attribute in the DB directly
assert_equal 1, ActiveRecord::Base.connection.query_cache.length
ActiveRecord::Base.uncached do
- found_parrot.name = 'Mary'
+ found_parrot.name = "Mary"
found_parrot.save
end
# Now reload, and verify that it gets the DB version, and not the querycache version
found_parrot.reload
- assert_equal 'Mary', found_parrot.name
+ assert_equal "Mary", found_parrot.name
found_parrot = Parrot.find(parrot.id)
- assert_equal 'Mary', found_parrot.name
+ assert_equal "Mary", found_parrot.name
ensure
ActiveRecord::Base.connection.disable_query_cache!
end
- class SaveTest < ActiveRecord::TestCase
- self.use_transactional_tests = false
+ def test_save_touch_false
+ parrot = Parrot.create!(
+ name: "Bob",
+ created_at: 1.day.ago,
+ updated_at: 1.day.ago)
- def test_save_touch_false
- widget = Class.new(ActiveRecord::Base) do
- connection.create_table :widgets, force: true do |t|
- t.string :name
- t.timestamps null: false
- end
+ created_at = parrot.created_at
+ updated_at = parrot.updated_at
- self.table_name = :widgets
- end
+ parrot.name = "Barb"
+ parrot.save!(touch: false)
+ assert_equal parrot.created_at, created_at
+ assert_equal parrot.updated_at, updated_at
+ end
- instance = widget.create!({
- name: 'Bob',
- created_at: 1.day.ago,
- updated_at: 1.day.ago
- })
-
- created_at = instance.created_at
- updated_at = instance.updated_at
-
- instance.name = 'Barb'
- instance.save!(touch: false)
- assert_equal instance.created_at, created_at
- assert_equal instance.updated_at, updated_at
- ensure
- ActiveRecord::Base.connection.drop_table widget.table_name
- widget.reset_column_information
- end
+ def test_reset_column_information_resets_children
+ child_class = Class.new(Topic)
+ child_class.new # force schema to load
+
+ ActiveRecord::Base.connection.add_column(:topics, :foo, :string)
+ Topic.reset_column_information
+
+ # this should redefine attribute methods
+ child_class.new
+
+ assert child_class.instance_methods.include?(:foo)
+ assert child_class.instance_methods.include?(:foo_changed?)
+ assert_equal "bar", child_class.new(foo: :bar).foo
+ ensure
+ ActiveRecord::Base.connection.remove_column(:topics, :foo)
+ Topic.reset_column_information
end
end
diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb
index daa3271777..fa7f759e51 100644
--- a/activerecord/test/cases/pooled_connections_test.rb
+++ b/activerecord/test/cases/pooled_connections_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "models/project"
require "timeout"
@@ -18,7 +20,7 @@ class PooledConnectionsTest < ActiveRecord::TestCase
# Will deadlock due to lack of Monitor timeouts in 1.9
def checkout_checkin_connections(pool_size, threads)
- ActiveRecord::Base.establish_connection(@connection.merge({:pool => pool_size, :checkout_timeout => 0.5}))
+ ActiveRecord::Base.establish_connection(@connection.merge(pool: pool_size, checkout_timeout: 0.5))
@connection_count = 0
@timed_out = 0
threads.times do
@@ -36,7 +38,7 @@ class PooledConnectionsTest < ActiveRecord::TestCase
end
def checkout_checkin_connections_loop(pool_size, loops)
- ActiveRecord::Base.establish_connection(@connection.merge({:pool => pool_size, :checkout_timeout => 0.5}))
+ ActiveRecord::Base.establish_connection(@connection.merge(pool: pool_size, checkout_timeout: 0.5))
@connection_count = 0
@timed_out = 0
loops.times do
@@ -44,7 +46,7 @@ class PooledConnectionsTest < ActiveRecord::TestCase
conn = ActiveRecord::Base.connection_pool.checkout
ActiveRecord::Base.connection_pool.checkin conn
@connection_count += 1
- ActiveRecord::Base.connection.tables
+ ActiveRecord::Base.connection.data_sources
rescue ActiveRecord::ConnectionTimeoutError
@timed_out += 1
end
@@ -66,7 +68,7 @@ class PooledConnectionsTest < ActiveRecord::TestCase
end
def test_pooled_connection_remove
- ActiveRecord::Base.establish_connection(@connection.merge({:pool => 2, :checkout_timeout => 0.5}))
+ ActiveRecord::Base.establish_connection(@connection.merge(pool: 2, checkout_timeout: 0.5))
old_connection = ActiveRecord::Base.connection
extra_connection = ActiveRecord::Base.connection_pool.checkout
ActiveRecord::Base.connection_pool.remove(extra_connection)
@@ -75,7 +77,7 @@ class PooledConnectionsTest < ActiveRecord::TestCase
private
- def add_record(name)
- ActiveRecord::Base.connection_pool.with_connection { Project.create! :name => name }
- end
+ def add_record(name)
+ ActiveRecord::Base.connection_pool.with_connection { Project.create! name: name }
+ end
end unless in_memory_db?
diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb
index 0745a37ee9..4ed7469039 100644
--- a/activerecord/test/cases/primary_keys_test.rb
+++ b/activerecord/test/cases/primary_keys_test.rb
@@ -1,12 +1,15 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'support/schema_dumping_helper'
-require 'models/topic'
-require 'models/reply'
-require 'models/subscriber'
-require 'models/movie'
-require 'models/keyboard'
-require 'models/mixed_case_monkey'
-require 'models/dashboard'
+require "support/schema_dumping_helper"
+require "models/topic"
+require "models/reply"
+require "models/subscriber"
+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
@@ -45,7 +48,7 @@ class PrimaryKeysTest < ActiveRecord::TestCase
topic = Topic.new
topic.title = "New Topic"
assert_nil topic.id
- assert_nothing_raised { topic.save! }
+ topic.save!
id = topic.id
topicReloaded = Topic.find(id)
@@ -54,22 +57,35 @@ class PrimaryKeysTest < ActiveRecord::TestCase
def test_customized_primary_key_auto_assigns_on_save
Keyboard.delete_all
- keyboard = Keyboard.new(:name => 'HHKB')
- assert_nothing_raised { keyboard.save! }
- assert_equal keyboard.id, Keyboard.find_by_name('HHKB').id
+ keyboard = Keyboard.new(name: "HHKB")
+ keyboard.save!
+ assert_equal keyboard.id, Keyboard.find_by_name("HHKB").id
end
def test_customized_primary_key_can_be_get_before_saving
keyboard = Keyboard.new
assert_nil keyboard.id
- assert_nothing_raised { assert_nil keyboard.key_number }
+ assert_nil keyboard.key_number
end
def test_customized_string_primary_key_settable_before_save
subscriber = Subscriber.new
- assert_nothing_raised { subscriber.id = 'webster123' }
- assert_equal 'webster123', subscriber.id
- assert_equal 'webster123', subscriber.nick
+ subscriber.id = "webster123"
+ assert_equal "webster123", subscriber.id
+ assert_equal "webster123", subscriber.nick
+ end
+
+ def test_update_with_non_primary_key_id_column
+ subscriber = Subscriber.first
+ subscriber.update(update_count: 1)
+ subscriber.reload
+ assert_equal 1, subscriber.update_count
+ end
+
+ def test_update_columns_with_non_primary_key_id_column
+ subscriber = Subscriber.first
+ subscriber.update_columns(id: 1)
+ assert_not_equal 1, subscriber.nick
end
def test_string_key
@@ -82,13 +98,19 @@ class PrimaryKeysTest < ActiveRecord::TestCase
subscriber.id = "jdoe"
assert_equal("jdoe", subscriber.id)
subscriber.name = "John Doe"
- assert_nothing_raised { subscriber.save! }
+ subscriber.save!
assert_equal("jdoe", subscriber.id)
subscriberReloaded = Subscriber.find("jdoe")
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,50 +135,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) }
+ 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]) }
+ 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 NoMethodError do
- ActiveRecord::Base.connection.supports_primary_key?
- end
- end
-
def test_primary_key_returns_value_if_it_exists
klass = Class.new(ActiveRecord::Base) do
- self.table_name = 'developers'
+ self.table_name = "developers"
end
- if ActiveRecord::Base.connection.supports_primary_key?
- assert_equal 'id', klass.primary_key
- end
+ assert_equal "id", klass.primary_key
end
def test_primary_key_returns_nil_if_it_does_not_exist
klass = Class.new(ActiveRecord::Base) do
- self.table_name = 'developers_projects'
+ self.table_name = "developers_projects"
end
- if ActiveRecord::Base.connection.supports_primary_key?
- assert_nil klass.primary_key
- end
+ assert_nil klass.primary_key
end
def test_quoted_primary_key_after_set_primary_key
- k = Class.new( ActiveRecord::Base )
+ k = Class.new(ActiveRecord::Base)
assert_equal k.connection.quote_column_name("id"), k.quoted_primary_key
k.primary_key = "foo"
assert_equal k.connection.quote_column_name("foo"), k.quoted_primary_key
@@ -168,25 +185,35 @@ class PrimaryKeysTest < ActiveRecord::TestCase
end
def test_primary_key_update_with_custom_key_name
- dashboard = Dashboard.create!(dashboard_id: '1')
- dashboard.id = '2'
+ dashboard = Dashboard.create!(dashboard_id: "1")
+ dashboard.id = "2"
dashboard.save!
dashboard = Dashboard.first
- assert_equal '2', dashboard.id
+ assert_equal "2", dashboard.id
+ end
+
+ def test_create_without_primary_key_no_extra_query
+ skip if current_adapter?(:OracleAdapter)
+
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "dashboards"
+ end
+ klass.create! # warmup schema cache
+ assert_queries(3, ignore_none: true) { klass.create! }
end
if current_adapter?(:PostgreSQLAdapter)
def test_serial_with_quoted_sequence_name
column = MixedCaseMonkey.columns_hash[MixedCaseMonkey.primary_key]
assert_equal "nextval('\"mixed_case_monkeys_monkeyID_seq\"'::regclass)", column.default_function
- assert column.serial?
+ assert_predicate column, :serial?
end
def test_serial_with_unquoted_sequence_name
column = Topic.columns_hash[Topic.primary_key]
assert_equal "nextval('topics_id_seq'::regclass)", column.default_function
- assert column.serial?
+ assert_predicate column, :serial?
end
end
end
@@ -199,17 +226,54 @@ class PrimaryKeyWithNoConnectionTest < ActiveRecord::TestCase
connection = ActiveRecord::Base.remove_connection
model = Class.new(ActiveRecord::Base)
- model.primary_key = 'foo'
+ model.primary_key = "foo"
- assert_equal 'foo', model.primary_key
+ assert_equal "foo", model.primary_key
ActiveRecord::Base.establish_connection(connection)
- assert_equal 'foo', model.primary_key
+ assert_equal "foo", model.primary_key
end
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
@@ -224,39 +288,129 @@ class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase
end
teardown do
- @connection.drop_table(:barcodes) if @connection.table_exists? :barcodes
+ @connection.drop_table(:barcodes, if_exists: true)
end
def test_any_type_primary_key
assert_equal "code", Barcode.primary_key
- column_type = Barcode.type_for_attribute(Barcode.primary_key)
- assert_equal :string, column_type.type
- assert_equal 42, column_type.limit
+ column = Barcode.column_for_attribute(Barcode.primary_key)
+ assert_not column.null
+ assert_equal :string, column.type
+ assert_equal 42, column.limit
+ ensure
+ Barcode.reset_column_information
end
test "schema dump primary key includes type and options" do
schema = dump_table_schema "barcodes"
assert_match %r{create_table "barcodes", primary_key: "code", id: :string, limit: 42}, schema
+ assert_no_match %r{t\.index \["code"\]}, 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
-if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
- class PrimaryKeyWithAnsiQuotesTest < ActiveRecord::TestCase
- self.use_transactional_tests = false
+class CompositePrimaryKeyTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
+
+ self.use_transactional_tests = false
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ @connection.schema_cache.clear!
+ @connection.create_table(:uber_barcodes, primary_key: ["region", "code"], force: true) do |t|
+ 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
+ @connection.create_table(:travels, primary_key: ["from", "to"], force: true) do |t|
+ t.string :from
+ t.string :to
+ end
+ end
+
+ def teardown
+ @connection.drop_table :uber_barcodes, if_exists: true
+ @connection.drop_table :barcodes_reverse, if_exists: true
+ @connection.drop_table :travels, if_exists: true
+ end
- def test_primary_key_method_with_ansi_quotes
- con = ActiveRecord::Base.connection
- con.execute("SET SESSION sql_mode='ANSI_QUOTES'")
- assert_equal "id", con.primary_key("topics")
- ensure
- con.reconnect!
+ def test_composite_primary_key
+ assert_equal ["region", "code"], @connection.primary_keys("uber_barcodes")
+ end
+
+ def test_composite_primary_key_with_reserved_words
+ assert_equal ["from", "to"], @connection.primary_keys("travels")
+ 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
+ "uber_barcodes"
+ end
+ end
+ warning = capture(:stderr) do
+ assert_nil model.primary_key
end
+ assert_match(/WARNING: Active Record does not support composite primary key\./, warning)
+ end
+
+ def test_collectly_dump_composite_primary_key
+ schema = dump_table_schema "uber_barcodes"
+ assert_match %r{create_table "uber_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
+
+class PrimaryKeyIntegerNilDefaultTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
+
+ self.use_transactional_tests = false
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def teardown
+ @connection.drop_table :int_defaults, if_exists: true
+ 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
+
+ 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, :MysqlAdapter, :Mysql2Adapter)
- class PrimaryKeyBigSerialTest < ActiveRecord::TestCase
+if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter)
+ class PrimaryKeyIntegerTest < ActiveRecord::TestCase
include SchemaDumpingHelper
self.use_transactional_tests = false
@@ -266,45 +420,55 @@ if current_adapter?(:PostgreSQLAdapter, :MysqlAdapter, :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_predicate 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}, schema
- else
- assert_match %r{create_table "widgets", id: :bigint}, schema
- end
+ assert_match %r{create_table "widgets", id: :#{@pk_type}, }, schema
end
- if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
+ if current_adapter?(:Mysql2Adapter)
test "primary key column type with options" do
- @connection.create_table(:widgets, id: :primary_key, limit: 8, force: true)
- column = @connection.columns(:widgets).find { |c| c.name == 'id' }
- assert column.auto_increment?
+ @connection.create_table(:widgets, id: :primary_key, limit: 4, unsigned: true, force: true)
+ column = @connection.columns(:widgets).find { |c| c.name == "id" }
+ assert_predicate column, :auto_increment?
assert_equal :integer, column.type
- assert_equal 8, column.limit
+ assert_not_predicate column, :bigint?
+ assert_predicate column, :unsigned?
+
+ schema = dump_table_schema "widgets"
+ assert_match %r{create_table "widgets", id: :integer, unsigned: true, }, schema
+ 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_predicate column, :auto_increment?
+ assert_equal :integer, column.type
+ assert_predicate column, :bigint?
+ assert_predicate column, :unsigned?
+
+ schema = dump_table_schema "widgets"
+ assert_match %r{create_table "widgets", id: :bigint, unsigned: true, }, schema
end
end
end
diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb
index 2f0b5df286..565190c476 100644
--- a/activerecord/test/cases/query_cache_test.rb
+++ b/activerecord/test/cases/query_cache_test.rb
@@ -1,123 +1,166 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/topic'
-require 'models/task'
-require 'models/category'
-require 'models/post'
-require 'rack'
+require "models/topic"
+require "models/task"
+require "models/category"
+require "models/post"
+require "rack"
class QueryCacheTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
+
fixtures :tasks, :topics, :categories, :posts, :categories_posts
- teardown do
+ class ShouldNotHaveExceptionsLogger < ActiveRecord::LogSubscriber
+ attr_reader :logger, :events
+
+ def initialize
+ super
+ @logger = ::Logger.new File::NULL
+ @exception = false
+ @events = []
+ end
+
+ def exception?
+ @exception
+ end
+
+ def sql(event)
+ @events << event
+ super
+ rescue
+ @exception = true
+ end
+ end
+
+ def teardown
Task.connection.clear_query_cache
ActiveRecord::Base.connection.disable_query_cache!
+ super
end
def test_exceptional_middleware_clears_and_disables_cache_on_error
- assert !ActiveRecord::Base.connection.query_cache_enabled, 'cache off'
+ assert_cache :off
- mw = ActiveRecord::QueryCache.new lambda { |env|
+ mw = middleware { |env|
Task.find 1
Task.find 1
- assert_equal 1, ActiveRecord::Base.connection.query_cache.length
- raise "lol borked"
- }
- assert_raises(RuntimeError) { mw.call({}) }
-
- assert_equal 0, ActiveRecord::Base.connection.query_cache.length
- assert !ActiveRecord::Base.connection.query_cache_enabled, 'cache off'
- end
-
- def test_exceptional_middleware_leaves_enabled_cache_alone
- ActiveRecord::Base.connection.enable_query_cache!
-
- mw = ActiveRecord::QueryCache.new lambda { |env|
+ query_cache = ActiveRecord::Base.connection.query_cache
+ assert_equal 1, query_cache.length, query_cache.keys
raise "lol borked"
}
assert_raises(RuntimeError) { mw.call({}) }
- assert ActiveRecord::Base.connection.query_cache_enabled, 'cache on'
+ assert_cache :off
end
- def test_exceptional_middleware_assigns_original_connection_id_on_error
- connection_id = ActiveRecord::Base.connection_id
-
- mw = ActiveRecord::QueryCache.new lambda { |env|
- ActiveRecord::Base.connection_id = self.object_id
- raise "lol borked"
- }
- assert_raises(RuntimeError) { mw.call({}) }
-
- assert_equal connection_id, ActiveRecord::Base.connection_id
+ 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
+
+ ActiveRecord::Base.connection_pool.connections.each do |conn|
+ assert_cache :off, conn
+ end
+
+ assert_not_predicate 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
+
+ started = Concurrent::Event.new
+ checked = Concurrent::Event.new
+
+ thread_2_connection = nil
+ thread = Thread.new {
+ thread_2_connection = ActiveRecord::Base.connection
+
+ assert_equal thread_2_connection, thread_1_connection
+ assert_cache :off
+
+ middleware {
+ assert_cache :clean
+
+ Task.find 1
+ assert_cache :dirty
+
+ 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.connection_pool.disconnect!
+ end
+ end
end
def test_middleware_delegates
called = false
- mw = ActiveRecord::QueryCache.new lambda { |env|
+ mw = middleware { |env|
called = true
[200, {}, nil]
}
mw.call({})
- assert called, 'middleware should delegate'
+ assert called, "middleware should delegate"
end
def test_middleware_caches
- mw = ActiveRecord::QueryCache.new lambda { |env|
+ mw = middleware { |env|
Task.find 1
Task.find 1
- assert_equal 1, ActiveRecord::Base.connection.query_cache.length
+ query_cache = ActiveRecord::Base.connection.query_cache
+ assert_equal 1, query_cache.length, query_cache.keys
[200, {}, nil]
}
mw.call({})
end
def test_cache_enabled_during_call
- assert !ActiveRecord::Base.connection.query_cache_enabled, 'cache off'
+ assert_cache :off
- mw = ActiveRecord::QueryCache.new lambda { |env|
- assert ActiveRecord::Base.connection.query_cache_enabled, 'cache on'
+ mw = middleware { |env|
+ assert_cache :clean
[200, {}, nil]
}
mw.call({})
end
- def test_cache_on_during_body_write
- streaming = Class.new do
- def each
- yield ActiveRecord::Base.connection.query_cache_enabled
- end
- end
-
- mw = ActiveRecord::QueryCache.new lambda { |env|
- [200, {}, streaming.new]
- }
- body = mw.call({}).last
- body.each { |x| assert x, 'cache should be on' }
- body.close
- assert !ActiveRecord::Base.connection.query_cache_enabled, 'cache disabled'
- end
-
- def test_cache_off_after_close
- mw = ActiveRecord::QueryCache.new lambda { |env| [200, {}, nil] }
- body = mw.call({}).last
-
- assert ActiveRecord::Base.connection.query_cache_enabled, 'cache enabled'
- body.close
- assert !ActiveRecord::Base.connection.query_cache_enabled, 'cache disabled'
- end
-
- def test_cache_clear_after_close
- mw = ActiveRecord::QueryCache.new lambda { |env|
- Post.first
- [200, {}, nil]
- }
- body = mw.call({}).last
-
- assert !ActiveRecord::Base.connection.query_cache.empty?, 'cache not empty'
- body.close
- assert ActiveRecord::Base.connection.query_cache.empty?, 'cache should be empty'
- end
-
def test_cache_passing_a_relation
post = Post.first
Post.cache do
@@ -147,7 +190,7 @@ class QueryCacheTest < ActiveRecord::TestCase
Task.cache do
assert_queries(2) { Task.find(1); Task.find(2) }
end
- assert_queries(0) { Task.find(1); Task.find(1); Task.find(2) }
+ assert_no_queries { Task.find(1); Task.find(1); Task.find(2) }
end
end
@@ -157,6 +200,52 @@ class QueryCacheTest < ActiveRecord::TestCase
end
end
+ def test_exists_queries_with_cache
+ Post.cache do
+ assert_queries(1) { Post.exists?; Post.exists? }
+ end
+ end
+
+ def test_select_all_with_cache
+ Post.cache do
+ assert_queries(1) do
+ 2.times { Post.connection.select_all(Post.all) }
+ end
+ end
+ end
+
+ def test_select_one_with_cache
+ Post.cache do
+ assert_queries(1) do
+ 2.times { Post.connection.select_one(Post.all) }
+ end
+ end
+ end
+
+ def test_select_value_with_cache
+ Post.cache do
+ assert_queries(1) do
+ 2.times { Post.connection.select_value(Post.all) }
+ end
+ end
+ end
+
+ def test_select_values_with_cache
+ Post.cache do
+ assert_queries(1) do
+ 2.times { Post.connection.select_values(Post.all) }
+ end
+ end
+ end
+
+ def test_select_rows_with_cache
+ Post.cache do
+ assert_queries(1) do
+ 2.times { Post.connection.select_rows(Post.all) }
+ end
+ end
+ end
+
def test_query_cache_dups_results_correctly
Task.cache do
now = Time.now.utc
@@ -168,9 +257,55 @@ class QueryCacheTest < ActiveRecord::TestCase
end
end
+ def test_cache_notifications_can_be_overridden
+ logger = ShouldNotHaveExceptionsLogger.new
+ subscriber = ActiveSupport::Notifications.subscribe "sql.active_record", logger
+
+ connection = ActiveRecord::Base.connection.dup
+
+ def connection.cache_notification_info(sql, name, binds)
+ super.merge(neat: true)
+ end
+
+ connection.cache do
+ connection.select_all "select 1"
+ connection.select_all "select 1"
+ end
+
+ assert_equal true, logger.events.last.payload[:neat]
+ ensure
+ ActiveSupport::Notifications.unsubscribe subscriber
+ end
+
+ def test_cache_does_not_raise_exceptions
+ logger = ShouldNotHaveExceptionsLogger.new
+ subscriber = ActiveSupport::Notifications.subscribe "sql.active_record", logger
+
+ ActiveRecord::Base.cache do
+ assert_queries(1) { Task.find(1); Task.find(1) }
+ end
+
+ assert_not_predicate logger, :exception?
+ ensure
+ 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 frozen_error_class 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
- Topic.columns # don't count this query
assert_queries(1) { Topic.find(1); Topic.find(1); }
end
@@ -179,15 +314,10 @@ class QueryCacheTest < ActiveRecord::TestCase
end
end
- def test_cache_does_not_wrap_string_results_in_arrays
+ def test_cache_does_not_wrap_results_in_arrays
Task.cache do
- # Oracle adapter returns count() as Fixnum or Float
- if current_adapter?(:OracleAdapter)
- assert_kind_of Numeric, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks")
- elsif current_adapter?(:SQLite3Adapter, :Mysql2Adapter, :PostgreSQLAdapter)
- # Future versions of the sqlite3 adapter will return numeric
- assert_instance_of Fixnum,
- Task.connection.select_value("SELECT count(*) AS count_all FROM tasks")
+ if current_adapter?(:SQLite3Adapter, :Mysql2Adapter, :PostgreSQLAdapter, :OracleAdapter)
+ assert_equal 2, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks")
else
assert_instance_of String, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks")
end
@@ -213,42 +343,190 @@ class QueryCacheTest < ActiveRecord::TestCase
ActiveRecord::Base.configurations = conf
end
+ def test_cache_is_available_when_using_a_not_connected_connection
+ skip "In-Memory DB can't test for using a not connected connection" if in_memory_db?
+ with_temporary_connection_pool do
+ 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"
+ assert_not_predicate Task, :connected?
+
+ Task.cache do
+ begin
+ assert_queries(1) { 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_no_queries 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
+ end
+
def test_query_cache_doesnt_leak_cached_results_of_rolled_back_queries
ActiveRecord::Base.connection.enable_query_cache!
post = Post.first
Post.transaction do
- post.update_attributes(title: 'rollback')
- assert_equal 1, Post.where(title: 'rollback').to_a.count
+ post.update(title: "rollback")
+ assert_equal 1, Post.where(title: "rollback").to_a.count
raise ActiveRecord::Rollback
end
- assert_equal 0, Post.where(title: 'rollback').to_a.count
+ assert_equal 0, Post.where(title: "rollback").to_a.count
ActiveRecord::Base.connection.uncached do
- assert_equal 0, Post.where(title: 'rollback').to_a.count
+ assert_equal 0, Post.where(title: "rollback").to_a.count
end
begin
Post.transaction do
- post.update_attributes(title: 'rollback')
- assert_equal 1, Post.where(title: 'rollback').to_a.count
- raise 'broken'
+ post.update(title: "rollback")
+ assert_equal 1, Post.where(title: "rollback").to_a.count
+ raise "broken"
end
rescue Exception
end
- assert_equal 0, Post.where(title: 'rollback').to_a.count
+ assert_equal 0, Post.where(title: "rollback").to_a.count
ActiveRecord::Base.connection.uncached do
- assert_equal 0, Post.where(title: 'rollback').to_a.count
+ assert_equal 0, Post.where(title: "rollback").to_a.count
end
end
+
+ def test_query_cached_even_when_types_are_reset
+ Task.cache do
+ # Warm the cache
+ Task.find(1)
+
+ # Preload the type cache again (so we don't have those queries issued during our assertions)
+ Task.connection.send(:reload_type_map)
+
+ # Clear places where type information is cached
+ Task.reset_column_information
+ Task.initialize_find_by_cache
+ Task.define_attribute_methods
+
+ assert_no_queries do
+ Task.find(1)
+ end
+ end
+ end
+
+ def test_query_cache_does_not_establish_connection_if_unconnected
+ with_temporary_connection_pool do
+ ActiveRecord::Base.clear_active_connections!
+ assert_not ActiveRecord::Base.connection_handler.active_connections? # sanity check
+
+ middleware {
+ assert_not ActiveRecord::Base.connection_handler.active_connections?, "QueryCache forced ActiveRecord::Base to establish a connection in setup"
+ }.call({})
+
+ assert_not ActiveRecord::Base.connection_handler.active_connections?, "QueryCache forced ActiveRecord::Base to establish a connection in cleanup"
+ end
+ end
+
+ def test_query_cache_is_enabled_on_connections_established_after_middleware_runs
+ with_temporary_connection_pool do
+ ActiveRecord::Base.clear_active_connections!
+ assert_not ActiveRecord::Base.connection_handler.active_connections? # sanity check
+
+ middleware {
+ assert_predicate ActiveRecord::Base.connection, :query_cache_enabled
+ }.call({})
+ assert_not_predicate ActiveRecord::Base.connection, :query_cache_enabled
+ end
+ end
+
+ def test_query_caching_is_local_to_the_current_thread
+ 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
+
+ Thread.new {
+ assert_not ActiveRecord::Base.connection_pool.query_cache_enabled
+ assert_not ActiveRecord::Base.connection.query_cache_enabled
+ }.join
+ }.call({})
+ end
+ end
+
+ def test_query_cache_is_enabled_on_all_connection_pools
+ middleware {
+ ActiveRecord::Base.connection_handler.connection_pool_list.each do |pool|
+ assert pool.query_cache_enabled
+ assert pool.connection.query_cache_enabled
+ end
+ }.call({})
+ end
+
+ private
+
+ def 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
+
+ yield
+ ensure
+ ActiveRecord::Base.connection_handler.send(:owner_to_pool)["primary"] = old_pool
+ end
+
+ def middleware(&app)
+ executor = Class.new(ActiveSupport::Executor)
+ ActiveRecord::QueryCache.install_executor_hooks executor
+ lambda { |env| executor.wrap { app.call(env) } }
+ end
+
+ def assert_cache(state, connection = ActiveRecord::Base.connection)
+ case state
+ when :off
+ assert_not connection.query_cache_enabled, "cache should be off"
+ assert connection.query_cache.empty?, "cache should be empty"
+ when :clean
+ assert connection.query_cache_enabled, "cache should be on"
+ assert connection.query_cache.empty?, "cache should be empty"
+ when :dirty
+ assert connection.query_cache_enabled, "cache should be on"
+ assert_not connection.query_cache.empty?, "cache should be dirty"
+ else
+ raise "unknown state"
+ end
+ end
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)
@@ -262,61 +540,78 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase
end
def test_find
- Task.connection.expects(:clear_query_cache).times(1)
+ assert_called(Task.connection, :clear_query_cache) do
+ assert_not Task.connection.query_cache_enabled
+ Task.cache do
+ assert Task.connection.query_cache_enabled
+ Task.find(1)
- assert !Task.connection.query_cache_enabled
- Task.cache do
- assert Task.connection.query_cache_enabled
- Task.find(1)
+ Task.uncached do
+ assert_not Task.connection.query_cache_enabled
+ Task.find(1)
+ end
- Task.uncached do
- assert !Task.connection.query_cache_enabled
- Task.find(1)
+ assert Task.connection.query_cache_enabled
end
-
- assert Task.connection.query_cache_enabled
+ assert_not Task.connection.query_cache_enabled
end
- assert !Task.connection.query_cache_enabled
end
def test_update
- Task.connection.expects(:clear_query_cache).times(2)
- Task.cache do
- task = Task.find(1)
- task.starting = Time.now.utc
- task.save!
+ assert_called(Task.connection, :clear_query_cache, times: 2) do
+ Task.cache do
+ task = Task.find(1)
+ task.starting = Time.now.utc
+ task.save!
+ end
end
end
def test_destroy
- Task.connection.expects(:clear_query_cache).times(2)
- Task.cache do
- Task.find(1).destroy
+ assert_called(Task.connection, :clear_query_cache, times: 2) do
+ Task.cache do
+ Task.find(1).destroy
+ end
end
end
def test_insert
- ActiveRecord::Base.connection.expects(:clear_query_cache).times(2)
- Task.cache do
- Task.create!
+ assert_called(ActiveRecord::Base.connection, :clear_query_cache, times: 2) do
+ Task.cache do
+ Task.create!
+ end
end
end
def test_cache_is_expired_by_habtm_update
- ActiveRecord::Base.connection.expects(:clear_query_cache).times(2)
- ActiveRecord::Base.cache do
- c = Category.first
- p = Post.first
- p.categories << c
+ assert_called(ActiveRecord::Base.connection, :clear_query_cache, times: 2) do
+ ActiveRecord::Base.cache do
+ c = Category.first
+ p = Post.first
+ p.categories << c
+ end
end
end
def test_cache_is_expired_by_habtm_delete
- ActiveRecord::Base.connection.expects(:clear_query_cache).times(2)
- ActiveRecord::Base.cache do
- p = Post.find(1)
- assert p.categories.any?
- p.categories.delete_all
+ assert_called(ActiveRecord::Base.connection, :clear_query_cache, times: 2) do
+ ActiveRecord::Base.cache do
+ p = Post.find(1)
+ assert_predicate p.categories, :any?
+ p.categories.delete_all
+ 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 6d91f96bf6..723fccc8d9 100644
--- a/activerecord/test/cases/quoting_test.rb
+++ b/activerecord/test/cases/quoting_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
module ActiveRecord
@@ -8,28 +10,28 @@ module ActiveRecord
end
def test_quoted_true
- assert_equal "'t'", @quoter.quoted_true
+ assert_equal "TRUE", @quoter.quoted_true
end
def test_quoted_false
- assert_equal "'f'", @quoter.quoted_false
+ assert_equal "FALSE", @quoter.quoted_false
end
def test_quote_column_name
- assert_equal "foo", @quoter.quote_column_name('foo')
+ assert_equal "foo", @quoter.quote_column_name("foo")
end
def test_quote_table_name
- assert_equal "foo", @quoter.quote_table_name('foo')
+ assert_equal "foo", @quoter.quote_table_name("foo")
end
def test_quote_table_name_calls_quote_column_name
@quoter.extend(Module.new {
def quote_column_name(string)
- 'lol'
+ "lol"
end
})
- assert_equal 'lol', @quoter.quote_table_name('foo')
+ assert_equal "lol", @quoter.quote_table_name("foo")
end
def test_quote_string
@@ -44,27 +46,86 @@ module ActiveRecord
assert_equal t.to_s(:db), @quoter.quoted_date(t)
end
- def test_quoted_time_utc
+ def test_quoted_timestamp_utc
with_timezone_config default: :utc do
t = Time.now.change(usec: 0)
assert_equal t.getutc.to_s(:db), @quoter.quoted_date(t)
end
end
- def test_quoted_time_local
+ def test_quoted_timestamp_local
with_timezone_config default: :local do
t = Time.now.change(usec: 0)
assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t)
end
end
- def test_quoted_time_crazy
+ def test_quoted_timestamp_crazy
with_timezone_config default: :asdfasdf do
t = Time.now.change(usec: 0)
assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t)
end
end
+ def test_quoted_time_utc
+ with_timezone_config default: :utc do
+ t = Time.now.change(usec: 0)
+
+ expected = t.change(year: 2000, month: 1, day: 1)
+ expected = expected.getutc.to_s(:db).slice(11..-1)
+
+ assert_equal expected, @quoter.quoted_time(t)
+ end
+ end
+
+ def test_quoted_time_local
+ with_timezone_config default: :local do
+ t = Time.now.change(usec: 0)
+
+ expected = t.change(year: 2000, month: 1, day: 1)
+ expected = expected.getlocal.to_s(:db).sub("2000-01-01 ", "")
+
+ assert_equal expected, @quoter.quoted_time(t)
+ end
+ end
+
+ def test_quoted_time_dst_utc
+ with_env_tz "America/New_York" do
+ with_timezone_config default: :utc do
+ t = Time.new(2000, 7, 1, 0, 0, 0, "+04:30")
+
+ expected = t.change(year: 2000, month: 1, day: 1)
+ expected = expected.getutc.to_s(:db).slice(11..-1)
+
+ assert_equal expected, @quoter.quoted_time(t)
+ end
+ end
+ end
+
+ def test_quoted_time_dst_local
+ with_env_tz "America/New_York" do
+ with_timezone_config default: :local do
+ t = Time.new(2000, 7, 1, 0, 0, 0, "+04:30")
+
+ expected = t.change(year: 2000, month: 1, day: 1)
+ expected = expected.getlocal.to_s(:db).slice(11..-1)
+
+ assert_equal expected, @quoter.quoted_time(t)
+ end
+ end
+ end
+
+ def test_quoted_time_crazy
+ with_timezone_config default: :asdfasdf do
+ t = Time.now.change(usec: 0)
+
+ expected = t.change(year: 2000, month: 1, day: 1)
+ expected = expected.getlocal.to_s(:db).sub("2000-01-01 ", "")
+
+ assert_equal expected, @quoter.quoted_time(t)
+ end
+ end
+
def test_quoted_datetime_utc
with_timezone_config default: :utc do
t = Time.now.change(usec: 0).to_datetime
@@ -81,73 +142,156 @@ module ActiveRecord
end
end
- def test_quote_with_quoted_id
- assert_equal 1, @quoter.quote(Struct.new(:quoted_id).new(1), nil)
- 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_fixnum
- fixnum = 1
- assert_equal fixnum.to_s, @quoter.quote(fixnum, nil)
+ def test_quote_integer
+ integer = 1
+ 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)
+ bigdec = BigDecimal((1 << 100).to_s)
+ 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)
+ @quoter.extend(Module.new { def quoted_date(value) "lol" end })
+ 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
+ assert_equal "'Object'", @quoter.quote(Object)
end
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
def test_quote_string_no_column
- assert_equal "'lo\\\\l'", @quoter.quote('lo\l', nil)
+ assert_equal "'lo\\\\l'", @quoter.quote('lo\l')
end
def test_quote_as_mb_chars_no_column
string = ActiveSupport::Multibyte::Chars.new('lo\l')
- assert_equal "'lo\\\\l'", @quoter.quote(string, nil)
- end
-
- def test_string_with_crazy_column
- assert_equal "'lo\\\\l'", @quoter.quote('lo\l')
+ assert_equal "'lo\\\\l'", @quoter.quote(string)
end
def test_quote_duration
assert_equal "1800", @quoter.quote(30.minutes)
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
+ if current_adapter?(:Mysql2Adapter)
+ expected = date
+ else
+ expected = @conn.quoted_date(date)
+ end
+ assert_equal expected, @conn.type_cast(date)
+ end
+
+ def test_type_cast_time
+ time = Time.now
+ if current_adapter?(:Mysql2Adapter)
+ expected = time
+ else
+ expected = @conn.quoted_date(time)
+ end
+ 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
+ end
+
+ class QuoteBooleanTest < ActiveRecord::TestCase
+ def setup
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def test_quote_returns_frozen_string
+ assert_predicate @connection.quote(true), :frozen?
+ assert_predicate @connection.quote(false), :frozen?
+ end
+
+ def test_type_cast_returns_frozen_value
+ assert_predicate @connection.type_cast(true), :frozen?
+ 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 @connection.type_cast(value.id), @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 1c919f0b57..059fa76132 100644
--- a/activerecord/test/cases/readonly_test.rb
+++ b/activerecord/test/cases/readonly_test.rb
@@ -1,27 +1,30 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/author'
-require 'models/post'
-require 'models/comment'
-require 'models/developer'
-require 'models/computer'
-require 'models/project'
-require 'models/reader'
-require 'models/person'
+require "models/author"
+require "models/post"
+require "models/comment"
+require "models/developer"
+require "models/computer"
+require "models/project"
+require "models/reader"
+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)
- assert !dev.readonly?
+ assert_not_predicate dev, :readonly?
dev.readonly!
- assert dev.readonly?
+ assert_predicate dev, :readonly?
assert_nothing_raised do
- dev.name = 'Luscious forbidden fruit.'
- assert !dev.save
- dev.name = 'Forbidden.'
+ dev.name = "Luscious forbidden fruit."
+ assert_not dev.save
+ dev.name = "Forbidden."
end
e = assert_raise(ActiveRecord::ReadOnlyRecord) { dev.save }
@@ -34,74 +37,73 @@ class ReadOnlyTest < ActiveRecord::TestCase
assert_equal "Developer is marked as readonly", e.message
end
-
def test_find_with_readonly_option
- Developer.all.each { |d| assert !d.readonly? }
- Developer.readonly(false).each { |d| assert !d.readonly? }
+ Developer.all.each { |d| assert_not d.readonly? }
+ Developer.readonly(false).each { |d| assert_not d.readonly? }
Developer.readonly(true).each { |d| assert d.readonly? }
Developer.readonly.each { |d| assert d.readonly? }
end
def test_find_with_joins_option_does_not_imply_readonly
- Developer.joins(' ').each { |d| assert_not d.readonly? }
- Developer.joins(' ').readonly(true).each { |d| assert d.readonly? }
+ Developer.joins(" ").each { |d| assert_not d.readonly? }
+ Developer.joins(" ").readonly(true).each { |d| assert d.readonly? }
- Developer.joins(', projects').each { |d| assert_not d.readonly? }
- Developer.joins(', projects').readonly(true).each { |d| assert d.readonly? }
+ Developer.joins(", projects").each { |d| assert_not d.readonly? }
+ Developer.joins(", projects").readonly(true).each { |d| assert d.readonly? }
end
def test_has_many_find_readonly
post = Post.find(1)
- assert !post.comments.empty?
- assert !post.comments.any?(&:readonly?)
- assert !post.comments.to_a.any?(&:readonly?)
+ assert_not_empty post.comments
+ assert_not post.comments.any?(&:readonly?)
+ assert_not post.comments.to_a.any?(&:readonly?)
assert post.comments.readonly(true).all?(&:readonly?)
end
def test_has_many_with_through_is_not_implicitly_marked_readonly
assert people = Post.find(1).people
- assert !people.any?(&:readonly?)
+ assert_not people.any?(&:readonly?)
end
def test_has_many_with_through_is_not_implicitly_marked_readonly_while_finding_by_id
- assert !posts(:welcome).people.find(1).readonly?
+ assert_not_predicate posts(:welcome).people.find(1), :readonly?
end
def test_has_many_with_through_is_not_implicitly_marked_readonly_while_finding_first
- assert !posts(:welcome).people.first.readonly?
+ assert_not_predicate posts(:welcome).people.first, :readonly?
end
def test_has_many_with_through_is_not_implicitly_marked_readonly_while_finding_last
- assert !posts(:welcome).people.last.readonly?
+ assert_not_predicate posts(:welcome).people.last, :readonly?
end
def test_readonly_scoping
- Post.where('1=1').scoping do
- assert !Post.find(1).readonly?
- assert Post.readonly(true).find(1).readonly?
- assert !Post.readonly(false).find(1).readonly?
+ Post.where("1=1").scoping do
+ assert_not_predicate Post.find(1), :readonly?
+ assert_predicate Post.readonly(true).find(1), :readonly?
+ assert_not_predicate Post.readonly(false).find(1), :readonly?
end
- Post.joins(' ').scoping do
- assert !Post.find(1).readonly?
- assert Post.readonly.find(1).readonly?
- assert !Post.readonly(false).find(1).readonly?
+ Post.joins(" ").scoping do
+ assert_not_predicate Post.find(1), :readonly?
+ assert_predicate Post.readonly.find(1), :readonly?
+ assert_not_predicate Post.readonly(false).find(1), :readonly?
end
# Oracle barfs on this because the join includes unqualified and
# conflicting column names
unless current_adapter?(:OracleAdapter)
- Post.joins(', developers').scoping do
- assert_not Post.find(1).readonly?
- assert Post.readonly.find(1).readonly?
- assert !Post.readonly(false).find(1).readonly?
+ Post.joins(", developers").scoping do
+ assert_not_predicate Post.find(1), :readonly?
+ assert_predicate Post.readonly.find(1), :readonly?
+ assert_not_predicate Post.readonly(false).find(1), :readonly?
end
end
Post.readonly(true).scoping do
- assert Post.find(1).readonly?
- assert Post.readonly.find(1).readonly?
- assert !Post.readonly(false).find(1).readonly?
+ assert_predicate Post.find(1), :readonly?
+ assert_predicate Post.readonly.find(1), :readonly?
+ assert_not_predicate Post.readonly(false).find(1), :readonly?
end
end
@@ -109,10 +111,10 @@ class ReadOnlyTest < ActiveRecord::TestCase
developer = Developer.find(1)
project = Post.find(1)
- assert !developer.projects.all_as_method.first.readonly?
- assert !developer.projects.all_as_scope.first.readonly?
+ assert_not_predicate developer.projects.all_as_method.first, :readonly?
+ assert_not_predicate developer.projects.all_as_scope.first, :readonly?
- assert !project.comments.all_as_method.first.readonly?
- assert !project.comments.all_as_scope.first.readonly?
+ assert_not_predicate project.comments.all_as_method.first, :readonly?
+ assert_not_predicate project.comments.all_as_scope.first, :readonly?
end
end
diff --git a/activerecord/test/cases/reaper_test.rb b/activerecord/test/cases/reaper_test.rb
index cccfc6774e..b630f782bc 100644
--- a/activerecord/test/cases/reaper_test.rb
+++ b/activerecord/test/cases/reaper_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
module ActiveRecord
@@ -16,6 +18,7 @@ module ActiveRecord
class FakePool
attr_reader :reaped
+ attr_reader :flushed
def initialize
@reaped = false
@@ -24,20 +27,24 @@ module ActiveRecord
def reap
@reaped = true
end
+
+ def flush
+ @flushed = true
+ end
end
# A reaper with nil time should never reap connections
def test_nil_time
fp = FakePool.new
- assert !fp.reaped
+ assert_not fp.reaped
reaper = ConnectionPool::Reaper.new(fp, nil)
reaper.run
- assert !fp.reaped
+ assert_not fp.reaped
end
def test_some_time
fp = FakePool.new
- assert !fp.reaped
+ assert_not fp.reaped
reaper = ConnectionPool::Reaper.new(fp, 0.0001)
reaper.run
@@ -45,6 +52,7 @@ module ActiveRecord
Thread.pass
end
assert fp.reaped
+ assert fp.flushed
end
def test_pool_has_reaper
@@ -53,14 +61,14 @@ module ActiveRecord
def test_reaping_frequency_configuration
spec = ActiveRecord::Base.connection_pool.spec.dup
- spec.config[:reaping_frequency] = 100
+ spec.config[:reaping_frequency] = "10.01"
pool = ConnectionPool.new spec
- assert_equal 100, pool.reaper.frequency
+ assert_equal 10.01, pool.reaper.frequency
end
def test_connection_pool_starts_reaper
spec = ActiveRecord::Base.connection_pool.spec.dup
- spec.config[:reaping_frequency] = '0.0001'
+ spec.config[:reaping_frequency] = "0.0001"
pool = ConnectionPool.new spec
@@ -71,14 +79,14 @@ module ActiveRecord
end
Thread.pass while conn.nil?
- assert conn.in_use?
+ assert_predicate conn, :in_use?
child.terminate
while conn.in_use?
Thread.pass
end
- assert !conn.in_use?
+ assert_not_predicate conn, :in_use?
end
end
end
diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb
index 7b47c80331..abadafbad4 100644
--- a/activerecord/test/cases/reflection_test.rb
+++ b/activerecord/test/cases/reflection_test.rb
@@ -1,29 +1,31 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/topic'
-require 'models/customer'
-require 'models/company'
-require 'models/company_in_module'
-require 'models/ship'
-require 'models/pirate'
-require 'models/price_estimate'
-require 'models/essay'
-require 'models/author'
-require 'models/organization'
-require 'models/post'
-require 'models/tagging'
-require 'models/category'
-require 'models/book'
-require 'models/subscriber'
-require 'models/subscription'
-require 'models/tag'
-require 'models/sponsor'
-require 'models/edge'
-require 'models/hotel'
-require 'models/chef'
-require 'models/department'
-require 'models/cake_designer'
-require 'models/drink_designer'
-require 'models/recipe'
+require "models/topic"
+require "models/customer"
+require "models/company"
+require "models/company_in_module"
+require "models/ship"
+require "models/pirate"
+require "models/price_estimate"
+require "models/essay"
+require "models/author"
+require "models/organization"
+require "models/post"
+require "models/tagging"
+require "models/category"
+require "models/book"
+require "models/subscriber"
+require "models/subscription"
+require "models/tag"
+require "models/sponsor"
+require "models/edge"
+require "models/hotel"
+require "models/chef"
+require "models/department"
+require "models/cake_designer"
+require "models/drink_designer"
+require "models/recipe"
class ReflectionTest < ActiveRecord::TestCase
include ActiveRecord::Reflection
@@ -64,13 +66,16 @@ class ReflectionTest < ActiveRecord::TestCase
def test_column_string_type_and_limit
assert_equal :string, @first.column_for_attribute("title").type
+ assert_equal :string, @first.column_for_attribute(:title).type
+ assert_equal :string, @first.type_for_attribute("title").type
+ assert_equal :string, @first.type_for_attribute(:title).type
assert_equal 250, @first.column_for_attribute("title").limit
end
def test_column_null_not_null
subscriber = Subscriber.first
assert subscriber.column_for_attribute("name").null
- assert !subscriber.column_for_attribute("nick").null
+ assert_not subscriber.column_for_attribute("nick").null
end
def test_human_name_for_column
@@ -79,14 +84,20 @@ class ReflectionTest < ActiveRecord::TestCase
def test_integer_columns
assert_equal :integer, @first.column_for_attribute("id").type
+ assert_equal :integer, @first.column_for_attribute(:id).type
+ assert_equal :integer, @first.type_for_attribute("id").type
+ assert_equal :integer, @first.type_for_attribute(:id).type
end
def test_non_existent_columns_return_null_object
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
+
+ column = @first.column_for_attribute(:attribute_that_doesnt_exist)
+ assert_instance_of ActiveRecord::ConnectionAdapters::NullColumn, column
end
def test_non_existent_types_are_identity_types
@@ -96,10 +107,21 @@ class ReflectionTest < ActiveRecord::TestCase
assert_equal object, type.deserialize(object)
assert_equal object, type.cast(object)
assert_equal object, type.serialize(object)
+
+ type = @first.type_for_attribute(:attribute_that_doesnt_exist)
+ assert_equal object, type.deserialize(object)
+ assert_equal object, type.cast(object)
+ assert_equal object, type.serialize(object)
end
def test_reflection_klass_for_nested_class_name
- 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
@@ -107,28 +129,28 @@ class ReflectionTest < ActiveRecord::TestCase
def test_irregular_reflection_class_name
ActiveSupport::Inflector.inflections do |inflect|
- inflect.irregular 'plural_irregular', 'plurales_irregulares'
+ inflect.irregular "plural_irregular", "plurales_irregulares"
end
- reflection = ActiveRecord::Reflection.create(:has_many, 'plurales_irregulares', nil, {}, ActiveRecord::Base)
- assert_equal 'PluralIrregular', reflection.class_name
+ reflection = ActiveRecord::Reflection.create(:has_many, "plurales_irregulares", nil, {}, ActiveRecord::Base)
+ assert_equal "PluralIrregular", reflection.class_name
end
def test_aggregation_reflection
reflection_for_address = AggregateReflection.new(
- :address, nil, { :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ] }, Customer
+ :address, nil, { mapping: [ %w(address_street street), %w(address_city city), %w(address_country country) ] }, Customer
)
reflection_for_balance = AggregateReflection.new(
- :balance, nil, { :class_name => "Money", :mapping => %w(balance amount) }, Customer
+ :balance, nil, { class_name: "Money", mapping: %w(balance amount) }, Customer
)
reflection_for_gps_location = AggregateReflection.new(
- :gps_location, nil, { }, Customer
+ :gps_location, nil, {}, Customer
)
- assert Customer.reflect_on_all_aggregations.include?(reflection_for_gps_location)
- assert Customer.reflect_on_all_aggregations.include?(reflection_for_balance)
- assert Customer.reflect_on_all_aggregations.include?(reflection_for_address)
+ assert_includes Customer.reflect_on_all_aggregations, reflection_for_gps_location
+ assert_includes Customer.reflect_on_all_aggregations, reflection_for_balance
+ assert_includes Customer.reflect_on_all_aggregations, reflection_for_address
assert_equal reflection_for_address, Customer.reflect_on_aggregation(:address)
@@ -141,37 +163,37 @@ class ReflectionTest < ActiveRecord::TestCase
expected = Pirate.reflect_on_all_associations.select { |r| r.options[:autosave] }
received = Pirate.reflect_on_all_autosave_associations
- assert !received.empty?
+ assert_not_empty received
assert_not_equal Pirate.reflect_on_all_associations.length, received.length
assert_equal expected, received
end
def test_has_many_reflection
- reflection_for_clients = ActiveRecord::Reflection.create(:has_many, :clients, nil, { :order => "id", :dependent => :destroy }, Firm)
+ reflection_for_clients = ActiveRecord::Reflection.create(:has_many, :clients, nil, { order: "id", dependent: :destroy }, Firm)
assert_equal reflection_for_clients, Firm.reflect_on_association(:clients)
assert_equal Client, Firm.reflect_on_association(:clients).klass
- assert_equal 'companies', Firm.reflect_on_association(:clients).table_name
+ assert_equal "companies", Firm.reflect_on_association(:clients).table_name
assert_equal Client, Firm.reflect_on_association(:clients_of_firm).klass
- assert_equal 'companies', Firm.reflect_on_association(:clients_of_firm).table_name
+ assert_equal "companies", Firm.reflect_on_association(:clients_of_firm).table_name
end
def test_has_one_reflection
- reflection_for_account = ActiveRecord::Reflection.create(:has_one, :account, nil, { :foreign_key => "firm_id", :dependent => :destroy }, Firm)
+ reflection_for_account = ActiveRecord::Reflection.create(:has_one, :account, nil, { foreign_key: "firm_id", dependent: :destroy }, Firm)
assert_equal reflection_for_account, Firm.reflect_on_association(:account)
assert_equal Account, Firm.reflect_on_association(:account).klass
- assert_equal 'accounts', Firm.reflect_on_association(:account).table_name
+ assert_equal "accounts", Firm.reflect_on_association(:account).table_name
end
def test_belongs_to_inferred_foreign_key_from_assoc_name
Company.belongs_to :foo
assert_equal "foo_id", Company.reflect_on_association(:foo).foreign_key
- Company.belongs_to :bar, :class_name => "Xyzzy"
+ Company.belongs_to :bar, class_name: "Xyzzy"
assert_equal "bar_id", Company.reflect_on_association(:bar).foreign_key
- Company.belongs_to :baz, :class_name => "Xyzzy", :foreign_key => "xyzzy_id"
+ Company.belongs_to :baz, class_name: "Xyzzy", foreign_key: "xyzzy_id"
assert_equal "xyzzy_id", Company.reflect_on_association(:baz).foreign_key
end
@@ -180,45 +202,45 @@ class ReflectionTest < ActiveRecord::TestCase
assert_reflection MyApplication::Business::Firm,
:clients_of_firm,
- :klass => MyApplication::Business::Client,
- :class_name => 'Client',
- :table_name => 'companies'
+ klass: MyApplication::Business::Client,
+ class_name: "Client",
+ table_name: "companies"
assert_reflection MyApplication::Billing::Account,
:firm,
- :klass => MyApplication::Business::Firm,
- :class_name => 'MyApplication::Business::Firm',
- :table_name => 'companies'
+ klass: MyApplication::Business::Firm,
+ class_name: "MyApplication::Business::Firm",
+ table_name: "companies"
assert_reflection MyApplication::Billing::Account,
:qualified_billing_firm,
- :klass => MyApplication::Billing::Firm,
- :class_name => 'MyApplication::Billing::Firm',
- :table_name => 'companies'
+ klass: MyApplication::Billing::Firm,
+ class_name: "MyApplication::Billing::Firm",
+ table_name: "companies"
assert_reflection MyApplication::Billing::Account,
:unqualified_billing_firm,
- :klass => MyApplication::Billing::Firm,
- :class_name => 'Firm',
- :table_name => 'companies'
+ klass: MyApplication::Billing::Firm,
+ class_name: "Firm",
+ table_name: "companies"
assert_reflection MyApplication::Billing::Account,
:nested_qualified_billing_firm,
- :klass => MyApplication::Billing::Nested::Firm,
- :class_name => 'MyApplication::Billing::Nested::Firm',
- :table_name => 'companies'
+ klass: MyApplication::Billing::Nested::Firm,
+ class_name: "MyApplication::Billing::Nested::Firm",
+ table_name: "companies"
assert_reflection MyApplication::Billing::Account,
:nested_unqualified_billing_firm,
- :klass => MyApplication::Billing::Nested::Firm,
- :class_name => 'Nested::Firm',
- :table_name => 'companies'
+ klass: MyApplication::Billing::Nested::Firm,
+ class_name: "Nested::Firm",
+ table_name: "companies"
ensure
ActiveRecord::Base.store_full_sti_class = true
end
def test_reflection_should_not_raise_error_when_compared_to_other_object
- assert_not_equal Object.new, Firm._reflections['clients']
+ assert_not_equal Object.new, Firm._reflections["clients"]
end
def test_reflections_should_return_keys_as_strings
@@ -226,7 +248,7 @@ class ReflectionTest < ActiveRecord::TestCase
end
def test_has_and_belongs_to_many_reflection
- assert_equal :has_and_belongs_to_many, Category.reflections['posts'].macro
+ assert_equal :has_and_belongs_to_many, Category.reflections["posts"].macro
assert_equal :posts, Category.reflect_on_all_associations(:has_and_belongs_to_many).first.name
end
@@ -245,37 +267,35 @@ class ReflectionTest < ActiveRecord::TestCase
assert_equal expected, actual
end
- def test_scope_chain
- expected = [
- [Tagging.reflect_on_association(:tag).scope, Post.reflect_on_association(:first_blue_tags).scope],
- [Post.reflect_on_association(:first_taggings).scope],
- [Author.reflect_on_association(:misc_posts).scope]
- ]
- actual = Author.reflect_on_association(:misc_post_first_blue_tags).scope_chain
- assert_equal expected, actual
+ def test_scope_chain_does_not_interfere_with_hmt_with_polymorphic_case
+ hotel = Hotel.create!
+ department = hotel.departments.create!
+ department.chefs.create!(employable: CakeDesigner.create!)
+ department.chefs.create!(employable: DrinkDesigner.create!)
- expected = [
- [
- Tagging.reflect_on_association(:blue_tag).scope,
- Post.reflect_on_association(:first_blue_tags_2).scope,
- Author.reflect_on_association(:misc_post_first_blue_tags_2).scope
- ],
- [],
- []
- ]
- actual = Author.reflect_on_association(:misc_post_first_blue_tags_2).scope_chain
- assert_equal expected, actual
+ assert_equal 1, hotel.cake_designers.size
+ assert_equal 1, hotel.cake_designers.count
+ assert_equal 1, hotel.drink_designers.size
+ assert_equal 1, hotel.drink_designers.count
+ assert_equal 2, hotel.chefs.size
+ assert_equal 2, hotel.chefs.count
end
- def test_scope_chain_does_not_interfere_with_hmt_with_polymorphic_case
- @hotel = Hotel.create!
- @department = @hotel.departments.create!
- @department.chefs.create!(employable: CakeDesigner.create!)
- @department.chefs.create!(employable: DrinkDesigner.create!)
+ def test_scope_chain_does_not_interfere_with_hmt_with_polymorphic_case_and_sti
+ hotel = Hotel.create!
+ hotel.mocktail_designers << MocktailDesigner.create!
+
+ assert_equal 1, hotel.mocktail_designers.size
+ assert_equal 1, hotel.mocktail_designers.count
+ assert_equal 1, hotel.chef_lists.size
+ assert_equal 1, hotel.chef_lists.count
+
+ hotel.mocktail_designers = []
- assert_equal 1, @hotel.cake_designers.size
- assert_equal 1, @hotel.drink_designers.size
- assert_equal 2, @hotel.chefs.size
+ assert_equal 0, hotel.mocktail_designers.size
+ assert_equal 0, hotel.mocktail_designers.count
+ assert_equal 0, hotel.chef_lists.size
+ assert_equal 0, hotel.chef_lists.count
end
def test_scope_chain_of_polymorphic_association_does_not_leak_into_other_hmt_associations
@@ -295,12 +315,12 @@ class ReflectionTest < ActiveRecord::TestCase
end
def test_nested?
- assert !Author.reflect_on_association(:comments).nested?
- assert Author.reflect_on_association(:tags).nested?
+ assert_not_predicate Author.reflect_on_association(:comments), :nested?
+ assert_predicate Author.reflect_on_association(:tags), :nested?
# Only goes :through once, but the through_reflection is a has_and_belongs_to_many, so this is
# a nested through association
- assert Category.reflect_on_association(:post_comments).nested?
+ assert_predicate Category.reflect_on_association(:post_comments), :nested?
end
def test_association_primary_key
@@ -335,42 +355,49 @@ class ReflectionTest < ActiveRecord::TestCase
assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.active_record_primary_key }
end
+ def test_type
+ assert_equal "taggable_type", Post.reflect_on_association(:taggings).type.to_s
+ assert_equal "imageable_class", Post.reflect_on_association(:images).type.to_s
+ assert_nil Post.reflect_on_association(:readers).type
+ end
+
def test_foreign_type
assert_equal "sponsorable_type", Sponsor.reflect_on_association(:sponsorable).foreign_type.to_s
assert_equal "sponsorable_type", Sponsor.reflect_on_association(:thing).foreign_type.to_s
+ assert_nil Sponsor.reflect_on_association(:sponsor_club).foreign_type
end
def test_collection_association
- assert Pirate.reflect_on_association(:birds).collection?
- assert Pirate.reflect_on_association(:parrots).collection?
+ assert_predicate Pirate.reflect_on_association(:birds), :collection?
+ assert_predicate Pirate.reflect_on_association(:parrots), :collection?
- assert !Pirate.reflect_on_association(:ship).collection?
- assert !Ship.reflect_on_association(:pirate).collection?
+ assert_not_predicate Pirate.reflect_on_association(:ship), :collection?
+ assert_not_predicate Ship.reflect_on_association(:pirate), :collection?
end
def test_default_association_validation
- assert ActiveRecord::Reflection.create(:has_many, :clients, nil, {}, Firm).validate?
+ assert_predicate ActiveRecord::Reflection.create(:has_many, :clients, nil, {}, Firm), :validate?
- assert !ActiveRecord::Reflection.create(:has_one, :client, nil, {}, Firm).validate?
- assert !ActiveRecord::Reflection.create(:belongs_to, :client, nil, {}, Firm).validate?
+ assert_not_predicate ActiveRecord::Reflection.create(:has_one, :client, nil, {}, Firm), :validate?
+ assert_not_predicate ActiveRecord::Reflection.create(:belongs_to, :client, nil, {}, Firm), :validate?
end
def test_always_validate_association_if_explicit
- assert ActiveRecord::Reflection.create(:has_one, :client, nil, { :validate => true }, Firm).validate?
- assert ActiveRecord::Reflection.create(:belongs_to, :client, nil, { :validate => true }, Firm).validate?
- assert ActiveRecord::Reflection.create(:has_many, :clients, nil, { :validate => true }, Firm).validate?
+ assert_predicate ActiveRecord::Reflection.create(:has_one, :client, nil, { validate: true }, Firm), :validate?
+ assert_predicate ActiveRecord::Reflection.create(:belongs_to, :client, nil, { validate: true }, Firm), :validate?
+ assert_predicate ActiveRecord::Reflection.create(:has_many, :clients, nil, { validate: true }, Firm), :validate?
end
def test_validate_association_if_autosave
- assert ActiveRecord::Reflection.create(:has_one, :client, nil, { :autosave => true }, Firm).validate?
- assert ActiveRecord::Reflection.create(:belongs_to, :client, nil, { :autosave => true }, Firm).validate?
- assert ActiveRecord::Reflection.create(:has_many, :clients, nil, { :autosave => true }, Firm).validate?
+ assert_predicate ActiveRecord::Reflection.create(:has_one, :client, nil, { autosave: true }, Firm), :validate?
+ assert_predicate ActiveRecord::Reflection.create(:belongs_to, :client, nil, { autosave: true }, Firm), :validate?
+ assert_predicate ActiveRecord::Reflection.create(:has_many, :clients, nil, { autosave: true }, Firm), :validate?
end
def test_never_validate_association_if_explicit
- assert !ActiveRecord::Reflection.create(:has_one, :client, nil, { :autosave => true, :validate => false }, Firm).validate?
- assert !ActiveRecord::Reflection.create(:belongs_to, :client, nil, { :autosave => true, :validate => false }, Firm).validate?
- assert !ActiveRecord::Reflection.create(:has_many, :clients, nil, { :autosave => true, :validate => false }, Firm).validate?
+ assert_not_predicate ActiveRecord::Reflection.create(:has_one, :client, nil, { autosave: true, validate: false }, Firm), :validate?
+ assert_not_predicate ActiveRecord::Reflection.create(:belongs_to, :client, nil, { autosave: true, validate: false }, Firm), :validate?
+ assert_not_predicate ActiveRecord::Reflection.create(:has_many, :clients, nil, { autosave: true, validate: false }, Firm), :validate?
end
def test_foreign_key
@@ -378,66 +405,75 @@ class ReflectionTest < ActiveRecord::TestCase
assert_equal "category_id", Post.reflect_on_association(:categorizations).foreign_key.to_s
end
- def test_through_reflection_scope_chain_does_not_modify_other_reflections
- orig_conds = 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
- 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
+ error = assert_raises(ArgumentError) do
+ ActiveRecord::Reflection.create(:has_many, :clients, nil, { class_name: Client }, Firm)
+ end
+ assert_equal "A class was passed to `:class_name` but we are expecting a string.", error.message
+ end
+
def test_join_table
- category = Struct.new(:table_name, :pluralize_table_names).new('categories', true)
- product = Struct.new(:table_name, :pluralize_table_names).new('products', true)
+ category = Struct.new(:table_name, :pluralize_table_names).new("categories", true)
+ product = Struct.new(:table_name, :pluralize_table_names).new("products", true)
reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, product)
- reflection.stubs(:klass).returns(category)
- assert_equal 'categories_products', reflection.join_table
+ reflection.stub(:klass, category) do
+ assert_equal "categories_products", reflection.join_table
+ end
reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, {}, category)
- reflection.stubs(:klass).returns(product)
- assert_equal 'categories_products', reflection.join_table
+ reflection.stub(:klass, product) do
+ assert_equal "categories_products", reflection.join_table
+ end
end
def test_join_table_with_common_prefix
- category = Struct.new(:table_name, :pluralize_table_names).new('catalog_categories', true)
- product = Struct.new(:table_name, :pluralize_table_names).new('catalog_products', true)
+ category = Struct.new(:table_name, :pluralize_table_names).new("catalog_categories", true)
+ product = Struct.new(:table_name, :pluralize_table_names).new("catalog_products", true)
reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, product)
- reflection.stubs(:klass).returns(category)
- assert_equal 'catalog_categories_products', reflection.join_table
+ reflection.stub(:klass, category) do
+ assert_equal "catalog_categories_products", reflection.join_table
+ end
reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, {}, category)
- reflection.stubs(:klass).returns(product)
- assert_equal 'catalog_categories_products', reflection.join_table
+ reflection.stub(:klass, product) do
+ assert_equal "catalog_categories_products", reflection.join_table
+ end
end
def test_join_table_with_different_prefix
- category = Struct.new(:table_name, :pluralize_table_names).new('catalog_categories', true)
- page = Struct.new(:table_name, :pluralize_table_names).new('content_pages', true)
+ category = Struct.new(:table_name, :pluralize_table_names).new("catalog_categories", true)
+ page = Struct.new(:table_name, :pluralize_table_names).new("content_pages", true)
reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, page)
- reflection.stubs(:klass).returns(category)
- assert_equal 'catalog_categories_content_pages', reflection.join_table
+ reflection.stub(:klass, category) do
+ assert_equal "catalog_categories_content_pages", reflection.join_table
+ end
reflection = ActiveRecord::Reflection.create(:has_many, :pages, nil, {}, category)
- reflection.stubs(:klass).returns(page)
- assert_equal 'catalog_categories_content_pages', reflection.join_table
+ reflection.stub(:klass, page) do
+ assert_equal "catalog_categories_content_pages", reflection.join_table
+ end
end
def test_join_table_can_be_overridden
- category = Struct.new(:table_name, :pluralize_table_names).new('categories', true)
- product = Struct.new(:table_name, :pluralize_table_names).new('products', true)
+ category = Struct.new(:table_name, :pluralize_table_names).new("categories", true)
+ product = Struct.new(:table_name, :pluralize_table_names).new("products", true)
- reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, { :join_table => 'product_categories' }, product)
- reflection.stubs(:klass).returns(category)
- assert_equal 'product_categories', reflection.join_table
+ reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, { join_table: "product_categories" }, product)
+ reflection.stub(:klass, category) do
+ assert_equal "product_categories", reflection.join_table
+ end
- reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, { :join_table => 'product_categories' }, category)
- reflection.stubs(:klass).returns(product)
- assert_equal 'product_categories', reflection.join_table
+ reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, { join_table: "product_categories" }, category)
+ reflection.stub(:klass, product) do
+ assert_equal "product_categories", reflection.join_table
+ end
end
def test_includes_accepts_symbols
@@ -456,7 +492,7 @@ class ReflectionTest < ActiveRecord::TestCase
department.chefs.create!
assert_nothing_raised do
- assert_equal department.chefs, Hotel.includes(['departments' => 'chefs']).first.chefs
+ assert_equal department.chefs, Hotel.includes(["departments" => "chefs"]).first.chefs
end
end
diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb
index 989f4e1e5d..a8030c2d64 100644
--- a/activerecord/test/cases/relation/delegation_test.rb
+++ b/activerecord/test/cases/relation/delegation_test.rb
@@ -1,38 +1,19 @@
-require 'cases/helper'
-require 'models/post'
-require 'models/comment'
+# frozen_string_literal: true
-module ActiveRecord
- class DelegationTest < ActiveRecord::TestCase
- fixtures :posts
-
- def call_method(target, method)
- method_arity = target.to_a.method(method).arity
-
- if method_arity.zero?
- target.public_send(method)
- elsif method_arity < 0
- if method == :shuffle!
- target.public_send(method)
- else
- target.public_send(method, 1)
- end
- elsif method_arity == 1
- target.public_send(method, 1)
- else
- raise NotImplementedError
- end
- end
- end
+require "cases/helper"
+require "models/post"
+require "models/comment"
- module DelegationWhitelistBlacklistTests
+module ActiveRecord
+ module ArrayDelegationTests
ARRAY_DELEGATES = [
- :+, :-, :|, :&, :[],
+ :+, :-, :|, :&, :[], :shuffle,
:all?, :collect, :compact, :detect, :each, :each_cons, :each_with_index,
: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
+ :map, :none?, :one?, :partition, :reject, :reverse, :rotate,
+ :sample, :second, :sort, :sort_by, :slice, :third, :index, :rindex,
+ :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|
@@ -40,29 +21,65 @@ module ActiveRecord
assert_respond_to target, method
end
end
+ end
+
+ module DeprecatedArelDelegationTests
+ AREL_METHODS = [
+ :with, :orders, :froms, :project, :projections, :taken, :constraints, :exists, :locked, :where_sql,
+ :ast, :source, :join_sources, :to_dot, :create_insert, :create_true, :create_false
+ ]
- ActiveRecord::Delegation::BLACKLISTED_ARRAY_METHODS.each do |method|
- define_method "test_#{method}_is_not_delegated_to_Array" do
- assert_raises(NoMethodError) { call_method(target, method) }
+ def test_deprecate_arel_delegation
+ AREL_METHODS.each do |method|
+ assert_deprecated { target.public_send(method) }
+ assert_deprecated { target.public_send(method) }
end
end
end
- class DelegationAssociationTest < DelegationTest
- include DelegationWhitelistBlacklistTests
+ class DelegationAssociationTest < ActiveRecord::TestCase
+ include ArrayDelegationTests
+ include DeprecatedArelDelegationTests
def target
- Post.first.comments
+ Post.new.comments
end
end
- class DelegationRelationTest < DelegationTest
- include DelegationWhitelistBlacklistTests
-
- fixtures :comments
+ class DelegationRelationTest < ActiveRecord::TestCase
+ include ArrayDelegationTests
+ include DeprecatedArelDelegationTests
def target
Comment.all
end
end
+
+ class QueryingMethodsDelegationTest < ActiveRecord::TestCase
+ QUERYING_METHODS = [
+ :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :none?, :one?,
+ :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, :third_to_last, :third_to_last!, :second_to_last, :second_to_last!,
+ :first_or_create, :first_or_create!, :first_or_initialize,
+ :find_or_create_by, :find_or_create_by!, :create_or_find_by, :create_or_find_by!, :find_or_initialize_by,
+ :find_by, :find_by!,
+ :destroy_all, :delete_all, :update_all,
+ :find_each, :find_in_batches, :in_batches,
+ :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :left_joins, :left_outer_joins, :or,
+ :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :extending,
+ :having, :create_with, :distinct, :references, :none, :unscope, :merge,
+ :count, :average, :minimum, :maximum, :sum, :calculate,
+ :pluck, :pick, :ids,
+ ]
+
+ def test_delegate_querying_methods
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "posts"
+ end
+
+ QUERYING_METHODS.each do |method|
+ assert_respond_to klass.all, method
+ assert_respond_to klass, method
+ end
+ end
+ end
end
diff --git a/activerecord/test/cases/relation/delete_all_test.rb b/activerecord/test/cases/relation/delete_all_test.rb
new file mode 100644
index 0000000000..446d7621ea
--- /dev/null
+++ b/activerecord/test/cases/relation/delete_all_test.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/author"
+require "models/post"
+require "models/pet"
+require "models/toy"
+
+class DeleteAllTest < ActiveRecord::TestCase
+ fixtures :authors, :author_addresses, :posts, :pets, :toys
+
+ def test_destroy_all
+ davids = Author.where(name: "David")
+
+ # Force load
+ assert_equal [authors(:david)], davids.to_a
+ assert_predicate davids, :loaded?
+
+ assert_difference("Author.count", -1) do
+ destroyed = davids.destroy_all
+ assert_equal [authors(:david)], destroyed
+ assert_predicate destroyed.first, :frozen?
+ end
+
+ assert_equal [], davids.to_a
+ assert_predicate davids, :loaded?
+ end
+
+ def test_delete_all
+ davids = Author.where(name: "David")
+
+ assert_difference("Author.count", -1) { davids.delete_all }
+ assert_not_predicate davids, :loaded?
+ end
+
+ def test_delete_all_loaded
+ davids = Author.where(name: "David")
+
+ # Force load
+ assert_equal [authors(:david)], davids.to_a
+ assert_predicate davids, :loaded?
+
+ assert_difference("Author.count", -1) { davids.delete_all }
+
+ assert_equal [], davids.to_a
+ assert_predicate davids, :loaded?
+ end
+
+ def test_delete_all_with_unpermitted_relation_raises_error
+ assert_raises(ActiveRecord::ActiveRecordError) { Author.distinct.delete_all }
+ assert_raises(ActiveRecord::ActiveRecordError) { Author.group(:name).delete_all }
+ assert_raises(ActiveRecord::ActiveRecordError) { Author.having("SUM(id) < 3").delete_all }
+ end
+
+ def test_delete_all_with_joins_and_where_part_is_hash
+ pets = Pet.joins(:toys).where(toys: { name: "Bone" })
+
+ assert_equal true, pets.exists?
+ assert_equal pets.count, pets.delete_all
+ end
+
+ def test_delete_all_with_joins_and_where_part_is_not_hash
+ pets = Pet.joins(:toys).where("toys.name = ?", "Bone")
+
+ assert_equal true, pets.exists?
+ assert_equal pets.count, pets.delete_all
+ end
+
+ def test_delete_all_with_left_joins
+ pets = Pet.left_joins(:toys).where(toys: { name: "Bone" })
+
+ assert_equal true, pets.exists?
+ assert_equal pets.count, pets.delete_all
+ end
+
+ def test_delete_all_with_includes
+ pets = Pet.includes(:toys).where(toys: { name: "Bone" })
+
+ assert_equal true, pets.exists?
+ assert_equal pets.count, pets.delete_all
+ end
+
+ unless current_adapter?(:OracleAdapter)
+ def test_delete_all_with_order_and_limit_deletes_subset_only
+ author = authors(:david)
+ limited_posts = Post.where(author: author).order(:id).limit(1)
+ assert_equal 1, limited_posts.size
+ assert_equal 2, limited_posts.limit(2).size
+ assert_equal 1, limited_posts.delete_all
+ assert_raise(ActiveRecord::RecordNotFound) { posts(:welcome) }
+ assert posts(:thinking)
+ end
+
+ def test_delete_all_with_order_and_limit_and_offset_deletes_subset_only
+ author = authors(:david)
+ limited_posts = Post.where(author: author).order(:id).limit(1).offset(1)
+ assert_equal 1, limited_posts.size
+ assert_equal 2, limited_posts.limit(2).size
+ assert_equal 1, limited_posts.delete_all
+ assert_raise(ActiveRecord::RecordNotFound) { posts(:thinking) }
+ assert posts(:welcome)
+ end
+ end
+end
diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb
index 0a2e874e4f..224e4f39a8 100644
--- a/activerecord/test/cases/relation/merging_test.rb
+++ b/activerecord/test/cases/relation/merging_test.rb
@@ -1,27 +1,29 @@
-require 'cases/helper'
-require 'models/author'
-require 'models/comment'
-require 'models/developer'
-require 'models/computer'
-require 'models/post'
-require 'models/project'
-require 'models/rating'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/author"
+require "models/comment"
+require "models/developer"
+require "models/computer"
+require "models/post"
+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"))
+ devs = Developer.where("salary >= 80000").merge(Developer.limit(2)).merge(Developer.order("id ASC").where("id < 3"))
assert_equal [developers(:david), developers(:jamis)], devs.to_a
- dev_with_count = Developer.limit(1).merge(Developer.order('id DESC')).merge(Developer.select('developers.*'))
+ dev_with_count = Developer.limit(1).merge(Developer.order("id DESC")).merge(Developer.select("developers.*"))
assert_equal [developers(:poor_jamis)], dev_with_count.to_a
end
def test_relation_to_sql
post = Post.first
sql = post.comments.to_sql
- assert_match(/.?post_id.? = #{post.id}\Z/i, sql)
+ assert_match(/.?post_id.? = #{post.id}\z/i, sql)
end
def test_relation_merging_with_arel_equalities_keeps_last_equality
@@ -34,10 +36,10 @@ class RelationMergingTest < ActiveRecord::TestCase
def test_relation_merging_with_arel_equalities_keeps_last_equality_with_non_attribute_left_hand
salary_attr = Developer.arel_table[:salary]
devs = Developer.where(
- Arel::Nodes::NamedFunction.new('abs', [salary_attr]).eq(80000)
+ Arel::Nodes::NamedFunction.new("abs", [salary_attr]).eq(80000)
).merge(
Developer.where(
- Arel::Nodes::NamedFunction.new('abs', [salary_attr]).eq(9000)
+ Arel::Nodes::NamedFunction.new("abs", [salary_attr]).eq(9000)
)
)
assert_equal [developers(:poor_jamis)], devs.to_a
@@ -45,8 +47,8 @@ class RelationMergingTest < ActiveRecord::TestCase
def test_relation_merging_with_eager_load
relations = []
- relations << Post.order('comments.id DESC').merge(Post.eager_load(:last_comment)).merge(Post.all)
- relations << Post.eager_load(:last_comment).merge(Post.order('comments.id DESC')).merge(Post.all)
+ relations << Post.order("comments.id DESC").merge(Post.eager_load(:last_comment)).merge(Post.all)
+ relations << Post.eager_load(:last_comment).merge(Post.order("comments.id DESC")).merge(Post.all)
relations.each do |posts|
post = posts.find { |p| p.id == 1 }
@@ -56,7 +58,7 @@ class RelationMergingTest < ActiveRecord::TestCase
def test_relation_merging_with_locks
devs = Developer.lock.where("salary >= 80000").order("id DESC").merge(Developer.limit(2))
- assert devs.locked.present?
+ assert_predicate devs, :locked?
end
def test_relation_merging_with_preload
@@ -66,29 +68,37 @@ class RelationMergingTest < ActiveRecord::TestCase
end
def test_relation_merging_with_joins
- comments = Comment.joins(:post).where(:body => 'Thank you for the welcome').merge(Post.where(:body => 'Such a lovely day'))
+ comments = Comment.joins(:post).where(body: "Thank you for the welcome").merge(Post.where(body: "Such a lovely day"))
+ assert_equal 1, comments.count
+ end
+
+ def test_relation_merging_with_left_outer_joins
+ comments = Comment.joins(:post).where(body: "Thank you for the welcome").merge(Post.left_outer_joins(:author).where(body: "Such a lovely day"))
+
assert_equal 1, comments.count
end
+ def test_relation_merging_with_skip_query_cache
+ assert_equal Post.all.merge(Post.all.skip_query_cache!).skip_query_cache_value, true
+ end
+
def test_relation_merging_with_association
assert_queries(2) do # one for loading post, and another one merged query
- post = Post.where(:body => 'Such a lovely day').first
- comments = Comment.where(:body => 'Thank you for the welcome').merge(post.comments)
+ post = Post.where(body: "Such a lovely day").first
+ comments = Comment.where(body: "Thank you for the welcome").merge(post.comments)
assert_equal 1, comments.count
end
end
test "merge collapses wheres from the LHS only" do
- left = Post.where(title: "omg").where(comments_count: 1)
+ left = Post.where(title: "omg").where(comments_count: 1)
right = Post.where(title: "wtf").where(title: "bbq")
- expected = [left.bound_attributes[1]] + right.bound_attributes
- merged = left.merge(right)
+ merged = left.merge(right)
- assert_equal expected, merged.bound_attributes
- assert !merged.to_sql.include?("omg")
- assert merged.to_sql.include?("wtf")
- assert merged.to_sql.include?("bbq")
+ assert_not_includes merged.to_sql, "omg"
+ assert_includes merged.to_sql, "wtf"
+ assert_includes merged.to_sql, "bbq"
end
def test_merging_reorders_bind_params
@@ -104,10 +114,31 @@ class RelationMergingTest < ActiveRecord::TestCase
post = PostThatLoadsCommentsInAnAfterSaveHook.create!(title: "First Post", body: "Blah blah blah.")
assert_equal "First comment!", post.comments.where(body: "First comment!").first_or_create.body
end
+
+ def test_merging_with_from_clause
+ relation = Post.all
+ assert_empty relation.from_clause
+ relation = relation.merge(Post.from("posts"))
+ assert_not_empty relation.from_clause
+ end
+
+ def test_merging_with_from_clause_on_different_class
+ assert Comment.joins(:post).merge(Post.from("posts")).first
+ end
+
+ def test_merging_with_order_with_binds
+ relation = Post.all.merge(Post.order([Arel.sql("title LIKE ?"), "%suffix"]))
+ assert_equal ["title LIKE '%suffix'"], relation.order_values
+ end
+
+ def test_merging_with_order_without_binds
+ relation = Post.all.merge(Post.order(Arel.sql("title LIKE '%?'")))
+ assert_equal ["title LIKE '%?'"], relation.order_values
+ end
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).
@@ -136,7 +167,7 @@ class MergingDifferentRelationsTest < ActiveRecord::TestCase
assert_equal ["Mary", "Mary", "Mary", "David"], posts_by_author_name
end
- test "relation merging (using a proc argument)" do
+ test "relation merging (using a proc argument)" do
dev = Developer.where(name: "Jamis").first
comment_1 = dev.comments.create!(body: "I'm Jamis", post: Post.first)
diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb
index ba4d9d2503..f82ecd4449 100644
--- a/activerecord/test/cases/relation/mutation_test.rb
+++ b/activerecord/test/cases/relation/mutation_test.rb
@@ -1,33 +1,10 @@
-require 'cases/helper'
-require 'models/post'
+# frozen_string_literal: true
-module ActiveRecord
- class RelationMutationTest < ActiveSupport::TestCase
- class FakeKlass < Struct.new(:table_name, :name)
- extend ActiveRecord::Delegation::DelegateCache
- inherited self
-
- def connection
- Post.connection
- end
-
- def relation_delegate_class(klass)
- self.class.relation_delegate_class(klass)
- end
-
- def attribute_alias?(name)
- false
- end
-
- def sanitize_sql(sql)
- sql
- end
- end
-
- def relation
- @relation ||= Relation.new FakeKlass.new('posts'), Post.arel_table, Post.predicate_builder
- end
+require "cases/helper"
+require "models/post"
+module ActiveRecord
+ class RelationMutationTest < ActiveRecord::TestCase
(Relation::MULTI_VALUE_METHODS - [:references, :extending, :order, :unscope, :select]).each do |method|
test "##{method}!" do
assert relation.public_send("#{method}!", :foo).equal?(relation)
@@ -36,36 +13,37 @@ module ActiveRecord
end
test "#_select!" do
- assert relation.public_send("_select!", :foo).equal?(relation)
- assert_equal [:foo], relation.public_send("select_values")
+ assert relation._select!(:foo).equal?(relation)
+ assert_equal [:foo], relation.select_values
end
- test '#order!' do
- assert relation.order!('name ASC').equal?(relation)
- assert_equal ['name ASC'], relation.order_values
+ test "#order!" do
+ assert relation.order!("name ASC").equal?(relation)
+ assert_equal ["name ASC"], relation.order_values
end
- test '#order! with symbol prepends the table name' do
+ test "#order! with symbol prepends the table name" do
assert relation.order!(:name).equal?(relation)
node = relation.order_values.first
- assert node.ascending?
+ assert_predicate node, :ascending?
assert_equal :name, node.expr.name
assert_equal "posts", node.expr.relation.name
end
- test '#order! on non-string does not attempt regexp match for references' do
+ test "#order! on non-string does not attempt regexp match for references" do
obj = Object.new
- obj.expects(:=~).never
- assert relation.order!(obj)
- assert_equal [obj], relation.order_values
+ assert_not_called(obj, :=~) do
+ assert relation.order!(obj)
+ assert_equal [obj], relation.order_values
+ end
end
- test '#references!' do
+ test "#references!" do
assert relation.references!(:foo).equal?(relation)
- assert relation.references_values.include?('foo')
+ assert_includes relation.references_values, "foo"
end
- test 'extending!' do
+ test "extending!" do
mod, mod2 = Module.new, Module.new
assert relation.extending!(mod).equal?(relation)
@@ -76,99 +54,97 @@ module ActiveRecord
assert_equal [mod, mod2], relation.extending_values
end
- test 'extending! with empty args' do
+ test "extending! with empty args" do
relation.extending!
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, :skip_query_cache]).each do |method|
test "##{method}!" do
assert relation.public_send("#{method}!", :foo).equal?(relation)
assert_equal :foo, relation.public_send("#{method}_value")
end
end
- test '#from!' do
- assert relation.from!('foo').equal?(relation)
- assert_equal 'foo', relation.from_clause.value
+ test "#from!" do
+ assert relation.from!("foo").equal?(relation)
+ assert_equal "foo", relation.from_clause.value
end
- test '#lock!' do
- assert relation.lock!('foo').equal?(relation)
- assert_equal 'foo', relation.lock_value
+ test "#lock!" do
+ assert relation.lock!("foo").equal?(relation)
+ assert_equal "foo", relation.lock_value
end
- test '#reorder!' do
- @relation = self.relation.order('foo')
+ test "#reorder!" do
+ @relation = relation.order("foo")
- assert relation.reorder!('bar').equal?(relation)
- assert_equal ['bar'], relation.order_values
+ assert relation.reorder!("bar").equal?(relation)
+ assert_equal ["bar"], relation.order_values
assert relation.reordering_value
end
- test '#reorder! with symbol prepends the table name' do
+ test "#reorder! with symbol prepends the table name" do
assert relation.reorder!(:name).equal?(relation)
node = relation.order_values.first
- assert node.ascending?
+ assert_predicate node, :ascending?
assert_equal :name, node.expr.name
assert_equal "posts", node.expr.relation.name
end
- test 'reverse_order!' do
- @relation = Post.order('title ASC, comments_count DESC')
+ test "reverse_order!" do
+ @relation = Post.order("title ASC, comments_count DESC")
relation.reverse_order!
- assert_equal 'title DESC', relation.order_values.first
- assert_equal 'comments_count ASC', relation.order_values.last
-
+ assert_equal "title DESC", relation.order_values.first
+ assert_equal "comments_count ASC", relation.order_values.last
relation.reverse_order!
- assert_equal 'title ASC', relation.order_values.first
- assert_equal 'comments_count DESC', relation.order_values.last
+ assert_equal "title ASC", relation.order_values.first
+ assert_equal "comments_count DESC", relation.order_values.last
end
- test 'create_with!' do
- assert relation.create_with!(foo: 'bar').equal?(relation)
- assert_equal({foo: 'bar'}, relation.create_with_value)
+ test "create_with!" do
+ assert relation.create_with!(foo: "bar").equal?(relation)
+ assert_equal({ foo: "bar" }, relation.create_with_value)
end
- test 'test_merge!' do
+ test "merge!" do
assert relation.merge!(select: :foo).equal?(relation)
assert_equal [:foo], relation.select_values
end
- test 'merge with a proc' do
+ test "merge with a proc" do
assert_equal [:foo], relation.merge(-> { select(:foo) }).select_values
end
- test 'none!' do
+ test "none!" do
assert relation.none!.equal?(relation)
assert_equal [NullRelation], relation.extending_values
assert relation.is_a?(NullRelation)
end
- test 'distinct!' do
+ test "distinct!" do
relation.distinct! :foo
assert_equal :foo, relation.distinct_value
+ end
- assert_deprecated do
- assert_equal :foo, relation.uniq_value # deprecated access
- end
+ test "skip_query_cache!" do
+ relation.skip_query_cache!
+ assert relation.skip_query_cache_value
end
- test 'uniq! was replaced by distinct!' do
- assert_deprecated(/use distinct! instead/) do
- relation.uniq! :foo
- end
+ test "skip_preloading!" do
+ relation.skip_preloading!
+ assert relation.skip_preloading_value
+ end
- assert_deprecated(/use distinct_value instead/) do
- assert_equal :foo, relation.uniq_value # deprecated access
+ private
+ def relation
+ @relation ||= Relation.new(FakeKlass)
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 2006fc9611..065819e0f1 100644
--- a/activerecord/test/cases/relation/or_test.rb
+++ b/activerecord/test/cases/relation/or_test.rb
@@ -1,32 +1,37 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/post'
+require "models/author"
+require "models/categorization"
+require "models/post"
module ActiveRecord
class OrTest < ActiveRecord::TestCase
fixtures :posts
+ fixtures :authors, :author_addresses
def test_or_with_relation
- expected = Post.where('id = 1 or id = 2').to_a
- assert_equal expected, Post.where('id = 1').or(Post.where('id = 2')).to_a
+ expected = Post.where("id = 1 or id = 2").to_a
+ assert_equal expected, Post.where("id = 1").or(Post.where("id = 2")).to_a
end
def test_or_identity
- expected = Post.where('id = 1').to_a
- assert_equal expected, Post.where('id = 1').or(Post.where('id = 1')).to_a
+ expected = Post.where("id = 1").to_a
+ assert_equal expected, Post.where("id = 1").or(Post.where("id = 1")).to_a
end
def test_or_with_null_left
- expected = Post.where('id = 1').to_a
- assert_equal expected, Post.none.or(Post.where('id = 1')).to_a
+ expected = Post.where("id = 1").to_a
+ assert_equal expected, Post.none.or(Post.where("id = 1")).to_a
end
def test_or_with_null_right
- expected = Post.where('id = 1').to_a
- assert_equal expected, Post.where('id = 1').or(Post.none).to_a
+ expected = Post.where("id = 1").to_a
+ assert_equal expected, Post.where("id = 1").or(Post.none).to_a
end
def test_or_with_bind_params
- assert_equal Post.find([1, 2]), Post.where(id: 1).or(Post.where(id: 2)).to_a
+ assert_equal Post.find([1, 2]).sort_by(&:id), Post.where(id: 1).or(Post.where(id: 2)).sort_by(&:id)
end
def test_or_with_null_both
@@ -36,49 +41,97 @@ module ActiveRecord
def test_or_without_left_where
expected = Post.all
- assert_equal expected, Post.or(Post.where('id = 1')).to_a
+ assert_equal expected, Post.or(Post.where("id = 1")).to_a
end
def test_or_without_right_where
expected = Post.all
- assert_equal expected, Post.where('id = 1').or(Post.all).to_a
+ assert_equal expected, Post.where("id = 1").or(Post.all).to_a
end
def test_or_preserves_other_querying_methods
- expected = Post.where('id = 1 or id = 2 or id = 3').order('body asc').to_a
- partial = Post.order('body asc')
- assert_equal expected, partial.where('id = 1').or(partial.where(:id => [2, 3])).to_a
- assert_equal expected, Post.order('body asc').where('id = 1').or(Post.order('body asc').where(:id => [2, 3])).to_a
+ expected = Post.where("id = 1 or id = 2 or id = 3").order("body asc").to_a
+ partial = Post.order("body asc")
+ assert_equal expected, partial.where("id = 1").or(partial.where(id: [2, 3])).to_a
+ assert_equal expected, Post.order("body asc").where("id = 1").or(Post.order("body asc").where(id: [2, 3])).to_a
end
def test_or_with_incompatible_relations
- assert_raises ArgumentError do
- Post.order('body asc').where('id = 1').or(Post.order('id desc').where(:id => [2, 3])).to_a
+ error = assert_raises ArgumentError do
+ Post.order("body asc").where("id = 1").or(Post.order("id desc").where(id: [2, 3])).to_a
+ end
+
+ assert_equal "Relation passed to #or must be structurally compatible. Incompatible values: [:order]", error.message
+ end
+
+ def test_or_with_unscope_where
+ expected = Post.where("id = 1 or id = 2")
+ partial = Post.where("id = 1 and id != 2")
+ assert_equal expected, partial.or(partial.unscope(:where).where("id = 2")).to_a
+ end
+
+ def test_or_with_unscope_where_column
+ expected = Post.where("id = 1 or id = 2")
+ partial = Post.where(id: 1).where.not(id: 2)
+ assert_equal expected, partial.or(partial.unscope(where: :id).where("id = 2")).to_a
+ end
+
+ def test_or_with_unscope_order
+ expected = Post.where("id = 1 or id = 2")
+ assert_equal expected, Post.order("body asc").where("id = 1").unscope(:order).or(Post.where("id = 2")).to_a
+ end
+
+ def test_or_with_incompatible_unscope
+ error = assert_raises ArgumentError do
+ Post.order("body asc").where("id = 1").or(Post.order("body asc").where("id = 2").unscope(:order)).to_a
end
+
+ assert_equal "Relation passed to #or must be structurally compatible. Incompatible values: [:order]", error.message
end
def test_or_when_grouping
- groups = Post.where('id < 10').group('body').select('body, COUNT(*) AS c')
- expected = groups.having("COUNT(*) > 1 OR body like 'Such%'").to_a.map {|o| [o.body, o.c] }
- assert_equal expected, groups.having('COUNT(*) > 1').or(groups.having("body like 'Such%'")).to_a.map {|o| [o.body, o.c] }
+ groups = Post.where("id < 10").group("body").select("body, COUNT(*) AS c")
+ expected = groups.having("COUNT(*) > 1 OR body like 'Such%'").to_a.map { |o| [o.body, o.c] }
+ assert_equal expected, groups.having("COUNT(*) > 1").or(groups.having("body like 'Such%'")).to_a.map { |o| [o.body, o.c] }
end
def test_or_with_named_scope
expected = Post.where("id = 1 or body LIKE '\%a\%'").to_a
- assert_equal expected, Post.where('id = 1').or(Post.containing_the_letter_a)
+ assert_equal expected, Post.where("id = 1").or(Post.containing_the_letter_a)
end
def test_or_inside_named_scope
- expected = Post.where("body LIKE '\%a\%' OR title LIKE ?", "%'%").order('id DESC').to_a
+ expected = Post.where("body LIKE '\%a\%' OR title LIKE ?", "%'%").order("id DESC").to_a
assert_equal expected, Post.order(id: :desc).typographically_interesting
end
def test_or_on_loaded_relation
- expected = Post.where('id = 1 or id = 2').to_a
- p = Post.where('id = 1')
+ expected = Post.where("id = 1 or id = 2").to_a
+ p = Post.where("id = 1")
p.load
- assert_equal p.loaded?, true
- assert_equal expected, p.or(Post.where('id = 2')).to_a
+ assert_equal true, p.loaded?
+ assert_equal expected, p.or(Post.where("id = 2")).to_a
+ end
+
+ def test_or_with_non_relation_object_raises_error
+ assert_raises ArgumentError do
+ Post.where(id: [1, 2, 3]).or(title: "Rails")
+ end
+ end
+
+ def test_or_with_references_inequality
+ joined = Post.includes(:author)
+ actual = joined.where(authors: { id: 1 })
+ .or(joined.where(title: "I don't have any comments"))
+ expected = Author.find(1).posts + Post.where(title: "I don't have any comments")
+ assert_equal expected.sort_by(&:id), actual.sort_by(&:id)
+ end
+
+ def test_or_with_scope_on_association
+ author = Author.first
+ assert_nothing_raised do
+ author.top_posts.or(author.other_top_posts)
+ end
end
end
end
diff --git a/activerecord/test/cases/relation/predicate_builder_test.rb b/activerecord/test/cases/relation/predicate_builder_test.rb
index 8f62014622..b432330deb 100644
--- a/activerecord/test/cases/relation/predicate_builder_test.rb
+++ b/activerecord/test/cases/relation/predicate_builder_test.rb
@@ -1,11 +1,13 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/topic'
+require "models/topic"
module ActiveRecord
class PredicateBuilderTest < ActiveRecord::TestCase
def test_registering_new_handlers
Topic.predicate_builder.register_handler(Regexp, proc do |column, value|
- Arel::Nodes::InfixOperation.new('~', column, Arel.sql(value.source))
+ Arel::Nodes::InfixOperation.new("~", column, Arel.sql(value.source))
end)
assert_match %r{["`]topics["`]\.["`]title["`] ~ rails}i, Topic.where(title: /rails/).to_sql
diff --git a/activerecord/test/cases/relation/record_fetch_warning_test.rb b/activerecord/test/cases/relation/record_fetch_warning_test.rb
index 62f0a7cc49..22d32d75bc 100644
--- a/activerecord/test/cases/relation/record_fetch_warning_test.rb
+++ b/activerecord/test/cases/relation/record_fetch_warning_test.rb
@@ -1,28 +1,42 @@
-require 'cases/helper'
-require 'models/post'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/post"
+require "active_record/relation/record_fetch_warning"
module ActiveRecord
class RecordFetchWarningTest < ActiveRecord::TestCase
fixtures :posts
- def test_warn_on_records_fetched_greater_than
- original_logger = ActiveRecord::Base.logger
- orginal_warn_on_records_fetched_greater_than = ActiveRecord::Base.warn_on_records_fetched_greater_than
+ def setup
+ @original_logger = ActiveRecord::Base.logger
+ @original_warn_on_records_fetched_greater_than = ActiveRecord::Base.warn_on_records_fetched_greater_than
+ @log = StringIO.new
+ end
- log = StringIO.new
- ActiveRecord::Base.logger = ActiveSupport::Logger.new(log)
+ def teardown
+ ActiveRecord::Base.logger = @original_logger
+ ActiveRecord::Base.warn_on_records_fetched_greater_than = @original_warn_on_records_fetched_greater_than
+ end
+
+ def test_warn_on_records_fetched_greater_than_allowed_limit
+ ActiveRecord::Base.logger = ActiveSupport::Logger.new(@log)
ActiveRecord::Base.logger.level = Logger::WARN
+ ActiveRecord::Base.warn_on_records_fetched_greater_than = 1
- require 'active_record/relation/record_fetch_warning'
+ Post.all.to_a
- ActiveRecord::Base.warn_on_records_fetched_greater_than = 1
+ assert_match(/Query fetched/, @log.string)
+ end
+
+ def test_does_not_warn_on_records_fetched_less_than_allowed_limit
+ ActiveRecord::Base.logger = ActiveSupport::Logger.new(@log)
+ ActiveRecord::Base.logger.level = Logger::WARN
+ ActiveRecord::Base.warn_on_records_fetched_greater_than = 100
Post.all.to_a
- assert_match(/Query fetched/, log.string)
- ensure
- ActiveRecord::Base.logger = original_logger
- ActiveRecord::Base.warn_on_records_fetched_greater_than = orginal_warn_on_records_fetched_greater_than
+ assert_no_match(/Query fetched/, @log.string)
end
end
end
diff --git a/activerecord/test/cases/relation/select_test.rb b/activerecord/test/cases/relation/select_test.rb
new file mode 100644
index 0000000000..dec8a6925d
--- /dev/null
+++ b/activerecord/test/cases/relation/select_test.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/post"
+
+module ActiveRecord
+ class SelectTest < ActiveRecord::TestCase
+ fixtures :posts
+
+ def test_select_with_nil_argument
+ expected = Post.select(:title).to_sql
+ assert_equal expected, Post.select(nil).select(:title).to_sql
+ end
+ end
+end
diff --git a/activerecord/test/cases/relation/update_all_test.rb b/activerecord/test/cases/relation/update_all_test.rb
new file mode 100644
index 0000000000..09c365f31b
--- /dev/null
+++ b/activerecord/test/cases/relation/update_all_test.rb
@@ -0,0 +1,239 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/author"
+require "models/category"
+require "models/comment"
+require "models/computer"
+require "models/developer"
+require "models/post"
+require "models/person"
+require "models/pet"
+require "models/toy"
+require "models/topic"
+require "models/tag"
+require "models/tagging"
+require "models/warehouse_thing"
+
+class UpdateAllTest < ActiveRecord::TestCase
+ fixtures :authors, :author_addresses, :comments, :developers, :posts, :people, :pets, :toys, :tags, :taggings, "warehouse-things"
+
+ class TopicWithCallbacks < ActiveRecord::Base
+ self.table_name = :topics
+ cattr_accessor :topic_count
+ before_update { |topic| topic.author_name = "David" if topic.author_name.blank? }
+ after_update { |topic| topic.class.topic_count = topic.class.count }
+ end
+
+ def test_update_all_with_scope
+ tag = Tag.first
+ Post.tagged_with(tag.id).update_all(title: "rofl")
+ posts = Post.tagged_with(tag.id).all.to_a
+ assert_operator posts.length, :>, 0
+ posts.each { |post| assert_equal "rofl", post.title }
+ 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
+ end
+
+ def test_update_all_with_blank_argument
+ assert_raises(ArgumentError) { Comment.update_all({}) }
+ end
+
+ def test_update_all_with_joins
+ pets = Pet.joins(:toys).where(toys: { name: "Bone" })
+
+ assert_equal true, pets.exists?
+ assert_equal pets.count, pets.update_all(name: "Bob")
+ end
+
+ def test_update_all_with_left_joins
+ pets = Pet.left_joins(:toys).where(toys: { name: "Bone" })
+
+ assert_equal true, pets.exists?
+ assert_equal pets.count, pets.update_all(name: "Bob")
+ end
+
+ def test_update_all_with_includes
+ pets = Pet.includes(:toys).where(toys: { name: "Bone" })
+
+ assert_equal true, pets.exists?
+ assert_equal pets.count, pets.update_all(name: "Bob")
+ end
+
+ def test_update_all_with_joins_and_limit
+ comments = Comment.joins(:post).where("posts.id" => posts(:welcome).id).limit(1)
+ assert_equal 1, comments.update_all(post_id: posts(:thinking).id)
+ assert_equal posts(:thinking), comments(:greetings).post
+ end
+
+ def test_update_all_with_joins_and_limit_and_order
+ comments = Comment.joins(:post).where("posts.id" => posts(:welcome).id).order("comments.id").limit(1)
+ assert_equal 1, comments.update_all(post_id: posts(:thinking).id)
+ assert_equal posts(:thinking), comments(:greetings).post
+ assert_equal posts(:welcome), comments(:more_greetings).post
+ end
+
+ def test_update_all_with_joins_and_offset
+ all_comments = Comment.joins(:post).where("posts.id" => posts(:welcome).id)
+ count = all_comments.count
+ comments = all_comments.offset(1)
+
+ assert_equal count - 1, comments.update_all(post_id: posts(:thinking).id)
+ end
+
+ def test_update_all_with_joins_and_offset_and_order
+ all_comments = Comment.joins(:post).where("posts.id" => posts(:welcome).id).order("posts.id", "comments.id")
+ count = all_comments.count
+ comments = all_comments.offset(1)
+
+ assert_equal count - 1, comments.update_all(post_id: posts(:thinking).id)
+ assert_equal posts(:thinking), comments(:more_greetings).post
+ assert_equal posts(:welcome), comments(:greetings).post
+ end
+
+ def test_update_counters_with_joins
+ assert_nil pets(:parrot).integer
+
+ Pet.joins(:toys).where(toys: { name: "Bone" }).update_counters(integer: 1)
+
+ assert_equal 1, pets(:parrot).reload.integer
+ end
+
+ def test_touch_all_updates_records_timestamps
+ david = developers(:david)
+ david_previously_updated_at = david.updated_at
+ jamis = developers(:jamis)
+ jamis_previously_updated_at = jamis.updated_at
+ Developer.where(name: "David").touch_all
+
+ assert_not_equal david_previously_updated_at, david.reload.updated_at
+ assert_equal jamis_previously_updated_at, jamis.reload.updated_at
+ end
+
+ def test_touch_all_with_custom_timestamp
+ developer = developers(:david)
+ previously_created_at = developer.created_at
+ previously_updated_at = developer.updated_at
+ Developer.where(name: "David").touch_all(:created_at)
+ developer.reload
+
+ assert_not_equal previously_created_at, developer.created_at
+ assert_not_equal previously_updated_at, developer.updated_at
+ end
+
+ def test_touch_all_with_given_time
+ developer = developers(:david)
+ previously_created_at = developer.created_at
+ previously_updated_at = developer.updated_at
+ new_time = Time.utc(2015, 2, 16, 4, 54, 0)
+ Developer.where(name: "David").touch_all(:created_at, time: new_time)
+ developer.reload
+
+ assert_not_equal previously_created_at, developer.created_at
+ assert_not_equal previously_updated_at, developer.updated_at
+ assert_equal new_time, developer.created_at
+ assert_equal new_time, developer.updated_at
+ end
+
+ def test_touch_all_updates_locking_column
+ person = people(:david)
+
+ assert_difference -> { person.reload.lock_version }, +1 do
+ Person.where(first_name: "David").touch_all
+ end
+ end
+
+ def test_update_on_relation
+ topic1 = TopicWithCallbacks.create! title: "arel", author_name: nil
+ topic2 = TopicWithCallbacks.create! title: "activerecord", author_name: nil
+ topics = TopicWithCallbacks.where(id: [topic1.id, topic2.id])
+ topics.update(title: "adequaterecord")
+
+ assert_equal TopicWithCallbacks.count, TopicWithCallbacks.topic_count
+
+ assert_equal "adequaterecord", topic1.reload.title
+ assert_equal "adequaterecord", topic2.reload.title
+ # Testing that the before_update callbacks have run
+ assert_equal "David", topic1.reload.author_name
+ assert_equal "David", topic2.reload.author_name
+ end
+
+ def test_update_with_ids_on_relation
+ topic1 = TopicWithCallbacks.create!(title: "arel", author_name: nil)
+ topic2 = TopicWithCallbacks.create!(title: "activerecord", author_name: nil)
+ topics = TopicWithCallbacks.none
+ topics.update(
+ [topic1.id, topic2.id],
+ [{ title: "adequaterecord" }, { title: "adequaterecord" }]
+ )
+
+ assert_equal TopicWithCallbacks.count, TopicWithCallbacks.topic_count
+
+ assert_equal "adequaterecord", topic1.reload.title
+ assert_equal "adequaterecord", topic2.reload.title
+ # Testing that the before_update callbacks have run
+ assert_equal "David", topic1.reload.author_name
+ assert_equal "David", topic2.reload.author_name
+ end
+
+ def test_update_on_relation_passing_active_record_object_is_not_permitted
+ topic = Topic.create!(title: "Foo", author_name: nil)
+ assert_raises(ArgumentError) do
+ Topic.where(id: topic.id).update(topic, title: "Bar")
+ end
+ end
+
+ # Oracle UPDATE does not support ORDER BY
+ unless current_adapter?(:OracleAdapter)
+ def test_update_all_ignores_order_without_limit_from_association
+ author = authors(:david)
+ assert_nothing_raised do
+ assert_equal author.posts_with_comments_and_categories.length, author.posts_with_comments_and_categories.update_all([ "body = ?", "bulk update!" ])
+ end
+ end
+
+ def test_update_all_doesnt_ignore_order
+ assert_equal authors(:david).id + 1, authors(:mary).id # make sure there is going to be a duplicate PK error
+ test_update_with_order_succeeds = lambda do |order|
+ begin
+ Author.order(order).update_all("id = id + 1")
+ rescue ActiveRecord::ActiveRecordError
+ false
+ end
+ end
+
+ if test_update_with_order_succeeds.call("id DESC")
+ # test that this wasn't a fluke and using an incorrect order results in an exception
+ assert_not test_update_with_order_succeeds.call("id ASC")
+ else
+ # test that we're failing because the current Arel's engine doesn't support UPDATE ORDER BY queries is using subselects instead
+ assert_sql(/\AUPDATE .+ \(SELECT .* ORDER BY id DESC\)\z/i) do
+ test_update_with_order_succeeds.call("id DESC")
+ end
+ end
+ end
+
+ def test_update_all_with_order_and_limit_updates_subset_only
+ author = authors(:david)
+ limited_posts = author.posts_sorted_by_id_limited
+ assert_equal 1, limited_posts.size
+ assert_equal 2, limited_posts.limit(2).size
+ assert_equal 1, limited_posts.update_all([ "body = ?", "bulk update!" ])
+ assert_equal "bulk update!", posts(:welcome).body
+ assert_not_equal "bulk update!", posts(:thinking).body
+ end
+
+ def test_update_all_with_order_and_limit_and_offset_updates_subset_only
+ author = authors(:david)
+ limited_posts = author.posts_sorted_by_id_limited.offset(1)
+ assert_equal 1, limited_posts.size
+ assert_equal 2, limited_posts.limit(2).size
+ assert_equal 1, limited_posts.update_all([ "body = ?", "bulk update!" ])
+ assert_equal "bulk update!", posts(:thinking).body
+ assert_not_equal "bulk update!", posts(:welcome).body
+ end
+ end
+end
diff --git a/activerecord/test/cases/relation/where_chain_test.rb b/activerecord/test/cases/relation/where_chain_test.rb
index 27bbd80f79..a68eb2b446 100644
--- a/activerecord/test/cases/relation/where_chain_test.rb
+++ b/activerecord/test/cases/relation/where_chain_test.rb
@@ -1,6 +1,8 @@
-require 'cases/helper'
-require 'models/post'
-require 'models/comment'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/post"
+require "models/comment"
module ActiveRecord
class WhereChainTest < ActiveRecord::TestCase
@@ -8,12 +10,12 @@ module ActiveRecord
def setup
super
- @name = 'title'
+ @name = "title"
end
def test_not_inverts_where_clause
- relation = Post.where.not(title: 'hello')
- expected_where_clause = Post.where(title: 'hello').where_clause.invert
+ relation = Post.where.not(title: "hello")
+ expected_where_clause = Post.where(title: "hello").where_clause.invert
assert_equal expected_where_clause, relation.where_clause
end
@@ -25,55 +27,55 @@ module ActiveRecord
end
def test_association_not_eq
- expected = Arel::Nodes::Grouping.new(Comment.arel_table[@name].not_eq(Arel::Nodes::BindParam.new))
- relation = Post.joins(:comments).where.not(comments: {title: 'hello'})
+ expected = Comment.arel_table[@name].not_eq(Arel::Nodes::BindParam.new(1))
+ relation = Post.joins(:comments).where.not(comments: { title: "hello" })
assert_equal(expected.to_sql, relation.where_clause.ast.to_sql)
end
def test_not_eq_with_preceding_where
- relation = Post.where(title: 'hello').where.not(title: 'world')
+ relation = Post.where(title: "hello").where.not(title: "world")
expected_where_clause =
- Post.where(title: 'hello').where_clause +
- Post.where(title: 'world').where_clause.invert
+ Post.where(title: "hello").where_clause +
+ Post.where(title: "world").where_clause.invert
assert_equal expected_where_clause, relation.where_clause
end
def test_not_eq_with_succeeding_where
- relation = Post.where.not(title: 'hello').where(title: 'world')
+ relation = Post.where.not(title: "hello").where(title: "world")
expected_where_clause =
- Post.where(title: 'hello').where_clause.invert +
- Post.where(title: 'world').where_clause
+ Post.where(title: "hello").where_clause.invert +
+ Post.where(title: "world").where_clause
assert_equal expected_where_clause, relation.where_clause
end
def test_chaining_multiple
- relation = Post.where.not(author_id: [1, 2]).where.not(title: 'ruby on rails')
+ relation = Post.where.not(author_id: [1, 2]).where.not(title: "ruby on rails")
expected_where_clause =
Post.where(author_id: [1, 2]).where_clause.invert +
- Post.where(title: 'ruby on rails').where_clause.invert
+ Post.where(title: "ruby on rails").where_clause.invert
assert_equal expected_where_clause, relation.where_clause
end
def test_rewhere_with_one_condition
- relation = Post.where(title: 'hello').where(title: 'world').rewhere(title: 'alone')
- expected = Post.where(title: 'alone')
+ relation = Post.where(title: "hello").where(title: "world").rewhere(title: "alone")
+ expected = Post.where(title: "alone")
assert_equal expected.where_clause, relation.where_clause
end
def test_rewhere_with_multiple_overwriting_conditions
- relation = Post.where(title: 'hello').where(body: 'world').rewhere(title: 'alone', body: 'again')
- expected = Post.where(title: 'alone', body: 'again')
+ relation = Post.where(title: "hello").where(body: "world").rewhere(title: "alone", body: "again")
+ expected = Post.where(title: "alone", body: "again")
assert_equal expected.where_clause, relation.where_clause
end
def test_rewhere_with_one_overwriting_condition_and_one_unrelated
- relation = Post.where(title: 'hello').where(body: 'world').rewhere(title: 'alone')
- expected = Post.where(body: 'world', title: 'alone')
+ relation = Post.where(title: "hello").where(body: "world").rewhere(title: "alone")
+ expected = Post.where(body: "world", title: "alone")
assert_equal expected.where_clause, relation.where_clause
end
diff --git a/activerecord/test/cases/relation/where_clause_test.rb b/activerecord/test/cases/relation/where_clause_test.rb
index c20ed94d90..0b06cec40b 100644
--- a/activerecord/test/cases/relation/where_clause_test.rb
+++ b/activerecord/test/cases/relation/where_clause_test.rb
@@ -1,78 +1,86 @@
+# frozen_string_literal: true
+
require "cases/helper"
class ActiveRecord::Relation
class WhereClauseTest < ActiveRecord::TestCase
test "+ combines two where clauses" do
- first_clause = WhereClause.new([table["id"].eq(bind_param)], [["id", 1]])
- second_clause = WhereClause.new([table["name"].eq(bind_param)], [["name", "Sean"]])
+ first_clause = WhereClause.new([table["id"].eq(bind_param(1))])
+ second_clause = WhereClause.new([table["name"].eq(bind_param("Sean"))])
combined = WhereClause.new(
- [table["id"].eq(bind_param), table["name"].eq(bind_param)],
- [["id", 1], ["name", "Sean"]],
+ [table["id"].eq(bind_param(1)), table["name"].eq(bind_param("Sean"))],
)
assert_equal combined, first_clause + second_clause
end
test "+ is associative, but not commutative" do
- a = WhereClause.new(["a"], ["bind a"])
- b = WhereClause.new(["b"], ["bind b"])
- c = WhereClause.new(["c"], ["bind c"])
+ a = WhereClause.new(["a"])
+ b = WhereClause.new(["b"])
+ c = WhereClause.new(["c"])
assert_equal a + (b + c), (a + b) + c
assert_not_equal a + b, b + a
end
test "an empty where clause is the identity value for +" do
- clause = WhereClause.new([table["id"].eq(bind_param)], [["id", 1]])
+ clause = WhereClause.new([table["id"].eq(bind_param(1))])
assert_equal clause, clause + WhereClause.empty
end
test "merge combines two where clauses" do
- a = WhereClause.new([table["id"].eq(1)], [])
- b = WhereClause.new([table["name"].eq("Sean")], [])
- expected = WhereClause.new([table["id"].eq(1), table["name"].eq("Sean")], [])
+ a = WhereClause.new([table["id"].eq(1)])
+ b = WhereClause.new([table["name"].eq("Sean")])
+ expected = WhereClause.new([table["id"].eq(1), table["name"].eq("Sean")])
assert_equal expected, a.merge(b)
end
test "merge keeps the right side, when two equality clauses reference the same column" do
- a = WhereClause.new([table["id"].eq(1), table["name"].eq("Sean")], [])
- b = WhereClause.new([table["name"].eq("Jim")], [])
- expected = WhereClause.new([table["id"].eq(1), table["name"].eq("Jim")], [])
+ a = WhereClause.new([table["id"].eq(1), table["name"].eq("Sean")])
+ b = WhereClause.new([table["name"].eq("Jim")])
+ expected = WhereClause.new([table["id"].eq(1), table["name"].eq("Jim")])
assert_equal expected, a.merge(b)
end
test "merge removes bind parameters matching overlapping equality clauses" do
a = WhereClause.new(
- [table["id"].eq(bind_param), table["name"].eq(bind_param)],
- [attribute("id", 1), attribute("name", "Sean")],
+ [table["id"].eq(bind_param(1)), table["name"].eq(bind_param("Sean"))],
)
b = WhereClause.new(
- [table["name"].eq(bind_param)],
- [attribute("name", "Jim")]
+ [table["name"].eq(bind_param("Jim"))],
)
expected = WhereClause.new(
- [table["id"].eq(bind_param), table["name"].eq(bind_param)],
- [attribute("id", 1), attribute("name", "Jim")],
+ [table["id"].eq(bind_param(1)), table["name"].eq(bind_param("Jim"))],
)
assert_equal expected, a.merge(b)
end
test "merge allows for columns with the same name from different tables" do
- skip "This is not possible as of 4.2, and the binds do not yet contain sufficient information for this to happen"
- # We might be able to change the implementation to remove conflicts by index, rather than column name
+ table2 = Arel::Table.new("table2")
+ a = WhereClause.new(
+ [table["id"].eq(bind_param(1)), table2["id"].eq(bind_param(2))],
+ )
+ b = WhereClause.new(
+ [table["id"].eq(bind_param(3))],
+ )
+ expected = WhereClause.new(
+ [table2["id"].eq(bind_param(2)), table["id"].eq(bind_param(3))],
+ )
+
+ assert_equal expected, a.merge(b)
end
test "a clause knows if it is empty" do
- assert WhereClause.empty.empty?
- assert_not WhereClause.new(["anything"], []).empty?
+ assert_empty WhereClause.empty
+ assert_not_empty WhereClause.new(["anything"])
end
test "invert cannot handle nil" do
- where_clause = WhereClause.new([nil], [])
+ where_clause = WhereClause.new([nil])
assert_raises ArgumentError do
where_clause.invert
@@ -84,39 +92,53 @@ class ActiveRecord::Relation
original = WhereClause.new([
table["id"].in([1, 2, 3]),
table["id"].eq(1),
+ table["id"].is_not_distinct_from(1),
+ table["id"].is_distinct_from(2),
"sql literal",
random_object
- ], [])
+ ])
expected = WhereClause.new([
table["id"].not_in([1, 2, 3]),
table["id"].not_eq(1),
+ table["id"].is_distinct_from(1),
+ table["id"].is_not_distinct_from(2),
Arel::Nodes::Not.new(Arel::Nodes::SqlLiteral.new("sql literal")),
Arel::Nodes::Not.new(random_object)
- ], [])
+ ])
assert_equal expected, original.invert
end
- test "accept removes binary predicates referencing a given column" do
+ test "except removes binary predicates referencing a given column" do
where_clause = WhereClause.new([
table["id"].in([1, 2, 3]),
- table["name"].eq(bind_param),
- table["age"].gteq(bind_param),
- ], [
- attribute("name", "Sean"),
- attribute("age", 30),
+ table["name"].eq(bind_param("Sean")),
+ table["age"].gteq(bind_param(30)),
])
- expected = WhereClause.new([table["age"].gteq(bind_param)], [attribute("age", 30)])
+ expected = WhereClause.new([table["age"].gteq(bind_param(30))])
assert_equal expected, where_clause.except("id", "name")
end
+ test "except jumps over unhandled binds (like with OR) correctly" do
+ wcs = (0..9).map do |i|
+ WhereClause.new([table["id#{i}"].eq(bind_param(i))])
+ end
+
+ wc = wcs[0] + wcs[1] + wcs[2].or(wcs[3]) + wcs[4] + wcs[5] + wcs[6].or(wcs[7]) + wcs[8] + wcs[9]
+
+ expected = wcs[0] + wcs[2].or(wcs[3]) + wcs[5] + wcs[6].or(wcs[7]) + wcs[9]
+ actual = wc.except("id1", "id2", "id4", "id7", "id8")
+
+ assert_equal expected, actual
+ end
+
test "ast groups its predicates with AND" do
predicates = [
table["id"].in([1, 2, 3]),
- table["name"].eq(bind_param),
+ table["name"].eq(bind_param(nil)),
]
- where_clause = WhereClause.new(predicates, [])
+ where_clause = WhereClause.new(predicates)
expected = Arel::Nodes::And.new(predicates)
assert_equal expected, where_clause.ast
@@ -128,55 +150,96 @@ class ActiveRecord::Relation
table["id"].in([1, 2, 3]),
"foo = bar",
random_object,
- ], [])
+ ])
expected = Arel::Nodes::And.new([
table["id"].in([1, 2, 3]),
Arel::Nodes::Grouping.new(Arel.sql("foo = bar")),
- Arel::Nodes::Grouping.new(random_object),
+ random_object,
])
assert_equal expected, where_clause.ast
end
test "ast removes any empty strings" do
- where_clause = WhereClause.new([table["id"].in([1, 2, 3])], [])
- where_clause_with_empty = WhereClause.new([table["id"].in([1, 2, 3]), ''], [])
+ where_clause = WhereClause.new([table["id"].in([1, 2, 3])])
+ where_clause_with_empty = WhereClause.new([table["id"].in([1, 2, 3]), ""])
assert_equal where_clause.ast, where_clause_with_empty.ast
end
test "or joins the two clauses using OR" do
- where_clause = WhereClause.new([table["id"].eq(bind_param)], [attribute("id", 1)])
- other_clause = WhereClause.new([table["name"].eq(bind_param)], [attribute("name", "Sean")])
+ where_clause = WhereClause.new([table["id"].eq(bind_param(1))])
+ other_clause = WhereClause.new([table["name"].eq(bind_param("Sean"))])
expected_ast =
Arel::Nodes::Grouping.new(
- Arel::Nodes::Or.new(table["id"].eq(bind_param), table["name"].eq(bind_param))
+ Arel::Nodes::Or.new(table["id"].eq(bind_param(1)), table["name"].eq(bind_param("Sean")))
)
- expected_binds = where_clause.binds + other_clause.binds
assert_equal expected_ast.to_sql, where_clause.or(other_clause).ast.to_sql
- assert_equal expected_binds, where_clause.or(other_clause).binds
end
test "or returns an empty where clause when either side is empty" do
- where_clause = WhereClause.new([table["id"].eq(bind_param)], [attribute("id", 1)])
+ where_clause = WhereClause.new([table["id"].eq(bind_param(1))])
assert_equal WhereClause.empty, where_clause.or(WhereClause.empty)
assert_equal WhereClause.empty, WhereClause.empty.or(where_clause)
end
- private
+ test "or places common conditions before the OR" do
+ a = WhereClause.new(
+ [table["id"].eq(bind_param(1)), table["name"].eq(bind_param("Sean"))],
+ )
+ b = WhereClause.new(
+ [table["id"].eq(bind_param(1)), table["hair_color"].eq(bind_param("black"))],
+ )
+
+ common = WhereClause.new(
+ [table["id"].eq(bind_param(1))],
+ )
+
+ or_clause = WhereClause.new([table["name"].eq(bind_param("Sean"))])
+ .or(WhereClause.new([table["hair_color"].eq(bind_param("black"))]))
- def table
- Arel::Table.new("table")
+ assert_equal common + or_clause, a.or(b)
end
- def bind_param
- Arel::Nodes::BindParam.new
+ test "or can detect identical or as being a common condition" do
+ common_or = WhereClause.new([table["name"].eq(bind_param("Sean"))])
+ .or(WhereClause.new([table["hair_color"].eq(bind_param("black"))]))
+
+ a = common_or + WhereClause.new([table["id"].eq(bind_param(1))])
+ b = common_or + WhereClause.new([table["foo"].eq(bind_param("bar"))])
+
+ new_or = WhereClause.new([table["id"].eq(bind_param(1))])
+ .or(WhereClause.new([table["foo"].eq(bind_param("bar"))]))
+
+ assert_equal common_or + new_or, a.or(b)
end
- def attribute(name, value)
- ActiveRecord::Attribute.with_cast_value(name, value, ActiveRecord::Type::Value.new)
+ test "or will use only common conditions if one side only has common conditions" do
+ only_common = WhereClause.new([
+ table["id"].eq(bind_param(1)),
+ "foo = bar",
+ ])
+
+ common_with_extra = WhereClause.new([
+ table["id"].eq(bind_param(1)),
+ "foo = bar",
+ table["extra"].eq(bind_param("pluto")),
+ ])
+
+ assert_equal only_common, only_common.or(common_with_extra)
+ assert_equal only_common, common_with_extra.or(only_common)
end
+
+ private
+
+ def table
+ Arel::Table.new("table")
+ end
+
+ def bind_param(value)
+ Arel::Nodes::BindParam.new(value)
+ end
end
end
diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb
index 6af31017d6..99797528b2 100644
--- a/activerecord/test/cases/relation/where_test.rb
+++ b/activerecord/test/cases/relation/where_test.rb
@@ -1,12 +1,15 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "models/author"
require "models/binary"
require "models/cake_designer"
+require "models/car"
require "models/chef"
+require "models/post"
require "models/comment"
require "models/edge"
require "models/essay"
-require "models/post"
require "models/price_estimate"
require "models/topic"
require "models/treasure"
@@ -14,11 +17,11 @@ require "models/vertex"
module ActiveRecord
class WhereTest < ActiveRecord::TestCase
- fixtures :posts, :edges, :authors, :binaries, :essays
+ fixtures :posts, :edges, :authors, :author_addresses, :binaries, :essays, :cars, :treasures, :price_estimates, :topics
def test_where_copies_bind_params
author = authors(:david)
- posts = author.posts.where('posts.id != 1')
+ posts = author.posts.where("posts.id != 1")
joined = Post.where(id: posts)
assert_operator joined.length, :>, 0
@@ -47,8 +50,12 @@ module ActiveRecord
assert_equal [chef], chefs.to_a
end
+ def test_where_with_casted_value_is_nil
+ assert_equal 4, Topic.where(last_read: "").count
+ end
+
def test_rewhere_on_root
- assert_equal posts(:welcome), Post.rewhere(title: 'Welcome to the weblog').first
+ assert_equal posts(:welcome), Post.rewhere(title: "Welcome to the weblog").first
end
def test_belongs_to_shallow_where
@@ -63,12 +70,12 @@ module ActiveRecord
end
def test_belongs_to_array_value_where
- assert_equal Post.where(author_id: [1,2]).to_sql, Post.where(author: [1,2]).to_sql
+ assert_equal Post.where(author_id: [1, 2]).to_sql, Post.where(author: [1, 2]).to_sql
end
def test_belongs_to_nested_relation_where
- expected = Post.where(author_id: Author.where(id: [1,2])).to_sql
- actual = Post.where(author: Author.where(id: [1,2])).to_sql
+ expected = Post.where(author_id: Author.where(id: [1, 2])).to_sql
+ actual = Post.where(author: Author.where(id: [1, 2])).to_sql
assert_equal expected, actual
end
@@ -86,7 +93,7 @@ module ActiveRecord
def test_belongs_to_nested_where_with_relation
author = authors(:david)
- expected = Author.where(id: author ).joins(:posts)
+ expected = Author.where(id: author).joins(:posts)
actual = Author.where(posts: { author_id: Author.where(id: author.id) }).joins(:posts)
assert_equal expected.to_a, actual.to_a
@@ -96,27 +103,57 @@ module ActiveRecord
treasure = Treasure.new
treasure.id = 1
- expected = PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: 1)
+ expected = PriceEstimate.where(estimate_of_type: "Treasure", estimate_of_id: 1)
actual = PriceEstimate.where(estimate_of: treasure)
assert_equal expected.to_sql, actual.to_sql
end
+ def test_polymorphic_shallow_where_not
+ treasure = treasures(:sapphire)
+
+ expected = [price_estimates(:diamond), price_estimates(:honda)]
+ actual = PriceEstimate.where.not(estimate_of: treasure)
+
+ assert_equal expected.sort_by(&:id), actual.sort_by(&:id)
+ end
+
def test_polymorphic_nested_array_where
treasure = Treasure.new
treasure.id = 1
hidden = HiddenTreasure.new
hidden.id = 2
- expected = PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: [treasure, hidden])
+ expected = PriceEstimate.where(estimate_of_type: "Treasure", estimate_of_id: [treasure, hidden])
actual = PriceEstimate.where(estimate_of: [treasure, hidden])
assert_equal expected.to_sql, actual.to_sql
end
+ def test_polymorphic_nested_array_where_not
+ treasure = treasures(:diamond)
+ car = cars(:honda)
+
+ expected = [price_estimates(:sapphire_1), price_estimates(:sapphire_2)]
+ actual = PriceEstimate.where.not(estimate_of: [treasure, car])
+
+ assert_equal expected.sort_by(&:id), actual.sort_by(&:id)
+ end
+
+ def test_polymorphic_array_where_multiple_types
+ treasure_1 = treasures(:diamond)
+ treasure_2 = treasures(:sapphire)
+ car = cars(:honda)
+
+ expected = [price_estimates(:diamond), price_estimates(:sapphire_1), price_estimates(:sapphire_2), price_estimates(:honda)].sort
+ actual = PriceEstimate.where(estimate_of: [treasure_1, treasure_2, car]).to_a.sort
+
+ assert_equal expected, actual
+ end
+
def test_polymorphic_nested_relation_where
- expected = PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: Treasure.where(id: [1,2]))
- actual = PriceEstimate.where(estimate_of: Treasure.where(id: [1,2]))
+ expected = PriceEstimate.where(estimate_of_type: "Treasure", estimate_of_id: Treasure.where(id: [1, 2]))
+ actual = PriceEstimate.where(estimate_of: Treasure.where(id: [1, 2]))
assert_equal expected.to_sql, actual.to_sql
end
@@ -125,7 +162,7 @@ module ActiveRecord
treasure = HiddenTreasure.new
treasure.id = 1
- expected = PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: 1)
+ expected = PriceEstimate.where(estimate_of_type: "Treasure", estimate_of_id: 1)
actual = PriceEstimate.where(estimate_of: treasure)
assert_equal expected.to_sql, actual.to_sql
@@ -135,7 +172,7 @@ module ActiveRecord
thing = Post.new
thing.id = 1
- expected = Treasure.where(price_estimates: { thing_type: 'Post', thing_id: 1 }).joins(:price_estimates)
+ expected = Treasure.where(price_estimates: { thing_type: "Post", thing_id: 1 }).joins(:price_estimates)
actual = Treasure.where(price_estimates: { thing: thing }).joins(:price_estimates)
assert_equal expected.to_sql, actual.to_sql
@@ -145,7 +182,7 @@ module ActiveRecord
treasure = HiddenTreasure.new
treasure.id = 1
- expected = Treasure.where(price_estimates: { estimate_of_type: 'Treasure', estimate_of_id: 1 }).joins(:price_estimates)
+ expected = Treasure.where(price_estimates: { estimate_of_type: "Treasure", estimate_of_id: 1 }).joins(:price_estimates)
actual = Treasure.where(price_estimates: { estimate_of: treasure }).joins(:price_estimates)
assert_equal expected.to_sql, actual.to_sql
@@ -170,40 +207,40 @@ module ActiveRecord
treasure.id = 1
decorated_treasure = treasure_decorator.new(treasure)
- expected = PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: 1)
+ expected = PriceEstimate.where(estimate_of_type: "Treasure", estimate_of_id: 1)
actual = PriceEstimate.where(estimate_of: decorated_treasure)
assert_equal expected.to_sql, actual.to_sql
end
def test_aliased_attribute
- expected = Topic.where(heading: 'The First Topic')
- actual = Topic.where(title: 'The First Topic')
+ expected = Topic.where(heading: "The First Topic")
+ actual = Topic.where(title: "The First Topic")
assert_equal expected.to_sql, actual.to_sql
end
def test_where_error
- assert_raises(ActiveRecord::StatementInvalid) do
- Post.where(:id => { 'posts.author_id' => 10 }).first
+ assert_nothing_raised do
+ Post.where(id: { "posts.author_id" => 10 }).first
end
end
def test_where_with_table_name
post = Post.first
- assert_equal post, Post.where(:posts => { 'id' => post.id }).first
+ assert_equal post, Post.where(posts: { "id" => post.id }).first
end
def test_where_with_table_name_and_empty_hash
- assert_equal 0, Post.where(:posts => {}).count
+ assert_equal 0, Post.where(posts: {}).count
end
def test_where_with_table_name_and_empty_array
- assert_equal 0, Post.where(:id => []).count
+ assert_equal 0, Post.where(id: []).count
end
def test_where_with_empty_hash_and_no_foreign_key
- assert_equal 0, Edge.where(:sink => {}).count
+ assert_equal 0, Edge.where(sink: {}).count
end
def test_where_with_blank_conditions
@@ -213,32 +250,32 @@ module ActiveRecord
end
def test_where_with_integer_for_string_column
- count = Post.where(:title => 0).count
+ count = Post.where(title: 0).count
assert_equal 0, count
end
def test_where_with_float_for_string_column
- count = Post.where(:title => 0.0).count
+ count = Post.where(title: 0.0).count
assert_equal 0, count
end
def test_where_with_boolean_for_string_column
- count = Post.where(:title => false).count
+ count = Post.where(title: false).count
assert_equal 0, count
end
def test_where_with_decimal_for_string_column
- count = Post.where(:title => BigDecimal.new(0)).count
+ count = Post.where(title: BigDecimal(0)).count
assert_equal 0, count
end
def test_where_with_duration_for_string_column
- count = Post.where(:title => 0.seconds).count
+ count = Post.where(title: 0.seconds).count
assert_equal 0, count
end
def test_where_with_integer_for_binary_column
- count = Binary.where(:data => 0).count
+ count = Binary.where(data: 0).count
assert_equal 0, count
end
@@ -276,5 +313,54 @@ module ActiveRecord
assert_equal essays(:david_modest_proposal), essay
end
+
+ def test_where_with_relation_on_has_many_association
+ essay = essays(:david_modest_proposal)
+ author = Author.where(essays: Essay.where(id: essay.id)).first
+
+ assert_equal authors(:david), author
+ end
+
+ def test_where_with_relation_on_has_one_association
+ author = authors(:david)
+ author_address = AuthorAddress.where(author: Author.where(id: author.id)).first
+ assert_equal author_addresses(:david_address), author_address
+ end
+
+
+ def test_where_on_association_with_select_relation
+ essay = Essay.where(author: Author.where(name: "David").select(:name)).take
+ assert_equal essays(:david_modest_proposal), essay
+ end
+
+ def test_where_with_strong_parameters
+ protected_params = Class.new do
+ attr_reader :permitted
+ alias :permitted? :permitted
+
+ def initialize(parameters)
+ @parameters = parameters
+ @permitted = false
+ end
+
+ def to_h
+ @parameters
+ end
+
+ def permit!
+ @permitted = true
+ self
+ end
+ end
+
+ author = authors(:david)
+ params = protected_params.new(name: author.name)
+ assert_raises(ActiveModel::ForbiddenAttributesError) { Author.where(params) }
+ assert_equal author, Author.where(params.permit!).first
+ end
+
+ def test_where_with_unsupported_arguments
+ assert_raises(ArgumentError) { Author.where(42) }
+ end
end
end
diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb
index 37d3965022..68161f6a84 100644
--- a/activerecord/test/cases/relation_test.rb
+++ b/activerecord/test/cases/relation_test.rb
@@ -1,125 +1,106 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/post'
-require 'models/comment'
-require 'models/author'
-require 'models/rating'
+require "models/post"
+require "models/comment"
+require "models/author"
+require "models/rating"
+require "models/categorization"
module ActiveRecord
class RelationTest < ActiveRecord::TestCase
- fixtures :posts, :comments, :authors
-
- class FakeKlass < Struct.new(:table_name, :name)
- extend ActiveRecord::Delegation::DelegateCache
-
- inherited self
-
- def self.connection
- Post.connection
- end
-
- def self.table_name
- 'fake_table'
- end
- end
+ fixtures :posts, :comments, :authors, :author_addresses, :ratings, :categorizations
def test_construction
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass, table: :b)
assert_equal FakeKlass, relation.klass
assert_equal :b, relation.table
- assert !relation.loaded, 'relation is not loaded'
+ assert_not relation.loaded, "relation is not loaded"
end
def test_responds_to_model_and_returns_klass
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass)
assert_equal FakeKlass, relation.model
end
def test_initialize_single_values
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass)
(Relation::SINGLE_VALUE_METHODS - [:create_with]).each do |method|
assert_nil relation.send("#{method}_value"), method.to_s
end
- assert_equal({}, relation.create_with_value)
+ value = relation.create_with_value
+ assert_equal({}, value)
+ assert_predicate value, :frozen?
end
def test_multi_value_initialize
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass)
Relation::MULTI_VALUE_METHODS.each do |method|
- assert_equal [], relation.send("#{method}_values"), method.to_s
+ values = relation.send("#{method}_values")
+ assert_equal [], values, method.to_s
+ assert_predicate values, :frozen?, method.to_s
end
end
def test_extensions
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass)
assert_equal [], relation.extensions
end
def test_empty_where_values_hash
- relation = Relation.new(FakeKlass, :b, nil)
- assert_equal({}, relation.where_values_hash)
-
- relation.where! :hello
+ relation = Relation.new(FakeKlass)
assert_equal({}, relation.where_values_hash)
end
def test_has_values
- relation = Relation.new(Post, Post.arel_table, Post.predicate_builder)
- relation.where! relation.table[:id].eq(10)
- assert_equal({:id => 10}, relation.where_values_hash)
+ relation = Relation.new(Post)
+ relation.where!(id: 10)
+ assert_equal({ "id" => 10 }, relation.where_values_hash)
end
def test_values_wrong_table
- relation = Relation.new(Post, Post.arel_table, Post.predicate_builder)
+ relation = Relation.new(Post)
relation.where! Comment.arel_table[:id].eq(10)
assert_equal({}, relation.where_values_hash)
end
def test_tree_is_not_traversed
- relation = Relation.new(Post, Post.arel_table, Post.predicate_builder)
- # FIXME: Remove the Arel::Nodes::Quoted in Rails 5.1
+ relation = Relation.new(Post)
left = relation.table[:id].eq(10)
right = relation.table[:id].eq(10)
- combine = left.and right
+ combine = left.or(right)
relation.where! combine
assert_equal({}, relation.where_values_hash)
end
- def test_table_name_delegates_to_klass
- relation = Relation.new(FakeKlass.new('posts'), :b, Post.predicate_builder)
- assert_equal 'posts', relation.table_name
- end
-
def test_scope_for_create
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass)
assert_equal({}, relation.scope_for_create)
end
def test_create_with_value
- relation = Relation.new(Post, Post.arel_table, Post.predicate_builder)
- hash = { :hello => 'world' }
- relation.create_with_value = hash
- assert_equal hash, relation.scope_for_create
+ relation = Relation.new(Post)
+ relation.create_with_value = { hello: "world" }
+ assert_equal({ "hello" => "world" }, relation.scope_for_create)
end
def test_create_with_value_with_wheres
- relation = Relation.new(Post, Post.arel_table, Post.predicate_builder)
- # FIXME: Remove the Arel::Nodes::Quoted in Rails 5.1
- relation.where! relation.table[:id].eq(10)
- relation.create_with_value = {:hello => 'world'}
- assert_equal({:hello => 'world', :id => 10}, relation.scope_for_create)
- end
-
- # FIXME: is this really wanted or expected behavior?
- def test_scope_for_create_is_cached
- relation = Relation.new(Post, Post.arel_table, Post.predicate_builder)
+ relation = Relation.new(Post)
assert_equal({}, relation.scope_for_create)
- # FIXME: Remove the Arel::Nodes::Quoted in Rails 5.1
- relation.where! relation.table[:id].eq(10)
- assert_equal({}, relation.scope_for_create)
+ relation.where!(id: 10)
+ assert_equal({ "id" => 10 }, relation.scope_for_create)
- relation.create_with_value = {:hello => 'world'}
- assert_equal({}, relation.scope_for_create)
+ relation.create_with_value = { hello: "world" }
+ assert_equal({ "hello" => "world", "id" => 10 }, relation.scope_for_create)
+ end
+
+ def test_empty_scope
+ relation = Relation.new(Post)
+ assert_predicate relation, :empty_scope?
+
+ relation.merge!(relation)
+ assert_predicate relation, :empty_scope?
end
def test_bad_constants_raise_errors
@@ -129,89 +110,89 @@ module ActiveRecord
end
def test_empty_eager_loading?
- relation = Relation.new(FakeKlass, :b, nil)
- assert !relation.eager_loading?
+ relation = Relation.new(FakeKlass)
+ assert_not_predicate relation, :eager_loading?
end
def test_eager_load_values
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass)
relation.eager_load! :b
- assert relation.eager_loading?
+ assert_predicate relation, :eager_loading?
end
def test_references_values
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass)
assert_equal [], relation.references_values
relation = relation.references(:foo).references(:omg, :lol)
- assert_equal ['foo', 'omg', 'lol'], relation.references_values
+ assert_equal ["foo", "omg", "lol"], relation.references_values
end
def test_references_values_dont_duplicate
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass)
relation = relation.references(:foo).references(:foo)
- assert_equal ['foo'], relation.references_values
+ assert_equal ["foo"], relation.references_values
end
- test 'merging a hash into a relation' do
- relation = Relation.new(FakeKlass, :b, nil)
- relation = relation.merge where: :lol, readonly: true
+ test "merging a hash into a relation" do
+ relation = Relation.new(Post)
+ relation = relation.merge where: { name: :lol }, readonly: true
- assert_equal Relation::WhereClause.new([:lol], []), relation.where_clause
+ assert_equal({ "name" => :lol }, relation.where_clause.to_h)
assert_equal true, relation.readonly_value
end
- test 'merging an empty hash into a relation' do
- assert_equal Relation::WhereClause.empty, Relation.new(FakeKlass, :b, nil).merge({}).where_clause
+ test "merging an empty hash into a relation" do
+ assert_equal Relation::WhereClause.empty, Relation.new(FakeKlass).merge({}).where_clause
end
- test 'merging a hash with unknown keys raises' do
- assert_raises(ArgumentError) { Relation::HashMerger.new(nil, omg: 'lol') }
+ test "merging a hash with unknown keys raises" do
+ assert_raises(ArgumentError) { Relation::HashMerger.new(nil, omg: "lol") }
end
- test 'merging nil or false raises' do
- relation = Relation.new(FakeKlass, :b, nil)
+ test "merging nil or false raises" do
+ relation = Relation.new(FakeKlass)
e = assert_raises(ArgumentError) do
relation = relation.merge nil
end
- assert_equal 'invalid argument: nil.', e.message
+ assert_equal "invalid argument: nil.", e.message
e = assert_raises(ArgumentError) do
relation = relation.merge false
end
- assert_equal 'invalid argument: false.', e.message
+ assert_equal "invalid argument: false.", e.message
end
- test '#values returns a dup of the values' do
- relation = Relation.new(FakeKlass, :b, nil).where! :foo
+ test "#values returns a dup of the values" do
+ relation = Relation.new(Post).where!(name: :foo)
values = relation.values
values[:where] = nil
assert_not_nil relation.where_clause
end
- test 'relations can be created with a values hash' do
- relation = Relation.new(FakeKlass, :b, nil, select: [:foo])
+ test "relations can be created with a values hash" do
+ relation = Relation.new(FakeKlass, values: { select: [:foo] })
assert_equal [:foo], relation.select_values
end
- test 'merging a hash interpolates conditions' do
+ test "merging a hash interpolates conditions" do
klass = Class.new(FakeKlass) do
def self.sanitize_sql(args)
- raise unless args == ['foo = ?', 'bar']
- 'foo = bar'
+ raise unless args == ["foo = ?", "bar"]
+ "foo = bar"
end
end
- relation = Relation.new(klass, :b, nil)
- relation.merge!(where: ['foo = ?', 'bar'])
- assert_equal Relation::WhereClause.new(['foo = bar'], []), relation.where_clause
+ relation = Relation.new(klass)
+ relation.merge!(where: ["foo = ?", "bar"])
+ assert_equal Relation::WhereClause.new(["foo = bar"]), relation.where_clause
end
def test_merging_readonly_false
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass)
readonly_false_relation = relation.readonly(false)
# test merging in both directions
assert_equal false, relation.merge(readonly_false_relation).readonly_value
@@ -221,7 +202,50 @@ module ActiveRecord
def test_relation_merging_with_merged_joins_as_symbols
special_comments_with_ratings = SpecialComment.joins(:ratings)
posts_with_special_comments_with_ratings = Post.group("posts.id").joins(:special_comments).merge(special_comments_with_ratings)
- assert_equal 3, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count.length
+ assert_equal({ 4 => 2 }, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count)
+ end
+
+ def test_relation_merging_with_merged_symbol_joins_keeps_inner_joins
+ queries = capture_sql { Author.joins(:posts).merge(Post.joins(:comments)).to_a }
+
+ nb_inner_join = queries.sum { |sql| sql.scan(/INNER\s+JOIN/i).size }
+ assert_equal 2, nb_inner_join, "Wrong amount of INNER JOIN in query"
+ assert queries.none? { |sql| /LEFT\s+(OUTER)?\s+JOIN/i.match?(sql) }, "Shouldn't have any LEFT JOIN in query"
+ end
+
+ def test_relation_merging_with_merged_symbol_joins_has_correct_size_and_count
+ # Has one entry per comment
+ merged_authors_with_commented_posts_relation = Author.joins(:posts).merge(Post.joins(:comments))
+
+ post_ids_with_author = Post.joins(:author).pluck(:id)
+ manual_comments_on_post_that_have_author = Comment.where(post_id: post_ids_with_author).pluck(:id)
+
+ assert_equal manual_comments_on_post_that_have_author.size, merged_authors_with_commented_posts_relation.count
+ assert_equal manual_comments_on_post_that_have_author.size, merged_authors_with_commented_posts_relation.to_a.size
+ end
+
+ def test_relation_merging_with_merged_symbol_joins_is_aliased
+ categorizations_with_authors = Categorization.joins(:author)
+ queries = capture_sql { Post.joins(:author, :categorizations).merge(Author.select(:id)).merge(categorizations_with_authors).to_a }
+
+ nb_inner_join = queries.sum { |sql| sql.scan(/INNER\s+JOIN/i).size }
+ assert_equal 3, nb_inner_join, "Wrong amount of INNER JOIN in query"
+
+ # using `\W` as the column separator
+ assert queries.any? { |sql| %r[INNER\s+JOIN\s+#{Regexp.escape(Author.quoted_table_name)}\s+\Wauthors_categorizations\W]i.match?(sql) }, "Should be aliasing the child INNER JOINs in query"
+ end
+
+ def test_relation_with_merged_joins_aliased_works
+ categorizations_with_authors = Categorization.joins(:author)
+ posts_with_joins_and_merges = Post.joins(:author, :categorizations)
+ .merge(Author.select(:id)).merge(categorizations_with_authors)
+
+ author_with_posts = Author.joins(:posts).ids
+ categorizations_with_author = Categorization.joins(:author).ids
+ posts_with_author_and_categorizations = Post.joins(:categorizations).where(author_id: author_with_posts, categorizations: { id: categorizations_with_author }).ids
+
+ assert_equal posts_with_author_and_categorizations.size, posts_with_joins_and_merges.count
+ assert_equal posts_with_author_and_categorizations.size, posts_with_joins_and_merges.to_a.size
end
def test_relation_merging_with_joins_as_join_dependency_pick_proper_parent
@@ -234,32 +258,53 @@ module ActiveRecord
assert_equal 3, relation.where(id: post.id).pluck(:id).size
end
+ def test_merge_raises_with_invalid_argument
+ assert_raises ArgumentError do
+ relation = Relation.new(FakeKlass)
+ relation.merge(true)
+ end
+ end
+
def test_respond_to_for_non_selected_element
post = Post.select(:title).first
- assert_equal false, post.respond_to?(:body), "post should not respond_to?(:body) since invoking it raises exception"
+ assert_not_respond_to post, :body, "post should not respond_to?(:body) since invoking it raises exception"
silence_warnings { post = Post.select("'title' as post_title").first }
- assert_equal false, post.respond_to?(:title), "post should not respond_to?(:body) since invoking it raises exception"
+ assert_not_respond_to post, :title, "post should not respond_to?(:body) since invoking it raises exception"
end
def test_select_quotes_when_using_from_clause
- if sqlite3_version_includes_quoting_bug?
- skip <<-ERROR.squish
- You are using an outdated version of SQLite3 which has a bug in
- quoted column names. Please update SQLite3 and rebuild the sqlite3
- ruby gem
- ERROR
- end
+ skip_if_sqlite3_version_includes_quoting_bug
quoted_join = ActiveRecord::Base.connection.quote_table_name("join")
selected = Post.select(:join).from(Post.select("id as #{quoted_join}")).map(&:join)
assert_equal Post.pluck(:id), selected
end
+ def test_selecting_aliased_attribute_quotes_column_name_when_from_is_used
+ skip_if_sqlite3_version_includes_quoting_bug
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = :test_with_keyword_column_name
+ alias_attribute :description, :desc
+ end
+ klass.create!(description: "foo")
+
+ assert_equal ["foo"], klass.select(:description).from(klass.all).map(&:desc)
+ end
+
def test_relation_merging_with_merged_joins_as_strings
join_string = "LEFT OUTER JOIN #{Rating.quoted_table_name} ON #{SpecialComment.quoted_table_name}.id = #{Rating.quoted_table_name}.comment_id"
special_comments_with_ratings = SpecialComment.joins join_string
posts_with_special_comments_with_ratings = Post.group("posts.id").joins(:special_comments).merge(special_comments_with_ratings)
- assert_equal 3, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count.length
+ assert_equal({ 2 => 1, 4 => 3, 5 => 1 }, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count)
+ end
+
+ def test_relation_merging_keeps_joining_order
+ authors = Author.where(id: 1)
+ posts = Post.joins(:author).merge(authors)
+ comments = Comment.joins(:post).merge(posts)
+ ratings = Rating.joins(:comment).merge(comments)
+
+ assert_equal 3, ratings.count
end
class EnsureRoundTripTypeCasting < ActiveRecord::Type::Value
@@ -267,19 +312,24 @@ module ActiveRecord
:string
end
+ def cast(value)
+ raise value unless value == "value from user"
+ "cast value"
+ end
+
def deserialize(value)
raise value unless value == "type cast for database"
"type cast from database"
end
def serialize(value)
- raise value unless value == "value from user"
+ raise value unless value == "cast value"
"type cast for database"
end
end
class UpdateAllTestModel < ActiveRecord::Base
- self.table_name = 'posts'
+ self.table_name = "posts"
attribute :body, EnsureRoundTripTypeCasting.new
end
@@ -290,15 +340,33 @@ module ActiveRecord
assert_equal "type cast from database", UpdateAllTestModel.first.body
end
+ def test_skip_preloading_after_arel_has_been_generated
+ assert_nothing_raised do
+ relation = Comment.all
+ relation.arel
+ relation.skip_preloading!
+ end
+ end
+
private
- def sqlite3_version_includes_quoting_bug?
- if current_adapter?(:SQLite3Adapter)
- selected_quoted_column_names = ActiveRecord::Base.connection.exec_query(
- 'SELECT "join" FROM (SELECT id AS "join" FROM posts) subquery'
- ).columns
- ["join"] != selected_quoted_column_names
+ def skip_if_sqlite3_version_includes_quoting_bug
+ if sqlite3_version_includes_quoting_bug?
+ skip <<-ERROR.squish
+ You are using an outdated version of SQLite3 which has a bug in
+ quoted column names. Please update SQLite3 and rebuild the sqlite3
+ ruby gem
+ ERROR
+ end
+ end
+
+ def sqlite3_version_includes_quoting_bug?
+ if current_adapter?(:SQLite3Adapter)
+ selected_quoted_column_names = ActiveRecord::Base.connection.exec_query(
+ 'SELECT "join" FROM (SELECT id AS "join" FROM posts) subquery'
+ ).columns
+ ["join"] != selected_quoted_column_names
+ end
end
- end
end
end
diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb
index 5f48c2b40f..e471ee8039 100644
--- a/activerecord/test/cases/relations_test.rb
+++ b/activerecord/test/cases/relations_test.rb
@@ -1,43 +1,44 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/tag'
-require 'models/tagging'
-require 'models/post'
-require 'models/topic'
-require 'models/comment'
-require 'models/author'
-require 'models/entrant'
-require 'models/developer'
-require 'models/computer'
-require 'models/reply'
-require 'models/company'
-require 'models/bird'
-require 'models/car'
-require 'models/engine'
-require 'models/tyre'
-require 'models/minivan'
-require 'models/aircraft'
+require "models/tag"
+require "models/tagging"
+require "models/post"
+require "models/topic"
+require "models/comment"
+require "models/author"
+require "models/entrant"
+require "models/developer"
+require "models/project"
+require "models/person"
+require "models/computer"
+require "models/reply"
+require "models/company"
+require "models/bird"
+require "models/car"
+require "models/engine"
+require "models/tyre"
+require "models/minivan"
require "models/possession"
require "models/reader"
+require "models/category"
+require "models/categorization"
+require "models/edge"
+require "models/subscriber"
class RelationTest < ActiveRecord::TestCase
- fixtures :authors, :topics, :entrants, :developers, :companies, :developers_projects, :accounts, :categories, :categorizations, :posts, :comments,
- :tags, :taggings, :cars, :minivans
-
- class TopicWithCallbacks < ActiveRecord::Base
- self.table_name = :topics
- before_update { |topic| topic.author_name = 'David' if topic.author_name.blank? }
- end
+ fixtures :authors, :author_addresses, :topics, :entrants, :developers, :people, :companies, :developers_projects, :accounts, :categories, :categorizations, :categories_posts, :posts, :comments, :tags, :taggings, :cars, :minivans
def test_do_not_double_quote_string_id
van = Minivan.last
assert van
- assert_equal van.id, Minivan.where(:minivan_id => van).to_a.first.minivan_id
+ assert_equal van.id, Minivan.where(minivan_id: van).to_a.first.minivan_id
end
def test_do_not_double_quote_string_id_with_array
van = Minivan.last
assert van
- assert_equal van, Minivan.where(:minivan_id => [van]).to_a.first
+ assert_equal van, Minivan.where(minivan_id: [van]).to_a.first
end
def test_two_scopes_with_includes_should_not_drop_any_include
@@ -52,12 +53,12 @@ class RelationTest < ActiveRecord::TestCase
end
def test_dynamic_finder
- x = Post.where('author_id = ?', 1)
- assert x.klass.respond_to?(:find_by_id), '@klass should handle dynamic finders'
+ x = Post.where("author_id = ?", 1)
+ assert_respond_to x.klass, :find_by_id
end
def test_multivalue_where
- posts = Post.where('author_id = ? AND id = ?', 1, 1)
+ posts = Post.where("author_id = ? AND id = ?", 1, 1)
assert_equal 1, posts.to_a.size
end
@@ -95,28 +96,51 @@ class RelationTest < ActiveRecord::TestCase
2.times { assert_equal 5, topics.to_a.size }
end
- assert topics.loaded?
+ assert_predicate topics, :loaded?
end
def test_scoped_first
- topics = Topic.all.order('id ASC')
+ topics = Topic.all.order("id ASC")
assert_queries(1) do
2.times { assert_equal "The First Topic", topics.first.title }
end
- assert ! topics.loaded?
+ assert_not_predicate topics, :loaded?
end
def test_loaded_first
- topics = Topic.all.order('id ASC')
+ topics = Topic.all.order("id ASC")
+ topics.load # force load
- assert_queries(1) do
- topics.to_a # force load
- 2.times { assert_equal "The First Topic", topics.first.title }
+ assert_no_queries do
+ assert_equal "The First Topic", topics.first.title
+ end
+
+ assert_predicate topics, :loaded?
+ end
+
+ def test_loaded_first_with_limit
+ topics = Topic.all.order("id ASC")
+ topics.load # force load
+
+ assert_no_queries do
+ assert_equal ["The First Topic",
+ "The Second Topic of the day"], topics.first(2).map(&:title)
end
- assert topics.loaded?
+ assert_predicate topics, :loaded?
+ end
+
+ def test_first_get_more_than_available
+ topics = Topic.all.order("id ASC")
+ unloaded_first = topics.first(10)
+ topics.load # force load
+
+ assert_no_queries do
+ loaded_first = topics.first(10)
+ assert_equal unloaded_first, loaded_first
+ end
end
def test_reload
@@ -126,28 +150,28 @@ class RelationTest < ActiveRecord::TestCase
2.times { topics.to_a }
end
- assert topics.loaded?
+ assert_predicate topics, :loaded?
original_size = topics.to_a.size
- Topic.create! :title => 'fake'
+ Topic.create! title: "fake"
assert_queries(1) { topics.reload }
assert_equal original_size + 1, topics.size
- assert topics.loaded?
+ assert_predicate topics, :loaded?
end
def test_finding_with_subquery
- relation = Topic.where(:approved => true)
- assert_equal relation.to_a, Topic.select('*').from(relation).to_a
- assert_equal relation.to_a, Topic.select('subquery.*').from(relation).to_a
- assert_equal relation.to_a, Topic.select('a.*').from(relation, :a).to_a
+ relation = Topic.where(approved: true)
+ assert_equal relation.to_a, Topic.select("*").from(relation).to_a
+ assert_equal relation.to_a, Topic.select("subquery.*").from(relation).to_a
+ assert_equal relation.to_a, Topic.select("a.*").from(relation, :a).to_a
end
def test_finding_with_subquery_with_binds
relation = Post.first.comments
- assert_equal relation.to_a, Comment.select('*').from(relation).to_a
- assert_equal relation.to_a, Comment.select('subquery.*').from(relation).to_a
- assert_equal relation.to_a, Comment.select('a.*').from(relation, :a).to_a
+ assert_equal relation.to_a, Comment.select("*").from(relation).to_a
+ assert_equal relation.to_a, Comment.select("subquery.*").from(relation).to_a
+ assert_equal relation.to_a, Comment.select("a.*").from(relation, :a).to_a
end
def test_finding_with_subquery_without_select_does_not_change_the_select
@@ -158,25 +182,37 @@ class RelationTest < ActiveRecord::TestCase
end
def test_select_with_subquery_in_from_does_not_use_original_table_name
- relation = Comment.group(:type).select('COUNT(post_id) AS post_count, type')
- subquery = Comment.from(relation).select('type','post_count')
- assert_equal(relation.map(&:post_count).sort,subquery.map(&:post_count).sort)
+ relation = Comment.group(:type).select("COUNT(post_id) AS post_count, type")
+ subquery = Comment.from(relation).select("type", "post_count")
+ assert_equal(relation.map(&:post_count).sort, subquery.map(&:post_count).sort)
end
def test_group_with_subquery_in_from_does_not_use_original_table_name
- relation = Comment.group(:type).select('COUNT(post_id) AS post_count,type')
- subquery = Comment.from(relation).group('type').average("post_count")
- assert_equal(relation.map(&:post_count).sort,subquery.values.sort)
+ relation = Comment.group(:type).select("COUNT(post_id) AS post_count,type")
+ subquery = Comment.from(relation).group("type").average("post_count")
+ assert_equal(relation.map(&:post_count).sort, subquery.values.sort)
+ end
+
+ def test_finding_with_subquery_with_eager_loading_in_from
+ relation = Comment.includes(:post).where("posts.type": "Post")
+ assert_equal relation.to_a, Comment.select("*").from(relation).to_a
+ assert_equal relation.to_a, Comment.select("subquery.*").from(relation).to_a
+ assert_equal relation.to_a, Comment.select("a.*").from(relation, :a).to_a
+ end
+
+ def test_finding_with_subquery_with_eager_loading_in_where
+ relation = Comment.includes(:post).where("posts.type": "Post")
+ assert_equal relation.sort_by(&:id), Comment.where(id: relation).sort_by(&:id)
end
def test_finding_with_conditions
- assert_equal ["David"], Author.where(:name => 'David').map(&:name)
- assert_equal ['Mary'], Author.where(["name = ?", 'Mary']).map(&:name)
- assert_equal ['Mary'], Author.where("name = ?", 'Mary').map(&:name)
+ assert_equal ["David"], Author.where(name: "David").map(&:name)
+ assert_equal ["Mary"], Author.where(["name = ?", "Mary"]).map(&:name)
+ assert_equal ["Mary"], Author.where("name = ?", "Mary").map(&:name)
end
def test_finding_with_order
- topics = Topic.order('id')
+ topics = Topic.order("id")
assert_equal 5, topics.to_a.size
assert_equal topics(:first).title, topics.first.title
end
@@ -188,19 +224,84 @@ class RelationTest < ActiveRecord::TestCase
end
def test_finding_with_assoc_order
- topics = Topic.order(:id => :desc)
+ topics = Topic.order(id: :desc)
+ assert_equal 5, topics.to_a.size
+ assert_equal topics(:fifth).title, topics.first.title
+ end
+
+ 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_reverted_assoc_order
- topics = Topic.order(:id => :asc).reverse_order
+ 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(Arel.sql("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(Arel.sql("author_name, length(title), id")).reverse_order
+ assert_equal topics(:second).title, topics.first.title
+ topics = Topic.order(Arel.sql("length(author_name), id, length(title)")).reverse_order
+ assert_equal topics(:fifth).title, topics.first.title
+ end
+
+ def test_reverse_order_with_multiargument_function
+ assert_raises(ActiveRecord::IrreversibleOrderError) do
+ Topic.order(Arel.sql("concat(author_name, title)")).reverse_order
+ end
+ assert_raises(ActiveRecord::IrreversibleOrderError) do
+ Topic.order(Arel.sql("concat(lower(author_name), title)")).reverse_order
+ end
+ assert_raises(ActiveRecord::IrreversibleOrderError) do
+ Topic.order(Arel.sql("concat(author_name, lower(title))")).reverse_order
+ end
+ assert_raises(ActiveRecord::IrreversibleOrderError) do
+ Topic.order(Arel.sql("concat(lower(author_name), title, length(title)")).reverse_order
+ 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(Arel.sql("title NULLS FIRST")).reverse_order
+ end
+ assert_raises(ActiveRecord::IrreversibleOrderError) do
+ Topic.order(Arel.sql("title nulls last")).reverse_order
+ end
+ end
+
+ def test_default_reverse_order_on_table_without_primary_key
+ assert_raises(ActiveRecord::IrreversibleOrderError) do
+ Edge.all.reverse_order
+ end
+ end
+
def test_order_with_hash_and_symbol_generates_the_same_sql
- assert_equal Topic.order(:id).to_sql, Topic.order(:id => :asc).to_sql
+ assert_equal Topic.order(:id).to_sql, Topic.order(id: :asc).to_sql
end
def test_finding_with_desc_order_with_string
@@ -210,7 +311,7 @@ class RelationTest < ActiveRecord::TestCase
end
def test_finding_with_asc_order_with_string
- topics = Topic.order(id: 'asc')
+ topics = Topic.order(id: "asc")
assert_equal 5, topics.to_a.size
assert_equal [topics(:first), topics(:second), topics(:third), topics(:fourth), topics(:fifth)], topics.to_a
end
@@ -224,7 +325,7 @@ class RelationTest < ActiveRecord::TestCase
assert_includes Topic.order(id: "DESC").to_sql, "DESC"
assert_includes Topic.order(id: "desc").to_sql, "DESC"
assert_includes Topic.order(id: :DESC).to_sql, "DESC"
- assert_includes Topic.order(id: :desc).to_sql,"DESC"
+ assert_includes Topic.order(id: :desc).to_sql, "DESC"
end
def test_raising_exception_on_invalid_hash_params
@@ -238,7 +339,7 @@ class RelationTest < ActiveRecord::TestCase
end
def test_finding_with_order_concatenated
- topics = Topic.order('author_name').order('title')
+ topics = Topic.order("author_name").order("title")
assert_equal 5, topics.to_a.size
assert_equal topics(:fourth).title, topics.first.title
end
@@ -256,19 +357,19 @@ class RelationTest < ActiveRecord::TestCase
end
def test_finding_with_reorder
- topics = Topic.order('author_name').order('title').reorder('id').to_a
+ topics = Topic.order("author_name").order("title").reorder("id").to_a
topics_titles = topics.map(&:title)
- assert_equal ['The First Topic', 'The Second Topic of the day', 'The Third Topic of the day', 'The Fourth Topic of the day', 'The Fifth Topic of the day'], topics_titles
+ assert_equal ["The First Topic", "The Second Topic of the day", "The Third Topic of the day", "The Fourth Topic of the day", "The Fifth Topic of the day"], topics_titles
end
def test_finding_with_reorder_by_aliased_attributes
- topics = Topic.order('author_name').reorder(:heading)
+ topics = Topic.order("author_name").reorder(:heading)
assert_equal 5, topics.to_a.size
assert_equal topics(:fifth).title, topics.first.title
end
def test_finding_with_assoc_reorder_by_aliased_attributes
- topics = Topic.order('author_name').reorder(heading: :desc)
+ topics = Topic.order("author_name").reorder(heading: :desc)
assert_equal 5, topics.to_a.size
assert_equal topics(:third).title, topics.first.title
end
@@ -282,21 +383,32 @@ class RelationTest < ActiveRecord::TestCase
def test_finding_with_cross_table_order_and_limit
tags = Tag.includes(:taggings).
- order("tags.name asc", "taggings.taggable_id asc", "REPLACE('abc', taggings.taggable_type, taggings.taggable_type)").
+ order("tags.name asc", "taggings.taggable_id asc", Arel.sql("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)")).
limit(1).to_a
assert_equal 1, tags.length
end
def test_finding_with_complex_order_and_limit
- tags = Tag.includes(:taggings).references(:taggings).order("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)").limit(1).to_a
+ tags = Tag.includes(:taggings).references(:taggings).order(Arel.sql("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)")).limit(1).to_a
assert_equal 1, tags.length
end
def test_finding_with_complex_order
- tags = Tag.includes(:taggings).references(:taggings).order("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)").to_a
+ tags = Tag.includes(:taggings).references(:taggings).order(Arel.sql("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)")).to_a
assert_equal 3, tags.length
end
+ def test_finding_with_sanitized_order
+ query = Tag.order([Arel.sql("field(id, ?)"), [1, 3, 2]]).to_sql
+ assert_match(/field\(id, 1,3,2\)/, query)
+
+ query = Tag.order([Arel.sql("field(id, ?)"), []]).to_sql
+ assert_match(/field\(id, NULL\)/, query)
+
+ query = Tag.order([Arel.sql("field(id, ?)"), nil]).to_sql
+ assert_match(/field\(id, NULL\)/, query)
+ end
+
def test_finding_with_order_limit_and_offset
entrants = Entrant.order("id ASC").limit(2).offset(1)
@@ -315,149 +427,32 @@ class RelationTest < ActiveRecord::TestCase
end
def test_select_with_block
- even_ids = Developer.all.select {|d| d.id % 2 == 0 }.map(&:id)
+ even_ids = Developer.all.select { |d| d.id % 2 == 0 }.map(&:id)
assert_equal [2, 4, 6, 8, 10], even_ids.sort
end
- def test_none
- assert_no_queries(ignore_none: false) do
- assert_equal [], Developer.none
- assert_equal [], Developer.all.none
- end
- end
-
- def test_none_chainable
- assert_no_queries(ignore_none: false) do
- assert_equal [], Developer.none.where(:name => 'David')
- end
- end
-
- def test_none_chainable_to_existing_scope_extension_method
- assert_no_queries(ignore_none: false) do
- assert_equal 1, Topic.anonymous_extension.none.one
- end
- end
-
- def test_none_chained_to_methods_firing_queries_straight_to_db
- assert_no_queries(ignore_none: false) do
- assert_equal [], Developer.none.pluck(:id, :name)
- assert_equal 0, Developer.none.delete_all
- assert_equal 0, Developer.none.update_all(:name => 'David')
- assert_equal 0, Developer.none.delete(1)
- assert_equal false, Developer.none.exists?(1)
- end
- end
-
- def test_null_relation_content_size_methods
- assert_no_queries(ignore_none: false) do
- assert_equal 0, Developer.none.size
- assert_equal 0, Developer.none.count
- assert_equal true, Developer.none.empty?
- assert_equal true, Developer.none.none?
- assert_equal false, Developer.none.any?
- assert_equal false, Developer.none.one?
- assert_equal false, Developer.none.many?
- end
- end
-
- def test_null_relation_calculations_methods
- 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')
- end
- end
-
- def test_null_relation_metadata_methods
- assert_equal "", Developer.none.to_sql
- assert_equal({}, Developer.none.where_values_hash)
- end
-
- def test_null_relation_where_values_hash
- assert_equal({ 'salary' => 100_000 }, Developer.none.where(salary: 100_000).where_values_hash)
- end
-
- def test_null_relation_sum
- ac = Aircraft.new
- assert_equal Hash.new, ac.engines.group(:id).sum(:id)
- assert_equal 0, ac.engines.count
- ac.save
- assert_equal Hash.new, ac.engines.group(:id).sum(:id)
- assert_equal 0, ac.engines.count
- end
-
- def test_null_relation_count
- ac = Aircraft.new
- assert_equal Hash.new, ac.engines.group(:id).count
- assert_equal 0, ac.engines.count
- ac.save
- assert_equal Hash.new, ac.engines.group(:id).count
- assert_equal 0, ac.engines.count
- end
-
- def test_null_relation_size
- ac = Aircraft.new
- assert_equal Hash.new, ac.engines.group(:id).size
- assert_equal 0, ac.engines.size
- ac.save
- assert_equal Hash.new, ac.engines.group(:id).size
- assert_equal 0, ac.engines.size
- end
-
- 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)
- ac.save
- assert_equal Hash.new, ac.engines.group(:car_id).average(:id)
- assert_equal 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)
- ac.save
- assert_equal Hash.new, ac.engines.group(:car_id).minimum(:id)
- assert_equal 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)
- ac.save
- assert_equal Hash.new, ac.engines.group(:car_id).maximum(:id)
- assert_equal nil, ac.engines.maximum(:id)
- end
-
- def test_null_relation_in_where_condition
- assert_operator Comment.count, :>, 0 # precondition, make sure there are comments.
- assert_equal 0, Comment.where(post_id: Post.none).to_a.size
- end
-
def test_joins_with_nil_argument
assert_nothing_raised { DependentFirm.joins(nil).first }
end
def test_finding_with_hash_conditions_on_joined_table
- firms = DependentFirm.joins(:account).where({:name => 'RailsCore', :accounts => { :credit_limit => 55..60 }}).to_a
+ firms = DependentFirm.joins(:account).where(name: "RailsCore", accounts: { credit_limit: 55..60 }).to_a
assert_equal 1, firms.size
assert_equal companies(:rails_core), firms.first
end
def test_find_all_with_join
- developers_on_project_one = Developer.joins('LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id').
- where('project_id=1').to_a
+ developers_on_project_one = Developer.joins("LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id").
+ where("project_id=1").to_a
assert_equal 3, developers_on_project_one.length
developer_names = developers_on_project_one.map(&:name)
- assert developer_names.include?('David')
- assert developer_names.include?('Jamis')
+ assert_includes developer_names, "David"
+ assert_includes developer_names, "Jamis"
end
def test_find_on_hash_conditions
- assert_equal Topic.all.merge!(:where => {:approved => false}).to_a, Topic.where({ :approved => false }).to_a
+ assert_equal Topic.all.merge!(where: { approved: false }).to_a, Topic.where(approved: false).to_a
end
def test_joins_with_string_array
@@ -485,18 +480,10 @@ class RelationTest < ActiveRecord::TestCase
assert_nothing_raised { Topic.reorder([]) }
end
- def test_scoped_responds_to_delegated_methods
- relation = Topic.all
-
- ["map", "uniq", "sort", "insert", "delete", "update"].each do |method|
- assert_respond_to relation, method, "Topic.all should respond to #{method.inspect}"
- end
- end
-
- def test_respond_to_delegates_to_relation
+ def test_respond_to_delegates_to_arel
relation = Topic.all
fake_arel = Struct.new(:responds) {
- def respond_to? method, access = false
+ def respond_to?(method, access = false)
responds << [method, access]
end
}.new []
@@ -506,32 +493,28 @@ class RelationTest < ActiveRecord::TestCase
relation.respond_to?(:matching_attributes)
assert_equal [:matching_attributes, false], fake_arel.responds.first
-
- fake_arel.responds = []
- relation.respond_to?(:matching_attributes, true)
- assert_equal [:matching_attributes, true], fake_arel.responds.first
end
def test_respond_to_dynamic_finders
relation = Topic.all
["find_by_title", "find_by_title_and_author_name"].each do |method|
- assert_respond_to relation, method, "Topic.all should respond to #{method.inspect}"
+ assert_respond_to relation, method
end
end
def test_respond_to_class_methods_and_scopes
- assert Topic.all.respond_to?(:by_lifo)
+ assert_respond_to Topic.all, :by_lifo
end
def test_find_with_readonly_option
- Developer.all.each { |d| assert !d.readonly? }
+ Developer.all.each { |d| assert_not d.readonly? }
Developer.all.readonly.each { |d| assert d.readonly? }
end
def test_eager_association_loading_of_stis_with_multiple_references
- authors = Author.eager_load(:posts => { :special_comments => { :post => [ :special_comments, :very_special_comment ] } }).
- order('comments.body, very_special_comments_posts.body').where('posts.id = 4').to_a
+ authors = Author.eager_load(posts: { special_comments: { post: [ :special_comments, :very_special_comment ] } }).
+ order("comments.body, very_special_comments_posts.body").where("posts.id = 4").to_a
assert_equal [authors(:david)], authors
assert_no_queries do
@@ -542,27 +525,27 @@ class RelationTest < ActiveRecord::TestCase
def test_find_with_preloaded_associations
assert_queries(2) do
- posts = Post.preload(:comments).order('posts.id')
+ posts = Post.preload(:comments).order("posts.id")
assert posts.first.comments.first
end
assert_queries(2) do
- posts = Post.preload(:comments).order('posts.id')
+ posts = Post.preload(:comments).order("posts.id")
assert posts.first.comments.first
end
assert_queries(2) do
- posts = Post.preload(:author).order('posts.id')
+ posts = Post.preload(:author).order("posts.id")
assert posts.first.author
end
assert_queries(2) do
- posts = Post.preload(:author).order('posts.id')
+ posts = Post.preload(:author).order("posts.id")
assert posts.first.author
end
assert_queries(3) do
- posts = Post.preload(:author, :comments).order('posts.id')
+ posts = Post.preload(:author, :comments).order("posts.id")
assert posts.first.author
assert posts.first.comments.first
end
@@ -577,58 +560,48 @@ class RelationTest < ActiveRecord::TestCase
def test_find_with_included_associations
assert_queries(2) do
- posts = Post.includes(:comments).order('posts.id')
+ posts = Post.includes(:comments).order("posts.id")
assert posts.first.comments.first
end
assert_queries(2) do
- posts = Post.all.includes(:comments).order('posts.id')
+ posts = Post.all.includes(:comments).order("posts.id")
assert posts.first.comments.first
end
assert_queries(2) do
- posts = Post.includes(:author).order('posts.id')
+ posts = Post.includes(:author).order("posts.id")
assert posts.first.author
end
assert_queries(3) do
- posts = Post.includes(:author, :comments).order('posts.id')
+ posts = Post.includes(:author, :comments).order("posts.id")
assert posts.first.author
assert posts.first.comments.first
end
end
- def test_default_scope_with_conditions_string
- assert_equal Developer.where(name: 'David').map(&:id).sort, DeveloperCalledDavid.all.map(&:id).sort
- assert_nil DeveloperCalledDavid.create!.name
- end
-
- def test_default_scope_with_conditions_hash
- assert_equal Developer.where(name: 'Jamis').map(&:id).sort, DeveloperCalledJamis.all.map(&:id).sort
- assert_equal 'Jamis', DeveloperCalledJamis.create!.name
- end
-
def test_default_scoping_finder_methods
- developers = DeveloperCalledDavid.order('id').map(&:id).sort
- assert_equal Developer.where(name: 'David').map(&:id).sort, developers
+ developers = DeveloperCalledDavid.order("id").map(&:id).sort
+ assert_equal Developer.where(name: "David").map(&:id).sort, developers
end
def test_includes_with_select
- query = Post.select('comments_count AS ranking').order('ranking').includes(:comments)
+ query = Post.select("comments_count AS ranking").order("ranking").includes(:comments)
.where(comments: { id: 1 })
- assert_equal ['comments_count AS ranking'], query.select_values
+ assert_equal ["comments_count AS ranking"], query.select_values
assert_equal 1, query.to_a.size
end
def test_preloading_with_associations_and_merges
- post = Post.create! title: 'Uhuu', body: 'body'
+ post = Post.create! title: "Uhuu", body: "body"
reader = Reader.create! post_id: post.id, person_id: 1
- comment = Comment.create! post_id: post.id, body: 'body'
+ comment = Comment.create! post_id: post.id, body: "body"
- assert !comment.respond_to?(:readers)
+ assert_not_respond_to comment, :readers
- post_rel = Post.preload(:readers).joins(:readers).where(title: 'Uhuu')
+ post_rel = Post.preload(:readers).joins(:readers).where(title: "Uhuu")
result_comment = Comment.joins(:post).merge(post_rel).to_a.first
assert_equal comment, result_comment
@@ -637,7 +610,7 @@ class RelationTest < ActiveRecord::TestCase
assert_equal [reader], result_comment.post.readers.to_a
end
- post_rel = Post.includes(:readers).where(title: 'Uhuu')
+ post_rel = Post.includes(:readers).where(title: "Uhuu")
result_comment = Comment.joins(:post).merge(post_rel).first
assert_equal comment, result_comment
@@ -648,17 +621,17 @@ class RelationTest < ActiveRecord::TestCase
end
def test_preloading_with_associations_default_scopes_and_merges
- post = Post.create! title: 'Uhuu', body: 'body'
+ post = Post.create! title: "Uhuu", body: "body"
reader = Reader.create! post_id: post.id, person_id: 1
- post_rel = PostWithPreloadDefaultScope.preload(:readers).joins(:readers).where(title: 'Uhuu')
+ post_rel = PostWithPreloadDefaultScope.preload(:readers).joins(:readers).where(title: "Uhuu")
result_post = PostWithPreloadDefaultScope.all.merge(post_rel).to_a.first
assert_no_queries do
assert_equal [reader], result_post.readers.to_a
end
- post_rel = PostWithIncludesDefaultScope.includes(:readers).where(title: 'Uhuu')
+ post_rel = PostWithIncludesDefaultScope.includes(:readers).where(title: "Uhuu")
result_post = PostWithIncludesDefaultScope.all.merge(post_rel).to_a.first
assert_no_queries do
@@ -670,11 +643,11 @@ class RelationTest < ActiveRecord::TestCase
posts = Post.preload(:comments)
post = posts.find { |p| p.id == 1 }
assert_equal 2, post.comments.size
- assert post.comments.include?(comments(:greetings))
+ assert_includes post.comments, comments(:greetings)
post = Post.where("posts.title = 'Welcome to the weblog'").preload(:comments).first
assert_equal 2, post.comments.size
- assert post.comments.include?(comments(:greetings))
+ assert_includes post.comments, comments(:greetings)
posts = Post.preload(:last_comment)
post = posts.find { |p| p.id == 1 }
@@ -683,9 +656,9 @@ class RelationTest < ActiveRecord::TestCase
def test_to_sql_on_eager_join
expected = assert_sql {
- Post.eager_load(:last_comment).order('comments.id DESC').to_a
+ Post.eager_load(:last_comment).order("comments.id DESC").to_a
}.first
- actual = Post.eager_load(:last_comment).order('comments.id DESC').to_sql
+ actual = Post.eager_load(:last_comment).order("comments.id DESC").to_sql
assert_equal expected, actual
end
@@ -696,7 +669,7 @@ class RelationTest < ActiveRecord::TestCase
end
def test_loading_with_one_association_with_non_preload
- posts = Post.eager_load(:last_comment).order('comments.id DESC')
+ posts = Post.eager_load(:last_comment).order("comments.id DESC")
post = posts.find { |p| p.id == 1 }
assert_equal Post.find(1).last_comment, post.last_comment
end
@@ -707,7 +680,6 @@ class RelationTest < ActiveRecord::TestCase
expected_taggings = taggings(:welcome_general, :thinking_general)
assert_no_queries do
- assert_equal expected_taggings, author.taggings.distinct.sort_by(&:id)
assert_equal expected_taggings, author.taggings.uniq.sort_by(&:id)
end
@@ -720,40 +692,40 @@ class RelationTest < ActiveRecord::TestCase
author = Author.all.find_by_id!(authors(:david).id)
assert_equal "David", author.name
- assert_raises(ActiveRecord::RecordNotFound) { Author.all.find_by_id_and_name!(20, 'invalid') }
+ assert_raises(ActiveRecord::RecordNotFound) { Author.all.find_by_id_and_name!(20, "invalid") }
end
def test_find_id
authors = Author.all
david = authors.find(authors(:david).id)
- assert_equal 'David', david.name
+ assert_equal "David", david.name
- assert_raises(ActiveRecord::RecordNotFound) { authors.where(:name => 'lifo').find('42') }
+ assert_raises(ActiveRecord::RecordNotFound) { authors.where(name: "lifo").find("42") }
end
def test_find_ids
- authors = Author.order('id ASC')
+ authors = Author.order("id ASC")
results = authors.find(authors(:david).id, authors(:mary).id)
assert_kind_of Array, results
assert_equal 2, results.size
- assert_equal 'David', results[0].name
- assert_equal 'Mary', results[1].name
+ assert_equal "David", results[0].name
+ assert_equal "Mary", results[1].name
assert_equal results, authors.find([authors(:david).id, authors(:mary).id])
- assert_raises(ActiveRecord::RecordNotFound) { authors.where(:name => 'lifo').find(authors(:david).id, '42') }
- assert_raises(ActiveRecord::RecordNotFound) { authors.find(['42', 43]) }
+ assert_raises(ActiveRecord::RecordNotFound) { authors.where(name: "lifo").find(authors(:david).id, "42") }
+ assert_raises(ActiveRecord::RecordNotFound) { authors.find(["42", 43]) }
end
def test_find_in_empty_array
- authors = Author.all.where(:id => [])
- assert authors.to_a.blank?
+ authors = Author.all.where(id: [])
+ assert_predicate authors.to_a, :blank?
end
def test_where_with_ar_object
author = Author.first
- authors = Author.all.where(:id => author)
+ authors = Author.all.where(id: author)
assert_equal 1, authors.to_a.length
end
@@ -763,15 +735,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])
@@ -781,25 +744,25 @@ class RelationTest < ActiveRecord::TestCase
def test_find_all_using_where_twice_should_or_the_relation
david = authors(:david)
relation = Author.unscoped
- relation = relation.where(:name => david.name)
- relation = relation.where(:name => 'Santiago')
- relation = relation.where(:id => david.id)
+ relation = relation.where(name: david.name)
+ relation = relation.where(name: "Santiago")
+ relation = relation.where(id: david.id)
assert_equal [], relation.to_a
end
def test_multi_where_ands_queries
relation = Author.unscoped
david = authors(:david)
- sql = relation.where(:name => david.name).where(:name => 'Santiago').to_sql
- assert_match('AND', sql)
+ sql = relation.where(name: david.name).where(name: "Santiago").to_sql
+ assert_match("AND", sql)
end
def test_find_all_with_multiple_should_use_and
david = authors(:david)
relation = [
- { :name => david.name },
- { :name => 'Santiago' },
- { :name => 'tenderlove' },
+ { name: david.name },
+ { name: "Santiago" },
+ { name: "tenderlove" },
].inject(Author.unscoped) do |memo, param|
memo.where(param)
end
@@ -815,20 +778,18 @@ class RelationTest < ActiveRecord::TestCase
def test_find_all_using_where_with_relation
david = authors(:david)
- # switching the lines below would succeed in current rails
- # assert_queries(2) {
assert_queries(1) {
- relation = Author.where(:id => Author.where(:id => david.id))
+ relation = Author.where(id: Author.where(id: david.id))
assert_equal [david], relation.to_a
}
assert_queries(1) {
- relation = Author.where('id in (?)', Author.where(id: david).select(:id))
+ relation = Author.where("id in (?)", Author.where(id: david).select(:id))
assert_equal [david], relation.to_a
}
assert_queries(1) do
- relation = Author.where('id in (:author_ids)', author_ids: Author.where(id: david).select(:id))
+ relation = Author.where("id in (:author_ids)", author_ids: Author.where(id: david).select(:id))
assert_equal [david], relation.to_a
end
end
@@ -843,22 +804,20 @@ class RelationTest < ActiveRecord::TestCase
end
assert_queries(1) do
- relation = Post.where('id in (?)', david.posts.select(:id))
- assert_equal davids_posts, relation.order(:id).to_a, 'should process Relation as bind variables'
+ relation = Post.where("id in (?)", david.posts.select(:id))
+ assert_equal davids_posts, relation.order(:id).to_a, "should process Relation as bind variables"
end
assert_queries(1) do
- relation = Post.where('id in (:post_ids)', post_ids: david.posts.select(:id))
- assert_equal davids_posts, relation.order(:id).to_a, 'should process Relation as named bind variables'
+ relation = Post.where("id in (:post_ids)", post_ids: david.posts.select(:id))
+ assert_equal davids_posts, relation.order(:id).to_a, "should process Relation as named bind variables"
end
end
def test_find_all_using_where_with_relation_and_alternate_primary_key
cool_first = minivans(:cool_first)
- # switching the lines below would succeed in current rails
- # assert_queries(2) {
assert_queries(1) {
- relation = Minivan.where(:minivan_id => Minivan.where(:name => cool_first.name))
+ relation = Minivan.where(minivan_id: Minivan.where(name: cool_first.name))
assert_equal [cool_first], relation.to_a
}
end
@@ -866,10 +825,10 @@ class RelationTest < ActiveRecord::TestCase
def test_find_all_using_where_with_relation_does_not_alter_select_values
david = authors(:david)
- subquery = Author.where(:id => david.id)
+ subquery = Author.where(id: david.id)
assert_queries(1) {
- relation = Author.where(:id => subquery)
+ relation = Author.where(id: subquery)
assert_equal [david], relation.to_a
}
@@ -879,94 +838,32 @@ class RelationTest < ActiveRecord::TestCase
def test_find_all_using_where_with_relation_with_joins
david = authors(:david)
assert_queries(1) {
- relation = Author.where(:id => Author.joins(:posts).where(:id => david.id))
+ relation = Author.where(id: Author.joins(:posts).where(id: david.id))
assert_equal [david], relation.to_a
}
end
-
def test_find_all_using_where_with_relation_with_select_to_build_subquery
david = authors(:david)
assert_queries(1) {
- relation = Author.where(:name => Author.where(:id => david.id).select(:name))
+ relation = Author.where(name: Author.where(id: david.id).select(:name))
assert_equal [david], relation.to_a
}
end
- def test_exists
- davids = Author.where(:name => 'David')
- assert davids.exists?
- assert davids.exists?(authors(:david).id)
- assert ! davids.exists?(authors(:mary).id)
- assert ! davids.exists?("42")
- assert ! davids.exists?(42)
- assert ! davids.exists?(davids.new.id)
-
- fake = Author.where(:name => 'fake author')
- assert ! fake.exists?
- assert ! fake.exists?(authors(:david).id)
- end
-
- def test_exists_uses_existing_scope
- post = authors(:david).posts.first
- authors = Author.includes(:posts).where(name: "David", posts: { id: post.id })
- assert authors.exists?(authors(:david).id)
- end
-
def test_last
authors = Author.all
assert_equal authors(:bob), authors.last
end
- def test_destroy_all
- davids = Author.where(:name => 'David')
-
- # Force load
- assert_equal [authors(:david)], davids.to_a
- assert davids.loaded?
-
- assert_difference('Author.count', -1) { davids.destroy_all }
-
- assert_equal [], davids.to_a
- assert davids.loaded?
- end
-
- def test_delete_all
- davids = Author.where(:name => 'David')
-
- assert_difference('Author.count', -1) { davids.delete_all }
- assert ! davids.loaded?
- end
-
- def test_delete_all_loaded
- davids = Author.where(:name => 'David')
-
- # Force load
- assert_equal [authors(:david)], davids.to_a
- assert davids.loaded?
-
- assert_difference('Author.count', -1) { davids.delete_all }
-
- assert_equal [], davids.to_a
- assert davids.loaded?
- end
-
- def test_delete_all_with_unpermitted_relation_raises_error
- assert_raises(ActiveRecord::ActiveRecordError) { Author.limit(10).delete_all }
- assert_raises(ActiveRecord::ActiveRecordError) { Author.distinct.delete_all }
- assert_raises(ActiveRecord::ActiveRecordError) { Author.group(:name).delete_all }
- assert_raises(ActiveRecord::ActiveRecordError) { Author.having('SUM(id) < 3').delete_all }
- assert_raises(ActiveRecord::ActiveRecordError) { Author.offset(10).delete_all }
- end
-
def test_select_with_aggregates
posts = Post.select(:title, :body)
assert_equal 11, posts.count(:all)
assert_equal 11, posts.size
- assert posts.any?
- assert posts.many?
- assert_not posts.empty?
+ assert_predicate posts, :any?
+ assert_predicate posts, :many?
+ assert_not_empty posts
end
def test_select_takes_a_variable_list_of_args
@@ -995,8 +892,13 @@ class RelationTest < ActiveRecord::TestCase
assert_equal 11, posts.count(:all)
assert_equal 11, posts.count(:id)
- assert_equal 1, posts.where('comments_count > 1').count
- assert_equal 9, posts.where(:comments_count => 0).count
+ assert_equal 3, posts.where("comments_count > 1").count
+ assert_equal 6, posts.where(comments_count: 0).count
+ end
+
+ def test_count_with_block
+ posts = Post.all
+ assert_equal 8, posts.count { |p| p.comments_count.even? }
end
def test_count_on_association_relation
@@ -1007,42 +909,52 @@ class RelationTest < ActiveRecord::TestCase
assert_equal author.posts.where(author_id: author.id).size, posts.count
assert_equal 0, author.posts.where(author_id: another_author.id).size
- assert author.posts.where(author_id: another_author.id).empty?
+ assert_empty author.posts.where(author_id: another_author.id)
end
def test_count_with_distinct
posts = Post.all
- assert_equal 3, posts.distinct(true).count(:comments_count)
+ assert_equal 4, posts.distinct(true).count(:comments_count)
assert_equal 11, posts.distinct(false).count(:comments_count)
- assert_equal 3, posts.distinct(true).select(:comments_count).count
+ assert_equal 4, posts.distinct(true).select(:comments_count).count
assert_equal 11, posts.distinct(false).select(:comments_count).count
end
- def test_update_all_with_scope
- tag = Tag.first
- Post.tagged_with(tag.id).update_all title: "rofl"
- list = Post.tagged_with(tag.id).all.to_a
- assert_operator list.length, :>, 0
- list.each { |post| assert_equal 'rofl', post.title }
+ def test_size_with_distinct
+ posts = Post.distinct.select(:author_id, :comments_count)
+ assert_queries(1) { assert_equal 8, posts.size }
+ assert_queries(1) { assert_equal 8, posts.load.size }
+ end
+
+ def test_size_with_eager_loading_and_custom_order
+ posts = Post.includes(:comments).order("comments.id")
+ assert_queries(1) { assert_equal 11, posts.size }
+ assert_queries(1) { assert_equal 11, posts.load.size }
+ end
+
+ def test_size_with_eager_loading_and_custom_order_and_distinct
+ posts = Post.includes(:comments).order("comments.id").distinct
+ assert_queries(1) { assert_equal 11, posts.size }
+ assert_queries(1) { assert_equal 11, posts.load.size }
end
def test_count_explicit_columns
- Post.update_all(:comments_count => nil)
+ Post.update_all(comments_count: nil)
posts = Post.all
- assert_equal [0], posts.select('comments_count').where('id is not null').group('id').order('id').count.values.uniq
- assert_equal 0, posts.where('id is not null').select('comments_count').count
+ assert_equal [0], posts.select("comments_count").where("id is not null").group("id").order("id").count.values.uniq
+ assert_equal 0, posts.where("id is not null").select("comments_count").count
- assert_equal 11, posts.select('comments_count').count('id')
- assert_equal 0, posts.select('comments_count').count
+ assert_equal 11, posts.select("comments_count").count("id")
+ assert_equal 0, posts.select("comments_count").count
assert_equal 0, posts.count(:comments_count)
- assert_equal 0, posts.count('comments_count')
+ assert_equal 0, posts.count("comments_count")
end
def test_multiple_selects
- post = Post.all.select('comments_count').select('title').order("id ASC").first
+ post = Post.all.select("comments_count").select("title").order("id ASC").first
assert_equal "Welcome to the weblog", post.title
assert_equal 2, post.comments_count
end
@@ -1051,31 +963,31 @@ class RelationTest < ActiveRecord::TestCase
posts = Post.all
assert_queries(1) { assert_equal 11, posts.size }
- assert ! posts.loaded?
+ assert_not_predicate posts, :loaded?
- best_posts = posts.where(:comments_count => 0)
- best_posts.to_a # force load
- assert_no_queries { assert_equal 9, best_posts.size }
+ best_posts = posts.where(comments_count: 0)
+ best_posts.load # force load
+ assert_no_queries { assert_equal 6, best_posts.size }
end
def test_size_with_limit
posts = Post.limit(10)
assert_queries(1) { assert_equal 10, posts.size }
- assert ! posts.loaded?
+ assert_not_predicate posts, :loaded?
- best_posts = posts.where(:comments_count => 0)
- best_posts.to_a # force load
- assert_no_queries { assert_equal 9, best_posts.size }
+ best_posts = posts.where(comments_count: 0)
+ best_posts.load # force load
+ assert_no_queries { assert_equal 6, best_posts.size }
end
def test_size_with_zero_limit
posts = Post.limit(0)
assert_no_queries { assert_equal 0, posts.size }
- assert ! posts.loaded?
+ assert_not_predicate posts, :loaded?
- posts.to_a # force load
+ posts.load # force load
assert_no_queries { assert_equal 0, posts.size }
end
@@ -1083,13 +995,13 @@ class RelationTest < ActiveRecord::TestCase
posts = Post.limit(0)
assert_no_queries { assert_equal true, posts.empty? }
- assert ! posts.loaded?
+ assert_not_predicate posts, :loaded?
end
def test_count_complex_chained_relations
- posts = Post.select('comments_count').where('id is not null').group("author_id").where("comments_count > 0")
+ posts = Post.select("comments_count").where("id is not null").group("author_id").where("comments_count > 0")
- expected = { 1 => 2 }
+ expected = { 1 => 4, 2 => 1 }
assert_equal expected, posts.count
end
@@ -1097,14 +1009,14 @@ class RelationTest < ActiveRecord::TestCase
posts = Post.all
assert_queries(1) { assert_equal false, posts.empty? }
- assert ! posts.loaded?
+ assert_not_predicate posts, :loaded?
- no_posts = posts.where(:title => "")
+ no_posts = posts.where(title: "")
assert_queries(1) { assert_equal true, no_posts.empty? }
- assert ! no_posts.loaded?
+ assert_not_predicate no_posts, :loaded?
- best_posts = posts.where(:comments_count => 0)
- best_posts.to_a # force load
+ best_posts = posts.where(comments_count: 0)
+ best_posts.load # force load
assert_no_queries { assert_equal false, best_posts.empty? }
end
@@ -1112,11 +1024,11 @@ class RelationTest < ActiveRecord::TestCase
posts = Post.select("comments_count").where("id is not null").group("author_id").where("comments_count > 0")
assert_queries(1) { assert_equal false, posts.empty? }
- assert ! posts.loaded?
+ assert_not_predicate posts, :loaded?
- no_posts = posts.where(:title => "")
+ no_posts = posts.where(title: "")
assert_queries(1) { assert_equal true, no_posts.empty? }
- assert ! no_posts.loaded?
+ assert_not_predicate no_posts, :loaded?
end
def test_any
@@ -1128,17 +1040,17 @@ class RelationTest < ActiveRecord::TestCase
# the SHOW TABLES result to be cached so we don't have to do it again in the block.
#
# This is obviously a rubbish fix but it's the best I can come up with for now...
- posts.where(:id => nil).any?
+ posts.where(id: nil).any?
assert_queries(3) do
assert posts.any? # Uses COUNT()
- assert ! posts.where(:id => nil).any?
+ assert_not_predicate posts.where(id: nil), :any?
- assert posts.any? {|p| p.id > 0 }
- assert ! posts.any? {|p| p.id <= 0 }
+ assert posts.any? { |p| p.id > 0 }
+ assert_not posts.any? { |p| p.id <= 0 }
end
- assert posts.loaded?
+ assert_predicate posts, :loaded?
end
def test_many
@@ -1146,50 +1058,60 @@ class RelationTest < ActiveRecord::TestCase
assert_queries(2) do
assert posts.many? # Uses COUNT()
- assert posts.many? {|p| p.id > 0 }
- assert ! posts.many? {|p| p.id < 2 }
+ assert posts.many? { |p| p.id > 0 }
+ assert_not posts.many? { |p| p.id < 2 }
end
- assert posts.loaded?
+ assert_predicate posts, :loaded?
end
def test_many_with_limits
posts = Post.all
- assert posts.many?
- assert ! posts.limit(1).many?
+ assert_predicate posts, :many?
+ assert_not_predicate posts.limit(1), :many?
end
def test_none?
posts = Post.all
assert_queries(1) do
- assert ! posts.none? # Uses COUNT()
+ assert_not posts.none? # Uses COUNT()
end
- assert ! posts.loaded?
+ assert_not_predicate posts, :loaded?
assert_queries(1) do
- assert posts.none? {|p| p.id < 0 }
- assert ! posts.none? {|p| p.id == 1 }
+ assert posts.none? { |p| p.id < 0 }
+ assert_not posts.none? { |p| p.id == 1 }
end
- assert posts.loaded?
+ assert_predicate posts, :loaded?
end
def test_one
posts = Post.all
assert_queries(1) do
- assert ! posts.one? # Uses COUNT()
+ assert_not posts.one? # Uses COUNT()
end
- assert ! posts.loaded?
+ assert_not_predicate posts, :loaded?
assert_queries(1) do
- assert ! posts.one? {|p| p.id < 3 }
- assert posts.one? {|p| p.id == 1 }
+ assert_not posts.one? { |p| p.id < 3 }
+ assert posts.one? { |p| p.id == 1 }
end
- assert posts.loaded?
+ assert_predicate posts, :loaded?
+ end
+
+ def test_to_a_should_dup_target
+ posts = Post.all
+
+ original_size = posts.size
+ removed = posts.to_a.pop
+
+ assert_equal original_size, posts.size
+ assert_includes posts.to_a, removed
end
def test_build
@@ -1200,11 +1122,11 @@ class RelationTest < ActiveRecord::TestCase
end
def test_scoped_build
- posts = Post.where(:title => 'You told a lie')
+ posts = Post.where(title: "You told a lie")
post = posts.new
assert_kind_of Post, post
- assert_equal 'You told a lie', post.title
+ assert_equal "You told a lie", post.title
end
def test_create
@@ -1212,11 +1134,11 @@ class RelationTest < ActiveRecord::TestCase
sparrow = birds.create
assert_kind_of Bird, sparrow
- assert !sparrow.persisted?
+ assert_not_predicate sparrow, :persisted?
- hen = birds.where(:name => 'hen').create
- assert hen.persisted?
- assert_equal 'hen', hen.name
+ hen = birds.where(name: "hen").create
+ assert_predicate hen, :persisted?
+ assert_equal "hen", hen.name
end
def test_create_bang
@@ -1224,201 +1146,248 @@ class RelationTest < ActiveRecord::TestCase
assert_raises(ActiveRecord::RecordInvalid) { birds.create! }
- hen = birds.where(:name => 'hen').create!
+ hen = birds.where(name: "hen").create!
assert_kind_of Bird, hen
- assert hen.persisted?
- assert_equal 'hen', hen.name
+ assert_predicate hen, :persisted?
+ assert_equal "hen", hen.name
+ end
+
+ def test_create_with_polymorphic_association
+ author = authors(:david)
+ post = posts(:welcome)
+ comment = Comment.where(post: post, author: author).create!(body: "hello")
+
+ assert_equal author, comment.author
+ assert_equal post, comment.post
end
def test_first_or_create
- parrot = Bird.where(:color => 'green').first_or_create(:name => 'parrot')
+ parrot = Bird.where(color: "green").first_or_create(name: "parrot")
assert_kind_of Bird, parrot
- assert parrot.persisted?
- assert_equal 'parrot', parrot.name
- assert_equal 'green', parrot.color
+ assert_predicate parrot, :persisted?
+ assert_equal "parrot", parrot.name
+ assert_equal "green", parrot.color
- same_parrot = Bird.where(:color => 'green').first_or_create(:name => 'parakeet')
+ same_parrot = Bird.where(color: "green").first_or_create(name: "parakeet")
assert_kind_of Bird, same_parrot
- assert same_parrot.persisted?
+ assert_predicate same_parrot, :persisted?
assert_equal parrot, same_parrot
end
def test_first_or_create_with_no_parameters
- parrot = Bird.where(:color => 'green').first_or_create
+ parrot = Bird.where(color: "green").first_or_create
assert_kind_of Bird, parrot
- assert !parrot.persisted?
- assert_equal 'green', parrot.color
+ assert_not_predicate parrot, :persisted?
+ assert_equal "green", parrot.color
end
def test_first_or_create_with_block
- parrot = Bird.where(:color => 'green').first_or_create { |bird| bird.name = 'parrot' }
+ parrot = Bird.where(color: "green").first_or_create { |bird| bird.name = "parrot" }
assert_kind_of Bird, parrot
- assert parrot.persisted?
- assert_equal 'green', parrot.color
- assert_equal 'parrot', parrot.name
+ assert_predicate parrot, :persisted?
+ assert_equal "green", parrot.color
+ assert_equal "parrot", parrot.name
- same_parrot = Bird.where(:color => 'green').first_or_create { |bird| bird.name = 'parakeet' }
+ same_parrot = Bird.where(color: "green").first_or_create { |bird| bird.name = "parakeet" }
assert_equal parrot, same_parrot
end
def test_first_or_create_with_array
- several_green_birds = Bird.where(:color => 'green').first_or_create([{:name => 'parrot'}, {:name => 'parakeet'}])
+ several_green_birds = Bird.where(color: "green").first_or_create([{ name: "parrot" }, { name: "parakeet" }])
assert_kind_of Array, several_green_birds
several_green_birds.each { |bird| assert bird.persisted? }
- same_parrot = Bird.where(:color => 'green').first_or_create([{:name => 'hummingbird'}, {:name => 'macaw'}])
+ same_parrot = Bird.where(color: "green").first_or_create([{ name: "hummingbird" }, { name: "macaw" }])
assert_kind_of Bird, same_parrot
assert_equal several_green_birds.first, same_parrot
end
def test_first_or_create_bang_with_valid_options
- parrot = Bird.where(:color => 'green').first_or_create!(:name => 'parrot')
+ parrot = Bird.where(color: "green").first_or_create!(name: "parrot")
assert_kind_of Bird, parrot
- assert parrot.persisted?
- assert_equal 'parrot', parrot.name
- assert_equal 'green', parrot.color
+ assert_predicate parrot, :persisted?
+ assert_equal "parrot", parrot.name
+ assert_equal "green", parrot.color
- same_parrot = Bird.where(:color => 'green').first_or_create!(:name => 'parakeet')
+ same_parrot = Bird.where(color: "green").first_or_create!(name: "parakeet")
assert_kind_of Bird, same_parrot
- assert same_parrot.persisted?
+ assert_predicate same_parrot, :persisted?
assert_equal parrot, same_parrot
end
def test_first_or_create_bang_with_invalid_options
- assert_raises(ActiveRecord::RecordInvalid) { Bird.where(:color => 'green').first_or_create!(:pirate_id => 1) }
+ assert_raises(ActiveRecord::RecordInvalid) { Bird.where(color: "green").first_or_create!(pirate_id: 1) }
end
def test_first_or_create_bang_with_no_parameters
- assert_raises(ActiveRecord::RecordInvalid) { Bird.where(:color => 'green').first_or_create! }
+ assert_raises(ActiveRecord::RecordInvalid) { Bird.where(color: "green").first_or_create! }
end
def test_first_or_create_bang_with_valid_block
- parrot = Bird.where(:color => 'green').first_or_create! { |bird| bird.name = 'parrot' }
+ parrot = Bird.where(color: "green").first_or_create! { |bird| bird.name = "parrot" }
assert_kind_of Bird, parrot
- assert parrot.persisted?
- assert_equal 'green', parrot.color
- assert_equal 'parrot', parrot.name
+ assert_predicate parrot, :persisted?
+ assert_equal "green", parrot.color
+ assert_equal "parrot", parrot.name
- same_parrot = Bird.where(:color => 'green').first_or_create! { |bird| bird.name = 'parakeet' }
+ same_parrot = Bird.where(color: "green").first_or_create! { |bird| bird.name = "parakeet" }
assert_equal parrot, same_parrot
end
def test_first_or_create_bang_with_invalid_block
assert_raise(ActiveRecord::RecordInvalid) do
- Bird.where(:color => 'green').first_or_create! { |bird| bird.pirate_id = 1 }
+ Bird.where(color: "green").first_or_create! { |bird| bird.pirate_id = 1 }
end
end
def test_first_or_create_with_valid_array
- several_green_birds = Bird.where(:color => 'green').first_or_create!([{:name => 'parrot'}, {:name => 'parakeet'}])
+ several_green_birds = Bird.where(color: "green").first_or_create!([{ name: "parrot" }, { name: "parakeet" }])
assert_kind_of Array, several_green_birds
several_green_birds.each { |bird| assert bird.persisted? }
- same_parrot = Bird.where(:color => 'green').first_or_create!([{:name => 'hummingbird'}, {:name => 'macaw'}])
+ same_parrot = Bird.where(color: "green").first_or_create!([{ name: "hummingbird" }, { name: "macaw" }])
assert_kind_of Bird, same_parrot
assert_equal several_green_birds.first, same_parrot
end
def test_first_or_create_with_invalid_array
- assert_raises(ActiveRecord::RecordInvalid) { Bird.where(:color => 'green').first_or_create!([ {:name => 'parrot'}, {:pirate_id => 1} ]) }
+ assert_raises(ActiveRecord::RecordInvalid) { Bird.where(color: "green").first_or_create!([ { name: "parrot" }, { pirate_id: 1 } ]) }
end
def test_first_or_initialize
- parrot = Bird.where(:color => 'green').first_or_initialize(:name => 'parrot')
+ parrot = Bird.where(color: "green").first_or_initialize(name: "parrot")
assert_kind_of Bird, parrot
- assert !parrot.persisted?
- assert parrot.valid?
- assert parrot.new_record?
- assert_equal 'parrot', parrot.name
- assert_equal 'green', parrot.color
+ assert_not_predicate parrot, :persisted?
+ assert_predicate parrot, :valid?
+ assert_predicate parrot, :new_record?
+ assert_equal "parrot", parrot.name
+ assert_equal "green", parrot.color
end
def test_first_or_initialize_with_no_parameters
- parrot = Bird.where(:color => 'green').first_or_initialize
+ parrot = Bird.where(color: "green").first_or_initialize
assert_kind_of Bird, parrot
- assert !parrot.persisted?
- assert !parrot.valid?
- assert parrot.new_record?
- assert_equal 'green', parrot.color
+ assert_not_predicate parrot, :persisted?
+ assert_not_predicate parrot, :valid?
+ assert_predicate parrot, :new_record?
+ assert_equal "green", parrot.color
end
def test_first_or_initialize_with_block
- parrot = Bird.where(:color => 'green').first_or_initialize { |bird| bird.name = 'parrot' }
+ parrot = Bird.where(color: "green").first_or_initialize { |bird| bird.name = "parrot" }
assert_kind_of Bird, parrot
- assert !parrot.persisted?
- assert parrot.valid?
- assert parrot.new_record?
- assert_equal 'green', parrot.color
- assert_equal 'parrot', parrot.name
+ assert_not_predicate parrot, :persisted?
+ assert_predicate parrot, :valid?
+ assert_predicate parrot, :new_record?
+ assert_equal "green", parrot.color
+ assert_equal "parrot", parrot.name
end
def test_find_or_create_by
- assert_nil Bird.find_by(name: 'bob')
+ assert_nil Bird.find_by(name: "bob")
- bird = Bird.find_or_create_by(name: 'bob')
- assert bird.persisted?
+ bird = Bird.find_or_create_by(name: "bob")
+ assert_predicate bird, :persisted?
- assert_equal bird, Bird.find_or_create_by(name: 'bob')
+ assert_equal bird, Bird.find_or_create_by(name: "bob")
end
def test_find_or_create_by_with_create_with
- assert_nil Bird.find_by(name: 'bob')
+ assert_nil Bird.find_by(name: "bob")
- bird = Bird.create_with(color: 'green').find_or_create_by(name: 'bob')
- assert bird.persisted?
- assert_equal 'green', bird.color
+ bird = Bird.create_with(color: "green").find_or_create_by(name: "bob")
+ assert_predicate bird, :persisted?
+ assert_equal "green", bird.color
- assert_equal bird, Bird.create_with(color: 'blue').find_or_create_by(name: 'bob')
+ assert_equal bird, Bird.create_with(color: "blue").find_or_create_by(name: "bob")
end
def test_find_or_create_by!
- assert_raises(ActiveRecord::RecordInvalid) { Bird.find_or_create_by!(color: 'green') }
+ assert_raises(ActiveRecord::RecordInvalid) { Bird.find_or_create_by!(color: "green") }
+ end
+
+ def test_create_or_find_by
+ assert_nil Subscriber.find_by(nick: "bob")
+
+ subscriber = Subscriber.create!(nick: "bob")
+
+ assert_equal subscriber, Subscriber.create_or_find_by(nick: "bob")
+ assert_not_equal subscriber, Subscriber.create_or_find_by(nick: "cat")
+ end
+
+ def test_create_or_find_by_with_non_unique_attributes
+ Subscriber.create!(nick: "bob", name: "the builder")
+
+ assert_raises(ActiveRecord::RecordNotFound) do
+ Subscriber.create_or_find_by(nick: "bob", name: "the cat")
+ end
+ end
+
+ def test_create_or_find_by_within_transaction
+ assert_nil Subscriber.find_by(nick: "bob")
+
+ subscriber = Subscriber.create!(nick: "bob")
+
+ Subscriber.transaction do
+ assert_equal subscriber, Subscriber.create_or_find_by(nick: "bob")
+ assert_not_equal subscriber, Subscriber.create_or_find_by(nick: "cat")
+ end
end
def test_find_or_initialize_by
- assert_nil Bird.find_by(name: 'bob')
+ assert_nil Bird.find_by(name: "bob")
- bird = Bird.find_or_initialize_by(name: 'bob')
- assert bird.new_record?
+ bird = Bird.find_or_initialize_by(name: "bob")
+ assert_predicate bird, :new_record?
bird.save!
- assert_equal bird, Bird.find_or_initialize_by(name: 'bob')
+ assert_equal bird, Bird.find_or_initialize_by(name: "bob")
end
- def test_explicit_create_scope
- hens = Bird.where(:name => 'hen')
- assert_equal 'hen', hens.new.name
+ def test_explicit_create_with
+ hens = Bird.where(name: "hen")
+ assert_equal "hen", hens.new.name
+
+ hens = hens.create_with(name: "cock")
+ assert_equal "cock", hens.new.name
+ end
- hens = hens.create_with(:name => 'cock')
- assert_equal 'cock', hens.new.name
+ def test_create_with_nested_attributes
+ assert_difference("Project.count", 1) do
+ developers = Developer.where(name: "Aaron")
+ developers = developers.create_with(
+ projects_attributes: [{ name: "p1" }]
+ )
+ developers.create!
+ end
end
def test_except
- relation = Post.where(:author_id => 1).order('id ASC').limit(1)
+ relation = Post.where(author_id: 1).order("id ASC").limit(1)
assert_equal [posts(:welcome)], relation.to_a
author_posts = relation.except(:order, :limit)
- assert_equal Post.where(:author_id => 1).to_a, author_posts.to_a
+ assert_equal Post.where(author_id: 1).to_a, author_posts.to_a
all_posts = relation.except(:where, :order, :limit)
assert_equal Post.all, all_posts
end
def test_only
- relation = Post.where(:author_id => 1).order('id ASC').limit(1)
+ relation = Post.where(author_id: 1).order("id ASC").limit(1)
assert_equal [posts(:welcome)], relation.to_a
author_posts = relation.only(:where)
- assert_equal Post.where(:author_id => 1).to_a, author_posts.to_a
+ assert_equal Post.where(author_id: 1).to_a, author_posts.to_a
all_posts = relation.only(:limit)
- assert_equal Post.limit(1).to_a.first, all_posts.first
+ assert_equal Post.limit(1).to_a, all_posts.to_a
end
def test_anonymous_extension
- relation = Post.where(:author_id => 1).order('id ASC').extending do
+ relation = Post.where(author_id: 1).order("id ASC").extending do
def author
- 'lifo'
+ "lifo"
end
end
@@ -1427,7 +1396,7 @@ class RelationTest < ActiveRecord::TestCase
end
def test_named_extension
- relation = Post.where(:author_id => 1).order('id ASC').extending(Post::NamedExtension)
+ relation = Post.where(author_id: 1).order("id ASC").extending(Post::NamedExtension)
assert_equal "lifo", relation.author
assert_equal "lifo", relation.limit(1).author
end
@@ -1437,29 +1406,29 @@ class RelationTest < ActiveRecord::TestCase
end
def test_default_scope_order_with_scope_order
- assert_equal 'zyke', CoolCar.order_using_new_style.limit(1).first.name
- assert_equal 'zyke', FastCar.order_using_new_style.limit(1).first.name
+ assert_equal "zyke", CoolCar.order_using_new_style.limit(1).first.name
+ assert_equal "zyke", FastCar.order_using_new_style.limit(1).first.name
end
def test_order_using_scoping
- car1 = CoolCar.order('id DESC').scoping do
- CoolCar.all.merge!(order: 'id asc').first
+ car1 = CoolCar.order("id DESC").scoping do
+ CoolCar.all.merge!(order: "id asc").first
end
- assert_equal 'zyke', car1.name
+ assert_equal "zyke", car1.name
- car2 = FastCar.order('id DESC').scoping do
- FastCar.all.merge!(order: 'id asc').first
+ car2 = FastCar.order("id DESC").scoping do
+ FastCar.all.merge!(order: "id asc").first
end
- assert_equal 'zyke', car2.name
+ assert_equal "zyke", car2.name
end
def test_unscoped_block_style
- assert_equal 'honda', CoolCar.unscoped { CoolCar.order_using_new_style.limit(1).first.name}
- assert_equal 'honda', FastCar.unscoped { FastCar.order_using_new_style.limit(1).first.name}
+ assert_equal "honda", CoolCar.unscoped { CoolCar.order_using_new_style.limit(1).first.name }
+ assert_equal "honda", FastCar.unscoped { FastCar.order_using_new_style.limit(1).first.name }
end
def test_intersection_with_array
- relation = Author.where(:name => "David")
+ relation = Author.where(name: "David")
rails_author = relation.first
assert_equal [rails_author], [rails_author] & relation
@@ -1471,92 +1440,31 @@ class RelationTest < ActiveRecord::TestCase
end
def test_ordering_with_extra_spaces
- assert_equal authors(:david), Author.order('id DESC , name DESC').last
- end
-
- def test_update_all_with_blank_argument
- assert_raises(ArgumentError) { Comment.update_all({}) }
- end
-
- def test_update_all_with_joins
- comments = Comment.joins(:post).where('posts.id' => posts(:welcome).id)
- count = comments.count
-
- assert_equal count, comments.update_all(:post_id => posts(:thinking).id)
- assert_equal posts(:thinking), comments(:greetings).post
- end
-
- def test_update_all_with_joins_and_limit
- comments = Comment.joins(:post).where('posts.id' => posts(:welcome).id).limit(1)
- assert_equal 1, comments.update_all(:post_id => posts(:thinking).id)
- end
-
- def test_update_all_with_joins_and_limit_and_order
- comments = Comment.joins(:post).where('posts.id' => posts(:welcome).id).order('comments.id').limit(1)
- assert_equal 1, comments.update_all(:post_id => posts(:thinking).id)
- assert_equal posts(:thinking), comments(:greetings).post
- assert_equal posts(:welcome), comments(:more_greetings).post
- end
-
- def test_update_all_with_joins_and_offset
- all_comments = Comment.joins(:post).where('posts.id' => posts(:welcome).id)
- count = all_comments.count
- comments = all_comments.offset(1)
-
- assert_equal count - 1, comments.update_all(:post_id => posts(:thinking).id)
- end
-
- def test_update_all_with_joins_and_offset_and_order
- all_comments = Comment.joins(:post).where('posts.id' => posts(:welcome).id).order('posts.id', 'comments.id')
- count = all_comments.count
- comments = all_comments.offset(1)
-
- assert_equal count - 1, comments.update_all(:post_id => posts(:thinking).id)
- assert_equal posts(:thinking), comments(:more_greetings).post
- assert_equal posts(:welcome), comments(:greetings).post
- end
-
- def test_update_on_relation
- topic1 = TopicWithCallbacks.create! title: 'arel', author_name: nil
- topic2 = TopicWithCallbacks.create! title: 'activerecord', author_name: nil
- topics = TopicWithCallbacks.where(id: [topic1.id, topic2.id])
- topics.update(title: 'adequaterecord')
-
- assert_equal 'adequaterecord', topic1.reload.title
- assert_equal 'adequaterecord', topic2.reload.title
- # Testing that the before_update callbacks have run
- assert_equal 'David', topic1.reload.author_name
- assert_equal 'David', topic2.reload.author_name
+ assert_equal authors(:david), Author.order("id DESC , name DESC").last
end
def test_distinct
- tag1 = Tag.create(:name => 'Foo')
- tag2 = Tag.create(:name => 'Foo')
+ tag1 = Tag.create(name: "Foo")
+ tag2 = Tag.create(name: "Foo")
- query = Tag.select(:name).where(:id => [tag1.id, tag2.id])
+ query = Tag.select(:name).where(id: [tag1.id, tag2.id])
- assert_equal ['Foo', 'Foo'], query.map(&:name)
+ 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) }
+ assert_equal ["Foo"], query.distinct.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)
+ assert_equal ["Foo"], query.distinct(true).map(&:name)
end
+ assert_equal ["Foo", "Foo"], query.distinct(true).distinct(false).map(&:name)
end
def test_doesnt_add_having_values_if_options_are_blank
- scope = Post.having('')
- assert scope.having_clause.empty?
+ scope = Post.having("")
+ assert_empty scope.having_clause
scope = Post.having([])
- assert scope.having_clause.empty?
+ assert_empty scope.having_clause
end
def test_having_with_binds_for_both_where_and_having
@@ -1582,72 +1490,86 @@ class RelationTest < ActiveRecord::TestCase
def test_references_triggers_eager_loading
scope = Post.includes(:comments)
- assert !scope.eager_loading?
- assert scope.references(:comments).eager_loading?
+ assert_not_predicate scope, :eager_loading?
+ assert_predicate scope.references(:comments), :eager_loading?
end
def test_references_doesnt_trigger_eager_loading_if_reference_not_included
scope = Post.references(:comments)
- assert !scope.eager_loading?
+ assert_not_predicate scope, :eager_loading?
end
def test_automatically_added_where_references
- scope = Post.where(:comments => { :body => "Bla" })
- assert_equal ['comments'], scope.references_values
+ scope = Post.where(comments: { body: "Bla" })
+ assert_equal ["comments"], scope.references_values
- scope = Post.where('comments.body' => 'Bla')
- assert_equal ['comments'], scope.references_values
+ scope = Post.where("comments.body" => "Bla")
+ assert_equal ["comments"], scope.references_values
end
def test_automatically_added_where_not_references
scope = Post.where.not(comments: { body: "Bla" })
- assert_equal ['comments'], scope.references_values
+ assert_equal ["comments"], scope.references_values
- scope = Post.where.not('comments.body' => 'Bla')
- assert_equal ['comments'], scope.references_values
+ scope = Post.where.not("comments.body" => "Bla")
+ assert_equal ["comments"], scope.references_values
end
def test_automatically_added_having_references
- scope = Post.having(:comments => { :body => "Bla" })
- assert_equal ['comments'], scope.references_values
+ scope = Post.having(comments: { body: "Bla" })
+ assert_equal ["comments"], scope.references_values
- scope = Post.having('comments.body' => 'Bla')
- assert_equal ['comments'], scope.references_values
+ scope = Post.having("comments.body" => "Bla")
+ assert_equal ["comments"], scope.references_values
end
def test_automatically_added_order_references
- scope = Post.order('comments.body')
- assert_equal ['comments'], scope.references_values
+ scope = Post.order("comments.body")
+ assert_equal ["comments"], scope.references_values
+
+ scope = Post.order(Arel.sql("#{Comment.quoted_table_name}.#{Comment.quoted_primary_key}"))
+ if current_adapter?(:OracleAdapter)
+ assert_equal ["COMMENTS"], scope.references_values
+ else
+ assert_equal ["comments"], scope.references_values
+ end
- scope = Post.order('comments.body', 'yaks.body')
- assert_equal ['comments', 'yaks'], scope.references_values
+ scope = Post.order("comments.body", "yaks.body")
+ assert_equal ["comments", "yaks"], scope.references_values
# Don't infer yaks, let's not go down that road again...
- scope = Post.order('comments.body, yaks.body')
- assert_equal ['comments'], scope.references_values
+ scope = Post.order("comments.body, yaks.body")
+ assert_equal ["comments"], scope.references_values
- scope = Post.order('comments.body asc')
- assert_equal ['comments'], scope.references_values
+ scope = Post.order("comments.body asc")
+ assert_equal ["comments"], scope.references_values
- scope = Post.order('foo(comments.body)')
+ scope = Post.order(Arel.sql("foo(comments.body)"))
assert_equal [], scope.references_values
end
def test_automatically_added_reorder_references
- scope = Post.reorder('comments.body')
+ scope = Post.reorder("comments.body")
assert_equal %w(comments), scope.references_values
- scope = Post.reorder('comments.body', 'yaks.body')
+ scope = Post.reorder(Arel.sql("#{Comment.quoted_table_name}.#{Comment.quoted_primary_key}"))
+ if current_adapter?(:OracleAdapter)
+ assert_equal ["COMMENTS"], scope.references_values
+ else
+ assert_equal ["comments"], scope.references_values
+ end
+
+ scope = Post.reorder("comments.body", "yaks.body")
assert_equal %w(comments yaks), scope.references_values
# Don't infer yaks, let's not go down that road again...
- scope = Post.reorder('comments.body, yaks.body')
+ scope = Post.reorder("comments.body, yaks.body")
assert_equal %w(comments), scope.references_values
- scope = Post.reorder('comments.body asc')
+ scope = Post.reorder("comments.body asc")
assert_equal %w(comments), scope.references_values
- scope = Post.reorder('foo(comments.body)')
+ scope = Post.reorder(Arel.sql("foo(comments.body)"))
assert_equal [], scope.references_values
end
@@ -1672,7 +1594,7 @@ class RelationTest < ActiveRecord::TestCase
# checking if there are topics is used before you actually display them,
# thus it shouldn't invoke an extra count query.
assert_no_queries { assert topics.present? }
- assert_no_queries { assert !topics.blank? }
+ assert_no_queries { assert_not topics.blank? }
# shows count of topics and loops after loading the query should not trigger extra queries either.
assert_no_queries { topics.size }
@@ -1682,7 +1604,7 @@ class RelationTest < ActiveRecord::TestCase
# count always trigger the COUNT query.
assert_queries(1) { topics.count }
- assert topics.loaded?
+ assert_predicate topics, :loaded?
end
test "find_by with hash conditions returns the first matching record" do
@@ -1694,11 +1616,11 @@ class RelationTest < ActiveRecord::TestCase
end
test "find_by with multi-arg conditions returns the first matching record" do
- assert_equal posts(:eager_other), Post.order(:id).find_by('author_id = ?', 2)
+ assert_equal posts(:eager_other), Post.order(:id).find_by("author_id = ?", 2)
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
@@ -1718,7 +1640,7 @@ class RelationTest < ActiveRecord::TestCase
end
test "find_by! with multi-arg conditions returns the first matching record" do
- assert_equal posts(:eager_other), Post.order(:id).find_by!('author_id = ?', 2)
+ assert_equal posts(:eager_other), Post.order(:id).find_by!("author_id = ?", 2)
end
test "find_by! doesn't have implicit ordering" do
@@ -1740,7 +1662,7 @@ class RelationTest < ActiveRecord::TestCase
relation.to_a
assert_raises(ActiveRecord::ImmutableRelation) do
- relation.where! 'foo'
+ relation.where! "foo"
end
end
@@ -1758,7 +1680,7 @@ class RelationTest < ActiveRecord::TestCase
relation.to_a
assert_raises(ActiveRecord::ImmutableRelation) do
- relation.merge! where: 'foo'
+ relation.merge! where: "foo"
end
end
@@ -1773,7 +1695,7 @@ class RelationTest < ActiveRecord::TestCase
test "relations with cached arel can't be mutated [internal API]" do
relation = Post.all
- relation.count
+ relation.arel
assert_raises(ActiveRecord::ImmutableRelation) { relation.limit!(5) }
assert_raises(ActiveRecord::ImmutableRelation) { relation.where!("1 = 2") }
@@ -1789,6 +1711,12 @@ class RelationTest < ActiveRecord::TestCase
assert_equal "#<ActiveRecord::Relation [#{Post.limit(10).map(&:inspect).join(', ')}, ...]>", relation.inspect
end
+ test "relations don't load all records in #inspect" do
+ assert_sql(/LIMIT|ROWNUM <=|FETCH FIRST/) do
+ Post.all.inspect
+ end
+ end
+
test "already-loaded relations don't perform a new query in #inspect" do
relation = Post.limit(2)
relation.to_a
@@ -1800,19 +1728,27 @@ class RelationTest < ActiveRecord::TestCase
end
end
- test 'using a custom table affects the wheres' do
- table_alias = Post.arel_table.alias('omg_posts')
+ test "using a custom table affects the wheres" do
+ post = posts(:welcome)
- table_metadata = ActiveRecord::TableMetadata.new(Post, table_alias)
- predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata)
- relation = ActiveRecord::Relation.new(Post, table_alias, predicate_builder)
- relation.where!(:foo => "bar")
+ assert_equal post, custom_post_relation.where!(title: post.title).take
+ end
+
+ test "using a custom table with joins affects the joins" do
+ post = posts(:welcome)
+
+ assert_equal post, custom_post_relation.joins(:author).where!(title: post.title).take
+ end
- node = relation.arel.constraints.first.grep(Arel::Attributes::Attribute).first
- assert_equal table_alias, node.relation
+ test "arel_attribute respects a custom table" do
+ assert_equal [posts(:sti_comments)], custom_post_relation.ranked_by_comments.limit_by(1).to_a
end
- test '#load' do
+ test "alias_tracker respects a custom table" do
+ assert_equal posts(:welcome), custom_post_relation("categories_posts").joins(:categories).first
+ end
+
+ test "#load" do
relation = Post.all
assert_queries(1) do
assert_equal relation, relation.load
@@ -1820,9 +1756,9 @@ class RelationTest < ActiveRecord::TestCase
assert_no_queries { relation.to_a }
end
- test 'group with select and includes' do
- authors_count = Post.select('author_id, COUNT(author_id) AS num_posts').
- group('author_id').order('author_id').includes(:author).to_a
+ test "group with select and includes" do
+ authors_count = Post.select("author_id, COUNT(author_id) AS num_posts").
+ group("author_id").order("author_id").includes(:author).to_a
assert_no_queries do
result = authors_count.map do |post|
@@ -1843,46 +1779,114 @@ class RelationTest < ActiveRecord::TestCase
test "delegations do not leak to other classes" do
Topic.all.by_lifo
assert Topic.all.class.method_defined?(:by_lifo)
- assert !Post.all.respond_to?(:by_lifo)
+ assert_not_respond_to Post.all, :by_lifo
+ end
+
+ def test_unscope_with_subquery
+ 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']
- left.bind_values += [[column, 20]]
+ def test_locked_should_not_build_arel
+ posts = Post.locked
+ assert_predicate posts, :locked?
+ assert_nothing_raised { posts.lock!(false) }
+ end
- relation = left.unscope(where: :id)
- assert_equal [], relation.bind_values
+ def test_relation_join_method
+ assert_equal "Thank you for the welcome,Thank you again for the welcome", Post.first.comments.join(",")
end
- def test_merging_removes_rhs_bind_parameters
- left = Post.where(id: 20)
- right = Post.where(id: [1,2,3,4])
+ def test_relation_with_private_kernel_method
+ accounts = Account.all
+ assert_equal [accounts(:signals37)], accounts.open
+ assert_equal [accounts(:signals37)], accounts.available
+
+ sub_accounts = SubAccount.all
+ assert_equal [accounts(:signals37)], sub_accounts.open
+ assert_equal [accounts(:signals37)], sub_accounts.available
+ end
- merged = left.merge(right)
- assert_equal [], merged.bind_values
+ test "#skip_query_cache!" do
+ Post.cache do
+ assert_queries(1) do
+ Post.all.load
+ Post.all.load
+ end
+
+ assert_queries(2) do
+ Post.all.skip_query_cache!.load
+ Post.all.skip_query_cache!.load
+ end
+ end
end
- def test_merging_keeps_lhs_bind_parameters
- binds = [ActiveRecord::Relation::QueryAttribute.new("id", 20, Post.type_for_attribute("id"))]
+ test "#skip_query_cache! with an eager load" do
+ Post.cache do
+ assert_queries(1) do
+ Post.eager_load(:comments).load
+ Post.eager_load(:comments).load
+ end
- right = Post.where(id: 20)
- left = Post.where(id: 10)
+ assert_queries(2) do
+ Post.eager_load(:comments).skip_query_cache!.load
+ Post.eager_load(:comments).skip_query_cache!.load
+ end
+ end
+ end
+
+ test "#skip_query_cache! with a preload" do
+ Post.cache do
+ assert_queries(2) do
+ Post.preload(:comments).load
+ Post.preload(:comments).load
+ end
- merged = left.merge(right)
- assert_equal binds, merged.bound_attributes
+ assert_queries(4) do
+ Post.preload(:comments).skip_query_cache!.load
+ Post.preload(:comments).skip_query_cache!.load
+ end
+ end
end
- def test_merging_reorders_bind_params
- post = Post.first
- right = Post.where(id: post.id)
- left = Post.where(title: post.title)
+ test "#where with set" do
+ david = authors(:david)
+ mary = authors(:mary)
- merged = left.merge(right)
- assert_equal post, merged.first
+ authors = Author.where(name: ["David", "Mary"].to_set)
+ assert_equal [david, mary], authors
end
- def test_relation_join_method
- assert_equal 'Thank you for the welcome,Thank you again for the welcome', Post.first.comments.join(",")
+ test "#where with empty set" do
+ authors = Author.where(name: Set.new)
+ assert_empty authors
end
+
+ private
+ def custom_post_relation(alias_name = "omg_posts")
+ table_alias = Post.arel_table.alias(alias_name)
+ table_metadata = ActiveRecord::TableMetadata.new(Post, table_alias)
+ predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata)
+
+ ActiveRecord::Relation.create(
+ Post,
+ table: table_alias,
+ predicate_builder: predicate_builder
+ )
+ end
end
diff --git a/activerecord/test/cases/reload_models_test.rb b/activerecord/test/cases/reload_models_test.rb
index 431fbf1297..72f4bfaf6d 100644
--- a/activerecord/test/cases/reload_models_test.rb
+++ b/activerecord/test/cases/reload_models_test.rb
@@ -1,22 +1,26 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/owner'
-require 'models/pet'
+require "models/owner"
+require "models/pet"
class ReloadModelsTest < ActiveRecord::TestCase
+ include ActiveSupport::Testing::Isolation
+
fixtures :pets, :owners
def test_has_one_with_reload
- pet = Pet.find_by_name('parrot')
- pet.owner = Owner.find_by_name('ashley')
+ pet = Pet.find_by_name("parrot")
+ pet.owner = Owner.find_by_name("ashley")
# Reload the class Owner, simulating auto-reloading of model classes in a
# development environment. Note that meanwhile the class Pet is not
# reloaded, simulating a class that is present in a plugin.
Object.class_eval { remove_const :Owner }
- Kernel.load(File.expand_path(File.join(File.dirname(__FILE__), "../models/owner.rb")))
+ Kernel.load(File.expand_path("../models/owner.rb", __dir__))
- pet = Pet.find_by_name('parrot')
- pet.owner = Owner.find_by_name('ashley')
- assert_equal pet.owner, Owner.find_by_name('ashley')
+ pet = Pet.find_by_name("parrot")
+ pet.owner = Owner.find_by_name("ashley")
+ assert_equal pet.owner, Owner.find_by_name("ashley")
end
-end
+end unless in_memory_db?
diff --git a/activerecord/test/cases/reserved_word_test.rb b/activerecord/test/cases/reserved_word_test.rb
new file mode 100644
index 0000000000..e32605fd11
--- /dev/null
+++ b/activerecord/test/cases/reserved_word_test.rb
@@ -0,0 +1,141 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+
+class ReservedWordTest < ActiveRecord::TestCase
+ self.use_instantiated_fixtures = true
+ self.use_transactional_tests = false
+
+ class Group < ActiveRecord::Base
+ Group.table_name = "group"
+ belongs_to :select
+ has_one :values
+ end
+
+ class Select < ActiveRecord::Base
+ Select.table_name = "select"
+ has_many :groups
+ end
+
+ class Values < ActiveRecord::Base
+ Values.table_name = "values"
+ end
+
+ class Distinct < ActiveRecord::Base
+ Distinct.table_name = "distinct"
+ has_and_belongs_to_many :selects
+ has_many :values, through: :groups
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table :select, force: true
+ @connection.create_table :distinct, force: true
+ @connection.create_table :distinct_select, id: false, force: true do |t|
+ t.belongs_to :distinct
+ t.belongs_to :select
+ end
+ @connection.create_table :group, force: true do |t|
+ t.string :order
+ t.belongs_to :select
+ end
+ @connection.create_table :values, primary_key: :as, force: true do |t|
+ t.belongs_to :group
+ end
+ end
+
+ def teardown
+ @connection.drop_table :select, if_exists: true
+ @connection.drop_table :distinct, if_exists: true
+ @connection.drop_table :distinct_select, if_exists: true
+ @connection.drop_table :group, if_exists: true
+ @connection.drop_table :values, if_exists: true
+ @connection.drop_table :order, if_exists: true
+ end
+
+ def test_create_tables
+ assert_not @connection.table_exists?(:order)
+
+ @connection.create_table :order do |t|
+ t.string :group
+ end
+
+ assert @connection.table_exists?(:order)
+ end
+
+ def test_rename_tables
+ assert_nothing_raised { @connection.rename_table(:group, :order) }
+ end
+
+ def test_change_columns
+ assert_nothing_raised { @connection.change_column_default(:group, :order, "whatever") }
+ assert_nothing_raised { @connection.change_column("group", "order", :text, default: nil) }
+ assert_nothing_raised { @connection.rename_column(:group, :order, :values) }
+ end
+
+ def test_introspect
+ assert_equal ["id", "order", "select_id"], @connection.columns(:group).map(&:name).sort
+ assert_equal ["index_group_on_select_id"], @connection.indexes(:group).map(&:name).sort
+ end
+
+ def test_activerecord_model
+ x = Group.new
+ x.order = "x"
+ x.save!
+ x.order = "y"
+ x.save!
+ assert_equal x, Group.find_by_order("y")
+ assert_equal x, Group.find(x.id)
+ end
+
+ def test_delete_all_with_subselect
+ create_test_fixtures :values
+ assert_equal 1, Values.order(:as).limit(1).offset(1).delete_all
+ assert_raise(ActiveRecord::RecordNotFound) { Values.find(2) }
+ assert Values.find(1)
+ end
+
+ def test_has_one_associations
+ create_test_fixtures :group, :values
+ v = Group.find(1).values
+ assert_equal 2, v.id
+ end
+
+ def test_belongs_to_associations
+ create_test_fixtures :select, :group
+ gs = Select.find(2).groups
+ assert_equal 2, gs.length
+ assert_equal [2, 3], gs.collect(&:id).sort
+ end
+
+ def test_has_and_belongs_to_many
+ create_test_fixtures :select, :distinct, :distinct_select
+ s = Distinct.find(1).selects
+ assert_equal 2, s.length
+ assert_equal [1, 2], s.collect(&:id).sort
+ end
+
+ def test_activerecord_introspection
+ assert_predicate Group, :table_exists?
+ assert_equal ["id", "order", "select_id"], Group.columns.map(&:name).sort
+ end
+
+ def test_calculations_work_with_reserved_words
+ create_test_fixtures :group
+ assert_equal 3, Group.count
+ end
+
+ def test_associations_work_with_reserved_words
+ create_test_fixtures :select, :group
+ selects = Select.all.merge!(includes: [:groups]).to_a
+ assert_no_queries do
+ selects.each { |select| select.groups }
+ end
+ end
+
+ private
+ # custom fixture loader, uses FixtureSet#create_fixtures and appends base_path to the current file's path
+ def create_test_fixtures(*fixture_names)
+ ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT + "/reserved_words", fixture_names)
+ end
+end
diff --git a/activerecord/test/cases/result_test.rb b/activerecord/test/cases/result_test.rb
index dec01dfa76..825aee2423 100644
--- a/activerecord/test/cases/result_test.rb
+++ b/activerecord/test/cases/result_test.rb
@@ -1,44 +1,69 @@
+# frozen_string_literal: true
+
require "cases/helper"
module ActiveRecord
class ResultTest < ActiveRecord::TestCase
def result
- Result.new(['col_1', 'col_2'], [
- ['row 1 col 1', 'row 1 col 2'],
- ['row 2 col 1', 'row 2 col 2'],
- ['row 3 col 1', 'row 3 col 2'],
+ Result.new(["col_1", "col_2"], [
+ ["row 1 col 1", "row 1 col 2"],
+ ["row 2 col 1", "row 2 col 2"],
+ ["row 3 col 1", "row 3 col 2"],
])
end
+ test "includes_column?" do
+ assert result.includes_column?("col_1")
+ assert_not result.includes_column?("foo")
+ end
+
test "length" do
assert_equal 3, result.length
end
- test "to_hash returns row_hashes" do
+ test "to_a returns row_hashes" do
assert_equal [
- {'col_1' => 'row 1 col 1', 'col_2' => 'row 1 col 2'},
- {'col_1' => 'row 2 col 1', 'col_2' => 'row 2 col 2'},
- {'col_1' => 'row 3 col 1', 'col_2' => 'row 3 col 2'},
- ], result.to_hash
+ { "col_1" => "row 1 col 1", "col_2" => "row 1 col 2" },
+ { "col_1" => "row 2 col 1", "col_2" => "row 2 col 2" },
+ { "col_1" => "row 3 col 1", "col_2" => "row 3 col 2" },
+ ], result.to_a
+ end
+
+ test "to_hash (deprecated) returns row_hashes" do
+ assert_deprecated do
+ assert_equal [
+ { "col_1" => "row 1 col 1", "col_2" => "row 1 col 2" },
+ { "col_1" => "row 2 col 1", "col_2" => "row 2 col 2" },
+ { "col_1" => "row 3 col 1", "col_2" => "row 3 col 2" },
+ ], result.to_hash
+ end
+ end
+
+ test "first returns first row as a hash" do
+ assert_equal(
+ { "col_1" => "row 1 col 1", "col_2" => "row 1 col 2" }, result.first)
+ end
+
+ test "last returns last row as a hash" do
+ assert_equal(
+ { "col_1" => "row 3 col 1", "col_2" => "row 3 col 2" }, result.last)
end
test "each with block returns row hashes" do
result.each do |row|
- assert_equal ['col_1', 'col_2'], row.keys
+ assert_equal ["col_1", "col_2"], row.keys
end
end
test "each without block returns an enumerator" do
result.each.with_index do |row, index|
- assert_equal ['col_1', 'col_2'], row.keys
+ assert_equal ["col_1", "col_2"], row.keys
assert_kind_of Integer, index
end
end
- if Enumerator.method_defined? :size
- test "each without block returns a sized enumerator" do
- assert_equal 3, result.each.size
- end
+ test "each without block returns a sized enumerator" do
+ assert_equal 3, result.each.size
end
test "cast_values returns rows after type casting" do
diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb
index 262e0abc22..778cf86ac3 100644
--- a/activerecord/test/cases/sanitize_test.rb
+++ b/activerecord/test/cases/sanitize_test.rb
@@ -1,7 +1,10 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/binary'
-require 'models/author'
-require 'models/post'
+require "models/binary"
+require "models/author"
+require "models/post"
+require "models/customer"
class SanitizeTest < ActiveRecord::TestCase
def setup
@@ -9,64 +12,174 @@ class SanitizeTest < ActiveRecord::TestCase
def test_sanitize_sql_array_handles_string_interpolation
quoted_bambi = ActiveRecord::Base.connection.quote_string("Bambi")
- assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=%s", "Bambi"])
- assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=%s", "Bambi".mb_chars])
+ assert_equal "name='#{quoted_bambi}'", Binary.sanitize_sql_array(["name='%s'", "Bambi"])
+ assert_equal "name='#{quoted_bambi}'", Binary.sanitize_sql_array(["name='%s'", "Bambi".mb_chars])
quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote_string("Bambi\nand\nThumper")
- assert_equal "name=#{quoted_bambi_and_thumper}",Binary.send(:sanitize_sql_array, ["name=%s", "Bambi\nand\nThumper"])
- assert_equal "name=#{quoted_bambi_and_thumper}",Binary.send(:sanitize_sql_array, ["name=%s", "Bambi\nand\nThumper".mb_chars])
+ assert_equal "name='#{quoted_bambi_and_thumper}'", Binary.sanitize_sql_array(["name='%s'", "Bambi\nand\nThumper"])
+ assert_equal "name='#{quoted_bambi_and_thumper}'", Binary.sanitize_sql_array(["name='%s'", "Bambi\nand\nThumper".mb_chars])
end
def test_sanitize_sql_array_handles_bind_variables
quoted_bambi = ActiveRecord::Base.connection.quote("Bambi")
- assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi"])
- assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi".mb_chars])
+ assert_equal "name=#{quoted_bambi}", Binary.sanitize_sql_array(["name=?", "Bambi"])
+ assert_equal "name=#{quoted_bambi}", Binary.sanitize_sql_array(["name=?", "Bambi".mb_chars])
+ quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote("Bambi\nand\nThumper")
+ assert_equal "name=#{quoted_bambi_and_thumper}", Binary.sanitize_sql_array(["name=?", "Bambi\nand\nThumper"])
+ assert_equal "name=#{quoted_bambi_and_thumper}", Binary.sanitize_sql_array(["name=?", "Bambi\nand\nThumper".mb_chars])
+ end
+
+ def test_sanitize_sql_array_handles_named_bind_variables
+ quoted_bambi = ActiveRecord::Base.connection.quote("Bambi")
+ assert_equal "name=#{quoted_bambi}", Binary.sanitize_sql_array(["name=:name", name: "Bambi"])
+ assert_equal "name=#{quoted_bambi} AND id=1", Binary.sanitize_sql_array(["name=:name AND id=:id", name: "Bambi", id: 1])
+
quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote("Bambi\nand\nThumper")
- assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi\nand\nThumper"])
- assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi\nand\nThumper".mb_chars])
+ assert_equal "name=#{quoted_bambi_and_thumper}", Binary.sanitize_sql_array(["name=:name", name: "Bambi\nand\nThumper"])
+ assert_equal "name=#{quoted_bambi_and_thumper} AND name2=#{quoted_bambi_and_thumper}", Binary.sanitize_sql_array(["name=:name AND name2=:name", name: "Bambi\nand\nThumper"])
end
def test_sanitize_sql_array_handles_relations
- david = Author.create!(name: 'David')
+ david = Author.create!(name: "David")
david_posts = david.posts.select(:id)
sub_query_pattern = /\(\bselect\b.*?\bwhere\b.*?\)/i
- select_author_sql = Post.send(:sanitize_sql_array, ['id in (?)', david_posts])
- assert_match(sub_query_pattern, select_author_sql, 'should sanitize `Relation` as subquery for bind variables')
+ select_author_sql = Post.sanitize_sql_array(["id in (?)", david_posts])
+ assert_match(sub_query_pattern, select_author_sql, "should sanitize `Relation` as subquery for bind variables")
- select_author_sql = Post.send(:sanitize_sql_array, ['id in (:post_ids)', post_ids: david_posts])
- assert_match(sub_query_pattern, select_author_sql, 'should sanitize `Relation` as subquery for named bind variables')
+ select_author_sql = Post.sanitize_sql_array(["id in (:post_ids)", post_ids: david_posts])
+ assert_match(sub_query_pattern, select_author_sql, "should sanitize `Relation` as subquery for named bind variables")
end
def test_sanitize_sql_array_handles_empty_statement
- select_author_sql = Post.send(:sanitize_sql_array, [''])
- assert_equal('', select_author_sql)
+ select_author_sql = Post.sanitize_sql_array([""])
+ assert_equal("", select_author_sql)
end
def test_sanitize_sql_like
- assert_equal '100\%', Binary.send(:sanitize_sql_like, '100%')
- assert_equal 'snake\_cased\_string', Binary.send(:sanitize_sql_like, 'snake_cased_string')
- assert_equal 'C:\\\\Programs\\\\MsPaint', Binary.send(:sanitize_sql_like, 'C:\\Programs\\MsPaint')
- assert_equal 'normal string 42', Binary.send(:sanitize_sql_like, 'normal string 42')
+ assert_equal '100\%', Binary.sanitize_sql_like("100%")
+ assert_equal 'snake\_cased\_string', Binary.sanitize_sql_like("snake_cased_string")
+ assert_equal 'C:\\\\Programs\\\\MsPaint', Binary.sanitize_sql_like('C:\\Programs\\MsPaint')
+ assert_equal "normal string 42", Binary.sanitize_sql_like("normal string 42")
end
def test_sanitize_sql_like_with_custom_escape_character
- assert_equal '100!%', Binary.send(:sanitize_sql_like, '100%', '!')
- assert_equal 'snake!_cased!_string', Binary.send(:sanitize_sql_like, 'snake_cased_string', '!')
- assert_equal 'great!!', Binary.send(:sanitize_sql_like, 'great!', '!')
- assert_equal 'C:\\Programs\\MsPaint', Binary.send(:sanitize_sql_like, 'C:\\Programs\\MsPaint', '!')
- assert_equal 'normal string 42', Binary.send(:sanitize_sql_like, 'normal string 42', '!')
+ assert_equal "100!%", Binary.sanitize_sql_like("100%", "!")
+ assert_equal "snake!_cased!_string", Binary.sanitize_sql_like("snake_cased_string", "!")
+ assert_equal "great!!", Binary.sanitize_sql_like("great!", "!")
+ assert_equal 'C:\\Programs\\MsPaint', Binary.sanitize_sql_like('C:\\Programs\\MsPaint', "!")
+ assert_equal "normal string 42", Binary.sanitize_sql_like("normal string 42", "!")
end
def test_sanitize_sql_like_example_use_case
searchable_post = Class.new(Post) do
- def self.search(term)
- where("title LIKE ?", sanitize_sql_like(term, '!'))
+ def self.search_as_method(term)
+ where("title LIKE ?", sanitize_sql_like(term, "!"))
end
+
+ scope :search_as_scope, -> (term) {
+ where("title LIKE ?", sanitize_sql_like(term, "!"))
+ }
+ end
+
+ assert_sql(/LIKE '20!% !_reduction!_!!'/) do
+ searchable_post.search_as_method("20% _reduction_!").to_a
end
assert_sql(/LIKE '20!% !_reduction!_!!'/) do
- searchable_post.search("20% _reduction_!").to_a
+ searchable_post.search_as_scope("20% _reduction_!").to_a
+ end
+ end
+
+ def test_bind_arity
+ assert_nothing_raised { bind "" }
+ assert_raise(ActiveRecord::PreparedStatementInvalid) { bind "", 1 }
+
+ assert_raise(ActiveRecord::PreparedStatementInvalid) { bind "?" }
+ assert_nothing_raised { bind "?", 1 }
+ assert_raise(ActiveRecord::PreparedStatementInvalid) { bind "?", 1, 1 }
+ end
+
+ def test_named_bind_variables
+ assert_equal "1", bind(":a", a: 1) # ' ruby-mode
+ assert_equal "1 1", bind(":a :a", a: 1) # ' ruby-mode
+
+ assert_nothing_raised { bind("'+00:00'", foo: "bar") }
+ end
+
+ def test_named_bind_arity
+ assert_nothing_raised { bind "name = :name", name: "37signals" }
+ assert_nothing_raised { bind "name = :name", name: "37signals", id: 1 }
+ assert_raise(ActiveRecord::PreparedStatementInvalid) { bind "name = :name", id: 1 }
+ end
+
+ class SimpleEnumerable
+ include Enumerable
+
+ def initialize(ary)
+ @ary = ary
+ end
+
+ def each(&b)
+ @ary.each(&b)
end
end
+
+ def test_bind_enumerable
+ quoted_abc = %(#{ActiveRecord::Base.connection.quote('a')},#{ActiveRecord::Base.connection.quote('b')},#{ActiveRecord::Base.connection.quote('c')})
+
+ assert_equal "1,2,3", bind("?", [1, 2, 3])
+ assert_equal quoted_abc, bind("?", %w(a b c))
+
+ assert_equal "1,2,3", bind(":a", a: [1, 2, 3])
+ assert_equal quoted_abc, bind(":a", a: %w(a b c)) # '
+
+ assert_equal "1,2,3", bind("?", SimpleEnumerable.new([1, 2, 3]))
+ assert_equal quoted_abc, bind("?", SimpleEnumerable.new(%w(a b c)))
+
+ assert_equal "1,2,3", bind(":a", a: SimpleEnumerable.new([1, 2, 3]))
+ assert_equal quoted_abc, bind(":a", a: SimpleEnumerable.new(%w(a b c))) # '
+ end
+
+ def test_bind_empty_enumerable
+ quoted_nil = ActiveRecord::Base.connection.quote(nil)
+ assert_equal quoted_nil, bind("?", [])
+ assert_equal " in (#{quoted_nil})", bind(" in (?)", [])
+ assert_equal "foo in (#{quoted_nil})", bind("foo in (?)", [])
+ end
+
+ def test_bind_empty_string
+ quoted_empty = ActiveRecord::Base.connection.quote("")
+ assert_equal quoted_empty, bind("?", "")
+ end
+
+ def test_bind_chars
+ quoted_bambi = ActiveRecord::Base.connection.quote("Bambi")
+ quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote("Bambi\nand\nThumper")
+ assert_equal "name=#{quoted_bambi}", bind("name=?", "Bambi")
+ assert_equal "name=#{quoted_bambi_and_thumper}", bind("name=?", "Bambi\nand\nThumper")
+ assert_equal "name=#{quoted_bambi}", bind("name=?", "Bambi".mb_chars)
+ assert_equal "name=#{quoted_bambi_and_thumper}", bind("name=?", "Bambi\nand\nThumper".mb_chars)
+ end
+
+ def test_named_bind_with_postgresql_type_casts
+ l = Proc.new { bind(":a::integer '2009-01-01'::date", a: "10") }
+ assert_nothing_raised(&l)
+ assert_equal "#{ActiveRecord::Base.connection.quote('10')}::integer '2009-01-01'::date", l.call
+ end
+
+ def test_deprecated_expand_hash_conditions_for_aggregates
+ assert_deprecated do
+ assert_equal({ "balance" => 50 }, Customer.send(:expand_hash_conditions_for_aggregates, balance: Money.new(50)))
+ end
+ end
+
+ private
+ def bind(statement, *vars)
+ if vars.first.is_a?(Hash)
+ ActiveRecord::Base.send(:replace_named_bind_variables, statement, vars.first)
+ else
+ ActiveRecord::Base.send(:replace_bind_variables, statement, vars)
+ end
+ end
end
diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb
index feb1c29656..dda3efa47c 100644
--- a/activerecord/test/cases/schema_dumper_test.rb
+++ b/activerecord/test/cases/schema_dumper_test.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'support/schema_dumping_helper'
+require "support/schema_dumping_helper"
class SchemaDumperTest < ActiveRecord::TestCase
include SchemaDumpingHelper
@@ -17,10 +19,16 @@ class SchemaDumperTest < ActiveRecord::TestCase
dump_all_table_schema []
end
+ def test_dump_schema_information_with_empty_versions
+ ActiveRecord::SchemaMigration.delete_all
+ schema_info = ActiveRecord::Base.connection.dump_schema_information
+ assert_no_match(/INSERT INTO/, schema_info)
+ end
+
def test_dump_schema_information_outputs_lexically_ordered_versions
versions = %w{ 20100101010101 20100201010101 20100301010101 }
versions.reverse_each do |v|
- ActiveRecord::SchemaMigration.create!(:version => v)
+ ActiveRecord::SchemaMigration.create!(version: v)
end
schema_info = ActiveRecord::Base.connection.dump_schema_information
@@ -29,20 +37,18 @@ class SchemaDumperTest < ActiveRecord::TestCase
ActiveRecord::SchemaMigration.delete_all
end
- def test_magic_comment
- assert_match "# encoding: #{Encoding.default_external.name}", standard_dump
- end
-
def test_schema_dump
output = standard_dump
assert_match %r{create_table "accounts"}, output
assert_match %r{create_table "authors"}, output
+ assert_no_match %r{(?<=, ) do \|t\|}, output
assert_no_match %r{create_table "schema_migrations"}, output
+ assert_no_match %r{create_table "ar_internal_metadata"}, output
end
def test_schema_dump_uses_force_cascade_on_create_table
output = dump_table_schema "authors"
- assert_match %r{create_table "authors", force: :cascade}, output
+ assert_match %r{create_table "authors",.* force: :cascade}, output
end
def test_schema_dump_excludes_sqlite_sequence
@@ -55,38 +61,35 @@ class SchemaDumperTest < ActiveRecord::TestCase
assert_match %r{create_table "CamelCase"}, output
end
- def assert_line_up(lines, pattern, required = false)
+ def assert_no_line_up(lines, pattern)
return assert(true) if lines.empty?
matches = lines.map { |line| line.match(pattern) }
- assert matches.all? if required
matches.compact!
return assert(true) if matches.empty?
- assert_equal 1, matches.map{ |match| match.offset(0).first }.uniq.length
+ line_matches = lines.map { |line| [line, line.match(pattern)] }.select { |line, match| match }
+ assert line_matches.all? { |line, match|
+ start = match.offset(0).first
+ line[start - 2..start - 1] == ", "
+ }
end
def column_definition_lines(output = standard_dump)
- output.scan(/^( *)create_table.*?\n(.*?)^\1end/m).map{ |m| m.last.split(/\n/) }
+ output.scan(/^( *)create_table.*?\n(.*?)^\1end/m).map { |m| m.last.split(/\n/) }
end
- def test_types_line_up
+ def test_types_no_line_up
column_definition_lines.each do |column_set|
next if column_set.empty?
- lengths = column_set.map do |column|
- if match = column.match(/\bt\.\w+\s+"/)
- match[0].length
- end
- end.compact
-
- assert_equal 1, lengths.uniq.length
+ assert column_set.all? { |column| !column.match(/\bt\.\w+\s{2,}/) }
end
end
- def test_arguments_line_up
+ def test_arguments_no_line_up
column_definition_lines.each do |column_set|
- assert_line_up(column_set, /default: /)
- assert_line_up(column_set, /limit: /)
- assert_line_up(column_set, /null: /)
+ assert_no_line_up(column_set, /default: /)
+ assert_no_line_up(column_set, /limit: /)
+ assert_no_line_up(column_set, /null: /)
end
end
@@ -103,61 +106,47 @@ class SchemaDumperTest < ActiveRecord::TestCase
def test_schema_dump_includes_limit_constraint_for_integer_columns
output = dump_all_table_schema([/^(?!integer_limits)/])
- assert_match %r{c_int_without_limit}, output
+ assert_match %r{"c_int_without_limit"(?!.*limit)}, output
if current_adapter?(:PostgreSQLAdapter)
- assert_no_match %r{c_int_without_limit.*limit:}, output
-
assert_match %r{c_int_1.*limit: 2}, output
assert_match %r{c_int_2.*limit: 2}, output
# int 3 is 4 bytes in postgresql
- assert_match %r{c_int_3.*}, output
- assert_no_match %r{c_int_3.*limit:}, output
-
- assert_match %r{c_int_4.*}, output
- assert_no_match %r{c_int_4.*limit:}, output
- elsif current_adapter?(:MysqlAdapter, :Mysql2Adapter)
- assert_match %r{c_int_without_limit.*limit: 4}, output
-
+ assert_match %r{"c_int_3"(?!.*limit)}, output
+ assert_match %r{"c_int_4"(?!.*limit)}, output
+ elsif current_adapter?(:Mysql2Adapter)
assert_match %r{c_int_1.*limit: 1}, output
assert_match %r{c_int_2.*limit: 2}, output
assert_match %r{c_int_3.*limit: 3}, output
- assert_match %r{c_int_4.*}, output
- assert_no_match %r{c_int_4.*:limit}, output
+ assert_match %r{"c_int_4"(?!.*limit)}, output
elsif current_adapter?(:SQLite3Adapter)
- assert_no_match %r{c_int_without_limit.*limit:}, output
-
assert_match %r{c_int_1.*limit: 1}, output
assert_match %r{c_int_2.*limit: 2}, output
assert_match %r{c_int_3.*limit: 3}, output
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
assert_match %r{c_int_8.*limit: 8}, output
else
- assert_match %r{c_int_5.*limit: 8}, output
- assert_match %r{c_int_6.*limit: 8}, output
- assert_match %r{c_int_7.*limit: 8}, output
- assert_match %r{c_int_8.*limit: 8}, output
+ assert_match %r{t\.bigint\s+"c_int_5"$}, output
+ assert_match %r{t\.bigint\s+"c_int_6"$}, output
+ assert_match %r{t\.bigint\s+"c_int_7"$}, output
+ assert_match %r{t\.bigint\s+"c_int_8"$}, output
end
end
def test_schema_dump_with_string_ignored_table
- output = dump_all_table_schema(['accounts'])
+ output = dump_all_table_schema(["accounts"])
assert_no_match %r{create_table "accounts"}, output
assert_match %r{create_table "authors"}, output
assert_no_match %r{create_table "schema_migrations"}, output
+ assert_no_match %r{create_table "ar_internal_metadata"}, output
end
def test_schema_dump_with_regexp_ignored_table
@@ -165,30 +154,51 @@ class SchemaDumperTest < ActiveRecord::TestCase
assert_no_match %r{create_table "accounts"}, output
assert_match %r{create_table "authors"}, output
assert_no_match %r{create_table "schema_migrations"}, output
+ assert_no_match %r{create_table "ar_internal_metadata"}, output
end
def test_schema_dumps_index_columns_in_right_order
- index_definition = standard_dump.split(/\n/).grep(/t\.index.*company_index/).first.strip
- if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter)
- assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", using: :btree', index_definition
+ index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*company_index/).first.strip
+ if current_adapter?(:Mysql2Adapter)
+ if ActiveRecord::Base.connection.supports_index_sort_order?
+ assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", length: { type: 10 }, order: { rating: :desc }', index_definition
+ else
+ assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", length: { type: 10 }', index_definition
+ end
+ elsif ActiveRecord::Base.connection.supports_index_sort_order?
+ assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", order: { rating: :desc }', index_definition
else
assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index"', index_definition
end
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?(:MysqlAdapter, :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 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
end
+ def test_schema_dumps_index_sort_order
+ index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*_name_and_rating/).first.strip
+ if ActiveRecord::Base.connection.supports_index_sort_order?
+ assert_equal 't.index ["name", "rating"], name: "index_companies_on_name_and_rating", order: :desc', index_definition
+ else
+ assert_equal 't.index ["name", "rating"], name: "index_companies_on_name_and_rating"', index_definition
+ end
+ end
+
+ def test_schema_dumps_index_length
+ index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*_name_and_description/).first.strip
+ if current_adapter?(:Mysql2Adapter)
+ assert_equal 't.index ["name", "description"], name: "index_companies_on_name_and_description", length: 10', index_definition
+ else
+ assert_equal 't.index ["name", "description"], name: "index_companies_on_name_and_description"', index_definition
+ end
+ end
+
def test_schema_dump_should_honor_nonstandard_primary_keys
output = standard_dump
match = output.match(%r{create_table "movies"(.*)do})
@@ -197,16 +207,43 @@ class SchemaDumperTest < ActiveRecord::TestCase
end
def test_schema_dump_should_use_false_as_default
- output = standard_dump
+ output = dump_table_schema "booleans"
assert_match %r{t\.boolean\s+"has_fun",.+default: false}, output
end
- if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
- def test_schema_dump_should_add_default_value_for_mysql_text_field
- output = standard_dump
- assert_match %r{t\.text\s+"body",\s+limit: 65535,\s+null: false$}, output
+ def test_schema_dump_does_not_include_limit_for_text_field
+ output = dump_table_schema "admin_users"
+ assert_match %r{t\.text\s+"params"$}, output
+ end
+
+ def test_schema_dump_does_not_include_limit_for_binary_field
+ output = dump_table_schema "binaries"
+ assert_match %r{t\.binary\s+"data"$}, output
+ end
+
+ def test_schema_dump_does_not_include_limit_for_float_field
+ output = dump_table_schema "numeric_data"
+ assert_match %r{t\.float\s+"temperature"$}, output
+ end
+
+ if ActiveRecord::Base.connection.supports_expression_index?
+ def test_schema_dump_expression_indices
+ index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*company_expression_index/).first.strip
+ index_definition.sub!(/, name: "company_expression_index"\z/, "")
+
+ if current_adapter?(:PostgreSQLAdapter)
+ assert_match %r{CASE.+lower\(\(name\)::text\).+END\) DESC"\z}i, index_definition
+ elsif current_adapter?(:Mysql2Adapter)
+ assert_match %r{CASE.+lower\(`name`\).+END\) DESC"\z}i, index_definition
+ elsif current_adapter?(:SQLite3Adapter)
+ assert_match %r{CASE.+lower\(name\).+END\) DESC"\z}i, index_definition
+ else
+ assert false
+ end
end
+ end
+ if current_adapter?(:Mysql2Adapter)
def test_schema_dump_includes_length_for_mysql_binary_fields
output = standard_dump
assert_match %r{t\.binary\s+"var_binary",\s+limit: 255$}, output
@@ -215,12 +252,12 @@ class SchemaDumperTest < ActiveRecord::TestCase
def test_schema_dump_includes_length_for_mysql_blob_and_text_fields
output = standard_dump
- assert_match %r{t\.binary\s+"tiny_blob",\s+limit: 255$}, output
- assert_match %r{t\.binary\s+"normal_blob",\s+limit: 65535$}, output
+ assert_match %r{t\.blob\s+"tiny_blob",\s+limit: 255$}, output
+ assert_match %r{t\.binary\s+"normal_blob"$}, output
assert_match %r{t\.binary\s+"medium_blob",\s+limit: 16777215$}, output
assert_match %r{t\.binary\s+"long_blob",\s+limit: 4294967295$}, output
assert_match %r{t\.text\s+"tiny_text",\s+limit: 255$}, output
- assert_match %r{t\.text\s+"normal_text",\s+limit: 65535$}, output
+ assert_match %r{t\.text\s+"normal_text"$}, output
assert_match %r{t\.text\s+"medium_text",\s+limit: 16777215$}, output
assert_match %r{t\.text\s+"long_text",\s+limit: 4294967295$}, output
end
@@ -231,9 +268,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
@@ -244,35 +281,62 @@ class SchemaDumperTest < ActiveRecord::TestCase
if current_adapter?(:PostgreSQLAdapter)
def test_schema_dump_includes_bigint_default
- output = standard_dump
- assert_match %r{t\.integer\s+"bigint_default",\s+limit: 8,\s+default: 0}, output
+ 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
- assert_match %r{t\.integer\s+"big_int_data_points\",\s+limit: 8,\s+array: true}, output
+ 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
- if ActiveRecord::Base.connection.supports_extensions?
- def test_schema_dump_includes_extensions
- connection = ActiveRecord::Base.connection
+ 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
+
+ def test_schema_dump_includes_extensions
+ connection = ActiveRecord::Base.connection
- connection.stubs(:extensions).returns(['hstore'])
+ connection.stub(:extensions, ["hstore"]) do
output = perform_schema_dump
assert_match "# These are extensions that must be enabled", output
assert_match %r{enable_extension "hstore"}, output
+ end
- connection.stubs(:extensions).returns([])
+ connection.stub(:extensions, []) do
output = perform_schema_dump
assert_no_match "# These are extensions that must be enabled", output
assert_no_match %r{enable_extension}, output
end
end
+
+ def test_schema_dump_includes_extensions_in_alphabetic_order
+ connection = ActiveRecord::Base.connection
+
+ connection.stub(:extensions, ["hstore", "uuid-ossp", "xml2"]) do
+ output = perform_schema_dump
+ enabled_extensions = output.scan(%r{enable_extension "(.+)"}).flatten
+ assert_equal ["hstore", "uuid-ossp", "xml2"], enabled_extensions
+ end
+
+ connection.stub(:extensions, ["uuid-ossp", "xml2", "hstore"]) do
+ output = perform_schema_dump
+ enabled_extensions = output.scan(%r{enable_extension "(.+)"}).flatten
+ assert_equal ["hstore", "uuid-ossp", "xml2"], enabled_extensions
+ end
+ end
end
def test_schema_dump_keeps_large_precision_integer_columns_as_decimal
@@ -297,7 +361,7 @@ class SchemaDumperTest < ActiveRecord::TestCase
def test_schema_dump_keeps_id_false_when_id_is_false_and_unique_not_null_column_added
output = standard_dump
- assert_match %r{create_table "subscribers", id: false}, output
+ assert_match %r{create_table "string_key_objects", id: false}, output
end
if ActiveRecord::Base.connection.supports_foreign_keys?
@@ -312,17 +376,17 @@ class SchemaDumperTest < ActiveRecord::TestCase
end
end
- class CreateDogMigration < ActiveRecord::Migration
+ class CreateDogMigration < ActiveRecord::Migration::Current
def up
create_table("dog_owners") do |t|
end
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"
end
- add_index "dogs", [:name]
- add_foreign_key :dogs, :dog_owners, column: "owner_id" if supports_foreign_keys?
end
def down
drop_table("dogs")
@@ -332,8 +396,8 @@ class SchemaDumperTest < ActiveRecord::TestCase
def test_schema_dump_with_table_name_prefix_and_suffix
original, $stdout = $stdout, StringIO.new
- ActiveRecord::Base.table_name_prefix = 'foo_'
- ActiveRecord::Base.table_name_suffix = '_bar'
+ ActiveRecord::Base.table_name_prefix = "foo_"
+ ActiveRecord::Base.table_name_suffix = "_bar"
migration = CreateDogMigration.new
migration.migrate(:up)
@@ -342,6 +406,7 @@ class SchemaDumperTest < ActiveRecord::TestCase
assert_no_match %r{create_table "foo_.+_bar"}, output
assert_no_match %r{add_index "foo_.+_bar"}, output
assert_no_match %r{create_table "schema_migrations"}, output
+ assert_no_match %r{create_table "ar_internal_metadata"}, output
if ActiveRecord::Base.connection.supports_foreign_keys?
assert_no_match %r{add_foreign_key "foo_.+_bar"}, output
@@ -350,7 +415,64 @@ class SchemaDumperTest < ActiveRecord::TestCase
ensure
migration.migrate(:down)
- ActiveRecord::Base.table_name_suffix = ActiveRecord::Base.table_name_prefix = ''
+ ActiveRecord::Base.table_name_suffix = ActiveRecord::Base.table_name_prefix = ""
+ $stdout = original
+ end
+
+ def test_schema_dump_with_table_name_prefix_and_suffix_regexp_escape
+ original, $stdout = $stdout, StringIO.new
+ ActiveRecord::Base.table_name_prefix = "foo$"
+ ActiveRecord::Base.table_name_suffix = "$bar"
+
+ migration = CreateDogMigration.new
+ migration.migrate(:up)
+
+ output = perform_schema_dump
+ assert_no_match %r{create_table "foo\$.+\$bar"}, output
+ assert_no_match %r{add_index "foo\$.+\$bar"}, output
+ assert_no_match %r{create_table "schema_migrations"}, output
+ assert_no_match %r{create_table "ar_internal_metadata"}, output
+
+ if ActiveRecord::Base.connection.supports_foreign_keys?
+ assert_no_match %r{add_foreign_key "foo\$.+\$bar"}, output
+ assert_no_match %r{add_foreign_key "[^"]+", "foo\$.+\$bar"}, output
+ end
+ ensure
+ migration.migrate(:down)
+
+ ActiveRecord::Base.table_name_suffix = ActiveRecord::Base.table_name_prefix = ""
+ $stdout = original
+ end
+
+ def test_schema_dump_with_table_name_prefix_and_ignoring_tables
+ original, $stdout = $stdout, StringIO.new
+
+ create_cat_migration = Class.new(ActiveRecord::Migration::Current) do
+ def change
+ create_table("cats") do |t|
+ end
+ create_table("omg_cats") do |t|
+ end
+ end
+ end
+
+ original_table_name_prefix = ActiveRecord::Base.table_name_prefix
+ original_schema_dumper_ignore_tables = ActiveRecord::SchemaDumper.ignore_tables
+ ActiveRecord::Base.table_name_prefix = "omg_"
+ ActiveRecord::SchemaDumper.ignore_tables = ["cats"]
+ migration = create_cat_migration.new
+ migration.migrate(:up)
+
+ stream = StringIO.new
+ output = ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream).string
+
+ assert_match %r{create_table "omg_cats"}, output
+ assert_no_match %r{create_table "cats"}, output
+ ensure
+ migration.migrate(:down)
+ ActiveRecord::Base.table_name_prefix = original_table_name_prefix
+ ActiveRecord::SchemaDumper.ignore_tables = original_schema_dumper_ignore_tables
+
$stdout = original
end
end
@@ -360,25 +482,40 @@ 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.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)
+ @connection.create_table :infinity_defaults, force: true do |t|
+ t.float :float_with_inf_default, default: Float::INFINITY
+ t.float :float_with_nan_default, default: Float::NAN
+ end
end
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
+ skip unless current_adapter?(:PostgreSQLAdapter)
+ output = dump_table_schema("infinity_defaults")
+ assert_match %r{t\.float\s+"float_with_inf_default",\s+default: ::Float::INFINITY}, output
+ assert_match %r{t\.float\s+"float_with_nan_default",\s+default: ::Float::NAN}, output
end
end
diff --git a/activerecord/test/cases/schema_loading_test.rb b/activerecord/test/cases/schema_loading_test.rb
new file mode 100644
index 0000000000..f539156466
--- /dev/null
+++ b/activerecord/test/cases/schema_loading_test.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+
+module SchemaLoadCounter
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ attr_accessor :load_schema_calls
+
+ def load_schema!
+ self.load_schema_calls ||= 0
+ self.load_schema_calls += 1
+ super
+ end
+ end
+end
+
+class SchemaLoadingTest < ActiveRecord::TestCase
+ def test_basic_model_is_loaded_once
+ klass = define_model
+ klass.new
+ assert_equal 1, klass.load_schema_calls
+ end
+
+ def test_model_with_custom_lock_is_loaded_once
+ klass = define_model do |c|
+ c.table_name = :lock_without_defaults_cust
+ c.locking_column = :custom_lock_version
+ end
+ klass.new
+ assert_equal 1, klass.load_schema_calls
+ end
+
+ def test_model_with_changed_custom_lock_is_loaded_twice
+ klass = define_model do |c|
+ c.table_name = :lock_without_defaults_cust
+ end
+ klass.new
+ klass.locking_column = :custom_lock_version
+ klass.new
+ assert_equal 2, klass.load_schema_calls
+ end
+
+ private
+
+ def define_model
+ Class.new(ActiveRecord::Base) do
+ include SchemaLoadCounter
+ self.table_name = :lock_without_defaults
+ yield self if block_given?
+ end
+ end
+end
diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb
index 0dbc60940e..6281712df6 100644
--- a/activerecord/test/cases/scoping/default_scoping_test.rb
+++ b/activerecord/test/cases/scoping/default_scoping_test.rb
@@ -1,14 +1,20 @@
-require 'cases/helper'
-require 'models/post'
-require 'models/comment'
-require 'models/developer'
-require 'models/computer'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/post"
+require "models/comment"
+require "models/developer"
+require "models/project"
+require "models/computer"
+require "models/vehicle"
+require "models/cat"
+require "concurrent/atomic/cyclic_barrier"
class DefaultScopingTest < ActiveRecord::TestCase
fixtures :developers, :posts, :comments
def test_default_scope
- expected = Developer.all.merge!(:order => 'salary DESC').to_a.collect(&:salary)
+ expected = Developer.all.merge!(order: "salary DESC").to_a.collect(&:salary)
received = DeveloperOrderedBySalary.all.collect(&:salary)
assert_equal expected, received
end
@@ -47,59 +53,48 @@ class DefaultScopingTest < ActiveRecord::TestCase
end
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_equal Developer.where(name: "David").map(&:id).sort, DeveloperCalledDavid.all.map(&:id).sort
+ assert_nil DeveloperCalledDavid.create!.name
end
def test_default_scope_with_conditions_hash
- assert_equal Developer.where(name: 'Jamis').map(&:id).sort, DeveloperCalledJamis.all.map(&:id).sort
- assert_equal 'Jamis', DeveloperCalledJamis.create!.name
- end
-
- unless in_memory_db?
- def test_default_scoping_with_threads
- 2.times do
- Thread.new {
- assert DeveloperOrderedBySalary.all.to_sql.include?('salary DESC')
- DeveloperOrderedBySalary.connection.close
- }.join
- end
- end
+ assert_equal Developer.where(name: "Jamis").map(&:id).sort, DeveloperCalledJamis.all.map(&:id).sort
+ assert_equal "Jamis", DeveloperCalledJamis.create!.name
end
def test_default_scope_with_inheritance
wheres = InheritedPoorDeveloperCalledJamis.all.where_values_hash
- assert_equal "Jamis", wheres['name']
- assert_equal 50000, wheres['salary']
+ assert_equal "Jamis", wheres["name"]
+ assert_equal 50000, wheres["salary"]
end
def test_default_scope_with_module_includes
wheres = ModuleIncludedPoorDeveloperCalledJamis.all.where_values_hash
- assert_equal "Jamis", wheres['name']
- assert_equal 50000, wheres['salary']
+ assert_equal "Jamis", wheres["name"]
+ assert_equal 50000, wheres["salary"]
end
def test_default_scope_with_multiple_calls
wheres = MultiplePoorDeveloperCalledJamis.all.where_values_hash
- assert_equal "Jamis", wheres['name']
- assert_equal 50000, wheres['salary']
+ assert_equal "Jamis", wheres["name"]
+ assert_equal 50000, wheres["salary"]
end
def test_scope_overwrites_default
- expected = Developer.all.merge!(order: 'salary DESC, name DESC').to_a.collect(&:name)
+ expected = Developer.all.merge!(order: "salary DESC, name DESC").to_a.collect(&:name)
received = DeveloperOrderedBySalary.by_name.to_a.collect(&:name)
assert_equal expected, received
end
def test_reorder_overrides_default_scope_order
- expected = Developer.order('name DESC').collect(&:name)
- received = DeveloperOrderedBySalary.reorder('name DESC').collect(&:name)
+ expected = Developer.order("name DESC").collect(&:name)
+ received = DeveloperOrderedBySalary.reorder("name DESC").collect(&:name)
assert_equal expected, received
end
def test_order_after_reorder_combines_orders
- expected = Developer.order('name DESC, id DESC').collect { |dev| [dev.name, dev.id] }
- received = Developer.order('name ASC').reorder('name DESC').order('id DESC').collect { |dev| [dev.name, dev.id] }
+ expected = Developer.order("name DESC, id DESC").collect { |dev| [dev.name, dev.id] }
+ received = Developer.order("name ASC").reorder("name DESC").order("id DESC").collect { |dev| [dev.name, dev.id] }
assert_equal expected, received
end
@@ -110,107 +105,107 @@ class DefaultScopingTest < ActiveRecord::TestCase
end
def test_unscope_after_reordering_and_combining
- expected = Developer.order('id DESC, name DESC').collect { |dev| [dev.name, dev.id] }
- received = DeveloperOrderedBySalary.reorder('name DESC').unscope(:order).order('id DESC, name DESC').collect { |dev| [dev.name, dev.id] }
+ expected = Developer.order("id DESC, name DESC").collect { |dev| [dev.name, dev.id] }
+ received = DeveloperOrderedBySalary.reorder("name DESC").unscope(:order).order("id DESC, name DESC").collect { |dev| [dev.name, dev.id] }
assert_equal expected, received
expected_2 = Developer.all.collect { |dev| [dev.name, dev.id] }
- received_2 = Developer.order('id DESC, name DESC').unscope(:order).collect { |dev| [dev.name, dev.id] }
+ received_2 = Developer.order("id DESC, name DESC").unscope(:order).collect { |dev| [dev.name, dev.id] }
assert_equal expected_2, received_2
expected_3 = Developer.all.collect { |dev| [dev.name, dev.id] }
- received_3 = Developer.reorder('name DESC').unscope(:order).collect { |dev| [dev.name, dev.id] }
+ received_3 = Developer.reorder("name DESC").unscope(:order).collect { |dev| [dev.name, dev.id] }
assert_equal expected_3, received_3
end
def test_unscope_with_where_attributes
- expected = Developer.order('salary DESC').collect(&:name)
- received = DeveloperOrderedBySalary.where(name: 'David').unscope(where: :name).collect(&:name)
- assert_equal expected, received
+ expected = Developer.order("salary DESC").collect(&:name)
+ received = DeveloperOrderedBySalary.where(name: "David").unscope(where: :name).collect(&:name)
+ assert_equal expected.sort, received.sort
- expected_2 = Developer.order('salary DESC').collect(&:name)
- received_2 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope({:where => :name}, :select).collect(&:name)
- assert_equal expected_2, received_2
+ expected_2 = Developer.order("salary DESC").collect(&:name)
+ received_2 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope({ where: :name }, :select).collect(&:name)
+ assert_equal expected_2.sort, received_2.sort
- expected_3 = Developer.order('salary DESC').collect(&:name)
+ expected_3 = Developer.order("salary DESC").collect(&:name)
received_3 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope(:select, :where).collect(&:name)
- assert_equal expected_3, received_3
+ assert_equal expected_3.sort, received_3.sort
- expected_4 = Developer.order('salary DESC').collect(&:name)
+ expected_4 = Developer.order("salary DESC").collect(&:name)
received_4 = DeveloperOrderedBySalary.where.not("name" => "Jamis").unscope(where: :name).collect(&:name)
- assert_equal expected_4, received_4
+ assert_equal expected_4.sort, received_4.sort
- expected_5 = Developer.order('salary DESC').collect(&:name)
+ expected_5 = Developer.order("salary DESC").collect(&:name)
received_5 = DeveloperOrderedBySalary.where.not("name" => ["Jamis", "David"]).unscope(where: :name).collect(&:name)
- assert_equal expected_5, received_5
+ assert_equal expected_5.sort, received_5.sort
- expected_6 = Developer.order('salary DESC').collect(&:name)
- received_6 = DeveloperOrderedBySalary.where(Developer.arel_table['name'].eq('David')).unscope(where: :name).collect(&:name)
- assert_equal expected_6, received_6
+ expected_6 = Developer.order("salary DESC").collect(&:name)
+ received_6 = DeveloperOrderedBySalary.where(Developer.arel_table["name"].eq("David")).unscope(where: :name).collect(&:name)
+ assert_equal expected_6.sort, received_6.sort
- expected_7 = Developer.order('salary DESC').collect(&:name)
- received_7 = DeveloperOrderedBySalary.where(Developer.arel_table[:name].eq('David')).unscope(where: :name).collect(&:name)
- assert_equal expected_7, received_7
+ expected_7 = Developer.order("salary DESC").collect(&:name)
+ received_7 = DeveloperOrderedBySalary.where(Developer.arel_table[:name].eq("David")).unscope(where: :name).collect(&:name)
+ assert_equal expected_7.sort, received_7.sort
end
def test_unscope_comparison_where_clauses
# unscoped for WHERE (`developers`.`id` <= 2)
- expected = Developer.order('salary DESC').collect(&:name)
+ expected = Developer.order("salary DESC").collect(&:name)
received = DeveloperOrderedBySalary.where(id: -Float::INFINITY..2).unscope(where: :id).collect { |dev| dev.name }
- assert_equal expected, received
+ assert_equal expected.sort, received.sort
# unscoped for WHERE (`developers`.`id` < 2)
- expected = Developer.order('salary DESC').collect(&:name)
+ expected = Developer.order("salary DESC").collect(&:name)
received = DeveloperOrderedBySalary.where(id: -Float::INFINITY...2).unscope(where: :id).collect { |dev| dev.name }
- assert_equal expected, received
+ assert_equal expected.sort, received.sort
end
def test_unscope_multiple_where_clauses
- expected = Developer.order('salary DESC').collect(&:name)
- received = DeveloperOrderedBySalary.where(name: 'Jamis').where(id: 1).unscope(where: [:name, :id]).collect(&:name)
- assert_equal expected, received
+ expected = Developer.order("salary DESC").collect(&:name)
+ received = DeveloperOrderedBySalary.where(name: "Jamis").where(id: 1).unscope(where: [:name, :id]).collect(&:name)
+ assert_equal expected.sort, received.sort
end
def test_unscope_string_where_clauses_involved
- dev_relation = Developer.order('salary DESC').where("created_at > ?", 1.year.ago)
+ dev_relation = Developer.order("salary DESC").where("created_at > ?", 1.year.ago)
expected = dev_relation.collect(&:name)
- dev_ordered_relation = DeveloperOrderedBySalary.where(name: 'Jamis').where("created_at > ?", 1.year.ago)
+ dev_ordered_relation = DeveloperOrderedBySalary.where(name: "Jamis").where("created_at > ?", 1.year.ago)
received = dev_ordered_relation.unscope(where: [:name]).collect(&:name)
- assert_equal expected, received
+ assert_equal expected.sort, received.sort
end
def test_unscope_with_grouping_attributes
- expected = Developer.order('salary DESC').collect(&:name)
+ expected = Developer.order("salary DESC").collect(&:name)
received = DeveloperOrderedBySalary.group(:name).unscope(:group).collect(&:name)
- assert_equal expected, received
+ assert_equal expected.sort, received.sort
- expected_2 = Developer.order('salary DESC').collect(&:name)
+ expected_2 = Developer.order("salary DESC").collect(&:name)
received_2 = DeveloperOrderedBySalary.group("name").unscope(:group).collect(&:name)
- assert_equal expected_2, received_2
+ assert_equal expected_2.sort, received_2.sort
end
def test_unscope_with_limit_in_query
- expected = Developer.order('salary DESC').collect(&:name)
+ expected = Developer.order("salary DESC").collect(&:name)
received = DeveloperOrderedBySalary.limit(1).unscope(:limit).collect(&:name)
- assert_equal expected, received
+ assert_equal expected.sort, received.sort
end
def test_order_to_unscope_reordering
- scope = DeveloperOrderedBySalary.order('salary DESC, name ASC').reverse_order.unscope(:order)
- assert !(scope.to_sql =~ /order/i)
+ scope = DeveloperOrderedBySalary.order("salary DESC, name ASC").reverse_order.unscope(:order)
+ assert_no_match(/order/i, scope.to_sql)
end
def test_unscope_reverse_order
expected = Developer.all.collect(&:name)
- received = Developer.order('salary DESC').reverse_order.unscope(:order).collect(&:name)
+ received = Developer.order("salary DESC").reverse_order.unscope(:order).collect(&:name)
assert_equal expected, received
end
def test_unscope_select
- expected = Developer.order('salary ASC').collect(&:name)
- received = Developer.order('salary DESC').reverse_order.select(:name).unscope(:select).collect(&:name)
+ expected = Developer.order("salary ASC").collect(&:name)
+ received = Developer.order("salary DESC").reverse_order.select(:name).unscope(:select).collect(&:name)
assert_equal expected, received
expected_2 = Developer.all.collect(&:id)
@@ -226,7 +221,19 @@ class DefaultScopingTest < ActiveRecord::TestCase
def test_unscope_joins_and_select_on_developers_projects
expected = Developer.all.collect(&:name)
- received = Developer.joins('JOIN developers_projects ON id = developer_id').select(:id).unscope(:joins, :select).collect(&:name)
+ received = Developer.joins("JOIN developers_projects ON id = developer_id").select(:id).unscope(:joins, :select).collect(&:name)
+ assert_equal expected, received
+ end
+
+ def test_unscope_left_outer_joins
+ expected = Developer.all.collect(&:name)
+ received = Developer.left_outer_joins(:projects).select(:id).unscope(:left_outer_joins, :select).collect(&:name)
+ assert_equal expected, received
+ end
+
+ def test_unscope_left_joins
+ expected = Developer.all.collect(&:name)
+ received = Developer.left_joins(:projects).select(:id).unscope(:left_joins, :select).collect(&:name)
assert_equal expected, received
end
@@ -247,8 +254,8 @@ class DefaultScopingTest < ActiveRecord::TestCase
scope :by_name, -> name { unscope(where: :name).where(name: name) }
end
- expected = developer_klass.where(name: 'Jamis').collect { |dev| [dev.name, dev.id] }
- received = developer_klass.where(name: 'David').by_name('Jamis').collect { |dev| [dev.name, dev.id] }
+ expected = developer_klass.where(name: "Jamis").collect { |dev| [dev.name, dev.id] }
+ received = developer_klass.where(name: "David").by_name("Jamis").collect { |dev| [dev.name, dev.id] }
assert_equal expected, received
end
@@ -262,11 +269,11 @@ class DefaultScopingTest < ActiveRecord::TestCase
end
assert_raises(ArgumentError) do
- Developer.order('name DESC').reverse_order.unscope(:reverse_order)
+ Developer.order("name DESC").reverse_order.unscope(:reverse_order)
end
assert_raises(ArgumentError) do
- Developer.order('name DESC').where(name: "Jamis").unscope()
+ Developer.order("name DESC").where(name: "Jamis").unscope()
end
end
@@ -296,40 +303,40 @@ class DefaultScopingTest < ActiveRecord::TestCase
def test_unscope_merging
merged = Developer.where(name: "Jamis").merge(Developer.unscope(:where))
- assert merged.where_clause.empty?
- assert !merged.where(name: "Jon").where_clause.empty?
+ assert_empty merged.where_clause
+ assert_not_empty merged.where(name: "Jon").where_clause
end
def test_order_in_default_scope_should_not_prevail
- expected = Developer.all.merge!(order: 'salary desc').to_a.collect(&:salary)
- received = DeveloperOrderedBySalary.all.merge!(order: 'salary').to_a.collect(&:salary)
+ expected = Developer.all.merge!(order: "salary desc").to_a.collect(&:salary)
+ received = DeveloperOrderedBySalary.all.merge!(order: "salary").to_a.collect(&:salary)
assert_equal expected, received
end
def test_create_attribute_overwrites_default_scoping
- assert_equal 'David', PoorDeveloperCalledJamis.create!(:name => 'David').name
- assert_equal 200000, PoorDeveloperCalledJamis.create!(:name => 'David', :salary => 200000).salary
+ assert_equal "David", PoorDeveloperCalledJamis.create!(name: "David").name
+ assert_equal 200000, PoorDeveloperCalledJamis.create!(name: "David", salary: 200000).salary
end
def test_create_attribute_overwrites_default_values
- assert_equal nil, PoorDeveloperCalledJamis.create!(:salary => nil).salary
- assert_equal 50000, PoorDeveloperCalledJamis.create!(:name => 'David').salary
+ assert_nil PoorDeveloperCalledJamis.create!(salary: nil).salary
+ assert_equal 50000, PoorDeveloperCalledJamis.create!(name: "David").salary
end
def test_default_scope_attribute
- jamis = PoorDeveloperCalledJamis.new(:name => 'David')
+ jamis = PoorDeveloperCalledJamis.new(name: "David")
assert_equal 50000, jamis.salary
end
def test_where_attribute
- aaron = PoorDeveloperCalledJamis.where(:salary => 20).new(:name => 'Aaron')
+ aaron = PoorDeveloperCalledJamis.where(salary: 20).new(name: "Aaron")
assert_equal 20, aaron.salary
- assert_equal 'Aaron', aaron.name
+ assert_equal "Aaron", aaron.name
end
def test_where_attribute_merge
- aaron = PoorDeveloperCalledJamis.where(:name => 'foo').new(:name => 'Aaron')
- assert_equal 'Aaron', aaron.name
+ aaron = PoorDeveloperCalledJamis.where(name: "foo").new(name: "Aaron")
+ assert_equal "Aaron", aaron.name
end
def test_scope_composed_by_limit_and_then_offset_is_equal_to_scope_composed_by_offset_and_then_limit
@@ -339,33 +346,53 @@ class DefaultScopingTest < ActiveRecord::TestCase
end
def test_create_with_merge
- aaron = PoorDeveloperCalledJamis.create_with(:name => 'foo', :salary => 20).merge(
- PoorDeveloperCalledJamis.create_with(:name => 'Aaron')).new
+ aaron = PoorDeveloperCalledJamis.create_with(name: "foo", salary: 20).merge(
+ PoorDeveloperCalledJamis.create_with(name: "Aaron")).new
assert_equal 20, aaron.salary
- assert_equal 'Aaron', aaron.name
+ assert_equal "Aaron", aaron.name
- aaron = PoorDeveloperCalledJamis.create_with(:name => 'foo', :salary => 20).
- create_with(:name => 'Aaron').new
+ aaron = PoorDeveloperCalledJamis.create_with(name: "foo", salary: 20).
+ create_with(name: "Aaron").new
assert_equal 20, aaron.salary
- assert_equal 'Aaron', aaron.name
+ assert_equal "Aaron", aaron.name
+ end
+
+ def test_create_with_using_both_string_and_symbol
+ jamis = PoorDeveloperCalledJamis.create_with(name: "foo").create_with("name" => "Aaron").new
+ assert_equal "Aaron", jamis.name
end
def test_create_with_reset
- jamis = PoorDeveloperCalledJamis.create_with(:name => 'Aaron').create_with(nil).new
- assert_equal 'Jamis', jamis.name
+ jamis = PoorDeveloperCalledJamis.create_with(name: "Aaron").create_with(nil).new
+ assert_equal "Jamis", jamis.name
+ end
+
+ def test_create_with_takes_precedence_over_where
+ developer = Developer.where(name: nil).create_with(name: "Aaron").new
+ assert_equal "Aaron", developer.name
+ end
+
+ def test_create_with_nested_attributes
+ assert_difference("Project.count", 1) do
+ Developer.create_with(
+ projects_attributes: [{ name: "p1" }]
+ ).scoping do
+ Developer.create!(name: "Aaron")
+ end
+ end
end
# FIXME: I don't know if this is *desired* behavior, but it is *today's*
# behavior.
def test_create_with_empty_hash_will_not_reset
- jamis = PoorDeveloperCalledJamis.create_with(:name => 'Aaron').create_with({}).new
- assert_equal 'Aaron', jamis.name
+ jamis = PoorDeveloperCalledJamis.create_with(name: "Aaron").create_with({}).new
+ assert_equal "Aaron", jamis.name
end
def test_unscoped_with_named_scope_should_not_have_default_scope
assert_equal [DeveloperCalledJamis.find(developers(:poor_jamis).id)], DeveloperCalledJamis.poor
- assert DeveloperCalledJamis.unscoped.poor.include?(developers(:david).becomes(DeveloperCalledJamis))
+ assert_includes DeveloperCalledJamis.unscoped.poor, developers(:david).becomes(DeveloperCalledJamis)
assert_equal 11, DeveloperCalledJamis.unscoped.length
assert_equal 1, DeveloperCalledJamis.poor.length
@@ -373,6 +400,46 @@ class DefaultScopingTest < ActiveRecord::TestCase
assert_equal 10, DeveloperCalledJamis.unscoped { DeveloperCalledJamis.poor }.length
end
+ def test_default_scope_with_joins
+ assert_equal Comment.where(post_id: SpecialPostWithDefaultScope.pluck(:id)).count,
+ Comment.joins(:special_post_with_default_scope).count
+ assert_equal Comment.where(post_id: Post.pluck(:id)).count,
+ Comment.joins(:post).count
+ end
+
+ def test_joins_not_affected_by_scope_other_than_default_or_unscoped
+ without_scope_on_post = Comment.joins(:post).to_a
+ with_scope_on_post = nil
+ Post.where(id: [1, 5, 6]).scoping do
+ with_scope_on_post = Comment.joins(:post).to_a
+ end
+
+ assert_equal with_scope_on_post, without_scope_on_post
+ end
+
+ def test_unscoped_with_joins_should_not_have_default_scope
+ assert_equal SpecialPostWithDefaultScope.unscoped { Comment.joins(:special_post_with_default_scope).to_a },
+ Comment.joins(:post).to_a
+ end
+
+ def test_sti_association_with_unscoped_not_affected_by_default_scope
+ post = posts(:thinking)
+ comments = [comments(:does_it_hurt)]
+
+ post.special_comments.update_all(deleted_at: Time.now)
+
+ assert_raises(ActiveRecord::RecordNotFound) { Post.joins(:special_comments).find(post.id) }
+ assert_equal [], post.special_comments
+
+ SpecialComment.unscoped do
+ assert_equal post, Post.joins(:special_comments).find(post.id)
+ assert_equal comments, Post.joins(:special_comments).find(post.id).special_comments
+ assert_equal comments, Post.eager_load(:special_comments).find(post.id).special_comments
+ assert_equal comments, Post.includes(:special_comments).find(post.id).special_comments
+ assert_equal comments, Post.preload(:special_comments).find(post.id).special_comments
+ end
+ end
+
def test_default_scope_select_ignored_by_aggregations
assert_equal DeveloperWithSelect.all.to_a.count, DeveloperWithSelect.count
end
@@ -395,9 +462,9 @@ class DefaultScopingTest < ActiveRecord::TestCase
def test_default_scope_include_with_count
d = DeveloperWithIncludes.create!
- d.audit_logs.create! :message => 'foo'
+ d.audit_logs.create! message: "foo"
- assert_equal 1, DeveloperWithIncludes.where(:audit_logs => { :message => 'foo' }).count
+ assert_equal 1, DeveloperWithIncludes.where(audit_logs: { message: "foo" }).count
end
def test_default_scope_with_references_works_through_collection_association
@@ -418,24 +485,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
@@ -451,6 +500,76 @@ class DefaultScopingTest < ActiveRecord::TestCase
test "a scope can remove the condition from the default scope" do
scope = DeveloperCalledJamis.david2
assert_equal 1, scope.where_clause.ast.children.length
- assert_equal Developer.where(name: "David"), scope
+ assert_equal Developer.where(name: "David").map(&:id), scope.map(&:id)
+ end
+
+ def test_with_abstract_class_where_clause_should_not_be_duplicated
+ scope = Bus.all
+ assert_equal scope.where_clause.ast.children.length, 1
+ end
+
+ def test_sti_conditions_are_not_carried_in_default_scope
+ ConditionalStiPost.create! body: ""
+ SubConditionalStiPost.create! body: ""
+ SubConditionalStiPost.create! title: "Hello world", body: ""
+
+ assert_equal 2, ConditionalStiPost.count
+ assert_equal 2, ConditionalStiPost.all.to_a.size
+ assert_equal 3, ConditionalStiPost.unscope(where: :title).to_a.size
+
+ assert_equal 1, SubConditionalStiPost.count
+ assert_equal 1, SubConditionalStiPost.all.to_a.size
+ assert_equal 2, SubConditionalStiPost.unscope(where: :title).to_a.size
+ end
+
+ def test_with_abstract_class_scope_should_be_executed_in_correct_context
+ vegetarian_pattern, gender_pattern = if current_adapter?(:Mysql2Adapter)
+ [/`lions`.`is_vegetarian`/, /`lions`.`gender`/]
+ elsif current_adapter?(:OracleAdapter)
+ [/"LIONS"."IS_VEGETARIAN"/, /"LIONS"."GENDER"/]
+ else
+ [/"lions"."is_vegetarian"/, /"lions"."gender"/]
+ end
+
+ assert_match vegetarian_pattern, Lion.all.to_sql
+ assert_match gender_pattern, Lion.female.to_sql
end
end
+
+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
+ 2.times { ThreadsafeDeveloper.unscoped.create! }
+
+ 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)
+ ensure
+ ThreadsafeDeveloper.unscoped.destroy_all
+ 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 e4cc533517..f707951a16 100644
--- a/activerecord/test/cases/scoping/named_scoping_test.rb
+++ b/activerecord/test/cases/scoping/named_scoping_test.rb
@@ -1,17 +1,19 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/post'
-require 'models/topic'
-require 'models/comment'
-require 'models/reply'
-require 'models/author'
-require 'models/developer'
-require 'models/computer'
+require "models/post"
+require "models/topic"
+require "models/comment"
+require "models/reply"
+require "models/author"
+require "models/developer"
+require "models/computer"
class NamedScopingTest < ActiveRecord::TestCase
fixtures :posts, :authors, :topics, :comments, :author_addresses
def test_implements_enumerable
- assert !Topic.all.empty?
+ assert_not_empty Topic.all
assert_equal Topic.all.to_a, Topic.base
assert_equal Topic.all.to_a, Topic.base.to_a
@@ -23,8 +25,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
@@ -33,12 +35,12 @@ class NamedScopingTest < ActiveRecord::TestCase
all_posts.to_a
new_post = Topic.create!
- assert !all_posts.include?(new_post)
- assert all_posts.reload.include?(new_post)
+ assert_not_includes all_posts, new_post
+ assert_includes all_posts.reload, new_post
end
def test_delegates_finds_and_calculations_to_the_base_class
- assert !Topic.all.empty?
+ assert_not_empty Topic.all
assert_equal Topic.all.to_a, Topic.base.to_a
assert_equal Topic.first, Topic.base.first
@@ -46,26 +48,33 @@ class NamedScopingTest < ActiveRecord::TestCase
assert_equal Topic.average(:replies_count), Topic.base.average(:replies_count)
end
+ def test_calling_merge_at_first_in_scope
+ Topic.class_eval do
+ scope :calling_merge_at_first_in_scope, Proc.new { merge(Topic.replied) }
+ end
+ assert_equal Topic.calling_merge_at_first_in_scope.to_a, Topic.replied.to_a
+ end
+
def test_method_missing_priority_when_delegating
klazz = Class.new(ActiveRecord::Base) do
self.table_name = "topics"
- scope :since, Proc.new { where('written_on >= ?', Time.now - 1.day) }
- scope :to, Proc.new { where('written_on <= ?', Time.now) }
+ scope :since, Proc.new { where("written_on >= ?", Time.now - 1.day) }
+ scope :to, Proc.new { where("written_on <= ?", Time.now) }
end
assert_equal klazz.to.since.to_a, klazz.since.to.to_a
end
def test_scope_should_respond_to_own_methods_and_methods_of_the_proxy
- assert Topic.approved.respond_to?(:limit)
- assert Topic.approved.respond_to?(:count)
- assert Topic.approved.respond_to?(:length)
+ assert_respond_to Topic.approved, :limit
+ assert_respond_to Topic.approved, :count
+ assert_respond_to Topic.approved, :length
end
def test_scopes_with_options_limit_finds_to_those_matching_the_criteria_specified
- assert !Topic.all.merge!(:where => {:approved => true}).to_a.empty?
+ assert_not_empty Topic.all.merge!(where: { approved: true }).to_a
- assert_equal Topic.all.merge!(:where => {:approved => true}).to_a, Topic.approved
- assert_equal Topic.where(:approved => true).count, Topic.approved.count
+ assert_equal Topic.all.merge!(where: { approved: true }).to_a, Topic.approved
+ assert_equal Topic.where(approved: true).count, Topic.approved.count
end
def test_scopes_with_string_name_can_be_composed
@@ -75,17 +84,17 @@ class NamedScopingTest < ActiveRecord::TestCase
end
def test_scopes_are_composable
- assert_equal((approved = Topic.all.merge!(:where => {:approved => true}).to_a), Topic.approved)
- assert_equal((replied = Topic.all.merge!(:where => 'replies_count > 0').to_a), Topic.replied)
- assert !(approved == replied)
- assert !(approved & replied).empty?
+ assert_equal((approved = Topic.all.merge!(where: { approved: true }).to_a), Topic.approved)
+ assert_equal((replied = Topic.all.merge!(where: "replies_count > 0").to_a), Topic.replied)
+ assert_not (approved == replied)
+ assert_not_empty (approved & replied)
assert_equal approved & replied, Topic.approved.replied
end
def test_procedural_scopes
- topics_written_before_the_third = Topic.where('written_on < ?', topics(:third).written_on)
- topics_written_before_the_second = Topic.where('written_on < ?', topics(:second).written_on)
+ topics_written_before_the_third = Topic.where("written_on < ?", topics(:third).written_on)
+ topics_written_before_the_second = Topic.where("written_on < ?", topics(:second).written_on)
assert_not_equal topics_written_before_the_second, topics_written_before_the_third
assert_equal topics_written_before_the_third, Topic.written_before(topics(:third).written_on)
@@ -101,31 +110,33 @@ class NamedScopingTest < ActiveRecord::TestCase
def test_scope_with_object
objects = Topic.with_object
assert_operator objects.length, :>, 0
- assert objects.all?(&:approved?), 'all objects should be approved'
+ assert objects.all?(&:approved?), "all objects should be approved"
end
def test_has_many_associations_have_access_to_scopes
assert_not_equal Post.containing_the_letter_a, authors(:david).posts
- assert !Post.containing_the_letter_a.empty?
+ assert_not_empty Post.containing_the_letter_a
- assert_equal authors(:david).posts & Post.containing_the_letter_a, authors(:david).posts.containing_the_letter_a
+ expected = authors(:david).posts & Post.containing_the_letter_a
+ assert_equal expected.sort_by(&:id), authors(:david).posts.containing_the_letter_a.sort_by(&:id)
end
def test_scope_with_STI
- assert_equal 3,Post.containing_the_letter_a.count
- assert_equal 1,SpecialPost.containing_the_letter_a.count
+ assert_equal 3, Post.containing_the_letter_a.count
+ assert_equal 1, SpecialPost.containing_the_letter_a.count
end
def test_has_many_through_associations_have_access_to_scopes
assert_not_equal Comment.containing_the_letter_e, authors(:david).comments
- assert !Comment.containing_the_letter_e.empty?
+ assert_not_empty Comment.containing_the_letter_e
- assert_equal authors(:david).comments & Comment.containing_the_letter_e, authors(:david).comments.containing_the_letter_e
+ expected = authors(:david).comments & Comment.containing_the_letter_e
+ assert_equal expected.sort_by(&:id), authors(:david).comments.containing_the_letter_e.sort_by(&:id)
end
def test_scopes_honor_current_scopes_from_when_defined
- assert !Post.ranked_by_comments.limit_by(5).empty?
- assert !authors(:david).posts.ranked_by_comments.limit_by(5).empty?
+ assert_not_empty Post.ranked_by_comments.limit_by(5)
+ assert_not_empty authors(:david).posts.ranked_by_comments.limit_by(5)
assert_not_equal Post.ranked_by_comments.limit_by(5), authors(:david).posts.ranked_by_comments.limit_by(5)
assert_not_equal Post.top(5), authors(:david).posts.top(5)
# Oracle sometimes sorts differently if WHERE condition is changed
@@ -140,27 +151,43 @@ class NamedScopingTest < ActiveRecord::TestCase
assert_equal "The scope body needs to be callable.", e.message
end
+ def test_scopes_name_is_relation_method
+ conflicts = [
+ :records,
+ :to_ary,
+ :to_sql,
+ :explain
+ ]
+
+ conflicts.each do |name|
+ e = assert_raises ArgumentError do
+ Class.new(Post).class_eval { scope name, -> { where(approved: true) } }
+ end
+ assert_match(/You tried to define a scope named \"#{name}\" on the model/, e.message)
+ end
+ end
+
def test_active_records_have_scope_named__all__
- assert !Topic.all.empty?
+ assert_not_empty Topic.all
assert_equal Topic.all.to_a, Topic.base
end
def test_active_records_have_scope_named__scoped__
scope = Topic.where("content LIKE '%Have%'")
- assert !scope.empty?
+ assert_not_empty scope
assert_equal scope, Topic.all.merge!(where: "content LIKE '%Have%'")
end
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
@@ -171,7 +198,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
@@ -180,7 +207,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
@@ -188,28 +215,29 @@ class NamedScopingTest < ActiveRecord::TestCase
def test_any_should_call_proxy_found_if_using_a_block
topics = Topic.base
assert_queries(1) do
- topics.expects(:empty?).never
- topics.any? { true }
+ assert_not_called(topics, :empty?) do
+ topics.any? { true }
+ end
end
end
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
def test_model_class_should_respond_to_any
- assert Topic.any?
+ assert_predicate Topic, :any?
Topic.delete_all
- assert !Topic.any?
+ assert_not_predicate Topic, :any?
end
def test_many_should_not_load_results
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
@@ -217,35 +245,36 @@ class NamedScopingTest < ActiveRecord::TestCase
def test_many_should_call_proxy_found_if_using_a_block
topics = Topic.base
assert_queries(1) do
- topics.expects(:size).never
- topics.many? { true }
+ assert_not_called(topics, :size) do
+ topics.many? { true }
+ end
end
end
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
def test_many_should_return_false_if_none_or_one
- topics = Topic.base.where(:id => 0)
- assert !topics.many?
- topics = Topic.base.where(:id => 1)
- assert !topics.many?
+ topics = Topic.base.where(id: 0)
+ assert_not_predicate topics, :many?
+ topics = Topic.base.where(id: 1)
+ assert_not_predicate topics, :many?
end
def test_many_should_return_true_if_more_than_one
- assert Topic.base.many?
+ assert_predicate Topic.base, :many?
end
def test_model_class_should_respond_to_many
Topic.delete_all
- assert !Topic.many?
+ assert_not_predicate Topic, :many?
Topic.create!
- assert !Topic.many?
+ assert_not_predicate Topic, :many?
Topic.create!
- assert Topic.many?
+ assert_predicate Topic, :many?
end
def test_should_build_on_top_of_scope
@@ -271,7 +300,14 @@ class NamedScopingTest < ActiveRecord::TestCase
def test_should_build_on_top_of_chained_scopes
topic = Topic.approved.by_lifo.build({})
assert topic.approved
- assert_equal 'lifo', topic.author_name
+ assert_equal "lifo", topic.author_name
+ end
+
+ def test_deprecated_delegating_private_method
+ assert_deprecated do
+ scope = Topic.all.by_private_lifo
+ assert_not scope.instance_variable_get(:@delegate_to_klass)
+ end
end
def test_reserved_scope_names
@@ -299,7 +335,7 @@ class NamedScopingTest < ActiveRecord::TestCase
:relation, # private class method on AR::Base
:new, # redefined class method on AR::Base
:all, # a default scope
- :public, # some imporant methods on Module and Class
+ :public, # some important methods on Module and Class
:protected,
:private,
:name,
@@ -318,12 +354,12 @@ class NamedScopingTest < ActiveRecord::TestCase
conflicts.each do |name|
e = assert_raises(ArgumentError, "scope `#{name}` should not be allowed") do
- klass.class_eval { scope name, ->{ where(approved: true) } }
+ klass.class_eval { scope name, -> { where(approved: true) } }
end
assert_match(/You tried to define a scope named \"#{name}\" on the model/, e.message)
e = assert_raises(ArgumentError, "scope `#{name}` should not be allowed") do
- subklass.class_eval { scope name, ->{ where(approved: true) } }
+ subklass.class_eval { scope name, -> { where(approved: true) } }
end
assert_match(/You tried to define a scope named \"#{name}\" on the model/, e.message)
end
@@ -331,12 +367,12 @@ class NamedScopingTest < ActiveRecord::TestCase
non_conflicts.each do |name|
assert_nothing_raised do
silence_warnings do
- klass.class_eval { scope name, ->{ where(approved: true) } }
+ klass.class_eval { scope name, -> { where(approved: true) } }
end
end
assert_nothing_raised do
- subklass.class_eval { scope name, ->{ where(approved: true) } }
+ subklass.class_eval { scope name, -> { where(approved: true) } }
end
end
end
@@ -348,7 +384,7 @@ class NamedScopingTest < ActiveRecord::TestCase
klass = Class.new(ActiveRecord::Base) do
self.table_name = "topics"
scope :"title containing space", -> { where("title LIKE '% %'") }
- scope :approved, -> { where(:approved => true) }
+ scope :approved, -> { where(approved: true) }
end
assert_equal klass.send(:"title containing space"), klass.where("title LIKE '% %'")
assert_equal klass.approved.send(:"title containing space"), klass.approved.where("title LIKE '% %'")
@@ -363,7 +399,7 @@ class NamedScopingTest < ActiveRecord::TestCase
end
def test_should_use_where_in_query_for_scope
- assert_equal Developer.where(name: 'Jamis').to_set, Developer.where(id: Developer.jamises).to_set
+ assert_equal Developer.where(name: "Jamis").to_set, Developer.where(id: Developer.jamises).to_set
end
def test_size_should_use_count_when_results_are_not_loaded
@@ -375,7 +411,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
@@ -394,16 +430,16 @@ class NamedScopingTest < ActiveRecord::TestCase
def test_chaining_applies_last_conditions_when_creating
post = Topic.rejected.new
- assert !post.approved?
+ assert_not_predicate post, :approved?
post = Topic.rejected.approved.new
- assert post.approved?
+ assert_predicate post, :approved?
post = Topic.approved.rejected.new
- assert !post.approved?
+ assert_not_predicate post, :approved?
post = Topic.approved.rejected.approved.new
- assert post.approved?
+ assert_predicate post, :approved?
end
def test_chaining_combines_conditions_when_searching
@@ -422,12 +458,12 @@ class NamedScopingTest < ActiveRecord::TestCase
assert_equal 4, Topic.approved.count
assert_queries(5) do
- Topic.approved.find_each(:batch_size => 1) {|t| assert t.approved? }
+ Topic.approved.find_each(batch_size: 1) { |t| assert t.approved? }
end
assert_queries(3) do
- Topic.approved.find_in_batches(:batch_size => 2) do |group|
- group.each {|t| assert t.approved? }
+ Topic.approved.find_in_batches(batch_size: 2) do |group|
+ group.each { |t| assert t.approved? }
end
end
end
@@ -438,9 +474,29 @@ class NamedScopingTest < ActiveRecord::TestCase
end
end
+ def test_scopes_with_reserved_names
+ class << Topic
+ def public_method; end
+ public :public_method
+
+ def protected_method; end
+ protected :protected_method
+
+ def private_method; end
+ private :private_method
+ end
+
+ [:public_method, :protected_method, :private_method].each do |reserved_method|
+ assert Topic.respond_to?(reserved_method, true)
+ assert_called(ActiveRecord::Base.logger, :warn) do
+ silence_warnings { Topic.scope(reserved_method, -> { }) }
+ end
+ end
+ end
+
def test_scopes_on_relations
# Topic.replied
- approved_topics = Topic.all.approved.order('id DESC')
+ approved_topics = Topic.all.approved.order("id DESC")
assert_equal topics(:fifth), approved_topics.first
replied_approved_topics = approved_topics.replied
@@ -448,9 +504,9 @@ class NamedScopingTest < ActiveRecord::TestCase
end
def test_index_on_scope
- approved = Topic.approved.order('id ASC')
+ approved = Topic.approved.order("id ASC")
assert_equal topics(:second), approved[0]
- assert approved.loaded?
+ assert_predicate approved, :loaded?
end
def test_nested_scopes_queries_size
@@ -489,7 +545,7 @@ class NamedScopingTest < ActiveRecord::TestCase
def test_scopes_to_get_newest
post = posts(:welcome)
old_last_comment = post.comments.newest
- new_comment = post.comments.create(:body => "My new comment")
+ new_comment = post.comments.create(body: "My new comment")
assert_equal new_comment, post.comments.newest
assert_not_equal old_last_comment, post.comments.newest
end
@@ -512,15 +568,34 @@ class NamedScopingTest < ActiveRecord::TestCase
def test_eager_default_scope_relations_are_remove
klass = Class.new(ActiveRecord::Base)
- klass.table_name = 'posts'
+ klass.table_name = "posts"
assert_raises(ArgumentError) do
- klass.send(:default_scope, klass.where(:id => posts(:welcome).id))
+ klass.send(:default_scope, klass.where(id: posts(:welcome).id))
end
end
def test_subclass_merges_scopes_properly
- assert_equal 1, SpecialComment.where(body: 'go crazy').created.count
+ assert_equal 1, SpecialComment.where(body: "go crazy").created.count
end
+ def test_model_class_should_respond_to_extending
+ assert_raises OopsError do
+ Comment.unscoped.oops_comments.destroy_all
+ end
+ end
+
+ def test_model_class_should_respond_to_none
+ assert_not_predicate Topic, :none?
+ Topic.delete_all
+ assert_predicate Topic, :none?
+ end
+
+ def test_model_class_should_respond_to_one
+ assert_not_predicate Topic, :one?
+ Topic.delete_all
+ assert_not_predicate Topic, :one?
+ Topic.create!
+ assert_predicate Topic, :one?
+ end
end
diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb
index 4bfffbe9c6..b4f4379e5e 100644
--- a/activerecord/test/cases/scoping/relation_scoping_test.rb
+++ b/activerecord/test/cases/scoping/relation_scoping_test.rb
@@ -1,16 +1,18 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/post'
-require 'models/author'
-require 'models/developer'
-require 'models/computer'
-require 'models/project'
-require 'models/comment'
-require 'models/category'
-require 'models/person'
-require 'models/reference'
+require "models/post"
+require "models/author"
+require "models/developer"
+require "models/computer"
+require "models/project"
+require "models/comment"
+require "models/category"
+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)
@@ -28,7 +30,7 @@ class RelationScopingTest < ActiveRecord::TestCase
def test_scope_breaks_caching_on_collections
author = authors :david
ids = author.reload.special_posts_with_default_scope.map(&:id)
- assert_equal [1,5,6], ids.sort
+ assert_equal [1, 5, 6], ids.sort
scoped_posts = SpecialPostWithDefaultScope.unscoped do
author = authors :david
author.reload.special_posts_with_default_scope.to_a
@@ -103,13 +105,13 @@ class RelationScopingTest < ActiveRecord::TestCase
Developer.select("id, name").scoping do
developer = Developer.where("name = 'David'").first
assert_equal "David", developer.name
- assert !developer.has_attribute?(:salary)
+ assert_not developer.has_attribute?(:salary)
end
end
def test_scope_select_concatenates
Developer.select("id, name").scoping do
- developer = Developer.select('salary').where("name = 'David'").first
+ developer = Developer.select("salary").where("name = 'David'").first
assert_equal 80000, developer.salary
assert developer.has_attribute?(:id)
assert developer.has_attribute?(:name)
@@ -122,7 +124,7 @@ class RelationScopingTest < ActiveRecord::TestCase
assert_equal 1, Developer.count
end
- Developer.where('salary = 100000').scoping do
+ Developer.where("salary = 100000").scoping do
assert_equal 8, Developer.count
assert_equal 1, Developer.where("name LIKE 'fixture_1%'").count
end
@@ -131,49 +133,49 @@ class RelationScopingTest < ActiveRecord::TestCase
def test_scoped_find_include
# with the include, will retrieve only developers for the given project
scoped_developers = Developer.includes(:projects).scoping do
- Developer.where('projects.id' => 2).to_a
+ Developer.where("projects.id" => 2).to_a
end
- assert scoped_developers.include?(developers(:david))
- assert !scoped_developers.include?(developers(:jamis))
+ assert_includes scoped_developers, developers(:david)
+ assert_not_includes scoped_developers, developers(:jamis)
assert_equal 1, scoped_developers.size
end
def test_scoped_find_joins
- scoped_developers = Developer.joins('JOIN developers_projects ON id = developer_id').scoping do
- Developer.where('developers_projects.project_id = 2').to_a
+ scoped_developers = Developer.joins("JOIN developers_projects ON id = developer_id").scoping do
+ Developer.where("developers_projects.project_id = 2").to_a
end
- assert scoped_developers.include?(developers(:david))
- assert !scoped_developers.include?(developers(:jamis))
+ assert_includes scoped_developers, developers(:david)
+ assert_not_includes scoped_developers, developers(:jamis)
assert_equal 1, scoped_developers.size
assert_equal developers(:david).attributes, scoped_developers.first.attributes
end
def test_scoped_create_with_where
- new_comment = VerySpecialComment.where(:post_id => 1).scoping do
- VerySpecialComment.create :body => "Wonderful world"
+ new_comment = VerySpecialComment.where(post_id: 1).scoping do
+ VerySpecialComment.create body: "Wonderful world"
end
assert_equal 1, new_comment.post_id
- assert Post.find(1).comments.include?(new_comment)
+ assert_includes Post.find(1).comments, new_comment
end
def test_scoped_create_with_create_with
- new_comment = VerySpecialComment.create_with(:post_id => 1).scoping do
- VerySpecialComment.create :body => "Wonderful world"
+ new_comment = VerySpecialComment.create_with(post_id: 1).scoping do
+ VerySpecialComment.create body: "Wonderful world"
end
assert_equal 1, new_comment.post_id
- assert Post.find(1).comments.include?(new_comment)
+ assert_includes Post.find(1).comments, new_comment
end
def test_scoped_create_with_create_with_has_higher_priority
- new_comment = VerySpecialComment.where(:post_id => 2).create_with(:post_id => 1).scoping do
- VerySpecialComment.create :body => "Wonderful world"
+ new_comment = VerySpecialComment.where(post_id: 2).create_with(post_id: 1).scoping do
+ VerySpecialComment.create body: "Wonderful world"
end
assert_equal 1, new_comment.post_id
- assert Post.find(1).comments.include?(new_comment)
+ assert_includes Post.find(1).comments, new_comment
end
def test_ensure_that_method_scoping_is_correctly_restored
@@ -193,7 +195,7 @@ class RelationScopingTest < ActiveRecord::TestCase
end
def test_update_all_default_scope_filters_on_joins
- DeveloperFilteredOnJoins.update_all(:salary => 65000)
+ DeveloperFilteredOnJoins.update_all(salary: 65000)
assert_equal 65000, Developer.find(developers(:david).id).salary
# has not changed jamis
@@ -209,23 +211,79 @@ class RelationScopingTest < ActiveRecord::TestCase
assert_not_equal [], Developer.all
end
- def test_current_scope_does_not_pollute_other_subclasses
- Post.none.scoping do
- assert StiPost.all.any?
+ def test_current_scope_does_not_pollute_sibling_subclasses
+ Comment.none.scoping do
+ assert_not_predicate SpecialComment.all, :any?
+ assert_not_predicate VerySpecialComment.all, :any?
+ assert_not_predicate SubSpecialComment.all, :any?
+ end
+
+ SpecialComment.none.scoping do
+ assert_predicate Comment.all, :any?
+ assert_predicate VerySpecialComment.all, :any?
+ assert_not_predicate SubSpecialComment.all, :any?
+ end
+
+ SubSpecialComment.none.scoping do
+ assert_predicate Comment.all, :any?
+ assert_predicate VerySpecialComment.all, :any?
+ assert_predicate SpecialComment.all, :any?
+ end
+ end
+
+ def test_scoping_is_correctly_restored
+ Comment.unscoped do
+ SpecialComment.unscoped.created
+ end
+
+ assert_nil Comment.send(:current_scope)
+ assert_nil SpecialComment.send(:current_scope)
+ end
+
+ def test_scoping_respects_current_class
+ Comment.unscoped do
+ assert_equal "a comment...", Comment.all.what_are_you
+ assert_equal "a special comment...", SpecialComment.all.what_are_you
+ end
+ end
+
+ def test_scoping_respects_sti_constraint
+ Comment.unscoped do
+ assert_equal comments(:greetings), Comment.find(1)
+ assert_raises(ActiveRecord::RecordNotFound) { SpecialComment.find(1) }
+ end
+ end
+
+ def test_scoping_works_in_the_scope_block
+ expected = SpecialPostWithDefaultScope.unscoped.to_a
+ assert_equal expected, SpecialPostWithDefaultScope.unscoped_all
+ end
+
+ def test_circular_joins_with_scoping_does_not_crash
+ posts = Post.joins(comments: :post).scoping do
+ Post.first(10)
+ end
+ assert_equal posts, Post.joins(comments: :post).first(10)
+ end
+
+ def test_circular_left_joins_with_scoping_does_not_crash
+ posts = Post.left_joins(comments: :post).scoping do
+ Post.first(10)
end
+ assert_equal posts, Post.left_joins(comments: :post).first(10)
end
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
+ Developer.where("salary = 80000").scoping do
Developer.limit(10).scoping do
devs = Developer.all
sql = devs.to_sql
- assert_match '(salary = 80000)', sql
- assert_match 'LIMIT 10', sql
+ assert_match "(salary = 80000)", sql
+ assert_match(/LIMIT 10|ROWNUM <= 10|FETCH FIRST 10 ROWS ONLY/, sql)
end
end
end
@@ -239,39 +297,39 @@ class NestedRelationScopingTest < ActiveRecord::TestCase
end
def test_replace_options
- Developer.where(:name => 'David').scoping do
+ Developer.where(name: "David").scoping do
Developer.unscoped do
- assert_equal 'Jamis', Developer.where(:name => 'Jamis').first[:name]
+ assert_equal "Jamis", Developer.where(name: "Jamis").first[:name]
end
- assert_equal 'David', Developer.first[:name]
+ assert_equal "David", Developer.first[:name]
end
end
def test_three_level_nested_exclusive_scoped_find
Developer.where("name = 'Jamis'").scoping do
- assert_equal 'Jamis', Developer.first.name
+ assert_equal "Jamis", Developer.first.name
Developer.unscoped.where("name = 'David'") do
- assert_equal 'David', Developer.first.name
+ 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
- assert_equal 'David', Developer.first.name
+ assert_equal "David", Developer.first.name
end
# ensure that scoping is restored
- assert_equal 'Jamis', Developer.first.name
+ assert_equal "Jamis", Developer.first.name
end
end
def test_nested_scoped_create
- comment = Comment.create_with(:post_id => 1).scoping do
- Comment.create_with(:post_id => 2).scoping do
- Comment.create :body => "Hey guys, nested scopes are broken. Please fix!"
+ comment = Comment.create_with(post_id: 1).scoping do
+ Comment.create_with(post_id: 2).scoping do
+ Comment.create body: "Hey guys, nested scopes are broken. Please fix!"
end
end
@@ -279,15 +337,15 @@ class NestedRelationScopingTest < ActiveRecord::TestCase
end
def test_nested_exclusive_scope_for_create
- comment = Comment.create_with(:body => "Hey guys, nested scopes are broken. Please fix!").scoping do
- Comment.unscoped.create_with(:post_id => 1).scoping do
- assert Comment.new.body.blank?
- Comment.create :body => "Hey guys"
+ comment = Comment.create_with(body: "Hey guys, nested scopes are broken. Please fix!").scoping do
+ Comment.unscoped.create_with(post_id: 1).scoping do
+ assert_predicate Comment.new.body, :blank?
+ Comment.create body: "Hey guys"
end
end
assert_equal 1, comment.post_id
- assert_equal 'Hey guys', comment.body
+ assert_equal "Hey guys", comment.body
end
end
@@ -299,24 +357,24 @@ class HasManyScopingTest < ActiveRecord::TestCase
end
def test_forwarding_of_static_methods
- assert_equal 'a comment...', Comment.what_are_you
- assert_equal 'a comment...', @welcome.comments.what_are_you
+ assert_equal "a comment...", Comment.what_are_you
+ assert_equal "a comment...", @welcome.comments.what_are_you
end
def test_forwarding_to_scoped
- assert_equal 4, Comment.search_by_type('Comment').size
- assert_equal 2, @welcome.comments.search_by_type('Comment').size
+ assert_equal 4, Comment.search_by_type("Comment").size
+ assert_equal 2, @welcome.comments.search_by_type("Comment").size
end
def test_nested_scope_finder
- Comment.where('1=0').scoping do
+ Comment.where("1=0").scoping do
assert_equal 0, @welcome.comments.count
- assert_equal 'a comment...', @welcome.comments.what_are_you
+ assert_equal "a comment...", @welcome.comments.what_are_you
end
- Comment.where('1=1').scoping do
+ Comment.where("1=1").scoping do
assert_equal 2, @welcome.comments.count
- assert_equal 'a comment...', @welcome.comments.what_are_you
+ assert_equal "a comment...", @welcome.comments.what_are_you
end
end
@@ -331,7 +389,7 @@ class HasManyScopingTest < ActiveRecord::TestCase
end
def test_should_maintain_default_scope_on_eager_loaded_associations
- michael = Person.where(:id => people(:michael).id).includes(:bad_references).first
+ michael = Person.where(id: people(:michael).id).includes(:bad_references).first
magician = BadReference.find(1)
assert_equal [magician], michael.bad_references
end
@@ -345,19 +403,19 @@ class HasAndBelongsToManyScopingTest < ActiveRecord::TestCase
end
def test_forwarding_of_static_methods
- assert_equal 'a category...', Category.what_are_you
- assert_equal 'a category...', @welcome.categories.what_are_you
+ assert_equal "a category...", Category.what_are_you
+ assert_equal "a category...", @welcome.categories.what_are_you
end
def test_nested_scope_finder
- Category.where('1=0').scoping do
+ Category.where("1=0").scoping do
assert_equal 0, @welcome.categories.count
- assert_equal 'a category...', @welcome.categories.what_are_you
+ assert_equal "a category...", @welcome.categories.what_are_you
end
- Category.where('1=1').scoping do
+ Category.where("1=1").scoping do
assert_equal 2, @welcome.categories.count
- assert_equal 'a category...', @welcome.categories.what_are_you
+ assert_equal "a category...", @welcome.categories.what_are_you
end
end
end
diff --git a/activerecord/test/cases/secure_token_test.rb b/activerecord/test/cases/secure_token_test.rb
index e731443fc2..f5fa6aa302 100644
--- a/activerecord/test/cases/secure_token_test.rb
+++ b/activerecord/test/cases/secure_token_test.rb
@@ -1,5 +1,7 @@
-require 'cases/helper'
-require 'models/user'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/user"
class SecureTokenTest < ActiveRecord::TestCase
setup do
@@ -27,6 +29,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/serialization_test.rb b/activerecord/test/cases/serialization_test.rb
index 35b13ea247..932780bfef 100644
--- a/activerecord/test/cases/serialization_test.rb
+++ b/activerecord/test/cases/serialization_test.rb
@@ -1,25 +1,27 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/contact'
-require 'models/topic'
-require 'models/book'
-require 'models/author'
-require 'models/post'
+require "models/contact"
+require "models/topic"
+require "models/book"
+require "models/author"
+require "models/post"
class SerializationTest < ActiveRecord::TestCase
fixtures :books
- FORMATS = [ :xml, :json ]
+ FORMATS = [ :json ]
def setup
@contact_attributes = {
- :name => 'aaron stack',
- :age => 25,
- :avatar => 'binarydata',
- :created_at => Time.utc(2006, 8, 1),
- :awesome => false,
- :preferences => { :gem => '<strong>ruby</strong>' },
- :alternative_id => nil,
- :id => nil
+ name: "aaron stack",
+ age: 25,
+ avatar: "binarydata",
+ created_at: Time.utc(2006, 8, 1),
+ awesome: false,
+ preferences: { gem: "<strong>ruby</strong>" },
+ alternative_id: nil,
+ id: nil
}
end
@@ -38,7 +40,7 @@ class SerializationTest < ActiveRecord::TestCase
def test_serialize_should_allow_attribute_only_filtering
FORMATS.each do |format|
- @serialized = Contact.new(@contact_attributes).send("to_#{format}", :only => [ :age, :name ])
+ @serialized = Contact.new(@contact_attributes).send("to_#{format}", only: [ :age, :name ])
contact = Contact.new.send("from_#{format}", @serialized)
assert_equal @contact_attributes[:name], contact.name, "For #{format}"
assert_nil contact.avatar, "For #{format}"
@@ -47,7 +49,7 @@ class SerializationTest < ActiveRecord::TestCase
def test_serialize_should_allow_attribute_except_filtering
FORMATS.each do |format|
- @serialized = Contact.new(@contact_attributes).send("to_#{format}", :except => [ :age, :name ])
+ @serialized = Contact.new(@contact_attributes).send("to_#{format}", except: [ :age, :name ])
contact = Contact.new.send("from_#{format}", @serialized)
assert_nil contact.name, "For #{format}"
assert_nil contact.age, "For #{format}"
@@ -60,20 +62,20 @@ class SerializationTest < ActiveRecord::TestCase
ActiveRecord::Base.include_root_in_json = true
klazz = Class.new(ActiveRecord::Base)
- klazz.table_name = 'topics'
+ klazz.table_name = "topics"
assert klazz.include_root_in_json
klazz.include_root_in_json = false
assert ActiveRecord::Base.include_root_in_json
- assert !klazz.include_root_in_json
- assert !klazz.new.include_root_in_json
+ assert_not klazz.include_root_in_json
+ assert_not klazz.new.include_root_in_json
ensure
ActiveRecord::Base.include_root_in_json = original_root_in_json
end
def test_read_attribute_for_serialization_with_format_without_method_missing
klazz = Class.new(ActiveRecord::Base)
- klazz.table_name = 'books'
+ klazz.table_name = "books"
book = klazz.new
assert_nil book.read_attribute_for_serialization(:format)
@@ -81,18 +83,18 @@ class SerializationTest < ActiveRecord::TestCase
def test_read_attribute_for_serialization_with_format_after_init
klazz = Class.new(ActiveRecord::Base)
- klazz.table_name = 'books'
+ klazz.table_name = "books"
- book = klazz.new(format: 'paperback')
- assert_equal 'paperback', book.read_attribute_for_serialization(:format)
+ book = klazz.new(format: "paperback")
+ assert_equal "paperback", book.read_attribute_for_serialization(:format)
end
def test_read_attribute_for_serialization_with_format_after_find
klazz = Class.new(ActiveRecord::Base)
- klazz.table_name = 'books'
+ klazz.table_name = "books"
book = klazz.find(books(:awdr).id)
- assert_equal 'paperback', book.read_attribute_for_serialization(:format)
+ assert_equal "paperback", book.read_attribute_for_serialization(:format)
end
def test_find_records_by_serialized_attributes_through_join
diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb
index 6056156698..1192b30b14 100644
--- a/activerecord/test/cases/serialized_attribute_test.rb
+++ b/activerecord/test/cases/serialized_attribute_test.rb
@@ -1,10 +1,12 @@
-require 'cases/helper'
-require 'models/topic'
-require 'models/reply'
-require 'models/person'
-require 'models/traffic_light'
-require 'models/post'
-require 'bcrypt'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/topic"
+require "models/reply"
+require "models/person"
+require "models/traffic_light"
+require "models/post"
+require "bcrypt"
class SerializedAttributeTest < ActiveRecord::TestCase
fixtures :topics, :posts
@@ -25,7 +27,7 @@ class SerializedAttributeTest < ActiveRecord::TestCase
def test_serialized_attribute
Topic.serialize("content", MyObject)
- myobj = MyObject.new('value1', 'value2')
+ myobj = MyObject.new("value1", "value2")
topic = Topic.create("content" => myobj)
assert_equal(myobj, topic.content)
@@ -36,7 +38,7 @@ class SerializedAttributeTest < ActiveRecord::TestCase
def test_serialized_attribute_in_base_class
Topic.serialize("content", Hash)
- hash = { 'content1' => 'value1', 'content2' => 'value2' }
+ hash = { "content1" => "value1", "content2" => "value2" }
important_topic = ImportantTopic.create("content" => hash)
assert_equal(hash, important_topic.content)
@@ -97,7 +99,7 @@ class SerializedAttributeTest < ActiveRecord::TestCase
end
def test_serialized_attribute_declared_in_subclass
- hash = { 'important1' => 'value1', 'important2' => 'value2' }
+ hash = { "important1" => "value1", "important2" => "value2" }
important_topic = ImportantTopic.create("important" => hash)
assert_equal(hash, important_topic.important)
@@ -107,7 +109,7 @@ class SerializedAttributeTest < ActiveRecord::TestCase
end
def test_serialized_time_attribute
- myobj = Time.local(2008,1,1,1,0)
+ myobj = Time.local(2008, 1, 1, 1, 0)
topic = Topic.create("content" => myobj).reload
assert_equal(myobj, topic.content)
end
@@ -124,26 +126,26 @@ class SerializedAttributeTest < ActiveRecord::TestCase
end
def test_nil_not_serialized_without_class_constraint
- assert Topic.new(:content => nil).save
- assert_equal 1, Topic.where(:content => nil).count
+ assert Topic.new(content: nil).save
+ assert_equal 1, Topic.where(content: nil).count
end
def test_nil_not_serialized_with_class_constraint
Topic.serialize :content, Hash
- assert Topic.new(:content => nil).save
- assert_equal 1, Topic.where(:content => nil).count
+ assert Topic.new(content: nil).save
+ assert_equal 1, Topic.where(content: nil).count
end
def test_serialized_attribute_should_raise_exception_on_assignment_with_wrong_type
Topic.serialize(:content, Hash)
assert_raise(ActiveRecord::SerializationTypeMismatch) do
- Topic.new(content: 'string')
+ Topic.new(content: "string")
end
end
def test_should_raise_exception_on_serialized_attribute_with_type_mismatch
- myobj = MyObject.new('value1', 'value2')
- topic = Topic.new(:content => myobj)
+ myobj = MyObject.new("value1", "value2")
+ topic = Topic.new(content: myobj)
assert topic.save
Topic.serialize(:content, Hash)
assert_raise(ActiveRecord::SerializationTypeMismatch) { Topic.find(topic.id).content }
@@ -152,11 +154,32 @@ class SerializedAttributeTest < ActiveRecord::TestCase
def test_serialized_attribute_with_class_constraint
settings = { "color" => "blue" }
Topic.serialize(:content, Hash)
- topic = Topic.new(:content => settings)
+ topic = Topic.new(content: settings)
assert topic.save
assert_equal(settings, Topic.find(topic.id).content)
end
+ def test_where_by_serialized_attribute_with_array
+ settings = [ "color" => "green" ]
+ Topic.serialize(:content, Array)
+ topic = Topic.create!(content: settings)
+ assert_equal topic, Topic.where(content: settings).take
+ end
+
+ def test_where_by_serialized_attribute_with_hash
+ settings = { "color" => "green" }
+ Topic.serialize(:content, Hash)
+ topic = Topic.create!(content: settings)
+ assert_equal topic, Topic.where(content: settings).take
+ end
+
+ def test_where_by_serialized_attribute_with_hash_in_array
+ settings = { "color" => "green" }
+ Topic.serialize(:content, Hash)
+ topic = Topic.create!(content: settings)
+ assert_equal topic, Topic.where(content: [settings]).take
+ end
+
def test_serialized_default_class
Topic.serialize(:content, Hash)
topic = Topic.new
@@ -175,17 +198,17 @@ class SerializedAttributeTest < ActiveRecord::TestCase
end
def test_serialized_boolean_value_true
- topic = Topic.new(:content => true)
+ 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)
+ 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
@@ -200,18 +223,18 @@ class SerializedAttributeTest < ActiveRecord::TestCase
end
Topic.serialize(:content, some_class)
- topic = Topic.new(:content => some_class.new('my value'))
+ topic = Topic.new(content: some_class.new("my value"))
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
with_timezone_config aware_attributes: true do
Topic.serialize(:content, MyObject)
- myobj = MyObject.new('value1', 'value2')
+ myobj = MyObject.new("value1", "value2")
topic = Topic.create(content: myobj)
assert_equal(myobj, Topic.select(:content).find(topic.id).content)
@@ -220,8 +243,8 @@ class SerializedAttributeTest < ActiveRecord::TestCase
end
def test_serialize_attribute_can_be_serialized_in_an_integer_column
- insures = ['life']
- person = SerializedPerson.new(first_name: 'David', insures: insures)
+ insures = ["life"]
+ person = SerializedPerson.new(first_name: "David", insures: insures)
assert person.save
person = person.reload
assert_equal(insures, person.insures)
@@ -233,6 +256,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)
@@ -256,7 +293,7 @@ class SerializedAttributeTest < ActiveRecord::TestCase
topic = Topic.new(content: nil)
- assert_not topic.content_changed?
+ assert_not_predicate topic, :content_changed?
end
def test_classes_without_no_arg_constructors_are_not_supported
@@ -285,7 +322,7 @@ class SerializedAttributeTest < ActiveRecord::TestCase
topic = Topic.create!(content: {})
topic2 = Topic.create!(content: nil)
- assert_equal [topic, topic2], Topic.where(content: nil)
+ assert_equal [topic, topic2], Topic.where(content: nil).sort_by(&:id)
end
def test_nil_is_always_persisted_as_null
@@ -295,4 +332,65 @@ class SerializedAttributeTest < ActiveRecord::TestCase
topic.update_attribute :content, nil
assert_equal [topic], Topic.where(content: nil)
end
+
+ def test_mutation_detection_does_not_double_serialize
+ coder = Object.new
+ def coder.dump(value)
+ return if value.nil?
+ value + " encoded"
+ end
+ def coder.load(value)
+ return if value.nil?
+ value.gsub(" encoded", "")
+ end
+ type = Class.new(ActiveModel::Type::Value) do
+ include ActiveModel::Type::Helpers::Mutable
+
+ def serialize(value)
+ return if value.nil?
+ value + " serialized"
+ end
+
+ def deserialize(value)
+ return if value.nil?
+ value.gsub(" serialized", "")
+ end
+ end.new
+ model = Class.new(Topic) do
+ attribute :foo, type
+ serialize :foo, coder
+ end
+
+ topic = model.create!(foo: "bar")
+ topic.foo
+ assert_not_predicate topic, :changed?
+ end
+
+ def test_serialized_attribute_works_under_concurrent_initial_access
+ model = Topic.dup
+
+ topic = model.last
+ topic.update group: "1"
+
+ model.serialize :group, JSON
+ model.reset_column_information
+
+ # This isn't strictly necessary for the test, but a little bit of
+ # knowledge of internals allows us to make failures far more likely.
+ model.define_singleton_method(:define_attribute) do |*args|
+ Thread.pass
+ super(*args)
+ end
+
+ threads = 4.times.map do
+ Thread.new do
+ topic.reload.group
+ end
+ end
+
+ # All the threads should retrieve the value knowing it is JSON, and
+ # thus decode it. If this fails, some threads will instead see the
+ # raw string ("1"), or raise an exception.
+ assert_equal [1] * threads.size, threads.map(&:value)
+ end
end
diff --git a/activerecord/test/cases/statement_cache_test.rb b/activerecord/test/cases/statement_cache_test.rb
index a704b861cb..e3c12f68fd 100644
--- a/activerecord/test/cases/statement_cache_test.rb
+++ b/activerecord/test/cases/statement_cache_test.rb
@@ -1,8 +1,10 @@
-require 'cases/helper'
-require 'models/book'
-require 'models/liquid'
-require 'models/molecule'
-require 'models/electron'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/book"
+require "models/liquid"
+require "models/molecule"
+require "models/electron"
module ActiveRecord
class StatementCacheTest < ActiveRecord::TestCase
@@ -10,22 +12,20 @@ module ActiveRecord
@connection = ActiveRecord::Base.connection
end
- #Cache v 1.1 tests
def test_statement_cache
Book.create(name: "my book")
Book.create(name: "my other book")
cache = StatementCache.create(Book.connection) do |params|
- Book.where(:name => params.bind)
+ Book.where(name: params.bind)
end
- b = cache.execute([ "my book" ], Book, Book.connection)
+ b = cache.execute([ "my book" ], Book.connection)
assert_equal "my book", b[0].name
- b = cache.execute([ "my other book" ], Book, Book.connection)
+ b = cache.execute([ "my other book" ], Book.connection)
assert_equal "my other book", b[0].name
end
-
def test_statement_cache_id
b1 = Book.create(name: "my book")
b2 = Book.create(name: "my other book")
@@ -34,9 +34,9 @@ module ActiveRecord
Book.where(id: params.bind)
end
- b = cache.execute([ b1.id ], Book, Book.connection)
+ b = cache.execute([ b1.id ], Book.connection)
assert_equal b1.name, b[0].name
- b = cache.execute([ b2.id ], Book, Book.connection)
+ b = cache.execute([ b2.id ], Book.connection)
assert_equal b2.name, b[0].name
end
@@ -50,8 +50,6 @@ module ActiveRecord
assert_equal("my other book", b.name)
end
- #End
-
def test_statement_cache_with_simple_statement
cache = ActiveRecord::StatementCache.create(Book.connection) do |params|
Book.where(name: "my book").where("author_id > 3")
@@ -59,20 +57,20 @@ module ActiveRecord
Book.create(name: "my book", author_id: 4)
- books = cache.execute([], Book, Book.connection)
+ books = cache.execute([], Book.connection)
assert_equal "my book", books[0].name
end
def test_statement_cache_with_complex_statement
cache = ActiveRecord::StatementCache.create(Book.connection) do |params|
- Liquid.joins(:molecules => :electrons).where('molecules.name' => 'dioxane', 'electrons.name' => 'lepton')
+ Liquid.joins(molecules: :electrons).where("molecules.name" => "dioxane", "electrons.name" => "lepton")
end
- salty = Liquid.create(name: 'salty')
- molecule = salty.molecules.create(name: 'dioxane')
- molecule.electrons.create(name: 'lepton')
+ salty = Liquid.create(name: "salty")
+ molecule = salty.molecules.create(name: "dioxane")
+ molecule.electrons.create(name: "lepton")
- liquids = cache.execute([], Book, Book.connection)
+ liquids = cache.execute([], Book.connection)
assert_equal "salty", liquids[0].name
end
@@ -85,14 +83,52 @@ module ActiveRecord
Book.create(name: "my book")
end
- first_books = cache.execute([], Book, Book.connection)
+ first_books = cache.execute([], Book.connection)
3.times do
Book.create(name: "my book")
end
- additional_books = cache.execute([], Book, Book.connection)
+ additional_books = cache.execute([], Book.connection)
assert first_books != additional_books
end
+
+ def test_unprepared_statements_dont_share_a_cache_with_prepared_statements
+ Book.create(name: "my book")
+ Book.create(name: "my other book")
+
+ book = Book.find_by(name: "my book")
+ other_book = Book.connection.unprepared_statement do
+ Book.find_by(name: "my other book")
+ end
+
+ assert_not_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/store_test.rb b/activerecord/test/cases/store_test.rb
index e9cdf94c99..4457cfbd37 100644
--- a/activerecord/test/cases/store_test.rb
+++ b/activerecord/test/cases/store_test.rb
@@ -1,60 +1,82 @@
-require 'cases/helper'
-require 'models/admin'
-require 'models/admin/user'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/admin"
+require "models/admin/user"
class StoreTest < ActiveRecord::TestCase
fixtures :'admin/users'
setup do
- @john = Admin::User.create!(:name => 'John Doe', :color => 'black', :remember_login => true, :height => 'tall', :is_a_good_guy => true)
+ @john = Admin::User.create!(
+ name: "John Doe", color: "black", remember_login: true,
+ height: "tall", is_a_good_guy: true,
+ parent_name: "Quinn", partner_name: "Dallas",
+ partner_birthday: "1997-11-1"
+ )
end
test "reading store attributes through accessors" do
- assert_equal 'black', @john.color
+ assert_equal "black", @john.color
assert_nil @john.homepage
end
test "writing store attributes through accessors" do
- @john.color = 'red'
- @john.homepage = '37signals.com'
+ @john.color = "red"
+ @john.homepage = "37signals.com"
+
+ assert_equal "red", @john.color
+ assert_equal "37signals.com", @john.homepage
+ end
+
+ test "reading store attributes through accessors with prefix" do
+ assert_equal "Quinn", @john.parent_name
+ assert_nil @john.parent_birthday
+ assert_equal "Dallas", @john.partner_name
+ assert_equal "1997-11-1", @john.partner_birthday
+ end
+
+ test "writing store attributes through accessors with prefix" do
+ @john.partner_name = "River"
+ @john.partner_birthday = "1999-2-11"
- assert_equal 'red', @john.color
- assert_equal '37signals.com', @john.homepage
+ assert_equal "River", @john.partner_name
+ assert_equal "1999-2-11", @john.partner_birthday
end
test "accessing attributes not exposed by accessors" do
- @john.settings[:icecream] = 'graeters'
+ @john.settings[:icecream] = "graeters"
@john.save
- assert_equal 'graeters', @john.reload.settings[:icecream]
+ assert_equal "graeters", @john.reload.settings[:icecream]
end
test "overriding a read accessor" do
- @john.settings[:phone_number] = '1234567890'
+ @john.settings[:phone_number] = "1234567890"
- assert_equal '(123) 456-7890', @john.phone_number
+ assert_equal "(123) 456-7890", @john.phone_number
end
test "overriding a read accessor using super" do
@john.settings[:color] = nil
- assert_equal 'red', @john.color
+ assert_equal "red", @john.color
end
test "updating the store will mark it as changed" do
- @john.color = 'red'
- assert @john.settings_changed?
+ @john.color = "red"
+ assert_predicate @john, :settings_changed?
end
test "updating the store populates the changed array correctly" do
- @john.color = 'red'
- assert_equal 'black', @john.settings_change[0]['color']
- assert_equal 'red', @john.settings_change[1]['color']
+ @john.color = "red"
+ assert_equal "black", @john.settings_change[0]["color"]
+ assert_equal "red", @john.settings_change[1]["color"]
end
test "updating the store won't mark it as changed if an attribute isn't changed" do
@john.color = @john.color
- assert !@john.settings_changed?
+ assert_not_predicate @john, :settings_changed?
end
test "object initialization with not nullable column" do
@@ -67,75 +89,75 @@ class StoreTest < ActiveRecord::TestCase
end
test "overriding a write accessor" do
- @john.phone_number = '(123) 456-7890'
+ @john.phone_number = "(123) 456-7890"
- assert_equal '1234567890', @john.settings[:phone_number]
+ assert_equal "1234567890", @john.settings[:phone_number]
end
test "overriding a write accessor using super" do
- @john.color = 'yellow'
+ @john.color = "yellow"
- assert_equal 'blue', @john.color
+ assert_equal "blue", @john.color
end
test "preserve store attributes data in HashWithIndifferentAccess format without any conversion" do
- @john.json_data = ActiveSupport::HashWithIndifferentAccess.new(:height => 'tall', 'weight' => 'heavy')
- @john.height = 'low'
+ @john.json_data = ActiveSupport::HashWithIndifferentAccess.new(:height => "tall", "weight" => "heavy")
+ @john.height = "low"
assert_equal true, @john.json_data.instance_of?(ActiveSupport::HashWithIndifferentAccess)
- assert_equal 'low', @john.json_data[:height]
- assert_equal 'low', @john.json_data['height']
- assert_equal 'heavy', @john.json_data[:weight]
- assert_equal 'heavy', @john.json_data['weight']
+ assert_equal "low", @john.json_data[:height]
+ assert_equal "low", @john.json_data["height"]
+ assert_equal "heavy", @john.json_data[:weight]
+ assert_equal "heavy", @john.json_data["weight"]
end
test "convert store attributes from Hash to HashWithIndifferentAccess saving the data and access attributes indifferently" do
- user = Admin::User.find_by_name('Jamis')
- assert_equal 'symbol', user.settings[:symbol]
- assert_equal 'symbol', user.settings['symbol']
- assert_equal 'string', user.settings[:string]
- assert_equal 'string', user.settings['string']
+ user = Admin::User.find_by_name("Jamis")
+ assert_equal "symbol", user.settings[:symbol]
+ assert_equal "symbol", user.settings["symbol"]
+ assert_equal "string", user.settings[:string]
+ assert_equal "string", user.settings["string"]
assert_equal true, user.settings.instance_of?(ActiveSupport::HashWithIndifferentAccess)
- user.height = 'low'
- assert_equal 'symbol', user.settings[:symbol]
- assert_equal 'symbol', user.settings['symbol']
- assert_equal 'string', user.settings[:string]
- assert_equal 'string', user.settings['string']
+ user.height = "low"
+ assert_equal "symbol", user.settings[:symbol]
+ assert_equal "symbol", user.settings["symbol"]
+ assert_equal "string", user.settings[:string]
+ assert_equal "string", user.settings["string"]
assert_equal true, user.settings.instance_of?(ActiveSupport::HashWithIndifferentAccess)
end
- test "convert store attributes from any format other than Hash or HashWithIndifferent access losing the data" do
+ test "convert store attributes from any format other than Hash or HashWithIndifferentAccess losing the data" do
@john.json_data = "somedata"
- @john.height = 'low'
+ @john.height = "low"
assert_equal true, @john.json_data.instance_of?(ActiveSupport::HashWithIndifferentAccess)
- assert_equal 'low', @john.json_data[:height]
- assert_equal 'low', @john.json_data['height']
- assert_equal false, @john.json_data.delete_if { |k, v| k == 'height' }.any?
+ assert_equal "low", @john.json_data[:height]
+ assert_equal "low", @john.json_data["height"]
+ assert_equal false, @john.json_data.delete_if { |k, v| k == "height" }.any?
end
test "reading store attributes through accessors encoded with JSON" do
- assert_equal 'tall', @john.height
+ assert_equal "tall", @john.height
assert_nil @john.weight
end
test "writing store attributes through accessors encoded with JSON" do
- @john.height = 'short'
- @john.weight = 'heavy'
+ @john.height = "short"
+ @john.weight = "heavy"
- assert_equal 'short', @john.height
- assert_equal 'heavy', @john.weight
+ assert_equal "short", @john.height
+ assert_equal "heavy", @john.weight
end
test "accessing attributes not exposed by accessors encoded with JSON" do
- @john.json_data['somestuff'] = 'somecoolstuff'
+ @john.json_data["somestuff"] = "somecoolstuff"
@john.save
- assert_equal 'somecoolstuff', @john.reload.json_data['somestuff']
+ assert_equal "somecoolstuff", @john.reload.json_data["somestuff"]
end
test "updating the store will mark it as changed encoded with JSON" do
- @john.height = 'short'
- assert @john.json_data_changed?
+ @john.height = "short"
+ assert_predicate @john, :json_data_changed?
end
test "object initialization with not nullable column encoded with JSON" do
@@ -177,6 +199,7 @@ class StoreTest < ActiveRecord::TestCase
assert_equal [:color], first_model.stored_attributes[:data]
assert_equal [:color, :width, :height], second_model.stored_attributes[:data]
assert_equal [:color, :area, :volume], third_model.stored_attributes[:data]
+ assert_equal [:color], first_model.stored_attributes[:data]
end
test "YAML coder initializes the store when a Nil value is given" do
@@ -191,4 +214,38 @@ class StoreTest < ActiveRecord::TestCase
second_dump = YAML.dump(loaded)
assert_equal @john, YAML.load(second_dump)
end
+
+ test "read store attributes through accessors with default suffix" do
+ @john.configs[:two_factor_auth] = true
+ assert_equal true, @john.two_factor_auth_configs
+ end
+
+ test "write store attributes through accessors with default suffix" do
+ @john.two_factor_auth_configs = false
+ assert_equal false, @john.configs[:two_factor_auth]
+ end
+
+ test "read store attributes through accessors with custom suffix" do
+ @john.configs[:login_retry] = 3
+ assert_equal 3, @john.login_retry_config
+ end
+
+ test "write store attributes through accessors with custom suffix" do
+ @john.login_retry_config = 5
+ assert_equal 5, @john.configs[:login_retry]
+ end
+
+ test "read accessor without pre/suffix in the same store as other pre/suffixed accessors still works" do
+ @john.configs[:secret_question] = "What is your high school?"
+ assert_equal "What is your high school?", @john.secret_question
+ end
+
+ test "write accessor without pre/suffix in the same store as other pre/suffixed accessors still works" do
+ @john.secret_question = "What was the Rails version when you first worked on it?"
+ assert_equal "What was the Rails version when you first worked on it?", @john.configs[:secret_question]
+ end
+
+ test "prefix/suffix do not affect stored attributes" do
+ assert_equal [:secret_question, :two_factor_auth, :login_retry], Admin::User.stored_attributes[:configs]
+ end
end
diff --git a/activerecord/test/cases/suppressor_test.rb b/activerecord/test/cases/suppressor_test.rb
index 72c5c16555..9be5356901 100644
--- a/activerecord/test/cases/suppressor_test.rb
+++ b/activerecord/test/cases/suppressor_test.rb
@@ -1,6 +1,8 @@
-require 'cases/helper'
-require 'models/notification'
-require 'models/user'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/notification"
+require "models/user"
class SuppressorTest < ActiveRecord::TestCase
def test_suppresses_create
@@ -15,22 +17,22 @@ class SuppressorTest < ActiveRecord::TestCase
end
def test_suppresses_update
- user = User.create! token: 'asdf'
+ user = User.create! token: "asdf"
User.suppress do
- user.update token: 'ghjkl'
- assert_equal 'asdf', user.reload.token
+ user.update token: "ghjkl"
+ assert_equal "asdf", user.reload.token
- user.update! token: 'zxcvbnm'
- assert_equal 'asdf', user.reload.token
+ user.update! token: "zxcvbnm"
+ assert_equal "asdf", user.reload.token
- user.token = 'qwerty'
+ user.token = "qwerty"
user.save
- assert_equal 'asdf', user.reload.token
+ assert_equal "asdf", user.reload.token
- user.token = 'uiop'
+ user.token = "uiop"
user.save!
- assert_equal 'asdf', user.reload.token
+ assert_equal "asdf", user.reload.token
end
end
@@ -46,7 +48,30 @@ class SuppressorTest < ActiveRecord::TestCase
Notification.suppress { UserWithNotification.create! }
assert_difference -> { Notification.count } do
- Notification.create!
+ Notification.create!(message: "New Comment")
+ end
+ end
+
+ def test_suppresses_validations_on_create
+ assert_no_difference -> { Notification.count } do
+ Notification.suppress do
+ User.create
+ User.create!
+ User.new.save
+ User.new.save!
+ end
+ end
+ end
+
+ def test_suppresses_when_nested_multiple_times
+ assert_no_difference -> { Notification.count } do
+ Notification.suppress do
+ Notification.suppress { }
+ Notification.create
+ Notification.create!
+ Notification.new.save
+ Notification.new.save!
+ end
end
end
end
diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb
index 38164b2228..3fd1813d64 100644
--- a/activerecord/test/cases/tasks/database_tasks_test.rb
+++ b/activerecord/test/cases/tasks/database_tasks_test.rb
@@ -1,23 +1,111 @@
-require 'cases/helper'
-require 'active_record/tasks/database_tasks'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "active_record/tasks/database_tasks"
module ActiveRecord
module DatabaseTasksSetupper
def setup
- @mysql_tasks, @postgresql_tasks, @sqlite_tasks = stub, stub, stub
- ActiveRecord::Tasks::MySQLDatabaseTasks.stubs(:new).returns @mysql_tasks
- ActiveRecord::Tasks::PostgreSQLDatabaseTasks.stubs(:new).returns @postgresql_tasks
- ActiveRecord::Tasks::SQLiteDatabaseTasks.stubs(:new).returns @sqlite_tasks
+ @mysql_tasks, @postgresql_tasks, @sqlite_tasks = Array.new(
+ 3,
+ Class.new do
+ def create; end
+ def drop; end
+ def purge; end
+ def charset; end
+ def collation; end
+ def structure_dump(*); end
+ def structure_load(*); end
+ end.new
+ )
+
+ $stdout, @original_stdout = StringIO.new, $stdout
+ $stderr, @original_stderr = StringIO.new, $stderr
+ end
+
+ def teardown
+ $stdout, $stderr = @original_stdout, @original_stderr
+ end
+
+ def with_stubbed_new
+ ActiveRecord::Tasks::MySQLDatabaseTasks.stub(:new, @mysql_tasks) do
+ ActiveRecord::Tasks::PostgreSQLDatabaseTasks.stub(:new, @postgresql_tasks) do
+ ActiveRecord::Tasks::SQLiteDatabaseTasks.stub(:new, @sqlite_tasks) do
+ yield
+ end
+ end
+ end
end
end
ADAPTERS_TASKS = {
- mysql: :mysql_tasks,
mysql2: :mysql_tasks,
postgresql: :postgresql_tasks,
sqlite3: :sqlite_tasks
}
+ class DatabaseTasksUtilsTask < ActiveRecord::TestCase
+ def test_raises_an_error_when_called_with_protected_environment
+ protected_environments = ActiveRecord::Base.protected_environments
+ current_env = ActiveRecord::Base.connection.migration_context.current_environment
+
+ assert_called_on_instance_of(
+ ActiveRecord::MigrationContext,
+ :current_version,
+ times: 6,
+ returns: 1
+ ) do
+ assert_not_includes protected_environments, current_env
+ # Assert no error
+ ActiveRecord::Tasks::DatabaseTasks.check_protected_environments!
+
+ ActiveRecord::Base.protected_environments = [current_env]
+
+ assert_raise(ActiveRecord::ProtectedEnvironmentError) do
+ ActiveRecord::Tasks::DatabaseTasks.check_protected_environments!
+ end
+ end
+ ensure
+ ActiveRecord::Base.protected_environments = protected_environments
+ end
+
+ def test_raises_an_error_when_called_with_protected_environment_which_name_is_a_symbol
+ protected_environments = ActiveRecord::Base.protected_environments
+ current_env = ActiveRecord::Base.connection.migration_context.current_environment
+ assert_called_on_instance_of(
+ ActiveRecord::MigrationContext,
+ :current_version,
+ times: 6,
+ returns: 1
+ ) do
+ assert_not_includes protected_environments, current_env
+ # Assert no error
+ ActiveRecord::Tasks::DatabaseTasks.check_protected_environments!
+
+ ActiveRecord::Base.protected_environments = [current_env.to_sym]
+ assert_raise(ActiveRecord::ProtectedEnvironmentError) do
+ ActiveRecord::Tasks::DatabaseTasks.check_protected_environments!
+ end
+ end
+ ensure
+ ActiveRecord::Base.protected_environments = protected_environments
+ end
+
+ def test_raises_an_error_if_no_migrations_have_been_made
+ ActiveRecord::InternalMetadata.stub(:table_exists?, false) do
+ assert_called_on_instance_of(
+ ActiveRecord::MigrationContext,
+ :current_version,
+ returns: 1
+ ) do
+ assert_raise(ActiveRecord::NoEnvironmentInSchemaError) do
+ ActiveRecord::Tasks::DatabaseTasks.check_protected_environments!
+ end
+ end
+ end
+ end
+ end
+
class DatabaseTasksRegisterTask < ActiveRecord::TestCase
def test_register_task
klazz = Class.new do
@@ -26,16 +114,17 @@ module ActiveRecord
end
instance = klazz.new
- klazz.stubs(:new).returns instance
- instance.expects(:structure_dump).with("awesome-file.sql")
-
- ActiveRecord::Tasks::DatabaseTasks.register_task(/foo/, klazz)
- ActiveRecord::Tasks::DatabaseTasks.structure_dump({'adapter' => :foo}, "awesome-file.sql")
+ klazz.stub(:new, instance) do
+ assert_called_with(instance, :structure_dump, ["awesome-file.sql", nil]) do
+ ActiveRecord::Tasks::DatabaseTasks.register_task(/foo/, klazz)
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump({ "adapter" => :foo }, "awesome-file.sql")
+ end
+ end
end
def test_unregistered_task
assert_raise(ActiveRecord::Tasks::DatabaseNotSupported) do
- ActiveRecord::Tasks::DatabaseTasks.structure_dump({'adapter' => :bar}, "awesome-file.sql")
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump({ "adapter" => :bar }, "awesome-file.sql")
end
end
end
@@ -45,121 +134,324 @@ module ActiveRecord
ADAPTERS_TASKS.each do |k, v|
define_method("test_#{k}_create") do
- eval("@#{v}").expects(:create)
- ActiveRecord::Tasks::DatabaseTasks.create 'adapter' => k
+ with_stubbed_new do
+ assert_called(eval("@#{v}"), :create) do
+ ActiveRecord::Tasks::DatabaseTasks.create "adapter" => k
+ end
+ end
end
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
+ ActiveRecord::Base.clear_cache!
+ FileUtils.rm_rf(path)
+ end
+ end
+
class DatabaseTasksCreateAllTest < ActiveRecord::TestCase
def setup
- @configurations = {'development' => {'database' => 'my-db'}}
+ @configurations = { "development" => { "database" => "my-db" } }
- ActiveRecord::Base.stubs(:configurations).returns(@configurations)
+ $stdout, @original_stdout = StringIO.new, $stdout
+ $stderr, @original_stderr = StringIO.new, $stderr
end
- def test_ignores_configurations_without_databases
- @configurations['development'].merge!('database' => nil)
+ def teardown
+ $stdout, $stderr = @original_stdout, @original_stderr
+ end
- ActiveRecord::Tasks::DatabaseTasks.expects(:create).never
+ def test_ignores_configurations_without_databases
+ @configurations["development"]["database"] = nil
- ActiveRecord::Tasks::DatabaseTasks.create_all
+ with_stubbed_configurations_establish_connection do
+ assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :create) do
+ ActiveRecord::Tasks::DatabaseTasks.create_all
+ end
+ end
end
def test_ignores_remote_databases
- @configurations['development'].merge!('host' => 'my.server.tld')
- $stderr.stubs(:puts).returns(nil)
+ @configurations["development"]["host"] = "my.server.tld"
- ActiveRecord::Tasks::DatabaseTasks.expects(:create).never
-
- ActiveRecord::Tasks::DatabaseTasks.create_all
+ with_stubbed_configurations_establish_connection do
+ assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :create) do
+ ActiveRecord::Tasks::DatabaseTasks.create_all
+ end
+ end
end
def test_warning_for_remote_databases
- @configurations['development'].merge!('host' => 'my.server.tld')
+ @configurations["development"]["host"] = "my.server.tld"
- $stderr.expects(:puts).with('This task only modifies local databases. my-db is on a remote host.')
+ with_stubbed_configurations_establish_connection do
+ ActiveRecord::Tasks::DatabaseTasks.create_all
- ActiveRecord::Tasks::DatabaseTasks.create_all
+ assert_match "This task only modifies local databases. my-db is on a remote host.",
+ $stderr.string
+ end
end
def test_creates_configurations_with_local_ip
- @configurations['development'].merge!('host' => '127.0.0.1')
-
- ActiveRecord::Tasks::DatabaseTasks.expects(:create)
+ @configurations["development"]["host"] = "127.0.0.1"
- ActiveRecord::Tasks::DatabaseTasks.create_all
+ with_stubbed_configurations_establish_connection do
+ assert_called(ActiveRecord::Tasks::DatabaseTasks, :create) do
+ ActiveRecord::Tasks::DatabaseTasks.create_all
+ end
+ end
end
def test_creates_configurations_with_local_host
- @configurations['development'].merge!('host' => 'localhost')
+ @configurations["development"]["host"] = "localhost"
- ActiveRecord::Tasks::DatabaseTasks.expects(:create)
-
- ActiveRecord::Tasks::DatabaseTasks.create_all
+ with_stubbed_configurations_establish_connection do
+ assert_called(ActiveRecord::Tasks::DatabaseTasks, :create) do
+ ActiveRecord::Tasks::DatabaseTasks.create_all
+ end
+ end
end
def test_creates_configurations_with_blank_hosts
- @configurations['development'].merge!('host' => nil)
+ @configurations["development"]["host"] = nil
- ActiveRecord::Tasks::DatabaseTasks.expects(:create)
-
- ActiveRecord::Tasks::DatabaseTasks.create_all
+ with_stubbed_configurations_establish_connection do
+ assert_called(ActiveRecord::Tasks::DatabaseTasks, :create) do
+ ActiveRecord::Tasks::DatabaseTasks.create_all
+ end
+ end
end
+
+ private
+ def with_stubbed_configurations_establish_connection
+ old_configurations = ActiveRecord::Base.configurations
+ ActiveRecord::Base.configurations = @configurations
+
+ # To refrain from connecting to a newly created empty DB in
+ # sqlite3_mem tests
+ ActiveRecord::Base.connection_handler.stub(:establish_connection, nil) do
+ yield
+ end
+ ensure
+ ActiveRecord::Base.configurations = old_configurations
+ end
end
class DatabaseTasksCreateCurrentTest < ActiveRecord::TestCase
def setup
@configurations = {
- 'development' => {'database' => 'dev-db'},
- 'test' => {'database' => 'test-db'},
- 'production' => {'database' => 'prod-db'}
+ "development" => { "database" => "dev-db" },
+ "test" => { "database" => "test-db" },
+ "production" => { "url" => "abstract://prod-db-host/prod-db" }
}
-
- ActiveRecord::Base.stubs(:configurations).returns(@configurations)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
end
def test_creates_current_environment_database
- ActiveRecord::Tasks::DatabaseTasks.expects(:create).
- with('database' => 'prod-db')
+ with_stubbed_configurations_establish_connection do
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :create,
+ ["database" => "test-db"],
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create_current(
+ ActiveSupport::StringInquirer.new("test")
+ )
+ end
+ end
+ end
- ActiveRecord::Tasks::DatabaseTasks.create_current(
- ActiveSupport::StringInquirer.new('production')
- )
+ def test_creates_current_environment_database_with_url
+ with_stubbed_configurations_establish_connection do
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :create,
+ ["adapter" => "abstract", "database" => "prod-db", "host" => "prod-db-host"],
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create_current(
+ ActiveSupport::StringInquirer.new("production")
+ )
+ end
+ end
end
def test_creates_test_and_development_databases_when_env_was_not_specified
- ActiveRecord::Tasks::DatabaseTasks.expects(:create).
- with('database' => 'dev-db')
- ActiveRecord::Tasks::DatabaseTasks.expects(:create).
- with('database' => 'test-db')
- ENV.expects(:[]).with('RAILS_ENV').returns(nil)
-
- ActiveRecord::Tasks::DatabaseTasks.create_current(
- ActiveSupport::StringInquirer.new('development')
- )
+ with_stubbed_configurations_establish_connection do
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :create,
+ [
+ ["database" => "dev-db"],
+ ["database" => "test-db"]
+ ],
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create_current(
+ ActiveSupport::StringInquirer.new("development")
+ )
+ end
+ end
end
- def test_creates_only_development_database_when_rails_env_is_development
- ActiveRecord::Tasks::DatabaseTasks.expects(:create).
- with('database' => 'dev-db')
- ENV.expects(:[]).with('RAILS_ENV').returns('development')
+ def test_creates_test_and_development_databases_when_rails_env_is_development
+ old_env = ENV["RAILS_ENV"]
+ ENV["RAILS_ENV"] = "development"
+
+ with_stubbed_configurations_establish_connection do
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :create,
+ [
+ ["database" => "dev-db"],
+ ["database" => "test-db"]
+ ],
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create_current(
+ ActiveSupport::StringInquirer.new("development")
+ )
+ end
+ end
+ ensure
+ ENV["RAILS_ENV"] = old_env
+ end
- ActiveRecord::Tasks::DatabaseTasks.create_current(
- ActiveSupport::StringInquirer.new('development')
- )
+ def test_establishes_connection_for_the_given_environments
+ ActiveRecord::Tasks::DatabaseTasks.stub(:create, nil) do
+ assert_called_with(ActiveRecord::Base, :establish_connection, [:development]) do
+ ActiveRecord::Tasks::DatabaseTasks.create_current(
+ ActiveSupport::StringInquirer.new("development")
+ )
+ end
+ end
end
- def test_establishes_connection_for_the_given_environment
- ActiveRecord::Tasks::DatabaseTasks.stubs(:create).returns true
+ private
+ def with_stubbed_configurations_establish_connection
+ old_configurations = ActiveRecord::Base.configurations
+ ActiveRecord::Base.configurations = @configurations
- ActiveRecord::Base.expects(:establish_connection).with(:development)
+ ActiveRecord::Base.connection_handler.stub(:establish_connection, nil) do
+ yield
+ end
+ ensure
+ ActiveRecord::Base.configurations = old_configurations
+ end
+ end
- ActiveRecord::Tasks::DatabaseTasks.create_current(
- ActiveSupport::StringInquirer.new('development')
- )
+ class DatabaseTasksCreateCurrentThreeTierTest < ActiveRecord::TestCase
+ def setup
+ @configurations = {
+ "development" => { "primary" => { "database" => "dev-db" }, "secondary" => { "database" => "secondary-dev-db" } },
+ "test" => { "primary" => { "database" => "test-db" }, "secondary" => { "database" => "secondary-test-db" } },
+ "production" => { "primary" => { "url" => "abstract://prod-db-host/prod-db" }, "secondary" => { "url" => "abstract://secondary-prod-db-host/secondary-prod-db" } }
+ }
+ end
+
+ def test_creates_current_environment_database
+ with_stubbed_configurations_establish_connection do
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :create,
+ [
+ ["database" => "test-db"],
+ ["database" => "secondary-test-db"]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create_current(
+ ActiveSupport::StringInquirer.new("test")
+ )
+ end
+ end
+ end
+
+ def test_creates_current_environment_database_with_url
+ with_stubbed_configurations_establish_connection do
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :create,
+ [
+ ["adapter" => "abstract", "database" => "prod-db", "host" => "prod-db-host"],
+ ["adapter" => "abstract", "database" => "secondary-prod-db", "host" => "secondary-prod-db-host"]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create_current(
+ ActiveSupport::StringInquirer.new("production")
+ )
+ end
+ end
end
+
+ def test_creates_test_and_development_databases_when_env_was_not_specified
+ with_stubbed_configurations_establish_connection do
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :create,
+ [
+ ["database" => "dev-db"],
+ ["database" => "secondary-dev-db"],
+ ["database" => "test-db"],
+ ["database" => "secondary-test-db"]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create_current(
+ ActiveSupport::StringInquirer.new("development")
+ )
+ end
+ end
+ end
+
+ def test_creates_test_and_development_databases_when_rails_env_is_development
+ old_env = ENV["RAILS_ENV"]
+ ENV["RAILS_ENV"] = "development"
+
+ with_stubbed_configurations_establish_connection do
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :create,
+ [
+ ["database" => "dev-db"],
+ ["database" => "secondary-dev-db"],
+ ["database" => "test-db"],
+ ["database" => "secondary-test-db"]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create_current(
+ ActiveSupport::StringInquirer.new("development")
+ )
+ end
+ end
+ ensure
+ ENV["RAILS_ENV"] = old_env
+ end
+
+ def test_establishes_connection_for_the_given_environments_config
+ ActiveRecord::Tasks::DatabaseTasks.stub(:create, nil) do
+ assert_called_with(
+ ActiveRecord::Base,
+ :establish_connection,
+ [:development]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create_current(
+ ActiveSupport::StringInquirer.new("development")
+ )
+ end
+ end
+ end
+
+ private
+ def with_stubbed_configurations_establish_connection
+ old_configurations = ActiveRecord::Base.configurations
+ ActiveRecord::Base.configurations = @configurations
+
+ ActiveRecord::Base.connection_handler.stub(:establish_connection, nil) do
+ yield
+ end
+ ensure
+ ActiveRecord::Base.configurations = old_configurations
+ end
end
class DatabaseTasksDropTest < ActiveRecord::TestCase
@@ -167,123 +459,431 @@ module ActiveRecord
ADAPTERS_TASKS.each do |k, v|
define_method("test_#{k}_drop") do
- eval("@#{v}").expects(:drop)
- ActiveRecord::Tasks::DatabaseTasks.drop 'adapter' => k
+ with_stubbed_new do
+ assert_called(eval("@#{v}"), :drop) do
+ ActiveRecord::Tasks::DatabaseTasks.drop "adapter" => k
+ end
+ end
end
end
end
class DatabaseTasksDropAllTest < ActiveRecord::TestCase
def setup
- @configurations = {:development => {'database' => 'my-db'}}
+ @configurations = { development: { "database" => "my-db" } }
- ActiveRecord::Base.stubs(:configurations).returns(@configurations)
+ $stdout, @original_stdout = StringIO.new, $stdout
+ $stderr, @original_stderr = StringIO.new, $stderr
end
- def test_ignores_configurations_without_databases
- @configurations[:development].merge!('database' => nil)
+ def teardown
+ $stdout, $stderr = @original_stdout, @original_stderr
+ end
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop).never
+ def test_ignores_configurations_without_databases
+ @configurations[:development]["database"] = nil
- ActiveRecord::Tasks::DatabaseTasks.drop_all
+ with_stubbed_configurations do
+ assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_all
+ end
+ end
end
def test_ignores_remote_databases
- @configurations[:development].merge!('host' => 'my.server.tld')
- $stderr.stubs(:puts).returns(nil)
+ @configurations[:development]["host"] = "my.server.tld"
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop).never
-
- ActiveRecord::Tasks::DatabaseTasks.drop_all
+ with_stubbed_configurations do
+ assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_all
+ end
+ end
end
def test_warning_for_remote_databases
- @configurations[:development].merge!('host' => 'my.server.tld')
+ @configurations[:development]["host"] = "my.server.tld"
- $stderr.expects(:puts).with('This task only modifies local databases. my-db is on a remote host.')
+ with_stubbed_configurations do
+ ActiveRecord::Tasks::DatabaseTasks.drop_all
- ActiveRecord::Tasks::DatabaseTasks.drop_all
+ assert_match "This task only modifies local databases. my-db is on a remote host.",
+ $stderr.string
+ end
end
def test_drops_configurations_with_local_ip
- @configurations[:development].merge!('host' => '127.0.0.1')
-
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop)
+ @configurations[:development]["host"] = "127.0.0.1"
- ActiveRecord::Tasks::DatabaseTasks.drop_all
+ with_stubbed_configurations do
+ assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_all
+ end
+ end
end
def test_drops_configurations_with_local_host
- @configurations[:development].merge!('host' => 'localhost')
+ @configurations[:development]["host"] = "localhost"
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop)
-
- ActiveRecord::Tasks::DatabaseTasks.drop_all
+ with_stubbed_configurations do
+ assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_all
+ end
+ end
end
def test_drops_configurations_with_blank_hosts
- @configurations[:development].merge!('host' => nil)
+ @configurations[:development]["host"] = nil
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop)
-
- ActiveRecord::Tasks::DatabaseTasks.drop_all
+ with_stubbed_configurations do
+ assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_all
+ end
+ end
end
+
+ private
+ def with_stubbed_configurations
+ old_configurations = ActiveRecord::Base.configurations
+ ActiveRecord::Base.configurations = @configurations
+
+ yield
+ ensure
+ ActiveRecord::Base.configurations = old_configurations
+ end
end
class DatabaseTasksDropCurrentTest < ActiveRecord::TestCase
def setup
@configurations = {
- 'development' => {'database' => 'dev-db'},
- 'test' => {'database' => 'test-db'},
- 'production' => {'database' => 'prod-db'}
+ "development" => { "database" => "dev-db" },
+ "test" => { "database" => "test-db" },
+ "production" => { "url" => "abstract://prod-db-host/prod-db" }
}
+ end
+
+ def test_drops_current_environment_database
+ with_stubbed_configurations do
+ assert_called_with(ActiveRecord::Tasks::DatabaseTasks, :drop,
+ ["database" => "test-db"]) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_current(
+ ActiveSupport::StringInquirer.new("test")
+ )
+ end
+ end
+ end
+
+ def test_drops_current_environment_database_with_url
+ with_stubbed_configurations do
+ assert_called_with(ActiveRecord::Tasks::DatabaseTasks, :drop,
+ ["adapter" => "abstract", "database" => "prod-db", "host" => "prod-db-host"]) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_current(
+ ActiveSupport::StringInquirer.new("production")
+ )
+ end
+ end
+ end
+
+ def test_drops_test_and_development_databases_when_env_was_not_specified
+ with_stubbed_configurations do
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :drop,
+ [
+ ["database" => "dev-db"],
+ ["database" => "test-db"]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_current(
+ ActiveSupport::StringInquirer.new("development")
+ )
+ end
+ end
+ end
+
+ def test_drops_testand_development_databases_when_rails_env_is_development
+ old_env = ENV["RAILS_ENV"]
+ ENV["RAILS_ENV"] = "development"
+
+ with_stubbed_configurations do
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :drop,
+ [
+ ["database" => "dev-db"],
+ ["database" => "test-db"]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_current(
+ ActiveSupport::StringInquirer.new("development")
+ )
+ end
+ end
+ ensure
+ ENV["RAILS_ENV"] = old_env
+ end
+
+ private
+ def with_stubbed_configurations
+ old_configurations = ActiveRecord::Base.configurations
+ ActiveRecord::Base.configurations = @configurations
+
+ yield
+ ensure
+ ActiveRecord::Base.configurations = old_configurations
+ end
+ end
- ActiveRecord::Base.stubs(:configurations).returns(@configurations)
+ class DatabaseTasksDropCurrentThreeTierTest < ActiveRecord::TestCase
+ def setup
+ @configurations = {
+ "development" => { "primary" => { "database" => "dev-db" }, "secondary" => { "database" => "secondary-dev-db" } },
+ "test" => { "primary" => { "database" => "test-db" }, "secondary" => { "database" => "secondary-test-db" } },
+ "production" => { "primary" => { "url" => "abstract://prod-db-host/prod-db" }, "secondary" => { "url" => "abstract://secondary-prod-db-host/secondary-prod-db" } }
+ }
end
def test_drops_current_environment_database
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
- with('database' => 'prod-db')
+ with_stubbed_configurations do
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :drop,
+ [
+ ["database" => "test-db"],
+ ["database" => "secondary-test-db"]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_current(
+ ActiveSupport::StringInquirer.new("test")
+ )
+ end
+ end
+ end
- ActiveRecord::Tasks::DatabaseTasks.drop_current(
- ActiveSupport::StringInquirer.new('production')
- )
+ def test_drops_current_environment_database_with_url
+ with_stubbed_configurations do
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :drop,
+ [
+ ["adapter" => "abstract", "database" => "prod-db", "host" => "prod-db-host"],
+ ["adapter" => "abstract", "database" => "secondary-prod-db", "host" => "secondary-prod-db-host"]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_current(
+ ActiveSupport::StringInquirer.new("production")
+ )
+ end
+ end
end
def test_drops_test_and_development_databases_when_env_was_not_specified
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
- with('database' => 'dev-db')
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
- with('database' => 'test-db')
- ENV.expects(:[]).with('RAILS_ENV').returns(nil)
-
- ActiveRecord::Tasks::DatabaseTasks.drop_current(
- ActiveSupport::StringInquirer.new('development')
- )
+ with_stubbed_configurations do
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :drop,
+ [
+ ["database" => "dev-db"],
+ ["database" => "secondary-dev-db"],
+ ["database" => "test-db"],
+ ["database" => "secondary-test-db"]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_current(
+ ActiveSupport::StringInquirer.new("development")
+ )
+ end
+ end
end
- def test_drops_only_development_database_when_rails_env_is_development
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
- with('database' => 'dev-db')
- ENV.expects(:[]).with('RAILS_ENV').returns('development')
+ def test_drops_testand_development_databases_when_rails_env_is_development
+ old_env = ENV["RAILS_ENV"]
+ ENV["RAILS_ENV"] = "development"
+
+ with_stubbed_configurations do
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :drop,
+ [
+ ["database" => "dev-db"],
+ ["database" => "secondary-dev-db"],
+ ["database" => "test-db"],
+ ["database" => "secondary-test-db"]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_current(
+ ActiveSupport::StringInquirer.new("development")
+ )
+ end
+ end
+ ensure
+ ENV["RAILS_ENV"] = old_env
+ end
- ActiveRecord::Tasks::DatabaseTasks.drop_current(
- ActiveSupport::StringInquirer.new('development')
- )
+ private
+ def with_stubbed_configurations
+ old_configurations = ActiveRecord::Base.configurations
+ ActiveRecord::Base.configurations = @configurations
+
+ yield
+ ensure
+ ActiveRecord::Base.configurations = old_configurations
+ end
+ end
+
+ if current_adapter?(:SQLite3Adapter) && !in_memory_db?
+ class DatabaseTasksMigrationTestCase < ActiveRecord::TestCase
+ self.use_transactional_tests = false
+
+ # Use a memory db here to avoid having to rollback at the end
+ setup do
+ migrations_path = MIGRATIONS_ROOT + "/valid"
+ file = ActiveRecord::Base.connection.raw_connection.filename
+ @conn = ActiveRecord::Base.establish_connection adapter: "sqlite3",
+ database: ":memory:", migrations_paths: migrations_path
+ source_db = SQLite3::Database.new file
+ dest_db = ActiveRecord::Base.connection.raw_connection
+ backup = SQLite3::Backup.new(dest_db, "main", source_db, "main")
+ backup.step(-1)
+ backup.finish
+ end
+
+ teardown do
+ @conn.release_connection if @conn
+ ActiveRecord::Base.establish_connection :arunit
+ end
+ end
+
+ class DatabaseTasksMigrateTest < DatabaseTasksMigrationTestCase
+ def test_migrate_set_and_unset_verbose_and_version_env_vars
+ verbose, version = ENV["VERBOSE"], ENV["VERSION"]
+ ENV["VERSION"] = "2"
+ ENV["VERBOSE"] = "false"
+
+ # run down migration because it was already run on copied db
+ assert_empty capture_migration_output
+
+ ENV.delete("VERSION")
+ ENV.delete("VERBOSE")
+
+ # re-run up migration
+ assert_includes capture_migration_output, "migrating"
+ ensure
+ ENV["VERBOSE"], ENV["VERSION"] = verbose, version
+ end
+
+ def test_migrate_set_and_unset_empty_values_for_verbose_and_version_env_vars
+ verbose, version = ENV["VERBOSE"], ENV["VERSION"]
+
+ ENV["VERSION"] = "2"
+ ENV["VERBOSE"] = "false"
+
+ # run down migration because it was already run on copied db
+ assert_empty capture_migration_output
+
+ ENV["VERBOSE"] = ""
+ ENV["VERSION"] = ""
+
+ # re-run up migration
+ assert_includes capture_migration_output, "migrating"
+ ensure
+ ENV["VERBOSE"], ENV["VERSION"] = verbose, version
+ end
+
+ def test_migrate_set_and_unset_nonsense_values_for_verbose_and_version_env_vars
+ verbose, version = ENV["VERBOSE"], ENV["VERSION"]
+
+ # run down migration because it was already run on copied db
+ ENV["VERSION"] = "2"
+ ENV["VERBOSE"] = "false"
+
+ assert_empty capture_migration_output
+
+ ENV["VERBOSE"] = "yes"
+ ENV["VERSION"] = "2"
+
+ # run no migration because 2 was already run
+ assert_empty capture_migration_output
+ ensure
+ ENV["VERBOSE"], ENV["VERSION"] = verbose, version
+ end
+
+ private
+ def capture_migration_output
+ capture(:stdout) do
+ ActiveRecord::Tasks::DatabaseTasks.migrate
+ end
+ end
+ end
+
+ class DatabaseTasksMigrateStatusTest < DatabaseTasksMigrationTestCase
+ def test_migrate_status_table
+ ActiveRecord::SchemaMigration.create_table
+ output = capture_migration_status
+ assert_match(/database: :memory:/, output)
+ assert_match(/down 001 Valid people have last names/, output)
+ assert_match(/down 002 We need reminders/, output)
+ assert_match(/down 003 Innocent jointable/, output)
+ ActiveRecord::SchemaMigration.drop_table
+ end
+
+ private
+
+ def capture_migration_status
+ capture(:stdout) do
+ ActiveRecord::Tasks::DatabaseTasks.migrate_status
+ end
+ end
end
end
- class DatabaseTasksMigrateTest < ActiveRecord::TestCase
- def test_migrate_receives_correct_env_vars
- verbose, version = ENV['VERBOSE'], ENV['VERSION']
+ class DatabaseTasksMigrateErrorTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
+
+ def test_migrate_raise_error_on_invalid_version_format
+ version = ENV["VERSION"]
+
+ ENV["VERSION"] = "unknown"
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate }
+ assert_match(/Invalid format of target version/, e.message)
+
+ ENV["VERSION"] = "0.1.11"
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate }
+ assert_match(/Invalid format of target version/, e.message)
+
+ ENV["VERSION"] = "1.1.11"
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate }
+ assert_match(/Invalid format of target version/, e.message)
- ENV['VERBOSE'] = 'false'
- ENV['VERSION'] = '4'
+ ENV["VERSION"] = "0 "
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate }
+ assert_match(/Invalid format of target version/, e.message)
- ActiveRecord::Migrator.expects(:migrate).with(ActiveRecord::Migrator.migrations_paths, 4)
- ActiveRecord::Tasks::DatabaseTasks.migrate
+ ENV["VERSION"] = "1."
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate }
+ assert_match(/Invalid format of target version/, e.message)
+
+ ENV["VERSION"] = "1_"
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate }
+ assert_match(/Invalid format of target version/, e.message)
+
+ ENV["VERSION"] = "1_name"
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate }
+ assert_match(/Invalid format of target version/, e.message)
ensure
- ENV['VERBOSE'], ENV['VERSION'] = verbose, version
+ ENV["VERSION"] = version
+ end
+
+ def test_migrate_raise_error_on_failed_check_target_version
+ ActiveRecord::Tasks::DatabaseTasks.stub(:check_target_version, -> { raise "foo" }) do
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate }
+ assert_equal "foo", e.message
+ end
+ end
+
+ def test_migrate_clears_schema_cache_afterward
+ assert_called(ActiveRecord::Base, :clear_cache!) do
+ ActiveRecord::Tasks::DatabaseTasks.migrate
+ end
end
end
@@ -292,38 +892,55 @@ module ActiveRecord
ADAPTERS_TASKS.each do |k, v|
define_method("test_#{k}_purge") do
- eval("@#{v}").expects(:purge)
- ActiveRecord::Tasks::DatabaseTasks.purge 'adapter' => k
+ with_stubbed_new do
+ assert_called(eval("@#{v}"), :purge) do
+ ActiveRecord::Tasks::DatabaseTasks.purge "adapter" => k
+ end
+ end
end
end
end
class DatabaseTasksPurgeCurrentTest < ActiveRecord::TestCase
def test_purges_current_environment_database
+ old_configurations = ActiveRecord::Base.configurations
configurations = {
- 'development' => {'database' => 'dev-db'},
- 'test' => {'database' => 'test-db'},
- 'production' => {'database' => 'prod-db'}
+ "development" => { "database" => "dev-db" },
+ "test" => { "database" => "test-db" },
+ "production" => { "database" => "prod-db" }
}
- ActiveRecord::Base.stubs(:configurations).returns(configurations)
- ActiveRecord::Tasks::DatabaseTasks.expects(:purge).
- with('database' => 'prod-db')
- ActiveRecord::Base.expects(:establish_connection).with(:production)
+ ActiveRecord::Base.configurations = configurations
- ActiveRecord::Tasks::DatabaseTasks.purge_current('production')
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :purge,
+ ["database" => "prod-db"]
+ ) do
+ assert_called_with(ActiveRecord::Base, :establish_connection, [:production]) do
+ ActiveRecord::Tasks::DatabaseTasks.purge_current("production")
+ end
+ end
+ ensure
+ ActiveRecord::Base.configurations = old_configurations
end
end
class DatabaseTasksPurgeAllTest < ActiveRecord::TestCase
def test_purge_all_local_configurations
- configurations = {:development => {'database' => 'my-db'}}
- ActiveRecord::Base.stubs(:configurations).returns(configurations)
-
- ActiveRecord::Tasks::DatabaseTasks.expects(:purge).
- with('database' => 'my-db')
-
- ActiveRecord::Tasks::DatabaseTasks.purge_all
+ old_configurations = ActiveRecord::Base.configurations
+ configurations = { development: { "database" => "my-db" } }
+ ActiveRecord::Base.configurations = configurations
+
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :purge,
+ ["database" => "my-db"]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.purge_all
+ end
+ ensure
+ ActiveRecord::Base.configurations = old_configurations
end
end
@@ -332,8 +949,11 @@ module ActiveRecord
ADAPTERS_TASKS.each do |k, v|
define_method("test_#{k}_charset") do
- eval("@#{v}").expects(:charset)
- ActiveRecord::Tasks::DatabaseTasks.charset 'adapter' => k
+ with_stubbed_new do
+ assert_called(eval("@#{v}"), :charset) do
+ ActiveRecord::Tasks::DatabaseTasks.charset "adapter" => k
+ end
+ end
end
end
end
@@ -343,19 +963,130 @@ module ActiveRecord
ADAPTERS_TASKS.each do |k, v|
define_method("test_#{k}_collation") do
- eval("@#{v}").expects(:collation)
- ActiveRecord::Tasks::DatabaseTasks.collation 'adapter' => k
+ with_stubbed_new do
+ assert_called(eval("@#{v}"), :collation) do
+ ActiveRecord::Tasks::DatabaseTasks.collation "adapter" => k
+ end
+ end
end
end
end
+ class DatabaseTaskTargetVersionTest < ActiveRecord::TestCase
+ def test_target_version_returns_nil_if_version_does_not_exist
+ version = ENV.delete("VERSION")
+ assert_nil ActiveRecord::Tasks::DatabaseTasks.target_version
+ ensure
+ ENV["VERSION"] = version
+ end
+
+ def test_target_version_returns_nil_if_version_is_empty
+ version = ENV["VERSION"]
+
+ ENV["VERSION"] = ""
+ assert_nil ActiveRecord::Tasks::DatabaseTasks.target_version
+ ensure
+ ENV["VERSION"] = version
+ end
+
+ def test_target_version_returns_converted_to_integer_env_version_if_version_exists
+ version = ENV["VERSION"]
+
+ ENV["VERSION"] = "0"
+ assert_equal ENV["VERSION"].to_i, ActiveRecord::Tasks::DatabaseTasks.target_version
+
+ ENV["VERSION"] = "42"
+ assert_equal ENV["VERSION"].to_i, ActiveRecord::Tasks::DatabaseTasks.target_version
+
+ ENV["VERSION"] = "042"
+ assert_equal ENV["VERSION"].to_i, ActiveRecord::Tasks::DatabaseTasks.target_version
+ ensure
+ ENV["VERSION"] = version
+ end
+ end
+
+ class DatabaseTaskCheckTargetVersionTest < ActiveRecord::TestCase
+ def test_check_target_version_does_not_raise_error_on_empty_version
+ version = ENV["VERSION"]
+ ENV["VERSION"] = ""
+ assert_nothing_raised { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+ ensure
+ ENV["VERSION"] = version
+ end
+
+ def test_check_target_version_does_not_raise_error_if_version_is_not_setted
+ version = ENV.delete("VERSION")
+ assert_nothing_raised { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+ ensure
+ ENV["VERSION"] = version
+ end
+
+ def test_check_target_version_raises_error_on_invalid_version_format
+ version = ENV["VERSION"]
+
+ ENV["VERSION"] = "unknown"
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+ assert_match(/Invalid format of target version/, e.message)
+
+ ENV["VERSION"] = "0.1.11"
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+ assert_match(/Invalid format of target version/, e.message)
+
+ ENV["VERSION"] = "1.1.11"
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+ assert_match(/Invalid format of target version/, e.message)
+
+ ENV["VERSION"] = "0 "
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+ assert_match(/Invalid format of target version/, e.message)
+
+ ENV["VERSION"] = "1."
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+ assert_match(/Invalid format of target version/, e.message)
+
+ ENV["VERSION"] = "1_"
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+ assert_match(/Invalid format of target version/, e.message)
+
+ ENV["VERSION"] = "1_name"
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+ assert_match(/Invalid format of target version/, e.message)
+ ensure
+ ENV["VERSION"] = version
+ end
+
+ def test_check_target_version_does_not_raise_error_on_valid_version_format
+ version = ENV["VERSION"]
+
+ ENV["VERSION"] = "0"
+ assert_nothing_raised { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+
+ ENV["VERSION"] = "1"
+ assert_nothing_raised { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+
+ ENV["VERSION"] = "001"
+ assert_nothing_raised { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+
+ ENV["VERSION"] = "001_name.rb"
+ assert_nothing_raised { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+ ensure
+ ENV["VERSION"] = version
+ end
+ end
+
class DatabaseTasksStructureDumpTest < ActiveRecord::TestCase
include DatabaseTasksSetupper
ADAPTERS_TASKS.each do |k, v|
define_method("test_#{k}_structure_dump") do
- eval("@#{v}").expects(:structure_dump).with("awesome-file.sql")
- ActiveRecord::Tasks::DatabaseTasks.structure_dump({'adapter' => k}, "awesome-file.sql")
+ with_stubbed_new do
+ assert_called_with(
+ eval("@#{v}"), :structure_dump,
+ ["awesome-file.sql", nil]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump({ "adapter" => k }, "awesome-file.sql")
+ end
+ end
end
end
end
@@ -365,31 +1096,41 @@ module ActiveRecord
ADAPTERS_TASKS.each do |k, v|
define_method("test_#{k}_structure_load") do
- eval("@#{v}").expects(:structure_load).with("awesome-file.sql")
- ActiveRecord::Tasks::DatabaseTasks.structure_load({'adapter' => k}, "awesome-file.sql")
+ with_stubbed_new do
+ assert_called_with(
+ eval("@#{v}"),
+ :structure_load,
+ ["awesome-file.sql", nil]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_load({ "adapter" => k }, "awesome-file.sql")
+ end
+ end
end
end
end
class DatabaseTasksCheckSchemaFileTest < ActiveRecord::TestCase
def test_check_schema_file
- Kernel.expects(:abort).with(regexp_matches(/awesome-file.sql/))
- ActiveRecord::Tasks::DatabaseTasks.check_schema_file("awesome-file.sql")
+ assert_called_with(Kernel, :abort, [/awesome-file.sql/]) do
+ ActiveRecord::Tasks::DatabaseTasks.check_schema_file("awesome-file.sql")
+ end
end
end
class DatabaseTasksCheckSchemaFileDefaultsTest < ActiveRecord::TestCase
def test_check_schema_file_defaults
- ActiveRecord::Tasks::DatabaseTasks.stubs(:db_dir).returns('/tmp')
- assert_equal '/tmp/schema.rb', ActiveRecord::Tasks::DatabaseTasks.schema_file
+ ActiveRecord::Tasks::DatabaseTasks.stub(:db_dir, "/tmp") do
+ assert_equal "/tmp/schema.rb", ActiveRecord::Tasks::DatabaseTasks.schema_file
+ end
end
end
class DatabaseTasksCheckSchemaFileSpecifiedFormatsTest < ActiveRecord::TestCase
- {ruby: 'schema.rb', sql: 'structure.sql'}.each_pair do |fmt, filename|
+ { ruby: "schema.rb", sql: "structure.sql" }.each_pair do |fmt, filename|
define_method("test_check_schema_file_for_#{fmt}_format") do
- ActiveRecord::Tasks::DatabaseTasks.stubs(:db_dir).returns('/tmp')
- assert_equal "/tmp/#{filename}", ActiveRecord::Tasks::DatabaseTasks.schema_file(fmt)
+ ActiveRecord::Tasks::DatabaseTasks.stub(:db_dir, "/tmp") do
+ assert_equal "/tmp/#{filename}", ActiveRecord::Tasks::DatabaseTasks.schema_file(fmt)
+ end
end
end
end
diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb
index d0deb4c273..552e623fd4 100644
--- a/activerecord/test/cases/tasks/mysql_rake_test.rb
+++ b/activerecord/test/cases/tasks/mysql_rake_test.rb
@@ -1,320 +1,409 @@
-require 'cases/helper'
-
-if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
-module ActiveRecord
- class MysqlDBCreateTest < ActiveRecord::TestCase
- def setup
- @connection = stub(:create_database => true)
- @configuration = {
- 'adapter' => 'mysql',
- 'database' => 'my-app-db'
- }
-
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
- end
-
- def test_establishes_connection_without_database
- ActiveRecord::Base.expects(:establish_connection).
- with('adapter' => 'mysql', 'database' => nil)
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
-
- def test_creates_database_with_default_encoding_and_collation
- @connection.expects(:create_database).
- with('my-app-db', charset: 'utf8', collation: 'utf8_unicode_ci')
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
-
- def test_creates_database_with_given_encoding_and_default_collation
- @connection.expects(:create_database).
- with('my-app-db', charset: 'utf8', collation: 'utf8_unicode_ci')
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration.merge('encoding' => 'utf8')
- end
-
- def test_creates_database_with_given_encoding_and_no_collation
- @connection.expects(:create_database).
- with('my-app-db', charset: 'latin1')
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration.merge('encoding' => 'latin1')
- end
-
- def test_creates_database_with_given_collation_and_no_encoding
- @connection.expects(:create_database).
- with('my-app-db', collation: 'latin1_swedish_ci')
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration.merge('collation' => 'latin1_swedish_ci')
- end
+# frozen_string_literal: true
- def test_establishes_connection_to_database
- ActiveRecord::Base.expects(:establish_connection).with(@configuration)
+require "cases/helper"
+require "active_record/tasks/database_tasks"
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
-
- def test_create_when_database_exists_outputs_info_to_stderr
- $stderr.expects(:puts).with("my-app-db already exists").once
-
- ActiveRecord::Base.connection.stubs(:create_database).raises(
- ActiveRecord::StatementInvalid.new("Can't create database 'dev'; database exists:")
- )
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
- end
-
- if current_adapter?(:MysqlAdapter)
- class MysqlDBCreateAsRootTest < ActiveRecord::TestCase
+if current_adapter?(:Mysql2Adapter)
+ module ActiveRecord
+ class MysqlDBCreateTest < ActiveRecord::TestCase
def setup
- @connection = stub("Connection", create_database: true)
- @error = Mysql::Error.new "Invalid permissions"
+ @connection = Class.new { def create_database(*); end }.new
@configuration = {
- 'adapter' => 'mysql',
- 'database' => 'my-app-db',
- 'username' => 'pat',
- 'password' => 'wossname'
+ "adapter" => "mysql2",
+ "database" => "my-app-db"
}
-
- $stdin.stubs(:gets).returns("secret\n")
- $stdout.stubs(:print).returns(nil)
- @error.stubs(:errno).returns(1045)
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).
- raises(@error).
- then.returns(true)
+ $stdout, @original_stdout = StringIO.new, $stdout
+ $stderr, @original_stderr = StringIO.new, $stderr
end
- if defined?(::Mysql)
- def test_root_password_is_requested
- assert_permissions_granted_for "pat"
- $stdin.expects(:gets).returns("secret\n")
+ def teardown
+ $stdout, $stderr = @original_stdout, @original_stderr
+ end
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ def test_establishes_connection_without_database
+ ActiveRecord::Base.stub(:connection, @connection) do
+ assert_called_with(
+ ActiveRecord::Base,
+ :establish_connection,
+ [
+ [ "adapter" => "mysql2", "database" => nil ],
+ [ "adapter" => "mysql2", "database" => "my-app-db" ],
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ end
end
end
- def test_connection_established_as_root
- assert_permissions_granted_for "pat"
- ActiveRecord::Base.expects(:establish_connection).with(
- 'adapter' => 'mysql',
- 'database' => nil,
- 'username' => 'root',
- 'password' => 'secret'
- )
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ def test_creates_database_with_no_default_options
+ with_stubbed_connection_establish_connection do
+ assert_called_with(@connection, :create_database, ["my-app-db", {}]) do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ end
+ end
end
- def test_database_created_by_root
- assert_permissions_granted_for "pat"
- @connection.expects(:create_database).
- with('my-app-db', :charset => 'utf8', :collation => 'utf8_unicode_ci')
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ def test_creates_database_with_given_encoding
+ with_stubbed_connection_establish_connection do
+ assert_called_with(@connection, :create_database, ["my-app-db", charset: "latin1"]) do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration.merge("encoding" => "latin1")
+ end
+ end
end
- def test_grant_privileges_for_normal_user
- assert_permissions_granted_for "pat"
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ def test_creates_database_with_given_collation
+ with_stubbed_connection_establish_connection do
+ assert_called_with(
+ @connection,
+ :create_database,
+ ["my-app-db", collation: "latin1_swedish_ci"]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration.merge("collation" => "latin1_swedish_ci")
+ end
+ end
end
- def test_do_not_grant_privileges_for_root_user
- @configuration['username'] = 'root'
- @configuration['password'] = ''
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ def test_establishes_connection_to_database
+ ActiveRecord::Base.stub(:connection, @connection) do
+ assert_called_with(
+ ActiveRecord::Base,
+ :establish_connection,
+ [
+ ["adapter" => "mysql2", "database" => nil],
+ [@configuration]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ end
+ end
end
- def test_connection_established_as_normal_user
- assert_permissions_granted_for "pat"
- ActiveRecord::Base.expects(:establish_connection).returns do
- ActiveRecord::Base.expects(:establish_connection).with(
- 'adapter' => 'mysql',
- 'database' => 'my-app-db',
- 'username' => 'pat',
- 'password' => 'secret'
- )
+ def test_when_database_created_successfully_outputs_info_to_stdout
+ with_stubbed_connection_establish_connection do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
- raise @error
+ assert_equal "Created database 'my-app-db'\n", $stdout.string
end
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
end
- def test_sends_output_to_stderr_when_other_errors
- @error.stubs(:errno).returns(42)
+ def test_create_when_database_exists_outputs_info_to_stderr
+ with_stubbed_connection_establish_connection do
+ ActiveRecord::Base.connection.stub(
+ :create_database,
+ proc { raise ActiveRecord::Tasks::DatabaseAlreadyExists }
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
- $stderr.expects(:puts).at_least_once.returns(nil)
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ assert_equal "Database 'my-app-db' already exists\n", $stderr.string
+ end
+ end
end
private
- def assert_permissions_granted_for(db_user)
- db_name = @configuration['database']
- db_password = @configuration['password']
- @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON #{db_name}.* TO '#{db_user}'@'localhost' IDENTIFIED BY '#{db_password}' WITH GRANT OPTION;")
+
+ def with_stubbed_connection_establish_connection
+ ActiveRecord::Base.stub(:establish_connection, nil) do
+ ActiveRecord::Base.stub(:connection, @connection) do
+ yield
+ end
+ end
end
end
- end
- class MySQLDBDropTest < ActiveRecord::TestCase
- def setup
- @connection = stub(:drop_database => true)
- @configuration = {
- 'adapter' => 'mysql',
- 'database' => 'my-app-db'
- }
-
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
- end
+ class MysqlDBCreateWithInvalidPermissionsTest < ActiveRecord::TestCase
+ def setup
+ @error = Mysql2::Error.new("Invalid permissions")
+ @configuration = {
+ "adapter" => "mysql2",
+ "database" => "my-app-db",
+ "username" => "pat",
+ "password" => "wossname"
+ }
+ $stdout, @original_stdout = StringIO.new, $stdout
+ $stderr, @original_stderr = StringIO.new, $stderr
+ end
- def test_establishes_connection_to_mysql_database
- ActiveRecord::Base.expects(:establish_connection).with @configuration
+ def teardown
+ $stdout, $stderr = @original_stdout, @original_stderr
+ end
- ActiveRecord::Tasks::DatabaseTasks.drop @configuration
+ def test_raises_error
+ ActiveRecord::Base.stub(:establish_connection, -> * { raise @error }) do
+ assert_raises(Mysql2::Error, "Invalid permissions") do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ end
+ end
+ end
end
- def test_drops_database
- @connection.expects(:drop_database).with('my-app-db')
+ class MySQLDBDropTest < ActiveRecord::TestCase
+ def setup
+ @connection = Class.new { def drop_database(name); end }.new
+ @configuration = {
+ "adapter" => "mysql2",
+ "database" => "my-app-db"
+ }
+ $stdout, @original_stdout = StringIO.new, $stdout
+ $stderr, @original_stderr = StringIO.new, $stderr
+ end
- ActiveRecord::Tasks::DatabaseTasks.drop @configuration
- end
- end
+ def teardown
+ $stdout, $stderr = @original_stdout, @original_stderr
+ end
- class MySQLPurgeTest < ActiveRecord::TestCase
- def setup
- @connection = stub(:recreate_database => true)
- @configuration = {
- 'adapter' => 'mysql',
- 'database' => 'test-db'
- }
+ def test_establishes_connection_to_mysql_database
+ ActiveRecord::Base.stub(:connection, @connection) do
+ assert_called_with(
+ ActiveRecord::Base,
+ :establish_connection,
+ [@configuration]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.drop @configuration
+ end
+ end
+ end
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
- end
+ def test_drops_database
+ with_stubbed_connection_establish_connection do
+ assert_called_with(@connection, :drop_database, ["my-app-db"]) do
+ ActiveRecord::Tasks::DatabaseTasks.drop @configuration
+ end
+ end
+ end
- def test_establishes_connection_to_the_appropriate_database
- ActiveRecord::Base.expects(:establish_connection).with(@configuration)
+ def test_when_database_dropped_successfully_outputs_info_to_stdout
+ with_stubbed_connection_establish_connection do
+ ActiveRecord::Tasks::DatabaseTasks.drop @configuration
- ActiveRecord::Tasks::DatabaseTasks.purge @configuration
- end
+ assert_equal "Dropped database 'my-app-db'\n", $stdout.string
+ end
+ end
- def test_recreates_database_with_the_default_options
- @connection.expects(:recreate_database).
- with('test-db', charset: 'utf8', collation: 'utf8_unicode_ci')
+ private
- ActiveRecord::Tasks::DatabaseTasks.purge @configuration
+ def with_stubbed_connection_establish_connection
+ ActiveRecord::Base.stub(:establish_connection, nil) do
+ ActiveRecord::Base.stub(:connection, @connection) do
+ yield
+ end
+ end
+ end
end
- def test_recreates_database_with_the_given_options
- @connection.expects(:recreate_database).
- with('test-db', charset: 'latin', collation: 'latin1_swedish_ci')
+ class MySQLPurgeTest < ActiveRecord::TestCase
+ def setup
+ @connection = Class.new { def recreate_database(*); end }.new
+ @configuration = {
+ "adapter" => "mysql2",
+ "database" => "test-db"
+ }
+ end
- ActiveRecord::Tasks::DatabaseTasks.purge @configuration.merge(
- 'encoding' => 'latin', 'collation' => 'latin1_swedish_ci')
- end
- end
+ def test_establishes_connection_to_the_appropriate_database
+ ActiveRecord::Base.stub(:connection, @connection) do
+ assert_called_with(
+ ActiveRecord::Base,
+ :establish_connection,
+ [@configuration]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.purge @configuration
+ end
+ end
+ end
- class MysqlDBCharsetTest < ActiveRecord::TestCase
- def setup
- @connection = stub(:create_database => true)
- @configuration = {
- 'adapter' => 'mysql',
- 'database' => 'my-app-db'
- }
+ def test_recreates_database_with_no_default_options
+ with_stubbed_connection_establish_connection do
+ assert_called_with(@connection, :recreate_database, ["test-db", {}]) do
+ ActiveRecord::Tasks::DatabaseTasks.purge @configuration
+ end
+ end
+ end
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
- end
+ def test_recreates_database_with_the_given_options
+ with_stubbed_connection_establish_connection do
+ assert_called_with(
+ @connection,
+ :recreate_database,
+ ["test-db", charset: "latin", collation: "latin1_swedish_ci"]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.purge @configuration.merge(
+ "encoding" => "latin", "collation" => "latin1_swedish_ci")
+ end
+ end
+ end
+
+ private
- def test_db_retrieves_charset
- @connection.expects(:charset)
- ActiveRecord::Tasks::DatabaseTasks.charset @configuration
+ def with_stubbed_connection_establish_connection
+ ActiveRecord::Base.stub(:establish_connection, nil) do
+ ActiveRecord::Base.stub(:connection, @connection) do
+ yield
+ end
+ end
+ end
end
- end
- class MysqlDBCollationTest < ActiveRecord::TestCase
- def setup
- @connection = stub(:create_database => true)
- @configuration = {
- 'adapter' => 'mysql',
- 'database' => 'my-app-db'
- }
+ class MysqlDBCharsetTest < ActiveRecord::TestCase
+ def setup
+ @connection = Class.new { def charset; end }.new
+ @configuration = {
+ "adapter" => "mysql2",
+ "database" => "my-app-db"
+ }
+ end
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
+ def test_db_retrieves_charset
+ ActiveRecord::Base.stub(:connection, @connection) do
+ assert_called(@connection, :charset) do
+ ActiveRecord::Tasks::DatabaseTasks.charset @configuration
+ end
+ end
+ end
end
- def test_db_retrieves_collation
- @connection.expects(:collation)
- ActiveRecord::Tasks::DatabaseTasks.collation @configuration
- end
- end
+ class MysqlDBCollationTest < ActiveRecord::TestCase
+ def setup
+ @connection = Class.new { def collation; end }.new
+ @configuration = {
+ "adapter" => "mysql2",
+ "database" => "my-app-db"
+ }
+ end
- class MySQLStructureDumpTest < ActiveRecord::TestCase
- def setup
- @configuration = {
- 'adapter' => 'mysql',
- 'database' => 'test-db'
- }
+ def test_db_retrieves_collation
+ ActiveRecord::Base.stub(:connection, @connection) do
+ assert_called(@connection, :collation) do
+ ActiveRecord::Tasks::DatabaseTasks.collation @configuration
+ end
+ end
+ end
end
- def test_structure_dump
- filename = "awesome-file.sql"
- Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "--routines", "test-db").returns(true)
+ class MySQLStructureDumpTest < ActiveRecord::TestCase
+ def setup
+ @configuration = {
+ "adapter" => "mysql2",
+ "database" => "test-db"
+ }
+ end
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
- end
+ def test_structure_dump
+ filename = "awesome-file.sql"
+ assert_called_with(
+ Kernel,
+ :system,
+ ["mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db"],
+ returns: true
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
+ end
+ end
- def test_warn_when_external_structure_dump_fails
- filename = "awesome-file.sql"
- Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "--routines", "test-db").returns(false)
+ def test_structure_dump_with_extra_flags
+ filename = "awesome-file.sql"
+ expected_command = ["mysqldump", "--noop", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db"]
- warnings = capture(:stderr) do
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
+ 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
- assert_match(/Could not dump the database structure/, warnings)
- end
+ def test_structure_dump_with_ignore_tables
+ filename = "awesome-file.sql"
+ ActiveRecord::SchemaDumper.stub(:ignore_tables, ["foo", "bar"]) do
+ assert_called_with(
+ Kernel,
+ :system,
+ ["mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "--ignore-table=test-db.foo", "--ignore-table=test-db.bar", "test-db"],
+ returns: true
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
+ end
+ end
+ end
- def test_structure_dump_with_port_number
- filename = "awesome-file.sql"
- Kernel.expects(:system).with("mysqldump", "--port=10000", "--result-file", filename, "--no-data", "--routines", "test-db").returns(true)
+ def test_warn_when_external_structure_dump_command_execution_fails
+ filename = "awesome-file.sql"
+ assert_called_with(
+ Kernel,
+ :system,
+ ["mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db"],
+ returns: false
+ ) do
+ e = assert_raise(RuntimeError) {
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
+ }
+ assert_match(/^failed to execute: `mysqldump`$/, e.message)
+ end
+ end
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(
- @configuration.merge('port' => 10000),
- filename)
- end
+ def test_structure_dump_with_port_number
+ filename = "awesome-file.sql"
+ assert_called_with(
+ Kernel,
+ :system,
+ ["mysqldump", "--port=10000", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db"],
+ returns: true
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(
+ @configuration.merge("port" => 10000),
+ filename)
+ end
+ end
- def test_structure_dump_with_ssl
- filename = "awesome-file.sql"
- Kernel.expects(:system).with("mysqldump", "--ssl-ca=ca.crt", "--result-file", filename, "--no-data", "--routines", "test-db").returns(true)
+ def test_structure_dump_with_ssl
+ filename = "awesome-file.sql"
+ assert_called_with(
+ Kernel,
+ :system,
+ ["mysqldump", "--ssl-ca=ca.crt", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db"],
+ returns: true
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(
+ @configuration.merge("sslca" => "ca.crt"),
+ filename)
+ end
+ end
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(
- @configuration.merge("sslca" => "ca.crt"),
- filename)
+ 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
- end
- class MySQLStructureLoadTest < ActiveRecord::TestCase
- def setup
- @configuration = {
- 'adapter' => 'mysql',
- 'database' => 'test-db'
- }
- end
+ class MySQLStructureLoadTest < ActiveRecord::TestCase
+ def setup
+ @configuration = {
+ "adapter" => "mysql2",
+ "database" => "test-db"
+ }
+ end
- 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")
+ def test_structure_load
+ filename = "awesome-file.sql"
+ expected_command = ["mysql", "--noop", "--execute", %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}, "--database", "test-db"]
- 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
end
diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb
index 084302cde5..065ba7734c 100644
--- a/activerecord/test/cases/tasks/postgresql_rake_test.rb
+++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb
@@ -1,278 +1,539 @@
-require 'cases/helper'
+# frozen_string_literal: true
-if current_adapter?(:PostgreSQLAdapter)
-module ActiveRecord
- class PostgreSQLDBCreateTest < ActiveRecord::TestCase
- def setup
- @connection = stub(:create_database => true)
- @configuration = {
- 'adapter' => 'postgresql',
- 'database' => 'my-app-db'
- }
-
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
- end
-
- def test_establishes_connection_to_postgresql_database
- ActiveRecord::Base.expects(:establish_connection).with(
- 'adapter' => 'postgresql',
- 'database' => 'postgres',
- 'schema_search_path' => 'public'
- )
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
+require "cases/helper"
+require "active_record/tasks/database_tasks"
- def test_creates_database_with_default_encoding
- @connection.expects(:create_database).
- with('my-app-db', @configuration.merge('encoding' => 'utf8'))
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
+if current_adapter?(:PostgreSQLAdapter)
+ module ActiveRecord
+ class PostgreSQLDBCreateTest < ActiveRecord::TestCase
+ def setup
+ @connection = Class.new { def create_database(*); end }.new
+ @configuration = {
+ "adapter" => "postgresql",
+ "database" => "my-app-db"
+ }
+ $stdout, @original_stdout = StringIO.new, $stdout
+ $stderr, @original_stderr = StringIO.new, $stderr
+ end
- def test_creates_database_with_given_encoding
- @connection.expects(:create_database).
- with('my-app-db', @configuration.merge('encoding' => 'latin'))
+ def teardown
+ $stdout, $stderr = @original_stdout, @original_stderr
+ end
- ActiveRecord::Tasks::DatabaseTasks.create @configuration.
- merge('encoding' => 'latin')
- end
+ def test_establishes_connection_to_postgresql_database
+ ActiveRecord::Base.stub(:connection, @connection) do
+ assert_called_with(
+ ActiveRecord::Base,
+ :establish_connection,
+ [
+ [
+ "adapter" => "postgresql",
+ "database" => "postgres",
+ "schema_search_path" => "public"
+ ],
+ [
+ "adapter" => "postgresql",
+ "database" => "my-app-db"
+ ]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ end
+ end
+ end
- def test_creates_database_with_given_collation_and_ctype
- @connection.expects(:create_database).
- with('my-app-db', @configuration.merge('encoding' => 'utf8', 'collation' => 'ja_JP.UTF8', 'ctype' => 'ja_JP.UTF8'))
+ def test_creates_database_with_default_encoding
+ with_stubbed_connection_establish_connection do
+ assert_called_with(
+ @connection,
+ :create_database,
+ ["my-app-db", @configuration.merge("encoding" => "utf8")]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ end
+ end
+ end
- ActiveRecord::Tasks::DatabaseTasks.create @configuration.
- merge('collation' => 'ja_JP.UTF8', 'ctype' => 'ja_JP.UTF8')
- end
+ def test_creates_database_with_given_encoding
+ with_stubbed_connection_establish_connection do
+ assert_called_with(
+ @connection,
+ :create_database,
+ ["my-app-db", @configuration.merge("encoding" => "latin")]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration.
+ merge("encoding" => "latin")
+ end
+ end
+ end
- def test_establishes_connection_to_new_database
- ActiveRecord::Base.expects(:establish_connection).with(@configuration)
+ def test_creates_database_with_given_collation_and_ctype
+ with_stubbed_connection_establish_connection do
+ assert_called_with(
+ @connection,
+ :create_database,
+ [
+ "my-app-db",
+ @configuration.merge(
+ "encoding" => "utf8",
+ "collation" => "ja_JP.UTF8",
+ "ctype" => "ja_JP.UTF8"
+ )
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration.
+ merge("collation" => "ja_JP.UTF8", "ctype" => "ja_JP.UTF8")
+ end
+ end
+ end
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
+ def test_establishes_connection_to_new_database
+ ActiveRecord::Base.stub(:connection, @connection) do
+ assert_called_with(
+ ActiveRecord::Base,
+ :establish_connection,
+ [
+ [
+ "adapter" => "postgresql",
+ "database" => "postgres",
+ "schema_search_path" => "public"
+ ],
+ [
+ @configuration
+ ]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ end
+ end
+ end
- def test_db_create_with_error_prints_message
- ActiveRecord::Base.stubs(:establish_connection).raises(Exception)
+ def test_db_create_with_error_prints_message
+ ActiveRecord::Base.stub(:connection, @connection) do
+ ActiveRecord::Base.stub(:establish_connection, -> * { raise Exception }) do
+ assert_raises(Exception) { ActiveRecord::Tasks::DatabaseTasks.create @configuration }
+ assert_match "Couldn't create '#{@configuration['database']}' database. Please check your configuration.", $stderr.string
+ end
+ end
+ end
- $stderr.stubs(:puts).returns(true)
- $stderr.expects(:puts).
- with("Couldn't create database for #{@configuration.inspect}")
+ def test_when_database_created_successfully_outputs_info_to_stdout
+ with_stubbed_connection_establish_connection do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
+ assert_equal "Created database 'my-app-db'\n", $stdout.string
+ end
+ end
- def test_create_when_database_exists_outputs_info_to_stderr
- $stderr.expects(:puts).with("my-app-db already exists").once
+ def test_create_when_database_exists_outputs_info_to_stderr
+ with_stubbed_connection_establish_connection do
+ ActiveRecord::Base.connection.stub(
+ :create_database,
+ proc { raise ActiveRecord::Tasks::DatabaseAlreadyExists }
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+
+ assert_equal "Database 'my-app-db' already exists\n", $stderr.string
+ end
+ end
+ end
- ActiveRecord::Base.connection.stubs(:create_database).raises(
- ActiveRecord::StatementInvalid.new('database "my-app-db" already exists')
- )
+ private
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ def with_stubbed_connection_establish_connection
+ ActiveRecord::Base.stub(:connection, @connection) do
+ ActiveRecord::Base.stub(:establish_connection, nil) do
+ yield
+ end
+ end
+ end
end
- end
- class PostgreSQLDBDropTest < ActiveRecord::TestCase
- def setup
- @connection = stub(:drop_database => true)
- @configuration = {
- 'adapter' => 'postgresql',
- 'database' => 'my-app-db'
- }
+ class PostgreSQLDBDropTest < ActiveRecord::TestCase
+ def setup
+ @connection = Class.new { def drop_database(*); end }.new
+ @configuration = {
+ "adapter" => "postgresql",
+ "database" => "my-app-db"
+ }
+ $stdout, @original_stdout = StringIO.new, $stdout
+ $stderr, @original_stderr = StringIO.new, $stderr
+ end
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
- end
+ def teardown
+ $stdout, $stderr = @original_stdout, @original_stderr
+ end
- def test_establishes_connection_to_postgresql_database
- ActiveRecord::Base.expects(:establish_connection).with(
- 'adapter' => 'postgresql',
- 'database' => 'postgres',
- 'schema_search_path' => 'public'
- )
+ def test_establishes_connection_to_postgresql_database
+ ActiveRecord::Base.stub(:connection, @connection) do
+ assert_called_with(
+ ActiveRecord::Base,
+ :establish_connection,
+ [
+ "adapter" => "postgresql",
+ "database" => "postgres",
+ "schema_search_path" => "public"
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.drop @configuration
+ end
+ end
+ end
- ActiveRecord::Tasks::DatabaseTasks.drop @configuration
- end
+ def test_drops_database
+ with_stubbed_connection_establish_connection do
+ assert_called_with(
+ @connection,
+ :drop_database,
+ ["my-app-db"]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.drop @configuration
+ end
+ end
+ end
- def test_drops_database
- @connection.expects(:drop_database).with('my-app-db')
+ def test_when_database_dropped_successfully_outputs_info_to_stdout
+ with_stubbed_connection_establish_connection do
+ ActiveRecord::Tasks::DatabaseTasks.drop @configuration
- ActiveRecord::Tasks::DatabaseTasks.drop @configuration
- end
- end
-
- class PostgreSQLPurgeTest < ActiveRecord::TestCase
- def setup
- @connection = stub(:create_database => true, :drop_database => true)
- @configuration = {
- 'adapter' => 'postgresql',
- 'database' => 'my-app-db'
- }
-
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:clear_active_connections!).returns(true)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
- end
+ assert_equal "Dropped database 'my-app-db'\n", $stdout.string
+ end
+ end
- def test_clears_active_connections
- ActiveRecord::Base.expects(:clear_active_connections!)
+ private
- ActiveRecord::Tasks::DatabaseTasks.purge @configuration
+ def with_stubbed_connection_establish_connection
+ ActiveRecord::Base.stub(:connection, @connection) do
+ ActiveRecord::Base.stub(:establish_connection, nil) do
+ yield
+ end
+ end
+ end
end
- def test_establishes_connection_to_postgresql_database
- ActiveRecord::Base.expects(:establish_connection).with(
- 'adapter' => 'postgresql',
- 'database' => 'postgres',
- 'schema_search_path' => 'public'
- )
+ class PostgreSQLPurgeTest < ActiveRecord::TestCase
+ def setup
+ @connection = Class.new do
+ def create_database(*); end
+ def drop_database(*); end
+ end.new
+ @configuration = {
+ "adapter" => "postgresql",
+ "database" => "my-app-db"
+ }
+ end
- ActiveRecord::Tasks::DatabaseTasks.purge @configuration
- end
+ def test_clears_active_connections
+ with_stubbed_connection do
+ ActiveRecord::Base.stub(:establish_connection, nil) do
+ assert_called(ActiveRecord::Base, :clear_active_connections!) do
+ ActiveRecord::Tasks::DatabaseTasks.purge @configuration
+ end
+ end
+ end
+ end
- def test_drops_database
- @connection.expects(:drop_database).with('my-app-db')
+ def test_establishes_connection_to_postgresql_database
+ with_stubbed_connection do
+ assert_called_with(
+ ActiveRecord::Base,
+ :establish_connection,
+ [
+ [
+ "adapter" => "postgresql",
+ "database" => "postgres",
+ "schema_search_path" => "public"
+ ],
+ [
+ "adapter" => "postgresql",
+ "database" => "my-app-db"
+ ]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.purge @configuration
+ end
+ end
+ end
- ActiveRecord::Tasks::DatabaseTasks.purge @configuration
- end
+ def test_drops_database
+ with_stubbed_connection do
+ ActiveRecord::Base.stub(:establish_connection, nil) do
+ assert_called_with(@connection, :drop_database, ["my-app-db"]) do
+ ActiveRecord::Tasks::DatabaseTasks.purge @configuration
+ end
+ end
+ end
+ end
- def test_creates_database
- @connection.expects(:create_database).
- with('my-app-db', @configuration.merge('encoding' => 'utf8'))
+ def test_creates_database
+ with_stubbed_connection do
+ ActiveRecord::Base.stub(:establish_connection, nil) do
+ assert_called_with(
+ @connection,
+ :create_database,
+ ["my-app-db", @configuration.merge("encoding" => "utf8")]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.purge @configuration
+ end
+ end
+ end
+ end
- ActiveRecord::Tasks::DatabaseTasks.purge @configuration
- end
+ def test_establishes_connection
+ with_stubbed_connection do
+ assert_called_with(
+ ActiveRecord::Base,
+ :establish_connection,
+ [
+ [
+ "adapter" => "postgresql",
+ "database" => "postgres",
+ "schema_search_path" => "public"
+ ],
+ [
+ @configuration
+ ]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.purge @configuration
+ end
+ end
+ end
- def test_establishes_connection
- ActiveRecord::Base.expects(:establish_connection).with(@configuration)
+ private
- ActiveRecord::Tasks::DatabaseTasks.purge @configuration
+ def with_stubbed_connection
+ ActiveRecord::Base.stub(:connection, @connection) do
+ yield
+ end
+ end
end
- end
- class PostgreSQLDBCharsetTest < ActiveRecord::TestCase
- def setup
- @connection = stub(:create_database => true)
- @configuration = {
- 'adapter' => 'postgresql',
- 'database' => 'my-app-db'
- }
-
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
- end
+ class PostgreSQLDBCharsetTest < ActiveRecord::TestCase
+ def setup
+ @connection = Class.new do
+ def create_database(*); end
+ def encoding; end
+ end.new
+ @configuration = {
+ "adapter" => "postgresql",
+ "database" => "my-app-db"
+ }
+ end
- def test_db_retrieves_charset
- @connection.expects(:encoding)
- ActiveRecord::Tasks::DatabaseTasks.charset @configuration
+ def test_db_retrieves_charset
+ ActiveRecord::Base.stub(:connection, @connection) do
+ assert_called(@connection, :encoding) do
+ ActiveRecord::Tasks::DatabaseTasks.charset @configuration
+ end
+ end
+ end
end
- end
- class PostgreSQLDBCollationTest < ActiveRecord::TestCase
- def setup
- @connection = stub(:create_database => true)
- @configuration = {
- 'adapter' => 'postgresql',
- 'database' => 'my-app-db'
- }
-
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
- end
+ class PostgreSQLDBCollationTest < ActiveRecord::TestCase
+ def setup
+ @connection = Class.new { def collation; end }.new
+ @configuration = {
+ "adapter" => "postgresql",
+ "database" => "my-app-db"
+ }
+ end
- def test_db_retrieves_collation
- @connection.expects(:collation)
- ActiveRecord::Tasks::DatabaseTasks.collation @configuration
+ def test_db_retrieves_collation
+ ActiveRecord::Base.stub(:connection, @connection) do
+ assert_called(@connection, :collation) do
+ ActiveRecord::Tasks::DatabaseTasks.collation @configuration
+ end
+ end
+ end
end
- end
- class PostgreSQLStructureDumpTest < ActiveRecord::TestCase
- def setup
- @connection = stub(:structure_dump => true)
- @configuration = {
- 'adapter' => 'postgresql',
- 'database' => 'my-app-db'
- }
- @filename = "awesome-file.sql"
-
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
- Kernel.stubs(:system)
- File.stubs(:open)
- end
+ class PostgreSQLStructureDumpTest < ActiveRecord::TestCase
+ def setup
+ @configuration = {
+ "adapter" => "postgresql",
+ "database" => "my-app-db"
+ }
+ @filename = "/tmp/awesome-file.sql"
+ FileUtils.touch(@filename)
+ end
- def test_structure_dump
- Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{@filename} my-app-db").returns(true)
+ def teardown
+ FileUtils.rm_f(@filename)
+ end
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
- end
+ def test_structure_dump
+ assert_called_with(
+ Kernel,
+ :system,
+ ["pg_dump", "-s", "-x", "-O", "-f", @filename, "my-app-db"],
+ returns: true
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
+ end
+ end
- def test_structure_dump_with_schema_search_path
- @configuration['schema_search_path'] = 'foo,bar'
+ def test_structure_dump_header_comments_removed
+ Kernel.stub(:system, true) do
+ File.write(@filename, "-- header comment\n\n-- more header comment\n statement \n-- lower comment\n")
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
- Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{@filename} --schema=foo --schema=bar my-app-db").returns(true)
+ assert_equal [" statement \n", "-- lower comment\n"], File.readlines(@filename).first(2)
+ end
+ end
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
- end
+ def test_structure_dump_with_extra_flags
+ expected_command = ["pg_dump", "-s", "-x", "-O", "-f", @filename, "--noop", "my-app-db"]
- def test_structure_dump_with_schema_search_path_and_dump_schemas_all
- @configuration['schema_search_path'] = 'foo,bar'
+ 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
- Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{@filename} my-app-db").returns(true)
+ def test_structure_dump_with_ignore_tables
+ assert_called(
+ ActiveRecord::SchemaDumper,
+ :ignore_tables,
+ returns: ["foo", "bar"]
+ ) do
+ assert_called_with(
+ Kernel,
+ :system,
+ ["pg_dump", "-s", "-x", "-O", "-f", @filename, "-T", "foo", "-T", "bar", "my-app-db"],
+ returns: true
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
+ end
+ end
+ end
- with_dump_schemas(:all) do
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
+ def test_structure_dump_with_schema_search_path
+ @configuration["schema_search_path"] = "foo,bar"
+
+ assert_called_with(
+ Kernel,
+ :system,
+ ["pg_dump", "-s", "-x", "-O", "-f", @filename, "--schema=foo", "--schema=bar", "my-app-db"],
+ returns: true
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
+ end
end
- end
- def test_structure_dump_with_dump_schemas_string
- Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{@filename} --schema=foo --schema=bar my-app-db").returns(true)
+ def test_structure_dump_with_schema_search_path_and_dump_schemas_all
+ @configuration["schema_search_path"] = "foo,bar"
+
+ assert_called_with(
+ Kernel,
+ :system,
+ ["pg_dump", "-s", "-x", "-O", "-f", @filename, "my-app-db"],
+ returns: true
+ ) do
+ with_dump_schemas(:all) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
+ end
+ end
+ end
- with_dump_schemas('foo,bar') do
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
+ def test_structure_dump_with_dump_schemas_string
+ assert_called_with(
+ Kernel,
+ :system,
+ ["pg_dump", "-s", "-x", "-O", "-f", @filename, "--schema=foo", "--schema=bar", "my-app-db"],
+ returns: true
+ ) do
+ with_dump_schemas("foo,bar") do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
+ end
+ end
end
- end
- private
+ def test_structure_dump_execution_fails
+ filename = "awesome-file.sql"
+ assert_called_with(
+ Kernel,
+ :system,
+ ["pg_dump", "-s", "-x", "-O", "-f", filename, "my-app-db"],
+ returns: nil
+ ) do
+ e = assert_raise(RuntimeError) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
+ end
+ assert_match("failed to execute:", e.message)
+ end
+ end
- def with_dump_schemas(value, &block)
- old_dump_schemas = ActiveRecord::Base.dump_schemas
- ActiveRecord::Base.dump_schemas = value
- yield
- ensure
- ActiveRecord::Base.dump_schemas = old_dump_schemas
+ private
+ def with_dump_schemas(value, &block)
+ old_dump_schemas = ActiveRecord::Base.dump_schemas
+ ActiveRecord::Base.dump_schemas = value
+ yield
+ 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
- end
- class PostgreSQLStructureLoadTest < ActiveRecord::TestCase
- def setup
- @connection = stub
- @configuration = {
- 'adapter' => 'postgresql',
- 'database' => 'my-app-db'
- }
-
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
- Kernel.stubs(:system)
- end
+ class PostgreSQLStructureLoadTest < ActiveRecord::TestCase
+ def setup
+ @configuration = {
+ "adapter" => "postgresql",
+ "database" => "my-app-db"
+ }
+ end
- def test_structure_load
- filename = "awesome-file.sql"
- Kernel.expects(:system).with("psql -X -q -f #{filename} my-app-db")
+ def test_structure_load
+ filename = "awesome-file.sql"
+ assert_called_with(
+ Kernel,
+ :system,
+ ["psql", "-v", "ON_ERROR_STOP=1", "-q", "-X", "-f", filename, @configuration["database"]],
+ returns: true
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename)
+ end
+ end
- 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", "-X", "-f", filename, "--noop", @configuration["database"]]
- def test_structure_load_accepts_path_with_spaces
- filename = "awesome file.sql"
- Kernel.expects(:system).with("psql -X -q -f awesome\\ file.sql my-app-db")
+ 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
- ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename)
+ def test_structure_load_accepts_path_with_spaces
+ filename = "awesome file.sql"
+ assert_called_with(
+ Kernel,
+ :system,
+ ["psql", "-v", "ON_ERROR_STOP=1", "-q", "-X", "-f", filename, @configuration["database"]],
+ returns: true
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename)
+ 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
end
diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb
index 750d5e42dc..c1092b97c1 100644
--- a/activerecord/test/cases/tasks/sqlite_rake_test.rb
+++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb
@@ -1,193 +1,264 @@
-require 'cases/helper'
-require 'pathname'
+# frozen_string_literal: true
-if current_adapter?(:SQLite3Adapter)
-module ActiveRecord
- class SqliteDBCreateTest < ActiveRecord::TestCase
- def setup
- @database = 'db_create.sqlite3'
- @connection = stub :connection
- @configuration = {
- 'adapter' => 'sqlite3',
- 'database' => @database
- }
-
- File.stubs(:exist?).returns(false)
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
- end
+require "cases/helper"
+require "active_record/tasks/database_tasks"
+require "pathname"
- def test_db_checks_database_exists
- File.expects(:exist?).with(@database).returns(false)
+if current_adapter?(:SQLite3Adapter)
+ module ActiveRecord
+ class SqliteDBCreateTest < ActiveRecord::TestCase
+ def setup
+ @database = "db_create.sqlite3"
+ @configuration = {
+ "adapter" => "sqlite3",
+ "database" => @database
+ }
+ $stdout, @original_stdout = StringIO.new, $stdout
+ $stderr, @original_stderr = StringIO.new, $stderr
+ end
- ActiveRecord::Tasks::DatabaseTasks.create @configuration, '/rails/root'
- end
+ def teardown
+ $stdout, $stderr = @original_stdout, @original_stderr
+ end
- def test_db_create_when_file_exists
- File.stubs(:exist?).returns(true)
+ def test_db_checks_database_exists
+ ActiveRecord::Base.stub(:establish_connection, nil) do
+ assert_called_with(File, :exist?, [@database], returns: false) do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root"
+ end
+ end
+ end
- $stderr.expects(:puts).with("#{@database} already exists")
+ def test_when_db_created_successfully_outputs_info_to_stdout
+ ActiveRecord::Base.stub(:establish_connection, nil) do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root"
- ActiveRecord::Tasks::DatabaseTasks.create @configuration, '/rails/root'
- end
+ assert_equal "Created database '#{@database}'\n", $stdout.string
+ end
+ end
- def test_db_create_with_file_does_nothing
- File.stubs(:exist?).returns(true)
- $stderr.stubs(:puts).returns(nil)
+ def test_db_create_when_file_exists
+ File.stub(:exist?, true) do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root"
- ActiveRecord::Base.expects(:establish_connection).never
+ assert_equal "Database '#{@database}' already exists\n", $stderr.string
+ end
+ end
- ActiveRecord::Tasks::DatabaseTasks.create @configuration, '/rails/root'
- end
+ def test_db_create_with_file_does_nothing
+ File.stub(:exist?, true) do
+ assert_not_called(ActiveRecord::Base, :establish_connection) do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root"
+ end
+ end
+ end
- def test_db_create_establishes_a_connection
- ActiveRecord::Base.expects(:establish_connection).with(@configuration)
+ def test_db_create_establishes_a_connection
+ assert_called_with(ActiveRecord::Base, :establish_connection, [@configuration]) do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root"
+ end
+ end
- ActiveRecord::Tasks::DatabaseTasks.create @configuration, '/rails/root'
+ def test_db_create_with_error_prints_message
+ ActiveRecord::Base.stub(:establish_connection, proc { raise Exception }) do
+ assert_raises(Exception) { ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" }
+ assert_match "Couldn't create '#{@configuration['database']}' database. Please check your configuration.", $stderr.string
+ end
+ end
end
- def test_db_create_with_error_prints_message
- ActiveRecord::Base.stubs(:establish_connection).raises(Exception)
-
- $stderr.stubs(:puts).returns(true)
- $stderr.expects(:puts).
- with("Couldn't create database for #{@configuration.inspect}")
+ class SqliteDBDropTest < ActiveRecord::TestCase
+ def setup
+ @database = "db_create.sqlite3"
+ @configuration = {
+ "adapter" => "sqlite3",
+ "database" => @database
+ }
+ @path = Class.new do
+ def to_s; "/absolute/path" end
+ def absolute?; true end
+ end.new
+
+ $stdout, @original_stdout = StringIO.new, $stdout
+ $stderr, @original_stderr = StringIO.new, $stderr
+ end
- ActiveRecord::Tasks::DatabaseTasks.create @configuration, '/rails/root'
- end
- end
+ def teardown
+ $stdout, $stderr = @original_stdout, @original_stderr
+ end
- class SqliteDBDropTest < ActiveRecord::TestCase
- def setup
- @database = "db_create.sqlite3"
- @path = stub(:to_s => '/absolute/path', :absolute? => true)
- @configuration = {
- 'adapter' => 'sqlite3',
- 'database' => @database
- }
-
- Pathname.stubs(:new).returns(@path)
- File.stubs(:join).returns('/former/relative/path')
- FileUtils.stubs(:rm).returns(true)
- end
+ def test_creates_path_from_database
+ assert_called_with(Pathname, :new, [@database], returns: @path) do
+ ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root"
+ end
+ end
- def test_creates_path_from_database
- Pathname.expects(:new).with(@database).returns(@path)
+ def test_removes_file_with_absolute_path
+ Pathname.stub(:new, @path) do
+ assert_called_with(FileUtils, :rm, ["/absolute/path"]) do
+ ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root"
+ end
+ end
+ end
- ActiveRecord::Tasks::DatabaseTasks.drop @configuration, '/rails/root'
- end
+ def test_generates_absolute_path_with_given_root
+ Pathname.stub(:new, @path) do
+ @path.stub(:absolute?, false) do
+ assert_called_with(File, :join, ["/rails/root", @path],
+ returns: "/former/relative/path"
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root"
+ end
+ end
+ end
+ end
- def test_removes_file_with_absolute_path
- File.stubs(:exist?).returns(true)
- @path.stubs(:absolute?).returns(true)
+ def test_removes_file_with_relative_path
+ File.stub(:join, "/former/relative/path") do
+ @path.stub(:absolute?, false) do
+ assert_called_with(FileUtils, :rm, ["/former/relative/path"]) do
+ ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root"
+ end
+ end
+ end
+ end
- FileUtils.expects(:rm).with('/absolute/path')
+ def test_when_db_dropped_successfully_outputs_info_to_stdout
+ FileUtils.stub(:rm, nil) do
+ ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root"
- ActiveRecord::Tasks::DatabaseTasks.drop @configuration, '/rails/root'
+ assert_equal "Dropped database '#{@database}'\n", $stdout.string
+ end
+ end
end
- def test_generates_absolute_path_with_given_root
- @path.stubs(:absolute?).returns(false)
-
- File.expects(:join).with('/rails/root', @path).
- returns('/former/relative/path')
+ class SqliteDBCharsetTest < ActiveRecord::TestCase
+ def setup
+ @database = "db_create.sqlite3"
+ @connection = Class.new { def encoding; end }.new
+ @configuration = {
+ "adapter" => "sqlite3",
+ "database" => @database
+ }
+ end
- ActiveRecord::Tasks::DatabaseTasks.drop @configuration, '/rails/root'
+ def test_db_retrieves_charset
+ ActiveRecord::Base.stub(:connection, @connection) do
+ assert_called(@connection, :encoding) do
+ ActiveRecord::Tasks::DatabaseTasks.charset @configuration, "/rails/root"
+ end
+ end
+ end
end
- def test_removes_file_with_relative_path
- File.stubs(:exist?).returns(true)
- @path.stubs(:absolute?).returns(false)
-
- FileUtils.expects(:rm).with('/former/relative/path')
-
- ActiveRecord::Tasks::DatabaseTasks.drop @configuration, '/rails/root'
- end
- end
+ class SqliteDBCollationTest < ActiveRecord::TestCase
+ def setup
+ @database = "db_create.sqlite3"
+ @configuration = {
+ "adapter" => "sqlite3",
+ "database" => @database
+ }
+ end
- class SqliteDBCharsetTest < ActiveRecord::TestCase
- def setup
- @database = 'db_create.sqlite3'
- @connection = stub :connection
- @configuration = {
- 'adapter' => 'sqlite3',
- 'database' => @database
- }
-
- File.stubs(:exist?).returns(false)
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
+ def test_db_retrieves_collation
+ assert_raise NoMethodError do
+ ActiveRecord::Tasks::DatabaseTasks.collation @configuration, "/rails/root"
+ end
+ end
end
- def test_db_retrieves_charset
- @connection.expects(:encoding)
- ActiveRecord::Tasks::DatabaseTasks.charset @configuration, '/rails/root'
- end
- end
+ class SqliteStructureDumpTest < ActiveRecord::TestCase
+ def setup
+ @database = "db_create.sqlite3"
+ @configuration = {
+ "adapter" => "sqlite3",
+ "database" => @database
+ }
- class SqliteDBCollationTest < ActiveRecord::TestCase
- def setup
- @database = 'db_create.sqlite3'
- @connection = stub :connection
- @configuration = {
- 'adapter' => 'sqlite3',
- 'database' => @database
- }
-
- File.stubs(:exist?).returns(false)
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
- end
+ `sqlite3 #{@database} 'CREATE TABLE bar(id INTEGER)'`
+ `sqlite3 #{@database} 'CREATE TABLE foo(id INTEGER)'`
+ end
- def test_db_retrieves_collation
- assert_raise NoMethodError do
- ActiveRecord::Tasks::DatabaseTasks.collation @configuration, '/rails/root'
+ def test_structure_dump
+ dbfile = @database
+ filename = "awesome-file.sql"
+
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump @configuration, filename, "/rails/root"
+ assert File.exist?(dbfile)
+ assert File.exist?(filename)
+ assert_match(/CREATE TABLE foo/, File.read(filename))
+ assert_match(/CREATE TABLE bar/, File.read(filename))
+ ensure
+ FileUtils.rm_f(filename)
+ FileUtils.rm_f(dbfile)
end
- end
- end
- class SqliteStructureDumpTest < ActiveRecord::TestCase
- def setup
- @database = "db_create.sqlite3"
- @configuration = {
- 'adapter' => 'sqlite3',
- 'database' => @database
- }
- end
+ def test_structure_dump_with_ignore_tables
+ dbfile = @database
+ filename = "awesome-file.sql"
+ assert_called(ActiveRecord::SchemaDumper, :ignore_tables, returns: ["foo"]) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename, "/rails/root")
+ end
+ assert File.exist?(dbfile)
+ assert File.exist?(filename)
+ assert_match(/bar/, File.read(filename))
+ assert_no_match(/foo/, File.read(filename))
+ ensure
+ FileUtils.rm_f(filename)
+ FileUtils.rm_f(dbfile)
+ end
- def test_structure_dump
- dbfile = @database
- filename = "awesome-file.sql"
+ def test_structure_dump_execution_fails
+ dbfile = @database
+ filename = "awesome-file.sql"
+ assert_called_with(
+ Kernel,
+ :system,
+ ["sqlite3", "--noop", "db_create.sqlite3", ".schema", out: "awesome-file.sql"],
+ returns: nil
+ ) do
+ e = assert_raise(RuntimeError) do
+ with_structure_dump_flags(["--noop"]) do
+ quietly { ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename, "/rails/root") }
+ end
+ end
+ assert_match("failed to execute:", e.message)
+ end
+ ensure
+ FileUtils.rm_f(filename)
+ FileUtils.rm_f(dbfile)
+ end
- ActiveRecord::Tasks::DatabaseTasks.structure_dump @configuration, filename, '/rails/root'
- assert File.exist?(dbfile)
- assert File.exist?(filename)
- ensure
- FileUtils.rm_f(filename)
- FileUtils.rm_f(dbfile)
+ 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
- end
- class SqliteStructureLoadTest < ActiveRecord::TestCase
- def setup
- @database = "db_create.sqlite3"
- @configuration = {
- 'adapter' => 'sqlite3',
- 'database' => @database
- }
- end
+ class SqliteStructureLoadTest < ActiveRecord::TestCase
+ def setup
+ @database = "db_create.sqlite3"
+ @configuration = {
+ "adapter" => "sqlite3",
+ "database" => @database
+ }
+ end
- def test_structure_load
- dbfile = @database
- filename = "awesome-file.sql"
+ def test_structure_load
+ dbfile = @database
+ filename = "awesome-file.sql"
- open(filename, 'w') { |f| f.puts("select datetime('now', 'localtime');") }
- ActiveRecord::Tasks::DatabaseTasks.structure_load @configuration, filename, '/rails/root'
- assert File.exist?(dbfile)
- ensure
- FileUtils.rm_f(filename)
- FileUtils.rm_f(dbfile)
+ open(filename, "w") { |f| f.puts("select datetime('now', 'localtime');") }
+ ActiveRecord::Tasks::DatabaseTasks.structure_load @configuration, filename, "/rails/root"
+ assert File.exist?(dbfile)
+ ensure
+ FileUtils.rm_f(filename)
+ FileUtils.rm_f(dbfile)
+ end
end
end
end
-end
diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb
index 7761ea5612..40947767f3 100644
--- a/activerecord/test/cases/test_case.rb
+++ b/activerecord/test/cases/test_case.rb
@@ -1,22 +1,37 @@
-require 'active_support/test_case'
-require 'active_support/testing/stream'
+# frozen_string_literal: true
+
+require "active_support"
+require "active_support/testing/autorun"
+require "active_support/testing/method_call_assertions"
+require "active_support/testing/stream"
+require "active_record/fixtures"
+
+require "cases/validations_repair_helper"
module ActiveRecord
# = Active Record Test Case
#
# Defines some test assertions to test against SQL queries.
class TestCase < ActiveSupport::TestCase #:nodoc:
+ include ActiveSupport::Testing::MethodCallAssertions
include ActiveSupport::Testing::Stream
+ include ActiveRecord::TestFixtures
+ include ActiveRecord::ValidationsRepairHelper
- def teardown
- SQLCounter.clear_log
+ self.fixture_path = FIXTURES_ROOT
+ self.use_instantiated_fixtures = false
+ self.use_transactional_tests = true
+
+ def create_fixtures(*fixture_set_names, &block)
+ ActiveRecord::FixtureSet.create_fixtures(ActiveRecord::TestCase.fixture_path, fixture_set_names, fixture_class_names, &block)
end
- def assert_date_from_db(expected, actual, message = nil)
- assert_equal expected.to_s, actual.to_s, message
+ def teardown
+ SQLCounter.clear_log
end
def capture_sql
+ ActiveRecord::Base.connection.materialize_transactions
SQLCounter.clear_log
yield
SQLCounter.log_all.dup
@@ -27,13 +42,14 @@ module ActiveRecord
ensure
failed_patterns = []
patterns_to_match.each do |pattern|
- failed_patterns << pattern unless SQLCounter.log_all.any?{ |sql| pattern === sql }
+ failed_patterns << pattern unless SQLCounter.log_all.any? { |sql| pattern === sql }
end
assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map(&:inspect).join(', ')} not found.#{SQLCounter.log.size == 0 ? '' : "\nQueries:\n#{SQLCounter.log.join("\n")}"}"
end
def assert_queries(num = 1, options = {})
ignore_none = options.fetch(:ignore_none) { num == :any }
+ ActiveRecord::Base.connection.materialize_transactions
SQLCounter.clear_log
x = yield
the_log = ignore_none ? SQLCounter.log_all : SQLCounter.log
@@ -51,11 +67,11 @@ module ActiveRecord
assert_queries(0, options, &block)
end
- def assert_column(model, column_name, msg=nil)
+ def assert_column(model, column_name, msg = nil)
assert has_column?(model, column_name), msg
end
- def assert_no_column(model, column_name, msg=nil)
+ def assert_no_column(model, column_name, msg = nil)
assert_not has_column?(model, column_name), msg
end
@@ -63,6 +79,10 @@ module ActiveRecord
model.reset_column_information
model.column_names.include?(column_name.to_s)
end
+
+ def frozen_error_class
+ Object.const_defined?(:FrozenError) ? FrozenError : RuntimeError
+ end
end
class PostgreSQLTestCase < TestCase
@@ -77,12 +97,6 @@ module ActiveRecord
end
end
- class MysqlTestCase < TestCase
- def self.run(*args)
- super if current_adapter?(:MysqlAdapter)
- end
- end
-
class SQLite3TestCase < TestCase
def self.run(*args)
super if current_adapter?(:SQLite3Adapter)
@@ -95,16 +109,16 @@ module ActiveRecord
def clear_log; self.log = []; self.log_all = []; end
end
- self.clear_log
+ clear_log
self.ignored_sql = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/, /^BEGIN/, /^COMMIT/]
# FIXME: this needs to be refactored so specific database can add their own
# ignored SQL, or better yet, use a different notification for the queries
# instead examining the SQL content.
- oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im, /^\s*select .* from all_constraints/im, /^\s*select .* from all_tab_cols/im]
- mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i, /^SHOW VARIABLES /]
- postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select tablename\b.*from pg_tables\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i]
+ oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im, /^\s*select .* from all_constraints/im, /^\s*select .* from all_tab_cols/im, /^\s*select .* from all_sequences/im]
+ mysql_ignored = [/^SHOW FULL TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i, /^SHOW VARIABLES /, /^\s*SELECT (?:column_name|table_name)\b.*\bFROM information_schema\.(?:key_column_usage|tables)\b/im]
+ postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select tablename\b.*from pg_tables\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i, /^\s*SELECT\b.*::regtype::oid\b/im]
sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im, /^\s*SELECT sql\b.*\bFROM sqlite_master/im]
[oracle_ignored, mysql_ignored, postgresql_ignored, sqlite3_ignored].each do |db_ignored_sql|
@@ -118,16 +132,13 @@ module ActiveRecord
end
def call(name, start, finish, message_id, values)
- sql = values[:sql]
-
- # FIXME: this seems bad. we should probably have a better way to indicate
- # the query was cached
- return if 'CACHE' == values[:name]
+ return if values[:cached]
+ sql = values[:sql]
self.class.log_all << sql
- self.class.log << sql unless ignore =~ sql
+ self.class.log << sql unless ignore.match?(sql)
end
end
- ActiveSupport::Notifications.subscribe('sql.active_record', SQLCounter.new)
+ ActiveSupport::Notifications.subscribe("sql.active_record", SQLCounter.new)
end
diff --git a/activerecord/test/cases/test_fixtures_test.rb b/activerecord/test/cases/test_fixtures_test.rb
index 3f4baf8378..4411410eda 100644
--- a/activerecord/test/cases/test_fixtures_test.rb
+++ b/activerecord/test/cases/test_fixtures_test.rb
@@ -1,34 +1,18 @@
-require 'cases/helper'
+# frozen_string_literal: true
+
+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
- def test_use_transactional_tests_can_be_overriden
+ def test_use_transactional_tests_can_be_overridden
@klass.use_transactional_tests = "foobar"
assert_equal "foobar", @klass.use_transactional_tests
diff --git a/activerecord/test/cases/time_precision_test.rb b/activerecord/test/cases/time_precision_test.rb
index ff7a81fe60..086500de38 100644
--- a/activerecord/test/cases/time_precision_test.rb
+++ b/activerecord/test/cases/time_precision_test.rb
@@ -1,108 +1,103 @@
-require 'cases/helper'
-require 'support/schema_dumping_helper'
+# frozen_string_literal: true
-if ActiveRecord::Base.connection.supports_datetime_with_precision?
-class TimePrecisionTest < ActiveRecord::TestCase
- include SchemaDumpingHelper
- self.use_transactional_tests = false
+require "cases/helper"
+require "support/schema_dumping_helper"
- class Foo < ActiveRecord::Base; end
+if subsecond_precision_supported?
+ class TimePrecisionTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
+ self.use_transactional_tests = false
- setup do
- @connection = ActiveRecord::Base.connection
- end
-
- teardown do
- @connection.drop_table :foos, if_exists: true
- end
+ class Foo < ActiveRecord::Base; end
- def test_time_data_type_with_precision
- @connection.create_table(:foos, force: true)
- @connection.add_column :foos, :start, :time, precision: 3
- @connection.add_column :foos, :finish, :time, precision: 6
- assert_equal 3, activerecord_column_option('foos', 'start', 'precision')
- assert_equal 6, activerecord_column_option('foos', 'finish', 'precision')
- end
+ setup do
+ @connection = ActiveRecord::Base.connection
+ Foo.reset_column_information
+ end
- def test_passing_precision_to_time_does_not_set_limit
- @connection.create_table(:foos, force: true) do |t|
- t.time :start, precision: 3
- t.time :finish, precision: 6
+ teardown do
+ @connection.drop_table :foos, if_exists: true
end
- assert_nil activerecord_column_option('foos', 'start', 'limit')
- assert_nil activerecord_column_option('foos', 'finish', 'limit')
- end
- def test_invalid_time_precision_raises_error
- assert_raises ActiveRecord::ActiveRecordError do
- @connection.create_table(:foos, force: true) do |t|
- t.time :start, precision: 7
- t.time :finish, precision: 7
- end
+ def test_time_data_type_with_precision
+ @connection.create_table(:foos, force: true)
+ @connection.add_column :foos, :start, :time, precision: 3
+ @connection.add_column :foos, :finish, :time, precision: 6
+ assert_equal 3, Foo.columns_hash["start"].precision
+ assert_equal 6, Foo.columns_hash["finish"].precision
end
- end
- def test_database_agrees_with_activerecord_about_precision
- @connection.create_table(:foos, force: true) do |t|
- t.time :start, precision: 2
- t.time :finish, precision: 4
+ def test_time_precision_is_truncated_on_assignment
+ @connection.create_table(:foos, force: true)
+ @connection.add_column :foos, :start, :time, precision: 0
+ @connection.add_column :foos, :finish, :time, precision: 6
+
+ time = ::Time.now.change(nsec: 123456789)
+ foo = Foo.new(start: time, finish: time)
+
+ assert_equal 0, foo.start.nsec
+ assert_equal 123456000, foo.finish.nsec
+
+ foo.save!
+ foo.reload
+
+ assert_equal 0, foo.start.nsec
+ assert_equal 123456000, foo.finish.nsec
end
- assert_equal 2, database_datetime_precision('foos', 'start')
- assert_equal 4, database_datetime_precision('foos', 'finish')
- end
- def test_formatting_time_according_to_precision
- @connection.create_table(:foos, force: true) do |t|
- t.time :start, precision: 0
- t.time :finish, precision: 4
+ def test_passing_precision_to_time_does_not_set_limit
+ @connection.create_table(:foos, force: true) do |t|
+ t.time :start, precision: 3
+ t.time :finish, precision: 6
+ end
+ assert_nil Foo.columns_hash["start"].limit
+ assert_nil Foo.columns_hash["finish"].limit
end
- time = ::Time.utc(2000, 1, 1, 12, 30, 0, 999999)
- Foo.create!(start: time, finish: time)
- assert foo = Foo.find_by(start: time)
- assert_equal 1, Foo.where(finish: time).count
- assert_equal time.to_s, foo.start.to_s
- assert_equal time.to_s, foo.finish.to_s
- assert_equal 000000, foo.start.usec
- assert_equal 999900, foo.finish.usec
- end
- def test_schema_dump_includes_time_precision
- @connection.create_table(:foos, force: true) do |t|
- t.time :start, precision: 4
- t.time :finish, precision: 6
+ def test_invalid_time_precision_raises_error
+ assert_raises ActiveRecord::ActiveRecordError do
+ @connection.create_table(:foos, force: true) do |t|
+ t.time :start, precision: 7
+ t.time :finish, precision: 7
+ end
+ end
end
- output = dump_table_schema("foos")
- assert_match %r{t\.time\s+"start",\s+precision: 4$}, output
- assert_match %r{t\.time\s+"finish",\s+precision: 6$}, output
- end
- if current_adapter?(:PostgreSQLAdapter)
- def test_time_precision_with_zero_should_be_dumped
+ def test_formatting_time_according_to_precision
@connection.create_table(:foos, force: true) do |t|
t.time :start, precision: 0
- t.time :finish, precision: 0
+ t.time :finish, precision: 4
end
- output = dump_table_schema("foos")
- assert_match %r{t\.time\s+"start",\s+precision: 0$}, output
- assert_match %r{t\.time\s+"finish",\s+precision: 0$}, output
+ time = ::Time.utc(2000, 1, 1, 12, 30, 0, 999999)
+ Foo.create!(start: time, finish: time)
+ assert foo = Foo.find_by(start: time)
+ assert_equal 1, Foo.where(finish: time).count
+ assert_equal time.to_s, foo.start.to_s
+ assert_equal time.to_s, foo.finish.to_s
+ assert_equal 000000, foo.start.usec
+ assert_equal 999900, foo.finish.usec
end
- end
- private
-
- def database_datetime_precision(table_name, column_name)
- results = @connection.exec_query("SELECT column_name, datetime_precision FROM information_schema.columns WHERE table_name = '#{table_name}'")
- result = results.find do |result_hash|
- result_hash["column_name"] == column_name
+ def test_schema_dump_includes_time_precision
+ @connection.create_table(:foos, force: true) do |t|
+ t.time :start, precision: 4
+ t.time :finish, precision: 6
+ end
+ output = dump_table_schema("foos")
+ assert_match %r{t\.time\s+"start",\s+precision: 4$}, output
+ assert_match %r{t\.time\s+"finish",\s+precision: 6$}, output
end
- result && result["datetime_precision"].to_i
- end
- def activerecord_column_option(tablename, column_name, option)
- result = @connection.columns(tablename).find do |column|
- column.name == column_name
+ 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
+ t.time :finish, precision: 0
+ end
+ output = dump_table_schema("foos")
+ assert_match %r{t\.time\s+"start",\s+precision: 0$}, output
+ assert_match %r{t\.time\s+"finish",\s+precision: 0$}, output
+ end
end
- result && result.send(option)
end
end
-end
diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb
index 5dab32995c..75ecd6fc40 100644
--- a/activerecord/test/cases/timestamp_test.rb
+++ b/activerecord/test/cases/timestamp_test.rb
@@ -1,12 +1,14 @@
-require 'cases/helper'
-require 'support/ddl_helper'
-require 'models/developer'
-require 'models/computer'
-require 'models/owner'
-require 'models/pet'
-require 'models/toy'
-require 'models/car'
-require 'models/task'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "support/ddl_helper"
+require "models/developer"
+require "models/computer"
+require "models/owner"
+require "models/pet"
+require "models/toy"
+require "models/car"
+require "models/task"
class TimestampTest < ActiveRecord::TestCase
fixtures :developers, :owners, :pets, :toys, :cars, :tasks
@@ -38,8 +40,8 @@ class TimestampTest < ActiveRecord::TestCase
assert_not_equal @previously_updated_at, @developer.updated_at
assert_equal previous_salary + 10000, @developer.salary
- assert @developer.salary_changed?, 'developer salary should have changed'
- assert @developer.changed?, 'developer should be marked as changed'
+ assert @developer.salary_changed?, "developer salary should have changed"
+ assert @developer.changed?, "developer should be marked as changed"
@developer.reload
assert_equal previous_salary, @developer.salary
end
@@ -74,36 +76,51 @@ class TimestampTest < ActiveRecord::TestCase
end
def test_touching_updates_timestamp_with_given_time
- previously_updated_at = @developer.updated_at
- new_time = Time.utc(2015, 2, 16, 0, 0, 0)
- @developer.touch(time: new_time)
+ previously_updated_at = @developer.updated_at
+ new_time = Time.utc(2015, 2, 16, 0, 0, 0)
+ @developer.touch(time: new_time)
- assert_not_equal previously_updated_at, @developer.updated_at
- assert_equal new_time, @developer.updated_at
+ assert_not_equal previously_updated_at, @developer.updated_at
+ assert_equal new_time, @developer.updated_at
end
def test_touching_an_attribute_updates_timestamp
previously_created_at = @developer.created_at
- @developer.touch(:created_at)
+ travel(1.second) do
+ @developer.touch(:created_at)
+ end
- assert !@developer.created_at_changed? , 'created_at should not be changed'
- assert !@developer.changed?, 'record should not be changed'
+ assert_not @developer.created_at_changed?, "created_at should not be changed"
+ assert_not @developer.changed?, "record should not be changed"
assert_not_equal previously_created_at, @developer.created_at
assert_not_equal @previously_updated_at, @developer.updated_at
end
+ def test_touching_update_at_attribute_as_symbol_updates_timestamp
+ travel(1.second) do
+ @developer.touch(:updated_at)
+ end
+
+ assert_not @developer.updated_at_changed?
+ assert_not @developer.changed?
+ assert_not_equal @previously_updated_at, @developer.updated_at
+ end
+
def test_touching_an_attribute_updates_it
task = Task.first
previous_value = task.ending
task.touch(:ending)
+
+ now = Time.now.change(usec: 0)
+
assert_not_equal previous_value, task.ending
- assert_in_delta Time.now, task.ending, 1
+ assert_in_delta now, task.ending, 1
end
def test_touching_an_attribute_updates_timestamp_with_given_time
- previously_updated_at = @developer.updated_at
+ previously_updated_at = @developer.updated_at
previously_created_at = @developer.created_at
- new_time = Time.utc(2015, 2, 16, 4, 54, 0)
+ new_time = Time.utc(2015, 2, 16, 4, 54, 0)
@developer.touch(:created_at, time: new_time)
assert_not_equal previously_created_at, @developer.created_at
@@ -118,10 +135,12 @@ class TimestampTest < ActiveRecord::TestCase
previous_ending = task.ending
task.touch(:starting, :ending)
+ now = Time.now.change(usec: 0)
+
assert_not_equal previous_starting, task.starting
assert_not_equal previous_ending, task.ending
- assert_in_delta Time.now, task.starting, 1
- assert_in_delta Time.now, task.ending, 1
+ assert_in_delta now, task.starting, 1
+ assert_in_delta now, task.ending, 1
end
def test_touching_a_record_without_timestamps_is_unexceptional
@@ -130,13 +149,13 @@ class TimestampTest < ActiveRecord::TestCase
def test_touching_a_no_touching_object
Developer.no_touching do
- assert @developer.no_touching?
- assert !@owner.no_touching?
+ assert_predicate @developer, :no_touching?
+ assert_not_predicate @owner, :no_touching?
@developer.touch
end
- assert !@developer.no_touching?
- assert !@owner.no_touching?
+ assert_not_predicate @developer, :no_touching?
+ assert_not_predicate @owner, :no_touching?
assert_equal @previously_updated_at, @developer.updated_at
end
@@ -153,26 +172,26 @@ class TimestampTest < ActiveRecord::TestCase
def test_global_no_touching
ActiveRecord::Base.no_touching do
- assert @developer.no_touching?
- assert @owner.no_touching?
+ assert_predicate @developer, :no_touching?
+ assert_predicate @owner, :no_touching?
@developer.touch
end
- assert !@developer.no_touching?
- assert !@owner.no_touching?
+ assert_not_predicate @developer, :no_touching?
+ assert_not_predicate @owner, :no_touching?
assert_equal @previously_updated_at, @developer.updated_at
end
def test_no_touching_threadsafe
Thread.new do
Developer.no_touching do
- assert @developer.no_touching?
+ assert_predicate @developer, :no_touching?
sleep(1)
end
end
- assert !@developer.no_touching?
+ assert_not_predicate @developer, :no_touching?
end
def test_no_touching_with_callbacks
@@ -199,8 +218,10 @@ class TimestampTest < ActiveRecord::TestCase
owner = pet.owner
previously_owner_updated_at = owner.updated_at
- pet.name = "Fluffy the Third"
- pet.save
+ travel(1.second) do
+ pet.name = "Fluffy the Third"
+ pet.save
+ end
assert_not_equal previously_owner_updated_at, pet.owner.updated_at
end
@@ -210,27 +231,29 @@ class TimestampTest < ActiveRecord::TestCase
owner = pet.owner
previously_owner_updated_at = owner.updated_at
- pet.destroy
+ travel(1.second) do
+ pet.destroy
+ end
assert_not_equal previously_owner_updated_at, pet.owner.updated_at
end
def test_saving_a_new_record_belonging_to_invalid_parent_with_touch_should_not_raise_exception
klass = Class.new(Owner) do
- def self.name; 'Owner'; end
+ def self.name; "Owner"; end
validate { errors.add(:base, :invalid) }
end
pet = Pet.new(owner: klass.new)
pet.save!
- assert pet.owner.new_record?
+ assert_predicate pet.owner, :new_record?
end
def test_saving_a_record_with_a_belongs_to_that_specifies_touching_a_specific_attribute_the_parent_should_update_that_attribute
klass = Class.new(ActiveRecord::Base) do
- def self.name; 'Pet'; end
- belongs_to :owner, :touch => :happy_at
+ def self.name; "Pet"; end
+ belongs_to :owner, touch: :happy_at
end
pet = klass.first
@@ -245,8 +268,8 @@ class TimestampTest < ActiveRecord::TestCase
def test_touching_a_record_with_a_belongs_to_that_uses_a_counter_cache_should_update_the_parent
klass = Class.new(ActiveRecord::Base) do
- def self.name; 'Pet'; end
- belongs_to :owner, :counter_cache => :use_count, :touch => true
+ def self.name; "Pet"; end
+ belongs_to :owner, counter_cache: :use_count, touch: true
end
pet = klass.first
@@ -254,16 +277,18 @@ class TimestampTest < ActiveRecord::TestCase
owner.update_columns(happy_at: 3.days.ago)
previously_owner_updated_at = owner.updated_at
- pet.name = "I'm a parrot"
- pet.save
+ travel(1.second) do
+ pet.name = "I'm a parrot"
+ pet.save
+ end
assert_not_equal previously_owner_updated_at, pet.owner.updated_at
end
def test_touching_a_record_touches_parent_record_and_grandparent_record
klass = Class.new(ActiveRecord::Base) do
- def self.name; 'Toy'; end
- belongs_to :pet, :touch => true
+ def self.name; "Toy"; end
+ belongs_to :pet, touch: true
end
toy = klass.first
@@ -280,12 +305,12 @@ class TimestampTest < ActiveRecord::TestCase
def test_touching_a_record_touches_polymorphic_record
klass = Class.new(ActiveRecord::Base) do
- def self.name; 'Toy'; end
+ def self.name; "Toy"; end
end
wheel_klass = Class.new(ActiveRecord::Base) do
- def self.name; 'Wheel'; end
- belongs_to :wheelable, :polymorphic => true, :touch => true
+ def self.name; "Wheel"; end
+ belongs_to :wheelable, polymorphic: true, touch: true
end
toy = klass.first
@@ -302,7 +327,7 @@ class TimestampTest < ActiveRecord::TestCase
def test_changing_parent_of_a_record_touches_both_new_and_old_parent_record
klass = Class.new(ActiveRecord::Base) do
- def self.name; 'Toy'; end
+ def self.name; "Toy"; end
belongs_to :pet, touch: true
end
@@ -328,12 +353,12 @@ class TimestampTest < ActiveRecord::TestCase
def test_changing_parent_of_a_record_touches_both_new_and_old_polymorphic_parent_record_changes_within_same_class
car_class = Class.new(ActiveRecord::Base) do
- def self.name; 'Car'; end
+ def self.name; "Car"; end
end
wheel_class = Class.new(ActiveRecord::Base) do
- def self.name; 'Wheel'; end
- belongs_to :wheelable, :polymorphic => true, :touch => true
+ def self.name; "Wheel"; end
+ belongs_to :wheelable, polymorphic: true, touch: true
end
car1 = car_class.find(1)
@@ -355,16 +380,16 @@ class TimestampTest < ActiveRecord::TestCase
def test_changing_parent_of_a_record_touches_both_new_and_old_polymorphic_parent_record_changes_with_other_class
car_class = Class.new(ActiveRecord::Base) do
- def self.name; 'Car'; end
+ def self.name; "Car"; end
end
toy_class = Class.new(ActiveRecord::Base) do
- def self.name; 'Toy'; end
+ def self.name; "Toy"; end
end
wheel_class = Class.new(ActiveRecord::Base) do
- def self.name; 'Wheel'; end
- belongs_to :wheelable, :polymorphic => true, :touch => true
+ def self.name; "Wheel"; end
+ belongs_to :wheelable, polymorphic: true, touch: true
end
car = car_class.find(1)
@@ -386,7 +411,7 @@ class TimestampTest < ActiveRecord::TestCase
def test_clearing_association_touches_the_old_record
klass = Class.new(ActiveRecord::Base) do
- def self.name; 'Toy'; end
+ def self.name; "Toy"; end
belongs_to :pet, touch: true
end
@@ -406,56 +431,30 @@ class TimestampTest < ActiveRecord::TestCase
def test_timestamp_column_values_are_present_in_the_callbacks
klass = Class.new(ActiveRecord::Base) do
- self.table_name = 'people'
+ self.table_name = "people"
before_create do
- self.born_at = self.created_at
+ self.born_at = created_at
end
end
- person = klass.create first_name: 'David'
+ person = klass.create first_name: "David"
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)
- end
-
- def test_index_is_created_for_both_timestamps
- ActiveRecord::Base.connection.create_table(:foos, force: true) do |t|
- t.timestamps(:foos, null: true, index: true)
- end
-
- indexes = ActiveRecord::Base.connection.indexes('foos')
- assert_equal ['created_at', 'updated_at'], indexes.flat_map(&:columns).sort
- ensure
- ActiveRecord::Base.connection.drop_table(:foos)
+ assert_equal ["created_at", "updated_at"], toy.send(:all_timestamp_attributes_in_model)
end
end
@@ -475,4 +474,15 @@ class TimestampsWithoutTransactionTest < ActiveRecord::TestCase
assert_nil post.updated_at
end
end
+
+ def test_index_is_created_for_both_timestamps
+ ActiveRecord::Base.connection.create_table(:foos, force: true) do |t|
+ t.timestamps null: true, index: true
+ end
+
+ indexes = ActiveRecord::Base.connection.indexes("foos")
+ assert_equal ["created_at", "updated_at"], indexes.flat_map(&:columns).sort
+ ensure
+ ActiveRecord::Base.connection.drop_table(:foos)
+ end
end
diff --git a/activerecord/test/cases/touch_later_test.rb b/activerecord/test/cases/touch_later_test.rb
index 49ada22529..cd3d5ed7d1 100644
--- a/activerecord/test/cases/touch_later_test.rb
+++ b/activerecord/test/cases/touch_later_test.rb
@@ -1,9 +1,11 @@
-require 'cases/helper'
-require 'models/invoice'
-require 'models/line_item'
-require 'models/topic'
-require 'models/node'
-require 'models/tree'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/invoice"
+require "models/line_item"
+require "models/topic"
+require "models/node"
+require "models/tree"
class TouchLaterTest < ActiveRecord::TestCase
fixtures :nodes, :trees
@@ -11,7 +13,7 @@ class TouchLaterTest < ActiveRecord::TestCase
def test_touch_laster_raise_if_non_persisted
invoice = Invoice.new
Invoice.transaction do
- refute invoice.persisted?
+ assert_not_predicate invoice, :persisted?
assert_raises(ActiveRecord::ActiveRecordError) do
invoice.touch_later
end
@@ -21,7 +23,16 @@ class TouchLaterTest < ActiveRecord::TestCase
def test_touch_later_dont_set_dirty_attributes
invoice = Invoice.create!
invoice.touch_later
- refute invoice.changed?
+ assert_not_predicate invoice, :changed?
+ end
+
+ def test_touch_later_respects_no_touching_policy
+ time = Time.now.utc - 25.days
+ topic = Topic.create!(updated_at: time, created_at: time)
+ Topic.no_touching do
+ topic.touch_later
+ end
+ assert_equal time.to_i, topic.updated_at.to_i
end
def test_touch_later_update_the_attributes
@@ -72,7 +83,7 @@ class TouchLaterTest < ActiveRecord::TestCase
end
def test_touch_touches_immediately_with_a_custom_time
- time = Time.now.utc - 25.days
+ time = (Time.now.utc - 25.days).change(nsec: 0)
topic = Topic.create!(updated_at: time, created_at: time)
assert_equal time, topic.updated_at
assert_equal time, topic.created_at
@@ -89,22 +100,20 @@ class TouchLaterTest < ActiveRecord::TestCase
def test_touch_later_dont_hit_the_db
invoice = Invoice.create!
- assert_queries(0) do
+ assert_no_queries do
invoice.touch_later
end
end
def test_touching_three_deep
- skip "Pending from #19324"
-
previous_tree_updated_at = trees(:root).updated_at
previous_grandparent_updated_at = nodes(:grandparent).updated_at
previous_parent_updated_at = nodes(:parent_a).updated_at
previous_child_updated_at = nodes(:child_one_of_a).updated_at
- travel 5.seconds
-
- Node.create! parent: nodes(:child_one_of_a), tree: trees(:root)
+ travel 5.seconds do
+ Node.create! parent: nodes(:child_one_of_a), tree: trees(:root)
+ end
assert_not_equal nodes(:child_one_of_a).reload.updated_at, previous_child_updated_at
assert_not_equal nodes(:parent_a).reload.updated_at, previous_parent_updated_at
diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb
index f2229939c8..aa6b7915a2 100644
--- a/activerecord/test/cases/transaction_callbacks_test.rb
+++ b/activerecord/test/cases/transaction_callbacks_test.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/owner'
-require 'models/pet'
-require 'models/topic'
+require "models/owner"
+require "models/pet"
+require "models/topic"
class TransactionCallbacksTest < ActiveRecord::TestCase
fixtures :topics, :owners, :pets
@@ -17,7 +19,7 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
attr_accessor :save_on_after_create
after_create do
- self.save! if save_on_after_create
+ save! if save_on_after_create
end
def history
@@ -34,10 +36,11 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
has_many :replies, class_name: "ReplyWithCallbacks", foreign_key: "parent_id"
+ before_commit { |record| record.do_before_commit(nil) }
after_commit { |record| record.do_after_commit(nil) }
- after_commit(on: :create) { |record| record.do_after_commit(:create) }
- after_commit(on: :update) { |record| record.do_after_commit(:update) }
- after_commit(on: :destroy) { |record| record.do_after_commit(:destroy) }
+ after_create_commit { |record| record.do_after_commit(:create) }
+ after_update_commit { |record| record.do_after_commit(:update) }
+ after_destroy_commit { |record| record.do_after_commit(:destroy) }
after_rollback { |record| record.do_after_rollback(nil) }
after_rollback(on: :create) { |record| record.do_after_rollback(:create) }
after_rollback(on: :update) { |record| record.do_after_rollback(:update) }
@@ -47,6 +50,12 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
@history ||= []
end
+ def before_commit_block(on = nil, &block)
+ @before_commit ||= {}
+ @before_commit[on] ||= []
+ @before_commit[on] << block
+ end
+
def after_commit_block(on = nil, &block)
@after_commit ||= {}
@after_commit[on] ||= []
@@ -59,14 +68,19 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
@after_rollback[on] << block
end
+ def do_before_commit(on)
+ blocks = @before_commit[on] if defined?(@before_commit)
+ blocks.each { |b| b.call(self) } if blocks
+ end
+
def do_after_commit(on)
blocks = @after_commit[on] if defined?(@after_commit)
- blocks.each{|b| b.call(self)} if blocks
+ blocks.each { |b| b.call(self) } if blocks
end
def do_after_rollback(on)
blocks = @after_rollback[on] if defined?(@after_rollback)
- blocks.each{|b| b.call(self)} if blocks
+ blocks.each { |b| b.call(self) } if blocks
end
end
@@ -74,9 +88,23 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
@first = TopicWithCallbacks.find(1)
end
+ # FIXME: Test behavior, not implementation.
+ def test_before_commit_exception_should_pop_transaction_stack
+ @first.before_commit_block { raise "better pop this txn from the stack!" }
+
+ original_txn = @first.class.connection.current_transaction
+
+ begin
+ @first.save!
+ fail
+ rescue
+ assert_equal original_txn, @first.class.connection.current_transaction
+ end
+ end
+
def test_call_after_commit_after_transaction_commits
- @first.after_commit_block{|r| r.history << :after_commit}
- @first.after_rollback_block{|r| r.history << :after_rollback}
+ @first.after_commit_block { |r| r.history << :after_commit }
+ @first.after_rollback_block { |r| r.history << :after_rollback }
@first.save!
assert_equal [:after_commit], @first.history
@@ -97,7 +125,7 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
end
def test_only_call_after_commit_on_create_after_transaction_commits_for_new_record
- new_record = TopicWithCallbacks.new(:title => "New topic", :written_on => Date.today)
+ new_record = TopicWithCallbacks.new(title: "New topic", written_on: Date.today)
add_transaction_execution_blocks new_record
new_record.save!
@@ -105,17 +133,34 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
end
def test_only_call_after_commit_on_create_after_transaction_commits_for_new_record_if_create_succeeds_creating_through_association
- topic = TopicWithCallbacks.create!(:title => "New topic", :written_on => Date.today)
+ topic = TopicWithCallbacks.create!(title: "New topic", written_on: Date.today)
reply = topic.replies.create
assert_equal [], reply.history
end
+ def test_only_call_after_commit_on_destroy_after_transaction_commits_for_destroyed_new_record
+ new_record = TopicWithCallbacks.new(title: "New topic", written_on: Date.today)
+ add_transaction_execution_blocks new_record
+
+ new_record.destroy
+ assert_equal [:commit_on_destroy], new_record.history
+ end
+
+ def test_save_in_after_create_commit_wont_invoke_extra_after_create_commit
+ new_record = TopicWithCallbacks.new(title: "New topic", written_on: Date.today)
+ add_transaction_execution_blocks new_record
+ new_record.after_commit_block(:create) { |r| r.save! }
+
+ new_record.save!
+ assert_equal [:commit_on_create, :commit_on_update], new_record.history
+ end
+
def test_only_call_after_commit_on_create_and_doesnt_leaky
- r = ReplyWithCallbacks.new(content: 'foo')
+ r = ReplyWithCallbacks.new(content: "foo")
r.save_on_after_create = true
r.save!
- r.content = 'bar'
+ r.content = "bar"
r.save!
r.save!
assert_equal [:commit_on_create], r.history
@@ -129,21 +174,21 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
end
def test_only_call_after_commit_on_top_level_transactions
- @first.after_commit_block{|r| r.history << :after_commit}
- assert @first.history.empty?
+ @first.after_commit_block { |r| r.history << :after_commit }
+ assert_empty @first.history
@first.transaction do
@first.transaction(requires_new: true) do
@first.touch
end
- assert @first.history.empty?
+ assert_empty @first.history
end
assert_equal [:after_commit], @first.history
end
def test_call_after_rollback_after_transaction_rollsback
- @first.after_commit_block{|r| r.history << :after_commit}
- @first.after_rollback_block{|r| r.history << :after_rollback}
+ @first.after_commit_block { |r| r.history << :after_commit }
+ @first.after_rollback_block { |r| r.history << :after_rollback }
Topic.transaction do
@first.save!
@@ -187,7 +232,7 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
end
def test_only_call_after_rollback_on_create_after_transaction_rollsback_for_new_record
- new_record = TopicWithCallbacks.new(:title => "New topic", :written_on => Date.today)
+ new_record = TopicWithCallbacks.new(title: "New topic", written_on: Date.today)
add_transaction_execution_blocks new_record
Topic.transaction do
@@ -217,20 +262,20 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
end
def test_only_call_after_rollback_on_records_rolled_back_to_a_savepoint
- def @first.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end
- def @first.commits(i=0); @commits ||= 0; @commits += i if i; end
- @first.after_rollback_block{|r| r.rollbacks(1)}
- @first.after_commit_block{|r| r.commits(1)}
+ def @first.rollbacks(i = 0); @rollbacks ||= 0; @rollbacks += i if i; end
+ def @first.commits(i = 0); @commits ||= 0; @commits += i if i; end
+ @first.after_rollback_block { |r| r.rollbacks(1) }
+ @first.after_commit_block { |r| r.commits(1) }
second = TopicWithCallbacks.find(3)
- def second.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end
- def second.commits(i=0); @commits ||= 0; @commits += i if i; end
- second.after_rollback_block{|r| r.rollbacks(1)}
- second.after_commit_block{|r| r.commits(1)}
+ def second.rollbacks(i = 0); @rollbacks ||= 0; @rollbacks += i if i; end
+ def second.commits(i = 0); @commits ||= 0; @commits += i if i; end
+ second.after_rollback_block { |r| r.rollbacks(1) }
+ second.after_commit_block { |r| r.commits(1) }
Topic.transaction do
@first.save!
- Topic.transaction(:requires_new => true) do
+ Topic.transaction(requires_new: true) do
second.save!
raise ActiveRecord::Rollback
end
@@ -243,19 +288,19 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
end
def test_only_call_after_rollback_on_records_rolled_back_to_a_savepoint_when_release_savepoint_fails
- def @first.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end
- def @first.commits(i=0); @commits ||= 0; @commits += i if i; end
+ def @first.rollbacks(i = 0); @rollbacks ||= 0; @rollbacks += i if i; end
+ def @first.commits(i = 0); @commits ||= 0; @commits += i if i; end
- @first.after_rollback_block{|r| r.rollbacks(1)}
- @first.after_commit_block{|r| r.commits(1)}
+ @first.after_rollback_block { |r| r.rollbacks(1) }
+ @first.after_commit_block { |r| r.commits(1) }
Topic.transaction do
@first.save
- Topic.transaction(:requires_new => true) do
+ Topic.transaction(requires_new: true) do
@first.save!
raise ActiveRecord::Rollback
end
- Topic.transaction(:requires_new => true) do
+ Topic.transaction(requires_new: true) do
@first.save!
raise ActiveRecord::Rollback
end
@@ -266,7 +311,7 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
end
def test_after_commit_callback_should_not_swallow_errors
- @first.after_commit_block{ fail "boom" }
+ @first.after_commit_block { fail "boom" }
assert_raises(RuntimeError) do
Topic.transaction do
@first.save!
@@ -277,8 +322,8 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
def test_after_commit_callback_when_raise_should_not_restore_state
first = TopicWithCallbacks.new
second = TopicWithCallbacks.new
- first.after_commit_block{ fail "boom" }
- second.after_commit_block{ fail "boom" }
+ first.after_commit_block { fail "boom" }
+ second.after_commit_block { fail "boom" }
begin
Topic.transaction do
@@ -296,7 +341,7 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
def test_after_rollback_callback_should_not_swallow_errors_when_set_to_raise
error_class = Class.new(StandardError)
- @first.after_rollback_block{ raise error_class }
+ @first.after_rollback_block { raise error_class }
assert_raises(error_class) do
Topic.transaction do
@first.save!
@@ -310,8 +355,8 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
first = TopicWithCallbacks.new
second = TopicWithCallbacks.new
- first.after_rollback_block{ raise error_class }
- second.after_rollback_block{ raise error_class }
+ first.after_rollback_block { raise error_class }
+ second.after_rollback_block { raise error_class }
begin
Topic.transaction do
@@ -329,16 +374,36 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
def test_after_rollback_callbacks_should_validate_on_condition
assert_raise(ArgumentError) { Topic.after_rollback(on: :save) }
- e = assert_raise(ArgumentError) { Topic.after_rollback(on: 'create') }
+ e = assert_raise(ArgumentError) { Topic.after_rollback(on: "create") }
assert_match(/:on conditions for after_commit and after_rollback callbacks have to be one of \[:create, :destroy, :update\]/, e.message)
end
def test_after_commit_callbacks_should_validate_on_condition
assert_raise(ArgumentError) { Topic.after_commit(on: :save) }
- e = assert_raise(ArgumentError) { Topic.after_commit(on: 'create') }
+ e = assert_raise(ArgumentError) { Topic.after_commit(on: "create") }
assert_match(/:on conditions for after_commit and after_rollback callbacks have to be one of \[:create, :destroy, :update\]/, e.message)
end
+ def test_after_commit_chain_not_called_on_errors
+ record_1 = TopicWithCallbacks.create!
+ record_2 = TopicWithCallbacks.create!
+ record_3 = TopicWithCallbacks.create!
+ callbacks = []
+ record_1.after_commit_block { raise }
+ record_2.after_commit_block { callbacks << record_2.id }
+ record_3.after_commit_block { callbacks << record_3.id }
+ begin
+ TopicWithCallbacks.transaction do
+ record_1.save!
+ record_2.save!
+ record_3.save!
+ end
+ rescue
+ # From record_1.after_commit
+ end
+ assert_equal [], callbacks
+ end
+
def test_saving_a_record_with_a_belongs_to_that_specifies_touching_the_parent_should_call_callbacks_on_the_parent_object
pet = Pet.first
owner = pet.owner
@@ -366,6 +431,28 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
end
end
+class TransactionAfterCommitCallbacksWithOptimisticLockingTest < ActiveRecord::TestCase
+ class PersonWithCallbacks < ActiveRecord::Base
+ self.table_name = :people
+
+ after_create_commit { |record| record.history << :commit_on_create }
+ after_update_commit { |record| record.history << :commit_on_update }
+ after_destroy_commit { |record| record.history << :commit_on_destroy }
+
+ def history
+ @history ||= []
+ end
+ end
+
+ def test_after_commit_callbacks_with_optimistic_locking
+ person = PersonWithCallbacks.create!(first_name: "first name")
+ person.update!(first_name: "another name")
+ person.destroy
+
+ assert_equal [:commit_on_create, :commit_on_update, :commit_on_destroy], person.history
+ end
+end
+
class CallbacksOnMultipleActionsTest < ActiveRecord::TestCase
self.use_transactional_tests = false
@@ -423,9 +510,52 @@ class CallbacksOnMultipleActionsTest < ActiveRecord::TestCase
end
end
+class CallbacksOnDestroyUpdateActionRaceTest < ActiveRecord::TestCase
+ class TopicWithHistory < ActiveRecord::Base
+ self.table_name = :topics
+
+ def self.clear_history
+ @@history = []
+ end
-class TransactionEnrollmentCallbacksTest < ActiveRecord::TestCase
+ 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
@@ -444,16 +574,16 @@ class TransactionEnrollmentCallbacksTest < ActiveRecord::TestCase
def test_commit_does_not_run_transactions_callbacks_without_enrollment
@topic.transaction do
- @topic.content = 'foo'
+ @topic.content = "foo"
@topic.save!
end
- assert @topic.history.empty?
+ assert_empty @topic.history
end
def test_commit_run_transactions_callbacks_with_explicit_enrollment
@topic.transaction do
2.times do
- @topic.content = 'foo'
+ @topic.content = "foo"
@topic.save!
end
@topic.class.connection.add_transaction_record(@topic)
@@ -461,19 +591,30 @@ class TransactionEnrollmentCallbacksTest < ActiveRecord::TestCase
assert_equal [:before_commit, :after_commit], @topic.history
end
+ def test_commit_run_transactions_callbacks_with_nested_transactions
+ @topic.transaction do
+ @topic.transaction(requires_new: true) do
+ @topic.content = "foo"
+ @topic.save!
+ @topic.class.connection.add_transaction_record(@topic)
+ end
+ end
+ assert_equal [:before_commit, :after_commit], @topic.history
+ end
+
def test_rollback_does_not_run_transactions_callbacks_without_enrollment
@topic.transaction do
- @topic.content = 'foo'
+ @topic.content = "foo"
@topic.save!
raise ActiveRecord::Rollback
end
- assert @topic.history.empty?
+ assert_empty @topic.history
end
def test_rollback_run_transactions_callbacks_with_explicit_enrollment
@topic.transaction do
2.times do
- @topic.content = 'foo'
+ @topic.content = "foo"
@topic.save!
end
@topic.class.connection.add_transaction_record(@topic)
@@ -482,3 +623,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/transaction_isolation_test.rb b/activerecord/test/cases/transaction_isolation_test.rb
index 2f7d208ed2..2932969412 100644
--- a/activerecord/test/cases/transaction_isolation_test.rb
+++ b/activerecord/test/cases/transaction_isolation_test.rb
@@ -1,4 +1,6 @@
-require 'cases/helper'
+# frozen_string_literal: true
+
+require "cases/helper"
unless ActiveRecord::Base.connection.supports_transaction_isolation?
class TransactionIsolationUnsupportedTest < ActiveRecord::TestCase
@@ -9,22 +11,20 @@ unless ActiveRecord::Base.connection.supports_transaction_isolation?
test "setting the isolation level raises an error" do
assert_raises(ActiveRecord::TransactionIsolationError) do
- Tag.transaction(isolation: :serializable) { }
+ Tag.transaction(isolation: :serializable) { Tag.connection.materialize_transactions }
end
end
end
-end
-
-if ActiveRecord::Base.connection.supports_transaction_isolation?
+else
class TransactionIsolationTest < ActiveRecord::TestCase
self.use_transactional_tests = false
class Tag < ActiveRecord::Base
- self.table_name = 'tags'
+ self.table_name = "tags"
end
class Tag2 < ActiveRecord::Base
- self.table_name = 'tags'
+ self.table_name = "tags"
end
setup do
@@ -63,18 +63,18 @@ if ActiveRecord::Base.connection.supports_transaction_isolation?
# We are testing that a nonrepeatable read does not happen
if ActiveRecord::Base.connection.transaction_isolation_levels.include?(:repeatable_read)
test "repeatable read" do
- tag = Tag.create(name: 'jon')
+ tag = Tag.create(name: "jon")
Tag.transaction(isolation: :repeatable_read) do
tag.reload
- Tag2.find(tag.id).update(name: 'emily')
+ Tag2.find(tag.id).update(name: "emily")
tag.reload
- assert_equal 'jon', tag.name
+ assert_equal "jon", tag.name
end
tag.reload
- assert_equal 'emily', tag.name
+ assert_equal "emily", tag.name
end
end
diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb
index 2468a91969..50740054f7 100644
--- a/activerecord/test/cases/transactions_test.rb
+++ b/activerecord/test/cases/transactions_test.rb
@@ -1,16 +1,18 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/topic'
-require 'models/reply'
-require 'models/developer'
-require 'models/computer'
-require 'models/book'
-require 'models/author'
-require 'models/post'
-require 'models/movie'
+require "models/topic"
+require "models/reply"
+require "models/developer"
+require "models/computer"
+require "models/book"
+require "models/author"
+require "models/post"
+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)
@@ -18,22 +20,22 @@ class TransactionTest < ActiveRecord::TestCase
def test_persisted_in_a_model_with_custom_primary_key_after_failed_save
movie = Movie.create
- assert !movie.persisted?
+ assert_not_predicate movie, :persisted?
end
def test_raise_after_destroy
- assert_not @first.frozen?
+ assert_not_predicate @first, :frozen?
assert_raises(RuntimeError) {
Topic.transaction do
@first.destroy
- assert @first.frozen?
+ assert_predicate @first, :frozen?
raise
end
}
assert @first.reload
- assert_not @first.frozen?
+ assert_not_predicate @first, :frozen?
end
def test_successful
@@ -45,7 +47,7 @@ class TransactionTest < ActiveRecord::TestCase
end
assert Topic.find(1).approved?, "First should have been approved"
- assert !Topic.find(2).approved?, "Second should have been unapproved"
+ assert_not Topic.find(2).approved?, "Second should have been unapproved"
end
def transaction_with_return
@@ -58,6 +60,11 @@ class TransactionTest < ActiveRecord::TestCase
end
end
+ def test_add_to_null_transaction
+ topic = Topic.new
+ topic.add_to_transaction
+ end
+
def test_successful_with_return
committed = false
@@ -73,7 +80,7 @@ class TransactionTest < ActiveRecord::TestCase
assert committed
assert Topic.find(1).approved?, "First should have been approved"
- assert !Topic.find(2).approved?, "Second should have been unapproved"
+ assert_not Topic.find(2).approved?, "Second should have been unapproved"
ensure
Topic.connection.class_eval do
remove_method :commit_db_transaction
@@ -93,7 +100,7 @@ class TransactionTest < ActiveRecord::TestCase
end
Topic.transaction do
- @first.approved = true
+ @first.approved = true
@first.save!
end
@@ -114,7 +121,7 @@ class TransactionTest < ActiveRecord::TestCase
end
assert Topic.find(1).approved?, "First should have been approved"
- assert !Topic.find(2).approved?, "Second should have been unapproved"
+ assert_not Topic.find(2).approved?, "Second should have been unapproved"
end
def test_failing_on_exception
@@ -131,41 +138,41 @@ class TransactionTest < ActiveRecord::TestCase
end
assert @first.approved?, "First should still be changed in the objects"
- assert !@second.approved?, "Second should still be changed in the objects"
+ assert_not @second.approved?, "Second should still be changed in the objects"
- assert !Topic.find(1).approved?, "First shouldn't have been approved"
+ assert_not Topic.find(1).approved?, "First shouldn't have been approved"
assert Topic.find(2).approved?, "Second should still be approved"
end
def test_raising_exception_in_callback_rollbacks_in_save
def @first.after_save_for_transaction
- raise 'Make the transaction rollback'
+ raise "Make the transaction rollback"
end
@first.approved = true
e = assert_raises(RuntimeError) { @first.save }
assert_equal "Make the transaction rollback", e.message
- assert !Topic.find(1).approved?
+ assert_not_predicate Topic.find(1), :approved?
end
def test_rolling_back_in_a_callback_rollbacks_before_save
def @first.before_save_for_transaction
raise ActiveRecord::Rollback
end
- assert !@first.approved
+ assert_not @first.approved
Topic.transaction do
- @first.approved = true
+ @first.approved = true
@first.save!
end
- assert !Topic.find(@first.id).approved?, "Should not commit the approved flag"
+ assert_not Topic.find(@first.id).approved?, "Should not commit the approved flag"
end
def test_raising_exception_in_nested_transaction_restore_state_in_save
topic = Topic.new
def topic.after_save_for_transaction
- raise 'Make the transaction rollback'
+ raise "Make the transaction rollback"
end
assert_raises(RuntimeError) do
@@ -175,13 +182,20 @@ class TransactionTest < ActiveRecord::TestCase
assert topic.new_record?, "#{topic.inspect} should be new record"
end
+ def test_transaction_state_is_cleared_when_record_is_persisted
+ author = Author.create! name: "foo"
+ author.name = nil
+ assert_not author.save
+ assert_not_predicate author, :new_record?
+ end
+
def test_update_should_rollback_on_failure
author = Author.find(1)
posts_count = author.posts.size
assert posts_count > 0
status = author.update(name: nil, post_ids: [])
- assert !status
- assert_equal posts_count, author.posts(true).size
+ assert_not status
+ assert_equal posts_count, author.posts.reload.size
end
def test_update_should_rollback_on_failure!
@@ -191,24 +205,14 @@ class TransactionTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::RecordInvalid) do
author.update!(name: nil, post_ids: [])
end
- assert_equal posts_count, author.posts(true).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
+ assert_equal posts_count, author.posts.reload.size
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
status = @first.destroy
- assert !status
+ assert_not status
@first.reload
assert_equal nbooks_before_destroy, Book.count
end
@@ -218,9 +222,9 @@ class TransactionTest < ActiveRecord::TestCase
send("add_cancelling_before_#{filter}_with_db_side_effect_to_topic", @first)
nbooks_before_save = Book.count
original_author_name = @first.author_name
- @first.author_name += '_this_should_not_end_up_in_the_db'
+ @first.author_name += "_this_should_not_end_up_in_the_db"
status = @first.save
- assert !status
+ assert_not status
assert_equal original_author_name, @first.reload.author_name
assert_equal nbooks_before_save, Book.count
end
@@ -229,7 +233,7 @@ class TransactionTest < ActiveRecord::TestCase
send("add_cancelling_before_#{filter}_with_db_side_effect_to_topic", @first)
nbooks_before_save = Book.count
original_author_name = @first.author_name
- @first.author_name += '_this_should_not_end_up_in_the_db'
+ @first.author_name += "_this_should_not_end_up_in_the_db"
begin
@first.save!
@@ -244,18 +248,18 @@ class TransactionTest < ActiveRecord::TestCase
def test_callback_rollback_in_create
topic = Class.new(Topic) {
def after_create_for_transaction
- raise 'Make the transaction rollback'
+ raise "Make the transaction rollback"
end
}
- new_topic = topic.new(:title => "A new topic",
- :author_name => "Ben",
- :author_email_address => "ben@example.com",
- :written_on => "2003-07-16t15:28:11.2233+01:00",
- :last_read => "2004-04-15",
- :bonus_time => "2005-01-30t15:28:00.00+01:00",
- :content => "Have a nice day",
- :approved => false)
+ new_topic = topic.new(title: "A new topic",
+ author_name: "Ben",
+ author_email_address: "ben@example.com",
+ written_on: "2003-07-16t15:28:11.2233+01:00",
+ last_read: "2004-04-15",
+ bonus_time: "2005-01-30t15:28:00.00+01:00",
+ content: "Have a nice day",
+ approved: false)
new_record_snapshot = !new_topic.persisted?
id_present = new_topic.has_attribute?(Topic.primary_key)
@@ -267,7 +271,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
@@ -279,8 +287,20 @@ class TransactionTest < ActiveRecord::TestCase
end
}
- new_topic = topic.create(:title => "A new topic")
- assert !new_topic.persisted?, "The topic should not be persisted"
+ new_topic = topic.create(title: "A new topic")
+ assert_not new_topic.persisted?, "The topic should not be persisted"
+ assert_nil new_topic.id, "The topic should not have an ID"
+ end
+
+ def test_callback_rollback_in_create_with_rollback_exception
+ topic = Class.new(Topic) {
+ def after_create_for_transaction
+ raise ActiveRecord::Rollback
+ end
+ }
+
+ new_topic = topic.create(title: "A new topic")
+ assert_not new_topic.persisted?, "The topic should not be persisted"
assert_nil new_topic.id, "The topic should not have an ID"
end
@@ -295,7 +315,77 @@ class TransactionTest < ActiveRecord::TestCase
end
assert Topic.find(1).approved?, "First should have been approved"
- assert !Topic.find(2).approved?, "Second should have been unapproved"
+ assert_not Topic.find(2).approved?, "Second should have been unapproved"
+ end
+
+ def test_nested_transaction_with_new_transaction_applies_parent_state_on_rollback
+ topic_one = Topic.new(title: "A new topic")
+ topic_two = Topic.new(title: "Another new topic")
+
+ Topic.transaction do
+ topic_one.save
+
+ Topic.transaction(requires_new: true) do
+ topic_two.save
+
+ assert_predicate topic_one, :persisted?
+ assert_predicate topic_two, :persisted?
+ end
+
+ raise ActiveRecord::Rollback
+ end
+
+ assert_not_predicate topic_one, :persisted?
+ assert_not_predicate topic_two, :persisted?
+ end
+
+ def test_nested_transaction_without_new_transaction_applies_parent_state_on_rollback
+ topic_one = Topic.new(title: "A new topic")
+ topic_two = Topic.new(title: "Another new topic")
+
+ Topic.transaction do
+ topic_one.save
+
+ Topic.transaction do
+ topic_two.save
+
+ assert_predicate topic_one, :persisted?
+ assert_predicate topic_two, :persisted?
+ end
+
+ raise ActiveRecord::Rollback
+ end
+
+ assert_not_predicate topic_one, :persisted?
+ assert_not_predicate topic_two, :persisted?
+ end
+
+ def test_double_nested_transaction_applies_parent_state_on_rollback
+ topic_one = Topic.new(title: "A new topic")
+ topic_two = Topic.new(title: "Another new topic")
+ topic_three = Topic.new(title: "Another new topic of course")
+
+ Topic.transaction do
+ topic_one.save
+
+ Topic.transaction do
+ topic_two.save
+
+ Topic.transaction do
+ topic_three.save
+ end
+ end
+
+ assert_predicate topic_one, :persisted?
+ assert_predicate topic_two, :persisted?
+ assert_predicate topic_three, :persisted?
+
+ raise ActiveRecord::Rollback
+ end
+
+ assert_not_predicate topic_one, :persisted?
+ assert_not_predicate topic_two, :persisted?
+ assert_not_predicate topic_three, :persisted?
end
def test_manually_rolling_back_a_transaction
@@ -309,15 +399,15 @@ class TransactionTest < ActiveRecord::TestCase
end
assert @first.approved?, "First should still be changed in the objects"
- assert !@second.approved?, "Second should still be changed in the objects"
+ assert_not @second.approved?, "Second should still be changed in the objects"
- assert !Topic.find(1).approved?, "First shouldn't have been approved"
+ assert_not Topic.find(1).approved?, "First shouldn't have been approved"
assert Topic.find(2).approved?, "Second should still be approved"
end
def test_invalid_keys_for_transaction
assert_raise ArgumentError do
- Topic.transaction :nested => true do
+ Topic.transaction nested: true do
end
end
end
@@ -330,7 +420,7 @@ class TransactionTest < ActiveRecord::TestCase
@second.save!
begin
- Topic.transaction :requires_new => true do
+ Topic.transaction requires_new: true do
@first.happy = false
@first.save!
raise
@@ -339,8 +429,8 @@ class TransactionTest < ActiveRecord::TestCase
end
end
- assert @first.reload.approved?
- assert !@second.reload.approved?
+ assert_predicate @first.reload, :approved?
+ assert_not_predicate @second.reload, :approved?
end if Topic.connection.supports_savepoints?
def test_force_savepoint_on_instance
@@ -351,7 +441,7 @@ class TransactionTest < ActiveRecord::TestCase
@second.save!
begin
- @second.transaction :requires_new => true do
+ @second.transaction requires_new: true do
@first.happy = false
@first.save!
raise
@@ -360,8 +450,8 @@ class TransactionTest < ActiveRecord::TestCase
end
end
- assert @first.reload.approved?
- assert !@second.reload.approved?
+ assert_predicate @first.reload, :approved?
+ assert_not_predicate @second.reload, :approved?
end if Topic.connection.supports_savepoints?
def test_no_savepoint_in_nested_transaction_without_force
@@ -381,8 +471,8 @@ class TransactionTest < ActiveRecord::TestCase
end
end
- assert !@first.reload.approved?
- assert !@second.reload.approved?
+ assert_not_predicate @first.reload, :approved?
+ assert_not_predicate @second.reload, :approved?
end if Topic.connection.supports_savepoints?
def test_many_savepoints
@@ -391,17 +481,17 @@ class TransactionTest < ActiveRecord::TestCase
@first.save!
begin
- Topic.transaction :requires_new => true do
+ Topic.transaction requires_new: true do
@first.content = "Two"
@first.save!
begin
- Topic.transaction :requires_new => true do
+ Topic.transaction requires_new: true do
@first.content = "Three"
@first.save!
begin
- Topic.transaction :requires_new => true do
+ Topic.transaction requires_new: true do
@first.content = "Four"
@first.save!
raise
@@ -431,19 +521,19 @@ class TransactionTest < ActiveRecord::TestCase
def test_using_named_savepoints
Topic.transaction do
- @first.approved = true
+ @first.approved = true
@first.save!
Topic.connection.create_savepoint("first")
- @first.approved = false
+ @first.approved = false
@first.save!
Topic.connection.rollback_to_savepoint("first")
- assert @first.reload.approved?
+ assert_predicate @first.reload, :approved?
- @first.approved = false
+ @first.approved = false
@first.save!
Topic.connection.release_savepoint("first")
- assert_not @first.reload.approved?
+ assert_not_predicate @first.reload, :approved?
end
end if Topic.connection.supports_savepoints?
@@ -480,27 +570,31 @@ class TransactionTest < ActiveRecord::TestCase
end
def test_rollback_when_commit_raises
- Topic.connection.expects(:begin_db_transaction)
- Topic.connection.expects(:commit_db_transaction).raises('OH NOES')
- Topic.connection.expects(:rollback_db_transaction)
-
- assert_raise RuntimeError do
- Topic.transaction do
- # do nothing
+ assert_called(Topic.connection, :begin_db_transaction) do
+ Topic.connection.stub(:commit_db_transaction, -> { raise("OH NOES") }) do
+ assert_called(Topic.connection, :rollback_db_transaction) do
+ e = assert_raise RuntimeError do
+ Topic.transaction do
+ Topic.connection.materialize_transactions
+ end
+ end
+ assert_equal "OH NOES", e.message
+ end
end
end
end
def test_rollback_when_saving_a_frozen_record
- topic = Topic.new(:title => 'test')
+ topic = Topic.new(title: "test")
topic.freeze
- e = assert_raise(RuntimeError) { topic.save }
- assert_match(/frozen/i, e.message) # Not good enough, but we can't do much
- # about it since there is no specific error
- # for frozen objects.
- assert !topic.persisted?, 'not persisted'
+ e = assert_raise(frozen_error_class) { topic.save }
+ # Not good enough, but we can't do much
+ # about it since there is no specific error
+ # for frozen objects.
+ assert_match(/frozen/i, e.message)
+ assert_not topic.persisted?, "not persisted"
assert_nil topic.id
- assert topic.frozen?, 'not frozen'
+ assert topic.frozen?, "not frozen"
end
def test_rollback_when_thread_killed
@@ -525,20 +619,20 @@ class TransactionTest < ActiveRecord::TestCase
thread.join
assert @first.approved?, "First should still be changed in the objects"
- assert !@second.approved?, "Second should still be changed in the objects"
+ assert_not @second.approved?, "Second should still be changed in the objects"
- assert !Topic.find(1).approved?, "First shouldn't have been approved"
+ assert_not Topic.find(1).approved?, "First shouldn't have been approved"
assert Topic.find(2).approved?, "Second should still be approved"
end
def test_restore_active_record_state_for_all_records_in_a_transaction
topic_without_callbacks = Class.new(ActiveRecord::Base) do
- self.table_name = 'topics'
+ self.table_name = "topics"
end
- topic_1 = Topic.new(:title => 'test_1')
- topic_2 = Topic.new(:title => 'test_2')
- topic_3 = topic_without_callbacks.new(:title => 'test_3')
+ topic_1 = Topic.new(title: "test_1")
+ topic_2 = Topic.new(title: "test_2")
+ topic_3 = topic_without_callbacks.new(title: "test_3")
Topic.transaction do
assert topic_1.save
@@ -546,27 +640,27 @@ class TransactionTest < ActiveRecord::TestCase
assert topic_3.save
@first.save
@second.destroy
- assert topic_1.persisted?, 'persisted'
+ assert topic_1.persisted?, "persisted"
assert_not_nil topic_1.id
- assert topic_2.persisted?, 'persisted'
+ assert topic_2.persisted?, "persisted"
assert_not_nil topic_2.id
- assert topic_3.persisted?, 'persisted'
+ assert topic_3.persisted?, "persisted"
assert_not_nil topic_3.id
- assert @first.persisted?, 'persisted'
+ assert @first.persisted?, "persisted"
assert_not_nil @first.id
- assert @second.destroyed?, 'destroyed'
+ assert @second.destroyed?, "destroyed"
raise ActiveRecord::Rollback
end
- assert !topic_1.persisted?, 'not persisted'
+ assert_not topic_1.persisted?, "not persisted"
assert_nil topic_1.id
- assert !topic_2.persisted?, 'not persisted'
+ assert_not topic_2.persisted?, "not persisted"
assert_nil topic_2.id
- assert !topic_3.persisted?, 'not persisted'
+ assert_not topic_3.persisted?, "not persisted"
assert_nil topic_3.id
- assert @first.persisted?, 'persisted'
+ assert @first.persisted?, "persisted"
assert_not_nil @first.id
- assert !@second.destroyed?, 'not destroyed'
+ assert_not @second.destroyed?, "not destroyed"
end
def test_restore_frozen_state_after_double_destroy
@@ -580,8 +674,130 @@ class TransactionTest < ActiveRecord::TestCase
raise ActiveRecord::Rollback
end
- assert_not reply.frozen?
- assert_not topic.frozen?
+ assert_not_predicate reply, :frozen?
+ assert_not_predicate topic, :frozen?
+ end
+
+ def test_restore_new_record_after_double_save
+ topic = Topic.new
+
+ Topic.transaction do
+ topic.save!
+ topic.save!
+ raise ActiveRecord::Rollback
+ end
+
+ assert_nil topic.id
+ assert_predicate topic, :new_record?
+ end
+
+ def test_dont_restore_new_record_in_subsequent_transaction
+ topic = Topic.new
+
+ Topic.transaction do
+ topic.save!
+ topic.save!
+ end
+
+ Topic.transaction do
+ topic.save!
+ raise ActiveRecord::Rollback
+ end
+
+ assert_predicate topic, :persisted?
+ assert_not_predicate topic, :new_record?
+ end
+
+ def test_restore_id_after_rollback
+ topic = Topic.new
+
+ Topic.transaction do
+ topic.save!
+ raise ActiveRecord::Rollback
+ end
+
+ assert_nil topic.id
+ end
+
+ def test_restore_custom_primary_key_after_rollback
+ movie = Movie.new(name: "foo")
+
+ Movie.transaction do
+ movie.save!
+ raise ActiveRecord::Rollback
+ end
+
+ assert_nil movie.movieid
+ end
+
+ def test_assign_id_after_rollback
+ topic = Topic.create!
+
+ Topic.transaction do
+ topic.save!
+ raise ActiveRecord::Rollback
+ end
+
+ topic.id = nil
+ assert_nil topic.id
+ end
+
+ def test_assign_custom_primary_key_after_rollback
+ movie = Movie.create!(name: "foo")
+
+ Movie.transaction do
+ movie.save!
+ raise ActiveRecord::Rollback
+ end
+
+ movie.movieid = nil
+ assert_nil movie.movieid
+ end
+
+ def test_read_attribute_after_rollback
+ topic = Topic.new
+
+ Topic.transaction do
+ topic.save!
+ raise ActiveRecord::Rollback
+ end
+
+ assert_nil topic.read_attribute(:id)
+ end
+
+ def test_read_attribute_with_custom_primary_key_after_rollback
+ movie = Movie.new(name: "foo")
+
+ Movie.transaction do
+ movie.save!
+ raise ActiveRecord::Rollback
+ end
+
+ assert_nil movie.read_attribute(:movieid)
+ end
+
+ def test_write_attribute_after_rollback
+ topic = Topic.create!
+
+ Topic.transaction do
+ topic.save!
+ raise ActiveRecord::Rollback
+ end
+
+ topic.write_attribute(:id, nil)
+ assert_nil topic.id
+ end
+
+ def test_write_attribute_with_custom_primary_key_after_rollback
+ movie = Movie.create!(name: "foo")
+
+ Movie.transaction do
+ movie.save!
+ raise ActiveRecord::Rollback
+ end
+
+ movie.write_attribute(:movieid, nil)
+ assert_nil movie.movieid
end
def test_rollback_of_frozen_records
@@ -590,7 +806,7 @@ class TransactionTest < ActiveRecord::TestCase
topic.destroy
raise ActiveRecord::Rollback
end
- assert topic.frozen?, 'frozen'
+ assert topic.frozen?, "frozen"
end
def test_rollback_for_freshly_persisted_records
@@ -599,7 +815,7 @@ class TransactionTest < ActiveRecord::TestCase
topic.destroy
raise ActiveRecord::Rollback
end
- assert topic.persisted?, 'persisted'
+ assert topic.persisted?, "persisted"
end
def test_sqlite_add_column_in_transaction
@@ -613,27 +829,27 @@ class TransactionTest < ActiveRecord::TestCase
assert_nothing_raised do
Topic.reset_column_information
- Topic.connection.add_column('topics', 'stuff', :string)
- assert Topic.column_names.include?('stuff')
+ Topic.connection.add_column("topics", "stuff", :string)
+ assert_includes Topic.column_names, "stuff"
Topic.reset_column_information
- Topic.connection.remove_column('topics', 'stuff')
- assert !Topic.column_names.include?('stuff')
+ Topic.connection.remove_column("topics", "stuff")
+ assert_not_includes Topic.column_names, "stuff"
end
if Topic.connection.supports_ddl_transactions?
assert_nothing_raised do
- Topic.transaction { Topic.connection.add_column('topics', 'stuff', :string) }
+ Topic.transaction { Topic.connection.add_column("topics", "stuff", :string) }
end
else
Topic.transaction do
- assert_raise(ActiveRecord::StatementInvalid) { Topic.connection.add_column('topics', 'stuff', :string) }
+ assert_raise(ActiveRecord::StatementInvalid) { Topic.connection.add_column("topics", "stuff", :string) }
raise ActiveRecord::Rollback
end
end
ensure
begin
- Topic.connection.remove_column('topics', 'stuff')
+ Topic.connection.remove_column("topics", "stuff")
rescue
ensure
Topic.reset_column_information
@@ -644,38 +860,76 @@ class TransactionTest < ActiveRecord::TestCase
connection = Topic.connection
transaction = ActiveRecord::ConnectionAdapters::TransactionManager.new(connection).begin_transaction
- assert transaction.open?
- assert !transaction.state.rolledback?
- assert !transaction.state.committed?
+ assert_predicate transaction, :open?
+ assert_not_predicate transaction.state, :rolledback?
+ assert_not_predicate transaction.state, :committed?
transaction.rollback
- assert transaction.state.rolledback?
- assert !transaction.state.committed?
+ assert_predicate transaction.state, :rolledback?
+ assert_not_predicate transaction.state, :committed?
end
def test_transactions_state_from_commit
connection = Topic.connection
transaction = ActiveRecord::ConnectionAdapters::TransactionManager.new(connection).begin_transaction
- assert transaction.open?
- assert !transaction.state.rolledback?
- assert !transaction.state.committed?
+ assert_predicate transaction, :open?
+ assert_not_predicate transaction.state, :rolledback?
+ assert_not_predicate transaction.state, :committed?
+
+ transaction.commit
+
+ assert_not_predicate transaction.state, :rolledback?
+ assert_predicate transaction.state, :committed?
+ end
+
+ def test_set_state_method_is_deprecated
+ connection = Topic.connection
+ transaction = ActiveRecord::ConnectionAdapters::TransactionManager.new(connection).begin_transaction
transaction.commit
- assert !transaction.state.rolledback?
- assert transaction.state.committed?
+ assert_deprecated do
+ transaction.state.set_state(:rolledback)
+ end
+ end
+
+ def test_mark_transaction_state_as_committed
+ connection = Topic.connection
+ transaction = ActiveRecord::ConnectionAdapters::TransactionManager.new(connection).begin_transaction
+
+ transaction.rollback
+
+ assert_equal :committed, transaction.state.commit!
+ end
+
+ def test_mark_transaction_state_as_rolledback
+ connection = Topic.connection
+ transaction = ActiveRecord::ConnectionAdapters::TransactionManager.new(connection).begin_transaction
+
+ transaction.commit
+
+ assert_equal :rolledback, transaction.state.rollback!
+ end
+
+ def test_mark_transaction_state_as_nil
+ connection = Topic.connection
+ transaction = ActiveRecord::ConnectionAdapters::TransactionManager.new(connection).begin_transaction
+
+ transaction.commit
+
+ assert_nil transaction.state.nullify!
end
def test_transaction_rollback_with_primarykeyless_tables
connection = ActiveRecord::Base.connection
connection.create_table(:transaction_without_primary_keys, force: true, id: false) do |t|
- t.integer :thing_id
+ t.integer :thing_id
end
klass = Class.new(ActiveRecord::Base) do
- self.table_name = 'transaction_without_primary_keys'
+ self.table_name = "transaction_without_primary_keys"
after_commit { } # necessary to trigger the has_transactional_callbacks branch
end
@@ -686,20 +940,90 @@ class TransactionTest < ActiveRecord::TestCase
end
end
ensure
- connection.drop_table 'transaction_without_primary_keys', if_exists: true
+ connection.drop_table "transaction_without_primary_keys", if_exists: true
end
- private
+ def test_empty_transaction_is_not_materialized
+ assert_no_queries do
+ Topic.transaction { }
+ end
+ end
+
+ def test_unprepared_statement_materializes_transaction
+ assert_sql(/BEGIN/i, /COMMIT/i) do
+ Topic.transaction { Topic.where("1=1").first }
+ end
+ end
+
+ if ActiveRecord::Base.connection.prepared_statements
+ def test_prepared_statement_materializes_transaction
+ Topic.first
+
+ assert_sql(/BEGIN/i, /COMMIT/i) do
+ Topic.transaction { Topic.first }
+ end
+ end
+ end
+
+ def test_savepoint_does_not_materialize_transaction
+ assert_no_queries do
+ Topic.transaction do
+ Topic.transaction(requires_new: true) { }
+ end
+ end
+ end
- %w(validation save destroy).each do |filter|
- define_method("add_cancelling_before_#{filter}_with_db_side_effect_to_topic") do |topic|
- meta = class << topic; self; end
- meta.send("define_method", "before_#{filter}_for_transaction") do
- Book.create
- throw(:abort)
+ def test_raising_does_not_materialize_transaction
+ assert_raise(RuntimeError) do
+ assert_no_queries do
+ Topic.transaction { raise }
end
end
end
+
+ def test_accessing_raw_connection_materializes_transaction
+ assert_sql(/BEGIN/i, /COMMIT/i) do
+ Topic.transaction { Topic.connection.raw_connection }
+ end
+ end
+
+ def test_accessing_raw_connection_disables_lazy_transactions
+ Topic.connection.raw_connection
+
+ assert_sql(/BEGIN/i, /COMMIT/i) do
+ Topic.transaction { }
+ end
+ end
+
+ def test_checking_in_connection_reenables_lazy_transactions
+ connection = Topic.connection_pool.checkout
+ connection.raw_connection
+ Topic.connection_pool.checkin connection
+
+ assert_no_queries do
+ connection.transaction { }
+ end
+ end
+
+ def test_transactions_can_be_manually_materialized
+ assert_sql(/BEGIN/i, /COMMIT/i) do
+ Topic.transaction do
+ Topic.connection.materialize_transactions
+ end
+ end
+ end
+
+ private
+
+ %w(validation save destroy).each do |filter|
+ define_method("add_cancelling_before_#{filter}_with_db_side_effect_to_topic") do |topic|
+ meta = class << topic; self; end
+ meta.send("define_method", "before_#{filter}_for_transaction") do
+ Book.create
+ throw(:abort)
+ end
+ end
+ end
end
class TransactionsWithTransactionalFixturesTest < ActiveRecord::TestCase
@@ -716,7 +1040,7 @@ class TransactionsWithTransactionalFixturesTest < ActiveRecord::TestCase
raise
end
rescue
- assert !@first.reload.approved?
+ assert_not_predicate @first.reload, :approved?
end
end
@@ -737,31 +1061,29 @@ class TransactionsWithTransactionalFixturesTest < ActiveRecord::TestCase
end
end
- assert !@first.reload.approved?
+ assert_not_predicate @first.reload, :approved?
end
end if Topic.connection.supports_savepoints?
-if current_adapter?(:PostgreSQLAdapter)
+if ActiveRecord::Base.connection.supports_transaction_isolation?
class ConcurrentTransactionTest < TransactionTest
# This will cause transactions to overlap and fail unless they are performed on
# separate database connections.
- unless in_memory_db?
- def test_transaction_per_thread
- threads = 3.times.map do
- Thread.new do
- Topic.transaction do
- topic = Topic.find(1)
- topic.approved = !topic.approved?
- assert topic.save!
- topic.approved = !topic.approved?
- assert topic.save!
- end
- Topic.connection.close
+ def test_transaction_per_thread
+ threads = 3.times.map do
+ Thread.new do
+ Topic.transaction do
+ topic = Topic.find(1)
+ topic.approved = !topic.approved?
+ assert topic.save!
+ topic.approved = !topic.approved?
+ assert topic.save!
end
+ Topic.connection.close
end
-
- threads.each(&:join)
end
+
+ threads.each(&:join)
end
# Test for dirty reads among simultaneous transactions.
diff --git a/activerecord/test/cases/type/adapter_specific_registry_test.rb b/activerecord/test/cases/type/adapter_specific_registry_test.rb
index 8b836b4793..b58bdd5549 100644
--- a/activerecord/test/cases/type/adapter_specific_registry_test.rb
+++ b/activerecord/test/cases/type/adapter_specific_registry_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
module ActiveRecord
diff --git a/activerecord/test/cases/type/date_time_test.rb b/activerecord/test/cases/type/date_time_test.rb
new file mode 100644
index 0000000000..c9558e25b5
--- /dev/null
+++ b/activerecord/test/cases/type/date_time_test.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/task"
+
+module ActiveRecord
+ module Type
+ class DateTimeTest < ActiveRecord::TestCase
+ def test_datetime_seconds_precision_applied_to_timestamp
+ skip "This test is invalid if subsecond precision isn't supported" unless subsecond_precision_supported?
+ p = Task.create!(starting: ::Time.now)
+ assert_equal p.starting.usec, p.reload.starting.usec
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/type/decimal_test.rb b/activerecord/test/cases/type/decimal_test.rb
deleted file mode 100644
index fe49d0e79a..0000000000
--- a/activerecord/test/cases/type/decimal_test.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-require "cases/helper"
-
-module ActiveRecord
- module Type
- class DecimalTest < ActiveRecord::TestCase
- def test_type_cast_decimal
- type = Decimal.new
- assert_equal BigDecimal.new("0"), type.cast(BigDecimal.new("0"))
- assert_equal BigDecimal.new("123"), type.cast(123.0)
- assert_equal BigDecimal.new("1"), type.cast(:"1")
- 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)
- end
-
- def test_type_cast_from_float_with_unspecified_precision
- type = Decimal.new
- assert_equal 22.68.to_d, type.cast(22.68)
- end
-
- def test_type_cast_decimal_from_rational_with_precision
- type = Decimal.new(precision: 2)
- assert_equal BigDecimal("0.33"), type.cast(Rational(1, 3))
- end
-
- def test_type_cast_decimal_from_rational_without_precision_defaults_to_18_36
- type = Decimal.new
- assert_equal BigDecimal("0.333333333333333333E0"), type.cast(Rational(1, 3))
- end
-
- def test_type_cast_decimal_from_object_responding_to_d
- value = Object.new
- def value.to_d
- BigDecimal.new("1")
- end
- type = Decimal.new
- assert_equal BigDecimal("1"), type.cast(value)
- end
-
- def test_changed?
- type = Decimal.new
-
- assert type.changed?(5.0, 5.0, '5.0wibble')
- assert_not type.changed?(5.0, 5.0, '5.0')
- assert_not type.changed?(-5.0, -5.0, '-5.0')
- end
- end
- end
-end
diff --git a/activerecord/test/cases/type/integer_test.rb b/activerecord/test/cases/type/integer_test.rb
index 0dcdbd0667..15d1a675a1 100644
--- a/activerecord/test/cases/type/integer_test.rb
+++ b/activerecord/test/cases/type/integer_test.rb
@@ -1,118 +1,20 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "models/company"
module ActiveRecord
module Type
class IntegerTest < ActiveRecord::TestCase
- test "simple values" do
- type = Type::Integer.new
- assert_equal 1, type.cast(1)
- assert_equal 1, type.cast('1')
- assert_equal 1, type.cast('1ignore')
- assert_equal 0, type.cast('bad1')
- assert_equal 0, type.cast('bad')
- assert_equal 1, type.cast(1.7)
- assert_equal 0, type.cast(false)
- assert_equal 1, type.cast(true)
- assert_nil type.cast(nil)
- end
-
- test "random objects cast to nil" do
- type = Type::Integer.new
- assert_nil type.cast([1,2])
- assert_nil type.cast({1 => 2})
- assert_nil type.cast(1..2)
- end
-
test "casting ActiveRecord models" do
type = Type::Integer.new
- firm = Firm.create(:name => 'Apple')
+ firm = Firm.create(name: "Apple")
assert_nil type.cast(firm)
end
- test "casting objects without to_i" do
- type = Type::Integer.new
- assert_nil type.cast(::Object.new)
- end
-
- test "casting nan and infinity" do
- type = Type::Integer.new
- assert_nil type.cast(::Float::NAN)
- assert_nil type.cast(1.0/0.0)
- end
-
- test "casting booleans for database" do
- type = Type::Integer.new
- assert_equal 1, type.serialize(true)
- assert_equal 0, type.serialize(false)
- end
-
- test "changed?" do
- type = Type::Integer.new
-
- assert type.changed?(5, 5, '5wibble')
- assert_not type.changed?(5, 5, '5')
- assert_not type.changed?(5, 5, '5.0')
- assert_not type.changed?(-5, -5, '-5')
- assert_not type.changed?(-5, -5, '-5.0')
- assert_not type.changed?(nil, nil, nil)
- end
-
- test "values below int min value are out of range" do
- assert_raises(::RangeError) do
- Integer.new.serialize(-2147483649)
- end
- end
-
- test "values above int max value are out of range" do
- assert_raises(::RangeError) do
- Integer.new.serialize(2147483648)
- end
- end
-
- test "very small numbers are out of range" do
- assert_raises(::RangeError) do
- Integer.new.serialize(-9999999999999999999999999999999)
- end
- end
-
- test "very large numbers are out of range" do
- assert_raises(::RangeError) do
- Integer.new.serialize(9999999999999999999999999999999)
- end
- end
-
- test "normal numbers are in range" do
- type = Integer.new
- assert_equal(0, type.serialize(0))
- assert_equal(-1, type.serialize(-1))
- assert_equal(1, type.serialize(1))
- end
-
- test "int max value is in range" do
- assert_equal(2147483647, Integer.new.serialize(2147483647))
- end
-
- test "int min value is in range" do
- assert_equal(-2147483648, Integer.new.serialize(-2147483648))
- end
-
- test "columns with a larger limit have larger ranges" do
- type = Integer.new(limit: 8)
-
- assert_equal(9223372036854775807, type.serialize(9223372036854775807))
- assert_equal(-9223372036854775808, type.serialize(-9223372036854775808))
- assert_raises(::RangeError) do
- type.serialize(-9999999999999999999999999999999)
- end
- assert_raises(::RangeError) do
- type.serialize(9999999999999999999999999999999)
- end
- end
-
test "values which are out of range can be re-assigned" do
klass = Class.new(ActiveRecord::Base) do
- self.table_name = 'posts'
+ self.table_name = "posts"
attribute :foo, :integer
end
model = klass.new
diff --git a/activerecord/test/cases/type/string_test.rb b/activerecord/test/cases/type/string_test.rb
index 56e9bf434d..9e7810a6a5 100644
--- a/activerecord/test/cases/type/string_test.rb
+++ b/activerecord/test/cases/type/string_test.rb
@@ -1,36 +1,24 @@
-require 'cases/helper'
+# frozen_string_literal: true
+
+require "cases/helper"
module ActiveRecord
class StringTypeTest < ActiveRecord::TestCase
- test "type casting" do
- type = Type::String.new
- assert_equal "t", type.cast(true)
- assert_equal "f", type.cast(false)
- assert_equal "123", type.cast(123)
- end
-
- test "values are duped coming out" do
- s = "foo"
- type = Type::String.new
- assert_not_same s, type.cast(s)
- assert_not_same s, type.deserialize(s)
- end
-
test "string mutations are detected" do
klass = Class.new(Base)
- klass.table_name = 'authors'
+ klass.table_name = "authors"
- author = klass.create!(name: 'Sean')
- assert_not author.changed?
+ author = klass.create!(name: "Sean")
+ assert_not_predicate author, :changed?
- author.name << ' Griffin'
- assert author.name_changed?
+ author.name << " Griffin"
+ assert_predicate author, :name_changed?
author.save!
author.reload
- assert_equal 'Sean Griffin', author.name
- assert_not author.changed?
+ assert_equal "Sean Griffin", author.name
+ assert_not_predicate author, :changed?
end
end
end
diff --git a/activerecord/test/cases/type/type_map_test.rb b/activerecord/test/cases/type/type_map_test.rb
index 172c6dfc4c..1ce515a90c 100644
--- a/activerecord/test/cases/type/type_map_test.rb
+++ b/activerecord/test/cases/type/type_map_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
module ActiveRecord
@@ -15,7 +17,7 @@ module ActiveRecord
mapping.register_type(/boolean/i, boolean)
- assert_equal mapping.lookup('boolean'), boolean
+ assert_equal mapping.lookup("boolean"), boolean
end
def test_overriding_registered_types
@@ -26,26 +28,26 @@ module ActiveRecord
mapping.register_type(/time/i, time)
mapping.register_type(/time/i, timestamp)
- assert_equal mapping.lookup('time'), timestamp
+ assert_equal mapping.lookup("time"), timestamp
end
def test_fuzzy_lookup
- string = String.new
+ string = +""
mapping = TypeMap.new
mapping.register_type(/varchar/i, string)
- assert_equal mapping.lookup('varchar(20)'), string
+ assert_equal mapping.lookup("varchar(20)"), string
end
def test_aliasing_types
- string = String.new
+ string = +""
mapping = TypeMap.new
mapping.register_type(/string/i, string)
- mapping.alias_type(/varchar/i, 'string')
+ mapping.alias_type(/varchar/i, "string")
- assert_equal mapping.lookup('varchar'), string
+ assert_equal mapping.lookup("varchar"), string
end
def test_changing_type_changes_aliases
@@ -54,37 +56,37 @@ module ActiveRecord
mapping = TypeMap.new
mapping.register_type(/timestamp/i, time)
- mapping.alias_type(/datetime/i, 'timestamp')
+ mapping.alias_type(/datetime/i, "timestamp")
mapping.register_type(/timestamp/i, timestamp)
- assert_equal mapping.lookup('datetime'), timestamp
+ assert_equal mapping.lookup("datetime"), timestamp
end
def test_aliases_keep_metadata
mapping = TypeMap.new
mapping.register_type(/decimal/i) { |sql_type| sql_type }
- mapping.alias_type(/number/i, 'decimal')
+ mapping.alias_type(/number/i, "decimal")
- assert_equal mapping.lookup('number(20)'), 'decimal(20)'
- assert_equal mapping.lookup('number'), 'decimal'
+ assert_equal mapping.lookup("number(20)"), "decimal(20)"
+ assert_equal mapping.lookup("number"), "decimal"
end
def test_register_proc
- string = String.new
+ string = +""
binary = Binary.new
mapping = TypeMap.new
mapping.register_type(/varchar/i) do |type|
- if type.include?('(')
+ if type.include?("(")
string
else
binary
end
end
- assert_equal mapping.lookup('varchar(20)'), string
- assert_equal mapping.lookup('varchar'), binary
+ assert_equal mapping.lookup("varchar(20)"), string
+ assert_equal mapping.lookup("varchar"), binary
end
def test_additional_lookup_args
@@ -92,16 +94,16 @@ module ActiveRecord
mapping.register_type(/varchar/i) do |type, limit|
if limit > 255
- 'text'
+ "text"
else
- 'string'
+ "string"
end
end
- mapping.alias_type(/string/i, 'varchar')
+ mapping.alias_type(/string/i, "varchar")
- assert_equal mapping.lookup('varchar', 200), 'string'
- assert_equal mapping.lookup('varchar', 400), 'text'
- assert_equal mapping.lookup('string', 400), 'text'
+ assert_equal mapping.lookup("varchar", 200), "string"
+ assert_equal mapping.lookup("varchar", 400), "text"
+ assert_equal mapping.lookup("string", 400), "text"
end
def test_requires_value_or_block
@@ -115,13 +117,13 @@ module ActiveRecord
def test_lookup_non_strings
mapping = HashLookupTypeMap.new
- mapping.register_type(1, 'string')
- mapping.register_type(2, 'int')
+ mapping.register_type(1, "string")
+ mapping.register_type(2, "int")
mapping.alias_type(3, 1)
- assert_equal mapping.lookup(1), 'string'
- assert_equal mapping.lookup(2), 'int'
- assert_equal mapping.lookup(3), 'string'
+ assert_equal mapping.lookup(1), "string"
+ assert_equal mapping.lookup(2), "int"
+ assert_equal mapping.lookup(3), "string"
assert_kind_of Type::Value, mapping.lookup(4)
end
@@ -174,4 +176,3 @@ module ActiveRecord
end
end
end
-
diff --git a/activerecord/test/cases/type/unsigned_integer_test.rb b/activerecord/test/cases/type/unsigned_integer_test.rb
index f2c910eade..dd05cf3fff 100644
--- a/activerecord/test/cases/type/unsigned_integer_test.rb
+++ b/activerecord/test/cases/type/unsigned_integer_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
module ActiveRecord
@@ -8,7 +10,7 @@ module ActiveRecord
end
test "minus value is out of range" do
- assert_raises(::RangeError) do
+ assert_raises(ActiveModel::RangeError) do
UnsignedInteger.new.serialize(-1)
end
end
diff --git a/activerecord/test/cases/type_test.rb b/activerecord/test/cases/type_test.rb
index d45a9b3141..93ae563c8b 100644
--- a/activerecord/test/cases/type_test.rb
+++ b/activerecord/test/cases/type_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
class TypeTest < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/types_test.rb b/activerecord/test/cases/types_test.rb
index 9b1859c2ce..3f7fb0a604 100644
--- a/activerecord/test/cases/types_test.rb
+++ b/activerecord/test/cases/types_test.rb
@@ -1,120 +1,17 @@
+# frozen_string_literal: true
+
require "cases/helper"
module ActiveRecord
module ConnectionAdapters
class TypesTest < ActiveRecord::TestCase
- def test_type_cast_boolean
- type = Type::Boolean.new
- assert type.cast('').nil?
- assert type.cast(nil).nil?
-
- assert type.cast(true)
- assert type.cast(1)
- assert type.cast('1')
- assert type.cast('t')
- assert type.cast('T')
- assert type.cast('true')
- assert type.cast('TRUE')
- assert type.cast('on')
- assert type.cast('ON')
- assert type.cast(' ')
- assert type.cast("\u3000\r\n")
- assert type.cast("\u0000")
- assert type.cast('SOMETHING RANDOM')
-
- # explicitly check for false vs nil
- assert_equal false, type.cast(false)
- assert_equal false, type.cast(0)
- assert_equal false, type.cast('0')
- assert_equal false, type.cast('f')
- assert_equal false, type.cast('F')
- assert_equal false, type.cast('false')
- assert_equal false, type.cast('FALSE')
- assert_equal false, type.cast('off')
- assert_equal false, type.cast('OFF')
- end
-
- def test_type_cast_float
- type = Type::Float.new
- assert_equal 1.0, type.cast("1")
- end
-
- def test_changing_float
- type = Type::Float.new
-
- assert type.changed?(5.0, 5.0, '5wibble')
- assert_not type.changed?(5.0, 5.0, '5')
- assert_not type.changed?(5.0, 5.0, '5.0')
- assert_not type.changed?(nil, nil, nil)
- end
-
- def test_type_cast_binary
- type = Type::Binary.new
- assert_equal nil, type.cast(nil)
- assert_equal "1", type.cast("1")
- assert_equal 1, type.cast(1)
- end
-
- 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')
-
- time_string = Time.now.utc.strftime("%T")
- assert_equal time_string, type.cast(time_string).strftime("%T")
- end
-
- 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')
-
- datetime_string = Time.now.utc.strftime("%FT%T")
- assert_equal datetime_string, type.cast(datetime_string).strftime("%FT%T")
- end
-
- 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')
-
- date_string = Time.now.utc.strftime("%F")
- assert_equal date_string, type.cast(date_string).strftime("%F")
- end
-
- def test_type_cast_duration_to_integer
- type = Type::Integer.new
- assert_equal 1800, type.cast(30.minutes)
- assert_equal 7200, type.cast(2.hours)
- end
-
- def test_string_to_time_with_timezone
- [:utc, :local].each do |zone|
- with_timezone_config default: zone do
- type = Type::DateTime.new
- assert_equal Time.utc(2013, 9, 4, 0, 0, 0), type.cast("Wed, 04 Sep 2013 03:00:00 EAT")
- end
- end
- end
-
- def test_type_equality
- assert_equal Type::Value.new, Type::Value.new
- assert_not_equal Type::Value.new, Type::Integer.new
- assert_not_equal Type::Value.new(precision: 1), Type::Value.new(precision: 2)
- end
-
def test_attributes_which_are_invalid_for_database_can_still_be_reassigned
type_which_cannot_go_to_the_database = Type::Value.new
def type_which_cannot_go_to_the_database.serialize(*)
raise
end
klass = Class.new(ActiveRecord::Base) do
- self.table_name = 'posts'
+ self.table_name = "posts"
attribute :foo, type_which_cannot_go_to_the_database
end
model = klass.new
diff --git a/activerecord/test/cases/unconnected_test.rb b/activerecord/test/cases/unconnected_test.rb
index b210584644..9eefc32745 100644
--- a/activerecord/test/cases/unconnected_test.rb
+++ b/activerecord/test/cases/unconnected_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
class TestRecord < ActiveRecord::Base
@@ -28,6 +30,6 @@ class TestUnconnectedAdapter < ActiveRecord::TestCase
end
def test_underlying_adapter_no_longer_active
- assert !@underlying.active?, "Removed adapter should no longer be active"
+ assert_not @underlying.active?, "Removed adapter should no longer be active"
end
end
diff --git a/activerecord/test/cases/unsafe_raw_sql_test.rb b/activerecord/test/cases/unsafe_raw_sql_test.rb
new file mode 100644
index 0000000000..d5d8f2a09a
--- /dev/null
+++ b/activerecord/test/cases/unsafe_raw_sql_test.rb
@@ -0,0 +1,319 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/post"
+require "models/comment"
+
+class UnsafeRawSqlTest < ActiveRecord::TestCase
+ fixtures :posts, :comments
+
+ test "order: allows string column name" do
+ ids_expected = Post.order(Arel.sql("title")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order("title").pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order("title").pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+
+ test "order: allows symbol column name" do
+ ids_expected = Post.order(Arel.sql("title")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order(:title).pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order(:title).pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+
+ test "order: allows downcase symbol direction" do
+ ids_expected = Post.order(Arel.sql("title") => Arel.sql("asc")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order(title: :asc).pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order(title: :asc).pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+
+ test "order: allows upcase symbol direction" do
+ ids_expected = Post.order(Arel.sql("title") => Arel.sql("ASC")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order(title: :ASC).pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order(title: :ASC).pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+
+ test "order: allows string direction" do
+ ids_expected = Post.order(Arel.sql("title") => Arel.sql("asc")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order(title: "asc").pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order(title: "asc").pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+
+ test "order: allows multiple columns" do
+ ids_expected = Post.order(Arel.sql("author_id"), Arel.sql("title")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order(:author_id, :title).pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order(:author_id, :title).pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+
+ test "order: allows mixed" do
+ ids_expected = Post.order(Arel.sql("author_id"), Arel.sql("title") => Arel.sql("asc")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order(:author_id, title: :asc).pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order(:author_id, title: :asc).pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+
+ test "order: allows table and column name" do
+ ids_expected = Post.order(Arel.sql("title")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order("posts.title").pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order("posts.title").pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+
+ test "order: allows column name and direction in string" do
+ ids_expected = Post.order(Arel.sql("title desc")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order("title desc").pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order("title desc").pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+
+ test "order: allows table name, column name and direction in string" do
+ ids_expected = Post.order(Arel.sql("title desc")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order("posts.title desc").pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order("posts.title desc").pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+
+ test "order: allows NULLS FIRST and NULLS LAST too" do
+ raise "precondition failed" if Post.count < 2
+
+ # Ensure there are NULL and non-NULL post types.
+ Post.first.update_column(:type, nil)
+ Post.last.update_column(:type, "Programming")
+
+ ["asc", "desc", ""].each do |direction|
+ %w(first last).each do |position|
+ ids_expected = Post.order(Arel.sql("type #{direction} nulls #{position}")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order("type #{direction} nulls #{position}").pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order("type #{direction} nulls #{position}").pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+ end
+ end if current_adapter?(:PostgreSQLAdapter)
+
+ test "order: disallows invalid column name" do
+ with_unsafe_raw_sql_disabled do
+ assert_raises(ActiveRecord::UnknownAttributeReference) do
+ Post.order("len(title) asc").pluck(:id)
+ end
+ end
+ end
+
+ test "order: disallows invalid direction" do
+ with_unsafe_raw_sql_disabled do
+ assert_raises(ArgumentError) do
+ Post.order(title: :foo).pluck(:id)
+ end
+ end
+ end
+
+ test "order: disallows invalid column with direction" do
+ with_unsafe_raw_sql_disabled do
+ assert_raises(ActiveRecord::UnknownAttributeReference) do
+ Post.order("len(title)" => :asc).pluck(:id)
+ end
+ end
+ end
+
+ test "order: always allows Arel" do
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order(Arel.sql("length(title)")).pluck(:title) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order(Arel.sql("length(title)")).pluck(:title) }
+
+ assert_equal ids_depr, ids_disabled
+ end
+
+ test "order: allows Arel.sql with binds" do
+ ids_expected = Post.order(Arel.sql("REPLACE(title, 'misc', 'zzzz'), id")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order([Arel.sql("REPLACE(title, ?, ?), id"), "misc", "zzzz"]).pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order([Arel.sql("REPLACE(title, ?, ?), id"), "misc", "zzzz"]).pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+
+ test "order: disallows invalid bind statement" do
+ with_unsafe_raw_sql_disabled do
+ assert_raises(ActiveRecord::UnknownAttributeReference) do
+ Post.order(["REPLACE(title, ?, ?), id", "misc", "zzzz"]).pluck(:id)
+ end
+ end
+ end
+
+ test "order: disallows invalid Array arguments" do
+ with_unsafe_raw_sql_disabled do
+ assert_raises(ActiveRecord::UnknownAttributeReference) do
+ Post.order(["author_id", "length(title)"]).pluck(:id)
+ end
+ end
+ end
+
+ test "order: allows valid Array arguments" do
+ ids_expected = Post.order(Arel.sql("author_id, length(title)")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order(["author_id", Arel.sql("length(title)")]).pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order(["author_id", Arel.sql("length(title)")]).pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+
+ test "order: logs deprecation warning for unrecognized column" do
+ with_unsafe_raw_sql_deprecated do
+ assert_deprecated(/Dangerous query method/) do
+ Post.order("length(title)")
+ end
+ end
+ end
+
+ test "pluck: allows string column name" do
+ titles_expected = Post.pluck(Arel.sql("title"))
+
+ titles_depr = with_unsafe_raw_sql_deprecated { Post.pluck("title") }
+ titles_disabled = with_unsafe_raw_sql_disabled { Post.pluck("title") }
+
+ assert_equal titles_expected, titles_depr
+ assert_equal titles_expected, titles_disabled
+ end
+
+ test "pluck: allows symbol column name" do
+ titles_expected = Post.pluck(Arel.sql("title"))
+
+ titles_depr = with_unsafe_raw_sql_deprecated { Post.pluck(:title) }
+ titles_disabled = with_unsafe_raw_sql_disabled { Post.pluck(:title) }
+
+ assert_equal titles_expected, titles_depr
+ assert_equal titles_expected, titles_disabled
+ end
+
+ test "pluck: allows multiple column names" do
+ values_expected = Post.pluck(Arel.sql("title"), Arel.sql("id"))
+
+ values_depr = with_unsafe_raw_sql_deprecated { Post.pluck(:title, :id) }
+ values_disabled = with_unsafe_raw_sql_disabled { Post.pluck(:title, :id) }
+
+ assert_equal values_expected, values_depr
+ assert_equal values_expected, values_disabled
+ end
+
+ test "pluck: allows column names with includes" do
+ values_expected = Post.includes(:comments).pluck(Arel.sql("title"), Arel.sql("id"))
+
+ values_depr = with_unsafe_raw_sql_deprecated { Post.includes(:comments).pluck(:title, :id) }
+ values_disabled = with_unsafe_raw_sql_disabled { Post.includes(:comments).pluck(:title, :id) }
+
+ assert_equal values_expected, values_depr
+ assert_equal values_expected, values_disabled
+ end
+
+ test "pluck: allows auto-generated attributes" do
+ values_expected = Post.pluck(Arel.sql("tags_count"))
+
+ values_depr = with_unsafe_raw_sql_deprecated { Post.pluck(:tags_count) }
+ values_disabled = with_unsafe_raw_sql_disabled { Post.pluck(:tags_count) }
+
+ assert_equal values_expected, values_depr
+ assert_equal values_expected, values_disabled
+ end
+
+ test "pluck: allows table and column names" do
+ titles_expected = Post.pluck(Arel.sql("title"))
+
+ titles_depr = with_unsafe_raw_sql_deprecated { Post.pluck("posts.title") }
+ titles_disabled = with_unsafe_raw_sql_disabled { Post.pluck("posts.title") }
+
+ assert_equal titles_expected, titles_depr
+ assert_equal titles_expected, titles_disabled
+ end
+
+ test "pluck: disallows invalid column name" do
+ with_unsafe_raw_sql_disabled do
+ assert_raises(ActiveRecord::UnknownAttributeReference) do
+ Post.pluck("length(title)")
+ end
+ end
+ end
+
+ test "pluck: disallows invalid column name amongst valid names" do
+ with_unsafe_raw_sql_disabled do
+ assert_raises(ActiveRecord::UnknownAttributeReference) do
+ Post.pluck(:title, "length(title)")
+ end
+ end
+ end
+
+ test "pluck: disallows invalid column names with includes" do
+ with_unsafe_raw_sql_disabled do
+ assert_raises(ActiveRecord::UnknownAttributeReference) do
+ Post.includes(:comments).pluck(:title, "length(title)")
+ end
+ end
+ end
+
+ test "pluck: always allows Arel" do
+ values_depr = with_unsafe_raw_sql_deprecated { Post.includes(:comments).pluck(:title, Arel.sql("length(title)")) }
+ values_disabled = with_unsafe_raw_sql_disabled { Post.includes(:comments).pluck(:title, Arel.sql("length(title)")) }
+
+ assert_equal values_depr, values_disabled
+ end
+
+ test "pluck: logs deprecation warning" do
+ with_unsafe_raw_sql_deprecated do
+ assert_deprecated(/Dangerous query method/) do
+ Post.includes(:comments).pluck(:title, "length(title)")
+ end
+ end
+ end
+
+ def with_unsafe_raw_sql_disabled(&blk)
+ with_config(:disabled, &blk)
+ end
+
+ def with_unsafe_raw_sql_deprecated(&blk)
+ with_config(:deprecated, &blk)
+ end
+
+ def with_config(new_value, &blk)
+ old_value = ActiveRecord::Base.allow_unsafe_raw_sql
+ ActiveRecord::Base.allow_unsafe_raw_sql = new_value
+ blk.call
+ ensure
+ ActiveRecord::Base.allow_unsafe_raw_sql = old_value
+ end
+end
diff --git a/activerecord/test/cases/validations/absence_validation_test.rb b/activerecord/test/cases/validations/absence_validation_test.rb
index dd43ee358c..8235a54d8a 100644
--- a/activerecord/test/cases/validations/absence_validation_test.rb
+++ b/activerecord/test/cases/validations/absence_validation_test.rb
@@ -1,8 +1,10 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/face'
-require 'models/interest'
-require 'models/man'
-require 'models/topic'
+require "models/face"
+require "models/interest"
+require "models/man"
+require "models/topic"
class AbsenceValidationTest < ActiveRecord::TestCase
def test_non_association
@@ -11,8 +13,8 @@ class AbsenceValidationTest < ActiveRecord::TestCase
validates_absence_of :name
end
- assert boy_klass.new.valid?
- assert_not boy_klass.new(name: "Alex").valid?
+ assert_predicate boy_klass.new, :valid?
+ assert_not_predicate boy_klass.new(name: "Alex"), :valid?
end
def test_has_one_marked_for_destruction
@@ -42,7 +44,7 @@ class AbsenceValidationTest < ActiveRecord::TestCase
assert_not boy.valid?, "should not be valid if has_many association is present"
i2.mark_for_destruction
- assert boy.valid?
+ assert_predicate boy, :valid?
end
def test_does_not_call_to_a_on_associations
@@ -52,24 +54,22 @@ class AbsenceValidationTest < ActiveRecord::TestCase
end
face_with_to_a = Face.new
- def face_with_to_a.to_a; ['(/)', '(\)']; end
+ def face_with_to_a.to_a; ["(/)", '(\)']; end
assert_nothing_raised { boy_klass.new(face: face_with_to_a).valid? }
end
- def test_does_not_validate_if_parent_record_is_validate_false
+ def test_validates_absence_of_virtual_attribute_on_model
repair_validations(Interest) do
- Interest.validates_absence_of(:topic)
- interest = Interest.new(topic: Topic.new(title: "Math"))
- interest.save!(validate: false)
- assert interest.persisted?
+ Interest.send(:attr_accessor, :token)
+ Interest.validates_absence_of(:token)
+
+ interest = Interest.create!(topic: "Thought Leadering")
+ assert_predicate interest, :valid?
- man = Man.new(interest_ids: [interest.id])
- man.save!
+ interest.token = "tl"
- assert_equal man.interests.size, 1
- assert interest.valid?
- assert man.valid?
+ assert_predicate interest, :invalid?
end
end
end
diff --git a/activerecord/test/cases/validations/association_validation_test.rb b/activerecord/test/cases/validations/association_validation_test.rb
index bff5ffa65e..ce6d42b34b 100644
--- a/activerecord/test/cases/validations/association_validation_test.rb
+++ b/activerecord/test/cases/validations/association_validation_test.rb
@@ -1,8 +1,10 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/topic'
-require 'models/reply'
-require 'models/man'
-require 'models/interest'
+require "models/topic"
+require "models/reply"
+require "models/man"
+require "models/interest"
class AssociationValidationTest < ActiveRecord::TestCase
fixtures :topics
@@ -14,25 +16,25 @@ class AssociationValidationTest < ActiveRecord::TestCase
Reply.validates_presence_of(:content)
t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
t.replies << [r = Reply.new("title" => "A reply"), r2 = Reply.new("title" => "Another reply", "content" => "non-empty"), r3 = Reply.new("title" => "Yet another reply"), r4 = Reply.new("title" => "The last reply", "content" => "non-empty")]
- assert !t.valid?
- assert t.errors[:replies].any?
+ assert_not_predicate t, :valid?
+ assert_predicate t.errors[:replies], :any?
assert_equal 1, r.errors.count # make sure all associated objects have been validated
assert_equal 0, r2.errors.count
assert_equal 1, r3.errors.count
assert_equal 0, r4.errors.count
r.content = r3.content = "non-empty"
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_associated_one
- Reply.validates :topic, :associated => true
- Topic.validates_presence_of( :content )
+ Reply.validates :topic, associated: true
+ Topic.validates_presence_of(:content)
r = Reply.new("title" => "A reply", "content" => "with content!")
r.topic = Topic.create("title" => "uhohuhoh")
- assert !r.valid?
- assert r.errors[:topic].any?
+ assert_not_predicate r, :valid?
+ assert_predicate r.errors[:topic], :any?
r.topic.content = "non-empty"
- assert r.valid?
+ assert_predicate r, :valid?
end
def test_validates_associated_marked_for_destruction
@@ -40,13 +42,25 @@ class AssociationValidationTest < ActiveRecord::TestCase
Reply.validates_presence_of(:content)
t = Topic.new
t.replies << Reply.new
- assert t.invalid?
+ assert_predicate t, :invalid?
t.replies.first.mark_for_destruction
- assert t.valid?
+ assert_predicate t, :valid?
+ end
+
+ def test_validates_associated_without_marked_for_destruction
+ reply = Class.new do
+ def valid?
+ true
+ end
+ end
+ Topic.validates_associated(:replies)
+ t = Topic.new
+ t.define_singleton_method(:replies) { [reply.new] }
+ assert_predicate t, :valid?
end
def test_validates_associated_with_custom_message_using_quotes
- Reply.validates_associated :topic, :message=> "This string contains 'single' and \"double\" quotes"
+ Reply.validates_associated :topic, message: "This string contains 'single' and \"double\" quotes"
Topic.validates_presence_of :content
r = Reply.create("title" => "A reply", "content" => "with content!")
r.topic = Topic.create("title" => "uhohuhoh")
@@ -57,19 +71,19 @@ class AssociationValidationTest < ActiveRecord::TestCase
def test_validates_associated_missing
Reply.validates_presence_of(:topic)
r = Reply.create("title" => "A reply", "content" => "with content!")
- assert !r.valid?
- assert r.errors[:topic].any?
+ assert_not_predicate r, :valid?
+ assert_predicate r.errors[:topic], :any?
r.topic = Topic.first
- assert r.valid?
+ assert_predicate r, :valid?
end
def test_validates_presence_of_belongs_to_association__parent_is_new_record
repair_validations(Interest) do
# Note that Interest and Man have the :inverse_of option set
Interest.validates_presence_of(:man)
- man = Man.new(:name => 'John')
- interest = man.interests.build(:topic => 'Airplanes')
+ man = Man.new(name: "John")
+ interest = man.interests.build(topic: "Airplanes")
assert interest.valid?, "Expected interest to be valid, but was not. Interest should have a man object associated"
end
end
@@ -77,8 +91,8 @@ class AssociationValidationTest < ActiveRecord::TestCase
def test_validates_presence_of_belongs_to_association__existing_parent
repair_validations(Interest) do
Interest.validates_presence_of(:man)
- man = Man.create!(:name => 'John')
- interest = man.interests.build(:topic => 'Airplanes')
+ man = Man.create!(name: "John")
+ interest = man.interests.build(topic: "Airplanes")
assert interest.valid?, "Expected interest to be valid, but was not. Interest should have a man object associated"
end
end
diff --git a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb
index 13d4d85afa..703c24b340 100644
--- a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb
+++ b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/topic'
+require "models/topic"
class I18nGenerateMessageValidationTest < ActiveRecord::TestCase
def setup
@@ -20,20 +22,20 @@ class I18nGenerateMessageValidationTest < ActiveRecord::TestCase
# validates_associated: generate_message(attr_name, :invalid, :message => custom_message, :value => value)
def test_generate_message_invalid_with_default_message
- assert_equal 'is invalid', @topic.errors.generate_message(:title, :invalid, :value => 'title')
+ assert_equal "is invalid", @topic.errors.generate_message(:title, :invalid, value: "title")
end
def test_generate_message_invalid_with_custom_message
- assert_equal 'custom message title', @topic.errors.generate_message(:title, :invalid, :message => 'custom message %{value}', :value => 'title')
+ assert_equal "custom message title", @topic.errors.generate_message(:title, :invalid, message: "custom message %{value}", value: "title")
end
# validates_uniqueness_of: generate_message(attr_name, :taken, :message => custom_message)
def test_generate_message_taken_with_default_message
- assert_equal "has already been taken", @topic.errors.generate_message(:title, :taken, :value => 'title')
+ assert_equal "has already been taken", @topic.errors.generate_message(:title, :taken, value: "title")
end
def test_generate_message_taken_with_custom_message
- assert_equal 'custom message title', @topic.errors.generate_message(:title, :taken, :message => 'custom message %{value}', :value => 'title')
+ assert_equal "custom message title", @topic.errors.generate_message(:title, :taken, message: "custom message %{value}", value: "title")
end
# ActiveRecord#RecordInvalid exception
@@ -47,7 +49,7 @@ class I18nGenerateMessageValidationTest < ActiveRecord::TestCase
test "RecordInvalid exception translation falls back to the :errors namespace" do
reset_i18n_load_path do
- I18n.backend.store_translations 'en', :errors => {:messages => {:record_invalid => 'fallback message'}}
+ I18n.backend.store_translations "en", errors: { messages: { record_invalid: "fallback message" } }
topic = Topic.new
topic.errors.add(:title, :blank)
assert_equal "fallback message", ActiveRecord::RecordInvalid.new(topic).message
@@ -56,29 +58,29 @@ class I18nGenerateMessageValidationTest < ActiveRecord::TestCase
test "translation for 'taken' can be overridden" do
reset_i18n_load_path do
- I18n.backend.store_translations "en", {errors: {attributes: {title: {taken: "Custom taken message" }}}}
- assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title')
+ I18n.backend.store_translations "en", errors: { attributes: { title: { taken: "Custom taken message" } } }
+ assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, value: "title")
end
end
test "translation for 'taken' can be overridden in activerecord scope" do
reset_i18n_load_path do
- I18n.backend.store_translations "en", {activerecord: {errors: {messages: {taken: "Custom taken message" }}}}
- assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title')
+ I18n.backend.store_translations "en", activerecord: { errors: { messages: { taken: "Custom taken message" } } }
+ assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, value: "title")
end
end
test "translation for 'taken' can be overridden in activerecord model scope" do
reset_i18n_load_path do
- I18n.backend.store_translations "en", {activerecord: {errors: {models: {topic: {taken: "Custom taken message" }}}}}
- assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title')
+ I18n.backend.store_translations "en", activerecord: { errors: { models: { topic: { taken: "Custom taken message" } } } }
+ assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, value: "title")
end
end
test "translation for 'taken' can be overridden in activerecord attributes scope" do
reset_i18n_load_path do
- I18n.backend.store_translations "en", {activerecord: {errors: {models: {topic: {attributes: {title: {taken: "Custom taken message" }}}}}}}
- assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title')
+ I18n.backend.store_translations "en", activerecord: { errors: { models: { topic: { attributes: { title: { taken: "Custom taken message" } } } } } }
+ assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, value: "title")
end
end
end
diff --git a/activerecord/test/cases/validations/i18n_validation_test.rb b/activerecord/test/cases/validations/i18n_validation_test.rb
index 268d7914b5..b7c52ea18c 100644
--- a/activerecord/test/cases/validations/i18n_validation_test.rb
+++ b/activerecord/test/cases/validations/i18n_validation_test.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/topic'
-require 'models/reply'
+require "models/topic"
+require "models/reply"
class I18nValidationTest < ActiveRecord::TestCase
repair_validations(Topic, Reply)
@@ -12,7 +14,7 @@ class I18nValidationTest < ActiveRecord::TestCase
@old_load_path, @old_backend = I18n.load_path.dup, I18n.backend
I18n.load_path.clear
I18n.backend = I18n::Backend::Simple.new
- I18n.backend.store_translations('en', :errors => {:messages => {:custom => nil}})
+ I18n.backend.store_translations("en", errors: { messages: { custom: nil } })
end
teardown do
@@ -21,12 +23,12 @@ class I18nValidationTest < ActiveRecord::TestCase
end
def unique_topic
- @unique ||= Topic.create :title => 'unique!'
+ @unique ||= Topic.create title: "unique!"
end
def replied_topic
@replied_topic ||= begin
- topic = Topic.create(:title => "topic")
+ topic = Topic.create(title: "topic")
topic.replies << Reply.new
topic
end
@@ -36,55 +38,48 @@ class I18nValidationTest < ActiveRecord::TestCase
# are used to generate tests to keep things DRY
#
COMMON_CASES = [
- # [ case, validation_options, generate_message_options]
+ # [ case, validation_options, generate_message_options]
[ "given no options", {}, {}],
- [ "given custom message", {:message => "custom"}, {:message => "custom"}],
- [ "given if condition", {:if => lambda { true }}, {}],
- [ "given unless condition", {:unless => lambda { false }}, {}],
- [ "given option that is not reserved", {:format => "jpg"}, {:format => "jpg" }]
- # TODO Add :on case, but below doesn't work, because then the validation isn't run for some reason
- # even when using .save instead .valid?
- # [ "given on condition", {on: :save}, {}]
+ [ "given custom message", { message: "custom" }, { message: "custom" }],
+ [ "given if condition", { if: lambda { true } }, {}],
+ [ "given unless condition", { unless: lambda { false } }, {}],
+ [ "given option that is not reserved", { format: "jpg" }, { format: "jpg" }],
+ [ "given on condition", { on: [:create, :update] }, {}]
]
- # validates_uniqueness_of w/ mocha
-
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_uniqueness_of on generated message #{name}" do
Topic.validates_uniqueness_of :title, validation_options
@topic.title = unique_topic.title
- @topic.errors.expects(:generate_message).with(:title, :taken, generate_message_options.merge(:value => 'unique!'))
- @topic.valid?
+ assert_called_with(@topic.errors, :generate_message, [:title, :taken, generate_message_options.merge(value: "unique!")]) do
+ @topic.valid?
+ end
end
end
- # validates_associated w/ mocha
-
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_associated on generated message #{name}" do
Topic.validates_associated :replies, validation_options
- replied_topic.errors.expects(:generate_message).with(:replies, :invalid, generate_message_options.merge(:value => replied_topic.replies))
- replied_topic.save
+ assert_called_with(replied_topic.errors, :generate_message, [:replies, :invalid, generate_message_options.merge(value: replied_topic.replies)]) do
+ replied_topic.save
+ end
end
end
- # validates_associated w/o mocha
-
def test_validates_associated_finds_custom_model_key_translation
- I18n.backend.store_translations 'en', :activerecord => {:errors => {:models => {:topic => {:attributes => {:replies => {:invalid => 'custom message'}}}}}}
- I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:invalid => 'global message'}}}
+ I18n.backend.store_translations "en", activerecord: { errors: { models: { topic: { attributes: { replies: { invalid: "custom message" } } } } } }
+ I18n.backend.store_translations "en", activerecord: { errors: { messages: { invalid: "global message" } } }
Topic.validates_associated :replies
replied_topic.valid?
- assert_equal ['custom message'], replied_topic.errors[:replies].uniq
+ assert_equal ["custom message"], replied_topic.errors[:replies].uniq
end
def test_validates_associated_finds_global_default_translation
- I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:invalid => 'global message'}}}
+ I18n.backend.store_translations "en", activerecord: { errors: { messages: { invalid: "global message" } } }
Topic.validates_associated :replies
replied_topic.valid?
- assert_equal ['global message'], replied_topic.errors[:replies]
+ assert_equal ["global message"], replied_topic.errors[:replies]
end
-
end
diff --git a/activerecord/test/cases/validations/length_validation_test.rb b/activerecord/test/cases/validations/length_validation_test.rb
index f95f8f0b8f..1fbcdc271b 100644
--- a/activerecord/test/cases/validations/length_validation_test.rb
+++ b/activerecord/test/cases/validations/length_validation_test.rb
@@ -1,78 +1,80 @@
-# -*- coding: utf-8 -*-
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/owner'
-require 'models/pet'
-require 'models/person'
+require "models/owner"
+require "models/pet"
+require "models/person"
class LengthValidationTest < ActiveRecord::TestCase
fixtures :owners
setup do
@owner = Class.new(Owner) do
- def self.name; 'Owner'; end
+ def self.name; "Owner"; end
end
end
-
def test_validates_size_of_association
assert_nothing_raised { @owner.validates_size_of :pets, minimum: 1 }
- o = @owner.new('name' => 'nopets')
- assert !o.save
- assert o.errors[:pets].any?
- o.pets.build('name' => 'apet')
- assert o.valid?
+ o = @owner.new("name" => "nopets")
+ assert_not o.save
+ assert_predicate o.errors[:pets], :any?
+ o.pets.build("name" => "apet")
+ assert_predicate o, :valid?
end
def test_validates_size_of_association_using_within
assert_nothing_raised { @owner.validates_size_of :pets, within: 1..2 }
- o = @owner.new('name' => 'nopets')
- assert !o.save
- assert o.errors[:pets].any?
+ o = @owner.new("name" => "nopets")
+ assert_not o.save
+ assert_predicate o.errors[:pets], :any?
- o.pets.build('name' => 'apet')
- assert o.valid?
+ o.pets.build("name" => "apet")
+ assert_predicate o, :valid?
- 2.times { o.pets.build('name' => 'apet') }
- assert !o.save
- assert o.errors[:pets].any?
+ 2.times { o.pets.build("name" => "apet") }
+ assert_not o.save
+ assert_predicate o.errors[:pets], :any?
end
def test_validates_size_of_association_utf8
@owner.validates_size_of :pets, minimum: 1
- o = @owner.new('name' => 'あいうえおかきくけこ')
- assert !o.save
- assert o.errors[:pets].any?
- o.pets.build('name' => 'あいうえおかきくけこ')
- assert o.valid?
+ o = @owner.new("name" => "あいうえおかきくけこ")
+ assert_not o.save
+ assert_predicate o.errors[:pets], :any?
+ o.pets.build("name" => "あいうえおかきくけこ")
+ assert_predicate o, :valid?
end
def test_validates_size_of_respects_records_marked_for_destruction
@owner.validates_size_of :pets, minimum: 1
owner = @owner.new
assert_not owner.save
- assert owner.errors[:pets].any?
+ assert_predicate owner.errors[:pets], :any?
pet = owner.pets.build
- assert owner.valid?
+ assert_predicate owner, :valid?
assert owner.save
pet_count = Pet.count
- assert_not owner.update_attributes pets_attributes: [ {_destroy: 1, id: pet.id} ]
- assert_not owner.valid?
- assert owner.errors[:pets].any?
+ assert_not owner.update pets_attributes: [ { _destroy: 1, id: pet.id } ]
+ assert_not_predicate owner, :valid?
+ assert_predicate owner.errors[:pets], :any?
assert_equal pet_count, Pet.count
end
- def test_does_not_validate_length_of_if_parent_record_is_validate_false
- @owner.validates_length_of :name, minimum: 1
- owner = @owner.new
- owner.save!(validate: false)
- assert owner.persisted?
+ def test_validates_length_of_virtual_attribute_on_model
+ repair_validations(Pet) do
+ Pet.send(:attr_accessor, :nickname)
+ Pet.validates_length_of(:name, minimum: 1)
+ Pet.validates_length_of(:nickname, minimum: 1)
- pet = Pet.new(owner_id: owner.id)
- pet.save!
+ pet = Pet.create!(name: "Fancy Pants", nickname: "Fancy")
- assert_equal owner.pets.size, 1
- assert owner.valid?
- assert pet.valid?
+ assert_predicate pet, :valid?
+
+ pet.nickname = ""
+
+ assert_predicate pet, :invalid?
+ end
end
end
diff --git a/activerecord/test/cases/validations/presence_validation_test.rb b/activerecord/test/cases/validations/presence_validation_test.rb
index 6f8ad06ab6..63c3f67da2 100644
--- a/activerecord/test/cases/validations/presence_validation_test.rb
+++ b/activerecord/test/cases/validations/presence_validation_test.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/man'
-require 'models/face'
-require 'models/interest'
-require 'models/speedometer'
-require 'models/dashboard'
+require "models/man"
+require "models/face"
+require "models/interest"
+require "models/speedometer"
+require "models/dashboard"
class PresenceValidationTest < ActiveRecord::TestCase
class Boy < Man; end
@@ -13,10 +15,10 @@ class PresenceValidationTest < ActiveRecord::TestCase
def test_validates_presence_of_non_association
Boy.validates_presence_of(:name)
b = Boy.new
- assert b.invalid?
+ assert_predicate b, :invalid?
b.name = "Alex"
- assert b.valid?
+ assert_predicate b, :valid?
end
def test_validates_presence_of_has_one
@@ -31,23 +33,23 @@ class PresenceValidationTest < ActiveRecord::TestCase
b = Boy.new
f = Face.new
b.face = f
- assert b.valid?
+ assert_predicate b, :valid?
f.mark_for_destruction
- assert b.invalid?
+ assert_predicate b, :invalid?
end
def test_validates_presence_of_has_many_marked_for_destruction
Boy.validates_presence_of(:interests)
b = Boy.new
b.interests << [i1 = Interest.new, i2 = Interest.new]
- assert b.valid?
+ assert_predicate b, :valid?
i1.mark_for_destruction
- assert b.valid?
+ assert_predicate b, :valid?
i2.mark_for_destruction
- assert b.invalid?
+ assert_predicate b, :invalid?
end
def test_validates_presence_doesnt_convert_to_array
@@ -57,7 +59,7 @@ class PresenceValidationTest < ActiveRecord::TestCase
dash = Dashboard.new
# dashboard has to_a method
- def dash.to_a; ['(/)', '(\)']; end
+ def dash.to_a; ["(/)", '(\)']; end
s = speedometer.new
s.dashboard = dash
@@ -65,19 +67,39 @@ class PresenceValidationTest < ActiveRecord::TestCase
assert_nothing_raised { s.valid? }
end
- def test_does_not_validate_presence_of_if_parent_record_is_validate_false
+ def test_validates_presence_of_virtual_attribute_on_model
repair_validations(Interest) do
+ Interest.send(:attr_accessor, :abbreviation)
Interest.validates_presence_of(:topic)
+ Interest.validates_presence_of(:abbreviation)
+
+ interest = Interest.create!(topic: "Thought Leadering", abbreviation: "tl")
+ assert_predicate interest, :valid?
+
+ interest.abbreviation = ""
+
+ assert_predicate interest, :invalid?
+ end
+ end
+
+ def test_validations_run_on_persisted_record
+ repair_validations(Interest) do
interest = Interest.new
- interest.save!(validate: false)
- assert interest.persisted?
+ interest.save!
+ assert_predicate interest, :valid?
- man = Man.new(interest_ids: [interest.id])
- man.save!
+ Interest.validates_presence_of(:topic)
- assert_equal man.interests.size, 1
- assert interest.valid?
- assert man.valid?
+ assert_not_predicate interest, :valid?
+ end
+ end
+
+ def test_validates_presence_with_on_context
+ repair_validations(Interest) do
+ Interest.validates_presence_of(:topic, on: :required_name)
+ interest = Interest.new
+ interest.save!
+ assert_not interest.valid?(:required_name)
end
end
end
diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb
index 2608c84be2..8f6f47e5fb 100644
--- a/activerecord/test/cases/validations/uniqueness_validation_test.rb
+++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb
@@ -1,9 +1,16 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/topic'
-require 'models/reply'
-require 'models/warehouse_thing'
-require 'models/guid'
-require 'models/event'
+require "models/topic"
+require "models/reply"
+require "models/warehouse_thing"
+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
@@ -24,7 +31,7 @@ end
class ReplyTitle; end
class ReplyWithTitleObject < Reply
- validates_uniqueness_of :content, :scope => :title
+ validates_uniqueness_of :content, scope: :title
def title; ReplyTitle.new; end
end
@@ -36,21 +43,33 @@ end
class BigIntTest < ActiveRecord::Base
INT_MAX_VALUE = 2147483647
- self.table_name = 'cars'
+ self.table_name = "cars"
validates :engines_count, uniqueness: true, inclusion: { in: 0..INT_MAX_VALUE }
end
class BigIntReverseTest < ActiveRecord::Base
INT_MAX_VALUE = 2147483647
- self.table_name = 'cars'
+ self.table_name = "cars"
validates :engines_count, inclusion: { in: 0..INT_MAX_VALUE }
validates :engines_count, uniqueness: true
end
+class CoolTopic < Topic
+ validates_uniqueness_of :id
+end
+
+class TopicWithAfterCreate < Topic
+ after_create :set_author
+
+ def set_author
+ update!(author_name: "#{title} #{id}")
+ end
+end
+
class UniquenessValidationTest < ActiveRecord::TestCase
INT_MAX_VALUE = 2147483647
- fixtures :topics, 'warehouse-things'
+ fixtures :topics, "warehouse-things"
repair_validations(Topic, Reply)
@@ -64,8 +83,8 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert t.save, "Should still save t as unique"
t2 = Topic.new("title" => "I'm uniqué!")
- assert !t2.valid?, "Shouldn't be valid"
- assert !t2.save, "Shouldn't save t2 as unique"
+ assert_not t2.valid?, "Shouldn't be valid"
+ assert_not t2.save, "Shouldn't save t2 as unique"
assert_equal ["has already been taken"], t2.errors[:title]
t2.title = "Now I am really also unique"
@@ -76,8 +95,8 @@ class UniquenessValidationTest < ActiveRecord::TestCase
Topic.alias_attribute :new_title, :title
Topic.validates_uniqueness_of(:new_title)
- topic = Topic.new(new_title: 'abc')
- assert topic.valid?
+ topic = Topic.new(new_title: "abc")
+ assert_predicate topic, :valid?
end
def test_validates_uniqueness_with_nil_value
@@ -87,39 +106,39 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert t.save, "Should save t as unique"
t2 = Topic.new("title" => nil)
- assert !t2.valid?, "Shouldn't be valid"
- assert !t2.save, "Shouldn't save t2 as unique"
+ assert_not t2.valid?, "Shouldn't be valid"
+ assert_not t2.save, "Shouldn't save t2 as unique"
assert_equal ["has already been taken"], t2.errors[:title]
end
def test_validates_uniqueness_with_validates
- Topic.validates :title, :uniqueness => true
- Topic.create!('title' => 'abc')
+ Topic.validates :title, uniqueness: true
+ Topic.create!("title" => "abc")
- t2 = Topic.new('title' => 'abc')
- assert !t2.valid?
+ t2 = Topic.new("title" => "abc")
+ assert_not_predicate t2, :valid?
assert t2.errors[:title]
end
def test_validate_uniqueness_when_integer_out_of_range
entry = BigIntTest.create(engines_count: INT_MAX_VALUE + 1)
- assert_equal entry.errors[:engines_count], ['is not included in the list']
+ assert_equal entry.errors[:engines_count], ["is not included in the list"]
end
def test_validate_uniqueness_when_integer_out_of_range_show_order_does_not_matter
entry = BigIntReverseTest.create(engines_count: INT_MAX_VALUE + 1)
- assert_equal entry.errors[:engines_count], ['is not included in the list']
+ assert_equal entry.errors[:engines_count], ["is not included in the list"]
end
def test_validates_uniqueness_with_newline_chars
- Topic.validates_uniqueness_of(:title, :case_sensitive => false)
+ Topic.validates_uniqueness_of(:title, case_sensitive: false)
t = Topic.new("title" => "new\nline")
assert t.save, "Should save t as unique"
end
def test_validate_uniqueness_with_scope
- Reply.validates_uniqueness_of(:content, :scope => "parent_id")
+ Reply.validates_uniqueness_of(:content, scope: "parent_id")
t = Topic.create("title" => "I'm unique!")
@@ -127,7 +146,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert r1.valid?, "Saving r1"
r2 = t.replies.create "title" => "r2", "content" => "hello world"
- assert !r2.valid?, "Saving r2 first time"
+ assert_not r2.valid?, "Saving r2 first time"
r2.content = "something else"
assert r2.save, "Saving r2 second time"
@@ -137,8 +156,15 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert r3.valid?, "Saving r3"
end
+ def test_validate_uniqueness_with_scope_invalid_syntax
+ error = assert_raises(ArgumentError) do
+ Reply.validates_uniqueness_of(:content, scope: { parent_id: false })
+ end
+ assert_match(/Pass a symbol or an array of symbols instead/, error.to_s)
+ end
+
def test_validate_uniqueness_with_object_scope
- Reply.validates_uniqueness_of(:content, :scope => :topic)
+ Reply.validates_uniqueness_of(:content, scope: :topic)
t = Topic.create("title" => "I'm unique!")
@@ -146,7 +172,20 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert r1.valid?, "Saving r1"
r2 = t.replies.create "title" => "r2", "content" => "hello world"
- assert !r2.valid?, "Saving r2 first time"
+ assert_not 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
@@ -154,7 +193,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert r1.valid?, "Saving r1"
r2 = ReplyWithTitleObject.create "title" => "r1", "content" => "hello world"
- assert !r2.valid?, "Saving r2 first time"
+ assert_not r2.valid?, "Saving r2 first time"
end
def test_validate_uniqueness_with_object_arg
@@ -166,7 +205,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert r1.valid?, "Saving r1"
r2 = t.replies.create "title" => "r2", "content" => "hello world"
- assert !r2.valid?, "Saving r2 first time"
+ assert_not r2.valid?, "Saving r2 first time"
end
def test_validate_uniqueness_scoped_to_defining_class
@@ -176,7 +215,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert r1.valid?, "Saving r1"
r2 = t.silly_unique_replies.create "title" => "r2", "content" => "a barrel of fun"
- assert !r2.valid?, "Saving r2"
+ assert_not r2.valid?, "Saving r2"
# Should succeed as validates_uniqueness_of only applies to
# UniqueReply and its subclasses
@@ -185,7 +224,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
end
def test_validate_uniqueness_with_scope_array
- Reply.validates_uniqueness_of(:author_name, :scope => [:author_email_address, :parent_id])
+ Reply.validates_uniqueness_of(:author_name, scope: [:author_email_address, :parent_id])
t = Topic.create("title" => "The earth is actually flat!")
@@ -193,23 +232,23 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert r1.valid?, "Saving r1"
r2 = t.replies.create "author_name" => "jeremy", "author_email_address" => "jeremy@rubyonrails.com", "title" => "You're crazy!", "content" => "Crazy reply again..."
- assert !r2.valid?, "Saving r2. Double reply by same author."
+ assert_not r2.valid?, "Saving r2. Double reply by same author."
r2.author_email_address = "jeremy_alt_email@rubyonrails.com"
assert r2.save, "Saving r2 the second time."
r3 = t.replies.create "author_name" => "jeremy", "author_email_address" => "jeremy_alt_email@rubyonrails.com", "title" => "You're wrong", "content" => "It's cubic"
- assert !r3.valid?, "Saving r3"
+ assert_not r3.valid?, "Saving r3"
r3.author_name = "jj"
assert r3.save, "Saving r3 the second time."
r3.author_name = "jeremy"
- assert !r3.save, "Saving r3 the third time."
+ assert_not r3.save, "Saving r3 the third time."
end
def test_validate_case_insensitive_uniqueness
- Topic.validates_uniqueness_of(:title, :parent_id, :case_sensitive => false, :allow_nil => true)
+ Topic.validates_uniqueness_of(:title, :parent_id, case_sensitive: false, allow_nil: true)
t = Topic.new("title" => "I'm unique!", :parent_id => 2)
assert t.save, "Should save t as unique"
@@ -218,17 +257,17 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert t.save, "Should still save t as unique"
t2 = Topic.new("title" => "I'm UNIQUE!", :parent_id => 1)
- assert !t2.valid?, "Shouldn't be valid"
- assert !t2.save, "Shouldn't save t2 as unique"
- assert t2.errors[:title].any?
- assert t2.errors[:parent_id].any?
+ assert_not t2.valid?, "Shouldn't be valid"
+ assert_not t2.save, "Shouldn't save t2 as unique"
+ assert_predicate t2.errors[:title], :any?
+ assert_predicate t2.errors[:parent_id], :any?
assert_equal ["has already been taken"], t2.errors[:title]
t2.title = "I'm truly UNIQUE!"
- assert !t2.valid?, "Shouldn't be valid"
- assert !t2.save, "Shouldn't save t2 as unique"
- assert t2.errors[:title].empty?
- assert t2.errors[:parent_id].any?
+ assert_not t2.valid?, "Shouldn't be valid"
+ assert_not t2.save, "Shouldn't save t2 as unique"
+ assert_empty t2.errors[:title]
+ assert_predicate t2.errors[:parent_id], :any?
t2.parent_id = 4
assert t2.save, "Should now save t2 as unique"
@@ -242,15 +281,15 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert t_utf8.save, "Should save t_utf8 as unique"
# If database hasn't UTF-8 character set, this test fails
- if Topic.all.merge!(:select => 'LOWER(title) AS title').find(t_utf8.id).title == "я тоже уникальный!"
+ if Topic.all.merge!(select: "LOWER(title) AS title").find(t_utf8.id).title == "я тоже уникальный!"
t2_utf8 = Topic.new("title" => "я тоже УНИКАЛЬНЫЙ!")
- assert !t2_utf8.valid?, "Shouldn't be valid"
- assert !t2_utf8.save, "Shouldn't save t2_utf8 as unique"
+ assert_not t2_utf8.valid?, "Shouldn't be valid"
+ assert_not t2_utf8.save, "Shouldn't save t2_utf8 as unique"
end
end
def test_validate_case_sensitive_uniqueness_with_special_sql_like_chars
- Topic.validates_uniqueness_of(:title, :case_sensitive => true)
+ Topic.validates_uniqueness_of(:title, case_sensitive: true)
t = Topic.new("title" => "I'm unique!")
assert t.save, "Should save t as unique"
@@ -263,7 +302,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
end
def test_validate_case_insensitive_uniqueness_with_special_sql_like_chars
- Topic.validates_uniqueness_of(:title, :case_sensitive => false)
+ Topic.validates_uniqueness_of(:title, case_sensitive: false)
t = Topic.new("title" => "I'm unique!")
assert t.save, "Should save t as unique"
@@ -276,7 +315,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
end
def test_validate_case_sensitive_uniqueness
- Topic.validates_uniqueness_of(:title, :case_sensitive => true, :allow_nil => true)
+ Topic.validates_uniqueness_of(:title, case_sensitive: true, allow_nil: true)
t = Topic.new("title" => "I'm unique!")
assert t.save, "Should save t as unique"
@@ -287,41 +326,41 @@ class UniquenessValidationTest < ActiveRecord::TestCase
t2 = Topic.new("title" => "I'M UNIQUE!")
assert t2.valid?, "Should be valid"
assert t2.save, "Should save t2 as unique"
- assert t2.errors[:title].empty?
- assert t2.errors[:parent_id].empty?
+ assert_empty t2.errors[:title]
+ assert_empty t2.errors[:parent_id]
assert_not_equal ["has already been taken"], t2.errors[:title]
t3 = Topic.new("title" => "I'M uNiQUe!")
assert t3.valid?, "Should be valid"
assert t3.save, "Should save t2 as unique"
- assert t3.errors[:title].empty?
- assert t3.errors[:parent_id].empty?
+ assert_empty t3.errors[:title]
+ assert_empty t3.errors[:parent_id]
assert_not_equal ["has already been taken"], t3.errors[:title]
end
def test_validate_case_sensitive_uniqueness_with_attribute_passed_as_integer
- Topic.validates_uniqueness_of(:title, :case_sensitive => true)
- Topic.create!('title' => 101)
+ Topic.validates_uniqueness_of(:title, case_sensitive: true)
+ Topic.create!("title" => 101)
- t2 = Topic.new('title' => 101)
- assert !t2.valid?
+ t2 = Topic.new("title" => 101)
+ assert_not_predicate t2, :valid?
assert t2.errors[:title]
end
def test_validate_uniqueness_with_non_standard_table_names
- i1 = WarehouseThing.create(:value => 1000)
- assert !i1.valid?, "i1 should not be valid"
+ i1 = WarehouseThing.create(value: 1000)
+ assert_not i1.valid?, "i1 should not be valid"
assert i1.errors[:value].any?, "Should not be empty"
end
def test_validates_uniqueness_inside_scoping
Topic.validates_uniqueness_of(:title)
- Topic.where(:author_name => "David").scoping do
+ Topic.where(author_name: "David").scoping do
t1 = Topic.new("title" => "I'm unique!", "author_name" => "Mary")
assert t1.save
t2 = Topic.new("title" => "I'm unique!", "author_name" => "David")
- assert !t2.valid?
+ assert_not_predicate t2, :valid?
end
end
@@ -335,46 +374,68 @@ class UniquenessValidationTest < ActiveRecord::TestCase
end
def test_validate_uniqueness_with_limit
- # Event.title is limited to 5 characters
- e1 = Event.create(:title => "abcde")
- assert e1.valid?, "Could not create an event with a unique, 5 character title"
- e2 = Event.create(:title => "abcdefgh")
- assert !e2.valid?, "Created an event whose title, with limit taken into account, is not unique"
+ if current_adapter?(:SQLite3Adapter)
+ # Event.title has limit 5, but SQLite doesn't truncate.
+ e1 = Event.create(title: "abcdefgh")
+ assert e1.valid?, "Could not create an event with a unique 8 characters title"
+
+ e2 = Event.create(title: "abcdefgh")
+ assert_not e2.valid?, "Created an event whose title is not unique"
+ elsif current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :OracleAdapter, :SQLServerAdapter)
+ assert_raise(ActiveRecord::ValueTooLong) do
+ Event.create(title: "abcdefgh")
+ end
+ else
+ assert_raise(ActiveRecord::StatementInvalid) do
+ Event.create(title: "abcdefgh")
+ end
+ end
end
def test_validate_uniqueness_with_limit_and_utf8
- # Event.title is limited to 5 characters
- e1 = Event.create(:title => "一二三四五")
- assert e1.valid?, "Could not create an event with a unique, 5 character title"
- e2 = Event.create(:title => "一二三四五六七八")
- assert !e2.valid?, "Created an event whose title, with limit taken into account, is not unique"
+ if current_adapter?(:SQLite3Adapter)
+ # 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, :OracleAdapter, :SQLServerAdapter)
+ assert_raise(ActiveRecord::ValueTooLong) do
+ Event.create(title: "一二三四五六七八")
+ end
+ else
+ assert_raise(ActiveRecord::StatementInvalid) do
+ Event.create(title: "一二三四五六七八")
+ end
+ end
end
def test_validate_straight_inheritance_uniqueness
- w1 = IneptWizard.create(:name => "Rincewind", :city => "Ankh-Morpork")
+ w1 = IneptWizard.create(name: "Rincewind", city: "Ankh-Morpork")
assert w1.valid?, "Saving w1"
# Should use validation from base class (which is abstract)
- w2 = IneptWizard.new(:name => "Rincewind", :city => "Quirm")
- assert !w2.valid?, "w2 shouldn't be valid"
+ w2 = IneptWizard.new(name: "Rincewind", city: "Quirm")
+ assert_not w2.valid?, "w2 shouldn't be valid"
assert w2.errors[:name].any?, "Should have errors for name"
assert_equal ["has already been taken"], w2.errors[:name], "Should have uniqueness message for name"
- w3 = Conjurer.new(:name => "Rincewind", :city => "Quirm")
- assert !w3.valid?, "w3 shouldn't be valid"
+ w3 = Conjurer.new(name: "Rincewind", city: "Quirm")
+ assert_not w3.valid?, "w3 shouldn't be valid"
assert w3.errors[:name].any?, "Should have errors for name"
assert_equal ["has already been taken"], w3.errors[:name], "Should have uniqueness message for name"
- w4 = Conjurer.create(:name => "The Amazing Bonko", :city => "Quirm")
+ w4 = Conjurer.create(name: "The Amazing Bonko", city: "Quirm")
assert w4.valid?, "Saving w4"
- w5 = Thaumaturgist.new(:name => "The Amazing Bonko", :city => "Lancre")
- assert !w5.valid?, "w5 shouldn't be valid"
+ w5 = Thaumaturgist.new(name: "The Amazing Bonko", city: "Lancre")
+ assert_not w5.valid?, "w5 shouldn't be valid"
assert w5.errors[:name].any?, "Should have errors for name"
assert_equal ["has already been taken"], w5.errors[:name], "Should have uniqueness message for name"
- w6 = Thaumaturgist.new(:name => "Mustrum Ridcully", :city => "Quirm")
- assert !w6.valid?, "w6 shouldn't be valid"
+ w6 = Thaumaturgist.new(name: "Mustrum Ridcully", city: "Quirm")
+ assert_not w6.valid?, "w6 shouldn't be valid"
assert w6.errors[:city].any?, "Should have errors for city"
assert_equal ["has already been taken"], w6.errors[:city], "Should have uniqueness message for city"
end
@@ -385,7 +446,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
Topic.create("title" => "I'm an unapproved topic", "approved" => false)
t3 = Topic.new("title" => "I'm a topic", "approved" => true)
- assert !t3.valid?, "t3 shouldn't be valid"
+ assert_not t3.valid?, "t3 shouldn't be valid"
t4 = Topic.new("title" => "I'm an unapproved topic", "approved" => false)
assert t4.valid?, "t4 should be valid"
@@ -399,32 +460,98 @@ class UniquenessValidationTest < ActiveRecord::TestCase
def test_validate_uniqueness_on_existing_relation
event = Event.create
- assert TopicWithUniqEvent.create(event: event).valid?
+ assert_predicate TopicWithUniqEvent.create(event: event), :valid?
topic = TopicWithUniqEvent.new(event: event)
- assert_not topic.valid?
- assert_equal ['has already been taken'], topic.errors[:event]
+ assert_not_predicate topic, :valid?
+ assert_equal ["has already been taken"], topic.errors[:event]
end
def test_validate_uniqueness_on_empty_relation
topic = TopicWithUniqEvent.new
- assert topic.valid?
+ assert_predicate topic, :valid?
+ end
+
+ def test_validate_uniqueness_of_custom_primary_key
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "keyboards"
+ self.primary_key = :key_number
+
+ validates_uniqueness_of :key_number
+
+ def self.name
+ "Keyboard"
+ end
+ end
+
+ klass.create!(key_number: 10)
+ key2 = klass.create!(key_number: 11)
+
+ key2.key_number = 10
+ assert_not_predicate key2, :valid?
+ end
+
+ def test_validate_uniqueness_without_primary_key
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "dashboards"
+
+ validates_uniqueness_of :dashboard_id
+
+ def self.name; "Dashboard" end
+ end
+
+ abc = klass.create!(dashboard_id: "abc")
+ assert_predicate klass.new(dashboard_id: "xyz"), :valid?
+ assert_not_predicate klass.new(dashboard_id: "abc"), :valid?
+
+ abc.dashboard_id = "def"
+
+ e = assert_raises ActiveRecord::UnknownPrimaryKey do
+ abc.save!
+ end
+ assert_match(/\AUnknown primary key for table dashboards in model/, e.message)
+ assert_match(/Can not validate uniqueness for persisted record without primary key.\z/, e.message)
end
- def test_does_not_validate_uniqueness_of_if_parent_record_is_validate_false
- Reply.validates_uniqueness_of(:content)
+ def test_validate_uniqueness_ignores_itself_when_primary_key_changed
+ Topic.validates_uniqueness_of(:title)
+
+ t = Topic.new("title" => "This is a unique title")
+ assert t.save, "Should save t as unique"
+
+ t.id += 1
+ assert t.valid?, "Should be valid"
+ assert t.save, "Should still save t as unique"
+ end
- Reply.create!(content: "Topic Title")
+ def test_validate_uniqueness_with_after_create_performing_save
+ TopicWithAfterCreate.validates_uniqueness_of(:title)
+ topic = TopicWithAfterCreate.create!(title: "Title1")
+ assert topic.author_name.start_with?("Title1")
+
+ topic2 = TopicWithAfterCreate.new(title: "Title1")
+ assert_not_predicate topic2, :valid?
+ assert_equal(["has already been taken"], topic2.errors[:title])
+ end
+
+ def test_validate_uniqueness_uuid
+ skip unless current_adapter?(:PostgreSQLAdapter)
+ item = UuidItem.create!(uuid: SecureRandom.uuid, title: "item1")
+ item.update(title: "item1-title2")
+ assert_empty item.errors
+
+ item2 = UuidValidatingItem.create!(uuid: SecureRandom.uuid, title: "item2")
+ item2.update(title: "item2-title2")
+ assert_empty item2.errors
+ end
- reply = Reply.new(content: "Topic Title")
- reply.save!(validate: false)
- assert reply.persisted?
+ def test_validate_uniqueness_regular_id
+ item = CoolTopic.create!(title: "MyItem")
+ assert_empty item.errors
- topic = Topic.new(reply_ids: [reply.id])
- topic.save!
+ item2 = CoolTopic.new(id: item.id, title: "MyItem2")
+ assert_not_predicate item2, :valid?
- assert_equal topic.replies.size, 1
- assert reply.valid?
- assert topic.valid?
+ assert_equal(["has already been taken"], item2.errors[:id])
end
end
diff --git a/activerecord/test/cases/validations_repair_helper.rb b/activerecord/test/cases/validations_repair_helper.rb
index b30666d876..6dc3b64b2b 100644
--- a/activerecord/test/cases/validations_repair_helper.rb
+++ b/activerecord/test/cases/validations_repair_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ValidationsRepairHelper
extend ActiveSupport::Concern
diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb
index f4f316f393..66763c727f 100644
--- a/activerecord/test/cases/validations_test.rb
+++ b/activerecord/test/cases/validations_test.rb
@@ -1,11 +1,13 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/topic'
-require 'models/reply'
-require 'models/person'
-require 'models/developer'
-require 'models/computer'
-require 'models/parrot'
-require 'models/company'
+require "models/topic"
+require "models/reply"
+require "models/developer"
+require "models/computer"
+require "models/parrot"
+require "models/company"
+require "models/price_estimate"
class ValidationsTest < ActiveRecord::TestCase
fixtures :topics, :developers
@@ -17,7 +19,7 @@ class ValidationsTest < ActiveRecord::TestCase
def test_valid_uses_create_context_when_new
r = WrongReply.new
r.title = "Wrong Create"
- assert_not r.valid?
+ assert_not_predicate r, :valid?
assert r.errors[:title].any?, "A reply with a bad title should mark that attribute as invalid"
assert_equal ["is Wrong Create"], r.errors[:title], "A reply with a bad content should contain an error"
end
@@ -36,8 +38,8 @@ class ValidationsTest < ActiveRecord::TestCase
end
def test_valid_using_special_context
- r = WrongReply.new(:title => "Valid title")
- assert !r.valid?(:special_case)
+ r = WrongReply.new(title: "Valid title")
+ assert_not r.valid?(:special_case)
assert_equal "Invalid", r.errors[:author_name].join
r.author_name = "secret"
@@ -52,6 +54,13 @@ class ValidationsTest < ActiveRecord::TestCase
assert r.valid?(:special_case)
end
+ def test_invalid_using_multiple_contexts
+ r = WrongReply.new(title: "Wrong Create")
+ assert r.invalid?([:special_case, :create])
+ assert_equal "Invalid", r.errors[:author_name].join
+ assert_equal "is Wrong Create", r.errors[:title].join
+ end
+
def test_validate
r = WrongReply.new
@@ -88,7 +97,7 @@ class ValidationsTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::RecordInvalid) do
WrongReply.new.validate!(:special_case)
end
- r = WrongReply.new(:title => "Valid title", :author_name => "secret", :content => "Good")
+ r = WrongReply.new(title: "Valid title", author_name: "secret", content: "Good")
assert r.validate!(:special_case)
end
@@ -100,7 +109,7 @@ class ValidationsTest < ActiveRecord::TestCase
def test_exception_on_create_bang_with_block
assert_raise(ActiveRecord::RecordInvalid) do
- WrongReply.create!({ "title" => "OK" }) do |r|
+ WrongReply.create!("title" => "OK") do |r|
r.content = nil
end
end
@@ -116,21 +125,21 @@ class ValidationsTest < ActiveRecord::TestCase
def test_save_without_validation
reply = WrongReply.new
- assert !reply.save
- assert reply.save(:validate => false)
+ assert_not reply.save
+ assert reply.save(validate: false)
end
def test_validates_acceptance_of_with_non_existent_table
Object.const_set :IncorporealModel, Class.new(ActiveRecord::Base)
- assert_nothing_raised ActiveRecord::StatementInvalid do
+ assert_nothing_raised do
IncorporealModel.validates_acceptance_of(:incorporeal_column)
end
end
def test_throw_away_typing
d = Developer.new("name" => "David", "salary" => "100,000")
- assert !d.valid?
+ assert_not_predicate d, :valid?
assert_equal 100, d.salary
assert_equal "100,000", d.salary_before_type_cast
end
@@ -149,16 +158,55 @@ class ValidationsTest < ActiveRecord::TestCase
end
def test_numericality_validation_with_mutation
- Topic.class_eval do
+ klass = Class.new(Topic) do
attribute :wibble, :string
validates_numericality_of :wibble, only_integer: true
end
- topic = Topic.new(wibble: '123-4567')
- topic.wibble.gsub!('-', '')
+ topic = klass.new(wibble: "123-4567")
+ topic.wibble.gsub!("-", "")
- assert topic.valid?
- ensure
- Topic.reset_column_information
+ assert_predicate topic, :valid?
+ end
+
+ def test_numericality_validation_checks_against_raw_value
+ klass = Class.new(Topic) do
+ def self.model_name
+ ActiveModel::Name.new(self, nil, "Topic")
+ end
+ attribute :wibble, :decimal, scale: 2, precision: 9
+ validates_numericality_of :wibble, greater_than_or_equal_to: BigDecimal("97.18")
+ end
+
+ assert_not_predicate klass.new(wibble: "97.179"), :valid?
+ assert_not_predicate klass.new(wibble: 97.179), :valid?
+ assert_not_predicate klass.new(wibble: BigDecimal("97.179")), :valid?
+ end
+
+ def test_numericality_validator_wont_be_affected_by_custom_getter
+ price_estimate = PriceEstimate.new(price: 50)
+
+ assert_equal "$50.00", price_estimate.price
+ assert_equal 50, price_estimate.price_before_type_cast
+ assert_equal 50, price_estimate.read_attribute(:price)
+
+ assert_predicate price_estimate, :price_came_from_user?
+ assert_predicate price_estimate, :valid?
+
+ price_estimate.save!
+
+ assert_not_predicate price_estimate, :price_came_from_user?
+ assert_predicate price_estimate, :valid?
+ end
+
+ def test_acceptance_validator_doesnt_require_db_connection
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "posts"
+ end
+ klass.reset_column_information
+
+ assert_no_queries do
+ klass.validates_acceptance_of(:foo)
+ end
end
end
diff --git a/activerecord/test/cases/view_test.rb b/activerecord/test/cases/view_test.rb
index f9dca1e196..7e2d66c62a 100644
--- a/activerecord/test/cases/view_test.rb
+++ b/activerecord/test/cases/view_test.rb
@@ -1,7 +1,11 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "models/book"
+require "support/schema_dumping_helper"
module ViewBehavior
+ include SchemaDumpingHelper
extend ActiveSupport::Concern
included do
@@ -9,20 +13,21 @@ module ViewBehavior
end
class Ebook < ActiveRecord::Base
+ self.table_name = "ebooks'"
self.primary_key = "id"
end
def setup
super
@connection = ActiveRecord::Base.connection
- create_view "ebooks", <<-SQL
+ create_view "ebooks'", <<-SQL
SELECT id, name, status FROM books WHERE format = 'ebook'
SQL
end
def teardown
super
- drop_view "ebooks"
+ drop_view "ebooks'"
end
def test_reading
@@ -31,9 +36,23 @@ module ViewBehavior
assert_equal ["Ruby for Rails"], books.map(&:name)
end
+ def test_views
+ assert_equal [Ebook.table_name], @connection.views
+ end
+
+ def test_view_exists
+ view_name = Ebook.table_name
+ assert @connection.view_exists?(view_name), "'#{view_name}' view should exist"
+ end
+
def test_table_exists
view_name = Ebook.table_name
- 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
+ view_name = Ebook.table_name
+ assert @connection.data_source_exists?(view_name), "'#{view_name}' should be a data source"
end
def test_column_definitions
@@ -43,71 +62,163 @@ module ViewBehavior
end
def test_attributes
- assert_equal({"id" => 2, "name" => "Ruby for Rails", "status" => 0},
+ assert_equal({ "id" => 2, "name" => "Ruby for Rails", "status" => 0 },
Ebook.first.attributes)
end
def test_does_not_assume_id_column_as_primary_key
model = Class.new(ActiveRecord::Base) do
- self.table_name = "ebooks"
+ self.table_name = "ebooks'"
end
assert_nil model.primary_key
end
+
+ def test_does_not_dump_view_as_table
+ schema = dump_table_schema "ebooks'"
+ assert_no_match %r{create_table "ebooks'"}, schema
+ end
+
+ private
+ def quote_table_name(name)
+ @connection.quote_table_name(name)
+ end
end
if ActiveRecord::Base.connection.supports_views?
-class ViewWithPrimaryKeyTest < ActiveRecord::TestCase
- include ViewBehavior
+ class ViewWithPrimaryKeyTest < ActiveRecord::TestCase
+ include ViewBehavior
- private
- def create_view(name, query)
- @connection.execute "CREATE VIEW #{name} AS #{query}"
- end
+ private
+ def create_view(name, query)
+ @connection.execute "CREATE VIEW #{quote_table_name(name)} AS #{query}"
+ end
- def drop_view(name)
- @connection.execute "DROP VIEW #{name}" if @connection.table_exists? name
+ def drop_view(name)
+ @connection.execute "DROP VIEW #{quote_table_name(name)}" if @connection.view_exists? name
+ end
end
-end
-class ViewWithoutPrimaryKeyTest < ActiveRecord::TestCase
- fixtures :books
+ class ViewWithoutPrimaryKeyTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
+ fixtures :books
- class Paperback < ActiveRecord::Base; end
+ class Paperback < ActiveRecord::Base; end
- setup do
- @connection = ActiveRecord::Base.connection
- @connection.execute <<-SQL
- CREATE VIEW paperbacks
- AS SELECT name, status FROM books WHERE format = 'paperback'
- SQL
- end
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.execute <<-SQL
+ CREATE VIEW paperbacks
+ AS SELECT name, status FROM books WHERE format = 'paperback'
+ SQL
+ end
- teardown do
- @connection.execute "DROP VIEW paperbacks" if @connection.table_exists? "paperbacks"
- end
+ teardown do
+ @connection.execute "DROP VIEW paperbacks" if @connection.view_exists? "paperbacks"
+ end
- def test_reading
- books = Paperback.all
- assert_equal ["Agile Web Development with Rails"], books.map(&:name)
- end
+ def test_reading
+ books = Paperback.all
+ assert_equal ["Agile Web Development with Rails"], books.map(&:name)
+ end
- def test_table_exists
- view_name = Paperback.table_name
- assert @connection.table_exists?(view_name), "'#{view_name}' table should exist"
- end
+ def test_views
+ assert_equal [Paperback.table_name], @connection.views
+ end
- def test_column_definitions
- assert_equal([["name", :string],
- ["status", :integer]], Paperback.columns.map { |c| [c.name, c.type] })
- end
+ def test_view_exists
+ view_name = Paperback.table_name
+ assert @connection.view_exists?(view_name), "'#{view_name}' view should exist"
+ end
- def test_attributes
- assert_equal({"name" => "Agile Web Development with Rails", "status" => 2},
- Paperback.first.attributes)
+ def test_table_exists
+ view_name = Paperback.table_name
+ assert_not @connection.table_exists?(view_name), "'#{view_name}' table should not exist"
+ end
+
+ def test_column_definitions
+ assert_equal([["name", :string],
+ ["status", :integer]], Paperback.columns.map { |c| [c.name, c.type] })
+ end
+
+ def test_attributes
+ assert_equal({ "name" => "Agile Web Development with Rails", "status" => 2 },
+ Paperback.first.attributes)
+ end
+
+ def test_does_not_have_a_primary_key
+ assert_nil Paperback.primary_key
+ end
+
+ def test_does_not_dump_view_as_table
+ schema = dump_table_schema "paperbacks"
+ assert_no_match %r{create_table "paperbacks"}, schema
+ end
end
- def test_does_not_have_a_primary_key
- assert_nil Paperback.primary_key
+ # sqlite dose not support CREATE, INSERT, and DELETE for VIEW
+ if current_adapter?(:Mysql2Adapter, :SQLServerAdapter) ||
+ current_adapter?(:PostgreSQLAdapter) && ActiveRecord::Base.connection.postgresql_version >= 90300
+
+ class UpdateableViewTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
+ fixtures :books
+
+ class PrintedBook < ActiveRecord::Base
+ self.primary_key = "id"
+ end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.execute <<-SQL
+ CREATE VIEW printed_books
+ AS SELECT id, name, status, format FROM books WHERE format = 'paperback'
+ SQL
+ end
+
+ teardown do
+ @connection.execute "DROP VIEW printed_books" if @connection.view_exists? "printed_books"
+ end
+
+ def test_update_record
+ book = PrintedBook.first
+ book.name = "AWDwR"
+ book.save!
+ book.reload
+ assert_equal "AWDwR", book.name
+ end
+
+ def test_insert_record
+ PrintedBook.create! name: "Rails in Action", status: 0, format: "paperback"
+
+ new_book = PrintedBook.last
+ assert_equal "Rails in Action", new_book.name
+ end
+
+ def test_update_record_to_fail_view_conditions
+ book = PrintedBook.first
+ book.format = "ebook"
+ book.save!
+
+ assert_raises ActiveRecord::RecordNotFound do
+ book.reload
+ end
+ end
+ end
+ 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?
+ class MaterializedViewTest < ActiveRecord::PostgreSQLTestCase
+ include ViewBehavior
+
+ private
+ def create_view(name, query)
+ @connection.execute "CREATE MATERIALIZED VIEW #{quote_table_name(name)} AS #{query}"
+ end
+
+ def drop_view(name)
+ @connection.execute "DROP MATERIALIZED VIEW #{quote_table_name(name)}" if @connection.view_exists? name
+ end
end
end
-end
diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb
deleted file mode 100644
index b30b50f597..0000000000
--- a/activerecord/test/cases/xml_serialization_test.rb
+++ /dev/null
@@ -1,447 +0,0 @@
-require "cases/helper"
-require "rexml/document"
-require 'models/contact'
-require 'models/post'
-require 'models/author'
-require 'models/comment'
-require 'models/company_in_module'
-require 'models/toy'
-require 'models/topic'
-require 'models/reply'
-require 'models/company'
-
-class XmlSerializationTest < ActiveRecord::TestCase
- def test_should_serialize_default_root
- @xml = Contact.new.to_xml
- assert_match %r{^<contact>}, @xml
- assert_match %r{</contact>$}, @xml
- end
-
- def test_should_serialize_default_root_with_namespace
- @xml = Contact.new.to_xml :namespace=>"http://xml.rubyonrails.org/contact"
- assert_match %r{^<contact xmlns="http://xml\.rubyonrails\.org/contact">}, @xml
- assert_match %r{</contact>$}, @xml
- end
-
- def test_should_serialize_custom_root
- @xml = Contact.new.to_xml :root => 'xml_contact'
- assert_match %r{^<xml-contact>}, @xml
- assert_match %r{</xml-contact>$}, @xml
- end
-
- def test_should_allow_undasherized_tags
- @xml = Contact.new.to_xml :root => 'xml_contact', :dasherize => false
- assert_match %r{^<xml_contact>}, @xml
- assert_match %r{</xml_contact>$}, @xml
- assert_match %r{<created_at}, @xml
- end
-
- def test_should_allow_camelized_tags
- @xml = Contact.new.to_xml :root => 'xml_contact', :camelize => true
- assert_match %r{^<XmlContact>}, @xml
- assert_match %r{</XmlContact>$}, @xml
- assert_match %r{<CreatedAt}, @xml
- end
-
- def test_should_allow_skipped_types
- @xml = Contact.new(:age => 25).to_xml :skip_types => true
- assert %r{<age>25</age>}.match(@xml)
- end
-
- def test_should_include_yielded_additions
- @xml = Contact.new.to_xml do |xml|
- xml.creator "David"
- end
- assert_match %r{<creator>David</creator>}, @xml
- end
-
- def test_to_xml_with_block
- value = "Rockin' the block"
- xml = Contact.new.to_xml(:skip_instruct => true) do |_xml|
- _xml.tag! "arbitrary-element", value
- end
- assert_equal "<contact>", xml.first(9)
- assert xml.include?(%(<arbitrary-element>#{value}</arbitrary-element>))
- end
-
- def test_should_skip_instruct_for_included_records
- @contact = Contact.new
- @contact.alternative = Contact.new(:name => 'Copa Cabana')
- @xml = @contact.to_xml(:include => [ :alternative ])
- assert_equal @xml.index('<?xml '), 0
- assert_nil @xml.index('<?xml ', 1)
- end
-end
-
-class DefaultXmlSerializationTest < ActiveRecord::TestCase
- def setup
- @contact = Contact.new(
- :name => 'aaron stack',
- :age => 25,
- :avatar => 'binarydata',
- :created_at => Time.utc(2006, 8, 1),
- :awesome => false,
- :preferences => { :gem => 'ruby' }
- )
- end
-
- def test_should_serialize_string
- assert_match %r{<name>aaron stack</name>}, @contact.to_xml
- end
-
- def test_should_serialize_integer
- assert_match %r{<age type="integer">25</age>}, @contact.to_xml
- end
-
- def test_should_serialize_binary
- xml = @contact.to_xml
- assert_match %r{YmluYXJ5ZGF0YQ==\n</avatar>}, xml
- assert_match %r{<avatar(.*)(type="binary")}, xml
- assert_match %r{<avatar(.*)(encoding="base64")}, xml
- end
-
- def test_should_serialize_datetime
- assert_match %r{<created-at type=\"dateTime\">2006-08-01T00:00:00Z</created-at>}, @contact.to_xml
- end
-
- def test_should_serialize_boolean
- assert_match %r{<awesome type=\"boolean\">false</awesome>}, @contact.to_xml
- end
-
- def test_should_serialize_hash
- assert_match %r{<preferences>\s*<gem>ruby</gem>\s*</preferences>}m, @contact.to_xml
- end
-
- def test_uses_serializable_hash_with_only_option
- def @contact.serializable_hash(options=nil)
- super(only: %w(name))
- end
-
- xml = @contact.to_xml
- assert_match %r{<name>aaron stack</name>}, xml
- assert_no_match %r{age}, xml
- assert_no_match %r{awesome}, xml
- end
-
- def test_uses_serializable_hash_with_except_option
- def @contact.serializable_hash(options=nil)
- super(except: %w(age))
- end
-
- xml = @contact.to_xml
- assert_match %r{<name>aaron stack</name>}, xml
- assert_match %r{<awesome type=\"boolean\">false</awesome>}, xml
- assert_no_match %r{age}, xml
- end
-
- def test_does_not_include_inheritance_column_from_sti
- @contact = ContactSti.new(@contact.attributes)
- assert_equal 'ContactSti', @contact.type
-
- xml = @contact.to_xml
- assert_match %r{<name>aaron stack</name>}, xml
- assert_no_match %r{<type}, xml
- assert_no_match %r{ContactSti}, xml
- end
-
- def test_serializable_hash_with_default_except_option_and_excluding_inheritance_column_from_sti
- @contact = ContactSti.new(@contact.attributes)
- assert_equal 'ContactSti', @contact.type
-
- def @contact.serializable_hash(options={})
- super({ except: %w(age) }.merge!(options))
- end
-
- xml = @contact.to_xml
- assert_match %r{<name>aaron stack</name>}, xml
- assert_no_match %r{age}, xml
- assert_no_match %r{<type}, xml
- assert_no_match %r{ContactSti}, xml
- end
-end
-
-class DefaultXmlSerializationTimezoneTest < ActiveRecord::TestCase
- def test_should_serialize_datetime_with_timezone
- with_timezone_config zone: "Pacific Time (US & Canada)" do
- toy = Toy.create(:name => 'Mickey', :updated_at => Time.utc(2006, 8, 1))
- assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml
- end
- end
-
- def test_should_serialize_datetime_with_timezone_reloaded
- with_timezone_config zone: "Pacific Time (US & Canada)" do
- toy = Toy.create(:name => 'Minnie', :updated_at => Time.utc(2006, 8, 1)).reload
- assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml
- end
- end
-end
-
-class NilXmlSerializationTest < ActiveRecord::TestCase
- def setup
- @xml = Contact.new.to_xml(:root => 'xml_contact')
- end
-
- def test_should_serialize_string
- assert_match %r{<name nil="true"/>}, @xml
- end
-
- def test_should_serialize_integer
- assert %r{<age (.*)/>}.match(@xml)
- attributes = $1
- assert_match %r{nil="true"}, attributes
- assert_match %r{type="integer"}, attributes
- end
-
- def test_should_serialize_binary
- assert %r{<avatar (.*)/>}.match(@xml)
- attributes = $1
- assert_match %r{type="binary"}, attributes
- assert_match %r{encoding="base64"}, attributes
- assert_match %r{nil="true"}, attributes
- end
-
- def test_should_serialize_datetime
- assert %r{<created-at (.*)/>}.match(@xml)
- attributes = $1
- assert_match %r{nil="true"}, attributes
- assert_match %r{type="dateTime"}, attributes
- end
-
- def test_should_serialize_boolean
- assert %r{<awesome (.*)/>}.match(@xml)
- attributes = $1
- assert_match %r{type="boolean"}, attributes
- assert_match %r{nil="true"}, attributes
- end
-
- def test_should_serialize_yaml
- assert_match %r{<preferences nil=\"true\"/>}, @xml
- end
-end
-
-class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase
- fixtures :topics, :companies, :accounts, :authors, :posts, :projects
-
- def test_to_xml
- xml = REXML::Document.new(topics(:first).to_xml(:indent => 0))
- bonus_time_in_current_timezone = topics(:first).bonus_time.xmlschema
- written_on_in_current_timezone = topics(:first).written_on.xmlschema
-
- assert_equal "topic", xml.root.name
- assert_equal "The First Topic" , xml.elements["//title"].text
- assert_equal "David" , xml.elements["//author-name"].text
- assert_match "Have a nice day", xml.elements["//content"].text
-
- assert_equal "1", xml.elements["//id"].text
- assert_equal "integer" , xml.elements["//id"].attributes['type']
-
- assert_equal "1", xml.elements["//replies-count"].text
- assert_equal "integer" , xml.elements["//replies-count"].attributes['type']
-
- assert_equal written_on_in_current_timezone, xml.elements["//written-on"].text
- assert_equal "dateTime" , xml.elements["//written-on"].attributes['type']
-
- assert_equal "david@loudthinking.com", xml.elements["//author-email-address"].text
-
- assert_equal nil, xml.elements["//parent-id"].text
- assert_equal "integer", xml.elements["//parent-id"].attributes['type']
- assert_equal "true", xml.elements["//parent-id"].attributes['nil']
-
- # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb)
- assert_equal "2004-04-15", xml.elements["//last-read"].text
- assert_equal "date" , xml.elements["//last-read"].attributes['type']
-
- # Oracle and DB2 don't have true boolean or time-only fields
- unless current_adapter?(:OracleAdapter, :DB2Adapter)
- assert_equal "false", xml.elements["//approved"].text
- assert_equal "boolean" , xml.elements["//approved"].attributes['type']
-
- assert_equal bonus_time_in_current_timezone, xml.elements["//bonus-time"].text
- assert_equal "dateTime" , xml.elements["//bonus-time"].attributes['type']
- end
- end
-
- def test_except_option
- xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => [:title, :replies_count])
- assert_equal "<topic>", xml.first(7)
- assert !xml.include?(%(<title>The First Topic</title>))
- assert xml.include?(%(<author-name>David</author-name>))
-
- xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => [:title, :author_name, :replies_count])
- assert !xml.include?(%(<title>The First Topic</title>))
- assert !xml.include?(%(<author-name>David</author-name>))
- end
-
- # to_xml used to mess with the hash the user provided which
- # caused the builder to be reused. This meant the document kept
- # getting appended to.
-
- def test_modules
- projects = MyApplication::Business::Project.all
- xml = projects.to_xml
- root = projects.first.class.to_s.underscore.pluralize.tr('/','_').dasherize
- assert_match "<#{root} type=\"array\">", xml
- assert_match "</#{root}>", xml
- end
-
- def test_passing_hash_shouldnt_reuse_builder
- options = {:include=>:posts}
- david = authors(:david)
- first_xml_size = david.to_xml(options).size
- second_xml_size = david.to_xml(options).size
- assert_equal first_xml_size, second_xml_size
- end
-
- def test_include_uses_association_name
- xml = authors(:david).to_xml :include=>:hello_posts, :indent => 0
- assert_match %r{<hello-posts type="array">}, xml
- assert_match %r{<hello-post type="Post">}, xml
- assert_match %r{<hello-post type="StiPost">}, xml
- end
-
- def test_included_associations_should_skip_types
- xml = authors(:david).to_xml :include=>:hello_posts, :indent => 0, :skip_types => true
- assert_match %r{<hello-posts>}, xml
- assert_match %r{<hello-post>}, xml
- assert_match %r{<hello-post>}, xml
- end
-
- def test_including_has_many_association
- xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :include => :replies, :except => :replies_count)
- assert_equal "<topic>", xml.first(7)
- assert xml.include?(%(<replies type="array"><reply>))
- assert xml.include?(%(<title>The Second Topic of the day</title>))
- end
-
- def test_including_belongs_to_association
- xml = companies(:first_client).to_xml(:indent => 0, :skip_instruct => true, :include => :firm)
- assert !xml.include?("<firm>")
-
- xml = companies(:second_client).to_xml(:indent => 0, :skip_instruct => true, :include => :firm)
- assert xml.include?("<firm>")
- end
-
- def test_including_multiple_associations
- xml = companies(:first_firm).to_xml(:indent => 0, :skip_instruct => true, :include => [ :clients, :account ])
- assert_equal "<firm>", xml.first(6)
- assert xml.include?(%(<account>))
- assert xml.include?(%(<clients type="array"><client>))
- end
-
- def test_including_association_with_options
- xml = companies(:first_firm).to_xml(
- :indent => 0, :skip_instruct => true,
- :include => { :clients => { :only => :name } }
- )
-
- assert_equal "<firm>", xml.first(6)
- assert xml.include?(%(<client><name>Summit</name></client>))
- assert xml.include?(%(<clients type="array"><client>))
- end
-
- def test_methods_are_called_on_object
- xml = authors(:david).to_xml :methods => :label, :indent => 0
- assert_match %r{<label>.*</label>}, xml
- end
-
- def test_should_not_call_methods_on_associations_that_dont_respond
- xml = authors(:david).to_xml :include=>:hello_posts, :methods => :label, :indent => 2
- assert !authors(:david).hello_posts.first.respond_to?(:label)
- assert_match %r{^ <label>.*</label>}, xml
- assert_no_match %r{^ <label>}, xml
- end
-
- def test_procs_are_called_on_object
- proc = Proc.new { |options| options[:builder].tag!('nationality', 'Danish') }
- xml = authors(:david).to_xml(:procs => [ proc ])
- assert_match %r{<nationality>Danish</nationality>}, xml
- end
-
- def test_dual_arity_procs_are_called_on_object
- proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) }
- xml = authors(:david).to_xml(:procs => [ proc ])
- assert_match %r{<name-reverse>divaD</name-reverse>}, xml
- end
-
- def test_top_level_procs_arent_applied_to_associations
- author_proc = Proc.new { |options| options[:builder].tag!('nationality', 'Danish') }
- xml = authors(:david).to_xml(:procs => [ author_proc ], :include => :posts, :indent => 2)
-
- assert_match %r{^ <nationality>Danish</nationality>}, xml
- assert_no_match %r{^ {6}<nationality>Danish</nationality>}, xml
- end
-
- def test_procs_on_included_associations_are_called
- posts_proc = Proc.new { |options| options[:builder].tag!('copyright', 'DHH') }
- xml = authors(:david).to_xml(
- :indent => 2,
- :include => {
- :posts => { :procs => [ posts_proc ] }
- }
- )
-
- assert_no_match %r{^ <copyright>DHH</copyright>}, xml
- assert_match %r{^ {6}<copyright>DHH</copyright>}, xml
- end
-
- def test_should_include_empty_has_many_as_empty_array
- authors(:david).posts.delete_all
- xml = authors(:david).to_xml :include=>:posts, :indent => 2
-
- assert_equal [], Hash.from_xml(xml)['author']['posts']
- assert_match %r{^ <posts type="array"/>}, xml
- end
-
- def test_should_has_many_array_elements_should_include_type_when_different_from_guessed_value
- xml = authors(:david).to_xml :include=>:posts_with_comments, :indent => 2
-
- assert Hash.from_xml(xml)
- assert_match %r{^ <posts-with-comments type="array">}, xml
- assert_match %r{^ <posts-with-comment type="Post">}, xml
- assert_match %r{^ <posts-with-comment type="StiPost">}, xml
-
- types = Hash.from_xml(xml)['author']['posts_with_comments'].collect {|t| t['type'] }
- assert types.include?('SpecialPost')
- assert types.include?('Post')
- assert types.include?('StiPost')
- end
-
- def test_should_produce_xml_for_methods_returning_array
- xml = authors(:david).to_xml(:methods => :social)
- array = Hash.from_xml(xml)['author']['social']
- assert_equal 2, array.size
- assert array.include? 'twitter'
- assert array.include? 'github'
- end
-
- def test_should_support_aliased_attributes
- xml = Author.select("name as firstname").to_xml
- Author.all.each do |author|
- assert xml.include?(%(<firstname>#{author.name}</firstname>)), xml
- end
- end
-
- def test_array_to_xml_including_has_many_association
- xml = [ topics(:first), topics(:second) ].to_xml(:indent => 0, :skip_instruct => true, :include => :replies)
- assert xml.include?(%(<replies type="array"><reply>))
- end
-
- def test_array_to_xml_including_methods
- xml = [ topics(:first), topics(:second) ].to_xml(:indent => 0, :skip_instruct => true, :methods => [ :topic_id ])
- assert xml.include?(%(<topic-id type="integer">#{topics(:first).topic_id}</topic-id>)), xml
- assert xml.include?(%(<topic-id type="integer">#{topics(:second).topic_id}</topic-id>)), xml
- end
-
- def test_array_to_xml_including_has_one_association
- xml = [ companies(:first_firm), companies(:rails_core) ].to_xml(:indent => 0, :skip_instruct => true, :include => :account)
- assert xml.include?(companies(:first_firm).account.to_xml(:indent => 0, :skip_instruct => true))
- assert xml.include?(companies(:rails_core).account.to_xml(:indent => 0, :skip_instruct => true))
- end
-
- def test_array_to_xml_including_belongs_to_association
- xml = [ companies(:first_client), companies(:second_client), companies(:another_client) ].to_xml(:indent => 0, :skip_instruct => true, :include => :firm)
- assert xml.include?(companies(:first_client).to_xml(:indent => 0, :skip_instruct => true))
- assert xml.include?(companies(:second_client).firm.to_xml(:indent => 0, :skip_instruct => true))
- assert xml.include?(companies(:another_client).firm.to_xml(:indent => 0, :skip_instruct => true))
- end
-end
diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb
index 56909a8630..60ebdce178 100644
--- a/activerecord/test/cases/yaml_serialization_test.rb
+++ b/activerecord/test/cases/yaml_serialization_test.rb
@@ -1,15 +1,17 @@
-require 'cases/helper'
-require 'models/topic'
-require 'models/reply'
-require 'models/post'
-require 'models/author'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/topic"
+require "models/reply"
+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
- topic = Topic.new(:written_on => DateTime.now)
+ topic = Topic.new(written_on: DateTime.now)
assert_nothing_raised { topic.to_yaml }
end
end
@@ -22,8 +24,8 @@ class YamlSerializationTest < ActiveRecord::TestCase
end
def test_roundtrip_serialized_column
- topic = Topic.new(:content => {:omg=>:lol})
- assert_equal({:omg=>:lol}, YAML.load(YAML.dump(topic)).content)
+ topic = Topic.new(content: { omg: :lol })
+ assert_equal({ omg: :lol }, YAML.load(YAML.dump(topic)).content)
end
def test_psych_roundtrip
@@ -67,16 +69,16 @@ class YamlSerializationTest < ActiveRecord::TestCase
assert_not topic.new_record?, "Saved records are not new"
assert_not YAML.load(YAML.dump(topic)).new_record?, "Saved record should not be new after deserialization"
- topic = Topic.select('title').last
+ topic = Topic.select("title").last
assert_not topic.new_record?, "Loaded records without ID are not new"
assert_not YAML.load(YAML.dump(topic)).new_record?, "Record should not be new after deserialization"
end
def test_types_of_virtual_columns_are_not_changed_on_round_trip
- author = Author.select('authors.*, count(posts.id) as posts_count')
+ author = Author.select("authors.*, count(posts.id) as posts_count")
.joins(:posts)
- .group('authors.id')
+ .group("authors.id")
.first
dumped = YAML.load(YAML.dump(author))
@@ -88,14 +90,14 @@ class YamlSerializationTest < ActiveRecord::TestCase
coder = {}
Topic.first.encode_with(coder)
- assert coder['active_record_yaml_version']
+ assert coder["active_record_yaml_version"]
end
def test_deserializing_rails_41_yaml
topic = YAML.load(yaml_fixture("rails_4_1"))
- assert topic.new_record?
- assert_equal nil, topic.id
+ assert_predicate topic, :new_record?
+ assert_nil topic.id
assert_equal "The First Topic", topic.title
assert_equal({ omg: :lol }, topic.content)
end
@@ -103,19 +105,37 @@ class YamlSerializationTest < ActiveRecord::TestCase
def test_deserializing_rails_4_2_0_yaml
topic = YAML.load(yaml_fixture("rails_4_2_0"))
- assert_not topic.new_record?
+ assert_not_predicate topic, :new_record?
assert_equal 1, topic.id
assert_equal "The First Topic", topic.title
assert_equal("Have a nice day", topic.content)
end
- private
+ def test_yaml_encoding_keeps_mutations
+ author = Author.first
+ author.name = "Sean"
+ dumped = YAML.load(YAML.dump(author))
- def yaml_fixture(file_name)
- path = File.expand_path(
- "../../support/yaml_compatibility_fixtures/#{file_name}.yml",
- __FILE__
- )
- File.read(path)
+ assert_equal "Sean", dumped.name
+ assert_equal author.name_was, dumped.name_was
+ assert_equal author.changes, dumped.changes
end
+
+ def test_yaml_encoding_keeps_false_values
+ topic = Topic.first
+ topic.approved = false
+ dumped = YAML.load(YAML.dump(topic))
+
+ assert_equal false, dumped.approved
+ end
+
+ private
+
+ def yaml_fixture(file_name)
+ path = File.expand_path(
+ "../support/yaml_compatibility_fixtures/#{file_name}.yml",
+ __dir__
+ )
+ File.read(path)
+ end
end
diff --git a/activerecord/test/config.example.yml b/activerecord/test/config.example.yml
index e3b55d640e..18347cd07d 100644
--- a/activerecord/test/config.example.yml
+++ b/activerecord/test/config.example.yml
@@ -51,23 +51,15 @@ connections:
password: arunit
database: arunit2
- mysql:
- arunit:
- username: rails
- encoding: utf8
- collation: utf8_unicode_ci
- arunit2:
- username: rails
- encoding: utf8
-
mysql2:
arunit:
username: rails
- encoding: utf8
- collation: utf8_unicode_ci
+ encoding: utf8mb4
+ collation: utf8mb4_unicode_ci
arunit2:
username: rails
- encoding: utf8
+ encoding: utf8mb4
+ collation: utf8mb4_general_ci
oracle:
arunit:
@@ -86,6 +78,9 @@ connections:
postgresql:
arunit:
min_messages: warning
+ arunit_without_prepared_statements:
+ min_messages: warning
+ prepared_statements: false
arunit2:
min_messages: warning
diff --git a/activerecord/test/config.rb b/activerecord/test/config.rb
index 6e2e8b2145..72cdfb16ef 100644
--- a/activerecord/test/config.rb
+++ b/activerecord/test/config.rb
@@ -1,4 +1,6 @@
-TEST_ROOT = File.expand_path(File.dirname(__FILE__))
+# frozen_string_literal: true
+
+TEST_ROOT = __dir__
ASSETS_ROOT = TEST_ROOT + "/assets"
FIXTURES_ROOT = TEST_ROOT + "/fixtures"
MIGRATIONS_ROOT = TEST_ROOT + "/migrations"
diff --git a/activerecord/test/fixtures/.gitignore b/activerecord/test/fixtures/.gitignore
deleted file mode 100644
index 885029a512..0000000000
--- a/activerecord/test/fixtures/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-*.sqlite* \ No newline at end of file
diff --git a/activerecord/test/fixtures/all/namespaced/accounts.yml b/activerecord/test/fixtures/all/namespaced/accounts.yml
new file mode 100644
index 0000000000..9e341a15af
--- /dev/null
+++ b/activerecord/test/fixtures/all/namespaced/accounts.yml
@@ -0,0 +1,2 @@
+signals37:
+ name: 37signals
diff --git a/activerecord/test/fixtures/author_addresses.yml b/activerecord/test/fixtures/author_addresses.yml
index 7b90572187..cf75e5998d 100644
--- a/activerecord/test/fixtures/author_addresses.yml
+++ b/activerecord/test/fixtures/author_addresses.yml
@@ -3,3 +3,9 @@ david_address:
david_address_extra:
id: 2
+
+mary_address:
+ id: 3
+
+bob_address:
+ id: 4
diff --git a/activerecord/test/fixtures/authors.yml b/activerecord/test/fixtures/authors.yml
index 832236a486..41c124179e 100644
--- a/activerecord/test/fixtures/authors.yml
+++ b/activerecord/test/fixtures/authors.yml
@@ -9,7 +9,9 @@ david:
mary:
id: 2
name: Mary
+ author_address_id: 3
bob:
id: 3
name: Bob
+ author_address_id: 4
diff --git a/activerecord/test/fixtures/bad_posts.yml b/activerecord/test/fixtures/bad_posts.yml
new file mode 100644
index 0000000000..addee8e3bf
--- /dev/null
+++ b/activerecord/test/fixtures/bad_posts.yml
@@ -0,0 +1,9 @@
+# Please do not use this fixture without `set_fixture_class` as Post
+
+_fixture:
+ model_class: BadPostModel
+
+bad_welcome:
+ author_id: 1
+ title: Welcome to the another weblog
+ body: It's really nice today
diff --git a/activerecord/test/fixtures/binaries.yml b/activerecord/test/fixtures/binaries.yml
index ec8f2facdc..53b7883369 100644
--- a/activerecord/test/fixtures/binaries.yml
+++ b/activerecord/test/fixtures/binaries.yml
@@ -131,3 +131,7 @@ flowers:
SgCUASgCUASgCUASgAC74PbXOTvE5/En7jpSoLE8/wBn7uPJjKyj46T9D/NT
pKsXyQzxNpdNP0/akB5484WkMKh4RfXG4UafNmH7b0UxWMrb7Nxg6rl9Z/Im
w+vWq0iscQwxQroiUIvkKsRZQBKAJQBKAJQB/9k=
+
+binary_helper:
+ id: 2
+ data: <%= binary(ASSETS_ROOT + "/flowers.jpg") %>
diff --git a/activerecord/test/fixtures/books.yml b/activerecord/test/fixtures/books.yml
index a304fba399..699623a6f9 100644
--- a/activerecord/test/fixtures/books.yml
+++ b/activerecord/test/fixtures/books.yml
@@ -9,6 +9,7 @@ awdr:
author_visibility: :visible
illustrator_visibility: :visible
font_size: :medium
+ difficulty: :medium
rfr:
author_id: 1
@@ -24,6 +25,7 @@ ddd:
name: "Domain-Driven Design"
format: "hardcover"
status: 2
+ read_status: "forgotten"
tlg:
author_id: 1
diff --git a/activerecord/test/fixtures/citations.yml b/activerecord/test/fixtures/citations.yml
new file mode 100644
index 0000000000..396099621c
--- /dev/null
+++ b/activerecord/test/fixtures/citations.yml
@@ -0,0 +1,5 @@
+<% 65536.times do |i| %>
+fixture_no_<%= i %>:
+ id: <%= i %>
+ book2_id: <%= i*i %>
+<% end %>
diff --git a/activerecord/test/fixtures/content.yml b/activerecord/test/fixtures/content.yml
new file mode 100644
index 0000000000..0d12ee03dc
--- /dev/null
+++ b/activerecord/test/fixtures/content.yml
@@ -0,0 +1,3 @@
+content:
+ id: 1
+ title: How to use Rails
diff --git a/activerecord/test/fixtures/content_positions.yml b/activerecord/test/fixtures/content_positions.yml
new file mode 100644
index 0000000000..9e85773f8e
--- /dev/null
+++ b/activerecord/test/fixtures/content_positions.yml
@@ -0,0 +1,3 @@
+content_positions:
+ id: 1
+ content_id: 1
diff --git a/activerecord/test/fixtures/customers.yml b/activerecord/test/fixtures/customers.yml
index 0399ff83b9..7d6c1366d0 100644
--- a/activerecord/test/fixtures/customers.yml
+++ b/activerecord/test/fixtures/customers.yml
@@ -23,4 +23,13 @@ barney:
address_street: Quiet Road
address_city: Peaceful Town
address_country: Tranquil Land
- gps_location: NULL \ No newline at end of file
+ gps_location: NULL
+
+mary:
+ id: 4
+ name: Mary
+ balance: 1
+ address_street: Funny Street
+ address_city: Peaceful Town
+ address_country: Nation Land
+ gps_location: NULL
diff --git a/activerecord/test/fixtures/memberships.yml b/activerecord/test/fixtures/memberships.yml
index a5d52bd438..f7ca227533 100644
--- a/activerecord/test/fixtures/memberships.yml
+++ b/activerecord/test/fixtures/memberships.yml
@@ -26,6 +26,13 @@ blarpy_winkup_crazy_club:
favourite: false
type: CurrentMembership
+super_membership_of_boring_club:
+ joined_on: <%= 3.weeks.ago.to_s(:db) %>
+ club: boring_club
+ member_id: 1
+ favourite: false
+ type: SuperMembership
+
selected_membership_of_boring_club:
joined_on: <%= 3.weeks.ago.to_s(:db) %>
club: boring_club
diff --git a/activerecord/test/fixtures/minimalistics.yml b/activerecord/test/fixtures/minimalistics.yml
index c3ec546209..83df0551bc 100644
--- a/activerecord/test/fixtures/minimalistics.yml
+++ b/activerecord/test/fixtures/minimalistics.yml
@@ -1,2 +1,5 @@
+zero:
+ id: 0
+
first:
id: 1
diff --git a/activerecord/test/fixtures/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/naked/yml/parrots.yml b/activerecord/test/fixtures/naked/yml/parrots.yml
new file mode 100644
index 0000000000..76f66e01ae
--- /dev/null
+++ b/activerecord/test/fixtures/naked/yml/parrots.yml
@@ -0,0 +1,3 @@
+george:
+ arrr: "Curious George"
+ foobar: Foobar
diff --git a/activerecord/test/fixtures/naked/yml/trees.yml b/activerecord/test/fixtures/naked/yml/trees.yml
new file mode 100644
index 0000000000..d163b98f21
--- /dev/null
+++ b/activerecord/test/fixtures/naked/yml/trees.yml
@@ -0,0 +1,3 @@
+root:
+ :id: 1
+ :name: The Root
diff --git a/activerecord/test/fixtures/other_comments.yml b/activerecord/test/fixtures/other_comments.yml
new file mode 100644
index 0000000000..55e8216ec7
--- /dev/null
+++ b/activerecord/test/fixtures/other_comments.yml
@@ -0,0 +1,6 @@
+_fixture:
+ model_class: Comment
+
+second_greetings:
+ post: second_welcome
+ body: Thank you for the second welcome
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/other_posts.yml b/activerecord/test/fixtures/other_posts.yml
new file mode 100644
index 0000000000..3e11a33802
--- /dev/null
+++ b/activerecord/test/fixtures/other_posts.yml
@@ -0,0 +1,8 @@
+_fixture:
+ model_class: Post
+
+second_welcome:
+ author_id: 1
+ title: Welcome to the another weblog
+ body: It's really nice today
+ comments_count: 1
diff --git a/activerecord/test/fixtures/posts.yml b/activerecord/test/fixtures/posts.yml
index 86d46f753a..8d7e1e0ae7 100644
--- a/activerecord/test/fixtures/posts.yml
+++ b/activerecord/test/fixtures/posts.yml
@@ -28,6 +28,7 @@ sti_comments:
author_id: 1
title: sti comments
body: hello
+ comments_count: 5
type: Post
sti_post_and_comments:
@@ -35,6 +36,7 @@ sti_post_and_comments:
author_id: 1
title: sti me
body: hello
+ comments_count: 2
type: StiPost
sti_habtm:
@@ -50,6 +52,8 @@ eager_other:
title: eager loading with OR'd conditions
body: hello
type: Post
+ comments_count: 1
+ tags_count: 3
misc_by_bob:
id: 8
@@ -57,6 +61,7 @@ misc_by_bob:
title: misc post by bob
body: hello
type: Post
+ tags_count: 1
misc_by_mary:
id: 9
@@ -64,6 +69,7 @@ misc_by_mary:
title: misc post by mary
body: hello
type: Post
+ tags_count: 1
other_by_bob:
id: 10
@@ -71,6 +77,7 @@ other_by_bob:
title: other post by bob
body: hello
type: Post
+ tags_count: 1
other_by_mary:
id: 11
@@ -78,3 +85,4 @@ other_by_mary:
title: other post by mary
body: hello
type: Post
+ tags_count: 1
diff --git a/activerecord/test/fixtures/price_estimates.yml b/activerecord/test/fixtures/price_estimates.yml
index 1149ab17a2..406d65a142 100644
--- a/activerecord/test/fixtures/price_estimates.yml
+++ b/activerecord/test/fixtures/price_estimates.yml
@@ -1,7 +1,16 @@
-saphire_1:
+sapphire_1:
price: 10
estimate_of: sapphire (Treasure)
sapphire_2:
price: 20
estimate_of: sapphire (Treasure)
+
+diamond:
+ price: 30
+ estimate_of: diamond (Treasure)
+
+honda:
+ price: 40
+ estimate_of_type: Car
+ estimate_of_id: 1
diff --git a/activerecord/test/fixtures/reserved_words/values.yml b/activerecord/test/fixtures/reserved_words/values.yml
index 7d109609ab..9ed9e5edc5 100644
--- a/activerecord/test/fixtures/reserved_words/values.yml
+++ b/activerecord/test/fixtures/reserved_words/values.yml
@@ -1,7 +1,7 @@
values1:
- id: 1
+ as: 1
group_id: 2
values2:
- id: 2
+ as: 2
group_id: 1
diff --git a/activerecord/test/fixtures/sponsors.yml b/activerecord/test/fixtures/sponsors.yml
index 2da541c539..02ddb8dd38 100644
--- a/activerecord/test/fixtures/sponsors.yml
+++ b/activerecord/test/fixtures/sponsors.yml
@@ -10,3 +10,6 @@ crazy_club_sponsor_for_groucho:
sponsor_club: crazy_club
sponsorable_id: 3
sponsorable_type: Member
+sponsor_for_author_david:
+ sponsorable_id: 1
+ sponsorable_type: Author
diff --git a/activerecord/test/fixtures/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/fixtures/teapots.yml b/activerecord/test/fixtures/teapots.yml
deleted file mode 100644
index ff515beb45..0000000000
--- a/activerecord/test/fixtures/teapots.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-bob:
- id: 1
- name: Bob
diff --git a/activerecord/test/migrations/10_urban/9_add_expressions.rb b/activerecord/test/migrations/10_urban/9_add_expressions.rb
index 79a342e574..4b0d5fb6fa 100644
--- a/activerecord/test/migrations/10_urban/9_add_expressions.rb
+++ b/activerecord/test/migrations/10_urban/9_add_expressions.rb
@@ -1,4 +1,6 @@
-class AddExpressions < ActiveRecord::Migration
+# frozen_string_literal: true
+
+class AddExpressions < ActiveRecord::Migration::Current
def self.up
create_table("expressions") do |t|
t.column :expression, :string
diff --git a/activerecord/test/migrations/decimal/1_give_me_big_numbers.rb b/activerecord/test/migrations/decimal/1_give_me_big_numbers.rb
index 0aed7cbd84..7d4233fe31 100644
--- a/activerecord/test/migrations/decimal/1_give_me_big_numbers.rb
+++ b/activerecord/test/migrations/decimal/1_give_me_big_numbers.rb
@@ -1,10 +1,12 @@
-class GiveMeBigNumbers < ActiveRecord::Migration
+# frozen_string_literal: true
+
+class GiveMeBigNumbers < ActiveRecord::Migration::Current
def self.up
create_table :big_numbers do |table|
- table.column :bank_balance, :decimal, :precision => 10, :scale => 2
- table.column :big_bank_balance, :decimal, :precision => 15, :scale => 2
- table.column :world_population, :decimal, :precision => 10
- table.column :my_house_population, :decimal, :precision => 2
+ table.column :bank_balance, :decimal, precision: 10, scale: 2
+ table.column :big_bank_balance, :decimal, precision: 15, scale: 2
+ table.column :world_population, :decimal, precision: 20
+ table.column :my_house_population, :decimal, precision: 2
table.column :value_of_e, :decimal
end
end
diff --git a/actionview/test/tmp/.gitkeep b/activerecord/test/migrations/empty/.keep
index e69de29bb2..e69de29bb2 100644
--- a/actionview/test/tmp/.gitkeep
+++ b/activerecord/test/migrations/empty/.keep
diff --git a/activerecord/test/migrations/magic/1_currencies_have_symbols.rb b/activerecord/test/migrations/magic/1_currencies_have_symbols.rb
index c066c068c2..2ba2875751 100644
--- a/activerecord/test/migrations/magic/1_currencies_have_symbols.rb
+++ b/activerecord/test/migrations/magic/1_currencies_have_symbols.rb
@@ -1,9 +1,10 @@
+# frozen_string_literal: true
# coding: ISO-8859-15
-class CurrenciesHaveSymbols < ActiveRecord::Migration
+class CurrenciesHaveSymbols < ActiveRecord::Migration::Current
def self.up
- # We use for default currency symbol
- add_column "currencies", "symbol", :string, :default => ""
+ # We use € for default currency symbol
+ add_column "currencies", "symbol", :string, default: "€"
end
def self.down
diff --git a/activerecord/test/migrations/missing/1000_people_have_middle_names.rb b/activerecord/test/migrations/missing/1000_people_have_middle_names.rb
index 4b83d61beb..d3c9b127fb 100644
--- a/activerecord/test/migrations/missing/1000_people_have_middle_names.rb
+++ b/activerecord/test/migrations/missing/1000_people_have_middle_names.rb
@@ -1,4 +1,6 @@
-class PeopleHaveMiddleNames < ActiveRecord::Migration
+# frozen_string_literal: true
+
+class PeopleHaveMiddleNames < ActiveRecord::Migration::Current
def self.up
add_column "people", "middle_name", :string
end
diff --git a/activerecord/test/migrations/missing/1_people_have_last_names.rb b/activerecord/test/migrations/missing/1_people_have_last_names.rb
index 68209f3ce9..bd5f5ea11e 100644
--- a/activerecord/test/migrations/missing/1_people_have_last_names.rb
+++ b/activerecord/test/migrations/missing/1_people_have_last_names.rb
@@ -1,4 +1,6 @@
-class PeopleHaveLastNames < ActiveRecord::Migration
+# frozen_string_literal: true
+
+class PeopleHaveLastNames < ActiveRecord::Migration::Current
def self.up
add_column "people", "last_name", :string
end
diff --git a/activerecord/test/migrations/missing/3_we_need_reminders.rb b/activerecord/test/migrations/missing/3_we_need_reminders.rb
index 25bb49cb32..4647268c6e 100644
--- a/activerecord/test/migrations/missing/3_we_need_reminders.rb
+++ b/activerecord/test/migrations/missing/3_we_need_reminders.rb
@@ -1,4 +1,6 @@
-class WeNeedReminders < ActiveRecord::Migration
+# frozen_string_literal: true
+
+class WeNeedReminders < ActiveRecord::Migration::Current
def self.up
create_table("reminders") do |t|
t.column :content, :text
diff --git a/activerecord/test/migrations/missing/4_innocent_jointable.rb b/activerecord/test/migrations/missing/4_innocent_jointable.rb
index 002a1bf2a6..8063bc0558 100644
--- a/activerecord/test/migrations/missing/4_innocent_jointable.rb
+++ b/activerecord/test/migrations/missing/4_innocent_jointable.rb
@@ -1,6 +1,8 @@
-class InnocentJointable < ActiveRecord::Migration
+# frozen_string_literal: true
+
+class InnocentJointable < ActiveRecord::Migration::Current
def self.up
- create_table("people_reminders", :id => false) do |t|
+ create_table("people_reminders", id: false) do |t|
t.column :reminder_id, :integer
t.column :person_id, :integer
end
diff --git a/activerecord/test/migrations/rename/1_we_need_things.rb b/activerecord/test/migrations/rename/1_we_need_things.rb
index f5484ac54f..8e71a1d996 100644
--- a/activerecord/test/migrations/rename/1_we_need_things.rb
+++ b/activerecord/test/migrations/rename/1_we_need_things.rb
@@ -1,4 +1,6 @@
-class WeNeedThings < ActiveRecord::Migration
+# frozen_string_literal: true
+
+class WeNeedThings < ActiveRecord::Migration::Current
def self.up
create_table("things") do |t|
t.column :content, :text
diff --git a/activerecord/test/migrations/rename/2_rename_things.rb b/activerecord/test/migrations/rename/2_rename_things.rb
index 533a113ea8..110fe3f0fa 100644
--- a/activerecord/test/migrations/rename/2_rename_things.rb
+++ b/activerecord/test/migrations/rename/2_rename_things.rb
@@ -1,4 +1,6 @@
-class RenameThings < ActiveRecord::Migration
+# frozen_string_literal: true
+
+class RenameThings < ActiveRecord::Migration::Current
def self.up
rename_table "things", "awesome_things"
end
diff --git a/activerecord/test/migrations/to_copy/1_people_have_hobbies.rb b/activerecord/test/migrations/to_copy/1_people_have_hobbies.rb
index 639841f663..badccf65cc 100644
--- a/activerecord/test/migrations/to_copy/1_people_have_hobbies.rb
+++ b/activerecord/test/migrations/to_copy/1_people_have_hobbies.rb
@@ -1,4 +1,6 @@
-class PeopleHaveLastNames < ActiveRecord::Migration
+# frozen_string_literal: true
+
+class PeopleHaveHobbies < ActiveRecord::Migration::Current
def self.up
add_column "people", "hobbies", :text
end
diff --git a/activerecord/test/migrations/to_copy/2_people_have_descriptions.rb b/activerecord/test/migrations/to_copy/2_people_have_descriptions.rb
index b3d0b30640..1d19d5d6f4 100644
--- a/activerecord/test/migrations/to_copy/2_people_have_descriptions.rb
+++ b/activerecord/test/migrations/to_copy/2_people_have_descriptions.rb
@@ -1,4 +1,6 @@
-class PeopleHaveLastNames < ActiveRecord::Migration
+# frozen_string_literal: true
+
+class PeopleHaveDescriptions < ActiveRecord::Migration::Current
def self.up
add_column "people", "description", :text
end
diff --git a/activerecord/test/migrations/to_copy2/1_create_articles.rb b/activerecord/test/migrations/to_copy2/1_create_articles.rb
index 0f048d90f7..85c166b319 100644
--- a/activerecord/test/migrations/to_copy2/1_create_articles.rb
+++ b/activerecord/test/migrations/to_copy2/1_create_articles.rb
@@ -1,4 +1,6 @@
-class CreateArticles < ActiveRecord::Migration
+# frozen_string_literal: true
+
+class CreateArticles < ActiveRecord::Migration::Current
def self.up
end
diff --git a/activerecord/test/migrations/to_copy2/2_create_comments.rb b/activerecord/test/migrations/to_copy2/2_create_comments.rb
index 0f048d90f7..1d213a1705 100644
--- a/activerecord/test/migrations/to_copy2/2_create_comments.rb
+++ b/activerecord/test/migrations/to_copy2/2_create_comments.rb
@@ -1,4 +1,6 @@
-class CreateArticles < ActiveRecord::Migration
+# frozen_string_literal: true
+
+class CreateComments < ActiveRecord::Migration::Current
def self.up
end
diff --git a/activerecord/test/migrations/to_copy_with_name_collision/1_people_have_hobbies.rb b/activerecord/test/migrations/to_copy_with_name_collision/1_people_have_hobbies.rb
index e438cf5999..d9fef596f5 100644
--- a/activerecord/test/migrations/to_copy_with_name_collision/1_people_have_hobbies.rb
+++ b/activerecord/test/migrations/to_copy_with_name_collision/1_people_have_hobbies.rb
@@ -1,4 +1,6 @@
-class PeopleHaveLastNames < ActiveRecord::Migration
+# frozen_string_literal: true
+
+class PeopleHaveHobbies < ActiveRecord::Migration::Current
def self.up
add_column "people", "hobbies", :string
end
diff --git a/activerecord/test/migrations/to_copy_with_timestamps/20090101010101_people_have_hobbies.rb b/activerecord/test/migrations/to_copy_with_timestamps/20090101010101_people_have_hobbies.rb
index 639841f663..badccf65cc 100644
--- a/activerecord/test/migrations/to_copy_with_timestamps/20090101010101_people_have_hobbies.rb
+++ b/activerecord/test/migrations/to_copy_with_timestamps/20090101010101_people_have_hobbies.rb
@@ -1,4 +1,6 @@
-class PeopleHaveLastNames < ActiveRecord::Migration
+# frozen_string_literal: true
+
+class PeopleHaveHobbies < ActiveRecord::Migration::Current
def self.up
add_column "people", "hobbies", :text
end
diff --git a/activerecord/test/migrations/to_copy_with_timestamps/20090101010202_people_have_descriptions.rb b/activerecord/test/migrations/to_copy_with_timestamps/20090101010202_people_have_descriptions.rb
index b3d0b30640..1d19d5d6f4 100644
--- a/activerecord/test/migrations/to_copy_with_timestamps/20090101010202_people_have_descriptions.rb
+++ b/activerecord/test/migrations/to_copy_with_timestamps/20090101010202_people_have_descriptions.rb
@@ -1,4 +1,6 @@
-class PeopleHaveLastNames < ActiveRecord::Migration
+# frozen_string_literal: true
+
+class PeopleHaveDescriptions < ActiveRecord::Migration::Current
def self.up
add_column "people", "description", :text
end
diff --git a/activerecord/test/migrations/to_copy_with_timestamps2/20090101010101_create_articles.rb b/activerecord/test/migrations/to_copy_with_timestamps2/20090101010101_create_articles.rb
index 0f048d90f7..85c166b319 100644
--- a/activerecord/test/migrations/to_copy_with_timestamps2/20090101010101_create_articles.rb
+++ b/activerecord/test/migrations/to_copy_with_timestamps2/20090101010101_create_articles.rb
@@ -1,4 +1,6 @@
-class CreateArticles < ActiveRecord::Migration
+# frozen_string_literal: true
+
+class CreateArticles < ActiveRecord::Migration::Current
def self.up
end
diff --git a/activerecord/test/migrations/to_copy_with_timestamps2/20090101010202_create_comments.rb b/activerecord/test/migrations/to_copy_with_timestamps2/20090101010202_create_comments.rb
index 2b048edbb5..1d213a1705 100644
--- a/activerecord/test/migrations/to_copy_with_timestamps2/20090101010202_create_comments.rb
+++ b/activerecord/test/migrations/to_copy_with_timestamps2/20090101010202_create_comments.rb
@@ -1,4 +1,6 @@
-class CreateComments < ActiveRecord::Migration
+# frozen_string_literal: true
+
+class CreateComments < ActiveRecord::Migration::Current
def self.up
end
diff --git a/activerecord/test/migrations/valid/1_valid_people_have_last_names.rb b/activerecord/test/migrations/valid/1_valid_people_have_last_names.rb
index 06cb911117..3bedcdcdf0 100644
--- a/activerecord/test/migrations/valid/1_valid_people_have_last_names.rb
+++ b/activerecord/test/migrations/valid/1_valid_people_have_last_names.rb
@@ -1,4 +1,6 @@
-class ValidPeopleHaveLastNames < ActiveRecord::Migration
+# frozen_string_literal: true
+
+class ValidPeopleHaveLastNames < ActiveRecord::Migration::Current
def self.up
add_column "people", "last_name", :string
end
diff --git a/activerecord/test/migrations/valid/2_we_need_reminders.rb b/activerecord/test/migrations/valid/2_we_need_reminders.rb
index 25bb49cb32..4647268c6e 100644
--- a/activerecord/test/migrations/valid/2_we_need_reminders.rb
+++ b/activerecord/test/migrations/valid/2_we_need_reminders.rb
@@ -1,4 +1,6 @@
-class WeNeedReminders < ActiveRecord::Migration
+# frozen_string_literal: true
+
+class WeNeedReminders < ActiveRecord::Migration::Current
def self.up
create_table("reminders") do |t|
t.column :content, :text
diff --git a/activerecord/test/migrations/valid/3_innocent_jointable.rb b/activerecord/test/migrations/valid/3_innocent_jointable.rb
index 002a1bf2a6..8063bc0558 100644
--- a/activerecord/test/migrations/valid/3_innocent_jointable.rb
+++ b/activerecord/test/migrations/valid/3_innocent_jointable.rb
@@ -1,6 +1,8 @@
-class InnocentJointable < ActiveRecord::Migration
+# frozen_string_literal: true
+
+class InnocentJointable < ActiveRecord::Migration::Current
def self.up
- create_table("people_reminders", :id => false) do |t|
+ create_table("people_reminders", id: false) do |t|
t.column :reminder_id, :integer
t.column :person_id, :integer
end
diff --git a/activerecord/test/migrations/valid_with_subdirectories/1_valid_people_have_last_names.rb b/activerecord/test/migrations/valid_with_subdirectories/1_valid_people_have_last_names.rb
index 06cb911117..3bedcdcdf0 100644
--- a/activerecord/test/migrations/valid_with_subdirectories/1_valid_people_have_last_names.rb
+++ b/activerecord/test/migrations/valid_with_subdirectories/1_valid_people_have_last_names.rb
@@ -1,4 +1,6 @@
-class ValidPeopleHaveLastNames < ActiveRecord::Migration
+# frozen_string_literal: true
+
+class ValidPeopleHaveLastNames < ActiveRecord::Migration::Current
def self.up
add_column "people", "last_name", :string
end
diff --git a/activerecord/test/migrations/valid_with_subdirectories/sub/2_we_need_reminders.rb b/activerecord/test/migrations/valid_with_subdirectories/sub/2_we_need_reminders.rb
index 25bb49cb32..4647268c6e 100644
--- a/activerecord/test/migrations/valid_with_subdirectories/sub/2_we_need_reminders.rb
+++ b/activerecord/test/migrations/valid_with_subdirectories/sub/2_we_need_reminders.rb
@@ -1,4 +1,6 @@
-class WeNeedReminders < ActiveRecord::Migration
+# frozen_string_literal: true
+
+class WeNeedReminders < ActiveRecord::Migration::Current
def self.up
create_table("reminders") do |t|
t.column :content, :text
diff --git a/activerecord/test/migrations/valid_with_subdirectories/sub1/3_innocent_jointable.rb b/activerecord/test/migrations/valid_with_subdirectories/sub1/3_innocent_jointable.rb
index 002a1bf2a6..8063bc0558 100644
--- a/activerecord/test/migrations/valid_with_subdirectories/sub1/3_innocent_jointable.rb
+++ b/activerecord/test/migrations/valid_with_subdirectories/sub1/3_innocent_jointable.rb
@@ -1,6 +1,8 @@
-class InnocentJointable < ActiveRecord::Migration
+# frozen_string_literal: true
+
+class InnocentJointable < ActiveRecord::Migration::Current
def self.up
- create_table("people_reminders", :id => false) do |t|
+ create_table("people_reminders", id: false) do |t|
t.column :reminder_id, :integer
t.column :person_id, :integer
end
diff --git a/activerecord/test/migrations/valid_with_timestamps/20100101010101_valid_with_timestamps_people_have_last_names.rb b/activerecord/test/migrations/valid_with_timestamps/20100101010101_valid_with_timestamps_people_have_last_names.rb
index 1da99ceaba..b938847170 100644
--- a/activerecord/test/migrations/valid_with_timestamps/20100101010101_valid_with_timestamps_people_have_last_names.rb
+++ b/activerecord/test/migrations/valid_with_timestamps/20100101010101_valid_with_timestamps_people_have_last_names.rb
@@ -1,4 +1,6 @@
-class ValidWithTimestampsPeopleHaveLastNames < ActiveRecord::Migration
+# frozen_string_literal: true
+
+class ValidWithTimestampsPeopleHaveLastNames < ActiveRecord::Migration::Current
def self.up
add_column "people", "last_name", :string
end
diff --git a/activerecord/test/migrations/valid_with_timestamps/20100201010101_valid_with_timestamps_we_need_reminders.rb b/activerecord/test/migrations/valid_with_timestamps/20100201010101_valid_with_timestamps_we_need_reminders.rb
index cb6d735c8b..94551e8208 100644
--- a/activerecord/test/migrations/valid_with_timestamps/20100201010101_valid_with_timestamps_we_need_reminders.rb
+++ b/activerecord/test/migrations/valid_with_timestamps/20100201010101_valid_with_timestamps_we_need_reminders.rb
@@ -1,4 +1,6 @@
-class ValidWithTimestampsWeNeedReminders < ActiveRecord::Migration
+# frozen_string_literal: true
+
+class ValidWithTimestampsWeNeedReminders < ActiveRecord::Migration::Current
def self.up
create_table("reminders") do |t|
t.column :content, :text
diff --git a/activerecord/test/migrations/valid_with_timestamps/20100301010101_valid_with_timestamps_innocent_jointable.rb b/activerecord/test/migrations/valid_with_timestamps/20100301010101_valid_with_timestamps_innocent_jointable.rb
index 4bd4b4714d..672edc5253 100644
--- a/activerecord/test/migrations/valid_with_timestamps/20100301010101_valid_with_timestamps_innocent_jointable.rb
+++ b/activerecord/test/migrations/valid_with_timestamps/20100301010101_valid_with_timestamps_innocent_jointable.rb
@@ -1,6 +1,8 @@
-class ValidWithTimestampsInnocentJointable < ActiveRecord::Migration
+# frozen_string_literal: true
+
+class ValidWithTimestampsInnocentJointable < ActiveRecord::Migration::Current
def self.up
- create_table("people_reminders", :id => false) do |t|
+ create_table("people_reminders", id: false) do |t|
t.column :reminder_id, :integer
t.column :person_id, :integer
end
diff --git a/activerecord/test/migrations/version_check/20131219224947_migration_version_check.rb b/activerecord/test/migrations/version_check/20131219224947_migration_version_check.rb
index 9d46485a31..91bfbbdfd1 100644
--- a/activerecord/test/migrations/version_check/20131219224947_migration_version_check.rb
+++ b/activerecord/test/migrations/version_check/20131219224947_migration_version_check.rb
@@ -1,4 +1,6 @@
-class MigrationVersionCheck < ActiveRecord::Migration
+# frozen_string_literal: true
+
+class MigrationVersionCheck < ActiveRecord::Migration::Current
def self.up
raise "incorrect migration version" unless version == 20131219224947
end
diff --git a/activerecord/test/models/account.rb b/activerecord/test/models/account.rb
new file mode 100644
index 0000000000..639e395743
--- /dev/null
+++ b/activerecord/test/models/account.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+class Account < ActiveRecord::Base
+ belongs_to :firm, class_name: "Company"
+ belongs_to :unautosaved_firm, foreign_key: "firm_id", class_name: "Firm", autosave: false
+
+ alias_attribute :available_credit, :credit_limit
+
+ def self.destroyed_account_ids
+ @destroyed_account_ids ||= Hash.new { |h, k| h[k] = [] }
+ end
+
+ # Test private kernel method through collection proxy using has_many.
+ scope :open, -> { where("firm_name = ?", "37signals") }
+ scope :available, -> { open }
+
+ before_destroy do |account|
+ if account.firm
+ Account.destroyed_account_ids[account.firm.id] << account.id
+ end
+ end
+
+ validate :check_empty_credit_limit
+
+ private
+ def check_empty_credit_limit
+ errors.add("credit_limit", :blank) if credit_limit.blank?
+ end
+
+ def private_method
+ "Sir, yes sir!"
+ end
+end
+
+class SubAccount < Account
+ def self.instantiate_instance_of(klass, attributes, column_types = {}, &block)
+ klass = superclass
+ super
+ end
+ private_class_method :instantiate_instance_of
+end
diff --git a/activerecord/test/models/admin.rb b/activerecord/test/models/admin.rb
index a38e3f4846..a40b5a33b2 100644
--- a/activerecord/test/models/admin.rb
+++ b/activerecord/test/models/admin.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
module Admin
def self.table_name_prefix
- 'admin_'
+ "admin_"
end
end
diff --git a/activerecord/test/models/admin/account.rb b/activerecord/test/models/admin/account.rb
index bd23192d20..41fe2d782b 100644
--- a/activerecord/test/models/admin/account.rb
+++ b/activerecord/test/models/admin/account.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Admin::Account < ActiveRecord::Base
has_many :users
end
diff --git a/activerecord/test/models/admin/randomly_named_c1.rb b/activerecord/test/models/admin/randomly_named_c1.rb
index b64ae7fc41..d89b8dd293 100644
--- a/activerecord/test/models/admin/randomly_named_c1.rb
+++ b/activerecord/test/models/admin/randomly_named_c1.rb
@@ -1,7 +1,9 @@
-class Admin::ClassNameThatDoesNotFollowCONVENTIONS1 < ActiveRecord::Base
- self.table_name = :randomly_named_table2
-end
-
-class Admin::ClassNameThatDoesNotFollowCONVENTIONS2 < ActiveRecord::Base
- self.table_name = :randomly_named_table3
-end
+# frozen_string_literal: true
+
+class Admin::ClassNameThatDoesNotFollowCONVENTIONS1 < ActiveRecord::Base
+ self.table_name = :randomly_named_table2
+end
+
+class Admin::ClassNameThatDoesNotFollowCONVENTIONS2 < ActiveRecord::Base
+ self.table_name = :randomly_named_table3
+end
diff --git a/activerecord/test/models/admin/user.rb b/activerecord/test/models/admin/user.rb
index 48a110bd23..691f9f11be 100644
--- a/activerecord/test/models/admin/user.rb
+++ b/activerecord/test/models/admin/user.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Admin::User < ActiveRecord::Base
class Coder
def initialize(default = {})
@@ -15,26 +17,32 @@ class Admin::User < ActiveRecord::Base
belongs_to :account
store :params, accessors: [ :token ], coder: YAML
- store :settings, :accessors => [ :color, :homepage ]
+ store :settings, accessors: [ :color, :homepage ]
store_accessor :settings, :favorite_food
- store :preferences, :accessors => [ :remember_login ]
- store :json_data, :accessors => [ :height, :weight ], :coder => Coder.new
- store :json_data_empty, :accessors => [ :is_a_good_guy ], :coder => Coder.new
+ store :parent, accessors: [:birthday, :name], prefix: true
+ store :spouse, accessors: [:birthday], prefix: :partner
+ store_accessor :spouse, :name, prefix: :partner
+ store :configs, accessors: [ :secret_question ]
+ store :configs, accessors: [ :two_factor_auth ], suffix: true
+ store_accessor :configs, :login_retry, suffix: :config
+ store :preferences, accessors: [ :remember_login ]
+ store :json_data, accessors: [ :height, :weight ], coder: Coder.new
+ store :json_data_empty, accessors: [ :is_a_good_guy ], coder: Coder.new
def phone_number
- read_store_attribute(:settings, :phone_number).gsub(/(\d{3})(\d{3})(\d{4})/,'(\1) \2-\3')
+ read_store_attribute(:settings, :phone_number).gsub(/(\d{3})(\d{3})(\d{4})/, '(\1) \2-\3')
end
def phone_number=(value)
- write_store_attribute(:settings, :phone_number, value && value.gsub(/[^\d]/,''))
+ write_store_attribute(:settings, :phone_number, value && value.gsub(/[^\d]/, ""))
end
def color
- super || 'red'
+ super || "red"
end
def color=(value)
- value = 'blue' unless %w(black red green blue).include?(value)
+ value = "blue" unless %w(black red green blue).include?(value)
super
end
end
diff --git a/activerecord/test/models/aircraft.rb b/activerecord/test/models/aircraft.rb
index 1f35ef45da..4fdea46cf7 100644
--- a/activerecord/test/models/aircraft.rb
+++ b/activerecord/test/models/aircraft.rb
@@ -1,4 +1,7 @@
+# frozen_string_literal: true
+
class Aircraft < ActiveRecord::Base
self.pluralize_table_names = false
- has_many :engines, :foreign_key => "car_id"
+ has_many :engines, foreign_key: "car_id"
+ has_many :wheels, as: :wheelable
end
diff --git a/activerecord/test/models/arunit2_model.rb b/activerecord/test/models/arunit2_model.rb
index 04b8b15d3d..5b0da8a249 100644
--- a/activerecord/test/models/arunit2_model.rb
+++ b/activerecord/test/models/arunit2_model.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ARUnit2Model < ActiveRecord::Base
self.abstract_class = true
end
diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb
index 8c1f14bd36..8b5a2fa0c8 100644
--- a/activerecord/test/models/author.rb
+++ b/activerecord/test/models/author.rb
@@ -1,151 +1,169 @@
+# frozen_string_literal: true
+
class Author < ActiveRecord::Base
has_many :posts
has_many :serialized_posts
has_one :post
- has_many :very_special_comments, :through => :posts
- has_many :posts_with_comments, -> { includes(:comments) }, :class_name => "Post"
- has_many :popular_grouped_posts, -> { includes(:comments).group("type").having("SUM(comments_count) > 1").select("type") }, :class_name => "Post"
- has_many :posts_with_comments_sorted_by_comment_id, -> { includes(:comments).order('comments.id') }, :class_name => "Post"
- has_many :posts_sorted_by_id_limited, -> { order('posts.id').limit(1) }, :class_name => "Post"
- has_many :posts_with_categories, -> { includes(:categories) }, :class_name => "Post"
- has_many :posts_with_comments_and_categories, -> { includes(:comments, :categories).order("posts.id") }, :class_name => "Post"
- has_many :posts_with_special_categorizations, :class_name => 'PostWithSpecialCategorization'
- has_one :post_about_thinking, -> { where("posts.title like '%thinking%'") }, :class_name => 'Post'
- has_one :post_about_thinking_with_last_comment, -> { where("posts.title like '%thinking%'").includes(:last_comment) }, :class_name => 'Post'
+ has_many :very_special_comments, through: :posts
+ has_many :posts_with_comments, -> { includes(:comments) }, class_name: "Post"
+ has_many :popular_grouped_posts, -> { includes(:comments).group("type").having("SUM(comments_count) > 1").select("type") }, class_name: "Post"
+ has_many :posts_with_comments_sorted_by_comment_id, -> { includes(:comments).order("comments.id") }, class_name: "Post"
+ has_many :posts_sorted_by_id, -> { order(:id) }, class_name: "Post"
+ has_many :posts_sorted_by_id_limited, -> { order("posts.id").limit(1) }, class_name: "Post"
+ has_many :posts_with_categories, -> { includes(:categories) }, class_name: "Post"
+ has_many :posts_with_comments_and_categories, -> { includes(:comments, :categories).order("posts.id") }, class_name: "Post"
+ has_many :posts_with_special_categorizations, class_name: "PostWithSpecialCategorization"
+ has_one :post_about_thinking, -> { where("posts.title like '%thinking%'") }, class_name: "Post"
+ has_one :post_about_thinking_with_last_comment, -> { where("posts.title like '%thinking%'").includes(:last_comment) }, class_name: "Post"
has_many :comments, through: :posts do
def ratings
Rating.joins(:comment).merge(self)
end
end
- has_many :comments_containing_the_letter_e, :through => :posts, :source => :comments
- has_many :comments_with_order_and_conditions, -> { order('comments.body').where("comments.body like 'Thank%'") }, :through => :posts, :source => :comments
- has_many :comments_with_include, -> { includes(:post) }, :through => :posts, :source => :comments
+ has_many :comments_containing_the_letter_e, through: :posts, source: :comments
+ has_many :comments_with_order_and_conditions, -> { order("comments.body").where("comments.body like 'Thank%'") }, through: :posts, source: :comments
+ has_many :comments_with_include, -> { includes(:post).where(posts: { type: "Post" }) }, through: :posts, source: :comments
+ has_many :comments_for_first_author, -> { for_first_author }, through: :posts, source: :comments
has_many :first_posts
- has_many :comments_on_first_posts, -> { order('posts.id desc, comments.id asc') }, :through => :first_posts, :source => :comments
+ has_many :comments_on_first_posts, -> { order("posts.id desc, comments.id asc") }, through: :first_posts, source: :comments
has_one :first_post
- has_one :comment_on_first_post, -> { order('posts.id desc, comments.id asc') }, :through => :first_post, :source => :comments
+ has_one :comment_on_first_post, -> { order("posts.id desc, comments.id asc") }, through: :first_post, source: :comments
- has_many :thinking_posts, -> { where(:title => 'So I was thinking') }, :dependent => :delete_all, :class_name => 'Post'
- has_many :welcome_posts, -> { where(:title => 'Welcome to the weblog') }, :class_name => 'Post'
+ has_many :thinking_posts, -> { where(title: "So I was thinking") }, dependent: :delete_all, class_name: "Post"
+ has_many :welcome_posts, -> { where(title: "Welcome to the weblog") }, class_name: "Post"
has_many :welcome_posts_with_one_comment,
- -> { where(title: 'Welcome to the weblog').where('comments_count = ?', 1) },
- class_name: 'Post'
+ -> { where(title: "Welcome to the weblog").where("comments_count = ?", 1) },
+ class_name: "Post"
has_many :welcome_posts_with_comments,
- -> { where(title: 'Welcome to the weblog').where(Post.arel_table[:comments_count].gt(0)) },
- class_name: 'Post'
+ -> { where(title: "Welcome to the weblog").where(Post.arel_table[:comments_count].gt(0)) },
+ class_name: "Post"
- has_many :comments_desc, -> { order('comments.id DESC') }, :through => :posts, :source => :comments
- has_many :funky_comments, :through => :posts, :source => :comments
- has_many :ordered_uniq_comments, -> { distinct.order('comments.id') }, :through => :posts, :source => :comments
- has_many :ordered_uniq_comments_desc, -> { distinct.order('comments.id DESC') }, :through => :posts, :source => :comments
- has_many :readonly_comments, -> { readonly }, :through => :posts, :source => :comments
+ has_many :comments_desc, -> { order("comments.id DESC") }, through: :posts_sorted_by_id, source: :comments
+ has_many :unordered_comments, -> { unscope(:order).distinct }, through: :posts_sorted_by_id_limited, source: :comments
+ has_many :funky_comments, through: :posts, source: :comments
+ has_many :ordered_uniq_comments, -> { distinct.order("comments.id") }, through: :posts, source: :comments
+ has_many :ordered_uniq_comments_desc, -> { distinct.order("comments.id DESC") }, through: :posts, source: :comments
+ has_many :readonly_comments, -> { readonly }, through: :posts, source: :comments
has_many :special_posts
- has_many :special_post_comments, :through => :special_posts, :source => :comments
- has_many :special_posts_with_default_scope, :class_name => 'SpecialPostWithDefaultScope'
-
- has_many :sti_posts, :class_name => 'StiPost'
- has_many :sti_post_comments, :through => :sti_posts, :source => :comments
-
- has_many :special_nonexistent_posts, -> { where("posts.body = 'nonexistent'") }, :class_name => "SpecialPost"
- has_many :special_nonexistent_post_comments, -> { where('comments.post_id' => 0) }, :through => :special_nonexistent_posts, :source => :comments
- has_many :nonexistent_comments, :through => :posts
-
- has_many :hello_posts, -> { where "posts.body = 'hello'" }, :class_name => "Post"
- has_many :hello_post_comments, :through => :hello_posts, :source => :comments
- has_many :posts_with_no_comments, -> { where('comments.id' => nil).includes(:comments) }, :class_name => 'Post'
-
- has_many :hello_posts_with_hash_conditions, -> { where(:body => 'hello') }, :class_name => "Post"
- has_many :hello_post_comments_with_hash_conditions, :through =>
-:hello_posts_with_hash_conditions, :source => :comments
-
- has_many :other_posts, :class_name => "Post"
- has_many :posts_with_callbacks, :class_name => "Post", :before_add => :log_before_adding,
- :after_add => :log_after_adding,
- :before_remove => :log_before_removing,
- :after_remove => :log_after_removing
- has_many :posts_with_proc_callbacks, :class_name => "Post",
- :before_add => Proc.new {|o, r| o.post_log << "before_adding#{r.id || '<new>'}"},
- :after_add => Proc.new {|o, r| o.post_log << "after_adding#{r.id || '<new>'}"},
- :before_remove => Proc.new {|o, r| o.post_log << "before_removing#{r.id}"},
- :after_remove => Proc.new {|o, r| o.post_log << "after_removing#{r.id}"}
- has_many :posts_with_multiple_callbacks, :class_name => "Post",
- :before_add => [:log_before_adding, Proc.new {|o, r| o.post_log << "before_adding_proc#{r.id || '<new>'}"}],
- :after_add => [:log_after_adding, Proc.new {|o, r| o.post_log << "after_adding_proc#{r.id || '<new>'}"}]
- has_many :unchangable_posts, :class_name => "Post", :before_add => :raise_exception, :after_add => :log_after_adding
-
- has_many :categorizations
- has_many :categories, :through => :categorizations
- has_many :named_categories, :through => :categorizations
+ has_many :special_post_comments, through: :special_posts, source: :comments
+ has_many :special_posts_with_default_scope, class_name: "SpecialPostWithDefaultScope"
+
+ has_many :sti_posts, class_name: "StiPost"
+ has_many :sti_post_comments, through: :sti_posts, source: :comments
+
+ has_many :special_nonexistent_posts, -> { where("posts.body = 'nonexistent'") }, class_name: "SpecialPost"
+ has_many :special_nonexistent_post_comments, -> { where("comments.post_id" => 0) }, through: :special_nonexistent_posts, source: :comments
+ has_many :nonexistent_comments, through: :posts
+
+ has_many :hello_posts, -> { where "posts.body = 'hello'" }, class_name: "Post"
+ has_many :hello_post_comments, through: :hello_posts, source: :comments
+ has_many :posts_with_no_comments, -> { where("comments.id" => nil).includes(:comments) }, class_name: "Post"
+
+ has_many :hello_posts_with_hash_conditions, -> { where(body: "hello") }, class_name: "Post"
+ has_many :hello_post_comments_with_hash_conditions, through: :hello_posts_with_hash_conditions, source: :comments
+
+ has_many :other_posts, class_name: "Post"
+ has_many :posts_with_callbacks, class_name: "Post", before_add: :log_before_adding,
+ after_add: :log_after_adding,
+ before_remove: :log_before_removing,
+ after_remove: :log_after_removing
+ has_many :posts_with_proc_callbacks, class_name: "Post",
+ before_add: Proc.new { |o, r| o.post_log << "before_adding#{r.id || '<new>'}" },
+ after_add: Proc.new { |o, r| o.post_log << "after_adding#{r.id || '<new>'}" },
+ before_remove: Proc.new { |o, r| o.post_log << "before_removing#{r.id}" },
+ after_remove: Proc.new { |o, r| o.post_log << "after_removing#{r.id}" }
+ has_many :posts_with_multiple_callbacks, class_name: "Post",
+ before_add: [:log_before_adding, Proc.new { |o, r| o.post_log << "before_adding_proc#{r.id || '<new>'}" }],
+ after_add: [:log_after_adding, Proc.new { |o, r| o.post_log << "after_adding_proc#{r.id || '<new>'}" }]
+ has_many :unchangeable_posts, class_name: "Post", before_add: :raise_exception, after_add: :log_after_adding
+
+ has_many :categorizations, -> { }
+ has_many :categories, through: :categorizations
+ has_many :named_categories, through: :categorizations
has_many :special_categorizations
- has_many :special_categories, :through => :special_categorizations, :source => :category
- has_one :special_category, :through => :special_categorizations, :source => :category
+ has_many :special_categories, through: :special_categorizations, source: :category
+ has_one :special_category, through: :special_categorizations, source: :category
+
+ has_many :special_categories_with_conditions, -> { where(categorizations: { special: true }) }, through: :categorizations, source: :category
+ has_many :nonspecial_categories_with_conditions, -> { where(categorizations: { special: false }) }, through: :categorizations, source: :category
- has_many :categories_like_general, -> { where(:name => 'General') }, :through => :categorizations, :source => :category, :class_name => 'Category'
+ has_many :categories_like_general, -> { where(name: "General") }, through: :categorizations, source: :category, class_name: "Category"
- has_many :categorized_posts, :through => :categorizations, :source => :post
- has_many :unique_categorized_posts, -> { distinct }, :through => :categorizations, :source => :post
+ has_many :categorized_posts, through: :categorizations, source: :post
+ has_many :unique_categorized_posts, -> { distinct }, through: :categorizations, source: :post
- has_many :nothings, :through => :kateggorisatons, :class_name => 'Category'
+ has_many :nothings, through: :kateggorisatons, class_name: "Category"
has_many :author_favorites
- has_many :favorite_authors, -> { order('name') }, :through => :author_favorites
+ has_many :favorite_authors, -> { order("name") }, through: :author_favorites
- has_many :taggings, :through => :posts, :source => :taggings
- has_many :taggings_2, :through => :posts, :source => :tagging
- has_many :tags, :through => :posts
- has_many :post_categories, :through => :posts, :source => :categories
- has_many :tagging_tags, :through => :taggings, :source => :tag
+ has_many :taggings, through: :posts, source: :taggings
+ has_many :taggings_2, through: :posts, source: :tagging
+ has_many :tags, through: :posts
+ has_many :ordered_tags, through: :posts
+ has_many :post_categories, through: :posts, source: :categories
+ has_many :tagging_tags, through: :taggings, source: :tag
- has_many :similar_posts, -> { distinct }, :through => :tags, :source => :tagged_posts
- has_many :distinct_tags, -> { select("DISTINCT tags.*").order("tags.name") }, :through => :posts, :source => :tags
+ has_many :similar_posts, -> { distinct }, through: :tags, source: :tagged_posts
+ has_many :ordered_posts, -> { distinct }, through: :ordered_tags, source: :tagged_posts
+ has_many :distinct_tags, -> { select("DISTINCT tags.*").order("tags.name") }, through: :posts, source: :tags
- has_many :tags_with_primary_key, :through => :posts
+ has_many :tags_with_primary_key, through: :posts
has_many :books
- has_many :subscriptions, :through => :books
- has_many :subscribers, -> { order("subscribers.nick") }, :through => :subscriptions
- has_many :distinct_subscribers, -> { select("DISTINCT subscribers.*").order("subscribers.nick") }, :through => :subscriptions, :source => :subscriber
+ has_many :unpublished_books, -> { where(status: [:proposed, :written]) }, class_name: "Book"
+ has_many :subscriptions, through: :books
+ has_many :subscribers, -> { order("subscribers.nick") }, through: :subscriptions
+ has_many :distinct_subscribers, -> { select("DISTINCT subscribers.*").order("subscribers.nick") }, through: :subscriptions, source: :subscriber
- has_one :essay, :primary_key => :name, :as => :writer
- has_one :essay_category, :through => :essay, :source => :category
- has_one :essay_owner, :through => :essay, :source => :owner
+ has_one :essay, primary_key: :name, as: :writer
+ has_one :essay_category, through: :essay, source: :category
+ has_one :essay_owner, through: :essay, source: :owner
- has_one :essay_2, :primary_key => :name, :class_name => 'Essay', :foreign_key => :author_id
- has_one :essay_category_2, :through => :essay_2, :source => :category
+ has_one :essay_2, primary_key: :name, class_name: "Essay", foreign_key: :author_id
+ has_one :essay_category_2, through: :essay_2, source: :category
- has_many :essays, :primary_key => :name, :as => :writer
- has_many :essay_categories, :through => :essays, :source => :category
- has_many :essay_owners, :through => :essays, :source => :owner
+ has_many :essays, primary_key: :name, as: :writer
+ has_many :essay_categories, through: :essays, source: :category
+ has_many :essay_owners, through: :essays, source: :owner
- has_many :essays_2, :primary_key => :name, :class_name => 'Essay', :foreign_key => :author_id
- has_many :essay_categories_2, :through => :essays_2, :source => :category
+ has_many :essays_2, primary_key: :name, class_name: "Essay", foreign_key: :author_id
+ has_many :essay_categories_2, through: :essays_2, source: :category
- belongs_to :owned_essay, :primary_key => :name, :class_name => 'Essay'
- has_one :owned_essay_category, :through => :owned_essay, :source => :category
+ belongs_to :owned_essay, primary_key: :name, class_name: "Essay"
+ has_one :owned_essay_category, through: :owned_essay, source: :category
- belongs_to :author_address, :dependent => :destroy
- belongs_to :author_address_extra, :dependent => :delete, :class_name => "AuthorAddress"
+ belongs_to :author_address, dependent: :destroy
+ belongs_to :author_address_extra, dependent: :delete, class_name: "AuthorAddress"
- has_many :category_post_comments, :through => :categories, :source => :post_comments
+ has_many :category_post_comments, through: :categories, source: :post_comments
- has_many :misc_posts, -> { where(:posts => { :title => ['misc post by bob', 'misc post by mary'] }) }, :class_name => 'Post'
- has_many :misc_post_first_blue_tags, :through => :misc_posts, :source => :first_blue_tags
+ has_many :misc_posts, -> { where(posts: { title: ["misc post by bob", "misc post by mary"] }) }, class_name: "Post"
+ has_many :misc_post_first_blue_tags, through: :misc_posts, source: :first_blue_tags
- has_many :misc_post_first_blue_tags_2, -> { where(:posts => { :title => ['misc post by bob', 'misc post by mary'] }) },
- :through => :posts, :source => :first_blue_tags_2
+ has_many :misc_post_first_blue_tags_2, -> { where(posts: { title: ["misc post by bob", "misc post by mary"] }) },
+ through: :posts, source: :first_blue_tags_2
- has_many :posts_with_default_include, :class_name => 'PostWithDefaultInclude'
- has_many :comments_on_posts_with_default_include, :through => :posts_with_default_include, :source => :comments
+ has_many :posts_with_default_include, class_name: "PostWithDefaultInclude"
+ has_many :comments_on_posts_with_default_include, through: :posts_with_default_include, source: :comments
has_many :posts_with_signature, ->(record) { where("posts.title LIKE ?", "%by #{record.name.downcase}%") }, class_name: "Post"
- scope :relation_include_posts, -> { includes(:posts) }
- scope :relation_include_tags, -> { includes(:tags) }
+ has_many :posts_with_extension, -> { order(:title) }, class_name: "Post" do
+ def extension_method; end
+ end
+
+ has_many :posts_with_extension_and_instance, ->(record) { order(:title) }, class_name: "Post" do
+ def extension_method; end
+ end
+
+ has_many :top_posts, -> { order(id: :asc) }, class_name: "Post"
+ has_many :other_top_posts, -> { order(id: :asc) }, class_name: "Post"
attr_accessor :post_log
after_initialize :set_post_log
@@ -200,5 +218,5 @@ end
class AuthorFavorite < ActiveRecord::Base
belongs_to :author
- belongs_to :favorite_author, :class_name => "Author"
+ belongs_to :favorite_author, class_name: "Author"
end
diff --git a/activerecord/test/models/auto_id.rb b/activerecord/test/models/auto_id.rb
index 82c6544bd5..fd672603bb 100644
--- a/activerecord/test/models/auto_id.rb
+++ b/activerecord/test/models/auto_id.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AutoId < ActiveRecord::Base
self.table_name = "auto_id_tests"
self.primary_key = "auto_id"
diff --git a/activerecord/test/models/autoloadable/extra_firm.rb b/activerecord/test/models/autoloadable/extra_firm.rb
index 5578ba0d9b..c46e34c101 100644
--- a/activerecord/test/models/autoloadable/extra_firm.rb
+++ b/activerecord/test/models/autoloadable/extra_firm.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
class ExtraFirm < Company
end
diff --git a/activerecord/test/models/binary.rb b/activerecord/test/models/binary.rb
index 39b2f5090a..b93f87519f 100644
--- a/activerecord/test/models/binary.rb
+++ b/activerecord/test/models/binary.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
class Binary < ActiveRecord::Base
end
diff --git a/activerecord/test/models/bird.rb b/activerecord/test/models/bird.rb
index 2a51d903b8..be08636ac6 100644
--- a/activerecord/test/models/bird.rb
+++ b/activerecord/test/models/bird.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Bird < ActiveRecord::Base
belongs_to :pirate
validates_presence_of :name
@@ -5,7 +7,7 @@ class Bird < ActiveRecord::Base
accepts_nested_attributes_for :pirate
attr_accessor :cancel_save_from_callback
- before_save :cancel_save_callback_method, :if => :cancel_save_from_callback
+ before_save :cancel_save_callback_method, if: :cancel_save_from_callback
def cancel_save_callback_method
throw(:abort)
end
diff --git a/activerecord/test/models/book.rb b/activerecord/test/models/book.rb
index 24bfe47bbf..afdda1a81e 100644
--- a/activerecord/test/models/book.rb
+++ b/activerecord/test/models/book.rb
@@ -1,19 +1,23 @@
+# frozen_string_literal: true
+
class Book < ActiveRecord::Base
- has_many :authors
+ belongs_to :author
- has_many :citations, :foreign_key => 'book1_id'
+ has_many :citations, foreign_key: "book1_id"
has_many :references, -> { distinct }, through: :citations, source: :reference_of
has_many :subscriptions
has_many :subscribers, through: :subscriptions
enum status: [:proposed, :written, :published]
- enum read_status: {unread: 0, reading: 2, read: 3}
+ enum read_status: { unread: 0, reading: 2, read: 3, forgotten: nil }
enum nullable_status: [:single, :married]
- enum language: [:english, :spanish, :french], enum_prefix: :in
- enum author_visibility: [:visible, :invisible], enum_prefix: true
- enum illustrator_visibility: [:visible, :invisible], enum_prefix: true
- enum font_size: [:small, :medium, :large], enum_prefix: :with, enum_suffix: true
+ enum language: [:english, :spanish, :french], _prefix: :in
+ enum author_visibility: [:visible, :invisible], _prefix: true
+ enum illustrator_visibility: [:visible, :invisible], _prefix: true
+ enum font_size: [:small, :medium, :large], _prefix: :with, _suffix: true
+ enum difficulty: [:easy, :medium, :hard], _suffix: :to_read
+ enum cover: { hard: "hard", soft: "soft" }
def published!
super
diff --git a/activerecord/test/models/boolean.rb b/activerecord/test/models/boolean.rb
index 7bae22e5f9..bee757fb9c 100644
--- a/activerecord/test/models/boolean.rb
+++ b/activerecord/test/models/boolean.rb
@@ -1,2 +1,7 @@
+# frozen_string_literal: true
+
class Boolean < ActiveRecord::Base
+ def has_fun
+ super
+ end
end
diff --git a/activerecord/test/models/bulb.rb b/activerecord/test/models/bulb.rb
index a6e83fe353..ab92f7025d 100644
--- a/activerecord/test/models/bulb.rb
+++ b/activerecord/test/models/bulb.rb
@@ -1,6 +1,9 @@
+# frozen_string_literal: true
+
class Bulb < ActiveRecord::Base
- default_scope { where(:name => 'defaulty') }
- belongs_to :car, :touch => true
+ default_scope { where(name: "defaulty") }
+ belongs_to :car, touch: true
+ scope :awesome, -> { where(frickinawesome: true) }
attr_reader :scope_after_initialize, :attributes_after_initialize
@@ -34,7 +37,7 @@ class CustomBulb < Bulb
after_initialize :set_awesomeness
def set_awesomeness
- self.frickinawesome = true if name == 'Dude'
+ self.frickinawesome = true if name == "Dude"
end
end
diff --git a/activerecord/test/models/cake_designer.rb b/activerecord/test/models/cake_designer.rb
index 9c57ef573a..0b2a00edfd 100644
--- a/activerecord/test/models/cake_designer.rb
+++ b/activerecord/test/models/cake_designer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CakeDesigner < ActiveRecord::Base
has_one :chef, as: :employable
end
diff --git a/activerecord/test/models/car.rb b/activerecord/test/models/car.rb
index 81263b79d1..8614926626 100644
--- a/activerecord/test/models/car.rb
+++ b/activerecord/test/models/car.rb
@@ -1,26 +1,33 @@
+# frozen_string_literal: true
+
class Car < ActiveRecord::Base
has_many :bulbs
has_many :all_bulbs, -> { unscope where: :name }, class_name: "Bulb"
- has_many :funky_bulbs, class_name: 'FunkyBulb', dependent: :destroy
- has_many :failed_bulbs, class_name: 'FailedBulb', dependent: :destroy
- has_many :foo_bulbs, -> { where(:name => 'foo') }, :class_name => "Bulb"
+ has_many :funky_bulbs, class_name: "FunkyBulb", dependent: :destroy
+ has_many :failed_bulbs, class_name: "FailedBulb", dependent: :destroy
+ has_many :foo_bulbs, -> { where(name: "foo") }, class_name: "Bulb"
+ has_many :awesome_bulbs, -> { awesome }, class_name: "Bulb"
has_one :bulb
has_many :tyres
- has_many :engines, :dependent => :destroy, inverse_of: :my_car
- has_many :wheels, :as => :wheelable, :dependent => :destroy
+ has_many :engines, dependent: :destroy, inverse_of: :my_car
+ has_many :wheels, as: :wheelable, dependent: :destroy
+
+ has_many :price_estimates, as: :estimate_of
scope :incl_tyres, -> { includes(:tyres) }
scope :incl_engines, -> { includes(:engines) }
- scope :order_using_new_style, -> { order('name asc') }
+ scope :order_using_new_style, -> { order("name asc") }
+
+ attribute :wheels_owned_at, :datetime, default: -> { Time.now }
end
class CoolCar < Car
- default_scope { order('name desc') }
+ default_scope { order("name desc") }
end
class FastCar < Car
- default_scope { order('name desc') }
+ default_scope { order("name desc") }
end
diff --git a/activerecord/test/models/carrier.rb b/activerecord/test/models/carrier.rb
new file mode 100644
index 0000000000..995a9d3bef
--- /dev/null
+++ b/activerecord/test/models/carrier.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+class Carrier < ActiveRecord::Base
+end
diff --git a/activerecord/test/models/cat.rb b/activerecord/test/models/cat.rb
new file mode 100644
index 0000000000..43013964b6
--- /dev/null
+++ b/activerecord/test/models/cat.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class Cat < ActiveRecord::Base
+ self.abstract_class = true
+
+ enum gender: [:female, :male]
+
+ default_scope -> { where(is_vegetarian: false) }
+end
+
+class Lion < Cat
+end
diff --git a/activerecord/test/models/categorization.rb b/activerecord/test/models/categorization.rb
index 6588531de6..68b0ea90d3 100644
--- a/activerecord/test/models/categorization.rb
+++ b/activerecord/test/models/categorization.rb
@@ -1,18 +1,20 @@
+# frozen_string_literal: true
+
class Categorization < ActiveRecord::Base
belongs_to :post
- belongs_to :category
- belongs_to :named_category, :class_name => 'Category', :foreign_key => :named_category_name, :primary_key => :name
+ belongs_to :category, counter_cache: true
+ belongs_to :named_category, class_name: "Category", foreign_key: :named_category_name, primary_key: :name
belongs_to :author
- has_many :post_taggings, :through => :author, :source => :taggings
+ has_many :post_taggings, through: :author, source: :taggings
- belongs_to :author_using_custom_pk, :class_name => 'Author', :foreign_key => :author_id, :primary_key => :author_address_extra_id
- has_many :authors_using_custom_pk, :class_name => 'Author', :foreign_key => :id, :primary_key => :category_id
+ belongs_to :author_using_custom_pk, class_name: "Author", foreign_key: :author_id, primary_key: :author_address_extra_id
+ has_many :authors_using_custom_pk, class_name: "Author", foreign_key: :id, primary_key: :category_id
end
class SpecialCategorization < ActiveRecord::Base
- self.table_name = 'categorizations'
- default_scope { where(:special => true) }
+ self.table_name = "categorizations"
+ default_scope { where(special: true) }
belongs_to :author
belongs_to :category
diff --git a/activerecord/test/models/category.rb b/activerecord/test/models/category.rb
index 272223e1d8..2ccc00bed9 100644
--- a/activerecord/test/models/category.rb
+++ b/activerecord/test/models/category.rb
@@ -1,34 +1,45 @@
+# frozen_string_literal: true
+
class Category < ActiveRecord::Base
has_and_belongs_to_many :posts
- has_and_belongs_to_many :special_posts, :class_name => "Post"
- has_and_belongs_to_many :other_posts, :class_name => "Post"
- has_and_belongs_to_many :posts_with_authors_sorted_by_author_id, -> { includes(:authors).order("authors.id") }, :class_name => "Post"
+ has_and_belongs_to_many :special_posts, class_name: "Post"
+ has_and_belongs_to_many :other_posts, class_name: "Post"
+ has_and_belongs_to_many :posts_with_authors_sorted_by_author_id, -> { includes(:authors).order("authors.id") }, class_name: "Post"
has_and_belongs_to_many :select_testing_posts,
- -> { select 'posts.*, 1 as correctness_marker' },
- :class_name => 'Post',
- :foreign_key => 'category_id',
- :association_foreign_key => 'post_id'
+ -> { select "posts.*, 1 as correctness_marker" },
+ class_name: "Post",
+ foreign_key: "category_id",
+ association_foreign_key: "post_id"
has_and_belongs_to_many :post_with_conditions,
- -> { where :title => 'Yet Another Testing Title' },
- :class_name => 'Post'
+ -> { where title: "Yet Another Testing Title" },
+ class_name: "Post"
- has_and_belongs_to_many :popular_grouped_posts, -> { group("posts.type").having("sum(comments.post_id) > 2").includes(:comments) }, :class_name => "Post"
- has_and_belongs_to_many :posts_grouped_by_title, -> { group("title").select("title") }, :class_name => "Post"
+ has_and_belongs_to_many :popular_grouped_posts, -> { group("posts.type").having("sum(comments.post_id) > 2").includes(:comments) }, class_name: "Post"
+ has_and_belongs_to_many :posts_grouped_by_title, -> { group("title").select("title") }, class_name: "Post"
def self.what_are_you
- 'a category...'
+ "a category..."
end
has_many :categorizations
has_many :special_categorizations
- has_many :post_comments, :through => :posts, :source => :comments
+ has_many :post_comments, through: :posts, source: :comments
+
+ has_many :authors, through: :categorizations
+ has_many :authors_with_select, -> { select "authors.*, categorizations.post_id" }, through: :categorizations, source: :author
- has_many :authors, :through => :categorizations
- has_many :authors_with_select, -> { select 'authors.*, categorizations.post_id' }, :through => :categorizations, :source => :author
+ scope :general, -> { where(name: "General") }
- scope :general, -> { where(:name => 'General') }
+ # Should be delegated `ast` and `locked` to `arel`.
+ def self.ast
+ raise
+ end
+
+ def self.locked
+ raise
+ end
end
class SpecialCategory < Category
diff --git a/activerecord/test/models/chef.rb b/activerecord/test/models/chef.rb
index 698a52e045..ff528644bc 100644
--- a/activerecord/test/models/chef.rb
+++ b/activerecord/test/models/chef.rb
@@ -1,4 +1,10 @@
+# frozen_string_literal: true
+
class Chef < ActiveRecord::Base
belongs_to :employable, polymorphic: true
has_many :recipes
end
+
+class ChefList < Chef
+ belongs_to :employable_list, polymorphic: true
+end
diff --git a/activerecord/test/models/citation.rb b/activerecord/test/models/citation.rb
index 3d87eb795c..cee3d18173 100644
--- a/activerecord/test/models/citation.rb
+++ b/activerecord/test/models/citation.rb
@@ -1,3 +1,6 @@
+# frozen_string_literal: true
+
class Citation < ActiveRecord::Base
- belongs_to :reference_of, :class_name => "Book", :foreign_key => :book2_id
+ belongs_to :reference_of, class_name: "Book", foreign_key: :book2_id
+ has_many :citations
end
diff --git a/activerecord/test/models/club.rb b/activerecord/test/models/club.rb
index 6ceafe5858..2006e05fcf 100644
--- a/activerecord/test/models/club.rb
+++ b/activerecord/test/models/club.rb
@@ -1,23 +1,27 @@
+# frozen_string_literal: true
+
class Club < ActiveRecord::Base
has_one :membership
- has_many :memberships, :inverse_of => false
- has_many :members, :through => :memberships
+ has_many :memberships, inverse_of: false
+ has_many :members, through: :memberships
has_one :sponsor
- has_one :sponsored_member, :through => :sponsor, :source => :sponsorable, :source_type => "Member"
+ has_one :sponsored_member, through: :sponsor, source: :sponsorable, source_type: "Member"
belongs_to :category
has_many :favourites, -> { where(memberships: { favourite: true }) }, through: :memberships, source: :member
+ scope :general, -> { left_joins(:category).where(categories: { name: "General" }) }
+
private
- def private_method
- "I'm sorry sir, this is a *private* club, not a *pirate* club"
- end
+ def private_method
+ "I'm sorry sir, this is a *private* club, not a *pirate* club"
+ end
end
class SuperClub < ActiveRecord::Base
self.table_name = "clubs"
- has_many :memberships, class_name: 'SuperMembership', foreign_key: 'club_id'
+ has_many :memberships, class_name: "SuperMembership", foreign_key: "club_id"
has_many :members, through: :memberships
end
diff --git a/activerecord/test/models/college.rb b/activerecord/test/models/college.rb
index 501af4a8dd..52017dda42 100644
--- a/activerecord/test/models/college.rb
+++ b/activerecord/test/models/college.rb
@@ -1,5 +1,7 @@
-require_dependency 'models/arunit2_model'
-require 'active_support/core_ext/object/with_options'
+# frozen_string_literal: true
+
+require_dependency "models/arunit2_model"
+require "active_support/core_ext/object/with_options"
class College < ARUnit2Model
has_many :courses
diff --git a/activerecord/test/models/column.rb b/activerecord/test/models/column.rb
index 499358b4cf..d3cd419a00 100644
--- a/activerecord/test/models/column.rb
+++ b/activerecord/test/models/column.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Column < ActiveRecord::Base
belongs_to :record
end
diff --git a/activerecord/test/models/column_name.rb b/activerecord/test/models/column_name.rb
index 460eb4fe20..c6047c507b 100644
--- a/activerecord/test/models/column_name.rb
+++ b/activerecord/test/models/column_name.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ColumnName < ActiveRecord::Base
self.table_name = "colnametests"
end
diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb
index b38b17e90e..f0f0576709 100644
--- a/activerecord/test/models/comment.rb
+++ b/activerecord/test/models/comment.rb
@@ -1,25 +1,47 @@
+# frozen_string_literal: true
+
+# `counter_cache` requires association class before `attr_readonly`.
+class Post < ActiveRecord::Base; end
+
class Comment < ActiveRecord::Base
- scope :limit_by, lambda {|l| limit(l) }
+ scope :limit_by, lambda { |l| limit(l) }
scope :containing_the_letter_e, -> { where("comments.body LIKE '%e%'") }
scope :not_again, -> { where("comments.body NOT LIKE '%again%'") }
- scope :for_first_post, -> { where(:post_id => 1) }
+ scope :for_first_post, -> { where(post_id: 1) }
scope :for_first_author, -> { joins(:post).where("posts.author_id" => 1) }
scope :created, -> { all }
- belongs_to :post, :counter_cache => true
+ belongs_to :post, counter_cache: true
belongs_to :author, polymorphic: true
belongs_to :resource, polymorphic: true
- belongs_to :developer
has_many :ratings
- belongs_to :first_post, :foreign_key => :post_id
+ belongs_to :first_post, foreign_key: :post_id
+ belongs_to :special_post_with_default_scope, foreign_key: :post_id
+
+ has_many :children, class_name: "Comment", foreign_key: :parent_id
+ belongs_to :parent, class_name: "Comment", counter_cache: :children_count
+
+ class ::OopsError < RuntimeError; end
+
+ module OopsExtension
+ def destroy_all(*)
+ raise OopsError
+ end
+ end
+
+ default_scope { extending OopsExtension }
- has_many :children, :class_name => 'Comment', :foreign_key => :parent_id
- belongs_to :parent, :class_name => 'Comment', :counter_cache => :children_count
+ scope :oops_comments, -> { extending OopsExtension }
+
+ # Should not be called if extending modules that having the method exists on an association.
+ def self.greeting
+ raise
+ end
def self.what_are_you
- 'a comment...'
+ "a comment..."
end
def self.search_by_type(q)
@@ -37,6 +59,11 @@ class Comment < ActiveRecord::Base
end
class SpecialComment < Comment
+ default_scope { where(deleted_at: nil) }
+
+ def self.what_are_you
+ "a special comment..."
+ end
end
class SubSpecialComment < SpecialComment
@@ -49,11 +76,17 @@ class CommentThatAutomaticallyAltersPostBody < Comment
belongs_to :post, class_name: "PostThatLoadsCommentsInAnAfterSaveHook", foreign_key: :post_id
after_save do |comment|
- comment.post.update_attributes(body: "Automatically altered")
+ comment.post.update(body: "Automatically altered")
end
end
class CommentWithDefaultScopeReferencesAssociation < Comment
- default_scope ->{ includes(:developer).order('developers.name').references(:developer) }
+ default_scope -> { includes(:developer).order("developers.name").references(:developer) }
belongs_to :developer
end
+
+class CommentWithAfterCreateUpdate < Comment
+ after_create do
+ update(body: "bar")
+ end
+end
diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb
index 67936e8e5d..838f515aad 100644
--- a/activerecord/test/models/company.rb
+++ b/activerecord/test/models/company.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AbstractCompany < ActiveRecord::Base
self.abstract_class = true
end
@@ -7,14 +9,13 @@ class Company < AbstractCompany
validates_presence_of :name
- has_one :dummy_account, :foreign_key => "firm_id", :class_name => "Account"
+ has_one :dummy_account, foreign_key: "firm_id", class_name: "Account"
has_many :contracts
- has_many :developers, :through => :contracts
- has_many :accounts
+ has_many :developers, through: :contracts
scope :of_first_firm, lambda {
- joins(:account => :firm).
- where('firms.id' => 1)
+ joins(account: :firm).
+ where("firms.id" => 1)
}
def arbitrary_method
@@ -23,12 +24,12 @@ class Company < AbstractCompany
private
- def private_method
- "I am Jack's innermost fears and aspirations"
- end
+ def private_method
+ "I am Jack's innermost fears and aspirations"
+ end
- class SpecialCo < Company
- end
+ class SpecialCo < Company
+ end
end
module Namespaced
@@ -36,7 +37,7 @@ module Namespaced
end
class Firm < ::Company
- has_many :clients, :class_name => 'Namespaced::Client'
+ has_many :clients, class_name: "Namespaced::Client"
end
class Client < ::Company
@@ -46,45 +47,50 @@ end
class Firm < Company
to_param :name
- has_many :clients, -> { order "id" }, :dependent => :destroy, :before_remove => :log_before_remove, :after_remove => :log_after_remove
- has_many :unsorted_clients, :class_name => "Client"
- has_many :unsorted_clients_with_symbol, :class_name => :Client
- has_many :clients_sorted_desc, -> { order "id DESC" }, :class_name => "Client"
- has_many :clients_of_firm, -> { order "id" }, :foreign_key => "client_of", :class_name => "Client", :inverse_of => :firm
- has_many :clients_ordered_by_name, -> { order "name" }, :class_name => "Client"
- has_many :unvalidated_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :validate => false
- has_many :dependent_clients_of_firm, -> { order "id" }, :foreign_key => "client_of", :class_name => "Client", :dependent => :destroy
- has_many :exclusively_dependent_clients_of_firm, -> { order "id" }, :foreign_key => "client_of", :class_name => "Client", :dependent => :delete_all
- has_many :limited_clients, -> { limit 1 }, :class_name => "Client"
- has_many :clients_with_interpolated_conditions, ->(firm) { where "rating > #{firm.rating}" }, :class_name => "Client"
- has_many :clients_like_ms, -> { where("name = 'Microsoft'").order("id") }, :class_name => "Client"
- has_many :clients_like_ms_with_hash_conditions, -> { where(:name => 'Microsoft').order("id") }, :class_name => "Client"
- has_many :plain_clients, :class_name => 'Client'
- has_many :clients_using_primary_key, :class_name => 'Client',
- :primary_key => 'name', :foreign_key => 'firm_name'
- has_many :clients_using_primary_key_with_delete_all, :class_name => 'Client',
- :primary_key => 'name', :foreign_key => 'firm_name', :dependent => :delete_all
- has_many :clients_grouped_by_firm_id, -> { group("firm_id").select("firm_id") }, :class_name => "Client"
- has_many :clients_grouped_by_name, -> { group("name").select("name") }, :class_name => "Client"
-
- has_one :account, :foreign_key => "firm_id", :dependent => :destroy, :validate => true
- has_one :unvalidated_account, :foreign_key => "firm_id", :class_name => 'Account', :validate => false
- has_one :account_with_select, -> { select("id, firm_id") }, :foreign_key => "firm_id", :class_name=>'Account'
- has_one :readonly_account, -> { readonly }, :foreign_key => "firm_id", :class_name => "Account"
+ has_many :clients, -> { order "id" }, dependent: :destroy, before_remove: :log_before_remove, after_remove: :log_after_remove
+ has_many :unsorted_clients, class_name: "Client"
+ has_many :unsorted_clients_with_symbol, class_name: :Client
+ has_many :clients_sorted_desc, -> { order "id DESC" }, class_name: "Client"
+ has_many :clients_of_firm, -> { order "id" }, foreign_key: "client_of", class_name: "Client", inverse_of: :firm
+ has_many :clients_ordered_by_name, -> { order "name" }, class_name: "Client"
+ has_many :unvalidated_clients_of_firm, foreign_key: "client_of", class_name: "Client", validate: false
+ has_many :dependent_clients_of_firm, -> { order "id" }, foreign_key: "client_of", class_name: "Client", dependent: :destroy
+ has_many :exclusively_dependent_clients_of_firm, -> { order "id" }, foreign_key: "client_of", class_name: "Client", dependent: :delete_all
+ has_many :limited_clients, -> { limit 1 }, class_name: "Client"
+ has_many :clients_with_interpolated_conditions, ->(firm) { where "rating > #{firm.rating}" }, class_name: "Client"
+ has_many :clients_like_ms, -> { where("name = 'Microsoft'").order("id") }, class_name: "Client"
+ has_many :clients_like_ms_with_hash_conditions, -> { where(name: "Microsoft").order("id") }, class_name: "Client"
+ has_many :plain_clients, class_name: "Client"
+ has_many :clients_using_primary_key, class_name: "Client",
+ primary_key: "name", foreign_key: "firm_name"
+ has_many :clients_using_primary_key_with_delete_all, class_name: "Client",
+ primary_key: "name", foreign_key: "firm_name", dependent: :delete_all
+ has_many :clients_grouped_by_firm_id, -> { group("firm_id").select("firm_id") }, class_name: "Client"
+ has_many :clients_grouped_by_name, -> { group("name").select("name") }, class_name: "Client"
+
+ has_one :account, foreign_key: "firm_id", dependent: :destroy, validate: true
+ has_one :unvalidated_account, foreign_key: "firm_id", class_name: "Account", validate: false
+ has_one :account_with_select, -> { select("id, firm_id") }, foreign_key: "firm_id", class_name: "Account"
+ has_one :readonly_account, -> { readonly }, foreign_key: "firm_id", class_name: "Account"
# added order by id as in fixtures there are two accounts for Rails Core
# Oracle tests were failing because of that as the second fixture was selected
- has_one :account_using_primary_key, -> { order('id') }, :primary_key => "firm_id", :class_name => "Account"
- has_one :account_using_foreign_and_primary_keys, :foreign_key => "firm_name", :primary_key => "name", :class_name => "Account"
- has_one :account_with_inexistent_foreign_key, class_name: 'Account', foreign_key: "inexistent"
- has_one :deletable_account, :foreign_key => "firm_id", :class_name => "Account", :dependent => :delete
+ has_one :account_using_primary_key, -> { order("id") }, primary_key: "firm_id", class_name: "Account"
+ has_one :account_using_foreign_and_primary_keys, foreign_key: "firm_name", primary_key: "name", class_name: "Account"
+ has_one :account_with_inexistent_foreign_key, class_name: "Account", foreign_key: "inexistent"
+ has_one :deletable_account, foreign_key: "firm_id", class_name: "Account", dependent: :delete
- has_one :account_limit_500_with_hash_conditions, -> { where :credit_limit => 500 }, :foreign_key => "firm_id", :class_name => "Account"
+ has_one :account_limit_500_with_hash_conditions, -> { where credit_limit: 500 }, foreign_key: "firm_id", class_name: "Account"
- has_one :unautosaved_account, :foreign_key => "firm_id", :class_name => 'Account', :autosave => false
+ has_one :unautosaved_account, foreign_key: "firm_id", class_name: "Account", autosave: false
has_many :accounts
- has_many :unautosaved_accounts, :foreign_key => "firm_id", :class_name => 'Account', :autosave => false
+ has_many :unautosaved_accounts, foreign_key: "firm_id", class_name: "Account", autosave: false
+
+ has_many :association_with_references, -> { references(:foo) }, class_name: "Client"
+
+ has_many :developers_with_select, -> { select("id, name, first_name") }, class_name: "Developer"
- has_many :association_with_references, -> { references(:foo) }, :class_name => 'Client'
+ has_one :lead_developer, class_name: "Developer"
+ has_many :projects
def log
@log ||= []
@@ -101,32 +107,38 @@ class Firm < Company
end
class DependentFirm < Company
- has_one :account, :foreign_key => "firm_id", :dependent => :nullify
- has_many :companies, :foreign_key => 'client_of', :dependent => :nullify
- has_one :company, :foreign_key => 'client_of', :dependent => :nullify
+ has_one :account, foreign_key: "firm_id", dependent: :nullify
+ has_many :companies, foreign_key: "client_of", dependent: :nullify
+ has_one :company, foreign_key: "client_of", dependent: :nullify
end
class RestrictedWithExceptionFirm < Company
- has_one :account, -> { order("id") }, :foreign_key => "firm_id", :dependent => :restrict_with_exception
- has_many :companies, -> { order("id") }, :foreign_key => 'client_of', :dependent => :restrict_with_exception
+ has_one :account, -> { order("id") }, foreign_key: "firm_id", dependent: :restrict_with_exception
+ has_many :companies, -> { order("id") }, foreign_key: "client_of", dependent: :restrict_with_exception
end
class RestrictedWithErrorFirm < Company
- has_one :account, -> { order("id") }, :foreign_key => "firm_id", :dependent => :restrict_with_error
- has_many :companies, -> { order("id") }, :foreign_key => 'client_of', :dependent => :restrict_with_error
+ has_one :account, -> { order("id") }, foreign_key: "firm_id", dependent: :restrict_with_error
+ has_many :companies, -> { order("id") }, foreign_key: "client_of", dependent: :restrict_with_error
+end
+
+class Agency < Firm
+ has_many :projects, foreign_key: :firm_id
+
+ accepts_nested_attributes_for :projects
end
class Client < Company
- belongs_to :firm, :foreign_key => "client_of"
- belongs_to :firm_with_basic_id, :class_name => "Firm", :foreign_key => "firm_id"
- belongs_to :firm_with_select, -> { select("id") }, :class_name => "Firm", :foreign_key => "firm_id"
- belongs_to :firm_with_other_name, :class_name => "Firm", :foreign_key => "client_of"
- belongs_to :firm_with_condition, -> { where "1 = ?", 1 }, :class_name => "Firm", :foreign_key => "client_of"
- belongs_to :firm_with_primary_key, :class_name => "Firm", :primary_key => "name", :foreign_key => "firm_name"
- belongs_to :firm_with_primary_key_symbols, :class_name => "Firm", :primary_key => :name, :foreign_key => :firm_name
- belongs_to :readonly_firm, -> { readonly }, :class_name => "Firm", :foreign_key => "firm_id"
- belongs_to :bob_firm, -> { where :name => "Bob" }, :class_name => "Firm", :foreign_key => "client_of"
- has_many :accounts, :through => :firm, :source => :accounts
+ belongs_to :firm, foreign_key: "client_of"
+ belongs_to :firm_with_basic_id, class_name: "Firm", foreign_key: "firm_id"
+ belongs_to :firm_with_select, -> { select("id") }, class_name: "Firm", foreign_key: "firm_id"
+ belongs_to :firm_with_other_name, class_name: "Firm", foreign_key: "client_of"
+ belongs_to :firm_with_condition, -> { where "1 = ?", 1 }, class_name: "Firm", foreign_key: "client_of"
+ belongs_to :firm_with_primary_key, class_name: "Firm", primary_key: "name", foreign_key: "firm_name"
+ belongs_to :firm_with_primary_key_symbols, class_name: "Firm", primary_key: :name, foreign_key: :firm_name
+ belongs_to :readonly_firm, -> { readonly }, class_name: "Firm", foreign_key: "firm_id"
+ belongs_to :bob_firm, -> { where name: "Bob" }, class_name: "Firm", foreign_key: "client_of"
+ has_many :accounts, through: :firm, source: :accounts
belongs_to :account
validate do
@@ -139,6 +151,21 @@ class Client < Company
raise RaisedOnSave if raise_on_save
end
+ attr_accessor :throw_on_save
+ before_save do
+ throw :abort if throw_on_save
+ end
+
+ attr_accessor :rollback_on_save
+ after_save do
+ raise ActiveRecord::Rollback if rollback_on_save
+ end
+
+ attr_accessor :rollback_on_create_called
+ after_rollback(on: :create) do |client|
+ client.rollback_on_create_called = true
+ end
+
class RaisedOnDestroy < RuntimeError; end
attr_accessor :raise_on_destroy
before_destroy do
@@ -149,7 +176,7 @@ class Client < Company
# is calling client.destroy, deleting from the database, or setting
# foreign keys to NULL.
def self.destroyed_client_ids
- @destroyed_client_ids ||= Hash.new { |h,k| h[k] = [] }
+ @destroyed_client_ids ||= Hash.new { |h, k| h[k] = [] }
end
before_destroy do |client|
@@ -168,20 +195,13 @@ class Client < Company
def overwrite_to_raise
end
-
- class << self
- private
-
- def private_method
- "darkness"
- end
- end
end
class ExclusivelyDependentFirm < Company
- has_one :account, :foreign_key => "firm_id", :dependent => :delete
- has_many :dependent_sanitized_conditional_clients_of_firm, -> { order("id").where("name = 'BigShot Inc.'") }, :foreign_key => "client_of", :class_name => "Client", :dependent => :delete_all
- has_many :dependent_conditional_clients_of_firm, -> { order("id").where("name = ?", 'BigShot Inc.') }, :foreign_key => "client_of", :class_name => "Client", :dependent => :delete_all
+ has_one :account, foreign_key: "firm_id", dependent: :delete
+ has_many :dependent_sanitized_conditional_clients_of_firm, -> { order("id").where("name = 'BigShot Inc.'") }, foreign_key: "client_of", class_name: "Client", dependent: :delete_all
+ has_many :dependent_hash_conditional_clients_of_firm, -> { order("id").where(name: "BigShot Inc.") }, foreign_key: "client_of", class_name: "Client", dependent: :delete_all
+ has_many :dependent_conditional_clients_of_firm, -> { order("id").where("name = ?", "BigShot Inc.") }, foreign_key: "client_of", class_name: "Client", dependent: :delete_all
end
class SpecialClient < Client
@@ -190,39 +210,12 @@ end
class VerySpecialClient < SpecialClient
end
-class Account < ActiveRecord::Base
- belongs_to :firm, :class_name => 'Company'
- belongs_to :unautosaved_firm, :foreign_key => "firm_id", :class_name => "Firm", :autosave => false
-
- alias_attribute :available_credit, :credit_limit
-
- def self.destroyed_account_ids
- @destroyed_account_ids ||= Hash.new { |h,k| h[k] = [] }
- end
-
- # Test private kernel method through collection proxy using has_many.
- def self.open
- where('firm_name = ?', '37signals')
- end
-
- before_destroy do |account|
- if account.firm
- Account.destroyed_account_ids[account.firm.id] << account.id
- end
- true
- end
-
- validate :check_empty_credit_limit
-
- protected
-
- def check_empty_credit_limit
- errors.add("credit_limit", :blank) if credit_limit.blank?
- end
-
- private
+class NewlyContractedCompany < Company
+ has_many :new_contracts, foreign_key: "company_id"
- def private_method
- "Sir, yes sir!"
+ before_save do
+ self.new_contracts << NewContract.new
end
end
+
+require "models/account"
diff --git a/activerecord/test/models/company_in_module.rb b/activerecord/test/models/company_in_module.rb
index bf0a0d1c3e..52b7e06a63 100644
--- a/activerecord/test/models/company_in_module.rb
+++ b/activerecord/test/models/company_in_module.rb
@@ -1,4 +1,6 @@
-require 'active_support/core_ext/object/with_options'
+# frozen_string_literal: true
+
+require "active_support/core_ext/object/with_options"
module MyApplication
module Business
@@ -6,23 +8,23 @@ module MyApplication
end
class Firm < Company
- has_many :clients, -> { order("id") }, :dependent => :destroy
- has_many :clients_sorted_desc, -> { order("id DESC") }, :class_name => "Client"
- has_many :clients_of_firm, -> { order "id" }, :foreign_key => "client_of", :class_name => "Client"
- has_many :clients_like_ms, -> { where("name = 'Microsoft'").order("id") }, :class_name => "Client"
- has_one :account, :class_name => 'MyApplication::Billing::Account', :dependent => :destroy
+ has_many :clients, -> { order("id") }, dependent: :destroy
+ has_many :clients_sorted_desc, -> { order("id DESC") }, class_name: "Client"
+ has_many :clients_of_firm, -> { order "id" }, foreign_key: "client_of", class_name: "Client"
+ has_many :clients_like_ms, -> { where("name = 'Microsoft'").order("id") }, class_name: "Client"
+ has_one :account, class_name: "MyApplication::Billing::Account", dependent: :destroy
end
class Client < Company
- belongs_to :firm, :foreign_key => "client_of"
- belongs_to :firm_with_other_name, :class_name => "Firm", :foreign_key => "client_of"
+ belongs_to :firm, foreign_key: "client_of"
+ belongs_to :firm_with_other_name, class_name: "Firm", foreign_key: "client_of"
class Contact < ActiveRecord::Base; end
end
class Developer < ActiveRecord::Base
has_and_belongs_to_many :projects
- validates_length_of :name, :within => (3..20)
+ validates_length_of :name, within: (3..20)
end
class Project < ActiveRecord::Base
@@ -31,14 +33,14 @@ module MyApplication
module Prefixed
def self.table_name_prefix
- 'prefixed_'
+ "prefixed_"
end
class Company < ActiveRecord::Base
end
class Firm < Company
- self.table_name = 'companies'
+ self.table_name = "companies"
end
module Nested
@@ -49,14 +51,14 @@ module MyApplication
module Suffixed
def self.table_name_suffix
- '_suffixed'
+ "_suffixed"
end
class Company < ActiveRecord::Base
end
class Firm < Company
- self.table_name = 'companies'
+ self.table_name = "companies"
end
module Nested
@@ -68,31 +70,31 @@ module MyApplication
module Billing
class Firm < ActiveRecord::Base
- self.table_name = 'companies'
+ self.table_name = "companies"
end
module Nested
class Firm < ActiveRecord::Base
- self.table_name = 'companies'
+ self.table_name = "companies"
end
end
class Account < ActiveRecord::Base
- with_options(:foreign_key => :firm_id) do |i|
- i.belongs_to :firm, :class_name => 'MyApplication::Business::Firm'
- i.belongs_to :qualified_billing_firm, :class_name => 'MyApplication::Billing::Firm'
- i.belongs_to :unqualified_billing_firm, :class_name => 'Firm'
- i.belongs_to :nested_qualified_billing_firm, :class_name => 'MyApplication::Billing::Nested::Firm'
- i.belongs_to :nested_unqualified_billing_firm, :class_name => 'Nested::Firm'
+ with_options(foreign_key: :firm_id) do |i|
+ i.belongs_to :firm, class_name: "MyApplication::Business::Firm"
+ i.belongs_to :qualified_billing_firm, class_name: "MyApplication::Billing::Firm"
+ i.belongs_to :unqualified_billing_firm, class_name: "Firm"
+ i.belongs_to :nested_qualified_billing_firm, class_name: "MyApplication::Billing::Nested::Firm"
+ i.belongs_to :nested_unqualified_billing_firm, class_name: "Nested::Firm"
end
validate :check_empty_credit_limit
- protected
+ private
- def check_empty_credit_limit
- errors.add("credit_card", :blank) if credit_card.blank?
- end
+ def check_empty_credit_limit
+ errors.add("credit_card", :blank) if credit_card.blank?
+ end
end
end
end
diff --git a/activerecord/test/models/computer.rb b/activerecord/test/models/computer.rb
index cc8deb1b2b..582b4a38b5 100644
--- a/activerecord/test/models/computer.rb
+++ b/activerecord/test/models/computer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Computer < ActiveRecord::Base
- belongs_to :developer, :foreign_key=>'developer'
+ belongs_to :developer, foreign_key: "developer"
end
diff --git a/activerecord/test/models/contact.rb b/activerecord/test/models/contact.rb
index 3ea17c3abf..6e02ff199b 100644
--- a/activerecord/test/models/contact.rb
+++ b/activerecord/test/models/contact.rb
@@ -1,11 +1,13 @@
+# frozen_string_literal: true
+
module ContactFakeColumns
def self.extended(base)
base.class_eval do
- establish_connection(:adapter => 'fake')
+ establish_connection(adapter: "fake")
- connection.tables = [table_name]
+ connection.data_sources = [table_name]
connection.primary_keys = {
- table_name => 'id'
+ table_name => "id"
}
column :id, :integer
@@ -19,7 +21,7 @@ module ContactFakeColumns
serialize :preferences
- belongs_to :alternative, :class_name => 'Contact'
+ belongs_to :alternative, class_name: "Contact"
end
end
@@ -37,5 +39,5 @@ class ContactSti < ActiveRecord::Base
extend ContactFakeColumns
column :type, :string
- def type; 'ContactSti' end
+ def type; "ContactSti" end
end
diff --git a/activerecord/test/models/content.rb b/activerecord/test/models/content.rb
new file mode 100644
index 0000000000..14bbee53d8
--- /dev/null
+++ b/activerecord/test/models/content.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+class Content < ActiveRecord::Base
+ self.table_name = "content"
+ has_one :content_position, dependent: :destroy
+
+ def self.destroyed_ids
+ @destroyed_ids ||= []
+ end
+
+ before_destroy do |object|
+ Content.destroyed_ids << object.id
+ end
+end
+
+class ContentWhichRequiresTwoDestroyCalls < ActiveRecord::Base
+ self.table_name = "content"
+ has_one :content_position, foreign_key: "content_id", dependent: :destroy
+
+ after_initialize do
+ @destroy_count = 0
+ end
+
+ before_destroy do
+ @destroy_count += 1
+ if @destroy_count == 1
+ throw :abort
+ end
+ end
+end
+
+class ContentPosition < ActiveRecord::Base
+ belongs_to :content, dependent: :destroy
+
+ def self.destroyed_ids
+ @destroyed_ids ||= []
+ end
+
+ before_destroy do |object|
+ ContentPosition.destroyed_ids << object.id
+ end
+end
diff --git a/activerecord/test/models/contract.rb b/activerecord/test/models/contract.rb
index cdf7b267b5..3f663375c4 100644
--- a/activerecord/test/models/contract.rb
+++ b/activerecord/test/models/contract.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
class Contract < ActiveRecord::Base
belongs_to :company
- belongs_to :developer
- belongs_to :firm, :foreign_key => 'company_id'
+ belongs_to :developer, primary_key: :id
+ belongs_to :firm, foreign_key: "company_id"
before_save :hi
after_save :bye
@@ -18,3 +20,7 @@ class Contract < ActiveRecord::Base
@bye_count += 1
end
end
+
+class NewContract < Contract
+ validates :company_id, presence: true
+end
diff --git a/activerecord/test/models/country.rb b/activerecord/test/models/country.rb
index 7db9a4e731..4b4a276a98 100644
--- a/activerecord/test/models/country.rb
+++ b/activerecord/test/models/country.rb
@@ -1,7 +1,5 @@
-class Country < ActiveRecord::Base
-
- self.primary_key = :country_id
+# frozen_string_literal: true
+class Country < ActiveRecord::Base
has_and_belongs_to_many :treaties
-
end
diff --git a/activerecord/test/models/course.rb b/activerecord/test/models/course.rb
index f3d0e05ff7..4f346124ea 100644
--- a/activerecord/test/models/course.rb
+++ b/activerecord/test/models/course.rb
@@ -1,4 +1,6 @@
-require_dependency 'models/arunit2_model'
+# frozen_string_literal: true
+
+require_dependency "models/arunit2_model"
class Course < ARUnit2Model
belongs_to :college
diff --git a/activerecord/test/models/customer.rb b/activerecord/test/models/customer.rb
index afe4b3d707..bc501a5ce0 100644
--- a/activerecord/test/models/customer.rb
+++ b/activerecord/test/models/customer.rb
@@ -1,12 +1,15 @@
+# frozen_string_literal: true
+
class Customer < ActiveRecord::Base
cattr_accessor :gps_conversion_was_run
- composed_of :address, :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ], :allow_nil => true
- composed_of :balance, :class_name => "Money", :mapping => %w(balance amount), :converter => Proc.new(&:to_money)
- composed_of :gps_location, :allow_nil => true
- composed_of :non_blank_gps_location, :class_name => "GpsLocation", :allow_nil => true, :mapping => %w(gps_location gps_location),
- :converter => lambda { |gps| self.gps_conversion_was_run = true; gps.blank? ? nil : GpsLocation.new(gps)}
- composed_of :fullname, :mapping => %w(name to_s), :constructor => Proc.new { |name| Fullname.parse(name) }, :converter => :parse
+ composed_of :address, mapping: [ %w(address_street street), %w(address_city city), %w(address_country country) ], allow_nil: true
+ composed_of :balance, class_name: "Money", mapping: %w(balance amount)
+ composed_of :gps_location, allow_nil: true
+ composed_of :non_blank_gps_location, class_name: "GpsLocation", allow_nil: true, mapping: %w(gps_location gps_location),
+ converter: lambda { |gps| self.gps_conversion_was_run = true; gps.blank? ? nil : GpsLocation.new(gps) }
+ composed_of :fullname, mapping: %w(name to_s), constructor: Proc.new { |name| Fullname.parse(name) }, converter: :parse
+ composed_of :fullname_no_converter, mapping: %w(name to_s), class_name: "Fullname"
end
class Address
@@ -55,7 +58,7 @@ class GpsLocation
end
def ==(other)
- self.latitude == other.latitude && self.longitude == other.longitude
+ latitude == other.latitude && longitude == other.longitude
end
end
@@ -64,7 +67,12 @@ class Fullname
def self.parse(str)
return nil unless str
- new(*str.to_s.split)
+
+ if str.is_a?(Hash)
+ new(str[:first], str[:last])
+ else
+ new(*str.to_s.split)
+ end
end
def initialize(first, last = nil)
diff --git a/activerecord/test/models/customer_carrier.rb b/activerecord/test/models/customer_carrier.rb
new file mode 100644
index 0000000000..6cb9d5239d
--- /dev/null
+++ b/activerecord/test/models/customer_carrier.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class CustomerCarrier < ActiveRecord::Base
+ cattr_accessor :current_customer
+
+ belongs_to :customer
+ belongs_to :carrier
+
+ default_scope -> {
+ if current_customer
+ where(customer: current_customer)
+ else
+ all
+ end
+ }
+end
diff --git a/activerecord/test/models/dashboard.rb b/activerecord/test/models/dashboard.rb
index 1b3b54545f..d25ceeafb1 100644
--- a/activerecord/test/models/dashboard.rb
+++ b/activerecord/test/models/dashboard.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Dashboard < ActiveRecord::Base
self.primary_key = :dashboard_id
end
diff --git a/activerecord/test/models/default.rb b/activerecord/test/models/default.rb
index 887e9cc999..90f1046d87 100644
--- a/activerecord/test/models/default.rb
+++ b/activerecord/test/models/default.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
class Default < ActiveRecord::Base
end
diff --git a/activerecord/test/models/department.rb b/activerecord/test/models/department.rb
index 08004a0ed3..868b9bf4bf 100644
--- a/activerecord/test/models/department.rb
+++ b/activerecord/test/models/department.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Department < ActiveRecord::Base
has_many :chefs
belongs_to :hotel
diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb
index d2a5a7fc49..8881c69368 100644
--- a/activerecord/test/models/developer.rb
+++ b/activerecord/test/models/developer.rb
@@ -1,4 +1,6 @@
-require 'ostruct'
+# frozen_string_literal: true
+
+require "ostruct"
module DeveloperProjectsAssociationExtension2
def find_least_recent
@@ -7,61 +9,72 @@ module DeveloperProjectsAssociationExtension2
end
class Developer < ActiveRecord::Base
+ self.ignored_columns = %w(first_name last_name)
+
has_and_belongs_to_many :projects do
def find_most_recent
order("id DESC").first
end
end
+ belongs_to :mentor
+
accepts_nested_attributes_for :projects
has_and_belongs_to_many :shared_computers, class_name: "Computer"
has_and_belongs_to_many :projects_extended_by_name,
-> { extending(DeveloperProjectsAssociationExtension) },
- :class_name => "Project",
- :join_table => "developers_projects",
- :association_foreign_key => "project_id"
+ class_name: "Project",
+ join_table: "developers_projects",
+ association_foreign_key: "project_id"
has_and_belongs_to_many :projects_extended_by_name_twice,
-> { extending(DeveloperProjectsAssociationExtension, DeveloperProjectsAssociationExtension2) },
- :class_name => "Project",
- :join_table => "developers_projects",
- :association_foreign_key => "project_id"
+ class_name: "Project",
+ join_table: "developers_projects",
+ association_foreign_key: "project_id"
has_and_belongs_to_many :projects_extended_by_name_and_block,
-> { extending(DeveloperProjectsAssociationExtension) },
- :class_name => "Project",
- :join_table => "developers_projects",
- :association_foreign_key => "project_id" do
+ class_name: "Project",
+ join_table: "developers_projects",
+ association_foreign_key: "project_id" do
def find_least_recent
order("id ASC").first
end
end
- has_and_belongs_to_many :special_projects, :join_table => 'developers_projects', :association_foreign_key => 'project_id'
+ has_and_belongs_to_many :special_projects, join_table: "developers_projects", association_foreign_key: "project_id"
has_and_belongs_to_many :sym_special_projects,
- :join_table => :developers_projects,
- :association_foreign_key => 'project_id',
- :class_name => 'SpecialProject'
+ join_table: :developers_projects,
+ association_foreign_key: "project_id",
+ class_name: "SpecialProject"
has_many :audit_logs
has_many :contracts
- has_many :firms, :through => :contracts, :source => :firm
+ has_many :firms, through: :contracts, source: :firm
has_many :comments, ->(developer) { where(body: "I'm #{developer.name}") }
has_many :ratings, through: :comments
+ has_one :ship, dependent: :nullify
- scope :jamises, -> { where(:name => 'Jamis') }
+ belongs_to :firm
+ has_many :contracted_projects, class_name: "Project"
- validates_inclusion_of :salary, :in => 50000..200000
- validates_length_of :name, :within => 3..20
+ scope :jamises, -> { where(name: "Jamis") }
+
+ validates_inclusion_of :salary, in: 50000..200000
+ validates_length_of :name, within: 3..20
before_create do |developer|
- developer.audit_logs.build :message => "Computer created"
+ developer.audit_logs.build message: "Computer created"
end
+ attr_accessor :last_name
+ define_attribute_method "last_name"
+
def log=(message)
- audit_logs.build :message => message
+ audit_logs.build message: message
end
after_find :track_instance_count
@@ -72,17 +85,27 @@ class Developer < ActiveRecord::Base
self.class.instance_count += 1
end
private :track_instance_count
+end
+
+class SubDeveloper < Developer
+end
+
+class SymbolIgnoredDeveloper < ActiveRecord::Base
+ self.table_name = "developers"
+ self.ignored_columns = [:first_name, :last_name]
+ attr_accessor :last_name
+ define_attribute_method "last_name"
end
class AuditLog < ActiveRecord::Base
- belongs_to :developer, :validate => true
- belongs_to :unvalidated_developer, :class_name => 'Developer'
+ belongs_to :developer, validate: true
+ belongs_to :unvalidated_developer, class_name: "Developer"
end
class DeveloperWithBeforeDestroyRaise < ActiveRecord::Base
- self.table_name = 'developers'
- has_and_belongs_to_many :projects, :join_table => 'developers_projects', :foreign_key => 'developer_id'
+ self.table_name = "developers"
+ has_and_belongs_to_many :projects, join_table: "developers_projects", foreign_key: "developer_id"
before_destroy :raise_if_projects_empty!
def raise_if_projects_empty!
@@ -91,63 +114,63 @@ class DeveloperWithBeforeDestroyRaise < ActiveRecord::Base
end
class DeveloperWithSelect < ActiveRecord::Base
- self.table_name = 'developers'
- default_scope { select('name') }
+ self.table_name = "developers"
+ default_scope { select("name") }
end
class DeveloperWithIncludes < ActiveRecord::Base
- self.table_name = 'developers'
- has_many :audit_logs, :foreign_key => :developer_id
+ self.table_name = "developers"
+ has_many :audit_logs, foreign_key: :developer_id
default_scope { includes(:audit_logs) }
end
class DeveloperFilteredOnJoins < ActiveRecord::Base
- self.table_name = 'developers'
- has_and_belongs_to_many :projects, -> { order('projects.id') }, :foreign_key => 'developer_id', :join_table => 'developers_projects'
+ self.table_name = "developers"
+ has_and_belongs_to_many :projects, -> { order("projects.id") }, foreign_key: "developer_id", join_table: "developers_projects"
def self.default_scope
- joins(:projects).where(:projects => { :name => 'Active Controller' })
+ joins(:projects).where(projects: { name: "Active Controller" })
end
end
class DeveloperOrderedBySalary < ActiveRecord::Base
- self.table_name = 'developers'
- default_scope { order('salary DESC') }
+ self.table_name = "developers"
+ default_scope { order("salary DESC") }
- scope :by_name, -> { order('name DESC') }
+ scope :by_name, -> { order("name DESC") }
end
class DeveloperCalledDavid < ActiveRecord::Base
- self.table_name = 'developers'
+ self.table_name = "developers"
default_scope { where("name = 'David'") }
end
class LazyLambdaDeveloperCalledDavid < ActiveRecord::Base
- self.table_name = 'developers'
- default_scope lambda { where(:name => 'David') }
+ self.table_name = "developers"
+ default_scope lambda { where(name: "David") }
end
class LazyBlockDeveloperCalledDavid < ActiveRecord::Base
- self.table_name = 'developers'
- default_scope { where(:name => 'David') }
+ self.table_name = "developers"
+ default_scope { where(name: "David") }
end
class CallableDeveloperCalledDavid < ActiveRecord::Base
- self.table_name = 'developers'
- default_scope OpenStruct.new(:call => where(:name => 'David'))
+ self.table_name = "developers"
+ default_scope OpenStruct.new(call: where(name: "David"))
end
class ClassMethodDeveloperCalledDavid < ActiveRecord::Base
- self.table_name = 'developers'
+ self.table_name = "developers"
def self.default_scope
- where(:name => 'David')
+ where(name: "David")
end
end
class ClassMethodReferencingScopeDeveloperCalledDavid < ActiveRecord::Base
- self.table_name = 'developers'
- scope :david, -> { where(:name => 'David') }
+ self.table_name = "developers"
+ scope :david, -> { where(name: "David") }
def self.default_scope
david
@@ -155,61 +178,61 @@ class ClassMethodReferencingScopeDeveloperCalledDavid < ActiveRecord::Base
end
class LazyBlockReferencingScopeDeveloperCalledDavid < ActiveRecord::Base
- self.table_name = 'developers'
- scope :david, -> { where(:name => 'David') }
+ self.table_name = "developers"
+ scope :david, -> { where(name: "David") }
default_scope { david }
end
class DeveloperCalledJamis < ActiveRecord::Base
- self.table_name = 'developers'
+ self.table_name = "developers"
- default_scope { where(:name => 'Jamis') }
- scope :poor, -> { where('salary < 150000') }
+ default_scope { where(name: "Jamis") }
+ scope :poor, -> { where("salary < 150000") }
scope :david, -> { where name: "David" }
scope :david2, -> { unscoped.where name: "David" }
end
class PoorDeveloperCalledJamis < ActiveRecord::Base
- self.table_name = 'developers'
+ self.table_name = "developers"
- default_scope -> { where(:name => 'Jamis', :salary => 50000) }
+ default_scope -> { where(name: "Jamis", salary: 50000) }
end
class InheritedPoorDeveloperCalledJamis < DeveloperCalledJamis
- self.table_name = 'developers'
+ self.table_name = "developers"
- default_scope -> { where(:salary => 50000) }
+ default_scope -> { where(salary: 50000) }
end
class MultiplePoorDeveloperCalledJamis < ActiveRecord::Base
- self.table_name = 'developers'
+ self.table_name = "developers"
- default_scope -> { where(:name => 'Jamis') }
- default_scope -> { where(:salary => 50000) }
+ default_scope -> { where(name: "Jamis") }
+ default_scope -> { where(salary: 50000) }
end
module SalaryDefaultScope
extend ActiveSupport::Concern
- included { default_scope { where(:salary => 50000) } }
+ included { default_scope { where(salary: 50000) } }
end
class ModuleIncludedPoorDeveloperCalledJamis < DeveloperCalledJamis
- self.table_name = 'developers'
+ self.table_name = "developers"
include SalaryDefaultScope
end
class EagerDeveloperWithDefaultScope < ActiveRecord::Base
- self.table_name = 'developers'
- has_and_belongs_to_many :projects, -> { order('projects.id') }, :foreign_key => 'developer_id', :join_table => 'developers_projects'
+ self.table_name = "developers"
+ has_and_belongs_to_many :projects, -> { order("projects.id") }, foreign_key: "developer_id", join_table: "developers_projects"
default_scope { includes(:projects) }
end
class EagerDeveloperWithClassMethodDefaultScope < ActiveRecord::Base
- self.table_name = 'developers'
- has_and_belongs_to_many :projects, -> { order('projects.id') }, :foreign_key => 'developer_id', :join_table => 'developers_projects'
+ self.table_name = "developers"
+ has_and_belongs_to_many :projects, -> { order("projects.id") }, foreign_key: "developer_id", join_table: "developers_projects"
def self.default_scope
includes(:projects)
@@ -217,31 +240,31 @@ class EagerDeveloperWithClassMethodDefaultScope < ActiveRecord::Base
end
class EagerDeveloperWithLambdaDefaultScope < ActiveRecord::Base
- self.table_name = 'developers'
- has_and_belongs_to_many :projects, -> { order('projects.id') }, :foreign_key => 'developer_id', :join_table => 'developers_projects'
+ self.table_name = "developers"
+ has_and_belongs_to_many :projects, -> { order("projects.id") }, foreign_key: "developer_id", join_table: "developers_projects"
default_scope lambda { includes(:projects) }
end
class EagerDeveloperWithBlockDefaultScope < ActiveRecord::Base
- self.table_name = 'developers'
- has_and_belongs_to_many :projects, -> { order('projects.id') }, :foreign_key => 'developer_id', :join_table => 'developers_projects'
+ self.table_name = "developers"
+ has_and_belongs_to_many :projects, -> { order("projects.id") }, foreign_key: "developer_id", join_table: "developers_projects"
default_scope { includes(:projects) }
end
class EagerDeveloperWithCallableDefaultScope < ActiveRecord::Base
- self.table_name = 'developers'
- has_and_belongs_to_many :projects, -> { order('projects.id') }, :foreign_key => 'developer_id', :join_table => 'developers_projects'
+ self.table_name = "developers"
+ has_and_belongs_to_many :projects, -> { order("projects.id") }, foreign_key: "developer_id", join_table: "developers_projects"
- default_scope OpenStruct.new(:call => includes(:projects))
+ default_scope OpenStruct.new(call: includes(:projects))
end
class ThreadsafeDeveloper < ActiveRecord::Base
- self.table_name = 'developers'
+ 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
@@ -250,3 +273,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/dog.rb b/activerecord/test/models/dog.rb
index b02b8447b8..75d284ac25 100644
--- a/activerecord/test/models/dog.rb
+++ b/activerecord/test/models/dog.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Dog < ActiveRecord::Base
belongs_to :breeder, class_name: "DogLover", counter_cache: :bred_dogs_count
belongs_to :trainer, class_name: "DogLover", counter_cache: :trained_dogs_count
diff --git a/activerecord/test/models/dog_lover.rb b/activerecord/test/models/dog_lover.rb
index 2c5be94aea..aabe914f77 100644
--- a/activerecord/test/models/dog_lover.rb
+++ b/activerecord/test/models/dog_lover.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class DogLover < ActiveRecord::Base
has_many :trained_dogs, class_name: "Dog", foreign_key: :trainer_id, dependent: :destroy
has_many :bred_dogs, class_name: "Dog", foreign_key: :breeder_id
diff --git a/activerecord/test/models/doubloon.rb b/activerecord/test/models/doubloon.rb
index 2b11d128e2..febadc3a5a 100644
--- a/activerecord/test/models/doubloon.rb
+++ b/activerecord/test/models/doubloon.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AbstractDoubloon < ActiveRecord::Base
# This has functionality that might be shared by multiple classes.
@@ -8,5 +10,5 @@ end
class Doubloon < AbstractDoubloon
# This uses an abstract class that defines attributes and associations.
- self.table_name = 'doubloons'
+ self.table_name = "doubloons"
end
diff --git a/activerecord/test/models/drink_designer.rb b/activerecord/test/models/drink_designer.rb
index 2db968ef11..eb6701b84e 100644
--- a/activerecord/test/models/drink_designer.rb
+++ b/activerecord/test/models/drink_designer.rb
@@ -1,3 +1,8 @@
+# frozen_string_literal: true
+
class DrinkDesigner < ActiveRecord::Base
has_one :chef, as: :employable
end
+
+class MocktailDesigner < DrinkDesigner
+end
diff --git a/activerecord/test/models/edge.rb b/activerecord/test/models/edge.rb
index 55e0c31fcb..a04ab103de 100644
--- a/activerecord/test/models/edge.rb
+++ b/activerecord/test/models/edge.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
# This class models an edge in a directed graph.
class Edge < ActiveRecord::Base
- belongs_to :source, :class_name => 'Vertex', :foreign_key => 'source_id'
- belongs_to :sink, :class_name => 'Vertex', :foreign_key => 'sink_id'
+ belongs_to :source, class_name: "Vertex", foreign_key: "source_id"
+ belongs_to :sink, class_name: "Vertex", foreign_key: "sink_id"
end
diff --git a/activerecord/test/models/electron.rb b/activerecord/test/models/electron.rb
index 6fc270673f..902006b314 100644
--- a/activerecord/test/models/electron.rb
+++ b/activerecord/test/models/electron.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Electron < ActiveRecord::Base
belongs_to :molecule
diff --git a/activerecord/test/models/engine.rb b/activerecord/test/models/engine.rb
index 851ff8c22b..396a52b3b9 100644
--- a/activerecord/test/models/engine.rb
+++ b/activerecord/test/models/engine.rb
@@ -1,4 +1,5 @@
+# frozen_string_literal: true
+
class Engine < ActiveRecord::Base
- belongs_to :my_car, :class_name => 'Car', :foreign_key => 'car_id', :counter_cache => :engines_count
+ belongs_to :my_car, class_name: "Car", foreign_key: "car_id", counter_cache: :engines_count
end
-
diff --git a/activerecord/test/models/entrant.rb b/activerecord/test/models/entrant.rb
index 4682ce48c8..2c086e451f 100644
--- a/activerecord/test/models/entrant.rb
+++ b/activerecord/test/models/entrant.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Entrant < ActiveRecord::Base
belongs_to :course
end
diff --git a/activerecord/test/models/essay.rb b/activerecord/test/models/essay.rb
index ec4b982b5b..e59db4d877 100644
--- a/activerecord/test/models/essay.rb
+++ b/activerecord/test/models/essay.rb
@@ -1,5 +1,8 @@
+# frozen_string_literal: true
+
class Essay < ActiveRecord::Base
- belongs_to :writer, :primary_key => :name, :polymorphic => true
- belongs_to :category, :primary_key => :name
- has_one :owner, :primary_key => :name
+ belongs_to :author
+ belongs_to :writer, primary_key: :name, polymorphic: true
+ belongs_to :category, primary_key: :name
+ has_one :owner, primary_key: :name
end
diff --git a/activerecord/test/models/event.rb b/activerecord/test/models/event.rb
index 365ab32b0b..a7cdc39e5c 100644
--- a/activerecord/test/models/event.rb
+++ b/activerecord/test/models/event.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Event < ActiveRecord::Base
validates_uniqueness_of :title
end
diff --git a/activerecord/test/models/eye.rb b/activerecord/test/models/eye.rb
index dc8ae2b3f6..f3608b62ef 100644
--- a/activerecord/test/models/eye.rb
+++ b/activerecord/test/models/eye.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Eye < ActiveRecord::Base
attr_reader :after_create_callbacks_stack
attr_reader :after_update_callbacks_stack
@@ -15,19 +17,19 @@ class Eye < ActiveRecord::Base
after_create :trace_after_create2
after_update :trace_after_update2
after_save :trace_after_save2
-
+
def trace_after_create
(@after_create_callbacks_stack ||= []) << !iris.persisted?
end
alias trace_after_create2 trace_after_create
def trace_after_update
- (@after_update_callbacks_stack ||= []) << iris.changed?
+ (@after_update_callbacks_stack ||= []) << iris.has_changes_to_save?
end
alias trace_after_update2 trace_after_update
def trace_after_save
- (@after_save_callbacks_stack ||= []) << iris.changed?
+ (@after_save_callbacks_stack ||= []) << iris.has_changes_to_save?
end
alias trace_after_save2 trace_after_save
end
diff --git a/activerecord/test/models/face.rb b/activerecord/test/models/face.rb
index 91e46f83e5..e900fd40fb 100644
--- a/activerecord/test/models/face.rb
+++ b/activerecord/test/models/face.rb
@@ -1,9 +1,16 @@
+# frozen_string_literal: true
+
class Face < ActiveRecord::Base
- belongs_to :man, :inverse_of => :face
- belongs_to :polymorphic_man, :polymorphic => true, :inverse_of => :polymorphic_face
- # Oracle identifier lengh is limited to 30 bytes or less, `polymorphic` renamed `poly`
- belongs_to :poly_man_without_inverse, :polymorphic => true
+ belongs_to :man, inverse_of: :face
+ belongs_to :human, polymorphic: true
+ belongs_to :polymorphic_man, polymorphic: true, inverse_of: :polymorphic_face
+ # Oracle identifier length is limited to 30 bytes or less, `polymorphic` renamed `poly`
+ belongs_to :poly_man_without_inverse, polymorphic: true
# These is a "broken" inverse_of for the purposes of testing
- belongs_to :horrible_man, :class_name => 'Man', :inverse_of => :horrible_face
- belongs_to :horrible_polymorphic_man, :polymorphic => true, :inverse_of => :horrible_polymorphic_face
+ belongs_to :horrible_man, class_name: "Man", inverse_of: :horrible_face
+ belongs_to :horrible_polymorphic_man, polymorphic: true, inverse_of: :horrible_polymorphic_face
+
+ validate do
+ man
+ end
end
diff --git a/activerecord/test/models/family.rb b/activerecord/test/models/family.rb
new file mode 100644
index 0000000000..0713dba1a6
--- /dev/null
+++ b/activerecord/test/models/family.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+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..a8ea907c05
--- /dev/null
+++ b/activerecord/test/models/family_tree.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+class FamilyTree < ActiveRecord::Base
+ belongs_to :member, class_name: "User", foreign_key: "member_id"
+ belongs_to :family
+end
diff --git a/activerecord/test/models/friendship.rb b/activerecord/test/models/friendship.rb
index 4b411ca8e0..9f1712a8ec 100644
--- a/activerecord/test/models/friendship.rb
+++ b/activerecord/test/models/friendship.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
class Friendship < ActiveRecord::Base
- belongs_to :friend, class_name: 'Person'
+ belongs_to :friend, class_name: "Person"
# friend_too exists to test a bug, and probably shouldn't be used elsewhere
- belongs_to :friend_too, foreign_key: 'friend_id', class_name: 'Person', counter_cache: :friends_too_count
- belongs_to :follower, class_name: 'Person'
+ belongs_to :friend_too, foreign_key: "friend_id", class_name: "Person", counter_cache: :friends_too_count
+ belongs_to :follower, class_name: "Person"
end
diff --git a/activerecord/test/models/frog.rb b/activerecord/test/models/frog.rb
new file mode 100644
index 0000000000..73601aacdd
--- /dev/null
+++ b/activerecord/test/models/frog.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+class Frog < ActiveRecord::Base
+ after_save do
+ with_lock do
+ end
+ end
+end
diff --git a/activerecord/test/models/guid.rb b/activerecord/test/models/guid.rb
index 05653ba498..ec71c37690 100644
--- a/activerecord/test/models/guid.rb
+++ b/activerecord/test/models/guid.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
class Guid < ActiveRecord::Base
end
diff --git a/activerecord/test/models/guitar.rb b/activerecord/test/models/guitar.rb
new file mode 100644
index 0000000000..649b998665
--- /dev/null
+++ b/activerecord/test/models/guitar.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+class Guitar < ActiveRecord::Base
+ has_many :tuning_pegs, index_errors: true
+ accepts_nested_attributes_for :tuning_pegs
+end
diff --git a/activerecord/test/models/hotel.rb b/activerecord/test/models/hotel.rb
index 491f8dfde3..1a433c3cab 100644
--- a/activerecord/test/models/hotel.rb
+++ b/activerecord/test/models/hotel.rb
@@ -1,7 +1,13 @@
+# frozen_string_literal: true
+
class Hotel < ActiveRecord::Base
has_many :departments
has_many :chefs, through: :departments
- has_many :cake_designers, source_type: 'CakeDesigner', source: :employable, through: :chefs
- has_many :drink_designers, source_type: 'DrinkDesigner', source: :employable, through: :chefs
+ has_many :cake_designers, source_type: "CakeDesigner", source: :employable, through: :chefs
+ has_many :drink_designers, source_type: "DrinkDesigner", source: :employable, through: :chefs
+
+ has_many :chef_lists, as: :employable_list
+ has_many :mocktail_designers, through: :chef_lists, source: :employable, source_type: "MocktailDesigner"
+
has_many :recipes, through: :chefs
end
diff --git a/activerecord/test/models/image.rb b/activerecord/test/models/image.rb
index 7ae8e4a7f6..b4808293cc 100644
--- a/activerecord/test/models/image.rb
+++ b/activerecord/test/models/image.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Image < ActiveRecord::Base
belongs_to :imageable, foreign_key: :imageable_identifier, foreign_type: :imageable_class
end
diff --git a/activerecord/test/models/interest.rb b/activerecord/test/models/interest.rb
index d5d9226204..899b8f9b9d 100644
--- a/activerecord/test/models/interest.rb
+++ b/activerecord/test/models/interest.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
class Interest < ActiveRecord::Base
- belongs_to :man, :inverse_of => :interests
- belongs_to :polymorphic_man, :polymorphic => true, :inverse_of => :polymorphic_interests
- belongs_to :zine, :inverse_of => :interests
+ belongs_to :man, inverse_of: :interests
+ belongs_to :polymorphic_man, polymorphic: true, inverse_of: :polymorphic_interests
+ belongs_to :zine, inverse_of: :interests
end
diff --git a/activerecord/test/models/invoice.rb b/activerecord/test/models/invoice.rb
index fc6ef0230e..1851792ed5 100644
--- a/activerecord/test/models/invoice.rb
+++ b/activerecord/test/models/invoice.rb
@@ -1,4 +1,6 @@
+# frozen_string_literal: true
+
class Invoice < ActiveRecord::Base
- has_many :line_items, :autosave => true
- before_save {|record| record.balance = record.line_items.map(&:amount).sum }
+ has_many :line_items, autosave: true
+ before_save { |record| record.balance = record.line_items.map(&:amount).sum }
end
diff --git a/activerecord/test/models/item.rb b/activerecord/test/models/item.rb
index c2571dd7fb..8d079d56e6 100644
--- a/activerecord/test/models/item.rb
+++ b/activerecord/test/models/item.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
class AbstractItem < ActiveRecord::Base
self.abstract_class = true
- has_one :tagging, :as => :taggable
+ has_one :tagging, as: :taggable
end
class Item < AbstractItem
diff --git a/activerecord/test/models/job.rb b/activerecord/test/models/job.rb
index f7b0e787b1..52817a8435 100644
--- a/activerecord/test/models/job.rb
+++ b/activerecord/test/models/job.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
class Job < ActiveRecord::Base
has_many :references
- has_many :people, :through => :references
- belongs_to :ideal_reference, :class_name => 'Reference'
+ has_many :people, through: :references
+ belongs_to :ideal_reference, class_name: "Reference"
- has_many :agents, :through => :people
+ has_many :agents, through: :people
end
diff --git a/activerecord/test/models/joke.rb b/activerecord/test/models/joke.rb
index edda4655dc..436ffb6471 100644
--- a/activerecord/test/models/joke.rb
+++ b/activerecord/test/models/joke.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
class Joke < ActiveRecord::Base
- self.table_name = 'funny_jokes'
+ self.table_name = "funny_jokes"
end
class GoodJoke < ActiveRecord::Base
- self.table_name = 'funny_jokes'
+ self.table_name = "funny_jokes"
end
diff --git a/activerecord/test/models/keyboard.rb b/activerecord/test/models/keyboard.rb
index 39347e274e..d200e0fb56 100644
--- a/activerecord/test/models/keyboard.rb
+++ b/activerecord/test/models/keyboard.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Keyboard < ActiveRecord::Base
- self.primary_key = 'key_number'
+ self.primary_key = "key_number"
end
diff --git a/activerecord/test/models/legacy_thing.rb b/activerecord/test/models/legacy_thing.rb
index eead181a0e..e0210c8922 100644
--- a/activerecord/test/models/legacy_thing.rb
+++ b/activerecord/test/models/legacy_thing.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class LegacyThing < ActiveRecord::Base
self.locking_column = :version
end
diff --git a/activerecord/test/models/lesson.rb b/activerecord/test/models/lesson.rb
index 4c88153068..e546339689 100644
--- a/activerecord/test/models/lesson.rb
+++ b/activerecord/test/models/lesson.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class LessonError < Exception
end
diff --git a/activerecord/test/models/line_item.rb b/activerecord/test/models/line_item.rb
index 0dd921a300..3a51cf03b2 100644
--- a/activerecord/test/models/line_item.rb
+++ b/activerecord/test/models/line_item.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class LineItem < ActiveRecord::Base
- belongs_to :invoice, :touch => true
+ belongs_to :invoice, touch: true
end
diff --git a/activerecord/test/models/liquid.rb b/activerecord/test/models/liquid.rb
index 69d4d7df1a..b2fd305d66 100644
--- a/activerecord/test/models/liquid.rb
+++ b/activerecord/test/models/liquid.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Liquid < ActiveRecord::Base
self.table_name = :liquid
has_many :molecules, -> { distinct }
diff --git a/activerecord/test/models/man.rb b/activerecord/test/models/man.rb
index 4fbb6b226b..e26920e951 100644
--- a/activerecord/test/models/man.rb
+++ b/activerecord/test/models/man.rb
@@ -1,11 +1,16 @@
+# frozen_string_literal: true
+
class Man < ActiveRecord::Base
- has_one :face, :inverse_of => :man
- has_one :polymorphic_face, :class_name => 'Face', :as => :polymorphic_man, :inverse_of => :polymorphic_man
- has_one :polymorphic_face_without_inverse, :class_name => 'Face', :as => :poly_man_without_inverse
- has_many :interests, :inverse_of => :man
- has_many :polymorphic_interests, :class_name => 'Interest', :as => :polymorphic_man, :inverse_of => :polymorphic_man
+ has_one :face, inverse_of: :man
+ has_one :polymorphic_face, class_name: "Face", as: :polymorphic_man, inverse_of: :polymorphic_man
+ has_one :polymorphic_face_without_inverse, class_name: "Face", as: :poly_man_without_inverse
+ has_many :interests, inverse_of: :man
+ has_many :polymorphic_interests, class_name: "Interest", as: :polymorphic_man, inverse_of: :polymorphic_man
# These are "broken" inverse_of associations for the purposes of testing
- has_one :dirty_face, :class_name => 'Face', :inverse_of => :dirty_man
- has_many :secret_interests, :class_name => 'Interest', :inverse_of => :secret_man
+ has_one :dirty_face, class_name: "Face", inverse_of: :dirty_man
+ has_many :secret_interests, class_name: "Interest", inverse_of: :secret_man
has_one :mixed_case_monkey
end
+
+class Human < Man
+end
diff --git a/activerecord/test/models/matey.rb b/activerecord/test/models/matey.rb
index 47b0baa974..a77ac21e96 100644
--- a/activerecord/test/models/matey.rb
+++ b/activerecord/test/models/matey.rb
@@ -1,4 +1,6 @@
+# frozen_string_literal: true
+
class Matey < ActiveRecord::Base
belongs_to :pirate
- belongs_to :target, :class_name => 'Pirate'
+ belongs_to :target, class_name: "Pirate"
end
diff --git a/activerecord/test/models/member.rb b/activerecord/test/models/member.rb
index dc0566d8a7..6e33ac0a6d 100644
--- a/activerecord/test/models/member.rb
+++ b/activerecord/test/models/member.rb
@@ -1,32 +1,39 @@
+# frozen_string_literal: true
+
class Member < ActiveRecord::Base
has_one :current_membership
has_one :selected_membership
has_one :membership
- has_one :club, :through => :current_membership
- has_one :selected_club, :through => :selected_membership, :source => :club
- has_one :favourite_club, -> { where "memberships.favourite = ?", true }, :through => :membership, :source => :club
- has_one :hairy_club, -> { where :clubs => {:name => "Moustache and Eyebrow Fancier Club"} }, :through => :membership, :source => :club
- has_one :sponsor, :as => :sponsorable
- has_one :sponsor_club, :through => :sponsor
- has_one :member_detail, :inverse_of => false
- has_one :organization, :through => :member_detail
+ has_one :club, through: :current_membership
+ has_one :selected_club, through: :selected_membership, source: :club
+ has_one :favourite_club, -> { where "memberships.favourite = ?", true }, through: :membership, source: :club
+ has_one :hairy_club, -> { where clubs: { name: "Moustache and Eyebrow Fancier Club" } }, through: :membership, source: :club
+ has_one :sponsor, as: :sponsorable
+ has_one :sponsor_club, through: :sponsor
+ has_one :member_detail, inverse_of: false
+ has_one :organization, through: :member_detail
belongs_to :member_type
- has_many :nested_member_types, :through => :member_detail, :source => :member_type
- has_one :nested_member_type, :through => :member_detail, :source => :member_type
+ has_many :nested_member_types, through: :member_detail, source: :member_type
+ has_one :nested_member_type, through: :member_detail, source: :member_type
+
+ has_many :nested_sponsors, through: :sponsor_club, source: :sponsor
+ has_one :nested_sponsor, through: :sponsor_club, source: :sponsor
- has_many :nested_sponsors, :through => :sponsor_club, :source => :sponsor
- has_one :nested_sponsor, :through => :sponsor_club, :source => :sponsor
+ has_many :organization_member_details, through: :member_detail
+ has_many :organization_member_details_2, through: :organization, source: :member_details
- has_many :organization_member_details, :through => :member_detail
- has_many :organization_member_details_2, :through => :organization, :source => :member_details
+ has_one :club_category, through: :club, source: :category
+ has_one :general_club, -> { general }, through: :current_membership, source: :club
- has_one :club_category, :through => :club, :source => :category
+ has_many :super_memberships
+ has_many :favourite_memberships, -> { where(favourite: true) }, class_name: "Membership"
+ has_many :clubs, through: :favourite_memberships
- has_many :current_memberships, -> { where :favourite => true }
- has_many :clubs, :through => :current_memberships
+ has_many :tenant_memberships
+ has_many :tenant_clubs, through: :tenant_memberships, class_name: "Club", source: :club
- has_one :club_through_many, :through => :current_memberships, :source => :club
+ has_one :club_through_many, through: :favourite_memberships, source: :club
belongs_to :admittable, polymorphic: true
has_one :premium_club, through: :admittable
@@ -34,5 +41,5 @@ end
class SelfMember < ActiveRecord::Base
self.table_name = "members"
- has_and_belongs_to_many :friends, :class_name => "SelfMember", :join_table => "member_friends"
+ has_and_belongs_to_many :friends, class_name: "SelfMember", join_table: "member_friends"
end
diff --git a/activerecord/test/models/member_detail.rb b/activerecord/test/models/member_detail.rb
index 9d253aa126..e121a849d0 100644
--- a/activerecord/test/models/member_detail.rb
+++ b/activerecord/test/models/member_detail.rb
@@ -1,7 +1,11 @@
+# frozen_string_literal: true
+
class MemberDetail < ActiveRecord::Base
- belongs_to :member, :inverse_of => false
+ belongs_to :member, inverse_of: false
belongs_to :organization
- has_one :member_type, :through => :member
+ has_one :member_type, through: :member
+ has_one :membership, through: :member
+ has_one :admittable, through: :member, source_type: "Member"
- has_many :organization_member_details, :through => :organization, :source => :member_details
+ has_many :organization_member_details, through: :organization, source: :member_details
end
diff --git a/activerecord/test/models/member_type.rb b/activerecord/test/models/member_type.rb
index a13561c72a..b49b168d03 100644
--- a/activerecord/test/models/member_type.rb
+++ b/activerecord/test/models/member_type.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class MemberType < ActiveRecord::Base
has_many :members
end
diff --git a/activerecord/test/models/membership.rb b/activerecord/test/models/membership.rb
index df7167ee93..09ee7544b3 100644
--- a/activerecord/test/models/membership.rb
+++ b/activerecord/test/models/membership.rb
@@ -1,4 +1,7 @@
+# frozen_string_literal: true
+
class Membership < ActiveRecord::Base
+ enum type: %i(Membership CurrentMembership SuperMembership SelectedMembership TenantMembership)
belongs_to :member
belongs_to :club
end
@@ -9,7 +12,7 @@ class CurrentMembership < Membership
end
class SuperMembership < Membership
- belongs_to :member, -> { order('members.id DESC') }
+ belongs_to :member, -> { order("members.id DESC") }
belongs_to :club
end
@@ -18,3 +21,18 @@ class SelectedMembership < Membership
select("'1' as foo")
end
end
+
+class TenantMembership < Membership
+ cattr_accessor :current_member
+
+ belongs_to :member
+ belongs_to :club
+
+ default_scope -> {
+ if current_member
+ where(member: current_member)
+ else
+ all
+ end
+ }
+end
diff --git a/activerecord/test/models/mentor.rb b/activerecord/test/models/mentor.rb
new file mode 100644
index 0000000000..2fbb62c435
--- /dev/null
+++ b/activerecord/test/models/mentor.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class Mentor < ActiveRecord::Base
+ has_many :developers
+end
diff --git a/activerecord/test/models/minimalistic.rb b/activerecord/test/models/minimalistic.rb
index 2e3f8e081a..c67b086853 100644
--- a/activerecord/test/models/minimalistic.rb
+++ b/activerecord/test/models/minimalistic.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
class Minimalistic < ActiveRecord::Base
end
diff --git a/activerecord/test/models/minivan.rb b/activerecord/test/models/minivan.rb
index 4fe79720ad..d9d331798a 100644
--- a/activerecord/test/models/minivan.rb
+++ b/activerecord/test/models/minivan.rb
@@ -1,9 +1,10 @@
+# frozen_string_literal: true
+
class Minivan < ActiveRecord::Base
self.primary_key = :minivan_id
belongs_to :speedometer
- has_one :dashboard, :through => :speedometer
+ has_one :dashboard, through: :speedometer
attr_readonly :color
-
end
diff --git a/activerecord/test/models/mixed_case_monkey.rb b/activerecord/test/models/mixed_case_monkey.rb
index 1c35006665..8e92f68817 100644
--- a/activerecord/test/models/mixed_case_monkey.rb
+++ b/activerecord/test/models/mixed_case_monkey.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class MixedCaseMonkey < ActiveRecord::Base
belongs_to :man
end
diff --git a/activerecord/test/models/molecule.rb b/activerecord/test/models/molecule.rb
index 26870c8f88..7da08a85c4 100644
--- a/activerecord/test/models/molecule.rb
+++ b/activerecord/test/models/molecule.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Molecule < ActiveRecord::Base
belongs_to :liquid
has_many :electrons
diff --git a/activerecord/test/models/movie.rb b/activerecord/test/models/movie.rb
index 0302abad1e..fa2ea900c7 100644
--- a/activerecord/test/models/movie.rb
+++ b/activerecord/test/models/movie.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Movie < ActiveRecord::Base
self.primary_key = "movieid"
diff --git a/activerecord/test/models/node.rb b/activerecord/test/models/node.rb
index 07dd2dbccb..ae46c76b46 100644
--- a/activerecord/test/models/node.rb
+++ b/activerecord/test/models/node.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
class Node < ActiveRecord::Base
belongs_to :tree, touch: true
- belongs_to :parent, class_name: 'Node', touch: true, optional: true
- has_many :children, class_name: 'Node', foreign_key: :parent_id, dependent: :destroy
+ belongs_to :parent, class_name: "Node", touch: true, optional: true
+ has_many :children, class_name: "Node", foreign_key: :parent_id, dependent: :destroy
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..e954375989
--- /dev/null
+++ b/activerecord/test/models/non_primary_key.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+class NonPrimaryKey < ActiveRecord::Base
+end
diff --git a/activerecord/test/models/notification.rb b/activerecord/test/models/notification.rb
index b4b4b8f1b6..3f8728af5e 100644
--- a/activerecord/test/models/notification.rb
+++ b/activerecord/test/models/notification.rb
@@ -1,2 +1,5 @@
+# frozen_string_literal: true
+
class Notification < ActiveRecord::Base
+ validates_presence_of :message
end
diff --git a/activerecord/test/models/numeric_data.rb b/activerecord/test/models/numeric_data.rb
new file mode 100644
index 0000000000..666e1a5778
--- /dev/null
+++ b/activerecord/test/models/numeric_data.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class NumericData < ActiveRecord::Base
+ self.table_name = "numeric_data"
+ # Decimal columns with 0 scale being automatically treated as integers
+ # is deprecated, and will be removed in a future version of Rails.
+ attribute :world_population, :big_integer
+ attribute :my_house_population, :big_integer
+ attribute :atoms_in_universe, :big_integer
+end
diff --git a/activerecord/test/models/order.rb b/activerecord/test/models/order.rb
index e838c0b70d..36866b398f 100644
--- a/activerecord/test/models/order.rb
+++ b/activerecord/test/models/order.rb
@@ -1,4 +1,6 @@
+# frozen_string_literal: true
+
class Order < ActiveRecord::Base
- belongs_to :billing, :class_name => 'Customer', :foreign_key => 'billing_customer_id'
- belongs_to :shipping, :class_name => 'Customer', :foreign_key => 'shipping_customer_id'
+ belongs_to :billing, class_name: "Customer", foreign_key: "billing_customer_id"
+ belongs_to :shipping, class_name: "Customer", foreign_key: "shipping_customer_id"
end
diff --git a/activerecord/test/models/organization.rb b/activerecord/test/models/organization.rb
index f3e92f3067..099e7e38e0 100644
--- a/activerecord/test/models/organization.rb
+++ b/activerecord/test/models/organization.rb
@@ -1,14 +1,16 @@
+# frozen_string_literal: true
+
class Organization < ActiveRecord::Base
has_many :member_details
- has_many :members, :through => :member_details
+ has_many :members, through: :member_details
- has_many :authors, :primary_key => :name
- has_many :author_essay_categories, :through => :authors, :source => :essay_categories
+ has_many :authors, primary_key: :name
+ has_many :author_essay_categories, through: :authors, source: :essay_categories
- has_one :author, :primary_key => :name
- has_one :author_owned_essay_category, :through => :author, :source => :owned_essay_category
+ has_one :author, primary_key: :name
+ has_one :author_owned_essay_category, through: :author, source: :owned_essay_category
- has_many :posts, :through => :author, :source => :posts
+ has_many :posts, through: :author, source: :posts
- scope :clubs, -> { from('clubs') }
+ scope :clubs, -> { from("clubs") }
end
diff --git a/activerecord/test/models/other_dog.rb b/activerecord/test/models/other_dog.rb
new file mode 100644
index 0000000000..a0fda5ae1b
--- /dev/null
+++ b/activerecord/test/models/other_dog.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require_dependency "models/arunit2_model"
+
+class OtherDog < ARUnit2Model
+ self.table_name = "dogs"
+end
diff --git a/activerecord/test/models/owner.rb b/activerecord/test/models/owner.rb
index cedb774b10..5fa50d9918 100644
--- a/activerecord/test/models/owner.rb
+++ b/activerecord/test/models/owner.rb
@@ -1,18 +1,21 @@
+# frozen_string_literal: true
+
class Owner < ActiveRecord::Base
self.primary_key = :owner_id
- has_many :pets, -> { order 'pets.name desc' }
- has_many :toys, :through => :pets
+ has_many :pets, -> { order "pets.name desc" }
+ has_many :toys, through: :pets
+ has_many :persons, through: :pets
- belongs_to :last_pet, class_name: 'Pet'
+ belongs_to :last_pet, class_name: "Pet"
scope :including_last_pet, -> {
- select(%q[
+ select('
owners.*, (
select p.pet_id from pets p
where p.owner_id = owners.owner_id
order by p.name desc
limit 1
) as last_pet_id
- ]).includes(:last_pet)
+ ').includes(:last_pet)
}
after_commit :execute_blocks
diff --git a/activerecord/test/models/parrot.rb b/activerecord/test/models/parrot.rb
index b26035d944..3bb5316eca 100644
--- a/activerecord/test/models/parrot.rb
+++ b/activerecord/test/models/parrot.rb
@@ -1,29 +1,36 @@
+# frozen_string_literal: true
+
class Parrot < ActiveRecord::Base
self.inheritance_column = :parrot_sti_class
has_and_belongs_to_many :pirates
has_and_belongs_to_many :treasures
- has_many :loots, :as => :looter
+ has_many :loots, as: :looter
alias_attribute :title, :name
validates_presence_of :name
- attr_accessor :cancel_save_from_callback
- before_save :cancel_save_callback_method, :if => :cancel_save_from_callback
+ attribute :cancel_save_from_callback
+ before_save :cancel_save_callback_method, if: :cancel_save_from_callback
def cancel_save_callback_method
throw(:abort)
end
+
+ before_update :increment_updated_count
+ def increment_updated_count
+ self.updated_count += 1
+ end
+
+ def self.delete_all(*)
+ connection.delete("DELETE FROM parrots_pirates")
+ connection.delete("DELETE FROM parrots_treasures")
+ super
+ end
end
class LiveParrot < Parrot
end
class DeadParrot < Parrot
- belongs_to :killer, :class_name => 'Pirate', foreign_key: :killer_id
-end
-
-class FunkyParrot < Parrot
- before_destroy do
- raise "before_destroy was called"
- end
+ belongs_to :killer, class_name: "Pirate", foreign_key: :killer_id
end
diff --git a/activerecord/test/models/person.rb b/activerecord/test/models/person.rb
index ad12f00d42..5cba1e440e 100644
--- a/activerecord/test/models/person.rb
+++ b/activerecord/test/models/person.rb
@@ -1,74 +1,74 @@
+# frozen_string_literal: true
+
class Person < ActiveRecord::Base
has_many :readers
has_many :secure_readers
has_one :reader
- has_many :posts, :through => :readers
- has_many :secure_posts, :through => :secure_readers
- has_many :posts_with_no_comments, -> { includes(:comments).where('comments.id is null').references(:comments) },
- :through => :readers, :source => :post
+ has_many :posts, through: :readers
+ has_many :secure_posts, through: :secure_readers
+ has_many :posts_with_no_comments, -> { includes(:comments).where("comments.id is null").references(:comments) },
+ through: :readers, source: :post
- has_many :friendships, foreign_key: 'friend_id'
+ has_many :friendships, foreign_key: "friend_id"
# friends_too exists to test a bug, and probably shouldn't be used elsewhere
- has_many :friends_too, foreign_key: 'friend_id', class_name: 'Friendship'
+ has_many :friends_too, foreign_key: "friend_id", class_name: "Friendship"
has_many :followers, through: :friendships
has_many :references
has_many :bad_references
- has_many :fixed_bad_references, -> { where :favourite => true }, :class_name => 'BadReference'
- has_one :favourite_reference, -> { where 'favourite=?', true }, :class_name => 'Reference'
- has_many :posts_with_comments_sorted_by_comment_id, -> { includes(:comments).order('comments.id') }, :through => :readers, :source => :post
+ has_many :fixed_bad_references, -> { where favourite: true }, class_name: "BadReference"
+ has_one :favourite_reference, -> { where "favourite=?", true }, class_name: "Reference"
+ has_many :posts_with_comments_sorted_by_comment_id, -> { includes(:comments).order("comments.id") }, through: :readers, source: :post
has_many :first_posts, -> { where(id: [1, 2]) }, through: :readers
- has_many :jobs, :through => :references
- has_many :jobs_with_dependent_destroy, :source => :job, :through => :references, :dependent => :destroy
- has_many :jobs_with_dependent_delete_all, :source => :job, :through => :references, :dependent => :delete_all
- has_many :jobs_with_dependent_nullify, :source => :job, :through => :references, :dependent => :nullify
+ has_many :jobs, through: :references
+ has_many :jobs_with_dependent_destroy, source: :job, through: :references, dependent: :destroy
+ has_many :jobs_with_dependent_delete_all, source: :job, through: :references, dependent: :delete_all
+ has_many :jobs_with_dependent_nullify, source: :job, through: :references, dependent: :nullify
- belongs_to :primary_contact, :class_name => 'Person'
- has_many :agents, :class_name => 'Person', :foreign_key => 'primary_contact_id'
- has_many :agents_of_agents, :through => :agents, :source => :agents
- belongs_to :number1_fan, :class_name => 'Person'
+ belongs_to :primary_contact, class_name: "Person"
+ has_many :agents, class_name: "Person", foreign_key: "primary_contact_id"
+ has_many :agents_of_agents, through: :agents, source: :agents
+ belongs_to :number1_fan, class_name: "Person"
- has_many :personal_legacy_things, :dependent => :destroy
+ has_many :personal_legacy_things, dependent: :destroy
- has_many :agents_posts, :through => :agents, :source => :posts
- has_many :agents_posts_authors, :through => :agents_posts, :source => :author
+ has_many :agents_posts, through: :agents, source: :posts
+ has_many :agents_posts_authors, through: :agents_posts, source: :author
has_many :essays, primary_key: "first_name", foreign_key: "writer_id"
- scope :males, -> { where(:gender => 'M') }
- scope :females, -> { where(:gender => 'F') }
+ scope :males, -> { where(gender: "M") }
end
class PersonWithDependentDestroyJobs < ActiveRecord::Base
- self.table_name = 'people'
+ self.table_name = "people"
- has_many :references, :foreign_key => :person_id
- has_many :jobs, :source => :job, :through => :references, :dependent => :destroy
+ has_many :references, foreign_key: :person_id
+ has_many :jobs, source: :job, through: :references, dependent: :destroy
end
class PersonWithDependentDeleteAllJobs < ActiveRecord::Base
- self.table_name = 'people'
+ self.table_name = "people"
- has_many :references, :foreign_key => :person_id
- has_many :jobs, :source => :job, :through => :references, :dependent => :delete_all
+ has_many :references, foreign_key: :person_id
+ has_many :jobs, source: :job, through: :references, dependent: :delete_all
end
class PersonWithDependentNullifyJobs < ActiveRecord::Base
- self.table_name = 'people'
+ self.table_name = "people"
- has_many :references, :foreign_key => :person_id
- has_many :jobs, :source => :job, :through => :references, :dependent => :nullify
+ has_many :references, foreign_key: :person_id
+ has_many :jobs, source: :job, through: :references, dependent: :nullify
end
-
class LoosePerson < ActiveRecord::Base
- self.table_name = 'people'
+ self.table_name = "people"
self.abstract_class = true
- has_one :best_friend, :class_name => 'LoosePerson', :foreign_key => :best_friend_id
- belongs_to :best_friend_of, :class_name => 'LoosePerson', :foreign_key => :best_friend_of_id
- has_many :best_friends, :class_name => 'LoosePerson', :foreign_key => :best_friend_id
+ has_one :best_friend, class_name: "LoosePerson", foreign_key: :best_friend_id
+ belongs_to :best_friend_of, class_name: "LoosePerson", foreign_key: :best_friend_of_id
+ has_many :best_friends, class_name: "LoosePerson", foreign_key: :best_friend_id
accepts_nested_attributes_for :best_friend, :best_friend_of, :best_friends
end
@@ -76,11 +76,11 @@ end
class LooseDescendant < LoosePerson; end
class TightPerson < ActiveRecord::Base
- self.table_name = 'people'
+ self.table_name = "people"
- has_one :best_friend, :class_name => 'TightPerson', :foreign_key => :best_friend_id
- belongs_to :best_friend_of, :class_name => 'TightPerson', :foreign_key => :best_friend_of_id
- has_many :best_friends, :class_name => 'TightPerson', :foreign_key => :best_friend_id
+ has_one :best_friend, class_name: "TightPerson", foreign_key: :best_friend_id
+ belongs_to :best_friend_of, class_name: "TightPerson", foreign_key: :best_friend_of_id
+ has_many :best_friends, class_name: "TightPerson", foreign_key: :best_friend_id
accepts_nested_attributes_for :best_friend, :best_friend_of, :best_friends
end
@@ -88,56 +88,56 @@ end
class TightDescendant < TightPerson; end
class RichPerson < ActiveRecord::Base
- self.table_name = 'people'
+ self.table_name = "people"
- has_and_belongs_to_many :treasures, :join_table => 'peoples_treasures'
+ has_and_belongs_to_many :treasures, join_table: "peoples_treasures"
before_validation :run_before_create, on: :create
before_validation :run_before_validation
private
- def run_before_create
- self.first_name = first_name.to_s + 'run_before_create'
- end
+ def run_before_create
+ self.first_name = first_name.to_s + "run_before_create"
+ end
- def run_before_validation
- self.first_name = first_name.to_s + 'run_before_validation'
- end
+ def run_before_validation
+ self.first_name = first_name.to_s + "run_before_validation"
+ end
end
class NestedPerson < ActiveRecord::Base
- self.table_name = 'people'
+ self.table_name = "people"
- has_one :best_friend, :class_name => 'NestedPerson', :foreign_key => :best_friend_id
- accepts_nested_attributes_for :best_friend, :update_only => true
+ has_one :best_friend, class_name: "NestedPerson", foreign_key: :best_friend_id
+ accepts_nested_attributes_for :best_friend, update_only: true
def comments=(new_comments)
raise RuntimeError
end
def best_friend_first_name=(new_name)
- assign_attributes({ :best_friend_attributes => { :first_name => new_name } })
+ assign_attributes(best_friend_attributes: { first_name: new_name })
end
end
class Insure
INSURES = %W{life annuality}
- def self.load mask
+ def self.load(mask)
INSURES.select do |insure|
(1 << INSURES.index(insure)) & mask.to_i > 0
end
end
- def self.dump insures
+ def self.dump(insures)
numbers = insures.map { |insure| INSURES.index(insure) }
numbers.inject(0) { |sum, n| sum + (1 << n) }
end
end
class SerializedPerson < ActiveRecord::Base
- self.table_name = 'people'
+ self.table_name = "people"
serialize :insures, Insure
end
diff --git a/activerecord/test/models/personal_legacy_thing.rb b/activerecord/test/models/personal_legacy_thing.rb
index a7ee3a0bca..ed8b70cfcc 100644
--- a/activerecord/test/models/personal_legacy_thing.rb
+++ b/activerecord/test/models/personal_legacy_thing.rb
@@ -1,4 +1,6 @@
+# frozen_string_literal: true
+
class PersonalLegacyThing < ActiveRecord::Base
self.locking_column = :version
- belongs_to :person, :counter_cache => true
+ belongs_to :person, counter_cache: true
end
diff --git a/activerecord/test/models/pet.rb b/activerecord/test/models/pet.rb
index f7970d7aab..9bda2109e6 100644
--- a/activerecord/test/models/pet.rb
+++ b/activerecord/test/models/pet.rb
@@ -1,9 +1,14 @@
+# frozen_string_literal: true
+
class Pet < ActiveRecord::Base
attr_accessor :current_user
self.primary_key = :pet_id
- belongs_to :owner, :touch => true
+ belongs_to :owner, touch: true
has_many :toys
+ has_many :pet_treasures
+ has_many :treasures, through: :pet_treasures
+ has_many :persons, through: :treasures, source: :looter, source_type: "Person"
class << self
attr_accessor :after_destroy_output
diff --git a/activerecord/test/models/pet_treasure.rb b/activerecord/test/models/pet_treasure.rb
new file mode 100644
index 0000000000..47b9f57fad
--- /dev/null
+++ b/activerecord/test/models/pet_treasure.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+class PetTreasure < ActiveRecord::Base
+ self.table_name = "pets_treasures"
+
+ belongs_to :pet
+ belongs_to :treasure
+end
diff --git a/activerecord/test/models/pirate.rb b/activerecord/test/models/pirate.rb
index 30545bdcd7..fd5083e597 100644
--- a/activerecord/test/models/pirate.rb
+++ b/activerecord/test/models/pirate.rb
@@ -1,47 +1,55 @@
+# frozen_string_literal: true
+
class Pirate < ActiveRecord::Base
- belongs_to :parrot, :validate => true
- belongs_to :non_validated_parrot, :class_name => 'Parrot'
- has_and_belongs_to_many :parrots, -> { order('parrots.id ASC') }, :validate => true
- has_and_belongs_to_many :non_validated_parrots, :class_name => 'Parrot'
- has_and_belongs_to_many :parrots_with_method_callbacks, :class_name => "Parrot",
- :before_add => :log_before_add,
- :after_add => :log_after_add,
- :before_remove => :log_before_remove,
- :after_remove => :log_after_remove
- has_and_belongs_to_many :parrots_with_proc_callbacks, :class_name => "Parrot",
- :before_add => proc {|p,pa| p.ship_log << "before_adding_proc_parrot_#{pa.id || '<new>'}"},
- :after_add => proc {|p,pa| p.ship_log << "after_adding_proc_parrot_#{pa.id || '<new>'}"},
- :before_remove => proc {|p,pa| p.ship_log << "before_removing_proc_parrot_#{pa.id}"},
- :after_remove => proc {|p,pa| p.ship_log << "after_removing_proc_parrot_#{pa.id}"}
+ belongs_to :parrot, validate: true
+ belongs_to :non_validated_parrot, class_name: "Parrot"
+ has_and_belongs_to_many :parrots, -> { order("parrots.id ASC") }, validate: true
+ has_and_belongs_to_many :non_validated_parrots, class_name: "Parrot"
+ has_and_belongs_to_many :parrots_with_method_callbacks, class_name: "Parrot",
+ before_add: :log_before_add,
+ after_add: :log_after_add,
+ before_remove: :log_before_remove,
+ after_remove: :log_after_remove
+ has_and_belongs_to_many :parrots_with_proc_callbacks, class_name: "Parrot",
+ before_add: proc { |p, pa| p.ship_log << "before_adding_proc_parrot_#{pa.id || '<new>'}" },
+ after_add: proc { |p, pa| p.ship_log << "after_adding_proc_parrot_#{pa.id || '<new>'}" },
+ before_remove: proc { |p, pa| p.ship_log << "before_removing_proc_parrot_#{pa.id}" },
+ after_remove: proc { |p, pa| p.ship_log << "after_removing_proc_parrot_#{pa.id}" }
has_and_belongs_to_many :autosaved_parrots, class_name: "Parrot", autosave: true
- has_many :treasures, :as => :looter
- has_many :treasure_estimates, :through => :treasures, :source => :price_estimates
+ module PostTreasuresExtension
+ def build(attributes = {})
+ super({ name: "from extension" }.merge(attributes))
+ end
+ end
+
+ has_many :treasures, as: :looter, extend: PostTreasuresExtension
+ has_many :treasure_estimates, through: :treasures, source: :price_estimates
has_one :ship
- has_one :update_only_ship, :class_name => 'Ship'
- has_one :non_validated_ship, :class_name => 'Ship'
- has_many :birds, -> { order('birds.id ASC') }
- has_many :birds_with_method_callbacks, :class_name => "Bird",
- :before_add => :log_before_add,
- :after_add => :log_after_add,
- :before_remove => :log_before_remove,
- :after_remove => :log_after_remove
- has_many :birds_with_proc_callbacks, :class_name => "Bird",
- :before_add => proc {|p,b| p.ship_log << "before_adding_proc_bird_#{b.id || '<new>'}"},
- :after_add => proc {|p,b| p.ship_log << "after_adding_proc_bird_#{b.id || '<new>'}"},
- :before_remove => proc {|p,b| p.ship_log << "before_removing_proc_bird_#{b.id}"},
- :after_remove => proc {|p,b| p.ship_log << "after_removing_proc_bird_#{b.id}"}
- has_many :birds_with_reject_all_blank, :class_name => "Bird"
-
- has_one :foo_bulb, -> { where :name => 'foo' }, :foreign_key => :car_id, :class_name => "Bulb"
-
- accepts_nested_attributes_for :parrots, :birds, :allow_destroy => true, :reject_if => proc(&:empty?)
- accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc(&:empty?)
- accepts_nested_attributes_for :update_only_ship, :update_only => true
+ has_one :update_only_ship, class_name: "Ship"
+ has_one :non_validated_ship, class_name: "Ship"
+ has_many :birds, -> { order("birds.id ASC") }
+ has_many :birds_with_method_callbacks, class_name: "Bird",
+ before_add: :log_before_add,
+ after_add: :log_after_add,
+ before_remove: :log_before_remove,
+ after_remove: :log_after_remove
+ has_many :birds_with_proc_callbacks, class_name: "Bird",
+ before_add: proc { |p, b| p.ship_log << "before_adding_proc_bird_#{b.id || '<new>'}" },
+ after_add: proc { |p, b| p.ship_log << "after_adding_proc_bird_#{b.id || '<new>'}" },
+ before_remove: proc { |p, b| p.ship_log << "before_removing_proc_bird_#{b.id}" },
+ after_remove: proc { |p, b| p.ship_log << "after_removing_proc_bird_#{b.id}" }
+ has_many :birds_with_reject_all_blank, class_name: "Bird"
+
+ has_one :foo_bulb, -> { where name: "foo" }, foreign_key: :car_id, class_name: "Bulb"
+
+ accepts_nested_attributes_for :parrots, :birds, allow_destroy: true, reject_if: proc(&:empty?)
+ accepts_nested_attributes_for :ship, allow_destroy: true, reject_if: proc(&:empty?)
+ accepts_nested_attributes_for :update_only_ship, update_only: true
accepts_nested_attributes_for :parrots_with_method_callbacks, :parrots_with_proc_callbacks,
- :birds_with_method_callbacks, :birds_with_proc_callbacks, :allow_destroy => true
- accepts_nested_attributes_for :birds_with_reject_all_blank, :reject_if => :all_blank
+ :birds_with_method_callbacks, :birds_with_proc_callbacks, allow_destroy: true
+ accepts_nested_attributes_for :birds_with_reject_all_blank, reject_if: :all_blank
validates_presence_of :catchphrase
@@ -50,11 +58,11 @@ class Pirate < ActiveRecord::Base
end
def reject_empty_ships_on_create(attributes)
- attributes.delete('_reject_me_if_new').present? && !persisted?
+ attributes.delete("_reject_me_if_new").present? && !persisted?
end
attr_accessor :cancel_save_from_callback, :parrots_limit
- before_save :cancel_save_callback_method, :if => :cancel_save_from_callback
+ before_save :cancel_save_callback_method, if: :cancel_save_from_callback
def cancel_save_callback_method
throw(:abort)
end
@@ -82,11 +90,11 @@ class Pirate < ActiveRecord::Base
end
class DestructivePirate < Pirate
- has_one :dependent_ship, :class_name => 'Ship', :foreign_key => :pirate_id, :dependent => :destroy
+ has_one :dependent_ship, class_name: "Ship", foreign_key: :pirate_id, dependent: :destroy
end
class FamousPirate < ActiveRecord::Base
- self.table_name = 'pirates'
+ self.table_name = "pirates"
has_many :famous_ships
validates_presence_of :catchphrase, on: :conference
end
diff --git a/activerecord/test/models/possession.rb b/activerecord/test/models/possession.rb
index ddf759113b..9b843e1525 100644
--- a/activerecord/test/models/possession.rb
+++ b/activerecord/test/models/possession.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Possession < ActiveRecord::Base
- self.table_name = 'having'
+ self.table_name = "having"
end
diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb
index 10f13b67da..710a75ad30 100644
--- a/activerecord/test/models/post.rb
+++ b/activerecord/test/models/post.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Post < ActiveRecord::Base
class CategoryPost < ActiveRecord::Base
self.table_name = "categories_posts"
@@ -7,36 +9,38 @@ class Post < ActiveRecord::Base
module NamedExtension
def author
- 'lifo'
+ "lifo"
end
end
module NamedExtension2
def greeting
- "hello"
+ "hullo"
end
end
scope :containing_the_letter_a, -> { where("body LIKE '%a%'") }
scope :titled_with_an_apostrophe, -> { where("title LIKE '%''%'") }
- scope :ranked_by_comments, -> { order("comments_count DESC") }
+ scope :ranked_by_comments, -> { order(arel_attribute(:comments_count).desc) }
- scope :limit_by, lambda {|l| limit(l) }
+ scope :limit_by, lambda { |l| limit(l) }
+ scope :locked, -> { lock }
belongs_to :author
+ belongs_to :readonly_author, -> { readonly }, class_name: "Author", foreign_key: :author_id
- belongs_to :author_with_posts, -> { includes(:posts) }, :class_name => "Author", :foreign_key => :author_id
- belongs_to :author_with_address, -> { includes(:author_address) }, :class_name => "Author", :foreign_key => :author_id
+ belongs_to :author_with_posts, -> { includes(:posts) }, class_name: "Author", foreign_key: :author_id
+ belongs_to :author_with_address, -> { includes(:author_address) }, class_name: "Author", foreign_key: :author_id
def first_comment
super.body
end
- has_one :first_comment, -> { order('id ASC') }, :class_name => 'Comment'
- has_one :last_comment, -> { order('id desc') }, :class_name => 'Comment'
+ has_one :first_comment, -> { order("id ASC") }, class_name: "Comment"
+ has_one :last_comment, -> { order("id desc") }, class_name: "Comment"
- scope :with_special_comments, -> { joins(:comments).where(:comments => {:type => 'SpecialComment'}) }
- scope :with_very_special_comments, -> { joins(:comments).where(:comments => {:type => 'VerySpecialComment'}) }
- scope :with_post, ->(post_id) { joins(:comments).where(:comments => { :post_id => post_id }) }
+ scope :with_special_comments, -> { joins(:comments).where(comments: { type: "SpecialComment" }) }
+ scope :with_very_special_comments, -> { joins(:comments).where(comments: { type: "VerySpecialComment" }) }
+ scope :with_post, ->(post_id) { joins(:comments).where(comments: { post_id: post_id }) }
scope :with_comments, -> { preload(:comments) }
scope :with_tags, -> { preload(:taggings) }
@@ -46,7 +50,7 @@ class Post < ActiveRecord::Base
scope :typographically_interesting, -> { containing_the_letter_a.or(titled_with_an_apostrophe) }
- has_many :comments do
+ has_many :comments do
def find_most_recent
order("id DESC").first
end
@@ -58,6 +62,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
@@ -68,92 +76,96 @@ class Post < ActiveRecord::Base
has_many :comments_with_extend_2, extend: [NamedExtension, NamedExtension2], class_name: "Comment", foreign_key: "post_id"
- has_many :author_favorites, :through => :author
- has_many :author_categorizations, :through => :author, :source => :categorizations
- has_many :author_addresses, :through => :author
+ has_many :author_favorites, through: :author
+ has_many :author_categorizations, through: :author, source: :categorizations
+ has_many :author_addresses, through: :author
has_many :author_address_extra_with_address,
through: :author_with_address,
source: :author_address_extra
has_one :very_special_comment
- has_one :very_special_comment_with_post, -> { includes(:post) }, :class_name => "VerySpecialComment"
- has_one :very_special_comment_with_post_with_joins, -> { joins(:post).order('posts.id') }, class_name: "VerySpecialComment"
+ has_one :very_special_comment_with_post, -> { includes(:post) }, class_name: "VerySpecialComment"
+ has_one :very_special_comment_with_post_with_joins, -> { joins(:post).order("posts.id") }, class_name: "VerySpecialComment"
has_many :special_comments
- has_many :nonexistent_comments, -> { where 'comments.id < 0' }, :class_name => 'Comment'
+ has_many :nonexistent_comments, -> { where "comments.id < 0" }, class_name: "Comment"
- has_many :special_comments_ratings, :through => :special_comments, :source => :ratings
- has_many :special_comments_ratings_taggings, :through => :special_comments_ratings, :source => :taggings
+ has_many :special_comments_ratings, through: :special_comments, source: :ratings
+ has_many :special_comments_ratings_taggings, through: :special_comments_ratings, source: :taggings
- has_many :category_posts, :class_name => 'CategoryPost'
+ has_many :category_posts, class_name: "CategoryPost"
has_many :scategories, through: :category_posts, source: :category
has_and_belongs_to_many :categories
- has_and_belongs_to_many :special_categories, :join_table => "categories_posts", :association_foreign_key => 'category_id'
+ has_and_belongs_to_many :special_categories, join_table: "categories_posts", association_foreign_key: "category_id"
- has_many :taggings, :as => :taggable, :counter_cache => :tags_count
- has_many :tags, :through => :taggings do
+ has_many :taggings, as: :taggable, counter_cache: :tags_count
+ has_many :tags, through: :taggings do
def add_joins_and_select
- select('tags.*, authors.id as author_id')
- .joins('left outer join posts on taggings.taggable_id = posts.id left outer join authors on posts.author_id = authors.id')
+ select("tags.*, authors.id as author_id")
+ .joins("left outer join posts on taggings.taggable_id = posts.id left outer join authors on posts.author_id = authors.id")
.to_a
end
end
- has_many :taggings_with_delete_all, :class_name => 'Tagging', :as => :taggable, :dependent => :delete_all
- has_many :taggings_with_destroy, :class_name => 'Tagging', :as => :taggable, :dependent => :destroy
+ has_many :indestructible_taggings, as: :taggable, counter_cache: :indestructible_tags_count
+ has_many :indestructible_tags, through: :indestructible_taggings, source: :tag
+
+ has_many :taggings_with_delete_all, class_name: "Tagging", as: :taggable, dependent: :delete_all, counter_cache: :taggings_with_delete_all_count
+ has_many :taggings_with_destroy, class_name: "Tagging", as: :taggable, dependent: :destroy, counter_cache: :taggings_with_destroy_count
- has_many :tags_with_destroy, :through => :taggings, :source => :tag, :dependent => :destroy
- has_many :tags_with_nullify, :through => :taggings, :source => :tag, :dependent => :nullify
+ has_many :tags_with_destroy, through: :taggings, source: :tag, dependent: :destroy, counter_cache: :tags_with_destroy_count
+ has_many :tags_with_nullify, through: :taggings, source: :tag, dependent: :nullify, counter_cache: :tags_with_nullify_count
- has_many :misc_tags, -> { where :tags => { :name => 'Misc' } }, :through => :taggings, :source => :tag
- has_many :funky_tags, :through => :taggings, :source => :tag
- has_many :super_tags, :through => :taggings
- has_many :tags_with_primary_key, :through => :taggings, :source => :tag_with_primary_key
- has_one :tagging, :as => :taggable
+ has_many :misc_tags, -> { where tags: { name: "Misc" } }, through: :taggings, source: :tag
+ has_many :funky_tags, through: :taggings, source: :tag
+ has_many :super_tags, through: :taggings
+ has_many :ordered_tags, through: :taggings
+ has_many :tags_with_primary_key, through: :taggings, source: :tag_with_primary_key
+ has_one :tagging, as: :taggable
- has_many :first_taggings, -> { where :taggings => { :comment => 'first' } }, :as => :taggable, :class_name => 'Tagging'
- has_many :first_blue_tags, -> { where :tags => { :name => 'Blue' } }, :through => :first_taggings, :source => :tag
+ has_many :first_taggings, -> { where taggings: { comment: "first" } }, as: :taggable, class_name: "Tagging"
+ has_many :first_blue_tags, -> { where tags: { name: "Blue" } }, through: :first_taggings, source: :tag
- has_many :first_blue_tags_2, -> { where :taggings => { :comment => 'first' } }, :through => :taggings, :source => :blue_tag
+ has_many :first_blue_tags_2, -> { where taggings: { comment: "first" } }, through: :taggings, source: :blue_tag
- has_many :invalid_taggings, -> { where 'taggings.id < 0' }, :as => :taggable, :class_name => "Tagging"
- has_many :invalid_tags, :through => :invalid_taggings, :source => :tag
+ has_many :invalid_taggings, -> { where "taggings.id < 0" }, as: :taggable, class_name: "Tagging"
+ has_many :invalid_tags, through: :invalid_taggings, source: :tag
- has_many :categorizations, :foreign_key => :category_id
- has_many :authors, :through => :categorizations
+ has_many :categorizations, foreign_key: :category_id
+ has_many :authors, through: :categorizations
- has_many :categorizations_using_author_id, :primary_key => :author_id, :foreign_key => :post_id, :class_name => 'Categorization'
- has_many :authors_using_author_id, :through => :categorizations_using_author_id, :source => :author
+ has_many :categorizations_using_author_id, primary_key: :author_id, foreign_key: :post_id, class_name: "Categorization"
+ has_many :authors_using_author_id, through: :categorizations_using_author_id, source: :author
- has_many :taggings_using_author_id, :primary_key => :author_id, :as => :taggable, :class_name => 'Tagging'
- has_many :tags_using_author_id, :through => :taggings_using_author_id, :source => :tag
+ has_many :taggings_using_author_id, primary_key: :author_id, as: :taggable, class_name: "Tagging"
+ has_many :tags_using_author_id, through: :taggings_using_author_id, source: :tag
- has_many :images, :as => :imageable, :foreign_key => :imageable_identifier, :foreign_type => :imageable_class
- has_one :main_image, :as => :imageable, :foreign_key => :imageable_identifier, :foreign_type => :imageable_class, :class_name => 'Image'
+ has_many :images, as: :imageable, foreign_key: :imageable_identifier, foreign_type: :imageable_class
+ has_one :main_image, as: :imageable, foreign_key: :imageable_identifier, foreign_type: :imageable_class, class_name: "Image"
- has_many :standard_categorizations, :class_name => 'Categorization', :foreign_key => :post_id
- has_many :author_using_custom_pk, :through => :standard_categorizations
- has_many :authors_using_custom_pk, :through => :standard_categorizations
- has_many :named_categories, :through => :standard_categorizations
+ has_many :standard_categorizations, class_name: "Categorization", foreign_key: :post_id
+ has_many :author_using_custom_pk, through: :standard_categorizations
+ has_many :authors_using_custom_pk, through: :standard_categorizations
+ has_many :named_categories, through: :standard_categorizations
has_many :readers
has_many :secure_readers
- has_many :readers_with_person, -> { includes(:person) }, :class_name => "Reader"
- has_many :people, :through => :readers
- has_many :single_people, :through => :readers
- has_many :people_with_callbacks, :source=>:person, :through => :readers,
- :before_add => lambda {|owner, reader| log(:added, :before, reader.first_name) },
- :after_add => lambda {|owner, reader| log(:added, :after, reader.first_name) },
- :before_remove => lambda {|owner, reader| log(:removed, :before, reader.first_name) },
- :after_remove => lambda {|owner, reader| log(:removed, :after, reader.first_name) }
- has_many :skimmers, -> { where :skimmer => true }, :class_name => 'Reader'
- has_many :impatient_people, :through => :skimmers, :source => :person
+ has_many :readers_with_person, -> { includes(:person) }, class_name: "Reader"
+ has_many :people, through: :readers
+ has_many :single_people, through: :readers
+ has_many :people_with_callbacks, source: :person, through: :readers,
+ before_add: lambda { |owner, reader| log(:added, :before, reader.first_name) },
+ after_add: lambda { |owner, reader| log(:added, :after, reader.first_name) },
+ before_remove: lambda { |owner, reader| log(:removed, :before, reader.first_name) },
+ after_remove: lambda { |owner, reader| log(:removed, :after, reader.first_name) }
+ has_many :skimmers, -> { where skimmer: true }, class_name: "Reader"
+ has_many :impatient_people, through: :skimmers, source: :person
has_many :lazy_readers
- has_many :lazy_readers_skimmers_or_not, -> { where(skimmer: [ true, false ]) }, :class_name => 'LazyReader'
+ has_many :lazy_readers_skimmers_or_not, -> { where(skimmer: [ true, false ]) }, class_name: "LazyReader"
- has_many :lazy_people, :through => :lazy_readers, :source => :person
- has_many :lazy_readers_unscope_skimmers, -> { skimmers_or_not }, :class_name => 'LazyReader'
- has_many :lazy_people_unscope_skimmers, :through => :lazy_readers_unscope_skimmers, :source => :person
+ has_many :lazy_people, through: :lazy_readers, source: :person
+ has_many :lazy_readers_unscope_skimmers, -> { skimmers_or_not }, class_name: "LazyReader"
+ has_many :lazy_people_unscope_skimmers, through: :lazy_readers_unscope_skimmers, source: :person
def self.top(limit)
ranked_by_comments.limit_by(limit)
@@ -167,7 +179,7 @@ class Post < ActiveRecord::Base
@log = []
end
- def self.log(message=nil, side=nil, new_record=nil)
+ def self.log(message = nil, side = nil, new_record = nil)
return @log if message.nil?
@log << [message, side, new_record]
end
@@ -176,61 +188,77 @@ end
class SpecialPost < Post; end
class StiPost < Post
+ has_one :special_comment, class_name: "SpecialComment"
+end
+
+class AbstractStiPost < Post
self.abstract_class = true
- has_one :special_comment, :class_name => "SpecialComment"
end
class SubStiPost < StiPost
self.table_name = Post.table_name
end
+class SubAbstractStiPost < AbstractStiPost; end
+
class FirstPost < ActiveRecord::Base
- self.table_name = 'posts'
- default_scope { where(:id => 1) }
+ self.inheritance_column = :disabled
+ self.table_name = "posts"
+ default_scope { where(id: 1) }
+
+ has_many :comments, foreign_key: :post_id
+ has_one :comment, foreign_key: :post_id
+end
- has_many :comments, :foreign_key => :post_id
- has_one :comment, :foreign_key => :post_id
+class TaggedPost < Post
+ has_many :taggings, -> { rewhere(taggable_type: "TaggedPost") }, as: :taggable
+ has_many :tags, through: :taggings
end
class PostWithDefaultInclude < ActiveRecord::Base
- self.table_name = 'posts'
+ self.inheritance_column = :disabled
+ self.table_name = "posts"
default_scope { includes(:comments) }
- has_many :comments, :foreign_key => :post_id
+ has_many :comments, foreign_key: :post_id
end
class PostWithSpecialCategorization < Post
- has_many :categorizations, :foreign_key => :post_id
- default_scope { where(:type => 'PostWithSpecialCategorization').joins(:categorizations).where(:categorizations => { :special => true }) }
+ has_many :categorizations, foreign_key: :post_id
+ default_scope { where(type: "PostWithSpecialCategorization").joins(:categorizations).where(categorizations: { special: true }) }
end
class PostWithDefaultScope < ActiveRecord::Base
- self.table_name = 'posts'
+ self.inheritance_column = :disabled
+ self.table_name = "posts"
default_scope { order(:title) }
end
class PostWithPreloadDefaultScope < ActiveRecord::Base
- self.table_name = 'posts'
+ self.table_name = "posts"
- has_many :readers, foreign_key: 'post_id'
+ has_many :readers, foreign_key: "post_id"
default_scope { preload(:readers) }
end
class PostWithIncludesDefaultScope < ActiveRecord::Base
- self.table_name = 'posts'
+ self.table_name = "posts"
- has_many :readers, foreign_key: 'post_id'
+ has_many :readers, foreign_key: "post_id"
default_scope { includes(:readers) }
end
class SpecialPostWithDefaultScope < ActiveRecord::Base
- self.table_name = 'posts'
- default_scope { where(:id => [1, 5,6]) }
+ self.inheritance_column = :disabled
+ self.table_name = "posts"
+ default_scope { where(id: [1, 5, 6]) }
+ scope :unscoped_all, -> { unscoped { all } }
end
class PostThatLoadsCommentsInAnAfterSaveHook < ActiveRecord::Base
- self.table_name = 'posts'
+ self.inheritance_column = :disabled
+ self.table_name = "posts"
has_many :comments, class_name: "CommentThatAutomaticallyAltersPostBody", foreign_key: :post_id
after_save do |post|
@@ -239,7 +267,8 @@ class PostThatLoadsCommentsInAnAfterSaveHook < ActiveRecord::Base
end
class PostWithAfterCreateCallback < ActiveRecord::Base
- self.table_name = 'posts'
+ self.inheritance_column = :disabled
+ self.table_name = "posts"
has_many :comments, foreign_key: :post_id
after_create do |post|
@@ -248,7 +277,8 @@ class PostWithAfterCreateCallback < ActiveRecord::Base
end
class PostWithCommentWithDefaultScopeReferencesAssociation < ActiveRecord::Base
- self.table_name = 'posts'
+ self.inheritance_column = :disabled
+ self.table_name = "posts"
has_many :comment_with_default_scope_references_associations, foreign_key: :post_id
has_one :first_comment, class_name: "CommentWithDefaultScopeReferencesAssociation", foreign_key: :post_id
end
@@ -256,3 +286,58 @@ end
class SerializedPost < ActiveRecord::Base
serialize :title
end
+
+class ConditionalStiPost < Post
+ default_scope { where(title: "Untitled") }
+end
+
+class SubConditionalStiPost < ConditionalStiPost
+end
+
+class FakeKlass
+ extend ActiveRecord::Delegation::DelegateCache
+
+ class << self
+ def connection
+ Post.connection
+ end
+
+ def table_name
+ "posts"
+ end
+
+ def attribute_alias?(name)
+ false
+ end
+
+ def sanitize_sql(sql)
+ sql
+ end
+
+ def sanitize_sql_for_order(sql)
+ sql
+ end
+
+ def arel_attribute(name, table)
+ table[name]
+ end
+
+ def disallow_raw_sql!(*args)
+ # noop
+ end
+
+ def arel_table
+ Post.arel_table
+ end
+
+ def predicate_builder
+ Post.predicate_builder
+ end
+
+ def base_class?
+ true
+ end
+ end
+
+ inherited self
+end
diff --git a/activerecord/test/models/price_estimate.rb b/activerecord/test/models/price_estimate.rb
index d09e2a88a3..669d0991f7 100644
--- a/activerecord/test/models/price_estimate.rb
+++ b/activerecord/test/models/price_estimate.rb
@@ -1,4 +1,14 @@
+# frozen_string_literal: true
+
class PriceEstimate < ActiveRecord::Base
- belongs_to :estimate_of, :polymorphic => true
+ include ActiveSupport::NumberHelper
+
+ belongs_to :estimate_of, polymorphic: true
belongs_to :thing, polymorphic: true
+
+ validates_numericality_of :price
+
+ def price
+ number_to_currency super
+ end
end
diff --git a/activerecord/test/models/professor.rb b/activerecord/test/models/professor.rb
new file mode 100644
index 0000000000..abc23f40ff
--- /dev/null
+++ b/activerecord/test/models/professor.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require_dependency "models/arunit2_model"
+
+class Professor < ARUnit2Model
+ has_and_belongs_to_many :courses
+end
diff --git a/activerecord/test/models/project.rb b/activerecord/test/models/project.rb
index 7f42a4b1f8..846cef625b 100644
--- a/activerecord/test/models/project.rb
+++ b/activerecord/test/models/project.rb
@@ -1,16 +1,29 @@
+# frozen_string_literal: true
+
class Project < ActiveRecord::Base
- has_and_belongs_to_many :developers, -> { distinct.order 'developers.name desc, developers.id desc' }
- has_and_belongs_to_many :readonly_developers, -> { readonly }, :class_name => "Developer"
- has_and_belongs_to_many :non_unique_developers, -> { order 'developers.name desc, developers.id desc' }, :class_name => 'Developer'
- has_and_belongs_to_many :limited_developers, -> { limit 1 }, :class_name => "Developer"
- has_and_belongs_to_many :developers_named_david, -> { where("name = 'David'").distinct }, :class_name => "Developer"
- has_and_belongs_to_many :developers_named_david_with_hash_conditions, -> { where(:name => 'David').distinct }, :class_name => "Developer"
- has_and_belongs_to_many :salaried_developers, -> { where "salary > 0" }, :class_name => "Developer"
- has_and_belongs_to_many :developers_with_callbacks, :class_name => "Developer", :before_add => Proc.new {|o, r| o.developers_log << "before_adding#{r.id || '<new>'}"},
- :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"
+ belongs_to :mentor
+ has_and_belongs_to_many :developers, -> { distinct.order "developers.name desc, developers.id desc" }
+ has_and_belongs_to_many :readonly_developers, -> { readonly }, class_name: "Developer"
+ has_and_belongs_to_many :non_unique_developers, -> { order "developers.name desc, developers.id desc" }, class_name: "Developer"
+ has_and_belongs_to_many :limited_developers, -> { limit 1 }, class_name: "Developer"
+ has_and_belongs_to_many :developers_named_david, -> { where("name = 'David'").distinct }, class_name: "Developer"
+ has_and_belongs_to_many :developers_named_david_with_hash_conditions, -> { where(name: "David").distinct }, class_name: "Developer"
+ has_and_belongs_to_many :salaried_developers, -> { where "salary > 0" }, class_name: "Developer"
+ has_and_belongs_to_many :developers_with_callbacks, class_name: "Developer", before_add: Proc.new { |o, r| o.developers_log << "before_adding#{r.id || '<new>'}" },
+ 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_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
+
+ begin
+ previous_value, ActiveRecord::Base.belongs_to_required_by_default =
+ ActiveRecord::Base.belongs_to_required_by_default, true
+ has_and_belongs_to_many :developers_required_by_default, class_name: "Developer"
+ ensure
+ ActiveRecord::Base.belongs_to_required_by_default = previous_value
+ end
attr_accessor :developers_log
after_initialize :set_developers_log
diff --git a/activerecord/test/models/publisher.rb b/activerecord/test/models/publisher.rb
index 0d4a7f9235..53677197c4 100644
--- a/activerecord/test/models/publisher.rb
+++ b/activerecord/test/models/publisher.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
module Publisher
end
diff --git a/activerecord/test/models/publisher/article.rb b/activerecord/test/models/publisher/article.rb
index d73a8eb936..355c22dcc5 100644
--- a/activerecord/test/models/publisher/article.rb
+++ b/activerecord/test/models/publisher/article.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Publisher::Article < ActiveRecord::Base
has_and_belongs_to_many :magazines
has_and_belongs_to_many :tags
diff --git a/activerecord/test/models/publisher/magazine.rb b/activerecord/test/models/publisher/magazine.rb
index 82e1a14008..425ede8df2 100644
--- a/activerecord/test/models/publisher/magazine.rb
+++ b/activerecord/test/models/publisher/magazine.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Publisher::Magazine < ActiveRecord::Base
has_and_belongs_to_many :articles
end
diff --git a/activerecord/test/models/randomly_named_c1.rb b/activerecord/test/models/randomly_named_c1.rb
index d4be1e13b4..f90a7b9336 100644
--- a/activerecord/test/models/randomly_named_c1.rb
+++ b/activerecord/test/models/randomly_named_c1.rb
@@ -1,3 +1,5 @@
-class ClassNameThatDoesNotFollowCONVENTIONS < ActiveRecord::Base
- self.table_name = :randomly_named_table1
-end
+# frozen_string_literal: true
+
+class ClassNameThatDoesNotFollowCONVENTIONS < ActiveRecord::Base
+ self.table_name = :randomly_named_table1
+end
diff --git a/activerecord/test/models/rating.rb b/activerecord/test/models/rating.rb
index 25a52c4ad7..cf06bc6931 100644
--- a/activerecord/test/models/rating.rb
+++ b/activerecord/test/models/rating.rb
@@ -1,4 +1,6 @@
+# frozen_string_literal: true
+
class Rating < ActiveRecord::Base
belongs_to :comment
- has_many :taggings, :as => :taggable
+ has_many :taggings, as: :taggable
end
diff --git a/activerecord/test/models/reader.rb b/activerecord/test/models/reader.rb
index 91afc1898c..d25627e430 100644
--- a/activerecord/test/models/reader.rb
+++ b/activerecord/test/models/reader.rb
@@ -1,22 +1,24 @@
+# frozen_string_literal: true
+
class Reader < ActiveRecord::Base
belongs_to :post
- belongs_to :person, :inverse_of => :readers
- belongs_to :single_person, :class_name => 'Person', :foreign_key => :person_id, :inverse_of => :reader
+ belongs_to :person, inverse_of: :readers
+ belongs_to :single_person, class_name: "Person", foreign_key: :person_id, inverse_of: :reader
belongs_to :first_post, -> { where(id: [2, 3]) }
end
class SecureReader < ActiveRecord::Base
self.table_name = "readers"
- belongs_to :secure_post, :class_name => "Post", :foreign_key => "post_id"
- belongs_to :secure_person, :inverse_of => :secure_readers, :class_name => "Person", :foreign_key => "person_id"
+ belongs_to :secure_post, class_name: "Post", foreign_key: "post_id"
+ belongs_to :secure_person, inverse_of: :secure_readers, class_name: "Person", foreign_key: "person_id"
end
class LazyReader < ActiveRecord::Base
self.table_name = "readers"
default_scope -> { where(skimmer: true) }
- scope :skimmers_or_not, -> { unscope(:where => :skimmer) }
+ scope :skimmers_or_not, -> { unscope(where: :skimmer) }
belongs_to :post
belongs_to :person
diff --git a/activerecord/test/models/recipe.rb b/activerecord/test/models/recipe.rb
index c387230603..e53f5c8fb1 100644
--- a/activerecord/test/models/recipe.rb
+++ b/activerecord/test/models/recipe.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Recipe < ActiveRecord::Base
belongs_to :chef
end
diff --git a/activerecord/test/models/record.rb b/activerecord/test/models/record.rb
index f77ac9fc03..63143e296a 100644
--- a/activerecord/test/models/record.rb
+++ b/activerecord/test/models/record.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
class Record < ActiveRecord::Base
end
diff --git a/activerecord/test/models/reference.rb b/activerecord/test/models/reference.rb
index c2f9068f57..2a7a1e3b77 100644
--- a/activerecord/test/models/reference.rb
+++ b/activerecord/test/models/reference.rb
@@ -1,8 +1,10 @@
+# frozen_string_literal: true
+
class Reference < ActiveRecord::Base
belongs_to :person
belongs_to :job
- has_many :agents_posts_authors, :through => :person
+ has_many :agents_posts_authors, through: :person
class << self; attr_accessor :make_comments; end
self.make_comments = false
@@ -17,6 +19,6 @@ class Reference < ActiveRecord::Base
end
class BadReference < ActiveRecord::Base
- self.table_name = 'references'
- default_scope { where(:favourite => false) }
+ self.table_name = "references"
+ default_scope { where(favourite: false) }
end
diff --git a/activerecord/test/models/reply.rb b/activerecord/test/models/reply.rb
index 3e82e55d89..0807bcf875 100644
--- a/activerecord/test/models/reply.rb
+++ b/activerecord/test/models/reply.rb
@@ -1,27 +1,35 @@
-require 'models/topic'
+# frozen_string_literal: true
+
+require "models/topic"
class Reply < Topic
- belongs_to :topic, :foreign_key => "parent_id", :counter_cache => true
- belongs_to :topic_with_primary_key, :class_name => "Topic", :primary_key => "title", :foreign_key => "parent_title", :counter_cache => "replies_count"
- has_many :replies, :class_name => "SillyReply", :dependent => :destroy, :foreign_key => "parent_id"
+ belongs_to :topic, foreign_key: "parent_id", counter_cache: true
+ belongs_to :topic_with_primary_key, class_name: "Topic", primary_key: "title", foreign_key: "parent_title", counter_cache: "replies_count", touch: true
+ has_many :replies, class_name: "SillyReply", dependent: :destroy, foreign_key: "parent_id"
+ has_many :silly_unique_replies, dependent: :destroy, foreign_key: "parent_id"
+end
+
+class SillyReply < Topic
+ belongs_to :reply, foreign_key: "parent_id", counter_cache: :replies_count
end
class UniqueReply < Reply
- belongs_to :topic, :foreign_key => 'parent_id', :counter_cache => true
- validates_uniqueness_of :content, :scope => 'parent_id'
+ belongs_to :topic, foreign_key: "parent_id", counter_cache: true
+ validates_uniqueness_of :content, scope: "parent_id"
end
class SillyUniqueReply < UniqueReply
+ validates :content, uniqueness: true
end
class WrongReply < Reply
validate :errors_on_empty_content
- validate :title_is_wrong_create, :on => :create
+ validate :title_is_wrong_create, on: :create
validate :check_empty_title
- validate :check_content_mismatch, :on => :create
- validate :check_wrong_update, :on => :update
- validate :check_author_name_is_secret, :on => :special_case
+ validate :check_content_mismatch, on: :create
+ validate :check_wrong_update, on: :update
+ validate :check_author_name_is_secret, on: :special_case
def check_empty_title
errors[:title] << "Empty" unless attribute_present?("title")
@@ -50,12 +58,8 @@ class WrongReply < Reply
end
end
-class SillyReply < Reply
- belongs_to :reply, :foreign_key => "parent_id", :counter_cache => :replies_count
-end
-
module Web
class Reply < Web::Topic
- belongs_to :topic, :foreign_key => "parent_id", :counter_cache => true, :class_name => 'Web::Topic'
+ belongs_to :topic, foreign_key: "parent_id", counter_cache: true, class_name: "Web::Topic"
end
end
diff --git a/activerecord/test/models/ship.rb b/activerecord/test/models/ship.rb
index 312caef604..7973219a79 100644
--- a/activerecord/test/models/ship.rb
+++ b/activerecord/test/models/ship.rb
@@ -1,26 +1,41 @@
+# frozen_string_literal: true
+
class Ship < ActiveRecord::Base
self.record_timestamps = false
belongs_to :pirate
- belongs_to :update_only_pirate, :class_name => 'Pirate'
- has_many :parts, :class_name => 'ShipPart'
+ belongs_to :update_only_pirate, class_name: "Pirate"
+ belongs_to :developer, dependent: :destroy
+ has_many :parts, class_name: "ShipPart"
has_many :treasures
- accepts_nested_attributes_for :parts, :allow_destroy => true
- accepts_nested_attributes_for :pirate, :allow_destroy => true, :reject_if => proc(&:empty?)
- accepts_nested_attributes_for :update_only_pirate, :update_only => true
+ accepts_nested_attributes_for :parts, allow_destroy: true
+ accepts_nested_attributes_for :pirate, allow_destroy: true, reject_if: proc(&:empty?)
+ accepts_nested_attributes_for :update_only_pirate, update_only: true
validates_presence_of :name
attr_accessor :cancel_save_from_callback
- before_save :cancel_save_callback_method, :if => :cancel_save_from_callback
+ before_save :cancel_save_callback_method, if: :cancel_save_from_callback
def cancel_save_callback_method
throw(:abort)
end
end
+class ShipWithoutNestedAttributes < ActiveRecord::Base
+ self.table_name = "ships"
+ has_many :prisoners, inverse_of: :ship, foreign_key: :ship_id
+ has_many :parts, class_name: "ShipPart", foreign_key: :ship_id
+
+ validates :name, presence: true
+end
+
+class Prisoner < ActiveRecord::Base
+ belongs_to :ship, autosave: true, class_name: "ShipWithoutNestedAttributes", inverse_of: :prisoners
+end
+
class FamousShip < ActiveRecord::Base
- self.table_name = 'ships'
+ self.table_name = "ships"
belongs_to :famous_pirate
validates_presence_of :name, on: :conference
end
diff --git a/activerecord/test/models/ship_part.rb b/activerecord/test/models/ship_part.rb
index 05c65f8a4a..f6d7a8ae5e 100644
--- a/activerecord/test/models/ship_part.rb
+++ b/activerecord/test/models/ship_part.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
class ShipPart < ActiveRecord::Base
belongs_to :ship
- has_many :trinkets, :class_name => "Treasure", :as => :looter
- accepts_nested_attributes_for :trinkets, :allow_destroy => true
+ has_many :trinkets, class_name: "Treasure", as: :looter
+ accepts_nested_attributes_for :trinkets, allow_destroy: true
accepts_nested_attributes_for :ship
validates_presence_of :name
diff --git a/activerecord/test/models/shop.rb b/activerecord/test/models/shop.rb
index 607a0a5b41..92afe70b92 100644
--- a/activerecord/test/models/shop.rb
+++ b/activerecord/test/models/shop.rb
@@ -1,10 +1,12 @@
+# frozen_string_literal: true
+
module Shop
class Collection < ActiveRecord::Base
- has_many :products, :dependent => :nullify
+ has_many :products, dependent: :nullify
end
class Product < ActiveRecord::Base
- has_many :variants, :dependent => :delete_all
+ has_many :variants, dependent: :delete_all
belongs_to :type
class Type < ActiveRecord::Base
diff --git a/activerecord/test/models/shop_account.rb b/activerecord/test/models/shop_account.rb
new file mode 100644
index 0000000000..97fb058331
--- /dev/null
+++ b/activerecord/test/models/shop_account.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+class ShopAccount < ActiveRecord::Base
+ belongs_to :customer
+ belongs_to :customer_carrier
+
+ has_one :carrier, through: :customer_carrier
+end
diff --git a/activerecord/test/models/speedometer.rb b/activerecord/test/models/speedometer.rb
index 497c3aba9a..e456907a22 100644
--- a/activerecord/test/models/speedometer.rb
+++ b/activerecord/test/models/speedometer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Speedometer < ActiveRecord::Base
self.primary_key = :speedometer_id
belongs_to :dashboard
diff --git a/activerecord/test/models/sponsor.rb b/activerecord/test/models/sponsor.rb
index ec3dcf8a97..18ff103ffe 100644
--- a/activerecord/test/models/sponsor.rb
+++ b/activerecord/test/models/sponsor.rb
@@ -1,7 +1,10 @@
+# frozen_string_literal: true
+
class Sponsor < ActiveRecord::Base
- belongs_to :sponsor_club, :class_name => "Club", :foreign_key => "club_id"
- belongs_to :sponsorable, :polymorphic => true
- belongs_to :thing, :polymorphic => true, :foreign_type => :sponsorable_type, :foreign_key => :sponsorable_id
- belongs_to :sponsorable_with_conditions, -> { where :name => 'Ernie'}, :polymorphic => true,
- :foreign_type => 'sponsorable_type', :foreign_key => 'sponsorable_id'
+ belongs_to :sponsor_club, class_name: "Club", foreign_key: "club_id"
+ belongs_to :sponsorable, polymorphic: true
+ belongs_to :sponsor, polymorphic: true
+ belongs_to :thing, polymorphic: true, foreign_type: :sponsorable_type, foreign_key: :sponsorable_id
+ belongs_to :sponsorable_with_conditions, -> { where name: "Ernie" }, polymorphic: true,
+ foreign_type: "sponsorable_type", foreign_key: "sponsorable_id"
end
diff --git a/activerecord/test/models/string_key_object.rb b/activerecord/test/models/string_key_object.rb
index f084ec1bdc..473c145f4c 100644
--- a/activerecord/test/models/string_key_object.rb
+++ b/activerecord/test/models/string_key_object.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class StringKeyObject < ActiveRecord::Base
self.primary_key = :id
end
diff --git a/activerecord/test/models/student.rb b/activerecord/test/models/student.rb
index 28a0b6c99b..e750798f74 100644
--- a/activerecord/test/models/student.rb
+++ b/activerecord/test/models/student.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Student < ActiveRecord::Base
has_and_belongs_to_many :lessons
belongs_to :college
diff --git a/activerecord/test/models/subject.rb b/activerecord/test/models/subject.rb
deleted file mode 100644
index 8e28f8b86b..0000000000
--- a/activerecord/test/models/subject.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# used for OracleSynonymTest, see test/synonym_test_oracle.rb
-#
-class Subject < ActiveRecord::Base
-
- # added initialization of author_email_address in the same way as in Topic class
- # as otherwise synonym test was failing
- after_initialize :set_email_address
-
- protected
- def set_email_address
- unless self.persisted?
- self.author_email_address = 'test@test.com'
- end
- end
-
-end
diff --git a/activerecord/test/models/subscriber.rb b/activerecord/test/models/subscriber.rb
index 76e85a0cd3..b21969ca2d 100644
--- a/activerecord/test/models/subscriber.rb
+++ b/activerecord/test/models/subscriber.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
class Subscriber < ActiveRecord::Base
- self.primary_key = 'nick'
+ self.primary_key = "nick"
has_many :subscriptions
- has_many :books, :through => :subscriptions
+ has_many :books, through: :subscriptions
end
class SpecialSubscriber < Subscriber
diff --git a/activerecord/test/models/subscription.rb b/activerecord/test/models/subscription.rb
index bcac4738a3..d1d5d21621 100644
--- a/activerecord/test/models/subscription.rb
+++ b/activerecord/test/models/subscription.rb
@@ -1,4 +1,6 @@
+# frozen_string_literal: true
+
class Subscription < ActiveRecord::Base
- belongs_to :subscriber, :counter_cache => :books_count
+ belongs_to :subscriber, counter_cache: :books_count
belongs_to :book
end
diff --git a/activerecord/test/models/tag.rb b/activerecord/test/models/tag.rb
index 80d4725f7e..c1a8890a8a 100644
--- a/activerecord/test/models/tag.rb
+++ b/activerecord/test/models/tag.rb
@@ -1,7 +1,16 @@
+# frozen_string_literal: true
+
class Tag < ActiveRecord::Base
has_many :taggings
- has_many :taggables, :through => :taggings
+ has_many :taggables, through: :taggings
has_one :tagging
- has_many :tagged_posts, :through => :taggings, :source => 'taggable', :source_type => 'Post'
+ has_many :tagged_posts, through: :taggings, source: "taggable", source_type: "Post"
+end
+
+class OrderedTag < Tag
+ self.table_name = "tags"
+
+ has_many :ordered_taggings, -> { order("taggings.id DESC") }, foreign_key: "tag_id", class_name: "Tagging"
+ has_many :tagged_posts, through: :ordered_taggings, source: "taggable", source_type: "Post"
end
diff --git a/activerecord/test/models/tagging.rb b/activerecord/test/models/tagging.rb
index a6c05da26a..6d4230f6f4 100644
--- a/activerecord/test/models/tagging.rb
+++ b/activerecord/test/models/tagging.rb
@@ -1,13 +1,20 @@
+# frozen_string_literal: true
+
# test that attr_readonly isn't called on the :taggable polymorphic association
module Taggable
end
class Tagging < ActiveRecord::Base
belongs_to :tag, -> { includes(:tagging) }
- belongs_to :super_tag, :class_name => 'Tag', :foreign_key => 'super_tag_id'
- belongs_to :invalid_tag, :class_name => 'Tag', :foreign_key => 'tag_id'
- belongs_to :blue_tag, -> { where :tags => { :name => 'Blue' } }, :class_name => 'Tag', :foreign_key => :tag_id
- belongs_to :tag_with_primary_key, :class_name => 'Tag', :foreign_key => :tag_id, :primary_key => :custom_primary_key
- belongs_to :taggable, :polymorphic => true, :counter_cache => :tags_count
- has_many :things, :through => :taggable
+ belongs_to :super_tag, class_name: "Tag", foreign_key: "super_tag_id"
+ belongs_to :invalid_tag, class_name: "Tag", foreign_key: "tag_id"
+ belongs_to :ordered_tag, class_name: "OrderedTag", foreign_key: "tag_id"
+ belongs_to :blue_tag, -> { where tags: { name: "Blue" } }, class_name: "Tag", foreign_key: :tag_id
+ belongs_to :tag_with_primary_key, class_name: "Tag", foreign_key: :tag_id, primary_key: :custom_primary_key
+ belongs_to :taggable, polymorphic: true, counter_cache: :tags_count
+ has_many :things, through: :taggable
+end
+
+class IndestructibleTagging < Tagging
+ before_destroy { throw :abort }
end
diff --git a/activerecord/test/models/task.rb b/activerecord/test/models/task.rb
index e36989dd56..dabe3ce06b 100644
--- a/activerecord/test/models/task.rb
+++ b/activerecord/test/models/task.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Task < ActiveRecord::Base
def updated_at
ending
diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb
index f81ffe1d90..03430154db 100644
--- a/activerecord/test/models/topic.rb
+++ b/activerecord/test/models/topic.rb
@@ -1,20 +1,30 @@
+# frozen_string_literal: true
+
class Topic < ActiveRecord::Base
scope :base, -> { all }
scope :written_before, lambda { |time|
if time
- where 'written_on < ?', time
+ where "written_on < ?", time
end
}
- scope :approved, -> { where(:approved => true) }
- scope :rejected, -> { where(:approved => false) }
+ scope :approved, -> { where(approved: true) }
+ scope :rejected, -> { where(approved: false) }
scope :scope_with_lambda, lambda { all }
- scope :by_lifo, -> { where(:author_name => 'lifo') }
- scope :replied, -> { where 'replies_count > 0' }
+ scope :by_private_lifo, -> { where(author_name: private_lifo) }
+ scope :by_lifo, -> { where(author_name: "lifo") }
+ scope :replied, -> { where "replies_count > 0" }
- scope 'approved_as_string', -> { where(:approved => true) }
- scope :anonymous_extension, -> { all } do
+ class << self
+ private
+ def private_lifo
+ "lifo"
+ end
+ end
+
+ scope "approved_as_string", -> { where(approved: true) }
+ scope :anonymous_extension, -> { } do
def one
1
end
@@ -22,7 +32,7 @@ class Topic < ActiveRecord::Base
scope :with_object, Class.new(Struct.new(:klass)) {
def call
- klass.where(:approved => true)
+ klass.where(approved: true)
end
}.new(self)
@@ -32,23 +42,17 @@ class Topic < ActiveRecord::Base
end
end
- has_many :replies, :dependent => :destroy, :foreign_key => "parent_id"
- has_many :approved_replies, -> { approved }, class_name: 'Reply', foreign_key: "parent_id", counter_cache: 'replies_count'
+ has_many :replies, dependent: :destroy, foreign_key: "parent_id", autosave: true
+ has_many :approved_replies, -> { approved }, class_name: "Reply", foreign_key: "parent_id", counter_cache: "replies_count"
- has_many :unique_replies, :dependent => :destroy, :foreign_key => "parent_id"
- has_many :silly_unique_replies, :dependent => :destroy, :foreign_key => "parent_id"
+ has_many :unique_replies, dependent: :destroy, foreign_key: "parent_id"
+ has_many :silly_unique_replies, dependent: :destroy, foreign_key: "parent_id"
serialize :content
before_create :default_written_on
before_destroy :destroy_children
- # Explicitly define as :date column so that returned Oracle DATE values would be typecasted to Date and not Time.
- # Some tests depend on assumption that this attribute will have Date values.
- if current_adapter?(:OracleEnhancedAdapter)
- set_date_columns :last_read
- end
-
def parent
Topic.find(parent_id)
end
@@ -69,29 +73,42 @@ class Topic < ActiveRecord::Base
after_initialize :set_email_address
+ attr_accessor :change_approved_before_save
+ before_save :change_approved_callback
+
class_attribute :after_initialize_called
after_initialize do
self.class.after_initialize_called = true
end
+ attr_accessor :after_touch_called
+
+ after_initialize do
+ self.after_touch_called = 0
+ end
+
+ after_touch do
+ self.after_touch_called += 1
+ end
+
def approved=(val)
@custom_approved = val
write_attribute(:approved, val)
end
- protected
+ private
def default_written_on
self.written_on = Time.now unless attribute_present?("written_on")
end
def destroy_children
- self.class.delete_all "parent_id = #{id}"
+ self.class.where("parent_id = #{id}").delete_all
end
def set_email_address
- unless self.persisted?
- self.author_email_address = 'test@test.com'
+ unless persisted? || will_save_change_to_author_email_address?
+ self.author_email_address = "test@test.com"
end
end
@@ -100,6 +117,10 @@ class Topic < ActiveRecord::Base
def before_destroy_for_transaction; end
def after_save_for_transaction; end
def after_create_for_transaction; end
+
+ def change_approved_callback
+ self.approved = change_approved_before_save unless change_approved_before_save.nil?
+ end
end
class ImportantTopic < Topic
@@ -117,8 +138,12 @@ class BlankTopic < Topic
end
end
+class TitlePrimaryKeyTopic < Topic
+ self.primary_key = :title
+end
+
module Web
class Topic < ActiveRecord::Base
- has_many :replies, :dependent => :destroy, :foreign_key => "parent_id", :class_name => 'Web::Reply'
+ has_many :replies, dependent: :destroy, foreign_key: "parent_id", class_name: "Web::Reply"
end
end
diff --git a/activerecord/test/models/toy.rb b/activerecord/test/models/toy.rb
index ddc7048a56..4a5697eeb1 100644
--- a/activerecord/test/models/toy.rb
+++ b/activerecord/test/models/toy.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Toy < ActiveRecord::Base
self.primary_key = :toy_id
belongs_to :pet
diff --git a/activerecord/test/models/traffic_light.rb b/activerecord/test/models/traffic_light.rb
index a6b7edb882..0b88815cbd 100644
--- a/activerecord/test/models/traffic_light.rb
+++ b/activerecord/test/models/traffic_light.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class TrafficLight < ActiveRecord::Base
serialize :state, Array
serialize :long_state, Array
diff --git a/activerecord/test/models/treasure.rb b/activerecord/test/models/treasure.rb
index ffc65466d5..b51db56c37 100644
--- a/activerecord/test/models/treasure.rb
+++ b/activerecord/test/models/treasure.rb
@@ -1,10 +1,13 @@
+# frozen_string_literal: true
+
class Treasure < ActiveRecord::Base
has_and_belongs_to_many :parrots
- belongs_to :looter, :polymorphic => true
+ belongs_to :looter, polymorphic: true
+ # No counter_cache option given
belongs_to :ship
- has_many :price_estimates, :as => :estimate_of
- has_and_belongs_to_many :rich_people, join_table: 'peoples_treasures', validate: false
+ has_many :price_estimates, as: :estimate_of
+ has_and_belongs_to_many :rich_people, join_table: "peoples_treasures", validate: false
accepts_nested_attributes_for :looter
end
diff --git a/activerecord/test/models/treaty.rb b/activerecord/test/models/treaty.rb
index 41fd1350f3..b87a757d2a 100644
--- a/activerecord/test/models/treaty.rb
+++ b/activerecord/test/models/treaty.rb
@@ -1,7 +1,5 @@
-class Treaty < ActiveRecord::Base
-
- self.primary_key = :treaty_id
+# frozen_string_literal: true
+class Treaty < ActiveRecord::Base
has_and_belongs_to_many :countries
-
end
diff --git a/activerecord/test/models/tree.rb b/activerecord/test/models/tree.rb
index dc29cccc9c..77050c5fff 100644
--- a/activerecord/test/models/tree.rb
+++ b/activerecord/test/models/tree.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Tree < ActiveRecord::Base
has_many :nodes, dependent: :destroy
end
diff --git a/activerecord/test/models/tuning_peg.rb b/activerecord/test/models/tuning_peg.rb
new file mode 100644
index 0000000000..6e052e1d0f
--- /dev/null
+++ b/activerecord/test/models/tuning_peg.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+class TuningPeg < ActiveRecord::Base
+ belongs_to :guitar
+ validates_numericality_of :pitch
+end
diff --git a/activerecord/test/models/tyre.rb b/activerecord/test/models/tyre.rb
index e50a21ca68..d627026585 100644
--- a/activerecord/test/models/tyre.rb
+++ b/activerecord/test/models/tyre.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Tyre < ActiveRecord::Base
belongs_to :car
diff --git a/activerecord/test/models/user.rb b/activerecord/test/models/user.rb
index f5dc93e994..3efbc45d2a 100644
--- a/activerecord/test/models/user.rb
+++ b/activerecord/test/models/user.rb
@@ -1,6 +1,18 @@
+# frozen_string_literal: true
+
+require "models/job"
+
class User < ActiveRecord::Base
has_secure_token
has_secure_token :auth_token
+
+ has_and_belongs_to_many :jobs_pool,
+ 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/models/uuid_child.rb b/activerecord/test/models/uuid_child.rb
index a3d0962ad6..9fce361cc8 100644
--- a/activerecord/test/models/uuid_child.rb
+++ b/activerecord/test/models/uuid_child.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class UuidChild < ActiveRecord::Base
belongs_to :uuid_parent
end
diff --git a/activerecord/test/models/uuid_item.rb b/activerecord/test/models/uuid_item.rb
new file mode 100644
index 0000000000..41f68c4c18
--- /dev/null
+++ b/activerecord/test/models/uuid_item.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+class UuidItem < ActiveRecord::Base
+end
+
+class UuidValidatingItem < UuidItem
+ validates_uniqueness_of :uuid
+end
diff --git a/activerecord/test/models/uuid_parent.rb b/activerecord/test/models/uuid_parent.rb
index 5634f22d0c..05db61855e 100644
--- a/activerecord/test/models/uuid_parent.rb
+++ b/activerecord/test/models/uuid_parent.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class UuidParent < ActiveRecord::Base
has_many :uuid_children
end
diff --git a/activerecord/test/models/vegetables.rb b/activerecord/test/models/vegetables.rb
index 1f41cde3a5..cfaab08ed1 100644
--- a/activerecord/test/models/vegetables.rb
+++ b/activerecord/test/models/vegetables.rb
@@ -1,9 +1,10 @@
-class Vegetable < ActiveRecord::Base
+# frozen_string_literal: true
+class Vegetable < ActiveRecord::Base
validates_presence_of :name
def self.inheritance_column
- 'custom_type'
+ "custom_type"
end
end
@@ -20,5 +21,5 @@ class KingCole < GreenCabbage
end
class RedCabbage < Cabbage
- belongs_to :seller, :class_name => 'Company'
+ belongs_to :seller, class_name: "Company"
end
diff --git a/activerecord/test/models/vehicle.rb b/activerecord/test/models/vehicle.rb
new file mode 100644
index 0000000000..c9b3338522
--- /dev/null
+++ b/activerecord/test/models/vehicle.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Vehicle < ActiveRecord::Base
+ self.abstract_class = true
+ default_scope -> { where("tires_count IS NOT NULL") }
+end
+
+class Bus < Vehicle
+end
diff --git a/activerecord/test/models/vertex.rb b/activerecord/test/models/vertex.rb
index 48bb851e62..0ad8114898 100644
--- a/activerecord/test/models/vertex.rb
+++ b/activerecord/test/models/vertex.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
# This class models a vertex in a directed graph.
class Vertex < ActiveRecord::Base
- has_many :sink_edges, :class_name => 'Edge', :foreign_key => 'source_id'
- has_many :sinks, :through => :sink_edges
+ has_many :sink_edges, class_name: "Edge", foreign_key: "source_id"
+ has_many :sinks, through: :sink_edges
has_and_belongs_to_many :sources,
- :class_name => 'Vertex', :join_table => 'edges',
- :foreign_key => 'sink_id', :association_foreign_key => 'source_id'
+ class_name: "Vertex", join_table: "edges",
+ foreign_key: "sink_id", association_foreign_key: "source_id"
end
diff --git a/activerecord/test/models/warehouse_thing.rb b/activerecord/test/models/warehouse_thing.rb
index f20bd1a245..099633af55 100644
--- a/activerecord/test/models/warehouse_thing.rb
+++ b/activerecord/test/models/warehouse_thing.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class WarehouseThing < ActiveRecord::Base
self.table_name = "warehouse-things"
diff --git a/activerecord/test/models/wheel.rb b/activerecord/test/models/wheel.rb
index 26868bce5e..22fc74995f 100644
--- a/activerecord/test/models/wheel.rb
+++ b/activerecord/test/models/wheel.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Wheel < ActiveRecord::Base
- belongs_to :wheelable, :polymorphic => true, :counter_cache => true
+ belongs_to :wheelable, polymorphic: true, counter_cache: true, touch: :wheels_owned_at
end
diff --git a/activerecord/test/models/without_table.rb b/activerecord/test/models/without_table.rb
index 50c824e4ac..fc0a52d6ad 100644
--- a/activerecord/test/models/without_table.rb
+++ b/activerecord/test/models/without_table.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class WithoutTable < ActiveRecord::Base
- default_scope -> { where(:published => true) }
+ default_scope -> { where(published: true) }
end
diff --git a/activerecord/test/models/zine.rb b/activerecord/test/models/zine.rb
index c2d0fdaf25..6f361665ef 100644
--- a/activerecord/test/models/zine.rb
+++ b/activerecord/test/models/zine.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Zine < ActiveRecord::Base
- has_many :interests, :inverse_of => :zine
+ has_many :interests, inverse_of: :zine
end
diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb
index 52d3290c84..ccca9a2d9b 100644
--- a/activerecord/test/schema/mysql2_specific_schema.rb
+++ b/activerecord/test/schema/mysql2_specific_schema.rb
@@ -1,32 +1,57 @@
+# frozen_string_literal: true
+
ActiveRecord::Schema.define do
+ if subsecond_precision_supported?
+ create_table :datetime_defaults, force: true do |t|
+ t.datetime :modified_datetime, default: -> { "CURRENT_TIMESTAMP" }
+ t.datetime :precise_datetime, precision: 6, default: -> { "CURRENT_TIMESTAMP(6)" }
+ end
+
+ create_table :timestamp_defaults, force: true do |t|
+ t.timestamp :nullable_timestamp
+ t.timestamp :modified_timestamp, default: -> { "CURRENT_TIMESTAMP" }
+ t.timestamp :precise_timestamp, precision: 6, default: -> { "CURRENT_TIMESTAMP(6)" }
+ end
+ end
+
+ create_table :defaults, force: true do |t|
+ t.date :fixed_date, default: "2004-01-01"
+ t.datetime :fixed_time, default: "2004-01-01 00:00:00"
+ t.column :char1, "char(1)", default: "Y"
+ t.string :char2, limit: 50, default: "a varchar field"
+ if supports_default_expression?
+ t.binary :uuid, limit: 36, default: -> { "(uuid())" }
+ end
+ end
+
create_table :binary_fields, force: true do |t|
t.binary :var_binary, limit: 255
t.binary :var_binary_large, limit: 4095
- t.column :tiny_blob, 'tinyblob', limit: 255
- t.binary :normal_blob, limit: 65535
- t.binary :medium_blob, limit: 16777215
- t.binary :long_blob, limit: 2147483647
- t.text :tiny_text, limit: 255
- t.text :normal_text, limit: 65535
- t.text :medium_text, limit: 16777215
- t.text :long_text, limit: 2147483647
- end
+ t.tinyblob :tiny_blob
+ t.blob :normal_blob
+ t.mediumblob :medium_blob
+ t.longblob :long_blob
+ t.tinytext :tiny_text
+ t.text :normal_text
+ t.mediumtext :medium_text
+ t.longtext :long_text
- add_index :binary_fields, :var_binary
+ t.index :var_binary
+ end
- create_table :key_tests, force: true, :options => 'ENGINE=MyISAM' do |t|
+ create_table :key_tests, force: true do |t|
t.string :awesome
t.string :pizza
t.string :snacks
+ t.index :awesome, type: :fulltext, name: "index_key_tests_on_awesome"
+ t.index :pizza, using: :btree, name: "index_key_tests_on_pizza"
+ t.index :snacks, name: "index_key_tests_on_snack"
end
- add_index :key_tests, :awesome, :type => :fulltext, :name => 'index_key_tests_on_awesome'
- add_index :key_tests, :pizza, :using => :btree, :name => 'index_key_tests_on_pizza'
- add_index :key_tests, :snacks, :name => 'index_key_tests_on_snack'
-
create_table :collation_tests, id: false, force: true do |t|
- t.string :string_cs_column, limit: 1, collation: 'utf8_bin'
- t.string :string_ci_column, limit: 1, collation: 'utf8_general_ci'
+ t.string :string_cs_column, limit: 1, collation: "utf8mb4_bin"
+ t.string :string_ci_column, limit: 1, collation: "utf8mb4_general_ci"
+ t.binary :binary_column, limit: 1
end
ActiveRecord::Base.connection.execute <<-SQL
@@ -40,11 +65,22 @@ BEGIN
END
SQL
+ ActiveRecord::Base.connection.execute <<-SQL
+DROP PROCEDURE IF EXISTS topics;
+SQL
+
+ ActiveRecord::Base.connection.execute <<-SQL
+CREATE PROCEDURE topics(IN num INT) SQL SECURITY INVOKER
+BEGIN
+ select * from topics limit num;
+END
+SQL
+
ActiveRecord::Base.connection.drop_table "enum_tests", if_exists: true
ActiveRecord::Base.connection.execute <<-SQL
CREATE TABLE enum_tests (
- enum_column ENUM('text','blob','tiny','medium','long')
+ enum_column ENUM('text','blob','tiny','medium','long','unsigned','bigint')
)
SQL
end
diff --git a/activerecord/test/schema/mysql_specific_schema.rb b/activerecord/test/schema/mysql_specific_schema.rb
deleted file mode 100644
index 90f5a60d7b..0000000000
--- a/activerecord/test/schema/mysql_specific_schema.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-ActiveRecord::Schema.define do
- create_table :binary_fields, force: true do |t|
- t.binary :var_binary, limit: 255
- t.binary :var_binary_large, limit: 4095
- t.column :tiny_blob, 'tinyblob', limit: 255
- t.binary :normal_blob, limit: 65535
- t.binary :medium_blob, limit: 16777215
- t.binary :long_blob, limit: 2147483647
- t.text :tiny_text, limit: 255
- t.text :normal_text, limit: 65535
- t.text :medium_text, limit: 16777215
- t.text :long_text, limit: 2147483647
- end
-
- add_index :binary_fields, :var_binary
-
- create_table :key_tests, force: true, :options => 'ENGINE=MyISAM' do |t|
- t.string :awesome
- t.string :pizza
- t.string :snacks
- end
-
- add_index :key_tests, :awesome, :type => :fulltext, :name => 'index_key_tests_on_awesome'
- add_index :key_tests, :pizza, :using => :btree, :name => 'index_key_tests_on_pizza'
- add_index :key_tests, :snacks, :name => 'index_key_tests_on_snack'
-
- create_table :collation_tests, id: false, force: true do |t|
- t.string :string_cs_column, limit: 1, collation: 'utf8_bin'
- t.string :string_ci_column, limit: 1, collation: 'utf8_general_ci'
- end
-
- ActiveRecord::Base.connection.execute <<-SQL
-DROP PROCEDURE IF EXISTS ten;
-SQL
-
- ActiveRecord::Base.connection.execute <<-SQL
-CREATE PROCEDURE ten() SQL SECURITY INVOKER
-BEGIN
- select 10;
-END
-SQL
-
- ActiveRecord::Base.connection.execute <<-SQL
-DROP PROCEDURE IF EXISTS topics;
-SQL
-
- ActiveRecord::Base.connection.execute <<-SQL
-CREATE PROCEDURE topics() SQL SECURITY INVOKER
-BEGIN
- select * from topics limit 1;
-END
-SQL
-
- ActiveRecord::Base.connection.drop_table "enum_tests", if_exists: true
-
- ActiveRecord::Base.connection.execute <<-SQL
-CREATE TABLE enum_tests (
- enum_column ENUM('text','blob','tiny','medium','long')
-)
-SQL
-
-end
diff --git a/activerecord/test/schema/oracle_specific_schema.rb b/activerecord/test/schema/oracle_specific_schema.rb
index 264d9b8910..bc1e45ca80 100644
--- a/activerecord/test/schema/oracle_specific_schema.rb
+++ b/activerecord/test/schema/oracle_specific_schema.rb
@@ -1,5 +1,6 @@
-ActiveRecord::Schema.define do
+# frozen_string_literal: true
+ActiveRecord::Schema.define do
execute "drop table test_oracle_defaults" rescue nil
execute "drop sequence test_oracle_defaults_seq" rescue nil
execute "drop sequence companies_nonstd_seq" rescue nil
@@ -36,5 +37,4 @@ create sequence test_oracle_defaults_seq minvalue 10000
)
SQL
execute "create sequence defaults_seq minvalue 10000"
-
end
diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb
index df0362573b..975824ed51 100644
--- a/activerecord/test/schema/postgresql_specific_schema.rb
+++ b/activerecord/test/schema/postgresql_specific_schema.rb
@@ -1,67 +1,61 @@
+# frozen_string_literal: true
+
ActiveRecord::Schema.define do
+ enable_extension!("uuid-ossp", ActiveRecord::Base.connection)
+ enable_extension!("pgcrypto", ActiveRecord::Base.connection) if ActiveRecord::Base.connection.supports_pgcrypto_uuid?
- enable_extension!('uuid-ossp', ActiveRecord::Base.connection)
+ uuid_default = connection.supports_pgcrypto_uuid? ? {} : { default: "uuid_generate_v4()" }
- create_table :uuid_parents, id: :uuid, force: true do |t|
+ create_table :uuid_parents, id: :uuid, force: true, **uuid_default do |t|
t.string :name
end
- create_table :uuid_children, id: :uuid, force: true do |t|
+ create_table :uuid_children, id: :uuid, force: true, **uuid_default do |t|
t.string :name
t.uuid :uuid_parent_id
end
- %w(postgresql_times postgresql_oids defaults postgresql_timestamp_with_zones
- postgresql_partitioned_table postgresql_partitioned_table_parent).each do |table_name|
- drop_table table_name, if_exists: true
+ create_table :defaults, force: true do |t|
+ t.date :modified_date, default: -> { "CURRENT_DATE" }
+ t.date :modified_date_function, default: -> { "now()" }
+ t.date :fixed_date, default: "2004-01-01"
+ t.datetime :modified_time, default: -> { "CURRENT_TIMESTAMP" }
+ t.datetime :modified_time_function, default: -> { "now()" }
+ t.datetime :fixed_time, default: "2004-01-01 00:00:00.000000-00"
+ t.column :char1, "char(1)", default: "Y"
+ 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: "--- []
+
+"
+ end
+
+ create_table :postgresql_times, force: true do |t|
+ t.interval :time_interval
+ t.interval :scaled_time_interval, precision: 6
end
- execute 'DROP SEQUENCE IF EXISTS companies_nonstd_seq CASCADE'
- execute 'CREATE SEQUENCE companies_nonstd_seq START 101 OWNED BY companies.id'
+ 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')"
- execute 'DROP SEQUENCE IF EXISTS companies_id_seq'
+ execute "DROP SEQUENCE IF EXISTS companies_id_seq"
- execute 'DROP FUNCTION IF EXISTS partitioned_insert_trigger()'
+ execute "DROP FUNCTION IF EXISTS partitioned_insert_trigger()"
%w(accounts_id_seq developers_id_seq projects_id_seq topics_id_seq customers_id_seq orders_id_seq).each do |seq_name|
execute "SELECT setval('#{seq_name}', 100)"
end
execute <<_SQL
- CREATE TABLE defaults (
- id serial primary key,
- modified_date date default CURRENT_DATE,
- modified_date_function date default now(),
- fixed_date date default '2004-01-01',
- modified_time timestamp default CURRENT_TIMESTAMP,
- modified_time_function timestamp default now(),
- fixed_time timestamp default '2004-01-01 00:00:00.000000-00',
- char1 char(1) default 'Y',
- char2 character varying(50) default 'a varchar field',
- char3 text default 'a text field',
- bigint_default bigint default 0::bigint,
- multiline_default text DEFAULT '--- []
-
-'::text
-);
-_SQL
-
- 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
@@ -91,7 +85,7 @@ _SQL
FOR EACH ROW EXECUTE PROCEDURE partitioned_insert_trigger();
_SQL
rescue ActiveRecord::StatementInvalid => e
- if e.message =~ /language "plpgsql" does not exist/
+ if e.message.include?('language "plpgsql" does not exist')
execute "CREATE LANGUAGE 'plpgsql';"
retry
else
@@ -109,4 +103,9 @@ _SQL
t.integer :big_int_data_points, limit: 8, array: true
t.decimal :decimal_array_default, array: true, default: [1.23, 3.45]
end
+
+ create_table :uuid_items, force: true, id: false do |t|
+ t.uuid :uuid, primary_key: true, **uuid_default
+ t.string :title
+ end
end
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index 6872b49ad9..7034c773d2 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -1,11 +1,6 @@
+# frozen_string_literal: true
ActiveRecord::Schema.define do
- def except(adapter_names_to_exclude)
- unless [adapter_names_to_exclude].flatten.include?(adapter_name)
- yield
- end
- end
-
# ------------------------------------------------------------------- #
# #
# Please keep these create table statements in alphabetical order #
@@ -14,7 +9,7 @@ ActiveRecord::Schema.define do
# ------------------------------------------------------------------- #
create_table :accounts, force: true do |t|
- t.integer :firm_id
+ t.references :firm, index: false
t.string :firm_name
t.integer :credit_limit
end
@@ -26,9 +21,12 @@ ActiveRecord::Schema.define do
create_table :admin_users, force: true do |t|
t.string :name
t.string :settings, null: true, limit: 1024
+ t.string :parent, null: true, limit: 1024
+ t.string :spouse, null: true, limit: 1024
+ t.string :configs, null: true, limit: 1024
# MySQL does not allow default values for blobs. Fake it out with a
# big varchar below.
- t.string :preferences, null: true, default: '', limit: 1024
+ t.string :preferences, null: true, default: "", limit: 1024
t.string :json_data, null: true, limit: 1024
t.string :json_data_empty, null: true, default: "", limit: 1024
t.text :params
@@ -37,6 +35,8 @@ ActiveRecord::Schema.define do
create_table :aircraft, force: true do |t|
t.string :name
+ t.integer :wheels_count, default: 0, null: false
+ t.datetime :wheels_owned_at
end
create_table :articles, force: true do |t|
@@ -60,8 +60,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
@@ -93,8 +93,8 @@ ActiveRecord::Schema.define do
t.integer :pirate_id
end
- create_table :books, force: true do |t|
- t.integer :author_id
+ create_table :books, id: :integer, force: true do |t|
+ t.references :author
t.string :format
t.column :name, :string
t.column :status, :integer, default: 0
@@ -104,6 +104,8 @@ ActiveRecord::Schema.define do
t.column :author_visibility, :integer, default: 0
t.column :illustrator_visibility, :integer, default: 0
t.column :font_size, :integer, default: 0
+ t.column :difficulty, :integer, default: 0
+ t.column :cover, :string, default: "hard"
end
create_table :booleans, force: true do |t|
@@ -111,10 +113,10 @@ ActiveRecord::Schema.define do
t.boolean :has_fun, null: false, default: false
end
- create_table :bulbs, force: true do |t|
+ create_table :bulbs, primary_key: "ID", force: true do |t|
t.integer :car_id
t.string :name
- t.boolean :frickinawesome
+ t.boolean :frickinawesome, default: false
t.string :color
end
@@ -125,11 +127,17 @@ ActiveRecord::Schema.define do
create_table :cars, force: true do |t|
t.string :name
t.integer :engines_count
- t.integer :wheels_count
+ t.integer :wheels_count, default: 0, null: false
+ t.datetime :wheels_owned_at
t.column :lock_version, :integer, null: false, default: 0
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|
t.string :name, null: false
t.string :type
@@ -150,8 +158,9 @@ ActiveRecord::Schema.define do
end
create_table :citations, force: true do |t|
- t.column :book1_id, :integer
- t.column :book2_id, :integer
+ t.references :book1
+ t.references :book2
+ t.references :citation
end
create_table :clubs, force: true do |t|
@@ -188,22 +197,35 @@ ActiveRecord::Schema.define do
t.string :resource_id
t.string :resource_type
t.integer :developer_id
+ t.datetime :updated_at
+ t.datetime :deleted_at
+ t.integer :comments
end
create_table :companies, force: true do |t|
t.string :type
- t.integer :firm_id
+ t.references :firm, index: false
t.string :firm_name
t.string :name
- t.integer :client_of
- t.integer :rating, default: 1
+ t.bigint :client_of
+ t.bigint :rating, default: 1
t.integer :account_id
t.string :description, default: ""
+ t.index [:name, :rating], order: :desc
+ t.index [:name, :description], length: 10
+ t.index [:firm_id, :type, :rating], name: "company_index", length: { type: 10 }, order: { rating: :desc }
+ t.index [:firm_id, :type], name: "company_partial_index", where: "(rating > 10)"
+ t.index :name, name: "company_name_index", using: :btree
+ t.index "(CASE WHEN rating > 0 THEN lower(name) END) DESC", name: "company_expression_index" if supports_expression_index?
end
- add_index :companies, [:firm_id, :type, :rating], name: "company_index"
- add_index :companies, [:firm_id, :type], name: "company_partial_index", where: "rating > 10"
- add_index :companies, :name, name: 'company_name_index', using: :btree
+ create_table :content, force: true do |t|
+ t.string :title
+ end
+
+ create_table :content_positions, force: true do |t|
+ t.integer :content_id
+ end
create_table :vegetables, force: true do |t|
t.string :name
@@ -223,8 +245,8 @@ ActiveRecord::Schema.define do
end
create_table :contracts, force: true do |t|
- t.integer :developer_id
- t.integer :company_id
+ t.references :developer, index: false
+ t.references :company, index: false
end
create_table :customers, force: true do |t|
@@ -236,6 +258,11 @@ ActiveRecord::Schema.define do
t.string :gps_location
end
+ create_table :customer_carriers, force: true do |t|
+ t.references :customer
+ t.references :carrier
+ end
+
create_table :dashboards, force: true, id: false do |t|
t.string :dashboard_id
t.string :name
@@ -243,11 +270,21 @@ ActiveRecord::Schema.define do
create_table :developers, force: true do |t|
t.string :name
+ t.string :first_name
t.integer :salary, default: 70000
- t.datetime :created_at
- t.datetime :updated_at
- t.datetime :created_on
- t.datetime :updated_on
+ t.references :firm, index: false
+ t.integer :mentor_id
+ if subsecond_precision_supported?
+ t.datetime :created_at, precision: 6
+ t.datetime :updated_at, precision: 6
+ t.datetime :created_on, precision: 6
+ t.datetime :updated_on, precision: 6
+ else
+ t.datetime :created_at
+ t.datetime :updated_at
+ t.datetime :created_on
+ t.datetime :updated_on
+ end
end
create_table :developers_projects, force: true, id: false do |t|
@@ -278,11 +315,11 @@ ActiveRecord::Schema.define do
create_table :edges, force: true, id: false do |t|
t.column :source_id, :integer, null: false
t.column :sink_id, :integer, null: false
+ t.index [:source_id, :sink_id], unique: true, name: "unique_edge_index"
end
- add_index :edges, [:source_id, :sink_id], unique: true, name: 'unique_edge_index'
create_table :engines, force: true do |t|
- t.integer :car_id
+ t.references :car, index: false
end
create_table :entrants, force: true do |t|
@@ -305,6 +342,19 @@ 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 :frogs, force: true do |t|
+ t.string :name
+ end
+
create_table :funny_jokes, force: true do |t|
t.string :name
end
@@ -331,6 +381,10 @@ ActiveRecord::Schema.define do
t.column :key, :string
end
+ create_table :guitars, force: true do |t|
+ t.string :color
+ end
+
create_table :inept_wizards, force: true do |t|
t.column :name, :string, null: false
t.column :city, :string, null: false
@@ -346,7 +400,11 @@ ActiveRecord::Schema.define do
create_table :invoices, force: true do |t|
t.integer :balance
- t.datetime :updated_at
+ if subsecond_precision_supported?
+ t.datetime :updated_at, precision: 6
+ else
+ t.datetime :updated_at
+ end
end
create_table :iris, force: true do |t|
@@ -362,11 +420,19 @@ ActiveRecord::Schema.define do
t.integer :ideal_reference_id
end
+ create_table :jobs_pool, force: true, id: false do |t|
+ t.references :job, null: false, index: true
+ t.references :user, null: false, index: true
+ end
+
create_table :keyboards, force: true, id: false do |t|
t.primary_key :key_number
t.string :name
end
+ create_table :kitchens, force: true do |t|
+ end
+
create_table :legacy_things, force: true do |t|
t.integer :tps_report_number
t.integer :version, null: false, default: 0
@@ -381,6 +447,14 @@ ActiveRecord::Schema.define do
t.references :student
end
+ create_table :students, force: true do |t|
+ t.string :name
+ t.boolean :active
+ t.integer :college_id
+ end
+
+ add_foreign_key :lessons_students, :students, on_delete: :cascade
+
create_table :lint_models, force: true
create_table :line_items, force: true do |t|
@@ -388,12 +462,21 @@ ActiveRecord::Schema.define do
t.integer :amount
end
+ create_table :lions, force: true do |t|
+ t.integer :gender
+ t.boolean :is_vegetarian, default: false
+ end
+
create_table :lock_without_defaults, force: true do |t|
+ t.column :title, :string
t.column :lock_version, :integer
+ t.timestamps null: true
end
create_table :lock_without_defaults_cust, force: true do |t|
+ t.column :title, :string
t.column :custom_lock_version, :integer
+ t.timestamps null: true
end
create_table :magazines, force: true do |t|
@@ -407,7 +490,8 @@ ActiveRecord::Schema.define do
create_table :members, force: true do |t|
t.string :name
- t.integer :member_type_id
+ t.references :member_type, index: false
+ t.references :admittable, polymorphic: true, index: false
end
create_table :member_details, force: true do |t|
@@ -425,13 +509,17 @@ ActiveRecord::Schema.define do
t.datetime :joined_on
t.integer :club_id, :member_id
t.boolean :favourite, default: false
- t.string :type
+ t.integer :type
end
create_table :member_types, force: true do |t|
t.string :name
end
+ create_table :mentors, force: true do |t|
+ t.string :name
+ end
+
create_table :minivans, force: true, id: false do |t|
t.string :minivan_id
t.string :name
@@ -470,7 +558,7 @@ ActiveRecord::Schema.define do
create_table :numeric_data, force: true do |t|
t.decimal :bank_balance, precision: 10, scale: 2
t.decimal :big_bank_balance, precision: 15, scale: 2
- t.decimal :world_population, precision: 10, scale: 0
+ t.decimal :world_population, precision: 20, scale: 0
t.decimal :my_house_population, precision: 2, scale: 0
t.decimal :decimal_number_with_default, precision: 3, scale: 2, default: 2.78
t.float :temperature
@@ -496,7 +584,11 @@ ActiveRecord::Schema.define do
create_table :owners, primary_key: :owner_id, force: true do |t|
t.string :name
- t.column :updated_at, :datetime
+ if subsecond_precision_supported?
+ t.column :updated_at, :datetime, precision: 6
+ else
+ t.column :updated_at, :datetime
+ end
t.column :happy_at, :datetime
t.string :essay_id
end
@@ -509,25 +601,55 @@ ActiveRecord::Schema.define do
t.integer :non_poly_two_id
end
- create_table :parrots, force: true do |t|
- t.column :name, :string
- t.column :color, :string
- t.column :parrot_sti_class, :string
- t.column :killer_id, :integer
- t.column :created_at, :datetime
- t.column :created_on, :datetime
- t.column :updated_at, :datetime
- t.column :updated_on, :datetime
- end
+ disable_referential_integrity do
+ create_table :parrots, force: :cascade do |t|
+ t.string :name
+ t.string :color
+ t.string :parrot_sti_class
+ t.integer :killer_id
+ t.integer :updated_count, :integer, default: 0
+ if subsecond_precision_supported?
+ t.datetime :created_at, precision: 0
+ t.datetime :created_on, precision: 0
+ t.datetime :updated_at, precision: 0
+ t.datetime :updated_on, precision: 0
+ else
+ t.datetime :created_at
+ t.datetime :created_on
+ t.datetime :updated_at
+ t.datetime :updated_on
+ end
+ end
- create_table :parrots_pirates, id: false, force: true do |t|
- t.column :parrot_id, :integer
- t.column :pirate_id, :integer
- end
+ create_table :pirates, force: :cascade do |t|
+ t.string :catchphrase
+ t.integer :parrot_id
+ t.integer :non_validated_parrot_id
+ if subsecond_precision_supported?
+ t.datetime :created_on, precision: 6
+ t.datetime :updated_on, precision: 6
+ else
+ t.datetime :created_on
+ t.datetime :updated_on
+ end
+ end
- create_table :parrots_treasures, id: false, force: true do |t|
- t.column :parrot_id, :integer
- t.column :treasure_id, :integer
+ create_table :treasures, force: :cascade do |t|
+ t.string :name
+ t.string :type
+ t.references :looter, polymorphic: true
+ t.references :ship
+ end
+
+ create_table :parrots_pirates, id: false, force: true do |t|
+ t.references :parrot, foreign_key: true
+ t.references :pirate, foreign_key: true
+ end
+
+ create_table :parrots_treasures, id: false, force: true do |t|
+ t.references :parrot, foreign_key: true
+ t.references :treasure, foreign_key: true
+ end
end
create_table :people, force: true do |t|
@@ -560,20 +682,22 @@ ActiveRecord::Schema.define do
create_table :pets, primary_key: :pet_id, force: true do |t|
t.string :name
t.integer :owner_id, :integer
- t.timestamps null: false
+ if subsecond_precision_supported?
+ t.timestamps null: false, precision: 6
+ else
+ t.timestamps null: false
+ end
end
- create_table :pirates, force: true do |t|
- t.column :catchphrase, :string
- t.column :parrot_id, :integer
- t.integer :non_validated_parrot_id
- t.column :created_on, :datetime
- t.column :updated_on, :datetime
+ create_table :pets_treasures, force: true do |t|
+ t.column :treasure_id, :integer
+ t.column :pet_id, :integer
+ t.column :rainbow_color, :string
end
create_table :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)
@@ -586,6 +710,7 @@ ActiveRecord::Schema.define do
t.integer :taggings_with_delete_all_count, default: 0
t.integer :taggings_with_destroy_count, default: 0
t.integer :tags_count, default: 0
+ t.integer :indestructible_tags_count, default: 0
t.integer :tags_with_destroy_count, default: 0
t.integer :tags_with_nullify_count, default: 0
end
@@ -619,6 +744,8 @@ ActiveRecord::Schema.define do
create_table :projects, force: true do |t|
t.string :name
t.string :type
+ t.references :firm, index: false
+ t.integer :mentor_id
end
create_table :randomly_named_table1, force: true do |t|
@@ -665,7 +792,10 @@ ActiveRecord::Schema.define do
create_table :ships, force: true do |t|
t.string :name
t.integer :pirate_id
+ t.belongs_to :developer
t.integer :update_only_pirate_id
+ # Conventionally named column for counter_cache
+ t.integer :treasures_count, default: 0
t.datetime :created_at
t.datetime :created_on
t.datetime :updated_at
@@ -675,7 +805,24 @@ ActiveRecord::Schema.define do
create_table :ship_parts, force: true do |t|
t.string :name
t.integer :ship_id
- t.datetime :updated_at
+ if subsecond_precision_supported?
+ t.datetime :updated_at, precision: 6
+ else
+ t.datetime :updated_at
+ end
+ end
+
+ create_table :prisoners, force: true do |t|
+ t.belongs_to :ship
+ end
+
+ create_table :sinks, force: true do |t|
+ t.references :kitchen
+ end
+
+ create_table :shop_accounts, force: true do |t|
+ t.references :customer
+ t.references :customer_carrier
end
create_table :speedometers, force: true, id: false do |t|
@@ -686,28 +833,25 @@ ActiveRecord::Schema.define do
create_table :sponsors, force: true do |t|
t.integer :club_id
- t.integer :sponsorable_id
- t.string :sponsorable_type
+ t.references :sponsorable, polymorphic: true, index: false
+ t.references :sponsor, polymorphic: true, index: false
end
- create_table :string_key_objects, id: false, primary_key: :id, force: true do |t|
- t.string :id
- t.string :name
- t.integer :lock_version, null: false, default: 0
- end
-
- create_table :students, force: true do |t|
+ create_table :string_key_objects, id: false, force: true do |t|
+ t.string :id, null: false
t.string :name
- t.boolean :active
- t.integer :college_id
+ t.integer :lock_version, null: false, default: 0
+ t.index :id, unique: true
end
- create_table :subscribers, force: true, id: false do |t|
+ create_table :subscribers, id: false, force: true do |t|
t.string :nick, null: false
t.string :name
- t.column :books_count, :integer, null: false, default: 0
+ t.integer :id
+ t.integer :books_count, null: false, default: 0
+ t.integer :update_count, null: false, default: 0
+ t.index :nick, unique: true
end
- add_index :subscribers, :nick, unique: true
create_table :subscriptions, force: true do |t|
t.string :subscriber_id
@@ -725,6 +869,7 @@ ActiveRecord::Schema.define do
t.column :taggable_type, :string
t.column :taggable_id, :integer
t.string :comment
+ t.string :type
end
create_table :tasks, force: true do |t|
@@ -736,7 +881,7 @@ ActiveRecord::Schema.define do
t.string :title, limit: 250
t.string :author_name
t.string :author_email_address
- if mysql_56?
+ if subsecond_precision_supported?
t.datetime :written_on, precision: 6
else
t.datetime :written_on
@@ -759,7 +904,11 @@ ActiveRecord::Schema.define do
t.string :parent_title
t.string :type
t.string :group
- t.timestamps null: true
+ if subsecond_precision_supported?
+ t.timestamps null: true, precision: 6
+ else
+ t.timestamps null: true
+ end
end
create_table :toys, primary_key: :toy_id, force: true do |t|
@@ -776,12 +925,9 @@ ActiveRecord::Schema.define do
t.datetime :updated_at
end
- create_table :treasures, force: true do |t|
- t.column :name, :string
- t.column :type, :string
- t.column :looter_id, :integer
- t.column :looter_type, :string
- t.belongs_to :ship
+ create_table :tuning_pegs, force: true do |t|
+ t.integer :guitar_id
+ t.float :pitch
end
create_table :tyres, force: true do |t|
@@ -797,7 +943,7 @@ ActiveRecord::Schema.define do
t.column :label, :string
end
- create_table 'warehouse-things', force: true do |t|
+ create_table "warehouse-things", force: true do |t|
t.integer :value
end
@@ -805,7 +951,6 @@ ActiveRecord::Schema.define do
create_table(t, force: true) { }
end
- # NOTE - the following 4 tables are used by models that have :inverse_of options on the associations
create_table :men, force: true do |t|
t.string :name
end
@@ -819,6 +964,7 @@ ActiveRecord::Schema.define do
t.string :poly_man_without_inverse_type
t.integer :horrible_polymorphic_man_id
t.string :horrible_polymorphic_man_type
+ t.references :human, polymorphic: true, index: false
end
create_table :interests, force: true do |t|
@@ -829,23 +975,26 @@ ActiveRecord::Schema.define do
t.integer :zine_id
end
- create_table :wheels, force: true do |t|
- t.references :wheelable, polymorphic: true
- end
-
create_table :zines, force: true do |t|
t.string :title
end
- create_table :countries, force: true, id: false, primary_key: 'country_id' do |t|
- t.string :country_id
+ create_table :wheels, force: true do |t|
+ t.integer :size
+ t.references :wheelable, polymorphic: true
+ end
+
+ create_table :countries, force: true, id: false do |t|
+ t.string :country_id, primary_key: true
t.string :name
end
- create_table :treaties, force: true, id: false, primary_key: 'treaty_id' do |t|
- t.string :treaty_id
+
+ create_table :treaties, force: true, id: false do |t|
+ t.string :treaty_id, primary_key: true
t.string :name
end
- create_table :countries_treaties, force: true, id: false do |t|
+
+ create_table :countries_treaties, force: true, primary_key: [:country_id, :treaty_id] do |t|
t.string :country_id, null: false
t.string :treaty_id, null: false
end
@@ -862,9 +1011,9 @@ ActiveRecord::Schema.define do
t.string :name
end
create_table :weirds, force: true do |t|
- t.string 'a$b'
- t.string 'なまえ'
- t.string 'from'
+ t.string "a$b"
+ t.string "なまえ"
+ t.string "from"
end
create_table :nodes, force: true do |t|
@@ -891,6 +1040,8 @@ ActiveRecord::Schema.define do
t.integer :employable_id
t.string :employable_type
t.integer :department_id
+ t.string :employable_list_type
+ t.integer :employable_list_id
end
create_table :recipes, force: true do |t|
t.integer :chef_id
@@ -900,30 +1051,35 @@ ActiveRecord::Schema.define do
create_table :records, force: true do |t|
end
- except 'SQLite' do
- # 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"
- add_foreign_key :lessons_students, :students
end
create_table :overloaded_types, force: true do |t|
t.float :overloaded_float, default: 500
t.float :unoverloaded_float
t.string :overloaded_string_with_limit, limit: 255
- t.string :string_with_default, default: 'the original default'
+ t.string :string_with_default, default: "the original default"
end
create_table :users, force: true do |t|
t.string :token
t.string :auth_token
end
+
+ 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|
@@ -934,3 +1090,14 @@ end
College.connection.create_table :colleges, force: true do |t|
t.column :name, :string, null: false
end
+
+Professor.connection.create_table :professors, force: true do |t|
+ t.column :name, :string, null: false
+end
+
+Professor.connection.create_table :courses_professors, id: false, force: true do |t|
+ 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
index b5552c2755..18192292e4 100644
--- a/activerecord/test/schema/sqlite_specific_schema.rb
+++ b/activerecord/test/schema/sqlite_specific_schema.rb
@@ -1,22 +1,11 @@
+# frozen_string_literal: true
+
ActiveRecord::Schema.define do
- create_table :table_with_autoincrement, :force => true do |t|
- t.column :name, :string
+ create_table :defaults, force: true do |t|
+ t.date :fixed_date, default: "2004-01-01"
+ t.datetime :fixed_time, default: "2004-01-01 00:00:00"
+ t.column :char1, "char(1)", default: "Y"
+ t.string :char2, limit: 50, default: "a varchar field"
+ t.text :char3, limit: 50, default: "a text field"
end
-
- 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/config.rb b/activerecord/test/support/config.rb
index 6d123688a3..bd6d5c339b 100644
--- a/activerecord/test/support/config.rb
+++ b/activerecord/test/support/config.rb
@@ -1,7 +1,9 @@
-require 'yaml'
-require 'erubis'
-require 'fileutils'
-require 'pathname'
+# frozen_string_literal: true
+
+require "yaml"
+require "erb"
+require "fileutils"
+require "pathname"
module ARTest
class << self
@@ -12,28 +14,29 @@ module ARTest
private
def config_file
- Pathname.new(ENV['ARCONFIG'] || TEST_ROOT + '/config.yml')
+ Pathname.new(ENV["ARCONFIG"] || TEST_ROOT + "/config.yml")
end
def read_config
unless config_file.exist?
- FileUtils.cp TEST_ROOT + '/config.example.yml', config_file
+ FileUtils.cp TEST_ROOT + "/config.example.yml", config_file
end
- erb = Erubis::Eruby.new(config_file.read)
+ erb = ERB.new(config_file.read)
expand_config(YAML.parse(erb.result(binding)).transform)
end
def expand_config(config)
- config['connections'].each do |adapter, connection|
- dbs = [['arunit', 'activerecord_unittest'], ['arunit2', 'activerecord_unittest2']]
+ config["connections"].each do |adapter, connection|
+ dbs = [["arunit", "activerecord_unittest"], ["arunit2", "activerecord_unittest2"],
+ ["arunit_without_prepared_statements", "activerecord_unittest"]]
dbs.each do |name, dbname|
unless connection[name].is_a?(Hash)
- connection[name] = { 'database' => connection[name] }
+ connection[name] = { "database" => connection[name] }
end
- connection[name]['database'] ||= dbname
- connection[name]['adapter'] ||= adapter
+ connection[name]["database"] ||= dbname
+ connection[name]["adapter"] ||= adapter
end
end
diff --git a/activerecord/test/support/connection.rb b/activerecord/test/support/connection.rb
index d11fd9cfc1..2a4fa53460 100644
--- a/activerecord/test/support/connection.rb
+++ b/activerecord/test/support/connection.rb
@@ -1,14 +1,21 @@
-require 'active_support/logger'
-require 'models/college'
-require 'models/course'
+# frozen_string_literal: true
+
+require "active_support/logger"
+require "models/college"
+require "models/course"
+require "models/professor"
+require "models/other_dog"
module ARTest
def self.connection_name
- ENV['ARCONN'] || config['default_connection']
+ ENV["ARCONN"] || config["default_connection"]
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/activerecord/test/support/connection_helper.rb b/activerecord/test/support/connection_helper.rb
index 4a19e5df44..3bb1b370c1 100644
--- a/activerecord/test/support/connection_helper.rb
+++ b/activerecord/test/support/connection_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ConnectionHelper
def run_without_connection
original_connection = ActiveRecord::Base.remove_connection
diff --git a/activerecord/test/support/ddl_helper.rb b/activerecord/test/support/ddl_helper.rb
index 43cb235e01..a18bf5ea0a 100644
--- a/activerecord/test/support/ddl_helper.rb
+++ b/activerecord/test/support/ddl_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module DdlHelper
def with_example_table(connection, table_name, definition = nil)
connection.execute("CREATE TABLE #{table_name}(#{definition})")
diff --git a/activerecord/test/support/schema_dumping_helper.rb b/activerecord/test/support/schema_dumping_helper.rb
index 2d1651454d..777e6a7c1b 100644
--- a/activerecord/test/support/schema_dumping_helper.rb
+++ b/activerecord/test/support/schema_dumping_helper.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
module SchemaDumpingHelper
def dump_table_schema(table, connection = ActiveRecord::Base.connection)
old_ignore_tables = ActiveRecord::SchemaDumper.ignore_tables
- ActiveRecord::SchemaDumper.ignore_tables = connection.tables - [table]
+ ActiveRecord::SchemaDumper.ignore_tables = connection.data_sources - [table]
stream = StringIO.new
ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
stream.string
diff --git a/activestorage/.babelrc b/activestorage/.babelrc
new file mode 100644
index 0000000000..ed751f8745
--- /dev/null
+++ b/activestorage/.babelrc
@@ -0,0 +1,8 @@
+{
+ "presets": [
+ ["env", { "modules": false } ]
+ ],
+ "plugins": [
+ "external-helpers"
+ ]
+}
diff --git a/activestorage/.eslintrc b/activestorage/.eslintrc
new file mode 100644
index 0000000000..3d9ecd4bce
--- /dev/null
+++ b/activestorage/.eslintrc
@@ -0,0 +1,19 @@
+{
+ "extends": "eslint:recommended",
+ "rules": {
+ "semi": ["error", "never"],
+ "quotes": ["error", "double"],
+ "no-unused-vars": ["error", { "vars": "all", "args": "none" }]
+ },
+ "plugins": [
+ "import"
+ ],
+ "env": {
+ "browser": true,
+ "es6": true
+ },
+ "parserOptions": {
+ "ecmaVersion": 6,
+ "sourceType": "module"
+ }
+}
diff --git a/activestorage/.gitignore b/activestorage/.gitignore
new file mode 100644
index 0000000000..3e78878ffc
--- /dev/null
+++ b/activestorage/.gitignore
@@ -0,0 +1,6 @@
+/src/
+/test/dummy/db/*.sqlite3
+/test/dummy/db/*.sqlite3-journal
+/test/dummy/log/*.log
+/test/dummy/tmp/
+/test/service/configurations.yml
diff --git a/activestorage/CHANGELOG.md b/activestorage/CHANGELOG.md
new file mode 100644
index 0000000000..f4e2826dc6
--- /dev/null
+++ b/activestorage/CHANGELOG.md
@@ -0,0 +1,123 @@
+* Add `ActiveStorage.routes_prefix` for configuring generated routes.
+
+ *Chris Bisnett*
+
+* `ActiveStorage::Service::AzureStorageService` only handles specifically
+ relevant types of `Azure::Core::Http::HTTPError`. It previously obscured
+ other types of `HTTPError`, which is the azure-storage gem’s catch-all
+ exception class.
+
+ *Cameron Bothner*
+
+* `ActiveStorage::DiskController#show` generates a 404 Not Found response when
+ the requested file is missing from the disk service. It previously raised
+ `Errno::ENOENT`.
+
+ *Cameron Bothner*
+
+* `ActiveStorage::Blob#download` and `ActiveStorage::Blob#open` raise
+ `ActiveStorage::FileNotFoundError` when the corresponding file is missing
+ from the storage service. Services translate service-specific missing object
+ exceptions (e.g. `Google::Cloud::NotFoundError` for the GCS service and
+ `Errno::ENOENT` for the disk service) into
+ `ActiveStorage::FileNotFoundError`.
+
+ *Cameron Bothner*
+
+* Added the `ActiveStorage::SetCurrent` concern for custom Active Storage
+ controllers that can't inherit from `ActiveStorage::BaseController`.
+
+ *George Claghorn*
+
+* Active Storage error classes like `ActiveStorage::IntegrityError` and
+ `ActiveStorage::UnrepresentableError` now inherit from `ActiveStorage::Error`
+ instead of `StandardError`. This permits rescuing `ActiveStorage::Error` to
+ handle all Active Storage errors.
+
+ *Andrei Makarov*, *George Claghorn*
+
+* Uploaded files assigned to a record are persisted to storage when the record
+ is saved instead of immediately.
+
+ In Rails 5.2, the following causes an uploaded file in `params[:avatar]` to
+ be stored:
+
+ ```ruby
+ @user.avatar = params[:avatar]
+ ```
+
+ In Rails 6, the uploaded file is stored when `@user` is successfully saved.
+
+ *George Claghorn*
+
+* Add the ability to reflect on defined attachments using the existing
+ ActiveRecord reflection mechanism.
+
+ *Kevin Deisz*
+
+* Variant arguments of `false` or `nil` will no longer be passed to the
+ processor. For example, the following will not have the monochrome
+ variation applied:
+
+ ```ruby
+ avatar.variant(monochrome: false)
+ ```
+
+ *Jacob Smith*
+
+* Generated attachment getter and setter methods are created
+ within the model's `GeneratedAssociationMethods` module to
+ allow overriding and composition using `super`.
+
+ *Josh Susser*, *Jamon Douglas*
+
+* Add `ActiveStorage::Blob#open`, which downloads a blob to a tempfile on disk
+ and yields the tempfile. Deprecate `ActiveStorage::Downloading`.
+
+ *David Robertson*, *George Claghorn*
+
+* Pass in `identify: false` as an argument when providing a `content_type` for
+ `ActiveStorage::Attached::{One,Many}#attach` to bypass automatic content
+ type inference. For example:
+
+ ```ruby
+ @message.image.attach(
+ io: File.open('/path/to/file'),
+ filename: 'file.pdf',
+ content_type: 'application/pdf',
+ identify: false
+ )
+ ```
+
+ *Ryan Davidson*
+
+* The Google Cloud Storage service properly supports streaming downloads.
+ It now requires version 1.11 or newer of the google-cloud-storage gem.
+
+ *George Claghorn*
+
+* Use the [ImageProcessing](https://github.com/janko-m/image_processing) gem
+ for Active Storage variants, and deprecate the MiniMagick backend.
+
+ This means that variants are now automatically oriented if the original
+ image was rotated. Also, in addition to the existing ImageMagick
+ operations, variants can now use `:resize_to_fit`, `:resize_to_fill`, and
+ other ImageProcessing macros. These are now recommended over raw `:resize`,
+ as they also sharpen the thumbnail after resizing.
+
+ The ImageProcessing gem also comes with a backend implemented on
+ [libvips](http://jcupitt.github.io/libvips/), an alternative to
+ ImageMagick which has significantly better performance than
+ ImageMagick in most cases, both in terms of speed and memory usage. In
+ Active Storage it's now possible to switch to the libvips backend by
+ changing `Rails.application.config.active_storage.variant_processor` to
+ `:vips`.
+
+ *Janko Marohnić*
+
+* Rails 6 requires Ruby 2.4.1 or newer.
+
+ *Jeremy Daer*
+
+
+Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/activestorage/CHANGELOG.md) for previous changes.
diff --git a/activestorage/MIT-LICENSE b/activestorage/MIT-LICENSE
new file mode 100644
index 0000000000..eed89ac398
--- /dev/null
+++ b/activestorage/MIT-LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2017-2018 David Heinemeier Hansson, Basecamp
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"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/activestorage/README.md b/activestorage/README.md
new file mode 100644
index 0000000000..bd31f0ea58
--- /dev/null
+++ b/activestorage/README.md
@@ -0,0 +1,161 @@
+# Active Storage
+
+Active Storage makes it simple to upload and reference files in cloud services like [Amazon S3](https://aws.amazon.com/s3/), [Google Cloud Storage](https://cloud.google.com/storage/docs/), or [Microsoft Azure Storage](https://azure.microsoft.com/en-us/services/storage/), and attach those files to Active Records. Supports having one main service and mirrors in other services for redundancy. It also provides a disk service for testing or local deployments, but the focus is on cloud storage.
+
+Files can be uploaded from the server to the cloud or directly from the client to the cloud.
+
+Image files can furthermore be transformed using on-demand variants for quality, aspect ratio, size, or any other [MiniMagick](https://github.com/minimagick/minimagick) or [Vips](http://www.rubydoc.info/gems/ruby-vips/Vips/Image) supported transformation.
+
+## Compared to other storage solutions
+
+A key difference to how Active Storage works compared to other attachment solutions in Rails is through the use of built-in [Blob](https://github.com/rails/rails/blob/master/activestorage/app/models/active_storage/blob.rb) and [Attachment](https://github.com/rails/rails/blob/master/activestorage/app/models/active_storage/attachment.rb) models (backed by Active Record). This means existing application models do not need to be modified with additional columns to associate with files. Active Storage uses polymorphic associations via the `Attachment` join model, which then connects to the actual `Blob`.
+
+`Blob` models store attachment metadata (filename, content-type, etc.), and their identifier key in the storage service. Blob models do not store the actual binary data. They are intended to be immutable in spirit. One file, one blob. You can associate the same blob with multiple application models as well. And if you want to do transformations of a given `Blob`, the idea is that you'll simply create a new one, rather than attempt to mutate the existing one (though of course you can delete the previous version later if you don't need it).
+
+## Installation
+
+Run `rails active_storage:install` to copy over active_storage migrations.
+
+NOTE: If the task cannot be found, verify that `require "active_storage/engine"` is present in `config/application.rb`.
+
+## Examples
+
+One attachment:
+
+```ruby
+class User < ApplicationRecord
+ # Associates an attachment and a blob. When the user is destroyed they are
+ # purged by default (models destroyed, and resource files deleted).
+ has_one_attached :avatar
+end
+
+# Attach an avatar to the user.
+user.avatar.attach(io: File.open("/path/to/face.jpg"), filename: "face.jpg", content_type: "image/jpg")
+
+# Does the user have an avatar?
+user.avatar.attached? # => true
+
+# Synchronously destroy the avatar and actual resource files.
+user.avatar.purge
+
+# Destroy the associated models and actual resource files async, via Active Job.
+user.avatar.purge_later
+
+# Does the user have an avatar?
+user.avatar.attached? # => false
+
+# Generate a permanent URL for the blob that points to the application.
+# Upon access, a redirect to the actual service endpoint is returned.
+# This indirection decouples the public URL from the actual one, and
+# allows for example mirroring attachments in different services for
+# high-availability. The redirection has an HTTP expiration of 5 min.
+url_for(user.avatar)
+
+class AvatarsController < ApplicationController
+ def update
+ # params[:avatar] contains a ActionDispatch::Http::UploadedFile object
+ Current.user.avatar.attach(params.require(:avatar))
+ redirect_to Current.user
+ end
+end
+```
+
+Many attachments:
+
+```ruby
+class Message < ApplicationRecord
+ has_many_attached :images
+end
+```
+
+```erb
+<%= form_with model: @message, local: true do |form| %>
+ <%= form.text_field :title, placeholder: "Title" %><br>
+ <%= form.text_area :content %><br><br>
+
+ <%= form.file_field :images, multiple: true %><br>
+ <%= form.submit %>
+<% end %>
+```
+
+```ruby
+class MessagesController < ApplicationController
+ def index
+ # Use the built-in with_attached_images scope to avoid N+1
+ @messages = Message.all.with_attached_images
+ end
+
+ def create
+ message = Message.create! params.require(:message).permit(:title, :content)
+ message.images.attach(params[:message][:images])
+ redirect_to message
+ end
+
+ def show
+ @message = Message.find(params[:id])
+ end
+end
+```
+
+Variation of image attachment:
+
+```erb
+<%# Hitting the variant URL will lazy transform the original blob and then redirect to its new service location %>
+<%= image_tag user.avatar.variant(resize_to_fit: [100, 100]) %>
+```
+
+## Direct uploads
+
+Active Storage, with its included JavaScript library, supports uploading directly from the client to the cloud.
+
+### Direct upload installation
+
+1. Include `activestorage.js` in your application's JavaScript bundle.
+
+ Using the asset pipeline:
+ ```js
+ //= require activestorage
+ ```
+ Using the npm package:
+ ```js
+ import * as ActiveStorage from "activestorage"
+ ActiveStorage.start()
+ ```
+2. Annotate file inputs with the direct upload URL.
+
+ ```ruby
+ <%= form.file_field :attachments, multiple: true, direct_upload: true %>
+ ```
+3. That's it! Uploads begin upon form submission.
+
+### Direct upload JavaScript events
+
+| Event name | Event target | Event data (`event.detail`) | Description |
+| --- | --- | --- | --- |
+| `direct-uploads:start` | `<form>` | None | A form containing files for direct upload fields was submitted. |
+| `direct-upload:initialize` | `<input>` | `{id, file}` | Dispatched for every file after form submission. |
+| `direct-upload:start` | `<input>` | `{id, file}` | A direct upload is starting. |
+| `direct-upload:before-blob-request` | `<input>` | `{id, file, xhr}` | Before making a request to your application for direct upload metadata. |
+| `direct-upload:before-storage-request` | `<input>` | `{id, file, xhr}` | Before making a request to store a file. |
+| `direct-upload:progress` | `<input>` | `{id, file, progress}` | As requests to store files progress. |
+| `direct-upload:error` | `<input>` | `{id, file, error}` | An error occurred. An `alert` will display unless this event is canceled. |
+| `direct-upload:end` | `<input>` | `{id, file}` | A direct upload has ended. |
+| `direct-uploads:end` | `<form>` | None | All direct uploads have ended. |
+
+## License
+
+Active Storage is released under the [MIT License](https://opensource.org/licenses/MIT).
+
+## Support
+
+API documentation is at:
+
+* http://api.rubyonrails.org
+
+Bug reports for the Ruby on Rails project can be filed here:
+
+* https://github.com/rails/rails/issues
+
+Feature requests should be discussed on the rails-core mailing list here:
+
+* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core
diff --git a/activestorage/Rakefile b/activestorage/Rakefile
new file mode 100644
index 0000000000..2e86d3d860
--- /dev/null
+++ b/activestorage/Rakefile
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require "bundler/setup"
+require "bundler/gem_tasks"
+require "rake/testtask"
+
+Rake::TestTask.new do |t|
+ t.libs << "app/controllers"
+ t.libs << "test"
+ t.test_files = FileList["test/**/*_test.rb"]
+ t.verbose = true
+ t.warning = true
+end
+
+task :package
+
+task default: :test
diff --git a/activestorage/activestorage.gemspec b/activestorage/activestorage.gemspec
new file mode 100644
index 0000000000..2c8816df25
--- /dev/null
+++ b/activestorage/activestorage.gemspec
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip
+
+Gem::Specification.new do |s|
+ s.platform = Gem::Platform::RUBY
+ s.name = "activestorage"
+ s.version = version
+ s.summary = "Local and cloud file storage framework."
+ s.description = "Attach cloud and local files in Rails applications."
+
+ s.required_ruby_version = ">= 2.4.1"
+
+ s.license = "MIT"
+
+ s.author = "David Heinemeier Hansson"
+ s.email = "david@loudthinking.com"
+ s.homepage = "http://rubyonrails.org"
+
+ s.files = Dir["CHANGELOG.md", "MIT-LICENSE", "README.md", "lib/**/*", "app/**/*", "config/**/*", "db/**/*"]
+ s.require_path = "lib"
+
+ s.metadata = {
+ "source_code_uri" => "https://github.com/rails/rails/tree/v#{version}/activestorage",
+ "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/activestorage/CHANGELOG.md"
+ }
+
+ # NOTE: Please read our dependency guidelines before updating versions:
+ # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves
+
+ s.add_dependency "actionpack", version
+ s.add_dependency "activerecord", version
+
+ s.add_dependency "marcel", "~> 0.3.1"
+end
diff --git a/activestorage/app/assets/javascripts/activestorage.js b/activestorage/app/assets/javascripts/activestorage.js
new file mode 100644
index 0000000000..375eb6b533
--- /dev/null
+++ b/activestorage/app/assets/javascripts/activestorage.js
@@ -0,0 +1,939 @@
+(function(global, factory) {
+ typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define([ "exports" ], factory) : factory(global.ActiveStorage = {});
+})(this, function(exports) {
+ "use strict";
+ function createCommonjsModule(fn, module) {
+ return module = {
+ exports: {}
+ }, fn(module, module.exports), module.exports;
+ }
+ var sparkMd5 = createCommonjsModule(function(module, exports) {
+ (function(factory) {
+ {
+ module.exports = factory();
+ }
+ })(function(undefined) {
+ var hex_chr = [ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f" ];
+ function md5cycle(x, k) {
+ var a = x[0], b = x[1], c = x[2], d = x[3];
+ a += (b & c | ~b & d) + k[0] - 680876936 | 0;
+ a = (a << 7 | a >>> 25) + b | 0;
+ d += (a & b | ~a & c) + k[1] - 389564586 | 0;
+ d = (d << 12 | d >>> 20) + a | 0;
+ c += (d & a | ~d & b) + k[2] + 606105819 | 0;
+ c = (c << 17 | c >>> 15) + d | 0;
+ b += (c & d | ~c & a) + k[3] - 1044525330 | 0;
+ b = (b << 22 | b >>> 10) + c | 0;
+ a += (b & c | ~b & d) + k[4] - 176418897 | 0;
+ a = (a << 7 | a >>> 25) + b | 0;
+ d += (a & b | ~a & c) + k[5] + 1200080426 | 0;
+ d = (d << 12 | d >>> 20) + a | 0;
+ c += (d & a | ~d & b) + k[6] - 1473231341 | 0;
+ c = (c << 17 | c >>> 15) + d | 0;
+ b += (c & d | ~c & a) + k[7] - 45705983 | 0;
+ b = (b << 22 | b >>> 10) + c | 0;
+ a += (b & c | ~b & d) + k[8] + 1770035416 | 0;
+ a = (a << 7 | a >>> 25) + b | 0;
+ d += (a & b | ~a & c) + k[9] - 1958414417 | 0;
+ d = (d << 12 | d >>> 20) + a | 0;
+ c += (d & a | ~d & b) + k[10] - 42063 | 0;
+ c = (c << 17 | c >>> 15) + d | 0;
+ b += (c & d | ~c & a) + k[11] - 1990404162 | 0;
+ b = (b << 22 | b >>> 10) + c | 0;
+ a += (b & c | ~b & d) + k[12] + 1804603682 | 0;
+ a = (a << 7 | a >>> 25) + b | 0;
+ d += (a & b | ~a & c) + k[13] - 40341101 | 0;
+ d = (d << 12 | d >>> 20) + a | 0;
+ c += (d & a | ~d & b) + k[14] - 1502002290 | 0;
+ c = (c << 17 | c >>> 15) + d | 0;
+ b += (c & d | ~c & a) + k[15] + 1236535329 | 0;
+ b = (b << 22 | b >>> 10) + c | 0;
+ a += (b & d | c & ~d) + k[1] - 165796510 | 0;
+ a = (a << 5 | a >>> 27) + b | 0;
+ d += (a & c | b & ~c) + k[6] - 1069501632 | 0;
+ d = (d << 9 | d >>> 23) + a | 0;
+ c += (d & b | a & ~b) + k[11] + 643717713 | 0;
+ c = (c << 14 | c >>> 18) + d | 0;
+ b += (c & a | d & ~a) + k[0] - 373897302 | 0;
+ b = (b << 20 | b >>> 12) + c | 0;
+ a += (b & d | c & ~d) + k[5] - 701558691 | 0;
+ a = (a << 5 | a >>> 27) + b | 0;
+ d += (a & c | b & ~c) + k[10] + 38016083 | 0;
+ d = (d << 9 | d >>> 23) + a | 0;
+ c += (d & b | a & ~b) + k[15] - 660478335 | 0;
+ c = (c << 14 | c >>> 18) + d | 0;
+ b += (c & a | d & ~a) + k[4] - 405537848 | 0;
+ b = (b << 20 | b >>> 12) + c | 0;
+ a += (b & d | c & ~d) + k[9] + 568446438 | 0;
+ a = (a << 5 | a >>> 27) + b | 0;
+ d += (a & c | b & ~c) + k[14] - 1019803690 | 0;
+ d = (d << 9 | d >>> 23) + a | 0;
+ c += (d & b | a & ~b) + k[3] - 187363961 | 0;
+ c = (c << 14 | c >>> 18) + d | 0;
+ b += (c & a | d & ~a) + k[8] + 1163531501 | 0;
+ b = (b << 20 | b >>> 12) + c | 0;
+ a += (b & d | c & ~d) + k[13] - 1444681467 | 0;
+ a = (a << 5 | a >>> 27) + b | 0;
+ d += (a & c | b & ~c) + k[2] - 51403784 | 0;
+ d = (d << 9 | d >>> 23) + a | 0;
+ c += (d & b | a & ~b) + k[7] + 1735328473 | 0;
+ c = (c << 14 | c >>> 18) + d | 0;
+ b += (c & a | d & ~a) + k[12] - 1926607734 | 0;
+ b = (b << 20 | b >>> 12) + c | 0;
+ a += (b ^ c ^ d) + k[5] - 378558 | 0;
+ a = (a << 4 | a >>> 28) + b | 0;
+ d += (a ^ b ^ c) + k[8] - 2022574463 | 0;
+ d = (d << 11 | d >>> 21) + a | 0;
+ c += (d ^ a ^ b) + k[11] + 1839030562 | 0;
+ c = (c << 16 | c >>> 16) + d | 0;
+ b += (c ^ d ^ a) + k[14] - 35309556 | 0;
+ b = (b << 23 | b >>> 9) + c | 0;
+ a += (b ^ c ^ d) + k[1] - 1530992060 | 0;
+ a = (a << 4 | a >>> 28) + b | 0;
+ d += (a ^ b ^ c) + k[4] + 1272893353 | 0;
+ d = (d << 11 | d >>> 21) + a | 0;
+ c += (d ^ a ^ b) + k[7] - 155497632 | 0;
+ c = (c << 16 | c >>> 16) + d | 0;
+ b += (c ^ d ^ a) + k[10] - 1094730640 | 0;
+ b = (b << 23 | b >>> 9) + c | 0;
+ a += (b ^ c ^ d) + k[13] + 681279174 | 0;
+ a = (a << 4 | a >>> 28) + b | 0;
+ d += (a ^ b ^ c) + k[0] - 358537222 | 0;
+ d = (d << 11 | d >>> 21) + a | 0;
+ c += (d ^ a ^ b) + k[3] - 722521979 | 0;
+ c = (c << 16 | c >>> 16) + d | 0;
+ b += (c ^ d ^ a) + k[6] + 76029189 | 0;
+ b = (b << 23 | b >>> 9) + c | 0;
+ a += (b ^ c ^ d) + k[9] - 640364487 | 0;
+ a = (a << 4 | a >>> 28) + b | 0;
+ d += (a ^ b ^ c) + k[12] - 421815835 | 0;
+ d = (d << 11 | d >>> 21) + a | 0;
+ c += (d ^ a ^ b) + k[15] + 530742520 | 0;
+ c = (c << 16 | c >>> 16) + d | 0;
+ b += (c ^ d ^ a) + k[2] - 995338651 | 0;
+ b = (b << 23 | b >>> 9) + c | 0;
+ a += (c ^ (b | ~d)) + k[0] - 198630844 | 0;
+ a = (a << 6 | a >>> 26) + b | 0;
+ d += (b ^ (a | ~c)) + k[7] + 1126891415 | 0;
+ d = (d << 10 | d >>> 22) + a | 0;
+ c += (a ^ (d | ~b)) + k[14] - 1416354905 | 0;
+ c = (c << 15 | c >>> 17) + d | 0;
+ b += (d ^ (c | ~a)) + k[5] - 57434055 | 0;
+ b = (b << 21 | b >>> 11) + c | 0;
+ a += (c ^ (b | ~d)) + k[12] + 1700485571 | 0;
+ a = (a << 6 | a >>> 26) + b | 0;
+ d += (b ^ (a | ~c)) + k[3] - 1894986606 | 0;
+ d = (d << 10 | d >>> 22) + a | 0;
+ c += (a ^ (d | ~b)) + k[10] - 1051523 | 0;
+ c = (c << 15 | c >>> 17) + d | 0;
+ b += (d ^ (c | ~a)) + k[1] - 2054922799 | 0;
+ b = (b << 21 | b >>> 11) + c | 0;
+ a += (c ^ (b | ~d)) + k[8] + 1873313359 | 0;
+ a = (a << 6 | a >>> 26) + b | 0;
+ d += (b ^ (a | ~c)) + k[15] - 30611744 | 0;
+ d = (d << 10 | d >>> 22) + a | 0;
+ c += (a ^ (d | ~b)) + k[6] - 1560198380 | 0;
+ c = (c << 15 | c >>> 17) + d | 0;
+ b += (d ^ (c | ~a)) + k[13] + 1309151649 | 0;
+ b = (b << 21 | b >>> 11) + c | 0;
+ a += (c ^ (b | ~d)) + k[4] - 145523070 | 0;
+ a = (a << 6 | a >>> 26) + b | 0;
+ d += (b ^ (a | ~c)) + k[11] - 1120210379 | 0;
+ d = (d << 10 | d >>> 22) + a | 0;
+ c += (a ^ (d | ~b)) + k[2] + 718787259 | 0;
+ c = (c << 15 | c >>> 17) + d | 0;
+ b += (d ^ (c | ~a)) + k[9] - 343485551 | 0;
+ b = (b << 21 | b >>> 11) + c | 0;
+ x[0] = a + x[0] | 0;
+ x[1] = b + x[1] | 0;
+ x[2] = c + x[2] | 0;
+ x[3] = d + x[3] | 0;
+ }
+ function md5blk(s) {
+ var md5blks = [], i;
+ for (i = 0; i < 64; i += 4) {
+ md5blks[i >> 2] = s.charCodeAt(i) + (s.charCodeAt(i + 1) << 8) + (s.charCodeAt(i + 2) << 16) + (s.charCodeAt(i + 3) << 24);
+ }
+ return md5blks;
+ }
+ function md5blk_array(a) {
+ var md5blks = [], i;
+ for (i = 0; i < 64; i += 4) {
+ md5blks[i >> 2] = a[i] + (a[i + 1] << 8) + (a[i + 2] << 16) + (a[i + 3] << 24);
+ }
+ return md5blks;
+ }
+ function md51(s) {
+ var n = s.length, state = [ 1732584193, -271733879, -1732584194, 271733878 ], i, length, tail, tmp, lo, hi;
+ for (i = 64; i <= n; i += 64) {
+ md5cycle(state, md5blk(s.substring(i - 64, i)));
+ }
+ s = s.substring(i - 64);
+ length = s.length;
+ tail = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ];
+ for (i = 0; i < length; i += 1) {
+ tail[i >> 2] |= s.charCodeAt(i) << (i % 4 << 3);
+ }
+ tail[i >> 2] |= 128 << (i % 4 << 3);
+ if (i > 55) {
+ md5cycle(state, tail);
+ for (i = 0; i < 16; i += 1) {
+ tail[i] = 0;
+ }
+ }
+ tmp = n * 8;
+ tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/);
+ lo = parseInt(tmp[2], 16);
+ hi = parseInt(tmp[1], 16) || 0;
+ tail[14] = lo;
+ tail[15] = hi;
+ md5cycle(state, tail);
+ return state;
+ }
+ function md51_array(a) {
+ var n = a.length, state = [ 1732584193, -271733879, -1732584194, 271733878 ], i, length, tail, tmp, lo, hi;
+ for (i = 64; i <= n; i += 64) {
+ md5cycle(state, md5blk_array(a.subarray(i - 64, i)));
+ }
+ a = i - 64 < n ? a.subarray(i - 64) : new Uint8Array(0);
+ length = a.length;
+ tail = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ];
+ for (i = 0; i < length; i += 1) {
+ tail[i >> 2] |= a[i] << (i % 4 << 3);
+ }
+ tail[i >> 2] |= 128 << (i % 4 << 3);
+ if (i > 55) {
+ md5cycle(state, tail);
+ for (i = 0; i < 16; i += 1) {
+ tail[i] = 0;
+ }
+ }
+ tmp = n * 8;
+ tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/);
+ lo = parseInt(tmp[2], 16);
+ hi = parseInt(tmp[1], 16) || 0;
+ tail[14] = lo;
+ tail[15] = hi;
+ md5cycle(state, tail);
+ return state;
+ }
+ function rhex(n) {
+ var s = "", j;
+ for (j = 0; j < 4; j += 1) {
+ s += hex_chr[n >> j * 8 + 4 & 15] + hex_chr[n >> j * 8 & 15];
+ }
+ return s;
+ }
+ function hex(x) {
+ var i;
+ for (i = 0; i < x.length; i += 1) {
+ x[i] = rhex(x[i]);
+ }
+ return x.join("");
+ }
+ if (hex(md51("hello")) !== "5d41402abc4b2a76b9719d911017c592") ;
+ if (typeof ArrayBuffer !== "undefined" && !ArrayBuffer.prototype.slice) {
+ (function() {
+ function clamp(val, length) {
+ val = val | 0 || 0;
+ if (val < 0) {
+ return Math.max(val + length, 0);
+ }
+ return Math.min(val, length);
+ }
+ ArrayBuffer.prototype.slice = function(from, to) {
+ var length = this.byteLength, begin = clamp(from, length), end = length, num, target, targetArray, sourceArray;
+ if (to !== undefined) {
+ end = clamp(to, length);
+ }
+ if (begin > end) {
+ return new ArrayBuffer(0);
+ }
+ num = end - begin;
+ target = new ArrayBuffer(num);
+ targetArray = new Uint8Array(target);
+ sourceArray = new Uint8Array(this, begin, num);
+ targetArray.set(sourceArray);
+ return target;
+ };
+ })();
+ }
+ function toUtf8(str) {
+ if (/[\u0080-\uFFFF]/.test(str)) {
+ str = unescape(encodeURIComponent(str));
+ }
+ return str;
+ }
+ function utf8Str2ArrayBuffer(str, returnUInt8Array) {
+ var length = str.length, buff = new ArrayBuffer(length), arr = new Uint8Array(buff), i;
+ for (i = 0; i < length; i += 1) {
+ arr[i] = str.charCodeAt(i);
+ }
+ return returnUInt8Array ? arr : buff;
+ }
+ function arrayBuffer2Utf8Str(buff) {
+ return String.fromCharCode.apply(null, new Uint8Array(buff));
+ }
+ function concatenateArrayBuffers(first, second, returnUInt8Array) {
+ var result = new Uint8Array(first.byteLength + second.byteLength);
+ result.set(new Uint8Array(first));
+ result.set(new Uint8Array(second), first.byteLength);
+ return returnUInt8Array ? result : result.buffer;
+ }
+ function hexToBinaryString(hex) {
+ var bytes = [], length = hex.length, x;
+ for (x = 0; x < length - 1; x += 2) {
+ bytes.push(parseInt(hex.substr(x, 2), 16));
+ }
+ return String.fromCharCode.apply(String, bytes);
+ }
+ function SparkMD5() {
+ this.reset();
+ }
+ SparkMD5.prototype.append = function(str) {
+ this.appendBinary(toUtf8(str));
+ return this;
+ };
+ SparkMD5.prototype.appendBinary = function(contents) {
+ this._buff += contents;
+ this._length += contents.length;
+ var length = this._buff.length, i;
+ for (i = 64; i <= length; i += 64) {
+ md5cycle(this._hash, md5blk(this._buff.substring(i - 64, i)));
+ }
+ this._buff = this._buff.substring(i - 64);
+ return this;
+ };
+ SparkMD5.prototype.end = function(raw) {
+ var buff = this._buff, length = buff.length, i, tail = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], ret;
+ for (i = 0; i < length; i += 1) {
+ tail[i >> 2] |= buff.charCodeAt(i) << (i % 4 << 3);
+ }
+ this._finish(tail, length);
+ ret = hex(this._hash);
+ if (raw) {
+ ret = hexToBinaryString(ret);
+ }
+ this.reset();
+ return ret;
+ };
+ SparkMD5.prototype.reset = function() {
+ this._buff = "";
+ this._length = 0;
+ this._hash = [ 1732584193, -271733879, -1732584194, 271733878 ];
+ return this;
+ };
+ SparkMD5.prototype.getState = function() {
+ return {
+ buff: this._buff,
+ length: this._length,
+ hash: this._hash
+ };
+ };
+ SparkMD5.prototype.setState = function(state) {
+ this._buff = state.buff;
+ this._length = state.length;
+ this._hash = state.hash;
+ return this;
+ };
+ SparkMD5.prototype.destroy = function() {
+ delete this._hash;
+ delete this._buff;
+ delete this._length;
+ };
+ SparkMD5.prototype._finish = function(tail, length) {
+ var i = length, tmp, lo, hi;
+ tail[i >> 2] |= 128 << (i % 4 << 3);
+ if (i > 55) {
+ md5cycle(this._hash, tail);
+ for (i = 0; i < 16; i += 1) {
+ tail[i] = 0;
+ }
+ }
+ tmp = this._length * 8;
+ tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/);
+ lo = parseInt(tmp[2], 16);
+ hi = parseInt(tmp[1], 16) || 0;
+ tail[14] = lo;
+ tail[15] = hi;
+ md5cycle(this._hash, tail);
+ };
+ SparkMD5.hash = function(str, raw) {
+ return SparkMD5.hashBinary(toUtf8(str), raw);
+ };
+ SparkMD5.hashBinary = function(content, raw) {
+ var hash = md51(content), ret = hex(hash);
+ return raw ? hexToBinaryString(ret) : ret;
+ };
+ SparkMD5.ArrayBuffer = function() {
+ this.reset();
+ };
+ SparkMD5.ArrayBuffer.prototype.append = function(arr) {
+ var buff = concatenateArrayBuffers(this._buff.buffer, arr, true), length = buff.length, i;
+ this._length += arr.byteLength;
+ for (i = 64; i <= length; i += 64) {
+ md5cycle(this._hash, md5blk_array(buff.subarray(i - 64, i)));
+ }
+ this._buff = i - 64 < length ? new Uint8Array(buff.buffer.slice(i - 64)) : new Uint8Array(0);
+ return this;
+ };
+ SparkMD5.ArrayBuffer.prototype.end = function(raw) {
+ var buff = this._buff, length = buff.length, tail = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], i, ret;
+ for (i = 0; i < length; i += 1) {
+ tail[i >> 2] |= buff[i] << (i % 4 << 3);
+ }
+ this._finish(tail, length);
+ ret = hex(this._hash);
+ if (raw) {
+ ret = hexToBinaryString(ret);
+ }
+ this.reset();
+ return ret;
+ };
+ SparkMD5.ArrayBuffer.prototype.reset = function() {
+ this._buff = new Uint8Array(0);
+ this._length = 0;
+ this._hash = [ 1732584193, -271733879, -1732584194, 271733878 ];
+ return this;
+ };
+ SparkMD5.ArrayBuffer.prototype.getState = function() {
+ var state = SparkMD5.prototype.getState.call(this);
+ state.buff = arrayBuffer2Utf8Str(state.buff);
+ return state;
+ };
+ SparkMD5.ArrayBuffer.prototype.setState = function(state) {
+ state.buff = utf8Str2ArrayBuffer(state.buff, true);
+ return SparkMD5.prototype.setState.call(this, state);
+ };
+ SparkMD5.ArrayBuffer.prototype.destroy = SparkMD5.prototype.destroy;
+ SparkMD5.ArrayBuffer.prototype._finish = SparkMD5.prototype._finish;
+ SparkMD5.ArrayBuffer.hash = function(arr, raw) {
+ var hash = md51_array(new Uint8Array(arr)), ret = hex(hash);
+ return raw ? hexToBinaryString(ret) : ret;
+ };
+ return SparkMD5;
+ });
+ });
+ var classCallCheck = function(instance, Constructor) {
+ if (!(instance instanceof Constructor)) {
+ throw new TypeError("Cannot call a class as a function");
+ }
+ };
+ var createClass = function() {
+ function defineProperties(target, props) {
+ for (var i = 0; i < props.length; i++) {
+ var descriptor = props[i];
+ descriptor.enumerable = descriptor.enumerable || false;
+ descriptor.configurable = true;
+ if ("value" in descriptor) descriptor.writable = true;
+ Object.defineProperty(target, descriptor.key, descriptor);
+ }
+ }
+ return function(Constructor, protoProps, staticProps) {
+ if (protoProps) defineProperties(Constructor.prototype, protoProps);
+ if (staticProps) defineProperties(Constructor, staticProps);
+ return Constructor;
+ };
+ }();
+ var fileSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
+ var FileChecksum = function() {
+ createClass(FileChecksum, null, [ {
+ key: "create",
+ value: function create(file, callback) {
+ var instance = new FileChecksum(file);
+ instance.create(callback);
+ }
+ } ]);
+ function FileChecksum(file) {
+ classCallCheck(this, FileChecksum);
+ this.file = file;
+ this.chunkSize = 2097152;
+ this.chunkCount = Math.ceil(this.file.size / this.chunkSize);
+ this.chunkIndex = 0;
+ }
+ createClass(FileChecksum, [ {
+ key: "create",
+ value: function create(callback) {
+ var _this = this;
+ this.callback = callback;
+ this.md5Buffer = new sparkMd5.ArrayBuffer();
+ this.fileReader = new FileReader();
+ this.fileReader.addEventListener("load", function(event) {
+ return _this.fileReaderDidLoad(event);
+ });
+ this.fileReader.addEventListener("error", function(event) {
+ return _this.fileReaderDidError(event);
+ });
+ this.readNextChunk();
+ }
+ }, {
+ key: "fileReaderDidLoad",
+ value: function fileReaderDidLoad(event) {
+ this.md5Buffer.append(event.target.result);
+ if (!this.readNextChunk()) {
+ var binaryDigest = this.md5Buffer.end(true);
+ var base64digest = btoa(binaryDigest);
+ this.callback(null, base64digest);
+ }
+ }
+ }, {
+ key: "fileReaderDidError",
+ value: function fileReaderDidError(event) {
+ this.callback("Error reading " + this.file.name);
+ }
+ }, {
+ key: "readNextChunk",
+ value: function readNextChunk() {
+ if (this.chunkIndex < this.chunkCount || this.chunkIndex == 0 && this.chunkCount == 0) {
+ var start = this.chunkIndex * this.chunkSize;
+ var end = Math.min(start + this.chunkSize, this.file.size);
+ var bytes = fileSlice.call(this.file, start, end);
+ this.fileReader.readAsArrayBuffer(bytes);
+ this.chunkIndex++;
+ return true;
+ } else {
+ return false;
+ }
+ }
+ } ]);
+ return FileChecksum;
+ }();
+ function getMetaValue(name) {
+ var element = findElement(document.head, 'meta[name="' + name + '"]');
+ if (element) {
+ return element.getAttribute("content");
+ }
+ }
+ function findElements(root, selector) {
+ if (typeof root == "string") {
+ selector = root;
+ root = document;
+ }
+ var elements = root.querySelectorAll(selector);
+ return toArray$1(elements);
+ }
+ function findElement(root, selector) {
+ if (typeof root == "string") {
+ selector = root;
+ root = document;
+ }
+ return root.querySelector(selector);
+ }
+ function dispatchEvent(element, type) {
+ var eventInit = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
+ var disabled = element.disabled;
+ var bubbles = eventInit.bubbles, cancelable = eventInit.cancelable, detail = eventInit.detail;
+ var event = document.createEvent("Event");
+ event.initEvent(type, bubbles || true, cancelable || true);
+ event.detail = detail || {};
+ try {
+ element.disabled = false;
+ element.dispatchEvent(event);
+ } finally {
+ element.disabled = disabled;
+ }
+ return event;
+ }
+ function toArray$1(value) {
+ if (Array.isArray(value)) {
+ return value;
+ } else if (Array.from) {
+ return Array.from(value);
+ } else {
+ return [].slice.call(value);
+ }
+ }
+ var BlobRecord = function() {
+ function BlobRecord(file, checksum, url) {
+ var _this = this;
+ classCallCheck(this, BlobRecord);
+ this.file = file;
+ this.attributes = {
+ filename: file.name,
+ content_type: file.type,
+ byte_size: file.size,
+ checksum: checksum
+ };
+ this.xhr = new XMLHttpRequest();
+ this.xhr.open("POST", url, true);
+ this.xhr.responseType = "json";
+ this.xhr.setRequestHeader("Content-Type", "application/json");
+ this.xhr.setRequestHeader("Accept", "application/json");
+ this.xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
+ this.xhr.setRequestHeader("X-CSRF-Token", getMetaValue("csrf-token"));
+ this.xhr.addEventListener("load", function(event) {
+ return _this.requestDidLoad(event);
+ });
+ this.xhr.addEventListener("error", function(event) {
+ return _this.requestDidError(event);
+ });
+ }
+ createClass(BlobRecord, [ {
+ key: "create",
+ value: function create(callback) {
+ this.callback = callback;
+ this.xhr.send(JSON.stringify({
+ blob: this.attributes
+ }));
+ }
+ }, {
+ key: "requestDidLoad",
+ value: function requestDidLoad(event) {
+ if (this.status >= 200 && this.status < 300) {
+ var response = this.response;
+ var direct_upload = response.direct_upload;
+ delete response.direct_upload;
+ this.attributes = response;
+ this.directUploadData = direct_upload;
+ this.callback(null, this.toJSON());
+ } else {
+ this.requestDidError(event);
+ }
+ }
+ }, {
+ key: "requestDidError",
+ value: function requestDidError(event) {
+ this.callback('Error creating Blob for "' + this.file.name + '". Status: ' + this.status);
+ }
+ }, {
+ key: "toJSON",
+ value: function toJSON() {
+ var result = {};
+ for (var key in this.attributes) {
+ result[key] = this.attributes[key];
+ }
+ return result;
+ }
+ }, {
+ key: "status",
+ get: function get$$1() {
+ return this.xhr.status;
+ }
+ }, {
+ key: "response",
+ get: function get$$1() {
+ var _xhr = this.xhr, responseType = _xhr.responseType, response = _xhr.response;
+ if (responseType == "json") {
+ return response;
+ } else {
+ return JSON.parse(response);
+ }
+ }
+ } ]);
+ return BlobRecord;
+ }();
+ var BlobUpload = function() {
+ function BlobUpload(blob) {
+ var _this = this;
+ classCallCheck(this, BlobUpload);
+ this.blob = blob;
+ this.file = blob.file;
+ var _blob$directUploadDat = blob.directUploadData, url = _blob$directUploadDat.url, headers = _blob$directUploadDat.headers;
+ this.xhr = new XMLHttpRequest();
+ this.xhr.open("PUT", url, true);
+ this.xhr.responseType = "text";
+ for (var key in headers) {
+ this.xhr.setRequestHeader(key, headers[key]);
+ }
+ this.xhr.addEventListener("load", function(event) {
+ return _this.requestDidLoad(event);
+ });
+ this.xhr.addEventListener("error", function(event) {
+ return _this.requestDidError(event);
+ });
+ }
+ createClass(BlobUpload, [ {
+ key: "create",
+ value: function create(callback) {
+ this.callback = callback;
+ this.xhr.send(this.file.slice());
+ }
+ }, {
+ key: "requestDidLoad",
+ value: function requestDidLoad(event) {
+ var _xhr = this.xhr, status = _xhr.status, response = _xhr.response;
+ if (status >= 200 && status < 300) {
+ this.callback(null, response);
+ } else {
+ this.requestDidError(event);
+ }
+ }
+ }, {
+ key: "requestDidError",
+ value: function requestDidError(event) {
+ this.callback('Error storing "' + this.file.name + '". Status: ' + this.xhr.status);
+ }
+ } ]);
+ return BlobUpload;
+ }();
+ var id = 0;
+ var DirectUpload = function() {
+ function DirectUpload(file, url, delegate) {
+ classCallCheck(this, DirectUpload);
+ this.id = ++id;
+ this.file = file;
+ this.url = url;
+ this.delegate = delegate;
+ }
+ createClass(DirectUpload, [ {
+ key: "create",
+ value: function create(callback) {
+ var _this = this;
+ FileChecksum.create(this.file, function(error, checksum) {
+ if (error) {
+ callback(error);
+ return;
+ }
+ var blob = new BlobRecord(_this.file, checksum, _this.url);
+ notify(_this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr);
+ blob.create(function(error) {
+ if (error) {
+ callback(error);
+ } else {
+ var upload = new BlobUpload(blob);
+ notify(_this.delegate, "directUploadWillStoreFileWithXHR", upload.xhr);
+ upload.create(function(error) {
+ if (error) {
+ callback(error);
+ } else {
+ callback(null, blob.toJSON());
+ }
+ });
+ }
+ });
+ });
+ }
+ } ]);
+ return DirectUpload;
+ }();
+ function notify(object, methodName) {
+ if (object && typeof object[methodName] == "function") {
+ for (var _len = arguments.length, messages = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
+ messages[_key - 2] = arguments[_key];
+ }
+ return object[methodName].apply(object, messages);
+ }
+ }
+ var DirectUploadController = function() {
+ function DirectUploadController(input, file) {
+ classCallCheck(this, DirectUploadController);
+ this.input = input;
+ this.file = file;
+ this.directUpload = new DirectUpload(this.file, this.url, this);
+ this.dispatch("initialize");
+ }
+ createClass(DirectUploadController, [ {
+ key: "start",
+ value: function start(callback) {
+ var _this = this;
+ var hiddenInput = document.createElement("input");
+ hiddenInput.type = "hidden";
+ hiddenInput.name = this.input.name;
+ this.input.insertAdjacentElement("beforebegin", hiddenInput);
+ this.dispatch("start");
+ this.directUpload.create(function(error, attributes) {
+ if (error) {
+ hiddenInput.parentNode.removeChild(hiddenInput);
+ _this.dispatchError(error);
+ } else {
+ hiddenInput.value = attributes.signed_id;
+ }
+ _this.dispatch("end");
+ callback(error);
+ });
+ }
+ }, {
+ key: "uploadRequestDidProgress",
+ value: function uploadRequestDidProgress(event) {
+ var progress = event.loaded / event.total * 100;
+ if (progress) {
+ this.dispatch("progress", {
+ progress: progress
+ });
+ }
+ }
+ }, {
+ key: "dispatch",
+ value: function dispatch(name) {
+ var detail = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
+ detail.file = this.file;
+ detail.id = this.directUpload.id;
+ return dispatchEvent(this.input, "direct-upload:" + name, {
+ detail: detail
+ });
+ }
+ }, {
+ key: "dispatchError",
+ value: function dispatchError(error) {
+ var event = this.dispatch("error", {
+ error: error
+ });
+ if (!event.defaultPrevented) {
+ alert(error);
+ }
+ }
+ }, {
+ key: "directUploadWillCreateBlobWithXHR",
+ value: function directUploadWillCreateBlobWithXHR(xhr) {
+ this.dispatch("before-blob-request", {
+ xhr: xhr
+ });
+ }
+ }, {
+ key: "directUploadWillStoreFileWithXHR",
+ value: function directUploadWillStoreFileWithXHR(xhr) {
+ var _this2 = this;
+ this.dispatch("before-storage-request", {
+ xhr: xhr
+ });
+ xhr.upload.addEventListener("progress", function(event) {
+ return _this2.uploadRequestDidProgress(event);
+ });
+ }
+ }, {
+ key: "url",
+ get: function get$$1() {
+ return this.input.getAttribute("data-direct-upload-url");
+ }
+ } ]);
+ return DirectUploadController;
+ }();
+ var inputSelector = "input[type=file][data-direct-upload-url]:not([disabled])";
+ var DirectUploadsController = function() {
+ function DirectUploadsController(form) {
+ classCallCheck(this, DirectUploadsController);
+ this.form = form;
+ this.inputs = findElements(form, inputSelector).filter(function(input) {
+ return input.files.length;
+ });
+ }
+ createClass(DirectUploadsController, [ {
+ key: "start",
+ value: function start(callback) {
+ var _this = this;
+ var controllers = this.createDirectUploadControllers();
+ var startNextController = function startNextController() {
+ var controller = controllers.shift();
+ if (controller) {
+ controller.start(function(error) {
+ if (error) {
+ callback(error);
+ _this.dispatch("end");
+ } else {
+ startNextController();
+ }
+ });
+ } else {
+ callback();
+ _this.dispatch("end");
+ }
+ };
+ this.dispatch("start");
+ startNextController();
+ }
+ }, {
+ key: "createDirectUploadControllers",
+ value: function createDirectUploadControllers() {
+ var controllers = [];
+ this.inputs.forEach(function(input) {
+ toArray$1(input.files).forEach(function(file) {
+ var controller = new DirectUploadController(input, file);
+ controllers.push(controller);
+ });
+ });
+ return controllers;
+ }
+ }, {
+ key: "dispatch",
+ value: function dispatch(name) {
+ var detail = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
+ return dispatchEvent(this.form, "direct-uploads:" + name, {
+ detail: detail
+ });
+ }
+ } ]);
+ return DirectUploadsController;
+ }();
+ var processingAttribute = "data-direct-uploads-processing";
+ var submitButtonsByForm = new WeakMap();
+ var started = false;
+ function start() {
+ if (!started) {
+ started = true;
+ document.addEventListener("click", didClick, true);
+ document.addEventListener("submit", didSubmitForm);
+ document.addEventListener("ajax:before", didSubmitRemoteElement);
+ }
+ }
+ function didClick(event) {
+ var target = event.target;
+ if (target.tagName == "INPUT" && target.type == "submit" && target.form) {
+ submitButtonsByForm.set(target.form, target);
+ }
+ }
+ function didSubmitForm(event) {
+ handleFormSubmissionEvent(event);
+ }
+ function didSubmitRemoteElement(event) {
+ if (event.target.tagName == "FORM") {
+ handleFormSubmissionEvent(event);
+ }
+ }
+ function handleFormSubmissionEvent(event) {
+ var form = event.target;
+ if (form.hasAttribute(processingAttribute)) {
+ event.preventDefault();
+ return;
+ }
+ var controller = new DirectUploadsController(form);
+ var inputs = controller.inputs;
+ if (inputs.length) {
+ event.preventDefault();
+ form.setAttribute(processingAttribute, "");
+ inputs.forEach(disable);
+ controller.start(function(error) {
+ form.removeAttribute(processingAttribute);
+ if (error) {
+ inputs.forEach(enable);
+ } else {
+ submitForm(form);
+ }
+ });
+ }
+ }
+ function submitForm(form) {
+ var button = submitButtonsByForm.get(form) || findElement(form, "input[type=submit]");
+ if (button) {
+ var _button = button, disabled = _button.disabled;
+ button.disabled = false;
+ button.focus();
+ button.click();
+ button.disabled = disabled;
+ } else {
+ button = document.createElement("input");
+ button.type = "submit";
+ button.style.display = "none";
+ form.appendChild(button);
+ button.click();
+ form.removeChild(button);
+ }
+ submitButtonsByForm.delete(form);
+ }
+ function disable(input) {
+ input.disabled = true;
+ }
+ function enable(input) {
+ input.disabled = false;
+ }
+ function autostart() {
+ if (window.ActiveStorage) {
+ start();
+ }
+ }
+ setTimeout(autostart, 1);
+ exports.start = start;
+ exports.DirectUpload = DirectUpload;
+ Object.defineProperty(exports, "__esModule", {
+ value: true
+ });
+});
diff --git a/activestorage/app/controllers/active_storage/base_controller.rb b/activestorage/app/controllers/active_storage/base_controller.rb
new file mode 100644
index 0000000000..b27d2bd8aa
--- /dev/null
+++ b/activestorage/app/controllers/active_storage/base_controller.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+# The base class for all Active Storage controllers.
+class ActiveStorage::BaseController < ActionController::Base
+ include ActiveStorage::SetCurrent
+
+ protect_from_forgery with: :exception
+end
diff --git a/activestorage/app/controllers/active_storage/blobs_controller.rb b/activestorage/app/controllers/active_storage/blobs_controller.rb
new file mode 100644
index 0000000000..4fc3fbe824
--- /dev/null
+++ b/activestorage/app/controllers/active_storage/blobs_controller.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+# Take a signed permanent reference for a blob and turn it into an expiring service URL for download.
+# Note: These URLs are publicly accessible. If you need to enforce access protection beyond the
+# security-through-obscurity factor of the signed blob references, you'll need to implement your own
+# authenticated redirection controller.
+class ActiveStorage::BlobsController < ActiveStorage::BaseController
+ include ActiveStorage::SetBlob
+
+ def show
+ expires_in ActiveStorage.service_urls_expire_in
+ redirect_to @blob.service_url(disposition: params[:disposition])
+ end
+end
diff --git a/activestorage/app/controllers/active_storage/direct_uploads_controller.rb b/activestorage/app/controllers/active_storage/direct_uploads_controller.rb
new file mode 100644
index 0000000000..78b43fc94c
--- /dev/null
+++ b/activestorage/app/controllers/active_storage/direct_uploads_controller.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+# Creates a new blob on the server side in anticipation of a direct-to-service upload from the client side.
+# When the client-side upload is completed, the signed_blob_id can be submitted as part of the form to reference
+# the blob that was created up front.
+class ActiveStorage::DirectUploadsController < ActiveStorage::BaseController
+ def create
+ blob = ActiveStorage::Blob.create_before_direct_upload!(blob_args)
+ render json: direct_upload_json(blob)
+ end
+
+ private
+ def blob_args
+ params.require(:blob).permit(:filename, :byte_size, :checksum, :content_type, :metadata).to_h.symbolize_keys
+ end
+
+ def direct_upload_json(blob)
+ blob.as_json(root: false, methods: :signed_id).merge(direct_upload: {
+ url: blob.service_url_for_direct_upload,
+ headers: blob.service_headers_for_direct_upload
+ })
+ end
+end
diff --git a/activestorage/app/controllers/active_storage/disk_controller.rb b/activestorage/app/controllers/active_storage/disk_controller.rb
new file mode 100644
index 0000000000..99982202dd
--- /dev/null
+++ b/activestorage/app/controllers/active_storage/disk_controller.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+# Serves files stored with the disk service in the same way that the cloud services do.
+# This means using expiring, signed URLs that are meant for immediate access, not permanent linking.
+# Always go through the BlobsController, or your own authenticated controller, rather than directly
+# to the service url.
+class ActiveStorage::DiskController < ActiveStorage::BaseController
+ skip_forgery_protection
+
+ def show
+ if key = decode_verified_key
+ serve_file disk_service.path_for(key), content_type: params[:content_type], disposition: params[:disposition]
+ else
+ head :not_found
+ end
+ rescue Errno::ENOENT
+ head :not_found
+ end
+
+ def update
+ if token = decode_verified_token
+ if acceptable_content?(token)
+ disk_service.upload token[:key], request.body, checksum: token[:checksum]
+ else
+ head :unprocessable_entity
+ end
+ else
+ head :not_found
+ end
+ rescue ActiveStorage::IntegrityError
+ head :unprocessable_entity
+ end
+
+ private
+ def disk_service
+ ActiveStorage::Blob.service
+ end
+
+
+ def decode_verified_key
+ ActiveStorage.verifier.verified(params[:encoded_key], purpose: :blob_key)
+ end
+
+ def serve_file(path, content_type:, disposition:)
+ Rack::File.new(nil).serving(request, path).tap do |(status, headers, body)|
+ self.status = status
+ self.response_body = body
+
+ headers.each do |name, value|
+ response.headers[name] = value
+ end
+
+ response.headers["Content-Type"] = content_type || DEFAULT_SEND_FILE_TYPE
+ response.headers["Content-Disposition"] = disposition || DEFAULT_SEND_FILE_DISPOSITION
+ end
+ end
+
+
+ def decode_verified_token
+ ActiveStorage.verifier.verified(params[:encoded_token], purpose: :blob_token)
+ end
+
+ def acceptable_content?(token)
+ token[:content_type] == request.content_mime_type && token[:content_length] == request.content_length
+ end
+end
diff --git a/activestorage/app/controllers/active_storage/representations_controller.rb b/activestorage/app/controllers/active_storage/representations_controller.rb
new file mode 100644
index 0000000000..98e11e5dbb
--- /dev/null
+++ b/activestorage/app/controllers/active_storage/representations_controller.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+# Take a signed permanent reference for a blob representation and turn it into an expiring service URL for download.
+# Note: These URLs are publicly accessible. If you need to enforce access protection beyond the
+# security-through-obscurity factor of the signed blob and variation reference, you'll need to implement your own
+# authenticated redirection controller.
+class ActiveStorage::RepresentationsController < ActiveStorage::BaseController
+ include ActiveStorage::SetBlob
+
+ def show
+ expires_in ActiveStorage.service_urls_expire_in
+ redirect_to @blob.representation(params[:variation_key]).processed.service_url(disposition: params[:disposition])
+ end
+end
diff --git a/activestorage/app/controllers/concerns/active_storage/set_blob.rb b/activestorage/app/controllers/concerns/active_storage/set_blob.rb
new file mode 100644
index 0000000000..f072954d78
--- /dev/null
+++ b/activestorage/app/controllers/concerns/active_storage/set_blob.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module ActiveStorage::SetBlob #:nodoc:
+ extend ActiveSupport::Concern
+
+ included do
+ before_action :set_blob
+ end
+
+ private
+ def set_blob
+ @blob = ActiveStorage::Blob.find_signed(params[:signed_blob_id] || params[:signed_id])
+ rescue ActiveSupport::MessageVerifier::InvalidSignature
+ head :not_found
+ end
+end
diff --git a/activestorage/app/controllers/concerns/active_storage/set_current.rb b/activestorage/app/controllers/concerns/active_storage/set_current.rb
new file mode 100644
index 0000000000..597afe7064
--- /dev/null
+++ b/activestorage/app/controllers/concerns/active_storage/set_current.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+# Sets the <tt>ActiveStorage::Current.host</tt> attribute, which the disk service uses to generate URLs.
+# Include this concern in custom controllers that call ActiveStorage::Blob#service_url,
+# ActiveStorage::Variant#service_url, or ActiveStorage::Preview#service_url so the disk service can
+# generate URLs using the same host, protocol, and base path as the current request.
+module ActiveStorage::SetCurrent
+ extend ActiveSupport::Concern
+
+ included do
+ before_action do
+ ActiveStorage::Current.host = request.base_url
+ end
+ end
+end
diff --git a/activestorage/app/javascript/activestorage/blob_record.js b/activestorage/app/javascript/activestorage/blob_record.js
new file mode 100644
index 0000000000..ff847892b2
--- /dev/null
+++ b/activestorage/app/javascript/activestorage/blob_record.js
@@ -0,0 +1,68 @@
+import { getMetaValue } from "./helpers"
+
+export class BlobRecord {
+ constructor(file, checksum, url) {
+ this.file = file
+
+ this.attributes = {
+ filename: file.name,
+ content_type: file.type,
+ byte_size: file.size,
+ checksum: checksum
+ }
+
+ this.xhr = new XMLHttpRequest
+ this.xhr.open("POST", url, true)
+ this.xhr.responseType = "json"
+ this.xhr.setRequestHeader("Content-Type", "application/json")
+ this.xhr.setRequestHeader("Accept", "application/json")
+ this.xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest")
+ this.xhr.setRequestHeader("X-CSRF-Token", getMetaValue("csrf-token"))
+ this.xhr.addEventListener("load", event => this.requestDidLoad(event))
+ this.xhr.addEventListener("error", event => this.requestDidError(event))
+ }
+
+ get status() {
+ return this.xhr.status
+ }
+
+ get response() {
+ const { responseType, response } = this.xhr
+ if (responseType == "json") {
+ return response
+ } else {
+ // Shim for IE 11: https://connect.microsoft.com/IE/feedback/details/794808
+ return JSON.parse(response)
+ }
+ }
+
+ create(callback) {
+ this.callback = callback
+ this.xhr.send(JSON.stringify({ blob: this.attributes }))
+ }
+
+ requestDidLoad(event) {
+ if (this.status >= 200 && this.status < 300) {
+ const { response } = this
+ const { direct_upload } = response
+ delete response.direct_upload
+ this.attributes = response
+ this.directUploadData = direct_upload
+ this.callback(null, this.toJSON())
+ } else {
+ this.requestDidError(event)
+ }
+ }
+
+ requestDidError(event) {
+ this.callback(`Error creating Blob for "${this.file.name}". Status: ${this.status}`)
+ }
+
+ toJSON() {
+ const result = {}
+ for (const key in this.attributes) {
+ result[key] = this.attributes[key]
+ }
+ return result
+ }
+}
diff --git a/activestorage/app/javascript/activestorage/blob_upload.js b/activestorage/app/javascript/activestorage/blob_upload.js
new file mode 100644
index 0000000000..277cc8ff8e
--- /dev/null
+++ b/activestorage/app/javascript/activestorage/blob_upload.js
@@ -0,0 +1,35 @@
+export class BlobUpload {
+ constructor(blob) {
+ this.blob = blob
+ this.file = blob.file
+
+ const { url, headers } = blob.directUploadData
+
+ this.xhr = new XMLHttpRequest
+ this.xhr.open("PUT", url, true)
+ this.xhr.responseType = "text"
+ for (const key in headers) {
+ this.xhr.setRequestHeader(key, headers[key])
+ }
+ this.xhr.addEventListener("load", event => this.requestDidLoad(event))
+ this.xhr.addEventListener("error", event => this.requestDidError(event))
+ }
+
+ create(callback) {
+ this.callback = callback
+ this.xhr.send(this.file.slice())
+ }
+
+ requestDidLoad(event) {
+ const { status, response } = this.xhr
+ if (status >= 200 && status < 300) {
+ this.callback(null, response)
+ } else {
+ this.requestDidError(event)
+ }
+ }
+
+ requestDidError(event) {
+ this.callback(`Error storing "${this.file.name}". Status: ${this.xhr.status}`)
+ }
+}
diff --git a/activestorage/app/javascript/activestorage/direct_upload.js b/activestorage/app/javascript/activestorage/direct_upload.js
new file mode 100644
index 0000000000..c2eedf289b
--- /dev/null
+++ b/activestorage/app/javascript/activestorage/direct_upload.js
@@ -0,0 +1,48 @@
+import { FileChecksum } from "./file_checksum"
+import { BlobRecord } from "./blob_record"
+import { BlobUpload } from "./blob_upload"
+
+let id = 0
+
+export class DirectUpload {
+ constructor(file, url, delegate) {
+ this.id = ++id
+ this.file = file
+ this.url = url
+ this.delegate = delegate
+ }
+
+ create(callback) {
+ FileChecksum.create(this.file, (error, checksum) => {
+ if (error) {
+ callback(error)
+ return
+ }
+
+ const blob = new BlobRecord(this.file, checksum, this.url)
+ notify(this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr)
+
+ blob.create(error => {
+ if (error) {
+ callback(error)
+ } else {
+ const upload = new BlobUpload(blob)
+ notify(this.delegate, "directUploadWillStoreFileWithXHR", upload.xhr)
+ upload.create(error => {
+ if (error) {
+ callback(error)
+ } else {
+ callback(null, blob.toJSON())
+ }
+ })
+ }
+ })
+ })
+ }
+}
+
+function notify(object, methodName, ...messages) {
+ if (object && typeof object[methodName] == "function") {
+ return object[methodName](...messages)
+ }
+}
diff --git a/activestorage/app/javascript/activestorage/direct_upload_controller.js b/activestorage/app/javascript/activestorage/direct_upload_controller.js
new file mode 100644
index 0000000000..987050889a
--- /dev/null
+++ b/activestorage/app/javascript/activestorage/direct_upload_controller.js
@@ -0,0 +1,67 @@
+import { DirectUpload } from "./direct_upload"
+import { dispatchEvent } from "./helpers"
+
+export class DirectUploadController {
+ constructor(input, file) {
+ this.input = input
+ this.file = file
+ this.directUpload = new DirectUpload(this.file, this.url, this)
+ this.dispatch("initialize")
+ }
+
+ start(callback) {
+ const hiddenInput = document.createElement("input")
+ hiddenInput.type = "hidden"
+ hiddenInput.name = this.input.name
+ this.input.insertAdjacentElement("beforebegin", hiddenInput)
+
+ this.dispatch("start")
+
+ this.directUpload.create((error, attributes) => {
+ if (error) {
+ hiddenInput.parentNode.removeChild(hiddenInput)
+ this.dispatchError(error)
+ } else {
+ hiddenInput.value = attributes.signed_id
+ }
+
+ this.dispatch("end")
+ callback(error)
+ })
+ }
+
+ uploadRequestDidProgress(event) {
+ const progress = event.loaded / event.total * 100
+ if (progress) {
+ this.dispatch("progress", { progress })
+ }
+ }
+
+ get url() {
+ return this.input.getAttribute("data-direct-upload-url")
+ }
+
+ dispatch(name, detail = {}) {
+ detail.file = this.file
+ detail.id = this.directUpload.id
+ return dispatchEvent(this.input, `direct-upload:${name}`, { detail })
+ }
+
+ dispatchError(error) {
+ const event = this.dispatch("error", { error })
+ if (!event.defaultPrevented) {
+ alert(error)
+ }
+ }
+
+ // DirectUpload delegate
+
+ directUploadWillCreateBlobWithXHR(xhr) {
+ this.dispatch("before-blob-request", { xhr })
+ }
+
+ directUploadWillStoreFileWithXHR(xhr) {
+ this.dispatch("before-storage-request", { xhr })
+ xhr.upload.addEventListener("progress", event => this.uploadRequestDidProgress(event))
+ }
+}
diff --git a/activestorage/app/javascript/activestorage/direct_uploads_controller.js b/activestorage/app/javascript/activestorage/direct_uploads_controller.js
new file mode 100644
index 0000000000..94b89c9119
--- /dev/null
+++ b/activestorage/app/javascript/activestorage/direct_uploads_controller.js
@@ -0,0 +1,50 @@
+import { DirectUploadController } from "./direct_upload_controller"
+import { findElements, dispatchEvent, toArray } from "./helpers"
+
+const inputSelector = "input[type=file][data-direct-upload-url]:not([disabled])"
+
+export class DirectUploadsController {
+ constructor(form) {
+ this.form = form
+ this.inputs = findElements(form, inputSelector).filter(input => input.files.length)
+ }
+
+ start(callback) {
+ const controllers = this.createDirectUploadControllers()
+
+ const startNextController = () => {
+ const controller = controllers.shift()
+ if (controller) {
+ controller.start(error => {
+ if (error) {
+ callback(error)
+ this.dispatch("end")
+ } else {
+ startNextController()
+ }
+ })
+ } else {
+ callback()
+ this.dispatch("end")
+ }
+ }
+
+ this.dispatch("start")
+ startNextController()
+ }
+
+ createDirectUploadControllers() {
+ const controllers = []
+ this.inputs.forEach(input => {
+ toArray(input.files).forEach(file => {
+ const controller = new DirectUploadController(input, file)
+ controllers.push(controller)
+ })
+ })
+ return controllers
+ }
+
+ dispatch(name, detail = {}) {
+ return dispatchEvent(this.form, `direct-uploads:${name}`, { detail })
+ }
+}
diff --git a/activestorage/app/javascript/activestorage/file_checksum.js b/activestorage/app/javascript/activestorage/file_checksum.js
new file mode 100644
index 0000000000..a9dbef69ea
--- /dev/null
+++ b/activestorage/app/javascript/activestorage/file_checksum.js
@@ -0,0 +1,53 @@
+import SparkMD5 from "spark-md5"
+
+const fileSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
+
+export class FileChecksum {
+ static create(file, callback) {
+ const instance = new FileChecksum(file)
+ instance.create(callback)
+ }
+
+ constructor(file) {
+ this.file = file
+ this.chunkSize = 2097152 // 2MB
+ this.chunkCount = Math.ceil(this.file.size / this.chunkSize)
+ this.chunkIndex = 0
+ }
+
+ create(callback) {
+ this.callback = callback
+ this.md5Buffer = new SparkMD5.ArrayBuffer
+ this.fileReader = new FileReader
+ this.fileReader.addEventListener("load", event => this.fileReaderDidLoad(event))
+ this.fileReader.addEventListener("error", event => this.fileReaderDidError(event))
+ this.readNextChunk()
+ }
+
+ fileReaderDidLoad(event) {
+ this.md5Buffer.append(event.target.result)
+
+ if (!this.readNextChunk()) {
+ const binaryDigest = this.md5Buffer.end(true)
+ const base64digest = btoa(binaryDigest)
+ this.callback(null, base64digest)
+ }
+ }
+
+ fileReaderDidError(event) {
+ this.callback(`Error reading ${this.file.name}`)
+ }
+
+ readNextChunk() {
+ if (this.chunkIndex < this.chunkCount || (this.chunkIndex == 0 && this.chunkCount == 0)) {
+ const start = this.chunkIndex * this.chunkSize
+ const end = Math.min(start + this.chunkSize, this.file.size)
+ const bytes = fileSlice.call(this.file, start, end)
+ this.fileReader.readAsArrayBuffer(bytes)
+ this.chunkIndex++
+ return true
+ } else {
+ return false
+ }
+ }
+}
diff --git a/activestorage/app/javascript/activestorage/helpers.js b/activestorage/app/javascript/activestorage/helpers.js
new file mode 100644
index 0000000000..7e83c447e7
--- /dev/null
+++ b/activestorage/app/javascript/activestorage/helpers.js
@@ -0,0 +1,51 @@
+export function getMetaValue(name) {
+ const element = findElement(document.head, `meta[name="${name}"]`)
+ if (element) {
+ return element.getAttribute("content")
+ }
+}
+
+export function findElements(root, selector) {
+ if (typeof root == "string") {
+ selector = root
+ root = document
+ }
+ const elements = root.querySelectorAll(selector)
+ return toArray(elements)
+}
+
+export function findElement(root, selector) {
+ if (typeof root == "string") {
+ selector = root
+ root = document
+ }
+ return root.querySelector(selector)
+}
+
+export function dispatchEvent(element, type, eventInit = {}) {
+ const { disabled } = element
+ const { bubbles, cancelable, detail } = eventInit
+ const event = document.createEvent("Event")
+
+ event.initEvent(type, bubbles || true, cancelable || true)
+ event.detail = detail || {}
+
+ try {
+ element.disabled = false
+ element.dispatchEvent(event)
+ } finally {
+ element.disabled = disabled
+ }
+
+ return event
+}
+
+export function toArray(value) {
+ if (Array.isArray(value)) {
+ return value
+ } else if (Array.from) {
+ return Array.from(value)
+ } else {
+ return [].slice.call(value)
+ }
+}
diff --git a/activestorage/app/javascript/activestorage/index.js b/activestorage/app/javascript/activestorage/index.js
new file mode 100644
index 0000000000..a340008fb9
--- /dev/null
+++ b/activestorage/app/javascript/activestorage/index.js
@@ -0,0 +1,11 @@
+import { start } from "./ujs"
+import { DirectUpload } from "./direct_upload"
+export { start, DirectUpload }
+
+function autostart() {
+ if (window.ActiveStorage) {
+ start()
+ }
+}
+
+setTimeout(autostart, 1)
diff --git a/activestorage/app/javascript/activestorage/ujs.js b/activestorage/app/javascript/activestorage/ujs.js
new file mode 100644
index 0000000000..f5353389ef
--- /dev/null
+++ b/activestorage/app/javascript/activestorage/ujs.js
@@ -0,0 +1,86 @@
+import { DirectUploadsController } from "./direct_uploads_controller"
+import { findElement } from "./helpers"
+
+const processingAttribute = "data-direct-uploads-processing"
+const submitButtonsByForm = new WeakMap
+let started = false
+
+export function start() {
+ if (!started) {
+ started = true
+ document.addEventListener("click", didClick, true)
+ document.addEventListener("submit", didSubmitForm)
+ document.addEventListener("ajax:before", didSubmitRemoteElement)
+ }
+}
+
+function didClick(event) {
+ const { target } = event
+ if (target.tagName == "INPUT" && target.type == "submit" && target.form) {
+ submitButtonsByForm.set(target.form, target)
+ }
+}
+
+function didSubmitForm(event) {
+ handleFormSubmissionEvent(event)
+}
+
+function didSubmitRemoteElement(event) {
+ if (event.target.tagName == "FORM") {
+ handleFormSubmissionEvent(event)
+ }
+}
+
+function handleFormSubmissionEvent(event) {
+ const form = event.target
+
+ if (form.hasAttribute(processingAttribute)) {
+ event.preventDefault()
+ return
+ }
+
+ const controller = new DirectUploadsController(form)
+ const { inputs } = controller
+
+ if (inputs.length) {
+ event.preventDefault()
+ form.setAttribute(processingAttribute, "")
+ inputs.forEach(disable)
+ controller.start(error => {
+ form.removeAttribute(processingAttribute)
+ if (error) {
+ inputs.forEach(enable)
+ } else {
+ submitForm(form)
+ }
+ })
+ }
+}
+
+function submitForm(form) {
+ let button = submitButtonsByForm.get(form) || findElement(form, "input[type=submit]")
+
+ if (button) {
+ const { disabled } = button
+ button.disabled = false
+ button.focus()
+ button.click()
+ button.disabled = disabled
+ } else {
+ button = document.createElement("input")
+ button.type = "submit"
+ button.style.display = "none"
+ form.appendChild(button)
+ button.click()
+ form.removeChild(button)
+ }
+ submitButtonsByForm.delete(form)
+}
+
+function disable(input) {
+ input.disabled = true
+}
+
+function enable(input) {
+ input.disabled = false
+}
diff --git a/activestorage/app/jobs/active_storage/analyze_job.rb b/activestorage/app/jobs/active_storage/analyze_job.rb
new file mode 100644
index 0000000000..804ee4557a
--- /dev/null
+++ b/activestorage/app/jobs/active_storage/analyze_job.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+# Provides asynchronous analysis of ActiveStorage::Blob records via ActiveStorage::Blob#analyze_later.
+class ActiveStorage::AnalyzeJob < ActiveStorage::BaseJob
+ retry_on ActiveStorage::IntegrityError, attempts: 10, wait: :exponentially_longer
+
+ def perform(blob)
+ blob.analyze
+ end
+end
diff --git a/activestorage/app/jobs/active_storage/base_job.rb b/activestorage/app/jobs/active_storage/base_job.rb
new file mode 100644
index 0000000000..6caab42a2d
--- /dev/null
+++ b/activestorage/app/jobs/active_storage/base_job.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class ActiveStorage::BaseJob < ActiveJob::Base
+ queue_as { ActiveStorage.queue }
+end
diff --git a/activestorage/app/jobs/active_storage/purge_job.rb b/activestorage/app/jobs/active_storage/purge_job.rb
new file mode 100644
index 0000000000..2604977bf1
--- /dev/null
+++ b/activestorage/app/jobs/active_storage/purge_job.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+# Provides asynchronous purging of ActiveStorage::Blob records via ActiveStorage::Blob#purge_later.
+class ActiveStorage::PurgeJob < ActiveStorage::BaseJob
+ discard_on ActiveRecord::RecordNotFound
+ retry_on ActiveRecord::Deadlocked, attempts: 10, wait: :exponentially_longer
+
+ def perform(blob)
+ blob.purge
+ end
+end
diff --git a/activestorage/app/models/active_storage/attachment.rb b/activestorage/app/models/active_storage/attachment.rb
new file mode 100644
index 0000000000..13758d9179
--- /dev/null
+++ b/activestorage/app/models/active_storage/attachment.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/delegation"
+
+# Attachments associate records with blobs. Usually that's a one record-many blobs relationship,
+# but it is possible to associate many different records with the same blob. A foreign-key constraint
+# on the attachments table prevents blobs from being purged if they’re still attached to any records.
+class ActiveStorage::Attachment < ActiveRecord::Base
+ self.table_name = "active_storage_attachments"
+
+ belongs_to :record, polymorphic: true, touch: true
+ belongs_to :blob, class_name: "ActiveStorage::Blob"
+
+ delegate_missing_to :blob
+
+ after_create_commit :analyze_blob_later, :identify_blob
+ after_destroy_commit :purge_dependent_blob_later
+
+ # Synchronously deletes the attachment and {purges the blob}[rdoc-ref:ActiveStorage::Blob#purge].
+ def purge
+ delete
+ blob&.purge
+ end
+
+ # Deletes the attachment and {enqueues a background job}[rdoc-ref:ActiveStorage::Blob#purge_later] to purge the blob.
+ def purge_later
+ delete
+ blob&.purge_later
+ end
+
+ private
+ def identify_blob
+ blob.identify
+ end
+
+ def analyze_blob_later
+ blob.analyze_later unless blob.analyzed?
+ end
+
+ def purge_dependent_blob_later
+ blob&.purge_later if dependent == :purge_later
+ end
+
+
+ def dependent
+ record.attachment_reflections[name]&.options[:dependent]
+ end
+end
diff --git a/activestorage/app/models/active_storage/blob.rb b/activestorage/app/models/active_storage/blob.rb
new file mode 100644
index 0000000000..53aa9f0237
--- /dev/null
+++ b/activestorage/app/models/active_storage/blob.rb
@@ -0,0 +1,243 @@
+# frozen_string_literal: true
+
+require "active_storage/downloader"
+
+# A blob is a record that contains the metadata about a file and a key for where that file resides on the service.
+# Blobs can be created in two ways:
+#
+# 1. Subsequent to the file being uploaded server-side to the service via <tt>create_after_upload!</tt>.
+# 2. Ahead of the file being directly uploaded client-side to the service via <tt>create_before_direct_upload!</tt>.
+#
+# The first option doesn't require any client-side JavaScript integration, and can be used by any other back-end
+# service that deals with files. The second option is faster, since you're not using your own server as a staging
+# point for uploads, and can work with deployments like Heroku that do not provide large amounts of disk space.
+#
+# Blobs are intended to be immutable in as-so-far as their reference to a specific file goes. You're allowed to
+# update a blob's metadata on a subsequent pass, but you should not update the key or change the uploaded file.
+# If you need to create a derivative or otherwise change the blob, simply create a new blob and purge the old one.
+class ActiveStorage::Blob < ActiveRecord::Base
+ require_dependency "active_storage/blob/analyzable"
+ require_dependency "active_storage/blob/identifiable"
+ require_dependency "active_storage/blob/representable"
+
+ include Analyzable
+ include Identifiable
+ include Representable
+
+ self.table_name = "active_storage_blobs"
+
+ has_secure_token :key
+ store :metadata, accessors: [ :analyzed, :identified ], coder: ActiveRecord::Coders::JSON
+
+ class_attribute :service
+
+ has_many :attachments
+
+ scope :unattached, -> { left_joins(:attachments).where(ActiveStorage::Attachment.table_name => { blob_id: nil }) }
+
+ before_destroy(prepend: true) do
+ raise ActiveRecord::InvalidForeignKey if attachments.exists?
+ end
+
+ class << self
+ # You can used the signed ID of a blob to refer to it on the client side without fear of tampering.
+ # This is particularly helpful for direct uploads where the client-side needs to refer to the blob
+ # that was created ahead of the upload itself on form submission.
+ #
+ # The signed ID is also used to create stable URLs for the blob through the BlobsController.
+ def find_signed(id)
+ find ActiveStorage.verifier.verify(id, purpose: :blob_id)
+ end
+
+ # Returns a new, unsaved blob instance after the +io+ has been uploaded to the service.
+ # When providing a content type, pass <tt>identify: false</tt> to bypass automatic content type inference.
+ def build_after_upload(io:, filename:, content_type: nil, metadata: nil, identify: true)
+ new(filename: filename, content_type: content_type, metadata: metadata).tap do |blob|
+ blob.upload(io, identify: identify)
+ end
+ end
+
+ def build_after_unfurling(io:, filename:, content_type: nil, metadata: nil, identify: true) #:nodoc:
+ new(filename: filename, content_type: content_type, metadata: metadata).tap do |blob|
+ blob.unfurl(io, identify: identify)
+ end
+ end
+
+ # Returns a saved blob instance after the +io+ has been uploaded to the service. Note, the blob is first built,
+ # then the +io+ is uploaded, then the blob is saved. This is done this way to avoid uploading (which may take
+ # time), while having an open database transaction.
+ # When providing a content type, pass <tt>identify: false</tt> to bypass automatic content type inference.
+ def create_after_upload!(io:, filename:, content_type: nil, metadata: nil, identify: true)
+ build_after_upload(io: io, filename: filename, content_type: content_type, metadata: metadata, identify: identify).tap(&:save!)
+ end
+
+ # Returns a saved blob _without_ uploading a file to the service. This blob will point to a key where there is
+ # no file yet. It's intended to be used together with a client-side upload, which will first create the blob
+ # in order to produce the signed URL for uploading. This signed URL points to the key generated by the blob.
+ # Once the form using the direct upload is submitted, the blob can be associated with the right record using
+ # the signed ID.
+ def create_before_direct_upload!(filename:, byte_size:, checksum:, content_type: nil, metadata: nil)
+ create! filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type, metadata: metadata
+ end
+ end
+
+ # Returns a signed ID for this blob that's suitable for reference on the client-side without fear of tampering.
+ # It uses the framework-wide verifier on <tt>ActiveStorage.verifier</tt>, but with a dedicated purpose.
+ def signed_id
+ ActiveStorage.verifier.generate(id, purpose: :blob_id)
+ end
+
+ # Returns the key pointing to the file on the service that's associated with this blob. The key is in the
+ # standard secure-token format from Rails. So it'll look like: XTAPjJCJiuDrLk3TmwyJGpUo. This key is not intended
+ # to be revealed directly to the user. Always refer to blobs using the signed_id or a verified form of the key.
+ def key
+ # We can't wait until the record is first saved to have a key for it
+ self[:key] ||= self.class.generate_unique_secure_token
+ end
+
+ # Returns an ActiveStorage::Filename instance of the filename that can be
+ # queried for basename, extension, and a sanitized version of the filename
+ # that's safe to use in URLs.
+ def filename
+ ActiveStorage::Filename.new(self[:filename])
+ end
+
+ # Returns true if the content_type of this blob is in the image range, like image/png.
+ def image?
+ content_type.start_with?("image")
+ end
+
+ # Returns true if the content_type of this blob is in the audio range, like audio/mpeg.
+ def audio?
+ content_type.start_with?("audio")
+ end
+
+ # Returns true if the content_type of this blob is in the video range, like video/mp4.
+ def video?
+ content_type.start_with?("video")
+ end
+
+ # Returns true if the content_type of this blob is in the text range, like text/plain.
+ def text?
+ content_type.start_with?("text")
+ end
+
+
+ # Returns the URL of the blob on the service. This URL is intended to be short-lived for security and not used directly
+ # with users. Instead, the +service_url+ should only be exposed as a redirect from a stable, possibly authenticated URL.
+ # Hiding the +service_url+ behind a redirect also gives you the power to change services without updating all URLs. And
+ # it allows permanent URLs that redirect to the +service_url+ to be cached in the view.
+ def service_url(expires_in: ActiveStorage.service_urls_expire_in, disposition: :inline, filename: nil, **options)
+ filename = ActiveStorage::Filename.wrap(filename || self.filename)
+
+ service.url key, expires_in: expires_in, filename: filename, content_type: content_type,
+ disposition: forcibly_serve_as_binary? ? :attachment : disposition, **options
+ end
+
+ # Returns a URL that can be used to directly upload a file for this blob on the service. This URL is intended to be
+ # short-lived for security and only generated on-demand by the client-side JavaScript responsible for doing the uploading.
+ def service_url_for_direct_upload(expires_in: ActiveStorage.service_urls_expire_in)
+ service.url_for_direct_upload key, expires_in: expires_in, content_type: content_type, content_length: byte_size, checksum: checksum
+ end
+
+ # Returns a Hash of headers for +service_url_for_direct_upload+ requests.
+ def service_headers_for_direct_upload
+ service.headers_for_direct_upload key, filename: filename, content_type: content_type, content_length: byte_size, checksum: checksum
+ end
+
+
+ # Uploads the +io+ to the service on the +key+ for this blob. Blobs are intended to be immutable, so you shouldn't be
+ # using this method after a file has already been uploaded to fit with a blob. If you want to create a derivative blob,
+ # you should instead simply create a new blob based on the old one.
+ #
+ # Prior to uploading, we compute the checksum, which is sent to the service for transit integrity validation. If the
+ # checksum does not match what the service receives, an exception will be raised. We also measure the size of the +io+
+ # and store that in +byte_size+ on the blob record. The content type is automatically extracted from the +io+ unless
+ # you specify a +content_type+ and pass +identify+ as false.
+ #
+ # Normally, you do not have to call this method directly at all. Use the factory class methods of +build_after_upload+
+ # and +create_after_upload!+.
+ def upload(io, identify: true)
+ unfurl io, identify: identify
+ upload_without_unfurling io
+ end
+
+ def unfurl(io, identify: true) #:nodoc:
+ self.checksum = compute_checksum_in_chunks(io)
+ self.content_type = extract_content_type(io) if content_type.nil? || identify
+ self.byte_size = io.size
+ self.identified = true
+ end
+
+ def upload_without_unfurling(io) #:nodoc:
+ service.upload key, io, checksum: checksum
+ end
+
+ # Downloads the file associated with this blob. If no block is given, the entire file is read into memory and returned.
+ # That'll use a lot of RAM for very large files. If a block is given, then the download is streamed and yielded in chunks.
+ def download(&block)
+ service.download key, &block
+ end
+
+ # Downloads the blob to a tempfile on disk. Yields the tempfile.
+ #
+ # The tempfile's name is prefixed with +ActiveStorage-+ and the blob's ID. Its extension matches that of the blob.
+ #
+ # By default, the tempfile is created in <tt>Dir.tmpdir</tt>. Pass +tempdir:+ to create it in a different directory:
+ #
+ # blob.open(tempdir: "/path/to/tmp") do |file|
+ # # ...
+ # end
+ #
+ # The tempfile is automatically closed and unlinked after the given block is executed.
+ #
+ # Raises ActiveStorage::IntegrityError if the downloaded data does not match the blob's checksum.
+ def open(tempdir: nil, &block)
+ ActiveStorage::Downloader.new(self, tempdir: tempdir).download_blob_to_tempfile(&block)
+ end
+
+
+ # Deletes the files on the service associated with the blob. This should only be done if the blob is going to be
+ # deleted as well or you will essentially have a dead reference. It's recommended to use #purge and #purge_later
+ # methods in most circumstances.
+ def delete
+ service.delete(key)
+ service.delete_prefixed("variants/#{key}/") if image?
+ end
+
+ # Deletes the file on the service and then destroys the blob record. This is the recommended way to dispose of unwanted
+ # blobs. Note, though, that deleting the file off the service will initiate a HTTP connection to the service, which may
+ # be slow or prevented, so you should not use this method inside a transaction or in callbacks. Use #purge_later instead.
+ def purge
+ destroy
+ delete
+ rescue ActiveRecord::InvalidForeignKey
+ end
+
+ # Enqueues an ActiveStorage::PurgeJob to call #purge. This is the recommended way to purge blobs from a transaction,
+ # an Active Record callback, or in any other real-time scenario.
+ def purge_later
+ ActiveStorage::PurgeJob.perform_later(self)
+ end
+
+ private
+ def compute_checksum_in_chunks(io)
+ Digest::MD5.new.tap do |checksum|
+ while chunk = io.read(5.megabytes)
+ checksum << chunk
+ end
+
+ io.rewind
+ end.base64digest
+ end
+
+ def extract_content_type(io)
+ Marcel::MimeType.for io, name: filename.to_s, declared_type: content_type
+ end
+
+ def forcibly_serve_as_binary?
+ ActiveStorage.content_types_to_serve_as_binary.include?(content_type)
+ end
+
+ ActiveSupport.run_load_hooks(:active_storage_blob, self)
+end
diff --git a/activestorage/app/models/active_storage/blob/analyzable.rb b/activestorage/app/models/active_storage/blob/analyzable.rb
new file mode 100644
index 0000000000..5bda6e6d73
--- /dev/null
+++ b/activestorage/app/models/active_storage/blob/analyzable.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require "active_storage/analyzer/null_analyzer"
+
+module ActiveStorage::Blob::Analyzable
+ # Extracts and stores metadata from the file associated with this blob using a relevant analyzer. Active Storage comes
+ # with built-in analyzers for images and videos. See ActiveStorage::Analyzer::ImageAnalyzer and
+ # ActiveStorage::Analyzer::VideoAnalyzer for information about the specific attributes they extract and the third-party
+ # libraries they require.
+ #
+ # To choose the analyzer for a blob, Active Storage calls +accept?+ on each registered analyzer in order. It uses the
+ # first analyzer for which +accept?+ returns true when given the blob. If no registered analyzer accepts the blob, no
+ # metadata is extracted from it.
+ #
+ # In a Rails application, add or remove analyzers by manipulating +Rails.application.config.active_storage.analyzers+
+ # in an initializer:
+ #
+ # # Add a custom analyzer for Microsoft Office documents:
+ # Rails.application.config.active_storage.analyzers.append DOCXAnalyzer
+ #
+ # # Remove the built-in video analyzer:
+ # Rails.application.config.active_storage.analyzers.delete ActiveStorage::Analyzer::VideoAnalyzer
+ #
+ # Outside of a Rails application, manipulate +ActiveStorage.analyzers+ instead.
+ #
+ # You won't ordinarily need to call this method from a Rails application. New blobs are automatically and asynchronously
+ # analyzed via #analyze_later when they're attached for the first time.
+ def analyze
+ update! metadata: metadata.merge(extract_metadata_via_analyzer)
+ end
+
+ # Enqueues an ActiveStorage::AnalyzeJob which calls #analyze.
+ #
+ # This method is automatically called for a blob when it's attached for the first time. You can call it to analyze a blob
+ # again (e.g. if you add a new analyzer or modify an existing one).
+ def analyze_later
+ ActiveStorage::AnalyzeJob.perform_later(self)
+ end
+
+ # Returns true if the blob has been analyzed.
+ def analyzed?
+ analyzed
+ end
+
+ private
+ def extract_metadata_via_analyzer
+ analyzer.metadata.merge(analyzed: true)
+ end
+
+ def analyzer
+ analyzer_class.new(self)
+ end
+
+ def analyzer_class
+ ActiveStorage.analyzers.detect { |klass| klass.accept?(self) } || ActiveStorage::Analyzer::NullAnalyzer
+ end
+end
diff --git a/activestorage/app/models/active_storage/blob/identifiable.rb b/activestorage/app/models/active_storage/blob/identifiable.rb
new file mode 100644
index 0000000000..2c17ddc25f
--- /dev/null
+++ b/activestorage/app/models/active_storage/blob/identifiable.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module ActiveStorage::Blob::Identifiable
+ def identify
+ update! content_type: identify_content_type, identified: true unless identified?
+ end
+
+ def identified?
+ identified
+ end
+
+ private
+ def identify_content_type
+ Marcel::MimeType.for download_identifiable_chunk, name: filename.to_s, declared_type: content_type
+ end
+
+ def download_identifiable_chunk
+ if byte_size.positive?
+ service.download_chunk key, 0...4.kilobytes
+ else
+ ""
+ end
+ end
+end
diff --git a/activestorage/app/models/active_storage/blob/representable.rb b/activestorage/app/models/active_storage/blob/representable.rb
new file mode 100644
index 0000000000..03d5511481
--- /dev/null
+++ b/activestorage/app/models/active_storage/blob/representable.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+module ActiveStorage::Blob::Representable
+ extend ActiveSupport::Concern
+
+ included do
+ has_one_attached :preview_image
+ end
+
+ # Returns an ActiveStorage::Variant instance with the set of +transformations+ provided. This is only relevant for image
+ # files, and it allows any image to be transformed for size, colors, and the like. Example:
+ #
+ # avatar.variant(resize_to_fit: [100, 100]).processed.service_url
+ #
+ # This will create and process a variant of the avatar blob that's constrained to a height and width of 100px.
+ # Then it'll upload said variant to the service according to a derivative key of the blob and the transformations.
+ #
+ # Frequently, though, you don't actually want to transform the variant right away. But rather simply refer to a
+ # specific variant that can be created by a controller on-demand. Like so:
+ #
+ # <%= image_tag Current.user.avatar.variant(resize_to_fit: [100, 100]) %>
+ #
+ # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::RepresentationsController
+ # can then produce on-demand.
+ #
+ # Raises ActiveStorage::InvariableError if ImageMagick cannot transform the blob. To determine whether a blob is
+ # variable, call ActiveStorage::Blob#variable?.
+ def variant(transformations)
+ if variable?
+ ActiveStorage::Variant.new(self, transformations)
+ else
+ raise ActiveStorage::InvariableError
+ end
+ end
+
+ # Returns true if ImageMagick can transform the blob (its content type is in +ActiveStorage.variable_content_types+).
+ def variable?
+ ActiveStorage.variable_content_types.include?(content_type)
+ end
+
+
+ # Returns an ActiveStorage::Preview instance with the set of +transformations+ provided. A preview is an image generated
+ # from a non-image blob. Active Storage comes with built-in previewers for videos and PDF documents. The video previewer
+ # extracts the first frame from a video and the PDF previewer extracts the first page from a PDF document.
+ #
+ # blob.preview(resize_to_fit: [100, 100]).processed.service_url
+ #
+ # Avoid processing previews synchronously in views. Instead, link to a controller action that processes them on demand.
+ # Active Storage provides one, but you may want to create your own (for example, if you need authentication). Here’s
+ # how to use the built-in version:
+ #
+ # <%= image_tag video.preview(resize_to_fit: [100, 100]) %>
+ #
+ # This method raises ActiveStorage::UnpreviewableError if no previewer accepts the receiving blob. To determine
+ # whether a blob is accepted by any previewer, call ActiveStorage::Blob#previewable?.
+ def preview(transformations)
+ if previewable?
+ ActiveStorage::Preview.new(self, transformations)
+ else
+ raise ActiveStorage::UnpreviewableError
+ end
+ end
+
+ # Returns true if any registered previewer accepts the blob. By default, this will return true for videos and PDF documents.
+ def previewable?
+ ActiveStorage.previewers.any? { |klass| klass.accept?(self) }
+ end
+
+
+ # Returns an ActiveStorage::Preview for a previewable blob or an ActiveStorage::Variant for a variable image blob.
+ #
+ # blob.representation(resize_to_fit: [100, 100]).processed.service_url
+ #
+ # Raises ActiveStorage::UnrepresentableError if the receiving blob is neither variable nor previewable. Call
+ # ActiveStorage::Blob#representable? to determine whether a blob is representable.
+ #
+ # See ActiveStorage::Blob#preview and ActiveStorage::Blob#variant for more information.
+ def representation(transformations)
+ case
+ when previewable?
+ preview transformations
+ when variable?
+ variant transformations
+ else
+ raise ActiveStorage::UnrepresentableError
+ end
+ end
+
+ # Returns true if the blob is variable or previewable.
+ def representable?
+ variable? || previewable?
+ end
+end
diff --git a/activestorage/app/models/active_storage/current.rb b/activestorage/app/models/active_storage/current.rb
new file mode 100644
index 0000000000..7e431d8462
--- /dev/null
+++ b/activestorage/app/models/active_storage/current.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class ActiveStorage::Current < ActiveSupport::CurrentAttributes #:nodoc:
+ attribute :host
+end
diff --git a/activestorage/app/models/active_storage/filename.rb b/activestorage/app/models/active_storage/filename.rb
new file mode 100644
index 0000000000..2a03e0173d
--- /dev/null
+++ b/activestorage/app/models/active_storage/filename.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+# Encapsulates a string representing a filename to provide convenient access to parts of it and sanitization.
+# A Filename instance is returned by ActiveStorage::Blob#filename, and is comparable so it can be used for sorting.
+class ActiveStorage::Filename
+ include Comparable
+
+ class << self
+ # Returns a Filename instance based on the given filename. If the filename is a Filename, it is
+ # returned unmodified. If it is a String, it is passed to ActiveStorage::Filename.new.
+ def wrap(filename)
+ filename.kind_of?(self) ? filename : new(filename)
+ end
+ end
+
+ def initialize(filename)
+ @filename = filename
+ end
+
+ # Returns the part of the filename preceding any extension.
+ #
+ # ActiveStorage::Filename.new("racecar.jpg").base # => "racecar"
+ # ActiveStorage::Filename.new("racecar").base # => "racecar"
+ # ActiveStorage::Filename.new(".gitignore").base # => ".gitignore"
+ def base
+ File.basename @filename, extension_with_delimiter
+ end
+
+ # Returns the extension of the filename (i.e. the substring following the last dot, excluding a dot at the
+ # beginning) with the dot that precedes it. If the filename has no extension, an empty string is returned.
+ #
+ # ActiveStorage::Filename.new("racecar.jpg").extension_with_delimiter # => ".jpg"
+ # ActiveStorage::Filename.new("racecar").extension_with_delimiter # => ""
+ # ActiveStorage::Filename.new(".gitignore").extension_with_delimiter # => ""
+ def extension_with_delimiter
+ File.extname @filename
+ end
+
+ # Returns the extension of the filename (i.e. the substring following the last dot, excluding a dot at
+ # the beginning). If the filename has no extension, an empty string is returned.
+ #
+ # ActiveStorage::Filename.new("racecar.jpg").extension_without_delimiter # => "jpg"
+ # ActiveStorage::Filename.new("racecar").extension_without_delimiter # => ""
+ # ActiveStorage::Filename.new(".gitignore").extension_without_delimiter # => ""
+ def extension_without_delimiter
+ extension_with_delimiter.from(1).to_s
+ end
+
+ alias_method :extension, :extension_without_delimiter
+
+ # Returns the sanitized filename.
+ #
+ # ActiveStorage::Filename.new("foo:bar.jpg").sanitized # => "foo-bar.jpg"
+ # ActiveStorage::Filename.new("foo/bar.jpg").sanitized # => "foo-bar.jpg"
+ #
+ # Characters considered unsafe for storage (e.g. \, $, and the RTL override character) are replaced with a dash.
+ def sanitized
+ @filename.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�").strip.tr("\u{202E}%$|:;/\t\r\n\\", "-")
+ end
+
+ # Returns the sanitized version of the filename.
+ def to_s
+ sanitized.to_s
+ end
+
+ def as_json(*)
+ to_s
+ end
+
+ def to_json
+ to_s
+ end
+
+ def <=>(other)
+ to_s.downcase <=> other.to_s.downcase
+ end
+end
diff --git a/activestorage/app/models/active_storage/preview.rb b/activestorage/app/models/active_storage/preview.rb
new file mode 100644
index 0000000000..dd50494799
--- /dev/null
+++ b/activestorage/app/models/active_storage/preview.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+# Some non-image blobs can be previewed: that is, they can be presented as images. A video blob can be previewed by
+# extracting its first frame, and a PDF blob can be previewed by extracting its first page.
+#
+# A previewer extracts a preview image from a blob. Active Storage provides previewers for videos and PDFs:
+# ActiveStorage::Previewer::VideoPreviewer and ActiveStorage::Previewer::PDFPreviewer. Build custom previewers by
+# subclassing ActiveStorage::Previewer and implementing the requisite methods. Consult the ActiveStorage::Previewer
+# documentation for more details on what's required of previewers.
+#
+# To choose the previewer for a blob, Active Storage calls +accept?+ on each registered previewer in order. It uses the
+# first previewer for which +accept?+ returns true when given the blob. In a Rails application, add or remove previewers
+# by manipulating +Rails.application.config.active_storage.previewers+ in an initializer:
+#
+# Rails.application.config.active_storage.previewers
+# # => [ ActiveStorage::Previewer::PDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ]
+#
+# # Add a custom previewer for Microsoft Office documents:
+# Rails.application.config.active_storage.previewers << DOCXPreviewer
+# # => [ ActiveStorage::Previewer::PDFPreviewer, ActiveStorage::Previewer::VideoPreviewer, DOCXPreviewer ]
+#
+# Outside of a Rails application, modify +ActiveStorage.previewers+ instead.
+#
+# The built-in previewers rely on third-party system libraries. Specifically, the built-in video previewer requires
+# {FFmpeg}[https://www.ffmpeg.org]. Two PDF previewers are provided: one requires {Poppler}[https://poppler.freedesktop.org],
+# and the other requires {muPDF}[https://mupdf.com] (version 1.8 or newer). To preview PDFs, install either Poppler or muPDF.
+#
+# These libraries are not provided by Rails. You must install them yourself to use the built-in previewers. Before you
+# install and use third-party software, make sure you understand the licensing implications of doing so.
+class ActiveStorage::Preview
+ class UnprocessedError < StandardError; end
+
+ attr_reader :blob, :variation
+
+ def initialize(blob, variation_or_variation_key)
+ @blob, @variation = blob, ActiveStorage::Variation.wrap(variation_or_variation_key)
+ end
+
+ # Processes the preview if it has not been processed yet. Returns the receiving Preview instance for convenience:
+ #
+ # blob.preview(resize_to_fit: [100, 100]).processed.service_url
+ #
+ # Processing a preview generates an image from its blob and attaches the preview image to the blob. Because the preview
+ # image is stored with the blob, it is only generated once.
+ def processed
+ process unless processed?
+ self
+ end
+
+ # Returns the blob's attached preview image.
+ def image
+ blob.preview_image
+ end
+
+ # Returns the URL of the preview's variant on the service. Raises ActiveStorage::Preview::UnprocessedError if the
+ # preview has not been processed yet.
+ #
+ # This method synchronously processes a variant of the preview image, so do not call it in views. Instead, generate
+ # a stable URL that redirects to the short-lived URL returned by this method.
+ def service_url(**options)
+ if processed?
+ variant.service_url(options)
+ else
+ raise UnprocessedError
+ end
+ end
+
+ private
+ def processed?
+ image.attached?
+ end
+
+ def process
+ previewer.preview { |attachable| image.attach(attachable) }
+ end
+
+ def variant
+ ActiveStorage::Variant.new(image, variation).processed
+ end
+
+
+ def previewer
+ previewer_class.new(blob)
+ end
+
+ def previewer_class
+ ActiveStorage.previewers.detect { |klass| klass.accept?(blob) }
+ end
+end
diff --git a/activestorage/app/models/active_storage/variant.rb b/activestorage/app/models/active_storage/variant.rb
new file mode 100644
index 0000000000..ea57fa5f78
--- /dev/null
+++ b/activestorage/app/models/active_storage/variant.rb
@@ -0,0 +1,131 @@
+# frozen_string_literal: true
+
+require "ostruct"
+
+# Image blobs can have variants that are the result of a set of transformations applied to the original.
+# These variants are used to create thumbnails, fixed-size avatars, or any other derivative image from the
+# original.
+#
+# Variants rely on {ImageProcessing}[https://github.com/janko-m/image_processing] gem for the actual transformations
+# of the file, so you must add <tt>gem "image_processing"</tt> to your Gemfile if you wish to use variants. By
+# default, images will be processed with {ImageMagick}[http://imagemagick.org] using the
+# {MiniMagick}[https://github.com/minimagick/minimagick] gem, but you can also switch to the
+# {libvips}[http://jcupitt.github.io/libvips/] processor operated by the {ruby-vips}[https://github.com/jcupitt/ruby-vips]
+# gem).
+#
+# Rails.application.config.active_storage.variant_processor
+# # => :mini_magick
+#
+# Rails.application.config.active_storage.variant_processor = :vips
+# # => :vips
+#
+# Note that to create a variant it's necessary to download the entire blob file from the service. Because of this process,
+# you also want to be considerate about when the variant is actually processed. You shouldn't be processing variants inline
+# in a template, for example. Delay the processing to an on-demand controller, like the one provided in
+# ActiveStorage::RepresentationsController.
+#
+# To refer to such a delayed on-demand variant, simply link to the variant through the resolved route provided
+# by Active Storage like so:
+#
+# <%= image_tag Current.user.avatar.variant(resize_to_fit: [100, 100]) %>
+#
+# This will create a URL for that specific blob with that specific variant, which the ActiveStorage::RepresentationsController
+# can then produce on-demand.
+#
+# When you do want to actually produce the variant needed, call +processed+. This will check that the variant
+# has already been processed and uploaded to the service, and, if so, just return that. Otherwise it will perform
+# the transformations, upload the variant to the service, and return itself again. Example:
+#
+# avatar.variant(resize_to_fit: [100, 100]).processed.service_url
+#
+# This will create and process a variant of the avatar blob that's constrained to a height and width of 100.
+# Then it'll upload said variant to the service according to a derivative key of the blob and the transformations.
+#
+# You can combine any number of ImageMagick/libvips operations into a variant, as well as any macros provided by the
+# ImageProcessing gem (such as +resize_to_fit+):
+#
+# avatar.variant(resize_to_fit: [800, 800], monochrome: true, rotate: "-90")
+#
+# Visit the following links for a list of available ImageProcessing commands and ImageMagick/libvips operations:
+#
+# * {ImageProcessing::MiniMagick}[https://github.com/janko-m/image_processing/blob/master/doc/minimagick.md#methods]
+# * {ImageMagick reference}[https://www.imagemagick.org/script/mogrify.php]
+# * {ImageProcessing::Vips}[https://github.com/janko-m/image_processing/blob/master/doc/vips.md#methods]
+# * {ruby-vips reference}[http://www.rubydoc.info/gems/ruby-vips/Vips/Image]
+class ActiveStorage::Variant
+ WEB_IMAGE_CONTENT_TYPES = %w[ image/png image/jpeg image/jpg image/gif ]
+
+ attr_reader :blob, :variation
+ delegate :service, to: :blob
+
+ def initialize(blob, variation_or_variation_key)
+ @blob, @variation = blob, ActiveStorage::Variation.wrap(variation_or_variation_key)
+ end
+
+ # Returns the variant instance itself after it's been processed or an existing processing has been found on the service.
+ def processed
+ process unless processed?
+ self
+ end
+
+ # Returns a combination key of the blob and the variation that together identifies a specific variant.
+ def key
+ "variants/#{blob.key}/#{Digest::SHA256.hexdigest(variation.key)}"
+ end
+
+ # Returns the URL of the variant on the service. This URL is intended to be short-lived for security and not used directly
+ # with users. Instead, the +service_url+ should only be exposed as a redirect from a stable, possibly authenticated URL.
+ # Hiding the +service_url+ behind a redirect also gives you the power to change services without updating all URLs. And
+ # it allows permanent URLs that redirect to the +service_url+ to be cached in the view.
+ #
+ # Use <tt>url_for(variant)</tt> (or the implied form, like +link_to variant+ or +redirect_to variant+) to get the stable URL
+ # for a variant that points to the ActiveStorage::RepresentationsController, which in turn will use this +service_call+ method
+ # for its redirection.
+ def service_url(expires_in: ActiveStorage.service_urls_expire_in, disposition: :inline)
+ service.url key, expires_in: expires_in, disposition: disposition, filename: filename, content_type: content_type
+ end
+
+ # Returns the receiving variant. Allows ActiveStorage::Variant and ActiveStorage::Preview instances to be used interchangeably.
+ def image
+ self
+ end
+
+ private
+ def processed?
+ service.exist?(key)
+ end
+
+ def process
+ blob.open do |image|
+ transform(image) { |output| upload(output) }
+ end
+ end
+
+ def transform(image, &block)
+ variation.transform(image, format: format, &block)
+ end
+
+ def upload(file)
+ service.upload(key, file)
+ end
+
+
+ def specification
+ @specification ||=
+ if WEB_IMAGE_CONTENT_TYPES.include?(blob.content_type)
+ Specification.new \
+ filename: blob.filename,
+ content_type: blob.content_type,
+ format: nil
+ else
+ Specification.new \
+ filename: ActiveStorage::Filename.new("#{blob.filename.base}.png"),
+ content_type: "image/png",
+ format: "png"
+ end
+ end
+
+ delegate :filename, :content_type, :format, to: :specification
+
+ class Specification < OpenStruct; end
+end
diff --git a/activestorage/app/models/active_storage/variation.rb b/activestorage/app/models/active_storage/variation.rb
new file mode 100644
index 0000000000..3adc2407e5
--- /dev/null
+++ b/activestorage/app/models/active_storage/variation.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+# A set of transformations that can be applied to a blob to create a variant. This class is exposed via
+# the ActiveStorage::Blob#variant method and should rarely be used directly.
+#
+# In case you do need to use this directly, it's instantiated using a hash of transformations where
+# the key is the command and the value is the arguments. Example:
+#
+# ActiveStorage::Variation.new(resize_to_fit: [100, 100], monochrome: true, trim: true, rotate: "-90")
+#
+# The options map directly to {ImageProcessing}[https://github.com/janko-m/image_processing] commands.
+class ActiveStorage::Variation
+ attr_reader :transformations
+
+ class << self
+ # Returns a Variation instance based on the given variator. If the variator is a Variation, it is
+ # returned unmodified. If it is a String, it is passed to ActiveStorage::Variation.decode. Otherwise,
+ # it is assumed to be a transformations Hash and is passed directly to the constructor.
+ def wrap(variator)
+ case variator
+ when self
+ variator
+ when String
+ decode variator
+ else
+ new variator
+ end
+ end
+
+ # Returns a Variation instance with the transformations that were encoded by +encode+.
+ def decode(key)
+ new ActiveStorage.verifier.verify(key, purpose: :variation)
+ end
+
+ # Returns a signed key for the +transformations+, which can be used to refer to a specific
+ # variation in a URL or combined key (like <tt>ActiveStorage::Variant#key</tt>).
+ def encode(transformations)
+ ActiveStorage.verifier.generate(transformations, purpose: :variation)
+ end
+ end
+
+ def initialize(transformations)
+ @transformations = transformations
+ end
+
+ # Accepts a File object, performs the +transformations+ against it, and
+ # saves the transformed image into a temporary file. If +format+ is specified
+ # it will be the format of the result image, otherwise the result image
+ # retains the source format.
+ def transform(file, format: nil, &block)
+ ActiveSupport::Notifications.instrument("transform.active_storage") do
+ transformer.transform(file, format: format, &block)
+ end
+ end
+
+ # Returns a signed key for all the +transformations+ that this variation was instantiated with.
+ def key
+ self.class.encode(transformations)
+ end
+
+ private
+ def transformer
+ if ActiveStorage.variant_processor
+ begin
+ require "image_processing"
+ rescue LoadError
+ ActiveSupport::Deprecation.warn <<~WARNING
+ Generating image variants will require the image_processing gem in Rails 6.1.
+ Please add `gem 'image_processing', '~> 1.2'` to your Gemfile.
+ WARNING
+
+ ActiveStorage::Transformers::MiniMagickTransformer.new(transformations)
+ else
+ ActiveStorage::Transformers::ImageProcessingTransformer.new(transformations)
+ end
+ else
+ ActiveStorage::Transformers::MiniMagickTransformer.new(transformations)
+ end
+ end
+end
diff --git a/activestorage/bin/test b/activestorage/bin/test
new file mode 100755
index 0000000000..c53377cc97
--- /dev/null
+++ b/activestorage/bin/test
@@ -0,0 +1,5 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+COMPONENT_ROOT = File.expand_path("..", __dir__)
+require_relative "../../tools/test"
diff --git a/activestorage/config/routes.rb b/activestorage/config/routes.rb
new file mode 100644
index 0000000000..3af7361cff
--- /dev/null
+++ b/activestorage/config/routes.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+Rails.application.routes.draw do
+ scope ActiveStorage.routes_prefix do
+ get "/blobs/:signed_id/*filename" => "active_storage/blobs#show", as: :rails_service_blob
+
+ get "/representations/:signed_blob_id/:variation_key/*filename" => "active_storage/representations#show", as: :rails_blob_representation
+
+ get "/disk/:encoded_key/*filename" => "active_storage/disk#show", as: :rails_disk_service
+ put "/disk/:encoded_token" => "active_storage/disk#update", as: :update_rails_disk_service
+ post "/direct_uploads" => "active_storage/direct_uploads#create", as: :rails_direct_uploads
+ end
+
+ direct :rails_representation do |representation, options|
+ signed_blob_id = representation.blob.signed_id
+ variation_key = representation.variation.key
+ filename = representation.blob.filename
+
+ route_for(:rails_blob_representation, signed_blob_id, variation_key, filename, options)
+ end
+
+ resolve("ActiveStorage::Variant") { |variant, options| route_for(:rails_representation, variant, options) }
+ resolve("ActiveStorage::Preview") { |preview, options| route_for(:rails_representation, preview, options) }
+
+
+ direct :rails_blob do |blob, options|
+ route_for(:rails_service_blob, blob.signed_id, blob.filename, options)
+ end
+
+ resolve("ActiveStorage::Blob") { |blob, options| route_for(:rails_blob, blob, options) }
+ resolve("ActiveStorage::Attachment") { |attachment, options| route_for(:rails_blob, attachment.blob, options) }
+end
diff --git a/activestorage/db/migrate/20170806125915_create_active_storage_tables.rb b/activestorage/db/migrate/20170806125915_create_active_storage_tables.rb
new file mode 100644
index 0000000000..cfaf01cd5e
--- /dev/null
+++ b/activestorage/db/migrate/20170806125915_create_active_storage_tables.rb
@@ -0,0 +1,26 @@
+class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
+ def change
+ create_table :active_storage_blobs do |t|
+ t.string :key, null: false
+ t.string :filename, null: false
+ t.string :content_type
+ t.text :metadata
+ t.bigint :byte_size, null: false
+ t.string :checksum, null: false
+ t.datetime :created_at, null: false
+
+ t.index [ :key ], unique: true
+ end
+
+ create_table :active_storage_attachments do |t|
+ t.string :name, null: false
+ t.references :record, null: false, polymorphic: true, index: false
+ t.references :blob, null: false
+
+ t.datetime :created_at, null: false
+
+ t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true
+ t.foreign_key :active_storage_blobs, column: :blob_id
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage.rb b/activestorage/lib/active_storage.rb
new file mode 100644
index 0000000000..a94ef626f2
--- /dev/null
+++ b/activestorage/lib/active_storage.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+#--
+# Copyright (c) 2017-2018 David Heinemeier Hansson, Basecamp
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "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.
+#++
+
+require "active_record"
+require "active_support"
+require "active_support/rails"
+
+require "active_storage/version"
+require "active_storage/errors"
+
+require "marcel"
+
+module ActiveStorage
+ extend ActiveSupport::Autoload
+
+ autoload :Attached
+ autoload :Service
+ autoload :Previewer
+ autoload :Analyzer
+
+ mattr_accessor :logger
+ mattr_accessor :verifier
+ mattr_accessor :queue
+ mattr_accessor :previewers, default: []
+ mattr_accessor :analyzers, default: []
+ mattr_accessor :variant_processor, default: :mini_magick
+ mattr_accessor :paths, default: {}
+ mattr_accessor :variable_content_types, default: []
+ mattr_accessor :content_types_to_serve_as_binary, default: []
+ mattr_accessor :service_urls_expire_in, default: 5.minutes
+ mattr_accessor :routes_prefix, default: "/rails/active_storage"
+
+ module Transformers
+ extend ActiveSupport::Autoload
+
+ autoload :Transformer
+ autoload :ImageProcessingTransformer
+ autoload :MiniMagickTransformer
+ end
+end
diff --git a/activestorage/lib/active_storage/analyzer.rb b/activestorage/lib/active_storage/analyzer.rb
new file mode 100644
index 0000000000..caa25418a5
--- /dev/null
+++ b/activestorage/lib/active_storage/analyzer.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ # This is an abstract base class for analyzers, which extract metadata from blobs. See
+ # ActiveStorage::Analyzer::ImageAnalyzer for an example of a concrete subclass.
+ class Analyzer
+ attr_reader :blob
+
+ # Implement this method in a concrete subclass. Have it return true when given a blob from which
+ # the analyzer can extract metadata.
+ def self.accept?(blob)
+ false
+ end
+
+ def initialize(blob)
+ @blob = blob
+ end
+
+ # Override this method in a concrete subclass. Have it return a Hash of metadata.
+ def metadata
+ raise NotImplementedError
+ end
+
+ private
+ # Downloads the blob to a tempfile on disk. Yields the tempfile.
+ def download_blob_to_tempfile(&block) #:doc:
+ blob.open tempdir: tempdir, &block
+ end
+
+ def logger #:doc:
+ ActiveStorage.logger
+ end
+
+ def tempdir #:doc:
+ Dir.tmpdir
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/analyzer/image_analyzer.rb b/activestorage/lib/active_storage/analyzer/image_analyzer.rb
new file mode 100644
index 0000000000..3b39de91be
--- /dev/null
+++ b/activestorage/lib/active_storage/analyzer/image_analyzer.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ # Extracts width and height in pixels from an image blob.
+ #
+ # If the image contains EXIF data indicating its angle is 90 or 270 degrees, its width and height are swapped for convenience.
+ #
+ # Example:
+ #
+ # ActiveStorage::Analyzer::ImageAnalyzer.new(blob).metadata
+ # # => { width: 4104, height: 2736 }
+ #
+ # This analyzer relies on the third-party {MiniMagick}[https://github.com/minimagick/minimagick] gem. MiniMagick requires
+ # the {ImageMagick}[http://www.imagemagick.org] system library.
+ class Analyzer::ImageAnalyzer < Analyzer
+ def self.accept?(blob)
+ blob.image?
+ end
+
+ def metadata
+ read_image do |image|
+ if rotated_image?(image)
+ { width: image.height, height: image.width }
+ else
+ { width: image.width, height: image.height }
+ end
+ end
+ rescue LoadError
+ logger.info "Skipping image analysis because the mini_magick gem isn't installed"
+ {}
+ end
+
+ private
+ def read_image
+ download_blob_to_tempfile do |file|
+ require "mini_magick"
+ yield MiniMagick::Image.new(file.path)
+ end
+ end
+
+ def rotated_image?(image)
+ %w[ RightTop LeftBottom ].include?(image["%[orientation]"])
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/analyzer/null_analyzer.rb b/activestorage/lib/active_storage/analyzer/null_analyzer.rb
new file mode 100644
index 0000000000..8ff7ce48e5
--- /dev/null
+++ b/activestorage/lib/active_storage/analyzer/null_analyzer.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ class Analyzer::NullAnalyzer < Analyzer # :nodoc:
+ def self.accept?(blob)
+ true
+ end
+
+ def metadata
+ {}
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/analyzer/video_analyzer.rb b/activestorage/lib/active_storage/analyzer/video_analyzer.rb
new file mode 100644
index 0000000000..18d8ff8237
--- /dev/null
+++ b/activestorage/lib/active_storage/analyzer/video_analyzer.rb
@@ -0,0 +1,118 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ # Extracts the following from a video blob:
+ #
+ # * Width (pixels)
+ # * Height (pixels)
+ # * Duration (seconds)
+ # * Angle (degrees)
+ # * Display aspect ratio
+ #
+ # Example:
+ #
+ # ActiveStorage::VideoAnalyzer.new(blob).metadata
+ # # => { width: 640.0, height: 480.0, duration: 5.0, angle: 0, display_aspect_ratio: [4, 3] }
+ #
+ # When a video's angle is 90 or 270 degrees, its width and height are automatically swapped for convenience.
+ #
+ # This analyzer requires the {FFmpeg}[https://www.ffmpeg.org] system library, which is not provided by Rails.
+ class Analyzer::VideoAnalyzer < Analyzer
+ def self.accept?(blob)
+ blob.video?
+ end
+
+ def metadata
+ { width: width, height: height, duration: duration, angle: angle, display_aspect_ratio: display_aspect_ratio }.compact
+ end
+
+ private
+ def width
+ if rotated?
+ computed_height || encoded_height
+ else
+ encoded_width
+ end
+ end
+
+ def height
+ if rotated?
+ encoded_width
+ else
+ computed_height || encoded_height
+ end
+ end
+
+ def duration
+ Float(video_stream["duration"]) if video_stream["duration"]
+ end
+
+ def angle
+ Integer(tags["rotate"]) if tags["rotate"]
+ end
+
+ def display_aspect_ratio
+ if descriptor = video_stream["display_aspect_ratio"]
+ if terms = descriptor.split(":", 2)
+ numerator = Integer(terms[0])
+ denominator = Integer(terms[1])
+
+ [numerator, denominator] unless numerator == 0
+ end
+ end
+ end
+
+
+ def rotated?
+ angle == 90 || angle == 270
+ end
+
+ def computed_height
+ if encoded_width && display_height_scale
+ encoded_width * display_height_scale
+ end
+ end
+
+ def encoded_width
+ @encoded_width ||= Float(video_stream["width"]) if video_stream["width"]
+ end
+
+ def encoded_height
+ @encoded_height ||= Float(video_stream["height"]) if video_stream["height"]
+ end
+
+ def display_height_scale
+ @display_height_scale ||= Float(display_aspect_ratio.last) / display_aspect_ratio.first if display_aspect_ratio
+ end
+
+
+ def tags
+ @tags ||= video_stream["tags"] || {}
+ end
+
+ def video_stream
+ @video_stream ||= streams.detect { |stream| stream["codec_type"] == "video" } || {}
+ end
+
+ def streams
+ probe["streams"] || []
+ end
+
+ def probe
+ download_blob_to_tempfile { |file| probe_from(file) }
+ end
+
+ def probe_from(file)
+ IO.popen([ ffprobe_path, "-print_format", "json", "-show_streams", "-v", "error", file.path ]) do |output|
+ JSON.parse(output.read)
+ end
+ rescue Errno::ENOENT
+ logger.info "Skipping video analysis because FFmpeg isn't installed"
+ {}
+ end
+
+ def ffprobe_path
+ ActiveStorage.paths[:ffprobe] || "ffprobe"
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/attached.rb b/activestorage/lib/active_storage/attached.rb
new file mode 100644
index 0000000000..b540f85fbe
--- /dev/null
+++ b/activestorage/lib/active_storage/attached.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/delegation"
+
+module ActiveStorage
+ # Abstract base class for the concrete ActiveStorage::Attached::One and ActiveStorage::Attached::Many
+ # classes that both provide proxy access to the blob association for a record.
+ class Attached
+ attr_reader :name, :record
+
+ def initialize(name, record)
+ @name, @record = name, record
+ end
+
+ private
+ def change
+ record.attachment_changes[name]
+ end
+ end
+end
+
+require "active_storage/attached/model"
+require "active_storage/attached/one"
+require "active_storage/attached/many"
+require "active_storage/attached/changes"
diff --git a/activestorage/lib/active_storage/attached/changes.rb b/activestorage/lib/active_storage/attached/changes.rb
new file mode 100644
index 0000000000..1db3906a63
--- /dev/null
+++ b/activestorage/lib/active_storage/attached/changes.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ module Attached::Changes #:nodoc:
+ extend ActiveSupport::Autoload
+
+ eager_autoload do
+ autoload :CreateOne
+ autoload :CreateMany
+ autoload :CreateOneOfMany
+
+ autoload :DeleteOne
+ autoload :DeleteMany
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/attached/changes/create_many.rb b/activestorage/lib/active_storage/attached/changes/create_many.rb
new file mode 100644
index 0000000000..a7a8553e0f
--- /dev/null
+++ b/activestorage/lib/active_storage/attached/changes/create_many.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ class Attached::Changes::CreateMany #:nodoc:
+ attr_reader :name, :record, :attachables
+
+ def initialize(name, record, attachables)
+ @name, @record, @attachables = name, record, Array(attachables)
+ end
+
+ def attachments
+ @attachments ||= subchanges.collect(&:attachment)
+ end
+
+ def blobs
+ @blobs ||= subchanges.collect(&:blob)
+ end
+
+ def upload
+ subchanges.each(&:upload)
+ end
+
+ def save
+ assign_associated_attachments
+ reset_associated_blobs
+ end
+
+ private
+ def subchanges
+ @subchanges ||= attachables.collect { |attachable| build_subchange_from(attachable) }
+ end
+
+ def build_subchange_from(attachable)
+ ActiveStorage::Attached::Changes::CreateOneOfMany.new(name, record, attachable)
+ end
+
+
+ def assign_associated_attachments
+ record.public_send("#{name}_attachments=", attachments)
+ end
+
+ def reset_associated_blobs
+ record.public_send("#{name}_blobs").reset
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/attached/changes/create_one.rb b/activestorage/lib/active_storage/attached/changes/create_one.rb
new file mode 100644
index 0000000000..5812fd2b08
--- /dev/null
+++ b/activestorage/lib/active_storage/attached/changes/create_one.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require "action_dispatch"
+require "action_dispatch/http/upload"
+
+module ActiveStorage
+ class Attached::Changes::CreateOne #:nodoc:
+ attr_reader :name, :record, :attachable
+
+ def initialize(name, record, attachable)
+ @name, @record, @attachable = name, record, attachable
+ end
+
+ def attachment
+ @attachment ||= find_or_build_attachment
+ end
+
+ def blob
+ @blob ||= find_or_build_blob
+ end
+
+ def upload
+ case attachable
+ when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
+ blob.upload_without_unfurling(attachable.open)
+ when Hash
+ blob.upload_without_unfurling(attachable.fetch(:io))
+ end
+ end
+
+ def save
+ record.public_send("#{name}_attachment=", attachment)
+ end
+
+ private
+ def find_or_build_attachment
+ find_attachment || build_attachment
+ end
+
+ def find_attachment
+ if record.public_send("#{name}_blob") == blob
+ record.public_send("#{name}_attachment")
+ end
+ end
+
+ def build_attachment
+ ActiveStorage::Attachment.new(record: record, name: name, blob: blob)
+ end
+
+ def find_or_build_blob
+ case attachable
+ when ActiveStorage::Blob
+ attachable
+ when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
+ ActiveStorage::Blob.build_after_unfurling \
+ io: attachable.open,
+ filename: attachable.original_filename,
+ content_type: attachable.content_type
+ when Hash
+ ActiveStorage::Blob.build_after_unfurling(attachable)
+ when String
+ ActiveStorage::Blob.find_signed(attachable)
+ else
+ raise ArgumentError, "Could not find or build blob: expected attachable, got #{attachable.inspect}"
+ end
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/attached/changes/create_one_of_many.rb b/activestorage/lib/active_storage/attached/changes/create_one_of_many.rb
new file mode 100644
index 0000000000..7268e87316
--- /dev/null
+++ b/activestorage/lib/active_storage/attached/changes/create_one_of_many.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ class Attached::Changes::CreateOneOfMany < Attached::Changes::CreateOne #:nodoc:
+ private
+ def find_attachment
+ record.public_send("#{name}_attachments").detect { |attachment| attachment.blob_id == blob.id }
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/attached/changes/delete_many.rb b/activestorage/lib/active_storage/attached/changes/delete_many.rb
new file mode 100644
index 0000000000..6cbd1158dc
--- /dev/null
+++ b/activestorage/lib/active_storage/attached/changes/delete_many.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ class Attached::Changes::DeleteMany #:nodoc:
+ attr_reader :name, :record
+
+ def initialize(name, record)
+ @name, @record = name, record
+ end
+
+ def attachments
+ ActiveStorage::Attachment.none
+ end
+
+ def blobs
+ ActiveStorage::Blob.none
+ end
+
+ def save
+ record.public_send("#{name}_attachments=", [])
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/attached/changes/delete_one.rb b/activestorage/lib/active_storage/attached/changes/delete_one.rb
new file mode 100644
index 0000000000..2f7d356613
--- /dev/null
+++ b/activestorage/lib/active_storage/attached/changes/delete_one.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ class Attached::Changes::DeleteOne #:nodoc:
+ attr_reader :name, :record
+
+ def initialize(name, record)
+ @name, @record = name, record
+ end
+
+ def attachment
+ nil
+ end
+
+ def save
+ record.public_send("#{name}_attachment=", nil)
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/attached/many.rb b/activestorage/lib/active_storage/attached/many.rb
new file mode 100644
index 0000000000..25f88284df
--- /dev/null
+++ b/activestorage/lib/active_storage/attached/many.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ # Decorated proxy object representing of multiple attachments to a model.
+ class Attached::Many < Attached
+ delegate_missing_to :attachments
+
+ # Returns all the associated attachment records.
+ #
+ # All methods called on this proxy object that aren't listed here will automatically be delegated to +attachments+.
+ def attachments
+ change.present? ? change.attachments : record.public_send("#{name}_attachments")
+ end
+
+ # Returns all attached blobs.
+ def blobs
+ change.present? ? change.blobs : record.public_send("#{name}_blobs")
+ end
+
+ # Attaches one or more +attachables+ to the record.
+ #
+ # If the record is persisted and unchanged, the attachments are saved to
+ # the database immediately. Otherwise, they'll be saved to the DB when the
+ # record is next saved.
+ #
+ # document.images.attach(params[:images]) # Array of ActionDispatch::Http::UploadedFile objects
+ # document.images.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload
+ # document.images.attach(io: File.open("/path/to/racecar.jpg"), filename: "racecar.jpg", content_type: "image/jpg")
+ # document.images.attach([ first_blob, second_blob ])
+ def attach(*attachables)
+ if record.persisted? && !record.changed?
+ record.update(name => blobs + attachables.flatten)
+ else
+ record.public_send("#{name}=", blobs + attachables.flatten)
+ end
+ end
+
+ # Returns true if any attachments has been made.
+ #
+ # class Gallery < ActiveRecord::Base
+ # has_many_attached :photos
+ # end
+ #
+ # Gallery.new.photos.attached? # => false
+ def attached?
+ attachments.any?
+ end
+
+ # Deletes associated attachments without purging them, leaving their respective blobs in place.
+ def detach
+ attachments.delete_all if attached?
+ end
+
+ ##
+ # :method: purge
+ #
+ # Directly purges each associated attachment (i.e. destroys the blobs and
+ # attachments and deletes the files on the service).
+
+ ##
+ # :method: purge_later
+ #
+ # Purges each associated attachment through the queuing system.
+ end
+end
diff --git a/activestorage/lib/active_storage/attached/model.rb b/activestorage/lib/active_storage/attached/model.rb
new file mode 100644
index 0000000000..ae7f0685f2
--- /dev/null
+++ b/activestorage/lib/active_storage/attached/model.rb
@@ -0,0 +1,140 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ # Provides the class-level DSL for declaring an Active Record model's attachments.
+ module Attached::Model
+ extend ActiveSupport::Concern
+
+ class_methods do
+ # Specifies the relation between a single attachment and the model.
+ #
+ # class User < ActiveRecord::Base
+ # has_one_attached :avatar
+ # end
+ #
+ # There is no column defined on the model side, Active Storage takes
+ # care of the mapping between your records and the attachment.
+ #
+ # To avoid N+1 queries, you can include the attached blobs in your query like so:
+ #
+ # User.with_attached_avatar
+ #
+ # Under the covers, this relationship is implemented as a +has_one+ association to a
+ # ActiveStorage::Attachment record and a +has_one-through+ association to a
+ # ActiveStorage::Blob record. These associations are available as +avatar_attachment+
+ # and +avatar_blob+. But you shouldn't need to work with these associations directly in
+ # most circumstances.
+ #
+ # The system has been designed to having you go through the ActiveStorage::Attached::One
+ # proxy that provides the dynamic proxy to the associations and factory methods, like +attach+.
+ #
+ # If the +:dependent+ option isn't set, the attachment will be purged
+ # (i.e. destroyed) whenever the record is destroyed.
+ def has_one_attached(name, dependent: :purge_later)
+ generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
+ def #{name}
+ @active_storage_attached_#{name} ||= ActiveStorage::Attached::One.new("#{name}", self)
+ end
+
+ def #{name}=(attachable)
+ attachment_changes["#{name}"] =
+ if attachable.nil?
+ ActiveStorage::Attached::Changes::DeleteOne.new("#{name}", self)
+ else
+ ActiveStorage::Attached::Changes::CreateOne.new("#{name}", self, attachable)
+ end
+ end
+ CODE
+
+ has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record, inverse_of: :record, dependent: :destroy
+ has_one :"#{name}_blob", through: :"#{name}_attachment", class_name: "ActiveStorage::Blob", source: :blob
+
+ scope :"with_attached_#{name}", -> { includes("#{name}_attachment": :blob) }
+
+ after_save { attachment_changes[name.to_s]&.save }
+
+ after_commit(on: %i[ create update ]) { attachment_changes.delete(name.to_s).try(:upload) }
+
+ ActiveRecord::Reflection.add_attachment_reflection(
+ self,
+ name,
+ ActiveRecord::Reflection.create(:has_one_attached, name, nil, { dependent: dependent }, self)
+ )
+ end
+
+ # Specifies the relation between multiple attachments and the model.
+ #
+ # class Gallery < ActiveRecord::Base
+ # has_many_attached :photos
+ # end
+ #
+ # There are no columns defined on the model side, Active Storage takes
+ # care of the mapping between your records and the attachments.
+ #
+ # To avoid N+1 queries, you can include the attached blobs in your query like so:
+ #
+ # Gallery.where(user: Current.user).with_attached_photos
+ #
+ # Under the covers, this relationship is implemented as a +has_many+ association to a
+ # ActiveStorage::Attachment record and a +has_many-through+ association to a
+ # ActiveStorage::Blob record. These associations are available as +photos_attachments+
+ # and +photos_blobs+. But you shouldn't need to work with these associations directly in
+ # most circumstances.
+ #
+ # The system has been designed to having you go through the ActiveStorage::Attached::Many
+ # proxy that provides the dynamic proxy to the associations and factory methods, like +#attach+.
+ #
+ # If the +:dependent+ option isn't set, all the attachments will be purged
+ # (i.e. destroyed) whenever the record is destroyed.
+ def has_many_attached(name, dependent: :purge_later)
+ generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
+ def #{name}
+ @active_storage_attached_#{name} ||= ActiveStorage::Attached::Many.new("#{name}", self)
+ end
+
+ def #{name}=(attachables)
+ attachment_changes["#{name}"] =
+ if attachables.nil? || Array(attachables).none?
+ ActiveStorage::Attached::Changes::DeleteMany.new("#{name}", self)
+ else
+ ActiveStorage::Attached::Changes::CreateMany.new("#{name}", self, attachables)
+ end
+ end
+ CODE
+
+ has_many :"#{name}_attachments", -> { where(name: name) }, as: :record, class_name: "ActiveStorage::Attachment", inverse_of: :record, dependent: :destroy do
+ def purge
+ each(&:purge)
+ reset
+ end
+
+ def purge_later
+ each(&:purge_later)
+ reset
+ end
+ end
+ has_many :"#{name}_blobs", through: :"#{name}_attachments", class_name: "ActiveStorage::Blob", source: :blob
+
+ scope :"with_attached_#{name}", -> { includes("#{name}_attachments": :blob) }
+
+ after_save { attachment_changes[name.to_s]&.save }
+
+ after_commit(on: %i[ create update ]) { attachment_changes.delete(name.to_s).try(:upload) }
+
+ ActiveRecord::Reflection.add_attachment_reflection(
+ self,
+ name,
+ ActiveRecord::Reflection.create(:has_many_attached, name, nil, { dependent: dependent }, self)
+ )
+ end
+ end
+
+ def attachment_changes #:nodoc:
+ @attachment_changes ||= {}
+ end
+
+ def reload(*) #:nodoc:
+ super.tap { @attachment_changes = nil }
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/attached/one.rb b/activestorage/lib/active_storage/attached/one.rb
new file mode 100644
index 0000000000..c039226fcd
--- /dev/null
+++ b/activestorage/lib/active_storage/attached/one.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ # Representation of a single attachment to a model.
+ class Attached::One < Attached
+ delegate_missing_to :attachment
+
+ # Returns the associated attachment record.
+ #
+ # You don't have to call this method to access the attachment's methods as
+ # they are all available at the model level.
+ def attachment
+ change.present? ? change.attachment : record.public_send("#{name}_attachment")
+ end
+
+ def blank?
+ !attached?
+ end
+
+ # Attaches an +attachable+ to the record.
+ #
+ # If the record is persisted and unchanged, the attachment is saved to
+ # the database immediately. Otherwise, it'll be saved to the DB when the
+ # record is next saved.
+ #
+ # person.avatar.attach(params[:avatar]) # ActionDispatch::Http::UploadedFile object
+ # person.avatar.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload
+ # person.avatar.attach(io: File.open("/path/to/face.jpg"), filename: "face.jpg", content_type: "image/jpg")
+ # person.avatar.attach(avatar_blob) # ActiveStorage::Blob object
+ def attach(attachable)
+ if record.persisted? && !record.changed?
+ record.update(name => attachable)
+ else
+ record.public_send("#{name}=", attachable)
+ end
+ end
+
+ # Returns +true+ if an attachment has been made.
+ #
+ # class User < ActiveRecord::Base
+ # has_one_attached :avatar
+ # end
+ #
+ # User.new.avatar.attached? # => false
+ def attached?
+ attachment.present?
+ end
+
+ # Deletes the attachment without purging it, leaving its blob in place.
+ def detach
+ if attached?
+ attachment.delete
+ write_attachment nil
+ end
+ end
+
+ # Directly purges the attachment (i.e. destroys the blob and
+ # attachment and deletes the file on the service).
+ def purge
+ if attached?
+ attachment.purge
+ write_attachment nil
+ end
+ end
+
+ # Purges the attachment through the queuing system.
+ def purge_later
+ if attached?
+ attachment.purge_later
+ write_attachment nil
+ end
+ end
+
+ private
+ def write_attachment(attachment)
+ record.public_send("#{name}_attachment=", attachment)
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/downloader.rb b/activestorage/lib/active_storage/downloader.rb
new file mode 100644
index 0000000000..87be6efb05
--- /dev/null
+++ b/activestorage/lib/active_storage/downloader.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ class Downloader #:nodoc:
+ def initialize(blob, tempdir: nil)
+ @blob = blob
+ @tempdir = tempdir
+ end
+
+ def download_blob_to_tempfile
+ open_tempfile do |file|
+ download_blob_to file
+ verify_integrity_of file
+ yield file
+ end
+ end
+
+ private
+ attr_reader :blob, :tempdir
+
+ def open_tempfile
+ file = Tempfile.open([ "ActiveStorage-#{blob.id}-", blob.filename.extension_with_delimiter ], tempdir)
+
+ begin
+ yield file
+ ensure
+ file.close!
+ end
+ end
+
+ def download_blob_to(file)
+ file.binmode
+ blob.download { |chunk| file.write(chunk) }
+ file.flush
+ file.rewind
+ end
+
+ def verify_integrity_of(file)
+ unless Digest::MD5.file(file).base64digest == blob.checksum
+ raise ActiveStorage::IntegrityError
+ end
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/downloading.rb b/activestorage/lib/active_storage/downloading.rb
new file mode 100644
index 0000000000..df820bc088
--- /dev/null
+++ b/activestorage/lib/active_storage/downloading.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require "tmpdir"
+require "active_support/core_ext/string/filters"
+
+module ActiveStorage
+ module Downloading
+ def self.included(klass)
+ ActiveSupport::Deprecation.warn <<~MESSAGE.squish, caller_locations(2)
+ ActiveStorage::Downloading is deprecated and will be removed in Active Storage 6.1.
+ Use ActiveStorage::Blob#open instead.
+ MESSAGE
+ end
+
+ private
+ # Opens a new tempfile in #tempdir and copies blob data into it. Yields the tempfile.
+ def download_blob_to_tempfile #:doc:
+ open_tempfile_for_blob do |file|
+ download_blob_to file
+ yield file
+ end
+ end
+
+ def open_tempfile_for_blob
+ tempfile = Tempfile.open([ "ActiveStorage", blob.filename.extension_with_delimiter ], tempdir)
+
+ begin
+ yield tempfile
+ ensure
+ tempfile.close!
+ end
+ end
+
+ # Efficiently downloads blob data into the given file.
+ def download_blob_to(file) #:doc:
+ file.binmode
+ blob.download { |chunk| file.write(chunk) }
+ file.flush
+ file.rewind
+ end
+
+ # Returns the directory in which tempfiles should be opened. Defaults to +Dir.tmpdir+.
+ def tempdir #:doc:
+ Dir.tmpdir
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/engine.rb b/activestorage/lib/active_storage/engine.rb
new file mode 100644
index 0000000000..7eb93b5e16
--- /dev/null
+++ b/activestorage/lib/active_storage/engine.rb
@@ -0,0 +1,110 @@
+# frozen_string_literal: true
+
+require "rails"
+require "active_storage"
+
+require "active_storage/previewer/poppler_pdf_previewer"
+require "active_storage/previewer/mupdf_previewer"
+require "active_storage/previewer/video_previewer"
+
+require "active_storage/analyzer/image_analyzer"
+require "active_storage/analyzer/video_analyzer"
+
+require "active_storage/reflection"
+
+module ActiveStorage
+ class Engine < Rails::Engine # :nodoc:
+ isolate_namespace ActiveStorage
+
+ config.active_storage = ActiveSupport::OrderedOptions.new
+ config.active_storage.previewers = [ ActiveStorage::Previewer::PopplerPDFPreviewer, ActiveStorage::Previewer::MuPDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ]
+ config.active_storage.analyzers = [ ActiveStorage::Analyzer::ImageAnalyzer, ActiveStorage::Analyzer::VideoAnalyzer ]
+ config.active_storage.paths = ActiveSupport::OrderedOptions.new
+
+ config.active_storage.variable_content_types = %w(
+ image/png
+ image/gif
+ image/jpg
+ image/jpeg
+ image/vnd.adobe.photoshop
+ image/vnd.microsoft.icon
+ )
+
+ config.active_storage.content_types_to_serve_as_binary = %w(
+ text/html
+ text/javascript
+ image/svg+xml
+ application/postscript
+ application/x-shockwave-flash
+ text/xml
+ application/xml
+ application/xhtml+xml
+ )
+
+ config.eager_load_namespaces << ActiveStorage
+
+ initializer "active_storage.configs" do
+ config.after_initialize do |app|
+ ActiveStorage.logger = app.config.active_storage.logger || Rails.logger
+ ActiveStorage.queue = app.config.active_storage.queue
+ ActiveStorage.variant_processor = app.config.active_storage.variant_processor || :mini_magick
+ ActiveStorage.previewers = app.config.active_storage.previewers || []
+ ActiveStorage.analyzers = app.config.active_storage.analyzers || []
+ ActiveStorage.paths = app.config.active_storage.paths || {}
+ ActiveStorage.routes_prefix = app.config.active_storage.routes_prefix || "/rails/active_storage"
+
+ ActiveStorage.variable_content_types = app.config.active_storage.variable_content_types || []
+ ActiveStorage.content_types_to_serve_as_binary = app.config.active_storage.content_types_to_serve_as_binary || []
+ ActiveStorage.service_urls_expire_in = app.config.active_storage.service_urls_expire_in || 5.minutes
+ end
+ end
+
+ initializer "active_storage.attached" do
+ require "active_storage/attached"
+
+ ActiveSupport.on_load(:active_record) do
+ include ActiveStorage::Attached::Model
+ end
+ end
+
+ initializer "active_storage.verifier" do
+ config.after_initialize do |app|
+ ActiveStorage.verifier = app.message_verifier("ActiveStorage")
+ end
+ end
+
+ initializer "active_storage.services" do
+ ActiveSupport.on_load(:active_storage_blob) do
+ if config_choice = Rails.configuration.active_storage.service
+ configs = Rails.configuration.active_storage.service_configurations ||= begin
+ config_file = Pathname.new(Rails.root.join("config/storage.yml"))
+ raise("Couldn't find Active Storage configuration in #{config_file}") unless config_file.exist?
+
+ require "yaml"
+ require "erb"
+
+ YAML.load(ERB.new(config_file.read).result) || {}
+ rescue Psych::SyntaxError => e
+ raise "YAML syntax error occurred while parsing #{config_file}. " \
+ "Please note that YAML must be consistently indented using spaces. Tabs are not allowed. " \
+ "Error: #{e.message}"
+ end
+
+ ActiveStorage::Blob.service =
+ begin
+ ActiveStorage::Service.configure config_choice, configs
+ rescue => e
+ raise e, "Cannot load `Rails.config.active_storage.service`:\n#{e.message}", e.backtrace
+ end
+ end
+ end
+ end
+
+ initializer "active_storage.reflection" do
+ ActiveSupport.on_load(:active_record) do
+ include Reflection::ActiveRecordExtensions
+ ActiveRecord::Reflection.singleton_class.prepend(Reflection::ReflectionExtension)
+ end
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/errors.rb b/activestorage/lib/active_storage/errors.rb
new file mode 100644
index 0000000000..6475c1d076
--- /dev/null
+++ b/activestorage/lib/active_storage/errors.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ # Generic base class for all Active Storage exceptions.
+ class Error < StandardError; end
+
+ # Raised when ActiveStorage::Blob#variant is called on a blob that isn't variable.
+ # Use ActiveStorage::Blob#variable? to determine whether a blob is variable.
+ class InvariableError < Error; end
+
+ # Raised when ActiveStorage::Blob#preview is called on a blob that isn't previewable.
+ # Use ActiveStorage::Blob#previewable? to determine whether a blob is previewable.
+ class UnpreviewableError < Error; end
+
+ # Raised when ActiveStorage::Blob#representation is called on a blob that isn't representable.
+ # Use ActiveStorage::Blob#representable? to determine whether a blob is representable.
+ class UnrepresentableError < Error; end
+
+ # Raised when uploaded or downloaded data does not match a precomputed checksum.
+ # Indicates that a network error or a software bug caused data corruption.
+ class IntegrityError < Error; end
+
+ # Raised when ActiveStorage::Blob#download is called on a blob where the
+ # backing file is no longer present in its service.
+ class FileNotFoundError < Error; end
+end
diff --git a/activestorage/lib/active_storage/gem_version.rb b/activestorage/lib/active_storage/gem_version.rb
new file mode 100644
index 0000000000..492620731b
--- /dev/null
+++ b/activestorage/lib/active_storage/gem_version.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ # Returns the version of the currently loaded Active Storage as a <tt>Gem::Version</tt>.
+ def self.gem_version
+ Gem::Version.new VERSION::STRING
+ end
+
+ module VERSION
+ MAJOR = 6
+ MINOR = 0
+ TINY = 0
+ PRE = "alpha"
+
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
+ end
+end
diff --git a/activestorage/lib/active_storage/log_subscriber.rb b/activestorage/lib/active_storage/log_subscriber.rb
new file mode 100644
index 0000000000..6c0b4c30e7
--- /dev/null
+++ b/activestorage/lib/active_storage/log_subscriber.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require "active_support/log_subscriber"
+
+module ActiveStorage
+ class LogSubscriber < ActiveSupport::LogSubscriber
+ def service_upload(event)
+ message = "Uploaded file to key: #{key_in(event)}"
+ message += " (checksum: #{event.payload[:checksum]})" if event.payload[:checksum]
+ info event, color(message, GREEN)
+ end
+
+ def service_download(event)
+ info event, color("Downloaded file from key: #{key_in(event)}", BLUE)
+ end
+
+ alias_method :service_streaming_download, :service_download
+
+ def service_delete(event)
+ info event, color("Deleted file from key: #{key_in(event)}", RED)
+ end
+
+ def service_delete_prefixed(event)
+ info event, color("Deleted files by key prefix: #{event.payload[:prefix]}", RED)
+ end
+
+ def service_exist(event)
+ debug event, color("Checked if file exists at key: #{key_in(event)} (#{event.payload[:exist] ? "yes" : "no"})", BLUE)
+ end
+
+ def service_url(event)
+ debug event, color("Generated URL for file at key: #{key_in(event)} (#{event.payload[:url]})", BLUE)
+ end
+
+ def logger
+ ActiveStorage.logger
+ end
+
+ private
+ def info(event, colored_message)
+ super log_prefix_for_service(event) + colored_message
+ end
+
+ def debug(event, colored_message)
+ super log_prefix_for_service(event) + colored_message
+ end
+
+ def log_prefix_for_service(event)
+ color " #{event.payload[:service]} Storage (#{event.duration.round(1)}ms) ", CYAN
+ end
+
+ def key_in(event)
+ event.payload[:key]
+ end
+ end
+end
+
+ActiveStorage::LogSubscriber.attach_to :active_storage
diff --git a/activestorage/lib/active_storage/previewer.rb b/activestorage/lib/active_storage/previewer.rb
new file mode 100644
index 0000000000..95a041fd16
--- /dev/null
+++ b/activestorage/lib/active_storage/previewer.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ # This is an abstract base class for previewers, which generate images from blobs. See
+ # ActiveStorage::Previewer::MuPDFPreviewer and ActiveStorage::Previewer::VideoPreviewer for
+ # examples of concrete subclasses.
+ class Previewer
+ attr_reader :blob
+
+ # Implement this method in a concrete subclass. Have it return true when given a blob from which
+ # the previewer can generate an image.
+ def self.accept?(blob)
+ false
+ end
+
+ def initialize(blob)
+ @blob = blob
+ end
+
+ # Override this method in a concrete subclass. Have it yield an attachable preview image (i.e.
+ # anything accepted by ActiveStorage::Attached::One#attach).
+ def preview
+ raise NotImplementedError
+ end
+
+ private
+ # Downloads the blob to a tempfile on disk. Yields the tempfile.
+ def download_blob_to_tempfile(&block) #:doc:
+ blob.open tempdir: tempdir, &block
+ end
+
+ # Executes a system command, capturing its binary output in a tempfile. Yields the tempfile.
+ #
+ # Use this method to shell out to a system library (e.g. muPDF or FFmpeg) for preview image
+ # generation. The resulting tempfile can be used as the +:io+ value in an attachable Hash:
+ #
+ # def preview
+ # download_blob_to_tempfile do |input|
+ # draw "my-drawing-command", input.path, "--format", "png", "-" do |output|
+ # yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png"
+ # end
+ # end
+ # end
+ #
+ # The output tempfile is opened in the directory returned by #tempdir.
+ def draw(*argv) #:doc:
+ open_tempfile do |file|
+ instrument :preview, key: blob.key do
+ capture(*argv, to: file)
+ end
+
+ yield file
+ end
+ end
+
+ def open_tempfile
+ tempfile = Tempfile.open("ActiveStorage-", tempdir)
+
+ begin
+ yield tempfile
+ ensure
+ tempfile.close!
+ end
+ end
+
+ def instrument(operation, payload = {}, &block)
+ ActiveSupport::Notifications.instrument "#{operation}.active_storage", payload, &block
+ end
+
+ def capture(*argv, to:)
+ to.binmode
+ IO.popen(argv, err: File::NULL) { |out| IO.copy_stream(out, to) }
+ to.rewind
+ end
+
+ def logger #:doc:
+ ActiveStorage.logger
+ end
+
+ def tempdir #:doc:
+ Dir.tmpdir
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/previewer/mupdf_previewer.rb b/activestorage/lib/active_storage/previewer/mupdf_previewer.rb
new file mode 100644
index 0000000000..ae02a4889d
--- /dev/null
+++ b/activestorage/lib/active_storage/previewer/mupdf_previewer.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ class Previewer::MuPDFPreviewer < Previewer
+ class << self
+ def accept?(blob)
+ blob.content_type == "application/pdf" && mutool_exists?
+ end
+
+ def mutool_path
+ ActiveStorage.paths[:mutool] || "mutool"
+ end
+
+ def mutool_exists?
+ return @mutool_exists unless @mutool_exists.nil?
+
+ system mutool_path, out: File::NULL, err: File::NULL
+
+ @mutool_exists = $?.exitstatus == 1
+ end
+ end
+
+ def preview
+ download_blob_to_tempfile do |input|
+ draw_first_page_from input do |output|
+ yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png"
+ end
+ end
+ end
+
+ private
+ def draw_first_page_from(file, &block)
+ draw self.class.mutool_path, "draw", "-F", "png", "-o", "-", file.path, "1", &block
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/previewer/poppler_pdf_previewer.rb b/activestorage/lib/active_storage/previewer/poppler_pdf_previewer.rb
new file mode 100644
index 0000000000..69eb617d7b
--- /dev/null
+++ b/activestorage/lib/active_storage/previewer/poppler_pdf_previewer.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ class Previewer::PopplerPDFPreviewer < Previewer
+ class << self
+ def accept?(blob)
+ blob.content_type == "application/pdf" && pdftoppm_exists?
+ end
+
+ def pdftoppm_path
+ ActiveStorage.paths[:pdftoppm] || "pdftoppm"
+ end
+
+ def pdftoppm_exists?
+ return @pdftoppm_exists if defined?(@pdftoppm_exists)
+
+ @pdftoppm_exists = system(pdftoppm_path, "-v", out: File::NULL, err: File::NULL)
+ end
+ end
+
+ def preview
+ download_blob_to_tempfile do |input|
+ draw_first_page_from input do |output|
+ yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png"
+ end
+ end
+ end
+
+ private
+ def draw_first_page_from(file, &block)
+ # use 72 dpi to match thumbnail dimesions of the PDF
+ draw self.class.pdftoppm_path, "-singlefile", "-r", "72", "-png", file.path, &block
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/previewer/video_previewer.rb b/activestorage/lib/active_storage/previewer/video_previewer.rb
new file mode 100644
index 0000000000..50e13d202a
--- /dev/null
+++ b/activestorage/lib/active_storage/previewer/video_previewer.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ class Previewer::VideoPreviewer < Previewer
+ def self.accept?(blob)
+ blob.video?
+ end
+
+ def preview
+ download_blob_to_tempfile do |input|
+ draw_relevant_frame_from input do |output|
+ yield io: output, filename: "#{blob.filename.base}.jpg", content_type: "image/jpeg"
+ end
+ end
+ end
+
+ private
+ def draw_relevant_frame_from(file, &block)
+ draw ffmpeg_path, "-i", file.path, "-y", "-vframes", "1", "-f", "image2", "-", &block
+ end
+
+ def ffmpeg_path
+ ActiveStorage.paths[:ffmpeg] || "ffmpeg"
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/reflection.rb b/activestorage/lib/active_storage/reflection.rb
new file mode 100644
index 0000000000..ce248c88b5
--- /dev/null
+++ b/activestorage/lib/active_storage/reflection.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ module Reflection
+ # Holds all the metadata about a has_one_attached attachment as it was
+ # specified in the Active Record class.
+ class HasOneAttachedReflection < ActiveRecord::Reflection::MacroReflection #:nodoc:
+ def macro
+ :has_one_attached
+ end
+ end
+
+ # Holds all the metadata about a has_many_attached attachment as it was
+ # specified in the Active Record class.
+ class HasManyAttachedReflection < ActiveRecord::Reflection::MacroReflection #:nodoc:
+ def macro
+ :has_many_attached
+ end
+ end
+
+ module ReflectionExtension # :nodoc:
+ def add_attachment_reflection(model, name, reflection)
+ model.attachment_reflections = model.attachment_reflections.merge(name.to_s => reflection)
+ end
+
+ private
+ def reflection_class_for(macro)
+ case macro
+ when :has_one_attached
+ HasOneAttachedReflection
+ when :has_many_attached
+ HasManyAttachedReflection
+ else
+ super
+ end
+ end
+ end
+
+ module ActiveRecordExtensions
+ extend ActiveSupport::Concern
+
+ included do
+ class_attribute :attachment_reflections, instance_writer: false, default: {}
+ end
+
+ module ClassMethods
+ # Returns an array of reflection objects for all the attachments in the
+ # class.
+ def reflect_on_all_attachments
+ attachment_reflections.values
+ end
+
+ # Returns the reflection object for the named +attachment+.
+ #
+ # User.reflect_on_attachment(:avatar)
+ # # => the avatar reflection
+ #
+ def reflect_on_attachment(attachment)
+ attachment_reflections[attachment.to_s]
+ end
+ end
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/service.rb b/activestorage/lib/active_storage/service.rb
new file mode 100644
index 0000000000..54ba08fb87
--- /dev/null
+++ b/activestorage/lib/active_storage/service.rb
@@ -0,0 +1,131 @@
+# frozen_string_literal: true
+
+require "active_storage/log_subscriber"
+require "action_dispatch"
+require "action_dispatch/http/content_disposition"
+
+module ActiveStorage
+ # Abstract class serving as an interface for concrete services.
+ #
+ # The available services are:
+ #
+ # * +Disk+, to manage attachments saved directly on the hard drive.
+ # * +GCS+, to manage attachments through Google Cloud Storage.
+ # * +S3+, to manage attachments through Amazon S3.
+ # * +AzureStorage+, to manage attachments through Microsoft Azure Storage.
+ # * +Mirror+, to be able to use several services to manage attachments.
+ #
+ # Inside a Rails application, you can set-up your services through the
+ # generated <tt>config/storage.yml</tt> file and reference one
+ # of the aforementioned constant under the +service+ key. For example:
+ #
+ # local:
+ # service: Disk
+ # root: <%= Rails.root.join("storage") %>
+ #
+ # You can checkout the service's constructor to know which keys are required.
+ #
+ # Then, in your application's configuration, you can specify the service to
+ # use like this:
+ #
+ # config.active_storage.service = :local
+ #
+ # If you are using Active Storage outside of a Ruby on Rails application, you
+ # can configure the service to use like this:
+ #
+ # ActiveStorage::Blob.service = ActiveStorage::Service.configure(
+ # :Disk,
+ # root: Pathname("/foo/bar/storage")
+ # )
+ class Service
+ extend ActiveSupport::Autoload
+ autoload :Configurator
+
+ class << self
+ # Configure an Active Storage service by name from a set of configurations,
+ # typically loaded from a YAML file. The Active Storage engine uses this
+ # to set the global Active Storage service when the app boots.
+ def configure(service_name, configurations)
+ Configurator.build(service_name, configurations)
+ end
+
+ # Override in subclasses that stitch together multiple services and hence
+ # need to build additional services using the configurator.
+ #
+ # Passes the configurator and all of the service's config as keyword args.
+ #
+ # See MirrorService for an example.
+ def build(configurator:, service: nil, **service_config) #:nodoc:
+ new(**service_config)
+ end
+ end
+
+ # Upload the +io+ to the +key+ specified. If a +checksum+ is provided, the service will
+ # ensure a match when the upload has completed or raise an ActiveStorage::IntegrityError.
+ def upload(key, io, checksum: nil)
+ raise NotImplementedError
+ end
+
+ # Return the content of the file at the +key+.
+ def download(key)
+ raise NotImplementedError
+ end
+
+ # Return the partial content in the byte +range+ of the file at the +key+.
+ def download_chunk(key, range)
+ raise NotImplementedError
+ end
+
+ # Delete the file at the +key+.
+ def delete(key)
+ raise NotImplementedError
+ end
+
+ # Delete files at keys starting with the +prefix+.
+ def delete_prefixed(prefix)
+ raise NotImplementedError
+ end
+
+ # Return +true+ if a file exists at the +key+.
+ def exist?(key)
+ raise NotImplementedError
+ end
+
+ # Returns a signed, temporary URL for the file at the +key+. The URL will be valid for the amount
+ # of seconds specified in +expires_in+. You must also provide the +disposition+ (+:inline+ or +:attachment+),
+ # +filename+, and +content_type+ that you wish the file to be served with on request.
+ def url(key, expires_in:, disposition:, filename:, content_type:)
+ raise NotImplementedError
+ end
+
+ # Returns a signed, temporary URL that a direct upload file can be PUT to on the +key+.
+ # The URL will be valid for the amount of seconds specified in +expires_in+.
+ # You must also provide the +content_type+, +content_length+, and +checksum+ of the file
+ # that will be uploaded. All these attributes will be validated by the service upon upload.
+ def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
+ raise NotImplementedError
+ end
+
+ # Returns a Hash of headers for +url_for_direct_upload+ requests.
+ def headers_for_direct_upload(key, filename:, content_type:, content_length:, checksum:)
+ {}
+ end
+
+ private
+ def instrument(operation, payload = {}, &block)
+ ActiveSupport::Notifications.instrument(
+ "service_#{operation}.active_storage",
+ payload.merge(service: service_name), &block)
+ end
+
+ def service_name
+ # ActiveStorage::Service::DiskService => Disk
+ self.class.name.split("::").third.remove("Service")
+ end
+
+ def content_disposition_with(type: "inline", filename:)
+ disposition = (type.to_s.presence_in(%w( attachment inline )) || "inline")
+ ActionDispatch::Http::ContentDisposition.format(disposition: disposition, filename: filename.sanitized)
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/service/azure_storage_service.rb b/activestorage/lib/active_storage/service/azure_storage_service.rb
new file mode 100644
index 0000000000..8de3889cb5
--- /dev/null
+++ b/activestorage/lib/active_storage/service/azure_storage_service.rb
@@ -0,0 +1,167 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/numeric/bytes"
+require "azure/storage"
+require "azure/storage/core/auth/shared_access_signature"
+
+module ActiveStorage
+ # Wraps the Microsoft Azure Storage Blob Service as an Active Storage service.
+ # See ActiveStorage::Service for the generic API documentation that applies to all services.
+ class Service::AzureStorageService < Service
+ attr_reader :client, :blobs, :container, :signer
+
+ def initialize(storage_account_name:, storage_access_key:, container:)
+ @client = Azure::Storage::Client.create(storage_account_name: storage_account_name, storage_access_key: storage_access_key)
+ @signer = Azure::Storage::Core::Auth::SharedAccessSignature.new(storage_account_name, storage_access_key)
+ @blobs = client.blob_client
+ @container = container
+ end
+
+ def upload(key, io, checksum: nil)
+ instrument :upload, key: key, checksum: checksum do
+ handle_errors do
+ blobs.create_block_blob(container, key, IO.try_convert(io) || io, content_md5: checksum)
+ end
+ end
+ end
+
+ def download(key, &block)
+ if block_given?
+ instrument :streaming_download, key: key do
+ stream(key, &block)
+ end
+ else
+ instrument :download, key: key do
+ handle_errors do
+ _, io = blobs.get_blob(container, key)
+ io.force_encoding(Encoding::BINARY)
+ end
+ end
+ end
+ end
+
+ def download_chunk(key, range)
+ instrument :download_chunk, key: key, range: range do
+ handle_errors do
+ _, io = blobs.get_blob(container, key, start_range: range.begin, end_range: range.exclude_end? ? range.end - 1 : range.end)
+ io.force_encoding(Encoding::BINARY)
+ end
+ end
+ end
+
+ def delete(key)
+ instrument :delete, key: key do
+ begin
+ blobs.delete_blob(container, key)
+ rescue Azure::Core::Http::HTTPError => e
+ raise unless e.type == "BlobNotFound"
+ # Ignore files already deleted
+ end
+ end
+ end
+
+ def delete_prefixed(prefix)
+ instrument :delete_prefixed, prefix: prefix do
+ marker = nil
+
+ loop do
+ results = blobs.list_blobs(container, prefix: prefix, marker: marker)
+
+ results.each do |blob|
+ blobs.delete_blob(container, blob.name)
+ end
+
+ break unless marker = results.continuation_token.presence
+ end
+ end
+ end
+
+ def exist?(key)
+ instrument :exist, key: key do |payload|
+ answer = blob_for(key).present?
+ payload[:exist] = answer
+ answer
+ end
+ end
+
+ def url(key, expires_in:, filename:, disposition:, content_type:)
+ instrument :url, key: key do |payload|
+ generated_url = signer.signed_uri(
+ uri_for(key), false,
+ service: "b",
+ permissions: "r",
+ expiry: format_expiry(expires_in),
+ content_disposition: content_disposition_with(type: disposition, filename: filename),
+ content_type: content_type
+ ).to_s
+
+ payload[:url] = generated_url
+
+ generated_url
+ end
+ end
+
+ def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
+ instrument :url, key: key do |payload|
+ generated_url = signer.signed_uri(
+ uri_for(key), false,
+ service: "b",
+ permissions: "rw",
+ expiry: format_expiry(expires_in)
+ ).to_s
+
+ payload[:url] = generated_url
+
+ generated_url
+ end
+ end
+
+ def headers_for_direct_upload(key, content_type:, checksum:, **)
+ { "Content-Type" => content_type, "Content-MD5" => checksum, "x-ms-blob-type" => "BlockBlob" }
+ end
+
+ private
+ def uri_for(key)
+ blobs.generate_uri("#{container}/#{key}")
+ end
+
+ def blob_for(key)
+ blobs.get_blob_properties(container, key)
+ rescue Azure::Core::Http::HTTPError
+ false
+ end
+
+ def format_expiry(expires_in)
+ expires_in ? Time.now.utc.advance(seconds: expires_in).iso8601 : nil
+ end
+
+ # Reads the object for the given key in chunks, yielding each to the block.
+ def stream(key)
+ blob = blob_for(key)
+
+ chunk_size = 5.megabytes
+ offset = 0
+
+ raise ActiveStorage::FileNotFoundError unless blob.present?
+
+ while offset < blob.properties[:content_length]
+ _, chunk = blobs.get_blob(container, key, start_range: offset, end_range: offset + chunk_size - 1)
+ yield chunk.force_encoding(Encoding::BINARY)
+ offset += chunk_size
+ end
+ end
+
+ def handle_errors
+ yield
+ rescue Azure::Core::Http::HTTPError => e
+ case e.type
+ when "BlobNotFound"
+ raise ActiveStorage::FileNotFoundError
+ when "Md5Mismatch"
+ raise ActiveStorage::IntegrityError
+ else
+ raise
+ end
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/service/configurator.rb b/activestorage/lib/active_storage/service/configurator.rb
new file mode 100644
index 0000000000..fa80c66c3b
--- /dev/null
+++ b/activestorage/lib/active_storage/service/configurator.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ class Service::Configurator #:nodoc:
+ attr_reader :configurations
+
+ def self.build(service_name, configurations)
+ new(configurations).build(service_name)
+ end
+
+ def initialize(configurations)
+ @configurations = configurations.deep_symbolize_keys
+ end
+
+ def build(service_name)
+ config = config_for(service_name.to_sym)
+ resolve(config.fetch(:service)).build(**config, configurator: self)
+ end
+
+ private
+ def config_for(name)
+ configurations.fetch name do
+ raise "Missing configuration for the #{name.inspect} Active Storage service. Configurations available for #{configurations.keys.inspect}"
+ end
+ end
+
+ def resolve(class_name)
+ require "active_storage/service/#{class_name.to_s.underscore}_service"
+ ActiveStorage::Service.const_get(:"#{class_name.camelize}Service")
+ rescue LoadError
+ raise "Missing service adapter for #{class_name.inspect}"
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/service/disk_service.rb b/activestorage/lib/active_storage/service/disk_service.rb
new file mode 100644
index 0000000000..52f3a3df16
--- /dev/null
+++ b/activestorage/lib/active_storage/service/disk_service.rb
@@ -0,0 +1,162 @@
+# frozen_string_literal: true
+
+require "fileutils"
+require "pathname"
+require "digest/md5"
+require "active_support/core_ext/numeric/bytes"
+
+module ActiveStorage
+ # Wraps a local disk path as an Active Storage service. See ActiveStorage::Service for the generic API
+ # documentation that applies to all services.
+ class Service::DiskService < Service
+ attr_reader :root
+
+ def initialize(root:)
+ @root = root
+ end
+
+ def upload(key, io, checksum: nil)
+ instrument :upload, key: key, checksum: checksum do
+ IO.copy_stream(io, make_path_for(key))
+ ensure_integrity_of(key, checksum) if checksum
+ end
+ end
+
+ def download(key, &block)
+ if block_given?
+ instrument :streaming_download, key: key do
+ stream key, &block
+ end
+ else
+ instrument :download, key: key do
+ begin
+ File.binread path_for(key)
+ rescue Errno::ENOENT
+ raise ActiveStorage::FileNotFoundError
+ end
+ end
+ end
+ end
+
+ def download_chunk(key, range)
+ instrument :download_chunk, key: key, range: range do
+ begin
+ File.open(path_for(key), "rb") do |file|
+ file.seek range.begin
+ file.read range.size
+ end
+ rescue Errno::ENOENT
+ raise ActiveStorage::FileNotFoundError
+ end
+ end
+ end
+
+ def delete(key)
+ instrument :delete, key: key do
+ begin
+ File.delete path_for(key)
+ rescue Errno::ENOENT
+ # Ignore files already deleted
+ end
+ end
+ end
+
+ def delete_prefixed(prefix)
+ instrument :delete_prefixed, prefix: prefix do
+ Dir.glob(path_for("#{prefix}*")).each do |path|
+ FileUtils.rm_rf(path)
+ end
+ end
+ end
+
+ def exist?(key)
+ instrument :exist, key: key do |payload|
+ answer = File.exist? path_for(key)
+ payload[:exist] = answer
+ answer
+ end
+ end
+
+ def url(key, expires_in:, filename:, disposition:, content_type:)
+ instrument :url, key: key do |payload|
+ verified_key_with_expiration = ActiveStorage.verifier.generate(key, expires_in: expires_in, purpose: :blob_key)
+
+ generated_url =
+ url_helpers.rails_disk_service_url(
+ verified_key_with_expiration,
+ host: current_host,
+ filename: filename,
+ disposition: content_disposition_with(type: disposition, filename: filename),
+ content_type: content_type
+ )
+
+ payload[:url] = generated_url
+
+ generated_url
+ end
+ end
+
+ def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
+ instrument :url, key: key do |payload|
+ verified_token_with_expiration = ActiveStorage.verifier.generate(
+ {
+ key: key,
+ content_type: content_type,
+ content_length: content_length,
+ checksum: checksum
+ },
+ { expires_in: expires_in,
+ purpose: :blob_token }
+ )
+
+ generated_url = url_helpers.update_rails_disk_service_url(verified_token_with_expiration, host: current_host)
+
+ payload[:url] = generated_url
+
+ generated_url
+ end
+ end
+
+ def headers_for_direct_upload(key, content_type:, **)
+ { "Content-Type" => content_type }
+ end
+
+ def path_for(key) #:nodoc:
+ File.join root, folder_for(key), key
+ end
+
+ private
+ def stream(key)
+ File.open(path_for(key), "rb") do |file|
+ while data = file.read(5.megabytes)
+ yield data
+ end
+ end
+ rescue Errno::ENOENT
+ raise ActiveStorage::FileNotFoundError
+ end
+
+ def folder_for(key)
+ [ key[0..1], key[2..3] ].join("/")
+ end
+
+ def make_path_for(key)
+ path_for(key).tap { |path| FileUtils.mkdir_p File.dirname(path) }
+ end
+
+ def ensure_integrity_of(key, checksum)
+ unless Digest::MD5.file(path_for(key)).base64digest == checksum
+ delete key
+ raise ActiveStorage::IntegrityError
+ end
+ end
+
+ def url_helpers
+ @url_helpers ||= Rails.application.routes.url_helpers
+ end
+
+ def current_host
+ ActiveStorage::Current.host
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/service/gcs_service.rb b/activestorage/lib/active_storage/service/gcs_service.rb
new file mode 100644
index 0000000000..18c0f14cfc
--- /dev/null
+++ b/activestorage/lib/active_storage/service/gcs_service.rb
@@ -0,0 +1,143 @@
+# frozen_string_literal: true
+
+gem "google-cloud-storage", "~> 1.11"
+require "google/cloud/storage"
+
+module ActiveStorage
+ # Wraps the Google Cloud Storage as an Active Storage service. See ActiveStorage::Service for the generic API
+ # documentation that applies to all services.
+ class Service::GCSService < Service
+ def initialize(**config)
+ @config = config
+ end
+
+ def upload(key, io, checksum: nil)
+ instrument :upload, key: key, checksum: checksum do
+ begin
+ # The official GCS client library doesn't allow us to create a file with no Content-Type metadata.
+ # We need the file we create to have no Content-Type so we can control it via the response-content-type
+ # param in signed URLs. Workaround: let the GCS client create the file with an inferred
+ # Content-Type (usually "application/octet-stream") then clear it.
+ bucket.create_file(io, key, md5: checksum).update do |file|
+ file.content_type = nil
+ end
+ rescue Google::Cloud::InvalidArgumentError
+ raise ActiveStorage::IntegrityError
+ end
+ end
+ end
+
+ def download(key, &block)
+ if block_given?
+ instrument :streaming_download, key: key do
+ stream(key, &block)
+ end
+ else
+ instrument :download, key: key do
+ begin
+ file_for(key).download.string
+ rescue Google::Cloud::NotFoundError
+ raise ActiveStorage::FileNotFoundError
+ end
+ end
+ end
+ end
+
+ def download_chunk(key, range)
+ instrument :download_chunk, key: key, range: range do
+ begin
+ file_for(key).download(range: range).string
+ rescue Google::Cloud::NotFoundError
+ raise ActiveStorage::FileNotFoundError
+ end
+ end
+ end
+
+ def delete(key)
+ instrument :delete, key: key do
+ begin
+ file_for(key).delete
+ rescue Google::Cloud::NotFoundError
+ # Ignore files already deleted
+ end
+ end
+ end
+
+ def delete_prefixed(prefix)
+ instrument :delete_prefixed, prefix: prefix do
+ bucket.files(prefix: prefix).all do |file|
+ begin
+ file.delete
+ rescue Google::Cloud::NotFoundError
+ # Ignore concurrently-deleted files
+ end
+ end
+ end
+ end
+
+ def exist?(key)
+ instrument :exist, key: key do |payload|
+ answer = file_for(key).exists?
+ payload[:exist] = answer
+ answer
+ end
+ end
+
+ def url(key, expires_in:, filename:, content_type:, disposition:)
+ instrument :url, key: key do |payload|
+ generated_url = file_for(key).signed_url expires: expires_in, query: {
+ "response-content-disposition" => content_disposition_with(type: disposition, filename: filename),
+ "response-content-type" => content_type
+ }
+
+ payload[:url] = generated_url
+
+ generated_url
+ end
+ end
+
+ def url_for_direct_upload(key, expires_in:, checksum:, **)
+ instrument :url, key: key do |payload|
+ generated_url = bucket.signed_url key, method: "PUT", expires: expires_in, content_md5: checksum
+
+ payload[:url] = generated_url
+
+ generated_url
+ end
+ end
+
+ def headers_for_direct_upload(key, checksum:, **)
+ { "Content-MD5" => checksum }
+ end
+
+ private
+ attr_reader :config
+
+ def file_for(key, skip_lookup: true)
+ bucket.file(key, skip_lookup: skip_lookup)
+ end
+
+ # Reads the file for the given key in chunks, yielding each to the block.
+ def stream(key)
+ file = file_for(key, skip_lookup: false)
+
+ chunk_size = 5.megabytes
+ offset = 0
+
+ raise ActiveStorage::FileNotFoundError unless file.present?
+
+ while offset < file.size
+ yield file.download(range: offset..(offset + chunk_size - 1)).string
+ offset += chunk_size
+ end
+ end
+
+ def bucket
+ @bucket ||= client.bucket(config.fetch(:bucket))
+ end
+
+ def client
+ @client ||= Google::Cloud::Storage.new(config.except(:bucket))
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/service/mirror_service.rb b/activestorage/lib/active_storage/service/mirror_service.rb
new file mode 100644
index 0000000000..6002ef5a00
--- /dev/null
+++ b/activestorage/lib/active_storage/service/mirror_service.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/delegation"
+
+module ActiveStorage
+ # Wraps a set of mirror services and provides a single ActiveStorage::Service object that will all
+ # have the files uploaded to them. A +primary+ service is designated to answer calls to +download+, +exists?+,
+ # and +url+.
+ class Service::MirrorService < Service
+ attr_reader :primary, :mirrors
+
+ delegate :download, :download_chunk, :exist?, :url, to: :primary
+
+ # Stitch together from named services.
+ def self.build(primary:, mirrors:, configurator:, **options) #:nodoc:
+ new \
+ primary: configurator.build(primary),
+ mirrors: mirrors.collect { |name| configurator.build name }
+ end
+
+ def initialize(primary:, mirrors:)
+ @primary, @mirrors = primary, mirrors
+ end
+
+ # Upload the +io+ to the +key+ specified to all services. If a +checksum+ is provided, all services will
+ # ensure a match when the upload has completed or raise an ActiveStorage::IntegrityError.
+ def upload(key, io, checksum: nil)
+ each_service.collect do |service|
+ service.upload key, io.tap(&:rewind), checksum: checksum
+ end
+ end
+
+ # Delete the file at the +key+ on all services.
+ def delete(key)
+ perform_across_services :delete, key
+ end
+
+ # Delete files at keys starting with the +prefix+ on all services.
+ def delete_prefixed(prefix)
+ perform_across_services :delete_prefixed, prefix
+ end
+
+ private
+ def each_service(&block)
+ [ primary, *mirrors ].each(&block)
+ end
+
+ def perform_across_services(method, *args)
+ # FIXME: Convert to be threaded
+ each_service.collect do |service|
+ service.public_send method, *args
+ end
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/service/s3_service.rb b/activestorage/lib/active_storage/service/s3_service.rb
new file mode 100644
index 0000000000..89a9e54158
--- /dev/null
+++ b/activestorage/lib/active_storage/service/s3_service.rb
@@ -0,0 +1,122 @@
+# frozen_string_literal: true
+
+require "aws-sdk-s3"
+require "active_support/core_ext/numeric/bytes"
+
+module ActiveStorage
+ # Wraps the Amazon Simple Storage Service (S3) as an Active Storage service.
+ # See ActiveStorage::Service for the generic API documentation that applies to all services.
+ class Service::S3Service < Service
+ attr_reader :client, :bucket, :upload_options
+
+ def initialize(bucket:, upload: {}, **options)
+ @client = Aws::S3::Resource.new(**options)
+ @bucket = @client.bucket(bucket)
+
+ @upload_options = upload
+ end
+
+ def upload(key, io, checksum: nil)
+ instrument :upload, key: key, checksum: checksum do
+ begin
+ object_for(key).put(upload_options.merge(body: io, content_md5: checksum))
+ rescue Aws::S3::Errors::BadDigest
+ raise ActiveStorage::IntegrityError
+ end
+ end
+ end
+
+ def download(key, &block)
+ if block_given?
+ instrument :streaming_download, key: key do
+ stream(key, &block)
+ end
+ else
+ instrument :download, key: key do
+ begin
+ object_for(key).get.body.string.force_encoding(Encoding::BINARY)
+ rescue Aws::S3::Errors::NoSuchKey
+ raise ActiveStorage::FileNotFoundError
+ end
+ end
+ end
+ end
+
+ def download_chunk(key, range)
+ instrument :download_chunk, key: key, range: range do
+ begin
+ object_for(key).get(range: "bytes=#{range.begin}-#{range.exclude_end? ? range.end - 1 : range.end}").body.read.force_encoding(Encoding::BINARY)
+ rescue Aws::S3::Errors::NoSuchKey
+ raise ActiveStorage::FileNotFoundError
+ end
+ end
+ end
+
+ def delete(key)
+ instrument :delete, key: key do
+ object_for(key).delete
+ end
+ end
+
+ def delete_prefixed(prefix)
+ instrument :delete_prefixed, prefix: prefix do
+ bucket.objects(prefix: prefix).batch_delete!
+ end
+ end
+
+ def exist?(key)
+ instrument :exist, key: key do |payload|
+ answer = object_for(key).exists?
+ payload[:exist] = answer
+ answer
+ end
+ end
+
+ def url(key, expires_in:, filename:, disposition:, content_type:)
+ instrument :url, key: key do |payload|
+ generated_url = object_for(key).presigned_url :get, expires_in: expires_in.to_i,
+ response_content_disposition: content_disposition_with(type: disposition, filename: filename),
+ response_content_type: content_type
+
+ payload[:url] = generated_url
+
+ generated_url
+ end
+ end
+
+ def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
+ instrument :url, key: key do |payload|
+ generated_url = object_for(key).presigned_url :put, expires_in: expires_in.to_i,
+ content_type: content_type, content_length: content_length, content_md5: checksum
+
+ payload[:url] = generated_url
+
+ generated_url
+ end
+ end
+
+ def headers_for_direct_upload(key, content_type:, checksum:, **)
+ { "Content-Type" => content_type, "Content-MD5" => checksum }
+ end
+
+ private
+ def object_for(key)
+ bucket.object(key)
+ end
+
+ # Reads the object for the given key in chunks, yielding each to the block.
+ def stream(key)
+ object = object_for(key)
+
+ chunk_size = 5.megabytes
+ offset = 0
+
+ raise ActiveStorage::FileNotFoundError unless object.exists?
+
+ while offset < object.content_length
+ yield object.get(range: "bytes=#{offset}-#{offset + chunk_size - 1}").body.read.force_encoding(Encoding::BINARY)
+ offset += chunk_size
+ end
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/transformers/image_processing_transformer.rb b/activestorage/lib/active_storage/transformers/image_processing_transformer.rb
new file mode 100644
index 0000000000..7f8685b72d
--- /dev/null
+++ b/activestorage/lib/active_storage/transformers/image_processing_transformer.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require "image_processing"
+
+module ActiveStorage
+ module Transformers
+ class ImageProcessingTransformer < Transformer
+ private
+ def process(file, format:)
+ processor.
+ source(file).
+ loader(page: 0).
+ convert(format).
+ apply(operations).
+ call
+ end
+
+ def processor
+ ImageProcessing.const_get(ActiveStorage.variant_processor.to_s.camelize)
+ end
+
+ def operations
+ transformations.each_with_object([]) do |(name, argument), list|
+ if name.to_s == "combine_options"
+ ActiveSupport::Deprecation.warn <<~WARNING
+ Active Storage's ImageProcessing transformer doesn't support :combine_options,
+ as it always generates a single ImageMagick command. Passing :combine_options will
+ not be supported in Rails 6.1.
+ WARNING
+
+ list.concat argument.keep_if { |key, value| value.present? }.to_a
+ elsif argument.present?
+ list << [ name, argument ]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/transformers/mini_magick_transformer.rb b/activestorage/lib/active_storage/transformers/mini_magick_transformer.rb
new file mode 100644
index 0000000000..e8e99cea9e
--- /dev/null
+++ b/activestorage/lib/active_storage/transformers/mini_magick_transformer.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require "mini_magick"
+
+module ActiveStorage
+ module Transformers
+ class MiniMagickTransformer < Transformer
+ private
+ def process(file, format:)
+ image = MiniMagick::Image.new(file.path, file)
+
+ transformations.each do |name, argument_or_subtransformations|
+ image.mogrify do |command|
+ if name.to_s == "combine_options"
+ argument_or_subtransformations.each do |subtransformation_name, subtransformation_argument|
+ pass_transform_argument(command, subtransformation_name, subtransformation_argument)
+ end
+ else
+ pass_transform_argument(command, name, argument_or_subtransformations)
+ end
+ end
+ end
+
+ image.format(format) if format
+
+ image.tempfile.tap(&:open)
+ end
+
+ def pass_transform_argument(command, method, argument)
+ if argument == true
+ command.public_send(method)
+ elsif argument.present?
+ command.public_send(method, argument)
+ end
+ end
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/transformers/transformer.rb b/activestorage/lib/active_storage/transformers/transformer.rb
new file mode 100644
index 0000000000..2e21201004
--- /dev/null
+++ b/activestorage/lib/active_storage/transformers/transformer.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ module Transformers
+ # A Transformer applies a set of transformations to an image.
+ #
+ # The following concrete subclasses are included in Active Storage:
+ #
+ # * ActiveStorage::Transformers::ImageProcessingTransformer:
+ # backed by ImageProcessing, a common interface for MiniMagick and ruby-vips
+ #
+ # * ActiveStorage::Transformers::MiniMagickTransformer:
+ # backed by MiniMagick, a wrapper around the ImageMagick CLI
+ class Transformer
+ attr_reader :transformations
+
+ def initialize(transformations)
+ @transformations = transformations
+ end
+
+ # Applies the transformations to the source image in +file+, producing a target image in the
+ # specified +format+. Yields an open Tempfile containing the target image. Closes and unlinks
+ # the output tempfile after yielding to the given block. Returns the result of the block.
+ def transform(file, format:)
+ output = process(file, format: format)
+
+ begin
+ yield output
+ ensure
+ output.close!
+ end
+ end
+
+ private
+ # Returns an open Tempfile containing a transformed image in the given +format+.
+ # All subclasses implement this method.
+ def process(file, format:) #:doc:
+ raise NotImplementedError
+ end
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/version.rb b/activestorage/lib/active_storage/version.rb
new file mode 100644
index 0000000000..4b6631832b
--- /dev/null
+++ b/activestorage/lib/active_storage/version.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require_relative "gem_version"
+
+module ActiveStorage
+ # Returns the version of the currently loaded ActiveStorage as a <tt>Gem::Version</tt>
+ def self.version
+ gem_version
+ end
+end
diff --git a/activestorage/lib/tasks/activestorage.rake b/activestorage/lib/tasks/activestorage.rake
new file mode 100644
index 0000000000..ac254d717f
--- /dev/null
+++ b/activestorage/lib/tasks/activestorage.rake
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+namespace :active_storage do
+ # Prevent migration installation task from showing up twice.
+ Rake::Task["install:migrations"].clear_comments
+
+ desc "Copy over the migration needed to the application"
+ task install: :environment do
+ if Rake::Task.task_defined?("active_storage:install:migrations")
+ Rake::Task["active_storage:install:migrations"].invoke
+ else
+ Rake::Task["app:active_storage:install:migrations"].invoke
+ end
+ end
+end
diff --git a/activestorage/package.json b/activestorage/package.json
new file mode 100644
index 0000000000..00876985cf
--- /dev/null
+++ b/activestorage/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "activestorage",
+ "version": "6.0.0-alpha",
+ "description": "Attach cloud and local files in Rails applications",
+ "main": "app/assets/javascripts/activestorage.js",
+ "files": [
+ "app/assets/javascripts/*.js",
+ "src/*.js"
+ ],
+ "homepage": "http://rubyonrails.org/",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/rails/rails.git"
+ },
+ "bugs": {
+ "url": "https://github.com/rails/rails/issues"
+ },
+ "author": "Javan Makhmali <javan@javan.us>",
+ "license": "MIT",
+ "dependencies": {
+ "spark-md5": "^3.0.0"
+ },
+ "devDependencies": {
+ "babel-core": "^6.25.0",
+ "babel-plugin-external-helpers": "^6.22.0",
+ "babel-preset-env": "^1.6.0",
+ "eslint": "^4.3.0",
+ "eslint-plugin-import": "^2.7.0",
+ "rollup": "^0.58.2",
+ "rollup-plugin-babel": "^3.0.4",
+ "rollup-plugin-commonjs": "^9.1.0",
+ "rollup-plugin-node-resolve": "^3.3.0",
+ "rollup-plugin-uglify": "^3.0.0"
+ },
+ "scripts": {
+ "prebuild": "yarn lint",
+ "build": "rollup --config rollup.config.js",
+ "lint": "eslint app/javascript",
+ "prepublishOnly": "rm -rf src && cp -R app/javascript/activestorage src"
+ }
+}
diff --git a/activestorage/rollup.config.js b/activestorage/rollup.config.js
new file mode 100644
index 0000000000..1b4f9477ab
--- /dev/null
+++ b/activestorage/rollup.config.js
@@ -0,0 +1,28 @@
+import resolve from "rollup-plugin-node-resolve"
+import commonjs from "rollup-plugin-commonjs"
+import babel from "rollup-plugin-babel"
+import uglify from "rollup-plugin-uglify"
+
+const uglifyOptions = {
+ mangle: false,
+ compress: false,
+ output: {
+ beautify: true,
+ indent_level: 2
+ }
+}
+
+export default {
+ input: "app/javascript/activestorage/index.js",
+ output: {
+ file: "app/assets/javascripts/activestorage.js",
+ format: "umd",
+ name: "ActiveStorage"
+ },
+ plugins: [
+ resolve(),
+ commonjs(),
+ babel(),
+ uglify(uglifyOptions)
+ ]
+}
diff --git a/activestorage/test/analyzer/image_analyzer_test.rb b/activestorage/test/analyzer/image_analyzer_test.rb
new file mode 100644
index 0000000000..55bb5e7280
--- /dev/null
+++ b/activestorage/test/analyzer/image_analyzer_test.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+require "active_storage/analyzer/image_analyzer"
+
+class ActiveStorage::Analyzer::ImageAnalyzerTest < ActiveSupport::TestCase
+ test "analyzing a JPEG image" do
+ blob = create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg")
+ metadata = extract_metadata_from(blob)
+
+ assert_equal 4104, metadata[:width]
+ assert_equal 2736, metadata[:height]
+ end
+
+ test "analyzing a rotated JPEG image" do
+ blob = create_file_blob(filename: "racecar_rotated.jpg", content_type: "image/jpeg")
+ metadata = extract_metadata_from(blob)
+
+ assert_equal 2736, metadata[:width]
+ assert_equal 4104, metadata[:height]
+ end
+
+ test "analyzing an SVG image without an XML declaration" do
+ blob = create_file_blob(filename: "icon.svg", content_type: "image/svg+xml")
+ metadata = extract_metadata_from(blob)
+
+ assert_equal 792, metadata[:width]
+ assert_equal 584, metadata[:height]
+ end
+end
diff --git a/activestorage/test/analyzer/video_analyzer_test.rb b/activestorage/test/analyzer/video_analyzer_test.rb
new file mode 100644
index 0000000000..d30f49315a
--- /dev/null
+++ b/activestorage/test/analyzer/video_analyzer_test.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+require "active_storage/analyzer/video_analyzer"
+
+class ActiveStorage::Analyzer::VideoAnalyzerTest < ActiveSupport::TestCase
+ test "analyzing a video" do
+ blob = create_file_blob(filename: "video.mp4", content_type: "video/mp4")
+ metadata = extract_metadata_from(blob)
+
+ assert_equal 640, metadata[:width]
+ assert_equal 480, metadata[:height]
+ assert_equal [4, 3], metadata[:display_aspect_ratio]
+ assert_equal 5.166648, metadata[:duration]
+ assert_not_includes metadata, :angle
+ end
+
+ test "analyzing a rotated video" do
+ blob = create_file_blob(filename: "rotated_video.mp4", content_type: "video/mp4")
+ metadata = extract_metadata_from(blob)
+
+ assert_equal 480, metadata[:width]
+ assert_equal 640, metadata[:height]
+ assert_equal [4, 3], metadata[:display_aspect_ratio]
+ assert_equal 5.227975, metadata[:duration]
+ assert_equal 90, metadata[:angle]
+ end
+
+ test "analyzing a video with rectangular samples" do
+ blob = create_file_blob(filename: "video_with_rectangular_samples.mp4", content_type: "video/mp4")
+ metadata = extract_metadata_from(blob)
+
+ assert_equal 1280, metadata[:width]
+ assert_equal 720, metadata[:height]
+ assert_equal [16, 9], metadata[:display_aspect_ratio]
+ end
+
+ test "analyzing a video with an undefined display aspect ratio" do
+ blob = create_file_blob(filename: "video_with_undefined_display_aspect_ratio.mp4", content_type: "video/mp4")
+ metadata = extract_metadata_from(blob)
+
+ assert_equal 640, metadata[:width]
+ assert_equal 480, metadata[:height]
+ assert_nil metadata[:display_aspect_ratio]
+ end
+
+ test "analyzing a video without a video stream" do
+ blob = create_file_blob(filename: "video_without_video_stream.mp4", content_type: "video/mp4")
+ metadata = extract_metadata_from(blob)
+ assert_equal({ "analyzed" => true, "identified" => true }, metadata)
+ end
+end
diff --git a/activestorage/test/controllers/blobs_controller_test.rb b/activestorage/test/controllers/blobs_controller_test.rb
new file mode 100644
index 0000000000..9c811df895
--- /dev/null
+++ b/activestorage/test/controllers/blobs_controller_test.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+class ActiveStorage::BlobsControllerTest < ActionDispatch::IntegrationTest
+ setup do
+ @blob = create_file_blob filename: "racecar.jpg"
+ end
+
+ test "showing blob with invalid signed ID" do
+ get rails_service_blob_url("invalid", "racecar.jpg")
+ assert_response :not_found
+ end
+
+ test "showing blob utilizes browser caching" do
+ get rails_blob_url(@blob)
+
+ assert_redirected_to(/racecar\.jpg/)
+ assert_equal "max-age=300, private", @response.headers["Cache-Control"]
+ end
+end
diff --git a/activestorage/test/controllers/direct_uploads_controller_test.rb b/activestorage/test/controllers/direct_uploads_controller_test.rb
new file mode 100644
index 0000000000..1b16da17d9
--- /dev/null
+++ b/activestorage/test/controllers/direct_uploads_controller_test.rb
@@ -0,0 +1,147 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+if SERVICE_CONFIGURATIONS[:s3] && SERVICE_CONFIGURATIONS[:s3][:access_key_id].present?
+ class ActiveStorage::S3DirectUploadsControllerTest < ActionDispatch::IntegrationTest
+ setup do
+ @old_service = ActiveStorage::Blob.service
+ ActiveStorage::Blob.service = ActiveStorage::Service.configure(:s3, SERVICE_CONFIGURATIONS)
+ end
+
+ teardown do
+ ActiveStorage::Blob.service = @old_service
+ end
+
+ test "creating new direct upload" do
+ checksum = Digest::MD5.base64digest("Hello")
+
+ post rails_direct_uploads_url, params: { blob: {
+ filename: "hello.txt", byte_size: 6, checksum: checksum, content_type: "text/plain" } }
+
+ response.parsed_body.tap do |details|
+ assert_equal ActiveStorage::Blob.find(details["id"]), ActiveStorage::Blob.find_signed(details["signed_id"])
+ assert_equal "hello.txt", details["filename"]
+ assert_equal 6, details["byte_size"]
+ assert_equal checksum, details["checksum"]
+ assert_equal "text/plain", details["content_type"]
+ assert_match SERVICE_CONFIGURATIONS[:s3][:bucket], details["direct_upload"]["url"]
+ assert_match(/s3(-[-a-z0-9]+)?\.(\S+)?amazonaws\.com/, details["direct_upload"]["url"])
+ assert_equal({ "Content-Type" => "text/plain", "Content-MD5" => checksum }, details["direct_upload"]["headers"])
+ end
+ end
+ end
+else
+ puts "Skipping S3 Direct Upload tests because no S3 configuration was supplied"
+end
+
+if SERVICE_CONFIGURATIONS[:gcs]
+ class ActiveStorage::GCSDirectUploadsControllerTest < ActionDispatch::IntegrationTest
+ setup do
+ @config = SERVICE_CONFIGURATIONS[:gcs]
+
+ @old_service = ActiveStorage::Blob.service
+ ActiveStorage::Blob.service = ActiveStorage::Service.configure(:gcs, SERVICE_CONFIGURATIONS)
+ end
+
+ teardown do
+ ActiveStorage::Blob.service = @old_service
+ end
+
+ test "creating new direct upload" do
+ checksum = Digest::MD5.base64digest("Hello")
+
+ post rails_direct_uploads_url, params: { blob: {
+ filename: "hello.txt", byte_size: 6, checksum: checksum, content_type: "text/plain" } }
+
+ @response.parsed_body.tap do |details|
+ assert_equal ActiveStorage::Blob.find(details["id"]), ActiveStorage::Blob.find_signed(details["signed_id"])
+ assert_equal "hello.txt", details["filename"]
+ assert_equal 6, details["byte_size"]
+ assert_equal checksum, details["checksum"]
+ assert_equal "text/plain", details["content_type"]
+ assert_match %r{storage\.googleapis\.com/#{@config[:bucket]}}, details["direct_upload"]["url"]
+ assert_equal({ "Content-MD5" => checksum }, details["direct_upload"]["headers"])
+ end
+ end
+ end
+else
+ puts "Skipping GCS Direct Upload tests because no GCS configuration was supplied"
+end
+
+if SERVICE_CONFIGURATIONS[:azure]
+ class ActiveStorage::AzureStorageDirectUploadsControllerTest < ActionDispatch::IntegrationTest
+ setup do
+ @config = SERVICE_CONFIGURATIONS[:azure]
+
+ @old_service = ActiveStorage::Blob.service
+ ActiveStorage::Blob.service = ActiveStorage::Service.configure(:azure, SERVICE_CONFIGURATIONS)
+ end
+
+ teardown do
+ ActiveStorage::Blob.service = @old_service
+ end
+
+ test "creating new direct upload" do
+ checksum = Digest::MD5.base64digest("Hello")
+
+ post rails_direct_uploads_url, params: { blob: {
+ filename: "hello.txt", byte_size: 6, checksum: checksum, content_type: "text/plain" } }
+
+ @response.parsed_body.tap do |details|
+ assert_equal ActiveStorage::Blob.find(details["id"]), ActiveStorage::Blob.find_signed(details["signed_id"])
+ assert_equal "hello.txt", details["filename"]
+ assert_equal 6, details["byte_size"]
+ assert_equal checksum, details["checksum"]
+ assert_equal "text/plain", details["content_type"]
+ assert_match %r{#{@config[:storage_account_name]}\.blob\.core\.windows\.net/#{@config[:container]}}, details["direct_upload"]["url"]
+ assert_equal({ "Content-Type" => "text/plain", "Content-MD5" => checksum, "x-ms-blob-type" => "BlockBlob" }, details["direct_upload"]["headers"])
+ end
+ end
+ end
+else
+ puts "Skipping Azure Storage Direct Upload tests because no Azure Storage configuration was supplied"
+end
+
+class ActiveStorage::DiskDirectUploadsControllerTest < ActionDispatch::IntegrationTest
+ test "creating new direct upload" do
+ checksum = Digest::MD5.base64digest("Hello")
+
+ post rails_direct_uploads_url, params: { blob: {
+ filename: "hello.txt", byte_size: 6, checksum: checksum, content_type: "text/plain" } }
+
+ @response.parsed_body.tap do |details|
+ assert_equal ActiveStorage::Blob.find(details["id"]), ActiveStorage::Blob.find_signed(details["signed_id"])
+ assert_equal "hello.txt", details["filename"]
+ assert_equal 6, details["byte_size"]
+ assert_equal checksum, details["checksum"]
+ assert_equal "text/plain", details["content_type"]
+ assert_match(/rails\/active_storage\/disk/, details["direct_upload"]["url"])
+ assert_equal({ "Content-Type" => "text/plain" }, details["direct_upload"]["headers"])
+ end
+ end
+
+ test "creating new direct upload does not include root in json" do
+ checksum = Digest::MD5.base64digest("Hello")
+
+ set_include_root_in_json(true) do
+ post rails_direct_uploads_url, params: { blob: {
+ filename: "hello.txt", byte_size: 6, checksum: checksum, content_type: "text/plain" } }
+ end
+
+ @response.parsed_body.tap do |details|
+ assert_nil details["blob"]
+ assert_not_nil details["id"]
+ end
+ end
+
+ private
+ def set_include_root_in_json(value)
+ original = ActiveRecord::Base.include_root_in_json
+ ActiveRecord::Base.include_root_in_json = value
+ yield
+ ensure
+ ActiveRecord::Base.include_root_in_json = original
+ end
+end
diff --git a/activestorage/test/controllers/disk_controller_test.rb b/activestorage/test/controllers/disk_controller_test.rb
new file mode 100644
index 0000000000..7b5e989699
--- /dev/null
+++ b/activestorage/test/controllers/disk_controller_test.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+class ActiveStorage::DiskControllerTest < ActionDispatch::IntegrationTest
+ test "showing blob inline" do
+ blob = create_blob
+ get blob.service_url
+ assert_response :ok
+ assert_equal "inline; filename=\"hello.txt\"; filename*=UTF-8''hello.txt", response.headers["Content-Disposition"]
+ assert_equal "text/plain", response.headers["Content-Type"]
+ assert_equal "Hello world!", response.body
+ end
+
+ test "showing blob as attachment" do
+ blob = create_blob
+ get blob.service_url(disposition: :attachment)
+ assert_response :ok
+ assert_equal "attachment; filename=\"hello.txt\"; filename*=UTF-8''hello.txt", response.headers["Content-Disposition"]
+ assert_equal "text/plain", response.headers["Content-Type"]
+ assert_equal "Hello world!", response.body
+ end
+
+ test "showing blob range inline" do
+ blob = create_blob
+ get blob.service_url, headers: { "Range" => "bytes=5-9" }
+ assert_response :partial_content
+ assert_equal "inline; filename=\"hello.txt\"; filename*=UTF-8''hello.txt", response.headers["Content-Disposition"]
+ assert_equal "text/plain", response.headers["Content-Type"]
+ assert_equal " worl", response.body
+ end
+
+ test "showing blob that does not exist" do
+ blob = create_blob
+ blob.delete
+
+ get blob.service_url
+ assert_response :not_found
+ end
+
+
+ test "directly uploading blob with integrity" do
+ data = "Something else entirely!"
+ blob = create_blob_before_direct_upload byte_size: data.size, checksum: Digest::MD5.base64digest(data)
+
+ put blob.service_url_for_direct_upload, params: data, headers: { "Content-Type" => "text/plain" }
+ assert_response :no_content
+ assert_equal data, blob.download
+ end
+
+ test "directly uploading blob without integrity" do
+ data = "Something else entirely!"
+ blob = create_blob_before_direct_upload byte_size: data.size, checksum: Digest::MD5.base64digest("bad data")
+
+ put blob.service_url_for_direct_upload, params: data
+ assert_response :unprocessable_entity
+ assert_not blob.service.exist?(blob.key)
+ end
+
+ test "directly uploading blob with mismatched content type" do
+ data = "Something else entirely!"
+ blob = create_blob_before_direct_upload byte_size: data.size, checksum: Digest::MD5.base64digest(data)
+
+ put blob.service_url_for_direct_upload, params: data, headers: { "Content-Type" => "application/octet-stream" }
+ assert_response :unprocessable_entity
+ assert_not blob.service.exist?(blob.key)
+ end
+
+ test "directly uploading blob with different but equivalent content type" do
+ data = "Something else entirely!"
+ blob = create_blob_before_direct_upload(
+ byte_size: data.size, checksum: Digest::MD5.base64digest(data), content_type: "application/x-gzip")
+
+ put blob.service_url_for_direct_upload, params: data, headers: { "Content-Type" => "application/x-gzip" }
+ assert_response :no_content
+ assert_equal data, blob.download
+ end
+
+ test "directly uploading blob with mismatched content length" do
+ data = "Something else entirely!"
+ blob = create_blob_before_direct_upload byte_size: data.size - 1, checksum: Digest::MD5.base64digest(data)
+
+ put blob.service_url_for_direct_upload, params: data, headers: { "Content-Type" => "text/plain" }
+ assert_response :unprocessable_entity
+ assert_not blob.service.exist?(blob.key)
+ end
+
+ test "directly uploading blob with invalid token" do
+ put update_rails_disk_service_url(encoded_token: "invalid"),
+ params: "Something else entirely!", headers: { "Content-Type" => "text/plain" }
+ assert_response :not_found
+ end
+end
diff --git a/activestorage/test/controllers/representations_controller_test.rb b/activestorage/test/controllers/representations_controller_test.rb
new file mode 100644
index 0000000000..2662cc5283
--- /dev/null
+++ b/activestorage/test/controllers/representations_controller_test.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+class ActiveStorage::RepresentationsControllerWithVariantsTest < ActionDispatch::IntegrationTest
+ setup do
+ @blob = create_file_blob filename: "racecar.jpg"
+ end
+
+ test "showing variant inline" do
+ get rails_blob_representation_url(
+ filename: @blob.filename,
+ signed_blob_id: @blob.signed_id,
+ variation_key: ActiveStorage::Variation.encode(resize: "100x100"))
+
+ assert_redirected_to(/racecar\.jpg\?.*disposition=inline/)
+
+ image = read_image(@blob.variant(resize: "100x100"))
+ assert_equal 100, image.width
+ assert_equal 67, image.height
+ end
+
+ test "showing variant with invalid signed blob ID" do
+ get rails_blob_representation_url(
+ filename: @blob.filename,
+ signed_blob_id: "invalid",
+ variation_key: ActiveStorage::Variation.encode(resize: "100x100"))
+
+ assert_response :not_found
+ end
+end
+
+class ActiveStorage::RepresentationsControllerWithPreviewsTest < ActionDispatch::IntegrationTest
+ setup do
+ @blob = create_file_blob filename: "report.pdf", content_type: "application/pdf"
+ end
+
+ test "showing preview inline" do
+ get rails_blob_representation_url(
+ filename: @blob.filename,
+ signed_blob_id: @blob.signed_id,
+ variation_key: ActiveStorage::Variation.encode(resize: "100x100"))
+
+ assert_predicate @blob.preview_image, :attached?
+ assert_redirected_to(/report\.png\?.*disposition=inline/)
+
+ image = read_image(@blob.preview_image.variant(resize: "100x100"))
+ assert_equal 77, image.width
+ assert_equal 100, image.height
+ end
+
+ test "showing preview with invalid signed blob ID" do
+ get rails_blob_representation_url(
+ filename: @blob.filename,
+ signed_blob_id: "invalid",
+ variation_key: ActiveStorage::Variation.encode(resize: "100x100"))
+
+ assert_response :not_found
+ end
+end
diff --git a/activestorage/test/database/create_users_migration.rb b/activestorage/test/database/create_users_migration.rb
new file mode 100644
index 0000000000..fdba87cacf
--- /dev/null
+++ b/activestorage/test/database/create_users_migration.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class ActiveStorageCreateUsers < ActiveRecord::Migration[5.2]
+ def change
+ create_table :users do |t|
+ t.string :name
+ end
+ end
+end
diff --git a/activestorage/test/database/setup.rb b/activestorage/test/database/setup.rb
new file mode 100644
index 0000000000..daeeb5695b
--- /dev/null
+++ b/activestorage/test/database/setup.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require_relative "create_users_migration"
+
+ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
+ActiveRecord::Base.connection.migration_context.migrate
+ActiveStorageCreateUsers.migrate(:up)
diff --git a/activestorage/test/dummy/Rakefile b/activestorage/test/dummy/Rakefile
new file mode 100644
index 0000000000..c4f9523878
--- /dev/null
+++ b/activestorage/test/dummy/Rakefile
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+require_relative "config/application"
+
+Rails.application.load_tasks
diff --git a/activestorage/test/dummy/app/assets/config/manifest.js b/activestorage/test/dummy/app/assets/config/manifest.js
new file mode 100644
index 0000000000..bb109908b2
--- /dev/null
+++ b/activestorage/test/dummy/app/assets/config/manifest.js
@@ -0,0 +1,3 @@
+
+//= link_tree ../images
+//= link_directory ../stylesheets .css
diff --git a/activerecord/test/migrations/empty/.gitkeep b/activestorage/test/dummy/app/assets/images/.keep
index e69de29bb2..e69de29bb2 100644
--- a/activerecord/test/migrations/empty/.gitkeep
+++ b/activestorage/test/dummy/app/assets/images/.keep
diff --git a/activestorage/test/dummy/app/assets/javascripts/application.js b/activestorage/test/dummy/app/assets/javascripts/application.js
new file mode 100644
index 0000000000..e54c6461cc
--- /dev/null
+++ b/activestorage/test/dummy/app/assets/javascripts/application.js
@@ -0,0 +1,13 @@
+// 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.
+//
+// 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.
+//
+// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
+// about supported directives.
+//
+//= require_tree .
diff --git a/activestorage/test/dummy/app/assets/stylesheets/application.css b/activestorage/test/dummy/app/assets/stylesheets/application.css
new file mode 100644
index 0000000000..0ebd7fe829
--- /dev/null
+++ b/activestorage/test/dummy/app/assets/stylesheets/application.css
@@ -0,0 +1,15 @@
+/*
+ * 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.
+ *
+ * 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
+ * files in this directory. Styles in this file should be added after the last require_* statement.
+ * It is generally better to create a new file per style scope.
+ *
+ *= require_tree .
+ *= require_self
+ */
diff --git a/activestorage/test/dummy/app/controllers/application_controller.rb b/activestorage/test/dummy/app/controllers/application_controller.rb
new file mode 100644
index 0000000000..280cc28ce2
--- /dev/null
+++ b/activestorage/test/dummy/app/controllers/application_controller.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class ApplicationController < ActionController::Base
+ protect_from_forgery with: :exception
+end
diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/mailers/.empty_directory b/activestorage/test/dummy/app/controllers/concerns/.keep
index e69de29bb2..e69de29bb2 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/app/mailers/.empty_directory
+++ b/activestorage/test/dummy/app/controllers/concerns/.keep
diff --git a/activestorage/test/dummy/app/helpers/application_helper.rb b/activestorage/test/dummy/app/helpers/application_helper.rb
new file mode 100644
index 0000000000..15b06f0f67
--- /dev/null
+++ b/activestorage/test/dummy/app/helpers/application_helper.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+module ApplicationHelper
+end
diff --git a/activestorage/test/dummy/app/jobs/application_job.rb b/activestorage/test/dummy/app/jobs/application_job.rb
new file mode 100644
index 0000000000..d92ffddcb5
--- /dev/null
+++ b/activestorage/test/dummy/app/jobs/application_job.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+class ApplicationJob < ActiveJob::Base
+end
diff --git a/activestorage/test/dummy/app/models/application_record.rb b/activestorage/test/dummy/app/models/application_record.rb
new file mode 100644
index 0000000000..71fbba5b32
--- /dev/null
+++ b/activestorage/test/dummy/app/models/application_record.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class ApplicationRecord < ActiveRecord::Base
+ self.abstract_class = true
+end
diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/models/.empty_directory b/activestorage/test/dummy/app/models/concerns/.keep
index e69de29bb2..e69de29bb2 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/app/models/.empty_directory
+++ b/activestorage/test/dummy/app/models/concerns/.keep
diff --git a/activestorage/test/dummy/app/views/layouts/application.html.erb b/activestorage/test/dummy/app/views/layouts/application.html.erb
new file mode 100644
index 0000000000..a6eb0174b7
--- /dev/null
+++ b/activestorage/test/dummy/app/views/layouts/application.html.erb
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Dummy</title>
+ <%= csrf_meta_tags %>
+
+ <%= stylesheet_link_tag 'application', media: 'all' %>
+ <%= javascript_include_tag 'application' %>
+ </head>
+
+ <body>
+ <%= yield %>
+ </body>
+</html>
diff --git a/activestorage/test/dummy/bin/bundle b/activestorage/test/dummy/bin/bundle
new file mode 100755
index 0000000000..5015ba6f8b
--- /dev/null
+++ b/activestorage/test/dummy/bin/bundle
@@ -0,0 +1,5 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
+load Gem.bin_path("bundler", "bundle")
diff --git a/activestorage/test/dummy/bin/rails b/activestorage/test/dummy/bin/rails
new file mode 100755
index 0000000000..22f2d8deee
--- /dev/null
+++ b/activestorage/test/dummy/bin/rails
@@ -0,0 +1,6 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+APP_PATH = File.expand_path("../config/application", __dir__)
+require_relative "../config/boot"
+require "rails/commands"
diff --git a/activestorage/test/dummy/bin/rake b/activestorage/test/dummy/bin/rake
new file mode 100755
index 0000000000..e436ea54a1
--- /dev/null
+++ b/activestorage/test/dummy/bin/rake
@@ -0,0 +1,6 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require_relative "../config/boot"
+require "rake"
+Rake.application.run
diff --git a/activestorage/test/dummy/bin/yarn b/activestorage/test/dummy/bin/yarn
new file mode 100755
index 0000000000..c9b7498378
--- /dev/null
+++ b/activestorage/test/dummy/bin/yarn
@@ -0,0 +1,13 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+VENDOR_PATH = File.expand_path("..", __dir__)
+Dir.chdir(VENDOR_PATH) do
+ begin
+ exec "yarnpkg #{ARGV.join(" ")}"
+ rescue Errno::ENOENT
+ $stderr.puts "Yarn executable was not detected in the system."
+ $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install"
+ exit 1
+ end
+end
diff --git a/activestorage/test/dummy/config.ru b/activestorage/test/dummy/config.ru
new file mode 100644
index 0000000000..bff88d608a
--- /dev/null
+++ b/activestorage/test/dummy/config.ru
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+# This file is used by Rack-based servers to start the application.
+
+require_relative "config/environment"
+
+run Rails.application
diff --git a/activestorage/test/dummy/config/application.rb b/activestorage/test/dummy/config/application.rb
new file mode 100644
index 0000000000..bd14ac0b1a
--- /dev/null
+++ b/activestorage/test/dummy/config/application.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require_relative "boot"
+
+require "rails"
+require "active_model/railtie"
+require "active_job/railtie"
+require "active_record/railtie"
+require "action_controller/railtie"
+require "action_view/railtie"
+require "sprockets/railtie"
+require "active_storage/engine"
+
+Bundler.require(*Rails.groups)
+
+module Dummy
+ class Application < Rails::Application
+ config.load_defaults 5.2
+
+ config.active_storage.service = :local
+ end
+end
diff --git a/activestorage/test/dummy/config/boot.rb b/activestorage/test/dummy/config/boot.rb
new file mode 100644
index 0000000000..59459d4ae3
--- /dev/null
+++ b/activestorage/test/dummy/config/boot.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+# Set up gems listed in the Gemfile.
+ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../Gemfile", __dir__)
+
+require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
+$LOAD_PATH.unshift File.expand_path("../../../lib", __dir__)
diff --git a/activestorage/test/dummy/config/database.yml b/activestorage/test/dummy/config/database.yml
new file mode 100644
index 0000000000..0d02f24980
--- /dev/null
+++ b/activestorage/test/dummy/config/database.yml
@@ -0,0 +1,25 @@
+# SQLite version 3.x
+# gem install sqlite3
+#
+# Ensure the SQLite 3 gem is defined in your Gemfile
+# gem 'sqlite3'
+#
+default: &default
+ adapter: sqlite3
+ pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
+ timeout: 5000
+
+development:
+ <<: *default
+ database: db/development.sqlite3
+
+# Warning: The database defined as "test" will be erased and
+# re-generated from your development database when you run "rake".
+# Do not set this db to the same as development or production.
+test:
+ <<: *default
+ database: db/test.sqlite3
+
+production:
+ <<: *default
+ database: db/production.sqlite3
diff --git a/activestorage/test/dummy/config/environment.rb b/activestorage/test/dummy/config/environment.rb
new file mode 100644
index 0000000000..7df99e89c6
--- /dev/null
+++ b/activestorage/test/dummy/config/environment.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+# Load the Rails application.
+require_relative "application"
+
+# Initialize the Rails application.
+Rails.application.initialize!
diff --git a/activestorage/test/dummy/config/environments/development.rb b/activestorage/test/dummy/config/environments/development.rb
new file mode 100644
index 0000000000..47fc5bf25c
--- /dev/null
+++ b/activestorage/test/dummy/config/environments/development.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ # In the development environment your application's code is reloaded on
+ # every request. This slows down response time but is perfect for development
+ # since you don't have to restart the web server when you make code changes.
+ config.cache_classes = false
+
+ # Do not eager load code on boot.
+ config.eager_load = false
+
+ # Show full error reports.
+ config.consider_all_requests_local = true
+
+ # Enable/disable caching. By default caching is disabled.
+ if Rails.root.join("tmp/caching-dev.txt").exist?
+ config.action_controller.perform_caching = true
+
+ config.cache_store = :memory_store
+ config.public_file_server.headers = {
+ "Cache-Control" => "public, max-age=#{2.days.seconds.to_i}"
+ }
+ else
+ config.action_controller.perform_caching = false
+
+ config.cache_store = :null_store
+ end
+
+ # Print deprecation notices to the Rails logger.
+ config.active_support.deprecation = :log
+
+ # Raise an error on page load if there are pending migrations.
+ config.active_record.migration_error = :page_load
+
+ # Debug mode disables concatenation and preprocessing of assets.
+ # This option may cause significant delays in view rendering with a large
+ # number of complex assets.
+ config.assets.debug = true
+
+ # Suppress logger output for asset requests.
+ config.assets.quiet = true
+
+ # Raises error for missing translations
+ # config.action_view.raise_on_missing_translations = true
+
+ # Use an evented file watcher to asynchronously detect changes in source code,
+ # routes, locales, etc. This feature depends on the listen gem.
+ # config.file_watcher = ActiveSupport::EventedFileUpdateChecker
+end
diff --git a/activestorage/test/dummy/config/environments/production.rb b/activestorage/test/dummy/config/environments/production.rb
new file mode 100644
index 0000000000..be7f5b80d4
--- /dev/null
+++ b/activestorage/test/dummy/config/environments/production.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ # Code is not reloaded between requests.
+ config.cache_classes = true
+
+ # Eager load code on boot. This eager loads most of Rails and
+ # your application in memory, allowing both threaded web servers
+ # and those relying on copy on write to perform better.
+ # Rake tasks automatically ignore this option for performance.
+ config.eager_load = true
+
+ # Full error reports are disabled and caching is turned on.
+ 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?
+
+ # Compress CSS using a preprocessor.
+ # config.assets.css_compressor = :sass
+
+ # Do not fallback to assets pipeline if a precompiled asset is missed.
+ config.assets.compile = false
+
+ # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
+
+ # Enable serving of images, stylesheets, and JavaScripts from an asset server.
+ # config.action_controller.asset_host = 'http://assets.example.com'
+
+ # Specifies the header that your server uses for sending files.
+ # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
+ # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
+
+ # Mount Action Cable outside main process or domain
+ # config.action_cable.mount_path = nil
+ # config.action_cable.url = 'wss://example.com/cable'
+ # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
+
+ # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
+ # config.force_ssl = true
+
+ # Use the lowest log level to ensure availability of diagnostic information
+ # when problems arise.
+ config.log_level = :debug
+
+ # Prepend all log lines with the following tags.
+ config.log_tags = [ :request_id ]
+
+ # Use a different cache store in production.
+ # config.cache_store = :mem_cache_store
+
+ # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
+ # the I18n.default_locale when a translation cannot be found).
+ config.i18n.fallbacks = true
+
+ # Send deprecation notices to registered listeners.
+ config.active_support.deprecation = :notify
+
+ # Use default logging formatter so that PID and timestamp are not suppressed.
+ config.log_formatter = ::Logger::Formatter.new
+
+ # Use a different logger for distributed setups.
+ # require 'syslog/logger'
+ # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
+
+ if ENV["RAILS_LOG_TO_STDOUT"].present?
+ logger = ActiveSupport::Logger.new(STDOUT)
+ logger.formatter = config.log_formatter
+ config.logger = ActiveSupport::TaggedLogging.new(logger)
+ end
+
+ # Do not dump schema after migrations.
+ config.active_record.dump_schema_after_migration = false
+end
diff --git a/activestorage/test/dummy/config/environments/test.rb b/activestorage/test/dummy/config/environments/test.rb
new file mode 100644
index 0000000000..74a802d98c
--- /dev/null
+++ b/activestorage/test/dummy/config/environments/test.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ # The test environment is used exclusively to run your application's
+ # test suite. You never need to work with it otherwise. Remember that
+ # your test database is "scratch space" for the test suite and is wiped
+ # and recreated between test runs. Don't rely on the data there!
+ config.cache_classes = true
+
+ # Do not eager load code on boot. This avoids loading your whole application
+ # just for the purpose of running a single test. If you are using a tool that
+ # preloads Rails for running tests, you may have to set it to true.
+ config.eager_load = false
+
+ # Configure public file server for tests with Cache-Control for performance.
+ config.public_file_server.enabled = true
+ config.public_file_server.headers = {
+ "Cache-Control" => "public, max-age=#{1.hour.seconds.to_i}"
+ }
+
+ # Show full error reports and disable caching.
+ config.consider_all_requests_local = true
+ config.action_controller.perform_caching = false
+
+ # Raise exceptions instead of rendering exception templates.
+ config.action_dispatch.show_exceptions = false
+
+ # Print deprecation notices to the stderr.
+ config.active_support.deprecation = :stderr
+
+ # Disable request forgery protection in test environment.
+ config.action_controller.allow_forgery_protection = false
+
+ # Raises error for missing translations
+ # config.action_view.raise_on_missing_translations = true
+end
diff --git a/activestorage/test/dummy/config/initializers/application_controller_renderer.rb b/activestorage/test/dummy/config/initializers/application_controller_renderer.rb
new file mode 100644
index 0000000000..315ac48a9a
--- /dev/null
+++ b/activestorage/test/dummy/config/initializers/application_controller_renderer.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+# Be sure to restart your server when you modify this file.
+
+# ApplicationController.renderer.defaults.merge!(
+# http_host: 'example.org',
+# https: false
+# )
diff --git a/activestorage/test/dummy/config/initializers/assets.rb b/activestorage/test/dummy/config/initializers/assets.rb
new file mode 100644
index 0000000000..ba194685a2
--- /dev/null
+++ b/activestorage/test/dummy/config/initializers/assets.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+# Be sure to restart your server when you modify this file.
+
+# 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.
+# Rails.application.config.assets.paths << Emoji.images_path
+# Add Yarn node_modules folder to the asset load path.
+Rails.application.config.assets.paths << Rails.root.join("node_modules")
+
+# Precompile additional assets.
+# application.js, application.css, and all non-JS/CSS in the app/assets
+# folder are already added.
+# Rails.application.config.assets.precompile += %w( admin.js admin.css )
diff --git a/activestorage/test/dummy/config/initializers/backtrace_silencers.rb b/activestorage/test/dummy/config/initializers/backtrace_silencers.rb
new file mode 100644
index 0000000000..d0f0d3b5df
--- /dev/null
+++ b/activestorage/test/dummy/config/initializers/backtrace_silencers.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+# Be sure to restart your server when you modify this file.
+
+# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
+# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
+
+# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
+# Rails.backtrace_cleaner.remove_silencers!
diff --git a/activestorage/test/dummy/config/initializers/cookies_serializer.rb b/activestorage/test/dummy/config/initializers/cookies_serializer.rb
new file mode 100644
index 0000000000..ee8dff9c99
--- /dev/null
+++ b/activestorage/test/dummy/config/initializers/cookies_serializer.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+# Be sure to restart your server when you modify this file.
+
+# Specify a serializer for the signed and encrypted cookie jars.
+# Valid options are :json, :marshal, and :hybrid.
+Rails.application.config.action_dispatch.cookies_serializer = :json
diff --git a/activestorage/test/dummy/config/initializers/filter_parameter_logging.rb b/activestorage/test/dummy/config/initializers/filter_parameter_logging.rb
new file mode 100644
index 0000000000..7a4f47b4c2
--- /dev/null
+++ b/activestorage/test/dummy/config/initializers/filter_parameter_logging.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+# Be sure to restart your server when you modify this file.
+
+# Configure sensitive parameters which will be filtered from the log file.
+Rails.application.config.filter_parameters += [:password]
diff --git a/activestorage/test/dummy/config/initializers/inflections.rb b/activestorage/test/dummy/config/initializers/inflections.rb
new file mode 100644
index 0000000000..aa7435fbc9
--- /dev/null
+++ b/activestorage/test/dummy/config/initializers/inflections.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+# Be sure to restart your server when you modify this file.
+
+# Add new inflection rules using the following format. Inflections
+# are locale specific, and you may define rules for as many different
+# locales as you wish. All of these examples are active by default:
+# ActiveSupport::Inflector.inflections(:en) do |inflect|
+# inflect.plural /^(ox)$/i, '\1en'
+# inflect.singular /^(ox)en/i, '\1'
+# inflect.irregular 'person', 'people'
+# inflect.uncountable %w( fish sheep )
+# end
+
+# These inflection rules are supported but not enabled by default:
+# ActiveSupport::Inflector.inflections(:en) do |inflect|
+# inflect.acronym 'RESTful'
+# end
diff --git a/activestorage/test/dummy/config/initializers/mime_types.rb b/activestorage/test/dummy/config/initializers/mime_types.rb
new file mode 100644
index 0000000000..6e1d16f027
--- /dev/null
+++ b/activestorage/test/dummy/config/initializers/mime_types.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+# Be sure to restart your server when you modify this file.
+
+# Add new mime types for use in respond_to blocks:
+# Mime::Type.register "text/richtext", :rtf
diff --git a/activestorage/test/dummy/config/initializers/wrap_parameters.rb b/activestorage/test/dummy/config/initializers/wrap_parameters.rb
new file mode 100644
index 0000000000..2f3c0db471
--- /dev/null
+++ b/activestorage/test/dummy/config/initializers/wrap_parameters.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+# Be sure to restart your server when you modify this file.
+
+# This file contains settings for ActionController::ParamsWrapper which
+# is enabled by default.
+
+# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
+ActiveSupport.on_load(:action_controller) do
+ wrap_parameters format: [:json]
+end
+
+# To enable root element in JSON for ActiveRecord objects.
+# ActiveSupport.on_load(:active_record) do
+# self.include_root_in_json = true
+# end
diff --git a/activestorage/test/dummy/config/routes.rb b/activestorage/test/dummy/config/routes.rb
new file mode 100644
index 0000000000..edf04d2d63
--- /dev/null
+++ b/activestorage/test/dummy/config/routes.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+Rails.application.routes.draw do
+end
diff --git a/activestorage/test/dummy/config/secrets.yml b/activestorage/test/dummy/config/secrets.yml
new file mode 100644
index 0000000000..18ada4405e
--- /dev/null
+++ b/activestorage/test/dummy/config/secrets.yml
@@ -0,0 +1,32 @@
+# Be sure to restart your server when you modify this file.
+
+# Your secret key is used for verifying the integrity of signed cookies.
+# If you change this key, all old signed cookies will become invalid!
+
+# Make sure the secret is at least 30 characters and all random,
+# no regular words or you'll be exposed to dictionary attacks.
+# You can use `rails secret` to generate a secure secret key.
+
+# Make sure the secrets in this file are kept private
+# if you're sharing your code publicly.
+
+# Shared secrets are available across all environments.
+
+# shared:
+# api_key: a1B2c3D4e5F6
+
+# Environmental secrets are only available for that specific environment.
+
+development:
+ secret_key_base: e0ef5744b10d988669be6b2660c259749779964f3dcb487fd6199743b3558e2d89f7681d6a15d16d144e28979cbdae41885f4fb4c2cf56ff92ac22df282ffb66
+
+test:
+ secret_key_base: 6fb1f3a828a8dcd6ac8dc07b43be4a5265ad64379120d417252a1578fe1f790e7b85ade4f95994de1ac8fb78581690de6e3a6ac4af36a0f0139667418c750d05
+
+# Do not keep production secrets in the unencrypted secrets file.
+# Instead, either read values from the environment.
+# Or, use `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/activestorage/test/dummy/config/spring.rb b/activestorage/test/dummy/config/spring.rb
new file mode 100644
index 0000000000..ff5ba06b6d
--- /dev/null
+++ b/activestorage/test/dummy/config/spring.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+%w(
+ .ruby-version
+ .rbenv-vars
+ tmp/restart.txt
+ tmp/caching-dev.txt
+).each { |path| Spring.watch(path) }
diff --git a/activestorage/test/dummy/config/storage.yml b/activestorage/test/dummy/config/storage.yml
new file mode 100644
index 0000000000..2c6762e0d6
--- /dev/null
+++ b/activestorage/test/dummy/config/storage.yml
@@ -0,0 +1,3 @@
+local:
+ service: Disk
+ root: <%= Rails.root.join("storage") %>
diff --git a/activestorage/test/dummy/config/webpacker.yml b/activestorage/test/dummy/config/webpacker.yml
new file mode 100644
index 0000000000..c1515a2e95
--- /dev/null
+++ b/activestorage/test/dummy/config/webpacker.yml
@@ -0,0 +1,72 @@
+# Note: You must restart bin/webpack-dev-server for changes to take effect
+
+default: &default
+ source_path: app/javascript
+ source_entry_path: packs
+ public_output_path: packs
+ cache_path: tmp/cache/webpacker
+ check_yarn_integrity: false
+
+ # Additional paths webpack should lookup modules
+ # ['app/assets', 'engine/foo/app/assets']
+ resolved_paths: []
+
+ # Reload manifest.json on all requests so we reload latest compiled packs
+ cache_manifest: false
+
+ extensions:
+ - .js
+ - .sass
+ - .scss
+ - .css
+ - .module.sass
+ - .module.scss
+ - .module.css
+ - .png
+ - .svg
+ - .gif
+ - .jpeg
+ - .jpg
+
+development:
+ <<: *default
+ compile: true
+
+ # Verifies that versions and hashed value of the package contents in the project's package.json
+ check_yarn_integrity: true
+
+ # Reference: https://webpack.js.org/configuration/dev-server/
+ dev_server:
+ https: false
+ host: localhost
+ port: 3035
+ public: localhost:3035
+ hmr: false
+ # Inline should be set to true if using HMR
+ inline: true
+ overlay: true
+ compress: true
+ disable_host_check: true
+ use_local_ip: false
+ quiet: false
+ headers:
+ 'Access-Control-Allow-Origin': '*'
+ watch_options:
+ ignored: /node_modules/
+
+
+test:
+ <<: *default
+ compile: true
+
+ # Compile test packs to a separate directory
+ public_output_path: packs-test
+
+production:
+ <<: *default
+
+ # Production depends on precompilation of packs prior to booting for performance.
+ compile: false
+
+ # Cache manifest.json for performance
+ cache_manifest: true
diff --git a/activestorage/test/dummy/lib/assets/.keep b/activestorage/test/dummy/lib/assets/.keep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/activestorage/test/dummy/lib/assets/.keep
diff --git a/activestorage/test/dummy/log/.keep b/activestorage/test/dummy/log/.keep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/activestorage/test/dummy/log/.keep
diff --git a/activestorage/test/dummy/package.json b/activestorage/test/dummy/package.json
new file mode 100644
index 0000000000..caa2d7bb3f
--- /dev/null
+++ b/activestorage/test/dummy/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "dummy",
+ "private": true,
+ "dependencies": {}
+}
diff --git a/activestorage/test/dummy/public/404.html b/activestorage/test/dummy/public/404.html
new file mode 100644
index 0000000000..2be3af26fc
--- /dev/null
+++ b/activestorage/test/dummy/public/404.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>The page you were looking for doesn't exist (404)</title>
+ <meta name="viewport" content="width=device-width,initial-scale=1">
+ <style>
+ .rails-default-error-page {
+ background-color: #EFEFEF;
+ color: #2E2F30;
+ text-align: center;
+ font-family: arial, sans-serif;
+ margin: 0;
+ }
+
+ .rails-default-error-page div.dialog {
+ width: 95%;
+ max-width: 33em;
+ margin: 4em auto 0;
+ }
+
+ .rails-default-error-page div.dialog > div {
+ border: 1px solid #CCC;
+ border-right-color: #999;
+ border-left-color: #999;
+ border-bottom-color: #BBB;
+ border-top: #B00100 solid 4px;
+ border-top-left-radius: 9px;
+ border-top-right-radius: 9px;
+ background-color: white;
+ padding: 7px 12% 0;
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
+ }
+
+ .rails-default-error-page h1 {
+ font-size: 100%;
+ color: #730E15;
+ line-height: 1.5em;
+ }
+
+ .rails-default-error-page div.dialog > p {
+ margin: 0 0 1em;
+ padding: 1em;
+ background-color: #F7F7F7;
+ border: 1px solid #CCC;
+ border-right-color: #999;
+ border-left-color: #999;
+ border-bottom-color: #999;
+ border-bottom-left-radius: 4px;
+ border-bottom-right-radius: 4px;
+ border-top-color: #DADADA;
+ color: #666;
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
+ }
+ </style>
+</head>
+
+<body class="rails-default-error-page">
+ <!-- This file lives in public/404.html -->
+ <div class="dialog">
+ <div>
+ <h1>The page you were looking for doesn't exist.</h1>
+ <p>You may have mistyped the address or the page may have moved.</p>
+ </div>
+ <p>If you are the application owner check the logs for more information.</p>
+ </div>
+</body>
+</html>
diff --git a/activestorage/test/dummy/public/422.html b/activestorage/test/dummy/public/422.html
new file mode 100644
index 0000000000..c08eac0d1d
--- /dev/null
+++ b/activestorage/test/dummy/public/422.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>The change you wanted was rejected (422)</title>
+ <meta name="viewport" content="width=device-width,initial-scale=1">
+ <style>
+ .rails-default-error-page {
+ background-color: #EFEFEF;
+ color: #2E2F30;
+ text-align: center;
+ font-family: arial, sans-serif;
+ margin: 0;
+ }
+
+ .rails-default-error-page div.dialog {
+ width: 95%;
+ max-width: 33em;
+ margin: 4em auto 0;
+ }
+
+ .rails-default-error-page div.dialog > div {
+ border: 1px solid #CCC;
+ border-right-color: #999;
+ border-left-color: #999;
+ border-bottom-color: #BBB;
+ border-top: #B00100 solid 4px;
+ border-top-left-radius: 9px;
+ border-top-right-radius: 9px;
+ background-color: white;
+ padding: 7px 12% 0;
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
+ }
+
+ .rails-default-error-page h1 {
+ font-size: 100%;
+ color: #730E15;
+ line-height: 1.5em;
+ }
+
+ .rails-default-error-page div.dialog > p {
+ margin: 0 0 1em;
+ padding: 1em;
+ background-color: #F7F7F7;
+ border: 1px solid #CCC;
+ border-right-color: #999;
+ border-left-color: #999;
+ border-bottom-color: #999;
+ border-bottom-left-radius: 4px;
+ border-bottom-right-radius: 4px;
+ border-top-color: #DADADA;
+ color: #666;
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
+ }
+ </style>
+</head>
+
+<body class="rails-default-error-page">
+ <!-- This file lives in public/422.html -->
+ <div class="dialog">
+ <div>
+ <h1>The change you wanted was rejected.</h1>
+ <p>Maybe you tried to change something you didn't have access to.</p>
+ </div>
+ <p>If you are the application owner check the logs for more information.</p>
+ </div>
+</body>
+</html>
diff --git a/activestorage/test/dummy/public/500.html b/activestorage/test/dummy/public/500.html
new file mode 100644
index 0000000000..78a030af22
--- /dev/null
+++ b/activestorage/test/dummy/public/500.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>We're sorry, but something went wrong (500)</title>
+ <meta name="viewport" content="width=device-width,initial-scale=1">
+ <style>
+ .rails-default-error-page {
+ background-color: #EFEFEF;
+ color: #2E2F30;
+ text-align: center;
+ font-family: arial, sans-serif;
+ margin: 0;
+ }
+
+ .rails-default-error-page div.dialog {
+ width: 95%;
+ max-width: 33em;
+ margin: 4em auto 0;
+ }
+
+ .rails-default-error-page div.dialog > div {
+ border: 1px solid #CCC;
+ border-right-color: #999;
+ border-left-color: #999;
+ border-bottom-color: #BBB;
+ border-top: #B00100 solid 4px;
+ border-top-left-radius: 9px;
+ border-top-right-radius: 9px;
+ background-color: white;
+ padding: 7px 12% 0;
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
+ }
+
+ .rails-default-error-page h1 {
+ font-size: 100%;
+ color: #730E15;
+ line-height: 1.5em;
+ }
+
+ .rails-default-error-page div.dialog > p {
+ margin: 0 0 1em;
+ padding: 1em;
+ background-color: #F7F7F7;
+ border: 1px solid #CCC;
+ border-right-color: #999;
+ border-left-color: #999;
+ border-bottom-color: #999;
+ border-bottom-left-radius: 4px;
+ border-bottom-right-radius: 4px;
+ border-top-color: #DADADA;
+ color: #666;
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
+ }
+ </style>
+</head>
+
+<body class="rails-default-error-page">
+ <!-- This file lives in public/500.html -->
+ <div class="dialog">
+ <div>
+ <h1>We're sorry, but something went wrong.</h1>
+ </div>
+ <p>If you are the application owner check the logs for more information.</p>
+ </div>
+</body>
+</html>
diff --git a/activestorage/test/dummy/public/apple-touch-icon-precomposed.png b/activestorage/test/dummy/public/apple-touch-icon-precomposed.png
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/activestorage/test/dummy/public/apple-touch-icon-precomposed.png
diff --git a/activestorage/test/dummy/public/apple-touch-icon.png b/activestorage/test/dummy/public/apple-touch-icon.png
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/activestorage/test/dummy/public/apple-touch-icon.png
diff --git a/activestorage/test/dummy/public/favicon.ico b/activestorage/test/dummy/public/favicon.ico
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/activestorage/test/dummy/public/favicon.ico
diff --git a/activestorage/test/fixtures/files/empty_file.txt b/activestorage/test/fixtures/files/empty_file.txt
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/activestorage/test/fixtures/files/empty_file.txt
diff --git a/activestorage/test/fixtures/files/favicon.ico b/activestorage/test/fixtures/files/favicon.ico
new file mode 100644
index 0000000000..87192a8a07
--- /dev/null
+++ b/activestorage/test/fixtures/files/favicon.ico
Binary files differ
diff --git a/activestorage/test/fixtures/files/icon.psd b/activestorage/test/fixtures/files/icon.psd
new file mode 100644
index 0000000000..631fceeaab
--- /dev/null
+++ b/activestorage/test/fixtures/files/icon.psd
Binary files differ
diff --git a/activestorage/test/fixtures/files/icon.svg b/activestorage/test/fixtures/files/icon.svg
new file mode 100644
index 0000000000..6cfb0e241e
--- /dev/null
+++ b/activestorage/test/fixtures/files/icon.svg
@@ -0,0 +1,13 @@
+<!-- The XML declaration is intentionally omitted. -->
+<svg width="792px" height="584px" viewBox="0 0 792 584" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path d="M9.51802657,28.724593 C9.51802657,18.2155955 18.1343454,9.60822622 28.6542694,9.60822622 L763.245541,9.60822622 C773.765465,9.60822622 782.381784,18.2155955 782.381784,28.724593 L782.381784,584 L792,584 L792,28.724593 C792,12.911054 779.075522,0 763.245541,0 L28.7544592,0 C12.9244782,0 0,12.911054 0,28.724593 L0,584 L9.61821632,584 C9.51802657,584 9.51802657,28.724593 9.51802657,28.724593 L9.51802657,28.724593 Z" id="Shape" opacity="0.3" fill="#CCCCCC"></path>
+ <circle id="Oval" fill="#FFCC33" cx="119.1" cy="147.2" r="33"></circle>
+ <circle id="Oval" fill="#3399FF" cx="119.1" cy="281.1" r="33"></circle>
+ <circle id="Oval" fill="#FF3333" cx="676.1" cy="376.8" r="33"></circle>
+ <circle id="Oval" fill="#FFCC33" cx="119.1" cy="477.2" r="33"></circle>
+ <rect id="Rectangle-path" opacity="0.75" fill="#CCCCCC" x="176.5" y="130.5" width="442.1" height="33.5"></rect>
+ <rect id="Rectangle-path" opacity="0.75" fill="#CCCCCC" x="176.5" y="183.1" width="442.1" height="33.5"></rect>
+ <rect id="Rectangle-path" opacity="0.75" fill="#CCCCCC" x="176.5" y="265.8" width="442.1" height="33.5"></rect>
+ <rect id="Rectangle-path" opacity="0.75" fill="#CCCCCC" x="176.5" y="363.4" width="442.1" height="33.5"></rect>
+ <rect id="Rectangle-path" opacity="0.75" fill="#CCCCCC" x="176.5" y="465.3" width="442.1" height="33.5"></rect>
+</svg>
diff --git a/activestorage/test/fixtures/files/image.gif b/activestorage/test/fixtures/files/image.gif
new file mode 100644
index 0000000000..90c05f671c
--- /dev/null
+++ b/activestorage/test/fixtures/files/image.gif
Binary files differ
diff --git a/activestorage/test/fixtures/files/racecar.jpg b/activestorage/test/fixtures/files/racecar.jpg
new file mode 100644
index 0000000000..934b4caa22
--- /dev/null
+++ b/activestorage/test/fixtures/files/racecar.jpg
Binary files differ
diff --git a/activestorage/test/fixtures/files/racecar_rotated.jpg b/activestorage/test/fixtures/files/racecar_rotated.jpg
new file mode 100644
index 0000000000..89e6d54f98
--- /dev/null
+++ b/activestorage/test/fixtures/files/racecar_rotated.jpg
Binary files differ
diff --git a/activestorage/test/fixtures/files/report.pdf b/activestorage/test/fixtures/files/report.pdf
new file mode 100644
index 0000000000..cccb9b5d64
--- /dev/null
+++ b/activestorage/test/fixtures/files/report.pdf
Binary files differ
diff --git a/activestorage/test/fixtures/files/rotated_video.mp4 b/activestorage/test/fixtures/files/rotated_video.mp4
new file mode 100644
index 0000000000..4c7a4e9e57
--- /dev/null
+++ b/activestorage/test/fixtures/files/rotated_video.mp4
Binary files differ
diff --git a/activestorage/test/fixtures/files/video.mp4 b/activestorage/test/fixtures/files/video.mp4
new file mode 100644
index 0000000000..8fb1c5b24d
--- /dev/null
+++ b/activestorage/test/fixtures/files/video.mp4
Binary files differ
diff --git a/activestorage/test/fixtures/files/video_with_rectangular_samples.mp4 b/activestorage/test/fixtures/files/video_with_rectangular_samples.mp4
new file mode 100644
index 0000000000..12b04afc87
--- /dev/null
+++ b/activestorage/test/fixtures/files/video_with_rectangular_samples.mp4
Binary files differ
diff --git a/activestorage/test/fixtures/files/video_with_undefined_display_aspect_ratio.mp4 b/activestorage/test/fixtures/files/video_with_undefined_display_aspect_ratio.mp4
new file mode 100644
index 0000000000..eb354e756f
--- /dev/null
+++ b/activestorage/test/fixtures/files/video_with_undefined_display_aspect_ratio.mp4
Binary files differ
diff --git a/activestorage/test/fixtures/files/video_without_video_stream.mp4 b/activestorage/test/fixtures/files/video_without_video_stream.mp4
new file mode 100644
index 0000000000..e6a55f868b
--- /dev/null
+++ b/activestorage/test/fixtures/files/video_without_video_stream.mp4
Binary files differ
diff --git a/activestorage/test/jobs/purge_job_test.rb b/activestorage/test/jobs/purge_job_test.rb
new file mode 100644
index 0000000000..251022a96f
--- /dev/null
+++ b/activestorage/test/jobs/purge_job_test.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+class ActiveStorage::PurgeJobTest < ActiveJob::TestCase
+ setup { @blob = create_blob }
+
+ test "purges" do
+ assert_difference -> { ActiveStorage::Blob.count }, -1 do
+ ActiveStorage::PurgeJob.perform_now @blob
+ end
+
+ assert_not ActiveStorage::Blob.exists?(@blob.id)
+ assert_not ActiveStorage::Blob.service.exist?(@blob.key)
+ end
+
+ test "ignores missing blob" do
+ @blob.purge
+
+ perform_enqueued_jobs do
+ assert_nothing_raised do
+ ActiveStorage::PurgeJob.perform_later @blob
+ end
+ end
+ end
+end
diff --git a/activestorage/test/models/attached/many_test.rb b/activestorage/test/models/attached/many_test.rb
new file mode 100644
index 0000000000..3b563b3fc8
--- /dev/null
+++ b/activestorage/test/models/attached/many_test.rb
@@ -0,0 +1,596 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+class ActiveStorage::ManyAttachedTest < ActiveSupport::TestCase
+ include ActiveJob::TestHelper
+
+ setup do
+ @user = User.create!(name: "Josh")
+ end
+
+ teardown { ActiveStorage::Blob.all.each(&:delete) }
+
+ test "attaching existing blobs to an existing record" do
+ @user.highlights.attach create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg")
+ assert_equal "funky.jpg", @user.highlights.first.filename.to_s
+ assert_equal "town.jpg", @user.highlights.second.filename.to_s
+ end
+
+ test "attaching existing blobs from signed IDs to an existing record" do
+ @user.highlights.attach create_blob(filename: "funky.jpg").signed_id, create_blob(filename: "town.jpg").signed_id
+ assert_equal "funky.jpg", @user.highlights.first.filename.to_s
+ assert_equal "town.jpg", @user.highlights.second.filename.to_s
+ end
+
+ test "attaching new blobs from Hashes to an existing record" do
+ @user.highlights.attach(
+ { io: StringIO.new("STUFF"), filename: "funky.jpg", content_type: "image/jpg" },
+ { io: StringIO.new("THINGS"), filename: "town.jpg", content_type: "image/jpeg" })
+
+ assert_equal "funky.jpg", @user.highlights.first.filename.to_s
+ assert_equal "town.jpg", @user.highlights.second.filename.to_s
+ end
+
+ test "attaching new blobs from uploaded files to an existing record" do
+ @user.highlights.attach fixture_file_upload("racecar.jpg"), fixture_file_upload("video.mp4")
+ assert_equal "racecar.jpg", @user.highlights.first.filename.to_s
+ assert_equal "video.mp4", @user.highlights.second.filename.to_s
+ end
+
+ test "attaching existing blobs to an existing, changed record" do
+ @user.name = "Tina"
+ assert @user.changed?
+
+ @user.highlights.attach create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg")
+ assert_equal "funky.jpg", @user.highlights.first.filename.to_s
+ assert_equal "town.jpg", @user.highlights.second.filename.to_s
+ assert_not @user.highlights.first.persisted?
+ assert_not @user.highlights.second.persisted?
+ assert @user.will_save_change_to_name?
+
+ @user.save!
+ assert_equal "funky.jpg", @user.highlights.reload.first.filename.to_s
+ assert_equal "town.jpg", @user.highlights.second.filename.to_s
+ end
+
+ test "attaching existing blobs from signed IDs to an existing, changed record" do
+ @user.name = "Tina"
+ assert @user.changed?
+
+ @user.highlights.attach create_blob(filename: "funky.jpg").signed_id, create_blob(filename: "town.jpg").signed_id
+ assert_equal "funky.jpg", @user.highlights.first.filename.to_s
+ assert_equal "town.jpg", @user.highlights.second.filename.to_s
+ assert_not @user.highlights.first.persisted?
+ assert_not @user.highlights.second.persisted?
+ assert @user.will_save_change_to_name?
+
+ @user.save!
+ assert_equal "funky.jpg", @user.highlights.reload.first.filename.to_s
+ assert_equal "town.jpg", @user.highlights.second.filename.to_s
+ end
+
+ test "attaching new blobs from Hashes to an existing, changed record" do
+ @user.name = "Tina"
+ assert @user.changed?
+
+ @user.highlights.attach(
+ { io: StringIO.new("STUFF"), filename: "funky.jpg", content_type: "image/jpg" },
+ { io: StringIO.new("THINGS"), filename: "town.jpg", content_type: "image/jpeg" })
+
+ assert_equal "funky.jpg", @user.highlights.first.filename.to_s
+ assert_equal "town.jpg", @user.highlights.second.filename.to_s
+ assert_not @user.highlights.first.persisted?
+ assert_not @user.highlights.second.persisted?
+ assert @user.will_save_change_to_name?
+
+ @user.save!
+ assert_equal "funky.jpg", @user.highlights.reload.first.filename.to_s
+ assert_equal "town.jpg", @user.highlights.second.filename.to_s
+ end
+
+ test "attaching new blobs from uploaded files to an existing, changed record" do
+ @user.name = "Tina"
+ assert @user.changed?
+
+ @user.highlights.attach fixture_file_upload("racecar.jpg"), fixture_file_upload("video.mp4")
+ assert_equal "racecar.jpg", @user.highlights.first.filename.to_s
+ assert_equal "video.mp4", @user.highlights.second.filename.to_s
+ assert_not @user.highlights.first.persisted?
+ assert_not @user.highlights.second.persisted?
+ assert @user.will_save_change_to_name?
+
+ @user.save!
+ assert_equal "racecar.jpg", @user.highlights.reload.first.filename.to_s
+ assert_equal "video.mp4", @user.highlights.second.filename.to_s
+ end
+
+ test "attaching existing blobs to an existing record one at a time" do
+ @user.highlights.attach create_blob(filename: "funky.jpg")
+ @user.highlights.attach create_blob(filename: "town.jpg")
+ assert_equal "funky.jpg", @user.highlights.first.filename.to_s
+ assert_equal "town.jpg", @user.highlights.second.filename.to_s
+
+ @user.reload
+ assert_equal "funky.jpg", @user.highlights.first.filename.to_s
+ assert_equal "town.jpg", @user.highlights.second.filename.to_s
+ end
+
+ test "updating an existing record to attach existing blobs" do
+ @user.update! highlights: [ create_file_blob(filename: "racecar.jpg"), create_file_blob(filename: "video.mp4") ]
+ assert_equal "racecar.jpg", @user.highlights.first.filename.to_s
+ assert_equal "video.mp4", @user.highlights.second.filename.to_s
+ end
+
+ test "updating an existing record to attach existing blobs from signed IDs" do
+ @user.update! highlights: [ create_blob(filename: "funky.jpg").signed_id, create_blob(filename: "town.jpg").signed_id ]
+ assert_equal "funky.jpg", @user.highlights.first.filename.to_s
+ assert_equal "town.jpg", @user.highlights.second.filename.to_s
+ end
+
+ test "successfully updating an existing record to attach new blobs from uploaded files" do
+ @user.highlights = [ fixture_file_upload("racecar.jpg"), fixture_file_upload("video.mp4") ]
+ assert_equal "racecar.jpg", @user.highlights.first.filename.to_s
+ assert_equal "video.mp4", @user.highlights.second.filename.to_s
+ assert_not ActiveStorage::Blob.service.exist?(@user.highlights.first.key)
+ assert_not ActiveStorage::Blob.service.exist?(@user.highlights.second.key)
+
+ @user.save!
+ assert ActiveStorage::Blob.service.exist?(@user.highlights.first.key)
+ assert ActiveStorage::Blob.service.exist?(@user.highlights.second.key)
+ end
+
+ test "unsuccessfully updating an existing record to attach new blobs from uploaded files" do
+ assert_not @user.update(name: "", highlights: [ fixture_file_upload("racecar.jpg"), fixture_file_upload("video.mp4") ])
+ assert_equal "racecar.jpg", @user.highlights.first.filename.to_s
+ assert_equal "video.mp4", @user.highlights.second.filename.to_s
+ assert_not ActiveStorage::Blob.service.exist?(@user.highlights.first.key)
+ assert_not ActiveStorage::Blob.service.exist?(@user.highlights.second.key)
+ end
+
+ test "replacing existing, dependent attachments on an existing record via assign and attach" do
+ [ create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg") ].tap do |old_blobs|
+ @user.highlights.attach old_blobs
+
+ @user.highlights = []
+ assert_not @user.highlights.attached?
+
+ perform_enqueued_jobs do
+ @user.highlights.attach create_blob(filename: "whenever.jpg"), create_blob(filename: "wherever.jpg")
+ end
+
+ assert_equal "whenever.jpg", @user.highlights.first.filename.to_s
+ assert_equal "wherever.jpg", @user.highlights.second.filename.to_s
+ assert_not ActiveStorage::Blob.exists?(old_blobs.first.id)
+ assert_not ActiveStorage::Blob.exists?(old_blobs.second.id)
+ assert_not ActiveStorage::Blob.service.exist?(old_blobs.first.key)
+ assert_not ActiveStorage::Blob.service.exist?(old_blobs.second.key)
+ end
+ end
+
+ test "replacing existing, independent attachments on an existing record via assign and attach" do
+ @user.vlogs.attach create_blob(filename: "funky.mp4"), create_blob(filename: "town.mp4")
+
+ @user.vlogs = []
+ assert_not @user.vlogs.attached?
+
+ assert_no_enqueued_jobs only: ActiveStorage::PurgeJob do
+ @user.vlogs.attach create_blob(filename: "whenever.mp4"), create_blob(filename: "wherever.mp4")
+ end
+
+ assert_equal "whenever.mp4", @user.vlogs.first.filename.to_s
+ assert_equal "wherever.mp4", @user.vlogs.second.filename.to_s
+ end
+
+ test "successfully updating an existing record to replace existing, dependent attachments" do
+ [ create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg") ].tap do |old_blobs|
+ @user.highlights.attach old_blobs
+
+ perform_enqueued_jobs do
+ @user.update! highlights: [ create_blob(filename: "whenever.jpg"), create_blob(filename: "wherever.jpg") ]
+ end
+
+ assert_equal "whenever.jpg", @user.highlights.first.filename.to_s
+ assert_equal "wherever.jpg", @user.highlights.second.filename.to_s
+ assert_not ActiveStorage::Blob.exists?(old_blobs.first.id)
+ assert_not ActiveStorage::Blob.exists?(old_blobs.second.id)
+ assert_not ActiveStorage::Blob.service.exist?(old_blobs.first.key)
+ assert_not ActiveStorage::Blob.service.exist?(old_blobs.second.key)
+ end
+ end
+
+ test "successfully updating an existing record to replace existing, independent attachments" do
+ @user.vlogs.attach create_blob(filename: "funky.mp4"), create_blob(filename: "town.mp4")
+
+ assert_no_enqueued_jobs only: ActiveStorage::PurgeJob do
+ @user.update! vlogs: [ create_blob(filename: "whenever.mp4"), create_blob(filename: "wherever.mp4") ]
+ end
+
+ assert_equal "whenever.mp4", @user.vlogs.first.filename.to_s
+ assert_equal "wherever.mp4", @user.vlogs.second.filename.to_s
+ end
+
+ test "unsuccessfully updating an existing record to replace existing attachments" do
+ @user.highlights.attach create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg")
+
+ assert_no_enqueued_jobs do
+ assert_not @user.update(name: "", highlights: [ fixture_file_upload("racecar.jpg"), fixture_file_upload("video.mp4") ])
+ end
+
+ assert_equal "racecar.jpg", @user.highlights.first.filename.to_s
+ assert_equal "video.mp4", @user.highlights.second.filename.to_s
+ assert_not ActiveStorage::Blob.service.exist?(@user.highlights.first.key)
+ assert_not ActiveStorage::Blob.service.exist?(@user.highlights.second.key)
+ end
+
+ test "updating an existing record to attach one new blob and one previously-attached blob" do
+ [ create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg") ].tap do |blobs|
+ @user.highlights.attach blobs.first
+
+ perform_enqueued_jobs do
+ assert_no_changes -> { @user.highlights_attachments.first.id } do
+ @user.update! highlights: blobs
+ end
+ end
+
+ assert_equal "funky.jpg", @user.highlights.first.filename.to_s
+ assert_equal "town.jpg", @user.highlights.second.filename.to_s
+ assert ActiveStorage::Blob.service.exist?(@user.highlights.first.key)
+ end
+ end
+
+ test "updating an existing record to remove dependent attachments" do
+ [ create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg") ].tap do |blobs|
+ @user.highlights.attach blobs
+
+ assert_enqueued_with job: ActiveStorage::PurgeJob, args: [ blobs.first ] do
+ assert_enqueued_with job: ActiveStorage::PurgeJob, args: [ blobs.second ] do
+ @user.update! highlights: []
+ end
+ end
+
+ assert_not @user.highlights.attached?
+ end
+ end
+
+ test "updating an existing record to remove independent attachments" do
+ [ create_blob(filename: "funky.mp4"), create_blob(filename: "town.mp4") ].tap do |blobs|
+ @user.vlogs.attach blobs
+
+ assert_no_enqueued_jobs only: ActiveStorage::PurgeJob do
+ @user.update! vlogs: []
+ end
+
+ assert_not @user.vlogs.attached?
+ end
+ end
+
+ test "analyzing a new blob from an uploaded file after attaching it to an existing record" do
+ perform_enqueued_jobs do
+ @user.highlights.attach fixture_file_upload("racecar.jpg")
+ end
+
+ assert @user.highlights.reload.first.analyzed?
+ assert_equal 4104, @user.highlights.first.metadata[:width]
+ assert_equal 2736, @user.highlights.first.metadata[:height]
+ end
+
+ test "analyzing a new blob from an uploaded file after attaching it to an existing record via update" do
+ perform_enqueued_jobs do
+ @user.update! highlights: [ fixture_file_upload("racecar.jpg") ]
+ end
+
+ assert @user.highlights.reload.first.analyzed?
+ assert_equal 4104, @user.highlights.first.metadata[:width]
+ assert_equal 2736, @user.highlights.first.metadata[:height]
+ end
+
+ test "analyzing a directly-uploaded blob after attaching it to an existing record" do
+ perform_enqueued_jobs do
+ @user.highlights.attach directly_upload_file_blob(filename: "racecar.jpg")
+ end
+
+ assert @user.highlights.reload.first.analyzed?
+ assert_equal 4104, @user.highlights.first.metadata[:width]
+ assert_equal 2736, @user.highlights.first.metadata[:height]
+ end
+
+ test "analyzing a directly-uploaded blob after attaching it to an existing record via update" do
+ perform_enqueued_jobs do
+ @user.update! highlights: [ directly_upload_file_blob(filename: "racecar.jpg") ]
+ end
+
+ assert @user.highlights.reload.first.analyzed?
+ assert_equal 4104, @user.highlights.first.metadata[:width]
+ assert_equal 2736, @user.highlights.first.metadata[:height]
+ end
+
+ test "attaching existing blobs to a new record" do
+ User.new(name: "Jason").tap do |user|
+ user.highlights.attach create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg")
+ assert user.new_record?
+ assert_equal "funky.jpg", user.highlights.first.filename.to_s
+ assert_equal "town.jpg", user.highlights.second.filename.to_s
+
+ user.save!
+ assert_equal "funky.jpg", user.highlights.first.filename.to_s
+ assert_equal "town.jpg", user.highlights.second.filename.to_s
+ end
+ end
+
+ test "attaching an existing blob from a signed ID to a new record" do
+ User.new(name: "Jason").tap do |user|
+ user.avatar.attach create_blob(filename: "funky.jpg").signed_id
+ assert user.new_record?
+ assert_equal "funky.jpg", user.avatar.filename.to_s
+
+ user.save!
+ assert_equal "funky.jpg", user.reload.avatar.filename.to_s
+ end
+ end
+
+ test "attaching new blobs from Hashes to a new record" do
+ User.new(name: "Jason").tap do |user|
+ user.highlights.attach(
+ { io: StringIO.new("STUFF"), filename: "funky.jpg", content_type: "image/jpg" },
+ { io: StringIO.new("THINGS"), filename: "town.jpg", content_type: "image/jpg" })
+
+ assert user.new_record?
+ assert user.highlights.first.new_record?
+ assert user.highlights.second.new_record?
+ assert user.highlights.first.blob.new_record?
+ assert user.highlights.second.blob.new_record?
+ assert_equal "funky.jpg", user.highlights.first.filename.to_s
+ assert_equal "town.jpg", user.highlights.second.filename.to_s
+ assert_not ActiveStorage::Blob.service.exist?(user.highlights.first.key)
+ assert_not ActiveStorage::Blob.service.exist?(user.highlights.second.key)
+
+ user.save!
+ assert user.highlights.first.persisted?
+ assert user.highlights.second.persisted?
+ assert user.highlights.first.blob.persisted?
+ assert user.highlights.second.blob.persisted?
+ assert_equal "funky.jpg", user.reload.highlights.first.filename.to_s
+ assert_equal "town.jpg", user.highlights.second.filename.to_s
+ assert ActiveStorage::Blob.service.exist?(user.highlights.first.key)
+ assert ActiveStorage::Blob.service.exist?(user.highlights.second.key)
+ end
+ end
+
+ test "attaching new blobs from uploaded files to a new record" do
+ User.new(name: "Jason").tap do |user|
+ user.highlights.attach fixture_file_upload("racecar.jpg"), fixture_file_upload("video.mp4")
+ assert user.new_record?
+ assert user.highlights.first.new_record?
+ assert user.highlights.second.new_record?
+ assert user.highlights.first.blob.new_record?
+ assert user.highlights.second.blob.new_record?
+ assert_equal "racecar.jpg", user.highlights.first.filename.to_s
+ assert_equal "video.mp4", user.highlights.second.filename.to_s
+ assert_not ActiveStorage::Blob.service.exist?(user.highlights.first.key)
+ assert_not ActiveStorage::Blob.service.exist?(user.highlights.second.key)
+
+ user.save!
+ assert user.highlights.first.persisted?
+ assert user.highlights.second.persisted?
+ assert user.highlights.first.blob.persisted?
+ assert user.highlights.second.blob.persisted?
+ assert_equal "racecar.jpg", user.reload.highlights.first.filename.to_s
+ assert_equal "video.mp4", user.highlights.second.filename.to_s
+ assert ActiveStorage::Blob.service.exist?(user.highlights.first.key)
+ assert ActiveStorage::Blob.service.exist?(user.highlights.second.key)
+ end
+ end
+
+ test "creating a record with existing blobs attached" do
+ user = User.create!(name: "Jason", highlights: [ create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg") ])
+ assert_equal "funky.jpg", user.reload.highlights.first.filename.to_s
+ assert_equal "town.jpg", user.reload.highlights.second.filename.to_s
+ end
+
+ test "creating a record with an existing blob from signed IDs attached" do
+ user = User.create!(name: "Jason", highlights: [
+ create_blob(filename: "funky.jpg").signed_id, create_blob(filename: "town.jpg").signed_id ])
+ assert_equal "funky.jpg", user.reload.highlights.first.filename.to_s
+ assert_equal "town.jpg", user.reload.highlights.second.filename.to_s
+ end
+
+ test "creating a record with new blobs from uploaded files attached" do
+ User.new(name: "Jason", highlights: [ fixture_file_upload("racecar.jpg"), fixture_file_upload("video.mp4") ]).tap do |user|
+ assert user.new_record?
+ assert user.highlights.first.new_record?
+ assert user.highlights.second.new_record?
+ assert user.highlights.first.blob.new_record?
+ assert user.highlights.second.blob.new_record?
+ assert_equal "racecar.jpg", user.highlights.first.filename.to_s
+ assert_equal "video.mp4", user.highlights.second.filename.to_s
+ assert_not ActiveStorage::Blob.service.exist?(user.highlights.first.key)
+ assert_not ActiveStorage::Blob.service.exist?(user.highlights.second.key)
+
+ user.save!
+ assert_equal "racecar.jpg", user.highlights.first.filename.to_s
+ assert_equal "video.mp4", user.highlights.second.filename.to_s
+ end
+ end
+
+ test "creating a record with an unexpected object attached" do
+ error = assert_raises(ArgumentError) { User.create!(name: "Jason", highlights: :foo) }
+ assert_equal "Could not find or build blob: expected attachable, got :foo", error.message
+ end
+
+ test "analyzing a new blob from an uploaded file after attaching it to a new record" do
+ perform_enqueued_jobs do
+ user = User.create!(name: "Jason", highlights: [ fixture_file_upload("racecar.jpg") ])
+ assert user.highlights.reload.first.analyzed?
+ assert_equal 4104, user.highlights.first.metadata[:width]
+ assert_equal 2736, user.highlights.first.metadata[:height]
+ end
+ end
+
+ test "analyzing a directly-uploaded blob after attaching it to a new record" do
+ perform_enqueued_jobs do
+ user = User.create!(name: "Jason", highlights: [ directly_upload_file_blob(filename: "racecar.jpg") ])
+ assert user.highlights.reload.first.analyzed?
+ assert_equal 4104, user.highlights.first.metadata[:width]
+ assert_equal 2736, user.highlights.first.metadata[:height]
+ end
+ end
+
+ test "detaching" do
+ [ create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg") ].tap do |blobs|
+ @user.highlights.attach blobs
+ assert @user.highlights.attached?
+
+ perform_enqueued_jobs do
+ @user.highlights.detach
+ end
+
+ assert_not @user.highlights.attached?
+ assert ActiveStorage::Blob.exists?(blobs.first.id)
+ assert ActiveStorage::Blob.exists?(blobs.second.id)
+ assert ActiveStorage::Blob.service.exist?(blobs.first.key)
+ assert ActiveStorage::Blob.service.exist?(blobs.second.key)
+ end
+ end
+
+ test "purging" do
+ [ create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg") ].tap do |blobs|
+ @user.highlights.attach blobs
+ assert @user.highlights.attached?
+
+ @user.highlights.purge
+ assert_not @user.highlights.attached?
+ assert_not ActiveStorage::Blob.exists?(blobs.first.id)
+ assert_not ActiveStorage::Blob.exists?(blobs.second.id)
+ assert_not ActiveStorage::Blob.service.exist?(blobs.first.key)
+ assert_not ActiveStorage::Blob.service.exist?(blobs.second.key)
+ end
+ end
+
+ test "purging attachment with shared blobs" do
+ [
+ create_blob(filename: "funky.jpg"),
+ create_blob(filename: "town.jpg"),
+ create_blob(filename: "worm.jpg")
+ ].tap do |blobs|
+ @user.highlights.attach blobs
+ assert @user.highlights.attached?
+
+ another_user = User.create!(name: "John")
+ shared_blobs = [blobs.second, blobs.third]
+ another_user.highlights.attach shared_blobs
+ assert another_user.highlights.attached?
+
+ @user.highlights.purge
+ assert_not @user.highlights.attached?
+
+ assert_not ActiveStorage::Blob.exists?(blobs.first.id)
+ assert ActiveStorage::Blob.exists?(blobs.second.id)
+ assert ActiveStorage::Blob.exists?(blobs.third.id)
+
+ assert_not ActiveStorage::Blob.service.exist?(blobs.first.key)
+ assert ActiveStorage::Blob.service.exist?(blobs.second.key)
+ assert ActiveStorage::Blob.service.exist?(blobs.third.key)
+ end
+ end
+
+ test "purging later" do
+ [ create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg") ].tap do |blobs|
+ @user.highlights.attach blobs
+ assert @user.highlights.attached?
+
+ perform_enqueued_jobs do
+ @user.highlights.purge_later
+ end
+
+ assert_not @user.highlights.attached?
+ assert_not ActiveStorage::Blob.exists?(blobs.first.id)
+ assert_not ActiveStorage::Blob.exists?(blobs.second.id)
+ assert_not ActiveStorage::Blob.service.exist?(blobs.first.key)
+ assert_not ActiveStorage::Blob.service.exist?(blobs.second.key)
+ end
+ end
+
+ test "purging attachment later with shared blobs" do
+ [
+ create_blob(filename: "funky.jpg"),
+ create_blob(filename: "town.jpg"),
+ create_blob(filename: "worm.jpg")
+ ].tap do |blobs|
+ @user.highlights.attach blobs
+ assert @user.highlights.attached?
+
+ another_user = User.create!(name: "John")
+ shared_blobs = [blobs.second, blobs.third]
+ another_user.highlights.attach shared_blobs
+ assert another_user.highlights.attached?
+
+ perform_enqueued_jobs do
+ @user.highlights.purge_later
+ end
+
+ assert_not @user.highlights.attached?
+ assert_not ActiveStorage::Blob.exists?(blobs.first.id)
+ assert ActiveStorage::Blob.exists?(blobs.second.id)
+ assert ActiveStorage::Blob.exists?(blobs.third.id)
+
+ assert_not ActiveStorage::Blob.service.exist?(blobs.first.key)
+ assert ActiveStorage::Blob.service.exist?(blobs.second.key)
+ assert ActiveStorage::Blob.service.exist?(blobs.third.key)
+ end
+ end
+
+ test "purging dependent attachment later on destroy" do
+ [ create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg") ].tap do |blobs|
+ @user.highlights.attach blobs
+
+ perform_enqueued_jobs do
+ @user.destroy!
+ end
+
+ assert_not ActiveStorage::Blob.exists?(blobs.first.id)
+ assert_not ActiveStorage::Blob.exists?(blobs.second.id)
+ assert_not ActiveStorage::Blob.service.exist?(blobs.first.key)
+ assert_not ActiveStorage::Blob.service.exist?(blobs.second.key)
+ end
+ end
+
+ test "not purging independent attachment on destroy" do
+ [ create_blob(filename: "funky.mp4"), create_blob(filename: "town.mp4") ].tap do |blobs|
+ @user.vlogs.attach blobs
+
+ assert_no_enqueued_jobs do
+ @user.destroy!
+ end
+ end
+ end
+
+ test "clearing change on reload" do
+ @user.highlights = [ create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg") ]
+ assert @user.highlights.attached?
+
+ @user.reload
+ assert_not @user.highlights.attached?
+ end
+
+ test "overriding attached reader" do
+ @user.highlights.attach create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg")
+
+ assert_equal "funky.jpg", @user.highlights.first.filename.to_s
+ assert_equal "town.jpg", @user.highlights.second.filename.to_s
+
+ begin
+ User.class_eval do
+ def highlights
+ super.reverse
+ end
+ end
+
+ assert_equal "town.jpg", @user.highlights.first.filename.to_s
+ assert_equal "funky.jpg", @user.highlights.second.filename.to_s
+ ensure
+ User.send(:remove_method, :highlights)
+ end
+ end
+end
diff --git a/activestorage/test/models/attached/one_test.rb b/activestorage/test/models/attached/one_test.rb
new file mode 100644
index 0000000000..561c3e9d23
--- /dev/null
+++ b/activestorage/test/models/attached/one_test.rb
@@ -0,0 +1,513 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+class ActiveStorage::OneAttachedTest < ActiveSupport::TestCase
+ include ActiveJob::TestHelper
+
+ setup do
+ @user = User.create!(name: "Josh")
+ end
+
+ teardown { ActiveStorage::Blob.all.each(&:delete) }
+
+ test "attaching an existing blob to an existing record" do
+ @user.avatar.attach create_blob(filename: "funky.jpg")
+ assert_equal "funky.jpg", @user.avatar.filename.to_s
+ end
+
+ test "attaching an existing blob from a signed ID to an existing record" do
+ @user.avatar.attach create_blob(filename: "funky.jpg").signed_id
+ assert_equal "funky.jpg", @user.avatar.filename.to_s
+ end
+
+ test "attaching a new blob from a Hash to an existing record" do
+ @user.avatar.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg"
+ assert_equal "town.jpg", @user.avatar.filename.to_s
+ end
+
+ test "attaching a new blob from an uploaded file to an existing record" do
+ @user.avatar.attach fixture_file_upload("racecar.jpg")
+ assert_equal "racecar.jpg", @user.avatar.filename.to_s
+ end
+
+ test "attaching an existing blob to an existing, changed record" do
+ @user.name = "Tina"
+ assert @user.changed?
+
+ @user.avatar.attach create_blob(filename: "funky.jpg")
+ assert_equal "funky.jpg", @user.avatar.filename.to_s
+ assert_not @user.avatar.persisted?
+ assert @user.will_save_change_to_name?
+
+ @user.save!
+ assert_equal "funky.jpg", @user.reload.avatar.filename.to_s
+ end
+
+ test "attaching an existing blob from a signed ID to an existing, changed record" do
+ @user.name = "Tina"
+ assert @user.changed?
+
+ @user.avatar.attach create_blob(filename: "funky.jpg").signed_id
+ assert_equal "funky.jpg", @user.avatar.filename.to_s
+ assert_not @user.avatar.persisted?
+ assert @user.will_save_change_to_name?
+
+ @user.save!
+ assert_equal "funky.jpg", @user.reload.avatar.filename.to_s
+ end
+
+ test "attaching a new blob from a Hash to an existing, changed record" do
+ @user.name = "Tina"
+ assert @user.changed?
+
+ @user.avatar.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg"
+ assert_equal "town.jpg", @user.avatar.filename.to_s
+ assert_not @user.avatar.persisted?
+ assert @user.will_save_change_to_name?
+
+ @user.save!
+ assert_equal "town.jpg", @user.reload.avatar.filename.to_s
+ end
+
+ test "attaching a new blob from an uploaded file to an existing, changed record" do
+ @user.name = "Tina"
+ assert @user.changed?
+
+ @user.avatar.attach fixture_file_upload("racecar.jpg")
+ assert_equal "racecar.jpg", @user.avatar.filename.to_s
+ assert_not @user.avatar.persisted?
+ assert @user.will_save_change_to_name?
+
+ @user.save!
+ assert_equal "racecar.jpg", @user.reload.avatar.filename.to_s
+ end
+
+ test "updating an existing record to attach an existing blob" do
+ @user.update! avatar: create_blob(filename: "funky.jpg")
+ assert_equal "funky.jpg", @user.avatar.filename.to_s
+ end
+
+ test "updating an existing record to attach an existing blob from a signed ID" do
+ @user.update! avatar: create_blob(filename: "funky.jpg").signed_id
+ assert_equal "funky.jpg", @user.avatar.filename.to_s
+ end
+
+ test "successfully updating an existing record to attach a new blob from an uploaded file" do
+ @user.avatar = fixture_file_upload("racecar.jpg")
+ assert_equal "racecar.jpg", @user.avatar.filename.to_s
+ assert_not ActiveStorage::Blob.service.exist?(@user.avatar.key)
+
+ @user.save!
+ assert ActiveStorage::Blob.service.exist?(@user.avatar.key)
+ end
+
+ test "unsuccessfully updating an existing record to attach a new blob from an uploaded file" do
+ assert_not @user.update(name: "", avatar: fixture_file_upload("racecar.jpg"))
+ assert_equal "racecar.jpg", @user.avatar.filename.to_s
+ assert_not ActiveStorage::Blob.service.exist?(@user.avatar.key)
+ end
+
+ test "successfully replacing an existing, dependent attachment on an existing record" do
+ create_blob(filename: "funky.jpg").tap do |old_blob|
+ @user.avatar.attach old_blob
+
+ perform_enqueued_jobs do
+ @user.avatar.attach create_blob(filename: "town.jpg")
+ end
+
+ assert_equal "town.jpg", @user.avatar.filename.to_s
+ assert_not ActiveStorage::Blob.exists?(old_blob.id)
+ assert_not ActiveStorage::Blob.service.exist?(old_blob.key)
+ end
+ end
+
+ test "replacing an existing, independent attachment on an existing record" do
+ @user.cover_photo.attach create_blob(filename: "funky.jpg")
+
+ assert_no_enqueued_jobs only: ActiveStorage::PurgeJob do
+ @user.cover_photo.attach create_blob(filename: "town.jpg")
+ end
+
+ assert_equal "town.jpg", @user.cover_photo.filename.to_s
+ end
+
+ test "replacing an attached blob on an existing record with itself" do
+ create_blob(filename: "funky.jpg").tap do |blob|
+ @user.avatar.attach blob
+
+ assert_no_changes -> { @user.reload.avatar_attachment.id } do
+ assert_no_enqueued_jobs do
+ @user.avatar.attach blob
+ end
+ end
+
+ assert_equal "funky.jpg", @user.avatar.filename.to_s
+ assert ActiveStorage::Blob.service.exist?(@user.avatar.key)
+ end
+ end
+
+ test "successfully updating an existing record to replace an existing, dependent attachment" do
+ create_blob(filename: "funky.jpg").tap do |old_blob|
+ @user.avatar.attach old_blob
+
+ perform_enqueued_jobs do
+ @user.update! avatar: create_blob(filename: "town.jpg")
+ end
+
+ assert_equal "town.jpg", @user.avatar.filename.to_s
+ assert_not ActiveStorage::Blob.exists?(old_blob.id)
+ assert_not ActiveStorage::Blob.service.exist?(old_blob.key)
+ end
+ end
+
+ test "successfully updating an existing record to replace an existing, independent attachment" do
+ @user.cover_photo.attach create_blob(filename: "funky.jpg")
+
+ assert_no_enqueued_jobs only: ActiveStorage::PurgeJob do
+ @user.update! cover_photo: create_blob(filename: "town.jpg")
+ end
+
+ assert_equal "town.jpg", @user.cover_photo.filename.to_s
+ end
+
+ test "unsuccessfully updating an existing record to replace an existing attachment" do
+ @user.avatar.attach create_blob(filename: "funky.jpg")
+
+ assert_no_enqueued_jobs do
+ assert_not @user.update(name: "", avatar: fixture_file_upload("racecar.jpg"))
+ end
+
+ assert_equal "racecar.jpg", @user.avatar.filename.to_s
+ assert_not ActiveStorage::Blob.service.exist?(@user.avatar.key)
+ end
+
+ test "updating an existing record to replace an attached blob with itself" do
+ create_blob(filename: "funky.jpg").tap do |blob|
+ @user.avatar.attach blob
+
+ assert_no_enqueued_jobs do
+ assert_no_changes -> { @user.reload.avatar_attachment.id } do
+ @user.update! avatar: blob
+ end
+ end
+ end
+ end
+
+ test "removing a dependent attachment from an existing record" do
+ create_blob(filename: "funky.jpg").tap do |blob|
+ @user.avatar.attach blob
+
+ assert_enqueued_with job: ActiveStorage::PurgeJob, args: [ blob ] do
+ @user.avatar.attach nil
+ end
+
+ assert_not @user.avatar.attached?
+ end
+ end
+
+ test "removing an independent attachment from an existing record" do
+ create_blob(filename: "funky.jpg").tap do |blob|
+ @user.cover_photo.attach blob
+
+ assert_no_enqueued_jobs only: ActiveStorage::PurgeJob do
+ @user.cover_photo.attach nil
+ end
+
+ assert_not @user.cover_photo.attached?
+ end
+ end
+
+ test "updating an existing record to remove a dependent attachment" do
+ create_blob(filename: "funky.jpg").tap do |blob|
+ @user.avatar.attach blob
+
+ assert_enqueued_with job: ActiveStorage::PurgeJob, args: [ blob ] do
+ @user.update! avatar: nil
+ end
+
+ assert_not @user.avatar.attached?
+ end
+ end
+
+ test "updating an existing record to remove an independent attachment" do
+ create_blob(filename: "funky.jpg").tap do |blob|
+ @user.cover_photo.attach blob
+
+ assert_no_enqueued_jobs only: ActiveStorage::PurgeJob do
+ @user.update! cover_photo: nil
+ end
+
+ assert_not @user.cover_photo.attached?
+ end
+ end
+
+ test "analyzing a new blob from an uploaded file after attaching it to an existing record" do
+ perform_enqueued_jobs do
+ @user.avatar.attach fixture_file_upload("racecar.jpg")
+ end
+
+ assert @user.avatar.reload.analyzed?
+ assert_equal 4104, @user.avatar.metadata[:width]
+ assert_equal 2736, @user.avatar.metadata[:height]
+ end
+
+ test "analyzing a new blob from an uploaded file after attaching it to an existing record via update" do
+ perform_enqueued_jobs do
+ @user.update! avatar: fixture_file_upload("racecar.jpg")
+ end
+
+ assert @user.avatar.reload.analyzed?
+ assert_equal 4104, @user.avatar.metadata[:width]
+ assert_equal 2736, @user.avatar.metadata[:height]
+ end
+
+ test "analyzing a directly-uploaded blob after attaching it to an existing record" do
+ perform_enqueued_jobs do
+ @user.avatar.attach directly_upload_file_blob(filename: "racecar.jpg")
+ end
+
+ assert @user.avatar.reload.analyzed?
+ assert_equal 4104, @user.avatar.metadata[:width]
+ assert_equal 2736, @user.avatar.metadata[:height]
+ end
+
+ test "analyzing a directly-uploaded blob after attaching it to an existing record via updates" do
+ perform_enqueued_jobs do
+ @user.update! avatar: directly_upload_file_blob(filename: "racecar.jpg")
+ end
+
+ assert @user.avatar.reload.analyzed?
+ assert_equal 4104, @user.avatar.metadata[:width]
+ assert_equal 2736, @user.avatar.metadata[:height]
+ end
+
+ test "attaching an existing blob to a new record" do
+ User.new(name: "Jason").tap do |user|
+ user.avatar.attach create_blob(filename: "funky.jpg")
+ assert user.new_record?
+ assert_equal "funky.jpg", user.avatar.filename.to_s
+
+ user.save!
+ assert_equal "funky.jpg", user.reload.avatar.filename.to_s
+ end
+ end
+
+ test "attaching an existing blob from a signed ID to a new record" do
+ User.new(name: "Jason").tap do |user|
+ user.avatar.attach create_blob(filename: "funky.jpg").signed_id
+ assert user.new_record?
+ assert_equal "funky.jpg", user.avatar.filename.to_s
+
+ user.save!
+ assert_equal "funky.jpg", user.reload.avatar.filename.to_s
+ end
+ end
+
+ test "attaching a new blob from a Hash to a new record" do
+ User.new(name: "Jason").tap do |user|
+ user.avatar.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg"
+ assert user.new_record?
+ assert user.avatar.attachment.new_record?
+ assert user.avatar.blob.new_record?
+ assert_equal "town.jpg", user.avatar.filename.to_s
+ assert_not ActiveStorage::Blob.service.exist?(user.avatar.key)
+
+ user.save!
+ assert user.avatar.attachment.persisted?
+ assert user.avatar.blob.persisted?
+ assert_equal "town.jpg", user.reload.avatar.filename.to_s
+ assert ActiveStorage::Blob.service.exist?(user.avatar.key)
+ end
+ end
+
+ test "attaching a new blob from an uploaded file to a new record" do
+ User.new(name: "Jason").tap do |user|
+ user.avatar.attach fixture_file_upload("racecar.jpg")
+ assert user.new_record?
+ assert user.avatar.attachment.new_record?
+ assert user.avatar.blob.new_record?
+ assert_equal "racecar.jpg", user.avatar.filename.to_s
+ assert_not ActiveStorage::Blob.service.exist?(user.avatar.key)
+
+ user.save!
+ assert user.avatar.attachment.persisted?
+ assert user.avatar.blob.persisted?
+ assert_equal "racecar.jpg", user.reload.avatar.filename.to_s
+ assert ActiveStorage::Blob.service.exist?(user.avatar.key)
+ end
+ end
+
+ test "creating a record with an existing blob attached" do
+ user = User.create!(name: "Jason", avatar: create_blob(filename: "funky.jpg"))
+ assert_equal "funky.jpg", user.reload.avatar.filename.to_s
+ end
+
+ test "creating a record with an existing blob from a signed ID attached" do
+ user = User.create!(name: "Jason", avatar: create_blob(filename: "funky.jpg").signed_id)
+ assert_equal "funky.jpg", user.reload.avatar.filename.to_s
+ end
+
+ test "creating a record with a new blob from an uploaded file attached" do
+ User.new(name: "Jason", avatar: fixture_file_upload("racecar.jpg")).tap do |user|
+ assert user.new_record?
+ assert user.avatar.attachment.new_record?
+ assert user.avatar.blob.new_record?
+ assert_equal "racecar.jpg", user.avatar.filename.to_s
+ assert_not ActiveStorage::Blob.service.exist?(user.avatar.key)
+
+ user.save!
+ assert_equal "racecar.jpg", user.reload.avatar.filename.to_s
+ end
+ end
+
+ test "creating a record with an unexpected object attached" do
+ error = assert_raises(ArgumentError) { User.create!(name: "Jason", avatar: :foo) }
+ assert_equal "Could not find or build blob: expected attachable, got :foo", error.message
+ end
+
+ test "analyzing a new blob from an uploaded file after attaching it to a new record" do
+ perform_enqueued_jobs do
+ user = User.create!(name: "Jason", avatar: fixture_file_upload("racecar.jpg"))
+ assert user.avatar.reload.analyzed?
+ assert_equal 4104, user.avatar.metadata[:width]
+ assert_equal 2736, user.avatar.metadata[:height]
+ end
+ end
+
+ test "analyzing a directly-uploaded blob after attaching it to a new record" do
+ perform_enqueued_jobs do
+ user = User.create!(name: "Jason", avatar: directly_upload_file_blob(filename: "racecar.jpg"))
+ assert user.avatar.reload.analyzed?
+ assert_equal 4104, user.avatar.metadata[:width]
+ assert_equal 2736, user.avatar.metadata[:height]
+ end
+ end
+
+ test "detaching" do
+ create_blob(filename: "funky.jpg").tap do |blob|
+ @user.avatar.attach blob
+ assert @user.avatar.attached?
+
+ perform_enqueued_jobs do
+ @user.avatar.detach
+ end
+
+ assert_not @user.avatar.attached?
+ assert ActiveStorage::Blob.exists?(blob.id)
+ assert ActiveStorage::Blob.service.exist?(blob.key)
+ end
+ end
+
+ test "purging" do
+ create_blob(filename: "funky.jpg").tap do |blob|
+ @user.avatar.attach blob
+ assert @user.avatar.attached?
+
+ @user.avatar.purge
+ assert_not @user.avatar.attached?
+ assert_not ActiveStorage::Blob.exists?(blob.id)
+ assert_not ActiveStorage::Blob.service.exist?(blob.key)
+ end
+ end
+
+ test "purging an attachment with a shared blob" do
+ create_blob(filename: "funky.jpg").tap do |blob|
+ @user.avatar.attach blob
+ assert @user.avatar.attached?
+
+ another_user = User.create!(name: "John")
+ another_user.avatar.attach blob
+ assert another_user.avatar.attached?
+
+ @user.avatar.purge
+ assert_not @user.avatar.attached?
+ assert ActiveStorage::Blob.exists?(blob.id)
+ assert ActiveStorage::Blob.service.exist?(blob.key)
+ end
+ end
+
+ test "purging later" do
+ create_blob(filename: "funky.jpg").tap do |blob|
+ @user.avatar.attach blob
+ assert @user.avatar.attached?
+
+ perform_enqueued_jobs do
+ @user.avatar.purge_later
+ end
+
+ assert_not @user.avatar.attached?
+ assert_not ActiveStorage::Blob.exists?(blob.id)
+ assert_not ActiveStorage::Blob.service.exist?(blob.key)
+ end
+ end
+
+ test "purging an attachment later with shared blob" do
+ create_blob(filename: "funky.jpg").tap do |blob|
+ @user.avatar.attach blob
+ assert @user.avatar.attached?
+
+ another_user = User.create!(name: "John")
+ another_user.avatar.attach blob
+ assert another_user.avatar.attached?
+
+ perform_enqueued_jobs do
+ @user.avatar.purge_later
+ end
+
+ assert_not @user.avatar.attached?
+ assert ActiveStorage::Blob.exists?(blob.id)
+ assert ActiveStorage::Blob.service.exist?(blob.key)
+ end
+ end
+
+ test "purging dependent attachment later on destroy" do
+ create_blob(filename: "funky.jpg").tap do |blob|
+ @user.avatar.attach blob
+
+ perform_enqueued_jobs do
+ @user.destroy!
+ end
+
+ assert_not ActiveStorage::Blob.exists?(blob.id)
+ assert_not ActiveStorage::Blob.service.exist?(blob.key)
+ end
+ end
+
+ test "not purging independent attachment on destroy" do
+ create_blob(filename: "funky.jpg").tap do |blob|
+ @user.cover_photo.attach blob
+
+ assert_no_enqueued_jobs do
+ @user.destroy!
+ end
+ end
+ end
+
+ test "clearing change on reload" do
+ @user.avatar = create_blob(filename: "funky.jpg")
+ assert @user.avatar.attached?
+
+ @user.reload
+ assert_not @user.avatar.attached?
+ end
+
+ test "overriding attached reader" do
+ @user.avatar.attach create_blob(filename: "funky.jpg")
+
+ assert_equal "funky.jpg", @user.avatar.filename.to_s
+
+ begin
+ User.class_eval do
+ def avatar
+ super.filename.to_s.reverse
+ end
+ end
+
+ assert_equal "gpj.yknuf", @user.avatar
+ ensure
+ User.send(:remove_method, :avatar)
+ end
+ end
+end
diff --git a/activestorage/test/models/blob_test.rb b/activestorage/test/models/blob_test.rb
new file mode 100644
index 0000000000..1a6a89de56
--- /dev/null
+++ b/activestorage/test/models/blob_test.rb
@@ -0,0 +1,191 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+require "active_support/testing/method_call_assertions"
+
+class ActiveStorage::BlobTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::MethodCallAssertions
+
+ test "unattached scope" do
+ [ create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg") ].tap do |blobs|
+ User.create! name: "DHH", avatar: blobs.first
+ assert_includes ActiveStorage::Blob.unattached, blobs.second
+ assert_not_includes ActiveStorage::Blob.unattached, blobs.first
+
+ User.create! name: "Jason", avatar: blobs.second
+ assert_not_includes ActiveStorage::Blob.unattached, blobs.second
+ end
+ end
+
+ test "create after upload sets byte size and checksum" do
+ data = "Hello world!"
+ blob = create_blob data: data
+
+ assert_equal data, blob.download
+ assert_equal data.length, blob.byte_size
+ assert_equal Digest::MD5.base64digest(data), blob.checksum
+ end
+
+ test "create after upload extracts content type from data" do
+ blob = create_file_blob content_type: "application/octet-stream"
+ assert_equal "image/jpeg", blob.content_type
+ end
+
+ test "create after upload extracts content type from filename" do
+ blob = create_blob content_type: "application/octet-stream"
+ assert_equal "text/plain", blob.content_type
+ end
+
+ test "create after upload extracts content_type from io when no content_type given and identify: false" do
+ blob = create_blob content_type: nil, identify: false
+ assert_equal "text/plain", blob.content_type
+ end
+
+ test "create after upload uses content_type when identify: false" do
+ blob = create_blob data: "Article,dates,analysis\n1, 2, 3", filename: "table.csv", content_type: "text/csv", identify: false
+ assert_equal "text/csv", blob.content_type
+ end
+
+ test "image?" do
+ blob = create_file_blob filename: "racecar.jpg"
+ assert_predicate blob, :image?
+ assert_not_predicate blob, :audio?
+ end
+
+ test "video?" do
+ blob = create_file_blob(filename: "video.mp4", content_type: "video/mp4")
+ assert_predicate blob, :video?
+ assert_not_predicate blob, :audio?
+ end
+
+ test "text?" do
+ blob = create_blob data: "Hello world!"
+ assert_predicate blob, :text?
+ assert_not_predicate blob, :audio?
+ end
+
+ test "download yields chunks" do
+ blob = create_blob data: "a" * 5.0625.megabytes
+ chunks = []
+
+ blob.download do |chunk|
+ chunks << chunk
+ end
+
+ assert_equal 2, chunks.size
+ assert_equal "a" * 5.megabytes, chunks.first
+ assert_equal "a" * 64.kilobytes, chunks.second
+ end
+
+ test "open with integrity" do
+ create_file_blob(filename: "racecar.jpg").tap do |blob|
+ blob.open do |file|
+ assert file.binmode?
+ assert_equal 0, file.pos
+ assert File.basename(file.path).starts_with?("ActiveStorage-#{blob.id}-")
+ assert file.path.ends_with?(".jpg")
+ assert_equal file_fixture("racecar.jpg").binread, file.read, "Expected downloaded file to match fixture file"
+ end
+ end
+ end
+
+ test "open without integrity" do
+ create_blob(data: "Hello, world!").tap do |blob|
+ blob.update! checksum: Digest::MD5.base64digest("Goodbye, world!")
+
+ assert_raises ActiveStorage::IntegrityError do
+ blob.open { |file| flunk "Expected integrity check to fail" }
+ end
+ end
+ end
+
+ test "open in a custom tempdir" do
+ tempdir = Dir.mktmpdir
+
+ create_file_blob(filename: "racecar.jpg").open(tempdir: tempdir) do |file|
+ assert file.binmode?
+ assert_equal 0, file.pos
+ assert_match(/\.jpg\z/, file.path)
+ assert file.path.starts_with?(tempdir)
+ assert_equal file_fixture("racecar.jpg").binread, file.read, "Expected downloaded file to match fixture file"
+ end
+ end
+
+ test "urls expiring in 5 minutes" do
+ blob = create_blob
+
+ freeze_time do
+ assert_equal expected_url_for(blob), blob.service_url
+ assert_equal expected_url_for(blob, disposition: :attachment), blob.service_url(disposition: :attachment)
+ end
+ end
+
+ test "urls force attachment as content disposition for content types served as binary" do
+ blob = create_blob(content_type: "text/html")
+
+ freeze_time do
+ assert_equal expected_url_for(blob, disposition: :attachment), blob.service_url
+ assert_equal expected_url_for(blob, disposition: :attachment), blob.service_url(disposition: :inline)
+ end
+ end
+
+ test "urls allow for custom filename" do
+ blob = create_blob(filename: "original.txt")
+ new_filename = ActiveStorage::Filename.new("new.txt")
+
+ freeze_time do
+ assert_equal expected_url_for(blob), blob.service_url
+ assert_equal expected_url_for(blob, filename: new_filename), blob.service_url(filename: new_filename)
+ assert_equal expected_url_for(blob, filename: new_filename), blob.service_url(filename: "new.txt")
+ assert_equal expected_url_for(blob, filename: blob.filename), blob.service_url(filename: nil)
+ end
+ end
+
+ test "urls allow for custom options" do
+ blob = create_blob(filename: "original.txt")
+
+ arguments = [
+ blob.key,
+ expires_in: ActiveStorage.service_urls_expire_in,
+ disposition: :inline,
+ content_type: blob.content_type,
+ filename: blob.filename,
+ thumb_size: "300x300",
+ thumb_mode: "crop"
+ ]
+ assert_called_with(blob.service, :url, arguments) do
+ blob.service_url(thumb_size: "300x300", thumb_mode: "crop")
+ end
+ end
+
+ test "purge deletes file from external service" do
+ blob = create_blob
+
+ blob.purge
+ assert_not ActiveStorage::Blob.service.exist?(blob.key)
+ end
+
+ test "purge deletes variants from external service" do
+ blob = create_file_blob
+ variant = blob.variant(resize: "100>").processed
+
+ blob.purge
+ assert_not ActiveStorage::Blob.service.exist?(variant.key)
+ end
+
+ test "purge does nothing when attachments exist" do
+ create_blob.tap do |blob|
+ User.create! name: "DHH", avatar: blob
+ assert_no_difference(-> { ActiveStorage::Blob.count }) { blob.purge }
+ assert ActiveStorage::Blob.service.exist?(blob.key)
+ end
+ end
+
+ private
+ def expected_url_for(blob, disposition: :inline, filename: nil)
+ filename ||= blob.filename
+ query_string = { content_type: blob.content_type, disposition: ActionDispatch::Http::ContentDisposition.format(disposition: disposition, filename: filename.sanitized) }.to_param
+ "https://example.com/rails/active_storage/disk/#{ActiveStorage.verifier.generate(blob.key, expires_in: 5.minutes, purpose: :blob_key)}/#{filename}?#{query_string}"
+ end
+end
diff --git a/activestorage/test/models/filename_test.rb b/activestorage/test/models/filename_test.rb
new file mode 100644
index 0000000000..715116309f
--- /dev/null
+++ b/activestorage/test/models/filename_test.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class ActiveStorage::FilenameTest < ActiveSupport::TestCase
+ test "base" do
+ assert_equal "racecar", ActiveStorage::Filename.new("racecar.jpg").base
+ assert_equal "race.car", ActiveStorage::Filename.new("race.car.jpg").base
+ assert_equal "racecar", ActiveStorage::Filename.new("racecar").base
+ end
+
+ test "extension with delimiter" do
+ assert_equal ".jpg", ActiveStorage::Filename.new("racecar.jpg").extension_with_delimiter
+ assert_equal ".jpg", ActiveStorage::Filename.new("race.car.jpg").extension_with_delimiter
+ assert_equal "", ActiveStorage::Filename.new("racecar").extension_with_delimiter
+ end
+
+ test "extension without delimiter" do
+ assert_equal "jpg", ActiveStorage::Filename.new("racecar.jpg").extension_without_delimiter
+ assert_equal "jpg", ActiveStorage::Filename.new("race.car.jpg").extension_without_delimiter
+ assert_equal "", ActiveStorage::Filename.new("racecar").extension_without_delimiter
+ end
+
+ test "sanitize" do
+ "%$|:;/\t\r\n\\".each_char do |character|
+ filename = ActiveStorage::Filename.new("foo#{character}bar.pdf")
+ assert_equal "foo-bar.pdf", filename.sanitized
+ assert_equal "foo-bar.pdf", filename.to_s
+ end
+ end
+
+ test "sanitize transcodes to valid UTF-8" do
+ { (+"\xF6").force_encoding(Encoding::ISO8859_1) => "ö",
+ (+"\xC3").force_encoding(Encoding::ISO8859_1) => "Ã",
+ "\xAD" => "�",
+ "\xCF" => "�",
+ "\x00" => "",
+ }.each do |actual, expected|
+ assert_equal expected, ActiveStorage::Filename.new(actual).sanitized
+ end
+ end
+
+ test "strips RTL override chars used to spoof unsafe executables as docs" do
+ # Would be displayed in Windows as "evilexe.pdf" due to the right-to-left
+ # (RTL) override char!
+ assert_equal "evil-fdp.exe", ActiveStorage::Filename.new("evil\u{202E}fdp.exe").sanitized
+ end
+
+ test "compare case-insensitively" do
+ assert_operator ActiveStorage::Filename.new("foobar.pdf"), :==, ActiveStorage::Filename.new("FooBar.PDF")
+ end
+
+ test "compare sanitized" do
+ assert_operator ActiveStorage::Filename.new("foo-bar.pdf"), :==, ActiveStorage::Filename.new("foo\tbar.pdf")
+ end
+end
diff --git a/activestorage/test/models/presence_validation_test.rb b/activestorage/test/models/presence_validation_test.rb
new file mode 100644
index 0000000000..13ba3c900d
--- /dev/null
+++ b/activestorage/test/models/presence_validation_test.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+class ActiveStorage::PresenceValidationTest < ActiveSupport::TestCase
+ class Admin < User; end
+
+ teardown do
+ Admin.clear_validators!
+ end
+
+ test "validates_presence_of has_one_attached" do
+ Admin.validates_presence_of :avatar
+ a = Admin.new(name: "DHH")
+ assert_predicate a, :invalid?
+
+ a.avatar.attach create_blob(filename: "funky.jpg")
+ assert_predicate a, :valid?
+ end
+
+ test "validates_presence_of has_many_attached" do
+ Admin.validates_presence_of :highlights
+ a = Admin.new(name: "DHH")
+ assert_predicate a, :invalid?
+
+ a.highlights.attach create_blob(filename: "funky.jpg")
+ assert_predicate a, :valid?
+ end
+end
diff --git a/activestorage/test/models/preview_test.rb b/activestorage/test/models/preview_test.rb
new file mode 100644
index 0000000000..e7ae399fb7
--- /dev/null
+++ b/activestorage/test/models/preview_test.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+class ActiveStorage::PreviewTest < ActiveSupport::TestCase
+ test "previewing a PDF" do
+ blob = create_file_blob(filename: "report.pdf", content_type: "application/pdf")
+ preview = blob.preview(resize: "640x280").processed
+
+ assert_predicate preview.image, :attached?
+ assert_equal "report.png", preview.image.filename.to_s
+ assert_equal "image/png", preview.image.content_type
+
+ image = read_image(preview.image)
+ assert_equal 612, image.width
+ assert_equal 792, image.height
+ end
+
+ test "previewing an MP4 video" do
+ blob = create_file_blob(filename: "video.mp4", content_type: "video/mp4")
+ preview = blob.preview(resize: "640x280").processed
+
+ assert_predicate preview.image, :attached?
+ assert_equal "video.jpg", preview.image.filename.to_s
+ assert_equal "image/jpeg", preview.image.content_type
+
+ image = read_image(preview.image)
+ assert_equal 640, image.width
+ assert_equal 480, image.height
+ end
+
+ test "previewing an unpreviewable blob" do
+ blob = create_file_blob
+
+ assert_raises ActiveStorage::UnpreviewableError do
+ blob.preview resize: "640x280"
+ end
+ end
+end
diff --git a/activestorage/test/models/reflection_test.rb b/activestorage/test/models/reflection_test.rb
new file mode 100644
index 0000000000..98606b0617
--- /dev/null
+++ b/activestorage/test/models/reflection_test.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class ActiveStorage::ReflectionTest < ActiveSupport::TestCase
+ test "reflecting on a singular attachment" do
+ reflection = User.reflect_on_attachment(:avatar)
+ assert_equal User, reflection.active_record
+ assert_equal :avatar, reflection.name
+ assert_equal :has_one_attached, reflection.macro
+ assert_equal :purge_later, reflection.options[:dependent]
+ end
+
+ test "reflection on a singular attachment with the same name as an attachment on another model" do
+ reflection = Group.reflect_on_attachment(:avatar)
+ assert_equal Group, reflection.active_record
+ end
+
+ test "reflecting on a collection attachment" do
+ reflection = User.reflect_on_attachment(:highlights)
+ assert_equal User, reflection.active_record
+ assert_equal :highlights, reflection.name
+ assert_equal :has_many_attached, reflection.macro
+ assert_equal :purge_later, reflection.options[:dependent]
+ end
+
+ test "reflecting on all attachments" do
+ reflections = User.reflect_on_all_attachments.sort_by(&:name)
+ assert_equal [ User ], reflections.collect(&:active_record).uniq
+ assert_equal %i[ avatar cover_photo highlights vlogs ], reflections.collect(&:name)
+ assert_equal %i[ has_one_attached has_one_attached has_many_attached has_many_attached ], reflections.collect(&:macro)
+ assert_equal [ :purge_later, false, :purge_later, false ], reflections.collect { |reflection| reflection.options[:dependent] }
+ end
+end
diff --git a/activestorage/test/models/representation_test.rb b/activestorage/test/models/representation_test.rb
new file mode 100644
index 0000000000..2a06b31c77
--- /dev/null
+++ b/activestorage/test/models/representation_test.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+class ActiveStorage::RepresentationTest < ActiveSupport::TestCase
+ test "representing an image" do
+ blob = create_file_blob
+ representation = blob.representation(resize: "100x100").processed
+
+ image = read_image(representation.image)
+ assert_equal 100, image.width
+ assert_equal 67, image.height
+ end
+
+ test "representing a PDF" do
+ blob = create_file_blob(filename: "report.pdf", content_type: "application/pdf")
+ representation = blob.representation(resize: "640x280").processed
+
+ image = read_image(representation.image)
+ assert_equal 612, image.width
+ assert_equal 792, image.height
+ end
+
+ test "representing an MP4 video" do
+ blob = create_file_blob(filename: "video.mp4", content_type: "video/mp4")
+ representation = blob.representation(resize: "640x280").processed
+
+ image = read_image(representation.image)
+ assert_equal 640, image.width
+ assert_equal 480, image.height
+ end
+
+ test "representing an unrepresentable blob" do
+ blob = create_blob
+
+ assert_raises ActiveStorage::UnrepresentableError do
+ blob.representation resize: "100x100"
+ end
+ end
+end
diff --git a/activestorage/test/models/variant_test.rb b/activestorage/test/models/variant_test.rb
new file mode 100644
index 0000000000..6577f1cd9f
--- /dev/null
+++ b/activestorage/test/models/variant_test.rb
@@ -0,0 +1,177 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+class ActiveStorage::VariantTest < ActiveSupport::TestCase
+ test "resized variation of JPEG blob" do
+ blob = create_file_blob(filename: "racecar.jpg")
+ variant = blob.variant(resize: "100x100").processed
+ assert_match(/racecar\.jpg/, variant.service_url)
+
+ image = read_image(variant)
+ assert_equal 100, image.width
+ assert_equal 67, image.height
+ end
+
+ test "resized and monochrome variation of JPEG blob" do
+ blob = create_file_blob(filename: "racecar.jpg")
+ variant = blob.variant(resize: "100x100", monochrome: true).processed
+ assert_match(/racecar\.jpg/, variant.service_url)
+
+ image = read_image(variant)
+ assert_equal 100, image.width
+ assert_equal 67, image.height
+ assert_match(/Gray/, image.colorspace)
+ end
+
+ test "monochrome with default variant_processor" do
+ begin
+ ActiveStorage.variant_processor = nil
+
+ blob = create_file_blob(filename: "racecar.jpg")
+ variant = blob.variant(monochrome: true).processed
+ image = read_image(variant)
+ assert_match(/Gray/, image.colorspace)
+ ensure
+ ActiveStorage.variant_processor = :mini_magick
+ end
+ end
+
+ test "disabled variation of JPEG blob" do
+ blob = create_file_blob(filename: "racecar.jpg")
+ variant = blob.variant(resize: "100x100", monochrome: false).processed
+ assert_match(/racecar\.jpg/, variant.service_url)
+
+ image = read_image(variant)
+ assert_equal 100, image.width
+ assert_equal 67, image.height
+ assert_match(/RGB/, image.colorspace)
+ end
+
+ test "disabled variation of JPEG blob with :combine_options" do
+ blob = create_file_blob(filename: "racecar.jpg")
+ variant = ActiveSupport::Deprecation.silence do
+ blob.variant(combine_options: {
+ resize: "100x100",
+ monochrome: false
+ }).processed
+ end
+ assert_match(/racecar\.jpg/, variant.service_url)
+
+ image = read_image(variant)
+ assert_equal 100, image.width
+ assert_equal 67, image.height
+ assert_match(/RGB/, image.colorspace)
+ end
+
+ test "disabled variation using :combine_options" do
+ begin
+ ActiveStorage.variant_processor = nil
+ blob = create_file_blob(filename: "racecar.jpg")
+ variant = ActiveSupport::Deprecation.silence do
+ blob.variant(combine_options: {
+ crop: "100x100+0+0",
+ monochrome: false
+ }).processed
+ end
+ assert_match(/racecar\.jpg/, variant.service_url)
+
+ image = read_image(variant)
+ assert_equal 100, image.width
+ assert_equal 100, image.height
+ assert_match(/RGB/, image.colorspace)
+ ensure
+ ActiveStorage.variant_processor = :mini_magick
+ end
+ end
+
+ test "center-weighted crop of JPEG blob using :combine_options" do
+ begin
+ ActiveStorage.variant_processor = nil
+ blob = create_file_blob(filename: "racecar.jpg")
+ variant = ActiveSupport::Deprecation.silence do
+ blob.variant(combine_options: {
+ gravity: "center",
+ resize: "100x100^",
+ crop: "100x100+0+0",
+ }).processed
+ end
+ assert_match(/racecar\.jpg/, variant.service_url)
+
+ image = read_image(variant)
+ assert_equal 100, image.width
+ assert_equal 100, image.height
+ ensure
+ ActiveStorage.variant_processor = :mini_magick
+ end
+ end
+
+ test "center-weighted crop of JPEG blob using :resize_to_fill" do
+ blob = create_file_blob(filename: "racecar.jpg")
+ variant = blob.variant(resize_to_fill: [100, 100]).processed
+ assert_match(/racecar\.jpg/, variant.service_url)
+
+ image = read_image(variant)
+ assert_equal 100, image.width
+ assert_equal 100, image.height
+ end
+
+ test "resized variation of PSD blob" do
+ blob = create_file_blob(filename: "icon.psd", content_type: "image/vnd.adobe.photoshop")
+ variant = blob.variant(resize: "20x20").processed
+ assert_match(/icon\.png/, variant.service_url)
+
+ image = read_image(variant)
+ assert_equal "PNG", image.type
+ assert_equal 20, image.width
+ assert_equal 20, image.height
+ end
+
+ test "resized variation of ICO blob" do
+ blob = create_file_blob(filename: "favicon.ico", content_type: "image/vnd.microsoft.icon")
+ variant = blob.variant(resize: "20x20").processed
+ assert_match(/icon\.png/, variant.service_url)
+
+ image = read_image(variant)
+ assert_equal "PNG", image.type
+ assert_equal 20, image.width
+ assert_equal 20, image.height
+ end
+
+ test "optimized variation of GIF blob" do
+ blob = create_file_blob(filename: "image.gif", content_type: "image/gif")
+
+ assert_nothing_raised do
+ blob.variant(layers: "Optimize").processed
+ end
+ end
+
+ test "variation of invariable blob" do
+ assert_raises ActiveStorage::InvariableError do
+ create_file_blob(filename: "report.pdf", content_type: "application/pdf").variant(resize: "100x100")
+ end
+ end
+
+ test "service_url doesn't grow in length despite long variant options" do
+ blob = create_file_blob(filename: "racecar.jpg")
+ variant = blob.variant(font: "a" * 10_000).processed
+ assert_operator variant.service_url.length, :<, 525
+ end
+
+ test "works for vips processor" do
+ begin
+ ActiveStorage.variant_processor = :vips
+ blob = create_file_blob(filename: "racecar.jpg")
+ variant = blob.variant(thumbnail_image: 100).processed
+
+ image = read_image(variant)
+ assert_equal 100, image.width
+ assert_equal 67, image.height
+ rescue LoadError
+ # libvips not installed
+ ensure
+ ActiveStorage.variant_processor = :mini_magick
+ end
+ end
+end
diff --git a/activestorage/test/previewer/mupdf_previewer_test.rb b/activestorage/test/previewer/mupdf_previewer_test.rb
new file mode 100644
index 0000000000..6c2db6fcbf
--- /dev/null
+++ b/activestorage/test/previewer/mupdf_previewer_test.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+require "active_storage/previewer/mupdf_previewer"
+
+class ActiveStorage::Previewer::MuPDFPreviewerTest < ActiveSupport::TestCase
+ setup do
+ @blob = create_file_blob(filename: "report.pdf", content_type: "application/pdf")
+ end
+
+ test "previewing a PDF document" do
+ ActiveStorage::Previewer::MuPDFPreviewer.new(@blob).preview do |attachable|
+ assert_equal "image/png", attachable[:content_type]
+ assert_equal "report.png", attachable[:filename]
+
+ image = MiniMagick::Image.read(attachable[:io])
+ assert_equal 612, image.width
+ assert_equal 792, image.height
+ end
+ end
+end
diff --git a/activestorage/test/previewer/poppler_pdf_previewer_test.rb b/activestorage/test/previewer/poppler_pdf_previewer_test.rb
new file mode 100644
index 0000000000..2b41c8b642
--- /dev/null
+++ b/activestorage/test/previewer/poppler_pdf_previewer_test.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+require "active_storage/previewer/poppler_pdf_previewer"
+
+class ActiveStorage::Previewer::PopplerPDFPreviewerTest < ActiveSupport::TestCase
+ setup do
+ @blob = create_file_blob(filename: "report.pdf", content_type: "application/pdf")
+ end
+
+ test "previewing a PDF document" do
+ ActiveStorage::Previewer::PopplerPDFPreviewer.new(@blob).preview do |attachable|
+ assert_equal "image/png", attachable[:content_type]
+ assert_equal "report.png", attachable[:filename]
+
+ image = MiniMagick::Image.read(attachable[:io])
+ assert_equal 612, image.width
+ assert_equal 792, image.height
+ end
+ end
+end
diff --git a/activestorage/test/previewer/video_previewer_test.rb b/activestorage/test/previewer/video_previewer_test.rb
new file mode 100644
index 0000000000..9dc350205b
--- /dev/null
+++ b/activestorage/test/previewer/video_previewer_test.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+require "active_storage/previewer/video_previewer"
+
+class ActiveStorage::Previewer::VideoPreviewerTest < ActiveSupport::TestCase
+ setup do
+ @blob = create_file_blob(filename: "video.mp4", content_type: "video/mp4")
+ end
+
+ test "previewing an MP4 video" do
+ ActiveStorage::Previewer::VideoPreviewer.new(@blob).preview do |attachable|
+ assert_equal "image/jpeg", attachable[:content_type]
+ assert_equal "video.jpg", attachable[:filename]
+
+ image = MiniMagick::Image.read(attachable[:io])
+ assert_equal 640, image.width
+ assert_equal 480, image.height
+ assert_equal "image/jpeg", image.mime_type
+ end
+ end
+end
diff --git a/activestorage/test/service/azure_storage_service_test.rb b/activestorage/test/service/azure_storage_service_test.rb
new file mode 100644
index 0000000000..09c2e7f99c
--- /dev/null
+++ b/activestorage/test/service/azure_storage_service_test.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require "service/shared_service_tests"
+require "uri"
+
+if SERVICE_CONFIGURATIONS[:azure]
+ class ActiveStorage::Service::AzureStorageServiceTest < ActiveSupport::TestCase
+ SERVICE = ActiveStorage::Service.configure(:azure, SERVICE_CONFIGURATIONS)
+
+ include ActiveStorage::Service::SharedServiceTests
+
+ test "signed URL generation" do
+ url = @service.url(@key, expires_in: 5.minutes,
+ disposition: :inline, filename: ActiveStorage::Filename.new("avatar.png"), content_type: "image/png")
+
+ assert_match(/(\S+)&rscd=inline%3B\+filename%3D%22avatar\.png%22%3B\+filename\*%3DUTF-8%27%27avatar\.png&rsct=image%2Fpng/, url)
+ assert_match SERVICE_CONFIGURATIONS[:azure][:container], url
+ end
+
+ test "uploading a tempfile" do
+ begin
+ key = SecureRandom.base58(24)
+ data = "Something else entirely!"
+
+ Tempfile.open do |file|
+ file.write(data)
+ file.rewind
+ @service.upload(key, file)
+ end
+
+ assert_equal data, @service.download(key)
+ ensure
+ @service.delete(key)
+ end
+ end
+ end
+else
+ puts "Skipping Azure Storage Service tests because no Azure configuration was supplied"
+end
diff --git a/activestorage/test/service/configurations.example.yml b/activestorage/test/service/configurations.example.yml
new file mode 100644
index 0000000000..a63aa33302
--- /dev/null
+++ b/activestorage/test/service/configurations.example.yml
@@ -0,0 +1,29 @@
+# s3:
+# service: S3
+# access_key_id: ""
+# secret_access_key: ""
+# region: ""
+# bucket: ""
+#
+# gcs:
+# service: GCS
+# credentials: {
+# type: "service_account",
+# project_id: "",
+# private_key_id: "",
+# private_key: "",
+# client_email: "",
+# client_id: "",
+# auth_uri: "https://accounts.google.com/o/oauth2/auth",
+# token_uri: "https://accounts.google.com/o/oauth2/token",
+# auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs",
+# client_x509_cert_url: ""
+# }
+# project:
+# bucket:
+#
+# azure:
+# service: AzureStorage
+# storage_account_name: ""
+# storage_access_key: ""
+# container: ""
diff --git a/activestorage/test/service/configurations.yml.enc b/activestorage/test/service/configurations.yml.enc
new file mode 100644
index 0000000000..648924a562
--- /dev/null
+++ b/activestorage/test/service/configurations.yml.enc
Binary files differ
diff --git a/activestorage/test/service/configurator_test.rb b/activestorage/test/service/configurator_test.rb
new file mode 100644
index 0000000000..3ef9cf9fb6
--- /dev/null
+++ b/activestorage/test/service/configurator_test.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require "service/shared_service_tests"
+
+class ActiveStorage::Service::ConfiguratorTest < ActiveSupport::TestCase
+ test "builds correct service instance based on service name" do
+ service = ActiveStorage::Service::Configurator.build(:foo, foo: { service: "Disk", root: "path" })
+ assert_instance_of ActiveStorage::Service::DiskService, service
+ assert_equal "path", service.root
+ end
+
+ test "builds correct service instance based on lowercase service name" do
+ service = ActiveStorage::Service::Configurator.build(:foo, foo: { service: "disk", root: "path" })
+ assert_instance_of ActiveStorage::Service::DiskService, service
+ assert_equal "path", service.root
+ end
+
+ test "raises error when passing non-existent service name" do
+ assert_raise RuntimeError do
+ ActiveStorage::Service::Configurator.build(:bigfoot, {})
+ end
+ end
+end
diff --git a/activestorage/test/service/disk_service_test.rb b/activestorage/test/service/disk_service_test.rb
new file mode 100644
index 0000000000..a0218bff1c
--- /dev/null
+++ b/activestorage/test/service/disk_service_test.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require "service/shared_service_tests"
+
+class ActiveStorage::Service::DiskServiceTest < ActiveSupport::TestCase
+ SERVICE = ActiveStorage::Service::DiskService.new(root: File.join(Dir.tmpdir, "active_storage"))
+
+ include ActiveStorage::Service::SharedServiceTests
+
+ test "url generation" do
+ assert_match(/^https:\/\/example.com\/rails\/active_storage\/disk\/.*\/avatar\.png\?content_type=image%2Fpng&disposition=inline/,
+ @service.url(@key, expires_in: 5.minutes, disposition: :inline, filename: ActiveStorage::Filename.new("avatar.png"), content_type: "image/png"))
+ end
+
+ test "headers_for_direct_upload generation" do
+ assert_equal({ "Content-Type" => "application/json" }, @service.headers_for_direct_upload(@key, content_type: "application/json"))
+ end
+end
diff --git a/activestorage/test/service/gcs_service_test.rb b/activestorage/test/service/gcs_service_test.rb
new file mode 100644
index 0000000000..2ba2f8b346
--- /dev/null
+++ b/activestorage/test/service/gcs_service_test.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require "service/shared_service_tests"
+require "net/http"
+
+if SERVICE_CONFIGURATIONS[:gcs]
+ class ActiveStorage::Service::GCSServiceTest < ActiveSupport::TestCase
+ SERVICE = ActiveStorage::Service.configure(:gcs, SERVICE_CONFIGURATIONS)
+
+ include ActiveStorage::Service::SharedServiceTests
+
+ test "direct upload" do
+ begin
+ key = SecureRandom.base58(24)
+ data = "Something else entirely!"
+ checksum = Digest::MD5.base64digest(data)
+ url = @service.url_for_direct_upload(key, expires_in: 5.minutes, content_type: "text/plain", content_length: data.size, checksum: checksum)
+
+ uri = URI.parse url
+ request = Net::HTTP::Put.new uri.request_uri
+ request.body = data
+ request.add_field "Content-Type", ""
+ request.add_field "Content-MD5", checksum
+ Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
+ http.request request
+ end
+
+ assert_equal data, @service.download(key)
+ ensure
+ @service.delete key
+ end
+ end
+
+ test "signed URL generation" do
+ assert_match(/storage\.googleapis\.com\/.*response-content-disposition=inline.*test\.txt.*response-content-type=text%2Fplain/,
+ @service.url(@key, expires_in: 2.minutes, disposition: :inline, filename: ActiveStorage::Filename.new("test.txt"), content_type: "text/plain"))
+ end
+
+ test "signed URL response headers" do
+ begin
+ key = SecureRandom.base58(24)
+ data = "Something else entirely!"
+ @service.upload(key, StringIO.new(data), checksum: Digest::MD5.base64digest(data))
+
+ url = @service.url(key, expires_in: 2.minutes, disposition: :inline, filename: ActiveStorage::Filename.new("test.txt"), content_type: "text/plain")
+ response = Net::HTTP.get_response(URI(url))
+ assert_equal "text/plain", response.content_type
+ ensure
+ @service.delete key
+ end
+ end
+ end
+else
+ puts "Skipping GCS Service tests because no GCS configuration was supplied"
+end
diff --git a/activestorage/test/service/mirror_service_test.rb b/activestorage/test/service/mirror_service_test.rb
new file mode 100644
index 0000000000..bb502dde60
--- /dev/null
+++ b/activestorage/test/service/mirror_service_test.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require "service/shared_service_tests"
+
+class ActiveStorage::Service::MirrorServiceTest < ActiveSupport::TestCase
+ mirror_config = (1..3).map do |i|
+ [ "mirror_#{i}",
+ service: "Disk",
+ root: Dir.mktmpdir("active_storage_tests_mirror_#{i}") ]
+ end.to_h
+
+ config = mirror_config.merge \
+ mirror: { service: "Mirror", primary: "primary", mirrors: mirror_config.keys },
+ primary: { service: "Disk", root: Dir.mktmpdir("active_storage_tests_primary") }
+
+ SERVICE = ActiveStorage::Service.configure :mirror, config
+
+ include ActiveStorage::Service::SharedServiceTests
+
+ test "uploading to all services" do
+ begin
+ key = SecureRandom.base58(24)
+ data = "Something else entirely!"
+ io = StringIO.new(data)
+ checksum = Digest::MD5.base64digest(data)
+
+ @service.upload key, io.tap(&:read), checksum: checksum
+ assert_predicate io, :eof?
+
+ assert_equal data, @service.primary.download(key)
+ @service.mirrors.each do |mirror|
+ assert_equal data, mirror.download(key)
+ end
+ ensure
+ @service.delete key
+ end
+ end
+
+ test "downloading from primary service" do
+ key = SecureRandom.base58(24)
+ data = "Something else entirely!"
+ checksum = Digest::MD5.base64digest(data)
+
+ @service.primary.upload key, StringIO.new(data), checksum: checksum
+
+ assert_equal data, @service.download(key)
+ end
+
+ test "deleting from all services" do
+ @service.delete @key
+
+ assert_not SERVICE.primary.exist?(@key)
+ SERVICE.mirrors.each do |mirror|
+ assert_not mirror.exist?(@key)
+ end
+ end
+
+ test "URL generation in primary service" do
+ filename = ActiveStorage::Filename.new("test.txt")
+
+ freeze_time do
+ assert_equal @service.primary.url(@key, expires_in: 2.minutes, disposition: :inline, filename: filename, content_type: "text/plain"),
+ @service.url(@key, expires_in: 2.minutes, disposition: :inline, filename: filename, content_type: "text/plain")
+ end
+ end
+end
diff --git a/activestorage/test/service/s3_service_test.rb b/activestorage/test/service/s3_service_test.rb
new file mode 100644
index 0000000000..559aa028f2
--- /dev/null
+++ b/activestorage/test/service/s3_service_test.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require "service/shared_service_tests"
+require "net/http"
+
+if SERVICE_CONFIGURATIONS[:s3]
+ class ActiveStorage::Service::S3ServiceTest < ActiveSupport::TestCase
+ SERVICE = ActiveStorage::Service.configure(:s3, SERVICE_CONFIGURATIONS)
+
+ include ActiveStorage::Service::SharedServiceTests
+
+ test "direct upload" do
+ begin
+ key = SecureRandom.base58(24)
+ data = "Something else entirely!"
+ checksum = Digest::MD5.base64digest(data)
+ url = @service.url_for_direct_upload(key, expires_in: 5.minutes, content_type: "text/plain", content_length: data.size, checksum: checksum)
+
+ uri = URI.parse url
+ request = Net::HTTP::Put.new uri.request_uri
+ request.body = data
+ request.add_field "Content-Type", "text/plain"
+ request.add_field "Content-MD5", checksum
+ Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
+ http.request request
+ end
+
+ assert_equal data, @service.download(key)
+ ensure
+ @service.delete key
+ end
+ end
+
+ test "upload a zero byte file" do
+ blob = directly_upload_file_blob filename: "empty_file.txt", content_type: nil
+ user = User.create! name: "DHH", avatar: blob
+
+ assert_equal user.avatar.blob, blob
+ end
+
+ test "signed URL generation" do
+ url = @service.url(@key, expires_in: 5.minutes,
+ disposition: :inline, filename: ActiveStorage::Filename.new("avatar.png"), content_type: "image/png")
+
+ assert_match(/s3(-[-a-z0-9]+)?\.(\S+)?amazonaws.com.*response-content-disposition=inline.*avatar\.png.*response-content-type=image%2Fpng/, url)
+ assert_match SERVICE_CONFIGURATIONS[:s3][:bucket], url
+ end
+
+ test "uploading with server-side encryption" do
+ config = SERVICE_CONFIGURATIONS.deep_merge(s3: { upload: { server_side_encryption: "AES256" } })
+ service = ActiveStorage::Service.configure(:s3, config)
+
+ begin
+ key = SecureRandom.base58(24)
+ data = "Something else entirely!"
+ service.upload key, StringIO.new(data), checksum: Digest::MD5.base64digest(data)
+
+ assert_equal "AES256", service.bucket.object(key).server_side_encryption
+ ensure
+ service.delete key
+ end
+ end
+ end
+else
+ puts "Skipping S3 Service tests because no S3 configuration was supplied"
+end
diff --git a/activestorage/test/service/shared_service_tests.rb b/activestorage/test/service/shared_service_tests.rb
new file mode 100644
index 0000000000..ca2490f2bc
--- /dev/null
+++ b/activestorage/test/service/shared_service_tests.rb
@@ -0,0 +1,130 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "active_support/core_ext/securerandom"
+
+module ActiveStorage::Service::SharedServiceTests
+ extend ActiveSupport::Concern
+
+ FIXTURE_DATA = (+"\211PNG\r\n\032\n\000\000\000\rIHDR\000\000\000\020\000\000\000\020\001\003\000\000\000%=m\"\000\000\000\006PLTE\000\000\000\377\377\377\245\331\237\335\000\000\0003IDATx\234c\370\377\237\341\377_\206\377\237\031\016\2603\334?\314p\1772\303\315\315\f7\215\031\356\024\203\320\275\317\f\367\201R\314\f\017\300\350\377\177\000Q\206\027(\316]\233P\000\000\000\000IEND\256B`\202").force_encoding(Encoding::BINARY)
+
+ included do
+ setup do
+ @key = SecureRandom.base58(24)
+ @service = self.class.const_get(:SERVICE)
+ @service.upload @key, StringIO.new(FIXTURE_DATA)
+ end
+
+ teardown do
+ @service.delete @key
+ end
+
+ test "uploading with integrity" do
+ begin
+ key = SecureRandom.base58(24)
+ data = "Something else entirely!"
+ @service.upload(key, StringIO.new(data), checksum: Digest::MD5.base64digest(data))
+
+ assert_equal data, @service.download(key)
+ ensure
+ @service.delete key
+ end
+ end
+
+ test "uploading without integrity" do
+ begin
+ key = SecureRandom.base58(24)
+ data = "Something else entirely!"
+
+ assert_raises(ActiveStorage::IntegrityError) do
+ @service.upload(key, StringIO.new(data), checksum: Digest::MD5.base64digest("bad data"))
+ end
+
+ assert_not @service.exist?(key)
+ ensure
+ @service.delete key
+ end
+ end
+
+ test "downloading" do
+ assert_equal FIXTURE_DATA, @service.download(@key)
+ end
+
+ test "downloading a nonexistent file" do
+ assert_raises(ActiveStorage::FileNotFoundError) do
+ @service.download(SecureRandom.base58(24))
+ end
+ end
+
+
+ test "downloading in chunks" do
+ key = SecureRandom.base58(24)
+ expected_chunks = [ "a" * 5.megabytes, "b" ]
+ actual_chunks = []
+
+ begin
+ @service.upload key, StringIO.new(expected_chunks.join)
+
+ @service.download key do |chunk|
+ actual_chunks << chunk
+ end
+
+ assert_equal expected_chunks, actual_chunks, "Downloaded chunks did not match uploaded data"
+ ensure
+ @service.delete key
+ end
+ end
+
+ test "downloading a nonexistent file in chunks" do
+ assert_raises(ActiveStorage::FileNotFoundError) do
+ @service.download(SecureRandom.base58(24)) { }
+ end
+ end
+
+
+ test "downloading partially" do
+ assert_equal "\x10\x00\x00", @service.download_chunk(@key, 19..21)
+ assert_equal "\x10\x00\x00", @service.download_chunk(@key, 19...22)
+ end
+
+ test "partially downloading a nonexistent file" do
+ assert_raises(ActiveStorage::FileNotFoundError) do
+ @service.download_chunk(SecureRandom.base58(24), 19..21)
+ end
+ end
+
+
+ test "existing" do
+ assert @service.exist?(@key)
+ assert_not @service.exist?(@key + "nonsense")
+ end
+
+ test "deleting" do
+ @service.delete @key
+ assert_not @service.exist?(@key)
+ end
+
+ test "deleting nonexistent key" do
+ assert_nothing_raised do
+ @service.delete SecureRandom.base58(24)
+ end
+ end
+
+ test "deleting by prefix" do
+ begin
+ @service.upload("a/a/a", StringIO.new(FIXTURE_DATA))
+ @service.upload("a/a/b", StringIO.new(FIXTURE_DATA))
+ @service.upload("a/b/a", StringIO.new(FIXTURE_DATA))
+
+ @service.delete_prefixed("a/a/")
+ assert_not @service.exist?("a/a/a")
+ assert_not @service.exist?("a/a/b")
+ assert @service.exist?("a/b/a")
+ ensure
+ @service.delete("a/a/a")
+ @service.delete("a/a/b")
+ @service.delete("a/b/a")
+ end
+ end
+ end
+end
diff --git a/activestorage/test/template/image_tag_test.rb b/activestorage/test/template/image_tag_test.rb
new file mode 100644
index 0000000000..f0b166c225
--- /dev/null
+++ b/activestorage/test/template/image_tag_test.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+class ActiveStorage::ImageTagTest < ActionView::TestCase
+ tests ActionView::Helpers::AssetTagHelper
+
+ setup do
+ @blob = create_file_blob filename: "racecar.jpg"
+ end
+
+ test "blob" do
+ assert_dom_equal %(<img src="#{polymorphic_url @blob}" />), image_tag(@blob)
+ end
+
+ test "variant" do
+ variant = @blob.variant(resize: "100x100")
+ assert_dom_equal %(<img src="#{polymorphic_url variant}" />), image_tag(variant)
+ end
+
+ test "preview" do
+ blob = create_file_blob(filename: "report.pdf", content_type: "application/pdf")
+ preview = blob.preview(resize: "100x100")
+ assert_dom_equal %(<img src="#{polymorphic_url preview}" />), image_tag(preview)
+ end
+
+ test "attachment" do
+ attachment = ActiveStorage::Attachment.new(blob: @blob)
+ assert_dom_equal %(<img src="#{polymorphic_url attachment}" />), image_tag(attachment)
+ end
+
+ test "error when attachment's empty" do
+ @user = User.create!(name: "DHH")
+
+ assert_not_predicate @user.avatar, :attached?
+ assert_raises(ArgumentError) { image_tag(@user.avatar) }
+ end
+
+ test "error when object can't be resolved into url" do
+ unresolvable_object = ActionView::Helpers::AssetTagHelper
+ assert_raises(ArgumentError) { image_tag(unresolvable_object) }
+ end
+end
diff --git a/activestorage/test/test_helper.rb b/activestorage/test/test_helper.rb
new file mode 100644
index 0000000000..144c224421
--- /dev/null
+++ b/activestorage/test/test_helper.rb
@@ -0,0 +1,103 @@
+# frozen_string_literal: true
+
+ENV["RAILS_ENV"] ||= "test"
+require_relative "dummy/config/environment.rb"
+
+require "bundler/setup"
+require "active_support"
+require "active_support/test_case"
+require "active_support/testing/autorun"
+require "image_processing/mini_magick"
+
+begin
+ require "byebug"
+rescue LoadError
+end
+
+require "active_job"
+ActiveJob::Base.queue_adapter = :test
+ActiveJob::Base.logger = ActiveSupport::Logger.new(nil)
+
+# Filter out the backtrace from minitest while preserving the one from other libraries.
+Minitest.backtrace_filter = Minitest::BacktraceFilter.new
+
+require "yaml"
+SERVICE_CONFIGURATIONS = begin
+ erb = ERB.new(Pathname.new(File.expand_path("service/configurations.yml", __dir__)).read)
+ configuration = YAML.load(erb.result) || {}
+ configuration.deep_symbolize_keys
+rescue Errno::ENOENT
+ puts "Missing service configuration file in test/service/configurations.yml"
+ {}
+end
+
+require "tmpdir"
+ActiveStorage::Blob.service = ActiveStorage::Service::DiskService.new(root: Dir.mktmpdir("active_storage_tests"))
+
+ActiveStorage.logger = ActiveSupport::Logger.new(nil)
+ActiveStorage.verifier = ActiveSupport::MessageVerifier.new("Testing")
+
+class ActiveSupport::TestCase
+ self.file_fixture_path = File.expand_path("fixtures/files", __dir__)
+
+ setup do
+ ActiveStorage::Current.host = "https://example.com"
+ end
+
+ teardown do
+ ActiveStorage::Current.reset
+ end
+
+ private
+ def create_blob(data: "Hello world!", filename: "hello.txt", content_type: "text/plain", identify: true)
+ ActiveStorage::Blob.create_after_upload! io: StringIO.new(data), filename: filename, content_type: content_type, identify: identify
+ end
+
+ def create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg", metadata: nil)
+ ActiveStorage::Blob.create_after_upload! io: file_fixture(filename).open, filename: filename, content_type: content_type, metadata: metadata
+ end
+
+ def create_blob_before_direct_upload(filename: "hello.txt", byte_size:, checksum:, content_type: "text/plain")
+ ActiveStorage::Blob.create_before_direct_upload! filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type
+ end
+
+ def directly_upload_file_blob(filename: "racecar.jpg", content_type: "image/jpeg")
+ file = file_fixture(filename)
+ byte_size = file.size
+ checksum = Digest::MD5.file(file).base64digest
+
+ create_blob_before_direct_upload(filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type).tap do |blob|
+ ActiveStorage::Blob.service.upload(blob.key, file.open)
+ end
+ end
+
+ def read_image(blob_or_variant)
+ MiniMagick::Image.open blob_or_variant.service.send(:path_for, blob_or_variant.key)
+ end
+
+ def extract_metadata_from(blob)
+ blob.tap(&:analyze).metadata
+ end
+
+ def fixture_file_upload(filename)
+ Rack::Test::UploadedFile.new file_fixture(filename).to_s
+ end
+end
+
+require "global_id"
+GlobalID.app = "ActiveStorageExampleApp"
+ActiveRecord::Base.send :include, GlobalID::Identification
+
+class User < ActiveRecord::Base
+ validates :name, presence: true
+
+ has_one_attached :avatar
+ has_one_attached :cover_photo, dependent: false
+
+ has_many_attached :highlights
+ has_many_attached :vlogs, dependent: false
+end
+
+class Group < ActiveRecord::Base
+ has_one_attached :avatar
+end
diff --git a/activestorage/yarn.lock b/activestorage/yarn.lock
new file mode 100644
index 0000000000..44eae3c5b1
--- /dev/null
+++ b/activestorage/yarn.lock
@@ -0,0 +1,1923 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@types/estree@0.0.38":
+ version "0.0.38"
+ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.38.tgz#c1be40aa933723c608820a99a373a16d215a1ca2"
+
+"@types/node@*":
+ version "9.6.6"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-9.6.6.tgz#439b91f9caf3983cad2eef1e11f6bedcbf9431d2"
+
+acorn-jsx@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b"
+ dependencies:
+ acorn "^3.0.4"
+
+acorn@^3.0.4:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
+
+acorn@^5.0.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.1.1.tgz#53fe161111f912ab999ee887a90a0bc52822fd75"
+
+ajv-keywords@^1.0.0:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c"
+
+ajv@^4.7.0:
+ version "4.11.8"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536"
+ dependencies:
+ co "^4.6.0"
+ json-stable-stringify "^1.0.1"
+
+ajv@^5.2.0:
+ version "5.2.2"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.2.tgz#47c68d69e86f5d953103b0074a9430dc63da5e39"
+ dependencies:
+ co "^4.6.0"
+ fast-deep-equal "^1.0.0"
+ json-schema-traverse "^0.3.0"
+ json-stable-stringify "^1.0.1"
+
+ansi-escapes@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-2.0.0.tgz#5bae52be424878dd9783e8910e3fc2922e83c81b"
+
+ansi-regex@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
+
+ansi-regex@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
+
+ansi-styles@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
+
+ansi-styles@^3.1.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88"
+ dependencies:
+ color-convert "^1.9.0"
+
+argparse@^1.0.7:
+ version "1.0.9"
+ resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86"
+ dependencies:
+ sprintf-js "~1.0.2"
+
+arr-diff@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf"
+ dependencies:
+ arr-flatten "^1.0.1"
+
+arr-flatten@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1"
+
+array-union@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39"
+ dependencies:
+ array-uniq "^1.0.1"
+
+array-uniq@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
+
+array-unique@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53"
+
+arrify@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
+
+babel-code-frame@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4"
+ dependencies:
+ chalk "^1.1.0"
+ esutils "^2.0.2"
+ js-tokens "^3.0.0"
+
+babel-core@^6.24.1, babel-core@^6.25.0:
+ version "6.25.0"
+ resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.25.0.tgz#7dd42b0463c742e9d5296deb3ec67a9322dad729"
+ dependencies:
+ babel-code-frame "^6.22.0"
+ babel-generator "^6.25.0"
+ babel-helpers "^6.24.1"
+ babel-messages "^6.23.0"
+ babel-register "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.25.0"
+ babel-traverse "^6.25.0"
+ babel-types "^6.25.0"
+ babylon "^6.17.2"
+ convert-source-map "^1.1.0"
+ debug "^2.1.1"
+ json5 "^0.5.0"
+ lodash "^4.2.0"
+ minimatch "^3.0.2"
+ path-is-absolute "^1.0.0"
+ private "^0.1.6"
+ slash "^1.0.0"
+ source-map "^0.5.0"
+
+babel-generator@^6.25.0:
+ version "6.25.0"
+ resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.25.0.tgz#33a1af70d5f2890aeb465a4a7793c1df6a9ea9fc"
+ dependencies:
+ babel-messages "^6.23.0"
+ babel-runtime "^6.22.0"
+ babel-types "^6.25.0"
+ detect-indent "^4.0.0"
+ jsesc "^1.3.0"
+ lodash "^4.2.0"
+ source-map "^0.5.0"
+ trim-right "^1.0.1"
+
+babel-helper-builder-binary-assignment-operator-visitor@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz#cce4517ada356f4220bcae8a02c2b346f9a56664"
+ dependencies:
+ babel-helper-explode-assignable-expression "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-helper-call-delegate@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz#ece6aacddc76e41c3461f88bfc575bd0daa2df8d"
+ dependencies:
+ babel-helper-hoist-variables "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helper-define-map@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.24.1.tgz#7a9747f258d8947d32d515f6aa1c7bd02204a080"
+ dependencies:
+ babel-helper-function-name "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+ lodash "^4.2.0"
+
+babel-helper-explode-assignable-expression@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz#f25b82cf7dc10433c55f70592d5746400ac22caa"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helper-function-name@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9"
+ dependencies:
+ babel-helper-get-function-arity "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helper-get-function-arity@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz#8f7782aa93407c41d3aa50908f89b031b1b6853d"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-helper-hoist-variables@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz#1ecb27689c9d25513eadbc9914a73f5408be7a76"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-helper-optimise-call-expression@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz#f7a13427ba9f73f8f4fa993c54a97882d1244257"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-helper-regex@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.24.1.tgz#d36e22fab1008d79d88648e32116868128456ce8"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+ lodash "^4.2.0"
+
+babel-helper-remap-async-to-generator@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz#5ec581827ad723fecdd381f1c928390676e4551b"
+ dependencies:
+ babel-helper-function-name "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helper-replace-supers@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz#bf6dbfe43938d17369a213ca8a8bf74b6a90ab1a"
+ dependencies:
+ babel-helper-optimise-call-expression "^6.24.1"
+ babel-messages "^6.23.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helpers@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-messages@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-check-es2015-constants@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz#35157b101426fd2ffd3da3f75c7d1e91835bbf8a"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-external-helpers@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-external-helpers/-/babel-plugin-external-helpers-6.22.0.tgz#2285f48b02bd5dede85175caf8c62e86adccefa1"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-syntax-async-functions@^6.8.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95"
+
+babel-plugin-syntax-exponentiation-operator@^6.8.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de"
+
+babel-plugin-syntax-trailing-function-commas@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3"
+
+babel-plugin-transform-async-to-generator@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz#6536e378aff6cb1d5517ac0e40eb3e9fc8d08761"
+ dependencies:
+ babel-helper-remap-async-to-generator "^6.24.1"
+ babel-plugin-syntax-async-functions "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-arrow-functions@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-block-scoped-functions@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz#bbc51b49f964d70cb8d8e0b94e820246ce3a6141"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-block-scoping@^6.23.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.24.1.tgz#76c295dc3a4741b1665adfd3167215dcff32a576"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+ lodash "^4.2.0"
+
+babel-plugin-transform-es2015-classes@^6.23.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz#5a4c58a50c9c9461e564b4b2a3bfabc97a2584db"
+ dependencies:
+ babel-helper-define-map "^6.24.1"
+ babel-helper-function-name "^6.24.1"
+ babel-helper-optimise-call-expression "^6.24.1"
+ babel-helper-replace-supers "^6.24.1"
+ babel-messages "^6.23.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-computed-properties@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz#6fe2a8d16895d5634f4cd999b6d3480a308159b3"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-es2015-destructuring@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-duplicate-keys@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz#73eb3d310ca969e3ef9ec91c53741a6f1576423e"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-for-of@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz#f47c95b2b613df1d3ecc2fdb7573623c75248691"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-function-name@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz#834c89853bc36b1af0f3a4c5dbaa94fd8eacaa8b"
+ dependencies:
+ babel-helper-function-name "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-literals@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz#4f54a02d6cd66cf915280019a31d31925377ca2e"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-modules-amd@^6.22.0, babel-plugin-transform-es2015-modules-amd@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz#3b3e54017239842d6d19c3011c4bd2f00a00d154"
+ dependencies:
+ babel-plugin-transform-es2015-modules-commonjs "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-es2015-modules-commonjs@^6.23.0, babel-plugin-transform-es2015-modules-commonjs@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.24.1.tgz#d3e310b40ef664a36622200097c6d440298f2bfe"
+ dependencies:
+ babel-plugin-transform-strict-mode "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-modules-systemjs@^6.23.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz#ff89a142b9119a906195f5f106ecf305d9407d23"
+ dependencies:
+ babel-helper-hoist-variables "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-es2015-modules-umd@^6.23.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz#ac997e6285cd18ed6176adb607d602344ad38468"
+ dependencies:
+ babel-plugin-transform-es2015-modules-amd "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-es2015-object-super@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz#24cef69ae21cb83a7f8603dad021f572eb278f8d"
+ dependencies:
+ babel-helper-replace-supers "^6.24.1"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-parameters@^6.23.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz#57ac351ab49caf14a97cd13b09f66fdf0a625f2b"
+ dependencies:
+ babel-helper-call-delegate "^6.24.1"
+ babel-helper-get-function-arity "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-shorthand-properties@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz#24f875d6721c87661bbd99a4622e51f14de38aa0"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-spread@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz#d6d68a99f89aedc4536c81a542e8dd9f1746f8d1"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-sticky-regex@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz#00c1cdb1aca71112cdf0cf6126c2ed6b457ccdbc"
+ dependencies:
+ babel-helper-regex "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-template-literals@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz#a84b3450f7e9f8f1f6839d6d687da84bb1236d8d"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-typeof-symbol@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz#dec09f1cddff94b52ac73d505c84df59dcceb372"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-unicode-regex@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz#d38b12f42ea7323f729387f18a7c5ae1faeb35e9"
+ dependencies:
+ babel-helper-regex "^6.24.1"
+ babel-runtime "^6.22.0"
+ regexpu-core "^2.0.0"
+
+babel-plugin-transform-exponentiation-operator@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz#2ab0c9c7f3098fa48907772bb813fe41e8de3a0e"
+ dependencies:
+ babel-helper-builder-binary-assignment-operator-visitor "^6.24.1"
+ babel-plugin-syntax-exponentiation-operator "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-regenerator@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.24.1.tgz#b8da305ad43c3c99b4848e4fe4037b770d23c418"
+ dependencies:
+ regenerator-transform "0.9.11"
+
+babel-plugin-transform-strict-mode@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-preset-env@^1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.6.0.tgz#2de1c782a780a0a5d605d199c957596da43c44e4"
+ dependencies:
+ babel-plugin-check-es2015-constants "^6.22.0"
+ babel-plugin-syntax-trailing-function-commas "^6.22.0"
+ babel-plugin-transform-async-to-generator "^6.22.0"
+ babel-plugin-transform-es2015-arrow-functions "^6.22.0"
+ babel-plugin-transform-es2015-block-scoped-functions "^6.22.0"
+ babel-plugin-transform-es2015-block-scoping "^6.23.0"
+ babel-plugin-transform-es2015-classes "^6.23.0"
+ babel-plugin-transform-es2015-computed-properties "^6.22.0"
+ babel-plugin-transform-es2015-destructuring "^6.23.0"
+ babel-plugin-transform-es2015-duplicate-keys "^6.22.0"
+ babel-plugin-transform-es2015-for-of "^6.23.0"
+ babel-plugin-transform-es2015-function-name "^6.22.0"
+ babel-plugin-transform-es2015-literals "^6.22.0"
+ babel-plugin-transform-es2015-modules-amd "^6.22.0"
+ babel-plugin-transform-es2015-modules-commonjs "^6.23.0"
+ babel-plugin-transform-es2015-modules-systemjs "^6.23.0"
+ babel-plugin-transform-es2015-modules-umd "^6.23.0"
+ babel-plugin-transform-es2015-object-super "^6.22.0"
+ babel-plugin-transform-es2015-parameters "^6.23.0"
+ babel-plugin-transform-es2015-shorthand-properties "^6.22.0"
+ babel-plugin-transform-es2015-spread "^6.22.0"
+ babel-plugin-transform-es2015-sticky-regex "^6.22.0"
+ babel-plugin-transform-es2015-template-literals "^6.22.0"
+ babel-plugin-transform-es2015-typeof-symbol "^6.23.0"
+ babel-plugin-transform-es2015-unicode-regex "^6.22.0"
+ babel-plugin-transform-exponentiation-operator "^6.22.0"
+ babel-plugin-transform-regenerator "^6.22.0"
+ browserslist "^2.1.2"
+ invariant "^2.2.2"
+ semver "^5.3.0"
+
+babel-register@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.24.1.tgz#7e10e13a2f71065bdfad5a1787ba45bca6ded75f"
+ dependencies:
+ babel-core "^6.24.1"
+ babel-runtime "^6.22.0"
+ core-js "^2.4.0"
+ home-or-tmp "^2.0.0"
+ lodash "^4.2.0"
+ mkdirp "^0.5.1"
+ source-map-support "^0.4.2"
+
+babel-runtime@^6.18.0, babel-runtime@^6.22.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.23.0.tgz#0a9489f144de70efb3ce4300accdb329e2fc543b"
+ dependencies:
+ core-js "^2.4.0"
+ regenerator-runtime "^0.10.0"
+
+babel-template@^6.24.1, babel-template@^6.25.0:
+ version "6.25.0"
+ resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.25.0.tgz#665241166b7c2aa4c619d71e192969552b10c071"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-traverse "^6.25.0"
+ babel-types "^6.25.0"
+ babylon "^6.17.2"
+ lodash "^4.2.0"
+
+babel-traverse@^6.24.1, babel-traverse@^6.25.0:
+ version "6.25.0"
+ resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.25.0.tgz#2257497e2fcd19b89edc13c4c91381f9512496f1"
+ dependencies:
+ babel-code-frame "^6.22.0"
+ babel-messages "^6.23.0"
+ babel-runtime "^6.22.0"
+ babel-types "^6.25.0"
+ babylon "^6.17.2"
+ debug "^2.2.0"
+ globals "^9.0.0"
+ invariant "^2.2.0"
+ lodash "^4.2.0"
+
+babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.25.0:
+ version "6.25.0"
+ resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.25.0.tgz#70afb248d5660e5d18f811d91c8303b54134a18e"
+ dependencies:
+ babel-runtime "^6.22.0"
+ esutils "^2.0.2"
+ lodash "^4.2.0"
+ to-fast-properties "^1.0.1"
+
+babylon@^6.17.2:
+ version "6.17.4"
+ resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.17.4.tgz#3e8b7402b88d22c3423e137a1577883b15ff869a"
+
+balanced-match@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
+
+brace-expansion@^1.1.7:
+ version "1.1.8"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292"
+ dependencies:
+ balanced-match "^1.0.0"
+ concat-map "0.0.1"
+
+braces@^1.8.2:
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7"
+ dependencies:
+ expand-range "^1.8.1"
+ preserve "^0.2.0"
+ repeat-element "^1.1.2"
+
+browserslist@^2.1.2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.2.2.tgz#e9b4618b8a01c193f9786beea09f6fd10dbe31c3"
+ dependencies:
+ caniuse-lite "^1.0.30000704"
+ electron-to-chromium "^1.3.16"
+
+builtin-modules@^1.0.0, builtin-modules@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
+
+builtin-modules@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-2.0.0.tgz#60b7ef5ae6546bd7deefa74b08b62a43a232648e"
+
+caller-path@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f"
+ dependencies:
+ callsites "^0.2.0"
+
+callsites@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca"
+
+caniuse-lite@^1.0.30000704:
+ version "1.0.30000706"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000706.tgz#bc59abc41ba7d4a3634dda95befded6114e1f24e"
+
+chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
+ dependencies:
+ ansi-styles "^2.2.1"
+ escape-string-regexp "^1.0.2"
+ has-ansi "^2.0.0"
+ strip-ansi "^3.0.0"
+ supports-color "^2.0.0"
+
+chalk@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.0.1.tgz#dbec49436d2ae15f536114e76d14656cdbc0f44d"
+ dependencies:
+ ansi-styles "^3.1.0"
+ escape-string-regexp "^1.0.5"
+ supports-color "^4.0.0"
+
+circular-json@^0.3.1:
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66"
+
+cli-cursor@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5"
+ dependencies:
+ restore-cursor "^2.0.0"
+
+cli-width@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a"
+
+co@^4.6.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
+
+color-convert@^1.9.0:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a"
+ dependencies:
+ color-name "^1.1.1"
+
+color-name@^1.1.1:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+
+commander@~2.13.0:
+ version "2.13.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c"
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+
+concat-stream@^1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7"
+ dependencies:
+ inherits "^2.0.3"
+ readable-stream "^2.2.2"
+ typedarray "^0.0.6"
+
+contains-path@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a"
+
+convert-source-map@^1.1.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5"
+
+core-js@^2.4.0:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e"
+
+core-util-is@~1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
+
+cross-spawn@^5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
+ dependencies:
+ lru-cache "^4.0.1"
+ shebang-command "^1.2.0"
+ which "^1.2.9"
+
+debug@^2.1.1, debug@^2.2.0, debug@^2.6.8:
+ version "2.6.8"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc"
+ dependencies:
+ ms "2.0.0"
+
+deep-is@~0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
+
+del@^2.0.2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8"
+ dependencies:
+ globby "^5.0.0"
+ is-path-cwd "^1.0.0"
+ is-path-in-cwd "^1.0.0"
+ object-assign "^4.0.1"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+ rimraf "^2.2.8"
+
+detect-indent@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208"
+ dependencies:
+ repeating "^2.0.0"
+
+doctrine@1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa"
+ dependencies:
+ esutils "^2.0.2"
+ isarray "^1.0.0"
+
+doctrine@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.0.0.tgz#c73d8d2909d22291e1a007a395804da8b665fe63"
+ dependencies:
+ esutils "^2.0.2"
+ isarray "^1.0.0"
+
+electron-to-chromium@^1.3.16:
+ version "1.3.16"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.16.tgz#d0e026735754770901ae301a21664cba45d92f7d"
+
+error-ex@^1.2.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc"
+ dependencies:
+ is-arrayish "^0.2.1"
+
+escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+
+eslint-import-resolver-node@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.1.tgz#4422574cde66a9a7b099938ee4d508a199e0e3cc"
+ dependencies:
+ debug "^2.6.8"
+ resolve "^1.2.0"
+
+eslint-module-utils@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.1.1.tgz#abaec824177613b8a95b299639e1b6facf473449"
+ dependencies:
+ debug "^2.6.8"
+ pkg-dir "^1.0.0"
+
+eslint-plugin-import@^2.7.0:
+ version "2.7.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.7.0.tgz#21de33380b9efb55f5ef6d2e210ec0e07e7fa69f"
+ dependencies:
+ builtin-modules "^1.1.1"
+ contains-path "^0.1.0"
+ debug "^2.6.8"
+ doctrine "1.5.0"
+ eslint-import-resolver-node "^0.3.1"
+ eslint-module-utils "^2.1.1"
+ has "^1.0.1"
+ lodash.cond "^4.3.0"
+ minimatch "^3.0.3"
+ read-pkg-up "^2.0.0"
+
+eslint-scope@^3.7.1:
+ version "3.7.1"
+ resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8"
+ dependencies:
+ esrecurse "^4.1.0"
+ estraverse "^4.1.1"
+
+eslint@^4.3.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.3.0.tgz#fcd7c96376bbf34c85ee67ed0012a299642b108f"
+ dependencies:
+ ajv "^5.2.0"
+ babel-code-frame "^6.22.0"
+ chalk "^1.1.3"
+ concat-stream "^1.6.0"
+ cross-spawn "^5.1.0"
+ debug "^2.6.8"
+ doctrine "^2.0.0"
+ eslint-scope "^3.7.1"
+ espree "^3.4.3"
+ esquery "^1.0.0"
+ estraverse "^4.2.0"
+ esutils "^2.0.2"
+ file-entry-cache "^2.0.0"
+ functional-red-black-tree "^1.0.1"
+ glob "^7.1.2"
+ globals "^9.17.0"
+ ignore "^3.3.3"
+ imurmurhash "^0.1.4"
+ inquirer "^3.0.6"
+ is-resolvable "^1.0.0"
+ js-yaml "^3.8.4"
+ json-stable-stringify "^1.0.1"
+ levn "^0.3.0"
+ lodash "^4.17.4"
+ minimatch "^3.0.2"
+ mkdirp "^0.5.1"
+ natural-compare "^1.4.0"
+ optionator "^0.8.2"
+ path-is-inside "^1.0.2"
+ pluralize "^4.0.0"
+ progress "^2.0.0"
+ require-uncached "^1.0.3"
+ semver "^5.3.0"
+ strip-json-comments "~2.0.1"
+ table "^4.0.1"
+ text-table "~0.2.0"
+
+espree@^3.4.3:
+ version "3.4.3"
+ resolved "https://registry.yarnpkg.com/espree/-/espree-3.4.3.tgz#2910b5ccd49ce893c2ffffaab4fd8b3a31b82374"
+ dependencies:
+ acorn "^5.0.1"
+ acorn-jsx "^3.0.0"
+
+esprima@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804"
+
+esquery@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.0.tgz#cfba8b57d7fba93f17298a8a006a04cda13d80fa"
+ dependencies:
+ estraverse "^4.0.0"
+
+esrecurse@^4.1.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.0.tgz#fa9568d98d3823f9a41d91e902dcab9ea6e5b163"
+ dependencies:
+ estraverse "^4.1.0"
+ object-assign "^4.0.1"
+
+estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
+
+estree-walker@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.2.1.tgz#bdafe8095383d8414d5dc2ecf4c9173b6db9412e"
+
+estree-walker@^0.3.0:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.3.1.tgz#e6b1a51cf7292524e7237c312e5fe6660c1ce1aa"
+
+estree-walker@^0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.5.1.tgz#64fc375053abc6f57d73e9bd2f004644ad3c5854"
+
+esutils@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
+
+expand-brackets@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b"
+ dependencies:
+ is-posix-bracket "^0.1.0"
+
+expand-range@^1.8.1:
+ version "1.8.2"
+ resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337"
+ dependencies:
+ fill-range "^2.1.0"
+
+external-editor@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.0.4.tgz#1ed9199da9cbfe2ef2f7a31b2fde8b0d12368972"
+ dependencies:
+ iconv-lite "^0.4.17"
+ jschardet "^1.4.2"
+ tmp "^0.0.31"
+
+extglob@^0.3.1:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1"
+ dependencies:
+ is-extglob "^1.0.0"
+
+fast-deep-equal@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
+
+fast-levenshtein@~2.0.4:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
+
+figures@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962"
+ dependencies:
+ escape-string-regexp "^1.0.5"
+
+file-entry-cache@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361"
+ dependencies:
+ flat-cache "^1.2.1"
+ object-assign "^4.0.1"
+
+filename-regex@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26"
+
+fill-range@^2.1.0:
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723"
+ dependencies:
+ is-number "^2.1.0"
+ isobject "^2.0.0"
+ randomatic "^1.1.3"
+ repeat-element "^1.1.2"
+ repeat-string "^1.5.2"
+
+find-up@^1.0.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f"
+ dependencies:
+ path-exists "^2.0.0"
+ pinkie-promise "^2.0.0"
+
+find-up@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
+ dependencies:
+ locate-path "^2.0.0"
+
+flat-cache@^1.2.1:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.2.2.tgz#fa86714e72c21db88601761ecf2f555d1abc6b96"
+ dependencies:
+ circular-json "^0.3.1"
+ del "^2.0.2"
+ graceful-fs "^4.1.2"
+ write "^0.2.1"
+
+for-in@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
+
+for-own@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce"
+ dependencies:
+ for-in "^1.0.1"
+
+fs.realpath@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+
+function-bind@^1.0.2:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.0.tgz#16176714c801798e4e8f2cf7f7529467bb4a5771"
+
+functional-red-black-tree@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
+
+glob-base@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
+ dependencies:
+ glob-parent "^2.0.0"
+ is-glob "^2.0.0"
+
+glob-parent@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28"
+ dependencies:
+ is-glob "^2.0.0"
+
+glob@^7.0.3, glob@^7.0.5, glob@^7.1.2:
+ version "7.1.2"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.0.4"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+globals@^9.0.0, globals@^9.17.0:
+ version "9.18.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
+
+globby@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d"
+ dependencies:
+ array-union "^1.0.1"
+ arrify "^1.0.0"
+ glob "^7.0.3"
+ object-assign "^4.0.1"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+
+graceful-fs@^4.1.2:
+ version "4.1.11"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
+
+has-ansi@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
+ dependencies:
+ ansi-regex "^2.0.0"
+
+has-flag@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51"
+
+has@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28"
+ dependencies:
+ function-bind "^1.0.2"
+
+home-or-tmp@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"
+ dependencies:
+ os-homedir "^1.0.0"
+ os-tmpdir "^1.0.1"
+
+hosted-git-info@^2.1.4:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c"
+
+iconv-lite@^0.4.17:
+ version "0.4.18"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.18.tgz#23d8656b16aae6742ac29732ea8f0336a4789cf2"
+
+ignore@^3.3.3:
+ version "3.3.3"
+ resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.3.tgz#432352e57accd87ab3110e82d3fea0e47812156d"
+
+imurmurhash@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
+
+inflight@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+ dependencies:
+ once "^1.3.0"
+ wrappy "1"
+
+inherits@2, inherits@^2.0.3, inherits@~2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
+
+inquirer@^3.0.6:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.2.1.tgz#06ceb0f540f45ca548c17d6840959878265fa175"
+ dependencies:
+ ansi-escapes "^2.0.0"
+ chalk "^2.0.0"
+ cli-cursor "^2.1.0"
+ cli-width "^2.0.0"
+ external-editor "^2.0.4"
+ figures "^2.0.0"
+ lodash "^4.3.0"
+ mute-stream "0.0.7"
+ run-async "^2.2.0"
+ rx-lite "^4.0.8"
+ rx-lite-aggregates "^4.0.8"
+ string-width "^2.1.0"
+ strip-ansi "^4.0.0"
+ through "^2.3.6"
+
+invariant@^2.2.0, invariant@^2.2.2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360"
+ dependencies:
+ loose-envify "^1.0.0"
+
+is-arrayish@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
+
+is-buffer@^1.1.5:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc"
+
+is-builtin-module@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe"
+ dependencies:
+ builtin-modules "^1.0.0"
+
+is-dotfile@^1.0.0:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1"
+
+is-equal-shallow@^0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534"
+ dependencies:
+ is-primitive "^2.0.0"
+
+is-extendable@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
+
+is-extglob@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0"
+
+is-finite@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa"
+ dependencies:
+ number-is-nan "^1.0.0"
+
+is-fullwidth-code-point@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
+
+is-glob@^2.0.0, is-glob@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863"
+ dependencies:
+ is-extglob "^1.0.0"
+
+is-module@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591"
+
+is-number@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f"
+ dependencies:
+ kind-of "^3.0.2"
+
+is-number@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
+ dependencies:
+ kind-of "^3.0.2"
+
+is-path-cwd@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d"
+
+is-path-in-cwd@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc"
+ dependencies:
+ is-path-inside "^1.0.0"
+
+is-path-inside@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.0.tgz#fc06e5a1683fbda13de667aff717bbc10a48f37f"
+ dependencies:
+ path-is-inside "^1.0.1"
+
+is-posix-bracket@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4"
+
+is-primitive@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575"
+
+is-promise@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa"
+
+is-resolvable@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.0.0.tgz#8df57c61ea2e3c501408d100fb013cf8d6e0cc62"
+ dependencies:
+ tryit "^1.0.1"
+
+isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+
+isexe@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+
+isobject@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
+ dependencies:
+ isarray "1.0.0"
+
+js-tokens@^3.0.0:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
+
+js-yaml@^3.8.4:
+ version "3.9.0"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.9.0.tgz#4ffbbf25c2ac963b8299dc74da7e3740de1c18ce"
+ dependencies:
+ argparse "^1.0.7"
+ esprima "^4.0.0"
+
+jschardet@^1.4.2:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-1.5.0.tgz#a61f310306a5a71188e1b1acd08add3cfbb08b1e"
+
+jsesc@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b"
+
+jsesc@~0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
+
+json-schema-traverse@^0.3.0:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340"
+
+json-stable-stringify@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af"
+ dependencies:
+ jsonify "~0.0.0"
+
+json5@^0.5.0:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
+
+jsonify@~0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
+
+kind-of@^3.0.2:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
+ dependencies:
+ is-buffer "^1.1.5"
+
+kind-of@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57"
+ dependencies:
+ is-buffer "^1.1.5"
+
+levn@^0.3.0, levn@~0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
+ dependencies:
+ prelude-ls "~1.1.2"
+ type-check "~0.3.2"
+
+load-json-file@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8"
+ dependencies:
+ graceful-fs "^4.1.2"
+ parse-json "^2.2.0"
+ pify "^2.0.0"
+ strip-bom "^3.0.0"
+
+locate-path@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
+ dependencies:
+ p-locate "^2.0.0"
+ path-exists "^3.0.0"
+
+lodash.cond@^4.3.0:
+ version "4.5.2"
+ resolved "https://registry.yarnpkg.com/lodash.cond/-/lodash.cond-4.5.2.tgz#f471a1da486be60f6ab955d17115523dd1d255d5"
+
+lodash@^4.0.0, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0:
+ version "4.17.4"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
+
+loose-envify@^1.0.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848"
+ dependencies:
+ js-tokens "^3.0.0"
+
+lru-cache@^4.0.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55"
+ dependencies:
+ pseudomap "^1.0.2"
+ yallist "^2.1.2"
+
+magic-string@^0.22.4:
+ version "0.22.5"
+ resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.22.5.tgz#8e9cf5afddf44385c1da5bc2a6a0dbd10b03657e"
+ dependencies:
+ vlq "^0.2.2"
+
+micromatch@^2.3.11:
+ version "2.3.11"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565"
+ dependencies:
+ arr-diff "^2.0.0"
+ array-unique "^0.2.1"
+ braces "^1.8.2"
+ expand-brackets "^0.1.4"
+ extglob "^0.3.1"
+ filename-regex "^2.0.0"
+ is-extglob "^1.0.0"
+ is-glob "^2.0.1"
+ kind-of "^3.0.2"
+ normalize-path "^2.0.1"
+ object.omit "^2.0.0"
+ parse-glob "^3.0.4"
+ regex-cache "^0.4.2"
+
+mimic-fn@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18"
+
+minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
+ dependencies:
+ brace-expansion "^1.1.7"
+
+minimist@0.0.8:
+ version "0.0.8"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
+
+mkdirp@^0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
+ dependencies:
+ minimist "0.0.8"
+
+ms@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+
+mute-stream@0.0.7:
+ version "0.0.7"
+ resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
+
+natural-compare@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
+
+normalize-package-data@^2.3.2:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f"
+ dependencies:
+ hosted-git-info "^2.1.4"
+ is-builtin-module "^1.0.0"
+ semver "2 || 3 || 4 || 5"
+ validate-npm-package-license "^3.0.1"
+
+normalize-path@^2.0.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
+ dependencies:
+ remove-trailing-separator "^1.0.1"
+
+number-is-nan@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
+
+object-assign@^4.0.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+
+object.omit@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa"
+ dependencies:
+ for-own "^0.1.4"
+ is-extendable "^0.1.1"
+
+once@^1.3.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+ dependencies:
+ wrappy "1"
+
+onetime@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
+ dependencies:
+ mimic-fn "^1.0.0"
+
+optionator@^0.8.2:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64"
+ dependencies:
+ deep-is "~0.1.3"
+ fast-levenshtein "~2.0.4"
+ levn "~0.3.0"
+ prelude-ls "~1.1.2"
+ type-check "~0.3.2"
+ wordwrap "~1.0.0"
+
+os-homedir@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
+
+os-tmpdir@^1.0.1, os-tmpdir@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
+
+p-limit@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.1.0.tgz#b07ff2d9a5d88bec806035895a2bab66a27988bc"
+
+p-locate@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
+ dependencies:
+ p-limit "^1.1.0"
+
+parse-glob@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c"
+ dependencies:
+ glob-base "^0.3.0"
+ is-dotfile "^1.0.0"
+ is-extglob "^1.0.0"
+ is-glob "^2.0.0"
+
+parse-json@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
+ dependencies:
+ error-ex "^1.2.0"
+
+path-exists@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b"
+ dependencies:
+ pinkie-promise "^2.0.0"
+
+path-exists@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
+
+path-is-absolute@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+
+path-is-inside@^1.0.1, path-is-inside@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
+
+path-parse@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1"
+
+path-type@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73"
+ dependencies:
+ pify "^2.0.0"
+
+pify@^2.0.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
+
+pinkie-promise@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
+ dependencies:
+ pinkie "^2.0.0"
+
+pinkie@^2.0.0:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
+
+pkg-dir@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-1.0.0.tgz#7a4b508a8d5bb2d629d447056ff4e9c9314cf3d4"
+ dependencies:
+ find-up "^1.0.0"
+
+pluralize@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-4.0.0.tgz#59b708c1c0190a2f692f1c7618c446b052fd1762"
+
+prelude-ls@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
+
+preserve@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
+
+private@^0.1.6:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1"
+
+process-nextick-args@~1.0.6:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3"
+
+progress@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f"
+
+pseudomap@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
+
+randomatic@^1.1.3:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c"
+ dependencies:
+ is-number "^3.0.0"
+ kind-of "^4.0.0"
+
+read-pkg-up@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be"
+ dependencies:
+ find-up "^2.0.0"
+ read-pkg "^2.0.0"
+
+read-pkg@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8"
+ dependencies:
+ load-json-file "^2.0.0"
+ normalize-package-data "^2.3.2"
+ path-type "^2.0.0"
+
+readable-stream@^2.2.2:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c"
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.3"
+ isarray "~1.0.0"
+ process-nextick-args "~1.0.6"
+ safe-buffer "~5.1.1"
+ string_decoder "~1.0.3"
+ util-deprecate "~1.0.1"
+
+regenerate@^1.2.1:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.2.tgz#d1941c67bad437e1be76433add5b385f95b19260"
+
+regenerator-runtime@^0.10.0:
+ version "0.10.5"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz#336c3efc1220adcedda2c9fab67b5a7955a33658"
+
+regenerator-transform@0.9.11:
+ version "0.9.11"
+ resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.9.11.tgz#3a7d067520cb7b7176769eb5ff868691befe1283"
+ dependencies:
+ babel-runtime "^6.18.0"
+ babel-types "^6.19.0"
+ private "^0.1.6"
+
+regex-cache@^0.4.2:
+ version "0.4.3"
+ resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.3.tgz#9b1a6c35d4d0dfcef5711ae651e8e9d3d7114145"
+ dependencies:
+ is-equal-shallow "^0.1.3"
+ is-primitive "^2.0.0"
+
+regexpu-core@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240"
+ dependencies:
+ regenerate "^1.2.1"
+ regjsgen "^0.2.0"
+ regjsparser "^0.1.4"
+
+regjsgen@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7"
+
+regjsparser@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c"
+ dependencies:
+ jsesc "~0.5.0"
+
+remove-trailing-separator@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.0.2.tgz#69b062d978727ad14dc6b56ba4ab772fd8d70511"
+
+repeat-element@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a"
+
+repeat-string@^1.5.2:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
+
+repeating@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda"
+ dependencies:
+ is-finite "^1.0.0"
+
+require-uncached@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3"
+ dependencies:
+ caller-path "^0.1.0"
+ resolve-from "^1.0.0"
+
+resolve-from@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226"
+
+resolve@^1.1.6, resolve@^1.5.0:
+ version "1.7.1"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.7.1.tgz#aadd656374fd298aee895bc026b8297418677fd3"
+ dependencies:
+ path-parse "^1.0.5"
+
+resolve@^1.2.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.4.0.tgz#a75be01c53da25d934a98ebd0e4c4a7312f92a86"
+ dependencies:
+ path-parse "^1.0.5"
+
+restore-cursor@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"
+ dependencies:
+ onetime "^2.0.0"
+ signal-exit "^3.0.2"
+
+rimraf@^2.2.8:
+ version "2.6.1"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d"
+ dependencies:
+ glob "^7.0.5"
+
+rollup-plugin-babel@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/rollup-plugin-babel/-/rollup-plugin-babel-3.0.4.tgz#41b3e762fe64450dd61da3105a2cf7ad76be4edc"
+ dependencies:
+ rollup-pluginutils "^1.5.0"
+
+rollup-plugin-commonjs@^9.1.0:
+ version "9.1.0"
+ resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-9.1.0.tgz#468341aab32499123ee9a04b22f51d9bf26fdd94"
+ dependencies:
+ estree-walker "^0.5.1"
+ magic-string "^0.22.4"
+ resolve "^1.5.0"
+ rollup-pluginutils "^2.0.1"
+
+rollup-plugin-node-resolve@^3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-3.3.0.tgz#c26d110a36812cbefa7ce117cadcd3439aa1c713"
+ dependencies:
+ builtin-modules "^2.0.0"
+ is-module "^1.0.0"
+ resolve "^1.1.6"
+
+rollup-plugin-uglify@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/rollup-plugin-uglify/-/rollup-plugin-uglify-3.0.0.tgz#a34eca24617709c6bf1778e9653baafa06099b86"
+ dependencies:
+ uglify-es "^3.3.7"
+
+rollup-pluginutils@^1.5.0:
+ version "1.5.2"
+ resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-1.5.2.tgz#1e156e778f94b7255bfa1b3d0178be8f5c552408"
+ dependencies:
+ estree-walker "^0.2.1"
+ minimatch "^3.0.2"
+
+rollup-pluginutils@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.0.1.tgz#7ec95b3573f6543a46a6461bd9a7c544525d0fc0"
+ dependencies:
+ estree-walker "^0.3.0"
+ micromatch "^2.3.11"
+
+rollup@^0.58.2:
+ version "0.58.2"
+ resolved "https://registry.yarnpkg.com/rollup/-/rollup-0.58.2.tgz#2feddea8c0c022f3e74b35c48e3c21b3433803ce"
+ dependencies:
+ "@types/estree" "0.0.38"
+ "@types/node" "*"
+
+run-async@^2.2.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"
+ dependencies:
+ is-promise "^2.1.0"
+
+rx-lite-aggregates@^4.0.8:
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz#753b87a89a11c95467c4ac1626c4efc4e05c67be"
+ dependencies:
+ rx-lite "*"
+
+rx-lite@*, rx-lite@^4.0.8:
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444"
+
+safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
+
+"semver@2 || 3 || 4 || 5", semver@^5.3.0:
+ version "5.4.1"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e"
+
+shebang-command@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
+ dependencies:
+ shebang-regex "^1.0.0"
+
+shebang-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
+
+signal-exit@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
+
+slash@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
+
+slice-ansi@0.0.4:
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35"
+
+source-map-support@^0.4.2:
+ version "0.4.15"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.15.tgz#03202df65c06d2bd8c7ec2362a193056fef8d3b1"
+ dependencies:
+ source-map "^0.5.6"
+
+source-map@^0.5.0, source-map@^0.5.6:
+ version "0.5.6"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
+
+source-map@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+
+spark-md5@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/spark-md5/-/spark-md5-3.0.0.tgz#3722227c54e2faf24b1dc6d933cc144e6f71bfef"
+
+spdx-correct@~1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40"
+ dependencies:
+ spdx-license-ids "^1.0.2"
+
+spdx-expression-parse@~1.0.0:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c"
+
+spdx-license-ids@^1.0.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57"
+
+sprintf-js@~1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
+
+string-width@^2.0.0, string-width@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
+ dependencies:
+ is-fullwidth-code-point "^2.0.0"
+ strip-ansi "^4.0.0"
+
+string_decoder@~1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab"
+ dependencies:
+ safe-buffer "~5.1.0"
+
+strip-ansi@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
+ dependencies:
+ ansi-regex "^2.0.0"
+
+strip-ansi@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
+ dependencies:
+ ansi-regex "^3.0.0"
+
+strip-bom@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
+
+strip-json-comments@~2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
+
+supports-color@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
+
+supports-color@^4.0.0:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.2.1.tgz#65a4bb2631e90e02420dba5554c375a4754bb836"
+ dependencies:
+ has-flag "^2.0.0"
+
+table@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/table/-/table-4.0.1.tgz#a8116c133fac2c61f4a420ab6cdf5c4d61f0e435"
+ dependencies:
+ ajv "^4.7.0"
+ ajv-keywords "^1.0.0"
+ chalk "^1.1.1"
+ lodash "^4.0.0"
+ slice-ansi "0.0.4"
+ string-width "^2.0.0"
+
+text-table@~0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
+
+through@^2.3.6:
+ version "2.3.8"
+ resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
+
+tmp@^0.0.31:
+ version "0.0.31"
+ resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7"
+ dependencies:
+ os-tmpdir "~1.0.1"
+
+to-fast-properties@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47"
+
+trim-right@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
+
+tryit@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb"
+
+type-check@~0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
+ dependencies:
+ prelude-ls "~1.1.2"
+
+typedarray@^0.0.6:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
+
+uglify-es@^3.3.7:
+ version "3.3.9"
+ resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677"
+ dependencies:
+ commander "~2.13.0"
+ source-map "~0.6.1"
+
+util-deprecate@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+
+validate-npm-package-license@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc"
+ dependencies:
+ spdx-correct "~1.0.0"
+ spdx-expression-parse "~1.0.0"
+
+vlq@^0.2.2:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26"
+
+which@^1.2.9:
+ version "1.2.14"
+ resolved "https://registry.yarnpkg.com/which/-/which-1.2.14.tgz#9a87c4378f03e827cecaf1acdf56c736c01c14e5"
+ dependencies:
+ isexe "^2.0.0"
+
+wordwrap@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
+
+wrappy@1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+
+write@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757"
+ dependencies:
+ mkdirp "^0.5.1"
+
+yallist@^2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
diff --git a/activesupport/.gitignore b/activesupport/.gitignore
new file mode 100644
index 0000000000..8cde8514fd
--- /dev/null
+++ b/activesupport/.gitignore
@@ -0,0 +1 @@
+/test/fixtures/isolation_test/
diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md
index 334eebc268..98d232da33 100644
--- a/activesupport/CHANGELOG.md
+++ b/activesupport/CHANGELOG.md
@@ -1,306 +1,292 @@
-* Fix not calling `#default` on `HashWithIndifferentAccess#to_hash` when only
- `default_proc` is set, which could raise.
+* Fix bug where `#to_options` for `ActiveSupport::HashWithIndifferentAccess`
+ would not act as alias for `#symbolize_keys`.
- *Simon Eskildsen*
+ *Nick Weiland*
-* Fix setting `default_proc` on `HashWithIndifferentAccess#dup`
+* Improve the logic that detects non-autoloaded constants.
- *Simon Eskildsen*
+ *Jan Habermann*, *Xavier Noria*
-* Fix a range of values for parameters of the Time#change
+* Deprecate `ActiveSupport::Multibyte::Unicode#pack_graphemes(array)` and `ActiveSuppport::Multibyte::Unicode#unpack_graphemes(string)`
+ in favor of `array.flatten.pack("U*")` and `string.scan(/\X/).map(&:codepoints)`, respectively.
- *Nikolay Kondratyev*
+ *Francesco Rodríguez*
-* Add `Enumerable#pluck` to get the same values from arrays as from ActiveRecord
- associations.
+* Deprecate `ActiveSupport::Multibyte::Chars.consumes?` in favor of `String#is_utf8?`.
- Fixes #20339.
+ *Francesco Rodríguez*
- *Kevin Deisz*
+* Fix duration being rounded to a full second.
+ ```
+ time = DateTime.parse("2018-1-1")
+ time += 0.51.seconds
+ ```
+ Will now correctly add 0.51 second and not 1 full second.
-* Add a bang version to `ActiveSupport::OrderedOptions` get methods which will raise
- an `KeyError` if the value is `.blank?`
+ *Edouard Chin*
- Before:
+* Deprecate `ActiveSupport::Multibyte::Unicode#normalize` and `ActiveSuppport::Multibyte::Chars#normalize`
+ in favor of `String#unicode_normalize`
- if (slack_url = Rails.application.secrets.slack_url).present?
- # Do something worthwhile
- else
- # Raise as important secret password is not specified
- end
-
- After:
-
- slack_url = Rails.application.secrets.slack_url!
+ *Francesco Rodríguez*
- *Aditya Sanghi*, *Gaurish Sharma*
+* Deprecate `ActiveSupport::Multibyte::Unicode#downcase/upcase/swapcase` in favor of
+ `String#downcase/upcase/swapcase`.
-* Remove deprecated `Class#superclass_delegating_accessor`.
- Use `Class#class_attribute` instead.
+ *Francesco Rodríguez*
- *Akshay Vishnoi*
+* Add `ActiveSupport::ParameterFilter`.
-* Patch `Delegator` to work with `#try`.
+ *Yoshiyuki Kinjo*
- Fixes #5790.
+* Rename `Module#parent`, `Module#parents`, and `Module#parent_name` to
+ `module_parent`, `module_parents`, and `module_parent_name`.
- *Nate Smith*
+ *Gannon McGibbon*
-* Add `Integer#positive?` and `Integer#negative?` query methods
- in the vein of `Fixnum#zero?`.
+* Deprecate the use of `LoggerSilence` in favor of `ActiveSupport::LoggerSilence`
- This makes it nicer to do things like `bunch_of_numbers.select(&:positive?)`.
+ *Edouard Chin*
- *DHH*
+* Deprecate using negative limits in `String#first` and `String#last`.
-* Encoding `ActiveSupport::TimeWithZone` to YAML now preserves the timezone information.
+ *Gannon McGibbon*, *Eric Turner*
- Fixes #9183.
+* Fix bug where `#without` for `ActiveSupport::HashWithIndifferentAccess` would fail
+ with symbol arguments
- *Andrew White*
+ *Abraham Chan*
-* Added `ActiveSupport::TimeZone#strptime` to allow parsing times as if
- from a given timezone.
+* Treat `#delete_prefix`, `#delete_suffix` and `#unicode_normalize` results as non-`html_safe`.
+ Ensure safety of arguments for `#insert`, `#[]=` and `#replace` calls on `html_safe` Strings.
- *Paul A Jungwirth*
+ *Janosch Müller*
-* `ActiveSupport::Callbacks#skip_callback` now raises an `ArgumentError` if
- an unrecognized callback is removed.
+* Changed `ActiveSupport::TaggedLogging.new` to return a new logger instance instead
+ of mutating the one received as parameter.
- *Iain Beeston*
+ *Thierry Joyal*
-* Added `ActiveSupport::ArrayInquirer` and `Array#inquiry`.
+* Define `unfreeze_time` as an alias of `travel_back` in `ActiveSupport::Testing::TimeHelpers`.
- Wrapping an array in an `ArrayInquirer` gives a friendlier way to check its
- contents:
+ The alias is provided for symmetry with `freeze_time`.
- variants = ActiveSupport::ArrayInquirer.new([:phone, :tablet])
+ *Ryan Davidson*
- variants.phone? # => true
- variants.tablet? # => true
- variants.desktop? # => false
+* Add support for tracing constant autoloads. Just throw
- variants.any?(:phone, :tablet) # => true
- variants.any?(:phone, :desktop) # => true
- variants.any?(:desktop, :watch) # => false
+ ActiveSupport::Dependencies.logger = Rails.logger
+ ActiveSupport::Dependencies.verbose = true
- `Array#inquiry` is a shortcut for wrapping the receiving array in an
- `ArrayInquirer`.
+ in an initializer.
- *George Claghorn*
+ *Xavier Noria*
-* Deprecate `alias_method_chain` in favour of `Module#prepend` introduced in
- Ruby 2.0.
+* Maintain `html_safe?` on html_safe strings when sliced.
- *Kir Shatrov*
+ string = "<div>test</div>".html_safe
+ string[-1..1].html_safe? # => true
-* Added `#without` on `Enumerable` and `Array` to return a copy of an
- enumerable without the specified elements.
+ *Elom Gomez*, *Yumin Wong*
- *Todd Bealmear*
+* Add `Array#extract!`.
-* Fixed a problem where `String#truncate_words` would get stuck with a complex
- string.
+ The method removes and returns the elements for which the block returns a true value.
+ If no block is given, an Enumerator is returned instead.
- *Henrik Nygren*
+ numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
+ odd_numbers = numbers.extract! { |number| number.odd? } # => [1, 3, 5, 7, 9]
+ numbers # => [0, 2, 4, 6, 8]
-* Fixed a roundtrip problem with `AS::SafeBuffer` where primitive-like strings
- will be dumped as primitives:
+ *bogdanvlviv*
- Before:
+* Support not to cache `nil` for `ActiveSupport::Cache#fetch`.
- YAML.load ActiveSupport::SafeBuffer.new("Hello").to_yaml # => "Hello"
- YAML.load ActiveSupport::SafeBuffer.new("true").to_yaml # => true
- YAML.load ActiveSupport::SafeBuffer.new("false").to_yaml # => false
- YAML.load ActiveSupport::SafeBuffer.new("1").to_yaml # => 1
- YAML.load ActiveSupport::SafeBuffer.new("1.1").to_yaml # => 1.1
+ cache.fetch('bar', skip_nil: true) { nil }
+ cache.exist?('bar') # => false
- After:
+ *Martin Hong*
- YAML.load ActiveSupport::SafeBuffer.new("Hello").to_yaml # => "Hello"
- YAML.load ActiveSupport::SafeBuffer.new("true").to_yaml # => "true"
- YAML.load ActiveSupport::SafeBuffer.new("false").to_yaml # => "false"
- YAML.load ActiveSupport::SafeBuffer.new("1").to_yaml # => "1"
- YAML.load ActiveSupport::SafeBuffer.new("1.1").to_yaml # => "1.1"
-
- *Godfrey Chan*
+* Add "event object" support to the notification system.
+ Before this change, end users were forced to create hand made artisanal
+ event objects on their own, like this:
-* Enable `number_to_percentage` to keep the number's precision by allowing
- `:precision` to be `nil`.
-
- *Jack Xu*
-
-* `config_accessor` became a private method, as with Ruby's `attr_accessor`.
-
- *Akira Matsuda*
+ ActiveSupport::Notifications.subscribe('wait') do |*args|
+ @event = ActiveSupport::Notifications::Event.new(*args)
+ end
-* `AS::Testing::TimeHelpers#travel_to` now changes `DateTime.now` as well as
- `Time.now` and `Date.today`.
+ ActiveSupport::Notifications.instrument('wait') do
+ sleep 1
+ end
- *Yuki Nishijima*
+ @event.duration # => 1000.138
-* Add `file_fixture` to `ActiveSupport::TestCase`.
- It provides a simple mechanism to access sample files in your test cases.
+ After this change, if the block passed to `subscribe` only takes one
+ parameter, the framework will yield an event object to the block. Now
+ end users are no longer required to make their own:
- By default file fixtures are stored in `test/fixtures/files`. This can be
- configured per test-case using the `file_fixture_path` class attribute.
+ ActiveSupport::Notifications.subscribe('wait') do |event|
+ @event = event
+ end
- *Yves Senn*
+ ActiveSupport::Notifications.instrument('wait') do
+ sleep 1
+ end
-* Return value of yielded block in `File.atomic_write`.
+ p @event.allocations # => 7
+ p @event.cpu_time # => 0.256
+ p @event.idle_time # => 1003.2399
- *Ian Ker-Seymer*
+ Now you can enjoy event objects without making them yourself. Neat!
-* Duplicate frozen array when assigning it to a `HashWithIndifferentAccess` so
- that it doesn't raise a `RuntimeError` when calling `map!` on it in `convert_value`.
+ *Aaron "t.lo" Patterson*
- Fixes #18550.
+* Add cpu_time, idle_time, and allocations to Event.
- *Aditya Kapoor*
+ *Eileen M. Uchitelle*, *Aaron Patterson*
-* Add missing time zone definitions for Russian Federation and sync them
- with `zone.tab` file from tzdata version 2014j (latest).
+* RedisCacheStore: support key expiry in increment/decrement.
- *Andrey Novikov*
+ Pass `:expires_in` to `#increment` and `#decrement` to set a Redis EXPIRE on the key.
-* Add `SecureRandom.base58` for generation of random base58 strings.
+ If the key is already set to expire, RedisCacheStore won't extend its expiry.
- *Matthew Draper*, *Guillermo Iguaran*
+ Rails.cache.increment("some_key", 1, expires_in: 2.minutes)
-* Add `#prev_day` and `#next_day` counterparts to `#yesterday` and
- `#tomorrow` for `Date`, `Time`, and `DateTime`.
+ *Jason Lee*
- *George Claghorn*
+* Allow `Range#===` and `Range#cover?` on Range.
-* Add `same_time` option to `#next_week` and `#prev_week` for `Date`, `Time`,
- and `DateTime`.
+ `Range#cover?` can now accept a range argument like `Range#include?` and
+ `Range#===`. `Range#===` works correctly on Ruby 2.6. `Range#include?` is moved
+ into a new file, with these two methods.
- *George Claghorn*
+ *Requiring active_support/core_ext/range/include_range is now deprecated.*
+ *Use `require "active_support/core_ext/range/compare_range"` instead.*
-* Add `#on_weekend?`, `#next_weekday`, `#prev_weekday` methods to `Date`,
- `Time`, and `DateTime`.
+ *utilum*
- `#on_weekend?` returns `true` if the receiving date/time falls on a Saturday
- or Sunday.
+* Add `index_with` to Enumerable.
- `#next_weekday` returns a new date/time representing the next day that does
- not fall on a Saturday or Sunday.
+ Allows creating a hash from an enumerable with the value from a passed block
+ or a default argument.
- `#prev_weekday` returns a new date/time representing the previous day that
- does not fall on a Saturday or Sunday.
+ %i( title body ).index_with { |attr| post.public_send(attr) }
+ # => { title: "hey", body: "what's up?" }
- *George Claghorn*
+ %i( title body ).index_with(nil)
+ # => { title: nil, body: nil }
-* Change the default test order from `:sorted` to `:random`.
+ Closely linked with `index_by`, which creates a hash where the keys are extracted from a block.
- *Rafael Mendonça França*
+ *Kasper Timm Hansen*
-* Remove deprecated `ActiveSupport::JSON::Encoding::CircularReferenceError`.
+* Fix bug where `ActiveSupport::Timezone.all` would fail when tzinfo data for
+ any timezone defined in `ActiveSupport::TimeZone::MAPPING` is missing.
- *Rafael Mendonça França*
+ *Dominik Sander*
-* Remove deprecated methods `ActiveSupport::JSON::Encoding.encode_big_decimal_as_string=`
- and `ActiveSupport::JSON::Encoding.encode_big_decimal_as_string`.
+* Redis cache store: `delete_matched` no longer blocks the Redis server.
+ (Switches from evaled Lua to a batched SCAN + DEL loop.)
- *Rafael Mendonça França*
+ *Gleb Mazovetskiy*
-* Remove deprecated `ActiveSupport::SafeBuffer#prepend`.
+* Fix bug where `ActiveSupport::Cache` will massively inflate the storage
+ size when compression is enabled (which is true by default). This patch
+ does not attempt to repair existing data: please manually flush the cache
+ to clear out the problematic entries.
- *Rafael Mendonça França*
+ *Godfrey Chan*
-* Remove deprecated methods at `Kernel`.
+* Fix bug where `URI.unescape` would fail with mixed Unicode/escaped character input:
- `silence_stderr`, `silence_stream`, `capture` and `quietly`.
+ URI.unescape("\xe3\x83\x90") # => "バ"
+ URI.unescape("%E3%83%90") # => "バ"
+ URI.unescape("\xe3\x83\x90%E3%83%90") # => Encoding::CompatibilityError
- *Rafael Mendonça França*
+ *Ashe Connor*, *Aaron Patterson*
-* Remove deprecated `active_support/core_ext/big_decimal/yaml_conversions`
- file.
+* Add `before?` and `after?` methods to `Date`, `DateTime`,
+ `Time`, and `TimeWithZone`.
- *Rafael Mendonça França*
+ *Nick Holden*
-* Remove deprecated methods `ActiveSupport::Cache::Store.instrument` and
- `ActiveSupport::Cache::Store.instrument=`.
+* `ActiveSupport::Inflector#ordinal` and `ActiveSupport::Inflector#ordinalize` now support
+ translations through I18n.
- *Rafael Mendonça França*
+ # locale/fr.rb
-* Change the way in which callback chains can be halted.
+ {
+ fr: {
+ number: {
+ nth: {
+ ordinals: lambda do |_key, number:, **_options|
+ if number.to_i.abs == 1
+ 'er'
+ else
+ 'e'
+ end
+ end,
- The preferred method to halt a callback chain from now on is to explicitly
- `throw(:abort)`.
- In the past, returning `false` in an ActiveSupport callback had the side
- effect of halting the callback chain. This is not recommended anymore and,
- depending on the value of
- `Callbacks::CallbackChain.halt_and_display_warning_on_return_false`, will
- either not work at all or display a deprecation warning.
+ ordinalized: lambda do |_key, number:, **_options|
+ "#{number}#{ActiveSupport::Inflector.ordinal(number)}"
+ end
+ }
+ }
+ }
+ }
-* Add `Callbacks::CallbackChain.halt_and_display_warning_on_return_false`
- Setting `Callbacks::CallbackChain.halt_and_display_warning_on_return_false`
- to `true` will let an app support the deprecated way of halting callback
- chains by returning `false`.
+ *Christian Blais*
- Setting the value to `false` will tell the app to ignore any `false` value
- returned by callbacks, and only halt the chain upon `throw(:abort)`.
+* Add `:private` option to ActiveSupport's `Module#delegate`
+ in order to delegate methods as private:
- The value can also be set with the Rails configuration option
- `config.active_support.halt_callback_chains_on_return_false`.
+ class User < ActiveRecord::Base
+ has_one :profile
+ delegate :date_of_birth, to: :profile, private: true
- When the configuration option is missing, its value is `true`, so older apps
- ported to Rails 5.0 will not break (but display a deprecation warning).
- For new Rails 5.0 apps, its value is set to `false` in an initializer, so
- these apps will support the new behavior by default.
+ def age
+ Date.today.year - date_of_birth.year
+ end
+ end
- *claudiob*
+ # User.new.age # => 29
+ # User.new.date_of_birth
+ # => NoMethodError: private method `date_of_birth' called for #<User:0x00000008221340>
-* Changes arguments and default value of CallbackChain's `:terminator` option
+ *Tomas Valent*
- Chains of callbacks defined without an explicit `:terminator` option will
- now be halted as soon as a `before_` callback throws `:abort`.
+* `String#truncate_bytes` to truncate a string to a maximum bytesize without
+ breaking multibyte characters or grapheme clusters like 👩‍👩‍👦‍👦.
- Chains of callbacks defined with a `:terminator` option will maintain their
- existing behavior of halting as soon as a `before_` callback matches the
- terminator's expectation.
+ *Jeremy Daer*
- *claudiob*
+* `String#strip_heredoc` preserves frozenness.
-* Deprecate `MissingSourceFile` in favor of `LoadError`.
+ "foo".freeze.strip_heredoc.frozen? # => true
- `MissingSourceFile` was just an alias to `LoadError` and was not being
- raised inside the framework.
+ Fixes that frozen string literals would inadvertently become unfrozen:
- *Rafael Mendonça França*
+ # frozen_string_literal: true
-* Add support for error dispatcher classes in `ActiveSupport::Rescuable`.
- Now it acts closer to Ruby's rescue.
+ foo = <<-MSG.strip_heredoc
+ la la la
+ MSG
- Example:
+ foo.frozen? # => false !??
- class BaseController < ApplicationController
- module ErrorDispatcher
- def self.===(other)
- Exception === other && other.respond_to?(:status)
- end
- end
+ *Jeremy Daer*
- rescue_from ErrorDispatcher do |error|
- render status: error.status, json: { error: error.to_s }
- end
- end
+* Rails 6 requires Ruby 2.4.1 or newer.
- *Genadi Samokovarov*
+ *Jeremy Daer*
-* Add `#verified` and `#valid_message?` methods to `ActiveSupport::MessageVerifier`
+* Adds parallel testing to Rails.
- Previously, the only way to decode a message with `ActiveSupport::MessageVerifier`
- was to use `#verify`, which would raise an exception on invalid messages. Now
- `#verified` can also be used, which returns `nil` on messages that cannot be
- decoded.
+ Parallelize your test suite with forked processes or threads.
- Previously, there was no way to check if a message's format was valid without
- attempting to decode it. `#valid_message?` is a boolean convenience method that
- checks whether the message is valid without actually decoding it.
+ *Eileen M. Uchitelle*, *Aaron Patterson*
- *Logan Leger*
-Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/activesupport/CHANGELOG.md) for previous changes.
+Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/activesupport/CHANGELOG.md) for previous changes.
diff --git a/activesupport/MIT-LICENSE b/activesupport/MIT-LICENSE
index 7bffebb076..8f769c0767 100644
--- a/activesupport/MIT-LICENSE
+++ b/activesupport/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2005-2015 David Heinemeier Hansson
+Copyright (c) 2005-2018 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
@@ -17,4 +17,4 @@ 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. \ No newline at end of file
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/activesupport/README.rdoc b/activesupport/README.rdoc
index cd72f53821..c770324be8 100644
--- a/activesupport/README.rdoc
+++ b/activesupport/README.rdoc
@@ -10,7 +10,7 @@ outside of Rails.
The latest version of Active Support can be installed with RubyGems:
- % gem install activesupport
+ $ gem install activesupport
Source code can be downloaded as part of the Rails project on GitHub:
@@ -21,7 +21,7 @@ Source code can be downloaded as part of the Rails project on GitHub:
Active Support is released under the MIT license:
-* http://www.opensource.org/licenses/MIT
+* https://opensource.org/licenses/MIT
== Support
@@ -30,11 +30,10 @@ API documentation is at:
* http://api.rubyonrails.org
-Bug reports can be filed for the Ruby on Rails project here:
+Bug reports for the Ruby on Rails project can be filed here:
* https://github.com/rails/rails/issues
Feature requests should be discussed on the rails-core mailing list here:
* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core
-
diff --git a/activesupport/Rakefile b/activesupport/Rakefile
index 81c242d4b1..f10f19be0a 100644
--- a/activesupport/Rakefile
+++ b/activesupport/Rakefile
@@ -1,18 +1,43 @@
-require 'rake/testtask'
+# frozen_string_literal: true
+
+require "rake/testtask"
+
+task default: :test
+
+task :package
-task :default => :test
Rake::TestTask.new do |t|
- t.libs << 'test'
- t.pattern = 'test/**/*_test.rb'
+ t.libs << "test"
+ t.pattern = "test/**/*_test.rb"
t.warning = true
t.verbose = true
t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
end
+Rake::Task[:test].enhance do
+ Rake::Task["test:cache_stores:redis:ruby"].invoke
+end
+
namespace :test do
task :isolated do
Dir.glob("test/**/*_test.rb").all? do |file|
- sh(Gem.ruby, '-w', '-Ilib:test', file)
- end or raise "Failures"
+ sh(Gem.ruby, "-w", "-Ilib:test", file)
+ end || raise("Failures")
+ end
+
+ namespace :cache_stores do
+ namespace :redis do
+ %w[ ruby hiredis ].each do |driver|
+ task("env:#{driver}") { ENV["REDIS_DRIVER"] = driver }
+
+ Rake::TestTask.new(driver => "env:#{driver}") do |t|
+ t.libs << "test"
+ t.test_files = ["test/cache/stores/redis_cache_store_test.rb"]
+ t.warning = true
+ t.verbose = true
+ t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
+ end
+ end
+ end
end
end
diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec
index 12c6a4d449..448a2eeebb 100644
--- a/activesupport/activesupport.gemspec
+++ b/activesupport/activesupport.gemspec
@@ -1,28 +1,37 @@
-version = File.read(File.expand_path('../../RAILS_VERSION', __FILE__)).strip
+# frozen_string_literal: true
+
+version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip
Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
- s.name = 'activesupport'
+ s.name = "activesupport"
s.version = version
- s.summary = 'A toolkit of support libraries and Ruby core extensions extracted from the Rails framework.'
- s.description = 'A toolkit of support libraries and Ruby core extensions extracted from the Rails framework. Rich support for multibyte strings, internationalization, time zones, and testing.'
+ s.summary = "A toolkit of support libraries and Ruby core extensions extracted from the Rails framework."
+ s.description = "A toolkit of support libraries and Ruby core extensions extracted from the Rails framework. Rich support for multibyte strings, internationalization, time zones, and testing."
+
+ s.required_ruby_version = ">= 2.4.1"
+
+ s.license = "MIT"
- s.required_ruby_version = '>= 2.2.2'
+ s.author = "David Heinemeier Hansson"
+ s.email = "david@loudthinking.com"
+ s.homepage = "http://rubyonrails.org"
- s.license = 'MIT'
+ s.files = Dir["CHANGELOG.md", "MIT-LICENSE", "README.rdoc", "lib/**/*"]
+ s.require_path = "lib"
- s.author = 'David Heinemeier Hansson'
- s.email = 'david@loudthinking.com'
- s.homepage = 'http://www.rubyonrails.org'
+ s.rdoc_options.concat ["--encoding", "UTF-8"]
- s.files = Dir['CHANGELOG.md', 'MIT-LICENSE', 'README.rdoc', 'lib/**/*']
- s.require_path = 'lib'
+ s.metadata = {
+ "source_code_uri" => "https://github.com/rails/rails/tree/v#{version}/activesupport",
+ "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/activesupport/CHANGELOG.md"
+ }
- s.rdoc_options.concat ['--encoding', 'UTF-8']
+ # NOTE: Please read our dependency guidelines before updating versions:
+ # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves
- s.add_dependency 'i18n', '~> 0.7'
- s.add_dependency 'json', '~> 1.7', '>= 1.7.7'
- s.add_dependency 'tzinfo', '~> 1.1'
- s.add_dependency 'minitest', '~> 5.1'
- s.add_dependency 'thread_safe','~> 0.3', '>= 0.3.4'
+ s.add_dependency "i18n", ">= 0.7", "< 2"
+ s.add_dependency "tzinfo", "~> 1.1"
+ s.add_dependency "minitest", "~> 5.1"
+ s.add_dependency "concurrent-ruby", "~> 1.0", ">= 1.0.2"
end
diff --git a/activesupport/bin/generate_tables b/activesupport/bin/generate_tables
deleted file mode 100755
index 71a6b78652..0000000000
--- a/activesupport/bin/generate_tables
+++ /dev/null
@@ -1,144 +0,0 @@
-#!/usr/bin/env ruby
-
-begin
- $:.unshift(File.expand_path(File.dirname(__FILE__) + '/../lib'))
- require 'active_support'
-rescue IOError
-end
-
-require 'open-uri'
-require 'tmpdir'
-
-module ActiveSupport
- module Multibyte
- module Unicode
-
- class UnicodeDatabase
- def load; end
- end
-
- class DatabaseGenerator
- BASE_URI = "http://www.unicode.org/Public/#{UNICODE_VERSION}/ucd/"
- SOURCES = {
- :codepoints => BASE_URI + 'UnicodeData.txt',
- :composition_exclusion => BASE_URI + 'CompositionExclusions.txt',
- :grapheme_break_property => BASE_URI + 'auxiliary/GraphemeBreakProperty.txt',
- :cp1252 => 'http://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP1252.TXT'
- }
-
- def initialize
- @ucd = Unicode::UnicodeDatabase.new
- end
-
- def parse_codepoints(line)
- codepoint = Codepoint.new
- raise "Could not parse input." unless line =~ /^
- ([0-9A-F]+); # code
- ([^;]+); # name
- ([A-Z]+); # general category
- ([0-9]+); # canonical combining class
- ([A-Z]+); # bidi class
- (<([A-Z]*)>)? # decomposition type
- ((\ ?[0-9A-F]+)*); # decomposition mapping
- ([0-9]*); # decimal digit
- ([0-9]*); # digit
- ([^;]*); # numeric
- ([YN]*); # bidi mirrored
- ([^;]*); # unicode 1.0 name
- ([^;]*); # iso comment
- ([0-9A-F]*); # simple uppercase mapping
- ([0-9A-F]*); # simple lowercase mapping
- ([0-9A-F]*)$/ix # simple titlecase mapping
- codepoint.code = $1.hex
- #codepoint.name = $2
- #codepoint.category = $3
- codepoint.combining_class = Integer($4)
- #codepoint.bidi_class = $5
- codepoint.decomp_type = $7
- codepoint.decomp_mapping = ($8=='') ? nil : $8.split.collect(&:hex)
- #codepoint.bidi_mirrored = ($13=='Y') ? true : false
- codepoint.uppercase_mapping = ($16=='') ? 0 : $16.hex
- codepoint.lowercase_mapping = ($17=='') ? 0 : $17.hex
- #codepoint.titlecase_mapping = ($18=='') ? nil : $18.hex
- @ucd.codepoints[codepoint.code] = codepoint
- end
-
- def parse_grapheme_break_property(line)
- if line =~ /^([0-9A-F.]+)\s*;\s*([\w]+)\s*#/
- type = $2.downcase.intern
- @ucd.boundary[type] ||= []
- if $1.include? '..'
- parts = $1.split '..'
- @ucd.boundary[type] << (parts[0].hex..parts[1].hex)
- else
- @ucd.boundary[type] << $1.hex
- end
- end
- end
-
- def parse_composition_exclusion(line)
- if line =~ /^([0-9A-F]+)/i
- @ucd.composition_exclusion << $1.hex
- end
- end
-
- def parse_cp1252(line)
- if line =~ /^([0-9A-Fx]+)\s([0-9A-Fx]+)/i
- @ucd.cp1252[$1.hex] = $2.hex
- end
- end
-
- def create_composition_map
- @ucd.codepoints.each do |_, cp|
- if !cp.nil? and cp.combining_class == 0 and cp.decomp_type.nil? and !cp.decomp_mapping.nil? and cp.decomp_mapping.length == 2 and @ucd.codepoints[cp.decomp_mapping[0]].combining_class == 0 and !@ucd.composition_exclusion.include?(cp.code)
- @ucd.composition_map[cp.decomp_mapping[0]] ||= {}
- @ucd.composition_map[cp.decomp_mapping[0]][cp.decomp_mapping[1]] = cp.code
- end
- end
- end
-
- def normalize_boundary_map
- @ucd.boundary.each do |k,v|
- if [:lf, :cr].include? k
- @ucd.boundary[k] = v[0]
- end
- end
- end
-
- def parse
- SOURCES.each do |type, url|
- filename = File.join(Dir.tmpdir, "#{url.split('/').last}")
- unless File.exist?(filename)
- $stderr.puts "Downloading #{url.split('/').last}"
- File.open(filename, 'wb') do |target|
- open(url) do |source|
- source.each_line { |line| target.write line }
- end
- end
- end
- File.open(filename) do |file|
- file.each_line { |line| send "parse_#{type}".intern, line }
- end
- end
- create_composition_map
- normalize_boundary_map
- end
-
- def dump_to(filename)
- File.open(filename, 'wb') do |f|
- f.write Marshal.dump([@ucd.codepoints, @ucd.composition_exclusion, @ucd.composition_map, @ucd.boundary, @ucd.cp1252])
- end
- end
- end
- end
- end
-end
-
-if __FILE__ == $0
- filename = ActiveSupport::Multibyte::Unicode::UnicodeDatabase.filename
- generator = ActiveSupport::Multibyte::Unicode::DatabaseGenerator.new
- generator.parse
- print "Writing to: #{filename}"
- generator.dump_to filename
- puts " (#{File.size(filename)} bytes)"
-end
diff --git a/activesupport/bin/test b/activesupport/bin/test
index 404cabba51..c53377cc97 100755
--- a/activesupport/bin/test
+++ b/activesupport/bin/test
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
-COMPONENT_ROOT = File.expand_path("../../", __FILE__)
-require File.expand_path("../tools/test", COMPONENT_ROOT)
-exit Minitest.run(ARGV)
+# frozen_string_literal: true
+
+COMPONENT_ROOT = File.expand_path("..", __dir__)
+require_relative "../../tools/test"
diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb
index 588d6c49f9..a4fb697669 100644
--- a/activesupport/lib/active_support.rb
+++ b/activesupport/lib/active_support.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
#--
-# Copyright (c) 2005-2015 David Heinemeier Hansson
+# Copyright (c) 2005-2018 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -21,21 +23,27 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
-require 'securerandom'
+require "securerandom"
require "active_support/dependencies/autoload"
require "active_support/version"
require "active_support/logger"
require "active_support/lazy_load_hooks"
+require "active_support/core_ext/date_and_time/compatibility"
module ActiveSupport
extend ActiveSupport::Autoload
autoload :Concern
+ autoload :CurrentAttributes
autoload :Dependencies
autoload :DescendantsTracker
+ autoload :ExecutionWrapper
+ autoload :Executor
autoload :FileUpdateChecker
+ autoload :EventedFileUpdateChecker
autoload :LogSubscriber
autoload :Notifications
+ autoload :Reloader
eager_autoload do
autoload :BacktraceCleaner
@@ -45,6 +53,7 @@ module ActiveSupport
autoload :Callbacks
autoload :Configurable
autoload :Deprecation
+ autoload :Digest
autoload :Gzip
autoload :Inflector
autoload :JSON
@@ -74,12 +83,12 @@ module ActiveSupport
cattr_accessor :test_order # :nodoc:
- def self.halt_callback_chains_on_return_false
- Callbacks::CallbackChain.halt_and_display_warning_on_return_false
+ def self.to_time_preserves_timezone
+ DateAndTime::Compatibility.preserve_timezone
end
- def self.halt_callback_chains_on_return_false=(value)
- Callbacks::CallbackChain.halt_and_display_warning_on_return_false = value
+ def self.to_time_preserves_timezone=(value)
+ DateAndTime::Compatibility.preserve_timezone = value
end
end
diff --git a/activesupport/lib/active_support/all.rb b/activesupport/lib/active_support/all.rb
index f537818300..4adf446af8 100644
--- a/activesupport/lib/active_support/all.rb
+++ b/activesupport/lib/active_support/all.rb
@@ -1,3 +1,5 @@
-require 'active_support'
-require 'active_support/time'
-require 'active_support/core_ext'
+# frozen_string_literal: true
+
+require "active_support"
+require "active_support/time"
+require "active_support/core_ext"
diff --git a/activesupport/lib/active_support/array_inquirer.rb b/activesupport/lib/active_support/array_inquirer.rb
index e7188d7adb..b2b9e9c0b7 100644
--- a/activesupport/lib/active_support/array_inquirer.rb
+++ b/activesupport/lib/active_support/array_inquirer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveSupport
# Wrapping an array in an +ArrayInquirer+ gives a friendlier way to check
# its string-like contents:
@@ -9,8 +11,10 @@ module ActiveSupport
# variants.desktop? # => false
class ArrayInquirer < Array
# Passes each element of +candidates+ collection to ArrayInquirer collection.
- # The method returns true if at least one element is the same. If +candidates+
- # collection is not given, method returns true.
+ # The method returns true if any element from the ArrayInquirer collection
+ # is equal to the stringified or symbolized form of any element in the +candidates+ collection.
+ #
+ # If +candidates+ collection is not given, method returns true.
#
# variants = ActiveSupport::ArrayInquirer.new([:phone, :tablet])
#
@@ -18,23 +22,23 @@ 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
candidates.any? do |candidate|
- include?(candidate) || include?(candidate.to_sym)
+ include?(candidate.to_sym) || include?(candidate.to_s)
end
end
end
private
def respond_to_missing?(name, include_private = false)
- name[-1] == '?'
+ (name[-1] == "?") || super
end
def method_missing(name, *args)
- if name[-1] == '?'
+ if name[-1] == "?"
any?(name[0..-2])
else
super
diff --git a/activesupport/lib/active_support/backtrace_cleaner.rb b/activesupport/lib/active_support/backtrace_cleaner.rb
index e161ec4cca..62973eca58 100644
--- a/activesupport/lib/active_support/backtrace_cleaner.rb
+++ b/activesupport/lib/active_support/backtrace_cleaner.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveSupport
# Backtraces often include many lines that are not relevant for the context
# under review. This makes it hard to find the signal amongst the backtrace
@@ -12,9 +14,9 @@ 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 =~ /mongrel|rubygems/ } # skip any lines from mongrel or rubygems
+ # bc.add_silencer { |line| line =~ /puma|rubygems/ } # skip any lines from puma or rubygems
# bc.clean(exception.backtrace) # perform the cleanup
#
# To reconfigure an existing BacktraceCleaner (like the default one in Rails)
@@ -29,6 +31,9 @@ module ActiveSupport
class BacktraceCleaner
def initialize
@filters, @silencers = [], []
+ add_gem_filter
+ add_gem_silencer
+ add_stdlib_silencer
end
# Returns the backtrace after all filters and silencers have been run
@@ -59,8 +64,8 @@ module ActiveSupport
# Adds a silencer from the block provided. If the silencer returns +true+
# for a given line, it will be excluded from the clean backtrace.
#
- # # Will reject all lines that include the word "mongrel", like "/gems/mongrel/server.rb" or "/app/my_mongrel_server/rb"
- # backtrace_cleaner.add_silencer { |line| line =~ /mongrel/ }
+ # # Will reject all lines that include the word "puma", like "/gems/puma/server.rb" or "/app/my_puma_server/rb"
+ # backtrace_cleaner.add_silencer { |line| line =~ /puma/ }
def add_silencer(&block)
@silencers << block
end
@@ -80,6 +85,26 @@ module ActiveSupport
end
private
+
+ FORMATTED_GEMS_PATTERN = /\A[^\/]+ \([\w.]+\) /
+
+ def add_gem_filter
+ gems_paths = (Gem.path | [Gem.default_dir]).map { |p| Regexp.escape(p) }
+ return if gems_paths.empty?
+
+ gems_regexp = %r{(#{gems_paths.join('|')})/(bundler/)?gems/([^/]+)-([\w.]+)/(.*)}
+ gems_result = '\3 (\4) \5'
+ add_filter { |line| line.sub(gems_regexp, gems_result) }
+ end
+
+ def add_gem_silencer
+ add_silencer { |line| FORMATTED_GEMS_PATTERN.match?(line) }
+ end
+
+ def add_stdlib_silencer
+ add_silencer { |line| line.start_with?(RbConfig::CONFIG["rubylibdir"]) }
+ end
+
def filter_backtrace(backtrace)
@filters.each do |f|
backtrace = backtrace.map { |line| f.call(line) }
diff --git a/activesupport/lib/active_support/benchmarkable.rb b/activesupport/lib/active_support/benchmarkable.rb
index 805b7a714f..f481d68198 100644
--- a/activesupport/lib/active_support/benchmarkable.rb
+++ b/activesupport/lib/active_support/benchmarkable.rb
@@ -1,5 +1,7 @@
-require 'active_support/core_ext/benchmark'
-require 'active_support/core_ext/hash/keys'
+# frozen_string_literal: true
+
+require "active_support/core_ext/benchmark"
+require "active_support/core_ext/hash/keys"
module ActiveSupport
module Benchmarkable
@@ -38,8 +40,8 @@ module ActiveSupport
options[:level] ||= :info
result = nil
- ms = Benchmark.ms { result = options[:silence] ? silence { yield } : yield }
- logger.send(options[:level], '%s (%.1fms)' % [ message, ms ])
+ ms = Benchmark.ms { result = options[:silence] ? logger.silence { yield } : yield }
+ logger.send(options[:level], "%s (%.1fms)" % [ message, ms ])
result
else
yield
diff --git a/activesupport/lib/active_support/builder.rb b/activesupport/lib/active_support/builder.rb
index 321e462acd..3fa7e6b26d 100644
--- a/activesupport/lib/active_support/builder.rb
+++ b/activesupport/lib/active_support/builder.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
begin
- require 'builder'
+ require "builder"
rescue LoadError => e
$stderr.puts "You don't have builder installed in your application. Please add it to your Gemfile and run bundle install"
raise e
diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb
index 837974bc85..e8518645d9 100644
--- a/activesupport/lib/active_support/cache.rb
+++ b/activesupport/lib/active_support/cache.rb
@@ -1,32 +1,33 @@
-require 'benchmark'
-require 'zlib'
-require 'active_support/core_ext/array/extract_options'
-require 'active_support/core_ext/array/wrap'
-require 'active_support/core_ext/benchmark'
-require 'active_support/core_ext/module/attribute_accessors'
-require 'active_support/core_ext/numeric/bytes'
-require 'active_support/core_ext/numeric/time'
-require 'active_support/core_ext/object/to_param'
-require 'active_support/core_ext/string/inflections'
+# frozen_string_literal: true
+
+require "zlib"
+require "active_support/core_ext/array/extract_options"
+require "active_support/core_ext/array/wrap"
+require "active_support/core_ext/module/attribute_accessors"
+require "active_support/core_ext/numeric/bytes"
+require "active_support/core_ext/numeric/time"
+require "active_support/core_ext/object/to_param"
+require "active_support/core_ext/string/inflections"
module ActiveSupport
# See ActiveSupport::Cache::Store for documentation.
module Cache
- autoload :FileStore, 'active_support/cache/file_store'
- autoload :MemoryStore, 'active_support/cache/memory_store'
- autoload :MemCacheStore, 'active_support/cache/mem_cache_store'
- autoload :NullStore, 'active_support/cache/null_store'
+ autoload :FileStore, "active_support/cache/file_store"
+ autoload :MemoryStore, "active_support/cache/memory_store"
+ autoload :MemCacheStore, "active_support/cache/mem_cache_store"
+ autoload :NullStore, "active_support/cache/null_store"
+ autoload :RedisCacheStore, "active_support/cache/redis_cache_store"
# These options mean something to all cache implementations. Individual cache
# implementations may support additional options.
UNIVERSAL_OPTIONS = [:namespace, :compress, :compress_threshold, :expires_in, :race_condition_ttl]
module Strategy
- autoload :LocalCache, 'active_support/cache/strategy/local_cache'
+ autoload :LocalCache, "active_support/cache/strategy/local_cache"
end
class << self
- # Creates a new CacheStore object according to the given options.
+ # Creates a new Store object according to the given options.
#
# If no arguments are passed to this method, then a new
# ActiveSupport::Cache::MemoryStore object will be returned.
@@ -72,12 +73,12 @@ 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)
- expanded_cache_key = namespace ? "#{namespace}/" : ""
+ expanded_cache_key = (namespace ? "#{namespace}/" : "").dup
if prefix = ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"]
expanded_cache_key << "#{prefix}/"
@@ -90,16 +91,19 @@ module ActiveSupport
private
def retrieve_cache_key(key)
case
- when key.respond_to?(:cache_key) then key.cache_key
- when key.is_a?(Array) then key.map { |element| retrieve_cache_key(element) }.to_param
- when key.respond_to?(:to_a) then retrieve_cache_key(key.to_a)
- else key.to_param
+ when key.respond_to?(:cache_key_with_version) then key.cache_key_with_version
+ when key.respond_to?(:cache_key) then key.cache_key
+ when key.is_a?(Array) then key.map { |element| retrieve_cache_key(element) }.to_param
+ when key.respond_to?(:to_a) then retrieve_cache_key(key.to_a)
+ else key.to_param
end.to_s
end
# Obtains the specified cache store class, given the name of the +store+.
# Raises an error when the store class cannot be found.
def retrieve_store_class(store)
+ # require_relative cannot be used here because the class might be
+ # provided by another gem, like redis-activesupport for example.
require "active_support/cache/#{store}"
rescue LoadError => e
raise "Could not find cache store adapter for #{store} (#{e})"
@@ -145,32 +149,48 @@ module ActiveSupport
# cache.namespace = -> { @last_mod_time } # Set the namespace to a variable
# @last_mod_time = Time.now # Invalidate the entire cache by changing namespace
#
- # Caches can also store values in a compressed format to save space and
- # reduce time spent sending data. Since there is overhead, values must be
- # large enough to warrant compression. To turn on compression either pass
- # <tt>compress: true</tt> in the initializer or as an option to +fetch+
- # or +write+. To specify the threshold at which to compress values, set the
- # <tt>:compress_threshold</tt> option. The default threshold is 16K.
+ # Cached data larger than 1kB are compressed by default. To turn off
+ # compression, pass <tt>compress: false</tt> to the initializer or to
+ # individual +fetch+ or +write+ method calls. The 1kB compression
+ # threshold is configurable with the <tt>:compress_threshold</tt> option,
+ # specified in bytes.
class Store
- cattr_accessor :logger, :instance_writer => true
+ cattr_accessor :logger, instance_writer: true
attr_reader :silence, :options
alias :silence? :silence
- # Create a new cache. The options will be passed to any write method calls
+ class << self
+ private
+ def retrieve_pool_options(options)
+ {}.tap do |pool_options|
+ pool_options[:size] = options.delete(:pool_size) if options[:pool_size]
+ pool_options[:timeout] = options.delete(:pool_timeout) if options[:pool_timeout]
+ end
+ end
+
+ def ensure_connection_pool_added!
+ require "connection_pool"
+ rescue LoadError => e
+ $stderr.puts "You don't have connection_pool installed in your application. Please add it to your Gemfile and run bundle install"
+ raise e
+ end
+ end
+
+ # Creates a new cache. The options will be passed to any write method calls
# except for <tt>:namespace</tt> which can be used to set the global
# namespace for the cache.
def initialize(options = nil)
@options = options ? options.dup : {}
end
- # Silence the logger.
+ # Silences the logger.
def silence!
@silence = true
self
end
- # Silence the logger within a block.
+ # Silences the logger within a block.
def mute
previous_silence, @silence = defined?(@silence) && @silence, true
yield
@@ -197,13 +217,27 @@ module ActiveSupport
# cache.fetch('city') # => "Duckburgh"
#
# You may also specify additional options via the +options+ argument.
- # Setting <tt>force: true</tt> will force a cache miss:
+ # Setting <tt>force: true</tt> forces a cache "miss," meaning we treat
+ # the cache value as missing even if it's present. Passing a block is
+ # required when +force+ is true so this always results in a cache write.
#
# cache.write('today', 'Monday')
- # cache.fetch('today', force: true) # => nil
+ # cache.fetch('today', force: true) { 'Tuesday' } # => 'Tuesday'
+ # cache.fetch('today', force: true) # => ArgumentError
#
- # Setting <tt>:compress</tt> will store a large cache entry set by the call
- # in a compressed format.
+ # The +:force+ option is useful when you're calling some other method to
+ # ask whether you should force a cache write. Otherwise, it's clearer to
+ # just call <tt>Cache#write</tt>.
+ #
+ # Setting <tt>skip_nil: true</tt> will not cache nil result:
+ #
+ # cache.fetch('foo') { nil }
+ # cache.fetch('bar', skip_nil: true) { nil }
+ # cache.exist?('foo') # => true
+ # cache.exist?('bar') # => false
+ #
+ #
+ # Setting <tt>compress: false</tt> disables compression of the cache entry.
#
# Setting <tt>:expires_in</tt> will set an expiration time on the cache.
# All caches support auto-expiring content after a specified number of
@@ -214,6 +248,10 @@ module ActiveSupport
# cache = ActiveSupport::Cache::MemoryStore.new(expires_in: 5.minutes)
# cache.write(key, value, expires_in: 1.minute) # Set a lower value for one entry
#
+ # Setting <tt>:version</tt> verifies the cache stored under <tt>name</tt>
+ # is of the same version. nil is returned on mismatches despite contents.
+ # This feature is used to support recyclable cache keys.
+ #
# Setting <tt>:race_condition_ttl</tt> is very useful in situations where
# a cache entry is used very frequently and is under heavy load. If a
# cache expires and due to heavy load several different processes will try
@@ -242,22 +280,23 @@ module ActiveSupport
# sleep 60
#
# Thread.new do
- # val_1 = cache.fetch('foo', race_condition_ttl: 10) do
+ # val_1 = cache.fetch('foo', race_condition_ttl: 10.seconds) do
# sleep 1
# 'new value 1'
# end
# end
#
# Thread.new do
- # val_2 = cache.fetch('foo', race_condition_ttl: 10) do
+ # val_2 = cache.fetch('foo', race_condition_ttl: 10.seconds) do
# 'new value 2'
# end
# end
#
- # # val_1 => "new value 1"
- # # val_2 => "original value"
- # # sleep 10 # First thread extend the life of cache by another 10 seconds
- # # cache.fetch('foo') => "new value 1"
+ # cache.fetch('foo') # => "original value"
+ # sleep 10 # First thread extended the life of cache by another 10 seconds
+ # cache.fetch('foo') # => "new value 1"
+ # val_1 # => "new value 1"
+ # val_2 # => "original value"
#
# Other options will be handled by the specific cache store implementation.
# Internally, #fetch calls #read_entry, and calls #write_entry on a cache
@@ -275,36 +314,54 @@ module ActiveSupport
def fetch(name, options = nil)
if block_given?
options = merged_options(options)
- key = namespaced_key(name, options)
+ key = normalize_key(name, options)
- cached_entry = find_cached_entry(key, name, options) unless options[:force]
- entry = handle_expired_entry(cached_entry, key, options)
+ entry = nil
+ instrument(:read, name, options) do |payload|
+ cached_entry = read_entry(key, options) unless options[:force]
+ entry = handle_expired_entry(cached_entry, key, options)
+ entry = nil if entry && entry.mismatched?(normalize_version(name, options))
+ payload[:super_operation] = :fetch if payload
+ payload[:hit] = !!entry if payload
+ end
if entry
get_entry_value(entry, name, options)
else
save_block_result_to_cache(name, options) { |_name| yield _name }
end
+ elsif options && options[:force]
+ raise ArgumentError, "Missing block: Calling `Cache#fetch` with `force: true` requires a block."
else
read(name, options)
end
end
- # Fetches data from the cache, using the given key. If there is data in
+ # Reads data from the cache, using the given key. If there is data in
# the cache with the given key, then that data is returned. Otherwise,
# +nil+ is returned.
#
+ # Note, if data was written with the <tt>:expires_in</tt> or
+ # <tt>:version</tt> options, both of these conditions are applied before
+ # the data is returned.
+ #
# Options are passed to the underlying cache implementation.
def read(name, options = nil)
options = merged_options(options)
- key = namespaced_key(name, options)
+ key = normalize_key(name, options)
+ version = normalize_version(name, options)
+
instrument(:read, name, options) do |payload|
entry = read_entry(key, options)
+
if entry
if entry.expired?
delete_entry(key, options)
payload[:hit] = false if payload
nil
+ elsif entry.mismatched?(version)
+ payload[:hit] = false if payload
+ nil
else
payload[:hit] = true if payload
entry.value
@@ -316,7 +373,7 @@ module ActiveSupport
end
end
- # Read multiple values at once from the cache. Options can be passed
+ # Reads multiple values at once from the cache. Options can be passed
# in the last argument.
#
# Some cache implementation may optimize this method.
@@ -326,20 +383,23 @@ module ActiveSupport
options = names.extract_options!
options = merged_options(options)
- instrument_multi(:read, names, options) do |payload|
- results = {}
- names.each do |name|
- key = namespaced_key(name, options)
- entry = read_entry(key, options)
- if entry
- if entry.expired?
- delete_entry(key, options)
- else
- results[name] = entry.value
- end
- end
+ instrument :read_multi, names, options do |payload|
+ read_multi_entries(names, options).tap do |results|
+ payload[:hits] = results.keys
end
- results
+ end
+ end
+
+ # Cache Storage API to write multiple values at once.
+ def write_multi(hash, options = nil)
+ options = merged_options(options)
+
+ instrument :write_multi, hash, options do |payload|
+ entries = hash.each_with_object({}) do |(name, value), memo|
+ memo[normalize_key(name, options)] = Entry.new(value, options.merge(version: normalize_version(name, options)))
+ end
+
+ write_multi_entries entries, options
end
end
@@ -347,8 +407,9 @@ module ActiveSupport
# the cache with the given keys, then that data is returned. Otherwise,
# the supplied block is called for each key for which there was no data,
# and the result will be written to the cache and returned.
- #
- # Options are passed to the underlying cache implementation.
+ # Therefore, you need to pass a block that returns the data to be written
+ # to the cache. If you do not want to write the cache when the cache is
+ # not found, use #read_multi.
#
# Returns a hash with the data for each of the names. For example:
#
@@ -359,16 +420,35 @@ module ActiveSupport
# # => { "bim" => "bam",
# # "unknown_key" => "Fallback value for key: unknown_key" }
#
+ # Options are passed to the underlying cache implementation. For example:
+ #
+ # cache.fetch_multi("fizz", expires_in: 5.seconds) do |key|
+ # "buzz"
+ # end
+ # # => {"fizz"=>"buzz"}
+ # cache.read("fizz")
+ # # => "buzz"
+ # sleep(6)
+ # cache.read("fizz")
+ # # => nil
def fetch_multi(*names)
+ raise ArgumentError, "Missing block: `Cache#fetch_multi` requires a block." unless block_given?
+
options = names.extract_options!
options = merged_options(options)
- results = read_multi(*names, options)
- names.each_with_object({}) do |name, memo|
- memo[name] = results.fetch(name) do
- value = yield name
- write(name, value, options)
- value
+ instrument :read_multi, names, options do |payload|
+ read_multi_entries(names, options).tap do |results|
+ payload[:hits] = results.keys
+ payload[:super_operation] = :fetch_multi
+
+ writes = {}
+
+ (names - results.keys).each do |name|
+ results[name] = writes[name] = yield(name)
+ end
+
+ write_multi writes, options
end
end
end
@@ -380,8 +460,8 @@ module ActiveSupport
options = merged_options(options)
instrument(:write, name, options) do
- entry = Entry.new(value, options)
- write_entry(namespaced_key(name, options), entry, options)
+ entry = Entry.new(value, options.merge(version: normalize_version(name, options)))
+ write_entry(normalize_key(name, options), entry, options)
end
end
@@ -392,7 +472,7 @@ module ActiveSupport
options = merged_options(options)
instrument(:delete, name) do
- delete_entry(namespaced_key(name, options), options)
+ delete_entry(normalize_key(name, options), options)
end
end
@@ -403,12 +483,12 @@ module ActiveSupport
options = merged_options(options)
instrument(:exist?, name) do
- entry = read_entry(namespaced_key(name, options), options)
- (entry && !entry.expired?) || false
+ entry = read_entry(normalize_key(name, options), options)
+ (entry && !entry.expired? && !entry.mismatched?(normalize_version(name, options))) || false
end
end
- # Delete all entries with keys matching the pattern.
+ # Deletes all entries with keys matching the pattern.
#
# Options are passed to the underlying cache implementation.
#
@@ -417,7 +497,7 @@ module ActiveSupport
raise NotImplementedError.new("#{self.class.name} does not support delete_matched")
end
- # Increment an integer value in the cache.
+ # Increments an integer value in the cache.
#
# Options are passed to the underlying cache implementation.
#
@@ -426,7 +506,7 @@ module ActiveSupport
raise NotImplementedError.new("#{self.class.name} does not support increment")
end
- # Decrement an integer value in the cache.
+ # Decrements an integer value in the cache.
#
# Options are passed to the underlying cache implementation.
#
@@ -435,7 +515,7 @@ module ActiveSupport
raise NotImplementedError.new("#{self.class.name} does not support decrement")
end
- # Cleanup the cache by removing expired entries.
+ # Cleanups the cache by removing expired entries.
#
# Options are passed to the underlying cache implementation.
#
@@ -444,7 +524,7 @@ module ActiveSupport
raise NotImplementedError.new("#{self.class.name} does not support cleanup")
end
- # Clear the entire cache. Be careful with this method since it could
+ # Clears the entire cache. Be careful with this method since it could
# affect other processes if shared cache is being used.
#
# The options hash is passed to the underlying cache implementation.
@@ -454,16 +534,16 @@ module ActiveSupport
raise NotImplementedError.new("#{self.class.name} does not support clear")
end
- protected
- # Add the namespace defined in the options to a pattern designed to
+ 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
- if source.start_with?('^')
+ if source.start_with?("^")
source = source[1, source.length]
else
source = ".*#{source[0, source.length]}"
@@ -474,81 +554,135 @@ module ActiveSupport
end
end
- # Read an entry from the cache implementation. Subclasses must implement
+ # Reads an entry from the cache implementation. Subclasses must implement
# this method.
- def read_entry(key, options) # :nodoc:
+ def read_entry(key, options)
raise NotImplementedError.new
end
- # Write an entry to the cache implementation. Subclasses must implement
+ # Writes an entry to the cache implementation. Subclasses must implement
# this method.
- def write_entry(key, entry, options) # :nodoc:
+ def write_entry(key, entry, options)
raise NotImplementedError.new
end
- # Delete an entry from the cache implementation. Subclasses must
+ # Reads multiple entries from the cache implementation. Subclasses MAY
# implement this method.
- def delete_entry(key, options) # :nodoc:
+ def read_multi_entries(names, options)
+ results = {}
+ names.each do |name|
+ key = normalize_key(name, options)
+ version = normalize_version(name, options)
+ entry = read_entry(key, options)
+
+ if entry
+ if entry.expired?
+ delete_entry(key, options)
+ elsif entry.mismatched?(version)
+ # Skip mismatched versions
+ else
+ results[name] = entry.value
+ end
+ end
+ end
+ results
+ end
+
+ # Writes multiple entries to the cache implementation. Subclasses MAY
+ # implement this method.
+ def write_multi_entries(hash, options)
+ hash.each do |key, entry|
+ write_entry key, entry, options
+ end
+ end
+
+ # Deletes an entry from the cache implementation. Subclasses must
+ # implement this method.
+ def delete_entry(key, options)
raise NotImplementedError.new
end
- private
- # Merge the default options with ones specific to a method call.
- def merged_options(call_options) # :nodoc:
+ # Merges the default options with ones specific to a method call.
+ def merged_options(call_options)
if call_options
- options.merge(call_options)
+ if options.empty?
+ call_options
+ else
+ options.merge(call_options)
+ end
else
- options.dup
+ options
end
end
- # Expand key to be a consistent string value. Invoke +cache_key+ if
+ # Expands and namespaces the cache key. May be overridden by
+ # cache stores to do additional normalization.
+ def normalize_key(key, options = nil)
+ namespace_key expanded_key(key), options
+ end
+
+ # Prefix the key with a namespace string:
+ #
+ # namespace_key 'foo', namespace: 'cache'
+ # # => 'cache:foo'
+ #
+ # With a namespace block:
+ #
+ # namespace_key 'foo', namespace: -> { 'cache' }
+ # # => 'cache:foo'
+ def namespace_key(key, options = nil)
+ options = merged_options(options)
+ namespace = options[:namespace]
+
+ if namespace.respond_to?(:call)
+ namespace = namespace.call
+ end
+
+ if namespace
+ "#{namespace}:#{key}"
+ else
+ key
+ end
+ end
+
+ # Expands key to be a consistent string value. Invokes +cache_key+ if
# 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
when Array
if key.size > 1
- key = key.collect{|element| expanded_key(element)}
+ key = key.collect { |element| expanded_key(element) }
else
- key = key.first
+ key = expanded_key(key.first)
end
when Hash
- key = key.sort_by { |k,_| k.to_s }.collect{|k,v| "#{k}=#{v}"}
+ key = key.sort_by { |k, _| k.to_s }.collect { |k, v| "#{k}=#{v}" }
end
key.to_param
end
- # Prefix a key with the namespace. Namespace and key will be delimited
- # with a colon.
- def namespaced_key(key, options)
- key = expanded_key(key)
- namespace = options[:namespace] if options
- prefix = namespace.is_a?(Proc) ? namespace.call : namespace
- key = "#{prefix}:#{key}" if prefix
- key
+ def normalize_version(key, options = nil)
+ (options && options[:version].try(:to_param)) || expanded_version(key)
end
- def instrument(operation, key, options = nil)
- log { "Cache #{operation}: #{key}#{options.blank? ? "" : " (#{options.inspect})"}" }
-
- payload = { :key => key }
- payload.merge!(options) if options.is_a?(Hash)
- ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload){ yield(payload) }
+ def expanded_version(key)
+ case
+ when key.respond_to?(:cache_version) then key.cache_version.to_param
+ when key.is_a?(Array) then key.map { |element| expanded_version(element) }.compact.to_param
+ when key.respond_to?(:to_a) then expanded_version(key.to_a)
+ end
end
- def instrument_multi(operation, keys, options = nil)
- log do
- formatted_keys = keys.map { |k| "- #{k}" }.join("\n")
- "Caches multi #{operation}:\n#{formatted_keys}#{options.blank? ? "" : " (#{options.inspect})"}"
- end
+ def instrument(operation, key, options = nil)
+ log { "Cache #{operation}: #{normalize_key(key, options)}#{options.blank? ? "" : " (#{options.inspect})"}" }
- payload = { key: keys }
+ payload = { key: key }
payload.merge!(options) if options.is_a?(Hash)
- ActiveSupport::Notifications.instrument("cache_#{operation}_multi.active_support", payload) { yield(payload) }
+ ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload) { yield(payload) }
end
def log
@@ -556,13 +690,6 @@ module ActiveSupport
logger.debug(yield)
end
- def find_cached_entry(key, name, options)
- instrument(:read, name, options) do |payload|
- payload[:super_operation] = :fetch if payload
- read_entry(key, options)
- end
- end
-
def handle_expired_entry(entry, key, options)
if entry && entry.expired?
race_ttl = options[:race_condition_ttl].to_i
@@ -570,7 +697,7 @@ module ActiveSupport
# When an entry has a positive :race_condition_ttl defined, put the stale entry back into the cache
# for a brief period while the entry is being recalculated.
entry.expires_at = Time.now + race_ttl
- write_entry(key, entry, :expires_in => race_ttl * 2)
+ write_entry(key, entry, expires_in: race_ttl * 2)
else
delete_entry(key, options)
end
@@ -580,49 +707,52 @@ module ActiveSupport
end
def get_entry_value(entry, name, options)
- instrument(:fetch_hit, name, options) { |payload| }
+ instrument(:fetch_hit, name, options) { }
entry.value
end
def save_block_result_to_cache(name, options)
- result = instrument(:generate, name, options) do |payload|
+ result = instrument(:generate, name, options) do
yield(name)
end
- write(name, result, options)
+ write(name, result, options) unless result.nil? && options[:skip_nil]
result
end
end
- # This class is used to represent cache entries. Cache entries have a value and an optional
- # expiration time. The expiration time is used to support the :race_condition_ttl option
- # on the cache.
+ # This class is used to represent cache entries. Cache entries have a value, an optional
+ # expiration time, and an optional version. The expiration time is used to support the :race_condition_ttl option
+ # on the cache. The version is used to support the :version option on the cache for rejecting
+ # mismatches.
#
# Since cache entries in most instances will be serialized, the internals of this class are highly optimized
# using short instance variable names that are lazily defined.
class Entry # :nodoc:
- DEFAULT_COMPRESS_LIMIT = 16.kilobytes
-
- # Create a new cache entry for the specified value. Options supported are
- # +:compress+, +:compress_threshold+, and +:expires_in+.
- def initialize(value, options = {})
- if should_compress?(value, options)
- @value = compress(value)
- @compressed = true
- else
- @value = value
- end
+ attr_reader :version
+
+ DEFAULT_COMPRESS_LIMIT = 1.kilobyte
+ # Creates a new cache entry for the specified value. Options supported are
+ # +:compress+, +:compress_threshold+, +:version+ and +:expires_in+.
+ def initialize(value, compress: true, compress_threshold: DEFAULT_COMPRESS_LIMIT, version: nil, expires_in: nil, **)
+ @value = value
+ @version = version
@created_at = Time.now.to_f
- @expires_in = options[:expires_in]
- @expires_in = @expires_in.to_f if @expires_in
+ @expires_in = expires_in && expires_in.to_f
+
+ compress!(compress_threshold) if compress
end
def value
compressed? ? uncompress(@value) : @value
end
- # Check if the entry is expired. The +expires_in+ parameter can override
+ def mismatched?(version)
+ @version && version && @version != version
+ end
+
+ # Checks if the entry is expired. The +expires_in+ parameter can override
# the value set when the entry was created.
def expired?
@expires_in && @created_at + @expires_in <= Time.now.to_f
@@ -643,21 +773,17 @@ module ActiveSupport
# Returns the size of the cached value. This could be less than
# <tt>value.size</tt> if the data is compressed.
def size
- if defined?(@s)
- @s
+ case value
+ when NilClass
+ 0
+ when String
+ @value.bytesize
else
- case value
- when NilClass
- 0
- when String
- @value.bytesize
- else
- @s = Marshal.dump(@value).bytesize
- end
+ @s ||= Marshal.dump(@value).bytesize
end
end
- # Duplicate the value in a class. This is used by cache implementations that don't natively
+ # Duplicates the value in a class. This is used by cache implementations that don't natively
# serialize entries to protect against accidental cache modifications.
def dup_value!
if @value && !compressed? && !(@value.is_a?(Numeric) || @value == true || @value == false)
@@ -670,23 +796,30 @@ module ActiveSupport
end
private
- def should_compress?(value, options)
- if value && options[:compress]
- compress_threshold = options[:compress_threshold] || DEFAULT_COMPRESS_LIMIT
- serialized_value_size = (value.is_a?(String) ? value : Marshal.dump(value)).bytesize
-
- return true if serialized_value_size >= compress_threshold
+ def compress!(compress_threshold)
+ case @value
+ when nil, true, false, Numeric
+ uncompressed_size = 0
+ when String
+ uncompressed_size = @value.bytesize
+ else
+ serialized = Marshal.dump(@value)
+ uncompressed_size = serialized.bytesize
end
- false
- end
+ if uncompressed_size >= compress_threshold
+ serialized ||= Marshal.dump(@value)
+ compressed = Zlib::Deflate.deflate(serialized)
- def compressed?
- defined?(@compressed) ? @compressed : false
+ if compressed.bytesize < uncompressed_size
+ @value = compressed
+ @compressed = true
+ end
+ end
end
- def compress(value)
- Zlib::Deflate.deflate(Marshal.dump(value))
+ def compressed?
+ defined?(@compressed)
end
def uncompress(value)
diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb
index e6a8b84214..53a2b07536 100644
--- a/activesupport/lib/active_support/cache/file_store.rb
+++ b/activesupport/lib/active_support/cache/file_store.rb
@@ -1,7 +1,9 @@
-require 'active_support/core_ext/marshal'
-require 'active_support/core_ext/file/atomic'
-require 'active_support/core_ext/string/conversions'
-require 'uri/common'
+# frozen_string_literal: true
+
+require "active_support/core_ext/marshal"
+require "active_support/core_ext/file/atomic"
+require "active_support/core_ext/string/conversions"
+require "uri/common"
module ActiveSupport
module Cache
@@ -10,25 +12,31 @@ module ActiveSupport
# FileStore implements the Strategy::LocalCache strategy which implements
# an in-memory cache inside of a block.
class FileStore < Store
+ prepend Strategy::LocalCache
attr_reader :cache_path
DIR_FORMATTER = "%03X"
FILENAME_MAX_SIZE = 228 # max filename size on file system is 255, minus room for timestamp and random characters appended by Tempfile (used by atomic write)
FILEPATH_MAX_SIZE = 900 # max is 1024, plus some room
- EXCLUDED_DIRS = ['.', '..'].freeze
+ EXCLUDED_DIRS = [".", ".."].freeze
+ GITKEEP_FILES = [".gitkeep", ".keep"].freeze
def initialize(cache_path, options = nil)
super(options)
@cache_path = cache_path.to_s
- extend Strategy::LocalCache
+ end
+
+ # Advertise cache versioning support.
+ def self.supports_cache_versioning?
+ true
end
# Deletes all items from the cache. In this case it deletes all the entries in the specified
- # file store directory except for .gitkeep. Be careful which directory is specified in your
+ # file store directory except for .keep or .gitkeep. Be careful which directory is specified in your
# config file when using +FileStore+ because everything in that directory will be deleted.
def clear(options = nil)
- root_dirs = Dir.entries(cache_path).reject {|f| (EXCLUDED_DIRS + [".gitkeep"]).include?(f)}
- FileUtils.rm_r(root_dirs.collect{|f| File.join(cache_path, f)})
+ root_dirs = exclude_from(cache_path, EXCLUDED_DIRS + GITKEEP_FILES)
+ FileUtils.rm_r(root_dirs.collect { |f| File.join(cache_path, f) })
rescue Errno::ENOENT
end
@@ -36,9 +44,8 @@ module ActiveSupport
def cleanup(options = nil)
options = merged_options(options)
search_dir(cache_path) do |fname|
- key = file_path_key(fname)
- entry = read_entry(key, options)
- delete_entry(key, options) if entry && entry.expired?
+ entry = read_entry(fname, options)
+ delete_entry(fname, options) if entry && entry.expired?
end
end
@@ -60,17 +67,16 @@ module ActiveSupport
matcher = key_matcher(matcher, options)
search_dir(cache_path) do |path|
key = file_path_key(path)
- delete_entry(key, options) if key.match(matcher)
+ delete_entry(path, options) if key.match(matcher)
end
end
end
- protected
+ private
def read_entry(key, options)
- file_name = key_file_path(key)
- if File.exist?(file_name)
- File.open(file_name) { |f| Marshal.load(f) }
+ if File.exist?(key)
+ File.open(key) { |f| Marshal.load(f) }
end
rescue => e
logger.error("FileStoreError (#{e}): #{e.message}") if logger
@@ -78,33 +84,30 @@ module ActiveSupport
end
def write_entry(key, entry, options)
- file_name = key_file_path(key)
- return false if options[:unless_exist] && File.exist?(file_name)
- ensure_cache_path(File.dirname(file_name))
- File.atomic_write(file_name, cache_path) {|f| Marshal.dump(entry, f)}
+ return false if options[:unless_exist] && File.exist?(key)
+ ensure_cache_path(File.dirname(key))
+ File.atomic_write(key, cache_path) { |f| Marshal.dump(entry, f) }
true
end
def delete_entry(key, options)
- file_name = key_file_path(key)
- if File.exist?(file_name)
+ if File.exist?(key)
begin
- File.delete(file_name)
- delete_empty_directories(File.dirname(file_name))
+ File.delete(key)
+ delete_empty_directories(File.dirname(key))
true
rescue => e
# Just in case the error was caused by another process deleting the file first.
- raise e if File.exist?(file_name)
+ raise e if File.exist?(key)
false
end
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|
+ File.open(file_name, "r+") do |f|
begin
f.flock File::LOCK_EX
yield
@@ -118,24 +121,30 @@ module ActiveSupport
end
# Translate a key into a file path.
- def key_file_path(key)
- if key.size > FILEPATH_MAX_SIZE
- key = Digest::MD5.hexdigest(key)
+ def normalize_key(key, options)
+ key = super
+ fname = URI.encode_www_form_component(key)
+
+ if fname.size > FILEPATH_MAX_SIZE
+ fname = ActiveSupport::Digest.hexdigest(key)
end
- fname = URI.encode_www_form_component(key)
hash = Zlib.adler32(fname)
hash, dir_1 = hash.divmod(0x1000)
dir_2 = hash.modulo(0x1000)
- fname_paths = []
# Make sure file name doesn't exceed file system limits.
- begin
- fname_paths << fname[0, FILENAME_MAX_SIZE]
- fname = fname[FILENAME_MAX_SIZE..-1]
- end until fname.blank?
+ if fname.length < FILENAME_MAX_SIZE
+ fname_paths = fname
+ else
+ fname_paths = []
+ begin
+ fname_paths << fname[0, FILENAME_MAX_SIZE]
+ fname = fname[FILENAME_MAX_SIZE..-1]
+ end until fname.blank?
+ end
- File.join(cache_path, DIR_FORMATTER % dir_1, DIR_FORMATTER % dir_2, *fname_paths)
+ File.join(cache_path, DIR_FORMATTER % dir_1, DIR_FORMATTER % dir_2, fname_paths)
end
# Translate a file path into a key.
@@ -147,7 +156,7 @@ module ActiveSupport
# Delete empty directories in the cache.
def delete_empty_directories(dir)
return if File.realpath(dir) == File.realpath(cache_path)
- if Dir.entries(dir).reject {|f| EXCLUDED_DIRS.include?(f)}.empty?
+ if exclude_from(dir, EXCLUDED_DIRS).empty?
Dir.delete(dir) rescue nil
delete_empty_directories(File.dirname(dir))
end
@@ -174,7 +183,7 @@ module ActiveSupport
# Modifies the amount of an already existing integer value that is stored in the cache.
# If the key is not found nothing is done.
def modify_value(name, amount, options)
- file_name = key_file_path(namespaced_key(name, options))
+ file_name = normalize_key(name, options)
lock_file(file_name) do
options = merged_options(options)
@@ -186,6 +195,11 @@ module ActiveSupport
end
end
end
+
+ # Exclude entries from source directory
+ def exclude_from(source, excludes)
+ Dir.entries(source).reject { |f| excludes.include?(f) }
+ end
end
end
end
diff --git a/activesupport/lib/active_support/cache/mem_cache_store.rb b/activesupport/lib/active_support/cache/mem_cache_store.rb
index 47133bf550..174c784deb 100644
--- a/activesupport/lib/active_support/cache/mem_cache_store.rb
+++ b/activesupport/lib/active_support/cache/mem_cache_store.rb
@@ -1,18 +1,19 @@
+# frozen_string_literal: true
+
begin
- require 'dalli'
+ require "dalli"
rescue LoadError => e
$stderr.puts "You don't have dalli installed in your application. Please add it to your Gemfile and run bundle install"
raise e
end
-require 'digest/md5'
-require 'active_support/core_ext/marshal'
-require 'active_support/core_ext/array/extract_options'
+require "active_support/core_ext/marshal"
+require "active_support/core_ext/array/extract_options"
module ActiveSupport
module Cache
# A cache store implementation which stores data in Memcached:
- # http://memcached.org/
+ # https://memcached.org
#
# This is currently the most popular cache store for production websites.
#
@@ -24,6 +25,36 @@ module ActiveSupport
# MemCacheStore implements the Strategy::LocalCache strategy which implements
# an in-memory cache inside of a block.
class MemCacheStore < Store
+ # Provide support for raw values in the local cache strategy.
+ module LocalCacheWithRaw # :nodoc:
+ private
+ def read_entry(key, options)
+ entry = super
+ if options[:raw] && local_cache && entry
+ entry = deserialize_entry(entry.value)
+ end
+ entry
+ end
+
+ def write_entry(key, entry, options)
+ if options[:raw] && local_cache
+ raw_entry = Entry.new(entry.value.to_s)
+ raw_entry.expires_at = entry.expires_at
+ super(key, raw_entry, options)
+ else
+ super
+ end
+ end
+ end
+
+ # Advertise cache versioning support.
+ def self.supports_cache_versioning?
+ true
+ end
+
+ prepend Strategy::LocalCache
+ prepend LocalCacheWithRaw
+
ESCAPE_KEY_CHARS = /[\x00-\x20%\x7F-\xFF]/n
# Creates a new Dalli::Client instance with specified addresses and options.
@@ -37,7 +68,14 @@ module ActiveSupport
addresses = addresses.flatten
options = addresses.extract_options!
addresses = ["localhost:11211"] if addresses.empty?
- Dalli::Client.new(addresses, options)
+ pool_options = retrieve_pool_options(options)
+
+ if pool_options.empty?
+ Dalli::Client.new(addresses, options)
+ else
+ ensure_connection_pool_added!
+ ConnectionPool.new(pool_options) { Dalli::Client.new(addresses, options.merge(threadsafe: false)) }
+ end
end
# Creates a new MemCacheStore object, with the given memcached server
@@ -60,85 +98,56 @@ module ActiveSupport
@data = addresses.first
else
mem_cache_options = options.dup
- UNIVERSAL_OPTIONS.each{|name| mem_cache_options.delete(name)}
+ UNIVERSAL_OPTIONS.each { |name| mem_cache_options.delete(name) }
@data = self.class.build_mem_cache(*(addresses + [mem_cache_options]))
end
-
- extend Strategy::LocalCache
- extend LocalCacheWithRaw
- end
-
- # Reads multiple values from the cache using a single call to the
- # servers for all keys. Options can be passed in the last argument.
- def read_multi(*names)
- options = names.extract_options!
- options = merged_options(options)
-
- instrument_multi(:read, names, options) do
- keys_to_names = Hash[names.map{|name| [escape_key(namespaced_key(name, options)), name]}]
- raw_values = @data.get_multi(keys_to_names.keys, :raw => true)
- values = {}
- raw_values.each do |key, value|
- entry = deserialize_entry(value)
- values[keys_to_names[key]] = entry.value unless entry.expired?
- end
- values
- end
end
# Increment a cached value. This method uses the memcached incr atomic
# operator and can only be used on values written with the :raw option.
# Calling it on a value not stored with :raw will initialize that value
# to zero.
- def increment(name, amount = 1, options = nil) # :nodoc:
+ def increment(name, amount = 1, options = nil)
options = merged_options(options)
- instrument(:increment, name, :amount => amount) do
- @data.incr(escape_key(namespaced_key(name, options)), amount)
+ instrument(:increment, name, amount: amount) do
+ rescue_error_with nil do
+ @data.with { |c| c.incr(normalize_key(name, options), amount, options[:expires_in]) }
+ end
end
- rescue Dalli::DalliError => e
- logger.error("DalliError (#{e}): #{e.message}") if logger
- nil
end
# Decrement a cached value. This method uses the memcached decr atomic
# operator and can only be used on values written with the :raw option.
# Calling it on a value not stored with :raw will initialize that value
# to zero.
- def decrement(name, amount = 1, options = nil) # :nodoc:
+ def decrement(name, amount = 1, options = nil)
options = merged_options(options)
- instrument(:decrement, name, :amount => amount) do
- @data.decr(escape_key(namespaced_key(name, options)), amount)
+ instrument(:decrement, name, amount: amount) do
+ rescue_error_with nil do
+ @data.with { |c| c.decr(normalize_key(name, options), amount, options[:expires_in]) }
+ end
end
- rescue Dalli::DalliError => e
- logger.error("DalliError (#{e}): #{e.message}") if logger
- nil
end
# Clear the entire cache on all memcached servers. This method should
# be used with care when shared cache is being used.
def clear(options = nil)
- @data.flush_all
- rescue Dalli::DalliError => e
- logger.error("DalliError (#{e}): #{e.message}") if logger
- nil
+ rescue_error_with(nil) { @data.with { |c| c.flush_all } }
end
# Get the statistics from the memcached servers.
def stats
- @data.stats
+ @data.with { |c| c.stats }
end
- protected
+ private
# Read an entry from the cache.
- def read_entry(key, options) # :nodoc:
- deserialize_entry(@data.get(escape_key(key), options))
- rescue Dalli::DalliError => e
- logger.error("DalliError (#{e}): #{e.message}") if logger
- nil
+ def read_entry(key, options)
+ rescue_error_with(nil) { deserialize_entry(@data.with { |c| c.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
@@ -146,30 +155,42 @@ module ActiveSupport
# Set the memcache expire a few minutes in the future to support race condition ttls on read
expires_in += 5.minutes
end
- @data.send(method, escape_key(key), value, expires_in, options)
- rescue Dalli::DalliError => e
- logger.error("DalliError (#{e}): #{e.message}") if logger
- false
+ rescue_error_with false do
+ @data.with { |c| c.send(method, key, value, expires_in, options) }
+ end
end
- # Delete an entry from the cache.
- def delete_entry(key, options) # :nodoc:
- @data.delete(escape_key(key))
- rescue Dalli::DalliError => e
- logger.error("DalliError (#{e}): #{e.message}") if logger
- false
+ # Reads multiple entries from the cache implementation.
+ def read_multi_entries(names, options)
+ keys_to_names = Hash[names.map { |name| [normalize_key(name, options), name] }]
+
+ raw_values = @data.with { |c| c.get_multi(keys_to_names.keys) }
+ values = {}
+
+ raw_values.each do |key, value|
+ entry = deserialize_entry(value)
+
+ unless entry.expired? || entry.mismatched?(normalize_version(keys_to_names[key], options))
+ values[keys_to_names[key]] = entry.value
+ end
+ end
+
+ values
end
- private
+ # Delete an entry from the cache.
+ def delete_entry(key, options)
+ rescue_error_with(false) { @data.with { |c| c.delete(key) } }
+ end
# 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.
- def escape_key(key)
- key = key.to_s.dup
+ def normalize_key(key, options)
+ key = super.dup
key = key.force_encoding(Encoding::ASCII_8BIT)
- key = key.gsub(ESCAPE_KEY_CHARS){ |match| "%#{match.getbyte(0).to_s(16).upcase}" }
- key = "#{key[0, 213]}:md5:#{Digest::MD5.hexdigest(key)}" if key.size > 250
+ key = key.gsub(ESCAPE_KEY_CHARS) { |match| "%#{match.getbyte(0).to_s(16).upcase}" }
+ key = "#{key[0, 213]}:md5:#{ActiveSupport::Digest.hexdigest(key)}" if key.size > 250
key
end
@@ -177,32 +198,15 @@ module ActiveSupport
if raw_value
entry = Marshal.load(raw_value) rescue raw_value
entry.is_a?(Entry) ? entry : Entry.new(entry)
- else
- nil
end
end
- # Provide support for raw values in the local cache strategy.
- module LocalCacheWithRaw # :nodoc:
- protected
- def read_entry(key, options)
- entry = super
- if options[:raw] && local_cache && entry
- entry = deserialize_entry(entry.value)
- end
- entry
- end
-
- def write_entry(key, entry, options) # :nodoc:
- retval = super
- if options[:raw] && local_cache && retval
- raw_entry = Entry.new(entry.value.to_s)
- raw_entry.expires_at = entry.expires_at
- local_cache.write_entry(key, raw_entry, options)
- end
- retval
- end
- end
+ def rescue_error_with(fallback)
+ yield
+ rescue Dalli::DalliError => e
+ logger.error("DalliError (#{e}): #{e.message}") if logger
+ fallback
+ end
end
end
end
diff --git a/activesupport/lib/active_support/cache/memory_store.rb b/activesupport/lib/active_support/cache/memory_store.rb
index 90bb2c38c3..106b616529 100644
--- a/activesupport/lib/active_support/cache/memory_store.rb
+++ b/activesupport/lib/active_support/cache/memory_store.rb
@@ -1,10 +1,12 @@
-require 'monitor'
+# frozen_string_literal: true
+
+require "monitor"
module ActiveSupport
module Cache
# A cache store implementation which stores everything into memory in the
# same process. If you're running multiple Ruby on Rails server processes
- # (which is the case if you're using mongrel_cluster or Phusion Passenger),
+ # (which is the case if you're using Phusion Passenger or puma clustered mode),
# then this means that Rails server process instances won't be able
# to share cache data with each other and this may not be the most
# appropriate cache in that scenario.
@@ -28,6 +30,12 @@ module ActiveSupport
@pruning = false
end
+ # Advertise cache versioning support.
+ def self.supports_cache_versioning?
+ true
+ end
+
+ # Delete all data stored in a given cache store.
def clear(options = nil)
synchronize do
@data.clear
@@ -39,8 +47,8 @@ module ActiveSupport
# Preemptively iterates through all stored keys and removes the ones which have expired.
def cleanup(options = nil)
options = merged_options(options)
- instrument(:cleanup, :size => @data.size) do
- keys = synchronize{ @data.keys }
+ instrument(:cleanup, size: @data.size) do
+ keys = synchronize { @data.keys }
keys.each do |key|
entry = @data[key]
delete_entry(key, options) if entry && entry.expired?
@@ -56,8 +64,8 @@ module ActiveSupport
begin
start_time = Time.now
cleanup
- instrument(:prune, target_size, :from => @cache_size) do
- keys = synchronize{ @key_access.keys.sort{|a,b| @key_access[a].to_f <=> @key_access[b].to_f} }
+ instrument(:prune, target_size, from: @cache_size) do
+ keys = synchronize { @key_access.keys.sort { |a, b| @key_access[a].to_f <=> @key_access[b].to_f } }
keys.each do |key|
delete_entry(key, options)
return if @cache_size <= target_size || (max_time && Time.now - start_time > max_time)
@@ -75,32 +83,15 @@ module ActiveSupport
# Increment an integer value in the cache.
def increment(name, amount = 1, options = nil)
- synchronize do
- options = merged_options(options)
- if num = read(name, options)
- num = num.to_i + amount
- write(name, num, options)
- num
- else
- nil
- end
- end
+ modify_value(name, amount, options)
end
# Decrement an integer value in the cache.
def decrement(name, amount = 1, options = nil)
- synchronize do
- options = merged_options(options)
- if num = read(name, options)
- num = num.to_i - amount
- write(name, num, options)
- num
- else
- nil
- end
- end
+ 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
@@ -122,15 +113,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
@@ -142,7 +133,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]
@@ -159,7 +150,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)
@@ -167,6 +158,17 @@ module ActiveSupport
!!entry
end
end
+
+ def modify_value(name, amount, options)
+ synchronize do
+ options = merged_options(options)
+ if num = read(name, options)
+ num = num.to_i + amount
+ write(name, num, options)
+ num
+ end
+ end
+ end
end
end
end
diff --git a/activesupport/lib/active_support/cache/null_store.rb b/activesupport/lib/active_support/cache/null_store.rb
index 4427eaafcd..8452a28fd8 100644
--- a/activesupport/lib/active_support/cache/null_store.rb
+++ b/activesupport/lib/active_support/cache/null_store.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveSupport
module Cache
# A cache store implementation which doesn't actually store anything. Useful in
@@ -8,9 +10,11 @@ module ActiveSupport
# be cached inside blocks that utilize this strategy. See
# ActiveSupport::Cache::Strategy::LocalCache for more details.
class NullStore < Store
- def initialize(options = nil)
- super(options)
- extend Strategy::LocalCache
+ prepend Strategy::LocalCache
+
+ # Advertise cache versioning support.
+ def self.supports_cache_versioning?
+ true
end
def clear(options = nil)
@@ -28,15 +32,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/redis_cache_store.rb b/activesupport/lib/active_support/cache/redis_cache_store.rb
new file mode 100644
index 0000000000..9a55e49e27
--- /dev/null
+++ b/activesupport/lib/active_support/cache/redis_cache_store.rb
@@ -0,0 +1,485 @@
+# frozen_string_literal: true
+
+begin
+ gem "redis", ">= 4.0.1"
+ require "redis"
+ require "redis/distributed"
+rescue LoadError
+ warn "The Redis cache store requires the redis gem, version 4.0.1 or later. Please add it to your Gemfile: `gem \"redis\", \"~> 4.0\"`"
+ raise
+end
+
+# Prefer the hiredis driver but don't require it.
+begin
+ require "redis/connection/hiredis"
+rescue LoadError
+end
+
+require "digest/sha2"
+require "active_support/core_ext/marshal"
+
+module ActiveSupport
+ module Cache
+ module ConnectionPoolLike
+ def with
+ yield self
+ end
+ end
+
+ ::Redis.include(ConnectionPoolLike)
+ ::Redis::Distributed.include(ConnectionPoolLike)
+
+ # Redis cache store.
+ #
+ # Deployment note: Take care to use a *dedicated Redis cache* rather
+ # than pointing this at your existing Redis server. It won't cope well
+ # with mixed usage patterns and it won't expire cache entries by default.
+ #
+ # Redis cache server setup guide: https://redis.io/topics/lru-cache
+ #
+ # * Supports vanilla Redis, hiredis, and Redis::Distributed.
+ # * Supports Memcached-like sharding across Redises with Redis::Distributed.
+ # * Fault tolerant. If the Redis server is unavailable, no exceptions are
+ # raised. Cache fetches are all misses and writes are dropped.
+ # * Local cache. Hot in-memory primary cache within block/middleware scope.
+ # * +read_multi+ and +write_multi+ support for Redis mget/mset. Use Redis::Distributed
+ # 4.0.1+ for distributed mget support.
+ # * +delete_matched+ support for Redis KEYS globs.
+ class RedisCacheStore < Store
+ # Keys are truncated with their own SHA2 digest if they exceed 1kB
+ MAX_KEY_BYTESIZE = 1024
+
+ DEFAULT_REDIS_OPTIONS = {
+ connect_timeout: 20,
+ read_timeout: 1,
+ write_timeout: 1,
+ reconnect_attempts: 0,
+ }
+
+ DEFAULT_ERROR_HANDLER = -> (method:, returning:, exception:) do
+ if logger
+ logger.error { "RedisCacheStore: #{method} failed, returned #{returning.inspect}: #{exception.class}: #{exception.message}" }
+ end
+ end
+
+ # The maximum number of entries to receive per SCAN call.
+ SCAN_BATCH_SIZE = 1000
+ private_constant :SCAN_BATCH_SIZE
+
+ # Advertise cache versioning support.
+ def self.supports_cache_versioning?
+ true
+ end
+
+ # Support raw values in the local cache strategy.
+ module LocalCacheWithRaw # :nodoc:
+ private
+ def read_entry(key, options)
+ entry = super
+ if options[:raw] && local_cache && entry
+ entry = deserialize_entry(entry.value)
+ end
+ entry
+ end
+
+ def write_entry(key, entry, options)
+ if options[:raw] && local_cache
+ raw_entry = Entry.new(serialize_entry(entry, raw: true))
+ raw_entry.expires_at = entry.expires_at
+ super(key, raw_entry, options)
+ else
+ super
+ end
+ end
+
+ def write_multi_entries(entries, options)
+ if options[:raw] && local_cache
+ raw_entries = entries.map do |key, entry|
+ raw_entry = Entry.new(serialize_entry(entry, raw: true))
+ raw_entry.expires_at = entry.expires_at
+ end.to_h
+
+ super(raw_entries, options)
+ else
+ super
+ end
+ end
+ end
+
+ prepend Strategy::LocalCache
+ prepend LocalCacheWithRaw
+
+ class << self
+ # Factory method to create a new Redis instance.
+ #
+ # Handles four options: :redis block, :redis instance, single :url
+ # string, and multiple :url strings.
+ #
+ # Option Class Result
+ # :redis Proc -> options[:redis].call
+ # :redis Object -> options[:redis]
+ # :url String -> Redis.new(url: …)
+ # :url Array -> Redis::Distributed.new([{ url: … }, { url: … }, …])
+ #
+ def build_redis(redis: nil, url: nil, **redis_options) #:nodoc:
+ urls = Array(url)
+
+ if redis.is_a?(Proc)
+ redis.call
+ elsif redis
+ redis
+ elsif urls.size > 1
+ build_redis_distributed_client urls: urls, **redis_options
+ else
+ build_redis_client url: urls.first, **redis_options
+ end
+ end
+
+ private
+ def build_redis_distributed_client(urls:, **redis_options)
+ ::Redis::Distributed.new([], DEFAULT_REDIS_OPTIONS.merge(redis_options)).tap do |dist|
+ urls.each { |u| dist.add_node url: u }
+ end
+ end
+
+ def build_redis_client(url:, **redis_options)
+ ::Redis.new DEFAULT_REDIS_OPTIONS.merge(redis_options.merge(url: url))
+ end
+ end
+
+ attr_reader :redis_options
+ attr_reader :max_key_bytesize
+
+ # Creates a new Redis cache store.
+ #
+ # Handles three options: block provided to instantiate, single URL
+ # provided, and multiple URLs provided.
+ #
+ # :redis Proc -> options[:redis].call
+ # :url String -> Redis.new(url: …)
+ # :url Array -> Redis::Distributed.new([{ url: … }, { url: … }, …])
+ #
+ # No namespace is set by default. Provide one if the Redis cache
+ # server is shared with other apps: <tt>namespace: 'myapp-cache'</tt>.
+ #
+ # Compression is enabled by default with a 1kB threshold, so cached
+ # values larger than 1kB are automatically compressed. Disable by
+ # passing <tt>compress: false</tt> or change the threshold by passing
+ # <tt>compress_threshold: 4.kilobytes</tt>.
+ #
+ # No expiry is set on cache entries by default. Redis is expected to
+ # be configured with an eviction policy that automatically deletes
+ # least-recently or -frequently used keys when it reaches max memory.
+ # See https://redis.io/topics/lru-cache for cache server setup.
+ #
+ # Race condition TTL is not set by default. This can be used to avoid
+ # "thundering herd" cache writes when hot cache entries are expired.
+ # See <tt>ActiveSupport::Cache::Store#fetch</tt> for more.
+ def initialize(namespace: nil, compress: true, compress_threshold: 1.kilobyte, expires_in: nil, race_condition_ttl: nil, error_handler: DEFAULT_ERROR_HANDLER, **redis_options)
+ @redis_options = redis_options
+
+ @max_key_bytesize = MAX_KEY_BYTESIZE
+ @error_handler = error_handler
+
+ super namespace: namespace,
+ compress: compress, compress_threshold: compress_threshold,
+ expires_in: expires_in, race_condition_ttl: race_condition_ttl
+ end
+
+ def redis
+ @redis ||= begin
+ pool_options = self.class.send(:retrieve_pool_options, redis_options)
+
+ if pool_options.any?
+ self.class.send(:ensure_connection_pool_added!)
+ ::ConnectionPool.new(pool_options) { self.class.build_redis(**redis_options) }
+ else
+ self.class.build_redis(**redis_options)
+ end
+ end
+ end
+
+ def inspect
+ instance = @redis || @redis_options
+ "<##{self.class} options=#{options.inspect} redis=#{instance.inspect}>"
+ end
+
+ # Cache Store API implementation.
+ #
+ # Read multiple values at once. Returns a hash of requested keys ->
+ # fetched values.
+ def read_multi(*names)
+ if mget_capable?
+ instrument(:read_multi, names, options) do |payload|
+ read_multi_mget(*names).tap do |results|
+ payload[:hits] = results.keys
+ end
+ end
+ else
+ super
+ end
+ end
+
+ # Cache Store API implementation.
+ #
+ # Supports Redis KEYS glob patterns:
+ #
+ # h?llo matches hello, hallo and hxllo
+ # h*llo matches hllo and heeeello
+ # h[ae]llo matches hello and hallo, but not hillo
+ # h[^e]llo matches hallo, hbllo, ... but not hello
+ # h[a-b]llo matches hallo and hbllo
+ #
+ # Use \ to escape special characters if you want to match them verbatim.
+ #
+ # See https://redis.io/commands/KEYS for more.
+ #
+ # Failsafe: Raises errors.
+ def delete_matched(matcher, options = nil)
+ instrument :delete_matched, matcher do
+ unless String === matcher
+ raise ArgumentError, "Only Redis glob strings are supported: #{matcher.inspect}"
+ end
+ redis.with do |c|
+ pattern = namespace_key(matcher, options)
+ cursor = "0"
+ # Fetch keys in batches using SCAN to avoid blocking the Redis server.
+ begin
+ cursor, keys = c.scan(cursor, match: pattern, count: SCAN_BATCH_SIZE)
+ c.del(*keys) unless keys.empty?
+ end until cursor == "0"
+ end
+ end
+ end
+
+ # Cache Store API implementation.
+ #
+ # Increment a cached value. This method uses the Redis incr atomic
+ # operator and can only be used on values written with the :raw option.
+ # Calling it on a value not stored with :raw will initialize that value
+ # to zero.
+ #
+ # Failsafe: Raises errors.
+ def increment(name, amount = 1, options = nil)
+ instrument :increment, name, amount: amount do
+ failsafe :increment do
+ options = merged_options(options)
+ key = normalize_key(name, options)
+
+ redis.with do |c|
+ c.incrby(key, amount).tap do
+ write_key_expiry(c, key, options)
+ end
+ end
+ end
+ end
+ end
+
+ # Cache Store API implementation.
+ #
+ # Decrement a cached value. This method uses the Redis decr atomic
+ # operator and can only be used on values written with the :raw option.
+ # Calling it on a value not stored with :raw will initialize that value
+ # to zero.
+ #
+ # Failsafe: Raises errors.
+ def decrement(name, amount = 1, options = nil)
+ instrument :decrement, name, amount: amount do
+ failsafe :decrement do
+ options = merged_options(options)
+ key = normalize_key(name, options)
+
+ redis.with do |c|
+ c.decrby(key, amount).tap do
+ write_key_expiry(c, key, options)
+ end
+ end
+ end
+ end
+ end
+
+ # Cache Store API implementation.
+ #
+ # Removes expired entries. Handled natively by Redis least-recently-/
+ # least-frequently-used expiry, so manual cleanup is not supported.
+ def cleanup(options = nil)
+ super
+ end
+
+ # Clear the entire cache on all Redis servers. Safe to use on
+ # shared servers if the cache is namespaced.
+ #
+ # Failsafe: Raises errors.
+ def clear(options = nil)
+ failsafe :clear do
+ if namespace = merged_options(options)[:namespace]
+ delete_matched "*", namespace: namespace
+ else
+ redis.with { |c| c.flushdb }
+ end
+ end
+ end
+
+ def mget_capable? #:nodoc:
+ set_redis_capabilities unless defined? @mget_capable
+ @mget_capable
+ end
+
+ def mset_capable? #:nodoc:
+ set_redis_capabilities unless defined? @mset_capable
+ @mset_capable
+ end
+
+ private
+ def set_redis_capabilities
+ case redis
+ when Redis::Distributed
+ @mget_capable = true
+ @mset_capable = false
+ else
+ @mget_capable = true
+ @mset_capable = true
+ end
+ end
+
+ # Store provider interface:
+ # Read an entry from the cache.
+ def read_entry(key, options = nil)
+ failsafe :read_entry do
+ deserialize_entry redis.with { |c| c.get(key) }
+ end
+ end
+
+ def read_multi_entries(names, _options)
+ if mget_capable?
+ read_multi_mget(*names)
+ else
+ super
+ end
+ end
+
+ def read_multi_mget(*names)
+ options = names.extract_options!
+ options = merged_options(options)
+
+ keys = names.map { |name| normalize_key(name, options) }
+
+ values = failsafe(:read_multi_mget, returning: {}) do
+ redis.with { |c| c.mget(*keys) }
+ end
+
+ names.zip(values).each_with_object({}) do |(name, value), results|
+ if value
+ entry = deserialize_entry(value)
+ unless entry.nil? || entry.expired? || entry.mismatched?(normalize_version(name, options))
+ results[name] = entry.value
+ end
+ end
+ end
+ end
+
+ # Write an entry to the cache.
+ #
+ # Requires Redis 2.6.12+ for extended SET options.
+ def write_entry(key, entry, unless_exist: false, raw: false, expires_in: nil, race_condition_ttl: nil, **options)
+ serialized_entry = serialize_entry(entry, raw: raw)
+
+ # If race condition TTL is in use, ensure that cache entries
+ # stick around a bit longer after they would have expired
+ # so we can purposefully serve stale entries.
+ if race_condition_ttl && expires_in && expires_in > 0 && !raw
+ expires_in += 5.minutes
+ end
+
+ failsafe :write_entry, returning: false do
+ if unless_exist || expires_in
+ modifiers = {}
+ modifiers[:nx] = unless_exist
+ modifiers[:px] = (1000 * expires_in.to_f).ceil if expires_in
+
+ redis.with { |c| c.set key, serialized_entry, modifiers }
+ else
+ redis.with { |c| c.set key, serialized_entry }
+ end
+ end
+ end
+
+ def write_key_expiry(client, key, options)
+ if options[:expires_in] && client.ttl(key).negative?
+ client.expire key, options[:expires_in].to_i
+ end
+ end
+
+ # Delete an entry from the cache.
+ def delete_entry(key, options)
+ failsafe :delete_entry, returning: false do
+ redis.with { |c| c.del key }
+ end
+ end
+
+ # Nonstandard store provider API to write multiple values at once.
+ def write_multi_entries(entries, expires_in: nil, **options)
+ if entries.any?
+ if mset_capable? && expires_in.nil?
+ failsafe :write_multi_entries do
+ redis.with { |c| c.mapped_mset(serialize_entries(entries, raw: options[:raw])) }
+ end
+ else
+ super
+ end
+ end
+ end
+
+ # Truncate keys that exceed 1kB.
+ def normalize_key(key, options)
+ truncate_key super.b
+ end
+
+ def truncate_key(key)
+ if key.bytesize > max_key_bytesize
+ suffix = ":sha2:#{::Digest::SHA2.hexdigest(key)}"
+ truncate_at = max_key_bytesize - suffix.bytesize
+ "#{key.byteslice(0, truncate_at)}#{suffix}"
+ else
+ key
+ end
+ end
+
+ def deserialize_entry(serialized_entry)
+ if serialized_entry
+ entry = Marshal.load(serialized_entry) rescue serialized_entry
+ entry.is_a?(Entry) ? entry : Entry.new(entry)
+ end
+ end
+
+ def serialize_entry(entry, raw: false)
+ if raw
+ entry.value.to_s
+ else
+ Marshal.dump(entry)
+ end
+ end
+
+ def serialize_entries(entries, raw: false)
+ entries.transform_values do |entry|
+ serialize_entry entry, raw: raw
+ end
+ end
+
+ def failsafe(method, returning: nil)
+ yield
+ rescue ::Redis::BaseConnectionError => e
+ handle_exception exception: e, method: method, returning: returning
+ returning
+ end
+
+ def handle_exception(exception:, method:, returning:)
+ if @error_handler
+ @error_handler.(method: method, exception: exception, returning: returning)
+ end
+ rescue => failsafe
+ warn "RedisCacheStore ignored exception in handle_exception: #{failsafe.class}: #{failsafe.message}\n #{failsafe.backtrace.join("\n ")}"
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/cache/strategy/local_cache.rb b/activesupport/lib/active_support/cache/strategy/local_cache.rb
index fe5bc82c30..39b32fc7f6 100644
--- a/activesupport/lib/active_support/cache/strategy/local_cache.rb
+++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb
@@ -1,6 +1,8 @@
-require 'active_support/core_ext/object/duplicable'
-require 'active_support/core_ext/string/inflections'
-require 'active_support/per_thread_registry'
+# frozen_string_literal: true
+
+require "active_support/core_ext/object/duplicable"
+require "active_support/core_ext/string/inflections"
+require "active_support/per_thread_registry"
module ActiveSupport
module Cache
@@ -9,7 +11,7 @@ module ActiveSupport
# duration of a block. Repeated calls to the cache for the same key will hit the
# in-memory cache for faster access.
module LocalCache
- autoload :Middleware, 'active_support/cache/strategy/local_cache_middleware'
+ autoload :Middleware, "active_support/cache/strategy/local_cache_middleware"
# Class for storing and registering the local caches.
class LocalCacheRegistry # :nodoc:
@@ -52,6 +54,17 @@ module ActiveSupport
@data[key]
end
+ def read_multi_entries(keys, options)
+ values = {}
+
+ keys.each do |name|
+ entry = read_entry(name, options)
+ values[name] = entry.value if entry
+ end
+
+ values
+ end
+
def write_entry(key, value, options)
@data[key] = value
true
@@ -60,12 +73,17 @@ module ActiveSupport
def delete_entry(key, options)
!!@data.delete(key)
end
+
+ def fetch_entry(key, options = nil) # :nodoc:
+ @data.fetch(key) { @data[key] = yield }
+ end
end
# Use a local cache for the duration of block.
def with_local_cache
use_temporary_local_cache(LocalStore.new) { yield }
end
+
# Middleware class can be inserted as a Rack handler to be local cache for the
# duration of request.
def middleware
@@ -75,67 +93,82 @@ module ActiveSupport
end
def clear(options = nil) # :nodoc:
- local_cache.clear(options) if local_cache
+ return super unless cache = local_cache
+ cache.clear(options)
super
end
def cleanup(options = nil) # :nodoc:
- local_cache.clear(options) if local_cache
+ return super unless cache = local_cache
+ cache.clear
super
end
def increment(name, amount = 1, options = nil) # :nodoc:
- value = bypass_local_cache{super}
- set_cache_value(value, name, amount, options)
+ return super unless local_cache
+ value = bypass_local_cache { super }
+ write_cache_value(name, value, options)
value
end
def decrement(name, amount = 1, options = nil) # :nodoc:
- value = bypass_local_cache{super}
- set_cache_value(value, name, amount, options)
+ return super unless local_cache
+ value = bypass_local_cache { super }
+ write_cache_value(name, value, options)
value
end
- protected
- def read_entry(key, options) # :nodoc:
- if local_cache
- entry = local_cache.read_entry(key, options)
- unless entry
- entry = super
- local_cache.write_entry(key, entry, options)
- end
- entry
+ private
+ def read_entry(key, options)
+ if cache = local_cache
+ cache.fetch_entry(key) { super }
else
super
end
end
- def write_entry(key, entry, options) # :nodoc:
- local_cache.write_entry(key, entry, options) if local_cache
+ def read_multi_entries(keys, options)
+ return super unless local_cache
+
+ local_entries = local_cache.read_multi_entries(keys, options)
+ missed_keys = keys - local_entries.keys
+
+ if missed_keys.any?
+ local_entries.merge!(super(missed_keys, options))
+ else
+ local_entries
+ end
+ end
+
+ def write_entry(key, entry, options)
+ if options[:unless_exist]
+ local_cache.delete_entry(key, options) if local_cache
+ else
+ local_cache.write_entry(key, entry, options) if local_cache
+ end
+
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 set_cache_value(value, name, amount, options) # :nodoc:
- if local_cache
- local_cache.mute do
- if value
- local_cache.write(name, value, options)
- else
- local_cache.delete(name, options)
- end
+ def write_cache_value(name, value, options)
+ name = normalize_key(name, options)
+ cache = local_cache
+ cache.mute do
+ if value
+ cache.write(name, value, options)
+ else
+ cache.delete(name, options)
end
end
end
- private
-
def local_cache_key
- @local_cache_key ||= "#{self.class.name.underscore}_local_cache_#{object_id}".gsub(/[\/-]/, '_').to_sym
+ @local_cache_key ||= "#{self.class.name.underscore}_local_cache_#{object_id}".gsub(/[\/-]/, "_").to_sym
end
def local_cache
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 a6f24b1a3c..62542bdb22 100644
--- a/activesupport/lib/active_support/cache/strategy/local_cache_middleware.rb
+++ b/activesupport/lib/active_support/cache/strategy/local_cache_middleware.rb
@@ -1,11 +1,12 @@
-require 'rack/body_proxy'
-require 'rack/utils'
+# frozen_string_literal: true
+
+require "rack/body_proxy"
+require "rack/utils"
module ActiveSupport
module Cache
module Strategy
module LocalCache
-
#--
# This class wraps up local storage for middlewares. Only the middleware method should
# construct them.
@@ -13,9 +14,9 @@ module ActiveSupport
attr_reader :name, :local_cache_key
def initialize(name, local_cache_key)
- @name = name
+ @name = name
@local_cache_key = local_cache_key
- @app = nil
+ @app = nil
end
def new(app)
@@ -29,13 +30,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 e8ab3a7db5..d0644a0f7e 100644
--- a/activesupport/lib/active_support/callbacks.rb
+++ b/activesupport/lib/active_support/callbacks.rb
@@ -1,12 +1,14 @@
-require 'active_support/concern'
-require 'active_support/descendants_tracker'
-require 'active_support/core_ext/array/extract_options'
-require 'active_support/core_ext/class/attribute'
-require 'active_support/core_ext/kernel/reporting'
-require 'active_support/core_ext/kernel/singleton_class'
-require 'active_support/core_ext/string/filters'
-require 'active_support/deprecation'
-require 'thread'
+# frozen_string_literal: true
+
+require "active_support/concern"
+require "active_support/descendants_tracker"
+require "active_support/core_ext/array/extract_options"
+require "active_support/core_ext/class/attribute"
+require "active_support/core_ext/kernel/reporting"
+require "active_support/core_ext/kernel/singleton_class"
+require "active_support/core_ext/string/filters"
+require "active_support/deprecation"
+require "thread"
module ActiveSupport
# Callbacks are code hooks that are run at key points in an object's life cycle.
@@ -21,6 +23,9 @@ module ActiveSupport
# +ClassMethods.set_callback+), and run the installed callbacks at the
# appropriate times (via +run_callbacks+).
#
+ # By default callbacks are halted by throwing +:abort+.
+ # See +ClassMethods.define_callbacks+ for details.
+ #
# Three kinds of callbacks are supported: before callbacks, run before a
# certain event; after callbacks, run after the event; and around callbacks,
# blocks that surround the event, triggering it when they yield. Callback code
@@ -62,6 +67,7 @@ module ActiveSupport
included do
extend ActiveSupport::DescendantsTracker
+ class_attribute :__callbacks, instance_writer: false, default: {}
end
CALLBACK_FILTER_TYPES = [:before, :after, :around]
@@ -79,745 +85,772 @@ module ActiveSupport
# run_callbacks :save do
# save
# end
- def run_callbacks(kind, &block)
- callbacks = send("_#{kind}_callbacks")
+ #
+ #--
+ #
+ # As this method is used in many places, and often wraps large portions of
+ # user code, it has an additional design goal of minimizing its impact on
+ # the visible call stack. An exception from inside a :before or :after
+ # callback can be as noisy as it likes -- but when control has passed
+ # smoothly through and into the supplied block, we want as little evidence
+ # as possible that we were here.
+ def run_callbacks(kind)
+ callbacks = __callbacks[kind.to_sym]
if callbacks.empty?
yield if block_given?
else
- runner = callbacks.compile
- e = Filters::Environment.new(self, false, nil, block)
- runner.call(e).value
+ env = Filters::Environment.new(self, false, nil)
+ next_sequence = callbacks.compile
+
+ invoke_sequence = Proc.new do
+ skipped = nil
+ while true
+ 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
+ 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
+ break env.value
+ end
+ end
+
+ # Common case: no 'around' callbacks defined
+ if next_sequence.final?
+ next_sequence.invoke_before(env)
+ env.value = !env.halted && (!block_given? || yield)
+ next_sequence.invoke_after(env)
+ env.value
+ else
+ invoke_sequence.call
+ end
end
end
private
- # A hook invoked every time a before callback is halted.
- # This can be overridden in AS::Callback implementors in order
- # to provide better debugging/logging.
- def halted_callback_hook(filter)
- end
+ # A hook invoked every time a before callback is halted.
+ # This can be overridden in ActiveSupport::Callbacks implementors in order
+ # to provide better debugging/logging.
+ def halted_callback_hook(filter)
+ end
- module Conditionals # :nodoc:
- class Value
- def initialize(&block)
- @block = block
+ module Conditionals # :nodoc:
+ class Value
+ def initialize(&block)
+ @block = block
+ end
+ def call(target, value); @block.call(value); end
end
- def call(target, value); @block.call(value); end
end
- end
- module Filters
- Environment = Struct.new(:target, :halted, :value, :run_block)
+ module Filters
+ Environment = Struct.new(:target, :halted, :value)
- class End
- def call(env)
- block = env.run_block
- env.value = !env.halted && (!block || block.call)
- env
- end
- end
- ENDING = End.new
-
- class Before
- def self.build(callback_sequence, user_callback, user_conditions, chain_config, filter)
- halted_lambda = chain_config[:terminator]
-
- if chain_config.key?(:terminator) && user_conditions.any?
- halting_and_conditional(callback_sequence, user_callback, user_conditions, halted_lambda, filter)
- elsif chain_config.key? :terminator
- halting(callback_sequence, user_callback, halted_lambda, filter)
- elsif user_conditions.any?
- conditional(callback_sequence, user_callback, user_conditions)
- else
- simple callback_sequence, user_callback
- end
- end
+ class Before
+ def self.build(callback_sequence, user_callback, user_conditions, chain_config, filter)
+ halted_lambda = chain_config[:terminator]
- def self.halting_and_conditional(callback_sequence, user_callback, user_conditions, halted_lambda, filter)
- callback_sequence.before do |env|
- target = env.target
- value = env.value
- halted = env.halted
+ if user_conditions.any?
+ halting_and_conditional(callback_sequence, user_callback, user_conditions, halted_lambda, filter)
+ else
+ halting(callback_sequence, user_callback, halted_lambda, filter)
+ end
+ end
- if !halted && user_conditions.all? { |c| c.call(target, value) }
- result_lambda = -> { user_callback.call target, value }
- env.halted = halted_lambda.call(target, result_lambda)
- if env.halted
- target.send :halted_callback_hook, filter
+ def self.halting_and_conditional(callback_sequence, user_callback, user_conditions, halted_lambda, filter)
+ callback_sequence.before do |env|
+ target = env.target
+ value = env.value
+ halted = env.halted
+
+ if !halted && user_conditions.all? { |c| c.call(target, value) }
+ result_lambda = -> { user_callback.call target, value }
+ env.halted = halted_lambda.call(target, result_lambda)
+ if env.halted
+ target.send :halted_callback_hook, filter
+ end
end
- end
- env
+ env
+ end
end
- end
- private_class_method :halting_and_conditional
+ private_class_method :halting_and_conditional
- def self.halting(callback_sequence, user_callback, halted_lambda, filter)
- callback_sequence.before do |env|
- target = env.target
- value = env.value
- halted = env.halted
+ def self.halting(callback_sequence, user_callback, halted_lambda, filter)
+ callback_sequence.before do |env|
+ target = env.target
+ value = env.value
+ halted = env.halted
- unless halted
- result_lambda = -> { user_callback.call target, value }
- env.halted = halted_lambda.call(target, result_lambda)
+ unless halted
+ result_lambda = -> { user_callback.call target, value }
+ env.halted = halted_lambda.call(target, result_lambda)
- if env.halted
- target.send :halted_callback_hook, filter
+ if env.halted
+ target.send :halted_callback_hook, filter
+ end
end
- end
- env
+ env
+ end
end
+ private_class_method :halting
end
- private_class_method :halting
-
- def self.conditional(callback_sequence, user_callback, user_conditions)
- callback_sequence.before do |env|
- target = env.target
- value = env.value
- if user_conditions.all? { |c| c.call(target, value) }
- user_callback.call target, value
+ class After
+ def self.build(callback_sequence, user_callback, user_conditions, chain_config)
+ if chain_config[:skip_after_callbacks_if_terminated]
+ if user_conditions.any?
+ halting_and_conditional(callback_sequence, user_callback, user_conditions)
+ else
+ halting(callback_sequence, user_callback)
+ end
+ else
+ if user_conditions.any?
+ conditional callback_sequence, user_callback, user_conditions
+ else
+ simple callback_sequence, user_callback
+ end
end
-
- env
end
- end
- private_class_method :conditional
- def self.simple(callback_sequence, user_callback)
- callback_sequence.before do |env|
- user_callback.call env.target, env.value
+ def self.halting_and_conditional(callback_sequence, user_callback, user_conditions)
+ callback_sequence.after do |env|
+ target = env.target
+ value = env.value
+ halted = env.halted
- env
- end
- end
- private_class_method :simple
- end
+ if !halted && user_conditions.all? { |c| c.call(target, value) }
+ user_callback.call target, value
+ end
- class After
- def self.build(callback_sequence, user_callback, user_conditions, chain_config)
- if chain_config[:skip_after_callbacks_if_terminated]
- if chain_config.key?(:terminator) && user_conditions.any?
- halting_and_conditional(callback_sequence, user_callback, user_conditions)
- elsif chain_config.key?(:terminator)
- halting(callback_sequence, user_callback)
- elsif user_conditions.any?
- conditional callback_sequence, user_callback, user_conditions
- else
- simple callback_sequence, user_callback
- end
- else
- if user_conditions.any?
- conditional callback_sequence, user_callback, user_conditions
- else
- simple callback_sequence, user_callback
+ env
end
end
- end
+ private_class_method :halting_and_conditional
- def self.halting_and_conditional(callback_sequence, user_callback, user_conditions)
- callback_sequence.after do |env|
- target = env.target
- value = env.value
- halted = env.halted
+ def self.halting(callback_sequence, user_callback)
+ callback_sequence.after do |env|
+ unless env.halted
+ user_callback.call env.target, env.value
+ end
- if !halted && user_conditions.all? { |c| c.call(target, value) }
- user_callback.call target, value
+ env
end
+ end
+ private_class_method :halting
+
+ def self.conditional(callback_sequence, user_callback, user_conditions)
+ callback_sequence.after do |env|
+ target = env.target
+ value = env.value
+
+ if user_conditions.all? { |c| c.call(target, value) }
+ user_callback.call target, value
+ end
- env
+ env
+ end
end
- end
- private_class_method :halting_and_conditional
+ private_class_method :conditional
- def self.halting(callback_sequence, user_callback)
- callback_sequence.after do |env|
- unless env.halted
+ def self.simple(callback_sequence, user_callback)
+ callback_sequence.after do |env|
user_callback.call env.target, env.value
- end
- env
+ env
+ end
end
+ private_class_method :simple
end
- private_class_method :halting
+ end
- def self.conditional(callback_sequence, user_callback, user_conditions)
- callback_sequence.after do |env|
- target = env.target
- value = env.value
+ class Callback #:nodoc:#
+ def self.build(chain, filter, kind, options)
+ if filter.is_a?(String)
+ raise ArgumentError, <<-MSG.squish
+ Passing string to define a callback is not supported. See the `.set_callback`
+ documentation to see supported values.
+ MSG
+ end
- if user_conditions.all? { |c| c.call(target, value) }
- user_callback.call target, value
- end
+ new chain.name, filter, kind, options, chain.config
+ end
- env
- end
+ attr_accessor :kind, :name
+ attr_reader :chain_config
+
+ def initialize(name, filter, kind, options, chain_config)
+ @chain_config = chain_config
+ @name = name
+ @kind = kind
+ @filter = filter
+ @key = compute_identifier filter
+ @if = check_conditionals(Array(options[:if]))
+ @unless = check_conditionals(Array(options[:unless]))
end
- private_class_method :conditional
- def self.simple(callback_sequence, user_callback)
- callback_sequence.after do |env|
- user_callback.call env.target, env.value
+ def filter; @key; end
+ def raw_filter; @filter; end
- env
- end
+ def merge_conditional_options(chain, if_option:, unless_option:)
+ options = {
+ if: @if.dup,
+ unless: @unless.dup
+ }
+
+ options[:if].concat Array(unless_option)
+ options[:unless].concat Array(if_option)
+
+ self.class.build chain, @filter, @kind, options
end
- private_class_method :simple
- end
- class Around
- def self.build(callback_sequence, user_callback, user_conditions, chain_config)
- if chain_config.key?(:terminator) && user_conditions.any?
- halting_and_conditional(callback_sequence, user_callback, user_conditions)
- elsif chain_config.key? :terminator
- halting(callback_sequence, user_callback)
- elsif user_conditions.any?
- conditional(callback_sequence, user_callback, user_conditions)
+ def matches?(_kind, _filter)
+ @kind == _kind && filter == _filter
+ end
+
+ def duplicates?(other)
+ case @filter
+ when Symbol
+ matches?(other.kind, other.filter)
else
- simple(callback_sequence, user_callback)
+ false
end
end
- def self.halting_and_conditional(callback_sequence, user_callback, user_conditions)
- callback_sequence.around do |env, &run|
- target = env.target
- value = env.value
- halted = env.halted
-
- if !halted && user_conditions.all? { |c| c.call(target, value) }
- user_callback.call(target, value) {
- run.call.value
- }
- env
- else
- run.call
- end
+ # Wraps code with filter
+ def apply(callback_sequence)
+ user_conditions = conditions_lambdas
+ user_callback = CallTemplate.build(@filter, self)
+
+ case kind
+ when :before
+ Filters::Before.build(callback_sequence, user_callback.make_lambda, user_conditions, chain_config, @filter)
+ when :after
+ Filters::After.build(callback_sequence, user_callback.make_lambda, user_conditions, chain_config)
+ when :around
+ callback_sequence.around(user_callback, user_conditions)
end
end
- private_class_method :halting_and_conditional
- def self.halting(callback_sequence, user_callback)
- callback_sequence.around do |env, &run|
- target = env.target
- value = env.value
+ def current_scopes
+ Array(chain_config[:scope]).map { |s| public_send(s) }
+ end
- if env.halted
- run.call
- else
- user_callback.call(target, value) {
- run.call.value
- }
- env
+ private
+ def check_conditionals(conditionals)
+ if conditionals.any? { |c| c.is_a?(String) }
+ raise ArgumentError, <<-MSG.squish
+ Passing string to be evaluated in :if and :unless conditional
+ options is not supported. Pass a symbol for an instance method,
+ or a lambda, proc or block, instead.
+ MSG
end
- end
- end
- private_class_method :halting
- def self.conditional(callback_sequence, user_callback, user_conditions)
- callback_sequence.around do |env, &run|
- target = env.target
- value = env.value
+ conditionals
+ end
- if user_conditions.all? { |c| c.call(target, value) }
- user_callback.call(target, value) {
- run.call.value
- }
- env
+ def compute_identifier(filter)
+ case filter
+ when ::Proc
+ filter.object_id
else
- run.call
+ filter
end
end
- end
- private_class_method :conditional
- def self.simple(callback_sequence, user_callback)
- callback_sequence.around do |env, &run|
- user_callback.call(env.target, env.value) {
- run.call.value
- }
- env
+ def conditions_lambdas
+ @if.map { |c| CallTemplate.build(c, self).make_lambda } +
+ @unless.map { |c| CallTemplate.build(c, self).inverted_lambda }
end
- end
- private_class_method :simple
end
- end
- class Callback #:nodoc:#
- def self.build(chain, filter, kind, options)
- new chain.name, filter, kind, options, chain.config
- end
+ # A future invocation of user-supplied code (either as a callback,
+ # or a condition filter).
+ class CallTemplate # :nodoc:
+ def initialize(target, method, arguments, block)
+ @override_target = target
+ @method_name = method
+ @arguments = arguments
+ @override_block = block
+ end
- attr_accessor :kind, :name
- attr_reader :chain_config
-
- def initialize(name, filter, kind, options, chain_config)
- @chain_config = chain_config
- @name = name
- @kind = kind
- @filter = filter
- @key = compute_identifier filter
- @if = Array(options[:if])
- @unless = Array(options[:unless])
- end
+ # Return the parts needed to make this call, with the given
+ # input values.
+ #
+ # Returns an array of the form:
+ #
+ # [target, block, method, *arguments]
+ #
+ # This array can be used as such:
+ #
+ # target.send(method, *arguments, &block)
+ #
+ # The actual invocation is left up to the caller to minimize
+ # call stack pollution.
+ def expand(target, value, block)
+ result = @arguments.map { |arg|
+ case arg
+ when :value; value
+ when :target; target
+ when :block; block || raise(ArgumentError)
+ end
+ }
- def filter; @key; end
- def raw_filter; @filter; end
+ result.unshift @method_name
+ result.unshift @override_block || block
+ result.unshift @override_target || target
- def merge_conditional_options(chain, if_option:, unless_option:)
- options = {
- :if => @if.dup,
- :unless => @unless.dup
- }
+ # target, block, method, *arguments = result
+ # target.send(method, *arguments, &block)
+ result
+ end
- options[:if].concat Array(unless_option)
- options[:unless].concat Array(if_option)
+ # Return a lambda that will make this call when given the input
+ # values.
+ def make_lambda
+ lambda do |target, value, &block|
+ target, block, method, *arguments = expand(target, value, block)
+ target.send(method, *arguments, &block)
+ end
+ end
- self.class.build chain, @filter, @kind, options
- end
+ # Return a lambda that will make this call when given the input
+ # values, but then return the boolean inverse of that result.
+ def inverted_lambda
+ lambda do |target, value, &block|
+ target, block, method, *arguments = expand(target, value, block)
+ ! target.send(method, *arguments, &block)
+ end
+ end
- def matches?(_kind, _filter)
- @kind == _kind && filter == _filter
- end
+ # Filters support:
+ #
+ # Symbols:: A method to call.
+ # Procs:: A proc to call with the object.
+ # Objects:: An object with a <tt>before_foo</tt> method on it to call.
+ #
+ # All of these objects are converted into a CallTemplate and handled
+ # the same after this point.
+ def self.build(filter, callback)
+ case filter
+ when Symbol
+ new(nil, filter, [], nil)
+ when Conditionals::Value
+ new(filter, :call, [:target, :value], nil)
+ when ::Proc
+ if filter.arity > 1
+ new(nil, :instance_exec, [:target, :block], filter)
+ elsif filter.arity > 0
+ new(nil, :instance_exec, [:target], filter)
+ else
+ new(nil, :instance_exec, [], filter)
+ end
+ else
+ method_to_call = callback.current_scopes.join("_")
- def duplicates?(other)
- case @filter
- when Symbol, String
- matches?(other.kind, other.filter)
- else
- false
+ new(filter, method_to_call, [:target], nil)
+ end
end
end
- # Wraps code with filter
- def apply(callback_sequence)
- user_conditions = conditions_lambdas
- user_callback = make_lambda @filter
+ # Execute before and after filters in a sequence instead of
+ # chaining them with nested lambda calls, see:
+ # https://github.com/rails/rails/issues/18011
+ class CallbackSequence # :nodoc:
+ def initialize(nested = nil, call_template = nil, user_conditions = nil)
+ @nested = nested
+ @call_template = call_template
+ @user_conditions = user_conditions
- case kind
- when :before
- Filters::Before.build(callback_sequence, user_callback, user_conditions, chain_config, @filter)
- when :after
- Filters::After.build(callback_sequence, user_callback, user_conditions, chain_config)
- when :around
- Filters::Around.build(callback_sequence, user_callback, user_conditions, chain_config)
+ @before = []
+ @after = []
end
- end
-
- private
- def invert_lambda(l)
- lambda { |*args, &blk| !l.call(*args, &blk) }
- end
+ def before(&before)
+ @before.unshift(before)
+ self
+ end
- # Filters support:
- #
- # Symbols:: A method to call.
- # Strings:: Some content to evaluate.
- # Procs:: A proc to call with the object.
- # Objects:: An object with a <tt>before_foo</tt> method on it to call.
- #
- # All of these objects are converted into a lambda and handled
- # the same after this point.
- def make_lambda(filter)
- case filter
- when Symbol
- lambda { |target, _, &blk| target.send filter, &blk }
- when String
- l = eval "lambda { |value| #{filter} }"
- lambda { |target, value| target.instance_exec(value, &l) }
- when Conditionals::Value then filter
- when ::Proc
- if filter.arity > 1
- return lambda { |target, _, &block|
- raise ArgumentError unless block
- target.instance_exec(target, block, &filter)
- }
- end
-
- if filter.arity <= 0
- lambda { |target, _| target.instance_exec(&filter) }
- else
- lambda { |target, _| target.instance_exec(target, &filter) }
- end
- else
- scopes = Array(chain_config[:scope])
- method_to_call = scopes.map{ |s| public_send(s) }.join("_")
+ def after(&after)
+ @after.push(after)
+ self
+ end
- lambda { |target, _, &blk|
- filter.public_send method_to_call, target, &blk
- }
+ def around(call_template, user_conditions)
+ CallbackSequence.new(self, call_template, user_conditions)
end
- end
- def compute_identifier(filter)
- case filter
- when String, ::Proc
- filter.object_id
- else
- filter
+ def skip?(arg)
+ arg.halted || !@user_conditions.all? { |c| c.call(arg.target, arg.value) }
end
- end
- def conditions_lambdas
- @if.map { |c| make_lambda c } +
- @unless.map { |c| invert_lambda make_lambda c }
- end
- end
+ attr_reader :nested
- # Execute before and after filters in a sequence instead of
- # chaining them with nested lambda calls, see:
- # https://github.com/rails/rails/issues/18011
- class CallbackSequence
- def initialize(&call)
- @call = call
- @before = []
- @after = []
- end
+ def final?
+ !@call_template
+ end
- def before(&before)
- @before.unshift(before)
- self
- end
+ def expand_call_template(arg, block)
+ @call_template.expand(arg.target, arg.value, block)
+ end
- def after(&after)
- @after.push(after)
- self
- end
+ def invoke_before(arg)
+ @before.each { |b| b.call(arg) }
+ end
- def around(&around)
- CallbackSequence.new do |arg|
- around.call(arg) {
- self.call(arg)
- }
+ def invoke_after(arg)
+ @after.each { |a| a.call(arg) }
end
end
- def call(arg)
- @before.each { |b| b.call(arg) }
- value = @call.call(arg)
- @after.each { |a| a.call(arg) }
- value
- end
- end
+ class CallbackChain #:nodoc:#
+ include Enumerable
- # An Array with a compile method.
- class CallbackChain #:nodoc:#
- include Enumerable
-
- attr_reader :name, :config
-
- # If true, any callback 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+.
- class_attribute :halt_and_display_warning_on_return_false
- self.halt_and_display_warning_on_return_false = true
-
- def initialize(name, config)
- @name = name
- @config = {
- scope: [:kind],
- terminator: default_terminator
- }.merge!(config)
- @chain = []
- @callbacks = nil
- @mutex = Mutex.new
- end
+ attr_reader :name, :config
- def each(&block); @chain.each(&block); end
- def index(o); @chain.index(o); end
- def empty?; @chain.empty?; end
+ def initialize(name, config)
+ @name = name
+ @config = {
+ scope: [:kind],
+ terminator: default_terminator
+ }.merge!(config)
+ @chain = []
+ @callbacks = nil
+ @mutex = Mutex.new
+ end
- def insert(index, o)
- @callbacks = nil
- @chain.insert(index, o)
- end
+ def each(&block); @chain.each(&block); end
+ def index(o); @chain.index(o); end
+ def empty?; @chain.empty?; end
- def delete(o)
- @callbacks = nil
- @chain.delete(o)
- end
+ def insert(index, o)
+ @callbacks = nil
+ @chain.insert(index, o)
+ end
- def clear
- @callbacks = nil
- @chain.clear
- self
- end
+ def delete(o)
+ @callbacks = nil
+ @chain.delete(o)
+ end
- def initialize_copy(other)
- @callbacks = nil
- @chain = other.chain.dup
- @mutex = Mutex.new
- end
+ def clear
+ @callbacks = nil
+ @chain.clear
+ self
+ end
- def compile
- @callbacks || @mutex.synchronize do
- final_sequence = CallbackSequence.new { |env| Filters::ENDING.call(env) }
- @callbacks ||= @chain.reverse.inject(final_sequence) do |callback_sequence, callback|
- callback.apply callback_sequence
+ def initialize_copy(other)
+ @callbacks = nil
+ @chain = other.chain.dup
+ @mutex = Mutex.new
+ end
+
+ def compile
+ @callbacks || @mutex.synchronize do
+ final_sequence = CallbackSequence.new
+ @callbacks ||= @chain.reverse.inject(final_sequence) do |callback_sequence, callback|
+ callback.apply callback_sequence
+ end
end
end
- end
- def append(*callbacks)
- callbacks.each { |c| append_one(c) }
- end
+ def append(*callbacks)
+ callbacks.each { |c| append_one(c) }
+ end
- def prepend(*callbacks)
- callbacks.each { |c| prepend_one(c) }
- end
+ def prepend(*callbacks)
+ callbacks.each { |c| prepend_one(c) }
+ end
- protected
- def chain; @chain; end
+ protected
+ attr_reader :chain
- private
+ private
- def append_one(callback)
- @callbacks = nil
- remove_duplicates(callback)
- @chain.push(callback)
- end
+ def append_one(callback)
+ @callbacks = nil
+ remove_duplicates(callback)
+ @chain.push(callback)
+ end
- def prepend_one(callback)
- @callbacks = nil
- remove_duplicates(callback)
- @chain.unshift(callback)
- end
+ def prepend_one(callback)
+ @callbacks = nil
+ remove_duplicates(callback)
+ @chain.unshift(callback)
+ end
- def remove_duplicates(callback)
- @callbacks = nil
- @chain.delete_if { |c| callback.duplicates?(c) }
- end
+ def remove_duplicates(callback)
+ @callbacks = nil
+ @chain.delete_if { |c| callback.duplicates?(c) }
+ end
- def default_terminator
- Proc.new do |target, result_lambda|
- terminate = true
- catch(:abort) do
- result = result_lambda.call if result_lambda.is_a?(Proc)
- if halt_and_display_warning_on_return_false && result == false
- display_deprecation_warning_for_false_terminator
- else
- terminate = false
+ def default_terminator
+ Proc.new do |target, result_lambda|
+ terminate = true
+ catch(:abort) do
+ result_lambda.call
+ terminate = false
+ end
+ terminate
end
end
- terminate
- end
end
- def display_deprecation_warning_for_false_terminator
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Returning `false` in a callback will not implicitly halt a callback chain in the next release of Rails.
- To explicitly halt a callback chain, please use `throw :abort` instead.
- MSG
- end
- end
-
- module ClassMethods
- def normalize_callback_params(filters, block) # :nodoc:
- type = CALLBACK_FILTER_TYPES.include?(filters.first) ? filters.shift : :before
- options = filters.extract_options!
- filters.unshift(block) if block
- [type, filters, options.dup]
- end
+ module ClassMethods
+ def normalize_callback_params(filters, block) # :nodoc:
+ type = CALLBACK_FILTER_TYPES.include?(filters.first) ? filters.shift : :before
+ options = filters.extract_options!
+ filters.unshift(block) if block
+ [type, filters, options.dup]
+ end
- # This is used internally to append, prepend and skip callbacks to the
- # CallbackChain.
- def __update_callbacks(name) #:nodoc:
- ([self] + ActiveSupport::DescendantsTracker.descendants(self)).reverse_each do |target|
- chain = target.get_callbacks name
- yield target, chain.dup
+ # This is used internally to append, prepend and skip callbacks to the
+ # CallbackChain.
+ def __update_callbacks(name) #:nodoc:
+ ([self] + ActiveSupport::DescendantsTracker.descendants(self)).reverse_each do |target|
+ chain = target.get_callbacks name
+ yield target, chain.dup
+ end
end
- end
- # Install a callback for the given event.
- #
- # set_callback :save, :before, :before_meth
- # set_callback :save, :after, :after_meth, if: :condition
- # set_callback :save, :around, ->(r, block) { stuff; result = block.call; stuff }
- #
- # The second argument indicates whether the callback is to be run +:before+,
- # +:after+, or +:around+ the event. If omitted, +:before+ is assumed. This
- # means the first example above can also be written as:
- #
- # set_callback :save, :before_meth
- #
- # 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; 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
- # an argument.
- #
- # Before and around callbacks are called in the order that they are set;
- # after callbacks are called in the reverse order.
- #
- # Around callbacks can access the return value from the event, if it
- # wasn't halted, from the +yield+ call.
- #
- # ===== Options
- #
- # * <tt>:if</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 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>: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)
- self_chain = get_callbacks name
- mapped = filters.map do |filter|
- Callback.build(self_chain, filter, type, options)
- end
-
- __update_callbacks(name) do |target, chain|
- options[:prepend] ? chain.prepend(*mapped) : chain.append(*mapped)
- target.set_callbacks name, chain
+ # Install a callback for the given event.
+ #
+ # set_callback :save, :before, :before_method
+ # set_callback :save, :after, :after_method, if: :condition
+ # set_callback :save, :around, ->(r, block) { stuff; result = block.call; stuff }
+ #
+ # The second argument indicates whether the callback is to be run +:before+,
+ # +:after+, or +:around+ the event. If omitted, +:before+ is assumed. This
+ # means the first example above can also be written as:
+ #
+ # set_callback :save, :before_method
+ #
+ # The callback can be specified as a symbol naming an instance method; as a
+ # 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
+ # an argument.
+ #
+ # Before and around callbacks are called in the order that they are set;
+ # after callbacks are called in the reverse order.
+ #
+ # Around callbacks can access the return value from the event, if it
+ # wasn't halted, from the +yield+ call.
+ #
+ # ===== Options
+ #
+ # * <tt>:if</tt> - A symbol or an array of symbols, each naming an instance
+ # method or a proc; the callback will be called only when they all return
+ # a true value.
+ #
+ # If a proc is given, its body is evaluated in the context of the
+ # current object. It can also optionally accept the current object as
+ # an argument.
+ # * <tt>:unless</tt> - A symbol or an array of symbols, each naming an
+ # instance method or a proc; the callback will be called only when they
+ # all return a false value.
+ #
+ # If a proc is given, its body is evaluated in the context of the
+ # current object. It can also optionally accept the current object as
+ # an argument.
+ # * <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)
+
+ self_chain = get_callbacks name
+ mapped = filters.map do |filter|
+ Callback.build(self_chain, filter, type, options)
+ end
+
+ __update_callbacks(name) do |target, chain|
+ options[:prepend] ? chain.prepend(*mapped) : chain.append(*mapped)
+ target.set_callbacks name, chain
+ end
end
- end
- # Skip a previously set callback. Like +set_callback+, <tt>:if</tt> or
- # <tt>:unless</tt> options may be passed in order to control when the
- # callback is skipped.
- #
- # class Writer < Person
- # skip_callback :validate, :before, :check_membership, if: -> { self.age > 18 }
- # end
- #
- # An <tt>ArgumentError</tt> will be raised if the callback has not
- # 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)
- options[:raise] = true unless options.key?(:raise)
-
- __update_callbacks(name) do |target, chain|
- filters.each do |filter|
- callback = chain.find {|c| c.matches?(type, filter) }
-
- if !callback && options[:raise]
- raise ArgumentError, "#{type.to_s.capitalize} #{name} callback #{filter.inspect} has not been defined"
- end
+ # Skip a previously set callback. Like +set_callback+, <tt>:if</tt> or
+ # <tt>:unless</tt> options may be passed in order to control when the
+ # callback is skipped.
+ #
+ # class Writer < Person
+ # skip_callback :validate, :before, :check_membership, if: -> { age > 18 }
+ # end
+ #
+ # An <tt>ArgumentError</tt> will be raised if the callback has not
+ # 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)
+
+ options[:raise] = true unless options.key?(:raise)
+
+ __update_callbacks(name) do |target, chain|
+ filters.each do |filter|
+ callback = chain.find { |c| c.matches?(type, filter) }
+
+ if !callback && options[:raise]
+ raise ArgumentError, "#{type.to_s.capitalize} #{name} callback #{filter.inspect} has not been defined"
+ end
- if callback && (options.key?(:if) || options.key?(:unless))
- new_callback = callback.merge_conditional_options(chain, if_option: options[:if], unless_option: options[:unless])
- chain.insert(chain.index(callback), new_callback)
- end
+ if callback && (options.key?(:if) || options.key?(:unless))
+ new_callback = callback.merge_conditional_options(chain, if_option: options[:if], unless_option: options[:unless])
+ chain.insert(chain.index(callback), new_callback)
+ end
- chain.delete(callback)
+ chain.delete(callback)
+ end
+ target.set_callbacks name, chain
end
- target.set_callbacks name, chain
end
- end
- # Remove all set callbacks for the given event.
- def reset_callbacks(name)
- callbacks = get_callbacks name
+ # Remove all set callbacks for the given event.
+ def reset_callbacks(name)
+ callbacks = get_callbacks name
- ActiveSupport::DescendantsTracker.descendants(self).each do |target|
- chain = target.get_callbacks(name).dup
- callbacks.each { |c| chain.delete(c) }
- target.set_callbacks name, chain
+ ActiveSupport::DescendantsTracker.descendants(self).each do |target|
+ chain = target.get_callbacks(name).dup
+ callbacks.each { |c| chain.delete(c) }
+ target.set_callbacks name, chain
+ end
+
+ set_callbacks(name, callbacks.dup.clear)
end
- self.set_callbacks name, callbacks.dup.clear
- end
+ # Define sets of events in the object life cycle that support callbacks.
+ #
+ # define_callbacks :validate
+ # define_callbacks :initialize, :save, :destroy
+ #
+ # ===== Options
+ #
+ # * <tt>:terminator</tt> - Determines when a before filter will halt the
+ # callback chain, preventing following before and around callbacks from
+ # being called and the event from being triggered.
+ # This should be a lambda to be executed.
+ # The current object and the result lambda of the callback will be provided
+ # to the terminator lambda.
+ #
+ # define_callbacks :validate, terminator: ->(target, result_lambda) { result_lambda.call == false }
+ #
+ # In this example, if any before validate callbacks returns +false+,
+ # any successive before and around callback is not executed.
+ #
+ # The default terminator halts the chain when a callback throws +:abort+.
+ #
+ # * <tt>:skip_after_callbacks_if_terminated</tt> - Determines if after
+ # callbacks should be terminated by the <tt>:terminator</tt> option. By
+ # default after callbacks are executed no matter if callback chain was
+ # terminated or not. This option has no effect if <tt>:terminator</tt>
+ # option is set to +nil+.
+ #
+ # * <tt>:scope</tt> - Indicates which methods should be executed when an
+ # object is used as a callback.
+ #
+ # class Audit
+ # def before(caller)
+ # puts 'Audit: before is called'
+ # end
+ #
+ # def before_save(caller)
+ # puts 'Audit: before_save is called'
+ # end
+ # end
+ #
+ # class Account
+ # include ActiveSupport::Callbacks
+ #
+ # define_callbacks :save
+ # set_callback :save, :before, Audit.new
+ #
+ # def save
+ # run_callbacks :save do
+ # puts 'save in main'
+ # end
+ # end
+ # end
+ #
+ # In the above case whenever you save an account the method
+ # <tt>Audit#before</tt> will be called. On the other hand
+ #
+ # define_callbacks :save, scope: [:kind, :name]
+ #
+ # would trigger <tt>Audit#before_save</tt> instead. That's constructed
+ # by calling <tt>#{kind}_#{name}</tt> on the given instance. In this
+ # case "kind" is "before" and "name" is "save". In this context +:kind+
+ # and +:name+ have special meanings: +:kind+ refers to the kind of
+ # callback (before/after/around) and +:name+ refers to the method on
+ # which callbacks are being defined.
+ #
+ # A declaration like
+ #
+ # define_callbacks :save, scope: [:name]
+ #
+ # would call <tt>Audit#save</tt>.
+ #
+ # ===== Notes
+ #
+ # +names+ passed to +define_callbacks+ must not end with
+ # <tt>!</tt>, <tt>?</tt> or <tt>=</tt>.
+ #
+ # Calling +define_callbacks+ multiple times with the same +names+ will
+ # overwrite previous callbacks registered with +set_callback+.
+ def define_callbacks(*names)
+ options = names.extract_options!
+
+ names.each do |name|
+ name = name.to_sym
+
+ ([self] + ActiveSupport::DescendantsTracker.descendants(self)).each do |target|
+ target.set_callbacks name, CallbackChain.new(name, options)
+ end
+
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def _run_#{name}_callbacks(&block)
+ run_callbacks #{name.inspect}, &block
+ end
- # Define sets of events in the object life cycle that support callbacks.
- #
- # define_callbacks :validate
- # define_callbacks :initialize, :save, :destroy
- #
- # ===== Options
- #
- # * <tt>:terminator</tt> - Determines when a before filter will halt the
- # callback chain, preventing following before and around callbacks from
- # being called and the event from being triggered.
- # This should be a lambda to be executed.
- # The current object and the return result of the callback will be called
- # with the lambda.
- #
- # define_callbacks :validate, terminator: ->(target, result) { result == false }
- #
- # In this example, if any before validate callbacks returns +false+,
- # any successive before and around callback is not executed.
- # Defaults to +false+, meaning no value halts the chain.
- #
- # * <tt>:skip_after_callbacks_if_terminated</tt> - Determines if after
- # callbacks should be terminated by the <tt>:terminator</tt> option. By
- # default after callbacks are executed no matter if callback chain was
- # terminated or not. This option makes sense only when <tt>:terminator</tt>
- # option is specified.
- #
- # * <tt>:scope</tt> - Indicates which methods should be executed when an
- # object is used as a callback.
- #
- # class Audit
- # def before(caller)
- # puts 'Audit: before is called'
- # end
- #
- # def before_save(caller)
- # puts 'Audit: before_save is called'
- # end
- # end
- #
- # class Account
- # include ActiveSupport::Callbacks
- #
- # define_callbacks :save
- # set_callback :save, :before, Audit.new
- #
- # def save
- # run_callbacks :save do
- # puts 'save in main'
- # end
- # end
- # end
- #
- # In the above case whenever you save an account the method
- # <tt>Audit#before</tt> will be called. On the other hand
- #
- # define_callbacks :save, scope: [:kind, :name]
- #
- # would trigger <tt>Audit#before_save</tt> instead. That's constructed
- # by calling <tt>#{kind}_#{name}</tt> on the given instance. In this
- # case "kind" is "before" and "name" is "save". In this context +:kind+
- # and +:name+ have special meanings: +:kind+ refers to the kind of
- # callback (before/after/around) and +:name+ refers to the method on
- # which callbacks are being defined.
- #
- # A declaration like
- #
- # define_callbacks :save, scope: [:name]
- #
- # would call <tt>Audit#save</tt>.
- #
- # NOTE: +method_name+ passed to `define_model_callbacks` must not end with
- # `!`, `?` or `=`.
- def define_callbacks(*names)
- options = names.extract_options!
-
- names.each do |name|
- class_attribute "_#{name}_callbacks"
- set_callbacks name, CallbackChain.new(name, options)
+ def self._#{name}_callbacks
+ get_callbacks(#{name.inspect})
+ end
+
+ def self._#{name}_callbacks=(value)
+ set_callbacks(#{name.inspect}, value)
+ end
+
+ def _#{name}_callbacks
+ __callbacks[#{name.inspect}]
+ end
+ RUBY
+ end
end
- end
- protected
+ protected
- def get_callbacks(name)
- send "_#{name}_callbacks"
- end
+ def get_callbacks(name) # :nodoc:
+ __callbacks[name.to_sym]
+ end
- def set_callbacks(name, callbacks)
- send "_#{name}_callbacks=", callbacks
+ def set_callbacks(name, callbacks) # :nodoc:
+ self.__callbacks = __callbacks.merge(name.to_sym => callbacks)
+ end
end
- end
end
end
diff --git a/activesupport/lib/active_support/concern.rb b/activesupport/lib/active_support/concern.rb
index 0403eb70ca..b0a0d845e5 100644
--- a/activesupport/lib/active_support/concern.rb
+++ b/activesupport/lib/active_support/concern.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveSupport
# A typical module looks like this:
#
@@ -111,7 +113,7 @@ module ActiveSupport
def append_features(base)
if base.instance_variable_defined?(:@_dependencies)
base.instance_variable_get(:@_dependencies) << self
- return false
+ false
else
return false if base < self
@_dependencies.each { |dep| base.include(dep) }
diff --git a/activesupport/lib/active_support/concurrency/latch.rb b/activesupport/lib/active_support/concurrency/latch.rb
deleted file mode 100644
index 1507de433e..0000000000
--- a/activesupport/lib/active_support/concurrency/latch.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-require 'thread'
-require 'monitor'
-
-module ActiveSupport
- module Concurrency
- class Latch
- def initialize(count = 1)
- @count = count
- @lock = Monitor.new
- @cv = @lock.new_cond
- end
-
- def release
- @lock.synchronize do
- @count -= 1 if @count > 0
- @cv.broadcast if @count.zero?
- end
- end
-
- def await
- @lock.synchronize do
- @cv.wait_while { @count > 0 }
- end
- end
- end
- end
-end
diff --git a/activesupport/lib/active_support/concurrency/load_interlock_aware_monitor.rb b/activesupport/lib/active_support/concurrency/load_interlock_aware_monitor.rb
new file mode 100644
index 0000000000..a8455c0048
--- /dev/null
+++ b/activesupport/lib/active_support/concurrency/load_interlock_aware_monitor.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require "monitor"
+
+module ActiveSupport
+ module Concurrency
+ # A monitor that will permit dependency loading while blocked waiting for
+ # the lock.
+ class LoadInterlockAwareMonitor < Monitor
+ # Enters an exclusive section, but allows dependency loading while blocked
+ def mon_enter
+ mon_try_enter ||
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads { super }
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/concurrency/share_lock.rb b/activesupport/lib/active_support/concurrency/share_lock.rb
index f1c6230084..f18ccf1c88 100644
--- a/activesupport/lib/active_support/concurrency/share_lock.rb
+++ b/activesupport/lib/active_support/concurrency/share_lock.rb
@@ -1,17 +1,13 @@
-require 'thread'
-require 'monitor'
+# frozen_string_literal: true
+
+require "thread"
+require "monitor"
module ActiveSupport
module Concurrency
# A share/exclusive lock, otherwise known as a read/write lock.
#
# https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock
- #--
- # Note that a pending Exclusive lock attempt does not block incoming
- # Share requests (i.e., we are "read-preferring"). That seems
- # consistent with the behavior of +loose_upgrades+, but may be the
- # wrong choice otherwise: it nominally reduces the possibility of
- # deadlock by risking starvation instead.
class ShareLock
include MonitorMixin
@@ -19,48 +15,75 @@ module ActiveSupport
# we need exclusive locks to be reentrant, and we need to be able
# to upgrade share locks to exclusive.
+ def raw_state # :nodoc:
+ synchronize do
+ threads = @sleeping.keys | @sharing.keys | @waiting.keys
+ threads |= [@exclusive_thread] if @exclusive_thread
+
+ data = {}
+
+ threads.each do |thread|
+ purpose, compatible = @waiting[thread]
+
+ data[thread] = {
+ thread: thread,
+ sharing: @sharing[thread],
+ exclusive: @exclusive_thread == thread,
+ purpose: purpose,
+ compatible: compatible,
+ waiting: !!@waiting[thread],
+ sleeper: @sleeping[thread],
+ }
+ end
- # If +loose_upgrades+ is false (the default), then a thread that
- # is waiting on an Exclusive lock will continue to hold any Share
- # lock that it has already established. This is safer, but can
- # lead to deadlock.
- #
- # If +loose_upgrades+ is true, a thread waiting on an Exclusive
- # lock will temporarily relinquish its Share lock. Being less
- # strict, this behavior prevents some classes of deadlocks. For
- # many resources, loose upgrades are sufficient: if a thread is
- # awaiting a lock, it is not running any other code.
- attr_reader :loose_upgrades
-
- def initialize(loose_upgrades = false)
- @loose_upgrades = loose_upgrades
+ # NB: Yields while holding our *internal* synchronize lock,
+ # which is supposed to be used only for a few instructions at
+ # a time. This allows the caller to inspect additional state
+ # without things changing out from underneath, but would have
+ # disastrous effects upon normal operation. Fortunately, this
+ # method is only intended to be called when things have
+ # already gone wrong.
+ yield data
+ end
+ end
+ def initialize
super()
@cv = new_cond
@sharing = Hash.new(0)
+ @waiting = {}
+ @sleeping = {}
@exclusive_thread = nil
@exclusive_depth = 0
end
- # Returns false if +no_wait+ is specified and the lock is not
+ # Returns false if +no_wait+ is set and the lock is not
# immediately available. Otherwise, returns true after the lock
# has been acquired.
- def start_exclusive(no_wait=false)
+ #
+ # +purpose+ and +compatible+ work together; while this thread is
+ # waiting for the exclusive lock, it will yield its share (if any)
+ # to any other attempt whose +purpose+ appears in this attempt's
+ # +compatible+ list. This allows a "loose" upgrade, which, being
+ # less strict, prevents some classes of deadlocks.
+ #
+ # For many resources, loose upgrades are sufficient: if a thread
+ # is awaiting a lock, it is not running any other code. With
+ # +purpose+ matching, it is possible to yield only to other
+ # threads whose activity will not interfere.
+ def start_exclusive(purpose: nil, compatible: [], no_wait: false)
synchronize do
unless @exclusive_thread == Thread.current
- return false if no_wait && busy?
+ if busy_for_exclusive?(purpose)
+ return false if no_wait
- loose_shares = nil
- if @loose_upgrades
- loose_shares = @sharing.delete(Thread.current)
+ yield_shares(purpose: purpose, compatible: compatible, block_share: true) do
+ wait_for(:start_exclusive) { busy_for_exclusive?(purpose) }
+ end
end
-
- @cv.wait_while { busy? } if busy?
-
@exclusive_thread = Thread.current
- @sharing[Thread.current] = loose_shares if loose_shares
end
@exclusive_depth += 1
@@ -70,13 +93,19 @@ module ActiveSupport
# Relinquish the exclusive lock. Must only be called by the thread
# that called start_exclusive (and currently holds the lock).
- def stop_exclusive
+ def stop_exclusive(compatible: [])
synchronize do
raise "invalid unlock" if @exclusive_thread != Thread.current
@exclusive_depth -= 1
if @exclusive_depth == 0
@exclusive_thread = nil
+
+ if eligible_waiters?(compatible)
+ yield_shares(compatible: compatible, block_share: true) do
+ wait_for(:stop_exclusive) { @exclusive_thread || eligible_waiters?(compatible) }
+ end
+ end
@cv.broadcast
end
end
@@ -84,8 +113,16 @@ module ActiveSupport
def start_sharing
synchronize do
- if @exclusive_thread && @exclusive_thread != Thread.current
- @cv.wait_while { @exclusive_thread }
+ if @sharing[Thread.current] > 0 || @exclusive_thread == Thread.current
+ # We already hold a lock; nothing to wait for
+ elsif @waiting[Thread.current]
+ # We're nested inside a +yield_shares+ call: we'll resume as
+ # soon as there isn't an exclusive lock in our way
+ wait_for(:start_sharing) { @exclusive_thread }
+ else
+ # This is an initial / outermost share call: any outstanding
+ # requests for an exclusive lock get to go first
+ wait_for(:start_sharing) { busy_for_sharing?(false) }
end
@sharing[Thread.current] += 1
end
@@ -106,12 +143,14 @@ module ActiveSupport
# +no_wait+ is set and the lock is not immediately available,
# returns +nil+ without yielding. Otherwise, returns the result of
# the block.
- def exclusive(no_wait=false)
- if start_exclusive(no_wait)
+ #
+ # See +start_exclusive+ for other options.
+ def exclusive(purpose: nil, compatible: [], after_compatible: [], no_wait: false)
+ if start_exclusive(purpose: purpose, compatible: compatible, no_wait: no_wait)
begin
yield
ensure
- stop_exclusive
+ stop_exclusive(compatible: after_compatible)
end
end
end
@@ -126,13 +165,63 @@ module ActiveSupport
end
end
- private
+ # Temporarily give up all held Share locks while executing the
+ # supplied block, allowing any +compatible+ exclusive lock request
+ # to proceed.
+ def yield_shares(purpose: nil, compatible: [], block_share: false)
+ loose_shares = previous_wait = nil
+ synchronize do
+ if loose_shares = @sharing.delete(Thread.current)
+ if previous_wait = @waiting[Thread.current]
+ purpose = nil unless purpose == previous_wait[0]
+ compatible &= previous_wait[1]
+ end
+ compatible |= [false] unless block_share
+ @waiting[Thread.current] = [purpose, compatible]
+ end
- # Must be called within synchronize
- def busy?
- (@exclusive_thread && @exclusive_thread != Thread.current) ||
- @sharing.size > (@sharing[Thread.current] > 0 ? 1 : 0)
+ @cv.broadcast
+ end
+
+ begin
+ yield
+ ensure
+ synchronize do
+ wait_for(:yield_shares) { @exclusive_thread && @exclusive_thread != Thread.current }
+
+ if previous_wait
+ @waiting[Thread.current] = previous_wait
+ else
+ @waiting.delete Thread.current
+ end
+ @sharing[Thread.current] = loose_shares if loose_shares
+ end
+ end
end
+
+ private
+
+ # Must be called within synchronize
+ def busy_for_exclusive?(purpose)
+ busy_for_sharing?(purpose) ||
+ @sharing.size > (@sharing[Thread.current] > 0 ? 1 : 0)
+ end
+
+ def busy_for_sharing?(purpose)
+ (@exclusive_thread && @exclusive_thread != Thread.current) ||
+ @waiting.any? { |t, (_, c)| t != Thread.current && !c.include?(purpose) }
+ end
+
+ def eligible_waiters?(compatible)
+ @waiting.any? { |t, (p, _)| compatible.include?(p) && @waiting.all? { |t2, (_, c2)| t == t2 || c2.include?(p) } }
+ end
+
+ def wait_for(method)
+ @sleeping[Thread.current] = method
+ @cv.wait_while { yield }
+ ensure
+ @sleeping.delete Thread.current
+ end
end
end
end
diff --git a/activesupport/lib/active_support/configurable.rb b/activesupport/lib/active_support/configurable.rb
index 8256c325af..9acf674c40 100644
--- a/activesupport/lib/active_support/configurable.rb
+++ b/activesupport/lib/active_support/configurable.rb
@@ -1,6 +1,7 @@
-require 'active_support/concern'
-require 'active_support/ordered_options'
-require 'active_support/core_ext/array/extract_options'
+# frozen_string_literal: true
+
+require "active_support/concern"
+require "active_support/ordered_options"
module ActiveSupport
# Configurable provides a <tt>config</tt> method to store and retrieve
@@ -103,11 +104,9 @@ module ActiveSupport
# end
#
# User.hair_colors # => [:brown, :black, :blonde, :red]
- def config_accessor(*names)
- options = names.extract_options!
-
+ def config_accessor(*names, instance_reader: true, instance_writer: true, instance_accessor: true) # :doc:
names.each do |name|
- raise NameError.new('invalid config attribute name') unless name =~ /\A[_A-Za-z]\w*\z/
+ raise NameError.new("invalid config attribute name") unless /\A[_A-Za-z]\w*\z/.match?(name)
reader, reader_line = "def #{name}; config.#{name}; end", __LINE__
writer, writer_line = "def #{name}=(value); config.#{name} = value; end", __LINE__
@@ -115,9 +114,9 @@ module ActiveSupport
singleton_class.class_eval reader, __FILE__, reader_line
singleton_class.class_eval writer, __FILE__, writer_line
- unless options[:instance_accessor] == false
- class_eval reader, __FILE__, reader_line unless options[:instance_reader] == false
- class_eval writer, __FILE__, writer_line unless options[:instance_writer] == false
+ if instance_accessor
+ class_eval reader, __FILE__, reader_line if instance_reader
+ class_eval writer, __FILE__, writer_line if instance_writer
end
send("#{name}=", yield) if block_given?
end
@@ -145,4 +144,3 @@ module ActiveSupport
end
end
end
-
diff --git a/activesupport/lib/active_support/core_ext.rb b/activesupport/lib/active_support/core_ext.rb
index 52706c3d7a..f590605d84 100644
--- a/activesupport/lib/active_support/core_ext.rb
+++ b/activesupport/lib/active_support/core_ext.rb
@@ -1,4 +1,5 @@
-DEPRECATED_FILES = ["#{File.dirname(__FILE__)}/core_ext/struct.rb"]
-(Dir["#{File.dirname(__FILE__)}/core_ext/*.rb"] - DEPRECATED_FILES).each do |path|
+# frozen_string_literal: true
+
+Dir.glob(File.expand_path("core_ext/*.rb", __dir__)).each do |path|
require path
end
diff --git a/activesupport/lib/active_support/core_ext/array.rb b/activesupport/lib/active_support/core_ext/array.rb
index 7551551bd7..a2569c798b 100644
--- a/activesupport/lib/active_support/core_ext/array.rb
+++ b/activesupport/lib/active_support/core_ext/array.rb
@@ -1,7 +1,10 @@
-require 'active_support/core_ext/array/wrap'
-require 'active_support/core_ext/array/access'
-require 'active_support/core_ext/array/conversions'
-require 'active_support/core_ext/array/extract_options'
-require 'active_support/core_ext/array/grouping'
-require 'active_support/core_ext/array/prepend_and_append'
-require 'active_support/core_ext/array/inquiry'
+# frozen_string_literal: true
+
+require "active_support/core_ext/array/wrap"
+require "active_support/core_ext/array/access"
+require "active_support/core_ext/array/conversions"
+require "active_support/core_ext/array/extract"
+require "active_support/core_ext/array/extract_options"
+require "active_support/core_ext/array/grouping"
+require "active_support/core_ext/array/prepend_and_append"
+require "active_support/core_ext/array/inquiry"
diff --git a/activesupport/lib/active_support/core_ext/array/access.rb b/activesupport/lib/active_support/core_ext/array/access.rb
index 3177d8498e..b7ff7a3907 100644
--- a/activesupport/lib/active_support/core_ext/array/access.rb
+++ b/activesupport/lib/active_support/core_ext/array/access.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Array
# Returns the tail of the array from +position+.
#
@@ -31,10 +33,10 @@ 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.
+ # Note: This is an optimization of <tt>Enumerable#without</tt> that uses <tt>Array#-</tt>
+ # instead of <tt>Array#reject</tt> for performance reasons.
def without(*elements)
self - elements
end
@@ -73,4 +75,18 @@ class Array
def forty_two
self[41]
end
+
+ # Equal to <tt>self[-3]</tt>.
+ #
+ # %w( a b c d e ).third_to_last # => "c"
+ def third_to_last
+ self[-3]
+ end
+
+ # Equal to <tt>self[-2]</tt>.
+ #
+ # %w( a b c d e ).second_to_last # => "d"
+ def second_to_last
+ self[-2]
+ end
end
diff --git a/activesupport/lib/active_support/core_ext/array/conversions.rb b/activesupport/lib/active_support/core_ext/array/conversions.rb
index d80df21e7d..ea688ed2ea 100644
--- a/activesupport/lib/active_support/core_ext/array/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/array/conversions.rb
@@ -1,8 +1,10 @@
-require 'active_support/xml_mini'
-require 'active_support/core_ext/hash/keys'
-require 'active_support/core_ext/string/inflections'
-require 'active_support/core_ext/object/to_param'
-require 'active_support/core_ext/object/to_query'
+# frozen_string_literal: true
+
+require "active_support/xml_mini"
+require "active_support/core_ext/hash/keys"
+require "active_support/core_ext/string/inflections"
+require "active_support/core_ext/object/to_param"
+require "active_support/core_ext/object/to_query"
class Array
# Converts the array to a comma-separated sentence where the last element is
@@ -32,7 +34,7 @@ class Array
# ['one', 'two', 'three'].to_sentence # => "one, two, and three"
#
# ['one', 'two'].to_sentence(passing: 'invalid option')
- # # => ArgumentError: Unknown key :passing
+ # # => ArgumentError: Unknown key: :passing. Valid keys are: :words_connector, :two_words_connector, :last_word_connector, :locale
#
# ['one', 'two'].to_sentence(two_words_connector: '-')
# # => "one-two"
@@ -60,9 +62,9 @@ class Array
options.assert_valid_keys(:words_connector, :two_words_connector, :last_word_connector, :locale)
default_connectors = {
- :words_connector => ', ',
- :two_words_connector => ' and ',
- :last_word_connector => ', and '
+ words_connector: ", ",
+ two_words_connector: " and ",
+ last_word_connector: ", and "
}
if defined?(I18n)
i18n_connectors = I18n.translate(:'support.array', locale: options[:locale], default: {})
@@ -72,7 +74,7 @@ class Array
case length
when 0
- ''
+ ""
when 1
"#{self[0]}"
when 2
@@ -85,14 +87,16 @@ class Array
# Extends <tt>Array#to_s</tt> to convert a collection of elements into a
# comma separated id list if <tt>:db</tt> argument is given as the format.
#
- # Blog.all.to_formatted_s(:db) # => "1,2,3"
+ # Blog.all.to_formatted_s(:db) # => "1,2,3"
+ # Blog.none.to_formatted_s(:db) # => "null"
+ # [1,2].to_formatted_s # => "[1, 2]"
def to_formatted_s(format = :default)
case format
when :db
if empty?
- 'null'
+ "null"
else
- collect(&:id).join(',')
+ collect(&:id).join(",")
end
else
to_default_s
@@ -177,7 +181,7 @@ class Array
# </messages>
#
def to_xml(options = {})
- require 'active_support/builder' unless defined?(Builder)
+ require "active_support/builder" unless defined?(Builder)
options = options.dup
options[:indent] ||= 2
@@ -185,9 +189,9 @@ class Array
options[:root] ||= \
if first.class != Hash && all? { |e| e.is_a?(first.class) }
underscored = ActiveSupport::Inflector.underscore(first.class.name)
- ActiveSupport::Inflector.pluralize(underscored).tr('/', '_')
+ ActiveSupport::Inflector.pluralize(underscored).tr("/", "_")
else
- 'objects'
+ "objects"
end
builder = options[:builder]
@@ -195,7 +199,7 @@ class Array
root = ActiveSupport::XmlMini.rename_key(options[:root].to_s, options)
children = options.delete(:children) || root.singularize
- attributes = options[:skip_types] ? {} : { type: 'array' }
+ attributes = options[:skip_types] ? {} : { type: "array" }
if empty?
builder.tag!(root, attributes)
diff --git a/activesupport/lib/active_support/core_ext/array/extract.rb b/activesupport/lib/active_support/core_ext/array/extract.rb
new file mode 100644
index 0000000000..cc5a8a3f88
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/array/extract.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class Array
+ # Removes and returns the elements for which the block returns a true value.
+ # If no block is given, an Enumerator is returned instead.
+ #
+ # numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
+ # odd_numbers = numbers.extract! { |number| number.odd? } # => [1, 3, 5, 7, 9]
+ # numbers # => [0, 2, 4, 6, 8]
+ def extract!
+ return to_enum(:extract!) { size } unless block_given?
+
+ extracted_elements = []
+
+ reject! do |element|
+ extracted_elements << element if yield(element)
+ end
+
+ extracted_elements
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/array/extract_options.rb b/activesupport/lib/active_support/core_ext/array/extract_options.rb
index 9008a0df2a..8c7cb2e780 100644
--- a/activesupport/lib/active_support/core_ext/array/extract_options.rb
+++ b/activesupport/lib/active_support/core_ext/array/extract_options.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Hash
# By default, only instances of Hash itself are extractable.
# Subclasses of Hash may implement this method and return
diff --git a/activesupport/lib/active_support/core_ext/array/grouping.rb b/activesupport/lib/active_support/core_ext/array/grouping.rb
index 87ae052eb0..67e760bc4b 100644
--- a/activesupport/lib/active_support/core_ext/array/grouping.rb
+++ b/activesupport/lib/active_support/core_ext/array/grouping.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Array
# Splits or iterates over the array in groups of size +number+,
# padding any remaining slots with +fill_with+ unless it is +false+.
@@ -89,28 +91,19 @@ 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 = dup
+ result = []
if block_given?
- inject([[]]) do |results, element|
- if yield(element)
- results << []
- else
- results.last << element
- end
-
- results
+ while (idx = arr.index { |i| yield i })
+ result << arr.shift(idx)
+ arr.shift
end
else
- results, arr = [[]], self.dup
- until arr.empty?
- if (idx = arr.index(value))
- results.last.concat(arr.shift(idx))
- arr.shift
- results << []
- else
- results.last.concat(arr.shift(arr.size))
- end
+ while (idx = arr.index(value))
+ result << arr.shift(idx)
+ arr.shift
end
- results
end
+ result << arr
end
end
diff --git a/activesupport/lib/active_support/core_ext/array/inquiry.rb b/activesupport/lib/active_support/core_ext/array/inquiry.rb
index e8f44cc378..92c61bf201 100644
--- a/activesupport/lib/active_support/core_ext/array/inquiry.rb
+++ b/activesupport/lib/active_support/core_ext/array/inquiry.rb
@@ -1,4 +1,6 @@
-require 'active_support/array_inquirer'
+# frozen_string_literal: true
+
+require "active_support/array_inquirer"
class Array
# Wraps the array in an +ArrayInquirer+ object, which gives a friendlier way
diff --git a/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb b/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb
index f8d48b69df..661971d7cd 100644
--- a/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb
+++ b/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
class Array
# The human way of thinking about adding stuff to the end of a list is with append.
- alias_method :append, :<<
+ alias_method :append, :push unless [].respond_to?(:append)
# The human way of thinking about adding stuff to the beginning of a list is with prepend.
- alias_method :prepend, :unshift
-end \ No newline at end of file
+ alias_method :prepend, :unshift unless [].respond_to?(:prepend)
+end
diff --git a/activesupport/lib/active_support/core_ext/array/wrap.rb b/activesupport/lib/active_support/core_ext/array/wrap.rb
index b611d34c27..d62f97edbf 100644
--- a/activesupport/lib/active_support/core_ext/array/wrap.rb
+++ b/activesupport/lib/active_support/core_ext/array/wrap.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Array
# Wraps its argument in an array unless it is already an array (or array-like).
#
diff --git a/activesupport/lib/active_support/core_ext/benchmark.rb b/activesupport/lib/active_support/core_ext/benchmark.rb
index eb25b2bc44..641b58c8b8 100644
--- a/activesupport/lib/active_support/core_ext/benchmark.rb
+++ b/activesupport/lib/active_support/core_ext/benchmark.rb
@@ -1,4 +1,6 @@
-require 'benchmark'
+# frozen_string_literal: true
+
+require "benchmark"
class << Benchmark
# Benchmark realtime in milliseconds.
diff --git a/activesupport/lib/active_support/core_ext/big_decimal.rb b/activesupport/lib/active_support/core_ext/big_decimal.rb
index 8143113cfa..9e6a9d6331 100644
--- a/activesupport/lib/active_support/core_ext/big_decimal.rb
+++ b/activesupport/lib/active_support/core_ext/big_decimal.rb
@@ -1 +1,3 @@
-require 'active_support/core_ext/big_decimal/conversions'
+# frozen_string_literal: true
+
+require "active_support/core_ext/big_decimal/conversions"
diff --git a/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb b/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb
index 234283e792..52bd229416 100644
--- a/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb
@@ -1,15 +1,14 @@
-require 'bigdecimal'
-require 'bigdecimal/util'
+# frozen_string_literal: true
-class BigDecimal
- DEFAULT_STRING_FORMAT = 'F'
- alias_method :to_default_s, :to_s
+require "bigdecimal"
+require "bigdecimal/util"
- def to_s(format = nil, options = nil)
- if format.is_a?(Symbol)
- to_formatted_s(format, options || {})
- else
- to_default_s(format || DEFAULT_STRING_FORMAT)
+module ActiveSupport
+ module BigDecimalWithDefaultFormat #:nodoc:
+ def to_s(format = "F")
+ super(format)
end
end
end
+
+BigDecimal.prepend(ActiveSupport::BigDecimalWithDefaultFormat)
diff --git a/activesupport/lib/active_support/core_ext/class.rb b/activesupport/lib/active_support/core_ext/class.rb
index ef903d59b5..1c110fd07b 100644
--- a/activesupport/lib/active_support/core_ext/class.rb
+++ b/activesupport/lib/active_support/core_ext/class.rb
@@ -1,2 +1,4 @@
-require 'active_support/core_ext/class/attribute'
-require 'active_support/core_ext/class/subclasses'
+# frozen_string_literal: true
+
+require "active_support/core_ext/class/attribute"
+require "active_support/core_ext/class/subclasses"
diff --git a/activesupport/lib/active_support/core_ext/class/attribute.rb b/activesupport/lib/active_support/core_ext/class/attribute.rb
index f2b7bb3ef1..fa33ff945f 100644
--- a/activesupport/lib/active_support/core_ext/class/attribute.rb
+++ b/activesupport/lib/active_support/core_ext/class/attribute.rb
@@ -1,11 +1,23 @@
-require 'active_support/core_ext/kernel/singleton_class'
-require 'active_support/core_ext/module/remove_method'
-require 'active_support/core_ext/array/extract_options'
+# frozen_string_literal: true
+
+require "active_support/core_ext/kernel/singleton_class"
+require "active_support/core_ext/module/redefine_method"
+require "active_support/core_ext/array/extract_options"
class Class
# Declare a class-level attribute whose value is inheritable by subclasses.
# Subclasses can change their own value and it will not impact parent class.
#
+ # ==== Options
+ #
+ # * <tt>:instance_reader</tt> - Sets the instance reader method (defaults to true).
+ # * <tt>:instance_writer</tt> - Sets the instance writer method (defaults to true).
+ # * <tt>:instance_accessor</tt> - Sets both instance methods (defaults to true).
+ # * <tt>:instance_predicate</tt> - Sets a predicate method (defaults to true).
+ # * <tt>:default</tt> - Sets a default value for the attribute (defaults to nil).
+ #
+ # ==== Examples
+ #
# class Base
# class_attribute :setting
# end
@@ -20,14 +32,14 @@ class Class
# Base.setting # => true
#
# In the above case as long as Subclass does not assign a value to setting
- # by performing <tt>Subclass.setting = _something_ </tt>, <tt>Subclass.setting</tt>
+ # by performing <tt>Subclass.setting = _something_</tt>, <tt>Subclass.setting</tt>
# would read value assigned to parent class. Once Subclass assigns a value then
# the value assigned by Subclass would be returned.
#
# This matches normal Ruby method inheritance: think of writing an attribute
# on a subclass as overriding the reader method. However, you need to be aware
# when using +class_attribute+ with mutable structures as +Array+ or +Hash+.
- # In such cases, you don't want to do changes in places but use setters:
+ # In such cases, you don't want to do changes in place. Instead use setters:
#
# Base.setting = []
# Base.setting # => []
@@ -68,28 +80,35 @@ class Class
# object.setting = false # => NoMethodError
#
# To opt out of both instance methods, pass <tt>instance_accessor: false</tt>.
+ #
+ # To set a default value for the attribute, pass <tt>default:</tt>, like so:
+ #
+ # class_attribute :settings, default: {}
def class_attribute(*attrs)
options = attrs.extract_options!
- instance_reader = options.fetch(:instance_accessor, true) && options.fetch(:instance_reader, true)
- instance_writer = options.fetch(:instance_accessor, true) && options.fetch(:instance_writer, true)
+ instance_reader = options.fetch(:instance_accessor, true) && options.fetch(:instance_reader, true)
+ instance_writer = options.fetch(:instance_accessor, true) && options.fetch(:instance_writer, true)
instance_predicate = options.fetch(:instance_predicate, true)
+ default_value = options.fetch(:default, nil)
attrs.each do |name|
+ singleton_class.silence_redefinition_of_method(name)
define_singleton_method(name) { nil }
+
+ singleton_class.silence_redefinition_of_method("#{name}?")
define_singleton_method("#{name}?") { !!public_send(name) } if instance_predicate
- ivar = "@#{name}"
+ ivar = "@#{name}".to_sym
+ singleton_class.silence_redefinition_of_method("#{name}=")
define_singleton_method("#{name}=") do |val|
singleton_class.class_eval do
- remove_possible_method(name)
- define_method(name) { val }
+ redefine_method(name) { val }
end
if singleton_class?
class_eval do
- remove_possible_method(name)
- define_method(name) do
+ redefine_method(name) do
if instance_variable_defined? ivar
instance_variable_get ivar
else
@@ -102,18 +121,26 @@ class Class
end
if instance_reader
- remove_possible_method name
- define_method(name) do
+ redefine_method(name) do
if instance_variable_defined?(ivar)
instance_variable_get ivar
else
self.class.public_send name
end
end
- define_method("#{name}?") { !!public_send(name) } if instance_predicate
+
+ redefine_method("#{name}?") { !!public_send(name) } if instance_predicate
+ end
+
+ if instance_writer
+ redefine_method("#{name}=") do |val|
+ instance_variable_set ivar, val
+ end
end
- attr_writer name if instance_writer
+ unless default_value.nil?
+ self.send("#{name}=", default_value)
+ end
end
end
end
diff --git a/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb b/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb
index 84d5e95e7a..a77354e153 100644
--- a/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb
+++ b/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb
@@ -1,4 +1,6 @@
+# frozen_string_literal: true
+
# cattr_* became mattr_* aliases in 7dfbd91b0780fbd6a1dd9bfbc176e10894871d2d,
# but we keep this around for libraries that directly require it knowing they
# want cattr_*. No need to deprecate.
-require 'active_support/core_ext/module/attribute_accessors'
+require "active_support/core_ext/module/attribute_accessors"
diff --git a/activesupport/lib/active_support/core_ext/class/subclasses.rb b/activesupport/lib/active_support/core_ext/class/subclasses.rb
index 3c4bfc5f1e..56fb46a88d 100644
--- a/activesupport/lib/active_support/core_ext/class/subclasses.rb
+++ b/activesupport/lib/active_support/core_ext/class/subclasses.rb
@@ -1,19 +1,33 @@
-require 'active_support/core_ext/module/anonymous'
-require 'active_support/core_ext/module/reachable'
+# frozen_string_literal: true
class Class
begin
- ObjectSpace.each_object(Class.new) {}
+ # Test if this Ruby supports each_object against singleton_class
+ ObjectSpace.each_object(Numeric.singleton_class) { }
- def descendants # :nodoc:
+ # Returns an array with all classes that are < than its receiver.
+ #
+ # class C; end
+ # C.descendants # => []
+ #
+ # class B < C; end
+ # C.descendants # => [B]
+ #
+ # class A < B; end
+ # C.descendants # => [B, A]
+ #
+ # class D < C; end
+ # C.descendants # => [B, A, D]
+ def descendants
descendants = []
ObjectSpace.each_object(singleton_class) do |k|
+ next if k.singleton_class?
descendants.unshift k unless k == self
end
descendants
end
- rescue StandardError # JRuby
- def descendants # :nodoc:
+ rescue StandardError # JRuby 9.0.4.0 and earlier
+ def descendants
descendants = []
ObjectSpace.each_object(Class) do |k|
descendants.unshift k if k < self
@@ -25,8 +39,6 @@ class Class
# Returns an array with the direct children of +self+.
#
- # Integer.subclasses # => [Fixnum, Bignum]
- #
# class Foo; end
# class Bar < Foo; end
# class Baz < Bar; end
diff --git a/activesupport/lib/active_support/core_ext/date.rb b/activesupport/lib/active_support/core_ext/date.rb
index 465fedda80..cce73f2db2 100644
--- a/activesupport/lib/active_support/core_ext/date.rb
+++ b/activesupport/lib/active_support/core_ext/date.rb
@@ -1,5 +1,7 @@
-require 'active_support/core_ext/date/acts_like'
-require 'active_support/core_ext/date/calculations'
-require 'active_support/core_ext/date/conversions'
-require 'active_support/core_ext/date/zones'
+# frozen_string_literal: true
+require "active_support/core_ext/date/acts_like"
+require "active_support/core_ext/date/blank"
+require "active_support/core_ext/date/calculations"
+require "active_support/core_ext/date/conversions"
+require "active_support/core_ext/date/zones"
diff --git a/activesupport/lib/active_support/core_ext/date/acts_like.rb b/activesupport/lib/active_support/core_ext/date/acts_like.rb
index cd90cee236..c8077f3774 100644
--- a/activesupport/lib/active_support/core_ext/date/acts_like.rb
+++ b/activesupport/lib/active_support/core_ext/date/acts_like.rb
@@ -1,4 +1,6 @@
-require 'active_support/core_ext/object/acts_like'
+# frozen_string_literal: true
+
+require "active_support/core_ext/object/acts_like"
class Date
# Duck-types as a Date-like class. See Object#acts_like?.
diff --git a/activesupport/lib/active_support/core_ext/date/blank.rb b/activesupport/lib/active_support/core_ext/date/blank.rb
new file mode 100644
index 0000000000..e6271c79b3
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/date/blank.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require "date"
+
+class Date #:nodoc:
+ # No Date is blank:
+ #
+ # Date.today.blank? # => false
+ #
+ # @return [false]
+ def blank?
+ false
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/date/calculations.rb b/activesupport/lib/active_support/core_ext/date/calculations.rb
index c60e833441..1cd7acb05d 100644
--- a/activesupport/lib/active_support/core_ext/date/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/date/calculations.rb
@@ -1,9 +1,11 @@
-require 'date'
-require 'active_support/duration'
-require 'active_support/core_ext/object/acts_like'
-require 'active_support/core_ext/date/zones'
-require 'active_support/core_ext/time/zones'
-require 'active_support/core_ext/date_and_time/calculations'
+# frozen_string_literal: true
+
+require "date"
+require "active_support/duration"
+require "active_support/core_ext/object/acts_like"
+require "active_support/core_ext/date/zones"
+require "active_support/core_ext/time/zones"
+require "active_support/core_ext/date_and_time/calculations"
class Date
include DateAndTime::Calculations
@@ -26,7 +28,7 @@ class Date
Thread.current[:beginning_of_week] = find_beginning_of_week!(week_start)
end
- # Returns week start day symbol (e.g. :monday), or raises an ArgumentError for invalid day symbol.
+ # Returns week start day symbol (e.g. :monday), or raises an +ArgumentError+ for invalid day symbol.
def find_beginning_of_week!(week_start)
raise ArgumentError, "Invalid beginning of week: #{week_start}" unless ::Date::DAYS_INTO_WEEK.key?(week_start)
week_start
@@ -129,11 +131,11 @@ class Date
options.fetch(:day, day)
)
end
-
+
# 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/conversions.rb b/activesupport/lib/active_support/core_ext/date/conversions.rb
index 31479a1269..870119dc7f 100644
--- a/activesupport/lib/active_support/core_ext/date/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/date/conversions.rb
@@ -1,30 +1,24 @@
-require 'date'
-require 'active_support/inflector/methods'
-require 'active_support/core_ext/date/zones'
-require 'active_support/core_ext/module/remove_method'
+# frozen_string_literal: true
+
+require "date"
+require "active_support/inflector/methods"
+require "active_support/core_ext/date/zones"
+require "active_support/core_ext/module/redefine_method"
class Date
DATE_FORMATS = {
- :short => '%e %b',
- :long => '%B %e, %Y',
- :db => '%Y-%m-%d',
- :number => '%Y%m%d',
- :long_ordinal => lambda { |date|
+ short: "%d %b",
+ long: "%B %d, %Y",
+ db: "%Y-%m-%d",
+ number: "%Y%m%d",
+ long_ordinal: lambda { |date|
day_format = ActiveSupport::Inflector.ordinalize(date.day)
date.strftime("%B #{day_format}, %Y") # => "April 25th, 2007"
},
- :rfc822 => '%e %b %Y',
- :iso8601 => lambda { |date| date.iso8601 }
+ rfc822: "%d %b %Y",
+ iso8601: lambda { |date| date.iso8601 }
}
- # Ruby 1.9 has Date#to_time which converts to localtime only.
- remove_method :to_time
-
- # Ruby 1.9 has Date#xmlschema which converts to a string without the time
- # component. This removal may generate an issue on FreeBSD, that's why we
- # need to use remove_possible_method here
- remove_possible_method :xmlschema
-
# Convert to a formatted string. See DATE_FORMATS for predefined formats.
#
# This method is aliased to <tt>to_s</tt>.
@@ -65,24 +59,32 @@ class Date
# Overrides the default inspect method with a human readable one, e.g., "Mon, 21 Feb 2005"
def readable_inspect
- strftime('%a, %d %b %Y')
+ strftime("%a, %d %b %Y")
end
alias_method :default_inspect, :inspect
alias_method :inspect, :readable_inspect
+ silence_redefinition_of_method :to_time
+
# Converts a Date instance to a Time, where the time is set to the beginning of the day.
# The timezone can be either :local or :utc (default :local).
#
# date = Date.new(2007, 11, 10) # => Sat, 10 Nov 2007
#
- # date.to_time # => Sat Nov 10 00:00:00 0800 2007
- # date.to_time(:local) # => Sat Nov 10 00:00:00 0800 2007
+ # date.to_time # => 2007-11-10 00:00:00 0800
+ # date.to_time(:local) # => 2007-11-10 00:00:00 0800
+ #
+ # date.to_time(:utc) # => 2007-11-10 00:00:00 UTC
#
- # date.to_time(:utc) # => Sat Nov 10 00:00:00 UTC 2007
+ # NOTE: The :local timezone is Ruby's *process* timezone, i.e. ENV['TZ'].
+ # If the *application's* timezone is needed, then use +in_time_zone+ instead.
def to_time(form = :local)
+ raise ArgumentError, "Expected :local or :utc, got #{form.inspect}." unless [:local, :utc].include?(form)
::Time.send(form, year, month, day)
end
+ silence_redefinition_of_method :xmlschema
+
# Returns a string which represents the time in used time zone as DateTime
# defined by XML Schema:
#
diff --git a/activesupport/lib/active_support/core_ext/date/zones.rb b/activesupport/lib/active_support/core_ext/date/zones.rb
index d109b430db..2dcf97cff8 100644
--- a/activesupport/lib/active_support/core_ext/date/zones.rb
+++ b/activesupport/lib/active_support/core_ext/date/zones.rb
@@ -1,5 +1,7 @@
-require 'date'
-require 'active_support/core_ext/date_and_time/zones'
+# frozen_string_literal: true
+
+require "date"
+require "active_support/core_ext/date_and_time/zones"
class Date
include DateAndTime::Zones
diff --git a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
index 01153606c9..05abd83221 100644
--- a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
@@ -1,13 +1,17 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/object/try"
+
module DateAndTime
module Calculations
DAYS_INTO_WEEK = {
- :monday => 0,
- :tuesday => 1,
- :wednesday => 2,
- :thursday => 3,
- :friday => 4,
- :saturday => 5,
- :sunday => 6
+ sunday: 0,
+ monday: 1,
+ tuesday: 2,
+ wednesday: 3,
+ thursday: 4,
+ friday: 5,
+ saturday: 6
}
WEEKEND_DAYS = [ 6, 0 ]
@@ -16,9 +20,9 @@ module DateAndTime
advance(days: -1)
end
- # Returns a new date/time representing the previous day.
- def prev_day
- advance(days: -1)
+ # Returns a new date/time the specified number of days ago.
+ def prev_day(days = 1)
+ advance(days: -days)
end
# Returns a new date/time representing tomorrow.
@@ -26,9 +30,9 @@ module DateAndTime
advance(days: 1)
end
- # Returns a new date/time representing the next day.
- def next_day
- advance(days: 1)
+ # Returns a new date/time the specified number of days in the future.
+ def next_day(days = 1)
+ advance(days: days)
end
# Returns true if the date/time is today.
@@ -51,76 +55,116 @@ module DateAndTime
WEEKEND_DAYS.include?(wday)
end
+ # Returns true if the date/time does not fall on a Saturday or Sunday.
+ def on_weekday?
+ !WEEKEND_DAYS.include?(wday)
+ end
+
+ # Returns true if the date/time falls before <tt>date_or_time</tt>.
+ def before?(date_or_time)
+ self < date_or_time
+ end
+
+ # Returns true if the date/time falls after <tt>date_or_time</tt>.
+ def after?(date_or_time)
+ self > date_or_time
+ end
+
# Returns a new date/time the specified number of days ago.
def days_ago(days)
- advance(:days => -days)
+ advance(days: -days)
end
# Returns a new date/time the specified number of days in the future.
def days_since(days)
- advance(:days => days)
+ advance(days: days)
end
# Returns a new date/time the specified number of weeks ago.
def weeks_ago(weeks)
- advance(:weeks => -weeks)
+ advance(weeks: -weeks)
end
# Returns a new date/time the specified number of weeks in the future.
def weeks_since(weeks)
- advance(:weeks => weeks)
+ advance(weeks: weeks)
end
# Returns a new date/time the specified number of months ago.
def months_ago(months)
- advance(:months => -months)
+ advance(months: -months)
end
# Returns a new date/time the specified number of months in the future.
def months_since(months)
- advance(:months => months)
+ advance(months: months)
end
# Returns a new date/time the specified number of years ago.
def years_ago(years)
- advance(:years => -years)
+ advance(years: -years)
end
# Returns a new date/time the specified number of years in the future.
def years_since(years)
- advance(:years => years)
+ advance(years: years)
end
# Returns a new date/time at the start of the month.
- # DateTime objects will have a time set to 0:00.
+ #
+ # today = Date.today # => Thu, 18 Jun 2015
+ # today.beginning_of_month # => Mon, 01 Jun 2015
+ #
+ # +DateTime+ objects will have a time set to 0:00.
+ #
+ # now = DateTime.current # => Thu, 18 Jun 2015 15:23:13 +0000
+ # now.beginning_of_month # => Mon, 01 Jun 2015 00:00:00 +0000
def beginning_of_month
- first_hour(change(:day => 1))
+ first_hour(change(day: 1))
end
alias :at_beginning_of_month :beginning_of_month
# Returns a new date/time at the start of the quarter.
- # Example: 1st January, 1st July, 1st October.
- # DateTime objects will have a time set to 0:00.
+ #
+ # today = Date.today # => Fri, 10 Jul 2015
+ # today.beginning_of_quarter # => Wed, 01 Jul 2015
+ #
+ # +DateTime+ objects will have a time set to 0:00.
+ #
+ # now = DateTime.current # => Fri, 10 Jul 2015 18:41:29 +0000
+ # now.beginning_of_quarter # => Wed, 01 Jul 2015 00:00:00 +0000
def beginning_of_quarter
first_quarter_month = [10, 7, 4, 1].detect { |m| m <= month }
- beginning_of_month.change(:month => first_quarter_month)
+ beginning_of_month.change(month: first_quarter_month)
end
alias :at_beginning_of_quarter :beginning_of_quarter
# Returns a new date/time at the end of the quarter.
- # Example: 31st March, 30th June, 30th September.
- # DateTime objects will have a time set to 23:59:59.
+ #
+ # today = Date.today # => Fri, 10 Jul 2015
+ # today.end_of_quarter # => Wed, 30 Sep 2015
+ #
+ # +DateTime+ objects will have a time set to 23:59:59.
+ #
+ # now = DateTime.current # => Fri, 10 Jul 2015 18:41:29 +0000
+ # now.end_of_quarter # => Wed, 30 Sep 2015 23:59:59 +0000
def end_of_quarter
last_quarter_month = [3, 6, 9, 12].detect { |m| m >= month }
- beginning_of_month.change(:month => last_quarter_month).end_of_month
+ beginning_of_month.change(month: last_quarter_month).end_of_month
end
alias :at_end_of_quarter :end_of_quarter
- # Return a new date/time at the beginning of the year.
- # Example: 1st January.
- # DateTime objects will have a time set to 0:00.
+ # Returns a new date/time at the beginning of the year.
+ #
+ # today = Date.today # => Fri, 10 Jul 2015
+ # today.beginning_of_year # => Thu, 01 Jan 2015
+ #
+ # +DateTime+ objects will have a time set to 0:00.
+ #
+ # now = DateTime.current # => Fri, 10 Jul 2015 18:41:29 +0000
+ # now.beginning_of_year # => Thu, 01 Jan 2015 00:00:00 +0000
def beginning_of_year
- change(:month => 1).beginning_of_month
+ change(month: 1).beginning_of_month
end
alias :at_beginning_of_year :beginning_of_year
@@ -138,8 +182,8 @@ module DateAndTime
#
# +DateTime+ objects have their time set to 0:00 unless +same_time+ is true.
#
- # now = Time.current # => Thu, 07 May 2015 13:31:16 UTC +00:00
- # now.next_week # => Mon, 11 May 2015 00:00:00 UTC +00:00
+ # now = DateTime.current # => Thu, 07 May 2015 13:31:16 +0000
+ # now.next_week # => Mon, 11 May 2015 00:00:00 +0000
def next_week(given_day_in_next_week = Date.beginning_of_week, same_time: false)
result = first_hour(weeks_since(1).beginning_of_week.days_since(days_span(given_day_in_next_week)))
same_time ? copy_time_to(result) : result
@@ -154,9 +198,9 @@ module DateAndTime
end
end
- # Short-hand for months_since(1).
- def next_month
- months_since(1)
+ # Returns a new date/time the specified number of months in the future.
+ def next_month(months = 1)
+ advance(months: months)
end
# Short-hand for months_since(3)
@@ -164,9 +208,9 @@ module DateAndTime
months_since(3)
end
- # Short-hand for years_since(1).
- def next_year
- years_since(1)
+ # Returns a new date/time the specified number of years in the future.
+ def next_year(years = 1)
+ advance(years: years)
end
# Returns a new date/time representing the given day in the previous week.
@@ -189,11 +233,15 @@ module DateAndTime
end
alias_method :last_weekday, :prev_weekday
+ # Returns a new date/time the specified number of months ago.
+ def prev_month(months = 1)
+ advance(months: -months)
+ end
+
# Short-hand for months_ago(1).
- def prev_month
+ def last_month
months_ago(1)
end
- alias_method :last_month, :prev_month
# Short-hand for months_ago(3).
def prev_quarter
@@ -201,19 +249,22 @@ module DateAndTime
end
alias_method :last_quarter, :prev_quarter
+ # Returns a new date/time the specified number of years ago.
+ def prev_year(years = 1)
+ advance(years: -years)
+ end
+
# Short-hand for years_ago(1).
- def prev_year
+ def last_year
years_ago(1)
end
- alias_method :last_year, :prev_year
# Returns the number of days to the start of the week on the given day.
# Week is assumed to start on +start_day+, default is
# +Date.beginning_of_week+ or +config.beginning_of_week+ when set.
def days_to_week_start(start_day = Date.beginning_of_week)
- start_day_number = DAYS_INTO_WEEK[start_day]
- current_day_number = wday != 0 ? wday - 1 : 6
- (current_day_number - start_day_number) % 7
+ start_day_number = DAYS_INTO_WEEK.fetch(start_day)
+ (wday - start_day_number) % 7
end
# Returns a new date/time representing the start of this week on the given day.
@@ -258,12 +309,17 @@ module DateAndTime
# Returns a new date/time representing the end of the year.
# DateTime objects will have a time set to 23:59:59.
def end_of_year
- change(:month => 12).end_of_month
+ change(month: 12).end_of_month
end
alias :at_end_of_year :end_of_year
+ # Returns a Range representing the whole day of the current date/time.
+ def all_day
+ beginning_of_day..end_of_day
+ end
+
# Returns a Range representing the whole week of the current date/time.
- # Week starts on start_day, default is <tt>Date.week_start</tt> or <tt>config.week_start</tt> when set.
+ # Week starts on start_day, default is <tt>Date.beginning_of_week</tt> or <tt>config.beginning_of_week</tt> when set.
def all_week(start_day = Date.beginning_of_week)
beginning_of_week(start_day)..end_of_week(start_day)
end
@@ -283,6 +339,28 @@ module DateAndTime
beginning_of_year..end_of_year
end
+ # Returns a new date/time representing the next occurrence of the specified day of week.
+ #
+ # today = Date.today # => Thu, 14 Dec 2017
+ # today.next_occurring(:monday) # => Mon, 18 Dec 2017
+ # today.next_occurring(:thursday) # => Thu, 21 Dec 2017
+ def next_occurring(day_of_week)
+ from_now = DAYS_INTO_WEEK.fetch(day_of_week) - wday
+ from_now += 7 unless from_now > 0
+ advance(days: from_now)
+ end
+
+ # Returns a new date/time representing the previous occurrence of the specified day of week.
+ #
+ # today = Date.today # => Thu, 14 Dec 2017
+ # today.prev_occurring(:monday) # => Mon, 11 Dec 2017
+ # today.prev_occurring(:thursday) # => Thu, 07 Dec 2017
+ def prev_occurring(day_of_week)
+ ago = wday - DAYS_INTO_WEEK.fetch(day_of_week)
+ ago += 7 unless ago > 0
+ advance(days: -ago)
+ end
+
private
def first_hour(date_or_time)
date_or_time.acts_like?(:time) ? date_or_time.beginning_of_day : date_or_time
@@ -293,11 +371,11 @@ module DateAndTime
end
def days_span(day)
- (DAYS_INTO_WEEK[day] - DAYS_INTO_WEEK[Date.beginning_of_week]) % 7
+ (DAYS_INTO_WEEK.fetch(day) - DAYS_INTO_WEEK.fetch(Date.beginning_of_week)) % 7
end
def copy_time_to(other)
- other.change(hour: hour, min: min, sec: sec, usec: try(:usec))
+ other.change(hour: hour, min: min, sec: sec, nsec: try(:nsec))
end
end
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
new file mode 100644
index 0000000000..d33c36ef73
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/attribute_accessors"
+
+module DateAndTime
+ module Compatibility
+ # If true, +to_time+ preserves the timezone offset of receiver.
+ #
+ # NOTE: With Ruby 2.4+ the default for +to_time+ changed from
+ # converting to the local system time, to preserving the offset
+ # of the receiver. For backwards compatibility we're overriding
+ # 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, default: false
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/date_and_time/zones.rb b/activesupport/lib/active_support/core_ext/date_and_time/zones.rb
index 96c6df9407..894fd9b76d 100644
--- a/activesupport/lib/active_support/core_ext/date_and_time/zones.rb
+++ b/activesupport/lib/active_support/core_ext/date_and_time/zones.rb
@@ -1,11 +1,13 @@
+# frozen_string_literal: true
+
module DateAndTime
module Zones
# Returns the simultaneous time in <tt>Time.zone</tt> if a zone is given or
# if Time.zone_default is set. Otherwise, it returns the current time.
#
# Time.zone = 'Hawaii' # => 'Hawaii'
- # DateTime.utc(2000).in_time_zone # => Fri, 31 Dec 1999 14:00:00 HST -10:00
- # Date.new(2000).in_time_zone # => Sat, 01 Jan 2000 00:00:00 HST -10:00
+ # Time.utc(2000).in_time_zone # => Fri, 31 Dec 1999 14:00:00 HST -10:00
+ # Date.new(2000).in_time_zone # => Sat, 01 Jan 2000 00:00:00 HST -10:00
#
# This method is similar to Time#localtime, except that it uses <tt>Time.zone</tt> as the local zone
# instead of the operating system's time zone.
@@ -14,8 +16,7 @@ module DateAndTime
# and the conversion will be based on that zone instead of <tt>Time.zone</tt>.
#
# Time.utc(2000).in_time_zone('Alaska') # => Fri, 31 Dec 1999 15:00:00 AKST -09:00
- # DateTime.utc(2000).in_time_zone('Alaska') # => Fri, 31 Dec 1999 15:00:00 AKST -09:00
- # Date.new(2000).in_time_zone('Alaska') # => Sat, 01 Jan 2000 00:00:00 AKST -09:00
+ # Date.new(2000).in_time_zone('Alaska') # => Sat, 01 Jan 2000 00:00:00 AKST -09:00
def in_time_zone(zone = ::Time.zone)
time_zone = ::Time.find_zone! zone
time = acts_like?(:time) ? self : nil
@@ -23,19 +24,18 @@ module DateAndTime
if time_zone
time_with_zone(time, time_zone)
else
- time || self.to_time
+ time || to_time
end
end
private
- def time_with_zone(time, zone)
- if time
- ActiveSupport::TimeWithZone.new(time.utc? ? time : time.getutc, zone)
- else
- ActiveSupport::TimeWithZone.new(nil, zone, to_time(:utc))
+ def time_with_zone(time, zone)
+ if time
+ ActiveSupport::TimeWithZone.new(time.utc? ? time : time.getutc, zone)
+ else
+ ActiveSupport::TimeWithZone.new(nil, zone, to_time(:utc))
+ end
end
- end
end
end
-
diff --git a/activesupport/lib/active_support/core_ext/date_time.rb b/activesupport/lib/active_support/core_ext/date_time.rb
index e8a27b9f38..790dbeec1b 100644
--- a/activesupport/lib/active_support/core_ext/date_time.rb
+++ b/activesupport/lib/active_support/core_ext/date_time.rb
@@ -1,4 +1,7 @@
-require 'active_support/core_ext/date_time/acts_like'
-require 'active_support/core_ext/date_time/calculations'
-require 'active_support/core_ext/date_time/conversions'
-require 'active_support/core_ext/date_time/zones'
+# frozen_string_literal: true
+
+require "active_support/core_ext/date_time/acts_like"
+require "active_support/core_ext/date_time/blank"
+require "active_support/core_ext/date_time/calculations"
+require "active_support/core_ext/date_time/compatibility"
+require "active_support/core_ext/date_time/conversions"
diff --git a/activesupport/lib/active_support/core_ext/date_time/acts_like.rb b/activesupport/lib/active_support/core_ext/date_time/acts_like.rb
index 8fbbe0d3e9..5dccdfe219 100644
--- a/activesupport/lib/active_support/core_ext/date_time/acts_like.rb
+++ b/activesupport/lib/active_support/core_ext/date_time/acts_like.rb
@@ -1,5 +1,7 @@
-require 'date'
-require 'active_support/core_ext/object/acts_like'
+# frozen_string_literal: true
+
+require "date"
+require "active_support/core_ext/object/acts_like"
class DateTime
# Duck-types as a Date-like class. See Object#acts_like?.
diff --git a/activesupport/lib/active_support/core_ext/date_time/blank.rb b/activesupport/lib/active_support/core_ext/date_time/blank.rb
new file mode 100644
index 0000000000..a52c8bc150
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/date_time/blank.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require "date"
+
+class DateTime #:nodoc:
+ # No DateTime is ever blank:
+ #
+ # DateTime.now.blank? # => false
+ #
+ # @return [false]
+ def blank?
+ false
+ 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 95617fb8c2..bc670c3e76 100644
--- a/activesupport/lib/active_support/core_ext/date_time/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/date_time/calculations.rb
@@ -1,4 +1,6 @@
-require 'date'
+# frozen_string_literal: true
+
+require "date"
class DateTime
class << self
@@ -28,6 +30,13 @@ class DateTime
end_of_day.to_i - to_i
end
+ # Returns the fraction of a second as a +Rational+
+ #
+ # DateTime.new(2012, 8, 29, 0, 0, 0.5).subsec # => (1/2)
+ def subsec
+ sec_fraction
+ end
+
# Returns a new DateTime where one or more of the elements have been changed
# according to the +options+ parameter. The time options (<tt>:hour</tt>,
# <tt>:min</tt>, <tt>:sec</tt>) reset cascadingly, so if only the hour is
@@ -40,13 +49,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)
)
@@ -68,7 +87,7 @@ class DateTime
end
d = to_date.advance(options)
- datetime_advanced_by_date = change(:year => d.year, :month => d.month, :day => d.day)
+ datetime_advanced_by_date = change(year: d.year, month: d.month, day: d.day)
seconds_to_advance = \
options.fetch(:seconds, 0) +
options.fetch(:minutes, 0) * 60 +
@@ -91,13 +110,13 @@ class DateTime
# instance time. Do not use this method in combination with x.months, use
# months_since instead!
def since(seconds)
- self + Rational(seconds.round, 86400)
+ self + Rational(seconds, 86400)
end
alias :in :since
# Returns a new DateTime representing the start of the day (0:00).
def beginning_of_day
- change(:hour => 0)
+ change(hour: 0)
end
alias :midnight :beginning_of_day
alias :at_midnight :beginning_of_day
@@ -105,7 +124,7 @@ class DateTime
# Returns a new DateTime representing the middle of the day (12:00)
def middle_of_day
- change(:hour => 12)
+ change(hour: 12)
end
alias :midday :middle_of_day
alias :noon :middle_of_day
@@ -115,42 +134,60 @@ 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
# Returns a new DateTime representing the start of the hour (hh:00:00).
def beginning_of_hour
- change(:min => 0)
+ change(min: 0)
end
alias :at_beginning_of_hour :beginning_of_hour
# 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
# Returns a new DateTime representing the start of the minute (hh:mm:00).
def beginning_of_minute
- change(:sec => 0)
+ change(sec: 0)
end
alias :at_beginning_of_minute :beginning_of_minute
# 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
- # Adjusts DateTime to UTC by adding its offset value; offset is set to 0.
+ # Returns a <tt>Time</tt> instance of the simultaneous time in the system timezone.
+ def localtime(utc_offset = nil)
+ utc = new_offset(0)
+
+ Time.utc(
+ utc.year, utc.month, utc.day,
+ utc.hour, utc.min, utc.sec + utc.sec_fraction
+ ).getlocal(utc_offset)
+ end
+ alias_method :getlocal, :localtime
+
+ # Returns a <tt>Time</tt> instance of the simultaneous time in the UTC timezone.
#
# DateTime.civil(2005, 2, 21, 10, 11, 12, Rational(-6, 24)) # => Mon, 21 Feb 2005 10:11:12 -0600
- # DateTime.civil(2005, 2, 21, 10, 11, 12, Rational(-6, 24)).utc # => Mon, 21 Feb 2005 16:11:12 +0000
+ # DateTime.civil(2005, 2, 21, 10, 11, 12, Rational(-6, 24)).utc # => Mon, 21 Feb 2005 16:11:12 UTC
def utc
- new_offset(0)
+ utc = new_offset(0)
+
+ Time.utc(
+ utc.year, utc.month, utc.day,
+ utc.hour, utc.min, utc.sec + utc.sec_fraction
+ )
end
+ alias_method :getgm, :utc
alias_method :getutc, :utc
+ alias_method :gmtime, :utc
# Returns +true+ if <tt>offset == 0</tt>.
def utc?
@@ -165,13 +202,10 @@ class DateTime
# Layers additional behavior on DateTime#<=> so that Time and
# ActiveSupport::TimeWithZone instances can be compared with a DateTime.
def <=>(other)
- if other.kind_of?(Infinity)
- super
- elsif other.respond_to? :to_datetime
+ if other.respond_to? :to_datetime
super other.to_datetime rescue nil
else
- nil
+ super
end
end
-
end
diff --git a/activesupport/lib/active_support/core_ext/date_time/compatibility.rb b/activesupport/lib/active_support/core_ext/date_time/compatibility.rb
new file mode 100644
index 0000000000..7600a067cc
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/date_time/compatibility.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/date_and_time/compatibility"
+require "active_support/core_ext/module/redefine_method"
+
+class DateTime
+ include DateAndTime::Compatibility
+
+ silence_redefinition_of_method :to_time
+
+ # Either return an instance of +Time+ with the same UTC offset
+ # as +self+ or an instance of +Time+ representing the same time
+ # in the 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/date_time/conversions.rb b/activesupport/lib/active_support/core_ext/date_time/conversions.rb
index 2a9c09fc29..29725c89f7 100644
--- a/activesupport/lib/active_support/core_ext/date_time/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/date_time/conversions.rb
@@ -1,8 +1,10 @@
-require 'date'
-require 'active_support/inflector/methods'
-require 'active_support/core_ext/time/conversions'
-require 'active_support/core_ext/date_time/calculations'
-require 'active_support/values/time_zone'
+# frozen_string_literal: true
+
+require "date"
+require "active_support/inflector/methods"
+require "active_support/core_ext/time/conversions"
+require "active_support/core_ext/date_time/calculations"
+require "active_support/values/time_zone"
class DateTime
# Convert to a formatted string. See Time::DATE_FORMATS for predefined formats.
@@ -40,6 +42,8 @@ class DateTime
alias_method :to_default_s, :to_s if instance_methods(false).include?(:to_s)
alias_method :to_s, :to_formatted_s
+ # Returns a formatted string of the offset from UTC, or an alternative
+ # string if the time zone is already UTC.
#
# datetime = DateTime.civil(2000, 1, 1, 0, 0, 0, Rational(-6, 24))
# datetime.formatted_offset # => "-06:00"
@@ -62,7 +66,7 @@ class DateTime
# # => Sun, 01 Jan 2012 00:00:00 +0300
# DateTime.civil_from_format :local, 2012, 12, 17
# # => Mon, 17 Dec 2012 00:00:00 +0000
- def self.civil_from_format(utc_or_local, year, month=1, day=1, hour=0, min=0, sec=0)
+ def self.civil_from_format(utc_or_local, year, month = 1, day = 1, hour = 0, min = 0, sec = 0)
if utc_or_local.to_sym == :local
offset = ::Time.local(year, month, day).utc_offset.to_r / 86400
else
@@ -93,11 +97,11 @@ class DateTime
private
- def offset_in_seconds
- (offset * 86400).to_i
- end
+ def offset_in_seconds
+ (offset * 86400).to_i
+ end
- def seconds_since_unix_epoch
- (jd - 2440588) * 86400 - offset_in_seconds + seconds_since_midnight
- end
+ def seconds_since_unix_epoch
+ (jd - 2440588) * 86400 - offset_in_seconds + seconds_since_midnight
+ end
end
diff --git a/activesupport/lib/active_support/core_ext/date_time/zones.rb b/activesupport/lib/active_support/core_ext/date_time/zones.rb
deleted file mode 100644
index c39f358395..0000000000
--- a/activesupport/lib/active_support/core_ext/date_time/zones.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-require 'date'
-require 'active_support/core_ext/date_and_time/zones'
-
-class DateTime
- include DateAndTime::Zones
-end
diff --git a/activesupport/lib/active_support/core_ext/digest/uuid.rb b/activesupport/lib/active_support/core_ext/digest/uuid.rb
index 593c51bba2..6e949a2d72 100644
--- a/activesupport/lib/active_support/core_ext/digest/uuid.rb
+++ b/activesupport/lib/active_support/core_ext/digest/uuid.rb
@@ -1,4 +1,6 @@
-require 'securerandom'
+# frozen_string_literal: true
+
+require "securerandom"
module Digest
module UUID
@@ -12,7 +14,7 @@ module Digest
# Using Digest::MD5 generates version 3 UUIDs; Digest::SHA1 generates version 5 UUIDs.
# uuid_from_hash always generates the same UUID for a given name and namespace combination.
#
- # See RFC 4122 for details of UUID at: http://www.ietf.org/rfc/rfc4122.txt
+ # See RFC 4122 for details of UUID at: https://www.ietf.org/rfc/rfc4122.txt
def self.uuid_from_hash(hash_class, uuid_namespace, name)
if hash_class == Digest::MD5
version = 3
@@ -26,7 +28,7 @@ module Digest
hash.update(uuid_namespace)
hash.update(name)
- ary = hash.digest.unpack('NnnnnN')
+ ary = hash.digest.unpack("NnnnnN")
ary[2] = (ary[2] & 0x0FFF) | (version << 12)
ary[3] = (ary[3] & 0x3FFF) | 0x8000
@@ -35,12 +37,12 @@ module Digest
# Convenience method for uuid_from_hash using Digest::MD5.
def self.uuid_v3(uuid_namespace, name)
- self.uuid_from_hash(Digest::MD5, uuid_namespace, name)
+ uuid_from_hash(Digest::MD5, uuid_namespace, name)
end
# Convenience method for uuid_from_hash using Digest::SHA1.
def self.uuid_v5(uuid_namespace, name)
- self.uuid_from_hash(Digest::SHA1, uuid_namespace, name)
+ uuid_from_hash(Digest::SHA1, uuid_namespace, name)
end
# Convenience method for SecureRandom.uuid.
diff --git a/activesupport/lib/active_support/core_ext/enumerable.rb b/activesupport/lib/active_support/core_ext/enumerable.rb
index fc7531d088..d87d63f287 100644
--- a/activesupport/lib/active_support/core_ext/enumerable.rb
+++ b/activesupport/lib/active_support/core_ext/enumerable.rb
@@ -1,4 +1,21 @@
+# frozen_string_literal: true
+
module Enumerable
+ INDEX_WITH_DEFAULT = Object.new
+ private_constant :INDEX_WITH_DEFAULT
+
+ # Enumerable#sum was added in Ruby 2.4, but it only works with Numeric elements
+ # when we omit an identity.
+
+ # :stopdoc:
+
+ # We can't use Refinements here because Refinements with Module which will be prepended
+ # doesn't work well https://bugs.ruby-lang.org/issues/13446
+ alias :_original_sum_with_required_identity :sum
+ private :_original_sum_with_required_identity
+
+ # :startdoc:
+
# Calculates a sum from the elements.
#
# payments.sum { |p| p.price * p.tax_rate }
@@ -12,33 +29,58 @@ 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:
#
# [].sum(Payment.new(0)) { |i| i.amount } # => Payment.new(0)
- def sum(identity = 0, &block)
- if block_given?
+ def sum(identity = nil, &block)
+ if identity
+ _original_sum_with_required_identity(identity, &block)
+ elsif block_given?
map(&block).sum(identity)
else
- inject { |sum, element| sum + element } || identity
+ inject(:+) || 0
end
end
- # Convert an enumerable to a hash.
+ # Convert an enumerable to a hash keying it by the block return value.
#
# 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?
- Hash[map { |elem| [yield(elem), elem] }]
+ result = {}
+ each { |elem| result[yield(elem)] = elem }
+ result
else
to_enum(:index_by) { size if respond_to?(:size) }
end
end
+ # Convert an enumerable to a hash keying it with the enumerable items and with the values returned in the block.
+ #
+ # post = Post.new(title: "hey there", body: "what's up?")
+ #
+ # %i( title body ).index_with { |attr_name| post.public_send(attr_name) }
+ # # => { title: "hey there", body: "what's up?" }
+ def index_with(default = INDEX_WITH_DEFAULT)
+ if block_given?
+ result = {}
+ each { |elem| result[elem] = yield(elem) }
+ result
+ elsif default != INDEX_WITH_DEFAULT
+ result = {}
+ each { |elem| result[elem] = default }
+ result
+ else
+ to_enum(:index_with) { size if respond_to?(:size) }
+ end
+ end
+
# Returns +true+ if the enumerable has more than 1 element. Functionally
# equivalent to <tt>enum.to_a.size > 1</tt>. Can be called with a block too,
# much like any?, so <tt>people.many? { |p| p.age > 26 }</tt> returns +true+
@@ -64,10 +106,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
@@ -75,10 +117,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] } }
@@ -91,16 +133,36 @@ end
class Range #:nodoc:
# Optimize range sum to use arithmetic progression if a block is not given and
# we have a range of numeric values.
- def sum(identity = 0)
+ def sum(identity = nil)
if block_given? || !(first.is_a?(Integer) && last.is_a?(Integer))
super
else
actual_last = exclude_end? ? (last - 1) : last
if actual_last >= first
- (actual_last - first + 1) * (actual_last + first) / 2
+ sum = identity || 0
+ sum + (actual_last - first + 1) * (actual_last + first) / 2
else
- identity
+ identity || 0
end
end
end
end
+
+# Using Refinements here in order not to expose our internal method
+using Module.new {
+ refine Array do
+ alias :orig_sum :sum
+ end
+}
+
+class Array #:nodoc:
+ # Array#sum was added in Ruby 2.4 but it only works with Numeric elements.
+ def sum(init = nil, &block)
+ if init.is_a?(Numeric) || first.is_a?(Numeric)
+ init ||= 0
+ orig_sum(init, &block)
+ else
+ super
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/file.rb b/activesupport/lib/active_support/core_ext/file.rb
index dc24afbe7f..64553bfa4e 100644
--- a/activesupport/lib/active_support/core_ext/file.rb
+++ b/activesupport/lib/active_support/core_ext/file.rb
@@ -1 +1,3 @@
-require 'active_support/core_ext/file/atomic'
+# frozen_string_literal: true
+
+require "active_support/core_ext/file/atomic"
diff --git a/activesupport/lib/active_support/core_ext/file/atomic.rb b/activesupport/lib/active_support/core_ext/file/atomic.rb
index fad6fa8d9d..8e288833b6 100644
--- a/activesupport/lib/active_support/core_ext/file/atomic.rb
+++ b/activesupport/lib/active_support/core_ext/file/atomic.rb
@@ -1,4 +1,6 @@
-require 'fileutils'
+# frozen_string_literal: true
+
+require "fileutils"
class File
# Write to a file atomically. Useful for situations where you don't
@@ -8,54 +10,56 @@ class File
# file.write('hello')
# end
#
- # If your temp directory is not on the same filesystem as the file you're
- # trying to write, you can provide a different temporary directory.
+ # This method needs to create a temporary file. By default it will create it
+ # in the same directory as the destination file. If you don't like this
+ # behavior you can provide a different directory but it must be on the
+ # same physical filesystem as the file you're trying to write.
#
# File.atomic_write('/data/something.important', '/data/tmp') do |file|
# file.write('hello')
# end
- def self.atomic_write(file_name, temp_dir = Dir.tmpdir)
- require 'tempfile' unless defined?(Tempfile)
- require 'fileutils' unless defined?(FileUtils)
-
- temp_file = Tempfile.new(basename(file_name), temp_dir)
- temp_file.binmode
- return_val = yield temp_file
- temp_file.close
+ def self.atomic_write(file_name, temp_dir = dirname(file_name))
+ require "tempfile" unless defined?(Tempfile)
- if File.exist?(file_name)
- # Get original file permissions
- old_stat = stat(file_name)
- else
- # If not possible, probe which are the default permissions in the
- # destination directory.
- old_stat = probe_stat_in(dirname(file_name))
- end
+ Tempfile.open(".#{basename(file_name)}", temp_dir) do |temp_file|
+ temp_file.binmode
+ return_val = yield temp_file
+ temp_file.close
- # Overwrite original file with temp file
- FileUtils.mv(temp_file.path, file_name)
+ old_stat = if exist?(file_name)
+ # Get original file permissions
+ stat(file_name)
+ elsif temp_dir != dirname(file_name)
+ # If not possible, probe which are the default permissions in the
+ # destination directory.
+ probe_stat_in(dirname(file_name))
+ end
- # Set correct permissions on new file
- begin
- chown(old_stat.uid, old_stat.gid, file_name)
- # This operation will affect filesystem ACL's
- chmod(old_stat.mode, file_name)
+ if old_stat
+ # Set correct permissions on new file
+ begin
+ chown(old_stat.uid, old_stat.gid, temp_file.path)
+ # This operation will affect filesystem ACL's
+ chmod(old_stat.mode, temp_file.path)
+ rescue Errno::EPERM, Errno::EACCES
+ # Changing file ownership failed, moving on.
+ end
+ end
- # Make sure we return the result of the yielded block
+ # Overwrite original file with temp file
+ rename(temp_file.path, file_name)
return_val
- rescue Errno::EPERM, Errno::EACCES
- # Changing file ownership failed, moving on.
end
end
# Private utility method.
def self.probe_stat_in(dir) #:nodoc:
basename = [
- '.permissions_check',
+ ".permissions_check",
Thread.current.object_id,
Process.pid,
rand(1000000)
- ].join('.')
+ ].join(".")
file_name = join(dir, basename)
FileUtils.touch(file_name)
diff --git a/activesupport/lib/active_support/core_ext/hash.rb b/activesupport/lib/active_support/core_ext/hash.rb
index af4d1da0eb..c4b9e5f1a0 100644
--- a/activesupport/lib/active_support/core_ext/hash.rb
+++ b/activesupport/lib/active_support/core_ext/hash.rb
@@ -1,9 +1,9 @@
-require 'active_support/core_ext/hash/compact'
-require 'active_support/core_ext/hash/conversions'
-require 'active_support/core_ext/hash/deep_merge'
-require 'active_support/core_ext/hash/except'
-require 'active_support/core_ext/hash/indifferent_access'
-require 'active_support/core_ext/hash/keys'
-require 'active_support/core_ext/hash/reverse_merge'
-require 'active_support/core_ext/hash/slice'
-require 'active_support/core_ext/hash/transform_values'
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/conversions"
+require "active_support/core_ext/hash/deep_merge"
+require "active_support/core_ext/hash/except"
+require "active_support/core_ext/hash/indifferent_access"
+require "active_support/core_ext/hash/keys"
+require "active_support/core_ext/hash/reverse_merge"
+require "active_support/core_ext/hash/slice"
diff --git a/activesupport/lib/active_support/core_ext/hash/compact.rb b/activesupport/lib/active_support/core_ext/hash/compact.rb
index 5dc9a05ec7..28c8d86b9b 100644
--- a/activesupport/lib/active_support/core_ext/hash/compact.rb
+++ b/activesupport/lib/active_support/core_ext/hash/compact.rb
@@ -1,20 +1,5 @@
-class Hash
- # Returns a hash with non +nil+ values.
- #
- # hash = { a: true, b: false, c: nil}
- # hash.compact # => { a: true, b: false}
- # hash # => { a: true, b: false, c: nil}
- # { c: nil }.compact # => {}
- def compact
- self.select { |_, value| !value.nil? }
- end
+# frozen_string_literal: true
- # Replaces current hash with non +nil+ values.
- #
- # hash = { a: true, b: false, c: nil}
- # hash.compact! # => { a: true, b: false}
- # hash # => { a: true, b: false}
- def compact!
- self.reject! { |_, value| value.nil? }
- end
-end
+require "active_support/deprecation"
+
+ActiveSupport::Deprecation.warn "Ruby 2.4+ (required by Rails 6) provides Hash#compact and Hash#compact! natively, so requiring active_support/core_ext/hash/compact is no longer necessary. Requiring it will raise LoadError in Rails 6.1."
diff --git a/activesupport/lib/active_support/core_ext/hash/conversions.rb b/activesupport/lib/active_support/core_ext/hash/conversions.rb
index 8594d9bf2e..5b48254646 100644
--- a/activesupport/lib/active_support/core_ext/hash/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/hash/conversions.rb
@@ -1,11 +1,13 @@
-require 'active_support/xml_mini'
-require 'active_support/time'
-require 'active_support/core_ext/object/blank'
-require 'active_support/core_ext/object/to_param'
-require 'active_support/core_ext/object/to_query'
-require 'active_support/core_ext/array/wrap'
-require 'active_support/core_ext/hash/reverse_merge'
-require 'active_support/core_ext/string/inflections'
+# frozen_string_literal: true
+
+require "active_support/xml_mini"
+require "active_support/time"
+require "active_support/core_ext/object/blank"
+require "active_support/core_ext/object/to_param"
+require "active_support/core_ext/object/to_query"
+require "active_support/core_ext/array/wrap"
+require "active_support/core_ext/hash/reverse_merge"
+require "active_support/core_ext/string/inflections"
class Hash
# Returns a string containing an XML representation of its receiver:
@@ -31,7 +33,7 @@ class Hash
# with +key+ as <tt>:root</tt>, and +key+ singularized as second argument. The
# callable can add nodes by using <tt>options[:builder]</tt>.
#
- # 'foo'.to_xml(lambda { |options, key| options[:builder].b(key) })
+ # {foo: lambda { |options, key| options[:builder].b(key) }}.to_xml
# # => "<b>foo</b>"
#
# * If +value+ responds to +to_xml+ the method is invoked with +key+ as <tt>:root</tt>.
@@ -55,8 +57,7 @@ class Hash
#
# XML_TYPE_NAMES = {
# "Symbol" => "symbol",
- # "Fixnum" => "integer",
- # "Bignum" => "integer",
+ # "Integer" => "integer",
# "BigDecimal" => "decimal",
# "Float" => "float",
# "TrueClass" => "boolean",
@@ -72,11 +73,11 @@ class Hash
# configure your own builder with the <tt>:builder</tt> option. The method also accepts
# options like <tt>:dasherize</tt> and friends, they are forwarded to the builder.
def to_xml(options = {})
- require 'active_support/builder' unless defined?(Builder)
+ require "active_support/builder" unless defined?(Builder)
options = options.dup
options[:indent] ||= 2
- options[:root] ||= 'hash'
+ options[:root] ||= "hash"
options[:builder] ||= Builder::XmlMarkup.new(indent: options[:indent])
builder = options[:builder]
@@ -138,6 +139,8 @@ end
module ActiveSupport
class XMLConverter # :nodoc:
+ # Raised if the XML contains attributes with type="yaml" or
+ # type="symbol". Read Hash#from_xml for more details.
class DisallowedType < StandardError
def initialize(type)
super "Disallowed type attribute: #{type.inspect}"
@@ -158,36 +161,36 @@ module ActiveSupport
private
def normalize_keys(params)
case params
- when Hash
- Hash[params.map { |k,v| [k.to_s.tr('-', '_'), normalize_keys(v)] } ]
- when Array
- params.map { |v| normalize_keys(v) }
- else
- params
+ when Hash
+ Hash[params.map { |k, v| [k.to_s.tr("-", "_"), normalize_keys(v)] } ]
+ when Array
+ params.map { |v| normalize_keys(v) }
+ else
+ params
end
end
def deep_to_h(value)
case value
- when Hash
- process_hash(value)
- when Array
- process_array(value)
- when String
- value
- else
- raise "can't typecast #{value.class.name} - #{value.inspect}"
+ when Hash
+ process_hash(value)
+ when Array
+ process_array(value)
+ when String
+ value
+ else
+ raise "can't typecast #{value.class.name} - #{value.inspect}"
end
end
def process_hash(value)
- if value.include?('type') && !value['type'].is_a?(Hash) && @disallowed_types.include?(value['type'])
- raise DisallowedType, value['type']
+ if value.include?("type") && !value["type"].is_a?(Hash) && @disallowed_types.include?(value["type"])
+ raise DisallowedType, value["type"]
end
if become_array?(value)
- _, entries = Array.wrap(value.detect { |k,v| not v.is_a?(String) })
- if entries.nil? || value['__content__'].try(:empty?)
+ _, entries = Array.wrap(value.detect { |k, v| not v.is_a?(String) })
+ if entries.nil? || value["__content__"].try(:empty?)
[]
else
case entries
@@ -203,28 +206,28 @@ module ActiveSupport
process_content(value)
elsif become_empty_string?(value)
- ''
+ ""
elsif become_hash?(value)
- xml_value = Hash[value.map { |k,v| [k, deep_to_h(v)] }]
+ xml_value = Hash[value.map { |k, v| [k, deep_to_h(v)] }]
# Turn { files: { file: #<StringIO> } } into { files: #<StringIO> } so it is compatible with
# how multipart uploaded files from HTML appear
- xml_value['file'].is_a?(StringIO) ? xml_value['file'] : xml_value
+ xml_value["file"].is_a?(StringIO) ? xml_value["file"] : xml_value
end
end
def become_content?(value)
- value['type'] == 'file' || (value['__content__'] && (value.keys.size == 1 || value['__content__'].present?))
+ value["type"] == "file" || (value["__content__"] && (value.keys.size == 1 || value["__content__"].present?))
end
def become_array?(value)
- value['type'] == 'array'
+ value["type"] == "array"
end
def become_empty_string?(value)
# { "string" => true }
# No tests fail when the second term is removed.
- value['type'] == 'string' && value['nil'] != 'true'
+ value["type"] == "string" && value["nil"] != "true"
end
def become_hash?(value)
@@ -233,19 +236,19 @@ module ActiveSupport
def nothing?(value)
# blank or nil parsed values are represented by nil
- value.blank? || value['nil'] == 'true'
+ value.blank? || value["nil"] == "true"
end
def garbage?(value)
# If the type is the only element which makes it then
# this still makes the value nil, except if type is
# an XML node(where type['value'] is a Hash)
- value['type'] && !value['type'].is_a?(::Hash) && value.size == 1
+ value["type"] && !value["type"].is_a?(::Hash) && value.size == 1
end
def process_content(value)
- content = value['__content__']
- if parser = ActiveSupport::XmlMini::PARSING[value['type']]
+ content = value["__content__"]
+ if parser = ActiveSupport::XmlMini::PARSING[value["type"]]
parser.arity == 1 ? parser.call(content) : parser.call(content, value)
else
content
@@ -256,6 +259,5 @@ module ActiveSupport
value.map! { |i| deep_to_h(i) }
value.length > 1 ? value : value.first
end
-
end
end
diff --git a/activesupport/lib/active_support/core_ext/hash/deep_merge.rb b/activesupport/lib/active_support/core_ext/hash/deep_merge.rb
index 9c9faf67ea..9bc50b7bc6 100644
--- a/activesupport/lib/active_support/core_ext/hash/deep_merge.rb
+++ b/activesupport/lib/active_support/core_ext/hash/deep_merge.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Hash
# Returns a new hash with +self+ and +other_hash+ merged recursively.
#
@@ -19,20 +21,14 @@ class Hash
# Same as +deep_merge+, but modifies +self+.
def deep_merge!(other_hash, &block)
- other_hash.each_pair do |current_key, other_value|
- this_value = self[current_key]
-
- self[current_key] = if this_value.is_a?(Hash) && other_value.is_a?(Hash)
- this_value.deep_merge(other_value, &block)
+ merge!(other_hash) do |key, this_val, other_val|
+ if this_val.is_a?(Hash) && other_val.is_a?(Hash)
+ this_val.deep_merge(other_val, &block)
+ elsif block_given?
+ block.call(key, this_val, other_val)
else
- if block_given? && key?(current_key)
- block.call(current_key, this_value, other_value)
- else
- other_value
- end
+ other_val
end
end
-
- self
end
end
diff --git a/activesupport/lib/active_support/core_ext/hash/except.rb b/activesupport/lib/active_support/core_ext/hash/except.rb
index 6e397abf51..6258610c98 100644
--- a/activesupport/lib/active_support/core_ext/hash/except.rb
+++ b/activesupport/lib/active_support/core_ext/hash/except.rb
@@ -1,8 +1,11 @@
+# frozen_string_literal: true
+
class Hash
- # Returns a hash that includes everything but the given keys.
- # hash = { a: true, b: false, c: nil}
- # hash.except(:c) # => { a: true, b: false}
- # hash # => { a: true, b: false, c: nil}
+ # Returns a hash that includes everything except given keys.
+ # hash = { a: true, b: false, c: nil }
+ # hash.except(:c) # => { a: true, b: false }
+ # hash.except(:a, :b) # => { c: nil }
+ # hash # => { a: true, b: false, c: nil }
#
# This is useful for limiting a set of parameters to everything but a few known toggles:
# @person.update(params[:person].except(:admin))
@@ -10,10 +13,10 @@ class Hash
dup.except!(*keys)
end
- # Replaces the hash without the given keys.
- # hash = { a: true, b: false, c: nil}
- # hash.except!(:c) # => { a: true, b: false}
- # hash # => { a: true, b: false }
+ # Removes the given keys from hash and returns it.
+ # hash = { a: true, b: false, c: nil }
+ # hash.except!(:c) # => { a: true, b: false }
+ # hash # => { a: true, b: false }
def except!(*keys)
keys.each { |key| delete(key) }
self
diff --git a/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb b/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb
index 28cb3e2a3b..a38f33f128 100644
--- a/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb
+++ b/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb
@@ -1,12 +1,13 @@
-require 'active_support/hash_with_indifferent_access'
+# frozen_string_literal: true
-class Hash
+require "active_support/hash_with_indifferent_access"
+class Hash
# Returns an <tt>ActiveSupport::HashWithIndifferentAccess</tt> out of its receiver:
#
# { a: 1 }.with_indifferent_access['a'] # => 1
def with_indifferent_access
- ActiveSupport::HashWithIndifferentAccess.new_from_hash_copying_default(self)
+ ActiveSupport::HashWithIndifferentAccess.new(self)
end
# Called when object is nested under an object that receives
diff --git a/activesupport/lib/active_support/core_ext/hash/keys.rb b/activesupport/lib/active_support/core_ext/hash/keys.rb
index c30044b9ff..bdf196ec3d 100644
--- a/activesupport/lib/active_support/core_ext/hash/keys.rb
+++ b/activesupport/lib/active_support/core_ext/hash/keys.rb
@@ -1,28 +1,34 @@
+# frozen_string_literal: true
+
class Hash
- # Returns a new hash with all keys converted using the block operation.
+ # Returns a new hash with all keys converted using the +block+ operation.
#
# hash = { name: 'Rob', age: '28' }
#
- # hash.transform_keys{ |key| key.to_s.upcase }
- # # => {"NAME"=>"Rob", "AGE"=>"28"}
+ # hash.transform_keys { |key| key.to_s.upcase } # => {"NAME"=>"Rob", "AGE"=>"28"}
+ #
+ # If you do not provide a +block+, it will return an Enumerator
+ # for chaining with other methods:
+ #
+ # hash.transform_keys.with_index { |k, i| [k, i].join } # => {"name0"=>"Rob", "age1"=>"28"}
def transform_keys
- return enum_for(:transform_keys) unless block_given?
- result = self.class.new
+ return enum_for(:transform_keys) { size } unless block_given?
+ result = {}
each_key do |key|
result[yield(key)] = self[key]
end
result
- end
+ end unless method_defined? :transform_keys
- # Destructively converts all keys using the block operations.
- # Same as transform_keys but modifies +self+.
+ # Destructively converts all keys using the +block+ operations.
+ # Same as +transform_keys+ but modifies +self+.
def transform_keys!
- return enum_for(:transform_keys!) unless block_given?
+ return enum_for(:transform_keys!) { size } unless block_given?
keys.each do |key|
self[yield(key)] = delete(key)
end
self
- end
+ end unless method_defined? :transform_keys!
# Returns a new hash with all keys converted to strings.
#
@@ -48,19 +54,19 @@ class Hash
# hash.symbolize_keys
# # => {:name=>"Rob", :age=>"28"}
def symbolize_keys
- transform_keys{ |key| key.to_sym rescue key }
+ transform_keys { |key| key.to_sym rescue key }
end
alias_method :to_options, :symbolize_keys
# Destructively converts all keys to symbols, as long as they respond
# to +to_sym+. Same as +symbolize_keys+, but modifies +self+.
def symbolize_keys!
- transform_keys!{ |key| key.to_sym rescue key }
+ transform_keys! { |key| key.to_sym rescue key }
end
alias_method :to_options!, :symbolize_keys!
# Validates all keys in a hash match <tt>*valid_keys</tt>, raising
- # ArgumentError on a mismatch.
+ # +ArgumentError+ on a mismatch.
#
# Note that keys are treated differently than HashWithIndifferentAccess,
# meaning that string and symbol keys will not match.
@@ -124,14 +130,14 @@ class Hash
# hash.deep_symbolize_keys
# # => {:person=>{:name=>"Rob", :age=>"28"}}
def deep_symbolize_keys
- deep_transform_keys{ |key| key.to_sym rescue key }
+ deep_transform_keys { |key| key.to_sym rescue key }
end
# Destructively converts all keys to symbols, as long as they respond
# to +to_sym+. This includes the keys from the root hash and from all
# nested hashes and arrays.
def deep_symbolize_keys!
- deep_transform_keys!{ |key| key.to_sym rescue key }
+ deep_transform_keys! { |key| key.to_sym rescue key }
end
private
@@ -143,7 +149,7 @@ class Hash
result[yield(key)] = _deep_transform_keys_in_object(value, &block)
end
when Array
- object.map {|e| _deep_transform_keys_in_object(e, &block) }
+ object.map { |e| _deep_transform_keys_in_object(e, &block) }
else
object
end
@@ -158,7 +164,7 @@ class Hash
end
object
when Array
- object.map! {|e| _deep_transform_keys_in_object!(e, &block)}
+ object.map! { |e| _deep_transform_keys_in_object!(e, &block) }
else
object
end
diff --git a/activesupport/lib/active_support/core_ext/hash/reverse_merge.rb b/activesupport/lib/active_support/core_ext/hash/reverse_merge.rb
index fbb482435d..ef8d592829 100644
--- a/activesupport/lib/active_support/core_ext/hash/reverse_merge.rb
+++ b/activesupport/lib/active_support/core_ext/hash/reverse_merge.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Hash
# Merges the caller into +other_hash+. For example,
#
@@ -12,11 +14,12 @@ class Hash
def reverse_merge(other_hash)
other_hash.merge(self)
end
+ alias_method :with_defaults, :reverse_merge
# Destructive +reverse_merge+.
def reverse_merge!(other_hash)
- # right wins if there is no left
- merge!( other_hash ){|key,left,right| left }
+ replace(reverse_merge(other_hash))
end
alias_method :reverse_update, :reverse_merge!
+ alias_method :with_defaults!, :reverse_merge!
end
diff --git a/activesupport/lib/active_support/core_ext/hash/slice.rb b/activesupport/lib/active_support/core_ext/hash/slice.rb
index 1d5f38231a..2bd0a56ea4 100644
--- a/activesupport/lib/active_support/core_ext/hash/slice.rb
+++ b/activesupport/lib/active_support/core_ext/hash/slice.rb
@@ -1,11 +1,13 @@
+# frozen_string_literal: true
+
class Hash
- # Slices a hash to include only the given keys. Returns a hash containing
+ # Slices a hash to include only the given keys. Returns a hash containing
# the given keys.
- #
+ #
# { a: 1, b: 2, c: 3, d: 4 }.slice(:a, :b)
# # => {:a=>1, :b=>2}
- #
- # This is useful for limiting an options hash to valid keys before
+ #
+ # This is useful for limiting an options hash to valid keys before
# passing to a method:
#
# def search(criteria = {})
@@ -19,9 +21,8 @@ class Hash
# valid_keys = [:mass, :velocity, :time]
# search(options.slice(*valid_keys))
def slice(*keys)
- keys.map! { |key| convert_key(key) } if respond_to?(:convert_key, true)
- keys.each_with_object(self.class.new) { |k, hash| hash[k] = self[k] if has_key?(k) }
- end
+ keys.each_with_object(Hash.new) { |k, hash| hash[k] = self[k] if has_key?(k) }
+ end unless method_defined?(:slice)
# Replaces the hash with only the given keys.
# Returns a hash containing the removed key/value pairs.
@@ -29,7 +30,6 @@ class Hash
# { a: 1, b: 2, c: 3, d: 4 }.slice!(:a, :b)
# # => {:c=>3, :d=>4}
def slice!(*keys)
- keys.map! { |key| convert_key(key) } if respond_to?(:convert_key, true)
omit = slice(*self.keys - keys)
hash = slice(*keys)
hash.default = default
diff --git a/activesupport/lib/active_support/core_ext/hash/transform_values.rb b/activesupport/lib/active_support/core_ext/hash/transform_values.rb
index e9bcce761f..fc15130c9e 100644
--- a/activesupport/lib/active_support/core_ext/hash/transform_values.rb
+++ b/activesupport/lib/active_support/core_ext/hash/transform_values.rb
@@ -1,23 +1,5 @@
-class Hash
- # Returns a new hash with the results of running +block+ once for every value.
- # The keys are unchanged.
- #
- # { a: 1, b: 2, c: 3 }.transform_values { |x| x * 2 }
- # # => { a: 2, b: 4, c: 6 }
- def transform_values
- return enum_for(:transform_values) unless block_given?
- result = self.class.new
- each do |key, value|
- result[key] = yield(value)
- end
- result
- end
+# frozen_string_literal: true
- # Destructive +transform_values+
- def transform_values!
- return enum_for(:transform_values!) unless block_given?
- each do |key, value|
- self[key] = yield(value)
- end
- end
-end
+require "active_support/deprecation"
+
+ActiveSupport::Deprecation.warn "Ruby 2.4+ (required by Rails 6) provides Hash#transform_values natively, so requiring active_support/core_ext/hash/transform_values is no longer necessary. Requiring it will raise LoadError in Rails 6.1."
diff --git a/activesupport/lib/active_support/core_ext/integer.rb b/activesupport/lib/active_support/core_ext/integer.rb
index a44a1b4c74..d22701306a 100644
--- a/activesupport/lib/active_support/core_ext/integer.rb
+++ b/activesupport/lib/active_support/core_ext/integer.rb
@@ -1,3 +1,5 @@
-require 'active_support/core_ext/integer/multiple'
-require 'active_support/core_ext/integer/inflections'
-require 'active_support/core_ext/integer/time'
+# frozen_string_literal: true
+
+require "active_support/core_ext/integer/multiple"
+require "active_support/core_ext/integer/inflections"
+require "active_support/core_ext/integer/time"
diff --git a/activesupport/lib/active_support/core_ext/integer/inflections.rb b/activesupport/lib/active_support/core_ext/integer/inflections.rb
index 56f2ed5985..aef3266f28 100644
--- a/activesupport/lib/active_support/core_ext/integer/inflections.rb
+++ b/activesupport/lib/active_support/core_ext/integer/inflections.rb
@@ -1,4 +1,6 @@
-require 'active_support/inflector'
+# frozen_string_literal: true
+
+require "active_support/inflector"
class Integer
# Ordinalize turns a number into an ordinal string used to denote the
diff --git a/activesupport/lib/active_support/core_ext/integer/multiple.rb b/activesupport/lib/active_support/core_ext/integer/multiple.rb
index c668c7c2eb..bd57a909c5 100644
--- a/activesupport/lib/active_support/core_ext/integer/multiple.rb
+++ b/activesupport/lib/active_support/core_ext/integer/multiple.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Integer
# Check whether the integer is evenly divisible by the argument.
#
@@ -5,6 +7,6 @@ class Integer
# 6.multiple_of?(5) # => false
# 10.multiple_of?(2) # => true
def multiple_of?(number)
- number != 0 ? self % number == 0 : zero?
+ number == 0 ? self == 0 : self % number == 0
end
end
diff --git a/activesupport/lib/active_support/core_ext/integer/time.rb b/activesupport/lib/active_support/core_ext/integer/time.rb
index f0b7382ef3..5efb89cf9f 100644
--- a/activesupport/lib/active_support/core_ext/integer/time.rb
+++ b/activesupport/lib/active_support/core_ext/integer/time.rb
@@ -1,29 +1,22 @@
-require 'active_support/duration'
-require 'active_support/core_ext/numeric/time'
+# frozen_string_literal: true
+
+require "active_support/duration"
+require "active_support/core_ext/numeric/time"
class Integer
- # Enables the use of time calculations and declarations, like <tt>45.minutes +
- # 2.hours + 4.years</tt>.
- #
- # These methods use Time#advance for precise date calculations when using
- # <tt>from_now</tt>, +ago+, etc. as well as adding or subtracting their
- # results from a Time object.
- #
- # # equivalent to Time.now.advance(months: 1)
- # 1.month.from_now
+ # Returns a Duration instance matching the number of months provided.
#
- # # equivalent to Time.now.advance(years: 2)
- # 2.years.from_now
- #
- # # equivalent to Time.now.advance(months: 4, years: 5)
- # (4.months + 5.years).from_now
+ # 2.months # => 2 months
def months
- ActiveSupport::Duration.new(self * 30.days, [[:months, self]])
+ ActiveSupport::Duration.months(self)
end
alias :month :months
+ # Returns a Duration instance matching the number of years provided.
+ #
+ # 2.years # => 2 years
def years
- ActiveSupport::Duration.new(self * 365.25.days, [[:years, self]])
+ ActiveSupport::Duration.years(self)
end
alias :year :years
end
diff --git a/activesupport/lib/active_support/core_ext/kernel.rb b/activesupport/lib/active_support/core_ext/kernel.rb
index 364ed9d65f..0f4356fbdd 100644
--- a/activesupport/lib/active_support/core_ext/kernel.rb
+++ b/activesupport/lib/active_support/core_ext/kernel.rb
@@ -1,4 +1,6 @@
-require 'active_support/core_ext/kernel/agnostics'
-require 'active_support/core_ext/kernel/concern'
-require 'active_support/core_ext/kernel/reporting'
-require 'active_support/core_ext/kernel/singleton_class'
+# frozen_string_literal: true
+
+require "active_support/core_ext/kernel/agnostics"
+require "active_support/core_ext/kernel/concern"
+require "active_support/core_ext/kernel/reporting"
+require "active_support/core_ext/kernel/singleton_class"
diff --git a/activesupport/lib/active_support/core_ext/kernel/agnostics.rb b/activesupport/lib/active_support/core_ext/kernel/agnostics.rb
index 64837d87aa..403b5f31f0 100644
--- a/activesupport/lib/active_support/core_ext/kernel/agnostics.rb
+++ b/activesupport/lib/active_support/core_ext/kernel/agnostics.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Object
# Makes backticks behave (somewhat more) similarly on all platforms.
# On win32 `nonexistent_command` raises Errno::ENOENT; on Unix, the
diff --git a/activesupport/lib/active_support/core_ext/kernel/concern.rb b/activesupport/lib/active_support/core_ext/kernel/concern.rb
index bf72caa058..0b2baed780 100644
--- a/activesupport/lib/active_support/core_ext/kernel/concern.rb
+++ b/activesupport/lib/active_support/core_ext/kernel/concern.rb
@@ -1,6 +1,10 @@
-require 'active_support/core_ext/module/concerning'
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/concerning"
module Kernel
+ module_function
+
# A shortcut to define a toplevel concern, not within a module.
#
# See Module::Concerning for more.
diff --git a/activesupport/lib/active_support/core_ext/kernel/debugger.rb b/activesupport/lib/active_support/core_ext/kernel/debugger.rb
deleted file mode 100644
index 1fde3db070..0000000000
--- a/activesupport/lib/active_support/core_ext/kernel/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/activesupport/lib/active_support/core_ext/kernel/reporting.rb b/activesupport/lib/active_support/core_ext/kernel/reporting.rb
index 9189e6d977..9155bd6c10 100644
--- a/activesupport/lib/active_support/core_ext/kernel/reporting.rb
+++ b/activesupport/lib/active_support/core_ext/kernel/reporting.rb
@@ -1,7 +1,9 @@
-require 'tempfile'
+# frozen_string_literal: true
module Kernel
- # Sets $VERBOSE to nil for the duration of the block and back to its original
+ module_function
+
+ # Sets $VERBOSE to +nil+ for the duration of the block and back to its original
# value afterwards.
#
# silence_warnings do
diff --git a/activesupport/lib/active_support/core_ext/kernel/singleton_class.rb b/activesupport/lib/active_support/core_ext/kernel/singleton_class.rb
index 9bbf1bbd73..6715eba80a 100644
--- a/activesupport/lib/active_support/core_ext/kernel/singleton_class.rb
+++ b/activesupport/lib/active_support/core_ext/kernel/singleton_class.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Kernel
# class_eval on an object acts like singleton_class.class_eval.
def class_eval(*args, &block)
diff --git a/activesupport/lib/active_support/core_ext/load_error.rb b/activesupport/lib/active_support/core_ext/load_error.rb
index d9fb392752..b81ed0605e 100644
--- a/activesupport/lib/active_support/core_ext/load_error.rb
+++ b/activesupport/lib/active_support/core_ext/load_error.rb
@@ -1,30 +1,9 @@
-require 'active_support/deprecation/proxy_wrappers'
+# frozen_string_literal: true
class LoadError
- REGEXPS = [
- /^no such file to load -- (.+)$/i,
- /^Missing \w+ (?:file\s*)?([^\s]+.rb)$/i,
- /^Missing API definition file in (.+)$/i,
- /^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)
- location.sub(/\.rb$/, '') == path.sub(/\.rb$/, '')
+ location.sub(/\.rb$/, "") == path.to_s.sub(/\.rb$/, "")
end
end
-
-MissingSourceFile = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('MissingSourceFile', 'LoadError')
diff --git a/activesupport/lib/active_support/core_ext/marshal.rb b/activesupport/lib/active_support/core_ext/marshal.rb
index 20a0856e71..0c72cd7b47 100644
--- a/activesupport/lib/active_support/core_ext/marshal.rb
+++ b/activesupport/lib/active_support/core_ext/marshal.rb
@@ -1,12 +1,17 @@
+# frozen_string_literal: true
+
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 (.+)|)
+ if exc.message.match(%r|undefined class/module (.+?)(?:::)?\z|)
# try loading the class/module
- $1.constantize
- # if it is a IO we need to go back to read the object
+ loaded = $1.constantize
+
+ raise unless $1 == loaded.name
+
+ # if it is an IO we need to go back to read the object
source.rewind if source.respond_to?(:rewind)
retry
else
diff --git a/activesupport/lib/active_support/core_ext/module.rb b/activesupport/lib/active_support/core_ext/module.rb
index b4efff8b24..d91e3fba6a 100644
--- a/activesupport/lib/active_support/core_ext/module.rb
+++ b/activesupport/lib/active_support/core_ext/module.rb
@@ -1,11 +1,14 @@
-require 'active_support/core_ext/module/aliasing'
-require 'active_support/core_ext/module/introspection'
-require 'active_support/core_ext/module/anonymous'
-require 'active_support/core_ext/module/reachable'
-require 'active_support/core_ext/module/attribute_accessors'
-require 'active_support/core_ext/module/attr_internal'
-require 'active_support/core_ext/module/concerning'
-require 'active_support/core_ext/module/delegation'
-require 'active_support/core_ext/module/deprecation'
-require 'active_support/core_ext/module/remove_method'
-require 'active_support/core_ext/module/qualified_const'
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/aliasing"
+require "active_support/core_ext/module/introspection"
+require "active_support/core_ext/module/anonymous"
+require "active_support/core_ext/module/reachable"
+require "active_support/core_ext/module/attribute_accessors"
+require "active_support/core_ext/module/attribute_accessors_per_thread"
+require "active_support/core_ext/module/attr_internal"
+require "active_support/core_ext/module/concerning"
+require "active_support/core_ext/module/delegation"
+require "active_support/core_ext/module/deprecation"
+require "active_support/core_ext/module/redefine_method"
+require "active_support/core_ext/module/remove_method"
diff --git a/activesupport/lib/active_support/core_ext/module/aliasing.rb b/activesupport/lib/active_support/core_ext/module/aliasing.rb
index b6934b9c54..6f64d11627 100644
--- a/activesupport/lib/active_support/core_ext/module/aliasing.rb
+++ b/activesupport/lib/active_support/core_ext/module/aliasing.rb
@@ -1,52 +1,6 @@
-class Module
- # NOTE: This method is deprecated. Please use <tt>Module#prepend</tt> that
- # comes with Ruby 2.0 or newer instead.
- #
- # Encapsulates the common pattern of:
- #
- # alias_method :foo_without_feature, :foo
- # alias_method :foo, :foo_with_feature
- #
- # With this, you simply do:
- #
- # alias_method_chain :foo, :feature
- #
- # And both aliases are set up for you.
- #
- # Query and bang methods (foo?, foo!) keep the same punctuation:
- #
- # alias_method_chain :foo?, :feature
- #
- # is equivalent to
- #
- # alias_method :foo_without_feature?, :foo?
- # alias_method :foo?, :foo_with_feature?
- #
- # so you can safely chain foo, foo?, foo! and/or foo= with the same feature.
- def alias_method_chain(target, feature)
- ActiveSupport::Deprecation.warn("alias_method_chain is deprecated. Please, use Module#prepend instead. From module, you can access the original method using super.")
-
- # Strip out punctuation on predicates, bang or writer methods since
- # e.g. target?_without_feature is not a valid method name.
- aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ''), $1
- yield(aliased_target, punctuation) if block_given?
-
- with_method = "#{aliased_target}_with_#{feature}#{punctuation}"
- without_method = "#{aliased_target}_without_#{feature}#{punctuation}"
-
- alias_method without_method, target
- alias_method target, with_method
-
- case
- when public_method_defined?(without_method)
- public target
- when protected_method_defined?(without_method)
- protected target
- when private_method_defined?(without_method)
- private target
- end
- end
+# frozen_string_literal: true
+class Module
# Allows you to make aliases for attributes, which includes
# getter, setter, and a predicate.
#
@@ -65,6 +19,9 @@ class Module
# e.subject = "Megastars"
# e.title # => "Megastars"
def alias_attribute(new_name, old_name)
+ # The following reader methods use an explicit `self` receiver in order to
+ # support aliases that start with an uppercase letter. Otherwise, they would
+ # be resolved as constants instead.
module_eval <<-STR, __FILE__, __LINE__ + 1
def #{new_name}; self.#{old_name}; end # def subject; self.title; end
def #{new_name}?; self.#{old_name}?; end # def subject?; self.title?; end
diff --git a/activesupport/lib/active_support/core_ext/module/anonymous.rb b/activesupport/lib/active_support/core_ext/module/anonymous.rb
index 0ecc67a855..d1c86b8722 100644
--- a/activesupport/lib/active_support/core_ext/module/anonymous.rb
+++ b/activesupport/lib/active_support/core_ext/module/anonymous.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Module
# A module may or may not have a name.
#
@@ -7,7 +9,7 @@ class Module
# m = Module.new
# m.name # => nil
#
- # +anonymous?+ method returns true if module does not have a name:
+ # +anonymous?+ method returns true if module does not have a name, false otherwise:
#
# Module.new.anonymous? # => true
#
@@ -18,8 +20,10 @@ class Module
# via the +module+ or +class+ keyword or by an explicit assignment:
#
# m = Module.new # creates an anonymous module
- # M = m # => m gets a name here as a side-effect
+ # m.anonymous? # => true
+ # M = m # m gets a name here as a side-effect
# m.name # => "M"
+ # m.anonymous? # => false
def anonymous?
name.nil?
end
diff --git a/activesupport/lib/active_support/core_ext/module/attr_internal.rb b/activesupport/lib/active_support/core_ext/module/attr_internal.rb
index 93fb598650..7801f6d181 100644
--- a/activesupport/lib/active_support/core_ext/module/attr_internal.rb
+++ b/activesupport/lib/active_support/core_ext/module/attr_internal.rb
@@ -1,12 +1,14 @@
+# frozen_string_literal: true
+
class Module
# Declares an attribute reader backed by an internally-named instance variable.
def attr_internal_reader(*attrs)
- attrs.each {|attr_name| attr_internal_define(attr_name, :reader)}
+ attrs.each { |attr_name| attr_internal_define(attr_name, :reader) }
end
# Declares an attribute writer backed by an internally-named instance variable.
def attr_internal_writer(*attrs)
- attrs.each {|attr_name| attr_internal_define(attr_name, :writer)}
+ attrs.each { |attr_name| attr_internal_define(attr_name, :writer) }
end
# Declares an attribute reader and writer backed by an internally-named instance
@@ -18,7 +20,7 @@ class Module
alias_method :attr_internal, :attr_internal_accessor
class << self; attr_accessor :attr_internal_naming_format end
- self.attr_internal_naming_format = '@_%s'
+ self.attr_internal_naming_format = "@_%s"
private
def attr_internal_ivar_name(attr)
@@ -26,7 +28,7 @@ class Module
end
def attr_internal_define(attr_name, type)
- internal_name = attr_internal_ivar_name(attr_name).sub(/\A@/, '')
+ internal_name = attr_internal_ivar_name(attr_name).sub(/\A@/, "")
# use native attr_* methods as they are faster on some Ruby implementations
send("attr_#{type}", internal_name)
attr_name, internal_name = "#{attr_name}=", "#{internal_name}=" if type == :writer
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 d4e6b5a1ac..5850e0193f 100644
--- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb
+++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb
@@ -1,12 +1,13 @@
-require 'active_support/core_ext/array/extract_options'
+# frozen_string_literal: true
# Extends the module object with class/module and instance accessors for
# class/module attributes, just like the native attr* accessors for instance
# attributes.
class Module
# Defines a class attribute and creates a class and instance reader methods.
- # The underlying the class variable is set to +nil+, if it is not previously
- # defined.
+ # The underlying class variable is set to +nil+, if it is not previously
+ # 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
@@ -19,15 +20,15 @@ class Module
# The attribute name must be a valid method name in Ruby.
#
# module Foo
- # mattr_reader :"1_Badname "
+ # mattr_reader :"1_Badname"
# end
- # # => NameError: invalid attribute name
+ # # => NameError: invalid attribute name: 1_Badname
#
# If you want to opt out the creation on the instance reader method, pass
# <tt>instance_reader: false</tt> or <tt>instance_accessor: false</tt>.
#
# module HairColors
- # mattr_writer :hair_colors, instance_reader: false
+ # mattr_reader :hair_colors, instance_reader: false
# end
#
# class Person
@@ -36,24 +37,20 @@ class Module
#
# Person.new.hair_colors # => NoMethodError
#
- #
- # Also, you can pass a block to set up the attribute with a default value.
+ # You can set a default value for the attribute.
#
# module HairColors
- # cattr_reader :hair_colors do
- # [:brown, :black, :blonde, :red]
- # end
+ # mattr_reader :hair_colors, default: [:brown, :black, :blonde, :red]
# end
#
# class Person
# include HairColors
# end
#
- # Person.hair_colors # => [:brown, :black, :blonde, :red]
- def mattr_reader(*syms)
- options = syms.extract_options!
+ # Person.new.hair_colors # => [:brown, :black, :blonde, :red]
+ def mattr_reader(*syms, instance_reader: true, instance_accessor: true, default: nil)
syms.each do |sym|
- raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/
+ raise NameError.new("invalid attribute name: #{sym}") unless /\A[_A-Za-z]\w*\z/.match?(sym)
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
@@#{sym} = nil unless defined? @@#{sym}
@@ -62,20 +59,24 @@ class Module
end
EOS
- unless options[:instance_reader] == false || options[:instance_accessor] == false
+ if instance_reader && instance_accessor
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
def #{sym}
@@#{sym}
end
EOS
end
- class_variable_set("@@#{sym}", yield) if block_given?
+
+ sym_default_value = (block_given? && default.nil?) ? yield : default
+ class_variable_set("@@#{sym}", sym_default_value) unless sym_default_value.nil?
end
end
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
@@ -103,12 +104,10 @@ class Module
#
# Person.new.hair_colors = [:blonde, :red] # => NoMethodError
#
- # Also, you can pass a block to set up the attribute with a default value.
+ # You can set a default value for the attribute.
#
- # class HairColors
- # mattr_writer :hair_colors do
- # [:brown, :black, :blonde, :red]
- # end
+ # module HairColors
+ # mattr_writer :hair_colors, default: [:brown, :black, :blonde, :red]
# end
#
# class Person
@@ -116,10 +115,9 @@ class Module
# end
#
# Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red]
- def mattr_writer(*syms)
- options = syms.extract_options!
+ def mattr_writer(*syms, instance_writer: true, instance_accessor: true, default: nil)
syms.each do |sym|
- raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/
+ raise NameError.new("invalid attribute name: #{sym}") unless /\A[_A-Za-z]\w*\z/.match?(sym)
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
@@#{sym} = nil unless defined? @@#{sym}
@@ -128,19 +126,23 @@ class Module
end
EOS
- unless options[:instance_writer] == false || options[:instance_accessor] == false
+ if instance_writer && instance_accessor
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
def #{sym}=(obj)
@@#{sym} = obj
end
EOS
end
- send("#{sym}=", yield) if block_given?
+
+ sym_default_value = (block_given? && default.nil?) ? yield : default
+ send("#{sym}=", sym_default_value) unless sym_default_value.nil?
end
end
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
@@ -150,19 +152,19 @@ class Module
# include HairColors
# end
#
- # Person.hair_colors = [:brown, :black, :blonde, :red]
- # Person.hair_colors # => [:brown, :black, :blonde, :red]
+ # HairColors.hair_colors = [:brown, :black, :blonde, :red]
+ # HairColors.hair_colors # => [:brown, :black, :blonde, :red]
# Person.new.hair_colors # => [:brown, :black, :blonde, :red]
#
# If a subclass changes the value then that would also change the value for
# parent class. Similarly if parent class changes the value then that would
# change the value of subclasses too.
#
- # class Male < Person
+ # class Citizen < Person
# end
#
- # Male.hair_colors << :blue
- # Person.hair_colors # => [:brown, :black, :blonde, :red, :blue]
+ # Citizen.new.hair_colors << :blue
+ # Person.new.hair_colors # => [:brown, :black, :blonde, :red, :blue]
#
# To opt out of the instance writer method, pass <tt>instance_writer: false</tt>.
# To opt out of the instance reader method, pass <tt>instance_reader: false</tt>.
@@ -191,12 +193,10 @@ class Module
# Person.new.hair_colors = [:brown] # => NoMethodError
# Person.new.hair_colors # => NoMethodError
#
- # Also you can pass a block to set up the attribute with a default value.
+ # You can set a default value for the attribute.
#
# module HairColors
- # mattr_accessor :hair_colors do
- # [:brown, :black, :blonde, :red]
- # end
+ # mattr_accessor :hair_colors, default: [:brown, :black, :blonde, :red]
# end
#
# class Person
@@ -204,9 +204,9 @@ class Module
# end
#
# Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red]
- def mattr_accessor(*syms, &blk)
- mattr_reader(*syms, &blk)
- mattr_writer(*syms, &blk)
+ def mattr_accessor(*syms, instance_reader: true, instance_writer: true, instance_accessor: true, default: nil, &blk)
+ mattr_reader(*syms, instance_reader: instance_reader, instance_accessor: instance_accessor, default: default, &blk)
+ mattr_writer(*syms, instance_writer: instance_writer, instance_accessor: instance_accessor, default: default)
end
alias :cattr_accessor :mattr_accessor
end
diff --git a/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb b/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb
new file mode 100644
index 0000000000..cb996e9e6d
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb
@@ -0,0 +1,144 @@
+# frozen_string_literal: true
+
+# Extends the module object with class/module and instance accessors for
+# class/module attributes, just like the native attr* accessors for instance
+# attributes, but does so on a per-thread basis.
+#
+# So the values are scoped within the Thread.current space under the class name
+# of the module.
+class Module
+ # Defines a per-thread class attribute and creates class and instance reader methods.
+ # The underlying per-thread class variable is set to +nil+, if it is not previously defined.
+ #
+ # module Current
+ # thread_mattr_reader :user
+ # end
+ #
+ # Current.user # => nil
+ # Thread.current[:attr_Current_user] = "DHH"
+ # Current.user # => "DHH"
+ #
+ # The attribute name must be a valid method name in Ruby.
+ #
+ # module Foo
+ # thread_mattr_reader :"1_Badname"
+ # end
+ # # => NameError: invalid attribute name: 1_Badname
+ #
+ # If you want to opt out of the creation of the instance reader method, pass
+ # <tt>instance_reader: false</tt> or <tt>instance_accessor: false</tt>.
+ #
+ # class Current
+ # thread_mattr_reader :user, instance_reader: false
+ # end
+ #
+ # Current.new.user # => NoMethodError
+ def thread_mattr_reader(*syms, instance_reader: true, instance_accessor: true) # :nodoc:
+ syms.each do |sym|
+ raise NameError.new("invalid attribute name: #{sym}") unless /^[_A-Za-z]\w*$/.match?(sym)
+
+ # The following generated method concatenates `name` because we want it
+ # to work with inheritance via polymorphism.
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
+ def self.#{sym}
+ Thread.current["attr_" + name + "_#{sym}"]
+ end
+ EOS
+
+ if instance_reader && instance_accessor
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
+ def #{sym}
+ self.class.#{sym}
+ end
+ EOS
+ end
+ end
+ end
+ alias :thread_cattr_reader :thread_mattr_reader
+
+ # Defines a per-thread class attribute and creates a class and instance writer methods to
+ # allow assignment to the attribute.
+ #
+ # module Current
+ # thread_mattr_writer :user
+ # end
+ #
+ # Current.user = "DHH"
+ # Thread.current[:attr_Current_user] # => "DHH"
+ #
+ # If you want to opt out of the creation of the instance writer method, pass
+ # <tt>instance_writer: false</tt> or <tt>instance_accessor: false</tt>.
+ #
+ # class Current
+ # thread_mattr_writer :user, instance_writer: false
+ # end
+ #
+ # Current.new.user = "DHH" # => NoMethodError
+ def thread_mattr_writer(*syms, instance_writer: true, instance_accessor: true) # :nodoc:
+ syms.each do |sym|
+ raise NameError.new("invalid attribute name: #{sym}") unless /^[_A-Za-z]\w*$/.match?(sym)
+
+ # The following generated method concatenates `name` because we want it
+ # to work with inheritance via polymorphism.
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
+ def self.#{sym}=(obj)
+ Thread.current["attr_" + name + "_#{sym}"] = obj
+ end
+ EOS
+
+ if instance_writer && instance_accessor
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
+ def #{sym}=(obj)
+ self.class.#{sym} = obj
+ end
+ EOS
+ end
+ end
+ end
+ alias :thread_cattr_writer :thread_mattr_writer
+
+ # Defines both class and instance accessors for class attributes.
+ #
+ # class Account
+ # thread_mattr_accessor :user
+ # end
+ #
+ # Account.user = "DHH"
+ # Account.user # => "DHH"
+ # Account.new.user # => "DHH"
+ #
+ # If a subclass changes the value, the parent class' value is not changed.
+ # Similarly, if the parent class changes the value, the value of subclasses
+ # is not changed.
+ #
+ # class Customer < Account
+ # end
+ #
+ # Customer.user = "Rafael"
+ # Customer.user # => "Rafael"
+ # Account.user # => "DHH"
+ #
+ # To opt out of the instance writer method, pass <tt>instance_writer: false</tt>.
+ # To opt out of the instance reader method, pass <tt>instance_reader: false</tt>.
+ #
+ # class Current
+ # thread_mattr_accessor :user, instance_writer: false, instance_reader: false
+ # end
+ #
+ # Current.new.user = "DHH" # => NoMethodError
+ # Current.new.user # => NoMethodError
+ #
+ # Or pass <tt>instance_accessor: false</tt>, to opt out both instance methods.
+ #
+ # class Current
+ # thread_mattr_accessor :user, instance_accessor: false
+ # end
+ #
+ # Current.new.user = "DHH" # => NoMethodError
+ # Current.new.user # => NoMethodError
+ def thread_mattr_accessor(*syms, instance_reader: true, instance_writer: true, instance_accessor: true)
+ thread_mattr_reader(*syms, instance_reader: instance_reader, instance_accessor: instance_accessor)
+ thread_mattr_writer(*syms, instance_writer: instance_writer, instance_accessor: instance_accessor)
+ end
+ alias :thread_cattr_accessor :thread_mattr_accessor
+end
diff --git a/activesupport/lib/active_support/core_ext/module/concerning.rb b/activesupport/lib/active_support/core_ext/module/concerning.rb
index 65b88b9bbd..7bbbf321ab 100644
--- a/activesupport/lib/active_support/core_ext/module/concerning.rb
+++ b/activesupport/lib/active_support/core_ext/module/concerning.rb
@@ -1,4 +1,6 @@
-require 'active_support/concern'
+# frozen_string_literal: true
+
+require "active_support/concern"
class Module
# = Bite-sized separation of concerns
@@ -20,7 +22,7 @@ class Module
#
# == Using comments:
#
- # class Todo
+ # class Todo < ApplicationRecord
# # Other todo implementation
# # ...
#
@@ -28,7 +30,6 @@ class Module
# has_many :events
#
# before_create :track_creation
- # after_destroy :track_deletion
#
# private
# def track_creation
@@ -40,7 +41,7 @@ class Module
#
# Noisy syntax.
#
- # class Todo
+ # class Todo < ApplicationRecord
# # Other todo implementation
# # ...
#
@@ -50,7 +51,6 @@ class Module
# included do
# has_many :events
# before_create :track_creation
- # after_destroy :track_deletion
# end
#
# private
@@ -68,7 +68,7 @@ class Module
# increased overhead can be a reasonable tradeoff even if it reduces our
# at-a-glance perception of how things work.
#
- # class Todo
+ # class Todo < ApplicationRecord
# # Other todo implementation
# # ...
#
@@ -80,7 +80,7 @@ class Module
# By quieting the mix-in noise, we arrive at a natural, low-ceremony way to
# separate bite-sized concerns.
#
- # class Todo
+ # class Todo < ApplicationRecord
# # Other todo implementation
# # ...
#
@@ -88,7 +88,6 @@ class Module
# included do
# has_many :events
# before_create :track_creation
- # after_destroy :track_deletion
# end
#
# private
@@ -99,7 +98,7 @@ class Module
# end
#
# Todo.ancestors
- # # => [Todo, Todo::EventTracking, Object]
+ # # => [Todo, Todo::EventTracking, ApplicationRecord, Object]
#
# This small step has some wonderful ripple effects. We can
# * grok the behavior of our class in one glance,
diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb
index 9b7a429db9..3128539112 100644
--- a/activesupport/lib/active_support/core_ext/module/delegation.rb
+++ b/activesupport/lib/active_support/core_ext/module/delegation.rb
@@ -1,14 +1,18 @@
-require 'set'
+# frozen_string_literal: true
+
+require "set"
class Module
# Error generated by +delegate+ when a method is called on +nil+ and +allow_nil+
# option is not used.
class DelegationError < NoMethodError; end
- RUBY_RESERVED_WORDS = Set.new(
- %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)
+ 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(
+ RUBY_RESERVED_KEYWORDS + DELEGATION_RESERVED_KEYWORDS
).freeze
# Provides a +delegate+ class method to easily expose contained objects'
@@ -17,7 +21,9 @@ class Module
# ==== Options
# * <tt>:to</tt> - Specifies the target object
# * <tt>:prefix</tt> - Prefixes the new method with the target name or a custom prefix
- # * <tt>:allow_nil</tt> - if set to true, prevents a +NoMethodError+ from being raised
+ # * <tt>:allow_nil</tt> - If set to true, prevents a +Module::DelegationError+
+ # from being raised
+ # * <tt>:private</tt> - If set to true, changes method visibility to private
#
# The macro receives one or more method names (specified as symbols or
# strings) and the name of the target object via the <tt>:to</tt> option
@@ -108,19 +114,34 @@ class Module
# invoice.customer_name # => 'John Doe'
# invoice.customer_address # => 'Vimmersvej 13'
#
+ # The delegated methods are public by default.
+ # Pass <tt>private: true</tt> to change that.
+ #
+ # class User < ActiveRecord::Base
+ # has_one :profile
+ # delegate :first_name, to: :profile
+ # delegate :date_of_birth, to: :profile, private: true
+ #
+ # def age
+ # Date.today.year - date_of_birth.year
+ # end
+ # end
+ #
+ # User.new.first_name # => "Tomas"
+ # User.new.date_of_birth # => NoMethodError: private method `date_of_birth' called for #<User:0x00000008221340>
+ # User.new.age # => 2
+ #
# If the target is +nil+ and does not respond to the delegated method a
- # +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. If you wish to instead return +nil+,
+ # use the <tt>:allow_nil</tt> option.
#
# 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:
@@ -147,36 +168,32 @@ class Module
# Foo.new("Bar").name # raises NoMethodError: undefined method `name'
#
# The target method must be public, otherwise it will raise +NoMethodError+.
- #
- def delegate(*methods)
- options = methods.pop
- unless options.is_a?(Hash) && to = options[:to]
- raise ArgumentError, 'Delegation needs a target. Supply an options hash with a :to key as the last argument (e.g. delegate :hello, to: :greeter).'
+ def delegate(*methods, to: nil, prefix: nil, allow_nil: nil, private: nil)
+ unless to
+ raise ArgumentError, "Delegation needs a target. Supply an options hash with a :to key as the last argument (e.g. delegate :hello, to: :greeter)."
end
- prefix, allow_nil = options.values_at(:prefix, :allow_nil)
-
- if prefix == true && to =~ /^[^a-z_]/
- raise ArgumentError, 'Can only automatically set the delegation prefix when delegating to a method.'
+ if prefix == true && /^[^a-z_]/.match?(to)
+ raise ArgumentError, "Can only automatically set the delegation prefix when delegating to a method."
end
method_prefix = \
if prefix
"#{prefix == true ? to : prefix}_"
else
- ''
+ ""
end
- file, line = caller(1, 1).first.split(':', 2)
- line = line.to_i
+ location = caller_locations(1, 1).first
+ file, line = location.path, location.lineno
to = to.to_s
- to = "self.#{to}" if RUBY_RESERVED_WORDS.include?(to)
+ to = "self.#{to}" if DELEGATION_RESERVED_METHOD_NAMES.include?(to)
- methods.each do |method|
+ method_names = methods.map do |method|
# Attribute writer methods only accept one argument. Makes sure []=
# methods still accept two arguments.
- definition = (method =~ /[^\]]=$/) ? 'arg' : '*args, &block'
+ definition = /[^\]]=$/.match?(method) ? "arg" : "*args, &block"
# The following generated method calls the target exactly once, storing
# the returned value in a dummy variable.
@@ -193,7 +210,7 @@ class Module
" _.#{method}(#{definition})",
"end",
"end"
- ].join ';'
+ ].join ";"
else
exception = %(raise DelegationError, "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}")
@@ -208,10 +225,83 @@ class Module
" raise",
" end",
"end"
- ].join ';'
+ ].join ";"
end
module_eval(method_def, file, line)
end
+
+ private(*method_names) if private
+ method_names
+ end
+
+ # When building decorators, a common pattern may emerge:
+ #
+ # class Partition
+ # def initialize(event)
+ # @event = event
+ # end
+ #
+ # def person
+ # @event.detail.person || @event.creator
+ # end
+ #
+ # private
+ # def respond_to_missing?(name, include_private = false)
+ # @event.respond_to?(name, include_private)
+ # end
+ #
+ # def method_missing(method, *args, &block)
+ # @event.send(method, *args, &block)
+ # end
+ # end
+ #
+ # With <tt>Module#delegate_missing_to</tt>, the above is condensed to:
+ #
+ # class Partition
+ # delegate_missing_to :@event
+ #
+ # def initialize(event)
+ # @event = event
+ # end
+ #
+ # def person
+ # @event.detail.person || @event.creator
+ # end
+ # end
+ #
+ # The target can be anything callable within the object, e.g. instance
+ # variables, methods, constants, etc.
+ #
+ # The delegated method must be public on the target, otherwise it will
+ # raise +NoMethodError+.
+ def delegate_missing_to(target)
+ target = target.to_s
+ target = "self.#{target}" if DELEGATION_RESERVED_METHOD_NAMES.include?(target)
+
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def respond_to_missing?(name, include_private = false)
+ # It may look like an oversight, but we deliberately do not pass
+ # +include_private+, because they do not get delegated.
+
+ #{target}.respond_to?(name) || super
+ end
+
+ def method_missing(method, *args, &block)
+ if #{target}.respond_to?(method)
+ #{target}.public_send(method, *args, &block)
+ else
+ begin
+ super
+ rescue NoMethodError
+ if #{target}.nil?
+ raise DelegationError, "\#{method} delegated to #{target}, but #{target} is nil"
+ else
+ raise
+ end
+ end
+ end
+ end
+ RUBY
end
end
diff --git a/activesupport/lib/active_support/core_ext/module/deprecation.rb b/activesupport/lib/active_support/core_ext/module/deprecation.rb
index 56d670fbe8..71c42eb357 100644
--- a/activesupport/lib/active_support/core_ext/module/deprecation.rb
+++ b/activesupport/lib/active_support/core_ext/module/deprecation.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Module
# deprecate :foo
# deprecate bar: 'message'
@@ -13,8 +15,8 @@ class Module
#
# class MyLib::Deprecator
# def deprecation_warning(deprecated_method_name, message, caller_backtrace = nil)
- # message = "#{deprecated_method_name} is deprecated and will be removed from MyLibrary | #{message}"
- # Kernel.warn message
+ # message = "#{deprecated_method_name} is deprecated and will be removed from MyLibrary | #{message}"
+ # Kernel.warn message
# end
# end
def deprecate(*method_names)
diff --git a/activesupport/lib/active_support/core_ext/module/introspection.rb b/activesupport/lib/active_support/core_ext/module/introspection.rb
index f1d26ef28f..9b6df40596 100644
--- a/activesupport/lib/active_support/core_ext/module/introspection.rb
+++ b/activesupport/lib/active_support/core_ext/module/introspection.rb
@@ -1,17 +1,29 @@
-require 'active_support/inflector'
+# frozen_string_literal: true
+
+require "active_support/inflector"
class Module
# Returns the name of the module containing this one.
#
- # M::N.parent_name # => "M"
- def parent_name
- if defined? @parent_name
+ # M::N.module_parent_name # => "M"
+ def module_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
+ def parent_name
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ `Module#parent_name` has been renamed to `module_parent_name`.
+ `parent_name` is deprecated and will be removed in Rails 6.1.
+ MSG
+ module_parent_name
+ end
+
# Returns the module which contains this one according to its name.
#
# module M
@@ -20,15 +32,23 @@ class Module
# end
# X = M::N
#
- # M::N.parent # => M
- # X.parent # => M
+ # M::N.module_parent # => M
+ # X.module_parent # => M
#
# The parent of top-level and anonymous modules is Object.
#
- # M.parent # => Object
- # Module.new.parent # => Object
+ # M.module_parent # => Object
+ # Module.new.module_parent # => Object
+ def module_parent
+ module_parent_name ? ActiveSupport::Inflector.constantize(module_parent_name) : Object
+ end
+
def parent
- parent_name ? ActiveSupport::Inflector.constantize(parent_name) : Object
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ `Module#parent` has been renamed to `module_parent`.
+ `parent` is deprecated and will be removed in Rails 6.1.
+ MSG
+ module_parent
end
# Returns all the parents of this module according to its name, ordered from
@@ -40,15 +60,15 @@ class Module
# end
# X = M::N
#
- # M.parents # => [Object]
- # M::N.parents # => [M, Object]
- # X.parents # => [M, Object]
- def parents
+ # M.module_parents # => [Object]
+ # M::N.module_parents # => [M, Object]
+ # X.module_parents # => [M, Object]
+ def module_parents
parents = []
- if parent_name
- parts = parent_name.split('::')
+ if module_parent_name
+ parts = module_parent_name.split("::")
until parts.empty?
- parents << ActiveSupport::Inflector.constantize(parts * '::')
+ parents << ActiveSupport::Inflector.constantize(parts * "::")
parts.pop
end
end
@@ -56,7 +76,11 @@ class Module
parents
end
- def local_constants #:nodoc:
- constants(false)
+ def parents
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ `Module#parents` has been renamed to `module_parents`.
+ `parents` is deprecated and will be removed in Rails 6.1.
+ MSG
+ module_parents
end
end
diff --git a/activesupport/lib/active_support/core_ext/module/method_transplanting.rb b/activesupport/lib/active_support/core_ext/module/method_transplanting.rb
deleted file mode 100644
index 1fde3db070..0000000000
--- a/activesupport/lib/active_support/core_ext/module/method_transplanting.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/activesupport/lib/active_support/core_ext/module/qualified_const.rb b/activesupport/lib/active_support/core_ext/module/qualified_const.rb
deleted file mode 100644
index 65525013db..0000000000
--- a/activesupport/lib/active_support/core_ext/module/qualified_const.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-require 'active_support/core_ext/string/inflections'
-
-#--
-# Allows code reuse in the methods below without polluting Module.
-#++
-module QualifiedConstUtils
- def self.raise_if_absolute(path)
- raise NameError.new("wrong constant name #$&") if path =~ /\A::[^:]+/
- end
-
- def self.names(path)
- path.split('::')
- end
-end
-
-##
-# Extends the API for constants to be able to deal with qualified names. Arguments
-# are assumed to be relative to the receiver.
-#
-#--
-# Qualified names are required to be relative because we are extending existing
-# methods that expect constant names, ie, relative paths of length 1. For example,
-# Object.const_get('::String') raises NameError and so does qualified_const_get.
-#++
-class Module
- def qualified_const_defined?(path, search_parents=true)
- QualifiedConstUtils.raise_if_absolute(path)
-
- QualifiedConstUtils.names(path).inject(self) do |mod, name|
- return unless mod.const_defined?(name, search_parents)
- mod.const_get(name)
- end
- return true
- end
-
- def qualified_const_get(path)
- QualifiedConstUtils.raise_if_absolute(path)
-
- QualifiedConstUtils.names(path).inject(self) do |mod, name|
- mod.const_get(name)
- end
- end
-
- def qualified_const_set(path, value)
- QualifiedConstUtils.raise_if_absolute(path)
-
- const_name = path.demodulize
- mod_name = path.deconstantize
- mod = mod_name.empty? ? self : qualified_const_get(mod_name)
- mod.const_set(const_name, value)
- end
-end
diff --git a/activesupport/lib/active_support/core_ext/module/reachable.rb b/activesupport/lib/active_support/core_ext/module/reachable.rb
index 5d3d0e9851..e9cbda5245 100644
--- a/activesupport/lib/active_support/core_ext/module/reachable.rb
+++ b/activesupport/lib/active_support/core_ext/module/reachable.rb
@@ -1,8 +1,11 @@
-require 'active_support/core_ext/module/anonymous'
-require 'active_support/core_ext/string/inflections'
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/anonymous"
+require "active_support/core_ext/string/inflections"
class Module
def reachable? #:nodoc:
!anonymous? && name.safe_constantize.equal?(self)
end
+ deprecate :reachable?
end
diff --git a/activesupport/lib/active_support/core_ext/module/redefine_method.rb b/activesupport/lib/active_support/core_ext/module/redefine_method.rb
new file mode 100644
index 0000000000..5bd8e6e973
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/module/redefine_method.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+class Module
+ # Marks the named method as intended to be redefined, if it exists.
+ # Suppresses the Ruby method redefinition warning. Prefer
+ # #redefine_method where possible.
+ def silence_redefinition_of_method(method)
+ if method_defined?(method) || private_method_defined?(method)
+ # This suppresses the "method redefined" warning; the self-alias
+ # looks odd, but means we don't need to generate a unique name
+ alias_method method, method
+ end
+ end
+
+ # Replaces the existing method definition, if there is one, with the passed
+ # block as its body.
+ def redefine_method(method, &block)
+ visibility = method_visibility(method)
+ silence_redefinition_of_method(method)
+ define_method(method, &block)
+ send(visibility, method)
+ end
+
+ # Replaces the existing singleton method definition, if there is one, with
+ # the passed block as its body.
+ def redefine_singleton_method(method, &block)
+ singleton_class.redefine_method(method, &block)
+ end
+
+ def method_visibility(method) # :nodoc:
+ case
+ when private_method_defined?(method)
+ :private
+ when protected_method_defined?(method)
+ :protected
+ else
+ :public
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/module/remove_method.rb b/activesupport/lib/active_support/core_ext/module/remove_method.rb
index 52632d2c6b..97eb5f9eca 100644
--- a/activesupport/lib/active_support/core_ext/module/remove_method.rb
+++ b/activesupport/lib/active_support/core_ext/module/remove_method.rb
@@ -1,3 +1,7 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/redefine_method"
+
class Module
# Removes the named method, if it exists.
def remove_possible_method(method)
@@ -6,10 +10,8 @@ class Module
end
end
- # Replaces the existing method definition, if there is one, with the passed
- # block as its body.
- def redefine_method(method, &block)
- remove_possible_method(method)
- define_method(method, &block)
+ # Removes the named singleton method, if it exists.
+ def remove_possible_singleton_method(method)
+ singleton_class.remove_possible_method(method)
end
end
diff --git a/activesupport/lib/active_support/core_ext/name_error.rb b/activesupport/lib/active_support/core_ext/name_error.rb
index 6b447d772b..6d37cd9dfd 100644
--- a/activesupport/lib/active_support/core_ext/name_error.rb
+++ b/activesupport/lib/active_support/core_ext/name_error.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class NameError
# Extract the name of the missing constant from the exception message.
#
@@ -8,6 +10,11 @@ class NameError
# end
# # => "HelloWorld"
def missing_name
+ # Since ruby v2.3.0 `did_you_mean` gem is loaded by default.
+ # It extends NameError#message with spell corrections which are SLOW.
+ # We should use original_message message instead.
+ message = respond_to?(:original_message) ? original_message : self.message
+
if /undefined local variable or method/ !~ message
$1 if /((::)?([A-Z]\w*)(::[A-Z]\w*)*)$/ =~ message
end
diff --git a/activesupport/lib/active_support/core_ext/numeric.rb b/activesupport/lib/active_support/core_ext/numeric.rb
index bcdc3eace2..fe778470f1 100644
--- a/activesupport/lib/active_support/core_ext/numeric.rb
+++ b/activesupport/lib/active_support/core_ext/numeric.rb
@@ -1,4 +1,5 @@
-require 'active_support/core_ext/numeric/bytes'
-require 'active_support/core_ext/numeric/time'
-require 'active_support/core_ext/numeric/inquiry'
-require 'active_support/core_ext/numeric/conversions'
+# frozen_string_literal: true
+
+require "active_support/core_ext/numeric/bytes"
+require "active_support/core_ext/numeric/time"
+require "active_support/core_ext/numeric/conversions"
diff --git a/activesupport/lib/active_support/core_ext/numeric/bytes.rb b/activesupport/lib/active_support/core_ext/numeric/bytes.rb
index dfbca32474..b002eba406 100644
--- a/activesupport/lib/active_support/core_ext/numeric/bytes.rb
+++ b/activesupport/lib/active_support/core_ext/numeric/bytes.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Numeric
KILOBYTE = 1024
MEGABYTE = KILOBYTE * 1024
diff --git a/activesupport/lib/active_support/core_ext/numeric/conversions.rb b/activesupport/lib/active_support/core_ext/numeric/conversions.rb
index 0c8ff79237..8acad6164a 100644
--- a/activesupport/lib/active_support/core_ext/numeric/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/numeric/conversions.rb
@@ -1,145 +1,136 @@
-require 'active_support/core_ext/big_decimal/conversions'
-require 'active_support/number_helper'
+# frozen_string_literal: true
-class Numeric
+require "active_support/core_ext/big_decimal/conversions"
+require "active_support/number_helper"
+require "active_support/core_ext/module/deprecation"
- # Provides options for converting numbers into formatted strings.
- # Options are provided for phone numbers, currency, percentage,
- # precision, positional notation, file size and pretty printing.
- #
- # ==== Options
- #
- # For details on which formats use which options, see ActiveSupport::NumberHelper
- #
- # ==== Examples
- #
- # Phone Numbers:
- # 5551234.to_s(:phone) # => 555-1234
- # 1235551234.to_s(:phone) # => 123-555-1234
- # 1235551234.to_s(:phone, area_code: true) # => (123) 555-1234
- # 1235551234.to_s(:phone, delimiter: ' ') # => 123 555 1234
- # 1235551234.to_s(:phone, area_code: true, extension: 555) # => (123) 555-1234 x 555
- # 1235551234.to_s(:phone, country_code: 1) # => +1-123-555-1234
- # 1235551234.to_s(:phone, country_code: 1, extension: 1343, delimiter: '.')
- # # => +1.123.555.1234 x 1343
- #
- # Currency:
- # 1234567890.50.to_s(:currency) # => $1,234,567,890.50
- # 1234567890.506.to_s(:currency) # => $1,234,567,890.51
- # 1234567890.506.to_s(:currency, precision: 3) # => $1,234,567,890.506
- # 1234567890.506.to_s(:currency, locale: :fr) # => 1 234 567 890,51 €
- # -1234567890.50.to_s(:currency, negative_format: '(%u%n)')
- # # => ($1,234,567,890.50)
- # 1234567890.50.to_s(:currency, unit: '&pound;', separator: ',', delimiter: '')
- # # => &pound;1234567890,50
- # 1234567890.50.to_s(:currency, unit: '&pound;', separator: ',', delimiter: '', format: '%n %u')
- # # => 1234567890,50 &pound;
- #
- # Percentage:
- # 100.to_s(:percentage) # => 100.000%
- # 100.to_s(:percentage, precision: 0) # => 100%
- # 1000.to_s(:percentage, delimiter: '.', separator: ',') # => 1.000,000%
- # 302.24398923423.to_s(:percentage, precision: 5) # => 302.24399%
- # 1000.to_s(:percentage, locale: :fr) # => 1 000,000%
- # 100.to_s(:percentage, format: '%n %') # => 100 %
- #
- # Delimited:
- # 12345678.to_s(:delimited) # => 12,345,678
- # 12345678.05.to_s(:delimited) # => 12,345,678.05
- # 12345678.to_s(:delimited, delimiter: '.') # => 12.345.678
- # 12345678.to_s(:delimited, delimiter: ',') # => 12,345,678
- # 12345678.05.to_s(:delimited, separator: ' ') # => 12,345,678 05
- # 12345678.05.to_s(:delimited, locale: :fr) # => 12 345 678,05
- # 98765432.98.to_s(:delimited, delimiter: ' ', separator: ',')
- # # => 98 765 432,98
- #
- # Rounded:
- # 111.2345.to_s(:rounded) # => 111.235
- # 111.2345.to_s(:rounded, precision: 2) # => 111.23
- # 13.to_s(:rounded, precision: 5) # => 13.00000
- # 389.32314.to_s(:rounded, precision: 0) # => 389
- # 111.2345.to_s(:rounded, significant: true) # => 111
- # 111.2345.to_s(:rounded, precision: 1, significant: true) # => 100
- # 13.to_s(:rounded, precision: 5, significant: true) # => 13.000
- # 111.234.to_s(:rounded, locale: :fr) # => 111,234
- # 13.to_s(:rounded, precision: 5, significant: true, strip_insignificant_zeros: true)
- # # => 13
- # 389.32314.to_s(:rounded, precision: 4, significant: true) # => 389.3
- # 1111.2345.to_s(:rounded, precision: 2, separator: ',', delimiter: '.')
- # # => 1.111,23
- #
- # Human-friendly size in Bytes:
- # 123.to_s(:human_size) # => 123 Bytes
- # 1234.to_s(:human_size) # => 1.21 KB
- # 12345.to_s(:human_size) # => 12.1 KB
- # 1234567.to_s(:human_size) # => 1.18 MB
- # 1234567890.to_s(:human_size) # => 1.15 GB
- # 1234567890123.to_s(:human_size) # => 1.12 TB
- # 1234567.to_s(:human_size, precision: 2) # => 1.2 MB
- # 483989.to_s(:human_size, precision: 2) # => 470 KB
- # 1234567.to_s(:human_size, precision: 2, separator: ',') # => 1,2 MB
- # 1234567890123.to_s(:human_size, precision: 5) # => "1.1229 TB"
- # 524288000.to_s(:human_size, precision: 5) # => "500 MB"
- #
- # Human-friendly format:
- # 123.to_s(:human) # => "123"
- # 1234.to_s(:human) # => "1.23 Thousand"
- # 12345.to_s(:human) # => "12.3 Thousand"
- # 1234567.to_s(:human) # => "1.23 Million"
- # 1234567890.to_s(:human) # => "1.23 Billion"
- # 1234567890123.to_s(:human) # => "1.23 Trillion"
- # 1234567890123456.to_s(:human) # => "1.23 Quadrillion"
- # 1234567890123456789.to_s(:human) # => "1230 Quadrillion"
- # 489939.to_s(:human, precision: 2) # => "490 Thousand"
- # 489939.to_s(:human, precision: 4) # => "489.9 Thousand"
- # 1234567.to_s(:human, precision: 4,
- # significant: false) # => "1.2346 Million"
- # 1234567.to_s(:human, precision: 1,
- # separator: ',',
- # significant: false) # => "1,2 Million"
- def to_formatted_s(format = :default, options = {})
- case format
- when :phone
- return ActiveSupport::NumberHelper.number_to_phone(self, options)
- when :currency
- return ActiveSupport::NumberHelper.number_to_currency(self, options)
- when :percentage
- return ActiveSupport::NumberHelper.number_to_percentage(self, options)
- when :delimited
- return ActiveSupport::NumberHelper.number_to_delimited(self, options)
- when :rounded
- return ActiveSupport::NumberHelper.number_to_rounded(self, options)
- when :human
- return ActiveSupport::NumberHelper.number_to_human(self, options)
- when :human_size
- return ActiveSupport::NumberHelper.number_to_human_size(self, options)
- else
- self.to_default_s
- end
- end
-
- [Fixnum, Bignum].each do |klass|
- klass.class_eval do
- alias_method :to_default_s, :to_s
- def to_s(base_or_format = 10, options = nil)
- if base_or_format.is_a?(Symbol)
- to_formatted_s(base_or_format, options || {})
- else
- to_default_s(base_or_format)
- end
- end
- end
- end
-
- Float.class_eval do
- alias_method :to_default_s, :to_s
- def to_s(*args)
- if args.empty?
- to_default_s
+module ActiveSupport
+ module NumericWithFormat
+ # Provides options for converting numbers into formatted strings.
+ # Options are provided for phone numbers, currency, percentage,
+ # precision, positional notation, file size and pretty printing.
+ #
+ # ==== Options
+ #
+ # For details on which formats use which options, see ActiveSupport::NumberHelper
+ #
+ # ==== Examples
+ #
+ # Phone Numbers:
+ # 5551234.to_s(:phone) # => "555-1234"
+ # 1235551234.to_s(:phone) # => "123-555-1234"
+ # 1235551234.to_s(:phone, area_code: true) # => "(123) 555-1234"
+ # 1235551234.to_s(:phone, delimiter: ' ') # => "123 555 1234"
+ # 1235551234.to_s(:phone, area_code: true, extension: 555) # => "(123) 555-1234 x 555"
+ # 1235551234.to_s(:phone, country_code: 1) # => "+1-123-555-1234"
+ # 1235551234.to_s(:phone, country_code: 1, extension: 1343, delimiter: '.')
+ # # => "+1.123.555.1234 x 1343"
+ #
+ # Currency:
+ # 1234567890.50.to_s(:currency) # => "$1,234,567,890.50"
+ # 1234567890.506.to_s(:currency) # => "$1,234,567,890.51"
+ # 1234567890.506.to_s(:currency, precision: 3) # => "$1,234,567,890.506"
+ # 1234567890.506.to_s(:currency, locale: :fr) # => "1 234 567 890,51 €"
+ # -1234567890.50.to_s(:currency, negative_format: '(%u%n)')
+ # # => "($1,234,567,890.50)"
+ # 1234567890.50.to_s(:currency, unit: '&pound;', separator: ',', delimiter: '')
+ # # => "&pound;1234567890,50"
+ # 1234567890.50.to_s(:currency, unit: '&pound;', separator: ',', delimiter: '', format: '%n %u')
+ # # => "1234567890,50 &pound;"
+ #
+ # Percentage:
+ # 100.to_s(:percentage) # => "100.000%"
+ # 100.to_s(:percentage, precision: 0) # => "100%"
+ # 1000.to_s(:percentage, delimiter: '.', separator: ',') # => "1.000,000%"
+ # 302.24398923423.to_s(:percentage, precision: 5) # => "302.24399%"
+ # 1000.to_s(:percentage, locale: :fr) # => "1 000,000%"
+ # 100.to_s(:percentage, format: '%n %') # => "100.000 %"
+ #
+ # Delimited:
+ # 12345678.to_s(:delimited) # => "12,345,678"
+ # 12345678.05.to_s(:delimited) # => "12,345,678.05"
+ # 12345678.to_s(:delimited, delimiter: '.') # => "12.345.678"
+ # 12345678.to_s(:delimited, delimiter: ',') # => "12,345,678"
+ # 12345678.05.to_s(:delimited, separator: ' ') # => "12,345,678 05"
+ # 12345678.05.to_s(:delimited, locale: :fr) # => "12 345 678,05"
+ # 98765432.98.to_s(:delimited, delimiter: ' ', separator: ',')
+ # # => "98 765 432,98"
+ #
+ # Rounded:
+ # 111.2345.to_s(:rounded) # => "111.235"
+ # 111.2345.to_s(:rounded, precision: 2) # => "111.23"
+ # 13.to_s(:rounded, precision: 5) # => "13.00000"
+ # 389.32314.to_s(:rounded, precision: 0) # => "389"
+ # 111.2345.to_s(:rounded, significant: true) # => "111"
+ # 111.2345.to_s(:rounded, precision: 1, significant: true) # => "100"
+ # 13.to_s(:rounded, precision: 5, significant: true) # => "13.000"
+ # 111.234.to_s(:rounded, locale: :fr) # => "111,234"
+ # 13.to_s(:rounded, precision: 5, significant: true, strip_insignificant_zeros: true)
+ # # => "13"
+ # 389.32314.to_s(:rounded, precision: 4, significant: true) # => "389.3"
+ # 1111.2345.to_s(:rounded, precision: 2, separator: ',', delimiter: '.')
+ # # => "1.111,23"
+ #
+ # Human-friendly size in Bytes:
+ # 123.to_s(:human_size) # => "123 Bytes"
+ # 1234.to_s(:human_size) # => "1.21 KB"
+ # 12345.to_s(:human_size) # => "12.1 KB"
+ # 1234567.to_s(:human_size) # => "1.18 MB"
+ # 1234567890.to_s(:human_size) # => "1.15 GB"
+ # 1234567890123.to_s(:human_size) # => "1.12 TB"
+ # 1234567890123456.to_s(:human_size) # => "1.1 PB"
+ # 1234567890123456789.to_s(:human_size) # => "1.07 EB"
+ # 1234567.to_s(:human_size, precision: 2) # => "1.2 MB"
+ # 483989.to_s(:human_size, precision: 2) # => "470 KB"
+ # 1234567.to_s(:human_size, precision: 2, separator: ',') # => "1,2 MB"
+ # 1234567890123.to_s(:human_size, precision: 5) # => "1.1228 TB"
+ # 524288000.to_s(:human_size, precision: 5) # => "500 MB"
+ #
+ # Human-friendly format:
+ # 123.to_s(:human) # => "123"
+ # 1234.to_s(:human) # => "1.23 Thousand"
+ # 12345.to_s(:human) # => "12.3 Thousand"
+ # 1234567.to_s(:human) # => "1.23 Million"
+ # 1234567890.to_s(:human) # => "1.23 Billion"
+ # 1234567890123.to_s(:human) # => "1.23 Trillion"
+ # 1234567890123456.to_s(:human) # => "1.23 Quadrillion"
+ # 1234567890123456789.to_s(:human) # => "1230 Quadrillion"
+ # 489939.to_s(:human, precision: 2) # => "490 Thousand"
+ # 489939.to_s(:human, precision: 4) # => "489.9 Thousand"
+ # 1234567.to_s(:human, precision: 4,
+ # significant: false) # => "1.2346 Million"
+ # 1234567.to_s(:human, precision: 1,
+ # separator: ',',
+ # significant: false) # => "1,2 Million"
+ def to_s(format = nil, options = nil)
+ case format
+ when nil
+ super()
+ when Integer, String
+ super(format)
+ when :phone
+ ActiveSupport::NumberHelper.number_to_phone(self, options || {})
+ when :currency
+ ActiveSupport::NumberHelper.number_to_currency(self, options || {})
+ when :percentage
+ ActiveSupport::NumberHelper.number_to_percentage(self, options || {})
+ when :delimited
+ ActiveSupport::NumberHelper.number_to_delimited(self, options || {})
+ when :rounded
+ ActiveSupport::NumberHelper.number_to_rounded(self, options || {})
+ when :human
+ ActiveSupport::NumberHelper.number_to_human(self, options || {})
+ when :human_size
+ ActiveSupport::NumberHelper.number_to_human_size(self, options || {})
+ when Symbol
+ super()
else
- to_formatted_s(*args)
+ super(format)
end
end
end
-
end
+
+Integer.prepend ActiveSupport::NumericWithFormat
+Float.prepend ActiveSupport::NumericWithFormat
+BigDecimal.prepend ActiveSupport::NumericWithFormat
diff --git a/activesupport/lib/active_support/core_ext/numeric/inquiry.rb b/activesupport/lib/active_support/core_ext/numeric/inquiry.rb
index 7e7ac1b0b2..e05e40825b 100644
--- a/activesupport/lib/active_support/core_ext/numeric/inquiry.rb
+++ b/activesupport/lib/active_support/core_ext/numeric/inquiry.rb
@@ -1,26 +1,5 @@
-unless 1.respond_to?(:positive?) # TODO: Remove this file when we drop support to ruby < 2.3
-class Numeric
- # Returns true if the number is positive.
- #
- # 1.positive? # => true
- # 0.positive? # => false
- # -1.positive? # => false
- def positive?
- self > 0
- end
+# frozen_string_literal: true
- # Returns true if the number is negative.
- #
- # -1.negative? # => true
- # 0.negative? # => false
- # 1.negative? # => false
- def negative?
- self < 0
- end
-end
+require "active_support/deprecation"
-class Complex
- undef :positive?
- undef :negative?
-end
-end
+ActiveSupport::Deprecation.warn "Ruby 2.4+ (required by Rails 6) provides Numeric#positive? and Numeric#negative? natively, so requiring active_support/core_ext/numeric/inquiry is no longer necessary. Requiring it will raise LoadError in Rails 6.1."
diff --git a/activesupport/lib/active_support/core_ext/numeric/time.rb b/activesupport/lib/active_support/core_ext/numeric/time.rb
index 6c4a975495..bc4627f7a2 100644
--- a/activesupport/lib/active_support/core_ext/numeric/time.rb
+++ b/activesupport/lib/active_support/core_ext/numeric/time.rb
@@ -1,41 +1,33 @@
-require 'active_support/duration'
-require 'active_support/core_ext/time/calculations'
-require 'active_support/core_ext/time/acts_like'
-require 'active_support/core_ext/date/calculations'
-require 'active_support/core_ext/date/acts_like'
+# frozen_string_literal: true
+
+require "active_support/duration"
+require "active_support/core_ext/time/calculations"
+require "active_support/core_ext/time/acts_like"
+require "active_support/core_ext/date/calculations"
+require "active_support/core_ext/date/acts_like"
class Numeric
- # Enables the use of time calculations and declarations, like 45.minutes + 2.hours + 4.years.
- #
- # These methods use Time#advance for precise date calculations when using from_now, ago, etc.
- # as well as adding or subtracting their results from a Time object. For example:
- #
- # # equivalent to Time.current.advance(months: 1)
- # 1.month.from_now
- #
- # # equivalent to Time.current.advance(years: 2)
- # 2.years.from_now
+ # Returns a Duration instance matching the number of seconds provided.
#
- # # equivalent to Time.current.advance(months: 4, years: 5)
- # (4.months + 5.years).from_now
+ # 2.seconds # => 2 seconds
def seconds
- ActiveSupport::Duration.new(self, [[:seconds, self]])
+ ActiveSupport::Duration.seconds(self)
end
alias :second :seconds
# Returns a Duration instance matching the number of minutes provided.
#
- # 2.minutes # => 120 seconds
+ # 2.minutes # => 2 minutes
def minutes
- ActiveSupport::Duration.new(self * 60, [[:seconds, self * 60]])
+ ActiveSupport::Duration.minutes(self)
end
alias :minute :minutes
# Returns a Duration instance matching the number of hours provided.
#
- # 2.hours # => 7_200 seconds
+ # 2.hours # => 2 hours
def hours
- ActiveSupport::Duration.new(self * 3600, [[:seconds, self * 3600]])
+ ActiveSupport::Duration.hours(self)
end
alias :hour :hours
@@ -43,31 +35,31 @@ class Numeric
#
# 2.days # => 2 days
def days
- ActiveSupport::Duration.new(self * 24.hours, [[:days, self]])
+ ActiveSupport::Duration.days(self)
end
alias :day :days
# Returns a Duration instance matching the number of weeks provided.
#
- # 2.weeks # => 14 days
+ # 2.weeks # => 2 weeks
def weeks
- ActiveSupport::Duration.new(self * 7.days, [[:days, self * 7]])
+ ActiveSupport::Duration.weeks(self)
end
alias :week :weeks
# Returns a Duration instance matching the number of fortnights provided.
#
- # 2.fortnights # => 28 days
+ # 2.fortnights # => 4 weeks
def fortnights
- ActiveSupport::Duration.new(self * 2.weeks, [[:days, self * 14]])
+ ActiveSupport::Duration.weeks(self * 2)
end
alias :fortnight :fortnights
# Returns the number of milliseconds equivalent to the seconds provided.
- # Used with the standard time durations, like 1.hour.in_milliseconds --
- # so we can feed them to JavaScript functions like getTime().
+ # Used with the standard time durations.
#
- # 2.in_milliseconds # => 2_000
+ # 2.in_milliseconds # => 2000
+ # 1.hour.in_milliseconds # => 3600000
def in_milliseconds
self * 1000
end
diff --git a/activesupport/lib/active_support/core_ext/object.rb b/activesupport/lib/active_support/core_ext/object.rb
index f4f9152d6a..efd34cc692 100644
--- a/activesupport/lib/active_support/core_ext/object.rb
+++ b/activesupport/lib/active_support/core_ext/object.rb
@@ -1,14 +1,16 @@
-require 'active_support/core_ext/object/acts_like'
-require 'active_support/core_ext/object/blank'
-require 'active_support/core_ext/object/duplicable'
-require 'active_support/core_ext/object/deep_dup'
-require 'active_support/core_ext/object/try'
-require 'active_support/core_ext/object/inclusion'
+# frozen_string_literal: true
-require 'active_support/core_ext/object/conversions'
-require 'active_support/core_ext/object/instance_variables'
+require "active_support/core_ext/object/acts_like"
+require "active_support/core_ext/object/blank"
+require "active_support/core_ext/object/duplicable"
+require "active_support/core_ext/object/deep_dup"
+require "active_support/core_ext/object/try"
+require "active_support/core_ext/object/inclusion"
-require 'active_support/core_ext/object/json'
-require 'active_support/core_ext/object/to_param'
-require 'active_support/core_ext/object/to_query'
-require 'active_support/core_ext/object/with_options'
+require "active_support/core_ext/object/conversions"
+require "active_support/core_ext/object/instance_variables"
+
+require "active_support/core_ext/object/json"
+require "active_support/core_ext/object/to_param"
+require "active_support/core_ext/object/to_query"
+require "active_support/core_ext/object/with_options"
diff --git a/activesupport/lib/active_support/core_ext/object/acts_like.rb b/activesupport/lib/active_support/core_ext/object/acts_like.rb
index 3912cc5ace..403ee20e39 100644
--- a/activesupport/lib/active_support/core_ext/object/acts_like.rb
+++ b/activesupport/lib/active_support/core_ext/object/acts_like.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Object
# A duck-type assistant method. For example, Active Support extends Date
# to define an <tt>acts_like_date?</tt> method, and extends Time to define
@@ -5,6 +7,15 @@ class Object
# <tt>x.acts_like?(:date)</tt> to do duck-type-safe comparisons, since classes that
# we want to act like Time simply need to define an <tt>acts_like_time?</tt> method.
def acts_like?(duck)
- respond_to? :"acts_like_#{duck}?"
+ case duck
+ when :time
+ respond_to? :acts_like_time?
+ when :date
+ respond_to? :acts_like_date?
+ when :string
+ respond_to? :acts_like_string?
+ else
+ respond_to? :"acts_like_#{duck}?"
+ end
end
end
diff --git a/activesupport/lib/active_support/core_ext/object/blank.rb b/activesupport/lib/active_support/core_ext/object/blank.rb
index 38e43478df..f36fef6cc9 100644
--- a/activesupport/lib/active_support/core_ext/object/blank.rb
+++ b/activesupport/lib/active_support/core_ext/object/blank.rb
@@ -1,12 +1,14 @@
-# encoding: utf-8
+# frozen_string_literal: true
+
+require "concurrent/map"
class Object
# An object is blank if it's false, empty, or a whitespace string.
- # For example, '', ' ', +nil+, [], and {} are all blank.
+ # For example, +nil+, '', ' ', [], {}, and +false+ are all blank.
#
# This simplifies
#
- # address.nil? || address.empty?
+ # !address || address.empty?
#
# to
#
@@ -100,6 +102,9 @@ end
class String
BLANK_RE = /\A[[:space:]]*\z/
+ ENCODED_BLANKS = Concurrent::Map.new do |h, enc|
+ h[enc] = Regexp.new(BLANK_RE.source.encode(enc), BLANK_RE.options | Regexp::FIXEDENCODING)
+ end
# A string is blank if it's empty or contains whitespaces only:
#
@@ -114,7 +119,15 @@ class String
#
# @return [true, false]
def blank?
- BLANK_RE === self
+ # The regexp that matches blank strings is expensive. For the case of empty
+ # strings we can speed up this method (~3.5x) with an empty? call. The
+ # penalty for the rest of strings is marginal.
+ empty? ||
+ begin
+ BLANK_RE.match?(self)
+ rescue Encoding::CompatibilityError
+ ENCODED_BLANKS[self.encoding].match?(self)
+ end
end
end
@@ -129,3 +142,14 @@ class Numeric #:nodoc:
false
end
end
+
+class Time #:nodoc:
+ # No Time is blank:
+ #
+ # Time.now.blank? # => false
+ #
+ # @return [false]
+ def blank?
+ false
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/object/conversions.rb b/activesupport/lib/active_support/core_ext/object/conversions.rb
index 540f7aadb0..624fb8d77c 100644
--- a/activesupport/lib/active_support/core_ext/object/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/object/conversions.rb
@@ -1,4 +1,6 @@
-require 'active_support/core_ext/object/to_param'
-require 'active_support/core_ext/object/to_query'
-require 'active_support/core_ext/array/conversions'
-require 'active_support/core_ext/hash/conversions'
+# frozen_string_literal: true
+
+require "active_support/core_ext/object/to_param"
+require "active_support/core_ext/object/to_query"
+require "active_support/core_ext/array/conversions"
+require "active_support/core_ext/hash/conversions"
diff --git a/activesupport/lib/active_support/core_ext/object/deep_dup.rb b/activesupport/lib/active_support/core_ext/object/deep_dup.rb
index ad5b2af161..c66c5eb2d9 100644
--- a/activesupport/lib/active_support/core_ext/object/deep_dup.rb
+++ b/activesupport/lib/active_support/core_ext/object/deep_dup.rb
@@ -1,4 +1,6 @@
-require 'active_support/core_ext/object/duplicable'
+# frozen_string_literal: true
+
+require "active_support/core_ext/object/duplicable"
class Object
# Returns a deep copy of object if it's duplicable. If it's
@@ -39,9 +41,15 @@ class Hash
# hash[:a][:c] # => nil
# dup[:a][:c] # => "c"
def deep_dup
- each_with_object(dup) do |(key, value), hash|
- hash.delete(key)
- hash[key.deep_dup] = value.deep_dup
+ hash = dup
+ each_pair do |key, value|
+ if key.frozen? && ::String === key
+ hash[key] = value.deep_dup
+ else
+ hash.delete(key)
+ hash[key.deep_dup] = value.deep_dup
+ end
end
+ hash
end
end
diff --git a/activesupport/lib/active_support/core_ext/object/duplicable.rb b/activesupport/lib/active_support/core_ext/object/duplicable.rb
index 6fc0cb53f0..c78ee6bbfc 100644
--- a/activesupport/lib/active_support/core_ext/object/duplicable.rb
+++ b/activesupport/lib/active_support/core_ext/object/duplicable.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
#--
-# 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 +21,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,61 +29,90 @@ 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
+
+ # Some symbols couldn't be duped in Ruby 2.4.0 only, due to a bug.
+ # This feature check catches any regression.
+ "symbol_from_string".to_sym.dup
+ rescue TypeError
+
+ # Symbols are not duplicable:
+ #
+ # :my_symbol.duplicable? # => false
+ # :my_symbol.dup # => TypeError: can't dup Symbol
+ def duplicable?
+ false
+ end
end
end
class Numeric
- # Numbers are not duplicable:
- #
- # 3.duplicable? # => false
- # 3.dup # => TypeError: can't dup Fixnum
- 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
-require 'bigdecimal'
+require "bigdecimal"
class BigDecimal
# BigDecimals are duplicable:
#
- # BigDecimal.new("1.2").duplicable? # => true
- # BigDecimal.new("1.2").dup # => #<BigDecimal:7f9d698eee10,'0.12E1',18(18)>
+ # BigDecimal("1.2").duplicable? # => true
+ # BigDecimal("1.2").dup # => #<BigDecimal:...,'0.12E1',18(18)>
def duplicable?
true
end
@@ -96,3 +127,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/inclusion.rb b/activesupport/lib/active_support/core_ext/object/inclusion.rb
index 55f281b213..6064e92f20 100644
--- a/activesupport/lib/active_support/core_ext/object/inclusion.rb
+++ b/activesupport/lib/active_support/core_ext/object/inclusion.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Object
# Returns true if this object is included in the argument. Argument must be
# any object which responds to +#include?+. Usage:
@@ -5,7 +7,7 @@ class Object
# characters = ["Konata", "Kagami", "Tsukasa"]
# "Konata".in?(characters) # => true
#
- # This will throw an ArgumentError if the argument doesn't respond
+ # This will throw an +ArgumentError+ if the argument doesn't respond
# to +#include?+.
def in?(another_object)
another_object.include?(self)
@@ -18,10 +20,10 @@ class Object
#
# params[:bucket_type].presence_in %w( project calendar )
#
- # This will throw an ArgumentError if the argument doesn't respond to +#include?+.
+ # This will throw an +ArgumentError+ if the argument doesn't respond to +#include?+.
#
# @return [Object]
def presence_in(another_object)
- self.in?(another_object) ? self : nil
+ in?(another_object) ? self : nil
end
end
diff --git a/activesupport/lib/active_support/core_ext/object/instance_variables.rb b/activesupport/lib/active_support/core_ext/object/instance_variables.rb
index 593a7a4940..12fdf840b5 100644
--- a/activesupport/lib/active_support/core_ext/object/instance_variables.rb
+++ b/activesupport/lib/active_support/core_ext/object/instance_variables.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Object
# Returns a hash with string keys that maps instance variable names without "@" to their
# corresponding values.
diff --git a/activesupport/lib/active_support/core_ext/object/json.rb b/activesupport/lib/active_support/core_ext/object/json.rb
index 0db787010c..416059d17b 100644
--- a/activesupport/lib/active_support/core_ext/object/json.rb
+++ b/activesupport/lib/active_support/core_ext/object/json.rb
@@ -1,15 +1,20 @@
-# Hack to load json gem first so we can overwrite its to_json.
-require 'json'
-require 'bigdecimal'
-require 'active_support/core_ext/big_decimal/conversions' # for #to_s
-require 'active_support/core_ext/hash/except'
-require 'active_support/core_ext/hash/slice'
-require 'active_support/core_ext/object/instance_variables'
-require 'time'
-require 'active_support/core_ext/time/conversions'
-require 'active_support/core_ext/date_time/conversions'
-require 'active_support/core_ext/date/conversions'
+# frozen_string_literal: true
+# Hack to load json gem first so we can overwrite its to_json.
+require "json"
+require "bigdecimal"
+require "uri/generic"
+require "pathname"
+require "active_support/core_ext/big_decimal/conversions" # for #to_s
+require "active_support/core_ext/hash/except"
+require "active_support/core_ext/hash/slice"
+require "active_support/core_ext/object/instance_variables"
+require "time"
+require "active_support/core_ext/time/conversions"
+require "active_support/core_ext/date_time/conversions"
+require "active_support/core_ext/date/conversions"
+
+#--
# The JSON gem adds a few modules to Ruby core classes containing :to_json definition, overwriting
# their default behavior. That said, we need to define the basic to_json method in all of them,
# otherwise they will always use to_json gem implementation, which is backwards incompatible in
@@ -131,6 +136,12 @@ module Enumerable
end
end
+class IO
+ def as_json(options = nil) #:nodoc:
+ to_s
+ end
+end
+
class Range
def as_json(options = nil) #:nodoc:
to_s
@@ -187,13 +198,31 @@ class DateTime
if ActiveSupport::JSON::Encoding.use_standard_json_time_format
xmlschema(ActiveSupport::JSON::Encoding.time_precision)
else
- strftime('%Y/%m/%d %H:%M:%S %z')
+ strftime("%Y/%m/%d %H:%M:%S %z")
end
end
end
+class URI::Generic #:nodoc:
+ def as_json(options = nil)
+ to_s
+ end
+end
+
+class Pathname #:nodoc:
+ def as_json(options = nil)
+ to_s
+ end
+end
+
class Process::Status #:nodoc:
def as_json(options = nil)
- { :exitstatus => exitstatus, :pid => pid }
+ { exitstatus: exitstatus, pid: pid }
+ end
+end
+
+class Exception
+ def as_json(options = nil)
+ to_s
end
end
diff --git a/activesupport/lib/active_support/core_ext/object/to_param.rb b/activesupport/lib/active_support/core_ext/object/to_param.rb
index 684d4ef57e..6d2bdd70f3 100644
--- a/activesupport/lib/active_support/core_ext/object/to_param.rb
+++ b/activesupport/lib/active_support/core_ext/object/to_param.rb
@@ -1 +1,3 @@
-require 'active_support/core_ext/object/to_query'
+# frozen_string_literal: true
+
+require "active_support/core_ext/object/to_query"
diff --git a/activesupport/lib/active_support/core_ext/object/to_query.rb b/activesupport/lib/active_support/core_ext/object/to_query.rb
index ec5ace4e16..bac6ff9c97 100644
--- a/activesupport/lib/active_support/core_ext/object/to_query.rb
+++ b/activesupport/lib/active_support/core_ext/object/to_query.rb
@@ -1,4 +1,6 @@
-require 'cgi'
+# frozen_string_literal: true
+
+require "cgi"
class Object
# Alias of <tt>to_s</tt>.
@@ -38,7 +40,7 @@ class Array
# Calls <tt>to_param</tt> on all its elements and joins the result with
# slashes. This is used by <tt>url_for</tt> in Action Pack.
def to_param
- collect(&:to_param).join '/'
+ collect(&:to_param).join "/"
end
# Converts an array into a string suitable for use as a URL query string,
@@ -51,7 +53,7 @@ class Array
if empty?
nil.to_query(prefix)
else
- collect { |value| value.to_query(prefix) }.join '&'
+ collect { |value| value.to_query(prefix) }.join "&"
end
end
end
@@ -73,11 +75,14 @@ class Hash
#
# This method is also aliased as +to_param+.
def to_query(namespace = nil)
- collect do |key, value|
+ query = collect do |key, value|
unless (value.is_a?(Hash) || value.is_a?(Array)) && value.empty?
value.to_query(namespace ? "#{namespace}[#{key}]" : key)
end
- end.compact.sort! * '&'
+ end.compact
+
+ query.sort! unless namespace.to_s.include?("[]")
+ query.join("&")
end
alias_method :to_param, :to_query
diff --git a/activesupport/lib/active_support/core_ext/object/try.rb b/activesupport/lib/active_support/core_ext/object/try.rb
index 69be6c4abc..ef8a1f476d 100644
--- a/activesupport/lib/active_support/core_ext/object/try.rb
+++ b/activesupport/lib/active_support/core_ext/object/try.rb
@@ -1,20 +1,30 @@
-require 'delegate'
+# frozen_string_literal: true
+
+require "delegate"
module ActiveSupport
module Tryable #:nodoc:
- def try(*a, &b)
- try!(*a, &b) if a.empty? || respond_to?(a.first)
+ def try(method_name = nil, *args, &b)
+ if method_name.nil? && block_given?
+ if b.arity == 0
+ instance_eval(&b)
+ else
+ yield self
+ end
+ elsif respond_to?(method_name)
+ public_send(method_name, *args, &b)
+ end
end
- def try!(*a, &b)
- if a.empty? && block_given?
- if b.arity.zero?
+ def try!(method_name = nil, *args, &b)
+ if method_name.nil? && block_given?
+ if b.arity == 0
instance_eval(&b)
else
yield self
end
else
- public_send(*a, &b)
+ public_send(method_name, *args, &b)
end
end
end
@@ -94,12 +104,12 @@ class Object
# :call-seq:
# try!(*a, &b)
#
- # Same as #try, but raises a NoMethodError exception if the receiver is
+ # Same as #try, but raises a +NoMethodError+ exception if the receiver is
# not +nil+ and does not implement the tried method.
#
# "a".try!(:upcase) # => "A"
# nil.try!(:upcase) # => nil
- # 123.try!(:upcase) # => NoMethodError: undefined method `upcase' for 123:Fixnum
+ # 123.try!(:upcase) # => NoMethodError: undefined method `upcase' for 123:Integer
end
class Delegator
@@ -133,14 +143,14 @@ class NilClass
#
# With +try+
# @person.try(:children).try(:first).try(:name)
- def try(*args)
+ def try(method_name = nil, *args)
nil
end
# Calling +try!+ on +nil+ always returns +nil+.
#
# nil.try!(:name) # => nil
- def try!(*args)
+ def try!(method_name = nil, *args)
nil
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 513c8b1d55..1d46add6e0 100644
--- a/activesupport/lib/active_support/core_ext/object/with_options.rb
+++ b/activesupport/lib/active_support/core_ext/object/with_options.rb
@@ -1,4 +1,6 @@
-require 'active_support/option_merger'
+# frozen_string_literal: true
+
+require "active_support/option_merger"
class Object
# An elegant way to factor duplication out of options passed to a series of
@@ -60,7 +62,18 @@ class Object
#
# validates :content, length: { minimum: 50 }, if: -> { content.present? }
#
- # Hence the inherited default for `if` key is ignored.
+ # Hence the inherited default for +if+ key is ignored.
+ #
+ # NOTE: You cannot call class methods implicitly inside of with_options.
+ # You can access these methods using the class name instead:
+ #
+ # 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)
diff --git a/activesupport/lib/active_support/core_ext/range.rb b/activesupport/lib/active_support/core_ext/range.rb
index 9368e81235..78814fd189 100644
--- a/activesupport/lib/active_support/core_ext/range.rb
+++ b/activesupport/lib/active_support/core_ext/range.rb
@@ -1,4 +1,7 @@
-require 'active_support/core_ext/range/conversions'
-require 'active_support/core_ext/range/include_range'
-require 'active_support/core_ext/range/overlaps'
-require 'active_support/core_ext/range/each'
+# frozen_string_literal: true
+
+require "active_support/core_ext/range/conversions"
+require "active_support/core_ext/range/compare_range"
+require "active_support/core_ext/range/include_time_with_zone"
+require "active_support/core_ext/range/overlaps"
+require "active_support/core_ext/range/each"
diff --git a/activesupport/lib/active_support/core_ext/range/compare_range.rb b/activesupport/lib/active_support/core_ext/range/compare_range.rb
new file mode 100644
index 0000000000..6f6d2a27bb
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/range/compare_range.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module ActiveSupport
+ module CompareWithRange
+ # Extends the default Range#=== to support range comparisons.
+ # (1..5) === (1..5) # => true
+ # (1..5) === (2..3) # => true
+ # (1..5) === (2..6) # => false
+ #
+ # The native Range#=== behavior is untouched.
+ # ('a'..'f') === ('c') # => true
+ # (5..9) === (11) # => false
+ def ===(value)
+ if value.is_a?(::Range)
+ # 1...10 includes 1..9 but it does not include 1..10.
+ operator = exclude_end? && !value.exclude_end? ? :< : :<=
+ super(value.first) && value.last.send(operator, last)
+ else
+ super
+ end
+ end
+
+ # Extends the default Range#include? to support range comparisons.
+ # (1..5).include?(1..5) # => true
+ # (1..5).include?(2..3) # => true
+ # (1..5).include?(2..6) # => false
+ #
+ # The native Range#include? behavior is untouched.
+ # ('a'..'f').include?('c') # => true
+ # (5..9).include?(11) # => false
+ def include?(value)
+ if value.is_a?(::Range)
+ # 1...10 includes 1..9 but it does not include 1..10.
+ operator = exclude_end? && !value.exclude_end? ? :< : :<=
+ super(value.first) && value.last.send(operator, last)
+ else
+ super
+ end
+ end
+
+ # Extends the default Range#cover? to support range comparisons.
+ # (1..5).cover?(1..5) # => true
+ # (1..5).cover?(2..3) # => true
+ # (1..5).cover?(2..6) # => false
+ #
+ # The native Range#cover? behavior is untouched.
+ # ('a'..'f').cover?('c') # => true
+ # (5..9).cover?(11) # => false
+ def cover?(value)
+ if value.is_a?(::Range)
+ # 1...10 covers 1..9 but it does not cover 1..10.
+ operator = exclude_end? && !value.exclude_end? ? :< : :<=
+ super(value.first) && value.last.send(operator, last)
+ else
+ super
+ end
+ end
+ end
+end
+
+Range.prepend(ActiveSupport::CompareWithRange)
diff --git a/activesupport/lib/active_support/core_ext/range/conversions.rb b/activesupport/lib/active_support/core_ext/range/conversions.rb
index 83eced50bf..024e32db40 100644
--- a/activesupport/lib/active_support/core_ext/range/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/range/conversions.rb
@@ -1,34 +1,41 @@
-class Range
- RANGE_FORMATS = {
- :db => Proc.new { |start, stop| "BETWEEN '#{start.to_s(:db)}' AND '#{stop.to_s(:db)}'" }
- }
+# frozen_string_literal: true
- # Convert range to a formatted string. See RANGE_FORMATS for predefined formats.
- #
- # This method is aliased to <tt>to_s</tt>.
- #
- # range = (1..100) # => 1..100
- #
- # range.to_formatted_s # => "1..100"
- # range.to_s # => "1..100"
- #
- # range.to_formatted_s(:db) # => "BETWEEN '1' AND '100'"
- # range.to_s(:db) # => "BETWEEN '1' AND '100'"
- #
- # == Adding your own range formats to to_formatted_s
- # You can add your own formats to the Range::RANGE_FORMATS hash.
- # Use the format name as the hash key and a Proc instance.
- #
- # # config/initializers/range_formats.rb
- # Range::RANGE_FORMATS[:short] = ->(start, stop) { "Between #{start.to_s(:db)} and #{stop.to_s(:db)}" }
- def to_formatted_s(format = :default)
- if formatter = RANGE_FORMATS[format]
- formatter.call(first, last)
- else
- to_default_s
+module ActiveSupport
+ module RangeWithFormat
+ RANGE_FORMATS = {
+ db: -> (start, stop) do
+ case start
+ when String then "BETWEEN '#{start}' AND '#{stop}'"
+ else
+ "BETWEEN '#{start.to_s(:db)}' AND '#{stop.to_s(:db)}'"
+ end
+ end
+ }
+
+ # Convert range to a formatted string. See RANGE_FORMATS for predefined formats.
+ #
+ # range = (1..100) # => 1..100
+ #
+ # range.to_s # => "1..100"
+ # range.to_s(:db) # => "BETWEEN '1' AND '100'"
+ #
+ # == Adding your own range formats to to_s
+ # You can add your own formats to the Range::RANGE_FORMATS hash.
+ # Use the format name as the hash key and a Proc instance.
+ #
+ # # config/initializers/range_formats.rb
+ # Range::RANGE_FORMATS[:short] = ->(start, stop) { "Between #{start.to_s(:db)} and #{stop.to_s(:db)}" }
+ def to_s(format = :default)
+ if formatter = RANGE_FORMATS[format]
+ formatter.call(first, last)
+ else
+ super()
+ end
end
- end
- alias_method :to_default_s, :to_s
- alias_method :to_s, :to_formatted_s
+ alias_method :to_default_s, :to_s
+ alias_method :to_formatted_s, :to_s
+ end
end
+
+Range.prepend(ActiveSupport::RangeWithFormat)
diff --git a/activesupport/lib/active_support/core_ext/range/each.rb b/activesupport/lib/active_support/core_ext/range/each.rb
index dc6dad5ced..2f22cd0e92 100644
--- a/activesupport/lib/active_support/core_ext/range/each.rb
+++ b/activesupport/lib/active_support/core_ext/range/each.rb
@@ -1,3 +1,7 @@
+# frozen_string_literal: true
+
+require "active_support/time_with_zone"
+
module ActiveSupport
module EachTimeWithZone #:nodoc:
def each(&block)
@@ -13,7 +17,7 @@ module ActiveSupport
private
def ensure_iteration_allowed
- raise TypeError, "can't iterate from #{first.class}" if first.is_a?(Time)
+ raise TypeError, "can't iterate from #{first.class}" if first.is_a?(TimeWithZone)
end
end
end
diff --git a/activesupport/lib/active_support/core_ext/range/include_range.rb b/activesupport/lib/active_support/core_ext/range/include_range.rb
index c69e1e3fb9..2da2c587a3 100644
--- a/activesupport/lib/active_support/core_ext/range/include_range.rb
+++ b/activesupport/lib/active_support/core_ext/range/include_range.rb
@@ -1,23 +1,9 @@
-module ActiveSupport
- module IncludeWithRange #:nodoc:
- # Extends the default Range#include? to support range comparisons.
- # (1..5).include?(1..5) # => true
- # (1..5).include?(2..3) # => true
- # (1..5).include?(2..6) # => false
- #
- # The native Range#include? behavior is untouched.
- # ('a'..'f').include?('c') # => true
- # (5..9).include?(11) # => false
- def include?(value)
- if value.is_a?(::Range)
- # 1...10 includes 1..9 but it does not include 1..10.
- operator = exclude_end? && !value.exclude_end? ? :< : :<=
- super(value.first) && value.last.send(operator, last)
- else
- super
- end
- end
- end
-end
+# frozen_string_literal: true
-Range.prepend(ActiveSupport::IncludeWithRange)
+require "active_support/deprecation"
+
+ActiveSupport::Deprecation.warn "You have required `active_support/core_ext/range/include_range`. " \
+"This file will be removed in Rails 6.1. You should require `active_support/core_ext/range/compare_range` " \
+ "instead."
+
+require "active_support/core_ext/range/compare_range"
diff --git a/activesupport/lib/active_support/core_ext/range/include_time_with_zone.rb b/activesupport/lib/active_support/core_ext/range/include_time_with_zone.rb
new file mode 100644
index 0000000000..5f80acf68e
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/range/include_time_with_zone.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require "active_support/time_with_zone"
+
+module ActiveSupport
+ module IncludeTimeWithZone #:nodoc:
+ # Extends the default Range#include? to support ActiveSupport::TimeWithZone.
+ #
+ # (1.hour.ago..1.hour.from_now).include?(Time.current) # => true
+ #
+ def include?(value)
+ if first.is_a?(TimeWithZone)
+ cover?(value)
+ elsif last.is_a?(TimeWithZone)
+ cover?(value)
+ else
+ super
+ end
+ end
+ end
+end
+
+Range.prepend(ActiveSupport::IncludeTimeWithZone)
diff --git a/activesupport/lib/active_support/core_ext/range/overlaps.rb b/activesupport/lib/active_support/core_ext/range/overlaps.rb
index 603657c180..f753607f8b 100644
--- a/activesupport/lib/active_support/core_ext/range/overlaps.rb
+++ b/activesupport/lib/active_support/core_ext/range/overlaps.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Range
# Compare two ranges and see if they overlap each other
# (1..5).overlaps?(4..6) # => true
diff --git a/activesupport/lib/active_support/core_ext/regexp.rb b/activesupport/lib/active_support/core_ext/regexp.rb
index 784145f5fb..d92943c7ae 100644
--- a/activesupport/lib/active_support/core_ext/regexp.rb
+++ b/activesupport/lib/active_support/core_ext/regexp.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Regexp #:nodoc:
def multiline?
options & MULTILINE == MULTILINE
diff --git a/activesupport/lib/active_support/core_ext/securerandom.rb b/activesupport/lib/active_support/core_ext/securerandom.rb
index 6cdbea1f37..b4a491f5fd 100644
--- a/activesupport/lib/active_support/core_ext/securerandom.rb
+++ b/activesupport/lib/active_support/core_ext/securerandom.rb
@@ -1,17 +1,19 @@
-require 'securerandom'
+# frozen_string_literal: true
+
+require "securerandom"
module SecureRandom
- BASE58_ALPHABET = ('0'..'9').to_a + ('A'..'Z').to_a + ('a'..'z').to_a - ['0', 'O', 'I', 'l']
+ BASE58_ALPHABET = ("0".."9").to_a + ("A".."Z").to_a + ("a".."z").to_a - ["0", "O", "I", "l"]
# SecureRandom.base58 generates a random base58 string.
#
# The argument _n_ specifies the length, of the random string to be generated.
#
- # If _n_ is not specified or is nil, 16 is assumed. It may be larger in the future.
+ # If _n_ is not specified or is +nil+, 16 is assumed. It may be larger in the future.
#
# The result may contain alphanumeric characters except 0, O, I and l
#
- # p SecureRandom.base58 #=> "4kUgL2pdQMSCQtjE"
- # p SecureRandom.base58(24) #=> "77TMHrHJFvFDwodq8w7Ev2m7"
+ # p SecureRandom.base58 # => "4kUgL2pdQMSCQtjE"
+ # p SecureRandom.base58(24) # => "77TMHrHJFvFDwodq8w7Ev2m7"
#
def self.base58(n = 16)
SecureRandom.random_bytes(n).unpack("C*").map do |byte|
diff --git a/activesupport/lib/active_support/core_ext/string.rb b/activesupport/lib/active_support/core_ext/string.rb
index c656db2c6c..757d15c51a 100644
--- a/activesupport/lib/active_support/core_ext/string.rb
+++ b/activesupport/lib/active_support/core_ext/string.rb
@@ -1,13 +1,15 @@
-require 'active_support/core_ext/string/conversions'
-require 'active_support/core_ext/string/filters'
-require 'active_support/core_ext/string/multibyte'
-require 'active_support/core_ext/string/starts_ends_with'
-require 'active_support/core_ext/string/inflections'
-require 'active_support/core_ext/string/access'
-require 'active_support/core_ext/string/behavior'
-require 'active_support/core_ext/string/output_safety'
-require 'active_support/core_ext/string/exclude'
-require 'active_support/core_ext/string/strip'
-require 'active_support/core_ext/string/inquiry'
-require 'active_support/core_ext/string/indent'
-require 'active_support/core_ext/string/zones'
+# frozen_string_literal: true
+
+require "active_support/core_ext/string/conversions"
+require "active_support/core_ext/string/filters"
+require "active_support/core_ext/string/multibyte"
+require "active_support/core_ext/string/starts_ends_with"
+require "active_support/core_ext/string/inflections"
+require "active_support/core_ext/string/access"
+require "active_support/core_ext/string/behavior"
+require "active_support/core_ext/string/output_safety"
+require "active_support/core_ext/string/exclude"
+require "active_support/core_ext/string/strip"
+require "active_support/core_ext/string/inquiry"
+require "active_support/core_ext/string/indent"
+require "active_support/core_ext/string/zones"
diff --git a/activesupport/lib/active_support/core_ext/string/access.rb b/activesupport/lib/active_support/core_ext/string/access.rb
index ebd0dd3fc7..4ca24028b0 100644
--- a/activesupport/lib/active_support/core_ext/string/access.rb
+++ b/activesupport/lib/active_support/core_ext/string/access.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
class String
- # If you pass a single Fixnum, returns a substring of one character at that
+ # If you pass a single integer, returns a substring of one character at that
# position. The first character of the string is at position 0, the next at
# position 1, and so on. If a range is supplied, a substring containing
# characters at offsets given by the range is returned. In both cases, if an
- # offset is negative, it is counted from the end of the string. Returns nil
+ # offset is negative, it is counted from the end of the string. Returns +nil+
# if the initial offset falls outside the string. Returns an empty string if
# the beginning of the range is greater than the end of the string.
#
@@ -17,7 +19,7 @@ class String
#
# If a Regexp is given, the matching portion of the string is returned.
# If a String is given, that given string is returned if it occurs in
- # the string. In both cases, nil is returned if there is no match.
+ # the string. In both cases, +nil+ is returned if there is no match.
#
# str = "hello"
# str.at(/lo/) # => "lo"
@@ -73,10 +75,14 @@ class String
# str.first(0) # => ""
# str.first(6) # => "hello"
def first(limit = 1)
+ ActiveSupport::Deprecation.warn(
+ "Calling String#first with a negative integer limit " \
+ "will raise an ArgumentError in Rails 6.1."
+ ) if limit < 0
if limit == 0
- ''
+ ""
elsif limit >= size
- self.dup
+ dup
else
to(limit - 1)
end
@@ -93,10 +99,14 @@ class String
# str.last(0) # => ""
# str.last(6) # => "hello"
def last(limit = 1)
+ ActiveSupport::Deprecation.warn(
+ "Calling String#last with a negative integer limit " \
+ "will raise an ArgumentError in Rails 6.1."
+ ) if limit < 0
if limit == 0
- ''
+ ""
elsif limit >= size
- self.dup
+ dup
else
from(-limit)
end
diff --git a/activesupport/lib/active_support/core_ext/string/behavior.rb b/activesupport/lib/active_support/core_ext/string/behavior.rb
index 710f1f4670..35a5aa7840 100644
--- a/activesupport/lib/active_support/core_ext/string/behavior.rb
+++ b/activesupport/lib/active_support/core_ext/string/behavior.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class String
# Enables more predictable duck-typing on String-like classes. See <tt>Object#acts_like?</tt>.
def acts_like_string?
diff --git a/activesupport/lib/active_support/core_ext/string/conversions.rb b/activesupport/lib/active_support/core_ext/string/conversions.rb
index 3e0cb8a7ac..29a88b07ad 100644
--- a/activesupport/lib/active_support/core_ext/string/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/string/conversions.rb
@@ -1,5 +1,7 @@
-require 'date'
-require 'active_support/core_ext/time/calculations'
+# frozen_string_literal: true
+
+require "date"
+require "active_support/core_ext/time/calculations"
class String
# Converts a string to a Time value.
@@ -14,11 +16,12 @@ class String
# "06:12".to_time # => 2012-12-13 06:12:00 +0100
# "2012-12-13 06:12".to_time # => 2012-12-13 06:12:00 +0100
# "2012-12-13T06:12".to_time # => 2012-12-13 06:12:00 +0100
- # "2012-12-13T06:12".to_time(:utc) # => 2012-12-13 05:12:00 UTC
+ # "2012-12-13T06:12".to_time(:utc) # => 2012-12-13 06:12:00 UTC
# "12/13/2012".to_time # => ArgumentError: argument out of range
def to_time(form = :local)
parts = Date._parse(self, false)
- return if parts.empty?
+ used_keys = %i(year mon mday hour min sec sec_fraction offset)
+ return if (parts.keys & used_keys).empty?
now = Time.now
time = Time.new(
@@ -31,7 +34,7 @@ class String
parts.fetch(:offset, form == :utc ? 0 : nil)
)
- form == :utc ? time.utc : time.getlocal
+ form == :utc ? time.utc : time.to_time
end
# Converts a string to a Date value.
diff --git a/activesupport/lib/active_support/core_ext/string/exclude.rb b/activesupport/lib/active_support/core_ext/string/exclude.rb
index 0ac684f6ee..8e462689f1 100644
--- a/activesupport/lib/active_support/core_ext/string/exclude.rb
+++ b/activesupport/lib/active_support/core_ext/string/exclude.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class String
# The inverse of <tt>String#include?</tt>. Returns true if the string
# does not include the other string.
diff --git a/activesupport/lib/active_support/core_ext/string/filters.rb b/activesupport/lib/active_support/core_ext/string/filters.rb
index 375ec1aef8..df0e79afa8 100644
--- a/activesupport/lib/active_support/core_ext/string/filters.rb
+++ b/activesupport/lib/active_support/core_ext/string/filters.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class String
# Returns the string, first removing all whitespace on both ends of
# the string, and then changing remaining consecutive whitespace
@@ -17,7 +19,7 @@ class String
# str.squish! # => "foo bar boo"
# str # => "foo bar boo"
def squish!
- gsub!(/[[:space:]]+/, ' ')
+ gsub!(/[[:space:]]+/, " ")
strip!
self
end
@@ -64,7 +66,7 @@ class String
def truncate(truncate_at, options = {})
return dup unless length > truncate_at
- omission = options[:omission] || '...'
+ omission = options[:omission] || "..."
length_with_room_for_omission = truncate_at - omission.length
stop = \
if options[:separator]
@@ -76,6 +78,47 @@ class String
"#{self[0, stop]}#{omission}"
end
+ # Truncates +text+ to at most <tt>bytesize</tt> bytes in length without
+ # breaking string encoding by splitting multibyte characters or breaking
+ # grapheme clusters ("perceptual characters") by truncating at combining
+ # characters.
+ #
+ # >> "🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪".size
+ # => 20
+ # >> "🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪".bytesize
+ # => 80
+ # >> "🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪".truncate_bytes(20)
+ # => "🔪🔪🔪🔪…"
+ #
+ # The truncated text ends with the <tt>:omission</tt> string, defaulting
+ # to "…", for a total length not exceeding <tt>bytesize</tt>.
+ def truncate_bytes(truncate_at, omission: "…")
+ omission ||= ""
+
+ case
+ when bytesize <= truncate_at
+ dup
+ when omission.bytesize > truncate_at
+ raise ArgumentError, "Omission #{omission.inspect} is #{omission.bytesize}, larger than the truncation length of #{truncate_at} bytes"
+ when omission.bytesize == truncate_at
+ omission.dup
+ else
+ self.class.new.tap do |cut|
+ cut_at = truncate_at - omission.bytesize
+
+ scan(/\X/) do |grapheme|
+ if cut.bytesize + grapheme.bytesize <= cut_at
+ cut << grapheme
+ else
+ break
+ end
+ end
+
+ cut << omission
+ end
+ end
+ end
+
# Truncates a given +text+ after a given number of words (<tt>words_count</tt>):
#
# 'Once upon a time in a world far far away'.truncate_words(4)
@@ -94,7 +137,7 @@ class String
sep = options[:separator] || /\s+/
sep = Regexp.escape(sep.to_s) unless Regexp === sep
if self =~ /\A((?>.+?#{sep}){#{words_count - 1}}.+?)#{sep}.*/m
- $1 + (options[:omission] || '...')
+ $1 + (options[:omission] || "...")
else
dup
end
diff --git a/activesupport/lib/active_support/core_ext/string/indent.rb b/activesupport/lib/active_support/core_ext/string/indent.rb
index ce3a69cf5f..af9d181487 100644
--- a/activesupport/lib/active_support/core_ext/string/indent.rb
+++ b/activesupport/lib/active_support/core_ext/string/indent.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
class String
# Same as +indent+, except it indents the receiver in-place.
#
# Returns the indented string, or +nil+ if there was nothing to indent.
- def indent!(amount, indent_string=nil, indent_empty_lines=false)
- indent_string = indent_string || self[/^[ \t]/] || ' '
+ def indent!(amount, indent_string = nil, indent_empty_lines = false)
+ indent_string = indent_string || self[/^[ \t]/] || " "
re = indent_empty_lines ? /^/ : /^(?!$)/
gsub!(re, indent_string * amount)
end
@@ -37,7 +39,7 @@ class String
# "foo\n\nbar".indent(2) # => " foo\n\n bar"
# "foo\n\nbar".indent(2, nil, true) # => " foo\n \n bar"
#
- def indent(amount, indent_string=nil, indent_empty_lines=false)
- dup.tap {|_| _.indent!(amount, indent_string, indent_empty_lines)}
+ def indent(amount, indent_string = nil, indent_empty_lines = false)
+ dup.tap { |_| _.indent!(amount, indent_string, indent_empty_lines) }
end
end
diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb
index 97f9720b2b..8af301734a 100644
--- a/activesupport/lib/active_support/core_ext/string/inflections.rb
+++ b/activesupport/lib/active_support/core_ext/string/inflections.rb
@@ -1,5 +1,7 @@
-require 'active_support/inflector/methods'
-require 'active_support/inflector/transliterate'
+# frozen_string_literal: true
+
+require "active_support/inflector/methods"
+require "active_support/inflector/transliterate"
# String inflections define new methods on the String class to transform names for different purposes.
# For instance, you can figure out the name of a table from the name of a class.
@@ -31,7 +33,7 @@ class String
def pluralize(count = nil, locale = :en)
locale = count if count.is_a?(Symbol)
if count == 1
- self.dup
+ dup
else
ActiveSupport::Inflector.pluralize(self, locale)
end
@@ -67,7 +69,7 @@ class String
end
# +safe_constantize+ tries to find a declared constant with the name specified
- # in the string. It returns nil when the name is not in CamelCase
+ # in the string. It returns +nil+ when the name is not in CamelCase
# or is not initialized. See ActiveSupport::Inflector.safe_constantize
#
# 'Module'.safe_constantize # => Module
@@ -92,6 +94,8 @@ class String
ActiveSupport::Inflector.camelize(self, true)
when :lower
ActiveSupport::Inflector.camelize(self, false)
+ else
+ raise ArgumentError, "Invalid option, use either :upper or :lower."
end
end
alias_method :camelcase, :camelize
@@ -100,12 +104,17 @@ class String
# a nicer looking title. +titleize+ is meant for creating pretty output. It is not
# used in the Rails internals.
#
+ # The trailing '_id','Id'.. can be kept and capitalized by setting the
+ # optional parameter +keep_id_suffix+ to true.
+ # By default, this parameter is false.
+ #
# +titleize+ is also aliased as +titlecase+.
#
- # 'man from the boondocks'.titleize # => "Man From The Boondocks"
- # 'x-men: the last stand'.titleize # => "X Men: The Last Stand"
- def titleize
- ActiveSupport::Inflector.titleize(self)
+ # 'man from the boondocks'.titleize # => "Man From The Boondocks"
+ # 'x-men: the last stand'.titleize # => "X Men: The Last Stand"
+ # 'string_ending_with_id'.titleize(keep_id_suffix: true) # => "String Ending With Id"
+ def titleize(keep_id_suffix: false)
+ ActiveSupport::Inflector.titleize(self, keep_id_suffix: keep_id_suffix)
end
alias_method :titlecase, :titleize
@@ -128,10 +137,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
@@ -164,15 +173,29 @@ class String
#
# <%= link_to(@person.name, person_path) %>
# # => <a href="/person/1-donald-e-knuth">Donald E. Knuth</a>
- def parameterize(sep = '-')
- ActiveSupport::Inflector.parameterize(self, sep)
+ #
+ # To preserve the case of the characters in a string, use the +preserve_case+ argument.
+ #
+ # class Person
+ # def to_param
+ # "#{id}-#{name.parameterize(preserve_case: true)}"
+ # end
+ # end
+ #
+ # @person = Person.find(1)
+ # # => #<Person id: 1, name: "Donald E. Knuth">
+ #
+ # <%= link_to(@person.name, person_path) %>
+ # # => <a href="/person/1-Donald-E-Knuth">Donald E. Knuth</a>
+ def parameterize(separator: "-", preserve_case: false)
+ ActiveSupport::Inflector.parameterize(self, separator: separator, preserve_case: preserve_case)
end
# Creates the name of a table like Rails does for models to table names. This method
# uses the +pluralize+ method on the last word in the string.
#
# 'RawScaledScorer'.tableize # => "raw_scaled_scorers"
- # 'egg_and_ham'.tableize # => "egg_and_hams"
+ # 'ham_and_egg'.tableize # => "ham_and_eggs"
# 'fancyCategory'.tableize # => "fancy_categories"
def tableize
ActiveSupport::Inflector.tableize(self)
@@ -182,13 +205,13 @@ class String
# Note that this returns a string and not a class. (To convert to an actual class
# follow +classify+ with +constantize+.)
#
- # 'egg_and_hams'.classify # => "EggAndHam"
+ # 'ham_and_eggs'.classify # => "HamAndEgg"
# 'posts'.classify # => "Post"
def classify
ActiveSupport::Inflector.classify(self)
end
- # Capitalizes the first word, turns underscores into spaces, and strips a
+ # Capitalizes the first word, turns underscores into spaces, and (by default)strips a
# trailing '_id' if present.
# Like +titleize+, this is meant for creating pretty output.
#
@@ -196,12 +219,26 @@ class String
# optional parameter +capitalize+ to false.
# By default, this parameter is true.
#
- # 'employee_salary'.humanize # => "Employee salary"
- # 'author_id'.humanize # => "Author"
- # 'author_id'.humanize(capitalize: false) # => "author"
- # '_id'.humanize # => "Id"
- def humanize(options = {})
- ActiveSupport::Inflector.humanize(self, options)
+ # The trailing '_id' can be kept and capitalized by setting the
+ # optional parameter +keep_id_suffix+ to true.
+ # By default, this parameter is false.
+ #
+ # 'employee_salary'.humanize # => "Employee salary"
+ # 'author_id'.humanize # => "Author"
+ # 'author_id'.humanize(capitalize: false) # => "author"
+ # '_id'.humanize # => "Id"
+ # 'author_id'.humanize(keep_id_suffix: true) # => "Author Id"
+ def humanize(capitalize: true, keep_id_suffix: false)
+ ActiveSupport::Inflector.humanize(self, capitalize: capitalize, keep_id_suffix: keep_id_suffix)
+ end
+
+ # Converts just the first character to uppercase.
+ #
+ # 'what a Lovely Day'.upcase_first # => "What a Lovely Day"
+ # 'w'.upcase_first # => "W"
+ # ''.upcase_first # => ""
+ def upcase_first
+ ActiveSupport::Inflector.upcase_first(self)
end
# Creates a foreign key name from a class name.
diff --git a/activesupport/lib/active_support/core_ext/string/inquiry.rb b/activesupport/lib/active_support/core_ext/string/inquiry.rb
index 1dcd949536..a796d5fb4f 100644
--- a/activesupport/lib/active_support/core_ext/string/inquiry.rb
+++ b/activesupport/lib/active_support/core_ext/string/inquiry.rb
@@ -1,4 +1,6 @@
-require 'active_support/string_inquirer'
+# frozen_string_literal: true
+
+require "active_support/string_inquirer"
class String
# Wraps the current string in the <tt>ActiveSupport::StringInquirer</tt> class,
diff --git a/activesupport/lib/active_support/core_ext/string/multibyte.rb b/activesupport/lib/active_support/core_ext/string/multibyte.rb
index 7055f7f699..6cceb46507 100644
--- a/activesupport/lib/active_support/core_ext/string/multibyte.rb
+++ b/activesupport/lib/active_support/core_ext/string/multibyte.rb
@@ -1,4 +1,6 @@
-require 'active_support/multibyte'
+# frozen_string_literal: true
+
+require "active_support/multibyte"
class String
# == Multibyte proxy
@@ -9,12 +11,13 @@ class String
# encapsulates the original string. A Unicode safe version of all the String methods are defined on this proxy
# class. If the proxy class doesn't respond to a certain method, it's forwarded to the encapsulated string.
#
- # name = 'Claus Müller'
- # name.reverse # => "rell??M sualC"
- # name.length # => 13
+ # >> "lj".mb_chars.upcase.to_s
+ # => "LJ"
+ #
+ # NOTE: Ruby 2.4 and later support native Unicode case mappings:
#
- # name.mb_chars.reverse.to_s # => "rellüM sualC"
- # name.mb_chars.length # => 12
+ # >> "lj".upcase
+ # => "LJ"
#
# == Method chaining
#
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 c676b26b06..3a80de4617 100644
--- a/activesupport/lib/active_support/core_ext/string/output_safety.rb
+++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb
@@ -1,33 +1,32 @@
-require 'erb'
-require 'active_support/core_ext/kernel/singleton_class'
+# frozen_string_literal: true
+
+require "erb"
+require "active_support/core_ext/kernel/singleton_class"
+require "active_support/core_ext/module/redefine_method"
+require "active_support/multibyte/unicode"
class ERB
module Util
- HTML_ESCAPE = { '&' => '&amp;', '>' => '&gt;', '<' => '&lt;', '"' => '&quot;', "'" => '&#39;' }
- JSON_ESCAPE = { '&' => '\u0026', '>' => '\u003e', '<' => '\u003c', "\u2028" => '\u2028', "\u2029" => '\u2029' }
- HTML_ESCAPE_REGEXP = /[&"'><]/
+ HTML_ESCAPE = { "&" => "&amp;", ">" => "&gt;", "<" => "&lt;", '"' => "&quot;", "'" => "&#39;" }
+ JSON_ESCAPE = { "&" => '\u0026', ">" => '\u003e', "<" => '\u003c', "\u2028" => '\u2028', "\u2029" => '\u2029' }
HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+)|(#[xX][\dA-Fa-f]+));)/
JSON_ESCAPE_REGEXP = /[\u2028\u2029&><]/u
# A utility method for escaping HTML tag characters.
# This method is also aliased as <tt>h</tt>.
#
- # In your ERB templates, use this method to escape any unsafe content. For example:
- # <%= h @person.name %>
- #
# puts html_escape('is a > 0 & a < 10?')
# # => is a &gt; 0 &amp; a &lt; 10?
def html_escape(s)
unwrapped_html_escape(s).html_safe
end
- # Aliasing twice issues a warning "discarding old...". Remove first to avoid it.
- remove_method(:h)
+ silence_redefinition_of_method :h
alias h html_escape
module_function :h
- singleton_class.send(:remove_method, :html_escape)
+ singleton_class.silence_redefinition_of_method :html_escape
module_function :html_escape
# HTML escapes strings but doesn't wrap them with an ActiveSupport::SafeBuffer.
@@ -37,7 +36,7 @@ class ERB
if s.html_safe?
s
else
- s.gsub(HTML_ESCAPE_REGEXP, HTML_ESCAPE)
+ CGI.escapeHTML(ActiveSupport::Multibyte::Unicode.tidy_bytes(s))
end
end
module_function :unwrapped_html_escape
@@ -50,7 +49,7 @@ class ERB
# html_escape_once('&lt;&lt; Accept & Checkout')
# # => "&lt;&lt; Accept &amp; Checkout"
def html_escape_once(s)
- result = s.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
+ result = ActiveSupport::Multibyte::Unicode.tidy_bytes(s.to_s).gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
s.html_safe? ? result.html_safe : result
end
@@ -86,7 +85,7 @@ class ERB
# use inside HTML attributes.
#
# If your JSON is being used downstream for insertion into the DOM, be aware of
- # whether or not it is being inserted via +html()+. Most JQuery plugins do this.
+ # whether or not it is being inserted via +html()+. Most jQuery plugins do this.
# If that is the case, be sure to +html_escape+ or +sanitize+ any user-generated
# content returned by your JSON.
#
@@ -135,34 +134,32 @@ end
module ActiveSupport #:nodoc:
class SafeBuffer < String
UNSAFE_STRING_METHODS = %w(
- capitalize chomp chop delete downcase gsub lstrip next reverse rstrip
- slice squeeze strip sub succ swapcase tr tr_s upcase
+ capitalize chomp chop delete delete_prefix delete_suffix
+ downcase gsub lstrip next reverse rstrip slice squeeze strip
+ sub succ swapcase tr tr_s unicode_normalize upcase
)
alias_method :original_concat, :concat
private :original_concat
+ # Raised when <tt>ActiveSupport::SafeBuffer#safe_concat</tt> is called on unsafe buffers.
class SafeConcatError < StandardError
def initialize
- super 'Could not concatenate to the buffer because it is not html safe.'
+ super "Could not concatenate to the buffer because it is not html safe."
end
end
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
+ if 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
@@ -171,7 +168,7 @@ module ActiveSupport #:nodoc:
original_concat(value)
end
- def initialize(*)
+ def initialize(str = "")
@html_safe = true
super
end
@@ -190,10 +187,22 @@ module ActiveSupport #:nodoc:
end
alias << concat
+ def insert(index, value)
+ super(index, html_escape_interpolated_argument(value))
+ end
+
def prepend(value)
super(html_escape_interpolated_argument(value))
end
+ def replace(value)
+ super(html_escape_interpolated_argument(value))
+ end
+
+ def []=(index, value)
+ super(index, html_escape_interpolated_argument(value))
+ end
+
def +(other)
dup.concat(other)
end
@@ -201,7 +210,7 @@ module ActiveSupport #:nodoc:
def %(args)
case args
when Hash
- escaped_args = Hash[args.map { |k,arg| [k, html_escape_interpolated_argument(arg)] }]
+ escaped_args = Hash[args.map { |k, arg| [k, html_escape_interpolated_argument(arg)] }]
else
escaped_args = Array(args).map { |arg| html_escape_interpolated_argument(arg) }
end
@@ -242,18 +251,17 @@ module ActiveSupport #:nodoc:
private
- def html_escape_interpolated_argument(arg)
- (!html_safe? || arg.html_safe?) ? arg :
- arg.to_s.gsub(ERB::Util::HTML_ESCAPE_REGEXP, ERB::Util::HTML_ESCAPE)
- end
+ def html_escape_interpolated_argument(arg)
+ (!html_safe? || arg.html_safe?) ? arg : CGI.escapeHTML(arg.to_s)
+ end
end
end
class String
# Marks a string as trusted safe. It will be inserted into HTML with no
- # additional escaping performed. It is your responsibilty to ensure that the
+ # additional escaping performed. It is your responsibility to ensure that the
# string contains no malicious content. This method is equivalent to the
- # `raw` helper in views. It is recommended that you use `sanitize` instead of
+ # +raw+ helper in views. It is recommended that you use +sanitize+ instead of
# this method. It should never be called on user input.
def html_safe
ActiveSupport::SafeBuffer.new(self)
diff --git a/activesupport/lib/active_support/core_ext/string/starts_ends_with.rb b/activesupport/lib/active_support/core_ext/string/starts_ends_with.rb
index 641acf62d0..919eb7a573 100644
--- a/activesupport/lib/active_support/core_ext/string/starts_ends_with.rb
+++ b/activesupport/lib/active_support/core_ext/string/starts_ends_with.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class String
alias_method :starts_with?, :start_with?
alias_method :ends_with?, :end_with?
diff --git a/activesupport/lib/active_support/core_ext/string/strip.rb b/activesupport/lib/active_support/core_ext/string/strip.rb
index 086c610976..60e9952ee6 100644
--- a/activesupport/lib/active_support/core_ext/string/strip.rb
+++ b/activesupport/lib/active_support/core_ext/string/strip.rb
@@ -1,4 +1,4 @@
-require 'active_support/core_ext/object/try'
+# frozen_string_literal: true
class String
# Strips indentation in heredocs.
@@ -17,10 +17,11 @@ class String
#
# the user would see the usage message aligned against the left margin.
#
- # Technically, it looks for the least indented line in the whole string, and removes
- # that amount of leading whitespace.
+ # Technically, it looks for the least indented non-empty line
+ # in the whole string, and removes that amount of leading whitespace.
def strip_heredoc
- indent = scan(/^[ \t]*(?=\S)/).min.try(:size) || 0
- gsub(/^[ \t]{#{indent}}/, '')
+ gsub(/^#{scan(/^[ \t]*(?=\S)/).min}/, "").tap do |stripped|
+ stripped.freeze if frozen?
+ end
end
end
diff --git a/activesupport/lib/active_support/core_ext/string/zones.rb b/activesupport/lib/active_support/core_ext/string/zones.rb
index 510c884c18..55dc231464 100644
--- a/activesupport/lib/active_support/core_ext/string/zones.rb
+++ b/activesupport/lib/active_support/core_ext/string/zones.rb
@@ -1,5 +1,7 @@
-require 'active_support/core_ext/string/conversions'
-require 'active_support/core_ext/time/zones'
+# frozen_string_literal: true
+
+require "active_support/core_ext/string/conversions"
+require "active_support/core_ext/time/zones"
class String
# Converts String to a TimeWithZone in the current zone if Time.zone or Time.zone_default
diff --git a/activesupport/lib/active_support/core_ext/struct.rb b/activesupport/lib/active_support/core_ext/struct.rb
deleted file mode 100644
index 1fde3db070..0000000000
--- a/activesupport/lib/active_support/core_ext/struct.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/activesupport/lib/active_support/core_ext/time.rb b/activesupport/lib/active_support/core_ext/time.rb
index 72c3234630..c809def05f 100644
--- a/activesupport/lib/active_support/core_ext/time.rb
+++ b/activesupport/lib/active_support/core_ext/time.rb
@@ -1,4 +1,7 @@
-require 'active_support/core_ext/time/acts_like'
-require 'active_support/core_ext/time/calculations'
-require 'active_support/core_ext/time/conversions'
-require 'active_support/core_ext/time/zones'
+# frozen_string_literal: true
+
+require "active_support/core_ext/time/acts_like"
+require "active_support/core_ext/time/calculations"
+require "active_support/core_ext/time/compatibility"
+require "active_support/core_ext/time/conversions"
+require "active_support/core_ext/time/zones"
diff --git a/activesupport/lib/active_support/core_ext/time/acts_like.rb b/activesupport/lib/active_support/core_ext/time/acts_like.rb
index 3f853b7893..8572b49639 100644
--- a/activesupport/lib/active_support/core_ext/time/acts_like.rb
+++ b/activesupport/lib/active_support/core_ext/time/acts_like.rb
@@ -1,4 +1,6 @@
-require 'active_support/core_ext/object/acts_like'
+# frozen_string_literal: true
+
+require "active_support/core_ext/object/acts_like"
class Time
# Duck-types as a Time-like class. See Object#acts_like?.
diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb
index c554501893..120768dec5 100644
--- a/activesupport/lib/active_support/core_ext/time/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/time/calculations.rb
@@ -1,8 +1,11 @@
-require 'active_support/duration'
-require 'active_support/core_ext/time/conversions'
-require 'active_support/time_with_zone'
-require 'active_support/core_ext/time/zones'
-require 'active_support/core_ext/date_and_time/calculations'
+# frozen_string_literal: true
+
+require "active_support/duration"
+require "active_support/core_ext/time/conversions"
+require "active_support/time_with_zone"
+require "active_support/core_ext/time/zones"
+require "active_support/core_ext/date_and_time/calculations"
+require "active_support/core_ext/date/calculations"
class Time
include DateAndTime::Calculations
@@ -15,9 +18,9 @@ class Time
super || (self == Time && other.is_a?(ActiveSupport::TimeWithZone))
end
- # Return the number of days in the given month.
+ # Returns the number of days in the given month.
# If no year is specified, it will use the current year.
- def days_in_month(month, year = now.year)
+ def days_in_month(month, year = current.year)
if month == 2 && ::Date.gregorian_leap?(year)
29
else
@@ -25,6 +28,12 @@ class Time
end
end
+ # Returns the number of days in the given year.
+ # If no year is specified, it will use the current year.
+ def days_in_year(year = current.year)
+ days_in_month(2, year) + 337
+ end
+
# Returns <tt>Time.zone.now</tt> when <tt>Time.zone</tt> or <tt>config.time_zone</tt> are set, otherwise just returns <tt>Time.now</tt>.
def current
::Time.zone ? ::Time.zone.now : ::Time.now
@@ -46,15 +55,38 @@ 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.
#
- # Time.new(2012, 8, 29, 0, 0, 0).seconds_since_midnight # => 0
- # Time.new(2012, 8, 29, 12, 34, 56).seconds_since_midnight # => 45296
- # Time.new(2012, 8, 29, 23, 59, 59).seconds_since_midnight # => 86399
+ # Time.new(2012, 8, 29, 0, 0, 0).seconds_since_midnight # => 0.0
+ # Time.new(2012, 8, 29, 12, 34, 56).seconds_since_midnight # => 45296.0
+ # Time.new(2012, 8, 29, 23, 59, 59).seconds_since_midnight # => 86399.0
def seconds_since_midnight
- to_i - change(:hour => 0).to_i + (usec / 1.0e+6)
+ to_i - change(hour: 0).to_i + (usec / 1.0e+6)
end
# Returns the number of seconds until 23:59:59.
@@ -66,40 +98,53 @@ class Time
end_of_day.to_i - to_i
end
+ # Returns the fraction of a second as a +Rational+
+ #
+ # Time.new(2012, 8, 29, 0, 0, 0.5).sec_fraction # => (1/2)
+ def sec_fraction
+ subsec
+ end
+
# Returns a new Time where one or more of the elements have been changed according
# to the +options+ parameter. The time options (<tt>:hour</tt>, <tt>:min</tt>,
# <tt>:sec</tt>, <tt>:usec</tt>, <tt>:nsec</tt>) reset cascadingly, so if only
# the hour is passed, then minute, sec, usec and nsec is set to 0. If the hour
- # and minute is passed, then sec, usec and nsec is set to 0. The +options+
- # parameter takes a hash with any of these keys: <tt>:year</tt>, <tt>:month</tt>,
- # <tt>:day</tt>, <tt>:hour</tt>, <tt>:min</tt>, <tt>:sec</tt>, <tt>:usec</tt>
- # <tt>:nsec</tt>. Pass either <tt>:usec</tt> or <tt>:nsec</tt>, not both.
+ # and minute is passed, then sec, usec and nsec is set to 0. The +options+ parameter
+ # takes a hash with any of these keys: <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>,
+ # <tt>:hour</tt>, <tt>:min</tt>, <tt>:sec</tt>, <tt>:usec</tt>, <tt>:nsec</tt>,
+ # <tt>:offset</tt>. Pass either <tt>:usec</tt> or <tt>:nsec</tt>, not both.
#
# Time.new(2012, 8, 29, 22, 35, 0).change(day: 1) # => Time.new(2012, 8, 1, 22, 35, 0)
# Time.new(2012, 8, 29, 22, 35, 0).change(year: 1981, day: 1) # => Time.new(1981, 8, 1, 22, 35, 0)
# Time.new(2012, 8, 29, 22, 35, 0).change(year: 1981, hour: 0) # => Time.new(1981, 8, 29, 0, 0, 0)
def change(options)
- new_year = options.fetch(:year, year)
- new_month = options.fetch(:month, month)
- new_day = options.fetch(:day, day)
- new_hour = options.fetch(:hour, hour)
- new_min = options.fetch(:min, options[:hour] ? 0 : min)
- new_sec = options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec)
+ new_year = options.fetch(:year, year)
+ new_month = options.fetch(:month, month)
+ new_day = options.fetch(:day, day)
+ new_hour = options.fetch(:hour, hour)
+ new_min = options.fetch(:min, options[:hour] ? 0 : min)
+ new_sec = options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec)
+ new_offset = options.fetch(:offset, nil)
if new_nsec = options[:nsec]
raise ArgumentError, "Can't change both :nsec and :usec at the same time: #{options.inspect}" if options[:usec]
new_usec = Rational(new_nsec, 1000)
else
- new_usec = options.fetch(:usec, (options[:hour] || options[:min] || options[:sec]) ? 0 : Rational(nsec, 1000))
+ new_usec = options.fetch(:usec, (options[:hour] || options[:min] || options[:sec]) ? 0 : Rational(nsec, 1000))
end
- if utc?
- ::Time.utc(new_year, new_month, new_day, new_hour, new_min, new_sec, new_usec)
+ raise ArgumentError, "argument out of range" if new_usec >= 1000000
+
+ new_sec += Rational(new_usec, 1000000)
+
+ if new_offset
+ ::Time.new(new_year, new_month, new_day, new_hour, new_min, new_sec, new_offset)
+ elsif utc?
+ ::Time.utc(new_year, new_month, new_day, new_hour, new_min, new_sec)
elsif zone
- ::Time.local(new_year, new_month, new_day, new_hour, new_min, new_sec, new_usec)
+ ::Time.local(new_year, new_month, new_day, new_hour, new_min, new_sec)
else
- raise ArgumentError, 'argument out of range' if new_usec >= 1000000
- ::Time.new(new_year, new_month, new_day, new_hour, new_min, new_sec + (new_usec.to_r / 1000000), utc_offset)
+ ::Time.new(new_year, new_month, new_day, new_hour, new_min, new_sec, utc_offset)
end
end
@@ -108,6 +153,12 @@ class Time
# takes a hash with any of these keys: <tt>:years</tt>, <tt>:months</tt>,
# <tt>:weeks</tt>, <tt>:days</tt>, <tt>:hours</tt>, <tt>:minutes</tt>,
# <tt>:seconds</tt>.
+ #
+ # Time.new(2015, 8, 1, 14, 35, 0).advance(seconds: 1) # => 2015-08-01 14:35:01 -0700
+ # Time.new(2015, 8, 1, 14, 35, 0).advance(minutes: 1) # => 2015-08-01 14:36:00 -0700
+ # Time.new(2015, 8, 1, 14, 35, 0).advance(hours: 1) # => 2015-08-01 15:35:00 -0700
+ # Time.new(2015, 8, 1, 14, 35, 0).advance(days: 1) # => 2015-08-02 14:35:00 -0700
+ # Time.new(2015, 8, 1, 14, 35, 0).advance(weeks: 1) # => 2015-08-08 14:35:00 -0700
def advance(options)
unless options[:weeks].nil?
options[:weeks], partial_weeks = options[:weeks].divmod(1)
@@ -121,7 +172,7 @@ class Time
d = to_date.advance(options)
d = d.gregorian if d.julian?
- time_advanced_by_date = change(:year => d.year, :month => d.month, :day => d.day)
+ time_advanced_by_date = change(year: d.year, month: d.month, day: d.day)
seconds_to_advance = \
options.fetch(:seconds, 0) +
options.fetch(:minutes, 0) * 60 +
@@ -149,8 +200,7 @@ class Time
# Returns a new Time representing the start of the day (0:00)
def beginning_of_day
- #(self - seconds_since_midnight).change(usec: 0)
- change(:hour => 0)
+ change(hour: 0)
end
alias :midnight :beginning_of_day
alias :at_midnight :beginning_of_day
@@ -158,7 +208,7 @@ class Time
# Returns a new Time representing the middle of the day (12:00)
def middle_of_day
- change(:hour => 12)
+ change(hour: 12)
end
alias :midday :middle_of_day
alias :noon :middle_of_day
@@ -169,50 +219,45 @@ class Time
# Returns a new Time representing the end of the day, 23:59:59.999999
def end_of_day
change(
- :hour => 23,
- :min => 59,
- :sec => 59,
- :usec => Rational(999999999, 1000)
+ hour: 23,
+ min: 59,
+ sec: 59,
+ usec: Rational(999999999, 1000)
)
end
alias :at_end_of_day :end_of_day
# Returns a new Time representing the start of the hour (x:00)
def beginning_of_hour
- change(:min => 0)
+ change(min: 0)
end
alias :at_beginning_of_hour :beginning_of_hour
# Returns a new Time representing the end of the hour, x:59:59.999999
def end_of_hour
change(
- :min => 59,
- :sec => 59,
- :usec => Rational(999999999, 1000)
+ min: 59,
+ sec: 59,
+ usec: Rational(999999999, 1000)
)
end
alias :at_end_of_hour :end_of_hour
# Returns a new Time representing the start of the minute (x:xx:00)
def beginning_of_minute
- change(:sec => 0)
+ change(sec: 0)
end
alias :at_beginning_of_minute :beginning_of_minute
# Returns a new Time representing the end of the minute, x:xx:59.999999
def end_of_minute
change(
- :sec => 59,
- :usec => Rational(999999999, 1000)
+ sec: 59,
+ usec: Rational(999999999, 1000)
)
end
alias :at_end_of_minute :end_of_minute
- # Returns a Range representing the whole day of the current time.
- def all_day
- beginning_of_day..end_of_day
- end
-
def plus_with_duration(other) #:nodoc:
if ActiveSupport::Duration === other
other.since(self)
@@ -267,5 +312,4 @@ class Time
end
alias_method :eql_without_coercion, :eql?
alias_method :eql?, :eql_with_coercion
-
end
diff --git a/activesupport/lib/active_support/core_ext/time/compatibility.rb b/activesupport/lib/active_support/core_ext/time/compatibility.rb
new file mode 100644
index 0000000000..495e4f307b
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/time/compatibility.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/date_and_time/compatibility"
+require "active_support/core_ext/module/redefine_method"
+
+class Time
+ include DateAndTime::Compatibility
+
+ silence_redefinition_of_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 dbf1f2f373..345cb2832c 100644
--- a/activesupport/lib/active_support/core_ext/time/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/time/conversions.rb
@@ -1,30 +1,33 @@
-require 'active_support/inflector/methods'
-require 'active_support/values/time_zone'
+# frozen_string_literal: true
+
+require "active_support/inflector/methods"
+require "active_support/values/time_zone"
class Time
DATE_FORMATS = {
- :db => '%Y-%m-%d %H:%M:%S',
- :number => '%Y%m%d%H%M%S',
- :nsec => '%Y%m%d%H%M%S%9N',
- :time => '%H:%M',
- :short => '%d %b %H:%M',
- :long => '%B %d, %Y %H:%M',
- :long_ordinal => lambda { |time|
+ db: "%Y-%m-%d %H:%M:%S",
+ number: "%Y%m%d%H%M%S",
+ nsec: "%Y%m%d%H%M%S%9N",
+ usec: "%Y%m%d%H%M%S%6N",
+ time: "%H:%M",
+ short: "%d %b %H:%M",
+ long: "%B %d, %Y %H:%M",
+ long_ordinal: lambda { |time|
day_format = ActiveSupport::Inflector.ordinalize(time.day)
time.strftime("%B #{day_format}, %Y %H:%M")
},
- :rfc822 => lambda { |time|
+ rfc822: lambda { |time|
offset_format = time.formatted_offset(false)
time.strftime("%a, %d %b %Y %H:%M:%S #{offset_format}")
},
- :iso8601 => lambda { |time| time.iso8601 }
+ iso8601: lambda { |time| time.iso8601 }
}
# Converts to a formatted string. See DATE_FORMATS for built-in formats.
#
# This method is aliased to <tt>to_s</tt>.
#
- # time = Time.now # => Thu Jan 18 06:10:17 CST 2007
+ # time = Time.now # => 2007-01-18 06:10:17 -06:00
#
# time.to_formatted_s(:time) # => "06:10"
# time.to_s(:time) # => "06:10"
@@ -55,11 +58,15 @@ class Time
alias_method :to_default_s, :to_s
alias_method :to_s, :to_formatted_s
- # Returns the UTC offset as an +HH:MM formatted string.
+ # Returns a formatted string of the offset from UTC, or an alternative
+ # string if the time zone is already UTC.
#
# Time.local(2000).formatted_offset # => "-06:00"
# Time.local(2000).formatted_offset(false) # => "-0600"
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/core_ext/time/marshal.rb b/activesupport/lib/active_support/core_ext/time/marshal.rb
deleted file mode 100644
index 467bad1726..0000000000
--- a/activesupport/lib/active_support/core_ext/time/marshal.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-require 'active_support/deprecation'
-
-ActiveSupport::Deprecation.warn("This is deprecated and will be removed in Rails 5.1 with no replacement.")
diff --git a/activesupport/lib/active_support/core_ext/time/zones.rb b/activesupport/lib/active_support/core_ext/time/zones.rb
index d683e7c777..a5588fd488 100644
--- a/activesupport/lib/active_support/core_ext/time/zones.rb
+++ b/activesupport/lib/active_support/core_ext/time/zones.rb
@@ -1,6 +1,8 @@
-require 'active_support/time_with_zone'
-require 'active_support/core_ext/time/acts_like'
-require 'active_support/core_ext/date_and_time/zones'
+# frozen_string_literal: true
+
+require "active_support/time_with_zone"
+require "active_support/core_ext/time/acts_like"
+require "active_support/core_ext/date_and_time/zones"
class Time
include DateAndTime::Zones
@@ -40,7 +42,23 @@ class Time
Thread.current[:time_zone] = find_zone!(time_zone)
end
- # Allows override of <tt>Time.zone</tt> locally inside supplied block; resets <tt>Time.zone</tt> to existing value when done.
+ # Allows override of <tt>Time.zone</tt> locally inside supplied block;
+ # resets <tt>Time.zone</tt> to existing value when done.
+ #
+ # class ApplicationController < ActionController::Base
+ # around_action :set_time_zone
+ #
+ # private
+ #
+ # def set_time_zone
+ # Time.use_zone(current_user.timezone) { yield }
+ # end
+ # end
+ #
+ # NOTE: This won't affect any <tt>ActiveSupport::TimeWithZone</tt>
+ # objects that have already been created, e.g. any model timestamp
+ # attributes that have been read before the block will remain in
+ # the application's default timezone.
def use_zone(time_zone)
new_zone = find_zone!(time_zone)
begin
@@ -53,7 +71,7 @@ class Time
# Returns a TimeZone instance matching the time zone provided.
# Accepts the time zone in any format supported by <tt>Time.zone=</tt>.
- # Raises an ArgumentError for invalid time zones.
+ # Raises an +ArgumentError+ for invalid time zones.
#
# Time.find_zone! "America/New_York" # => #<ActiveSupport::TimeZone @name="America/New_York" ...>
# Time.find_zone! "EST" # => #<ActiveSupport::TimeZone @name="EST" ...>
@@ -65,7 +83,8 @@ class Time
if !time_zone || time_zone.is_a?(ActiveSupport::TimeZone)
time_zone
else
- # lookup timezone based on identifier (unless we've been passed a TZInfo::Timezone)
+ # Look up the timezone based on the identifier (unless we've been
+ # passed a TZInfo::Timezone)
unless time_zone.respond_to?(:period_for_local)
time_zone = ActiveSupport::TimeZone[time_zone] || TZInfo::Timezone.get(time_zone)
end
diff --git a/activesupport/lib/active_support/core_ext/uri.rb b/activesupport/lib/active_support/core_ext/uri.rb
index 0b2ff817c3..cdd81ae562 100644
--- a/activesupport/lib/active_support/core_ext/uri.rb
+++ b/activesupport/lib/active_support/core_ext/uri.rb
@@ -1,18 +1,17 @@
-# encoding: utf-8
+# frozen_string_literal: true
-require 'uri'
-str = "\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E" # Ni-ho-nn-go in UTF-8, means Japanese.
-parser = URI::Parser.new
+require "uri"
-unless str == parser.unescape(parser.escape(str))
+if RUBY_VERSION < "2.6.0"
+ require "active_support/core_ext/module/redefine_method"
URI::Parser.class_eval do
- remove_method :unescape
+ silence_redefinition_of_method :unescape
def unescape(str, escaped = /%[a-fA-F\d]{2}/)
# TODO: Are we actually sure that ASCII == UTF-8?
# YK: My initial experiments say yes, but let's be sure please
enc = str.encoding
enc = Encoding::UTF_8 if enc == Encoding::US_ASCII
- str.gsub(escaped) { |match| [match[1, 2].hex].pack('C') }.force_encoding(enc)
+ str.dup.force_encoding(Encoding::ASCII_8BIT).gsub(escaped) { |match| [match[1, 2].hex].pack("C") }.force_encoding(enc)
end
end
end
diff --git a/activesupport/lib/active_support/current_attributes.rb b/activesupport/lib/active_support/current_attributes.rb
new file mode 100644
index 0000000000..3145ff87a1
--- /dev/null
+++ b/activesupport/lib/active_support/current_attributes.rb
@@ -0,0 +1,197 @@
+# frozen_string_literal: true
+
+require "active_support/callbacks"
+
+module ActiveSupport
+ # Abstract super class that provides a thread-isolated attributes singleton, which resets automatically
+ # before and after each request. This allows you to keep all the per-request attributes easily
+ # available to the whole system.
+ #
+ # The following full app-like example demonstrates how to use a Current class to
+ # facilitate easy access to the global, per-request attributes without passing them deeply
+ # around everywhere:
+ #
+ # # app/models/current.rb
+ # class Current < ActiveSupport::CurrentAttributes
+ # attribute :account, :user
+ # attribute :request_id, :user_agent, :ip_address
+ #
+ # resets { Time.zone = nil }
+ #
+ # def user=(user)
+ # super
+ # self.account = user.account
+ # Time.zone = user.time_zone
+ # end
+ # end
+ #
+ # # app/controllers/concerns/authentication.rb
+ # module Authentication
+ # extend ActiveSupport::Concern
+ #
+ # included do
+ # before_action :authenticate
+ # end
+ #
+ # private
+ # def authenticate
+ # if authenticated_user = User.find_by(id: cookies.encrypted[:user_id])
+ # Current.user = authenticated_user
+ # else
+ # redirect_to new_session_url
+ # end
+ # end
+ # end
+ #
+ # # app/controllers/concerns/set_current_request_details.rb
+ # module SetCurrentRequestDetails
+ # extend ActiveSupport::Concern
+ #
+ # included do
+ # before_action do
+ # Current.request_id = request.uuid
+ # Current.user_agent = request.user_agent
+ # Current.ip_address = request.ip
+ # end
+ # end
+ # end
+ #
+ # class ApplicationController < ActionController::Base
+ # include Authentication
+ # include SetCurrentRequestDetails
+ # end
+ #
+ # class MessagesController < ApplicationController
+ # def create
+ # Current.account.messages.create(message_params)
+ # end
+ # end
+ #
+ # class Message < ApplicationRecord
+ # belongs_to :creator, default: -> { Current.user }
+ # after_create { |message| Event.create(record: message) }
+ # end
+ #
+ # class Event < ApplicationRecord
+ # before_create do
+ # self.request_id = Current.request_id
+ # self.user_agent = Current.user_agent
+ # self.ip_address = Current.ip_address
+ # end
+ # end
+ #
+ # A word of caution: It's easy to overdo a global singleton like Current and tangle your model as a result.
+ # Current should only be used for a few, top-level globals, like account, user, and request details.
+ # The attributes stuck in Current should be used by more or less all actions on all requests. If you start
+ # sticking controller-specific attributes in there, you're going to create a mess.
+ class CurrentAttributes
+ include ActiveSupport::Callbacks
+ define_callbacks :reset
+
+ class << self
+ # Returns singleton instance for this class in this thread. If none exists, one is created.
+ def instance
+ current_instances[name] ||= new
+ end
+
+ # Declares one or more attributes that will be given both class and instance accessor methods.
+ def attribute(*names)
+ generated_attribute_methods.module_eval do
+ names.each do |name|
+ define_method(name) do
+ attributes[name.to_sym]
+ end
+
+ define_method("#{name}=") do |attribute|
+ attributes[name.to_sym] = attribute
+ end
+ end
+ end
+
+ names.each do |name|
+ define_singleton_method(name) do
+ instance.public_send(name)
+ end
+
+ define_singleton_method("#{name}=") do |attribute|
+ instance.public_send("#{name}=", attribute)
+ end
+ end
+ end
+
+ # Calls this block after #reset is called on the instance. Used for resetting external collaborators, like Time.zone.
+ def resets(&block)
+ set_callback :reset, :after, &block
+ end
+
+ delegate :set, :reset, to: :instance
+
+ def reset_all # :nodoc:
+ current_instances.each_value(&:reset)
+ end
+
+ def clear_all # :nodoc:
+ reset_all
+ current_instances.clear
+ end
+
+ private
+ def generated_attribute_methods
+ @generated_attribute_methods ||= Module.new.tap { |mod| include mod }
+ end
+
+ def current_instances
+ Thread.current[:current_attributes_instances] ||= {}
+ end
+
+ 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.
+ singleton_class.delegate name, to: :instance
+
+ send(name, *args, &block)
+ end
+ end
+
+ attr_accessor :attributes
+
+ def initialize
+ @attributes = {}
+ end
+
+ # Expose one or more attributes within a block. Old values are returned after the block concludes.
+ # Example demonstrating the common use of needing to set Current attributes outside the request-cycle:
+ #
+ # class Chat::PublicationJob < ApplicationJob
+ # def perform(attributes, room_number, creator)
+ # Current.set(person: creator) do
+ # Chat::Publisher.publish(attributes: attributes, room_number: room_number)
+ # end
+ # end
+ # end
+ def set(set_attributes)
+ old_attributes = compute_attributes(set_attributes.keys)
+ assign_attributes(set_attributes)
+ yield
+ ensure
+ assign_attributes(old_attributes)
+ end
+
+ # Reset all attributes. Should be called before and after actions, when used as a per-request singleton.
+ def reset
+ run_callbacks :reset do
+ self.attributes = {}
+ end
+ end
+
+ private
+ def assign_attributes(new_attributes)
+ new_attributes.each { |key, value| public_send("#{key}=", value) }
+ end
+
+ def compute_attributes(keys)
+ keys.collect { |key| [ key, public_send(key) ] }.to_h
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb
index 770c845435..d5d00b5e6e 100644
--- a/activesupport/lib/active_support/dependencies.rb
+++ b/activesupport/lib/active_support/dependencies.rb
@@ -1,94 +1,89 @@
-require 'set'
-require 'thread'
-require 'thread_safe'
-require 'pathname'
-require 'active_support/core_ext/module/aliasing'
-require 'active_support/core_ext/module/attribute_accessors'
-require 'active_support/core_ext/module/introspection'
-require 'active_support/core_ext/module/anonymous'
-require 'active_support/core_ext/module/qualified_const'
-require 'active_support/core_ext/object/blank'
-require 'active_support/core_ext/kernel/reporting'
-require 'active_support/core_ext/load_error'
-require 'active_support/core_ext/name_error'
-require 'active_support/core_ext/string/starts_ends_with'
+# frozen_string_literal: true
+
+require "set"
+require "thread"
+require "concurrent/map"
+require "pathname"
+require "active_support/core_ext/module/aliasing"
+require "active_support/core_ext/module/attribute_accessors"
+require "active_support/core_ext/module/introspection"
+require "active_support/core_ext/module/anonymous"
+require "active_support/core_ext/object/blank"
+require "active_support/core_ext/kernel/reporting"
+require "active_support/core_ext/load_error"
+require "active_support/core_ext/name_error"
+require "active_support/core_ext/string/starts_ends_with"
require "active_support/dependencies/interlock"
-require 'active_support/inflector'
+require "active_support/inflector"
module ActiveSupport #:nodoc:
module Dependencies #:nodoc:
extend self
- mattr_accessor :interlock
- self.interlock = Interlock.new
+ mattr_accessor :interlock, default: Interlock.new
# :doc:
# Execute the supplied block without interference from any
- # concurrent loads
+ # concurrent loads.
def self.run_interlock
Dependencies.interlock.running { yield }
end
# Execute the supplied block while holding an exclusive lock,
# preventing any other thread from being inside a #run_interlock
- # block at the same time
+ # block at the same time.
def self.load_interlock
Dependencies.interlock.loading { yield }
end
+ # Execute the supplied block while holding an exclusive lock,
+ # preventing any other thread from being inside a #run_interlock
+ # block at the same time.
+ def self.unload_interlock
+ Dependencies.interlock.unloading { yield }
+ end
+
# :nodoc:
# Should we turn on Ruby warnings on the first load of dependent files?
- mattr_accessor :warnings_on_first_load
- self.warnings_on_first_load = false
+ mattr_accessor :warnings_on_first_load, default: false
# All files ever loaded.
- mattr_accessor :history
- self.history = Set.new
+ mattr_accessor :history, default: Set.new
# All files currently loaded.
- mattr_accessor :loaded
- self.loaded = Set.new
+ mattr_accessor :loaded, default: Set.new
# Stack of files being loaded.
- mattr_accessor :loading
- self.loading = []
+ mattr_accessor :loading, default: []
# Should we load files or require them?
- mattr_accessor :mechanism
- self.mechanism = ENV['NO_RELOAD'] ? :require : :load
+ mattr_accessor :mechanism, default: ENV["NO_RELOAD"] ? :require : :load
# The set of directories from which we may automatically load files. Files
# under these directories will be reloaded on each request in development mode,
# unless the directory also appears in autoload_once_paths.
- mattr_accessor :autoload_paths
- self.autoload_paths = []
+ mattr_accessor :autoload_paths, default: []
# The set of directories from which automatically loaded constants are loaded
# only once. All directories in this set must also be present in +autoload_paths+.
- mattr_accessor :autoload_once_paths
- self.autoload_once_paths = []
+ mattr_accessor :autoload_once_paths, default: []
# An array of qualified constant names that have been loaded. Adding a name
# to this array will cause it to be unloaded the next time Dependencies are
# cleared.
- mattr_accessor :autoloaded_constants
- self.autoloaded_constants = []
+ mattr_accessor :autoloaded_constants, default: []
# An array of constant names that need to be unloaded on every request. Used
# to allow arbitrary constants to be marked for unloading.
- mattr_accessor :explicitly_unloadable_constants
- self.explicitly_unloadable_constants = []
+ mattr_accessor :explicitly_unloadable_constants, default: []
- # The logger is used for generating information on the action run-time
- # (including benchmarking) if available. Can be set to nil for no logging.
- # Compatible with both Ruby's own Logger and Log4r loggers.
+ # The logger used when tracing autoloads.
mattr_accessor :logger
- # Set to +true+ to enable logging of const_missing and file loads.
- mattr_accessor :log_activity
- self.log_activity = false
+ # If true, trace autoloads with +logger.debug+.
+ mattr_accessor :verbose, default: false
# The WatchStack keeps a stack of the modules being watched as files are
# loaded. If a file in the process of being loaded (parent.rb) triggers the
@@ -96,7 +91,7 @@ module ActiveSupport #:nodoc:
# handles the new constants.
#
# If child.rb is being autoloaded, its constants will be added to
- # autoloaded_constants. If it was being `require`d, they will be discarded.
+ # autoloaded_constants. If it was being required, they will be discarded.
#
# This is handled by walking back up the watch stack and adding the constants
# found by child.rb to the list of original constants in parent.rb.
@@ -108,9 +103,11 @@ module ActiveSupport #:nodoc:
# parent.rb then requires namespace/child.rb, the stack will look like
# [[Object], [Namespace]].
+ attr_reader :watching
+
def initialize
@watching = []
- @stack = Hash.new { |h,k| h[k] = [] }
+ @stack = Hash.new { |h, k| h[k] = [] }
end
def each(&block)
@@ -136,11 +133,11 @@ module ActiveSupport #:nodoc:
next unless mod.is_a?(Module)
# Get a list of the constants that were added
- new_constants = mod.local_constants - original_constants
+ new_constants = mod.constants(false) - original_constants
- # self[namespace] returns an Array of the constants that are being evaluated
+ # @stack[namespace] returns an Array of the constants that are being evaluated
# for that namespace. For instance, if parent.rb requires child.rb, the first
- # element of self[Object] will be an Array of the constants that were present
+ # element of @stack[Object] will be an Array of the constants that were present
# before parent.rb was required. The second element will be an Array of the
# constants that were present before child.rb was required.
@stack[namespace].each do |namespace_constants|
@@ -164,7 +161,7 @@ module ActiveSupport #:nodoc:
@watching << namespaces.map do |namespace|
module_name = Dependencies.to_constant_name(namespace)
original_constants = Dependencies.qualified_const_defined?(module_name) ?
- Inflector.constantize(module_name).local_constants : []
+ Inflector.constantize(module_name).constants(false) : []
@stack[module_name] << original_constants
module_name
@@ -172,14 +169,13 @@ module ActiveSupport #:nodoc:
end
private
- def pop_modules(modules)
- modules.each { |mod| @stack[mod].pop }
- end
+ def pop_modules(modules)
+ modules.each { |mod| @stack[mod].pop }
+ end
end
# An internal stack used to record which constants are loaded by any block.
- mattr_accessor :constant_watch_stack
- self.constant_watch_stack = WatchStack.new
+ mattr_accessor :constant_watch_stack, default: WatchStack.new
# Module includes this module.
module ModuleConstMissing #:nodoc:
@@ -236,6 +232,8 @@ module ActiveSupport #:nodoc:
Dependencies.require_or_load(file_name)
end
+ # :doc:
+
# Interprets a file using <tt>mechanism</tt> and marks its defined
# constants as autoloaded. <tt>file_name</tt> can be either a string or
# respond to <tt>to_path</tt>.
@@ -245,7 +243,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}"
@@ -254,13 +252,15 @@ module ActiveSupport #:nodoc:
Dependencies.depend_on(file_name, message)
end
+ # :nodoc:
+
def load_dependency(file)
- Dependencies.load_interlock do
- if Dependencies.load? && ActiveSupport::Dependencies.constant_watch_stack.watching?
- Dependencies.new_constants_in(Object) { yield }
- else
- yield
- end
+ if Dependencies.load? && Dependencies.constant_watch_stack.watching?
+ descs = Dependencies.constant_watch_stack.watching.flatten.uniq
+
+ Dependencies.new_constants_in(*descs) { yield }
+ else
+ yield
end
rescue Exception => exception # errors from loading file
exception.blame_file! file if exception.respond_to? :blame_file!
@@ -286,17 +286,17 @@ module ActiveSupport #:nodoc:
private
- def load(file, wrap = false)
- result = false
- load_dependency(file) { result = super }
- result
- end
+ def load(file, wrap = false)
+ result = false
+ load_dependency(file) { result = super }
+ result
+ end
- def require(file)
- result = false
- load_dependency(file) { result = super }
- result
- end
+ def require(file)
+ result = false
+ load_dependency(file) { result = super }
+ result
+ end
end
# Exception file-blaming.
@@ -347,8 +347,7 @@ module ActiveSupport #:nodoc:
end
def clear
- log_call
- Dependencies.load_interlock do
+ Dependencies.unload_interlock do
loaded.clear
loading.clear
remove_unloadable_constants!
@@ -356,8 +355,7 @@ module ActiveSupport #:nodoc:
end
def require_or_load(file_name, const_path = nil)
- log_call file_name, const_path
- file_name = $` if file_name =~ /\.rb\z/
+ file_name = file_name.chomp(".rb")
expanded = File.expand_path(file_name)
return if loaded.include?(expanded)
@@ -372,20 +370,17 @@ module ActiveSupport #:nodoc:
begin
if load?
- log "loading #{file_name}"
-
# Enable warnings if this file has not been loaded before and
# warnings_on_first_load is set.
load_args = ["#{file_name}.rb"]
load_args << const_path unless const_path.nil?
- if !warnings_on_first_load or history.include?(expanded)
+ if !warnings_on_first_load || history.include?(expanded)
result = load_file(*load_args)
else
enable_warnings { result = load_file(*load_args) }
end
else
- log "requiring #{file_name}"
result = require file_name
end
rescue Exception
@@ -410,19 +405,19 @@ module ActiveSupport #:nodoc:
# constant paths which would cause Dependencies to attempt to load this
# file.
def loadable_constants_for_path(path, bases = autoload_paths)
- path = $` if path =~ /\.rb\z/
+ path = path.chomp(".rb")
expanded_path = File.expand_path(path)
paths = []
bases.each do |root|
expanded_root = File.expand_path(root)
- next unless %r{\A#{Regexp.escape(expanded_root)}(/|\\)} =~ expanded_path
+ next unless expanded_path.start_with?(expanded_root)
- nesting = expanded_path[(expanded_root.size)..-1]
- nesting = nesting[1..-1] if nesting && nesting[0] == ?/
- next if nesting.blank?
+ root_size = expanded_root.size
+ next if expanded_path[root_size] != ?/
- paths << nesting.camelize
+ nesting = expanded_path[(root_size + 1)..-1]
+ paths << nesting.camelize unless nesting.blank?
end
paths.uniq!
@@ -431,7 +426,7 @@ module ActiveSupport #:nodoc:
# Search for a file in autoload_paths matching the provided suffix.
def search_for_file(path_suffix)
- path_suffix = path_suffix.sub(/(\.rb)?$/, ".rb")
+ path_suffix += ".rb" unless path_suffix.ends_with?(".rb")
autoload_paths.each do |root|
path = File.join(root, path_suffix)
@@ -465,7 +460,9 @@ module ActiveSupport #:nodoc:
return nil unless base_path = autoloadable_module?(path_suffix)
mod = Module.new
into.const_set const_name, mod
+ log("constant #{qualified_name} autoloaded (module autovivified from #{File.join(base_path, path_suffix)})")
autoloaded_constants << qualified_name unless autoload_once_paths.include?(base_path)
+ autoloaded_constants.uniq!
mod
end
@@ -478,7 +475,6 @@ module ActiveSupport #:nodoc:
# set of names that the file at +path+ may define. See
# +loadable_constants_for_path+ for more details.
def load_file(path, const_paths = loadable_constants_for_path(path))
- log_call path, const_paths
const_paths = [const_paths].compact unless const_paths.is_a? Array
parent_paths = const_paths.collect { |const_path| const_path[/.*(?=::)/] || ::Object }
@@ -489,7 +485,6 @@ module ActiveSupport #:nodoc:
autoloaded_constants.concat newly_defined_paths unless load_once_path?(path)
autoloaded_constants.uniq!
- log "loading #{path} defined #{newly_defined_paths * ', '}" unless newly_defined_paths.empty?
result
end
@@ -503,32 +498,35 @@ module ActiveSupport #:nodoc:
# it is not possible to load the constant into from_mod, try its parent
# module using +const_missing+.
def load_missing_constant(from_mod, const_name)
- log_call from_mod, const_name
-
unless qualified_const_defined?(from_mod.name) && Inflector.constantize(from_mod.name).equal?(from_mod)
raise ArgumentError, "A copy of #{from_mod} has been removed from the module tree but is still active!"
end
- qualified_name = qualified_name_for from_mod, const_name
+ qualified_name = qualified_name_for(from_mod, const_name)
path_suffix = qualified_name.underscore
file_path = search_for_file(path_suffix)
if file_path
expanded = File.expand_path(file_path)
- expanded.sub!(/\.rb\z/, '')
+ expanded.sub!(/\.rb\z/, "")
if loading.include?(expanded)
raise "Circular dependency detected while autoloading constant #{qualified_name}"
else
require_or_load(expanded, qualified_name)
- raise LoadError, "Unable to autoload constant #{qualified_name}, expected #{file_path} to define it" unless from_mod.const_defined?(const_name, false)
- return from_mod.const_get(const_name)
+
+ if from_mod.const_defined?(const_name, false)
+ log("constant #{qualified_name} autoloaded from #{expanded}.rb")
+ return from_mod.const_get(const_name)
+ else
+ raise LoadError, "Unable to autoload constant #{qualified_name}, expected #{file_path} to define it"
+ end
end
elsif mod = autoload_module!(from_mod, const_name, qualified_name, path_suffix)
return mod
- elsif (parent = from_mod.parent) && parent != from_mod &&
- ! from_mod.parents.any? { |p| p.const_defined?(const_name, false) }
+ elsif (parent = from_mod.module_parent) && parent != from_mod &&
+ ! from_mod.module_parents.any? { |p| p.const_defined?(const_name, false) }
# If our parents do not have a constant named +const_name+ then we are free
# to attempt to load upwards. If they do have such a constant, then this
# const_missing must be due to from_mod::const_name, which should not
@@ -560,7 +558,7 @@ module ActiveSupport #:nodoc:
end
name_error = NameError.new("uninitialized constant #{qualified_name}", const_name)
- name_error.set_backtrace(caller.reject {|l| l.starts_with? __FILE__ })
+ name_error.set_backtrace(caller.reject { |l| l.starts_with? __FILE__ })
raise name_error
end
@@ -572,6 +570,7 @@ module ActiveSupport #:nodoc:
# as the environment will be in an inconsistent state, e.g. other constants
# may have already been unloaded and not accessible.
def remove_unloadable_constants!
+ log("removing unloadable constants")
autoloaded_constants.each { |const| remove_constant const }
autoloaded_constants.clear
Reference.clear!
@@ -580,7 +579,7 @@ module ActiveSupport #:nodoc:
class ClassCache
def initialize
- @store = ThreadSafe::Cache.new
+ @store = Concurrent::Map.new
end
def empty?
@@ -604,7 +603,7 @@ module ActiveSupport #:nodoc:
def store(klass)
return self unless klass.respond_to?(:name)
- raise(ArgumentError, 'anonymous classes cannot be cached') if klass.name.empty?
+ raise(ArgumentError, "anonymous classes cannot be cached") if klass.name.empty?
@store[klass.name] = klass
self
end
@@ -638,7 +637,7 @@ module ActiveSupport #:nodoc:
return false if desc.is_a?(Module) && desc.anonymous?
name = to_constant_name desc
return false unless qualified_const_defined?(name)
- return autoloaded_constants.include?(name)
+ autoloaded_constants.include?(name)
end
# Will the provided constant descriptor be unloaded?
@@ -668,54 +667,49 @@ module ActiveSupport #:nodoc:
# exception, any new constants are regarded as being only partially defined
# and will be removed immediately.
def new_constants_in(*descs)
- log_call(*descs)
-
constant_watch_stack.watch_namespaces(descs)
- aborting = true
+ success = false
begin
yield # Now yield to the code that is to define new constants.
- aborting = false
+ success = true
ensure
new_constants = constant_watch_stack.new_constants
- log "New constants: #{new_constants * ', '}"
- return new_constants unless aborting
+ return new_constants if success
- log "Error during loading, removing partially loaded constants "
- new_constants.each { |c| remove_constant(c) }.clear
+ # Remove partially loaded constants.
+ new_constants.each { |c| remove_constant(c) }
end
-
- []
end
# Convert the provided const desc to a qualified constant name (as a string).
# A module, class, symbol, or string may be provided.
def to_constant_name(desc) #:nodoc:
case desc
- when String then desc.sub(/^::/, '')
- when Symbol then desc.to_s
- when Module
- desc.name ||
- raise(ArgumentError, "Anonymous modules have no name to be referenced by")
- else raise TypeError, "Not a valid constant descriptor: #{desc.inspect}"
+ when String then desc.sub(/^::/, "")
+ when Symbol then desc.to_s
+ when Module
+ desc.name ||
+ raise(ArgumentError, "Anonymous modules have no name to be referenced by")
+ else raise TypeError, "Not a valid constant descriptor: #{desc.inspect}"
end
end
def remove_constant(const) #:nodoc:
# Normalize ::Foo, ::Object::Foo, Object::Foo, Object::Object::Foo, etc. as Foo.
- normalized = const.to_s.sub(/\A::/, '')
- normalized.sub!(/\A(Object::)+/, '')
+ normalized = const.to_s.sub(/\A::/, "")
+ normalized.sub!(/\A(Object::)+/, "")
- constants = normalized.split('::')
+ constants = normalized.split("::")
to_remove = constants.pop
# Remove the file path from the loaded list.
file_path = search_for_file(const.underscore)
if file_path
expanded = File.expand_path(file_path)
- expanded.sub!(/\.rb\z/, '')
- self.loaded.delete(expanded)
+ expanded.sub!(/\.rb\z/, "")
+ loaded.delete(expanded)
end
if constants.empty?
@@ -728,13 +722,11 @@ module ActiveSupport #:nodoc:
# here than require the caller to be clever. We check the parent
# rather than the very const argument because we do not want to
# trigger Kernel#autoloads, see the comment below.
- parent_name = constants.join('::')
+ parent_name = constants.join("::")
return unless qualified_const_defined?(parent_name)
parent = constantize(parent_name)
end
- log "removing constant #{const}"
-
# In an autoloaded user.rb like this
#
# autoload :Foo, 'foo'
@@ -755,7 +747,7 @@ module ActiveSupport #:nodoc:
begin
constantized = parent.const_get(to_remove, false)
rescue NameError
- log "the constant #{const} is not reachable anymore, skipping"
+ # The constant is no longer reachable, just skip it.
return
else
constantized.before_remove_const if constantized.respond_to?(:before_remove_const)
@@ -765,27 +757,13 @@ module ActiveSupport #:nodoc:
begin
parent.instance_eval { remove_const to_remove }
rescue NameError
- log "the constant #{const} is not reachable anymore, skipping"
+ # The constant is no longer reachable, just skip it.
end
end
- protected
- def log_call(*args)
- if log_activity?
- arg_str = args.collect(&:inspect) * ', '
- /in `([a-z_\?\!]+)'/ =~ caller(1).first
- selector = $1 || '<unknown>'
- log "called #{selector}(#{arg_str})"
- end
- end
-
- def log(msg)
- logger.debug "Dependencies: #{msg}" if log_activity?
- end
-
- def log_activity?
- logger && log_activity
- end
+ def log(message)
+ logger.debug("autoloading: #{message}") if logger && verbose
+ end
end
end
diff --git a/activesupport/lib/active_support/dependencies/autoload.rb b/activesupport/lib/active_support/dependencies/autoload.rb
index 13036d521d..1cee85d98f 100644
--- a/activesupport/lib/active_support/dependencies/autoload.rb
+++ b/activesupport/lib/active_support/dependencies/autoload.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "active_support/inflector/methods"
module ActiveSupport
diff --git a/activesupport/lib/active_support/dependencies/interlock.rb b/activesupport/lib/active_support/dependencies/interlock.rb
index 148212c951..948be75638 100644
--- a/activesupport/lib/active_support/dependencies/interlock.rb
+++ b/activesupport/lib/active_support/dependencies/interlock.rb
@@ -1,28 +1,34 @@
-require 'active_support/concurrency/share_lock'
+# frozen_string_literal: true
+
+require "active_support/concurrency/share_lock"
module ActiveSupport #:nodoc:
module Dependencies #:nodoc:
class Interlock
def initialize # :nodoc:
- @lock = ActiveSupport::Concurrency::ShareLock.new(true)
+ @lock = ActiveSupport::Concurrency::ShareLock.new
end
def loading
- @lock.exclusive do
+ @lock.exclusive(purpose: :load, compatible: [:load], after_compatible: [:load]) do
yield
end
end
- # Attempt to obtain a "loading" (exclusive) lock. If possible,
- # execute the supplied block while holding the lock. If there is
- # concurrent activity, return immediately (without executing the
- # block) instead of waiting.
- def attempt_loading
- @lock.exclusive(true) do
+ def unloading
+ @lock.exclusive(purpose: :unload, compatible: [:load, :unload], after_compatible: [:load, :unload]) do
yield
end
end
+ def start_unloading
+ @lock.start_exclusive(purpose: :unload, compatible: [:load, :unload])
+ end
+
+ def done_unloading
+ @lock.stop_exclusive(compatible: [:load, :unload])
+ end
+
def start_running
@lock.start_sharing
end
@@ -36,6 +42,16 @@ module ActiveSupport #:nodoc:
yield
end
end
+
+ def permit_concurrent_loads
+ @lock.yield_shares(compatible: [:load]) do
+ yield
+ end
+ end
+
+ def raw_state(&block) # :nodoc:
+ @lock.raw_state(&block)
+ end
end
end
end
diff --git a/activesupport/lib/active_support/deprecation.rb b/activesupport/lib/active_support/deprecation.rb
index 46e9996d59..7271ab565b 100644
--- a/activesupport/lib/active_support/deprecation.rb
+++ b/activesupport/lib/active_support/deprecation.rb
@@ -1,4 +1,6 @@
-require 'singleton'
+# frozen_string_literal: true
+
+require "singleton"
module ActiveSupport
# \Deprecation specifies the API used by Rails to deprecate methods, instance
@@ -12,12 +14,13 @@ module ActiveSupport
# a circular require warning for active_support/deprecation.rb.
#
# So, we define the constant first, and load dependencies later.
- require 'active_support/deprecation/instance_delegator'
- require 'active_support/deprecation/behaviors'
- require 'active_support/deprecation/reporting'
- require 'active_support/deprecation/method_wrappers'
- require 'active_support/deprecation/proxy_wrappers'
- require 'active_support/core_ext/module/deprecation'
+ require "active_support/deprecation/instance_delegator"
+ require "active_support/deprecation/behaviors"
+ require "active_support/deprecation/reporting"
+ require "active_support/deprecation/constant_accessor"
+ require "active_support/deprecation/method_wrappers"
+ require "active_support/deprecation/proxy_wrappers"
+ require "active_support/core_ext/module/deprecation"
include Singleton
include InstanceDelegator
@@ -29,10 +32,10 @@ module ActiveSupport
attr_accessor :deprecation_horizon
# It accepts two parameters on initialization. The first is a version of library
- # and the second is a library name
+ # and the second is a library name.
#
# ActiveSupport::Deprecation.new('2.0', 'MyLibrary')
- def initialize(deprecation_horizon = '5.0', gem_name = 'Rails')
+ def initialize(deprecation_horizon = "6.1", gem_name = "Rails")
self.gem_name = gem_name
self.deprecation_horizon = deprecation_horizon
# By default, warnings are not silenced and debugging is off.
diff --git a/activesupport/lib/active_support/deprecation/behaviors.rb b/activesupport/lib/active_support/deprecation/behaviors.rb
index 28d2d78643..725667d139 100644
--- a/activesupport/lib/active_support/deprecation/behaviors.rb
+++ b/activesupport/lib/active_support/deprecation/behaviors.rb
@@ -1,41 +1,49 @@
+# frozen_string_literal: true
+
require "active_support/notifications"
module ActiveSupport
+ # Raised when <tt>ActiveSupport::Deprecation::Behavior#behavior</tt> is set with <tt>:raise</tt>.
+ # You would set <tt>:raise</tt>, as a behavior to raise errors and proactively report exceptions from deprecations.
class DeprecationException < StandardError
end
class Deprecation
# Default warning behaviors per Rails.env.
DEFAULT_BEHAVIORS = {
- raise: ->(message, callstack) {
+ raise: ->(message, callstack, deprecation_horizon, gem_name) {
e = DeprecationException.new(message)
- e.set_backtrace(callstack)
+ e.set_backtrace(callstack.map(&:to_s))
raise e
},
- stderr: ->(message, callstack) {
+ stderr: ->(message, callstack, deprecation_horizon, gem_name) {
$stderr.puts(message)
$stderr.puts callstack.join("\n ") if debug
},
- log: ->(message, callstack) {
+ log: ->(message, callstack, deprecation_horizon, gem_name) {
logger =
if defined?(Rails.logger) && Rails.logger
Rails.logger
else
- require 'active_support/logger'
+ require "active_support/logger"
ActiveSupport::Logger.new($stderr)
end
logger.warn message
logger.debug callstack.join("\n ") if debug
},
- notify: ->(message, callstack) {
- ActiveSupport::Notifications.instrument("deprecation.rails",
- :message => message, :callstack => callstack)
+ notify: ->(message, callstack, deprecation_horizon, gem_name) {
+ notification_name = "deprecation.#{gem_name.underscore.tr('/', '_')}"
+ ActiveSupport::Notifications.instrument(notification_name,
+ message: message,
+ callstack: callstack,
+ gem_name: gem_name,
+ deprecation_horizon: deprecation_horizon)
},
- silence: ->(message, callstack) {},
+ silence: ->(message, callstack, deprecation_horizon, gem_name) { },
}
# Behavior module allows to determine how to display deprecation messages.
@@ -77,12 +85,25 @@ module ActiveSupport
# ActiveSupport::Deprecation.behavior = :stderr
# ActiveSupport::Deprecation.behavior = [:stderr, :log]
# ActiveSupport::Deprecation.behavior = MyCustomHandler
- # ActiveSupport::Deprecation.behavior = ->(message, callstack) {
+ # ActiveSupport::Deprecation.behavior = ->(message, callstack, deprecation_horizon, gem_name) {
# # custom stuff
# }
def behavior=(behavior)
- @behavior = Array(behavior).map { |b| DEFAULT_BEHAVIORS[b] || b }
+ @behavior = Array(behavior).map { |b| DEFAULT_BEHAVIORS[b] || arity_coerce(b) }
end
+
+ private
+ def arity_coerce(behavior)
+ unless behavior.respond_to?(:call)
+ raise ArgumentError, "#{behavior.inspect} is not a valid deprecation behavior."
+ end
+
+ if behavior.arity == 4 || behavior.arity == -1
+ behavior
+ else
+ -> message, callstack, _, _ { behavior.call(message, callstack) }
+ end
+ end
end
end
end
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..1ed0015812
--- /dev/null
+++ b/activesupport/lib/active_support/deprecation/constant_accessor.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+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)
+ require "active_support/inflector/methods"
+
+ 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/instance_delegator.rb b/activesupport/lib/active_support/deprecation/instance_delegator.rb
index 8472a58add..8beda373a2 100644
--- a/activesupport/lib/active_support/deprecation/instance_delegator.rb
+++ b/activesupport/lib/active_support/deprecation/instance_delegator.rb
@@ -1,11 +1,14 @@
-require 'active_support/core_ext/kernel/singleton_class'
-require 'active_support/core_ext/module/delegation'
+# frozen_string_literal: true
+
+require "active_support/core_ext/kernel/singleton_class"
+require "active_support/core_ext/module/delegation"
module ActiveSupport
class Deprecation
module InstanceDelegator # :nodoc:
def self.included(base)
base.extend(ClassMethods)
+ base.singleton_class.prepend(OverrideDelegators)
base.public_class_method :new
end
@@ -19,6 +22,18 @@ module ActiveSupport
singleton_class.delegate(method_name, to: :instance)
end
end
+
+ module OverrideDelegators # :nodoc:
+ def warn(message = nil, callstack = nil)
+ callstack ||= caller_locations(2)
+ super
+ end
+
+ def deprecation_warning(deprecated_method_name, message = nil, caller_backtrace = nil)
+ caller_backtrace ||= caller_locations(2)
+ super
+ end
+ 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 c74e9c40ac..81482092fe 100644
--- a/activesupport/lib/active_support/deprecation/method_wrappers.rb
+++ b/activesupport/lib/active_support/deprecation/method_wrappers.rb
@@ -1,44 +1,77 @@
-require 'active_support/core_ext/module/aliasing'
-require 'active_support/core_ext/array/extract_options'
+# frozen_string_literal: true
+
+require "active_support/core_ext/array/extract_options"
module ActiveSupport
class Deprecation
module MethodWrapper
# Declare that a method has been deprecated.
#
- # module Fred
- # extend self
- #
- # def foo; end
- # def bar; end
- # def baz; end
+ # class Fred
+ # def aaa; end
+ # def bbb; end
+ # def ccc; end
+ # def ddd; end
+ # def eee; end
# end
#
- # ActiveSupport::Deprecation.deprecate_methods(Fred, :foo, bar: :qux, baz: 'use Bar#baz instead')
- # # => [:foo, :bar, :baz]
+ # Using the default deprecator:
+ # ActiveSupport::Deprecation.deprecate_methods(Fred, :aaa, bbb: :zzz, ccc: 'use Bar#ccc instead')
+ # # => Fred
+ #
+ # Fred.new.aaa
+ # # DEPRECATION WARNING: aaa is deprecated and will be removed from Rails 5.1. (called from irb_binding at (irb):10)
+ # # => nil
+ #
+ # Fred.new.bbb
+ # # DEPRECATION WARNING: bbb is deprecated and will be removed from Rails 5.1 (use zzz instead). (called from irb_binding at (irb):11)
+ # # => nil
+ #
+ # Fred.new.ccc
+ # # DEPRECATION WARNING: ccc is deprecated and will be removed from Rails 5.1 (use Bar#ccc instead). (called from irb_binding at (irb):12)
+ # # => nil
#
- # Fred.foo
- # # => "DEPRECATION WARNING: foo is deprecated and will be removed from Rails 4.1."
+ # Passing in a custom deprecator:
+ # custom_deprecator = ActiveSupport::Deprecation.new('next-release', 'MyGem')
+ # ActiveSupport::Deprecation.deprecate_methods(Fred, ddd: :zzz, deprecator: custom_deprecator)
+ # # => [:ddd]
#
- # Fred.bar
- # # => "DEPRECATION WARNING: bar is deprecated and will be removed from Rails 4.1 (use qux instead)."
+ # Fred.new.ddd
+ # DEPRECATION WARNING: ddd is deprecated and will be removed from MyGem next-release (use zzz instead). (called from irb_binding at (irb):15)
+ # # => nil
#
- # Fred.baz
- # # => "DEPRECATION WARNING: baz is deprecated and will be removed from Rails 4.1 (use Bar#baz instead)."
+ # Using a custom deprecator directly:
+ # custom_deprecator = ActiveSupport::Deprecation.new('next-release', 'MyGem')
+ # custom_deprecator.deprecate_methods(Fred, eee: :zzz)
+ # # => [:eee]
+ #
+ # Fred.new.eee
+ # DEPRECATION WARNING: eee is deprecated and will be removed from MyGem next-release (use zzz instead). (called from irb_binding at (irb):18)
+ # # => nil
def deprecate_methods(target_module, *method_names)
options = method_names.extract_options!
- deprecator = options.delete(:deprecator) || ActiveSupport::Deprecation.instance
+ deprecator = options.delete(:deprecator) || self
method_names += options.keys
method_names.each do |method_name|
- mod = Module.new do
- define_method(method_name) do |*args, &block|
- deprecator.deprecation_warning(method_name, options[method_name])
- super(*args, &block)
- end
+ aliased_method, punctuation = method_name.to_s.sub(/([?!=])$/, ""), $1
+ with_method = "#{aliased_method}_with_deprecation#{punctuation}"
+ without_method = "#{aliased_method}_without_deprecation#{punctuation}"
+
+ target_module.send(:define_method, with_method) do |*args, &block|
+ deprecator.deprecation_warning(method_name, options[method_name])
+ send(without_method, *args, &block)
end
- target_module.prepend(mod)
+ target_module.send(:alias_method, without_method, method_name)
+ target_module.send(:alias_method, method_name, with_method)
+
+ case
+ when target_module.protected_method_defined?(without_method)
+ target_module.send(:protected, method_name)
+ when target_module.private_method_defined?(without_method)
+ target_module.send(:private, method_name)
+ end
end
end
end
diff --git a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb
index 9b264cbb79..56f1e23136 100644
--- a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb
+++ b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb
@@ -1,4 +1,4 @@
-require 'active_support/inflector/methods'
+# frozen_string_literal: true
module ActiveSupport
class Deprecation
@@ -10,7 +10,7 @@ module ActiveSupport
super
end
- instance_methods.each { |m| undef_method m unless m =~ /^__|^object_id$/ }
+ instance_methods.each { |m| undef_method m unless /^__|^object_id$/.match?(m) }
# Don't give a deprecation warning on inspect since test/unit and error
# logs rely on it for diagnostics.
@@ -20,7 +20,7 @@ module ActiveSupport
private
def method_missing(called, *args, &block)
- warn caller, called, args
+ warn caller_locations, called, args
target.__send__(called, *args, &block)
end
end
@@ -80,7 +80,7 @@ module ActiveSupport
# example.old_request.to_s
# # => DEPRECATION WARNING: @request is deprecated! Call request.to_s instead of
# @request.to_s
- # (Bactrace information…)
+ # (Backtrace information…)
# "special_request"
#
# example.request.to_s
@@ -111,20 +111,23 @@ module ActiveSupport
#
# PLANETS = %w(mercury venus earth mars jupiter saturn uranus neptune pluto)
#
- # (In a later update, the orignal implementation of `PLANETS` has been removed.)
+ # # (In a later update, the original implementation of `PLANETS` has been removed.)
#
# PLANETS_POST_2006 = %w(mercury venus earth mars jupiter saturn uranus neptune)
# PLANETS = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('PLANETS', 'PLANETS_POST_2006')
#
# PLANETS.map { |planet| planet.capitalize }
# # => DEPRECATION WARNING: PLANETS is deprecated! Use PLANETS_POST_2006 instead.
- # (Bactrace information…)
+ # (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.")
+ require "active_support/inflector/methods"
+
@old_const = old_const
@new_const = new_const
@deprecator = deprecator
+ @message = message
end
# Returns the class of the new constant.
@@ -142,7 +145,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 a7d265d732..7075b5b869 100644
--- a/activesupport/lib/active_support/deprecation/reporting.rb
+++ b/activesupport/lib/active_support/deprecation/reporting.rb
@@ -1,3 +1,7 @@
+# frozen_string_literal: true
+
+require "rbconfig"
+
module ActiveSupport
class Deprecation
module Reporting
@@ -14,9 +18,9 @@ module ActiveSupport
def warn(message = nil, callstack = nil)
return if silenced
- callstack ||= caller(2)
+ callstack ||= caller_locations(2)
deprecation_message(callstack, message).tap do |m|
- behavior.each { |b| b.call(m, callstack) }
+ behavior.each { |b| b.call(m, callstack, deprecation_horizon, gem_name) }
end
end
@@ -37,7 +41,7 @@ module ActiveSupport
end
def deprecation_warning(deprecated_method_name, message = nil, caller_backtrace = nil)
- caller_backtrace ||= caller(2)
+ caller_backtrace ||= caller_locations(2)
deprecated_method_warning(deprecated_method_name, message).tap do |msg|
warn(msg, caller_backtrace)
end
@@ -46,24 +50,23 @@ 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}"
case message
- when Symbol then "#{warning} (use #{message} instead)"
- when String then "#{warning} (#{message})"
- else warning
+ when Symbol then "#{warning} (use #{message} instead)"
+ when String then "#{warning} (#{message})"
+ else warning
end
end
def deprecation_message(callstack, message = nil)
message ||= "You are using deprecated behavior which will be removed from the next major or minor release."
- message += '.' unless message =~ /\.$/
"DEPRECATION WARNING: #{message} #{deprecation_caller_message(callstack)}"
end
@@ -79,8 +82,19 @@ module ActiveSupport
end
def extract_callstack(callstack)
- rails_gem_root = File.expand_path("../../../../..", __FILE__) + "/"
- offending_line = callstack.find { |line| !line.start_with?(rails_gem_root) } || callstack.first
+ return _extract_callstack(callstack) if callstack.first.is_a? String
+
+ offending_line = callstack.find { |frame|
+ frame.absolute_path && !ignored_callstack(frame.absolute_path)
+ } || callstack.first
+
+ [offending_line.path, offending_line.lineno, offending_line.label]
+ end
+
+ def _extract_callstack(callstack)
+ warn "Please pass `caller_locations` to the deprecation API" if $VERBOSE
+ offending_line = callstack.find { |line| !ignored_callstack(line) } || callstack.first
+
if offending_line
if md = offending_line.match(/^(.+?):(\d+)(?::in `(.*?)')?/)
md.captures
@@ -89,6 +103,12 @@ module ActiveSupport
end
end
end
+
+ RAILS_GEM_ROOT = File.expand_path("../../../..", __dir__) + "/"
+
+ def ignored_callstack(path)
+ path.start_with?(RAILS_GEM_ROOT) || path.start_with?(RbConfig::CONFIG["rubylibdir"])
+ end
end
end
end
diff --git a/activesupport/lib/active_support/descendants_tracker.rb b/activesupport/lib/active_support/descendants_tracker.rb
index 27861e01d0..a4cee788b6 100644
--- a/activesupport/lib/active_support/descendants_tracker.rb
+++ b/activesupport/lib/active_support/descendants_tracker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveSupport
# This module provides an internal implementation to track descendants
# which is faster than iterating through ObjectSpace.
diff --git a/activesupport/lib/active_support/digest.rb b/activesupport/lib/active_support/digest.rb
new file mode 100644
index 0000000000..fba10fbdcf
--- /dev/null
+++ b/activesupport/lib/active_support/digest.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module ActiveSupport
+ class Digest #:nodoc:
+ class <<self
+ def hash_digest_class
+ @hash_digest_class ||= ::Digest::MD5
+ end
+
+ def hash_digest_class=(klass)
+ raise ArgumentError, "#{klass} is expected to implement hexdigest class method" unless klass.respond_to?(:hexdigest)
+ @hash_digest_class = klass
+ end
+
+ def hexdigest(arg)
+ hash_digest_class.hexdigest(arg)[0...32]
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb
index c63b61e97a..88897f811e 100644
--- a/activesupport/lib/active_support/duration.rb
+++ b/activesupport/lib/active_support/duration.rb
@@ -1,5 +1,10 @@
-require 'active_support/core_ext/array/conversions'
-require 'active_support/core_ext/object/acts_like'
+# frozen_string_literal: true
+
+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,19 +12,237 @@ module ActiveSupport
#
# 1.month.ago # equivalent to Time.now.advance(months: -1)
class Duration
+ 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)
+ if Duration === other
+ seconds = value + other.parts[:seconds]
+ new_parts = other.parts.merge(seconds: seconds)
+ new_value = value + other.value
+
+ Duration.new(new_value, new_parts)
+ else
+ calculate(:+, other)
+ end
+ end
+
+ def -(other)
+ if Duration === other
+ seconds = value - other.parts[:seconds]
+ new_parts = other.parts.map { |part, other_value| [part, -other_value] }.to_h
+ new_parts = new_parts.merge(seconds: seconds)
+ new_value = value - other.value
+
+ Duration.new(new_value, new_parts)
+ else
+ calculate(:-, other)
+ end
+ end
+
+ def *(other)
+ if Duration === other
+ new_parts = other.parts.map { |part, other_value| [part, value * other_value] }.to_h
+ new_value = value * other.value
+
+ Duration.new(new_value, new_parts)
+ else
+ calculate(:*, other)
+ end
+ end
+
+ def /(other)
+ if Duration === other
+ value / other.value
+ else
+ calculate(:/, other)
+ end
+ end
+
+ def %(other)
+ if Duration === other
+ Duration.build(value % other.value)
+ else
+ calculate(:%, other)
+ end
+ end
+
+ private
+ def calculate(op, other)
+ if Scalar === other
+ Scalar.new(value.public_send(op, other.value))
+ 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
+
+ PARTS = [:years, :months, :weeks, :days, :hours, :minutes, :seconds].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}[https://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
+
+ # Creates a new Duration from a seconds value that is converted
+ # to the individual parts:
+ #
+ # ActiveSupport::Duration.build(31556952).parts # => {:years=>1}
+ # ActiveSupport::Duration.build(2716146).parts # => {:months=>1, :days=>1}
+ #
+ def build(value)
+ parts = {}
+ remainder = value.to_f
+
+ PARTS.each do |part|
+ unless part == :seconds
+ part_in_seconds = PARTS_IN_SECONDS[part]
+ parts[part] = remainder.div(part_in_seconds)
+ remainder = (remainder % part_in_seconds).round(9)
+ end
+ end
+
+ parts[:seconds] = remainder
+
+ new(value, parts)
+ 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
+ @parts.reject! { |k, v| v.zero? }
+ 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
@@ -29,8 +252,44 @@ 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.new(value / other.value, parts.map { |type, number| [type, number / other.value] })
+ elsif Duration === other
+ value / other.value
+ elsif Numeric === other
+ Duration.new(value / other, parts.map { |type, number| [type, number / other] })
+ else
+ raise_type_error(other)
+ end
+ end
+
+ # Returns the modulo of this Duration by another Duration or Numeric.
+ # Numeric values are treated as seconds.
+ def %(other)
+ if Duration === other || Scalar === other
+ Duration.build(value % other.value)
+ elsif Numeric === other
+ Duration.build(value % other)
+ else
+ raise_type_error(other)
+ end
+ end
+
def -@ #:nodoc:
- Duration.new(-value, parts.map { |type,number| [type, -number] })
+ Duration.new(-value, parts.map { |type, number| [type, -number] })
end
def is_a?(klass) #:nodoc:
@@ -67,14 +326,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
@@ -94,18 +353,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.
@@ -113,12 +367,15 @@ module ActiveSupport
sum(-1, time)
end
alias :until :ago
+ alias :before :ago
def inspect #:nodoc:
+ return "0 seconds" if parts.empty?
+
parts.
- reduce(::Hash.new(0)) { |h,(l,r)| h[l] += r; h }.
- sort_by {|unit, _ | [:years, :months, :days, :minutes, :seconds].index(unit)}.
- map {|unit, val| "#{val} #{val == 1 ? unit.to_s.chop : unit.to_s}"}.
+ reduce(::Hash.new(0)) { |h, (l, r)| h[l] += r; h }.
+ sort_by { |unit, _ | PARTS.index(unit) }.
+ map { |unit, val| "#{val} #{val == 1 ? unit.to_s.chop : unit.to_s}" }.
to_sentence(locale: ::I18n.default_locale)
end
@@ -126,19 +383,31 @@ module ActiveSupport
to_i
end
- def respond_to_missing?(method, include_private=false) #:nodoc:
- @value.respond_to?(method, include_private)
+ def init_with(coder) #:nodoc:
+ initialize(coder["value"], coder["parts"])
+ end
+
+ def encode_with(coder) #:nodoc:
+ coder.map = { "value" => @value, "parts" => @parts }
end
- delegate :<=>, to: :value
+ # 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
- protected
+ private
- def sum(sign, time = ::Time.current) #:nodoc:
- parts.inject(time) do |t,(type,number)|
+ 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
t.since(sign * number)
+ elsif type == :minutes
+ t.since(sign * number * 60)
+ elsif type == :hours
+ t.since(sign * number * 3600)
else
t.advance(type => sign * number)
end
@@ -148,10 +417,16 @@ module ActiveSupport
end
end
- private
+ def respond_to_missing?(method, _)
+ value.respond_to?(method)
+ end
+
+ def method_missing(method, *args, &block)
+ value.public_send(method, *args, &block)
+ end
- def method_missing(method, *args, &block) #:nodoc:
- value.send(method, *args, &block)
+ 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_parser.rb b/activesupport/lib/active_support/duration/iso8601_parser.rb
new file mode 100644
index 0000000000..d3233e6111
--- /dev/null
+++ b/activesupport/lib/active_support/duration/iso8601_parser.rb
@@ -0,0 +1,124 @@
+# frozen_string_literal: true
+
+require "strscan"
+
+module ActiveSupport
+ class Duration
+ # Parses a string formatted according to ISO 8601 Duration into the hash.
+ #
+ # See {ISO 8601}[https://en.wikipedia.org/wiki/ISO_8601#Durations] for more information.
+ #
+ # This parser allows negative parts to be present in pattern.
+ class ISO8601Parser # :nodoc:
+ class ParsingError < ::ArgumentError; end
+
+ PERIOD_OR_COMMA = /\.|,/
+ PERIOD = "."
+ COMMA = ","
+
+ SIGN_MARKER = /\A\-|\+|/
+ DATE_MARKER = /P/
+ TIME_MARKER = /T/
+ DATE_COMPONENT = /(\-?\d+(?:[.,]\d+)?)(Y|M|D|W)/
+ TIME_COMPONENT = /(\-?\d+(?:[.,]\d+)?)(H|M|S)/
+
+ DATE_TO_PART = { "Y" => :years, "M" => :months, "W" => :weeks, "D" => :days }
+ TIME_TO_PART = { "H" => :hours, "M" => :minutes, "S" => :seconds }
+
+ DATE_COMPONENTS = [:years, :months, :days]
+ TIME_COMPONENTS = [:hours, :minutes, :seconds]
+
+ attr_reader :parts, :scanner
+ attr_accessor :mode, :sign
+
+ def initialize(string)
+ @scanner = StringScanner.new(string)
+ @parts = {}
+ @mode = :start
+ @sign = 1
+ end
+
+ def parse!
+ while !finished?
+ case mode
+ when :start
+ if scan(SIGN_MARKER)
+ self.sign = (scanner.matched == "-") ? -1 : 1
+ self.mode = :sign
+ else
+ raise_parsing_error
+ end
+
+ when :sign
+ if scan(DATE_MARKER)
+ self.mode = :date
+ else
+ raise_parsing_error
+ end
+
+ when :date
+ if scan(TIME_MARKER)
+ self.mode = :time
+ elsif scan(DATE_COMPONENT)
+ parts[DATE_TO_PART[scanner[2]]] = number * sign
+ else
+ raise_parsing_error
+ end
+
+ when :time
+ if scan(TIME_COMPONENT)
+ parts[TIME_TO_PART[scanner[2]]] = number * sign
+ else
+ raise_parsing_error
+ end
+
+ end
+ end
+
+ validate!
+ parts
+ end
+
+ private
+
+ def finished?
+ scanner.eos?
+ end
+
+ # Parses number which can be a float with either comma or period.
+ def number
+ PERIOD_OR_COMMA.match?(scanner[1]) ? scanner[1].tr(COMMA, PERIOD).to_f : scanner[1].to_i
+ end
+
+ def scan(pattern)
+ scanner.scan(pattern)
+ end
+
+ def raise_parsing_error(reason = nil)
+ raise ParsingError, "Invalid ISO 8601 duration: #{scanner.string.inspect} #{reason}".strip
+ end
+
+ # Checks for various semantic errors as stated in ISO 8601 standard.
+ def validate!
+ raise_parsing_error("is empty duration") if parts.empty?
+
+ # Mixing any of Y, M, D with W is invalid.
+ if parts.key?(:weeks) && (parts.keys & DATE_COMPONENTS).any?
+ raise_parsing_error("mixing weeks with other date parts not allowed")
+ end
+
+ # Specifying an empty T part is invalid.
+ if mode == :time && (parts.keys & TIME_COMPONENTS).empty?
+ raise_parsing_error("time part marker is present but time part is empty")
+ end
+
+ fractions = parts.values.reject(&:zero?).select { |a| (a % 1) != 0 }
+ unless fractions.empty? || (fractions.size == 1 && fractions.last == @parts.values.reject(&:zero?).last)
+ raise_parsing_error "(only last part can be fractional)"
+ end
+
+ true
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/duration/iso8601_serializer.rb b/activesupport/lib/active_support/duration/iso8601_serializer.rb
new file mode 100644
index 0000000000..1125454919
--- /dev/null
+++ b/activesupport/lib/active_support/duration/iso8601_serializer.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/object/blank"
+
+module ActiveSupport
+ class Duration
+ # Serializes duration to string according to ISO 8601 Duration format.
+ class ISO8601Serializer # :nodoc:
+ def initialize(duration, precision: nil)
+ @duration = duration
+ @precision = precision
+ end
+
+ # Builds and returns output string.
+ def serialize
+ parts, sign = normalize
+ return "PT0S" if parts.empty?
+
+ output = +"P"
+ output << "#{parts[:years]}Y" if parts.key?(:years)
+ output << "#{parts[:months]}M" if parts.key?(:months)
+ output << "#{parts[:weeks]}W" if parts.key?(:weeks)
+ output << "#{parts[:days]}D" if parts.key?(:days)
+ time = +""
+ time << "#{parts[:hours]}H" if parts.key?(:hours)
+ time << "#{parts[:minutes]}M" if parts.key?(:minutes)
+ if parts.key?(:seconds)
+ time << "#{sprintf(@precision ? "%0.0#{@precision}f" : '%g', parts[:seconds])}S"
+ end
+ output << "T#{time}" unless time.empty?
+ "#{sign}#{output}"
+ end
+
+ private
+
+ # Return pair of duration's parts and whole duration sign.
+ # Parts are summarized (as they can become repetitive due to addition, etc).
+ # Zero parts are removed as not significant.
+ # If all parts are negative it will negate all of them and return minus as a sign.
+ def normalize
+ parts = @duration.parts.each_with_object(Hash.new(0)) do |(k, v), p|
+ p[k] += v unless v.zero?
+ end
+ # If all parts are negative - let's make a negative duration
+ sign = ""
+ if parts.values.all? { |v| v < 0 }
+ sign = "-"
+ parts.transform_values!(&:-@)
+ end
+ [parts, sign]
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/encrypted_configuration.rb b/activesupport/lib/active_support/encrypted_configuration.rb
new file mode 100644
index 0000000000..cc1d026737
--- /dev/null
+++ b/activesupport/lib/active_support/encrypted_configuration.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require "yaml"
+require "active_support/encrypted_file"
+require "active_support/ordered_options"
+require "active_support/core_ext/object/inclusion"
+require "active_support/core_ext/module/delegation"
+
+module ActiveSupport
+ class EncryptedConfiguration < EncryptedFile
+ delegate :[], :fetch, to: :config
+ delegate_missing_to :options
+
+ def initialize(config_path:, key_path:, env_key:, raise_if_missing_key:)
+ super content_path: config_path, key_path: key_path,
+ env_key: env_key, raise_if_missing_key: raise_if_missing_key
+ end
+
+ # Allow a config to be started without a file present
+ def read
+ super
+ rescue ActiveSupport::EncryptedFile::MissingContentError
+ ""
+ end
+
+ def write(contents)
+ deserialize(contents)
+
+ super
+ end
+
+ def config
+ @config ||= deserialize(read).deep_symbolize_keys
+ end
+
+ private
+ def options
+ @options ||= ActiveSupport::InheritableOptions.new(config)
+ end
+
+ def deserialize(config)
+ YAML.load(config).presence || {}
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/encrypted_file.rb b/activesupport/lib/active_support/encrypted_file.rb
new file mode 100644
index 0000000000..c66f1b557e
--- /dev/null
+++ b/activesupport/lib/active_support/encrypted_file.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+require "pathname"
+require "active_support/message_encryptor"
+
+module ActiveSupport
+ class EncryptedFile
+ class MissingContentError < RuntimeError
+ def initialize(content_path)
+ super "Missing encrypted content file in #{content_path}."
+ end
+ end
+
+ class MissingKeyError < RuntimeError
+ def initialize(key_path:, env_key:)
+ super \
+ "Missing encryption key to decrypt file with. " +
+ "Ask your team for your master key and write it to #{key_path} or put it in the ENV['#{env_key}']."
+ end
+ end
+
+ CIPHER = "aes-128-gcm"
+
+ def self.generate_key
+ SecureRandom.hex(ActiveSupport::MessageEncryptor.key_len(CIPHER))
+ end
+
+
+ attr_reader :content_path, :key_path, :env_key, :raise_if_missing_key
+
+ def initialize(content_path:, key_path:, env_key:, raise_if_missing_key:)
+ @content_path, @key_path = Pathname.new(content_path), Pathname.new(key_path)
+ @env_key, @raise_if_missing_key = env_key, raise_if_missing_key
+ end
+
+ def key
+ read_env_key || read_key_file || handle_missing_key
+ end
+
+ def read
+ if !key.nil? && content_path.exist?
+ decrypt content_path.binread
+ else
+ raise MissingContentError, content_path
+ end
+ end
+
+ def write(contents)
+ IO.binwrite "#{content_path}.tmp", encrypt(contents)
+ FileUtils.mv "#{content_path}.tmp", content_path
+ end
+
+ def change(&block)
+ writing read, &block
+ end
+
+
+ private
+ def writing(contents)
+ tmp_file = "#{Process.pid}.#{content_path.basename.to_s.chomp('.enc')}"
+ tmp_path = Pathname.new File.join(Dir.tmpdir, tmp_file)
+ tmp_path.binwrite contents
+
+ yield tmp_path
+
+ updated_contents = tmp_path.binread
+
+ write(updated_contents) if updated_contents != contents
+ ensure
+ FileUtils.rm(tmp_path) if tmp_path.exist?
+ end
+
+
+ def encrypt(contents)
+ encryptor.encrypt_and_sign contents
+ end
+
+ def decrypt(contents)
+ encryptor.decrypt_and_verify contents
+ end
+
+ def encryptor
+ @encryptor ||= ActiveSupport::MessageEncryptor.new([ key ].pack("H*"), cipher: CIPHER)
+ end
+
+
+ def read_env_key
+ ENV[env_key]
+ end
+
+ def read_key_file
+ key_path.binread.strip if key_path.exist?
+ end
+
+ def handle_missing_key
+ raise MissingKeyError, key_path: key_path, env_key: env_key if raise_if_missing_key
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/evented_file_update_checker.rb b/activesupport/lib/active_support/evented_file_update_checker.rb
new file mode 100644
index 0000000000..97e982eb05
--- /dev/null
+++ b/activesupport/lib/active_support/evented_file_update_checker.rb
@@ -0,0 +1,205 @@
+# frozen_string_literal: true
+
+require "set"
+require "pathname"
+require "concurrent/atomic/atomic_boolean"
+
+module ActiveSupport
+ # Allows you to "listen" to changes in a file system.
+ # The evented file updater does not hit disk when checking for updates
+ # instead it uses platform specific file system events to trigger a change
+ # in state.
+ #
+ # The file checker takes an array of files to watch or a hash specifying directories
+ # and file extensions to watch. It also takes a block that is called when
+ # EventedFileUpdateChecker#execute is run or when EventedFileUpdateChecker#execute_if_updated
+ # is run and there have been changes to the file system.
+ #
+ # Note: Forking will cause the first call to `updated?` to return `true`.
+ #
+ # Example:
+ #
+ # checker = ActiveSupport::EventedFileUpdateChecker.new(["/tmp/foo"]) { puts "changed" }
+ # checker.updated?
+ # # => false
+ # checker.execute_if_updated
+ # # => nil
+ #
+ # FileUtils.touch("/tmp/foo")
+ #
+ # checker.updated?
+ # # => true
+ # checker.execute_if_updated
+ # # => "changed"
+ #
+ 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
+
+ @dirs = {}
+ dirs.each do |dir, exts|
+ @dirs[@ph.xpath(dir)] = Array(exts).map { |ext| @ph.normalize_extension(ext) }
+ end
+
+ @block = block
+ @updated = Concurrent::AtomicBoolean.new(false)
+ @lcsp = @ph.longest_common_subpath(@dirs.keys)
+ @pid = Process.pid
+ @boot_mutex = Mutex.new
+
+ if (@dtw = directories_to_watch).any?
+ # Loading listen triggers warnings. These are originated by a legit
+ # usage of attr_* macros for private attributes, but adds a lot of noise
+ # to our test suite. Thus, we lazy load it and disable warnings locally.
+ silence_warnings do
+ begin
+ require "listen"
+ rescue LoadError => e
+ raise LoadError, "Could not load the 'listen' gem. Add `gem 'listen'` to the development group of your Gemfile", e.backtrace
+ end
+ end
+ end
+ boot!
+ end
+
+ def updated?
+ @boot_mutex.synchronize do
+ if @pid != Process.pid
+ boot!
+ @pid = Process.pid
+ @updated.make_true
+ end
+ end
+ @updated.true?
+ end
+
+ def execute
+ @updated.make_false
+ @block.call
+ end
+
+ def execute_if_updated
+ if updated?
+ yield if block_given?
+ execute
+ true
+ end
+ end
+
+ private
+ def boot!
+ Listen.to(*@dtw, &method(:changed)).start
+ end
+
+ def changed(modified, added, removed)
+ unless updated?
+ @updated.make_true if (modified + added + removed).any? { |f| watching?(f) }
+ end
+ end
+
+ def watching?(file)
+ file = @ph.xpath(file)
+
+ if @files.member?(file)
+ true
+ elsif file.directory?
+ false
+ else
+ ext = @ph.normalize_extension(file.extname)
+
+ file.dirname.ascend do |dir|
+ if @dirs.fetch(dir, []).include?(ext)
+ break true
+ elsif dir == @lcsp || dir.root?
+ break false
+ end
+ end
+ end
+ end
+
+ def directories_to_watch
+ dtw = (@files + @dirs.keys).map { |f| @ph.existing_parent(f) }
+ 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
+
+ class PathHelper
+ def xpath(path)
+ Pathname.new(path).expand_path
+ end
+
+ def normalize_extension(ext)
+ ext.to_s.sub(/\A\./, "")
+ end
+
+ # Given a collection of Pathname objects returns the longest subpath
+ # common to all of them, or +nil+ if there is none.
+ def longest_common_subpath(paths)
+ return if paths.empty?
+
+ lcsp = Pathname.new(paths[0])
+
+ paths[1..-1].each do |path|
+ until ascendant_of?(lcsp, path)
+ if lcsp.root?
+ # If we get here a root directory is not an ascendant of path.
+ # This may happen if there are paths in different drives on
+ # Windows.
+ return
+ else
+ lcsp = lcsp.parent
+ end
+ end
+ end
+
+ lcsp
+ end
+
+ # Returns the deepest existing ascendant, which could be the argument itself.
+ def existing_parent(dir)
+ dir.ascend do |ascendant|
+ break ascendant if ascendant.directory?
+ end
+ end
+
+ # Filters out directories which are descendants of others in the collection (stable).
+ def filter_out_descendants(dirs)
+ return dirs if dirs.length < 2
+
+ dirs_sorted_by_nparts = dirs.sort_by { |dir| dir.each_filename.to_a.length }
+ descendants = []
+
+ until dirs_sorted_by_nparts.empty?
+ dir = dirs_sorted_by_nparts.shift
+
+ dirs_sorted_by_nparts.reject! do |possible_descendant|
+ ascendant_of?(dir, possible_descendant) && descendants << possible_descendant
+ end
+ end
+
+ # Array#- preserves order.
+ dirs - descendants
+ end
+
+ private
+
+ def ascendant_of?(base, other)
+ base != other && other.ascend do |ascendant|
+ break true if base == ascendant
+ end
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/execution_wrapper.rb b/activesupport/lib/active_support/execution_wrapper.rb
new file mode 100644
index 0000000000..ca810db584
--- /dev/null
+++ b/activesupport/lib/active_support/execution_wrapper.rb
@@ -0,0 +1,129 @@
+# frozen_string_literal: true
+
+require "active_support/callbacks"
+require "concurrent/hash"
+
+module ActiveSupport
+ class ExecutionWrapper
+ include ActiveSupport::Callbacks
+
+ Null = Object.new # :nodoc:
+ def Null.complete! # :nodoc:
+ end
+
+ define_callbacks :run
+ define_callbacks :complete
+
+ def self.to_run(*args, &block)
+ set_callback(:run, *args, &block)
+ end
+
+ def self.to_complete(*args, &block)
+ set_callback(:complete, *args, &block)
+ end
+
+ RunHook = Struct.new(:hook) do # :nodoc:
+ def before(target)
+ hook_state = target.send(:hook_state)
+ hook_state[hook] = hook.run
+ end
+ end
+
+ CompleteHook = Struct.new(:hook) do # :nodoc:
+ def before(target)
+ hook_state = target.send(:hook_state)
+ if hook_state.key?(hook)
+ hook.complete hook_state[hook]
+ end
+ end
+ alias after before
+ end
+
+ # Register an object to be invoked during both the +run+ and
+ # +complete+ steps.
+ #
+ # +hook.complete+ will be passed the value returned from +hook.run+,
+ # and will only be invoked if +run+ has previously been called.
+ # (Mostly, this means it won't be invoked if an exception occurs in
+ # a preceding +to_run+ block; all ordinary +to_complete+ blocks are
+ # invoked in that situation.)
+ def self.register_hook(hook, outer: false)
+ if outer
+ to_run RunHook.new(hook), prepend: true
+ to_complete :after, CompleteHook.new(hook)
+ else
+ to_run RunHook.new(hook)
+ to_complete CompleteHook.new(hook)
+ end
+ end
+
+ # Run this execution.
+ #
+ # Returns an instance, whose +complete!+ method *must* be invoked
+ # after the work has been performed.
+ #
+ # Where possible, prefer +wrap+.
+ def self.run!
+ if active?
+ Null
+ else
+ new.tap do |instance|
+ success = nil
+ begin
+ instance.run!
+ success = true
+ ensure
+ instance.complete! unless success
+ end
+ end
+ end
+ end
+
+ # Perform the work in the supplied block as an execution.
+ def self.wrap
+ return yield if active?
+
+ instance = run!
+ begin
+ yield
+ ensure
+ instance.complete!
+ end
+ end
+
+ class << self # :nodoc:
+ attr_accessor :active
+ end
+
+ def self.inherited(other) # :nodoc:
+ super
+ other.active = Concurrent::Hash.new
+ end
+
+ self.active = Concurrent::Hash.new
+
+ def self.active? # :nodoc:
+ @active[Thread.current]
+ end
+
+ def run! # :nodoc:
+ self.class.active[Thread.current] = true
+ run_callbacks(:run)
+ end
+
+ # Complete this in-flight execution. This method *must* be called
+ # exactly once on the result of any call to +run!+.
+ #
+ # Where possible, prefer +wrap+.
+ def complete!
+ run_callbacks(:complete)
+ ensure
+ self.class.active.delete Thread.current
+ end
+
+ private
+ def hook_state
+ @_hook_state ||= {}
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/executor.rb b/activesupport/lib/active_support/executor.rb
new file mode 100644
index 0000000000..ce391b07ec
--- /dev/null
+++ b/activesupport/lib/active_support/executor.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+require "active_support/execution_wrapper"
+
+module ActiveSupport
+ class Executor < ExecutionWrapper
+ end
+end
diff --git a/activesupport/lib/active_support/file_update_checker.rb b/activesupport/lib/active_support/file_update_checker.rb
index 78b627c286..1a0bb10815 100644
--- a/activesupport/lib/active_support/file_update_checker.rb
+++ b/activesupport/lib/active_support/file_update_checker.rb
@@ -1,3 +1,7 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/time/calculations"
+
module ActiveSupport
# FileUpdateChecker specifies the API used by Rails to watch files
# and control reloading. The API depends on four methods:
@@ -23,7 +27,7 @@ module ActiveSupport
# I18n.reload!
# end
#
- # ActionDispatch::Reloader.to_prepare do
+ # ActiveSupport::Reloader.to_prepare do
# i18n_reloader.execute_if_updated
# end
class FileUpdateChecker
@@ -35,7 +39,11 @@ module ActiveSupport
# This method must also receive a block that will be called once a path
# changes. The array of files and list of directories cannot be changed
# after FileUpdateChecker has been initialized.
- def initialize(files, dirs={}, &block)
+ 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
@@ -81,6 +89,7 @@ module ActiveSupport
# Execute the block given if updated.
def execute_if_updated
if updated?
+ yield if block_given?
execute
true
else
@@ -90,48 +99,65 @@ module ActiveSupport
private
- def watched
- @watched || begin
- all = @files.select { |f| File.exist?(f) }
- all.concat(Dir[@glob]) if @glob
- all
+ def watched
+ @watched || begin
+ all = @files.select { |f| File.exist?(f) }
+ all.concat(Dir[@glob]) if @glob
+ all
+ end
end
- end
- def updated_at(paths)
- @updated_at || max_mtime(paths) || Time.at(0)
- end
+ def updated_at(paths)
+ @updated_at || max_mtime(paths) || Time.at(0)
+ end
- # This method returns the maximum mtime of the files in +paths+, or +nil+
- # if the array is empty.
- #
- # Files with a mtime in the future are ignored. Such abnormal situation
- # can happen for example if the user changes the clock by hand. It is
- # healthy to consider this edge case because with mtimes in the future
- # reloading is not triggered.
- def max_mtime(paths)
- time_now = Time.now
- paths.map {|path| File.mtime(path)}.reject {|mtime| time_now < mtime}.max
- end
+ # This method returns the maximum mtime of the files in +paths+, or +nil+
+ # if the array is empty.
+ #
+ # Files with a mtime in the future are ignored. Such abnormal situation
+ # can happen for example if the user changes the clock by hand. It is
+ # healthy to consider this edge case because with mtimes in the future
+ # reloading is not triggered.
+ def max_mtime(paths)
+ time_now = Time.now
+ max_mtime = nil
+
+ # Time comparisons are performed with #compare_without_coercion because
+ # AS redefines these operators in a way that is much slower and does not
+ # bring any benefit in this particular code.
+ #
+ # Read t1.compare_without_coercion(t2) < 0 as t1 < t2.
+ paths.each do |path|
+ mtime = File.mtime(path)
- def compile_glob(hash)
- hash.freeze # Freeze so changes aren't accidentally pushed
- return if hash.empty?
+ next if time_now.compare_without_coercion(mtime) < 0
- globs = hash.map do |key, value|
- "#{escape(key)}/**/*#{compile_ext(value)}"
+ if max_mtime.nil? || max_mtime.compare_without_coercion(mtime) < 0
+ max_mtime = mtime
+ end
+ end
+
+ max_mtime
end
- "{#{globs.join(",")}}"
- end
- def escape(key)
- key.gsub(',','\,')
- end
+ def compile_glob(hash)
+ hash.freeze # Freeze so changes aren't accidentally pushed
+ return if hash.empty?
- def compile_ext(array)
- array = Array(array)
- return if array.empty?
- ".{#{array.join(",")}}"
- end
+ globs = hash.map do |key, value|
+ "#{escape(key)}/**/*#{compile_ext(value)}"
+ end
+ "{#{globs.join(",")}}"
+ end
+
+ def escape(key)
+ key.gsub(",", '\,')
+ end
+
+ def compile_ext(array)
+ array = Array(array)
+ return if array.empty?
+ ".{#{array.join(",")}}"
+ end
end
end
diff --git a/activesupport/lib/active_support/gem_version.rb b/activesupport/lib/active_support/gem_version.rb
index 7068f09d87..c951ad16a3 100644
--- a/activesupport/lib/active_support/gem_version.rb
+++ b/activesupport/lib/active_support/gem_version.rb
@@ -1,11 +1,13 @@
+# frozen_string_literal: true
+
module ActiveSupport
- # Returns the version of the currently loaded Active Support as a <tt>Gem::Version</tt>
+ # Returns the version of the currently loaded Active Support as a <tt>Gem::Version</tt>.
def self.gem_version
Gem::Version.new VERSION::STRING
end
module VERSION
- MAJOR = 5
+ MAJOR = 6
MINOR = 0
TINY = 0
PRE = "alpha"
diff --git a/activesupport/lib/active_support/gzip.rb b/activesupport/lib/active_support/gzip.rb
index b837c879bb..7ffa6d90a2 100644
--- a/activesupport/lib/active_support/gzip.rb
+++ b/activesupport/lib/active_support/gzip.rb
@@ -1,5 +1,7 @@
-require 'zlib'
-require 'stringio'
+# frozen_string_literal: true
+
+require "zlib"
+require "stringio"
module ActiveSupport
# A convenient wrapper for the zlib standard library that allows
@@ -9,7 +11,7 @@ module ActiveSupport
# # => "\x1F\x8B\b\x00o\x8D\xCDO\x00\x03K\xCE\xCF-(J-.V\xC8MU\x04\x00R>n\x83\f\x00\x00\x00"
#
# ActiveSupport::Gzip.decompress(gzip)
- # # => "compress me!"
+ # # => "compress me!"
module Gzip
class Stream < StringIO
def initialize(*)
@@ -21,11 +23,11 @@ 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.
- def self.compress(source, level=Zlib::DEFAULT_COMPRESSION, strategy=Zlib::DEFAULT_STRATEGY)
+ def self.compress(source, level = Zlib::DEFAULT_COMPRESSION, strategy = Zlib::DEFAULT_STRATEGY)
output = Stream.new
gz = Zlib::GzipWriter.new(output, level, strategy)
gz.write(source)
diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb
index 63690a1342..f1af76019a 100644
--- a/activesupport/lib/active_support/hash_with_indifferent_access.rb
+++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb
@@ -1,5 +1,8 @@
-require 'active_support/core_ext/hash/keys'
-require 'active_support/core_ext/hash/reverse_merge'
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/keys"
+require "active_support/core_ext/hash/reverse_merge"
+require "active_support/core_ext/hash/except"
module ActiveSupport
# Implements a hash where keys <tt>:foo</tt> and <tt>"foo"</tt> are considered
@@ -40,6 +43,12 @@ module ActiveSupport
# rgb = { black: '#000000', white: '#FFFFFF' }.with_indifferent_access
#
# which may be handy.
+ #
+ # To access this class outside of Rails, require the core extension with:
+ #
+ # require "active_support/core_ext/hash/indifferent_access"
+ #
+ # which will, in turn, require this file.
class HashWithIndifferentAccess < Hash
# Returns +true+ so that <tt>Array#extract_options!</tt> finds members of
# this class.
@@ -59,24 +68,12 @@ module ActiveSupport
if constructor.respond_to?(:to_hash)
super()
update(constructor)
- else
- super(constructor)
- end
- end
- def default(key = nil)
- if key.is_a?(Symbol) && include?(key = key.to_s)
- self[key]
+ hash = constructor.to_hash
+ self.default = hash.default if hash.default
+ self.default_proc = hash.default_proc if hash.default_proc
else
- super
- end
- end
-
- def self.new_from_hash_copying_default(hash)
- hash = hash.to_hash
- new(hash).tap do |new_hash|
- new_hash.default = hash.default
- new_hash.default_proc = hash.default_proc if hash.default_proc
+ super(constructor)
end
end
@@ -92,7 +89,7 @@ module ActiveSupport
# hash = ActiveSupport::HashWithIndifferentAccess.new
# hash[:key] = 'value'
#
- # This value can be later fetched using either +:key+ or +'key'+.
+ # This value can be later fetched using either +:key+ or <tt>'key'</tt>.
def []=(key, value)
regular_writer(convert_key(key), convert_value(value, for: :assignment))
end
@@ -154,6 +151,19 @@ module ActiveSupport
alias_method :has_key?, :key?
alias_method :member?, :key?
+ # Same as <tt>Hash#[]</tt> where the key passed as argument can be
+ # either a string or a symbol:
+ #
+ # counters = ActiveSupport::HashWithIndifferentAccess.new
+ # counters[:foo] = 1
+ #
+ # counters['foo'] # => 1
+ # counters[:foo] # => 1
+ # counters[:zoo] # => nil
+ def [](key)
+ super(convert_key(key))
+ end
+
# Same as <tt>Hash#fetch</tt> where the key passed as argument can be
# either a string or a symbol:
#
@@ -168,6 +178,34 @@ module ActiveSupport
super(convert_key(key), *extras)
end
+ # Same as <tt>Hash#dig</tt> where the key passed as argument can be
+ # either a string or a symbol:
+ #
+ # counters = ActiveSupport::HashWithIndifferentAccess.new
+ # counters[:foo] = { bar: 1 }
+ #
+ # counters.dig('foo', 'bar') # => 1
+ # counters.dig(:foo, :bar) # => 1
+ # counters.dig(:zoo) # => nil
+ def dig(*args)
+ args[0] = convert_key(args[0]) if args.size > 0
+ super(*args)
+ end
+
+ # Same as <tt>Hash#default</tt> where the key passed as argument can be
+ # either a string or a symbol:
+ #
+ # hash = ActiveSupport::HashWithIndifferentAccess.new(1)
+ # hash.default # => 1
+ #
+ # hash = ActiveSupport::HashWithIndifferentAccess.new { |hash, key| key }
+ # hash.default # => nil
+ # hash.default('foo') # => 'foo'
+ # hash.default(:foo) # => 'foo'
+ def default(*args)
+ super(*args.map { |arg| convert_key(arg) })
+ end
+
# Returns an array of the values at the specified indices:
#
# hash = ActiveSupport::HashWithIndifferentAccess.new
@@ -178,13 +216,26 @@ module ActiveSupport
indices.collect { |key| self[convert_key(key)] }
end
+ # Returns an array of the values at the specified indices, but also
+ # raises an exception when one of the keys can't be found.
+ #
+ # hash = ActiveSupport::HashWithIndifferentAccess.new
+ # hash[:a] = 'x'
+ # hash[:b] = 'y'
+ # hash.fetch_values('a', 'b') # => ["x", "y"]
+ # hash.fetch_values('a', 'c') { |key| 'z' } # => ["x", "z"]
+ # hash.fetch_values('a', 'c') # => KeyError: key not found: "c"
+ def fetch_values(*indices, &block)
+ indices.collect { |key| fetch(key, &block) }
+ end
+
# Returns a shallow copy of the hash.
#
# hash = ActiveSupport::HashWithIndifferentAccess.new({ a: { b: 'b' } })
# dup = hash.dup
# dup[:a][:c] = 'c'
#
- # hash[:a][:c] # => nil
+ # hash[:a][:c] # => "c"
# dup[:a][:c] # => "c"
def dup
self.class.new(self).tap do |new_hash|
@@ -196,7 +247,7 @@ module ActiveSupport
# modify the receiver but rather returns a new hash with indifferent
# access with the result of the merge.
def merge(hash, &block)
- self.dup.update(hash, &block)
+ dup.update(hash, &block)
end
# Like +merge+ but the other way around: Merges the receiver into the
@@ -206,20 +257,22 @@ module ActiveSupport
# hash['a'] = nil
# hash.reverse_merge(a: 0, b: 1) # => {"a"=>nil, "b"=>1}
def reverse_merge(other_hash)
- super(self.class.new_from_hash_copying_default(other_hash))
+ super(self.class.new(other_hash))
end
+ alias_method :with_defaults, :reverse_merge
# Same semantics as +reverse_merge+ but modifies the receiver in-place.
def reverse_merge!(other_hash)
- replace(reverse_merge( other_hash ))
+ super(self.class.new(other_hash))
end
+ alias_method :with_defaults!, :reverse_merge!
# Replaces the contents of this hash with other_hash.
#
# h = { "a" => 100, "b" => 200 }
# h.replace({ "c" => 300, "d" => 400 }) # => {"c"=>300, "d"=>400}
def replace(other_hash)
- super(self.class.new_from_hash_copying_default(other_hash))
+ super(self.class.new(other_hash))
end
# Removes the specified key from the hash.
@@ -227,6 +280,8 @@ module ActiveSupport
super(convert_key(key))
end
+ alias_method :without, :except
+
def stringify_keys!; self end
def deep_stringify_keys!; self end
def stringify_keys; dup end
@@ -234,17 +289,52 @@ module ActiveSupport
undef :symbolize_keys!
undef :deep_symbolize_keys!
def symbolize_keys; to_hash.symbolize_keys! end
+ alias_method :to_options, :symbolize_keys
def deep_symbolize_keys; to_hash.deep_symbolize_keys! end
def to_options!; self end
def select(*args, &block)
+ return to_enum(:select) unless block_given?
dup.tap { |hash| hash.select!(*args, &block) }
end
def reject(*args, &block)
+ return to_enum(:reject) unless block_given?
dup.tap { |hash| hash.reject!(*args, &block) }
end
+ def transform_values(*args, &block)
+ return to_enum(:transform_values) unless block_given?
+ dup.tap { |hash| hash.transform_values!(*args, &block) }
+ end
+
+ def transform_keys(*args, &block)
+ return to_enum(:transform_keys) unless block_given?
+ dup.tap { |hash| hash.transform_keys!(*args, &block) }
+ end
+
+ def transform_keys!
+ return enum_for(:transform_keys!) { size } unless block_given?
+ keys.each do |key|
+ self[yield(key)] = delete(key)
+ end
+ self
+ end
+
+ def slice(*keys)
+ keys.map! { |key| convert_key(key) }
+ self.class.new(super)
+ end
+
+ def slice!(*keys)
+ keys.map! { |key| convert_key(key) }
+ super
+ end
+
+ def compact
+ dup.tap(&:compact!)
+ end
+
# Convert to a regular hash with string keys.
def to_hash
_new_hash = Hash.new
@@ -256,12 +346,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
@@ -278,7 +368,7 @@ module ActiveSupport
end
end
- def set_defaults(target)
+ def set_defaults(target) # :doc:
if default_proc
target.default_proc = default_proc.dup
else
@@ -288,4 +378,6 @@ module ActiveSupport
end
end
+# :stopdoc:
+
HashWithIndifferentAccess = ActiveSupport::HashWithIndifferentAccess
diff --git a/activesupport/lib/active_support/i18n.rb b/activesupport/lib/active_support/i18n.rb
index 6cc98191d4..39dab1cc71 100644
--- a/activesupport/lib/active_support/i18n.rb
+++ b/activesupport/lib/active_support/i18n.rb
@@ -1,13 +1,16 @@
-require 'active_support/core_ext/hash/deep_merge'
-require 'active_support/core_ext/hash/except'
-require 'active_support/core_ext/hash/slice'
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/deep_merge"
+require "active_support/core_ext/hash/except"
+require "active_support/core_ext/hash/slice"
begin
- require 'i18n'
+ require "i18n"
rescue LoadError => e
$stderr.puts "The i18n gem is not available. Please add it to your Gemfile and run bundle install"
raise e
end
-require 'active_support/lazy_load_hooks'
+require "active_support/lazy_load_hooks"
ActiveSupport.run_load_hooks(:i18n)
-I18n.load_path << "#{File.dirname(__FILE__)}/locale/en.yml"
+I18n.load_path << File.expand_path("locale/en.yml", __dir__)
+I18n.load_path << File.expand_path("locale/en.rb", __dir__)
diff --git a/activesupport/lib/active_support/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb
index 95f3f6255a..584930e413 100644
--- a/activesupport/lib/active_support/i18n_railtie.rb
+++ b/activesupport/lib/active_support/i18n_railtie.rb
@@ -1,7 +1,10 @@
+# frozen_string_literal: true
+
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 +24,6 @@ module I18n
I18n::Railtie.initialize_i18n(app)
end
- protected
-
@i18n_inited = false
# Setup i18n configuration.
@@ -37,10 +38,12 @@ module I18n
enforce_available_locales = I18n.enforce_available_locales if enforce_available_locales.nil?
I18n.enforce_available_locales = false
+ reloadable_paths = []
app.config.i18n.each do |setting, value|
case setting
when :railties_load_path
- app.config.i18n.load_path.unshift(*value)
+ reloadable_paths = value
+ app.config.i18n.load_path.unshift(*value.flat_map(&:existent))
when :load_path
I18n.load_path += value
else
@@ -53,14 +56,17 @@ module I18n
# Restore available locales check so it will take place from now on.
I18n.enforce_available_locales = enforce_available_locales
- reloader = ActiveSupport::FileUpdateChecker.new(I18n.load_path.dup){ I18n.reload! }
+ directories = watched_dirs_with_extensions(reloadable_paths)
+ reloader = app.config.file_watcher.new(I18n.load_path.dup, directories) do
+ I18n.load_path.keep_if { |p| File.exist?(p) }
+ I18n.load_path |= reloadable_paths.flat_map(&:existent)
+
+ I18n.reload!
+ end
+
app.reloaders << reloader
- ActionDispatch::Reloader.to_prepare do
- reloader.execute_if_updated
- # TODO: remove the following line as soon as the return value of
- # callbacks is ignored, that is, returning `false` does not
- # display a deprecation warning or halts the callback chain.
- true
+ app.reloader.to_run do
+ reloader.execute_if_updated { require_unload_lock! }
end
reloader.execute
@@ -74,13 +80,26 @@ module I18n
def self.init_fallbacks(fallbacks)
include_fallbacks_module
- args = case fallbacks
- when ActiveSupport::OrderedOptions
- [*(fallbacks[:defaults] || []) << fallbacks[:map]].compact
- when Hash, Array
- Array.wrap(fallbacks)
- else # TrueClass
- []
+ args = \
+ case fallbacks
+ when ActiveSupport::OrderedOptions
+ [*(fallbacks[:defaults] || []) << fallbacks[:map]].compact
+ when Hash, Array
+ Array.wrap(fallbacks)
+ else # TrueClass
+ [I18n.default_locale]
+ end
+
+ if args.empty? || args.first.is_a?(Hash)
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Using I18n fallbacks with an empty `defaults` sets the defaults to
+ include the `default_locale`. This behavior will change in Rails 6.1.
+ If you desire the default locale to be included in the defaults, please
+ explicitly configure it with `config.i18n.fallbacks.defaults =
+ [I18n.default_locale]` or `config.i18n.fallbacks = [I18n.default_locale,
+ {...}]`
+ MSG
+ args.unshift I18n.default_locale
end
I18n.fallbacks = I18n::Locale::Fallbacks.new(*args)
@@ -96,5 +115,11 @@ module I18n
raise "Unexpected fallback type #{fallbacks.inspect}"
end
end
+
+ def self.watched_dirs_with_extensions(paths)
+ paths.each_with_object({}) do |path, result|
+ result[path.absolute_current] = path.extensions
+ end
+ end
end
end
diff --git a/activesupport/lib/active_support/inflections.rb b/activesupport/lib/active_support/inflections.rb
index 2ca1124e76..baf1cb3038 100644
--- a/activesupport/lib/active_support/inflections.rb
+++ b/activesupport/lib/active_support/inflections.rb
@@ -1,4 +1,6 @@
-require 'active_support/inflector/inflections'
+# frozen_string_literal: true
+
+require "active_support/inflector/inflections"
#--
# Defines the standard inflection rules. These are the starting point for
@@ -8,8 +10,8 @@ require 'active_support/inflector/inflections'
#++
module ActiveSupport
Inflector.inflections(:en) do |inflect|
- inflect.plural(/$/, 's')
- inflect.plural(/s$/i, 's')
+ inflect.plural(/$/, "s")
+ inflect.plural(/s$/i, "s")
inflect.plural(/^(ax|test)is$/i, '\1es')
inflect.plural(/(octop|vir)us$/i, '\1i')
inflect.plural(/(octop|vir)i$/i, '\1i')
@@ -18,7 +20,7 @@ module ActiveSupport
inflect.plural(/(buffal|tomat)o$/i, '\1oes')
inflect.plural(/([ti])um$/i, '\1a')
inflect.plural(/([ti])a$/i, '\1a')
- inflect.plural(/sis$/i, 'ses')
+ inflect.plural(/sis$/i, "ses")
inflect.plural(/(?:([^f])fe|([lr])f)$/i, '\1\2ves')
inflect.plural(/(hive)$/i, '\1s')
inflect.plural(/([^aeiouy]|qu)y$/i, '\1ies')
@@ -30,7 +32,7 @@ module ActiveSupport
inflect.plural(/^(oxen)$/i, '\1')
inflect.plural(/(quiz)$/i, '\1zes')
- inflect.singular(/s$/i, '')
+ inflect.singular(/s$/i, "")
inflect.singular(/(ss)$/i, '\1')
inflect.singular(/(n)ews$/i, '\1ews')
inflect.singular(/([ti])a$/i, '\1um')
@@ -58,12 +60,12 @@ module ActiveSupport
inflect.singular(/(quiz)zes$/i, '\1')
inflect.singular(/(database)s$/i, '\1')
- inflect.irregular('person', 'people')
- inflect.irregular('man', 'men')
- inflect.irregular('child', 'children')
- inflect.irregular('sex', 'sexes')
- inflect.irregular('move', 'moves')
- inflect.irregular('zombie', 'zombies')
+ inflect.irregular("person", "people")
+ inflect.irregular("man", "men")
+ inflect.irregular("child", "children")
+ inflect.irregular("sex", "sexes")
+ inflect.irregular("move", "moves")
+ inflect.irregular("zombie", "zombies")
inflect.uncountable(%w(equipment information rice money species series fish sheep jeans police))
end
diff --git a/activesupport/lib/active_support/inflector.rb b/activesupport/lib/active_support/inflector.rb
index 215a60eba7..d77f04c9c5 100644
--- a/activesupport/lib/active_support/inflector.rb
+++ b/activesupport/lib/active_support/inflector.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
# in case active_support/inflector is required without the rest of active_support
-require 'active_support/inflector/inflections'
-require 'active_support/inflector/transliterate'
-require 'active_support/inflector/methods'
+require "active_support/inflector/inflections"
+require "active_support/inflector/transliterate"
+require "active_support/inflector/methods"
-require 'active_support/inflections'
-require 'active_support/core_ext/string/inflections'
+require "active_support/inflections"
+require "active_support/core_ext/string/inflections"
diff --git a/activesupport/lib/active_support/inflector/inflections.rb b/activesupport/lib/active_support/inflector/inflections.rb
index 486838bd15..2b86d233e5 100644
--- a/activesupport/lib/active_support/inflector/inflections.rb
+++ b/activesupport/lib/active_support/inflector/inflections.rb
@@ -1,6 +1,9 @@
-require 'thread_safe'
-require 'active_support/core_ext/array/prepend_and_append'
-require 'active_support/i18n'
+# frozen_string_literal: true
+
+require "concurrent/map"
+require "active_support/core_ext/array/prepend_and_append"
+require "active_support/i18n"
+require "active_support/deprecation"
module ActiveSupport
module Inflector
@@ -25,23 +28,60 @@ module ActiveSupport
# singularization rules that is runs. This guarantees that your rules run
# before any of the rules that may already have been loaded.
class Inflections
- @__instance__ = ThreadSafe::Cache.new
+ @__instance__ = Concurrent::Map.new
+
+ class Uncountables < Array
+ def initialize
+ @regex_array = []
+ super
+ end
+
+ def delete(entry)
+ super entry
+ @regex_array.delete(to_regex(entry))
+ end
+
+ def <<(*word)
+ add(word)
+ end
+
+ def add(words)
+ 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.match? str }
+ end
+
+ private
+ def to_regex(string)
+ /\b#{::Regexp.escape(string)}\Z/i
+ end
+ end
def self.instance(locale = :en)
@__instance__[locale] ||= new
end
attr_reader :plurals, :singulars, :uncountables, :humans, :acronyms, :acronym_regex
+ deprecate :acronym_regex
+
+ attr_reader :acronyms_camelize_regex, :acronyms_underscore_regex # :nodoc:
def initialize
- @plurals, @singulars, @uncountables, @humans, @acronyms, @acronym_regex = [], [], [], [], {}, /(?=a)b/
+ @plurals, @singulars, @uncountables, @humans, @acronyms = [], [], Uncountables.new, [], {}
+ define_acronym_regex_patterns
end
# Private, for the test suite.
def initialize_dup(orig) # :nodoc:
- %w(plurals singulars uncountables humans acronyms acronym_regex).each do |scope|
+ %w(plurals singulars uncountables humans acronyms).each do |scope|
instance_variable_set("@#{scope}", orig.send(scope).dup)
end
+ define_acronym_regex_patterns
end
# Specifies a new acronym. An acronym must be specified as it will appear
@@ -95,7 +135,7 @@ module ActiveSupport
# camelize 'mcdonald' # => 'McDonald'
def acronym(word)
@acronyms[word.downcase] = word
- @acronym_regex = /#{@acronyms.values.join("|")}/
+ define_acronym_regex_patterns
end
# Specifies a new pluralization rule and its replacement. The rule can
@@ -160,7 +200,7 @@ module ActiveSupport
# uncountable 'money', 'information'
# uncountable %w( money information rice )
def uncountable(*words)
- @uncountables += words.flatten.map(&:downcase)
+ @uncountables.add(words)
end
# Specifies a humanized form of a string by a regular expression rule or
@@ -184,12 +224,20 @@ module ActiveSupport
# clear :plurals
def clear(scope = :all)
case scope
- when :all
- @plurals, @singulars, @uncountables, @humans = [], [], [], []
- else
- instance_variable_set "@#{scope}", []
+ when :all
+ @plurals, @singulars, @uncountables, @humans = [], [], Uncountables.new, []
+ else
+ instance_variable_set "@#{scope}", []
end
end
+
+ private
+
+ def define_acronym_regex_patterns
+ @acronym_regex = @acronyms.empty? ? /(?=a)b/ : /#{@acronyms.values.join("|")}/
+ @acronyms_camelize_regex = /^(?:#{@acronym_regex}(?=\b|[A-Z_])|\w)/
+ @acronyms_underscore_regex = /(?:(?<=([A-Za-z\d]))|\b)(#{@acronym_regex})(?=\b|[^a-z])/
+ end
end
# Yields a singleton instance of Inflector::Inflections so you can specify
diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb
index be369d21c6..1af9833d46 100644
--- a/activesupport/lib/active_support/inflector/methods.rb
+++ b/activesupport/lib/active_support/inflector/methods.rb
@@ -1,6 +1,6 @@
-# encoding: utf-8
+# frozen_string_literal: true
-require 'active_support/inflections'
+require "active_support/inflections"
module ActiveSupport
# The Inflector transforms words from singular to plural, class names to table
@@ -29,7 +29,7 @@ module ActiveSupport
# pluralize('CamelOctopus') # => "CamelOctopi"
# pluralize('ley', :es) # => "leyes"
def pluralize(word, locale = :en)
- apply_inflections(word, inflections(locale).plurals)
+ apply_inflections(word, inflections(locale).plurals, locale)
end
# The reverse of #pluralize, returns the singular form of a word in a
@@ -46,7 +46,7 @@ module ActiveSupport
# singularize('CamelOctopi') # => "CamelOctopus"
# singularize('leyes', :es) # => "ley"
def singularize(word, locale = :en)
- apply_inflections(word, inflections(locale).singulars)
+ apply_inflections(word, inflections(locale).singulars, locale)
end
# Converts strings to UpperCamelCase.
@@ -70,10 +70,10 @@ module ActiveSupport
if uppercase_first_letter
string = string.sub(/^[a-z\d]*/) { |match| inflections.acronyms[match] || match.capitalize }
else
- string = string.sub(/^(?:#{inflections.acronym_regex}(?=\b|[A-Z_])|\w)/) { |match| match.downcase }
+ string = string.sub(inflections.acronyms_camelize_regex) { |match| match.downcase }
end
string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{inflections.acronyms[$2] || $2.capitalize}" }
- string.gsub!('/'.freeze, '::'.freeze)
+ string.gsub!("/", "::")
string
end
@@ -89,11 +89,11 @@ module ActiveSupport
#
# camelize(underscore('SSLError')) # => "SslError"
def underscore(camel_cased_word)
- return camel_cased_word unless camel_cased_word =~ /[A-Z-]|::/
- word = camel_cased_word.to_s.gsub('::'.freeze, '/'.freeze)
- word.gsub!(/(?:(?<=([A-Za-z\d]))|\b)(#{inflections.acronym_regex})(?=\b|[^a-z])/) { "#{$1 && '_'}#{$2.downcase}" }
- word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
- word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
+ return camel_cased_word unless /[A-Z-]|::/.match?(camel_cased_word)
+ word = camel_cased_word.to_s.gsub("::", "/")
+ word.gsub!(inflections.acronyms_underscore_regex) { "#{$1 && '_' }#{$2.downcase}" }
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
+ word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
word.tr!("-", "_")
word.downcase!
word
@@ -109,58 +109,79 @@ module ActiveSupport
# * Replaces underscores with spaces, if any.
# * Downcases all words except acronyms.
# * Capitalizes the first word.
- #
# The capitalization of the first word can be turned off by setting the
# +:capitalize+ option to false (default is true).
#
- # humanize('employee_salary') # => "Employee salary"
- # humanize('author_id') # => "Author"
- # humanize('author_id', capitalize: false) # => "author"
- # humanize('_id') # => "Id"
+ # The trailing '_id' can be kept and capitalized by setting the
+ # optional parameter +keep_id_suffix+ to true (default is false).
+ #
+ # humanize('employee_salary') # => "Employee salary"
+ # humanize('author_id') # => "Author"
+ # humanize('author_id', capitalize: false) # => "author"
+ # humanize('_id') # => "Id"
+ # humanize('author_id', keep_id_suffix: true) # => "Author Id"
#
# If "SSL" was defined to be an acronym:
#
# humanize('ssl_error') # => "SSL error"
#
- def humanize(lower_case_and_underscored_word, options = {})
+ def humanize(lower_case_and_underscored_word, capitalize: true, keep_id_suffix: false)
result = lower_case_and_underscored_word.to_s.dup
inflections.humans.each { |(rule, replacement)| break if result.sub!(rule, replacement) }
- result.sub!(/\A_+/, '')
- result.sub!(/_id\z/, '')
- result.tr!('_', ' ')
+ result.sub!(/\A_+/, "")
+ unless keep_id_suffix
+ result.sub!(/_id\z/, "")
+ end
+ result.tr!("_", " ")
result.gsub!(/([a-z\d]*)/i) do |match|
- "#{inflections.acronyms[match] || match.downcase}"
+ "#{inflections.acronyms[match.downcase] || match.downcase}"
end
- if options.fetch(:capitalize, true)
+ if capitalize
result.sub!(/\A\w/) { |match| match.upcase }
end
result
end
+ # Converts just the first character to uppercase.
+ #
+ # upcase_first('what a Lovely Day') # => "What a Lovely Day"
+ # upcase_first('w') # => "W"
+ # upcase_first('') # => ""
+ def upcase_first(string)
+ string.length > 0 ? string[0].upcase.concat(string[1..-1]) : ""
+ end
+
# Capitalizes all the words and replaces some characters in the string to
# create a nicer looking title. +titleize+ is meant for creating pretty
# output. It is not used in the Rails internals.
#
+ # The trailing '_id','Id'.. can be kept and capitalized by setting the
+ # optional parameter +keep_id_suffix+ to true.
+ # By default, this parameter is false.
+ #
# +titleize+ is also aliased as +titlecase+.
#
- # titleize('man from the boondocks') # => "Man From The Boondocks"
- # titleize('x-men: the last stand') # => "X Men: The Last Stand"
- # titleize('TheManWithoutAPast') # => "The Man Without A Past"
- # titleize('raiders_of_the_lost_ark') # => "Raiders Of The Lost Ark"
- def titleize(word)
- humanize(underscore(word)).gsub(/\b(?<!['’`])[a-z]/) { |match| match.capitalize }
+ # titleize('man from the boondocks') # => "Man From The Boondocks"
+ # titleize('x-men: the last stand') # => "X Men: The Last Stand"
+ # titleize('TheManWithoutAPast') # => "The Man Without A Past"
+ # 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, keep_id_suffix: false)
+ humanize(underscore(word), keep_id_suffix: keep_id_suffix).gsub(/\b(?<!\w['’`])[a-z]/) do |match|
+ match.capitalize
+ end
end
# Creates the name of a table like Rails does for models to table names.
# This method uses the #pluralize method on the last word in the string.
#
# tableize('RawScaledScorer') # => "raw_scaled_scorers"
- # tableize('egg_and_ham') # => "egg_and_hams"
+ # tableize('ham_and_egg') # => "ham_and_eggs"
# tableize('fancyCategory') # => "fancy_categories"
def tableize(class_name)
pluralize(underscore(class_name))
@@ -170,36 +191,36 @@ module ActiveSupport
# names to models. Note that this returns a string and not a Class (To
# convert to an actual class follow +classify+ with #constantize).
#
- # classify('egg_and_hams') # => "EggAndHam"
+ # classify('ham_and_eggs') # => "HamAndEgg"
# classify('posts') # => "Post"
#
# Singular names are not handled correctly:
#
- # classify('calculus') # => "Calculu"
+ # classify('calculus') # => "Calculus"
def classify(table_name)
# strip out any leading schema name
- camelize(singularize(table_name.to_s.sub(/.*\./, '')))
+ camelize(singularize(table_name.to_s.sub(/.*\./, "")))
end
# Replaces underscores with dashes in the string.
#
# dasherize('puni_puni') # => "puni-puni"
def dasherize(underscored_word)
- underscored_word.tr('_', '-')
+ underscored_word.tr("_", "-")
end
# 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)
path = path.to_s
- if i = path.rindex('::')
- path[(i+2)..-1]
+ if i = path.rindex("::")
+ path[(i + 2)..-1]
else
path
end
@@ -215,7 +236,7 @@ module ActiveSupport
#
# See also #demodulize.
def deconstantize(path)
- path.to_s[0, path.rindex('::') || 0] # implementation based on the one in facets' Module#spacename
+ path.to_s[0, path.rindex("::") || 0] # implementation based on the one in facets' Module#spacename
end
# Creates a foreign key name from a class name.
@@ -231,8 +252,8 @@ module ActiveSupport
# Tries to find a constant with the name specified in the argument string.
#
- # 'Module'.constantize # => Module
- # 'Foo::Bar'.constantize # => Foo::Bar
+ # constantize('Module') # => Module
+ # constantize('Foo::Bar') # => Foo::Bar
#
# The name is assumed to be the one of a top-level constant, no matter
# whether it starts with "::" or not. No lexical context is taken into
@@ -241,14 +262,14 @@ module ActiveSupport
# C = 'outside'
# module M
# C = 'inside'
- # C # => 'inside'
- # 'C'.constantize # => 'outside', same as ::C
+ # C # => 'inside'
+ # constantize('C') # => 'outside', same as ::C
# end
#
# NameError is raised when the name is not in CamelCase or the constant is
# unknown.
def constantize(camel_cased_word)
- names = camel_cased_word.split('::')
+ names = camel_cased_word.split("::")
# Trigger a built-in NameError exception including the ill-formed constant in the message.
Object.const_get(camel_cased_word) if names.empty?
@@ -266,7 +287,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
@@ -306,7 +327,7 @@ module ActiveSupport
raise if e.name && !(camel_cased_word.to_s.split("::").include?(e.name.to_s) ||
e.name.to_s == camel_cased_word.to_s)
rescue ArgumentError => e
- raise unless e.message =~ /not missing constant #{const_regexp(camel_cased_word)}\!$/
+ raise unless /not missing constant #{const_regexp(camel_cased_word)}!$/.match?(e.message)
end
# Returns the suffix that should be added to a number to denote the position
@@ -319,18 +340,7 @@ module ActiveSupport
# ordinal(-11) # => "th"
# ordinal(-1021) # => "st"
def ordinal(number)
- abs_number = number.to_i.abs
-
- if (11..13).include?(abs_number % 100)
- "th"
- else
- case abs_number % 10
- when 1; "st"
- when 2; "nd"
- when 3; "rd"
- else "th"
- end
- end
+ I18n.translate("number.nth.ordinals", number: number)
end
# Turns a number into an ordinal string used to denote the position in an
@@ -343,41 +353,44 @@ module ActiveSupport
# ordinalize(-11) # => "-11th"
# ordinalize(-1021) # => "-1021st"
def ordinalize(number)
- "#{number}#{ordinal(number)}"
+ I18n.translate("number.nth.ordinalized", number: number)
end
private
- # Mounts a regular expression, returned as a string to ease interpolation,
- # that will match part by part the given constant.
- #
- # const_regexp("Foo::Bar::Baz") # => "Foo(::Bar(::Baz)?)?"
- # const_regexp("::") # => "::"
- def const_regexp(camel_cased_word) #:nodoc:
- parts = camel_cased_word.split("::")
+ # Mounts a regular expression, returned as a string to ease interpolation,
+ # that will match part by part the given constant.
+ #
+ # const_regexp("Foo::Bar::Baz") # => "Foo(::Bar(::Baz)?)?"
+ # const_regexp("::") # => "::"
+ def const_regexp(camel_cased_word)
+ parts = camel_cased_word.split("::")
- return Regexp.escape(camel_cased_word) if parts.blank?
+ return Regexp.escape(camel_cased_word) if parts.blank?
- last = parts.pop
+ last = parts.pop
- parts.reverse.inject(last) do |acc, part|
- part.empty? ? acc : "#{part}(::#{acc})?"
+ parts.reverse.inject(last) do |acc, part|
+ part.empty? ? acc : "#{part}(::#{acc})?"
+ end
end
- end
-
- # Applies inflection rules for +singularize+ and +pluralize+.
- #
- # apply_inflections('post', inflections.plurals) # => "posts"
- # apply_inflections('posts', inflections.singulars) # => "post"
- def apply_inflections(word, rules)
- result = word.to_s.dup
- if word.empty? || inflections.uncountables.include?(result.downcase[/\b\w+\Z/])
- result
- else
- rules.each { |(rule, replacement)| break if result.sub!(rule, replacement) }
- result
+ # Applies inflection rules for +singularize+ and +pluralize+.
+ #
+ # If passed an optional +locale+ parameter, the uncountables will be
+ # found for that locale.
+ #
+ # apply_inflections('post', inflections.plurals, :en) # => "posts"
+ # apply_inflections('posts', inflections.singulars, :en) # => "post"
+ def apply_inflections(word, rules, locale = :en)
+ result = word.to_s.dup
+
+ if word.empty? || inflections(locale).uncountables.uncountable?(result)
+ result
+ else
+ rules.each { |(rule, replacement)| break if result.sub!(rule, replacement) }
+ result
+ end
end
- end
end
end
diff --git a/activesupport/lib/active_support/inflector/transliterate.rb b/activesupport/lib/active_support/inflector/transliterate.rb
index edea142e82..0d2a17970f 100644
--- a/activesupport/lib/active_support/inflector/transliterate.rb
+++ b/activesupport/lib/active_support/inflector/transliterate.rb
@@ -1,10 +1,10 @@
-# encoding: utf-8
-require 'active_support/core_ext/string/multibyte'
-require 'active_support/i18n'
+# frozen_string_literal: true
+
+require "active_support/core_ext/string/multibyte"
+require "active_support/i18n"
module ActiveSupport
module Inflector
-
# Replaces non-ASCII characters with an ASCII approximation, or if none
# exists, a replacement character which defaults to "?".
#
@@ -59,29 +59,60 @@ module ActiveSupport
# transliterate('Jürgen')
# # => "Juergen"
def transliterate(string, replacement = "?")
- I18n.transliterate(ActiveSupport::Multibyte::Unicode.normalize(
- ActiveSupport::Multibyte::Unicode.tidy_bytes(string), :c),
- :replacement => replacement)
+ raise ArgumentError, "Can only transliterate strings. Received #{string.class.name}" unless string.is_a?(String)
+
+ I18n.transliterate(
+ ActiveSupport::Multibyte::Unicode.tidy_bytes(string).unicode_normalize(:nfc),
+ replacement: replacement
+ )
end
# Replaces special characters in a string so that it may be used as part of
# a 'pretty' URL.
#
# parameterize("Donald E. Knuth") # => "donald-e-knuth"
- # parameterize("^trés|Jolie-- ") # => "tres-jolie"
- def parameterize(string, sep = '-')
- # replace accented chars with their ascii equivalents
+ # parameterize("^très|Jolie-- ") # => "tres-jolie"
+ #
+ # To use a custom separator, override the +separator+ argument.
+ #
+ # parameterize("Donald E. Knuth", separator: '_') # => "donald_e_knuth"
+ # parameterize("^très|Jolie__ ", separator: '_') # => "tres_jolie"
+ #
+ # To preserve the case of the characters in a string, use the +preserve_case+ argument.
+ #
+ # parameterize("Donald E. Knuth", preserve_case: true) # => "Donald-E-Knuth"
+ # parameterize("^très|Jolie-- ", preserve_case: true) # => "tres-Jolie"
+ #
+ # It preserves dashes and underscores unless they are used as separators:
+ #
+ # parameterize("^très|Jolie__ ") # => "tres-jolie__"
+ # parameterize("^très|Jolie-- ", separator: "_") # => "tres_jolie--"
+ # parameterize("^très_Jolie-- ", separator: ".") # => "tres_jolie--"
+ #
+ def parameterize(string, separator: "-", preserve_case: false)
+ # Replace accented chars with their ASCII equivalents.
parameterized_string = transliterate(string)
- # Turn unwanted chars into the separator
- parameterized_string.gsub!(/[^a-z0-9\-_]+/i, sep)
- unless sep.nil? || sep.empty?
- re_sep = Regexp.escape(sep)
+
+ # Turn unwanted chars into the separator.
+ parameterized_string.gsub!(/[^a-z0-9\-_]+/i, separator)
+
+ unless separator.nil? || separator.empty?
+ if separator == "-"
+ re_duplicate_separator = /-{2,}/
+ re_leading_trailing_separator = /^-|-$/i
+ else
+ re_sep = Regexp.escape(separator)
+ re_duplicate_separator = /#{re_sep}{2,}/
+ re_leading_trailing_separator = /^#{re_sep}|#{re_sep}$/i
+ end
# No more than one of the separator in a row.
- parameterized_string.gsub!(/#{re_sep}{2,}/, sep)
+ parameterized_string.gsub!(re_duplicate_separator, separator)
# Remove leading/trailing separator.
- parameterized_string.gsub!(/^#{re_sep}|#{re_sep}$/i, '')
+ parameterized_string.gsub!(re_leading_trailing_separator, "")
end
- parameterized_string.downcase
+
+ parameterized_string.downcase! unless preserve_case
+ parameterized_string
end
end
end
diff --git a/activesupport/lib/active_support/json.rb b/activesupport/lib/active_support/json.rb
index 3e1d9b1d33..d7887175c0 100644
--- a/activesupport/lib/active_support/json.rb
+++ b/activesupport/lib/active_support/json.rb
@@ -1,2 +1,4 @@
-require 'active_support/json/decoding'
-require 'active_support/json/encoding'
+# frozen_string_literal: true
+
+require "active_support/json/decoding"
+require "active_support/json/encoding"
diff --git a/activesupport/lib/active_support/json/decoding.rb b/activesupport/lib/active_support/json/decoding.rb
index 2932954f03..8c0e016dc5 100644
--- a/activesupport/lib/active_support/json/decoding.rb
+++ b/activesupport/lib/active_support/json/decoding.rb
@@ -1,6 +1,8 @@
-require 'active_support/core_ext/module/attribute_accessors'
-require 'active_support/core_ext/module/delegation'
-require 'json'
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/attribute_accessors"
+require "active_support/core_ext/module/delegation"
+require "json"
module ActiveSupport
# Look for and parse json strings that look like ISO 8601 times.
@@ -8,7 +10,8 @@ module ActiveSupport
module JSON
# matches YAML-formatted dates
- DATE_REGEX = /^(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{1,2}-\d{1,2}[T \t]+\d{1,2}:\d{2}:\d{2}(\.[0-9]*)?(([ \t]*)Z|[-+]\d{2}?(:\d{2})?))$/
+ DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/
+ DATETIME_REGEX = /^(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{1,2}-\d{1,2}[T \t]+\d{1,2}:\d{2}:\d{2}(\.[0-9]*)?(([ \t]*)Z|[-+]\d{2}?(:\d{2})?)?)$/
class << self
# Parses a JSON string (JavaScript Object Notation) into a hash.
@@ -48,7 +51,13 @@ module ActiveSupport
nil
when DATE_REGEX
begin
- DateTime.parse(data)
+ Date.parse(data)
+ rescue ArgumentError
+ data
+ end
+ when DATETIME_REGEX
+ begin
+ Time.zone.parse(data)
rescue ArgumentError
data
end
diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb
index 031c5e9339..de1b8ac8cf 100644
--- a/activesupport/lib/active_support/json/encoding.rb
+++ b/activesupport/lib/active_support/json/encoding.rb
@@ -1,5 +1,7 @@
-require 'active_support/core_ext/object/json'
-require 'active_support/core_ext/module/delegation'
+# frozen_string_literal: true
+
+require "active_support/core_ext/object/json"
+require "active_support/core_ext/module/delegation"
module ActiveSupport
class << self
@@ -7,7 +9,7 @@ module ActiveSupport
:time_precision, :time_precision=,
:escape_html_entities_in_json, :escape_html_entities_in_json=,
:json_encoder, :json_encoder=,
- :to => :'ActiveSupport::JSON::Encoding'
+ to: :'ActiveSupport::JSON::Encoding'
end
module JSON
@@ -40,9 +42,9 @@ module ActiveSupport
ESCAPED_CHARS = {
"\u2028" => '\u2028',
"\u2029" => '\u2029',
- '>' => '\u003e',
- '<' => '\u003c',
- '&' => '\u0026',
+ ">" => '\u003e',
+ "<" => '\u003c',
+ "&" => '\u0026',
}
ESCAPE_REGEX_WITH_HTML_ENTITIES = /[\u2028\u2029><&]/u
@@ -52,9 +54,13 @@ module ActiveSupport
class EscapedString < String #:nodoc:
def to_json(*)
if Encoding.escape_html_entities_in_json
- super.gsub ESCAPE_REGEX_WITH_HTML_ENTITIES, ESCAPED_CHARS
+ s = super
+ s.gsub! ESCAPE_REGEX_WITH_HTML_ENTITIES, ESCAPED_CHARS
+ s
else
- super.gsub ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, ESCAPED_CHARS
+ s = super
+ s.gsub! ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, ESCAPED_CHARS
+ s
end
end
@@ -68,7 +74,8 @@ module ActiveSupport
:ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, :EscapedString
# Convert an object into a "JSON-ready" representation composed of
- # primitives like Hash, Array, String, Numeric, and true/false/nil.
+ # primitives like Hash, Array, String, Numeric,
+ # and +true+/+false+/+nil+.
# Recursively calls #as_json to the object to recursively build a
# fully JSON-ready object.
#
@@ -84,7 +91,7 @@ module ActiveSupport
when String
EscapedString.new(value)
when Numeric, NilClass, TrueClass, FalseClass
- value
+ value.as_json
when Hash
Hash[value.map { |k, v| [jsonify(k), jsonify(v)] }]
when Array
diff --git a/activesupport/lib/active_support/key_generator.rb b/activesupport/lib/active_support/key_generator.rb
index 51d2da3a79..00edcdd05a 100644
--- a/activesupport/lib/active_support/key_generator.rb
+++ b/activesupport/lib/active_support/key_generator.rb
@@ -1,8 +1,10 @@
-require 'thread_safe'
-require 'openssl'
+# frozen_string_literal: true
+
+require "concurrent/map"
+require "openssl"
module ActiveSupport
- # KeyGenerator is a simple wrapper around OpenSSL's implementation of PBKDF2
+ # KeyGenerator is a simple wrapper around OpenSSL's implementation of PBKDF2.
# It can be used to derive a number of keys for various purposes from a given secret.
# This lets Rails applications have a single secure secret, but avoid reusing that
# key in multiple incompatible contexts.
@@ -17,25 +19,23 @@ module ActiveSupport
# Returns a derived key suitable for use. The default key_size is chosen
# to be compatible with the default settings of ActiveSupport::MessageVerifier.
# i.e. OpenSSL::Digest::SHA1#block_length
- def generate_key(salt, key_size=64)
+ def generate_key(salt, key_size = 64)
OpenSSL::PKCS5.pbkdf2_hmac_sha1(@secret, salt, @iterations, key_size)
end
end
# CachingKeyGenerator is a wrapper around KeyGenerator which allows users to avoid
# re-executing the key generation process when it's called using the same salt and
- # key_size
+ # key_size.
class CachingKeyGenerator
def initialize(key_generator)
@key_generator = key_generator
- @cache_keys = ThreadSafe::Cache.new
+ @cache_keys = Concurrent::Map.new
end
- # Returns a derived key suitable for use. The default key_size is chosen
- # to be compatible with the default settings of ActiveSupport::MessageVerifier.
- # i.e. OpenSSL::Digest::SHA1#block_length
- def generate_key(salt, key_size=64)
- @cache_keys["#{salt}#{key_size}"] ||= @key_generator.generate_key(salt, key_size)
+ # Returns a derived key suitable for use.
+ def generate_key(*args)
+ @cache_keys[args.join] ||= @key_generator.generate_key(*args)
end
end
@@ -53,21 +53,21 @@ module ActiveSupport
private
- # To prevent users from using something insecure like "Password" we make sure that the
- # secret they've provided is at least 30 characters in length.
- def ensure_secret_secure(secret)
- if secret.blank?
- raise ArgumentError, "A secret is required to generate an integrity hash " \
- "for cookie session data. Set a secret_key_base of at least " \
- "#{SECRET_MIN_LENGTH} characters in config/secrets.yml."
- end
+ # To prevent users from using something insecure like "Password" we make sure that the
+ # secret they've provided is at least 30 characters in length.
+ def ensure_secret_secure(secret)
+ if secret.blank?
+ raise ArgumentError, "A secret is required to generate an integrity hash " \
+ "for cookie session data. Set a secret_key_base of at least " \
+ "#{SECRET_MIN_LENGTH} characters by running `rails credentials:edit`."
+ end
- if secret.length < SECRET_MIN_LENGTH
- raise ArgumentError, "Secret should be something secure, " \
- "like \"#{SecureRandom.hex(16)}\". The value you " \
- "provided, \"#{secret}\", is shorter than the minimum length " \
- "of #{SECRET_MIN_LENGTH} characters."
+ if secret.length < SECRET_MIN_LENGTH
+ raise ArgumentError, "Secret should be something secure, " \
+ "like \"#{SecureRandom.hex(16)}\". The value you " \
+ "provided, \"#{secret}\", is shorter than the minimum length " \
+ "of #{SECRET_MIN_LENGTH} characters."
+ end
end
- end
end
end
diff --git a/activesupport/lib/active_support/lazy_load_hooks.rb b/activesupport/lib/active_support/lazy_load_hooks.rb
index e2b8f0f648..a6b096a973 100644
--- a/activesupport/lib/active_support/lazy_load_hooks.rb
+++ b/activesupport/lib/active_support/lazy_load_hooks.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveSupport
# lazy_load_hooks allows Rails to lazily load a lot of components and thus
# making the app boot faster. Because of this feature now there is no need to
@@ -15,34 +17,66 @@ module ActiveSupport
# end
# end
#
- # When the entirety of +activerecord/lib/active_record/base.rb+ has been
+ # When the entirety of +ActiveRecord::Base+ has been
# evaluated then +run_load_hooks+ is invoked. The very last line of
- # +activerecord/lib/active_record/base.rb+ is:
+ # +ActiveRecord::Base+ is:
#
# ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Base)
- @load_hooks = Hash.new { |h,k| h[k] = [] }
- @loaded = Hash.new { |h,k| h[k] = [] }
-
- def self.on_load(name, options = {}, &block)
- @loaded[name].each do |base|
- execute_hook(base, options, block)
+ module LazyLoadHooks
+ def self.extended(base) # :nodoc:
+ base.class_eval do
+ @load_hooks = Hash.new { |h, k| h[k] = [] }
+ @loaded = Hash.new { |h, k| h[k] = [] }
+ @run_once = Hash.new { |h, k| h[k] = [] }
+ end
end
- @load_hooks[name] << [block, options]
- end
+ # Declares a block that will be executed when a Rails component is fully
+ # loaded.
+ #
+ # Options:
+ #
+ # * <tt>:yield</tt> - Yields the object that run_load_hooks to +block+.
+ # * <tt>:run_once</tt> - Given +block+ will run only once.
+ def on_load(name, options = {}, &block)
+ @loaded[name].each do |base|
+ execute_hook(name, base, options, block)
+ end
- def self.execute_hook(base, options, block)
- if options[:yield]
- block.call(base)
- else
- base.instance_eval(&block)
+ @load_hooks[name] << [block, options]
end
- end
- def self.run_load_hooks(name, base = Object)
- @loaded[name] << base
- @load_hooks[name].each do |hook, options|
- execute_hook(base, options, hook)
+ def run_load_hooks(name, base = Object)
+ @loaded[name] << base
+ @load_hooks[name].each do |hook, options|
+ execute_hook(name, base, options, hook)
+ end
end
+
+ private
+
+ def with_execution_control(name, block, once)
+ unless @run_once[name].include?(block)
+ @run_once[name] << block if once
+
+ yield
+ end
+ end
+
+ def execute_hook(name, base, options, block)
+ with_execution_control(name, block, options[:run_once]) do
+ if options[:yield]
+ block.call(base)
+ else
+ if base.is_a?(Module)
+ base.class_eval(&block)
+ else
+ base.instance_eval(&block)
+ end
+ end
+ end
+ end
end
+
+ extend LazyLoadHooks
end
diff --git a/activesupport/lib/active_support/locale/en.rb b/activesupport/lib/active_support/locale/en.rb
new file mode 100644
index 0000000000..a2a7ea7ae1
--- /dev/null
+++ b/activesupport/lib/active_support/locale/en.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+{
+ en: {
+ number: {
+ nth: {
+ ordinals: lambda do |_key, number:, **_options|
+ case number
+ when 1; "st"
+ when 2; "nd"
+ when 3; "rd"
+ when 4, 5, 6, 7, 8, 9, 10, 11, 12, 13; "th"
+ else
+ num_modulo = number.to_i.abs % 100
+ num_modulo %= 10 if num_modulo > 13
+ case num_modulo
+ when 1; "st"
+ when 2; "nd"
+ when 3; "rd"
+ else "th"
+ end
+ end
+ end,
+
+ ordinalized: lambda do |_key, number:, **_options|
+ "#{number}#{ActiveSupport::Inflector.ordinal(number)}"
+ end
+ }
+ }
+ }
+}
diff --git a/activesupport/lib/active_support/locale/en.yml b/activesupport/lib/active_support/locale/en.yml
index a4563ace8f..c64b7598ee 100644
--- a/activesupport/lib/active_support/locale/en.yml
+++ b/activesupport/lib/active_support/locale/en.yml
@@ -106,6 +106,8 @@ en:
mb: "MB"
gb: "GB"
tb: "TB"
+ pb: "PB"
+ eb: "EB"
# Used in NumberHelper.number_to_human()
decimal_units:
format: "%n %u"
diff --git a/activesupport/lib/active_support/log_subscriber.rb b/activesupport/lib/active_support/log_subscriber.rb
index e95dc5a866..0f7be06c8e 100644
--- a/activesupport/lib/active_support/log_subscriber.rb
+++ b/activesupport/lib/active_support/log_subscriber.rb
@@ -1,6 +1,8 @@
-require 'active_support/core_ext/module/attribute_accessors'
-require 'active_support/core_ext/class/attribute'
-require 'active_support/subscriber'
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/attribute_accessors"
+require "active_support/core_ext/class/attribute"
+require "active_support/subscriber"
module ActiveSupport
# ActiveSupport::LogSubscriber is an object set to consume
@@ -49,8 +51,7 @@ module ActiveSupport
CYAN = "\e[36m"
WHITE = "\e[37m"
- mattr_accessor :colorize_logging
- self.colorize_logging = true
+ mattr_accessor :colorize_logging, default: true
class << self
def logger
@@ -81,11 +82,13 @@ module ActiveSupport
def finish(name, id, payload)
super if logger
- rescue Exception => e
- logger.error "Could not log #{name.inspect} event. #{e.class}: #{e.message} #{e.backtrace}"
+ rescue => e
+ if logger
+ logger.error "Could not log #{name.inspect} event. #{e.class}: #{e.message} #{e.backtrace}"
+ end
end
- protected
+ private
%w(info debug warn error fatal unknown).each do |level|
class_eval <<-METHOD, __FILE__, __LINE__ + 1
@@ -95,11 +98,11 @@ module ActiveSupport
METHOD
end
- # Set color by using a string or one of the defined constants. If a third
+ # Set color by using a symbol or one of the defined constants. If a third
# 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/log_subscriber/test_helper.rb b/activesupport/lib/active_support/log_subscriber/test_helper.rb
index 75f353f62c..3f19ef5009 100644
--- a/activesupport/lib/active_support/log_subscriber/test_helper.rb
+++ b/activesupport/lib/active_support/log_subscriber/test_helper.rb
@@ -1,6 +1,8 @@
-require 'active_support/log_subscriber'
-require 'active_support/logger'
-require 'active_support/notifications'
+# frozen_string_literal: true
+
+require "active_support/log_subscriber"
+require "active_support/logger"
+require "active_support/notifications"
module ActiveSupport
class LogSubscriber
@@ -10,7 +12,7 @@ module ActiveSupport
# class SyncLogSubscriberTest < ActiveSupport::TestCase
# include ActiveSupport::LogSubscriber::TestHelper
#
- # def setup
+ # setup do
# ActiveRecord::LogSubscriber.attach_to(:active_record)
# end
#
@@ -33,7 +35,7 @@ module ActiveSupport
# you can collect them doing @logger.logged(level), where level is the level
# used in logging, like info, debug, warn and so on.
module TestHelper
- def setup
+ def setup # :nodoc:
@logger = MockLogger.new
@notifier = ActiveSupport::Notifications::Fanout.new
@@ -44,7 +46,7 @@ module ActiveSupport
ActiveSupport::Notifications.notifier = @notifier
end
- def teardown
+ def teardown # :nodoc:
set_logger(nil)
ActiveSupport::Notifications.notifier = @old_notifier
end
@@ -58,15 +60,15 @@ module ActiveSupport
def initialize(level = DEBUG)
@flush_count = 0
@level = level
- @logged = Hash.new { |h,k| h[k] = [] }
+ @logged = Hash.new { |h, k| h[k] = [] }
end
def method_missing(level, message = nil)
- if block_given?
- @logged[level] << yield
- else
- @logged[level] << message
- end
+ if block_given?
+ @logged[level] << yield
+ else
+ @logged[level] << message
+ end
end
def logged(level)
diff --git a/activesupport/lib/active_support/logger.rb b/activesupport/lib/active_support/logger.rb
index 33fccdcf95..b8555c887b 100644
--- a/activesupport/lib/active_support/logger.rb
+++ b/activesupport/lib/active_support/logger.rb
@@ -1,11 +1,24 @@
-require 'active_support/core_ext/module/attribute_accessors'
-require 'active_support/logger_silence'
-require 'logger'
+# frozen_string_literal: true
+
+require "active_support/logger_silence"
+require "active_support/logger_thread_safe_level"
+require "logger"
module ActiveSupport
class Logger < ::Logger
include LoggerSilence
+ # Returns true if the logger destination matches one of the sources
+ #
+ # logger = Logger.new(STDOUT)
+ # ActiveSupport::Logger.logger_outputs_to?(logger, STDOUT)
+ # # => true
+ def self.logger_outputs_to?(logger, *sources)
+ logdev = logger.instance_variable_get("@logdev")
+ logger_source = logdev.dev if logdev.respond_to?(:dev)
+ sources.any? { |source| source == logger_source }
+ end
+
# Broadcasts logs to multiple loggers.
def self.broadcast(logger) # :nodoc:
Module.new do
@@ -38,6 +51,29 @@ module ActiveSupport
logger.level = level
super(level)
end
+
+ define_method(:local_level=) do |level|
+ logger.local_level = level if logger.respond_to?(:local_level=)
+ super(level) if respond_to?(:local_level=)
+ end
+
+ define_method(:silence) do |level = Logger::ERROR, &block|
+ if logger.respond_to?(:silence)
+ logger.silence(level) do
+ if defined?(super)
+ super(level, &block)
+ else
+ block.call(self)
+ end
+ end
+ else
+ if defined?(super)
+ super(level, &block)
+ else
+ block.call(self)
+ end
+ end
+ end
end
end
diff --git a/activesupport/lib/active_support/logger_silence.rb b/activesupport/lib/active_support/logger_silence.rb
index a8efdef944..b2444c1e34 100644
--- a/activesupport/lib/active_support/logger_silence.rb
+++ b/activesupport/lib/active_support/logger_silence.rb
@@ -1,24 +1,45 @@
-require 'active_support/concern'
+# frozen_string_literal: true
+
+require "active_support/concern"
+require "active_support/core_ext/module/attribute_accessors"
+require "active_support/logger_thread_safe_level"
module LoggerSilence
extend ActiveSupport::Concern
-
+
included do
- cattr_accessor :silencer
- self.silencer = true
+ ActiveSupport::Deprecation.warn(
+ "Including LoggerSilence is deprecated and will be removed in Rails 6.1. " \
+ "Please use `ActiveSupport::LoggerSilence` instead"
+ )
+
+ include ActiveSupport::LoggerSilence
end
+end
+
+module ActiveSupport
+ module LoggerSilence
+ extend ActiveSupport::Concern
+
+ included do
+ cattr_accessor :silencer, default: true
+ include ActiveSupport::LoggerThreadSafeLevel
+ end
+
+ # Silences the logger for the duration of the block.
+ def silence(temporary_level = Logger::ERROR)
+ if silencer
+ begin
+ old_local_level = local_level
+ self.local_level = temporary_level
- # Silences the logger for the duration of the block.
- def silence(temporary_level = Logger::ERROR)
- if silencer
- begin
- old_logger_level, self.level = level, temporary_level
+ yield self
+ ensure
+ self.local_level = old_local_level
+ end
+ else
yield self
- ensure
- self.level = old_logger_level
end
- else
- yield self
end
end
-end \ No newline at end of file
+end
diff --git a/activesupport/lib/active_support/logger_thread_safe_level.rb b/activesupport/lib/active_support/logger_thread_safe_level.rb
new file mode 100644
index 0000000000..f16c90cfc6
--- /dev/null
+++ b/activesupport/lib/active_support/logger_thread_safe_level.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require "active_support/concern"
+require "active_support/core_ext/module/attribute_accessors"
+require "concurrent"
+
+module ActiveSupport
+ module LoggerThreadSafeLevel # :nodoc:
+ extend ActiveSupport::Concern
+
+ included do
+ cattr_accessor :local_levels, default: Concurrent::Map.new(initial_capacity: 2), instance_accessor: false
+ end
+
+ Logger::Severity.constants.each do |severity|
+ class_eval(<<-EOT, __FILE__, __LINE__ + 1)
+ def #{severity.downcase}? # def debug?
+ Logger::#{severity} >= level # DEBUG >= level
+ end # end
+ EOT
+ end
+
+ def after_initialize
+ ActiveSupport::Deprecation.warn(
+ "Logger don't need to call #after_initialize directly anymore. It will be deprecated without replacement in " \
+ "Rails 6.1."
+ )
+ end
+
+ def local_log_id
+ Thread.current.__id__
+ end
+
+ def local_level
+ self.class.local_levels[local_log_id]
+ end
+
+ def local_level=(level)
+ if level
+ self.class.local_levels[local_log_id] = level
+ else
+ self.class.local_levels.delete(local_log_id)
+ end
+ end
+
+ def level
+ local_level || super
+ end
+
+ def add(severity, message = nil, progname = nil, &block) # :nodoc:
+ return true if @logdev.nil? || (severity || UNKNOWN) < level
+ super
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb
index 92ab6fe648..6f7302e732 100644
--- a/activesupport/lib/active_support/message_encryptor.rb
+++ b/activesupport/lib/active_support/message_encryptor.rb
@@ -1,6 +1,11 @@
-require 'openssl'
-require 'base64'
-require 'active_support/core_ext/array/extract_options'
+# frozen_string_literal: true
+
+require "openssl"
+require "base64"
+require "active_support/core_ext/array/extract_options"
+require "active_support/core_ext/module/attribute_accessors"
+require "active_support/message_verifier"
+require "active_support/messages/metadata"
module ActiveSupport
# MessageEncryptor is a simple way to encrypt values which get stored
@@ -12,12 +17,83 @@ module ActiveSupport
# This can be used in situations similar to the <tt>MessageVerifier</tt>, but
# 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"
+ # len = ActiveSupport::MessageEncryptor.key_len
+ # salt = SecureRandom.random_bytes(len)
+ # key = ActiveSupport::KeyGenerator.new('password').generate_key(salt, len) # => "\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"
+ #
+ # === Confining messages to a specific purpose
+ #
+ # By default any message can be used throughout your app. But they can also be
+ # confined to a specific +:purpose+.
+ #
+ # token = crypt.encrypt_and_sign("this is the chair", purpose: :login)
+ #
+ # Then that same purpose must be passed when verifying to get the data back out:
+ #
+ # crypt.decrypt_and_verify(token, purpose: :login) # => "this is the chair"
+ # crypt.decrypt_and_verify(token, purpose: :shipping) # => nil
+ # crypt.decrypt_and_verify(token) # => nil
+ #
+ # Likewise, if a message has no purpose it won't be returned when verifying with
+ # a specific purpose.
+ #
+ # token = crypt.encrypt_and_sign("the conversation is lively")
+ # crypt.decrypt_and_verify(token, purpose: :scare_tactics) # => nil
+ # crypt.decrypt_and_verify(token) # => "the conversation is lively"
+ #
+ # === Making messages expire
+ #
+ # By default messages last forever and verifying one year from now will still
+ # return the original value. But messages can be set to expire at a given
+ # time with +:expires_in+ or +:expires_at+.
+ #
+ # crypt.encrypt_and_sign(parcel, expires_in: 1.month)
+ # crypt.encrypt_and_sign(doowad, expires_at: Time.now.end_of_year)
+ #
+ # Then the messages can be verified and returned upto the expire time.
+ # Thereafter, verifying returns +nil+.
+ #
+ # === Rotating keys
+ #
+ # MessageEncryptor also supports rotating out old configurations by falling
+ # back to a stack of encryptors. Call +rotate+ to build and add an encryptor
+ # so +decrypt_and_verify+ will also try the fallback.
+ #
+ # By default any rotated encryptors use the values of the primary
+ # encryptor unless specified otherwise.
+ #
+ # You'd give your encryptor the new defaults:
+ #
+ # crypt = ActiveSupport::MessageEncryptor.new(@secret, cipher: "aes-256-gcm")
+ #
+ # Then gradually rotate the old values out by adding them as fallbacks. Any message
+ # generated with the old values will then work until the rotation is removed.
+ #
+ # crypt.rotate old_secret # Fallback to an old secret instead of @secret.
+ # crypt.rotate cipher: "aes-256-cbc" # Fallback to an old cipher instead of aes-256-gcm.
+ #
+ # Though if both the secret and the cipher was changed at the same time,
+ # the above should be combined into:
+ #
+ # crypt.rotate old_secret, cipher: "aes-256-cbc"
class MessageEncryptor
+ prepend Messages::Rotator::Encryptor
+
+ cattr_accessor :use_authenticated_message_encryption, instance_accessor: false, default: false
+
+ class << self
+ def default_cipher #:nodoc:
+ if use_authenticated_message_encryption
+ "aes-256-gcm"
+ else
+ "aes-256-cbc"
+ end
+ end
+ end
+
module NullSerializer #:nodoc:
def self.load(value)
value
@@ -28,80 +104,124 @@ module ActiveSupport
end
end
+ module NullVerifier #:nodoc:
+ def self.verify(value)
+ value
+ end
+
+ def self.generate(value)
+ value
+ end
+ end
+
class InvalidMessage < StandardError; end
OpenSSLCipherError = OpenSSL::Cipher::CipherError
# Initialize a new MessageEncryptor. +secret+ must be at least as long as
- # the cipher key size. For the default 'aes-256-cbc' cipher, this is 256
+ # the cipher key size. For the default 'aes-256-gcm' cipher, this is 256
# bits. If you are using a user-entered secret, you can generate a suitable
- # key with <tt>OpenSSL::Digest::SHA256.new(user_secret).digest</tt> or
- # similar.
+ # 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'.
- # * <tt>:digest</tt> - String of digest to use for signing. Default is +SHA1+.
+ # <tt>OpenSSL::Cipher.ciphers</tt>. Default is 'aes-256-gcm'.
+ # * <tt>:digest</tt> - String of digest to use for signing. Default is
+ # +SHA1+. Ignored when using an AEAD cipher like 'aes-256-gcm'.
# * <tt>:serializer</tt> - Object serializer to use. Default is +Marshal+.
def initialize(secret, *signature_key_or_options)
options = signature_key_or_options.extract_options!
sign_secret = signature_key_or_options.first
@secret = secret
@sign_secret = sign_secret
- @cipher = options[:cipher] || 'aes-256-cbc'
- @verifier = MessageVerifier.new(@sign_secret || @secret, digest: options[:digest] || 'SHA1', serializer: NullSerializer)
+ @cipher = options[:cipher] || self.class.default_cipher
+ @digest = options[:digest] || "SHA1" unless aead_mode?
+ @verifier = resolve_verifier
@serializer = options[:serializer] || Marshal
end
# Encrypt and sign a message. We need to sign the message in order to avoid
- # padding attacks. Reference: http://www.limited-entropy.com/padding-oracle-attacks.
- def encrypt_and_sign(value)
- verifier.generate(_encrypt(value))
+ # padding attacks. Reference: https://www.limited-entropy.com/padding-oracle-attacks/.
+ def encrypt_and_sign(value, expires_at: nil, expires_in: nil, purpose: nil)
+ verifier.generate(_encrypt(value, expires_at: expires_at, expires_in: expires_in, purpose: purpose))
end
# Decrypt and verify a message. We need to verify the message in order to
- # avoid padding attacks. Reference: http://www.limited-entropy.com/padding-oracle-attacks.
- def decrypt_and_verify(value)
- _decrypt(verifier.verify(value))
+ # avoid padding attacks. Reference: https://www.limited-entropy.com/padding-oracle-attacks/.
+ def decrypt_and_verify(data, purpose: nil, **)
+ _decrypt(verifier.verify(data), purpose)
end
- private
-
- def _encrypt(value)
- cipher = new_cipher
- cipher.encrypt
- cipher.key = @secret
+ # Given a cipher, returns the key length of the cipher to help generate the key of desired size
+ def self.key_len(cipher = default_cipher)
+ OpenSSL::Cipher.new(cipher).key_len
+ end
- # Rely on OpenSSL for the initialization vector
- iv = cipher.random_iv
+ private
+ def _encrypt(value, **metadata_options)
+ cipher = new_cipher
+ cipher.encrypt
+ cipher.key = @secret
- encrypted_data = cipher.update(@serializer.dump(value))
- encrypted_data << cipher.final
+ # Rely on OpenSSL for the initialization vector
+ iv = cipher.random_iv
+ cipher.auth_data = "" if aead_mode?
- "#{::Base64.strict_encode64 encrypted_data}--#{::Base64.strict_encode64 iv}"
- end
+ encrypted_data = cipher.update(Messages::Metadata.wrap(@serializer.dump(value), metadata_options))
+ encrypted_data << cipher.final
- def _decrypt(encrypted_message)
- cipher = new_cipher
- encrypted_data, iv = encrypted_message.split("--").map {|v| ::Base64.strict_decode64(v)}
+ blob = "#{::Base64.strict_encode64 encrypted_data}--#{::Base64.strict_encode64 iv}"
+ blob = "#{blob}--#{::Base64.strict_encode64 cipher.auth_tag}" if aead_mode?
+ blob
+ end
- cipher.decrypt
- cipher.key = @secret
- cipher.iv = iv
+ def _decrypt(encrypted_message, purpose)
+ cipher = new_cipher
+ encrypted_data, iv, auth_tag = encrypted_message.split("--").map { |v| ::Base64.strict_decode64(v) }
+
+ # Currently the OpenSSL bindings do not raise an error if auth_tag is
+ # truncated, which would allow an attacker to easily forge it. See
+ # https://github.com/ruby/openssl/issues/63
+ raise InvalidMessage if aead_mode? && (auth_tag.nil? || auth_tag.bytes.length != 16)
+
+ cipher.decrypt
+ cipher.key = @secret
+ cipher.iv = iv
+ if aead_mode?
+ cipher.auth_tag = auth_tag
+ cipher.auth_data = ""
+ end
+
+ decrypted_data = cipher.update(encrypted_data)
+ decrypted_data << cipher.final
+
+ message = Messages::Metadata.verify(decrypted_data, purpose)
+ @serializer.load(message) if message
+ rescue OpenSSLCipherError, TypeError, ArgumentError
+ raise InvalidMessage
+ end
- decrypted_data = cipher.update(encrypted_data)
- decrypted_data << cipher.final
+ def new_cipher
+ OpenSSL::Cipher.new(@cipher)
+ end
- @serializer.load(decrypted_data)
- rescue OpenSSLCipherError, TypeError, ArgumentError
- raise InvalidMessage
- end
+ attr_reader :verifier
- def new_cipher
- OpenSSL::Cipher::Cipher.new(@cipher)
- end
+ def aead_mode?
+ @aead_mode ||= new_cipher.authenticated?
+ end
- def verifier
- @verifier
- end
+ def resolve_verifier
+ if aead_mode?
+ NullVerifier
+ else
+ MessageVerifier.new(@sign_secret || @secret, digest: @digest, serializer: NullSerializer)
+ end
+ end
end
end
diff --git a/activesupport/lib/active_support/message_verifier.rb b/activesupport/lib/active_support/message_verifier.rb
index b2a4404968..64c557bec6 100644
--- a/activesupport/lib/active_support/message_verifier.rb
+++ b/activesupport/lib/active_support/message_verifier.rb
@@ -1,6 +1,10 @@
-require 'base64'
-require 'active_support/core_ext/object/blank'
-require 'active_support/security_utils'
+# frozen_string_literal: true
+
+require "base64"
+require "active_support/core_ext/object/blank"
+require "active_support/security_utils"
+require "active_support/messages/metadata"
+require "active_support/messages/rotator"
module ActiveSupport
# +MessageVerifier+ makes it easy to generate and verify messages which are
@@ -15,7 +19,7 @@ module ActiveSupport
# In the authentication filter:
#
# id, time = @verifier.verify(cookies[:remember_me])
- # if time < Time.now
+ # if Time.now < time
# self.current_user = User.find(id)
# end
#
@@ -24,13 +28,85 @@ module ActiveSupport
# hash upon initialization:
#
# @verifier = ActiveSupport::MessageVerifier.new('s3Krit', serializer: YAML)
+ #
+ # +MessageVerifier+ creates HMAC signatures using SHA1 hash algorithm by default.
+ # If you want to use a different hash algorithm, you can change it by providing
+ # +:digest+ key as an option while initializing the verifier:
+ #
+ # @verifier = ActiveSupport::MessageVerifier.new('s3Krit', digest: 'SHA256')
+ #
+ # === Confining messages to a specific purpose
+ #
+ # By default any message can be used throughout your app. But they can also be
+ # confined to a specific +:purpose+.
+ #
+ # token = @verifier.generate("this is the chair", purpose: :login)
+ #
+ # Then that same purpose must be passed when verifying to get the data back out:
+ #
+ # @verifier.verified(token, purpose: :login) # => "this is the chair"
+ # @verifier.verified(token, purpose: :shipping) # => nil
+ # @verifier.verified(token) # => nil
+ #
+ # @verifier.verify(token, purpose: :login) # => "this is the chair"
+ # @verifier.verify(token, purpose: :shipping) # => ActiveSupport::MessageVerifier::InvalidSignature
+ # @verifier.verify(token) # => ActiveSupport::MessageVerifier::InvalidSignature
+ #
+ # Likewise, if a message has no purpose it won't be returned when verifying with
+ # a specific purpose.
+ #
+ # token = @verifier.generate("the conversation is lively")
+ # @verifier.verified(token, purpose: :scare_tactics) # => nil
+ # @verifier.verified(token) # => "the conversation is lively"
+ #
+ # @verifier.verify(token, purpose: :scare_tactics) # => ActiveSupport::MessageVerifier::InvalidSignature
+ # @verifier.verify(token) # => "the conversation is lively"
+ #
+ # === Making messages expire
+ #
+ # By default messages last forever and verifying one year from now will still
+ # return the original value. But messages can be set to expire at a given
+ # time with +:expires_in+ or +:expires_at+.
+ #
+ # @verifier.generate(parcel, expires_in: 1.month)
+ # @verifier.generate(doowad, expires_at: Time.now.end_of_year)
+ #
+ # Then the messages can be verified and returned upto the expire time.
+ # Thereafter, the +verified+ method returns +nil+ while +verify+ raises
+ # <tt>ActiveSupport::MessageVerifier::InvalidSignature</tt>.
+ #
+ # === Rotating keys
+ #
+ # MessageVerifier also supports rotating out old configurations by falling
+ # back to a stack of verifiers. Call +rotate+ to build and add a verifier to
+ # so either +verified+ or +verify+ will also try verifying with the fallback.
+ #
+ # By default any rotated verifiers use the values of the primary
+ # verifier unless specified otherwise.
+ #
+ # You'd give your verifier the new defaults:
+ #
+ # verifier = ActiveSupport::MessageVerifier.new(@secret, digest: "SHA512", serializer: JSON)
+ #
+ # Then gradually rotate the old values out by adding them as fallbacks. Any message
+ # generated with the old values will then work until the rotation is removed.
+ #
+ # verifier.rotate old_secret # Fallback to an old secret instead of @secret.
+ # verifier.rotate digest: "SHA256" # Fallback to an old digest instead of SHA512.
+ # verifier.rotate serializer: Marshal # Fallback to an old serializer instead of JSON.
+ #
+ # Though the above would most likely be combined into one rotation:
+ #
+ # verifier.rotate old_secret, digest: "SHA256", serializer: Marshal
class MessageVerifier
+ prepend Messages::Rotator::Verifier
+
class InvalidSignature < StandardError; end
def initialize(secret, options = {})
- raise ArgumentError, 'Secret should not be nil.' unless secret
+ raise ArgumentError, "Secret should not be nil." unless secret
@secret = secret
- @digest = options[:digest] || 'SHA1'
+ @digest = options[:digest] || "SHA1"
@serializer = options[:serializer] || Marshal
end
@@ -71,13 +147,14 @@ module ActiveSupport
#
# incompatible_message = "test--dad7b06c94abba8d46a15fafaef56c327665d5ff"
# verifier.verified(incompatible_message) # => TypeError: incompatible marshal file format
- def verified(signed_message)
+ def verified(signed_message, purpose: nil, **)
if valid_message?(signed_message)
begin
data = signed_message.split("--")[0]
- @serializer.load(decode(data))
+ message = Messages::Metadata.verify(decode(data), purpose)
+ @serializer.load(message) if message
rescue ArgumentError => argument_error
- return if argument_error.message =~ %r{invalid base64}
+ return if argument_error.message.include?("invalid base64")
raise
end
end
@@ -95,8 +172,8 @@ module ActiveSupport
#
# other_verifier = ActiveSupport::MessageVerifier.new 'd1ff3r3nt-s3Krit'
# other_verifier.verify(signed_message) # => ActiveSupport::MessageVerifier::InvalidSignature
- def verify(signed_message)
- verified(signed_message) || raise(InvalidSignature)
+ def verify(*args)
+ verified(*args) || raise(InvalidSignature)
end
# Generates a signed message for the provided value.
@@ -106,8 +183,8 @@ module ActiveSupport
#
# verifier = ActiveSupport::MessageVerifier.new 's3Krit'
# verifier.generate 'a private message' # => "BAhJIhRwcml2YXRlLW1lc3NhZ2UGOgZFVA==--e2d724331ebdee96a10fb99b089508d1c72bd772"
- def generate(value)
- data = encode(@serializer.dump(value))
+ def generate(value, expires_at: nil, expires_in: nil, purpose: nil)
+ data = encode(Messages::Metadata.wrap(@serializer.dump(value), expires_at: expires_at, expires_in: expires_in, purpose: purpose))
"#{data}--#{generate_digest(data)}"
end
@@ -121,7 +198,7 @@ module ActiveSupport
end
def generate_digest(data)
- require 'openssl' unless defined?(OpenSSL)
+ require "openssl" unless defined?(OpenSSL)
OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get(@digest).new, @secret, data)
end
end
diff --git a/activesupport/lib/active_support/messages/metadata.rb b/activesupport/lib/active_support/messages/metadata.rb
new file mode 100644
index 0000000000..e97caac766
--- /dev/null
+++ b/activesupport/lib/active_support/messages/metadata.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require "time"
+
+module ActiveSupport
+ module Messages #:nodoc:
+ class Metadata #:nodoc:
+ def initialize(message, expires_at = nil, purpose = nil)
+ @message, @expires_at, @purpose = message, expires_at, purpose
+ end
+
+ def as_json(options = {})
+ { _rails: { message: @message, exp: @expires_at, pur: @purpose } }
+ end
+
+ class << self
+ def wrap(message, expires_at: nil, expires_in: nil, purpose: nil)
+ if expires_at || expires_in || purpose
+ JSON.encode new(encode(message), pick_expiry(expires_at, expires_in), purpose)
+ else
+ message
+ end
+ end
+
+ def verify(message, purpose)
+ extract_metadata(message).verify(purpose)
+ end
+
+ private
+ def pick_expiry(expires_at, expires_in)
+ if expires_at
+ expires_at.utc.iso8601(3)
+ elsif expires_in
+ Time.now.utc.advance(seconds: expires_in).iso8601(3)
+ end
+ end
+
+ def extract_metadata(message)
+ data = JSON.decode(message) rescue nil
+
+ if data.is_a?(Hash) && data.key?("_rails")
+ new(decode(data["_rails"]["message"]), data["_rails"]["exp"], data["_rails"]["pur"])
+ else
+ new(message)
+ end
+ end
+
+ def encode(message)
+ ::Base64.strict_encode64(message)
+ end
+
+ def decode(message)
+ ::Base64.strict_decode64(message)
+ end
+ end
+
+ def verify(purpose)
+ @message if match?(purpose) && fresh?
+ end
+
+ private
+ def match?(purpose)
+ @purpose.to_s == purpose.to_s
+ end
+
+ def fresh?
+ @expires_at.nil? || Time.now.utc < Time.iso8601(@expires_at)
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/messages/rotation_configuration.rb b/activesupport/lib/active_support/messages/rotation_configuration.rb
new file mode 100644
index 0000000000..bd50d6d348
--- /dev/null
+++ b/activesupport/lib/active_support/messages/rotation_configuration.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module ActiveSupport
+ module Messages
+ class RotationConfiguration # :nodoc:
+ attr_reader :signed, :encrypted
+
+ def initialize
+ @signed, @encrypted = [], []
+ end
+
+ def rotate(kind, *args)
+ case kind
+ when :signed
+ @signed << args
+ when :encrypted
+ @encrypted << args
+ end
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/messages/rotator.rb b/activesupport/lib/active_support/messages/rotator.rb
new file mode 100644
index 0000000000..823a399d67
--- /dev/null
+++ b/activesupport/lib/active_support/messages/rotator.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+module ActiveSupport
+ module Messages
+ module Rotator # :nodoc:
+ def initialize(*, **options)
+ super
+
+ @options = options
+ @rotations = []
+ end
+
+ def rotate(*secrets, **options)
+ @rotations << build_rotation(*secrets, @options.merge(options))
+ end
+
+ module Encryptor
+ include Rotator
+
+ def decrypt_and_verify(*args, on_rotation: nil, **options)
+ super
+ rescue MessageEncryptor::InvalidMessage, MessageVerifier::InvalidSignature
+ run_rotations(on_rotation) { |encryptor| encryptor.decrypt_and_verify(*args, options) } || raise
+ end
+
+ private
+ def build_rotation(secret = @secret, sign_secret = @sign_secret, options)
+ self.class.new(secret, sign_secret, options)
+ end
+ end
+
+ module Verifier
+ include Rotator
+
+ def verified(*args, on_rotation: nil, **options)
+ super || run_rotations(on_rotation) { |verifier| verifier.verified(*args, options) }
+ end
+
+ private
+ def build_rotation(secret = @secret, options)
+ self.class.new(secret, options)
+ end
+ end
+
+ private
+ def run_rotations(on_rotation)
+ @rotations.find do |rotation|
+ if message = yield(rotation) rescue next
+ on_rotation.call if on_rotation
+ return message
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/multibyte.rb b/activesupport/lib/active_support/multibyte.rb
index ffebd9a60b..3fe3a05e93 100644
--- a/activesupport/lib/active_support/multibyte.rb
+++ b/activesupport/lib/active_support/multibyte.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
module ActiveSupport #:nodoc:
module Multibyte
- autoload :Chars, 'active_support/multibyte/chars'
- autoload :Unicode, 'active_support/multibyte/unicode'
+ autoload :Chars, "active_support/multibyte/chars"
+ autoload :Unicode, "active_support/multibyte/unicode"
# The proxy class returned when calling mb_chars. You can use this accessor
# to configure your own proxy class so you can support other encodings. See
diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb
index 45cf6fc1ef..a1e23aeaca 100644
--- a/activesupport/lib/active_support/multibyte/chars.rb
+++ b/activesupport/lib/active_support/multibyte/chars.rb
@@ -1,8 +1,9 @@
-# encoding: utf-8
-require 'active_support/json'
-require 'active_support/core_ext/string/access'
-require 'active_support/core_ext/string/behavior'
-require 'active_support/core_ext/module/delegation'
+# frozen_string_literal: true
+
+require "active_support/json"
+require "active_support/core_ext/string/access"
+require "active_support/core_ext/string/behavior"
+require "active_support/core_ext/module/delegation"
module ActiveSupport #:nodoc:
module Multibyte #:nodoc:
@@ -16,7 +17,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
+ # # => #<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
@@ -46,7 +48,7 @@ module ActiveSupport #:nodoc:
alias to_s wrapped_string
alias to_str wrapped_string
- delegate :<=>, :=~, :acts_like_string?, :to => :wrapped_string
+ delegate :<=>, :=~, :acts_like_string?, to: :wrapped_string
# Creates a new Chars instance by wrapping _string_.
def initialize(string)
@@ -57,7 +59,7 @@ module ActiveSupport #:nodoc:
# Forward all undefined methods to the wrapped string.
def method_missing(method, *args, &block)
result = @wrapped_string.__send__(method, *args, &block)
- if method.to_s =~ /!$/
+ if /!$/.match?(method)
self if result
else
result.kind_of?(String) ? chars(result) : result
@@ -74,6 +76,11 @@ module ActiveSupport #:nodoc:
# Returns +true+ when the proxy class can handle the string. Returns
# +false+ otherwise.
def self.consumes?(string)
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ ActiveSupport::Multibyte::Chars.consumes? is deprecated and will be
+ removed from Rails 6.1. Use string.is_utf8? instead.
+ MSG
+
string.encoding == Encoding::UTF_8
end
@@ -87,7 +94,8 @@ module ActiveSupport #:nodoc:
end
# Works like <tt>String#slice!</tt>, but returns an instance of
- # Chars, or nil if the string was not modified.
+ # Chars, or +nil+ if the string was not modified. The string will not be
+ # modified if the range given is out of bounds
#
# string = 'Welcome'
# string.mb_chars.slice!(3) # => #<ActiveSupport::Multibyte::Chars:0x000000038109b8 @wrapped_string="c">
@@ -95,14 +103,17 @@ module ActiveSupport #:nodoc:
# string.mb_chars.slice!(0..3) # => #<ActiveSupport::Multibyte::Chars:0x00000002eb80a0 @wrapped_string="Welo">
# string # => 'me'
def slice!(*args)
- chars(@wrapped_string.slice!(*args))
+ string_sliced = @wrapped_string.slice!(*args)
+ if string_sliced
+ chars(string_sliced)
+ end
end
# Reverses all characters in the string.
#
# 'Café'.mb_chars.reverse.to_s # => 'éfaC'
def reverse
- chars(Unicode.unpack_graphemes(@wrapped_string).reverse.flatten.pack('U*'))
+ chars(@wrapped_string.scan(/\X/).reverse.join)
end
# Limits the byte size of the string to a number of bytes without breaking
@@ -111,43 +122,15 @@ module ActiveSupport #:nodoc:
#
# 'こんにちは'.mb_chars.limit(7).to_s # => "こん"
def limit(limit)
- slice(0...translate_offset(limit))
- end
-
- # Converts characters in the string to uppercase.
- #
- # 'Laurent, où sont les tests ?'.mb_chars.upcase.to_s # => "LAURENT, OÙ SONT LES TESTS ?"
- def upcase
- chars Unicode.upcase(@wrapped_string)
- end
-
- # Converts characters in the string to lowercase.
- #
- # 'VĚDA A VÝZKUM'.mb_chars.downcase.to_s # => "věda a výzkum"
- def downcase
- chars Unicode.downcase(@wrapped_string)
- end
-
- # Converts characters in the string to the opposite case.
- #
- # 'El Cañón".mb_chars.swapcase.to_s # => "eL cAÑÓN"
- def swapcase
- chars Unicode.swapcase(@wrapped_string)
- end
-
- # Converts the first character to uppercase and the remainder to lowercase.
- #
- # 'über'.mb_chars.capitalize.to_s # => "Über"
- def capitalize
- (slice(0) || chars('')).upcase + (slice(1..-1) || chars('')).downcase
+ truncate_bytes(limit, omission: nil)
end
# 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)})
+ chars(downcase.to_s.gsub(/\b('?\S)/u) { $1.upcase })
end
alias_method :titlecase, :titleize
@@ -159,7 +142,24 @@ module ActiveSupport #:nodoc:
# <tt>:c</tt>, <tt>:kc</tt>, <tt>:d</tt>, or <tt>:kd</tt>. Default is
# ActiveSupport::Multibyte::Unicode.default_normalization_form
def normalize(form = nil)
- chars(Unicode.normalize(@wrapped_string, form))
+ form ||= Unicode.default_normalization_form
+
+ # See https://www.unicode.org/reports/tr15, Table 1
+ if alias_form = Unicode::NORMALIZATION_FORM_ALIASES[form]
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ ActiveSupport::Multibyte::Chars#normalize is deprecated and will be
+ removed from Rails 6.1. Use #unicode_normalize(:#{alias_form}) instead.
+ MSG
+
+ send(:unicode_normalize, alias_form)
+ else
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ ActiveSupport::Multibyte::Chars#normalize is deprecated and will be
+ removed from Rails 6.1. Use #unicode_normalize instead.
+ MSG
+
+ raise ArgumentError, "#{form} is not a valid normalization variant", caller
+ end
end
# Performs canonical decomposition on all the characters.
@@ -167,7 +167,7 @@ module ActiveSupport #:nodoc:
# 'é'.length # => 2
# 'é'.mb_chars.decompose.to_s.length # => 3
def decompose
- chars(Unicode.decompose(:canonical, @wrapped_string.codepoints.to_a).pack('U*'))
+ chars(Unicode.decompose(:canonical, @wrapped_string.codepoints.to_a).pack("U*"))
end
# Performs composition on all the characters.
@@ -175,7 +175,7 @@ module ActiveSupport #:nodoc:
# 'é'.length # => 3
# 'é'.mb_chars.compose.to_s.length # => 2
def compose
- chars(Unicode.compose(@wrapped_string.codepoints.to_a).pack('U*'))
+ chars(Unicode.compose(@wrapped_string.codepoints.to_a).pack("U*"))
end
# Returns the number of grapheme clusters in the string.
@@ -183,7 +183,7 @@ module ActiveSupport #:nodoc:
# 'क्षि'.mb_chars.length # => 4
# 'क्षि'.mb_chars.grapheme_length # => 3
def grapheme_length
- Unicode.unpack_graphemes(@wrapped_string).length
+ @wrapped_string.scan(/\X/).length
end
# Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent
@@ -199,28 +199,16 @@ module ActiveSupport #:nodoc:
to_s.as_json(options)
end
- %w(capitalize downcase reverse tidy_bytes upcase).each do |method|
+ %w(reverse tidy_bytes).each do |method|
define_method("#{method}!") do |*args|
@wrapped_string = send(method, *args).to_s
self
end
end
- protected
-
- def translate_offset(byte_offset) #:nodoc:
- return nil if byte_offset.nil?
- return 0 if @wrapped_string == ''
-
- begin
- @wrapped_string.byteslice(0...byte_offset).unpack('U*').length
- rescue ArgumentError
- byte_offset -= 1
- retry
- end
- end
+ private
- 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 35efebc65f..ce8ecece69 100644
--- a/activesupport/lib/active_support/multibyte/unicode.rb
+++ b/activesupport/lib/active_support/multibyte/unicode.rb
@@ -1,17 +1,24 @@
-# encoding: utf-8
+# frozen_string_literal: true
+
module ActiveSupport
module Multibyte
module Unicode
-
extend self
# A list of all available normalization forms.
- # See http://www.unicode.org/reports/tr15/tr15-29.html for more
+ # See https://www.unicode.org/reports/tr15/tr15-29.html for more
# information about normalization.
NORMALIZATION_FORMS = [:c, :kc, :d, :kd]
+ NORMALIZATION_FORM_ALIASES = { # :nodoc:
+ c: :nfc,
+ d: :nfd,
+ kc: :nfkc,
+ kd: :nfkd
+ }
+
# The Unicode version that is supported by the implementation
- UNICODE_VERSION = '7.0.0'
+ UNICODE_VERSION = RbConfig::CONFIG["UNICODE_VERSION"]
# The default normalization used for operations that require
# normalization. It can be set to any of the normalizations
@@ -21,194 +28,44 @@ module ActiveSupport
attr_accessor :default_normalization_form
@default_normalization_form = :kc
- # Hangul character boundaries and properties
- HANGUL_SBASE = 0xAC00
- HANGUL_LBASE = 0x1100
- HANGUL_VBASE = 0x1161
- HANGUL_TBASE = 0x11A7
- HANGUL_LCOUNT = 19
- HANGUL_VCOUNT = 21
- HANGUL_TCOUNT = 28
- HANGUL_NCOUNT = HANGUL_VCOUNT * HANGUL_TCOUNT
- HANGUL_SCOUNT = 11172
- HANGUL_SLAST = HANGUL_SBASE + HANGUL_SCOUNT
- HANGUL_JAMO_FIRST = 0x1100
- HANGUL_JAMO_LAST = 0x11FF
-
- # All the unicode whitespace
- WHITESPACE = [
- (0x0009..0x000D).to_a, # White_Space # Cc [5] <control-0009>..<control-000D>
- 0x0020, # White_Space # Zs SPACE
- 0x0085, # White_Space # Cc <control-0085>
- 0x00A0, # White_Space # Zs NO-BREAK SPACE
- 0x1680, # White_Space # Zs OGHAM SPACE MARK
- (0x2000..0x200A).to_a, # White_Space # Zs [11] EN QUAD..HAIR SPACE
- 0x2028, # White_Space # Zl LINE SEPARATOR
- 0x2029, # White_Space # Zp PARAGRAPH SEPARATOR
- 0x202F, # White_Space # Zs NARROW NO-BREAK SPACE
- 0x205F, # White_Space # Zs MEDIUM MATHEMATICAL SPACE
- 0x3000, # White_Space # Zs IDEOGRAPHIC SPACE
- ].flatten.freeze
-
- # BOM (byte order mark) can also be seen as whitespace, it's a
- # non-rendering character used to distinguish between little and big
- # endian. This is not an issue in utf-8, so it must be ignored.
- LEADERS_AND_TRAILERS = WHITESPACE + [65279] # ZERO-WIDTH NO-BREAK SPACE aka BOM
-
- # Returns a regular expression pattern that matches the passed Unicode
- # codepoints.
- def self.codepoints_to_pattern(array_of_codepoints) #:nodoc:
- array_of_codepoints.collect{ |e| [e].pack 'U*' }.join('|')
- end
- TRAILERS_PAT = /(#{codepoints_to_pattern(LEADERS_AND_TRAILERS)})+\Z/u
- LEADERS_PAT = /\A(#{codepoints_to_pattern(LEADERS_AND_TRAILERS)})+/u
-
- # Detect whether the codepoint is in a certain character class. Returns
- # +true+ when it's in the specified character class and +false+ otherwise.
- # Valid character classes are: <tt>:cr</tt>, <tt>:lf</tt>, <tt>:l</tt>,
- # <tt>:v</tt>, <tt>:lv</tt>, <tt>:lvt</tt> and <tt>:t</tt>.
- #
- # Primarily used by the grapheme cluster support.
- def in_char_class?(codepoint, classes)
- classes.detect { |c| database.boundary[c] === codepoint } ? true : false
- end
-
# Unpack the string at grapheme boundaries. Returns a list of character
# lists.
#
# Unicode.unpack_graphemes('क्षि') # => [[2325, 2381], [2359], [2367]]
# Unicode.unpack_graphemes('Café') # => [[67], [97], [102], [233]]
def unpack_graphemes(string)
- codepoints = string.codepoints.to_a
- unpacked = []
- pos = 0
- marker = 0
- eoc = codepoints.length
- while(pos < eoc)
- pos += 1
- previous = codepoints[pos-1]
- current = codepoints[pos]
- if (
- # CR X LF
- ( previous == database.boundary[:cr] and current == database.boundary[:lf] ) or
- # L X (L|V|LV|LVT)
- ( database.boundary[:l] === previous and in_char_class?(current, [:l,:v,:lv,:lvt]) ) or
- # (LV|V) X (V|T)
- ( in_char_class?(previous, [:lv,:v]) and in_char_class?(current, [:v,:t]) ) or
- # (LVT|T) X (T)
- ( in_char_class?(previous, [:lvt,:t]) and database.boundary[:t] === current ) or
- # X Extend
- (database.boundary[:extend] === current)
- )
- else
- unpacked << codepoints[marker..pos-1]
- marker = pos
- end
- end
- unpacked
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ ActiveSupport::Multibyte::Unicode#unpack_graphemes is deprecated and will be
+ removed from Rails 6.1. Use string.scan(/\X/).map(&:codepoints) instead.
+ MSG
+
+ string.scan(/\X/).map(&:codepoints)
end
# Reverse operation of unpack_graphemes.
#
# Unicode.pack_graphemes(Unicode.unpack_graphemes('क्षि')) # => 'क्षि'
def pack_graphemes(unpacked)
- unpacked.flatten.pack('U*')
- end
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ ActiveSupport::Multibyte::Unicode#pack_graphemes is deprecated and will be
+ removed from Rails 6.1. Use array.flatten.pack("U*") instead.
+ MSG
- # Re-order codepoints so the string becomes canonical.
- def reorder_characters(codepoints)
- length = codepoints.length- 1
- pos = 0
- while pos < length do
- cp1, cp2 = database.codepoints[codepoints[pos]], database.codepoints[codepoints[pos+1]]
- if (cp1.combining_class > cp2.combining_class) && (cp2.combining_class > 0)
- codepoints[pos..pos+1] = cp2.code, cp1.code
- pos += (pos > 0 ? -1 : 1)
- else
- pos += 1
- end
- end
- codepoints
+ unpacked.flatten.pack("U*")
end
# Decompose composed characters to the decomposed form.
def decompose(type, codepoints)
- codepoints.inject([]) do |decomposed, cp|
- # if it's a hangul syllable starter character
- if HANGUL_SBASE <= cp and cp < HANGUL_SLAST
- sindex = cp - HANGUL_SBASE
- ncp = [] # new codepoints
- ncp << HANGUL_LBASE + sindex / HANGUL_NCOUNT
- ncp << HANGUL_VBASE + (sindex % HANGUL_NCOUNT) / HANGUL_TCOUNT
- tindex = sindex % HANGUL_TCOUNT
- ncp << (HANGUL_TBASE + tindex) unless tindex == 0
- decomposed.concat ncp
- # if the codepoint is decomposable in with the current decomposition type
- elsif (ncp = database.codepoints[cp].decomp_mapping) and (!database.codepoints[cp].decomp_type || type == :compatibility)
- decomposed.concat decompose(type, ncp.dup)
- else
- decomposed << cp
- end
+ if type == :compatibility
+ codepoints.pack("U*").unicode_normalize(:nfkd).codepoints
+ else
+ codepoints.pack("U*").unicode_normalize(:nfd).codepoints
end
end
# Compose decomposed characters to the composed form.
def compose(codepoints)
- pos = 0
- eoa = codepoints.length - 1
- starter_pos = 0
- starter_char = codepoints[0]
- previous_combining_class = -1
- while pos < eoa
- pos += 1
- lindex = starter_char - HANGUL_LBASE
- # -- Hangul
- if 0 <= lindex and lindex < HANGUL_LCOUNT
- vindex = codepoints[starter_pos+1] - HANGUL_VBASE rescue vindex = -1
- if 0 <= vindex and vindex < HANGUL_VCOUNT
- tindex = codepoints[starter_pos+2] - HANGUL_TBASE rescue tindex = -1
- if 0 <= tindex and tindex < HANGUL_TCOUNT
- j = starter_pos + 2
- eoa -= 2
- else
- tindex = 0
- j = starter_pos + 1
- eoa -= 1
- end
- codepoints[starter_pos..j] = (lindex * HANGUL_VCOUNT + vindex) * HANGUL_TCOUNT + tindex + HANGUL_SBASE
- end
- starter_pos += 1
- starter_char = codepoints[starter_pos]
- # -- Other characters
- else
- current_char = codepoints[pos]
- current = database.codepoints[current_char]
- if current.combining_class > previous_combining_class
- if ref = database.composition_map[starter_char]
- composition = ref[current_char]
- else
- composition = nil
- end
- unless composition.nil?
- codepoints[starter_pos] = composition
- starter_char = composition
- codepoints.delete_at pos
- eoa -= 1
- pos -= 1
- previous_combining_class = -1
- else
- previous_combining_class = current.combining_class
- end
- else
- previous_combining_class = current.combining_class
- end
- if current.combining_class == 0
- starter_pos = pos
- starter_char = codepoints[pos]
- end
- end
- end
- codepoints
+ codepoints.pack("U*").unicode_normalize(:nfc).codepoints
end
# Rubinius' String#scrub, however, doesn't support ASCII-incompatible chars.
@@ -235,7 +92,7 @@ module ActiveSupport
reader = Encoding::Converter.new(Encoding::UTF_8, Encoding::UTF_16LE)
source = string.dup
- out = ''.force_encoding(Encoding::UTF_16LE)
+ out = "".force_encoding(Encoding::UTF_16LE)
loop do
reader.primitive_convert(source, out)
@@ -257,133 +114,44 @@ module ActiveSupport
# * <tt>string</tt> - The string to perform normalization on.
# * <tt>form</tt> - The form you want to normalize in. Should be one of
# the following: <tt>:c</tt>, <tt>:kc</tt>, <tt>:d</tt>, or <tt>:kd</tt>.
- # Default is ActiveSupport::Multibyte.default_normalization_form.
- def normalize(string, form=nil)
+ # Default is ActiveSupport::Multibyte::Unicode.default_normalization_form.
+ def normalize(string, form = nil)
form ||= @default_normalization_form
- # See http://www.unicode.org/reports/tr15, Table 1
- codepoints = string.codepoints.to_a
- case form
- when :d
- reorder_characters(decompose(:canonical, codepoints))
- when :c
- compose(reorder_characters(decompose(:canonical, codepoints)))
- when :kd
- reorder_characters(decompose(:compatibility, codepoints))
- when :kc
- compose(reorder_characters(decompose(:compatibility, codepoints)))
- else
- raise ArgumentError, "#{form} is not a valid normalization variant", caller
- end.pack('U*')
- end
-
- def downcase(string)
- apply_mapping string, :lowercase_mapping
- end
- def upcase(string)
- apply_mapping string, :uppercase_mapping
- end
-
- def swapcase(string)
- apply_mapping string, :swapcase_mapping
- end
-
- # Holds data about a codepoint in the Unicode database.
- class Codepoint
- attr_accessor :code, :combining_class, :decomp_type, :decomp_mapping, :uppercase_mapping, :lowercase_mapping
-
- # Initializing Codepoint object with default values
- def initialize
- @combining_class = 0
- @uppercase_mapping = 0
- @lowercase_mapping = 0
- end
-
- def swapcase_mapping
- uppercase_mapping > 0 ? uppercase_mapping : lowercase_mapping
+ # See https://www.unicode.org/reports/tr15, Table 1
+ if alias_form = NORMALIZATION_FORM_ALIASES[form]
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ ActiveSupport::Multibyte::Unicode#normalize is deprecated and will be
+ removed from Rails 6.1. Use String#unicode_normalize(:#{alias_form}) instead.
+ MSG
+
+ string.unicode_normalize(alias_form)
+ else
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ ActiveSupport::Multibyte::Unicode#normalize is deprecated and will be
+ removed from Rails 6.1. Use String#unicode_normalize instead.
+ MSG
+
+ raise ArgumentError, "#{form} is not a valid normalization variant", caller
end
end
- # Holds static data from the Unicode database.
- class UnicodeDatabase
- ATTRIBUTES = :codepoints, :composition_exclusion, :composition_map, :boundary, :cp1252
-
- attr_writer(*ATTRIBUTES)
-
- def initialize
- @codepoints = Hash.new(Codepoint.new)
- @composition_exclusion = []
- @composition_map = {}
- @boundary = {}
- @cp1252 = {}
- end
+ %w(downcase upcase swapcase).each do |method|
+ define_method(method) do |string|
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ ActiveSupport::Multibyte::Unicode##{method} is deprecated and
+ will be removed from Rails 6.1. Use String methods directly.
+ MSG
- # Lazy load the Unicode database so it's only loaded when it's actually used
- ATTRIBUTES.each do |attr_name|
- class_eval(<<-EOS, __FILE__, __LINE__ + 1)
- def #{attr_name} # def codepoints
- load # load
- @#{attr_name} # @codepoints
- end # end
- EOS
- end
-
- # Loads the Unicode database and returns all the internal objects of
- # UnicodeDatabase.
- def load
- begin
- @codepoints, @composition_exclusion, @composition_map, @boundary, @cp1252 = File.open(self.class.filename, 'rb') { |f| Marshal.load f.read }
- rescue => e
- raise IOError.new("Couldn't load the Unicode tables for UTF8Handler (#{e.message}), ActiveSupport::Multibyte is unusable")
- end
-
- # Redefine the === method so we can write shorter rules for grapheme cluster breaks
- @boundary.each do |k,_|
- @boundary[k].instance_eval do
- def ===(other)
- detect { |i| i === other } ? true : false
- end
- end if @boundary[k].kind_of?(Array)
- end
-
- # define attr_reader methods for the instance variables
- class << self
- attr_reader(*ATTRIBUTES)
- end
- end
-
- # Returns the directory in which the data files are stored.
- def self.dirname
- File.dirname(__FILE__) + '/../values/'
- end
-
- # Returns the filename for the data file for this version.
- def self.filename
- File.expand_path File.join(dirname, "unicode_tables.dat")
+ string.send(method)
end
end
private
- def apply_mapping(string, mapping) #:nodoc:
- database.codepoints
- string.each_codepoint.map do |codepoint|
- cp = database.codepoints[codepoint]
- if cp and (ncp = cp.send(mapping)) and ncp > 0
- ncp
- else
- codepoint
- end
- end.pack('U*')
- end
-
- def recode_windows1252_chars(string)
- string.encode(Encoding::UTF_8, Encoding::Windows_1252, invalid: :replace, undef: :replace)
- end
-
- def database
- @database ||= UnicodeDatabase.new
- end
+ def recode_windows1252_chars(string)
+ string.encode(Encoding::UTF_8, Encoding::Windows_1252, invalid: :replace, undef: :replace)
+ end
end
end
end
diff --git a/activesupport/lib/active_support/notifications.rb b/activesupport/lib/active_support/notifications.rb
index b9f8e1ab2c..01cc363e2b 100644
--- a/activesupport/lib/active_support/notifications.rb
+++ b/activesupport/lib/active_support/notifications.rb
@@ -1,6 +1,8 @@
-require 'active_support/notifications/instrumenter'
-require 'active_support/notifications/fanout'
-require 'active_support/per_thread_registry'
+# frozen_string_literal: true
+
+require "active_support/notifications/instrumenter"
+require "active_support/notifications/fanout"
+require "active_support/per_thread_registry"
module ActiveSupport
# = Notifications
@@ -13,7 +15,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.
@@ -32,7 +34,7 @@ module ActiveSupport
# name # => String, name of the event (such as 'render' from above)
# start # => Time, when the instrumented block started execution
# finish # => Time, when the instrumented block ended execution
- # id # => String, unique ID for this notification
+ # id # => String, unique ID for the instrumenter that fired the event
# payload # => Hash, the payload
# end
#
@@ -48,7 +50,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
@@ -57,20 +59,22 @@ module ActiveSupport
# event.payload # => { extra: :information }
#
# The block in the <tt>subscribe</tt> call gets the name of the event, start
- # timestamp, end timestamp, a string with a unique identifier for that event
+ # timestamp, end timestamp, a string with a unique identifier for that event's instrumenter
# (something like "535801666f04d0298cd6"), and a hash with the payload, in
# that order.
#
# If an exception happens during that particular instrumentation the payload will
# have a key <tt>:exception</tt> with an array of two elements as value: a string with
# the name of the exception class, and the exception message.
+ # The <tt>:exception_object</tt> key of the payload will have the exception
+ # itself as the value.
#
# As the previous example depicts, the class <tt>ActiveSupport::Notifications::Event</tt>
# is able to take the arguments as they come and provide an object-oriented
# interface to that data.
#
- # It is also possible to pass an object as the second parameter passed to the
- # <tt>subscribe</tt> method instead of a block:
+ # It is also possible to pass an object which responds to <tt>call</tt> method
+ # as the second parameter to the <tt>subscribe</tt> method instead of a block:
#
# module ActionController
# class PageRequest
@@ -167,6 +171,24 @@ module ActiveSupport
end
end
+ # Subscribe to a given event name with the passed +block+.
+ #
+ # You can subscribe to events by passing a String to match exact event
+ # names, or by passing a Regexp to match all events that match a pattern.
+ #
+ # ActiveSupport::Notifications.subscribe(/render/) do |*args|
+ # ...
+ # end
+ #
+ # The +block+ will receive five parameters with information about the event:
+ #
+ # ActiveSupport::Notifications.subscribe('render') do |name, start, finish, id, payload|
+ # name # => String, name of the event (such as 'render' from above)
+ # start # => Time, when the instrumented block started execution
+ # finish # => Time, when the instrumented block ended execution
+ # id # => String, unique ID for the instrumenter that fired the event
+ # payload # => Hash, the payload
+ # end
def subscribe(*args, &block)
notifier.subscribe(*args, &block)
end
diff --git a/activesupport/lib/active_support/notifications/fanout.rb b/activesupport/lib/active_support/notifications/fanout.rb
index 6bf8c7d5de..4e4ca70942 100644
--- a/activesupport/lib/active_support/notifications/fanout.rb
+++ b/activesupport/lib/active_support/notifications/fanout.rb
@@ -1,5 +1,7 @@
-require 'mutex_m'
-require 'thread_safe'
+# frozen_string_literal: true
+
+require "mutex_m"
+require "concurrent/map"
module ActiveSupport
module Notifications
@@ -12,7 +14,7 @@ module ActiveSupport
def initialize
@subscribers = []
- @listeners_for = ThreadSafe::Cache.new
+ @listeners_for = Concurrent::Map.new
super
end
@@ -42,8 +44,8 @@ module ActiveSupport
listeners_for(name).each { |s| s.start(name, id, payload) }
end
- def finish(name, id, payload)
- listeners_for(name).each { |s| s.finish(name, id, payload) }
+ def finish(name, id, payload, listeners = listeners_for(name))
+ listeners.each { |s| s.finish(name, id, payload) }
end
def publish(name, *args)
@@ -51,7 +53,7 @@ module ActiveSupport
end
def listeners_for(name)
- # this is correctly done double-checked locking (ThreadSafe::Cache's lookups have volatile semantics)
+ # this is correctly done double-checked locking (Concurrent::Map's lookups have volatile semantics)
@listeners_for[name] || synchronize do
# use synchronisation when accessing @subscribers
@listeners_for[name] ||= @subscribers.select { |s| s.subscribed_to?(name) }
@@ -68,12 +70,29 @@ module ActiveSupport
module Subscribers # :nodoc:
def self.new(pattern, listener)
- if listener.respond_to?(:start) and listener.respond_to?(:finish)
- subscriber = Evented.new pattern, listener
+ subscriber_class = Timed
+
+ if listener.respond_to?(:start) && listener.respond_to?(:finish)
+ subscriber_class = Evented
else
- subscriber = Timed.new pattern, listener
+ # Doing all this to detect a block like `proc { |x| }` vs
+ # `proc { |*x| }` or `proc { |**x| }`
+ if listener.respond_to?(:parameters)
+ params = listener.parameters
+ if params.length == 1 && params.first.first == :opt
+ subscriber_class = EventObject
+ end
+ end
end
+ wrap_all pattern, subscriber_class.new(pattern, listener)
+ end
+
+ def self.event_object_subscriber(pattern, block)
+ wrap_all pattern, EventObject.new(pattern, block)
+ end
+
+ def self.wrap_all(pattern, subscriber)
unless pattern
AllMessages.new(subscriber)
else
@@ -111,7 +130,7 @@ module ActiveSupport
end
end
- class Timed < Evented
+ class Timed < Evented # :nodoc:
def publish(name, *args)
@delegate.call name, *args
end
@@ -128,6 +147,27 @@ module ActiveSupport
end
end
+ class EventObject < Evented
+ def start(name, id, payload)
+ stack = Thread.current[:_event_stack] ||= []
+ event = build_event name, id, payload
+ event.start!
+ stack.push event
+ end
+
+ def finish(name, id, payload)
+ stack = Thread.current[:_event_stack]
+ event = stack.pop
+ event.finish!
+ @delegate.call event
+ end
+
+ private
+ def build_event(name, id, payload)
+ ActiveSupport::Notifications::Event.new name, nil, nil, id, payload
+ end
+ end
+
class AllMessages # :nodoc:
def initialize(delegate)
@delegate = delegate
diff --git a/activesupport/lib/active_support/notifications/instrumenter.rb b/activesupport/lib/active_support/notifications/instrumenter.rb
index 075ddc2382..9184f8f54b 100644
--- a/activesupport/lib/active_support/notifications/instrumenter.rb
+++ b/activesupport/lib/active_support/notifications/instrumenter.rb
@@ -1,4 +1,6 @@
-require 'securerandom'
+# frozen_string_literal: true
+
+require "securerandom"
module ActiveSupport
module Notifications
@@ -14,15 +16,17 @@ module ActiveSupport
# Instrument the given block by measuring the time taken to execute it
# and publish it. Notice that events get sent even if an error occurs
# in the passed-in block.
- def instrument(name, payload={})
- start name, payload
+ def instrument(name, payload = {})
+ # some of the listeners might have state
+ listeners_state = start name, payload
begin
yield payload
rescue Exception => e
payload[:exception] = [e.class.name, e.message]
+ payload[:exception_object] = e
raise e
ensure
- finish name, payload
+ finish_with_state listeners_state, name, payload
end
end
@@ -36,17 +40,26 @@ module ActiveSupport
@notifier.finish name, @id, payload
end
+ def finish_with_state(listeners_state, name, payload)
+ @notifier.finish name, @id, payload, listeners_state
+ end
+
private
- def unique_id
- SecureRandom.hex(10)
- end
+ def unique_id
+ SecureRandom.hex(10)
+ end
end
class Event
attr_reader :name, :time, :transaction_id, :payload, :children
attr_accessor :end
+ def self.clock_gettime_supported? # :nodoc:
+ defined?(Process::CLOCK_PROCESS_CPUTIME_ID) &&
+ !Gem.win_platform?
+ end
+
def initialize(name, start, ending, transaction_id, payload)
@name = name
@payload = payload.dup
@@ -55,6 +68,42 @@ module ActiveSupport
@end = ending
@children = []
@duration = nil
+ @cpu_time_start = nil
+ @cpu_time_finish = nil
+ @allocation_count_start = 0
+ @allocation_count_finish = 0
+ end
+
+ # Record information at the time this event starts
+ def start!
+ @time = now
+ @cpu_time_start = now_cpu
+ @allocation_count_start = now_allocations
+ end
+
+ # Record information at the time this event finishes
+ def finish!
+ @cpu_time_finish = now_cpu
+ @end = now
+ @allocation_count_finish = now_allocations
+ end
+
+ # Returns the CPU time (in milliseconds) passed since the call to
+ # +start!+ and the call to +finish!+
+ def cpu_time
+ (@cpu_time_finish - @cpu_time_start) * 1000
+ end
+
+ # Returns the idle time time (in milliseconds) passed since the call to
+ # +start!+ and the call to +finish!+
+ def idle_time
+ duration - cpu_time
+ end
+
+ # Returns the number of allocations made since the call to +start!+ and
+ # the call to +finish!+
+ def allocations
+ @allocation_count_finish - @allocation_count_start
end
# Returns the difference in milliseconds between when the execution of the
@@ -80,6 +129,31 @@ module ActiveSupport
def parent_of?(event)
@children.include? event
end
+
+ private
+ def now
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ end
+
+ if clock_gettime_supported?
+ def now_cpu
+ Process.clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID)
+ end
+ else
+ def now_cpu
+ 0
+ end
+ end
+
+ if defined?(JRUBY_VERSION)
+ def now_allocations
+ 0
+ end
+ else
+ def now_allocations
+ GC.stat :total_allocated_objects
+ end
+ end
end
end
end
diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb
index 258d9b34e1..d19a2f64d4 100644
--- a/activesupport/lib/active_support/number_helper.rb
+++ b/activesupport/lib/active_support/number_helper.rb
@@ -1,9 +1,14 @@
+# frozen_string_literal: true
+
+require "active_support/dependencies/autoload"
+
module ActiveSupport
module NumberHelper
extend ActiveSupport::Autoload
eager_autoload do
autoload :NumberConverter
+ autoload :RoundingHelper
autoload :NumberToRoundedConverter
autoload :NumberToDelimitedConverter
autoload :NumberToHumanConverter
@@ -15,7 +20,7 @@ module ActiveSupport
extend self
- # Formats a +number+ into a US phone number (e.g., (555)
+ # Formats a +number+ into a phone number (US by default e.g., (555)
# 123-9876). You can customize the format in the +options+ hash.
#
# ==== Options
@@ -27,19 +32,26 @@ module ActiveSupport
# end of the generated number.
# * <tt>:country_code</tt> - Sets the country code for the phone
# number.
+ # * <tt>:pattern</tt> - Specifies how the number is divided into three
+ # groups with the custom regexp to override the default format.
# ==== Examples
#
- # number_to_phone(5551234) # => 555-1234
- # number_to_phone('5551234') # => 555-1234
- # number_to_phone(1235551234) # => 123-555-1234
- # number_to_phone(1235551234, area_code: true) # => (123) 555-1234
- # number_to_phone(1235551234, delimiter: ' ') # => 123 555 1234
- # number_to_phone(1235551234, area_code: true, extension: 555) # => (123) 555-1234 x 555
- # number_to_phone(1235551234, country_code: 1) # => +1-123-555-1234
- # number_to_phone('123a456') # => 123a456
+ # number_to_phone(5551234) # => "555-1234"
+ # number_to_phone('5551234') # => "555-1234"
+ # number_to_phone(1235551234) # => "123-555-1234"
+ # number_to_phone(1235551234, area_code: true) # => "(123) 555-1234"
+ # number_to_phone(1235551234, delimiter: ' ') # => "123 555 1234"
+ # number_to_phone(1235551234, area_code: true, extension: 555) # => "(123) 555-1234 x 555"
+ # number_to_phone(1235551234, country_code: 1) # => "+1-123-555-1234"
+ # number_to_phone('123a456') # => "123a456"
#
# number_to_phone(1235551234, country_code: 1, extension: 1343, delimiter: '.')
- # # => +1.123.555.1234 x 1343
+ # # => "+1.123.555.1234 x 1343"
+ #
+ # number_to_phone(75561234567, pattern: /(\d{1,4})(\d{4})(\d{4})$/, area_code: true)
+ # # => "(755) 6123-4567"
+ # number_to_phone(13312345678, pattern: /(\d{3})(\d{4})(\d{4})$/)
+ # # => "133-1234-5678"
def number_to_phone(number, options = {})
NumberToPhoneConverter.convert(number, options)
end
@@ -47,6 +59,14 @@ module ActiveSupport
# Formats a +number+ into a currency string (e.g., $13.65). You
# can customize the format in the +options+ hash.
#
+ # The currency unit and number formatting of the current locale will be used
+ # unless otherwise specified in the provided options. No currency conversion
+ # is performed. If the user is given a way to change their locale, they will
+ # also be able to change the relative value of the currency displayed with
+ # this helper. If your application will ever support multiple locales, you
+ # may want to specify a constant <tt>:locale</tt> option or consider
+ # using a library capable of currency conversion.
+ #
# ==== Options
#
# * <tt>:locale</tt> - Sets the locale to be used for formatting
@@ -63,25 +83,30 @@ 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.
+ # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes
+ # insignificant zeros after the decimal separator (defaults to
+ # +false+).
#
# ==== Examples
#
- # number_to_currency(1234567890.50) # => $1,234,567,890.50
- # number_to_currency(1234567890.506) # => $1,234,567,890.51
- # number_to_currency(1234567890.506, precision: 3) # => $1,234,567,890.506
- # number_to_currency(1234567890.506, locale: :fr) # => 1 234 567 890,51 €
- # number_to_currency('123a456') # => $123a456
+ # number_to_currency(1234567890.50) # => "$1,234,567,890.50"
+ # number_to_currency(1234567890.506) # => "$1,234,567,890.51"
+ # number_to_currency(1234567890.506, precision: 3) # => "$1,234,567,890.506"
+ # number_to_currency(1234567890.506, locale: :fr) # => "1 234 567 890,51 €"
+ # number_to_currency('123a456') # => "$123a456"
#
# number_to_currency(-1234567890.50, negative_format: '(%u%n)')
- # # => ($1,234,567,890.50)
+ # # => "($1,234,567,890.50)"
# number_to_currency(1234567890.50, unit: '&pound;', separator: ',', delimiter: '')
- # # => &pound;1234567890,50
+ # # => "&pound;1234567890,50"
# number_to_currency(1234567890.50, unit: '&pound;', separator: ',', delimiter: '', format: '%n %u')
- # # => 1234567890,50 &pound;
+ # # => "1234567890,50 &pound;"
+ # number_to_currency(1234567890.50, strip_insignificant_zeros: true)
+ # # => "$1,234,567,890.5"
def number_to_currency(number, options = {})
NumberToCurrencyConverter.convert(number, options)
end
@@ -94,7 +119,7 @@ module ActiveSupport
# * <tt>:locale</tt> - Sets the locale to be used for formatting
# (defaults to current locale).
# * <tt>:precision</tt> - Sets the precision of the number
- # (defaults to 3). Keeps the number's precision if nil.
+ # (defaults to 3). Keeps the number's precision if +nil+.
# * <tt>:significant</tt> - If +true+, precision will be the number
# of significant_digits. If +false+, the number of fractional
# digits (defaults to +false+).
@@ -110,15 +135,15 @@ module ActiveSupport
#
# ==== Examples
#
- # number_to_percentage(100) # => 100.000%
- # number_to_percentage('98') # => 98.000%
- # number_to_percentage(100, precision: 0) # => 100%
- # number_to_percentage(1000, delimiter: '.', separator: ',') # => 1.000,000%
- # number_to_percentage(302.24398923423, precision: 5) # => 302.24399%
- # number_to_percentage(1000, locale: :fr) # => 1 000,000%
- # number_to_percentage:(1000, precision: nil) # => 1000%
- # number_to_percentage('98a') # => 98a%
- # number_to_percentage(100, format: '%n %') # => 100 %
+ # number_to_percentage(100) # => "100.000%"
+ # number_to_percentage('98') # => "98.000%"
+ # number_to_percentage(100, precision: 0) # => "100%"
+ # number_to_percentage(1000, delimiter: '.', separator: ',') # => "1.000,000%"
+ # number_to_percentage(302.24398923423, precision: 5) # => "302.24399%"
+ # number_to_percentage(1000, locale: :fr) # => "1000,000%"
+ # number_to_percentage(1000, precision: nil) # => "1000%"
+ # number_to_percentage('98a') # => "98a%"
+ # number_to_percentage(100, format: '%n %') # => "100.000 %"
def number_to_percentage(number, options = {})
NumberToPercentageConverter.convert(number, options)
end
@@ -135,19 +160,25 @@ module ActiveSupport
# 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.
#
# ==== Examples
#
- # number_to_delimited(12345678) # => 12,345,678
- # number_to_delimited('123456') # => 123,456
- # number_to_delimited(12345678.05) # => 12,345,678.05
- # number_to_delimited(12345678, delimiter: '.') # => 12.345.678
- # number_to_delimited(12345678, delimiter: ',') # => 12,345,678
- # number_to_delimited(12345678.05, separator: ' ') # => 12,345,678 05
- # number_to_delimited(12345678.05, locale: :fr) # => 12 345 678,05
- # number_to_delimited('112a') # => 112a
+ # number_to_delimited(12345678) # => "12,345,678"
+ # number_to_delimited('123456') # => "123,456"
+ # number_to_delimited(12345678.05) # => "12,345,678.05"
+ # number_to_delimited(12345678, delimiter: '.') # => "12.345.678"
+ # number_to_delimited(12345678, delimiter: ',') # => "12,345,678"
+ # number_to_delimited(12345678.05, separator: ' ') # => "12,345,678 05"
+ # number_to_delimited(12345678.05, locale: :fr) # => "12 345 678,05"
+ # number_to_delimited('112a') # => "112a"
# number_to_delimited(98765432.98, delimiter: ' ', separator: ',')
- # # => 98 765 432,98
+ # # => "98 765 432,98"
+ # number_to_delimited("123456.78",
+ # delimiter_pattern: /(\d+?)(?=(\d\d)+(\d)(?!\d))/)
+ # # => "1,23,456.78"
def number_to_delimited(number, options = {})
NumberToDelimitedConverter.convert(number, options)
end
@@ -162,7 +193,7 @@ module ActiveSupport
# * <tt>:locale</tt> - Sets the locale to be used for formatting
# (defaults to current locale).
# * <tt>:precision</tt> - Sets the precision of the number
- # (defaults to 3). Keeps the number's precision if nil.
+ # (defaults to 3). Keeps the number's precision if +nil+.
# * <tt>:significant</tt> - If +true+, precision will be the number
# of significant_digits. If +false+, the number of fractional
# digits (defaults to +false+).
@@ -176,22 +207,22 @@ module ActiveSupport
#
# ==== Examples
#
- # number_to_rounded(111.2345) # => 111.235
- # number_to_rounded(111.2345, precision: 2) # => 111.23
- # number_to_rounded(13, precision: 5) # => 13.00000
- # number_to_rounded(389.32314, precision: 0) # => 389
- # number_to_rounded(111.2345, significant: true) # => 111
- # number_to_rounded(111.2345, precision: 1, significant: true) # => 100
- # number_to_rounded(13, precision: 5, significant: true) # => 13.000
- # number_to_rounded(13, precision: nil) # => 13
- # number_to_rounded(111.234, locale: :fr) # => 111,234
+ # number_to_rounded(111.2345) # => "111.235"
+ # number_to_rounded(111.2345, precision: 2) # => "111.23"
+ # number_to_rounded(13, precision: 5) # => "13.00000"
+ # number_to_rounded(389.32314, precision: 0) # => "389"
+ # number_to_rounded(111.2345, significant: true) # => "111"
+ # number_to_rounded(111.2345, precision: 1, significant: true) # => "100"
+ # number_to_rounded(13, precision: 5, significant: true) # => "13.000"
+ # number_to_rounded(13, precision: nil) # => "13"
+ # number_to_rounded(111.234, locale: :fr) # => "111,234"
#
# number_to_rounded(13, precision: 5, significant: true, strip_insignificant_zeros: true)
- # # => 13
+ # # => "13"
#
- # number_to_rounded(389.32314, precision: 4, significant: true) # => 389.3
+ # number_to_rounded(389.32314, precision: 4, significant: true) # => "389.3"
# number_to_rounded(1111.2345, precision: 2, separator: ',', delimiter: '.')
- # # => 1.111,23
+ # # => "1.111,23"
def number_to_rounded(number, options = {})
NumberToRoundedConverter.convert(number, options)
end
@@ -220,20 +251,20 @@ module ActiveSupport
# * <tt>:strip_insignificant_zeros</tt> - If +true+ removes
# insignificant zeros after the decimal separator (defaults to
# +true+)
- # * <tt>:prefix</tt> - If +:si+ formats the number using the SI
- # prefix (defaults to :binary)
#
# ==== Examples
#
- # number_to_human_size(123) # => 123 Bytes
- # number_to_human_size(1234) # => 1.21 KB
- # number_to_human_size(12345) # => 12.1 KB
- # number_to_human_size(1234567) # => 1.18 MB
- # number_to_human_size(1234567890) # => 1.15 GB
- # number_to_human_size(1234567890123) # => 1.12 TB
- # number_to_human_size(1234567, precision: 2) # => 1.2 MB
- # number_to_human_size(483989, precision: 2) # => 470 KB
- # number_to_human_size(1234567, precision: 2, separator: ',') # => 1,2 MB
+ # number_to_human_size(123) # => "123 Bytes"
+ # number_to_human_size(1234) # => "1.21 KB"
+ # number_to_human_size(12345) # => "12.1 KB"
+ # number_to_human_size(1234567) # => "1.18 MB"
+ # number_to_human_size(1234567890) # => "1.15 GB"
+ # number_to_human_size(1234567890123) # => "1.12 TB"
+ # number_to_human_size(1234567890123456) # => "1.1 PB"
+ # number_to_human_size(1234567890123456789) # => "1.07 EB"
+ # number_to_human_size(1234567, precision: 2) # => "1.2 MB"
+ # number_to_human_size(483989, precision: 2) # => "470 KB"
+ # number_to_human_size(1234567, precision: 2, separator: ',') # => "1,2 MB"
# number_to_human_size(1234567890123, precision: 5) # => "1.1228 TB"
# number_to_human_size(524288000, precision: 5) # => "500 MB"
def number_to_human_size(number, options = {})
diff --git a/activesupport/lib/active_support/number_helper/number_converter.rb b/activesupport/lib/active_support/number_helper/number_converter.rb
index 9d976f1831..06ba797a13 100644
--- a/activesupport/lib/active_support/number_helper/number_converter.rb
+++ b/activesupport/lib/active_support/number_helper/number_converter.rb
@@ -1,8 +1,10 @@
-require 'active_support/core_ext/big_decimal/conversions'
-require 'active_support/core_ext/object/blank'
-require 'active_support/core_ext/hash/keys'
-require 'active_support/i18n'
-require 'active_support/core_ext/class/attribute'
+# frozen_string_literal: true
+
+require "active_support/core_ext/big_decimal/conversions"
+require "active_support/core_ext/object/blank"
+require "active_support/core_ext/hash/keys"
+require "active_support/i18n"
+require "active_support/core_ext/class/attribute"
module ActiveSupport
module NumberHelper
@@ -139,17 +141,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 +162,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
@@ -169,10 +171,10 @@ module ActiveSupport
end
def default_value(key)
- key.split('.').reduce(DEFAULTS) { |defaults, k| defaults[k.to_sym] }
+ 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/number_helper/number_to_currency_converter.rb b/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb
index 7986eb50f0..0e8ae82dd5 100644
--- a/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb
+++ b/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb
@@ -1,3 +1,7 @@
+# frozen_string_literal: true
+
+require "active_support/number_helper/number_converter"
+
module ActiveSupport
module NumberHelper
class NumberToCurrencyConverter < NumberConverter # :nodoc:
@@ -7,23 +11,19 @@ module ActiveSupport
number = self.number.to_s.strip
format = options[:format]
- if is_negative?(number)
+ if number.to_f.negative?
format = options[:negative_format]
number = absolute_value(number)
end
rounded_number = NumberToRoundedConverter.convert(number, options)
- format.gsub('%n'.freeze, rounded_number).gsub('%u'.freeze, options[:unit])
+ format.gsub("%n", rounded_number).gsub("%u", options[:unit])
end
private
- def is_negative?(number)
- number.to_f.phase != 0
- end
-
def absolute_value(number)
- number.respond_to?(:abs) ? number.abs : number.sub(/\A-/, '')
+ number.respond_to?(:abs) ? number.abs : number.sub(/\A-/, "")
end
def options
diff --git a/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb b/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb
index d85cc086d7..467a580a2e 100644
--- a/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb
+++ b/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb
@@ -1,9 +1,13 @@
+# frozen_string_literal: true
+
+require "active_support/number_helper/number_converter"
+
module ActiveSupport
module NumberHelper
class NumberToDelimitedConverter < NumberConverter #:nodoc:
self.validate_float = true
- DELIMITED_REGEX = /(\d)(?=(\d\d\d)+(?!\d))/
+ DEFAULT_DELIMITER_REGEX = /(\d)(?=(\d\d\d)+(?!\d))/
def convert
parts.join(options[:separator])
@@ -12,12 +16,16 @@ module ActiveSupport
private
def parts
- left, right = number.to_s.split('.')
- left.gsub!(DELIMITED_REGEX) do |digit_to_delimit|
+ left, right = number.to_s.split(".")
+ left.gsub!(delimiter_pattern) do |digit_to_delimit|
"#{digit_to_delimit}#{options[:delimiter]}"
end
[left, right].compact
end
+
+ def delimiter_pattern
+ options.fetch(:delimiter_pattern, DEFAULT_DELIMITER_REGEX)
+ end
end
end
end
diff --git a/activesupport/lib/active_support/number_helper/number_to_human_converter.rb b/activesupport/lib/active_support/number_helper/number_to_human_converter.rb
index 5c6fe2df83..494408fc01 100644
--- a/activesupport/lib/active_support/number_helper/number_to_human_converter.rb
+++ b/activesupport/lib/active_support/number_helper/number_to_human_converter.rb
@@ -1,3 +1,7 @@
+# frozen_string_literal: true
+
+require "active_support/number_helper/number_converter"
+
module ActiveSupport
module NumberHelper
class NumberToHumanConverter < NumberConverter # :nodoc:
@@ -9,6 +13,7 @@ module ActiveSupport
self.validate_float = true
def convert # :nodoc:
+ @number = RoundingHelper.new(options).round(number)
@number = Float(number)
# for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files
@@ -18,27 +23,26 @@ module ActiveSupport
units = opts[:units]
exponent = calculate_exponent(units)
- @number = number / (10 ** exponent)
-
- unit = determine_unit(units, exponent)
+ @number = number / (10**exponent)
rounded_number = NumberToRoundedConverter.convert(number, options)
- format.gsub('%n'.freeze, rounded_number).gsub('%u'.freeze, unit).strip
+ unit = determine_unit(units, exponent)
+ format.gsub("%n", rounded_number).gsub("%u", unit).strip
end
private
def format
- options[:format] || translate_in_locale('human.decimal_units.format')
+ options[:format] || translate_in_locale("human.decimal_units.format")
end
def determine_unit(units, exponent)
exp = DECIMAL_UNITS[exponent]
case units
when Hash
- units[exp] || ''
+ units[exp] || ""
when String, Symbol
- I18n.translate("#{units}.#{exp}", :locale => options[:locale], :count => number.to_i)
+ I18n.translate("#{units}.#{exp}", locale: options[:locale], count: number.to_i)
else
translate_in_locale("human.decimal_units.units.#{exp}", count: number.to_i)
end
@@ -54,7 +58,7 @@ module ActiveSupport
when Hash
units
when String, Symbol
- I18n.translate(units.to_s, :locale => options[:locale], :raise => true)
+ I18n.translate(units.to_s, locale: options[:locale], raise: true)
when nil
translate_in_locale("human.decimal_units.units", raise: true)
else
diff --git a/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb b/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb
index ac0d20b454..91262fa656 100644
--- a/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb
+++ b/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb
@@ -1,7 +1,11 @@
+# frozen_string_literal: true
+
+require "active_support/number_helper/number_converter"
+
module ActiveSupport
module NumberHelper
class NumberToHumanSizeConverter < NumberConverter #:nodoc:
- STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb]
+ STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb, :pb, :eb]
self.namespace = :human
self.validate_float = true
@@ -17,24 +21,24 @@ module ActiveSupport
if smaller_than_base?
number_to_format = number.to_i.to_s
else
- human_size = number / (base ** exponent)
+ human_size = number / (base**exponent)
number_to_format = NumberToRoundedConverter.convert(human_size, options)
end
- conversion_format.gsub('%n'.freeze, number_to_format).gsub('%u'.freeze, unit)
+ conversion_format.gsub("%n", number_to_format).gsub("%u", unit)
end
private
def conversion_format
- translate_number_value_with_default('human.storage_units.format', :locale => options[:locale], :raise => true)
+ translate_number_value_with_default("human.storage_units.format", locale: options[:locale], raise: true)
end
def unit
- translate_number_value_with_default(storage_unit_key, :locale => options[:locale], :count => number.to_i, :raise => true)
+ translate_number_value_with_default(storage_unit_key, locale: options[:locale], count: number.to_i, raise: true)
end
def storage_unit_key
- key_end = smaller_than_base? ? 'byte' : STORAGE_UNITS[exponent]
+ key_end = smaller_than_base? ? "byte" : STORAGE_UNITS[exponent]
"human.storage_units.units.#{key_end}"
end
@@ -50,9 +54,8 @@ module ActiveSupport
end
def base
- opts[:prefix] == :si ? 1000 : 1024
+ 1024
end
end
end
end
-
diff --git a/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb b/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb
index 4c04d40c19..0c2e190f8a 100644
--- a/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb
+++ b/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb
@@ -1,3 +1,7 @@
+# frozen_string_literal: true
+
+require "active_support/number_helper/number_converter"
+
module ActiveSupport
module NumberHelper
class NumberToPercentageConverter < NumberConverter # :nodoc:
@@ -5,7 +9,7 @@ module ActiveSupport
def convert
rounded_number = NumberToRoundedConverter.convert(number, options)
- options[:format].gsub('%n'.freeze, rounded_number)
+ options[:format].gsub("%n", rounded_number)
end
end
end
diff --git a/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb b/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb
index af2ee56d91..d5e72981b4 100644
--- a/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb
+++ b/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb
@@ -1,8 +1,12 @@
+# frozen_string_literal: true
+
+require "active_support/number_helper/number_converter"
+
module ActiveSupport
module NumberHelper
class NumberToPhoneConverter < NumberConverter #:nodoc:
def convert
- str = country_code(opts[:country_code])
+ str = country_code(opts[:country_code]).dup
str << convert_to_phone_number(number.to_s.strip)
str << phone_ext(opts[:extension])
end
@@ -18,12 +22,16 @@ module ActiveSupport
end
def convert_with_area_code(number)
- number.gsub!(/(\d{1,3})(\d{3})(\d{4}$)/,"(\\1) \\2#{delimiter}\\3")
+ default_pattern = /(\d{1,3})(\d{3})(\d{4}$)/
+ number.gsub!(regexp_pattern(default_pattern),
+ "(\\1) \\2#{delimiter}\\3")
number
end
def convert_without_area_code(number)
- number.gsub!(/(\d{0,3})(\d{3})(\d{4})$/,"\\1#{delimiter}\\2#{delimiter}\\3")
+ default_pattern = /(\d{0,3})(\d{3})(\d{4})$/
+ number.gsub!(regexp_pattern(default_pattern),
+ "\\1#{delimiter}\\2#{delimiter}\\3")
number.slice!(0, 1) if start_with_delimiter?(number)
number
end
@@ -43,7 +51,10 @@ module ActiveSupport
def phone_ext(ext)
ext.blank? ? "" : " x #{ext}"
end
+
+ def regexp_pattern(default_pattern)
+ opts.fetch :pattern, default_pattern
+ end
end
end
end
-
diff --git a/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb
index 981c562551..6ceb9a572e 100644
--- a/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb
+++ b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb
@@ -1,3 +1,7 @@
+# frozen_string_literal: true
+
+require "active_support/number_helper/number_converter"
+
module ActiveSupport
module NumberHelper
class NumberToRoundedConverter < NumberConverter # :nodoc:
@@ -5,38 +9,28 @@ module ActiveSupport
self.validate_float = true
def convert
- precision = options.delete :precision
-
- if precision
- case number
- when Float, String
- @number = BigDecimal(number.to_s)
- when Rational
- @number = BigDecimal(number, digit_count(number.to_i) + precision)
- else
- @number = number.to_d
- end
+ helper = RoundingHelper.new(options)
+ rounded_number = helper.round(number)
- if options.delete(:significant) && precision > 0
- digits, rounded_number = digits_and_rounded_number(precision)
+ if precision = options[:precision]
+ if options[:significant] && precision > 0
+ digits = helper.digit_count(rounded_number)
precision -= digits
precision = 0 if precision < 0 # don't let it be negative
- else
- rounded_number = number.round(precision)
- rounded_number = rounded_number.to_i if precision == 0 && rounded_number.finite?
- rounded_number = rounded_number.abs if rounded_number.zero? # prevent showing negative zeros
end
formatted_string =
if BigDecimal === rounded_number && rounded_number.finite?
- s = rounded_number.to_s('F') + '0'*precision
- a, b = s.split('.', 2)
- a + '.' + b[0, precision]
+ s = rounded_number.to_s("F")
+ s << "0" * precision
+ a, b = s.split(".", 2)
+ a << "."
+ a << b[0, precision]
else
"%00.#{precision}f" % rounded_number
end
else
- formatted_string = number
+ formatted_string = rounded_number
end
delimited_number = NumberToDelimitedConverter.convert(formatted_string, options)
@@ -45,26 +39,6 @@ module ActiveSupport
private
- def digits_and_rounded_number(precision)
- if zero?
- [1, 0]
- else
- digits = digit_count(number)
- multiplier = 10 ** (digits - precision)
- rounded_number = calculate_rounded_number(multiplier)
- digits = digit_count(rounded_number) # After rounding, the number of digits may have changed
- [digits, rounded_number]
- end
- end
-
- def calculate_rounded_number(multiplier)
- (number / BigDecimal.new(multiplier.to_f.to_s)).round * multiplier
- end
-
- def digit_count(number)
- number.zero? ? 1 : (Math.log10(absolute_number(number)) + 1).floor
- end
-
def strip_insignificant_zeros
options[:strip_insignificant_zeros]
end
@@ -72,19 +46,11 @@ module ActiveSupport
def format_number(number)
if strip_insignificant_zeros
escaped_separator = Regexp.escape(options[:separator])
- number.sub(/(#{escaped_separator})(\d*[1-9])?0+\z/, '\1\2').sub(/#{escaped_separator}\z/, '')
+ number.sub(/(#{escaped_separator})(\d*[1-9])?0+\z/, '\1\2').sub(/#{escaped_separator}\z/, "")
else
number
end
end
-
- def absolute_number(number)
- number.respond_to?(:abs) ? number.abs : number.to_d.abs
- end
-
- def zero?
- number.respond_to?(:zero?) ? number.zero? : number.to_d.zero?
- end
end
end
end
diff --git a/activesupport/lib/active_support/number_helper/rounding_helper.rb b/activesupport/lib/active_support/number_helper/rounding_helper.rb
new file mode 100644
index 0000000000..2ad8d49c4e
--- /dev/null
+++ b/activesupport/lib/active_support/number_helper/rounding_helper.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+module ActiveSupport
+ module NumberHelper
+ class RoundingHelper # :nodoc:
+ attr_reader :options
+
+ def initialize(options)
+ @options = options
+ end
+
+ def round(number)
+ return number unless precision
+ number = convert_to_decimal(number)
+ if significant && precision > 0
+ round_significant(number)
+ else
+ round_without_significant(number)
+ end
+ end
+
+ def digit_count(number)
+ return 1 if number.zero?
+ (Math.log10(absolute_number(number)) + 1).floor
+ end
+
+ private
+ def round_without_significant(number)
+ number = number.round(precision)
+ number = number.to_i if precision == 0 && number.finite?
+ number = number.abs if number.zero? # prevent showing negative zeros
+ number
+ end
+
+ def round_significant(number)
+ return 0 if number.zero?
+ digits = digit_count(number)
+ multiplier = 10**(digits - precision)
+ (number / BigDecimal(multiplier.to_f.to_s)).round * multiplier
+ end
+
+ def convert_to_decimal(number)
+ case number
+ when Float, String
+ BigDecimal(number.to_s)
+ when Rational
+ BigDecimal(number, digit_count(number.to_i) + precision)
+ else
+ number.to_d
+ end
+ end
+
+ def precision
+ options[:precision]
+ end
+
+ def significant
+ options[:significant]
+ end
+
+ def absolute_number(number)
+ number.respond_to?(:abs) ? number.abs : number.to_d.abs
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/option_merger.rb b/activesupport/lib/active_support/option_merger.rb
index dea84e437f..ab9ca727f6 100644
--- a/activesupport/lib/active_support/option_merger.rb
+++ b/activesupport/lib/active_support/option_merger.rb
@@ -1,4 +1,6 @@
-require 'active_support/core_ext/hash/deep_merge'
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/deep_merge"
module ActiveSupport
class OptionMerger #:nodoc:
diff --git a/activesupport/lib/active_support/ordered_hash.rb b/activesupport/lib/active_support/ordered_hash.rb
index 4680d5acb7..5758513021 100644
--- a/activesupport/lib/active_support/ordered_hash.rb
+++ b/activesupport/lib/active_support/ordered_hash.rb
@@ -1,11 +1,13 @@
-require 'yaml'
+# frozen_string_literal: true
+
+require "yaml"
YAML.add_builtin_type("omap") do |type, val|
- ActiveSupport::OrderedHash[val.map{ |v| v.to_a.first }]
+ ActiveSupport::OrderedHash[val.map { |v| v.to_a.first }]
end
module ActiveSupport
- # <tt>ActiveSupport::OrderedHash</tt> implements a hash that preserves
+ # DEPRECATED: <tt>ActiveSupport::OrderedHash</tt> implements a hash that preserves
# insertion order.
#
# oh = ActiveSupport::OrderedHash.new
@@ -25,7 +27,7 @@ module ActiveSupport
end
def encode_with(coder)
- coder.represent_seq '!omap', map { |k,v| { k => v } }
+ coder.represent_seq "!omap", map { |k, v| { k => v } }
end
def select(*args, &block)
diff --git a/activesupport/lib/active_support/ordered_options.rb b/activesupport/lib/active_support/ordered_options.rb
index 45864990ce..c4e419f546 100644
--- a/activesupport/lib/active_support/ordered_options.rb
+++ b/activesupport/lib/active_support/ordered_options.rb
@@ -1,3 +1,7 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/object/blank"
+
module ActiveSupport
# Usually key value pairs are handled something like this:
#
@@ -20,7 +24,7 @@ module ActiveSupport
# To raise an exception when the value is blank, append a
# bang to the key name, like:
#
- # h.dog! # => raises KeyError
+ # h.dog! # => raises KeyError: :dog is blank
#
class OrderedOptions < Hash
alias_method :_get, :[] # preserve the original #[] method
@@ -36,13 +40,13 @@ module ActiveSupport
def method_missing(name, *args)
name_string = name.to_s
- if name_string.chomp!('=')
+ if name_string.chomp!("=")
self[name_string] = args.first
else
- bangs = name_string.chomp!('!')
+ bangs = name_string.chomp!("!")
if bangs
- fetch(name_string.to_sym).presence || raise(KeyError.new("#{name_string} is blank."))
+ self[name_string].presence || raise(KeyError.new(":#{name_string} is blank"))
else
self[name_string]
end
@@ -66,9 +70,9 @@ module ActiveSupport
def initialize(parent = nil)
if parent.kind_of?(OrderedOptions)
# use the faster _get when dealing with OrderedOptions
- super() { |h,k| parent._get(k) }
+ super() { |h, k| parent._get(k) }
elsif parent
- super() { |h,k| parent[k] }
+ super() { |h, k| parent[k] }
else
super()
end
diff --git a/activesupport/lib/active_support/parameter_filter.rb b/activesupport/lib/active_support/parameter_filter.rb
new file mode 100644
index 0000000000..1389d82523
--- /dev/null
+++ b/activesupport/lib/active_support/parameter_filter.rb
@@ -0,0 +1,124 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/object/duplicable"
+require "active_support/core_ext/array/extract"
+
+module ActiveSupport
+ # +ParameterFilter+ allows you to specify keys for sensitive data from
+ # hash-like object and replace corresponding value. Filtering only certain
+ # sub-keys from a hash is possible by using the dot notation:
+ # 'credit_card.number'. If a proc is given, each key and value of a hash and
+ # all sub-hashes are passed to it, where the value or the key can be replaced
+ # using String#replace or similar methods.
+ #
+ # ActiveSupport::ParameterFilter.new([:password])
+ # => replaces the value to all keys matching /password/i with "[FILTERED]"
+ #
+ # ActiveSupport::ParameterFilter.new([:foo, "bar"])
+ # => replaces the value to all keys matching /foo|bar/i with "[FILTERED]"
+ #
+ # ActiveSupport::ParameterFilter.new(["credit_card.code"])
+ # => replaces { credit_card: {code: "xxxx"} } with "[FILTERED]", does not
+ # change { file: { code: "xxxx"} }
+ #
+ # ActiveSupport::ParameterFilter.new([-> (k, v) do
+ # v.reverse! if k =~ /secret/i
+ # end])
+ # => reverses the value to all keys matching /secret/i
+ class ParameterFilter
+ FILTERED = "[FILTERED]" # :nodoc:
+
+ # Create instance with given filters. Supported type of filters are +String+, +Regexp+, and +Proc+.
+ # Other types of filters are treated as +String+ using +to_s+.
+ # For +Proc+ filters, key, value, and optional original hash is passed to block arguments.
+ #
+ # ==== Options
+ #
+ # * <tt>:mask</tt> - A replaced object when filtered. Defaults to +"[FILTERED]"+
+ def initialize(filters = [], mask: FILTERED)
+ @filters = filters
+ @mask = mask
+ end
+
+ # Mask value of +params+ if key matches one of filters.
+ def filter(params)
+ compiled_filter.call(params)
+ end
+
+ # Returns filtered value for given key. For +Proc+ filters, third block argument is not populated.
+ def filter_param(key, value)
+ @filters.empty? ? value : compiled_filter.value_for_key(key, value)
+ end
+
+ private
+
+ def compiled_filter
+ @compiled_filter ||= CompiledFilter.compile(@filters, mask: @mask)
+ end
+
+ class CompiledFilter # :nodoc:
+ def self.compile(filters, mask:)
+ return lambda { |params| params.dup } if filters.empty?
+
+ strings, regexps, blocks = [], [], []
+
+ filters.each do |item|
+ case item
+ when Proc
+ blocks << item
+ when Regexp
+ regexps << item
+ else
+ strings << Regexp.escape(item.to_s)
+ end
+ end
+
+ deep_regexps = regexps.extract! { |r| r.to_s.include?("\\.") }
+ deep_strings = strings.extract! { |s| s.include?("\\.") }
+
+ regexps << Regexp.new(strings.join("|"), true) unless strings.empty?
+ deep_regexps << Regexp.new(deep_strings.join("|"), true) unless deep_strings.empty?
+
+ new regexps, deep_regexps, blocks, mask: mask
+ end
+
+ attr_reader :regexps, :deep_regexps, :blocks
+
+ def initialize(regexps, deep_regexps, blocks, mask:)
+ @regexps = regexps
+ @deep_regexps = deep_regexps.any? ? deep_regexps : nil
+ @blocks = blocks
+ @mask = mask
+ end
+
+ def call(params, parents = [], original_params = params)
+ filtered_params = params.class.new
+
+ params.each do |key, value|
+ filtered_params[key] = value_for_key(key, value, parents, original_params)
+ end
+
+ filtered_params
+ end
+
+ def value_for_key(key, value, parents = [], original_params = nil)
+ parents.push(key) if deep_regexps
+ if regexps.any? { |r| r.match?(key) }
+ value = @mask
+ elsif deep_regexps && (joined = parents.join(".")) && deep_regexps.any? { |r| r.match?(joined) }
+ value = @mask
+ elsif value.is_a?(Hash)
+ value = call(value, parents, original_params)
+ elsif value.is_a?(Array)
+ value = value.map { |v| v.is_a?(Hash) ? call(v, parents, original_params) : v }
+ elsif blocks.any?
+ key = key.dup if key.duplicable?
+ value = value.dup if value.duplicable?
+ blocks.each { |b| b.arity == 2 ? b.call(key, value) : b.call(key, value, original_params) }
+ end
+ parents.pop if deep_regexps
+ value
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/per_thread_registry.rb b/activesupport/lib/active_support/per_thread_registry.rb
index ca2e4d5625..eb92fb4371 100644
--- a/activesupport/lib/active_support/per_thread_registry.rb
+++ b/activesupport/lib/active_support/per_thread_registry.rb
@@ -1,4 +1,11 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/delegation"
+
module ActiveSupport
+ # NOTE: This approach has been deprecated for end-user code in favor of {thread_mattr_accessor}[rdoc-ref:Module#thread_mattr_accessor] and friends.
+ # Please use that approach instead.
+ #
# This module is used to encapsulate access to thread local variables.
#
# Instead of polluting the thread locals namespace:
@@ -33,19 +40,19 @@ module ActiveSupport
# If the class has an initializer, it must accept no arguments.
module PerThreadRegistry
def self.extended(object)
- object.instance_variable_set '@per_thread_registry_key', object.name.freeze
+ object.instance_variable_set "@per_thread_registry_key", object.name.freeze
end
def instance
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.
- define_singleton_method(name) do |*a, &b|
- instance.public_send(name, *a, &b)
- end
+ #
+ # By letting #delegate handle it, we avoid an enclosure that'll capture args.
+ singleton_class.delegate name, to: :instance
send(name, *args, &block)
end
diff --git a/activesupport/lib/active_support/proxy_object.rb b/activesupport/lib/active_support/proxy_object.rb
index 20a0fd8e62..0965fcd2d9 100644
--- a/activesupport/lib/active_support/proxy_object.rb
+++ b/activesupport/lib/active_support/proxy_object.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveSupport
# A class with no predefined methods that behaves similarly to Builder's
# BlankSlate. Used for proxy classes.
diff --git a/activesupport/lib/active_support/rails.rb b/activesupport/lib/active_support/rails.rb
index c8e3a4bf53..8b727a69ec 100644
--- a/activesupport/lib/active_support/rails.rb
+++ b/activesupport/lib/active_support/rails.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# This is private interface.
#
# Rails components cherry pick from Active Support as needed, but there are a
@@ -9,19 +11,19 @@
# Rails and can change anytime.
# Defines Object#blank? and Object#present?.
-require 'active_support/core_ext/object/blank'
+require "active_support/core_ext/object/blank"
# Rails own autoload, eager_load, etc.
-require 'active_support/dependencies/autoload'
+require "active_support/dependencies/autoload"
# Support for ClassMethods and the included macro.
-require 'active_support/concern'
+require "active_support/concern"
# Defines Class#class_attribute.
-require 'active_support/core_ext/class/attribute'
+require "active_support/core_ext/class/attribute"
# Defines Module#delegate.
-require 'active_support/core_ext/module/delegation'
+require "active_support/core_ext/module/delegation"
# Defines ActiveSupport::Deprecation.
-require 'active_support/deprecation'
+require "active_support/deprecation"
diff --git a/activesupport/lib/active_support/railtie.rb b/activesupport/lib/active_support/railtie.rb
index cd0fb51009..605b50d346 100644
--- a/activesupport/lib/active_support/railtie.rb
+++ b/activesupport/lib/active_support/railtie.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "active_support"
require "active_support/i18n_railtie"
@@ -7,6 +9,21 @@ module ActiveSupport
config.eager_load_namespaces << ActiveSupport
+ initializer "active_support.set_authenticated_message_encryption" do |app|
+ config.after_initialize do
+ unless app.config.active_support.use_authenticated_message_encryption.nil?
+ ActiveSupport::MessageEncryptor.use_authenticated_message_encryption =
+ app.config.active_support.use_authenticated_message_encryption
+ end
+ end
+ end
+
+ initializer "active_support.reset_all_current_attributes_instances" do |app|
+ app.reloader.before_class_unload { ActiveSupport::CurrentAttributes.clear_all }
+ app.executor.to_run { ActiveSupport::CurrentAttributes.reset_all }
+ app.executor.to_complete { ActiveSupport::CurrentAttributes.reset_all }
+ end
+
initializer "active_support.deprecation_behavior" do |app|
if deprecation = app.config.active_support.deprecation
ActiveSupport::Deprecation.behavior = deprecation
@@ -21,31 +38,43 @@ module ActiveSupport
rescue TZInfo::DataSourceNotFound => e
raise e.exception "tzinfo-data is not present. Please add gem 'tzinfo-data' to your Gemfile and run bundle install"
end
- require 'active_support/core_ext/time/zones'
- zone_default = Time.find_zone!(app.config.time_zone)
-
- unless zone_default
- raise 'Value assigned to config.time_zone not recognized. ' \
- 'Run "rake -D time" for a list of tasks for finding appropriate time zone names.'
- end
-
- Time.zone_default = zone_default
+ require "active_support/core_ext/time/zones"
+ Time.zone_default = Time.find_zone!(app.config.time_zone)
end
# Sets the default week start
# If assigned value is not a valid day symbol (e.g. :sunday, :monday, ...), an exception will be raised.
initializer "active_support.initialize_beginning_of_week" do |app|
- require 'active_support/core_ext/date/calculations'
+ require "active_support/core_ext/date/calculations"
beginning_of_week_default = Date.find_beginning_of_week!(app.config.beginning_of_week)
Date.beginning_of_week_default = beginning_of_week_default
end
+ initializer "active_support.require_master_key" do |app|
+ if app.config.respond_to?(:require_master_key) && app.config.require_master_key
+ begin
+ app.credentials.key
+ rescue ActiveSupport::EncryptedFile::MissingKeyError => error
+ $stderr.puts error.message
+ exit 1
+ end
+ end
+ end
+
initializer "active_support.set_configs" do |app|
app.config.active_support.each do |k, v|
k = "#{k}="
ActiveSupport.send(k, v) if ActiveSupport.respond_to? k
end
end
+
+ initializer "active_support.set_hash_digest_class" do |app|
+ config.after_initialize do
+ if app.config.active_support.use_sha1_digests
+ ActiveSupport::Digest.hash_digest_class = ::Digest::SHA1
+ end
+ end
+ end
end
end
diff --git a/activesupport/lib/active_support/reloader.rb b/activesupport/lib/active_support/reloader.rb
new file mode 100644
index 0000000000..fea18e9712
--- /dev/null
+++ b/activesupport/lib/active_support/reloader.rb
@@ -0,0 +1,132 @@
+# frozen_string_literal: true
+
+require "active_support/execution_wrapper"
+require "active_support/executor"
+
+module ActiveSupport
+ #--
+ # This class defines several callbacks:
+ #
+ # to_prepare -- Run once at application startup, and also from
+ # +to_run+.
+ #
+ # to_run -- Run before a work run that is reloading. If
+ # +reload_classes_only_on_change+ is true (the default), the class
+ # unload will have already occurred.
+ #
+ # to_complete -- Run after a work run that has reloaded. If
+ # +reload_classes_only_on_change+ is false, the class unload will
+ # have occurred after the work run, but before this callback.
+ #
+ # before_class_unload -- Run immediately before the classes are
+ # unloaded.
+ #
+ # after_class_unload -- Run immediately after the classes are
+ # unloaded.
+ #
+ class Reloader < ExecutionWrapper
+ define_callbacks :prepare
+
+ define_callbacks :class_unload
+
+ # Registers a callback that will run once at application startup and every time the code is reloaded.
+ def self.to_prepare(*args, &block)
+ set_callback(:prepare, *args, &block)
+ end
+
+ # Registers a callback that will run immediately before the classes are unloaded.
+ def self.before_class_unload(*args, &block)
+ set_callback(:class_unload, *args, &block)
+ end
+
+ # Registers a callback that will run immediately after the classes are unloaded.
+ def self.after_class_unload(*args, &block)
+ set_callback(:class_unload, :after, *args, &block)
+ end
+
+ to_run(:after) { self.class.prepare! }
+
+ # Initiate a manual reload
+ def self.reload!
+ executor.wrap do
+ new.tap do |instance|
+ begin
+ instance.run!
+ ensure
+ instance.complete!
+ end
+ end
+ end
+ prepare!
+ end
+
+ def self.run! # :nodoc:
+ if check!
+ super
+ else
+ Null
+ end
+ end
+
+ # Run the supplied block as a work unit, reloading code as needed
+ def self.wrap
+ executor.wrap do
+ super
+ end
+ end
+
+ class_attribute :executor, default: Executor
+ class_attribute :check, default: lambda { false }
+
+ def self.check! # :nodoc:
+ @should_reload ||= check.call
+ end
+
+ def self.reloaded! # :nodoc:
+ @should_reload = false
+ end
+
+ def self.prepare! # :nodoc:
+ new.run_callbacks(:prepare)
+ end
+
+ def initialize
+ super
+ @locked = false
+ end
+
+ # Acquire the ActiveSupport::Dependencies::Interlock unload lock,
+ # ensuring it will be released automatically
+ def require_unload_lock!
+ unless @locked
+ ActiveSupport::Dependencies.interlock.start_unloading
+ @locked = true
+ end
+ end
+
+ # Release the unload lock if it has been previously obtained
+ def release_unload_lock!
+ if @locked
+ @locked = false
+ ActiveSupport::Dependencies.interlock.done_unloading
+ end
+ end
+
+ def run! # :nodoc:
+ super
+ release_unload_lock!
+ end
+
+ def class_unload!(&block) # :nodoc:
+ require_unload_lock!
+ run_callbacks(:class_unload, &block)
+ end
+
+ def complete! # :nodoc:
+ super
+ self.class.reloaded!
+ ensure
+ release_unload_lock!
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/rescuable.rb b/activesupport/lib/active_support/rescuable.rb
index fcf5553061..e0fa29cacb 100644
--- a/activesupport/lib/active_support/rescuable.rb
+++ b/activesupport/lib/active_support/rescuable.rb
@@ -1,7 +1,8 @@
-require 'active_support/concern'
-require 'active_support/core_ext/class/attribute'
-require 'active_support/core_ext/string/inflections'
-require 'active_support/core_ext/array/extract_options'
+# frozen_string_literal: true
+
+require "active_support/concern"
+require "active_support/core_ext/class/attribute"
+require "active_support/core_ext/string/inflections"
module ActiveSupport
# Rescuable module adds support for easier exception handling.
@@ -9,8 +10,7 @@ module ActiveSupport
extend Concern
included do
- class_attribute :rescue_handlers
- self.rescue_handlers = []
+ class_attribute :rescue_handlers, default: []
end
module ClassMethods
@@ -37,7 +37,7 @@ module ActiveSupport
# render xml: exception, status: 500
# end
#
- # protected
+ # private
# def deny_access
# ...
# end
@@ -48,14 +48,12 @@ module ActiveSupport
# end
#
# Exceptions raised inside exception handlers are not propagated up.
- def rescue_from(*klasses, &block)
- options = klasses.extract_options!
-
- unless options.has_key?(:with)
+ def rescue_from(*klasses, with: nil, &block)
+ unless with
if block_given?
- options[:with] = block
+ with = block
else
- raise ArgumentError, "Need a handler. Supply an options hash that has a :with key as the last argument."
+ raise ArgumentError, "Need a handler. Pass the with: keyword argument or provide a block."
end
end
@@ -65,55 +63,112 @@ module ActiveSupport
elsif klass.is_a?(String)
klass
else
- raise ArgumentError, "#{klass} is neither an Exception nor a String"
+ raise ArgumentError, "#{klass.inspect} must be an Exception class or a String referencing an Exception class"
end
# Put the new handler at the end because the list is read in reverse.
- self.rescue_handlers += [[key, options[:with]]]
+ self.rescue_handlers += [[key, with]]
end
end
- end
- # Tries to rescue the exception by looking up and calling a registered handler.
- def rescue_with_handler(exception)
- if handler = handler_for_rescue(exception)
- handler.arity != 0 ? handler.call(exception) : handler.call
- true # don't rely on the return value of the handler
- end
- end
+ # Matches an exception to a handler based on the exception class.
+ #
+ # If no handler matches the exception, check for a handler matching the
+ # (optional) exception.cause. If no handler matches the exception or its
+ # cause, this returns +nil+, so you can deal with unhandled exceptions.
+ # Be sure to re-raise unhandled exceptions if this is what you expect.
+ #
+ # begin
+ # …
+ # rescue => exception
+ # rescue_with_handler(exception) || raise
+ # end
+ #
+ # Returns the exception if it was handled and +nil+ if it was not.
+ def rescue_with_handler(exception, object: self, visited_exceptions: [])
+ visited_exceptions << exception
- def handler_for_rescue(exception)
- # We go from right to left because pairs are pushed onto rescue_handlers
- # as rescue_from declarations are found.
- _, rescuer = self.class.rescue_handlers.reverse.detect do |klass_name, handler|
- # The purpose of allowing strings in rescue_from is to support the
- # declaration of handler associations for exception classes whose
- # definition is yet unknown.
- #
- # Since this loop needs the constants it would be inconsistent to
- # assume they should exist at this point. An early raised exception
- # could trigger some other handler and the array could include
- # precisely a string whose corresponding constant has not yet been
- # seen. This is why we are tolerant to unknown constants.
- #
- # Note that this tolerance only matters if the exception was given as
- # a string, otherwise a NameError will be raised by the interpreter
- # itself when rescue_from CONSTANT is executed.
- klass = self.class.const_get(klass_name) rescue nil
- klass ||= (klass_name.constantize rescue nil)
- klass === exception if klass
+ if handler = handler_for_rescue(exception, object: object)
+ handler.call exception
+ exception
+ elsif exception
+ if visited_exceptions.include?(exception.cause)
+ nil
+ else
+ rescue_with_handler(exception.cause, object: object, visited_exceptions: visited_exceptions)
+ end
+ end
end
- case rescuer
- when Symbol
- method(rescuer)
- when Proc
- if rescuer.arity == 0
- Proc.new { instance_exec(&rescuer) }
- else
- Proc.new { |_exception| instance_exec(_exception, &rescuer) }
+ def handler_for_rescue(exception, object: self) #:nodoc:
+ case rescuer = find_rescue_handler(exception)
+ when Symbol
+ method = object.method(rescuer)
+ if method.arity == 0
+ -> e { method.call }
+ else
+ method
+ end
+ when Proc
+ if rescuer.arity == 0
+ -> e { object.instance_exec(&rescuer) }
+ else
+ -> e { object.instance_exec(e, &rescuer) }
+ end
end
end
+
+ private
+ def find_rescue_handler(exception)
+ if exception
+ # Handlers are in order of declaration but the most recently declared
+ # is the highest priority match, so we search for matching handlers
+ # in reverse.
+ _, handler = rescue_handlers.reverse_each.detect do |class_or_name, _|
+ if klass = constantize_rescue_handler_class(class_or_name)
+ klass === exception
+ end
+ end
+
+ handler
+ end
+ end
+
+ def constantize_rescue_handler_class(class_or_name)
+ case class_or_name
+ when String, Symbol
+ begin
+ # Try a lexical lookup first since we support
+ #
+ # class Super
+ # rescue_from 'Error', with: …
+ # end
+ #
+ # class Sub
+ # class Error < StandardError; end
+ # end
+ #
+ # so an Error raised in Sub will hit the 'Error' handler.
+ const_get class_or_name
+ rescue NameError
+ class_or_name.safe_constantize
+ end
+ else
+ class_or_name
+ end
+ end
+ end
+
+ # Delegates to the class method, but uses the instance as the subject for
+ # rescue_from handlers (method calls, instance_exec blocks).
+ def rescue_with_handler(exception)
+ self.class.rescue_with_handler exception, object: self
+ end
+
+ # Internal handler lookup. Delegates to class method. Some libraries call
+ # this directly, so keeping it around for compatibility.
+ def handler_for_rescue(exception) #:nodoc:
+ self.class.handler_for_rescue exception, object: self
end
end
end
diff --git a/activesupport/lib/active_support/security_utils.rb b/activesupport/lib/active_support/security_utils.rb
index 64c4801179..20b6b9cd3f 100644
--- a/activesupport/lib/active_support/security_utils.rb
+++ b/activesupport/lib/active_support/security_utils.rb
@@ -1,13 +1,15 @@
+# frozen_string_literal: true
+
+require "digest/sha2"
+
module ActiveSupport
module SecurityUtils
- # Constant time string comparison.
+ # Constant time string comparison, for fixed length strings.
#
# The values compared should be of fixed length, such as strings
- # that have already been processed by HMAC. This should not be used
- # on variable length plaintext strings because it could leak length info
- # via timing attacks.
- def secure_compare(a, b)
- return false unless a.bytesize == b.bytesize
+ # that have already been processed by HMAC. Raises in case of length mismatch.
+ def fixed_length_secure_compare(a, b)
+ raise ArgumentError, "string length mismatch." unless a.bytesize == b.bytesize
l = a.unpack "C#{a.bytesize}"
@@ -15,6 +17,15 @@ module ActiveSupport
b.each_byte { |byte| res |= byte ^ l.shift }
res == 0
end
+ module_function :fixed_length_secure_compare
+
+ # Constant time string comparison, for variable length strings.
+ #
+ # The values are first processed by SHA256, so that we don't leak length info
+ # via timing attacks.
+ def secure_compare(a, b)
+ fixed_length_secure_compare(::Digest::SHA256.hexdigest(a), ::Digest::SHA256.hexdigest(b)) && a == b
+ end
module_function :secure_compare
end
end
diff --git a/activesupport/lib/active_support/string_inquirer.rb b/activesupport/lib/active_support/string_inquirer.rb
index bc673150d0..a3af36720e 100644
--- a/activesupport/lib/active_support/string_inquirer.rb
+++ b/activesupport/lib/active_support/string_inquirer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveSupport
# Wrapping a string in this class gives you a prettier way to test
# for equality. The value returned by <tt>Rails.env</tt> is wrapped
@@ -8,15 +10,21 @@ module ActiveSupport
# you can call this:
#
# Rails.env.production?
+ #
+ # == Instantiating a new StringInquirer
+ #
+ # vehicle = ActiveSupport::StringInquirer.new('car')
+ # vehicle.car? # => true
+ # vehicle.bike? # => false
class StringInquirer < String
private
def respond_to_missing?(method_name, include_private = false)
- method_name[-1] == '?'
+ (method_name[-1] == "?") || super
end
def method_missing(method_name, *arguments)
- if method_name[-1] == '?'
+ if method_name[-1] == "?"
self == method_name[0..-2]
else
super
diff --git a/activesupport/lib/active_support/subscriber.rb b/activesupport/lib/active_support/subscriber.rb
index 1cd4b807ad..f3e902f9dd 100644
--- a/activesupport/lib/active_support/subscriber.rb
+++ b/activesupport/lib/active_support/subscriber.rb
@@ -1,4 +1,7 @@
-require 'active_support/per_thread_registry'
+# frozen_string_literal: true
+
+require "active_support/per_thread_registry"
+require "active_support/notifications"
module ActiveSupport
# ActiveSupport::Subscriber is an object set to consume
@@ -23,9 +26,8 @@ module ActiveSupport
# the +sql+ method.
class Subscriber
class << self
-
# Attach the subscriber to a namespace.
- def attach_to(namespace, subscriber=new, notifier=ActiveSupport::Notifications)
+ def attach_to(namespace, subscriber = new, notifier = ActiveSupport::Notifications)
@namespace = namespace
@subscriber = subscriber
@notifier = notifier
@@ -52,21 +54,20 @@ module ActiveSupport
@@subscribers ||= []
end
- protected
-
- attr_reader :subscriber, :notifier, :namespace
+ private
+ attr_reader :subscriber, :notifier, :namespace
- def add_event_subscriber(event)
- return if %w{ start finish }.include?(event.to_s)
+ def add_event_subscriber(event) # :doc:
+ return if %w{ start finish }.include?(event.to_s)
- pattern = "#{event}.#{namespace}"
+ pattern = "#{event}.#{namespace}"
- # Don't add multiple subscribers (eg. if methods are redefined).
- return if subscriber.patterns.include?(pattern)
+ # Don't add multiple subscribers (eg. if methods are redefined).
+ return if subscriber.patterns.include?(pattern)
- subscriber.patterns << pattern
- notifier.subscribe(pattern, subscriber)
- end
+ subscriber.patterns << pattern
+ notifier.subscribe(pattern, subscriber)
+ end
end
attr_reader :patterns # :nodoc:
@@ -78,25 +79,24 @@ module ActiveSupport
end
def start(name, id, payload)
- e = ActiveSupport::Notifications::Event.new(name, Time.now, nil, id, payload)
+ event = ActiveSupport::Notifications::Event.new(name, nil, nil, id, payload)
+ event.start!
parent = event_stack.last
- parent << e if parent
+ parent << event if parent
- event_stack.push e
+ event_stack.push event
end
def finish(name, id, payload)
- finished = Time.now
- event = event_stack.pop
- event.end = finished
+ event = event_stack.pop
+ event.finish!
event.payload.merge!(payload)
- method = name.split('.'.freeze).first
+ method = name.split(".").first
send(method, event)
end
private
-
def event_stack
SubscriberQueueRegistry.instance.get_queue(@queue_key)
end
diff --git a/activesupport/lib/active_support/tagged_logging.rb b/activesupport/lib/active_support/tagged_logging.rb
index bcd7bf74c0..d8a86d997e 100644
--- a/activesupport/lib/active_support/tagged_logging.rb
+++ b/activesupport/lib/active_support/tagged_logging.rb
@@ -1,7 +1,9 @@
-require 'active_support/core_ext/module/delegation'
-require 'active_support/core_ext/object/blank'
-require 'logger'
-require 'active_support/logger'
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/delegation"
+require "active_support/core_ext/object/blank"
+require "logger"
+require "active_support/logger"
module ActiveSupport
# Wraps any standard Logger object to provide tagging capabilities.
@@ -44,22 +46,30 @@ module ActiveSupport
def current_tags
# We use our object ID here to avoid conflicting with other instances
- thread_key = @thread_key ||= "activesupport_tagged_logging_tags:#{object_id}".freeze
+ thread_key = @thread_key ||= "activesupport_tagged_logging_tags:#{object_id}"
Thread.current[thread_key] ||= []
end
- private
- def tags_text
- tags = current_tags
- if tags.any?
- tags.collect { |tag| "[#{tag}] " }.join
- end
+ def tags_text
+ tags = current_tags
+ if tags.one?
+ "[#{tags[0]}] "
+ elsif tags.any?
+ tags.collect { |tag| "[#{tag}] " }.join
end
+ end
end
def self.new(logger)
- # Ensure we set a default formatter so we aren't extending nil!
- logger.formatter ||= ActiveSupport::Logger::SimpleFormatter.new
+ logger = logger.dup
+
+ if logger.formatter
+ logger.formatter = logger.formatter.dup
+ else
+ # Ensure we set a default formatter so we aren't extending nil!
+ logger.formatter = ActiveSupport::Logger::SimpleFormatter.new
+ end
+
logger.formatter.extend Formatter
logger.extend(self)
end
diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb
index ae6f00b861..ef12c6b9b0 100644
--- a/activesupport/lib/active_support/test_case.rb
+++ b/activesupport/lib/active_support/test_case.rb
@@ -1,16 +1,17 @@
-gem 'minitest' # make sure we get the gem, not stdlib
-require 'minitest'
-require 'active_support/testing/tagged_logging'
-require 'active_support/testing/setup_and_teardown'
-require 'active_support/testing/assertions'
-require 'active_support/testing/deprecation'
-require 'active_support/testing/declarative'
-require 'active_support/testing/isolation'
-require 'active_support/testing/constant_lookup'
-require 'active_support/testing/time_helpers'
-require 'active_support/testing/file_fixtures'
-require 'active_support/testing/composite_filter'
-require 'active_support/core_ext/kernel/reporting'
+# frozen_string_literal: true
+
+gem "minitest" # make sure we get the gem, not stdlib
+require "minitest"
+require "active_support/testing/tagged_logging"
+require "active_support/testing/setup_and_teardown"
+require "active_support/testing/assertions"
+require "active_support/testing/deprecation"
+require "active_support/testing/declarative"
+require "active_support/testing/isolation"
+require "active_support/testing/constant_lookup"
+require "active_support/testing/time_helpers"
+require "active_support/testing/file_fixtures"
+require "active_support/testing/parallelization"
module ActiveSupport
class TestCase < ::Minitest::Test
@@ -40,20 +41,96 @@ module ActiveSupport
ActiveSupport.test_order ||= :random
end
- def run(reporter, options = {})
- if options[:patterns] && options[:patterns].any? { |p| p =~ /:\d+/ }
- options[:filter] = \
- Testing::CompositeFilter.new(self, options[:filter], options[:patterns])
+ # Parallelizes the test suite.
+ #
+ # Takes a +workers+ argument that controls how many times the process
+ # is forked. For each process a new database will be created suffixed
+ # with the worker number.
+ #
+ # test-database-0
+ # test-database-1
+ #
+ # If <tt>ENV["PARALLEL_WORKERS"]</tt> is set the workers argument will be ignored
+ # and the environment variable will be used instead. This is useful for CI
+ # environments, or other environments where you may need more workers than
+ # you do for local testing.
+ #
+ # If the number of workers is set to +1+ or fewer, the tests will not be
+ # parallelized.
+ #
+ # The default parallelization method is to fork processes. If you'd like to
+ # use threads instead you can pass <tt>with: :threads</tt> to the +parallelize+
+ # method. Note the threaded parallelization does not create multiple
+ # database and will not work with system tests at this time.
+ #
+ # parallelize(workers: 2, with: :threads)
+ #
+ # The threaded parallelization uses minitest's parallel executor directly.
+ # The processes parallelization uses a Ruby DRb server.
+ def parallelize(workers: 2, with: :processes)
+ workers = ENV["PARALLEL_WORKERS"].to_i if ENV["PARALLEL_WORKERS"]
+
+ return if workers <= 1
+
+ executor = case with
+ when :processes
+ Testing::Parallelization.new(workers)
+ when :threads
+ Minitest::Parallel::Executor.new(workers)
+ else
+ raise ArgumentError, "#{with} is not a supported parallelization executor."
end
- super
+ self.lock_threads = false if defined?(self.lock_threads) && with == :threads
+
+ Minitest.parallel_executor = executor
+
+ parallelize_me!
+ end
+
+ # Set up hook for parallel testing. This can be used if you have multiple
+ # databases or any behavior that needs to be run after the process is forked
+ # but before the tests run.
+ #
+ # Note: this feature is not available with the threaded parallelization.
+ #
+ # In your +test_helper.rb+ add the following:
+ #
+ # class ActiveSupport::TestCase
+ # parallelize_setup do
+ # # create databases
+ # end
+ # end
+ def parallelize_setup(&block)
+ ActiveSupport::Testing::Parallelization.after_fork_hook do |worker|
+ yield worker
+ end
+ end
+
+ # Clean up hook for parallel testing. This can be used to drop databases
+ # if your app uses multiple write/read databases or other clean up before
+ # the tests finish. This runs before the forked process is closed.
+ #
+ # Note: this feature is not available with the threaded parallelization.
+ #
+ # In your +test_helper.rb+ add the following:
+ #
+ # class ActiveSupport::TestCase
+ # parallelize_teardown do
+ # # drop databases
+ # end
+ # end
+ def parallelize_teardown(&block)
+ ActiveSupport::Testing::Parallelization.run_cleanup_hook do |worker|
+ yield worker
+ end
end
end
alias_method :method_name, :name
include ActiveSupport::Testing::TaggedLogging
- include ActiveSupport::Testing::SetupAndTeardown
+ prepend ActiveSupport::Testing::SetupAndTeardown
include ActiveSupport::Testing::Assertions
include ActiveSupport::Testing::Deprecation
include ActiveSupport::Testing::TimeHelpers
@@ -76,13 +153,6 @@ module ActiveSupport
alias :assert_not_respond_to :refute_respond_to
alias :assert_not_same :refute_same
- # Reveals the intention that the block should not raise any exception.
- #
- # assert_nothing_raised do
- # ...
- # end
- def assert_nothing_raised(*args)
- yield
- end
+ ActiveSupport.run_load_hooks(:active_support_test_case, self)
end
end
diff --git a/activesupport/lib/active_support/testing/assertions.rb b/activesupport/lib/active_support/testing/assertions.rb
index d87ce3474d..b27ac7ce99 100644
--- a/activesupport/lib/active_support/testing/assertions.rb
+++ b/activesupport/lib/active_support/testing/assertions.rb
@@ -1,9 +1,11 @@
-require 'active_support/core_ext/object/blank'
+# frozen_string_literal: true
module ActiveSupport
module Testing
module Assertions
- # Assert that an expression is not truthy. Passes if <tt>object</tt> is
+ UNTRACKED = Object.new # :nodoc:
+
+ # Asserts that an expression is not truthy. Passes if <tt>object</tt> is
# +nil+ or +false+. "Truthy" means "considered true in a conditional"
# like <tt>if foo</tt>.
#
@@ -19,6 +21,17 @@ module ActiveSupport
assert !object, message
end
+ # Assertion that the block should not raise an exception.
+ #
+ # Passes if evaluated code in the yielded block raises no exception.
+ #
+ # assert_nothing_raised do
+ # perform_service(param: 'no_exception')
+ # end
+ def assert_nothing_raised
+ yield
+ end
+
# Test numeric difference between the return value of an expression as a
# result of what is evaluated in the yielded block.
#
@@ -45,6 +58,12 @@ module ActiveSupport
# post :create, params: { article: {...} }
# end
#
+ # A hash of expressions/numeric differences can also be passed in and evaluated.
+ #
+ # assert_difference ->{ Article.count } => 1, ->{ Notification.count } => 2 do
+ # post :create, params: { article: {...} }
+ # end
+ #
# A lambda or a list of lambdas can be passed in and evaluated:
#
# assert_difference ->{ Article.count }, 2 do
@@ -60,21 +79,31 @@ module ActiveSupport
# assert_difference 'Article.count', -1, 'An Article should be destroyed' do
# post :delete, params: { id: ... }
# end
- def assert_difference(expression, difference = 1, message = nil, &block)
- expressions = Array(expression)
+ def assert_difference(expression, *args, &block)
+ expressions =
+ if expression.is_a?(Hash)
+ message = args[0]
+ expression
+ else
+ difference = args[0] || 1
+ message = args[1]
+ Hash[Array(expression).map { |e| [e, difference] }]
+ end
- exps = expressions.map { |e|
+ exps = expressions.keys.map { |e|
e.respond_to?(:call) ? e : lambda { eval(e, block.binding) }
}
before = exps.map(&:call)
- yield
+ retval = yield
- expressions.zip(exps).each_with_index do |(code, e), i|
- error = "#{code.inspect} didn't change by #{difference}"
+ expressions.zip(exps, before) do |(code, diff), exp, before_value|
+ error = "#{code.inspect} didn't change by #{diff}"
error = "#{message}.\n#{error}" if message
- assert_equal(before[i] + difference, e.call, error)
+ assert_equal(before_value + diff, exp.call, error)
end
+
+ retval
end
# Assertion that the numeric result of evaluating an expression is not
@@ -84,14 +113,116 @@ module ActiveSupport
# post :create, params: { article: invalid_attributes }
# end
#
+ # A lambda can be passed in and evaluated.
+ #
+ # assert_no_difference -> { Article.count } do
+ # post :create, params: { article: invalid_attributes }
+ # end
+ #
# An error message can be specified.
#
# assert_no_difference 'Article.count', 'An Article should not be created' do
# post :create, params: { article: invalid_attributes }
# end
+ #
+ # An array of expressions can also be passed in and evaluated.
+ #
+ # assert_no_difference [ 'Article.count', -> { Post.count } ] do
+ # post :create, params: { article: invalid_attributes }
+ # end
def assert_no_difference(expression, message = nil, &block)
assert_difference expression, 0, message, &block
end
+
+ # Assertion that the result of evaluating an expression is changed before
+ # and after invoking the passed in block.
+ #
+ # assert_changes 'Status.all_good?' do
+ # post :create, params: { status: { ok: false } }
+ # end
+ #
+ # You can pass the block as a string to be evaluated in the context of
+ # the block. A lambda can be passed for the block as well.
+ #
+ # assert_changes -> { Status.all_good? } do
+ # post :create, params: { status: { ok: false } }
+ # end
+ #
+ # The assertion is useful to test side effects. The passed block can be
+ # anything that can be converted to string with #to_s.
+ #
+ # assert_changes :@object do
+ # @object = 42
+ # end
+ #
+ # The keyword arguments :from and :to can be given to specify the
+ # expected initial value and the expected value after the block was
+ # executed.
+ #
+ # assert_changes :@object, from: nil, to: :foo do
+ # @object = :foo
+ # end
+ #
+ # An error message can be specified.
+ #
+ # assert_changes -> { Status.all_good? }, 'Expected the status to be bad' do
+ # post :create, params: { status: { incident: true } }
+ # end
+ def assert_changes(expression, message = nil, from: UNTRACKED, to: UNTRACKED, &block)
+ exp = expression.respond_to?(:call) ? expression : -> { eval(expression.to_s, block.binding) }
+
+ before = exp.call
+ retval = yield
+
+ unless from == UNTRACKED
+ error = "#{expression.inspect} isn't #{from.inspect}"
+ error = "#{message}.\n#{error}" if message
+ assert from === before, error
+ end
+
+ after = exp.call
+
+ error = "#{expression.inspect} didn't change"
+ error = "#{error}. It was already #{to}" if before == to
+ error = "#{message}.\n#{error}" if message
+ assert before != after, error
+
+ unless to == UNTRACKED
+ error = "#{expression.inspect} didn't change to as expected\n"
+ error = "#{error}Expected: #{to.inspect}\n"
+ error = "#{error} Actual: #{after.inspect}"
+ error = "#{message}.\n#{error}" if message
+ assert to === after, error
+ end
+
+ retval
+ end
+
+ # Assertion that the result of evaluating an expression is not changed before
+ # and after invoking the passed in block.
+ #
+ # assert_no_changes 'Status.all_good?' do
+ # post :create, params: { status: { ok: true } }
+ # end
+ #
+ # An error message can be specified.
+ #
+ # assert_no_changes -> { Status.all_good? }, 'Expected the status to be good' do
+ # post :create, params: { status: { ok: false } }
+ # end
+ def assert_no_changes(expression, message = nil, &block)
+ exp = expression.respond_to?(:call) ? expression : -> { eval(expression.to_s, block.binding) }
+
+ before = exp.call
+ retval = yield
+ after = exp.call
+
+ error = "#{expression.inspect} did change to #{after}"
+ error = "#{message}.\n#{error}" if message
+ assert before == after, error
+
+ retval
+ end
end
end
end
diff --git a/activesupport/lib/active_support/testing/autorun.rb b/activesupport/lib/active_support/testing/autorun.rb
index 84c6b89340..889b41659a 100644
--- a/activesupport/lib/active_support/testing/autorun.rb
+++ b/activesupport/lib/active_support/testing/autorun.rb
@@ -1,12 +1,7 @@
-gem 'minitest'
+# frozen_string_literal: true
-require 'minitest'
+gem "minitest"
-if Minitest.respond_to?(:run_with_rails_extension)
- unless Minitest.run_with_rails_extension
- Minitest.run_with_autorun = true
- Minitest.autorun
- end
-else
- Minitest.autorun
-end
+require "minitest"
+
+Minitest.autorun
diff --git a/activesupport/lib/active_support/testing/composite_filter.rb b/activesupport/lib/active_support/testing/composite_filter.rb
deleted file mode 100644
index bde723e30b..0000000000
--- a/activesupport/lib/active_support/testing/composite_filter.rb
+++ /dev/null
@@ -1,54 +0,0 @@
-require 'method_source'
-
-module ActiveSupport
- module Testing
- class CompositeFilter # :nodoc:
- def initialize(runnable, filter, patterns)
- @runnable = runnable
- @filters = [ derive_regexp(filter), *derive_line_filters(patterns) ].compact
- end
-
- def ===(method)
- @filters.any? { |filter| filter === method }
- end
-
- private
- def derive_regexp(filter)
- filter =~ %r%/(.*)/% ? Regexp.new($1) : filter
- end
-
- def derive_line_filters(patterns)
- patterns.map do |file_and_line|
- file, line = file_and_line.split(':')
- Filter.new(@runnable, file, line) if file
- end
- end
-
- class Filter # :nodoc:
- def initialize(runnable, file, line)
- @runnable, @file = runnable, File.expand_path(file)
- @line = line.to_i if line
- end
-
- def ===(method)
- return unless @runnable.method_defined?(method)
-
- if @line
- test_file, test_range = definition_for(@runnable.instance_method(method))
- test_file == @file && test_range.include?(@line)
- else
- @runnable.instance_method(method).source_location.first == @file
- end
- end
-
- private
- def definition_for(method)
- file, start_line = method.source_location
- end_line = method.source.count("\n") + start_line - 1
-
- return file, start_line..end_line
- end
- end
- end
- end
-end
diff --git a/activesupport/lib/active_support/testing/constant_lookup.rb b/activesupport/lib/active_support/testing/constant_lookup.rb
index 07d477c0db..51167e9237 100644
--- a/activesupport/lib/active_support/testing/constant_lookup.rb
+++ b/activesupport/lib/active_support/testing/constant_lookup.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "active_support/concern"
require "active_support/inflector"
@@ -44,7 +46,6 @@ module ActiveSupport
end
end
end
-
end
end
end
diff --git a/activesupport/lib/active_support/testing/declarative.rb b/activesupport/lib/active_support/testing/declarative.rb
index 0bf3643a56..7c3403684d 100644
--- a/activesupport/lib/active_support/testing/declarative.rb
+++ b/activesupport/lib/active_support/testing/declarative.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveSupport
module Testing
module Declarative
@@ -9,7 +11,7 @@ module ActiveSupport
# ...
# end
def test(name, &block)
- test_name = "test_#{name.gsub(/\s+/,'_')}".to_sym
+ test_name = "test_#{name.gsub(/\s+/, '_')}".to_sym
defined = method_defined? test_name
raise "#{test_name} is already defined in #{self}" if defined
if block_given?
diff --git a/activesupport/lib/active_support/testing/deprecation.rb b/activesupport/lib/active_support/testing/deprecation.rb
index 6c94c611b6..18d63d2780 100644
--- a/activesupport/lib/active_support/testing/deprecation.rb
+++ b/activesupport/lib/active_support/testing/deprecation.rb
@@ -1,34 +1,37 @@
-require 'active_support/deprecation'
+# frozen_string_literal: true
+
+require "active_support/deprecation"
module ActiveSupport
module Testing
module Deprecation #:nodoc:
- def assert_deprecated(match = nil, &block)
- result, warnings = collect_deprecations(&block)
+ def assert_deprecated(match = nil, deprecator = nil, &block)
+ result, warnings = collect_deprecations(deprecator, &block)
assert !warnings.empty?, "Expected a deprecation warning within the block but received none"
if match
match = Regexp.new(Regexp.escape(match)) unless match.is_a?(Regexp)
- assert warnings.any? { |w| w =~ match }, "No deprecation warning matched #{match}: #{warnings.join(', ')}"
+ assert warnings.any? { |w| match.match?(w) }, "No deprecation warning matched #{match}: #{warnings.join(', ')}"
end
result
end
- def assert_not_deprecated(&block)
- result, deprecations = collect_deprecations(&block)
+ def assert_not_deprecated(deprecator = nil, &block)
+ result, deprecations = collect_deprecations(deprecator, &block)
assert deprecations.empty?, "Expected no deprecation warning within the block but received #{deprecations.size}: \n #{deprecations * "\n "}"
result
end
- def collect_deprecations
- old_behavior = ActiveSupport::Deprecation.behavior
+ def collect_deprecations(deprecator = nil)
+ deprecator ||= ActiveSupport::Deprecation
+ old_behavior = deprecator.behavior
deprecations = []
- ActiveSupport::Deprecation.behavior = Proc.new do |message, callstack|
+ deprecator.behavior = Proc.new do |message, callstack|
deprecations << message
end
result = yield
[result, deprecations]
ensure
- ActiveSupport::Deprecation.behavior = old_behavior
+ deprecator.behavior = old_behavior
end
end
end
diff --git a/activesupport/lib/active_support/testing/file_fixtures.rb b/activesupport/lib/active_support/testing/file_fixtures.rb
index 4c6a0801b8..4eb7a88576 100644
--- a/activesupport/lib/active_support/testing/file_fixtures.rb
+++ b/activesupport/lib/active_support/testing/file_fixtures.rb
@@ -1,3 +1,7 @@
+# frozen_string_literal: true
+
+require "active_support/concern"
+
module ActiveSupport
module Testing
# Adds simple access to sample files called file fixtures.
@@ -18,7 +22,7 @@ module ActiveSupport
# Returns a +Pathname+ to the fixture file named +fixture_name+.
#
- # Raises ArgumentError if +fixture_name+ can't be found.
+ # Raises +ArgumentError+ if +fixture_name+ can't be found.
def file_fixture(fixture_name)
path = Pathname.new(File.join(file_fixture_path, fixture_name))
diff --git a/activesupport/lib/active_support/testing/isolation.rb b/activesupport/lib/active_support/testing/isolation.rb
index 1de0a19998..652a10da23 100644
--- a/activesupport/lib/active_support/testing/isolation.rb
+++ b/activesupport/lib/active_support/testing/isolation.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
module ActiveSupport
module Testing
module Isolation
- require 'thread'
+ require "thread"
def self.included(klass) #:nodoc:
klass.class_eval do
@@ -13,17 +15,6 @@ module ActiveSupport
!ENV["NO_FORK"] && Process.respond_to?(:fork)
end
- @@class_setup_mutex = Mutex.new
-
- def _run_class_setup # class setup method should only happen in parent
- @@class_setup_mutex.synchronize do
- unless defined?(@@ran_class_setup) || ENV['ISOLATION_TEST']
- self.class.setup if self.class.respond_to?(:setup)
- @@ran_class_setup = true
- end
- end
- end
-
def run
serialized = run_in_isolation do
super
@@ -41,14 +32,31 @@ module ActiveSupport
pid = fork do
read.close
yield
- write.puts [Marshal.dump(self.dup)].pack("m")
+ begin
+ if error?
+ failures.map! { |e|
+ begin
+ Marshal.dump e
+ e
+ rescue TypeError
+ ex = Exception.new e.message
+ ex.set_backtrace e.backtrace
+ Minitest::UnexpectedError.new ex
+ end
+ }
+ end
+ test_result = defined?(Minitest::Result) ? Minitest::Result.from(self) : dup
+ result = Marshal.dump(test_result)
+ end
+
+ write.puts [result].pack("m")
exit!
end
write.close
result = read.read
Process.wait2(pid)
- return result.unpack("m")[0]
+ result.unpack1("m")
end
end
@@ -62,24 +70,27 @@ module ActiveSupport
if ENV["ISOLATION_TEST"]
yield
+ test_result = defined?(Minitest::Result) ? Minitest::Result.from(self) : dup
File.open(ENV["ISOLATION_OUTPUT"], "w") do |file|
- file.puts [Marshal.dump(self.dup)].pack("m")
+ file.puts [Marshal.dump(test_result)].pack("m")
end
exit!
else
Tempfile.open("isolation") do |tmpfile|
env = {
- 'ISOLATION_TEST' => self.class.name,
- 'ISOLATION_OUTPUT' => tmpfile.path
+ "ISOLATION_TEST" => self.class.name,
+ "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}##{self.name}"
- command = "#{Gem.ruby} #{load_paths} #{$0} '#{orig_args}' #{test_opts}"
+ test_opts = "-n#{self.class.name}##{name}"
+
+ load_path_args = []
+ $-I.each do |p|
+ load_path_args << "-I"
+ load_path_args << File.expand_path(p)
+ end
- # IO.popen lets us pass env in a cross-platform way
- child = IO.popen(env, command)
+ child = IO.popen([env, Gem.ruby, *load_path_args, $0, *ORIG_ARGV, test_opts])
begin
Process.wait(child.pid)
@@ -87,7 +98,7 @@ module ActiveSupport
nil
end
- return tmpfile.read.unpack("m")[0]
+ return tmpfile.read.unpack1("m")
end
end
end
diff --git a/activesupport/lib/active_support/testing/method_call_assertions.rb b/activesupport/lib/active_support/testing/method_call_assertions.rb
index e3cbe40308..eba41aa907 100644
--- a/activesupport/lib/active_support/testing/method_call_assertions.rb
+++ b/activesupport/lib/active_support/testing/method_call_assertions.rb
@@ -1,11 +1,15 @@
+# frozen_string_literal: true
+
+require "minitest/mock"
+
module ActiveSupport
module Testing
module MethodCallAssertions # :nodoc:
private
- def assert_called(object, method_name, message = nil, times: 1)
+ def assert_called(object, method_name, message = nil, times: 1, returns: nil)
times_called = 0
- object.stub(method_name, -> { times_called += 1 }) { yield }
+ object.stub(method_name, proc { times_called += 1; returns }) { yield }
error = "Expected #{method_name} to be called #{times} times, " \
"but was called #{times_called} times"
@@ -13,7 +17,7 @@ module ActiveSupport
assert_equal times, times_called, error
end
- def assert_called_with(object, method_name, args = [], returns: nil)
+ def assert_called_with(object, method_name, args, returns: nil)
mock = Minitest::Mock.new
if args.all? { |arg| arg.is_a?(Array) }
@@ -30,6 +34,39 @@ module ActiveSupport
def assert_not_called(object, method_name, message = nil, &block)
assert_called(object, method_name, message, times: 0, &block)
end
+
+ # TODO: No need to resort to #send once support for Ruby 2.4 is
+ # dropped.
+ def assert_called_on_instance_of(klass, method_name, message = nil, times: 1, returns: nil)
+ times_called = 0
+ klass.send(:define_method, "stubbed_#{method_name}") do |*|
+ times_called += 1
+
+ returns
+ end
+
+ klass.send(:alias_method, "original_#{method_name}", method_name)
+ klass.send(:alias_method, method_name, "stubbed_#{method_name}")
+
+ yield
+
+ error = "Expected #{method_name} to be called #{times} times, but was called #{times_called} times"
+ error = "#{message}.\n#{error}" if message
+
+ assert_equal times, times_called, error
+ ensure
+ klass.send(:alias_method, method_name, "original_#{method_name}")
+ klass.send(:undef_method, "original_#{method_name}")
+ klass.send(:undef_method, "stubbed_#{method_name}")
+ end
+
+ def assert_not_called_on_instance_of(klass, method_name, message = nil, &block)
+ assert_called_on_instance_of(klass, method_name, message, times: 0, &block)
+ end
+
+ def stub_any_instance(klass, instance: klass.new)
+ klass.stub(:new, instance) { yield instance }
+ end
end
end
-end \ No newline at end of file
+end
diff --git a/activesupport/lib/active_support/testing/parallelization.rb b/activesupport/lib/active_support/testing/parallelization.rb
new file mode 100644
index 0000000000..8de01eb19b
--- /dev/null
+++ b/activesupport/lib/active_support/testing/parallelization.rb
@@ -0,0 +1,110 @@
+# frozen_string_literal: true
+
+require "drb"
+require "drb/unix" unless Gem.win_platform?
+require "active_support/core_ext/module/attribute_accessors"
+
+module ActiveSupport
+ module Testing
+ class Parallelization # :nodoc:
+ class Server
+ include DRb::DRbUndumped
+
+ def initialize
+ @queue = Queue.new
+ end
+
+ def record(reporter, result)
+ raise DRb::DRbConnError if result.is_a?(DRb::DRbUnknown)
+
+ reporter.synchronize do
+ reporter.record(result)
+ end
+ end
+
+ def <<(o)
+ @queue << o
+ end
+
+ def pop; @queue.pop; end
+ end
+
+ @@after_fork_hooks = []
+
+ def self.after_fork_hook(&blk)
+ @@after_fork_hooks << blk
+ end
+
+ cattr_reader :after_fork_hooks
+
+ @@run_cleanup_hooks = []
+
+ def self.run_cleanup_hook(&blk)
+ @@run_cleanup_hooks << blk
+ end
+
+ cattr_reader :run_cleanup_hooks
+
+ def initialize(queue_size)
+ @queue_size = queue_size
+ @queue = Server.new
+ @pool = []
+
+ @url = DRb.start_service("drbunix:", @queue).uri
+ end
+
+ def after_fork(worker)
+ self.class.after_fork_hooks.each do |cb|
+ cb.call(worker)
+ end
+ end
+
+ def run_cleanup(worker)
+ self.class.run_cleanup_hooks.each do |cb|
+ cb.call(worker)
+ end
+ end
+
+ def start
+ @pool = @queue_size.times.map do |worker|
+ fork do
+ begin
+ DRb.stop_service
+
+ after_fork(worker)
+
+ queue = DRbObject.new_with_uri(@url)
+
+ while job = queue.pop
+ klass = job[0]
+ method = job[1]
+ reporter = job[2]
+ result = Minitest.run_one_method(klass, method)
+
+ begin
+ queue.record(reporter, result)
+ rescue DRb::DRbConnError
+ result.failures.each do |failure|
+ failure.exception = DRb::DRbRemoteError.new(failure.exception)
+ end
+ queue.record(reporter, result)
+ end
+ end
+ ensure
+ run_cleanup(worker)
+ end
+ end
+ end
+ end
+
+ def <<(work)
+ @queue << work
+ end
+
+ def shutdown
+ @queue_size.times { @queue << nil }
+ @pool.each { |pid| Process.waitpid pid }
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/testing/setup_and_teardown.rb b/activesupport/lib/active_support/testing/setup_and_teardown.rb
index 33f2b8dc9b..35321cd157 100644
--- a/activesupport/lib/active_support/testing/setup_and_teardown.rb
+++ b/activesupport/lib/active_support/testing/setup_and_teardown.rb
@@ -1,5 +1,6 @@
-require 'active_support/concern'
-require 'active_support/callbacks'
+# frozen_string_literal: true
+
+require "active_support/callbacks"
module ActiveSupport
module Testing
@@ -17,11 +18,10 @@ module ActiveSupport
# end
# end
module SetupAndTeardown
- extend ActiveSupport::Concern
-
- included do
- include ActiveSupport::Callbacks
- define_callbacks :setup, :teardown
+ def self.prepended(klass)
+ klass.include ActiveSupport::Callbacks
+ klass.define_callbacks :setup, :teardown
+ klass.extend ClassMethods
end
module ClassMethods
@@ -42,7 +42,12 @@ module ActiveSupport
end
def after_teardown # :nodoc:
- run_callbacks :teardown
+ begin
+ run_callbacks :teardown
+ rescue => e
+ self.failures << Minitest::UnexpectedError.new(e)
+ end
+
super
end
end
diff --git a/activesupport/lib/active_support/testing/stream.rb b/activesupport/lib/active_support/testing/stream.rb
index 895192ad05..127cfe1e12 100644
--- a/activesupport/lib/active_support/testing/stream.rb
+++ b/activesupport/lib/active_support/testing/stream.rb
@@ -1,42 +1,44 @@
+# frozen_string_literal: true
+
module ActiveSupport
module Testing
module Stream #:nodoc:
private
- def silence_stream(stream)
- old_stream = stream.dup
- stream.reopen(IO::NULL)
- stream.sync = true
- yield
- ensure
- stream.reopen(old_stream)
- old_stream.close
- end
+ def silence_stream(stream)
+ old_stream = stream.dup
+ stream.reopen(IO::NULL)
+ stream.sync = true
+ yield
+ ensure
+ stream.reopen(old_stream)
+ old_stream.close
+ end
- def quietly
- silence_stream(STDOUT) do
- silence_stream(STDERR) do
- yield
+ def quietly
+ silence_stream(STDOUT) do
+ silence_stream(STDERR) do
+ yield
+ end
end
end
- end
- def capture(stream)
- stream = stream.to_s
- captured_stream = Tempfile.new(stream)
- stream_io = eval("$#{stream}")
- origin_stream = stream_io.dup
- stream_io.reopen(captured_stream)
+ def capture(stream)
+ stream = stream.to_s
+ captured_stream = Tempfile.new(stream)
+ stream_io = eval("$#{stream}")
+ origin_stream = stream_io.dup
+ stream_io.reopen(captured_stream)
- yield
+ yield
- stream_io.rewind
- return captured_stream.read
- ensure
- captured_stream.close
- captured_stream.unlink
- stream_io.reopen(origin_stream)
- end
+ stream_io.rewind
+ captured_stream.read
+ ensure
+ captured_stream.close
+ captured_stream.unlink
+ stream_io.reopen(origin_stream)
+ end
end
end
end
diff --git a/activesupport/lib/active_support/testing/tagged_logging.rb b/activesupport/lib/active_support/testing/tagged_logging.rb
index 843ce4a867..9ca50c7918 100644
--- a/activesupport/lib/active_support/testing/tagged_logging.rb
+++ b/activesupport/lib/active_support/testing/tagged_logging.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveSupport
module Testing
# Logs a "PostsControllerTest: test name" heading before each test to
@@ -8,7 +10,7 @@ module ActiveSupport
def before_setup
if tagged_logger && tagged_logger.info?
heading = "#{self.class}: #{name}"
- divider = '-' * heading.size
+ divider = "-" * heading.size
tagged_logger.info divider
tagged_logger.info heading
tagged_logger.info divider
diff --git a/activesupport/lib/active_support/testing/time_helpers.rb b/activesupport/lib/active_support/testing/time_helpers.rb
index fca0947c5b..f160e66971 100644
--- a/activesupport/lib/active_support/testing/time_helpers.rb
+++ b/activesupport/lib/active_support/testing/time_helpers.rb
@@ -1,39 +1,49 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/redefine_method"
+require "active_support/core_ext/time/calculations"
+require "concurrent/map"
+
module ActiveSupport
module Testing
class SimpleStubs # :nodoc:
Stub = Struct.new(:object, :method_name, :original_method)
def initialize
- @stubs = {}
+ @stubs = Concurrent::Map.new { |h, k| h[k] = {} }
end
- def stub_object(object, method_name, return_value)
- key = [object.object_id, method_name]
-
- if stub = @stubs[key]
+ def stub_object(object, method_name, &block)
+ if stub = stubbing(object, method_name)
unstub_object(stub)
end
new_name = "__simple_stub__#{method_name}"
- @stubs[key] = Stub.new(object, method_name, new_name)
+ @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!
- @stubs.each_value do |stub|
- unstub_object(stub)
+ @stubs.each_value do |object_stubs|
+ object_stubs.each_value do |stub|
+ unstub_object(stub)
+ end
end
- @stubs = {}
+ @stubs.clear
+ end
+
+ def stubbing(object, method_name)
+ @stubs[object.object_id][method_name]
end
private
def unstub_object(stub)
singleton_class = stub.object.singleton_class
- singleton_class.send :undef_method, stub.method_name
+ singleton_class.send :silence_redefinition_of_method, stub.method_name
singleton_class.send :alias_method, stub.method_name, stub.original_method
singleton_class.send :undef_method, stub.original_method
end
@@ -41,8 +51,14 @@ module ActiveSupport
# Contains helpers that help you test passage of time.
module TimeHelpers
+ def after_teardown
+ travel_back
+ super
+ end
+
# Changes current time to the time in the future or in the past by a given time difference by
- # stubbing +Time.now+, +Date.today+, and +DateTime.now+.
+ # stubbing +Time.now+, +Date.today+, and +DateTime.now+. The stubs are automatically removed
+ # at the end of the test.
#
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
# travel 1.day
@@ -64,9 +80,10 @@ module ActiveSupport
# Changes current time to the given time by stubbing +Time.now+,
# +Date.today+, and +DateTime.now+ to return the time or date passed into this method.
+ # The stubs are automatically removed at the end of the test.
#
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
- # travel_to Time.new(2004, 11, 24, 01, 04, 44)
+ # travel_to Time.zone.local(2004, 11, 24, 01, 04, 44)
# Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
# Date.current # => Wed, 24 Nov 2004
# DateTime.current # => Wed, 24 Nov 2004 01:04:44 -0500
@@ -88,20 +105,48 @@ module ActiveSupport
# state at the end of the block:
#
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
- # travel_to Time.new(2004, 11, 24, 01, 04, 44) do
+ # travel_to Time.zone.local(2004, 11, 24, 01, 04, 44) do
# Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
# end
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
def travel_to(date_or_time)
+ if block_given? && simple_stubs.stubbing(Time, :now)
+ travel_to_nested_block_call = <<~MSG
+
+ Calling `travel_to` with a block, when we have previously already made a call to `travel_to`, 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
+
+ MSG
+ raise travel_to_nested_block_call
+ end
+
if date_or_time.is_a?(Date) && !date_or_time.is_a?(DateTime)
now = date_or_time.midnight.to_time
else
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
@@ -113,16 +158,37 @@ module ActiveSupport
end
# Returns the current time back to its original state, by removing the stubs added by
- # `travel` and `travel_to`.
+ # +travel+, +travel_to+, and +freeze_time+.
#
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
- # travel_to Time.new(2004, 11, 24, 01, 04, 44)
+ # travel_to Time.zone.local(2004, 11, 24, 01, 04, 44)
# Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
# travel_back
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
def travel_back
simple_stubs.unstub_all!
end
+ alias_method :unfreeze_time, :travel_back
+
+ # Calls +travel_to+ with +Time.now+.
+ #
+ # Time.current # => Sun, 09 Jul 2017 15:34:49 EST -05:00
+ # freeze_time
+ # sleep(1)
+ # Time.current # => Sun, 09 Jul 2017 15:34:49 EST -05:00
+ #
+ # This method also accepts a block, which will return the current time back to its original
+ # state at the end of the block:
+ #
+ # Time.current # => Sun, 09 Jul 2017 15:34:49 EST -05:00
+ # freeze_time do
+ # sleep(1)
+ # User.create.created_at # => Sun, 09 Jul 2017 15:34:49 EST -05:00
+ # end
+ # Time.current # => Sun, 09 Jul 2017 15:34:50 EST -05:00
+ def freeze_time(&block)
+ travel_to Time.now, &block
+ end
private
diff --git a/activesupport/lib/active_support/time.rb b/activesupport/lib/active_support/time.rb
index ea2d3391bd..51854675bf 100644
--- a/activesupport/lib/active_support/time.rb
+++ b/activesupport/lib/active_support/time.rb
@@ -1,18 +1,20 @@
+# frozen_string_literal: true
+
module ActiveSupport
- autoload :Duration, 'active_support/duration'
- autoload :TimeWithZone, 'active_support/time_with_zone'
- autoload :TimeZone, 'active_support/values/time_zone'
+ autoload :Duration, "active_support/duration"
+ autoload :TimeWithZone, "active_support/time_with_zone"
+ autoload :TimeZone, "active_support/values/time_zone"
end
-require 'date'
-require 'time'
+require "date"
+require "time"
-require 'active_support/core_ext/time'
-require 'active_support/core_ext/date'
-require 'active_support/core_ext/date_time'
+require "active_support/core_ext/time"
+require "active_support/core_ext/date"
+require "active_support/core_ext/date_time"
-require 'active_support/core_ext/integer/time'
-require 'active_support/core_ext/numeric/time'
+require "active_support/core_ext/integer/time"
+require "active_support/core_ext/numeric/time"
-require 'active_support/core_ext/string/conversions'
-require 'active_support/core_ext/string/zones'
+require "active_support/core_ext/string/conversions"
+require "active_support/core_ext/string/zones"
diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb
index 412c72d27c..3be5f6f7b5 100644
--- a/activesupport/lib/active_support/time_with_zone.rb
+++ b/activesupport/lib/active_support/time_with_zone.rb
@@ -1,6 +1,9 @@
-require 'active_support/duration'
-require 'active_support/values/time_zone'
-require 'active_support/core_ext/object/acts_like'
+# frozen_string_literal: true
+
+require "active_support/duration"
+require "active_support/values/time_zone"
+require "active_support/core_ext/object/acts_like"
+require "active_support/core_ext/date_and_time/compatibility"
module ActiveSupport
# A Time-like class that can represent a time in any time zone. Necessary
@@ -14,7 +17,7 @@ module ActiveSupport
# Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)'
# Time.zone.local(2007, 2, 10, 15, 30, 45) # => Sat, 10 Feb 2007 15:30:45 EST -05:00
# Time.zone.parse('2007-02-10 15:30:45') # => Sat, 10 Feb 2007 15:30:45 EST -05:00
- # Time.zone.at(1170361845) # => Sat, 10 Feb 2007 15:30:45 EST -05:00
+ # Time.zone.at(1171139445) # => Sat, 10 Feb 2007 15:30:45 EST -05:00
# Time.zone.now # => Sun, 18 May 2008 13:07:55 EDT -04:00
# Time.utc(2007, 2, 10, 20, 30, 45).in_time_zone # => Sat, 10 Feb 2007 15:30:45 EST -05:00
#
@@ -35,26 +38,29 @@ module ActiveSupport
# t.is_a?(Time) # => true
# t.is_a?(ActiveSupport::TimeWithZone) # => true
class TimeWithZone
-
# Report class name as 'Time' to thwart type checking.
def self.name
- 'Time'
+ "Time"
end
- include Comparable
+ PRECISIONS = Hash.new { |h, n| h[n] = "%FT%T.%#{n}N" }
+ PRECISIONS[0] = "%FT%T"
+
+ include Comparable, DateAndTime::Compatibility
attr_reader :time_zone
def initialize(utc_time, time_zone, local_time = nil, period = nil)
- @utc, @time_zone, @time = utc_time, time_zone, local_time
+ @utc = utc_time ? transfer_time_values_to_utc_constructor(utc_time) : nil
+ @time_zone, @time = time_zone, local_time
@period = @utc ? period : get_period_and_ensure_valid_local_time(period)
end
- # Returns a Time or DateTime instance that represents the time in +time_zone+.
+ # Returns a <tt>Time</tt> instance that represents the time in +time_zone+.
def time
@time ||= period.to_local(@utc)
end
- # Returns a Time or DateTime instance that represents the time in UTC.
+ # Returns a <tt>Time</tt> instance of the simultaneous time in the UTC timezone.
def utc
@utc ||= period.to_utc(@time)
end
@@ -74,10 +80,9 @@ module ActiveSupport
utc.in_time_zone(new_zone)
end
- # Returns a <tt>Time.local()</tt> instance of the simultaneous time in your
- # system's <tt>ENV['TZ']</tt> zone.
+ # Returns a <tt>Time</tt> instance of the simultaneous time in the system timezone.
def localtime(utc_offset = nil)
- utc.respond_to?(:getlocal) ? utc.getlocal(utc_offset) : utc.to_time.getlocal(utc_offset)
+ utc.getlocal(utc_offset)
end
alias_method :getlocal, :localtime
@@ -99,7 +104,7 @@ module ActiveSupport
# Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)'
# Time.zone.now.utc? # => false
def utc?
- time_zone.name == 'UTC'
+ period.offset.abbreviation == :UTC || period.offset.abbreviation == :UCT
end
alias_method :gmt?, :utc?
@@ -130,9 +135,9 @@ module ActiveSupport
period.zone_identifier.to_s
end
- # Returns a string of the object's date, time, zone and offset from UTC.
+ # Returns a string of the object's date, time, zone, and offset from UTC.
#
- # Time.zone.now.httpdate # => "Thu, 04 Dec 2014 11:00:25 EST -05:00"
+ # Time.zone.now.inspect # => "Thu, 04 Dec 2014 11:00:25 EST -05:00"
def inspect
"#{time.strftime('%a, %d %b %Y %H:%M:%S')} #{zone} #{formatted_offset}"
end
@@ -142,13 +147,10 @@ module ActiveSupport
#
# Time.zone.now.xmlschema # => "2014-12-04T11:02:37-05:00"
def xmlschema(fraction_digits = 0)
- fraction = if fraction_digits.to_i > 0
- (".%06i" % time.usec)[0, fraction_digits.to_i + 1]
- end
-
- "#{time.strftime("%Y-%m-%dT%H:%M:%S")}#{fraction}#{formatted_offset(true, 'Z')}"
+ "#{time.strftime(PRECISIONS[fraction_digits.to_i])}#{formatted_offset(true, 'Z')}"
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
@@ -171,12 +173,12 @@ module ActiveSupport
end
def init_with(coder) #:nodoc:
- initialize(coder['utc'], coder['zone'], coder['time'])
+ initialize(coder["utc"], coder["zone"], coder["time"])
end
def encode_with(coder) #:nodoc:
- coder.tag = '!ruby/object:ActiveSupport::TimeWithZone'
- coder.map = { 'utc' => utc, 'zone' => time_zone, 'time' => time }
+ coder.tag = "!ruby/object:ActiveSupport::TimeWithZone"
+ coder.map = { "utc" => utc, "zone" => time_zone, "time" => time }
end
# Returns a string of the object's date and time in the format used by
@@ -223,6 +225,8 @@ module ActiveSupport
def <=>(other)
utc <=> other
end
+ alias_method :before?, :<
+ alias_method :after?, :>
# Returns true if the current object's time is within the specified
# +min+ and +max+ time.
@@ -246,8 +250,9 @@ module ActiveSupport
utc.future?
end
+ # Returns +true+ if +other+ is equal to current object.
def eql?(other)
- utc.eql?(other)
+ other.eql?(utc)
end
def hash
@@ -279,13 +284,16 @@ module ActiveSupport
end
end
alias_method :since, :+
+ alias_method :in, :+
- # Returns a new TimeWithZone object that represents the difference between
- # the current object's time and the +other+ time.
+ # Subtracts an interval of time and returns a new TimeWithZone object unless
+ # the other value `acts_like?` time. Then it will return a Float of the difference
+ # between the two times that represents the difference between the current
+ # object's time and the +other+ time.
#
# Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)'
- # now = Time.zone.now # => Sun, 02 Nov 2014 01:26:28 EST -05:00
- # now - 1000 # => Sun, 02 Nov 2014 01:09:48 EST -05:00
+ # now = Time.zone.now # => Mon, 03 Nov 2014 00:26:28 EST -05:00
+ # now - 1000 # => Mon, 03 Nov 2014 00:09:48 EST -05:00
#
# If subtracting a Duration of variable length (i.e., years, months, days),
# move backward from #time, otherwise move backward from #utc, for accuracy
@@ -294,8 +302,14 @@ module ActiveSupport
# For instance, a time - 24.hours will go subtract exactly 24 hours, while a
# time - 1.day will subtract 23-25 hours, depending on the day.
#
- # now - 24.hours # => Sat, 01 Nov 2014 02:26:28 EDT -04:00
- # now - 1.day # => Sat, 01 Nov 2014 01:26:28 EDT -04:00
+ # now - 24.hours # => Sun, 02 Nov 2014 01:26:28 EDT -04:00
+ # now - 1.day # => Sun, 02 Nov 2014 00:26:28 EDT -04:00
+ #
+ # If both the TimeWithZone object and the other value act like Time, a Float
+ # will be returned.
+ #
+ # Time.zone.now - 1.day.ago # => 86399.999967
+ #
def -(other)
if other.acts_like?(:time)
to_time - other.to_time
@@ -307,10 +321,84 @@ module ActiveSupport
end
end
+ # Subtracts an interval of time from the current object's time and returns
+ # the result as a new TimeWithZone object.
+ #
+ # Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)'
+ # now = Time.zone.now # => Mon, 03 Nov 2014 00:26:28 EST -05:00
+ # now.ago(1000) # => Mon, 03 Nov 2014 00:09:48 EST -05:00
+ #
+ # If we're subtracting a Duration of variable length (i.e., years, months,
+ # days), move backward from #time, otherwise move backward from #utc, for
+ # accuracy when moving across DST boundaries.
+ #
+ # For instance, <tt>time.ago(24.hours)</tt> will move back exactly 24 hours,
+ # while <tt>time.ago(1.day)</tt> will move back 23-25 hours, depending on
+ # the day.
+ #
+ # now.ago(24.hours) # => Sun, 02 Nov 2014 01:26:28 EDT -04:00
+ # now.ago(1.day) # => Sun, 02 Nov 2014 00:26:28 EDT -04:00
def ago(other)
since(-other)
end
+ # Returns a new +ActiveSupport::TimeWithZone+ where one or more of the elements have
+ # been changed according to the +options+ parameter. The time options (<tt>:hour</tt>,
+ # <tt>:min</tt>, <tt>:sec</tt>, <tt>:usec</tt>, <tt>:nsec</tt>) reset cascadingly,
+ # so if only the hour is passed, then minute, sec, usec and nsec is set to 0. If the
+ # hour and minute is passed, then sec, usec and nsec is set to 0. The +options+
+ # parameter takes a hash with any of these keys: <tt>:year</tt>, <tt>:month</tt>,
+ # <tt>:day</tt>, <tt>:hour</tt>, <tt>:min</tt>, <tt>:sec</tt>, <tt>:usec</tt>,
+ # <tt>:nsec</tt>, <tt>:offset</tt>, <tt>:zone</tt>. Pass either <tt>:usec</tt>
+ # or <tt>:nsec</tt>, not both. Similarly, pass either <tt>:zone</tt> or
+ # <tt>:offset</tt>, not both.
+ #
+ # t = Time.zone.now # => Fri, 14 Apr 2017 11:45:15 EST -05:00
+ # t.change(year: 2020) # => Tue, 14 Apr 2020 11:45:15 EST -05:00
+ # t.change(hour: 12) # => Fri, 14 Apr 2017 12:00:00 EST -05:00
+ # t.change(min: 30) # => Fri, 14 Apr 2017 11:30:00 EST -05:00
+ # t.change(offset: "-10:00") # => Fri, 14 Apr 2017 11:45:15 HST -10:00
+ # t.change(zone: "Hawaii") # => Fri, 14 Apr 2017 11:45:15 HST -10:00
+ def change(options)
+ if options[:zone] && options[:offset]
+ raise ArgumentError, "Can't change both :offset and :zone at the same time: #{options.inspect}"
+ end
+
+ new_time = time.change(options)
+
+ if options[:zone]
+ new_zone = ::Time.find_zone(options[:zone])
+ elsif options[:offset]
+ new_zone = ::Time.find_zone(new_time.utc_offset)
+ end
+
+ new_zone ||= time_zone
+ periods = new_zone.periods_for_local(new_time)
+
+ self.class.new(nil, new_zone, new_time, periods.include?(period) ? period : nil)
+ end
+
+ # Uses Date to provide precise Time calculations for years, months, and days
+ # according to the proleptic Gregorian calendar. The result is returned as a
+ # new TimeWithZone object.
+ #
+ # The +options+ parameter takes a hash with any of these keys:
+ # <tt>:years</tt>, <tt>:months</tt>, <tt>:weeks</tt>, <tt>:days</tt>,
+ # <tt>:hours</tt>, <tt>:minutes</tt>, <tt>:seconds</tt>.
+ #
+ # If advancing by a value of variable length (i.e., years, weeks, months,
+ # days), move forward from #time, otherwise move forward from #utc, for
+ # accuracy when moving across DST boundaries.
+ #
+ # Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)'
+ # now = Time.zone.now # => Sun, 02 Nov 2014 01:26:28 EDT -04:00
+ # now.advance(seconds: 1) # => Sun, 02 Nov 2014 01:26:29 EDT -04:00
+ # now.advance(minutes: 1) # => Sun, 02 Nov 2014 01:27:28 EDT -04:00
+ # now.advance(hours: 1) # => Sun, 02 Nov 2014 01:26:28 EST -05:00
+ # now.advance(days: 1) # => Mon, 03 Nov 2014 01:26:28 EST -05:00
+ # now.advance(weeks: 1) # => Sun, 09 Nov 2014 01:26:28 EST -05:00
+ # now.advance(months: 1) # => Tue, 02 Dec 2014 01:26:28 EST -05:00
+ # now.advance(years: 1) # => Mon, 02 Nov 2015 01:26:28 EST -05:00
def advance(options)
# If we're advancing a value of variable length (i.e., years, weeks, months, days), advance from #time,
# otherwise advance from #utc, for accuracy when moving across DST boundaries
@@ -329,6 +417,11 @@ module ActiveSupport
EOV
end
+ # Returns Array of parts of Time in sequence of
+ # [seconds, minutes, hours, day, month, year, weekday, yearday, dst?, zone].
+ #
+ # now = Time.zone.now # => Tue, 18 Aug 2015 02:29:27 UTC +00:00
+ # now.to_a # => [27, 29, 2, 18, 8, 2015, 2, 230, false, "UTC"]
def to_a
[time.sec, time.min, time.hour, time.day, time.mon, time.year, time.wday, time.yday, dst?, zone]
end
@@ -358,13 +451,23 @@ module ActiveSupport
utc.to_r
end
- # Return an instance of Time in the system timezone.
- def to_time
- utc.to_time
+ # Returns an instance of DateTime with the timezone's UTC offset
+ #
+ # Time.zone.now.to_datetime # => Tue, 18 Aug 2015 02:32:20 +0000
+ # Time.current.in_time_zone('Hawaii').to_datetime # => Mon, 17 Aug 2015 16:32:20 -1000
+ def to_datetime
+ @to_datetime ||= utc.to_datetime.new_offset(Rational(utc_offset, 86_400))
end
- def to_datetime
- utc.to_datetime.new_offset(Rational(utc_offset, 86_400))
+ # 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>.
@@ -378,8 +481,14 @@ module ActiveSupport
end
alias_method :kind_of?, :is_a?
+ # An instance of ActiveSupport::TimeWithZone is never blank
+ def blank?
+ false
+ 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
@@ -402,7 +511,6 @@ module ActiveSupport
# Ensure proxy class responds to all methods that underlying time instance
# responds to.
def respond_to_missing?(sym, include_priv)
- # consistently respond false to acts_like?(:date), regardless of whether #time is a Time or DateTime
return false if sym.to_sym == :acts_like_date?
time.respond_to?(sym, include_priv)
end
@@ -412,7 +520,7 @@ module ActiveSupport
def method_missing(sym, *args, &block)
wrap_with_time_zone time.__send__(sym, *args, &block)
rescue NoMethodError => e
- raise e, e.message.sub(time.inspect, self.inspect), e.backtrace
+ raise e, e.message.sub(time.inspect, inspect), e.backtrace
end
private
@@ -430,11 +538,13 @@ module ActiveSupport
end
def transfer_time_values_to_utc_constructor(time)
- ::Time.utc(time.year, time.month, time.day, time.hour, time.min, time.sec, Rational(time.nsec, 1000))
+ # avoid creating another Time object if possible
+ return time if time.instance_of?(::Time) && time.utc?
+ ::Time.utc(time.year, time.month, time.day, time.hour, time.min, time.sec + time.subsec)
end
def duration_of_variable_length?(obj)
- ActiveSupport::Duration === obj && obj.parts.any? {|p| [:years, :months, :days].include?(p[0]) }
+ ActiveSupport::Duration === obj && obj.parts.any? { |p| [:years, :months, :weeks, :days].include?(p[0]) }
end
def wrap_with_time_zone(time)
diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb
index 2699a064d7..d9e033e23b 100644
--- a/activesupport/lib/active_support/values/time_zone.rb
+++ b/activesupport/lib/active_support/values/time_zone.rb
@@ -1,13 +1,13 @@
-require 'tzinfo'
-require 'thread_safe'
-require 'active_support/core_ext/object/blank'
-require 'active_support/core_ext/object/try'
+# frozen_string_literal: true
+
+require "tzinfo"
+require "concurrent/map"
module ActiveSupport
# The TimeZone class serves as a wrapper around TZInfo::Timezone instances.
# It allows us to do the following:
#
- # * Limit the set of zones provided by TZInfo to a meaningful subset of 146
+ # * Limit the set of zones provided by TZInfo to a meaningful subset of 134
# zones.
# * Retrieve and display zones with a friendlier name
# (e.g., "Eastern Time (US & Canada)" instead of "America/New_York").
@@ -23,19 +23,13 @@ module ActiveSupport
# config.time_zone = 'Eastern Time (US & Canada)'
# end
#
- # Time.zone # => #<TimeZone:0x514834...>
+ # Time.zone # => #<ActiveSupport::TimeZone:0x514834...>
# Time.zone.name # => "Eastern Time (US & Canada)"
# Time.zone.now # => Sun, 18 May 2008 14:30:44 EDT -04:00
- #
- # The version of TZInfo bundled with Active Support only includes the
- # definitions necessary to support the zones defined by the TimeZone class.
- # If you need to use zones that aren't defined by TimeZone, you'll need to
- # install the TZInfo gem (if a recent version of the gem is installed locally,
- # this will be used instead of the bundled version.)
class TimeZone
# Keys are Rails TimeZone names, values are TZInfo identifiers.
MAPPING = {
- "International Date Line West" => "Pacific/Midway",
+ "International Date Line West" => "Etc/GMT+12",
"Midway Island" => "Pacific/Midway",
"American Samoa" => "Pacific/Pago_Pago",
"Hawaii" => "Pacific/Honolulu",
@@ -66,6 +60,7 @@ module ActiveSupport
"Buenos Aires" => "America/Argentina/Buenos_Aires",
"Montevideo" => "America/Montevideo",
"Georgetown" => "America/Guyana",
+ "Puerto Rico" => "America/Puerto_Rico",
"Greenland" => "America/Godthab",
"Mid-Atlantic" => "Atlantic/South_Georgia",
"Azores" => "Atlantic/Azores",
@@ -92,7 +87,8 @@ module ActiveSupport
"Paris" => "Europe/Paris",
"Amsterdam" => "Europe/Amsterdam",
"Berlin" => "Europe/Berlin",
- "Bern" => "Europe/Berlin",
+ "Bern" => "Europe/Zurich",
+ "Zurich" => "Europe/Zurich",
"Rome" => "Europe/Rome",
"Stockholm" => "Europe/Stockholm",
"Vienna" => "Europe/Vienna",
@@ -186,19 +182,21 @@ module ActiveSupport
"Samoa" => "Pacific/Apia"
}
- UTC_OFFSET_WITH_COLON = '%s%02d:%02d'
- UTC_OFFSET_WITHOUT_COLON = UTC_OFFSET_WITH_COLON.tr(':', '')
+ UTC_OFFSET_WITH_COLON = "%s%02d:%02d" # :nodoc:
+ UTC_OFFSET_WITHOUT_COLON = UTC_OFFSET_WITH_COLON.tr(":", "") # :nodoc:
+ private_constant :UTC_OFFSET_WITH_COLON, :UTC_OFFSET_WITHOUT_COLON
- @lazy_zones_map = ThreadSafe::Cache.new
+ @lazy_zones_map = Concurrent::Map.new
+ @country_zones = Concurrent::Map.new
class << self
# Assumes self represents an offset from UTC in seconds (as returned from
# Time#utc_offset) and turns this into an +HH:MM formatted string.
#
- # TimeZone.seconds_to_utc_offset(-21_600) # => "-06:00"
+ # ActiveSupport::TimeZone.seconds_to_utc_offset(-21_600) # => "-06:00"
def seconds_to_utc_offset(seconds, colon = true)
format = colon ? UTC_OFFSET_WITH_COLON : UTC_OFFSET_WITHOUT_COLON
- sign = (seconds < 0 ? '-' : '+')
+ sign = (seconds < 0 ? "-" : "+")
hours = seconds.abs / 3600
minutes = (seconds.abs % 3600) / 60
format % [sign, hours, minutes]
@@ -231,31 +229,59 @@ module ActiveSupport
# Returns +nil+ if no such time zone is known to the system.
def [](arg)
case arg
- when String
+ when String
begin
@lazy_zones_map[arg] ||= create(arg)
rescue TZInfo::InvalidTimezoneIdentifier
nil
end
- when Numeric, ActiveSupport::Duration
- arg *= 3600 if arg.abs <= 13
- all.find { |z| z.utc_offset == arg.to_i }
- else
- raise ArgumentError, "invalid argument to TimeZone[]: #{arg.inspect}"
+ when Numeric, ActiveSupport::Duration
+ arg *= 3600 if arg.abs <= 13
+ all.find { |z| z.utc_offset == arg.to_i }
+ else
+ raise ArgumentError, "invalid argument to TimeZone[]: #{arg.inspect}"
end
end
# A convenience method for returning a collection of TimeZone objects
# for time zones in the USA.
def us_zones
- @us_zones ||= all.find_all { |z| z.name =~ /US|Arizona|Indiana|Hawaii|Alaska/ }
+ country_zones(:us)
+ end
+
+ # A convenience method for returning a collection of TimeZone objects
+ # 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] ||= load_country_zones(code)
+ end
+
+ def clear #:nodoc:
+ @lazy_zones_map = Concurrent::Map.new
+ @country_zones = Concurrent::Map.new
+ @zones = nil
+ @zones_map = nil
end
private
+ def load_country_zones(code)
+ country = TZInfo::Country.get(code)
+ country.zone_identifiers.flat_map do |tz_id|
+ if MAPPING.value?(tz_id)
+ MAPPING.inject([]) do |memo, (key, value)|
+ memo << self[key] if value == tz_id
+ memo
+ end
+ else
+ create(tz_id, nil, TZInfo::Timezone.new(tz_id))
+ end
+ end.sort!
+ end
+
def zones_map
- @zones_map ||= begin
- MAPPING.each_key {|place| self[place]} # load all the zones
- @lazy_zones_map
+ @zones_map ||= MAPPING.each_with_object({}) do |(name, _), zones|
+ timezone = self[name]
+ zones[name] = timezone if timezone
end
end
end
@@ -272,7 +298,6 @@ module ActiveSupport
@name = name
@utc_offset = utc_offset
@tzinfo = tzinfo || TimeZone.find_tzinfo(name)
- @current_period = nil
end
# Returns the offset of this time zone from UTC in seconds.
@@ -280,14 +305,17 @@ module ActiveSupport
if @utc_offset
@utc_offset
else
- @current_period ||= tzinfo.current_period if tzinfo
- @current_period.utc_offset if @current_period
+ tzinfo.current_period.utc_offset if tzinfo && tzinfo.current_period
end
end
- # Returns the offset of this time zone as a formatted string, of the
- # format "+HH:MM".
- def formatted_offset(colon=true, alternate_utc_string = nil)
+ # Returns a formatted string of the offset from UTC, or an alternative
+ # string if the time zone is already UTC.
+ #
+ # zone = ActiveSupport::TimeZone['Central Time (US & Canada)']
+ # zone.formatted_offset # => "-06:00"
+ # zone.formatted_offset(false) # => "-0600"
+ def formatted_offset(colon = true, alternate_utc_string = nil)
utc_offset == 0 && alternate_utc_string || self.class.seconds_to_utc_offset(utc_offset, colon)
end
@@ -327,8 +355,48 @@ module ActiveSupport
# Time.zone = 'Hawaii' # => "Hawaii"
# Time.utc(2000).to_f # => 946684800.0
# Time.zone.at(946684800.0) # => Fri, 31 Dec 1999 14:00:00 HST -10:00
- def at(secs)
- Time.at(secs).utc.in_time_zone(self)
+ #
+ # A second argument can be supplied to specify sub-second precision.
+ #
+ # Time.zone = 'Hawaii' # => "Hawaii"
+ # Time.at(946684800, 123456.789).nsec # => 123456789
+ def at(*args)
+ Time.at(*args).utc.in_time_zone(self)
+ 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 usually 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
@@ -347,10 +415,42 @@ module ActiveSupport
# components are supplied, then the day of the month defaults to 1:
#
# Time.zone.parse('Mar 2000') # => Wed, 01 Mar 2000 00:00:00 HST -10:00
- def parse(str, now=now())
+ #
+ # If the string is invalid then an +ArgumentError+ could be raised.
+ def parse(str, now = now())
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+,
@@ -371,7 +471,7 @@ module ActiveSupport
# components are supplied, then the day of the month defaults to 1:
#
# Time.zone.strptime('Mar 2000', '%b %Y') # => Wed, 01 Mar 2000 00:00:00 HST -10:00
- def strptime(str, format, now=now())
+ def strptime(str, format, now = now())
parts_to_time(DateTime._strptime(str, format), now)
end
@@ -384,7 +484,7 @@ module ActiveSupport
time_now.utc.in_time_zone(self)
end
- # Return the current date in this time zone.
+ # Returns the current date in this time zone.
def today
tzinfo.now.to_date
end
@@ -408,7 +508,7 @@ module ActiveSupport
# Adjust the given time to the simultaneous time in UTC. Returns a
# Time.utc() instance.
- def local_to_utc(time, dst=true)
+ def local_to_utc(time, dst = true)
tzinfo.local_to_utc(time, dst)
end
@@ -420,8 +520,8 @@ module ActiveSupport
# Available so that TimeZone instances respond like TZInfo::Timezone
# instances.
- def period_for_local(time, dst=true)
- tzinfo.period_for_local(time, dst)
+ def period_for_local(time, dst = true)
+ tzinfo.period_for_local(time, dst) { |periods| periods.last }
end
def periods_for_local(time) #:nodoc:
@@ -429,29 +529,34 @@ module ActiveSupport
end
def init_with(coder) #:nodoc:
- initialize(coder['name'])
+ initialize(coder["name"])
end
def encode_with(coder) #:nodoc:
- coder.tag ="!ruby/object:#{self.class}"
- coder.map = { 'name' => tzinfo.name }
+ coder.tag = "!ruby/object:#{self.class}"
+ coder.map = { "name" => tzinfo.name }
end
private
def parts_to_time(parts, now)
+ raise ArgumentError, "invalid date" if parts.nil?
return if parts.empty?
- time = Time.new(
- parts.fetch(:year, now.year),
- parts.fetch(:mon, now.month),
- parts.fetch(:mday, parts[:year] || parts[:mon] ? 1 : now.day),
- parts.fetch(:hour, 0),
- parts.fetch(:min, 0),
- parts.fetch(:sec, 0) + parts.fetch(:sec_fraction, 0),
- parts.fetch(:offset, 0)
- )
-
- if parts[:offset]
+ if parts[:seconds]
+ time = Time.at(parts[:seconds])
+ else
+ time = Time.new(
+ parts.fetch(:year, now.year),
+ parts.fetch(:mon, now.month),
+ parts.fetch(:mday, parts[:year] || parts[:mon] ? 1 : now.day),
+ parts.fetch(:hour, 0),
+ parts.fetch(:min, 0),
+ parts.fetch(:sec, 0) + parts.fetch(:sec_fraction, 0),
+ parts.fetch(:offset, 0)
+ )
+ end
+
+ if parts[:offset] || parts[:seconds]
TimeWithZone.new(time.utc, self)
else
TimeWithZone.new(nil, self, time)
diff --git a/activesupport/lib/active_support/values/unicode_tables.dat b/activesupport/lib/active_support/values/unicode_tables.dat
deleted file mode 100644
index 760be4c07a..0000000000
--- a/activesupport/lib/active_support/values/unicode_tables.dat
+++ /dev/null
Binary files differ
diff --git a/activesupport/lib/active_support/version.rb b/activesupport/lib/active_support/version.rb
index fe03984546..928838c837 100644
--- a/activesupport/lib/active_support/version.rb
+++ b/activesupport/lib/active_support/version.rb
@@ -1,4 +1,6 @@
-require_relative 'gem_version'
+# frozen_string_literal: true
+
+require_relative "gem_version"
module ActiveSupport
# Returns the version of the currently loaded ActiveSupport as a <tt>Gem::Version</tt>
diff --git a/activesupport/lib/active_support/xml_mini.rb b/activesupport/lib/active_support/xml_mini.rb
index df7b081993..e42eee07a3 100644
--- a/activesupport/lib/active_support/xml_mini.rb
+++ b/activesupport/lib/active_support/xml_mini.rb
@@ -1,9 +1,11 @@
-require 'time'
-require 'base64'
-require 'bigdecimal'
-require 'active_support/core_ext/module/delegation'
-require 'active_support/core_ext/string/inflections'
-require 'active_support/core_ext/date_time/calculations'
+# frozen_string_literal: true
+
+require "time"
+require "base64"
+require "bigdecimal"
+require "active_support/core_ext/module/delegation"
+require "active_support/core_ext/string/inflections"
+require "active_support/core_ext/date_time/calculations"
module ActiveSupport
# = XmlMini
@@ -20,11 +22,11 @@ module ActiveSupport
attr_writer :original_filename, :content_type
def original_filename
- @original_filename || 'untitled'
+ @original_filename || "untitled"
end
def content_type
- @content_type || 'application/octet-stream'
+ @content_type || "application/octet-stream"
end
end
@@ -32,20 +34,21 @@ module ActiveSupport
"binary" => "base64"
} unless defined?(DEFAULT_ENCODINGS)
- TYPE_NAMES = {
- "Symbol" => "symbol",
- "Fixnum" => "integer",
- "Bignum" => "integer",
- "BigDecimal" => "decimal",
- "Float" => "float",
- "TrueClass" => "boolean",
- "FalseClass" => "boolean",
- "Date" => "date",
- "DateTime" => "dateTime",
- "Time" => "dateTime",
- "Array" => "array",
- "Hash" => "hash"
- } unless defined?(TYPE_NAMES)
+ unless defined?(TYPE_NAMES)
+ TYPE_NAMES = {
+ "Symbol" => "symbol",
+ "Integer" => "integer",
+ "BigDecimal" => "decimal",
+ "Float" => "float",
+ "TrueClass" => "boolean",
+ "FalseClass" => "boolean",
+ "Date" => "date",
+ "DateTime" => "dateTime",
+ "Time" => "dateTime",
+ "Array" => "array",
+ "Hash" => "hash"
+ }
+ end
FORMATTING = {
"symbol" => Proc.new { |symbol| symbol.to_s },
@@ -63,10 +66,20 @@ 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 },
+ "yaml" => Proc.new { |yaml| YAML.load(yaml) rescue yaml },
"base64Binary" => Proc.new { |bin| ::Base64.decode64(bin) },
"binary" => Proc.new { |bin, entity| _parse_binary(bin, entity) },
"file" => Proc.new { |file, entity| _parse_file(file, entity) }
@@ -81,7 +94,7 @@ module ActiveSupport
attr_accessor :depth
self.depth = 100
- delegate :parse, :to => :backend
+ delegate :parse, to: :backend
def backend
current_thread_backend || @backend
@@ -103,7 +116,7 @@ module ActiveSupport
def to_tag(key, value, options)
type_name = options.delete(:type)
- merged_options = options.merge(:root => key, :skip_instruct => true)
+ merged_options = options.merge(root: key, skip_instruct: true)
if value.is_a?(::Method) || value.is_a?(::Proc)
if value.arity == 1
@@ -121,7 +134,7 @@ module ActiveSupport
key = rename_key(key.to_s, options)
- attributes = options[:skip_types] || type_name.nil? ? { } : { :type => type_name }
+ attributes = options[:skip_types] || type_name.nil? ? {} : { type: type_name }
attributes[:nil] = true if value.nil?
encoding = options[:encoding] || DEFAULT_ENCODINGS[type_name]
@@ -144,33 +157,31 @@ module ActiveSupport
key
end
- protected
-
- def _dasherize(key)
- # $2 must be a non-greedy regex for this to work
- left, middle, right = /\A(_*)(.*?)(_*)\Z/.match(key.strip)[1,3]
- "#{left}#{middle.tr('_ ', '--')}#{right}"
- end
+ private
- # TODO: Add support for other encodings
- def _parse_binary(bin, entity) #:nodoc:
- case entity['encoding']
- when 'base64'
- ::Base64.decode64(bin)
- else
- bin
+ def _dasherize(key)
+ # $2 must be a non-greedy regex for this to work
+ left, middle, right = /\A(_*)(.*?)(_*)\Z/.match(key.strip)[1, 3]
+ "#{left}#{middle.tr('_ ', '--')}#{right}"
end
- end
- def _parse_file(file, entity)
- f = StringIO.new(::Base64.decode64(file))
- f.extend(FileLike)
- f.original_filename = entity['name']
- f.content_type = entity['content_type']
- f
- end
+ # TODO: Add support for other encodings
+ def _parse_binary(bin, entity)
+ case entity["encoding"]
+ when "base64"
+ ::Base64.decode64(bin)
+ else
+ bin
+ end
+ end
- private
+ def _parse_file(file, entity)
+ f = StringIO.new(::Base64.decode64(file))
+ f.extend(FileLike)
+ f.original_filename = entity["name"]
+ f.content_type = entity["content_type"]
+ f
+ end
def current_thread_backend
Thread.current[:xml_mini_backend]
@@ -190,5 +201,5 @@ module ActiveSupport
end
end
- XmlMini.backend = 'REXML'
+ XmlMini.backend = "REXML"
end
diff --git a/activesupport/lib/active_support/xml_mini/jdom.rb b/activesupport/lib/active_support/xml_mini/jdom.rb
index 94751bbc04..32fe6ade28 100644
--- a/activesupport/lib/active_support/xml_mini/jdom.rb
+++ b/activesupport/lib/active_support/xml_mini/jdom.rb
@@ -1,9 +1,11 @@
-raise "JRuby is required to use the JDOM backend for XmlMini" unless RUBY_PLATFORM =~ /java/
+# frozen_string_literal: true
-require 'jruby'
+raise "JRuby is required to use the JDOM backend for XmlMini" unless RUBY_PLATFORM.include?("java")
+
+require "jruby"
include Java
-require 'active_support/core_ext/object/blank'
+require "active_support/core_ext/object/blank"
java_import javax.xml.parsers.DocumentBuilder unless defined? DocumentBuilder
java_import javax.xml.parsers.DocumentBuilderFactory unless defined? DocumentBuilderFactory
@@ -16,7 +18,7 @@ module ActiveSupport
module XmlMini_JDOM #:nodoc:
extend self
- CONTENT_KEY = '__content__'.freeze
+ CONTENT_KEY = "__content__"
NODE_TYPE_NAMES = %w{ATTRIBUTE_NODE CDATA_SECTION_NODE COMMENT_NODE DOCUMENT_FRAGMENT_NODE
DOCUMENT_NODE DOCUMENT_TYPE_NODE ELEMENT_NODE ENTITY_NODE ENTITY_REFERENCE_NODE NOTATION_NODE
@@ -38,7 +40,7 @@ module ActiveSupport
else
@dbf = DocumentBuilderFactory.new_instance
# secure processing of java xml
- # http://www.ibm.com/developerworks/xml/library/x-tipcfsx/index.html
+ # https://archive.is/9xcQQ
@dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false)
@dbf.setFeature("http://xml.org/sax/features/external-general-entities", false)
@dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false)
@@ -46,136 +48,136 @@ module ActiveSupport
xml_string_reader = StringReader.new(data)
xml_input_source = InputSource.new(xml_string_reader)
doc = @dbf.new_document_builder.parse(xml_input_source)
- merge_element!({CONTENT_KEY => ''}, doc.document_element, XmlMini.depth)
+ merge_element!({ CONTENT_KEY => "" }, doc.document_element, XmlMini.depth)
end
end
private
- # Convert an XML element and merge into the hash
- #
- # hash::
- # Hash to merge the converted element into.
- # element::
- # XML element to merge into hash
- def merge_element!(hash, element, depth)
- raise 'Document too deep!' if depth == 0
- delete_empty(hash)
- merge!(hash, element.tag_name, collapse(element, depth))
- end
-
- def delete_empty(hash)
- hash.delete(CONTENT_KEY) if hash[CONTENT_KEY] == ''
- end
+ # Convert an XML element and merge into the hash
+ #
+ # hash::
+ # Hash to merge the converted element into.
+ # element::
+ # XML element to merge into hash
+ def merge_element!(hash, element, depth)
+ raise "Document too deep!" if depth == 0
+ delete_empty(hash)
+ merge!(hash, element.tag_name, collapse(element, depth))
+ end
- # Actually converts an XML document element into a data structure.
- #
- # element::
- # The document element to be collapsed.
- def collapse(element, depth)
- hash = get_attributes(element)
+ def delete_empty(hash)
+ hash.delete(CONTENT_KEY) if hash[CONTENT_KEY] == ""
+ end
- child_nodes = element.child_nodes
- if child_nodes.length > 0
- (0...child_nodes.length).each do |i|
- child = child_nodes.item(i)
- merge_element!(hash, child, depth - 1) unless child.node_type == Node.TEXT_NODE
+ # Actually converts an XML document element into a data structure.
+ #
+ # element::
+ # The document element to be collapsed.
+ def collapse(element, depth)
+ hash = get_attributes(element)
+
+ child_nodes = element.child_nodes
+ if child_nodes.length > 0
+ (0...child_nodes.length).each do |i|
+ child = child_nodes.item(i)
+ merge_element!(hash, child, depth - 1) unless child.node_type == Node.TEXT_NODE
+ end
+ merge_texts!(hash, element) unless empty_content?(element)
+ hash
+ else
+ merge_texts!(hash, element)
end
- merge_texts!(hash, element) unless empty_content?(element)
- hash
- else
- merge_texts!(hash, element)
end
- end
- # Merge all the texts of an element into the hash
- #
- # hash::
- # Hash to add the converted element to.
- # element::
- # XML element whose texts are to me merged into the hash
- def merge_texts!(hash, element)
- delete_empty(hash)
- text_children = texts(element)
- if text_children.join.empty?
- hash
- else
- # must use value to prevent double-escaping
- merge!(hash, CONTENT_KEY, text_children.join)
+ # Merge all the texts of an element into the hash
+ #
+ # hash::
+ # Hash to add the converted element to.
+ # element::
+ # XML element whose texts are to me merged into the hash
+ def merge_texts!(hash, element)
+ delete_empty(hash)
+ text_children = texts(element)
+ if text_children.join.empty?
+ hash
+ else
+ # must use value to prevent double-escaping
+ merge!(hash, CONTENT_KEY, text_children.join)
+ end
end
- end
- # Adds a new key/value pair to an existing Hash. If the key to be added
- # already exists and the existing value associated with key is not
- # an Array, it will be wrapped in an Array. Then the new value is
- # appended to that Array.
- #
- # hash::
- # Hash to add key/value pair to.
- # key::
- # Key to be added.
- # value::
- # Value to be associated with key.
- def merge!(hash, key, value)
- if hash.has_key?(key)
- if hash[key].instance_of?(Array)
- hash[key] << value
+ # Adds a new key/value pair to an existing Hash. If the key to be added
+ # already exists and the existing value associated with key is not
+ # an Array, it will be wrapped in an Array. Then the new value is
+ # appended to that Array.
+ #
+ # hash::
+ # Hash to add key/value pair to.
+ # key::
+ # Key to be added.
+ # value::
+ # Value to be associated with key.
+ def merge!(hash, key, value)
+ if hash.has_key?(key)
+ if hash[key].instance_of?(Array)
+ hash[key] << value
+ else
+ hash[key] = [hash[key], value]
+ end
+ elsif value.instance_of?(Array)
+ hash[key] = [value]
else
- hash[key] = [hash[key], value]
+ hash[key] = value
end
- elsif value.instance_of?(Array)
- hash[key] = [value]
- else
- hash[key] = value
+ hash
end
- hash
- end
- # Converts the attributes array of an XML element into a hash.
- # Returns an empty Hash if node has no attributes.
- #
- # element::
- # XML element to extract attributes from.
- def get_attributes(element)
- attribute_hash = {}
- attributes = element.attributes
- (0...attributes.length).each do |i|
- attribute_hash[CONTENT_KEY] ||= ''
- attribute_hash[attributes.item(i).name] = attributes.item(i).value
+ # Converts the attributes array of an XML element into a hash.
+ # Returns an empty Hash if node has no attributes.
+ #
+ # element::
+ # XML element to extract attributes from.
+ def get_attributes(element)
+ attribute_hash = {}
+ attributes = element.attributes
+ (0...attributes.length).each do |i|
+ attribute_hash[CONTENT_KEY] ||= ""
+ attribute_hash[attributes.item(i).name] = attributes.item(i).value
+ end
+ attribute_hash
end
- attribute_hash
- end
- # Determines if a document element has text content
- #
- # element::
- # XML element to be checked.
- def texts(element)
- texts = []
- child_nodes = element.child_nodes
- (0...child_nodes.length).each do |i|
- item = child_nodes.item(i)
- if item.node_type == Node.TEXT_NODE
- texts << item.get_data
+ # Determines if a document element has text content
+ #
+ # element::
+ # XML element to be checked.
+ def texts(element)
+ texts = []
+ child_nodes = element.child_nodes
+ (0...child_nodes.length).each do |i|
+ item = child_nodes.item(i)
+ if item.node_type == Node.TEXT_NODE
+ texts << item.get_data
+ end
end
+ texts
end
- texts
- end
- # Determines if a document element has text content
- #
- # element::
- # XML element to be checked.
- def empty_content?(element)
- text = ''
- child_nodes = element.child_nodes
- (0...child_nodes.length).each do |i|
- item = child_nodes.item(i)
- if item.node_type == Node.TEXT_NODE
- text << item.get_data.strip
+ # Determines if a document element has text content
+ #
+ # element::
+ # XML element to be checked.
+ def empty_content?(element)
+ text = +""
+ child_nodes = element.child_nodes
+ (0...child_nodes.length).each do |i|
+ item = child_nodes.item(i)
+ if item.node_type == Node.TEXT_NODE
+ text << item.get_data.strip
+ end
end
+ text.strip.length == 0
end
- text.strip.length == 0
- end
end
end
diff --git a/activesupport/lib/active_support/xml_mini/libxml.rb b/activesupport/lib/active_support/xml_mini/libxml.rb
index bb0ea9c582..c2e999ef6c 100644
--- a/activesupport/lib/active_support/xml_mini/libxml.rb
+++ b/activesupport/lib/active_support/xml_mini/libxml.rb
@@ -1,6 +1,8 @@
-require 'libxml'
-require 'active_support/core_ext/object/blank'
-require 'stringio'
+# frozen_string_literal: true
+
+require "libxml"
+require "active_support/core_ext/object/blank"
+require "stringio"
module ActiveSupport
module XmlMini_LibXML #:nodoc:
@@ -11,18 +13,15 @@ module ActiveSupport
# XML Document string or IO to parse
def parse(data)
if !data.respond_to?(:read)
- data = StringIO.new(data || '')
+ data = StringIO.new(data || "")
end
- char = data.getc
- if char.nil?
+ if data.eof?
{}
else
- data.ungetc(char)
LibXML::XML::Parser.io(data).parse.to_hash
end
end
-
end
end
@@ -35,20 +34,20 @@ module LibXML #:nodoc:
end
module Node #:nodoc:
- CONTENT_ROOT = '__content__'.freeze
+ CONTENT_ROOT = "__content__"
# Convert XML document to hash.
#
# hash::
# Hash to merge the converted element into.
- def to_hash(hash={})
+ def to_hash(hash = {})
node_hash = {}
# Insert node hash into parent hash correctly.
case hash[name]
- when Array then hash[name] << node_hash
- when Hash then hash[name] = [hash[name], node_hash]
- when nil then hash[name] = node_hash
+ when Array then hash[name] << node_hash
+ when Hash then hash[name] = [hash[name], node_hash]
+ when nil then hash[name] = node_hash
end
# Handle child elements
@@ -56,7 +55,7 @@ module LibXML #:nodoc:
if c.element?
c.to_hash(node_hash)
elsif c.text? || c.cdata?
- node_hash[CONTENT_ROOT] ||= ''
+ node_hash[CONTENT_ROOT] ||= +""
node_hash[CONTENT_ROOT] << c.content
end
end
@@ -75,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 70a95299ec..ac8acdfc3c 100644
--- a/activesupport/lib/active_support/xml_mini/libxmlsax.rb
+++ b/activesupport/lib/active_support/xml_mini/libxmlsax.rb
@@ -1,6 +1,8 @@
-require 'libxml'
-require 'active_support/core_ext/object/blank'
-require 'stringio'
+# frozen_string_literal: true
+
+require "libxml"
+require "active_support/core_ext/object/blank"
+require "stringio"
module ActiveSupport
module XmlMini_LibXMLSAX #:nodoc:
@@ -9,11 +11,10 @@ module ActiveSupport
# Class that will build the hash while the XML document
# is being parsed using SAX events.
class HashBuilder
-
include LibXML::XML::SaxParser::Callbacks
- CONTENT_KEY = '__content__'.freeze
- HASH_SIZE_KEY = '__hash_size__'.freeze
+ CONTENT_KEY = "__content__"
+ HASH_SIZE_KEY = "__hash_size__"
attr_reader :hash
@@ -22,7 +23,7 @@ module ActiveSupport
end
def on_start_document
- @hash = { CONTENT_KEY => '' }
+ @hash = { CONTENT_KEY => +"" }
@hash_stack = [@hash]
end
@@ -32,20 +33,20 @@ module ActiveSupport
end
def on_start_element(name, attrs = {})
- new_hash = { CONTENT_KEY => '' }.merge!(attrs)
+ new_hash = { CONTENT_KEY => +"" }.merge!(attrs)
new_hash[HASH_SIZE_KEY] = new_hash.size + 1
case current_hash[name]
- when Array then current_hash[name] << new_hash
- when Hash then current_hash[name] = [current_hash[name], new_hash]
- when nil then current_hash[name] = new_hash
+ when Array then current_hash[name] << new_hash
+ when Hash then current_hash[name] = [current_hash[name], new_hash]
+ when nil then current_hash[name] = new_hash
end
@hash_stack.push(new_hash)
end
def on_end_element(name)
- if current_hash.length > current_hash.delete(HASH_SIZE_KEY) && current_hash[CONTENT_KEY].blank? || current_hash[CONTENT_KEY] == ''
+ if current_hash.length > current_hash.delete(HASH_SIZE_KEY) && current_hash[CONTENT_KEY].blank? || current_hash[CONTENT_KEY] == ""
current_hash.delete(CONTENT_KEY)
end
@hash_stack.pop
@@ -63,18 +64,15 @@ module ActiveSupport
def parse(data)
if !data.respond_to?(:read)
- data = StringIO.new(data || '')
+ data = StringIO.new(data || "")
end
- char = data.getc
- if char.nil?
+ if data.eof?
{}
else
- data.ungetc(char)
-
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/nokogiri.rb b/activesupport/lib/active_support/xml_mini/nokogiri.rb
index 619cc7522d..f76513f48b 100644
--- a/activesupport/lib/active_support/xml_mini/nokogiri.rb
+++ b/activesupport/lib/active_support/xml_mini/nokogiri.rb
@@ -1,11 +1,13 @@
+# frozen_string_literal: true
+
begin
- require 'nokogiri'
+ require "nokogiri"
rescue LoadError => e
$stderr.puts "You don't have nokogiri installed in your application. Please add it to your Gemfile and run bundle install"
raise e
end
-require 'active_support/core_ext/object/blank'
-require 'stringio'
+require "active_support/core_ext/object/blank"
+require "stringio"
module ActiveSupport
module XmlMini_Nokogiri #:nodoc:
@@ -16,14 +18,12 @@ module ActiveSupport
# XML Document string or IO to parse
def parse(data)
if !data.respond_to?(:read)
- data = StringIO.new(data || '')
+ data = StringIO.new(data || "")
end
- char = data.getc
- if char.nil?
+ if data.eof?
{}
else
- data.ungetc(char)
doc = Nokogiri::XML(data)
raise doc.errors.first if doc.errors.length > 0
doc.to_hash
@@ -38,20 +38,20 @@ module ActiveSupport
end
module Node #:nodoc:
- CONTENT_ROOT = '__content__'.freeze
+ CONTENT_ROOT = "__content__"
# Convert XML document to hash.
#
# hash::
# Hash to merge the converted element into.
- def to_hash(hash={})
+ def to_hash(hash = {})
node_hash = {}
# Insert node hash into parent hash correctly.
case hash[name]
- when Array then hash[name] << node_hash
- when Hash then hash[name] = [hash[name], node_hash]
- when nil then hash[name] = node_hash
+ when Array then hash[name] << node_hash
+ when Hash then hash[name] = [hash[name], node_hash]
+ when nil then hash[name] = node_hash
end
# Handle child elements
@@ -59,7 +59,7 @@ module ActiveSupport
if c.element?
c.to_hash(node_hash)
elsif c.text? || c.cdata?
- node_hash[CONTENT_ROOT] ||= ''
+ node_hash[CONTENT_ROOT] ||= +""
node_hash[CONTENT_ROOT] << c.content
end
end
diff --git a/activesupport/lib/active_support/xml_mini/nokogirisax.rb b/activesupport/lib/active_support/xml_mini/nokogirisax.rb
index be2d6a4cb1..55cd72e093 100644
--- a/activesupport/lib/active_support/xml_mini/nokogirisax.rb
+++ b/activesupport/lib/active_support/xml_mini/nokogirisax.rb
@@ -1,11 +1,13 @@
+# frozen_string_literal: true
+
begin
- require 'nokogiri'
+ require "nokogiri"
rescue LoadError => e
$stderr.puts "You don't have nokogiri installed in your application. Please add it to your Gemfile and run bundle install"
raise e
end
-require 'active_support/core_ext/object/blank'
-require 'stringio'
+require "active_support/core_ext/object/blank"
+require "stringio"
module ActiveSupport
module XmlMini_NokogiriSAX #:nodoc:
@@ -14,9 +16,8 @@ module ActiveSupport
# Class that will build the hash while the XML document
# is being parsed using SAX events.
class HashBuilder < Nokogiri::XML::SAX::Document
-
- CONTENT_KEY = '__content__'.freeze
- HASH_SIZE_KEY = '__hash_size__'.freeze
+ CONTENT_KEY = "__content__"
+ HASH_SIZE_KEY = "__hash_size__"
attr_reader :hash
@@ -38,20 +39,20 @@ module ActiveSupport
end
def start_element(name, attrs = [])
- new_hash = { CONTENT_KEY => '' }.merge!(Hash[attrs])
+ new_hash = { CONTENT_KEY => +"" }.merge!(Hash[attrs])
new_hash[HASH_SIZE_KEY] = new_hash.size + 1
case current_hash[name]
- when Array then current_hash[name] << new_hash
- when Hash then current_hash[name] = [current_hash[name], new_hash]
- when nil then current_hash[name] = new_hash
+ when Array then current_hash[name] << new_hash
+ when Hash then current_hash[name] = [current_hash[name], new_hash]
+ when nil then current_hash[name] = new_hash
end
@hash_stack.push(new_hash)
end
def end_element(name)
- if current_hash.length > current_hash.delete(HASH_SIZE_KEY) && current_hash[CONTENT_KEY].blank? || current_hash[CONTENT_KEY] == ''
+ if current_hash.length > current_hash.delete(HASH_SIZE_KEY) && current_hash[CONTENT_KEY].blank? || current_hash[CONTENT_KEY] == ""
current_hash.delete(CONTENT_KEY)
end
@hash_stack.pop
@@ -69,15 +70,13 @@ module ActiveSupport
def parse(data)
if !data.respond_to?(:read)
- data = StringIO.new(data || '')
+ data = StringIO.new(data || "")
end
- char = data.getc
- if char.nil?
+ if data.eof?
{}
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/lib/active_support/xml_mini/rexml.rb b/activesupport/lib/active_support/xml_mini/rexml.rb
index 924ed72345..8d6e3af066 100644
--- a/activesupport/lib/active_support/xml_mini/rexml.rb
+++ b/activesupport/lib/active_support/xml_mini/rexml.rb
@@ -1,12 +1,14 @@
-require 'active_support/core_ext/kernel/reporting'
-require 'active_support/core_ext/object/blank'
-require 'stringio'
+# frozen_string_literal: true
+
+require "active_support/core_ext/kernel/reporting"
+require "active_support/core_ext/object/blank"
+require "stringio"
module ActiveSupport
module XmlMini_REXML #:nodoc:
extend self
- CONTENT_KEY = '__content__'.freeze
+ CONTENT_KEY = "__content__"
# Parse an XML Document string or IO into a simple hash.
#
@@ -17,15 +19,13 @@ module ActiveSupport
# XML Document string or IO to parse
def parse(data)
if !data.respond_to?(:read)
- data = StringIO.new(data || '')
+ data = StringIO.new(data || "")
end
- char = data.getc
- if char.nil?
+ if data.eof?
{}
else
- data.ungetc(char)
- silence_warnings { require 'rexml/document' } unless defined?(REXML::Document)
+ silence_warnings { require "rexml/document" } unless defined?(REXML::Document)
doc = REXML::Document.new(data)
if doc.root
@@ -57,7 +57,7 @@ module ActiveSupport
hash = get_attributes(element)
if element.has_elements?
- element.each_element {|child| merge_element!(hash, child, depth - 1) }
+ element.each_element { |child| merge_element!(hash, child, depth - 1) }
merge_texts!(hash, element) unless empty_content?(element)
hash
else
@@ -76,7 +76,7 @@ module ActiveSupport
hash
else
# must use value to prevent double-escaping
- texts = ''
+ texts = +""
element.texts.each { |t| texts << t.value }
merge!(hash, CONTENT_KEY, texts)
end
@@ -115,7 +115,7 @@ module ActiveSupport
# XML element to extract attributes from.
def get_attributes(element)
attributes = {}
- element.attributes.each { |n,v| attributes[n] = v }
+ element.attributes.each { |n, v| attributes[n] = v }
attributes
end
diff --git a/activesupport/test/abstract_unit.rb b/activesupport/test/abstract_unit.rb
index 65a8edbabb..168d3655d3 100644
--- a/activesupport/test/abstract_unit.rb
+++ b/activesupport/test/abstract_unit.rb
@@ -1,45 +1,46 @@
-ORIG_ARGV = ARGV.dup
+# frozen_string_literal: true
-begin
- old, $VERBOSE = $VERBOSE, nil
- require File.expand_path('../../../load_paths', __FILE__)
-ensure
- $VERBOSE = old
-end
+ORIG_ARGV = ARGV.dup
-require 'active_support/core_ext/kernel/reporting'
+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'
-require 'active_support/testing/method_call_assertions'
+require "active_support/testing/autorun"
+require "active_support/testing/method_call_assertions"
-ENV['NO_RELOAD'] = '1'
-require 'active_support'
+ENV["NO_RELOAD"] = "1"
+require "active_support"
Thread.abort_on_exception = true
# Show backtraces for deprecated behavior for quicker cleanup.
ActiveSupport::Deprecation.debug = true
+# Default to old to_time behavior but allow running tests with new behavior
+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
+class ActiveSupport::TestCase
+ include ActiveSupport::Testing::MethodCallAssertions
-# Skips the current run on JRuby using Minitest::Assertions#skip
-def jruby_skip(message = '')
- skip message if defined?(JRUBY_VERSION)
-end
+ private
+ # Skips the current run on Rubinius using Minitest::Assertions#skip
+ def rubinius_skip(message = "")
+ skip message if RUBY_ENGINE == "rbx"
+ end
-require 'minitest/mock'
+ # 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
+ def frozen_error_class
+ Object.const_defined?(:FrozenError) ? FrozenError : RuntimeError
+ end
end
diff --git a/activesupport/test/array_inquirer_test.rb b/activesupport/test/array_inquirer_test.rb
index b25e5cca86..9a260edd8a 100644
--- a/activesupport/test/array_inquirer_test.rb
+++ b/activesupport/test/array_inquirer_test.rb
@@ -1,15 +1,17 @@
-require 'abstract_unit'
-require 'active_support/core_ext/array'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/array"
class ArrayInquirerTest < ActiveSupport::TestCase
def setup
- @array_inquirer = ActiveSupport::ArrayInquirer.new([:mobile, :tablet])
+ @array_inquirer = ActiveSupport::ArrayInquirer.new([:mobile, :tablet, "api"])
end
def test_individual
- assert @array_inquirer.mobile?
- assert @array_inquirer.tablet?
- assert_not @array_inquirer.desktop?
+ assert_predicate @array_inquirer, :mobile?
+ assert_predicate @array_inquirer, :tablet?
+ assert_not_predicate @array_inquirer, :desktop?
end
def test_any
@@ -18,6 +20,11 @@ class ArrayInquirerTest < ActiveSupport::TestCase
assert_not @array_inquirer.any?(:desktop, :watch)
end
+ def test_any_string_symbol_mismatch
+ assert @array_inquirer.any?("mobile")
+ assert @array_inquirer.any?(:api)
+ end
+
def test_any_with_block
assert @array_inquirer.any? { |v| v == :mobile }
assert_not @array_inquirer.any? { |v| v == :desktop }
@@ -28,9 +35,29 @@ class ArrayInquirerTest < ActiveSupport::TestCase
end
def test_inquiry
- result = [:mobile, :tablet].inquiry
+ result = [:mobile, :tablet, "api"].inquiry
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/autoload_test.rb b/activesupport/test/autoload_test.rb
index c18b007612..216b069420 100644
--- a/activesupport/test/autoload_test.rb
+++ b/activesupport/test/autoload_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class TestAutoloadModule < ActiveSupport::TestCase
include ActiveSupport::Testing::Isolation
@@ -31,7 +33,7 @@ class TestAutoloadModule < ActiveSupport::TestCase
end
end
- assert !$LOADED_FEATURES.include?(@some_class_path)
+ assert_not_includes $LOADED_FEATURES, @some_class_path
assert_nothing_raised { ::Fixtures::Autoload::SomeClass }
end
@@ -40,7 +42,7 @@ class TestAutoloadModule < ActiveSupport::TestCase
autoload :SomeClass
end
- assert !$LOADED_FEATURES.include?(@some_class_path)
+ assert_not_includes $LOADED_FEATURES, @some_class_path
assert_nothing_raised { ::Fixtures::Autoload::SomeClass }
end
@@ -51,9 +53,9 @@ class TestAutoloadModule < ActiveSupport::TestCase
end
end
- assert !$LOADED_FEATURES.include?(@some_class_path)
+ assert_not_includes $LOADED_FEATURES, @some_class_path
::Fixtures::Autoload.eager_load!
- assert $LOADED_FEATURES.include?(@some_class_path)
+ assert_includes $LOADED_FEATURES, @some_class_path
assert_nothing_raised { ::Fixtures::Autoload::SomeClass }
end
@@ -64,7 +66,7 @@ class TestAutoloadModule < ActiveSupport::TestCase
end
end
- assert !$LOADED_FEATURES.include?(@another_class_path)
+ assert_not_includes $LOADED_FEATURES, @another_class_path
assert_nothing_raised { ::Fixtures::AnotherClass }
end
@@ -75,7 +77,7 @@ class TestAutoloadModule < ActiveSupport::TestCase
end
end
- assert !$LOADED_FEATURES.include?(@another_class_path)
+ assert_not_includes $LOADED_FEATURES, @another_class_path
assert_nothing_raised { ::Fixtures::AnotherClass }
end
-end \ No newline at end of file
+end
diff --git a/activesupport/test/autoloading_fixtures/a/b.rb b/activesupport/test/autoloading_fixtures/a/b.rb
index 9c9e6454cf..27baaea08c 100644
--- a/activesupport/test/autoloading_fixtures/a/b.rb
+++ b/activesupport/test/autoloading_fixtures/a/b.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
class A::B
-end \ No newline at end of file
+end
diff --git a/activesupport/test/autoloading_fixtures/a/c/d.rb b/activesupport/test/autoloading_fixtures/a/c/d.rb
index 0f40d6fbc4..f07128673f 100644
--- a/activesupport/test/autoloading_fixtures/a/c/d.rb
+++ b/activesupport/test/autoloading_fixtures/a/c/d.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
class A::C::D
-end \ No newline at end of file
+end
diff --git a/activesupport/test/autoloading_fixtures/a/c/em/f.rb b/activesupport/test/autoloading_fixtures/a/c/em/f.rb
index 8b28e19148..78c96cf45f 100644
--- a/activesupport/test/autoloading_fixtures/a/c/em/f.rb
+++ b/activesupport/test/autoloading_fixtures/a/c/em/f.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
class A::C::EM::F
-end \ No newline at end of file
+end
diff --git a/activesupport/test/autoloading_fixtures/application.rb b/activesupport/test/autoloading_fixtures/application.rb
index d7d3096dcb..971cbe1b17 100644
--- a/activesupport/test/autoloading_fixtures/application.rb
+++ b/activesupport/test/autoloading_fixtures/application.rb
@@ -1 +1,3 @@
+# frozen_string_literal: true
+
ApplicationController = 10
diff --git a/activesupport/test/autoloading_fixtures/circular1.rb b/activesupport/test/autoloading_fixtures/circular1.rb
index a45761f066..7f891b5eb1 100644
--- a/activesupport/test/autoloading_fixtures/circular1.rb
+++ b/activesupport/test/autoloading_fixtures/circular1.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
silence_warnings do
Circular2
end
diff --git a/activesupport/test/autoloading_fixtures/circular2.rb b/activesupport/test/autoloading_fixtures/circular2.rb
index c847fa5001..1fdb4c261f 100644
--- a/activesupport/test/autoloading_fixtures/circular2.rb
+++ b/activesupport/test/autoloading_fixtures/circular2.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
Circular1
class Circular2
diff --git a/activesupport/test/autoloading_fixtures/class_folder.rb b/activesupport/test/autoloading_fixtures/class_folder.rb
index ad2b27be61..ff0826c298 100644
--- a/activesupport/test/autoloading_fixtures/class_folder.rb
+++ b/activesupport/test/autoloading_fixtures/class_folder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ClassFolder
- ConstantInClassFolder = 'indeed'
+ ConstantInClassFolder = "indeed"
end
diff --git a/activesupport/test/autoloading_fixtures/class_folder/class_folder_subclass.rb b/activesupport/test/autoloading_fixtures/class_folder/class_folder_subclass.rb
index 402609c583..cd901e9d71 100644
--- a/activesupport/test/autoloading_fixtures/class_folder/class_folder_subclass.rb
+++ b/activesupport/test/autoloading_fixtures/class_folder/class_folder_subclass.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ClassFolder::ClassFolderSubclass < ClassFolder
- ConstantInClassFolder = 'indeed'
+ ConstantInClassFolder = "indeed"
end
diff --git a/activesupport/test/autoloading_fixtures/class_folder/inline_class.rb b/activesupport/test/autoloading_fixtures/class_folder/inline_class.rb
index 8235e90724..960bfcbc70 100644
--- a/activesupport/test/autoloading_fixtures/class_folder/inline_class.rb
+++ b/activesupport/test/autoloading_fixtures/class_folder/inline_class.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
class ClassFolder::InlineClass
end
diff --git a/activesupport/test/autoloading_fixtures/class_folder/nested_class.rb b/activesupport/test/autoloading_fixtures/class_folder/nested_class.rb
index 57a13d89ea..98426b797d 100644
--- a/activesupport/test/autoloading_fixtures/class_folder/nested_class.rb
+++ b/activesupport/test/autoloading_fixtures/class_folder/nested_class.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ClassFolder
class NestedClass
end
diff --git a/activesupport/test/autoloading_fixtures/conflict.rb b/activesupport/test/autoloading_fixtures/conflict.rb
index 4ac6201902..c5d3f6bdc0 100644
--- a/activesupport/test/autoloading_fixtures/conflict.rb
+++ b/activesupport/test/autoloading_fixtures/conflict.rb
@@ -1 +1,3 @@
-Conflict = 2 \ No newline at end of file
+# frozen_string_literal: true
+
+Conflict = 2
diff --git a/activesupport/test/autoloading_fixtures/counting_loader.rb b/activesupport/test/autoloading_fixtures/counting_loader.rb
index 4225c4412c..6ac3a9828d 100644
--- a/activesupport/test/autoloading_fixtures/counting_loader.rb
+++ b/activesupport/test/autoloading_fixtures/counting_loader.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
$counting_loaded_times ||= 0
$counting_loaded_times += 1
diff --git a/activesupport/test/autoloading_fixtures/cross_site_dependency.rb b/activesupport/test/autoloading_fixtures/cross_site_dependency.rb
index 21ee554e92..8a18dcff10 100644
--- a/activesupport/test/autoloading_fixtures/cross_site_dependency.rb
+++ b/activesupport/test/autoloading_fixtures/cross_site_dependency.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
class CrossSiteDependency
-end \ No newline at end of file
+end
diff --git a/activesupport/test/autoloading_fixtures/d.rb b/activesupport/test/autoloading_fixtures/d.rb
index 45c794d4ca..72752d878e 100644
--- a/activesupport/test/autoloading_fixtures/d.rb
+++ b/activesupport/test/autoloading_fixtures/d.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
class D
-end \ No newline at end of file
+end
diff --git a/activesupport/test/autoloading_fixtures/em.rb b/activesupport/test/autoloading_fixtures/em.rb
index 16a1838667..2e0ac9a6f9 100644
--- a/activesupport/test/autoloading_fixtures/em.rb
+++ b/activesupport/test/autoloading_fixtures/em.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
class EM
-end \ No newline at end of file
+end
diff --git a/activesupport/test/autoloading_fixtures/html/some_class.rb b/activesupport/test/autoloading_fixtures/html/some_class.rb
index b43d15d891..fbbfd4a214 100644
--- a/activesupport/test/autoloading_fixtures/html/some_class.rb
+++ b/activesupport/test/autoloading_fixtures/html/some_class.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module HTML
class SomeClass
end
diff --git a/activesupport/test/autoloading_fixtures/load_path/loaded_constant.rb b/activesupport/test/autoloading_fixtures/load_path/loaded_constant.rb
index e3d1218c96..8735ce87e1 100644
--- a/activesupport/test/autoloading_fixtures/load_path/loaded_constant.rb
+++ b/activesupport/test/autoloading_fixtures/load_path/loaded_constant.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
+
module LoadedConstant
end
-
diff --git a/activesupport/test/autoloading_fixtures/loads_constant.rb b/activesupport/test/autoloading_fixtures/loads_constant.rb
index 0b30dc8bca..0bb434a956 100644
--- a/activesupport/test/autoloading_fixtures/loads_constant.rb
+++ b/activesupport/test/autoloading_fixtures/loads_constant.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module LoadsConstant
end
diff --git a/activesupport/test/autoloading_fixtures/module_folder/inline_class.rb b/activesupport/test/autoloading_fixtures/module_folder/inline_class.rb
index ca83437046..c11246b528 100644
--- a/activesupport/test/autoloading_fixtures/module_folder/inline_class.rb
+++ b/activesupport/test/autoloading_fixtures/module_folder/inline_class.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
class ModuleFolder::InlineClass
end
diff --git a/activesupport/test/autoloading_fixtures/module_folder/nested_class.rb b/activesupport/test/autoloading_fixtures/module_folder/nested_class.rb
index fc4076bd0a..69226b405c 100644
--- a/activesupport/test/autoloading_fixtures/module_folder/nested_class.rb
+++ b/activesupport/test/autoloading_fixtures/module_folder/nested_class.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ModuleFolder
class NestedClass
end
diff --git a/activesupport/test/autoloading_fixtures/module_folder/nested_sibling.rb b/activesupport/test/autoloading_fixtures/module_folder/nested_sibling.rb
index 80244b8bab..30de83af11 100644
--- a/activesupport/test/autoloading_fixtures/module_folder/nested_sibling.rb
+++ b/activesupport/test/autoloading_fixtures/module_folder/nested_sibling.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
class ModuleFolder::NestedSibling
-end \ No newline at end of file
+end
diff --git a/activesupport/test/autoloading_fixtures/module_folder/nested_with_require.rb b/activesupport/test/autoloading_fixtures/module_folder/nested_with_require.rb
new file mode 100644
index 0000000000..f9d6e675d7
--- /dev/null
+++ b/activesupport/test/autoloading_fixtures/module_folder/nested_with_require.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+require "dependencies/module_folder/lib_class"
+
+module ModuleFolder
+ class NestedWithRequire
+ end
+end
diff --git a/activesupport/test/autoloading_fixtures/module_with_custom_const_missing/a/b.rb b/activesupport/test/autoloading_fixtures/module_with_custom_const_missing/a/b.rb
index d12d02f3aa..f688c1ef35 100644
--- a/activesupport/test/autoloading_fixtures/module_with_custom_const_missing/a/b.rb
+++ b/activesupport/test/autoloading_fixtures/module_with_custom_const_missing/a/b.rb
@@ -1 +1,3 @@
-ModuleWithCustomConstMissing::A::B = "10" \ No newline at end of file
+# frozen_string_literal: true
+
+ModuleWithCustomConstMissing::A::B = "10"
diff --git a/activesupport/test/autoloading_fixtures/multiple_constant_file.rb b/activesupport/test/autoloading_fixtures/multiple_constant_file.rb
index a9ff4eb89c..1da26e6c2c 100644
--- a/activesupport/test/autoloading_fixtures/multiple_constant_file.rb
+++ b/activesupport/test/autoloading_fixtures/multiple_constant_file.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
MultipleConstantFile = 10
SiblingConstant = MultipleConstantFile * 2
diff --git a/activesupport/test/autoloading_fixtures/nested_with_require_parent.rb b/activesupport/test/autoloading_fixtures/nested_with_require_parent.rb
new file mode 100644
index 0000000000..e8fb321077
--- /dev/null
+++ b/activesupport/test/autoloading_fixtures/nested_with_require_parent.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class NestedWithRequireParent
+ ModuleFolder::NestedWithRequire
+end
diff --git a/activesupport/test/autoloading_fixtures/prepend.rb b/activesupport/test/autoloading_fixtures/prepend.rb
new file mode 100644
index 0000000000..bf9e36e12c
--- /dev/null
+++ b/activesupport/test/autoloading_fixtures/prepend.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+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..506c3c5920
--- /dev/null
+++ b/activesupport/test/autoloading_fixtures/prepend/sub_class_conflict.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+class Prepend::SubClassConflict
+end
diff --git a/activesupport/test/autoloading_fixtures/raises_arbitrary_exception.rb b/activesupport/test/autoloading_fixtures/raises_arbitrary_exception.rb
new file mode 100644
index 0000000000..118ee6bdd1
--- /dev/null
+++ b/activesupport/test/autoloading_fixtures/raises_arbitrary_exception.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+RaisesArbitraryException = 1
+_ = A::B # Autoloading recursion, also expected to be watched and discarded.
+
+raise Exception, "arbitrary exception message"
diff --git a/activesupport/test/autoloading_fixtures/raises_name_error.rb b/activesupport/test/autoloading_fixtures/raises_name_error.rb
index a49960abf0..c23afb7b12 100644
--- a/activesupport/test/autoloading_fixtures/raises_name_error.rb
+++ b/activesupport/test/autoloading_fixtures/raises_name_error.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RaisesNameError
FooBarBaz
end
diff --git a/activesupport/test/autoloading_fixtures/raises_no_method_error.rb b/activesupport/test/autoloading_fixtures/raises_no_method_error.rb
index e1b8fce24a..1000ce1cf5 100644
--- a/activesupport/test/autoloading_fixtures/raises_no_method_error.rb
+++ b/activesupport/test/autoloading_fixtures/raises_no_method_error.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RaisesNoMethodError
self.foobar_method_doesnt_exist
end
diff --git a/activesupport/test/autoloading_fixtures/requires_constant.rb b/activesupport/test/autoloading_fixtures/requires_constant.rb
index 14804a0de0..6e51998949 100644
--- a/activesupport/test/autoloading_fixtures/requires_constant.rb
+++ b/activesupport/test/autoloading_fixtures/requires_constant.rb
@@ -1,5 +1,6 @@
+# frozen_string_literal: true
+
require "loaded_constant"
module RequiresConstant
end
-
diff --git a/activesupport/test/autoloading_fixtures/should_not_be_required.rb b/activesupport/test/autoloading_fixtures/should_not_be_required.rb
index 1fcf170cc5..8deffa1816 100644
--- a/activesupport/test/autoloading_fixtures/should_not_be_required.rb
+++ b/activesupport/test/autoloading_fixtures/should_not_be_required.rb
@@ -1 +1,3 @@
+# frozen_string_literal: true
+
ShouldNotBeAutoloaded = 0
diff --git a/activesupport/test/autoloading_fixtures/throws.rb b/activesupport/test/autoloading_fixtures/throws.rb
new file mode 100644
index 0000000000..b6fb391032
--- /dev/null
+++ b/activesupport/test/autoloading_fixtures/throws.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+Throws = 1
+_ = A::B # Autoloading recursion, expected to be discarded.
+
+throw :t
diff --git a/activesupport/test/autoloading_fixtures/typo.rb b/activesupport/test/autoloading_fixtures/typo.rb
index 8e047f5fd4..d45cddbcf5 100644
--- a/activesupport/test/autoloading_fixtures/typo.rb
+++ b/activesupport/test/autoloading_fixtures/typo.rb
@@ -1,2 +1,3 @@
-TypO = 1
+# frozen_string_literal: true
+TypO = 1
diff --git a/activesupport/test/benchmarkable_test.rb b/activesupport/test/benchmarkable_test.rb
index 04d4f5e503..59a71d99be 100644
--- a/activesupport/test/benchmarkable_test.rb
+++ b/activesupport/test/benchmarkable_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class BenchmarkableTest < ActiveSupport::TestCase
include ActiveSupport::Benchmarkable
@@ -24,7 +26,7 @@ class BenchmarkableTest < ActiveSupport::TestCase
def test_without_block
assert_raise(LocalJumpError) { benchmark }
- assert buffer.empty?
+ assert_empty buffer
end
def test_defaults
@@ -36,27 +38,41 @@ class BenchmarkableTest < ActiveSupport::TestCase
def test_with_message
i_was_run = false
- benchmark('test_run') { i_was_run = true }
+ benchmark("test_run") { i_was_run = true }
assert i_was_run
- assert_last_logged 'test_run'
+ assert_last_logged "test_run"
+ end
+
+ def test_with_silence
+ assert_difference "buffer.count", +2 do
+ benchmark("test_run") do
+ logger.info "SOMETHING"
+ end
+ end
+
+ assert_difference "buffer.count", +1 do
+ benchmark("test_run", silence: true) do
+ logger.info "NOTHING"
+ end
+ end
end
def test_within_level
logger.level = ActiveSupport::Logger::DEBUG
- benchmark('included_debug_run', :level => :debug) { }
- assert_last_logged 'included_debug_run'
+ benchmark("included_debug_run", level: :debug) { }
+ assert_last_logged "included_debug_run"
end
def test_outside_level
logger.level = ActiveSupport::Logger::ERROR
- benchmark('skipped_debug_run', :level => :debug) { }
+ benchmark("skipped_debug_run", level: :debug) { }
assert_no_match(/skipped_debug_run/, buffer.last)
ensure
logger.level = ActiveSupport::Logger::DEBUG
end
private
- def assert_last_logged(message = 'Benchmarking')
+ def assert_last_logged(message = "Benchmarking")
assert_match(/^#{message} \(.*\)$/, buffer.last)
end
end
diff --git a/activesupport/test/broadcast_logger_test.rb b/activesupport/test/broadcast_logger_test.rb
index 6d4e3b74f7..7dfa8a62bd 100644
--- a/activesupport/test/broadcast_logger_test.rb
+++ b/activesupport/test/broadcast_logger_test.rb
@@ -1,82 +1,181 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module ActiveSupport
class BroadcastLoggerTest < TestCase
attr_reader :logger, :log1, :log2
- def setup
+
+ setup do
@log1 = FakeLogger.new
@log2 = FakeLogger.new
@log1.extend Logger.broadcast @log2
@logger = @log1
end
- def test_debug
- logger.debug "foo"
- assert_equal 'foo', log1.adds.first[2]
- assert_equal 'foo', log2.adds.first[2]
+ Logger::Severity.constants.each do |level_name|
+ method = level_name.downcase
+ level = Logger::Severity.const_get(level_name)
+
+ test "##{method} adds the message to all loggers" do
+ logger.send(method, "msg")
+
+ assert_equal [level, "msg", nil], log1.adds.first
+ assert_equal [level, "msg", nil], log2.adds.first
+ end
end
- def test_close
+ test "#close broadcasts to all loggers" do
logger.close
- assert log1.closed, 'should be closed'
- assert log2.closed, 'should be closed'
+
+ assert log1.closed, "should be closed"
+ assert log2.closed, "should be closed"
end
- def test_chevrons
+ test "#<< shovels the value into all loggers" do
logger << "foo"
+
assert_equal %w{ foo }, log1.chevrons
assert_equal %w{ foo }, log2.chevrons
end
- def test_level
- assert_nil logger.level
- logger.level = 10
- assert_equal 10, log1.level
- assert_equal 10, log2.level
+ test "#level= assigns the level to all loggers" do
+ assert_equal ::Logger::DEBUG, logger.level
+ logger.level = ::Logger::FATAL
+
+ assert_equal ::Logger::FATAL, log1.level
+ assert_equal ::Logger::FATAL, log2.level
end
- def test_progname
+ test "#progname= assigns to all the loggers" do
assert_nil logger.progname
- logger.progname = 10
- assert_equal 10, log1.progname
- assert_equal 10, log2.progname
+ logger.progname = ::Logger::FATAL
+
+ assert_equal ::Logger::FATAL, log1.progname
+ assert_equal ::Logger::FATAL, log2.progname
end
- def test_formatter
+ test "#formatter= assigns to all the loggers" do
assert_nil logger.formatter
- logger.formatter = 10
- assert_equal 10, log1.formatter
- assert_equal 10, log2.formatter
+ logger.formatter = ::Logger::FATAL
+
+ assert_equal ::Logger::FATAL, log1.formatter
+ assert_equal ::Logger::FATAL, log2.formatter
+ end
+
+ test "#local_level= assigns the local_level to all loggers" do
+ assert_equal ::Logger::DEBUG, logger.local_level
+ logger.local_level = ::Logger::FATAL
+
+ assert_equal ::Logger::FATAL, log1.local_level
+ 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"
+ end
+
+ assert_equal [], log1.adds
+ assert_equal [], log2.adds
+ end
+
+ test "#silence does not silence at or above ERROR" do
+ logger.silence do
+ logger.error "from error"
+ logger.unknown "from unknown"
+ end
+
+ assert_equal [[::Logger::ERROR, "from error", nil], [::Logger::UNKNOWN, "from unknown", nil]], log1.adds
+ assert_equal [[::Logger::ERROR, "from error", nil], [::Logger::UNKNOWN, "from unknown", nil]], log2.adds
+ end
+
+ test "#silence allows you to override the silence level" do
+ logger.silence(::Logger::FATAL) do
+ logger.error "unseen"
+ logger.fatal "seen"
+ end
+
+ assert_equal [[::Logger::FATAL, "seen", nil]], log1.adds
+ assert_equal [[::Logger::FATAL, "seen", nil]], log2.adds
+ end
+
+ test "Including top constant LoggerSilence is deprecated" do
+ assert_deprecated("Please use `ActiveSupport::LoggerSilence`") do
+ Class.new(CustomLogger) do
+ include ::LoggerSilence
+ end
+ end
end
- class FakeLogger
+ class CustomLogger
+ include ActiveSupport::LoggerSilence
+
attr_reader :adds, :closed, :chevrons
- attr_accessor :level, :progname, :formatter
+ attr_accessor :level, :progname, :formatter, :local_level
def initialize
- @adds = []
- @closed = false
- @chevrons = []
- @level = nil
- @progname = nil
- @formatter = nil
+ @adds = []
+ @closed = false
+ @chevrons = []
+ @level = ::Logger::DEBUG
+ @local_level = ::Logger::DEBUG
+ @progname = nil
+ @formatter = nil
end
- def debug msg, &block
- add(:omg, nil, msg, &block)
+ def debug(message, &block)
+ add(::Logger::DEBUG, message, &block)
end
- def << x
+ def info(message, &block)
+ add(::Logger::INFO, message, &block)
+ end
+
+ def warn(message, &block)
+ add(::Logger::WARN, message, &block)
+ end
+
+ def error(message, &block)
+ add(::Logger::ERROR, message, &block)
+ end
+
+ def fatal(message, &block)
+ add(::Logger::FATAL, message, &block)
+ end
+
+ def unknown(message, &block)
+ add(::Logger::UNKNOWN, message, &block)
+ end
+
+ def <<(x)
@chevrons << x
end
- def add(*args)
- @adds << args
+ def add(message_level, message = nil, progname = nil, &block)
+ @adds << [message_level, message, progname] if message_level >= local_level
end
def close
@closed = true
end
end
+
+ class FakeLogger < CustomLogger
+ end
end
end
diff --git a/activesupport/test/cache/behaviors.rb b/activesupport/test/cache/behaviors.rb
new file mode 100644
index 0000000000..2d39976be3
--- /dev/null
+++ b/activesupport/test/cache/behaviors.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require_relative "behaviors/autoloading_cache_behavior"
+require_relative "behaviors/cache_delete_matched_behavior"
+require_relative "behaviors/cache_increment_decrement_behavior"
+require_relative "behaviors/cache_instrumentation_behavior"
+require_relative "behaviors/cache_store_behavior"
+require_relative "behaviors/cache_store_version_behavior"
+require_relative "behaviors/connection_pool_behavior"
+require_relative "behaviors/encoded_key_cache_behavior"
+require_relative "behaviors/failure_safety_behavior"
+require_relative "behaviors/local_cache_behavior"
diff --git a/activesupport/test/cache/behaviors/autoloading_cache_behavior.rb b/activesupport/test/cache/behaviors/autoloading_cache_behavior.rb
new file mode 100644
index 0000000000..b340eb6c48
--- /dev/null
+++ b/activesupport/test/cache/behaviors/autoloading_cache_behavior.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require "dependencies_test_helpers"
+
+module AutoloadingCacheBehavior
+ include DependenciesTestHelpers
+
+ def test_simple_autoloading
+ with_autoloading_fixtures do
+ @cache.write("foo", EM.new)
+ end
+
+ remove_constants(:EM)
+ ActiveSupport::Dependencies.clear
+
+ with_autoloading_fixtures do
+ assert_kind_of EM, @cache.read("foo")
+ end
+
+ remove_constants(:EM)
+ ActiveSupport::Dependencies.clear
+ end
+
+ def test_two_classes_autoloading
+ with_autoloading_fixtures do
+ @cache.write("foo", [EM.new, ClassFolder.new])
+ end
+
+ remove_constants(:EM, :ClassFolder)
+ ActiveSupport::Dependencies.clear
+
+ with_autoloading_fixtures do
+ loaded = @cache.read("foo")
+ assert_kind_of Array, loaded
+ assert_equal 2, loaded.size
+ assert_kind_of EM, loaded[0]
+ assert_kind_of ClassFolder, loaded[1]
+ end
+
+ remove_constants(:EM, :ClassFolder)
+ ActiveSupport::Dependencies.clear
+ end
+end
diff --git a/activesupport/test/cache/behaviors/cache_delete_matched_behavior.rb b/activesupport/test/cache/behaviors/cache_delete_matched_behavior.rb
new file mode 100644
index 0000000000..ed8eba8fc2
--- /dev/null
+++ b/activesupport/test/cache/behaviors/cache_delete_matched_behavior.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module CacheDeleteMatchedBehavior
+ def test_delete_matched
+ @cache.write("foo", "bar")
+ @cache.write("fu", "baz")
+ @cache.write("foo/bar", "baz")
+ @cache.write("fu/baz", "bar")
+ @cache.delete_matched(/oo/)
+ assert_not @cache.exist?("foo")
+ assert @cache.exist?("fu")
+ assert_not @cache.exist?("foo/bar")
+ assert @cache.exist?("fu/baz")
+ end
+end
diff --git a/activesupport/test/cache/behaviors/cache_increment_decrement_behavior.rb b/activesupport/test/cache/behaviors/cache_increment_decrement_behavior.rb
new file mode 100644
index 0000000000..16b7abc679
--- /dev/null
+++ b/activesupport/test/cache/behaviors/cache_increment_decrement_behavior.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module CacheIncrementDecrementBehavior
+ def test_increment
+ @cache.write("foo", 1, raw: true)
+ assert_equal 1, @cache.read("foo").to_i
+ assert_equal 2, @cache.increment("foo")
+ assert_equal 2, @cache.read("foo").to_i
+ assert_equal 3, @cache.increment("foo")
+ assert_equal 3, @cache.read("foo").to_i
+
+ missing = @cache.increment("bar")
+ assert(missing.nil? || missing == 1)
+ end
+
+ def test_decrement
+ @cache.write("foo", 3, raw: true)
+ assert_equal 3, @cache.read("foo").to_i
+ assert_equal 2, @cache.decrement("foo")
+ assert_equal 2, @cache.read("foo").to_i
+ assert_equal 1, @cache.decrement("foo")
+ assert_equal 1, @cache.read("foo").to_i
+
+ missing = @cache.decrement("bar")
+ assert(missing.nil? || missing == -1)
+ end
+end
diff --git a/activesupport/test/cache/behaviors/cache_instrumentation_behavior.rb b/activesupport/test/cache/behaviors/cache_instrumentation_behavior.rb
new file mode 100644
index 0000000000..a4abdd37b9
--- /dev/null
+++ b/activesupport/test/cache/behaviors/cache_instrumentation_behavior.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module CacheInstrumentationBehavior
+ def test_fetch_multi_uses_write_multi_entries_store_provider_interface
+ assert_called(@cache, :write_multi_entries) do
+ @cache.fetch_multi "a", "b", "c" do |key|
+ key * 2
+ end
+ end
+ end
+
+ def test_write_multi_instrumentation
+ writes = { "a" => "aa", "b" => "bb" }
+
+ events = with_instrumentation "write_multi" do
+ @cache.write_multi(writes)
+ end
+
+ assert_equal %w[ cache_write_multi.active_support ], events.map(&:name)
+ assert_nil events[0].payload[:super_operation]
+ assert_equal({ "a" => "aa", "b" => "bb" }, events[0].payload[:key])
+ end
+
+ def test_instrumentation_with_fetch_multi_as_super_operation
+ @cache.write("b", "bb")
+
+ events = with_instrumentation "read_multi" do
+ @cache.fetch_multi("a", "b") { |key| key * 2 }
+ end
+
+ assert_equal %w[ cache_read_multi.active_support ], events.map(&:name)
+ assert_equal :fetch_multi, events[0].payload[:super_operation]
+ assert_equal ["b"], events[0].payload[:hits]
+ end
+
+ def test_read_multi_instrumentation
+ @cache.write("b", "bb")
+
+ events = with_instrumentation "read_multi" do
+ @cache.read_multi("a", "b") { |key| key * 2 }
+ end
+
+ assert_equal %w[ cache_read_multi.active_support ], events.map(&:name)
+ assert_equal ["b"], events[0].payload[:hits]
+ end
+
+ private
+ def with_instrumentation(method)
+ event_name = "cache_#{method}.active_support"
+
+ [].tap do |events|
+ ActiveSupport::Notifications.subscribe event_name do |*args|
+ events << ActiveSupport::Notifications::Event.new(*args)
+ end
+ yield
+ end
+ ensure
+ ActiveSupport::Notifications.unsubscribe event_name
+ end
+end
diff --git a/activesupport/test/cache/behaviors/cache_store_behavior.rb b/activesupport/test/cache/behaviors/cache_store_behavior.rb
new file mode 100644
index 0000000000..9f54b1e7de
--- /dev/null
+++ b/activesupport/test/cache/behaviors/cache_store_behavior.rb
@@ -0,0 +1,537 @@
+# frozen_string_literal: true
+
+# Tests the base functionality that should be identical across all cache stores.
+module CacheStoreBehavior
+ def test_should_read_and_write_strings
+ assert @cache.write("foo", "bar")
+ assert_equal "bar", @cache.read("foo")
+ end
+
+ def test_should_overwrite
+ @cache.write("foo", "bar")
+ @cache.write("foo", "baz")
+ assert_equal "baz", @cache.read("foo")
+ end
+
+ def test_fetch_without_cache_miss
+ @cache.write("foo", "bar")
+ assert_not_called(@cache, :write) do
+ assert_equal "bar", @cache.fetch("foo") { "baz" }
+ end
+ end
+
+ def test_fetch_with_cache_miss
+ assert_called_with(@cache, :write, ["foo", "baz", @cache.options]) do
+ assert_equal "baz", @cache.fetch("foo") { "baz" }
+ end
+ end
+
+ def test_fetch_with_cache_miss_passes_key_to_block
+ cache_miss = false
+ assert_equal 3, @cache.fetch("foo") { |key| cache_miss = true; key.length }
+ assert cache_miss
+
+ cache_miss = false
+ assert_equal 3, @cache.fetch("foo") { |key| cache_miss = true; key.length }
+ assert_not cache_miss
+ end
+
+ def test_fetch_with_forced_cache_miss
+ @cache.write("foo", "bar")
+ assert_not_called(@cache, :read) do
+ assert_called_with(@cache, :write, ["foo", "bar", @cache.options.merge(force: true)]) do
+ @cache.fetch("foo", force: true) { "bar" }
+ end
+ end
+ end
+
+ def test_fetch_with_cached_nil
+ @cache.write("foo", nil)
+ assert_not_called(@cache, :write) do
+ assert_nil @cache.fetch("foo") { "baz" }
+ end
+ end
+
+ def test_fetch_cache_miss_with_skip_nil
+ assert_not_called(@cache, :write) do
+ assert_nil @cache.fetch("foo", skip_nil: true) { nil }
+ assert_equal false, @cache.exist?("foo")
+ end
+ end
+
+ def test_fetch_with_forced_cache_miss_with_block
+ @cache.write("foo", "bar")
+ assert_equal "foo_bar", @cache.fetch("foo", force: true) { "foo_bar" }
+ end
+
+ def test_fetch_with_forced_cache_miss_without_block
+ @cache.write("foo", "bar")
+ assert_raises(ArgumentError) do
+ @cache.fetch("foo", force: true)
+ end
+
+ assert_equal "bar", @cache.read("foo")
+ end
+
+ def test_should_read_and_write_hash
+ assert @cache.write("foo", a: "b")
+ assert_equal({ a: "b" }, @cache.read("foo"))
+ end
+
+ def test_should_read_and_write_integer
+ assert @cache.write("foo", 1)
+ assert_equal 1, @cache.read("foo")
+ end
+
+ def test_should_read_and_write_nil
+ assert @cache.write("foo", nil)
+ assert_nil @cache.read("foo")
+ end
+
+ def test_should_read_and_write_false
+ assert @cache.write("foo", false)
+ assert_equal false, @cache.read("foo")
+ end
+
+ def test_read_multi
+ @cache.write("foo", "bar")
+ @cache.write("fu", "baz")
+ @cache.write("fud", "biz")
+ assert_equal({ "foo" => "bar", "fu" => "baz" }, @cache.read_multi("foo", "fu"))
+ end
+
+ def test_read_multi_with_expires
+ time = Time.now
+ @cache.write("foo", "bar", expires_in: 10)
+ @cache.write("fu", "baz")
+ @cache.write("fud", "biz")
+ Time.stub(:now, time + 11) do
+ assert_equal({ "fu" => "baz" }, @cache.read_multi("foo", "fu"))
+ end
+ end
+
+ def test_fetch_multi
+ @cache.write("foo", "bar")
+ @cache.write("fud", "biz")
+
+ values = @cache.fetch_multi("foo", "fu", "fud") { |value| value * 2 }
+
+ assert_equal({ "foo" => "bar", "fu" => "fufu", "fud" => "biz" }, values)
+ assert_equal("fufu", @cache.read("fu"))
+ end
+
+ def test_fetch_multi_without_expires_in
+ @cache.write("foo", "bar")
+ @cache.write("fud", "biz")
+
+ values = @cache.fetch_multi("foo", "fu", "fud", expires_in: nil) { |value| value * 2 }
+
+ assert_equal({ "foo" => "bar", "fu" => "fufu", "fud" => "biz" }, values)
+ assert_equal("fufu", @cache.read("fu"))
+ end
+
+ def test_multi_with_objects
+ cache_struct = Struct.new(:cache_key, :title)
+ foo = cache_struct.new("foo", "FOO!")
+ bar = cache_struct.new("bar")
+
+ @cache.write("bar", "BAM!")
+
+ values = @cache.fetch_multi(foo, bar) { |object| object.title }
+
+ assert_equal({ foo => "FOO!", bar => "BAM!" }, values)
+ end
+
+ def test_fetch_multi_without_block
+ assert_raises(ArgumentError) do
+ @cache.fetch_multi("foo")
+ end
+ end
+
+ # Use strings that are guaranteed to compress well, so we can easily tell if
+ # the compression kicked in or not.
+ SMALL_STRING = "0" * 100
+ LARGE_STRING = "0" * 2.kilobytes
+
+ SMALL_OBJECT = { data: SMALL_STRING }
+ LARGE_OBJECT = { data: LARGE_STRING }
+
+ def test_nil_with_default_compression_settings
+ assert_uncompressed(nil)
+ end
+
+ def test_nil_with_compress_true
+ assert_uncompressed(nil, compress: true)
+ end
+
+ def test_nil_with_compress_false
+ assert_uncompressed(nil, compress: false)
+ end
+
+ def test_nil_with_compress_low_compress_threshold
+ assert_uncompressed(nil, compress: true, compress_threshold: 1)
+ end
+
+ def test_small_string_with_default_compression_settings
+ assert_uncompressed(SMALL_STRING)
+ end
+
+ def test_small_string_with_compress_true
+ assert_uncompressed(SMALL_STRING, compress: true)
+ end
+
+ def test_small_string_with_compress_false
+ assert_uncompressed(SMALL_STRING, compress: false)
+ end
+
+ def test_small_string_with_low_compress_threshold
+ assert_compressed(SMALL_STRING, compress: true, compress_threshold: 1)
+ end
+
+ def test_small_object_with_default_compression_settings
+ assert_uncompressed(SMALL_OBJECT)
+ end
+
+ def test_small_object_with_compress_true
+ assert_uncompressed(SMALL_OBJECT, compress: true)
+ end
+
+ def test_small_object_with_compress_false
+ assert_uncompressed(SMALL_OBJECT, compress: false)
+ end
+
+ def test_small_object_with_low_compress_threshold
+ assert_compressed(SMALL_OBJECT, compress: true, compress_threshold: 1)
+ end
+
+ def test_large_string_with_default_compression_settings
+ assert_compressed(LARGE_STRING)
+ end
+
+ def test_large_string_with_compress_true
+ assert_compressed(LARGE_STRING, compress: true)
+ end
+
+ def test_large_string_with_compress_false
+ assert_uncompressed(LARGE_STRING, compress: false)
+ end
+
+ def test_large_string_with_high_compress_threshold
+ assert_uncompressed(LARGE_STRING, compress: true, compress_threshold: 1.megabyte)
+ end
+
+ def test_large_object_with_default_compression_settings
+ assert_compressed(LARGE_OBJECT)
+ end
+
+ def test_large_object_with_compress_true
+ assert_compressed(LARGE_OBJECT, compress: true)
+ end
+
+ def test_large_object_with_compress_false
+ assert_uncompressed(LARGE_OBJECT, compress: false)
+ end
+
+ def test_large_object_with_high_compress_threshold
+ assert_uncompressed(LARGE_OBJECT, compress: true, compress_threshold: 1.megabyte)
+ end
+
+ def test_incompressable_data
+ assert_uncompressed(nil, compress: true, compress_threshold: 1)
+ assert_uncompressed(true, compress: true, compress_threshold: 1)
+ assert_uncompressed(false, compress: true, compress_threshold: 1)
+ assert_uncompressed(0, compress: true, compress_threshold: 1)
+ assert_uncompressed(1.2345, compress: true, compress_threshold: 1)
+ assert_uncompressed("", compress: true, compress_threshold: 1)
+
+ incompressible = nil
+
+ # generate an incompressible string
+ loop do
+ incompressible = SecureRandom.random_bytes(1.kilobyte)
+ break if incompressible.bytesize < Zlib::Deflate.deflate(incompressible).bytesize
+ end
+
+ assert_uncompressed(incompressible, compress: true, compress_threshold: 1)
+ end
+
+ def test_cache_key
+ obj = Object.new
+ def obj.cache_key
+ :foo
+ end
+ @cache.write(obj, "bar")
+ assert_equal "bar", @cache.read("foo")
+ end
+
+ def test_param_as_cache_key
+ obj = Object.new
+ def obj.to_param
+ "foo"
+ end
+ @cache.write(obj, "bar")
+ assert_equal "bar", @cache.read("foo")
+ end
+
+ def test_unversioned_cache_key
+ obj = Object.new
+ def obj.cache_key
+ "foo"
+ end
+ def obj.cache_key_with_version
+ "foo-v1"
+ end
+ @cache.write(obj, "bar")
+ assert_equal "bar", @cache.read("foo")
+ end
+
+ def test_array_as_cache_key
+ @cache.write([:fu, "foo"], "bar")
+ assert_equal "bar", @cache.read("fu/foo")
+ end
+
+ InstanceTest = Struct.new(:name, :id) do
+ def cache_key
+ "#{name}/#{id}"
+ end
+
+ def to_param
+ "hello"
+ end
+ end
+
+ def test_array_with_single_instance_as_cache_key_uses_cache_key_method
+ test_instance_one = InstanceTest.new("test", 1)
+ test_instance_two = InstanceTest.new("test", 2)
+
+ @cache.write([test_instance_one], "one")
+ @cache.write([test_instance_two], "two")
+
+ assert_equal "one", @cache.read([test_instance_one])
+ assert_equal "two", @cache.read([test_instance_two])
+ end
+
+ def test_array_with_multiple_instances_as_cache_key_uses_cache_key_method
+ test_instance_one = InstanceTest.new("test", 1)
+ test_instance_two = InstanceTest.new("test", 2)
+ test_instance_three = InstanceTest.new("test", 3)
+
+ @cache.write([test_instance_one, test_instance_three], "one")
+ @cache.write([test_instance_two, test_instance_three], "two")
+
+ assert_equal "one", @cache.read([test_instance_one, test_instance_three])
+ assert_equal "two", @cache.read([test_instance_two, test_instance_three])
+ end
+
+ def test_format_of_expanded_key_for_single_instance
+ test_instance_one = InstanceTest.new("test", 1)
+
+ expanded_key = @cache.send(:expanded_key, test_instance_one)
+
+ assert_equal expanded_key, test_instance_one.cache_key
+ end
+
+ def test_format_of_expanded_key_for_single_instance_in_array
+ test_instance_one = InstanceTest.new("test", 1)
+
+ expanded_key = @cache.send(:expanded_key, [test_instance_one])
+
+ assert_equal expanded_key, test_instance_one.cache_key
+ end
+
+ def test_hash_as_cache_key
+ @cache.write({ foo: 1, fu: 2 }, "bar")
+ assert_equal "bar", @cache.read("foo=1/fu=2")
+ end
+
+ def test_keys_are_case_sensitive
+ @cache.write("foo", "bar")
+ assert_nil @cache.read("FOO")
+ end
+
+ def test_exist
+ @cache.write("foo", "bar")
+ assert_equal true, @cache.exist?("foo")
+ assert_equal false, @cache.exist?("bar")
+ end
+
+ def test_nil_exist
+ @cache.write("foo", nil)
+ assert @cache.exist?("foo")
+ end
+
+ def test_delete
+ @cache.write("foo", "bar")
+ assert @cache.exist?("foo")
+ assert @cache.delete("foo")
+ assert_not @cache.exist?("foo")
+ end
+
+ def test_original_store_objects_should_not_be_immutable
+ bar = +"bar"
+ @cache.write("foo", bar)
+ assert_nothing_raised { bar.gsub!(/.*/, "baz") }
+ end
+
+ def test_expires_in
+ time = Time.local(2008, 4, 24)
+
+ Time.stub(:now, time) do
+ @cache.write("foo", "bar")
+ assert_equal "bar", @cache.read("foo")
+ end
+
+ Time.stub(:now, time + 30) do
+ assert_equal "bar", @cache.read("foo")
+ end
+
+ Time.stub(:now, time + 61) do
+ assert_nil @cache.read("foo")
+ end
+ end
+
+ def test_race_condition_protection_skipped_if_not_defined
+ @cache.write("foo", "bar")
+ time = @cache.send(:read_entry, @cache.send(:normalize_key, "foo", {}), {}).expires_at
+
+ Time.stub(:now, Time.at(time)) do
+ result = @cache.fetch("foo") do
+ assert_nil @cache.read("foo")
+ "baz"
+ end
+ assert_equal "baz", result
+ end
+ end
+
+ def test_race_condition_protection_is_limited
+ time = Time.now
+ @cache.write("foo", "bar", expires_in: 60)
+ Time.stub(:now, time + 71) do
+ result = @cache.fetch("foo", race_condition_ttl: 10) do
+ assert_nil @cache.read("foo")
+ "baz"
+ end
+ assert_equal "baz", result
+ end
+ end
+
+ def test_race_condition_protection_is_safe
+ time = Time.now
+ @cache.write("foo", "bar", expires_in: 60)
+ Time.stub(:now, time + 61) do
+ begin
+ @cache.fetch("foo", race_condition_ttl: 10) do
+ assert_equal "bar", @cache.read("foo")
+ raise ArgumentError.new
+ end
+ rescue ArgumentError
+ end
+ assert_equal "bar", @cache.read("foo")
+ end
+ Time.stub(:now, time + 91) do
+ assert_nil @cache.read("foo")
+ end
+ end
+
+ def test_race_condition_protection
+ time = Time.now
+ @cache.write("foo", "bar", expires_in: 60)
+ Time.stub(:now, time + 61) do
+ result = @cache.fetch("foo", race_condition_ttl: 10) do
+ assert_equal "bar", @cache.read("foo")
+ "baz"
+ end
+ assert_equal "baz", result
+ end
+ end
+
+ def test_crazy_key_characters
+ crazy_key = "#/:*(<+=> )&$%@?;'\"\'`~-"
+ assert @cache.write(crazy_key, "1", raw: true)
+ assert_equal "1", @cache.read(crazy_key)
+ assert_equal "1", @cache.fetch(crazy_key)
+ assert @cache.delete(crazy_key)
+ assert_equal "2", @cache.fetch(crazy_key, raw: true) { "2" }
+ assert_equal 3, @cache.increment(crazy_key)
+ assert_equal 2, @cache.decrement(crazy_key)
+ end
+
+ def test_really_long_keys
+ key = "x" * 2048
+ assert @cache.write(key, "bar")
+ assert_equal "bar", @cache.read(key)
+ assert_equal "bar", @cache.fetch(key)
+ assert_nil @cache.read("#{key}x")
+ assert_equal({ key => "bar" }, @cache.read_multi(key))
+ assert @cache.delete(key)
+ end
+
+ def test_cache_hit_instrumentation
+ key = "test_key"
+ @events = []
+ ActiveSupport::Notifications.subscribe "cache_read.active_support" do |*args|
+ @events << ActiveSupport::Notifications::Event.new(*args)
+ end
+ assert @cache.write(key, "1", raw: true)
+ assert @cache.fetch(key) { }
+ assert_equal 1, @events.length
+ assert_equal "cache_read.active_support", @events[0].name
+ assert_equal :fetch, @events[0].payload[:super_operation]
+ assert @events[0].payload[:hit]
+ ensure
+ ActiveSupport::Notifications.unsubscribe "cache_read.active_support"
+ end
+
+ def test_cache_miss_instrumentation
+ @events = []
+ ActiveSupport::Notifications.subscribe(/^cache_(.*)\.active_support$/) do |*args|
+ @events << ActiveSupport::Notifications::Event.new(*args)
+ end
+ assert_not @cache.fetch("bad_key") { }
+ assert_equal 3, @events.length
+ assert_equal "cache_read.active_support", @events[0].name
+ assert_equal "cache_generate.active_support", @events[1].name
+ assert_equal "cache_write.active_support", @events[2].name
+ assert_equal :fetch, @events[0].payload[:super_operation]
+ assert_not @events[0].payload[:hit]
+ ensure
+ ActiveSupport::Notifications.unsubscribe "cache_read.active_support"
+ end
+
+ private
+
+ def assert_compressed(value, **options)
+ assert_compression(true, value, **options)
+ end
+
+ def assert_uncompressed(value, **options)
+ assert_compression(false, value, **options)
+ end
+
+ def assert_compression(should_compress, value, **options)
+ freeze_time do
+ @cache.write("actual", value, options)
+ @cache.write("uncompressed", value, options.merge(compress: false))
+ end
+
+ if value.nil?
+ assert_nil @cache.read("actual")
+ assert_nil @cache.read("uncompressed")
+ else
+ assert_equal value, @cache.read("actual")
+ assert_equal value, @cache.read("uncompressed")
+ end
+
+ actual_entry = @cache.send(:read_entry, @cache.send(:normalize_key, "actual", {}), {})
+ uncompressed_entry = @cache.send(:read_entry, @cache.send(:normalize_key, "uncompressed", {}), {})
+
+ actual_size = Marshal.dump(actual_entry).bytesize
+ uncompressed_size = Marshal.dump(uncompressed_entry).bytesize
+
+ if should_compress
+ assert_operator actual_size, :<, uncompressed_size, "value should be compressed"
+ else
+ assert_equal uncompressed_size, actual_size, "value should not be compressed"
+ end
+ end
+end
diff --git a/activesupport/test/cache/behaviors/cache_store_version_behavior.rb b/activesupport/test/cache/behaviors/cache_store_version_behavior.rb
new file mode 100644
index 0000000000..805f061839
--- /dev/null
+++ b/activesupport/test/cache/behaviors/cache_store_version_behavior.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+module CacheStoreVersionBehavior
+ ModelWithKeyAndVersion = Struct.new(:cache_key, :cache_version)
+
+ def test_fetch_with_right_version_should_hit
+ @cache.fetch("foo", version: 1) { "bar" }
+ assert_equal "bar", @cache.read("foo", version: 1)
+ end
+
+ def test_fetch_with_wrong_version_should_miss
+ @cache.fetch("foo", version: 1) { "bar" }
+ assert_nil @cache.read("foo", version: 2)
+ end
+
+ def test_read_with_right_version_should_hit
+ @cache.write("foo", "bar", version: 1)
+ assert_equal "bar", @cache.read("foo", version: 1)
+ end
+
+ def test_read_with_wrong_version_should_miss
+ @cache.write("foo", "bar", version: 1)
+ assert_nil @cache.read("foo", version: 2)
+ end
+
+ def test_exist_with_right_version_should_be_true
+ @cache.write("foo", "bar", version: 1)
+ assert @cache.exist?("foo", version: 1)
+ end
+
+ def test_exist_with_wrong_version_should_be_false
+ @cache.write("foo", "bar", version: 1)
+ assert_not @cache.exist?("foo", version: 2)
+ end
+
+ def test_reading_and_writing_with_model_supporting_cache_version
+ m1v1 = ModelWithKeyAndVersion.new("model/1", 1)
+ m1v2 = ModelWithKeyAndVersion.new("model/1", 2)
+
+ @cache.write(m1v1, "bar")
+ assert_equal "bar", @cache.read(m1v1)
+ assert_nil @cache.read(m1v2)
+ end
+
+ def test_reading_and_writing_with_model_supporting_cache_version_using_nested_key
+ m1v1 = ModelWithKeyAndVersion.new("model/1", 1)
+ m1v2 = ModelWithKeyAndVersion.new("model/1", 2)
+
+ @cache.write([ "something", m1v1 ], "bar")
+ assert_equal "bar", @cache.read([ "something", m1v1 ])
+ assert_nil @cache.read([ "something", m1v2 ])
+ end
+
+ def test_fetching_with_model_supporting_cache_version
+ m1v1 = ModelWithKeyAndVersion.new("model/1", 1)
+ m1v2 = ModelWithKeyAndVersion.new("model/1", 2)
+
+ @cache.fetch(m1v1) { "bar" }
+ assert_equal "bar", @cache.fetch(m1v1) { "bu" }
+ assert_equal "bu", @cache.fetch(m1v2) { "bu" }
+ end
+
+ def test_exist_with_model_supporting_cache_version
+ m1v1 = ModelWithKeyAndVersion.new("model/1", 1)
+ m1v2 = ModelWithKeyAndVersion.new("model/1", 2)
+
+ @cache.write(m1v1, "bar")
+ assert @cache.exist?(m1v1)
+ assert_not @cache.fetch(m1v2)
+ end
+
+ def test_fetch_multi_with_model_supporting_cache_version
+ m1v1 = ModelWithKeyAndVersion.new("model/1", 1)
+ m2v1 = ModelWithKeyAndVersion.new("model/2", 1)
+ m2v2 = ModelWithKeyAndVersion.new("model/2", 2)
+
+ first_fetch_values = @cache.fetch_multi(m1v1, m2v1) { |m| m.cache_key }
+ second_fetch_values = @cache.fetch_multi(m1v1, m2v2) { |m| m.cache_key + " 2nd" }
+
+ assert_equal({ m1v1 => "model/1", m2v1 => "model/2" }, first_fetch_values)
+ assert_equal({ m1v1 => "model/1", m2v2 => "model/2 2nd" }, second_fetch_values)
+ end
+
+ def test_version_is_normalized
+ @cache.write("foo", "bar", version: 1)
+ assert_equal "bar", @cache.read("foo", version: "1")
+ end
+end
diff --git a/activesupport/test/cache/behaviors/connection_pool_behavior.rb b/activesupport/test/cache/behaviors/connection_pool_behavior.rb
new file mode 100644
index 0000000000..4d1901a173
--- /dev/null
+++ b/activesupport/test/cache/behaviors/connection_pool_behavior.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module ConnectionPoolBehavior
+ def test_connection_pool
+ Thread.report_on_exception, original_report_on_exception = false, Thread.report_on_exception
+
+ threads = []
+
+ emulating_latency do
+ begin
+ cache = ActiveSupport::Cache.lookup_store(store, { pool_size: 2, pool_timeout: 1 }.merge(store_options))
+ cache.clear
+
+ assert_raises Timeout::Error do
+ # One of the three threads will fail in 1 second because our pool size
+ # is only two.
+ 3.times do
+ threads << Thread.new do
+ cache.read("latency")
+ end
+ end
+
+ threads.each(&:join)
+ end
+ ensure
+ threads.each(&:kill)
+ end
+ end
+ ensure
+ Thread.report_on_exception = original_report_on_exception
+ end
+
+ def test_no_connection_pool
+ threads = []
+
+ emulating_latency do
+ begin
+ cache = ActiveSupport::Cache.lookup_store(store, store_options)
+ cache.clear
+
+ assert_nothing_raised do
+ # Default connection pool size is 5, assuming 10 will make sure that
+ # the connection pool isn't used at all.
+ 10.times do
+ threads << Thread.new do
+ cache.read("latency")
+ end
+ end
+
+ threads.each(&:join)
+ end
+ ensure
+ threads.each(&:kill)
+ end
+ end
+ end
+
+ private
+ def store_options; {}; end
+end
diff --git a/activesupport/test/cache/behaviors/encoded_key_cache_behavior.rb b/activesupport/test/cache/behaviors/encoded_key_cache_behavior.rb
new file mode 100644
index 0000000000..842400f4a3
--- /dev/null
+++ b/activesupport/test/cache/behaviors/encoded_key_cache_behavior.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+# https://rails.lighthouseapp.com/projects/8994/tickets/6225-memcachestore-cant-deal-with-umlauts-and-special-characters
+# The error is caused by character encodings that can't be compared with ASCII-8BIT regular expressions and by special
+# characters like the umlaut in UTF-8.
+module EncodedKeyCacheBehavior
+ Encoding.list.each do |encoding|
+ define_method "test_#{encoding.name.underscore}_encoded_values" do
+ key = (+"foo").force_encoding(encoding)
+ assert @cache.write(key, "1", raw: true)
+ assert_equal "1", @cache.read(key)
+ assert_equal "1", @cache.fetch(key)
+ assert @cache.delete(key)
+ assert_equal "2", @cache.fetch(key, raw: true) { "2" }
+ assert_equal 3, @cache.increment(key)
+ assert_equal 2, @cache.decrement(key)
+ end
+ end
+
+ def test_common_utf8_values
+ key = (+"\xC3\xBCmlaut").force_encoding(Encoding::UTF_8)
+ assert @cache.write(key, "1", raw: true)
+ assert_equal "1", @cache.read(key)
+ assert_equal "1", @cache.fetch(key)
+ assert @cache.delete(key)
+ assert_equal "2", @cache.fetch(key, raw: true) { "2" }
+ assert_equal 3, @cache.increment(key)
+ assert_equal 2, @cache.decrement(key)
+ end
+
+ def test_retains_encoding
+ key = (+"\xC3\xBCmlaut").force_encoding(Encoding::UTF_8)
+ assert @cache.write(key, "1", raw: true)
+ assert_equal Encoding::UTF_8, key.encoding
+ end
+end
diff --git a/activesupport/test/cache/behaviors/failure_safety_behavior.rb b/activesupport/test/cache/behaviors/failure_safety_behavior.rb
new file mode 100644
index 0000000000..43b67d81db
--- /dev/null
+++ b/activesupport/test/cache/behaviors/failure_safety_behavior.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+module FailureSafetyBehavior
+ def test_fetch_read_failure_returns_nil
+ @cache.write("foo", "bar")
+
+ emulating_unavailability do |cache|
+ assert_nil cache.fetch("foo")
+ end
+ end
+
+ def test_fetch_read_failure_does_not_attempt_to_write
+ end
+
+ def test_read_failure_returns_nil
+ @cache.write("foo", "bar")
+
+ emulating_unavailability do |cache|
+ assert_nil cache.read("foo")
+ end
+ end
+
+ def test_read_multi_failure_returns_empty_hash
+ @cache.write_multi("foo" => "bar", "baz" => "quux")
+
+ emulating_unavailability do |cache|
+ assert_equal Hash.new, cache.read_multi("foo", "baz")
+ end
+ end
+
+ def test_write_failure_returns_false
+ emulating_unavailability do |cache|
+ assert_equal false, cache.write("foo", "bar")
+ end
+ end
+
+ def test_write_multi_failure_not_raises
+ emulating_unavailability do |cache|
+ assert_nothing_raised do
+ cache.write_multi("foo" => "bar", "baz" => "quux")
+ end
+ end
+ end
+
+ def test_fetch_multi_failure_returns_fallback_results
+ @cache.write_multi("foo" => "bar", "baz" => "quux")
+
+ emulating_unavailability do |cache|
+ fetched = cache.fetch_multi("foo", "baz") { |k| "unavailable" }
+ assert_equal Hash["foo" => "unavailable", "baz" => "unavailable"], fetched
+ end
+ end
+
+ def test_delete_failure_returns_false
+ @cache.write("foo", "bar")
+
+ emulating_unavailability do |cache|
+ assert_equal false, cache.delete("foo")
+ end
+ end
+
+ def test_exist_failure_returns_false
+ @cache.write("foo", "bar")
+
+ emulating_unavailability do |cache|
+ assert_not cache.exist?("foo")
+ end
+ end
+
+ def test_increment_failure_returns_nil
+ @cache.write("foo", 1, raw: true)
+
+ emulating_unavailability do |cache|
+ assert_nil cache.increment("foo")
+ end
+ end
+
+ def test_decrement_failure_returns_nil
+ @cache.write("foo", 1, raw: true)
+
+ emulating_unavailability do |cache|
+ assert_nil cache.decrement("foo")
+ end
+ end
+
+ def test_clear_failure_returns_nil
+ emulating_unavailability do |cache|
+ assert_nil cache.clear
+ end
+ end
+end
diff --git a/activesupport/test/cache/behaviors/local_cache_behavior.rb b/activesupport/test/cache/behaviors/local_cache_behavior.rb
new file mode 100644
index 0000000000..baa38ba6ac
--- /dev/null
+++ b/activesupport/test/cache/behaviors/local_cache_behavior.rb
@@ -0,0 +1,154 @@
+# frozen_string_literal: true
+
+module LocalCacheBehavior
+ def test_local_writes_are_persistent_on_the_remote_cache
+ retval = @cache.with_local_cache do
+ @cache.write("foo", "bar")
+ end
+ assert retval
+ assert_equal "bar", @cache.read("foo")
+ end
+
+ def test_clear_also_clears_local_cache
+ @cache.with_local_cache do
+ @cache.write("foo", "bar")
+ @cache.clear
+ assert_nil @cache.read("foo")
+ end
+
+ assert_nil @cache.read("foo")
+ end
+
+ def test_cleanup_clears_local_cache_but_not_remote_cache
+ begin
+ @cache.cleanup
+ rescue NotImplementedError
+ skip
+ end
+
+ @cache.with_local_cache do
+ @cache.write("foo", "bar")
+ assert_equal "bar", @cache.read("foo")
+
+ @cache.send(:bypass_local_cache) { @cache.write("foo", "baz") }
+ assert_equal "bar", @cache.read("foo")
+
+ @cache.cleanup
+ assert_equal "baz", @cache.read("foo")
+ end
+ end
+
+ def test_local_cache_of_write
+ @cache.with_local_cache do
+ @cache.write("foo", "bar")
+ @peek.delete("foo")
+ assert_equal "bar", @cache.read("foo")
+ end
+ end
+
+ def test_local_cache_of_read
+ @cache.write("foo", "bar")
+ @cache.with_local_cache do
+ assert_equal "bar", @cache.read("foo")
+ end
+ end
+
+ def test_local_cache_of_read_nil
+ @cache.with_local_cache do
+ assert_nil @cache.read("foo")
+ @cache.send(:bypass_local_cache) { @cache.write "foo", "bar" }
+ assert_nil @cache.read("foo")
+ end
+ end
+
+ def test_local_cache_fetch
+ @cache.with_local_cache do
+ @cache.send(:local_cache).write "foo", "bar"
+ assert_equal "bar", @cache.send(:local_cache).fetch("foo")
+ end
+ end
+
+ def test_local_cache_of_write_nil
+ @cache.with_local_cache do
+ assert @cache.write("foo", nil)
+ assert_nil @cache.read("foo")
+ @peek.write("foo", "bar")
+ assert_nil @cache.read("foo")
+ end
+ end
+
+ def test_local_cache_of_write_with_unless_exist
+ @cache.with_local_cache do
+ @cache.write("foo", "bar")
+ @cache.write("foo", "baz", unless_exist: true)
+ assert_equal @peek.read("foo"), @cache.read("foo")
+ end
+ end
+
+ def test_local_cache_of_delete
+ @cache.with_local_cache do
+ @cache.write("foo", "bar")
+ @cache.delete("foo")
+ assert_nil @cache.read("foo")
+ end
+ end
+
+ def test_local_cache_of_exist
+ @cache.with_local_cache do
+ @cache.write("foo", "bar")
+ @peek.delete("foo")
+ assert @cache.exist?("foo")
+ end
+ end
+
+ def test_local_cache_of_increment
+ @cache.with_local_cache do
+ @cache.write("foo", 1, raw: true)
+ @peek.write("foo", 2, raw: true)
+ @cache.increment("foo")
+ assert_equal 3, @cache.read("foo")
+ end
+ end
+
+ def test_local_cache_of_decrement
+ @cache.with_local_cache do
+ @cache.write("foo", 1, raw: true)
+ @peek.write("foo", 3, raw: true)
+ @cache.decrement("foo")
+ assert_equal 2, @cache.read("foo")
+ end
+ end
+
+ def test_local_cache_of_fetch_multi
+ @cache.with_local_cache do
+ @cache.fetch_multi("foo", "bar") { |_key| true }
+ @peek.delete("foo")
+ @peek.delete("bar")
+ assert_equal true, @cache.read("foo")
+ assert_equal true, @cache.read("bar")
+ end
+ end
+
+ def test_local_cache_of_read_multi
+ @cache.with_local_cache do
+ @cache.write("foo", "foo", raw: true)
+ @cache.write("bar", "bar", raw: true)
+ values = @cache.read_multi("foo", "bar")
+ assert_equal "foo", @cache.read("foo")
+ assert_equal "bar", @cache.read("bar")
+ assert_equal "foo", values["foo"]
+ assert_equal "bar", values["bar"]
+ end
+ end
+
+ def test_middleware
+ app = lambda { |env|
+ result = @cache.write("foo", "bar")
+ assert_equal "bar", @cache.read("foo") # make sure 'foo' was written
+ assert result
+ [200, {}, []]
+ }
+ app = @cache.middleware.new(app)
+ app.call({})
+ end
+end
diff --git a/activesupport/test/cache/cache_entry_test.rb b/activesupport/test/cache/cache_entry_test.rb
new file mode 100644
index 0000000000..ec20a288e1
--- /dev/null
+++ b/activesupport/test/cache/cache_entry_test.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/cache"
+
+class CacheEntryTest < ActiveSupport::TestCase
+ def test_expired
+ entry = ActiveSupport::Cache::Entry.new("value")
+ assert_not entry.expired?, "entry not expired"
+ entry = ActiveSupport::Cache::Entry.new("value", expires_in: 60)
+ assert_not entry.expired?, "entry not expired"
+ Time.stub(:now, Time.now + 61) do
+ assert entry.expired?, "entry is expired"
+ end
+ end
+end
diff --git a/activesupport/test/cache/cache_key_test.rb b/activesupport/test/cache/cache_key_test.rb
new file mode 100644
index 0000000000..c2240d03c2
--- /dev/null
+++ b/activesupport/test/cache/cache_key_test.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/cache"
+
+class CacheKeyTest < ActiveSupport::TestCase
+ def test_entry_legacy_optional_ivars
+ legacy = Class.new(ActiveSupport::Cache::Entry) do
+ def initialize(value, options = {})
+ @value = value
+ @expires_in = nil
+ @created_at = nil
+ super
+ end
+ end
+
+ entry = legacy.new "foo"
+ assert_equal "foo", entry.value
+ end
+
+ def test_expand_cache_key
+ assert_equal "1/2/true", ActiveSupport::Cache.expand_cache_key([1, "2", true])
+ assert_equal "name/1/2/true", ActiveSupport::Cache.expand_cache_key([1, "2", true], :name)
+ end
+
+ def test_expand_cache_key_with_rails_cache_id
+ with_env("RAILS_CACHE_ID" => "c99") do
+ assert_equal "c99/foo", ActiveSupport::Cache.expand_cache_key(:foo)
+ assert_equal "c99/foo", ActiveSupport::Cache.expand_cache_key([:foo])
+ assert_equal "c99/foo/bar", ActiveSupport::Cache.expand_cache_key([:foo, :bar])
+ assert_equal "nm/c99/foo", ActiveSupport::Cache.expand_cache_key(:foo, :nm)
+ assert_equal "nm/c99/foo", ActiveSupport::Cache.expand_cache_key([:foo], :nm)
+ assert_equal "nm/c99/foo/bar", ActiveSupport::Cache.expand_cache_key([:foo, :bar], :nm)
+ end
+ end
+
+ def test_expand_cache_key_with_rails_app_version
+ with_env("RAILS_APP_VERSION" => "rails3") do
+ assert_equal "rails3/foo", ActiveSupport::Cache.expand_cache_key(:foo)
+ end
+ end
+
+ def test_expand_cache_key_rails_cache_id_should_win_over_rails_app_version
+ with_env("RAILS_CACHE_ID" => "c99", "RAILS_APP_VERSION" => "rails3") do
+ assert_equal "c99/foo", ActiveSupport::Cache.expand_cache_key(:foo)
+ end
+ end
+
+ def test_expand_cache_key_respond_to_cache_key
+ key = +"foo"
+ def key.cache_key
+ :foo_key
+ end
+ assert_equal "foo_key", ActiveSupport::Cache.expand_cache_key(key)
+ end
+
+ def test_expand_cache_key_array_with_something_that_responds_to_cache_key
+ key = +"foo"
+ def key.cache_key
+ :foo_key
+ end
+ assert_equal "foo_key", ActiveSupport::Cache.expand_cache_key([key])
+ end
+
+ def test_expand_cache_key_of_nil
+ assert_equal "", ActiveSupport::Cache.expand_cache_key(nil)
+ end
+
+ def test_expand_cache_key_of_false
+ assert_equal "false", ActiveSupport::Cache.expand_cache_key(false)
+ end
+
+ def test_expand_cache_key_of_true
+ assert_equal "true", ActiveSupport::Cache.expand_cache_key(true)
+ end
+
+ def test_expand_cache_key_of_array_like_object
+ assert_equal "foo/bar/baz", ActiveSupport::Cache.expand_cache_key(%w{foo bar baz}.to_enum)
+ end
+
+ private
+
+ def with_env(kv)
+ old_values = {}
+ kv.each { |key, value| old_values[key], ENV[key] = ENV[key], value }
+ yield
+ ensure
+ old_values.each { |key, value| ENV[key] = value }
+ end
+end
diff --git a/activesupport/test/cache/cache_store_logger_test.rb b/activesupport/test/cache/cache_store_logger_test.rb
new file mode 100644
index 0000000000..4648b2d361
--- /dev/null
+++ b/activesupport/test/cache/cache_store_logger_test.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/cache"
+
+class CacheStoreLoggerTest < ActiveSupport::TestCase
+ def setup
+ @cache = ActiveSupport::Cache.lookup_store(:memory_store)
+
+ @buffer = StringIO.new
+ @cache.logger = ActiveSupport::Logger.new(@buffer)
+ end
+
+ def test_logging
+ @cache.fetch("foo") { "bar" }
+ assert_predicate @buffer.string, :present?
+ end
+
+ def test_log_with_string_namespace
+ @cache.fetch("foo", namespace: "string_namespace") { "bar" }
+ assert_match %r{string_namespace:foo}, @buffer.string
+ end
+
+ def test_log_with_proc_namespace
+ proc = Proc.new do
+ "proc_namespace"
+ end
+ @cache.fetch("foo", namespace: proc) { "bar" }
+ assert_match %r{proc_namespace:foo}, @buffer.string
+ end
+
+ def test_mute_logging
+ @cache.mute { @cache.fetch("foo") { "bar" } }
+ assert_predicate @buffer.string, :blank?
+ end
+end
diff --git a/activesupport/test/cache/cache_store_namespace_test.rb b/activesupport/test/cache/cache_store_namespace_test.rb
new file mode 100644
index 0000000000..dfdb3262f2
--- /dev/null
+++ b/activesupport/test/cache/cache_store_namespace_test.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/cache"
+
+class CacheStoreNamespaceTest < ActiveSupport::TestCase
+ def test_static_namespace
+ cache = ActiveSupport::Cache.lookup_store(:memory_store, namespace: "tester")
+ cache.write("foo", "bar")
+ assert_equal "bar", cache.read("foo")
+ assert_equal "bar", cache.instance_variable_get(:@data)["tester:foo"].value
+ end
+
+ def test_proc_namespace
+ test_val = "tester"
+ proc = lambda { test_val }
+ cache = ActiveSupport::Cache.lookup_store(:memory_store, namespace: proc)
+ cache.write("foo", "bar")
+ assert_equal "bar", cache.read("foo")
+ assert_equal "bar", cache.instance_variable_get(:@data)["tester:foo"].value
+ end
+
+ def test_delete_matched_key_start
+ cache = ActiveSupport::Cache.lookup_store(:memory_store, namespace: "tester")
+ cache.write("foo", "bar")
+ cache.write("fu", "baz")
+ cache.delete_matched(/^fo/)
+ assert_not cache.exist?("foo")
+ assert cache.exist?("fu")
+ end
+
+ def test_delete_matched_key
+ cache = ActiveSupport::Cache.lookup_store(:memory_store, namespace: "foo")
+ cache.write("foo", "bar")
+ cache.write("fu", "baz")
+ cache.delete_matched(/OO/i)
+ assert_not cache.exist?("foo")
+ assert cache.exist?("fu")
+ end
+end
diff --git a/activesupport/test/cache/cache_store_setting_test.rb b/activesupport/test/cache/cache_store_setting_test.rb
new file mode 100644
index 0000000000..368cb39f97
--- /dev/null
+++ b/activesupport/test/cache/cache_store_setting_test.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/cache"
+require "dalli"
+
+class CacheStoreSettingTest < ActiveSupport::TestCase
+ def test_memory_store_gets_created_if_no_arguments_passed_to_lookup_store_method
+ store = ActiveSupport::Cache.lookup_store
+ assert_kind_of(ActiveSupport::Cache::MemoryStore, store)
+ end
+
+ def test_memory_store
+ store = ActiveSupport::Cache.lookup_store :memory_store
+ assert_kind_of(ActiveSupport::Cache::MemoryStore, store)
+ end
+
+ def test_file_fragment_cache_store
+ store = ActiveSupport::Cache.lookup_store :file_store, "/path/to/cache/directory"
+ assert_kind_of(ActiveSupport::Cache::FileStore, store)
+ assert_equal "/path/to/cache/directory", store.cache_path
+ end
+
+ def test_mem_cache_fragment_cache_store
+ assert_called_with(Dalli::Client, :new, [%w[localhost], {}]) do
+ store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost"
+ assert_kind_of(ActiveSupport::Cache::MemCacheStore, store)
+ end
+ end
+
+ def test_mem_cache_fragment_cache_store_with_given_mem_cache
+ mem_cache = Dalli::Client.new
+ assert_not_called(Dalli::Client, :new) do
+ store = ActiveSupport::Cache.lookup_store :mem_cache_store, mem_cache
+ assert_kind_of(ActiveSupport::Cache::MemCacheStore, store)
+ end
+ end
+
+ def test_mem_cache_fragment_cache_store_with_not_dalli_client
+ assert_not_called(Dalli::Client, :new) do
+ memcache = Object.new
+ assert_raises(ArgumentError) do
+ ActiveSupport::Cache.lookup_store :mem_cache_store, memcache
+ end
+ end
+ end
+
+ def test_mem_cache_fragment_cache_store_with_multiple_servers
+ assert_called_with(Dalli::Client, :new, [%w[localhost 192.168.1.1], {}]) do
+ store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost", "192.168.1.1"
+ assert_kind_of(ActiveSupport::Cache::MemCacheStore, store)
+ end
+ end
+
+ def test_mem_cache_fragment_cache_store_with_options
+ assert_called_with(Dalli::Client, :new, [%w[localhost 192.168.1.1], { timeout: 10 }]) do
+ store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost", "192.168.1.1", namespace: "foo", timeout: 10
+ assert_kind_of(ActiveSupport::Cache::MemCacheStore, store)
+ assert_equal "foo", store.options[:namespace]
+ end
+ end
+
+ def test_object_assigned_fragment_cache_store
+ store = ActiveSupport::Cache.lookup_store ActiveSupport::Cache::FileStore.new("/path/to/cache/directory")
+ assert_kind_of(ActiveSupport::Cache::FileStore, store)
+ assert_equal "/path/to/cache/directory", store.cache_path
+ end
+end
diff --git a/activesupport/test/cache/local_cache_middleware_test.rb b/activesupport/test/cache/local_cache_middleware_test.rb
new file mode 100644
index 0000000000..e46fa59784
--- /dev/null
+++ b/activesupport/test/cache/local_cache_middleware_test.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/cache"
+
+module ActiveSupport
+ module Cache
+ module Strategy
+ module LocalCache
+ class MiddlewareTest < ActiveSupport::TestCase
+ def test_local_cache_cleared_on_close
+ 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"
+ [200, {}, []]
+ })
+ _, _, body = middleware.call({})
+ assert LocalCacheRegistry.cache_for(key), "should still have a cache"
+ body.each { }
+ assert LocalCacheRegistry.cache_for(key), "should still have a cache"
+ body.close
+ assert_nil LocalCacheRegistry.cache_for(key)
+ end
+
+ def test_local_cache_cleared_and_response_should_be_present_on_invalid_parameters_error
+ 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"
+ raise Rack::Utils::InvalidParameterError
+ })
+ response = middleware.call({})
+ assert response, "response should exist"
+ assert_nil LocalCacheRegistry.cache_for(key)
+ end
+
+ def test_local_cache_cleared_on_exception
+ 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"
+ raise
+ })
+ 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
+ end
+end
diff --git a/activesupport/test/cache/stores/file_store_test.rb b/activesupport/test/cache/stores/file_store_test.rb
new file mode 100644
index 0000000000..f6855bb308
--- /dev/null
+++ b/activesupport/test/cache/stores/file_store_test.rb
@@ -0,0 +1,134 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/cache"
+require_relative "../behaviors"
+require "pathname"
+
+class FileStoreTest < ActiveSupport::TestCase
+ def setup
+ Dir.mkdir(cache_dir) unless File.exist?(cache_dir)
+ @cache = ActiveSupport::Cache.lookup_store(:file_store, cache_dir, expires_in: 60)
+ @peek = ActiveSupport::Cache.lookup_store(:file_store, cache_dir, expires_in: 60)
+ @cache_with_pathname = ActiveSupport::Cache.lookup_store(:file_store, Pathname.new(cache_dir), expires_in: 60)
+
+ @buffer = StringIO.new
+ @cache.logger = ActiveSupport::Logger.new(@buffer)
+ end
+
+ def teardown
+ FileUtils.rm_r(cache_dir)
+ rescue Errno::ENOENT
+ end
+
+ def cache_dir
+ File.join(Dir.pwd, "tmp_cache")
+ end
+
+ include CacheStoreBehavior
+ include CacheStoreVersionBehavior
+ include LocalCacheBehavior
+ include CacheDeleteMatchedBehavior
+ include CacheIncrementDecrementBehavior
+ include CacheInstrumentationBehavior
+ include AutoloadingCacheBehavior
+
+ def test_clear
+ gitkeep = File.join(cache_dir, ".gitkeep")
+ keep = File.join(cache_dir, ".keep")
+ FileUtils.touch([gitkeep, keep])
+ @cache.clear
+ assert File.exist?(gitkeep)
+ assert File.exist?(keep)
+ end
+
+ def test_clear_without_cache_dir
+ FileUtils.rm_r(cache_dir)
+ @cache.clear
+ end
+
+ def test_long_uri_encoded_keys
+ @cache.write("%" * 870, 1)
+ assert_equal 1, @cache.read("%" * 870)
+ end
+
+ def test_key_transformation
+ key = @cache.send(:normalize_key, "views/index?id=1", {})
+ assert_equal "views/index?id=1", @cache.send(:file_path_key, key)
+ end
+
+ def test_key_transformation_with_pathname
+ FileUtils.touch(File.join(cache_dir, "foo"))
+ key = @cache_with_pathname.send(:normalize_key, "views/index?id=1", {})
+ assert_equal "views/index?id=1", @cache_with_pathname.send(:file_path_key, key)
+ end
+
+ # Test that generated cache keys are short enough to have Tempfile stuff added to them and
+ # remain valid
+ def test_filename_max_size
+ key = "#{'A' * ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE}"
+ path = @cache.send(:normalize_key, key, {})
+ basename = File.basename(path)
+ dirname = File.dirname(path)
+ Dir::Tmpname.create(basename, Dir.tmpdir + dirname) do |tmpname, n, opts|
+ assert File.basename(tmpname + ".lock").length <= 255, "Temp filename too long: #{File.basename(tmpname + '.lock').length}"
+ end
+ end
+
+ # Because file systems have a maximum filename size, filenames > max size should be split in to directories
+ # If filename is 'AAAAB', where max size is 4, the returned path should be AAAA/B
+ def test_key_transformation_max_filename_size
+ key = "#{'A' * ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE}B"
+ path = @cache.send(:normalize_key, key, {})
+ assert path.split("/").all? { |dir_name| dir_name.size <= ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE }
+ assert_equal "B", File.basename(path)
+ end
+
+ # If nothing has been stored in the cache, there is a chance the cache directory does not yet exist
+ # Ensure delete_matched gracefully handles this case
+ def test_delete_matched_when_cache_directory_does_not_exist
+ assert_nothing_raised do
+ ActiveSupport::Cache::FileStore.new("/test/cache/directory").delete_matched(/does_not_exist/)
+ end
+ end
+
+ def test_delete_does_not_delete_empty_parent_dir
+ sub_cache_dir = File.join(cache_dir, "subdir/")
+ sub_cache_store = ActiveSupport::Cache::FileStore.new(sub_cache_dir)
+ assert_nothing_raised do
+ assert sub_cache_store.write("foo", "bar")
+ assert sub_cache_store.delete("foo")
+ end
+ assert File.exist?(cache_dir), "Parent of top level cache dir was deleted!"
+ assert File.exist?(sub_cache_dir), "Top level cache dir was deleted!"
+ assert_empty Dir.entries(sub_cache_dir).reject { |f| ActiveSupport::Cache::FileStore::EXCLUDED_DIRS.include?(f) }
+ end
+
+ def test_log_exception_when_cache_read_fails
+ File.stub(:exist?, -> { raise StandardError.new("failed") }) do
+ @cache.send(:read_entry, "winston", {})
+ assert_predicate @buffer.string, :present?
+ end
+ end
+
+ def test_cleanup_removes_all_expired_entries
+ time = Time.now
+ @cache.write("foo", "bar", expires_in: 10)
+ @cache.write("baz", "qux")
+ @cache.write("quux", "corge", expires_in: 20)
+ Time.stub(:now, time + 15) do
+ @cache.cleanup
+ assert_not @cache.exist?("foo")
+ assert @cache.exist?("baz")
+ assert @cache.exist?("quux")
+ assert_equal 2, Dir.glob(File.join(cache_dir, "**")).size
+ end
+ end
+
+ def test_write_with_unless_exist
+ assert_equal true, @cache.write(1, "aaaaaaaaaa")
+ assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true)
+ @cache.write(1, nil)
+ assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true)
+ end
+end
diff --git a/activesupport/test/cache/stores/mem_cache_store_test.rb b/activesupport/test/cache/stores/mem_cache_store_test.rb
new file mode 100644
index 0000000000..f426a37c66
--- /dev/null
+++ b/activesupport/test/cache/stores/mem_cache_store_test.rb
@@ -0,0 +1,139 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/cache"
+require_relative "../behaviors"
+require "dalli"
+
+# Emulates a latency on Dalli's back-end for the key latency to facilitate
+# connection pool testing.
+class SlowDalliClient < Dalli::Client
+ def get(key, options = {})
+ if key =~ /latency/
+ sleep 3
+ else
+ super
+ end
+ end
+end
+
+class UnavailableDalliServer < Dalli::Server
+ def alive?
+ false
+ end
+end
+
+class MemCacheStoreTest < ActiveSupport::TestCase
+ begin
+ ss = Dalli::Client.new("localhost:11211").stats
+ raise Dalli::DalliError unless ss["localhost:11211"]
+
+ MEMCACHE_UP = true
+ rescue Dalli::DalliError
+ $stderr.puts "Skipping memcached tests. Start memcached and try again."
+ MEMCACHE_UP = false
+ end
+
+ def setup
+ skip "memcache server is not up" unless MEMCACHE_UP
+
+ @cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, expires_in: 60)
+ @peek = ActiveSupport::Cache.lookup_store(:mem_cache_store)
+ @data = @cache.instance_variable_get(:@data)
+ @cache.clear
+ @cache.silence!
+ @cache.logger = ActiveSupport::Logger.new(File::NULL)
+ end
+
+ include CacheStoreBehavior
+ include CacheStoreVersionBehavior
+ include LocalCacheBehavior
+ include CacheIncrementDecrementBehavior
+ include CacheInstrumentationBehavior
+ include EncodedKeyCacheBehavior
+ include AutoloadingCacheBehavior
+ include ConnectionPoolBehavior
+ include FailureSafetyBehavior
+
+ def test_raw_values
+ cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true)
+ cache.clear
+ cache.write("foo", 2)
+ assert_equal "2", cache.read("foo")
+ end
+
+ def test_raw_values_with_marshal
+ cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true)
+ cache.clear
+ cache.write("foo", Marshal.dump([]))
+ assert_equal [], cache.read("foo")
+ end
+
+ def test_local_cache_raw_values
+ cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true)
+ cache.clear
+ cache.with_local_cache do
+ cache.write("foo", 2)
+ assert_equal "2", cache.read("foo")
+ end
+ end
+
+ def test_increment_expires_in
+ cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true)
+ cache.clear
+ assert_called_with cache.instance_variable_get(:@data), :incr, [ "foo", 1, 60 ] do
+ cache.increment("foo", 1, expires_in: 60)
+ end
+ end
+
+ def test_decrement_expires_in
+ cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true)
+ cache.clear
+ assert_called_with cache.instance_variable_get(:@data), :decr, [ "foo", 1, 60 ] do
+ cache.decrement("foo", 1, expires_in: 60)
+ end
+ end
+
+ def test_local_cache_raw_values_with_marshal
+ cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true)
+ cache.clear
+ cache.with_local_cache do
+ cache.write("foo", Marshal.dump([]))
+ assert_equal [], cache.read("foo")
+ end
+ end
+
+ def test_read_should_return_a_different_object_id_each_time_it_is_called
+ @cache.write("foo", "bar")
+ value = @cache.read("foo")
+ assert_not_equal value.object_id, @cache.read("foo").object_id
+ value << "bingo"
+ assert_not_equal value, @cache.read("foo")
+ end
+
+ private
+
+ def store
+ :mem_cache_store
+ end
+
+ def emulating_latency
+ old_client = Dalli.send(:remove_const, :Client)
+ Dalli.const_set(:Client, SlowDalliClient)
+
+ yield
+ ensure
+ Dalli.send(:remove_const, :Client)
+ Dalli.const_set(:Client, old_client)
+ end
+
+ def emulating_unavailability
+ old_server = Dalli.send(:remove_const, :Server)
+ Dalli.const_set(:Server, UnavailableDalliServer)
+
+ yield ActiveSupport::Cache::MemCacheStore.new
+ ensure
+ Dalli.send(:remove_const, :Server)
+ Dalli.const_set(:Server, old_server)
+ end
+end
diff --git a/activesupport/test/cache/stores/memory_store_test.rb b/activesupport/test/cache/stores/memory_store_test.rb
new file mode 100644
index 0000000000..4c0a4f549d
--- /dev/null
+++ b/activesupport/test/cache/stores/memory_store_test.rb
@@ -0,0 +1,116 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/cache"
+require_relative "../behaviors"
+
+class MemoryStoreTest < ActiveSupport::TestCase
+ def setup
+ @cache = ActiveSupport::Cache.lookup_store(:memory_store, expires_in: 60)
+ end
+
+ include CacheStoreBehavior
+ include CacheStoreVersionBehavior
+ include CacheDeleteMatchedBehavior
+ include CacheIncrementDecrementBehavior
+ include CacheInstrumentationBehavior
+end
+
+class MemoryStorePruningTest < ActiveSupport::TestCase
+ def setup
+ @record_size = ActiveSupport::Cache.lookup_store(:memory_store).send(:cached_size, 1, ActiveSupport::Cache::Entry.new("aaaaaaaaaa"))
+ @cache = ActiveSupport::Cache.lookup_store(:memory_store, expires_in: 60, size: @record_size * 10 + 1)
+ end
+
+ def test_prune_size
+ @cache.write(1, "aaaaaaaaaa") && sleep(0.001)
+ @cache.write(2, "bbbbbbbbbb") && sleep(0.001)
+ @cache.write(3, "cccccccccc") && sleep(0.001)
+ @cache.write(4, "dddddddddd") && sleep(0.001)
+ @cache.write(5, "eeeeeeeeee") && sleep(0.001)
+ @cache.read(2) && sleep(0.001)
+ @cache.read(4)
+ @cache.prune(@record_size * 3)
+ assert @cache.exist?(5)
+ assert @cache.exist?(4)
+ assert_not @cache.exist?(3), "no entry"
+ assert @cache.exist?(2)
+ assert_not @cache.exist?(1), "no entry"
+ end
+
+ def test_prune_size_on_write
+ @cache.write(1, "aaaaaaaaaa") && sleep(0.001)
+ @cache.write(2, "bbbbbbbbbb") && sleep(0.001)
+ @cache.write(3, "cccccccccc") && sleep(0.001)
+ @cache.write(4, "dddddddddd") && sleep(0.001)
+ @cache.write(5, "eeeeeeeeee") && sleep(0.001)
+ @cache.write(6, "ffffffffff") && sleep(0.001)
+ @cache.write(7, "gggggggggg") && sleep(0.001)
+ @cache.write(8, "hhhhhhhhhh") && sleep(0.001)
+ @cache.write(9, "iiiiiiiiii") && sleep(0.001)
+ @cache.write(10, "kkkkkkkkkk") && sleep(0.001)
+ @cache.read(2) && sleep(0.001)
+ @cache.read(4) && sleep(0.001)
+ @cache.write(11, "llllllllll")
+ assert @cache.exist?(11)
+ assert @cache.exist?(10)
+ assert @cache.exist?(9)
+ assert @cache.exist?(8)
+ assert @cache.exist?(7)
+ assert_not @cache.exist?(6), "no entry"
+ assert_not @cache.exist?(5), "no entry"
+ assert @cache.exist?(4)
+ assert_not @cache.exist?(3), "no entry"
+ assert @cache.exist?(2)
+ assert_not @cache.exist?(1), "no entry"
+ end
+
+ def test_prune_size_on_write_based_on_key_length
+ @cache.write(1, "aaaaaaaaaa") && sleep(0.001)
+ @cache.write(2, "bbbbbbbbbb") && sleep(0.001)
+ @cache.write(3, "cccccccccc") && sleep(0.001)
+ @cache.write(4, "dddddddddd") && sleep(0.001)
+ @cache.write(5, "eeeeeeeeee") && sleep(0.001)
+ @cache.write(6, "ffffffffff") && sleep(0.001)
+ @cache.write(7, "gggggggggg") && sleep(0.001)
+ @cache.write(8, "hhhhhhhhhh") && sleep(0.001)
+ @cache.write(9, "iiiiiiiiii") && sleep(0.001)
+ long_key = "*" * 2 * @record_size
+ @cache.write(long_key, "llllllllll")
+ assert @cache.exist?(long_key)
+ assert @cache.exist?(9)
+ assert @cache.exist?(8)
+ assert @cache.exist?(7)
+ assert @cache.exist?(6)
+ assert_not @cache.exist?(5), "no entry"
+ assert_not @cache.exist?(4), "no entry"
+ assert_not @cache.exist?(3), "no entry"
+ assert_not @cache.exist?(2), "no entry"
+ assert_not @cache.exist?(1), "no entry"
+ end
+
+ def test_pruning_is_capped_at_a_max_time
+ def @cache.delete_entry(*args)
+ sleep(0.01)
+ super
+ end
+ @cache.write(1, "aaaaaaaaaa") && sleep(0.001)
+ @cache.write(2, "bbbbbbbbbb") && sleep(0.001)
+ @cache.write(3, "cccccccccc") && sleep(0.001)
+ @cache.write(4, "dddddddddd") && sleep(0.001)
+ @cache.write(5, "eeeeeeeeee") && sleep(0.001)
+ @cache.prune(30, 0.001)
+ assert @cache.exist?(5)
+ assert @cache.exist?(4)
+ assert @cache.exist?(3)
+ assert @cache.exist?(2)
+ assert_not @cache.exist?(1)
+ end
+
+ def test_write_with_unless_exist
+ assert_equal true, @cache.write(1, "aaaaaaaaaa")
+ assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true)
+ @cache.write(1, nil)
+ assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true)
+ end
+end
diff --git a/activesupport/test/cache/stores/null_store_test.rb b/activesupport/test/cache/stores/null_store_test.rb
new file mode 100644
index 0000000000..a891cbffc8
--- /dev/null
+++ b/activesupport/test/cache/stores/null_store_test.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/cache"
+require_relative "../behaviors"
+
+class NullStoreTest < ActiveSupport::TestCase
+ def setup
+ @cache = ActiveSupport::Cache.lookup_store(:null_store)
+ end
+
+ def test_clear
+ @cache.clear
+ end
+
+ def test_cleanup
+ @cache.cleanup
+ end
+
+ def test_write
+ assert_equal true, @cache.write("name", "value")
+ end
+
+ def test_read
+ @cache.write("name", "value")
+ assert_nil @cache.read("name")
+ end
+
+ def test_delete
+ @cache.write("name", "value")
+ assert_equal false, @cache.delete("name")
+ end
+
+ def test_increment
+ @cache.write("name", 1, raw: true)
+ assert_nil @cache.increment("name")
+ end
+
+ def test_decrement
+ @cache.write("name", 1, raw: true)
+ assert_nil @cache.increment("name")
+ end
+
+ def test_delete_matched
+ @cache.write("name", "value")
+ @cache.delete_matched(/name/)
+ end
+
+ def test_local_store_strategy
+ @cache.with_local_cache do
+ @cache.write("name", "value")
+ assert_equal "value", @cache.read("name")
+ @cache.delete("name")
+ assert_nil @cache.read("name")
+ @cache.write("name", "value")
+ end
+ assert_nil @cache.read("name")
+ end
+end
diff --git a/activesupport/test/cache/stores/redis_cache_store_test.rb b/activesupport/test/cache/stores/redis_cache_store_test.rb
new file mode 100644
index 0000000000..305a2c184d
--- /dev/null
+++ b/activesupport/test/cache/stores/redis_cache_store_test.rb
@@ -0,0 +1,280 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/cache"
+require "active_support/cache/redis_cache_store"
+require_relative "../behaviors"
+
+driver_name = %w[ ruby hiredis ].include?(ENV["REDIS_DRIVER"]) ? ENV["REDIS_DRIVER"] : "hiredis"
+driver = Object.const_get("Redis::Connection::#{driver_name.camelize}")
+
+Redis::Connection.drivers.clear
+Redis::Connection.drivers.append(driver)
+
+# Emulates a latency on Redis's back-end for the key latency to facilitate
+# connection pool testing.
+class SlowRedis < Redis
+ def get(key, options = {})
+ if key =~ /latency/
+ sleep 3
+ else
+ super
+ end
+ end
+end
+
+module ActiveSupport::Cache::RedisCacheStoreTests
+ DRIVER = %w[ ruby hiredis ].include?(ENV["REDIS_DRIVER"]) ? ENV["REDIS_DRIVER"] : "hiredis"
+
+ class LookupTest < ActiveSupport::TestCase
+ test "may be looked up as :redis_cache_store" do
+ assert_kind_of ActiveSupport::Cache::RedisCacheStore,
+ ActiveSupport::Cache.lookup_store(:redis_cache_store)
+ end
+ end
+
+ class InitializationTest < ActiveSupport::TestCase
+ test "omitted URL uses Redis client with default settings" do
+ assert_called_with Redis, :new, [
+ url: nil,
+ connect_timeout: 20, read_timeout: 1, write_timeout: 1,
+ reconnect_attempts: 0, driver: DRIVER
+ ] do
+ build
+ end
+ end
+
+ test "no URLs uses Redis client with default settings" do
+ assert_called_with Redis, :new, [
+ url: nil,
+ connect_timeout: 20, read_timeout: 1, write_timeout: 1,
+ reconnect_attempts: 0, driver: DRIVER
+ ] do
+ build url: []
+ end
+ end
+
+ test "singular URL uses Redis client" do
+ assert_called_with Redis, :new, [
+ url: "redis://localhost:6379/0",
+ connect_timeout: 20, read_timeout: 1, write_timeout: 1,
+ reconnect_attempts: 0, driver: DRIVER
+ ] do
+ build url: "redis://localhost:6379/0"
+ end
+ end
+
+ test "one URL uses Redis client" do
+ assert_called_with Redis, :new, [
+ url: "redis://localhost:6379/0",
+ connect_timeout: 20, read_timeout: 1, write_timeout: 1,
+ reconnect_attempts: 0, driver: DRIVER
+ ] do
+ build url: %w[ redis://localhost:6379/0 ]
+ end
+ end
+
+ test "multiple URLs uses Redis::Distributed client" do
+ assert_called_with Redis, :new, [
+ [ url: "redis://localhost:6379/0",
+ connect_timeout: 20, read_timeout: 1, write_timeout: 1,
+ reconnect_attempts: 0, driver: DRIVER ],
+ [ url: "redis://localhost:6379/1",
+ connect_timeout: 20, read_timeout: 1, write_timeout: 1,
+ reconnect_attempts: 0, driver: DRIVER ],
+ ], returns: Redis.new do
+ @cache = build url: %w[ redis://localhost:6379/0 redis://localhost:6379/1 ]
+ assert_kind_of ::Redis::Distributed, @cache.redis
+ end
+ end
+
+ test "block argument uses yielded client" do
+ block = -> { :custom_redis_client }
+ assert_called block, :call do
+ build redis: block
+ end
+ end
+
+ test "instance of Redis uses given instance" do
+ redis_instance = Redis.new
+ @cache = build(redis: redis_instance)
+ assert_same @cache.redis, redis_instance
+ end
+
+ private
+ def build(**kwargs)
+ ActiveSupport::Cache::RedisCacheStore.new(driver: DRIVER, **kwargs).tap(&:redis)
+ end
+ end
+
+ class StoreTest < ActiveSupport::TestCase
+ setup do
+ @namespace = "namespace"
+
+ @cache = ActiveSupport::Cache::RedisCacheStore.new(timeout: 0.1, namespace: @namespace, expires_in: 60, driver: DRIVER)
+ # @cache.logger = Logger.new($stdout) # For test debugging
+
+ # For LocalCacheBehavior tests
+ @peek = ActiveSupport::Cache::RedisCacheStore.new(timeout: 0.1, namespace: @namespace, driver: DRIVER)
+ end
+
+ teardown do
+ @cache.clear
+ @cache.redis.disconnect!
+ end
+ end
+
+ class RedisCacheStoreCommonBehaviorTest < StoreTest
+ include CacheStoreBehavior
+ include CacheStoreVersionBehavior
+ include LocalCacheBehavior
+ include CacheIncrementDecrementBehavior
+ include CacheInstrumentationBehavior
+ include AutoloadingCacheBehavior
+
+ def test_fetch_multi_uses_redis_mget
+ assert_called(@cache.redis, :mget, returns: []) do
+ @cache.fetch_multi("a", "b", "c") do |key|
+ key * 2
+ end
+ end
+ end
+
+ def test_increment_expires_in
+ assert_called_with @cache.redis, :incrby, [ "#{@namespace}:foo", 1 ] do
+ assert_called_with @cache.redis, :expire, [ "#{@namespace}:foo", 60 ] do
+ @cache.increment "foo", 1, expires_in: 60
+ end
+ end
+
+ # key and ttl exist
+ @cache.redis.setex "#{@namespace}:bar", 120, 1
+ assert_not_called @cache.redis, :expire do
+ @cache.increment "bar", 1, expires_in: 2.minutes
+ end
+
+ # key exist but not have expire
+ @cache.redis.set "#{@namespace}:dar", 10
+ assert_called_with @cache.redis, :expire, [ "#{@namespace}:dar", 60 ] do
+ @cache.increment "dar", 1, expires_in: 60
+ end
+ end
+
+ def test_decrement_expires_in
+ assert_called_with @cache.redis, :decrby, [ "#{@namespace}:foo", 1 ] do
+ assert_called_with @cache.redis, :expire, [ "#{@namespace}:foo", 60 ] do
+ @cache.decrement "foo", 1, expires_in: 60
+ end
+ end
+
+ # key and ttl exist
+ @cache.redis.setex "#{@namespace}:bar", 120, 1
+ assert_not_called @cache.redis, :expire do
+ @cache.decrement "bar", 1, expires_in: 2.minutes
+ end
+
+ # key exist but not have expire
+ @cache.redis.set "#{@namespace}:dar", 10
+ assert_called_with @cache.redis, :expire, [ "#{@namespace}:dar", 60 ] do
+ @cache.decrement "dar", 1, expires_in: 60
+ end
+ end
+ end
+
+ class ConnectionPoolBehaviourTest < StoreTest
+ include ConnectionPoolBehavior
+
+ private
+
+ def store
+ :redis_cache_store
+ end
+
+ def emulating_latency
+ old_redis = Object.send(:remove_const, :Redis)
+ Object.const_set(:Redis, SlowRedis)
+
+ yield
+ ensure
+ Object.send(:remove_const, :Redis)
+ Object.const_set(:Redis, old_redis)
+ end
+ end
+
+ class RedisDistributedConnectionPoolBehaviourTest < ConnectionPoolBehaviourTest
+ private
+ def store_options
+ { url: %w[ redis://localhost:6379/0 redis://localhost:6379/0 ] }
+ end
+ end
+
+ # Separate test class so we can omit the namespace which causes expected,
+ # appropriate complaints about incompatible string encodings.
+ class KeyEncodingSafetyTest < StoreTest
+ include EncodedKeyCacheBehavior
+
+ setup do
+ @cache = ActiveSupport::Cache::RedisCacheStore.new(timeout: 0.1, driver: DRIVER)
+ @cache.logger = nil
+ end
+ end
+
+ class StoreAPITest < StoreTest
+ end
+
+ class UnavailableRedisClient < Redis::Client
+ def ensure_connected
+ raise Redis::BaseConnectionError
+ end
+ end
+
+ class FailureSafetyTest < StoreTest
+ include FailureSafetyBehavior
+
+ private
+
+ def emulating_unavailability
+ old_client = Redis.send(:remove_const, :Client)
+ Redis.const_set(:Client, UnavailableRedisClient)
+
+ yield ActiveSupport::Cache::RedisCacheStore.new
+ ensure
+ Redis.send(:remove_const, :Client)
+ Redis.const_set(:Client, old_client)
+ end
+ end
+
+ class DeleteMatchedTest < StoreTest
+ test "deletes keys matching glob" do
+ @cache.write("foo", "bar")
+ @cache.write("fu", "baz")
+ @cache.delete_matched("foo*")
+ assert_not @cache.exist?("foo")
+ assert @cache.exist?("fu")
+ end
+
+ test "fails with regexp matchers" do
+ assert_raise ArgumentError do
+ @cache.delete_matched(/OO/i)
+ end
+ end
+ end
+
+ class ClearTest < StoreTest
+ test "clear all cache key" do
+ @cache.write("foo", "bar")
+ @cache.write("fu", "baz")
+ @cache.clear
+ assert_not @cache.exist?("foo")
+ assert_not @cache.exist?("fu")
+ end
+
+ test "only clear namespace cache key" do
+ @cache.write("foo", "bar")
+ @cache.redis.set("fu", "baz")
+ @cache.clear
+ assert_not @cache.exist?("foo")
+ assert @cache.redis.exists("fu")
+ end
+ end
+end
diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb
deleted file mode 100644
index 74ceff44f9..0000000000
--- a/activesupport/test/caching_test.rb
+++ /dev/null
@@ -1,1095 +0,0 @@
-require 'logger'
-require 'abstract_unit'
-require 'active_support/cache'
-require 'dependencies_test_helpers'
-
-module ActiveSupport
- module Cache
- module Strategy
- module LocalCache
- class MiddlewareTest < ActiveSupport::TestCase
- def test_local_cache_cleared_on_close
- 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'
- [200, {}, []]
- })
- _, _, body = middleware.call({})
- assert LocalCacheRegistry.cache_for(key), 'should still have a cache'
- body.each { }
- assert LocalCacheRegistry.cache_for(key), 'should still have a cache'
- body.close
- assert_nil LocalCacheRegistry.cache_for(key)
- end
-
- def test_local_cache_cleared_on_exception
- 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'
- raise
- })
- assert_raises(RuntimeError) { middleware.call({}) }
- assert_nil LocalCacheRegistry.cache_for(key)
- end
- end
- end
- end
- end
-end
-
-class CacheKeyTest < ActiveSupport::TestCase
- def test_entry_legacy_optional_ivars
- legacy = Class.new(ActiveSupport::Cache::Entry) do
- def initialize(value, options = {})
- @value = value
- @expires_in = nil
- @created_at = nil
- super
- end
- end
-
- entry = legacy.new 'foo'
- assert_equal 'foo', entry.value
- end
-
- def test_expand_cache_key
- assert_equal '1/2/true', ActiveSupport::Cache.expand_cache_key([1, '2', true])
- assert_equal 'name/1/2/true', ActiveSupport::Cache.expand_cache_key([1, '2', true], :name)
- end
-
- def test_expand_cache_key_with_rails_cache_id
- with_env('RAILS_CACHE_ID' => 'c99') do
- assert_equal 'c99/foo', ActiveSupport::Cache.expand_cache_key(:foo)
- assert_equal 'c99/foo', ActiveSupport::Cache.expand_cache_key([:foo])
- assert_equal 'c99/foo/bar', ActiveSupport::Cache.expand_cache_key([:foo, :bar])
- assert_equal 'nm/c99/foo', ActiveSupport::Cache.expand_cache_key(:foo, :nm)
- assert_equal 'nm/c99/foo', ActiveSupport::Cache.expand_cache_key([:foo], :nm)
- assert_equal 'nm/c99/foo/bar', ActiveSupport::Cache.expand_cache_key([:foo, :bar], :nm)
- end
- end
-
- def test_expand_cache_key_with_rails_app_version
- with_env('RAILS_APP_VERSION' => 'rails3') do
- assert_equal 'rails3/foo', ActiveSupport::Cache.expand_cache_key(:foo)
- end
- end
-
- def test_expand_cache_key_rails_cache_id_should_win_over_rails_app_version
- with_env('RAILS_CACHE_ID' => 'c99', 'RAILS_APP_VERSION' => 'rails3') do
- assert_equal 'c99/foo', ActiveSupport::Cache.expand_cache_key(:foo)
- end
- end
-
- def test_expand_cache_key_respond_to_cache_key
- key = 'foo'
- def key.cache_key
- :foo_key
- end
- assert_equal 'foo_key', ActiveSupport::Cache.expand_cache_key(key)
- end
-
- def test_expand_cache_key_array_with_something_that_responds_to_cache_key
- key = 'foo'
- def key.cache_key
- :foo_key
- end
- assert_equal 'foo_key', ActiveSupport::Cache.expand_cache_key([key])
- end
-
- def test_expand_cache_key_of_nil
- assert_equal '', ActiveSupport::Cache.expand_cache_key(nil)
- end
-
- def test_expand_cache_key_of_false
- assert_equal 'false', ActiveSupport::Cache.expand_cache_key(false)
- end
-
- def test_expand_cache_key_of_true
- assert_equal 'true', ActiveSupport::Cache.expand_cache_key(true)
- end
-
- def test_expand_cache_key_of_array_like_object
- assert_equal 'foo/bar/baz', ActiveSupport::Cache.expand_cache_key(%w{foo bar baz}.to_enum)
- end
-
- private
-
- def with_env(kv)
- old_values = {}
- kv.each { |key, value| old_values[key], ENV[key] = ENV[key], value }
- yield
- ensure
- old_values.each { |key, value| ENV[key] = value}
- end
-end
-
-class CacheStoreSettingTest < ActiveSupport::TestCase
- def test_file_fragment_cache_store
- store = ActiveSupport::Cache.lookup_store :file_store, "/path/to/cache/directory"
- assert_kind_of(ActiveSupport::Cache::FileStore, store)
- assert_equal "/path/to/cache/directory", store.cache_path
- end
-
- def test_mem_cache_fragment_cache_store
- assert_called_with(Dalli::Client, :new, [%w[localhost], {}]) do
- store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost"
- assert_kind_of(ActiveSupport::Cache::MemCacheStore, store)
- end
- end
-
- def test_mem_cache_fragment_cache_store_with_given_mem_cache
- mem_cache = Dalli::Client.new
- assert_not_called(Dalli::Client, :new) do
- store = ActiveSupport::Cache.lookup_store :mem_cache_store, mem_cache
- assert_kind_of(ActiveSupport::Cache::MemCacheStore, store)
- end
- end
-
- def test_mem_cache_fragment_cache_store_with_not_dalli_client
- assert_not_called(Dalli::Client, :new) do
- memcache = Object.new
- assert_raises(ArgumentError) do
- ActiveSupport::Cache.lookup_store :mem_cache_store, memcache
- end
- end
- end
-
- def test_mem_cache_fragment_cache_store_with_multiple_servers
- assert_called_with(Dalli::Client, :new, [%w[localhost 192.168.1.1], {}]) do
- store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost", '192.168.1.1'
- assert_kind_of(ActiveSupport::Cache::MemCacheStore, store)
- end
- end
-
- def test_mem_cache_fragment_cache_store_with_options
- assert_called_with(Dalli::Client, :new, [%w[localhost 192.168.1.1], { :timeout => 10 }]) do
- store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost", '192.168.1.1', :namespace => 'foo', :timeout => 10
- assert_kind_of(ActiveSupport::Cache::MemCacheStore, store)
- assert_equal 'foo', store.options[:namespace]
- end
- end
-
- def test_object_assigned_fragment_cache_store
- store = ActiveSupport::Cache.lookup_store ActiveSupport::Cache::FileStore.new("/path/to/cache/directory")
- assert_kind_of(ActiveSupport::Cache::FileStore, store)
- assert_equal "/path/to/cache/directory", store.cache_path
- end
-end
-
-class CacheStoreNamespaceTest < ActiveSupport::TestCase
- def test_static_namespace
- cache = ActiveSupport::Cache.lookup_store(:memory_store, :namespace => "tester")
- cache.write("foo", "bar")
- assert_equal "bar", cache.read("foo")
- assert_equal "bar", cache.instance_variable_get(:@data)["tester:foo"].value
- end
-
- def test_proc_namespace
- test_val = "tester"
- proc = lambda{test_val}
- cache = ActiveSupport::Cache.lookup_store(:memory_store, :namespace => proc)
- cache.write("foo", "bar")
- assert_equal "bar", cache.read("foo")
- assert_equal "bar", cache.instance_variable_get(:@data)["tester:foo"].value
- end
-
- def test_delete_matched_key_start
- cache = ActiveSupport::Cache.lookup_store(:memory_store, :namespace => "tester")
- cache.write("foo", "bar")
- cache.write("fu", "baz")
- cache.delete_matched(/^fo/)
- assert !cache.exist?("foo")
- assert cache.exist?("fu")
- end
-
- def test_delete_matched_key
- cache = ActiveSupport::Cache.lookup_store(:memory_store, :namespace => "foo")
- cache.write("foo", "bar")
- cache.write("fu", "baz")
- cache.delete_matched(/OO/i)
- assert !cache.exist?("foo")
- assert cache.exist?("fu")
- end
-end
-
-# Tests the base functionality that should be identical across all cache stores.
-module CacheStoreBehavior
- def test_should_read_and_write_strings
- assert @cache.write('foo', 'bar')
- assert_equal 'bar', @cache.read('foo')
- end
-
- def test_should_overwrite
- @cache.write('foo', 'bar')
- @cache.write('foo', 'baz')
- assert_equal 'baz', @cache.read('foo')
- end
-
- def test_fetch_without_cache_miss
- @cache.write('foo', 'bar')
- assert_not_called(@cache, :write) do
- assert_equal 'bar', @cache.fetch('foo') { 'baz' }
- end
- end
-
- def test_fetch_with_cache_miss
- assert_called_with(@cache, :write, ['foo', 'baz', @cache.options]) do
- assert_equal 'baz', @cache.fetch('foo') { 'baz' }
- end
- end
-
- def test_fetch_with_cache_miss_passes_key_to_block
- cache_miss = false
- assert_equal 3, @cache.fetch('foo') { |key| cache_miss = true; key.length }
- assert cache_miss
-
- cache_miss = false
- assert_equal 3, @cache.fetch('foo') { |key| cache_miss = true; key.length }
- assert !cache_miss
- end
-
- def test_fetch_with_forced_cache_miss
- @cache.write('foo', 'bar')
- assert_not_called(@cache, :read) do
- assert_called_with(@cache, :write, ['foo', 'bar', @cache.options.merge(:force => true)]) do
- @cache.fetch('foo', :force => true) { 'bar' }
- end
- end
- end
-
- def test_fetch_with_cached_nil
- @cache.write('foo', nil)
- assert_not_called(@cache, :write) do
- assert_nil @cache.fetch('foo') { 'baz' }
- end
- end
-
- def test_should_read_and_write_hash
- assert @cache.write('foo', {:a => "b"})
- assert_equal({:a => "b"}, @cache.read('foo'))
- end
-
- def test_should_read_and_write_integer
- assert @cache.write('foo', 1)
- assert_equal 1, @cache.read('foo')
- end
-
- def test_should_read_and_write_nil
- assert @cache.write('foo', nil)
- assert_equal nil, @cache.read('foo')
- end
-
- def test_should_read_and_write_false
- assert @cache.write('foo', false)
- assert_equal false, @cache.read('foo')
- end
-
- def test_read_multi
- @cache.write('foo', 'bar')
- @cache.write('fu', 'baz')
- @cache.write('fud', 'biz')
- assert_equal({"foo" => "bar", "fu" => "baz"}, @cache.read_multi('foo', 'fu'))
- end
-
- def test_read_multi_with_expires
- time = Time.now
- @cache.write('foo', 'bar', :expires_in => 10)
- @cache.write('fu', 'baz')
- @cache.write('fud', 'biz')
- Time.stub(:now, time + 11) do
- assert_equal({"fu" => "baz"}, @cache.read_multi('foo', 'fu'))
- end
- end
-
- def test_fetch_multi
- @cache.write('foo', 'bar')
- @cache.write('fud', 'biz')
-
- values = @cache.fetch_multi('foo', 'fu', 'fud') { |value| value * 2 }
-
- assert_equal({ 'foo' => 'bar', 'fu' => 'fufu', 'fud' => 'biz' }, values)
- assert_equal('fufu', @cache.read('fu'))
- end
-
- def test_multi_with_objects
- cache_struct = Struct.new(:cache_key, :title)
- foo = cache_struct.new('foo', 'FOO!')
- bar = cache_struct.new('bar')
-
- @cache.write('bar', 'BAM!')
-
- values = @cache.fetch_multi(foo, bar) { |object| object.title }
-
- assert_equal({ foo => 'FOO!', bar => 'BAM!' }, values)
- end
-
- def test_read_and_write_compressed_small_data
- @cache.write('foo', 'bar', :compress => true)
- assert_equal 'bar', @cache.read('foo')
- end
-
- def test_read_and_write_compressed_large_data
- @cache.write('foo', 'bar', :compress => true, :compress_threshold => 2)
- assert_equal 'bar', @cache.read('foo')
- end
-
- def test_read_and_write_compressed_nil
- @cache.write('foo', nil, :compress => true)
- assert_nil @cache.read('foo')
- end
-
- def test_cache_key
- obj = Object.new
- def obj.cache_key
- :foo
- end
- @cache.write(obj, "bar")
- assert_equal "bar", @cache.read("foo")
- end
-
- def test_param_as_cache_key
- obj = Object.new
- def obj.to_param
- "foo"
- end
- @cache.write(obj, "bar")
- assert_equal "bar", @cache.read("foo")
- end
-
- def test_array_as_cache_key
- @cache.write([:fu, "foo"], "bar")
- assert_equal "bar", @cache.read("fu/foo")
- end
-
- def test_hash_as_cache_key
- @cache.write({:foo => 1, :fu => 2}, "bar")
- assert_equal "bar", @cache.read("foo=1/fu=2")
- end
-
- def test_keys_are_case_sensitive
- @cache.write("foo", "bar")
- assert_nil @cache.read("FOO")
- end
-
- def test_exist
- @cache.write('foo', 'bar')
- assert_equal true, @cache.exist?('foo')
- assert_equal false, @cache.exist?('bar')
- end
-
- def test_nil_exist
- @cache.write('foo', nil)
- assert @cache.exist?('foo')
- end
-
- def test_delete
- @cache.write('foo', 'bar')
- assert @cache.exist?('foo')
- assert @cache.delete('foo')
- assert !@cache.exist?('foo')
- end
-
- def test_original_store_objects_should_not_be_immutable
- bar = 'bar'
- @cache.write('foo', bar)
- assert_nothing_raised { bar.gsub!(/.*/, 'baz') }
- end
-
- def test_expires_in
- time = Time.local(2008, 4, 24)
-
- Time.stub(:now, time) do
- @cache.write('foo', 'bar')
- assert_equal 'bar', @cache.read('foo')
- end
-
- Time.stub(:now, time + 30) do
- assert_equal 'bar', @cache.read('foo')
- end
-
- Time.stub(:now, time + 61) do
- assert_nil @cache.read('foo')
- end
- end
-
- def test_race_condition_protection_skipped_if_not_defined
- @cache.write('foo', 'bar')
- time = @cache.send(:read_entry, 'foo', {}).expires_at
-
- Time.stub(:now, Time.at(time)) do
- result = @cache.fetch('foo') do
- assert_equal nil, @cache.read('foo')
- 'baz'
- end
- assert_equal 'baz', result
- end
- end
-
- def test_race_condition_protection_is_limited
- time = Time.now
- @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')
- "baz"
- end
- assert_equal "baz", result
- end
- end
-
- def test_race_condition_protection_is_safe
- time = Time.now
- @cache.write('foo', 'bar', :expires_in => 60)
- Time.stub(:now, time + 61) do
- begin
- @cache.fetch('foo', :race_condition_ttl => 10) do
- assert_equal 'bar', @cache.read('foo')
- raise ArgumentError.new
- end
- rescue ArgumentError
- end
- assert_equal "bar", @cache.read('foo')
- end
- Time.stub(:now, time + 91) do
- assert_nil @cache.read('foo')
- end
- end
-
- def test_race_condition_protection
- time = Time.now
- @cache.write('foo', 'bar', :expires_in => 60)
- Time.stub(:now, time + 61) do
- result = @cache.fetch('foo', :race_condition_ttl => 10) do
- assert_equal 'bar', @cache.read('foo')
- "baz"
- end
- assert_equal "baz", result
- end
- end
-
- def test_crazy_key_characters
- crazy_key = "#/:*(<+=> )&$%@?;'\"\'`~-"
- assert @cache.write(crazy_key, "1", :raw => true)
- assert_equal "1", @cache.read(crazy_key)
- assert_equal "1", @cache.fetch(crazy_key)
- assert @cache.delete(crazy_key)
- assert_equal "2", @cache.fetch(crazy_key, :raw => true) { "2" }
- assert_equal 3, @cache.increment(crazy_key)
- assert_equal 2, @cache.decrement(crazy_key)
- end
-
- def test_really_long_keys
- key = ""
- 900.times{key << "x"}
- assert @cache.write(key, "bar")
- assert_equal "bar", @cache.read(key)
- assert_equal "bar", @cache.fetch(key)
- assert_nil @cache.read("#{key}x")
- assert_equal({key => "bar"}, @cache.read_multi(key))
- assert @cache.delete(key)
- end
-end
-
-# https://rails.lighthouseapp.com/projects/8994/tickets/6225-memcachestore-cant-deal-with-umlauts-and-special-characters
-# The error is caused by character encodings that can't be compared with ASCII-8BIT regular expressions and by special
-# characters like the umlaut in UTF-8.
-module EncodedKeyCacheBehavior
- Encoding.list.each do |encoding|
- define_method "test_#{encoding.name.underscore}_encoded_values" do
- key = "foo".force_encoding(encoding)
- assert @cache.write(key, "1", :raw => true)
- assert_equal "1", @cache.read(key)
- assert_equal "1", @cache.fetch(key)
- assert @cache.delete(key)
- assert_equal "2", @cache.fetch(key, :raw => true) { "2" }
- assert_equal 3, @cache.increment(key)
- assert_equal 2, @cache.decrement(key)
- end
- end
-
- def test_common_utf8_values
- key = "\xC3\xBCmlaut".force_encoding(Encoding::UTF_8)
- assert @cache.write(key, "1", :raw => true)
- assert_equal "1", @cache.read(key)
- assert_equal "1", @cache.fetch(key)
- assert @cache.delete(key)
- assert_equal "2", @cache.fetch(key, :raw => true) { "2" }
- assert_equal 3, @cache.increment(key)
- assert_equal 2, @cache.decrement(key)
- end
-
- def test_retains_encoding
- key = "\xC3\xBCmlaut".force_encoding(Encoding::UTF_8)
- assert @cache.write(key, "1", :raw => true)
- assert_equal Encoding::UTF_8, key.encoding
- end
-end
-
-module CacheDeleteMatchedBehavior
- def test_delete_matched
- @cache.write("foo", "bar")
- @cache.write("fu", "baz")
- @cache.write("foo/bar", "baz")
- @cache.write("fu/baz", "bar")
- @cache.delete_matched(/oo/)
- assert !@cache.exist?("foo")
- assert @cache.exist?("fu")
- assert !@cache.exist?("foo/bar")
- assert @cache.exist?("fu/baz")
- end
-end
-
-module CacheIncrementDecrementBehavior
- def test_increment
- @cache.write('foo', 1, :raw => true)
- assert_equal 1, @cache.read('foo').to_i
- assert_equal 2, @cache.increment('foo')
- assert_equal 2, @cache.read('foo').to_i
- assert_equal 3, @cache.increment('foo')
- assert_equal 3, @cache.read('foo').to_i
- assert_nil @cache.increment('bar')
- end
-
- def test_decrement
- @cache.write('foo', 3, :raw => true)
- assert_equal 3, @cache.read('foo').to_i
- assert_equal 2, @cache.decrement('foo')
- assert_equal 2, @cache.read('foo').to_i
- assert_equal 1, @cache.decrement('foo')
- assert_equal 1, @cache.read('foo').to_i
- assert_nil @cache.decrement('bar')
- end
-end
-
-module LocalCacheBehavior
- def test_local_writes_are_persistent_on_the_remote_cache
- retval = @cache.with_local_cache do
- @cache.write('foo', 'bar')
- end
- assert retval
- assert_equal 'bar', @cache.read('foo')
- end
-
- def test_clear_also_clears_local_cache
- @cache.with_local_cache do
- @cache.write('foo', 'bar')
- @cache.clear
- assert_nil @cache.read('foo')
- end
-
- assert_nil @cache.read('foo')
- end
-
- def test_local_cache_of_write
- @cache.with_local_cache do
- @cache.write('foo', 'bar')
- @peek.delete('foo')
- assert_equal 'bar', @cache.read('foo')
- end
- end
-
- def test_local_cache_of_read
- @cache.write('foo', 'bar')
- @cache.with_local_cache do
- assert_equal 'bar', @cache.read('foo')
- end
- end
-
- def test_local_cache_of_write_nil
- @cache.with_local_cache do
- assert @cache.write('foo', nil)
- assert_nil @cache.read('foo')
- @peek.write('foo', 'bar')
- assert_nil @cache.read('foo')
- end
- end
-
- def test_local_cache_of_delete
- @cache.with_local_cache do
- @cache.write('foo', 'bar')
- @cache.delete('foo')
- assert_nil @cache.read('foo')
- end
- end
-
- def test_local_cache_of_exist
- @cache.with_local_cache do
- @cache.write('foo', 'bar')
- @peek.delete('foo')
- assert @cache.exist?('foo')
- end
- end
-
- def test_local_cache_of_increment
- @cache.with_local_cache do
- @cache.write('foo', 1, :raw => true)
- @peek.write('foo', 2, :raw => true)
- @cache.increment('foo')
- assert_equal 3, @cache.read('foo')
- end
- end
-
- def test_local_cache_of_decrement
- @cache.with_local_cache do
- @cache.write('foo', 1, :raw => true)
- @peek.write('foo', 3, :raw => true)
- @cache.decrement('foo')
- assert_equal 2, @cache.read('foo')
- end
- end
-
- def test_middleware
- app = lambda { |env|
- result = @cache.write('foo', 'bar')
- assert_equal 'bar', @cache.read('foo') # make sure 'foo' was written
- assert result
- [200, {}, []]
- }
- app = @cache.middleware.new(app)
- app.call({})
- end
-end
-
-module AutoloadingCacheBehavior
- include DependenciesTestHelpers
- def test_simple_autoloading
- with_autoloading_fixtures do
- @cache.write('foo', EM.new)
- end
-
- remove_constants(:EM)
- ActiveSupport::Dependencies.clear
-
- with_autoloading_fixtures do
- assert_kind_of EM, @cache.read('foo')
- end
-
- remove_constants(:EM)
- ActiveSupport::Dependencies.clear
- end
-
- def test_two_classes_autoloading
- with_autoloading_fixtures do
- @cache.write('foo', [EM.new, ClassFolder.new])
- end
-
- remove_constants(:EM, :ClassFolder)
- ActiveSupport::Dependencies.clear
-
- with_autoloading_fixtures do
- loaded = @cache.read('foo')
- assert_kind_of Array, loaded
- assert_equal 2, loaded.size
- assert_kind_of EM, loaded[0]
- assert_kind_of ClassFolder, loaded[1]
- end
-
- remove_constants(:EM, :ClassFolder)
- ActiveSupport::Dependencies.clear
- end
-end
-
-class FileStoreTest < ActiveSupport::TestCase
- def setup
- Dir.mkdir(cache_dir) unless File.exist?(cache_dir)
- @cache = ActiveSupport::Cache.lookup_store(:file_store, cache_dir, :expires_in => 60)
- @peek = ActiveSupport::Cache.lookup_store(:file_store, cache_dir, :expires_in => 60)
- @cache_with_pathname = ActiveSupport::Cache.lookup_store(:file_store, Pathname.new(cache_dir), :expires_in => 60)
-
- @buffer = StringIO.new
- @cache.logger = ActiveSupport::Logger.new(@buffer)
- end
-
- def teardown
- FileUtils.rm_r(cache_dir)
- rescue Errno::ENOENT
- end
-
- def cache_dir
- File.join(Dir.pwd, 'tmp_cache')
- end
-
- include CacheStoreBehavior
- include LocalCacheBehavior
- include CacheDeleteMatchedBehavior
- include CacheIncrementDecrementBehavior
- include AutoloadingCacheBehavior
-
- def test_clear
- filepath = File.join(cache_dir, ".gitkeep")
- FileUtils.touch(filepath)
- @cache.clear
- assert File.exist?(filepath)
- end
-
- def test_clear_without_cache_dir
- FileUtils.rm_r(cache_dir)
- @cache.clear
- end
-
- def test_long_keys
- @cache.write("a"*10000, 1)
- assert_equal 1, @cache.read("a"*10000)
- end
-
- def test_key_transformation
- key = @cache.send(:key_file_path, "views/index?id=1")
- assert_equal "views/index?id=1", @cache.send(:file_path_key, key)
- end
-
- def test_key_transformation_with_pathname
- FileUtils.touch(File.join(cache_dir, "foo"))
- key = @cache_with_pathname.send(:key_file_path, "views/index?id=1")
- assert_equal "views/index?id=1", @cache_with_pathname.send(:file_path_key, key)
- end
-
- # Test that generated cache keys are short enough to have Tempfile stuff added to them and
- # remain valid
- def test_filename_max_size
- key = "#{'A' * ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE}"
- path = @cache.send(:key_file_path, key)
- Dir::Tmpname.create(path) do |tmpname, n, opts|
- assert File.basename(tmpname+'.lock').length <= 255, "Temp filename too long: #{File.basename(tmpname+'.lock').length}"
- end
- end
-
- # Because file systems have a maximum filename size, filenames > max size should be split in to directories
- # If filename is 'AAAAB', where max size is 4, the returned path should be AAAA/B
- def test_key_transformation_max_filename_size
- key = "#{'A' * ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE}B"
- path = @cache.send(:key_file_path, key)
- assert path.split('/').all? { |dir_name| dir_name.size <= ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE}
- assert_equal 'B', File.basename(path)
- end
-
- # If nothing has been stored in the cache, there is a chance the cache directory does not yet exist
- # Ensure delete_matched gracefully handles this case
- def test_delete_matched_when_cache_directory_does_not_exist
- assert_nothing_raised(Exception) do
- ActiveSupport::Cache::FileStore.new('/test/cache/directory').delete_matched(/does_not_exist/)
- end
- end
-
- def test_delete_does_not_delete_empty_parent_dir
- sub_cache_dir = File.join(cache_dir, 'subdir/')
- sub_cache_store = ActiveSupport::Cache::FileStore.new(sub_cache_dir)
- assert_nothing_raised(Exception) do
- assert sub_cache_store.write('foo', 'bar')
- assert sub_cache_store.delete('foo')
- end
- assert File.exist?(cache_dir), "Parent of top level cache dir was deleted!"
- assert File.exist?(sub_cache_dir), "Top level cache dir was deleted!"
- assert Dir.entries(sub_cache_dir).reject {|f| ActiveSupport::Cache::FileStore::EXCLUDED_DIRS.include?(f)}.empty?
- end
-
- def test_log_exception_when_cache_read_fails
- File.stub(:exist?, -> { raise StandardError.new("failed") }) do
- @cache.send(:read_entry, "winston", {})
- assert @buffer.string.present?
- end
- end
-
- def test_cleanup_removes_all_expired_entries
- time = Time.now
- @cache.write('foo', 'bar', expires_in: 10)
- @cache.write('baz', 'qux')
- @cache.write('quux', 'corge', expires_in: 20)
- Time.stub(:now, time + 15) do
- @cache.cleanup
- assert_not @cache.exist?('foo')
- assert @cache.exist?('baz')
- assert @cache.exist?('quux')
- end
- end
-
- def test_write_with_unless_exist
- assert_equal true, @cache.write(1, "aaaaaaaaaa")
- assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true)
- @cache.write(1, nil)
- assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true)
- end
-end
-
-class MemoryStoreTest < ActiveSupport::TestCase
- def setup
- @record_size = ActiveSupport::Cache.lookup_store(:memory_store).send(:cached_size, 1, ActiveSupport::Cache::Entry.new("aaaaaaaaaa"))
- @cache = ActiveSupport::Cache.lookup_store(:memory_store, :expires_in => 60, :size => @record_size * 10 + 1)
- end
-
- include CacheStoreBehavior
- include CacheDeleteMatchedBehavior
- include CacheIncrementDecrementBehavior
-
- def test_prune_size
- @cache.write(1, "aaaaaaaaaa") && sleep(0.001)
- @cache.write(2, "bbbbbbbbbb") && sleep(0.001)
- @cache.write(3, "cccccccccc") && sleep(0.001)
- @cache.write(4, "dddddddddd") && sleep(0.001)
- @cache.write(5, "eeeeeeeeee") && sleep(0.001)
- @cache.read(2) && sleep(0.001)
- @cache.read(4)
- @cache.prune(@record_size * 3)
- assert @cache.exist?(5)
- assert @cache.exist?(4)
- assert !@cache.exist?(3), "no entry"
- assert @cache.exist?(2)
- assert !@cache.exist?(1), "no entry"
- end
-
- def test_prune_size_on_write
- @cache.write(1, "aaaaaaaaaa") && sleep(0.001)
- @cache.write(2, "bbbbbbbbbb") && sleep(0.001)
- @cache.write(3, "cccccccccc") && sleep(0.001)
- @cache.write(4, "dddddddddd") && sleep(0.001)
- @cache.write(5, "eeeeeeeeee") && sleep(0.001)
- @cache.write(6, "ffffffffff") && sleep(0.001)
- @cache.write(7, "gggggggggg") && sleep(0.001)
- @cache.write(8, "hhhhhhhhhh") && sleep(0.001)
- @cache.write(9, "iiiiiiiiii") && sleep(0.001)
- @cache.write(10, "kkkkkkkkkk") && sleep(0.001)
- @cache.read(2) && sleep(0.001)
- @cache.read(4) && sleep(0.001)
- @cache.write(11, "llllllllll")
- assert @cache.exist?(11)
- assert @cache.exist?(10)
- assert @cache.exist?(9)
- assert @cache.exist?(8)
- assert @cache.exist?(7)
- assert !@cache.exist?(6), "no entry"
- assert !@cache.exist?(5), "no entry"
- assert @cache.exist?(4)
- assert !@cache.exist?(3), "no entry"
- assert @cache.exist?(2)
- assert !@cache.exist?(1), "no entry"
- end
-
- def test_prune_size_on_write_based_on_key_length
- @cache.write(1, "aaaaaaaaaa") && sleep(0.001)
- @cache.write(2, "bbbbbbbbbb") && sleep(0.001)
- @cache.write(3, "cccccccccc") && sleep(0.001)
- @cache.write(4, "dddddddddd") && sleep(0.001)
- @cache.write(5, "eeeeeeeeee") && sleep(0.001)
- @cache.write(6, "ffffffffff") && sleep(0.001)
- @cache.write(7, "gggggggggg") && sleep(0.001)
- @cache.write(8, "hhhhhhhhhh") && sleep(0.001)
- @cache.write(9, "iiiiiiiiii") && sleep(0.001)
- long_key = '*' * 2 * @record_size
- @cache.write(long_key, "llllllllll")
- assert @cache.exist?(long_key)
- assert @cache.exist?(9)
- assert @cache.exist?(8)
- assert @cache.exist?(7)
- assert @cache.exist?(6)
- assert !@cache.exist?(5), "no entry"
- assert !@cache.exist?(4), "no entry"
- assert !@cache.exist?(3), "no entry"
- assert !@cache.exist?(2), "no entry"
- assert !@cache.exist?(1), "no entry"
- end
-
- def test_pruning_is_capped_at_a_max_time
- def @cache.delete_entry (*args)
- sleep(0.01)
- super
- end
- @cache.write(1, "aaaaaaaaaa") && sleep(0.001)
- @cache.write(2, "bbbbbbbbbb") && sleep(0.001)
- @cache.write(3, "cccccccccc") && sleep(0.001)
- @cache.write(4, "dddddddddd") && sleep(0.001)
- @cache.write(5, "eeeeeeeeee") && sleep(0.001)
- @cache.prune(30, 0.001)
- assert @cache.exist?(5)
- assert @cache.exist?(4)
- assert @cache.exist?(3)
- assert @cache.exist?(2)
- assert !@cache.exist?(1)
- end
-
- def test_write_with_unless_exist
- assert_equal true, @cache.write(1, "aaaaaaaaaa")
- assert_equal false, @cache.write(1, "aaaaaaaaaa", :unless_exist => true)
- @cache.write(1, nil)
- assert_equal false, @cache.write(1, "aaaaaaaaaa", :unless_exist => true)
- end
-end
-
-class MemCacheStoreTest < ActiveSupport::TestCase
- require 'dalli'
-
- begin
- ss = Dalli::Client.new('localhost:11211').stats
- raise Dalli::DalliError unless ss['localhost:11211']
-
- MEMCACHE_UP = true
- rescue Dalli::DalliError
- $stderr.puts "Skipping memcached tests. Start memcached and try again."
- MEMCACHE_UP = false
- end
-
- def setup
- skip "memcache server is not up" unless MEMCACHE_UP
-
- @cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, :expires_in => 60)
- @peek = ActiveSupport::Cache.lookup_store(:mem_cache_store)
- @data = @cache.instance_variable_get(:@data)
- @cache.clear
- @cache.silence!
- @cache.logger = ActiveSupport::Logger.new("/dev/null")
- end
-
- include CacheStoreBehavior
- include LocalCacheBehavior
- include CacheIncrementDecrementBehavior
- include EncodedKeyCacheBehavior
- include AutoloadingCacheBehavior
-
- def test_raw_values
- cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, :raw => true)
- cache.clear
- cache.write("foo", 2)
- assert_equal "2", cache.read("foo")
- end
-
- def test_raw_values_with_marshal
- cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, :raw => true)
- cache.clear
- cache.write("foo", Marshal.dump([]))
- assert_equal [], cache.read("foo")
- end
-
- def test_local_cache_raw_values
- cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, :raw => true)
- cache.clear
- cache.with_local_cache do
- cache.write("foo", 2)
- assert_equal "2", cache.read("foo")
- end
- end
-
- def test_local_cache_raw_values_with_marshal
- cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, :raw => true)
- cache.clear
- cache.with_local_cache do
- cache.write("foo", Marshal.dump([]))
- assert_equal [], cache.read("foo")
- end
- end
-
- def test_read_should_return_a_different_object_id_each_time_it_is_called
- @cache.write('foo', 'bar')
- value = @cache.read('foo')
- assert_not_equal value.object_id, @cache.read('foo').object_id
- value << 'bingo'
- assert_not_equal value, @cache.read('foo')
- end
-end
-
-class NullStoreTest < ActiveSupport::TestCase
- def setup
- @cache = ActiveSupport::Cache.lookup_store(:null_store)
- end
-
- def test_clear
- @cache.clear
- end
-
- def test_cleanup
- @cache.cleanup
- end
-
- def test_write
- assert_equal true, @cache.write("name", "value")
- end
-
- def test_read
- @cache.write("name", "value")
- assert_nil @cache.read("name")
- end
-
- def test_delete
- @cache.write("name", "value")
- assert_equal false, @cache.delete("name")
- end
-
- def test_increment
- @cache.write("name", 1, :raw => true)
- assert_nil @cache.increment("name")
- end
-
- def test_decrement
- @cache.write("name", 1, :raw => true)
- assert_nil @cache.increment("name")
- end
-
- def test_delete_matched
- @cache.write("name", "value")
- @cache.delete_matched(/name/)
- end
-
- def test_local_store_strategy
- @cache.with_local_cache do
- @cache.write("name", "value")
- assert_equal "value", @cache.read("name")
- @cache.delete("name")
- assert_nil @cache.read("name")
- @cache.write("name", "value")
- end
- assert_nil @cache.read("name")
- end
-end
-
-class CacheStoreLoggerTest < ActiveSupport::TestCase
- def setup
- @cache = ActiveSupport::Cache.lookup_store(:memory_store)
-
- @buffer = StringIO.new
- @cache.logger = ActiveSupport::Logger.new(@buffer)
- end
-
- def test_logging
- @cache.fetch('foo') { 'bar' }
- assert @buffer.string.present?
- end
-
- def test_mute_logging
- @cache.mute { @cache.fetch('foo') { 'bar' } }
- assert @buffer.string.blank?
- end
-
- def test_multi_read_loggin
- @cache.write 'hello', 'goodbye'
- @cache.write 'world', 'earth'
-
- @cache.read_multi('hello', 'world')
-
- assert_match "Caches multi read:\n- hello\n- world", @buffer.string
- end
-end
-
-class CacheEntryTest < ActiveSupport::TestCase
- def test_expired
- entry = ActiveSupport::Cache::Entry.new("value")
- assert !entry.expired?, 'entry not expired'
- entry = ActiveSupport::Cache::Entry.new("value", :expires_in => 60)
- assert !entry.expired?, 'entry not expired'
- Time.stub(:now, Time.now + 61) do
- assert entry.expired?, 'entry is expired'
- end
- end
-
- def test_compress_values
- value = "value" * 100
- entry = ActiveSupport::Cache::Entry.new(value, :compress => true, :compress_threshold => 1)
- assert_equal value, entry.value
- assert(value.bytesize > entry.size, "value is compressed")
- end
-
- def test_non_compress_values
- value = "value" * 100
- entry = ActiveSupport::Cache::Entry.new(value)
- assert_equal value, entry.value
- assert_equal value.bytesize, entry.size
- end
-end
diff --git a/activesupport/test/callback_inheritance_test.rb b/activesupport/test/callback_inheritance_test.rb
index 1adfe4edf4..5633b6e2b8 100644
--- a/activesupport/test/callback_inheritance_test.rb
+++ b/activesupport/test/callback_inheritance_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class GrandParent
include ActiveSupport::Callbacks
@@ -9,8 +11,8 @@ class GrandParent
end
define_callbacks :dispatch
- set_callback :dispatch, :before, :before1, :before2, :if => proc {|c| c.action_name == "index" || c.action_name == "update" }
- set_callback :dispatch, :after, :after1, :after2, :if => proc {|c| c.action_name == "update" || c.action_name == "delete" }
+ set_callback :dispatch, :before, :before1, :before2, if: proc { |c| c.action_name == "index" || c.action_name == "update" }
+ set_callback :dispatch, :after, :after1, :after2, if: proc { |c| c.action_name == "update" || c.action_name == "delete" }
def before1
@log << "before1"
@@ -37,12 +39,12 @@ class GrandParent
end
class Parent < GrandParent
- skip_callback :dispatch, :before, :before2, :unless => proc {|c| c.action_name == "update" }
- skip_callback :dispatch, :after, :after2, :unless => proc {|c| c.action_name == "delete" }
+ skip_callback :dispatch, :before, :before2, unless: proc { |c| c.action_name == "update" }
+ skip_callback :dispatch, :after, :after2, unless: proc { |c| c.action_name == "delete" }
end
class Child < GrandParent
- skip_callback :dispatch, :before, :before2, :unless => proc {|c| c.action_name == "update" }, :if => :state_open?
+ skip_callback :dispatch, :before, :before2, unless: proc { |c| c.action_name == "update" }, if: :state_open?
def state_open?
@state == :open
@@ -162,10 +164,10 @@ end
class DynamicInheritedCallbacks < ActiveSupport::TestCase
def test_callbacks_looks_to_the_superclass_before_running
child = EmptyChild.new.dispatch
- assert !child.performed?
+ assert_not_predicate child, :performed?
EmptyParent.set_callback :dispatch, :before, :perform!
child = EmptyChild.new.dispatch
- assert child.performed?
+ assert_predicate child, :performed?
end
def test_callbacks_should_be_performed_once_in_child_class
@@ -174,3 +176,13 @@ class DynamicInheritedCallbacks < ActiveSupport::TestCase
assert_equal 1, child.count
end
end
+
+class DynamicDefinedCallbacks < ActiveSupport::TestCase
+ def test_callbacks_should_be_performed_once_in_child_class_after_dynamic_define
+ GrandParent.define_callbacks(:foo)
+ GrandParent.set_callback(:foo, :before, :before1)
+ parent = Parent.new("foo")
+ parent.run_callbacks(:foo)
+ assert_equal %w(before1), parent.log
+ end
+end
diff --git a/activesupport/test/callbacks_test.rb b/activesupport/test/callbacks_test.rb
index cda9732cae..466b364e9d 100644
--- a/activesupport/test/callbacks_test.rb
+++ b/activesupport/test/callbacks_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module CallbacksTest
class Record
@@ -23,10 +25,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
@@ -56,27 +54,30 @@ module CallbacksTest
end
class Person < Record
+ attr_accessor :save_fails
+
[:before_save, :after_save].each do |callback_method|
callback_method_sym = callback_method.to_sym
send(callback_method, callback_symbol(callback_method_sym))
- 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, callback_object(callback_method_sym.to_s.gsub(/_save/, "")))
send(callback_method, CallbackClass)
send(callback_method) { |model| model.history << [callback_method_sym, :block] }
end
def save
- run_callbacks :save
+ run_callbacks :save do
+ raise "inside save" if save_fails
+ end
end
end
class PersonSkipper < Person
- skip_callback :save, :before, :before_save_method, :if => :yes
- skip_callback :save, :after, :after_save_method, :unless => :yes
- skip_callback :save, :after, :after_save_method, :if => :no
- skip_callback :save, :before, :before_save_method, :unless => :no
- skip_callback :save, :before, CallbackClass , :if => :yes
+ skip_callback :save, :before, :before_save_method, if: :yes
+ skip_callback :save, :after, :after_save_method, unless: :yes
+ skip_callback :save, :after, :after_save_method, if: :no
+ skip_callback :save, :before, :before_save_method, unless: :no
+ skip_callback :save, :before, CallbackClass, if: :yes
def yes; true; end
def no; false; end
end
@@ -89,7 +90,7 @@ module CallbacksTest
define_callbacks :dispatch
- set_callback :dispatch, :before, :log, :unless => proc {|c| c.action_name == :index || c.action_name == :show }
+ set_callback :dispatch, :before, :log, unless: proc { |c| c.action_name == :index || c.action_name == :show }
set_callback :dispatch, :after, :log2
attr_reader :action_name, :logger
@@ -114,7 +115,7 @@ module CallbacksTest
end
class Child < ParentController
- skip_callback :dispatch, :before, :log, :if => proc {|c| c.action_name == :update}
+ skip_callback :dispatch, :before, :log, if: proc { |c| c.action_name == :update }
skip_callback :dispatch, :after, :log2
end
@@ -125,10 +126,10 @@ module CallbacksTest
super
end
- before_save Proc.new {|r| r.history << [:before_save, :starts_true, :if] }, :if => :starts_true
- before_save Proc.new {|r| r.history << [:before_save, :starts_false, :if] }, :if => :starts_false
- before_save Proc.new {|r| r.history << [:before_save, :starts_true, :unless] }, :unless => :starts_true
- before_save Proc.new {|r| r.history << [:before_save, :starts_false, :unless] }, :unless => :starts_false
+ before_save Proc.new { |r| r.history << [:before_save, :starts_true, :if] }, if: :starts_true
+ before_save Proc.new { |r| r.history << [:before_save, :starts_false, :if] }, if: :starts_false
+ before_save Proc.new { |r| r.history << [:before_save, :starts_true, :unless] }, unless: :starts_true
+ before_save Proc.new { |r| r.history << [:before_save, :starts_false, :unless] }, unless: :starts_false
def starts_true
if @@starts_true
@@ -181,27 +182,20 @@ module CallbacksTest
end
end
-
-
class ConditionalPerson < Record
# proc
- before_save Proc.new { |r| r.history << [:before_save, :proc] }, :if => Proc.new { |r| true }
- before_save Proc.new { |r| r.history << "b00m" }, :if => Proc.new { |r| false }
- before_save Proc.new { |r| r.history << [:before_save, :proc] }, :unless => Proc.new { |r| false }
- before_save Proc.new { |r| r.history << "b00m" }, :unless => Proc.new { |r| true }
+ before_save Proc.new { |r| r.history << [:before_save, :proc] }, if: Proc.new { |r| true }
+ before_save Proc.new { |r| r.history << "b00m" }, if: Proc.new { |r| false }
+ before_save Proc.new { |r| r.history << [:before_save, :proc] }, unless: Proc.new { |r| false }
+ before_save Proc.new { |r| r.history << "b00m" }, unless: Proc.new { |r| true }
# symbol
- before_save Proc.new { |r| r.history << [:before_save, :symbol] }, :if => :yes
- before_save Proc.new { |r| r.history << "b00m" }, :if => :no
- before_save Proc.new { |r| r.history << [:before_save, :symbol] }, :unless => :no
- before_save Proc.new { |r| r.history << "b00m" }, :unless => :yes
- # string
- 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'
+ before_save Proc.new { |r| r.history << [:before_save, :symbol] }, if: :yes
+ before_save Proc.new { |r| r.history << "b00m" }, if: :no
+ before_save Proc.new { |r| r.history << [:before_save, :symbol] }, unless: :no
+ before_save Proc.new { |r| r.history << "b00m" }, unless: :yes
# 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
+ 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
def yes; true; end
def other_yes; true; end
@@ -222,26 +216,64 @@ module CallbacksTest
define_callbacks :save
end
- class AroundPerson < MySuper
+ class MySlate < MySuper
attr_reader :history
+ attr_accessor :save_fails
- set_callback :save, :before, :nope, :if => :no
- set_callback :save, :before, :nope, :unless => :yes
- set_callback :save, :after, :tweedle
- 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 }
- set_callback :save, :before, :yup, :if => proc { true }
- set_callback :save, :before, :yup, :unless => proc { false }
- set_callback :save, :around, :tweedle_dum
- set_callback :save, :around, :w0tyes, :if => :yes
- set_callback :save, :around, :w0tno, :if => :no
- set_callback :save, :around, :tweedle_deedle
+ 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
+ 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 }
+ set_callback :save, :before, :yup, if: proc { true }
+ set_callback :save, :before, :yup, unless: proc { false }
+ set_callback :save, :around, :tweedle_dum
+ set_callback :save, :around, :w0tyes, if: :yes
+ set_callback :save, :around, :w0tno, if: :no
+ set_callback :save, :around, :tweedle_deedle
+
def nope
@history << "boom"
end
@@ -261,10 +293,6 @@ module CallbacksTest
yield
end
- def tweedle_dee
- @history << "tweedle dee"
- end
-
def tweedle_dum
@history << "tweedle dum pre"
yield
@@ -280,16 +308,6 @@ module CallbacksTest
yield
@history << "tweedle deedle post"
end
-
- def initialize
- @history = []
- end
-
- def save
- run_callbacks :save do
- @history << "running"
- end
- end
end
class AroundPersonResult < MySuper
@@ -323,7 +341,7 @@ module CallbacksTest
define_callbacks :save
attr_reader :stuff
- set_callback :save, :before, :action, :if => :yes
+ set_callback :save, :before, :action, if: :yes
def yes() true end
@@ -361,7 +379,6 @@ module CallbacksTest
end
class ExtendCallbacks
-
include ActiveSupport::Callbacks
define_callbacks :save
@@ -391,7 +408,6 @@ module CallbacksTest
around = AroundPerson.new
around.save
assert_equal [
- "tweedle dee",
"yup", "yup",
"tweedle dum pre",
"w0tyes before",
@@ -405,6 +421,96 @@ 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
+ around.save_fails = true
+
+ exception = (around.save rescue $!)
+
+ # Make sure we have the exception we're expecting
+ assert_equal "inside save", exception.message
+
+ call_stack = exception.backtrace_locations
+ call_stack.pop caller_locations(0).size
+
+ # Yes, this looks like an implementation test, but it's the least
+ # obtuse way of asserting that there aren't a load of entries in
+ # the call stack for each callback.
+ #
+ # If you've renamed a method, or squeezed more lines out, go ahead
+ # and update this assertion. But if you're here because a
+ # refactoring added new lines, please reconsider.
+
+ # As shown here, our current budget is one line for run_callbacks
+ # itself, plus N+1 lines where N is the number of :around
+ # callbacks that have been invoked, if there are any (plus
+ # whatever the callbacks do themselves, of course).
+
+ assert_equal [
+ "block in save",
+ "block in run_callbacks",
+ "tweedle_deedle",
+ "block in run_callbacks",
+ "w0tyes",
+ "block in run_callbacks",
+ "tweedle_dum",
+ "block in run_callbacks",
+ "run_callbacks",
+ "save"
+ ], call_stack.map(&:label)
+ end
+
+ def test_short_call_stack
+ person = Person.new
+ person.save_fails = true
+
+ exception = (person.save rescue $!)
+
+ # Make sure we have the exception we're expecting
+ assert_equal "inside save", exception.message
+
+ call_stack = exception.backtrace_locations
+ call_stack.pop caller_locations(0).size
+
+ # This budget much simpler: with no :around callbacks invoked,
+ # there should be just one line. run_callbacks yields directly
+ # back to its caller.
+
+ assert_equal [
+ "block in save",
+ "run_callbacks",
+ "save"
+ ], call_stack.map(&:label)
+ end
+ end
+
class AroundCallbackResultTest < ActiveSupport::TestCase
def test_save_around
around = AroundPersonResult.new
@@ -419,7 +525,6 @@ module CallbacksTest
assert_equal [], person.history
person.save
assert_equal [
- [:before_save, :string],
[:before_save, :proc],
[:before_save, :object],
[:before_save, :block],
@@ -427,7 +532,6 @@ module CallbacksTest
[:after_save, :class],
[:after_save, :object],
[:after_save, :proc],
- [:after_save, :string],
[:after_save, :symbol]
], person.history
end
@@ -446,21 +550,18 @@ module CallbacksTest
[:after_save, :class],
[:after_save, :object],
[:after_save, :proc],
- [:after_save, :string],
[:after_save, :symbol]
], person.history
end
end
class CallbacksTest < ActiveSupport::TestCase
-
def test_save_person
person = Person.new
assert_equal [], person.history
person.save
assert_equal [
[:before_save, :symbol],
- [:before_save, :string],
[:before_save, :proc],
[:before_save, :object],
[:before_save, :class],
@@ -469,7 +570,6 @@ module CallbacksTest
[:after_save, :class],
[:after_save, :object],
[:after_save, :proc],
- [:after_save, :string],
[:after_save, :symbol]
], person.history
end
@@ -484,15 +584,11 @@ module CallbacksTest
[:before_save, :proc],
[:before_save, :symbol],
[:before_save, :symbol],
- [:before_save, :string],
- [:before_save, :string],
[:before_save, :combined_symbol],
], person.history
end
end
-
-
class ResetCallbackTest < ActiveSupport::TestCase
def test_save_conditional_person
person = CleanPerson.new
@@ -637,7 +733,7 @@ module CallbacksTest
class CustomScopeObject
include ActiveSupport::Callbacks
- define_callbacks :save, :scope => [:kind, :name]
+ define_callbacks :save, scope: [:kind, :name]
set_callback :save, :before, CallbackObject.new
attr_accessor :record
@@ -733,7 +829,7 @@ module CallbacksTest
def test_block_never_called_if_terminated
obj = CallbackTerminator.new
obj.save
- assert !obj.saved
+ assert_not obj.saved
end
end
@@ -761,45 +857,15 @@ module CallbacksTest
def test_block_never_called_if_abort_is_thrown
obj = CallbackDefaultTerminator.new
obj.save
- assert !obj.saved
- end
- end
-
- class CallbackFalseTerminatorWithoutConfigTest < ActiveSupport::TestCase
- def test_returning_false_halts_callback_if_config_variable_is_not_set
- obj = CallbackFalseTerminator.new
- assert_deprecated do
- obj.save
- assert_equal :second, obj.halted
- assert !obj.saved
- end
- end
- end
-
- class CallbackFalseTerminatorWithConfigTrueTest < ActiveSupport::TestCase
- def setup
- ActiveSupport::Callbacks::CallbackChain.halt_and_display_warning_on_return_false = true
- end
-
- def test_returning_false_halts_callback_if_config_variable_is_true
- obj = CallbackFalseTerminator.new
- assert_deprecated do
- obj.save
- assert_equal :second, obj.halted
- assert !obj.saved
- end
+ assert_not obj.saved
end
end
- class CallbackFalseTerminatorWithConfigFalseTest < ActiveSupport::TestCase
- def setup
- ActiveSupport::Callbacks::CallbackChain.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
@@ -814,7 +880,7 @@ module CallbacksTest
class WriterSkipper < Person
attr_accessor :age
- skip_callback :save, :before, :before_save_method, :if => lambda {self.age > 21}
+ skip_callback :save, :before, :before_save_method, if: -> { age > 21 }
end
class WriterCallbacksTest < ActiveSupport::TestCase
@@ -825,7 +891,6 @@ module CallbacksTest
writer.save
assert_equal [
[:before_save, :symbol],
- [:before_save, :string],
[:before_save, :proc],
[:before_save, :object],
[:before_save, :class],
@@ -834,7 +899,6 @@ module CallbacksTest
[:after_save, :class],
[:after_save, :object],
[:after_save, :proc],
- [:after_save, :string],
[:after_save, :symbol]
], writer.history
end
@@ -889,7 +953,7 @@ module CallbacksTest
def test_proc_arity_2
assert_raises(ArgumentError) do
- klass = build_class(->(x,y) { })
+ klass = build_class(->(x, y) { })
klass.new.run
end
end
@@ -907,7 +971,7 @@ module CallbacksTest
Class.new {
include ActiveSupport::Callbacks
define_callbacks :foo
- set_callback :foo, :before, :foo, :if => callback
+ set_callback :foo, :before, :foo, if: callback
def foo; end
def run; run_callbacks :foo; end
}
@@ -922,11 +986,11 @@ module CallbacksTest
}
klass = Class.new {
include ActiveSupport::Callbacks
- define_callbacks :foo, :scope => [:name]
- set_callback :foo, :before, :foo, :if => callback
+ define_callbacks :foo, scope: [:name]
+ 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
@@ -968,7 +1032,7 @@ module CallbacksTest
def test_proc_arity2
assert_raises(ArgumentError) do
- object = build_class(->(a,b) { }).new
+ object = build_class(->(a, b) { }).new
object.run
end
end
@@ -1048,14 +1112,6 @@ module CallbacksTest
assert_equal 1, calls.length
end
- def test_add_eval
- calls = []
- klass = 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 {
@@ -1090,7 +1146,7 @@ module CallbacksTest
def test_skip_string # raises error
calls = []
- klass = build_class("bar")
+ klass = build_class(:bar)
klass.class_eval { define_method(:bar) { calls << klass } }
assert_raises(ArgumentError) { klass.skip "bar" }
klass.new.run
@@ -1115,4 +1171,26 @@ module CallbacksTest
assert_equal 1, calls.length
end
end
+
+ class NotSupportedStringConditionalTest < ActiveSupport::TestCase
+ def test_string_conditional_options
+ klass = Class.new(Record)
+
+ assert_raises(ArgumentError) { klass.before_save :tweedle, if: ["true"] }
+ assert_raises(ArgumentError) { klass.before_save :tweedle, if: "true" }
+ assert_raises(ArgumentError) { klass.after_save :tweedle, unless: "false" }
+ assert_raises(ArgumentError) { klass.skip_callback :save, :before, :tweedle, if: "true" }
+ assert_raises(ArgumentError) { klass.skip_callback :save, :after, :tweedle, unless: "false" }
+ end
+ end
+
+ class NotPermittedStringCallbackTest < ActiveSupport::TestCase
+ def test_passing_string_callback_is_not_permitted
+ klass = Class.new(Record)
+
+ assert_raises(ArgumentError) do
+ klass.before_save "tweedle"
+ end
+ end
+ end
end
diff --git a/activesupport/test/class_cache_test.rb b/activesupport/test/class_cache_test.rb
index b96f476ce6..1ef1939b4b 100644
--- a/activesupport/test/class_cache_test.rb
+++ b/activesupport/test/class_cache_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'active_support/dependencies'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/dependencies"
module ActiveSupport
module Dependencies
@@ -9,17 +11,17 @@ module ActiveSupport
end
def test_empty?
- assert @cache.empty?
+ assert_empty @cache
@cache.store(ClassCacheTest)
- assert !@cache.empty?
+ assert_not_empty @cache
end
def test_clear!
- assert @cache.empty?
+ assert_empty @cache
@cache.store(ClassCacheTest)
- assert !@cache.empty?
+ assert_not_empty @cache
@cache.clear!
- assert @cache.empty?
+ assert_empty @cache
end
def test_set_key
@@ -38,35 +40,35 @@ module ActiveSupport
end
def test_get_constantizes
- assert @cache.empty?
+ assert_empty @cache
assert_equal ClassCacheTest, @cache.get(ClassCacheTest.name)
end
def test_get_constantizes_fails_on_invalid_names
- assert @cache.empty?
+ assert_empty @cache
assert_raise NameError do
@cache.get("OmgTotallyInvalidConstantName")
end
end
def test_get_alias
- assert @cache.empty?
+ assert_empty @cache
assert_equal @cache[ClassCacheTest.name], @cache.get(ClassCacheTest.name)
end
def test_safe_get_constantizes
- assert @cache.empty?
+ assert_empty @cache
assert_equal ClassCacheTest, @cache.safe_get(ClassCacheTest.name)
end
def test_safe_get_constantizes_doesnt_fail_on_invalid_names
- assert @cache.empty?
- assert_equal nil, @cache.safe_get("OmgTotallyInvalidConstantName")
+ assert_empty @cache
+ assert_nil @cache.safe_get("OmgTotallyInvalidConstantName")
end
def test_new_rejects_strings
@cache.store ClassCacheTest.name
- assert !@cache.key?(ClassCacheTest.name)
+ assert_not @cache.key?(ClassCacheTest.name)
end
def test_store_returns_self
diff --git a/activesupport/test/clean_backtrace_test.rb b/activesupport/test/clean_backtrace_test.rb
index 05580352a9..a0a7056952 100644
--- a/activesupport/test/clean_backtrace_test.rb
+++ b/activesupport/test/clean_backtrace_test.rb
@@ -1,15 +1,17 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class BacktraceCleanerFilterTest < ActiveSupport::TestCase
def setup
@bc = ActiveSupport::BacktraceCleaner.new
- @bc.add_filter { |line| line.gsub("/my/prefix", '') }
+ @bc.add_filter { |line| line.gsub("/my/prefix", "") }
end
test "backtrace should filter all lines in a backtrace, removing prefixes" do
assert_equal \
- ["/my/class.rb", "/my/module.rb"],
- @bc.clean(["/my/prefix/my/class.rb", "/my/prefix/my/module.rb"])
+ ["/my/class.rb", "/my/module.rb"],
+ @bc.clean(["/my/prefix/my/class.rb", "/my/prefix/my/module.rb"])
end
test "backtrace cleaner should allow removing filters" do
@@ -20,13 +22,12 @@ class BacktraceCleanerFilterTest < ActiveSupport::TestCase
test "backtrace should contain unaltered lines if they dont match a filter" do
assert_equal "/my/other_prefix/my/class.rb", @bc.clean([ "/my/other_prefix/my/class.rb" ]).first
end
-
end
class BacktraceCleanerSilencerTest < ActiveSupport::TestCase
def setup
@bc = ActiveSupport::BacktraceCleaner.new
- @bc.add_silencer { |line| line =~ /mongrel/ }
+ @bc.add_silencer { |line| line.include?("mongrel") }
end
test "backtrace should not contain lines that match the silencer" do
@@ -44,8 +45,8 @@ end
class BacktraceCleanerMultipleSilencersTest < ActiveSupport::TestCase
def setup
@bc = ActiveSupport::BacktraceCleaner.new
- @bc.add_silencer { |line| line =~ /mongrel/ }
- @bc.add_silencer { |line| line =~ /yolo/ }
+ @bc.add_silencer { |line| line.include?("mongrel") }
+ @bc.add_silencer { |line| line.include?("yolo") }
end
test "backtrace should not contain lines that match the silencers" do
@@ -66,10 +67,50 @@ class BacktraceCleanerFilterAndSilencerTest < ActiveSupport::TestCase
def setup
@bc = ActiveSupport::BacktraceCleaner.new
@bc.add_filter { |line| line.gsub("/mongrel", "") }
- @bc.add_silencer { |line| line =~ /mongrel/ }
+ @bc.add_silencer { |line| line.include?("mongrel") }
end
test "backtrace should not silence lines that has first had their silence hook filtered out" do
assert_equal [ "/class.rb" ], @bc.clean([ "/mongrel/class.rb" ])
end
end
+
+class BacktraceCleanerDefaultFilterAndSilencerTest < ActiveSupport::TestCase
+ def setup
+ @bc = ActiveSupport::BacktraceCleaner.new
+ end
+
+ test "should format installed gems correctly" do
+ backtrace = [ "#{Gem.default_dir}/gems/nosuchgem-1.2.3/lib/foo.rb" ]
+ result = @bc.clean(backtrace, :all)
+ assert_equal "nosuchgem (1.2.3) lib/foo.rb", result[0]
+ end
+
+ 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
+ backtrace = [ "#{target_dir}/gems/nosuchgem-1.2.3/lib/foo.rb" ]
+ result = @bc.clean(backtrace, :all)
+ assert_equal "nosuchgem (1.2.3) lib/foo.rb", result[0]
+ end
+ end
+
+ test "should format gems installed by bundler" do
+ backtrace = [ "#{Gem.default_dir}/bundler/gems/nosuchgem-1.2.3/lib/foo.rb" ]
+ result = @bc.clean(backtrace, :all)
+ assert_equal "nosuchgem (1.2.3) lib/foo.rb", result[0]
+ end
+
+ test "should silence gems from the backtrace" do
+ backtrace = [ "#{Gem.path[0]}/gems/nosuchgem-1.2.3/lib/foo.rb" ]
+ result = @bc.clean(backtrace)
+ assert_empty result
+ end
+
+ test "should silence stdlib" do
+ backtrace = ["#{RbConfig::CONFIG["rubylibdir"]}/lib/foo.rb"]
+ result = @bc.clean(backtrace)
+ assert_empty result
+ end
+end
diff --git a/activesupport/test/clean_logger_test.rb b/activesupport/test/clean_logger_test.rb
index 02693a97dc..6d8f7064ce 100644
--- a/activesupport/test/clean_logger_test.rb
+++ b/activesupport/test/clean_logger_test.rb
@@ -1,6 +1,8 @@
-require 'abstract_unit'
-require 'stringio'
-require 'active_support/logger'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "stringio"
+require "active_support/logger"
class CleanLoggerTest < ActiveSupport::TestCase
def setup
@@ -9,14 +11,14 @@ class CleanLoggerTest < ActiveSupport::TestCase
end
def test_format_message
- @logger.error 'error'
+ @logger.error "error"
assert_equal "error\n", @out.string
end
def test_datetime_format
@logger.formatter = Logger::Formatter.new
@logger.formatter.datetime_format = "%Y-%m-%d"
- @logger.debug 'debug'
+ @logger.debug "debug"
assert_equal "%Y-%m-%d", @logger.formatter.datetime_format
assert_match(/D, \[\d\d\d\d-\d\d-\d\d#\d+\] DEBUG -- : debug/, @out.string)
end
diff --git a/activesupport/test/concern_test.rb b/activesupport/test/concern_test.rb
index 8ea701cfb7..98d8f3ee0d 100644
--- a/activesupport/test/concern_test.rb
+++ b/activesupport/test/concern_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'active_support/concern'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/concern"
class ConcernTest < ActiveSupport::TestCase
module Baz
@@ -66,17 +68,17 @@ class ConcernTest < ActiveSupport::TestCase
def test_module_is_included_normally
@klass.include(Baz)
assert_equal "baz", @klass.new.baz
- assert @klass.included_modules.include?(ConcernTest::Baz)
+ assert_includes @klass.included_modules, ConcernTest::Baz
end
def test_class_methods_are_extended
@klass.include(Baz)
assert_equal "baz", @klass.baz
- assert_equal ConcernTest::Baz::ClassMethods, (class << @klass; self.included_modules; end)[0]
+ assert_equal ConcernTest::Baz::ClassMethods, (class << @klass; included_modules; end)[0]
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
@@ -89,7 +91,7 @@ class ConcernTest < ActiveSupport::TestCase
end
end
@klass.include test_module
- assert_equal false, Object.respond_to?(:test)
+ assert_not_respond_to Object, :test
Qux.class_eval do
remove_const :ClassMethods
end
@@ -105,7 +107,7 @@ class ConcernTest < ActiveSupport::TestCase
assert_equal "bar", @klass.new.bar
assert_equal "bar+baz", @klass.new.baz
assert_equal "bar's baz + baz", @klass.baz
- assert @klass.included_modules.include?(ConcernTest::Bar)
+ assert_includes @klass.included_modules, ConcernTest::Bar
end
def test_dependencies_with_multiple_modules
diff --git a/activesupport/test/concurrency/load_interlock_aware_monitor_test.rb b/activesupport/test/concurrency/load_interlock_aware_monitor_test.rb
new file mode 100644
index 0000000000..2d0f45ec5f
--- /dev/null
+++ b/activesupport/test/concurrency/load_interlock_aware_monitor_test.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "concurrent/atomic/count_down_latch"
+require "active_support/concurrency/load_interlock_aware_monitor"
+
+module ActiveSupport
+ module Concurrency
+ class LoadInterlockAwareMonitorTest < ActiveSupport::TestCase
+ def setup
+ @monitor = ActiveSupport::Concurrency::LoadInterlockAwareMonitor.new
+ end
+
+ def test_entering_with_no_blocking
+ assert @monitor.mon_enter
+ end
+
+ def test_entering_with_blocking
+ load_interlock_latch = Concurrent::CountDownLatch.new
+ monitor_latch = Concurrent::CountDownLatch.new
+
+ able_to_use_monitor = false
+ able_to_load = false
+
+ thread_with_load_interlock = Thread.new do
+ ActiveSupport::Dependencies.interlock.running do
+ load_interlock_latch.count_down
+ monitor_latch.wait
+
+ @monitor.synchronize do
+ able_to_use_monitor = true
+ end
+ end
+ end
+
+ thread_with_monitor_lock = Thread.new do
+ @monitor.synchronize do
+ monitor_latch.count_down
+ load_interlock_latch.wait
+
+ ActiveSupport::Dependencies.interlock.loading do
+ able_to_load = true
+ end
+ end
+ end
+
+ thread_with_load_interlock.join
+ thread_with_monitor_lock.join
+
+ assert able_to_use_monitor
+ assert able_to_load
+ end
+ end
+ end
+end
diff --git a/activesupport/test/configurable_test.rb b/activesupport/test/configurable_test.rb
index 5d22ded2de..1cf40261dc 100644
--- a/activesupport/test/configurable_test.rb
+++ b/activesupport/test/configurable_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'active_support/configurable'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/configurable"
class ConfigurableActiveSupport < ActiveSupport::TestCase
class Parent
@@ -41,11 +43,11 @@ class ConfigurableActiveSupport < ActiveSupport::TestCase
test "configuration accessors are not available on instance" do
instance = Parent.new
- assert !instance.respond_to?(:bar)
- assert !instance.respond_to?(:bar=)
+ assert_not_respond_to instance, :bar
+ assert_not_respond_to instance, :bar=
- assert !instance.respond_to?(:baz)
- assert !instance.respond_to?(:baz=)
+ assert_not_respond_to instance, :baz
+ assert_not_respond_to instance, :baz=
end
test "configuration accessors can take a default value" do
@@ -111,7 +113,7 @@ class ConfigurableActiveSupport < ActiveSupport::TestCase
end
end
- test 'the config_accessor method should not be publicly callable' do
+ test "the config_accessor method should not be publicly callable" do
assert_raises NoMethodError do
Class.new {
include ActiveSupport::Configurable
@@ -121,11 +123,11 @@ class ConfigurableActiveSupport < ActiveSupport::TestCase
def assert_method_defined(object, method)
methods = object.public_methods.map(&:to_s)
- assert methods.include?(method.to_s), "Expected #{methods.inspect} to include #{method.to_s.inspect}"
+ assert_includes methods, method.to_s, "Expected #{methods.inspect} to include #{method.to_s.inspect}"
end
def assert_method_not_defined(object, method)
methods = object.public_methods.map(&:to_s)
- assert !methods.include?(method.to_s), "Expected #{methods.inspect} to not include #{method.to_s.inspect}"
+ assert_not_includes methods, method.to_s, "Expected #{methods.inspect} to not include #{method.to_s.inspect}"
end
end
diff --git a/activesupport/test/constantize_test_cases.rb b/activesupport/test/constantize_test_cases.rb
index 366e4e5ef0..2c6145940b 100644
--- a/activesupport/test/constantize_test_cases.rb
+++ b/activesupport/test/constantize_test_cases.rb
@@ -1,4 +1,6 @@
-require 'dependencies_test_helpers'
+# frozen_string_literal: true
+
+require "dependencies_test_helpers"
module Ace
module Base
@@ -73,6 +75,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
@@ -100,6 +107,10 @@ module ConstantizeTestCases
assert_nil yield("Ace::Gas::ConstantizeTestCases")
assert_nil yield("#<Class:0x7b8b718b>::Nested_1")
assert_nil yield("Ace::gas")
+ assert_nil yield("Object::ABC")
+ assert_nil yield("Object::Object::Object::ABC")
+ assert_nil yield("A::Object::B")
+ assert_nil yield("A::Object::Object::Object::B")
assert_raises(NameError) do
with_autoloading_fixtures do
diff --git a/activesupport/test/core_ext/array/access_test.rb b/activesupport/test/core_ext/array/access_test.rb
index 3f1e0c4cb4..8c217023cf 100644
--- a/activesupport/test/core_ext/array/access_test.rb
+++ b/activesupport/test/core_ext/array/access_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'active_support/core_ext/array'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/array"
class AccessTest < ActiveSupport::TestCase
def test_from
@@ -26,6 +28,8 @@ class AccessTest < ActiveSupport::TestCase
assert_equal array[3], array.fourth
assert_equal array[4], array.fifth
assert_equal array[41], array.forty_two
+ assert_equal array[-3], array.third_to_last
+ assert_equal array[-2], array.second_to_last
end
def test_without
diff --git a/activesupport/test/core_ext/array/conversions_test.rb b/activesupport/test/core_ext/array/conversions_test.rb
index 507e13f968..0a7c43d421 100644
--- a/activesupport/test/core_ext/array/conversions_test.rb
+++ b/activesupport/test/core_ext/array/conversions_test.rb
@@ -1,37 +1,39 @@
-require 'abstract_unit'
-require 'active_support/core_ext/array'
-require 'active_support/core_ext/big_decimal'
-require 'active_support/core_ext/hash'
-require 'active_support/core_ext/string'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/array"
+require "active_support/core_ext/big_decimal"
+require "active_support/core_ext/hash"
+require "active_support/core_ext/string"
class ToSentenceTest < ActiveSupport::TestCase
def test_plain_array_to_sentence
assert_equal "", [].to_sentence
- assert_equal "one", ['one'].to_sentence
- assert_equal "one and two", ['one', 'two'].to_sentence
- assert_equal "one, two, and three", ['one', 'two', 'three'].to_sentence
+ assert_equal "one", ["one"].to_sentence
+ assert_equal "one and two", ["one", "two"].to_sentence
+ assert_equal "one, two, and three", ["one", "two", "three"].to_sentence
end
def test_to_sentence_with_words_connector
- assert_equal "one two, and three", ['one', 'two', 'three'].to_sentence(words_connector: ' ')
- assert_equal "one & two, and three", ['one', 'two', 'three'].to_sentence(words_connector: ' & ')
- assert_equal "onetwo, and three", ['one', 'two', 'three'].to_sentence(words_connector: nil)
+ assert_equal "one two, and three", ["one", "two", "three"].to_sentence(words_connector: " ")
+ assert_equal "one & two, and three", ["one", "two", "three"].to_sentence(words_connector: " & ")
+ assert_equal "onetwo, and three", ["one", "two", "three"].to_sentence(words_connector: nil)
end
def test_to_sentence_with_last_word_connector
- assert_equal "one, two, and also three", ['one', 'two', 'three'].to_sentence(last_word_connector: ', and also ')
- assert_equal "one, twothree", ['one', 'two', 'three'].to_sentence(last_word_connector: nil)
- assert_equal "one, two three", ['one', 'two', 'three'].to_sentence(last_word_connector: ' ')
- assert_equal "one, two and three", ['one', 'two', 'three'].to_sentence(last_word_connector: ' and ')
+ assert_equal "one, two, and also three", ["one", "two", "three"].to_sentence(last_word_connector: ", and also ")
+ assert_equal "one, twothree", ["one", "two", "three"].to_sentence(last_word_connector: nil)
+ assert_equal "one, two three", ["one", "two", "three"].to_sentence(last_word_connector: " ")
+ assert_equal "one, two and three", ["one", "two", "three"].to_sentence(last_word_connector: " and ")
end
def test_two_elements
- assert_equal "one and two", ['one', 'two'].to_sentence
- assert_equal "one two", ['one', 'two'].to_sentence(two_words_connector: ' ')
+ assert_equal "one and two", ["one", "two"].to_sentence
+ assert_equal "one two", ["one", "two"].to_sentence(two_words_connector: " ")
end
def test_one_element
- assert_equal "one", ['one'].to_sentence
+ assert_equal "one", ["one"].to_sentence
end
def test_one_element_not_same_object
@@ -40,31 +42,31 @@ class ToSentenceTest < ActiveSupport::TestCase
end
def test_one_non_string_element
- assert_equal '1', [1].to_sentence
+ assert_equal "1", [1].to_sentence
end
def test_does_not_modify_given_hash
- options = { words_connector: ' ' }
- assert_equal "one two, and three", ['one', 'two', 'three'].to_sentence(options)
- assert_equal({ words_connector: ' ' }, options)
+ options = { words_connector: " " }
+ assert_equal "one two, and three", ["one", "two", "three"].to_sentence(options)
+ assert_equal({ words_connector: " " }, options)
end
def test_with_blank_elements
- assert_equal ", one, , two, and three", [nil, 'one', '', 'two', 'three'].to_sentence
+ assert_equal ", one, , two, and three", [nil, "one", "", "two", "three"].to_sentence
end
def test_with_invalid_options
exception = assert_raise ArgumentError do
- ['one', 'two'].to_sentence(passing: 'invalid option')
+ ["one", "two"].to_sentence(passing: "invalid option")
end
- assert_equal exception.message, "Unknown key: :passing. Valid keys are: :words_connector, :two_words_connector, :last_word_connector, :locale"
+ assert_equal "Unknown key: :passing. Valid keys are: :words_connector, :two_words_connector, :last_word_connector, :locale", exception.message
end
def test_always_returns_string
- assert_instance_of String, [ActiveSupport::SafeBuffer.new('one')].to_sentence
- assert_instance_of String, [ActiveSupport::SafeBuffer.new('one'), 'two'].to_sentence
- assert_instance_of String, [ActiveSupport::SafeBuffer.new('one'), 'two', 'three'].to_sentence
+ assert_instance_of String, [ActiveSupport::SafeBuffer.new("one")].to_sentence
+ assert_instance_of String, [ActiveSupport::SafeBuffer.new("one"), "two"].to_sentence
+ assert_instance_of String, [ActiveSupport::SafeBuffer.new("one"), "two", "three"].to_sentence
end
end
@@ -88,32 +90,32 @@ class ToXmlTest < ActiveSupport::TestCase
def test_to_xml_with_hash_elements
xml = [
{ name: "David", age: 26, age_in_millis: 820497600000 },
- { name: "Jason", age: 31, age_in_millis: BigDecimal.new('1.0') }
+ { name: "Jason", age: 31, age_in_millis: BigDecimal("1.0") }
].to_xml(skip_instruct: true, indent: 0)
assert_equal '<objects type="array"><object>', xml.first(30)
- assert xml.include?(%(<age type="integer">26</age>)), xml
- assert xml.include?(%(<age-in-millis type="integer">820497600000</age-in-millis>)), xml
- assert xml.include?(%(<name>David</name>)), xml
- assert xml.include?(%(<age type="integer">31</age>)), xml
- assert xml.include?(%(<age-in-millis type="decimal">1.0</age-in-millis>)), xml
- assert xml.include?(%(<name>Jason</name>)), xml
+ assert_includes xml, %(<age type="integer">26</age>), xml
+ assert_includes xml, %(<age-in-millis type="integer">820497600000</age-in-millis>), xml
+ assert_includes xml, %(<name>David</name>), xml
+ assert_includes xml, %(<age type="integer">31</age>), xml
+ assert_includes xml, %(<age-in-millis type="decimal">1.0</age-in-millis>), xml
+ assert_includes xml, %(<name>Jason</name>), xml
end
def test_to_xml_with_non_hash_elements
- xml = [1, 2, 3].to_xml(skip_instruct: true, indent: 0)
+ xml = %w[1 2 3].to_xml(skip_instruct: true, indent: 0)
- assert_equal '<fixnums type="array"><fixnum', xml.first(29)
- assert xml.include?(%(<fixnum type="integer">2</fixnum>)), xml
+ assert_equal '<strings type="array"><string', xml.first(29)
+ assert_includes xml, %(<string>2</string>), xml
end
def test_to_xml_with_non_hash_different_type_elements
- xml = [1, 2.0, '3'].to_xml(skip_instruct: true, indent: 0)
+ xml = [1, 2.0, "3"].to_xml(skip_instruct: true, indent: 0)
assert_equal '<objects type="array"><object', xml.first(29)
- assert xml.include?(%(<object type="integer">1</object>)), xml
- assert xml.include?(%(<object type="float">2.0</object>)), xml
- assert xml.include?(%(object>3</object>)), xml
+ assert_includes xml, %(<object type="integer">1</object>), xml
+ assert_includes xml, %(<object type="float">2.0</object>), xml
+ assert_includes xml, %(object>3</object>), xml
end
def test_to_xml_with_dedicated_name
@@ -130,10 +132,10 @@ class ToXmlTest < ActiveSupport::TestCase
].to_xml(skip_instruct: true, skip_types: true, indent: 0)
assert_equal "<objects><object>", xml.first(17)
- assert xml.include?(%(<street-address>Paulina</street-address>))
- assert xml.include?(%(<name>David</name>))
- assert xml.include?(%(<street-address>Evergreen</street-address>))
- assert xml.include?(%(<name>Jason</name>))
+ assert_includes xml, %(<street-address>Paulina</street-address>)
+ assert_includes xml, %(<name>David</name>)
+ assert_includes xml, %(<street-address>Evergreen</street-address>)
+ assert_includes xml, %(<name>Jason</name>)
end
def test_to_xml_with_indent_set
@@ -142,10 +144,10 @@ class ToXmlTest < ActiveSupport::TestCase
].to_xml(skip_instruct: true, skip_types: true, indent: 4)
assert_equal "<objects>\n <object>", xml.first(22)
- assert xml.include?(%(\n <street-address>Paulina</street-address>))
- assert xml.include?(%(\n <name>David</name>))
- assert xml.include?(%(\n <street-address>Evergreen</street-address>))
- assert xml.include?(%(\n <name>Jason</name>))
+ assert_includes xml, %(\n <street-address>Paulina</street-address>)
+ assert_includes xml, %(\n <name>David</name>)
+ assert_includes xml, %(\n <street-address>Evergreen</street-address>)
+ assert_includes xml, %(\n <name>Jason</name>)
end
def test_to_xml_with_dasherize_false
@@ -154,8 +156,8 @@ class ToXmlTest < ActiveSupport::TestCase
].to_xml(skip_instruct: true, skip_types: true, indent: 0, dasherize: false)
assert_equal "<objects><object>", xml.first(17)
- assert xml.include?(%(<street_address>Paulina</street_address>))
- assert xml.include?(%(<street_address>Evergreen</street_address>))
+ assert_includes xml, %(<street_address>Paulina</street_address>)
+ assert_includes xml, %(<street_address>Evergreen</street_address>)
end
def test_to_xml_with_dasherize_true
@@ -164,14 +166,14 @@ class ToXmlTest < ActiveSupport::TestCase
].to_xml(skip_instruct: true, skip_types: true, indent: 0, dasherize: true)
assert_equal "<objects><object>", xml.first(17)
- assert xml.include?(%(<street-address>Paulina</street-address>))
- assert xml.include?(%(<street-address>Evergreen</street-address>))
+ assert_includes xml, %(<street-address>Paulina</street-address>)
+ assert_includes xml, %(<street-address>Evergreen</street-address>)
end
def test_to_xml_with_instruct
xml = [
{ name: "David", age: 26, age_in_millis: 820497600000 },
- { name: "Jason", age: 31, age_in_millis: BigDecimal.new('1.0') }
+ { name: "Jason", age: 31, age_in_millis: BigDecimal("1.0") }
].to_xml(skip_instruct: false, indent: 0)
assert_match(/^<\?xml [^>]*/, xml)
@@ -181,12 +183,12 @@ class ToXmlTest < ActiveSupport::TestCase
def test_to_xml_with_block
xml = [
{ name: "David", age: 26, age_in_millis: 820497600000 },
- { name: "Jason", age: 31, age_in_millis: BigDecimal.new('1.0') }
+ { name: "Jason", age: 31, age_in_millis: BigDecimal("1.0") }
].to_xml(skip_instruct: true, indent: 0) do |builder|
builder.count 2
end
- assert xml.include?(%(<count>2</count>)), xml
+ assert_includes xml, %(<count>2</count>), xml
end
def test_to_xml_with_empty
diff --git a/activesupport/test/core_ext/array/extract_options_test.rb b/activesupport/test/core_ext/array/extract_options_test.rb
index 0481a507cf..7a4b15cd71 100644
--- a/activesupport/test/core_ext/array/extract_options_test.rb
+++ b/activesupport/test/core_ext/array/extract_options_test.rb
@@ -1,6 +1,8 @@
-require 'abstract_unit'
-require 'active_support/core_ext/array'
-require 'active_support/core_ext/hash'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/array"
+require "active_support/core_ext/hash"
class ExtractOptionsTest < ActiveSupport::TestCase
class HashSubclass < Hash
diff --git a/activesupport/test/core_ext/array/extract_test.rb b/activesupport/test/core_ext/array/extract_test.rb
new file mode 100644
index 0000000000..f26e055033
--- /dev/null
+++ b/activesupport/test/core_ext/array/extract_test.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/array"
+
+class ExtractTest < ActiveSupport::TestCase
+ def test_extract
+ numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
+ array_id = numbers.object_id
+
+ odd_numbers = numbers.extract!(&:odd?)
+
+ assert_equal [1, 3, 5, 7, 9], odd_numbers
+ assert_equal [0, 2, 4, 6, 8], numbers
+ assert_equal array_id, numbers.object_id
+ end
+
+ def test_extract_without_block
+ numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
+ array_id = numbers.object_id
+
+ extract_enumerator = numbers.extract!
+
+ assert_instance_of Enumerator, extract_enumerator
+ assert_equal numbers.size, extract_enumerator.size
+
+ odd_numbers = extract_enumerator.each(&:odd?)
+
+ assert_equal [1, 3, 5, 7, 9], odd_numbers
+ assert_equal [0, 2, 4, 6, 8], numbers
+ assert_equal array_id, numbers.object_id
+ end
+
+ def test_extract_on_empty_array
+ empty_array = []
+ array_id = empty_array.object_id
+
+ new_empty_array = empty_array.extract! { }
+
+ assert_equal [], new_empty_array
+ assert_equal [], empty_array
+ assert_equal array_id, empty_array.object_id
+ end
+end
diff --git a/activesupport/test/core_ext/array/grouping_test.rb b/activesupport/test/core_ext/array/grouping_test.rb
index 2eb0f05141..37111a5d7d 100644
--- a/activesupport/test/core_ext/array/grouping_test.rb
+++ b/activesupport/test/core_ext/array/grouping_test.rb
@@ -1,38 +1,32 @@
-require 'abstract_unit'
-require 'active_support/core_ext/array'
+# frozen_string_literal: true
-class GroupingTest < ActiveSupport::TestCase
- def setup
- Fixnum.send :private, :/ # test we avoid Integer#/ (redefined by mathn)
- end
-
- def teardown
- Fixnum.send :public, :/
- end
+require "abstract_unit"
+require "active_support/core_ext/array"
+class GroupingTest < ActiveSupport::TestCase
def test_in_groups_of_with_perfect_fit
groups = []
- ('a'..'i').to_a.in_groups_of(3) do |group|
+ ("a".."i").to_a.in_groups_of(3) do |group|
groups << group
end
assert_equal [%w(a b c), %w(d e f), %w(g h i)], groups
- assert_equal [%w(a b c), %w(d e f), %w(g h i)], ('a'..'i').to_a.in_groups_of(3)
+ assert_equal [%w(a b c), %w(d e f), %w(g h i)], ("a".."i").to_a.in_groups_of(3)
end
def test_in_groups_of_with_padding
groups = []
- ('a'..'g').to_a.in_groups_of(3) do |group|
+ ("a".."g").to_a.in_groups_of(3) do |group|
groups << group
end
- assert_equal [%w(a b c), %w(d e f), ['g', nil, nil]], groups
+ assert_equal [%w(a b c), %w(d e f), ["g", nil, nil]], groups
end
def test_in_groups_of_pads_with_specified_values
groups = []
- ('a'..'g').to_a.in_groups_of(3, 'foo') do |group|
+ ("a".."g").to_a.in_groups_of(3, "foo") do |group|
groups << group
end
@@ -42,7 +36,7 @@ class GroupingTest < ActiveSupport::TestCase
def test_in_groups_of_without_padding
groups = []
- ('a'..'g').to_a.in_groups_of(3, false) do |group|
+ ("a".."g").to_a.in_groups_of(3, false) do |group|
groups << group
end
@@ -82,8 +76,8 @@ class GroupingTest < ActiveSupport::TestCase
assert_equal [[1, 2, 3], [4, 5, nil], [6, 7, nil]],
array.in_groups(3)
- assert_equal [[1, 2, 3], [4, 5, 'foo'], [6, 7, 'foo']],
- array.in_groups(3, 'foo')
+ assert_equal [[1, 2, 3], [4, 5, "foo"], [6, 7, "foo"]],
+ array.in_groups(3, "foo")
end
def test_in_groups_without_padding
@@ -113,7 +107,7 @@ class SplitTest < ActiveSupport::TestCase
def test_split_with_block
a = (1..10).to_a
assert_equal [[1, 2], [4, 5], [7, 8], [10]], a.split { |i| i % 3 == 0 }
- assert_equal [1, 2, 3, 4, 5, 6, 7, 8, 9 ,10], a
+ assert_equal [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], a
end
def test_split_with_edge_values
@@ -123,4 +117,12 @@ class SplitTest < ActiveSupport::TestCase
assert_equal [[], [2, 3, 4], []], a.split { |i| i == 1 || i == 5 }
assert_equal [1, 2, 3, 4, 5], a
end
+
+ def test_split_with_repeated_values
+ a = [1, 2, 3, 5, 5, 3, 4, 6, 2, 1, 3]
+ assert_equal [[1, 2], [5, 5], [4, 6, 2, 1], []], a.split(3)
+ assert_equal [[1, 2, 3], [], [3, 4, 6, 2, 1, 3]], a.split(5)
+ assert_equal [[1, 2], [], [], [], [4, 6, 2, 1], []], a.split { |i| i == 3 || i == 5 }
+ assert_equal [1, 2, 3, 5, 5, 3, 4, 6, 2, 1, 3], a
+ end
end
diff --git a/activesupport/test/core_ext/array/prepend_append_test.rb b/activesupport/test/core_ext/array/prepend_append_test.rb
index 762aa69b2b..c34acd66ad 100644
--- a/activesupport/test/core_ext/array/prepend_append_test.rb
+++ b/activesupport/test/core_ext/array/prepend_append_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'active_support/core_ext/array'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/array"
class PrependAppendTest < ActiveSupport::TestCase
def test_append
diff --git a/activesupport/test/core_ext/array/wrap_test.rb b/activesupport/test/core_ext/array/wrap_test.rb
index baf426506f..46564b4d73 100644
--- a/activesupport/test/core_ext/array/wrap_test.rb
+++ b/activesupport/test/core_ext/array/wrap_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'active_support/core_ext/array'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/array"
class WrapTest < ActiveSupport::TestCase
class FakeCollection
diff --git a/activesupport/test/core_ext/bigdecimal_test.rb b/activesupport/test/core_ext/bigdecimal_test.rb
index 423a3f2e9d..62588be33b 100644
--- a/activesupport/test/core_ext/bigdecimal_test.rb
+++ b/activesupport/test/core_ext/bigdecimal_test.rb
@@ -1,9 +1,13 @@
-require 'abstract_unit'
-require 'active_support/core_ext/big_decimal'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/big_decimal"
class BigDecimalTest < ActiveSupport::TestCase
def test_to_s
- bd = BigDecimal.new '0.01'
- assert_equal '0.01', bd.to_s
+ bd = BigDecimal "0.01"
+ assert_equal "0.01", bd.to_s
+ assert_equal "+0.01", bd.to_s("+F")
+ assert_equal "+0.0 1", bd.to_s("+1F")
end
end
diff --git a/activesupport/test/core_ext/class/attribute_test.rb b/activesupport/test/core_ext/class/attribute_test.rb
index e7a1334db3..be6ad82367 100644
--- a/activesupport/test/core_ext/class/attribute_test.rb
+++ b/activesupport/test/core_ext/class/attribute_test.rb
@@ -1,23 +1,33 @@
-require 'abstract_unit'
-require 'active_support/core_ext/class/attribute'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/class/attribute"
class ClassAttributeTest < ActiveSupport::TestCase
def setup
- @klass = Class.new { class_attribute :setting }
+ @klass = Class.new do
+ class_attribute :setting
+ class_attribute :timeout, default: 5
+ end
+
@sub = Class.new(@klass)
end
- test 'defaults to nil' do
+ test "defaults to nil" do
assert_nil @klass.setting
assert_nil @sub.setting
end
- test 'inheritable' do
+ test "custom default" do
+ assert_equal 5, @klass.timeout
+ end
+
+ test "inheritable" do
@klass.setting = 1
assert_equal 1, @sub.setting
end
- test 'overridable' do
+ test "overridable" do
@sub.setting = 1
assert_nil @klass.setting
@@ -27,20 +37,20 @@ class ClassAttributeTest < ActiveSupport::TestCase
assert_equal 1, Class.new(@sub).setting
end
- test 'predicate method' do
+ test "predicate method" do
assert_equal false, @klass.setting?
@klass.setting = 1
assert_equal true, @klass.setting?
end
- test 'instance reader delegates to class' do
+ test "instance reader delegates to class" do
assert_nil @klass.new.setting
@klass.setting = 1
assert_equal 1, @klass.new.setting
end
- test 'instance override' do
+ test "instance override" do
object = @klass.new
object.setting = 1
assert_nil @klass.setting
@@ -48,43 +58,43 @@ class ClassAttributeTest < ActiveSupport::TestCase
assert_equal 1, object.setting
end
- test 'instance predicate' do
+ test "instance predicate" do
object = @klass.new
assert_equal false, object.setting?
object.setting = 1
assert_equal true, object.setting?
end
- test 'disabling instance writer' do
- object = Class.new { class_attribute :setting, :instance_writer => false }.new
- assert_raise(NoMethodError) { object.setting = 'boom' }
+ test "disabling instance writer" do
+ object = Class.new { class_attribute :setting, instance_writer: false }.new
+ assert_raise(NoMethodError) { object.setting = "boom" }
end
- test 'disabling instance reader' do
- object = Class.new { class_attribute :setting, :instance_reader => false }.new
+ test "disabling instance reader" do
+ object = Class.new { class_attribute :setting, instance_reader: false }.new
assert_raise(NoMethodError) { object.setting }
assert_raise(NoMethodError) { object.setting? }
end
- test 'disabling both instance writer and reader' do
- object = Class.new { class_attribute :setting, :instance_accessor => false }.new
+ test "disabling both instance writer and reader" do
+ object = Class.new { class_attribute :setting, instance_accessor: false }.new
assert_raise(NoMethodError) { object.setting }
assert_raise(NoMethodError) { object.setting? }
- assert_raise(NoMethodError) { object.setting = 'boom' }
+ assert_raise(NoMethodError) { object.setting = "boom" }
end
- test 'disabling instance predicate' do
+ test "disabling instance predicate" do
object = Class.new { class_attribute :setting, instance_predicate: false }.new
assert_raise(NoMethodError) { object.setting? }
end
- test 'works well with singleton classes' do
+ test "works well with singleton classes" do
object = @klass.new
- object.singleton_class.setting = 'foo'
- assert_equal 'foo', object.setting
+ object.singleton_class.setting = "foo"
+ assert_equal "foo", object.setting
end
- test 'setter returns set value' do
+ test "setter returns set value" do
val = @klass.send(:setting=, 1)
assert_equal 1, val
end
diff --git a/activesupport/test/core_ext/class_test.rb b/activesupport/test/core_ext/class_test.rb
index 9c6c579ef7..5ea288738e 100644
--- a/activesupport/test/core_ext/class_test.rb
+++ b/activesupport/test/core_ext/class_test.rb
@@ -1,6 +1,8 @@
-require 'abstract_unit'
-require 'active_support/core_ext/class'
-require 'set'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/class"
+require "set"
class ClassTest < ActiveSupport::TestCase
class Parent; end
@@ -25,4 +27,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
+ assert_not Parent.descendants.include?(klass), "descendants should not include singleton classes"
+ end
+
+ def test_subclasses_excludes_singleton_classes
+ klass = Parent.new.singleton_class
+ assert_not Parent.subclasses.include?(klass), "subclasses should not include singleton classes"
+ end
end
diff --git a/activesupport/test/core_ext/date_and_time_behavior.rb b/activesupport/test/core_ext/date_and_time_behavior.rb
index 784547bdf8..b77ea22701 100644
--- a/activesupport/test/core_ext/date_and_time_behavior.rb
+++ b/activesupport/test/core_ext/date_and_time_behavior.rb
@@ -1,107 +1,119 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module DateAndTimeBehavior
def test_yesterday
- assert_equal date_time_init(2005,2,21,10,10,10), date_time_init(2005,2,22,10,10,10).yesterday
- assert_equal date_time_init(2005,2,28,10,10,10), date_time_init(2005,3,2,10,10,10).yesterday.yesterday
+ assert_equal date_time_init(2005, 2, 21, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).yesterday
+ assert_equal date_time_init(2005, 2, 28, 10, 10, 10), date_time_init(2005, 3, 2, 10, 10, 10).yesterday.yesterday
end
def test_prev_day
- assert_equal date_time_init(2005,2,21,10,10,10), date_time_init(2005,2,22,10,10,10).prev_day
- assert_equal date_time_init(2005,2,28,10,10,10), date_time_init(2005,3,2,10,10,10).prev_day.prev_day
+ assert_equal date_time_init(2005, 2, 24, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).prev_day(-2)
+ assert_equal date_time_init(2005, 2, 23, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).prev_day(-1)
+ assert_equal date_time_init(2005, 2, 22, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).prev_day(0)
+ assert_equal date_time_init(2005, 2, 21, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).prev_day(1)
+ assert_equal date_time_init(2005, 2, 20, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).prev_day(2)
+ assert_equal date_time_init(2005, 2, 21, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).prev_day
+ assert_equal date_time_init(2005, 2, 28, 10, 10, 10), date_time_init(2005, 3, 2, 10, 10, 10).prev_day.prev_day
end
def test_tomorrow
- assert_equal date_time_init(2005,2,23,10,10,10), date_time_init(2005,2,22,10,10,10).tomorrow
- assert_equal date_time_init(2005,3,2,10,10,10), date_time_init(2005,2,28,10,10,10).tomorrow.tomorrow
+ assert_equal date_time_init(2005, 2, 23, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).tomorrow
+ assert_equal date_time_init(2005, 3, 2, 10, 10, 10), date_time_init(2005, 2, 28, 10, 10, 10).tomorrow.tomorrow
end
def test_next_day
- assert_equal date_time_init(2005,2,23,10,10,10), date_time_init(2005,2,22,10,10,10).next_day
- assert_equal date_time_init(2005,3,2,10,10,10), date_time_init(2005,2,28,10,10,10).next_day.next_day
+ assert_equal date_time_init(2005, 2, 20, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).next_day(-2)
+ assert_equal date_time_init(2005, 2, 21, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).next_day(-1)
+ assert_equal date_time_init(2005, 2, 22, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).next_day(0)
+ assert_equal date_time_init(2005, 2, 23, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).next_day(1)
+ assert_equal date_time_init(2005, 2, 24, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).next_day(2)
+ assert_equal date_time_init(2005, 2, 23, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).next_day
+ assert_equal date_time_init(2005, 3, 2, 10, 10, 10), date_time_init(2005, 2, 28, 10, 10, 10).next_day.next_day
end
def test_days_ago
- assert_equal date_time_init(2005,6,4,10,10,10), date_time_init(2005,6,5,10,10,10).days_ago(1)
- assert_equal date_time_init(2005,5,31,10,10,10), date_time_init(2005,6,5,10,10,10).days_ago(5)
+ assert_equal date_time_init(2005, 6, 4, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).days_ago(1)
+ assert_equal date_time_init(2005, 5, 31, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).days_ago(5)
end
def test_days_since
- assert_equal date_time_init(2005,6,6,10,10,10), date_time_init(2005,6,5,10,10,10).days_since(1)
- assert_equal date_time_init(2005,1,1,10,10,10), date_time_init(2004,12,31,10,10,10).days_since(1)
+ assert_equal date_time_init(2005, 6, 6, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).days_since(1)
+ assert_equal date_time_init(2005, 1, 1, 10, 10, 10), date_time_init(2004, 12, 31, 10, 10, 10).days_since(1)
end
def test_weeks_ago
- assert_equal date_time_init(2005,5,29,10,10,10), date_time_init(2005,6,5,10,10,10).weeks_ago(1)
- assert_equal date_time_init(2005,5,1,10,10,10), date_time_init(2005,6,5,10,10,10).weeks_ago(5)
- assert_equal date_time_init(2005,4,24,10,10,10), date_time_init(2005,6,5,10,10,10).weeks_ago(6)
- assert_equal date_time_init(2005,2,27,10,10,10), date_time_init(2005,6,5,10,10,10).weeks_ago(14)
- assert_equal date_time_init(2004,12,25,10,10,10), date_time_init(2005,1,1,10,10,10).weeks_ago(1)
+ assert_equal date_time_init(2005, 5, 29, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).weeks_ago(1)
+ assert_equal date_time_init(2005, 5, 1, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).weeks_ago(5)
+ assert_equal date_time_init(2005, 4, 24, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).weeks_ago(6)
+ assert_equal date_time_init(2005, 2, 27, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).weeks_ago(14)
+ assert_equal date_time_init(2004, 12, 25, 10, 10, 10), date_time_init(2005, 1, 1, 10, 10, 10).weeks_ago(1)
end
def test_weeks_since
- assert_equal date_time_init(2005,7,14,10,10,10), date_time_init(2005,7,7,10,10,10).weeks_since(1)
- assert_equal date_time_init(2005,7,14,10,10,10), date_time_init(2005,7,7,10,10,10).weeks_since(1)
- assert_equal date_time_init(2005,7,4,10,10,10), date_time_init(2005,6,27,10,10,10).weeks_since(1)
- assert_equal date_time_init(2005,1,4,10,10,10), date_time_init(2004,12,28,10,10,10).weeks_since(1)
+ assert_equal date_time_init(2005, 7, 14, 10, 10, 10), date_time_init(2005, 7, 7, 10, 10, 10).weeks_since(1)
+ assert_equal date_time_init(2005, 7, 14, 10, 10, 10), date_time_init(2005, 7, 7, 10, 10, 10).weeks_since(1)
+ assert_equal date_time_init(2005, 7, 4, 10, 10, 10), date_time_init(2005, 6, 27, 10, 10, 10).weeks_since(1)
+ assert_equal date_time_init(2005, 1, 4, 10, 10, 10), date_time_init(2004, 12, 28, 10, 10, 10).weeks_since(1)
end
def test_months_ago
- assert_equal date_time_init(2005,5,5,10,10,10), date_time_init(2005,6,5,10,10,10).months_ago(1)
- assert_equal date_time_init(2004,11,5,10,10,10), date_time_init(2005,6,5,10,10,10).months_ago(7)
- assert_equal date_time_init(2004,12,5,10,10,10), date_time_init(2005,6,5,10,10,10).months_ago(6)
- assert_equal date_time_init(2004,6,5,10,10,10), date_time_init(2005,6,5,10,10,10).months_ago(12)
- assert_equal date_time_init(2003,6,5,10,10,10), date_time_init(2005,6,5,10,10,10).months_ago(24)
+ assert_equal date_time_init(2005, 5, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).months_ago(1)
+ assert_equal date_time_init(2004, 11, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).months_ago(7)
+ assert_equal date_time_init(2004, 12, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).months_ago(6)
+ assert_equal date_time_init(2004, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).months_ago(12)
+ assert_equal date_time_init(2003, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).months_ago(24)
end
def test_months_since
- assert_equal date_time_init(2005,7,5,10,10,10), date_time_init(2005,6,5,10,10,10).months_since(1)
- assert_equal date_time_init(2006,1,5,10,10,10), date_time_init(2005,12,5,10,10,10).months_since(1)
- assert_equal date_time_init(2005,12,5,10,10,10), date_time_init(2005,6,5,10,10,10).months_since(6)
- assert_equal date_time_init(2006,6,5,10,10,10), date_time_init(2005,12,5,10,10,10).months_since(6)
- assert_equal date_time_init(2006,1,5,10,10,10), date_time_init(2005,6,5,10,10,10).months_since(7)
- assert_equal date_time_init(2006,6,5,10,10,10), date_time_init(2005,6,5,10,10,10).months_since(12)
- assert_equal date_time_init(2007,6,5,10,10,10), date_time_init(2005,6,5,10,10,10).months_since(24)
- assert_equal date_time_init(2005,4,30,10,10,10), date_time_init(2005,3,31,10,10,10).months_since(1)
- assert_equal date_time_init(2005,2,28,10,10,10), date_time_init(2005,1,29,10,10,10).months_since(1)
- assert_equal date_time_init(2005,2,28,10,10,10), date_time_init(2005,1,30,10,10,10).months_since(1)
- assert_equal date_time_init(2005,2,28,10,10,10), date_time_init(2005,1,31,10,10,10).months_since(1)
+ assert_equal date_time_init(2005, 7, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).months_since(1)
+ assert_equal date_time_init(2006, 1, 5, 10, 10, 10), date_time_init(2005, 12, 5, 10, 10, 10).months_since(1)
+ assert_equal date_time_init(2005, 12, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).months_since(6)
+ assert_equal date_time_init(2006, 6, 5, 10, 10, 10), date_time_init(2005, 12, 5, 10, 10, 10).months_since(6)
+ assert_equal date_time_init(2006, 1, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).months_since(7)
+ assert_equal date_time_init(2006, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).months_since(12)
+ assert_equal date_time_init(2007, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).months_since(24)
+ assert_equal date_time_init(2005, 4, 30, 10, 10, 10), date_time_init(2005, 3, 31, 10, 10, 10).months_since(1)
+ assert_equal date_time_init(2005, 2, 28, 10, 10, 10), date_time_init(2005, 1, 29, 10, 10, 10).months_since(1)
+ assert_equal date_time_init(2005, 2, 28, 10, 10, 10), date_time_init(2005, 1, 30, 10, 10, 10).months_since(1)
+ assert_equal date_time_init(2005, 2, 28, 10, 10, 10), date_time_init(2005, 1, 31, 10, 10, 10).months_since(1)
end
def test_years_ago
- assert_equal date_time_init(2004,6,5,10,10,10), date_time_init(2005,6,5,10,10,10).years_ago(1)
- assert_equal date_time_init(1998,6,5,10,10,10), date_time_init(2005,6,5,10,10,10).years_ago(7)
- assert_equal date_time_init(2003,2,28,10,10,10), date_time_init(2004,2,29,10,10,10).years_ago(1) # 1 year ago from leap day
+ assert_equal date_time_init(2004, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).years_ago(1)
+ assert_equal date_time_init(1998, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).years_ago(7)
+ assert_equal date_time_init(2003, 2, 28, 10, 10, 10), date_time_init(2004, 2, 29, 10, 10, 10).years_ago(1) # 1 year ago from leap day
end
def test_years_since
- assert_equal date_time_init(2006,6,5,10,10,10), date_time_init(2005,6,5,10,10,10).years_since(1)
- assert_equal date_time_init(2012,6,5,10,10,10), date_time_init(2005,6,5,10,10,10).years_since(7)
- assert_equal date_time_init(2005,2,28,10,10,10), date_time_init(2004,2,29,10,10,10).years_since(1) # 1 year since leap day
- assert_equal date_time_init(2182,6,5,10,10,10), date_time_init(2005,6,5,10,10,10).years_since(177)
+ assert_equal date_time_init(2006, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).years_since(1)
+ assert_equal date_time_init(2012, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).years_since(7)
+ assert_equal date_time_init(2005, 2, 28, 10, 10, 10), date_time_init(2004, 2, 29, 10, 10, 10).years_since(1) # 1 year since leap day
+ assert_equal date_time_init(2182, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).years_since(177)
end
def test_beginning_of_month
- assert_equal date_time_init(2005,2,1,0,0,0), date_time_init(2005,2,22,10,10,10).beginning_of_month
+ assert_equal date_time_init(2005, 2, 1, 0, 0, 0), date_time_init(2005, 2, 22, 10, 10, 10).beginning_of_month
end
def test_beginning_of_quarter
- assert_equal date_time_init(2005,1,1,0,0,0), date_time_init(2005,2,15,10,10,10).beginning_of_quarter
- assert_equal date_time_init(2005,1,1,0,0,0), date_time_init(2005,1,1,0,0,0).beginning_of_quarter
- assert_equal date_time_init(2005,10,1,0,0,0), date_time_init(2005,12,31,10,10,10).beginning_of_quarter
- assert_equal date_time_init(2005,4,1,0,0,0), date_time_init(2005,6,30,23,59,59).beginning_of_quarter
+ assert_equal date_time_init(2005, 1, 1, 0, 0, 0), date_time_init(2005, 2, 15, 10, 10, 10).beginning_of_quarter
+ assert_equal date_time_init(2005, 1, 1, 0, 0, 0), date_time_init(2005, 1, 1, 0, 0, 0).beginning_of_quarter
+ assert_equal date_time_init(2005, 10, 1, 0, 0, 0), date_time_init(2005, 12, 31, 10, 10, 10).beginning_of_quarter
+ assert_equal date_time_init(2005, 4, 1, 0, 0, 0), date_time_init(2005, 6, 30, 23, 59, 59).beginning_of_quarter
end
def test_end_of_quarter
- assert_equal date_time_init(2007,3,31,23,59,59,Rational(999999999, 1000)), date_time_init(2007,2,15,10,10,10).end_of_quarter
- assert_equal date_time_init(2007,3,31,23,59,59,Rational(999999999, 1000)), date_time_init(2007,3,31,0,0,0).end_of_quarter
- assert_equal date_time_init(2007,12,31,23,59,59,Rational(999999999, 1000)), date_time_init(2007,12,21,10,10,10).end_of_quarter
- assert_equal date_time_init(2007,6,30,23,59,59,Rational(999999999, 1000)), date_time_init(2007,4,1,0,0,0).end_of_quarter
- assert_equal date_time_init(2008,6,30,23,59,59,Rational(999999999, 1000)), date_time_init(2008,5,31,0,0,0).end_of_quarter
+ assert_equal date_time_init(2007, 3, 31, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 2, 15, 10, 10, 10).end_of_quarter
+ assert_equal date_time_init(2007, 3, 31, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 3, 31, 0, 0, 0).end_of_quarter
+ assert_equal date_time_init(2007, 12, 31, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 12, 21, 10, 10, 10).end_of_quarter
+ assert_equal date_time_init(2007, 6, 30, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 4, 1, 0, 0, 0).end_of_quarter
+ assert_equal date_time_init(2008, 6, 30, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2008, 5, 31, 0, 0, 0).end_of_quarter
end
def test_beginning_of_year
- assert_equal date_time_init(2005,1,1,0,0,0), date_time_init(2005,2,22,10,10,10).beginning_of_year
+ assert_equal date_time_init(2005, 1, 1, 0, 0, 0), date_time_init(2005, 2, 22, 10, 10, 10).beginning_of_year
end
def test_next_week
@@ -110,10 +122,10 @@ module DateAndTimeBehavior
# | 22/2 | | | | | # | | | | 4/3 | | # friday in next week `next_week(:friday)`
# 23/10 | | | | | | # 30/10 | | | | | | # monday in next week `next_week`
# 23/10 | | | | | | # | | 1/11 | | | | # wednesday in next week `next_week(:wednesday)`
- assert_equal date_time_init(2005,2,28,0,0,0), date_time_init(2005,2,22,15,15,10).next_week
- assert_equal date_time_init(2005,3,4,0,0,0), date_time_init(2005,2,22,15,15,10).next_week(:friday)
- assert_equal date_time_init(2006,10,30,0,0,0), date_time_init(2006,10,23,0,0,0).next_week
- assert_equal date_time_init(2006,11,1,0,0,0), date_time_init(2006,10,23,0,0,0).next_week(:wednesday)
+ assert_equal date_time_init(2005, 2, 28, 0, 0, 0), date_time_init(2005, 2, 22, 15, 15, 10).next_week
+ assert_equal date_time_init(2005, 3, 4, 0, 0, 0), date_time_init(2005, 2, 22, 15, 15, 10).next_week(:friday)
+ assert_equal date_time_init(2006, 10, 30, 0, 0, 0), date_time_init(2006, 10, 23, 0, 0, 0).next_week
+ assert_equal date_time_init(2006, 11, 1, 0, 0, 0), date_time_init(2006, 10, 23, 0, 0, 0).next_week(:wednesday)
end
def test_next_week_with_default_beginning_of_week_set
@@ -126,45 +138,63 @@ module DateAndTimeBehavior
end
def test_next_week_at_same_time
- assert_equal date_time_init(2005,2,28,15,15,10), date_time_init(2005,2,22,15,15,10).next_week(:monday, same_time: true)
- assert_equal date_time_init(2005,3,4,15,15,10), date_time_init(2005,2,22,15,15,10).next_week(:friday, same_time: true)
- assert_equal date_time_init(2006,10,30,0,0,0), date_time_init(2006,10,23,0,0,0).next_week(:monday, same_time: true)
- assert_equal date_time_init(2006,11,1,0,0,0), date_time_init(2006,10,23,0,0,0).next_week(:wednesday, same_time: true)
+ assert_equal date_time_init(2005, 2, 28, 15, 15, 10), date_time_init(2005, 2, 22, 15, 15, 10).next_week(:monday, same_time: true)
+ assert_equal date_time_init(2005, 2, 28, 15, 15, 10, 999999), date_time_init(2005, 2, 22, 15, 15, 10, 999999).next_week(:monday, same_time: true)
+ assert_equal date_time_init(2005, 2, 28, 15, 15, 10, Rational(999999999, 1000)), date_time_init(2005, 2, 22, 15, 15, 10, Rational(999999999, 1000)).next_week(:monday, same_time: true)
+ assert_equal date_time_init(2005, 3, 4, 15, 15, 10), date_time_init(2005, 2, 22, 15, 15, 10).next_week(:friday, same_time: true)
+ assert_equal date_time_init(2006, 10, 30, 0, 0, 0), date_time_init(2006, 10, 23, 0, 0, 0).next_week(:monday, same_time: true)
+ assert_equal date_time_init(2006, 11, 1, 0, 0, 0), date_time_init(2006, 10, 23, 0, 0, 0).next_week(:wednesday, same_time: true)
end
def test_next_weekday_on_wednesday
- assert_equal date_time_init(2015,1,8,0,0,0), date_time_init(2015,1,7,0,0,0).next_weekday
- assert_equal date_time_init(2015,1,8,15,15,10), date_time_init(2015,1,7,15,15,10).next_weekday
+ assert_equal date_time_init(2015, 1, 8, 0, 0, 0), date_time_init(2015, 1, 7, 0, 0, 0).next_weekday
+ assert_equal date_time_init(2015, 1, 8, 15, 15, 10), date_time_init(2015, 1, 7, 15, 15, 10).next_weekday
end
def test_next_weekday_on_friday
- assert_equal date_time_init(2015,1,5,0,0,0), date_time_init(2015,1,2,0,0,0).next_weekday
- assert_equal date_time_init(2015,1,5,15,15,10), date_time_init(2015,1,2,15,15,10).next_weekday
+ assert_equal date_time_init(2015, 1, 5, 0, 0, 0), date_time_init(2015, 1, 2, 0, 0, 0).next_weekday
+ assert_equal date_time_init(2015, 1, 5, 15, 15, 10), date_time_init(2015, 1, 2, 15, 15, 10).next_weekday
end
def test_next_weekday_on_saturday
- assert_equal date_time_init(2015,1,5,0,0,0), date_time_init(2015,1,3,0,0,0).next_weekday
- assert_equal date_time_init(2015,1,5,15,15,10), date_time_init(2015,1,3,15,15,10).next_weekday
+ assert_equal date_time_init(2015, 1, 5, 0, 0, 0), date_time_init(2015, 1, 3, 0, 0, 0).next_weekday
+ assert_equal date_time_init(2015, 1, 5, 15, 15, 10), date_time_init(2015, 1, 3, 15, 15, 10).next_weekday
+ end
+
+ def test_next_month
+ assert_equal date_time_init(2004, 12, 22, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).next_month(-2)
+ assert_equal date_time_init(2005, 1, 22, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).next_month(-1)
+ assert_equal date_time_init(2005, 2, 22, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).next_month(0)
+ assert_equal date_time_init(2005, 3, 22, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).next_month(1)
+ assert_equal date_time_init(2005, 4, 22, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).next_month(2)
+ assert_equal date_time_init(2005, 3, 22, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).next_month
+ assert_equal date_time_init(2005, 4, 22, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).next_month.next_month
end
def test_next_month_on_31st
- assert_equal date_time_init(2005,9,30,15,15,10), date_time_init(2005,8,31,15,15,10).next_month
+ assert_equal date_time_init(2005, 9, 30, 15, 15, 10), date_time_init(2005, 8, 31, 15, 15, 10).next_month
end
def test_next_quarter_on_31st
- assert_equal date_time_init(2005,11,30,15,15,10), date_time_init(2005,8,31,15,15,10).next_quarter
+ assert_equal date_time_init(2005, 11, 30, 15, 15, 10), date_time_init(2005, 8, 31, 15, 15, 10).next_quarter
end
def test_next_year
- assert_equal date_time_init(2006,6,5,10,10,10), date_time_init(2005,6,5,10,10,10).next_year
+ assert_equal date_time_init(2003, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).next_year(-2)
+ assert_equal date_time_init(2004, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).next_year(-1)
+ assert_equal date_time_init(2005, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).next_year(0)
+ assert_equal date_time_init(2006, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).next_year(1)
+ assert_equal date_time_init(2007, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).next_year(2)
+ assert_equal date_time_init(2006, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).next_year
+ assert_equal date_time_init(2007, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).next_year.next_year
end
def test_prev_week
- assert_equal date_time_init(2005,2,21,0,0,0), date_time_init(2005,3,1,15,15,10).prev_week
- assert_equal date_time_init(2005,2,22,0,0,0), date_time_init(2005,3,1,15,15,10).prev_week(:tuesday)
- assert_equal date_time_init(2005,2,25,0,0,0), date_time_init(2005,3,1,15,15,10).prev_week(:friday)
- assert_equal date_time_init(2006,10,30,0,0,0), date_time_init(2006,11,6,0,0,0).prev_week
- assert_equal date_time_init(2006,11,15,0,0,0), date_time_init(2006,11,23,0,0,0).prev_week(:wednesday)
+ assert_equal date_time_init(2005, 2, 21, 0, 0, 0), date_time_init(2005, 3, 1, 15, 15, 10).prev_week
+ assert_equal date_time_init(2005, 2, 22, 0, 0, 0), date_time_init(2005, 3, 1, 15, 15, 10).prev_week(:tuesday)
+ assert_equal date_time_init(2005, 2, 25, 0, 0, 0), date_time_init(2005, 3, 1, 15, 15, 10).prev_week(:friday)
+ assert_equal date_time_init(2006, 10, 30, 0, 0, 0), date_time_init(2006, 11, 6, 0, 0, 0).prev_week
+ assert_equal date_time_init(2006, 11, 15, 0, 0, 0), date_time_init(2006, 11, 23, 0, 0, 0).prev_week(:wednesday)
end
def test_prev_week_with_default_beginning_of_week
@@ -177,128 +207,194 @@ module DateAndTimeBehavior
end
def test_prev_week_at_same_time
- assert_equal date_time_init(2005,2,21,15,15,10), date_time_init(2005,3,1,15,15,10).prev_week(:monday, same_time: true)
- assert_equal date_time_init(2005,2,22,15,15,10), date_time_init(2005,3,1,15,15,10).prev_week(:tuesday, same_time: true)
- assert_equal date_time_init(2005,2,25,15,15,10), date_time_init(2005,3,1,15,15,10).prev_week(:friday, same_time: true)
- assert_equal date_time_init(2006,10,30,0,0,0), date_time_init(2006,11,6,0,0,0).prev_week(:monday, same_time: true)
- assert_equal date_time_init(2006,11,15,0,0,0), date_time_init(2006,11,23,0,0,0).prev_week(:wednesday, same_time: true)
+ assert_equal date_time_init(2005, 2, 21, 15, 15, 10), date_time_init(2005, 3, 1, 15, 15, 10).prev_week(:monday, same_time: true)
+ assert_equal date_time_init(2005, 2, 22, 15, 15, 10), date_time_init(2005, 3, 1, 15, 15, 10).prev_week(:tuesday, same_time: true)
+ assert_equal date_time_init(2005, 2, 25, 15, 15, 10), date_time_init(2005, 3, 1, 15, 15, 10).prev_week(:friday, same_time: true)
+ assert_equal date_time_init(2006, 10, 30, 0, 0, 0), date_time_init(2006, 11, 6, 0, 0, 0).prev_week(:monday, same_time: true)
+ assert_equal date_time_init(2006, 11, 15, 0, 0, 0), date_time_init(2006, 11, 23, 0, 0, 0).prev_week(:wednesday, same_time: true)
end
def test_prev_weekday_on_wednesday
- assert_equal date_time_init(2015,1,6,0,0,0), date_time_init(2015,1,7,0,0,0).prev_weekday
- assert_equal date_time_init(2015,1,6,15,15,10), date_time_init(2015,1,7,15,15,10).prev_weekday
+ assert_equal date_time_init(2015, 1, 6, 0, 0, 0), date_time_init(2015, 1, 7, 0, 0, 0).prev_weekday
+ assert_equal date_time_init(2015, 1, 6, 15, 15, 10), date_time_init(2015, 1, 7, 15, 15, 10).prev_weekday
end
def test_prev_weekday_on_monday
- assert_equal date_time_init(2015,1,2,0,0,0), date_time_init(2015,1,5,0,0,0).prev_weekday
- assert_equal date_time_init(2015,1,2,15,15,10), date_time_init(2015,1,5,15,15,10).prev_weekday
+ assert_equal date_time_init(2015, 1, 2, 0, 0, 0), date_time_init(2015, 1, 5, 0, 0, 0).prev_weekday
+ assert_equal date_time_init(2015, 1, 2, 15, 15, 10), date_time_init(2015, 1, 5, 15, 15, 10).prev_weekday
end
def test_prev_weekday_on_sunday
- assert_equal date_time_init(2015,1,2,0,0,0), date_time_init(2015,1,4,0,0,0).prev_weekday
- assert_equal date_time_init(2015,1,2,15,15,10), date_time_init(2015,1,4,15,15,10).prev_weekday
+ assert_equal date_time_init(2015, 1, 2, 0, 0, 0), date_time_init(2015, 1, 4, 0, 0, 0).prev_weekday
+ assert_equal date_time_init(2015, 1, 2, 15, 15, 10), date_time_init(2015, 1, 4, 15, 15, 10).prev_weekday
+ end
+
+ def test_prev_month
+ assert_equal date_time_init(2005, 4, 22, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).prev_month(-2)
+ assert_equal date_time_init(2005, 3, 22, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).prev_month(-1)
+ assert_equal date_time_init(2005, 2, 22, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).prev_month(0)
+ assert_equal date_time_init(2005, 1, 22, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).prev_month(1)
+ assert_equal date_time_init(2004, 12, 22, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).prev_month(2)
+ assert_equal date_time_init(2005, 1, 22, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).prev_month
+ assert_equal date_time_init(2004, 12, 22, 10, 10, 10), date_time_init(2005, 2, 22, 10, 10, 10).prev_month.prev_month
end
def test_prev_month_on_31st
- assert_equal date_time_init(2004,2,29,10,10,10), date_time_init(2004,3,31,10,10,10).prev_month
+ assert_equal date_time_init(2004, 2, 29, 10, 10, 10), date_time_init(2004, 3, 31, 10, 10, 10).prev_month
end
def test_prev_quarter_on_31st
- assert_equal date_time_init(2004,2,29,10,10,10), date_time_init(2004,5,31,10,10,10).prev_quarter
+ assert_equal date_time_init(2004, 2, 29, 10, 10, 10), date_time_init(2004, 5, 31, 10, 10, 10).prev_quarter
end
def test_prev_year
- assert_equal date_time_init(2004,6,5,10,10,10), date_time_init(2005,6,5,10,10,10).prev_year
+ assert_equal date_time_init(2007, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).prev_year(-2)
+ assert_equal date_time_init(2006, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).prev_year(-1)
+ assert_equal date_time_init(2005, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).prev_year(0)
+ assert_equal date_time_init(2004, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).prev_year(1)
+ assert_equal date_time_init(2003, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).prev_year(2)
+ assert_equal date_time_init(2004, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).prev_year
+ assert_equal date_time_init(2003, 6, 5, 10, 10, 10), date_time_init(2005, 6, 5, 10, 10, 10).prev_year.prev_year
+ end
+
+ def test_last_month_on_31st
+ assert_equal date_time_init(2004, 2, 29, 0, 0, 0), date_time_init(2004, 3, 31, 0, 0, 0).last_month
+ end
+
+ def test_last_year
+ assert_equal date_time_init(2004, 6, 5, 10, 0, 0), date_time_init(2005, 6, 5, 10, 0, 0).last_year
end
def test_days_to_week_start
- assert_equal 0, date_time_init(2011,11,01,0,0,0).days_to_week_start(:tuesday)
- assert_equal 1, date_time_init(2011,11,02,0,0,0).days_to_week_start(:tuesday)
- assert_equal 2, date_time_init(2011,11,03,0,0,0).days_to_week_start(:tuesday)
- assert_equal 3, date_time_init(2011,11,04,0,0,0).days_to_week_start(:tuesday)
- assert_equal 4, date_time_init(2011,11,05,0,0,0).days_to_week_start(:tuesday)
- assert_equal 5, date_time_init(2011,11,06,0,0,0).days_to_week_start(:tuesday)
- assert_equal 6, date_time_init(2011,11,07,0,0,0).days_to_week_start(:tuesday)
-
- assert_equal 3, date_time_init(2011,11,03,0,0,0).days_to_week_start(:monday)
- assert_equal 3, date_time_init(2011,11,04,0,0,0).days_to_week_start(:tuesday)
- assert_equal 3, date_time_init(2011,11,05,0,0,0).days_to_week_start(:wednesday)
- assert_equal 3, date_time_init(2011,11,06,0,0,0).days_to_week_start(:thursday)
- assert_equal 3, date_time_init(2011,11,07,0,0,0).days_to_week_start(:friday)
- assert_equal 3, date_time_init(2011,11,8,0,0,0).days_to_week_start(:saturday)
- assert_equal 3, date_time_init(2011,11,9,0,0,0).days_to_week_start(:sunday)
+ assert_equal 0, date_time_init(2011, 11, 01, 0, 0, 0).days_to_week_start(:tuesday)
+ assert_equal 1, date_time_init(2011, 11, 02, 0, 0, 0).days_to_week_start(:tuesday)
+ assert_equal 2, date_time_init(2011, 11, 03, 0, 0, 0).days_to_week_start(:tuesday)
+ assert_equal 3, date_time_init(2011, 11, 04, 0, 0, 0).days_to_week_start(:tuesday)
+ assert_equal 4, date_time_init(2011, 11, 05, 0, 0, 0).days_to_week_start(:tuesday)
+ assert_equal 5, date_time_init(2011, 11, 06, 0, 0, 0).days_to_week_start(:tuesday)
+ assert_equal 6, date_time_init(2011, 11, 07, 0, 0, 0).days_to_week_start(:tuesday)
+
+ assert_equal 3, date_time_init(2011, 11, 03, 0, 0, 0).days_to_week_start(:monday)
+ assert_equal 3, date_time_init(2011, 11, 04, 0, 0, 0).days_to_week_start(:tuesday)
+ assert_equal 3, date_time_init(2011, 11, 05, 0, 0, 0).days_to_week_start(:wednesday)
+ assert_equal 3, date_time_init(2011, 11, 06, 0, 0, 0).days_to_week_start(:thursday)
+ assert_equal 3, date_time_init(2011, 11, 07, 0, 0, 0).days_to_week_start(:friday)
+ assert_equal 3, date_time_init(2011, 11, 8, 0, 0, 0).days_to_week_start(:saturday)
+ assert_equal 3, date_time_init(2011, 11, 9, 0, 0, 0).days_to_week_start(:sunday)
end
def test_days_to_week_start_with_default_set
with_bw_default(:friday) do
- assert_equal 6, Time.local(2012,03,8,0,0,0).days_to_week_start
- assert_equal 5, Time.local(2012,03,7,0,0,0).days_to_week_start
- assert_equal 4, Time.local(2012,03,6,0,0,0).days_to_week_start
- assert_equal 3, Time.local(2012,03,5,0,0,0).days_to_week_start
- assert_equal 2, Time.local(2012,03,4,0,0,0).days_to_week_start
- assert_equal 1, Time.local(2012,03,3,0,0,0).days_to_week_start
- assert_equal 0, Time.local(2012,03,2,0,0,0).days_to_week_start
+ assert_equal 6, Time.local(2012, 03, 8, 0, 0, 0).days_to_week_start
+ assert_equal 5, Time.local(2012, 03, 7, 0, 0, 0).days_to_week_start
+ assert_equal 4, Time.local(2012, 03, 6, 0, 0, 0).days_to_week_start
+ assert_equal 3, Time.local(2012, 03, 5, 0, 0, 0).days_to_week_start
+ assert_equal 2, Time.local(2012, 03, 4, 0, 0, 0).days_to_week_start
+ assert_equal 1, Time.local(2012, 03, 3, 0, 0, 0).days_to_week_start
+ assert_equal 0, Time.local(2012, 03, 2, 0, 0, 0).days_to_week_start
end
end
def test_beginning_of_week
- assert_equal date_time_init(2005,1,31,0,0,0), date_time_init(2005,2,4,10,10,10).beginning_of_week
- assert_equal date_time_init(2005,11,28,0,0,0), date_time_init(2005,11,28,0,0,0).beginning_of_week #monday
- assert_equal date_time_init(2005,11,28,0,0,0), date_time_init(2005,11,29,0,0,0).beginning_of_week #tuesday
- assert_equal date_time_init(2005,11,28,0,0,0), date_time_init(2005,11,30,0,0,0).beginning_of_week #wednesday
- assert_equal date_time_init(2005,11,28,0,0,0), date_time_init(2005,12,01,0,0,0).beginning_of_week #thursday
- assert_equal date_time_init(2005,11,28,0,0,0), date_time_init(2005,12,02,0,0,0).beginning_of_week #friday
- assert_equal date_time_init(2005,11,28,0,0,0), date_time_init(2005,12,03,0,0,0).beginning_of_week #saturday
- assert_equal date_time_init(2005,11,28,0,0,0), date_time_init(2005,12,04,0,0,0).beginning_of_week #sunday
+ assert_equal date_time_init(2005, 1, 31, 0, 0, 0), date_time_init(2005, 2, 4, 10, 10, 10).beginning_of_week
+ assert_equal date_time_init(2005, 11, 28, 0, 0, 0), date_time_init(2005, 11, 28, 0, 0, 0).beginning_of_week # monday
+ assert_equal date_time_init(2005, 11, 28, 0, 0, 0), date_time_init(2005, 11, 29, 0, 0, 0).beginning_of_week # tuesday
+ assert_equal date_time_init(2005, 11, 28, 0, 0, 0), date_time_init(2005, 11, 30, 0, 0, 0).beginning_of_week # wednesday
+ assert_equal date_time_init(2005, 11, 28, 0, 0, 0), date_time_init(2005, 12, 01, 0, 0, 0).beginning_of_week # thursday
+ assert_equal date_time_init(2005, 11, 28, 0, 0, 0), date_time_init(2005, 12, 02, 0, 0, 0).beginning_of_week # friday
+ assert_equal date_time_init(2005, 11, 28, 0, 0, 0), date_time_init(2005, 12, 03, 0, 0, 0).beginning_of_week # saturday
+ assert_equal date_time_init(2005, 11, 28, 0, 0, 0), date_time_init(2005, 12, 04, 0, 0, 0).beginning_of_week # sunday
end
def test_end_of_week
- assert_equal date_time_init(2008,1,6,23,59,59,Rational(999999999, 1000)), date_time_init(2007,12,31,10,10,10).end_of_week
- assert_equal date_time_init(2007,9,2,23,59,59,Rational(999999999, 1000)), date_time_init(2007,8,27,0,0,0).end_of_week #monday
- assert_equal date_time_init(2007,9,2,23,59,59,Rational(999999999, 1000)), date_time_init(2007,8,28,0,0,0).end_of_week #tuesday
- assert_equal date_time_init(2007,9,2,23,59,59,Rational(999999999, 1000)), date_time_init(2007,8,29,0,0,0).end_of_week #wednesday
- assert_equal date_time_init(2007,9,2,23,59,59,Rational(999999999, 1000)), date_time_init(2007,8,30,0,0,0).end_of_week #thursday
- assert_equal date_time_init(2007,9,2,23,59,59,Rational(999999999, 1000)), date_time_init(2007,8,31,0,0,0).end_of_week #friday
- assert_equal date_time_init(2007,9,2,23,59,59,Rational(999999999, 1000)), date_time_init(2007,9,01,0,0,0).end_of_week #saturday
- assert_equal date_time_init(2007,9,2,23,59,59,Rational(999999999, 1000)), date_time_init(2007,9,02,0,0,0).end_of_week #sunday
+ assert_equal date_time_init(2008, 1, 6, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 12, 31, 10, 10, 10).end_of_week
+ assert_equal date_time_init(2007, 9, 2, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 8, 27, 0, 0, 0).end_of_week # monday
+ assert_equal date_time_init(2007, 9, 2, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 8, 28, 0, 0, 0).end_of_week # tuesday
+ assert_equal date_time_init(2007, 9, 2, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 8, 29, 0, 0, 0).end_of_week # wednesday
+ assert_equal date_time_init(2007, 9, 2, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 8, 30, 0, 0, 0).end_of_week # thursday
+ assert_equal date_time_init(2007, 9, 2, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 8, 31, 0, 0, 0).end_of_week # friday
+ assert_equal date_time_init(2007, 9, 2, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 9, 01, 0, 0, 0).end_of_week # saturday
+ assert_equal date_time_init(2007, 9, 2, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 9, 02, 0, 0, 0).end_of_week # sunday
end
def test_end_of_month
- assert_equal date_time_init(2005,3,31,23,59,59,Rational(999999999, 1000)), date_time_init(2005,3,20,10,10,10).end_of_month
- assert_equal date_time_init(2005,2,28,23,59,59,Rational(999999999, 1000)), date_time_init(2005,2,20,10,10,10).end_of_month
- assert_equal date_time_init(2005,4,30,23,59,59,Rational(999999999, 1000)), date_time_init(2005,4,20,10,10,10).end_of_month
+ assert_equal date_time_init(2005, 3, 31, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2005, 3, 20, 10, 10, 10).end_of_month
+ assert_equal date_time_init(2005, 2, 28, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2005, 2, 20, 10, 10, 10).end_of_month
+ assert_equal date_time_init(2005, 4, 30, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2005, 4, 20, 10, 10, 10).end_of_month
end
def test_end_of_year
- assert_equal date_time_init(2007,12,31,23,59,59,Rational(999999999, 1000)), date_time_init(2007,2,22,10,10,10).end_of_year
- assert_equal date_time_init(2007,12,31,23,59,59,Rational(999999999, 1000)), date_time_init(2007,12,31,10,10,10).end_of_year
+ assert_equal date_time_init(2007, 12, 31, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 2, 22, 10, 10, 10).end_of_year
+ assert_equal date_time_init(2007, 12, 31, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2007, 12, 31, 10, 10, 10).end_of_year
+ end
+
+ def test_next_occurring
+ assert_equal date_time_init(2017, 12, 18, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).next_occurring(:monday)
+ assert_equal date_time_init(2017, 12, 19, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).next_occurring(:tuesday)
+ assert_equal date_time_init(2017, 12, 20, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).next_occurring(:wednesday)
+ assert_equal date_time_init(2017, 12, 21, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).next_occurring(:thursday)
+ assert_equal date_time_init(2017, 12, 15, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).next_occurring(:friday)
+ assert_equal date_time_init(2017, 12, 16, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).next_occurring(:saturday)
+ assert_equal date_time_init(2017, 12, 17, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).next_occurring(:sunday)
+ end
+
+ def test_prev_occurring
+ assert_equal date_time_init(2017, 12, 11, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).prev_occurring(:monday)
+ assert_equal date_time_init(2017, 12, 12, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).prev_occurring(:tuesday)
+ assert_equal date_time_init(2017, 12, 13, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).prev_occurring(:wednesday)
+ assert_equal date_time_init(2017, 12, 7, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).prev_occurring(:thursday)
+ assert_equal date_time_init(2017, 12, 8, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).prev_occurring(:friday)
+ assert_equal date_time_init(2017, 12, 9, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).prev_occurring(:saturday)
+ assert_equal date_time_init(2017, 12, 10, 3, 14, 15), date_time_init(2017, 12, 14, 3, 14, 15).prev_occurring(:sunday)
end
def test_monday_with_default_beginning_of_week_set
with_bw_default(:saturday) do
- assert_equal date_time_init(2012,9,17,0,0,0), date_time_init(2012,9,18,0,0,0).monday
+ assert_equal date_time_init(2012, 9, 17, 0, 0, 0), date_time_init(2012, 9, 18, 0, 0, 0).monday
end
end
def test_sunday_with_default_beginning_of_week_set
with_bw_default(:wednesday) do
- assert_equal date_time_init(2012,9,23,23,59,59, Rational(999999999, 1000)), date_time_init(2012,9,19,0,0,0).sunday
+ assert_equal date_time_init(2012, 9, 23, 23, 59, 59, Rational(999999999, 1000)), date_time_init(2012, 9, 19, 0, 0, 0).sunday
end
end
def test_on_weekend_on_saturday
- assert date_time_init(2015,1,3,0,0,0).on_weekend?
- assert date_time_init(2015,1,3,15,15,10).on_weekend?
+ assert_predicate date_time_init(2015, 1, 3, 0, 0, 0), :on_weekend?
+ assert_predicate date_time_init(2015, 1, 3, 15, 15, 10), :on_weekend?
end
def test_on_weekend_on_sunday
- assert date_time_init(2015,1,4,0,0,0).on_weekend?
- assert date_time_init(2015,1,4,15,15,10).on_weekend?
+ assert_predicate date_time_init(2015, 1, 4, 0, 0, 0), :on_weekend?
+ assert_predicate date_time_init(2015, 1, 4, 15, 15, 10), :on_weekend?
end
def test_on_weekend_on_monday
- assert_not date_time_init(2015,1,5,0,0,0).on_weekend?
- assert_not date_time_init(2015,1,5,15,15,10).on_weekend?
+ assert_not_predicate date_time_init(2015, 1, 5, 0, 0, 0), :on_weekend?
+ assert_not_predicate date_time_init(2015, 1, 5, 15, 15, 10), :on_weekend?
+ end
+
+ def test_on_weekday_on_sunday
+ assert_not_predicate date_time_init(2015, 1, 4, 0, 0, 0), :on_weekday?
+ assert_not_predicate date_time_init(2015, 1, 4, 15, 15, 10), :on_weekday?
+ end
+
+ def test_on_weekday_on_monday
+ assert_predicate date_time_init(2015, 1, 5, 0, 0, 0), :on_weekday?
+ assert_predicate date_time_init(2015, 1, 5, 15, 15, 10), :on_weekday?
+ end
+
+ def test_before
+ assert_equal false, date_time_init(2017, 3, 6, 12, 0, 0).before?(date_time_init(2017, 3, 5, 12, 0, 0))
+ assert_equal false, date_time_init(2017, 3, 6, 12, 0, 0).before?(date_time_init(2017, 3, 6, 12, 0, 0))
+ assert_equal true, date_time_init(2017, 3, 6, 12, 0, 0).before?(date_time_init(2017, 3, 7, 12, 0, 0))
+ end
+
+ def test_after
+ assert_equal true, date_time_init(2017, 3, 6, 12, 0, 0).after?(date_time_init(2017, 3, 5, 12, 0, 0))
+ assert_equal false, date_time_init(2017, 3, 6, 12, 0, 0).after?(date_time_init(2017, 3, 6, 12, 0, 0))
+ assert_equal false, date_time_init(2017, 3, 6, 12, 0, 0).after?(date_time_init(2017, 3, 7, 12, 0, 0))
end
def with_bw_default(bw = :monday)
diff --git a/activesupport/test/core_ext/date_and_time_compatibility_test.rb b/activesupport/test/core_ext/date_and_time_compatibility_test.rb
new file mode 100644
index 0000000000..58a24b60b6
--- /dev/null
+++ b/activesupport/test/core_ext/date_and_time_compatibility_test.rb
@@ -0,0 +1,275 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/time"
+require "time_zone_test_helpers"
+
+class DateAndTimeCompatibilityTest < ActiveSupport::TestCase
+ include TimeZoneTestHelpers
+
+ def setup
+ @utc_time = Time.utc(2016, 4, 23, 14, 11, 12)
+ @date_time = DateTime.new(2016, 4, 23, 14, 11, 12, 0)
+ @utc_offset = 3600
+ @system_offset = -14400
+ @zone = ActiveSupport::TimeZone["London"]
+ end
+
+ def test_time_to_time_preserves_timezone
+ with_preserve_timezone(true) do
+ with_env_tz "US/Eastern" do
+ 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
+
+ def test_time_to_time_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)
+ 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
+
+ def test_datetime_to_time_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))
+ time = source.to_time
+
+ assert_instance_of Time, time
+ assert_equal @utc_time, time.getutc
+ assert_equal @utc_offset, time.utc_offset
+ end
+ end
+ end
+
+ def test_datetime_to_time_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))
+ 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_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
+ 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
+
+ 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 @utc_offset, time.utc_offset
+ end
+ end
+ end
+
+ def test_twz_to_time_does_not_preserve_time_zone
+ with_preserve_timezone(false) do
+ with_env_tz "US/Eastern" do
+ 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?
+
+ 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
+
+ def test_string_to_time_preserves_timezone
+ with_preserve_timezone(true) do
+ with_env_tz "US/Eastern" do
+ 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 @utc_offset, time.utc_offset
+ end
+ end
+ end
+
+ def test_string_to_time_does_not_preserve_time_zone
+ with_preserve_timezone(false) do
+ with_env_tz "US/Eastern" do
+ 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"
+ 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"
+ 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
+end
diff --git a/activesupport/test/core_ext/date_ext_test.rb b/activesupport/test/core_ext/date_ext_test.rb
index 0fc3f765f5..b8652884ce 100644
--- a/activesupport/test/core_ext/date_ext_test.rb
+++ b/activesupport/test/core_ext/date_ext_test.rb
@@ -1,22 +1,24 @@
-require 'abstract_unit'
-require 'active_support/time'
-require 'core_ext/date_and_time_behavior'
-require 'time_zone_test_helpers'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/time"
+require "core_ext/date_and_time_behavior"
+require "time_zone_test_helpers"
class DateExtCalculationsTest < ActiveSupport::TestCase
- def date_time_init(year,month,day,*args)
- Date.new(year,month,day)
+ def date_time_init(year, month, day, *args)
+ Date.new(year, month, day)
end
include DateAndTimeBehavior
include TimeZoneTestHelpers
def test_yesterday_in_calendar_reform
- assert_equal Date.new(1582,10,4), Date.new(1582,10,15).yesterday
+ assert_equal Date.new(1582, 10, 4), Date.new(1582, 10, 15).yesterday
end
def test_tomorrow_in_calendar_reform
- assert_equal Date.new(1582,10,15), Date.new(1582,10,4).tomorrow
+ assert_equal Date.new(1582, 10, 15), Date.new(1582, 10, 4).tomorrow
end
def test_to_s
@@ -30,13 +32,24 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
assert_equal "2005-02-21", date.to_s(:iso8601)
end
+ def test_to_s_with_single_digit_day
+ date = Date.new(2005, 2, 1)
+ assert_equal "2005-02-01", date.to_s
+ assert_equal "01 Feb", date.to_s(:short)
+ assert_equal "February 01, 2005", date.to_s(:long)
+ assert_equal "February 1st, 2005", date.to_s(:long_ordinal)
+ assert_equal "2005-02-01", date.to_s(:db)
+ assert_equal "01 Feb 2005", date.to_s(:rfc822)
+ assert_equal "2005-02-01", date.to_s(:iso8601)
+ end
+
def test_readable_inspect
assert_equal "Mon, 21 Feb 2005", Date.new(2005, 2, 21).readable_inspect
assert_equal Date.new(2005, 2, 21).readable_inspect, Date.new(2005, 2, 21).inspect
end
def test_to_time
- with_env_tz 'US/Eastern' do
+ with_env_tz "US/Eastern" do
assert_equal Time, Date.new(2005, 2, 21).to_time.class
assert_equal Time.local(2005, 2, 21), Date.new(2005, 2, 21).to_time
assert_equal Time.local(2005, 2, 21).utc_offset, Date.new(2005, 2, 21).to_time.utc_offset
@@ -49,6 +62,10 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
end
end
end
+
+ assert_raise(ArgumentError) do
+ Date.new(2005, 2, 21).to_time(:tokyo)
+ end
end
def test_compare_to_time
@@ -66,110 +83,102 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
end
def test_change
- assert_equal Date.new(2005, 2, 21), Date.new(2005, 2, 11).change(:day => 21)
- assert_equal Date.new(2007, 5, 11), Date.new(2005, 2, 11).change(:year => 2007, :month => 5)
- assert_equal Date.new(2006,2,22), Date.new(2005,2,22).change(:year => 2006)
- assert_equal Date.new(2005,6,22), Date.new(2005,2,22).change(:month => 6)
+ assert_equal Date.new(2005, 2, 21), Date.new(2005, 2, 11).change(day: 21)
+ assert_equal Date.new(2007, 5, 11), Date.new(2005, 2, 11).change(year: 2007, month: 5)
+ assert_equal Date.new(2006, 2, 22), Date.new(2005, 2, 22).change(year: 2006)
+ assert_equal Date.new(2005, 6, 22), Date.new(2005, 2, 22).change(month: 6)
end
def test_sunday
- assert_equal Date.new(2008,3,2), Date.new(2008,3,02).sunday
- assert_equal Date.new(2008,3,2), Date.new(2008,2,29).sunday
+ assert_equal Date.new(2008, 3, 2), Date.new(2008, 3, 02).sunday
+ assert_equal Date.new(2008, 3, 2), Date.new(2008, 2, 29).sunday
end
def test_beginning_of_week_in_calendar_reform
- assert_equal Date.new(1582,10,1), Date.new(1582,10,15).beginning_of_week #friday
+ assert_equal Date.new(1582, 10, 1), Date.new(1582, 10, 15).beginning_of_week # friday
end
def test_end_of_week_in_calendar_reform
- assert_equal Date.new(1582,10,17), Date.new(1582,10,4).end_of_week #thursday
+ assert_equal Date.new(1582, 10, 17), Date.new(1582, 10, 4).end_of_week # thursday
end
def test_end_of_year
- assert_equal Date.new(2008,12,31).to_s, Date.new(2008,2,22).end_of_year.to_s
+ assert_equal Date.new(2008, 12, 31).to_s, Date.new(2008, 2, 22).end_of_year.to_s
end
def test_end_of_month
- assert_equal Date.new(2005,3,31), Date.new(2005,3,20).end_of_month
- assert_equal Date.new(2005,2,28), Date.new(2005,2,20).end_of_month
- assert_equal Date.new(2005,4,30), Date.new(2005,4,20).end_of_month
+ assert_equal Date.new(2005, 3, 31), Date.new(2005, 3, 20).end_of_month
+ assert_equal Date.new(2005, 2, 28), Date.new(2005, 2, 20).end_of_month
+ assert_equal Date.new(2005, 4, 30), Date.new(2005, 4, 20).end_of_month
end
def test_prev_year_in_leap_years
- assert_equal Date.new(1999,2,28), Date.new(2000,2,29).prev_year
+ assert_equal Date.new(1999, 2, 28), Date.new(2000, 2, 29).prev_year
end
def test_prev_year_in_calendar_reform
- assert_equal Date.new(1582,10,4), Date.new(1583,10,14).prev_year
- end
-
- def test_last_year
- assert_equal Date.new(2004,6,5), Date.new(2005,6,5).last_year
+ assert_equal Date.new(1582, 10, 4), Date.new(1583, 10, 14).prev_year
end
def test_last_year_in_leap_years
- assert_equal Date.new(1999,2,28), Date.new(2000,2,29).last_year
+ assert_equal Date.new(1999, 2, 28), Date.new(2000, 2, 29).last_year
end
def test_last_year_in_calendar_reform
- assert_equal Date.new(1582,10,4), Date.new(1583,10,14).last_year
+ assert_equal Date.new(1582, 10, 4), Date.new(1583, 10, 14).last_year
end
def test_next_year_in_leap_years
- assert_equal Date.new(2001,2,28), Date.new(2000,2,29).next_year
+ assert_equal Date.new(2001, 2, 28), Date.new(2000, 2, 29).next_year
end
def test_next_year_in_calendar_reform
- assert_equal Date.new(1582,10,4), Date.new(1581,10,10).next_year
+ assert_equal Date.new(1582, 10, 4), Date.new(1581, 10, 10).next_year
end
def test_advance
- assert_equal Date.new(2006,2,28), Date.new(2005,2,28).advance(:years => 1)
- assert_equal Date.new(2005,6,28), Date.new(2005,2,28).advance(:months => 4)
- assert_equal Date.new(2005,3,21), Date.new(2005,2,28).advance(:weeks => 3)
- assert_equal Date.new(2005,3,5), Date.new(2005,2,28).advance(:days => 5)
- assert_equal Date.new(2012,9,28), Date.new(2005,2,28).advance(:years => 7, :months => 7)
- assert_equal Date.new(2013,10,3), Date.new(2005,2,28).advance(:years => 7, :months => 19, :days => 5)
- assert_equal Date.new(2013,10,17), Date.new(2005,2,28).advance(:years => 7, :months => 19, :weeks => 2, :days => 5)
- assert_equal Date.new(2005,2,28), Date.new(2004,2,29).advance(:years => 1) #leap day plus one year
+ assert_equal Date.new(2006, 2, 28), Date.new(2005, 2, 28).advance(years: 1)
+ assert_equal Date.new(2005, 6, 28), Date.new(2005, 2, 28).advance(months: 4)
+ assert_equal Date.new(2005, 3, 21), Date.new(2005, 2, 28).advance(weeks: 3)
+ assert_equal Date.new(2005, 3, 5), Date.new(2005, 2, 28).advance(days: 5)
+ assert_equal Date.new(2012, 9, 28), Date.new(2005, 2, 28).advance(years: 7, months: 7)
+ assert_equal Date.new(2013, 10, 3), Date.new(2005, 2, 28).advance(years: 7, months: 19, days: 5)
+ assert_equal Date.new(2013, 10, 17), Date.new(2005, 2, 28).advance(years: 7, months: 19, weeks: 2, days: 5)
+ assert_equal Date.new(2005, 2, 28), Date.new(2004, 2, 29).advance(years: 1) # leap day plus one year
end
def test_advance_does_first_years_and_then_days
- assert_equal Date.new(2012, 2, 29), Date.new(2011, 2, 28).advance(:years => 1, :days => 1)
+ assert_equal Date.new(2012, 2, 29), Date.new(2011, 2, 28).advance(years: 1, days: 1)
# If day was done first we would jump to 2012-03-01 instead.
end
def test_advance_does_first_months_and_then_days
- assert_equal Date.new(2010, 3, 29), Date.new(2010, 2, 28).advance(:months => 1, :days => 1)
+ assert_equal Date.new(2010, 3, 29), Date.new(2010, 2, 28).advance(months: 1, days: 1)
# If day was done first we would jump to 2010-04-01 instead.
end
def test_advance_in_calendar_reform
- assert_equal Date.new(1582,10,15), Date.new(1582,10,4).advance(:days => 1)
- assert_equal Date.new(1582,10,4), Date.new(1582,10,15).advance(:days => -1)
+ assert_equal Date.new(1582, 10, 15), Date.new(1582, 10, 4).advance(days: 1)
+ assert_equal Date.new(1582, 10, 4), Date.new(1582, 10, 15).advance(days: -1)
5.upto(14) do |day|
- assert_equal Date.new(1582,10,4), Date.new(1582,9,day).advance(:months => 1)
- assert_equal Date.new(1582,10,4), Date.new(1582,11,day).advance(:months => -1)
- assert_equal Date.new(1582,10,4), Date.new(1581,10,day).advance(:years => 1)
- assert_equal Date.new(1582,10,4), Date.new(1583,10,day).advance(:years => -1)
+ assert_equal Date.new(1582, 10, 4), Date.new(1582, 9, day).advance(months: 1)
+ assert_equal Date.new(1582, 10, 4), Date.new(1582, 11, day).advance(months: -1)
+ assert_equal Date.new(1582, 10, 4), Date.new(1581, 10, day).advance(years: 1)
+ assert_equal Date.new(1582, 10, 4), Date.new(1583, 10, day).advance(years: -1)
end
end
def test_last_week
- assert_equal Date.new(2005,5,9), Date.new(2005,5,17).last_week
- assert_equal Date.new(2006,12,25), Date.new(2007,1,7).last_week
- assert_equal Date.new(2010,2,12), Date.new(2010,2,19).last_week(:friday)
- assert_equal Date.new(2010,2,13), Date.new(2010,2,19).last_week(:saturday)
- assert_equal Date.new(2010,2,27), Date.new(2010,3,4).last_week(:saturday)
+ assert_equal Date.new(2005, 5, 9), Date.new(2005, 5, 17).last_week
+ assert_equal Date.new(2006, 12, 25), Date.new(2007, 1, 7).last_week
+ assert_equal Date.new(2010, 2, 12), Date.new(2010, 2, 19).last_week(:friday)
+ assert_equal Date.new(2010, 2, 13), Date.new(2010, 2, 19).last_week(:saturday)
+ assert_equal Date.new(2010, 2, 27), Date.new(2010, 3, 4).last_week(:saturday)
end
def test_next_week_in_calendar_reform
- assert_equal Date.new(1582,10,15), Date.new(1582,9,30).next_week(:friday)
- assert_equal Date.new(1582,10,18), Date.new(1582,10,4).next_week
- end
-
- def test_last_month_on_31st
- assert_equal Date.new(2004, 2, 29), Date.new(2004, 3, 31).last_month
+ assert_equal Date.new(1582, 10, 15), Date.new(1582, 9, 30).next_week(:friday)
+ assert_equal Date.new(1582, 10, 18), Date.new(1582, 10, 4).next_week
end
def test_last_quarter_on_31st
@@ -181,7 +190,7 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
end
def test_yesterday_constructor_when_zone_is_not_set
- with_env_tz 'UTC' do
+ with_env_tz "UTC" do
with_tz_default do
assert_equal(Date.today - 1, Date.yesterday)
end
@@ -189,8 +198,8 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
end
def test_yesterday_constructor_when_zone_is_set
- with_env_tz 'UTC' do
- with_tz_default ActiveSupport::TimeZone['Eastern Time (US & Canada)'] do # UTC -5
+ with_env_tz "UTC" do
+ with_tz_default ActiveSupport::TimeZone["Eastern Time (US & Canada)"] do # UTC -5
Time.stub(:now, Time.local(2000, 1, 1)) do
assert_equal Date.new(1999, 12, 30), Date.yesterday
end
@@ -203,7 +212,7 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
end
def test_tomorrow_constructor_when_zone_is_not_set
- with_env_tz 'UTC' do
+ with_env_tz "UTC" do
with_tz_default do
assert_equal(Date.today + 1, Date.tomorrow)
end
@@ -211,8 +220,8 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
end
def test_tomorrow_constructor_when_zone_is_set
- with_env_tz 'UTC' do
- with_tz_default ActiveSupport::TimeZone['Europe/Paris'] do # UTC +1
+ with_env_tz "UTC" do
+ with_tz_default ActiveSupport::TimeZone["Europe/Paris"] do # UTC +1
Time.stub(:now, Time.local(1999, 12, 31, 23)) do
assert_equal Date.new(2000, 1, 2), Date.tomorrow
end
@@ -221,84 +230,101 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
end
def test_since
- assert_equal Time.local(2005,2,21,0,0,45), Date.new(2005,2,21).since(45)
+ assert_equal Time.local(2005, 2, 21, 0, 0, 45), Date.new(2005, 2, 21).since(45)
end
def test_since_when_zone_is_set
- zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
- with_env_tz 'UTC' do
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ with_env_tz "UTC" do
with_tz_default zone do
- assert_equal zone.local(2005,2,21,0,0,45), Date.new(2005,2,21).since(45)
- assert_equal zone, Date.new(2005,2,21).since(45).time_zone
+ assert_equal zone.local(2005, 2, 21, 0, 0, 45), Date.new(2005, 2, 21).since(45)
+ assert_equal zone, Date.new(2005, 2, 21).since(45).time_zone
end
end
end
def test_ago
- assert_equal Time.local(2005,2,20,23,59,15), Date.new(2005,2,21).ago(45)
+ assert_equal Time.local(2005, 2, 20, 23, 59, 15), Date.new(2005, 2, 21).ago(45)
end
def test_ago_when_zone_is_set
- zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
- with_env_tz 'UTC' do
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ with_env_tz "UTC" do
with_tz_default zone do
- assert_equal zone.local(2005,2,20,23,59,15), Date.new(2005,2,21).ago(45)
- assert_equal zone, Date.new(2005,2,21).ago(45).time_zone
+ assert_equal zone.local(2005, 2, 20, 23, 59, 15), Date.new(2005, 2, 21).ago(45)
+ assert_equal zone, Date.new(2005, 2, 21).ago(45).time_zone
end
end
end
def test_beginning_of_day
- assert_equal Time.local(2005,2,21,0,0,0), Date.new(2005,2,21).beginning_of_day
+ assert_equal Time.local(2005, 2, 21, 0, 0, 0), Date.new(2005, 2, 21).beginning_of_day
end
def test_middle_of_day
- assert_equal Time.local(2005,2,21,12,0,0), Date.new(2005,2,21).middle_of_day
+ assert_equal Time.local(2005, 2, 21, 12, 0, 0), Date.new(2005, 2, 21).middle_of_day
end
def test_beginning_of_day_when_zone_is_set
- zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
- with_env_tz 'UTC' do
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ with_env_tz "UTC" do
with_tz_default zone do
- assert_equal zone.local(2005,2,21,0,0,0), Date.new(2005,2,21).beginning_of_day
- assert_equal zone, Date.new(2005,2,21).beginning_of_day.time_zone
+ assert_equal zone.local(2005, 2, 21, 0, 0, 0), Date.new(2005, 2, 21).beginning_of_day
+ assert_equal zone, Date.new(2005, 2, 21).beginning_of_day.time_zone
end
end
end
def test_end_of_day
- assert_equal Time.local(2005,2,21,23,59,59,Rational(999999999, 1000)), Date.new(2005,2,21).end_of_day
+ assert_equal Time.local(2005, 2, 21, 23, 59, 59, Rational(999999999, 1000)), Date.new(2005, 2, 21).end_of_day
end
def test_end_of_day_when_zone_is_set
- zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
- with_env_tz 'UTC' do
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ with_env_tz "UTC" do
with_tz_default zone do
- assert_equal zone.local(2005,2,21,23,59,59,Rational(999999999, 1000)), Date.new(2005,2,21).end_of_day
- assert_equal zone, Date.new(2005,2,21).end_of_day.time_zone
+ assert_equal zone.local(2005, 2, 21, 23, 59, 59, Rational(999999999, 1000)), Date.new(2005, 2, 21).end_of_day
+ assert_equal zone, Date.new(2005, 2, 21).end_of_day.time_zone
+ end
+ end
+ end
+
+ def test_all_day
+ beginning_of_day = Time.local(2011, 6, 7, 0, 0, 0)
+ end_of_day = Time.local(2011, 6, 7, 23, 59, 59, Rational(999999999, 1000))
+ assert_equal beginning_of_day..end_of_day, Date.new(2011, 6, 7).all_day
+ end
+
+ def test_all_day_when_zone_is_set
+ zone = ActiveSupport::TimeZone["Hawaii"]
+ with_env_tz "UTC" do
+ with_tz_default zone do
+ beginning_of_day = zone.local(2011, 6, 7, 0, 0, 0)
+ end_of_day = zone.local(2011, 6, 7, 23, 59, 59, Rational(999999999, 1000))
+ assert_equal beginning_of_day..end_of_day, Date.new(2011, 6, 7).all_day
end
end
end
def test_all_week
- assert_equal Date.new(2011,6,6)..Date.new(2011,6,12), Date.new(2011,6,7).all_week
- assert_equal Date.new(2011,6,5)..Date.new(2011,6,11), Date.new(2011,6,7).all_week(:sunday)
+ assert_equal Date.new(2011, 6, 6)..Date.new(2011, 6, 12), Date.new(2011, 6, 7).all_week
+ assert_equal Date.new(2011, 6, 5)..Date.new(2011, 6, 11), Date.new(2011, 6, 7).all_week(:sunday)
end
def test_all_month
- assert_equal Date.new(2011,6,1)..Date.new(2011,6,30), Date.new(2011,6,7).all_month
+ assert_equal Date.new(2011, 6, 1)..Date.new(2011, 6, 30), Date.new(2011, 6, 7).all_month
end
def test_all_quarter
- assert_equal Date.new(2011,4,1)..Date.new(2011,6,30), Date.new(2011,6,7).all_quarter
+ assert_equal Date.new(2011, 4, 1)..Date.new(2011, 6, 30), Date.new(2011, 6, 7).all_quarter
end
def test_all_year
- assert_equal Date.new(2011,1,1)..Date.new(2011,12,31), Date.new(2011,6,7).all_year
+ assert_equal Date.new(2011, 1, 1)..Date.new(2011, 12, 31), Date.new(2011, 6, 7).all_year
end
def test_xmlschema
- with_env_tz 'US/Eastern' do
+ with_env_tz "US/Eastern" do
assert_match(/^1980-02-28T00:00:00-05:?00$/, Date.new(1980, 2, 28).xmlschema)
assert_match(/^1980-06-28T00:00:00-04:?00$/, Date.new(1980, 6, 28).xmlschema)
# these tests are only of interest on platforms where older dates #to_time fail over to DateTime
@@ -310,8 +336,8 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
end
def test_xmlschema_when_zone_is_set
- with_env_tz 'UTC' do
- with_tz_default ActiveSupport::TimeZone['Eastern Time (US & Canada)'] do # UTC -5
+ with_env_tz "UTC" do
+ with_tz_default ActiveSupport::TimeZone["Eastern Time (US & Canada)"] do # UTC -5
assert_match(/^1980-02-28T00:00:00-05:?00$/, Date.new(1980, 2, 28).xmlschema)
assert_match(/^1980-06-28T00:00:00-04:?00$/, Date.new(1980, 6, 28).xmlschema)
end
@@ -321,21 +347,21 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
def test_past
Date.stub(:current, Date.new(2000, 1, 1)) do
assert_equal true, Date.new(1999, 12, 31).past?
- assert_equal false, Date.new(2000,1,1).past?
- assert_equal false, Date.new(2000,1,2).past?
+ assert_equal false, Date.new(2000, 1, 1).past?
+ assert_equal false, Date.new(2000, 1, 2).past?
end
end
def test_future
Date.stub(:current, Date.new(2000, 1, 1)) do
assert_equal false, Date.new(1999, 12, 31).future?
- assert_equal false, Date.new(2000,1,1).future?
- assert_equal true, Date.new(2000,1,2).future?
+ assert_equal false, Date.new(2000, 1, 1).future?
+ assert_equal true, Date.new(2000, 1, 2).future?
end
end
def test_current_returns_date_today_when_zone_not_set
- with_env_tz 'US/Central' do
+ with_env_tz "US/Central" do
Time.stub(:now, Time.local(1999, 12, 31, 23)) do
assert_equal Date.today, Date.current
end
@@ -343,8 +369,8 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
end
def test_current_returns_time_zone_today_when_zone_is_set
- Time.zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
- with_env_tz 'US/Central' do
+ Time.zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ with_env_tz "US/Central" do
assert_equal ::Time.zone.today, Date.current
end
ensure
@@ -352,15 +378,19 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
end
def test_date_advance_should_not_change_passed_options_hash
- options = { :years => 3, :months => 11, :days => 2 }
- Date.new(2005,2,28).advance(options)
- assert_equal({ :years => 3, :months => 11, :days => 2 }, options)
+ options = { years: 3, months: 11, days: 2 }
+ Date.new(2005, 2, 28).advance(options)
+ assert_equal({ years: 3, months: 11, days: 2 }, options)
end
end
class DateExtBehaviorTest < ActiveSupport::TestCase
def test_date_acts_like_date
- assert Date.new.acts_like_date?
+ assert_predicate Date.new, :acts_like_date?
+ end
+
+ def test_blank?
+ assert_not_predicate Date.new, :blank?
end
def test_freeze_doesnt_clobber_memoized_instance_methods
@@ -375,4 +405,3 @@ class DateExtBehaviorTest < ActiveSupport::TestCase
end
end
end
-
diff --git a/activesupport/test/core_ext/date_time_ext_test.rb b/activesupport/test/core_ext/date_time_ext_test.rb
index 6fe38c45ec..f9f6b21c9b 100644
--- a/activesupport/test/core_ext/date_time_ext_test.rb
+++ b/activesupport/test/core_ext/date_time_ext_test.rb
@@ -1,11 +1,13 @@
-require 'abstract_unit'
-require 'active_support/time'
-require 'core_ext/date_and_time_behavior'
-require 'time_zone_test_helpers'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/time"
+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
@@ -35,11 +37,29 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
end
def test_custom_date_format
- Time::DATE_FORMATS[:custom] = '%Y%m%d%H%M%S'
- assert_equal '20050221143000', DateTime.new(2005, 2, 21, 14, 30, 0).to_s(:custom)
+ Time::DATE_FORMATS[:custom] = "%Y%m%d%H%M%S"
+ assert_equal "20050221143000", DateTime.new(2005, 2, 21, 14, 30, 0).to_s(:custom)
Time::DATE_FORMATS.delete(:custom)
end
+ def test_localtime
+ with_env_tz "US/Eastern" do
+ assert_instance_of Time, DateTime.new(2016, 3, 11, 15, 11, 12, 0).localtime
+ assert_equal Time.local(2016, 3, 11, 10, 11, 12), DateTime.new(2016, 3, 11, 15, 11, 12, 0).localtime
+ assert_equal Time.local(2016, 3, 21, 11, 11, 12), DateTime.new(2016, 3, 21, 15, 11, 12, 0).localtime
+ assert_equal Time.local(2016, 4, 1, 11, 11, 12), DateTime.new(2016, 4, 1, 16, 11, 12, Rational(1, 24)).localtime
+ end
+ end
+
+ def test_getlocal
+ with_env_tz "US/Eastern" do
+ assert_instance_of Time, DateTime.new(2016, 3, 11, 15, 11, 12, 0).getlocal
+ assert_equal Time.local(2016, 3, 11, 10, 11, 12), DateTime.new(2016, 3, 11, 15, 11, 12, 0).getlocal
+ assert_equal Time.local(2016, 3, 21, 11, 11, 12), DateTime.new(2016, 3, 21, 15, 11, 12, 0).getlocal
+ assert_equal Time.local(2016, 4, 1, 11, 11, 12), DateTime.new(2016, 4, 1, 16, 11, 12, Rational(1, 24)).getlocal
+ end
+ end
+
def test_to_date
assert_equal Date.new(2005, 2, 21), DateTime.new(2005, 2, 21, 14, 30, 0).to_date
end
@@ -49,10 +69,16 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
end
def test_to_time
- with_env_tz 'US/Eastern' do
- assert_equal Time, DateTime.new(2005, 2, 21, 10, 11, 12, 0).to_time.class
- assert_equal Time.local(2005, 2, 21, 5, 11, 12), DateTime.new(2005, 2, 21, 10, 11, 12, 0).to_time
- assert_equal Time.local(2005, 2, 21, 5, 11, 12).utc_offset, DateTime.new(2005, 2, 21, 10, 11, 12, 0).to_time.utc_offset
+ with_env_tz "US/Eastern" do
+ assert_instance_of Time, DateTime.new(2005, 2, 21, 10, 11, 12, 0).to_time
+
+ if ActiveSupport.to_time_preserves_timezone
+ assert_equal Time.local(2005, 2, 21, 5, 11, 12).getlocal(0), DateTime.new(2005, 2, 21, 10, 11, 12, 0).to_time
+ assert_equal Time.local(2005, 2, 21, 5, 11, 12).getlocal(0).utc_offset, DateTime.new(2005, 2, 21, 10, 11, 12, 0).to_time.utc_offset
+ else
+ assert_equal Time.local(2005, 2, 21, 5, 11, 12), DateTime.new(2005, 2, 21, 10, 11, 12, 0).to_time
+ assert_equal Time.local(2005, 2, 21, 5, 11, 12).utc_offset, DateTime.new(2005, 2, 21, 10, 11, 12, 0).to_time.utc_offset
+ end
end
end
@@ -66,128 +92,134 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
end
def test_seconds_since_midnight
- assert_equal 1,DateTime.civil(2005,1,1,0,0,1).seconds_since_midnight
- assert_equal 60,DateTime.civil(2005,1,1,0,1,0).seconds_since_midnight
- assert_equal 3660,DateTime.civil(2005,1,1,1,1,0).seconds_since_midnight
- assert_equal 86399,DateTime.civil(2005,1,1,23,59,59).seconds_since_midnight
+ assert_equal 1, DateTime.civil(2005, 1, 1, 0, 0, 1).seconds_since_midnight
+ assert_equal 60, DateTime.civil(2005, 1, 1, 0, 1, 0).seconds_since_midnight
+ assert_equal 3660, DateTime.civil(2005, 1, 1, 1, 1, 0).seconds_since_midnight
+ assert_equal 86399, DateTime.civil(2005, 1, 1, 23, 59, 59).seconds_since_midnight
end
def test_seconds_until_end_of_day
- assert_equal 0, DateTime.civil(2005,1,1,23,59,59).seconds_until_end_of_day
- assert_equal 1, DateTime.civil(2005,1,1,23,59,58).seconds_until_end_of_day
- assert_equal 60, DateTime.civil(2005,1,1,23,58,59).seconds_until_end_of_day
- assert_equal 3660, DateTime.civil(2005,1,1,22,58,59).seconds_until_end_of_day
- assert_equal 86399, DateTime.civil(2005,1,1,0,0,0).seconds_until_end_of_day
+ assert_equal 0, DateTime.civil(2005, 1, 1, 23, 59, 59).seconds_until_end_of_day
+ assert_equal 1, DateTime.civil(2005, 1, 1, 23, 59, 58).seconds_until_end_of_day
+ assert_equal 60, DateTime.civil(2005, 1, 1, 23, 58, 59).seconds_until_end_of_day
+ assert_equal 3660, DateTime.civil(2005, 1, 1, 22, 58, 59).seconds_until_end_of_day
+ assert_equal 86399, DateTime.civil(2005, 1, 1, 0, 0, 0).seconds_until_end_of_day
end
def test_beginning_of_day
- assert_equal DateTime.civil(2005,2,4,0,0,0), DateTime.civil(2005,2,4,10,10,10).beginning_of_day
+ assert_equal DateTime.civil(2005, 2, 4, 0, 0, 0), DateTime.civil(2005, 2, 4, 10, 10, 10).beginning_of_day
end
def test_middle_of_day
- assert_equal DateTime.civil(2005,2,4,12,0,0), DateTime.civil(2005,2,4,10,10,10).middle_of_day
+ assert_equal DateTime.civil(2005, 2, 4, 12, 0, 0), DateTime.civil(2005, 2, 4, 10, 10, 10).middle_of_day
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
- assert_equal DateTime.civil(2005,2,4,19,0,0), DateTime.civil(2005,2,4,19,30,10).beginning_of_hour
+ assert_equal DateTime.civil(2005, 2, 4, 19, 0, 0), DateTime.civil(2005, 2, 4, 19, 30, 10).beginning_of_hour
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
- assert_equal DateTime.civil(2005,2,4,19,30,0), DateTime.civil(2005,2,4,19,30,10).beginning_of_minute
+ assert_equal DateTime.civil(2005, 2, 4, 19, 30, 0), DateTime.civil(2005, 2, 4, 19, 30, 10).beginning_of_minute
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
- end
-
- def test_last_year
- assert_equal DateTime.civil(2004,6,5,10), DateTime.civil(2005,6,5,10,0,0).last_year
+ 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_ago
- assert_equal DateTime.civil(2005,2,22,10,10,9), DateTime.civil(2005,2,22,10,10,10).ago(1)
- assert_equal DateTime.civil(2005,2,22,9,10,10), DateTime.civil(2005,2,22,10,10,10).ago(3600)
- assert_equal DateTime.civil(2005,2,20,10,10,10), DateTime.civil(2005,2,22,10,10,10).ago(86400*2)
- assert_equal DateTime.civil(2005,2,20,9,9,45), DateTime.civil(2005,2,22,10,10,10).ago(86400*2 + 3600 + 25)
+ assert_equal DateTime.civil(2005, 2, 22, 10, 10, 9), DateTime.civil(2005, 2, 22, 10, 10, 10).ago(1)
+ assert_equal DateTime.civil(2005, 2, 22, 9, 10, 10), DateTime.civil(2005, 2, 22, 10, 10, 10).ago(3600)
+ assert_equal DateTime.civil(2005, 2, 20, 10, 10, 10), DateTime.civil(2005, 2, 22, 10, 10, 10).ago(86400 * 2)
+ assert_equal DateTime.civil(2005, 2, 20, 9, 9, 45), DateTime.civil(2005, 2, 22, 10, 10, 10).ago(86400 * 2 + 3600 + 25)
end
def test_since
- assert_equal DateTime.civil(2005,2,22,10,10,11), DateTime.civil(2005,2,22,10,10,10).since(1)
- assert_equal DateTime.civil(2005,2,22,11,10,10), DateTime.civil(2005,2,22,10,10,10).since(3600)
- assert_equal DateTime.civil(2005,2,24,10,10,10), DateTime.civil(2005,2,22,10,10,10).since(86400*2)
- assert_equal DateTime.civil(2005,2,24,11,10,35), DateTime.civil(2005,2,22,10,10,10).since(86400*2 + 3600 + 25)
- assert_equal DateTime.civil(2005,2,22,10,10,11), DateTime.civil(2005,2,22,10,10,10).since(1.333)
- assert_equal DateTime.civil(2005,2,22,10,10,12), DateTime.civil(2005,2,22,10,10,10).since(1.667)
+ assert_equal DateTime.civil(2005, 2, 22, 10, 10, 11), DateTime.civil(2005, 2, 22, 10, 10, 10).since(1)
+ assert_equal DateTime.civil(2005, 2, 22, 11, 10, 10), DateTime.civil(2005, 2, 22, 10, 10, 10).since(3600)
+ assert_equal DateTime.civil(2005, 2, 24, 10, 10, 10), DateTime.civil(2005, 2, 22, 10, 10, 10).since(86400 * 2)
+ assert_equal DateTime.civil(2005, 2, 24, 11, 10, 35), DateTime.civil(2005, 2, 22, 10, 10, 10).since(86400 * 2 + 3600 + 25)
+ assert_not_equal DateTime.civil(2005, 2, 22, 10, 10, 11), DateTime.civil(2005, 2, 22, 10, 10, 10).since(1.333)
+ assert_not_equal DateTime.civil(2005, 2, 22, 10, 10, 12), DateTime.civil(2005, 2, 22, 10, 10, 10).since(1.667)
end
def test_change
- 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(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)
+
+ # datetime with non-zero offset
+ assert_equal DateTime.civil(2005, 2, 22, 15, 15, 10, Rational(-5, 24)), DateTime.civil(2005, 2, 22, 15, 15, 10, 0).change(offset: Rational(-5, 24))
# 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, 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
- assert_equal DateTime.civil(2006,2,28,15,15,10), DateTime.civil(2005,2,28,15,15,10).advance(:years => 1)
- assert_equal DateTime.civil(2005,6,28,15,15,10), DateTime.civil(2005,2,28,15,15,10).advance(:months => 4)
- assert_equal DateTime.civil(2005,3,21,15,15,10), DateTime.civil(2005,2,28,15,15,10).advance(:weeks => 3)
- assert_equal DateTime.civil(2005,3,5,15,15,10), DateTime.civil(2005,2,28,15,15,10).advance(:days => 5)
- assert_equal DateTime.civil(2012,9,28,15,15,10), DateTime.civil(2005,2,28,15,15,10).advance(:years => 7, :months => 7)
- assert_equal DateTime.civil(2013,10,3,15,15,10), DateTime.civil(2005,2,28,15,15,10).advance(:years => 7, :months => 19, :days => 5)
- assert_equal DateTime.civil(2013,10,17,15,15,10), DateTime.civil(2005,2,28,15,15,10).advance(:years => 7, :months => 19, :weeks => 2, :days => 5)
- assert_equal DateTime.civil(2001,12,27,15,15,10), DateTime.civil(2005,2,28,15,15,10).advance(:years => -3, :months => -2, :days => -1)
- assert_equal DateTime.civil(2005,2,28,15,15,10), DateTime.civil(2004,2,29,15,15,10).advance(:years => 1) #leap day plus one year
- assert_equal DateTime.civil(2005,2,28,20,15,10), DateTime.civil(2005,2,28,15,15,10).advance(:hours => 5)
- assert_equal DateTime.civil(2005,2,28,15,22,10), DateTime.civil(2005,2,28,15,15,10).advance(:minutes => 7)
- assert_equal DateTime.civil(2005,2,28,15,15,19), DateTime.civil(2005,2,28,15,15,10).advance(:seconds => 9)
- assert_equal DateTime.civil(2005,2,28,20,22,19), DateTime.civil(2005,2,28,15,15,10).advance(:hours => 5, :minutes => 7, :seconds => 9)
- assert_equal DateTime.civil(2005,2,28,10,8,1), DateTime.civil(2005,2,28,15,15,10).advance(:hours => -5, :minutes => -7, :seconds => -9)
- assert_equal DateTime.civil(2013,10,17,20,22,19), DateTime.civil(2005,2,28,15,15,10).advance(:years => 7, :months => 19, :weeks => 2, :days => 5, :hours => 5, :minutes => 7, :seconds => 9)
+ assert_equal DateTime.civil(2006, 2, 28, 15, 15, 10), DateTime.civil(2005, 2, 28, 15, 15, 10).advance(years: 1)
+ assert_equal DateTime.civil(2005, 6, 28, 15, 15, 10), DateTime.civil(2005, 2, 28, 15, 15, 10).advance(months: 4)
+ assert_equal DateTime.civil(2005, 3, 21, 15, 15, 10), DateTime.civil(2005, 2, 28, 15, 15, 10).advance(weeks: 3)
+ assert_equal DateTime.civil(2005, 3, 5, 15, 15, 10), DateTime.civil(2005, 2, 28, 15, 15, 10).advance(days: 5)
+ assert_equal DateTime.civil(2012, 9, 28, 15, 15, 10), DateTime.civil(2005, 2, 28, 15, 15, 10).advance(years: 7, months: 7)
+ assert_equal DateTime.civil(2013, 10, 3, 15, 15, 10), DateTime.civil(2005, 2, 28, 15, 15, 10).advance(years: 7, months: 19, days: 5)
+ assert_equal DateTime.civil(2013, 10, 17, 15, 15, 10), DateTime.civil(2005, 2, 28, 15, 15, 10).advance(years: 7, months: 19, weeks: 2, days: 5)
+ assert_equal DateTime.civil(2001, 12, 27, 15, 15, 10), DateTime.civil(2005, 2, 28, 15, 15, 10).advance(years: -3, months: -2, days: -1)
+ assert_equal DateTime.civil(2005, 2, 28, 15, 15, 10), DateTime.civil(2004, 2, 29, 15, 15, 10).advance(years: 1) # leap day plus one year
+ assert_equal DateTime.civil(2005, 2, 28, 20, 15, 10), DateTime.civil(2005, 2, 28, 15, 15, 10).advance(hours: 5)
+ assert_equal DateTime.civil(2005, 2, 28, 15, 22, 10), DateTime.civil(2005, 2, 28, 15, 15, 10).advance(minutes: 7)
+ assert_equal DateTime.civil(2005, 2, 28, 15, 15, 19), DateTime.civil(2005, 2, 28, 15, 15, 10).advance(seconds: 9)
+ assert_equal DateTime.civil(2005, 2, 28, 20, 22, 19), DateTime.civil(2005, 2, 28, 15, 15, 10).advance(hours: 5, minutes: 7, seconds: 9)
+ assert_equal DateTime.civil(2005, 2, 28, 10, 8, 1), DateTime.civil(2005, 2, 28, 15, 15, 10).advance(hours: -5, minutes: -7, seconds: -9)
+ assert_equal DateTime.civil(2013, 10, 17, 20, 22, 19), DateTime.civil(2005, 2, 28, 15, 15, 10).advance(years: 7, months: 19, weeks: 2, days: 5, hours: 5, minutes: 7, seconds: 9)
end
def test_advance_partial_days
- assert_equal DateTime.civil(2012,9,29,13,15,10), DateTime.civil(2012,9,28,1,15,10).advance(:days => 1.5)
- assert_equal DateTime.civil(2012,9,28,13,15,10), DateTime.civil(2012,9,28,1,15,10).advance(:days => 0.5)
- assert_equal DateTime.civil(2012,10,29,13,15,10), DateTime.civil(2012,9,28,1,15,10).advance(:days => 1.5, :months => 1)
+ assert_equal DateTime.civil(2012, 9, 29, 13, 15, 10), DateTime.civil(2012, 9, 28, 1, 15, 10).advance(days: 1.5)
+ assert_equal DateTime.civil(2012, 9, 28, 13, 15, 10), DateTime.civil(2012, 9, 28, 1, 15, 10).advance(days: 0.5)
+ assert_equal DateTime.civil(2012, 10, 29, 13, 15, 10), DateTime.civil(2012, 9, 28, 1, 15, 10).advance(days: 1.5, months: 1)
end
def test_advanced_processes_first_the_date_deltas_and_then_the_time_deltas
# If the time deltas were processed first, the following datetimes would be advanced to 2010/04/01 instead.
- assert_equal DateTime.civil(2010, 3, 29), DateTime.civil(2010, 2, 28, 23, 59, 59).advance(:months => 1, :seconds => 1)
- assert_equal DateTime.civil(2010, 3, 29), DateTime.civil(2010, 2, 28, 23, 59).advance(:months => 1, :minutes => 1)
- assert_equal DateTime.civil(2010, 3, 29), DateTime.civil(2010, 2, 28, 23).advance(:months => 1, :hours => 1)
- assert_equal DateTime.civil(2010, 3, 29), DateTime.civil(2010, 2, 28, 22, 58, 59).advance(:months => 1, :hours => 1, :minutes => 1, :seconds => 1)
+ assert_equal DateTime.civil(2010, 3, 29), DateTime.civil(2010, 2, 28, 23, 59, 59).advance(months: 1, seconds: 1)
+ assert_equal DateTime.civil(2010, 3, 29), DateTime.civil(2010, 2, 28, 23, 59).advance(months: 1, minutes: 1)
+ assert_equal DateTime.civil(2010, 3, 29), DateTime.civil(2010, 2, 28, 23).advance(months: 1, hours: 1)
+ assert_equal DateTime.civil(2010, 3, 29), DateTime.civil(2010, 2, 28, 22, 58, 59).advance(months: 1, hours: 1, minutes: 1, seconds: 1)
end
def test_last_week
- assert_equal DateTime.civil(2005,2,21), DateTime.civil(2005,3,1,15,15,10).last_week
- assert_equal DateTime.civil(2005,2,22), DateTime.civil(2005,3,1,15,15,10).last_week(:tuesday)
- assert_equal DateTime.civil(2005,2,25), DateTime.civil(2005,3,1,15,15,10).last_week(:friday)
- assert_equal DateTime.civil(2006,10,30), DateTime.civil(2006,11,6,0,0,0).last_week
- assert_equal DateTime.civil(2006,11,15), DateTime.civil(2006,11,23,0,0,0).last_week(:wednesday)
+ assert_equal DateTime.civil(2005, 2, 21), DateTime.civil(2005, 3, 1, 15, 15, 10).last_week
+ assert_equal DateTime.civil(2005, 2, 22), DateTime.civil(2005, 3, 1, 15, 15, 10).last_week(:tuesday)
+ assert_equal DateTime.civil(2005, 2, 25), DateTime.civil(2005, 3, 1, 15, 15, 10).last_week(:friday)
+ assert_equal DateTime.civil(2006, 10, 30), DateTime.civil(2006, 11, 6, 0, 0, 0).last_week
+ assert_equal DateTime.civil(2006, 11, 15), DateTime.civil(2006, 11, 23, 0, 0, 0).last_week(:wednesday)
end
- def test_last_month_on_31st
- assert_equal DateTime.civil(2004, 2, 29), DateTime.civil(2004, 3, 31).last_month
+ def test_date_time_should_have_correct_last_week_for_leap_year
+ assert_equal DateTime.civil(2016, 2, 29), DateTime.civil(2016, 3, 7).last_week
end
def test_last_quarter_on_31st
@@ -205,56 +237,56 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
def test_today_with_offset
Date.stub(:current, Date.new(2000, 1, 1)) do
- assert_equal false, DateTime.civil(1999,12,31,23,59,59, Rational(-18000, 86400)).today?
- assert_equal true, DateTime.civil(2000,1,1,0,0,0, Rational(-18000, 86400)).today?
- assert_equal true, DateTime.civil(2000,1,1,23,59,59, Rational(-18000, 86400)).today?
- assert_equal false, DateTime.civil(2000,1,2,0,0,0, Rational(-18000, 86400)).today?
+ assert_equal false, DateTime.civil(1999, 12, 31, 23, 59, 59, Rational(-18000, 86400)).today?
+ assert_equal true, DateTime.civil(2000, 1, 1, 0, 0, 0, Rational(-18000, 86400)).today?
+ assert_equal true, DateTime.civil(2000, 1, 1, 23, 59, 59, Rational(-18000, 86400)).today?
+ assert_equal false, DateTime.civil(2000, 1, 2, 0, 0, 0, Rational(-18000, 86400)).today?
end
end
def test_today_without_offset
Date.stub(:current, Date.new(2000, 1, 1)) do
- assert_equal false, DateTime.civil(1999,12,31,23,59,59).today?
- assert_equal true, DateTime.civil(2000,1,1,0).today?
- assert_equal true, DateTime.civil(2000,1,1,23,59,59).today?
- assert_equal false, DateTime.civil(2000,1,2,0).today?
+ assert_equal false, DateTime.civil(1999, 12, 31, 23, 59, 59).today?
+ assert_equal true, DateTime.civil(2000, 1, 1, 0).today?
+ assert_equal true, DateTime.civil(2000, 1, 1, 23, 59, 59).today?
+ assert_equal false, DateTime.civil(2000, 1, 2, 0).today?
end
end
def test_past_with_offset
- DateTime.stub(:current, DateTime.civil(2005,2,10,15,30,45, Rational(-18000, 86400))) do
- assert_equal true, DateTime.civil(2005,2,10,15,30,44, Rational(-18000, 86400)).past?
- assert_equal false, DateTime.civil(2005,2,10,15,30,45, Rational(-18000, 86400)).past?
- assert_equal false, DateTime.civil(2005,2,10,15,30,46, Rational(-18000, 86400)).past?
+ DateTime.stub(:current, DateTime.civil(2005, 2, 10, 15, 30, 45, Rational(-18000, 86400))) do
+ assert_equal true, DateTime.civil(2005, 2, 10, 15, 30, 44, Rational(-18000, 86400)).past?
+ assert_equal false, DateTime.civil(2005, 2, 10, 15, 30, 45, Rational(-18000, 86400)).past?
+ assert_equal false, DateTime.civil(2005, 2, 10, 15, 30, 46, Rational(-18000, 86400)).past?
end
end
def test_past_without_offset
- DateTime.stub(:current, DateTime.civil(2005,2,10,15,30,45, Rational(-18000, 86400))) do
- assert_equal true, DateTime.civil(2005,2,10,20,30,44).past?
- assert_equal false, DateTime.civil(2005,2,10,20,30,45).past?
- assert_equal false, DateTime.civil(2005,2,10,20,30,46).past?
+ DateTime.stub(:current, DateTime.civil(2005, 2, 10, 15, 30, 45, Rational(-18000, 86400))) do
+ assert_equal true, DateTime.civil(2005, 2, 10, 20, 30, 44).past?
+ assert_equal false, DateTime.civil(2005, 2, 10, 20, 30, 45).past?
+ assert_equal false, DateTime.civil(2005, 2, 10, 20, 30, 46).past?
end
end
def test_future_with_offset
- DateTime.stub(:current, DateTime.civil(2005,2,10,15,30,45, Rational(-18000, 86400))) do
- assert_equal false, DateTime.civil(2005,2,10,15,30,44, Rational(-18000, 86400)).future?
- assert_equal false, DateTime.civil(2005,2,10,15,30,45, Rational(-18000, 86400)).future?
- assert_equal true, DateTime.civil(2005,2,10,15,30,46, Rational(-18000, 86400)).future?
+ DateTime.stub(:current, DateTime.civil(2005, 2, 10, 15, 30, 45, Rational(-18000, 86400))) do
+ assert_equal false, DateTime.civil(2005, 2, 10, 15, 30, 44, Rational(-18000, 86400)).future?
+ assert_equal false, DateTime.civil(2005, 2, 10, 15, 30, 45, Rational(-18000, 86400)).future?
+ assert_equal true, DateTime.civil(2005, 2, 10, 15, 30, 46, Rational(-18000, 86400)).future?
end
end
def test_future_without_offset
- DateTime.stub(:current, DateTime.civil(2005,2,10,15,30,45, Rational(-18000, 86400))) do
- assert_equal false, DateTime.civil(2005,2,10,20,30,44).future?
- assert_equal false, DateTime.civil(2005,2,10,20,30,45).future?
- assert_equal true, DateTime.civil(2005,2,10,20,30,46).future?
+ DateTime.stub(:current, DateTime.civil(2005, 2, 10, 15, 30, 45, Rational(-18000, 86400))) do
+ assert_equal false, DateTime.civil(2005, 2, 10, 20, 30, 44).future?
+ assert_equal false, DateTime.civil(2005, 2, 10, 20, 30, 45).future?
+ assert_equal true, DateTime.civil(2005, 2, 10, 20, 30, 46).future?
end
end
def test_current_returns_date_today_when_zone_is_not_set
- with_env_tz 'US/Eastern' do
+ with_env_tz "US/Eastern" do
Time.stub(:now, Time.local(1999, 12, 31, 23, 59, 59)) do
assert_equal DateTime.new(1999, 12, 31, 23, 59, 59, Rational(-18000, 86400)), DateTime.current
end
@@ -262,8 +294,8 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
end
def test_current_returns_time_zone_today_when_zone_is_set
- Time.zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
- with_env_tz 'US/Eastern' do
+ Time.zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ with_env_tz "US/Eastern" do
Time.stub(:now, Time.local(1999, 12, 31, 23, 59, 59)) do
assert_equal DateTime.new(1999, 12, 31, 23, 59, 59, Rational(-18000, 86400)), DateTime.current
end
@@ -277,17 +309,21 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
end
def test_current_with_time_zone
- with_env_tz 'US/Eastern' do
+ with_env_tz "US/Eastern" do
assert_kind_of DateTime, DateTime.current
end
end
def test_acts_like_date
- assert DateTime.new.acts_like_date?
+ assert_predicate DateTime.new, :acts_like_date?
end
def test_acts_like_time
- assert DateTime.new.acts_like_time?
+ assert_predicate DateTime.new, :acts_like_time?
+ end
+
+ def test_blank?
+ assert_not_predicate DateTime.new, :blank?
end
def test_utc?
@@ -301,11 +337,12 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
assert_equal 0, DateTime.civil(2005, 2, 21, 10, 11, 12).utc_offset
assert_equal 0, DateTime.civil(2005, 2, 21, 10, 11, 12, 0).utc_offset
assert_equal 21600, DateTime.civil(2005, 2, 21, 10, 11, 12, 0.25).utc_offset
- assert_equal( -21600, DateTime.civil(2005, 2, 21, 10, 11, 12, -0.25).utc_offset )
- assert_equal( -18000, DateTime.civil(2005, 2, 21, 10, 11, 12, Rational(-5, 24)).utc_offset )
+ assert_equal(-21600, DateTime.civil(2005, 2, 21, 10, 11, 12, -0.25).utc_offset)
+ assert_equal(-18000, DateTime.civil(2005, 2, 21, 10, 11, 12, Rational(-5, 24)).utc_offset)
end
def test_utc
+ assert_instance_of Time, DateTime.civil(2005, 2, 21, 10, 11, 12, Rational(-6, 24)).utc
assert_equal DateTime.civil(2005, 2, 21, 16, 11, 12, 0), DateTime.civil(2005, 2, 21, 10, 11, 12, Rational(-6, 24)).utc
assert_equal DateTime.civil(2005, 2, 21, 15, 11, 12, 0), DateTime.civil(2005, 2, 21, 10, 11, 12, Rational(-5, 24)).utc
assert_equal DateTime.civil(2005, 2, 21, 10, 11, 12, 0), DateTime.civil(2005, 2, 21, 10, 11, 12, 0).utc
@@ -314,60 +351,83 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
end
def test_formatted_offset_with_utc
- assert_equal '+00:00', DateTime.civil(2000).formatted_offset
- assert_equal '+0000', DateTime.civil(2000).formatted_offset(false)
- assert_equal 'UTC', DateTime.civil(2000).formatted_offset(true, 'UTC')
+ assert_equal "+00:00", DateTime.civil(2000).formatted_offset
+ assert_equal "+0000", DateTime.civil(2000).formatted_offset(false)
+ assert_equal "UTC", DateTime.civil(2000).formatted_offset(true, "UTC")
end
def test_formatted_offset_with_local
dt = DateTime.civil(2005, 2, 21, 10, 11, 12, Rational(-5, 24))
- assert_equal '-05:00', dt.formatted_offset
- assert_equal '-0500', dt.formatted_offset(false)
+ assert_equal "-05:00", dt.formatted_offset
+ assert_equal "-0500", dt.formatted_offset(false)
end
def test_compare_with_time
- assert_equal 1, DateTime.civil(2000) <=> Time.utc(1999, 12, 31, 23, 59, 59)
- assert_equal 0, DateTime.civil(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0)
+ assert_equal 1, DateTime.civil(2000) <=> Time.utc(1999, 12, 31, 23, 59, 59)
+ assert_equal 0, DateTime.civil(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0)
assert_equal(-1, DateTime.civil(2000) <=> Time.utc(2000, 1, 1, 0, 0, 1))
end
def test_compare_with_datetime
- assert_equal 1, DateTime.civil(2000) <=> DateTime.civil(1999, 12, 31, 23, 59, 59)
- assert_equal 0, DateTime.civil(2000) <=> DateTime.civil(2000, 1, 1, 0, 0, 0)
+ assert_equal 1, DateTime.civil(2000) <=> DateTime.civil(1999, 12, 31, 23, 59, 59)
+ assert_equal 0, DateTime.civil(2000) <=> DateTime.civil(2000, 1, 1, 0, 0, 0)
assert_equal(-1, DateTime.civil(2000) <=> DateTime.civil(2000, 1, 1, 0, 0, 1))
end
def test_compare_with_time_with_zone
- assert_equal 1, DateTime.civil(2000) <=> ActiveSupport::TimeWithZone.new( Time.utc(1999, 12, 31, 23, 59, 59), ActiveSupport::TimeZone['UTC'] )
- assert_equal 0, DateTime.civil(2000) <=> ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone['UTC'] )
- assert_equal(-1, DateTime.civil(2000) <=> ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1, 0, 0, 1), ActiveSupport::TimeZone['UTC'] ))
+ assert_equal 1, DateTime.civil(2000) <=> ActiveSupport::TimeWithZone.new(Time.utc(1999, 12, 31, 23, 59, 59), ActiveSupport::TimeZone["UTC"])
+ assert_equal 0, DateTime.civil(2000) <=> ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone["UTC"])
+ assert_equal(-1, DateTime.civil(2000) <=> ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 1), ActiveSupport::TimeZone["UTC"]))
end
def test_compare_with_string
- assert_equal 1, DateTime.civil(2000) <=> Time.utc(1999, 12, 31, 23, 59, 59).to_s
- assert_equal 0, DateTime.civil(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0).to_s
- assert_equal( -1, DateTime.civil(2000) <=> Time.utc(2000, 1, 1, 0, 0, 1).to_s)
- assert_equal nil, DateTime.civil(2000) <=> "Invalid as Time"
+ assert_equal 1, DateTime.civil(2000) <=> Time.utc(1999, 12, 31, 23, 59, 59).to_s
+ assert_equal 0, DateTime.civil(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0).to_s
+ assert_equal(-1, DateTime.civil(2000) <=> Time.utc(2000, 1, 1, 0, 0, 1).to_s)
+ assert_nil DateTime.civil(2000) <=> "Invalid as Time"
+ end
+
+ def test_compare_with_integer
+ assert_equal 1, DateTime.civil(1970, 1, 1, 12, 0, 0) <=> 2440587
+ assert_equal 0, DateTime.civil(1970, 1, 1, 12, 0, 0) <=> 2440588
+ assert_equal(-1, DateTime.civil(1970, 1, 1, 12, 0, 0) <=> 2440589)
+ end
+
+ def test_compare_with_float
+ assert_equal 1, DateTime.civil(1970) <=> 2440586.5
+ assert_equal 0, DateTime.civil(1970) <=> 2440587.5
+ assert_equal(-1, DateTime.civil(1970) <=> 2440588.5)
+ end
+
+ def test_compare_with_rational
+ assert_equal 1, DateTime.civil(1970) <=> Rational(4881173, 2)
+ assert_equal 0, DateTime.civil(1970) <=> Rational(4881175, 2)
+ assert_equal(-1, DateTime.civil(1970) <=> Rational(4881177, 2))
end
def test_to_f
assert_equal 946684800.0, DateTime.civil(2000).to_f
- assert_equal 946684800.0, DateTime.civil(1999,12,31,19,0,0,Rational(-5,24)).to_f
- assert_equal 946684800.5, DateTime.civil(1999,12,31,19,0,0.5,Rational(-5,24)).to_f
+ assert_equal 946684800.0, DateTime.civil(1999, 12, 31, 19, 0, 0, Rational(-5, 24)).to_f
+ assert_equal 946684800.5, DateTime.civil(1999, 12, 31, 19, 0, 0.5, Rational(-5, 24)).to_f
end
def test_to_i
assert_equal 946684800, DateTime.civil(2000).to_i
- assert_equal 946684800, DateTime.civil(1999,12,31,19,0,0,Rational(-5,24)).to_i
+ assert_equal 946684800, DateTime.civil(1999, 12, 31, 19, 0, 0, Rational(-5, 24)).to_i
end
def test_usec
assert_equal 0, DateTime.civil(2000).usec
- assert_equal 500000, DateTime.civil(2000, 1, 1, 0, 0, Rational(1,2)).usec
+ assert_equal 500000, DateTime.civil(2000, 1, 1, 0, 0, Rational(1, 2)).usec
end
def test_nsec
assert_equal 0, DateTime.civil(2000).nsec
- assert_equal 500000000, DateTime.civil(2000, 1, 1, 0, 0, Rational(1,2)).nsec
+ assert_equal 500000000, DateTime.civil(2000, 1, 1, 0, 0, Rational(1, 2)).nsec
+ end
+
+ def test_subsec
+ assert_equal 0, DateTime.civil(2000).subsec
+ assert_equal Rational(1, 2), DateTime.civil(2000, 1, 1, 0, 0, Rational(1, 2)).subsec
end
end
diff --git a/activesupport/test/core_ext/digest/uuid_test.rb b/activesupport/test/core_ext/digest/uuid_test.rb
index 08e0a1d6e1..94cb7d9418 100644
--- a/activesupport/test/core_ext/digest/uuid_test.rb
+++ b/activesupport/test/core_ext/digest/uuid_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'active_support/core_ext/digest/uuid'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/digest/uuid"
class DigestUUIDExt < ActiveSupport::TestCase
def test_v3_uuids
@@ -18,7 +20,7 @@ class DigestUUIDExt < ActiveSupport::TestCase
def test_invalid_hash_class
assert_raise ArgumentError do
- Digest::UUID.uuid_from_hash(Digest::SHA2, Digest::UUID::OID_NAMESPACE, '1.2.3')
+ Digest::UUID.uuid_from_hash(Digest::SHA2, Digest::UUID::OID_NAMESPACE, "1.2.3")
end
end
end
diff --git a/activesupport/test/core_ext/duration_test.rb b/activesupport/test/core_ext/duration_test.rb
index 9e97acaffb..63934e2433 100644
--- a/activesupport/test/core_ext/duration_test.rb
+++ b/activesupport/test/core_ext/duration_test.rb
@@ -1,8 +1,11 @@
-require 'abstract_unit'
-require 'active_support/inflector'
-require 'active_support/time'
-require 'active_support/json'
-require 'time_zone_test_helpers'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/inflector"
+require "active_support/time"
+require "active_support/json"
+require "time_zone_test_helpers"
+require "yaml"
class DurationTest < ActiveSupport::TestCase
include TimeZoneTestHelpers
@@ -12,31 +15,31 @@ class DurationTest < ActiveSupport::TestCase
assert d.is_a?(ActiveSupport::Duration)
assert_kind_of ActiveSupport::Duration, d
assert_kind_of Numeric, d
- assert_kind_of Fixnum, d
- assert !d.is_a?(Hash)
+ assert_kind_of Integer, d
+ assert_not d.is_a?(Hash)
k = Class.new
class << k; undef_method :== end
- assert !d.is_a?(k)
+ assert_not d.is_a?(k)
end
def test_instance_of
- assert 1.minute.instance_of?(Fixnum)
+ assert 1.minute.instance_of?(Integer)
assert 2.days.instance_of?(ActiveSupport::Duration)
- assert !3.second.instance_of?(Numeric)
+ assert_not 3.second.instance_of?(Numeric)
end
def test_threequals
assert ActiveSupport::Duration === 1.day
- assert !(ActiveSupport::Duration === 1.day.to_i)
- assert !(ActiveSupport::Duration === 'foo')
+ assert_not (ActiveSupport::Duration === 1.day.to_i)
+ assert_not (ActiveSupport::Duration === "foo")
end
def test_equals
assert 1.day == 1.day
assert 1.day == 1.day.to_i
assert 1.day.to_i == 1.day
- assert !(1.day == 'foo')
+ assert_not (1.day == "foo")
end
def test_to_s
@@ -50,31 +53,34 @@ class DurationTest < ActiveSupport::TestCase
assert 1.minute.eql?(1.minute)
assert 1.minute.eql?(60.seconds)
assert 2.days.eql?(48.hours)
- assert !1.second.eql?(1)
- assert !1.eql?(1.second)
+ assert_not 1.second.eql?(1)
+ assert_not 1.eql?(1.second)
assert 1.minute.eql?(180.seconds - 2.minutes)
- assert !1.minute.eql?(60)
- assert !1.minute.eql?('foo')
+ assert_not 1.minute.eql?(60)
+ assert_not 1.minute.eql?("foo")
end
def test_inspect
- assert_equal '0 seconds', 0.seconds.inspect
- assert_equal '1 month', 1.month.inspect
- assert_equal '1 month and 1 day', (1.month + 1.day).inspect
- assert_equal '6 months and -2 days', (6.months - 2.days).inspect
- assert_equal '10 seconds', 10.seconds.inspect
- assert_equal '10 years, 2 months, and 1 day', (10.years + 2.months + 1.day).inspect
- assert_equal '10 years, 2 months, and 1 day', (10.years + 1.month + 1.day + 1.month).inspect
- assert_equal '10 years, 2 months, and 1 day', (1.day + 10.years + 2.months).inspect
- assert_equal '7 days', 1.week.inspect
- assert_equal '14 days', 1.fortnight.inspect
+ assert_equal "0 seconds", 0.seconds.inspect
+ assert_equal "1 month", 1.month.inspect
+ assert_equal "1 month and 1 day", (1.month + 1.day).inspect
+ assert_equal "6 months and -2 days", (6.months - 2.days).inspect
+ assert_equal "10 seconds", 10.seconds.inspect
+ assert_equal "10 years, 2 months, and 1 day", (10.years + 2.months + 1.day).inspect
+ assert_equal "10 years, 2 months, and 1 day", (10.years + 1.month + 1.day + 1.month).inspect
+ assert_equal "10 years, 2 months, and 1 day", (1.day + 10.years + 2.months).inspect
+ assert_equal "7 days", 7.days.inspect
+ assert_equal "1 week", 1.week.inspect
+ assert_equal "2 weeks", 1.fortnight.inspect
+ assert_equal "0 seconds", (10 % 5.seconds).inspect
+ assert_equal "10 minutes", (10.minutes + 0.seconds).inspect
end
def test_inspect_locale
current_locale = I18n.default_locale
I18n.default_locale = :de
- I18n.backend.store_translations(:de, { support: { array: { last_word_connector: ' und ' } } })
- assert_equal '10 years, 1 month und 1 day', (10.years + 1.month + 1.day).inspect
+ I18n.backend.store_translations(:de, support: { array: { last_word_connector: " und " } })
+ assert_equal "10 years, 1 month und 1 day", (10.years + 1.month + 1.day).inspect
ensure
I18n.default_locale = current_locale
end
@@ -83,13 +89,103 @@ 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.hour, 1.day / 24
+ assert_instance_of ActiveSupport::Duration, 1.day / 24
+
+ assert_equal 24, 86400 / 1.hour
+ assert_kind_of Integer, 86400 / 1.hour
+
+ assert_equal 24, 1.day / 1.hour
+ assert_kind_of Integer, 1.day / 1.hour
+
+ assert_equal 1, 1.day / 1.day
+ assert_kind_of Integer, 1.day / 1.hour
+ end
+
+ def test_modulo
+ assert_equal 1.minute, 5.minutes % 120
+ assert_instance_of ActiveSupport::Duration, 5.minutes % 120
+
+ assert_equal 1.minute, 5.minutes % 2.minutes
+ assert_instance_of ActiveSupport::Duration, 5.minutes % 2.minutes
+
+ assert_equal 1.minute, 5.minutes % 120.seconds
+ assert_instance_of ActiveSupport::Duration, 5.minutes % 120.seconds
+
+ assert_equal 5.minutes, 5.minutes % 1.hour
+ assert_instance_of ActiveSupport::Duration, 5.minutes % 1.hour
+
+ assert_equal 1.day, 36.days % 604800
+ assert_instance_of ActiveSupport::Duration, 36.days % 604800
+
+ assert_equal 1.day, 36.days % 7.days
+ assert_instance_of ActiveSupport::Duration, 36.days % 7.days
+
+ assert_equal 800.seconds, 8000 % 1.hour
+ assert_instance_of ActiveSupport::Duration, 8000 % 1.hour
+
+ assert_equal 1.month, 13.months % 1.year
+ assert_instance_of ActiveSupport::Duration, 13.months % 1.year
+ 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_date_added_with_multiplied_duration_larger_than_one_month
+ assert_equal Date.civil(2017, 2, 15), Date.civil(2017, 1, 1) + 1.day * 45
+ end
+
+ def test_date_added_with_divided_duration
+ assert_equal Date.civil(2017, 1, 3), Date.civil(2017, 1, 1) + 4.days / 2
+ end
+
+ def test_date_added_with_divided_duration_larger_than_one_month
+ assert_equal Date.civil(2017, 2, 15), Date.civil(2017, 1, 1) + 90.days / 2
+ end
+
def test_plus_with_time
assert_equal 1 + 1.second, 1.second + 1, "Duration + Numeric should == Numeric + Duration"
end
+ def test_time_plus_duration_returns_same_time_datatype
+ twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Moscow"], Time.utc(2016, 4, 28, 00, 45))
+ now = Time.now.utc
+ %w( second minute hour day week month year ).each do |unit|
+ assert_equal((now + 1.send(unit)).class, Time, "Time + 1.#{unit} must be Time")
+ assert_equal((twz + 1.send(unit)).class, ActiveSupport::TimeWithZone, "TimeWithZone + 1.#{unit} must be TimeWithZone")
+ end
+ end
+
def test_argument_error
e = assert_raise ArgumentError do
- 1.second.ago('')
+ 1.second.ago("")
end
assert_equal 'expected a time or date, got ""', e.message, "ensure ArgumentError is not being raised by dependencies.rb"
end
@@ -139,54 +235,67 @@ class DurationTest < ActiveSupport::TestCase
def test_since_and_ago_anchored_to_time_now_when_time_zone_is_not_set
Time.zone = nil
- with_env_tz 'US/Eastern' do
+ with_env_tz "US/Eastern" do
Time.stub(:now, Time.local(2000)) do
# since
assert_not_instance_of ActiveSupport::TimeWithZone, 5.seconds.since
- assert_equal Time.local(2000,1,1,0,0,5), 5.seconds.since
+ assert_equal Time.local(2000, 1, 1, 0, 0, 5), 5.seconds.since
# ago
assert_not_instance_of ActiveSupport::TimeWithZone, 5.seconds.ago
- assert_equal Time.local(1999,12,31,23,59,55), 5.seconds.ago
+ assert_equal Time.local(1999, 12, 31, 23, 59, 55), 5.seconds.ago
end
end
end
def test_since_and_ago_anchored_to_time_zone_now_when_time_zone_is_set
- Time.zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
- with_env_tz 'US/Eastern' do
+ Time.zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ with_env_tz "US/Eastern" do
Time.stub(:now, Time.local(2000)) do
# since
assert_instance_of ActiveSupport::TimeWithZone, 5.seconds.since
- assert_equal Time.utc(2000,1,1,0,0,5), 5.seconds.since.time
- assert_equal 'Eastern Time (US & Canada)', 5.seconds.since.time_zone.name
+ assert_equal Time.utc(2000, 1, 1, 0, 0, 5), 5.seconds.since.time
+ assert_equal "Eastern Time (US & Canada)", 5.seconds.since.time_zone.name
# ago
assert_instance_of ActiveSupport::TimeWithZone, 5.seconds.ago
- assert_equal Time.utc(1999,12,31,23,59,55), 5.seconds.ago.time
- assert_equal 'Eastern Time (US & Canada)', 5.seconds.ago.time_zone.name
+ assert_equal Time.utc(1999, 12, 31, 23, 59, 55), 5.seconds.ago.time
+ assert_equal "Eastern Time (US & Canada)", 5.seconds.ago.time_zone.name
end
end
ensure
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)
+ 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)
end
end
def test_adding_day_across_dst_boundary
- with_env_tz 'CET' do
- assert_equal Time.local(2009,3,29,0,0,0) + 1.day, Time.local(2009,3,30,0,0,0)
+ with_env_tz "CET" do
+ assert_equal Time.local(2009, 3, 29, 0, 0, 0) + 1.day, Time.local(2009, 3, 30, 0, 0, 0)
end
end
def test_delegation_with_block_works
counter = 0
assert_nothing_raised do
- 1.minute.times {counter += 1}
+ 1.minute.times { counter += 1 }
end
- assert_equal counter, 60
+ assert_equal 60, counter
end
def test_as_json
@@ -194,12 +303,16 @@ class DurationTest < ActiveSupport::TestCase
end
def test_to_json
- assert_equal '172800', 2.days.to_json
+ assert_equal "172800", 2.days.to_json
end
def test_case_when
- cased = case 1.day when 1.day then "ok" end
- assert_equal cased, "ok"
+ cased = \
+ case 1.day
+ when 1.day
+ "ok"
+ end
+ assert_equal "ok", cased
end
def test_respond_to
@@ -222,4 +335,338 @@ class DurationTest < ActiveSupport::TestCase
assert_equal(1, (1.minute <=> 1.second))
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_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_plus_parts
+ scalar = ActiveSupport::Duration::Scalar.new(10)
+
+ assert_equal({ days: 1, seconds: 10 }, (scalar + 1.day).parts)
+ assert_equal({ days: -1, seconds: 10 }, (scalar + -1.day).parts)
+ 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
+
+ assert_equal({ days: -1, seconds: 10 }, (scalar - 1.day).parts)
+ assert_equal({ days: 1, seconds: 10 }, (scalar - -1.day).parts)
+
+ 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_parts
+ scalar = ActiveSupport::Duration::Scalar.new(10)
+
+ assert_equal({ days: -1, seconds: 10 }, (scalar - 1.day).parts)
+ assert_equal({ days: 1, seconds: 10 }, (scalar - -1.day).parts)
+ 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_multiply_parts
+ scalar = ActiveSupport::Duration::Scalar.new(1)
+ assert_equal({ days: 2 }, (scalar * 2.days).parts)
+ assert_equal(172800, (scalar * 2.days).value)
+ assert_equal({ days: -2 }, (scalar * -2.days).parts)
+ assert_equal(-172800, (scalar * -2.days).value)
+ 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, 100.seconds / scalar
+ assert_equal 5, scalar / 2.seconds
+ assert_kind_of Integer, 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_modulo
+ scalar = ActiveSupport::Duration::Scalar.new(10)
+
+ assert_equal 1, 31 % scalar
+ assert_instance_of ActiveSupport::Duration::Scalar, 31 % scalar
+ assert_equal 1, scalar % 3
+ assert_instance_of ActiveSupport::Duration::Scalar, scalar % 3
+ assert_equal 1, 31.seconds % scalar
+ assert_instance_of ActiveSupport::Duration, 31.seconds % scalar
+ assert_equal 1, scalar % 3.seconds
+ assert_instance_of ActiveSupport::Duration, scalar % 3.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_modulo_parts
+ scalar = ActiveSupport::Duration::Scalar.new(82800)
+ assert_equal({ hours: 1 }, (scalar % 2.hours).parts)
+ assert_equal(3600, (scalar % 2.hours).value)
+ 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
+ #
+ # Copyright (c) 2012-2014 Arnau Siches
+ #
+ # MIT License
+ #
+ # Permission is hereby granted, free of charge, to any person obtaining
+ # a copy of this software and associated documentation files (the
+ # "Software"), to deal in the Software without restriction, including
+ # without limitation the rights to use, copy, modify, merge, publish,
+ # distribute, sublicense, and/or sell copies of the Software, and to
+ # permit persons to whom the Software is furnished to do so, subject to
+ # the following conditions:
+ #
+ # The above copyright notice and this permission notice shall be
+ # included in all copies or substantial portions of the Software.
+ #
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+ def test_iso8601_parsing_wrong_patterns_with_raise
+ invalid_patterns = ["", "P", "PT", "P1YT", "T", "PW", "P1Y1W", "~P1Y", ".P1Y", "P1.5Y0.5M", "P1.5Y1M", "P1.5MT10.5S"]
+ invalid_patterns.each do |pattern|
+ assert_raise ActiveSupport::Duration::ISO8601Parser::ParsingError, pattern.inspect do
+ ActiveSupport::Duration.parse(pattern)
+ end
+ end
+ end
+
+ def test_iso8601_output
+ expectations = [
+ ["P1Y", 1.year ],
+ ["P1W", 1.week ],
+ ["P1Y1M", 1.year + 1.month ],
+ ["P1Y1M1D", 1.year + 1.month + 1.day ],
+ ["-P1Y1D", -1.year - 1.day ],
+ ["P1Y-1DT-1S", 1.year - 1.day - 1.second ], # Parts with different signs are exists in PostgreSQL interval datatype.
+ ["PT1S", 1.second ],
+ ["PT1.4S", (1.4).seconds ],
+ ["P1Y1M1DT1H", 1.year + 1.month + 1.day + 1.hour],
+ ["PT0S", 0.minutes ],
+ ]
+ expectations.each do |expected_output, duration|
+ assert_equal expected_output, duration.iso8601, expected_output.inspect
+ end
+ end
+
+ def test_iso8601_output_precision
+ expectations = [
+ [nil, "P1Y1MT8.55S", 1.year + 1.month + (8.55).seconds ],
+ [0, "P1Y1MT9S", 1.year + 1.month + (8.55).seconds ],
+ [1, "P1Y1MT8.6S", 1.year + 1.month + (8.55).seconds ],
+ [2, "P1Y1MT8.55S", 1.year + 1.month + (8.55).seconds ],
+ [3, "P1Y1MT8.550S", 1.year + 1.month + (8.55).seconds ],
+ [nil, "PT1S", 1.second ],
+ [2, "PT1.00S", 1.second ],
+ [nil, "PT1.4S", (1.4).seconds ],
+ [0, "PT1S", (1.4).seconds ],
+ [1, "PT1.4S", (1.4).seconds ],
+ [5, "PT1.40000S", (1.4).seconds ],
+ ]
+ expectations.each do |precision, expected_output, duration|
+ assert_equal expected_output, duration.iso8601(precision: precision), expected_output.inspect
+ end
+ end
+
+ def test_iso8601_output_and_reparsing
+ patterns = %w[
+ P1Y P0.5Y P0,5Y P1Y1M P1Y0.5M P1Y0,5M P1Y1M1D P1Y1M0.5D P1Y1M0,5D P1Y1M1DT1H P1Y1M1DT0.5H P1Y1M1DT0,5H P1W +P1Y -P1Y
+ P1Y1M1DT1H1M P1Y1M1DT1H0.5M P1Y1M1DT1H0,5M P1Y1M1DT1H1M1S P1Y1M1DT1H1M1.0S P1Y1M1DT1H1M1,0S P-1Y-2M3DT-4H-5M-6S
+ ]
+ # That could be weird, but if we parse P1Y1M0.5D and output it to ISO 8601, we'll get P1Y1MT12.0H.
+ # So we check that initially parsed and reparsed duration added to time will result in the same time.
+ time = Time.current
+ patterns.each do |pattern|
+ duration = ActiveSupport::Duration.parse(pattern)
+ assert_equal time + duration, time + ActiveSupport::Duration.parse(duration.iso8601), pattern.inspect
+ end
+ end
+
+ def test_iso8601_parsing_across_spring_dst_boundary
+ with_env_tz eastern_time_zone do
+ with_tz_default "Eastern Time (US & Canada)" do
+ travel_to Time.utc(2016, 3, 11) do
+ assert_equal 604800, ActiveSupport::Duration.parse("P7D").to_i
+ assert_equal 604800, ActiveSupport::Duration.parse("P1W").to_i
+ end
+ end
+ end
+ end
+
+ def test_iso8601_parsing_across_autumn_dst_boundary
+ with_env_tz eastern_time_zone do
+ with_tz_default "Eastern Time (US & Canada)" do
+ travel_to Time.utc(2016, 11, 4) do
+ assert_equal 604800, ActiveSupport::Duration.parse("P7D").to_i
+ assert_equal 604800, ActiveSupport::Duration.parse("P1W").to_i
+ end
+ end
+ 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
+
+ def test_durations_survive_yaml_serialization
+ d1 = YAML.load(YAML.dump(10.minutes))
+ assert_equal 600, d1.to_i
+ assert_equal 660, (d1 + 60).to_i
+ end
+
+ private
+ def eastern_time_zone
+ if Gem.win_platform?
+ "EST5EDT"
+ else
+ "America/New_York"
+ end
+ end
end
diff --git a/activesupport/test/core_ext/enumerable_test.rb b/activesupport/test/core_ext/enumerable_test.rb
index f09b7d8850..b63464a36a 100644
--- a/activesupport/test/core_ext/enumerable_test.rb
+++ b/activesupport/test/core_ext/enumerable_test.rb
@@ -1,6 +1,8 @@
-require 'abstract_unit'
-require 'active_support/core_ext/array'
-require 'active_support/core_ext/enumerable'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/array"
+require "active_support/core_ext/enumerable"
Payment = Struct.new(:price)
ExpandedPayment = Struct.new(:dollars, :cents)
@@ -10,26 +12,31 @@ class SummablePayment < Payment
end
class EnumerableTests < ActiveSupport::TestCase
-
class GenericEnumerable
include Enumerable
+
def initialize(values = [1, 2, 3])
@values = values
end
def each
- @values.each{|v| yield v}
+ @values.each { |v| yield v }
end
end
+ def assert_typed_equal(e, v, cls, msg = nil)
+ assert_kind_of(cls, v, msg)
+ assert_equal(e, v, msg)
+ end
+
def test_sums
enum = GenericEnumerable.new([5, 15, 10])
assert_equal 30, enum.sum
- assert_equal 60, enum.sum { |i| i * 2}
+ assert_equal 60, enum.sum { |i| i * 2 }
enum = GenericEnumerable.new(%w(a b c))
- assert_equal 'abc', enum.sum
- assert_equal 'aabbcc', enum.sum { |i| i * 2 }
+ assert_equal "abc", enum.sum
+ assert_equal "aabbcc", enum.sum { |i| i * 2 }
payments = GenericEnumerable.new([ Payment.new(5), Payment.new(15), Payment.new(10) ])
assert_equal 30, payments.sum(&:price)
@@ -38,6 +45,40 @@ class EnumerableTests < ActiveSupport::TestCase
payments = GenericEnumerable.new([ SummablePayment.new(5), SummablePayment.new(15) ])
assert_equal SummablePayment.new(20), payments.sum
assert_equal SummablePayment.new(20), payments.sum { |p| p }
+
+ sum = GenericEnumerable.new([3, 5.quo(1)]).sum
+ assert_typed_equal(8, sum, Rational)
+
+ sum = GenericEnumerable.new([3, 5.quo(1)]).sum(0.0)
+ assert_typed_equal(8.0, sum, Float)
+
+ sum = GenericEnumerable.new([3, 5.quo(1), 7.0]).sum
+ assert_typed_equal(15.0, sum, Float)
+
+ sum = GenericEnumerable.new([3, 5.quo(1), Complex(7)]).sum
+ assert_typed_equal(Complex(15), sum, Complex)
+ assert_typed_equal(15, sum.real, Rational)
+ assert_typed_equal(0, sum.imag, Integer)
+
+ sum = GenericEnumerable.new([3.5, 5]).sum
+ assert_typed_equal(8.5, sum, Float)
+
+ sum = GenericEnumerable.new([2, 8.5]).sum
+ assert_typed_equal(10.5, sum, Float)
+
+ sum = GenericEnumerable.new([1.quo(2), 1]).sum
+ assert_typed_equal(3.quo(2), sum, Rational)
+
+ sum = GenericEnumerable.new([1.quo(2), 1.quo(3)]).sum
+ assert_typed_equal(5.quo(6), sum, Rational)
+
+ sum = GenericEnumerable.new([2.0, 3.0 * Complex::I]).sum
+ assert_typed_equal(Complex(2.0, 3.0), sum, Complex)
+ assert_typed_equal(2.0, sum.real, Float)
+ assert_typed_equal(3.0, sum.imag, Float)
+
+ sum = GenericEnumerable.new([1, 2]).sum(10) { |v| v * 2 }
+ assert_typed_equal(16, sum, Integer)
end
def test_nil_sums
@@ -55,6 +96,7 @@ class EnumerableTests < ActiveSupport::TestCase
assert_equal 0, GenericEnumerable.new([]).sum
assert_equal 0, GenericEnumerable.new([]).sum { |i| i + 10 }
assert_equal Payment.new(0), GenericEnumerable.new([]).sum(Payment.new(0))
+ assert_typed_equal 0.0, GenericEnumerable.new([]).sum(0.0), Float
end
def test_range_sums
@@ -62,12 +104,68 @@ class EnumerableTests < ActiveSupport::TestCase
assert_equal 10, (1..4).sum
assert_equal 10, (1..4.5).sum
assert_equal 6, (1...4).sum
- assert_equal 'abc', ('a'..'c').sum
+ assert_equal "abc", ("a".."c").sum
assert_equal 50_000_005_000_000, (0..10_000_000).sum
assert_equal 0, (10..0).sum
assert_equal 5, (10..0).sum(5)
assert_equal 10, (10..10).sum
assert_equal 42, (10...10).sum(42)
+ assert_typed_equal 20.0, (1..4).sum(0.0) { |i| i * 2 }, Float
+ assert_typed_equal 10.0, (1..4).sum(0.0), Float
+ assert_typed_equal 20.0, (1..4).sum(10.0), Float
+ assert_typed_equal 5.0, (10..0).sum(5.0), Float
+ end
+
+ def test_array_sums
+ enum = [5, 15, 10]
+ assert_equal 30, enum.sum
+ assert_equal 60, enum.sum { |i| i * 2 }
+
+ enum = %w(a b c)
+ assert_equal "abc", enum.sum
+ assert_equal "aabbcc", enum.sum { |i| i * 2 }
+
+ payments = [ Payment.new(5), Payment.new(15), Payment.new(10) ]
+ assert_equal 30, payments.sum(&:price)
+ assert_equal 60, payments.sum { |p| p.price * 2 }
+
+ payments = [ SummablePayment.new(5), SummablePayment.new(15) ]
+ assert_equal SummablePayment.new(20), payments.sum
+ assert_equal SummablePayment.new(20), payments.sum { |p| p }
+
+ sum = [3, 5.quo(1)].sum
+ assert_typed_equal(8, sum, Rational)
+
+ sum = [3, 5.quo(1)].sum(0.0)
+ assert_typed_equal(8.0, sum, Float)
+
+ sum = [3, 5.quo(1), 7.0].sum
+ assert_typed_equal(15.0, sum, Float)
+
+ sum = [3, 5.quo(1), Complex(7)].sum
+ assert_typed_equal(Complex(15), sum, Complex)
+ assert_typed_equal(15, sum.real, Rational)
+ assert_typed_equal(0, sum.imag, Integer)
+
+ sum = [3.5, 5].sum
+ assert_typed_equal(8.5, sum, Float)
+
+ sum = [2, 8.5].sum
+ assert_typed_equal(10.5, sum, Float)
+
+ sum = [1.quo(2), 1].sum
+ assert_typed_equal(3.quo(2), sum, Rational)
+
+ sum = [1.quo(2), 1.quo(3)].sum
+ assert_typed_equal(5.quo(6), sum, Rational)
+
+ sum = [2.0, 3.0 * Complex::I].sum
+ assert_typed_equal(Complex(2.0, 3.0), sum, Complex)
+ assert_typed_equal(2.0, sum.real, Float)
+ assert_typed_equal(3.0, sum.imag, Float)
+
+ sum = [1, 2].sum(10) { |v| v * 2 }
+ assert_typed_equal(16, sum, Integer)
end
def test_index_by
@@ -75,30 +173,43 @@ class EnumerableTests < ActiveSupport::TestCase
assert_equal({ 5 => Payment.new(5), 15 => Payment.new(15), 10 => Payment.new(10) },
payments.index_by(&:price))
assert_equal Enumerator, payments.index_by.class
- if Enumerator.method_defined? :size
- assert_equal nil, payments.index_by.size
- assert_equal 42, (1..42).index_by.size
- end
+ assert_nil payments.index_by.size
+ assert_equal 42, (1..42).index_by.size
assert_equal({ 5 => Payment.new(5), 15 => Payment.new(15), 10 => Payment.new(10) },
payments.index_by.each(&:price))
end
+ def test_index_with
+ payments = GenericEnumerable.new([ Payment.new(5), Payment.new(15), Payment.new(10) ])
+
+ assert_equal({ Payment.new(5) => 5, Payment.new(15) => 15, Payment.new(10) => 10 }, payments.index_with(&:price))
+
+ assert_equal({ title: nil, body: nil }, %i( title body ).index_with(nil))
+ assert_equal({ title: [], body: [] }, %i( title body ).index_with([]))
+ assert_equal({ title: {}, body: {} }, %i( title body ).index_with({}))
+
+ assert_equal Enumerator, payments.index_with.class
+ assert_nil payments.index_with.size
+ assert_equal 42, (1..42).index_with.size
+ assert_equal({ Payment.new(5) => 5, Payment.new(15) => 15, Payment.new(10) => 10 }, payments.index_with.each(&:price))
+ end
+
def test_many
- assert_equal false, GenericEnumerable.new([] ).many?
- assert_equal false, GenericEnumerable.new([ 1 ] ).many?
- assert_equal true, GenericEnumerable.new([ 1, 2 ] ).many?
-
- assert_equal false, GenericEnumerable.new([] ).many? {|x| x > 1 }
- assert_equal false, GenericEnumerable.new([ 2 ] ).many? {|x| x > 1 }
- assert_equal false, GenericEnumerable.new([ 1, 2 ] ).many? {|x| x > 1 }
- assert_equal true, GenericEnumerable.new([ 1, 2, 2 ]).many? {|x| x > 1 }
+ assert_equal false, GenericEnumerable.new([]).many?
+ assert_equal false, GenericEnumerable.new([ 1 ]).many?
+ assert_equal true, GenericEnumerable.new([ 1, 2 ]).many?
+
+ assert_equal false, GenericEnumerable.new([]).many? { |x| x > 1 }
+ assert_equal false, GenericEnumerable.new([ 2 ]).many? { |x| x > 1 }
+ assert_equal false, GenericEnumerable.new([ 1, 2 ]).many? { |x| x > 1 }
+ assert_equal true, GenericEnumerable.new([ 1, 2, 2 ]).many? { |x| x > 1 }
end
def test_many_iterates_only_on_what_is_needed
- infinity = 1.0/0.0
+ infinity = 1.0 / 0.0
very_long_enum = 0..infinity
assert_equal true, very_long_enum.many?
- assert_equal true, very_long_enum.many?{|x| x > 100}
+ assert_equal true, very_long_enum.many? { |x| x > 100 }
end
def test_exclude?
@@ -110,7 +221,7 @@ class EnumerableTests < ActiveSupport::TestCase
assert_equal [1, 2, 4], GenericEnumerable.new((1..5).to_a).without(3, 5)
assert_equal [1, 2, 4], (1..5).to_a.without(3, 5)
assert_equal [1, 2, 4], (1..5).to_set.without(3, 5)
- assert_equal({foo: 1, baz: 3}, {foo: 1, bar: 2, baz: 3}.without(:bar))
+ assert_equal({ foo: 1, baz: 3 }, { foo: 1, bar: 2, baz: 3 }.without(:bar))
end
def test_pluck
diff --git a/activesupport/test/core_ext/file_test.rb b/activesupport/test/core_ext/file_test.rb
index cde0132b97..9c97700e5d 100644
--- a/activesupport/test/core_ext/file_test.rb
+++ b/activesupport/test/core_ext/file_test.rb
@@ -1,12 +1,14 @@
-require 'abstract_unit'
-require 'active_support/core_ext/file'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/file"
class AtomicWriteTest < ActiveSupport::TestCase
def test_atomic_write_without_errors
contents = "Atomic Text"
File.atomic_write(file_name, Dir.pwd) do |file|
file.write(contents)
- assert !File.exist?(file_name)
+ assert_not File.exist?(file_name)
end
assert File.exist?(file_name)
assert_equal contents, File.read(file_name)
@@ -20,7 +22,7 @@ class AtomicWriteTest < ActiveSupport::TestCase
raise "something bad"
end
rescue
- assert !File.exist?(file_name)
+ assert_not File.exist?(file_name)
end
def test_atomic_write_preserves_file_permissions
@@ -48,7 +50,7 @@ class AtomicWriteTest < ActiveSupport::TestCase
contents = "Atomic Text"
File.atomic_write(file_name, Dir.pwd) do |file|
file.write(contents)
- assert !File.exist?(file_name)
+ assert_not File.exist?(file_name)
end
assert File.exist?(file_name)
assert_equal File.probe_stat_in(Dir.pwd).mode, file_mode
diff --git a/activesupport/test/core_ext/hash/transform_keys_test.rb b/activesupport/test/core_ext/hash/transform_keys_test.rb
index a7e12117f3..b9e41f7b25 100644
--- a/activesupport/test/core_ext/hash/transform_keys_test.rb
+++ b/activesupport/test/core_ext/hash/transform_keys_test.rb
@@ -1,32 +1,64 @@
-require 'abstract_unit'
-require 'active_support/core_ext/hash/keys'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/hash/keys"
class TransformKeysTest < ActiveSupport::TestCase
test "transform_keys returns a new hash with the keys computed from the block" do
- original = { a: 'a', b: 'b' }
+ original = { a: "a", b: "b" }
mapped = original.transform_keys { |k| "#{k}!".to_sym }
- assert_equal({ a: 'a', b: 'b' }, original)
- assert_equal({ a!: 'a', b!: 'b' }, mapped)
+ assert_equal({ a: "a", b: "b" }, original)
+ assert_equal({ a!: "a", b!: "b" }, mapped)
end
test "transform_keys! modifies the keys of the original" do
- original = { a: 'a', b: 'b' }
+ original = { a: "a", b: "b" }
mapped = original.transform_keys! { |k| "#{k}!".to_sym }
- assert_equal({ a!: 'a', b!: 'b' }, original)
+ assert_equal({ a!: "a", b!: "b" }, original)
assert_same original, mapped
end
- test "transform_keys returns an Enumerator if no block is given" do
- original = { a: 'a', b: 'b' }
+ test "transform_keys returns a sized Enumerator if no block is given" do
+ original = { a: "a", b: "b" }
enumerator = original.transform_keys
+ assert_equal original.size, enumerator.size
+ assert_equal Enumerator, enumerator.class
+ end
+
+ test "transform_keys! returns a sized Enumerator if no block is given" do
+ original = { a: "a", b: "b" }
+ enumerator = original.transform_keys!
+ assert_equal original.size, enumerator.size
assert_equal Enumerator, enumerator.class
end
test "transform_keys is chainable with Enumerable methods" do
- original = { a: 'a', b: 'b' }
+ original = { a: "a", b: "b" }
mapped = original.transform_keys.with_index { |k, i| [k, i].join.to_sym }
- assert_equal({ a0: 'a', b1: 'b' }, mapped)
+ assert_equal({ a0: "a", b1: "b" }, mapped)
+ end
+
+ test "transform_keys! is chainable with Enumerable methods" do
+ original = { a: "a", b: "b" }
+ original.transform_keys!.with_index { |k, i| [k, i].join.to_sym }
+ assert_equal({ a0: "a", b1: "b" }, original)
+ end
+
+ test "transform_keys returns a Hash instance when self is inherited from Hash" do
+ class HashDescendant < ::Hash
+ def initialize(elements = nil)
+ super(elements)
+ (elements || {}).each_pair { |key, value| self[key] = value }
+ end
+ end
+
+ original = HashDescendant.new(a: "a", b: "b")
+ mapped = original.transform_keys { |k| "#{k}!".to_sym }
+
+ assert_equal({ a: "a", b: "b" }, original)
+ assert_equal({ a!: "a", b!: "b" }, mapped)
+ assert_equal(::Hash, mapped.class)
end
end
diff --git a/activesupport/test/core_ext/hash/transform_values_test.rb b/activesupport/test/core_ext/hash/transform_values_test.rb
index 45ed11fef7..e481b5e4a9 100644
--- a/activesupport/test/core_ext/hash/transform_values_test.rb
+++ b/activesupport/test/core_ext/hash/transform_values_test.rb
@@ -1,61 +1,22 @@
-require 'abstract_unit'
-require 'active_support/core_ext/hash/indifferent_access'
-require 'active_support/core_ext/hash/transform_values'
+# frozen_string_literal: true
-class TransformValuesTest < ActiveSupport::TestCase
- test "transform_values returns a new hash with the values computed from the block" do
- original = { a: 'a', b: 'b' }
- mapped = original.transform_values { |v| v + '!' }
+require "abstract_unit"
+require "active_support/core_ext/hash/indifferent_access"
- assert_equal({ a: 'a', b: 'b' }, original)
- assert_equal({ a: 'a!', b: 'b!' }, mapped)
- end
-
- test "transform_values! modifies the values of the original" do
- original = { a: 'a', b: 'b' }
- mapped = original.transform_values! { |v| v + '!' }
-
- assert_equal({ a: 'a!', b: 'b!' }, original)
- assert_same original, mapped
+class TransformValuesDeprecatedRequireTest < ActiveSupport::TestCase
+ test "requiring transform_values is deprecated" do
+ assert_deprecated do
+ require "active_support/core_ext/hash/transform_values"
+ end
end
+end
+class IndifferentTransformValuesTest < ActiveSupport::TestCase
test "indifferent access is still indifferent after mapping values" do
- original = { a: 'a', b: 'b' }.with_indifferent_access
- mapped = original.transform_values { |v| v + '!' }
-
- assert_equal 'a!', mapped[:a]
- assert_equal 'a!', mapped['a']
- end
-
- # This is to be consistent with the behavior of Ruby's built in methods
- # (e.g. #select, #reject) as of 2.2
- test "default values do not persist during mapping" do
- original = Hash.new('foo')
- original[:a] = 'a'
- mapped = original.transform_values { |v| v + '!' }
-
- assert_equal 'a!', mapped[:a]
- assert_nil mapped[:b]
- end
-
- test "default procs do not persist after mapping" do
- original = Hash.new { 'foo' }
- original[:a] = 'a'
- mapped = original.transform_values { |v| v + '!' }
-
- assert_equal 'a!', mapped[:a]
- assert_nil mapped[:b]
- end
-
- test "transform_values returns an Enumerator if no block is given" do
- original = { a: 'a', b: 'b' }
- enumerator = original.transform_values
- assert_equal Enumerator, enumerator.class
- end
+ original = { a: "a", b: "b" }.with_indifferent_access
+ mapped = original.transform_values { |v| v + "!" }
- test "transform_values is chainable with Enumerable methods" do
- original = { a: 'a', b: 'b' }
- mapped = original.transform_values.with_index { |v, i| [v, i].join }
- assert_equal({ a: 'a0', b: 'b1' }, mapped)
+ assert_equal "a!", mapped[:a]
+ assert_equal "a!", mapped["a"]
end
end
diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb
index 663f782611..f4f0dd6b31 100644
--- a/activesupport/test/core_ext/hash_ext_test.rb
+++ b/activesupport/test/core_ext/hash_ext_test.rb
@@ -1,55 +1,32 @@
-require 'abstract_unit'
-require 'active_support/core_ext/hash'
-require 'bigdecimal'
-require 'active_support/core_ext/string/access'
-require 'active_support/ordered_hash'
-require 'active_support/core_ext/object/conversions'
-require 'active_support/core_ext/object/deep_dup'
-require 'active_support/inflections'
+# frozen_string_literal: true
-class HashExtTest < ActiveSupport::TestCase
- class IndifferentHash < ActiveSupport::HashWithIndifferentAccess
- end
-
- class SubclassingArray < Array
- end
-
- class SubclassingHash < Hash
- end
-
- class NonIndifferentHash < Hash
- def nested_under_indifferent_access
- self
- end
- end
-
- class HashByConversion
- def initialize(hash)
- @hash = hash
- end
-
- def to_hash
- @hash
- end
- end
+require "abstract_unit"
+require "active_support/core_ext/hash"
+require "bigdecimal"
+require "active_support/core_ext/string/access"
+require "active_support/ordered_hash"
+require "active_support/core_ext/object/conversions"
+require "active_support/core_ext/object/deep_dup"
+require "active_support/inflections"
+class HashExtTest < ActiveSupport::TestCase
def setup
- @strings = { 'a' => 1, 'b' => 2 }
- @nested_strings = { 'a' => { 'b' => { 'c' => 3 } } }
- @symbols = { :a => 1, :b => 2 }
- @nested_symbols = { :a => { :b => { :c => 3 } } }
- @mixed = { :a => 1, 'b' => 2 }
- @nested_mixed = { 'a' => { :b => { 'c' => 3 } } }
- @fixnums = { 0 => 1, 1 => 2 }
- @nested_fixnums = { 0 => { 1 => { 2 => 3} } }
+ @strings = { "a" => 1, "b" => 2 }
+ @nested_strings = { "a" => { "b" => { "c" => 3 } } }
+ @symbols = { a: 1, b: 2 }
+ @nested_symbols = { a: { b: { c: 3 } } }
+ @mixed = { :a => 1, "b" => 2 }
+ @nested_mixed = { "a" => { b: { "c" => 3 } } }
+ @integers = { 0 => 1, 1 => 2 }
+ @nested_integers = { 0 => { 1 => { 2 => 3 } } }
@illegal_symbols = { [] => 3 }
- @nested_illegal_symbols = { [] => { [] => 3} }
- @upcase_strings = { 'A' => 1, 'B' => 2 }
- @nested_upcase_strings = { 'A' => { 'B' => { 'C' => 3 } } }
- @string_array_of_hashes = { 'a' => [ { 'b' => 2 }, { 'c' => 3 }, 4 ] }
- @symbol_array_of_hashes = { :a => [ { :b => 2 }, { :c => 3 }, 4 ] }
- @mixed_array_of_hashes = { :a => [ { :b => 2 }, { 'c' => 3 }, 4 ] }
- @upcase_array_of_hashes = { 'A' => [ { 'B' => 2 }, { 'C' => 3 }, 4 ] }
+ @nested_illegal_symbols = { [] => { [] => 3 } }
+ @upcase_strings = { "A" => 1, "B" => 2 }
+ @nested_upcase_strings = { "A" => { "B" => { "C" => 3 } } }
+ @string_array_of_hashes = { "a" => [ { "b" => 2 }, { "c" => 3 }, 4 ] }
+ @symbol_array_of_hashes = { a: [ { b: 2 }, { c: 3 }, 4 ] }
+ @mixed_array_of_hashes = { a: [ { b: 2 }, { "c" => 3 }, 4 ] }
+ @upcase_array_of_hashes = { "A" => [ { "B" => 2 }, { "C" => 3 }, 4 ] }
end
def test_methods
@@ -68,66 +45,64 @@ class HashExtTest < ActiveSupport::TestCase
assert_respond_to h, :deep_stringify_keys!
assert_respond_to h, :to_options
assert_respond_to h, :to_options!
- assert_respond_to h, :compact
- assert_respond_to h, :compact!
assert_respond_to h, :except
assert_respond_to h, :except!
end
def test_transform_keys
- assert_equal @upcase_strings, @strings.transform_keys{ |key| key.to_s.upcase }
- assert_equal @upcase_strings, @symbols.transform_keys{ |key| key.to_s.upcase }
- assert_equal @upcase_strings, @mixed.transform_keys{ |key| key.to_s.upcase }
+ assert_equal @upcase_strings, @strings.transform_keys { |key| key.to_s.upcase }
+ assert_equal @upcase_strings, @symbols.transform_keys { |key| key.to_s.upcase }
+ assert_equal @upcase_strings, @mixed.transform_keys { |key| key.to_s.upcase }
end
def test_transform_keys_not_mutates
transformed_hash = @mixed.dup
- transformed_hash.transform_keys{ |key| key.to_s.upcase }
+ transformed_hash.transform_keys { |key| key.to_s.upcase }
assert_equal @mixed, transformed_hash
end
def test_deep_transform_keys
- assert_equal @nested_upcase_strings, @nested_symbols.deep_transform_keys{ |key| key.to_s.upcase }
- assert_equal @nested_upcase_strings, @nested_strings.deep_transform_keys{ |key| key.to_s.upcase }
- assert_equal @nested_upcase_strings, @nested_mixed.deep_transform_keys{ |key| key.to_s.upcase }
- assert_equal @upcase_array_of_hashes, @string_array_of_hashes.deep_transform_keys{ |key| key.to_s.upcase }
- assert_equal @upcase_array_of_hashes, @symbol_array_of_hashes.deep_transform_keys{ |key| key.to_s.upcase }
- assert_equal @upcase_array_of_hashes, @mixed_array_of_hashes.deep_transform_keys{ |key| key.to_s.upcase }
+ assert_equal @nested_upcase_strings, @nested_symbols.deep_transform_keys { |key| key.to_s.upcase }
+ assert_equal @nested_upcase_strings, @nested_strings.deep_transform_keys { |key| key.to_s.upcase }
+ assert_equal @nested_upcase_strings, @nested_mixed.deep_transform_keys { |key| key.to_s.upcase }
+ assert_equal @upcase_array_of_hashes, @string_array_of_hashes.deep_transform_keys { |key| key.to_s.upcase }
+ assert_equal @upcase_array_of_hashes, @symbol_array_of_hashes.deep_transform_keys { |key| key.to_s.upcase }
+ assert_equal @upcase_array_of_hashes, @mixed_array_of_hashes.deep_transform_keys { |key| key.to_s.upcase }
end
def test_deep_transform_keys_not_mutates
transformed_hash = @nested_mixed.deep_dup
- transformed_hash.deep_transform_keys{ |key| key.to_s.upcase }
+ transformed_hash.deep_transform_keys { |key| key.to_s.upcase }
assert_equal @nested_mixed, transformed_hash
end
def test_transform_keys!
- assert_equal @upcase_strings, @symbols.dup.transform_keys!{ |key| key.to_s.upcase }
- assert_equal @upcase_strings, @strings.dup.transform_keys!{ |key| key.to_s.upcase }
- assert_equal @upcase_strings, @mixed.dup.transform_keys!{ |key| key.to_s.upcase }
+ assert_equal @upcase_strings, @symbols.dup.transform_keys! { |key| key.to_s.upcase }
+ assert_equal @upcase_strings, @strings.dup.transform_keys! { |key| key.to_s.upcase }
+ assert_equal @upcase_strings, @mixed.dup.transform_keys! { |key| key.to_s.upcase }
end
def test_transform_keys_with_bang_mutates
transformed_hash = @mixed.dup
- transformed_hash.transform_keys!{ |key| key.to_s.upcase }
+ 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!
- assert_equal @nested_upcase_strings, @nested_symbols.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase }
- assert_equal @nested_upcase_strings, @nested_strings.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase }
- assert_equal @nested_upcase_strings, @nested_mixed.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase }
- assert_equal @upcase_array_of_hashes, @string_array_of_hashes.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase }
- assert_equal @upcase_array_of_hashes, @symbol_array_of_hashes.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase }
- assert_equal @upcase_array_of_hashes, @mixed_array_of_hashes.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase }
+ assert_equal @nested_upcase_strings, @nested_symbols.deep_dup.deep_transform_keys! { |key| key.to_s.upcase }
+ assert_equal @nested_upcase_strings, @nested_strings.deep_dup.deep_transform_keys! { |key| key.to_s.upcase }
+ assert_equal @nested_upcase_strings, @nested_mixed.deep_dup.deep_transform_keys! { |key| key.to_s.upcase }
+ assert_equal @upcase_array_of_hashes, @string_array_of_hashes.deep_dup.deep_transform_keys! { |key| key.to_s.upcase }
+ assert_equal @upcase_array_of_hashes, @symbol_array_of_hashes.deep_dup.deep_transform_keys! { |key| key.to_s.upcase }
+ assert_equal @upcase_array_of_hashes, @mixed_array_of_hashes.deep_dup.deep_transform_keys! { |key| key.to_s.upcase }
end
def test_deep_transform_keys_with_bang_mutates
transformed_hash = @nested_mixed.deep_dup
- transformed_hash.deep_transform_keys!{ |key| key.to_s.upcase }
+ 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 +142,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 +158,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
@@ -196,14 +171,14 @@ class HashExtTest < ActiveSupport::TestCase
assert_equal @nested_illegal_symbols, @nested_illegal_symbols.deep_dup.deep_symbolize_keys!
end
- def test_symbolize_keys_preserves_fixnum_keys
- assert_equal @fixnums, @fixnums.symbolize_keys
- assert_equal @fixnums, @fixnums.dup.symbolize_keys!
+ def test_symbolize_keys_preserves_integer_keys
+ assert_equal @integers, @integers.symbolize_keys
+ assert_equal @integers, @integers.dup.symbolize_keys!
end
- def test_deep_symbolize_keys_preserves_fixnum_keys
- assert_equal @nested_fixnums, @nested_fixnums.deep_symbolize_keys
- assert_equal @nested_fixnums, @nested_fixnums.deep_dup.deep_symbolize_keys!
+ def test_deep_symbolize_keys_preserves_integer_keys
+ assert_equal @nested_integers, @nested_integers.deep_symbolize_keys
+ assert_equal @nested_integers, @nested_integers.deep_dup.deep_symbolize_keys!
end
def test_stringify_keys
@@ -243,7 +218,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,481 +234,45 @@ 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 } } }
- end
-
- def test_symbolize_keys_for_hash_with_indifferent_access
- assert_instance_of Hash, @symbols.with_indifferent_access.symbolize_keys
- assert_equal @symbols, @symbols.with_indifferent_access.symbolize_keys
- assert_equal @symbols, @strings.with_indifferent_access.symbolize_keys
- assert_equal @symbols, @mixed.with_indifferent_access.symbolize_keys
- end
-
- def test_deep_symbolize_keys_for_hash_with_indifferent_access
- assert_instance_of Hash, @nested_symbols.with_indifferent_access.deep_symbolize_keys
- assert_equal @nested_symbols, @nested_symbols.with_indifferent_access.deep_symbolize_keys
- assert_equal @nested_symbols, @nested_strings.with_indifferent_access.deep_symbolize_keys
- assert_equal @nested_symbols, @nested_mixed.with_indifferent_access.deep_symbolize_keys
- end
-
-
- def test_symbolize_keys_bang_for_hash_with_indifferent_access
- assert_raise(NoMethodError) { @symbols.with_indifferent_access.dup.symbolize_keys! }
- assert_raise(NoMethodError) { @strings.with_indifferent_access.dup.symbolize_keys! }
- assert_raise(NoMethodError) { @mixed.with_indifferent_access.dup.symbolize_keys! }
- end
-
- def test_deep_symbolize_keys_bang_for_hash_with_indifferent_access
- assert_raise(NoMethodError) { @nested_symbols.with_indifferent_access.deep_dup.deep_symbolize_keys! }
- assert_raise(NoMethodError) { @nested_strings.with_indifferent_access.deep_dup.deep_symbolize_keys! }
- assert_raise(NoMethodError) { @nested_mixed.with_indifferent_access.deep_dup.deep_symbolize_keys! }
- end
-
- def test_symbolize_keys_preserves_keys_that_cant_be_symbolized_for_hash_with_indifferent_access
- assert_equal @illegal_symbols, @illegal_symbols.with_indifferent_access.symbolize_keys
- assert_raise(NoMethodError) { @illegal_symbols.with_indifferent_access.dup.symbolize_keys! }
- end
-
- def test_deep_symbolize_keys_preserves_keys_that_cant_be_symbolized_for_hash_with_indifferent_access
- assert_equal @nested_illegal_symbols, @nested_illegal_symbols.with_indifferent_access.deep_symbolize_keys
- assert_raise(NoMethodError) { @nested_illegal_symbols.with_indifferent_access.deep_dup.deep_symbolize_keys! }
- end
-
- def test_symbolize_keys_preserves_fixnum_keys_for_hash_with_indifferent_access
- assert_equal @fixnums, @fixnums.with_indifferent_access.symbolize_keys
- assert_raise(NoMethodError) { @fixnums.with_indifferent_access.dup.symbolize_keys! }
- end
-
- def test_deep_symbolize_keys_preserves_fixnum_keys_for_hash_with_indifferent_access
- assert_equal @nested_fixnums, @nested_fixnums.with_indifferent_access.deep_symbolize_keys
- assert_raise(NoMethodError) { @nested_fixnums.with_indifferent_access.deep_dup.deep_symbolize_keys! }
- end
-
- def test_stringify_keys_for_hash_with_indifferent_access
- assert_instance_of ActiveSupport::HashWithIndifferentAccess, @symbols.with_indifferent_access.stringify_keys
- assert_equal @strings, @symbols.with_indifferent_access.stringify_keys
- assert_equal @strings, @strings.with_indifferent_access.stringify_keys
- assert_equal @strings, @mixed.with_indifferent_access.stringify_keys
- end
-
- def test_deep_stringify_keys_for_hash_with_indifferent_access
- assert_instance_of ActiveSupport::HashWithIndifferentAccess, @nested_symbols.with_indifferent_access.deep_stringify_keys
- assert_equal @nested_strings, @nested_symbols.with_indifferent_access.deep_stringify_keys
- assert_equal @nested_strings, @nested_strings.with_indifferent_access.deep_stringify_keys
- assert_equal @nested_strings, @nested_mixed.with_indifferent_access.deep_stringify_keys
- end
-
- def test_stringify_keys_bang_for_hash_with_indifferent_access
- assert_instance_of ActiveSupport::HashWithIndifferentAccess, @symbols.with_indifferent_access.dup.stringify_keys!
- assert_equal @strings, @symbols.with_indifferent_access.dup.stringify_keys!
- assert_equal @strings, @strings.with_indifferent_access.dup.stringify_keys!
- assert_equal @strings, @mixed.with_indifferent_access.dup.stringify_keys!
- end
-
- def test_deep_stringify_keys_bang_for_hash_with_indifferent_access
- assert_instance_of ActiveSupport::HashWithIndifferentAccess, @nested_symbols.with_indifferent_access.dup.deep_stringify_keys!
- assert_equal @nested_strings, @nested_symbols.with_indifferent_access.deep_dup.deep_stringify_keys!
- assert_equal @nested_strings, @nested_strings.with_indifferent_access.deep_dup.deep_stringify_keys!
- assert_equal @nested_strings, @nested_mixed.with_indifferent_access.deep_dup.deep_stringify_keys!
- end
-
- def test_nested_under_indifferent_access
- foo = { "foo" => SubclassingHash.new.tap { |h| h["bar"] = "baz" } }.with_indifferent_access
- assert_kind_of ActiveSupport::HashWithIndifferentAccess, foo["foo"]
-
- foo = { "foo" => NonIndifferentHash.new.tap { |h| h["bar"] = "baz" } }.with_indifferent_access
- assert_kind_of NonIndifferentHash, foo["foo"]
-
- foo = { "foo" => IndifferentHash.new.tap { |h| h["bar"] = "baz" } }.with_indifferent_access
- assert_kind_of IndifferentHash, foo["foo"]
- end
-
- def test_indifferent_assorted
- @strings = @strings.with_indifferent_access
- @symbols = @symbols.with_indifferent_access
- @mixed = @mixed.with_indifferent_access
-
- assert_equal 'a', @strings.__send__(:convert_key, :a)
-
- assert_equal 1, @strings.fetch('a')
- assert_equal 1, @strings.fetch(:a.to_s)
- assert_equal 1, @strings.fetch(:a)
-
- hashes = { :@strings => @strings, :@symbols => @symbols, :@mixed => @mixed }
- method_map = { :'[]' => 1, :fetch => 1, :values_at => [1],
- :has_key? => true, :include? => true, :key? => true,
- :member? => true }
-
- hashes.each do |name, hash|
- method_map.sort_by(&:to_s).each do |meth, expected|
- assert_equal(expected, hash.__send__(meth, 'a'),
- "Calling #{name}.#{meth} 'a'")
- assert_equal(expected, hash.__send__(meth, :a),
- "Calling #{name}.#{meth} :a")
- end
- end
-
- assert_equal [1, 2], @strings.values_at('a', 'b')
- assert_equal [1, 2], @strings.values_at(:a, :b)
- assert_equal [1, 2], @symbols.values_at('a', 'b')
- assert_equal [1, 2], @symbols.values_at(:a, :b)
- assert_equal [1, 2], @mixed.values_at('a', 'b')
- assert_equal [1, 2], @mixed.values_at(:a, :b)
- end
-
- def test_indifferent_reading
- hash = HashWithIndifferentAccess.new
- hash["a"] = 1
- hash["b"] = true
- hash["c"] = false
- hash["d"] = nil
-
- 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]
- end
-
- def test_indifferent_reading_with_nonnil_default
- hash = HashWithIndifferentAccess.new(1)
- hash["a"] = 1
- hash["b"] = true
- hash["c"] = false
- hash["d"] = nil
-
- assert_equal 1, hash[:a]
- assert_equal true, hash[:b]
- assert_equal false, hash[:c]
- assert_equal nil, hash[:d]
- assert_equal 1, hash[:e]
- end
-
- def test_indifferent_writing
- hash = HashWithIndifferentAccess.new
- hash[:a] = 1
- 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
- end
-
- def test_indifferent_update
- hash = HashWithIndifferentAccess.new
- hash[:a] = 'a'
- hash['b'] = 'b'
-
- updated_with_strings = hash.update(@strings)
- 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 updated_with_symbols[:a], 1
- assert_equal updated_with_symbols['b'], 2
- assert_equal updated_with_symbols[:b], 2
-
- assert_equal updated_with_mixed[:a], 1
- assert_equal updated_with_mixed['b'], 2
-
- assert [updated_with_strings, updated_with_symbols, updated_with_mixed].all? { |h| h.keys.size == 2 }
- end
-
- def test_update_with_to_hash_conversion
- hash = HashWithIndifferentAccess.new
- hash.update HashByConversion.new({ :a => 1 })
- assert_equal hash['a'], 1
- end
-
- def test_indifferent_merging
- hash = HashWithIndifferentAccess.new
- hash[:a] = 'failure'
- hash['b'] = 'failure'
-
- other = { 'a' => 1, :b => 2 }
-
- merged = hash.merge(other)
-
- assert_equal HashWithIndifferentAccess, merged.class
- assert_equal 1, merged[:a]
- assert_equal 2, merged['b']
-
- hash.update(other)
-
- assert_equal 1, hash[:a]
- assert_equal 2, hash['b']
- end
-
- def test_merge_with_to_hash_conversion
- hash = HashWithIndifferentAccess.new
- merged = hash.merge HashByConversion.new({ :a => 1 })
- assert_equal merged['a'], 1
- end
-
- def test_indifferent_replace
- hash = HashWithIndifferentAccess.new
- hash[:a] = 42
-
- replaced = hash.replace(b: 12)
-
- assert hash.key?('b')
- assert !hash.key?(:a)
- assert_equal 12, hash[:b]
- assert_same hash, replaced
- end
-
- def test_replace_with_to_hash_conversion
- hash = HashWithIndifferentAccess.new
- hash[:a] = 42
-
- replaced = hash.replace(HashByConversion.new(b: 12))
-
- assert hash.key?('b')
- assert !hash.key?(:a)
- assert_equal 12, hash[:b]
- assert_same hash, replaced
- end
-
- def test_indifferent_merging_with_block
- hash = HashWithIndifferentAccess.new
- hash[:a] = 1
- hash['b'] = 3
-
- other = { 'a' => 4, :b => 2, 'c' => 10 }
-
- merged = hash.merge(other) { |key, old, new| old > new ? old : new }
-
- assert_equal HashWithIndifferentAccess, merged.class
- assert_equal 4, merged[:a]
- assert_equal 3, merged['b']
- assert_equal 10, merged[:c]
-
- other_indifferent = HashWithIndifferentAccess.new('a' => 9, :b => 2)
-
- merged = hash.merge(other_indifferent) { |key, old, new| old + new }
-
- assert_equal HashWithIndifferentAccess, merged.class
- assert_equal 10, merged[:a]
- assert_equal 5, merged[:b]
- end
-
- def test_indifferent_reverse_merging
- hash = HashWithIndifferentAccess.new key: :old_value
- hash.reverse_merge! key: :new_value
- assert_equal :old_value, hash[:key]
-
- hash = HashWithIndifferentAccess.new('some' => 'value', 'other' => 'value')
- hash.reverse_merge!(:some => 'noclobber', :another => 'clobber')
- assert_equal 'value', hash[:some]
- assert_equal 'clobber', hash[:another]
- end
-
- 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
- hash = get_hash.call
- assert_equal hash.delete('a'), 'foo'
- assert_equal hash.delete('a'), nil
- end
-
- def test_indifferent_select
- hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).select {|k,v| v == 1}
-
- assert_equal({ 'a' => 1 }, hash)
- assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash
- end
-
- def test_indifferent_select_returns_a_hash_when_unchanged
- hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).select {|k,v| true}
-
- assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash
- end
-
- def test_indifferent_select_bang
- indifferent_strings = ActiveSupport::HashWithIndifferentAccess.new(@strings)
- indifferent_strings.select! {|k,v| v == 1}
-
- assert_equal({ 'a' => 1 }, indifferent_strings)
- assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings
- end
-
- def test_indifferent_reject
- hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).reject {|k,v| v != 1}
-
- assert_equal({ 'a' => 1 }, hash)
- assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash
- end
-
- def test_indifferent_reject_bang
- indifferent_strings = ActiveSupport::HashWithIndifferentAccess.new(@strings)
- indifferent_strings.reject! {|k,v| v != 1}
-
- assert_equal({ 'a' => 1 }, indifferent_strings)
- assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings
- end
-
- def test_indifferent_to_hash
- # Should convert to a Hash with String keys.
- assert_equal @strings, @mixed.with_indifferent_access.to_hash
-
- # Should preserve the default value.
- mixed_with_default = @mixed.dup
- mixed_with_default.default = '1234'
- roundtrip = mixed_with_default.with_indifferent_access.to_hash
- assert_equal @strings, roundtrip
- assert_equal '1234', roundtrip.default
-
- # Ensure nested hashes are not HashWithIndiffereneAccess
- new_to_hash = @nested_mixed.with_indifferent_access.to_hash
- assert_not new_to_hash.instance_of?(HashWithIndifferentAccess)
- assert_not new_to_hash["a"].instance_of?(HashWithIndifferentAccess)
- assert_not new_to_hash["a"]["b"].instance_of?(HashWithIndifferentAccess)
- end
-
- def test_lookup_returns_the_same_object_that_is_stored_in_hash_indifferent_access
- hash = HashWithIndifferentAccess.new {|h, k| h[k] = []}
- hash[:a] << 1
-
- assert_equal [1], hash[:a]
- end
-
- def test_with_indifferent_access_has_no_side_effects_on_existing_hash
- hash = {content: [{:foo => :bar, 'bar' => 'baz'}]}
- hash.with_indifferent_access
-
- assert_equal [:foo, "bar"], hash[:content].first.keys
- end
-
- def test_indifferent_hash_with_array_of_hashes
- hash = { "urls" => { "url" => [ { "address" => "1" }, { "address" => "2" } ] }}.with_indifferent_access
- assert_equal "1", hash[:urls][:url].first[:address]
-
- hash = hash.to_hash
- assert_not hash.instance_of?(HashWithIndifferentAccess)
- assert_not hash["urls"].instance_of?(HashWithIndifferentAccess)
- assert_not hash["urls"]["url"].first.instance_of?(HashWithIndifferentAccess)
- end
-
- def test_should_preserve_array_subclass_when_value_is_array
- array = SubclassingArray.new
- array << { "address" => "1" }
- hash = { "urls" => { "url" => array }}.with_indifferent_access
- assert_equal SubclassingArray, hash[:urls][:url].class
- end
-
- def test_should_preserve_array_class_when_hash_value_is_frozen_array
- array = SubclassingArray.new
- array << { "address" => "1" }
- hash = { "urls" => { "url" => array.freeze }}.with_indifferent_access
- assert_equal SubclassingArray, hash[:urls][:url].class
- end
-
- def test_stringify_and_symbolize_keys_on_indifferent_preserves_hash
- h = HashWithIndifferentAccess.new
- h[:first] = 1
- h = h.stringify_keys
- assert_equal 1, h['first']
- h = HashWithIndifferentAccess.new
- h['first'] = 1
- h = h.symbolize_keys
- assert_equal 1, h[:first]
- end
-
- def test_deep_stringify_and_deep_symbolize_keys_on_indifferent_preserves_hash
- h = HashWithIndifferentAccess.new
- h[:first] = 1
- h = h.deep_stringify_keys
- assert_equal 1, h['first']
- h = HashWithIndifferentAccess.new
- h['first'] = 1
- h = h.deep_symbolize_keys
- assert_equal 1, h[:first]
- end
-
- def test_to_options_on_indifferent_preserves_hash
- h = HashWithIndifferentAccess.new
- h['first'] = 1
- h.to_options!
- assert_equal 1, h['first']
- end
-
- def test_to_options_on_indifferent_preserves_works_as_hash_with_dup
- h = HashWithIndifferentAccess.new({ a: { b: 'b' } })
- dup = h.dup
-
- dup[:a][:c] = 'c'
- assert_equal 'c', h[:a][:c]
- end
-
- def test_indifferent_sub_hashes
- h = {'user' => {'id' => 5}}.with_indifferent_access
- ['user', :user].each {|user| [:id, 'id'].each {|id| assert_equal 5, h[user][id], "h[#{user.inspect}][#{id.inspect}] should be 5"}}
-
- h = {:user => {:id => 5}}.with_indifferent_access
- ['user', :user].each {|user| [:id, 'id'].each {|id| assert_equal 5, h[user][id], "h[#{user.inspect}][#{id.inspect}] should be 5"}}
- end
-
- def test_indifferent_duplication
- # Should preserve default value
- h = HashWithIndifferentAccess.new
- h.default = '1234'
- assert_equal h.default, h.dup.default
-
- # Should preserve class for subclasses
- h = IndifferentHash.new
- assert_equal h.class, h.dup.class
+ assert_equal({ "a" => { b: { "c" => 3 } } }, @nested_mixed)
end
def test_assert_valid_keys
assert_nothing_raised do
- { :failure => "stuff", :funny => "business" }.assert_valid_keys([ :failure, :funny ])
- { :failure => "stuff", :funny => "business" }.assert_valid_keys(:failure, :funny)
+ { failure: "stuff", funny: "business" }.assert_valid_keys([ :failure, :funny ])
+ { failure: "stuff", funny: "business" }.assert_valid_keys(:failure, :funny)
end
# not all valid keys are required to be present
assert_nothing_raised do
- { :failure => "stuff", :funny => "business" }.assert_valid_keys([ :failure, :funny, :sunny ])
- { :failure => "stuff", :funny => "business" }.assert_valid_keys(:failure, :funny, :sunny)
+ { failure: "stuff", funny: "business" }.assert_valid_keys([ :failure, :funny, :sunny ])
+ { failure: "stuff", funny: "business" }.assert_valid_keys(:failure, :funny, :sunny)
end
exception = assert_raise ArgumentError do
- { :failore => "stuff", :funny => "business" }.assert_valid_keys([ :failure, :funny ])
+ { failore: "stuff", funny: "business" }.assert_valid_keys([ :failure, :funny ])
end
assert_equal "Unknown key: :failore. Valid keys are: :failure, :funny", exception.message
exception = assert_raise ArgumentError do
- { :failore => "stuff", :funny => "business" }.assert_valid_keys(:failure, :funny)
+ { failore: "stuff", funny: "business" }.assert_valid_keys(:failure, :funny)
end
assert_equal "Unknown key: :failore. Valid keys are: :failure, :funny", exception.message
exception = assert_raise ArgumentError do
- { :failore => "stuff", :funny => "business" }.assert_valid_keys([ :failure ])
+ { failore: "stuff", funny: "business" }.assert_valid_keys([ :failure ])
end
assert_equal "Unknown key: :failore. Valid keys are: :failure", exception.message
exception = assert_raise ArgumentError do
- { :failore => "stuff", :funny => "business" }.assert_valid_keys(:failure)
+ { failore: "stuff", funny: "business" }.assert_valid_keys(:failure)
end
assert_equal "Unknown key: :failore. Valid keys are: :failure", exception.message
end
- def test_assorted_keys_not_stringified
- original = {Object.new => 2, 1 => 2, [] => true}
- indiff = original.with_indifferent_access
- assert(!indiff.keys.any? {|k| k.kind_of? String}, "A key was converted to a string!")
- end
-
def test_deep_merge
- hash_1 = { :a => "a", :b => "b", :c => { :c1 => "c1", :c2 => "c2", :c3 => { :d1 => "d1" } } }
- hash_2 = { :a => 1, :c => { :c1 => 2, :c3 => { :d2 => "d2" } } }
- expected = { :a => 1, :b => "b", :c => { :c1 => 2, :c2 => "c2", :c3 => { :d1 => "d1", :d2 => "d2" } } }
+ hash_1 = { a: "a", b: "b", c: { c1: "c1", c2: "c2", c3: { d1: "d1" } } }
+ hash_2 = { a: 1, c: { c1: 2, c3: { d2: "d2" } } }
+ expected = { a: 1, b: "b", c: { c1: 2, c2: "c2", c3: { d1: "d1", d2: "d2" } } }
assert_equal expected, hash_1.deep_merge(hash_2)
hash_1.deep_merge!(hash_2)
@@ -741,60 +280,29 @@ class HashExtTest < ActiveSupport::TestCase
end
def test_deep_merge_with_block
- hash_1 = { :a => "a", :b => "b", :c => { :c1 => "c1", :c2 => "c2", :c3 => { :d1 => "d1" } } }
- hash_2 = { :a => 1, :c => { :c1 => 2, :c3 => { :d2 => "d2" } } }
- expected = { :a => [:a, "a", 1], :b => "b", :c => { :c1 => [:c1, "c1", 2], :c2 => "c2", :c3 => { :d1 => "d1", :d2 => "d2" } } }
- assert_equal(expected, hash_1.deep_merge(hash_2) { |k,o,n| [k, o, n] })
+ hash_1 = { a: "a", b: "b", c: { c1: "c1", c2: "c2", c3: { d1: "d1" } } }
+ hash_2 = { a: 1, c: { c1: 2, c3: { d2: "d2" } } }
+ expected = { a: [:a, "a", 1], b: "b", c: { c1: [:c1, "c1", 2], c2: "c2", c3: { d1: "d1", d2: "d2" } } }
+ assert_equal(expected, hash_1.deep_merge(hash_2) { |k, o, n| [k, o, n] })
- hash_1.deep_merge!(hash_2) { |k,o,n| [k, o, n] }
+ hash_1.deep_merge!(hash_2) { |k, o, n| [k, o, n] }
assert_equal expected, hash_1
end
def test_deep_merge_with_falsey_values
hash_1 = { e: false }
- hash_2 = { e: 'e' }
- expected = { e: [:e, false, 'e'] }
+ hash_2 = { e: "e" }
+ expected = { e: [:e, false, "e"] }
assert_equal(expected, hash_1.deep_merge(hash_2) { |k, o, n| [k, o, n] })
hash_1.deep_merge!(hash_2) { |k, o, n| [k, o, n] }
assert_equal expected, hash_1
end
- def test_deep_merge_on_indifferent_access
- hash_1 = HashWithIndifferentAccess.new({ :a => "a", :b => "b", :c => { :c1 => "c1", :c2 => "c2", :c3 => { :d1 => "d1" } } })
- hash_2 = HashWithIndifferentAccess.new({ :a => 1, :c => { :c1 => 2, :c3 => { :d2 => "d2" } } })
- hash_3 = { :a => 1, :c => { :c1 => 2, :c3 => { :d2 => "d2" } } }
- expected = { "a" => 1, "b" => "b", "c" => { "c1" => 2, "c2" => "c2", "c3" => { "d1" => "d1", "d2" => "d2" } } }
- assert_equal expected, hash_1.deep_merge(hash_2)
- assert_equal expected, hash_1.deep_merge(hash_3)
-
- hash_1.deep_merge!(hash_2)
- assert_equal expected, hash_1
- end
-
- def test_store_on_indifferent_access
- hash = HashWithIndifferentAccess.new
- hash.store(:test1, 1)
- hash.store('test1', 11)
- hash[:test2] = 2
- hash['test2'] = 22
- expected = { "test1" => 11, "test2" => 22 }
- assert_equal expected, hash
- end
-
- def test_constructor_on_indifferent_access
- hash = HashWithIndifferentAccess[:foo, 1]
- assert_equal 1, hash[:foo]
- assert_equal 1, hash['foo']
- hash[:foo] = 3
- assert_equal 3, hash[:foo]
- assert_equal 3, hash['foo']
- end
-
def test_reverse_merge
- defaults = { :a => "x", :b => "y", :c => 10 }.freeze
- options = { :a => 1, :b => 2 }
- expected = { :a => 1, :b => 2, :c => 10 }
+ defaults = { d: 0, a: "x", b: "y", c: 10 }.freeze
+ options = { a: 1, b: 2 }
+ expected = { d: 0, a: 1, b: 2, c: 10 }
# Should merge defaults into options, creating a new hash.
assert_equal expected, options.reverse_merge(defaults)
@@ -805,15 +313,33 @@ class HashExtTest < ActiveSupport::TestCase
assert_equal expected, merged.reverse_merge!(defaults)
assert_equal expected, merged
+ # Make the order consistent with the non-overwriting reverse merge.
+ assert_equal expected.keys, merged.keys
+
# Should be an alias for reverse_merge!
merged = options.dup
assert_equal expected, merged.reverse_update(defaults)
assert_equal expected, merged
end
+ def test_with_defaults_aliases_reverse_merge
+ defaults = { a: "x", b: "y", c: 10 }.freeze
+ options = { a: 1, b: 2 }
+ expected = { a: 1, b: 2, c: 10 }
+
+ # Should be an alias for reverse_merge
+ assert_equal expected, options.with_defaults(defaults)
+ assert_not_equal expected, options
+
+ # Should be an alias for reverse_merge!
+ merged = options.dup
+ assert_equal expected, merged.with_defaults!(defaults)
+ assert_equal expected, merged
+ end
+
def test_slice
- original = { :a => 'x', :b => 'y', :c => 10 }
- expected = { :a => 'x', :b => 'y' }
+ original = { a: "x", b: "y", c: 10 }
+ expected = { a: "x", b: "y" }
# Should return a new hash with only the given keys.
assert_equal expected, original.slice(:a, :b)
@@ -821,15 +347,15 @@ class HashExtTest < ActiveSupport::TestCase
end
def test_slice_inplace
- original = { :a => 'x', :b => 'y', :c => 10 }
- expected = { :c => 10 }
+ original = { a: "x", b: "y", c: 10 }
+ expected = { c: 10 }
# Should replace the hash with only the given keys.
assert_equal expected, original.slice!(:a, :b)
end
def test_slice_with_an_array_key
- original = { :a => 'x', :b => 'y', :c => 10, [:a, :b] => "an array key" }
+ original = { :a => "x", :b => "y", :c => 10, [:a, :b] => "an array key" }
expected = { [:a, :b] => "an array key", :c => 10 }
# Should return a new hash with only the given keys when given an array key.
@@ -838,53 +364,21 @@ class HashExtTest < ActiveSupport::TestCase
end
def test_slice_inplace_with_an_array_key
- original = { :a => 'x', :b => 'y', :c => 10, [:a, :b] => "an array key" }
- expected = { :a => 'x', :b => 'y' }
+ original = { :a => "x", :b => "y", :c => 10, [:a, :b] => "an array key" }
+ expected = { a: "x", b: "y" }
# Should replace the hash with only the given keys when given an array key.
assert_equal expected, original.slice!([:a, :b], :c)
end
def test_slice_with_splatted_keys
- original = { :a => 'x', :b => 'y', :c => 10, [:a, :b] => "an array key" }
- expected = { :a => 'x', :b => "y" }
+ original = { :a => "x", :b => "y", :c => 10, [:a, :b] => "an array key" }
+ expected = { a: "x", b: "y" }
# Should grab each of the splatted keys.
assert_equal expected, original.slice(*[:a, :b])
end
- def test_indifferent_slice
- original = { :a => 'x', :b => 'y', :c => 10 }.with_indifferent_access
- expected = { :a => 'x', :b => 'y' }.with_indifferent_access
-
- [['a', 'b'], [:a, :b]].each do |keys|
- # Should return a new hash with only the given keys.
- assert_equal expected, original.slice(*keys), keys.inspect
- assert_not_equal expected, original
- end
- end
-
- def test_indifferent_slice_inplace
- original = { :a => 'x', :b => 'y', :c => 10 }.with_indifferent_access
- expected = { :c => 10 }.with_indifferent_access
-
- [['a', 'b'], [:a, :b]].each do |keys|
- # Should replace the hash with only the given keys.
- copy = original.dup
- assert_equal expected, copy.slice!(*keys)
- end
- end
-
- def test_indifferent_slice_access_with_symbols
- original = {'login' => 'bender', 'password' => 'shiny', 'stuff' => 'foo'}
- original = original.with_indifferent_access
-
- slice = original.slice(:login, :password)
-
- assert_equal 'bender', slice[:login]
- assert_equal 'bender', slice['login']
- end
-
def test_slice_bang_does_not_override_default
hash = Hash.new(0)
hash.update(a: 1, b: 2)
@@ -904,39 +398,27 @@ class HashExtTest < ActiveSupport::TestCase
end
def test_extract
- original = {:a => 1, :b => 2, :c => 3, :d => 4}
- expected = {:a => 1, :b => 2}
- remaining = {:c => 3, :d => 4}
+ original = { a: 1, b: 2, c: 3, d: 4 }
+ expected = { a: 1, b: 2 }
+ remaining = { c: 3, d: 4 }
assert_equal expected, original.extract!(:a, :b, :x)
assert_equal remaining, original
end
def test_extract_nils
- original = {:a => nil, :b => nil}
- expected = {:a => nil}
+ original = { a: nil, b: nil }
+ expected = { a: nil }
extracted = original.extract!(:a, :x)
assert_equal expected, extracted
- assert_equal nil, extracted[:a]
- assert_equal nil, extracted[:x]
- end
-
- def test_indifferent_extract
- original = {:a => 1, 'b' => 2, :c => 3, 'd' => 4}.with_indifferent_access
- expected = {:a => 1, :b => 2}.with_indifferent_access
- remaining = {:c => 3, :d => 4}.with_indifferent_access
-
- [['a', 'b'], [:a, :b]].each do |keys|
- copy = original.dup
- assert_equal expected, copy.extract!(*keys)
- assert_equal remaining, copy
- end
+ assert_nil extracted[:a]
+ assert_nil extracted[:x]
end
def test_except
- original = { :a => 'x', :b => 'y', :c => 10 }
- expected = { :a => 'x', :b => 'y' }
+ original = { a: "x", b: "y", c: 10 }
+ expected = { a: "x", b: "y" }
# Should return a new hash without the given keys.
assert_equal expected, original.except(:c)
@@ -948,8 +430,8 @@ class HashExtTest < ActiveSupport::TestCase
end
def test_except_with_more_than_one_argument
- original = { :a => 'x', :b => 'y', :c => 10 }
- expected = { :a => 'x' }
+ original = { a: "x", b: "y", c: 10 }
+ expected = { a: "x" }
assert_equal expected, original.except(:b, :c)
@@ -958,91 +440,34 @@ class HashExtTest < ActiveSupport::TestCase
end
def test_except_with_original_frozen
- original = { :a => 'x', :b => 'y' }
+ original = { a: "x", b: "y" }
original.freeze
assert_nothing_raised { original.except(:a) }
- assert_raise(RuntimeError) { original.except!(:a) }
+ assert_raise(frozen_error_class) { original.except!(:a) }
end
def test_except_does_not_delete_values_in_original
- original = { :a => 'x', :b => 'y' }
+ original = { a: "x", b: "y" }
assert_not_called(original, :delete) do
original.except(:a)
end
end
- def test_compact
- hash_contain_nil_value = @symbols.merge(z: nil)
- hash_with_only_nil_values = { a: nil, b: nil }
-
- h = hash_contain_nil_value.dup
- assert_equal(@symbols, h.compact)
- assert_equal(hash_contain_nil_value, h)
-
- h = hash_with_only_nil_values.dup
- assert_equal({}, h.compact)
- assert_equal(hash_with_only_nil_values, h)
- end
-
- def test_compact!
- hash_contain_nil_value = @symbols.merge(z: nil)
- hash_with_only_nil_values = { a: nil, b: nil }
-
- h = hash_contain_nil_value.dup
- assert_equal(@symbols, h.compact!)
- assert_equal(@symbols, h)
-
- h = hash_with_only_nil_values.dup
- assert_equal({}, h.compact!)
- assert_equal({}, h)
- end
-
- def test_new_with_to_hash_conversion
- hash = HashWithIndifferentAccess.new(HashByConversion.new(a: 1))
- assert hash.key?('a')
- assert_equal 1, hash[:a]
- end
-
- def test_dup_with_default_proc
- hash = HashWithIndifferentAccess.new
- hash.default_proc = proc { |h, v| raise "walrus" }
- assert_nothing_raised { hash.dup }
- end
-
- def test_dup_with_default_proc_sets_proc
- hash = HashWithIndifferentAccess.new
- hash.default_proc = proc { |h, k| k + 1 }
- new_hash = hash.dup
-
- assert_equal 3, new_hash[2]
-
- new_hash.default = 2
- assert_equal 2, new_hash[:non_existant]
- end
-
- def test_to_hash_with_raising_default_proc
- hash = HashWithIndifferentAccess.new
- hash.default_proc = proc { |h, k| raise "walrus" }
-
- assert_nothing_raised { hash.to_hash }
- end
-
- def test_new_from_hash_copying_default_should_not_raise_when_default_proc_does
- hash = Hash.new
- hash.default_proc = proc { |h, k| raise "walrus" }
-
- assert_nothing_raised { HashWithIndifferentAccess.new_from_hash_copying_default(hash) }
+ def test_requiring_compact_is_deprecated
+ assert_deprecated do
+ require "active_support/core_ext/hash/compact"
+ end
end
end
class IWriteMyOwnXML
def to_xml(options = {})
options[:indent] ||= 2
- xml = options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
+ xml = options[:builder] ||= Builder::XmlMarkup.new(indent: options[:indent])
xml.instruct! unless options[:skip_instruct]
xml.level_one do
- xml.tag!(:second_level, 'content')
+ xml.tag!(:second_level, "content")
end
end
end
@@ -1055,139 +480,139 @@ class HashExtToParamTests < ActiveSupport::TestCase
end
def test_string_hash
- assert_equal '', {}.to_param
- assert_equal 'hello=world', { :hello => "world" }.to_param
- assert_equal 'hello=10', { "hello" => 10 }.to_param
- assert_equal 'hello=world&say_bye=true', {:hello => "world", "say_bye" => true}.to_param
+ assert_equal "", {}.to_param
+ assert_equal "hello=world", { hello: "world" }.to_param
+ assert_equal "hello=10", { "hello" => 10 }.to_param
+ assert_equal "hello=world&say_bye=true", { :hello => "world", "say_bye" => true }.to_param
end
def test_number_hash
- assert_equal '10=20&30=40&50=60', {10 => 20, 30 => 40, 50 => 60}.to_param
+ assert_equal "10=20&30=40&50=60", { 10 => 20, 30 => 40, 50 => 60 }.to_param
end
def test_to_param_hash
- assert_equal 'custom-1=param-1&custom2-1=param2-1', {ToParam.new('custom') => ToParam.new('param'), ToParam.new('custom2') => ToParam.new('param2')}.to_param
+ assert_equal "custom-1=param-1&custom2-1=param2-1", { ToParam.new("custom") => ToParam.new("param"), ToParam.new("custom2") => ToParam.new("param2") }.to_param
end
def test_to_param_hash_escapes_its_keys_and_values
- assert_equal 'param+1=A+string+with+%2F+characters+%26+that+should+be+%3F+escaped', { 'param 1' => 'A string with / characters & that should be ? escaped' }.to_param
+ assert_equal "param+1=A+string+with+%2F+characters+%26+that+should+be+%3F+escaped", { "param 1" => "A string with / characters & that should be ? escaped" }.to_param
end
def test_to_param_orders_by_key_in_ascending_order
- assert_equal 'a=2&b=1&c=0', Hash[*%w(b 1 c 0 a 2)].to_param
+ assert_equal "a=2&b=1&c=0", Hash[*%w(b 1 c 0 a 2)].to_param
end
end
class HashToXmlTest < ActiveSupport::TestCase
def setup
- @xml_options = { :root => :person, :skip_instruct => true, :indent => 0 }
+ @xml_options = { root: :person, skip_instruct: true, indent: 0 }
end
def test_one_level
- xml = { :name => "David", :street => "Paulina" }.to_xml(@xml_options)
+ xml = { name: "David", street: "Paulina" }.to_xml(@xml_options)
assert_equal "<person>", xml.first(8)
- assert xml.include?(%(<street>Paulina</street>))
- assert xml.include?(%(<name>David</name>))
+ assert_includes xml, %(<street>Paulina</street>)
+ assert_includes xml, %(<name>David</name>)
end
def test_one_level_dasherize_false
- xml = { :name => "David", :street_name => "Paulina" }.to_xml(@xml_options.merge(:dasherize => false))
+ xml = { name: "David", street_name: "Paulina" }.to_xml(@xml_options.merge(dasherize: false))
assert_equal "<person>", xml.first(8)
- assert xml.include?(%(<street_name>Paulina</street_name>))
- assert xml.include?(%(<name>David</name>))
+ assert_includes xml, %(<street_name>Paulina</street_name>)
+ assert_includes xml, %(<name>David</name>)
end
def test_one_level_dasherize_true
- xml = { :name => "David", :street_name => "Paulina" }.to_xml(@xml_options.merge(:dasherize => true))
+ xml = { name: "David", street_name: "Paulina" }.to_xml(@xml_options.merge(dasherize: true))
assert_equal "<person>", xml.first(8)
- assert xml.include?(%(<street-name>Paulina</street-name>))
- assert xml.include?(%(<name>David</name>))
+ assert_includes xml, %(<street-name>Paulina</street-name>)
+ assert_includes xml, %(<name>David</name>)
end
def test_one_level_camelize_true
- xml = { :name => "David", :street_name => "Paulina" }.to_xml(@xml_options.merge(:camelize => true))
+ xml = { name: "David", street_name: "Paulina" }.to_xml(@xml_options.merge(camelize: true))
assert_equal "<Person>", xml.first(8)
- assert xml.include?(%(<StreetName>Paulina</StreetName>))
- assert xml.include?(%(<Name>David</Name>))
+ assert_includes xml, %(<StreetName>Paulina</StreetName>)
+ assert_includes xml, %(<Name>David</Name>)
end
def test_one_level_camelize_lower
- xml = { :name => "David", :street_name => "Paulina" }.to_xml(@xml_options.merge(:camelize => :lower))
+ xml = { name: "David", street_name: "Paulina" }.to_xml(@xml_options.merge(camelize: :lower))
assert_equal "<person>", xml.first(8)
- assert xml.include?(%(<streetName>Paulina</streetName>))
- assert xml.include?(%(<name>David</name>))
+ assert_includes xml, %(<streetName>Paulina</streetName>)
+ assert_includes xml, %(<name>David</name>)
end
def test_one_level_with_types
- xml = { :name => "David", :street => "Paulina", :age => 26, :age_in_millis => 820497600000, :moved_on => Date.new(2005, 11, 15), :resident => :yes }.to_xml(@xml_options)
+ xml = { name: "David", street: "Paulina", age: 26, age_in_millis: 820497600000, moved_on: Date.new(2005, 11, 15), resident: :yes }.to_xml(@xml_options)
assert_equal "<person>", xml.first(8)
- assert xml.include?(%(<street>Paulina</street>))
- assert xml.include?(%(<name>David</name>))
- assert xml.include?(%(<age type="integer">26</age>))
- assert xml.include?(%(<age-in-millis type="integer">820497600000</age-in-millis>))
- assert xml.include?(%(<moved-on type="date">2005-11-15</moved-on>))
- assert xml.include?(%(<resident type="symbol">yes</resident>))
+ assert_includes xml, %(<street>Paulina</street>)
+ assert_includes xml, %(<name>David</name>)
+ assert_includes xml, %(<age type="integer">26</age>)
+ assert_includes xml, %(<age-in-millis type="integer">820497600000</age-in-millis>)
+ assert_includes xml, %(<moved-on type="date">2005-11-15</moved-on>)
+ assert_includes xml, %(<resident type="symbol">yes</resident>)
end
def test_one_level_with_nils
- xml = { :name => "David", :street => "Paulina", :age => nil }.to_xml(@xml_options)
+ xml = { name: "David", street: "Paulina", age: nil }.to_xml(@xml_options)
assert_equal "<person>", xml.first(8)
- assert xml.include?(%(<street>Paulina</street>))
- assert xml.include?(%(<name>David</name>))
- assert xml.include?(%(<age nil="true"/>))
+ assert_includes xml, %(<street>Paulina</street>)
+ assert_includes xml, %(<name>David</name>)
+ assert_includes xml, %(<age nil="true"/>)
end
def test_one_level_with_skipping_types
- xml = { :name => "David", :street => "Paulina", :age => nil }.to_xml(@xml_options.merge(:skip_types => true))
+ xml = { name: "David", street: "Paulina", age: nil }.to_xml(@xml_options.merge(skip_types: true))
assert_equal "<person>", xml.first(8)
- assert xml.include?(%(<street>Paulina</street>))
- assert xml.include?(%(<name>David</name>))
- assert xml.include?(%(<age nil="true"/>))
+ assert_includes xml, %(<street>Paulina</street>)
+ assert_includes xml, %(<name>David</name>)
+ assert_includes xml, %(<age nil="true"/>)
end
def test_one_level_with_yielding
- xml = { :name => "David", :street => "Paulina" }.to_xml(@xml_options) do |x|
+ xml = { name: "David", street: "Paulina" }.to_xml(@xml_options) do |x|
x.creator("Rails")
end
assert_equal "<person>", xml.first(8)
- assert xml.include?(%(<street>Paulina</street>))
- assert xml.include?(%(<name>David</name>))
- assert xml.include?(%(<creator>Rails</creator>))
+ assert_includes xml, %(<street>Paulina</street>)
+ assert_includes xml, %(<name>David</name>)
+ assert_includes xml, %(<creator>Rails</creator>)
end
def test_two_levels
- xml = { :name => "David", :address => { :street => "Paulina" } }.to_xml(@xml_options)
+ xml = { name: "David", address: { street: "Paulina" } }.to_xml(@xml_options)
assert_equal "<person>", xml.first(8)
- assert xml.include?(%(<address><street>Paulina</street></address>))
- assert xml.include?(%(<name>David</name>))
+ assert_includes xml, %(<address><street>Paulina</street></address>)
+ assert_includes xml, %(<name>David</name>)
end
def test_two_levels_with_second_level_overriding_to_xml
- xml = { :name => "David", :address => { :street => "Paulina" }, :child => IWriteMyOwnXML.new }.to_xml(@xml_options)
+ xml = { name: "David", address: { street: "Paulina" }, child: IWriteMyOwnXML.new }.to_xml(@xml_options)
assert_equal "<person>", xml.first(8)
- assert xml.include?(%(<address><street>Paulina</street></address>))
- assert xml.include?(%(<level_one><second_level>content</second_level></level_one>))
+ assert_includes xml, %(<address><street>Paulina</street></address>)
+ assert_includes xml, %(<level_one><second_level>content</second_level></level_one>)
end
def test_two_levels_with_array
- xml = { :name => "David", :addresses => [{ :street => "Paulina" }, { :street => "Evergreen" }] }.to_xml(@xml_options)
+ xml = { name: "David", addresses: [{ street: "Paulina" }, { street: "Evergreen" }] }.to_xml(@xml_options)
assert_equal "<person>", xml.first(8)
- assert xml.include?(%(<addresses type="array"><address>))
- assert xml.include?(%(<address><street>Paulina</street></address>))
- assert xml.include?(%(<address><street>Evergreen</street></address>))
- assert xml.include?(%(<name>David</name>))
+ assert_includes xml, %(<addresses type="array"><address>)
+ assert_includes xml, %(<address><street>Paulina</street></address>)
+ assert_includes xml, %(<address><street>Evergreen</street></address>)
+ assert_includes xml, %(<name>David</name>)
end
def test_three_levels_with_array
- xml = { :name => "David", :addresses => [{ :streets => [ { :name => "Paulina" }, { :name => "Paulina" } ] } ] }.to_xml(@xml_options)
- assert xml.include?(%(<addresses type="array"><address><streets type="array"><street><name>))
+ xml = { name: "David", addresses: [{ streets: [ { name: "Paulina" }, { name: "Paulina" } ] } ] }.to_xml(@xml_options)
+ assert_includes xml, %(<addresses type="array"><address><streets type="array"><street><name>)
end
def test_timezoned_attributes
xml = {
- :created_at => Time.utc(1999,2,2),
- :local_created_at => Time.utc(1999,2,2).in_time_zone('Eastern Time (US & Canada)')
+ created_at: Time.utc(1999, 2, 2),
+ local_created_at: Time.utc(1999, 2, 2).in_time_zone("Eastern Time (US & Canada)")
}.to_xml(@xml_options)
assert_match %r{<created-at type=\"dateTime\">1999-02-02T00:00:00Z</created-at>}, xml
assert_match %r{<local-created-at type=\"dateTime\">1999-02-01T19:00:00-05:00</local-created-at>}, xml
@@ -1226,17 +651,17 @@ class HashToXmlTest < ActiveSupport::TestCase
EOT
expected_topic_hash = {
- :title => "The First Topic",
- :author_name => "David",
- :id => 1,
- :approved => false,
- :replies_count => 0,
- :replies_close_in => 2592000000,
- :written_on => Date.new(2003, 7, 16),
- :viewed_at => Time.utc(2003, 7, 16, 9, 28),
- :content => "Have a nice day",
- :author_email_address => "david@loudthinking.com",
- :parent_id => nil
+ title: "The First Topic",
+ author_name: "David",
+ id: 1,
+ approved: false,
+ replies_count: 0,
+ replies_close_in: 2592000000,
+ written_on: Date.new(2003, 7, 16),
+ viewed_at: Time.utc(2003, 7, 16, 9, 28),
+ content: "Have a nice day",
+ author_email_address: "david@loudthinking.com",
+ parent_id: nil
}.stringify_keys
assert_equal expected_topic_hash, Hash.from_xml(topics_xml)["topics"].first
@@ -1261,18 +686,18 @@ class HashToXmlTest < ActiveSupport::TestCase
EOT
expected_topic_hash = {
- :title => "The First Topic",
- :author_name => "David",
- :id => 1,
- :approved => true,
- :replies_count => 0,
- :replies_close_in => 2592000000,
- :written_on => Date.new(2003, 7, 16),
- :viewed_at => Time.utc(2003, 7, 16, 9, 28),
- :author_email_address => "david@loudthinking.com",
- :parent_id => nil,
- :ad_revenue => BigDecimal("1.50"),
- :optimum_viewing_angle => 135.0,
+ title: "The First Topic",
+ author_name: "David",
+ id: 1,
+ approved: true,
+ replies_count: 0,
+ replies_close_in: 2592000000,
+ written_on: Date.new(2003, 7, 16),
+ viewed_at: Time.utc(2003, 7, 16, 9, 28),
+ author_email_address: "david@loudthinking.com",
+ parent_id: nil,
+ ad_revenue: BigDecimal("1.50"),
+ optimum_viewing_angle: 135.0,
}.stringify_keys
assert_equal expected_topic_hash, Hash.from_xml(topic_xml)["topic"]
@@ -1291,12 +716,12 @@ class HashToXmlTest < ActiveSupport::TestCase
EOT
expected_topic_hash = {
- :title => nil,
- :id => nil,
- :approved => nil,
- :written_on => nil,
- :viewed_at => nil,
- :parent_id => nil
+ title: nil,
+ id: nil,
+ approved: nil,
+ written_on: nil,
+ viewed_at: nil,
+ parent_id: nil
}.stringify_keys
assert_equal expected_topic_hash, Hash.from_xml(topic_xml)["topic"]
@@ -1335,17 +760,17 @@ class HashToXmlTest < ActiveSupport::TestCase
EOT
expected_topic_hash = {
- :title => "The First Topic",
- :author_name => "David",
- :id => 1,
- :approved => false,
- :replies_count => 0,
- :replies_close_in => 2592000000,
- :written_on => Date.new(2003, 7, 16),
- :viewed_at => Time.utc(2003, 7, 16, 9, 28),
- :content => "Have a nice day",
- :author_email_address => "david@loudthinking.com",
- :parent_id => nil
+ title: "The First Topic",
+ author_name: "David",
+ id: 1,
+ approved: false,
+ replies_count: 0,
+ replies_close_in: 2592000000,
+ written_on: Date.new(2003, 7, 16),
+ viewed_at: Time.utc(2003, 7, 16, 9, 28),
+ content: "Have a nice day",
+ author_email_address: "david@loudthinking.com",
+ parent_id: nil
}.stringify_keys
assert_equal expected_topic_hash, Hash.from_xml(topics_xml)["topics"].first
@@ -1361,14 +786,14 @@ class HashToXmlTest < ActiveSupport::TestCase
EOT
expected_topic_hash = {
- :id => "175756086",
- :owner => "55569174@N00",
- :secret => "0279bf37a1",
- :server => "76",
- :title => "Colored Pencil PhotoBooth Fun",
- :ispublic => "1",
- :isfriend => "0",
- :isfamily => "0",
+ id: "175756086",
+ owner: "55569174@N00",
+ secret: "0279bf37a1",
+ server: "76",
+ title: "Colored Pencil PhotoBooth Fun",
+ ispublic: "1",
+ isfriend: "0",
+ isfamily: "0",
}.stringify_keys
assert_equal expected_topic_hash, Hash.from_xml(topic_xml)["rsp"]["photos"]["photo"]
@@ -1396,7 +821,7 @@ class HashToXmlTest < ActiveSupport::TestCase
<posts type="array"></posts>
</blog>
XML
- expected_blog_hash = {"blog" => {"posts" => []}}
+ expected_blog_hash = { "blog" => { "posts" => [] } }
assert_equal expected_blog_hash, Hash.from_xml(blog_xml)
end
@@ -1407,7 +832,7 @@ class HashToXmlTest < ActiveSupport::TestCase
</posts>
</blog>
XML
- expected_blog_hash = {"blog" => {"posts" => []}}
+ expected_blog_hash = { "blog" => { "posts" => [] } }
assert_equal expected_blog_hash, Hash.from_xml(blog_xml)
end
@@ -1419,7 +844,7 @@ class HashToXmlTest < ActiveSupport::TestCase
</posts>
</blog>
XML
- expected_blog_hash = {"blog" => {"posts" => ["a post"]}}
+ expected_blog_hash = { "blog" => { "posts" => ["a post"] } }
assert_equal expected_blog_hash, Hash.from_xml(blog_xml)
end
@@ -1432,7 +857,7 @@ class HashToXmlTest < ActiveSupport::TestCase
</posts>
</blog>
XML
- expected_blog_hash = {"blog" => {"posts" => ["a post", "another post"]}}
+ expected_blog_hash = { "blog" => { "posts" => ["a post", "another post"] } }
assert_equal expected_blog_hash, Hash.from_xml(blog_xml)
end
@@ -1444,12 +869,12 @@ class HashToXmlTest < ActiveSupport::TestCase
</blog>
XML
hash = Hash.from_xml(blog_xml)
- assert hash.has_key?('blog')
- assert hash['blog'].has_key?('logo')
+ assert hash.has_key?("blog")
+ assert hash["blog"].has_key?("logo")
- file = hash['blog']['logo']
- assert_equal 'logo.png', file.original_filename
- assert_equal 'image/png', file.content_type
+ file = hash["blog"]["logo"]
+ assert_equal "logo.png", file.original_filename
+ assert_equal "image/png", file.content_type
end
def test_file_from_xml_with_defaults
@@ -1459,9 +884,9 @@ class HashToXmlTest < ActiveSupport::TestCase
</logo>
</blog>
XML
- file = Hash.from_xml(blog_xml)['blog']['logo']
- assert_equal 'untitled', file.original_filename
- assert_equal 'application/octet-stream', file.content_type
+ file = Hash.from_xml(blog_xml)["blog"]["logo"]
+ assert_equal "untitled", file.original_filename
+ assert_equal "application/octet-stream", file.content_type
end
def test_tag_with_attrs_and_whitespace
@@ -1470,7 +895,7 @@ class HashToXmlTest < ActiveSupport::TestCase
</blog>
XML
hash = Hash.from_xml(xml)
- assert_equal "bacon is the best", hash['blog']['name']
+ assert_equal "bacon is the best", hash["blog"]["name"]
end
def test_empty_cdata_from_xml
@@ -1493,13 +918,13 @@ class HashToXmlTest < ActiveSupport::TestCase
EOT
expected_bacon_hash = {
- :weight => 0.5,
- :chunky => true,
- :price => BigDecimal("12.50"),
- :expires_at => Time.utc(2007,12,25,12,34,56),
- :notes => "",
- :illustration => "babe.png",
- :caption => "That'll do, pig."
+ weight: 0.5,
+ chunky: true,
+ price: BigDecimal("12.50"),
+ expires_at: Time.utc(2007, 12, 25, 12, 34, 56),
+ notes: "",
+ illustration: "babe.png",
+ caption: "That'll do, pig."
}.stringify_keys
assert_equal expected_bacon_hash, Hash.from_xml(bacon_xml)["bacon"]
@@ -1515,8 +940,8 @@ class HashToXmlTest < ActiveSupport::TestCase
EOT
expected_product_hash = {
- :weight => 0.5,
- :image => {'type' => 'ProductImage', 'filename' => 'image.gif' },
+ weight: 0.5,
+ image: { "type" => "ProductImage", "filename" => "image.gif" },
}.stringify_keys
assert_equal expected_product_hash, Hash.from_xml(product_xml)["product"]
@@ -1539,82 +964,26 @@ class HashToXmlTest < ActiveSupport::TestCase
end
def test_from_xml_array_one
- expected = { 'numbers' => { 'type' => 'Array', 'value' => '1' }}
+ expected = { "numbers" => { "type" => "Array", "value" => "1" } }
assert_equal expected, Hash.from_xml('<numbers type="Array"><value>1</value></numbers>')
end
def test_from_xml_array_many
- expected = { 'numbers' => { 'type' => 'Array', 'value' => [ '1', '2' ] }}
+ expected = { "numbers" => { "type" => "Array", "value" => [ "1", "2" ] } }
assert_equal expected, Hash.from_xml('<numbers type="Array"><value>1</value><value>2</value></numbers>')
end
def test_from_trusted_xml_allows_symbol_and_yaml_types
- expected = { 'product' => { 'name' => :value }}
+ expected = { "product" => { "name" => :value } }
assert_equal expected, Hash.from_trusted_xml('<product><name type="symbol">value</name></product>')
assert_equal expected, Hash.from_trusted_xml('<product><name type="yaml">:value</name></product>')
end
- def test_should_use_default_proc_for_unknown_key
- hash_wia = HashWithIndifferentAccess.new { 1 + 2 }
- assert_equal 3, hash_wia[:new_key]
- end
-
- def test_should_use_default_proc_if_no_key_is_supplied
- hash_wia = HashWithIndifferentAccess.new { 1 + 2 }
- assert_equal 3, hash_wia.default
- end
-
- def test_should_use_default_value_for_unknown_key
- hash_wia = HashWithIndifferentAccess.new(3)
- assert_equal 3, hash_wia[:new_key]
- end
-
- def test_should_use_default_value_if_no_key_is_supplied
- hash_wia = HashWithIndifferentAccess.new(3)
- assert_equal 3, hash_wia.default
- end
-
- def test_should_nil_if_no_default_value_is_supplied
- hash_wia = HashWithIndifferentAccess.new
- assert_nil hash_wia.default
- end
-
- def test_should_return_dup_for_with_indifferent_access
- hash_wia = HashWithIndifferentAccess.new
- assert_equal hash_wia, hash_wia.with_indifferent_access
- assert_not_same hash_wia, hash_wia.with_indifferent_access
- end
-
-
- def test_allows_setting_frozen_array_values_with_indifferent_access
- value = [1, 2, 3].freeze
- hash = HashWithIndifferentAccess.new
- hash[:key] = value
- assert_equal hash[:key], value
- end
-
- def test_should_copy_the_default_value_when_converting_to_hash_with_indifferent_access
- hash = Hash.new(3)
- hash_wia = hash.with_indifferent_access
- assert_equal 3, hash_wia.default
- end
-
- def test_should_copy_the_default_proc_when_converting_to_hash_with_indifferent_access
- hash = Hash.new do
- 2 + 1
- end
- assert_equal 3, hash[:foo]
-
- hash_wia = hash.with_indifferent_access
- assert_equal 3, hash_wia[:foo]
- assert_equal 3, hash_wia[:bar]
- end
-
# The XML builder seems to fail miserably when trying to tag something
# with the same name as a Kernel method (throw, test, loop, select ...)
def test_kernel_method_names_to_xml
- hash = { :throw => { :ball => 'red' } }
- expected = '<person><throw><ball>red</ball></throw></person>'
+ hash = { throw: { ball: "red" } }
+ expected = "<person><throw><ball>red</ball></throw></person>"
assert_nothing_raised do
assert_equal expected, hash.to_xml(@xml_options)
@@ -1629,30 +998,30 @@ class HashToXmlTest < ActiveSupport::TestCase
def test_escaping_to_xml
hash = {
- :bare_string => 'First & Last Name',
- :pre_escaped_string => 'First &amp; Last Name'
+ bare_string: "First & Last Name",
+ pre_escaped_string: "First &amp; Last Name"
}.stringify_keys
- expected_xml = '<person><bare-string>First &amp; Last Name</bare-string><pre-escaped-string>First &amp;amp; Last Name</pre-escaped-string></person>'
+ expected_xml = "<person><bare-string>First &amp; Last Name</bare-string><pre-escaped-string>First &amp;amp; Last Name</pre-escaped-string></person>"
assert_equal expected_xml, hash.to_xml(@xml_options)
end
def test_unescaping_from_xml
- xml_string = '<person><bare-string>First &amp; Last Name</bare-string><pre-escaped-string>First &amp;amp; Last Name</pre-escaped-string></person>'
+ xml_string = "<person><bare-string>First &amp; Last Name</bare-string><pre-escaped-string>First &amp;amp; Last Name</pre-escaped-string></person>"
expected_hash = {
- :bare_string => 'First & Last Name',
- :pre_escaped_string => 'First &amp; Last Name'
+ bare_string: "First & Last Name",
+ pre_escaped_string: "First &amp; Last Name"
}.stringify_keys
- assert_equal expected_hash, Hash.from_xml(xml_string)['person']
+ assert_equal expected_hash, Hash.from_xml(xml_string)["person"]
end
def test_roundtrip_to_xml_from_xml
hash = {
- :bare_string => 'First & Last Name',
- :pre_escaped_string => 'First &amp; Last Name'
+ bare_string: "First & Last Name",
+ pre_escaped_string: "First &amp; Last Name"
}.stringify_keys
- assert_equal hash, Hash.from_xml(hash.to_xml(@xml_options))['person']
+ assert_equal hash, Hash.from_xml(hash.to_xml(@xml_options))["person"]
end
def test_datetime_xml_type_with_utc_time
@@ -1661,8 +1030,8 @@ class HashToXmlTest < ActiveSupport::TestCase
<alert_at type="datetime">2008-02-10T15:30:45Z</alert_at>
</alert>
XML
- alert_at = Hash.from_xml(alert_xml)['alert']['alert_at']
- assert alert_at.utc?
+ alert_at = Hash.from_xml(alert_xml)["alert"]["alert_at"]
+ assert_predicate alert_at, :utc?
assert_equal Time.utc(2008, 2, 10, 15, 30, 45), alert_at
end
@@ -1672,8 +1041,8 @@ class HashToXmlTest < ActiveSupport::TestCase
<alert_at type="datetime">2008-02-10T10:30:45-05:00</alert_at>
</alert>
XML
- alert_at = Hash.from_xml(alert_xml)['alert']['alert_at']
- assert alert_at.utc?
+ alert_at = Hash.from_xml(alert_xml)["alert"]["alert_at"]
+ assert_predicate alert_at, :utc?
assert_equal Time.utc(2008, 2, 10, 15, 30, 45), alert_at
end
@@ -1683,8 +1052,8 @@ class HashToXmlTest < ActiveSupport::TestCase
<alert_at type="datetime">2050-02-10T15:30:45Z</alert_at>
</alert>
XML
- alert_at = Hash.from_xml(alert_xml)['alert']['alert_at']
- assert alert_at.utc?
+ alert_at = Hash.from_xml(alert_xml)["alert"]["alert_at"]
+ assert_predicate alert_at, :utc?
assert_equal 2050, alert_at.year
assert_equal 2, alert_at.month
assert_equal 10, alert_at.day
@@ -1694,20 +1063,20 @@ class HashToXmlTest < ActiveSupport::TestCase
end
def test_to_xml_dups_options
- options = {:skip_instruct => true}
+ options = { skip_instruct: true }
{}.to_xml(options)
# :builder, etc, shouldn't be added to options
- assert_equal({:skip_instruct => true}, options)
+ assert_equal({ skip_instruct: true }, options)
end
def test_expansion_count_is_limited
expected =
case ActiveSupport::XmlMini.backend.name
- when 'ActiveSupport::XmlMini_REXML'; RuntimeError
- when 'ActiveSupport::XmlMini_Nokogiri'; Nokogiri::XML::SyntaxError
- when 'ActiveSupport::XmlMini_NokogiriSAX'; RuntimeError
- when 'ActiveSupport::XmlMini_LibXML'; LibXML::XML::Error
- when 'ActiveSupport::XmlMini_LibXMLSAX'; LibXML::XML::Error
+ when "ActiveSupport::XmlMini_REXML"; RuntimeError
+ when "ActiveSupport::XmlMini_Nokogiri"; Nokogiri::XML::SyntaxError
+ when "ActiveSupport::XmlMini_NokogiriSAX"; RuntimeError
+ when "ActiveSupport::XmlMini_LibXML"; LibXML::XML::Error
+ when "ActiveSupport::XmlMini_LibXMLSAX"; LibXML::XML::Error
end
assert_raise expected do
diff --git a/activesupport/test/core_ext/integer_ext_test.rb b/activesupport/test/core_ext/integer_ext_test.rb
index 41736fb672..5691dc5341 100644
--- a/activesupport/test/core_ext/integer_ext_test.rb
+++ b/activesupport/test/core_ext/integer_ext_test.rb
@@ -1,30 +1,32 @@
-require 'abstract_unit'
-require 'active_support/core_ext/integer'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/integer"
class IntegerExtTest < ActiveSupport::TestCase
PRIME = 22953686867719691230002707821868552601124472329079
def test_multiple_of
[ -7, 0, 7, 14 ].each { |i| assert i.multiple_of?(7) }
- [ -7, 7, 14 ].each { |i| assert ! i.multiple_of?(6) }
+ [ -7, 7, 14 ].each { |i| assert_not i.multiple_of?(6) }
# test the 0 edge case
assert 0.multiple_of?(0)
- assert !5.multiple_of?(0)
+ assert_not 5.multiple_of?(0)
# test with a prime
- [2, 3, 5, 7].each { |i| assert !PRIME.multiple_of?(i) }
+ [2, 3, 5, 7].each { |i| assert_not PRIME.multiple_of?(i) }
end
def test_ordinalize
# These tests are mostly just to ensure that the ordinalize method exists.
# Its results are tested comprehensively in the inflector test cases.
- assert_equal '1st', 1.ordinalize
- assert_equal '8th', 8.ordinalize
+ assert_equal "1st", 1.ordinalize
+ assert_equal "8th", 8.ordinalize
end
def test_ordinal
- assert_equal 'st', 1.ordinal
- assert_equal 'th', 8.ordinal
+ assert_equal "st", 1.ordinal
+ assert_equal "th", 8.ordinal
end
end
diff --git a/activesupport/test/core_ext/kernel/concern_test.rb b/activesupport/test/core_ext/kernel/concern_test.rb
index 478a00d2d2..b40ff6a623 100644
--- a/activesupport/test/core_ext/kernel/concern_test.rb
+++ b/activesupport/test/core_ext/kernel/concern_test.rb
@@ -1,9 +1,11 @@
-require 'abstract_unit'
-require 'active_support/core_ext/kernel/concern'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/kernel/concern"
class KernelConcernTest < ActiveSupport::TestCase
def test_may_be_defined_at_toplevel
- mod = ::TOPLEVEL_BINDING.eval 'concern(:ToplevelConcern) { }'
+ mod = ::TOPLEVEL_BINDING.eval "concern(:ToplevelConcern) { }"
assert_equal mod, ::ToplevelConcern
assert_kind_of ActiveSupport::Concern, ::ToplevelConcern
assert_not Object.ancestors.include?(::ToplevelConcern), mod.ancestors.inspect
diff --git a/activesupport/test/core_ext/kernel_test.rb b/activesupport/test/core_ext/kernel_test.rb
index 503e6595cb..ef11e10af8 100644
--- a/activesupport/test/core_ext/kernel_test.rb
+++ b/activesupport/test/core_ext/kernel_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'active_support/core_ext/kernel'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/kernel"
class KernelTest < ActiveSupport::TestCase
def test_silence_warnings
@@ -49,19 +51,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/load_error_test.rb b/activesupport/test/core_ext/load_error_test.rb
index b2a75a2bcc..126aa51cb4 100644
--- a/activesupport/test/core_ext/load_error_test.rb
+++ b/activesupport/test/core_ext/load_error_test.rb
@@ -1,26 +1,26 @@
-require 'abstract_unit'
-require 'active_support/core_ext/load_error'
+# frozen_string_literal: true
-
-class TestMissingSourceFile < ActiveSupport::TestCase
- def test_it_is_deprecated
- assert_deprecated do
- MissingSourceFile.new
- end
- end
-end
+require "abstract_unit"
+require "active_support/core_ext/load_error"
class TestLoadError < ActiveSupport::TestCase
def test_with_require
- assert_raise(LoadError) { require 'no_this_file_don\'t_exist' }
+ assert_raise(LoadError) { require "no_this_file_don't_exist" }
end
+
def test_with_load
- assert_raise(LoadError) { load 'nor_does_this_one' }
+ assert_raise(LoadError) { load "nor_does_this_one" }
end
+
def test_path
- begin load 'nor/this/one.rb'
+ begin load "nor/this/one.rb"
rescue LoadError => e
- assert_equal 'nor/this/one.rb', e.path
+ assert_equal "nor/this/one.rb", e.path
end
end
+
+ def test_is_missing_with_nil_path
+ error = LoadError.new(nil)
+ assert_nothing_raised { error.is_missing?("anything") }
+ end
end
diff --git a/activesupport/test/core_ext/marshal_test.rb b/activesupport/test/core_ext/marshal_test.rb
index 825df439a5..7ac051b4b1 100644
--- a/activesupport/test/core_ext/marshal_test.rb
+++ b/activesupport/test/core_ext/marshal_test.rb
@@ -1,6 +1,8 @@
-require 'abstract_unit'
-require 'active_support/core_ext/marshal'
-require 'dependencies_test_helpers'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/marshal"
+require "dependencies_test_helpers"
class MarshalTest < ActiveSupport::TestCase
include ActiveSupport::Testing::Isolation
@@ -12,13 +14,26 @@ class MarshalTest < ActiveSupport::TestCase
end
test "that Marshal#load still works" do
- sanity_data = ["test", [1, 2, 3], {a: [1, 2, 3]}, ActiveSupport::TestCase]
+ sanity_data = ["test", [1, 2, 3], { a: [1, 2, 3] }, ActiveSupport::TestCase]
sanity_data.each do |obj|
dumped = Marshal.dump(obj)
assert_equal Marshal.method(:load).super_method.call(dumped), Marshal.load(dumped)
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
@@ -29,7 +44,12 @@ class MarshalTest < ActiveSupport::TestCase
ActiveSupport::Dependencies.clear
with_autoloading_fixtures do
- assert_kind_of EM, Marshal.load(dumped)
+ object = nil
+ assert_nothing_raised do
+ object = Marshal.load(dumped)
+ end
+
+ assert_kind_of EM, object
end
end
@@ -43,7 +63,12 @@ class MarshalTest < ActiveSupport::TestCase
ActiveSupport::Dependencies.clear
with_autoloading_fixtures do
- assert_kind_of ClassFolder::ClassFolderSubclass, Marshal.load(dumped)
+ object = nil
+ assert_nothing_raised do
+ object = Marshal.load(dumped)
+ end
+
+ assert_kind_of ClassFolder::ClassFolderSubclass, object
end
end
@@ -64,6 +89,17 @@ class MarshalTest < ActiveSupport::TestCase
end
end
+ test "when one constant resolves to another" do
+ class Parent; C = Class.new; end
+ class Child < Parent; C = Class.new; end
+
+ dump = Marshal.dump(Child::C.new)
+
+ Child.send(:remove_const, :C)
+
+ assert_raise(ArgumentError) { Marshal.load(dump) }
+ end
+
test "that a real missing class is causing an exception" do
dumped = nil
with_autoloading_fixtures do
@@ -96,7 +132,7 @@ class MarshalTest < ActiveSupport::TestCase
Marshal.load(dumped)
end
- assert_nothing_raised("EM failed to load while we expect only SomeClass to fail loading") do
+ assert_nothing_raised do
EM.new
end
@@ -117,7 +153,12 @@ class MarshalTest < ActiveSupport::TestCase
ActiveSupport::Dependencies.clear
with_autoloading_fixtures do
- assert_kind_of EM, Marshal.load(f)
+ object = nil
+ assert_nothing_raised do
+ object = Marshal.load(f)
+ end
+
+ assert_kind_of EM, object
end
end
end
diff --git a/activesupport/test/core_ext/module/anonymous_test.rb b/activesupport/test/core_ext/module/anonymous_test.rb
index cb556af772..e03c217015 100644
--- a/activesupport/test/core_ext/module/anonymous_test.rb
+++ b/activesupport/test/core_ext/module/anonymous_test.rb
@@ -1,14 +1,16 @@
-require 'abstract_unit'
-require 'active_support/core_ext/module/anonymous'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/module/anonymous"
class AnonymousTest < ActiveSupport::TestCase
test "an anonymous class or module are anonymous" do
- assert Module.new.anonymous?
- assert Class.new.anonymous?
+ assert_predicate Module.new, :anonymous?
+ assert_predicate Class.new, :anonymous?
end
test "a named class or module are not anonymous" do
- assert !Kernel.anonymous?
- assert !Object.anonymous?
+ assert_not_predicate Kernel, :anonymous?
+ assert_not_predicate Object, :anonymous?
end
-end \ No newline at end of file
+end
diff --git a/activesupport/test/core_ext/module/attr_internal_test.rb b/activesupport/test/core_ext/module/attr_internal_test.rb
index 2aea14cf2b..9a65f75497 100644
--- a/activesupport/test/core_ext/module/attr_internal_test.rb
+++ b/activesupport/test/core_ext/module/attr_internal_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'active_support/core_ext/module/attr_internal'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/module/attr_internal"
class AttrInternalTest < ActiveSupport::TestCase
def setup
@@ -10,44 +12,44 @@ class AttrInternalTest < ActiveSupport::TestCase
def test_reader
assert_nothing_raised { @target.attr_internal_reader :foo }
- assert !@instance.instance_variable_defined?('@_foo')
+ assert_not @instance.instance_variable_defined?("@_foo")
assert_raise(NoMethodError) { @instance.foo = 1 }
- @instance.instance_variable_set('@_foo', 1)
+ @instance.instance_variable_set("@_foo", 1)
assert_nothing_raised { assert_equal 1, @instance.foo }
end
def test_writer
assert_nothing_raised { @target.attr_internal_writer :foo }
- assert !@instance.instance_variable_defined?('@_foo')
+ assert_not @instance.instance_variable_defined?("@_foo")
assert_nothing_raised { assert_equal 1, @instance.foo = 1 }
- assert_equal 1, @instance.instance_variable_get('@_foo')
+ assert_equal 1, @instance.instance_variable_get("@_foo")
assert_raise(NoMethodError) { @instance.foo }
end
def test_accessor
assert_nothing_raised { @target.attr_internal :foo }
- assert !@instance.instance_variable_defined?('@_foo')
+ assert_not @instance.instance_variable_defined?("@_foo")
assert_nothing_raised { assert_equal 1, @instance.foo = 1 }
- assert_equal 1, @instance.instance_variable_get('@_foo')
+ assert_equal 1, @instance.instance_variable_get("@_foo")
assert_nothing_raised { assert_equal 1, @instance.foo }
end
def test_naming_format
- assert_equal '@_%s', Module.attr_internal_naming_format
- assert_nothing_raised { Module.attr_internal_naming_format = '@abc%sdef' }
+ assert_equal "@_%s", Module.attr_internal_naming_format
+ assert_nothing_raised { Module.attr_internal_naming_format = "@abc%sdef" }
@target.attr_internal :foo
- assert !@instance.instance_variable_defined?('@_foo')
- assert !@instance.instance_variable_defined?('@abcfoodef')
+ assert_not @instance.instance_variable_defined?("@_foo")
+ assert_not @instance.instance_variable_defined?("@abcfoodef")
assert_nothing_raised { @instance.foo = 1 }
- assert !@instance.instance_variable_defined?('@_foo')
- assert @instance.instance_variable_defined?('@abcfoodef')
+ assert_not @instance.instance_variable_defined?("@_foo")
+ assert @instance.instance_variable_defined?("@abcfoodef")
ensure
- Module.attr_internal_naming_format = '@_%s'
+ Module.attr_internal_naming_format = "@_%s"
end
end
diff --git a/activesupport/test/core_ext/module/attribute_accessor_per_thread_test.rb b/activesupport/test/core_ext/module/attribute_accessor_per_thread_test.rb
new file mode 100644
index 0000000000..e0e331fc91
--- /dev/null
+++ b/activesupport/test/core_ext/module/attribute_accessor_per_thread_test.rb
@@ -0,0 +1,133 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/module/attribute_accessors_per_thread"
+
+class ModuleAttributeAccessorPerThreadTest < ActiveSupport::TestCase
+ def setup
+ @class = Class.new do
+ thread_mattr_accessor :foo
+ thread_mattr_accessor :bar, instance_writer: false
+ thread_mattr_reader :shaq, instance_reader: false
+ thread_mattr_accessor :camp, instance_accessor: false
+
+ def self.name; "MyClass" end
+ end
+
+ @subclass = Class.new(@class) do
+ def self.name; "SubMyClass" end
+ end
+
+ @object = @class.new
+ end
+
+ def test_should_use_mattr_default
+ Thread.new do
+ assert_nil @class.foo
+ assert_nil @object.foo
+ end.join
+ end
+
+ def test_should_set_mattr_value
+ Thread.new do
+ @class.foo = :test
+ assert_equal :test, @class.foo
+
+ @class.foo = :test2
+ assert_equal :test2, @class.foo
+ end.join
+ end
+
+ def test_should_not_create_instance_writer
+ Thread.new do
+ assert_respond_to @class, :foo
+ assert_respond_to @class, :foo=
+ assert_respond_to @object, :bar
+ assert_not_respond_to @object, :bar=
+ end.join
+ end
+
+ def test_should_not_create_instance_reader
+ Thread.new do
+ assert_respond_to @class, :shaq
+ assert_not_respond_to @object, :shaq
+ end.join
+ end
+
+ def test_should_not_create_instance_accessors
+ Thread.new do
+ assert_respond_to @class, :camp
+ assert_not_respond_to @object, :camp
+ assert_not_respond_to @object, :camp=
+ end.join
+ end
+
+ def test_values_should_not_bleed_between_threads
+ threads = []
+ threads << Thread.new do
+ @class.foo = "things"
+ sleep 1
+ assert_equal "things", @class.foo
+ end
+
+ threads << Thread.new do
+ @class.foo = "other things"
+ sleep 1
+ assert_equal "other things", @class.foo
+ end
+
+ threads << Thread.new do
+ @class.foo = "really other things"
+ sleep 1
+ assert_equal "really other things", @class.foo
+ end
+
+ threads.each { |t| t.join }
+ end
+
+ def test_should_raise_name_error_if_attribute_name_is_invalid
+ exception = assert_raises NameError do
+ Class.new do
+ thread_cattr_reader "1nvalid"
+ end
+ end
+ assert_equal "invalid attribute name: 1nvalid", exception.message
+
+ exception = assert_raises NameError do
+ Class.new do
+ thread_cattr_writer "1nvalid"
+ end
+ end
+ assert_equal "invalid attribute name: 1nvalid", exception.message
+
+ exception = assert_raises NameError do
+ Class.new do
+ thread_mattr_reader "1valid_part"
+ end
+ end
+ assert_equal "invalid attribute name: 1valid_part", exception.message
+
+ exception = assert_raises NameError do
+ Class.new do
+ thread_mattr_writer "2valid_part"
+ end
+ end
+ assert_equal "invalid attribute name: 2valid_part", exception.message
+ end
+
+ def test_should_return_same_value_by_class_or_instance_accessor
+ @class.foo = "fries"
+
+ assert_equal @class.foo, @object.foo
+ end
+
+ def test_should_not_affect_superclass_if_subclass_set_value
+ @class.foo = "super"
+ assert_equal "super", @class.foo
+ assert_nil @subclass.foo
+
+ @subclass.foo = "sub"
+ assert_equal "super", @class.foo
+ assert_equal "sub", @subclass.foo
+ end
+end
diff --git a/activesupport/test/core_ext/module/attribute_accessor_test.rb b/activesupport/test/core_ext/module/attribute_accessor_test.rb
index 48f3cc579f..33c583947a 100644
--- a/activesupport/test/core_ext/module/attribute_accessor_test.rb
+++ b/activesupport/test/core_ext/module/attribute_accessor_test.rb
@@ -1,18 +1,27 @@
-require 'abstract_unit'
-require 'active_support/core_ext/module/attribute_accessors'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/module/attribute_accessors"
class ModuleAttributeAccessorTest < ActiveSupport::TestCase
def setup
m = @module = Module.new do
mattr_accessor :foo
- mattr_accessor :bar, :instance_writer => false
- mattr_reader :shaq, :instance_reader => false
- mattr_accessor :camp, :instance_accessor => false
+ mattr_accessor :bar, instance_writer: false
+ mattr_reader :shaq, instance_reader: false
+ mattr_accessor :camp, instance_accessor: false
- cattr_accessor(:defa) { 'default_accessor_value' }
- cattr_reader(:defr) { 'default_reader_value' }
- cattr_writer(:defw) { 'default_writer_value' }
+ cattr_accessor(:defa) { "default_accessor_value" }
+ cattr_reader(:defr) { "default_reader_value" }
+ cattr_writer(:defw) { "default_writer_value" }
+ cattr_accessor(:deff) { false }
cattr_accessor(:quux) { :quux }
+
+ cattr_accessor :def_accessor, default: "default_accessor_value"
+ cattr_reader :def_reader, default: "default_reader_value"
+ cattr_writer :def_writer, default: "default_writer_value"
+ cattr_accessor :def_false, default: false
+ cattr_accessor(:def_priority, default: false) { :no_priority }
end
@class = Class.new
@class.instance_eval { include m }
@@ -24,6 +33,21 @@ class ModuleAttributeAccessorTest < ActiveSupport::TestCase
assert_nil @object.foo
end
+ def test_mattr_default_keyword_arguments
+ assert_equal "default_accessor_value", @module.def_accessor
+ assert_equal "default_reader_value", @module.def_reader
+ assert_equal "default_writer_value", @module.class_variable_get(:@@def_writer)
+ end
+
+ def test_mattr_can_default_to_false
+ assert_equal false, @module.def_false
+ assert_equal false, @module.deff
+ end
+
+ def test_mattr_default_priority
+ assert_equal false, @module.def_priority
+ end
+
def test_should_set_mattr_value
@module.foo = :test
assert_equal :test, @object.foo
@@ -41,18 +65,18 @@ class ModuleAttributeAccessorTest < ActiveSupport::TestCase
assert_respond_to @module, :foo
assert_respond_to @module, :foo=
assert_respond_to @object, :bar
- assert !@object.respond_to?(:bar=)
+ assert_not_respond_to @object, :bar=
end
def test_should_not_create_instance_reader
assert_respond_to @module, :shaq
- assert !@object.respond_to?(:shaq)
+ assert_not_respond_to @object, :shaq
end
def test_should_not_create_instance_accessors
assert_respond_to @module, :camp
- assert !@object.respond_to?(:camp)
- assert !@object.respond_to?(:camp=)
+ assert_not_respond_to @object, :camp
+ assert_not_respond_to @object, :camp=
end
def test_should_raise_name_error_if_attribute_name_is_invalid
@@ -69,11 +93,45 @@ class ModuleAttributeAccessorTest < ActiveSupport::TestCase
end
end
assert_equal "invalid attribute name: 1nvalid", exception.message
+
+ exception = assert_raises NameError do
+ Class.new do
+ mattr_reader "valid_part\ninvalid_part"
+ end
+ end
+ assert_equal "invalid attribute name: valid_part\ninvalid_part", exception.message
+
+ exception = assert_raises NameError do
+ Class.new do
+ mattr_writer "valid_part\ninvalid_part"
+ end
+ end
+ assert_equal "invalid attribute name: valid_part\ninvalid_part", exception.message
end
def test_should_use_default_value_if_block_passed
- assert_equal 'default_accessor_value', @module.defa
- assert_equal 'default_reader_value', @module.defr
- assert_equal 'default_writer_value', @module.class_variable_get('@@defw')
+ assert_equal "default_accessor_value", @module.defa
+ assert_equal "default_reader_value", @module.defr
+ assert_equal "default_writer_value", @module.class_variable_get("@@defw")
+ end
+
+ def test_method_invocation_should_not_invoke_the_default_block
+ count = 0
+
+ @module.cattr_accessor(:defcount) { count += 1 }
+
+ assert_equal 1, count
+ assert_no_difference "count" do
+ @module.defcount
+ end
+ end
+
+ def test_declaring_multiple_attributes_at_once_invokes_the_block_multiple_times
+ count = 0
+
+ @module.cattr_accessor(:defn1, :defn2) { count += 1 }
+
+ assert_equal 1, @module.defn1
+ assert_equal 2, @module.defn2
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 29c3053b47..81aac224f9 100644
--- a/activesupport/test/core_ext/module/attribute_aliasing_test.rb
+++ b/activesupport/test/core_ext/module/attribute_aliasing_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'active_support/core_ext/module/aliasing'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/module/aliasing"
module AttributeAliasing
class Content
@@ -28,15 +30,15 @@ class AttributeAliasingTest < ActiveSupport::TestCase
def test_attribute_alias
e = AttributeAliasing::Email.new
- assert !e.subject?
+ assert_not_predicate e, :subject?
e.title = "Upgrade computer"
assert_equal "Upgrade computer", e.subject
- assert e.subject?
+ assert_predicate e, :subject?
e.subject = "We got a long way to go"
assert_equal "We got a long way to go", e.title
- assert e.title?
+ assert_predicate e, :title?
end
def test_aliasing_to_uppercase_attributes
@@ -45,15 +47,15 @@ class AttributeAliasingTest < ActiveSupport::TestCase
# to more sensible ones, everything goes *foof*.
e = AttributeAliasing::Email.new
- assert !e.body?
- assert !e.Data?
+ assert_not_predicate e, :body?
+ assert_not_predicate e, :Data?
e.body = "No, really, this is not a joke."
assert_equal "No, really, this is not a joke.", e.Data
- assert e.Data?
+ assert_predicate e, :Data?
- e.Data = "Uppercased methods are teh suck"
- assert_equal "Uppercased methods are teh suck", e.body
- assert e.body?
+ e.Data = "Uppercased methods are the suck"
+ assert_equal "Uppercased methods are the suck", e.body
+ assert_predicate e, :body?
end
end
diff --git a/activesupport/test/core_ext/module/concerning_test.rb b/activesupport/test/core_ext/module/concerning_test.rb
index 07d860b71c..38fd60463d 100644
--- a/activesupport/test/core_ext/module/concerning_test.rb
+++ b/activesupport/test/core_ext/module/concerning_test.rb
@@ -1,10 +1,12 @@
-require 'abstract_unit'
-require 'active_support/core_ext/module/concerning'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/module/concerning"
class ModuleConcerningTest < ActiveSupport::TestCase
def test_concerning_declares_a_concern_and_includes_it_immediately
klass = Class.new { concerning(:Foo) { } }
- assert klass.ancestors.include?(klass::Foo), klass.ancestors.inspect
+ assert_includes klass.ancestors, klass::Foo, klass.ancestors.inspect
end
end
@@ -19,15 +21,15 @@ class ModuleConcernTest < ActiveSupport::TestCase
# Declares a concern but doesn't include it
assert klass.const_defined?(:Baz, false)
- assert !ModuleConcernTest.const_defined?(:Baz)
+ assert_not ModuleConcernTest.const_defined?(:Baz)
assert_kind_of ActiveSupport::Concern, klass::Baz
- assert !klass.ancestors.include?(klass::Baz), klass.ancestors.inspect
+ assert_not_includes klass.ancestors, klass::Baz, klass.ancestors.inspect
# Public method visibility by default
- assert klass::Baz.public_instance_methods.map(&:to_s).include?('should_be_public')
+ assert_includes klass::Baz.public_instance_methods.map(&:to_s), "should_be_public"
# Calls included hook
- assert_equal 1, Class.new { include klass::Baz }.instance_variable_get('@foo')
+ assert_equal 1, Class.new { include klass::Baz }.instance_variable_get("@foo")
end
class Foo
@@ -53,10 +55,10 @@ class ModuleConcernTest < ActiveSupport::TestCase
end
def test_using_class_methods_blocks_instead_of_ClassMethods_module
- assert !Foo.respond_to?(:will_be_orphaned)
- assert Foo.respond_to?(:hacked_on)
- assert Foo.respond_to?(:nicer_dsl)
- assert Foo.respond_to?(:doesnt_clobber)
+ assert_not_respond_to Foo, :will_be_orphaned
+ assert_respond_to Foo, :hacked_on
+ assert_respond_to Foo, :nicer_dsl
+ assert_respond_to Foo, :doesnt_clobber
# Orphan in Foo::ClassMethods, not Bar::ClassMethods.
assert Foo.const_defined?(:ClassMethods)
diff --git a/activesupport/test/core_ext/module/introspection_test.rb b/activesupport/test/core_ext/module/introspection_test.rb
new file mode 100644
index 0000000000..d8409d5e44
--- /dev/null
+++ b/activesupport/test/core_ext/module/introspection_test.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+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_module_parent_name
+ assert_equal "ParentA", ParentA::B.module_parent_name
+ assert_equal "ParentA::B", ParentA::B::C.module_parent_name
+ assert_nil ParentA.module_parent_name
+ end
+
+ def test_module_parent_name_when_frozen
+ assert_equal "ParentA", ParentA::FrozenB.module_parent_name
+ assert_equal "ParentA::B", ParentA::B::FrozenC.module_parent_name
+ end
+
+ def test_parent_name
+ assert_deprecated do
+ assert_equal "ParentA", ParentA::B.parent_name
+ end
+ end
+
+ def test_module_parent
+ assert_equal ParentA::B, ParentA::B::C.module_parent
+ assert_equal ParentA, ParentA::B.module_parent
+ assert_equal Object, ParentA.module_parent
+ end
+
+ def test_parent
+ assert_deprecated do
+ assert_equal ParentA, ParentA::B.parent
+ end
+ end
+
+ def test_module_parents
+ assert_equal [ParentA::B, ParentA, Object], ParentA::B::C.module_parents
+ assert_equal [ParentA, Object], ParentA::B.module_parents
+ end
+
+ def test_parents
+ assert_deprecated do
+ assert_equal [ParentA, Object], ParentA::B.parents
+ end
+ end
+end
diff --git a/activesupport/test/core_ext/module/qualified_const_test.rb b/activesupport/test/core_ext/module/qualified_const_test.rb
deleted file mode 100644
index 37c9228a64..0000000000
--- a/activesupport/test/core_ext/module/qualified_const_test.rb
+++ /dev/null
@@ -1,108 +0,0 @@
-require 'abstract_unit'
-require 'active_support/core_ext/module/qualified_const'
-
-module QualifiedConstTestMod
- X = false
-
- module M
- X = 1
-
- class C
- X = 2
- end
- end
-
- module N
- include M
- end
-end
-
-class QualifiedConstTest < ActiveSupport::TestCase
- test "Object.qualified_const_defined?" do
- assert Object.qualified_const_defined?("QualifiedConstTestMod")
- assert !Object.qualified_const_defined?("NonExistingQualifiedConstTestMod")
-
- assert Object.qualified_const_defined?("QualifiedConstTestMod::X")
- assert !Object.qualified_const_defined?("QualifiedConstTestMod::Y")
-
- assert Object.qualified_const_defined?("QualifiedConstTestMod::M::X")
- assert !Object.qualified_const_defined?("QualifiedConstTestMod::M::Y")
-
- if Module.method(:const_defined?).arity == 1
- assert !Object.qualified_const_defined?("QualifiedConstTestMod::N::X")
- else
- assert Object.qualified_const_defined?("QualifiedConstTestMod::N::X")
- assert !Object.qualified_const_defined?("QualifiedConstTestMod::N::X", false)
- assert Object.qualified_const_defined?("QualifiedConstTestMod::N::X", true)
- end
- end
-
- test "mod.qualified_const_defined?" do
- assert QualifiedConstTestMod.qualified_const_defined?("M")
- assert !QualifiedConstTestMod.qualified_const_defined?("NonExistingM")
-
- assert QualifiedConstTestMod.qualified_const_defined?("M::X")
- assert !QualifiedConstTestMod.qualified_const_defined?("M::Y")
-
- assert QualifiedConstTestMod.qualified_const_defined?("M::C::X")
- assert !QualifiedConstTestMod.qualified_const_defined?("M::C::Y")
-
- if Module.method(:const_defined?).arity == 1
- assert !QualifiedConstTestMod.qualified_const_defined?("QualifiedConstTestMod::N::X")
- else
- assert QualifiedConstTestMod.qualified_const_defined?("N::X")
- assert !QualifiedConstTestMod.qualified_const_defined?("N::X", false)
- assert QualifiedConstTestMod.qualified_const_defined?("N::X", true)
- end
- end
-
- test "qualified_const_get" do
- assert_equal false, Object.qualified_const_get("QualifiedConstTestMod::X")
- assert_equal false, QualifiedConstTestMod.qualified_const_get("X")
- assert_equal 1, QualifiedConstTestMod.qualified_const_get("M::X")
- assert_equal 1, QualifiedConstTestMod.qualified_const_get("N::X")
- assert_equal 2, QualifiedConstTestMod.qualified_const_get("M::C::X")
-
- assert_raise(NameError) { QualifiedConstTestMod.qualified_const_get("M::C::Y")}
- end
-
- test "qualified_const_set" do
- begin
- m = Module.new
- assert_equal m, Object.qualified_const_set("QualifiedConstTestMod2", m)
- assert_equal m, ::QualifiedConstTestMod2
-
- # We are going to assign to existing constants on purpose, so silence warnings.
- silence_warnings do
- assert_equal true, QualifiedConstTestMod.qualified_const_set("QualifiedConstTestMod::X", true)
- assert_equal true, QualifiedConstTestMod::X
-
- assert_equal 10, QualifiedConstTestMod::M.qualified_const_set("X", 10)
- assert_equal 10, QualifiedConstTestMod::M::X
- end
- ensure
- silence_warnings do
- QualifiedConstTestMod.qualified_const_set('QualifiedConstTestMod::X', false)
- QualifiedConstTestMod::M.qualified_const_set('X', 1)
- end
- end
- end
-
- test "reject absolute paths" do
- assert_raise_with_message(NameError, "wrong constant name ::X") { Object.qualified_const_defined?("::X")}
- assert_raise_with_message(NameError, "wrong constant name ::X") { Object.qualified_const_defined?("::X::Y")}
-
- assert_raise_with_message(NameError, "wrong constant name ::X") { Object.qualified_const_get("::X")}
- assert_raise_with_message(NameError, "wrong constant name ::X") { Object.qualified_const_get("::X::Y")}
-
- assert_raise_with_message(NameError, "wrong constant name ::X") { Object.qualified_const_set("::X", nil)}
- assert_raise_with_message(NameError, "wrong constant name ::X") { Object.qualified_const_set("::X::Y", nil)}
- end
-
- private
-
- def assert_raise_with_message(expected_exception, expected_message, &block)
- exception = assert_raise(expected_exception, &block)
- assert_equal expected_message, exception.message
- end
-end
diff --git a/activesupport/test/core_ext/module/reachable_test.rb b/activesupport/test/core_ext/module/reachable_test.rb
index 80eb31a5c4..f356d46957 100644
--- a/activesupport/test/core_ext/module/reachable_test.rb
+++ b/activesupport/test/core_ext/module/reachable_test.rb
@@ -1,15 +1,21 @@
-require 'abstract_unit'
-require 'active_support/core_ext/module/reachable'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/module/reachable"
class AnonymousTest < ActiveSupport::TestCase
test "an anonymous class or module is not reachable" do
- assert !Module.new.reachable?
- assert !Class.new.reachable?
+ assert_deprecated do
+ assert_not_predicate Module.new, :reachable?
+ assert_not_predicate Class.new, :reachable?
+ end
end
test "ordinary named classes or modules are reachable" do
- assert Kernel.reachable?
- assert Object.reachable?
+ assert_deprecated do
+ assert_predicate Kernel, :reachable?
+ assert_predicate Object, :reachable?
+ end
end
test "a named class or module whose constant has gone is not reachable" do
@@ -19,8 +25,10 @@ class AnonymousTest < ActiveSupport::TestCase
self.class.send(:remove_const, :C)
self.class.send(:remove_const, :M)
- assert !c.reachable?
- assert !m.reachable?
+ assert_deprecated do
+ assert_not_predicate c, :reachable?
+ assert_not_predicate m, :reachable?
+ end
end
test "a named class or module whose constants store different objects are not reachable" do
@@ -33,9 +41,11 @@ class AnonymousTest < ActiveSupport::TestCase
eval "class C; end"
eval "module M; end"
- assert C.reachable?
- assert M.reachable?
- assert !c.reachable?
- assert !m.reachable?
+ assert_deprecated do
+ assert_predicate C, :reachable?
+ assert_predicate M, :reachable?
+ assert_not_predicate c, :reachable?
+ assert_not_predicate m, :reachable?
+ end
end
-end \ No newline at end of file
+end
diff --git a/activesupport/test/core_ext/module/remove_method_test.rb b/activesupport/test/core_ext/module/remove_method_test.rb
index 4657f0c175..a18fc0a5e4 100644
--- a/activesupport/test/core_ext/module/remove_method_test.rb
+++ b/activesupport/test/core_ext/module/remove_method_test.rb
@@ -1,29 +1,59 @@
-require 'abstract_unit'
-require 'active_support/core_ext/module/remove_method'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/module/remove_method"
module RemoveMethodTests
class A
def do_something
- return 1
+ 1
+ end
+
+ def do_something_protected
+ 1
+ end
+ protected :do_something_protected
+
+ def do_something_private
+ 1
+ end
+ private :do_something_private
+
+ class << self
+ def do_something_else
+ 2
+ end
end
-
end
end
class RemoveMethodTest < ActiveSupport::TestCase
-
def test_remove_method_from_an_object
- RemoveMethodTests::A.class_eval{
- self.remove_possible_method(:do_something)
+ RemoveMethodTests::A.class_eval {
+ remove_possible_method(:do_something)
}
- assert !RemoveMethodTests::A.new.respond_to?(:do_something)
+ assert_not_respond_to RemoveMethodTests::A.new, :do_something
end
-
+
+ def test_remove_singleton_method_from_an_object
+ RemoveMethodTests::A.class_eval {
+ remove_possible_singleton_method(:do_something_else)
+ }
+ assert_not_respond_to RemoveMethodTests::A, :do_something_else
+ end
+
def test_redefine_method_in_an_object
- RemoveMethodTests::A.class_eval{
- self.redefine_method(:do_something) { return 100 }
+ RemoveMethodTests::A.class_eval {
+ redefine_method(:do_something) { return 100 }
+ redefine_method(:do_something_protected) { return 100 }
+ redefine_method(:do_something_private) { return 100 }
}
assert_equal 100, RemoveMethodTests::A.new.do_something
- end
+ assert_equal 100, RemoveMethodTests::A.new.send(:do_something_protected)
+ assert_equal 100, RemoveMethodTests::A.new.send(:do_something_private)
-end \ No newline at end of file
+ assert RemoveMethodTests::A.public_method_defined? :do_something
+ assert RemoveMethodTests::A.protected_method_defined? :do_something_protected
+ assert RemoveMethodTests::A.private_method_defined? :do_something_private
+ end
+end
diff --git a/activesupport/test/core_ext/module_test.rb b/activesupport/test/core_ext/module_test.rb
index bdfbadcf1d..04692f1484 100644
--- a/activesupport/test/core_ext/module_test.rb
+++ b/activesupport/test/core_ext/module_test.rb
@@ -1,74 +1,63 @@
-require 'abstract_unit'
-require 'active_support/core_ext/module'
+# frozen_string_literal: true
-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
+require "abstract_unit"
+require "active_support/core_ext/module"
Somewhere = Struct.new(:street, :city) do
attr_accessor :name
end
-class Someone < Struct.new(:name, :place)
- delegate :street, :city, :to_f, :to => :place
- delegate :name=, :to => :place, :prefix => true
- delegate :upcase, :to => "place.city"
- delegate :table_name, :to => :class
- delegate :table_name, :to => :class, :prefix => true
+Someone = Struct.new(:name, :place) do
+ delegate :street, :city, :to_f, to: :place
+ delegate :name=, to: :place, prefix: true
+ delegate :upcase, to: "place.city"
+ delegate :table_name, to: :class
+ delegate :table_name, to: :class, prefix: true
def self.table_name
- 'some_table'
+ "some_table"
end
- FAILED_DELEGATE_LINE = __LINE__ + 1
- delegate :foo, :to => :place
+ self::FAILED_DELEGATE_LINE = __LINE__ + 1
+ delegate :foo, to: :place
+
+ self::FAILED_DELEGATE_LINE_2 = __LINE__ + 1
+ delegate :bar, to: :place, allow_nil: true
+
+ private
- FAILED_DELEGATE_LINE_2 = __LINE__ + 1
- delegate :bar, :to => :place, :allow_nil => true
+ def private_name
+ "Private"
+ end
end
-Invoice = Struct.new(:client) do
- delegate :street, :city, :name, :to => :client, :prefix => true
- delegate :street, :city, :name, :to => :client, :prefix => :customer
+Invoice = Struct.new(:client) do
+ delegate :street, :city, :name, to: :client, prefix: true
+ delegate :street, :city, :name, to: :client, prefix: :customer
end
-Project = Struct.new(:description, :person) do
- delegate :name, :to => :person, :allow_nil => true
- delegate :to_f, :to => :description, :allow_nil => true
+Project = Struct.new(:description, :person) do
+ delegate :name, to: :person, allow_nil: true
+ delegate :to_f, to: :description, allow_nil: true
end
Developer = Struct.new(:client) do
- delegate :name, :to => :client, :prefix => nil
+ delegate :name, to: :client, prefix: nil
end
Event = Struct.new(:case) do
- delegate :foo, :to => :case
+ delegate :foo, to: :case
end
Tester = Struct.new(:client) do
- delegate :name, :to => :client, :prefix => false
+ delegate :name, to: :client, prefix: false
def foo; 1; end
end
Product = Struct.new(:name) do
- delegate :name, :to => :manufacturer, :prefix => true
- delegate :name, :to => :type, :prefix => true
+ delegate :name, to: :manufacturer, prefix: true
+ delegate :name, to: :type, prefix: true
def manufacturer
@manufacturer ||= begin
@@ -83,16 +72,56 @@ Product = Struct.new(:name) do
end
end
+module ExtraMissing
+ def method_missing(sym, *args)
+ if sym == :extra_missing
+ 42
+ else
+ super
+ end
+ end
+
+ def respond_to_missing?(sym, priv = false)
+ sym == :extra_missing || super
+ end
+end
+
+DecoratedTester = Struct.new(:client) do
+ include ExtraMissing
+
+ delegate_missing_to :client
+end
+
+class DecoratedReserved
+ delegate_missing_to :case
+
+ attr_reader :case
+
+ def initialize(kase)
+ @case = kase
+ end
+end
+
+class Block
+ def hello?
+ true
+ end
+end
+
+HasBlock = Struct.new(:block) do
+ delegate :hello?, to: :block
+end
+
class ParameterSet
- delegate :[], :[]=, :to => :@params
+ delegate :[], :[]=, to: :@params
def initialize
- @params = {:foo => "bar"}
+ @params = { foo: "bar" }
end
end
class Name
- delegate :upcase, :to => :@full_name
+ delegate :upcase, to: :@full_name
def initialize(first, last)
@full_name = "#{first} #{last}"
@@ -102,8 +131,8 @@ end
class SideEffect
attr_reader :ints
- delegate :to_i, :to => :shift, :allow_nil => true
- delegate :to_s, :to => :shift
+ delegate :to_i, to: :shift, allow_nil: true
+ delegate :to_s, to: :shift
def initialize
@ints = [1, 2, 3]
@@ -150,8 +179,8 @@ class ModuleTest < ActiveSupport::TestCase
end
def test_delegation_to_class_method
- assert_equal 'some_table', @david.table_name
- assert_equal 'some_table', @david.class_table_name
+ assert_equal "some_table", @david.table_name
+ assert_equal "some_table", @david.class_table_name
end
def test_missing_delegation_target
@@ -159,27 +188,42 @@ class ModuleTest < ActiveSupport::TestCase
Name.send :delegate, :nowhere
end
assert_raise(ArgumentError) do
- Name.send :delegate, :noplace, :tos => :hollywood
+ Name.send :delegate, :noplace, tos: :hollywood
+ end
+ end
+
+ def test_delegation_target_when_prefix_is_true
+ assert_nothing_raised do
+ Name.send :delegate, :go, to: :you, prefix: true
+ end
+ assert_nothing_raised do
+ Name.send :delegate, :go, to: :_you, prefix: true
+ end
+ assert_raise(ArgumentError) do
+ Name.send :delegate, :go, to: :You, prefix: true
+ end
+ assert_raise(ArgumentError) do
+ Name.send :delegate, :go, to: :@you, prefix: true
end
end
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
@@ -188,14 +232,14 @@ class ModuleTest < ActiveSupport::TestCase
def initialize(client)
@client = client
end
- delegate :name, :address, :to => :@client, :prefix => true
+ delegate :name, :address, to: :@client, prefix: true
end
end
end
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
@@ -216,7 +260,7 @@ class ModuleTest < ActiveSupport::TestCase
def test_delegation_with_allow_nil_and_nil_value_and_prefix
Project.class_eval do
- delegate :name, :to => :person, :allow_nil => true, :prefix => true
+ delegate :name, to: :person, allow_nil: true, prefix: true
end
rails = Project.new("Rails")
assert_nil rails.person_name
@@ -245,7 +289,7 @@ class ModuleTest < ActiveSupport::TestCase
assert_nothing_raised do
Class.new(parent) do
class << self
- delegate :parent_method, :to => :superclass
+ delegate :parent_method, to: :superclass
end
end
end
@@ -267,7 +311,7 @@ class ModuleTest < ActiveSupport::TestCase
rescue NoMethodError => e
file_and_line = "#{__FILE__}:#{Someone::FAILED_DELEGATE_LINE}"
# We can't simply check the first line of the backtrace, because JRuby reports the call to __send__ in the backtrace.
- assert e.backtrace.any?{|a| a.include?(file_and_line)},
+ assert e.backtrace.any? { |a| a.include?(file_and_line) },
"[#{e.backtrace.inspect}] did not include [#{file_and_line}]"
end
@@ -277,7 +321,7 @@ class ModuleTest < ActiveSupport::TestCase
rescue NoMethodError => e
file_and_line = "#{__FILE__}:#{Someone::FAILED_DELEGATE_LINE_2}"
# We can't simply check the first line of the backtrace, because JRuby reports the call to __send__ in the backtrace.
- assert e.backtrace.any?{|a| a.include?(file_and_line)},
+ assert e.backtrace.any? { |a| a.include?(file_and_line) },
"[#{e.backtrace.inspect}] did not include [#{file_and_line}]"
end
@@ -287,12 +331,12 @@ class ModuleTest < ActiveSupport::TestCase
assert_equal 1, se.to_i
assert_equal [2, 3], se.ints
- assert_equal '2', se.to_s
+ assert_equal "2", se.to_s
assert_equal [3], se.ints
end
def test_delegation_doesnt_mask_nested_no_method_error_on_nil_receiver
- product = Product.new('Widget')
+ product = Product.new("Widget")
# Nested NoMethodError is a different name from the delegation
assert_raise(NoMethodError) { product.manufacturer_name }
@@ -301,239 +345,166 @@ class ModuleTest < ActiveSupport::TestCase
assert_raise(NoMethodError) { product.type_name }
end
- def test_parent
- assert_equal Yz::Zy, Yz::Zy::Cd.parent
- assert_equal Yz, Yz::Zy.parent
- assert_equal Object, Yz.parent
+ def test_delegation_with_method_arguments
+ has_block = HasBlock.new(Block.new)
+ assert_predicate has_block, :hello?
end
- def test_parents
- assert_equal [Yz::Zy, Yz, Object], Yz::Zy::Cd.parents
- assert_equal [Yz, Object], Yz::Zy.parents
+ def test_delegate_missing_to_with_method
+ assert_equal "David", DecoratedTester.new(@david).name
end
- def test_local_constants
- assert_equal %w(Constant1 Constant3), Ab.local_constants.sort.map(&:to_s)
+ def test_delegate_missing_to_with_reserved_methods
+ assert_equal "David", DecoratedReserved.new(@david).name
end
-end
-module BarMethodAliaser
- def self.included(foo_class)
- foo_class.class_eval do
- include BarMethods
- alias_method_chain :bar, :baz
+ def test_delegate_missing_to_does_not_delegate_to_private_methods
+ e = assert_raises(NoMethodError) do
+ DecoratedReserved.new(@david).private_name
end
- end
-end
-module BarMethods
- def bar_with_baz
- bar_without_baz << '_with_baz'
+ assert_match(/undefined method `private_name' for/, e.message)
end
- def quux_with_baz!
- quux_without_baz! << '_with_baz'
- end
+ def test_delegate_missing_to_does_not_delegate_to_fake_methods
+ e = assert_raises(NoMethodError) do
+ DecoratedReserved.new(@david).my_fake_method
+ end
- def quux_with_baz?
- false
+ assert_match(/undefined method `my_fake_method' for/, e.message)
end
- def quux_with_baz=(v)
- send(:quux_without_baz=, v) << '_with_baz'
- end
+ def test_delegate_missing_to_raises_delegation_error_if_target_nil
+ e = assert_raises(Module::DelegationError) do
+ DecoratedTester.new(nil).name
+ end
- def duck_with_orange
- duck_without_orange << '_with_orange'
+ assert_equal "name delegated to client, but client is nil", e.message
end
-end
-class MethodAliasingTest < ActiveSupport::TestCase
- def setup
- Object.const_set :FooClassWithBarMethod, Class.new { def bar() 'bar' end }
- @instance = FooClassWithBarMethod.new
- end
+ def test_delegate_missing_to_affects_respond_to
+ assert_respond_to DecoratedTester.new(@david), :name
+ assert_not_respond_to DecoratedTester.new(@david), :private_name
+ assert_not_respond_to DecoratedTester.new(@david), :my_fake_method
- def teardown
- Object.instance_eval { remove_const :FooClassWithBarMethod }
+ assert DecoratedTester.new(@david).respond_to?(:name, true)
+ assert_not DecoratedTester.new(@david).respond_to?(:private_name, true)
+ assert_not DecoratedTester.new(@david).respond_to?(:my_fake_method, true)
end
- def test_alias_method_chain_deprecated
- assert_deprecated(/alias_method_chain/) do
- Module.new do
- def base
- end
-
- def base_with_deprecated
- end
+ def test_delegate_missing_to_respects_superclass_missing
+ assert_equal 42, DecoratedTester.new(@david).extra_missing
- alias_method_chain :base, :deprecated
- end
- end
+ assert_respond_to DecoratedTester.new(@david), :extra_missing
end
- def test_alias_method_chain
- assert_deprecated(/alias_method_chain/) do
- assert @instance.respond_to?(:bar)
- feature_aliases = [:bar_with_baz, :bar_without_baz]
-
- feature_aliases.each do |method|
- assert !@instance.respond_to?(method)
- end
-
- assert_equal 'bar', @instance.bar
-
- FooClassWithBarMethod.class_eval { include BarMethodAliaser }
+ def test_delegate_with_case
+ event = Event.new(Tester.new)
+ assert_equal 1, event.foo
+ end
- feature_aliases.each do |method|
- assert_respond_to @instance, method
+ def test_private_delegate
+ location = Class.new do
+ def initialize(place)
+ @place = place
end
- assert_equal 'bar_with_baz', @instance.bar
- assert_equal 'bar', @instance.bar_without_baz
+ private(*delegate(:street, :city, to: :@place))
end
- end
- def test_alias_method_chain_with_punctuation_method
- assert_deprecated(/alias_method_chain/) do
- FooClassWithBarMethod.class_eval do
- def quux!; 'quux' end
- end
+ place = location.new(Somewhere.new("Such street", "Sad city"))
- assert !@instance.respond_to?(:quux_with_baz!)
- FooClassWithBarMethod.class_eval do
- include BarMethodAliaser
- alias_method_chain :quux!, :baz
- end
- assert_respond_to @instance, :quux_with_baz!
+ assert_not_respond_to place, :street
+ assert_not_respond_to place, :city
- assert_equal 'quux_with_baz', @instance.quux!
- assert_equal 'quux', @instance.quux_without_baz!
- end
+ assert place.respond_to?(:street, true) # Asking for private method
+ assert place.respond_to?(:city, true)
end
- def test_alias_method_chain_with_same_names_between_predicates_and_bang_methods
- assert_deprecated(/alias_method_chain/) do
- FooClassWithBarMethod.class_eval do
- def quux!; 'quux!' end
- def quux?; true end
- def quux=(v); 'quux=' end
+ def test_private_delegate_prefixed
+ location = Class.new do
+ def initialize(place)
+ @place = place
end
- assert !@instance.respond_to?(:quux_with_baz!)
- assert !@instance.respond_to?(:quux_with_baz?)
- assert !@instance.respond_to?(:quux_with_baz=)
-
- FooClassWithBarMethod.class_eval { include BarMethodAliaser }
- assert_respond_to @instance, :quux_with_baz!
- assert_respond_to @instance, :quux_with_baz?
- assert_respond_to @instance, :quux_with_baz=
-
+ private(*delegate(:street, :city, to: :@place, prefix: :the))
+ end
- FooClassWithBarMethod.alias_method_chain :quux!, :baz
- assert_equal 'quux!_with_baz', @instance.quux!
- assert_equal 'quux!', @instance.quux_without_baz!
+ place = location.new(Somewhere.new("Such street", "Sad city"))
- FooClassWithBarMethod.alias_method_chain :quux?, :baz
- assert_equal false, @instance.quux?
- assert_equal true, @instance.quux_without_baz?
+ assert_not_respond_to place, :street
+ assert_not_respond_to place, :city
- FooClassWithBarMethod.alias_method_chain :quux=, :baz
- assert_equal 'quux=_with_baz', @instance.send(:quux=, 1234)
- assert_equal 'quux=', @instance.send(:quux_without_baz=, 1234)
- end
+ assert_not_respond_to place, :the_street
+ assert place.respond_to?(:the_street, true)
+ assert_not_respond_to place, :the_city
+ assert place.respond_to?(:the_city, true)
end
- def test_alias_method_chain_with_feature_punctuation
- assert_deprecated(/alias_method_chain/) do
- FooClassWithBarMethod.class_eval do
- def quux; 'quux' end
- def quux?; 'quux?' end
- include BarMethodAliaser
- alias_method_chain :quux, :baz!
- end
-
- assert_nothing_raised do
- assert_equal 'quux_with_baz', @instance.quux_with_baz!
+ def test_private_delegate_with_private_option
+ location = Class.new do
+ def initialize(place)
+ @place = place
end
- assert_raise(NameError) do
- FooClassWithBarMethod.alias_method_chain :quux?, :baz!
- end
+ delegate(:street, :city, to: :@place, private: true)
end
- end
- def test_alias_method_chain_yields_target_and_punctuation
- assert_deprecated(/alias_method_chain/) do
- args = nil
+ place = location.new(Somewhere.new("Such street", "Sad city"))
- FooClassWithBarMethod.class_eval do
- def quux?; end
- include BarMethods
+ assert_not_respond_to place, :street
+ assert_not_respond_to place, :city
- FooClassWithBarMethod.alias_method_chain :quux?, :baz do |target, punctuation|
- args = [target, punctuation]
- end
+ assert place.respond_to?(:street, true) # Asking for private method
+ assert place.respond_to?(:city, true)
+ end
+
+ def test_some_public_some_private_delegate_with_private_option
+ location = Class.new do
+ def initialize(place)
+ @place = place
end
- assert_not_nil args
- assert_equal 'quux', args[0]
- assert_equal '?', args[1]
+ delegate(:street, to: :@place)
+ delegate(:city, to: :@place, private: true)
end
- end
- def test_alias_method_chain_preserves_private_method_status
- assert_deprecated(/alias_method_chain/) do
- FooClassWithBarMethod.class_eval do
- def duck; 'duck' end
- include BarMethodAliaser
- private :duck
- alias_method_chain :duck, :orange
- end
+ place = location.new(Somewhere.new("Such street", "Sad city"))
- assert_raise NoMethodError do
- @instance.duck
- end
+ assert_respond_to place, :street
+ assert_not_respond_to place, :city
- assert_equal 'duck_with_orange', @instance.instance_eval { duck }
- assert FooClassWithBarMethod.private_method_defined?(:duck)
- end
+ assert place.respond_to?(:city, true) # Asking for private method
end
- def test_alias_method_chain_preserves_protected_method_status
- assert_deprecated(/alias_method_chain/) do
- FooClassWithBarMethod.class_eval do
- def duck; 'duck' end
- include BarMethodAliaser
- protected :duck
- alias_method_chain :duck, :orange
- end
-
- assert_raise NoMethodError do
- @instance.duck
+ def test_private_delegate_prefixed_with_private_option
+ location = Class.new do
+ def initialize(place)
+ @place = place
end
- assert_equal 'duck_with_orange', @instance.instance_eval { duck }
- assert FooClassWithBarMethod.protected_method_defined?(:duck)
+ delegate(:street, :city, to: :@place, prefix: :the, private: true)
end
+
+ place = location.new(Somewhere.new("Such street", "Sad city"))
+
+ assert_not_respond_to place, :the_street
+ assert place.respond_to?(:the_street, true)
+ assert_not_respond_to place, :the_city
+ assert place.respond_to?(:the_city, true)
end
- def test_alias_method_chain_preserves_public_method_status
- assert_deprecated(/alias_method_chain/) do
- FooClassWithBarMethod.class_eval do
- def duck; 'duck' end
- include BarMethodAliaser
- public :duck
- alias_method_chain :duck, :orange
+ def test_delegate_with_private_option_returns_names_of_delegate_methods
+ location = Class.new do
+ def initialize(place)
+ @place = place
end
-
- assert_equal 'duck_with_orange', @instance.duck
- assert FooClassWithBarMethod.public_method_defined?(:duck)
end
- end
- def test_delegate_with_case
- event = Event.new(Tester.new)
- assert_equal 1, event.foo
+ assert_equal [:street, :city],
+ location.delegate(:street, :city, to: :@place, private: true)
+
+ assert_equal [:the_street, :the_city],
+ location.delegate(:street, :city, to: :@place, prefix: :the, private: true)
end
end
diff --git a/activesupport/test/core_ext/name_error_test.rb b/activesupport/test/core_ext/name_error_test.rb
index 7525f80cf0..5c6c12ffc7 100644
--- a/activesupport/test/core_ext/name_error_test.rb
+++ b/activesupport/test/core_ext/name_error_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'active_support/core_ext/name_error'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/name_error"
class NameErrorTest < ActiveSupport::TestCase
def test_name_error_should_set_missing_name
@@ -15,7 +17,7 @@ class NameErrorTest < ActiveSupport::TestCase
exc = assert_raise NameError do
some_method_that_does_not_exist
end
- assert !exc.missing_name?(:Foo)
+ assert_not exc.missing_name?(:Foo)
assert_nil exc.missing_name
end
end
diff --git a/activesupport/test/core_ext/numeric_ext_test.rb b/activesupport/test/core_ext/numeric_ext_test.rb
index 2d8796179e..5005b9febd 100644
--- a/activesupport/test/core_ext/numeric_ext_test.rb
+++ b/activesupport/test/core_ext/numeric_ext_test.rb
@@ -1,18 +1,20 @@
-require 'abstract_unit'
-require 'active_support/time'
-require 'active_support/core_ext/numeric'
-require 'active_support/core_ext/integer'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/time"
+require "active_support/core_ext/numeric"
+require "active_support/core_ext/integer"
class NumericExtTimeAndDateTimeTest < ActiveSupport::TestCase
def setup
- @now = Time.local(2005,2,10,15,30,45)
- @dtnow = DateTime.civil(2005,2,10,15,30,45)
+ @now = Time.local(2005, 2, 10, 15, 30, 45)
+ @dtnow = DateTime.civil(2005, 2, 10, 15, 30, 45)
@seconds = {
1.minute => 60,
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
@@ -23,53 +25,53 @@ class NumericExtTimeAndDateTimeTest < ActiveSupport::TestCase
end
def test_irregular_durations
- assert_equal @now.advance(:days => 3000), 3000.days.since(@now)
- assert_equal @now.advance(:months => 1), 1.month.since(@now)
- assert_equal @now.advance(:months => -1), 1.month.until(@now)
- assert_equal @now.advance(:years => 20), 20.years.since(@now)
- assert_equal @dtnow.advance(:days => 3000), 3000.days.since(@dtnow)
- assert_equal @dtnow.advance(:months => 1), 1.month.since(@dtnow)
- assert_equal @dtnow.advance(:months => -1), 1.month.until(@dtnow)
- assert_equal @dtnow.advance(:years => 20), 20.years.since(@dtnow)
+ assert_equal @now.advance(days: 3000), 3000.days.since(@now)
+ assert_equal @now.advance(months: 1), 1.month.since(@now)
+ assert_equal @now.advance(months: -1), 1.month.until(@now)
+ assert_equal @now.advance(years: 20), 20.years.since(@now)
+ assert_equal @dtnow.advance(days: 3000), 3000.days.since(@dtnow)
+ assert_equal @dtnow.advance(months: 1), 1.month.since(@dtnow)
+ assert_equal @dtnow.advance(months: -1), 1.month.until(@dtnow)
+ assert_equal @dtnow.advance(years: 20), 20.years.since(@dtnow)
end
def test_duration_addition
- assert_equal @now.advance(:days => 1).advance(:months => 1), (1.day + 1.month).since(@now)
- assert_equal @now.advance(:days => 7), (1.week + 5.seconds - 5.seconds).since(@now)
- assert_equal @now.advance(:years => 2), (4.years - 2.years).since(@now)
- assert_equal @dtnow.advance(:days => 1).advance(:months => 1), (1.day + 1.month).since(@dtnow)
- assert_equal @dtnow.advance(:days => 7), (1.week + 5.seconds - 5.seconds).since(@dtnow)
- assert_equal @dtnow.advance(:years => 2), (4.years - 2.years).since(@dtnow)
+ assert_equal @now.advance(days: 1).advance(months: 1), (1.day + 1.month).since(@now)
+ assert_equal @now.advance(days: 7), (1.week + 5.seconds - 5.seconds).since(@now)
+ assert_equal @now.advance(years: 2), (4.years - 2.years).since(@now)
+ assert_equal @dtnow.advance(days: 1).advance(months: 1), (1.day + 1.month).since(@dtnow)
+ assert_equal @dtnow.advance(days: 7), (1.week + 5.seconds - 5.seconds).since(@dtnow)
+ assert_equal @dtnow.advance(years: 2), (4.years - 2.years).since(@dtnow)
end
def test_time_plus_duration
assert_equal @now + 8, @now + 8.seconds
assert_equal @now + 22.9, @now + 22.9.seconds
- assert_equal @now.advance(:days => 15), @now + 15.days
- assert_equal @now.advance(:months => 1), @now + 1.month
+ assert_equal @now.advance(days: 15), @now + 15.days
+ assert_equal @now.advance(months: 1), @now + 1.month
assert_equal @dtnow.since(8), @dtnow + 8.seconds
assert_equal @dtnow.since(22.9), @dtnow + 22.9.seconds
- assert_equal @dtnow.advance(:days => 15), @dtnow + 15.days
- assert_equal @dtnow.advance(:months => 1), @dtnow + 1.month
+ assert_equal @dtnow.advance(days: 15), @dtnow + 15.days
+ assert_equal @dtnow.advance(months: 1), @dtnow + 1.month
end
def test_chaining_duration_operations
- assert_equal @now.advance(:days => 2).advance(:months => -3), @now + 2.days - 3.months
- assert_equal @now.advance(:days => 1).advance(:months => 2), @now + 1.day + 2.months
- assert_equal @dtnow.advance(:days => 2).advance(:months => -3), @dtnow + 2.days - 3.months
- assert_equal @dtnow.advance(:days => 1).advance(:months => 2), @dtnow + 1.day + 2.months
+ assert_equal @now.advance(days: 2).advance(months: -3), @now + 2.days - 3.months
+ assert_equal @now.advance(days: 1).advance(months: 2), @now + 1.day + 2.months
+ assert_equal @dtnow.advance(days: 2).advance(months: -3), @dtnow + 2.days - 3.months
+ assert_equal @dtnow.advance(days: 1).advance(months: 2), @dtnow + 1.day + 2.months
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
- assert_equal Time.utc(2005,2,28,15,15,10), Time.utc(2004,2,29,15,15,10) + 1.year
- assert_equal DateTime.civil(2005,2,28,15,15,10), DateTime.civil(2004,2,29,15,15,10) + 1.year
+ assert_equal Time.utc(2005, 2, 28, 15, 15, 10), Time.utc(2004, 2, 29, 15, 15, 10) + 1.year
+ assert_equal DateTime.civil(2005, 2, 28, 15, 15, 10), DateTime.civil(2004, 2, 29, 15, 15, 10) + 1.year
end
end
@@ -83,16 +85,16 @@ class NumericExtDateTest < ActiveSupport::TestCase
assert_equal @today >> 1, @today + 1.month
assert_equal @today.to_time.since(1), @today + 1.second
assert_equal @today.to_time.since(60), @today + 1.minute
- assert_equal @today.to_time.since(60*60), @today + 1.hour
+ assert_equal @today.to_time.since(60 * 60), @today + 1.hour
end
def test_chaining_duration_operations
- assert_equal @today.advance(:days => 2).advance(:months => -3), @today + 2.days - 3.months
- assert_equal @today.advance(:days => 1).advance(:months => 2), @today + 1.day + 2.months
+ assert_equal @today.advance(days: 2).advance(months: -3), @today + 2.days - 3.months
+ assert_equal @today.advance(days: 1).advance(months: 2), @today + 1.day + 2.months
end
def test_add_one_year_to_leap_day
- assert_equal Date.new(2005,2,28), Date.new(2004,2,29) + 1.year
+ assert_equal Date.new(2005, 2, 28), Date.new(2004, 2, 29) + 1.year
end
end
@@ -102,12 +104,12 @@ class NumericExtSizeTest < ActiveSupport::TestCase
assert_equal 1024.kilobytes, 1.megabyte
assert_equal 3584.0.kilobytes, 3.5.megabytes
assert_equal 3584.0.megabytes, 3.5.gigabytes
- assert_equal 1.kilobyte ** 4, 1.terabyte
+ assert_equal 1.kilobyte**4, 1.terabyte
assert_equal 1024.kilobytes + 2.megabytes, 3.megabytes
assert_equal 2.gigabytes / 4, 512.megabytes
assert_equal 256.megabytes * 20 + 5.gigabytes, 10.gigabytes
- assert_equal 1.kilobyte ** 5, 1.petabyte
- assert_equal 1.kilobyte ** 6, 1.exabyte
+ assert_equal 1.kilobyte**5, 1.petabyte
+ assert_equal 1.kilobyte**6, 1.exabyte
end
def test_units_as_bytes_independently
@@ -143,56 +145,63 @@ class NumericExtFormattingTest < ActiveSupport::TestCase
gigabytes(number) * 1024
end
+ def petabytes(number)
+ terabytes(number) * 1024
+ end
+
+ def exabytes(number)
+ petabytes(number) * 1024
+ end
+
def test_to_s__phone
assert_equal("555-1234", 5551234.to_s(:phone))
assert_equal("800-555-1212", 8005551212.to_s(:phone))
- assert_equal("(800) 555-1212", 8005551212.to_s(:phone, :area_code => true))
- assert_equal("800 555 1212", 8005551212.to_s(:phone, :delimiter => " "))
- assert_equal("(800) 555-1212 x 123", 8005551212.to_s(:phone, :area_code => true, :extension => 123))
- assert_equal("800-555-1212", 8005551212.to_s(:phone, :extension => " "))
- assert_equal("555.1212", 5551212.to_s(:phone, :delimiter => '.'))
- assert_equal("+1-800-555-1212", 8005551212.to_s(:phone, :country_code => 1))
- assert_equal("+18005551212", 8005551212.to_s(:phone, :country_code => 1, :delimiter => ''))
+ assert_equal("(800) 555-1212", 8005551212.to_s(:phone, area_code: true))
+ assert_equal("800 555 1212", 8005551212.to_s(:phone, delimiter: " "))
+ assert_equal("(800) 555-1212 x 123", 8005551212.to_s(:phone, area_code: true, extension: 123))
+ assert_equal("800-555-1212", 8005551212.to_s(:phone, extension: " "))
+ assert_equal("555.1212", 5551212.to_s(:phone, delimiter: "."))
+ assert_equal("+1-800-555-1212", 8005551212.to_s(:phone, country_code: 1))
+ assert_equal("+18005551212", 8005551212.to_s(:phone, country_code: 1, delimiter: ""))
assert_equal("22-555-1212", 225551212.to_s(:phone))
- assert_equal("+45-22-555-1212", 225551212.to_s(:phone, :country_code => 45))
+ assert_equal("+45-22-555-1212", 225551212.to_s(:phone, country_code: 45))
end
def test_to_s__currency
assert_equal("$1,234,567,890.50", 1234567890.50.to_s(:currency))
assert_equal("$1,234,567,890.51", 1234567890.506.to_s(:currency))
assert_equal("-$1,234,567,890.50", -1234567890.50.to_s(:currency))
- assert_equal("-$ 1,234,567,890.50", -1234567890.50.to_s(:currency, :format => "%u %n"))
- assert_equal("($1,234,567,890.50)", -1234567890.50.to_s(:currency, :negative_format => "(%u%n)"))
- assert_equal("$1,234,567,892", 1234567891.50.to_s(:currency, :precision => 0))
- assert_equal("$1,234,567,890.5", 1234567890.50.to_s(:currency, :precision => 1))
- assert_equal("&pound;1234567890,50", 1234567890.50.to_s(:currency, :unit => "&pound;", :separator => ",", :delimiter => ""))
+ assert_equal("-$ 1,234,567,890.50", -1234567890.50.to_s(:currency, format: "%u %n"))
+ assert_equal("($1,234,567,890.50)", -1234567890.50.to_s(:currency, negative_format: "(%u%n)"))
+ assert_equal("$1,234,567,892", 1234567891.50.to_s(:currency, precision: 0))
+ assert_equal("$1,234,567,890.5", 1234567890.50.to_s(:currency, precision: 1))
+ assert_equal("&pound;1234567890,50", 1234567890.50.to_s(:currency, unit: "&pound;", separator: ",", delimiter: ""))
end
-
def test_to_s__rounded
assert_equal("-111.235", -111.2346.to_s(:rounded))
assert_equal("111.235", 111.2346.to_s(:rounded))
- assert_equal("31.83", 31.825.to_s(:rounded, :precision => 2))
- assert_equal("111.23", 111.2346.to_s(:rounded, :precision => 2))
- assert_equal("111.00", 111.to_s(:rounded, :precision => 2))
- assert_equal("3268", (32.6751 * 100.00).to_s(:rounded, :precision => 0))
- assert_equal("112", 111.50.to_s(:rounded, :precision => 0))
- assert_equal("1234567892", 1234567891.50.to_s(:rounded, :precision => 0))
- assert_equal("0", 0.to_s(:rounded, :precision => 0))
- assert_equal("0.00100", 0.001.to_s(:rounded, :precision => 5))
- assert_equal("0.001", 0.00111.to_s(:rounded, :precision => 3))
- assert_equal("10.00", 9.995.to_s(:rounded, :precision => 2))
- assert_equal("11.00", 10.995.to_s(:rounded, :precision => 2))
- assert_equal("0.00", -0.001.to_s(:rounded, :precision => 2))
+ assert_equal("31.83", 31.825.to_s(:rounded, precision: 2))
+ assert_equal("111.23", 111.2346.to_s(:rounded, precision: 2))
+ assert_equal("111.00", 111.to_s(:rounded, precision: 2))
+ assert_equal("3268", (32.6751 * 100.00).to_s(:rounded, precision: 0))
+ assert_equal("112", 111.50.to_s(:rounded, precision: 0))
+ assert_equal("1234567892", 1234567891.50.to_s(:rounded, precision: 0))
+ assert_equal("0", 0.to_s(:rounded, precision: 0))
+ assert_equal("0.00100", 0.001.to_s(:rounded, precision: 5))
+ assert_equal("0.001", 0.00111.to_s(:rounded, precision: 3))
+ assert_equal("10.00", 9.995.to_s(:rounded, precision: 2))
+ assert_equal("11.00", 10.995.to_s(:rounded, precision: 2))
+ assert_equal("0.00", -0.001.to_s(:rounded, precision: 2))
end
def test_to_s__percentage
assert_equal("100.000%", 100.to_s(:percentage))
- assert_equal("100%", 100.to_s(:percentage, :precision => 0))
- assert_equal("302.06%", 302.0574.to_s(:percentage, :precision => 2))
- assert_equal("123.4%", 123.400.to_s(:percentage, :precision => 3, :strip_insignificant_zeros => true))
- assert_equal("1.000,000%", 1000.to_s(:percentage, :delimiter => '.', :separator => ','))
- assert_equal("1000.000 %", 1000.to_s(:percentage, :format => "%n %"))
+ assert_equal("100%", 100.to_s(:percentage, precision: 0))
+ assert_equal("302.06%", 302.0574.to_s(:percentage, precision: 2))
+ assert_equal("123.4%", 123.400.to_s(:percentage, precision: 3, strip_insignificant_zeros: true))
+ assert_equal("1.000,000%", 1000.to_s(:percentage, delimiter: ".", separator: ","))
+ assert_equal("1000.000 %", 1000.to_s(:percentage, format: "%n %"))
end
def test_to_s__delimited
@@ -208,269 +217,198 @@ class NumericExtFormattingTest < ActiveSupport::TestCase
end
def test_to_s__delimited__with_options_hash
- assert_equal '12 345 678', 12345678.to_s(:delimited, :delimiter => ' ')
- assert_equal '12,345,678-05', 12345678.05.to_s(:delimited, :separator => '-')
- assert_equal '12.345.678,05', 12345678.05.to_s(:delimited, :separator => ',', :delimiter => '.')
- assert_equal '12.345.678,05', 12345678.05.to_s(:delimited, :delimiter => '.', :separator => ',')
+ assert_equal "12 345 678", 12345678.to_s(:delimited, delimiter: " ")
+ assert_equal "12,345,678-05", 12345678.05.to_s(:delimited, separator: "-")
+ assert_equal "12.345.678,05", 12345678.05.to_s(:delimited, separator: ",", delimiter: ".")
+ assert_equal "12.345.678,05", 12345678.05.to_s(:delimited, delimiter: ".", separator: ",")
end
-
def test_to_s__rounded_with_custom_delimiter_and_separator
- assert_equal '31,83', 31.825.to_s(:rounded, :precision => 2, :separator => ',')
- assert_equal '1.231,83', 1231.825.to_s(:rounded, :precision => 2, :separator => ',', :delimiter => '.')
+ assert_equal "31,83", 31.825.to_s(:rounded, precision: 2, separator: ",")
+ assert_equal "1.231,83", 1231.825.to_s(:rounded, precision: 2, separator: ",", delimiter: ".")
end
def test_to_s__rounded__with_significant_digits
- assert_equal "124000", 123987.to_s(:rounded, :precision => 3, :significant => true)
- assert_equal "120000000", 123987876.to_s(:rounded, :precision => 2, :significant => true )
- assert_equal "9775", 9775.to_s(:rounded, :precision => 4, :significant => true )
- assert_equal "5.4", 5.3923.to_s(:rounded, :precision => 2, :significant => true )
- assert_equal "5", 5.3923.to_s(:rounded, :precision => 1, :significant => true )
- assert_equal "1", 1.232.to_s(:rounded, :precision => 1, :significant => true )
- assert_equal "7", 7.to_s(:rounded, :precision => 1, :significant => true )
- assert_equal "1", 1.to_s(:rounded, :precision => 1, :significant => true )
- assert_equal "53", 52.7923.to_s(:rounded, :precision => 2, :significant => true )
- assert_equal "9775.00", 9775.to_s(:rounded, :precision => 6, :significant => true )
- assert_equal "5.392900", 5.3929.to_s(:rounded, :precision => 7, :significant => true )
- assert_equal "0.0", 0.to_s(:rounded, :precision => 2, :significant => true )
- assert_equal "0", 0.to_s(:rounded, :precision => 1, :significant => true )
- assert_equal "0.0001", 0.0001.to_s(:rounded, :precision => 1, :significant => true )
- assert_equal "0.000100", 0.0001.to_s(:rounded, :precision => 3, :significant => true )
- assert_equal "0.0001", 0.0001111.to_s(:rounded, :precision => 1, :significant => true )
- assert_equal "10.0", 9.995.to_s(:rounded, :precision => 3, :significant => true)
- assert_equal "9.99", 9.994.to_s(:rounded, :precision => 3, :significant => true)
- assert_equal "11.0", 10.995.to_s(:rounded, :precision => 3, :significant => true)
+ assert_equal "124000", 123987.to_s(:rounded, precision: 3, significant: true)
+ assert_equal "120000000", 123987876.to_s(:rounded, precision: 2, significant: true)
+ assert_equal "9775", 9775.to_s(:rounded, precision: 4, significant: true)
+ assert_equal "5.4", 5.3923.to_s(:rounded, precision: 2, significant: true)
+ assert_equal "5", 5.3923.to_s(:rounded, precision: 1, significant: true)
+ assert_equal "1", 1.232.to_s(:rounded, precision: 1, significant: true)
+ assert_equal "7", 7.to_s(:rounded, precision: 1, significant: true)
+ assert_equal "1", 1.to_s(:rounded, precision: 1, significant: true)
+ assert_equal "53", 52.7923.to_s(:rounded, precision: 2, significant: true)
+ assert_equal "9775.00", 9775.to_s(:rounded, precision: 6, significant: true)
+ assert_equal "5.392900", 5.3929.to_s(:rounded, precision: 7, significant: true)
+ assert_equal "0.0", 0.to_s(:rounded, precision: 2, significant: true)
+ assert_equal "0", 0.to_s(:rounded, precision: 1, significant: true)
+ assert_equal "0.0001", 0.0001.to_s(:rounded, precision: 1, significant: true)
+ assert_equal "0.000100", 0.0001.to_s(:rounded, precision: 3, significant: true)
+ assert_equal "0.0001", 0.0001111.to_s(:rounded, precision: 1, significant: true)
+ assert_equal "10.0", 9.995.to_s(:rounded, precision: 3, significant: true)
+ assert_equal "9.99", 9.994.to_s(:rounded, precision: 3, significant: true)
+ assert_equal "11.0", 10.995.to_s(:rounded, precision: 3, significant: true)
end
def test_to_s__rounded__with_strip_insignificant_zeros
- assert_equal "9775.43", 9775.43.to_s(:rounded, :precision => 4, :strip_insignificant_zeros => true )
- assert_equal "9775.2", 9775.2.to_s(:rounded, :precision => 6, :significant => true, :strip_insignificant_zeros => true )
- assert_equal "0", 0.to_s(:rounded, :precision => 6, :significant => true, :strip_insignificant_zeros => true )
+ assert_equal "9775.43", 9775.43.to_s(:rounded, precision: 4, strip_insignificant_zeros: true)
+ assert_equal "9775.2", 9775.2.to_s(:rounded, precision: 6, significant: true, strip_insignificant_zeros: true)
+ assert_equal "0", 0.to_s(:rounded, precision: 6, significant: true, strip_insignificant_zeros: true)
end
def test_to_s__rounded__with_significant_true_and_zero_precision
# Zero precision with significant is a mistake (would always return zero),
# so we treat it as if significant was false (increases backwards compatibility for number_to_human_size)
- assert_equal "124", 123.987.to_s(:rounded, :precision => 0, :significant => true)
- assert_equal "12", 12.to_s(:rounded, :precision => 0, :significant => true )
+ assert_equal "124", 123.987.to_s(:rounded, precision: 0, significant: true)
+ assert_equal "12", 12.to_s(:rounded, precision: 0, significant: true)
end
def test_to_s__human_size
- assert_equal '0 Bytes', 0.to_s(:human_size)
- assert_equal '1 Byte', 1.to_s(:human_size)
- assert_equal '3 Bytes', 3.14159265.to_s(:human_size)
- assert_equal '123 Bytes', 123.0.to_s(:human_size)
- assert_equal '123 Bytes', 123.to_s(:human_size)
- assert_equal '1.21 KB', 1234.to_s(:human_size)
- assert_equal '12.1 KB', 12345.to_s(:human_size)
- assert_equal '1.18 MB', 1234567.to_s(:human_size)
- assert_equal '1.15 GB', 1234567890.to_s(:human_size)
- assert_equal '1.12 TB', 1234567890123.to_s(:human_size)
- assert_equal '1030 TB', terabytes(1026).to_s(:human_size)
- assert_equal '444 KB', kilobytes(444).to_s(:human_size)
- assert_equal '1020 MB', megabytes(1023).to_s(:human_size)
- assert_equal '3 TB', terabytes(3).to_s(:human_size)
- assert_equal '1.2 MB', 1234567.to_s(:human_size, :precision => 2)
- assert_equal '3 Bytes', 3.14159265.to_s(:human_size, :precision => 4)
- assert_equal '1 KB', kilobytes(1.0123).to_s(:human_size, :precision => 2)
- assert_equal '1.01 KB', kilobytes(1.0100).to_s(:human_size, :precision => 4)
- assert_equal '10 KB', kilobytes(10.000).to_s(:human_size, :precision => 4)
- assert_equal '1 Byte', 1.1.to_s(:human_size)
- assert_equal '10 Bytes', 10.to_s(:human_size)
- end
-
- def test_to_s__human_size_with_si_prefix
- assert_equal '3 Bytes', 3.14159265.to_s(:human_size, :prefix => :si)
- assert_equal '123 Bytes', 123.0.to_s(:human_size, :prefix => :si)
- assert_equal '123 Bytes', 123.to_s(:human_size, :prefix => :si)
- assert_equal '1.23 KB', 1234.to_s(:human_size, :prefix => :si)
- assert_equal '12.3 KB', 12345.to_s(:human_size, :prefix => :si)
- assert_equal '1.23 MB', 1234567.to_s(:human_size, :prefix => :si)
- assert_equal '1.23 GB', 1234567890.to_s(:human_size, :prefix => :si)
- assert_equal '1.23 TB', 1234567890123.to_s(:human_size, :prefix => :si)
+ assert_equal "0 Bytes", 0.to_s(:human_size)
+ assert_equal "1 Byte", 1.to_s(:human_size)
+ assert_equal "3 Bytes", 3.14159265.to_s(:human_size)
+ assert_equal "123 Bytes", 123.0.to_s(:human_size)
+ assert_equal "123 Bytes", 123.to_s(:human_size)
+ assert_equal "1.21 KB", 1234.to_s(:human_size)
+ assert_equal "12.1 KB", 12345.to_s(:human_size)
+ assert_equal "1.18 MB", 1234567.to_s(:human_size)
+ assert_equal "1.15 GB", 1234567890.to_s(:human_size)
+ assert_equal "1.12 TB", 1234567890123.to_s(:human_size)
+ assert_equal "1.1 PB", 1234567890123456.to_s(:human_size)
+ assert_equal "1.07 EB", 1234567890123456789.to_s(:human_size)
+ assert_equal "1030 EB", exabytes(1026).to_s(:human_size)
+ assert_equal "444 KB", kilobytes(444).to_s(:human_size)
+ assert_equal "1020 MB", megabytes(1023).to_s(:human_size)
+ assert_equal "3 TB", terabytes(3).to_s(:human_size)
+ assert_equal "1.2 MB", 1234567.to_s(:human_size, precision: 2)
+ assert_equal "3 Bytes", 3.14159265.to_s(:human_size, precision: 4)
+ assert_equal "1 KB", kilobytes(1.0123).to_s(:human_size, precision: 2)
+ assert_equal "1.01 KB", kilobytes(1.0100).to_s(:human_size, precision: 4)
+ assert_equal "10 KB", kilobytes(10.000).to_s(:human_size, precision: 4)
+ assert_equal "1 Byte", 1.1.to_s(:human_size)
+ assert_equal "10 Bytes", 10.to_s(:human_size)
end
def test_to_s__human_size_with_options_hash
- assert_equal '1.2 MB', 1234567.to_s(:human_size, :precision => 2)
- assert_equal '3 Bytes', 3.14159265.to_s(:human_size, :precision => 4)
- assert_equal '1 KB', kilobytes(1.0123).to_s(:human_size, :precision => 2)
- assert_equal '1.01 KB', kilobytes(1.0100).to_s(:human_size, :precision => 4)
- assert_equal '10 KB', kilobytes(10.000).to_s(:human_size, :precision => 4)
- assert_equal '1 TB', 1234567890123.to_s(:human_size, :precision => 1)
- assert_equal '500 MB', 524288000.to_s(:human_size, :precision=>3)
- assert_equal '10 MB', 9961472.to_s(:human_size, :precision=>0)
- assert_equal '40 KB', 41010.to_s(:human_size, :precision => 1)
- assert_equal '40 KB', 41100.to_s(:human_size, :precision => 2)
- assert_equal '1.0 KB', kilobytes(1.0123).to_s(:human_size, :precision => 2, :strip_insignificant_zeros => false)
- assert_equal '1.012 KB', kilobytes(1.0123).to_s(:human_size, :precision => 3, :significant => false)
- assert_equal '1 KB', kilobytes(1.0123).to_s(:human_size, :precision => 0, :significant => true) #ignores significant it precision is 0
+ assert_equal "1.2 MB", 1234567.to_s(:human_size, precision: 2)
+ assert_equal "3 Bytes", 3.14159265.to_s(:human_size, precision: 4)
+ assert_equal "1 KB", kilobytes(1.0123).to_s(:human_size, precision: 2)
+ assert_equal "1.01 KB", kilobytes(1.0100).to_s(:human_size, precision: 4)
+ assert_equal "10 KB", kilobytes(10.000).to_s(:human_size, precision: 4)
+ assert_equal "1 TB", 1234567890123.to_s(:human_size, precision: 1)
+ assert_equal "500 MB", 524288000.to_s(:human_size, precision: 3)
+ assert_equal "10 MB", 9961472.to_s(:human_size, precision: 0)
+ assert_equal "40 KB", 41010.to_s(:human_size, precision: 1)
+ assert_equal "40 KB", 41100.to_s(:human_size, precision: 2)
+ assert_equal "1.0 KB", kilobytes(1.0123).to_s(:human_size, precision: 2, strip_insignificant_zeros: false)
+ assert_equal "1.012 KB", kilobytes(1.0123).to_s(:human_size, precision: 3, significant: false)
+ assert_equal "1 KB", kilobytes(1.0123).to_s(:human_size, precision: 0, significant: true) # ignores significant it precision is 0
end
def test_to_s__human_size_with_custom_delimiter_and_separator
- assert_equal '1,01 KB', kilobytes(1.0123).to_s(:human_size, :precision => 3, :separator => ',')
- assert_equal '1,01 KB', kilobytes(1.0100).to_s(:human_size, :precision => 4, :separator => ',')
- assert_equal '1.000,1 TB', terabytes(1000.1).to_s(:human_size, :precision => 5, :delimiter => '.', :separator => ',')
+ assert_equal "1,01 KB", kilobytes(1.0123).to_s(:human_size, precision: 3, separator: ",")
+ assert_equal "1,01 KB", kilobytes(1.0100).to_s(:human_size, precision: 4, separator: ",")
+ assert_equal "1.000,1 TB", terabytes(1000.1).to_s(:human_size, precision: 5, delimiter: ".", separator: ",")
end
def test_number_to_human
- assert_equal '-123', -123.to_s(:human)
- assert_equal '-0.5', -0.5.to_s(:human)
- assert_equal '0', 0.to_s(:human)
- assert_equal '0.5', 0.5.to_s(:human)
- assert_equal '123', 123.to_s(:human)
- assert_equal '1.23 Thousand', 1234.to_s(:human)
- assert_equal '12.3 Thousand', 12345.to_s(:human)
- assert_equal '1.23 Million', 1234567.to_s(:human)
- assert_equal '1.23 Billion', 1234567890.to_s(:human)
- assert_equal '1.23 Trillion', 1234567890123.to_s(:human)
- assert_equal '1.23 Quadrillion', 1234567890123456.to_s(:human)
- assert_equal '1230 Quadrillion', 1234567890123456789.to_s(:human)
- assert_equal '490 Thousand', 489939.to_s(:human, :precision => 2)
- assert_equal '489.9 Thousand', 489939.to_s(:human, :precision => 4)
- assert_equal '489 Thousand', 489000.to_s(:human, :precision => 4)
- assert_equal '489.0 Thousand', 489000.to_s(:human, :precision => 4, :strip_insignificant_zeros => false)
- assert_equal '1.2346 Million', 1234567.to_s(:human, :precision => 4, :significant => false)
- assert_equal '1,2 Million', 1234567.to_s(:human, :precision => 1, :significant => false, :separator => ',')
- assert_equal '1 Million', 1234567.to_s(:human, :precision => 0, :significant => true, :separator => ',') #significant forced to false
+ assert_equal "-123", -123.to_s(:human)
+ assert_equal "-0.5", -0.5.to_s(:human)
+ assert_equal "0", 0.to_s(:human)
+ assert_equal "0.5", 0.5.to_s(:human)
+ assert_equal "123", 123.to_s(:human)
+ assert_equal "1.23 Thousand", 1234.to_s(:human)
+ assert_equal "12.3 Thousand", 12345.to_s(:human)
+ assert_equal "1.23 Million", 1234567.to_s(:human)
+ assert_equal "1.23 Billion", 1234567890.to_s(:human)
+ assert_equal "1.23 Trillion", 1234567890123.to_s(:human)
+ assert_equal "1.23 Quadrillion", 1234567890123456.to_s(:human)
+ assert_equal "1230 Quadrillion", 1234567890123456789.to_s(:human)
+ assert_equal "490 Thousand", 489939.to_s(:human, precision: 2)
+ assert_equal "489.9 Thousand", 489939.to_s(:human, precision: 4)
+ assert_equal "489 Thousand", 489000.to_s(:human, precision: 4)
+ assert_equal "489.0 Thousand", 489000.to_s(:human, precision: 4, strip_insignificant_zeros: false)
+ assert_equal "1.2346 Million", 1234567.to_s(:human, precision: 4, significant: false)
+ assert_equal "1,2 Million", 1234567.to_s(:human, precision: 1, significant: false, separator: ",")
+ assert_equal "1 Million", 1234567.to_s(:human, precision: 0, significant: true, separator: ",") # significant forced to false
end
def test_number_to_human_with_custom_units
- #Only integers
- volume = {:unit => "ml", :thousand => "lt", :million => "m3"}
- assert_equal '123 lt', 123456.to_s(:human, :units => volume)
- assert_equal '12 ml', 12.to_s(:human, :units => volume)
- assert_equal '1.23 m3', 1234567.to_s(:human, :units => volume)
-
- #Including fractionals
- distance = {:mili => "mm", :centi => "cm", :deci => "dm", :unit => "m", :ten => "dam", :hundred => "hm", :thousand => "km"}
- assert_equal '1.23 mm', 0.00123.to_s(:human, :units => distance)
- assert_equal '1.23 cm', 0.0123.to_s(:human, :units => distance)
- assert_equal '1.23 dm', 0.123.to_s(:human, :units => distance)
- assert_equal '1.23 m', 1.23.to_s(:human, :units => distance)
- assert_equal '1.23 dam', 12.3.to_s(:human, :units => distance)
- assert_equal '1.23 hm', 123.to_s(:human, :units => distance)
- assert_equal '1.23 km', 1230.to_s(:human, :units => distance)
- assert_equal '1.23 km', 1230.to_s(:human, :units => distance)
- assert_equal '1.23 km', 1230.to_s(:human, :units => distance)
- assert_equal '12.3 km', 12300.to_s(:human, :units => distance)
-
- #The quantifiers don't need to be a continuous sequence
- gangster = {:hundred => "hundred bucks", :million => "thousand quids"}
- assert_equal '1 hundred bucks', 100.to_s(:human, :units => gangster)
- assert_equal '25 hundred bucks', 2500.to_s(:human, :units => gangster)
- assert_equal '25 thousand quids', 25000000.to_s(:human, :units => gangster)
- assert_equal '12300 thousand quids', 12345000000.to_s(:human, :units => gangster)
-
- #Spaces are stripped from the resulting string
- assert_equal '4', 4.to_s(:human, :units => {:unit => "", :ten => 'tens '})
- assert_equal '4.5 tens', 45.to_s(:human, :units => {:unit => "", :ten => ' tens '})
+ # Only integers
+ volume = { unit: "ml", thousand: "lt", million: "m3" }
+ assert_equal "123 lt", 123456.to_s(:human, units: volume)
+ assert_equal "12 ml", 12.to_s(:human, units: volume)
+ assert_equal "1.23 m3", 1234567.to_s(:human, units: volume)
+
+ # Including fractionals
+ distance = { mili: "mm", centi: "cm", deci: "dm", unit: "m", ten: "dam", hundred: "hm", thousand: "km" }
+ assert_equal "1.23 mm", 0.00123.to_s(:human, units: distance)
+ assert_equal "1.23 cm", 0.0123.to_s(:human, units: distance)
+ assert_equal "1.23 dm", 0.123.to_s(:human, units: distance)
+ assert_equal "1.23 m", 1.23.to_s(:human, units: distance)
+ assert_equal "1.23 dam", 12.3.to_s(:human, units: distance)
+ assert_equal "1.23 hm", 123.to_s(:human, units: distance)
+ assert_equal "1.23 km", 1230.to_s(:human, units: distance)
+ assert_equal "1.23 km", 1230.to_s(:human, units: distance)
+ assert_equal "1.23 km", 1230.to_s(:human, units: distance)
+ assert_equal "12.3 km", 12300.to_s(:human, units: distance)
+
+ # The quantifiers don't need to be a continuous sequence
+ gangster = { hundred: "hundred bucks", million: "thousand quids" }
+ assert_equal "1 hundred bucks", 100.to_s(:human, units: gangster)
+ assert_equal "25 hundred bucks", 2500.to_s(:human, units: gangster)
+ assert_equal "25 thousand quids", 25000000.to_s(:human, units: gangster)
+ assert_equal "12300 thousand quids", 12345000000.to_s(:human, units: gangster)
+
+ # Spaces are stripped from the resulting string
+ assert_equal "4", 4.to_s(:human, units: { unit: "", ten: "tens " })
+ assert_equal "4.5 tens", 45.to_s(:human, units: { unit: "", ten: " tens " })
end
def test_number_to_human_with_custom_format
- assert_equal '123 times Thousand', 123456.to_s(:human, :format => "%n times %u")
- volume = {:unit => "ml", :thousand => "lt", :million => "m3"}
- assert_equal '123.lt', 123456.to_s(:human, :units => volume, :format => "%n.%u")
+ assert_equal "123 times Thousand", 123456.to_s(:human, format: "%n times %u")
+ volume = { unit: "ml", thousand: "lt", million: "m3" }
+ assert_equal "123.lt", 123456.to_s(:human, units: volume, format: "%n.%u")
end
def test_to_s__injected_on_proper_types
- assert_equal Fixnum, 1230.class
- assert_equal '1.23 Thousand', 1230.to_s(:human)
+ assert_equal "1.23 Thousand", 1230.to_s(:human)
+ assert_equal "1.23 Thousand", Float(1230).to_s(:human)
+ assert_equal "100000 Quadrillion", (100**10).to_s(:human)
+ assert_equal "1 Million", BigDecimal("1000010").to_s(:human)
+ end
- assert_equal Float, Float(1230).class
- assert_equal '1.23 Thousand', Float(1230).to_s(:human)
+ def test_to_s_with_invalid_formatter
+ assert_equal "123", 123.to_s(:invalid)
+ assert_equal "2.5", 2.5.to_s(:invalid)
+ assert_equal "100000000000000000000", (100**10).to_s(:invalid)
+ assert_equal "1000010.0", BigDecimal("1000010").to_s(:invalid)
+ end
+
+ def test_default_to_s
+ assert_equal "123", 123.to_s
+ assert_equal "1111011", 123.to_s(2)
+
+ assert_equal "2.5", 2.5.to_s
+
+ assert_equal "100000000000000000000", (100**10).to_s
+ assert_equal "1010110101111000111010111100010110101100011000100000000000000000000", (100**10).to_s(2)
- assert_equal Bignum, (100**10).class
- assert_equal '100000 Quadrillion', (100**10).to_s(:human)
+ assert_equal "1000010.0", BigDecimal("1000010").to_s
+ assert_equal "10000 10.0", BigDecimal("1000010").to_s("5F")
- assert_equal BigDecimal, BigDecimal("1000010").class
- assert_equal '1 Million', BigDecimal("1000010").to_s(:human)
+ assert_raises TypeError do
+ 1.to_s({})
+ end
end
def test_in_milliseconds
assert_equal 10_000, 10.seconds.in_milliseconds
end
- # TODO: Remove positive and negative tests when we drop support to ruby < 2.3
- b = 2**64
- b *= b until Bignum === b
-
- T_ZERO = b.coerce(0).first
- T_ONE = b.coerce(1).first
- T_MONE = b.coerce(-1).first
-
- def test_positive
- assert_predicate(1, :positive?)
- assert_not_predicate(0, :positive?)
- assert_not_predicate(-1, :positive?)
- assert_predicate(+1.0, :positive?)
- assert_not_predicate(+0.0, :positive?)
- assert_not_predicate(-0.0, :positive?)
- assert_not_predicate(-1.0, :positive?)
- assert_predicate(+(0.0.next_float), :positive?)
- assert_not_predicate(-(0.0.next_float), :positive?)
- assert_predicate(Float::INFINITY, :positive?)
- assert_not_predicate(-Float::INFINITY, :positive?)
- assert_not_predicate(Float::NAN, :positive?)
-
- a = Class.new(Numeric) do
- def >(x); true; end
- end.new
- assert_predicate(a, :positive?)
-
- a = Class.new(Numeric) do
- def >(x); false; end
- end.new
- assert_not_predicate(a, :positive?)
-
- assert_predicate(1/2r, :positive?)
- assert_not_predicate(-1/2r, :positive?)
-
- assert_predicate(T_ONE, :positive?)
- assert_not_predicate(T_MONE, :positive?)
- assert_not_predicate(T_ZERO, :positive?)
-
- e = assert_raises(NoMethodError) do
- Complex(1).positive?
- end
-
- assert_match(/positive\?/, e.message)
- end
-
- def test_negative
- assert_predicate(-1, :negative?)
- assert_not_predicate(0, :negative?)
- assert_not_predicate(1, :negative?)
- assert_predicate(-1.0, :negative?)
- assert_not_predicate(-0.0, :negative?)
- assert_not_predicate(+0.0, :negative?)
- assert_not_predicate(+1.0, :negative?)
- assert_predicate(-(0.0.next_float), :negative?)
- assert_not_predicate(+(0.0.next_float), :negative?)
- assert_predicate(-Float::INFINITY, :negative?)
- assert_not_predicate(Float::INFINITY, :negative?)
- assert_not_predicate(Float::NAN, :negative?)
-
- a = Class.new(Numeric) do
- def <(x); true; end
- end.new
- assert_predicate(a, :negative?)
-
- a = Class.new(Numeric) do
- def <(x); false; end
- end.new
- assert_not_predicate(a, :negative?)
-
- assert_predicate(-1/2r, :negative?)
- assert_not_predicate(1/2r, :negative?)
-
- assert_not_predicate(T_ONE, :negative?)
- assert_predicate(T_MONE, :negative?)
- assert_not_predicate(T_ZERO, :negative?)
-
- e = assert_raises(NoMethodError) do
- Complex(1).negative?
+ def test_requiring_inquiry_is_deprecated
+ assert_deprecated do
+ require "active_support/core_ext/numeric/inquiry"
end
-
- assert_match(/negative\?/, e.message)
end
end
diff --git a/activesupport/test/core_ext/object/acts_like_test.rb b/activesupport/test/core_ext/object/acts_like_test.rb
index e68b1d23cb..31241caf0a 100644
--- a/activesupport/test/core_ext/object/acts_like_test.rb
+++ b/activesupport/test/core_ext/object/acts_like_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'active_support/core_ext/object'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/object"
class ObjectTests < ActiveSupport::TestCase
class DuckTime
@@ -15,19 +17,19 @@ class ObjectTests < ActiveSupport::TestCase
dt = DateTime.new
duck = DuckTime.new
- assert !object.acts_like?(:time)
- assert !object.acts_like?(:date)
+ assert_not object.acts_like?(:time)
+ assert_not object.acts_like?(:date)
assert time.acts_like?(:time)
- assert !time.acts_like?(:date)
+ assert_not time.acts_like?(:date)
- assert !date.acts_like?(:time)
+ assert_not date.acts_like?(:time)
assert date.acts_like?(:date)
assert dt.acts_like?(:time)
assert dt.acts_like?(:date)
assert duck.acts_like?(:time)
- assert !duck.acts_like?(:date)
+ assert_not duck.acts_like?(:date)
end
end
diff --git a/activesupport/test/core_ext/object/blank_test.rb b/activesupport/test/core_ext/object/blank_test.rb
index 8a5e385dd7..954f415383 100644
--- a/activesupport/test/core_ext/object/blank_test.rb
+++ b/activesupport/test/core_ext/object/blank_test.rb
@@ -1,6 +1,7 @@
+# frozen_string_literal: true
-require 'abstract_unit'
-require 'active_support/core_ext/object/blank'
+require "abstract_unit"
+require "active_support/core_ext/object/blank"
class BlankTest < ActiveSupport::TestCase
class EmptyTrue
@@ -15,8 +16,8 @@ class BlankTest < ActiveSupport::TestCase
end
end
- BLANK = [ EmptyTrue.new, nil, false, '', ' ', " \n\t \r ", ' ', "\u00a0", [], {} ]
- NOT = [ EmptyFalse.new, Object.new, true, 0, 1, 'a', [nil], { nil => 0 } ]
+ BLANK = [ EmptyTrue.new, nil, false, "", " ", " \n\t \r ", " ", "\u00a0", [], {}, " ".encode("UTF-16LE") ]
+ NOT = [ EmptyFalse.new, Object.new, true, 0, 1, "a", [nil], { nil => 0 }, Time.now, "my value".encode("UTF-16LE") ]
def test_blank
BLANK.each { |v| assert_equal true, v.blank?, "#{v.inspect} should be blank" }
@@ -29,7 +30,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 791b5e7172..1fb26ebac7 100644
--- a/activesupport/test/core_ext/object/deep_dup_test.rb
+++ b/activesupport/test/core_ext/object/deep_dup_test.rb
@@ -1,43 +1,44 @@
-require 'abstract_unit'
-require 'active_support/core_ext/object'
+# frozen_string_literal: true
-class DeepDupTest < ActiveSupport::TestCase
+require "abstract_unit"
+require "active_support/core_ext/object"
+class DeepDupTest < ActiveSupport::TestCase
def test_array_deep_dup
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
def test_hash_deep_dup
- hash = { :a => { :b => 'b' } }
+ hash = { a: { b: "b" } }
dup = hash.deep_dup
- dup[:a][:c] = 'c'
- assert_equal nil, hash[:a][:c]
- assert_equal 'c', dup[:a][:c]
+ dup[:a][:c] = "c"
+ assert_nil hash[:a][:c]
+ assert_equal "c", dup[:a][:c]
end
def test_array_deep_dup_with_hash_inside
- array = [1, { :a => 2, :b => 3 } ]
+ 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
def test_hash_deep_dup_with_array_inside
- hash = { :a => [1, 2] }
+ hash = { a: [1, 2] }
dup = hash.deep_dup
- dup[:a][2] = 'c'
- assert_equal nil, hash[:a][2]
- assert_equal 'c', dup[:a][2]
+ dup[:a][2] = "c"
+ assert_nil hash[:a][2]
+ assert_equal "c", dup[:a][2]
end
def test_deep_dup_initialize
zero_hash = Hash.new 0
- hash = { :a => zero_hash }
+ hash = { a: zero_hash }
dup = hash.deep_dup
assert_equal 0, dup[:a][44]
end
@@ -46,14 +47,13 @@ class DeepDupTest < ActiveSupport::TestCase
object = Object.new
dup = object.deep_dup
dup.instance_variable_set(:@a, 1)
- assert !object.instance_variable_defined?(:@a)
+ assert_not object.instance_variable_defined?(:@a)
assert dup.instance_variable_defined?(:@a)
end
def test_deep_dup_with_hash_class_key
- hash = { Fixnum => 1 }
+ hash = { Integer => 1 }
dup = hash.deep_dup
assert_equal 1, dup.keys.length
end
-
end
diff --git a/activesupport/test/core_ext/object/duplicable_test.rb b/activesupport/test/core_ext/object/duplicable_test.rb
index 042f5cfb34..5203434ae6 100644
--- a/activesupport/test/core_ext/object/duplicable_test.rb
+++ b/activesupport/test/core_ext/object/duplicable_test.rb
@@ -1,19 +1,25 @@
-require 'abstract_unit'
-require 'bigdecimal'
-require 'active_support/core_ext/object/duplicable'
-require 'active_support/core_ext/numeric/time'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "bigdecimal"
+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("4.56"), nil, false, true, 1, 2.3, Complex(1), Rational(1)]
+ else
+ RAISE_DUP = [method(:puts), Complex(1), Rational(1)]
+ ALLOW_DUP = ["1", "symbol_from_string".to_sym, Object.new, /foo/, [], {}, Time.now, Class.new, Module.new, BigDecimal("4.56"), nil, false, true, 1, 2.3]
+ end
def test_duplicable
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_not v.duplicable?, "#{ v.inspect } should not be duplicable"
assert_raises(TypeError, v.class.name) { v.dup }
end
diff --git a/activesupport/test/core_ext/object/inclusion_test.rb b/activesupport/test/core_ext/object/inclusion_test.rb
index 32d512eca3..8cbb4f848f 100644
--- a/activesupport/test/core_ext/object/inclusion_test.rb
+++ b/activesupport/test/core_ext/object/inclusion_test.rb
@@ -1,33 +1,35 @@
-require 'abstract_unit'
-require 'active_support/core_ext/object/inclusion'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/object/inclusion"
class InTest < ActiveSupport::TestCase
def test_in_array
- assert 1.in?([1,2])
- assert !3.in?([1,2])
+ assert 1.in?([1, 2])
+ assert_not 3.in?([1, 2])
end
def test_in_hash
h = { "a" => 100, "b" => 200 }
assert "a".in?(h)
- assert !"z".in?(h)
+ assert_not "z".in?(h)
end
def test_in_string
assert "lo".in?("hello")
- assert !"ol".in?("hello")
+ assert_not "ol".in?("hello")
assert ?h.in?("hello")
end
def test_in_range
assert 25.in?(1..50)
- assert !75.in?(1..50)
+ assert_not 75.in?(1..50)
end
def test_in_set
- s = Set.new([1,2])
+ s = Set.new([1, 2])
assert 1.in?(s)
- assert !3.in?(s)
+ assert_not 3.in?(s)
end
module A
@@ -43,14 +45,14 @@ class InTest < ActiveSupport::TestCase
def test_in_module
assert A.in?(B)
assert A.in?(C)
- assert !A.in?(A)
- assert !A.in?(D)
+ assert_not A.in?(A)
+ assert_not A.in?(D)
end
-
+
def test_no_method_catching
assert_raise(ArgumentError) { 1.in?(1) }
end
-
+
def test_presence_in
assert_equal "stuff", "stuff".presence_in(%w( lots of stuff ))
assert_nil "stuff".presence_in(%w( lots of crap ))
diff --git a/activesupport/test/core_ext/object/instance_variables_test.rb b/activesupport/test/core_ext/object/instance_variables_test.rb
index 9f4c5dc4f1..9052d209a3 100644
--- a/activesupport/test/core_ext/object/instance_variables_test.rb
+++ b/activesupport/test/core_ext/object/instance_variables_test.rb
@@ -1,11 +1,13 @@
-require 'abstract_unit'
-require 'active_support/core_ext/object'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/object"
class ObjectInstanceVariableTest < ActiveSupport::TestCase
def setup
@source, @dest = Object.new, Object.new
- @source.instance_variable_set(:@bar, 'bar')
- @source.instance_variable_set(:@baz, 'baz')
+ @source.instance_variable_set(:@bar, "bar")
+ @source.instance_variable_set(:@baz, "baz")
end
def test_instance_variable_names
@@ -13,19 +15,19 @@ class ObjectInstanceVariableTest < ActiveSupport::TestCase
end
def test_instance_values
- assert_equal({'bar' => 'bar', 'baz' => 'baz'}, @source.instance_values)
+ assert_equal({ "bar" => "bar", "baz" => "baz" }, @source.instance_values)
end
def test_instance_exec_passes_arguments_to_block
- assert_equal %w(hello goodbye), 'hello'.instance_exec('goodbye') { |v| [self, v] }
+ assert_equal %w(hello goodbye), (+"hello").instance_exec("goodbye") { |v| [self, v] }
end
def test_instance_exec_with_frozen_obj
- assert_equal %w(olleh goodbye), 'hello'.freeze.instance_exec('goodbye') { |v| [reverse, v] }
+ assert_equal %w(olleh goodbye), "hello".instance_exec("goodbye") { |v| [reverse, v] }
end
def test_instance_exec_nested
- assert_equal %w(goodbye olleh bar), 'hello'.instance_exec('goodbye') { |arg|
- [arg] + instance_exec('bar') { |v| [reverse, v] } }
+ assert_equal %w(goodbye olleh bar), (+"hello").instance_exec("goodbye") { |arg|
+ [arg] + instance_exec("bar") { |v| [reverse, v] } }
end
end
diff --git a/activesupport/test/core_ext/object/json_cherry_pick_test.rb b/activesupport/test/core_ext/object/json_cherry_pick_test.rb
index 2f7ea3a497..22659a4050 100644
--- a/activesupport/test/core_ext/object/json_cherry_pick_test.rb
+++ b/activesupport/test/core_ext/object/json_cherry_pick_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
# These test cases were added to test that cherry-picking the json extensions
# works correctly, primarily for dependencies problems reported in #16131. They
@@ -9,7 +11,7 @@ class JsonCherryPickTest < ActiveSupport::TestCase
include ActiveSupport::Testing::Isolation
def test_time_as_json
- require_or_skip 'active_support/core_ext/object/json'
+ require_or_skip "active_support/core_ext/object/json"
expected = Time.new(2004, 7, 25)
actual = Time.parse(expected.as_json)
@@ -18,7 +20,7 @@ class JsonCherryPickTest < ActiveSupport::TestCase
end
def test_date_as_json
- require_or_skip 'active_support/core_ext/object/json'
+ require_or_skip "active_support/core_ext/object/json"
expected = Date.new(2004, 7, 25)
actual = Date.parse(expected.as_json)
@@ -27,7 +29,7 @@ class JsonCherryPickTest < ActiveSupport::TestCase
end
def test_datetime_as_json
- require_or_skip 'active_support/core_ext/object/json'
+ require_or_skip "active_support/core_ext/object/json"
expected = DateTime.new(2004, 7, 25)
actual = DateTime.parse(expected.as_json)
diff --git a/activesupport/test/core_ext/object/json_gem_encoding_test.rb b/activesupport/test/core_ext/object/json_gem_encoding_test.rb
index 02ab17fb64..4cdb6ed09f 100644
--- a/activesupport/test/core_ext/object/json_gem_encoding_test.rb
+++ b/activesupport/test/core_ext/object/json_gem_encoding_test.rb
@@ -1,6 +1,8 @@
-require 'abstract_unit'
-require 'json'
-require 'json/encoding_test_cases'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "json"
+require "json/encoding_test_cases"
# These test cases were added to test that we do not interfere with json gem's
# output when the AS encoder is loaded, primarily for problems reported in
@@ -10,10 +12,10 @@ require 'json/encoding_test_cases'
# The AS::JSON encoder requires the BigDecimal core_ext, which, unfortunately,
# changes the BigDecimal#to_s output, and consequently the JSON gem output. So
-# we need to require this unfront to ensure we don't get a false failure, but
+# we need to require this upfront to ensure we don't get a false failure, but
# ideally we should just fix the BigDecimal core_ext to not change to_s without
# arguments.
-require 'active_support/core_ext/big_decimal'
+require "active_support/core_ext/big_decimal"
class JsonGemEncodingTest < ActiveSupport::TestCase
include ActiveSupport::Testing::Isolation
@@ -48,7 +50,7 @@ class JsonGemEncodingTest < ActiveSupport::TestCase
exception = e
end
- require_or_skip 'active_support/core_ext/object/json'
+ require_or_skip "active_support/core_ext/object/json"
if exception
assert_raises_with_message JSON::GeneratorError, e.message do
diff --git a/activesupport/test/core_ext/object/to_param_test.rb b/activesupport/test/core_ext/object/to_param_test.rb
index 30a7557dc2..612156bd99 100644
--- a/activesupport/test/core_ext/object/to_param_test.rb
+++ b/activesupport/test/core_ext/object/to_param_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'active_support/core_ext/object/to_param'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/object/to_param"
class ToParamTest < ActiveSupport::TestCase
class CustomString < String
@@ -10,8 +12,8 @@ class ToParamTest < ActiveSupport::TestCase
def test_object
foo = Object.new
- def foo.to_s; 'foo' end
- assert_equal 'foo', foo.to_param
+ def foo.to_s; "foo" end
+ assert_equal "foo", foo.to_param
end
def test_nil
@@ -25,13 +27,13 @@ class ToParamTest < ActiveSupport::TestCase
def test_array
# Empty Array
- assert_equal '', [].to_param
+ assert_equal "", [].to_param
array = [1, 2, 3, 4]
assert_equal "1/2/3/4", array.to_param
# Array of different objects
- array = [1, '3', { a: 1, b: 2 }, nil, true, false, CustomString.new('object')]
+ array = [1, "3", { a: 1, b: 2 }, nil, true, false, CustomString.new("object")]
assert_equal "1/3/a=1&b=2//true/false/custom-object", array.to_param
end
end
diff --git a/activesupport/test/core_ext/object/to_query_test.rb b/activesupport/test/core_ext/object/to_query_test.rb
index 09cab3ed35..561dadbbcf 100644
--- a/activesupport/test/core_ext/object/to_query_test.rb
+++ b/activesupport/test/core_ext/object/to_query_test.rb
@@ -1,82 +1,98 @@
-require 'abstract_unit'
-require 'active_support/ordered_hash'
-require 'active_support/core_ext/object/to_query'
-require 'active_support/core_ext/string/output_safety'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/ordered_hash"
+require "active_support/core_ext/object/to_query"
+require "active_support/core_ext/string/output_safety"
class ToQueryTest < ActiveSupport::TestCase
def test_simple_conversion
- assert_query_equal 'a=10', :a => 10
+ assert_query_equal "a=10", a: 10
end
def test_cgi_escaping
- assert_query_equal 'a%3Ab=c+d', 'a:b' => 'c d'
+ assert_query_equal "a%3Ab=c+d", "a:b" => "c d"
end
def test_html_safe_parameter_key
- assert_query_equal 'a%3Ab=c+d', 'a:b'.html_safe => 'c d'
+ assert_query_equal "a%3Ab=c+d", "a:b".html_safe => "c d"
end
def test_html_safe_parameter_value
- assert_query_equal 'a=%5B10%5D', 'a' => '[10]'.html_safe
+ assert_query_equal "a=%5B10%5D", "a" => "[10]".html_safe
end
def test_nil_parameter_value
empty = Object.new
def empty.to_param; nil end
- assert_query_equal 'a=', 'a' => empty
+ assert_query_equal "a=", "a" => empty
end
def test_nested_conversion
- assert_query_equal 'person%5Blogin%5D=seckar&person%5Bname%5D=Nicholas',
- :person => Hash[:login, 'seckar', :name, 'Nicholas']
+ assert_query_equal "person%5Blogin%5D=seckar&person%5Bname%5D=Nicholas",
+ person: Hash[:login, "seckar", :name, "Nicholas"]
end
def test_multiple_nested
- assert_query_equal 'account%5Bperson%5D%5Bid%5D=20&person%5Bid%5D=10',
- Hash[:account, {:person => {:id => 20}}, :person, {:id => 10}]
+ assert_query_equal "account%5Bperson%5D%5Bid%5D=20&person%5Bid%5D=10",
+ Hash[:account, { person: { id: 20 } }, :person, { id: 10 }]
end
def test_array_values
- assert_query_equal 'person%5Bid%5D%5B%5D=10&person%5Bid%5D%5B%5D=20',
- :person => {:id => [10, 20]}
+ assert_query_equal "person%5Bid%5D%5B%5D=10&person%5Bid%5D%5B%5D=20",
+ person: { id: [10, 20] }
end
def test_array_values_are_not_sorted
- assert_query_equal 'person%5Bid%5D%5B%5D=20&person%5Bid%5D%5B%5D=10',
- :person => {:id => [20, 10]}
+ assert_query_equal "person%5Bid%5D%5B%5D=20&person%5Bid%5D%5B%5D=10",
+ person: { id: [20, 10] }
end
def test_empty_array
- assert_equal "person%5B%5D=", [].to_query('person')
+ assert_equal "person%5B%5D=", [].to_query("person")
end
def test_nested_empty_hash
- assert_equal '',
+ assert_equal "",
{}.to_query
- assert_query_equal 'a=1&b%5Bc%5D=3',
- { a: 1, b: { c: 3, d: {} } }
- assert_query_equal '',
- { a: {b: {c: {}}} }
- assert_query_equal 'b%5Bc%5D=false&b%5Be%5D=&b%5Bf%5D=&p=12',
- { p: 12, b: { c: false, e: nil, f: '' } }
- assert_query_equal 'b%5Bc%5D=3&b%5Bf%5D=',
- { b: { c: 3, k: {}, f: '' } }
- assert_query_equal 'b=3',
- {a: [], b: 3}
+ assert_query_equal "a=1&b%5Bc%5D=3",
+ a: 1, b: { c: 3, d: {} }
+ assert_query_equal "",
+ a: { b: { c: {} } }
+ assert_query_equal "b%5Bc%5D=false&b%5Be%5D=&b%5Bf%5D=&p=12",
+ p: 12, b: { c: false, e: nil, f: "" }
+ assert_query_equal "b%5Bc%5D=3&b%5Bf%5D=",
+ b: { c: 3, k: {}, f: "" }
+ assert_query_equal "b=3",
+ a: [], b: 3
end
def test_hash_with_namespace
- hash = { name: 'Nakshay', nationality: 'Indian' }
- assert_equal "user%5Bname%5D=Nakshay&user%5Bnationality%5D=Indian", hash.to_query('user')
+ hash = { name: "Nakshay", nationality: "Indian" }
+ assert_equal "user%5Bname%5D=Nakshay&user%5Bnationality%5D=Indian", hash.to_query("user")
end
def test_hash_sorted_lexicographically
- hash = { type: 'human', name: 'Nakshay' }
+ hash = { type: "human", name: "Nakshay" }
assert_equal "name=Nakshay&type=human", hash.to_query
end
+ def test_hash_not_sorted_lexicographically_for_nested_structure
+ params = {
+ "foo" => {
+ "contents" => [
+ { "name" => "gorby", "id" => "123" },
+ { "name" => "puff", "d" => "true" }
+ ]
+ }
+ }
+ expected = "foo[contents][][name]=gorby&foo[contents][][id]=123&foo[contents][][name]=puff&foo[contents][][d]=true"
+
+ assert_equal expected, URI.decode_www_form_component(params.to_query)
+ end
+
private
def assert_query_equal(expected, actual)
- assert_equal expected.split('&'), actual.to_query.split('&')
+ assert_equal expected.split("&"), actual.to_query.split("&")
end
end
diff --git a/activesupport/test/core_ext/object/try_test.rb b/activesupport/test/core_ext/object/try_test.rb
index 5ea0f0eca6..a838334034 100644
--- a/activesupport/test/core_ext/object/try_test.rb
+++ b/activesupport/test/core_ext/object/try_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'active_support/core_ext/object'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/object"
class ObjectTryTest < ActiveSupport::TestCase
def setup
@@ -8,26 +10,26 @@ class ObjectTryTest < ActiveSupport::TestCase
def test_nonexisting_method
method = :undefined_method
- assert !@string.respond_to?(method)
+ assert_not_respond_to @string, method
assert_nil @string.try(method)
end
def test_nonexisting_method_with_arguments
method = :undefined_method
- assert !@string.respond_to?(method)
- assert_nil @string.try(method, 'llo', 'y')
+ assert_not_respond_to @string, method
+ assert_nil @string.try(method, "llo", "y")
end
def test_nonexisting_method_bang
method = :undefined_method
- assert !@string.respond_to?(method)
+ assert_not_respond_to @string, method
assert_raise(NoMethodError) { @string.try!(method) }
end
def test_nonexisting_method_with_arguments_bang
method = :undefined_method
- assert !@string.respond_to?(method)
- assert_raise(NoMethodError) { @string.try!(method, 'llo', 'y') }
+ assert_not_respond_to @string, method
+ assert_raise(NoMethodError) { @string.try!(method, "llo", "y") }
end
def test_valid_method
@@ -35,11 +37,11 @@ class ObjectTryTest < ActiveSupport::TestCase
end
def test_argument_forwarding
- assert_equal 'Hey', @string.try(:sub, 'llo', 'y')
+ assert_equal "Hey", @string.try(:sub, "llo", "y")
end
def test_block_forwarding
- assert_equal 'Hey', @string.try(:sub, 'llo') { |match| 'y' }
+ assert_equal "Hey", @string.try(:sub, "llo") { |match| "y" }
end
def test_nil_to_type
@@ -48,7 +50,7 @@ class ObjectTryTest < ActiveSupport::TestCase
end
def test_false_try
- assert_equal 'false', false.try(:to_s)
+ assert_equal "false", false.try(:to_s)
end
def test_try_only_block
@@ -76,9 +78,8 @@ class ObjectTryTest < ActiveSupport::TestCase
def test_try_with_private_method_bang
klass = Class.new do
private
-
def private_method
- 'private method'
+ "private method"
end
end
@@ -88,9 +89,8 @@ class ObjectTryTest < ActiveSupport::TestCase
def test_try_with_private_method
klass = Class.new do
private
-
def private_method
- 'private method'
+ "private method"
end
end
@@ -99,30 +99,29 @@ class ObjectTryTest < ActiveSupport::TestCase
class Decorator < SimpleDelegator
def delegator_method
- 'delegator method'
+ "delegator method"
end
def reverse
- 'overridden reverse'
+ "overridden reverse"
end
private
-
def private_delegator_method
- 'private delegator method'
+ "private delegator method"
end
end
def test_try_with_method_on_delegator
- assert_equal 'delegator method', Decorator.new(@string).try(:delegator_method)
+ assert_equal "delegator method", Decorator.new(@string).try(:delegator_method)
end
def test_try_with_method_on_delegator_target
- assert_equal 5, Decorator.new(@string).size
+ assert_equal 5, Decorator.new(@string).try(:size)
end
- def test_try_with_overriden_method_on_delegator
- assert_equal 'overridden reverse', Decorator.new(@string).reverse
+ def test_try_with_overridden_method_on_delegator
+ assert_equal "overridden reverse", Decorator.new(@string).try(:reverse)
end
def test_try_with_private_method_on_delegator
@@ -138,9 +137,8 @@ class ObjectTryTest < ActiveSupport::TestCase
def test_try_with_private_method_on_delegator_target
klass = Class.new do
private
-
def private_method
- 'private method'
+ "private method"
end
end
@@ -150,9 +148,8 @@ class ObjectTryTest < ActiveSupport::TestCase
def test_try_with_private_method_on_delegator_target_bang
klass = Class.new do
private
-
def private_method
- 'private method'
+ "private method"
end
end
diff --git a/activesupport/test/core_ext/range_ext_test.rb b/activesupport/test/core_ext/range_ext_test.rb
index f096328cee..4b8efb8a93 100644
--- a/activesupport/test/core_ext/range_ext_test.rb
+++ b/activesupport/test/core_ext/range_ext_test.rb
@@ -1,6 +1,9 @@
-require 'abstract_unit'
-require 'active_support/time'
-require 'active_support/core_ext/range'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/time"
+require "active_support/core_ext/numeric"
+require "active_support/core_ext/range"
class RangeTest < ActiveSupport::TestCase
def test_to_s_from_dates
@@ -13,6 +16,16 @@ class RangeTest < ActiveSupport::TestCase
assert_equal "BETWEEN '2005-12-10 15:30:00' AND '2005-12-10 17:30:00'", date_range.to_s(:db)
end
+ def test_to_s_with_alphabets
+ alphabet_range = ("a".."z")
+ assert_equal "BETWEEN 'a' AND 'z'", alphabet_range.to_s(:db)
+ end
+
+ def test_to_s_with_numeric
+ number_range = (1..100)
+ assert_equal "BETWEEN '1' AND '100'", number_range.to_s(:db)
+ end
+
def test_date_range
assert_instance_of Range, DateTime.new..DateTime.new
assert_instance_of Range, DateTime::Infinity.new..DateTime::Infinity.new
@@ -24,7 +37,7 @@ class RangeTest < ActiveSupport::TestCase
end
def test_overlaps_last_exclusive
- assert !(1...5).overlaps?(5..10)
+ assert_not (1...5).overlaps?(5..10)
end
def test_overlaps_first_inclusive
@@ -32,7 +45,7 @@ class RangeTest < ActiveSupport::TestCase
end
def test_overlaps_first_exclusive
- assert !(5..10).overlaps?(1...5)
+ assert_not (5..10).overlaps?(1...5)
end
def test_should_include_identical_inclusive
@@ -60,15 +73,15 @@ class RangeTest < ActiveSupport::TestCase
end
def test_exclusive_end_should_not_include_identical_with_inclusive_end
- assert !(1...10).include?(1..10)
+ assert_not_includes (1...10), 1..10
end
def test_should_not_include_overlapping_first
- assert !(2..8).include?(1..3)
+ assert_not_includes (2..8), 1..3
end
def test_should_not_include_overlapping_last
- assert !(2..8).include?(5..9)
+ assert_not_includes (2..8), 5..9
end
def test_should_include_identical_exclusive_with_floats
@@ -89,37 +102,40 @@ class RangeTest < ActiveSupport::TestCase
def test_no_overlaps_on_time
time_range_1 = Time.utc(2005, 12, 10, 15, 30)..Time.utc(2005, 12, 10, 17, 30)
time_range_2 = Time.utc(2005, 12, 10, 17, 31)..Time.utc(2005, 12, 10, 18, 00)
- assert !time_range_1.overlaps?(time_range_2)
+ assert_not time_range_1.overlaps?(time_range_2)
end
def test_each_on_time_with_zone
- twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone['Eastern Time (US & Canada)'] , Time.utc(2006,11,28,10,30))
+ twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Eastern Time (US & Canada)"], Time.utc(2006, 11, 28, 10, 30))
assert_raises TypeError do
- ((twz - 1.hour)..twz).each {}
+ ((twz - 1.hour)..twz).each { }
end
end
def test_step_on_time_with_zone
- twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone['Eastern Time (US & Canada)'] , Time.utc(2006,11,28,10,30))
+ twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Eastern Time (US & Canada)"], Time.utc(2006, 11, 28, 10, 30))
assert_raises TypeError do
- ((twz - 1.hour)..twz).step(1) {}
+ ((twz - 1.hour)..twz).step(1) { }
end
end
def test_include_on_time_with_zone
- twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone['Eastern Time (US & Canada)'] , Time.utc(2006,11,28,10,30))
- assert_raises TypeError do
- ((twz - 1.hour)..twz).include?(twz)
- end
+ twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Eastern Time (US & Canada)"], Time.utc(2006, 11, 28, 10, 30))
+ assert ((twz - 1.hour)..twz).include?(twz)
+ end
+
+ def test_case_equals_on_time_with_zone
+ twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Eastern Time (US & Canada)"], Time.utc(2006, 11, 28, 10, 30))
+ assert ((twz - 1.hour)..twz) === twz
end
def test_date_time_with_each
datetime = DateTime.now
- assert(((datetime - 1.hour)..datetime).each {})
+ assert(((datetime - 1.hour)..datetime).each { })
end
def test_date_time_with_step
datetime = DateTime.now
- assert(((datetime - 1.hour)..datetime).step(1) {})
+ assert(((datetime - 1.hour)..datetime).step(1) { })
end
end
diff --git a/activesupport/test/core_ext/regexp_ext_test.rb b/activesupport/test/core_ext/regexp_ext_test.rb
index c2398d31bd..7d18297b64 100644
--- a/activesupport/test/core_ext/regexp_ext_test.rb
+++ b/activesupport/test/core_ext/regexp_ext_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'active_support/core_ext/regexp'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/regexp"
class RegexpExtAccessTests < ActiveSupport::TestCase
def test_multiline
diff --git a/activesupport/test/core_ext/secure_random_test.rb b/activesupport/test/core_ext/secure_random_test.rb
index dfacb7fe9f..7067fb524c 100644
--- a/activesupport/test/core_ext/secure_random_test.rb
+++ b/activesupport/test/core_ext/secure_random_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'active_support/core_ext/securerandom'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/securerandom"
class SecureRandomTest < ActiveSupport::TestCase
def test_base58
diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb
index 3a5d6df06d..2468fe3603 100644
--- a/activesupport/test/core_ext/string_ext_test.rb
+++ b/activesupport/test/core_ext/string_ext_test.rb
@@ -1,15 +1,19 @@
-require 'date'
-require 'abstract_unit'
-require 'inflector_test_cases'
-require 'constantize_test_cases'
-
-require 'active_support/inflector'
-require 'active_support/core_ext/string'
-require 'active_support/time'
-require 'active_support/core_ext/string/strip'
-require 'active_support/core_ext/string/output_safety'
-require 'active_support/core_ext/string/indent'
-require 'time_zone_test_helpers'
+# frozen_string_literal: true
+
+require "date"
+require "abstract_unit"
+require "timeout"
+require "inflector_test_cases"
+require "constantize_test_cases"
+
+require "active_support/inflector"
+require "active_support/core_ext/string"
+require "active_support/time"
+require "active_support/core_ext/string/output_safety"
+require "active_support/core_ext/string/indent"
+require "active_support/core_ext/string/strip"
+require "time_zone_test_helpers"
+require "yaml"
class StringInflectionsTest < ActiveSupport::TestCase
include InflectorTestCases
@@ -17,12 +21,16 @@ class StringInflectionsTest < ActiveSupport::TestCase
include TimeZoneTestHelpers
def test_strip_heredoc_on_an_empty_string
- assert_equal '', ''.strip_heredoc
+ assert_equal "", "".strip_heredoc
+ end
+
+ def test_strip_heredoc_on_a_frozen_string
+ assert "".strip_heredoc.frozen?
end
def test_strip_heredoc_on_a_string_with_no_lines
- assert_equal 'x', 'x'.strip_heredoc
- assert_equal 'x', ' x'.strip_heredoc
+ assert_equal "x", "x".strip_heredoc
+ assert_equal "x", " x".strip_heredoc
end
def test_strip_heredoc_on_a_heredoc_with_no_margin
@@ -59,7 +67,7 @@ class StringInflectionsTest < ActiveSupport::TestCase
assert_equal("blargles", "blargle".pluralize(2))
end
- test 'pluralize with count = 1 still returns new string' do
+ test "pluralize with count = 1 still returns new string" do
name = "Kuldeep"
assert_not_same name.pluralize(1), name
end
@@ -76,6 +84,24 @@ class StringInflectionsTest < ActiveSupport::TestCase
end
end
+ def test_titleize_with_keep_id_suffix
+ MixtureToTitleCaseWithKeepIdSuffix.each do |before, titleized|
+ assert_equal(titleized, before.titleize(keep_id_suffix: true))
+ end
+ end
+
+ def test_upcase_first
+ assert_equal "What a Lovely Day", "what a Lovely Day".upcase_first
+ end
+
+ def test_upcase_first_with_one_char
+ assert_equal "W", "w".upcase_first
+ end
+
+ def test_upcase_first_with_empty_string
+ assert_equal "", "".upcase_first
+ end
+
def test_camelize
CamelToUnderscore.each do |camel, underscore|
assert_equal(camel, underscore.camelize)
@@ -83,7 +109,14 @@ class StringInflectionsTest < ActiveSupport::TestCase
end
def test_camelize_lower
- assert_equal('capital', 'Capital'.camelize(:lower))
+ assert_equal("capital", "Capital".camelize(:lower))
+ end
+
+ def test_camelize_invalid_option
+ e = assert_raise ArgumentError do
+ "Capital".camelize(nil)
+ end
+ assert_equal("Invalid option, use either :upper or :lower.", e.message)
end
def test_dasherize
@@ -139,19 +172,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(slugged, normal.parameterize(preserve_case: true))
end
end
def test_string_parameterized_no_separator
StringToParameterizeWithNoSeparator.each do |normal, slugged|
- assert_equal(normal.parameterize(''), slugged)
+ assert_equal(slugged, normal.parameterize(separator: ""))
+ end
+ end
+
+ def test_string_parameterized_no_separator_preserve_case
+ StringToParameterizePreserveCaseWithNoSeparator.each do |normal, 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('_'), slugged)
+ assert_equal(slugged, normal.parameterize(separator: "_"))
+ end
+ end
+
+ def test_string_parameterized_underscore_preserve_case
+ StringToParameterizePreserveCaseWithUnderscore.each do |normal, slugged|
+ assert_equal(slugged, normal.parameterize(separator: "_", preserve_case: true))
end
end
@@ -167,31 +218,37 @@ class StringInflectionsTest < ActiveSupport::TestCase
end
end
+ def test_humanize_with_keep_id_suffix
+ UnderscoreToHumanWithKeepIdSuffix.each do |underscore, human|
+ assert_equal(human, underscore.humanize(keep_id_suffix: true))
+ end
+ end
+
def test_humanize_with_html_escape
- assert_equal 'Hello', ERB::Util.html_escape("hello").humanize
+ assert_equal "Hello", ERB::Util.html_escape("hello").humanize
end
def test_ord
- assert_equal 97, 'a'.ord
- assert_equal 97, 'abc'.ord
+ assert_equal 97, "a".ord
+ assert_equal 97, "abc".ord
end
def test_starts_ends_with_alias
s = "hello"
- assert s.starts_with?('h')
- assert s.starts_with?('hel')
- assert !s.starts_with?('el')
+ assert s.starts_with?("h")
+ assert s.starts_with?("hel")
+ assert_not s.starts_with?("el")
- assert s.ends_with?('o')
- assert s.ends_with?('lo')
- assert !s.ends_with?('el')
+ assert s.ends_with?("o")
+ assert s.ends_with?("lo")
+ assert_not s.ends_with?("el")
end
def test_string_squish
- original = %{\u205f\u3000 A string surrounded by various unicode spaces,
+ 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:
@@ -206,8 +263,8 @@ class StringInflectionsTest < ActiveSupport::TestCase
end
def test_string_inquiry
- assert "production".inquiry.production?
- assert !"production".inquiry.development?
+ assert_predicate "production".inquiry, :production?
+ assert_not_predicate "production".inquiry, :development?
end
def test_truncate
@@ -216,16 +273,78 @@ class StringInflectionsTest < ActiveSupport::TestCase
end
def test_truncate_with_omission_and_separator
- assert_equal "Hello[...]", "Hello World!".truncate(10, :omission => "[...]")
- assert_equal "Hello[...]", "Hello Big World!".truncate(13, :omission => "[...]", :separator => ' ')
- assert_equal "Hello Big[...]", "Hello Big World!".truncate(14, :omission => "[...]", :separator => ' ')
- assert_equal "Hello Big[...]", "Hello Big World!".truncate(15, :omission => "[...]", :separator => ' ')
+ assert_equal "Hello[...]", "Hello World!".truncate(10, omission: "[...]")
+ assert_equal "Hello[...]", "Hello Big World!".truncate(13, omission: "[...]", separator: " ")
+ assert_equal "Hello Big[...]", "Hello Big World!".truncate(14, omission: "[...]", separator: " ")
+ assert_equal "Hello Big[...]", "Hello Big World!".truncate(15, omission: "[...]", separator: " ")
end
def test_truncate_with_omission_and_regexp_separator
- assert_equal "Hello[...]", "Hello Big World!".truncate(13, :omission => "[...]", :separator => /\s/)
- assert_equal "Hello Big[...]", "Hello Big World!".truncate(14, :omission => "[...]", :separator => /\s/)
- assert_equal "Hello Big[...]", "Hello Big World!".truncate(15, :omission => "[...]", :separator => /\s/)
+ assert_equal "Hello[...]", "Hello Big World!".truncate(13, omission: "[...]", separator: /\s/)
+ assert_equal "Hello Big[...]", "Hello Big World!".truncate(14, omission: "[...]", separator: /\s/)
+ assert_equal "Hello Big[...]", "Hello Big World!".truncate(15, omission: "[...]", separator: /\s/)
+ end
+
+ def test_truncate_bytes
+ assert_equal "👍👍👍👍", "👍👍👍👍".truncate_bytes(16)
+ assert_equal "👍👍👍👍", "👍👍👍👍".truncate_bytes(16, omission: nil)
+ assert_equal "👍👍👍👍", "👍👍👍👍".truncate_bytes(16, omission: " ")
+ assert_equal "👍👍👍👍", "👍👍👍👍".truncate_bytes(16, omission: "🖖")
+
+ assert_equal "👍👍👍…", "👍👍👍👍".truncate_bytes(15)
+ assert_equal "👍👍👍", "👍👍👍👍".truncate_bytes(15, omission: nil)
+ assert_equal "👍👍👍 ", "👍👍👍👍".truncate_bytes(15, omission: " ")
+ assert_equal "👍👍🖖", "👍👍👍👍".truncate_bytes(15, omission: "🖖")
+
+ assert_equal "…", "👍👍👍👍".truncate_bytes(5)
+ assert_equal "👍", "👍👍👍👍".truncate_bytes(5, omission: nil)
+ assert_equal "👍 ", "👍👍👍👍".truncate_bytes(5, omission: " ")
+ assert_equal "🖖", "👍👍👍👍".truncate_bytes(5, omission: "🖖")
+
+ assert_equal "…", "👍👍👍👍".truncate_bytes(4)
+ assert_equal "👍", "👍👍👍👍".truncate_bytes(4, omission: nil)
+ assert_equal " ", "👍👍👍👍".truncate_bytes(4, omission: " ")
+ assert_equal "🖖", "👍👍👍👍".truncate_bytes(4, omission: "🖖")
+
+ assert_raise ArgumentError do
+ "👍👍👍👍".truncate_bytes(3, omission: "🖖")
+ end
+ end
+
+ def test_truncate_bytes_preserves_codepoints
+ assert_equal "👍👍👍👍", "👍👍👍👍".truncate_bytes(16)
+ assert_equal "👍👍👍👍", "👍👍👍👍".truncate_bytes(16, omission: nil)
+ assert_equal "👍👍👍👍", "👍👍👍👍".truncate_bytes(16, omission: " ")
+ assert_equal "👍👍👍👍", "👍👍👍👍".truncate_bytes(16, omission: "🖖")
+
+ assert_equal "👍👍👍…", "👍👍👍👍".truncate_bytes(15)
+ assert_equal "👍👍👍", "👍👍👍👍".truncate_bytes(15, omission: nil)
+ assert_equal "👍👍👍 ", "👍👍👍👍".truncate_bytes(15, omission: " ")
+ assert_equal "👍👍🖖", "👍👍👍👍".truncate_bytes(15, omission: "🖖")
+
+ assert_equal "…", "👍👍👍👍".truncate_bytes(5)
+ assert_equal "👍", "👍👍👍👍".truncate_bytes(5, omission: nil)
+ assert_equal "👍 ", "👍👍👍👍".truncate_bytes(5, omission: " ")
+ assert_equal "🖖", "👍👍👍👍".truncate_bytes(5, omission: "🖖")
+
+ assert_equal "…", "👍👍👍👍".truncate_bytes(4)
+ assert_equal "👍", "👍👍👍👍".truncate_bytes(4, omission: nil)
+ assert_equal " ", "👍👍👍👍".truncate_bytes(4, omission: " ")
+ assert_equal "🖖", "👍👍👍👍".truncate_bytes(4, omission: "🖖")
+
+ assert_raise ArgumentError do
+ "👍👍👍👍".truncate_bytes(3, omission: "🖖")
+ end
+ end
+
+ def test_truncates_bytes_preserves_grapheme_clusters
+ assert_equal "a ", "a ❤️ b".truncate_bytes(2, omission: nil)
+ assert_equal "a ", "a ❤️ b".truncate_bytes(3, omission: nil)
+ assert_equal "a ", "a ❤️ b".truncate_bytes(7, omission: nil)
+ assert_equal "a ❤️", "a ❤️ b".truncate_bytes(8, omission: nil)
+
+ assert_equal "a ", "a 👩‍❤️‍👩".truncate_bytes(13, omission: nil)
+ assert_equal "", "👩‍❤️‍👩".truncate_bytes(13, omission: nil)
end
def test_truncate_words
@@ -234,37 +353,37 @@ class StringInflectionsTest < ActiveSupport::TestCase
end
def test_truncate_words_with_omission
- assert_equal "Hello Big World!", "Hello Big World!".truncate_words(3, :omission => "[...]")
- assert_equal "Hello Big[...]", "Hello Big World!".truncate_words(2, :omission => "[...]")
+ assert_equal "Hello Big World!", "Hello Big World!".truncate_words(3, omission: "[...]")
+ assert_equal "Hello Big[...]", "Hello Big World!".truncate_words(2, omission: "[...]")
end
def test_truncate_words_with_separator
- assert_equal "Hello<br>Big<br>World!...", "Hello<br>Big<br>World!<br>".truncate_words(3, :separator => '<br>')
- assert_equal "Hello<br>Big<br>World!", "Hello<br>Big<br>World!".truncate_words(3, :separator => '<br>')
- assert_equal "Hello\n<br>Big...", "Hello\n<br>Big<br>Wide<br>World!".truncate_words(2, :separator => '<br>')
+ assert_equal "Hello<br>Big<br>World!...", "Hello<br>Big<br>World!<br>".truncate_words(3, separator: "<br>")
+ assert_equal "Hello<br>Big<br>World!", "Hello<br>Big<br>World!".truncate_words(3, separator: "<br>")
+ assert_equal "Hello\n<br>Big...", "Hello\n<br>Big<br>Wide<br>World!".truncate_words(2, separator: "<br>")
end
def test_truncate_words_with_separator_and_omission
- assert_equal "Hello<br>Big<br>World![...]", "Hello<br>Big<br>World!<br>".truncate_words(3, :omission => "[...]", :separator => '<br>')
- assert_equal "Hello<br>Big<br>World!", "Hello<br>Big<br>World!".truncate_words(3, :omission => "[...]", :separator => '<br>')
+ assert_equal "Hello<br>Big<br>World![...]", "Hello<br>Big<br>World!<br>".truncate_words(3, omission: "[...]", separator: "<br>")
+ assert_equal "Hello<br>Big<br>World!", "Hello<br>Big<br>World!".truncate_words(3, omission: "[...]", separator: "<br>")
end
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
end
def test_truncate_multibyte
- assert_equal "\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 ...".force_encoding(Encoding::UTF_8),
- "\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 \354\225\204\353\235\274\353\246\254\354\230\244".force_encoding(Encoding::UTF_8).truncate(10)
+ assert_equal (+"\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 ...").force_encoding(Encoding::UTF_8),
+ (+"\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 \354\225\204\353\235\274\353\246\254\354\230\244").force_encoding(Encoding::UTF_8).truncate(10)
end
def test_truncate_should_not_be_html_safe
- assert !"Hello World!".truncate(12).html_safe?
+ assert_not_predicate "Hello World!".truncate(12), :html_safe?
end
def test_remove
@@ -281,7 +400,7 @@ class StringInflectionsTest < ActiveSupport::TestCase
end
def test_remove!
- original = "This is a very good day to die"
+ original = +"This is a very good day to die"
assert_equal "This is a good day to die", original.remove!(" very")
assert_equal "This is a good day to die", original
assert_equal "This is a good day", original.remove!(" to ", /die/)
@@ -298,7 +417,7 @@ class StringInflectionsTest < ActiveSupport::TestCase
end
class StringAccessTest < ActiveSupport::TestCase
- test "#at with Fixnum, returns a substring of one character at that position" do
+ test "#at with Integer, returns a substring of one character at that position" do
assert_equal "h", "hello".at(0)
end
@@ -308,22 +427,22 @@ 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 Fixnum, returns substring from the given position to the end" do
+ test "#from with positive Integer, returns substring from the given position to the end" do
assert_equal "llo", "hello".from(2)
end
- test "#from with negative Fixnum, position is counted from the end" do
+ test "#from with negative Integer, position is counted from the end" do
assert_equal "lo", "hello".from(-2)
end
- test "#to with positive Fixnum, substring from the beginning to the given position" do
+ test "#to with positive Integer, substring from the beginning to the given position" do
assert_equal "hel", "hello".to(2)
end
- test "#to with negative Fixnum, position is counted from the end" do
+ test "#to with negative Integer, position is counted from the end" do
assert_equal "hell", "hello".to(-2)
end
@@ -334,40 +453,58 @@ class StringAccessTest < ActiveSupport::TestCase
test "#first returns the first character" do
assert_equal "h", "hello".first
- assert_equal 'x', 'x'.first
+ assert_equal "x", "x".first
end
- test "#first with Fixnum, returns a substring from the beginning to position" do
+ test "#first with Integer, returns a substring from the beginning to position" do
assert_equal "he", "hello".first(2)
assert_equal "", "hello".first(0)
assert_equal "hello", "hello".first(10)
- assert_equal 'x', 'x'.first(4)
+ assert_equal "x", "x".first(4)
end
- test "#first with Fixnum >= string length still returns a new string" do
+ test "#first with Integer >= string length still returns a new string" do
string = "hello"
different_string = string.first(5)
assert_not_same different_string, string
end
+ test "#first with negative Integer is deprecated" do
+ string = "hello"
+ message = "Calling String#first with a negative integer limit " \
+ "will raise an ArgumentError in Rails 6.1."
+ assert_deprecated(message) do
+ string.first(-1)
+ end
+ end
+
test "#last returns the last character" do
assert_equal "o", "hello".last
- assert_equal 'x', 'x'.last
+ assert_equal "x", "x".last
end
- test "#last with Fixnum, returns a substring from the end to position" do
+ test "#last with Integer, returns a substring from the end to position" do
assert_equal "llo", "hello".last(3)
assert_equal "hello", "hello".last(10)
assert_equal "", "hello".last(0)
- assert_equal 'x', 'x'.last(4)
+ assert_equal "x", "x".last(4)
end
- test "#last with Fixnum >= string length still returns a new string" do
+ test "#last with Integer >= string length still returns a new string" do
string = "hello"
different_string = string.last(5)
assert_not_same different_string, string
end
+ test "#last with negative Integer is deprecated" do
+ string = "hello"
+ message = "Calling String#last with a negative integer limit " \
+ "will raise an ArgumentError in Rails 6.1."
+ assert_deprecated(message) do
+ string.last(-1)
+ end
+ end
+
test "access returns a real string" do
hash = {}
hash["h"] = true
@@ -410,16 +547,24 @@ class StringConversionsTest < ActiveSupport::TestCase
assert_equal Time.local(2011, 2, 27, 17, 50), "2011-02-27 13:50 -0100".to_time
assert_equal Time.utc(2011, 2, 27, 23, 50), "2011-02-27 22:50 -0100".to_time(:utc)
assert_equal Time.local(2005, 2, 27, 22, 50), "2005-02-27 14:50 -0500".to_time
+ assert_nil "010".to_time
assert_nil "".to_time
end
end
def test_string_to_time_utc_offset
with_env_tz "US/Eastern" do
- assert_equal 0, "2005-02-27 23:50".to_time(:utc).utc_offset
- assert_equal(-18000, "2005-02-27 23:50".to_time.utc_offset)
- assert_equal 0, "2005-02-27 22:50 -0100".to_time(:utc).utc_offset
- assert_equal(-18000, "2005-02-27 22:50 -0100".to_time.utc_offset)
+ if ActiveSupport.to_time_preserves_timezone
+ assert_equal 0, "2005-02-27 23:50".to_time(:utc).utc_offset
+ assert_equal(-18000, "2005-02-27 23:50".to_time.utc_offset)
+ assert_equal 0, "2005-02-27 22:50 -0100".to_time(:utc).utc_offset
+ assert_equal(-3600, "2005-02-27 22:50 -0100".to_time.utc_offset)
+ else
+ assert_equal 0, "2005-02-27 23:50".to_time(:utc).utc_offset
+ assert_equal(-18000, "2005-02-27 23:50".to_time.utc_offset)
+ assert_equal 0, "2005-02-27 22:50 -0100".to_time(:utc).utc_offset
+ assert_equal(-18000, "2005-02-27 22:50 -0100".to_time.utc_offset)
+ end
end
end
@@ -578,14 +723,14 @@ end
class StringBehaviourTest < ActiveSupport::TestCase
def test_acts_like_string
- assert 'Bambi'.acts_like_string?
+ assert_predicate "Bambi", :acts_like_string?
end
end
class CoreExtStringMultibyteTest < ActiveSupport::TestCase
- UTF8_STRING = 'こにちわ'
- ASCII_STRING = 'ohayo'.encode('US-ASCII')
- EUC_JP_STRING = 'さよなら'.encode('EUC-JP')
+ UTF8_STRING = "こにちわ"
+ ASCII_STRING = "ohayo".encode("US-ASCII")
+ EUC_JP_STRING = "さよなら".encode("EUC-JP")
INVALID_UTF8_STRING = "\270\236\010\210\245"
def test_core_ext_adds_mb_chars
@@ -593,10 +738,10 @@ class CoreExtStringMultibyteTest < ActiveSupport::TestCase
end
def test_string_should_recognize_utf8_strings
- assert UTF8_STRING.is_utf8?
- assert ASCII_STRING.is_utf8?
- assert !EUC_JP_STRING.is_utf8?
- assert !INVALID_UTF8_STRING.is_utf8?
+ assert_predicate UTF8_STRING, :is_utf8?
+ assert_predicate ASCII_STRING, :is_utf8?
+ assert_not_predicate EUC_JP_STRING, :is_utf8?
+ assert_not_predicate INVALID_UTF8_STRING, :is_utf8?
end
def test_mb_chars_returns_instance_of_proxy_class
@@ -606,7 +751,7 @@ end
class OutputSafetyTest < ActiveSupport::TestCase
def setup
- @string = "hello"
+ @string = +"hello"
@object = Class.new(Object) do
def to_s
"other"
@@ -615,28 +760,28 @@ class OutputSafetyTest < ActiveSupport::TestCase
end
test "A string is unsafe by default" do
- assert !@string.html_safe?
+ assert_not_predicate @string, :html_safe?
end
test "A string can be marked safe" do
string = @string.html_safe
- assert string.html_safe?
+ assert_predicate string, :html_safe?
end
test "Marking a string safe returns the string" do
assert_equal @string, @string.html_safe
end
- test "A fixnum is safe by default" do
- assert 5.html_safe?
+ test "An integer is safe by default" do
+ assert_predicate 5, :html_safe?
end
test "a float is safe by default" do
- assert 5.7.html_safe?
+ assert_predicate 5.7, :html_safe?
end
test "An object is unsafe by default" do
- assert !@object.html_safe?
+ assert_not_predicate @object, :html_safe?
end
test "Adding an object to a safe string returns a safe string" do
@@ -644,7 +789,7 @@ class OutputSafetyTest < ActiveSupport::TestCase
string << @object
assert_equal "helloother", string
- assert string.html_safe?
+ assert_predicate string, :html_safe?
end
test "Adding a safe string to another safe string returns a safe string" do
@@ -653,7 +798,7 @@ class OutputSafetyTest < ActiveSupport::TestCase
@combination = @other_string + string
assert_equal "otherhello", @combination
- assert @combination.html_safe?
+ assert_predicate @combination, :html_safe?
end
test "Adding an unsafe string to a safe string escapes it and returns a safe string" do
@@ -664,36 +809,36 @@ class OutputSafetyTest < ActiveSupport::TestCase
assert_equal "other&lt;foo&gt;", @combination
assert_equal "hello<foo>", @other_combination
- assert @combination.html_safe?
- assert !@other_combination.html_safe?
+ assert_predicate @combination, :html_safe?
+ assert_not_predicate @other_combination, :html_safe?
end
test "Prepending safe onto unsafe yields unsafe" do
@string.prepend "other".html_safe
- assert !@string.html_safe?
- assert_equal @string, "otherhello"
+ assert_not_predicate @string, :html_safe?
+ assert_equal "otherhello", @string
end
test "Prepending unsafe onto safe yields escaped safe" do
other = "other".html_safe
other.prepend "<foo>"
- assert other.html_safe?
- assert_equal other, "&lt;foo&gt;other"
+ assert_predicate other, :html_safe?
+ assert_equal "&lt;foo&gt;other", other
end
test "Concatting safe onto unsafe yields unsafe" do
- @other_string = "other"
+ @other_string = +"other"
string = @string.html_safe
@other_string.concat(string)
- assert !@other_string.html_safe?
+ assert_not_predicate @other_string, :html_safe?
end
test "Concatting unsafe onto safe yields escaped safe" do
@other_string = "other".html_safe
string = @other_string.concat("<foo>")
assert_equal "other&lt;foo&gt;", string
- assert string.html_safe?
+ assert_predicate string, :html_safe?
end
test "Concatting safe onto safe yields safe" do
@@ -701,22 +846,22 @@ class OutputSafetyTest < ActiveSupport::TestCase
string = @string.html_safe
@other_string.concat(string)
- assert @other_string.html_safe?
+ assert_predicate @other_string, :html_safe?
end
test "Concatting safe onto unsafe with << yields unsafe" do
- @other_string = "other"
+ @other_string = +"other"
string = @string.html_safe
@other_string << string
- assert !@other_string.html_safe?
+ assert_not_predicate @other_string, :html_safe?
end
test "Concatting unsafe onto safe with << yields escaped safe" do
@other_string = "other".html_safe
string = @other_string << "<foo>"
assert_equal "other&lt;foo&gt;", string
- assert string.html_safe?
+ assert_predicate string, :html_safe?
end
test "Concatting safe onto safe with << yields safe" do
@@ -724,7 +869,7 @@ class OutputSafetyTest < ActiveSupport::TestCase
string = @string.html_safe
@other_string << string
- assert @other_string.html_safe?
+ assert_predicate @other_string, :html_safe?
end
test "Concatting safe onto unsafe with % yields unsafe" do
@@ -732,7 +877,7 @@ class OutputSafetyTest < ActiveSupport::TestCase
string = @string.html_safe
@other_string = @other_string % string
- assert !@other_string.html_safe?
+ assert_not_predicate @other_string, :html_safe?
end
test "Concatting unsafe onto safe with % yields escaped safe" do
@@ -740,7 +885,7 @@ class OutputSafetyTest < ActiveSupport::TestCase
string = @other_string % "<foo>"
assert_equal "other&lt;foo&gt;", string
- assert string.html_safe?
+ assert_predicate string, :html_safe?
end
test "Concatting safe onto safe with % yields safe" do
@@ -748,7 +893,7 @@ class OutputSafetyTest < ActiveSupport::TestCase
string = @string.html_safe
@other_string = @other_string % string
- assert @other_string.html_safe?
+ assert_predicate @other_string, :html_safe?
end
test "Concatting with % doesn't modify a string" do
@@ -758,32 +903,80 @@ class OutputSafetyTest < ActiveSupport::TestCase
assert_equal ["<p>", "<b>", "<h1>"], @other_string
end
- test "Concatting a fixnum to safe always yields safe" do
+ test "Concatting an integer to safe always yields safe" do
string = @string.html_safe
string = string.concat(13)
- assert_equal "hello".concat(13), string
- assert string.html_safe?
+ assert_equal (+"hello").concat(13), string
+ assert_predicate string, :html_safe?
end
- test 'emits normal string yaml' do
- assert_equal 'foo'.to_yaml, 'foo'.html_safe.to_yaml(:foo => 1)
+ test "Inserting safe into safe yields safe" do
+ string = "foo".html_safe
+ string.insert(0, "<b>".html_safe)
+
+ assert_equal "<b>foo", string
+ assert_predicate string, :html_safe?
+ end
+
+ test "Inserting unsafe into safe yields escaped safe" do
+ string = "foo".html_safe
+ string.insert(0, "<b>")
+
+ assert_equal "&lt;b&gt;foo", string
+ assert_predicate string, :html_safe?
+ end
+
+ test "Replacing safe with safe yields safe" do
+ string = "foo".html_safe
+ string.replace("<b>".html_safe)
+
+ assert_equal "<b>", string
+ assert_predicate string, :html_safe?
+ end
+
+ test "Replacing safe with unsafe yields escaped safe" do
+ string = "foo".html_safe
+ string.replace("<b>")
+
+ assert_equal "&lt;b&gt;", string
+ assert_predicate string, :html_safe?
+ end
+
+ test "Replacing index of safe with safe yields safe" do
+ string = "foo".html_safe
+ string[0] = "<b>".html_safe
+
+ assert_equal "<b>oo", string
+ assert_predicate string, :html_safe?
+ end
+
+ test "Replacing index of safe with unsafe yields escaped safe" do
+ string = "foo".html_safe
+ string[0] = "<b>"
+
+ assert_equal "&lt;b&gt;oo", string
+ assert_predicate string, :html_safe?
+ end
+
+ test "emits normal string yaml" do
+ assert_equal "foo".to_yaml, "foo".html_safe.to_yaml(foo: 1)
end
test "call to_param returns a normal string" do
string = @string.html_safe
- assert string.html_safe?
- assert !string.to_param.html_safe?
+ assert_predicate string, :html_safe?
+ assert_not_predicate string.to_param, :html_safe?
end
test "ERB::Util.html_escape should escape unsafe characters" do
string = '<>&"\''
- expected = '&lt;&gt;&amp;&quot;&#39;'
+ expected = "&lt;&gt;&amp;&quot;&#39;"
assert_equal expected, ERB::Util.html_escape(string)
end
test "ERB::Util.html_escape should correctly handle invalid UTF-8 strings" do
- string = [192, 60].pack('CC')
- expected = 192.chr + "&lt;"
+ string = "\251 <"
+ expected = "© &lt;"
assert_equal expected, ERB::Util.html_escape(string)
end
@@ -793,24 +986,31 @@ class OutputSafetyTest < ActiveSupport::TestCase
end
test "ERB::Util.html_escape_once only escapes once" do
- string = '1 < 2 &amp; 3'
+ string = "1 < 2 &amp; 3"
escaped_string = "1 &lt; 2 &amp; 3"
assert_equal escaped_string, ERB::Util.html_escape_once(string)
assert_equal escaped_string, ERB::Util.html_escape_once(escaped_string)
end
+
+ test "ERB::Util.html_escape_once should correctly handle invalid UTF-8 strings" do
+ string = "\251 <"
+ expected = "© &lt;"
+ assert_equal expected, ERB::Util.html_escape_once(string)
+ end
end
class StringExcludeTest < ActiveSupport::TestCase
- test 'inverse of #include' do
- assert_equal false, 'foo'.exclude?('o')
- assert_equal true, 'foo'.exclude?('p')
+ test "inverse of #include" do
+ assert_equal false, "foo".exclude?("o")
+ assert_equal true, "foo".exclude?("p")
end
end
class StringIndentTest < ActiveSupport::TestCase
- test 'does not indent strings that only contain newlines (edge cases)' do
- ['', "\n", "\n" * 7].each do |str|
+ test "does not indent strings that only contain newlines (edge cases)" do
+ ["", "\n", "\n" * 7].each do |string|
+ str = string.dup
assert_nil str.indent!(8)
assert_equal str, str.indent(8)
assert_equal str, str.indent(1, "\t")
@@ -832,8 +1032,8 @@ class StringIndentTest < ActiveSupport::TestCase
# Nothing is said about existing indentation that mixes spaces and tabs, so
# there is nothing to test.
- test 'uses the indent char if passed' do
- assert_equal <<EXPECTED, <<ACTUAL.indent(4, '.')
+ test "uses the indent char if passed" do
+ assert_equal <<EXPECTED, <<ACTUAL.indent(4, ".")
.... def some_method(x, y)
.... some_code
.... end
@@ -843,7 +1043,7 @@ EXPECTED
end
ACTUAL
- assert_equal <<EXPECTED, <<ACTUAL.indent(2, '&nbsp;')
+ assert_equal <<EXPECTED, <<ACTUAL.indent(2, "&nbsp;")
&nbsp;&nbsp;&nbsp;&nbsp;def some_method(x, y)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;some_code
&nbsp;&nbsp;&nbsp;&nbsp;end
@@ -858,7 +1058,7 @@ ACTUAL
assert_equal " foo\n\n bar", "foo\n\nbar".indent(1)
end
- test 'indents blank lines if told so' do
+ test "indents blank lines if told so" do
assert_equal " foo\n \n bar", "foo\n\nbar".indent(1, nil, true)
end
end
diff --git a/activesupport/test/core_ext/time_ext_test.rb b/activesupport/test/core_ext/time_ext_test.rb
index b14c04fba6..7078f3506d 100644
--- a/activesupport/test/core_ext/time_ext_test.rb
+++ b/activesupport/test/core_ext/time_ext_test.rb
@@ -1,527 +1,546 @@
-require 'abstract_unit'
-require 'active_support/time'
-require 'core_ext/date_and_time_behavior'
-require 'time_zone_test_helpers'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/time"
+require "core_ext/date_and_time_behavior"
+require "time_zone_test_helpers"
class TimeExtCalculationsTest < ActiveSupport::TestCase
- def date_time_init(year,month,day,hour,minute,second,usec=0)
- Time.local(year,month,day,hour,minute,second,usec)
+ def date_time_init(year, month, day, hour, minute, second, usec = 0)
+ Time.local(year, month, day, hour, minute, second, usec)
end
include DateAndTimeBehavior
include TimeZoneTestHelpers
def test_seconds_since_midnight
- assert_equal 1,Time.local(2005,1,1,0,0,1).seconds_since_midnight
- assert_equal 60,Time.local(2005,1,1,0,1,0).seconds_since_midnight
- assert_equal 3660,Time.local(2005,1,1,1,1,0).seconds_since_midnight
- assert_equal 86399,Time.local(2005,1,1,23,59,59).seconds_since_midnight
- assert_equal 60.00001,Time.local(2005,1,1,0,1,0,10).seconds_since_midnight
+ assert_equal 1, Time.local(2005, 1, 1, 0, 0, 1).seconds_since_midnight
+ assert_equal 60, Time.local(2005, 1, 1, 0, 1, 0).seconds_since_midnight
+ assert_equal 3660, Time.local(2005, 1, 1, 1, 1, 0).seconds_since_midnight
+ assert_equal 86399, Time.local(2005, 1, 1, 23, 59, 59).seconds_since_midnight
+ assert_equal 60.00001, Time.local(2005, 1, 1, 0, 1, 0, 10).seconds_since_midnight
end
def test_seconds_since_midnight_at_daylight_savings_time_start
- with_env_tz 'US/Eastern' do
+ with_env_tz "US/Eastern" do
# dt: US: 2005 April 3rd 2:00am ST => April 3rd 3:00am DT
- assert_equal 2*3600-1, Time.local(2005,4,3,1,59,59).seconds_since_midnight, 'just before DST start'
- assert_equal 2*3600+1, Time.local(2005,4,3,3, 0, 1).seconds_since_midnight, 'just after DST start'
+ assert_equal 2 * 3600 - 1, Time.local(2005, 4, 3, 1, 59, 59).seconds_since_midnight, "just before DST start"
+ assert_equal 2 * 3600 + 1, Time.local(2005, 4, 3, 3, 0, 1).seconds_since_midnight, "just after DST start"
end
- with_env_tz 'NZ' do
+ with_env_tz "NZ" do
# dt: New Zealand: 2006 October 1st 2:00am ST => October 1st 3:00am DT
- assert_equal 2*3600-1, Time.local(2006,10,1,1,59,59).seconds_since_midnight, 'just before DST start'
- assert_equal 2*3600+1, Time.local(2006,10,1,3, 0, 1).seconds_since_midnight, 'just after DST start'
+ assert_equal 2 * 3600 - 1, Time.local(2006, 10, 1, 1, 59, 59).seconds_since_midnight, "just before DST start"
+ assert_equal 2 * 3600 + 1, Time.local(2006, 10, 1, 3, 0, 1).seconds_since_midnight, "just after DST start"
end
end
def test_seconds_since_midnight_at_daylight_savings_time_end
- with_env_tz 'US/Eastern' do
+ with_env_tz "US/Eastern" do
# st: US: 2005 October 30th 2:00am DT => October 30th 1:00am ST
# avoid setting a time between 1:00 and 2:00 since that requires specifying whether DST is active
- assert_equal 1*3600-1, Time.local(2005,10,30,0,59,59).seconds_since_midnight, 'just before DST end'
- assert_equal 3*3600+1, Time.local(2005,10,30,2, 0, 1).seconds_since_midnight, 'just after DST end'
+ assert_equal 1 * 3600 - 1, Time.local(2005, 10, 30, 0, 59, 59).seconds_since_midnight, "just before DST end"
+ assert_equal 3 * 3600 + 1, Time.local(2005, 10, 30, 2, 0, 1).seconds_since_midnight, "just after DST end"
# now set a time between 1:00 and 2:00 by specifying whether DST is active
# uses: Time.local( sec, min, hour, day, month, year, wday, yday, isdst, tz )
- assert_equal 1*3600+30*60, Time.local(0,30,1,30,10,2005,0,0,true,ENV['TZ']).seconds_since_midnight, 'before DST end'
- assert_equal 2*3600+30*60, Time.local(0,30,1,30,10,2005,0,0,false,ENV['TZ']).seconds_since_midnight, 'after DST end'
+ assert_equal 1 * 3600 + 30 * 60, Time.local(0, 30, 1, 30, 10, 2005, 0, 0, true, ENV["TZ"]).seconds_since_midnight, "before DST end"
+ assert_equal 2 * 3600 + 30 * 60, Time.local(0, 30, 1, 30, 10, 2005, 0, 0, false, ENV["TZ"]).seconds_since_midnight, "after DST end"
end
- with_env_tz 'NZ' do
+ with_env_tz "NZ" do
# st: New Zealand: 2006 March 19th 3:00am DT => March 19th 2:00am ST
# avoid setting a time between 2:00 and 3:00 since that requires specifying whether DST is active
- assert_equal 2*3600-1, Time.local(2006,3,19,1,59,59).seconds_since_midnight, 'just before DST end'
- assert_equal 4*3600+1, Time.local(2006,3,19,3, 0, 1).seconds_since_midnight, 'just after DST end'
+ assert_equal 2 * 3600 - 1, Time.local(2006, 3, 19, 1, 59, 59).seconds_since_midnight, "just before DST end"
+ assert_equal 4 * 3600 + 1, Time.local(2006, 3, 19, 3, 0, 1).seconds_since_midnight, "just after DST end"
# now set a time between 2:00 and 3:00 by specifying whether DST is active
# uses: Time.local( sec, min, hour, day, month, year, wday, yday, isdst, tz )
- assert_equal 2*3600+30*60, Time.local(0,30,2,19,3,2006,0,0,true, ENV['TZ']).seconds_since_midnight, 'before DST end'
- assert_equal 3*3600+30*60, Time.local(0,30,2,19,3,2006,0,0,false,ENV['TZ']).seconds_since_midnight, 'after DST end'
+ assert_equal 2 * 3600 + 30 * 60, Time.local(0, 30, 2, 19, 3, 2006, 0, 0, true, ENV["TZ"]).seconds_since_midnight, "before DST end"
+ assert_equal 3 * 3600 + 30 * 60, Time.local(0, 30, 2, 19, 3, 2006, 0, 0, false, ENV["TZ"]).seconds_since_midnight, "after DST end"
end
end
def test_seconds_until_end_of_day
- assert_equal 0, Time.local(2005,1,1,23,59,59).seconds_until_end_of_day
- assert_equal 1, Time.local(2005,1,1,23,59,58).seconds_until_end_of_day
- assert_equal 60, Time.local(2005,1,1,23,58,59).seconds_until_end_of_day
- assert_equal 3660, Time.local(2005,1,1,22,58,59).seconds_until_end_of_day
- assert_equal 86399, Time.local(2005,1,1,0,0,0).seconds_until_end_of_day
+ assert_equal 0, Time.local(2005, 1, 1, 23, 59, 59).seconds_until_end_of_day
+ assert_equal 1, Time.local(2005, 1, 1, 23, 59, 58).seconds_until_end_of_day
+ assert_equal 60, Time.local(2005, 1, 1, 23, 58, 59).seconds_until_end_of_day
+ assert_equal 3660, Time.local(2005, 1, 1, 22, 58, 59).seconds_until_end_of_day
+ assert_equal 86399, Time.local(2005, 1, 1, 0, 0, 0).seconds_until_end_of_day
end
def test_seconds_until_end_of_day_at_daylight_savings_time_start
- with_env_tz 'US/Eastern' do
+ with_env_tz "US/Eastern" do
# dt: US: 2005 April 3rd 2:00am ST => April 3rd 3:00am DT
- assert_equal 21*3600, Time.local(2005,4,3,1,59,59).seconds_until_end_of_day, 'just before DST start'
- assert_equal 21*3600-2, Time.local(2005,4,3,3,0,1).seconds_until_end_of_day, 'just after DST start'
+ assert_equal 21 * 3600, Time.local(2005, 4, 3, 1, 59, 59).seconds_until_end_of_day, "just before DST start"
+ assert_equal 21 * 3600 - 2, Time.local(2005, 4, 3, 3, 0, 1).seconds_until_end_of_day, "just after DST start"
end
- with_env_tz 'NZ' do
+ with_env_tz "NZ" do
# dt: New Zealand: 2006 October 1st 2:00am ST => October 1st 3:00am DT
- assert_equal 21*3600, Time.local(2006,10,1,1,59,59).seconds_until_end_of_day, 'just before DST start'
- assert_equal 21*3600-2, Time.local(2006,10,1,3,0,1).seconds_until_end_of_day, 'just after DST start'
+ assert_equal 21 * 3600, Time.local(2006, 10, 1, 1, 59, 59).seconds_until_end_of_day, "just before DST start"
+ assert_equal 21 * 3600 - 2, Time.local(2006, 10, 1, 3, 0, 1).seconds_until_end_of_day, "just after DST start"
end
end
def test_seconds_until_end_of_day_at_daylight_savings_time_end
- with_env_tz 'US/Eastern' do
+ with_env_tz "US/Eastern" do
# st: US: 2005 October 30th 2:00am DT => October 30th 1:00am ST
# avoid setting a time between 1:00 and 2:00 since that requires specifying whether DST is active
- assert_equal 24*3600, Time.local(2005,10,30,0,59,59).seconds_until_end_of_day, 'just before DST end'
- assert_equal 22*3600-2, Time.local(2005,10,30,2,0,1).seconds_until_end_of_day, 'just after DST end'
+ assert_equal 24 * 3600, Time.local(2005, 10, 30, 0, 59, 59).seconds_until_end_of_day, "just before DST end"
+ assert_equal 22 * 3600 - 2, Time.local(2005, 10, 30, 2, 0, 1).seconds_until_end_of_day, "just after DST end"
# now set a time between 1:00 and 2:00 by specifying whether DST is active
# uses: Time.local( sec, min, hour, day, month, year, wday, yday, isdst, tz )
- assert_equal 24*3600-30*60-1, Time.local(0,30,1,30,10,2005,0,0,true,ENV['TZ']).seconds_until_end_of_day, 'before DST end'
- assert_equal 23*3600-30*60-1, Time.local(0,30,1,30,10,2005,0,0,false,ENV['TZ']).seconds_until_end_of_day, 'after DST end'
+ assert_equal 24 * 3600 - 30 * 60 - 1, Time.local(0, 30, 1, 30, 10, 2005, 0, 0, true, ENV["TZ"]).seconds_until_end_of_day, "before DST end"
+ assert_equal 23 * 3600 - 30 * 60 - 1, Time.local(0, 30, 1, 30, 10, 2005, 0, 0, false, ENV["TZ"]).seconds_until_end_of_day, "after DST end"
end
- with_env_tz 'NZ' do
+ with_env_tz "NZ" do
# st: New Zealand: 2006 March 19th 3:00am DT => March 19th 2:00am ST
# avoid setting a time between 2:00 and 3:00 since that requires specifying whether DST is active
- assert_equal 23*3600, Time.local(2006,3,19,1,59,59).seconds_until_end_of_day, 'just before DST end'
- assert_equal 21*3600-2, Time.local(2006,3,19,3,0,1).seconds_until_end_of_day, 'just after DST end'
+ assert_equal 23 * 3600, Time.local(2006, 3, 19, 1, 59, 59).seconds_until_end_of_day, "just before DST end"
+ assert_equal 21 * 3600 - 2, Time.local(2006, 3, 19, 3, 0, 1).seconds_until_end_of_day, "just after DST end"
# now set a time between 2:00 and 3:00 by specifying whether DST is active
# uses: Time.local( sec, min, hour, day, month, year, wday, yday, isdst, tz )
- assert_equal 23*3600-30*60-1, Time.local(0,30,2,19,3,2006,0,0,true, ENV['TZ']).seconds_until_end_of_day, 'before DST end'
- assert_equal 22*3600-30*60-1, Time.local(0,30,2,19,3,2006,0,0,false,ENV['TZ']).seconds_until_end_of_day, 'after DST end'
+ assert_equal 23 * 3600 - 30 * 60 - 1, Time.local(0, 30, 2, 19, 3, 2006, 0, 0, true, ENV["TZ"]).seconds_until_end_of_day, "before DST end"
+ assert_equal 22 * 3600 - 30 * 60 - 1, Time.local(0, 30, 2, 19, 3, 2006, 0, 0, false, ENV["TZ"]).seconds_until_end_of_day, "after DST end"
end
end
+ def test_sec_fraction
+ time = Time.utc(2016, 4, 23, 0, 0, Rational(1, 10000000000))
+ assert_equal Rational(1, 10000000000), time.sec_fraction
+
+ time = Time.utc(2016, 4, 23, 0, 0, 0.0000000001)
+ assert_equal 0.0000000001.to_r, time.sec_fraction
+
+ time = Time.utc(2016, 4, 23, 0, 0, 0, Rational(1, 10000))
+ assert_equal Rational(1, 10000000000), time.sec_fraction
+
+ time = Time.utc(2016, 4, 23, 0, 0, 0, 0.0001)
+ assert_equal 0.0001.to_r / 1000000, time.sec_fraction
+ end
+
def test_beginning_of_day
- assert_equal Time.local(2005,2,4,0,0,0), Time.local(2005,2,4,10,10,10).beginning_of_day
- with_env_tz 'US/Eastern' do
- assert_equal Time.local(2006,4,2,0,0,0), Time.local(2006,4,2,10,10,10).beginning_of_day, 'start DST'
- assert_equal Time.local(2006,10,29,0,0,0), Time.local(2006,10,29,10,10,10).beginning_of_day, 'ends DST'
+ assert_equal Time.local(2005, 2, 4, 0, 0, 0), Time.local(2005, 2, 4, 10, 10, 10).beginning_of_day
+ with_env_tz "US/Eastern" do
+ assert_equal Time.local(2006, 4, 2, 0, 0, 0), Time.local(2006, 4, 2, 10, 10, 10).beginning_of_day, "start DST"
+ assert_equal Time.local(2006, 10, 29, 0, 0, 0), Time.local(2006, 10, 29, 10, 10, 10).beginning_of_day, "ends DST"
end
- with_env_tz 'NZ' do
- assert_equal Time.local(2006,3,19,0,0,0), Time.local(2006,3,19,10,10,10).beginning_of_day, 'ends DST'
- assert_equal Time.local(2006,10,1,0,0,0), Time.local(2006,10,1,10,10,10).beginning_of_day, 'start DST'
+ with_env_tz "NZ" do
+ assert_equal Time.local(2006, 3, 19, 0, 0, 0), Time.local(2006, 3, 19, 10, 10, 10).beginning_of_day, "ends DST"
+ assert_equal Time.local(2006, 10, 1, 0, 0, 0), Time.local(2006, 10, 1, 10, 10, 10).beginning_of_day, "start DST"
end
end
def test_middle_of_day
- assert_equal Time.local(2005,2,4,12,0,0), Time.local(2005,2,4,10,10,10).middle_of_day
- with_env_tz 'US/Eastern' do
- assert_equal Time.local(2006,4,2,12,0,0), Time.local(2006,4,2,10,10,10).middle_of_day, 'start DST'
- assert_equal Time.local(2006,10,29,12,0,0), Time.local(2006,10,29,10,10,10).middle_of_day, 'ends DST'
+ assert_equal Time.local(2005, 2, 4, 12, 0, 0), Time.local(2005, 2, 4, 10, 10, 10).middle_of_day
+ with_env_tz "US/Eastern" do
+ assert_equal Time.local(2006, 4, 2, 12, 0, 0), Time.local(2006, 4, 2, 10, 10, 10).middle_of_day, "start DST"
+ assert_equal Time.local(2006, 10, 29, 12, 0, 0), Time.local(2006, 10, 29, 10, 10, 10).middle_of_day, "ends DST"
end
- with_env_tz 'NZ' do
- assert_equal Time.local(2006,3,19,12,0,0), Time.local(2006,3,19,10,10,10).middle_of_day, 'ends DST'
- assert_equal Time.local(2006,10,1,12,0,0), Time.local(2006,10,1,10,10,10).middle_of_day, 'start DST'
+ with_env_tz "NZ" do
+ assert_equal Time.local(2006, 3, 19, 12, 0, 0), Time.local(2006, 3, 19, 10, 10, 10).middle_of_day, "ends DST"
+ assert_equal Time.local(2006, 10, 1, 12, 0, 0), Time.local(2006, 10, 1, 10, 10, 10).middle_of_day, "start DST"
end
end
def test_beginning_of_hour
- assert_equal Time.local(2005,2,4,19,0,0), Time.local(2005,2,4,19,30,10).beginning_of_hour
+ assert_equal Time.local(2005, 2, 4, 19, 0, 0), Time.local(2005, 2, 4, 19, 30, 10).beginning_of_hour
end
def test_beginning_of_minute
- assert_equal Time.local(2005,2,4,19,30,0), Time.local(2005,2,4,19,30,10).beginning_of_minute
+ assert_equal Time.local(2005, 2, 4, 19, 30, 0), Time.local(2005, 2, 4, 19, 30, 10).beginning_of_minute
end
def test_end_of_day
- assert_equal Time.local(2007,8,12,23,59,59,Rational(999999999, 1000)), Time.local(2007,8,12,10,10,10).end_of_day
- with_env_tz 'US/Eastern' do
- assert_equal Time.local(2007,4,2,23,59,59,Rational(999999999, 1000)), Time.local(2007,4,2,10,10,10).end_of_day, 'start DST'
- assert_equal Time.local(2007,10,29,23,59,59,Rational(999999999, 1000)), Time.local(2007,10,29,10,10,10).end_of_day, 'ends DST'
+ assert_equal Time.local(2007, 8, 12, 23, 59, 59, Rational(999999999, 1000)), Time.local(2007, 8, 12, 10, 10, 10).end_of_day
+ with_env_tz "US/Eastern" do
+ assert_equal Time.local(2007, 4, 2, 23, 59, 59, Rational(999999999, 1000)), Time.local(2007, 4, 2, 10, 10, 10).end_of_day, "start DST"
+ assert_equal Time.local(2007, 10, 29, 23, 59, 59, Rational(999999999, 1000)), Time.local(2007, 10, 29, 10, 10, 10).end_of_day, "ends DST"
end
- with_env_tz 'NZ' do
- assert_equal Time.local(2006,3,19,23,59,59,Rational(999999999, 1000)), Time.local(2006,3,19,10,10,10).end_of_day, 'ends DST'
- assert_equal Time.local(2006,10,1,23,59,59,Rational(999999999, 1000)), Time.local(2006,10,1,10,10,10).end_of_day, 'start DST'
+ with_env_tz "NZ" do
+ assert_equal Time.local(2006, 3, 19, 23, 59, 59, Rational(999999999, 1000)), Time.local(2006, 3, 19, 10, 10, 10).end_of_day, "ends DST"
+ assert_equal Time.local(2006, 10, 1, 23, 59, 59, Rational(999999999, 1000)), Time.local(2006, 10, 1, 10, 10, 10).end_of_day, "start DST"
end
- with_env_tz 'Asia/Yekaterinburg' do
- assert_equal Time.local(2015, 2, 8, 23, 59, 59, Rational(999999999, 1000)), Time.new(2015, 2, 8, 8, 0, 0, '+05:00').end_of_day
+ with_env_tz "Asia/Yekaterinburg" do
+ assert_equal Time.local(2015, 2, 8, 23, 59, 59, Rational(999999999, 1000)), Time.new(2015, 2, 8, 8, 0, 0, "+05:00").end_of_day
end
end
def test_end_of_hour
- assert_equal Time.local(2005,2,4,19,59,59,Rational(999999999, 1000)), Time.local(2005,2,4,19,30,10).end_of_hour
+ assert_equal Time.local(2005, 2, 4, 19, 59, 59, Rational(999999999, 1000)), Time.local(2005, 2, 4, 19, 30, 10).end_of_hour
end
def test_end_of_minute
- assert_equal Time.local(2005,2,4,19,30,59,Rational(999999999, 1000)), Time.local(2005,2,4,19,30,10).end_of_minute
- end
-
- def test_last_year
- assert_equal Time.local(2004,6,5,10), Time.local(2005,6,5,10,0,0).last_year
+ assert_equal Time.local(2005, 2, 4, 19, 30, 59, Rational(999999999, 1000)), Time.local(2005, 2, 4, 19, 30, 10).end_of_minute
end
def test_ago
- assert_equal Time.local(2005,2,22,10,10,9), Time.local(2005,2,22,10,10,10).ago(1)
- assert_equal Time.local(2005,2,22,9,10,10), Time.local(2005,2,22,10,10,10).ago(3600)
- assert_equal Time.local(2005,2,20,10,10,10), Time.local(2005,2,22,10,10,10).ago(86400*2)
- assert_equal Time.local(2005,2,20,9,9,45), Time.local(2005,2,22,10,10,10).ago(86400*2 + 3600 + 25)
+ assert_equal Time.local(2005, 2, 22, 10, 10, 9), Time.local(2005, 2, 22, 10, 10, 10).ago(1)
+ assert_equal Time.local(2005, 2, 22, 9, 10, 10), Time.local(2005, 2, 22, 10, 10, 10).ago(3600)
+ assert_equal Time.local(2005, 2, 20, 10, 10, 10), Time.local(2005, 2, 22, 10, 10, 10).ago(86400 * 2)
+ assert_equal Time.local(2005, 2, 20, 9, 9, 45), Time.local(2005, 2, 22, 10, 10, 10).ago(86400 * 2 + 3600 + 25)
end
def test_daylight_savings_time_crossings_backward_start
- with_env_tz 'US/Eastern' do
+ with_env_tz "US/Eastern" do
# dt: US: 2005 April 3rd 4:18am
- assert_equal Time.local(2005,4,2,3,18,0), Time.local(2005,4,3,4,18,0).ago(24.hours), 'dt-24.hours=>st'
- assert_equal Time.local(2005,4,2,3,18,0), Time.local(2005,4,3,4,18,0).ago(86400), 'dt-86400=>st'
- assert_equal Time.local(2005,4,2,3,18,0), Time.local(2005,4,3,4,18,0).ago(86400.seconds), 'dt-86400.seconds=>st'
+ assert_equal Time.local(2005, 4, 2, 3, 18, 0), Time.local(2005, 4, 3, 4, 18, 0).ago(24.hours), "dt-24.hours=>st"
+ assert_equal Time.local(2005, 4, 2, 3, 18, 0), Time.local(2005, 4, 3, 4, 18, 0).ago(86400), "dt-86400=>st"
+ assert_equal Time.local(2005, 4, 2, 3, 18, 0), Time.local(2005, 4, 3, 4, 18, 0).ago(86400.seconds), "dt-86400.seconds=>st"
- assert_equal Time.local(2005,4,1,4,18,0), Time.local(2005,4,2,4,18,0).ago(24.hours), 'st-24.hours=>st'
- assert_equal Time.local(2005,4,1,4,18,0), Time.local(2005,4,2,4,18,0).ago(86400), 'st-86400=>st'
- assert_equal Time.local(2005,4,1,4,18,0), Time.local(2005,4,2,4,18,0).ago(86400.seconds), 'st-86400.seconds=>st'
+ assert_equal Time.local(2005, 4, 1, 4, 18, 0), Time.local(2005, 4, 2, 4, 18, 0).ago(24.hours), "st-24.hours=>st"
+ assert_equal Time.local(2005, 4, 1, 4, 18, 0), Time.local(2005, 4, 2, 4, 18, 0).ago(86400), "st-86400=>st"
+ assert_equal Time.local(2005, 4, 1, 4, 18, 0), Time.local(2005, 4, 2, 4, 18, 0).ago(86400.seconds), "st-86400.seconds=>st"
end
- with_env_tz 'NZ' do
+ with_env_tz "NZ" do
# dt: New Zealand: 2006 October 1st 4:18am
- assert_equal Time.local(2006,9,30,3,18,0), Time.local(2006,10,1,4,18,0).ago(24.hours), 'dt-24.hours=>st'
- assert_equal Time.local(2006,9,30,3,18,0), Time.local(2006,10,1,4,18,0).ago(86400), 'dt-86400=>st'
- assert_equal Time.local(2006,9,30,3,18,0), Time.local(2006,10,1,4,18,0).ago(86400.seconds), 'dt-86400.seconds=>st'
+ assert_equal Time.local(2006, 9, 30, 3, 18, 0), Time.local(2006, 10, 1, 4, 18, 0).ago(24.hours), "dt-24.hours=>st"
+ assert_equal Time.local(2006, 9, 30, 3, 18, 0), Time.local(2006, 10, 1, 4, 18, 0).ago(86400), "dt-86400=>st"
+ assert_equal Time.local(2006, 9, 30, 3, 18, 0), Time.local(2006, 10, 1, 4, 18, 0).ago(86400.seconds), "dt-86400.seconds=>st"
- assert_equal Time.local(2006,9,29,4,18,0), Time.local(2006,9,30,4,18,0).ago(24.hours), 'st-24.hours=>st'
- assert_equal Time.local(2006,9,29,4,18,0), Time.local(2006,9,30,4,18,0).ago(86400), 'st-86400=>st'
- assert_equal Time.local(2006,9,29,4,18,0), Time.local(2006,9,30,4,18,0).ago(86400.seconds), 'st-86400.seconds=>st'
+ assert_equal Time.local(2006, 9, 29, 4, 18, 0), Time.local(2006, 9, 30, 4, 18, 0).ago(24.hours), "st-24.hours=>st"
+ assert_equal Time.local(2006, 9, 29, 4, 18, 0), Time.local(2006, 9, 30, 4, 18, 0).ago(86400), "st-86400=>st"
+ assert_equal Time.local(2006, 9, 29, 4, 18, 0), Time.local(2006, 9, 30, 4, 18, 0).ago(86400.seconds), "st-86400.seconds=>st"
end
end
def test_daylight_savings_time_crossings_backward_end
- with_env_tz 'US/Eastern' do
+ with_env_tz "US/Eastern" do
# st: US: 2005 October 30th 4:03am
- assert_equal Time.local(2005,10,29,5,3), Time.local(2005,10,30,4,3,0).ago(24.hours), 'st-24.hours=>dt'
- assert_equal Time.local(2005,10,29,5,3), Time.local(2005,10,30,4,3,0).ago(86400), 'st-86400=>dt'
- assert_equal Time.local(2005,10,29,5,3), Time.local(2005,10,30,4,3,0).ago(86400.seconds), 'st-86400.seconds=>dt'
+ assert_equal Time.local(2005, 10, 29, 5, 3), Time.local(2005, 10, 30, 4, 3, 0).ago(24.hours), "st-24.hours=>dt"
+ assert_equal Time.local(2005, 10, 29, 5, 3), Time.local(2005, 10, 30, 4, 3, 0).ago(86400), "st-86400=>dt"
+ assert_equal Time.local(2005, 10, 29, 5, 3), Time.local(2005, 10, 30, 4, 3, 0).ago(86400.seconds), "st-86400.seconds=>dt"
- assert_equal Time.local(2005,10,28,4,3), Time.local(2005,10,29,4,3,0).ago(24.hours), 'dt-24.hours=>dt'
- assert_equal Time.local(2005,10,28,4,3), Time.local(2005,10,29,4,3,0).ago(86400), 'dt-86400=>dt'
- assert_equal Time.local(2005,10,28,4,3), Time.local(2005,10,29,4,3,0).ago(86400.seconds), 'dt-86400.seconds=>dt'
+ assert_equal Time.local(2005, 10, 28, 4, 3), Time.local(2005, 10, 29, 4, 3, 0).ago(24.hours), "dt-24.hours=>dt"
+ assert_equal Time.local(2005, 10, 28, 4, 3), Time.local(2005, 10, 29, 4, 3, 0).ago(86400), "dt-86400=>dt"
+ assert_equal Time.local(2005, 10, 28, 4, 3), Time.local(2005, 10, 29, 4, 3, 0).ago(86400.seconds), "dt-86400.seconds=>dt"
end
- with_env_tz 'NZ' do
+ with_env_tz "NZ" do
# st: New Zealand: 2006 March 19th 4:03am
- assert_equal Time.local(2006,3,18,5,3), Time.local(2006,3,19,4,3,0).ago(24.hours), 'st-24.hours=>dt'
- assert_equal Time.local(2006,3,18,5,3), Time.local(2006,3,19,4,3,0).ago(86400), 'st-86400=>dt'
- assert_equal Time.local(2006,3,18,5,3), Time.local(2006,3,19,4,3,0).ago(86400.seconds), 'st-86400.seconds=>dt'
+ assert_equal Time.local(2006, 3, 18, 5, 3), Time.local(2006, 3, 19, 4, 3, 0).ago(24.hours), "st-24.hours=>dt"
+ assert_equal Time.local(2006, 3, 18, 5, 3), Time.local(2006, 3, 19, 4, 3, 0).ago(86400), "st-86400=>dt"
+ assert_equal Time.local(2006, 3, 18, 5, 3), Time.local(2006, 3, 19, 4, 3, 0).ago(86400.seconds), "st-86400.seconds=>dt"
- assert_equal Time.local(2006,3,17,4,3), Time.local(2006,3,18,4,3,0).ago(24.hours), 'dt-24.hours=>dt'
- assert_equal Time.local(2006,3,17,4,3), Time.local(2006,3,18,4,3,0).ago(86400), 'dt-86400=>dt'
- assert_equal Time.local(2006,3,17,4,3), Time.local(2006,3,18,4,3,0).ago(86400.seconds), 'dt-86400.seconds=>dt'
+ assert_equal Time.local(2006, 3, 17, 4, 3), Time.local(2006, 3, 18, 4, 3, 0).ago(24.hours), "dt-24.hours=>dt"
+ assert_equal Time.local(2006, 3, 17, 4, 3), Time.local(2006, 3, 18, 4, 3, 0).ago(86400), "dt-86400=>dt"
+ assert_equal Time.local(2006, 3, 17, 4, 3), Time.local(2006, 3, 18, 4, 3, 0).ago(86400.seconds), "dt-86400.seconds=>dt"
end
end
def test_daylight_savings_time_crossings_backward_start_1day
- with_env_tz 'US/Eastern' do
+ with_env_tz "US/Eastern" do
# dt: US: 2005 April 3rd 4:18am
- assert_equal Time.local(2005,4,2,4,18,0), Time.local(2005,4,3,4,18,0).ago(1.day), 'dt-1.day=>st'
- assert_equal Time.local(2005,4,1,4,18,0), Time.local(2005,4,2,4,18,0).ago(1.day), 'st-1.day=>st'
+ assert_equal Time.local(2005, 4, 2, 4, 18, 0), Time.local(2005, 4, 3, 4, 18, 0).ago(1.day), "dt-1.day=>st"
+ assert_equal Time.local(2005, 4, 1, 4, 18, 0), Time.local(2005, 4, 2, 4, 18, 0).ago(1.day), "st-1.day=>st"
end
- with_env_tz 'NZ' do
+ with_env_tz "NZ" do
# dt: New Zealand: 2006 October 1st 4:18am
- assert_equal Time.local(2006,9,30,4,18,0), Time.local(2006,10,1,4,18,0).ago(1.day), 'dt-1.day=>st'
- assert_equal Time.local(2006,9,29,4,18,0), Time.local(2006,9,30,4,18,0).ago(1.day), 'st-1.day=>st'
+ assert_equal Time.local(2006, 9, 30, 4, 18, 0), Time.local(2006, 10, 1, 4, 18, 0).ago(1.day), "dt-1.day=>st"
+ assert_equal Time.local(2006, 9, 29, 4, 18, 0), Time.local(2006, 9, 30, 4, 18, 0).ago(1.day), "st-1.day=>st"
end
end
def test_daylight_savings_time_crossings_backward_end_1day
- with_env_tz 'US/Eastern' do
+ with_env_tz "US/Eastern" do
# st: US: 2005 October 30th 4:03am
- assert_equal Time.local(2005,10,29,4,3), Time.local(2005,10,30,4,3,0).ago(1.day), 'st-1.day=>dt'
- assert_equal Time.local(2005,10,28,4,3), Time.local(2005,10,29,4,3,0).ago(1.day), 'dt-1.day=>dt'
+ assert_equal Time.local(2005, 10, 29, 4, 3), Time.local(2005, 10, 30, 4, 3, 0).ago(1.day), "st-1.day=>dt"
+ assert_equal Time.local(2005, 10, 28, 4, 3), Time.local(2005, 10, 29, 4, 3, 0).ago(1.day), "dt-1.day=>dt"
end
- with_env_tz 'NZ' do
+ with_env_tz "NZ" do
# st: New Zealand: 2006 March 19th 4:03am
- assert_equal Time.local(2006,3,18,4,3), Time.local(2006,3,19,4,3,0).ago(1.day), 'st-1.day=>dt'
- assert_equal Time.local(2006,3,17,4,3), Time.local(2006,3,18,4,3,0).ago(1.day), 'dt-1.day=>dt'
+ assert_equal Time.local(2006, 3, 18, 4, 3), Time.local(2006, 3, 19, 4, 3, 0).ago(1.day), "st-1.day=>dt"
+ assert_equal Time.local(2006, 3, 17, 4, 3), Time.local(2006, 3, 18, 4, 3, 0).ago(1.day), "dt-1.day=>dt"
end
end
def test_since
- assert_equal Time.local(2005,2,22,10,10,11), Time.local(2005,2,22,10,10,10).since(1)
- assert_equal Time.local(2005,2,22,11,10,10), Time.local(2005,2,22,10,10,10).since(3600)
- assert_equal Time.local(2005,2,24,10,10,10), Time.local(2005,2,22,10,10,10).since(86400*2)
- assert_equal Time.local(2005,2,24,11,10,35), Time.local(2005,2,22,10,10,10).since(86400*2 + 3600 + 25)
+ assert_equal Time.local(2005, 2, 22, 10, 10, 11), Time.local(2005, 2, 22, 10, 10, 10).since(1)
+ assert_equal Time.local(2005, 2, 22, 11, 10, 10), Time.local(2005, 2, 22, 10, 10, 10).since(3600)
+ assert_equal Time.local(2005, 2, 24, 10, 10, 10), Time.local(2005, 2, 22, 10, 10, 10).since(86400 * 2)
+ assert_equal Time.local(2005, 2, 24, 11, 10, 35), Time.local(2005, 2, 22, 10, 10, 10).since(86400 * 2 + 3600 + 25)
# when out of range of Time, returns a DateTime
- assert_equal DateTime.civil(2038,1,20,11,59,59), Time.utc(2038,1,18,11,59,59).since(86400*2)
+ assert_equal DateTime.civil(2038, 1, 20, 11, 59, 59), Time.utc(2038, 1, 18, 11, 59, 59).since(86400 * 2)
end
def test_daylight_savings_time_crossings_forward_start
- with_env_tz 'US/Eastern' do
+ with_env_tz "US/Eastern" do
# st: US: 2005 April 2nd 7:27pm
- assert_equal Time.local(2005,4,3,20,27,0), Time.local(2005,4,2,19,27,0).since(24.hours), 'st+24.hours=>dt'
- assert_equal Time.local(2005,4,3,20,27,0), Time.local(2005,4,2,19,27,0).since(86400), 'st+86400=>dt'
- assert_equal Time.local(2005,4,3,20,27,0), Time.local(2005,4,2,19,27,0).since(86400.seconds), 'st+86400.seconds=>dt'
+ assert_equal Time.local(2005, 4, 3, 20, 27, 0), Time.local(2005, 4, 2, 19, 27, 0).since(24.hours), "st+24.hours=>dt"
+ assert_equal Time.local(2005, 4, 3, 20, 27, 0), Time.local(2005, 4, 2, 19, 27, 0).since(86400), "st+86400=>dt"
+ assert_equal Time.local(2005, 4, 3, 20, 27, 0), Time.local(2005, 4, 2, 19, 27, 0).since(86400.seconds), "st+86400.seconds=>dt"
- assert_equal Time.local(2005,4,4,19,27,0), Time.local(2005,4,3,19,27,0).since(24.hours), 'dt+24.hours=>dt'
- assert_equal Time.local(2005,4,4,19,27,0), Time.local(2005,4,3,19,27,0).since(86400), 'dt+86400=>dt'
- assert_equal Time.local(2005,4,4,19,27,0), Time.local(2005,4,3,19,27,0).since(86400.seconds), 'dt+86400.seconds=>dt'
+ assert_equal Time.local(2005, 4, 4, 19, 27, 0), Time.local(2005, 4, 3, 19, 27, 0).since(24.hours), "dt+24.hours=>dt"
+ assert_equal Time.local(2005, 4, 4, 19, 27, 0), Time.local(2005, 4, 3, 19, 27, 0).since(86400), "dt+86400=>dt"
+ assert_equal Time.local(2005, 4, 4, 19, 27, 0), Time.local(2005, 4, 3, 19, 27, 0).since(86400.seconds), "dt+86400.seconds=>dt"
end
- with_env_tz 'NZ' do
+ with_env_tz "NZ" do
# st: New Zealand: 2006 September 30th 7:27pm
- assert_equal Time.local(2006,10,1,20,27,0), Time.local(2006,9,30,19,27,0).since(24.hours), 'st+24.hours=>dt'
- assert_equal Time.local(2006,10,1,20,27,0), Time.local(2006,9,30,19,27,0).since(86400), 'st+86400=>dt'
- assert_equal Time.local(2006,10,1,20,27,0), Time.local(2006,9,30,19,27,0).since(86400.seconds), 'st+86400.seconds=>dt'
+ assert_equal Time.local(2006, 10, 1, 20, 27, 0), Time.local(2006, 9, 30, 19, 27, 0).since(24.hours), "st+24.hours=>dt"
+ assert_equal Time.local(2006, 10, 1, 20, 27, 0), Time.local(2006, 9, 30, 19, 27, 0).since(86400), "st+86400=>dt"
+ assert_equal Time.local(2006, 10, 1, 20, 27, 0), Time.local(2006, 9, 30, 19, 27, 0).since(86400.seconds), "st+86400.seconds=>dt"
- assert_equal Time.local(2006,10,2,19,27,0), Time.local(2006,10,1,19,27,0).since(24.hours), 'dt+24.hours=>dt'
- assert_equal Time.local(2006,10,2,19,27,0), Time.local(2006,10,1,19,27,0).since(86400), 'dt+86400=>dt'
- assert_equal Time.local(2006,10,2,19,27,0), Time.local(2006,10,1,19,27,0).since(86400.seconds), 'dt+86400.seconds=>dt'
+ assert_equal Time.local(2006, 10, 2, 19, 27, 0), Time.local(2006, 10, 1, 19, 27, 0).since(24.hours), "dt+24.hours=>dt"
+ assert_equal Time.local(2006, 10, 2, 19, 27, 0), Time.local(2006, 10, 1, 19, 27, 0).since(86400), "dt+86400=>dt"
+ assert_equal Time.local(2006, 10, 2, 19, 27, 0), Time.local(2006, 10, 1, 19, 27, 0).since(86400.seconds), "dt+86400.seconds=>dt"
end
end
def test_daylight_savings_time_crossings_forward_start_1day
- with_env_tz 'US/Eastern' do
+ with_env_tz "US/Eastern" do
# st: US: 2005 April 2nd 7:27pm
- assert_equal Time.local(2005,4,3,19,27,0), Time.local(2005,4,2,19,27,0).since(1.day), 'st+1.day=>dt'
- assert_equal Time.local(2005,4,4,19,27,0), Time.local(2005,4,3,19,27,0).since(1.day), 'dt+1.day=>dt'
+ assert_equal Time.local(2005, 4, 3, 19, 27, 0), Time.local(2005, 4, 2, 19, 27, 0).since(1.day), "st+1.day=>dt"
+ assert_equal Time.local(2005, 4, 4, 19, 27, 0), Time.local(2005, 4, 3, 19, 27, 0).since(1.day), "dt+1.day=>dt"
end
- with_env_tz 'NZ' do
+ with_env_tz "NZ" do
# st: New Zealand: 2006 September 30th 7:27pm
- assert_equal Time.local(2006,10,1,19,27,0), Time.local(2006,9,30,19,27,0).since(1.day), 'st+1.day=>dt'
- assert_equal Time.local(2006,10,2,19,27,0), Time.local(2006,10,1,19,27,0).since(1.day), 'dt+1.day=>dt'
+ assert_equal Time.local(2006, 10, 1, 19, 27, 0), Time.local(2006, 9, 30, 19, 27, 0).since(1.day), "st+1.day=>dt"
+ assert_equal Time.local(2006, 10, 2, 19, 27, 0), Time.local(2006, 10, 1, 19, 27, 0).since(1.day), "dt+1.day=>dt"
end
end
def test_daylight_savings_time_crossings_forward_start_tomorrow
- with_env_tz 'US/Eastern' do
+ with_env_tz "US/Eastern" do
# st: US: 2005 April 2nd 7:27pm
- assert_equal Time.local(2005,4,3,19,27,0), Time.local(2005,4,2,19,27,0).tomorrow, 'st+1.day=>dt'
- assert_equal Time.local(2005,4,4,19,27,0), Time.local(2005,4,3,19,27,0).tomorrow, 'dt+1.day=>dt'
+ assert_equal Time.local(2005, 4, 3, 19, 27, 0), Time.local(2005, 4, 2, 19, 27, 0).tomorrow, "st+1.day=>dt"
+ assert_equal Time.local(2005, 4, 4, 19, 27, 0), Time.local(2005, 4, 3, 19, 27, 0).tomorrow, "dt+1.day=>dt"
end
- with_env_tz 'NZ' do
+ with_env_tz "NZ" do
# st: New Zealand: 2006 September 30th 7:27pm
- assert_equal Time.local(2006,10,1,19,27,0), Time.local(2006,9,30,19,27,0).tomorrow, 'st+1.day=>dt'
- assert_equal Time.local(2006,10,2,19,27,0), Time.local(2006,10,1,19,27,0).tomorrow, 'dt+1.day=>dt'
+ assert_equal Time.local(2006, 10, 1, 19, 27, 0), Time.local(2006, 9, 30, 19, 27, 0).tomorrow, "st+1.day=>dt"
+ assert_equal Time.local(2006, 10, 2, 19, 27, 0), Time.local(2006, 10, 1, 19, 27, 0).tomorrow, "dt+1.day=>dt"
end
end
def test_daylight_savings_time_crossings_backward_start_yesterday
- with_env_tz 'US/Eastern' do
+ with_env_tz "US/Eastern" do
# st: US: 2005 April 2nd 7:27pm
- assert_equal Time.local(2005,4,2,19,27,0), Time.local(2005,4,3,19,27,0).yesterday, 'dt-1.day=>st'
- assert_equal Time.local(2005,4,3,19,27,0), Time.local(2005,4,4,19,27,0).yesterday, 'dt-1.day=>dt'
+ assert_equal Time.local(2005, 4, 2, 19, 27, 0), Time.local(2005, 4, 3, 19, 27, 0).yesterday, "dt-1.day=>st"
+ assert_equal Time.local(2005, 4, 3, 19, 27, 0), Time.local(2005, 4, 4, 19, 27, 0).yesterday, "dt-1.day=>dt"
end
- with_env_tz 'NZ' do
+ with_env_tz "NZ" do
# st: New Zealand: 2006 September 30th 7:27pm
- assert_equal Time.local(2006,9,30,19,27,0), Time.local(2006,10,1,19,27,0).yesterday, 'dt-1.day=>st'
- assert_equal Time.local(2006,10,1,19,27,0), Time.local(2006,10,2,19,27,0).yesterday, 'dt-1.day=>dt'
+ assert_equal Time.local(2006, 9, 30, 19, 27, 0), Time.local(2006, 10, 1, 19, 27, 0).yesterday, "dt-1.day=>st"
+ assert_equal Time.local(2006, 10, 1, 19, 27, 0), Time.local(2006, 10, 2, 19, 27, 0).yesterday, "dt-1.day=>dt"
end
end
def test_daylight_savings_time_crossings_forward_end
- with_env_tz 'US/Eastern' do
+ with_env_tz "US/Eastern" do
# dt: US: 2005 October 30th 12:45am
- assert_equal Time.local(2005,10,30,23,45,0), Time.local(2005,10,30,0,45,0).since(24.hours), 'dt+24.hours=>st'
- assert_equal Time.local(2005,10,30,23,45,0), Time.local(2005,10,30,0,45,0).since(86400), 'dt+86400=>st'
- assert_equal Time.local(2005,10,30,23,45,0), Time.local(2005,10,30,0,45,0).since(86400.seconds), 'dt+86400.seconds=>st'
+ assert_equal Time.local(2005, 10, 30, 23, 45, 0), Time.local(2005, 10, 30, 0, 45, 0).since(24.hours), "dt+24.hours=>st"
+ assert_equal Time.local(2005, 10, 30, 23, 45, 0), Time.local(2005, 10, 30, 0, 45, 0).since(86400), "dt+86400=>st"
+ assert_equal Time.local(2005, 10, 30, 23, 45, 0), Time.local(2005, 10, 30, 0, 45, 0).since(86400.seconds), "dt+86400.seconds=>st"
- assert_equal Time.local(2005,11, 1,0,45,0), Time.local(2005,10,31,0,45,0).since(24.hours), 'st+24.hours=>st'
- assert_equal Time.local(2005,11, 1,0,45,0), Time.local(2005,10,31,0,45,0).since(86400), 'st+86400=>st'
- assert_equal Time.local(2005,11, 1,0,45,0), Time.local(2005,10,31,0,45,0).since(86400.seconds), 'st+86400.seconds=>st'
+ assert_equal Time.local(2005, 11, 1, 0, 45, 0), Time.local(2005, 10, 31, 0, 45, 0).since(24.hours), "st+24.hours=>st"
+ assert_equal Time.local(2005, 11, 1, 0, 45, 0), Time.local(2005, 10, 31, 0, 45, 0).since(86400), "st+86400=>st"
+ assert_equal Time.local(2005, 11, 1, 0, 45, 0), Time.local(2005, 10, 31, 0, 45, 0).since(86400.seconds), "st+86400.seconds=>st"
end
- with_env_tz 'NZ' do
+ with_env_tz "NZ" do
# dt: New Zealand: 2006 March 19th 1:45am
- assert_equal Time.local(2006,3,20,0,45,0), Time.local(2006,3,19,1,45,0).since(24.hours), 'dt+24.hours=>st'
- assert_equal Time.local(2006,3,20,0,45,0), Time.local(2006,3,19,1,45,0).since(86400), 'dt+86400=>st'
- assert_equal Time.local(2006,3,20,0,45,0), Time.local(2006,3,19,1,45,0).since(86400.seconds), 'dt+86400.seconds=>st'
+ assert_equal Time.local(2006, 3, 20, 0, 45, 0), Time.local(2006, 3, 19, 1, 45, 0).since(24.hours), "dt+24.hours=>st"
+ assert_equal Time.local(2006, 3, 20, 0, 45, 0), Time.local(2006, 3, 19, 1, 45, 0).since(86400), "dt+86400=>st"
+ assert_equal Time.local(2006, 3, 20, 0, 45, 0), Time.local(2006, 3, 19, 1, 45, 0).since(86400.seconds), "dt+86400.seconds=>st"
- assert_equal Time.local(2006,3,21,1,45,0), Time.local(2006,3,20,1,45,0).since(24.hours), 'st+24.hours=>st'
- assert_equal Time.local(2006,3,21,1,45,0), Time.local(2006,3,20,1,45,0).since(86400), 'st+86400=>st'
- assert_equal Time.local(2006,3,21,1,45,0), Time.local(2006,3,20,1,45,0).since(86400.seconds), 'st+86400.seconds=>st'
+ assert_equal Time.local(2006, 3, 21, 1, 45, 0), Time.local(2006, 3, 20, 1, 45, 0).since(24.hours), "st+24.hours=>st"
+ assert_equal Time.local(2006, 3, 21, 1, 45, 0), Time.local(2006, 3, 20, 1, 45, 0).since(86400), "st+86400=>st"
+ assert_equal Time.local(2006, 3, 21, 1, 45, 0), Time.local(2006, 3, 20, 1, 45, 0).since(86400.seconds), "st+86400.seconds=>st"
end
end
def test_daylight_savings_time_crossings_forward_end_1day
- with_env_tz 'US/Eastern' do
+ with_env_tz "US/Eastern" do
# dt: US: 2005 October 30th 12:45am
- assert_equal Time.local(2005,10,31,0,45,0), Time.local(2005,10,30,0,45,0).since(1.day), 'dt+1.day=>st'
- assert_equal Time.local(2005,11, 1,0,45,0), Time.local(2005,10,31,0,45,0).since(1.day), 'st+1.day=>st'
+ assert_equal Time.local(2005, 10, 31, 0, 45, 0), Time.local(2005, 10, 30, 0, 45, 0).since(1.day), "dt+1.day=>st"
+ assert_equal Time.local(2005, 11, 1, 0, 45, 0), Time.local(2005, 10, 31, 0, 45, 0).since(1.day), "st+1.day=>st"
end
- with_env_tz 'NZ' do
+ with_env_tz "NZ" do
# dt: New Zealand: 2006 March 19th 1:45am
- assert_equal Time.local(2006,3,20,1,45,0), Time.local(2006,3,19,1,45,0).since(1.day), 'dt+1.day=>st'
- assert_equal Time.local(2006,3,21,1,45,0), Time.local(2006,3,20,1,45,0).since(1.day), 'st+1.day=>st'
+ assert_equal Time.local(2006, 3, 20, 1, 45, 0), Time.local(2006, 3, 19, 1, 45, 0).since(1.day), "dt+1.day=>st"
+ assert_equal Time.local(2006, 3, 21, 1, 45, 0), Time.local(2006, 3, 20, 1, 45, 0).since(1.day), "st+1.day=>st"
end
end
def test_daylight_savings_time_crossings_forward_end_tomorrow
- with_env_tz 'US/Eastern' do
+ with_env_tz "US/Eastern" do
# dt: US: 2005 October 30th 12:45am
- assert_equal Time.local(2005,10,31,0,45,0), Time.local(2005,10,30,0,45,0).tomorrow, 'dt+1.day=>st'
- assert_equal Time.local(2005,11, 1,0,45,0), Time.local(2005,10,31,0,45,0).tomorrow, 'st+1.day=>st'
+ assert_equal Time.local(2005, 10, 31, 0, 45, 0), Time.local(2005, 10, 30, 0, 45, 0).tomorrow, "dt+1.day=>st"
+ assert_equal Time.local(2005, 11, 1, 0, 45, 0), Time.local(2005, 10, 31, 0, 45, 0).tomorrow, "st+1.day=>st"
end
- with_env_tz 'NZ' do
+ with_env_tz "NZ" do
# dt: New Zealand: 2006 March 19th 1:45am
- assert_equal Time.local(2006,3,20,1,45,0), Time.local(2006,3,19,1,45,0).tomorrow, 'dt+1.day=>st'
- assert_equal Time.local(2006,3,21,1,45,0), Time.local(2006,3,20,1,45,0).tomorrow, 'st+1.day=>st'
+ assert_equal Time.local(2006, 3, 20, 1, 45, 0), Time.local(2006, 3, 19, 1, 45, 0).tomorrow, "dt+1.day=>st"
+ assert_equal Time.local(2006, 3, 21, 1, 45, 0), Time.local(2006, 3, 20, 1, 45, 0).tomorrow, "st+1.day=>st"
end
end
def test_daylight_savings_time_crossings_backward_end_yesterday
- with_env_tz 'US/Eastern' do
+ with_env_tz "US/Eastern" do
# dt: US: 2005 October 30th 12:45am
- assert_equal Time.local(2005,10,30,0,45,0), Time.local(2005,10,31,0,45,0).yesterday, 'st-1.day=>dt'
- assert_equal Time.local(2005,10, 31,0,45,0), Time.local(2005,11,1,0,45,0).yesterday, 'st-1.day=>st'
+ assert_equal Time.local(2005, 10, 30, 0, 45, 0), Time.local(2005, 10, 31, 0, 45, 0).yesterday, "st-1.day=>dt"
+ assert_equal Time.local(2005, 10, 31, 0, 45, 0), Time.local(2005, 11, 1, 0, 45, 0).yesterday, "st-1.day=>st"
end
- with_env_tz 'NZ' do
+ with_env_tz "NZ" do
# dt: New Zealand: 2006 March 19th 1:45am
- assert_equal Time.local(2006,3,19,1,45,0), Time.local(2006,3,20,1,45,0).yesterday, 'st-1.day=>dt'
- assert_equal Time.local(2006,3,20,1,45,0), Time.local(2006,3,21,1,45,0).yesterday, 'st-1.day=>st'
+ assert_equal Time.local(2006, 3, 19, 1, 45, 0), Time.local(2006, 3, 20, 1, 45, 0).yesterday, "st-1.day=>dt"
+ assert_equal Time.local(2006, 3, 20, 1, 45, 0), Time.local(2006, 3, 21, 1, 45, 0).yesterday, "st-1.day=>st"
end
end
def test_change
- assert_equal Time.local(2006,2,22,15,15,10), Time.local(2005,2,22,15,15,10).change(:year => 2006)
- assert_equal Time.local(2005,6,22,15,15,10), Time.local(2005,2,22,15,15,10).change(:month => 6)
- assert_equal Time.local(2012,9,22,15,15,10), Time.local(2005,2,22,15,15,10).change(:year => 2012, :month => 9)
- assert_equal Time.local(2005,2,22,16), Time.local(2005,2,22,15,15,10).change(:hour => 16)
- assert_equal Time.local(2005,2,22,16,45), Time.local(2005,2,22,15,15,10).change(:hour => 16, :min => 45)
- assert_equal Time.local(2005,2,22,15,45), Time.local(2005,2,22,15,15,10).change(:min => 45)
-
- assert_equal Time.local(2005,1,2, 5, 0, 0, 0), Time.local(2005,1,2,11,22,33,44).change(:hour => 5)
- assert_equal Time.local(2005,1,2,11, 6, 0, 0), Time.local(2005,1,2,11,22,33,44).change(:min => 6)
- assert_equal Time.local(2005,1,2,11,22, 7, 0), Time.local(2005,1,2,11,22,33,44).change(:sec => 7)
- assert_equal Time.local(2005,1,2,11,22,33, 8), Time.local(2005,1,2,11,22,33,44).change(:usec => 8)
- assert_equal Time.local(2005,1,2,11,22,33, 8), Time.local(2005,1,2,11,22,33,2).change(:nsec => 8000)
- assert_raise(ArgumentError) { Time.local(2005,1,2,11,22,33, 8).change(:usec => 1, :nsec => 1) }
- assert_nothing_raised(ArgumentError) { Time.new(2015, 5, 9, 10, 00, 00, '+03:00').change(nsec: 999999999) }
+ assert_equal Time.local(2006, 2, 22, 15, 15, 10), Time.local(2005, 2, 22, 15, 15, 10).change(year: 2006)
+ assert_equal Time.local(2005, 6, 22, 15, 15, 10), Time.local(2005, 2, 22, 15, 15, 10).change(month: 6)
+ assert_equal Time.local(2012, 9, 22, 15, 15, 10), Time.local(2005, 2, 22, 15, 15, 10).change(year: 2012, month: 9)
+ assert_equal Time.local(2005, 2, 22, 16), Time.local(2005, 2, 22, 15, 15, 10).change(hour: 16)
+ assert_equal Time.local(2005, 2, 22, 16, 45), Time.local(2005, 2, 22, 15, 15, 10).change(hour: 16, min: 45)
+ assert_equal Time.local(2005, 2, 22, 15, 45), Time.local(2005, 2, 22, 15, 15, 10).change(min: 45)
+
+ assert_equal Time.local(2005, 1, 2, 5, 0, 0, 0), Time.local(2005, 1, 2, 11, 22, 33, 44).change(hour: 5)
+ assert_equal Time.local(2005, 1, 2, 11, 6, 0, 0), Time.local(2005, 1, 2, 11, 22, 33, 44).change(min: 6)
+ assert_equal Time.local(2005, 1, 2, 11, 22, 7, 0), Time.local(2005, 1, 2, 11, 22, 33, 44).change(sec: 7)
+ assert_equal Time.local(2005, 1, 2, 11, 22, 33, 8), Time.local(2005, 1, 2, 11, 22, 33, 44).change(usec: 8)
+ assert_equal Time.local(2005, 1, 2, 11, 22, 33, 8), Time.local(2005, 1, 2, 11, 22, 33, 2).change(nsec: 8000)
+ assert_raise(ArgumentError) { Time.local(2005, 1, 2, 11, 22, 33, 8).change(usec: 1, nsec: 1) }
+ assert_nothing_raised { Time.new(2015, 5, 9, 10, 00, 00, "+03:00").change(nsec: 999999999) }
end
def test_utc_change
- assert_equal Time.utc(2006,2,22,15,15,10), Time.utc(2005,2,22,15,15,10).change(:year => 2006)
- assert_equal Time.utc(2005,6,22,15,15,10), Time.utc(2005,2,22,15,15,10).change(:month => 6)
- assert_equal Time.utc(2012,9,22,15,15,10), Time.utc(2005,2,22,15,15,10).change(:year => 2012, :month => 9)
- assert_equal Time.utc(2005,2,22,16), Time.utc(2005,2,22,15,15,10).change(:hour => 16)
- assert_equal Time.utc(2005,2,22,16,45), Time.utc(2005,2,22,15,15,10).change(:hour => 16, :min => 45)
- assert_equal Time.utc(2005,2,22,15,45), Time.utc(2005,2,22,15,15,10).change(:min => 45)
- assert_equal Time.utc(2005,1,2,11,22,33,8), Time.utc(2005,1,2,11,22,33,2).change(:nsec => 8000)
+ assert_equal Time.utc(2006, 2, 22, 15, 15, 10), Time.utc(2005, 2, 22, 15, 15, 10).change(year: 2006)
+ assert_equal Time.utc(2005, 6, 22, 15, 15, 10), Time.utc(2005, 2, 22, 15, 15, 10).change(month: 6)
+ assert_equal Time.utc(2012, 9, 22, 15, 15, 10), Time.utc(2005, 2, 22, 15, 15, 10).change(year: 2012, month: 9)
+ assert_equal Time.utc(2005, 2, 22, 16), Time.utc(2005, 2, 22, 15, 15, 10).change(hour: 16)
+ assert_equal Time.utc(2005, 2, 22, 16, 45), Time.utc(2005, 2, 22, 15, 15, 10).change(hour: 16, min: 45)
+ assert_equal Time.utc(2005, 2, 22, 15, 45), Time.utc(2005, 2, 22, 15, 15, 10).change(min: 45)
+ assert_equal Time.utc(2005, 1, 2, 11, 22, 33, 8), Time.utc(2005, 1, 2, 11, 22, 33, 2).change(nsec: 8000)
end
def test_offset_change
- assert_equal Time.new(2006,2,22,15,15,10,"-08:00"), Time.new(2005,2,22,15,15,10,"-08:00").change(:year => 2006)
- assert_equal Time.new(2005,6,22,15,15,10,"-08:00"), Time.new(2005,2,22,15,15,10,"-08:00").change(:month => 6)
- assert_equal Time.new(2012,9,22,15,15,10,"-08:00"), Time.new(2005,2,22,15,15,10,"-08:00").change(:year => 2012, :month => 9)
- assert_equal Time.new(2005,2,22,16,0,0,"-08:00"), Time.new(2005,2,22,15,15,10,"-08:00").change(:hour => 16)
- assert_equal Time.new(2005,2,22,16,45,0,"-08:00"), Time.new(2005,2,22,15,15,10,"-08:00").change(:hour => 16, :min => 45)
- assert_equal Time.new(2005,2,22,15,45,0,"-08:00"), Time.new(2005,2,22,15,15,10,"-08:00").change(:min => 45)
- assert_equal Time.new(2005,2,22,15,15,10,"-08:00"), Time.new(2005,2,22,15,15,0,"-08:00").change(:sec => 10)
- assert_equal 10, Time.new(2005,2,22,15,15,0,"-08:00").change(:usec => 10).usec
- assert_equal 10, Time.new(2005,2,22,15,15,0,"-08:00").change(:nsec => 10).nsec
- assert_raise(ArgumentError) { Time.new(2005, 2, 22, 15, 15, 45, "-08:00").change(:usec => 1000000) }
- assert_raise(ArgumentError) { Time.new(2005, 2, 22, 15, 15, 45, "-08:00").change(:nsec => 1000000000) }
+ assert_equal Time.new(2006, 2, 22, 15, 15, 10, "-08:00"), Time.new(2005, 2, 22, 15, 15, 10, "-08:00").change(year: 2006)
+ assert_equal Time.new(2005, 6, 22, 15, 15, 10, "-08:00"), Time.new(2005, 2, 22, 15, 15, 10, "-08:00").change(month: 6)
+ assert_equal Time.new(2012, 9, 22, 15, 15, 10, "-08:00"), Time.new(2005, 2, 22, 15, 15, 10, "-08:00").change(year: 2012, month: 9)
+ assert_equal Time.new(2005, 2, 22, 16, 0, 0, "-08:00"), Time.new(2005, 2, 22, 15, 15, 10, "-08:00").change(hour: 16)
+ assert_equal Time.new(2005, 2, 22, 16, 45, 0, "-08:00"), Time.new(2005, 2, 22, 15, 15, 10, "-08:00").change(hour: 16, min: 45)
+ assert_equal Time.new(2005, 2, 22, 15, 45, 0, "-08:00"), Time.new(2005, 2, 22, 15, 15, 10, "-08:00").change(min: 45)
+ assert_equal Time.new(2005, 2, 22, 15, 15, 10, "-08:00"), Time.new(2005, 2, 22, 15, 15, 0, "-08:00").change(sec: 10)
+ assert_equal 10, Time.new(2005, 2, 22, 15, 15, 0, "-08:00").change(usec: 10).usec
+ assert_equal 10, Time.new(2005, 2, 22, 15, 15, 0, "-08:00").change(nsec: 10).nsec
+ assert_raise(ArgumentError) { Time.new(2005, 2, 22, 15, 15, 45, "-08:00").change(usec: 1000000) }
+ assert_raise(ArgumentError) { Time.new(2005, 2, 22, 15, 15, 45, "-08:00").change(nsec: 1000000000) }
+ end
+
+ def test_change_offset
+ assert_equal Time.new(2006, 2, 22, 15, 15, 10, "-08:00"), Time.new(2006, 2, 22, 15, 15, 10, "+01:00").change(offset: "-08:00")
+ assert_equal Time.new(2006, 2, 22, 15, 15, 10, -28800), Time.new(2006, 2, 22, 15, 15, 10, 3600).change(offset: -28800)
+ assert_raise(ArgumentError) { Time.new(2005, 2, 22, 15, 15, 45, "+01:00").change(usec: 1000000, offset: "-08:00") }
+ assert_raise(ArgumentError) { Time.new(2005, 2, 22, 15, 15, 45, "+01:00").change(nsec: 1000000000, offset: -28800) }
end
def test_advance
- assert_equal Time.local(2006,2,28,15,15,10), Time.local(2005,2,28,15,15,10).advance(:years => 1)
- assert_equal Time.local(2005,6,28,15,15,10), Time.local(2005,2,28,15,15,10).advance(:months => 4)
- assert_equal Time.local(2005,3,21,15,15,10), Time.local(2005,2,28,15,15,10).advance(:weeks => 3)
- assert_equal Time.local(2005,3,25,3,15,10), Time.local(2005,2,28,15,15,10).advance(:weeks => 3.5)
- assert_in_delta Time.local(2005,3,26,12,51,10), Time.local(2005,2,28,15,15,10).advance(:weeks => 3.7), 1
- assert_equal Time.local(2005,3,5,15,15,10), Time.local(2005,2,28,15,15,10).advance(:days => 5)
- assert_equal Time.local(2005,3,6,3,15,10), Time.local(2005,2,28,15,15,10).advance(:days => 5.5)
- assert_in_delta Time.local(2005,3,6,8,3,10), Time.local(2005,2,28,15,15,10).advance(:days => 5.7), 1
- assert_equal Time.local(2012,9,28,15,15,10), Time.local(2005,2,28,15,15,10).advance(:years => 7, :months => 7)
- assert_equal Time.local(2013,10,3,15,15,10), Time.local(2005,2,28,15,15,10).advance(:years => 7, :months => 19, :days => 5)
- assert_equal Time.local(2013,10,17,15,15,10), Time.local(2005,2,28,15,15,10).advance(:years => 7, :months => 19, :weeks => 2, :days => 5)
- assert_equal Time.local(2001,12,27,15,15,10), Time.local(2005,2,28,15,15,10).advance(:years => -3, :months => -2, :days => -1)
- assert_equal Time.local(2005,2,28,15,15,10), Time.local(2004,2,29,15,15,10).advance(:years => 1) #leap day plus one year
- assert_equal Time.local(2005,2,28,20,15,10), Time.local(2005,2,28,15,15,10).advance(:hours => 5)
- assert_equal Time.local(2005,2,28,15,22,10), Time.local(2005,2,28,15,15,10).advance(:minutes => 7)
- assert_equal Time.local(2005,2,28,15,15,19), Time.local(2005,2,28,15,15,10).advance(:seconds => 9)
- assert_equal Time.local(2005,2,28,20,22,19), Time.local(2005,2,28,15,15,10).advance(:hours => 5, :minutes => 7, :seconds => 9)
- assert_equal Time.local(2005,2,28,10,8,1), Time.local(2005,2,28,15,15,10).advance(:hours => -5, :minutes => -7, :seconds => -9)
- assert_equal Time.local(2013,10,17,20,22,19), Time.local(2005,2,28,15,15,10).advance(:years => 7, :months => 19, :weeks => 2, :days => 5, :hours => 5, :minutes => 7, :seconds => 9)
+ assert_equal Time.local(2006, 2, 28, 15, 15, 10), Time.local(2005, 2, 28, 15, 15, 10).advance(years: 1)
+ assert_equal Time.local(2005, 6, 28, 15, 15, 10), Time.local(2005, 2, 28, 15, 15, 10).advance(months: 4)
+ assert_equal Time.local(2005, 3, 21, 15, 15, 10), Time.local(2005, 2, 28, 15, 15, 10).advance(weeks: 3)
+ assert_equal Time.local(2005, 3, 25, 3, 15, 10), Time.local(2005, 2, 28, 15, 15, 10).advance(weeks: 3.5)
+ assert_in_delta Time.local(2005, 3, 26, 12, 51, 10), Time.local(2005, 2, 28, 15, 15, 10).advance(weeks: 3.7), 1
+ assert_equal Time.local(2005, 3, 5, 15, 15, 10), Time.local(2005, 2, 28, 15, 15, 10).advance(days: 5)
+ assert_equal Time.local(2005, 3, 6, 3, 15, 10), Time.local(2005, 2, 28, 15, 15, 10).advance(days: 5.5)
+ assert_in_delta Time.local(2005, 3, 6, 8, 3, 10), Time.local(2005, 2, 28, 15, 15, 10).advance(days: 5.7), 1
+ assert_equal Time.local(2012, 9, 28, 15, 15, 10), Time.local(2005, 2, 28, 15, 15, 10).advance(years: 7, months: 7)
+ assert_equal Time.local(2013, 10, 3, 15, 15, 10), Time.local(2005, 2, 28, 15, 15, 10).advance(years: 7, months: 19, days: 5)
+ assert_equal Time.local(2013, 10, 17, 15, 15, 10), Time.local(2005, 2, 28, 15, 15, 10).advance(years: 7, months: 19, weeks: 2, days: 5)
+ assert_equal Time.local(2001, 12, 27, 15, 15, 10), Time.local(2005, 2, 28, 15, 15, 10).advance(years: -3, months: -2, days: -1)
+ assert_equal Time.local(2005, 2, 28, 15, 15, 10), Time.local(2004, 2, 29, 15, 15, 10).advance(years: 1) # leap day plus one year
+ assert_equal Time.local(2005, 2, 28, 20, 15, 10), Time.local(2005, 2, 28, 15, 15, 10).advance(hours: 5)
+ assert_equal Time.local(2005, 2, 28, 15, 22, 10), Time.local(2005, 2, 28, 15, 15, 10).advance(minutes: 7)
+ assert_equal Time.local(2005, 2, 28, 15, 15, 19), Time.local(2005, 2, 28, 15, 15, 10).advance(seconds: 9)
+ assert_equal Time.local(2005, 2, 28, 20, 22, 19), Time.local(2005, 2, 28, 15, 15, 10).advance(hours: 5, minutes: 7, seconds: 9)
+ assert_equal Time.local(2005, 2, 28, 10, 8, 1), Time.local(2005, 2, 28, 15, 15, 10).advance(hours: -5, minutes: -7, seconds: -9)
+ assert_equal Time.local(2013, 10, 17, 20, 22, 19), Time.local(2005, 2, 28, 15, 15, 10).advance(years: 7, months: 19, weeks: 2, days: 5, hours: 5, minutes: 7, seconds: 9)
end
def test_utc_advance
- assert_equal Time.utc(2006,2,22,15,15,10), Time.utc(2005,2,22,15,15,10).advance(:years => 1)
- assert_equal Time.utc(2005,6,22,15,15,10), Time.utc(2005,2,22,15,15,10).advance(:months => 4)
- assert_equal Time.utc(2005,3,21,15,15,10), Time.utc(2005,2,28,15,15,10).advance(:weeks => 3)
- assert_equal Time.utc(2005,3,25,3,15,10), Time.utc(2005,2,28,15,15,10).advance(:weeks => 3.5)
- assert_in_delta Time.utc(2005,3,26,12,51,10), Time.utc(2005,2,28,15,15,10).advance(:weeks => 3.7), 1
- assert_equal Time.utc(2005,3,5,15,15,10), Time.utc(2005,2,28,15,15,10).advance(:days => 5)
- assert_equal Time.utc(2005,3,6,3,15,10), Time.utc(2005,2,28,15,15,10).advance(:days => 5.5)
- assert_in_delta Time.utc(2005,3,6,8,3,10), Time.utc(2005,2,28,15,15,10).advance(:days => 5.7), 1
- assert_equal Time.utc(2012,9,22,15,15,10), Time.utc(2005,2,22,15,15,10).advance(:years => 7, :months => 7)
- assert_equal Time.utc(2013,10,3,15,15,10), Time.utc(2005,2,22,15,15,10).advance(:years => 7, :months => 19, :days => 11)
- assert_equal Time.utc(2013,10,17,15,15,10), Time.utc(2005,2,28,15,15,10).advance(:years => 7, :months => 19, :weeks => 2, :days => 5)
- assert_equal Time.utc(2001,12,27,15,15,10), Time.utc(2005,2,28,15,15,10).advance(:years => -3, :months => -2, :days => -1)
- assert_equal Time.utc(2005,2,28,15,15,10), Time.utc(2004,2,29,15,15,10).advance(:years => 1) #leap day plus one year
- assert_equal Time.utc(2005,2,28,20,15,10), Time.utc(2005,2,28,15,15,10).advance(:hours => 5)
- assert_equal Time.utc(2005,2,28,15,22,10), Time.utc(2005,2,28,15,15,10).advance(:minutes => 7)
- assert_equal Time.utc(2005,2,28,15,15,19), Time.utc(2005,2,28,15,15,10).advance(:seconds => 9)
- assert_equal Time.utc(2005,2,28,20,22,19), Time.utc(2005,2,28,15,15,10).advance(:hours => 5, :minutes => 7, :seconds => 9)
- assert_equal Time.utc(2005,2,28,10,8,1), Time.utc(2005,2,28,15,15,10).advance(:hours => -5, :minutes => -7, :seconds => -9)
- assert_equal Time.utc(2013,10,17,20,22,19), Time.utc(2005,2,28,15,15,10).advance(:years => 7, :months => 19, :weeks => 2, :days => 5, :hours => 5, :minutes => 7, :seconds => 9)
+ assert_equal Time.utc(2006, 2, 22, 15, 15, 10), Time.utc(2005, 2, 22, 15, 15, 10).advance(years: 1)
+ assert_equal Time.utc(2005, 6, 22, 15, 15, 10), Time.utc(2005, 2, 22, 15, 15, 10).advance(months: 4)
+ assert_equal Time.utc(2005, 3, 21, 15, 15, 10), Time.utc(2005, 2, 28, 15, 15, 10).advance(weeks: 3)
+ assert_equal Time.utc(2005, 3, 25, 3, 15, 10), Time.utc(2005, 2, 28, 15, 15, 10).advance(weeks: 3.5)
+ assert_in_delta Time.utc(2005, 3, 26, 12, 51, 10), Time.utc(2005, 2, 28, 15, 15, 10).advance(weeks: 3.7), 1
+ assert_equal Time.utc(2005, 3, 5, 15, 15, 10), Time.utc(2005, 2, 28, 15, 15, 10).advance(days: 5)
+ assert_equal Time.utc(2005, 3, 6, 3, 15, 10), Time.utc(2005, 2, 28, 15, 15, 10).advance(days: 5.5)
+ assert_in_delta Time.utc(2005, 3, 6, 8, 3, 10), Time.utc(2005, 2, 28, 15, 15, 10).advance(days: 5.7), 1
+ assert_equal Time.utc(2012, 9, 22, 15, 15, 10), Time.utc(2005, 2, 22, 15, 15, 10).advance(years: 7, months: 7)
+ assert_equal Time.utc(2013, 10, 3, 15, 15, 10), Time.utc(2005, 2, 22, 15, 15, 10).advance(years: 7, months: 19, days: 11)
+ assert_equal Time.utc(2013, 10, 17, 15, 15, 10), Time.utc(2005, 2, 28, 15, 15, 10).advance(years: 7, months: 19, weeks: 2, days: 5)
+ assert_equal Time.utc(2001, 12, 27, 15, 15, 10), Time.utc(2005, 2, 28, 15, 15, 10).advance(years: -3, months: -2, days: -1)
+ assert_equal Time.utc(2005, 2, 28, 15, 15, 10), Time.utc(2004, 2, 29, 15, 15, 10).advance(years: 1) # leap day plus one year
+ assert_equal Time.utc(2005, 2, 28, 20, 15, 10), Time.utc(2005, 2, 28, 15, 15, 10).advance(hours: 5)
+ assert_equal Time.utc(2005, 2, 28, 15, 22, 10), Time.utc(2005, 2, 28, 15, 15, 10).advance(minutes: 7)
+ assert_equal Time.utc(2005, 2, 28, 15, 15, 19), Time.utc(2005, 2, 28, 15, 15, 10).advance(seconds: 9)
+ assert_equal Time.utc(2005, 2, 28, 20, 22, 19), Time.utc(2005, 2, 28, 15, 15, 10).advance(hours: 5, minutes: 7, seconds: 9)
+ assert_equal Time.utc(2005, 2, 28, 10, 8, 1), Time.utc(2005, 2, 28, 15, 15, 10).advance(hours: -5, minutes: -7, seconds: -9)
+ assert_equal Time.utc(2013, 10, 17, 20, 22, 19), Time.utc(2005, 2, 28, 15, 15, 10).advance(years: 7, months: 19, weeks: 2, days: 5, hours: 5, minutes: 7, seconds: 9)
end
def test_offset_advance
- assert_equal Time.new(2006,2,22,15,15,10,'-08:00'), Time.new(2005,2,22,15,15,10,'-08:00').advance(:years => 1)
- assert_equal Time.new(2005,6,22,15,15,10,'-08:00'), Time.new(2005,2,22,15,15,10,'-08:00').advance(:months => 4)
- assert_equal Time.new(2005,3,21,15,15,10,'-08:00'), Time.new(2005,2,28,15,15,10,'-08:00').advance(:weeks => 3)
- assert_equal Time.new(2005,3,25,3,15,10,'-08:00'), Time.new(2005,2,28,15,15,10,'-08:00').advance(:weeks => 3.5)
- assert_in_delta Time.new(2005,3,26,12,51,10,'-08:00'), Time.new(2005,2,28,15,15,10,'-08:00').advance(:weeks => 3.7), 1
- assert_equal Time.new(2005,3,5,15,15,10,'-08:00'), Time.new(2005,2,28,15,15,10,'-08:00').advance(:days => 5)
- assert_equal Time.new(2005,3,6,3,15,10,'-08:00'), Time.new(2005,2,28,15,15,10,'-08:00').advance(:days => 5.5)
- assert_in_delta Time.new(2005,3,6,8,3,10,'-08:00'), Time.new(2005,2,28,15,15,10,'-08:00').advance(:days => 5.7), 1
- assert_equal Time.new(2012,9,22,15,15,10,'-08:00'), Time.new(2005,2,22,15,15,10,'-08:00').advance(:years => 7, :months => 7)
- assert_equal Time.new(2013,10,3,15,15,10,'-08:00'), Time.new(2005,2,22,15,15,10,'-08:00').advance(:years => 7, :months => 19, :days => 11)
- assert_equal Time.new(2013,10,17,15,15,10,'-08:00'), Time.new(2005,2,28,15,15,10,'-08:00').advance(:years => 7, :months => 19, :weeks => 2, :days => 5)
- assert_equal Time.new(2001,12,27,15,15,10,'-08:00'), Time.new(2005,2,28,15,15,10,'-08:00').advance(:years => -3, :months => -2, :days => -1)
- assert_equal Time.new(2005,2,28,15,15,10,'-08:00'), Time.new(2004,2,29,15,15,10,'-08:00').advance(:years => 1) #leap day plus one year
- assert_equal Time.new(2005,2,28,20,15,10,'-08:00'), Time.new(2005,2,28,15,15,10,'-08:00').advance(:hours => 5)
- assert_equal Time.new(2005,2,28,15,22,10,'-08:00'), Time.new(2005,2,28,15,15,10,'-08:00').advance(:minutes => 7)
- assert_equal Time.new(2005,2,28,15,15,19,'-08:00'), Time.new(2005,2,28,15,15,10,'-08:00').advance(:seconds => 9)
- assert_equal Time.new(2005,2,28,20,22,19,'-08:00'), Time.new(2005,2,28,15,15,10,'-08:00').advance(:hours => 5, :minutes => 7, :seconds => 9)
- assert_equal Time.new(2005,2,28,10,8,1,'-08:00'), Time.new(2005,2,28,15,15,10,'-08:00').advance(:hours => -5, :minutes => -7, :seconds => -9)
- assert_equal Time.new(2013,10,17,20,22,19,'-08:00'), Time.new(2005,2,28,15,15,10,'-08:00').advance(:years => 7, :months => 19, :weeks => 2, :days => 5, :hours => 5, :minutes => 7, :seconds => 9)
+ assert_equal Time.new(2006, 2, 22, 15, 15, 10, "-08:00"), Time.new(2005, 2, 22, 15, 15, 10, "-08:00").advance(years: 1)
+ assert_equal Time.new(2005, 6, 22, 15, 15, 10, "-08:00"), Time.new(2005, 2, 22, 15, 15, 10, "-08:00").advance(months: 4)
+ assert_equal Time.new(2005, 3, 21, 15, 15, 10, "-08:00"), Time.new(2005, 2, 28, 15, 15, 10, "-08:00").advance(weeks: 3)
+ assert_equal Time.new(2005, 3, 25, 3, 15, 10, "-08:00"), Time.new(2005, 2, 28, 15, 15, 10, "-08:00").advance(weeks: 3.5)
+ assert_in_delta Time.new(2005, 3, 26, 12, 51, 10, "-08:00"), Time.new(2005, 2, 28, 15, 15, 10, "-08:00").advance(weeks: 3.7), 1
+ assert_equal Time.new(2005, 3, 5, 15, 15, 10, "-08:00"), Time.new(2005, 2, 28, 15, 15, 10, "-08:00").advance(days: 5)
+ assert_equal Time.new(2005, 3, 6, 3, 15, 10, "-08:00"), Time.new(2005, 2, 28, 15, 15, 10, "-08:00").advance(days: 5.5)
+ assert_in_delta Time.new(2005, 3, 6, 8, 3, 10, "-08:00"), Time.new(2005, 2, 28, 15, 15, 10, "-08:00").advance(days: 5.7), 1
+ assert_equal Time.new(2012, 9, 22, 15, 15, 10, "-08:00"), Time.new(2005, 2, 22, 15, 15, 10, "-08:00").advance(years: 7, months: 7)
+ assert_equal Time.new(2013, 10, 3, 15, 15, 10, "-08:00"), Time.new(2005, 2, 22, 15, 15, 10, "-08:00").advance(years: 7, months: 19, days: 11)
+ assert_equal Time.new(2013, 10, 17, 15, 15, 10, "-08:00"), Time.new(2005, 2, 28, 15, 15, 10, "-08:00").advance(years: 7, months: 19, weeks: 2, days: 5)
+ assert_equal Time.new(2001, 12, 27, 15, 15, 10, "-08:00"), Time.new(2005, 2, 28, 15, 15, 10, "-08:00").advance(years: -3, months: -2, days: -1)
+ assert_equal Time.new(2005, 2, 28, 15, 15, 10, "-08:00"), Time.new(2004, 2, 29, 15, 15, 10, "-08:00").advance(years: 1) # leap day plus one year
+ assert_equal Time.new(2005, 2, 28, 20, 15, 10, "-08:00"), Time.new(2005, 2, 28, 15, 15, 10, "-08:00").advance(hours: 5)
+ assert_equal Time.new(2005, 2, 28, 15, 22, 10, "-08:00"), Time.new(2005, 2, 28, 15, 15, 10, "-08:00").advance(minutes: 7)
+ assert_equal Time.new(2005, 2, 28, 15, 15, 19, "-08:00"), Time.new(2005, 2, 28, 15, 15, 10, "-08:00").advance(seconds: 9)
+ assert_equal Time.new(2005, 2, 28, 20, 22, 19, "-08:00"), Time.new(2005, 2, 28, 15, 15, 10, "-08:00").advance(hours: 5, minutes: 7, seconds: 9)
+ assert_equal Time.new(2005, 2, 28, 10, 8, 1, "-08:00"), Time.new(2005, 2, 28, 15, 15, 10, "-08:00").advance(hours: -5, minutes: -7, seconds: -9)
+ assert_equal Time.new(2013, 10, 17, 20, 22, 19, "-08:00"), Time.new(2005, 2, 28, 15, 15, 10, "-08:00").advance(years: 7, months: 19, weeks: 2, days: 5, hours: 5, minutes: 7, seconds: 9)
end
def test_advance_with_nsec
t = Time.at(0, Rational(108635108, 1000))
- assert_equal t, t.advance(:months => 0)
+ assert_equal t, t.advance(months: 0)
end
def test_advance_gregorian_proleptic
- assert_equal Time.local(1582,10,14,15,15,10), Time.local(1582,10,15,15,15,10).advance(:days => -1)
- assert_equal Time.local(1582,10,15,15,15,10), Time.local(1582,10,14,15,15,10).advance(:days => 1)
- assert_equal Time.local(1582,10,5,15,15,10), Time.local(1582,10,4,15,15,10).advance(:days => 1)
- assert_equal Time.local(1582,10,4,15,15,10), Time.local(1582,10,5,15,15,10).advance(:days => -1)
+ assert_equal Time.local(1582, 10, 14, 15, 15, 10), Time.local(1582, 10, 15, 15, 15, 10).advance(days: -1)
+ assert_equal Time.local(1582, 10, 15, 15, 15, 10), Time.local(1582, 10, 14, 15, 15, 10).advance(days: 1)
+ assert_equal Time.local(1582, 10, 5, 15, 15, 10), Time.local(1582, 10, 4, 15, 15, 10).advance(days: 1)
+ assert_equal Time.local(1582, 10, 4, 15, 15, 10), Time.local(1582, 10, 5, 15, 15, 10).advance(days: -1)
end
def test_last_week
- with_env_tz 'US/Eastern' do
- assert_equal Time.local(2005,2,21), Time.local(2005,3,1,15,15,10).last_week
- assert_equal Time.local(2005,2,22), Time.local(2005,3,1,15,15,10).last_week(:tuesday)
- assert_equal Time.local(2005,2,25), Time.local(2005,3,1,15,15,10).last_week(:friday)
- assert_equal Time.local(2006,10,30), Time.local(2006,11,6,0,0,0).last_week
- assert_equal Time.local(2006,11,15), Time.local(2006,11,23,0,0,0).last_week(:wednesday)
+ with_env_tz "US/Eastern" do
+ assert_equal Time.local(2005, 2, 21), Time.local(2005, 3, 1, 15, 15, 10).last_week
+ assert_equal Time.local(2005, 2, 22), Time.local(2005, 3, 1, 15, 15, 10).last_week(:tuesday)
+ assert_equal Time.local(2005, 2, 25), Time.local(2005, 3, 1, 15, 15, 10).last_week(:friday)
+ assert_equal Time.local(2006, 10, 30), Time.local(2006, 11, 6, 0, 0, 0).last_week
+ assert_equal Time.local(2006, 11, 15), Time.local(2006, 11, 23, 0, 0, 0).last_week(:wednesday)
end
end
def test_next_week_near_daylight_start
- with_env_tz 'US/Eastern' do
- assert_equal Time.local(2006,4,3), Time.local(2006,4,2,23,1,0).next_week, 'just crossed standard => daylight'
+ with_env_tz "US/Eastern" do
+ assert_equal Time.local(2006, 4, 3), Time.local(2006, 4, 2, 23, 1, 0).next_week, "just crossed standard => daylight"
end
- with_env_tz 'NZ' do
- assert_equal Time.local(2006,10,2), Time.local(2006,10,1,23,1,0).next_week, 'just crossed standard => daylight'
+ with_env_tz "NZ" do
+ assert_equal Time.local(2006, 10, 2), Time.local(2006, 10, 1, 23, 1, 0).next_week, "just crossed standard => daylight"
end
end
def test_next_week_near_daylight_end
- with_env_tz 'US/Eastern' do
- assert_equal Time.local(2006,10,30), Time.local(2006,10,29,23,1,0).next_week, 'just crossed daylight => standard'
+ with_env_tz "US/Eastern" do
+ assert_equal Time.local(2006, 10, 30), Time.local(2006, 10, 29, 23, 1, 0).next_week, "just crossed daylight => standard"
end
- with_env_tz 'NZ' do
- assert_equal Time.local(2006,3,20), Time.local(2006,3,19,23,1,0).next_week, 'just crossed daylight => standard'
+ with_env_tz "NZ" do
+ assert_equal Time.local(2006, 3, 20), Time.local(2006, 3, 19, 23, 1, 0).next_week, "just crossed daylight => standard"
end
end
@@ -534,6 +553,7 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
assert_equal "17:44", time.to_s(:time)
assert_equal "20050221174430", time.to_s(:number)
assert_equal "20050221174430123456789", time.to_s(:nsec)
+ assert_equal "20050221174430123456", time.to_s(:usec)
assert_equal "February 21, 2005 17:44", time.to_s(:long)
assert_equal "February 21st, 2005 17:44", time.to_s(:long_ordinal)
with_env_tz "UTC" do
@@ -549,28 +569,33 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
end
def test_custom_date_format
- Time::DATE_FORMATS[:custom] = '%Y%m%d%H%M%S'
- assert_equal '20050221143000', Time.local(2005, 2, 21, 14, 30, 0).to_s(:custom)
+ Time::DATE_FORMATS[:custom] = "%Y%m%d%H%M%S"
+ assert_equal "20050221143000", Time.local(2005, 2, 21, 14, 30, 0).to_s(:custom)
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
def test_to_datetime
assert_equal Time.utc(2005, 2, 21, 17, 44, 30).to_datetime, DateTime.civil(2005, 2, 21, 17, 44, 30, 0)
- with_env_tz 'US/Eastern' do
+ with_env_tz "US/Eastern" do
assert_equal Time.local(2005, 2, 21, 17, 44, 30).to_datetime, DateTime.civil(2005, 2, 21, 17, 44, 30, Rational(Time.local(2005, 2, 21, 17, 44, 30).utc_offset, 86400))
end
- with_env_tz 'NZ' do
+ with_env_tz "NZ" do
assert_equal Time.local(2005, 2, 21, 17, 44, 30).to_datetime, DateTime.civil(2005, 2, 21, 17, 44, 30, Rational(Time.local(2005, 2, 21, 17, 44, 30).utc_offset, 86400))
end
assert_equal ::Date::ITALY, Time.utc(2005, 2, 21, 17, 44, 30).to_datetime.start # use Ruby's default start value
end
def test_to_time
- with_env_tz 'US/Eastern' do
+ with_env_tz "US/Eastern" do
assert_equal Time, Time.local(2005, 2, 21, 17, 44, 30).to_time.class
assert_equal Time.local(2005, 2, 21, 17, 44, 30), Time.local(2005, 2, 21, 17, 44, 30).to_time
assert_equal Time.local(2005, 2, 21, 17, 44, 30).utc_offset, Time.local(2005, 2, 21, 17, 44, 30).to_time.utc_offset
@@ -616,8 +641,23 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
end
end
- def test_last_month_on_31st
- assert_equal Time.local(2004, 2, 29), Time.local(2004, 3, 31).last_month
+ def test_days_in_year_with_year
+ assert_equal 365, Time.days_in_year(2005)
+ assert_equal 366, Time.days_in_year(2004)
+ assert_equal 366, Time.days_in_year(2000)
+ assert_equal 365, Time.days_in_year(1900)
+ end
+
+ def test_days_in_year_in_common_year_without_year_arg
+ Time.stub(:now, Time.utc(2007)) do
+ assert_equal 365, Time.days_in_year
+ end
+ end
+
+ def test_days_in_year_in_leap_year_without_year_arg
+ Time.stub(:now, Time.utc(2008)) do
+ assert_equal 366, Time.days_in_year
+ end
end
def test_xmlschema_is_available
@@ -626,118 +666,118 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
def test_today_with_time_local
Date.stub(:current, Date.new(2000, 1, 1)) do
- assert_equal false, Time.local(1999,12,31,23,59,59).today?
- assert_equal true, Time.local(2000,1,1,0).today?
- assert_equal true, Time.local(2000,1,1,23,59,59).today?
- assert_equal false, Time.local(2000,1,2,0).today?
+ assert_equal false, Time.local(1999, 12, 31, 23, 59, 59).today?
+ assert_equal true, Time.local(2000, 1, 1, 0).today?
+ assert_equal true, Time.local(2000, 1, 1, 23, 59, 59).today?
+ assert_equal false, Time.local(2000, 1, 2, 0).today?
end
end
def test_today_with_time_utc
Date.stub(:current, Date.new(2000, 1, 1)) do
- assert_equal false, Time.utc(1999,12,31,23,59,59).today?
- assert_equal true, Time.utc(2000,1,1,0).today?
- assert_equal true, Time.utc(2000,1,1,23,59,59).today?
- assert_equal false, Time.utc(2000,1,2,0).today?
+ assert_equal false, Time.utc(1999, 12, 31, 23, 59, 59).today?
+ assert_equal true, Time.utc(2000, 1, 1, 0).today?
+ assert_equal true, Time.utc(2000, 1, 1, 23, 59, 59).today?
+ assert_equal false, Time.utc(2000, 1, 2, 0).today?
end
end
def test_past_with_time_current_as_time_local
- with_env_tz 'US/Eastern' do
- Time.stub(:current, Time.local(2005,2,10,15,30,45)) do
- assert_equal true, Time.local(2005,2,10,15,30,44).past?
- assert_equal false, Time.local(2005,2,10,15,30,45).past?
- assert_equal false, Time.local(2005,2,10,15,30,46).past?
- assert_equal true, Time.utc(2005,2,10,20,30,44).past?
- assert_equal false, Time.utc(2005,2,10,20,30,45).past?
- assert_equal false, Time.utc(2005,2,10,20,30,46).past?
+ with_env_tz "US/Eastern" do
+ Time.stub(:current, Time.local(2005, 2, 10, 15, 30, 45)) do
+ assert_equal true, Time.local(2005, 2, 10, 15, 30, 44).past?
+ assert_equal false, Time.local(2005, 2, 10, 15, 30, 45).past?
+ assert_equal false, Time.local(2005, 2, 10, 15, 30, 46).past?
+ assert_equal true, Time.utc(2005, 2, 10, 20, 30, 44).past?
+ assert_equal false, Time.utc(2005, 2, 10, 20, 30, 45).past?
+ assert_equal false, Time.utc(2005, 2, 10, 20, 30, 46).past?
end
end
end
def test_past_with_time_current_as_time_with_zone
- with_env_tz 'US/Eastern' do
- twz = Time.utc(2005,2,10,15,30,45).in_time_zone('Central Time (US & Canada)')
+ with_env_tz "US/Eastern" do
+ twz = Time.utc(2005, 2, 10, 15, 30, 45).in_time_zone("Central Time (US & Canada)")
Time.stub(:current, twz) do
- assert_equal true, Time.local(2005,2,10,10,30,44).past?
- assert_equal false, Time.local(2005,2,10,10,30,45).past?
- assert_equal false, Time.local(2005,2,10,10,30,46).past?
- assert_equal true, Time.utc(2005,2,10,15,30,44).past?
- assert_equal false, Time.utc(2005,2,10,15,30,45).past?
- assert_equal false, Time.utc(2005,2,10,15,30,46).past?
+ assert_equal true, Time.local(2005, 2, 10, 10, 30, 44).past?
+ assert_equal false, Time.local(2005, 2, 10, 10, 30, 45).past?
+ assert_equal false, Time.local(2005, 2, 10, 10, 30, 46).past?
+ assert_equal true, Time.utc(2005, 2, 10, 15, 30, 44).past?
+ assert_equal false, Time.utc(2005, 2, 10, 15, 30, 45).past?
+ assert_equal false, Time.utc(2005, 2, 10, 15, 30, 46).past?
end
end
end
def test_future_with_time_current_as_time_local
- with_env_tz 'US/Eastern' do
- Time.stub(:current, Time.local(2005,2,10,15,30,45)) do
- assert_equal false, Time.local(2005,2,10,15,30,44).future?
- assert_equal false, Time.local(2005,2,10,15,30,45).future?
- assert_equal true, Time.local(2005,2,10,15,30,46).future?
- assert_equal false, Time.utc(2005,2,10,20,30,44).future?
- assert_equal false, Time.utc(2005,2,10,20,30,45).future?
- assert_equal true, Time.utc(2005,2,10,20,30,46).future?
+ with_env_tz "US/Eastern" do
+ Time.stub(:current, Time.local(2005, 2, 10, 15, 30, 45)) do
+ assert_equal false, Time.local(2005, 2, 10, 15, 30, 44).future?
+ assert_equal false, Time.local(2005, 2, 10, 15, 30, 45).future?
+ assert_equal true, Time.local(2005, 2, 10, 15, 30, 46).future?
+ assert_equal false, Time.utc(2005, 2, 10, 20, 30, 44).future?
+ assert_equal false, Time.utc(2005, 2, 10, 20, 30, 45).future?
+ assert_equal true, Time.utc(2005, 2, 10, 20, 30, 46).future?
end
end
end
def test_future_with_time_current_as_time_with_zone
- with_env_tz 'US/Eastern' do
- twz = Time.utc(2005,2,10,15,30,45).in_time_zone('Central Time (US & Canada)')
+ with_env_tz "US/Eastern" do
+ twz = Time.utc(2005, 2, 10, 15, 30, 45).in_time_zone("Central Time (US & Canada)")
Time.stub(:current, twz) do
- assert_equal false, Time.local(2005,2,10,10,30,44).future?
- assert_equal false, Time.local(2005,2,10,10,30,45).future?
- assert_equal true, Time.local(2005,2,10,10,30,46).future?
- assert_equal false, Time.utc(2005,2,10,15,30,44).future?
- assert_equal false, Time.utc(2005,2,10,15,30,45).future?
- assert_equal true, Time.utc(2005,2,10,15,30,46).future?
+ assert_equal false, Time.local(2005, 2, 10, 10, 30, 44).future?
+ assert_equal false, Time.local(2005, 2, 10, 10, 30, 45).future?
+ assert_equal true, Time.local(2005, 2, 10, 10, 30, 46).future?
+ assert_equal false, Time.utc(2005, 2, 10, 15, 30, 44).future?
+ assert_equal false, Time.utc(2005, 2, 10, 15, 30, 45).future?
+ assert_equal true, Time.utc(2005, 2, 10, 15, 30, 46).future?
end
end
end
def test_acts_like_time
- assert Time.new.acts_like_time?
+ assert_predicate Time.new, :acts_like_time?
end
def test_formatted_offset_with_utc
- assert_equal '+00:00', Time.utc(2000).formatted_offset
- assert_equal '+0000', Time.utc(2000).formatted_offset(false)
- assert_equal 'UTC', Time.utc(2000).formatted_offset(true, 'UTC')
+ assert_equal "+00:00", Time.utc(2000).formatted_offset
+ assert_equal "+0000", Time.utc(2000).formatted_offset(false)
+ assert_equal "UTC", Time.utc(2000).formatted_offset(true, "UTC")
end
def test_formatted_offset_with_local
- with_env_tz 'US/Eastern' do
- assert_equal '-05:00', Time.local(2000).formatted_offset
- assert_equal '-0500', Time.local(2000).formatted_offset(false)
- assert_equal '-04:00', Time.local(2000, 7).formatted_offset
- assert_equal '-0400', Time.local(2000, 7).formatted_offset(false)
+ with_env_tz "US/Eastern" do
+ assert_equal "-05:00", Time.local(2000).formatted_offset
+ assert_equal "-0500", Time.local(2000).formatted_offset(false)
+ assert_equal "-04:00", Time.local(2000, 7).formatted_offset
+ assert_equal "-0400", Time.local(2000, 7).formatted_offset(false)
end
end
def test_compare_with_time
- assert_equal 1, Time.utc(2000) <=> Time.utc(1999, 12, 31, 23, 59, 59, 999)
- assert_equal 0, Time.utc(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0)
+ assert_equal 1, Time.utc(2000) <=> Time.utc(1999, 12, 31, 23, 59, 59, 999)
+ assert_equal 0, Time.utc(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0)
assert_equal(-1, Time.utc(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0, 001))
end
def test_compare_with_datetime
- assert_equal 1, Time.utc(2000) <=> DateTime.civil(1999, 12, 31, 23, 59, 59)
- assert_equal 0, Time.utc(2000) <=> DateTime.civil(2000, 1, 1, 0, 0, 0)
+ assert_equal 1, Time.utc(2000) <=> DateTime.civil(1999, 12, 31, 23, 59, 59)
+ assert_equal 0, Time.utc(2000) <=> DateTime.civil(2000, 1, 1, 0, 0, 0)
assert_equal(-1, Time.utc(2000) <=> DateTime.civil(2000, 1, 1, 0, 0, 1))
end
def test_compare_with_time_with_zone
- assert_equal 1, Time.utc(2000) <=> ActiveSupport::TimeWithZone.new( Time.utc(1999, 12, 31, 23, 59, 59), ActiveSupport::TimeZone['UTC'] )
- assert_equal 0, Time.utc(2000) <=> ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone['UTC'] )
- assert_equal(-1, Time.utc(2000) <=> ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1, 0, 0, 1), ActiveSupport::TimeZone['UTC'] ))
+ assert_equal 1, Time.utc(2000) <=> ActiveSupport::TimeWithZone.new(Time.utc(1999, 12, 31, 23, 59, 59), ActiveSupport::TimeZone["UTC"])
+ assert_equal 0, Time.utc(2000) <=> ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone["UTC"])
+ assert_equal(-1, Time.utc(2000) <=> ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 1), ActiveSupport::TimeZone["UTC"]))
end
def test_compare_with_string
- assert_equal 1, Time.utc(2000) <=> Time.utc(1999, 12, 31, 23, 59, 59, 999).to_s
- assert_equal 0, Time.utc(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0).to_s
- assert_equal( -1, Time.utc(2000) <=> Time.utc(2000, 1, 1, 0, 0, 1, 0).to_s)
- assert_equal nil, Time.utc(2000) <=> 'Invalid as Time'
+ assert_equal 1, Time.utc(2000) <=> Time.utc(1999, 12, 31, 23, 59, 59, 999).to_s
+ assert_equal 0, Time.utc(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0).to_s
+ assert_equal(-1, Time.utc(2000) <=> Time.utc(2000, 1, 1, 0, 0, 1, 0).to_s)
+ assert_nil Time.utc(2000) <=> "Invalid as Time"
end
def test_at_with_datetime
@@ -752,42 +792,42 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
end
def test_at_with_datetime_returns_local_time
- with_env_tz 'US/Eastern' do
- dt = DateTime.civil(2000, 1, 1, 0, 0, 0, '+0')
+ with_env_tz "US/Eastern" do
+ dt = DateTime.civil(2000, 1, 1, 0, 0, 0, "+0")
assert_equal Time.local(1999, 12, 31, 19, 0, 0), Time.at(dt)
- assert_equal 'EST', Time.at(dt).zone
+ assert_equal "EST", Time.at(dt).zone
assert_equal(-18000, Time.at(dt).utc_offset)
# Daylight savings
- dt = DateTime.civil(2000, 7, 1, 1, 0, 0, '+1')
+ dt = DateTime.civil(2000, 7, 1, 1, 0, 0, "+1")
assert_equal Time.local(2000, 6, 30, 20, 0, 0), Time.at(dt)
- assert_equal 'EDT', Time.at(dt).zone
+ assert_equal "EDT", Time.at(dt).zone
assert_equal(-14400, Time.at(dt).utc_offset)
end
end
def test_at_with_time_with_zone
- assert_equal Time.utc(2000, 1, 1, 0, 0, 0), Time.at(ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone['UTC']))
+ assert_equal Time.utc(2000, 1, 1, 0, 0, 0), Time.at(ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone["UTC"]))
# Only test this if the underlying Time.at raises a TypeError
begin
Time.at_without_coercion(Time.now, 0)
rescue TypeError
- assert_raise(TypeError) { assert_equal(Time.utc(2000, 1, 1, 0, 0, 0), Time.at(ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone['UTC']), 0)) }
+ assert_raise(TypeError) { assert_equal(Time.utc(2000, 1, 1, 0, 0, 0), Time.at(ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone["UTC"]), 0)) }
end
end
def test_at_with_time_with_zone_returns_local_time
- with_env_tz 'US/Eastern' do
- twz = ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone['London'])
+ with_env_tz "US/Eastern" do
+ twz = ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone["London"])
assert_equal Time.local(1999, 12, 31, 19, 0, 0), Time.at(twz)
- assert_equal 'EST', Time.at(twz).zone
+ assert_equal "EST", Time.at(twz).zone
assert_equal(-18000, Time.at(twz).utc_offset)
# Daylight savings
- twz = ActiveSupport::TimeWithZone.new(Time.utc(2000, 7, 1, 0, 0, 0), ActiveSupport::TimeZone['London'])
+ twz = ActiveSupport::TimeWithZone.new(Time.utc(2000, 7, 1, 0, 0, 0), ActiveSupport::TimeZone["London"])
assert_equal Time.local(2000, 6, 30, 20, 0, 0), Time.at(twz)
- assert_equal 'EDT', Time.at(twz).zone
+ assert_equal "EDT", Time.at(twz).zone
assert_equal(-14400, Time.at(twz).utc_offset)
end
end
@@ -797,121 +837,152 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
end
def test_at_with_utc_time
- with_env_tz 'US/Eastern' do
+ with_env_tz "US/Eastern" do
assert_equal Time.utc(2000), Time.at(Time.utc(2000))
- assert_equal 'UTC', Time.at(Time.utc(2000)).zone
+ assert_equal "UTC", Time.at(Time.utc(2000)).zone
assert_equal(0, Time.at(Time.utc(2000)).utc_offset)
end
end
def test_at_with_local_time
- with_env_tz 'US/Eastern' do
+ with_env_tz "US/Eastern" do
assert_equal Time.local(2000), Time.at(Time.local(2000))
- assert_equal 'EST', Time.at(Time.local(2000)).zone
+ assert_equal "EST", Time.at(Time.local(2000)).zone
assert_equal(-18000, Time.at(Time.local(2000)).utc_offset)
assert_equal Time.local(2000, 7, 1), Time.at(Time.local(2000, 7, 1))
- assert_equal 'EDT', Time.at(Time.local(2000, 7, 1)).zone
+ assert_equal "EDT", Time.at(Time.local(2000, 7, 1)).zone
assert_equal(-14400, Time.at(Time.local(2000, 7, 1)).utc_offset)
end
end
def test_eql?
- assert_equal true, Time.utc(2000).eql?( ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone['UTC']) )
- assert_equal true, Time.utc(2000).eql?( ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["Hawaii"]) )
- assert_equal false,Time.utc(2000, 1, 1, 0, 0, 1).eql?( ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone['UTC']) )
+ assert_equal true, Time.utc(2000).eql?(ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["UTC"]))
+ assert_equal true, Time.utc(2000).eql?(ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["Hawaii"]))
+ assert_equal false, Time.utc(2000, 1, 1, 0, 0, 1).eql?(ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["UTC"]))
end
def test_minus_with_time_with_zone
- assert_equal 86_400.0, Time.utc(2000, 1, 2) - ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1), ActiveSupport::TimeZone['UTC'] )
+ assert_equal 86_400.0, Time.utc(2000, 1, 2) - ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1), ActiveSupport::TimeZone["UTC"])
end
def test_minus_with_datetime
- assert_equal 86_400.0, Time.utc(2000, 1, 2) - DateTime.civil(2000, 1, 1)
+ assert_equal 86_400.0, Time.utc(2000, 1, 2) - DateTime.civil(2000, 1, 1)
end
def test_time_created_with_local_constructor_cannot_represent_times_during_hour_skipped_by_dst
- with_env_tz 'US/Eastern' do
+ with_env_tz "US/Eastern" do
# On Apr 2 2006 at 2:00AM in US, clocks were moved forward to 3:00AM.
# Therefore, 2AM EST doesn't exist for this date; Time.local fails over to 3:00AM EDT
assert_equal Time.local(2006, 4, 2, 3), Time.local(2006, 4, 2, 2)
- assert Time.local(2006, 4, 2, 2).dst?
+ assert_predicate Time.local(2006, 4, 2, 2), :dst?
end
end
def test_case_equality
assert Time === Time.utc(2000)
- assert Time === ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone['UTC'])
+ assert Time === ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["UTC"])
assert Time === Class.new(Time).utc(2000)
assert_equal false, Time === DateTime.civil(2000)
assert_equal false, Class.new(Time) === Time.utc(2000)
- assert_equal false, Class.new(Time) === ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone['UTC'])
+ assert_equal false, Class.new(Time) === ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["UTC"])
end
def test_all_day
- assert_equal Time.local(2011,6,7,0,0,0)..Time.local(2011,6,7,23,59,59,Rational(999999999, 1000)), Time.local(2011,6,7,10,10,10).all_day
+ assert_equal Time.local(2011, 6, 7, 0, 0, 0)..Time.local(2011, 6, 7, 23, 59, 59, Rational(999999999, 1000)), Time.local(2011, 6, 7, 10, 10, 10).all_day
end
def test_all_day_with_timezone
- beginning_of_day = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Hawaii"], Time.local(2011,6,7,0,0,0))
- end_of_day = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Hawaii"], Time.local(2011,6,7,23,59,59,Rational(999999999, 1000)))
+ beginning_of_day = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Hawaii"], Time.local(2011, 6, 7, 0, 0, 0))
+ end_of_day = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Hawaii"], Time.local(2011, 6, 7, 23, 59, 59, Rational(999999999, 1000)))
- assert_equal beginning_of_day, ActiveSupport::TimeWithZone.new(Time.local(2011,6,7,10,10,10), ActiveSupport::TimeZone["Hawaii"]).all_day.begin
- assert_equal end_of_day, ActiveSupport::TimeWithZone.new(Time.local(2011,6,7,10,10,10), ActiveSupport::TimeZone["Hawaii"]).all_day.end
+ assert_equal beginning_of_day, ActiveSupport::TimeWithZone.new(Time.local(2011, 6, 7, 10, 10, 10), ActiveSupport::TimeZone["Hawaii"]).all_day.begin
+ assert_equal end_of_day, ActiveSupport::TimeWithZone.new(Time.local(2011, 6, 7, 10, 10, 10), ActiveSupport::TimeZone["Hawaii"]).all_day.end
end
def test_all_week
- assert_equal Time.local(2011,6,6,0,0,0)..Time.local(2011,6,12,23,59,59,Rational(999999999, 1000)), Time.local(2011,6,7,10,10,10).all_week
- assert_equal Time.local(2011,6,5,0,0,0)..Time.local(2011,6,11,23,59,59,Rational(999999999, 1000)), Time.local(2011,6,7,10,10,10).all_week(:sunday)
+ assert_equal Time.local(2011, 6, 6, 0, 0, 0)..Time.local(2011, 6, 12, 23, 59, 59, Rational(999999999, 1000)), Time.local(2011, 6, 7, 10, 10, 10).all_week
+ assert_equal Time.local(2011, 6, 5, 0, 0, 0)..Time.local(2011, 6, 11, 23, 59, 59, Rational(999999999, 1000)), Time.local(2011, 6, 7, 10, 10, 10).all_week(:sunday)
end
def test_all_month
- assert_equal Time.local(2011,6,1,0,0,0)..Time.local(2011,6,30,23,59,59,Rational(999999999, 1000)), Time.local(2011,6,7,10,10,10).all_month
+ assert_equal Time.local(2011, 6, 1, 0, 0, 0)..Time.local(2011, 6, 30, 23, 59, 59, Rational(999999999, 1000)), Time.local(2011, 6, 7, 10, 10, 10).all_month
end
def test_all_quarter
- assert_equal Time.local(2011,4,1,0,0,0)..Time.local(2011,6,30,23,59,59,Rational(999999999, 1000)), Time.local(2011,6,7,10,10,10).all_quarter
+ assert_equal Time.local(2011, 4, 1, 0, 0, 0)..Time.local(2011, 6, 30, 23, 59, 59, Rational(999999999, 1000)), Time.local(2011, 6, 7, 10, 10, 10).all_quarter
end
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
+ 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
- def test_marshaling_with_utc_instance
+ def test_marshalling_with_utc_instance
t = Time.utc(2000)
- unmarshaled = Marshal.load(Marshal.dump(t))
- assert_equal "UTC", unmarshaled.zone
- assert_equal t, unmarshaled
+ unmarshalled = Marshal.load(Marshal.dump(t))
+ assert_equal "UTC", unmarshalled.zone
+ assert_equal t, unmarshalled
end
- def test_marshaling_with_local_instance
+ def test_marshalling_with_local_instance
t = Time.local(2000)
- unmarshaled = Marshal.load(Marshal.dump(t))
- assert_equal t.zone, unmarshaled.zone
- assert_equal t, unmarshaled
+ unmarshalled = Marshal.load(Marshal.dump(t))
+ assert_equal t.zone, unmarshalled.zone
+ assert_equal t, unmarshalled
end
- def test_marshaling_with_frozen_utc_instance
+ def test_marshalling_with_frozen_utc_instance
t = Time.utc(2000).freeze
- unmarshaled = Marshal.load(Marshal.dump(t))
- assert_equal "UTC", unmarshaled.zone
- assert_equal t, unmarshaled
+ unmarshalled = Marshal.load(Marshal.dump(t))
+ assert_equal "UTC", unmarshalled.zone
+ assert_equal t, unmarshalled
end
- def test_marshaling_with_frozen_local_instance
+ def test_marshalling_with_frozen_local_instance
t = Time.local(2000).freeze
- unmarshaled = Marshal.load(Marshal.dump(t))
- assert_equal t.zone, unmarshaled.zone
- assert_equal t, unmarshaled
+ unmarshalled = Marshal.load(Marshal.dump(t))
+ assert_equal t.zone, unmarshalled.zone
+ assert_equal t, unmarshalled
end
def test_marshalling_preserves_fractional_seconds
- t = Time.parse('00:00:00.500')
- unmarshaled = Marshal.load(Marshal.dump(t))
- assert_equal t.to_f, unmarshaled.to_f
- assert_equal t, unmarshaled
+ t = Time.parse("00:00:00.500")
+ unmarshalled = Marshal.load(Marshal.dump(t))
+ assert_equal t.to_f, unmarshalled.to_f
+ assert_equal t, unmarshalled
end
def test_last_quarter_on_31st
diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb
index 477a42114b..f6e836e446 100644
--- a/activesupport/test/core_ext/time_with_zone_test.rb
+++ b/activesupport/test/core_ext/time_with_zone_test.rb
@@ -1,20 +1,24 @@
-require 'abstract_unit'
-require 'active_support/time'
-require 'time_zone_test_helpers'
-require 'active_support/core_ext/string/strip'
-require 'yaml'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/time"
+require "time_zone_test_helpers"
+require "yaml"
class TimeWithZoneTest < ActiveSupport::TestCase
include TimeZoneTestHelpers
def setup
@utc = Time.utc(2000, 1, 1, 0)
- @time_zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
+ @time_zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
@twz = ActiveSupport::TimeWithZone.new(@utc, @time_zone)
+ @dt_twz = ActiveSupport::TimeWithZone.new(@utc.to_datetime, @time_zone)
end
def test_utc
assert_equal @utc, @twz.utc
+ assert_instance_of Time, @twz.utc
+ assert_instance_of Time, @dt_twz.utc
end
def test_time
@@ -26,37 +30,60 @@ class TimeWithZoneTest < ActiveSupport::TestCase
end
def test_in_time_zone
- Time.use_zone 'Alaska' do
- assert_equal ActiveSupport::TimeWithZone.new(@utc, ActiveSupport::TimeZone['Alaska']), @twz.in_time_zone
+ Time.use_zone "Alaska" do
+ assert_equal ActiveSupport::TimeWithZone.new(@utc, ActiveSupport::TimeZone["Alaska"]), @twz.in_time_zone
end
end
def test_in_time_zone_with_argument
- assert_equal ActiveSupport::TimeWithZone.new(@utc, ActiveSupport::TimeZone['Alaska']), @twz.in_time_zone('Alaska')
+ assert_equal ActiveSupport::TimeWithZone.new(@utc, ActiveSupport::TimeZone["Alaska"]), @twz.in_time_zone("Alaska")
end
def test_in_time_zone_with_new_zone_equal_to_old_zone_does_not_create_new_object
- assert_equal @twz.object_id, @twz.in_time_zone(ActiveSupport::TimeZone['Eastern Time (US & Canada)']).object_id
+ assert_equal @twz.object_id, @twz.in_time_zone(ActiveSupport::TimeZone["Eastern Time (US & Canada)"]).object_id
end
def test_in_time_zone_with_bad_argument
- assert_raise(ArgumentError) { @twz.in_time_zone('No such timezone exists') }
+ assert_raise(ArgumentError) { @twz.in_time_zone("No such timezone exists") }
assert_raise(ArgumentError) { @twz.in_time_zone(-15.hours) }
assert_raise(ArgumentError) { @twz.in_time_zone(Object.new) }
end
+ def test_in_time_zone_with_ambiguous_time
+ with_env_tz "Europe/Moscow" do
+ assert_equal Time.utc(2014, 10, 25, 22, 0, 0), Time.local(2014, 10, 26, 1, 0, 0).in_time_zone("Moscow")
+ end
+ end
+
def test_localtime
assert_equal @twz.localtime, @twz.utc.getlocal
+ assert_instance_of Time, @twz.localtime
+ assert_instance_of Time, @dt_twz.localtime
end
def test_utc?
assert_equal false, @twz.utc?
- assert_equal true, ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone['UTC']).utc?
+
+ assert_equal true, ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["UTC"]).utc?
+ assert_equal true, ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["Etc/UTC"]).utc?
+ assert_equal true, ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["Universal"]).utc?
+ assert_equal true, ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["UCT"]).utc?
+ assert_equal true, ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["Etc/UCT"]).utc?
+ assert_equal true, ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["Etc/Universal"]).utc?
+
+ assert_equal false, ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["Africa/Abidjan"]).utc?
+ assert_equal false, ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["Africa/Banjul"]).utc?
+ assert_equal false, ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["Africa/Freetown"]).utc?
+ assert_equal false, ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["GMT"]).utc?
+ assert_equal false, ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["GMT0"]).utc?
+ assert_equal false, ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["Greenwich"]).utc?
+ assert_equal false, ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["Iceland"]).utc?
+ assert_equal false, ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["Africa/Monrovia"]).utc?
end
def test_formatted_offset
- assert_equal '-05:00', @twz.formatted_offset
- assert_equal '-04:00', ActiveSupport::TimeWithZone.new(Time.utc(2000, 6), @time_zone).formatted_offset #dst
+ assert_equal "-05:00", @twz.formatted_offset
+ assert_equal "-04:00", ActiveSupport::TimeWithZone.new(Time.utc(2000, 6), @time_zone).formatted_offset # dst
end
def test_dst?
@@ -65,12 +92,12 @@ class TimeWithZoneTest < ActiveSupport::TestCase
end
def test_zone
- assert_equal 'EST', @twz.zone
- assert_equal 'EDT', ActiveSupport::TimeWithZone.new(Time.utc(2000, 6), @time_zone).zone #dst
+ assert_equal "EST", @twz.zone
+ assert_equal "EDT", ActiveSupport::TimeWithZone.new(Time.utc(2000, 6), @time_zone).zone # dst
end
def test_nsec
- local = Time.local(2011,6,7,23,59,59,Rational(999999999, 1000))
+ local = Time.local(2011, 6, 7, 23, 59, 59, Rational(999999999, 1000))
with_zone = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Hawaii"], local)
assert_equal local.nsec, with_zone.nsec
@@ -78,28 +105,28 @@ class TimeWithZoneTest < ActiveSupport::TestCase
end
def test_strftime
- assert_equal '1999-12-31 19:00:00 EST -0500', @twz.strftime('%Y-%m-%d %H:%M:%S %Z %z')
+ assert_equal "1999-12-31 19:00:00 EST -0500", @twz.strftime("%Y-%m-%d %H:%M:%S %Z %z")
end
def test_strftime_with_escaping
- assert_equal '%Z %z', @twz.strftime('%%Z %%z')
- assert_equal '%EST %-0500', @twz.strftime('%%%Z %%%z')
+ assert_equal "%Z %z", @twz.strftime("%%Z %%z")
+ assert_equal "%EST %-0500", @twz.strftime("%%%Z %%%z")
end
def test_inspect
- assert_equal 'Fri, 31 Dec 1999 19:00:00 EST -05:00', @twz.inspect
+ assert_equal "Fri, 31 Dec 1999 19:00:00 EST -05:00", @twz.inspect
end
def test_to_s
- assert_equal '1999-12-31 19:00:00 -0500', @twz.to_s
+ assert_equal "1999-12-31 19:00:00 -0500", @twz.to_s
end
def test_to_formatted_s
- assert_equal '1999-12-31 19:00:00 -0500', @twz.to_formatted_s
+ assert_equal "1999-12-31 19:00:00 -0500", @twz.to_formatted_s
end
def test_to_s_db
- assert_equal '2000-01-01 00:00:00', @twz.to_s(:db)
+ assert_equal "2000-01-01 00:00:00", @twz.to_s(:db)
end
def test_xmlschema
@@ -110,22 +137,32 @@ class TimeWithZoneTest < ActiveSupport::TestCase
@twz += 0.1234560001 # advance the time by a fraction of a second
assert_equal "1999-12-31T19:00:00.123-05:00", @twz.xmlschema(3)
assert_equal "1999-12-31T19:00:00.123456-05:00", @twz.xmlschema(6)
- assert_equal "1999-12-31T19:00:00.123456-05:00", @twz.xmlschema(12)
+ assert_equal "1999-12-31T19:00:00.123456000100-05:00", @twz.xmlschema(12)
end
def test_xmlschema_with_fractional_seconds_lower_than_hundred_thousand
@twz += 0.001234 # advance the time by a fraction
assert_equal "1999-12-31T19:00:00.001-05:00", @twz.xmlschema(3)
assert_equal "1999-12-31T19:00:00.001234-05:00", @twz.xmlschema(6)
- assert_equal "1999-12-31T19:00:00.001234-05:00", @twz.xmlschema(12)
+ assert_equal "1999-12-31T19:00:00.001234000000-05:00", @twz.xmlschema(12)
end
def test_xmlschema_with_nil_fractional_seconds
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
+ yaml = <<~EOF
--- !ruby/object:ActiveSupport::TimeWithZone
utc: 2000-01-01 00:00:00.000000000 Z
zone: !ruby/object:ActiveSupport::TimeZone
@@ -137,7 +174,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase
end
def test_ruby_to_yaml
- yaml = <<-EOF.strip_heredoc
+ yaml = <<~EOF
---
twz: !ruby/object:ActiveSupport::TimeWithZone
utc: 2000-01-01 00:00:00.000000000 Z
@@ -146,11 +183,11 @@ class TimeWithZoneTest < ActiveSupport::TestCase
time: 1999-12-31 19:00:00.000000000 Z
EOF
- assert_equal(yaml, { 'twz' => @twz }.to_yaml)
+ assert_equal(yaml, { "twz" => @twz }.to_yaml)
end
def test_yaml_load
- yaml = <<-EOF.strip_heredoc
+ yaml = <<~EOF
--- !ruby/object:ActiveSupport::TimeWithZone
utc: 2000-01-01 00:00:00.000000000 Z
zone: !ruby/object:ActiveSupport::TimeZone
@@ -162,7 +199,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase
end
def test_ruby_yaml_load
- yaml = <<-EOF.strip_heredoc
+ yaml = <<~EOF
---
twz: !ruby/object:ActiveSupport::TimeWithZone
utc: 2000-01-01 00:00:00.000000000 Z
@@ -171,11 +208,11 @@ class TimeWithZoneTest < ActiveSupport::TestCase
time: 1999-12-31 19:00:00.000000000 Z
EOF
- assert_equal({ 'twz' => @twz }, YAML.load(yaml))
+ assert_equal({ "twz" => @twz }, YAML.load(yaml))
end
def test_httpdate
- assert_equal 'Sat, 01 Jan 2000 00:00:00 GMT', @twz.httpdate
+ assert_equal "Sat, 01 Jan 2000 00:00:00 GMT", @twz.httpdate
end
def test_rfc2822
@@ -183,80 +220,98 @@ class TimeWithZoneTest < ActiveSupport::TestCase
end
def test_compare_with_time
- assert_equal 1, @twz <=> Time.utc(1999, 12, 31, 23, 59, 59)
- assert_equal 0, @twz <=> Time.utc(2000, 1, 1, 0, 0, 0)
+ assert_equal 1, @twz <=> Time.utc(1999, 12, 31, 23, 59, 59)
+ assert_equal 0, @twz <=> Time.utc(2000, 1, 1, 0, 0, 0)
assert_equal(-1, @twz <=> Time.utc(2000, 1, 1, 0, 0, 1))
end
def test_compare_with_datetime
- assert_equal 1, @twz <=> DateTime.civil(1999, 12, 31, 23, 59, 59)
- assert_equal 0, @twz <=> DateTime.civil(2000, 1, 1, 0, 0, 0)
+ assert_equal 1, @twz <=> DateTime.civil(1999, 12, 31, 23, 59, 59)
+ assert_equal 0, @twz <=> DateTime.civil(2000, 1, 1, 0, 0, 0)
assert_equal(-1, @twz <=> DateTime.civil(2000, 1, 1, 0, 0, 1))
end
def test_compare_with_time_with_zone
- assert_equal 1, @twz <=> ActiveSupport::TimeWithZone.new( Time.utc(1999, 12, 31, 23, 59, 59), ActiveSupport::TimeZone['UTC'] )
- assert_equal 0, @twz <=> ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone['UTC'] )
- assert_equal(-1, @twz <=> ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1, 0, 0, 1), ActiveSupport::TimeZone['UTC'] ))
+ assert_equal 1, @twz <=> ActiveSupport::TimeWithZone.new(Time.utc(1999, 12, 31, 23, 59, 59), ActiveSupport::TimeZone["UTC"])
+ assert_equal 0, @twz <=> ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone["UTC"])
+ assert_equal(-1, @twz <=> ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 1), ActiveSupport::TimeZone["UTC"]))
end
def test_between?
- assert @twz.between?(Time.utc(1999,12,31,23,59,59), Time.utc(2000,1,1,0,0,1))
- assert_equal false, @twz.between?(Time.utc(2000,1,1,0,0,1), Time.utc(2000,1,1,0,0,2))
+ assert @twz.between?(Time.utc(1999, 12, 31, 23, 59, 59), Time.utc(2000, 1, 1, 0, 0, 1))
+ assert_equal false, @twz.between?(Time.utc(2000, 1, 1, 0, 0, 1), Time.utc(2000, 1, 1, 0, 0, 2))
end
def test_today
Date.stub(:current, Date.new(2000, 1, 1)) do
- assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.utc(1999,12,31,23,59,59) ).today?
- assert_equal true, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.utc(2000,1,1,0) ).today?
- assert_equal true, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.utc(2000,1,1,23,59,59) ).today?
- assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.utc(2000,1,2,0) ).today?
+ assert_equal false, ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(1999, 12, 31, 23, 59, 59)).today?
+ assert_equal true, ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2000, 1, 1, 0)).today?
+ assert_equal true, ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2000, 1, 1, 23, 59, 59)).today?
+ assert_equal false, ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2000, 1, 2, 0)).today?
end
end
def test_past_with_time_current_as_time_local
- with_env_tz 'US/Eastern' do
- Time.stub(:current, Time.local(2005,2,10,15,30,45)) do
- assert_equal true, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,44)).past?
- assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,45)).past?
- assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,46)).past?
+ with_env_tz "US/Eastern" do
+ Time.stub(:current, Time.local(2005, 2, 10, 15, 30, 45)) do
+ assert_equal true, ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.local(2005, 2, 10, 15, 30, 44)).past?
+ assert_equal false, ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.local(2005, 2, 10, 15, 30, 45)).past?
+ assert_equal false, ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.local(2005, 2, 10, 15, 30, 46)).past?
end
end
end
def test_past_with_time_current_as_time_with_zone
- twz = ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,45) )
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.local(2005, 2, 10, 15, 30, 45))
Time.stub(:current, twz) do
- assert_equal true, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,44)).past?
- assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,45)).past?
- assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,46)).past?
+ assert_equal true, ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.local(2005, 2, 10, 15, 30, 44)).past?
+ assert_equal false, ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.local(2005, 2, 10, 15, 30, 45)).past?
+ assert_equal false, ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.local(2005, 2, 10, 15, 30, 46)).past?
end
end
def test_future_with_time_current_as_time_local
- with_env_tz 'US/Eastern' do
- Time.stub(:current, Time.local(2005,2,10,15,30,45)) do
- assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,44)).future?
- assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,45)).future?
- assert_equal true, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,46)).future?
+ with_env_tz "US/Eastern" do
+ Time.stub(:current, Time.local(2005, 2, 10, 15, 30, 45)) do
+ assert_equal false, ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.local(2005, 2, 10, 15, 30, 44)).future?
+ assert_equal false, ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.local(2005, 2, 10, 15, 30, 45)).future?
+ assert_equal true, ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.local(2005, 2, 10, 15, 30, 46)).future?
end
end
end
def test_future_with_time_current_as_time_with_zone
- twz = ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,45) )
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.local(2005, 2, 10, 15, 30, 45))
Time.stub(:current, twz) do
- assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,44)).future?
- assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,45)).future?
- assert_equal true, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,46)).future?
+ assert_equal false, ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.local(2005, 2, 10, 15, 30, 44)).future?
+ assert_equal false, ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.local(2005, 2, 10, 15, 30, 45)).future?
+ assert_equal true, ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.local(2005, 2, 10, 15, 30, 46)).future?
end
end
+ def test_before
+ twz = ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 12, 0, 0), @time_zone)
+ assert_equal false, twz.before?(ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 11, 59, 59), @time_zone))
+ assert_equal false, twz.before?(ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 12, 0, 0), @time_zone))
+ assert_equal true, twz.before?(ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 12, 00, 1), @time_zone))
+ end
+
+ def test_after
+ twz = ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 12, 0, 0), @time_zone)
+ assert_equal true, twz.after?(ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 11, 59, 59), @time_zone))
+ assert_equal false, twz.after?(ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 12, 0, 0), @time_zone))
+ assert_equal false, twz.after?(ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 12, 00, 1), @time_zone))
+ end
+
def test_eql?
+ assert_equal true, @twz.eql?(@twz.dup)
assert_equal true, @twz.eql?(Time.utc(2000))
- assert_equal true, @twz.eql?( ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["Hawaii"]) )
- assert_equal false, @twz.eql?( Time.utc(2000, 1, 1, 0, 0, 1) )
- assert_equal false, @twz.eql?( DateTime.civil(1999, 12, 31, 23, 59, 59) )
+ assert_equal true, @twz.eql?(ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["Hawaii"]))
+ assert_equal false, @twz.eql?(Time.utc(2000, 1, 1, 0, 0, 1))
+ assert_equal false, @twz.eql?(DateTime.civil(1999, 12, 31, 23, 59, 59))
+
+ other_twz = ActiveSupport::TimeWithZone.new(DateTime.now.utc, @time_zone)
+ assert_equal true, other_twz.eql?(other_twz.dup)
end
def test_hash
@@ -265,117 +320,117 @@ class TimeWithZoneTest < ActiveSupport::TestCase
end
def test_plus_with_integer
- assert_equal Time.utc(1999, 12, 31, 19, 0 ,5), (@twz + 5).time
+ assert_equal Time.utc(1999, 12, 31, 19, 0, 5), (@twz + 5).time
end
def test_plus_with_integer_when_self_wraps_datetime
datetime = DateTime.civil(2000, 1, 1, 0)
twz = ActiveSupport::TimeWithZone.new(datetime, @time_zone)
- assert_equal DateTime.civil(1999, 12, 31, 19, 0 ,5), (twz + 5).time
+ assert_equal DateTime.civil(1999, 12, 31, 19, 0, 5), (twz + 5).time
end
def test_plus_when_crossing_time_class_limit
twz = ActiveSupport::TimeWithZone.new(Time.utc(2038, 1, 19), @time_zone)
- assert_equal [0, 0, 19, 19, 1, 2038], (twz + 86_400).to_a[0,6]
+ assert_equal [0, 0, 19, 19, 1, 2038], (twz + 86_400).to_a[0, 6]
end
def test_plus_with_duration
- assert_equal Time.utc(2000, 1, 5, 19, 0 ,0), (@twz + 5.days).time
+ assert_equal Time.utc(2000, 1, 5, 19, 0, 0), (@twz + 5.days).time
end
def test_minus_with_integer
- assert_equal Time.utc(1999, 12, 31, 18, 59 ,55), (@twz - 5).time
+ assert_equal Time.utc(1999, 12, 31, 18, 59, 55), (@twz - 5).time
end
def test_minus_with_integer_when_self_wraps_datetime
datetime = DateTime.civil(2000, 1, 1, 0)
twz = ActiveSupport::TimeWithZone.new(datetime, @time_zone)
- assert_equal DateTime.civil(1999, 12, 31, 18, 59 ,55), (twz - 5).time
+ assert_equal DateTime.civil(1999, 12, 31, 18, 59, 55), (twz - 5).time
end
def test_minus_with_duration
- assert_equal Time.utc(1999, 12, 26, 19, 0 ,0), (@twz - 5.days).time
+ assert_equal Time.utc(1999, 12, 26, 19, 0, 0), (@twz - 5.days).time
end
def test_minus_with_time
- assert_equal 86_400.0, ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 2), ActiveSupport::TimeZone['UTC'] ) - Time.utc(2000, 1, 1)
- assert_equal 86_400.0, ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 2), ActiveSupport::TimeZone['Hawaii'] ) - Time.utc(2000, 1, 1)
+ assert_equal 86_400.0, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2), ActiveSupport::TimeZone["UTC"]) - Time.utc(2000, 1, 1)
+ assert_equal 86_400.0, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2), ActiveSupport::TimeZone["Hawaii"]) - Time.utc(2000, 1, 1)
end
def test_minus_with_time_precision
- assert_equal 86_399.999999998, ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 2, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone['UTC'] ) - Time.utc(2000, 1, 2, 0, 0, 0, Rational(1, 1000))
- assert_equal 86_399.999999998, ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 2, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone['Hawaii'] ) - Time.utc(2000, 1, 2, 0, 0, 0, Rational(1, 1000))
+ assert_equal 86_399.999999998, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone["UTC"]) - Time.utc(2000, 1, 2, 0, 0, 0, Rational(1, 1000))
+ assert_equal 86_399.999999998, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone["Hawaii"]) - Time.utc(2000, 1, 2, 0, 0, 0, Rational(1, 1000))
end
def test_minus_with_time_with_zone
- twz1 = ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1), ActiveSupport::TimeZone['UTC'] )
- twz2 = ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 2), ActiveSupport::TimeZone['UTC'] )
- assert_equal 86_400.0, twz2 - twz1
+ twz1 = ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1), ActiveSupport::TimeZone["UTC"])
+ twz2 = ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2), ActiveSupport::TimeZone["UTC"])
+ assert_equal 86_400.0, twz2 - twz1
end
def test_minus_with_time_with_zone_precision
- twz1 = ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1, 0, 0, 0, Rational(1, 1000)), ActiveSupport::TimeZone['UTC'] )
- twz2 = ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone['UTC'] )
- assert_equal 86_399.999999998, twz2 - twz1
+ twz1 = ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0, Rational(1, 1000)), ActiveSupport::TimeZone["UTC"])
+ twz2 = ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone["UTC"])
+ assert_equal 86_399.999999998, twz2 - twz1
end
def test_minus_with_datetime
- assert_equal 86_400.0, ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 2), ActiveSupport::TimeZone['UTC'] ) - DateTime.civil(2000, 1, 1)
+ assert_equal 86_400.0, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2), ActiveSupport::TimeZone["UTC"]) - DateTime.civil(2000, 1, 1)
end
def test_minus_with_datetime_precision
- assert_equal 86_399.999999999, ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone['UTC'] ) - DateTime.civil(2000, 1, 1)
+ assert_equal 86_399.999999999, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone["UTC"]) - DateTime.civil(2000, 1, 1)
end
def test_minus_with_wrapped_datetime
- assert_equal 86_400.0, ActiveSupport::TimeWithZone.new( DateTime.civil(2000, 1, 2), ActiveSupport::TimeZone['UTC'] ) - Time.utc(2000, 1, 1)
- assert_equal 86_400.0, ActiveSupport::TimeWithZone.new( DateTime.civil(2000, 1, 2), ActiveSupport::TimeZone['UTC'] ) - DateTime.civil(2000, 1, 1)
+ assert_equal 86_400.0, ActiveSupport::TimeWithZone.new(DateTime.civil(2000, 1, 2), ActiveSupport::TimeZone["UTC"]) - Time.utc(2000, 1, 1)
+ assert_equal 86_400.0, ActiveSupport::TimeWithZone.new(DateTime.civil(2000, 1, 2), ActiveSupport::TimeZone["UTC"]) - DateTime.civil(2000, 1, 1)
end
def test_plus_and_minus_enforce_spring_dst_rules
- utc = Time.utc(2006,4,2,6,59,59) # == Apr 2 2006 01:59:59 EST; i.e., 1 second before daylight savings start
+ utc = Time.utc(2006, 4, 2, 6, 59, 59) # == Apr 2 2006 01:59:59 EST; i.e., 1 second before daylight savings start
twz = ActiveSupport::TimeWithZone.new(utc, @time_zone)
- assert_equal Time.utc(2006,4,2,1,59,59), twz.time
+ assert_equal Time.utc(2006, 4, 2, 1, 59, 59), twz.time
assert_equal false, twz.dst?
- assert_equal 'EST', twz.zone
+ assert_equal "EST", twz.zone
twz = twz + 1
- assert_equal Time.utc(2006,4,2,3), twz.time # adding 1 sec springs forward to 3:00AM EDT
+ assert_equal Time.utc(2006, 4, 2, 3), twz.time # adding 1 sec springs forward to 3:00AM EDT
assert_equal true, twz.dst?
- assert_equal 'EDT', twz.zone
+ assert_equal "EDT", twz.zone
twz = twz - 1 # subtracting 1 second takes goes back to 1:59:59AM EST
- assert_equal Time.utc(2006,4,2,1,59,59), twz.time
+ assert_equal Time.utc(2006, 4, 2, 1, 59, 59), twz.time
assert_equal false, twz.dst?
- assert_equal 'EST', twz.zone
+ assert_equal "EST", twz.zone
end
def test_plus_and_minus_enforce_fall_dst_rules
- utc = Time.utc(2006,10,29,5,59,59) # == Oct 29 2006 01:59:59 EST; i.e., 1 second before daylight savings end
+ utc = Time.utc(2006, 10, 29, 5, 59, 59) # == Oct 29 2006 01:59:59 EST; i.e., 1 second before daylight savings end
twz = ActiveSupport::TimeWithZone.new(utc, @time_zone)
- assert_equal Time.utc(2006,10,29,1,59,59), twz.time
+ assert_equal Time.utc(2006, 10, 29, 1, 59, 59), twz.time
assert_equal true, twz.dst?
- assert_equal 'EDT', twz.zone
+ assert_equal "EDT", twz.zone
twz = twz + 1
- assert_equal Time.utc(2006,10,29,1), twz.time # adding 1 sec falls back from 1:59:59 EDT to 1:00AM EST
+ assert_equal Time.utc(2006, 10, 29, 1), twz.time # adding 1 sec falls back from 1:59:59 EDT to 1:00AM EST
assert_equal false, twz.dst?
- assert_equal 'EST', twz.zone
+ assert_equal "EST", twz.zone
twz = twz - 1
- assert_equal Time.utc(2006,10,29,1,59,59), twz.time # subtracting 1 sec goes back to 1:59:59AM EDT
+ assert_equal Time.utc(2006, 10, 29, 1, 59, 59), twz.time # subtracting 1 sec goes back to 1:59:59AM EDT
assert_equal true, twz.dst?
- assert_equal 'EDT', twz.zone
+ assert_equal "EDT", twz.zone
end
def test_to_a
- assert_equal [45, 30, 5, 1, 2, 2000, 2, 32, false, "HST"], ActiveSupport::TimeWithZone.new( Time.utc(2000, 2, 1, 15, 30, 45), ActiveSupport::TimeZone['Hawaii'] ).to_a
+ assert_equal [45, 30, 5, 1, 2, 2000, 2, 32, false, "HST"], ActiveSupport::TimeWithZone.new(Time.utc(2000, 2, 1, 15, 30, 45), ActiveSupport::TimeZone["Hawaii"]).to_a
end
def test_to_f
- result = ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1), ActiveSupport::TimeZone['Hawaii'] ).to_f
+ result = ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1), ActiveSupport::TimeZone["Hawaii"]).to_f
assert_equal 946684800.0, result
assert_kind_of Float, result
end
def test_to_i
- result = ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1), ActiveSupport::TimeZone['Hawaii'] ).to_i
+ result = ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1), ActiveSupport::TimeZone["Hawaii"]).to_i
assert_equal 946684800, result
assert_kind_of Integer, result
end
@@ -387,33 +442,51 @@ class TimeWithZoneTest < ActiveSupport::TestCase
end
def test_to_r
- result = ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1), ActiveSupport::TimeZone['Hawaii']).to_r
+ result = ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1), ActiveSupport::TimeZone["Hawaii"]).to_r
assert_equal Rational(946684800, 1), result
assert_kind_of Rational, result
end
def test_time_at
- time = ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1), ActiveSupport::TimeZone['Hawaii'])
+ time = ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1), ActiveSupport::TimeZone["Hawaii"])
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
def test_to_date
# 1 sec before midnight Jan 1 EST
- assert_equal Date.new(1999, 12, 31), ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1, 4, 59, 59), ActiveSupport::TimeZone['Eastern Time (US & Canada)'] ).to_date
+ assert_equal Date.new(1999, 12, 31), ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 4, 59, 59), ActiveSupport::TimeZone["Eastern Time (US & Canada)"]).to_date
# midnight Jan 1 EST
- assert_equal Date.new(2000, 1, 1), ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1, 5, 0, 0), ActiveSupport::TimeZone['Eastern Time (US & Canada)'] ).to_date
+ assert_equal Date.new(2000, 1, 1), ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 5, 0, 0), ActiveSupport::TimeZone["Eastern Time (US & Canada)"]).to_date
# 1 sec before midnight Jan 2 EST
- assert_equal Date.new(2000, 1, 1), ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 2, 4, 59, 59), ActiveSupport::TimeZone['Eastern Time (US & Canada)'] ).to_date
+ assert_equal Date.new(2000, 1, 1), ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2, 4, 59, 59), ActiveSupport::TimeZone["Eastern Time (US & Canada)"]).to_date
# midnight Jan 2 EST
- assert_equal Date.new(2000, 1, 2), ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 2, 5, 0, 0), ActiveSupport::TimeZone['Eastern Time (US & Canada)'] ).to_date
+ assert_equal Date.new(2000, 1, 2), ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2, 5, 0, 0), ActiveSupport::TimeZone["Eastern Time (US & Canada)"]).to_date
end
def test_to_datetime
@@ -421,7 +494,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase
end
def test_acts_like_time
- assert @twz.acts_like_time?
+ assert_predicate @twz, :acts_like_time?
assert @twz.acts_like?(:time)
assert ActiveSupport::TimeWithZone.new(DateTime.civil(2000), @time_zone).acts_like?(:time)
end
@@ -431,6 +504,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_predicate @twz, :blank?
+ end
+
def test_is_a
assert_kind_of Time, @twz
assert_kind_of Time, @twz
@@ -438,40 +515,40 @@ class TimeWithZoneTest < ActiveSupport::TestCase
end
def test_class_name
- assert_equal 'Time', ActiveSupport::TimeWithZone.name
+ assert_equal "Time", ActiveSupport::TimeWithZone.name
end
def test_method_missing_with_time_return_value
assert_instance_of ActiveSupport::TimeWithZone, @twz.months_since(1)
- assert_equal Time.utc(2000, 1, 31, 19, 0 ,0), @twz.months_since(1).time
+ assert_equal Time.utc(2000, 1, 31, 19, 0, 0), @twz.months_since(1).time
end
def test_marshal_dump_and_load
marshal_str = Marshal.dump(@twz)
mtime = Marshal.load(marshal_str)
assert_equal Time.utc(2000, 1, 1, 0), mtime.utc
- assert mtime.utc.utc?
- assert_equal ActiveSupport::TimeZone['Eastern Time (US & Canada)'], mtime.time_zone
+ assert_predicate mtime.utc, :utc?
+ assert_equal ActiveSupport::TimeZone["Eastern Time (US & Canada)"], mtime.time_zone
assert_equal Time.utc(1999, 12, 31, 19), mtime.time
- assert mtime.time.utc?
+ assert_predicate mtime.time, :utc?
assert_equal @twz.inspect, mtime.inspect
end
def test_marshal_dump_and_load_with_tzinfo_identifier
- twz = ActiveSupport::TimeWithZone.new(@utc, TZInfo::Timezone.get('America/New_York'))
+ twz = ActiveSupport::TimeWithZone.new(@utc, TZInfo::Timezone.get("America/New_York"))
marshal_str = Marshal.dump(twz)
mtime = Marshal.load(marshal_str)
assert_equal Time.utc(2000, 1, 1, 0), mtime.utc
- assert mtime.utc.utc?
- assert_equal 'America/New_York', mtime.time_zone.name
+ assert_predicate mtime.utc, :utc?
+ assert_equal "America/New_York", mtime.time_zone.name
assert_equal Time.utc(1999, 12, 31, 19), mtime.time
- assert mtime.time.utc?
+ assert_predicate mtime.time, :utc?
assert_equal @twz.inspect, mtime.inspect
end
def test_freeze
@twz.freeze
- assert @twz.frozen?
+ assert_predicate @twz, :frozen?
end
def test_freeze_preloads_instance_variables
@@ -479,17 +556,19 @@ class TimeWithZoneTest < ActiveSupport::TestCase
assert_nothing_raised do
@twz.period
@twz.time
+ @twz.to_datetime
+ @twz.to_time
end
end
def test_method_missing_with_non_time_return_value
time = @twz.time
- def time.foo; 'bar'; end
- assert_equal 'bar', @twz.foo
+ def time.foo; "bar"; end
+ assert_equal "bar", @twz.foo
end
def test_date_part_value_methods
- twz = ActiveSupport::TimeWithZone.new(Time.utc(1999,12,31,19,18,17,500), @time_zone)
+ twz = ActiveSupport::TimeWithZone.new(Time.utc(1999, 12, 31, 19, 18, 17, 500), @time_zone)
assert_not_called(twz, :method_missing) do
assert_equal 1999, twz.year
assert_equal 12, twz.month
@@ -509,19 +588,19 @@ class TimeWithZoneTest < ActiveSupport::TestCase
end
def test_usec_returns_sec_fraction_when_datetime_is_wrapped
- twz = ActiveSupport::TimeWithZone.new(DateTime.civil(2000, 1, 1, 0, 0, Rational(1,2)), @time_zone)
+ twz = ActiveSupport::TimeWithZone.new(DateTime.civil(2000, 1, 1, 0, 0, Rational(1, 2)), @time_zone)
assert_equal 500000, twz.usec
end
def test_nsec_returns_sec_fraction_when_datetime_is_wrapped
- twz = ActiveSupport::TimeWithZone.new(DateTime.civil(2000, 1, 1, 0, 0, Rational(1,2)), @time_zone)
+ twz = ActiveSupport::TimeWithZone.new(DateTime.civil(2000, 1, 1, 0, 0, Rational(1, 2)), @time_zone)
assert_equal 500000000, twz.nsec
end
def test_utc_to_local_conversion_saves_period_in_instance_variable
- assert_nil @twz.instance_variable_get('@period')
+ assert_nil @twz.instance_variable_get("@period")
@twz.time
- assert_kind_of TZInfo::TimezonePeriod, @twz.instance_variable_get('@period')
+ assert_kind_of TZInfo::TimezonePeriod, @twz.instance_variable_get("@period")
end
def test_instance_created_with_local_time_returns_correct_utc_time
@@ -530,17 +609,17 @@ class TimeWithZoneTest < ActiveSupport::TestCase
end
def test_instance_created_with_local_time_enforces_spring_dst_rules
- twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,4,2,2)) # first second of DST
- assert_equal Time.utc(2006,4,2,3), twz.time # springs forward to 3AM
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 4, 2, 2)) # first second of DST
+ assert_equal Time.utc(2006, 4, 2, 3), twz.time # springs forward to 3AM
assert_equal true, twz.dst?
- assert_equal 'EDT', twz.zone
+ assert_equal "EDT", twz.zone
end
def test_instance_created_with_local_time_enforces_fall_dst_rules
- twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,10,29,1)) # 1AM can be either DST or non-DST; we'll pick DST
- assert_equal Time.utc(2006,10,29,1), twz.time
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 10, 29, 1)) # 1AM can be either DST or non-DST; we'll pick DST
+ assert_equal Time.utc(2006, 10, 29, 1), twz.time
assert_equal true, twz.dst?
- assert_equal 'EDT', twz.zone
+ assert_equal "EDT", twz.zone
end
def test_ruby_19_weekday_name_query_methods
@@ -551,42 +630,48 @@ class TimeWithZoneTest < ActiveSupport::TestCase
end
def test_utc_to_local_conversion_with_far_future_datetime
- assert_equal [0,0,19,31,12,2049], ActiveSupport::TimeWithZone.new(DateTime.civil(2050), @time_zone).to_a[0,6]
+ assert_equal [0, 0, 19, 31, 12, 2049], ActiveSupport::TimeWithZone.new(DateTime.civil(2050), @time_zone).to_a[0, 6]
end
def test_local_to_utc_conversion_with_far_future_datetime
- assert_equal DateTime.civil(2050).to_f, ActiveSupport::TimeWithZone.new(nil, @time_zone, DateTime.civil(2049,12,31,19)).to_f
+ assert_equal DateTime.civil(2050).to_f, ActiveSupport::TimeWithZone.new(nil, @time_zone, DateTime.civil(2049, 12, 31, 19)).to_f
end
def test_change
assert_equal "Fri, 31 Dec 1999 19:00:00 EST -05:00", @twz.inspect
- assert_equal "Mon, 31 Dec 2001 19:00:00 EST -05:00", @twz.change(:year => 2001).inspect
- assert_equal "Wed, 31 Mar 1999 19:00:00 EST -05:00", @twz.change(:month => 3).inspect
- assert_equal "Wed, 03 Mar 1999 19:00:00 EST -05:00", @twz.change(:month => 2).inspect
- assert_equal "Wed, 15 Dec 1999 19:00:00 EST -05:00", @twz.change(:day => 15).inspect
- assert_equal "Fri, 31 Dec 1999 06:00:00 EST -05:00", @twz.change(:hour => 6).inspect
- assert_equal "Fri, 31 Dec 1999 19:15:00 EST -05:00", @twz.change(:min => 15).inspect
- assert_equal "Fri, 31 Dec 1999 19:00:30 EST -05:00", @twz.change(:sec => 30).inspect
+ assert_equal "Mon, 31 Dec 2001 19:00:00 EST -05:00", @twz.change(year: 2001).inspect
+ assert_equal "Wed, 31 Mar 1999 19:00:00 EST -05:00", @twz.change(month: 3).inspect
+ assert_equal "Wed, 03 Mar 1999 19:00:00 EST -05:00", @twz.change(month: 2).inspect
+ assert_equal "Wed, 15 Dec 1999 19:00:00 EST -05:00", @twz.change(day: 15).inspect
+ assert_equal "Fri, 31 Dec 1999 06:00:00 EST -05:00", @twz.change(hour: 6).inspect
+ assert_equal "Fri, 31 Dec 1999 19:15:00 EST -05:00", @twz.change(min: 15).inspect
+ assert_equal "Fri, 31 Dec 1999 19:00:30 EST -05:00", @twz.change(sec: 30).inspect
+ assert_equal "Fri, 31 Dec 1999 19:00:00 HST -10:00", @twz.change(offset: "-10:00").inspect
+ assert_equal "Fri, 31 Dec 1999 19:00:00 HST -10:00", @twz.change(offset: -36000).inspect
+ assert_equal "Fri, 31 Dec 1999 19:00:00 HST -10:00", @twz.change(zone: "Hawaii").inspect
+ assert_equal "Fri, 31 Dec 1999 19:00:00 HST -10:00", @twz.change(zone: -10).inspect
+ assert_equal "Fri, 31 Dec 1999 19:00:00 HST -10:00", @twz.change(zone: -36000).inspect
+ assert_equal "Fri, 31 Dec 1999 19:00:00 HST -10:00", @twz.change(zone: "Pacific/Honolulu").inspect
end
def test_change_at_dst_boundary
- twz = ActiveSupport::TimeWithZone.new(Time.at(1319936400).getutc, ActiveSupport::TimeZone['Madrid'])
- assert_equal twz, twz.change(:min => 0)
+ twz = ActiveSupport::TimeWithZone.new(Time.at(1319936400).getutc, ActiveSupport::TimeZone["Madrid"])
+ assert_equal twz, twz.change(min: 0)
end
def test_round_at_dst_boundary
- twz = ActiveSupport::TimeWithZone.new(Time.at(1319936400).getutc, ActiveSupport::TimeZone['Madrid'])
+ twz = ActiveSupport::TimeWithZone.new(Time.at(1319936400).getutc, ActiveSupport::TimeZone["Madrid"])
assert_equal twz, twz.round
end
def test_advance
assert_equal "Fri, 31 Dec 1999 19:00:00 EST -05:00", @twz.inspect
- assert_equal "Mon, 31 Dec 2001 19:00:00 EST -05:00", @twz.advance(:years => 2).inspect
- assert_equal "Fri, 31 Mar 2000 19:00:00 EST -05:00", @twz.advance(:months => 3).inspect
- assert_equal "Tue, 04 Jan 2000 19:00:00 EST -05:00", @twz.advance(:days => 4).inspect
- assert_equal "Sat, 01 Jan 2000 01:00:00 EST -05:00", @twz.advance(:hours => 6).inspect
- assert_equal "Fri, 31 Dec 1999 19:15:00 EST -05:00", @twz.advance(:minutes => 15).inspect
- assert_equal "Fri, 31 Dec 1999 19:00:30 EST -05:00", @twz.advance(:seconds => 30).inspect
+ assert_equal "Mon, 31 Dec 2001 19:00:00 EST -05:00", @twz.advance(years: 2).inspect
+ assert_equal "Fri, 31 Mar 2000 19:00:00 EST -05:00", @twz.advance(months: 3).inspect
+ assert_equal "Tue, 04 Jan 2000 19:00:00 EST -05:00", @twz.advance(days: 4).inspect
+ assert_equal "Sat, 01 Jan 2000 01:00:00 EST -05:00", @twz.advance(hours: 6).inspect
+ assert_equal "Fri, 31 Dec 1999 19:15:00 EST -05:00", @twz.advance(minutes: 15).inspect
+ assert_equal "Fri, 31 Dec 1999 19:00:30 EST -05:00", @twz.advance(seconds: 30).inspect
end
def test_beginning_of_year
@@ -651,6 +736,10 @@ class TimeWithZoneTest < ActiveSupport::TestCase
assert_equal "Fri, 31 Dec 1999 19:00:01 EST -05:00", @twz.since(1).inspect
end
+ def test_in
+ assert_equal "Fri, 31 Dec 1999 19:00:01 EST -05:00", @twz.in(1).inspect
+ end
+
def test_ago
assert_equal "Fri, 31 Dec 1999 18:59:59 EST -05:00", @twz.ago(1).inspect
end
@@ -660,204 +749,262 @@ class TimeWithZoneTest < ActiveSupport::TestCase
end
def test_advance_1_year_from_leap_day
- twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2004,2,29))
- assert_equal "Mon, 28 Feb 2005 00:00:00 EST -05:00", twz.advance(:years => 1).inspect
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2004, 2, 29))
+ assert_equal "Mon, 28 Feb 2005 00:00:00 EST -05:00", twz.advance(years: 1).inspect
assert_equal "Mon, 28 Feb 2005 00:00:00 EST -05:00", twz.years_since(1).inspect
assert_equal "Mon, 28 Feb 2005 00:00:00 EST -05:00", twz.since(1.year).inspect
+ assert_equal "Mon, 28 Feb 2005 00:00:00 EST -05:00", twz.in(1.year).inspect
assert_equal "Mon, 28 Feb 2005 00:00:00 EST -05:00", (twz + 1.year).inspect
end
def test_advance_1_month_from_last_day_of_january
- twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2005,1,31))
- assert_equal "Mon, 28 Feb 2005 00:00:00 EST -05:00", twz.advance(:months => 1).inspect
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2005, 1, 31))
+ assert_equal "Mon, 28 Feb 2005 00:00:00 EST -05:00", twz.advance(months: 1).inspect
assert_equal "Mon, 28 Feb 2005 00:00:00 EST -05:00", twz.months_since(1).inspect
assert_equal "Mon, 28 Feb 2005 00:00:00 EST -05:00", twz.since(1.month).inspect
+ assert_equal "Mon, 28 Feb 2005 00:00:00 EST -05:00", twz.in(1.month).inspect
assert_equal "Mon, 28 Feb 2005 00:00:00 EST -05:00", (twz + 1.month).inspect
end
def test_advance_1_month_from_last_day_of_january_during_leap_year
- twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2000,1,31))
- assert_equal "Tue, 29 Feb 2000 00:00:00 EST -05:00", twz.advance(:months => 1).inspect
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2000, 1, 31))
+ assert_equal "Tue, 29 Feb 2000 00:00:00 EST -05:00", twz.advance(months: 1).inspect
assert_equal "Tue, 29 Feb 2000 00:00:00 EST -05:00", twz.months_since(1).inspect
assert_equal "Tue, 29 Feb 2000 00:00:00 EST -05:00", twz.since(1.month).inspect
+ assert_equal "Tue, 29 Feb 2000 00:00:00 EST -05:00", twz.in(1.month).inspect
assert_equal "Tue, 29 Feb 2000 00:00:00 EST -05:00", (twz + 1.month).inspect
end
def test_advance_1_month_into_spring_dst_gap
- twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,3,2,2))
- assert_equal "Sun, 02 Apr 2006 03:00:00 EDT -04:00", twz.advance(:months => 1).inspect
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 3, 2, 2))
+ assert_equal "Sun, 02 Apr 2006 03:00:00 EDT -04:00", twz.advance(months: 1).inspect
assert_equal "Sun, 02 Apr 2006 03:00:00 EDT -04:00", twz.months_since(1).inspect
assert_equal "Sun, 02 Apr 2006 03:00:00 EDT -04:00", twz.since(1.month).inspect
+ assert_equal "Sun, 02 Apr 2006 03:00:00 EDT -04:00", twz.in(1.month).inspect
assert_equal "Sun, 02 Apr 2006 03:00:00 EDT -04:00", (twz + 1.month).inspect
end
def test_advance_1_second_into_spring_dst_gap
- twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,4,2,1,59,59))
- assert_equal "Sun, 02 Apr 2006 03:00:00 EDT -04:00", twz.advance(:seconds => 1).inspect
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 4, 2, 1, 59, 59))
+ assert_equal "Sun, 02 Apr 2006 03:00:00 EDT -04:00", twz.advance(seconds: 1).inspect
assert_equal "Sun, 02 Apr 2006 03:00:00 EDT -04:00", (twz + 1).inspect
+ assert_equal "Sun, 02 Apr 2006 03:00:00 EDT -04:00", (twz + 1.second).inspect
assert_equal "Sun, 02 Apr 2006 03:00:00 EDT -04:00", twz.since(1).inspect
assert_equal "Sun, 02 Apr 2006 03:00:00 EDT -04:00", twz.since(1.second).inspect
- assert_equal "Sun, 02 Apr 2006 03:00:00 EDT -04:00", (twz + 1.second).inspect
+ assert_equal "Sun, 02 Apr 2006 03:00:00 EDT -04:00", twz.in(1).inspect
+ assert_equal "Sun, 02 Apr 2006 03:00:00 EDT -04:00", twz.in(1.second).inspect
end
def test_advance_1_day_across_spring_dst_transition
- twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,4,1,10,30))
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 4, 1, 10, 30))
# In 2006, spring DST transition occurred Apr 2 at 2AM; this day was only 23 hours long
# When we advance 1 day, we want to end up at the same time on the next day
- assert_equal "Sun, 02 Apr 2006 10:30:00 EDT -04:00", twz.advance(:days => 1).inspect
+ assert_equal "Sun, 02 Apr 2006 10:30:00 EDT -04:00", twz.advance(days: 1).inspect
assert_equal "Sun, 02 Apr 2006 10:30:00 EDT -04:00", twz.since(1.days).inspect
+ assert_equal "Sun, 02 Apr 2006 10:30:00 EDT -04:00", twz.in(1.days).inspect
assert_equal "Sun, 02 Apr 2006 10:30:00 EDT -04:00", (twz + 1.days).inspect
assert_equal "Sun, 02 Apr 2006 10:30:01 EDT -04:00", twz.since(1.days + 1.second).inspect
+ assert_equal "Sun, 02 Apr 2006 10:30:01 EDT -04:00", twz.in(1.days + 1.second).inspect
assert_equal "Sun, 02 Apr 2006 10:30:01 EDT -04:00", (twz + 1.days + 1.second).inspect
end
def test_advance_1_day_across_spring_dst_transition_backwards
- twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,4,2,10,30))
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 4, 2, 10, 30))
# In 2006, spring DST transition occurred Apr 2 at 2AM; this day was only 23 hours long
# When we advance back 1 day, we want to end up at the same time on the previous day
- assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.advance(:days => -1).inspect
+ assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.advance(days: -1).inspect
assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.ago(1.days).inspect
assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", (twz - 1.days).inspect
assert_equal "Sat, 01 Apr 2006 10:30:01 EST -05:00", twz.ago(1.days - 1.second).inspect
end
def test_advance_1_day_expressed_as_number_of_seconds_minutes_or_hours_across_spring_dst_transition
- twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,4,1,10,30))
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 4, 1, 10, 30))
# In 2006, spring DST transition occurred Apr 2 at 2AM; this day was only 23 hours long
# When we advance a specific number of hours, minutes or seconds, we want to advance exactly that amount
assert_equal "Sun, 02 Apr 2006 11:30:00 EDT -04:00", (twz + 86400).inspect
assert_equal "Sun, 02 Apr 2006 11:30:00 EDT -04:00", (twz + 86400.seconds).inspect
assert_equal "Sun, 02 Apr 2006 11:30:00 EDT -04:00", twz.since(86400).inspect
assert_equal "Sun, 02 Apr 2006 11:30:00 EDT -04:00", twz.since(86400.seconds).inspect
- assert_equal "Sun, 02 Apr 2006 11:30:00 EDT -04:00", twz.advance(:seconds => 86400).inspect
+ assert_equal "Sun, 02 Apr 2006 11:30:00 EDT -04:00", twz.in(86400).inspect
+ assert_equal "Sun, 02 Apr 2006 11:30:00 EDT -04:00", twz.in(86400.seconds).inspect
+ assert_equal "Sun, 02 Apr 2006 11:30:00 EDT -04:00", twz.advance(seconds: 86400).inspect
assert_equal "Sun, 02 Apr 2006 11:30:00 EDT -04:00", (twz + 1440.minutes).inspect
assert_equal "Sun, 02 Apr 2006 11:30:00 EDT -04:00", twz.since(1440.minutes).inspect
- assert_equal "Sun, 02 Apr 2006 11:30:00 EDT -04:00", twz.advance(:minutes => 1440).inspect
+ assert_equal "Sun, 02 Apr 2006 11:30:00 EDT -04:00", twz.in(1440.minutes).inspect
+ assert_equal "Sun, 02 Apr 2006 11:30:00 EDT -04:00", twz.advance(minutes: 1440).inspect
assert_equal "Sun, 02 Apr 2006 11:30:00 EDT -04:00", (twz + 24.hours).inspect
assert_equal "Sun, 02 Apr 2006 11:30:00 EDT -04:00", twz.since(24.hours).inspect
- assert_equal "Sun, 02 Apr 2006 11:30:00 EDT -04:00", twz.advance(:hours => 24).inspect
+ assert_equal "Sun, 02 Apr 2006 11:30:00 EDT -04:00", twz.in(24.hours).inspect
+ assert_equal "Sun, 02 Apr 2006 11:30:00 EDT -04:00", twz.advance(hours: 24).inspect
end
def test_advance_1_day_expressed_as_number_of_seconds_minutes_or_hours_across_spring_dst_transition_backwards
- twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,4,2,11,30))
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 4, 2, 11, 30))
# In 2006, spring DST transition occurred Apr 2 at 2AM; this day was only 23 hours long
# When we advance a specific number of hours, minutes or seconds, we want to advance exactly that amount
assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", (twz - 86400).inspect
assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", (twz - 86400.seconds).inspect
assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.ago(86400).inspect
assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.ago(86400.seconds).inspect
- assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.advance(:seconds => -86400).inspect
+ assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.advance(seconds: -86400).inspect
assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", (twz - 1440.minutes).inspect
assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.ago(1440.minutes).inspect
- assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.advance(:minutes => -1440).inspect
+ assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.advance(minutes: -1440).inspect
assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", (twz - 24.hours).inspect
assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.ago(24.hours).inspect
- assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.advance(:hours => -24).inspect
+ assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.advance(hours: -24).inspect
end
def test_advance_1_day_across_fall_dst_transition
- twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,10,28,10,30))
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 10, 28, 10, 30))
# In 2006, fall DST transition occurred Oct 29 at 2AM; this day was 25 hours long
# When we advance 1 day, we want to end up at the same time on the next day
- assert_equal "Sun, 29 Oct 2006 10:30:00 EST -05:00", twz.advance(:days => 1).inspect
+ assert_equal "Sun, 29 Oct 2006 10:30:00 EST -05:00", twz.advance(days: 1).inspect
assert_equal "Sun, 29 Oct 2006 10:30:00 EST -05:00", twz.since(1.days).inspect
+ assert_equal "Sun, 29 Oct 2006 10:30:00 EST -05:00", twz.in(1.days).inspect
assert_equal "Sun, 29 Oct 2006 10:30:00 EST -05:00", (twz + 1.days).inspect
assert_equal "Sun, 29 Oct 2006 10:30:01 EST -05:00", twz.since(1.days + 1.second).inspect
+ assert_equal "Sun, 29 Oct 2006 10:30:01 EST -05:00", twz.in(1.days + 1.second).inspect
assert_equal "Sun, 29 Oct 2006 10:30:01 EST -05:00", (twz + 1.days + 1.second).inspect
end
def test_advance_1_day_across_fall_dst_transition_backwards
- twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,10,29,10,30))
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 10, 29, 10, 30))
# In 2006, fall DST transition occurred Oct 29 at 2AM; this day was 25 hours long
# When we advance backwards 1 day, we want to end up at the same time on the previous day
- assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.advance(:days => -1).inspect
+ assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.advance(days: -1).inspect
assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.ago(1.days).inspect
assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", (twz - 1.days).inspect
assert_equal "Sat, 28 Oct 2006 10:30:01 EDT -04:00", twz.ago(1.days - 1.second).inspect
end
def test_advance_1_day_expressed_as_number_of_seconds_minutes_or_hours_across_fall_dst_transition
- twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,10,28,10,30))
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 10, 28, 10, 30))
# In 2006, fall DST transition occurred Oct 29 at 2AM; this day was 25 hours long
# When we advance a specific number of hours, minutes or seconds, we want to advance exactly that amount
assert_equal "Sun, 29 Oct 2006 09:30:00 EST -05:00", (twz + 86400).inspect
assert_equal "Sun, 29 Oct 2006 09:30:00 EST -05:00", (twz + 86400.seconds).inspect
assert_equal "Sun, 29 Oct 2006 09:30:00 EST -05:00", twz.since(86400).inspect
assert_equal "Sun, 29 Oct 2006 09:30:00 EST -05:00", twz.since(86400.seconds).inspect
- assert_equal "Sun, 29 Oct 2006 09:30:00 EST -05:00", twz.advance(:seconds => 86400).inspect
+ assert_equal "Sun, 29 Oct 2006 09:30:00 EST -05:00", twz.in(86400).inspect
+ assert_equal "Sun, 29 Oct 2006 09:30:00 EST -05:00", twz.in(86400.seconds).inspect
+ assert_equal "Sun, 29 Oct 2006 09:30:00 EST -05:00", twz.advance(seconds: 86400).inspect
assert_equal "Sun, 29 Oct 2006 09:30:00 EST -05:00", (twz + 1440.minutes).inspect
assert_equal "Sun, 29 Oct 2006 09:30:00 EST -05:00", twz.since(1440.minutes).inspect
- assert_equal "Sun, 29 Oct 2006 09:30:00 EST -05:00", twz.advance(:minutes => 1440).inspect
+ assert_equal "Sun, 29 Oct 2006 09:30:00 EST -05:00", twz.in(1440.minutes).inspect
+ assert_equal "Sun, 29 Oct 2006 09:30:00 EST -05:00", twz.advance(minutes: 1440).inspect
assert_equal "Sun, 29 Oct 2006 09:30:00 EST -05:00", (twz + 24.hours).inspect
assert_equal "Sun, 29 Oct 2006 09:30:00 EST -05:00", twz.since(24.hours).inspect
- assert_equal "Sun, 29 Oct 2006 09:30:00 EST -05:00", twz.advance(:hours => 24).inspect
+ assert_equal "Sun, 29 Oct 2006 09:30:00 EST -05:00", twz.in(24.hours).inspect
+ assert_equal "Sun, 29 Oct 2006 09:30:00 EST -05:00", twz.advance(hours: 24).inspect
end
def test_advance_1_day_expressed_as_number_of_seconds_minutes_or_hours_across_fall_dst_transition_backwards
- twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,10,29,9,30))
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 10, 29, 9, 30))
# In 2006, fall DST transition occurred Oct 29 at 2AM; this day was 25 hours long
# When we advance a specific number of hours, minutes or seconds, we want to advance exactly that amount
assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", (twz - 86400).inspect
assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", (twz - 86400.seconds).inspect
assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.ago(86400).inspect
assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.ago(86400.seconds).inspect
- assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.advance(:seconds => -86400).inspect
+ assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.advance(seconds: -86400).inspect
assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", (twz - 1440.minutes).inspect
assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.ago(1440.minutes).inspect
- assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.advance(:minutes => -1440).inspect
+ assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.advance(minutes: -1440).inspect
assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", (twz - 24.hours).inspect
assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.ago(24.hours).inspect
- assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.advance(:hours => -24).inspect
+ assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.advance(hours: -24).inspect
+ end
+
+ def test_advance_1_week_across_spring_dst_transition
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 4, 1, 10, 30))
+ assert_equal "Sat, 08 Apr 2006 10:30:00 EDT -04:00", twz.advance(weeks: 1).inspect
+ assert_equal "Sat, 08 Apr 2006 10:30:00 EDT -04:00", twz.weeks_since(1).inspect
+ assert_equal "Sat, 08 Apr 2006 10:30:00 EDT -04:00", twz.since(1.week).inspect
+ assert_equal "Sat, 08 Apr 2006 10:30:00 EDT -04:00", twz.in(1.week).inspect
+ assert_equal "Sat, 08 Apr 2006 10:30:00 EDT -04:00", (twz + 1.week).inspect
+ end
+
+ def test_advance_1_week_across_spring_dst_transition_backwards
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 4, 8, 10, 30))
+ assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.advance(weeks: -1).inspect
+ assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.weeks_ago(1).inspect
+ assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.ago(1.week).inspect
+ assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", (twz - 1.week).inspect
+ end
+
+ def test_advance_1_week_across_fall_dst_transition
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 10, 28, 10, 30))
+ assert_equal "Sat, 04 Nov 2006 10:30:00 EST -05:00", twz.advance(weeks: 1).inspect
+ assert_equal "Sat, 04 Nov 2006 10:30:00 EST -05:00", twz.weeks_since(1).inspect
+ assert_equal "Sat, 04 Nov 2006 10:30:00 EST -05:00", twz.since(1.week).inspect
+ assert_equal "Sat, 04 Nov 2006 10:30:00 EST -05:00", twz.in(1.week).inspect
+ assert_equal "Sat, 04 Nov 2006 10:30:00 EST -05:00", (twz + 1.week).inspect
+ end
+
+ def test_advance_1_week_across_fall_dst_transition_backwards
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 11, 4, 10, 30))
+ assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.advance(weeks: -1).inspect
+ assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.weeks_ago(1).inspect
+ assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.ago(1.week).inspect
+ assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", (twz - 1.week).inspect
end
def test_advance_1_month_across_spring_dst_transition
- twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,4,1,10,30))
- assert_equal "Mon, 01 May 2006 10:30:00 EDT -04:00", twz.advance(:months => 1).inspect
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 4, 1, 10, 30))
+ assert_equal "Mon, 01 May 2006 10:30:00 EDT -04:00", twz.advance(months: 1).inspect
assert_equal "Mon, 01 May 2006 10:30:00 EDT -04:00", twz.months_since(1).inspect
assert_equal "Mon, 01 May 2006 10:30:00 EDT -04:00", twz.since(1.month).inspect
+ assert_equal "Mon, 01 May 2006 10:30:00 EDT -04:00", twz.in(1.month).inspect
assert_equal "Mon, 01 May 2006 10:30:00 EDT -04:00", (twz + 1.month).inspect
end
def test_advance_1_month_across_spring_dst_transition_backwards
- twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,5,1,10,30))
- assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.advance(:months => -1).inspect
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 5, 1, 10, 30))
+ assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.advance(months: -1).inspect
assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.months_ago(1).inspect
assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.ago(1.month).inspect
assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", (twz - 1.month).inspect
end
def test_advance_1_month_across_fall_dst_transition
- twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,10,28,10,30))
- assert_equal "Tue, 28 Nov 2006 10:30:00 EST -05:00", twz.advance(:months => 1).inspect
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 10, 28, 10, 30))
+ assert_equal "Tue, 28 Nov 2006 10:30:00 EST -05:00", twz.advance(months: 1).inspect
assert_equal "Tue, 28 Nov 2006 10:30:00 EST -05:00", twz.months_since(1).inspect
assert_equal "Tue, 28 Nov 2006 10:30:00 EST -05:00", twz.since(1.month).inspect
+ assert_equal "Tue, 28 Nov 2006 10:30:00 EST -05:00", twz.in(1.month).inspect
assert_equal "Tue, 28 Nov 2006 10:30:00 EST -05:00", (twz + 1.month).inspect
end
def test_advance_1_month_across_fall_dst_transition_backwards
- twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,11,28,10,30))
- assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.advance(:months => -1).inspect
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006, 11, 28, 10, 30))
+ assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.advance(months: -1).inspect
assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.months_ago(1).inspect
assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.ago(1.month).inspect
assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", (twz - 1.month).inspect
end
def test_advance_1_year
- twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2008,2,15,10,30))
- assert_equal "Sun, 15 Feb 2009 10:30:00 EST -05:00", twz.advance(:years => 1).inspect
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2008, 2, 15, 10, 30))
+ assert_equal "Sun, 15 Feb 2009 10:30:00 EST -05:00", twz.advance(years: 1).inspect
assert_equal "Sun, 15 Feb 2009 10:30:00 EST -05:00", twz.years_since(1).inspect
+ assert_equal "Sun, 15 Feb 2009 10:30:00 EST -05:00", twz.since(1.year).inspect
+ assert_equal "Sun, 15 Feb 2009 10:30:00 EST -05:00", twz.in(1.year).inspect
assert_equal "Sun, 15 Feb 2009 10:30:00 EST -05:00", (twz + 1.year).inspect
- assert_equal "Thu, 15 Feb 2007 10:30:00 EST -05:00", twz.advance(:years => -1).inspect
+ assert_equal "Thu, 15 Feb 2007 10:30:00 EST -05:00", twz.advance(years: -1).inspect
assert_equal "Thu, 15 Feb 2007 10:30:00 EST -05:00", twz.years_ago(1).inspect
assert_equal "Thu, 15 Feb 2007 10:30:00 EST -05:00", (twz - 1.year).inspect
end
def test_advance_1_year_during_dst
- twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2008,7,15,10,30))
- assert_equal "Wed, 15 Jul 2009 10:30:00 EDT -04:00", twz.advance(:years => 1).inspect
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2008, 7, 15, 10, 30))
+ assert_equal "Wed, 15 Jul 2009 10:30:00 EDT -04:00", twz.advance(years: 1).inspect
assert_equal "Wed, 15 Jul 2009 10:30:00 EDT -04:00", twz.years_since(1).inspect
+ assert_equal "Wed, 15 Jul 2009 10:30:00 EDT -04:00", twz.since(1.year).inspect
+ assert_equal "Wed, 15 Jul 2009 10:30:00 EDT -04:00", twz.in(1.year).inspect
assert_equal "Wed, 15 Jul 2009 10:30:00 EDT -04:00", (twz + 1.year).inspect
- assert_equal "Sun, 15 Jul 2007 10:30:00 EDT -04:00", twz.advance(:years => -1).inspect
+ assert_equal "Sun, 15 Jul 2007 10:30:00 EDT -04:00", twz.advance(years: -1).inspect
assert_equal "Sun, 15 Jul 2007 10:30:00 EDT -04:00", twz.years_ago(1).inspect
assert_equal "Sun, 15 Jul 2007 10:30:00 EDT -04:00", (twz - 1.year).inspect
end
@@ -885,13 +1032,13 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase
end
def test_in_time_zone
- Time.use_zone 'Alaska' do
- assert_equal 'Fri, 31 Dec 1999 15:00:00 AKST -09:00', @t.in_time_zone.inspect
- assert_equal 'Fri, 31 Dec 1999 15:00:00 AKST -09:00', @dt.in_time_zone.inspect
+ Time.use_zone "Alaska" do
+ assert_equal "Fri, 31 Dec 1999 15:00:00 AKST -09:00", @t.in_time_zone.inspect
+ assert_equal "Fri, 31 Dec 1999 15:00:00 AKST -09:00", @dt.in_time_zone.inspect
end
- Time.use_zone 'Hawaii' do
- assert_equal 'Fri, 31 Dec 1999 14:00:00 HST -10:00', @t.in_time_zone.inspect
- assert_equal 'Fri, 31 Dec 1999 14:00:00 HST -10:00', @dt.in_time_zone.inspect
+ Time.use_zone "Hawaii" do
+ assert_equal "Fri, 31 Dec 1999 14:00:00 HST -10:00", @t.in_time_zone.inspect
+ assert_equal "Fri, 31 Dec 1999 14:00:00 HST -10:00", @dt.in_time_zone.inspect
end
Time.use_zone nil do
assert_equal @t, @t.in_time_zone
@@ -901,20 +1048,20 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase
def test_nil_time_zone
Time.use_zone nil do
- assert !@t.in_time_zone.respond_to?(:period), 'no period method'
- assert !@dt.in_time_zone.respond_to?(:period), 'no period method'
+ assert_not_respond_to @t.in_time_zone, :period, "no period method"
+ assert_not_respond_to @dt.in_time_zone, :period, "no period method"
end
end
def test_in_time_zone_with_argument
- Time.use_zone 'Eastern Time (US & Canada)' do # Time.zone will not affect #in_time_zone(zone)
- assert_equal 'Fri, 31 Dec 1999 15:00:00 AKST -09:00', @t.in_time_zone('Alaska').inspect
- assert_equal 'Fri, 31 Dec 1999 15:00:00 AKST -09:00', @dt.in_time_zone('Alaska').inspect
- assert_equal 'Fri, 31 Dec 1999 14:00:00 HST -10:00', @t.in_time_zone('Hawaii').inspect
- assert_equal 'Fri, 31 Dec 1999 14:00:00 HST -10:00', @dt.in_time_zone('Hawaii').inspect
- assert_equal 'Sat, 01 Jan 2000 00:00:00 UTC +00:00', @t.in_time_zone('UTC').inspect
- assert_equal 'Sat, 01 Jan 2000 00:00:00 UTC +00:00', @dt.in_time_zone('UTC').inspect
- assert_equal 'Fri, 31 Dec 1999 15:00:00 AKST -09:00', @t.in_time_zone(-9.hours).inspect
+ Time.use_zone "Eastern Time (US & Canada)" do # Time.zone will not affect #in_time_zone(zone)
+ assert_equal "Fri, 31 Dec 1999 15:00:00 AKST -09:00", @t.in_time_zone("Alaska").inspect
+ assert_equal "Fri, 31 Dec 1999 15:00:00 AKST -09:00", @dt.in_time_zone("Alaska").inspect
+ assert_equal "Fri, 31 Dec 1999 14:00:00 HST -10:00", @t.in_time_zone("Hawaii").inspect
+ assert_equal "Fri, 31 Dec 1999 14:00:00 HST -10:00", @dt.in_time_zone("Hawaii").inspect
+ assert_equal "Sat, 01 Jan 2000 00:00:00 UTC +00:00", @t.in_time_zone("UTC").inspect
+ assert_equal "Sat, 01 Jan 2000 00:00:00 UTC +00:00", @dt.in_time_zone("UTC").inspect
+ assert_equal "Fri, 31 Dec 1999 15:00:00 AKST -09:00", @t.in_time_zone(-9.hours).inspect
end
end
@@ -928,119 +1075,119 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase
end
def test_in_time_zone_with_time_local_instance
- with_env_tz 'US/Eastern' do
+ with_env_tz "US/Eastern" do
time = Time.local(1999, 12, 31, 19) # == Time.utc(2000)
- assert_equal 'Fri, 31 Dec 1999 15:00:00 AKST -09:00', time.in_time_zone('Alaska').inspect
+ assert_equal "Fri, 31 Dec 1999 15:00:00 AKST -09:00", time.in_time_zone("Alaska").inspect
end
end
def test_localtime
- Time.zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
+ Time.zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
assert_equal @dt.in_time_zone.localtime, @dt.in_time_zone.utc.to_time.getlocal
end
def test_use_zone
- Time.zone = 'Alaska'
- Time.use_zone 'Hawaii' do
- assert_equal ActiveSupport::TimeZone['Hawaii'], Time.zone
+ Time.zone = "Alaska"
+ Time.use_zone "Hawaii" do
+ assert_equal ActiveSupport::TimeZone["Hawaii"], Time.zone
end
- assert_equal ActiveSupport::TimeZone['Alaska'], Time.zone
+ assert_equal ActiveSupport::TimeZone["Alaska"], Time.zone
end
def test_use_zone_with_exception_raised
- Time.zone = 'Alaska'
+ Time.zone = "Alaska"
assert_raise RuntimeError do
- Time.use_zone('Hawaii') { raise RuntimeError }
+ Time.use_zone("Hawaii") { raise RuntimeError }
end
- assert_equal ActiveSupport::TimeZone['Alaska'], Time.zone
+ assert_equal ActiveSupport::TimeZone["Alaska"], Time.zone
end
def test_use_zone_raises_on_invalid_timezone
- Time.zone = 'Alaska'
+ Time.zone = "Alaska"
assert_raise ArgumentError do
Time.use_zone("No such timezone exists") { }
end
- assert_equal ActiveSupport::TimeZone['Alaska'], Time.zone
+ assert_equal ActiveSupport::TimeZone["Alaska"], Time.zone
end
def test_time_zone_getter_and_setter
- Time.zone = ActiveSupport::TimeZone['Alaska']
- assert_equal ActiveSupport::TimeZone['Alaska'], Time.zone
- Time.zone = 'Alaska'
- assert_equal ActiveSupport::TimeZone['Alaska'], Time.zone
+ Time.zone = ActiveSupport::TimeZone["Alaska"]
+ assert_equal ActiveSupport::TimeZone["Alaska"], Time.zone
+ Time.zone = "Alaska"
+ assert_equal ActiveSupport::TimeZone["Alaska"], Time.zone
Time.zone = -9.hours
- assert_equal ActiveSupport::TimeZone['Alaska'], Time.zone
+ 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
old_zone_default = Time.zone_default
- Time.zone_default = ActiveSupport::TimeZone['Alaska']
- assert_equal ActiveSupport::TimeZone['Alaska'], Time.zone
- Time.zone = ActiveSupport::TimeZone['Hawaii']
- assert_equal ActiveSupport::TimeZone['Hawaii'], Time.zone
+ Time.zone_default = ActiveSupport::TimeZone["Alaska"]
+ assert_equal ActiveSupport::TimeZone["Alaska"], Time.zone
+ Time.zone = ActiveSupport::TimeZone["Hawaii"]
+ assert_equal ActiveSupport::TimeZone["Hawaii"], Time.zone
Time.zone = nil
- assert_equal ActiveSupport::TimeZone['Alaska'], Time.zone
+ assert_equal ActiveSupport::TimeZone["Alaska"], Time.zone
ensure
Time.zone_default = old_zone_default
end
def test_time_zone_setter_is_thread_safe
- Time.use_zone 'Paris' do
- t1 = Thread.new { Time.zone = 'Alaska' }.join
- t2 = Thread.new { Time.zone = 'Hawaii' }.join
+ Time.use_zone "Paris" do
+ t1 = Thread.new { Time.zone = "Alaska" }.join
+ t2 = Thread.new { Time.zone = "Hawaii" }.join
assert t1.stop?, "Thread 1 did not finish running"
assert t2.stop?, "Thread 2 did not finish running"
- assert_equal ActiveSupport::TimeZone['Paris'], Time.zone
- assert_equal ActiveSupport::TimeZone['Alaska'], t1[:time_zone]
- assert_equal ActiveSupport::TimeZone['Hawaii'], t2[:time_zone]
+ assert_equal ActiveSupport::TimeZone["Paris"], Time.zone
+ assert_equal ActiveSupport::TimeZone["Alaska"], t1[:time_zone]
+ assert_equal ActiveSupport::TimeZone["Hawaii"], t2[:time_zone]
end
end
def test_time_zone_setter_with_tzinfo_timezone_object_wraps_in_rails_time_zone
- tzinfo = TZInfo::Timezone.get('America/New_York')
+ tzinfo = TZInfo::Timezone.get("America/New_York")
Time.zone = tzinfo
assert_kind_of ActiveSupport::TimeZone, Time.zone
assert_equal tzinfo, Time.zone.tzinfo
- assert_equal 'America/New_York', Time.zone.name
+ assert_equal "America/New_York", Time.zone.name
assert_equal(-18_000, Time.zone.utc_offset)
end
def test_time_zone_setter_with_tzinfo_timezone_identifier_does_lookup_and_wraps_in_rails_time_zone
- Time.zone = 'America/New_York'
+ Time.zone = "America/New_York"
assert_kind_of ActiveSupport::TimeZone, Time.zone
- assert_equal 'America/New_York', Time.zone.tzinfo.name
- assert_equal 'America/New_York', Time.zone.name
+ assert_equal "America/New_York", Time.zone.tzinfo.name
+ assert_equal "America/New_York", Time.zone.name
assert_equal(-18_000, Time.zone.utc_offset)
end
def test_time_zone_setter_with_invalid_zone
- assert_raise(ArgumentError){ Time.zone = "No such timezone exists" }
- assert_raise(ArgumentError){ Time.zone = -15.hours }
- assert_raise(ArgumentError){ Time.zone = Object.new }
+ assert_raise(ArgumentError) { Time.zone = "No such timezone exists" }
+ assert_raise(ArgumentError) { Time.zone = -15.hours }
+ assert_raise(ArgumentError) { Time.zone = Object.new }
end
def test_find_zone_without_bang_returns_nil_if_time_zone_can_not_be_found
- assert_nil Time.find_zone('No such timezone exists')
+ assert_nil Time.find_zone("No such timezone exists")
assert_nil Time.find_zone(-15.hours)
assert_nil Time.find_zone(Object.new)
end
def test_find_zone_with_bang_raises_if_time_zone_can_not_be_found
- assert_raise(ArgumentError) { Time.find_zone!('No such timezone exists') }
+ assert_raise(ArgumentError) { Time.find_zone!("No such timezone exists") }
assert_raise(ArgumentError) { Time.find_zone!(-15.hours) }
assert_raise(ArgumentError) { Time.find_zone!(Object.new) }
end
def test_time_zone_setter_with_find_zone_without_bang
- assert_nil Time.zone = Time.find_zone('No such timezone exists')
+ assert_nil Time.zone = Time.find_zone("No such timezone exists")
assert_nil Time.zone = Time.find_zone(-15.hours)
assert_nil Time.zone = Time.find_zone(Object.new)
end
def test_current_returns_time_now_when_zone_not_set
- with_env_tz 'US/Eastern' do
+ with_env_tz "US/Eastern" do
Time.stub(:now, Time.local(2000)) do
assert_equal false, Time.current.is_a?(ActiveSupport::TimeWithZone)
assert_equal Time.local(2000), Time.current
@@ -1049,22 +1196,22 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase
end
def test_current_returns_time_zone_now_when_zone_set
- Time.zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
- with_env_tz 'US/Eastern' do
+ Time.zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ with_env_tz "US/Eastern" do
Time.stub(:now, Time.local(2000)) do
assert_equal true, Time.current.is_a?(ActiveSupport::TimeWithZone)
- assert_equal 'Eastern Time (US & Canada)', Time.current.time_zone.name
+ assert_equal "Eastern Time (US & Canada)", Time.current.time_zone.name
assert_equal Time.utc(2000), Time.current.time
end
end
end
def test_time_in_time_zone_doesnt_affect_receiver
- with_env_tz 'Europe/London' do
+ with_env_tz "Europe/London" do
time = Time.local(2000, 7, 1)
- time_with_zone = time.in_time_zone('Eastern Time (US & Canada)')
+ time_with_zone = time.in_time_zone("Eastern Time (US & Canada)")
assert_equal Time.utc(2000, 6, 30, 23, 0, 0), time_with_zone
- assert_not time.utc?, 'time expected to be local, but is UTC'
+ assert_not time.utc?, "time expected to be local, but is UTC"
end
end
end
@@ -1077,11 +1224,11 @@ class TimeWithZoneMethodsForDate < ActiveSupport::TestCase
end
def test_in_time_zone
- with_tz_default 'Alaska' do
- assert_equal 'Sat, 01 Jan 2000 00:00:00 AKST -09:00', @d.in_time_zone.inspect
+ with_tz_default "Alaska" do
+ assert_equal "Sat, 01 Jan 2000 00:00:00 AKST -09:00", @d.in_time_zone.inspect
end
- with_tz_default 'Hawaii' do
- assert_equal 'Sat, 01 Jan 2000 00:00:00 HST -10:00', @d.in_time_zone.inspect
+ with_tz_default "Hawaii" do
+ assert_equal "Sat, 01 Jan 2000 00:00:00 HST -10:00", @d.in_time_zone.inspect
end
with_tz_default nil do
assert_equal @d.to_time, @d.in_time_zone
@@ -1090,16 +1237,16 @@ class TimeWithZoneMethodsForDate < ActiveSupport::TestCase
def test_nil_time_zone
with_tz_default nil do
- assert !@d.in_time_zone.respond_to?(:period), 'no period method'
+ assert_not_respond_to @d.in_time_zone, :period, "no period method"
end
end
def test_in_time_zone_with_argument
- with_tz_default 'Eastern Time (US & Canada)' do # Time.zone will not affect #in_time_zone(zone)
- assert_equal 'Sat, 01 Jan 2000 00:00:00 AKST -09:00', @d.in_time_zone('Alaska').inspect
- assert_equal 'Sat, 01 Jan 2000 00:00:00 HST -10:00', @d.in_time_zone('Hawaii').inspect
- assert_equal 'Sat, 01 Jan 2000 00:00:00 UTC +00:00', @d.in_time_zone('UTC').inspect
- assert_equal 'Sat, 01 Jan 2000 00:00:00 AKST -09:00', @d.in_time_zone(-9.hours).inspect
+ with_tz_default "Eastern Time (US & Canada)" do # Time.zone will not affect #in_time_zone(zone)
+ assert_equal "Sat, 01 Jan 2000 00:00:00 AKST -09:00", @d.in_time_zone("Alaska").inspect
+ assert_equal "Sat, 01 Jan 2000 00:00:00 HST -10:00", @d.in_time_zone("Hawaii").inspect
+ assert_equal "Sat, 01 Jan 2000 00:00:00 UTC +00:00", @d.in_time_zone("UTC").inspect
+ assert_equal "Sat, 01 Jan 2000 00:00:00 AKST -09:00", @d.in_time_zone(-9.hours).inspect
end
end
@@ -1120,15 +1267,15 @@ class TimeWithZoneMethodsForString < ActiveSupport::TestCase
end
def test_in_time_zone
- with_tz_default 'Alaska' do
- assert_equal 'Sat, 01 Jan 2000 00:00:00 AKST -09:00', @s.in_time_zone.inspect
- assert_equal 'Fri, 31 Dec 1999 15:00:00 AKST -09:00', @u.in_time_zone.inspect
- assert_equal 'Fri, 31 Dec 1999 15:00:00 AKST -09:00', @z.in_time_zone.inspect
+ with_tz_default "Alaska" do
+ assert_equal "Sat, 01 Jan 2000 00:00:00 AKST -09:00", @s.in_time_zone.inspect
+ assert_equal "Fri, 31 Dec 1999 15:00:00 AKST -09:00", @u.in_time_zone.inspect
+ assert_equal "Fri, 31 Dec 1999 15:00:00 AKST -09:00", @z.in_time_zone.inspect
end
- with_tz_default 'Hawaii' do
- assert_equal 'Sat, 01 Jan 2000 00:00:00 HST -10:00', @s.in_time_zone.inspect
- assert_equal 'Fri, 31 Dec 1999 14:00:00 HST -10:00', @u.in_time_zone.inspect
- assert_equal 'Fri, 31 Dec 1999 14:00:00 HST -10:00', @z.in_time_zone.inspect
+ with_tz_default "Hawaii" do
+ assert_equal "Sat, 01 Jan 2000 00:00:00 HST -10:00", @s.in_time_zone.inspect
+ assert_equal "Fri, 31 Dec 1999 14:00:00 HST -10:00", @u.in_time_zone.inspect
+ assert_equal "Fri, 31 Dec 1999 14:00:00 HST -10:00", @z.in_time_zone.inspect
end
with_tz_default nil do
assert_equal @s.to_time, @s.in_time_zone
@@ -1139,26 +1286,26 @@ class TimeWithZoneMethodsForString < ActiveSupport::TestCase
def test_nil_time_zone
with_tz_default nil do
- assert !@s.in_time_zone.respond_to?(:period), 'no period method'
- assert !@u.in_time_zone.respond_to?(:period), 'no period method'
- assert !@z.in_time_zone.respond_to?(:period), 'no period method'
+ assert_not_respond_to @s.in_time_zone, :period, "no period method"
+ assert_not_respond_to @u.in_time_zone, :period, "no period method"
+ assert_not_respond_to @z.in_time_zone, :period, "no period method"
end
end
def test_in_time_zone_with_argument
- with_tz_default 'Eastern Time (US & Canada)' do # Time.zone will not affect #in_time_zone(zone)
- assert_equal 'Sat, 01 Jan 2000 00:00:00 AKST -09:00', @s.in_time_zone('Alaska').inspect
- assert_equal 'Fri, 31 Dec 1999 15:00:00 AKST -09:00', @u.in_time_zone('Alaska').inspect
- assert_equal 'Fri, 31 Dec 1999 15:00:00 AKST -09:00', @z.in_time_zone('Alaska').inspect
- assert_equal 'Sat, 01 Jan 2000 00:00:00 HST -10:00', @s.in_time_zone('Hawaii').inspect
- assert_equal 'Fri, 31 Dec 1999 14:00:00 HST -10:00', @u.in_time_zone('Hawaii').inspect
- assert_equal 'Fri, 31 Dec 1999 14:00:00 HST -10:00', @z.in_time_zone('Hawaii').inspect
- assert_equal 'Sat, 01 Jan 2000 00:00:00 UTC +00:00', @s.in_time_zone('UTC').inspect
- assert_equal 'Sat, 01 Jan 2000 00:00:00 UTC +00:00', @u.in_time_zone('UTC').inspect
- assert_equal 'Sat, 01 Jan 2000 00:00:00 UTC +00:00', @z.in_time_zone('UTC').inspect
- assert_equal 'Sat, 01 Jan 2000 00:00:00 AKST -09:00', @s.in_time_zone(-9.hours).inspect
- assert_equal 'Fri, 31 Dec 1999 15:00:00 AKST -09:00', @u.in_time_zone(-9.hours).inspect
- assert_equal 'Fri, 31 Dec 1999 15:00:00 AKST -09:00', @z.in_time_zone(-9.hours).inspect
+ with_tz_default "Eastern Time (US & Canada)" do # Time.zone will not affect #in_time_zone(zone)
+ assert_equal "Sat, 01 Jan 2000 00:00:00 AKST -09:00", @s.in_time_zone("Alaska").inspect
+ assert_equal "Fri, 31 Dec 1999 15:00:00 AKST -09:00", @u.in_time_zone("Alaska").inspect
+ assert_equal "Fri, 31 Dec 1999 15:00:00 AKST -09:00", @z.in_time_zone("Alaska").inspect
+ assert_equal "Sat, 01 Jan 2000 00:00:00 HST -10:00", @s.in_time_zone("Hawaii").inspect
+ assert_equal "Fri, 31 Dec 1999 14:00:00 HST -10:00", @u.in_time_zone("Hawaii").inspect
+ assert_equal "Fri, 31 Dec 1999 14:00:00 HST -10:00", @z.in_time_zone("Hawaii").inspect
+ assert_equal "Sat, 01 Jan 2000 00:00:00 UTC +00:00", @s.in_time_zone("UTC").inspect
+ assert_equal "Sat, 01 Jan 2000 00:00:00 UTC +00:00", @u.in_time_zone("UTC").inspect
+ assert_equal "Sat, 01 Jan 2000 00:00:00 UTC +00:00", @z.in_time_zone("UTC").inspect
+ assert_equal "Sat, 01 Jan 2000 00:00:00 AKST -09:00", @s.in_time_zone(-9.hours).inspect
+ assert_equal "Fri, 31 Dec 1999 15:00:00 AKST -09:00", @u.in_time_zone(-9.hours).inspect
+ assert_equal "Fri, 31 Dec 1999 15:00:00 AKST -09:00", @z.in_time_zone(-9.hours).inspect
end
end
@@ -1173,4 +1320,10 @@ class TimeWithZoneMethodsForString < ActiveSupport::TestCase
assert_raise(ArgumentError) { @u.in_time_zone(Object.new) }
assert_raise(ArgumentError) { @z.in_time_zone(Object.new) }
end
+
+ def test_in_time_zone_with_ambiguous_time
+ with_tz_default "Moscow" do
+ assert_equal Time.utc(2014, 10, 25, 22, 0, 0), "2014-10-26 01:00:00".in_time_zone
+ end
+ end
end
diff --git a/activesupport/test/core_ext/uri_ext_test.rb b/activesupport/test/core_ext/uri_ext_test.rb
index 1694fe7e72..c0686bc720 100644
--- a/activesupport/test/core_ext/uri_ext_test.rb
+++ b/activesupport/test/core_ext/uri_ext_test.rb
@@ -1,12 +1,14 @@
-require 'abstract_unit'
-require 'uri'
-require 'active_support/core_ext/uri'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "uri"
+require "active_support/core_ext/uri"
class URIExtTest < ActiveSupport::TestCase
def test_uri_decode_handle_multibyte
str = "\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E" # Ni-ho-nn-go in UTF-8, means Japanese.
parser = URI.parser
- assert_equal str, parser.unescape(parser.escape(str))
+ assert_equal str + str, parser.unescape(str + parser.escape(str).encode(Encoding::UTF_8))
end
end
diff --git a/activesupport/test/current_attributes_test.rb b/activesupport/test/current_attributes_test.rb
new file mode 100644
index 0000000000..1669f08f68
--- /dev/null
+++ b/activesupport/test/current_attributes_test.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class CurrentAttributesTest < ActiveSupport::TestCase
+ Person = Struct.new(:name, :time_zone)
+
+ class Current < ActiveSupport::CurrentAttributes
+ attribute :world, :account, :person, :request
+ delegate :time_zone, to: :person
+
+ resets { Time.zone = "UTC" }
+
+ def account=(account)
+ super
+ self.person = "#{account}'s person"
+ end
+
+ def person=(person)
+ super
+ Time.zone = person.try(:time_zone)
+ end
+
+ def request
+ "#{super} something"
+ end
+
+ def intro
+ "#{person.name}, in #{time_zone}"
+ end
+ end
+
+ setup do
+ @original_time_zone = Time.zone
+ Current.reset
+ end
+
+ teardown do
+ Time.zone = @original_time_zone
+ end
+
+ test "read and write attribute" do
+ Current.world = "world/1"
+ assert_equal "world/1", Current.world
+ end
+
+ test "read overwritten attribute method" do
+ Current.request = "request/1"
+ assert_equal "request/1 something", Current.request
+ end
+
+ test "set attribute via overwritten method" do
+ Current.account = "account/1"
+ assert_equal "account/1", Current.account
+ assert_equal "account/1's person", Current.person
+ end
+
+ test "set auxiliary class via overwritten method" do
+ Current.person = Person.new("David", "Central Time (US & Canada)")
+ assert_equal "Central Time (US & Canada)", Time.zone.name
+ end
+
+ test "resets auxiliary class via callback" do
+ Current.person = Person.new("David", "Central Time (US & Canada)")
+ assert_equal "Central Time (US & Canada)", Time.zone.name
+
+ Current.reset
+ assert_equal "UTC", Time.zone.name
+ end
+
+ test "set attribute only via scope" do
+ Current.world = "world/1"
+
+ Current.set(world: "world/2") do
+ assert_equal "world/2", Current.world
+ end
+
+ assert_equal "world/1", Current.world
+ end
+
+ test "set multiple attributes" do
+ Current.world = "world/1"
+ Current.account = "account/1"
+
+ Current.set(world: "world/2", account: "account/2") do
+ assert_equal "world/2", Current.world
+ assert_equal "account/2", Current.account
+ end
+
+ assert_equal "world/1", Current.world
+ assert_equal "account/1", Current.account
+ end
+
+ test "delegation" do
+ Current.person = Person.new("David", "Central Time (US & Canada)")
+ assert_equal "Central Time (US & Canada)", Current.time_zone
+ assert_equal "Central Time (US & Canada)", Current.instance.time_zone
+ end
+
+ test "all methods forward to the instance" do
+ Current.person = Person.new("David", "Central Time (US & Canada)")
+ assert_equal "David, in Central Time (US & Canada)", Current.intro
+ assert_equal "David, in Central Time (US & Canada)", Current.instance.intro
+ end
+end
diff --git a/activesupport/test/dependencies/check_warnings.rb b/activesupport/test/dependencies/check_warnings.rb
index 03c3dca1d6..f7d7d2dbc7 100644
--- a/activesupport/test/dependencies/check_warnings.rb
+++ b/activesupport/test/dependencies/check_warnings.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
$check_warnings_load_count += 1
$checked_verbose = $VERBOSE
diff --git a/activesupport/test/dependencies/conflict.rb b/activesupport/test/dependencies/conflict.rb
index e888b7b54c..aa22ec0b09 100644
--- a/activesupport/test/dependencies/conflict.rb
+++ b/activesupport/test/dependencies/conflict.rb
@@ -1 +1,3 @@
-Conflict = 1 \ No newline at end of file
+# frozen_string_literal: true
+
+Conflict = 1
diff --git a/activesupport/test/dependencies/cross_site_depender.rb b/activesupport/test/dependencies/cross_site_depender.rb
index a31015fc5e..3ddea7fc4b 100644
--- a/activesupport/test/dependencies/cross_site_depender.rb
+++ b/activesupport/test/dependencies/cross_site_depender.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CrossSiteDepender
CrossSiteDependency
-end \ No newline at end of file
+end
diff --git a/activesupport/test/dependencies/module_folder/lib_class.rb b/activesupport/test/dependencies/module_folder/lib_class.rb
new file mode 100644
index 0000000000..c6b52610c1
--- /dev/null
+++ b/activesupport/test/dependencies/module_folder/lib_class.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+ConstFromLib = 1
+
+module ModuleFolder
+ class LibClass
+ end
+end
diff --git a/activesupport/test/dependencies/mutual_one.rb b/activesupport/test/dependencies/mutual_one.rb
index 576eb31711..bb48fddd4d 100644
--- a/activesupport/test/dependencies/mutual_one.rb
+++ b/activesupport/test/dependencies/mutual_one.rb
@@ -1,4 +1,6 @@
+# frozen_string_literal: true
+
$mutual_dependencies_count += 1
-require_dependency 'mutual_two'
-require_dependency 'mutual_two.rb'
-require_dependency 'mutual_two'
+require_dependency "mutual_two"
+require_dependency "mutual_two.rb"
+require_dependency "mutual_two"
diff --git a/activesupport/test/dependencies/mutual_two.rb b/activesupport/test/dependencies/mutual_two.rb
index fdbc2dcd84..ed354ed75e 100644
--- a/activesupport/test/dependencies/mutual_two.rb
+++ b/activesupport/test/dependencies/mutual_two.rb
@@ -1,4 +1,6 @@
+# frozen_string_literal: true
+
$mutual_dependencies_count += 1
-require_dependency 'mutual_one.rb'
-require_dependency 'mutual_one'
-require_dependency 'mutual_one.rb'
+require_dependency "mutual_one.rb"
+require_dependency "mutual_one"
+require_dependency "mutual_one.rb"
diff --git a/activesupport/test/dependencies/raises_exception.rb b/activesupport/test/dependencies/raises_exception.rb
index dd745ac20e..67bfaabb3e 100644
--- a/activesupport/test/dependencies/raises_exception.rb
+++ b/activesupport/test/dependencies/raises_exception.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
$raises_exception_load_count += 1
-raise Exception, 'Loading me failed, so do not add to loaded or history.'
+raise Exception, "Loading me failed, so do not add to loaded or history."
$raises_exception_load_count += 1
diff --git a/activesupport/test/dependencies/raises_exception_without_blame_file.rb b/activesupport/test/dependencies/raises_exception_without_blame_file.rb
index 4b2da6ff30..3a6533cd3a 100644
--- a/activesupport/test/dependencies/raises_exception_without_blame_file.rb
+++ b/activesupport/test/dependencies/raises_exception_without_blame_file.rb
@@ -1,4 +1,6 @@
-exception = Exception.new('I am not blamable!')
+# frozen_string_literal: true
+
+exception = Exception.new("I am not blamable!")
class << exception
undef_method(:blame_file!)
end
diff --git a/activesupport/test/dependencies/requires_nonexistent0.rb b/activesupport/test/dependencies/requires_nonexistent0.rb
index 7e24b3916c..d09dbd2485 100644
--- a/activesupport/test/dependencies/requires_nonexistent0.rb
+++ b/activesupport/test/dependencies/requires_nonexistent0.rb
@@ -1 +1,3 @@
-require 'RMagickDontExistDude'
+# frozen_string_literal: true
+
+require "RMagickDontExistDude"
diff --git a/activesupport/test/dependencies/requires_nonexistent1.rb b/activesupport/test/dependencies/requires_nonexistent1.rb
index 41e6668164..ce96229172 100644
--- a/activesupport/test/dependencies/requires_nonexistent1.rb
+++ b/activesupport/test/dependencies/requires_nonexistent1.rb
@@ -1 +1,3 @@
-require_dependency 'requires_nonexistent0'
+# frozen_string_literal: true
+
+require_dependency "requires_nonexistent0"
diff --git a/activesupport/test/dependencies/service_one.rb b/activesupport/test/dependencies/service_one.rb
index f43bfea235..2a4a39144d 100644
--- a/activesupport/test/dependencies/service_one.rb
+++ b/activesupport/test/dependencies/service_one.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
$loaded_service_one ||= 0
$loaded_service_one += 1
class ServiceOne
-end \ No newline at end of file
+end
diff --git a/activesupport/test/dependencies/service_two.rb b/activesupport/test/dependencies/service_two.rb
index 5205a78bb8..29cd73cbcd 100644
--- a/activesupport/test/dependencies/service_two.rb
+++ b/activesupport/test/dependencies/service_two.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
class ServiceTwo
-end \ No newline at end of file
+end
diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb
index 757e600646..9f2755a25c 100644
--- a/activesupport/test/dependencies_test.rb
+++ b/activesupport/test/dependencies_test.rb
@@ -1,7 +1,9 @@
-require 'abstract_unit'
-require 'pp'
-require 'active_support/dependencies'
-require 'dependencies_test_helpers'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "pp"
+require "active_support/dependencies"
+require "dependencies_test_helpers"
module ModuleWithMissing
mattr_accessor :missing_count
@@ -29,18 +31,18 @@ class DependenciesTest < ActiveSupport::TestCase
def test_depend_on_path
expected = assert_raises(LoadError) do
- Kernel.require 'omgwtfbbq'
+ Kernel.require "omgwtfbbq"
end
e = assert_raises(LoadError) do
- ActiveSupport::Dependencies.depend_on 'omgwtfbbq'
+ ActiveSupport::Dependencies.depend_on "omgwtfbbq"
end
assert_equal expected.path, e.path
end
def test_require_dependency_accepts_an_object_which_implements_to_path
o = Object.new
- def o.to_path; 'dependencies/service_one'; end
+ def o.to_path; "dependencies/service_one"; end
assert_nothing_raised {
require_dependency o
}
@@ -51,8 +53,8 @@ class DependenciesTest < ActiveSupport::TestCase
def test_tracking_loaded_files
with_loading do
- require_dependency 'dependencies/service_one'
- require_dependency 'dependencies/service_two'
+ require_dependency "dependencies/service_one"
+ require_dependency "dependencies/service_two"
assert_equal 2, ActiveSupport::Dependencies.loaded.size
end
ensure
@@ -61,8 +63,8 @@ class DependenciesTest < ActiveSupport::TestCase
def test_tracking_identical_loaded_files
with_loading do
- require_dependency 'dependencies/service_one'
- require_dependency 'dependencies/service_one'
+ require_dependency "dependencies/service_one"
+ require_dependency "dependencies/service_one"
assert_equal 1, ActiveSupport::Dependencies.loaded.size
end
ensure
@@ -75,35 +77,36 @@ class DependenciesTest < ActiveSupport::TestCase
def test_dependency_which_raises_exception_isnt_added_to_loaded_set
with_loading do
- filename = 'dependencies/raises_exception'
+ filename = "dependencies/raises_exception"
+ expanded = File.expand_path(filename)
$raises_exception_load_count = 0
5.times do |count|
- e = assert_raise Exception, 'should have loaded dependencies/raises_exception which raises an exception' do
+ e = assert_raise Exception, "should have loaded dependencies/raises_exception which raises an exception" do
require_dependency filename
end
- assert_equal 'Loading me failed, so do not add to loaded or history.', e.message
+ assert_equal "Loading me failed, so do not add to loaded or history.", e.message
assert_equal count + 1, $raises_exception_load_count
- assert_not ActiveSupport::Dependencies.loaded.include?(filename)
- assert_not ActiveSupport::Dependencies.history.include?(filename)
+ assert_not ActiveSupport::Dependencies.loaded.include?(expanded)
+ assert_not ActiveSupport::Dependencies.history.include?(expanded)
end
end
end
def test_dependency_which_raises_doesnt_blindly_call_blame_file!
with_loading do
- filename = 'dependencies/raises_exception_without_blame_file'
+ filename = "dependencies/raises_exception_without_blame_file"
assert_raises(Exception) { require_dependency filename }
end
end
def test_warnings_should_be_enabled_on_first_load
- with_loading 'dependencies' do
+ with_loading "dependencies" do
old_warnings, ActiveSupport::Dependencies.warnings_on_first_load = ActiveSupport::Dependencies.warnings_on_first_load, true
filename = "check_warnings"
- expanded = File.expand_path("#{File.dirname(__FILE__)}/dependencies/#{filename}")
+ expanded = File.expand_path("dependencies/#{filename}", __dir__)
$check_warnings_load_count = 0
assert_not ActiveSupport::Dependencies.loaded.include?(expanded)
@@ -111,41 +114,41 @@ class DependenciesTest < ActiveSupport::TestCase
silence_warnings { require_dependency filename }
assert_equal 1, $check_warnings_load_count
- assert_equal true, $checked_verbose, 'On first load warnings should be enabled.'
+ assert_equal true, $checked_verbose, "On first load warnings should be enabled."
- assert ActiveSupport::Dependencies.loaded.include?(expanded)
+ assert_includes ActiveSupport::Dependencies.loaded, expanded
ActiveSupport::Dependencies.clear
assert_not ActiveSupport::Dependencies.loaded.include?(expanded)
- assert ActiveSupport::Dependencies.history.include?(expanded)
+ assert_includes ActiveSupport::Dependencies.history, expanded
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 ActiveSupport::Dependencies.loaded.include?(expanded)
+ assert_includes ActiveSupport::Dependencies.loaded, expanded
ActiveSupport::Dependencies.clear
assert_not ActiveSupport::Dependencies.loaded.include?(expanded)
- assert ActiveSupport::Dependencies.history.include?(expanded)
+ assert_includes ActiveSupport::Dependencies.history, expanded
enable_warnings { require_dependency filename }
assert_equal 3, $check_warnings_load_count
- assert_equal true, $checked_verbose, 'After first load warnings should be left alone.'
+ assert_equal true, $checked_verbose, "After first load warnings should be left alone."
- assert ActiveSupport::Dependencies.loaded.include?(expanded)
+ assert_includes ActiveSupport::Dependencies.loaded, expanded
ActiveSupport::Dependencies.warnings_on_first_load = old_warnings
end
end
def test_mutual_dependencies_dont_infinite_loop
- with_loading 'dependencies' do
+ with_loading "dependencies" do
$mutual_dependencies_count = 0
- assert_nothing_raised { require_dependency 'mutual_one' }
+ assert_nothing_raised { require_dependency "mutual_one" }
assert_equal 2, $mutual_dependencies_count
ActiveSupport::Dependencies.clear
$mutual_dependencies_count = 0
- assert_nothing_raised { require_dependency 'mutual_two' }
+ assert_nothing_raised { require_dependency "mutual_two" }
assert_equal 2, $mutual_dependencies_count
end
end
@@ -166,7 +169,7 @@ class DependenciesTest < ActiveSupport::TestCase
def test_require_dependency_does_not_assume_any_particular_constant_is_defined
with_autoloading_fixtures do
- require_dependency 'typo'
+ require_dependency "typo"
assert_equal 1, TypO
end
end
@@ -174,7 +177,7 @@ class DependenciesTest < ActiveSupport::TestCase
# Regression, see https://github.com/rails/rails/issues/16468.
def test_require_dependency_interaction_with_autoloading
with_autoloading_fixtures do
- require_dependency 'typo'
+ require_dependency "typo"
assert_equal 1, TypO
e = assert_raise(LoadError) { Typo }
@@ -182,6 +185,61 @@ class DependenciesTest < ActiveSupport::TestCase
end
end
+ # Regression see https://github.com/rails/rails/issues/31694
+ def test_included_constant_that_changes_to_have_exception_then_back_does_not_loop_forever
+ # This constant references a nested constant whose namespace will be auto-generated
+ parent_constant = <<-RUBY
+ class ConstantReloadError
+ AnotherConstant::ReloadError
+ end
+ RUBY
+
+ # This constant's namespace will be auto-generated,
+ # also, we'll edit it to contain an error at load-time
+ child_constant = <<-RUBY
+ class AnotherConstant::ReloadError
+ # no_such_method_as_this
+ end
+ RUBY
+
+ # Create a version which contains an error during loading
+ child_constant_with_error = child_constant.sub("# no_such_method_as_this", "no_such_method_as_this")
+
+ fixtures_path = File.join(__dir__, "autoloading_fixtures")
+ Dir.mktmpdir(nil, fixtures_path) do |tmpdir|
+ # Set up the file structure where constants will be loaded from
+ child_constant_path = "#{tmpdir}/another_constant/reload_error.rb"
+ File.write("#{tmpdir}/constant_reload_error.rb", parent_constant)
+ Dir.mkdir("#{tmpdir}/another_constant")
+ File.write(child_constant_path, child_constant_with_error)
+
+ tmpdir_name = tmpdir.split("/").last
+ with_loading("autoloading_fixtures/#{tmpdir_name}") do
+ # Load the file, with the error:
+ assert_raises(NameError) {
+ ConstantReloadError
+ }
+
+ Timeout.timeout(0.1) do
+ # Remove the constant, as if Rails development middleware is reloading changed files:
+ ActiveSupport::Dependencies.remove_unloadable_constants!
+ assert_not defined?(AnotherConstant::ReloadError)
+ end
+
+ # Change the file, so that it is **correct** this time:
+ File.write(child_constant_path, child_constant)
+
+ # Again: Remove the constant, as if Rails development middleware is reloading changed files:
+ ActiveSupport::Dependencies.remove_unloadable_constants!
+ assert_not defined?(AnotherConstant::ReloadError)
+
+ # Now, reload the _fixed_ constant:
+ assert ConstantReloadError
+ assert AnotherConstant::ReloadError
+ end
+ end
+ end
+
def test_module_loading
with_autoloading_fixtures do
assert_kind_of Module, A
@@ -224,6 +282,32 @@ class DependenciesTest < ActiveSupport::TestCase
remove_constants(:ModuleFolder)
end
+ def test_module_with_nested_class_requiring_lib_class
+ with_autoloading_fixtures do
+ ModuleFolder::NestedWithRequire
+
+ assert defined?(ModuleFolder::LibClass)
+ assert_not ActiveSupport::Dependencies.autoloaded_constants.include?("ModuleFolder::LibClass")
+ assert_not ActiveSupport::Dependencies.autoloaded_constants.include?("ConstFromLib")
+ end
+ ensure
+ remove_constants(:ModuleFolder)
+ remove_constants(:ConstFromLib)
+ end
+
+ def test_module_with_nested_class_and_parent_requiring_lib_class
+ with_autoloading_fixtures do
+ NestedWithRequireParent
+
+ assert defined?(ModuleFolder::LibClass)
+ assert_not ActiveSupport::Dependencies.autoloaded_constants.include?("ModuleFolder::LibClass")
+ assert_not ActiveSupport::Dependencies.autoloaded_constants.include?("ConstFromLib")
+ end
+ ensure
+ remove_constants(:ModuleFolder)
+ remove_constants(:ConstFromLib)
+ end
+
def test_directories_may_manifest_as_nested_classes
with_autoloading_fixtures do
assert_kind_of Class, ClassFolder
@@ -252,7 +336,7 @@ class DependenciesTest < ActiveSupport::TestCase
with_autoloading_fixtures do
assert_kind_of Class, ClassFolder::ClassFolderSubclass
assert_kind_of Class, ClassFolder
- assert_equal 'indeed', ClassFolder::ClassFolderSubclass::ConstantInClassFolder
+ assert_equal "indeed", ClassFolder::ClassFolderSubclass::ConstantInClassFolder
end
ensure
remove_constants(:ClassFolder)
@@ -268,8 +352,31 @@ class DependenciesTest < ActiveSupport::TestCase
remove_constants(:ModuleFolder)
end
+ def test_raising_discards_autoloaded_constants
+ with_autoloading_fixtures do
+ e = assert_raises(Exception) { RaisesArbitraryException }
+ assert_equal("arbitrary exception message", e.message)
+ assert_not defined?(A)
+ assert_not defined?(RaisesArbitraryException)
+ end
+ ensure
+ remove_constants(:A, :RaisesArbitraryException)
+ end
+
+ def test_throwing_discards_autoloaded_constants
+ with_autoloading_fixtures do
+ catch :t do
+ Throws
+ end
+ assert_not defined?(A)
+ assert_not defined?(Throws)
+ end
+ ensure
+ remove_constants(:A, :Throws)
+ end
+
def test_doesnt_break_normal_require
- path = File.expand_path("../autoloading_fixtures/load_path", __FILE__)
+ path = File.expand_path("autoloading_fixtures/load_path", __dir__)
original_path = $:.dup
$:.push(path)
with_autoloading_fixtures do
@@ -288,7 +395,7 @@ class DependenciesTest < ActiveSupport::TestCase
end
def test_doesnt_break_normal_require_nested
- path = File.expand_path("../autoloading_fixtures/load_path", __FILE__)
+ path = File.expand_path("autoloading_fixtures/load_path", __dir__)
original_path = $:.dup
$:.push(path)
@@ -308,12 +415,12 @@ class DependenciesTest < ActiveSupport::TestCase
end
def test_require_returns_true_when_file_not_yet_required
- path = File.expand_path("../autoloading_fixtures/load_path", __FILE__)
+ path = File.expand_path("autoloading_fixtures/load_path", __dir__)
original_path = $:.dup
$:.push(path)
with_loading do
- assert_equal true, require('loaded_constant')
+ assert_equal true, require("loaded_constant")
end
ensure
remove_constants(:LoadedConstant)
@@ -321,13 +428,13 @@ class DependenciesTest < ActiveSupport::TestCase
end
def test_require_returns_true_when_file_not_yet_required_even_when_no_new_constants_added
- path = File.expand_path("../autoloading_fixtures/load_path", __FILE__)
+ path = File.expand_path("autoloading_fixtures/load_path", __dir__)
original_path = $:.dup
$:.push(path)
with_loading do
Object.module_eval "module LoadedConstant; end"
- assert_equal true, require('loaded_constant')
+ assert_equal true, require("loaded_constant")
end
ensure
remove_constants(:LoadedConstant)
@@ -335,13 +442,13 @@ class DependenciesTest < ActiveSupport::TestCase
end
def test_require_returns_false_when_file_already_required
- path = File.expand_path("../autoloading_fixtures/load_path", __FILE__)
+ path = File.expand_path("autoloading_fixtures/load_path", __dir__)
original_path = $:.dup
$:.push(path)
with_loading do
- require 'loaded_constant'
- assert_equal false, require('loaded_constant')
+ require "loaded_constant"
+ assert_equal false, require("loaded_constant")
end
ensure
remove_constants(:LoadedConstant)
@@ -350,18 +457,18 @@ class DependenciesTest < ActiveSupport::TestCase
def test_require_raises_load_error_when_file_not_found
with_loading do
- assert_raise(LoadError) { require 'this_file_dont_exist_dude' }
+ assert_raise(LoadError) { require "this_file_dont_exist_dude" }
end
end
def test_load_returns_true_when_file_found
- path = File.expand_path("../autoloading_fixtures/load_path", __FILE__)
+ path = File.expand_path("autoloading_fixtures/load_path", __dir__)
original_path = $:.dup
$:.push(path)
with_loading do
- assert_equal true, load('loaded_constant.rb')
- assert_equal true, load('loaded_constant.rb')
+ assert_equal true, load("loaded_constant.rb")
+ assert_equal true, load("loaded_constant.rb")
end
ensure
remove_constants(:LoadedConstant)
@@ -370,18 +477,21 @@ class DependenciesTest < ActiveSupport::TestCase
def test_load_raises_load_error_when_file_not_found
with_loading do
- assert_raise(LoadError) { load 'this_file_dont_exist_dude.rb' }
+ assert_raise(LoadError) { load "this_file_dont_exist_dude.rb" }
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
@@ -406,38 +516,38 @@ class DependenciesTest < ActiveSupport::TestCase
end
def test_loadable_constants_for_path_should_handle_empty_autoloads
- assert_equal [], ActiveSupport::Dependencies.loadable_constants_for_path('hello')
+ assert_equal [], ActiveSupport::Dependencies.loadable_constants_for_path("hello")
end
def test_loadable_constants_for_path_should_handle_relative_paths
- fake_root = 'dependencies'
- relative_root = File.dirname(__FILE__) + '/dependencies'
- ['', '/'].each do |suffix|
+ fake_root = "dependencies"
+ relative_root = File.expand_path("dependencies", __dir__)
+ ["", "/"].each do |suffix|
with_loading fake_root + suffix do
- assert_equal ["A::B"], ActiveSupport::Dependencies.loadable_constants_for_path(relative_root + '/a/b')
+ assert_equal ["A::B"], ActiveSupport::Dependencies.loadable_constants_for_path(relative_root + "/a/b")
end
end
end
def test_loadable_constants_for_path_should_provide_all_results
- fake_root = '/usr/apps/backpack'
- with_loading fake_root, fake_root + '/lib' do
+ fake_root = "/usr/apps/backpack"
+ with_loading fake_root, fake_root + "/lib" do
root = ActiveSupport::Dependencies.autoload_paths.first
- assert_equal ["Lib::A::B", "A::B"], ActiveSupport::Dependencies.loadable_constants_for_path(root + '/lib/a/b')
+ assert_equal ["Lib::A::B", "A::B"], ActiveSupport::Dependencies.loadable_constants_for_path(root + "/lib/a/b")
end
end
def test_loadable_constants_for_path_should_uniq_results
- fake_root = '/usr/apps/backpack/lib'
- with_loading fake_root, fake_root + '/' do
+ fake_root = "/usr/apps/backpack/lib"
+ with_loading fake_root, fake_root + "/" do
root = ActiveSupport::Dependencies.autoload_paths.first
- assert_equal ["A::B"], ActiveSupport::Dependencies.loadable_constants_for_path(root + '/a/b')
+ assert_equal ["A::B"], ActiveSupport::Dependencies.loadable_constants_for_path(root + "/a/b")
end
end
def test_loadable_constants_with_load_path_without_trailing_slash
- path = File.dirname(__FILE__) + '/autoloading_fixtures/class_folder/inline_class.rb'
- with_loading 'autoloading_fixtures/class/' do
+ path = File.expand_path("autoloading_fixtures/class_folder/inline_class.rb", __dir__)
+ with_loading "autoloading_fixtures/class/" do
assert_equal [], ActiveSupport::Dependencies.loadable_constants_for_path(path)
end
end
@@ -451,9 +561,9 @@ class DependenciesTest < ActiveSupport::TestCase
def test_qualified_const_defined_should_not_call_const_missing
ModuleWithMissing.missing_count = 0
- assert ! ActiveSupport::Dependencies.qualified_const_defined?("ModuleWithMissing::A")
+ assert_not ActiveSupport::Dependencies.qualified_const_defined?("ModuleWithMissing::A")
assert_equal 0, ModuleWithMissing.missing_count
- assert ! ActiveSupport::Dependencies.qualified_const_defined?("ModuleWithMissing::A::B")
+ assert_not ActiveSupport::Dependencies.qualified_const_defined?("ModuleWithMissing::A::B")
assert_equal 0, ModuleWithMissing.missing_count
end
@@ -463,13 +573,13 @@ class DependenciesTest < ActiveSupport::TestCase
def test_autoloaded?
with_autoloading_fixtures do
- assert ! ActiveSupport::Dependencies.autoloaded?("ModuleFolder")
- assert ! ActiveSupport::Dependencies.autoloaded?("ModuleFolder::NestedClass")
+ assert_not ActiveSupport::Dependencies.autoloaded?("ModuleFolder")
+ assert_not ActiveSupport::Dependencies.autoloaded?("ModuleFolder::NestedClass")
assert ActiveSupport::Dependencies.autoloaded?(ModuleFolder)
assert ActiveSupport::Dependencies.autoloaded?("ModuleFolder")
- assert ! ActiveSupport::Dependencies.autoloaded?("ModuleFolder::NestedClass")
+ assert_not ActiveSupport::Dependencies.autoloaded?("ModuleFolder::NestedClass")
assert ActiveSupport::Dependencies.autoloaded?(ModuleFolder::NestedClass)
@@ -480,11 +590,11 @@ class DependenciesTest < ActiveSupport::TestCase
assert ActiveSupport::Dependencies.autoloaded?(:ModuleFolder)
# Anonymous modules aren't autoloaded.
- assert !ActiveSupport::Dependencies.autoloaded?(Module.new)
+ assert_not ActiveSupport::Dependencies.autoloaded?(Module.new)
nil_name = Module.new
def nil_name.name() nil end
- assert !ActiveSupport::Dependencies.autoloaded?(nil_name)
+ assert_not ActiveSupport::Dependencies.autoloaded?(nil_name)
end
ensure
remove_constants(:ModuleFolder)
@@ -501,31 +611,30 @@ class DependenciesTest < ActiveSupport::TestCase
end
def test_file_search
- with_loading 'dependencies' do
+ 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_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')
+ 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
end
def test_file_search_uses_first_in_load_path
- with_loading 'dependencies', 'autoloading_fixtures' do
+ with_loading "dependencies", "autoloading_fixtures" do
deps, autoload = ActiveSupport::Dependencies.autoload_paths
assert_match %r/dependencies/, deps
assert_match %r/autoloading_fixtures/, autoload
- assert_equal deps + '/conflict.rb', ActiveSupport::Dependencies.search_for_file('conflict')
+ assert_equal deps + "/conflict.rb", ActiveSupport::Dependencies.search_for_file("conflict")
end
- with_loading 'autoloading_fixtures', 'dependencies' do
+ with_loading "autoloading_fixtures", "dependencies" do
autoload, deps = ActiveSupport::Dependencies.autoload_paths
assert_match %r/dependencies/, deps
assert_match %r/autoloading_fixtures/, autoload
- assert_equal autoload + '/conflict.rb', ActiveSupport::Dependencies.search_for_file('conflict')
+ assert_equal autoload + "/conflict.rb", ActiveSupport::Dependencies.search_for_file("conflict")
end
-
end
def test_custom_const_missing_should_work
@@ -561,11 +670,11 @@ class DependenciesTest < ActiveSupport::TestCase
def test_const_missing_in_anonymous_modules_raises_if_the_constant_belongs_to_Object
with_autoloading_fixtures do
- require_dependency 'em'
+ require_dependency "em"
mod = Module.new
e = assert_raise(NameError) { mod::EM }
- assert_equal 'EM cannot be autoloaded from an anonymous class or module', e.message
+ assert_equal "EM cannot be autoloaded from an anonymous class or module", e.message
assert_equal :EM, e.name
end
ensure
@@ -573,7 +682,7 @@ class DependenciesTest < ActiveSupport::TestCase
end
def test_removal_from_tree_should_be_detected
- with_loading 'dependencies' do
+ with_loading "dependencies" do
c = ServiceOne
ActiveSupport::Dependencies.clear
assert_not defined?(ServiceOne)
@@ -587,7 +696,7 @@ class DependenciesTest < ActiveSupport::TestCase
end
def test_references_should_work
- with_loading 'dependencies' do
+ with_loading "dependencies" do
c = ActiveSupport::Dependencies.reference("ServiceOne")
service_one_first = ServiceOne
assert_equal service_one_first, c.get("ServiceOne")
@@ -602,7 +711,7 @@ class DependenciesTest < ActiveSupport::TestCase
end
def test_constantize_shortcut_for_cached_constant_lookups
- with_loading 'dependencies' do
+ with_loading "dependencies" do
assert_equal ServiceOne, ActiveSupport::Dependencies.constantize("ServiceOne")
end
ensure
@@ -610,7 +719,7 @@ class DependenciesTest < ActiveSupport::TestCase
end
def test_nested_load_error_isnt_rescued
- with_loading 'dependencies' do
+ with_loading "dependencies" do
assert_raise(LoadError) do
RequiresNonexistent1
end
@@ -636,7 +745,7 @@ class DependenciesTest < ActiveSupport::TestCase
def test_autoload_once_pathnames_do_not_add_to_autoloaded_constants
with_autoloading_fixtures do
- pathnames = ActiveSupport::Dependencies.autoload_paths.collect{|p| Pathname.new(p)}
+ pathnames = ActiveSupport::Dependencies.autoload_paths.collect { |p| Pathname.new(p) }
ActiveSupport::Dependencies.autoload_paths = pathnames
ActiveSupport::Dependencies.autoload_once_paths = pathnames
@@ -654,7 +763,7 @@ class DependenciesTest < ActiveSupport::TestCase
def test_application_should_special_case_application_controller
with_autoloading_fixtures do
- require_dependency 'application'
+ require_dependency "application"
assert_equal 10, ApplicationController
assert ActiveSupport::Dependencies.autoloaded?(:ApplicationController)
end
@@ -664,15 +773,15 @@ class DependenciesTest < ActiveSupport::TestCase
def test_preexisting_constants_are_not_marked_as_autoloaded
with_autoloading_fixtures do
- require_dependency 'em'
+ require_dependency "em"
assert ActiveSupport::Dependencies.autoloaded?(:EM)
ActiveSupport::Dependencies.clear
end
Object.const_set :EM, Class.new
with_autoloading_fixtures do
- require_dependency 'em'
- assert ! ActiveSupport::Dependencies.autoloaded?(:EM), "EM shouldn't be marked autoloaded!"
+ require_dependency "em"
+ assert_not ActiveSupport::Dependencies.autoloaded?(:EM), "EM shouldn't be marked autoloaded!"
ActiveSupport::Dependencies.clear
end
ensure
@@ -695,11 +804,11 @@ class DependenciesTest < ActiveSupport::TestCase
M.unloadable
ActiveSupport::Dependencies.clear
- assert ! defined?(M)
+ assert_not defined?(M)
Object.const_set :M, Module.new
ActiveSupport::Dependencies.clear
- assert ! defined?(M), "Dependencies should unload unloadable constants each time"
+ assert_not defined?(M), "Dependencies should unload unloadable constants each time"
end
end
@@ -724,9 +833,9 @@ class DependenciesTest < ActiveSupport::TestCase
Object.const_set :C, Class.new { def self.before_remove_const; end }
C.unloadable
assert_called(C, :before_remove_const, times: 1) do
- assert C.respond_to?(:before_remove_const)
+ assert_respond_to C, :before_remove_const
ActiveSupport::Dependencies.clear
- assert !defined?(C)
+ assert_not defined?(C)
end
ensure
remove_constants(:C)
@@ -734,14 +843,14 @@ class DependenciesTest < ActiveSupport::TestCase
def test_new_contants_in_without_constants
assert_equal [], (ActiveSupport::Dependencies.new_constants_in(Object) { })
- assert ActiveSupport::Dependencies.constant_watch_stack.all? {|k,v| v.empty? }
+ assert ActiveSupport::Dependencies.constant_watch_stack.all? { |k, v| v.empty? }
end
def test_new_constants_in_with_a_single_constant
assert_equal ["Hello"], ActiveSupport::Dependencies.new_constants_in(Object) {
Object.const_set :Hello, 10
}.map(&:to_s)
- assert ActiveSupport::Dependencies.constant_watch_stack.all? {|k,v| v.empty? }
+ assert ActiveSupport::Dependencies.constant_watch_stack.all? { |k, v| v.empty? }
ensure
remove_constants(:Hello)
end
@@ -758,7 +867,7 @@ class DependenciesTest < ActiveSupport::TestCase
end
assert_equal ["OuterAfter", "OuterBefore"], outer.sort.map(&:to_s)
- assert ActiveSupport::Dependencies.constant_watch_stack.all? {|k,v| v.empty? }
+ assert ActiveSupport::Dependencies.constant_watch_stack.all? { |k, v| v.empty? }
ensure
remove_constants(:OuterBefore, :Inner, :OuterAfter)
end
@@ -777,7 +886,7 @@ class DependenciesTest < ActiveSupport::TestCase
M.const_set :OuterAfter, 30
end
assert_equal ["M::OuterAfter", "M::OuterBefore"], outer.sort
- assert ActiveSupport::Dependencies.constant_watch_stack.all? {|k,v| v.empty? }
+ assert ActiveSupport::Dependencies.constant_watch_stack.all? { |k, v| v.empty? }
ensure
remove_constants(:M)
end
@@ -795,7 +904,7 @@ class DependenciesTest < ActiveSupport::TestCase
M.const_set :OuterAfter, 30
end
assert_equal ["M::OuterAfter", "M::OuterBefore"], outer.sort
- assert ActiveSupport::Dependencies.constant_watch_stack.all? {|k,v| v.empty? }
+ assert ActiveSupport::Dependencies.constant_watch_stack.all? { |k, v| v.empty? }
ensure
remove_constants(:M)
end
@@ -809,7 +918,7 @@ class DependenciesTest < ActiveSupport::TestCase
def test_new_constants_in_with_illegal_module_name_raises_correct_error
assert_raise(NameError) do
- ActiveSupport::Dependencies.new_constants_in("Illegal-Name") {}
+ ActiveSupport::Dependencies.new_constants_in("Illegal-Name") { }
end
end
@@ -818,7 +927,7 @@ class DependenciesTest < ActiveSupport::TestCase
assert_not defined?(MultipleConstantFile)
assert_not defined?(SiblingConstant)
- require_dependency 'multiple_constant_file'
+ require_dependency "multiple_constant_file"
assert defined?(MultipleConstantFile)
assert defined?(SiblingConstant)
assert ActiveSupport::Dependencies.autoloaded?(:MultipleConstantFile)
@@ -858,7 +967,7 @@ class DependenciesTest < ActiveSupport::TestCase
assert_not defined?(ClassFolder::NestedClass)
assert_not defined?(ClassFolder::SiblingClass)
- require_dependency 'class_folder/nested_class'
+ require_dependency "class_folder/nested_class"
assert defined?(ClassFolder::NestedClass)
assert defined?(ClassFolder::SiblingClass)
@@ -897,10 +1006,10 @@ class DependenciesTest < ActiveSupport::TestCase
def test_autoload_doesnt_shadow_no_method_error_with_relative_constant
with_autoloading_fixtures do
- assert !defined?(::RaisesNoMethodError), "::RaisesNoMethodError is defined but it hasn't been referenced yet!"
+ assert_not defined?(::RaisesNoMethodError), "::RaisesNoMethodError is defined but it hasn't been referenced yet!"
2.times do
assert_raise(NoMethodError) { RaisesNoMethodError }
- assert !defined?(::RaisesNoMethodError), "::RaisesNoMethodError is defined but it should have failed!"
+ assert_not defined?(::RaisesNoMethodError), "::RaisesNoMethodError is defined but it should have failed!"
end
end
ensure
@@ -909,10 +1018,10 @@ class DependenciesTest < ActiveSupport::TestCase
def test_autoload_doesnt_shadow_no_method_error_with_absolute_constant
with_autoloading_fixtures do
- assert !defined?(::RaisesNoMethodError), "::RaisesNoMethodError is defined but it hasn't been referenced yet!"
+ assert_not defined?(::RaisesNoMethodError), "::RaisesNoMethodError is defined but it hasn't been referenced yet!"
2.times do
assert_raise(NoMethodError) { ::RaisesNoMethodError }
- assert !defined?(::RaisesNoMethodError), "::RaisesNoMethodError is defined but it should have failed!"
+ assert_not defined?(::RaisesNoMethodError), "::RaisesNoMethodError is defined but it should have failed!"
end
end
ensure
@@ -936,14 +1045,14 @@ class DependenciesTest < ActiveSupport::TestCase
e = assert_raise NameError do
::RaisesNameError::FooBarBaz.object_id
end
- assert_equal 'uninitialized constant RaisesNameError::FooBarBaz', e.message
- assert !defined?(::RaisesNameError), "::RaisesNameError is defined but it should have failed!"
+ assert_equal "uninitialized constant RaisesNameError::FooBarBaz", e.message
+ assert_not defined?(::RaisesNameError), "::RaisesNameError is defined but it should have failed!"
end
- assert !defined?(::RaisesNameError)
+ assert_not defined?(::RaisesNameError)
2.times do
assert_raise(NameError) { ::RaisesNameError }
- assert !defined?(::RaisesNameError), "::RaisesNameError is defined but it should have failed!"
+ assert_not defined?(::RaisesNameError), "::RaisesNameError is defined but it should have failed!"
end
end
ensure
@@ -951,8 +1060,8 @@ class DependenciesTest < ActiveSupport::TestCase
end
def test_remove_constant_handles_double_colon_at_start
- Object.const_set 'DeleteMe', Module.new
- DeleteMe.const_set 'OrMe', Module.new
+ Object.const_set "DeleteMe", Module.new
+ DeleteMe.const_set "OrMe", Module.new
ActiveSupport::Dependencies.remove_constant "::DeleteMe::OrMe"
assert_not defined?(DeleteMe::OrMe)
assert defined?(DeleteMe)
@@ -963,9 +1072,9 @@ class DependenciesTest < ActiveSupport::TestCase
end
def test_remove_constant_does_not_trigger_loading_autoloads
- constant = 'ShouldNotBeAutoloaded'
+ constant = "ShouldNotBeAutoloaded"
Object.class_eval do
- autoload constant, File.expand_path('../autoloading_fixtures/should_not_be_required', __FILE__)
+ autoload constant, File.expand_path("autoloading_fixtures/should_not_be_required", __dir__)
end
assert_nil ActiveSupport::Dependencies.remove_constant(constant), "Kernel#autoload has been triggered by remove_constant"
@@ -978,8 +1087,8 @@ class DependenciesTest < ActiveSupport::TestCase
with_autoloading_fixtures do
_ = ::A # assignment to silence parse-time warning "possibly useless use of :: in void context"
_ = ::A::B # assignment to silence parse-time warning "possibly useless use of :: in void context"
- ActiveSupport::Dependencies.remove_constant('A')
- ActiveSupport::Dependencies.remove_constant('A::B')
+ ActiveSupport::Dependencies.remove_constant("A")
+ ActiveSupport::Dependencies.remove_constant("A::B")
assert_not defined?(A)
end
ensure
@@ -1013,10 +1122,9 @@ class DependenciesTest < ActiveSupport::TestCase
remove_constants(:A)
end
-
def test_autoload_once_paths_should_behave_when_recursively_loading
old_path = ActiveSupport::Dependencies.autoload_once_paths
- with_loading 'dependencies', 'autoloading_fixtures' do
+ with_loading "dependencies", "autoloading_fixtures" do
ActiveSupport::Dependencies.autoload_once_paths = [ActiveSupport::Dependencies.autoload_paths.last]
assert_not defined?(CrossSiteDependency)
assert_nothing_raised { CrossSiteDepender.nil? }
@@ -1037,22 +1145,63 @@ class DependenciesTest < ActiveSupport::TestCase
end
def test_load_and_require_stay_private
- assert Object.private_methods.include?(:load)
- assert Object.private_methods.include?(:require)
+ assert_includes Object.private_methods, :load
+ assert_includes Object.private_methods, :require
ActiveSupport::Dependencies.unhook!
- assert Object.private_methods.include?(:load)
- assert Object.private_methods.include?(:require)
+ assert_includes Object.private_methods, :load
+ assert_includes Object.private_methods, :require
ensure
ActiveSupport::Dependencies.hook!
end
+end
- def test_unhook
- ActiveSupport::Dependencies.unhook!
- assert !Module.new.respond_to?(:const_missing_without_dependencies)
- assert !Module.new.respond_to?(:load_without_new_constant_marking)
+class DependenciesLogging < ActiveSupport::TestCase
+ MESSAGE = "message"
+
+ def with_settings(logger, verbose)
+ original_logger = ActiveSupport::Dependencies.logger
+ original_verbose = ActiveSupport::Dependencies.verbose
+
+ ActiveSupport::Dependencies.logger = logger
+ ActiveSupport::Dependencies.verbose = verbose
+
+ yield
ensure
- ActiveSupport::Dependencies.hook!
+ ActiveSupport::Dependencies.logger = original_logger
+ ActiveSupport::Dependencies.verbose = original_verbose
+ end
+
+ def fake_logger
+ Class.new do
+ def self.debug(message)
+ message
+ end
+ end
+ end
+
+ test "does not log if the logger is nil and verbose is false" do
+ with_settings(nil, false) do
+ assert_nil ActiveSupport::Dependencies.log(MESSAGE)
+ end
+ end
+
+ test "does not log if the logger is nil and verbose is true" do
+ with_settings(nil, true) do
+ assert_nil ActiveSupport::Dependencies.log(MESSAGE)
+ end
+ end
+
+ test "does not log if the logger is set and verbose is false" do
+ with_settings(fake_logger, false) do
+ assert_nil ActiveSupport::Dependencies.log(MESSAGE)
+ end
+ end
+
+ test "logs if the logger is set and verbose is true" do
+ with_settings(fake_logger, true) do
+ assert_equal "autoloading: #{MESSAGE}", ActiveSupport::Dependencies.log(MESSAGE)
+ end
end
end
diff --git a/activesupport/test/dependencies_test_helpers.rb b/activesupport/test/dependencies_test_helpers.rb
index e4d5197112..b54a7e70c8 100644
--- a/activesupport/test/dependencies_test_helpers.rb
+++ b/activesupport/test/dependencies_test_helpers.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
module DependenciesTestHelpers
def with_loading(*from)
old_mechanism, ActiveSupport::Dependencies.mechanism = ActiveSupport::Dependencies.mechanism, :load
- this_dir = File.dirname(__FILE__)
+ this_dir = __dir__
parent_dir = File.dirname(this_dir)
path_copy = $LOAD_PATH.dup
$LOAD_PATH.unshift(parent_dir) unless $LOAD_PATH.include?(parent_dir)
@@ -17,7 +19,7 @@ module DependenciesTestHelpers
end
def with_autoloading_fixtures(&block)
- with_loading 'autoloading_fixtures', &block
+ with_loading "autoloading_fixtures", &block
end
def remove_constants(*constants)
@@ -25,4 +27,4 @@ module DependenciesTestHelpers
Object.send(:remove_const, constant) if Object.const_defined?(constant)
end
end
-end \ No newline at end of file
+end
diff --git a/activesupport/test/deprecation/method_wrappers_test.rb b/activesupport/test/deprecation/method_wrappers_test.rb
new file mode 100644
index 0000000000..b04bce7a11
--- /dev/null
+++ b/activesupport/test/deprecation/method_wrappers_test.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/deprecation"
+
+class MethodWrappersTest < ActiveSupport::TestCase
+ def setup
+ @klass = Class.new do
+ def new_method; "abc" end
+ alias_method :old_method, :new_method
+
+ protected
+
+ def new_protected_method; "abc" end
+ alias_method :old_protected_method, :new_protected_method
+
+ private
+
+ def new_private_method; "abc" end
+ alias_method :old_private_method, :new_private_method
+ end
+ end
+
+ def test_deprecate_methods_warning_default
+ warning = /old_method is deprecated and will be removed from Rails \d.\d \(use new_method instead\)/
+ ActiveSupport::Deprecation.deprecate_methods(@klass, old_method: :new_method)
+
+ assert_deprecated(warning) { assert_equal "abc", @klass.new.old_method }
+ end
+
+ def test_deprecate_methods_warning_with_optional_deprecator
+ warning = /old_method is deprecated and will be removed from MyGem next-release \(use new_method instead\)/
+ deprecator = ActiveSupport::Deprecation.new("next-release", "MyGem")
+ ActiveSupport::Deprecation.deprecate_methods(@klass, old_method: :new_method, deprecator: deprecator)
+
+ assert_deprecated(warning, deprecator) { assert_equal "abc", @klass.new.old_method }
+ end
+
+ def test_deprecate_methods_warning_when_deprecated_with_custom_deprecator
+ warning = /old_method is deprecated and will be removed from MyGem next-release \(use new_method instead\)/
+ deprecator = ActiveSupport::Deprecation.new("next-release", "MyGem")
+ deprecator.deprecate_methods(@klass, old_method: :new_method)
+
+ assert_deprecated(warning, deprecator) { assert_equal "abc", @klass.new.old_method }
+ end
+
+ def test_deprecate_methods_protected_method
+ ActiveSupport::Deprecation.deprecate_methods(@klass, old_protected_method: :new_protected_method)
+
+ assert(@klass.protected_method_defined?(:old_protected_method))
+ end
+
+ def test_deprecate_methods_private_method
+ ActiveSupport::Deprecation.deprecate_methods(@klass, old_private_method: :new_private_method)
+
+ assert(@klass.private_method_defined?(:old_private_method))
+ end
+
+ def test_deprecate_class_method
+ mod = Module.new do
+ extend self
+
+ def old_method
+ "abc"
+ end
+ end
+ ActiveSupport::Deprecation.deprecate_methods(mod, old_method: :new_method)
+
+ warning = /old_method is deprecated and will be removed from Rails \d.\d \(use new_method instead\)/
+ assert_deprecated(warning) { assert_equal "abc", mod.old_method }
+ end
+
+ def test_deprecate_method_when_class_extends_module
+ mod = Module.new do
+ def old_method
+ "abc"
+ end
+ end
+ @klass.extend mod
+ ActiveSupport::Deprecation.deprecate_methods(mod, old_method: :new_method)
+
+ warning = /old_method is deprecated and will be removed from Rails \d.\d \(use new_method instead\)/
+ assert_deprecated(warning) { assert_equal "abc", @klass.old_method }
+ end
+
+ def test_method_with_without_deprecation_is_exposed
+ ActiveSupport::Deprecation.deprecate_methods(@klass, old_method: :new_method)
+
+ warning = /old_method is deprecated and will be removed from Rails \d.\d \(use new_method instead\)/
+ assert_deprecated(warning) { assert_equal "abc", @klass.new.old_method_with_deprecation }
+ assert_equal "abc", @klass.new.old_method_without_deprecation
+ end
+end
diff --git a/activesupport/test/deprecation/proxy_wrappers_test.rb b/activesupport/test/deprecation/proxy_wrappers_test.rb
index e4f0f0f7c2..9e26052fb4 100644
--- a/activesupport/test/deprecation/proxy_wrappers_test.rb
+++ b/activesupport/test/deprecation/proxy_wrappers_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'active_support/deprecation'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/deprecation"
class ProxyWrappersTest < ActiveSupport::TestCase
Waffles = false
@@ -7,16 +9,16 @@ class ProxyWrappersTest < ActiveSupport::TestCase
def test_deprecated_object_proxy_doesnt_wrap_falsy_objects
proxy = ActiveSupport::Deprecation::DeprecatedObjectProxy.new(nil, "message")
- assert !proxy
+ assert_not proxy
end
def test_deprecated_instance_variable_proxy_doesnt_wrap_falsy_objects
proxy = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(nil, :waffles)
- assert !proxy
+ assert_not proxy
end
def test_deprecated_constant_proxy_doesnt_wrap_falsy_objects
proxy = ActiveSupport::Deprecation::DeprecatedConstantProxy.new(Waffles, NewWaffles)
- assert !proxy
+ assert_not proxy
end
end
diff --git a/activesupport/test/deprecation_test.rb b/activesupport/test/deprecation_test.rb
index 7e8844b301..105153584d 100644
--- a/activesupport/test/deprecation_test.rb
+++ b/activesupport/test/deprecation_test.rb
@@ -1,22 +1,24 @@
-require 'abstract_unit'
-require 'active_support/testing/stream'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/testing/stream"
class Deprecatee
def initialize
@request = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(self, :request)
- @_request = 'there we go'
+ @_request = "there we go"
end
def request; @_request end
def old_request; @request end
def partially(foo = nil)
- ActiveSupport::Deprecation.warn('calling with foo=nil is out') if foo.nil?
+ ActiveSupport::Deprecation.warn("calling with foo=nil is out") if foo.nil?
end
def not() 2 end
def none() 1 end
def one(a) a end
- def multi(a,b,c) [a,b,c] end
+ def multi(a, b, c) [a, b, c] end
deprecate :none, :one, :multi
def a; end
@@ -24,7 +26,7 @@ class Deprecatee
def c; end
def d; end
def e; end
- deprecate :a, :b, :c => :e, :d => "you now need to do something extra for this one"
+ deprecate :a, :b, c: :e, d: "you now need to do something extra for this one"
def f=(v); end
deprecate :f=
@@ -32,9 +34,20 @@ class Deprecatee
module B
C = 1
end
- A = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Deprecatee::A', 'Deprecatee::B::C')
+ 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
@@ -74,12 +87,12 @@ class DeprecationTest < ActiveSupport::TestCase
end
assert_deprecated(/multi is deprecated/) do
- assert_equal [1,2,3], @dtc.multi(1,2,3)
+ assert_equal [1, 2, 3], @dtc.multi(1, 2, 3)
end
end
def test_deprecate_object
- deprecated_object = ActiveSupport::Deprecation::DeprecatedObjectProxy.new(Object.new, ':bomb:')
+ deprecated_object = ActiveSupport::Deprecation::DeprecatedObjectProxy.new(Object.new, ":bomb:")
assert_deprecated(/:bomb:/) { deprecated_object.to_s }
end
@@ -89,29 +102,31 @@ class DeprecationTest < ActiveSupport::TestCase
end
def test_several_behaviors
- @a, @b = nil, nil
+ @a, @b, @c = nil, nil, nil
ActiveSupport::Deprecation.behavior = [
- Proc.new { |msg, callstack| @a = msg },
- Proc.new { |msg, callstack| @b = msg }
+ lambda { |msg, callstack, horizon, gem| @a = msg },
+ lambda { |msg, callstack| @b = msg },
+ lambda { |*args| @c = args },
]
@dtc.partially
assert_match(/foo=nil/, @a)
assert_match(/foo=nil/, @b)
+ assert_equal 4, @c.size
end
def test_raise_behaviour
ActiveSupport::Deprecation.behavior = :raise
- message = 'Revise this deprecated stuff now!'
- callstack = %w(foo bar baz)
+ message = "Revise this deprecated stuff now!"
+ callstack = caller_locations
e = assert_raise ActiveSupport::DeprecationException do
- ActiveSupport::Deprecation.behavior.first.call(message, callstack)
+ ActiveSupport::Deprecation.behavior.first.call(message, callstack, "horizon", "gem")
end
assert_equal message, e.message
- assert_equal callstack, e.backtrace
+ assert_equal callstack.map(&:to_s), e.backtrace.map(&:to_s)
end
def test_default_stderr_behavior
@@ -119,7 +134,7 @@ class DeprecationTest < ActiveSupport::TestCase
behavior = ActiveSupport::Deprecation.behavior.first
content = capture(:stderr) {
- assert_nil behavior.call('Some error!', ['call stack!'])
+ assert_nil behavior.call("Some error!", ["call stack!"], "horizon", "gem")
}
assert_match(/Some error!/, content)
assert_match(/call stack!/, content)
@@ -129,7 +144,7 @@ class DeprecationTest < ActiveSupport::TestCase
ActiveSupport::Deprecation.behavior = :stderr
content = capture(:stderr) {
- ActiveSupport::Deprecation.warn('Instance error!', ['instance call stack!'])
+ ActiveSupport::Deprecation.warn("Instance error!", ["instance call stack!"])
}
assert_match(/Instance error!/, content)
@@ -141,16 +156,45 @@ class DeprecationTest < ActiveSupport::TestCase
behavior = ActiveSupport::Deprecation.behavior.first
stderr_output = capture(:stderr) {
- assert_nil behavior.call('Some error!', ['call stack!'])
+ assert_nil behavior.call("Some error!", ["call stack!"], "horizon", "gem")
}
- assert stderr_output.blank?
+ assert_empty stderr_output
+ end
+
+ def test_default_notify_behavior
+ ActiveSupport::Deprecation.behavior = :notify
+ behavior = ActiveSupport::Deprecation.behavior.first
+
+ begin
+ events = []
+ ActiveSupport::Notifications.subscribe("deprecation.my_gem_custom") { |_, **args|
+ events << args
+ }
+
+ assert_nil behavior.call("Some error!", ["call stack!"], "horizon", "MyGem::Custom")
+ assert_equal 1, events.size
+ assert_equal "Some error!", events.first[:message]
+ assert_equal ["call stack!"], events.first[:callstack]
+ assert_equal "horizon", events.first[:deprecation_horizon]
+ assert_equal "MyGem::Custom", events.first[:gem_name]
+ ensure
+ ActiveSupport::Notifications.unsubscribe("deprecation.my_gem_custom")
+ end
+ end
+
+ def test_default_invalid_behavior
+ e = assert_raises(ArgumentError) do
+ ActiveSupport::Deprecation.behavior = :invalid
+ end
+
+ assert_equal ":invalid is not a valid deprecation behavior.", e.message
end
def test_deprecated_instance_variable_proxy
assert_not_deprecated { @dtc.request.size }
- assert_deprecated('@request.size') { assert_equal @dtc.request.size, @dtc.old_request.size }
- assert_deprecated('@request.to_s') { assert_equal @dtc.request.to_s, @dtc.old_request.to_s }
+ assert_deprecated("@request.size") { assert_equal @dtc.request.size, @dtc.old_request.size }
+ assert_deprecated("@request.to_s") { assert_equal @dtc.request.to_s, @dtc.old_request.to_s }
end
def test_deprecated_instance_variable_proxy_shouldnt_warn_on_inspect
@@ -159,10 +203,29 @@ class DeprecationTest < ActiveSupport::TestCase
def test_deprecated_constant_proxy
assert_not_deprecated { Deprecatee::B::C }
- assert_deprecated('Deprecatee::A') { assert_equal Deprecatee::B::C, Deprecatee::A }
+ assert_deprecated("Deprecatee::A") { assert_equal Deprecatee::B::C, Deprecatee::A }
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
+
+ def test_assert_not_deprecated
+ assert_raises(Minitest::Assertion) { assert_not_deprecated { @dtc.partially } }
+ end
+
def test_assert_deprecation_without_match
assert_deprecated do
@dtc.partially
@@ -170,12 +233,12 @@ class DeprecationTest < ActiveSupport::TestCase
end
def test_assert_deprecated_matches_any_warning
- assert_deprecated 'abc' do
- ActiveSupport::Deprecation.warn 'abc'
- ActiveSupport::Deprecation.warn 'def'
+ assert_deprecated "abc" do
+ ActiveSupport::Deprecation.warn "abc"
+ ActiveSupport::Deprecation.warn "def"
end
rescue Minitest::Assertion
- flunk 'assert_deprecated should match any warning in block, not just the last one'
+ flunk "assert_deprecated should match any warning in block, not just the last one"
end
def test_assert_not_deprecated_returns_result_of_block
@@ -183,17 +246,17 @@ class DeprecationTest < ActiveSupport::TestCase
end
def test_assert_deprecated_returns_result_of_block
- result = assert_deprecated('abc') do
- ActiveSupport::Deprecation.warn 'abc'
+ result = assert_deprecated("abc") do
+ ActiveSupport::Deprecation.warn "abc"
123
end
assert_equal 123, result
end
def test_assert_deprecated_warn_work_with_default_behavior
- ActiveSupport::Deprecation.instance_variable_set('@behavior' , nil)
- assert_deprecated('abc') do
- ActiveSupport::Deprecation.warn 'abc'
+ ActiveSupport::Deprecation.instance_variable_set("@behavior", nil)
+ assert_deprecated("abc") do
+ ActiveSupport::Deprecation.warn "abc"
end
end
@@ -272,12 +335,22 @@ class DeprecationTest < ActiveSupport::TestCase
def test_deprecated_constant_with_deprecator_given
deprecator = deprecator_with_messages
klass = Class.new
- klass.const_set(:OLD, ActiveSupport::Deprecation::DeprecatedConstantProxy.new('klass::OLD', 'Object', deprecator) )
+ klass.const_set(:OLD, ActiveSupport::Deprecation::DeprecatedConstantProxy.new("klass::OLD", "Object", deprecator))
assert_difference("deprecator.messages.size") do
klass::OLD.to_s
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
@@ -332,6 +405,10 @@ class DeprecationTest < ActiveSupport::TestCase
assert_match(/You are calling deprecated method/, object.last_message)
end
+ def test_default_deprecation_horizon_should_always_bigger_than_current_rails_version
+ assert_operator ActiveSupport::Deprecation.new.deprecation_horizon, :>, ActiveSupport::VERSION::STRING
+ end
+
def test_default_gem_name
deprecator = ActiveSupport::Deprecation.new
@@ -341,7 +418,7 @@ class DeprecationTest < ActiveSupport::TestCase
end
def test_custom_gem_name
- deprecator = ActiveSupport::Deprecation.new('2.0', 'Custom')
+ deprecator = ActiveSupport::Deprecation.new("2.0", "Custom")
deprecator.send(:deprecated_method_warning, :deprecated_method, "You are calling deprecated method").tap do |message|
assert_match(/is deprecated and will be removed from Custom/, message)
@@ -352,11 +429,10 @@ class DeprecationTest < ActiveSupport::TestCase
def deprecator_with_messages
klass = Class.new(ActiveSupport::Deprecation)
deprecator = klass.new
- deprecator.behavior = Proc.new{|message, callstack| deprecator.messages << message}
+ deprecator.behavior = Proc.new { |message, callstack| deprecator.messages << message }
def deprecator.messages
@messages ||= []
end
deprecator
end
-
end
diff --git a/activesupport/test/descendants_tracker_test_cases.rb b/activesupport/test/descendants_tracker_test_cases.rb
index 69e046998e..2c94c3c56c 100644
--- a/activesupport/test/descendants_tracker_test_cases.rb
+++ b/activesupport/test/descendants_tracker_test_cases.rb
@@ -1,4 +1,6 @@
-require 'set'
+# frozen_string_literal: true
+
+require "set"
module DescendantsTrackerTestCases
class Parent
@@ -35,31 +37,31 @@ module DescendantsTrackerTestCases
mark_as_autoloaded(*ALL) do
ActiveSupport::DescendantsTracker.clear
ALL.each do |k|
- assert ActiveSupport::DescendantsTracker.descendants(k).empty?
+ assert_empty ActiveSupport::DescendantsTracker.descendants(k)
end
end
end
- protected
-
- def assert_equal_sets(expected, actual)
- assert_equal Set.new(expected), Set.new(actual)
- end
+ private
- def mark_as_autoloaded(*klasses)
- # If ActiveSupport::Dependencies is not loaded, forget about autoloading.
- # This allows using AS::DescendantsTracker without AS::Dependencies.
- if defined? ActiveSupport::Dependencies
- old_autoloaded = ActiveSupport::Dependencies.autoloaded_constants.dup
- ActiveSupport::Dependencies.autoloaded_constants = klasses.map(&:name)
+ def assert_equal_sets(expected, actual)
+ assert_equal Set.new(expected), Set.new(actual)
end
- old_descendants = ActiveSupport::DescendantsTracker.class_eval("@@direct_descendants").dup
- old_descendants.each { |k, v| old_descendants[k] = v.dup }
+ def mark_as_autoloaded(*klasses)
+ # If ActiveSupport::Dependencies is not loaded, forget about autoloading.
+ # This allows using AS::DescendantsTracker without AS::Dependencies.
+ if defined? ActiveSupport::Dependencies
+ old_autoloaded = ActiveSupport::Dependencies.autoloaded_constants.dup
+ ActiveSupport::Dependencies.autoloaded_constants = klasses.map(&:name)
+ end
- yield
- ensure
- ActiveSupport::Dependencies.autoloaded_constants = old_autoloaded if defined? ActiveSupport::Dependencies
- ActiveSupport::DescendantsTracker.class_eval("@@direct_descendants").replace(old_descendants)
- end
+ old_descendants = ActiveSupport::DescendantsTracker.class_eval("@@direct_descendants").dup
+ old_descendants.each { |k, v| old_descendants[k] = v.dup }
+
+ yield
+ ensure
+ ActiveSupport::Dependencies.autoloaded_constants = old_autoloaded if defined? ActiveSupport::Dependencies
+ ActiveSupport::DescendantsTracker.class_eval("@@direct_descendants").replace(old_descendants)
+ end
end
diff --git a/activesupport/test/descendants_tracker_with_autoloading_test.rb b/activesupport/test/descendants_tracker_with_autoloading_test.rb
index a2ae066a21..d4fedb5a67 100644
--- a/activesupport/test/descendants_tracker_with_autoloading_test.rb
+++ b/activesupport/test/descendants_tracker_with_autoloading_test.rb
@@ -1,7 +1,9 @@
-require 'abstract_unit'
-require 'active_support/descendants_tracker'
-require 'active_support/dependencies'
-require 'descendants_tracker_test_cases'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/descendants_tracker"
+require "active_support/dependencies"
+require "descendants_tracker_test_cases"
class DescendantsTrackerWithAutoloadingTest < ActiveSupport::TestCase
include DescendantsTrackerTestCases
@@ -10,7 +12,7 @@ class DescendantsTrackerWithAutoloadingTest < ActiveSupport::TestCase
mark_as_autoloaded(*ALL) do
ActiveSupport::DescendantsTracker.clear
ALL.each do |k|
- assert ActiveSupport::DescendantsTracker.descendants(k).empty?
+ assert_empty ActiveSupport::DescendantsTracker.descendants(k)
end
end
end
diff --git a/activesupport/test/descendants_tracker_without_autoloading_test.rb b/activesupport/test/descendants_tracker_without_autoloading_test.rb
index 00b449af51..c65f69cba3 100644
--- a/activesupport/test/descendants_tracker_without_autoloading_test.rb
+++ b/activesupport/test/descendants_tracker_without_autoloading_test.rb
@@ -1,6 +1,8 @@
-require 'abstract_unit'
-require 'active_support/descendants_tracker'
-require 'descendants_tracker_test_cases'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/descendants_tracker"
+require "descendants_tracker_test_cases"
class DescendantsTrackerWithoutAutoloadingTest < ActiveSupport::TestCase
include DescendantsTrackerTestCases
@@ -11,7 +13,7 @@ class DescendantsTrackerWithoutAutoloadingTest < ActiveSupport::TestCase
parent_instance = Parent.new
parent_instance.singleton_class.descendants
ActiveSupport::DescendantsTracker.clear
- assert !ActiveSupport::DescendantsTracker.class_variable_get(:@@direct_descendants).key?(parent_instance.singleton_class)
+ assert_not ActiveSupport::DescendantsTracker.class_variable_get(:@@direct_descendants).key?(parent_instance.singleton_class)
end
end
end
diff --git a/activesupport/test/digest_test.rb b/activesupport/test/digest_test.rb
new file mode 100644
index 0000000000..83ff2a8d83
--- /dev/null
+++ b/activesupport/test/digest_test.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "openssl"
+
+class DigestTest < ActiveSupport::TestCase
+ class InvalidDigest; end
+ def test_with_default_hash_digest_class
+ assert_equal ::Digest::MD5.hexdigest("hello friend"), ActiveSupport::Digest.hexdigest("hello friend")
+ end
+
+ def test_with_custom_hash_digest_class
+ original_hash_digest_class = ActiveSupport::Digest.hash_digest_class
+
+ ActiveSupport::Digest.hash_digest_class = ::Digest::SHA1
+ digest = ActiveSupport::Digest.hexdigest("hello friend")
+
+ assert_equal 32, digest.length
+ assert_equal ::Digest::SHA1.hexdigest("hello friend")[0...32], digest
+ ensure
+ ActiveSupport::Digest.hash_digest_class = original_hash_digest_class
+ end
+
+ def test_should_raise_argument_error_if_custom_digest_is_missing_hexdigest_method
+ assert_raises(ArgumentError) { ActiveSupport::Digest.hash_digest_class = InvalidDigest }
+ end
+end
diff --git a/activesupport/test/encrypted_configuration_test.rb b/activesupport/test/encrypted_configuration_test.rb
new file mode 100644
index 0000000000..387d6e1c1f
--- /dev/null
+++ b/activesupport/test/encrypted_configuration_test.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/encrypted_configuration"
+
+class EncryptedConfigurationTest < ActiveSupport::TestCase
+ setup do
+ @credentials_config_path = File.join(Dir.tmpdir, "credentials.yml.enc")
+
+ @credentials_key_path = File.join(Dir.tmpdir, "master.key")
+ File.write(@credentials_key_path, ActiveSupport::EncryptedConfiguration.generate_key)
+
+ @credentials = ActiveSupport::EncryptedConfiguration.new(
+ config_path: @credentials_config_path, key_path: @credentials_key_path,
+ env_key: "RAILS_MASTER_KEY", raise_if_missing_key: true
+ )
+ end
+
+ teardown do
+ FileUtils.rm_rf @credentials_config_path
+ FileUtils.rm_rf @credentials_key_path
+ end
+
+ test "reading configuration by env key" do
+ FileUtils.rm_rf @credentials_key_path
+
+ begin
+ ENV["RAILS_MASTER_KEY"] = ActiveSupport::EncryptedConfiguration.generate_key
+ @credentials.write({ something: { good: true, bad: false } }.to_yaml)
+
+ assert @credentials[:something][:good]
+ assert_not @credentials.dig(:something, :bad)
+ assert_nil @credentials.fetch(:nothing, nil)
+ ensure
+ ENV["RAILS_MASTER_KEY"] = nil
+ end
+ end
+
+ test "reading configuration by key file" do
+ @credentials.write({ something: { good: true } }.to_yaml)
+
+ assert @credentials.something[:good]
+ end
+
+ test "reading comment-only configuration" do
+ @credentials.write("# comment")
+
+ assert_equal @credentials.config, {}
+ end
+
+ test "change configuration by key file" do
+ @credentials.write({ something: { good: true } }.to_yaml)
+ @credentials.change do |config_file|
+ config = YAML.load(config_file.read)
+ config_file.write config.merge(new: "things").to_yaml
+ end
+
+ assert @credentials.something[:good]
+ assert_equal "things", @credentials[:new]
+ end
+
+ test "raise error when writing an invalid format value" do
+ assert_raise(Psych::SyntaxError) do
+ @credentials.change do |config_file|
+ config_file.write "login: *login\n username: dummy"
+ end
+ end
+ end
+
+ test "raises key error when accessing config via bang method" do
+ assert_raise(KeyError) { @credentials.something! }
+ end
+end
diff --git a/activesupport/test/encrypted_file_test.rb b/activesupport/test/encrypted_file_test.rb
new file mode 100644
index 0000000000..ba3bbef903
--- /dev/null
+++ b/activesupport/test/encrypted_file_test.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/encrypted_file"
+
+class EncryptedFileTest < ActiveSupport::TestCase
+ setup do
+ @content = "One little fox jumped over the hedge"
+
+ @content_path = File.join(Dir.tmpdir, "content.txt.enc")
+
+ @key_path = File.join(Dir.tmpdir, "content.txt.key")
+ File.write(@key_path, ActiveSupport::EncryptedFile.generate_key)
+
+ @encrypted_file = ActiveSupport::EncryptedFile.new(
+ content_path: @content_path, key_path: @key_path, env_key: "CONTENT_KEY", raise_if_missing_key: true
+ )
+ end
+
+ teardown do
+ FileUtils.rm_rf @content_path
+ FileUtils.rm_rf @key_path
+ end
+
+ test "reading content by env key" do
+ FileUtils.rm_rf @key_path
+
+ begin
+ ENV["CONTENT_KEY"] = ActiveSupport::EncryptedFile.generate_key
+ @encrypted_file.write @content
+
+ assert_equal @content, @encrypted_file.read
+ ensure
+ ENV["CONTENT_KEY"] = nil
+ end
+ end
+
+ test "reading content by key file" do
+ @encrypted_file.write(@content)
+ assert_equal @content, @encrypted_file.read
+ end
+
+ test "change content by key file" do
+ @encrypted_file.write(@content)
+ @encrypted_file.change do |file|
+ file.write(file.read + " and went by the lake")
+ end
+
+ assert_equal "#{@content} and went by the lake", @encrypted_file.read
+ end
+
+ test "raise MissingKeyError when key is missing" do
+ assert_raise(ActiveSupport::EncryptedFile::MissingKeyError) do
+ ActiveSupport::EncryptedFile.new(
+ content_path: @content_path, key_path: "", env_key: "", raise_if_missing_key: true
+ ).read
+ end
+ end
+end
diff --git a/activesupport/test/evented_file_update_checker_test.rb b/activesupport/test/evented_file_update_checker_test.rb
new file mode 100644
index 0000000000..a557608986
--- /dev/null
+++ b/activesupport/test/evented_file_update_checker_test.rb
@@ -0,0 +1,197 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "pathname"
+require "file_update_checker_shared_tests"
+
+class EventedFileUpdateCheckerTest < ActiveSupport::TestCase
+ include FileUpdateCheckerSharedTests
+
+ def setup
+ skip if ENV["LISTEN"] == "0"
+ require "listen"
+ super
+ end
+
+ def new_checker(files = [], dirs = {}, &block)
+ ActiveSupport::EventedFileUpdateChecker.new(files, dirs, &block).tap do |c|
+ wait
+ end
+ end
+
+ def teardown
+ super
+ Listen.stop
+ end
+
+ def wait
+ sleep 1
+ end
+
+ def touch(files)
+ super
+ wait # wait for the events to fire
+ end
+
+ test "notifies forked processes" do
+ jruby_skip "Forking not available on JRuby"
+
+ FileUtils.touch(tmpfiles)
+
+ checker = new_checker(tmpfiles) { }
+ assert_not_predicate checker, :updated?
+
+ # Pipes used for flow control across fork.
+ boot_reader, boot_writer = IO.pipe
+ touch_reader, touch_writer = IO.pipe
+
+ pid = fork do
+ assert_predicate checker, :updated?
+
+ # Clear previous check value.
+ checker.execute
+ assert_not_predicate checker, :updated?
+
+ # Fork is booted, ready for file to be touched
+ # notify parent process.
+ boot_writer.write("booted")
+
+ # Wait for parent process to signal that file
+ # has been touched.
+ IO.select([touch_reader])
+
+ assert_predicate checker, :updated?
+ end
+
+ assert pid
+
+ # Wait for fork to be booted before touching files.
+ IO.select([boot_reader])
+ touch(tmpfiles)
+
+ # Notify fork that files have been touched.
+ touch_writer.write("touched")
+
+ assert_predicate checker, :updated?
+
+ Process.wait(pid)
+ end
+end
+
+class EventedFileUpdateCheckerPathHelperTest < ActiveSupport::TestCase
+ def pn(path)
+ Pathname.new(path)
+ end
+
+ setup do
+ @ph = ActiveSupport::EventedFileUpdateChecker::PathHelper.new
+ end
+
+ test "#xpath returns the expanded path as a Pathname object" do
+ assert_equal pn(__FILE__).expand_path, @ph.xpath(__FILE__)
+ end
+
+ test "#normalize_extension returns a bare extension as is" do
+ assert_equal "rb", @ph.normalize_extension("rb")
+ end
+
+ test "#normalize_extension removes a leading dot" do
+ assert_equal "rb", @ph.normalize_extension(".rb")
+ end
+
+ test "#normalize_extension supports symbols" do
+ assert_equal "rb", @ph.normalize_extension(:rb)
+ end
+
+ test "#longest_common_subpath finds the longest common subpath, if there is one" do
+ paths = %w(
+ /foo/bar
+ /foo/baz
+ /foo/bar/baz/woo/zoo
+ ).map { |path| pn(path) }
+
+ assert_equal pn("/foo"), @ph.longest_common_subpath(paths)
+ end
+
+ test "#longest_common_subpath returns the root directory as an edge case" do
+ paths = %w(
+ /foo/bar
+ /foo/baz
+ /foo/bar/baz/woo/zoo
+ /wadus
+ ).map { |path| pn(path) }
+
+ assert_equal pn("/"), @ph.longest_common_subpath(paths)
+ end
+
+ test "#longest_common_subpath returns nil for an empty collection" do
+ assert_nil @ph.longest_common_subpath([])
+ end
+
+ test "#existing_parent returns the most specific existing ascendant" do
+ wd = Pathname.getwd
+
+ assert_equal wd, @ph.existing_parent(wd)
+ assert_equal wd, @ph.existing_parent(wd.join("non-existing/directory"))
+ assert_equal pn("/"), @ph.existing_parent(pn("/non-existing/directory"))
+ end
+
+ test "#filter_out_descendants returns the same collection if there are no descendants (empty)" do
+ assert_equal [], @ph.filter_out_descendants([])
+ end
+
+ test "#filter_out_descendants returns the same collection if there are no descendants (one)" do
+ assert_equal ["/foo"], @ph.filter_out_descendants(["/foo"])
+ end
+
+ test "#filter_out_descendants returns the same collection if there are no descendants (several)" do
+ paths = %w(
+ /Rails.root/app/controllers
+ /Rails.root/app/models
+ /Rails.root/app/helpers
+ ).map { |path| pn(path) }
+
+ assert_equal paths, @ph.filter_out_descendants(paths)
+ end
+
+ test "#filter_out_descendants filters out descendants preserving order" do
+ paths = %w(
+ /Rails.root/app/controllers
+ /Rails.root/app/controllers/concerns
+ /Rails.root/app/models
+ /Rails.root/app/models/concerns
+ /Rails.root/app/helpers
+ ).map { |path| pn(path) }
+
+ assert_equal paths.values_at(0, 2, 4), @ph.filter_out_descendants(paths)
+ end
+
+ test "#filter_out_descendants works on path units" do
+ paths = %w(
+ /foo/bar
+ /foo/barrrr
+ ).map { |path| pn(path) }
+
+ assert_equal paths, @ph.filter_out_descendants(paths)
+ end
+
+ test "#filter_out_descendants deals correctly with the root directory" do
+ paths = %w(
+ /
+ /foo
+ /foo/bar
+ ).map { |path| pn(path) }
+
+ assert_equal paths.values_at(0), @ph.filter_out_descendants(paths)
+ end
+
+ test "#filter_out_descendants preserves duplicates" do
+ paths = %w(
+ /foo
+ /foo/bar
+ /foo
+ ).map { |path| pn(path) }
+
+ assert_equal paths.values_at(0, 2), @ph.filter_out_descendants(paths)
+ end
+end
diff --git a/activesupport/test/executor_test.rb b/activesupport/test/executor_test.rb
new file mode 100644
index 0000000000..3026f002c3
--- /dev/null
+++ b/activesupport/test/executor_test.rb
@@ -0,0 +1,242 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class ExecutorTest < ActiveSupport::TestCase
+ class DummyError < RuntimeError
+ end
+
+ def test_wrap_invokes_callbacks
+ called = []
+ executor.to_run { called << :run }
+ executor.to_complete { called << :complete }
+
+ executor.wrap do
+ called << :body
+ end
+
+ assert_equal [:run, :body, :complete], called
+ end
+
+ def test_callbacks_share_state
+ result = false
+ executor.to_run { @foo = true }
+ executor.to_complete { result = @foo }
+
+ executor.wrap { }
+
+ assert result
+ end
+
+ def test_separated_calls_invoke_callbacks
+ called = []
+ executor.to_run { called << :run }
+ executor.to_complete { called << :complete }
+
+ state = executor.run!
+ called << :body
+ state.complete!
+
+ assert_equal [:run, :body, :complete], called
+ end
+
+ def test_exceptions_unwind
+ called = []
+ executor.to_run { called << :run_1 }
+ executor.to_run { raise DummyError }
+ executor.to_run { called << :run_2 }
+ executor.to_complete { called << :complete }
+
+ assert_raises(DummyError) do
+ executor.wrap { called << :body }
+ end
+
+ assert_equal [:run_1, :complete], called
+ end
+
+ def test_avoids_double_wrapping
+ called = []
+ executor.to_run { called << :run }
+ executor.to_complete { called << :complete }
+
+ executor.wrap do
+ called << :early
+ executor.wrap do
+ called << :body
+ end
+ called << :late
+ end
+
+ assert_equal [:run, :early, :body, :late, :complete], called
+ end
+
+ def test_hooks_carry_state
+ supplied_state = :none
+
+ hook = Class.new do
+ define_method(:run) do
+ :some_state
+ end
+
+ define_method(:complete) do |state|
+ supplied_state = state
+ end
+ end.new
+
+ executor.register_hook(hook)
+
+ executor.wrap { }
+
+ assert_equal :some_state, supplied_state
+ end
+
+ def test_nil_state_is_sufficient
+ supplied_state = :none
+
+ hook = Class.new do
+ define_method(:run) do
+ nil
+ end
+
+ define_method(:complete) do |state|
+ supplied_state = state
+ end
+ end.new
+
+ executor.register_hook(hook)
+
+ executor.wrap { }
+
+ assert_nil supplied_state
+ end
+
+ def test_exception_skips_uninvoked_hook
+ supplied_state = :none
+
+ hook = Class.new do
+ define_method(:run) do
+ :some_state
+ end
+
+ define_method(:complete) do |state|
+ supplied_state = state
+ end
+ end.new
+
+ executor.to_run do
+ raise DummyError
+ end
+ executor.register_hook(hook)
+
+ assert_raises(DummyError) do
+ executor.wrap { }
+ end
+
+ assert_equal :none, supplied_state
+ end
+
+ def test_exception_unwinds_invoked_hook
+ supplied_state = :none
+
+ hook = Class.new do
+ define_method(:run) do
+ :some_state
+ end
+
+ define_method(:complete) do |state|
+ supplied_state = state
+ end
+ end.new
+
+ executor.register_hook(hook)
+ executor.to_run do
+ raise DummyError
+ end
+
+ assert_raises(DummyError) do
+ executor.wrap { }
+ end
+
+ assert_equal :some_state, supplied_state
+ end
+
+ def test_hook_insertion_order
+ invoked = []
+ supplied_state = []
+
+ hook_class = Class.new do
+ attr_accessor :letter
+
+ define_method(:initialize) do |letter|
+ self.letter = letter
+ end
+
+ define_method(:run) do
+ invoked << :"run_#{letter}"
+ :"state_#{letter}"
+ end
+
+ define_method(:complete) do |state|
+ invoked << :"complete_#{letter}"
+ supplied_state << state
+ end
+ end
+
+ executor.register_hook(hook_class.new(:a))
+ executor.register_hook(hook_class.new(:b))
+ executor.register_hook(hook_class.new(:c), outer: true)
+ executor.register_hook(hook_class.new(:d))
+
+ executor.wrap { }
+
+ assert_equal [:run_c, :run_a, :run_b, :run_d, :complete_a, :complete_b, :complete_d, :complete_c], invoked
+ assert_equal [:state_a, :state_b, :state_d, :state_c], supplied_state
+ end
+
+ def test_class_serial_is_unaffected
+ skip if !defined?(RubyVM)
+
+ hook = Class.new do
+ define_method(:run) do
+ nil
+ end
+
+ define_method(:complete) do |state|
+ nil
+ end
+ end.new
+
+ executor.register_hook(hook)
+
+ before = RubyVM.stat(:class_serial)
+ executor.wrap { }
+ executor.wrap { }
+ executor.wrap { }
+ after = RubyVM.stat(:class_serial)
+
+ assert_equal before, after
+ end
+
+ def test_separate_classes_can_wrap
+ other_executor = Class.new(ActiveSupport::Executor)
+
+ called = []
+ executor.to_run { called << :run }
+ executor.to_complete { called << :complete }
+ other_executor.to_run { called << :other_run }
+ other_executor.to_complete { called << :other_complete }
+
+ executor.wrap do
+ other_executor.wrap do
+ called << :body
+ end
+ end
+
+ assert_equal [:run, :other_run, :body, :other_complete, :complete], called
+ end
+
+ private
+ def executor
+ @executor ||= Class.new(ActiveSupport::Executor)
+ end
+end
diff --git a/activesupport/test/file_update_checker_shared_tests.rb b/activesupport/test/file_update_checker_shared_tests.rb
new file mode 100644
index 0000000000..72683816b3
--- /dev/null
+++ b/activesupport/test/file_update_checker_shared_tests.rb
@@ -0,0 +1,284 @@
+# frozen_string_literal: true
+
+require "fileutils"
+
+module FileUpdateCheckerSharedTests
+ extend ActiveSupport::Testing::Declarative
+ include FileUtils
+
+ def tmpdir
+ @tmpdir
+ end
+
+ def tmpfile(name)
+ File.join(tmpdir, name)
+ end
+
+ def tmpfiles
+ @tmpfiles ||= %w(foo.rb bar.rb baz.rb).map { |f| tmpfile(f) }
+ end
+
+ def run(*args)
+ capture_exceptions do
+ Dir.mktmpdir(nil, __dir__) { |dir| @tmpdir = dir; super }
+ end
+ end
+
+ test "should not execute the block if no paths are given" do
+ silence_warnings { require "listen" }
+ i = 0
+
+ checker = new_checker { i += 1 }
+
+ assert_not checker.execute_if_updated
+ assert_equal 0, i
+ end
+
+ test "should not execute the block if no files change" do
+ i = 0
+
+ FileUtils.touch(tmpfiles)
+
+ checker = new_checker(tmpfiles) { i += 1 }
+
+ assert_not checker.execute_if_updated
+ assert_equal 0, i
+ end
+
+ test "should execute the block once when files are created" do
+ i = 0
+
+ checker = new_checker(tmpfiles) { i += 1 }
+
+ touch(tmpfiles)
+ wait
+
+ assert checker.execute_if_updated
+ assert_equal 1, i
+ end
+
+ test "should execute the block once when files are modified" do
+ i = 0
+
+ FileUtils.touch(tmpfiles)
+
+ checker = new_checker(tmpfiles) { i += 1 }
+
+ touch(tmpfiles)
+ wait
+
+ assert checker.execute_if_updated
+ assert_equal 1, i
+ end
+
+ test "should execute the block once when files are deleted" do
+ i = 0
+
+ FileUtils.touch(tmpfiles)
+
+ checker = new_checker(tmpfiles) { i += 1 }
+
+ rm_f(tmpfiles)
+ wait
+
+ assert checker.execute_if_updated
+ assert_equal 1, i
+ end
+
+ test "updated should become true when watched files are created" do
+ i = 0
+
+ checker = new_checker(tmpfiles) { i += 1 }
+ assert_not_predicate checker, :updated?
+
+ touch(tmpfiles)
+ wait
+
+ assert_predicate checker, :updated?
+ end
+
+ test "updated should become true when watched files are modified" do
+ i = 0
+
+ FileUtils.touch(tmpfiles)
+
+ checker = new_checker(tmpfiles) { i += 1 }
+ assert_not_predicate checker, :updated?
+
+ touch(tmpfiles)
+ wait
+
+ assert_predicate checker, :updated?
+ end
+
+ test "updated should become true when watched files are deleted" do
+ i = 0
+
+ FileUtils.touch(tmpfiles)
+
+ checker = new_checker(tmpfiles) { i += 1 }
+ assert_not_predicate checker, :updated?
+
+ rm_f(tmpfiles)
+ wait
+
+ assert_predicate checker, :updated?
+ end
+
+ test "should be robust to handle files with wrong modified time" do
+ i = 0
+
+ FileUtils.touch(tmpfiles)
+
+ now = Time.now
+ time = Time.mktime(now.year + 1, now.month, now.day) # wrong mtime from the future
+ File.utime(time, time, tmpfiles[0])
+
+ checker = new_checker(tmpfiles) { i += 1 }
+
+ touch(tmpfiles[1..-1])
+ wait
+
+ assert checker.execute_if_updated
+ assert_equal 1, i
+ end
+
+ test "should return max_time for files with mtime = Time.at(0)" do
+ i = 0
+
+ FileUtils.touch(tmpfiles)
+
+ time = Time.at(0) # wrong mtime from the future
+ File.utime(time, time, tmpfiles[0])
+
+ checker = new_checker(tmpfiles) { i += 1 }
+
+ touch(tmpfiles[1..-1])
+ wait
+
+ assert checker.execute_if_updated
+ assert_equal 1, i
+ end
+
+ test "should cache updated result until execute" do
+ i = 0
+
+ checker = new_checker(tmpfiles) { i += 1 }
+ assert_not_predicate checker, :updated?
+
+ touch(tmpfiles)
+ wait
+
+ assert_predicate checker, :updated?
+ checker.execute
+ assert_not_predicate checker, :updated?
+ end
+
+ test "should execute the block if files change in a watched directory one extension" do
+ i = 0
+
+ checker = new_checker([], tmpdir => :rb) { i += 1 }
+
+ touch(tmpfile("foo.rb"))
+ wait
+
+ assert checker.execute_if_updated
+ assert_equal 1, i
+ end
+
+ test "should execute the block if files change in a watched directory several extensions" do
+ i = 0
+
+ checker = new_checker([], tmpdir => [:rb, :txt]) { i += 1 }
+
+ touch(tmpfile("foo.rb"))
+ wait
+
+ assert checker.execute_if_updated
+ assert_equal 1, i
+
+ touch(tmpfile("foo.txt"))
+ wait
+
+ assert checker.execute_if_updated
+ assert_equal 2, i
+ end
+
+ test "should not execute the block if the file extension is not watched" do
+ i = 0
+
+ checker = new_checker([], tmpdir => :txt) { i += 1 }
+
+ touch(tmpfile("foo.rb"))
+ wait
+
+ assert_not checker.execute_if_updated
+ assert_equal 0, i
+ end
+
+ test "does not assume files exist on instantiation" do
+ i = 0
+
+ non_existing = tmpfile("non_existing.rb")
+ checker = new_checker([non_existing]) { i += 1 }
+
+ touch(non_existing)
+ wait
+
+ assert checker.execute_if_updated
+ assert_equal 1, i
+ end
+
+ test "detects files in new subdirectories" do
+ i = 0
+
+ checker = new_checker([], tmpdir => :rb) { i += 1 }
+
+ subdir = tmpfile("subdir")
+ mkdir(subdir)
+ wait
+
+ assert_not checker.execute_if_updated
+ assert_equal 0, i
+
+ touch(File.join(subdir, "nested.rb"))
+ wait
+
+ assert checker.execute_if_updated
+ assert_equal 1, i
+ end
+
+ test "looked up extensions are inherited in subdirectories not listening to them" do
+ i = 0
+
+ subdir = tmpfile("subdir")
+ mkdir(subdir)
+
+ checker = new_checker([], tmpdir => :rb, subdir => :txt) { i += 1 }
+
+ touch(tmpfile("new.txt"))
+ wait
+
+ assert_not checker.execute_if_updated
+ assert_equal 0, i
+
+ # subdir does not look for Ruby files, but its parent tmpdir does.
+ touch(File.join(subdir, "nested.rb"))
+ wait
+
+ assert checker.execute_if_updated
+ assert_equal 1, i
+
+ touch(File.join(subdir, "nested.txt"))
+ wait
+
+ 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/file_update_checker_test.rb b/activesupport/test/file_update_checker_test.rb
index bd1df0f858..ec1df0df67 100644
--- a/activesupport/test/file_update_checker_test.rb
+++ b/activesupport/test/file_update_checker_test.rb
@@ -1,112 +1,21 @@
-require 'abstract_unit'
-require 'fileutils'
-require 'thread'
+# frozen_string_literal: true
-MTIME_FIXTURES_PATH = File.expand_path("../fixtures", __FILE__)
+require "abstract_unit"
+require "file_update_checker_shared_tests"
-class FileUpdateCheckerWithEnumerableTest < ActiveSupport::TestCase
- FILES = %w(1.txt 2.txt 3.txt)
+class FileUpdateCheckerTest < ActiveSupport::TestCase
+ include FileUpdateCheckerSharedTests
- def setup
- FileUtils.mkdir_p("tmp_watcher")
- FileUtils.touch(FILES)
+ def new_checker(files = [], dirs = {}, &block)
+ ActiveSupport::FileUpdateChecker.new(files, dirs, &block)
end
- def teardown
- FileUtils.rm_rf("tmp_watcher")
- FileUtils.rm_rf(FILES)
+ def wait
+ # noop
end
- def test_should_not_execute_the_block_if_no_paths_are_given
- i = 0
- checker = ActiveSupport::FileUpdateChecker.new([]){ i += 1 }
- checker.execute_if_updated
- assert_equal 0, i
- end
-
- def test_should_not_invoke_the_block_if_no_file_has_changed
- i = 0
- checker = ActiveSupport::FileUpdateChecker.new(FILES){ i += 1 }
- 5.times { assert !checker.execute_if_updated }
- assert_equal 0, i
- end
-
- def test_should_invoke_the_block_if_a_file_has_changed
- i = 0
- checker = ActiveSupport::FileUpdateChecker.new(FILES){ i += 1 }
- sleep(1)
- FileUtils.touch(FILES)
- assert checker.execute_if_updated
- assert_equal 1, i
- end
-
- def test_should_be_robust_enough_to_handle_deleted_files
- i = 0
- checker = ActiveSupport::FileUpdateChecker.new(FILES){ i += 1 }
- FileUtils.rm(FILES)
- assert checker.execute_if_updated
- assert_equal 1, i
- end
-
- def test_should_be_robust_to_handle_files_with_wrong_modified_time
- i = 0
- now = Time.now
- time = Time.mktime(now.year + 1, now.month, now.day) # wrong mtime from the future
- File.utime time, time, FILES[2]
-
- checker = ActiveSupport::FileUpdateChecker.new(FILES){ i += 1 }
-
- sleep(1)
- FileUtils.touch(FILES[0..1])
-
- assert checker.execute_if_updated
- assert_equal 1, i
- end
-
- def test_should_cache_updated_result_until_execute
- i = 0
- checker = ActiveSupport::FileUpdateChecker.new(FILES){ i += 1 }
- assert !checker.updated?
-
- sleep(1)
- FileUtils.touch(FILES)
-
- assert checker.updated?
- checker.execute
- assert !checker.updated?
- end
-
- def test_should_invoke_the_block_if_a_watched_dir_changed_its_glob
- i = 0
- checker = ActiveSupport::FileUpdateChecker.new([], "tmp_watcher" => [:txt]){ i += 1 }
- FileUtils.cd "tmp_watcher" do
- FileUtils.touch(FILES)
- end
- assert checker.execute_if_updated
- assert_equal 1, i
- end
-
- def test_should_not_invoke_the_block_if_a_watched_dir_changed_its_glob
- i = 0
- checker = ActiveSupport::FileUpdateChecker.new([], "tmp_watcher" => :rb){ i += 1 }
- FileUtils.cd "tmp_watcher" do
- FileUtils.touch(FILES)
- end
- assert !checker.execute_if_updated
- assert_equal 0, i
- end
-
- def test_should_not_block_if_a_strange_filename_used
- FileUtils.mkdir_p("tmp_watcher/valid,yetstrange,path,")
- FileUtils.touch(FILES.map { |file_name| "tmp_watcher/valid,yetstrange,path,/#{file_name}" })
-
- test = Thread.new do
- ActiveSupport::FileUpdateChecker.new([],"tmp_watcher/valid,yetstrange,path," => :txt) { i += 1 }
- Thread.exit
- end
- test.priority = -1
- test.join(5)
-
- assert !test.alive?
+ def touch(files)
+ sleep 1 # let's wait a bit to ensure there's a new mtime
+ super
end
end
diff --git a/activesupport/test/fixtures/autoload/another_class.rb b/activesupport/test/fixtures/autoload/another_class.rb
index a240b3de41..cf38336cf8 100644
--- a/activesupport/test/fixtures/autoload/another_class.rb
+++ b/activesupport/test/fixtures/autoload/another_class.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
class Fixtures::AnotherClass
-end \ No newline at end of file
+end
diff --git a/activesupport/test/fixtures/autoload/some_class.rb b/activesupport/test/fixtures/autoload/some_class.rb
index 13b3c73ef5..ff25eb995e 100644
--- a/activesupport/test/fixtures/autoload/some_class.rb
+++ b/activesupport/test/fixtures/autoload/some_class.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
class Fixtures::Autoload::SomeClass
-end \ No newline at end of file
+end
diff --git a/activesupport/test/gzip_test.rb b/activesupport/test/gzip_test.rb
index 0e3cf3b429..3d790f69c4 100644
--- a/activesupport/test/gzip_test.rb
+++ b/activesupport/test/gzip_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'active_support/core_ext/object/blank'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/object/blank"
class GzipTest < ActiveSupport::TestCase
def test_compress_should_decompress_to_the_same_value
@@ -13,14 +15,14 @@ class GzipTest < ActiveSupport::TestCase
end
def test_compress_should_return_a_binary_string
- compressed = ActiveSupport::Gzip.compress('')
+ compressed = ActiveSupport::Gzip.compress("")
- assert_equal Encoding.find('binary'), compressed.encoding
- assert !compressed.blank?, "a compressed blank string should not be blank"
+ assert_equal Encoding.find("binary"), compressed.encoding
+ assert_not compressed.blank?, "a compressed blank string should not be blank"
end
def test_compress_should_return_gzipped_string_by_compression_level
- source_string = "Hello World"*100
+ source_string = "Hello World" * 100
gzipped_by_speed = ActiveSupport::Gzip.compress(source_string, Zlib::BEST_SPEED)
assert_equal 1, Zlib::GzipReader.new(StringIO.new(gzipped_by_speed)).level
@@ -30,4 +32,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/hash_with_indifferent_access_test.rb b/activesupport/test/hash_with_indifferent_access_test.rb
new file mode 100644
index 0000000000..f81e0dc70f
--- /dev/null
+++ b/activesupport/test/hash_with_indifferent_access_test.rb
@@ -0,0 +1,831 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/hash"
+require "bigdecimal"
+require "active_support/core_ext/string/access"
+require "active_support/ordered_hash"
+require "active_support/core_ext/object/conversions"
+require "active_support/core_ext/object/deep_dup"
+require "active_support/inflections"
+
+class HashWithIndifferentAccessTest < ActiveSupport::TestCase
+ HashWithIndifferentAccess = ActiveSupport::HashWithIndifferentAccess
+
+ class IndifferentHash < ActiveSupport::HashWithIndifferentAccess
+ end
+
+ class SubclassingArray < Array
+ end
+
+ class SubclassingHash < Hash
+ end
+
+ class NonIndifferentHash < Hash
+ def nested_under_indifferent_access
+ self
+ end
+ end
+
+ class HashByConversion
+ def initialize(hash)
+ @hash = hash
+ end
+
+ def to_hash
+ @hash
+ end
+ end
+
+ def setup
+ @strings = { "a" => 1, "b" => 2 }
+ @nested_strings = { "a" => { "b" => { "c" => 3 } } }
+ @symbols = { a: 1, b: 2 }
+ @nested_symbols = { a: { b: { c: 3 } } }
+ @mixed = { :a => 1, "b" => 2 }
+ @nested_mixed = { "a" => { b: { "c" => 3 } } }
+ @integers = { 0 => 1, 1 => 2 }
+ @nested_integers = { 0 => { 1 => { 2 => 3 } } }
+ @illegal_symbols = { [] => 3 }
+ @nested_illegal_symbols = { [] => { [] => 3 } }
+ end
+
+ def test_symbolize_keys_for_hash_with_indifferent_access
+ assert_instance_of Hash, @symbols.with_indifferent_access.symbolize_keys
+ assert_equal @symbols, @symbols.with_indifferent_access.symbolize_keys
+ assert_equal @symbols, @strings.with_indifferent_access.symbolize_keys
+ assert_equal @symbols, @mixed.with_indifferent_access.symbolize_keys
+ end
+
+ def test_to_options_for_hash_with_indifferent_access
+ assert_instance_of Hash, @symbols.with_indifferent_access.to_options
+ assert_equal @symbols, @symbols.with_indifferent_access.to_options
+ assert_equal @symbols, @strings.with_indifferent_access.to_options
+ assert_equal @symbols, @mixed.with_indifferent_access.to_options
+ end
+
+ def test_deep_symbolize_keys_for_hash_with_indifferent_access
+ assert_instance_of Hash, @nested_symbols.with_indifferent_access.deep_symbolize_keys
+ assert_equal @nested_symbols, @nested_symbols.with_indifferent_access.deep_symbolize_keys
+ assert_equal @nested_symbols, @nested_strings.with_indifferent_access.deep_symbolize_keys
+ assert_equal @nested_symbols, @nested_mixed.with_indifferent_access.deep_symbolize_keys
+ end
+
+ def test_symbolize_keys_bang_for_hash_with_indifferent_access
+ assert_raise(NoMethodError) { @symbols.with_indifferent_access.dup.symbolize_keys! }
+ assert_raise(NoMethodError) { @strings.with_indifferent_access.dup.symbolize_keys! }
+ assert_raise(NoMethodError) { @mixed.with_indifferent_access.dup.symbolize_keys! }
+ end
+
+ def test_deep_symbolize_keys_bang_for_hash_with_indifferent_access
+ assert_raise(NoMethodError) { @nested_symbols.with_indifferent_access.deep_dup.deep_symbolize_keys! }
+ assert_raise(NoMethodError) { @nested_strings.with_indifferent_access.deep_dup.deep_symbolize_keys! }
+ assert_raise(NoMethodError) { @nested_mixed.with_indifferent_access.deep_dup.deep_symbolize_keys! }
+ end
+
+ def test_symbolize_keys_preserves_keys_that_cant_be_symbolized_for_hash_with_indifferent_access
+ assert_equal @illegal_symbols, @illegal_symbols.with_indifferent_access.symbolize_keys
+ assert_raise(NoMethodError) { @illegal_symbols.with_indifferent_access.dup.symbolize_keys! }
+ end
+
+ def test_deep_symbolize_keys_preserves_keys_that_cant_be_symbolized_for_hash_with_indifferent_access
+ assert_equal @nested_illegal_symbols, @nested_illegal_symbols.with_indifferent_access.deep_symbolize_keys
+ assert_raise(NoMethodError) { @nested_illegal_symbols.with_indifferent_access.deep_dup.deep_symbolize_keys! }
+ end
+
+ def test_symbolize_keys_preserves_integer_keys_for_hash_with_indifferent_access
+ assert_equal @integers, @integers.with_indifferent_access.symbolize_keys
+ assert_raise(NoMethodError) { @integers.with_indifferent_access.dup.symbolize_keys! }
+ end
+
+ def test_deep_symbolize_keys_preserves_integer_keys_for_hash_with_indifferent_access
+ assert_equal @nested_integers, @nested_integers.with_indifferent_access.deep_symbolize_keys
+ assert_raise(NoMethodError) { @nested_integers.with_indifferent_access.deep_dup.deep_symbolize_keys! }
+ end
+
+ def test_stringify_keys_for_hash_with_indifferent_access
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess, @symbols.with_indifferent_access.stringify_keys
+ assert_equal @strings, @symbols.with_indifferent_access.stringify_keys
+ assert_equal @strings, @strings.with_indifferent_access.stringify_keys
+ assert_equal @strings, @mixed.with_indifferent_access.stringify_keys
+ end
+
+ def test_deep_stringify_keys_for_hash_with_indifferent_access
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess, @nested_symbols.with_indifferent_access.deep_stringify_keys
+ assert_equal @nested_strings, @nested_symbols.with_indifferent_access.deep_stringify_keys
+ assert_equal @nested_strings, @nested_strings.with_indifferent_access.deep_stringify_keys
+ assert_equal @nested_strings, @nested_mixed.with_indifferent_access.deep_stringify_keys
+ end
+
+ def test_stringify_keys_bang_for_hash_with_indifferent_access
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess, @symbols.with_indifferent_access.dup.stringify_keys!
+ assert_equal @strings, @symbols.with_indifferent_access.dup.stringify_keys!
+ assert_equal @strings, @strings.with_indifferent_access.dup.stringify_keys!
+ assert_equal @strings, @mixed.with_indifferent_access.dup.stringify_keys!
+ end
+
+ def test_deep_stringify_keys_bang_for_hash_with_indifferent_access
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess, @nested_symbols.with_indifferent_access.dup.deep_stringify_keys!
+ assert_equal @nested_strings, @nested_symbols.with_indifferent_access.deep_dup.deep_stringify_keys!
+ assert_equal @nested_strings, @nested_strings.with_indifferent_access.deep_dup.deep_stringify_keys!
+ assert_equal @nested_strings, @nested_mixed.with_indifferent_access.deep_dup.deep_stringify_keys!
+ end
+
+ def test_nested_under_indifferent_access
+ foo = { "foo" => SubclassingHash.new.tap { |h| h["bar"] = "baz" } }.with_indifferent_access
+ assert_kind_of ActiveSupport::HashWithIndifferentAccess, foo["foo"]
+
+ foo = { "foo" => NonIndifferentHash.new.tap { |h| h["bar"] = "baz" } }.with_indifferent_access
+ assert_kind_of NonIndifferentHash, foo["foo"]
+
+ foo = { "foo" => IndifferentHash.new.tap { |h| h["bar"] = "baz" } }.with_indifferent_access
+ assert_kind_of IndifferentHash, foo["foo"]
+ end
+
+ def test_indifferent_assorted
+ @strings = @strings.with_indifferent_access
+ @symbols = @symbols.with_indifferent_access
+ @mixed = @mixed.with_indifferent_access
+
+ assert_equal "a", @strings.__send__(:convert_key, :a)
+
+ assert_equal 1, @strings.fetch("a")
+ assert_equal 1, @strings.fetch(:a.to_s)
+ assert_equal 1, @strings.fetch(:a)
+
+ hashes = { :@strings => @strings, :@symbols => @symbols, :@mixed => @mixed }
+ method_map = { '[]': 1, fetch: 1, values_at: [1],
+ has_key?: true, include?: true, key?: true,
+ member?: true }
+
+ hashes.each do |name, hash|
+ method_map.sort_by(&:to_s).each do |meth, expected|
+ assert_equal(expected, hash.__send__(meth, "a"),
+ "Calling #{name}.#{meth} 'a'")
+ assert_equal(expected, hash.__send__(meth, :a),
+ "Calling #{name}.#{meth} :a")
+ end
+ end
+
+ assert_equal [1, 2], @strings.values_at("a", "b")
+ assert_equal [1, 2], @strings.values_at(:a, :b)
+ assert_equal [1, 2], @symbols.values_at("a", "b")
+ assert_equal [1, 2], @symbols.values_at(:a, :b)
+ assert_equal [1, 2], @mixed.values_at("a", "b")
+ assert_equal [1, 2], @mixed.values_at(:a, :b)
+ end
+
+ def test_indifferent_fetch_values
+ @mixed = @mixed.with_indifferent_access
+
+ assert_equal [1, 2], @mixed.fetch_values("a", "b")
+ assert_equal [1, 2], @mixed.fetch_values(:a, :b)
+ assert_equal [1, 2], @mixed.fetch_values(:a, "b")
+ assert_equal [1, "c"], @mixed.fetch_values(:a, :c) { |key| key }
+ assert_raise(KeyError) { @mixed.fetch_values(:a, :c) }
+ end
+
+ def test_indifferent_reading
+ hash = HashWithIndifferentAccess.new
+ hash["a"] = 1
+ hash["b"] = true
+ hash["c"] = false
+ hash["d"] = nil
+
+ assert_equal 1, hash[:a]
+ assert_equal true, hash[:b]
+ assert_equal false, hash[:c]
+ assert_nil hash[:d]
+ assert_nil hash[:e]
+ end
+
+ def test_indifferent_reading_with_nonnil_default
+ hash = HashWithIndifferentAccess.new(1)
+ hash["a"] = 1
+ hash["b"] = true
+ hash["c"] = false
+ hash["d"] = nil
+
+ assert_equal 1, hash[:a]
+ assert_equal true, hash[:b]
+ assert_equal false, hash[:c]
+ assert_nil hash[:d]
+ assert_equal 1, hash[:e]
+ end
+
+ def test_indifferent_writing
+ hash = HashWithIndifferentAccess.new
+ hash[:a] = 1
+ hash["b"] = 2
+ 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
+ hash = HashWithIndifferentAccess.new
+ hash[:a] = "a"
+ hash["b"] = "b"
+
+ updated_with_strings = hash.update(@strings)
+ updated_with_symbols = hash.update(@symbols)
+ updated_with_mixed = hash.update(@mixed)
+
+ assert_equal 1, updated_with_strings[:a]
+ assert_equal 1, updated_with_strings["a"]
+ assert_equal 2, updated_with_strings["b"]
+
+ assert_equal 1, updated_with_symbols[:a]
+ assert_equal 2, updated_with_symbols["b"]
+ assert_equal 2, updated_with_symbols[:b]
+
+ 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
+
+ def test_update_with_to_hash_conversion
+ hash = HashWithIndifferentAccess.new
+ hash.update HashByConversion.new(a: 1)
+ assert_equal 1, hash["a"]
+ end
+
+ def test_indifferent_merging
+ hash = HashWithIndifferentAccess.new
+ hash[:a] = "failure"
+ hash["b"] = "failure"
+
+ other = { "a" => 1, :b => 2 }
+
+ merged = hash.merge(other)
+
+ assert_equal HashWithIndifferentAccess, merged.class
+ assert_equal 1, merged[:a]
+ assert_equal 2, merged["b"]
+
+ hash.update(other)
+
+ assert_equal 1, hash[:a]
+ assert_equal 2, hash["b"]
+ end
+
+ def test_merge_with_to_hash_conversion
+ hash = HashWithIndifferentAccess.new
+ merged = hash.merge HashByConversion.new(a: 1)
+ assert_equal 1, merged["a"]
+ end
+
+ def test_indifferent_replace
+ hash = HashWithIndifferentAccess.new
+ hash[:a] = 42
+
+ replaced = hash.replace(b: 12)
+
+ assert hash.key?("b")
+ assert_not hash.key?(:a)
+ assert_equal 12, hash[:b]
+ assert_same hash, replaced
+ end
+
+ def test_replace_with_to_hash_conversion
+ hash = HashWithIndifferentAccess.new
+ hash[:a] = 42
+
+ replaced = hash.replace(HashByConversion.new(b: 12))
+
+ assert hash.key?("b")
+ assert_not hash.key?(:a)
+ assert_equal 12, hash[:b]
+ assert_same hash, replaced
+ end
+
+ def test_indifferent_merging_with_block
+ hash = HashWithIndifferentAccess.new
+ hash[:a] = 1
+ hash["b"] = 3
+
+ other = { "a" => 4, :b => 2, "c" => 10 }
+
+ merged = hash.merge(other) { |key, old, new| old > new ? old : new }
+
+ assert_equal HashWithIndifferentAccess, merged.class
+ assert_equal 4, merged[:a]
+ assert_equal 3, merged["b"]
+ assert_equal 10, merged[:c]
+
+ other_indifferent = HashWithIndifferentAccess.new("a" => 9, :b => 2)
+
+ merged = hash.merge(other_indifferent) { |key, old, new| old + new }
+
+ assert_equal HashWithIndifferentAccess, merged.class
+ assert_equal 10, merged[:a]
+ assert_equal 5, merged[:b]
+ end
+
+ def test_indifferent_reverse_merging
+ hash = HashWithIndifferentAccess.new key: :old_value
+ hash.reverse_merge! key: :new_value
+ assert_equal :old_value, hash[:key]
+
+ hash = HashWithIndifferentAccess.new("some" => "value", "other" => "value")
+ hash.reverse_merge!(some: "noclobber", another: "clobber")
+ assert_equal "value", hash[:some]
+ assert_equal "clobber", hash[:another]
+ end
+
+ def test_indifferent_with_defaults_aliases_reverse_merge
+ hash = HashWithIndifferentAccess.new key: :old_value
+ actual = hash.with_defaults key: :new_value
+ assert_equal :old_value, actual[:key]
+
+ hash = HashWithIndifferentAccess.new key: :old_value
+ hash.with_defaults! key: :new_value
+ assert_equal :old_value, hash[:key]
+ end
+
+ def test_indifferent_deleting
+ get_hash = proc { { a: "foo" }.with_indifferent_access }
+ hash = get_hash.call
+ assert_equal "foo", hash.delete(:a)
+ assert_nil hash.delete(:a)
+ hash = get_hash.call
+ assert_equal "foo", hash.delete("a")
+ assert_nil hash.delete("a")
+ end
+
+ def test_indifferent_select
+ hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).select { |k, v| v == 1 }
+
+ assert_equal({ "a" => 1 }, hash)
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash
+ end
+
+ def test_indifferent_select_returns_enumerator
+ enum = ActiveSupport::HashWithIndifferentAccess.new(@strings).select
+ assert_instance_of Enumerator, enum
+ end
+
+ def test_indifferent_select_returns_a_hash_when_unchanged
+ hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).select { |k, v| true }
+
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash
+ end
+
+ def test_indifferent_select_bang
+ indifferent_strings = ActiveSupport::HashWithIndifferentAccess.new(@strings)
+ indifferent_strings.select! { |k, v| v == 1 }
+
+ assert_equal({ "a" => 1 }, indifferent_strings)
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings
+ end
+
+ def test_indifferent_reject
+ hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).reject { |k, v| v != 1 }
+
+ assert_equal({ "a" => 1 }, hash)
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash
+ end
+
+ def test_indifferent_reject_returns_enumerator
+ enum = ActiveSupport::HashWithIndifferentAccess.new(@strings).reject
+ assert_instance_of Enumerator, enum
+ end
+
+ def test_indifferent_reject_bang
+ indifferent_strings = ActiveSupport::HashWithIndifferentAccess.new(@strings)
+ indifferent_strings.reject! { |k, v| v != 1 }
+
+ assert_equal({ "a" => 1 }, indifferent_strings)
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings
+ end
+
+ def test_indifferent_transform_keys
+ hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).transform_keys { |k| k * 2 }
+
+ assert_equal({ "aa" => 1, "bb" => 2 }, hash)
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash
+
+ hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).transform_keys { |k| k.to_sym }
+
+ assert_equal(1, hash[:a])
+ assert_equal(1, hash["a"])
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash
+ end
+
+ def test_indifferent_transform_keys_bang
+ indifferent_strings = ActiveSupport::HashWithIndifferentAccess.new(@strings)
+ indifferent_strings.transform_keys! { |k| k * 2 }
+
+ assert_equal({ "aa" => 1, "bb" => 2 }, indifferent_strings)
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings
+
+ indifferent_strings = ActiveSupport::HashWithIndifferentAccess.new(@strings)
+ indifferent_strings.transform_keys! { |k| k.to_sym }
+
+ assert_equal(1, indifferent_strings[:a])
+ assert_equal(1, indifferent_strings["a"])
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings
+ end
+
+ def test_indifferent_transform_values
+ hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).transform_values { |v| v * 2 }
+
+ assert_equal({ "a" => 2, "b" => 4 }, hash)
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash
+ end
+
+ def test_indifferent_transform_values_bang
+ indifferent_strings = ActiveSupport::HashWithIndifferentAccess.new(@strings)
+ indifferent_strings.transform_values! { |v| v * 2 }
+
+ assert_equal({ "a" => 2, "b" => 4 }, indifferent_strings)
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings
+ end
+
+ def test_indifferent_compact
+ hash_contain_nil_value = @strings.merge("z" => nil)
+ hash = ActiveSupport::HashWithIndifferentAccess.new(hash_contain_nil_value)
+ 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
+
+ # Should preserve the default value.
+ mixed_with_default = @mixed.dup
+ mixed_with_default.default = "1234"
+ roundtrip = mixed_with_default.with_indifferent_access.to_hash
+ assert_equal @strings, roundtrip
+ assert_equal "1234", roundtrip.default
+
+ # Ensure nested hashes are not HashWithIndiffereneAccess
+ new_to_hash = @nested_mixed.with_indifferent_access.to_hash
+ assert_not new_to_hash.instance_of?(HashWithIndifferentAccess)
+ assert_not new_to_hash["a"].instance_of?(HashWithIndifferentAccess)
+ assert_not new_to_hash["a"]["b"].instance_of?(HashWithIndifferentAccess)
+ end
+
+ def test_lookup_returns_the_same_object_that_is_stored_in_hash_indifferent_access
+ hash = HashWithIndifferentAccess.new { |h, k| h[k] = [] }
+ hash[:a] << 1
+
+ assert_equal [1], hash[:a]
+ end
+
+ def test_with_indifferent_access_has_no_side_effects_on_existing_hash
+ hash = { content: [{ :foo => :bar, "bar" => "baz" }] }
+ hash.with_indifferent_access
+
+ assert_equal [:foo, "bar"], hash[:content].first.keys
+ end
+
+ def test_indifferent_hash_with_array_of_hashes
+ hash = { "urls" => { "url" => [ { "address" => "1" }, { "address" => "2" } ] } }.with_indifferent_access
+ assert_equal "1", hash[:urls][:url].first[:address]
+
+ hash = hash.to_hash
+ assert_not hash.instance_of?(HashWithIndifferentAccess)
+ assert_not hash["urls"].instance_of?(HashWithIndifferentAccess)
+ assert_not hash["urls"]["url"].first.instance_of?(HashWithIndifferentAccess)
+ end
+
+ def test_should_preserve_array_subclass_when_value_is_array
+ array = SubclassingArray.new
+ array << { "address" => "1" }
+ hash = { "urls" => { "url" => array } }.with_indifferent_access
+ assert_equal SubclassingArray, hash[:urls][:url].class
+ end
+
+ def test_should_preserve_array_class_when_hash_value_is_frozen_array
+ array = SubclassingArray.new
+ array << { "address" => "1" }
+ hash = { "urls" => { "url" => array.freeze } }.with_indifferent_access
+ assert_equal SubclassingArray, hash[:urls][:url].class
+ end
+
+ def test_stringify_and_symbolize_keys_on_indifferent_preserves_hash
+ h = HashWithIndifferentAccess.new
+ h[:first] = 1
+ h = h.stringify_keys
+ assert_equal 1, h["first"]
+ h = HashWithIndifferentAccess.new
+ h["first"] = 1
+ h = h.symbolize_keys
+ assert_equal 1, h[:first]
+ end
+
+ def test_deep_stringify_and_deep_symbolize_keys_on_indifferent_preserves_hash
+ h = HashWithIndifferentAccess.new
+ h[:first] = 1
+ h = h.deep_stringify_keys
+ assert_equal 1, h["first"]
+ h = HashWithIndifferentAccess.new
+ h["first"] = 1
+ h = h.deep_symbolize_keys
+ assert_equal 1, h[:first]
+ end
+
+ def test_to_options_on_indifferent_preserves_hash
+ h = HashWithIndifferentAccess.new
+ h["first"] = 1
+ h.to_options!
+ assert_equal 1, h["first"]
+ end
+
+ def test_to_options_on_indifferent_preserves_works_as_hash_with_dup
+ h = HashWithIndifferentAccess.new(a: { b: "b" })
+ dup = h.dup
+
+ dup[:a][:c] = "c"
+ assert_equal "c", h[:a][:c]
+ end
+
+ def test_indifferent_sub_hashes
+ h = { "user" => { "id" => 5 } }.with_indifferent_access
+ ["user", :user].each { |user| [:id, "id"].each { |id| assert_equal 5, h[user][id], "h[#{user.inspect}][#{id.inspect}] should be 5" } }
+
+ h = { user: { id: 5 } }.with_indifferent_access
+ ["user", :user].each { |user| [:id, "id"].each { |id| assert_equal 5, h[user][id], "h[#{user.inspect}][#{id.inspect}] should be 5" } }
+ end
+
+ def test_indifferent_duplication
+ # Should preserve default value
+ h = HashWithIndifferentAccess.new
+ h.default = "1234"
+ assert_equal h.default, h.dup.default
+
+ # Should preserve class for subclasses
+ h = IndifferentHash.new
+ assert_equal h.class, h.dup.class
+ end
+
+ def test_nested_dig_indifferent_access
+ data = { "this" => { "views" => 1234 } }.with_indifferent_access
+ assert_equal 1234, data.dig(:this, :views)
+ end
+
+ def test_argless_default_with_existing_nil_key
+ h = Hash.new(:default).merge(nil => "defined").with_indifferent_access
+
+ assert_equal :default, h.default
+ end
+
+ def test_default_with_argument
+ h = Hash.new { 5 }.merge(1 => 2).with_indifferent_access
+
+ assert_equal 5, h.default(1)
+ end
+
+ def test_default_proc
+ h = ActiveSupport::HashWithIndifferentAccess.new { |hash, key| key }
+
+ assert_nil h.default
+ assert_equal "foo", h.default("foo")
+ assert_equal "foo", h.default(:foo)
+ end
+
+ def test_double_conversion_with_nil_key
+ h = { nil => "defined" }.with_indifferent_access.with_indifferent_access
+
+ assert_nil h[:undefined_key]
+ end
+
+ def test_assorted_keys_not_stringified
+ original = { Object.new => 2, 1 => 2, [] => true }
+ indiff = original.with_indifferent_access
+ assert_not(indiff.keys.any? { |k| k.kind_of? String }, "A key was converted to a string!")
+ end
+
+ def test_deep_merge_on_indifferent_access
+ hash_1 = HashWithIndifferentAccess.new(a: "a", b: "b", c: { c1: "c1", c2: "c2", c3: { d1: "d1" } })
+ hash_2 = HashWithIndifferentAccess.new(a: 1, c: { c1: 2, c3: { d2: "d2" } })
+ hash_3 = { a: 1, c: { c1: 2, c3: { d2: "d2" } } }
+ expected = { "a" => 1, "b" => "b", "c" => { "c1" => 2, "c2" => "c2", "c3" => { "d1" => "d1", "d2" => "d2" } } }
+ assert_equal expected, hash_1.deep_merge(hash_2)
+ assert_equal expected, hash_1.deep_merge(hash_3)
+
+ hash_1.deep_merge!(hash_2)
+ assert_equal expected, hash_1
+ end
+
+ def test_store_on_indifferent_access
+ hash = HashWithIndifferentAccess.new
+ hash.store(:test1, 1)
+ hash.store("test1", 11)
+ hash[:test2] = 2
+ hash["test2"] = 22
+ expected = { "test1" => 11, "test2" => 22 }
+ assert_equal expected, hash
+ end
+
+ def test_constructor_on_indifferent_access
+ hash = HashWithIndifferentAccess[:foo, 1]
+ assert_equal 1, hash[:foo]
+ assert_equal 1, hash["foo"]
+ hash[:foo] = 3
+ assert_equal 3, hash[:foo]
+ assert_equal 3, hash["foo"]
+ end
+
+ def test_indifferent_slice
+ original = { a: "x", b: "y", c: 10 }.with_indifferent_access
+ expected = { a: "x", b: "y" }.with_indifferent_access
+
+ [["a", "b"], [:a, :b]].each do |keys|
+ # Should return a new hash with only the given keys.
+ assert_equal expected, original.slice(*keys), keys.inspect
+ assert_not_equal expected, original
+ end
+ end
+
+ def test_indifferent_slice_inplace
+ original = { a: "x", b: "y", c: 10 }.with_indifferent_access
+ expected = { c: 10 }.with_indifferent_access
+
+ [["a", "b"], [:a, :b]].each do |keys|
+ # Should replace the hash with only the given keys.
+ copy = original.dup
+ assert_equal expected, copy.slice!(*keys)
+ end
+ end
+
+ def test_indifferent_slice_access_with_symbols
+ original = { "login" => "bender", "password" => "shiny", "stuff" => "foo" }
+ original = original.with_indifferent_access
+
+ slice = original.slice(:login, :password)
+
+ assert_equal "bender", slice[:login]
+ assert_equal "bender", slice["login"]
+ end
+
+ def test_indifferent_without
+ original = { a: "x", b: "y", c: 10 }.with_indifferent_access
+ expected = { c: 10 }.with_indifferent_access
+
+ [["a", "b"], [:a, :b]].each do |keys|
+ # Should return a new hash without the given keys.
+ assert_equal expected, original.without(*keys), keys.inspect
+ assert_not_equal expected, original
+ end
+ end
+
+ def test_indifferent_extract
+ original = { :a => 1, "b" => 2, :c => 3, "d" => 4 }.with_indifferent_access
+ expected = { a: 1, b: 2 }.with_indifferent_access
+ remaining = { c: 3, d: 4 }.with_indifferent_access
+
+ [["a", "b"], [:a, :b]].each do |keys|
+ copy = original.dup
+ assert_equal expected, copy.extract!(*keys)
+ assert_equal remaining, copy
+ end
+ end
+
+ def test_new_with_to_hash_conversion
+ hash = HashWithIndifferentAccess.new(HashByConversion.new(a: 1))
+ assert hash.key?("a")
+ assert_equal 1, hash[:a]
+ end
+
+ def test_dup_with_default_proc
+ hash = HashWithIndifferentAccess.new
+ hash.default_proc = proc { |h, v| raise "walrus" }
+ assert_nothing_raised { hash.dup }
+ end
+
+ def test_dup_with_default_proc_sets_proc
+ hash = HashWithIndifferentAccess.new
+ hash.default_proc = proc { |h, k| k + 1 }
+ new_hash = hash.dup
+
+ assert_equal 3, new_hash[2]
+
+ new_hash.default = 2
+ assert_equal 2, new_hash[:non_existent]
+ end
+
+ def test_to_hash_with_raising_default_proc
+ hash = HashWithIndifferentAccess.new
+ hash.default_proc = proc { |h, k| raise "walrus" }
+
+ assert_nothing_raised { hash.to_hash }
+ end
+
+ def test_new_with_to_hash_conversion_copies_default
+ normal_hash = Hash.new(3)
+ normal_hash[:a] = 1
+
+ hash = HashWithIndifferentAccess.new(HashByConversion.new(normal_hash))
+ assert_equal 1, hash[:a]
+ assert_equal 3, hash[:b]
+ end
+
+ def test_new_with_to_hash_conversion_copies_default_proc
+ normal_hash = Hash.new { 1 + 2 }
+ normal_hash[:a] = 1
+
+ hash = HashWithIndifferentAccess.new(HashByConversion.new(normal_hash))
+ 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
+
+ def test_should_use_default_proc_for_unknown_key
+ hash_wia = HashWithIndifferentAccess.new { 1 + 2 }
+ assert_equal 3, hash_wia[:new_key]
+ end
+
+ def test_should_return_nil_if_no_key_is_supplied
+ hash_wia = HashWithIndifferentAccess.new { 1 + 2 }
+ assert_nil hash_wia.default
+ end
+
+ def test_should_use_default_value_for_unknown_key
+ hash_wia = HashWithIndifferentAccess.new(3)
+ assert_equal 3, hash_wia[:new_key]
+ end
+
+ def test_should_use_default_value_if_no_key_is_supplied
+ hash_wia = HashWithIndifferentAccess.new(3)
+ assert_equal 3, hash_wia.default
+ end
+
+ def test_should_nil_if_no_default_value_is_supplied
+ hash_wia = HashWithIndifferentAccess.new
+ assert_nil hash_wia.default
+ end
+
+ def test_should_return_dup_for_with_indifferent_access
+ hash_wia = HashWithIndifferentAccess.new
+ assert_equal hash_wia, hash_wia.with_indifferent_access
+ assert_not_same hash_wia, hash_wia.with_indifferent_access
+ end
+
+ def test_allows_setting_frozen_array_values_with_indifferent_access
+ value = [1, 2, 3].freeze
+ hash = HashWithIndifferentAccess.new
+ hash[:key] = value
+ assert_equal hash[:key], value
+ end
+
+ def test_should_copy_the_default_value_when_converting_to_hash_with_indifferent_access
+ hash = Hash.new(3)
+ hash_wia = hash.with_indifferent_access
+ assert_equal 3, hash_wia.default
+ end
+
+ def test_should_copy_the_default_proc_when_converting_to_hash_with_indifferent_access
+ hash = Hash.new do
+ 2 + 1
+ end
+ assert_equal 3, hash[:foo]
+
+ hash_wia = hash.with_indifferent_access
+ assert_equal 3, hash_wia[:foo]
+ assert_equal 3, hash_wia[:bar]
+ end
+end
diff --git a/activesupport/test/i18n_test.rb b/activesupport/test/i18n_test.rb
index 3faa15e7fd..8ad9441f9d 100644
--- a/activesupport/test/i18n_test.rb
+++ b/activesupport/test/i18n_test.rb
@@ -1,6 +1,8 @@
-require 'abstract_unit'
-require 'active_support/time'
-require 'active_support/core_ext/array/conversions'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/time"
+require "active_support/core_ext/array/conversions"
class I18nTest < ActiveSupport::TestCase
def setup
@@ -18,15 +20,15 @@ class I18nTest < ActiveSupport::TestCase
end
def test_date_localization_with_default_format
- assert_equal @date.strftime("%Y-%m-%d"), I18n.localize(@date, :format => :default)
+ assert_equal @date.strftime("%Y-%m-%d"), I18n.localize(@date, format: :default)
end
def test_date_localization_with_short_format
- assert_equal @date.strftime("%b %d"), I18n.localize(@date, :format => :short)
+ assert_equal @date.strftime("%b %d"), I18n.localize(@date, format: :short)
end
def test_date_localization_with_long_format
- assert_equal @date.strftime("%B %d, %Y"), I18n.localize(@date, :format => :long)
+ assert_equal @date.strftime("%B %d, %Y"), I18n.localize(@date, format: :long)
end
def test_time_localization_should_use_default_format
@@ -34,15 +36,15 @@ class I18nTest < ActiveSupport::TestCase
end
def test_time_localization_with_default_format
- assert_equal @time.strftime("%a, %d %b %Y %H:%M:%S %z"), I18n.localize(@time, :format => :default)
+ assert_equal @time.strftime("%a, %d %b %Y %H:%M:%S %z"), I18n.localize(@time, format: :default)
end
def test_time_localization_with_short_format
- assert_equal @time.strftime("%d %b %H:%M"), I18n.localize(@time, :format => :short)
+ assert_equal @time.strftime("%d %b %H:%M"), I18n.localize(@time, format: :short)
end
def test_time_localization_with_long_format
- assert_equal @time.strftime("%B %d, %Y %H:%M"), I18n.localize(@time, :format => :long)
+ assert_equal @time.strftime("%B %d, %Y %H:%M"), I18n.localize(@time, format: :long)
end
def test_day_names
@@ -66,39 +68,39 @@ class I18nTest < ActiveSupport::TestCase
end
def test_time_am
- assert_equal 'am', I18n.translate(:'time.am')
+ assert_equal "am", I18n.translate(:'time.am')
end
def test_time_pm
- assert_equal 'pm', I18n.translate(:'time.pm')
+ assert_equal "pm", I18n.translate(:'time.pm')
end
def test_words_connector
- assert_equal ', ', I18n.translate(:'support.array.words_connector')
+ assert_equal ", ", I18n.translate(:'support.array.words_connector')
end
def test_two_words_connector
- assert_equal ' and ', I18n.translate(:'support.array.two_words_connector')
+ assert_equal " and ", I18n.translate(:'support.array.two_words_connector')
end
def test_last_word_connector
- assert_equal ', and ', I18n.translate(:'support.array.last_word_connector')
+ assert_equal ", and ", I18n.translate(:'support.array.last_word_connector')
end
def test_to_sentence
default_two_words_connector = I18n.translate(:'support.array.two_words_connector')
default_last_word_connector = I18n.translate(:'support.array.last_word_connector')
- assert_equal 'a, b, and c', %w[a b c].to_sentence
- I18n.backend.store_translations 'en', :support => { :array => { :two_words_connector => ' & ' } }
- assert_equal 'a & b', %w[a b].to_sentence
- I18n.backend.store_translations 'en', :support => { :array => { :last_word_connector => ' and ' } }
- assert_equal 'a, b and c', %w[a b c].to_sentence
+ assert_equal "a, b, and c", %w[a b c].to_sentence
+ I18n.backend.store_translations "en", support: { array: { two_words_connector: " & " } }
+ assert_equal "a & b", %w[a b].to_sentence
+ I18n.backend.store_translations "en", support: { array: { last_word_connector: " and " } }
+ assert_equal "a, b and c", %w[a b c].to_sentence
ensure
- I18n.backend.store_translations 'en', :support => { :array => { :two_words_connector => default_two_words_connector } }
- I18n.backend.store_translations 'en', :support => { :array => { :last_word_connector => default_last_word_connector } }
+ I18n.backend.store_translations "en", support: { array: { two_words_connector: default_two_words_connector } }
+ I18n.backend.store_translations "en", support: { array: { last_word_connector: default_last_word_connector } }
end
def test_to_sentence_with_empty_i18n_store
- assert_equal 'a, b, and c', %w[a b c].to_sentence(locale: 'empty')
+ assert_equal "a, b, and c", %w[a b c].to_sentence(locale: "empty")
end
end
diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb
index a0764f6d6b..5e50acf5db 100644
--- a/activesupport/test/inflector_test.rb
+++ b/activesupport/test/inflector_test.rb
@@ -1,8 +1,10 @@
-require 'abstract_unit'
-require 'active_support/inflector'
+# frozen_string_literal: true
-require 'inflector_test_cases'
-require 'constantize_test_cases'
+require "abstract_unit"
+require "active_support/inflector"
+
+require "inflector_test_cases"
+require "constantize_test_cases"
class InflectorTest < ActiveSupport::TestCase
include InflectorTestCases
@@ -31,6 +33,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)
@@ -80,7 +108,6 @@ class InflectorTest < ActiveSupport::TestCase
end
end
-
def test_overwrite_previous_inflectors
assert_equal("series", ActiveSupport::Inflector.singularize("series"))
ActiveSupport::Inflector.inflections.singular "series", "serie"
@@ -94,6 +121,13 @@ class InflectorTest < ActiveSupport::TestCase
end
end
+ MixtureToTitleCaseWithKeepIdSuffix.each_with_index do |(before, titleized), index|
+ define_method "test_titleize_with_keep_id_suffix_mixture_to_title_case_#{index}" do
+ assert_equal(titleized, ActiveSupport::Inflector.titleize(before, keep_id_suffix: true),
+ "mixture to TitleCase with keep_id_suffix failed for #{before}")
+ end
+ end
+
def test_camelize
CamelToUnderscore.each do |camel, underscore|
assert_equal(camel, ActiveSupport::Inflector.camelize(underscore))
@@ -101,11 +135,11 @@ class InflectorTest < ActiveSupport::TestCase
end
def test_camelize_with_lower_downcases_the_first_letter
- assert_equal('capital', ActiveSupport::Inflector.camelize('Capital', false))
+ assert_equal("capital", ActiveSupport::Inflector.camelize("Capital", false))
end
def test_camelize_with_underscores
- assert_equal("CamelCase", ActiveSupport::Inflector.camelize('Camel_Case'))
+ assert_equal("CamelCase", ActiveSupport::Inflector.camelize("Camel_Case"))
end
def test_acronyms
@@ -190,6 +224,12 @@ class InflectorTest < ActiveSupport::TestCase
assert_equal("json_html_api", ActiveSupport::Inflector.underscore("JSONHTMLAPI"))
end
+ def test_acronym_regexp_is_deprecated
+ assert_deprecated do
+ ActiveSupport::Inflector.inflections.acronym_regex
+ end
+ end
+
def test_underscore
CamelToUnderscore.each do |camel, underscore|
assert_equal(underscore, ActiveSupport::Inflector.underscore(camel))
@@ -246,37 +286,27 @@ 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, '_'))
+ 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, '__sep__'))
+ assert_equal(parameterized_string.gsub("-", "__sep__"), ActiveSupport::Inflector.parameterize(some_string, separator: "__sep__"))
end
end
@@ -289,12 +319,12 @@ class InflectorTest < ActiveSupport::TestCase
def test_classify_with_symbol
assert_nothing_raised do
- assert_equal 'FooBar', ActiveSupport::Inflector.classify(:foo_bars)
+ assert_equal "FooBar", ActiveSupport::Inflector.classify(:foo_bars)
end
end
def test_classify_with_leading_schema_name
- assert_equal 'FooBar', ActiveSupport::Inflector.classify('schema.foo_bar')
+ assert_equal "FooBar", ActiveSupport::Inflector.classify("schema.foo_bar")
end
def test_humanize
@@ -309,6 +339,12 @@ class InflectorTest < ActiveSupport::TestCase
end
end
+ def test_humanize_with_keep_id_suffix
+ UnderscoreToHumanWithKeepIdSuffix.each do |underscore, human|
+ assert_equal(human, ActiveSupport::Inflector.humanize(underscore, keep_id_suffix: true))
+ end
+ end
+
def test_humanize_by_rule
ActiveSupport::Inflector.inflections do |inflect|
inflect.human(/_cnt$/i, '\1_count')
@@ -326,6 +362,19 @@ class InflectorTest < ActiveSupport::TestCase
assert_equal("Col rpted bugs", ActiveSupport::Inflector.humanize("COL_rpted_bugs"))
end
+ def test_humanize_with_acronyms
+ ActiveSupport::Inflector.inflections do |inflect|
+ inflect.acronym "LAX"
+ inflect.acronym "SFO"
+ end
+ assert_equal("LAX roundtrip to SFO", ActiveSupport::Inflector.humanize("LAX ROUNDTRIP TO SFO"))
+ assert_equal("LAX roundtrip to SFO", ActiveSupport::Inflector.humanize("LAX ROUNDTRIP TO SFO", capitalize: false))
+ assert_equal("LAX roundtrip to SFO", ActiveSupport::Inflector.humanize("lax roundtrip to sfo"))
+ assert_equal("LAX roundtrip to SFO", ActiveSupport::Inflector.humanize("lax roundtrip to sfo", capitalize: false))
+ assert_equal("LAX roundtrip to SFO", ActiveSupport::Inflector.humanize("Lax Roundtrip To Sfo"))
+ assert_equal("LAX roundtrip to SFO", ActiveSupport::Inflector.humanize("Lax Roundtrip To Sfo", capitalize: false))
+ end
+
def test_constantize
run_constantize_tests_on do |string|
ActiveSupport::Inflector.constantize(string)
@@ -385,31 +434,38 @@ class InflectorTest < ActiveSupport::TestCase
def test_inflector_locality
ActiveSupport::Inflector.inflections(:es) do |inflect|
- inflect.plural(/$/, 's')
- inflect.plural(/z$/i, 'ces')
+ inflect.plural(/$/, "s")
+ inflect.plural(/z$/i, "ces")
+
+ inflect.singular(/s$/, "")
+ inflect.singular(/es$/, "")
- inflect.singular(/s$/, '')
- inflect.singular(/es$/, '')
+ inflect.irregular("el", "los")
- inflect.irregular('el', 'los')
+ inflect.uncountable("agua")
end
- assert_equal('hijos', 'hijo'.pluralize(:es))
- assert_equal('luces', 'luz'.pluralize(:es))
- assert_equal('luzs', 'luz'.pluralize)
+ assert_equal("hijos", "hijo".pluralize(:es))
+ assert_equal("luces", "luz".pluralize(:es))
+ assert_equal("luzs", "luz".pluralize)
- assert_equal('sociedad', 'sociedades'.singularize(:es))
- assert_equal('sociedade', 'sociedades'.singularize)
+ assert_equal("sociedad", "sociedades".singularize(:es))
+ assert_equal("sociedade", "sociedades".singularize)
- assert_equal('los', 'el'.pluralize(:es))
- assert_equal('els', 'el'.pluralize)
+ assert_equal("los", "el".pluralize(:es))
+ assert_equal("els", "el".pluralize)
+
+ assert_equal("agua", "agua".pluralize(:es))
+ assert_equal("aguas", "agua".pluralize)
ActiveSupport::Inflector.inflections(:es) { |inflect| inflect.clear }
- assert ActiveSupport::Inflector.inflections(:es).plurals.empty?
- assert ActiveSupport::Inflector.inflections(:es).singulars.empty?
- assert !ActiveSupport::Inflector.inflections.plurals.empty?
- assert !ActiveSupport::Inflector.inflections.singulars.empty?
+ assert_empty ActiveSupport::Inflector.inflections(:es).plurals
+ assert_empty ActiveSupport::Inflector.inflections(:es).singulars
+ assert_empty ActiveSupport::Inflector.inflections(:es).uncountables
+ assert_not_empty ActiveSupport::Inflector.inflections.plurals
+ assert_not_empty ActiveSupport::Inflector.inflections.singulars
+ assert_not_empty ActiveSupport::Inflector.inflections.uncountables
end
def test_clear_all
@@ -417,15 +473,15 @@ class InflectorTest < ActiveSupport::TestCase
# ensure any data is present
inflect.plural(/(quiz)$/i, '\1zes')
inflect.singular(/(database)s$/i, '\1')
- inflect.uncountable('series')
+ inflect.uncountable("series")
inflect.human("col_rpted_bugs", "Reported bugs")
inflect.clear :all
- assert inflect.plurals.empty?
- assert inflect.singulars.empty?
- assert inflect.uncountables.empty?
- assert inflect.humans.empty?
+ assert_empty inflect.plurals
+ assert_empty inflect.singulars
+ assert_empty inflect.uncountables
+ assert_empty inflect.humans
end
end
@@ -434,15 +490,15 @@ class InflectorTest < ActiveSupport::TestCase
# ensure any data is present
inflect.plural(/(quiz)$/i, '\1zes')
inflect.singular(/(database)s$/i, '\1')
- inflect.uncountable('series')
+ inflect.uncountable("series")
inflect.human("col_rpted_bugs", "Reported bugs")
inflect.clear
- assert inflect.plurals.empty?
- assert inflect.singulars.empty?
- assert inflect.uncountables.empty?
- assert inflect.humans.empty?
+ assert_empty inflect.plurals
+ assert_empty inflect.singulars
+ assert_empty inflect.uncountables
+ assert_empty inflect.humans
end
end
@@ -508,12 +564,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 18a8b92eb9..689370cccf 100644
--- a/activesupport/test/inflector_test_cases.rb
+++ b/activesupport/test/inflector_test_cases.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
module InflectorTestCases
SingularToPlural = {
@@ -129,10 +130,10 @@ module InflectorTestCases
}
SymbolToLowerCamel = {
- :product => 'product',
- :special_guest => 'specialGuest',
- :application_controller => 'applicationController',
- :area51_controller => 'area51Controller'
+ product: "product",
+ special_guest: "specialGuest",
+ application_controller: "applicationController",
+ area51_controller: "area51Controller"
}
CamelToUnderscoreWithoutReverse = {
@@ -175,6 +176,17 @@ module InflectorTestCases
"Test with malformed utf8 \251" => "test-with-malformed-utf8"
}
+ StringToParameterizedPreserveCase = {
+ "Donald E. Knuth" => "Donald-E-Knuth",
+ "Random text with *(bad)* characters" => "Random-text-with-bad-characters",
+ "Allow_Under_Scores" => "Allow_Under_Scores",
+ "Trailing bad characters!@#" => "Trailing-bad-characters",
+ "!@#Leading bad characters" => "Leading-bad-characters",
+ "Squeeze separators" => "Squeeze-separators",
+ "Test with + sign" => "Test-with-sign",
+ "Test with malformed utf8 \xA9" => "Test-with-malformed-utf8"
+ }
+
StringToParameterizeWithNoSeparator = {
"Donald E. Knuth" => "donaldeknuth",
"With-some-dashes" => "with-some-dashes",
@@ -186,6 +198,17 @@ module InflectorTestCases
"Test with malformed utf8 \251" => "testwithmalformedutf8"
}
+ StringToParameterizePreserveCaseWithNoSeparator = {
+ "Donald E. Knuth" => "DonaldEKnuth",
+ "With-some-dashes" => "With-some-dashes",
+ "Random text with *(bad)* characters" => "Randomtextwithbadcharacters",
+ "Trailing bad characters!@#" => "Trailingbadcharacters",
+ "!@#Leading bad characters" => "Leadingbadcharacters",
+ "Squeeze separators" => "Squeezeseparators",
+ "Test with + sign" => "Testwithsign",
+ "Test with malformed utf8 \xA9" => "Testwithmalformedutf8"
+ }
+
StringToParameterizeWithUnderscore = {
"Donald E. Knuth" => "donald_e_knuth",
"Random text with *(bad)* characters" => "random_text_with_bad_characters",
@@ -198,21 +221,42 @@ module InflectorTestCases
"Test with malformed utf8 \251" => "test_with_malformed_utf8"
}
+ StringToParameterizePreserveCaseWithUnderscore = {
+ "Donald E. Knuth" => "Donald_E_Knuth",
+ "Random text with *(bad)* characters" => "Random_text_with_bad_characters",
+ "With-some-dashes" => "With-some-dashes",
+ "Allow_Under_Scores" => "Allow_Under_Scores",
+ "Trailing bad characters!@#" => "Trailing_bad_characters",
+ "!@#Leading bad characters" => "Leading_bad_characters",
+ "Squeeze separators" => "Squeeze_separators",
+ "Test with + sign" => "Test_with_sign",
+ "Test with malformed utf8 \xA9" => "Test_with_malformed_utf8"
+ }
+
StringToParameterizedAndNormalized = {
"Malmö" => "malmo",
"Garçons" => "garcons",
"Ops\331" => "opsu",
"Ærøskøbing" => "aeroskobing",
"Aßlar" => "asslar",
- "Japanese: 日本語" => "japanese"
+ "Japanese: 日本語" => "japanese"
}
UnderscoreToHuman = {
- 'employee_salary' => 'Employee salary',
- 'employee_id' => 'Employee',
- 'underground' => 'Underground',
- '_id' => 'Id',
- '_external_id' => 'External'
+ "employee_salary" => "Employee salary",
+ "employee_id" => "Employee",
+ "underground" => "Underground",
+ "_id" => "Id",
+ "_external_id" => "External"
+ }
+
+ UnderscoreToHumanWithKeepIdSuffix = {
+ "this_is_a_string_ending_with_id" => "This is a string ending with id",
+ "employee_id" => "Employee id",
+ "employee_id_something_else" => "Employee id something else",
+ "underground" => "Underground",
+ "_id" => "Id",
+ "_external_id" => "External id"
}
UnderscoreToHumanWithoutCapitalize = {
@@ -221,23 +265,31 @@ module InflectorTestCases
"underground" => "underground"
}
+ MixtureToTitleCaseWithKeepIdSuffix = {
+ "this_is_a_string_ending_with_id" => "This Is A String Ending With Id",
+ "EmployeeId" => "Employee Id",
+ "Author Id" => "Author Id"
+ }
+
MixtureToTitleCase = {
- 'active_record' => 'Active Record',
- 'ActiveRecord' => 'Active Record',
- 'action web service' => 'Action Web Service',
- 'Action Web Service' => 'Action Web Service',
- 'Action web service' => 'Action Web Service',
- 'actionwebservice' => 'Actionwebservice',
- 'Actionwebservice' => 'Actionwebservice',
+ "active_record" => "Active Record",
+ "ActiveRecord" => "Active Record",
+ "action web service" => "Action Web Service",
+ "Action Web Service" => "Action Web Service",
+ "Action web service" => "Action Web Service",
+ "actionwebservice" => "Actionwebservice",
+ "Actionwebservice" => "Actionwebservice",
"david's code" => "David's Code",
"David's code" => "David's Code",
"david's Code" => "David's Code",
"sgt. pepper's" => "Sgt. Pepper's",
"i've just seen a face" => "I've Just Seen A Face",
"maybe you'll be there" => "Maybe You'll Be There",
- "¿por qué?" => '¿Por Qué?',
+ "¿por qué?" => "¿Por Qué?",
"Fred’s" => "Fred’s",
- "Fred`s" => "Fred`s"
+ "Fred`s" => "Fred`s",
+ "this was 'fake news'" => "This Was 'Fake News'",
+ ActiveSupport::SafeBuffer.new("confirmation num") => "Confirmation Num"
}
OrdinalNumbers = {
@@ -311,13 +363,13 @@ module InflectorTestCases
}
Irregularities = {
- 'person' => 'people',
- 'man' => 'men',
- 'child' => 'children',
- 'sex' => 'sexes',
- 'move' => 'moves',
- 'cow' => 'kine', # Test inflections with different starting letters
- 'zombie' => 'zombies',
- 'genus' => 'genera'
+ "person" => "people",
+ "man" => "men",
+ "child" => "children",
+ "sex" => "sexes",
+ "move" => "moves",
+ "cow" => "kine", # Test inflections with different starting letters
+ "zombie" => "zombies",
+ "genus" => "genera"
}
end
diff --git a/activesupport/test/json/decoding_test.rb b/activesupport/test/json/decoding_test.rb
index f2fc456f4b..8d9587f248 100644
--- a/activesupport/test/json/decoding_test.rb
+++ b/activesupport/test/json/decoding_test.rb
@@ -1,8 +1,13 @@
-require 'abstract_unit'
-require 'active_support/json'
-require 'active_support/time'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/json"
+require "active_support/time"
+require "time_zone_test_helpers"
class TestJSONDecoding < ActiveSupport::TestCase
+ include TimeZoneTestHelpers
+
class Foo
def self.json_create(object)
"Foo"
@@ -10,72 +15,80 @@ class TestJSONDecoding < ActiveSupport::TestCase
end
TESTS = {
- %q({"returnTo":{"\/categories":"\/"}}) => {"returnTo" => {"/categories" => "/"}},
- %q({"return\\"To\\":":{"\/categories":"\/"}}) => {"return\"To\":" => {"/categories" => "/"}},
- %q({"returnTo":{"\/categories":1}}) => {"returnTo" => {"/categories" => 1}},
- %({"returnTo":[1,"a"]}) => {"returnTo" => [1, "a"]},
- %({"returnTo":[1,"\\"a\\",", "b"]}) => {"returnTo" => [1, "\"a\",", "b"]},
- %({"a": "'", "b": "5,000"}) => {"a" => "'", "b" => "5,000"},
- %({"a": "a's, b's and c's", "b": "5,000"}) => {"a" => "a's, b's and c's", "b" => "5,000"},
+ %q({"returnTo":{"\/categories":"\/"}}) => { "returnTo" => { "/categories" => "/" } },
+ %q({"return\\"To\\":":{"\/categories":"\/"}}) => { "return\"To\":" => { "/categories" => "/" } },
+ %q({"returnTo":{"\/categories":1}}) => { "returnTo" => { "/categories" => 1 } },
+ %({"returnTo":[1,"a"]}) => { "returnTo" => [1, "a"] },
+ %({"returnTo":[1,"\\"a\\",", "b"]}) => { "returnTo" => [1, "\"a\",", "b"] },
+ %({"a": "'", "b": "5,000"}) => { "a" => "'", "b" => "5,000" },
+ %({"a": "a's, b's and c's", "b": "5,000"}) => { "a" => "a's, b's and c's", "b" => "5,000" },
# multibyte
- %({"matzue": "松江", "asakusa": "浅草"}) => {"matzue" => "松江", "asakusa" => "浅草"},
- %({"a": "2007-01-01"}) => {'a' => Date.new(2007, 1, 1)},
- %({"a": "2007-01-01 01:12:34 Z"}) => {'a' => Time.utc(2007, 1, 1, 1, 12, 34)},
+ %({"matzue": "松江", "asakusa": "浅草"}) => { "matzue" => "松江", "asakusa" => "浅草" },
+ %({"a": "2007-01-01"}) => { "a" => Date.new(2007, 1, 1) },
+ %({"a": "2007-01-01 01:12:34 Z"}) => { "a" => Time.utc(2007, 1, 1, 1, 12, 34) },
%(["2007-01-01 01:12:34 Z"]) => [Time.utc(2007, 1, 1, 1, 12, 34)],
%(["2007-01-01 01:12:34 Z", "2007-01-01 01:12:35 Z"]) => [Time.utc(2007, 1, 1, 1, 12, 34), Time.utc(2007, 1, 1, 1, 12, 35)],
# no time zone
- %({"a": "2007-01-01 01:12:34"}) => {'a' => "2007-01-01 01:12:34"},
+ %({"a": "2007-01-01 01:12:34"}) => { "a" => Time.new(2007, 1, 1, 1, 12, 34, "-05:00") },
# invalid date
- %({"a": "1089-10-40"}) => {'a' => "1089-10-40"},
+ %({"a": "1089-10-40"}) => { "a" => "1089-10-40" },
# xmlschema date notation
- %({"a": "2009-08-10T19:01:02Z"}) => {'a' => Time.utc(2009, 8, 10, 19, 1, 2)},
- %({"a": "2009-08-10T19:01:02+02:00"}) => {'a' => Time.utc(2009, 8, 10, 17, 1, 2)},
- %({"a": "2009-08-10T19:01:02-05:00"}) => {'a' => Time.utc(2009, 8, 11, 00, 1, 2)},
+ %({"a": "2009-08-10T19:01:02"}) => { "a" => Time.new(2009, 8, 10, 19, 1, 2, "-04:00") },
+ %({"a": "2009-08-10T19:01:02Z"}) => { "a" => Time.utc(2009, 8, 10, 19, 1, 2) },
+ %({"a": "2009-08-10T19:01:02+02:00"}) => { "a" => Time.utc(2009, 8, 10, 17, 1, 2) },
+ %({"a": "2009-08-10T19:01:02-05:00"}) => { "a" => Time.utc(2009, 8, 11, 00, 1, 2) },
# needs to be *exact*
- %({"a": " 2007-01-01 01:12:34 Z "}) => {'a' => " 2007-01-01 01:12:34 Z "},
- %({"a": "2007-01-01 : it's your birthday"}) => {'a' => "2007-01-01 : it's your birthday"},
+ %({"a": " 2007-01-01 01:12:34 Z "}) => { "a" => " 2007-01-01 01:12:34 Z " },
+ %({"a": "2007-01-01 : it's your birthday"}) => { "a" => "2007-01-01 : it's your birthday" },
%([]) => [],
%({}) => {},
- %({"a":1}) => {"a" => 1},
- %({"a": ""}) => {"a" => ""},
- %({"a":"\\""}) => {"a" => "\""},
- %({"a": null}) => {"a" => nil},
- %({"a": true}) => {"a" => true},
- %({"a": false}) => {"a" => false},
- %q({"bad":"\\\\","trailing":""}) => {"bad" => "\\", "trailing" => ""},
- %q({"a": "http:\/\/test.host\/posts\/1"}) => {"a" => "http://test.host/posts/1"},
- %q({"a": "\u003cunicode\u0020escape\u003e"}) => {"a" => "<unicode escape>"},
- %q({"a": "\\\\u0020skip double backslashes"}) => {"a" => "\\u0020skip double backslashes"},
- %q({"a": "\u003cbr /\u003e"}) => {'a' => "<br />"},
- %q({"b":["\u003ci\u003e","\u003cb\u003e","\u003cu\u003e"]}) => {'b' => ["<i>","<b>","<u>"]},
+ %({"a":1}) => { "a" => 1 },
+ %({"a": ""}) => { "a" => "" },
+ %({"a":"\\""}) => { "a" => "\"" },
+ %({"a": null}) => { "a" => nil },
+ %({"a": true}) => { "a" => true },
+ %({"a": false}) => { "a" => false },
+ '{"bad":"\\\\","trailing":""}' => { "bad" => "\\", "trailing" => "" },
+ %q({"a": "http:\/\/test.host\/posts\/1"}) => { "a" => "http://test.host/posts/1" },
+ %q({"a": "\u003cunicode\u0020escape\u003e"}) => { "a" => "<unicode escape>" },
+ '{"a": "\\\\u0020skip double backslashes"}' => { "a" => "\\u0020skip double backslashes" },
+ %q({"a": "\u003cbr /\u003e"}) => { "a" => "<br />" },
+ %q({"b":["\u003ci\u003e","\u003cb\u003e","\u003cu\u003e"]}) => { "b" => ["<i>", "<b>", "<u>"] },
# test combination of dates and escaped or unicode encoded data in arrays
%q([{"d":"1970-01-01", "s":"\u0020escape"},{"d":"1970-01-01", "s":"\u0020escape"}]) =>
- [{'d' => Date.new(1970, 1, 1), 's' => ' escape'},{'d' => Date.new(1970, 1, 1), 's' => ' escape'}],
+ [{ "d" => Date.new(1970, 1, 1), "s" => " escape" }, { "d" => Date.new(1970, 1, 1), "s" => " escape" }],
%q([{"d":"1970-01-01","s":"http:\/\/example.com"},{"d":"1970-01-01","s":"http:\/\/example.com"}]) =>
- [{'d' => Date.new(1970, 1, 1), 's' => 'http://example.com'},
- {'d' => Date.new(1970, 1, 1), 's' => 'http://example.com'}],
+ [{ "d" => Date.new(1970, 1, 1), "s" => "http://example.com" },
+ { "d" => Date.new(1970, 1, 1), "s" => "http://example.com" }],
# tests escaping of "\n" char with Yaml backend
- %q({"a":"\n"}) => {"a"=>"\n"},
- %q({"a":"\u000a"}) => {"a"=>"\n"},
- %q({"a":"Line1\u000aLine2"}) => {"a"=>"Line1\nLine2"},
+ %q({"a":"\n"}) => { "a" => "\n" },
+ %q({"a":"\u000a"}) => { "a" => "\n" },
+ %q({"a":"Line1\u000aLine2"}) => { "a" => "Line1\nLine2" },
# prevent json unmarshalling
- %q({"json_class":"TestJSONDecoding::Foo"}) => {"json_class"=>"TestJSONDecoding::Foo"},
+ '{"json_class":"TestJSONDecoding::Foo"}' => { "json_class" => "TestJSONDecoding::Foo" },
# json "fragments" - these are invalid JSON, but ActionPack relies on this
- %q("a string") => "a string",
- %q(1.1) => 1.1,
- %q(1) => 1,
- %q(-1) => -1,
- %q(true) => true,
- %q(false) => false,
- %q(null) => nil
+ '"a string"' => "a string",
+ "1.1" => 1.1,
+ "1" => 1,
+ "-1" => -1,
+ "true" => true,
+ "false" => false,
+ "null" => nil
}
TESTS.each_with_index do |(json, expected), index|
+ fail_message = "JSON decoding failed for #{json}"
+
test "json decodes #{index}" do
- with_parse_json_times(true) do
- silence_warnings do
- assert_equal expected, ActiveSupport::JSON.decode(json), "JSON decoding \
- failed for #{json}"
+ with_tz_default "Eastern Time (US & Canada)" do
+ with_parse_json_times(true) do
+ silence_warnings do
+ 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
end
@@ -83,7 +96,7 @@ class TestJSONDecoding < ActiveSupport::TestCase
test "json decodes time json with time parsing disabled" do
with_parse_json_times(false) do
- expected = {"a" => "2007-01-01 01:12:34 Z"}
+ expected = { "a" => "2007-01-01 01:12:34 Z" }
assert_equal expected, ActiveSupport::JSON.decode(%({"a": "2007-01-01 01:12:34 Z"}))
end
end
@@ -101,12 +114,11 @@ class TestJSONDecoding < ActiveSupport::TestCase
private
- def with_parse_json_times(value)
- old_value = ActiveSupport.parse_json_times
- ActiveSupport.parse_json_times = value
- yield
- ensure
- ActiveSupport.parse_json_times = old_value
- end
+ def with_parse_json_times(value)
+ old_value = ActiveSupport.parse_json_times
+ ActiveSupport.parse_json_times = value
+ yield
+ ensure
+ ActiveSupport.parse_json_times = old_value
+ end
end
-
diff --git a/activesupport/test/json/encoding_test.rb b/activesupport/test/json/encoding_test.rb
index 9f4b62fd8b..8062873386 100644
--- a/activesupport/test/json/encoding_test.rb
+++ b/activesupport/test/json/encoding_test.rb
@@ -1,17 +1,22 @@
-require 'securerandom'
-require 'abstract_unit'
-require 'active_support/core_ext/string/inflections'
-require 'active_support/json'
-require 'active_support/time'
-require 'time_zone_test_helpers'
-require 'json/encoding_test_cases'
+# frozen_string_literal: true
+
+require "securerandom"
+require "abstract_unit"
+require "active_support/core_ext/string/inflections"
+require "active_support/json"
+require "active_support/time"
+require "time_zone_test_helpers"
+require "json/encoding_test_cases"
class TestJSONEncoding < ActiveSupport::TestCase
include TimeZoneTestHelpers
def sorted_json(json)
- return json unless json =~ /^\{.*\}$/
- '{' + json[1..-2].split(',').sort.join(',') + '}'
+ if json.start_with?("{") && json.end_with?("}")
+ "{" + json[1..-2].split(",").sort.join(",") + "}"
+ else
+ json
+ end
end
JSONTest::EncodingTestCases.constants.each do |class_tests|
@@ -19,8 +24,10 @@ class TestJSONEncoding < ActiveSupport::TestCase
begin
prev = ActiveSupport.use_standard_json_time_format
- ActiveSupport.escape_html_entities_in_json = class_tests !~ /^Standard/
- ActiveSupport.use_standard_json_time_format = class_tests =~ /^Standard/
+ standard_class_tests = /Standard/.match?(class_tests)
+
+ ActiveSupport.escape_html_entities_in_json = !standard_class_tests
+ ActiveSupport.use_standard_json_time_format = standard_class_tests
JSONTest::EncodingTestCases.const_get(class_tests).each do |pair|
assert_equal pair.last, sorted_json(ActiveSupport::JSON.encode(pair.first))
end
@@ -41,12 +48,12 @@ class TestJSONEncoding < ActiveSupport::TestCase
end
def test_hash_encoding
- assert_equal %({\"a\":\"b\"}), ActiveSupport::JSON.encode(:a => :b)
- assert_equal %({\"a\":1}), ActiveSupport::JSON.encode('a' => 1)
- assert_equal %({\"a\":[1,2]}), ActiveSupport::JSON.encode('a' => [1,2])
+ assert_equal %({\"a\":\"b\"}), ActiveSupport::JSON.encode(a: :b)
+ assert_equal %({\"a\":1}), ActiveSupport::JSON.encode("a" => 1)
+ assert_equal %({\"a\":[1,2]}), ActiveSupport::JSON.encode("a" => [1, 2])
assert_equal %({"1":2}), ActiveSupport::JSON.encode(1 => 2)
- assert_equal %({\"a\":\"b\",\"c\":\"d\"}), sorted_json(ActiveSupport::JSON.encode(:a => :b, :c => :d))
+ assert_equal %({\"a\":\"b\",\"c\":\"d\"}), sorted_json(ActiveSupport::JSON.encode(a: :b, c: :d))
end
def test_hash_keys_encoding
@@ -57,24 +64,24 @@ class TestJSONEncoding < ActiveSupport::TestCase
end
def test_utf8_string_encoded_properly
- result = ActiveSupport::JSON.encode('€2.99')
+ result = ActiveSupport::JSON.encode("€2.99")
assert_equal '"€2.99"', result
assert_equal(Encoding::UTF_8, result.encoding)
- result = ActiveSupport::JSON.encode('✎☺')
+ result = ActiveSupport::JSON.encode("✎☺")
assert_equal '"✎☺"', result
assert_equal(Encoding::UTF_8, result.encoding)
end
def test_non_utf8_string_transcodes
- s = '二'.encode('Shift_JIS')
+ s = "二".encode("Shift_JIS")
result = ActiveSupport::JSON.encode(s)
assert_equal '"二"', result
assert_equal Encoding::UTF_8, result.encoding
end
def test_wide_utf8_chars
- w = '𠜎'
+ w = "𠜎"
result = ActiveSupport::JSON.encode(w)
assert_equal '"𠜎"', result
end
@@ -83,33 +90,33 @@ class TestJSONEncoding < ActiveSupport::TestCase
hash = { string: "𐒑" }
json = ActiveSupport::JSON.encode(hash)
decoded_hash = ActiveSupport::JSON.decode(json)
- assert_equal "𐒑", decoded_hash['string']
+ assert_equal "𐒑", decoded_hash["string"]
end
def test_hash_key_identifiers_are_always_quoted
- values = {0 => 0, 1 => 1, :_ => :_, "$" => "$", "a" => "a", :A => :A, :A0 => :A0, "A0B" => "A0B"}
+ values = { 0 => 0, 1 => 1, :_ => :_, "$" => "$", "a" => "a", :A => :A, :A0 => :A0, "A0B" => "A0B" }
assert_equal %w( "$" "A" "A0" "A0B" "_" "a" "0" "1" ).sort, object_keys(ActiveSupport::JSON.encode(values))
end
def test_hash_should_allow_key_filtering_with_only
- assert_equal %({"a":1}), ActiveSupport::JSON.encode({'a' => 1, :b => 2, :c => 3}, :only => 'a')
+ assert_equal %({"a":1}), ActiveSupport::JSON.encode({ "a" => 1, :b => 2, :c => 3 }, { only: "a" })
end
def test_hash_should_allow_key_filtering_with_except
- assert_equal %({"b":2}), ActiveSupport::JSON.encode({'foo' => 'bar', :b => 2, :c => 3}, :except => ['foo', :c])
+ assert_equal %({"b":2}), ActiveSupport::JSON.encode({ "foo" => "bar", :b => 2, :c => 3 }, { except: ["foo", :c] })
end
def test_time_to_json_includes_local_offset
with_standard_json_time_format(true) do
- with_env_tz 'US/Eastern' do
- assert_equal %("2005-02-01T15:15:10.000-05:00"), ActiveSupport::JSON.encode(Time.local(2005,2,1,15,15,10))
+ with_env_tz "US/Eastern" do
+ assert_equal %("2005-02-01T15:15:10.000-05:00"), ActiveSupport::JSON.encode(Time.local(2005, 2, 1, 15, 15, 10))
end
end
end
def test_hash_with_time_to_json
with_standard_json_time_format(false) do
- assert_equal '{"time":"2009/01/01 00:00:00 +0000"}', { :time => Time.utc(2009) }.to_json
+ assert_equal '{"time":"2009/01/01 00:00:00 +0000"}', { time: Time.utc(2009) }.to_json
end
end
@@ -117,8 +124,8 @@ class TestJSONEncoding < ActiveSupport::TestCase
assert_nothing_raised do
hash = {
"CHI" => {
- :display_name => "chicago",
- :latitude => 123.234
+ display_name: "chicago",
+ latitude: 123.234
}
}
ActiveSupport::JSON.encode(hash)
@@ -127,64 +134,74 @@ class TestJSONEncoding < ActiveSupport::TestCase
def test_hash_like_with_options
h = JSONTest::Hashlike.new
- json = h.to_json :only => [:foo]
+ json = h.to_json only: [:foo]
- assert_equal({"foo"=>"hello"}, JSON.parse(json))
+ assert_equal({ "foo" => "hello" }, JSON.parse(json))
end
def test_object_to_json_with_options
obj = Object.new
obj.instance_variable_set :@foo, "hello"
obj.instance_variable_set :@bar, "world"
- json = obj.to_json :only => ["foo"]
+ json = obj.to_json only: ["foo"]
- assert_equal({"foo"=>"hello"}, JSON.parse(json))
+ assert_equal({ "foo" => "hello" }, JSON.parse(json))
end
def test_struct_to_json_with_options
struct = Struct.new(:foo, :bar).new
struct.foo = "hello"
struct.bar = "world"
- json = struct.to_json :only => [:foo]
+ json = struct.to_json only: [:foo]
+
+ assert_equal({ "foo" => "hello" }, JSON.parse(json))
+ end
+
+ def test_struct_to_json_with_options_nested
+ klass = Struct.new(:foo, :bar)
+ struct = klass.new "hello", "world"
+ parent_struct = klass.new struct, "world"
+ json = parent_struct.to_json only: [:foo]
- assert_equal({"foo"=>"hello"}, JSON.parse(json))
+ assert_equal({ "foo" => { "foo" => "hello" } }, JSON.parse(json))
end
+
def test_hash_should_pass_encoding_options_to_children_in_as_json
person = {
- :name => 'John',
- :address => {
- :city => 'London',
- :country => 'UK'
+ name: "John",
+ address: {
+ city: "London",
+ country: "UK"
}
}
- json = person.as_json :only => [:address, :city]
+ json = person.as_json only: [:address, :city]
- assert_equal({ 'address' => { 'city' => 'London' }}, json)
+ assert_equal({ "address" => { "city" => "London" } }, json)
end
def test_hash_should_pass_encoding_options_to_children_in_to_json
person = {
- :name => 'John',
- :address => {
- :city => 'London',
- :country => 'UK'
+ name: "John",
+ address: {
+ city: "London",
+ country: "UK"
}
}
- json = person.to_json :only => [:address, :city]
+ json = person.to_json only: [:address, :city]
assert_equal(%({"address":{"city":"London"}}), json)
end
def test_array_should_pass_encoding_options_to_children_in_as_json
people = [
- { :name => 'John', :address => { :city => 'London', :country => 'UK' }},
- { :name => 'Jean', :address => { :city => 'Paris' , :country => 'France' }}
+ { name: "John", address: { city: "London", country: "UK" } },
+ { name: "Jean", address: { city: "Paris", country: "France" } }
]
- json = people.as_json :only => [:address, :city]
+ json = people.as_json only: [:address, :city]
expected = [
- { 'address' => { 'city' => 'London' }},
- { 'address' => { 'city' => 'Paris' }}
+ { "address" => { "city" => "London" } },
+ { "address" => { "city" => "Paris" } }
]
assert_equal(expected, json)
@@ -192,20 +209,20 @@ class TestJSONEncoding < ActiveSupport::TestCase
def test_array_should_pass_encoding_options_to_children_in_to_json
people = [
- { :name => 'John', :address => { :city => 'London', :country => 'UK' }},
- { :name => 'Jean', :address => { :city => 'Paris' , :country => 'France' }}
+ { name: "John", address: { city: "London", country: "UK" } },
+ { name: "Jean", address: { city: "Paris", country: "France" } }
]
- json = people.to_json :only => [:address, :city]
+ json = people.to_json only: [:address, :city]
assert_equal(%([{"address":{"city":"London"}},{"address":{"city":"Paris"}}]), json)
end
People = Class.new(BasicObject) do
include Enumerable
- def initialize()
+ def initialize
@people = [
- { :name => 'John', :address => { :city => 'London', :country => 'UK' }},
- { :name => 'Jean', :address => { :city => 'Paris' , :country => 'France' }}
+ { name: "John", address: { city: "London", country: "UK" } },
+ { name: "Jean", address: { city: "Paris", country: "France" } }
]
end
def each(*, &blk)
@@ -217,32 +234,32 @@ class TestJSONEncoding < ActiveSupport::TestCase
end
def test_enumerable_should_generate_json_with_as_json
- json = People.new.as_json :only => [:address, :city]
+ json = People.new.as_json only: [:address, :city]
expected = [
- { 'address' => { 'city' => 'London' }},
- { 'address' => { 'city' => 'Paris' }}
+ { "address" => { "city" => "London" } },
+ { "address" => { "city" => "Paris" } }
]
assert_equal(expected, json)
end
def test_enumerable_should_generate_json_with_to_json
- json = People.new.to_json :only => [:address, :city]
+ json = People.new.to_json only: [:address, :city]
assert_equal(%([{"address":{"city":"London"}},{"address":{"city":"Paris"}}]), json)
end
def test_enumerable_should_pass_encoding_options_to_children_in_as_json
- json = People.new.each.as_json :only => [:address, :city]
+ json = People.new.each.as_json only: [:address, :city]
expected = [
- { 'address' => { 'city' => 'London' }},
- { 'address' => { 'city' => 'Paris' }}
+ { "address" => { "city" => "London" } },
+ { "address" => { "city" => "Paris" } }
]
assert_equal(expected, json)
end
def test_enumerable_should_pass_encoding_options_to_children_in_to_json
- json = People.new.each.to_json :only => [:address, :city]
+ json = People.new.each.to_json only: [:address, :city]
assert_equal(%([{"address":{"city":"London"}},{"address":{"city":"Paris"}}]), json)
end
@@ -250,7 +267,7 @@ class TestJSONEncoding < ActiveSupport::TestCase
class CustomWithOptions
attr_accessor :foo, :bar
- def as_json(options={})
+ def as_json(options = {})
options[:only] = %w(foo bar)
super(options)
end
@@ -261,9 +278,9 @@ class TestJSONEncoding < ActiveSupport::TestCase
f.foo = "hello"
f.bar = "world"
- hash = {"foo" => f, "other_hash" => {"foo" => "other_foo", "test" => "other_test"}}
- assert_equal({"foo"=>{"foo"=>"hello","bar"=>"world"},
- "other_hash" => {"foo"=>"other_foo","test"=>"other_test"}}, ActiveSupport::JSON.decode(hash.to_json))
+ hash = { "foo" => f, "other_hash" => { "foo" => "other_foo", "test" => "other_test" } }
+ assert_equal({ "foo" => { "foo" => "hello", "bar" => "world" },
+ "other_hash" => { "foo" => "other_foo", "test" => "other_test" } }, ActiveSupport::JSON.decode(hash.to_json))
end
def test_array_to_json_should_not_keep_options_around
@@ -271,9 +288,9 @@ class TestJSONEncoding < ActiveSupport::TestCase
f.foo = "hello"
f.bar = "world"
- array = [f, {"foo" => "other_foo", "test" => "other_test"}]
- assert_equal([{"foo"=>"hello","bar"=>"world"},
- {"foo"=>"other_foo","test"=>"other_test"}], ActiveSupport::JSON.decode(array.to_json))
+ array = [f, { "foo" => "other_foo", "test" => "other_test" }]
+ assert_equal([{ "foo" => "hello", "bar" => "world" },
+ { "foo" => "other_foo", "test" => "other_test" }], ActiveSupport::JSON.decode(array.to_json))
end
class OptionsTest
@@ -284,7 +301,7 @@ class TestJSONEncoding < ActiveSupport::TestCase
def test_hash_as_json_without_options
json = { foo: OptionsTest.new }.as_json
- assert_equal({"foo" => :default}, json)
+ assert_equal({ "foo" => :default }, json)
end
def test_array_as_json_without_options
@@ -293,13 +310,12 @@ class TestJSONEncoding < ActiveSupport::TestCase
end
def test_struct_encoding
- Struct.new('UserNameAndEmail', :name, :email)
- Struct.new('UserNameAndDate', :name, :date)
- Struct.new('Custom', :name, :sub)
- user_email = Struct::UserNameAndEmail.new 'David', 'sample@example.com'
- user_birthday = Struct::UserNameAndDate.new 'David', Date.new(2010, 01, 01)
- custom = Struct::Custom.new 'David', user_birthday
-
+ Struct.new("UserNameAndEmail", :name, :email)
+ Struct.new("UserNameAndDate", :name, :date)
+ Struct.new("Custom", :name, :sub)
+ user_email = Struct::UserNameAndEmail.new "David", "sample@example.com"
+ user_birthday = Struct::UserNameAndDate.new "David", Date.new(2010, 01, 01)
+ custom = Struct::Custom.new "David", user_birthday
json_strings = ""
json_string_and_date = ""
@@ -311,20 +327,20 @@ class TestJSONEncoding < ActiveSupport::TestCase
json_custom = custom.to_json
end
- assert_equal({"name" => "David",
+ assert_equal({ "name" => "David",
"sub" => {
"name" => "David",
- "date" => "2010-01-01" }}, ActiveSupport::JSON.decode(json_custom))
+ "date" => "2010-01-01" } }, ActiveSupport::JSON.decode(json_custom))
- assert_equal({"name" => "David", "email" => "sample@example.com"},
+ assert_equal({ "name" => "David", "email" => "sample@example.com" },
ActiveSupport::JSON.decode(json_strings))
- assert_equal({"name" => "David", "date" => "2010-01-01"},
+ assert_equal({ "name" => "David", "date" => "2010-01-01" },
ActiveSupport::JSON.decode(json_string_and_date))
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
@@ -336,7 +352,7 @@ class TestJSONEncoding < ActiveSupport::TestCase
super
end
- def as_json(options={})
+ def as_json(options = {})
@as_json_called = true
super
end
@@ -376,7 +392,7 @@ EXPECTED
def test_twz_to_json_with_use_standard_json_time_format_config_set_to_false
with_standard_json_time_format(false) do
- zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
time = ActiveSupport::TimeWithZone.new(Time.utc(2000), zone)
assert_equal "\"1999/12/31 19:00:00 -0500\"", ActiveSupport::JSON.encode(time)
end
@@ -384,7 +400,7 @@ EXPECTED
def test_twz_to_json_with_use_standard_json_time_format_config_set_to_true
with_standard_json_time_format(true) do
- zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
time = ActiveSupport::TimeWithZone.new(Time.utc(2000), zone)
assert_equal "\"1999-12-31T19:00:00.000-05:00\"", ActiveSupport::JSON.encode(time)
end
@@ -393,7 +409,7 @@ EXPECTED
def test_twz_to_json_with_custom_time_precision
with_standard_json_time_format(true) do
with_time_precision(0) do
- zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
time = ActiveSupport::TimeWithZone.new(Time.utc(2000), zone)
assert_equal "\"1999-12-31T19:00:00-05:00\"", ActiveSupport::JSON.encode(time)
end
@@ -417,12 +433,41 @@ EXPECTED
end
def test_twz_to_json_when_wrapping_a_date_time
- zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
time = ActiveSupport::TimeWithZone.new(DateTime.new(2000), zone)
assert_equal '"1999-12-31T19:00:00.000-05:00"', ActiveSupport::JSON.encode(time)
end
- protected
+ def test_exception_to_json
+ exception = Exception.new("foo")
+ assert_equal '"foo"', ActiveSupport::JSON.encode(exception)
+ end
+
+ class InfiniteNumber
+ def as_json(options = nil)
+ { "number" => Float::INFINITY }
+ end
+ end
+
+ def test_to_json_works_when_as_json_returns_infinite_number
+ assert_equal '{"number":null}', InfiniteNumber.new.to_json
+ end
+
+ class NaNNumber
+ def as_json(options = nil)
+ { "number" => Float::NAN }
+ end
+ end
+
+ def test_to_json_works_when_as_json_returns_NaN_number
+ assert_equal '{"number":null}', NaNNumber.new.to_json
+ end
+
+ def test_to_json_works_on_io_objects
+ assert_equal STDOUT.to_s.to_json, STDOUT.to_json
+ end
+
+ private
def object_keys(json_object)
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 0159ba8606..5a4700459f 100644
--- a/activesupport/test/json/encoding_test_cases.rb
+++ b/activesupport/test/json/encoding_test_cases.rb
@@ -1,4 +1,10 @@
-require 'bigdecimal'
+# frozen_string_literal: true
+
+require "bigdecimal"
+require "date"
+require "time"
+require "pathname"
+require "uri"
module JSONTest
class Foo
@@ -9,7 +15,7 @@ module JSONTest
class Hashlike
def to_hash
- { :foo => "hello", :bar => "world" }
+ { foo: "hello", bar: "world" }
end
end
@@ -23,7 +29,7 @@ module JSONTest
end
end
- class MyStruct < Struct.new(:name, :value)
+ MyStruct = Struct.new(:name, :value) do
def initialize(*)
@unused = "unused instance variable"
super
@@ -36,23 +42,23 @@ module JSONTest
NilTests = [[ nil, %(null) ]]
NumericTests = [[ 1, %(1) ],
[ 2.5, %(2.5) ],
- [ 0.0/0.0, %(null) ],
- [ 1.0/0.0, %(null) ],
- [ -1.0/0.0, %(null) ],
- [ BigDecimal('0.0')/BigDecimal('0.0'), %(null) ],
- [ BigDecimal('2.5'), %("#{BigDecimal('2.5')}") ]]
+ [ 0.0 / 0.0, %(null) ],
+ [ 1.0 / 0.0, %(null) ],
+ [ -1.0 / 0.0, %(null) ],
+ [ BigDecimal("0.0") / BigDecimal("0.0"), %(null) ],
+ [ BigDecimal("2.5"), %("#{BigDecimal('2.5')}") ]]
- StringTests = [[ 'this is the <string>', %("this is the \\u003cstring\\u003e")],
+ StringTests = [[ "this is the <string>", %("this is the \\u003cstring\\u003e")],
[ 'a "string" with quotes & an ampersand', %("a \\"string\\" with quotes \\u0026 an ampersand") ],
- [ 'http://test.host/posts/1', %("http://test.host/posts/1")],
+ [ "http://test.host/posts/1", %("http://test.host/posts/1")],
[ "Control characters: \x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\u2028\u2029",
%("Control characters: \\u0000\\u0001\\u0002\\u0003\\u0004\\u0005\\u0006\\u0007\\b\\t\\n\\u000b\\f\\r\\u000e\\u000f\\u0010\\u0011\\u0012\\u0013\\u0014\\u0015\\u0016\\u0017\\u0018\\u0019\\u001a\\u001b\\u001c\\u001d\\u001e\\u001f\\u2028\\u2029") ]]
- ArrayTests = [[ ['a', 'b', 'c'], %([\"a\",\"b\",\"c\"]) ],
- [ [1, 'a', :b, nil, false], %([1,\"a\",\"b\",null,false]) ]]
+ ArrayTests = [[ ["a", "b", "c"], %([\"a\",\"b\",\"c\"]) ],
+ [ [1, "a", :b, nil, false], %([1,\"a\",\"b\",null,false]) ]]
- HashTests = [[ {foo: "bar"}, %({\"foo\":\"bar\"}) ],
- [ {1 => 1, 2 => 'a', 3 => :b, 4 => nil, 5 => false}, %({\"1\":1,\"2\":\"a\",\"3\":\"b\",\"4\":null,\"5\":false}) ]]
+ HashTests = [[ { foo: "bar" }, %({\"foo\":\"bar\"}) ],
+ [ { 1 => 1, 2 => "a", 3 => :b, 4 => nil, 5 => false }, %({\"1\":1,\"2\":\"a\",\"3\":\"b\",\"4\":null,\"5\":false}) ]]
RangeTests = [[ 1..2, %("1..2")],
[ 1...2, %("1...2")],
@@ -67,22 +73,26 @@ module JSONTest
StructTests = [[ MyStruct.new(:foo, "bar"), %({\"name\":\"foo\",\"value\":\"bar\"}) ],
[ MyStruct.new(nil, nil), %({\"name\":null,\"value\":null}) ]]
CustomTests = [[ Custom.new("custom"), '"custom"' ],
- [ Custom.new(nil), 'null' ],
+ [ Custom.new(nil), "null" ],
[ Custom.new(:a), '"a"' ],
[ Custom.new([ :foo, "bar" ]), '["foo","bar"]' ],
- [ Custom.new({ :foo => "hello", :bar => "world" }), '{"bar":"world","foo":"hello"}' ],
+ [ Custom.new(foo: "hello", bar: "world"), '{"bar":"world","foo":"hello"}' ],
[ Custom.new(Hashlike.new), '{"bar":"world","foo":"hello"}' ],
[ Custom.new(Custom.new(Custom.new(:a))), '"a"' ]]
RegexpTests = [[ /^a/, '"(?-mix:^a)"' ], [/^\w{1,2}[a-z]+/ix, '"(?ix-m:^\\\\w{1,2}[a-z]+)"']]
- DateTests = [[ Date.new(2005,2,1), %("2005/02/01") ]]
- TimeTests = [[ Time.utc(2005,2,1,15,15,10), %("2005/02/01 15:15:10 +0000") ]]
- DateTimeTests = [[ DateTime.civil(2005,2,1,15,15,10), %("2005/02/01 15:15:10 +0000") ]]
+ URITests = [[ URI.parse("http://example.com"), %("http://example.com") ]]
+
+ PathnameTests = [[ Pathname.new("lib/index.rb"), %("lib/index.rb") ]]
+
+ DateTests = [[ Date.new(2005, 2, 1), %("2005/02/01") ]]
+ TimeTests = [[ Time.utc(2005, 2, 1, 15, 15, 10), %("2005/02/01 15:15:10 +0000") ]]
+ DateTimeTests = [[ DateTime.civil(2005, 2, 1, 15, 15, 10), %("2005/02/01 15:15:10 +0000") ]]
- StandardDateTests = [[ Date.new(2005,2,1), %("2005-02-01") ]]
- StandardTimeTests = [[ Time.utc(2005,2,1,15,15,10), %("2005-02-01T15:15:10.000Z") ]]
- StandardDateTimeTests = [[ DateTime.civil(2005,2,1,15,15,10), %("2005-02-01T15:15:10.000+00:00") ]]
- StandardStringTests = [[ 'this is the <string>', %("this is the <string>")]]
+ StandardDateTests = [[ Date.new(2005, 2, 1), %("2005-02-01") ]]
+ StandardTimeTests = [[ Time.utc(2005, 2, 1, 15, 15, 10), %("2005-02-01T15:15:10.000Z") ]]
+ StandardDateTimeTests = [[ DateTime.civil(2005, 2, 1, 15, 15, 10), %("2005-02-01T15:15:10.000+00:00") ]]
+ StandardStringTests = [[ "this is the <string>", %("this is the <string>")]]
end
end
diff --git a/activesupport/test/key_generator_test.rb b/activesupport/test/key_generator_test.rb
index f7e8e9a795..9dfc0b2154 100644
--- a/activesupport/test/key_generator_test.rb
+++ b/activesupport/test/key_generator_test.rb
@@ -1,62 +1,76 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
begin
- require 'openssl'
+ require "openssl"
OpenSSL::PKCS5
rescue LoadError, NameError
$stderr.puts "Skipping KeyGenerator test: broken OpenSSL install"
else
-require 'active_support/time'
-require 'active_support/json'
+ class KeyGeneratorTest < ActiveSupport::TestCase
+ def setup
+ @secret = SecureRandom.hex(64)
+ @generator = ActiveSupport::KeyGenerator.new(@secret, iterations: 2)
+ end
-class KeyGeneratorTest < ActiveSupport::TestCase
- def setup
- @secret = SecureRandom.hex(64)
- @generator = ActiveSupport::KeyGenerator.new(@secret, :iterations=>2)
- end
+ test "Generating a key of the default length" do
+ derived_key = @generator.generate_key("some_salt")
+ assert_kind_of String, derived_key
+ assert_equal 64, derived_key.length, "Should have generated a key of the default size"
+ end
- test "Generating a key of the default length" do
- derived_key = @generator.generate_key("some_salt")
- assert_kind_of String, derived_key
- assert_equal OpenSSL::Digest::SHA1.new.block_length, derived_key.length, "Should have generated a key of the default size"
- end
+ test "Generating a key of an alternative length" do
+ derived_key = @generator.generate_key("some_salt", 32)
+ assert_kind_of String, derived_key
+ assert_equal 32, derived_key.length, "Should have generated a key of the right size"
+ end
- test "Generating a key of an alternative length" do
- derived_key = @generator.generate_key("some_salt", 32)
- assert_kind_of String, derived_key
- assert_equal 32, derived_key.length, "Should have generated a key of the right size"
- end
-end
+ test "Expected results" do
+ # For any given set of inputs, this method must continue to return
+ # the same output: if it changes, any existing values relying on a
+ # key would break.
-class CachingKeyGeneratorTest < ActiveSupport::TestCase
- def setup
- @secret = SecureRandom.hex(64)
- @generator = ActiveSupport::KeyGenerator.new(@secret, :iterations=>2)
- @caching_generator = ActiveSupport::CachingKeyGenerator.new(@generator)
- end
+ expected = "b129376f68f1ecae788d7433310249d65ceec090ecacd4c872a3a9e9ec78e055739be5cc6956345d5ae38e7e1daa66f1de587dc8da2bf9e8b965af4b3918a122"
+ assert_equal expected, ActiveSupport::KeyGenerator.new("0" * 64).generate_key("some_salt").unpack1("H*")
- test "Generating a cached key for same salt and key size" do
- derived_key = @caching_generator.generate_key("some_salt", 32)
- cached_key = @caching_generator.generate_key("some_salt", 32)
+ expected = "b129376f68f1ecae788d7433310249d65ceec090ecacd4c872a3a9e9ec78e055"
+ assert_equal expected, ActiveSupport::KeyGenerator.new("0" * 64).generate_key("some_salt", 32).unpack1("H*")
- assert_equal derived_key, cached_key
- assert_equal derived_key.object_id, cached_key.object_id
+ expected = "cbea7f7f47df705967dc508f4e446fd99e7797b1d70011c6899cd39bbe62907b8508337d678505a7dc8184e037f1003ba3d19fc5d829454668e91d2518692eae"
+ assert_equal expected, ActiveSupport::KeyGenerator.new("0" * 64, iterations: 2).generate_key("some_salt").unpack1("H*")
+ end
end
- test "Does not cache key for different salt" do
- derived_key = @caching_generator.generate_key("some_salt", 32)
- different_salt_key = @caching_generator.generate_key("other_salt", 32)
+ class CachingKeyGeneratorTest < ActiveSupport::TestCase
+ def setup
+ @secret = SecureRandom.hex(64)
+ @generator = ActiveSupport::KeyGenerator.new(@secret, iterations: 2)
+ @caching_generator = ActiveSupport::CachingKeyGenerator.new(@generator)
+ end
- assert_not_equal derived_key, different_salt_key
- end
+ test "Generating a cached key for same salt and key size" do
+ derived_key = @caching_generator.generate_key("some_salt", 32)
+ cached_key = @caching_generator.generate_key("some_salt", 32)
+
+ assert_equal derived_key, cached_key
+ assert_equal derived_key.object_id, cached_key.object_id
+ end
+
+ test "Does not cache key for different salt" do
+ derived_key = @caching_generator.generate_key("some_salt", 32)
+ different_salt_key = @caching_generator.generate_key("other_salt", 32)
- test "Does not cache key for different length" do
- derived_key = @caching_generator.generate_key("some_salt", 32)
- different_length_key = @caching_generator.generate_key("some_salt", 64)
+ assert_not_equal derived_key, different_salt_key
+ end
- assert_not_equal derived_key, different_length_key
+ test "Does not cache key for different length" do
+ derived_key = @caching_generator.generate_key("some_salt", 32)
+ different_length_key = @caching_generator.generate_key("some_salt", 64)
+
+ assert_not_equal derived_key, different_length_key
+ end
end
-end
end
diff --git a/activesupport/test/lazy_load_hooks_test.rb b/activesupport/test/lazy_load_hooks_test.rb
index 7851634dbf..50a703e49f 100644
--- a/activesupport/test/lazy_load_hooks_test.rb
+++ b/activesupport/test/lazy_load_hooks_test.rb
@@ -1,4 +1,7 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/module/remove_method"
class LazyLoadHooksTest < ActiveSupport::TestCase
def test_basic_hook
@@ -18,6 +21,23 @@ class LazyLoadHooksTest < ActiveSupport::TestCase
assert_equal 7, i
end
+ def test_basic_hook_with_two_registrations_only_once
+ i = 0
+ block = proc { i += incr }
+ ActiveSupport.on_load(:basic_hook_with_two_once, run_once: true, &block)
+ ActiveSupport.on_load(:basic_hook_with_two_once) do
+ i += incr
+ end
+
+ ActiveSupport.on_load(:different_hook, run_once: true, &block)
+ ActiveSupport.run_load_hooks(:different_hook, FakeContext.new(2))
+ assert_equal 2, i
+ ActiveSupport.run_load_hooks(:basic_hook_with_two_once, FakeContext.new(2))
+ assert_equal 6, i
+ ActiveSupport.run_load_hooks(:basic_hook_with_two_once, FakeContext.new(5))
+ assert_equal 11, i
+ end
+
def test_hook_registered_after_run
i = 0
ActiveSupport.run_load_hooks(:registered_after)
@@ -35,6 +55,15 @@ class LazyLoadHooksTest < ActiveSupport::TestCase
assert_equal 7, i
end
+ def test_hook_registered_after_run_with_two_registrations_only_once
+ i = 0
+ ActiveSupport.run_load_hooks(:registered_after_with_two_once, FakeContext.new(2))
+ ActiveSupport.run_load_hooks(:registered_after_with_two_once, FakeContext.new(5))
+ assert_equal 0, i
+ ActiveSupport.on_load(:registered_after_with_two_once, run_once: true) { i += incr }
+ assert_equal 2, i
+ end
+
def test_hook_registered_interleaved_run_with_two_registrations
i = 0
ActiveSupport.run_load_hooks(:registered_interleaved_with_two, FakeContext.new(2))
@@ -45,6 +74,22 @@ class LazyLoadHooksTest < ActiveSupport::TestCase
assert_equal 7, i
end
+ def test_hook_registered_interleaved_run_with_two_registrations_once
+ i = 0
+ ActiveSupport
+ .run_load_hooks(:registered_interleaved_with_two_once, FakeContext.new(2))
+ assert_equal 0, i
+
+ ActiveSupport.on_load(:registered_interleaved_with_two_once, run_once: true) do
+ i += incr
+ end
+ assert_equal 2, i
+
+ ActiveSupport
+ .run_load_hooks(:registered_interleaved_with_two_once, FakeContext.new(5))
+ assert_equal 2, i
+ end
+
def test_hook_receives_a_context
i = 0
ActiveSupport.on_load(:contextual) { i += incr }
@@ -63,7 +108,7 @@ class LazyLoadHooksTest < ActiveSupport::TestCase
def test_hook_with_yield_true
i = 0
- ActiveSupport.on_load(:contextual_yield, :yield => true) do |obj|
+ ActiveSupport.on_load(:contextual_yield, yield: true) do |obj|
i += obj.incr + incr_amt
end
assert_equal 0, i
@@ -75,12 +120,60 @@ class LazyLoadHooksTest < ActiveSupport::TestCase
i = 0
ActiveSupport.run_load_hooks(:contextual_yield_after, FakeContext.new(2))
assert_equal 0, i
- ActiveSupport.on_load(:contextual_yield_after, :yield => true) do |obj|
+ ActiveSupport.on_load(:contextual_yield_after, yield: true) do |obj|
i += obj.incr + incr_amt
end
assert_equal 7, i
end
+ def test_hook_uses_class_eval_when_base_is_a_class
+ ActiveSupport.on_load(:uses_class_eval) do
+ def first_wrestler
+ "John Cena"
+ end
+ end
+
+ ActiveSupport.run_load_hooks(:uses_class_eval, FakeContext)
+ assert_equal "John Cena", FakeContext.new(0).first_wrestler
+ ensure
+ FakeContext.remove_possible_method(:first_wrestler)
+ end
+
+ def test_hook_uses_class_eval_when_base_is_a_module
+ mod = Module.new
+ ActiveSupport.on_load(:uses_class_eval2) do
+ def last_wrestler
+ "Dwayne Johnson"
+ end
+ end
+ ActiveSupport.run_load_hooks(:uses_class_eval2, mod)
+
+ klass = Class.new do
+ include mod
+ end
+
+ assert_equal "Dwayne Johnson", klass.new.last_wrestler
+ end
+
+ def test_hook_uses_instance_eval_when_base_is_an_instance
+ ActiveSupport.on_load(:uses_instance_eval) do
+ def second_wrestler
+ "Hulk Hogan"
+ end
+ end
+
+ context = FakeContext.new(1)
+ ActiveSupport.run_load_hooks(:uses_instance_eval, context)
+
+ assert_raises NoMethodError do
+ FakeContext.new(2).second_wrestler
+ end
+ assert_raises NoMethodError do
+ FakeContext.second_wrestler
+ end
+ assert_equal "Hulk Hogan", context.second_wrestler
+ end
+
private
def incr_amt
@@ -93,4 +186,4 @@ private
@incr = incr
end
end
-end \ No newline at end of file
+end
diff --git a/activesupport/test/load_paths_test.rb b/activesupport/test/load_paths_test.rb
deleted file mode 100644
index ac617a9fd8..0000000000
--- a/activesupport/test/load_paths_test.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-require 'abstract_unit'
-
-class LoadPathsTest < ActiveSupport::TestCase
- def test_uniq_load_paths
- load_paths_count = $LOAD_PATH.inject({}) { |paths, path|
- expanded_path = File.expand_path(path)
- paths[expanded_path] ||= 0
- paths[expanded_path] += 1
- paths
- }
- load_paths_count[File.expand_path('../../lib', __FILE__)] -= 1
-
- load_paths_count.select! { |k, v| v > 1 }
- assert load_paths_count.empty?, load_paths_count.inspect
- end
-end
diff --git a/activesupport/test/log_subscriber_test.rb b/activesupport/test/log_subscriber_test.rb
index 998a6887c5..7f05459493 100644
--- a/activesupport/test/log_subscriber_test.rb
+++ b/activesupport/test/log_subscriber_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'active_support/log_subscriber/test_helper'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/log_subscriber/test_helper"
class MyLogSubscriber < ActiveSupport::LogSubscriber
attr_reader :event
@@ -73,6 +75,22 @@ class SyncLogSubscriberTest < ActiveSupport::TestCase
assert_kind_of ActiveSupport::Notifications::Event, @log_subscriber.event
end
+ def test_event_attributes
+ ActiveSupport::LogSubscriber.attach_to :my_log_subscriber, @log_subscriber
+ instrument "some_event.my_log_subscriber"
+ wait
+ event = @log_subscriber.event
+ if defined?(JRUBY_VERSION)
+ assert_equal 0, event.cpu_time
+ assert_equal 0, event.allocations
+ else
+ assert_operator event.cpu_time, :>, 0
+ assert_operator event.allocations, :>, 0
+ end
+ assert_operator event.duration, :>, 0
+ assert_operator event.idle_time, :>, 0
+ end
+
def test_does_not_send_the_event_if_it_doesnt_match_the_class
ActiveSupport::LogSubscriber.attach_to :my_log_subscriber, @log_subscriber
instrument "unknown_event.my_log_subscriber"
@@ -116,7 +134,7 @@ class SyncLogSubscriberTest < ActiveSupport::TestCase
wait
assert_equal 1, @logger.logged(:info).size
- assert_equal 'some_event.my_log_subscriber', @logger.logged(:info).last
+ assert_equal "some_event.my_log_subscriber", @logger.logged(:info).last
assert_equal 1, @logger.logged(:error).size
assert_match 'Could not log "puke.my_log_subscriber" event. RuntimeError: puke', @logger.logged(:error).last
diff --git a/activesupport/test/logger_test.rb b/activesupport/test/logger_test.rb
index d2801849ca..160e1156b6 100644
--- a/activesupport/test/logger_test.rb
+++ b/activesupport/test/logger_test.rb
@@ -1,8 +1,12 @@
-require 'abstract_unit'
-require 'multibyte_test_helpers'
-require 'stringio'
-require 'fileutils'
-require 'tempfile'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "multibyte_test_helpers"
+require "stringio"
+require "fileutils"
+require "tempfile"
+require "tmpdir"
+require "concurrent/atomics"
class LoggerTest < ActiveSupport::TestCase
include MultibyteTestHelpers
@@ -16,19 +20,27 @@ class LoggerTest < ActiveSupport::TestCase
@logger = Logger.new(@output)
end
+ def test_log_outputs_to
+ assert Logger.logger_outputs_to?(@logger, @output), "Expected logger_outputs_to? @output to return true but was false"
+ assert Logger.logger_outputs_to?(@logger, @output, STDOUT), "Expected logger_outputs_to? @output or STDOUT to return true but was false"
+
+ assert_not Logger.logger_outputs_to?(@logger, STDOUT), "Expected logger_outputs_to? to STDOUT to return false, but was true"
+ assert_not Logger.logger_outputs_to?(@logger, STDOUT, STDERR), "Expected logger_outputs_to? to STDOUT or STDERR to return false, but was true"
+ end
+
def test_write_binary_data_to_existing_file
- t = Tempfile.new ['development', 'log']
+ t = Tempfile.new ["development", "log"]
t.binmode
- t.write 'hi mom!'
+ t.write "hi mom!"
t.close
- f = File.open(t.path, 'w')
+ f = File.open(t.path, "w")
f.binmode
logger = Logger.new f
logger.level = Logger::DEBUG
- str = "\x80"
+ str = +"\x80"
str.force_encoding("ASCII-8BIT")
logger.add Logger::DEBUG, str
@@ -38,15 +50,15 @@ class LoggerTest < ActiveSupport::TestCase
end
def test_write_binary_data_create_file
- fname = File.join Dir.tmpdir, 'lol', 'rofl.log'
+ fname = File.join Dir.tmpdir, "lol", "rofl.log"
FileUtils.mkdir_p File.dirname(fname)
- f = File.open(fname, 'w')
+ f = File.open(fname, "w")
f.binmode
logger = Logger.new f
logger.level = Logger::DEBUG
- str = "\x80"
+ str = +"\x80"
str.force_encoding("ASCII-8BIT")
logger.add Logger::DEBUG, str
@@ -58,43 +70,43 @@ class LoggerTest < ActiveSupport::TestCase
def test_should_log_debugging_message_when_debugging
@logger.level = Logger::DEBUG
@logger.add(Logger::DEBUG, @message)
- assert @output.string.include?(@message)
+ assert_includes @output.string, @message
end
def test_should_not_log_debug_messages_when_log_level_is_info
@logger.level = Logger::INFO
@logger.add(Logger::DEBUG, @message)
- assert ! @output.string.include?(@message)
+ assert_not @output.string.include?(@message)
end
def test_should_add_message_passed_as_block_when_using_add
@logger.level = Logger::INFO
- @logger.add(Logger::INFO) {@message}
- assert @output.string.include?(@message)
+ @logger.add(Logger::INFO) { @message }
+ assert_includes @output.string, @message
end
def test_should_add_message_passed_as_block_when_using_shortcut
@logger.level = Logger::INFO
- @logger.info {@message}
- assert @output.string.include?(@message)
+ @logger.info { @message }
+ assert_includes @output.string, @message
end
def test_should_convert_message_to_string
@logger.level = Logger::INFO
@logger.info @integer_message
- assert @output.string.include?(@integer_message.to_s)
+ assert_includes @output.string, @integer_message.to_s
end
def test_should_convert_message_to_string_when_passed_in_block
@logger.level = Logger::INFO
- @logger.info {@integer_message}
- assert @output.string.include?(@integer_message.to_s)
+ @logger.info { @integer_message }
+ assert_includes @output.string, @integer_message.to_s
end
def test_should_not_evaluate_block_if_message_wont_be_logged
@logger.level = Logger::INFO
evaluated = false
- @logger.add(Logger::DEBUG) {evaluated = true}
+ @logger.add(Logger::DEBUG) { evaluated = true }
assert evaluated == false
end
@@ -106,28 +118,154 @@ class LoggerTest < ActiveSupport::TestCase
def test_should_know_if_its_loglevel_is_below_a_given_level
Logger::Severity.constants.each do |level|
- next if level.to_s == 'UNKNOWN'
+ next if level.to_s == "UNKNOWN"
@logger.level = Logger::Severity.const_get(level) - 1
assert @logger.send("#{level.downcase}?"), "didn't know if it was #{level.downcase}? or below"
end
end
def test_buffer_multibyte
+ @logger.level = Logger::INFO
@logger.info(UNICODE_STRING)
@logger.info(BYTE_STRING)
- assert @output.string.include?(UNICODE_STRING)
+ assert_includes @output.string, UNICODE_STRING
byte_string = @output.string.dup
byte_string.force_encoding("ASCII-8BIT")
- assert byte_string.include?(BYTE_STRING)
+ assert_includes byte_string, BYTE_STRING
end
-
+
def test_silencing_everything_but_errors
@logger.silence do
@logger.debug "NOT THERE"
@logger.error "THIS IS HERE"
end
-
- assert !@output.string.include?("NOT THERE")
- assert @output.string.include?("THIS IS HERE")
+
+ assert_not @output.string.include?("NOT THERE")
+ assert_includes @output.string, "THIS IS HERE"
+ end
+
+ def test_logger_silencing_works_for_broadcast
+ another_output = StringIO.new
+ another_logger = ActiveSupport::Logger.new(another_output)
+
+ @logger.extend ActiveSupport::Logger.broadcast(another_logger)
+
+ @logger.debug "CORRECT DEBUG"
+ @logger.silence do |logger|
+ assert_kind_of ActiveSupport::Logger, logger
+ @logger.debug "FAILURE"
+ @logger.error "CORRECT ERROR"
+ end
+
+ assert_includes @output.string, "CORRECT DEBUG"
+ assert_includes @output.string, "CORRECT ERROR"
+ assert_not @output.string.include?("FAILURE")
+
+ assert_includes another_output.string, "CORRECT DEBUG"
+ assert_includes another_output.string, "CORRECT ERROR"
+ assert_not another_output.string.include?("FAILURE")
+ end
+
+ def test_broadcast_silencing_does_not_break_plain_ruby_logger
+ another_output = StringIO.new
+ another_logger = ::Logger.new(another_output)
+
+ @logger.extend ActiveSupport::Logger.broadcast(another_logger)
+
+ @logger.debug "CORRECT DEBUG"
+ @logger.silence do |logger|
+ assert_kind_of ActiveSupport::Logger, logger
+ @logger.debug "FAILURE"
+ @logger.error "CORRECT ERROR"
+ end
+
+ assert_includes @output.string, "CORRECT DEBUG"
+ assert_includes @output.string, "CORRECT ERROR"
+ assert_not @output.string.include?("FAILURE")
+
+ assert_includes another_output.string, "CORRECT DEBUG"
+ assert_includes another_output.string, "CORRECT ERROR"
+ assert_includes another_output.string, "FAILURE"
+ # We can't silence plain ruby Logger cause with thread safety
+ # but at least we don't break it
end
+
+ def test_logger_level_per_object_thread_safety
+ logger1 = Logger.new(StringIO.new)
+ logger2 = Logger.new(StringIO.new)
+
+ level = Logger::DEBUG
+ assert_equal level, logger1.level, "Expected level #{level_name(level)}, got #{level_name(logger1.level)}"
+ assert_equal level, logger2.level, "Expected level #{level_name(level)}, got #{level_name(logger2.level)}"
+
+ logger1.level = Logger::ERROR
+ assert_equal level, logger2.level, "Expected level #{level_name(level)}, got #{level_name(logger2.level)}"
+ end
+
+ def test_logger_level_main_thread_safety
+ @logger.level = Logger::INFO
+ assert_level(Logger::INFO)
+
+ latch = Concurrent::CountDownLatch.new
+ latch2 = Concurrent::CountDownLatch.new
+
+ t = Thread.new do
+ latch.wait
+ assert_level(Logger::INFO)
+ latch2.count_down
+ end
+
+ @logger.silence(Logger::ERROR) do
+ assert_level(Logger::ERROR)
+ latch.count_down
+ latch2.wait
+ end
+
+ t.join
+ end
+
+ def test_logger_level_local_thread_safety
+ @logger.level = Logger::INFO
+ assert_level(Logger::INFO)
+
+ thread_1_latch = Concurrent::CountDownLatch.new
+ thread_2_latch = Concurrent::CountDownLatch.new
+
+ threads = (1..2).collect do |thread_number|
+ Thread.new do
+ # force thread 2 to wait until thread 1 is already in @logger.silence
+ thread_2_latch.wait if thread_number == 2
+
+ @logger.silence(Logger::ERROR) do
+ assert_level(Logger::ERROR)
+ @logger.silence(Logger::DEBUG) do
+ # allow thread 2 to finish but hold thread 1
+ if thread_number == 1
+ thread_2_latch.count_down
+ thread_1_latch.wait
+ end
+ assert_level(Logger::DEBUG)
+ end
+ end
+
+ # allow thread 1 to finish
+ assert_level(Logger::INFO)
+ thread_1_latch.count_down if thread_number == 2
+ end
+ end
+
+ threads.each(&:join)
+ assert_level(Logger::INFO)
+ end
+
+ private
+ def level_name(level)
+ ::Logger::Severity.constants.find do |severity|
+ Logger.const_get(severity) == level
+ end.to_s
+ end
+
+ def assert_level(level)
+ assert_equal level, @logger.level, "Expected level #{level_name(level)}, got #{level_name(@logger.level)}"
+ end
end
diff --git a/activesupport/test/message_encryptor_test.rb b/activesupport/test/message_encryptor_test.rb
index eb71369397..9edf07f762 100644
--- a/activesupport/test/message_encryptor_test.rb
+++ b/activesupport/test/message_encryptor_test.rb
@@ -1,7 +1,10 @@
-require 'abstract_unit'
-require 'openssl'
-require 'active_support/time'
-require 'active_support/json'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "openssl"
+require "active_support/time"
+require "active_support/json"
+require_relative "metadata/shared_metadata_tests"
class MessageEncryptorTest < ActiveSupport::TestCase
class JSONSerializer
@@ -15,10 +18,10 @@ class MessageEncryptorTest < ActiveSupport::TestCase
end
def setup
- @secret = SecureRandom.hex(64)
- @verifier = ActiveSupport::MessageVerifier.new(@secret, :serializer => ActiveSupport::MessageEncryptor::NullSerializer)
+ @secret = SecureRandom.random_bytes(32)
+ @verifier = ActiveSupport::MessageVerifier.new(@secret, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
@encryptor = ActiveSupport::MessageEncryptor.new(@secret)
- @data = { :some => "data", :now => Time.local(2010) }
+ @data = { some: "data", now: Time.local(2010) }
end
def test_encrypting_twice_yields_differing_cipher_text
@@ -48,11 +51,21 @@ class MessageEncryptorTest < ActiveSupport::TestCase
assert_equal @data, @encryptor.decrypt_and_verify(message)
end
+ def test_backwards_compat_for_64_bytes_key
+ # 64 bit key
+ secret = ["3942b1bf81e622559ed509e3ff274a780784fe9e75b065866bd270438c74da822219de3156473cc27df1fd590e4baf68c95eeb537b6e4d4c5a10f41635b5597e"].pack("H*")
+ # Encryptor with 32 bit key, 64 bit secret for verifier
+ encryptor = ActiveSupport::MessageEncryptor.new(secret[0..31], secret)
+ # Message generated with 64 bit key
+ message = "eHdGeExnZEwvMSt3U3dKaFl1WFo0TjVvYzA0eGpjbm5WSkt5MXlsNzhpZ0ZnbWhBWFlQZTRwaXE1bVJCS2oxMDZhYVp2dVN3V0lNZUlWQ3c2eVhQbnhnVjFmeVVubmhRKzF3WnZyWHVNMDg9LS1HSisyakJVSFlPb05ISzRMaXRzcFdBPT0=--831a1d54a3cda8a0658dc668a03dedcbce13b5ca"
+ assert_equal "data", encryptor.decrypt_and_verify(message)[:some]
+ end
+
def test_alternative_serialization_method
prev = ActiveSupport.use_standard_json_time_format
ActiveSupport.use_standard_json_time_format = true
- encryptor = ActiveSupport::MessageEncryptor.new(SecureRandom.hex(64), SecureRandom.hex(64), :serializer => JSONSerializer.new)
- message = encryptor.encrypt_and_sign({ :foo => 123, 'bar' => Time.utc(2010) })
+ encryptor = ActiveSupport::MessageEncryptor.new(SecureRandom.random_bytes(32), SecureRandom.random_bytes(128), serializer: JSONSerializer.new)
+ message = encryptor.encrypt_and_sign(:foo => 123, "bar" => Time.utc(2010))
exp = { "foo" => 123, "bar" => "2010-01-01T00:00:00.000Z" }
assert_equal exp, encryptor.decrypt_and_verify(message)
ensure
@@ -61,7 +74,7 @@ class MessageEncryptorTest < ActiveSupport::TestCase
def test_message_obeys_strict_encoding
bad_encoding_characters = "\n!@#"
- message, iv = @encryptor.encrypt_and_sign("This is a very \n\nhumble string"+bad_encoding_characters)
+ message, iv = @encryptor.encrypt_and_sign("This is a very \n\nhumble string" + bad_encoding_characters)
assert_not_decrypted("#{::Base64.encode64 message.to_s}--#{::Base64.encode64 iv.to_s}")
assert_not_verified("#{::Base64.encode64 message.to_s}--#{::Base64.encode64 iv.to_s}")
@@ -70,23 +83,164 @@ class MessageEncryptorTest < ActiveSupport::TestCase
assert_not_verified([iv, message] * bad_encoding_characters)
end
+ def test_aead_mode_encryption
+ encryptor = ActiveSupport::MessageEncryptor.new(@secret, cipher: "aes-256-gcm")
+ message = encryptor.encrypt_and_sign(@data)
+ assert_equal @data, encryptor.decrypt_and_verify(message)
+ end
+
+ def test_aead_mode_with_hmac_cbc_cipher_text
+ encryptor = ActiveSupport::MessageEncryptor.new(@secret, cipher: "aes-256-gcm")
+
+ assert_aead_not_decrypted(encryptor, "eHdGeExnZEwvMSt3U3dKaFl1WFo0TjVvYzA0eGpjbm5WSkt5MXlsNzhpZ0ZnbWhBWFlQZTRwaXE1bVJCS2oxMDZhYVp2dVN3V0lNZUlWQ3c2eVhQbnhnVjFmeVVubmhRKzF3WnZyWHVNMDg9LS1HSisyakJVSFlPb05ISzRMaXRzcFdBPT0=--831a1d54a3cda8a0658dc668a03dedcbce13b5ca")
+ end
+
+ def test_messing_with_aead_values_causes_failures
+ encryptor = ActiveSupport::MessageEncryptor.new(@secret, cipher: "aes-256-gcm")
+ text, iv, auth_tag = encryptor.encrypt_and_sign(@data).split("--")
+ assert_aead_not_decrypted(encryptor, [iv, text, auth_tag] * "--")
+ assert_aead_not_decrypted(encryptor, [munge(text), iv, auth_tag] * "--")
+ assert_aead_not_decrypted(encryptor, [text, munge(iv), auth_tag] * "--")
+ assert_aead_not_decrypted(encryptor, [text, iv, munge(auth_tag)] * "--")
+ assert_aead_not_decrypted(encryptor, [munge(text), munge(iv), munge(auth_tag)] * "--")
+ assert_aead_not_decrypted(encryptor, [text, iv] * "--")
+ assert_aead_not_decrypted(encryptor, [text, iv, auth_tag[0..-2]] * "--")
+ end
+
+ def test_backwards_compatibility_decrypt_previously_encrypted_messages_without_metadata
+ secret = "\xB7\xF0\xBCW\xB1\x18`\xAB\xF0\x81\x10\xA4$\xF44\xEC\xA1\xDC\xC1\xDDD\xAF\xA9\xB8\x14\xCD\x18\x9A\x99 \x80)"
+ encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm")
+ encrypted_message = "9cVnFs2O3lL9SPvIJuxBOLS51nDiBMw=--YNI5HAfHEmZ7VDpl--ddFJ6tXA0iH+XGcCgMINYQ=="
+
+ assert_equal "Ruby on Rails", encryptor.decrypt_and_verify(encrypted_message)
+ end
+
+ def test_rotating_secret
+ old_message = ActiveSupport::MessageEncryptor.new(secrets[:old], cipher: "aes-256-gcm").encrypt_and_sign("old")
+
+ encryptor = ActiveSupport::MessageEncryptor.new(@secret, cipher: "aes-256-gcm")
+ encryptor.rotate secrets[:old]
+
+ assert_equal "old", encryptor.decrypt_and_verify(old_message)
+ end
+
+ def test_rotating_serializer
+ old_message = ActiveSupport::MessageEncryptor.new(secrets[:old], cipher: "aes-256-gcm", serializer: JSON).
+ encrypt_and_sign(ahoy: :hoy)
+
+ encryptor = ActiveSupport::MessageEncryptor.new(@secret, cipher: "aes-256-gcm", serializer: JSON)
+ encryptor.rotate secrets[:old]
+
+ assert_equal({ "ahoy" => "hoy" }, encryptor.decrypt_and_verify(old_message))
+ end
+
+ def test_rotating_aes_cbc_secrets
+ old_encryptor = ActiveSupport::MessageEncryptor.new(secrets[:old], "old sign", cipher: "aes-256-cbc")
+ old_message = old_encryptor.encrypt_and_sign("old")
+
+ encryptor = ActiveSupport::MessageEncryptor.new(@secret)
+ encryptor.rotate secrets[:old], "old sign", cipher: "aes-256-cbc"
+
+ assert_equal "old", encryptor.decrypt_and_verify(old_message)
+ end
+
+ def test_multiple_rotations
+ older_message = ActiveSupport::MessageEncryptor.new(secrets[:older], "older sign").encrypt_and_sign("older")
+ old_message = ActiveSupport::MessageEncryptor.new(secrets[:old], "old sign").encrypt_and_sign("old")
+
+ encryptor = ActiveSupport::MessageEncryptor.new(@secret)
+ encryptor.rotate secrets[:old], "old sign"
+ encryptor.rotate secrets[:older], "older sign"
+
+ assert_equal "new", encryptor.decrypt_and_verify(encryptor.encrypt_and_sign("new"))
+ assert_equal "old", encryptor.decrypt_and_verify(old_message)
+ assert_equal "older", encryptor.decrypt_and_verify(older_message)
+ end
+
+ def test_on_rotation_is_called_and_returns_modified_messages
+ older_message = ActiveSupport::MessageEncryptor.new(secrets[:older], "older sign").encrypt_and_sign(encoded: "message")
+
+ encryptor = ActiveSupport::MessageEncryptor.new(@secret)
+ encryptor.rotate secrets[:old]
+ encryptor.rotate secrets[:older], "older sign"
+
+ rotated = false
+ message = encryptor.decrypt_and_verify(older_message, on_rotation: proc { rotated = true })
+
+ assert_equal({ encoded: "message" }, message)
+ assert rotated
+ end
+
+ def test_with_rotated_metadata
+ old_message = ActiveSupport::MessageEncryptor.new(secrets[:old], cipher: "aes-256-gcm").
+ encrypt_and_sign("metadata", purpose: :rotation)
+
+ encryptor = ActiveSupport::MessageEncryptor.new(@secret, cipher: "aes-256-gcm")
+ encryptor.rotate secrets[:old]
+
+ assert_equal "metadata", encryptor.decrypt_and_verify(old_message, purpose: :rotation)
+ end
+
private
+ def assert_aead_not_decrypted(encryptor, value)
+ assert_raise(ActiveSupport::MessageEncryptor::InvalidMessage) do
+ encryptor.decrypt_and_verify(value)
+ end
+ end
- def assert_not_decrypted(value)
- assert_raise(ActiveSupport::MessageEncryptor::InvalidMessage) do
- @encryptor.decrypt_and_verify(@verifier.generate(value))
+ def assert_not_decrypted(value)
+ assert_raise(ActiveSupport::MessageEncryptor::InvalidMessage) do
+ @encryptor.decrypt_and_verify(@verifier.generate(value))
+ end
end
- end
- def assert_not_verified(value)
- assert_raise(ActiveSupport::MessageVerifier::InvalidSignature) do
- @encryptor.decrypt_and_verify(value)
+ def assert_not_verified(value)
+ assert_raise(ActiveSupport::MessageVerifier::InvalidSignature) do
+ @encryptor.decrypt_and_verify(value)
+ end
end
- end
- def munge(base64_string)
- bits = ::Base64.strict_decode64(base64_string)
- bits.reverse!
- ::Base64.strict_encode64(bits)
+ def secrets
+ @secrets ||= Hash.new { |h, k| h[k] = SecureRandom.random_bytes(32) }
+ end
+
+ def munge(base64_string)
+ bits = ::Base64.strict_decode64(base64_string)
+ bits.reverse!
+ ::Base64.strict_encode64(bits)
+ end
+end
+
+class MessageEncryptorMetadataTest < ActiveSupport::TestCase
+ include SharedMessageMetadataTests
+
+ setup do
+ @secret = SecureRandom.random_bytes(32)
+ @encryptor = ActiveSupport::MessageEncryptor.new(@secret, encryptor_options)
end
+
+ private
+ def generate(message, **options)
+ @encryptor.encrypt_and_sign(message, options)
+ end
+
+ def parse(data, **options)
+ @encryptor.decrypt_and_verify(data, options)
+ end
+
+ def encryptor_options; end
+end
+
+class MessageEncryptorMetadataMarshalTest < MessageEncryptorMetadataTest
+ private
+ def encryptor_options
+ { serializer: Marshal }
+ end
+end
+
+class MessageEncryptorMetadataJSONTest < MessageEncryptorMetadataTest
+ private
+ def encryptor_options
+ { serializer: MessageEncryptorTest::JSONSerializer.new }
+ end
end
diff --git a/activesupport/test/message_verifier_test.rb b/activesupport/test/message_verifier_test.rb
index 668d78492e..0fa53695e0 100644
--- a/activesupport/test/message_verifier_test.rb
+++ b/activesupport/test/message_verifier_test.rb
@@ -1,10 +1,12 @@
-require 'abstract_unit'
-require 'openssl'
-require 'active_support/time'
-require 'active_support/json'
+# frozen_string_literal: true
-class MessageVerifierTest < ActiveSupport::TestCase
+require "abstract_unit"
+require "openssl"
+require "active_support/time"
+require "active_support/json"
+require_relative "metadata/shared_metadata_tests"
+class MessageVerifierTest < ActiveSupport::TestCase
class JSONSerializer
def dump(value)
ActiveSupport::JSON.encode(value)
@@ -17,17 +19,18 @@ class MessageVerifierTest < ActiveSupport::TestCase
def setup
@verifier = ActiveSupport::MessageVerifier.new("Hey, I'm a secret!")
- @data = { :some => "data", :now => Time.local(2010) }
+ @data = { some: "data", now: Time.utc(2010) }
+ @secret = SecureRandom.random_bytes(32)
end
def test_valid_message
data, hash = @verifier.generate(@data).split("--")
- assert !@verifier.valid_message?(nil)
- assert !@verifier.valid_message?("")
- assert !@verifier.valid_message?("\xff") # invalid encoding
- assert !@verifier.valid_message?("#{data.reverse}--#{hash}")
- assert !@verifier.valid_message?("#{data}--#{hash.reverse}")
- assert !@verifier.valid_message?("purejunk")
+ assert_not @verifier.valid_message?(nil)
+ assert_not @verifier.valid_message?("")
+ assert_not @verifier.valid_message?("\xff") # invalid encoding
+ assert_not @verifier.valid_message?("#{data.reverse}--#{hash}")
+ assert_not @verifier.valid_message?("#{data}--#{hash.reverse}")
+ assert_not @verifier.valid_message?("purejunk")
end
def test_simple_round_tripping
@@ -37,7 +40,7 @@ class MessageVerifierTest < ActiveSupport::TestCase
end
def test_verified_returns_false_on_invalid_message
- assert !@verifier.verified("purejunk")
+ assert_not @verifier.verified("purejunk")
end
def test_verify_exception_on_invalid_message
@@ -49,8 +52,8 @@ class MessageVerifierTest < ActiveSupport::TestCase
def test_alternative_serialization_method
prev = ActiveSupport.use_standard_json_time_format
ActiveSupport.use_standard_json_time_format = true
- verifier = ActiveSupport::MessageVerifier.new("Hey, I'm a secret!", :serializer => JSONSerializer.new)
- message = verifier.generate({ :foo => 123, 'bar' => Time.utc(2010) })
+ verifier = ActiveSupport::MessageVerifier.new("Hey, I'm a secret!", serializer: JSONSerializer.new)
+ message = verifier.generate(:foo => 123, "bar" => Time.utc(2010))
exp = { "foo" => 123, "bar" => "2010-01-01T00:00:00.000Z" }
assert_equal exp, verifier.verified(message)
assert_equal exp, verifier.verify(message)
@@ -81,6 +84,121 @@ 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
+
+ def test_backward_compatibility_messages_signed_without_metadata
+ signed_message = "BAh7BzoJc29tZUkiCWRhdGEGOgZFVDoIbm93SXU6CVRpbWUNIIAbgAAAAAAHOgtvZmZzZXRpADoJem9uZUkiCFVUQwY7BkY=--d03c52c91dfe4ccc5159417c660461bcce005e96"
+ assert_equal @data, @verifier.verify(signed_message)
+ end
+
+ def test_rotating_secret
+ old_message = ActiveSupport::MessageVerifier.new("old", digest: "SHA1").generate("old")
+
+ verifier = ActiveSupport::MessageVerifier.new(@secret, digest: "SHA1")
+ verifier.rotate "old"
+
+ assert_equal "old", verifier.verified(old_message)
end
+
+ def test_multiple_rotations
+ old_message = ActiveSupport::MessageVerifier.new("old", digest: "SHA256").generate("old")
+ older_message = ActiveSupport::MessageVerifier.new("older", digest: "SHA1").generate("older")
+
+ verifier = ActiveSupport::MessageVerifier.new(@secret, digest: "SHA512")
+ verifier.rotate "old", digest: "SHA256"
+ verifier.rotate "older", digest: "SHA1"
+
+ assert_equal "new", verifier.verified(verifier.generate("new"))
+ assert_equal "old", verifier.verified(old_message)
+ assert_equal "older", verifier.verified(older_message)
+ end
+
+ def test_on_rotation_is_called_and_verified_returns_message
+ older_message = ActiveSupport::MessageVerifier.new("older", digest: "SHA1").generate(encoded: "message")
+
+ verifier = ActiveSupport::MessageVerifier.new(@secret, digest: "SHA512")
+ verifier.rotate "old", digest: "SHA256"
+ verifier.rotate "older", digest: "SHA1"
+
+ rotated = false
+ message = verifier.verified(older_message, on_rotation: proc { rotated = true })
+
+ assert_equal({ encoded: "message" }, message)
+ assert rotated
+ end
+
+ def test_rotations_with_metadata
+ old_message = ActiveSupport::MessageVerifier.new("old").generate("old", purpose: :rotation)
+
+ verifier = ActiveSupport::MessageVerifier.new(@secret)
+ verifier.rotate "old"
+
+ assert_equal "old", verifier.verified(old_message, purpose: :rotation)
+ end
+end
+
+class MessageVerifierMetadataTest < ActiveSupport::TestCase
+ include SharedMessageMetadataTests
+
+ setup do
+ @verifier = ActiveSupport::MessageVerifier.new("Hey, I'm a secret!", verifier_options)
+ end
+
+ def test_verify_raises_when_purpose_differs
+ assert_raise(ActiveSupport::MessageVerifier::InvalidSignature) do
+ @verifier.verify(generate(data, purpose: "payment"), purpose: "shipping")
+ end
+ end
+
+ def test_verify_raises_when_expired
+ signed_message = generate(data, expires_in: 1.month)
+
+ travel 2.months
+ assert_raise(ActiveSupport::MessageVerifier::InvalidSignature) do
+ @verifier.verify(signed_message)
+ end
+ end
+
+ private
+ def generate(message, **options)
+ @verifier.generate(message, options)
+ end
+
+ def parse(message, **options)
+ @verifier.verified(message, options)
+ end
+
+ def verifier_options
+ Hash.new
+ end
+end
+
+class MessageVerifierMetadataMarshalTest < MessageVerifierMetadataTest
+ private
+ def verifier_options
+ { serializer: Marshal }
+ end
+end
+
+class MessageVerifierMetadataJSONTest < MessageVerifierMetadataTest
+ private
+ def verifier_options
+ { serializer: MessageVerifierTest::JSONSerializer.new }
+ end
+end
+
+class MessageEncryptorMetadataNullSerializerTest < MessageVerifierMetadataTest
+ private
+ def data
+ "string message"
+ end
+
+ def null_serializing?
+ true
+ end
+
+ def verifier_options
+ { serializer: ActiveSupport::MessageEncryptor::NullSerializer }
+ end
end
diff --git a/activesupport/test/messages/rotation_configuration_test.rb b/activesupport/test/messages/rotation_configuration_test.rb
new file mode 100644
index 0000000000..2f6824ed21
--- /dev/null
+++ b/activesupport/test/messages/rotation_configuration_test.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/messages/rotation_configuration"
+
+class MessagesRotationConfiguration < ActiveSupport::TestCase
+ def setup
+ @config = ActiveSupport::Messages::RotationConfiguration.new
+ end
+
+ def test_signed_configurations
+ @config.rotate :signed, "older secret", salt: "salt", digest: "SHA1"
+ @config.rotate :signed, "old secret", salt: "salt", digest: "SHA256"
+
+ assert_equal [
+ [ "older secret", salt: "salt", digest: "SHA1" ],
+ [ "old secret", salt: "salt", digest: "SHA256" ] ], @config.signed
+ end
+
+ def test_encrypted_configurations
+ @config.rotate :encrypted, "old raw key", cipher: "aes-256-gcm"
+
+ assert_equal [ [ "old raw key", cipher: "aes-256-gcm" ] ], @config.encrypted
+ end
+end
diff --git a/activesupport/test/metadata/shared_metadata_tests.rb b/activesupport/test/metadata/shared_metadata_tests.rb
new file mode 100644
index 0000000000..cf571223e5
--- /dev/null
+++ b/activesupport/test/metadata/shared_metadata_tests.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+module SharedMessageMetadataTests
+ def null_serializing?
+ false
+ end
+
+ def test_encryption_and_decryption_with_same_purpose
+ assert_equal data, parse(generate(data, purpose: "checkout"), purpose: "checkout")
+ assert_equal data, parse(generate(data))
+
+ string_message = "address: #23, main street"
+ assert_equal string_message, parse(generate(string_message, purpose: "shipping"), purpose: "shipping")
+ end
+
+ def test_verifies_array_when_purpose_matches
+ unless null_serializing?
+ data = [ "credit_card_no: 5012-6748-9087-5678", { "card_holder" => "Donald", "issued_on" => Time.local(2017) }, 12345 ]
+ assert_equal data, parse(generate(data, purpose: :registration), purpose: :registration)
+ end
+ end
+
+ def test_encryption_and_decryption_with_different_purposes_returns_nil
+ assert_nil parse(generate(data, purpose: "payment"), purpose: "sign up")
+ assert_nil parse(generate(data, purpose: "payment"))
+ assert_nil parse(generate(data), purpose: "sign up")
+ end
+
+ def test_purpose_using_symbols
+ assert_equal data, parse(generate(data, purpose: :checkout), purpose: :checkout)
+ assert_equal data, parse(generate(data, purpose: :checkout), purpose: "checkout")
+ assert_equal data, parse(generate(data, purpose: "checkout"), purpose: :checkout)
+ end
+
+ def test_passing_expires_at_sets_expiration_date
+ encrypted_message = generate(data, expires_at: 1.hour.from_now)
+
+ travel 59.minutes
+ assert_equal data, parse(encrypted_message)
+
+ travel 2.minutes
+ assert_nil parse(encrypted_message)
+ end
+
+ def test_set_relative_expiration_date_by_passing_expires_in
+ encrypted_message = generate(data, expires_in: 2.hours)
+
+ travel 1.hour
+ assert_equal data, parse(encrypted_message)
+
+ travel 1.hour + 1.second
+ assert_nil parse(encrypted_message)
+ end
+
+ def test_passing_expires_in_less_than_a_second_is_not_expired
+ freeze_time do
+ encrypted_message = generate(data, expires_in: 1.second)
+
+ travel 0.5.seconds
+ assert_equal data, parse(encrypted_message)
+
+ travel 1.second
+ assert_nil parse(encrypted_message)
+ end
+ end
+
+ def test_favor_expires_at_over_expires_in
+ payment_related_message = generate(data, purpose: "payment", expires_at: 2.year.from_now, expires_in: 1.second)
+
+ travel 1.year
+ assert_equal data, parse(payment_related_message, purpose: :payment)
+
+ travel 1.year + 1.day
+ assert_nil parse(payment_related_message, purpose: "payment")
+ end
+
+ def test_skip_expires_at_and_expires_in_to_disable_expiration_check
+ payment_related_message = generate(data, purpose: "payment")
+
+ travel 100.years
+ assert_equal data, parse(payment_related_message, purpose: "payment")
+ end
+
+ private
+ def data
+ { "credit_card_no" => "5012-6784-9087-5678", "card_holder" => { "name" => "Donald" } }
+ end
+end
diff --git a/activesupport/test/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb
index e1c4b705f8..5f4e3f3fd3 100644
--- a/activesupport/test/multibyte_chars_test.rb
+++ b/activesupport/test/multibyte_chars_test.rb
@@ -1,6 +1,8 @@
-require 'abstract_unit'
-require 'multibyte_test_helpers'
-require 'active_support/core_ext/string/multibyte'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "multibyte_test_helpers"
+require "active_support/core_ext/string/multibyte"
class MultibyteCharsTest < ActiveSupport::TestCase
include MultibyteTestHelpers
@@ -16,7 +18,7 @@ class MultibyteCharsTest < ActiveSupport::TestCase
end
def test_should_allow_method_calls_to_string
- @chars.wrapped_string.singleton_class.class_eval { def __method_for_multibyte_testing; 'result'; end }
+ @chars.wrapped_string.singleton_class.class_eval { def __method_for_multibyte_testing; "result"; end }
assert_nothing_raised do
@chars.__method_for_multibyte_testing
@@ -27,14 +29,14 @@ class MultibyteCharsTest < ActiveSupport::TestCase
end
def test_forwarded_method_calls_should_return_new_chars_instance
- @chars.wrapped_string.singleton_class.class_eval { def __method_for_multibyte_testing; 'result'; end }
+ @chars.wrapped_string.singleton_class.class_eval { def __method_for_multibyte_testing; "result"; end }
assert_kind_of @proxy_class, @chars.__method_for_multibyte_testing
assert_not_equal @chars.object_id, @chars.__method_for_multibyte_testing.object_id
end
def test_forwarded_bang_method_calls_should_return_the_original_chars_instance_when_result_is_not_nil
- @chars.wrapped_string.singleton_class.class_eval { def __method_for_multibyte_testing!; 'result'; end }
+ @chars.wrapped_string.singleton_class.class_eval { def __method_for_multibyte_testing!; "result"; end }
assert_kind_of @proxy_class, @chars.__method_for_multibyte_testing!
assert_equal @chars.object_id, @chars.__method_for_multibyte_testing!.object_id
@@ -50,8 +52,8 @@ 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
- str = ''
+ 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 }
@@ -59,26 +61,32 @@ class MultibyteCharsTest < ActiveSupport::TestCase
end
def test_should_concatenate
- mb_a = 'a'.mb_chars
- mb_b = 'b'.mb_chars
- assert_equal 'ab', mb_a + 'b'
- assert_equal 'ab', 'a' + mb_b
- assert_equal 'ab', mb_a + mb_b
+ mb_a = (+"a").mb_chars
+ mb_b = (+"b").mb_chars
+ assert_equal "ab", mb_a + "b"
+ assert_equal "ab", "a" + mb_b
+ assert_equal "ab", mb_a + mb_b
- assert_equal 'ab', mb_a << 'b'
- assert_equal 'ab', 'a' << mb_b
- assert_equal 'abb', mb_a << mb_b
+ assert_equal "ab", mb_a << "b"
+ assert_equal "ab", (+"a") << mb_b
+ assert_equal "abb", mb_a << mb_b
end
def test_consumes_utf8_strings
- assert @proxy_class.consumes?(UNICODE_STRING)
- assert @proxy_class.consumes?(ASCII_STRING)
- assert !@proxy_class.consumes?(BYTE_STRING)
+ ActiveSupport::Deprecation.silence do
+ assert @proxy_class.consumes?(UNICODE_STRING)
+ assert @proxy_class.consumes?(ASCII_STRING)
+ assert_not @proxy_class.consumes?(BYTE_STRING)
+ end
+ end
+
+ def test_consumes_is_deprecated
+ assert_deprecated { @proxy_class.consumes?(UNICODE_STRING) }
end
def test_concatenation_should_return_a_proxy_class_instance
- assert_equal ActiveSupport::Multibyte.proxy_class, ('a'.mb_chars + 'b').class
- assert_equal ActiveSupport::Multibyte.proxy_class, ('a'.mb_chars << 'b').class
+ assert_equal ActiveSupport::Multibyte.proxy_class, ("a".mb_chars + "b").class
+ assert_equal ActiveSupport::Multibyte.proxy_class, ((+"a").mb_chars << "b").class
end
def test_ascii_strings_are_treated_at_utf8_strings
@@ -86,10 +94,10 @@ class MultibyteCharsTest < ActiveSupport::TestCase
end
def test_concatenate_should_return_proxy_instance
- assert(('a'.mb_chars + 'b').kind_of?(@proxy_class))
- assert(('a'.mb_chars + 'b'.mb_chars).kind_of?(@proxy_class))
- assert(('a'.mb_chars << 'b').kind_of?(@proxy_class))
- assert(('a'.mb_chars << 'b'.mb_chars).kind_of?(@proxy_class))
+ assert(("a".mb_chars + "b").kind_of?(@proxy_class))
+ assert(("a".mb_chars + "b".mb_chars).kind_of?(@proxy_class))
+ assert(((+"a").mb_chars << "b").kind_of?(@proxy_class))
+ assert(((+"a").mb_chars << "b".mb_chars).kind_of?(@proxy_class))
end
def test_should_return_string_as_json
@@ -115,12 +123,12 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase
%w{capitalize downcase lstrip reverse rstrip swapcase upcase}.each do |method|
class_eval(<<-EOTESTS, __FILE__, __LINE__ + 1)
def test_#{method}_bang_should_return_self_when_modifying_wrapped_string
- chars = ' él piDió Un bUen café '
+ chars = ' él piDió Un bUen café '.dup
assert_equal chars.object_id, chars.send("#{method}!").object_id
end
def test_#{method}_bang_should_change_wrapped_string
- original = ' él piDió Un bUen café '
+ original = ' él piDió Un bUen café '.dup
proxy = chars(original.dup)
proxy.send("#{method}!")
assert_not_equal original, proxy.to_s
@@ -133,7 +141,7 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase
end
def test_tidy_bytes_bang_should_change_wrapped_string
- original = " Un bUen café \x92"
+ original = +" Un bUen café \x92"
proxy = chars(original.dup)
proxy.tidy_bytes!
assert_not_equal original, proxy.to_s
@@ -146,28 +154,30 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase
def test_identity
assert_equal @chars, @chars
assert @chars.eql?(@chars)
- assert !@chars.eql?(UNICODE_STRING)
+ assert_not @chars.eql?(UNICODE_STRING)
end
def test_string_methods_are_chainable
- assert chars('').insert(0, '').kind_of?(ActiveSupport::Multibyte.proxy_class)
- assert chars('').rjust(1).kind_of?(ActiveSupport::Multibyte.proxy_class)
- assert chars('').ljust(1).kind_of?(ActiveSupport::Multibyte.proxy_class)
- assert chars('').center(1).kind_of?(ActiveSupport::Multibyte.proxy_class)
- assert chars('').rstrip.kind_of?(ActiveSupport::Multibyte.proxy_class)
- assert chars('').lstrip.kind_of?(ActiveSupport::Multibyte.proxy_class)
- assert chars('').strip.kind_of?(ActiveSupport::Multibyte.proxy_class)
- assert chars('').reverse.kind_of?(ActiveSupport::Multibyte.proxy_class)
- assert chars(' ').slice(0).kind_of?(ActiveSupport::Multibyte.proxy_class)
- assert chars('').limit(0).kind_of?(ActiveSupport::Multibyte.proxy_class)
- assert chars('').upcase.kind_of?(ActiveSupport::Multibyte.proxy_class)
- assert chars('').downcase.kind_of?(ActiveSupport::Multibyte.proxy_class)
- assert chars('').capitalize.kind_of?(ActiveSupport::Multibyte.proxy_class)
- assert chars('').normalize.kind_of?(ActiveSupport::Multibyte.proxy_class)
- assert chars('').decompose.kind_of?(ActiveSupport::Multibyte.proxy_class)
- assert chars('').compose.kind_of?(ActiveSupport::Multibyte.proxy_class)
- assert chars('').tidy_bytes.kind_of?(ActiveSupport::Multibyte.proxy_class)
- assert chars('').swapcase.kind_of?(ActiveSupport::Multibyte.proxy_class)
+ assert chars(+"").insert(0, "").kind_of?(ActiveSupport::Multibyte.proxy_class)
+ assert chars("").rjust(1).kind_of?(ActiveSupport::Multibyte.proxy_class)
+ assert chars("").ljust(1).kind_of?(ActiveSupport::Multibyte.proxy_class)
+ assert chars("").center(1).kind_of?(ActiveSupport::Multibyte.proxy_class)
+ assert chars("").rstrip.kind_of?(ActiveSupport::Multibyte.proxy_class)
+ assert chars("").lstrip.kind_of?(ActiveSupport::Multibyte.proxy_class)
+ assert chars("").strip.kind_of?(ActiveSupport::Multibyte.proxy_class)
+ assert chars("").reverse.kind_of?(ActiveSupport::Multibyte.proxy_class)
+ assert chars(" ").slice(0).kind_of?(ActiveSupport::Multibyte.proxy_class)
+ assert chars("").limit(0).kind_of?(ActiveSupport::Multibyte.proxy_class)
+ assert chars("").upcase.kind_of?(ActiveSupport::Multibyte.proxy_class)
+ assert chars("").downcase.kind_of?(ActiveSupport::Multibyte.proxy_class)
+ assert chars("").capitalize.kind_of?(ActiveSupport::Multibyte.proxy_class)
+ ActiveSupport::Deprecation.silence do
+ assert chars("").normalize.kind_of?(ActiveSupport::Multibyte.proxy_class)
+ end
+ assert chars("").decompose.kind_of?(ActiveSupport::Multibyte.proxy_class)
+ assert chars("").compose.kind_of?(ActiveSupport::Multibyte.proxy_class)
+ assert chars("").tidy_bytes.kind_of?(ActiveSupport::Multibyte.proxy_class)
+ assert chars("").swapcase.kind_of?(ActiveSupport::Multibyte.proxy_class)
end
def test_should_be_equal_to_the_wrapped_string
@@ -176,8 +186,8 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase
end
def test_should_not_be_equal_to_an_other_string
- assert_not_equal @chars, 'other'
- assert_not_equal 'other', @chars
+ assert_not_equal @chars, "other"
+ assert_not_equal "other", @chars
end
def test_sortability
@@ -195,29 +205,29 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase
end
def test_should_use_character_offsets_for_insert_offsets
- assert_equal '', ''.mb_chars.insert(0, '')
- assert_equal 'こわにちわ', @chars.insert(1, 'わ')
- assert_equal 'こわわわにちわ', @chars.insert(2, 'わわ')
- assert_equal 'わこわわわにちわ', @chars.insert(0, 'わ')
- assert_equal 'わこわわわにちわ', @chars.wrapped_string
+ assert_equal "", (+"").mb_chars.insert(0, "")
+ assert_equal "こわにちわ", @chars.insert(1, "わ")
+ assert_equal "こわわわにちわ", @chars.insert(2, "わわ")
+ assert_equal "わこわわわにちわ", @chars.insert(0, "わ")
+ assert_equal "わこわわわにちわ", @chars.wrapped_string
end
def test_insert_should_be_destructive
- @chars.insert(1, 'わ')
- assert_equal 'こわにちわ', @chars
+ @chars.insert(1, "わ")
+ assert_equal "こわにちわ", @chars
end
def test_insert_throws_index_error
- assert_raise(IndexError) { @chars.insert(-12, 'わ')}
- assert_raise(IndexError) { @chars.insert(12, 'わ') }
+ assert_raise(IndexError) { @chars.insert(-12, "わ") }
+ assert_raise(IndexError) { @chars.insert(12, "わ") }
end
def test_should_know_if_one_includes_the_other
- assert @chars.include?('')
- assert @chars.include?('ち')
- assert @chars.include?('わ')
- assert !@chars.include?('こちわ')
- assert !@chars.include?('a')
+ assert_includes @chars, ""
+ assert_includes @chars, "ち"
+ assert_includes @chars, "わ"
+ assert_not_includes @chars, "こちわ"
+ assert_not_includes @chars, "a"
end
def test_include_raises_when_nil_is_passed
@@ -227,62 +237,62 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase
end
def test_index_should_return_character_offset
- assert_nil @chars.index('u')
- assert_equal 0, @chars.index('こに')
- assert_equal 2, @chars.index('ち')
- assert_equal 2, @chars.index('ち', -2)
- assert_equal nil, @chars.index('ち', -1)
- assert_equal 3, @chars.index('わ')
- assert_equal 5, 'ééxééx'.mb_chars.index('x', 4)
+ assert_nil @chars.index("u")
+ assert_equal 0, @chars.index("こに")
+ assert_equal 2, @chars.index("ち")
+ assert_equal 2, @chars.index("ち", -2)
+ assert_nil @chars.index("ち", -1)
+ assert_equal 3, @chars.index("わ")
+ assert_equal 5, "ééxééx".mb_chars.index("x", 4)
end
def test_rindex_should_return_character_offset
- assert_nil @chars.rindex('u')
- assert_equal 1, @chars.rindex('に')
- assert_equal 2, @chars.rindex('ち', -2)
- assert_nil @chars.rindex('ち', -3)
- assert_equal 6, 'Café périferôl'.mb_chars.rindex('é')
- assert_equal 13, 'Café périferôl'.mb_chars.rindex(/\w/u)
+ assert_nil @chars.rindex("u")
+ assert_equal 1, @chars.rindex("に")
+ assert_equal 2, @chars.rindex("ち", -2)
+ assert_nil @chars.rindex("ち", -3)
+ assert_equal 6, "Café périferôl".mb_chars.rindex("é")
+ assert_equal 13, "Café périferôl".mb_chars.rindex(/\w/u)
end
def test_indexed_insert_should_take_character_offsets
- @chars[2] = 'a'
- assert_equal 'こにaわ', @chars
- @chars[2] = 'ηη'
- assert_equal 'こにηηわ', @chars
- @chars[3, 2] = 'λλλ'
- assert_equal 'こにηλλλ', @chars
+ @chars[2] = "a"
+ assert_equal "こにaわ", @chars
+ @chars[2] = "ηη"
+ assert_equal "こにηηわ", @chars
+ @chars[3, 2] = "λλλ"
+ assert_equal "こにηλλλ", @chars
@chars[1, 0] = "λ"
- assert_equal 'こλにηλλλ', @chars
+ assert_equal "こλにηλλλ", @chars
@chars[4..6] = "ηη"
- assert_equal 'こλにηηη', @chars
+ assert_equal "こλにηηη", @chars
@chars[/ηη/] = "λλλ"
- assert_equal 'こλにλλλη', @chars
+ assert_equal "こλにλλλη", @chars
@chars[/(λλ)(.)/, 2] = "α"
- assert_equal 'こλにλλαη', @chars
+ assert_equal "こλにλλαη", @chars
@chars["α"] = "¢"
- assert_equal 'こλにλλ¢η', @chars
+ assert_equal "こλにλλ¢η", @chars
@chars["λλ"] = "ααα"
- assert_equal 'こλにααα¢η', @chars
+ assert_equal "こλにααα¢η", @chars
end
def test_indexed_insert_should_raise_on_index_overflow
before = @chars.to_s
- assert_raise(IndexError) { @chars[10] = 'a' }
- assert_raise(IndexError) { @chars[10, 4] = 'a' }
- assert_raise(IndexError) { @chars[/ii/] = 'a' }
- assert_raise(IndexError) { @chars[/()/, 10] = 'a' }
+ assert_raise(IndexError) { @chars[10] = "a" }
+ assert_raise(IndexError) { @chars[10, 4] = "a" }
+ assert_raise(IndexError) { @chars[/ii/] = "a" }
+ assert_raise(IndexError) { @chars[/()/, 10] = "a" }
assert_equal before, @chars
end
def test_indexed_insert_should_raise_on_range_overflow
before = @chars.to_s
- assert_raise(RangeError) { @chars[10..12] = 'a' }
+ assert_raise(RangeError) { @chars[10..12] = "a" }
assert_equal before, @chars
end
def test_rjust_should_raise_argument_errors_on_bad_arguments
- assert_raise(ArgumentError) { @chars.rjust(10, '') }
+ assert_raise(ArgumentError) { @chars.rjust(10, "") }
assert_raise(ArgumentError) { @chars.rjust }
end
@@ -292,15 +302,15 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase
assert_equal UNICODE_STRING, @chars.rjust(4)
assert_equal " #{UNICODE_STRING}", @chars.rjust(5)
assert_equal " #{UNICODE_STRING}", @chars.rjust(7)
- assert_equal "---#{UNICODE_STRING}", @chars.rjust(7, '-')
- assert_equal "ααα#{UNICODE_STRING}", @chars.rjust(7, 'α')
- assert_equal "aba#{UNICODE_STRING}", @chars.rjust(7, 'ab')
- assert_equal "αηα#{UNICODE_STRING}", @chars.rjust(7, 'αη')
- assert_equal "αηαη#{UNICODE_STRING}", @chars.rjust(8, 'αη')
+ assert_equal "---#{UNICODE_STRING}", @chars.rjust(7, "-")
+ assert_equal "ααα#{UNICODE_STRING}", @chars.rjust(7, "α")
+ assert_equal "aba#{UNICODE_STRING}", @chars.rjust(7, "ab")
+ assert_equal "αηα#{UNICODE_STRING}", @chars.rjust(7, "αη")
+ assert_equal "αηαη#{UNICODE_STRING}", @chars.rjust(8, "αη")
end
def test_ljust_should_raise_argument_errors_on_bad_arguments
- assert_raise(ArgumentError) { @chars.ljust(10, '') }
+ assert_raise(ArgumentError) { @chars.ljust(10, "") }
assert_raise(ArgumentError) { @chars.ljust }
end
@@ -310,15 +320,15 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase
assert_equal UNICODE_STRING, @chars.ljust(4)
assert_equal "#{UNICODE_STRING} ", @chars.ljust(5)
assert_equal "#{UNICODE_STRING} ", @chars.ljust(7)
- assert_equal "#{UNICODE_STRING}---", @chars.ljust(7, '-')
- assert_equal "#{UNICODE_STRING}ααα", @chars.ljust(7, 'α')
- assert_equal "#{UNICODE_STRING}aba", @chars.ljust(7, 'ab')
- assert_equal "#{UNICODE_STRING}αηα", @chars.ljust(7, 'αη')
- assert_equal "#{UNICODE_STRING}αηαη", @chars.ljust(8, 'αη')
+ assert_equal "#{UNICODE_STRING}---", @chars.ljust(7, "-")
+ assert_equal "#{UNICODE_STRING}ααα", @chars.ljust(7, "α")
+ assert_equal "#{UNICODE_STRING}aba", @chars.ljust(7, "ab")
+ assert_equal "#{UNICODE_STRING}αηα", @chars.ljust(7, "αη")
+ assert_equal "#{UNICODE_STRING}αηαη", @chars.ljust(8, "αη")
end
def test_center_should_raise_argument_errors_on_bad_arguments
- assert_raise(ArgumentError) { @chars.center(10, '') }
+ assert_raise(ArgumentError) { @chars.center(10, "") }
assert_raise(ArgumentError) { @chars.center }
end
@@ -329,15 +339,15 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase
assert_equal "#{UNICODE_STRING} ", @chars.center(5)
assert_equal " #{UNICODE_STRING} ", @chars.center(6)
assert_equal " #{UNICODE_STRING} ", @chars.center(7)
- assert_equal "--#{UNICODE_STRING}--", @chars.center(8, '-')
- assert_equal "--#{UNICODE_STRING}---", @chars.center(9, '-')
- assert_equal "αα#{UNICODE_STRING}αα", @chars.center(8, 'α')
- assert_equal "αα#{UNICODE_STRING}ααα", @chars.center(9, 'α')
- assert_equal "a#{UNICODE_STRING}ab", @chars.center(7, 'ab')
- assert_equal "ab#{UNICODE_STRING}ab", @chars.center(8, 'ab')
- assert_equal "abab#{UNICODE_STRING}abab", @chars.center(12, 'ab')
- assert_equal "α#{UNICODE_STRING}αη", @chars.center(7, 'αη')
- assert_equal "αη#{UNICODE_STRING}αη", @chars.center(8, 'αη')
+ assert_equal "--#{UNICODE_STRING}--", @chars.center(8, "-")
+ assert_equal "--#{UNICODE_STRING}---", @chars.center(9, "-")
+ assert_equal "αα#{UNICODE_STRING}αα", @chars.center(8, "α")
+ assert_equal "αα#{UNICODE_STRING}ααα", @chars.center(9, "α")
+ assert_equal "a#{UNICODE_STRING}ab", @chars.center(7, "ab")
+ assert_equal "ab#{UNICODE_STRING}ab", @chars.center(8, "ab")
+ assert_equal "abab#{UNICODE_STRING}abab", @chars.center(12, "ab")
+ assert_equal "α#{UNICODE_STRING}αη", @chars.center(7, "αη")
+ assert_equal "αη#{UNICODE_STRING}αη", @chars.center(8, "αη")
end
def test_lstrip_strips_whitespace_from_the_left_of_the_string
@@ -367,56 +377,70 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase
end
def test_size_returns_characters_instead_of_bytes
- assert_equal 0, ''.mb_chars.size
+ assert_equal 0, "".mb_chars.size
assert_equal 4, @chars.size
assert_equal 4, @chars.length
assert_equal 5, ASCII_STRING.mb_chars.size
end
def test_reverse_reverses_characters
- assert_equal '', ''.mb_chars.reverse
- assert_equal 'わちにこ', @chars.reverse
+ assert_equal "", "".mb_chars.reverse
+ assert_equal "わちにこ", @chars.reverse
end
def test_reverse_should_work_with_normalized_strings
- str = 'bös'
- reversed_str = 'söb'
- assert_equal chars(reversed_str).normalize(:kc), chars(str).normalize(:kc).reverse
- assert_equal chars(reversed_str).normalize(:c), chars(str).normalize(:c).reverse
- assert_equal chars(reversed_str).normalize(:d), chars(str).normalize(:d).reverse
- assert_equal chars(reversed_str).normalize(:kd), chars(str).normalize(:kd).reverse
+ str = "bös"
+ reversed_str = "söb"
+ ActiveSupport::Deprecation.silence do
+ assert_equal chars(reversed_str).normalize(:kc), chars(str).normalize(:kc).reverse
+ assert_equal chars(reversed_str).normalize(:c), chars(str).normalize(:c).reverse
+ assert_equal chars(reversed_str).normalize(:d), chars(str).normalize(:d).reverse
+ assert_equal chars(reversed_str).normalize(:kd), chars(str).normalize(:kd).reverse
+ end
assert_equal chars(reversed_str).decompose, chars(str).decompose.reverse
assert_equal chars(reversed_str).compose, chars(str).compose.reverse
end
def test_slice_should_take_character_offsets
- assert_equal 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_equal '', ''.mb_chars.slice(0..10)
- assert_equal 'にちわ', @chars.slice(1..3)
- assert_equal 'にちわ', @chars.slice(1, 3)
- assert_equal 'こ', @chars.slice(0, 1)
- assert_equal 'ちわ', @chars.slice(2..10)
- assert_equal '', @chars.slice(4..10)
- assert_equal 'に', @chars.slice(/に/u)
- assert_equal 'にち', @chars.slice(/に./u)
- assert_equal 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 "".mb_chars.slice(0)
+ assert_equal "こ", @chars.slice(0)
+ assert_equal "わ", @chars.slice(3)
+ 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)
+ assert_equal "こ", @chars.slice(0, 1)
+ assert_equal "ちわ", @chars.slice(2..10)
+ assert_equal "", @chars.slice(4..10)
+ assert_equal "に", @chars.slice(/に/u)
+ assert_equal "にち", @chars.slice(/に./u)
+ assert_nil @chars.slice(/unknown/u)
+ assert_equal "にち", @chars.slice(/(にち)/u, 1)
+ assert_nil @chars.slice(/(にち)/u, 2)
+ assert_nil @chars.slice(7..6)
end
def test_slice_bang_returns_sliced_out_substring
- assert_equal 'にち', @chars.slice!(1..2)
+ assert_equal "にち", @chars.slice!(1..2)
+ end
+
+ def test_slice_bang_returns_nil_on_out_of_bound_arguments
+ assert_nil @chars.mb_chars.slice!(9..10)
end
def test_slice_bang_removes_the_slice_from_the_receiver
- chars = 'úüù'.mb_chars
- chars.slice!(0,2)
- assert_equal 'ù', chars
+ chars = (+"úüù").mb_chars
+ chars.slice!(0, 2)
+ assert_equal "ù", chars
+ end
+
+ def test_slice_bang_returns_nil_and_does_not_modify_receiver_if_out_of_bounds
+ string = +"úüù"
+ chars = string.mb_chars
+ assert_nil chars.slice!(4, 5)
+ assert_equal "úüù", chars
+ assert_equal "úüù", string
end
def test_slice_should_throw_exceptions_on_invalid_arguments
@@ -430,48 +454,48 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase
end
def test_upcase_should_upcase_ascii_characters
- assert_equal '', ''.mb_chars.upcase
- assert_equal 'ABC', 'aBc'.mb_chars.upcase
+ assert_equal "", "".mb_chars.upcase
+ assert_equal "ABC", "aBc".mb_chars.upcase
end
def test_downcase_should_downcase_ascii_characters
- assert_equal '', ''.mb_chars.downcase
- assert_equal 'abc', 'aBc'.mb_chars.downcase
+ assert_equal "", "".mb_chars.downcase
+ assert_equal "abc", "aBc".mb_chars.downcase
end
def test_swapcase_should_swap_ascii_characters
- assert_equal '', ''.mb_chars.swapcase
- assert_equal 'AbC', 'aBc'.mb_chars.swapcase
+ assert_equal "", "".mb_chars.swapcase
+ assert_equal "AbC", "aBc".mb_chars.swapcase
end
def test_capitalize_should_work_on_ascii_characters
- assert_equal '', ''.mb_chars.capitalize
- assert_equal 'Abc', 'abc'.mb_chars.capitalize
+ assert_equal "", "".mb_chars.capitalize
+ assert_equal "Abc", "abc".mb_chars.capitalize
end
def test_titleize_should_work_on_ascii_characters
- assert_equal '', ''.mb_chars.titleize
- assert_equal 'Abc Abc', 'abc abc'.mb_chars.titleize
+ assert_equal "", "".mb_chars.titleize
+ assert_equal "Abc Abc", "abc abc".mb_chars.titleize
end
def test_respond_to_knows_which_methods_the_proxy_responds_to
- assert ''.mb_chars.respond_to?(:slice) # Defined on Chars
- assert ''.mb_chars.respond_to?(:capitalize!) # Defined on Chars
- assert ''.mb_chars.respond_to?(:gsub) # Defined on String
- assert !''.mb_chars.respond_to?(:undefined_method) # Not defined
+ assert_respond_to "".mb_chars, :slice # Defined on Chars
+ assert_respond_to "".mb_chars, :capitalize! # Defined on Chars
+ assert_respond_to "".mb_chars, :gsub # Defined on String
+ assert_not_respond_to "".mb_chars, :undefined_method # Not defined
end
def test_method_works_for_proxyed_methods
- assert_equal 'll', 'hello'.mb_chars.method(:slice).call(2..3) # Defined on Chars
- chars = 'hello'.mb_chars
- assert_equal 'Hello', chars.method(:capitalize!).call # Defined on Chars
- assert_equal 'Hello', chars
- assert_equal 'jello', 'hello'.mb_chars.method(:gsub).call(/h/, 'j') # Defined on String
- assert_raise(NameError){ ''.mb_chars.method(:undefined_method) } # Not defined
+ assert_equal "ll", "hello".mb_chars.method(:slice).call(2..3) # Defined on Chars
+ chars = +"hello".mb_chars
+ assert_equal "Hello", chars.method(:capitalize!).call # Defined on Chars
+ assert_equal "Hello", chars
+ assert_equal "jello", "hello".mb_chars.method(:gsub).call(/h/, "j") # Defined on String
+ assert_raise(NameError) { "".mb_chars.method(:undefined_method) } # Not defined
end
def test_acts_like_string
- assert 'Bambi'.mb_chars.acts_like_string?
+ assert_predicate "Bambi".mb_chars, :acts_like_string?
end
end
@@ -483,25 +507,25 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase
def test_upcase_should_be_unicode_aware
assert_equal "АБВГД\0F", chars("аБвгд\0f").upcase
- assert_equal 'こにちわ', chars('こにちわ').upcase
+ assert_equal "こにちわ", chars("こにちわ").upcase
end
def test_downcase_should_be_unicode_aware
assert_equal "абвгд\0f", chars("аБвгд\0F").downcase
- assert_equal 'こにちわ', chars('こにちわ').downcase
+ assert_equal "こにちわ", chars("こにちわ").downcase
end
def test_swapcase_should_be_unicode_aware
assert_equal "аaéÜ\0f", chars("АAÉü\0F").swapcase
- assert_equal 'こにちわ', chars('こにちわ').swapcase
+ assert_equal "こにちわ", chars("こにちわ").swapcase
end
def test_capitalize_should_be_unicode_aware
- { 'аБвг аБвг' => 'Абвг абвг',
- 'аБвг АБВГ' => 'Абвг абвг',
- 'АБВГ АБВГ' => 'Абвг абвг',
- '' => '' }.each do |f,t|
- assert_equal t, chars(f).capitalize
+ { "аБвг аБвг" => "Абвг абвг",
+ "аБвг АБВГ" => "Абвг абвг",
+ "АБВГ АБВГ" => "Абвг абвг",
+ "" => "" }.each do |f, t|
+ assert_equal t, chars(f).capitalize
end
end
@@ -515,7 +539,7 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase
end
def test_limit_should_not_break_on_blank_strings
- example = chars('')
+ example = chars("")
assert_equal example, example.limit(0)
assert_equal example, example.limit(1)
end
@@ -525,23 +549,23 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase
bytesize = UNICODE_STRING.bytesize
assert_equal UNICODE_STRING, example.limit(bytesize)
- assert_equal '', example.limit(0)
- assert_equal '', example.limit(1)
- assert_equal 'こ', example.limit(3)
- assert_equal 'こに', example.limit(6)
- assert_equal 'こに', example.limit(8)
- assert_equal 'こにち', example.limit(9)
- assert_equal 'こにちわ', example.limit(50)
+ assert_equal "", example.limit(0)
+ assert_equal "", example.limit(1)
+ assert_equal "こ", example.limit(3)
+ assert_equal "こに", example.limit(6)
+ assert_equal "こに", example.limit(8)
+ assert_equal "こにち", example.limit(9)
+ assert_equal "こにちわ", example.limit(50)
end
def test_limit_should_work_on_an_ascii_string
ascii = chars(ASCII_STRING)
assert_equal ASCII_STRING, ascii.limit(ASCII_STRING.length)
- assert_equal '', ascii.limit(0)
- assert_equal 'o', ascii.limit(1)
- assert_equal 'oh', ascii.limit(2)
- assert_equal 'ohay', ascii.limit(4)
- assert_equal 'ohayo', ascii.limit(50)
+ assert_equal "", ascii.limit(0)
+ assert_equal "o", ascii.limit(1)
+ assert_equal "oh", ascii.limit(2)
+ assert_equal "ohay", ascii.limit(4)
+ assert_equal "ohayo", ascii.limit(50)
end
def test_limit_should_keep_under_the_specified_byte_limit
@@ -553,8 +577,10 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase
def test_composition_exclusion_is_set_up_properly
# Normalization of DEVANAGARI LETTER QA breaks when composition exclusion isn't used correctly
- qa = [0x915, 0x93c].pack('U*')
- assert_equal qa, chars(qa).normalize(:c)
+ qa = [0x915, 0x93c].pack("U*")
+ ActiveSupport::Deprecation.silence do
+ assert_equal qa, chars(qa).normalize(:c)
+ end
end
# Test for the Public Review Issue #29, bad explanation of composition might lead to a
@@ -563,18 +589,22 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase
[
[0x0B47, 0x0300, 0x0B3E],
[0x1100, 0x0300, 0x1161]
- ].map { |c| c.pack('U*') }.each do |c|
- assert_equal_codepoints c, chars(c).normalize(:c)
+ ].map { |c| c.pack("U*") }.each do |c|
+ ActiveSupport::Deprecation.silence do
+ assert_equal_codepoints c, chars(c).normalize(:c)
+ end
end
end
def test_normalization_shouldnt_strip_null_bytes
null_byte_str = "Test\0test"
- assert_equal null_byte_str, chars(null_byte_str).normalize(:kc)
- assert_equal null_byte_str, chars(null_byte_str).normalize(:c)
- assert_equal null_byte_str, chars(null_byte_str).normalize(:d)
- assert_equal null_byte_str, chars(null_byte_str).normalize(:kd)
+ ActiveSupport::Deprecation.silence do
+ assert_equal null_byte_str, chars(null_byte_str).normalize(:kc)
+ assert_equal null_byte_str, chars(null_byte_str).normalize(:c)
+ assert_equal null_byte_str, chars(null_byte_str).normalize(:d)
+ assert_equal null_byte_str, chars(null_byte_str).normalize(:kd)
+ end
assert_equal null_byte_str, chars(null_byte_str).decompose
assert_equal null_byte_str, chars(null_byte_str).compose
end
@@ -587,46 +617,73 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase
323 # COMBINING DOT BELOW
].pack("U*")
- assert_equal_codepoints '', chars('').normalize
- assert_equal_codepoints [44,105,106,328,323].pack("U*"), chars(comp_str).normalize(:kc).to_s
- assert_equal_codepoints [44,307,328,323].pack("U*"), chars(comp_str).normalize(:c).to_s
- assert_equal_codepoints [44,307,110,780,78,769].pack("U*"), chars(comp_str).normalize(:d).to_s
- assert_equal_codepoints [44,105,106,110,780,78,769].pack("U*"), chars(comp_str).normalize(:kd).to_s
+ ActiveSupport::Deprecation.silence do
+ assert_equal_codepoints "", chars("").normalize
+ assert_equal_codepoints [44, 105, 106, 328, 323].pack("U*"), chars(comp_str).normalize(:kc).to_s
+ assert_equal_codepoints [44, 307, 328, 323].pack("U*"), chars(comp_str).normalize(:c).to_s
+ assert_equal_codepoints [44, 307, 110, 780, 78, 769].pack("U*"), chars(comp_str).normalize(:d).to_s
+ assert_equal_codepoints [44, 105, 106, 110, 780, 78, 769].pack("U*"), chars(comp_str).normalize(:kd).to_s
+ end
end
def test_should_compute_grapheme_length
[
- ['', 0],
- ['abc', 3],
- ['こにちわ', 4],
- [[0x0924, 0x094D, 0x0930].pack('U*'), 2],
+ ["", 0],
+ ["abc", 3],
+ ["こにちわ", 4],
+ [[0x0924, 0x094D, 0x0930].pack("U*"), 2],
+ # GB3
[%w(cr lf), 1],
+ # GB4
+ [%w(cr n), 2],
+ [%w(lf n), 2],
+ [%w(control n), 2],
+ [%w(cr extend), 2],
+ [%w(lf extend), 2],
+ [%w(control extend), 2],
+ # GB 5
+ [%w(n cr), 2],
+ [%w(n lf), 2],
+ [%w(n control), 2],
+ [%w(extend cr), 2],
+ [%w(extend lf), 2],
+ [%w(extend control), 2],
+ # GB 6
[%w(l l), 1],
[%w(l v), 1],
[%w(l lv), 1],
[%w(l lvt), 1],
+ # GB7
[%w(lv v), 1],
[%w(lv t), 1],
[%w(v v), 1],
[%w(v t), 1],
+ # GB8
[%w(lvt t), 1],
[%w(t t), 1],
+ # GB8a
+ [%w(r r), 1],
+ # GB9
[%w(n extend), 1],
+ # GB9a
+ [%w(n spacingmark), 1],
+ # GB10
[%w(n n), 2],
+ # Other
[%w(n cr lf n), 3],
- [%w(n l v t), 2]
+ [%w(n l v t), 2],
+ [%w(cr extend n), 3],
].each do |input, expected_length|
if input.kind_of?(Array)
str = string_from_classes(input)
else
str = input
end
- assert_equal expected_length, chars(str).grapheme_length
+ assert_equal expected_length, chars(str).grapheme_length, input.inspect
end
end
def test_tidy_bytes_should_tidy_bytes
-
single_byte_cases = {
"\x21" => "!", # Valid ASCII byte, low
"\x41" => "A", # Valid ASCII byte, mid
@@ -661,9 +718,9 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase
end
byte_string = "\270\236\010\210\245"
- tidy_string = [0xb8, 0x17e, 0x8, 0x2c6, 0xa5].pack('U*')
+ tidy_string = [0xb8, 0x17e, 0x8, 0x2c6, 0xa5].pack("U*")
assert_equal_codepoints tidy_string, chars(byte_string).tidy_bytes
- assert_nothing_raised { chars(byte_string).tidy_bytes.to_s.unpack('U*') }
+ assert_nothing_raised { chars(byte_string).tidy_bytes.to_s.unpack("U*") }
# UTF-8 leading byte followed by too few continuation bytes
assert_equal_codepoints "\xc3\xb0\xc2\xa5\xc2\xa4\x21", chars("\xf0\xa5\xa4\x21").tidy_bytes
@@ -680,34 +737,61 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase
assert_equal BYTE_STRING.dup.mb_chars.class, ActiveSupport::Multibyte::Chars
end
- private
+ def test_unicode_normalize_deprecation
+ # String#unicode_normalize default form is `:nfc`, and
+ # different than Multibyte::Unicode default, `:nkfc`.
+ # Deprecation should suggest the right form if no params
+ # are given and default is used.
+ assert_deprecated(/unicode_normalize\(:nfkc\)/) do
+ ActiveSupport::Multibyte::Unicode.normalize("")
+ end
- def string_from_classes(classes)
- # Characters from the character classes as described in UAX #29
- character_from_class = {
- :l => 0x1100, :v => 0x1160, :t => 0x11A8, :lv => 0xAC00, :lvt => 0xAC01, :cr => 0x000D, :lf => 0x000A,
- :extend => 0x094D, :n => 0x64
- }
- classes.collect do |k|
- character_from_class[k.intern]
- end.pack('U*')
+ assert_deprecated(/unicode_normalize\(:nfd\)/) do
+ ActiveSupport::Multibyte::Unicode.normalize("", :d)
+ end
end
-end
-class MultibyteInternalsTest < ActiveSupport::TestCase
- include MultibyteTestHelpers
+ def test_chars_normalize_deprecation
+ # String#unicode_normalize default form is `:nfc`, and
+ # different than Multibyte::Unicode default, `:nkfc`.
+ # Deprecation should suggest the right form if no params
+ # are given and default is used.
+ assert_deprecated(/unicode_normalize\(:nfkc\)/) do
+ "".mb_chars.normalize
+ end
- test "Chars translates a character offset to a byte offset" do
- example = chars("Puisque c'était son erreur, il m'a aidé")
- [
- [0, 0],
- [3, 3],
- [12, 11],
- [14, 13],
- [41, 39]
- ].each do |byte_offset, character_offset|
- assert_equal character_offset, example.send(:translate_offset, byte_offset),
- "Expected byte offset #{byte_offset} to translate to #{character_offset}"
+ assert_deprecated(/unicode_normalize\(:nfc\)/) { "".mb_chars.normalize(:c) }
+ assert_deprecated(/unicode_normalize\(:nfd\)/) { "".mb_chars.normalize(:d) }
+ assert_deprecated(/unicode_normalize\(:nfkc\)/) { "".mb_chars.normalize(:kc) }
+ assert_deprecated(/unicode_normalize\(:nfkd\)/) { "".mb_chars.normalize(:kd) }
+ end
+
+ def test_unicode_deprecations
+ assert_deprecated { ActiveSupport::Multibyte::Unicode.downcase("") }
+ assert_deprecated { ActiveSupport::Multibyte::Unicode.upcase("") }
+ assert_deprecated { ActiveSupport::Multibyte::Unicode.swapcase("") }
+ end
+
+ def test_normalize_non_unicode_string
+ # Fullwidth Latin Capital Letter A in Windows 31J
+ str = "\u{ff21}".encode(Encoding::Windows_31J)
+ assert_raise Encoding::CompatibilityError do
+ ActiveSupport::Deprecation.silence do
+ ActiveSupport::Multibyte::Unicode.normalize(str)
+ end
end
end
+
+ private
+
+ def string_from_classes(classes)
+ # Characters from the character classes as described in UAX #29
+ character_from_class = {
+ l: 0x1100, v: 0x1160, t: 0x11A8, lv: 0xAC00, lvt: 0xAC01, cr: 0x000D, lf: 0x000A,
+ extend: 0x094D, n: 0x64, spacingmark: 0x0903, r: 0x1F1E6, control: 0x0001
+ }
+ classes.collect do |k|
+ character_from_class[k.intern]
+ end.pack("U*")
+ end
end
diff --git a/activesupport/test/multibyte_conformance_test.rb b/activesupport/test/multibyte_conformance_test.rb
index d8704716e7..b19689723f 100644
--- a/activesupport/test/multibyte_conformance_test.rb
+++ b/activesupport/test/multibyte_conformance_test.rb
@@ -1,36 +1,12 @@
+# frozen_string_literal: true
-require 'abstract_unit'
-require 'multibyte_test_helpers'
-
-require 'fileutils'
-require 'open-uri'
-require 'tmpdir'
-
-class Downloader
- def self.download(from, to)
- unless File.exist?(to)
- unless File.exist?(File.dirname(to))
- system "mkdir -p #{File.dirname(to)}"
- end
- open(from) do |source|
- File.open(to, 'w') do |target|
- source.each_line do |l|
- target.write l
- end
- end
- end
- end
- true
- end
-end
+require "abstract_unit"
+require "multibyte_test_helpers"
class MultibyteConformanceTest < ActiveSupport::TestCase
include MultibyteTestHelpers
- UNIDATA_URL = "http://www.unicode.org/Public/#{ActiveSupport::Multibyte::Unicode::UNICODE_VERSION}/ucd"
- UNIDATA_FILE = '/NormalizationTest.txt'
- CACHE_DIR = File.join(Dir.tmpdir, 'cache')
- FileUtils.mkdir_p(CACHE_DIR)
+ UNIDATA_FILE = "/NormalizationTest.txt"
RUN_P = begin
Downloader.download(UNIDATA_URL + UNIDATA_FILE, CACHE_DIR + UNIDATA_FILE)
rescue
@@ -42,83 +18,88 @@ class MultibyteConformanceTest < ActiveSupport::TestCase
end
def test_normalizations_C
- each_line_of_norm_tests do |*cols|
- col1, col2, col3, col4, col5, comment = *cols
-
- # CONFORMANCE:
- # 1. The following invariants must be true for all conformant implementations
- #
- # NFC
- # c2 == NFC(c1) == NFC(c2) == NFC(c3)
- assert_equal_codepoints col2, @proxy.new(col1).normalize(:c), "Form C - Col 2 has to be NFC(1) - #{comment}"
- assert_equal_codepoints col2, @proxy.new(col2).normalize(:c), "Form C - Col 2 has to be NFC(2) - #{comment}"
- assert_equal_codepoints col2, @proxy.new(col3).normalize(:c), "Form C - Col 2 has to be NFC(3) - #{comment}"
- #
- # c4 == NFC(c4) == NFC(c5)
- assert_equal_codepoints col4, @proxy.new(col4).normalize(:c), "Form C - Col 4 has to be C(4) - #{comment}"
- assert_equal_codepoints col4, @proxy.new(col5).normalize(:c), "Form C - Col 4 has to be C(5) - #{comment}"
+ ActiveSupport::Deprecation.silence do
+ each_line_of_norm_tests do |*cols|
+ col1, col2, col3, col4, col5, comment = *cols
+
+ # CONFORMANCE:
+ # 1. The following invariants must be true for all conformant implementations
+ #
+ # NFC
+ # c2 == NFC(c1) == NFC(c2) == NFC(c3)
+ assert_equal_codepoints col2, @proxy.new(col1).normalize(:c), "Form C - Col 2 has to be NFC(1) - #{comment}"
+ assert_equal_codepoints col2, @proxy.new(col2).normalize(:c), "Form C - Col 2 has to be NFC(2) - #{comment}"
+ assert_equal_codepoints col2, @proxy.new(col3).normalize(:c), "Form C - Col 2 has to be NFC(3) - #{comment}"
+ #
+ # c4 == NFC(c4) == NFC(c5)
+ assert_equal_codepoints col4, @proxy.new(col4).normalize(:c), "Form C - Col 4 has to be C(4) - #{comment}"
+ assert_equal_codepoints col4, @proxy.new(col5).normalize(:c), "Form C - Col 4 has to be C(5) - #{comment}"
+ end
end
end
def test_normalizations_D
- each_line_of_norm_tests do |*cols|
- col1, col2, col3, col4, col5, comment = *cols
- #
- # NFD
- # c3 == NFD(c1) == NFD(c2) == NFD(c3)
- assert_equal_codepoints col3, @proxy.new(col1).normalize(:d), "Form D - Col 3 has to be NFD(1) - #{comment}"
- assert_equal_codepoints col3, @proxy.new(col2).normalize(:d), "Form D - Col 3 has to be NFD(2) - #{comment}"
- assert_equal_codepoints col3, @proxy.new(col3).normalize(:d), "Form D - Col 3 has to be NFD(3) - #{comment}"
- # c5 == NFD(c4) == NFD(c5)
- assert_equal_codepoints col5, @proxy.new(col4).normalize(:d), "Form D - Col 5 has to be NFD(4) - #{comment}"
- assert_equal_codepoints col5, @proxy.new(col5).normalize(:d), "Form D - Col 5 has to be NFD(5) - #{comment}"
+ ActiveSupport::Deprecation.silence do
+ each_line_of_norm_tests do |*cols|
+ col1, col2, col3, col4, col5, comment = *cols
+ #
+ # NFD
+ # c3 == NFD(c1) == NFD(c2) == NFD(c3)
+ assert_equal_codepoints col3, @proxy.new(col1).normalize(:d), "Form D - Col 3 has to be NFD(1) - #{comment}"
+ assert_equal_codepoints col3, @proxy.new(col2).normalize(:d), "Form D - Col 3 has to be NFD(2) - #{comment}"
+ assert_equal_codepoints col3, @proxy.new(col3).normalize(:d), "Form D - Col 3 has to be NFD(3) - #{comment}"
+ # c5 == NFD(c4) == NFD(c5)
+ assert_equal_codepoints col5, @proxy.new(col4).normalize(:d), "Form D - Col 5 has to be NFD(4) - #{comment}"
+ assert_equal_codepoints col5, @proxy.new(col5).normalize(:d), "Form D - Col 5 has to be NFD(5) - #{comment}"
+ end
end
end
def test_normalizations_KC
- each_line_of_norm_tests do | *cols |
- col1, col2, col3, col4, col5, comment = *cols
- #
- # NFKC
- # c4 == NFKC(c1) == NFKC(c2) == NFKC(c3) == NFKC(c4) == NFKC(c5)
- assert_equal_codepoints col4, @proxy.new(col1).normalize(:kc), "Form D - Col 4 has to be NFKC(1) - #{comment}"
- assert_equal_codepoints col4, @proxy.new(col2).normalize(:kc), "Form D - Col 4 has to be NFKC(2) - #{comment}"
- assert_equal_codepoints col4, @proxy.new(col3).normalize(:kc), "Form D - Col 4 has to be NFKC(3) - #{comment}"
- assert_equal_codepoints col4, @proxy.new(col4).normalize(:kc), "Form D - Col 4 has to be NFKC(4) - #{comment}"
- assert_equal_codepoints col4, @proxy.new(col5).normalize(:kc), "Form D - Col 4 has to be NFKC(5) - #{comment}"
+ ActiveSupport::Deprecation.silence do
+ each_line_of_norm_tests do | *cols |
+ col1, col2, col3, col4, col5, comment = *cols
+ #
+ # NFKC
+ # c4 == NFKC(c1) == NFKC(c2) == NFKC(c3) == NFKC(c4) == NFKC(c5)
+ assert_equal_codepoints col4, @proxy.new(col1).normalize(:kc), "Form D - Col 4 has to be NFKC(1) - #{comment}"
+ assert_equal_codepoints col4, @proxy.new(col2).normalize(:kc), "Form D - Col 4 has to be NFKC(2) - #{comment}"
+ assert_equal_codepoints col4, @proxy.new(col3).normalize(:kc), "Form D - Col 4 has to be NFKC(3) - #{comment}"
+ assert_equal_codepoints col4, @proxy.new(col4).normalize(:kc), "Form D - Col 4 has to be NFKC(4) - #{comment}"
+ assert_equal_codepoints col4, @proxy.new(col5).normalize(:kc), "Form D - Col 4 has to be NFKC(5) - #{comment}"
+ end
end
end
def test_normalizations_KD
- each_line_of_norm_tests do | *cols |
- col1, col2, col3, col4, col5, comment = *cols
- #
- # NFKD
- # c5 == NFKD(c1) == NFKD(c2) == NFKD(c3) == NFKD(c4) == NFKD(c5)
- assert_equal_codepoints col5, @proxy.new(col1).normalize(:kd), "Form KD - Col 5 has to be NFKD(1) - #{comment}"
- assert_equal_codepoints col5, @proxy.new(col2).normalize(:kd), "Form KD - Col 5 has to be NFKD(2) - #{comment}"
- assert_equal_codepoints col5, @proxy.new(col3).normalize(:kd), "Form KD - Col 5 has to be NFKD(3) - #{comment}"
- assert_equal_codepoints col5, @proxy.new(col4).normalize(:kd), "Form KD - Col 5 has to be NFKD(4) - #{comment}"
- assert_equal_codepoints col5, @proxy.new(col5).normalize(:kd), "Form KD - Col 5 has to be NFKD(5) - #{comment}"
+ ActiveSupport::Deprecation.silence do
+ each_line_of_norm_tests do | *cols |
+ col1, col2, col3, col4, col5, comment = *cols
+ #
+ # NFKD
+ # c5 == NFKD(c1) == NFKD(c2) == NFKD(c3) == NFKD(c4) == NFKD(c5)
+ assert_equal_codepoints col5, @proxy.new(col1).normalize(:kd), "Form KD - Col 5 has to be NFKD(1) - #{comment}"
+ assert_equal_codepoints col5, @proxy.new(col2).normalize(:kd), "Form KD - Col 5 has to be NFKD(2) - #{comment}"
+ assert_equal_codepoints col5, @proxy.new(col3).normalize(:kd), "Form KD - Col 5 has to be NFKD(3) - #{comment}"
+ assert_equal_codepoints col5, @proxy.new(col4).normalize(:kd), "Form KD - Col 5 has to be NFKD(4) - #{comment}"
+ assert_equal_codepoints col5, @proxy.new(col5).normalize(:kd), "Form KD - Col 5 has to be NFKD(5) - #{comment}"
+ end
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
- File.open(File.join(CACHE_DIR, UNIDATA_FILE), 'r') do | f |
- until f.eof? || (max_test_lines > 38 and lines > max_test_lines)
- lines += 1
+ File.open(File.join(CACHE_DIR, UNIDATA_FILE), "r") do | f |
+ until f.eof?
line = f.gets.chomp!
- next if (line.empty? || line =~ /^\#/)
+ next if line.empty? || line.start_with?("#")
cols, comment = line.split("#")
cols = cols.split(";").map(&:strip).reject(&:empty?)
next unless cols.length == 5
# codepoints are in hex in the test suite, pack wants them as integers
- cols.map!{|c| c.split.map{|codepoint| codepoint.to_i(16)}.pack("U*") }
+ cols.map! { |c| c.split.map { |codepoint| codepoint.to_i(16) }.pack("U*") }
cols << comment
yield(*cols)
@@ -127,6 +108,6 @@ class MultibyteConformanceTest < ActiveSupport::TestCase
end
def inspect_codepoints(str)
- str.to_s.unpack("U*").map{|cp| cp.to_s(16) }.join(' ')
+ str.to_s.unpack("U*").map { |cp| cp.to_s(16) }.join(" ")
end
end
diff --git a/activesupport/test/multibyte_grapheme_break_conformance_test.rb b/activesupport/test/multibyte_grapheme_break_conformance_test.rb
new file mode 100644
index 0000000000..97963279af
--- /dev/null
+++ b/activesupport/test/multibyte_grapheme_break_conformance_test.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "multibyte_test_helpers"
+
+class MultibyteGraphemeBreakConformanceTest < ActiveSupport::TestCase
+ include MultibyteTestHelpers
+
+ UNIDATA_FILE = "/auxiliary/GraphemeBreakTest.txt"
+ RUN_P = begin
+ Downloader.download(UNIDATA_URL + UNIDATA_FILE, CACHE_DIR + UNIDATA_FILE)
+ rescue
+ end
+
+ def setup
+ skip "Unable to download test data" unless RUN_P
+ end
+
+ def test_breaks
+ ActiveSupport::Deprecation.silence do
+ each_line_of_break_tests do |*cols|
+ *clusters, comment = *cols
+ packed = ActiveSupport::Multibyte::Unicode.pack_graphemes(clusters)
+ assert_equal clusters, ActiveSupport::Multibyte::Unicode.unpack_graphemes(packed), comment
+ end
+ end
+ end
+
+ 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
+ File.open(File.join(CACHE_DIR, UNIDATA_FILE), "r") do | f |
+ until f.eof? || (max_test_lines > 21 && lines > max_test_lines)
+ lines += 1
+ line = f.gets.chomp!
+ next if line.empty? || line.start_with?("#")
+
+ cols, comment = line.split("#")
+ # Cluster breaks are represented by ÷
+ clusters = cols.split("÷").map { |e| e.strip }.reject { |e| e.empty? }
+ clusters = clusters.map do |cluster|
+ # Codepoints within each cluster are separated by ×
+ codepoints = cluster.split("×").map { |e| e.strip }.reject { |e| e.empty? }
+ # codepoints are in hex in the test suite, pack wants them as integers
+ codepoints.map { |codepoint| codepoint.to_i(16) }
+ end
+
+ # The tests contain a solitary U+D800 <Non Private Use High
+ # Surrogate, First> character, which Ruby does not allow to stand
+ # alone in a UTF-8 string. So we'll just skip it.
+ next if clusters.flatten.include?(0xd800)
+
+ clusters << comment.strip
+
+ yield(*clusters)
+ end
+ end
+ end
+end
diff --git a/activesupport/test/multibyte_normalization_conformance_test.rb b/activesupport/test/multibyte_normalization_conformance_test.rb
new file mode 100644
index 0000000000..82edf69294
--- /dev/null
+++ b/activesupport/test/multibyte_normalization_conformance_test.rb
@@ -0,0 +1,116 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "multibyte_test_helpers"
+
+class MultibyteNormalizationConformanceTest < ActiveSupport::TestCase
+ include MultibyteTestHelpers
+
+ UNIDATA_FILE = "/NormalizationTest.txt"
+ RUN_P = begin
+ Downloader.download(UNIDATA_URL + UNIDATA_FILE, CACHE_DIR + UNIDATA_FILE)
+ rescue
+ end
+
+ def setup
+ @proxy = ActiveSupport::Multibyte::Chars
+ skip "Unable to download test data" unless RUN_P
+ end
+
+ def test_normalizations_C
+ ActiveSupport::Deprecation.silence do
+ each_line_of_norm_tests do |*cols|
+ col1, col2, col3, col4, col5, comment = *cols
+
+ # CONFORMANCE:
+ # 1. The following invariants must be true for all conformant implementations
+ #
+ # NFC
+ # c2 == NFC(c1) == NFC(c2) == NFC(c3)
+ assert_equal_codepoints col2, @proxy.new(col1).normalize(:c), "Form C - Col 2 has to be NFC(1) - #{comment}"
+ assert_equal_codepoints col2, @proxy.new(col2).normalize(:c), "Form C - Col 2 has to be NFC(2) - #{comment}"
+ assert_equal_codepoints col2, @proxy.new(col3).normalize(:c), "Form C - Col 2 has to be NFC(3) - #{comment}"
+ #
+ # c4 == NFC(c4) == NFC(c5)
+ assert_equal_codepoints col4, @proxy.new(col4).normalize(:c), "Form C - Col 4 has to be C(4) - #{comment}"
+ assert_equal_codepoints col4, @proxy.new(col5).normalize(:c), "Form C - Col 4 has to be C(5) - #{comment}"
+ end
+ end
+ end
+
+ def test_normalizations_D
+ ActiveSupport::Deprecation.silence do
+ each_line_of_norm_tests do |*cols|
+ col1, col2, col3, col4, col5, comment = *cols
+ #
+ # NFD
+ # c3 == NFD(c1) == NFD(c2) == NFD(c3)
+ assert_equal_codepoints col3, @proxy.new(col1).normalize(:d), "Form D - Col 3 has to be NFD(1) - #{comment}"
+ assert_equal_codepoints col3, @proxy.new(col2).normalize(:d), "Form D - Col 3 has to be NFD(2) - #{comment}"
+ assert_equal_codepoints col3, @proxy.new(col3).normalize(:d), "Form D - Col 3 has to be NFD(3) - #{comment}"
+ # c5 == NFD(c4) == NFD(c5)
+ assert_equal_codepoints col5, @proxy.new(col4).normalize(:d), "Form D - Col 5 has to be NFD(4) - #{comment}"
+ assert_equal_codepoints col5, @proxy.new(col5).normalize(:d), "Form D - Col 5 has to be NFD(5) - #{comment}"
+ end
+ end
+ end
+
+ def test_normalizations_KC
+ ActiveSupport::Deprecation.silence do
+ each_line_of_norm_tests do | *cols |
+ col1, col2, col3, col4, col5, comment = *cols
+ #
+ # NFKC
+ # c4 == NFKC(c1) == NFKC(c2) == NFKC(c3) == NFKC(c4) == NFKC(c5)
+ assert_equal_codepoints col4, @proxy.new(col1).normalize(:kc), "Form D - Col 4 has to be NFKC(1) - #{comment}"
+ assert_equal_codepoints col4, @proxy.new(col2).normalize(:kc), "Form D - Col 4 has to be NFKC(2) - #{comment}"
+ assert_equal_codepoints col4, @proxy.new(col3).normalize(:kc), "Form D - Col 4 has to be NFKC(3) - #{comment}"
+ assert_equal_codepoints col4, @proxy.new(col4).normalize(:kc), "Form D - Col 4 has to be NFKC(4) - #{comment}"
+ assert_equal_codepoints col4, @proxy.new(col5).normalize(:kc), "Form D - Col 4 has to be NFKC(5) - #{comment}"
+ end
+ end
+ end
+
+ def test_normalizations_KD
+ ActiveSupport::Deprecation.silence do
+ each_line_of_norm_tests do | *cols |
+ col1, col2, col3, col4, col5, comment = *cols
+ #
+ # NFKD
+ # c5 == NFKD(c1) == NFKD(c2) == NFKD(c3) == NFKD(c4) == NFKD(c5)
+ assert_equal_codepoints col5, @proxy.new(col1).normalize(:kd), "Form KD - Col 5 has to be NFKD(1) - #{comment}"
+ assert_equal_codepoints col5, @proxy.new(col2).normalize(:kd), "Form KD - Col 5 has to be NFKD(2) - #{comment}"
+ assert_equal_codepoints col5, @proxy.new(col3).normalize(:kd), "Form KD - Col 5 has to be NFKD(3) - #{comment}"
+ assert_equal_codepoints col5, @proxy.new(col4).normalize(:kd), "Form KD - Col 5 has to be NFKD(4) - #{comment}"
+ assert_equal_codepoints col5, @proxy.new(col5).normalize(:kd), "Form KD - Col 5 has to be NFKD(5) - #{comment}"
+ end
+ end
+ end
+
+ 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
+ File.open(File.join(CACHE_DIR, UNIDATA_FILE), "r") do | f |
+ until f.eof? || (max_test_lines > 38 && lines > max_test_lines)
+ lines += 1
+ line = f.gets.chomp!
+ next if line.empty? || line.start_with?("#")
+
+ cols, comment = line.split("#")
+ cols = cols.split(";").map { |e| e.strip }.reject { |e| e.empty? }
+ next unless cols.length == 5
+
+ # codepoints are in hex in the test suite, pack wants them as integers
+ cols.map! { |c| c.split.map { |codepoint| codepoint.to_i(16) }.pack("U*") }
+ cols << comment
+
+ yield(*cols)
+ end
+ end
+ end
+
+ def inspect_codepoints(str)
+ str.to_s.unpack("U*").map { |cp| cp.to_s(16) }.join(" ")
+ end
+end
diff --git a/activesupport/test/multibyte_proxy_test.rb b/activesupport/test/multibyte_proxy_test.rb
index 11f5374017..ecedab2569 100644
--- a/activesupport/test/multibyte_proxy_test.rb
+++ b/activesupport/test/multibyte_proxy_test.rb
@@ -1,5 +1,6 @@
+# frozen_string_literal: true
-require 'abstract_unit'
+require "abstract_unit"
class MultibyteProxyText < ActiveSupport::TestCase
class AsciiOnlyEncoder
@@ -7,7 +8,7 @@ class MultibyteProxyText < ActiveSupport::TestCase
alias to_s wrapped_string
def initialize(string)
- @wrapped_string = string.gsub(/[^\u0000-\u007F]/, '?')
+ @wrapped_string = string.gsub(/[^\u0000-\u007F]/, "?")
end
end
diff --git a/activesupport/test/multibyte_test_helpers.rb b/activesupport/test/multibyte_test_helpers.rb
index 2e4b5cc873..7565655f25 100644
--- a/activesupport/test/multibyte_test_helpers.rb
+++ b/activesupport/test/multibyte_test_helpers.rb
@@ -1,18 +1,45 @@
+# frozen_string_literal: true
+
+require "fileutils"
+require "open-uri"
+require "tmpdir"
module MultibyteTestHelpers
- UNICODE_STRING = 'こにちわ'.freeze
- ASCII_STRING = 'ohayo'.freeze
- BYTE_STRING = "\270\236\010\210\245".force_encoding("ASCII-8BIT").freeze
+ class Downloader
+ def self.download(from, to)
+ unless File.exist?(to)
+ unless File.exist?(File.dirname(to))
+ system "mkdir -p #{File.dirname(to)}"
+ end
+ open(from) do |source|
+ File.open(to, "w") do |target|
+ source.each_line do |l|
+ target.write l
+ end
+ end
+ end
+ end
+ true
+ end
+ end
+
+ UNIDATA_URL = "http://www.unicode.org/Public/#{ActiveSupport::Multibyte::Unicode::UNICODE_VERSION}/ucd"
+ CACHE_DIR = "#{Dir.tmpdir}/cache/unicode_conformance/#{ActiveSupport::Multibyte::Unicode::UNICODE_VERSION}"
+ FileUtils.mkdir_p(CACHE_DIR)
+
+ UNICODE_STRING = "こにちわ"
+ ASCII_STRING = "ohayo"
+ BYTE_STRING = (+"\270\236\010\210\245").force_encoding("ASCII-8BIT").freeze
def chars(str)
ActiveSupport::Multibyte::Chars.new(str)
end
def inspect_codepoints(str)
- str.to_s.unpack("U*").map{|cp| cp.to_s(16) }.join(' ')
+ str.to_s.unpack("U*").map { |cp| cp.to_s(16) }.join(" ")
end
- def assert_equal_codepoints(expected, actual, message=nil)
+ def assert_equal_codepoints(expected, actual, message = nil)
assert_equal(inspect_codepoints(expected), inspect_codepoints(actual), message)
end
end
diff --git a/activesupport/test/multibyte_unicode_database_test.rb b/activesupport/test/multibyte_unicode_database_test.rb
deleted file mode 100644
index dd33641ec2..0000000000
--- a/activesupport/test/multibyte_unicode_database_test.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-require 'abstract_unit'
-
-
-class MultibyteUnicodeDatabaseTest < ActiveSupport::TestCase
-
- include ActiveSupport::Multibyte::Unicode
-
- def setup
- @ucd = UnicodeDatabase.new
- end
-
- UnicodeDatabase::ATTRIBUTES.each do |attribute|
- define_method "test_lazy_loading_on_attribute_access_of_#{attribute}" do
- assert_called(@ucd, :load) do
- @ucd.send(attribute)
- end
- end
- end
-
- def test_load
- @ucd.load
- UnicodeDatabase::ATTRIBUTES.each do |attribute|
- assert @ucd.send(attribute).length > 1
- end
- end
-end
diff --git a/activesupport/test/notifications/evented_notification_test.rb b/activesupport/test/notifications/evented_notification_test.rb
index f690ad43fc..4beb8194b9 100644
--- a/activesupport/test/notifications/evented_notification_test.rb
+++ b/activesupport/test/notifications/evented_notification_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module ActiveSupport
module Notifications
@@ -7,7 +9,7 @@ module ActiveSupport
attr_reader :events
def initialize
- @events = []
+ @events = []
end
def start(name, id, payload)
@@ -28,26 +30,26 @@ module ActiveSupport
def test_evented_listener
notifier = Fanout.new
listener = Listener.new
- notifier.subscribe 'hi', listener
- notifier.start 'hi', 1, {}
- notifier.start 'hi', 2, {}
- notifier.finish 'hi', 2, {}
- notifier.finish 'hi', 1, {}
+ notifier.subscribe "hi", listener
+ notifier.start "hi", 1, {}
+ notifier.start "hi", 2, {}
+ notifier.finish "hi", 2, {}
+ notifier.finish "hi", 1, {}
assert_equal 4, listener.events.length
assert_equal [
- [:start, 'hi', 1, {}],
- [:start, 'hi', 2, {}],
- [:finish, 'hi', 2, {}],
- [:finish, 'hi', 1, {}],
+ [:start, "hi", 1, {}],
+ [:start, "hi", 2, {}],
+ [:finish, "hi", 2, {}],
+ [:finish, "hi", 1, {}],
], listener.events
end
def test_evented_listener_no_events
notifier = Fanout.new
listener = Listener.new
- notifier.subscribe 'hi', listener
- notifier.start 'world', 1, {}
+ notifier.subscribe "hi", listener
+ notifier.start "world", 1, {}
assert_equal 0, listener.events.length
end
@@ -55,31 +57,31 @@ module ActiveSupport
notifier = Fanout.new
listener = Listener.new
notifier.subscribe nil, listener
- notifier.start 'hello', 1, {}
- notifier.start 'world', 1, {}
- notifier.finish 'world', 1, {}
- notifier.finish 'hello', 1, {}
+ notifier.start "hello", 1, {}
+ notifier.start "world", 1, {}
+ notifier.finish "world", 1, {}
+ notifier.finish "hello", 1, {}
assert_equal 4, listener.events.length
assert_equal [
- [:start, 'hello', 1, {}],
- [:start, 'world', 1, {}],
- [:finish, 'world', 1, {}],
- [:finish, 'hello', 1, {}],
+ [:start, "hello", 1, {}],
+ [:start, "world", 1, {}],
+ [:finish, "world", 1, {}],
+ [:finish, "hello", 1, {}],
], listener.events
end
def test_evented_listener_priority
notifier = Fanout.new
listener = ListenerWithTimedSupport.new
- notifier.subscribe 'hi', listener
+ notifier.subscribe "hi", listener
- notifier.start 'hi', 1, {}
- notifier.finish 'hi', 1, {}
+ notifier.start "hi", 1, {}
+ notifier.finish "hi", 1, {}
assert_equal [
- [:start, 'hi', 1, {}],
- [:finish, 'hi', 1, {}]
+ [:start, "hi", 1, {}],
+ [:finish, "hi", 1, {}]
], listener.events
end
end
diff --git a/activesupport/test/notifications/instrumenter_test.rb b/activesupport/test/notifications/instrumenter_test.rb
index f46e96f636..d5c9e82e9f 100644
--- a/activesupport/test/notifications/instrumenter_test.rb
+++ b/activesupport/test/notifications/instrumenter_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'active_support/notifications/instrumenter'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/notifications/instrumenter"
module ActiveSupport
module Notifications
@@ -22,11 +24,11 @@ module ActiveSupport
super
@notifier = TestNotifier.new
@instrumenter = Instrumenter.new @notifier
- @payload = { :foo => Object.new }
+ @payload = { foo: Object.new }
end
def test_instrument
- called = false
+ called = false
instrumenter.instrument("foo", payload) {
called = true
}
@@ -39,19 +41,19 @@ module ActiveSupport
assert_equal 1, notifier.finishes.size
name, _, payload = notifier.finishes.first
assert_equal "awesome", name
- assert_equal Hash[:result => 2], payload
+ assert_equal Hash[result: 2], payload
end
def test_start
instrumenter.start("foo", payload)
assert_equal [["foo", instrumenter.id, payload]], notifier.starts
- assert_predicate notifier.finishes, :empty?
+ assert_empty notifier.finishes
end
def test_finish
instrumenter.finish("foo", payload)
assert_equal [["foo", instrumenter.id, payload]], notifier.finishes
- assert_predicate notifier.starts, :empty?
+ assert_empty notifier.starts
end
end
end
diff --git a/activesupport/test/notifications_test.rb b/activesupport/test/notifications_test.rb
index f729f0a95b..54fd4345fb 100644
--- a/activesupport/test/notifications_test.rb
+++ b/activesupport/test/notifications_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'active_support/core_ext/module/delegation'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/module/delegation"
module Notifications
class TestCase < ActiveSupport::TestCase
@@ -24,6 +26,42 @@ module Notifications
end
end
+ class SubscribeEventObjects < TestCase
+ def test_subscribe_events
+ events = []
+ @notifier.subscribe do |event|
+ events << event
+ end
+
+ ActiveSupport::Notifications.instrument("foo")
+ event = events.first
+ assert event, "should have an event"
+ assert_operator event.allocations, :>, 0
+ assert_operator event.cpu_time, :>, 0
+ assert_operator event.idle_time, :>, 0
+ assert_operator event.duration, :>, 0
+ end
+
+ def test_subscribe_via_top_level_api
+ old_notifier = ActiveSupport::Notifications.notifier
+ ActiveSupport::Notifications.notifier = ActiveSupport::Notifications::Fanout.new
+
+ event = nil
+ ActiveSupport::Notifications.subscribe("foo") do |e|
+ event = e
+ end
+
+ ActiveSupport::Notifications.instrument("foo") do
+ 100.times { Object.new } # allocate at least 100 objects
+ end
+
+ assert event
+ assert_operator event.allocations, :>=, 100
+ ensure
+ ActiveSupport::Notifications.notifier = old_notifier
+ end
+ end
+
class SubscribedTest < TestCase
def test_subscribed
name = "foo"
@@ -31,7 +69,7 @@ module Notifications
expected = [name, name]
events = []
- callback = lambda {|*_| events << _.first}
+ callback = lambda { |*_| events << _.first }
ActiveSupport::Notifications.subscribed(callback, name) do
ActiveSupport::Notifications.instrument(name)
ActiveSupport::Notifications.instrument(name2)
@@ -42,6 +80,21 @@ module Notifications
ActiveSupport::Notifications.instrument(name)
assert_equal expected, events
end
+
+ def test_subsribing_to_instrumentation_while_inside_it
+ # the repro requires that there are no evented subscribers for the "foo" event,
+ # so we have to duplicate some of the setup code
+ old_notifier = ActiveSupport::Notifications.notifier
+ ActiveSupport::Notifications.notifier = ActiveSupport::Notifications::Fanout.new
+
+ ActiveSupport::Notifications.subscribe("foo", TestSubscriber.new)
+
+ ActiveSupport::Notifications.instrument("foo") do
+ ActiveSupport::Notifications.subscribe("foo") { }
+ end
+ ensure
+ ActiveSupport::Notifications.notifier = old_notifier
+ end
end
class UnsubscribeTest < TestCase
@@ -126,26 +179,26 @@ module Notifications
def test_log_subscriber_with_string
events = []
- @notifier.subscribe('1') { |*args| events << args }
+ @notifier.subscribe("1") { |*args| events << args }
- @notifier.publish '1'
- @notifier.publish '1.a'
- @notifier.publish 'a.1'
+ @notifier.publish "1"
+ @notifier.publish "1.a"
+ @notifier.publish "a.1"
@notifier.wait
- assert_equal [['1']], events
+ assert_equal [["1"]], events
end
def test_log_subscriber_with_pattern
events = []
@notifier.subscribe(/\d/) { |*args| events << args }
- @notifier.publish '1'
- @notifier.publish 'a.1'
- @notifier.publish '1.a'
+ @notifier.publish "1"
+ @notifier.publish "a.1"
+ @notifier.publish "1.a"
@notifier.wait
- assert_equal [['1'], ['a.1'], ['1.a']], events
+ assert_equal [["1"], ["a.1"], ["1.a"]], events
end
def test_multiple_log_subscribers
@@ -173,7 +226,7 @@ module Notifications
end
class InstrumentationTest < TestCase
- delegate :instrument, :to => ActiveSupport::Notifications
+ delegate :instrument, to: ActiveSupport::Notifications
def test_instrument_returns_block_result
assert_equal 2, instrument(:awesome) { 1 + 1 }
@@ -183,7 +236,7 @@ module Notifications
assert_equal 2, instrument(:awesome) { |p| p[:result] = 1 + 1 }
assert_equal 1, @events.size
assert_equal :awesome, @events.first.name
- assert_equal Hash[:result => 2], @events.first.payload
+ assert_equal Hash[result: 2], @events.first.payload
end
def test_instrumenter_exposes_its_id
@@ -191,24 +244,24 @@ module Notifications
end
def test_nested_events_can_be_instrumented
- instrument(:awesome, :payload => "notifications") do
- instrument(:wot, :payload => "child") do
+ instrument(:awesome, payload: "notifications") do
+ instrument(:wot, payload: "child") do
1 + 1
end
assert_equal 1, @events.size
assert_equal :wot, @events.first.name
- assert_equal Hash[:payload => "child"], @events.first.payload
+ assert_equal Hash[payload: "child"], @events.first.payload
end
assert_equal 2, @events.size
assert_equal :awesome, @events.last.name
- assert_equal Hash[:payload => "notifications"], @events.last.payload
+ assert_equal Hash[payload: "notifications"], @events.last.payload
end
def test_instrument_publishes_when_exception_is_raised
begin
- instrument(:awesome, :payload => "notifications") do
+ instrument(:awesome, payload: "notifications") do
raise "FAIL"
end
rescue RuntimeError => e
@@ -216,15 +269,15 @@ module Notifications
end
assert_equal 1, @events.size
- assert_equal Hash[:payload => "notifications",
- :exception => ["RuntimeError", "FAIL"]], @events.last.payload
+ assert_equal Hash[payload: "notifications",
+ exception: ["RuntimeError", "FAIL"], exception_object: e], @events.last.payload
end
def test_event_is_pushed_even_without_block
- instrument(:awesome, :payload => "notifications")
+ instrument(:awesome, payload: "notifications")
assert_equal 1, @events.size
assert_equal :awesome, @events.last.name
- assert_equal Hash[:payload => "notifications"], @events.last.payload
+ assert_equal Hash[payload: "notifications"], @events.last.payload
end
end
@@ -233,14 +286,14 @@ module Notifications
time = Time.now
event = event(:foo, time, time + 0.01, random_id, {})
- assert_equal :foo, event.name
- assert_equal time, event.time
+ assert_equal :foo, event.name
+ assert_equal time, event.time
assert_in_delta 10.0, event.duration, 0.00001
end
def test_events_consumes_information_given_as_payload
- event = event(:foo, Time.now, Time.now + 1, random_id, :payload => :bar)
- assert_equal Hash[:payload => :bar], event.payload
+ event = event(:foo, Time.now, Time.now + 1, random_id, payload: :bar)
+ assert_equal Hash[payload: :bar], event.payload
end
def test_event_is_parent_based_on_children
@@ -253,12 +306,12 @@ module Notifications
parent.children << child
assert parent.parent_of?(child)
- assert !child.parent_of?(parent)
- assert !parent.parent_of?(not_child)
- assert !not_child.parent_of?(parent)
+ assert_not child.parent_of?(parent)
+ assert_not parent.parent_of?(not_child)
+ assert_not not_child.parent_of?(parent)
end
- 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 e6925e9083..365fa96f4d 100644
--- a/activesupport/test/number_helper_i18n_test.rb
+++ b/activesupport/test/number_helper_i18n_test.rb
@@ -1,46 +1,49 @@
-require 'abstract_unit'
-require 'active_support/number_helper'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/number_helper"
+require "active_support/core_ext/hash/keys"
module ActiveSupport
class NumberHelperI18nTest < ActiveSupport::TestCase
include ActiveSupport::NumberHelper
def setup
- I18n.backend.store_translations 'ts',
- :number => {
- :format => { :precision => 3, :delimiter => ',', :separator => '.', :significant => false, :strip_insignificant_zeros => false },
- :currency => { :format => { :unit => '&$', :format => '%u - %n', :negative_format => '(%u - %n)', :precision => 2 } },
- :human => {
- :format => {
- :precision => 2,
- :significant => true,
- :strip_insignificant_zeros => true
+ I18n.backend.store_translations "ts",
+ number: {
+ format: { precision: 3, delimiter: ",", separator: ".", significant: false, strip_insignificant_zeros: false },
+ currency: { format: { unit: "&$", format: "%u - %n", negative_format: "(%u - %n)", precision: 2 } },
+ human: {
+ format: {
+ precision: 2,
+ significant: true,
+ strip_insignificant_zeros: true
},
- :storage_units => {
- :format => "%n %u",
- :units => {
- :byte => "b",
- :kb => "k"
+ storage_units: {
+ format: "%n %u",
+ units: {
+ byte: "b",
+ kb: "k"
}
},
- :decimal_units => {
- :format => "%n %u",
- :units => {
- :deci => {:one => "Tenth", :other => "Tenths"},
- :unit => "u",
- :ten => {:one => "Ten", :other => "Tens"},
- :thousand => "t",
- :million => "m",
- :billion =>"b",
- :trillion =>"t" ,
- :quadrillion =>"q"
+ decimal_units: {
+ format: "%n %u",
+ units: {
+ deci: { one: "Tenth", other: "Tenths" },
+ unit: "u",
+ ten: { one: "Ten", other: "Tens" },
+ thousand: "t",
+ million: "m",
+ billion: "b",
+ trillion: "t",
+ quadrillion: "q"
}
}
},
- :percentage => { :format => {:delimiter => '', :precision => 2, :strip_insignificant_zeros => true} },
- :precision => { :format => {:delimiter => '', :significant => true} }
+ percentage: { format: { delimiter: "", precision: 2, strip_insignificant_zeros: true } },
+ precision: { format: { delimiter: "", significant: true } }
},
- :custom_units_for_number_to_human => {:mili => "mm", :centi => "cm", :deci => "dm", :unit => "m", :ten => "dam", :hundred => "hm", :thousand => "km"}
+ custom_units_for_number_to_human: { mili: "mm", centi: "cm", deci: "dm", unit: "m", ten: "dam", hundred: "hm", thousand: "km" }
end
def teardown
@@ -48,101 +51,101 @@ module ActiveSupport
end
def test_number_to_i18n_currency
- assert_equal("&$ - 10.00", number_to_currency(10, :locale => 'ts'))
- assert_equal("(&$ - 10.00)", number_to_currency(-10, :locale => 'ts'))
- assert_equal("-10.00 - &$", number_to_currency(-10, :locale => 'ts', :format => "%n - %u"))
+ assert_equal("&$ - 10.00", number_to_currency(10, locale: "ts"))
+ assert_equal("(&$ - 10.00)", number_to_currency(-10, locale: "ts"))
+ assert_equal("-10.00 - &$", number_to_currency(-10, locale: "ts", format: "%n - %u"))
end
def test_number_to_currency_with_empty_i18n_store
- assert_equal("$10.00", number_to_currency(10, :locale => 'empty'))
- assert_equal("-$10.00", number_to_currency(-10, :locale => 'empty'))
+ assert_equal("$10.00", number_to_currency(10, locale: "empty"))
+ assert_equal("-$10.00", number_to_currency(-10, locale: "empty"))
end
def test_locale_default_format_has_precedence_over_helper_defaults
- I18n.backend.store_translations 'ts',
- { :number => { :format => { :separator => ";" } } }
+ I18n.backend.store_translations "ts",
+ number: { format: { separator: ";" } }
- assert_equal("&$ - 10;00", number_to_currency(10, :locale => 'ts'))
+ assert_equal("&$ - 10;00", number_to_currency(10, locale: "ts"))
end
def test_number_to_currency_without_currency_negative_format
- I18n.backend.store_translations 'no_negative_format', :number => {
- :currency => { :format => { :unit => '@', :format => '%n %u' } }
+ I18n.backend.store_translations "no_negative_format", number: {
+ currency: { format: { unit: "@", format: "%n %u" } }
}
- assert_equal("-10.00 @", number_to_currency(-10, :locale => 'no_negative_format'))
+ assert_equal("-10.00 @", number_to_currency(-10, locale: "no_negative_format"))
end
def test_number_with_i18n_precision
- #Delimiter was set to ""
- assert_equal("10000", number_to_rounded(10000, :locale => 'ts'))
+ # Delimiter was set to ""
+ assert_equal("10000", number_to_rounded(10000, locale: "ts"))
- #Precision inherited and significant was set
- assert_equal("1.00", number_to_rounded(1.0, :locale => 'ts'))
+ # Precision inherited and significant was set
+ assert_equal("1.00", number_to_rounded(1.0, locale: "ts"))
end
def test_number_with_i18n_precision_and_empty_i18n_store
- assert_equal("123456789.123", number_to_rounded(123456789.123456789, :locale => 'empty'))
- assert_equal("1.000", number_to_rounded(1.0000, :locale => 'empty'))
+ assert_equal("123456789.123", number_to_rounded(123456789.123456789, locale: "empty"))
+ assert_equal("1.000", number_to_rounded(1.0000, locale: "empty"))
end
def test_number_with_i18n_delimiter
- #Delimiter "," and separator "."
- assert_equal("1,000,000.234", number_to_delimited(1000000.234, :locale => 'ts'))
+ # Delimiter "," and separator "."
+ assert_equal("1,000,000.234", number_to_delimited(1000000.234, locale: "ts"))
end
def test_number_with_i18n_delimiter_and_empty_i18n_store
- assert_equal("1,000,000.234", number_to_delimited(1000000.234, :locale => 'empty'))
+ assert_equal("1,000,000.234", number_to_delimited(1000000.234, locale: "empty"))
end
def test_number_to_i18n_percentage
# to see if strip_insignificant_zeros is true
- assert_equal("1%", number_to_percentage(1, :locale => 'ts'))
+ assert_equal("1%", number_to_percentage(1, locale: "ts"))
# precision is 2, significant should be inherited
- assert_equal("1.24%", number_to_percentage(1.2434, :locale => 'ts'))
+ assert_equal("1.24%", number_to_percentage(1.2434, locale: "ts"))
# no delimiter
- assert_equal("12434%", number_to_percentage(12434, :locale => 'ts'))
+ assert_equal("12434%", number_to_percentage(12434, locale: "ts"))
end
def test_number_to_i18n_percentage_and_empty_i18n_store
- assert_equal("1.000%", number_to_percentage(1, :locale => 'empty'))
- assert_equal("1.243%", number_to_percentage(1.2434, :locale => 'empty'))
- assert_equal("12434.000%", number_to_percentage(12434, :locale => 'empty'))
+ assert_equal("1.000%", number_to_percentage(1, locale: "empty"))
+ assert_equal("1.243%", number_to_percentage(1.2434, locale: "empty"))
+ assert_equal("12434.000%", number_to_percentage(12434, locale: "empty"))
end
def test_number_to_i18n_human_size
- #b for bytes and k for kbytes
- assert_equal("2 k", number_to_human_size(2048, :locale => 'ts'))
- assert_equal("42 b", number_to_human_size(42, :locale => 'ts'))
+ # b for bytes and k for kbytes
+ assert_equal("2 k", number_to_human_size(2048, locale: "ts"))
+ assert_equal("42 b", number_to_human_size(42, locale: "ts"))
end
def test_number_to_i18n_human_size_with_empty_i18n_store
- assert_equal("2 KB", number_to_human_size(2048, :locale => 'empty'))
- assert_equal("42 Bytes", number_to_human_size(42, :locale => 'empty'))
+ assert_equal("2 KB", number_to_human_size(2048, locale: "empty"))
+ assert_equal("42 Bytes", number_to_human_size(42, locale: "empty"))
end
def test_number_to_human_with_default_translation_scope
- #Using t for thousand
- assert_equal "2 t", number_to_human(2000, :locale => 'ts')
- #Significant was set to true with precision 2, using b for billion
- assert_equal "1.2 b", number_to_human(1234567890, :locale => 'ts')
- #Using pluralization (Ten/Tens and Tenth/Tenths)
- assert_equal "1 Tenth", number_to_human(0.1, :locale => 'ts')
- assert_equal "1.3 Tenth", number_to_human(0.134, :locale => 'ts')
- assert_equal "2 Tenths", number_to_human(0.2, :locale => 'ts')
- assert_equal "1 Ten", number_to_human(10, :locale => 'ts')
- assert_equal "1.2 Ten", number_to_human(12, :locale => 'ts')
- assert_equal "2 Tens", number_to_human(20, :locale => 'ts')
+ # Using t for thousand
+ assert_equal "2 t", number_to_human(2000, locale: "ts")
+ # Significant was set to true with precision 2, using b for billion
+ assert_equal "1.2 b", number_to_human(1234567890, locale: "ts")
+ # Using pluralization (Ten/Tens and Tenth/Tenths)
+ assert_equal "1 Tenth", number_to_human(0.1, locale: "ts")
+ assert_equal "1.3 Tenth", number_to_human(0.134, locale: "ts")
+ assert_equal "2 Tenths", number_to_human(0.2, locale: "ts")
+ assert_equal "1 Ten", number_to_human(10, locale: "ts")
+ assert_equal "1.2 Ten", number_to_human(12, locale: "ts")
+ assert_equal "2 Tens", number_to_human(20, locale: "ts")
end
def test_number_to_human_with_empty_i18n_store
- assert_equal "2 Thousand", number_to_human(2000, :locale => 'empty')
- assert_equal "1.23 Billion", number_to_human(1234567890, :locale => 'empty')
+ assert_equal "2 Thousand", number_to_human(2000, locale: "empty")
+ assert_equal "1.23 Billion", number_to_human(1234567890, locale: "empty")
end
def test_number_to_human_with_custom_translation_scope
- #Significant was set to true with precision 2, with custom translated units
- assert_equal "4.3 cm", number_to_human(0.0432, :locale => 'ts', :units => :custom_units_for_number_to_human)
+ # Significant was set to true with precision 2, with custom translated units
+ assert_equal "4.3 cm", number_to_human(0.0432, locale: "ts", units: :custom_units_for_number_to_human)
end
end
end
diff --git a/activesupport/test/number_helper_test.rb b/activesupport/test/number_helper_test.rb
index 83efbffdfb..16ccc5572c 100644
--- a/activesupport/test/number_helper_test.rb
+++ b/activesupport/test/number_helper_test.rb
@@ -1,11 +1,12 @@
-require 'abstract_unit'
-require 'active_support/number_helper'
-require 'active_support/core_ext/string/output_safety'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/number_helper"
+require "active_support/core_ext/string/output_safety"
module ActiveSupport
module NumberHelper
class NumberHelperTest < ActiveSupport::TestCase
-
class TestClassWithInstanceNumberHelpers
include ActiveSupport::NumberHelper
end
@@ -34,21 +35,31 @@ module ActiveSupport
gigabytes(number) * 1024
end
+ def petabytes(number)
+ terabytes(number) * 1024
+ end
+
+ def exabytes(number)
+ petabytes(number) * 1024
+ end
+
def test_number_to_phone
[@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper|
assert_equal("555-1234", number_helper.number_to_phone(5551234))
assert_equal("800-555-1212", number_helper.number_to_phone(8005551212))
- assert_equal("(800) 555-1212", number_helper.number_to_phone(8005551212, {:area_code => true}))
- assert_equal("", number_helper.number_to_phone("", {:area_code => true}))
- assert_equal("800 555 1212", number_helper.number_to_phone(8005551212, {:delimiter => " "}))
- assert_equal("(800) 555-1212 x 123", number_helper.number_to_phone(8005551212, {:area_code => true, :extension => 123}))
- assert_equal("800-555-1212", number_helper.number_to_phone(8005551212, :extension => " "))
- assert_equal("555.1212", number_helper.number_to_phone(5551212, :delimiter => '.'))
+ assert_equal("(800) 555-1212", number_helper.number_to_phone(8005551212, area_code: true))
+ assert_equal("", number_helper.number_to_phone("", area_code: true))
+ assert_equal("800 555 1212", number_helper.number_to_phone(8005551212, delimiter: " "))
+ assert_equal("(800) 555-1212 x 123", number_helper.number_to_phone(8005551212, area_code: true, extension: 123))
+ assert_equal("800-555-1212", number_helper.number_to_phone(8005551212, extension: " "))
+ assert_equal("555.1212", number_helper.number_to_phone(5551212, delimiter: "."))
assert_equal("800-555-1212", number_helper.number_to_phone("8005551212"))
- assert_equal("+1-800-555-1212", number_helper.number_to_phone(8005551212, :country_code => 1))
- assert_equal("+18005551212", number_helper.number_to_phone(8005551212, :country_code => 1, :delimiter => ''))
+ assert_equal("+1-800-555-1212", number_helper.number_to_phone(8005551212, country_code: 1))
+ assert_equal("+18005551212", number_helper.number_to_phone(8005551212, country_code: 1, delimiter: ""))
assert_equal("22-555-1212", number_helper.number_to_phone(225551212))
- assert_equal("+45-22-555-1212", number_helper.number_to_phone(225551212, :country_code => 45))
+ assert_equal("+45-22-555-1212", number_helper.number_to_phone(225551212, country_code: 45))
+ assert_equal("(755) 6123-4567", number_helper.number_to_phone(75561234567, pattern: /(\d{3,4})(\d{4})(\d{4})/, area_code: true))
+ assert_equal("133-1234-5678", number_helper.number_to_phone(13312345678, pattern: /(\d{3})(\d{4})(\d{4})/))
end
end
@@ -57,29 +68,28 @@ module ActiveSupport
assert_equal("$1,234,567,890.50", number_helper.number_to_currency(1234567890.50))
assert_equal("$1,234,567,890.51", number_helper.number_to_currency(1234567890.506))
assert_equal("-$1,234,567,890.50", number_helper.number_to_currency(-1234567890.50))
- assert_equal("-$ 1,234,567,890.50", number_helper.number_to_currency(-1234567890.50, {:format => "%u %n"}))
- assert_equal("($1,234,567,890.50)", number_helper.number_to_currency(-1234567890.50, {:negative_format => "(%u%n)"}))
- assert_equal("$1,234,567,892", number_helper.number_to_currency(1234567891.50, {:precision => 0}))
- assert_equal("$1,234,567,890.5", number_helper.number_to_currency(1234567890.50, {:precision => 1}))
- assert_equal("&pound;1234567890,50", number_helper.number_to_currency(1234567890.50, {:unit => "&pound;", :separator => ",", :delimiter => ""}))
+ assert_equal("-$ 1,234,567,890.50", number_helper.number_to_currency(-1234567890.50, format: "%u %n"))
+ assert_equal("($1,234,567,890.50)", number_helper.number_to_currency(-1234567890.50, negative_format: "(%u%n)"))
+ assert_equal("$1,234,567,892", number_helper.number_to_currency(1234567891.50, precision: 0))
+ assert_equal("$1,234,567,890.5", number_helper.number_to_currency(1234567890.50, precision: 1))
+ assert_equal("&pound;1234567890,50", number_helper.number_to_currency(1234567890.50, unit: "&pound;", separator: ",", delimiter: ""))
assert_equal("$1,234,567,890.50", number_helper.number_to_currency("1234567890.50"))
- assert_equal("1,234,567,890.50 K&#269;", number_helper.number_to_currency("1234567890.50", {:unit => "K&#269;", :format => "%n %u"}))
- assert_equal("1,234,567,890.50 - K&#269;", number_helper.number_to_currency("-1234567890.50", {:unit => "K&#269;", :format => "%n %u", :negative_format => "%n - %u"}))
- assert_equal("0.00", number_helper.number_to_currency(+0.0, {:unit => "", :negative_format => "(%n)"}))
- assert_equal("(0.00)", number_helper.number_to_currency(-0.0, {:unit => "", :negative_format => "(%n)"}))
+ assert_equal("1,234,567,890.50 K&#269;", number_helper.number_to_currency("1234567890.50", unit: "K&#269;", format: "%n %u"))
+ assert_equal("1,234,567,890.50 - K&#269;", number_helper.number_to_currency("-1234567890.50", unit: "K&#269;", format: "%n %u", negative_format: "%n - %u"))
+ assert_equal("0.00", number_helper.number_to_currency(+0.0, unit: "", negative_format: "(%n)"))
end
end
def test_number_to_percentage
[@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper|
assert_equal("100.000%", number_helper.number_to_percentage(100))
- assert_equal("100%", number_helper.number_to_percentage(100, {:precision => 0}))
- assert_equal("302.06%", number_helper.number_to_percentage(302.0574, {:precision => 2}))
+ assert_equal("100%", number_helper.number_to_percentage(100, precision: 0))
+ assert_equal("302.06%", number_helper.number_to_percentage(302.0574, precision: 2))
assert_equal("100.000%", number_helper.number_to_percentage("100"))
assert_equal("1000.000%", number_helper.number_to_percentage("1000"))
- assert_equal("123.4%", number_helper.number_to_percentage(123.400, :precision => 3, :strip_insignificant_zeros => true))
- assert_equal("1.000,000%", number_helper.number_to_percentage(1000, :delimiter => '.', :separator => ','))
- assert_equal("1000.000 %", number_helper.number_to_percentage(1000, :format => "%n %"))
+ assert_equal("123.4%", number_helper.number_to_percentage(123.400, precision: 3, strip_insignificant_zeros: true))
+ assert_equal("1.000,000%", number_helper.number_to_percentage(1000, delimiter: ".", separator: ","))
+ assert_equal("1000.000 %", number_helper.number_to_percentage(1000, format: "%n %"))
assert_equal("98a%", number_helper.number_to_percentage("98a"))
assert_equal("NaN%", number_helper.number_to_percentage(Float::NAN))
assert_equal("Inf%", number_helper.number_to_percentage(Float::INFINITY))
@@ -106,15 +116,16 @@ module ActiveSupport
assert_equal("123,456,789.78901", number_helper.number_to_delimited(123456789.78901))
assert_equal("0.78901", number_helper.number_to_delimited(0.78901))
assert_equal("123,456.78", number_helper.number_to_delimited("123456.78"))
+ assert_equal("1,23,456.78", number_helper.number_to_delimited("123456.78", delimiter_pattern: /(\d+?)(?=(\d\d)+(\d)(?!\d))/))
assert_equal("123,456.78", number_helper.number_to_delimited("123456.78".html_safe))
end
end
def test_to_delimited_with_options_hash
[@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper|
- assert_equal '12 345 678', number_helper.number_to_delimited(12345678, :delimiter => ' ')
- assert_equal '12,345,678-05', number_helper.number_to_delimited(12345678.05, :separator => '-')
- assert_equal '12.345.678,05', number_helper.number_to_delimited(12345678.05, :separator => ',', :delimiter => '.')
+ assert_equal "12 345 678", number_helper.number_to_delimited(12345678, delimiter: " ")
+ assert_equal "12,345,678-05", number_helper.number_to_delimited(12345678.05, separator: "-")
+ assert_equal "12.345.678,05", number_helper.number_to_delimited(12345678.05, separator: ",", delimiter: ".")
end
end
@@ -122,77 +133,77 @@ module ActiveSupport
[@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper|
assert_equal("-111.235", number_helper.number_to_rounded(-111.2346))
assert_equal("111.235", number_helper.number_to_rounded(111.2346))
- assert_equal("31.83", number_helper.number_to_rounded(31.825, :precision => 2))
- assert_equal("111.23", number_helper.number_to_rounded(111.2346, :precision => 2))
- assert_equal("111.00", number_helper.number_to_rounded(111, :precision => 2))
+ assert_equal("31.83", number_helper.number_to_rounded(31.825, precision: 2))
+ assert_equal("111.23", number_helper.number_to_rounded(111.2346, precision: 2))
+ assert_equal("111.00", number_helper.number_to_rounded(111, precision: 2))
assert_equal("111.235", number_helper.number_to_rounded("111.2346"))
- assert_equal("31.83", number_helper.number_to_rounded("31.825", :precision => 2))
- assert_equal("3268", number_helper.number_to_rounded((32.6751 * 100.00), :precision => 0))
- assert_equal("112", number_helper.number_to_rounded(111.50, :precision => 0))
- assert_equal("1234567892", number_helper.number_to_rounded(1234567891.50, :precision => 0))
- assert_equal("0", number_helper.number_to_rounded(0, :precision => 0))
- assert_equal("0.00100", number_helper.number_to_rounded(0.001, :precision => 5))
- assert_equal("0.001", number_helper.number_to_rounded(0.00111, :precision => 3))
- assert_equal("10.00", number_helper.number_to_rounded(9.995, :precision => 2))
- assert_equal("11.00", number_helper.number_to_rounded(10.995, :precision => 2))
- assert_equal("0.00", number_helper.number_to_rounded(-0.001, :precision => 2))
-
- assert_equal("111.23460000000000000000", number_helper.number_to_rounded(111.2346, :precision => 20))
- assert_equal("111.23460000000000000000", number_helper.number_to_rounded(Rational(1112346, 10000), :precision => 20))
- assert_equal("111.23460000000000000000", number_helper.number_to_rounded('111.2346', :precision => 20))
- assert_equal("111.23460000000000000000", number_helper.number_to_rounded(BigDecimal(111.2346, Float::DIG), :precision => 20))
- assert_equal("111.2346" + "0"*96, number_helper.number_to_rounded('111.2346', :precision => 100))
- assert_equal("111.2346", number_helper.number_to_rounded(Rational(1112346, 10000), :precision => 4))
- assert_equal('0.00', number_helper.number_to_rounded(Rational(0, 1), :precision => 2))
+ assert_equal("31.83", number_helper.number_to_rounded("31.825", precision: 2))
+ assert_equal("3268", number_helper.number_to_rounded((32.6751 * 100.00), precision: 0))
+ assert_equal("112", number_helper.number_to_rounded(111.50, precision: 0))
+ assert_equal("1234567892", number_helper.number_to_rounded(1234567891.50, precision: 0))
+ assert_equal("0", number_helper.number_to_rounded(0, precision: 0))
+ assert_equal("0.00100", number_helper.number_to_rounded(0.001, precision: 5))
+ assert_equal("0.001", number_helper.number_to_rounded(0.00111, precision: 3))
+ assert_equal("10.00", number_helper.number_to_rounded(9.995, precision: 2))
+ assert_equal("11.00", number_helper.number_to_rounded(10.995, precision: 2))
+ assert_equal("0.00", number_helper.number_to_rounded(-0.001, precision: 2))
+
+ assert_equal("111.23460000000000000000", number_helper.number_to_rounded(111.2346, precision: 20))
+ assert_equal("111.23460000000000000000", number_helper.number_to_rounded(Rational(1112346, 10000), precision: 20))
+ assert_equal("111.23460000000000000000", number_helper.number_to_rounded("111.2346", precision: 20))
+ assert_equal("111.23460000000000000000", number_helper.number_to_rounded(BigDecimal(111.2346, Float::DIG), precision: 20))
+ assert_equal("111.2346" + "0" * 96, number_helper.number_to_rounded("111.2346", precision: 100))
+ assert_equal("111.2346", number_helper.number_to_rounded(Rational(1112346, 10000), precision: 4))
+ assert_equal("0.00", number_helper.number_to_rounded(Rational(0, 1), precision: 2))
end
end
def test_to_rounded_with_custom_delimiter_and_separator
[@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper|
- assert_equal '31,83', number_helper.number_to_rounded(31.825, :precision => 2, :separator => ',')
- assert_equal '1.231,83', number_helper.number_to_rounded(1231.825, :precision => 2, :separator => ',', :delimiter => '.')
+ assert_equal "31,83", number_helper.number_to_rounded(31.825, precision: 2, separator: ",")
+ assert_equal "1.231,83", number_helper.number_to_rounded(1231.825, precision: 2, separator: ",", delimiter: ".")
end
end
def test_to_rounded_with_significant_digits
[@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper|
- assert_equal "124000", number_helper.number_to_rounded(123987, :precision => 3, :significant => true)
- assert_equal "120000000", number_helper.number_to_rounded(123987876, :precision => 2, :significant => true )
- assert_equal "40000", number_helper.number_to_rounded("43523", :precision => 1, :significant => true )
- assert_equal "9775", number_helper.number_to_rounded(9775, :precision => 4, :significant => true )
- assert_equal "5.4", number_helper.number_to_rounded(5.3923, :precision => 2, :significant => true )
- assert_equal "5", number_helper.number_to_rounded(5.3923, :precision => 1, :significant => true )
- assert_equal "1", number_helper.number_to_rounded(1.232, :precision => 1, :significant => true )
- assert_equal "7", number_helper.number_to_rounded(7, :precision => 1, :significant => true )
- assert_equal "1", number_helper.number_to_rounded(1, :precision => 1, :significant => true )
- assert_equal "53", number_helper.number_to_rounded(52.7923, :precision => 2, :significant => true )
- assert_equal "9775.00", number_helper.number_to_rounded(9775, :precision => 6, :significant => true )
- assert_equal "5.392900", number_helper.number_to_rounded(5.3929, :precision => 7, :significant => true )
- assert_equal "0.0", number_helper.number_to_rounded(0, :precision => 2, :significant => true )
- assert_equal "0", number_helper.number_to_rounded(0, :precision => 1, :significant => true )
- assert_equal "0.0001", number_helper.number_to_rounded(0.0001, :precision => 1, :significant => true )
- assert_equal "0.000100", number_helper.number_to_rounded(0.0001, :precision => 3, :significant => true )
- assert_equal "0.0001", number_helper.number_to_rounded(0.0001111, :precision => 1, :significant => true )
- assert_equal "10.0", number_helper.number_to_rounded(9.995, :precision => 3, :significant => true)
- assert_equal "9.99", number_helper.number_to_rounded(9.994, :precision => 3, :significant => true)
- assert_equal "11.0", number_helper.number_to_rounded(10.995, :precision => 3, :significant => true)
-
- assert_equal "9775.0000000000000000", number_helper.number_to_rounded(9775, :precision => 20, :significant => true )
- assert_equal "9775.0000000000000000", number_helper.number_to_rounded(9775.0, :precision => 20, :significant => true )
- assert_equal "9775.0000000000000000", number_helper.number_to_rounded(Rational(9775, 1), :precision => 20, :significant => true )
- assert_equal "97.750000000000000000", number_helper.number_to_rounded(Rational(9775, 100), :precision => 20, :significant => true )
- assert_equal "9775.0000000000000000", number_helper.number_to_rounded(BigDecimal(9775), :precision => 20, :significant => true )
- assert_equal "9775.0000000000000000", number_helper.number_to_rounded("9775", :precision => 20, :significant => true )
- assert_equal "9775." + "0"*96, number_helper.number_to_rounded("9775", :precision => 100, :significant => true )
- assert_equal("97.7", number_helper.number_to_rounded(Rational(9772, 100), :precision => 3, :significant => true))
+ assert_equal "124000", number_helper.number_to_rounded(123987, precision: 3, significant: true)
+ assert_equal "120000000", number_helper.number_to_rounded(123987876, precision: 2, significant: true)
+ assert_equal "40000", number_helper.number_to_rounded("43523", precision: 1, significant: true)
+ assert_equal "9775", number_helper.number_to_rounded(9775, precision: 4, significant: true)
+ assert_equal "5.4", number_helper.number_to_rounded(5.3923, precision: 2, significant: true)
+ assert_equal "5", number_helper.number_to_rounded(5.3923, precision: 1, significant: true)
+ assert_equal "1", number_helper.number_to_rounded(1.232, precision: 1, significant: true)
+ assert_equal "7", number_helper.number_to_rounded(7, precision: 1, significant: true)
+ assert_equal "1", number_helper.number_to_rounded(1, precision: 1, significant: true)
+ assert_equal "53", number_helper.number_to_rounded(52.7923, precision: 2, significant: true)
+ assert_equal "9775.00", number_helper.number_to_rounded(9775, precision: 6, significant: true)
+ assert_equal "5.392900", number_helper.number_to_rounded(5.3929, precision: 7, significant: true)
+ assert_equal "0.0", number_helper.number_to_rounded(0, precision: 2, significant: true)
+ assert_equal "0", number_helper.number_to_rounded(0, precision: 1, significant: true)
+ assert_equal "0.0001", number_helper.number_to_rounded(0.0001, precision: 1, significant: true)
+ assert_equal "0.000100", number_helper.number_to_rounded(0.0001, precision: 3, significant: true)
+ assert_equal "0.0001", number_helper.number_to_rounded(0.0001111, precision: 1, significant: true)
+ assert_equal "10.0", number_helper.number_to_rounded(9.995, precision: 3, significant: true)
+ assert_equal "9.99", number_helper.number_to_rounded(9.994, precision: 3, significant: true)
+ assert_equal "11.0", number_helper.number_to_rounded(10.995, precision: 3, significant: true)
+
+ assert_equal "9775.0000000000000000", number_helper.number_to_rounded(9775, precision: 20, significant: true)
+ assert_equal "9775.0000000000000000", number_helper.number_to_rounded(9775.0, precision: 20, significant: true)
+ assert_equal "9775.0000000000000000", number_helper.number_to_rounded(Rational(9775, 1), precision: 20, significant: true)
+ assert_equal "97.750000000000000000", number_helper.number_to_rounded(Rational(9775, 100), precision: 20, significant: true)
+ assert_equal "9775.0000000000000000", number_helper.number_to_rounded(BigDecimal(9775), precision: 20, significant: true)
+ assert_equal "9775.0000000000000000", number_helper.number_to_rounded("9775", precision: 20, significant: true)
+ assert_equal "9775." + "0" * 96, number_helper.number_to_rounded("9775", precision: 100, significant: true)
+ assert_equal("97.7", number_helper.number_to_rounded(Rational(9772, 100), precision: 3, significant: true))
end
end
def test_to_rounded_with_strip_insignificant_zeros
[@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper|
- assert_equal "9775.43", number_helper.number_to_rounded(9775.43, :precision => 4, :strip_insignificant_zeros => true )
- assert_equal "9775.2", number_helper.number_to_rounded(9775.2, :precision => 6, :significant => true, :strip_insignificant_zeros => true )
- assert_equal "0", number_helper.number_to_rounded(0, :precision => 6, :significant => true, :strip_insignificant_zeros => true )
+ assert_equal "9775.43", number_helper.number_to_rounded(9775.43, precision: 4, strip_insignificant_zeros: true)
+ assert_equal "9775.2", number_helper.number_to_rounded(9775.2, precision: 6, significant: true, strip_insignificant_zeros: true)
+ assert_equal "0", number_helper.number_to_rounded(0, precision: 6, significant: true, strip_insignificant_zeros: true)
end
end
@@ -200,148 +211,145 @@ module ActiveSupport
[@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper|
# Zero precision with significant is a mistake (would always return zero),
# so we treat it as if significant was false (increases backwards compatibility for number_to_human_size)
- assert_equal "124", number_helper.number_to_rounded(123.987, :precision => 0, :significant => true)
- assert_equal "12", number_helper.number_to_rounded(12, :precision => 0, :significant => true )
- assert_equal "12", number_helper.number_to_rounded("12.3", :precision => 0, :significant => true )
+ assert_equal "124", number_helper.number_to_rounded(123.987, precision: 0, significant: true)
+ assert_equal "12", number_helper.number_to_rounded(12, precision: 0, significant: true)
+ assert_equal "12", number_helper.number_to_rounded("12.3", precision: 0, significant: true)
end
end
def test_number_number_to_human_size
[@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper|
- assert_equal '0 Bytes', number_helper.number_to_human_size(0)
- assert_equal '1 Byte', number_helper.number_to_human_size(1)
- assert_equal '3 Bytes', number_helper.number_to_human_size(3.14159265)
- assert_equal '123 Bytes', number_helper.number_to_human_size(123.0)
- assert_equal '123 Bytes', number_helper.number_to_human_size(123)
- assert_equal '1.21 KB', number_helper.number_to_human_size(1234)
- assert_equal '12.1 KB', number_helper.number_to_human_size(12345)
- assert_equal '1.18 MB', number_helper.number_to_human_size(1234567)
- assert_equal '1.15 GB', number_helper.number_to_human_size(1234567890)
- assert_equal '1.12 TB', number_helper.number_to_human_size(1234567890123)
- assert_equal '1030 TB', number_helper.number_to_human_size(terabytes(1026))
- assert_equal '444 KB', number_helper.number_to_human_size(kilobytes(444))
- assert_equal '1020 MB', number_helper.number_to_human_size(megabytes(1023))
- assert_equal '3 TB', number_helper.number_to_human_size(terabytes(3))
- assert_equal '1.2 MB', number_helper.number_to_human_size(1234567, :precision => 2)
- assert_equal '3 Bytes', number_helper.number_to_human_size(3.14159265, :precision => 4)
- assert_equal '123 Bytes', number_helper.number_to_human_size('123')
- assert_equal '1 KB', number_helper.number_to_human_size(kilobytes(1.0123), :precision => 2)
- assert_equal '1.01 KB', number_helper.number_to_human_size(kilobytes(1.0100), :precision => 4)
- assert_equal '10 KB', number_helper.number_to_human_size(kilobytes(10.000), :precision => 4)
- assert_equal '1 Byte', number_helper.number_to_human_size(1.1)
- assert_equal '10 Bytes', number_helper.number_to_human_size(10)
- end
- end
-
- def test_number_to_human_size_with_si_prefix
- [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper|
- assert_equal '3 Bytes', number_helper.number_to_human_size(3.14159265, :prefix => :si)
- assert_equal '123 Bytes', number_helper.number_to_human_size(123.0, :prefix => :si)
- assert_equal '123 Bytes', number_helper.number_to_human_size(123, :prefix => :si)
- assert_equal '1.23 KB', number_helper.number_to_human_size(1234, :prefix => :si)
- assert_equal '12.3 KB', number_helper.number_to_human_size(12345, :prefix => :si)
- assert_equal '1.23 MB', number_helper.number_to_human_size(1234567, :prefix => :si)
- assert_equal '1.23 GB', number_helper.number_to_human_size(1234567890, :prefix => :si)
- assert_equal '1.23 TB', number_helper.number_to_human_size(1234567890123, :prefix => :si)
+ assert_equal "0 Bytes", number_helper.number_to_human_size(0)
+ assert_equal "1 Byte", number_helper.number_to_human_size(1)
+ assert_equal "3 Bytes", number_helper.number_to_human_size(3.14159265)
+ assert_equal "123 Bytes", number_helper.number_to_human_size(123.0)
+ assert_equal "123 Bytes", number_helper.number_to_human_size(123)
+ assert_equal "1.21 KB", number_helper.number_to_human_size(1234)
+ assert_equal "12.1 KB", number_helper.number_to_human_size(12345)
+ assert_equal "1.18 MB", number_helper.number_to_human_size(1234567)
+ assert_equal "1.15 GB", number_helper.number_to_human_size(1234567890)
+ assert_equal "1.12 TB", number_helper.number_to_human_size(1234567890123)
+ assert_equal "1.1 PB", number_helper.number_to_human_size(1234567890123456)
+ assert_equal "1.07 EB", number_helper.number_to_human_size(1234567890123456789)
+ assert_equal "1030 EB", number_helper.number_to_human_size(exabytes(1026))
+ assert_equal "444 KB", number_helper.number_to_human_size(kilobytes(444))
+ assert_equal "1020 MB", number_helper.number_to_human_size(megabytes(1023))
+ assert_equal "3 TB", number_helper.number_to_human_size(terabytes(3))
+ assert_equal "1.2 MB", number_helper.number_to_human_size(1234567, precision: 2)
+ assert_equal "3 Bytes", number_helper.number_to_human_size(3.14159265, precision: 4)
+ assert_equal "123 Bytes", number_helper.number_to_human_size("123")
+ assert_equal "1 KB", number_helper.number_to_human_size(kilobytes(1.0123), precision: 2)
+ assert_equal "1.01 KB", number_helper.number_to_human_size(kilobytes(1.0100), precision: 4)
+ assert_equal "10 KB", number_helper.number_to_human_size(kilobytes(10.000), precision: 4)
+ assert_equal "1 Byte", number_helper.number_to_human_size(1.1)
+ assert_equal "10 Bytes", number_helper.number_to_human_size(10)
end
end
def test_number_to_human_size_with_options_hash
[@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper|
- assert_equal '1.2 MB', number_helper.number_to_human_size(1234567, :precision => 2)
- assert_equal '3 Bytes', number_helper.number_to_human_size(3.14159265, :precision => 4)
- assert_equal '1 KB', number_helper.number_to_human_size(kilobytes(1.0123), :precision => 2)
- assert_equal '1.01 KB', number_helper.number_to_human_size(kilobytes(1.0100), :precision => 4)
- assert_equal '10 KB', number_helper.number_to_human_size(kilobytes(10.000), :precision => 4)
- assert_equal '1 TB', number_helper.number_to_human_size(1234567890123, :precision => 1)
- assert_equal '500 MB', number_helper.number_to_human_size(524288000, :precision=>3)
- assert_equal '10 MB', number_helper.number_to_human_size(9961472, :precision=>0)
- assert_equal '40 KB', number_helper.number_to_human_size(41010, :precision => 1)
- assert_equal '40 KB', number_helper.number_to_human_size(41100, :precision => 2)
- assert_equal '1.0 KB', number_helper.number_to_human_size(kilobytes(1.0123), :precision => 2, :strip_insignificant_zeros => false)
- assert_equal '1.012 KB', number_helper.number_to_human_size(kilobytes(1.0123), :precision => 3, :significant => false)
- assert_equal '1 KB', number_helper.number_to_human_size(kilobytes(1.0123), :precision => 0, :significant => true) #ignores significant it precision is 0
+ assert_equal "1.2 MB", number_helper.number_to_human_size(1234567, precision: 2)
+ assert_equal "3 Bytes", number_helper.number_to_human_size(3.14159265, precision: 4)
+ assert_equal "1 KB", number_helper.number_to_human_size(kilobytes(1.0123), precision: 2)
+ assert_equal "1.01 KB", number_helper.number_to_human_size(kilobytes(1.0100), precision: 4)
+ assert_equal "10 KB", number_helper.number_to_human_size(kilobytes(10.000), precision: 4)
+ assert_equal "1 TB", number_helper.number_to_human_size(1234567890123, precision: 1)
+ assert_equal "500 MB", number_helper.number_to_human_size(524288000, precision: 3)
+ assert_equal "10 MB", number_helper.number_to_human_size(9961472, precision: 0)
+ assert_equal "40 KB", number_helper.number_to_human_size(41010, precision: 1)
+ assert_equal "40 KB", number_helper.number_to_human_size(41100, precision: 2)
+ assert_equal "1.0 KB", number_helper.number_to_human_size(kilobytes(1.0123), precision: 2, strip_insignificant_zeros: false)
+ assert_equal "1.012 KB", number_helper.number_to_human_size(kilobytes(1.0123), precision: 3, significant: false)
+ assert_equal "1 KB", number_helper.number_to_human_size(kilobytes(1.0123), precision: 0, significant: true) # ignores significant it precision is 0
end
end
def test_number_to_human_size_with_custom_delimiter_and_separator
[@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper|
- assert_equal '1,01 KB', number_helper.number_to_human_size(kilobytes(1.0123), :precision => 3, :separator => ',')
- assert_equal '1,01 KB', number_helper.number_to_human_size(kilobytes(1.0100), :precision => 4, :separator => ',')
- assert_equal '1.000,1 TB', number_helper.number_to_human_size(terabytes(1000.1), :precision => 5, :delimiter => '.', :separator => ',')
+ assert_equal "1,01 KB", number_helper.number_to_human_size(kilobytes(1.0123), precision: 3, separator: ",")
+ assert_equal "1,01 KB", number_helper.number_to_human_size(kilobytes(1.0100), precision: 4, separator: ",")
+ assert_equal "1.000,1 TB", number_helper.number_to_human_size(terabytes(1000.1), precision: 5, delimiter: ".", separator: ",")
end
end
def test_number_to_human
[@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper|
- assert_equal '-123', number_helper.number_to_human(-123)
- assert_equal '-0.5', number_helper.number_to_human(-0.5)
- assert_equal '0', number_helper.number_to_human(0)
- assert_equal '0.5', number_helper.number_to_human(0.5)
- assert_equal '123', number_helper.number_to_human(123)
- assert_equal '1.23 Thousand', number_helper.number_to_human(1234)
- assert_equal '12.3 Thousand', number_helper.number_to_human(12345)
- assert_equal '1.23 Million', number_helper.number_to_human(1234567)
- assert_equal '1.23 Billion', number_helper.number_to_human(1234567890)
- assert_equal '1.23 Trillion', number_helper.number_to_human(1234567890123)
- assert_equal '1.23 Quadrillion', number_helper.number_to_human(1234567890123456)
- assert_equal '1230 Quadrillion', number_helper.number_to_human(1234567890123456789)
- assert_equal '490 Thousand', number_helper.number_to_human(489939, :precision => 2)
- assert_equal '489.9 Thousand', number_helper.number_to_human(489939, :precision => 4)
- assert_equal '489 Thousand', number_helper.number_to_human(489000, :precision => 4)
- assert_equal '489.0 Thousand', number_helper.number_to_human(489000, :precision => 4, :strip_insignificant_zeros => false)
- assert_equal '1.2346 Million', number_helper.number_to_human(1234567, :precision => 4, :significant => false)
- assert_equal '1,2 Million', number_helper.number_to_human(1234567, :precision => 1, :significant => false, :separator => ',')
- assert_equal '1 Million', number_helper.number_to_human(1234567, :precision => 0, :significant => true, :separator => ',') #significant forced to false
+ assert_equal "-123", number_helper.number_to_human(-123)
+ assert_equal "-0.5", number_helper.number_to_human(-0.5)
+ assert_equal "0", number_helper.number_to_human(0)
+ assert_equal "0.5", number_helper.number_to_human(0.5)
+ assert_equal "123", number_helper.number_to_human(123)
+ assert_equal "1.23 Thousand", number_helper.number_to_human(1234)
+ assert_equal "12.3 Thousand", number_helper.number_to_human(12345)
+ assert_equal "1.23 Million", number_helper.number_to_human(1234567)
+ assert_equal "1.23 Billion", number_helper.number_to_human(1234567890)
+ assert_equal "1.23 Trillion", number_helper.number_to_human(1234567890123)
+ assert_equal "1.23 Quadrillion", number_helper.number_to_human(1234567890123456)
+ assert_equal "1230 Quadrillion", number_helper.number_to_human(1234567890123456789)
+ assert_equal "490 Thousand", number_helper.number_to_human(489939, precision: 2)
+ assert_equal "489.9 Thousand", number_helper.number_to_human(489939, precision: 4)
+ assert_equal "489 Thousand", number_helper.number_to_human(489000, precision: 4)
+ assert_equal "489.0 Thousand", number_helper.number_to_human(489000, precision: 4, strip_insignificant_zeros: false)
+ assert_equal "1.2346 Million", number_helper.number_to_human(1234567, precision: 4, significant: false)
+ assert_equal "1,2 Million", number_helper.number_to_human(1234567, precision: 1, significant: false, separator: ",")
+ assert_equal "1 Million", number_helper.number_to_human(1234567, precision: 0, significant: true, separator: ",") # significant forced to false
+ assert_equal "1 Million", number_helper.number_to_human(999999)
+ assert_equal "1 Billion", number_helper.number_to_human(999999999)
end
end
def test_number_to_human_with_custom_units
[@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper|
- #Only integers
- volume = {:unit => "ml", :thousand => "lt", :million => "m3"}
- assert_equal '123 lt', number_helper.number_to_human(123456, :units => volume)
- assert_equal '12 ml', number_helper.number_to_human(12, :units => volume)
- assert_equal '1.23 m3', number_helper.number_to_human(1234567, :units => volume)
-
- #Including fractionals
- distance = {:mili => "mm", :centi => "cm", :deci => "dm", :unit => "m", :ten => "dam", :hundred => "hm", :thousand => "km"}
- assert_equal '1.23 mm', number_helper.number_to_human(0.00123, :units => distance)
- assert_equal '1.23 cm', number_helper.number_to_human(0.0123, :units => distance)
- assert_equal '1.23 dm', number_helper.number_to_human(0.123, :units => distance)
- assert_equal '1.23 m', number_helper.number_to_human(1.23, :units => distance)
- assert_equal '1.23 dam', number_helper.number_to_human(12.3, :units => distance)
- assert_equal '1.23 hm', number_helper.number_to_human(123, :units => distance)
- assert_equal '1.23 km', number_helper.number_to_human(1230, :units => distance)
- assert_equal '1.23 km', number_helper.number_to_human(1230, :units => distance)
- assert_equal '1.23 km', number_helper.number_to_human(1230, :units => distance)
- assert_equal '12.3 km', number_helper.number_to_human(12300, :units => distance)
-
- #The quantifiers don't need to be a continuous sequence
- gangster = {:hundred => "hundred bucks", :million => "thousand quids"}
- assert_equal '1 hundred bucks', number_helper.number_to_human(100, :units => gangster)
- assert_equal '25 hundred bucks', number_helper.number_to_human(2500, :units => gangster)
- assert_equal '25 thousand quids', number_helper.number_to_human(25000000, :units => gangster)
- assert_equal '12300 thousand quids', number_helper.number_to_human(12345000000, :units => gangster)
-
- #Spaces are stripped from the resulting string
- assert_equal '4', number_helper.number_to_human(4, :units => {:unit => "", :ten => 'tens '})
- assert_equal '4.5 tens', number_helper.number_to_human(45, :units => {:unit => "", :ten => ' tens '})
+ # Only integers
+ volume = { unit: "ml", thousand: "lt", million: "m3" }
+ assert_equal "123 lt", number_helper.number_to_human(123456, units: volume)
+ assert_equal "12 ml", number_helper.number_to_human(12, units: volume)
+ assert_equal "1.23 m3", number_helper.number_to_human(1234567, units: volume)
+
+ # Including fractionals
+ distance = { mili: "mm", centi: "cm", deci: "dm", unit: "m", ten: "dam", hundred: "hm", thousand: "km" }
+ assert_equal "1.23 mm", number_helper.number_to_human(0.00123, units: distance)
+ assert_equal "1.23 cm", number_helper.number_to_human(0.0123, units: distance)
+ assert_equal "1.23 dm", number_helper.number_to_human(0.123, units: distance)
+ assert_equal "1.23 m", number_helper.number_to_human(1.23, units: distance)
+ assert_equal "1.23 dam", number_helper.number_to_human(12.3, units: distance)
+ assert_equal "1.23 hm", number_helper.number_to_human(123, units: distance)
+ assert_equal "1.23 km", number_helper.number_to_human(1230, units: distance)
+ assert_equal "1.23 km", number_helper.number_to_human(1230, units: distance)
+ assert_equal "1.23 km", number_helper.number_to_human(1230, units: distance)
+ assert_equal "12.3 km", number_helper.number_to_human(12300, units: distance)
+
+ # The quantifiers don't need to be a continuous sequence
+ gangster = { hundred: "hundred bucks", million: "thousand quids" }
+ assert_equal "1 hundred bucks", number_helper.number_to_human(100, units: gangster)
+ assert_equal "25 hundred bucks", number_helper.number_to_human(2500, units: gangster)
+ assert_equal "1000 hundred bucks", number_helper.number_to_human(100_000, units: gangster)
+ assert_equal "1 thousand quids", number_helper.number_to_human(999_999, units: gangster)
+ assert_equal "1 thousand quids", number_helper.number_to_human(1_000_000, units: gangster)
+ assert_equal "25 thousand quids", number_helper.number_to_human(25000000, units: gangster)
+ assert_equal "12300 thousand quids", number_helper.number_to_human(12345000000, units: gangster)
+
+ # Spaces are stripped from the resulting string
+ assert_equal "4", number_helper.number_to_human(4, units: { unit: "", ten: "tens " })
+ assert_equal "4.5 tens", number_helper.number_to_human(45, units: { unit: "", ten: " tens " })
+
+ # Uses only the provided units and does not try to use larger ones
+ assert_equal "1000 kilometers", number_helper.number_to_human(1_000_000, units: { unit: "meter", thousand: "kilometers" })
end
end
def test_number_to_human_with_custom_units_that_are_missing_the_needed_key
[@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper|
- assert_equal '123', number_helper.number_to_human(123, units: { thousand: 'k'})
- assert_equal '123', number_helper.number_to_human(123, units: {})
+ assert_equal "123", number_helper.number_to_human(123, units: { thousand: "k" })
+ assert_equal "123", number_helper.number_to_human(123, units: {})
end
end
def test_number_to_human_with_custom_format
[@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper|
- assert_equal '123 times Thousand', number_helper.number_to_human(123456, :format => "%n times %u")
- volume = {:unit => "ml", :thousand => "lt", :million => "m3"}
- assert_equal '123.lt', number_helper.number_to_human(123456, :units => volume, :format => "%n.%u")
+ assert_equal "123 times Thousand", number_helper.number_to_human(123456, format: "%n times %u")
+ volume = { unit: "ml", thousand: "lt", million: "m3" }
+ assert_equal "123.lt", number_helper.number_to_human(123456, units: volume, format: "%n.%u")
end
end
@@ -359,34 +367,34 @@ module ActiveSupport
def test_number_helpers_do_not_mutate_options_hash
[@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper|
- options = { 'raise' => true }
+ options = { "raise" => true }
number_helper.number_to_phone(1, options)
- assert_equal({ 'raise' => true }, options)
+ assert_equal({ "raise" => true }, options)
number_helper.number_to_currency(1, options)
- assert_equal({ 'raise' => true }, options)
+ assert_equal({ "raise" => true }, options)
number_helper.number_to_percentage(1, options)
- assert_equal({ 'raise' => true }, options)
+ assert_equal({ "raise" => true }, options)
number_helper.number_to_delimited(1, options)
- assert_equal({ 'raise' => true }, options)
+ assert_equal({ "raise" => true }, options)
number_helper.number_to_rounded(1, options)
- assert_equal({ 'raise' => true }, options)
+ assert_equal({ "raise" => true }, options)
number_helper.number_to_human_size(1, options)
- assert_equal({ 'raise' => true }, options)
+ assert_equal({ "raise" => true }, options)
number_helper.number_to_human(1, options)
- assert_equal({ 'raise' => true }, options)
+ assert_equal({ "raise" => true }, options)
end
end
def test_number_helpers_should_return_non_numeric_param_unchanged
[@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper|
- assert_equal("+1-x x 123", number_helper.number_to_phone("x", :country_code => 1, :extension => 123))
+ assert_equal("+1-x x 123", number_helper.number_to_phone("x", country_code: 1, extension: 123))
assert_equal("x", number_helper.number_to_phone("x"))
assert_equal("$x.", number_helper.number_to_currency("x."))
assert_equal("$x", number_helper.number_to_currency("x"))
@@ -394,11 +402,10 @@ module ActiveSupport
assert_equal("x", number_helper.number_to_delimited("x"))
assert_equal("x.", number_helper.number_to_rounded("x."))
assert_equal("x", number_helper.number_to_rounded("x"))
- assert_equal "x", number_helper.number_to_human_size('x')
- assert_equal "x", number_helper.number_to_human('x')
+ assert_equal "x", number_helper.number_to_human_size("x")
+ assert_equal "x", number_helper.number_to_human("x")
end
end
-
end
end
end
diff --git a/activesupport/test/option_merger_test.rb b/activesupport/test/option_merger_test.rb
index 4c0364e68b..935e2aee63 100644
--- a/activesupport/test/option_merger_test.rb
+++ b/activesupport/test/option_merger_test.rb
@@ -1,13 +1,15 @@
-require 'abstract_unit'
-require 'active_support/core_ext/object/with_options'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/object/with_options"
class OptionMergerTest < ActiveSupport::TestCase
def setup
- @options = {:hello => 'world'}
+ @options = { hello: "world" }
end
def test_method_with_options_merges_options_when_options_are_present
- local_options = {:cool => true}
+ local_options = { cool: true }
with_options(@options) do |o|
assert_equal local_options, method_with_options(local_options)
@@ -24,7 +26,7 @@ class OptionMergerTest < ActiveSupport::TestCase
end
def test_method_with_options_allows_to_overwrite_options
- local_options = {:hello => 'moon'}
+ local_options = { hello: "moon" }
assert_equal @options.keys, local_options.keys
with_options(@options) do |o|
@@ -40,34 +42,34 @@ class OptionMergerTest < ActiveSupport::TestCase
end
def test_nested_method_with_options_containing_hashes_merge
- with_options :conditions => { :method => :get } do |outer|
- outer.with_options :conditions => { :domain => "www" } do |inner|
- expected = { :conditions => { :method => :get, :domain => "www" } }
+ with_options conditions: { method: :get } do |outer|
+ outer.with_options conditions: { domain: "www" } do |inner|
+ expected = { conditions: { method: :get, domain: "www" } }
assert_equal expected, inner.method_with_options
end
end
end
def test_nested_method_with_options_containing_hashes_overwrite
- with_options :conditions => { :method => :get, :domain => "www" } do |outer|
- outer.with_options :conditions => { :method => :post } do |inner|
- expected = { :conditions => { :method => :post, :domain => "www" } }
+ with_options conditions: { method: :get, domain: "www" } do |outer|
+ outer.with_options conditions: { method: :post } do |inner|
+ expected = { conditions: { method: :post, domain: "www" } }
assert_equal expected, inner.method_with_options
end
end
end
def test_nested_method_with_options_containing_hashes_going_deep
- with_options :html => { :class => "foo", :style => { :margin => 0, :display => "block" } } do |outer|
- outer.with_options :html => { :title => "bar", :style => { :margin => "1em", :color => "#fff" } } do |inner|
- expected = { :html => { :class => "foo", :title => "bar", :style => { :margin => "1em", :display => "block", :color => "#fff" } } }
+ with_options html: { class: "foo", style: { margin: 0, display: "block" } } do |outer|
+ outer.with_options html: { title: "bar", style: { margin: "1em", color: "#fff" } } do |inner|
+ expected = { html: { class: "foo", title: "bar", style: { margin: "1em", display: "block", color: "#fff" } } }
assert_equal expected, inner.method_with_options
end
end
end
def test_nested_method_with_options_using_lambda
- local_lambda = lambda { { :lambda => true } }
+ local_lambda = lambda { { lambda: true } }
with_options(@options) do |o|
assert_equal @options.merge(local_lambda.call),
o.method_with_options(local_lambda).call
@@ -76,7 +78,7 @@ class OptionMergerTest < ActiveSupport::TestCase
# Needed when counting objects with the ObjectSpace
def test_option_merger_class_method
- assert_equal ActiveSupport::OptionMerger, ActiveSupport::OptionMerger.new('', '').class
+ assert_equal ActiveSupport::OptionMerger, ActiveSupport::OptionMerger.new("", "").class
end
def test_option_merger_implicit_receiver
diff --git a/activesupport/test/ordered_hash_test.rb b/activesupport/test/ordered_hash_test.rb
index 460a61613e..c70d3b4c37 100644
--- a/activesupport/test/ordered_hash_test.rb
+++ b/activesupport/test/ordered_hash_test.rb
@@ -1,8 +1,10 @@
-require 'abstract_unit'
-require 'active_support/json'
-require 'active_support/core_ext/object/json'
-require 'active_support/core_ext/hash/indifferent_access'
-require 'active_support/core_ext/array/extract_options'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/json"
+require "active_support/core_ext/object/json"
+require "active_support/core_ext/hash/indifferent_access"
+require "active_support/core_ext/array/extract_options"
class OrderedHashTest < ActiveSupport::TestCase
def setup
@@ -27,7 +29,7 @@ class OrderedHashTest < ActiveSupport::TestCase
end
def test_assignment
- key, value = 'purple', '5422a8'
+ key, value = "purple", "5422a8"
@ordered_hash[key] = value
assert_equal @keys.length + 1, @ordered_hash.length
@@ -37,8 +39,8 @@ class OrderedHashTest < ActiveSupport::TestCase
end
def test_delete
- key, value = 'white', 'ffffff'
- bad_key = 'black'
+ key, value = "white", "ffffff"
+ bad_key = "black"
@ordered_hash[key] = value
assert_equal @keys.length + 1, @ordered_hash.length
@@ -60,22 +62,22 @@ class OrderedHashTest < ActiveSupport::TestCase
end
def test_has_key
- assert_equal true, @ordered_hash.has_key?('blue')
- assert_equal true, @ordered_hash.key?('blue')
- assert_equal true, @ordered_hash.include?('blue')
- assert_equal true, @ordered_hash.member?('blue')
+ assert_equal true, @ordered_hash.has_key?("blue")
+ assert_equal true, @ordered_hash.key?("blue")
+ assert_equal true, @ordered_hash.include?("blue")
+ assert_equal true, @ordered_hash.member?("blue")
- assert_equal false, @ordered_hash.has_key?('indigo')
- assert_equal false, @ordered_hash.key?('indigo')
- assert_equal false, @ordered_hash.include?('indigo')
- assert_equal false, @ordered_hash.member?('indigo')
+ assert_equal false, @ordered_hash.has_key?("indigo")
+ assert_equal false, @ordered_hash.key?("indigo")
+ assert_equal false, @ordered_hash.include?("indigo")
+ assert_equal false, @ordered_hash.member?("indigo")
end
def test_has_value
- assert_equal true, @ordered_hash.has_value?('000099')
- assert_equal true, @ordered_hash.value?('000099')
- assert_equal false, @ordered_hash.has_value?('ABCABC')
- assert_equal false, @ordered_hash.value?('ABCABC')
+ assert_equal true, @ordered_hash.has_value?("000099")
+ assert_equal true, @ordered_hash.value?("000099")
+ assert_equal false, @ordered_hash.has_value?("ABCABC")
+ assert_equal false, @ordered_hash.value?("ABCABC")
end
def test_each_key
@@ -94,13 +96,13 @@ class OrderedHashTest < ActiveSupport::TestCase
def test_each
values = []
- assert_equal @ordered_hash, @ordered_hash.each {|key, value| values << value}
+ assert_equal @ordered_hash, @ordered_hash.each { |key, value| values << value }
assert_equal @values, values
assert_kind_of Enumerator, @ordered_hash.each
end
def test_each_with_index
- @ordered_hash.each_with_index { |pair, index| assert_equal [@keys[index], @values[index]], pair}
+ @ordered_hash.each_with_index { |pair, index| assert_equal [@keys[index], @values[index]], pair }
end
def test_each_pair
@@ -127,24 +129,24 @@ class OrderedHashTest < ActiveSupport::TestCase
def test_delete_if
copy = @ordered_hash.dup
- copy.delete('pink')
- assert_equal copy, @ordered_hash.delete_if { |k, _| k == 'pink' }
- assert !@ordered_hash.keys.include?('pink')
+ copy.delete("pink")
+ assert_equal copy, @ordered_hash.delete_if { |k, _| k == "pink" }
+ assert_not_includes @ordered_hash.keys, "pink"
end
def test_reject!
- (copy = @ordered_hash.dup).delete('pink')
- @ordered_hash.reject! { |k, _| k == 'pink' }
+ (copy = @ordered_hash.dup).delete("pink")
+ @ordered_hash.reject! { |k, _| k == "pink" }
assert_equal copy, @ordered_hash
- assert !@ordered_hash.keys.include?('pink')
+ assert_not_includes @ordered_hash.keys, "pink"
end
def test_reject
copy = @ordered_hash.dup
- new_ordered_hash = @ordered_hash.reject { |k, _| k == 'pink' }
+ new_ordered_hash = @ordered_hash.reject { |k, _| k == "pink" }
assert_equal copy, @ordered_hash
- assert !new_ordered_hash.keys.include?('pink')
- assert @ordered_hash.keys.include?('pink')
+ assert_not_includes new_ordered_hash.keys, "pink"
+ assert_includes @ordered_hash.keys, "pink"
assert_instance_of ActiveSupport::OrderedHash, new_ordered_hash
end
@@ -154,19 +156,19 @@ class OrderedHashTest < ActiveSupport::TestCase
end
def test_merge
- other_hash = ActiveSupport::OrderedHash.new
- other_hash['purple'] = '800080'
- other_hash['violet'] = 'ee82ee'
+ other_hash = ActiveSupport::OrderedHash.new
+ other_hash["purple"] = "800080"
+ other_hash["violet"] = "ee82ee"
merged = @ordered_hash.merge other_hash
assert_equal merged.length, @ordered_hash.length + other_hash.length
- assert_equal @keys + ['purple', 'violet'], merged.keys
+ assert_equal @keys + ["purple", "violet"], merged.keys
end
def test_merge_with_block
hash = ActiveSupport::OrderedHash.new
hash[:a] = 0
hash[:b] = 0
- merged = hash.merge(:b => 2, :c => 7) do |key, old_value, new_value|
+ merged = hash.merge(b: 2, c: 7) do |key, old_value, new_value|
new_value + 1
end
@@ -179,7 +181,7 @@ class OrderedHashTest < ActiveSupport::TestCase
hash = ActiveSupport::OrderedHash.new
hash[:a] = 0
hash[:b] = 0
- hash.merge!(:a => 1, :c => 7) do |key, old_value, new_value|
+ hash.merge!(a: 1, c: 7) do |key, old_value, new_value|
new_value + 3
end
@@ -191,7 +193,7 @@ class OrderedHashTest < ActiveSupport::TestCase
def test_shift
pair = @ordered_hash.shift
assert_equal [@keys.first, @values.first], pair
- assert !@ordered_hash.keys.include?(pair.first)
+ assert_not_includes @ordered_hash.keys, pair.first
end
def test_keys
@@ -201,7 +203,7 @@ class OrderedHashTest < ActiveSupport::TestCase
end
def test_inspect
- assert @ordered_hash.inspect.include?(@hash.inspect)
+ assert_includes @ordered_hash.inspect, @hash.inspect
end
def test_json
@@ -211,7 +213,7 @@ class OrderedHashTest < ActiveSupport::TestCase
end
def test_alternate_initialization_with_splat
- alternate = ActiveSupport::OrderedHash[1,2,3,4]
+ alternate = ActiveSupport::OrderedHash[1, 2, 3, 4]
assert_kind_of ActiveSupport::OrderedHash, alternate
assert_equal [1, 3], alternate.keys
end
@@ -220,29 +222,29 @@ class OrderedHashTest < ActiveSupport::TestCase
alternate = ActiveSupport::OrderedHash[ [
[1, 2],
[3, 4],
- [ 'missing value' ]
+ [ "missing value" ]
]]
assert_kind_of ActiveSupport::OrderedHash, alternate
- assert_equal [1, 3, 'missing value'], alternate.keys
+ assert_equal [1, 3, "missing value"], alternate.keys
assert_equal [2, 4, nil ], alternate.values
end
def test_alternate_initialization_raises_exception_on_odd_length_args
assert_raises ArgumentError do
- ActiveSupport::OrderedHash[1,2,3,4,5]
+ ActiveSupport::OrderedHash[1, 2, 3, 4, 5]
end
end
def test_replace_updates_keys
- @other_ordered_hash = ActiveSupport::OrderedHash[:black, '000000', :white, '000000']
+ @other_ordered_hash = ActiveSupport::OrderedHash[:black, "000000", :white, "000000"]
original = @ordered_hash.replace(@other_ordered_hash)
assert_same original, @ordered_hash
assert_equal @other_ordered_hash.keys, @ordered_hash.keys
end
def test_nested_under_indifferent_access
- flash = {:a => ActiveSupport::OrderedHash[:b, 1, :c, 2]}.with_indifferent_access
+ flash = { a: ActiveSupport::OrderedHash[:b, 1, :c, 2] }.with_indifferent_access
assert_kind_of ActiveSupport::OrderedHash, flash[:a]
end
@@ -295,17 +297,17 @@ class OrderedHashTest < ActiveSupport::TestCase
def test_psych_serialize_tag
yaml = Psych.dump(@ordered_hash)
- assert_match '!omap', yaml
+ assert_match "!omap", yaml
end
def test_has_yaml_tag
@ordered_hash[:array] = %w(a b c)
- assert_match '!omap', YAML.dump(@ordered_hash)
+ assert_match "!omap", YAML.dump(@ordered_hash)
end
def test_update_sets_keys
@updated_ordered_hash = ActiveSupport::OrderedHash.new
- @updated_ordered_hash.update(:name => "Bob")
+ @updated_ordered_hash.update(name: "Bob")
assert_equal [:name], @updated_ordered_hash.keys
end
diff --git a/activesupport/test/ordered_options_test.rb b/activesupport/test/ordered_options_test.rb
index 18767a3536..90394fee0a 100644
--- a/activesupport/test/ordered_options_test.rb
+++ b/activesupport/test/ordered_options_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'active_support/ordered_options'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/ordered_options"
class OrderedOptionsTest < ActiveSupport::TestCase
def test_usage
@@ -13,7 +15,7 @@ class OrderedOptionsTest < ActiveSupport::TestCase
a[:allow_concurrency] = false
assert_equal 1, a.size
- assert !a[:allow_concurrency]
+ assert_not a[:allow_concurrency]
a["else_where"] = 56
assert_equal 2, a.size
@@ -45,7 +47,7 @@ class OrderedOptionsTest < ActiveSupport::TestCase
a.allow_concurrency = false
assert_equal 1, a.size
- assert !a.allow_concurrency
+ assert_not a.allow_concurrency
a.else_where = 56
assert_equal 2, a.size
@@ -80,8 +82,8 @@ class OrderedOptionsTest < ActiveSupport::TestCase
def test_introspection
a = ActiveSupport::OrderedOptions.new
- assert a.respond_to?(:blah)
- assert a.respond_to?(:blah=)
+ assert_respond_to a, :blah
+ assert_respond_to a, :blah=
assert_equal 42, a.method(:blah=).call(42)
assert_equal 42, a.method(:blah).call
end
@@ -89,7 +91,20 @@ class OrderedOptionsTest < ActiveSupport::TestCase
def test_raises_with_bang
a = ActiveSupport::OrderedOptions.new
a[:foo] = :bar
- assert a.respond_to?(:foo!)
+ assert_respond_to a, :foo!
+
+ assert_nothing_raised { a.foo! }
+ assert_equal a.foo, a.foo!
+
+ assert_raises(KeyError) do
+ a.foo = nil
+ a.foo!
+ end
+ assert_raises(KeyError) { a.non_existing_key! }
+ end
+
+ def test_inheritable_options_with_bang
+ a = ActiveSupport::InheritableOptions.new(foo: :bar)
assert_nothing_raised { a.foo! }
assert_equal a.foo, a.foo!
diff --git a/activesupport/test/parameter_filter_test.rb b/activesupport/test/parameter_filter_test.rb
new file mode 100644
index 0000000000..d2dc71061d
--- /dev/null
+++ b/activesupport/test/parameter_filter_test.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/hash"
+require "active_support/parameter_filter"
+
+class ParameterFilterTest < ActiveSupport::TestCase
+ test "process parameter filter" do
+ test_hashes = [
+ [{ "foo" => "bar" }, { "foo" => "bar" }, %w'food'],
+ [{ "foo" => "bar" }, { "foo" => "[FILTERED]" }, %w'foo'],
+ [{ "foo" => "bar", "bar" => "foo" }, { "foo" => "[FILTERED]", "bar" => "foo" }, %w'foo baz'],
+ [{ "foo" => "bar", "baz" => "foo" }, { "foo" => "[FILTERED]", "baz" => "[FILTERED]" }, %w'foo baz'],
+ [{ "bar" => { "foo" => "bar", "bar" => "foo" } }, { "bar" => { "foo" => "[FILTERED]", "bar" => "foo" } }, %w'fo'],
+ [{ "foo" => { "foo" => "bar", "bar" => "foo" } }, { "foo" => "[FILTERED]" }, %w'f banana'],
+ [{ "deep" => { "cc" => { "code" => "bar", "bar" => "foo" }, "ss" => { "code" => "bar" } } }, { "deep" => { "cc" => { "code" => "[FILTERED]", "bar" => "foo" }, "ss" => { "code" => "bar" } } }, %w'deep.cc.code'],
+ [{ "baz" => [{ "foo" => "baz" }, "1"] }, { "baz" => [{ "foo" => "[FILTERED]" }, "1"] }, [/foo/]]]
+
+ test_hashes.each do |before_filter, after_filter, filter_words|
+ parameter_filter = ActiveSupport::ParameterFilter.new(filter_words)
+ assert_equal after_filter, parameter_filter.filter(before_filter)
+
+ filter_words << "blah"
+ filter_words << lambda { |key, value|
+ value.reverse! if key =~ /bargain/
+ }
+ filter_words << lambda { |key, value, original_params|
+ value.replace("world!") if original_params["barg"]["blah"] == "bar" && key == "hello"
+ }
+
+ parameter_filter = ActiveSupport::ParameterFilter.new(filter_words)
+ before_filter["barg"] = { :bargain => "gain", "blah" => "bar", "bar" => { "bargain" => { "blah" => "foo", "hello" => "world" } } }
+ after_filter["barg"] = { :bargain => "niag", "blah" => "[FILTERED]", "bar" => { "bargain" => { "blah" => "[FILTERED]", "hello" => "world!" } } }
+
+ assert_equal after_filter, parameter_filter.filter(before_filter)
+ end
+ end
+
+ test "filter should return mask option when value is filtered" do
+ mask = Object.new.freeze
+ test_hashes = [
+ [{ "foo" => "bar" }, { "foo" => "bar" }, %w'food'],
+ [{ "foo" => "bar" }, { "foo" => mask }, %w'foo'],
+ [{ "foo" => "bar", "bar" => "foo" }, { "foo" => mask, "bar" => "foo" }, %w'foo baz'],
+ [{ "foo" => "bar", "baz" => "foo" }, { "foo" => mask, "baz" => mask }, %w'foo baz'],
+ [{ "bar" => { "foo" => "bar", "bar" => "foo" } }, { "bar" => { "foo" => mask, "bar" => "foo" } }, %w'fo'],
+ [{ "foo" => { "foo" => "bar", "bar" => "foo" } }, { "foo" => mask }, %w'f banana'],
+ [{ "deep" => { "cc" => { "code" => "bar", "bar" => "foo" }, "ss" => { "code" => "bar" } } }, { "deep" => { "cc" => { "code" => mask, "bar" => "foo" }, "ss" => { "code" => "bar" } } }, %w'deep.cc.code'],
+ [{ "baz" => [{ "foo" => "baz" }, "1"] }, { "baz" => [{ "foo" => mask }, "1"] }, [/foo/]]]
+
+ test_hashes.each do |before_filter, after_filter, filter_words|
+ parameter_filter = ActiveSupport::ParameterFilter.new(filter_words, mask: mask)
+ assert_equal after_filter, parameter_filter.filter(before_filter)
+
+ filter_words << "blah"
+ filter_words << lambda { |key, value|
+ value.reverse! if key =~ /bargain/
+ }
+ filter_words << lambda { |key, value, original_params|
+ value.replace("world!") if original_params["barg"]["blah"] == "bar" && key == "hello"
+ }
+
+ parameter_filter = ActiveSupport::ParameterFilter.new(filter_words, mask: mask)
+ before_filter["barg"] = { :bargain => "gain", "blah" => "bar", "bar" => { "bargain" => { "blah" => "foo", "hello" => "world" } } }
+ after_filter["barg"] = { :bargain => "niag", "blah" => mask, "bar" => { "bargain" => { "blah" => mask, "hello" => "world!" } } }
+
+ assert_equal after_filter, parameter_filter.filter(before_filter)
+ end
+ end
+
+ test "filter_param" do
+ parameter_filter = ActiveSupport::ParameterFilter.new(["foo", /bar/])
+ assert_equal "[FILTERED]", parameter_filter.filter_param("food", "secret vlaue")
+ assert_equal "[FILTERED]", parameter_filter.filter_param("baz.foo", "secret vlaue")
+ assert_equal "[FILTERED]", parameter_filter.filter_param("barbar", "secret vlaue")
+ assert_equal "non secret value", parameter_filter.filter_param("baz", "non secret value")
+ end
+
+ test "filter_param can work with empty filters" do
+ parameter_filter = ActiveSupport::ParameterFilter.new
+ assert_equal "bar", parameter_filter.filter_param("foo", "bar")
+ end
+
+ test "parameter filter should maintain hash with indifferent access" do
+ test_hashes = [
+ [{ "foo" => "bar" }.with_indifferent_access, ["blah"]],
+ [{ "foo" => "bar" }.with_indifferent_access, []]
+ ]
+
+ test_hashes.each do |before_filter, filter_words|
+ parameter_filter = ActiveSupport::ParameterFilter.new(filter_words)
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess,
+ parameter_filter.filter(before_filter)
+ end
+ end
+
+ test "filter_param should return mask option when value is filtered" do
+ mask = Object.new.freeze
+ parameter_filter = ActiveSupport::ParameterFilter.new(["foo", /bar/], mask: mask)
+ assert_equal mask, parameter_filter.filter_param("food", "secret vlaue")
+ assert_equal mask, parameter_filter.filter_param("baz.foo", "secret vlaue")
+ assert_equal mask, parameter_filter.filter_param("barbar", "secret vlaue")
+ assert_equal "non secret value", parameter_filter.filter_param("baz", "non secret value")
+ end
+end
diff --git a/activesupport/test/reloader_test.rb b/activesupport/test/reloader_test.rb
new file mode 100644
index 0000000000..1b7cc253d9
--- /dev/null
+++ b/activesupport/test/reloader_test.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class ReloaderTest < ActiveSupport::TestCase
+ def test_prepare_callback
+ prepared = completed = false
+ reloader.to_prepare { prepared = true }
+ reloader.to_complete { completed = true }
+
+ assert_not prepared
+ assert_not completed
+ reloader.prepare!
+ assert prepared
+ assert_not completed
+
+ prepared = false
+ reloader.wrap do
+ assert prepared
+ prepared = false
+ end
+ assert_not 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
+ r.to_run { invoked = true }
+ r.wrap { }
+ assert invoked
+
+ r = new_reloader { false }
+ invoked = false
+ r.to_run { invoked = true }
+ r.wrap { }
+ assert_not invoked
+ end
+
+ def test_full_reload_sequence
+ called = []
+ reloader.to_prepare { called << :prepare }
+ reloader.to_run { called << :reloader_run }
+ reloader.to_complete { called << :reloader_complete }
+ reloader.executor.to_run { called << :executor_run }
+ reloader.executor.to_complete { called << :executor_complete }
+
+ reloader.wrap { }
+ assert_equal [:executor_run, :reloader_run, :prepare, :reloader_complete, :executor_complete], called
+
+ called = []
+ reloader.reload!
+ assert_equal [:executor_run, :reloader_run, :prepare, :reloader_complete, :executor_complete, :prepare], called
+
+ reloader.check = lambda { false }
+
+ called = []
+ reloader.wrap { }
+ assert_equal [:executor_run, :executor_complete], called
+
+ called = []
+ reloader.reload!
+ assert_equal [:executor_run, :reloader_run, :prepare, :reloader_complete, :executor_complete, :prepare], called
+ end
+
+ def test_class_unload_block
+ called = []
+ reloader.before_class_unload { called << :before_unload }
+ reloader.after_class_unload { called << :after_unload }
+ reloader.to_run do
+ class_unload! do
+ called << :unload
+ end
+ end
+ reloader.wrap { called << :body }
+
+ assert_equal [:before_unload, :unload, :after_unload, :body], called
+ end
+
+ private
+ def new_reloader(&check)
+ Class.new(ActiveSupport::Reloader).tap do |r|
+ r.check = check
+ r.executor = Class.new(ActiveSupport::Executor)
+ end
+ end
+
+ def reloader
+ @reloader ||= new_reloader { true }
+ end
+end
diff --git a/activesupport/test/rescuable_test.rb b/activesupport/test/rescuable_test.rb
index bd43ad0797..b1b8a25c5b 100644
--- a/activesupport/test/rescuable_test.rb
+++ b/activesupport/test/rescuable_test.rb
@@ -1,9 +1,8 @@
-require 'abstract_unit'
+# frozen_string_literal: true
-class WraithAttack < StandardError
-end
+require "abstract_unit"
-class NuclearExplosion < StandardError
+class WraithAttack < StandardError
end
class MadRonon < StandardError
@@ -19,16 +18,20 @@ module WeirdError
end
class Stargate
+ # Nest this so the 'NuclearExplosion' handler needs a lexical const_get
+ # to find it.
+ class NuclearExplosion < StandardError; end
+
attr_accessor :result
include ActiveSupport::Rescuable
- rescue_from WraithAttack, :with => :sos_first
+ rescue_from WraithAttack, with: :sos_first
- rescue_from WraithAttack, :with => :sos
+ rescue_from WraithAttack, with: :sos
- rescue_from 'NuclearExplosion' do
- @result = 'alldead'
+ rescue_from "NuclearExplosion" do
+ @result = "alldead"
end
rescue_from MadRonon do |e|
@@ -36,13 +39,15 @@ class Stargate
end
rescue_from WeirdError do
- @result = 'weird'
+ @result = "weird"
end
def dispatch(method)
send(method)
rescue Exception => e
- rescue_with_handler(e)
+ unless rescue_with_handler(e)
+ @result = "unhandled"
+ end
end
def attack
@@ -57,6 +62,34 @@ class Stargate
raise MadRonon.new("dex")
end
+ def crash
+ raise "unhandled RuntimeError"
+ end
+
+ def looped_crash
+ ex1 = StandardError.new("error 1")
+ ex2 = StandardError.new("error 2")
+ begin
+ begin
+ raise ex1
+ rescue
+ # sets the cause on ex2 to be ex1
+ raise ex2
+ end
+ rescue
+ # sets the cause on ex1 to be ex2
+ raise ex1
+ end
+ end
+
+ def fall_back_to_cause
+ # This exception is the cause and has a handler.
+ ronanize
+ rescue
+ # This is the exception we'll handle that doesn't have a cause.
+ raise "unhandled RuntimeError with a handleable cause"
+ end
+
def weird
StandardError.new.tap do |exc|
def exc.weird?
@@ -68,13 +101,12 @@ class Stargate
end
def sos
- @result = 'killed'
+ @result = "killed"
end
def sos_first
- @result = 'sos_first'
+ @result = "sos_first"
end
-
end
class CoolStargate < Stargate
@@ -82,14 +114,13 @@ class CoolStargate < Stargate
include ActiveSupport::Rescuable
- rescue_from CoolError, :with => :sos_cool_error
+ rescue_from CoolError, with: :sos_cool_error
def sos_cool_error
- @result = 'sos_cool_error'
+ @result = "sos_cool_error"
end
end
-
class RescuableTest < ActiveSupport::TestCase
def setup
@stargate = Stargate.new
@@ -98,22 +129,22 @@ class RescuableTest < ActiveSupport::TestCase
def test_rescue_from_with_method
@stargate.dispatch :attack
- assert_equal 'killed', @stargate.result
+ assert_equal "killed", @stargate.result
end
def test_rescue_from_with_block
@stargate.dispatch :nuke
- assert_equal 'alldead', @stargate.result
+ assert_equal "alldead", @stargate.result
end
def test_rescue_from_with_block_with_args
@stargate.dispatch :ronanize
- assert_equal 'dex', @stargate.result
+ assert_equal "dex", @stargate.result
end
def test_rescue_from_error_dispatchers_with_case_operator
@stargate.dispatch :weird
- assert_equal 'weird', @stargate.result
+ assert_equal "weird", @stargate.result
end
def test_rescues_defined_later_are_added_at_end_of_the_rescue_handlers_array
@@ -127,4 +158,19 @@ class RescuableTest < ActiveSupport::TestCase
result = @cool_stargate.send(:rescue_handlers).collect(&:first)
assert_equal expected, result
end
+
+ def test_rescue_falls_back_to_exception_cause
+ @stargate.dispatch :fall_back_to_cause
+ assert_equal "dex", @stargate.result
+ end
+
+ def test_unhandled_exceptions
+ @stargate.dispatch(:crash)
+ assert_equal "unhandled", @stargate.result
+ end
+
+ def test_rescue_handles_loops_in_exception_cause_chain
+ @stargate.dispatch :looped_crash
+ assert_equal "unhandled", @stargate.result
+ end
end
diff --git a/activesupport/test/safe_buffer_test.rb b/activesupport/test/safe_buffer_test.rb
index 18fb6d2fbf..49a3951623 100644
--- a/activesupport/test/safe_buffer_test.rb
+++ b/activesupport/test/safe_buffer_test.rb
@@ -1,6 +1,8 @@
-require 'abstract_unit'
-require 'active_support/core_ext/string/inflections'
-require 'yaml'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/string/inflections"
+require "yaml"
class SafeBufferTest < ActiveSupport::TestCase
def setup
@@ -8,7 +10,7 @@ class SafeBufferTest < ActiveSupport::TestCase
end
def test_titleize
- assert_equal 'Foo', "foo".html_safe.titleize
+ assert_equal "Foo", "foo".html_safe.titleize
end
test "Should look like a string" do
@@ -37,7 +39,7 @@ class SafeBufferTest < ActiveSupport::TestCase
end
test "Should be considered safe" do
- assert @buffer.html_safe?
+ assert_predicate @buffer, :html_safe?
end
test "Should return a safe buffer when calling to_s" do
@@ -46,26 +48,26 @@ class SafeBufferTest < ActiveSupport::TestCase
end
test "Should be converted to_yaml" do
- str = 'hello!'
+ str = "hello!"
buf = ActiveSupport::SafeBuffer.new str
yaml = buf.to_yaml
assert_match(/^--- #{str}/, yaml)
- assert_equal 'hello!', YAML.load(yaml)
+ assert_equal "hello!", YAML.load(yaml)
end
test "Should work in nested to_yaml conversion" do
- str = 'hello!'
- data = { 'str' => ActiveSupport::SafeBuffer.new(str) }
+ str = "hello!"
+ data = { "str" => ActiveSupport::SafeBuffer.new(str) }
yaml = YAML.dump data
- assert_equal({'str' => str}, YAML.load(yaml))
+ assert_equal({ "str" => str }, YAML.load(yaml))
end
test "Should work with primitive-like-strings in to_yaml conversion" do
- assert_equal 'true', YAML.load(ActiveSupport::SafeBuffer.new('true').to_yaml)
- assert_equal 'false', YAML.load(ActiveSupport::SafeBuffer.new('false').to_yaml)
- assert_equal '1', YAML.load(ActiveSupport::SafeBuffer.new('1').to_yaml)
- assert_equal '1.1', YAML.load(ActiveSupport::SafeBuffer.new('1.1').to_yaml)
+ assert_equal "true", YAML.load(ActiveSupport::SafeBuffer.new("true").to_yaml)
+ assert_equal "false", YAML.load(ActiveSupport::SafeBuffer.new("false").to_yaml)
+ assert_equal "1", YAML.load(ActiveSupport::SafeBuffer.new("1").to_yaml)
+ assert_equal "1.1", YAML.load(ActiveSupport::SafeBuffer.new("1.1").to_yaml)
end
test "Should work with underscore" do
@@ -73,44 +75,69 @@ class SafeBufferTest < ActiveSupport::TestCase
assert_equal "my_test", str
end
- test "Should not return safe buffer from gsub" do
- altered_buffer = @buffer.gsub('', 'asdf')
- assert_equal 'asdf', altered_buffer
- assert !altered_buffer.html_safe?
- end
+ {
+ capitalize: nil,
+ chomp: nil,
+ chop: nil,
+ delete: "foo",
+ delete_prefix: "foo",
+ delete_suffix: "foo",
+ downcase: nil,
+ gsub: ["foo", "bar"],
+ lstrip: nil,
+ next: nil,
+ reverse: nil,
+ rstrip: nil,
+ slice: "foo",
+ squeeze: nil,
+ strip: nil,
+ sub: ["foo", "bar"],
+ succ: nil,
+ swapcase: nil,
+ tr: ["foo", "bar"],
+ tr_s: ["foo", "bar"],
+ unicode_normalize: nil,
+ upcase: nil,
+ }.each do |unsafe_method, dummy_args|
+ test "Should not return safe buffer from #{unsafe_method}" do
+ skip unless String.method_defined?(unsafe_method)
+ altered_buffer = @buffer.send(unsafe_method, *dummy_args)
+ assert_not_predicate altered_buffer, :html_safe?
+ end
- test "Should not return safe buffer from gsub!" do
- @buffer.gsub!('', 'asdf')
- assert_equal 'asdf', @buffer
- assert !@buffer.html_safe?
+ test "Should not return safe buffer from #{unsafe_method}!" do
+ skip unless String.method_defined?("#{unsafe_method}!")
+ @buffer.send("#{unsafe_method}!", *dummy_args)
+ assert_not_predicate @buffer, :html_safe?
+ end
end
test "Should escape dirty buffers on add" do
clean = "hello".html_safe
- @buffer.gsub!('', '<>')
+ @buffer.gsub!("", "<>")
assert_equal "hello&lt;&gt;", clean + @buffer
end
test "Should concat as a normal string when safe" do
clean = "hello".html_safe
- @buffer.gsub!('', '<>')
+ @buffer.gsub!("", "<>")
assert_equal "<>hello", @buffer + clean
end
test "Should preserve html_safe? status on copy" do
- @buffer.gsub!('', '<>')
- assert !@buffer.dup.html_safe?
+ @buffer.gsub!("", "<>")
+ assert_not_predicate @buffer.dup, :html_safe?
end
test "Should return safe buffer when added with another safe buffer" do
clean = "<script>".html_safe
result_buffer = @buffer + clean
- assert result_buffer.html_safe?
+ assert_predicate result_buffer, :html_safe?
assert_equal "<script>", result_buffer
end
test "Should raise an error when safe_concat is called on unsafe buffers" do
- @buffer.gsub!('', '<>')
+ @buffer.gsub!("", "<>")
assert_raise ActiveSupport::SafeBuffer::SafeConcatError do
@buffer.safe_concat "BUSTED"
end
@@ -121,60 +148,72 @@ class SafeBufferTest < ActiveSupport::TestCase
end
test "clone_empty returns an empty buffer" do
- assert_equal '', ActiveSupport::SafeBuffer.new('foo').clone_empty
+ assert_equal "", ActiveSupport::SafeBuffer.new("foo").clone_empty
end
test "clone_empty keeps the original dirtyness" do
- assert @buffer.clone_empty.html_safe?
- assert !@buffer.gsub!('', '').clone_empty.html_safe?
+ assert_predicate @buffer.clone_empty, :html_safe?
+ assert_not_predicate @buffer.gsub!("", "").clone_empty, :html_safe?
end
test "Should be safe when sliced if original value was safe" do
- new_buffer = @buffer[0,0]
+ new_buffer = @buffer[0, 0]
assert_not_nil new_buffer
assert new_buffer.html_safe?, "should be safe"
end
test "Should continue unsafe on slice" do
- x = 'foo'.html_safe.gsub!('f', '<script>alert("lolpwnd");</script>')
+ x = "foo".html_safe.gsub!("f", '<script>alert("lolpwnd");</script>')
# calling gsub! makes the dirty flag true
- assert !x.html_safe?, "should not be safe"
+ assert_not x.html_safe?, "should not be safe"
# getting a slice of it
y = x[0..-1]
# should still be unsafe
- assert !y.html_safe?, "should not be safe"
+ assert_not y.html_safe?, "should not be safe"
+ end
+
+ test "Should continue safe on slice" do
+ x = "<div>foo</div>".html_safe
+
+ assert_predicate x, :html_safe?
+
+ # getting a slice of it
+ y = x[0..-1]
+
+ # should still be safe
+ assert_predicate y, :html_safe?
end
- test 'Should work with interpolation (array argument)' do
- x = 'foo %s bar'.html_safe % ['qux']
- assert_equal 'foo qux bar', x
+ test "Should work with interpolation (array argument)" do
+ x = "foo %s bar".html_safe % ["qux"]
+ assert_equal "foo qux bar", x
end
- test 'Should work with interpolation (hash argument)' do
- x = 'foo %{x} bar'.html_safe % { x: 'qux' }
- assert_equal 'foo qux bar', x
+ test "Should work with interpolation (hash argument)" do
+ x = "foo %{x} bar".html_safe % { x: "qux" }
+ assert_equal "foo qux bar", x
end
- test 'Should escape unsafe interpolated args' do
- x = 'foo %{x} bar'.html_safe % { x: '<br/>' }
- assert_equal 'foo &lt;br/&gt; bar', x
+ test "Should escape unsafe interpolated args" do
+ x = "foo %{x} bar".html_safe % { x: "<br/>" }
+ assert_equal "foo &lt;br/&gt; bar", x
end
- test 'Should not escape safe interpolated args' do
- x = 'foo %{x} bar'.html_safe % { x: '<br/>'.html_safe }
- assert_equal 'foo <br/> bar', x
+ test "Should not escape safe interpolated args" do
+ x = "foo %{x} bar".html_safe % { x: "<br/>".html_safe }
+ assert_equal "foo <br/> bar", x
end
- test 'Should interpolate to a safe string' do
- x = 'foo %{x} bar'.html_safe % { x: 'qux' }
- assert x.html_safe?, 'should be safe'
+ test "Should interpolate to a safe string" do
+ x = "foo %{x} bar".html_safe % { x: "qux" }
+ assert x.html_safe?, "should be safe"
end
- test 'Should not affect frozen objects when accessing characters' do
- x = 'Hello'.html_safe
- assert_equal x[/a/, 1], nil
+ test "Should not affect frozen objects when accessing characters" do
+ x = "Hello".html_safe
+ assert_nil x[/a/, 1]
end
end
diff --git a/activesupport/test/security_utils_test.rb b/activesupport/test/security_utils_test.rb
index 08d2e3baa6..fff9cc2a8d 100644
--- a/activesupport/test/security_utils_test.rb
+++ b/activesupport/test/security_utils_test.rb
@@ -1,9 +1,22 @@
-require 'abstract_unit'
-require 'active_support/security_utils'
+# frozen_string_literal: true
+
+require "abstract_unit"
+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 ActiveSupport::SecurityUtils.secure_compare("a", "a")
+ assert_not ActiveSupport::SecurityUtils.secure_compare("a", "b")
+ end
+
+ def test_fixed_length_secure_compare_should_perform_string_comparison
+ assert ActiveSupport::SecurityUtils.fixed_length_secure_compare("a", "a")
+ assert_not ActiveSupport::SecurityUtils.fixed_length_secure_compare("a", "b")
+ end
+
+ def test_fixed_length_secure_compare_raise_on_length_mismatch
+ assert_raises(ArgumentError, "string length mismatch.") do
+ ActiveSupport::SecurityUtils.fixed_length_secure_compare("a", "ab")
+ end
end
end
diff --git a/activesupport/test/share_lock_test.rb b/activesupport/test/share_lock_test.rb
new file mode 100644
index 0000000000..34479020e1
--- /dev/null
+++ b/activesupport/test/share_lock_test.rb
@@ -0,0 +1,580 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "concurrent/atomic/count_down_latch"
+require "active_support/concurrency/share_lock"
+
+class ShareLockTest < ActiveSupport::TestCase
+ def setup
+ @lock = ActiveSupport::Concurrency::ShareLock.new
+ end
+
+ def test_reentrancy
+ thread = Thread.new do
+ @lock.sharing { @lock.sharing { } }
+ @lock.exclusive { @lock.exclusive { } }
+ end
+ assert_threads_not_stuck thread
+ end
+
+ def test_sharing_doesnt_block
+ with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_latch|
+ assert_threads_not_stuck(Thread.new { @lock.sharing { } })
+ end
+ end
+
+ def test_sharing_blocks_exclusive
+ with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch|
+ @lock.exclusive(no_wait: true) { flunk } # polling should fail
+ exclusive_thread = Thread.new { @lock.exclusive { } }
+ assert_threads_stuck_but_releasable_by_latch exclusive_thread, sharing_thread_release_latch
+ end
+ end
+
+ def test_exclusive_blocks_sharing
+ with_thread_waiting_in_lock_section(:exclusive) do |exclusive_thread_release_latch|
+ sharing_thread = Thread.new { @lock.sharing { } }
+ assert_threads_stuck_but_releasable_by_latch sharing_thread, exclusive_thread_release_latch
+ end
+ end
+
+ def test_multiple_exlusives_are_able_to_progress
+ with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch|
+ exclusive_threads = (1..2).map do
+ Thread.new do
+ @lock.exclusive { }
+ end
+ end
+
+ assert_threads_stuck_but_releasable_by_latch exclusive_threads, sharing_thread_release_latch
+ end
+ end
+
+ def test_sharing_is_upgradeable_to_exclusive
+ upgrading_thread = Thread.new do
+ @lock.sharing do
+ @lock.exclusive { }
+ end
+ end
+ assert_threads_not_stuck upgrading_thread
+ end
+
+ def test_exclusive_upgrade_waits_for_other_sharers_to_leave
+ with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch|
+ in_sharing = Concurrent::CountDownLatch.new
+
+ upgrading_thread = Thread.new do
+ @lock.sharing do
+ in_sharing.count_down
+ @lock.exclusive { }
+ end
+ end
+
+ in_sharing.wait
+ assert_threads_stuck_but_releasable_by_latch upgrading_thread, sharing_thread_release_latch
+ end
+ end
+
+ def test_exclusive_matching_purpose
+ [true, false].each do |use_upgrading|
+ with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch|
+ exclusive_threads = (1..2).map do
+ Thread.new do
+ @lock.send(use_upgrading ? :sharing : :tap) do
+ @lock.exclusive(purpose: :load, compatible: [:load, :unload]) { }
+ end
+ end
+ end
+
+ assert_threads_stuck_but_releasable_by_latch exclusive_threads, sharing_thread_release_latch
+ end
+ end
+ end
+
+ def test_killed_thread_loses_lock
+ with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch|
+ thread = Thread.new do
+ @lock.sharing do
+ @lock.exclusive { }
+ end
+ end
+
+ assert_threads_stuck thread
+ thread.kill
+
+ sharing_thread_release_latch.count_down
+
+ thread = Thread.new do
+ @lock.exclusive { }
+ end
+
+ assert_threads_not_stuck thread
+ end
+ end
+
+ def test_exclusive_conflicting_purpose
+ [true, false].each do |use_upgrading|
+ with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch|
+ begin
+ together = Concurrent::CyclicBarrier.new(2)
+ conflicting_exclusive_threads = [
+ Thread.new do
+ @lock.send(use_upgrading ? :sharing : :tap) do
+ together.wait
+ @lock.exclusive(purpose: :red, compatible: [:green, :purple]) { }
+ end
+ end,
+ Thread.new do
+ @lock.send(use_upgrading ? :sharing : :tap) do
+ together.wait
+ @lock.exclusive(purpose: :blue, compatible: [:green]) { }
+ end
+ end
+ ]
+
+ assert_threads_stuck conflicting_exclusive_threads # wait for threads to get into their respective `exclusive {}` blocks
+
+ # This thread will be stuck as long as any other thread is in
+ # a sharing block. While it's blocked, it holds no lock, so it
+ # doesn't interfere with any other attempts.
+ no_purpose_thread = Thread.new do
+ @lock.exclusive { }
+ end
+ assert_threads_stuck no_purpose_thread
+
+ # This thread is compatible with both of the "primary"
+ # attempts above. It's initially stuck on the outer share
+ # lock, but as soon as that's released, it can run --
+ # regardless of whether those threads hold share locks.
+ compatible_thread = Thread.new do
+ @lock.exclusive(purpose: :green, compatible: []) { }
+ end
+ assert_threads_stuck compatible_thread
+
+ assert_threads_stuck conflicting_exclusive_threads
+
+ sharing_thread_release_latch.count_down
+
+ assert_threads_not_stuck compatible_thread # compatible thread is now able to squeak through
+
+ if use_upgrading
+ # The "primary" threads both each hold a share lock, and are
+ # mutually incompatible; they're still stuck.
+ assert_threads_stuck conflicting_exclusive_threads
+
+ # The thread without a specified purpose is also stuck; it's
+ # not compatible with anything.
+ assert_threads_stuck no_purpose_thread
+ else
+ # As the primaries didn't hold a share lock, as soon as the
+ # outer one was released, all the exclusive locks are free
+ # to be acquired in turn.
+
+ assert_threads_not_stuck conflicting_exclusive_threads
+ assert_threads_not_stuck no_purpose_thread
+ end
+ ensure
+ conflicting_exclusive_threads.each(&:kill)
+ no_purpose_thread.kill
+ end
+ end
+ end
+ end
+
+ def test_exclusive_ordering
+ scratch_pad = []
+ scratch_pad_mutex = Mutex.new
+
+ load_params = [:load, [:load]]
+ unload_params = [:unload, [:unload, :load]]
+
+ all_sharing = Concurrent::CyclicBarrier.new(4)
+
+ [load_params, load_params, unload_params, unload_params].permutation do |thread_params|
+ with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch|
+ threads = thread_params.map do |purpose, compatible|
+ Thread.new do
+ @lock.sharing do
+ all_sharing.wait
+ @lock.exclusive(purpose: purpose, compatible: compatible) do
+ scratch_pad_mutex.synchronize { scratch_pad << purpose }
+ end
+ end
+ end
+ end
+
+ sleep(0.01)
+ scratch_pad_mutex.synchronize { assert_empty scratch_pad }
+
+ sharing_thread_release_latch.count_down
+
+ assert_threads_not_stuck threads
+ scratch_pad_mutex.synchronize do
+ assert_equal [:load, :load, :unload, :unload], scratch_pad
+ scratch_pad.clear
+ end
+ end
+ end
+ end
+
+ def test_new_share_attempts_block_on_waiting_exclusive
+ with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch|
+ release_exclusive = Concurrent::CountDownLatch.new
+
+ waiting_exclusive = Thread.new do
+ @lock.sharing do
+ @lock.exclusive do
+ release_exclusive.wait
+ end
+ end
+ end
+ assert_threads_stuck waiting_exclusive
+
+ late_share_attempt = Thread.new do
+ @lock.sharing { }
+ end
+ assert_threads_stuck late_share_attempt
+
+ sharing_thread_release_latch.count_down
+ assert_threads_stuck late_share_attempt
+
+ release_exclusive.count_down
+ assert_threads_not_stuck late_share_attempt
+ end
+ end
+
+ def test_share_remains_reentrant_ignoring_a_waiting_exclusive
+ with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch|
+ ready = Concurrent::CyclicBarrier.new(2)
+ attempt_reentrancy = Concurrent::CountDownLatch.new
+
+ sharer = Thread.new do
+ @lock.sharing do
+ ready.wait
+ attempt_reentrancy.wait
+ @lock.sharing { }
+ end
+ end
+
+ exclusive = Thread.new do
+ @lock.sharing do
+ ready.wait
+ @lock.exclusive { }
+ end
+ end
+
+ assert_threads_stuck exclusive
+
+ attempt_reentrancy.count_down
+
+ assert_threads_not_stuck sharer
+ assert_threads_stuck exclusive
+ end
+ end
+
+ def test_compatible_exclusives_cooperate_to_both_proceed
+ ready = Concurrent::CyclicBarrier.new(2)
+ done = Concurrent::CyclicBarrier.new(2)
+
+ threads = 2.times.map do
+ Thread.new do
+ @lock.sharing do
+ ready.wait
+ @lock.exclusive(purpose: :x, compatible: [:x], after_compatible: [:x]) { }
+ done.wait
+ end
+ end
+ end
+
+ assert_threads_not_stuck threads
+ end
+
+ def test_manual_yield
+ ready = Concurrent::CyclicBarrier.new(2)
+ done = Concurrent::CyclicBarrier.new(2)
+
+ threads = [
+ Thread.new do
+ @lock.sharing do
+ ready.wait
+ @lock.exclusive(purpose: :x) { }
+ done.wait
+ end
+ end,
+
+ Thread.new do
+ @lock.sharing do
+ ready.wait
+ @lock.yield_shares(compatible: [:x]) do
+ done.wait
+ end
+ end
+ end,
+ ]
+
+ assert_threads_not_stuck threads
+ end
+
+ def test_manual_incompatible_yield
+ ready = Concurrent::CyclicBarrier.new(2)
+ done = Concurrent::CyclicBarrier.new(2)
+
+ threads = [
+ Thread.new do
+ @lock.sharing do
+ ready.wait
+ @lock.exclusive(purpose: :x) { }
+ done.wait
+ end
+ end,
+
+ Thread.new do
+ @lock.sharing do
+ ready.wait
+ @lock.yield_shares(compatible: [:y]) do
+ done.wait
+ end
+ end
+ end,
+ ]
+
+ assert_threads_stuck threads
+ ensure
+ threads.each(&:kill) if threads
+ end
+
+ def test_manual_recursive_yield
+ ready = Concurrent::CyclicBarrier.new(2)
+ done = Concurrent::CyclicBarrier.new(2)
+ do_nesting = Concurrent::CountDownLatch.new
+
+ threads = [
+ Thread.new do
+ @lock.sharing do
+ ready.wait
+ @lock.exclusive(purpose: :x) { }
+ done.wait
+ end
+ end,
+
+ Thread.new do
+ @lock.sharing do
+ @lock.yield_shares(compatible: [:x]) do
+ @lock.sharing do
+ ready.wait
+ do_nesting.wait
+ @lock.yield_shares(compatible: [:x, :y]) do
+ done.wait
+ end
+ end
+ end
+ end
+ end
+ ]
+
+ assert_threads_stuck threads
+ do_nesting.count_down
+
+ assert_threads_not_stuck threads
+ end
+
+ def test_manual_recursive_yield_cannot_expand_outer_compatible
+ ready = Concurrent::CyclicBarrier.new(2)
+ do_compatible_nesting = Concurrent::CountDownLatch.new
+ in_compatible_nesting = Concurrent::CountDownLatch.new
+
+ incompatible_thread = Thread.new do
+ @lock.sharing do
+ ready.wait
+ @lock.exclusive(purpose: :x) { }
+ end
+ end
+
+ yield_shares_thread = Thread.new do
+ @lock.sharing do
+ ready.wait
+ @lock.yield_shares(compatible: [:y]) do
+ do_compatible_nesting.wait
+ @lock.sharing do
+ @lock.yield_shares(compatible: [:x, :y]) do
+ in_compatible_nesting.wait
+ end
+ end
+ end
+ end
+ end
+
+ assert_threads_stuck incompatible_thread
+ do_compatible_nesting.count_down
+ assert_threads_stuck incompatible_thread
+ in_compatible_nesting.count_down
+ assert_threads_not_stuck [yield_shares_thread, incompatible_thread]
+ end
+
+ def test_manual_recursive_yield_restores_previous_compatible
+ ready = Concurrent::CyclicBarrier.new(2)
+ do_nesting = Concurrent::CountDownLatch.new
+ after_nesting = Concurrent::CountDownLatch.new
+
+ incompatible_thread = Thread.new do
+ ready.wait
+ @lock.exclusive(purpose: :z) { }
+ end
+
+ recursive_yield_shares_thread = Thread.new do
+ @lock.sharing do
+ ready.wait
+ @lock.yield_shares(compatible: [:y]) do
+ do_nesting.wait
+ @lock.sharing do
+ @lock.yield_shares(compatible: [:x, :y]) { }
+ end
+ after_nesting.wait
+ end
+ end
+ end
+
+ assert_threads_stuck incompatible_thread
+ do_nesting.count_down
+ assert_threads_stuck incompatible_thread
+
+ compatible_thread = Thread.new do
+ @lock.exclusive(purpose: :y) { }
+ end
+ assert_threads_not_stuck compatible_thread
+
+ post_nesting_incompatible_thread = Thread.new do
+ @lock.exclusive(purpose: :x) { }
+ end
+ assert_threads_stuck post_nesting_incompatible_thread
+
+ after_nesting.count_down
+ assert_threads_not_stuck recursive_yield_shares_thread
+ # post_nesting_incompatible_thread can now proceed
+ assert_threads_not_stuck post_nesting_incompatible_thread
+ # assert_threads_not_stuck can now proceed
+ assert_threads_not_stuck incompatible_thread
+ end
+
+ def test_in_shared_section_incompatible_non_upgrading_threads_cannot_preempt_upgrading_threads
+ scratch_pad = []
+ scratch_pad_mutex = Mutex.new
+
+ upgrading_load_params = [:load, [:load], true]
+ non_upgrading_unload_params = [:unload, [:load, :unload], false]
+
+ [upgrading_load_params, non_upgrading_unload_params].permutation do |thread_params|
+ with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch|
+ threads = thread_params.map do |purpose, compatible, use_upgrading|
+ Thread.new do
+ @lock.send(use_upgrading ? :sharing : :tap) do
+ @lock.exclusive(purpose: purpose, compatible: compatible) do
+ scratch_pad_mutex.synchronize { scratch_pad << purpose }
+ end
+ end
+ end
+ end
+
+ assert_threads_stuck threads
+ scratch_pad_mutex.synchronize { assert_empty scratch_pad }
+
+ sharing_thread_release_latch.count_down
+
+ assert_threads_not_stuck threads
+ scratch_pad_mutex.synchronize do
+ assert_equal [:load, :unload], scratch_pad
+ scratch_pad.clear
+ end
+ end
+ end
+ end
+
+ private
+
+ module CustomAssertions
+ SUFFICIENT_TIMEOUT = 0.2
+
+ private
+
+ def assert_threads_stuck_but_releasable_by_latch(threads, latch)
+ assert_threads_stuck threads
+ latch.count_down
+ assert_threads_not_stuck threads
+ end
+
+ def assert_threads_stuck(threads)
+ sleep(SUFFICIENT_TIMEOUT) # give threads time to do their business
+ assert(Array(threads).all? { |t| t.join(0.001).nil? })
+ end
+
+ def assert_threads_not_stuck(threads)
+ assert(Array(threads).all? { |t| t.join(SUFFICIENT_TIMEOUT) })
+ end
+ end
+
+ class CustomAssertionsTest < ActiveSupport::TestCase
+ include CustomAssertions
+
+ def setup
+ @latch = Concurrent::CountDownLatch.new
+ @thread = Thread.new { @latch.wait }
+ end
+
+ def teardown
+ @latch.count_down
+ @thread.join
+ end
+
+ def test_happy_path
+ assert_threads_stuck_but_releasable_by_latch @thread, @latch
+ end
+
+ def test_detects_stuck_thread
+ assert_raises(Minitest::Assertion) do
+ assert_threads_not_stuck @thread
+ end
+ end
+
+ def test_detects_free_thread
+ @latch.count_down
+ assert_raises(Minitest::Assertion) do
+ assert_threads_stuck @thread
+ end
+ end
+
+ def test_detects_already_released
+ @latch.count_down
+ assert_raises(Minitest::Assertion) do
+ assert_threads_stuck_but_releasable_by_latch @thread, @latch
+ end
+ end
+
+ def test_detects_remains_latched
+ another_latch = Concurrent::CountDownLatch.new
+ assert_raises(Minitest::Assertion) do
+ assert_threads_stuck_but_releasable_by_latch @thread, another_latch
+ end
+ end
+ end
+
+ include CustomAssertions
+
+ def with_thread_waiting_in_lock_section(lock_section)
+ in_section = Concurrent::CountDownLatch.new
+ section_release = Concurrent::CountDownLatch.new
+
+ stuck_thread = Thread.new do
+ @lock.send(lock_section) do
+ in_section.count_down
+ section_release.wait
+ end
+ end
+
+ in_section.wait
+
+ yield section_release
+ ensure
+ section_release.count_down
+ stuck_thread.join # clean up
+ end
+end
diff --git a/activesupport/test/silence_logger_test.rb b/activesupport/test/silence_logger_test.rb
new file mode 100644
index 0000000000..bd0c6b7f86
--- /dev/null
+++ b/activesupport/test/silence_logger_test.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/logger_silence"
+require "logger"
+
+class LoggerSilenceTest < ActiveSupport::TestCase
+ class MyLogger < ::Logger
+ include ActiveSupport::LoggerSilence
+ end
+
+ setup do
+ @io = StringIO.new
+ @logger = MyLogger.new(@io)
+ end
+
+ test "#silence silences the log" do
+ @logger.silence(Logger::ERROR) do
+ @logger.info("Foo")
+ end
+ @io.rewind
+
+ assert_empty @io.read
+ end
+
+ test "#debug? is true when setting the temporary level to Logger::DEBUG" do
+ @logger.level = Logger::INFO
+
+ @logger.silence(Logger::DEBUG) do
+ assert_predicate @logger, :debug?
+ end
+
+ assert_predicate @logger, :info?
+ end
+end
diff --git a/activesupport/test/string_inquirer_test.rb b/activesupport/test/string_inquirer_test.rb
index a2ed577eb0..09726b144a 100644
--- a/activesupport/test/string_inquirer_test.rb
+++ b/activesupport/test/string_inquirer_test.rb
@@ -1,16 +1,18 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class StringInquirerTest < ActiveSupport::TestCase
def setup
- @string_inquirer = ActiveSupport::StringInquirer.new('production')
+ @string_inquirer = ActiveSupport::StringInquirer.new("production")
end
def test_match
- assert @string_inquirer.production?
+ assert_predicate @string_inquirer, :production?
end
def test_miss
- assert_not @string_inquirer.development?
+ assert_not_predicate @string_inquirer, :development?
end
def test_missing_question_mark
@@ -20,4 +22,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/subscriber_test.rb b/activesupport/test/subscriber_test.rb
index a88d8d9eba..6b012e43af 100644
--- a/activesupport/test/subscriber_test.rb
+++ b/activesupport/test/subscriber_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'active_support/subscriber'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/subscriber"
class TestSubscriber < ActiveSupport::Subscriber
attach_to :doodle
@@ -16,9 +18,9 @@ class TestSubscriber < ActiveSupport::Subscriber
private
- def private_party(event)
- events << event
- end
+ def private_party(event)
+ events << event
+ end
end
# Monkey patch subscriber to test that only one subscriber per method is added.
diff --git a/activesupport/test/tagged_logging_test.rb b/activesupport/test/tagged_logging_test.rb
index 03a63e94e8..cff73472c3 100644
--- a/activesupport/test/tagged_logging_test.rb
+++ b/activesupport/test/tagged_logging_test.rb
@@ -1,6 +1,8 @@
-require 'abstract_unit'
-require 'active_support/logger'
-require 'active_support/tagged_logging'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/logger"
+require "active_support/tagged_logging"
class TaggedLoggingTest < ActiveSupport::TestCase
class MyLogger < ::ActiveSupport::Logger
@@ -14,12 +16,13 @@ class TaggedLoggingTest < ActiveSupport::TestCase
@logger = ActiveSupport::TaggedLogging.new(MyLogger.new(@output))
end
- test 'sets logger.formatter if missing and extends it with a tagging API' do
+ test "sets logger.formatter if missing and extends it with a tagging API" do
logger = Logger.new(StringIO.new)
assert_nil logger.formatter
- ActiveSupport::TaggedLogging.new(logger)
- assert_not_nil logger.formatter
- assert logger.formatter.respond_to?(:tagged)
+
+ other_logger = ActiveSupport::TaggedLogging.new(logger)
+ assert_not_nil other_logger.formatter
+ assert_respond_to other_logger.formatter, :tagged
end
test "tagged once" do
@@ -43,14 +46,14 @@ class TaggedLoggingTest < ActiveSupport::TestCase
end
test "push and pop tags directly" do
- assert_equal %w(A B C), @logger.push_tags('A', ['B', ' ', ['C']])
- @logger.info 'a'
+ assert_equal %w(A B C), @logger.push_tags("A", ["B", " ", ["C"]])
+ @logger.info "a"
assert_equal %w(C), @logger.pop_tags
- @logger.info 'b'
+ @logger.info "b"
assert_equal %w(B), @logger.pop_tags(1)
- @logger.info 'c'
+ @logger.info "c"
assert_equal [], @logger.clear_tags!
- @logger.info 'd'
+ @logger.info "d"
assert_equal "[A] [B] [C] a\n[A] [B] b\n[A] c\nd\n", @output.string
end
@@ -72,24 +75,37 @@ class TaggedLoggingTest < ActiveSupport::TestCase
test "keeps each tag in their own thread" do
@logger.tagged("BCX") do
Thread.new do
- @logger.tagged("OMG") { @logger.info "Cool story bro" }
+ @logger.info "Dull story"
+ @logger.tagged("OMG") { @logger.info "Cool story" }
end.join
@logger.info "Funky time"
end
- assert_equal "[OMG] Cool story bro\n[BCX] Funky time\n", @output.string
+ assert_equal "Dull story\n[OMG] Cool story\n[BCX] Funky time\n", @output.string
end
test "keeps each tag in their own instance" do
- @other_output = StringIO.new
- @other_logger = ActiveSupport::TaggedLogging.new(MyLogger.new(@other_output))
+ other_output = StringIO.new
+ other_logger = ActiveSupport::TaggedLogging.new(MyLogger.new(other_output))
+ @logger.tagged("OMG") do
+ other_logger.tagged("BCX") do
+ @logger.info "Cool story"
+ other_logger.info "Funky time"
+ end
+ end
+ assert_equal "[OMG] Cool story\n", @output.string
+ assert_equal "[BCX] Funky time\n", other_output.string
+ end
+
+ test "does not share the same formatter instance of the original logger" do
+ other_logger = ActiveSupport::TaggedLogging.new(@logger)
+
@logger.tagged("OMG") do
- @other_logger.tagged("BCX") do
- @logger.info "Cool story bro"
- @other_logger.info "Funky time"
+ other_logger.tagged("BCX") do
+ @logger.info "Cool story"
+ other_logger.info "Funky time"
end
end
- assert_equal "[OMG] Cool story bro\n", @output.string
- assert_equal "[BCX] Funky time\n", @other_output.string
+ assert_equal "[OMG] Cool story\n[BCX] Funky time\n", @output.string
end
test "cleans up the taggings on flush" do
@@ -97,11 +113,11 @@ class TaggedLoggingTest < ActiveSupport::TestCase
Thread.new do
@logger.tagged("OMG") do
@logger.flush
- @logger.info "Cool story bro"
+ @logger.info "Cool story"
end
end.join
end
- assert_equal "[FLUSHED]\nCool story bro\n", @output.string
+ assert_equal "[FLUSHED]\nCool story\n", @output.string
end
test "mixed levels of tagging" do
diff --git a/activesupport/test/test_case_test.rb b/activesupport/test/test_case_test.rb
index 9e6d1a91d0..8698c66e6d 100644
--- a/activesupport/test/test_case_test.rb
+++ b/activesupport/test/test_case_test.rb
@@ -1,6 +1,8 @@
-require 'abstract_unit'
+# frozen_string_literal: true
-class AssertDifferenceTest < ActiveSupport::TestCase
+require "abstract_unit"
+
+class AssertionsTest < ActiveSupport::TestCase
def setup
@object = Class.new do
attr_accessor :num
@@ -20,21 +22,21 @@ class AssertDifferenceTest < ActiveSupport::TestCase
assert_equal true, assert_not(false)
e = assert_raises(Minitest::Assertion) { assert_not true }
- assert_equal 'Expected true to be nil or false', e.message
+ assert_equal "Expected true to be nil or false", e.message
- e = assert_raises(Minitest::Assertion) { assert_not true, 'custom' }
- assert_equal 'custom', e.message
+ e = assert_raises(Minitest::Assertion) { assert_not true, "custom" }
+ assert_equal "custom", e.message
end
def test_assert_no_difference_pass
- assert_no_difference '@object.num' do
+ assert_no_difference "@object.num" do
# ...
end
end
def test_assert_no_difference_fail
error = assert_raises(Minitest::Assertion) do
- assert_no_difference '@object.num' do
+ assert_no_difference "@object.num" do
@object.increment
end
end
@@ -43,54 +45,79 @@ class AssertDifferenceTest < ActiveSupport::TestCase
def test_assert_no_difference_with_message_fail
error = assert_raises(Minitest::Assertion) do
- assert_no_difference '@object.num', 'Object Changed' do
+ assert_no_difference "@object.num", "Object Changed" do
@object.increment
end
end
assert_equal "Object Changed.\n\"@object.num\" didn't change by 0.\nExpected: 0\n Actual: 1", error.message
end
+ def test_assert_no_difference_with_multiple_expressions_pass
+ another_object = @object.dup
+ assert_no_difference ["@object.num", -> { another_object.num }] do
+ # ...
+ end
+ end
+
+ def test_assert_no_difference_with_multiple_expressions_fail
+ another_object = @object.dup
+ assert_raises(Minitest::Assertion) do
+ assert_no_difference ["@object.num", -> { another_object.num }], "Another Object Changed" do
+ another_object.increment
+ end
+ end
+ end
+
def test_assert_difference
- assert_difference '@object.num', +1 do
+ assert_difference "@object.num", +1 do
@object.increment
end
end
+ def test_assert_difference_retval
+ incremented = assert_difference "@object.num", +1 do
+ @object.increment
+ end
+
+ assert_equal incremented, 1
+ end
+
def test_assert_difference_with_implicit_difference
- assert_difference '@object.num' do
+ assert_difference "@object.num" do
@object.increment
end
end
def test_arbitrary_expression
- assert_difference '@object.num + 1', +2 do
+ assert_difference "@object.num + 1", +2 do
@object.increment
@object.increment
end
end
def test_negative_differences
- assert_difference '@object.num', -1 do
+ assert_difference "@object.num", -1 do
@object.decrement
end
end
def test_expression_is_evaluated_in_the_appropriate_scope
silence_warnings do
- local_scope = local_scope = 'foo'
- assert_difference('local_scope; @object.num') { @object.increment }
+ local_scope = "foo"
+ local_scope = local_scope # to suppress unused variable warning
+ assert_difference("local_scope; @object.num") { @object.increment }
end
end
def test_array_of_expressions
- assert_difference [ '@object.num', '@object.num + 1' ], +1 do
+ assert_difference [ "@object.num", "@object.num + 1" ], +1 do
@object.increment
end
end
def test_array_of_expressions_identify_failure
assert_raises(Minitest::Assertion) do
- assert_difference ['@object.num', '1 + 1'] do
+ assert_difference ["@object.num", "1 + 1"] do
@object.increment
end
end
@@ -98,14 +125,176 @@ class AssertDifferenceTest < ActiveSupport::TestCase
def test_array_of_expressions_identify_failure_when_message_provided
assert_raises(Minitest::Assertion) do
- assert_difference ['@object.num', '1 + 1'], 1, 'something went wrong' do
+ assert_difference ["@object.num", "1 + 1"], 1, "something went wrong" do
@object.increment
end
end
end
-end
-class AlsoDoingNothingTest < ActiveSupport::TestCase
+ def test_hash_of_expressions
+ assert_difference "@object.num" => 1, "@object.num + 1" => 1 do
+ @object.increment
+ end
+ end
+
+ def test_hash_of_expressions_with_message
+ error = assert_raises Minitest::Assertion do
+ assert_difference({ "@object.num" => 0 }, "Object Changed") do
+ @object.increment
+ end
+ end
+ assert_equal "Object Changed.\n\"@object.num\" didn't change by 0.\nExpected: 0\n Actual: 1", error.message
+ end
+
+ def test_hash_of_lambda_expressions
+ assert_difference -> { @object.num } => 1, -> { @object.num + 1 } => 1 do
+ @object.increment
+ end
+ end
+
+ def test_hash_of_expressions_identify_failure
+ assert_raises(Minitest::Assertion) do
+ assert_difference "@object.num" => 1, "1 + 1" => 1 do
+ @object.increment
+ end
+ end
+ end
+
+ def test_assert_changes_pass
+ assert_changes "@object.num" do
+ @object.increment
+ end
+ end
+
+ def test_assert_changes_pass_with_lambda
+ assert_changes -> { @object.num } do
+ @object.increment
+ end
+ end
+
+ def test_assert_changes_with_from_option
+ assert_changes "@object.num", from: 0 do
+ @object.increment
+ end
+ end
+
+ def test_assert_changes_with_from_option_with_wrong_value
+ assert_raises Minitest::Assertion do
+ assert_changes "@object.num", from: -1 do
+ @object.increment
+ end
+ end
+ end
+
+ def test_assert_changes_with_from_option_with_nil
+ error = assert_raises Minitest::Assertion do
+ assert_changes "@object.num", from: nil do
+ @object.increment
+ end
+ end
+ assert_equal "\"@object.num\" isn't nil", error.message
+ end
+
+ def test_assert_changes_with_to_option
+ assert_changes "@object.num", to: 1 do
+ @object.increment
+ end
+ end
+
+ def test_assert_changes_with_to_option_but_no_change_has_special_message
+ error = assert_raises Minitest::Assertion do
+ assert_changes "@object.num", to: 0 do
+ # no changes
+ end
+ end
+
+ assert_equal "\"@object.num\" didn't change. It was already 0", error.message
+ end
+
+ def test_assert_changes_with_wrong_to_option
+ assert_raises Minitest::Assertion do
+ assert_changes "@object.num", to: 2 do
+ @object.increment
+ end
+ end
+ end
+
+ def test_assert_changes_with_from_option_and_to_option
+ assert_changes "@object.num", from: 0, to: 1 do
+ @object.increment
+ end
+ end
+
+ def test_assert_changes_with_from_and_to_options_and_wrong_to_value
+ assert_raises Minitest::Assertion do
+ assert_changes "@object.num", from: 0, to: 2 do
+ @object.increment
+ end
+ end
+ end
+
+ def test_assert_changes_works_with_any_object
+ # Silences: instance variable @new_object not initialized.
+ retval = silence_warnings do
+ assert_changes :@new_object, from: nil, to: 42 do
+ @new_object = 42
+ end
+ end
+
+ assert_equal 42, retval
+ end
+
+ def test_assert_changes_works_with_nil
+ oldval = @object
+
+ retval = assert_changes :@object, from: oldval, to: nil do
+ @object = nil
+ end
+
+ assert_nil retval
+ end
+
+ def test_assert_changes_with_to_and_case_operator
+ token = nil
+
+ assert_changes -> { token }, to: /\w{32}/ do
+ token = SecureRandom.hex
+ end
+ end
+
+ def test_assert_changes_with_to_and_from_and_case_operator
+ token = SecureRandom.hex
+
+ assert_changes -> { token }, from: /\w{32}/, to: /\w{32}/ do
+ token = SecureRandom.hex
+ end
+ end
+
+ def test_assert_changes_with_message
+ error = assert_raises Minitest::Assertion do
+ assert_changes "@object.num", "@object.num should 1", to: 1 do
+ @object.decrement
+ end
+ end
+
+ assert_equal "@object.num should 1.\n\"@object.num\" didn't change to as expected\nExpected: 1\n Actual: -1", error.message
+ end
+
+ def test_assert_no_changes_pass
+ assert_no_changes "@object.num" do
+ # ...
+ end
+ end
+
+ def test_assert_no_changes_with_message
+ error = assert_raises Minitest::Assertion do
+ assert_no_changes "@object.num", "@object.num should not change" do
+ @object.increment
+ end
+ end
+
+ assert_equal "@object.num should not change.\n\"@object.num\" did change to 1", error.message
+ end
end
# Setup and teardown callbacks.
@@ -125,7 +314,7 @@ class SetupAndTeardownTest < ActiveSupport::TestCase
def teardown
end
- protected
+ private
def reset_callback_record
@called_back = []
@@ -150,7 +339,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
@@ -162,7 +351,7 @@ end
class TestCaseTaggedLoggingTest < ActiveSupport::TestCase
def before_setup
- require 'stringio'
+ require "stringio"
@out = StringIO.new
self.tagged_logger = ActiveSupport::TaggedLogging.new(Logger.new(@out))
super
diff --git a/activesupport/test/testing/after_teardown_test.rb b/activesupport/test/testing/after_teardown_test.rb
new file mode 100644
index 0000000000..961af49479
--- /dev/null
+++ b/activesupport/test/testing/after_teardown_test.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+module OtherAfterTeardown
+ def after_teardown
+ super
+
+ @witness = true
+ end
+end
+
+class AfterTeardownTest < ActiveSupport::TestCase
+ include OtherAfterTeardown
+
+ attr_writer :witness
+
+ MyError = Class.new(StandardError)
+
+ teardown do
+ raise MyError, "Test raises an error, all after_teardown should still get called"
+ end
+
+ def after_teardown
+ assert_changes -> { failures.count }, from: 0, to: 1 do
+ super
+ end
+
+ assert_equal true, @witness
+ failures.clear
+ end
+
+ def test_teardown_raise_but_all_after_teardown_method_are_called
+ assert true
+ end
+end
diff --git a/activesupport/test/testing/constant_lookup_test.rb b/activesupport/test/testing/constant_lookup_test.rb
index 0f16419c8b..37b7822950 100644
--- a/activesupport/test/testing/constant_lookup_test.rb
+++ b/activesupport/test/testing/constant_lookup_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'dependencies_test_helpers'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "dependencies_test_helpers"
class Foo; end
class Bar < Foo
@@ -69,7 +71,7 @@ class ConstantLookupTest < ActiveSupport::TestCase
def test_does_not_swallow_exception_on_no_name_error_within_constant
assert_raises(NameError) do
with_autoloading_fixtures do
- self.class.determine_constant_from_test_name('RaisesNameError')
+ self.class.determine_constant_from_test_name("RaisesNameError")
end
end
end
diff --git a/activesupport/test/testing/file_fixtures_test.rb b/activesupport/test/testing/file_fixtures_test.rb
index 91b8a9071c..35be8f5206 100644
--- a/activesupport/test/testing/file_fixtures_test.rb
+++ b/activesupport/test/testing/file_fixtures_test.rb
@@ -1,12 +1,16 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+require "pathname"
class FileFixturesTest < ActiveSupport::TestCase
- self.file_fixture_path = File.expand_path("../../file_fixtures", __FILE__)
+ self.file_fixture_path = File.expand_path("../file_fixtures", __dir__)
test "#file_fixture returns Pathname to file fixture" do
path = file_fixture("sample.txt")
assert_kind_of Pathname, path
- assert_match %r{activesupport/test/file_fixtures/sample.txt$}, path.to_s
+ assert_match %r{.*/test/file_fixtures/sample\.txt$}, path.to_s
end
test "raises an exception when the fixture file does not exist" do
@@ -18,11 +22,11 @@ class FileFixturesTest < ActiveSupport::TestCase
end
class FileFixturesPathnameDirectoryTest < ActiveSupport::TestCase
- self.file_fixture_path = Pathname.new(File.expand_path("../../file_fixtures", __FILE__))
+ self.file_fixture_path = Pathname.new(File.expand_path("../file_fixtures", __dir__))
test "#file_fixture_path returns Pathname to file fixture" do
path = file_fixture("sample.txt")
assert_kind_of Pathname, path
- assert_match %r{activesupport/test/file_fixtures/sample.txt$}, path.to_s
+ assert_match %r{.*/test/file_fixtures/sample\.txt$}, path.to_s
end
end
diff --git a/activesupport/test/testing/method_call_assertions_test.rb b/activesupport/test/testing/method_call_assertions_test.rb
index a9908aea0d..669463bd31 100644
--- a/activesupport/test/testing/method_call_assertions_test.rb
+++ b/activesupport/test/testing/method_call_assertions_test.rb
@@ -1,9 +1,8 @@
-require 'abstract_unit'
-require 'active_support/testing/method_call_assertions'
+# frozen_string_literal: true
-class MethodCallAssertionsTest < ActiveSupport::TestCase
- include ActiveSupport::Testing::MethodCallAssertions
+require "abstract_unit"
+class MethodCallAssertionsTest < ActiveSupport::TestCase
class Level
def increment; 1; end
def decrement; end
@@ -27,6 +26,20 @@ class MethodCallAssertionsTest < ActiveSupport::TestCase
end
end
+ def test_assert_called_method_with_arguments
+ assert_called(@object, :<<) do
+ @object << 2
+ end
+ end
+
+ def test_assert_called_returns
+ assert_called(@object, :increment, returns: 10) do
+ assert_equal 10, @object.increment
+ end
+
+ assert_equal 1, @object.increment
+ end
+
def test_assert_called_failure
error = assert_raises(Minitest::Assertion) do
assert_called(@object, :increment) do
@@ -39,7 +52,7 @@ class MethodCallAssertionsTest < ActiveSupport::TestCase
def test_assert_called_with_message
error = assert_raises(Minitest::Assertion) do
- assert_called(@object, :increment, 'dang it') do
+ assert_called(@object, :increment, "dang it") do
# Call nothing...
end
end
@@ -47,18 +60,20 @@ class MethodCallAssertionsTest < ActiveSupport::TestCase
assert_match(/dang it.\nExpected increment/, error.message)
end
- def test_assert_called_with
- assert_called_with(@object, :increment) do
- @object.increment
- end
- end
-
def test_assert_called_with_arguments
assert_called_with(@object, :<<, [ 2 ]) do
@object << 2
end
end
+ def test_assert_called_with_arguments_and_returns
+ assert_called_with(@object, :<<, [ 2 ], returns: 10) do
+ assert_equal(10, @object << 2)
+ end
+
+ assert_nil(@object << 2)
+ end
+
def test_assert_called_with_failure
assert_raises(MockExpectationError) do
assert_called_with(@object, :<<, [ 4567 ]) do
@@ -67,19 +82,72 @@ class MethodCallAssertionsTest < ActiveSupport::TestCase
end
end
- def test_assert_called_with_returns
- assert_called_with(@object, :increment, returns: 1) do
+ def test_assert_called_with_multiple_expected_arguments
+ assert_called_with(@object, :<<, [ [ 1 ], [ 2 ] ]) do
+ @object << 1
+ @object << 2
+ end
+ end
+
+ def test_assert_called_on_instance_of_with_defaults_to_expect_once
+ assert_called_on_instance_of Level, :increment do
@object.increment
end
end
- def test_assert_called_with_multiple_expected_arguments
- assert_called_with(@object, :<<, [ [ 1 ], [ 2 ] ]) do
- @object << 1
+ def test_assert_called_on_instance_of_more_than_once
+ assert_called_on_instance_of(Level, :increment, times: 2) do
+ @object.increment
+ @object.increment
+ end
+ end
+
+ def test_assert_called_on_instance_of_with_arguments
+ assert_called_on_instance_of(Level, :<<) do
@object << 2
end
end
+ def test_assert_called_on_instance_of_returns
+ assert_called_on_instance_of(Level, :increment, returns: 10) do
+ assert_equal 10, @object.increment
+ end
+
+ assert_equal 1, @object.increment
+ end
+
+ def test_assert_called_on_instance_of_failure
+ error = assert_raises(Minitest::Assertion) do
+ assert_called_on_instance_of(Level, :increment) do
+ # Call nothing...
+ end
+ end
+
+ assert_equal "Expected increment to be called 1 times, but was called 0 times.\nExpected: 1\n Actual: 0", error.message
+ end
+
+ def test_assert_called_on_instance_of_with_message
+ error = assert_raises(Minitest::Assertion) do
+ assert_called_on_instance_of(Level, :increment, "dang it") do
+ # Call nothing...
+ end
+ end
+
+ assert_match(/dang it.\nExpected increment/, error.message)
+ end
+
+ def test_assert_called_on_instance_of_nesting
+ assert_called_on_instance_of(Level, :increment, times: 3) do
+ assert_called_on_instance_of(Level, :decrement, times: 2) do
+ @object.increment
+ @object.decrement
+ @object.increment
+ @object.decrement
+ @object.increment
+ end
+ end
+ end
+
def test_assert_not_called
assert_not_called(@object, :decrement) do
@object.increment
@@ -95,4 +163,41 @@ class MethodCallAssertionsTest < ActiveSupport::TestCase
assert_equal "Expected increment to be called 0 times, but was called 1 times.\nExpected: 0\n Actual: 1", error.message
end
+
+ def test_assert_not_called_on_instance_of
+ assert_not_called_on_instance_of(Level, :decrement) do
+ @object.increment
+ end
+ end
+
+ def test_assert_not_called_on_instance_of_failure
+ error = assert_raises(Minitest::Assertion) do
+ assert_not_called_on_instance_of(Level, :increment) do
+ @object.increment
+ end
+ end
+
+ assert_equal "Expected increment to be called 0 times, but was called 1 times.\nExpected: 0\n Actual: 1", error.message
+ end
+
+ def test_assert_not_called_on_instance_of_nesting
+ assert_not_called_on_instance_of(Level, :increment) do
+ assert_not_called_on_instance_of(Level, :decrement) do
+ # Call nothing...
+ end
+ end
+ end
+
+ def test_stub_any_instance
+ stub_any_instance(Level) do |instance|
+ assert_equal instance, Level.new
+ end
+ end
+
+ def test_stub_any_instance_with_instance
+ stub_any_instance(Level, instance: @object) do |instance|
+ assert_equal @object, instance
+ assert_equal instance, Level.new
+ end
+ end
end
diff --git a/activesupport/test/time_travel_test.rb b/activesupport/test/time_travel_test.rb
index 59c3e52c2f..8c47f2cdc7 100644
--- a/activesupport/test/time_travel_test.rb
+++ b/activesupport/test/time_travel_test.rb
@@ -1,20 +1,26 @@
-require 'abstract_unit'
-require 'active_support/core_ext/date_time'
-require 'active_support/core_ext/numeric/time'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/date_time"
+require "active_support/core_ext/numeric/time"
class TimeTravelTest < ActiveSupport::TestCase
- teardown do
- travel_back
- end
+ class TimeSubclass < ::Time; end
+ class DateSubclass < ::Date; end
+ class DateTimeSubclass < ::DateTime; end
def test_time_helper_travel
Time.stub(:now, Time.now) do
- expected_time = Time.now + 1.day
- travel 1.day
+ begin
+ expected_time = Time.now + 1.day
+ travel 1.day
- assert_equal expected_time.to_s(:db), Time.now.to_s(:db)
- assert_equal expected_time.to_date, Date.today
- assert_equal expected_time.to_datetime.to_s(:db), DateTime.now.to_s(:db)
+ assert_equal expected_time.to_s(:db), Time.now.to_s(:db)
+ assert_equal expected_time.to_date, Date.today
+ assert_equal expected_time.to_datetime.to_s(:db), DateTime.now.to_s(:db)
+ ensure
+ travel_back
+ end
end
end
@@ -36,12 +42,16 @@ class TimeTravelTest < ActiveSupport::TestCase
def test_time_helper_travel_to
Time.stub(:now, Time.now) do
- expected_time = Time.new(2004, 11, 24, 01, 04, 44)
- travel_to expected_time
+ begin
+ expected_time = Time.new(2004, 11, 24, 01, 04, 44)
+ travel_to expected_time
- assert_equal expected_time, Time.now
- assert_equal Date.new(2004, 11, 24), Date.today
- assert_equal expected_time.to_datetime, DateTime.now
+ assert_equal expected_time, Time.now
+ assert_equal Date.new(2004, 11, 24), Date.today
+ assert_equal expected_time.to_datetime, DateTime.now
+ ensure
+ travel_back
+ end
end
end
@@ -63,17 +73,69 @@ class TimeTravelTest < ActiveSupport::TestCase
def test_time_helper_travel_back
Time.stub(:now, Time.now) do
- expected_time = Time.new(2004, 11, 24, 01, 04, 44)
+ begin
+ expected_time = Time.new(2004, 11, 24, 01, 04, 44)
- travel_to expected_time
- assert_equal expected_time, Time.now
- assert_equal Date.new(2004, 11, 24), Date.today
- assert_equal expected_time.to_datetime, DateTime.now
- travel_back
+ travel_to expected_time
+ assert_equal expected_time, Time.now
+ assert_equal Date.new(2004, 11, 24), Date.today
+ assert_equal expected_time.to_datetime, DateTime.now
+ travel_back
- assert_not_equal expected_time, Time.now
- assert_not_equal Date.new(2004, 11, 24), Date.today
- assert_not_equal expected_time.to_datetime, DateTime.now
+ assert_not_equal expected_time, Time.now
+ assert_not_equal Date.new(2004, 11, 24), Date.today
+ assert_not_equal expected_time.to_datetime, DateTime.now
+ ensure
+ travel_back
+ end
+ end
+ end
+
+ def test_time_helper_travel_to_with_nested_calls_with_blocks
+ Time.stub(:now, Time.now) do
+ 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
+ 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
+
+ def test_time_helper_travel_to_with_nested_calls
+ Time.stub(:now, Time.now) do
+ 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_nothing_raised do
+ travel_to(inner_expected_time)
+
+ assert_equal inner_expected_time, Time.now
+ end
+ end
+ end
+ end
+
+ def test_time_helper_travel_to_with_subsequent_calls
+ Time.stub(:now, Time.now) do
+ begin
+ initial_expected_time = Time.new(2004, 11, 24, 01, 04, 44)
+ subsequent_expected_time = Time.new(2004, 10, 24, 01, 04, 44)
+ assert_nothing_raised do
+ travel_to initial_expected_time
+ travel_to subsequent_expected_time
+
+ assert_equal subsequent_expected_time, Time.now
+
+ travel_back
+ end
+ ensure
+ travel_back
+ end
end
end
@@ -87,4 +149,45 @@ 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
+
+ def test_time_helper_freeze_time
+ expected_time = Time.now
+ freeze_time
+ sleep(1)
+
+ assert_equal expected_time.to_s(:db), Time.now.to_s(:db)
+ ensure
+ travel_back
+ end
+
+ def test_time_helper_freeze_time_with_block
+ expected_time = Time.now
+
+ freeze_time do
+ sleep(1)
+
+ assert_equal expected_time.to_s(:db), Time.now.to_s(:db)
+ end
+
+ assert_operator expected_time.to_s(:db), :<, Time.now.to_s(:db)
+ end
+
+ def test_time_helper_unfreeze_time
+ assert_equal method(:travel_back), method(:unfreeze_time)
+ end
end
diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb
index 00d40c4497..6d45a6726d 100644
--- a/activesupport/test/time_zone_test.rb
+++ b/activesupport/test/time_zone_test.rb
@@ -1,25 +1,27 @@
-require 'abstract_unit'
-require 'active_support/time'
-require 'time_zone_test_helpers'
-require 'yaml'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/time"
+require "time_zone_test_helpers"
+require "yaml"
class TimeZoneTest < ActiveSupport::TestCase
include TimeZoneTestHelpers
def test_utc_to_local
- zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
assert_equal Time.utc(1999, 12, 31, 19), zone.utc_to_local(Time.utc(2000, 1)) # standard offset -0500
assert_equal Time.utc(2000, 6, 30, 20), zone.utc_to_local(Time.utc(2000, 7)) # dst offset -0400
end
def test_local_to_utc
- zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
assert_equal Time.utc(2000, 1, 1, 5), zone.local_to_utc(Time.utc(2000, 1)) # standard offset -0500
assert_equal Time.utc(2000, 7, 1, 4), zone.local_to_utc(Time.utc(2000, 7)) # dst offset -0400
end
def test_period_for_local
- zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
assert_instance_of TZInfo::TimezonePeriod, zone.period_for_local(Time.utc(2000))
end
@@ -30,6 +32,12 @@ class TimeZoneTest < ActiveSupport::TestCase
end
end
+ def test_period_for_local_with_ambigiuous_time
+ zone = ActiveSupport::TimeZone["Moscow"]
+ period = zone.period_for_local(Time.utc(2015, 1, 1))
+ assert_equal period, zone.period_for_local(Time.utc(2014, 10, 26, 1, 0, 0))
+ end
+
def test_from_integer_to_map
assert_instance_of ActiveSupport::TimeZone, ActiveSupport::TimeZone[-28800] # PST
end
@@ -39,7 +47,7 @@ class TimeZoneTest < ActiveSupport::TestCase
end
ActiveSupport::TimeZone.all.each do |zone|
- name = zone.name.downcase.gsub(/[^a-z]/, '_')
+ name = zone.name.downcase.gsub(/[^a-z]/, "_")
define_method("test_from_#{name}_to_map") do
assert_instance_of ActiveSupport::TimeZone, ActiveSupport::TimeZone[zone.name]
end
@@ -51,86 +59,83 @@ class TimeZoneTest < ActiveSupport::TestCase
end
def test_now
- with_env_tz 'US/Eastern' do
- zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'].dup
+ with_env_tz "US/Eastern" do
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"].dup
def zone.time_now; Time.local(2000); end
assert_instance_of ActiveSupport::TimeWithZone, zone.now
- assert_equal Time.utc(2000,1,1,5), zone.now.utc
+ assert_equal Time.utc(2000, 1, 1, 5), zone.now.utc
assert_equal Time.utc(2000), zone.now.time
assert_equal zone, zone.now.time_zone
end
end
def test_now_enforces_spring_dst_rules
- with_env_tz 'US/Eastern' do
- zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'].dup
+ with_env_tz "US/Eastern" do
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"].dup
def zone.time_now
- Time.local(2006,4,2,2) # 2AM springs forward to 3AM
+ Time.local(2006, 4, 2, 2) # 2AM springs forward to 3AM
end
- assert_equal Time.utc(2006,4,2,3), zone.now.time
+ assert_equal Time.utc(2006, 4, 2, 3), zone.now.time
assert_equal true, zone.now.dst?
end
end
def test_now_enforces_fall_dst_rules
- with_env_tz 'US/Eastern' do
- zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'].dup
+ with_env_tz "US/Eastern" do
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"].dup
def zone.time_now
Time.at(1162098000) # equivalent to 1AM DST
end
- assert_equal Time.utc(2006,10,29,1), zone.now.time
+ assert_equal Time.utc(2006, 10, 29, 1), zone.now.time
assert_equal true, zone.now.dst?
end
end
def test_unknown_timezones_delegation_to_tzinfo
- zone = ActiveSupport::TimeZone['America/Montevideo']
+ zone = ActiveSupport::TimeZone["America/Montevideo"]
assert_equal ActiveSupport::TimeZone, zone.class
- assert_equal zone.object_id, ActiveSupport::TimeZone['America/Montevideo'].object_id
+ assert_equal zone.object_id, ActiveSupport::TimeZone["America/Montevideo"].object_id
assert_equal Time.utc(2010, 1, 31, 22), zone.utc_to_local(Time.utc(2010, 2)) # daylight saving offset -0200
assert_equal Time.utc(2010, 3, 31, 21), zone.utc_to_local(Time.utc(2010, 4)) # standard offset -0300
end
def test_today
travel_to(Time.utc(2000, 1, 1, 4, 59, 59)) # 1 sec before midnight Jan 1 EST
- assert_equal Date.new(1999, 12, 31), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].today
+ assert_equal Date.new(1999, 12, 31), ActiveSupport::TimeZone["Eastern Time (US & Canada)"].today
travel_to(Time.utc(2000, 1, 1, 5)) # midnight Jan 1 EST
- assert_equal Date.new(2000, 1, 1), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].today
+ assert_equal Date.new(2000, 1, 1), ActiveSupport::TimeZone["Eastern Time (US & Canada)"].today
travel_to(Time.utc(2000, 1, 2, 4, 59, 59)) # 1 sec before midnight Jan 2 EST
- assert_equal Date.new(2000, 1, 1), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].today
+ assert_equal Date.new(2000, 1, 1), ActiveSupport::TimeZone["Eastern Time (US & Canada)"].today
travel_to(Time.utc(2000, 1, 2, 5)) # midnight Jan 2 EST
- assert_equal Date.new(2000, 1, 2), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].today
- travel_back
+ assert_equal Date.new(2000, 1, 2), ActiveSupport::TimeZone["Eastern Time (US & Canada)"].today
end
def test_tomorrow
travel_to(Time.utc(2000, 1, 1, 4, 59, 59)) # 1 sec before midnight Jan 1 EST
- assert_equal Date.new(2000, 1, 1), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].tomorrow
+ assert_equal Date.new(2000, 1, 1), ActiveSupport::TimeZone["Eastern Time (US & Canada)"].tomorrow
travel_to(Time.utc(2000, 1, 1, 5)) # midnight Jan 1 EST
- assert_equal Date.new(2000, 1, 2), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].tomorrow
+ assert_equal Date.new(2000, 1, 2), ActiveSupport::TimeZone["Eastern Time (US & Canada)"].tomorrow
travel_to(Time.utc(2000, 1, 2, 4, 59, 59)) # 1 sec before midnight Jan 2 EST
- assert_equal Date.new(2000, 1, 2), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].tomorrow
+ assert_equal Date.new(2000, 1, 2), ActiveSupport::TimeZone["Eastern Time (US & Canada)"].tomorrow
travel_to(Time.utc(2000, 1, 2, 5)) # midnight Jan 2 EST
- assert_equal Date.new(2000, 1, 3), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].tomorrow
- travel_back
+ assert_equal Date.new(2000, 1, 3), ActiveSupport::TimeZone["Eastern Time (US & Canada)"].tomorrow
end
def test_yesterday
travel_to(Time.utc(2000, 1, 1, 4, 59, 59)) # 1 sec before midnight Jan 1 EST
- assert_equal Date.new(1999, 12, 30), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].yesterday
+ assert_equal Date.new(1999, 12, 30), ActiveSupport::TimeZone["Eastern Time (US & Canada)"].yesterday
travel_to(Time.utc(2000, 1, 1, 5)) # midnight Jan 1 EST
- assert_equal Date.new(1999, 12, 31), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].yesterday
+ assert_equal Date.new(1999, 12, 31), ActiveSupport::TimeZone["Eastern Time (US & Canada)"].yesterday
travel_to(Time.utc(2000, 1, 2, 4, 59, 59)) # 1 sec before midnight Jan 2 EST
- assert_equal Date.new(1999, 12, 31), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].yesterday
+ assert_equal Date.new(1999, 12, 31), ActiveSupport::TimeZone["Eastern Time (US & Canada)"].yesterday
travel_to(Time.utc(2000, 1, 2, 5)) # midnight Jan 2 EST
- assert_equal Date.new(2000, 1, 1), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].yesterday
- travel_back
+ assert_equal Date.new(2000, 1, 1), ActiveSupport::TimeZone["Eastern Time (US & Canada)"].yesterday
end
def test_travel_to_a_date
with_env_tz do
- Time.use_zone('Hawaii') do
+ Time.use_zone("Hawaii") do
date = Date.new(2014, 2, 18)
time = date.midnight
@@ -162,52 +167,57 @@ class TimeZoneTest < ActiveSupport::TestCase
def test_local_with_old_date
time = ActiveSupport::TimeZone["Hawaii"].local(1850, 2, 5, 15, 30, 45)
- assert_equal [45,30,15,5,2,1850], time.to_a[0,6]
+ assert_equal [45, 30, 15, 5, 2, 1850], time.to_a[0, 6]
assert_equal ActiveSupport::TimeZone["Hawaii"], time.time_zone
end
def test_local_enforces_spring_dst_rules
- zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
- twz = zone.local(2006,4,2,1,59,59) # 1 second before DST start
- assert_equal Time.utc(2006,4,2,1,59,59), twz.time
- assert_equal Time.utc(2006,4,2,6,59,59), twz.utc
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ twz = zone.local(2006, 4, 2, 1, 59, 59) # 1 second before DST start
+ assert_equal Time.utc(2006, 4, 2, 1, 59, 59), twz.time
+ assert_equal Time.utc(2006, 4, 2, 6, 59, 59), twz.utc
assert_equal false, twz.dst?
- assert_equal 'EST', twz.zone
- twz2 = zone.local(2006,4,2,2) # 2AM does not exist because at 2AM, time springs forward to 3AM
- assert_equal Time.utc(2006,4,2,3), twz2.time # twz is created for 3AM
- assert_equal Time.utc(2006,4,2,7), twz2.utc
+ assert_equal "EST", twz.zone
+ twz2 = zone.local(2006, 4, 2, 2) # 2AM does not exist because at 2AM, time springs forward to 3AM
+ assert_equal Time.utc(2006, 4, 2, 3), twz2.time # twz is created for 3AM
+ assert_equal Time.utc(2006, 4, 2, 7), twz2.utc
assert_equal true, twz2.dst?
- assert_equal 'EDT', twz2.zone
- twz3 = zone.local(2006,4,2,2,30) # 2:30AM does not exist because at 2AM, time springs forward to 3AM
- assert_equal Time.utc(2006,4,2,3,30), twz3.time # twz is created for 3:30AM
- assert_equal Time.utc(2006,4,2,7,30), twz3.utc
+ assert_equal "EDT", twz2.zone
+ twz3 = zone.local(2006, 4, 2, 2, 30) # 2:30AM does not exist because at 2AM, time springs forward to 3AM
+ assert_equal Time.utc(2006, 4, 2, 3, 30), twz3.time # twz is created for 3:30AM
+ assert_equal Time.utc(2006, 4, 2, 7, 30), twz3.utc
assert_equal true, twz3.dst?
- assert_equal 'EDT', twz3.zone
+ assert_equal "EDT", twz3.zone
end
def test_local_enforces_fall_dst_rules
# 1AM during fall DST transition is ambiguous, it could be either DST or non-DST 1AM
# Mirroring Time.local behavior, this method selects the DST time
- zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
- twz = zone.local(2006,10,29,1)
- assert_equal Time.utc(2006,10,29,1), twz.time
- assert_equal Time.utc(2006,10,29,5), twz.utc
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ twz = zone.local(2006, 10, 29, 1)
+ assert_equal Time.utc(2006, 10, 29, 1), twz.time
+ assert_equal Time.utc(2006, 10, 29, 5), twz.utc
assert_equal true, twz.dst?
- assert_equal 'EDT', twz.zone
+ assert_equal "EDT", twz.zone
+ end
+
+ def test_local_with_ambiguous_time
+ zone = ActiveSupport::TimeZone["Moscow"]
+ assert_equal Time.utc(2014, 10, 25, 22, 0, 0), zone.local(2014, 10, 26, 1, 0, 0)
end
def test_at
- zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
secs = 946684800.0
twz = zone.at(secs)
- assert_equal Time.utc(1999,12,31,19), twz.time
+ assert_equal Time.utc(1999, 12, 31, 19), twz.time
assert_equal Time.utc(2000), twz.utc
assert_equal zone, twz.time_zone
assert_equal secs, twz.to_f
end
def test_at_with_old_date
- zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
secs = DateTime.civil(1850).to_f
twz = zone.at(secs)
assert_equal [1850, 1, 1, 0], [twz.utc.year, twz.utc.mon, twz.utc.day, twz.utc.hour]
@@ -215,10 +225,114 @@ class TimeZoneTest < ActiveSupport::TestCase
assert_equal secs, twz.to_f
end
+ def test_at_with_microseconds
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ secs = 946684800.0
+ microsecs = 123456.789
+ twz = zone.at(secs, microsecs)
+ assert_equal zone, twz.time_zone
+ assert_equal secs, twz.to_i
+ assert_equal 123456789, twz.nsec
+ 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_iso8601_with_ambiguous_time
+ zone = ActiveSupport::TimeZone["Moscow"]
+ assert_equal Time.utc(2014, 10, 25, 22, 0, 0), zone.parse("2014-10-26T01:00:00")
+ end
+
def test_parse
- zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
- twz = zone.parse('1999-12-31 19:00:00')
- assert_equal Time.utc(1999,12,31,19), twz.time
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ twz = zone.parse("1999-12-31 19: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
@@ -226,175 +340,324 @@ class TimeZoneTest < ActiveSupport::TestCase
def test_parse_string_with_timezone
(-11..13).each do |timezone_offset|
zone = ActiveSupport::TimeZone[timezone_offset]
- twz = zone.parse('1999-12-31 19:00:00')
+ twz = zone.parse("1999-12-31 19:00:00")
assert_equal twz, zone.parse(twz.to_s)
end
end
def test_parse_with_old_date
- zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
- twz = zone.parse('1883-12-31 19:00:00')
- assert_equal [0,0,19,31,12,1883], twz.to_a[0,6]
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ twz = zone.parse("1883-12-31 19:00:00")
+ assert_equal [0, 0, 19, 31, 12, 1883], twz.to_a[0, 6]
assert_equal zone, twz.time_zone
end
def test_parse_far_future_date_with_time_zone_offset_in_string
- zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
- twz = zone.parse('2050-12-31 19: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]
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ twz = zone.parse("2050-12-31 19: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_parse_returns_nil_when_string_without_date_information_is_passed_in
- zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
- assert_nil zone.parse('foobar')
- assert_nil zone.parse(' ')
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ assert_nil zone.parse("foobar")
+ assert_nil zone.parse(" ")
end
def test_parse_with_incomplete_date
- zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
- zone.stub(:now, zone.local(1999,12,31)) do
- twz = zone.parse('19:00:00')
- assert_equal Time.utc(1999,12,31,19), twz.time
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ zone.stub(:now, zone.local(1999, 12, 31)) do
+ twz = zone.parse("19:00:00")
+ assert_equal Time.utc(1999, 12, 31, 19), twz.time
end
end
def test_parse_with_day_omitted
- with_env_tz 'US/Eastern' do
- zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
- assert_equal Time.local(2000, 2, 1), zone.parse('Feb', Time.local(2000, 1, 1))
- assert_equal Time.local(2005, 2, 1), zone.parse('Feb 2005', Time.local(2000, 1, 1))
- assert_equal Time.local(2005, 2, 2), zone.parse('2 Feb 2005', Time.local(2000, 1, 1))
+ with_env_tz "US/Eastern" do
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ assert_equal Time.local(2000, 2, 1), zone.parse("Feb", Time.local(2000, 1, 1))
+ assert_equal Time.local(2005, 2, 1), zone.parse("Feb 2005", Time.local(2000, 1, 1))
+ assert_equal Time.local(2005, 2, 2), zone.parse("2 Feb 2005", Time.local(2000, 1, 1))
end
end
def test_parse_should_not_black_out_system_timezone_dst_jump
- with_env_tz('EET') do
- zone = ActiveSupport::TimeZone['Pacific Time (US & Canada)']
- twz = zone.parse('2012-03-25 03:29:00')
- assert_equal [0, 29, 3, 25, 3, 2012], twz.to_a[0,6]
+ with_env_tz("EET") do
+ zone = ActiveSupport::TimeZone["Pacific Time (US & Canada)"]
+ twz = zone.parse("2012-03-25 03:29:00")
+ assert_equal [0, 29, 3, 25, 3, 2012], twz.to_a[0, 6]
end
end
def test_parse_should_black_out_app_timezone_dst_jump
- with_env_tz('EET') do
- zone = ActiveSupport::TimeZone['Pacific Time (US & Canada)']
- twz = zone.parse('2012-03-11 02:29:00')
- assert_equal [0, 29, 3, 11, 3, 2012], twz.to_a[0,6]
+ with_env_tz("EET") do
+ zone = ActiveSupport::TimeZone["Pacific Time (US & Canada)"]
+ twz = zone.parse("2012-03-11 02:29:00")
+ assert_equal [0, 29, 3, 11, 3, 2012], twz.to_a[0, 6]
end
end
def test_parse_with_missing_time_components
- zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
zone.stub(:now, zone.local(1999, 12, 31, 12, 59, 59)) do
- twz = zone.parse('2012-12-01')
+ twz = zone.parse("2012-12-01")
assert_equal Time.utc(2012, 12, 1), twz.time
end
end
def test_parse_with_javascript_date
- zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
twz = zone.parse("Mon May 28 2012 00:00:00 GMT-0700 (PDT)")
assert_equal Time.utc(2012, 5, 28, 7, 0, 0), twz.utc
end
def test_parse_doesnt_use_local_dst
- with_env_tz 'US/Eastern' do
- zone = ActiveSupport::TimeZone['UTC']
- twz = zone.parse('2013-03-10 02:00:00')
+ with_env_tz "US/Eastern" do
+ zone = ActiveSupport::TimeZone["UTC"]
+ twz = zone.parse("2013-03-10 02:00:00")
assert_equal Time.utc(2013, 3, 10, 2, 0, 0), twz.time
end
end
def test_parse_handles_dst_jump
- with_env_tz 'US/Eastern' do
- zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
- twz = zone.parse('2013-03-10 02:00:00')
+ with_env_tz "US/Eastern" do
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ twz = zone.parse("2013-03-10 02:00:00")
+ assert_equal Time.utc(2013, 3, 10, 3, 0, 0), twz.time
+ end
+ end
+
+ def test_parse_with_invalid_date
+ zone = ActiveSupport::TimeZone["UTC"]
+
+ exception = assert_raises(ArgumentError) do
+ zone.parse("9000")
+ end
+
+ assert_equal "argument out of range", exception.message
+ end
+
+ def test_parse_with_ambiguous_time
+ zone = ActiveSupport::TimeZone["Moscow"]
+ assert_equal Time.utc(2014, 10, 25, 22, 0, 0), zone.parse("2014-10-26 01:00:00")
+ end
+
+ def test_rfc3339
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ twz = zone.rfc3339("1999-12-31T14:00:00-10:00")
+ 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')
- assert_equal Time.utc(1999,12,31,17), twz
- assert_equal Time.utc(1999,12,31,12), twz.time
- assert_equal Time.utc(1999,12,31,17), twz.utc
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ twz = zone.strptime("1999-12-31 12:00:00", "%Y-%m-%d %H:%M:%S")
+ assert_equal Time.utc(1999, 12, 31, 17), twz
+ assert_equal Time.utc(1999, 12, 31, 12), twz.time
+ assert_equal Time.utc(1999, 12, 31, 17), twz.utc
assert_equal zone, twz.time_zone
end
def test_strptime_with_nondefault_time_zone
- with_tz_default ActiveSupport::TimeZone['Pacific Time (US & Canada)'] do
- zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
- twz = zone.strptime('1999-12-31 12:00:00', '%Y-%m-%d %H:%M:%S')
- assert_equal Time.utc(1999,12,31,17), twz
- assert_equal Time.utc(1999,12,31,12), twz.time
- assert_equal Time.utc(1999,12,31,17), twz.utc
+ with_tz_default ActiveSupport::TimeZone["Pacific Time (US & Canada)"] do
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ twz = zone.strptime("1999-12-31 12:00:00", "%Y-%m-%d %H:%M:%S")
+ assert_equal Time.utc(1999, 12, 31, 17), twz
+ assert_equal Time.utc(1999, 12, 31, 12), twz.time
+ assert_equal Time.utc(1999, 12, 31, 17), twz.utc
assert_equal zone, twz.time_zone
end
end
def test_strptime_with_explicit_time_zone_as_abbrev
- zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
- twz = zone.strptime('1999-12-31 12:00:00 PST', '%Y-%m-%d %H:%M:%S %Z')
- assert_equal Time.utc(1999,12,31,20), twz
- assert_equal Time.utc(1999,12,31,15), twz.time
- assert_equal Time.utc(1999,12,31,20), twz.utc
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ twz = zone.strptime("1999-12-31 12:00:00 PST", "%Y-%m-%d %H:%M:%S %Z")
+ assert_equal Time.utc(1999, 12, 31, 20), twz
+ assert_equal Time.utc(1999, 12, 31, 15), twz.time
+ assert_equal Time.utc(1999, 12, 31, 20), twz.utc
assert_equal zone, twz.time_zone
end
def test_strptime_with_explicit_time_zone_as_h_offset
- zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
- twz = zone.strptime('1999-12-31 12:00:00 -08', '%Y-%m-%d %H:%M:%S %:::z')
- assert_equal Time.utc(1999,12,31,20), twz
- assert_equal Time.utc(1999,12,31,15), twz.time
- assert_equal Time.utc(1999,12,31,20), twz.utc
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ twz = zone.strptime("1999-12-31 12:00:00 -08", "%Y-%m-%d %H:%M:%S %:::z")
+ assert_equal Time.utc(1999, 12, 31, 20), twz
+ assert_equal Time.utc(1999, 12, 31, 15), twz.time
+ assert_equal Time.utc(1999, 12, 31, 20), twz.utc
assert_equal zone, twz.time_zone
end
def test_strptime_with_explicit_time_zone_as_hm_offset
- zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
- twz = zone.strptime('1999-12-31 12:00:00 -08:00', '%Y-%m-%d %H:%M:%S %:z')
- assert_equal Time.utc(1999,12,31,20), twz
- assert_equal Time.utc(1999,12,31,15), twz.time
- assert_equal Time.utc(1999,12,31,20), twz.utc
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ twz = zone.strptime("1999-12-31 12:00:00 -08:00", "%Y-%m-%d %H:%M:%S %:z")
+ assert_equal Time.utc(1999, 12, 31, 20), twz
+ assert_equal Time.utc(1999, 12, 31, 15), twz.time
+ assert_equal Time.utc(1999, 12, 31, 20), twz.utc
assert_equal zone, twz.time_zone
end
def test_strptime_with_explicit_time_zone_as_hms_offset
- zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
- twz = zone.strptime('1999-12-31 12:00:00 -08:00:00', '%Y-%m-%d %H:%M:%S %::z')
- assert_equal Time.utc(1999,12,31,20), twz
- assert_equal Time.utc(1999,12,31,15), twz.time
- assert_equal Time.utc(1999,12,31,20), twz.utc
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ twz = zone.strptime("1999-12-31 12:00:00 -08:00:00", "%Y-%m-%d %H:%M:%S %::z")
+ assert_equal Time.utc(1999, 12, 31, 20), twz
+ assert_equal Time.utc(1999, 12, 31, 15), twz.time
+ assert_equal Time.utc(1999, 12, 31, 20), twz.utc
assert_equal zone, twz.time_zone
end
def test_strptime_with_almost_explicit_time_zone
- zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
- twz = zone.strptime('1999-12-31 12:00:00 %Z', '%Y-%m-%d %H:%M:%S %%Z')
- assert_equal Time.utc(1999,12,31,17), twz
- assert_equal Time.utc(1999,12,31,12), twz.time
- assert_equal Time.utc(1999,12,31,17), twz.utc
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ twz = zone.strptime("1999-12-31 12:00:00 %Z", "%Y-%m-%d %H:%M:%S %%Z")
+ assert_equal Time.utc(1999, 12, 31, 17), twz
+ assert_equal Time.utc(1999, 12, 31, 12), twz.time
+ assert_equal Time.utc(1999, 12, 31, 17), twz.utc
assert_equal zone, twz.time_zone
end
def test_strptime_with_day_omitted
- with_env_tz 'US/Eastern' do
- zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
- assert_equal Time.local(2000, 2, 1), zone.strptime('Feb', '%b', Time.local(2000, 1, 1))
- assert_equal Time.local(2005, 2, 1), zone.strptime('Feb 2005', '%b %Y', Time.local(2000, 1, 1))
- assert_equal Time.local(2005, 2, 2), zone.strptime('2 Feb 2005', '%e %b %Y', Time.local(2000, 1, 1))
+ with_env_tz "US/Eastern" do
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ assert_equal Time.local(2000, 2, 1), zone.strptime("Feb", "%b", Time.local(2000, 1, 1))
+ assert_equal Time.local(2005, 2, 1), zone.strptime("Feb 2005", "%b %Y", Time.local(2000, 1, 1))
+ assert_equal Time.local(2005, 2, 2), zone.strptime("2 Feb 2005", "%e %b %Y", Time.local(2000, 1, 1))
+ end
+ end
+
+ def test_strptime_with_malformed_string
+ with_env_tz "US/Eastern" do
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ assert_raise(ArgumentError) { zone.strptime("1999-12-31", "%Y/%m/%d") }
+ end
+ end
+
+ def test_strptime_with_timestamp_seconds
+ with_env_tz "US/Eastern" do
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ time_str = "1470272280"
+ time = zone.strptime(time_str, "%s")
+ assert_equal Time.at(1470272280), time
+ end
+ end
+
+ def test_strptime_with_timestamp_milliseconds
+ with_env_tz "US/Eastern" do
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ time_str = "1470272280000"
+ time = zone.strptime(time_str, "%Q")
+ assert_equal Time.at(1470272280), time
end
end
+ def test_strptime_with_ambiguous_time
+ zone = ActiveSupport::TimeZone["Moscow"]
+ assert_equal Time.utc(2014, 10, 25, 22, 0, 0), zone.strptime("2014-10-26 01:00:00", "%Y-%m-%d %H:%M:%S")
+ end
+
def test_utc_offset_lazy_loaded_from_tzinfo_when_not_passed_in_to_initialize
- tzinfo = TZInfo::Timezone.get('America/New_York')
+ 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
+ def test_utc_offset_is_not_cached_when_current_period_gets_stale
+ tz = ActiveSupport::TimeZone.create("Moscow")
+ travel_to(Time.utc(2014, 10, 25, 21)) do # 1 hour before TZ change
+ assert_equal 14400, tz.utc_offset, "utc_offset should be initialized according to current_period"
+ end
+
+ travel_to(Time.utc(2014, 10, 25, 22)) do # after TZ change
+ assert_equal 10800, tz.utc_offset, "utc_offset should not be cached when current_period gets stale"
+ end
+ end
+
def test_seconds_to_utc_offset_with_colon
assert_equal "-06:00", ActiveSupport::TimeZone.seconds_to_utc_offset(-21_600)
assert_equal "+00:00", ActiveSupport::TimeZone.seconds_to_utc_offset(0)
@@ -414,54 +677,76 @@ class TimeZoneTest < ActiveSupport::TestCase
end
def test_formatted_offset_positive
- zone = ActiveSupport::TimeZone['New Delhi']
+ zone = ActiveSupport::TimeZone["New Delhi"]
assert_equal "+05:30", zone.formatted_offset
assert_equal "+0530", zone.formatted_offset(false)
end
def test_formatted_offset_negative
- zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
assert_equal "-05:00", zone.formatted_offset
assert_equal "-0500", zone.formatted_offset(false)
end
def test_z_format_strings
- zone = ActiveSupport::TimeZone['Tokyo']
+ zone = ActiveSupport::TimeZone["Tokyo"]
twz = zone.now
- assert_equal '+0900', twz.strftime('%z')
- assert_equal '+09:00', twz.strftime('%:z')
- assert_equal '+09:00:00', twz.strftime('%::z')
+ assert_equal "+0900", twz.strftime("%z")
+ assert_equal "+09:00", twz.strftime("%:z")
+ assert_equal "+09:00:00", twz.strftime("%::z")
end
def test_formatted_offset_zero
- zone = ActiveSupport::TimeZone['London']
+ zone = ActiveSupport::TimeZone["London"]
assert_equal "+00:00", zone.formatted_offset
- assert_equal "UTC", zone.formatted_offset(true, 'UTC')
+ assert_equal "UTC", zone.formatted_offset(true, "UTC")
end
def test_zone_compare
- zone1 = ActiveSupport::TimeZone['Central Time (US & Canada)'] # offset -0600
- zone2 = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] # offset -0500
+ zone1 = ActiveSupport::TimeZone["Central Time (US & Canada)"] # offset -0600
+ zone2 = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] # offset -0500
assert zone1 < zone2
assert zone2 > zone1
assert zone1 == zone1
end
def test_zone_match
- zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
assert zone =~ /Eastern/
assert zone =~ /New_York/
assert zone !~ /Nonexistent_Place/
end
def test_to_s
- assert_equal "(GMT+05:30) New Delhi", ActiveSupport::TimeZone['New Delhi'].to_s
+ assert_equal "(GMT+05:30) New Delhi", ActiveSupport::TimeZone["New Delhi"].to_s
end
def test_all_sorted
all = ActiveSupport::TimeZone.all
- 1.upto( all.length-1 ) do |i|
- assert all[i-1] < all[i]
+ 1.upto(all.length - 1) do |i|
+ assert all[i - 1] < all[i]
+ end
+ end
+
+ def test_all_uninfluenced_by_time_zone_lookups_delegated_to_tzinfo
+ ActiveSupport::TimeZone.clear
+ galapagos = ActiveSupport::TimeZone["Pacific/Galapagos"]
+ all_zones = ActiveSupport::TimeZone.all
+ assert_not_includes all_zones, galapagos
+ end
+
+ def test_all_doesnt_raise_exception_with_missing_tzinfo_data
+ mappings = {
+ "Puerto Rico" => "America/Unknown",
+ "Pittsburgh" => "America/New_York"
+ }
+
+ with_tz_mappings(mappings) do
+ assert_nil ActiveSupport::TimeZone["Puerto Rico"]
+ assert_nil ActiveSupport::TimeZone[-9]
+ assert_nothing_raised do
+ ActiveSupport::TimeZone.all
+ end
end
end
@@ -487,8 +772,27 @@ class TimeZoneTest < ActiveSupport::TestCase
end
def test_us_zones
- assert ActiveSupport::TimeZone.us_zones.include?(ActiveSupport::TimeZone["Hawaii"])
- assert !ActiveSupport::TimeZone.us_zones.include?(ActiveSupport::TimeZone["Kuala Lumpur"])
+ assert_includes ActiveSupport::TimeZone.us_zones, ActiveSupport::TimeZone["Hawaii"]
+ assert_not_includes ActiveSupport::TimeZone.us_zones, ActiveSupport::TimeZone["Kuala Lumpur"]
+ end
+
+ def test_country_zones
+ assert_includes ActiveSupport::TimeZone.country_zones("ru"), ActiveSupport::TimeZone["Moscow"]
+ assert_not_includes ActiveSupport::TimeZone.country_zones(:ru), ActiveSupport::TimeZone["Kuala Lumpur"]
+ end
+
+ def test_country_zones_with_and_without_mappings
+ assert_includes ActiveSupport::TimeZone.country_zones("au"), ActiveSupport::TimeZone["Adelaide"]
+ assert_includes ActiveSupport::TimeZone.country_zones("au"), ActiveSupport::TimeZone["Australia/Lord_Howe"]
+ end
+
+ def test_country_zones_with_multiple_mappings
+ assert_includes ActiveSupport::TimeZone.country_zones("gb"), ActiveSupport::TimeZone["Edinburgh"]
+ assert_includes ActiveSupport::TimeZone.country_zones("gb"), ActiveSupport::TimeZone["London"]
+ end
+
+ def test_country_zones_without_mappings
+ assert_includes ActiveSupport::TimeZone.country_zones(:sv), ActiveSupport::TimeZone["America/El_Salvador"]
end
def test_to_yaml
diff --git a/activesupport/test/time_zone_test_helpers.rb b/activesupport/test/time_zone_test_helpers.rb
index 9632b89d09..85ed727c9b 100644
--- a/activesupport/test/time_zone_test_helpers.rb
+++ b/activesupport/test/time_zone_test_helpers.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module TimeZoneTestHelpers
def with_tz_default(tz = nil)
old_tz = Time.zone
@@ -7,10 +9,31 @@ module TimeZoneTestHelpers
Time.zone = old_tz
end
- def with_env_tz(new_tz = 'US/Eastern')
- old_tz, ENV['TZ'] = ENV['TZ'], new_tz
+ def with_env_tz(new_tz = "US/Eastern")
+ old_tz, ENV["TZ"] = ENV["TZ"], new_tz
+ yield
+ ensure
+ old_tz ? ENV["TZ"] = old_tz : ENV.delete("TZ")
+ end
+
+ def with_preserve_timezone(value)
+ old_preserve_tz = ActiveSupport.to_time_preserves_timezone
+ ActiveSupport.to_time_preserves_timezone = value
+ yield
+ ensure
+ ActiveSupport.to_time_preserves_timezone = old_preserve_tz
+ end
+
+ def with_tz_mappings(mappings)
+ old_mappings = ActiveSupport::TimeZone::MAPPING.dup
+ ActiveSupport::TimeZone.clear
+ ActiveSupport::TimeZone::MAPPING.clear
+ ActiveSupport::TimeZone::MAPPING.merge!(mappings)
+
yield
ensure
- old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
+ ActiveSupport::TimeZone.clear
+ ActiveSupport::TimeZone::MAPPING.clear
+ ActiveSupport::TimeZone::MAPPING.merge!(old_mappings)
end
end
diff --git a/activesupport/test/transliterate_test.rb b/activesupport/test/transliterate_test.rb
index 378421fedd..7d19447598 100644
--- a/activesupport/test/transliterate_test.rb
+++ b/activesupport/test/transliterate_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'active_support/inflector/transliterate'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/inflector/transliterate"
class TransliterateTest < ActiveSupport::TestCase
def test_transliterate_should_not_change_ascii_chars
@@ -13,7 +15,7 @@ class TransliterateTest < ActiveSupport::TestCase
# create string with range of Unicode's western characters with
# diacritics, excluding the division and multiplication signs which for
# some reason or other are floating in the middle of all the letters.
- string = (0xC0..0x17E).to_a.reject {|c| [0xD7, 0xF7].include?(c)}.pack("U*")
+ string = (0xC0..0x17E).to_a.reject { |c| [0xD7, 0xF7].include?(c) }.pack("U*")
string.each_char do |char|
assert_match %r{^[a-zA-Z']*$}, ActiveSupport::Inflector.transliterate(char)
end
@@ -21,7 +23,7 @@ class TransliterateTest < ActiveSupport::TestCase
def test_transliterate_should_work_with_custom_i18n_rules_and_uncomposed_utf8
char = [117, 776].pack("U*") # "ü" as ASCII "u" plus COMBINING DIAERESIS
- I18n.backend.store_translations(:de, :i18n => {:transliterate => {:rule => {"ü" => "ue"}}})
+ I18n.backend.store_translations(:de, i18n: { transliterate: { rule: { "ü" => "ue" } } })
default_locale, I18n.locale = I18n.locale, :de
assert_equal "ue", ActiveSupport::Inflector.transliterate(char)
ensure
@@ -31,4 +33,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/activesupport/test/xml_mini/jdom_engine_test.rb b/activesupport/test/xml_mini/jdom_engine_test.rb
index ed4de8aba2..97a533aafb 100644
--- a/activesupport/test/xml_mini/jdom_engine_test.rb
+++ b/activesupport/test/xml_mini/jdom_engine_test.rb
@@ -1,37 +1,10 @@
-if RUBY_PLATFORM =~ /java/
- require 'abstract_unit'
- require 'active_support/xml_mini'
- require 'active_support/core_ext/hash/conversions'
+# frozen_string_literal: true
+require_relative "xml_mini_engine_test"
- class JDOMEngineTest < ActiveSupport::TestCase
- include ActiveSupport
-
- FILES_DIR = File.dirname(__FILE__) + '/../fixtures/xml'
-
- def setup
- @default_backend = XmlMini.backend
- XmlMini.backend = 'JDOM'
- end
-
- def teardown
- XmlMini.backend = @default_backend
- end
-
- def test_file_from_xml
- hash = Hash.from_xml(<<-eoxml)
- <blog>
- <logo type="file" name="logo.png" content_type="image/png">
- </logo>
- </blog>
- eoxml
- assert hash.has_key?('blog')
- assert hash['blog'].has_key?('logo')
-
- file = hash['blog']['logo']
- assert_equal 'logo.png', file.original_filename
- assert_equal 'image/png', file.content_type
- end
+XMLMiniEngineTest.run_with_platform("java") do
+ class JDOMEngineTest < XMLMiniEngineTest
+ FILES_DIR = File.expand_path("../fixtures/xml", __dir__)
def test_not_allowed_to_expand_entities_to_files
attack_xml = <<-EOT
@@ -40,7 +13,7 @@ if RUBY_PLATFORM =~ /java/
]>
<member>x&a;</member>
EOT
- assert_equal 'x', Hash.from_xml(attack_xml)["member"]
+ assert_equal "x", Hash.from_xml(attack_xml)["member"]
end
def test_not_allowed_to_expand_parameter_entities_to_files
@@ -52,137 +25,29 @@ if RUBY_PLATFORM =~ /java/
<member>x&a;</member>
EOT
assert_raise Java::OrgXmlSax::SAXParseException do
- assert_equal 'x', Hash.from_xml(attack_xml)["member"]
+ assert_equal "x", Hash.from_xml(attack_xml)["member"]
end
end
-
def test_not_allowed_to_load_external_doctypes
attack_xml = <<-EOT
<!DOCTYPE member SYSTEM "file://#{FILES_DIR}/jdom_doctype.dtd">
<member>x&a;</member>
EOT
- assert_equal 'x', Hash.from_xml(attack_xml)["member"]
+ assert_equal "x", Hash.from_xml(attack_xml)["member"]
end
- def test_exception_thrown_on_expansion_attack
- assert_raise Java::OrgXmlSax::SAXParseException do
- attack_xml = <<-EOT
- <!DOCTYPE member [
- <!ENTITY a "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;">
- <!ENTITY b "&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;">
- <!ENTITY c "&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;">
- <!ENTITY d "&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;">
- <!ENTITY e "&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;">
- <!ENTITY f "&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;">
- <!ENTITY g "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx">
- ]>
- <member>
- &a;
- </member>
- EOT
- Hash.from_xml(attack_xml)
+ private
+ def engine
+ "JDOM"
end
- end
-
- def test_setting_JDOM_as_backend
- XmlMini.backend = 'JDOM'
- assert_equal XmlMini_JDOM, XmlMini.backend
- end
-
- def test_blank_returns_empty_hash
- assert_equal({}, XmlMini.parse(nil))
- assert_equal({}, XmlMini.parse(''))
- end
-
- def test_array_type_makes_an_array
- assert_equal_rexml(<<-eoxml)
- <blog>
- <posts type="array">
- <post>a post</post>
- <post>another post</post>
- </posts>
- </blog>
- eoxml
- end
-
- def test_one_node_document_as_hash
- assert_equal_rexml(<<-eoxml)
- <products/>
- eoxml
- end
-
- def test_one_node_with_attributes_document_as_hash
- assert_equal_rexml(<<-eoxml)
- <products foo="bar"/>
- eoxml
- end
- def test_products_node_with_book_node_as_hash
- assert_equal_rexml(<<-eoxml)
- <products>
- <book name="awesome" id="12345" />
- </products>
- eoxml
- end
-
- def test_products_node_with_two_book_nodes_as_hash
- assert_equal_rexml(<<-eoxml)
- <products>
- <book name="awesome" id="12345" />
- <book name="america" id="67890" />
- </products>
- eoxml
- end
-
- def test_single_node_with_content_as_hash
- assert_equal_rexml(<<-eoxml)
- <products>
- hello world
- </products>
- eoxml
- end
-
- def test_children_with_children
- assert_equal_rexml(<<-eoxml)
- <root>
- <products>
- <book name="america" id="67890" />
- </products>
- </root>
- eoxml
- end
-
- def test_children_with_text
- assert_equal_rexml(<<-eoxml)
- <root>
- <products>
- hello everyone
- </products>
- </root>
- eoxml
- end
-
- def test_children_with_non_adjacent_text
- assert_equal_rexml(<<-eoxml)
- <root>
- good
- <products>
- hello everyone
- </products>
- morning
- </root>
- eoxml
- end
+ def expansion_attack_error
+ Java::OrgXmlSax::SAXParseException
+ end
- private
- def assert_equal_rexml(xml)
- parsed_xml = XmlMini.parse(xml)
- hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) }
- assert_equal(hash, parsed_xml)
+ def extended_engine?
+ false
end
end
-
-else
- # don't run these test because we aren't running in JRuby
end
diff --git a/activesupport/test/xml_mini/libxml_engine_test.rb b/activesupport/test/xml_mini/libxml_engine_test.rb
index a8df2e1f7b..3eef3946a3 100644
--- a/activesupport/test/xml_mini/libxml_engine_test.rb
+++ b/activesupport/test/xml_mini/libxml_engine_test.rb
@@ -1,204 +1,21 @@
-begin
- require 'libxml'
-rescue LoadError
- # Skip libxml tests
-else
-require 'abstract_unit'
-require 'active_support/xml_mini'
-require 'active_support/core_ext/hash/conversions'
+# frozen_string_literal: true
-class LibxmlEngineTest < ActiveSupport::TestCase
- include ActiveSupport
+require_relative "xml_mini_engine_test"
- def setup
- @default_backend = XmlMini.backend
- XmlMini.backend = 'LibXML'
-
- LibXML::XML::Error.set_handler(&lambda { |error| }) #silence libxml, exceptions will do
- end
-
- def teardown
- XmlMini.backend = @default_backend
- end
-
- def test_exception_thrown_on_expansion_attack
- assert_raise LibXML::XML::Error do
- attack_xml = %{<?xml version="1.0" encoding="UTF-8"?>
- <!DOCTYPE member [
- <!ENTITY a "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;">
- <!ENTITY b "&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;">
- <!ENTITY c "&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;">
- <!ENTITY d "&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;">
- <!ENTITY e "&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;">
- <!ENTITY f "&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;">
- <!ENTITY g "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx">
- ]>
- <member>
- &a;
- </member>
- }
- Hash.from_xml(attack_xml)
+XMLMiniEngineTest.run_with_gem("libxml") do
+ class LibxmlEngineTest < XMLMiniEngineTest
+ def setup
+ super
+ LibXML::XML::Error.set_handler(&lambda { |error| }) # silence libxml, exceptions will do
end
- end
-
- def test_setting_libxml_as_backend
- XmlMini.backend = 'LibXML'
- assert_equal XmlMini_LibXML, XmlMini.backend
- end
-
- def test_blank_returns_empty_hash
- assert_equal({}, XmlMini.parse(nil))
- assert_equal({}, XmlMini.parse(''))
- end
-
- def test_array_type_makes_an_array
- assert_equal_rexml(<<-eoxml)
- <blog>
- <posts type="array">
- <post>a post</post>
- <post>another post</post>
- </posts>
- </blog>
- eoxml
- end
-
- def test_one_node_document_as_hash
- assert_equal_rexml(<<-eoxml)
- <products/>
- eoxml
- end
-
- def test_one_node_with_attributes_document_as_hash
- assert_equal_rexml(<<-eoxml)
- <products foo="bar"/>
- eoxml
- end
-
- def test_products_node_with_book_node_as_hash
- assert_equal_rexml(<<-eoxml)
- <products>
- <book name="awesome" id="12345" />
- </products>
- eoxml
- end
-
- def test_products_node_with_two_book_nodes_as_hash
- assert_equal_rexml(<<-eoxml)
- <products>
- <book name="awesome" id="12345" />
- <book name="america" id="67890" />
- </products>
- eoxml
- end
-
- def test_single_node_with_content_as_hash
- assert_equal_rexml(<<-eoxml)
- <products>
- hello world
- </products>
- eoxml
- end
-
- def test_children_with_children
- assert_equal_rexml(<<-eoxml)
- <root>
- <products>
- <book name="america" id="67890" />
- </products>
- </root>
- eoxml
- end
- def test_children_with_text
- assert_equal_rexml(<<-eoxml)
- <root>
- <products>
- hello everyone
- </products>
- </root>
- eoxml
- end
-
- def test_children_with_non_adjacent_text
- assert_equal_rexml(<<-eoxml)
- <root>
- good
- <products>
- hello everyone
- </products>
- morning
- </root>
- eoxml
- end
+ private
+ def engine
+ "LibXML"
+ end
- def test_parse_from_io
- io = StringIO.new(<<-eoxml)
- <root>
- good
- <products>
- hello everyone
- </products>
- morning
- </root>
- eoxml
- assert_equal_rexml(io)
+ def expansion_attack_error
+ LibXML::XML::Error
+ end
end
-
- def test_children_with_simple_cdata
- assert_equal_rexml(<<-eoxml)
- <root>
- <products>
- <![CDATA[cdatablock]]>
- </products>
- </root>
- eoxml
- end
-
- def test_children_with_multiple_cdata
- assert_equal_rexml(<<-eoxml)
- <root>
- <products>
- <![CDATA[cdatablock1]]><![CDATA[cdatablock2]]>
- </products>
- </root>
- eoxml
- end
-
- def test_children_with_text_and_cdata
- assert_equal_rexml(<<-eoxml)
- <root>
- <products>
- hello <![CDATA[cdatablock]]>
- morning
- </products>
- </root>
- eoxml
- end
-
- def test_children_with_blank_text
- assert_equal_rexml(<<-eoxml)
- <root>
- <products> </products>
- </root>
- eoxml
- end
-
- def test_children_with_blank_text_and_attribute
- assert_equal_rexml(<<-eoxml)
- <root>
- <products type="file"> </products>
- </root>
- eoxml
- end
-
-
- private
- def assert_equal_rexml(xml)
- parsed_xml = XmlMini.parse(xml)
- xml.rewind if xml.respond_to?(:rewind)
- hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) }
- assert_equal(hash, parsed_xml)
- end
-end
-
end
diff --git a/activesupport/test/xml_mini/libxmlsax_engine_test.rb b/activesupport/test/xml_mini/libxmlsax_engine_test.rb
index d6d90639e2..8e4a30a48e 100644
--- a/activesupport/test/xml_mini/libxmlsax_engine_test.rb
+++ b/activesupport/test/xml_mini/libxmlsax_engine_test.rb
@@ -1,195 +1,16 @@
-begin
- require 'libxml'
-rescue LoadError
- # Skip libxml tests
-else
-require 'abstract_unit'
-require 'active_support/xml_mini'
-require 'active_support/core_ext/hash/conversions'
+# frozen_string_literal: true
-class LibXMLSAXEngineTest < ActiveSupport::TestCase
- include ActiveSupport
+require_relative "xml_mini_engine_test"
- def setup
- @default_backend = XmlMini.backend
- XmlMini.backend = 'LibXMLSAX'
- end
-
- def teardown
- XmlMini.backend = @default_backend
- end
-
- def test_exception_thrown_on_expansion_attack
- assert_raise LibXML::XML::Error do
- attack_xml = <<-EOT
- <?xml version="1.0" encoding="UTF-8"?>
- <!DOCTYPE member [
- <!ENTITY a "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;">
- <!ENTITY b "&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;">
- <!ENTITY c "&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;">
- <!ENTITY d "&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;">
- <!ENTITY e "&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;">
- <!ENTITY f "&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;">
- <!ENTITY g "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx">
- ]>
- <member>
- &a;
- </member>
- EOT
-
- Hash.from_xml(attack_xml)
- end
- end
-
- def test_setting_libxml_as_backend
- XmlMini.backend = 'LibXMLSAX'
- assert_equal XmlMini_LibXMLSAX, XmlMini.backend
- end
-
- def test_blank_returns_empty_hash
- assert_equal({}, XmlMini.parse(nil))
- assert_equal({}, XmlMini.parse(''))
- end
-
- def test_array_type_makes_an_array
- assert_equal_rexml(<<-eoxml)
- <blog>
- <posts type="array">
- <post>a post</post>
- <post>another post</post>
- </posts>
- </blog>
- eoxml
- end
-
- def test_one_node_document_as_hash
- assert_equal_rexml(<<-eoxml)
- <products/>
- eoxml
- end
-
- def test_one_node_with_attributes_document_as_hash
- assert_equal_rexml(<<-eoxml)
- <products foo="bar"/>
- eoxml
- end
-
- def test_products_node_with_book_node_as_hash
- assert_equal_rexml(<<-eoxml)
- <products>
- <book name="awesome" id="12345" />
- </products>
- eoxml
- end
-
- def test_products_node_with_two_book_nodes_as_hash
- assert_equal_rexml(<<-eoxml)
- <products>
- <book name="awesome" id="12345" />
- <book name="america" id="67890" />
- </products>
- eoxml
- end
+XMLMiniEngineTest.run_with_gem("libxml") do
+ class LibXMLSAXEngineTest < XMLMiniEngineTest
+ private
+ def engine
+ "LibXMLSAX"
+ end
- def test_single_node_with_content_as_hash
- assert_equal_rexml(<<-eoxml)
- <products>
- hello world
- </products>
- eoxml
+ def expansion_attack_error
+ LibXML::XML::Error
+ end
end
-
- def test_children_with_children
- assert_equal_rexml(<<-eoxml)
- <root>
- <products>
- <book name="america" id="67890" />
- </products>
- </root>
- eoxml
- end
-
- def test_children_with_text
- assert_equal_rexml(<<-eoxml)
- <root>
- <products>
- hello everyone
- </products>
- </root>
- eoxml
- end
-
- def test_children_with_non_adjacent_text
- assert_equal_rexml(<<-eoxml)
- <root>
- good
- <products>
- hello everyone
- </products>
- morning
- </root>
- eoxml
- end
-
- def test_parse_from_io
- io = StringIO.new(<<-eoxml)
- <root>
- good
- <products>
- hello everyone
- </products>
- morning
- </root>
- eoxml
- assert_equal_rexml(io)
- end
-
- def test_children_with_simple_cdata
- assert_equal_rexml(<<-eoxml)
- <root>
- <products>
- <![CDATA[cdatablock]]>
- </products>
- </root>
- eoxml
- end
-
- def test_children_with_multiple_cdata
- assert_equal_rexml(<<-eoxml)
- <root>
- <products>
- <![CDATA[cdatablock1]]><![CDATA[cdatablock2]]>
- </products>
- </root>
- eoxml
- end
-
- def test_children_with_text_and_cdata
- assert_equal_rexml(<<-eoxml)
- <root>
- <products>
- hello <![CDATA[cdatablock]]>
- morning
- </products>
- </root>
- eoxml
- end
-
- def test_children_with_blank_text
- assert_equal_rexml(<<-eoxml)
- <root>
- <products> </products>
- </root>
- eoxml
- end
-
- private
- def assert_equal_rexml(xml)
- parsed_xml = XmlMini.parse(xml)
- xml.rewind if xml.respond_to?(:rewind)
- hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) }
- assert_equal(hash, parsed_xml)
- end
-end
-
end
diff --git a/activesupport/test/xml_mini/nokogiri_engine_test.rb b/activesupport/test/xml_mini/nokogiri_engine_test.rb
index 1314c9065a..f1584bcedf 100644
--- a/activesupport/test/xml_mini/nokogiri_engine_test.rb
+++ b/activesupport/test/xml_mini/nokogiri_engine_test.rb
@@ -1,215 +1,16 @@
-begin
- require 'nokogiri'
-rescue LoadError
- # Skip nokogiri tests
-else
-require 'abstract_unit'
-require 'active_support/xml_mini'
-require 'active_support/core_ext/hash/conversions'
+# frozen_string_literal: true
-class NokogiriEngineTest < ActiveSupport::TestCase
- def setup
- @default_backend = ActiveSupport::XmlMini.backend
- ActiveSupport::XmlMini.backend = 'Nokogiri'
- end
-
- def teardown
- ActiveSupport::XmlMini.backend = @default_backend
- end
-
- def test_file_from_xml
- hash = Hash.from_xml(<<-eoxml)
- <blog>
- <logo type="file" name="logo.png" content_type="image/png">
- </logo>
- </blog>
- eoxml
- assert hash.has_key?('blog')
- assert hash['blog'].has_key?('logo')
-
- file = hash['blog']['logo']
- assert_equal 'logo.png', file.original_filename
- assert_equal 'image/png', file.content_type
- end
-
- def test_exception_thrown_on_expansion_attack
- assert_raise Nokogiri::XML::SyntaxError do
- attack_xml = <<-EOT
- <?xml version="1.0" encoding="UTF-8"?>
- <!DOCTYPE member [
- <!ENTITY a "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;">
- <!ENTITY b "&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;">
- <!ENTITY c "&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;">
- <!ENTITY d "&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;">
- <!ENTITY e "&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;">
- <!ENTITY f "&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;">
- <!ENTITY g "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx">
- ]>
- <member>
- &a;
- </member>
- EOT
- Hash.from_xml(attack_xml)
- end
- end
-
- def test_setting_nokogiri_as_backend
- ActiveSupport::XmlMini.backend = 'Nokogiri'
- assert_equal ActiveSupport::XmlMini_Nokogiri, ActiveSupport::XmlMini.backend
- end
-
- def test_blank_returns_empty_hash
- assert_equal({}, ActiveSupport::XmlMini.parse(nil))
- assert_equal({}, ActiveSupport::XmlMini.parse(''))
- end
-
- def test_array_type_makes_an_array
- assert_equal_rexml(<<-eoxml)
- <blog>
- <posts type="array">
- <post>a post</post>
- <post>another post</post>
- </posts>
- </blog>
- eoxml
- end
-
- def test_one_node_document_as_hash
- assert_equal_rexml(<<-eoxml)
- <products/>
- eoxml
- end
-
- def test_one_node_with_attributes_document_as_hash
- assert_equal_rexml(<<-eoxml)
- <products foo="bar"/>
- eoxml
- end
-
- def test_products_node_with_book_node_as_hash
- assert_equal_rexml(<<-eoxml)
- <products>
- <book name="awesome" id="12345" />
- </products>
- eoxml
- end
-
- def test_products_node_with_two_book_nodes_as_hash
- assert_equal_rexml(<<-eoxml)
- <products>
- <book name="awesome" id="12345" />
- <book name="america" id="67890" />
- </products>
- eoxml
- end
-
- def test_single_node_with_content_as_hash
- assert_equal_rexml(<<-eoxml)
- <products>
- hello world
- </products>
- eoxml
- end
+require_relative "xml_mini_engine_test"
- def test_children_with_children
- assert_equal_rexml(<<-eoxml)
- <root>
- <products>
- <book name="america" id="67890" />
- </products>
- </root>
- eoxml
- end
-
- def test_children_with_text
- assert_equal_rexml(<<-eoxml)
- <root>
- <products>
- hello everyone
- </products>
- </root>
- eoxml
- end
+XMLMiniEngineTest.run_with_gem("nokogiri") do
+ class NokogiriEngineTest < XMLMiniEngineTest
+ private
+ def engine
+ "Nokogiri"
+ end
- def test_children_with_non_adjacent_text
- assert_equal_rexml(<<-eoxml)
- <root>
- good
- <products>
- hello everyone
- </products>
- morning
- </root>
- eoxml
+ def expansion_attack_error
+ Nokogiri::XML::SyntaxError
+ end
end
-
- def test_parse_from_io
- io = StringIO.new(<<-eoxml)
- <root>
- good
- <products>
- hello everyone
- </products>
- morning
- </root>
- eoxml
- assert_equal_rexml(io)
- end
-
- def test_children_with_simple_cdata
- assert_equal_rexml(<<-eoxml)
- <root>
- <products>
- <![CDATA[cdatablock]]>
- </products>
- </root>
- eoxml
- end
-
- def test_children_with_multiple_cdata
- assert_equal_rexml(<<-eoxml)
- <root>
- <products>
- <![CDATA[cdatablock1]]><![CDATA[cdatablock2]]>
- </products>
- </root>
- eoxml
- end
-
- def test_children_with_text_and_cdata
- assert_equal_rexml(<<-eoxml)
- <root>
- <products>
- hello <![CDATA[cdatablock]]>
- morning
- </products>
- </root>
- eoxml
- end
-
- def test_children_with_blank_text
- assert_equal_rexml(<<-eoxml)
- <root>
- <products> </products>
- </root>
- eoxml
- end
-
- def test_children_with_blank_text_and_attribute
- assert_equal_rexml(<<-eoxml)
- <root>
- <products type="file"> </products>
- </root>
- eoxml
- end
-
- private
- def assert_equal_rexml(xml)
- parsed_xml = ActiveSupport::XmlMini.parse(xml)
- xml.rewind if xml.respond_to?(:rewind)
- hash = ActiveSupport::XmlMini.with_backend('REXML') { ActiveSupport::XmlMini.parse(xml) }
- assert_equal(hash, parsed_xml)
- end
-end
-
end
diff --git a/activesupport/test/xml_mini/nokogirisax_engine_test.rb b/activesupport/test/xml_mini/nokogirisax_engine_test.rb
index 7978a50921..f38a56e83b 100644
--- a/activesupport/test/xml_mini/nokogirisax_engine_test.rb
+++ b/activesupport/test/xml_mini/nokogirisax_engine_test.rb
@@ -1,216 +1,16 @@
-begin
- require 'nokogiri'
-rescue LoadError
- # Skip nokogiri tests
-else
-require 'abstract_unit'
-require 'active_support/xml_mini'
-require 'active_support/core_ext/hash/conversions'
+# frozen_string_literal: true
-class NokogiriSAXEngineTest < ActiveSupport::TestCase
- def setup
- @default_backend = ActiveSupport::XmlMini.backend
- ActiveSupport::XmlMini.backend = 'NokogiriSAX'
- end
-
- def teardown
- ActiveSupport::XmlMini.backend = @default_backend
- end
-
- def test_file_from_xml
- hash = Hash.from_xml(<<-eoxml)
- <blog>
- <logo type="file" name="logo.png" content_type="image/png">
- </logo>
- </blog>
- eoxml
- assert hash.has_key?('blog')
- assert hash['blog'].has_key?('logo')
-
- file = hash['blog']['logo']
- assert_equal 'logo.png', file.original_filename
- assert_equal 'image/png', file.content_type
- end
-
- def test_exception_thrown_on_expansion_attack
- assert_raise RuntimeError do
- attack_xml = <<-EOT
- <?xml version="1.0" encoding="UTF-8"?>
- <!DOCTYPE member [
- <!ENTITY a "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;">
- <!ENTITY b "&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;">
- <!ENTITY c "&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;">
- <!ENTITY d "&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;">
- <!ENTITY e "&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;">
- <!ENTITY f "&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;">
- <!ENTITY g "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx">
- ]>
- <member>
- &a;
- </member>
- EOT
-
- Hash.from_xml(attack_xml)
- end
- end
-
- def test_setting_nokogirisax_as_backend
- ActiveSupport::XmlMini.backend = 'NokogiriSAX'
- assert_equal ActiveSupport::XmlMini_NokogiriSAX, ActiveSupport::XmlMini.backend
- end
+require_relative "xml_mini_engine_test"
- def test_blank_returns_empty_hash
- assert_equal({}, ActiveSupport::XmlMini.parse(nil))
- assert_equal({}, ActiveSupport::XmlMini.parse(''))
- end
-
- def test_array_type_makes_an_array
- assert_equal_rexml(<<-eoxml)
- <blog>
- <posts type="array">
- <post>a post</post>
- <post>another post</post>
- </posts>
- </blog>
- eoxml
- end
-
- def test_one_node_document_as_hash
- assert_equal_rexml(<<-eoxml)
- <products/>
- eoxml
- end
-
- def test_one_node_with_attributes_document_as_hash
- assert_equal_rexml(<<-eoxml)
- <products foo="bar"/>
- eoxml
- end
-
- def test_products_node_with_book_node_as_hash
- assert_equal_rexml(<<-eoxml)
- <products>
- <book name="awesome" id="12345" />
- </products>
- eoxml
- end
-
- def test_products_node_with_two_book_nodes_as_hash
- assert_equal_rexml(<<-eoxml)
- <products>
- <book name="awesome" id="12345" />
- <book name="america" id="67890" />
- </products>
- eoxml
- end
+XMLMiniEngineTest.run_with_gem("nokogiri") do
+ class NokogiriSAXEngineTest < XMLMiniEngineTest
+ private
+ def engine
+ "NokogiriSAX"
+ end
- def test_single_node_with_content_as_hash
- assert_equal_rexml(<<-eoxml)
- <products>
- hello world
- </products>
- eoxml
+ def expansion_attack_error
+ RuntimeError
+ end
end
-
- def test_children_with_children
- assert_equal_rexml(<<-eoxml)
- <root>
- <products>
- <book name="america" id="67890" />
- </products>
- </root>
- eoxml
- end
-
- def test_children_with_text
- assert_equal_rexml(<<-eoxml)
- <root>
- <products>
- hello everyone
- </products>
- </root>
- eoxml
- end
-
- def test_children_with_non_adjacent_text
- assert_equal_rexml(<<-eoxml)
- <root>
- good
- <products>
- hello everyone
- </products>
- morning
- </root>
- eoxml
- end
-
- def test_parse_from_io
- io = StringIO.new(<<-eoxml)
- <root>
- good
- <products>
- hello everyone
- </products>
- morning
- </root>
- eoxml
- assert_equal_rexml(io)
- end
-
- def test_children_with_simple_cdata
- assert_equal_rexml(<<-eoxml)
- <root>
- <products>
- <![CDATA[cdatablock]]>
- </products>
- </root>
- eoxml
- end
-
- def test_children_with_multiple_cdata
- assert_equal_rexml(<<-eoxml)
- <root>
- <products>
- <![CDATA[cdatablock1]]><![CDATA[cdatablock2]]>
- </products>
- </root>
- eoxml
- end
-
- def test_children_with_text_and_cdata
- assert_equal_rexml(<<-eoxml)
- <root>
- <products>
- hello <![CDATA[cdatablock]]>
- morning
- </products>
- </root>
- eoxml
- end
-
- def test_children_with_blank_text
- assert_equal_rexml(<<-eoxml)
- <root>
- <products> </products>
- </root>
- eoxml
- end
-
- def test_children_with_blank_text_and_attribute
- assert_equal_rexml(<<-eoxml)
- <root>
- <products type="file"> </products>
- </root>
- eoxml
- end
-
- private
- def assert_equal_rexml(xml)
- parsed_xml = ActiveSupport::XmlMini.parse(xml)
- xml.rewind if xml.respond_to?(:rewind)
- hash = ActiveSupport::XmlMini.with_backend('REXML') { ActiveSupport::XmlMini.parse(xml) }
- assert_equal(hash, parsed_xml)
- end
-end
-
end
diff --git a/activesupport/test/xml_mini/rexml_engine_test.rb b/activesupport/test/xml_mini/rexml_engine_test.rb
index f0067ca656..b711619ba7 100644
--- a/activesupport/test/xml_mini/rexml_engine_test.rb
+++ b/activesupport/test/xml_mini/rexml_engine_test.rb
@@ -1,35 +1,27 @@
-require 'abstract_unit'
-require 'active_support/xml_mini'
+# frozen_string_literal: true
-class REXMLEngineTest < ActiveSupport::TestCase
+require_relative "xml_mini_engine_test"
+
+class REXMLEngineTest < XMLMiniEngineTest
def test_default_is_rexml
assert_equal ActiveSupport::XmlMini_REXML, ActiveSupport::XmlMini.backend
end
- def test_set_rexml_as_backend
- ActiveSupport::XmlMini.backend = 'REXML'
- assert_equal ActiveSupport::XmlMini_REXML, ActiveSupport::XmlMini.backend
+ def test_parse_from_empty_string
+ assert_equal({}, ActiveSupport::XmlMini.parse(""))
end
- def test_parse_from_io
- ActiveSupport::XmlMini.backend = 'REXML'
- io = StringIO.new(<<-eoxml)
- <root>
- good
- <products>
- hello everyone
- </products>
- morning
- </root>
- eoxml
- assert_equal_rexml(io)
+ def test_parse_from_frozen_string
+ xml_string = "<root></root>"
+ assert_equal({ "root" => {} }, ActiveSupport::XmlMini.parse(xml_string))
end
private
- def assert_equal_rexml(xml)
- parsed_xml = ActiveSupport::XmlMini.parse(xml)
- xml.rewind if xml.respond_to?(:rewind)
- hash = ActiveSupport::XmlMini.with_backend('REXML') { ActiveSupport::XmlMini.parse(xml) }
- assert_equal(hash, parsed_xml)
+ def engine
+ "REXML"
+ end
+
+ def expansion_attack_error
+ RuntimeError
end
end
diff --git a/activesupport/test/xml_mini/xml_mini_engine_test.rb b/activesupport/test/xml_mini/xml_mini_engine_test.rb
new file mode 100644
index 0000000000..c62e7e32c9
--- /dev/null
+++ b/activesupport/test/xml_mini/xml_mini_engine_test.rb
@@ -0,0 +1,264 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/xml_mini"
+require "active_support/core_ext/hash/conversions"
+
+class XMLMiniEngineTest < ActiveSupport::TestCase
+ def self.run_with_gem(gem_name)
+ require gem_name
+ yield
+ rescue LoadError
+ # Skip tests unless gem is available
+ end
+
+ def self.run_with_platform(platform_name)
+ yield if RUBY_PLATFORM.include?(platform_name)
+ end
+
+ def self.inherited(base)
+ base.include EngineTests
+ super
+ end
+
+ def setup
+ @default_backend = ActiveSupport::XmlMini.backend
+ ActiveSupport::XmlMini.backend = engine
+ super
+ end
+
+ def teardown
+ ActiveSupport::XmlMini.backend = @default_backend
+ super
+ end
+
+ module EngineTests
+ def test_file_from_xml
+ hash = Hash.from_xml(<<-eoxml)
+ <blog>
+ <logo type="file" name="logo.png" content_type="image/png">
+ </logo>
+ </blog>
+ eoxml
+ assert hash.key?("blog")
+ assert hash["blog"].key?("logo")
+
+ file = hash["blog"]["logo"]
+ assert_equal "logo.png", file.original_filename
+ assert_equal "image/png", file.content_type
+ end
+
+ def test_exception_thrown_on_expansion_attack
+ assert_raise expansion_attack_error do
+ Hash.from_xml(<<-eoxml)
+ <?xml version="1.0" encoding="UTF-8"?>
+ <!DOCTYPE member [
+ <!ENTITY a "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;">
+ <!ENTITY b "&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;">
+ <!ENTITY c "&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;">
+ <!ENTITY d "&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;">
+ <!ENTITY e "&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;">
+ <!ENTITY f "&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;">
+ <!ENTITY g "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx">
+ ]>
+ <member>
+ &a;
+ </member>
+ eoxml
+ end
+ end
+
+ def test_setting_backend
+ assert_engine_class ActiveSupport::XmlMini.backend
+ end
+
+ def test_blank_returns_empty_hash
+ assert_equal({}, ActiveSupport::XmlMini.parse(nil))
+ assert_equal({}, ActiveSupport::XmlMini.parse(""))
+ end
+
+ def test_parse_from_frozen_string
+ xml_string = "<root/>"
+ assert_equal({ "root" => {} }, ActiveSupport::XmlMini.parse(xml_string))
+ end
+
+ def test_array_type_makes_an_array
+ assert_equal_rexml(<<-eoxml)
+ <blog>
+ <posts type="array">
+ <post>a post</post>
+ <post>another post</post>
+ </posts>
+ </blog>
+ eoxml
+ end
+
+ def test_one_node_document_as_hash
+ assert_equal_rexml(<<-eoxml)
+ <products/>
+ eoxml
+ end
+
+ def test_one_node_with_attributes_document_as_hash
+ assert_equal_rexml(<<-eoxml)
+ <products foo="bar"/>
+ eoxml
+ end
+
+ def test_products_node_with_book_node_as_hash
+ assert_equal_rexml(<<-eoxml)
+ <products>
+ <book name="awesome" id="12345" />
+ </products>
+ eoxml
+ end
+
+ def test_products_node_with_two_book_nodes_as_hash
+ assert_equal_rexml(<<-eoxml)
+ <products>
+ <book name="awesome" id="12345" />
+ <book name="america" id="67890" />
+ </products>
+ eoxml
+ end
+
+ def test_single_node_with_content_as_hash
+ assert_equal_rexml(<<-eoxml)
+ <products>
+ hello world
+ </products>
+ eoxml
+ end
+
+ def test_children_with_children
+ assert_equal_rexml(<<-eoxml)
+ <root>
+ <products>
+ <book name="america" id="67890" />
+ </products>
+ </root>
+ eoxml
+ end
+
+ def test_children_with_text
+ assert_equal_rexml(<<-eoxml)
+ <root>
+ <products>
+ hello everyone
+ </products>
+ </root>
+ eoxml
+ end
+
+ def test_children_with_non_adjacent_text
+ assert_equal_rexml(<<-eoxml)
+ <root>
+ good
+ <products>
+ hello everyone
+ </products>
+ morning
+ </root>
+ eoxml
+ end
+
+ def test_parse_from_io
+ skip_unless_extended_engine
+
+ assert_equal_rexml(StringIO.new(<<-eoxml))
+ <root>
+ good
+ <products>
+ hello everyone
+ </products>
+ morning
+ </root>
+ eoxml
+ end
+
+ def test_children_with_simple_cdata
+ skip_unless_extended_engine
+
+ assert_equal_rexml(<<-eoxml)
+ <root>
+ <products>
+ <![CDATA[cdatablock]]>
+ </products>
+ </root>
+ eoxml
+ end
+
+ def test_children_with_multiple_cdata
+ skip_unless_extended_engine
+
+ assert_equal_rexml(<<-eoxml)
+ <root>
+ <products>
+ <![CDATA[cdatablock1]]><![CDATA[cdatablock2]]>
+ </products>
+ </root>
+ eoxml
+ end
+
+ def test_children_with_text_and_cdata
+ skip_unless_extended_engine
+
+ assert_equal_rexml(<<-eoxml)
+ <root>
+ <products>
+ hello <![CDATA[cdatablock]]>
+ morning
+ </products>
+ </root>
+ eoxml
+ end
+
+ def test_children_with_blank_text
+ skip_unless_extended_engine
+
+ assert_equal_rexml(<<-eoxml)
+ <root>
+ <products> </products>
+ </root>
+ eoxml
+ end
+
+ def test_children_with_blank_text_and_attribute
+ skip_unless_extended_engine
+
+ assert_equal_rexml(<<-eoxml)
+ <root>
+ <products type="file"> </products>
+ </root>
+ eoxml
+ end
+
+ private
+ def engine
+ raise NotImplementedError
+ end
+
+ def assert_engine_class(actual)
+ assert_equal ActiveSupport.const_get("XmlMini_#{engine}"), actual
+ end
+
+ def assert_equal_rexml(xml)
+ parsed_xml = ActiveSupport::XmlMini.parse(xml)
+ xml.rewind if xml.respond_to?(:rewind)
+ hash = ActiveSupport::XmlMini.with_backend("REXML") { ActiveSupport::XmlMini.parse(xml) }
+ assert_equal(hash, parsed_xml)
+ end
+
+ def expansion_attack_error
+ raise NotImplementedError
+ end
+
+ def extended_engine?
+ true
+ end
+
+ def skip_unless_extended_engine
+ skip "#{engine} is not an extended engine" unless extended_engine?
+ end
+ end
+end
diff --git a/activesupport/test/xml_mini_test.rb b/activesupport/test/xml_mini_test.rb
index 55e8181b54..18a3f2ca66 100644
--- a/activesupport/test/xml_mini_test.rb
+++ b/activesupport/test/xml_mini_test.rb
@@ -1,9 +1,11 @@
-require 'abstract_unit'
-require 'active_support/xml_mini'
-require 'active_support/builder'
-require 'active_support/core_ext/hash'
-require 'active_support/core_ext/big_decimal'
-require 'yaml'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/xml_mini"
+require "active_support/builder"
+require "active_support/core_ext/hash"
+require "active_support/core_ext/big_decimal"
+require "yaml"
module XmlMiniTest
class RenameKeyTest < ActiveSupport::TestCase
@@ -12,23 +14,23 @@ module XmlMiniTest
end
def test_rename_key_dasherizes_with_dasherize_true
- assert_equal "my-key", ActiveSupport::XmlMini.rename_key("my_key", :dasherize => true)
+ assert_equal "my-key", ActiveSupport::XmlMini.rename_key("my_key", dasherize: true)
end
def test_rename_key_does_nothing_with_dasherize_false
- assert_equal "my_key", ActiveSupport::XmlMini.rename_key("my_key", :dasherize => false)
+ assert_equal "my_key", ActiveSupport::XmlMini.rename_key("my_key", dasherize: false)
end
def test_rename_key_camelizes_with_camelize_true
- assert_equal "MyKey", ActiveSupport::XmlMini.rename_key("my_key", :camelize => true)
+ assert_equal "MyKey", ActiveSupport::XmlMini.rename_key("my_key", camelize: true)
end
def test_rename_key_lower_camelizes_with_camelize_lower
- assert_equal "myKey", ActiveSupport::XmlMini.rename_key("my_key", :camelize => :lower)
+ assert_equal "myKey", ActiveSupport::XmlMini.rename_key("my_key", camelize: :lower)
end
def test_rename_key_lower_camelizes_with_camelize_upper
- assert_equal "MyKey", ActiveSupport::XmlMini.rename_key("my_key", :camelize => :upper)
+ assert_equal "MyKey", ActiveSupport::XmlMini.rename_key("my_key", camelize: :upper)
end
def test_rename_key_does_not_dasherize_leading_underscores
@@ -63,16 +65,16 @@ module XmlMiniTest
def setup
@xml = ActiveSupport::XmlMini
- @options = {:skip_instruct => true, :builder => Builder::XmlMarkup.new}
+ @options = { skip_instruct: true, builder: Builder::XmlMarkup.new }
end
test "#to_tag accepts a callable object and passes options with the builder" do
- @xml.to_tag(:some_tag, lambda {|o| o[:builder].br }, @options)
+ @xml.to_tag(:some_tag, lambda { |o| o[:builder].br }, @options)
assert_xml "<br/>"
end
test "#to_tag accepts a callable object and passes options and tag name" do
- @xml.to_tag(:tag, lambda {|o, t| o[:builder].b(t) }, @options)
+ @xml.to_tag(:tag, lambda { |o, t| o[:builder].b(t) }, @options)
assert_xml "<b>tag</b>"
end
@@ -92,58 +94,58 @@ module XmlMiniTest
end
test "#to_tag should use the type value in the options hash" do
- @xml.to_tag(:b, "blue", @options.merge(type: 'color'))
- assert_xml( "<b type=\"color\">blue</b>" )
+ @xml.to_tag(:b, "blue", @options.merge(type: "color"))
+ assert_xml("<b type=\"color\">blue</b>")
end
test "#to_tag accepts symbol types" do
@xml.to_tag(:b, :name, @options)
- assert_xml( "<b type=\"symbol\">name</b>" )
+ assert_xml("<b type=\"symbol\">name</b>")
end
test "#to_tag accepts boolean types" do
@xml.to_tag(:b, true, @options)
- assert_xml( "<b type=\"boolean\">true</b>")
+ assert_xml("<b type=\"boolean\">true</b>")
end
test "#to_tag accepts float types" do
@xml.to_tag(:b, 3.14, @options)
- assert_xml( "<b type=\"float\">3.14</b>")
+ assert_xml("<b type=\"float\">3.14</b>")
end
test "#to_tag accepts decimal types" do
- @xml.to_tag(:b, ::BigDecimal.new("1.2"), @options)
- assert_xml( "<b type=\"decimal\">1.2</b>")
+ @xml.to_tag(:b, BigDecimal("1.2"), @options)
+ assert_xml("<b type=\"decimal\">1.2</b>")
end
test "#to_tag accepts date types" do
- @xml.to_tag(:b, Date.new(2001,2,3), @options)
- assert_xml( "<b type=\"date\">2001-02-03</b>")
+ @xml.to_tag(:b, Date.new(2001, 2, 3), @options)
+ assert_xml("<b type=\"date\">2001-02-03</b>")
end
test "#to_tag accepts datetime types" do
- @xml.to_tag(:b, DateTime.new(2001,2,3,4,5,6,'+7'), @options)
- assert_xml( "<b type=\"dateTime\">2001-02-03T04:05:06+07:00</b>")
+ @xml.to_tag(:b, DateTime.new(2001, 2, 3, 4, 5, 6, "+7"), @options)
+ assert_xml("<b type=\"dateTime\">2001-02-03T04:05:06+07:00</b>")
end
test "#to_tag accepts time types" do
@xml.to_tag(:b, Time.new(1993, 02, 24, 12, 0, 0, "+09:00"), @options)
- assert_xml( "<b type=\"dateTime\">1993-02-24T12:00:00+09:00</b>")
+ assert_xml("<b type=\"dateTime\">1993-02-24T12:00:00+09:00</b>")
end
test "#to_tag accepts array types" do
@xml.to_tag(:b, ["first_name", "last_name"], @options)
- assert_xml( "<b type=\"array\"><b>first_name</b><b>last_name</b></b>" )
+ assert_xml("<b type=\"array\"><b>first_name</b><b>last_name</b></b>")
end
test "#to_tag accepts hash types" do
@xml.to_tag(:b, { first_name: "Bob", last_name: "Marley" }, @options)
- assert_xml( "<b><first-name>Bob</first-name><last-name>Marley</last-name></b>" )
+ assert_xml("<b><first-name>Bob</first-name><last-name>Marley</last-name></b>")
end
test "#to_tag should not add type when skip types option is set" do
@xml.to_tag(:b, "Bob", @options.merge(skip_types: 1))
- assert_xml( "<b>Bob</b>" )
+ assert_xml("<b>Bob</b>")
end
test "#to_tag should dasherize the space when passed a string with spaces as a key" do
@@ -233,62 +235,62 @@ module XmlMiniTest
end
def test_symbol
- parser = @parsing['symbol']
- assert_equal :symbol, parser.call('symbol')
+ parser = @parsing["symbol"]
+ assert_equal :symbol, parser.call("symbol")
assert_equal :symbol, parser.call(:symbol)
assert_equal :'123', parser.call(123)
- assert_raises(ArgumentError) { parser.call(Date.new(2013,11,12,02,11)) }
+ assert_raises(ArgumentError) { parser.call(Date.new(2013, 11, 12, 02, 11)) }
end
def test_date
- parser = @parsing['date']
- assert_equal Date.new(2013,11,12), parser.call("2013-11-12T0211Z")
+ parser = @parsing["date"]
+ assert_equal Date.new(2013, 11, 12), parser.call("2013-11-12T0211Z")
assert_raises(TypeError) { parser.call(1384190018) }
assert_raises(ArgumentError) { parser.call("not really a date") }
end
def test_datetime
- parser = @parsing['datetime']
- assert_equal Time.new(2013,11,12,02,11,00,0), parser.call("2013-11-12T02:11:00Z")
- assert_equal DateTime.new(2013,11,12), parser.call("2013-11-12T0211Z")
- assert_equal DateTime.new(2013,11,12,02,11), parser.call("2013-11-12T02:11Z")
- assert_equal DateTime.new(2013,11,12,02,11), parser.call("2013-11-12T11:11+9")
+ parser = @parsing["datetime"]
+ assert_equal Time.new(2013, 11, 12, 02, 11, 00, 0), parser.call("2013-11-12T02:11:00Z")
+ assert_equal DateTime.new(2013, 11, 12), parser.call("2013-11-12T0211Z")
+ assert_equal DateTime.new(2013, 11, 12, 02, 11), parser.call("2013-11-12T02:11Z")
+ assert_equal DateTime.new(2013, 11, 12, 02, 11), parser.call("2013-11-12T11:11+9")
assert_raises(ArgumentError) { parser.call("1384190018") }
end
def test_integer
- parser = @parsing['integer']
+ parser = @parsing["integer"]
assert_equal 123, parser.call(123)
assert_equal 123, parser.call(123.003)
assert_equal 123, parser.call("123")
assert_equal 0, parser.call("")
- assert_raises(ArgumentError) { parser.call(Date.new(2013,11,12,02,11)) }
+ assert_raises(ArgumentError) { parser.call(Date.new(2013, 11, 12, 02, 11)) }
end
def test_float
- parser = @parsing['float']
+ parser = @parsing["float"]
assert_equal 123, parser.call("123")
assert_equal 123.003, parser.call("123.003")
assert_equal 123.0, parser.call("123,003")
assert_equal 0.0, parser.call("")
assert_equal 123, parser.call(123)
assert_equal 123.05, parser.call(123.05)
- assert_raises(ArgumentError) { parser.call(Date.new(2013,11,12,02,11)) }
+ assert_raises(ArgumentError) { parser.call(Date.new(2013, 11, 12, 02, 11)) }
end
def test_decimal
- parser = @parsing['decimal']
+ parser = @parsing["decimal"]
assert_equal 123, parser.call("123")
assert_equal 123.003, parser.call("123.003")
assert_equal 123.0, parser.call("123,003")
assert_equal 0.0, parser.call("")
assert_equal 123, parser.call(123)
assert_raises(ArgumentError) { parser.call(123.04) }
- assert_raises(ArgumentError) { parser.call(Date.new(2013,11,12,02,11)) }
+ assert_raises(ArgumentError) { parser.call(Date.new(2013, 11, 12, 02, 11)) }
end
def test_boolean
- parser = @parsing['boolean']
+ parser = @parsing["boolean"]
[1, true, "1"].each do |value|
assert parser.call(value)
end
@@ -299,13 +301,13 @@ module XmlMiniTest
end
def test_string
- parser = @parsing['string']
+ parser = @parsing["string"]
assert_equal "123", parser.call(123)
assert_equal "123", parser.call("123")
assert_equal "[]", parser.call("[]")
assert_equal "[]", parser.call([])
assert_equal "{}", parser.call({})
- assert_raises(ArgumentError) { parser.call(Date.new(2013,11,12,02,11)) }
+ assert_raises(ArgumentError) { parser.call(Date.new(2013, 11, 12, 02, 11)) }
end
def test_yaml
@@ -316,14 +318,14 @@ product:
description : Basketball
YAML
expected = {
- "product"=> [
- {"sku"=>"BL394D", "quantity"=>4, "description"=>"Basketball"}
+ "product" => [
+ { "sku" => "BL394D", "quantity" => 4, "description" => "Basketball" }
]
}
- parser = @parsing['yaml']
+ parser = @parsing["yaml"]
assert_equal(expected, parser.call(yaml))
- assert_equal({1 => 'test'}, parser.call({1 => 'test'}))
- assert_equal({"1 => 'test'"=>nil}, parser.call("{1 => 'test'}"))
+ assert_equal({ 1 => "test" }, parser.call(1 => "test"))
+ assert_equal({ "1 => 'test'" => nil }, parser.call("{1 => 'test'}"))
end
def test_base64Binary_and_binary
@@ -341,12 +343,12 @@ in the continued and indefatigable generation of knowledge, exceeds the short
vehemence of any carnal pleasure.
EXPECTED
- parser = @parsing['base64Binary']
- assert_equal expected_base64.gsub(/\n/," ").strip, parser.call(base64)
+ parser = @parsing["base64Binary"]
+ assert_equal expected_base64.gsub(/\n/, " ").strip, parser.call(base64)
parser.call("NON BASE64 INPUT")
- parser = @parsing['binary']
- assert_equal expected_base64.gsub(/\n/," ").strip, parser.call(base64, 'encoding' => 'base64')
+ parser = @parsing["binary"]
+ assert_equal expected_base64.gsub(/\n/, " ").strip, parser.call(base64, "encoding" => "base64")
assert_equal "IGNORED INPUT", parser.call("IGNORED INPUT", {})
end
end
diff --git a/ci/qunit-selenium-runner.rb b/ci/qunit-selenium-runner.rb
new file mode 100644
index 0000000000..132b3d17eb
--- /dev/null
+++ b/ci/qunit-selenium-runner.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require "qunit/selenium/test_runner"
+require "chromedriver-helper"
+
+driver_options = Selenium::WebDriver::Chrome::Options.new
+driver_options.add_argument("--headless")
+driver_options.add_argument("--disable-gpu")
+driver_options.add_argument("--no-sandbox")
+
+driver = ::Selenium::WebDriver.for(:chrome, options: driver_options)
+result = QUnit::Selenium::TestRunner.new(driver).open(ARGV[0], timeout: 60)
+driver.quit
+
+puts "Time: #{result.duration} seconds, Total: #{result.assertions[:total]}, Passed: #{result.assertions[:passed]}, Failed: #{result.assertions[:failed]}"
+exit(result.tests[:failed] > 0 ? 1 : 0)
diff --git a/ci/travis.rb b/ci/travis.rb
index 52fef05fbf..1de281046f 100755
--- a/ci/travis.rb
+++ b/ci/travis.rb
@@ -1,29 +1,37 @@
#!/usr/bin/env ruby
-require 'fileutils'
+# frozen_string_literal: true
+
+require "fileutils"
include FileUtils
commands = [
- 'mysql -e "create database activerecord_unittest;"',
- 'mysql -e "create database activerecord_unittest2;"',
- 'psql -c "create database activerecord_unittest;" -U postgres',
- 'psql -c "create database activerecord_unittest2;" -U postgres'
+ 'mysql -e "create user rails@localhost;"',
+ 'mysql -e "grant all privileges on activerecord_unittest.* to rails@localhost;"',
+ 'mysql -e "grant all privileges on activerecord_unittest2.* to rails@localhost;"',
+ 'mysql -e "grant all privileges on inexistent_activerecord_unittest.* to rails@localhost;"',
+ 'mysql -e "create database activerecord_unittest default character set utf8mb4;"',
+ 'mysql -e "create database activerecord_unittest2 default character set utf8mb4;"',
+ 'psql -c "create database -E UTF8 -T template0 activerecord_unittest;" -U postgres',
+ 'psql -c "create database -E UTF8 -T template0 activerecord_unittest2;" -U postgres'
]
commands.each do |command|
- system("#{command} > /dev/null 2>&1")
+ system(command, [1, 2] => File::NULL)
end
class Build
MAP = {
- 'railties' => 'railties',
- 'ap' => 'actionpack',
- 'am' => 'actionmailer',
- 'amo' => 'activemodel',
- 'as' => 'activesupport',
- 'ar' => 'activerecord',
- 'av' => 'actionview',
- 'aj' => 'activejob',
- 'guides' => 'guides'
+ "railties" => "railties",
+ "ap" => "actionpack",
+ "am" => "actionmailer",
+ "amo" => "activemodel",
+ "as" => "activesupport",
+ "ar" => "activerecord",
+ "av" => "actionview",
+ "aj" => "activejob",
+ "ac" => "actioncable",
+ "ast" => "activestorage",
+ "guides" => "guides"
}
attr_reader :component, :options
@@ -35,8 +43,10 @@ class Build
def run!(options = {})
self.options.update(options)
+
Dir.chdir(dir) do
announce(heading)
+
if guides?
run_bug_report_templates
else
@@ -54,30 +64,45 @@ class Build
heading << "with #{adapter}" if activerecord?
heading << "in isolation" if isolated?
heading << "integration" if integration?
- heading.join(' ')
+ heading.join(" ")
end
def tasks
if activerecord?
- ['db:mysql:rebuild', "#{adapter}:#{'isolated_' if isolated?}test"]
+ tasks = ["#{adapter}:#{'isolated_' if isolated?}test"]
+ case adapter
+ when "mysql2"
+ tasks.unshift "db:mysql:rebuild"
+ when "postgresql"
+ tasks.unshift "db:postgresql:rebuild"
+ 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
def key
key = [gem]
key << adapter if activerecord?
- key << 'isolated' if isolated?
- key.join(':')
+ key << "isolated" if isolated?
+ key.join(":")
+ end
+
+ def activesupport?
+ gem == "activesupport"
end
def activerecord?
- gem == 'activerecord'
+ gem == "activerecord"
end
def guides?
- gem == 'guides'
+ gem == "guides"
+ end
+
+ def ujs?
+ component.split(":").last == "ujs"
end
def isolated?
@@ -85,68 +110,69 @@ class Build
end
def integration?
- component.split(':').last == 'integration'
+ component.split(":").last == "integration"
end
def gem
- MAP[component.split(':').first]
+ MAP[component.split(":").first]
end
alias :dir :gem
def adapter
- component.split(':').last
+ component.split(":").last
end
def rake(*tasks)
tasks.each do |task|
cmd = "bundle exec rake #{task}"
puts "Running command: #{cmd}"
- return false unless system(cmd)
+ return false unless system(env, cmd)
end
true
end
+ def env
+ if activesupport? && !isolated?
+ # There is a known issue with the listen tests that causes files to be
+ # incorrectly GC'ed even when they are still in-use. The current solution
+ # is to only run them in isolation to avoid random failures of our test suite.
+ { "LISTEN" => "0" }
+ else
+ {}
+ end
+ end
+
def run_bug_report_templates
- Dir.glob('bug_report_templates/*.rb').all? do |file|
- system(Gem.ruby, '-w', file)
+ Dir.glob("bug_report_templates/*.rb").all? do |file|
+ system(Gem.ruby, "-w", file)
end
end
end
-if ENV['GEM']=='aj:integration'
- ENV['QC_DATABASE_URL'] = 'postgres://postgres@localhost/active_jobs_qc_int_test'
- ENV['QUE_DATABASE_URL'] = 'postgres://postgres@localhost/active_jobs_que_int_test'
+if ENV["GEM"] == "aj:integration"
+ ENV["QC_DATABASE_URL"] = "postgres://postgres@localhost/active_jobs_qc_int_test"
+ ENV["QUE_DATABASE_URL"] = "postgres://postgres@localhost/active_jobs_que_int_test"
end
results = {}
-ENV['GEM'].split(',').each do |gem|
+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 gem == 'railties' && isolated
- next if gem == 'aj:integration' && isolated
- next if gem == 'guides' && isolated
-
- build = Build.new(gem, :isolated => isolated)
+ next if ENV["TRAVIS_PULL_REQUEST"] && ENV["TRAVIS_PULL_REQUEST"] != "false" && isolated
+ next if RUBY_VERSION < "2.5" && 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
+ next if gem == "ast" && isolated
+
+ build = Build.new(gem, isolated: isolated)
results[build.key] = build.run!
-
end
end
-# puts
-# puts "Build environment:"
-# puts " #{`cat /etc/issue`}"
-# puts " #{`uname -a`}"
-# puts " #{`ruby -v`}"
-# puts " #{`mysql --version`}"
-# puts " #{`pg_config --version`}"
-# puts " SQLite3: #{`sqlite3 -version`}"
-# `gem env`.each_line {|line| print " #{line}"}
-# puts " Bundled gems:"
-# `bundle show`.each_line {|line| print " #{line}"}
-# puts " Local gems:"
-# `gem list`.each_line {|line| print " #{line}"}
-
failures = results.select { |key, value| !value }
if failures.empty?
diff --git a/guides/CHANGELOG.md b/guides/CHANGELOG.md
index fd177b4238..516b643cb8 100644
--- a/guides/CHANGELOG.md
+++ b/guides/CHANGELOG.md
@@ -1,17 +1,10 @@
-* New section in Configuring: Configuring Active Job
+* New section _Troubleshooting_ in the _Autoloading and Reloading Constants_ guide.
- *Eliot Sykes*
+ *Xavier Noria*
-* New section in Active Record Association Basics: Single Table Inheritance
+* Rails 6 requires Ruby 2.4.1 or newer.
- *Andrey Nering*
+ *Jeremy Daer*
-* New section in Active Record Querying: Understanding The Method Chaining
- *Andrey Nering*
-
-* New section in Configuring: Search Engines Indexing
-
- *Andrey Nering*
-
-Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/guides/CHANGELOG.md) for previous changes.
+Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/guides/CHANGELOG.md) for previous changes.
diff --git a/guides/Rakefile b/guides/Rakefile
index 3c2099ac02..4116e6f9cc 100644
--- a/guides/Rakefile
+++ b/guides/Rakefile
@@ -1,38 +1,52 @@
-namespace :guides do
+# frozen_string_literal: true
+namespace :guides do
desc 'Generate guides (for authors), use ONLY=foo to process just "foo.md"'
- task :generate => 'generate:html'
+ task generate: "generate:html"
+
+ # 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
- ENV["WARN_BROKEN_LINKS"] = "1" # authors can't disable this
+ 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 0.1.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`"
+ unless /convert/.match?(`convert`)
+ abort "Please install ImageMagick"
end
- ENV['KINDLE'] = '1'
- Rake::Task['guides:generate:html'].invoke
+ ENV["KINDLE"] = "1"
+ Rake::Task["guides:generate:html"].invoke
end
end
# 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 +59,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,16 +79,13 @@ 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
-task :default => 'guides:help'
+task default: "guides:help"
diff --git a/guides/assets/images/4_0_release_notes/rails4_features.png b/guides/assets/images/4_0_release_notes/rails4_features.png
new file mode 100644
index 0000000000..ac73f05cf7
--- /dev/null
+++ b/guides/assets/images/4_0_release_notes/rails4_features.png
Binary files differ
diff --git a/guides/assets/images/akshaysurve.jpg b/guides/assets/images/akshaysurve.jpg
deleted file mode 100644
index cfc3333958..0000000000
--- a/guides/assets/images/akshaysurve.jpg
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/association_basics/belongs_to.png b/guides/assets/images/association_basics/belongs_to.png
new file mode 100644
index 0000000000..2b8c1d52ea
--- /dev/null
+++ b/guides/assets/images/association_basics/belongs_to.png
Binary files differ
diff --git a/guides/assets/images/association_basics/habtm.png b/guides/assets/images/association_basics/habtm.png
new file mode 100644
index 0000000000..7e508cc1a6
--- /dev/null
+++ b/guides/assets/images/association_basics/habtm.png
Binary files differ
diff --git a/guides/assets/images/association_basics/has_many.png b/guides/assets/images/association_basics/has_many.png
new file mode 100644
index 0000000000..36ccf9f0f6
--- /dev/null
+++ b/guides/assets/images/association_basics/has_many.png
Binary files differ
diff --git a/guides/assets/images/association_basics/has_many_through.png b/guides/assets/images/association_basics/has_many_through.png
new file mode 100644
index 0000000000..9e9caabd73
--- /dev/null
+++ b/guides/assets/images/association_basics/has_many_through.png
Binary files differ
diff --git a/guides/assets/images/association_basics/has_one.png b/guides/assets/images/association_basics/has_one.png
new file mode 100644
index 0000000000..c29c6b9c59
--- /dev/null
+++ b/guides/assets/images/association_basics/has_one.png
Binary files differ
diff --git a/guides/assets/images/association_basics/has_one_through.png b/guides/assets/images/association_basics/has_one_through.png
new file mode 100644
index 0000000000..fdf13286c4
--- /dev/null
+++ b/guides/assets/images/association_basics/has_one_through.png
Binary files differ
diff --git a/guides/assets/images/association_basics/polymorphic.png b/guides/assets/images/association_basics/polymorphic.png
new file mode 100644
index 0000000000..d630db9e01
--- /dev/null
+++ b/guides/assets/images/association_basics/polymorphic.png
Binary files differ
diff --git a/guides/assets/images/belongs_to.png b/guides/assets/images/belongs_to.png
deleted file mode 100644
index 43c963ffa8..0000000000
--- a/guides/assets/images/belongs_to.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/credits_pic_blank.gif b/guides/assets/images/credits_pic_blank.gif
deleted file mode 100644
index a6b335d0c9..0000000000
--- a/guides/assets/images/credits_pic_blank.gif
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/favicon.ico b/guides/assets/images/favicon.ico
index faa10b4580..87192a8a07 100644
--- a/guides/assets/images/favicon.ico
+++ b/guides/assets/images/favicon.ico
Binary files differ
diff --git a/guides/assets/images/fxn.png b/guides/assets/images/fxn.png
deleted file mode 100644
index 733d380cba..0000000000
--- a/guides/assets/images/fxn.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/getting_started/article_with_comments.png b/guides/assets/images/getting_started/article_with_comments.png
index c489e4c00e..3f16f3b280 100644
--- a/guides/assets/images/getting_started/article_with_comments.png
+++ b/guides/assets/images/getting_started/article_with_comments.png
Binary files differ
diff --git a/guides/assets/images/getting_started/challenge.png b/guides/assets/images/getting_started/challenge.png
index 5b88a842b2..d05ef31bbe 100644
--- a/guides/assets/images/getting_started/challenge.png
+++ b/guides/assets/images/getting_started/challenge.png
Binary files differ
diff --git a/guides/assets/images/getting_started/confirm_dialog.png b/guides/assets/images/getting_started/confirm_dialog.png
index 9755f581a6..ce65734e6c 100644
--- a/guides/assets/images/getting_started/confirm_dialog.png
+++ b/guides/assets/images/getting_started/confirm_dialog.png
Binary files differ
diff --git a/guides/assets/images/getting_started/forbidden_attributes_for_new_article.png b/guides/assets/images/getting_started/forbidden_attributes_for_new_article.png
index 9f32c68472..50b178808e 100644
--- a/guides/assets/images/getting_started/forbidden_attributes_for_new_article.png
+++ b/guides/assets/images/getting_started/forbidden_attributes_for_new_article.png
Binary files differ
diff --git a/guides/assets/images/getting_started/form_with_errors.png b/guides/assets/images/getting_started/form_with_errors.png
index 98bff37d4a..6eefd2885a 100644
--- a/guides/assets/images/getting_started/form_with_errors.png
+++ b/guides/assets/images/getting_started/form_with_errors.png
Binary files differ
diff --git a/guides/assets/images/getting_started/index_action_with_edit_link.png b/guides/assets/images/getting_started/index_action_with_edit_link.png
index 0566a3ffde..a2a087a598 100644
--- a/guides/assets/images/getting_started/index_action_with_edit_link.png
+++ b/guides/assets/images/getting_started/index_action_with_edit_link.png
Binary files differ
diff --git a/guides/assets/images/getting_started/new_article.png b/guides/assets/images/getting_started/new_article.png
index bd3ae4fa67..6edcc161b6 100644
--- a/guides/assets/images/getting_started/new_article.png
+++ b/guides/assets/images/getting_started/new_article.png
Binary files differ
diff --git a/guides/assets/images/getting_started/rails_welcome.png b/guides/assets/images/getting_started/rails_welcome.png
index 4d0cb417b7..88efe34a9d 100644
--- a/guides/assets/images/getting_started/rails_welcome.png
+++ b/guides/assets/images/getting_started/rails_welcome.png
Binary files differ
diff --git a/guides/assets/images/getting_started/routing_error_no_controller.png b/guides/assets/images/getting_started/routing_error_no_controller.png
index ed62862291..52150f0426 100644
--- a/guides/assets/images/getting_started/routing_error_no_controller.png
+++ b/guides/assets/images/getting_started/routing_error_no_controller.png
Binary files differ
diff --git a/guides/assets/images/getting_started/routing_error_no_route_matches.png b/guides/assets/images/getting_started/routing_error_no_route_matches.png
deleted file mode 100644
index 08c54f921f..0000000000
--- a/guides/assets/images/getting_started/routing_error_no_route_matches.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/getting_started/show_action_for_articles.png b/guides/assets/images/getting_started/show_action_for_articles.png
index 4dad704f89..68837131f7 100644
--- a/guides/assets/images/getting_started/show_action_for_articles.png
+++ b/guides/assets/images/getting_started/show_action_for_articles.png
Binary files differ
diff --git a/guides/assets/images/getting_started/template_is_missing_articles_new.png b/guides/assets/images/getting_started/template_is_missing_articles_new.png
index 4e636d09ff..a1603f5d28 100644
--- a/guides/assets/images/getting_started/template_is_missing_articles_new.png
+++ b/guides/assets/images/getting_started/template_is_missing_articles_new.png
Binary files differ
diff --git a/guides/assets/images/getting_started/unknown_action_create_for_articles.png b/guides/assets/images/getting_started/unknown_action_create_for_articles.png
index fd20cd53dc..ec4758e085 100644
--- a/guides/assets/images/getting_started/unknown_action_create_for_articles.png
+++ b/guides/assets/images/getting_started/unknown_action_create_for_articles.png
Binary files differ
diff --git a/guides/assets/images/getting_started/unknown_action_new_for_articles.png b/guides/assets/images/getting_started/unknown_action_new_for_articles.png
index e948a51e4a..f7e7464d61 100644
--- a/guides/assets/images/getting_started/unknown_action_new_for_articles.png
+++ b/guides/assets/images/getting_started/unknown_action_new_for_articles.png
Binary files differ
diff --git a/guides/assets/images/habtm.png b/guides/assets/images/habtm.png
deleted file mode 100644
index b062bc73fe..0000000000
--- a/guides/assets/images/habtm.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/has_many.png b/guides/assets/images/has_many.png
deleted file mode 100644
index e7589e3b75..0000000000
--- a/guides/assets/images/has_many.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/has_many_through.png b/guides/assets/images/has_many_through.png
deleted file mode 100644
index 858c898dc1..0000000000
--- a/guides/assets/images/has_many_through.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/has_one.png b/guides/assets/images/has_one.png
deleted file mode 100644
index 93faa05b07..0000000000
--- a/guides/assets/images/has_one.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/has_one_through.png b/guides/assets/images/has_one_through.png
deleted file mode 100644
index 07dac1a27d..0000000000
--- a/guides/assets/images/has_one_through.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/header_backdrop.png b/guides/assets/images/header_backdrop.png
deleted file mode 100644
index 72b030478f..0000000000
--- a/guides/assets/images/header_backdrop.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/i18n/demo_html_safe.png b/guides/assets/images/i18n/demo_html_safe.png
index 9afa8ebec1..be75d4830e 100644
--- a/guides/assets/images/i18n/demo_html_safe.png
+++ b/guides/assets/images/i18n/demo_html_safe.png
Binary files differ
diff --git a/guides/assets/images/i18n/demo_localized_pirate.png b/guides/assets/images/i18n/demo_localized_pirate.png
index bf8d0b558c..528cc27900 100644
--- a/guides/assets/images/i18n/demo_localized_pirate.png
+++ b/guides/assets/images/i18n/demo_localized_pirate.png
Binary files differ
diff --git a/guides/assets/images/i18n/demo_translated_en.png b/guides/assets/images/i18n/demo_translated_en.png
index e887bfa306..bbb5e93c3a 100644
--- a/guides/assets/images/i18n/demo_translated_en.png
+++ b/guides/assets/images/i18n/demo_translated_en.png
Binary files differ
diff --git a/guides/assets/images/i18n/demo_translated_pirate.png b/guides/assets/images/i18n/demo_translated_pirate.png
index aa5618a865..305fa93a14 100644
--- a/guides/assets/images/i18n/demo_translated_pirate.png
+++ b/guides/assets/images/i18n/demo_translated_pirate.png
Binary files differ
diff --git a/guides/assets/images/i18n/demo_translation_missing.png b/guides/assets/images/i18n/demo_translation_missing.png
index 867aa7c42d..e9833ba307 100644
--- a/guides/assets/images/i18n/demo_translation_missing.png
+++ b/guides/assets/images/i18n/demo_translation_missing.png
Binary files differ
diff --git a/guides/assets/images/i18n/demo_untranslated.png b/guides/assets/images/i18n/demo_untranslated.png
index 2ea6404822..2653abc491 100644
--- a/guides/assets/images/i18n/demo_untranslated.png
+++ b/guides/assets/images/i18n/demo_untranslated.png
Binary files differ
diff --git a/guides/assets/images/icons/README b/guides/assets/images/icons/README
deleted file mode 100644
index 09da77fc86..0000000000
--- a/guides/assets/images/icons/README
+++ /dev/null
@@ -1,5 +0,0 @@
-Replaced the plain DocBook XSL admonition icons with Jimmac's DocBook
-icons (http://jimmac.musichall.cz/ikony.php3). I dropped transparency
-from the Jimmac icons to get round MS IE and FOP PNG incompatibilities.
-
-Stuart Rackham
diff --git a/guides/assets/images/icons/callouts/1.png b/guides/assets/images/icons/callouts/1.png
deleted file mode 100644
index c5d02adcf4..0000000000
--- a/guides/assets/images/icons/callouts/1.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/callouts/10.png b/guides/assets/images/icons/callouts/10.png
deleted file mode 100644
index fe89f9ef83..0000000000
--- a/guides/assets/images/icons/callouts/10.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/callouts/11.png b/guides/assets/images/icons/callouts/11.png
deleted file mode 100644
index 3b7b9318e7..0000000000
--- a/guides/assets/images/icons/callouts/11.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/callouts/12.png b/guides/assets/images/icons/callouts/12.png
deleted file mode 100644
index 7b95925e9d..0000000000
--- a/guides/assets/images/icons/callouts/12.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/callouts/13.png b/guides/assets/images/icons/callouts/13.png
deleted file mode 100644
index 4b99fe8efc..0000000000
--- a/guides/assets/images/icons/callouts/13.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/callouts/14.png b/guides/assets/images/icons/callouts/14.png
deleted file mode 100644
index 4274e6580a..0000000000
--- a/guides/assets/images/icons/callouts/14.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/callouts/15.png b/guides/assets/images/icons/callouts/15.png
deleted file mode 100644
index 70e4bba615..0000000000
--- a/guides/assets/images/icons/callouts/15.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/callouts/2.png b/guides/assets/images/icons/callouts/2.png
deleted file mode 100644
index 8c57970ba9..0000000000
--- a/guides/assets/images/icons/callouts/2.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/callouts/3.png b/guides/assets/images/icons/callouts/3.png
deleted file mode 100644
index 57a33d15b4..0000000000
--- a/guides/assets/images/icons/callouts/3.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/callouts/4.png b/guides/assets/images/icons/callouts/4.png
deleted file mode 100644
index f061ab02b8..0000000000
--- a/guides/assets/images/icons/callouts/4.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/callouts/5.png b/guides/assets/images/icons/callouts/5.png
deleted file mode 100644
index b4de02da11..0000000000
--- a/guides/assets/images/icons/callouts/5.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/callouts/6.png b/guides/assets/images/icons/callouts/6.png
deleted file mode 100644
index 0e055eec1e..0000000000
--- a/guides/assets/images/icons/callouts/6.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/callouts/7.png b/guides/assets/images/icons/callouts/7.png
deleted file mode 100644
index 5ead87d040..0000000000
--- a/guides/assets/images/icons/callouts/7.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/callouts/8.png b/guides/assets/images/icons/callouts/8.png
deleted file mode 100644
index cb99545eb6..0000000000
--- a/guides/assets/images/icons/callouts/8.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/callouts/9.png b/guides/assets/images/icons/callouts/9.png
deleted file mode 100644
index 0ac03602f6..0000000000
--- a/guides/assets/images/icons/callouts/9.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/caution.png b/guides/assets/images/icons/caution.png
deleted file mode 100644
index 7227b54b32..0000000000
--- a/guides/assets/images/icons/caution.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/example.png b/guides/assets/images/icons/example.png
deleted file mode 100644
index de23c0aa87..0000000000
--- a/guides/assets/images/icons/example.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/home.png b/guides/assets/images/icons/home.png
deleted file mode 100644
index 24149d6e78..0000000000
--- a/guides/assets/images/icons/home.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/important.png b/guides/assets/images/icons/important.png
deleted file mode 100644
index dafcf0f59e..0000000000
--- a/guides/assets/images/icons/important.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/next.png b/guides/assets/images/icons/next.png
deleted file mode 100644
index 355b329f5a..0000000000
--- a/guides/assets/images/icons/next.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/note.png b/guides/assets/images/icons/note.png
deleted file mode 100644
index 08d35a6f5c..0000000000
--- a/guides/assets/images/icons/note.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/prev.png b/guides/assets/images/icons/prev.png
deleted file mode 100644
index ea564c865e..0000000000
--- a/guides/assets/images/icons/prev.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/tip.png b/guides/assets/images/icons/tip.png
deleted file mode 100644
index d834e6d1bb..0000000000
--- a/guides/assets/images/icons/tip.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/up.png b/guides/assets/images/icons/up.png
deleted file mode 100644
index 379f0045af..0000000000
--- a/guides/assets/images/icons/up.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/icons/warning.png b/guides/assets/images/icons/warning.png
deleted file mode 100644
index 72a8a5d873..0000000000
--- a/guides/assets/images/icons/warning.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/oscardelben.jpg b/guides/assets/images/oscardelben.jpg
deleted file mode 100644
index 9f3f67c2c7..0000000000
--- a/guides/assets/images/oscardelben.jpg
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/polymorphic.png b/guides/assets/images/polymorphic.png
deleted file mode 100644
index a3cbc4502a..0000000000
--- a/guides/assets/images/polymorphic.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/radar.png b/guides/assets/images/radar.png
deleted file mode 100644
index 421b62b623..0000000000
--- a/guides/assets/images/radar.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/rails4_features.png b/guides/assets/images/rails4_features.png
deleted file mode 100644
index b3bd5ef69e..0000000000
--- a/guides/assets/images/rails4_features.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/rails_guides_logo.gif b/guides/assets/images/rails_guides_logo.gif
index 9b0ad5af28..f7149a0415 100644
--- a/guides/assets/images/rails_guides_logo.gif
+++ b/guides/assets/images/rails_guides_logo.gif
Binary files differ
diff --git a/guides/assets/images/rails_guides_logo_1x.png b/guides/assets/images/rails_guides_logo_1x.png
new file mode 100644
index 0000000000..8c6810c312
--- /dev/null
+++ b/guides/assets/images/rails_guides_logo_1x.png
Binary files differ
diff --git a/guides/assets/images/rails_guides_logo_2x.png b/guides/assets/images/rails_guides_logo_2x.png
new file mode 100644
index 0000000000..accc6bbfa4
--- /dev/null
+++ b/guides/assets/images/rails_guides_logo_2x.png
Binary files differ
diff --git a/guides/assets/images/rails_logo_remix.gif b/guides/assets/images/rails_logo_remix.gif
deleted file mode 100644
index 58960ee4f9..0000000000
--- a/guides/assets/images/rails_logo_remix.gif
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/csrf.png b/guides/assets/images/security/csrf.png
index a8123d47c3..a8123d47c3 100644
--- a/guides/assets/images/csrf.png
+++ b/guides/assets/images/security/csrf.png
Binary files differ
diff --git a/guides/assets/images/security/session_fixation.png b/guides/assets/images/security/session_fixation.png
new file mode 100644
index 0000000000..e009484f09
--- /dev/null
+++ b/guides/assets/images/security/session_fixation.png
Binary files differ
diff --git a/guides/assets/images/session_fixation.png b/guides/assets/images/session_fixation.png
deleted file mode 100644
index ac3ab01614..0000000000
--- a/guides/assets/images/session_fixation.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/tab_yellow.png b/guides/assets/images/tab_yellow.png
deleted file mode 100644
index 3ab1c56c4d..0000000000
--- a/guides/assets/images/tab_yellow.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/vijaydev.jpg b/guides/assets/images/vijaydev.jpg
deleted file mode 100644
index fe5e4f1cb4..0000000000
--- a/guides/assets/images/vijaydev.jpg
+++ /dev/null
Binary files differ
diff --git a/guides/assets/javascripts/guides.js b/guides/assets/javascripts/guides.js
index a9c7f0d016..a37f5d1927 100644
--- a/guides/assets/javascripts/guides.js
+++ b/guides/assets/javascripts/guides.js
@@ -1,59 +1,75 @@
-$.fn.selectGuide = function(guide) {
- $("select", this).val(guide);
-};
-
-var guidesIndex = {
- bind: function() {
- var currentGuidePath = window.location.pathname;
- var currentGuide = currentGuidePath.substring(currentGuidePath.lastIndexOf("/")+1);
- $(".guides-index-small").
- on("change", "select", guidesIndex.navigate).
- selectGuide(currentGuide);
- $(document).on("click", ".more-info-button", function(e){
- e.stopPropagation();
- if ($(".more-info-links").is(":visible")) {
- $(".more-info-links").addClass("s-hidden").unwrap();
- } else {
- $(".more-info-links").wrap("<div class='more-info-container'></div>").removeClass("s-hidden");
- }
+(function() {
+ "use strict";
+
+ this.syntaxhighlighterConfig = { autoLinks: false };
+
+ this.wrap = function(elem, wrapper) {
+ elem.parentNode.insertBefore(wrapper, elem);
+ wrapper.appendChild(elem);
+ }
+
+ this.unwrap = function(elem) {
+ var wrapper = elem.parentNode;
+ wrapper.parentNode.replaceChild(elem, wrapper);
+ }
+
+ this.createElement = function(tagName, className) {
+ var elem = document.createElement(tagName);
+ elem.classList.add(className);
+ return elem;
+ }
+
+ // For old browsers
+ this.each = function(node, callback) {
+ var array = Array.prototype.slice.call(node);
+ for(var i = 0; i < array.length; i++) callback(array[i]);
+ }
+
+ // Viewable on local
+ if (window.location.protocol === "file:") Turbolinks.supported = false;
+
+ document.addEventListener("turbolinks:load", function() {
+ window.SyntaxHighlighter.highlight({ "auto-links": false });
+
+ var guidesMenu = document.getElementById("guidesMenu");
+ var guides = document.getElementById("guides");
+
+ guidesMenu.addEventListener("click", function(e) {
+ e.preventDefault();
+ guides.classList.toggle("visible");
});
- $("#guidesMenu").on("click", function(e) {
- $("#guides").toggle();
- return false;
+
+ each(document.querySelectorAll("#guides a"), function(element) {
+ element.addEventListener("click", function(e) {
+ guides.classList.toggle("visible");
+ });
});
- $(document).on("click", function(e){
- e.stopPropagation();
- var $button = $(".more-info-button");
- var element;
- // Cross browser find the element that had the event
- if (e.target) element = e.target;
- else if (e.srcElement) element = e.srcElement;
+ var guidesIndexItem = document.querySelector("select.guides-index-item");
+ var currentGuidePath = window.location.pathname;
+ guidesIndexItem.value = currentGuidePath.substring(currentGuidePath.lastIndexOf("/") + 1);
- // Defeat the older Safari bug:
- // http://www.quirksmode.org/js/events_properties.html
- if (element.nodeType === 3) element = element.parentNode;
+ guidesIndexItem.addEventListener("change", function(e) {
+ if (Turbolinks.supported) {
+ Turbolinks.visit(e.target.value);
+ } else {
+ window.location = e.target.value;
+ }
+ });
- var $element = $(element);
+ var moreInfoButton = document.querySelector(".more-info-button");
+ var moreInfoLinks = document.querySelector(".more-info-links");
- var $container = $element.parents(".more-info-container");
+ moreInfoButton.addEventListener("click", function(e) {
+ e.preventDefault();
- // We've captured a click outside the popup
- if($container.length === 0){
- $container = $button.next(".more-info-container");
- $container.find(".more-info-links").addClass("s-hidden").unwrap();
+ if (moreInfoLinks.classList.contains("s-hidden")) {
+ wrap(moreInfoLinks, createElement("div", "more-info-container"));
+ moreInfoLinks.classList.remove("s-hidden");
+ } else {
+ moreInfoLinks.classList.add("s-hidden");
+ unwrap(moreInfoLinks);
}
});
- },
- navigate: function(e){
- var $list = $(e.target);
- var url = $list.val();
- window.location = url;
- }
-};
-
-// Disable autolink inside example code blocks of guides.
-$(document).ready(function() {
- SyntaxHighlighter.defaults['auto-links'] = false;
- SyntaxHighlighter.all();
-});
+ });
+}).call(this);
diff --git a/guides/assets/javascripts/jquery.min.js b/guides/assets/javascripts/jquery.min.js
deleted file mode 100644
index 93adea19fd..0000000000
--- a/guides/assets/javascripts/jquery.min.js
+++ /dev/null
@@ -1,4 +0,0 @@
-/*! jQuery v1.7.2 jquery.com | jquery.org/license */
-(function(a,b){function cy(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cu(a){if(!cj[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){ck||(ck=c.createElement("iframe"),ck.frameBorder=ck.width=ck.height=0),b.appendChild(ck);if(!cl||!ck.createElement)cl=(ck.contentWindow||ck.contentDocument).document,cl.write((f.support.boxModel?"<!doctype html>":"")+"<html><body>"),cl.close();d=cl.createElement(a),cl.body.appendChild(d),e=f.css(d,"display"),b.removeChild(ck)}cj[a]=e}return cj[a]}function ct(a,b){var c={};f.each(cp.concat.apply([],cp.slice(0,b)),function(){c[this]=a});return c}function cs(){cq=b}function cr(){setTimeout(cs,0);return cq=f.now()}function ci(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ch(){try{return new a.XMLHttpRequest}catch(b){}}function cb(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g<i;g++){if(g===1)for(h in a.converters)typeof h=="string"&&(e[h.toLowerCase()]=a.converters[h]);l=k,k=d[g];if(k==="*")k=l;else if(l!=="*"&&l!==k){m=l+" "+k,n=e[m]||e["* "+k];if(!n){p=b;for(o in e){j=o.split(" ");if(j[0]===l||j[0]==="*"){p=e[j[1]+" "+k];if(p){o=e[o],o===!0?n=p:p===!0&&(n=o);break}}}}!n&&!p&&f.error("No conversion from "+m.replace(" "," to ")),n!==!0&&(c=n?n(c):p(o(c)))}}return c}function ca(a,c,d){var e=a.contents,f=a.dataTypes,g=a.responseFields,h,i,j,k;for(i in g)i in d&&(c[g[i]]=d[i]);while(f[0]==="*")f.shift(),h===b&&(h=a.mimeType||c.getResponseHeader("content-type"));if(h)for(i in e)if(e[i]&&e[i].test(h)){f.unshift(i);break}if(f[0]in d)j=f[0];else{for(i in d){if(!f[0]||a.converters[i+" "+f[0]]){j=i;break}k||(k=i)}j=j||k}if(j){j!==f[0]&&f.unshift(j);return d[j]}}function b_(a,b,c,d){if(f.isArray(b))f.each(b,function(b,e){c||bD.test(a)?d(a,e):b_(a+"["+(typeof e=="object"?b:"")+"]",e,c,d)});else if(!c&&f.type(b)==="object")for(var e in b)b_(a+"["+e+"]",b[e],c,d);else d(a,b)}function b$(a,c){var d,e,g=f.ajaxSettings.flatOptions||{};for(d in c)c[d]!==b&&((g[d]?a:e||(e={}))[d]=c[d]);e&&f.extend(!0,a,e)}function bZ(a,c,d,e,f,g){f=f||c.dataTypes[0],g=g||{},g[f]=!0;var h=a[f],i=0,j=h?h.length:0,k=a===bS,l;for(;i<j&&(k||!l);i++)l=h[i](c,d,e),typeof l=="string"&&(!k||g[l]?l=b:(c.dataTypes.unshift(l),l=bZ(a,c,d,e,l,g)));(k||!l)&&!g["*"]&&(l=bZ(a,c,d,e,"*",g));return l}function bY(a){return function(b,c){typeof b!="string"&&(c=b,b="*");if(f.isFunction(c)){var d=b.toLowerCase().split(bO),e=0,g=d.length,h,i,j;for(;e<g;e++)h=d[e],j=/^\+/.test(h),j&&(h=h.substr(1)||"*"),i=a[h]=a[h]||[],i[j?"unshift":"push"](c)}}}function bB(a,b,c){var d=b==="width"?a.offsetWidth:a.offsetHeight,e=b==="width"?1:0,g=4;if(d>0){if(c!=="border")for(;e<g;e+=2)c||(d-=parseFloat(f.css(a,"padding"+bx[e]))||0),c==="margin"?d+=parseFloat(f.css(a,c+bx[e]))||0:d-=parseFloat(f.css(a,"border"+bx[e]+"Width"))||0;return d+"px"}d=by(a,b);if(d<0||d==null)d=a.style[b];if(bt.test(d))return d;d=parseFloat(d)||0;if(c)for(;e<g;e+=2)d+=parseFloat(f.css(a,"padding"+bx[e]))||0,c!=="padding"&&(d+=parseFloat(f.css(a,"border"+bx[e]+"Width"))||0),c==="margin"&&(d+=parseFloat(f.css(a,c+bx[e]))||0);return d+"px"}function bo(a){var b=c.createElement("div");bh.appendChild(b),b.innerHTML=a.outerHTML;return b.firstChild}function bn(a){var b=(a.nodeName||"").toLowerCase();b==="input"?bm(a):b!=="script"&&typeof a.getElementsByTagName!="undefined"&&f.grep(a.getElementsByTagName("input"),bm)}function bm(a){if(a.type==="checkbox"||a.type==="radio")a.defaultChecked=a.checked}function bl(a){return typeof a.getElementsByTagName!="undefined"?a.getElementsByTagName("*"):typeof a.querySelectorAll!="undefined"?a.querySelectorAll("*"):[]}function bk(a,b){var c;b.nodeType===1&&(b.clearAttributes&&b.clearAttributes(),b.mergeAttributes&&b.mergeAttributes(a),c=b.nodeName.toLowerCase(),c==="object"?b.outerHTML=a.outerHTML:c!=="input"||a.type!=="checkbox"&&a.type!=="radio"?c==="option"?b.selected=a.defaultSelected:c==="input"||c==="textarea"?b.defaultValue=a.defaultValue:c==="script"&&b.text!==a.text&&(b.text=a.text):(a.checked&&(b.defaultChecked=b.checked=a.checked),b.value!==a.value&&(b.value=a.value)),b.removeAttribute(f.expando),b.removeAttribute("_submit_attached"),b.removeAttribute("_change_attached"))}function bj(a,b){if(b.nodeType===1&&!!f.hasData(a)){var c,d,e,g=f._data(a),h=f._data(b,g),i=g.events;if(i){delete h.handle,h.events={};for(c in i)for(d=0,e=i[c].length;d<e;d++)f.event.add(b,c,i[c][d])}h.data&&(h.data=f.extend({},h.data))}}function bi(a,b){return f.nodeName(a,"table")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function U(a){var b=V.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}function T(a,b,c){b=b||0;if(f.isFunction(b))return f.grep(a,function(a,d){var e=!!b.call(a,d,a);return e===c});if(b.nodeType)return f.grep(a,function(a,d){return a===b===c});if(typeof b=="string"){var d=f.grep(a,function(a){return a.nodeType===1});if(O.test(b))return f.filter(b,d,!c);b=f.filter(b,d)}return f.grep(a,function(a,d){return f.inArray(a,b)>=0===c})}function S(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function K(){return!0}function J(){return!1}function n(a,b,c){var d=b+"defer",e=b+"queue",g=b+"mark",h=f._data(a,d);h&&(c==="queue"||!f._data(a,e))&&(c==="mark"||!f._data(a,g))&&setTimeout(function(){!f._data(a,e)&&!f._data(a,g)&&(f.removeData(a,d,!0),h.fire())},0)}function m(a){for(var b in a){if(b==="data"&&f.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function l(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(k,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNumeric(d)?+d:j.test(d)?f.parseJSON(d):d}catch(g){}f.data(a,c,d)}else d=b}return d}function h(a){var b=g[a]={},c,d;a=a.split(/\s+/);for(c=0,d=a.length;c<d;c++)b[a[c]]=!0;return b}var c=a.document,d=a.navigator,e=a.location,f=function(){function J(){if(!e.isReady){try{c.documentElement.doScroll("left")}catch(a){setTimeout(J,1);return}e.ready()}}var e=function(a,b){return new e.fn.init(a,b,h)},f=a.jQuery,g=a.$,h,i=/^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,n=/^[\],:{}\s]*$/,o=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,p=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,q=/(?:^|:|,)(?:\s*\[)+/g,r=/(webkit)[ \/]([\w.]+)/,s=/(opera)(?:.*version)?[ \/]([\w.]+)/,t=/(msie) ([\w.]+)/,u=/(mozilla)(?:.*? rv:([\w.]+))?/,v=/-([a-z]|[0-9])/ig,w=/^-ms-/,x=function(a,b){return(b+"").toUpperCase()},y=d.userAgent,z,A,B,C=Object.prototype.toString,D=Object.prototype.hasOwnProperty,E=Array.prototype.push,F=Array.prototype.slice,G=String.prototype.trim,H=Array.prototype.indexOf,I={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=m.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.7.2",length:0,size:function(){return this.length},toArray:function(){return F.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?E.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),A.add(a);return this},eq:function(a){a=+a;return a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(F.apply(this,arguments),"slice",F.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:E,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j<k;j++)if((a=arguments[j])!=null)for(c in a){d=i[c],f=a[c];if(i===f)continue;l&&f&&(e.isPlainObject(f)||(g=e.isArray(f)))?(g?(g=!1,h=d&&e.isArray(d)?d:[]):h=d&&e.isPlainObject(d)?d:{},i[c]=e.extend(l,h,f)):f!==b&&(i[c]=f)}return i},e.extend({noConflict:function(b){a.$===e&&(a.$=g),b&&a.jQuery===e&&(a.jQuery=f);return e},isReady:!1,readyWait:1,holdReady:function(a){a?e.readyWait++:e.ready(!0)},ready:function(a){if(a===!0&&!--e.readyWait||a!==!0&&!e.isReady){if(!c.body)return setTimeout(e.ready,1);e.isReady=!0;if(a!==!0&&--e.readyWait>0)return;A.fireWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").off("ready")}},bindReady:function(){if(!A){A=e.Callbacks("once memory");if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",B,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",B),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&J()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a!=null&&a==a.window},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):I[C.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;try{if(a.constructor&&!D.call(a,"constructor")&&!D.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||D.call(a,d)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw new Error(a)},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(n.test(b.replace(o,"@").replace(p,"]").replace(q,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(c){if(typeof c!="string"||!c)return null;var d,f;try{a.DOMParser?(f=new DOMParser,d=f.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(g){d=b}(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&e.error("Invalid XML: "+c);return d},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(w,"ms-").replace(v,x)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g<h;)if(c.apply(a[g++],d)===!1)break}else if(i){for(f in a)if(c.call(a[f],f,a[f])===!1)break}else for(;g<h;)if(c.call(a[g],g,a[g++])===!1)break;return a},trim:G?function(a){return a==null?"":G.call(a)}:function(a){return a==null?"":(a+"").replace(k,"").replace(l,"")},makeArray:function(a,b){var c=b||[];if(a!=null){var d=e.type(a);a.length==null||d==="string"||d==="function"||d==="regexp"||e.isWindow(a)?E.call(c,a):e.merge(c,a)}return c},inArray:function(a,b,c){var d;if(b){if(H)return H.call(b,a,c);d=b.length,c=c?c<0?Math.max(0,d+c):c:0;for(;c<d;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,c){var d=a.length,e=0;if(typeof c.length=="number")for(var f=c.length;e<f;e++)a[d++]=c[e];else while(c[e]!==b)a[d++]=c[e++];a.length=d;return a},grep:function(a,b,c){var d=[],e;c=!!c;for(var f=0,g=a.length;f<g;f++)e=!!b(a[f],f),c!==e&&d.push(a[f]);return d},map:function(a,c,d){var f,g,h=[],i=0,j=a.length,k=a instanceof e||j!==b&&typeof j=="number"&&(j>0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i<j;i++)f=c(a[i],i,d),f!=null&&(h[h.length]=f);else for(g in a)f=c(a[g],g,d),f!=null&&(h[h.length]=f);return h.concat.apply([],h)},guid:1,proxy:function(a,c){if(typeof c=="string"){var d=a[c];c=a,a=d}if(!e.isFunction(a))return b;var f=F.call(arguments,2),g=function(){return a.apply(c,f.concat(F.call(arguments)))};g.guid=a.guid=a.guid||g.guid||e.guid++;return g},access:function(a,c,d,f,g,h,i){var j,k=d==null,l=0,m=a.length;if(d&&typeof d=="object"){for(l in d)e.access(a,c,l,d[l],1,h,f);g=1}else if(f!==b){j=i===b&&e.isFunction(f),k&&(j?(j=c,c=function(a,b,c){return j.call(e(a),c)}):(c.call(a,f),c=null));if(c)for(;l<m;l++)c(a[l],d,j?f.call(a[l],l,c(a[l],d)):f,i);g=1}return g?a:k?c.call(a):m?c(a[0],d):h},now:function(){return(new Date).getTime()},uaMatch:function(a){a=a.toLowerCase();var b=r.exec(a)||s.exec(a)||t.exec(a)||a.indexOf("compatible")<0&&u.exec(a)||[];return{browser:b[1]||"",version:b[2]||"0"}},sub:function(){function a(b,c){return new a.fn.init(b,c)}e.extend(!0,a,this),a.superclass=this,a.fn=a.prototype=this(),a.fn.constructor=a,a.sub=this.sub,a.fn.init=function(d,f){f&&f instanceof e&&!(f instanceof a)&&(f=a(f));return e.fn.init.call(this,d,f,b)},a.fn.init.prototype=a.fn;var b=a(c);return a},browser:{}}),e.each("Boolean Number String Function Array Date RegExp Object".split(" "),function(a,b){I["[object "+b+"]"]=b.toLowerCase()}),z=e.uaMatch(y),z.browser&&(e.browser[z.browser]=!0,e.browser.version=z.version),e.browser.webkit&&(e.browser.safari=!0),j.test(" ")&&(k=/^[\s\xA0]+/,l=/[\s\xA0]+$/),h=e(c),c.addEventListener?B=function(){c.removeEventListener("DOMContentLoaded",B,!1),e.ready()}:c.attachEvent&&(B=function(){c.readyState==="complete"&&(c.detachEvent("onreadystatechange",B),e.ready())});return e}(),g={};f.Callbacks=function(a){a=a?g[a]||h(a):{};var c=[],d=[],e,i,j,k,l,m,n=function(b){var d,e,g,h,i;for(d=0,e=b.length;d<e;d++)g=b[d],h=f.type(g),h==="array"?n(g):h==="function"&&(!a.unique||!p.has(g))&&c.push(g)},o=function(b,f){f=f||[],e=!a.memory||[b,f],i=!0,j=!0,m=k||0,k=0,l=c.length;for(;c&&m<l;m++)if(c[m].apply(b,f)===!1&&a.stopOnFalse){e=!0;break}j=!1,c&&(a.once?e===!0?p.disable():c=[]:d&&d.length&&(e=d.shift(),p.fireWith(e[0],e[1])))},p={add:function(){if(c){var a=c.length;n(arguments),j?l=c.length:e&&e!==!0&&(k=a,o(e[0],e[1]))}return this},remove:function(){if(c){var b=arguments,d=0,e=b.length;for(;d<e;d++)for(var f=0;f<c.length;f++)if(b[d]===c[f]){j&&f<=l&&(l--,f<=m&&m--),c.splice(f--,1);if(a.unique)break}}return this},has:function(a){if(c){var b=0,d=c.length;for(;b<d;b++)if(a===c[b])return!0}return!1},empty:function(){c=[];return this},disable:function(){c=d=e=b;return this},disabled:function(){return!c},lock:function(){d=b,(!e||e===!0)&&p.disable();return this},locked:function(){return!d},fireWith:function(b,c){d&&(j?a.once||d.push([b,c]):(!a.once||!e)&&o(b,c));return this},fire:function(){p.fireWith(this,arguments);return this},fired:function(){return!!i}};return p};var i=[].slice;f.extend({Deferred:function(a){var b=f.Callbacks("once memory"),c=f.Callbacks("once memory"),d=f.Callbacks("memory"),e="pending",g={resolve:b,reject:c,notify:d},h={done:b.add,fail:c.add,progress:d.add,state:function(){return e},isResolved:b.fired,isRejected:c.fired,then:function(a,b,c){i.done(a).fail(b).progress(c);return this},always:function(){i.done.apply(i,arguments).fail.apply(i,arguments);return this},pipe:function(a,b,c){return f.Deferred(function(d){f.each({done:[a,"resolve"],fail:[b,"reject"],progress:[c,"notify"]},function(a,b){var c=b[0],e=b[1],g;f.isFunction(c)?i[a](function(){g=c.apply(this,arguments),g&&f.isFunction(g.promise)?g.promise().then(d.resolve,d.reject,d.notify):d[e+"With"](this===i?d:this,[g])}):i[a](d[e])})}).promise()},promise:function(a){if(a==null)a=h;else for(var b in h)a[b]=h[b];return a}},i=h.promise({}),j;for(j in g)i[j]=g[j].fire,i[j+"With"]=g[j].fireWith;i.done(function(){e="resolved"},c.disable,d.lock).fail(function(){e="rejected"},b.disable,d.lock),a&&a.call(i,i);return i},when:function(a){function m(a){return function(b){e[a]=arguments.length>1?i.call(arguments,0):b,j.notifyWith(k,e)}}function l(a){return function(c){b[a]=arguments.length>1?i.call(arguments,0):c,--g||j.resolveWith(j,b)}}var b=i.call(arguments,0),c=0,d=b.length,e=Array(d),g=d,h=d,j=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred(),k=j.promise();if(d>1){for(;c<d;c++)b[c]&&b[c].promise&&f.isFunction(b[c].promise)?b[c].promise().then(l(c),j.reject,m(c)):--g;g||j.resolveWith(j,b)}else j!==a&&j.resolveWith(j,d?[a]:[]);return k}}),f.support=function(){var b,d,e,g,h,i,j,k,l,m,n,o,p=c.createElement("div"),q=c.documentElement;p.setAttribute("className","t"),p.innerHTML=" <link/><table></table><a href='/a' style='top:1px;float:left;opacity:.55;'>a</a><input type='checkbox'/>",d=p.getElementsByTagName("*"),e=p.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=p.getElementsByTagName("input")[0],b={leadingWhitespace:p.firstChild.nodeType===3,tbody:!p.getElementsByTagName("tbody").length,htmlSerialize:!!p.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:p.className!=="t",enctype:!!c.createElement("form").enctype,html5Clone:c.createElement("nav").cloneNode(!0).outerHTML!=="<:nav></:nav>",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,pixelMargin:!0},f.boxModel=b.boxModel=c.compatMode==="CSS1Compat",i.checked=!0,b.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,b.optDisabled=!h.disabled;try{delete p.test}catch(r){b.deleteExpando=!1}!p.addEventListener&&p.attachEvent&&p.fireEvent&&(p.attachEvent("onclick",function(){b.noCloneEvent=!1}),p.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),b.radioValue=i.value==="t",i.setAttribute("checked","checked"),i.setAttribute("name","t"),p.appendChild(i),j=c.createDocumentFragment(),j.appendChild(p.lastChild),b.checkClone=j.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=i.checked,j.removeChild(i),j.appendChild(p);if(p.attachEvent)for(n in{submit:1,change:1,focusin:1})m="on"+n,o=m in p,o||(p.setAttribute(m,"return;"),o=typeof p[m]=="function"),b[n+"Bubbles"]=o;j.removeChild(p),j=g=h=p=i=null,f(function(){var d,e,g,h,i,j,l,m,n,q,r,s,t,u=c.getElementsByTagName("body")[0];!u||(m=1,t="padding:0;margin:0;border:",r="position:absolute;top:0;left:0;width:1px;height:1px;",s=t+"0;visibility:hidden;",n="style='"+r+t+"5px solid #000;",q="<div "+n+"display:block;'><div style='"+t+"0;display:block;overflow:hidden;'></div></div>"+"<table "+n+"' cellpadding='0' cellspacing='0'>"+"<tr><td></td></tr></table>",d=c.createElement("div"),d.style.cssText=s+"width:0;height:0;position:static;top:0;margin-top:"+m+"px",u.insertBefore(d,u.firstChild),p=c.createElement("div"),d.appendChild(p),p.innerHTML="<table><tr><td style='"+t+"0;display:none'></td><td>t</td></tr></table>",k=p.getElementsByTagName("td"),o=k[0].offsetHeight===0,k[0].style.display="",k[1].style.display="none",b.reliableHiddenOffsets=o&&k[0].offsetHeight===0,a.getComputedStyle&&(p.innerHTML="",l=c.createElement("div"),l.style.width="0",l.style.marginRight="0",p.style.width="2px",p.appendChild(l),b.reliableMarginRight=(parseInt((a.getComputedStyle(l,null)||{marginRight:0}).marginRight,10)||0)===0),typeof p.style.zoom!="undefined"&&(p.innerHTML="",p.style.width=p.style.padding="1px",p.style.border=0,p.style.overflow="hidden",p.style.display="inline",p.style.zoom=1,b.inlineBlockNeedsLayout=p.offsetWidth===3,p.style.display="block",p.style.overflow="visible",p.innerHTML="<div style='width:5px;'></div>",b.shrinkWrapBlocks=p.offsetWidth!==3),p.style.cssText=r+s,p.innerHTML=q,e=p.firstChild,g=e.firstChild,i=e.nextSibling.firstChild.firstChild,j={doesNotAddBorder:g.offsetTop!==5,doesAddBorderForTableAndCells:i.offsetTop===5},g.style.position="fixed",g.style.top="20px",j.fixedPosition=g.offsetTop===20||g.offsetTop===15,g.style.position=g.style.top="",e.style.overflow="hidden",e.style.position="relative",j.subtractsBorderForOverflowNotVisible=g.offsetTop===-5,j.doesNotIncludeMarginInBodyOffset=u.offsetTop!==m,a.getComputedStyle&&(p.style.marginTop="1%",b.pixelMargin=(a.getComputedStyle(p,null)||{marginTop:0}).marginTop!=="1%"),typeof d.style.zoom!="undefined"&&(d.style.zoom=1),u.removeChild(d),l=p=d=null,f.extend(b,j))});return b}();var j=/^(?:\{.*\}|\[.*\])$/,k=/([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!m(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g,h,i,j=f.expando,k=typeof c=="string",l=a.nodeType,m=l?f.cache:a,n=l?a[j]:a[j]&&j,o=c==="events";if((!n||!m[n]||!o&&!e&&!m[n].data)&&k&&d===b)return;n||(l?a[j]=n=++f.uuid:n=j),m[n]||(m[n]={},l||(m[n].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?m[n]=f.extend(m[n],c):m[n].data=f.extend(m[n].data,c);g=h=m[n],e||(h.data||(h.data={}),h=h.data),d!==b&&(h[f.camelCase(c)]=d);if(o&&!h[c])return g.events;k?(i=h[c],i==null&&(i=h[f.camelCase(c)])):i=h;return i}},removeData:function(a,b,c){if(!!f.acceptData(a)){var d,e,g,h=f.expando,i=a.nodeType,j=i?f.cache:a,k=i?a[h]:h;if(!j[k])return;if(b){d=c?j[k]:j[k].data;if(d){f.isArray(b)||(b in d?b=[b]:(b=f.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,g=b.length;e<g;e++)delete d[b[e]];if(!(c?m:f.isEmptyObject)(d))return}}if(!c){delete j[k].data;if(!m(j[k]))return}f.support.deleteExpando||!j.setInterval?delete j[k]:j[k]=null,i&&(f.support.deleteExpando?delete a[h]:a.removeAttribute?a.removeAttribute(h):a[h]=null)}},_data:function(a,b,c){return f.data(a,b,c,!0)},acceptData:function(a){if(a.nodeName){var b=f.noData[a.nodeName.toLowerCase()];if(b)return b!==!0&&a.getAttribute("classid")===b}return!0}}),f.fn.extend({data:function(a,c){var d,e,g,h,i,j=this[0],k=0,m=null;if(a===b){if(this.length){m=f.data(j);if(j.nodeType===1&&!f._data(j,"parsedAttrs")){g=j.attributes;for(i=g.length;k<i;k++)h=g[k].name,h.indexOf("data-")===0&&(h=f.camelCase(h.substring(5)),l(j,h,m[h]));f._data(j,"parsedAttrs",!0)}}return m}if(typeof a=="object")return this.each(function(){f.data(this,a)});d=a.split(".",2),d[1]=d[1]?"."+d[1]:"",e=d[1]+"!";return f.access(this,function(c){if(c===b){m=this.triggerHandler("getData"+e,[d[0]]),m===b&&j&&(m=f.data(j,a),m=l(j,a,m));return m===b&&d[1]?this.data(d[0]):m}d[1]=c,this.each(function(){var b=f(this);b.triggerHandler("setData"+e,d),f.data(this,a,c),b.triggerHandler("changeData"+e,d)})},null,c,arguments.length>1,null,!1)},removeData:function(a){return this.each(function(){f.removeData(this,a)})}}),f.extend({_mark:function(a,b){a&&(b=(b||"fx")+"mark",f._data(a,b,(f._data(a,b)||0)+1))},_unmark:function(a,b,c){a!==!0&&(c=b,b=a,a=!1);if(b){c=c||"fx";var d=c+"mark",e=a?0:(f._data(b,d)||1)-1;e?f._data(b,d,e):(f.removeData(b,d,!0),n(b,c,"mark"))}},queue:function(a,b,c){var d;if(a){b=(b||"fx")+"queue",d=f._data(a,b),c&&(!d||f.isArray(c)?d=f._data(a,b,f.makeArray(c)):d.push(c));return d||[]}},dequeue:function(a,b){b=b||"fx";var c=f.queue(a,b),d=c.shift(),e={};d==="inprogress"&&(d=c.shift()),d&&(b==="fx"&&c.unshift("inprogress"),f._data(a,b+".run",e),d.call(a,function(){f.dequeue(a,b)},e)),c.length||(f.removeData(a,b+"queue "+b+".run",!0),n(a,b,"queue"))}}),f.fn.extend({queue:function(a,c){var d=2;typeof a!="string"&&(c=a,a="fx",d--);if(arguments.length<d)return f.queue(this[0],a);return c===b?this:this.each(function(){var b=f.queue(this,a,c);a==="fx"&&b[0]!=="inprogress"&&f.dequeue(this,a)})},dequeue:function(a){return this.each(function(){f.dequeue(this,a)})},delay:function(a,b){a=f.fx?f.fx.speeds[a]||a:a,b=b||"fx";return this.queue(b,function(b,c){var d=setTimeout(b,a);c.stop=function(){clearTimeout(d)}})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,c){function m(){--h||d.resolveWith(e,[e])}typeof a!="string"&&(c=a,a=b),a=a||"fx";var d=f.Deferred(),e=this,g=e.length,h=1,i=a+"defer",j=a+"queue",k=a+"mark",l;while(g--)if(l=f.data(e[g],i,b,!0)||(f.data(e[g],j,b,!0)||f.data(e[g],k,b,!0))&&f.data(e[g],i,f.Callbacks("once memory"),!0))h++,l.add(m);m();return d.promise(c)}});var o=/[\n\t\r]/g,p=/\s+/,q=/\r/g,r=/^(?:button|input)$/i,s=/^(?:button|input|object|select|textarea)$/i,t=/^a(?:rea)?$/i,u=/^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,v=f.support.getSetAttribute,w,x,y;f.fn.extend({attr:function(a,b){return f.access(this,f.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){f.removeAttr(this,a)})},prop:function(a,b){return f.access(this,f.prop,a,b,arguments.length>1)},removeProp:function(a){a=f.propFix[a]||a;return this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){var b,c,d,e,g,h,i;if(f.isFunction(a))return this.each(function(b){f(this).addClass(a.call(this,b,this.className))});if(a&&typeof a=="string"){b=a.split(p);for(c=0,d=this.length;c<d;c++){e=this[c];if(e.nodeType===1)if(!e.className&&b.length===1)e.className=a;else{g=" "+e.className+" ";for(h=0,i=b.length;h<i;h++)~g.indexOf(" "+b[h]+" ")||(g+=b[h]+" ");e.className=f.trim(g)}}}return this},removeClass:function(a){var c,d,e,g,h,i,j;if(f.isFunction(a))return this.each(function(b){f(this).removeClass(a.call(this,b,this.className))});if(a&&typeof a=="string"||a===b){c=(a||"").split(p);for(d=0,e=this.length;d<e;d++){g=this[d];if(g.nodeType===1&&g.className)if(a){h=(" "+g.className+" ").replace(o," ");for(i=0,j=c.length;i<j;i++)h=h.replace(" "+c[i]+" "," ");g.className=f.trim(h)}else g.className=""}}return this},toggleClass:function(a,b){var c=typeof a,d=typeof b=="boolean";if(f.isFunction(a))return this.each(function(c){f(this).toggleClass(a.call(this,c,this.className,b),b)});return this.each(function(){if(c==="string"){var e,g=0,h=f(this),i=b,j=a.split(p);while(e=j[g++])i=d?i:!h.hasClass(e),h[i?"addClass":"removeClass"](e)}else if(c==="undefined"||c==="boolean")this.className&&f._data(this,"__className__",this.className),this.className=this.className||a===!1?"":f._data(this,"__className__")||""})},hasClass:function(a){var b=" "+a+" ",c=0,d=this.length;for(;c<d;c++)if(this[c].nodeType===1&&(" "+this[c].className+" ").replace(o," ").indexOf(b)>-1)return!0;return!1},val:function(a){var c,d,e,g=this[0];{if(!!arguments.length){e=f.isFunction(a);return this.each(function(d){var g=f(this),h;if(this.nodeType===1){e?h=a.call(this,d,g.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.type]||f.valHooks[this.nodeName.toLowerCase()];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}if(g){c=f.valHooks[g.type]||f.valHooks[g.nodeName.toLowerCase()];if(c&&"get"in c&&(d=c.get(g,"value"))!==b)return d;d=g.value;return typeof d=="string"?d.replace(q,""):d==null?"":d}}}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,g=a.selectedIndex,h=[],i=a.options,j=a.type==="select-one";if(g<0)return null;c=j?g:0,d=j?g+1:i.length;for(;c<d;c++){e=i[c];if(e.selected&&(f.support.optDisabled?!e.disabled:e.getAttribute("disabled")===null)&&(!e.parentNode.disabled||!f.nodeName(e.parentNode,"optgroup"))){b=f(e).val();if(j)return b;h.push(b)}}if(j&&!h.length&&i.length)return f(i[g]).val();return h},set:function(a,b){var c=f.makeArray(b);f(a).find("option").each(function(){this.selected=f.inArray(f(this).val(),c)>=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,d,e){var g,h,i,j=a.nodeType;if(!!a&&j!==3&&j!==8&&j!==2){if(e&&c in f.attrFn)return f(a)[c](d);if(typeof a.getAttribute=="undefined")return f.prop(a,c,d);i=j!==1||!f.isXMLDoc(a),i&&(c=c.toLowerCase(),h=f.attrHooks[c]||(u.test(c)?x:w));if(d!==b){if(d===null){f.removeAttr(a,c);return}if(h&&"set"in h&&i&&(g=h.set(a,d,c))!==b)return g;a.setAttribute(c,""+d);return d}if(h&&"get"in h&&i&&(g=h.get(a,c))!==null)return g;g=a.getAttribute(c);return g===null?b:g}},removeAttr:function(a,b){var c,d,e,g,h,i=0;if(b&&a.nodeType===1){d=b.toLowerCase().split(p),g=d.length;for(;i<g;i++)e=d[i],e&&(c=f.propFix[e]||e,h=u.test(e),h||f.attr(a,e,""),a.removeAttribute(v?e:c),h&&c in a&&(a[c]=!1))}},attrHooks:{type:{set:function(a,b){if(r.test(a.nodeName)&&a.parentNode)f.error("type property can't be changed");else if(!f.support.radioValue&&b==="radio"&&f.nodeName(a,"input")){var c=a.value;a.setAttribute("type",b),c&&(a.value=c);return b}}},value:{get:function(a,b){if(w&&f.nodeName(a,"button"))return w.get(a,b);return b in a?a.value:null},set:function(a,b,c){if(w&&f.nodeName(a,"button"))return w.set(a,b,c);a.value=b}}},propFix:{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},prop:function(a,c,d){var e,g,h,i=a.nodeType;if(!!a&&i!==3&&i!==8&&i!==2){h=i!==1||!f.isXMLDoc(a),h&&(c=f.propFix[c]||c,g=f.propHooks[c]);return d!==b?g&&"set"in g&&(e=g.set(a,d,c))!==b?e:a[c]=d:g&&"get"in g&&(e=g.get(a,c))!==null?e:a[c]}},propHooks:{tabIndex:{get:function(a){var c=a.getAttributeNode("tabindex");return c&&c.specified?parseInt(c.value,10):s.test(a.nodeName)||t.test(a.nodeName)&&a.href?0:b}}}}),f.attrHooks.tabindex=f.propHooks.tabIndex,x={get:function(a,c){var d,e=f.prop(a,c);return e===!0||typeof e!="boolean"&&(d=a.getAttributeNode(c))&&d.nodeValue!==!1?c.toLowerCase():b},set:function(a,b,c){var d;b===!1?f.removeAttr(a,c):(d=f.propFix[c]||c,d in a&&(a[d]=!0),a.setAttribute(c,c.toLowerCase()));return c}},v||(y={name:!0,id:!0,coords:!0},w=f.valHooks.button={get:function(a,c){var d;d=a.getAttributeNode(c);return d&&(y[c]?d.nodeValue!=="":d.specified)?d.nodeValue:b},set:function(a,b,d){var e=a.getAttributeNode(d);e||(e=c.createAttribute(d),a.setAttributeNode(e));return e.nodeValue=b+""}},f.attrHooks.tabindex.set=w.set,f.each(["width","height"],function(a,b){f.attrHooks[b]=f.extend(f.attrHooks[b],{set:function(a,c){if(c===""){a.setAttribute(b,"auto");return c}}})}),f.attrHooks.contenteditable={get:w.get,set:function(a,b,c){b===""&&(b="false"),w.set(a,b,c)}}),f.support.hrefNormalized||f.each(["href","src","width","height"],function(a,c){f.attrHooks[c]=f.extend(f.attrHooks[c],{get:function(a){var d=a.getAttribute(c,2);return d===null?b:d}})}),f.support.style||(f.attrHooks.style={get:function(a){return a.style.cssText.toLowerCase()||b},set:function(a,b){return a.style.cssText=""+b}}),f.support.optSelected||(f.propHooks.selected=f.extend(f.propHooks.selected,{get:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex);return null}})),f.support.enctype||(f.propFix.enctype="encoding"),f.support.checkOn||f.each(["radio","checkbox"],function(){f.valHooks[this]={get:function(a){return a.getAttribute("value")===null?"on":a.value}}}),f.each(["radio","checkbox"],function(){f.valHooks[this]=f.extend(f.valHooks[this],{set:function(a,b){if(f.isArray(b))return a.checked=f.inArray(f(a).val(),b)>=0}})});var z=/^(?:textarea|input|select)$/i,A=/^([^\.]*)?(?:\.(.+))?$/,B=/(?:^|\s)hover(\.\S+)?\b/,C=/^key/,D=/^(?:mouse|contextmenu)|click/,E=/^(?:focusinfocus|focusoutblur)$/,F=/^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,G=function(
-a){var b=F.exec(a);b&&(b[1]=(b[1]||"").toLowerCase(),b[3]=b[3]&&new RegExp("(?:^|\\s)"+b[3]+"(?:\\s|$)"));return b},H=function(a,b){var c=a.attributes||{};return(!b[1]||a.nodeName.toLowerCase()===b[1])&&(!b[2]||(c.id||{}).value===b[2])&&(!b[3]||b[3].test((c["class"]||{}).value))},I=function(a){return f.event.special.hover?a:a.replace(B,"mouseenter$1 mouseleave$1")};f.event={add:function(a,c,d,e,g){var h,i,j,k,l,m,n,o,p,q,r,s;if(!(a.nodeType===3||a.nodeType===8||!c||!d||!(h=f._data(a)))){d.handler&&(p=d,d=p.handler,g=p.selector),d.guid||(d.guid=f.guid++),j=h.events,j||(h.events=j={}),i=h.handle,i||(h.handle=i=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.dispatch.apply(i.elem,arguments):b},i.elem=a),c=f.trim(I(c)).split(" ");for(k=0;k<c.length;k++){l=A.exec(c[k])||[],m=l[1],n=(l[2]||"").split(".").sort(),s=f.event.special[m]||{},m=(g?s.delegateType:s.bindType)||m,s=f.event.special[m]||{},o=f.extend({type:m,origType:l[1],data:e,handler:d,guid:d.guid,selector:g,quick:g&&G(g),namespace:n.join(".")},p),r=j[m];if(!r){r=j[m]=[],r.delegateCount=0;if(!s.setup||s.setup.call(a,e,n,i)===!1)a.addEventListener?a.addEventListener(m,i,!1):a.attachEvent&&a.attachEvent("on"+m,i)}s.add&&(s.add.call(a,o),o.handler.guid||(o.handler.guid=d.guid)),g?r.splice(r.delegateCount++,0,o):r.push(o),f.event.global[m]=!0}a=null}},global:{},remove:function(a,b,c,d,e){var g=f.hasData(a)&&f._data(a),h,i,j,k,l,m,n,o,p,q,r,s;if(!!g&&!!(o=g.events)){b=f.trim(I(b||"")).split(" ");for(h=0;h<b.length;h++){i=A.exec(b[h])||[],j=k=i[1],l=i[2];if(!j){for(j in o)f.event.remove(a,j+b[h],c,d,!0);continue}p=f.event.special[j]||{},j=(d?p.delegateType:p.bindType)||j,r=o[j]||[],m=r.length,l=l?new RegExp("(^|\\.)"+l.split(".").sort().join("\\.(?:.*\\.)?")+"(\\.|$)"):null;for(n=0;n<r.length;n++)s=r[n],(e||k===s.origType)&&(!c||c.guid===s.guid)&&(!l||l.test(s.namespace))&&(!d||d===s.selector||d==="**"&&s.selector)&&(r.splice(n--,1),s.selector&&r.delegateCount--,p.remove&&p.remove.call(a,s));r.length===0&&m!==r.length&&((!p.teardown||p.teardown.call(a,l)===!1)&&f.removeEvent(a,j,g.handle),delete o[j])}f.isEmptyObject(o)&&(q=g.handle,q&&(q.elem=null),f.removeData(a,["events","handle"],!0))}},customEvent:{getData:!0,setData:!0,changeData:!0},trigger:function(c,d,e,g){if(!e||e.nodeType!==3&&e.nodeType!==8){var h=c.type||c,i=[],j,k,l,m,n,o,p,q,r,s;if(E.test(h+f.event.triggered))return;h.indexOf("!")>=0&&(h=h.slice(0,-1),k=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if((!e||f.event.customEvent[h])&&!f.event.global[h])return;c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.isTrigger=!0,c.exclusive=k,c.namespace=i.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)"):null,o=h.indexOf(":")<0?"on"+h:"";if(!e){j=f.cache;for(l in j)j[l].events&&j[l].events[h]&&f.event.trigger(c,d,j[l].handle.elem,!0);return}c.result=b,c.target||(c.target=e),d=d!=null?f.makeArray(d):[],d.unshift(c),p=f.event.special[h]||{};if(p.trigger&&p.trigger.apply(e,d)===!1)return;r=[[e,p.bindType||h]];if(!g&&!p.noBubble&&!f.isWindow(e)){s=p.delegateType||h,m=E.test(s+h)?e:e.parentNode,n=null;for(;m;m=m.parentNode)r.push([m,s]),n=m;n&&n===e.ownerDocument&&r.push([n.defaultView||n.parentWindow||a,s])}for(l=0;l<r.length&&!c.isPropagationStopped();l++)m=r[l][0],c.type=r[l][1],q=(f._data(m,"events")||{})[c.type]&&f._data(m,"handle"),q&&q.apply(m,d),q=o&&m[o],q&&f.acceptData(m)&&q.apply(m,d)===!1&&c.preventDefault();c.type=h,!g&&!c.isDefaultPrevented()&&(!p._default||p._default.apply(e.ownerDocument,d)===!1)&&(h!=="click"||!f.nodeName(e,"a"))&&f.acceptData(e)&&o&&e[h]&&(h!=="focus"&&h!=="blur"||c.target.offsetWidth!==0)&&!f.isWindow(e)&&(n=e[o],n&&(e[o]=null),f.event.triggered=h,e[h](),f.event.triggered=b,n&&(e[o]=n));return c.result}},dispatch:function(c){c=f.event.fix(c||a.event);var d=(f._data(this,"events")||{})[c.type]||[],e=d.delegateCount,g=[].slice.call(arguments,0),h=!c.exclusive&&!c.namespace,i=f.event.special[c.type]||{},j=[],k,l,m,n,o,p,q,r,s,t,u;g[0]=c,c.delegateTarget=this;if(!i.preDispatch||i.preDispatch.call(this,c)!==!1){if(e&&(!c.button||c.type!=="click")){n=f(this),n.context=this.ownerDocument||this;for(m=c.target;m!=this;m=m.parentNode||this)if(m.disabled!==!0){p={},r=[],n[0]=m;for(k=0;k<e;k++)s=d[k],t=s.selector,p[t]===b&&(p[t]=s.quick?H(m,s.quick):n.is(t)),p[t]&&r.push(s);r.length&&j.push({elem:m,matches:r})}}d.length>e&&j.push({elem:this,matches:d.slice(e)});for(k=0;k<j.length&&!c.isPropagationStopped();k++){q=j[k],c.currentTarget=q.elem;for(l=0;l<q.matches.length&&!c.isImmediatePropagationStopped();l++){s=q.matches[l];if(h||!c.namespace&&!s.namespace||c.namespace_re&&c.namespace_re.test(s.namespace))c.data=s.data,c.handleObj=s,o=((f.event.special[s.origType]||{}).handle||s.handler).apply(q.elem,g),o!==b&&(c.result=o,o===!1&&(c.preventDefault(),c.stopPropagation()))}}i.postDispatch&&i.postDispatch.call(this,c);return c.result}},props:"attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){a.which==null&&(a.which=b.charCode!=null?b.charCode:b.keyCode);return a}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(a,d){var e,f,g,h=d.button,i=d.fromElement;a.pageX==null&&d.clientX!=null&&(e=a.target.ownerDocument||c,f=e.documentElement,g=e.body,a.pageX=d.clientX+(f&&f.scrollLeft||g&&g.scrollLeft||0)-(f&&f.clientLeft||g&&g.clientLeft||0),a.pageY=d.clientY+(f&&f.scrollTop||g&&g.scrollTop||0)-(f&&f.clientTop||g&&g.clientTop||0)),!a.relatedTarget&&i&&(a.relatedTarget=i===a.target?d.toElement:i),!a.which&&h!==b&&(a.which=h&1?1:h&2?3:h&4?2:0);return a}},fix:function(a){if(a[f.expando])return a;var d,e,g=a,h=f.event.fixHooks[a.type]||{},i=h.props?this.props.concat(h.props):this.props;a=f.Event(g);for(d=i.length;d;)e=i[--d],a[e]=g[e];a.target||(a.target=g.srcElement||c),a.target.nodeType===3&&(a.target=a.target.parentNode),a.metaKey===b&&(a.metaKey=a.ctrlKey);return h.filter?h.filter(a,g):a},special:{ready:{setup:f.bindReady},load:{noBubble:!0},focus:{delegateType:"focusin"},blur:{delegateType:"focusout"},beforeunload:{setup:function(a,b,c){f.isWindow(this)&&(this.onbeforeunload=c)},teardown:function(a,b){this.onbeforeunload===b&&(this.onbeforeunload=null)}}},simulate:function(a,b,c,d){var e=f.extend(new f.Event,c,{type:a,isSimulated:!0,originalEvent:{}});d?f.event.trigger(e,null,b):f.event.dispatch.call(b,e),e.isDefaultPrevented()&&c.preventDefault()}},f.event.handle=f.event.dispatch,f.removeEvent=c.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)}:function(a,b,c){a.detachEvent&&a.detachEvent("on"+b,c)},f.Event=function(a,b){if(!(this instanceof f.Event))return new f.Event(a,b);a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||a.returnValue===!1||a.getPreventDefault&&a.getPreventDefault()?K:J):this.type=a,b&&f.extend(this,b),this.timeStamp=a&&a.timeStamp||f.now(),this[f.expando]=!0},f.Event.prototype={preventDefault:function(){this.isDefaultPrevented=K;var a=this.originalEvent;!a||(a.preventDefault?a.preventDefault():a.returnValue=!1)},stopPropagation:function(){this.isPropagationStopped=K;var a=this.originalEvent;!a||(a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=K,this.stopPropagation()},isDefaultPrevented:J,isPropagationStopped:J,isImmediatePropagationStopped:J},f.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){f.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c=this,d=a.relatedTarget,e=a.handleObj,g=e.selector,h;if(!d||d!==c&&!f.contains(c,d))a.type=e.origType,h=e.handler.apply(this,arguments),a.type=b;return h}}}),f.support.submitBubbles||(f.event.special.submit={setup:function(){if(f.nodeName(this,"form"))return!1;f.event.add(this,"click._submit keypress._submit",function(a){var c=a.target,d=f.nodeName(c,"input")||f.nodeName(c,"button")?c.form:b;d&&!d._submit_attached&&(f.event.add(d,"submit._submit",function(a){a._submit_bubble=!0}),d._submit_attached=!0)})},postDispatch:function(a){a._submit_bubble&&(delete a._submit_bubble,this.parentNode&&!a.isTrigger&&f.event.simulate("submit",this.parentNode,a,!0))},teardown:function(){if(f.nodeName(this,"form"))return!1;f.event.remove(this,"._submit")}}),f.support.changeBubbles||(f.event.special.change={setup:function(){if(z.test(this.nodeName)){if(this.type==="checkbox"||this.type==="radio")f.event.add(this,"propertychange._change",function(a){a.originalEvent.propertyName==="checked"&&(this._just_changed=!0)}),f.event.add(this,"click._change",function(a){this._just_changed&&!a.isTrigger&&(this._just_changed=!1,f.event.simulate("change",this,a,!0))});return!1}f.event.add(this,"beforeactivate._change",function(a){var b=a.target;z.test(b.nodeName)&&!b._change_attached&&(f.event.add(b,"change._change",function(a){this.parentNode&&!a.isSimulated&&!a.isTrigger&&f.event.simulate("change",this.parentNode,a,!0)}),b._change_attached=!0)})},handle:function(a){var b=a.target;if(this!==b||a.isSimulated||a.isTrigger||b.type!=="radio"&&b.type!=="checkbox")return a.handleObj.handler.apply(this,arguments)},teardown:function(){f.event.remove(this,"._change");return z.test(this.nodeName)}}),f.support.focusinBubbles||f.each({focus:"focusin",blur:"focusout"},function(a,b){var d=0,e=function(a){f.event.simulate(b,a.target,f.event.fix(a),!0)};f.event.special[b]={setup:function(){d++===0&&c.addEventListener(a,e,!0)},teardown:function(){--d===0&&c.removeEventListener(a,e,!0)}}}),f.fn.extend({on:function(a,c,d,e,g){var h,i;if(typeof a=="object"){typeof c!="string"&&(d=d||c,c=b);for(i in a)this.on(i,c,d,a[i],g);return this}d==null&&e==null?(e=c,d=c=b):e==null&&(typeof c=="string"?(e=d,d=b):(e=d,d=c,c=b));if(e===!1)e=J;else if(!e)return this;g===1&&(h=e,e=function(a){f().off(a);return h.apply(this,arguments)},e.guid=h.guid||(h.guid=f.guid++));return this.each(function(){f.event.add(this,a,e,d,c)})},one:function(a,b,c,d){return this.on(a,b,c,d,1)},off:function(a,c,d){if(a&&a.preventDefault&&a.handleObj){var e=a.handleObj;f(a.delegateTarget).off(e.namespace?e.origType+"."+e.namespace:e.origType,e.selector,e.handler);return this}if(typeof a=="object"){for(var g in a)this.off(g,c,a[g]);return this}if(c===!1||typeof c=="function")d=c,c=b;d===!1&&(d=J);return this.each(function(){f.event.remove(this,a,d,c)})},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},live:function(a,b,c){f(this.context).on(a,this.selector,b,c);return this},die:function(a,b){f(this.context).off(a,this.selector||"**",b);return this},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return arguments.length==1?this.off(a,"**"):this.off(b,a,c)},trigger:function(a,b){return this.each(function(){f.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0])return f.event.trigger(a,b,this[0],!0)},toggle:function(a){var b=arguments,c=a.guid||f.guid++,d=0,e=function(c){var e=(f._data(this,"lastToggle"+a.guid)||0)%d;f._data(this,"lastToggle"+a.guid,e+1),c.preventDefault();return b[e].apply(this,arguments)||!1};e.guid=c;while(d<b.length)b[d++].guid=c;return this.click(e)},hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),f.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){f.fn[b]=function(a,c){c==null&&(c=a,a=null);return arguments.length>0?this.on(b,null,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0),C.test(b)&&(f.event.fixHooks[b]=f.event.keyHooks),D.test(b)&&(f.event.fixHooks[b]=f.event.mouseHooks)}),function(){function x(a,b,c,e,f,g){for(var h=0,i=e.length;h<i;h++){var j=e[h];if(j){var k=!1;j=j[a];while(j){if(j[d]===c){k=e[j.sizset];break}if(j.nodeType===1){g||(j[d]=c,j.sizset=h);if(typeof b!="string"){if(j===b){k=!0;break}}else if(m.filter(b,[j]).length>0){k=j;break}}j=j[a]}e[h]=k}}}function w(a,b,c,e,f,g){for(var h=0,i=e.length;h<i;h++){var j=e[h];if(j){var k=!1;j=j[a];while(j){if(j[d]===c){k=e[j.sizset];break}j.nodeType===1&&!g&&(j[d]=c,j.sizset=h);if(j.nodeName.toLowerCase()===b){k=j;break}j=j[a]}e[h]=k}}}var a=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d="sizcache"+(Math.random()+"").replace(".",""),e=0,g=Object.prototype.toString,h=!1,i=!0,j=/\\/g,k=/\r\n/g,l=/\W/;[0,0].sort(function(){i=!1;return 0});var m=function(b,d,e,f){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return e;var i,j,k,l,n,q,r,t,u=!0,v=m.isXML(d),w=[],x=b;do{a.exec(""),i=a.exec(x);if(i){x=i[3],w.push(i[1]);if(i[2]){l=i[3];break}}}while(i);if(w.length>1&&p.exec(b))if(w.length===2&&o.relative[w[0]])j=y(w[0]+w[1],d,f);else{j=o.relative[w[0]]?[d]:m(w.shift(),d);while(w.length)b=w.shift(),o.relative[b]&&(b+=w.shift()),j=y(b,j,f)}else{!f&&w.length>1&&d.nodeType===9&&!v&&o.match.ID.test(w[0])&&!o.match.ID.test(w[w.length-1])&&(n=m.find(w.shift(),d,v),d=n.expr?m.filter(n.expr,n.set)[0]:n.set[0]);if(d){n=f?{expr:w.pop(),set:s(f)}:m.find(w.pop(),w.length===1&&(w[0]==="~"||w[0]==="+")&&d.parentNode?d.parentNode:d,v),j=n.expr?m.filter(n.expr,n.set):n.set,w.length>0?k=s(j):u=!1;while(w.length)q=w.pop(),r=q,o.relative[q]?r=w.pop():q="",r==null&&(r=d),o.relative[q](k,r,v)}else k=w=[]}k||(k=j),k||m.error(q||b);if(g.call(k)==="[object Array]")if(!u)e.push.apply(e,k);else if(d&&d.nodeType===1)for(t=0;k[t]!=null;t++)k[t]&&(k[t]===!0||k[t].nodeType===1&&m.contains(d,k[t]))&&e.push(j[t]);else for(t=0;k[t]!=null;t++)k[t]&&k[t].nodeType===1&&e.push(j[t]);else s(k,e);l&&(m(l,h,e,f),m.uniqueSort(e));return e};m.uniqueSort=function(a){if(u){h=i,a.sort(u);if(h)for(var b=1;b<a.length;b++)a[b]===a[b-1]&&a.splice(b--,1)}return a},m.matches=function(a,b){return m(a,null,null,b)},m.matchesSelector=function(a,b){return m(b,null,null,[a]).length>0},m.find=function(a,b,c){var d,e,f,g,h,i;if(!a)return[];for(e=0,f=o.order.length;e<f;e++){h=o.order[e];if(g=o.leftMatch[h].exec(a)){i=g[1],g.splice(1,1);if(i.substr(i.length-1)!=="\\"){g[1]=(g[1]||"").replace(j,""),d=o.find[h](g,b,c);if(d!=null){a=a.replace(o.match[h],"");break}}}}d||(d=typeof b.getElementsByTagName!="undefined"?b.getElementsByTagName("*"):[]);return{set:d,expr:a}},m.filter=function(a,c,d,e){var f,g,h,i,j,k,l,n,p,q=a,r=[],s=c,t=c&&c[0]&&m.isXML(c[0]);while(a&&c.length){for(h in o.filter)if((f=o.leftMatch[h].exec(a))!=null&&f[2]){k=o.filter[h],l=f[1],g=!1,f.splice(1,1);if(l.substr(l.length-1)==="\\")continue;s===r&&(r=[]);if(o.preFilter[h]){f=o.preFilter[h](f,s,d,r,e,t);if(!f)g=i=!0;else if(f===!0)continue}if(f)for(n=0;(j=s[n])!=null;n++)j&&(i=k(j,f,n,s),p=e^i,d&&i!=null?p?g=!0:s[n]=!1:p&&(r.push(j),g=!0));if(i!==b){d||(s=r),a=a.replace(o.match[h],"");if(!g)return[];break}}if(a===q)if(g==null)m.error(a);else break;q=a}return s},m.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)};var n=m.getText=function(a){var b,c,d=a.nodeType,e="";if(d){if(d===1||d===9||d===11){if(typeof a.textContent=="string")return a.textContent;if(typeof a.innerText=="string")return a.innerText.replace(k,"");for(a=a.firstChild;a;a=a.nextSibling)e+=n(a)}else if(d===3||d===4)return a.nodeValue}else for(b=0;c=a[b];b++)c.nodeType!==8&&(e+=n(c));return e},o=m.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,CLASS:/\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/},leftMatch:{},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(a){return a.getAttribute("href")},type:function(a){return a.getAttribute("type")}},relative:{"+":function(a,b){var c=typeof b=="string",d=c&&!l.test(b),e=c&&!d;d&&(b=b.toLowerCase());for(var f=0,g=a.length,h;f<g;f++)if(h=a[f]){while((h=h.previousSibling)&&h.nodeType!==1);a[f]=e||h&&h.nodeName.toLowerCase()===b?h||!1:h===b}e&&m.filter(b,a,!0)},">":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!l.test(b)){b=b.toLowerCase();for(;e<f;e++){c=a[e];if(c){var g=c.parentNode;a[e]=g.nodeName.toLowerCase()===b?g:!1}}}else{for(;e<f;e++)c=a[e],c&&(a[e]=d?c.parentNode:c.parentNode===b);d&&m.filter(b,a,!0)}},"":function(a,b,c){var d,f=e++,g=x;typeof b=="string"&&!l.test(b)&&(b=b.toLowerCase(),d=b,g=w),g("parentNode",b,f,a,d,c)},"~":function(a,b,c){var d,f=e++,g=x;typeof b=="string"&&!l.test(b)&&(b=b.toLowerCase(),d=b,g=w),g("previousSibling",b,f,a,d,c)}},find:{ID:function(a,b,c){if(typeof b.getElementById!="undefined"&&!c){var d=b.getElementById(a[1]);return d&&d.parentNode?[d]:[]}},NAME:function(a,b){if(typeof b.getElementsByName!="undefined"){var c=[],d=b.getElementsByName(a[1]);for(var e=0,f=d.length;e<f;e++)d[e].getAttribute("name")===a[1]&&c.push(d[e]);return c.length===0?null:c}},TAG:function(a,b){if(typeof b.getElementsByTagName!="undefined")return b.getElementsByTagName(a[1])}},preFilter:{CLASS:function(a,b,c,d,e,f){a=" "+a[1].replace(j,"")+" ";if(f)return a;for(var g=0,h;(h=b[g])!=null;g++)h&&(e^(h.className&&(" "+h.className+" ").replace(/[\t\n\r]/g," ").indexOf(a)>=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(j,"")},TAG:function(a,b){return a[1].replace(j,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||m.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&m.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(j,"");!f&&o.attrMap[g]&&(a[1]=o.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(j,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=m(b[3],null,null,c);else{var g=m.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(o.match.POS.test(b[0])||o.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!m(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return b<c[3]-0},gt:function(a,b,c){return b>c[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=o.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||n([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h<i;h++)if(g[h]===a)return!1;return!0}m.error(e)},CHILD:function(a,b){var c,e,f,g,h,i,j,k=b[1],l=a;switch(k){case"only":case"first":while(l=l.previousSibling)if(l.nodeType===1)return!1;if(k==="first")return!0;l=a;case"last":while(l=l.nextSibling)if(l.nodeType===1)return!1;return!0;case"nth":c=b[2],e=b[3];if(c===1&&e===0)return!0;f=b[0],g=a.parentNode;if(g&&(g[d]!==f||!a.nodeIndex)){i=0;for(l=g.firstChild;l;l=l.nextSibling)l.nodeType===1&&(l.nodeIndex=++i);g[d]=f}j=a.nodeIndex-e;return c===0?j===0:j%c===0&&j/c>=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||!!a.nodeName&&a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=m.attr?m.attr(a,c):o.attrHandle[c]?o.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":!f&&m.attr?d!=null:f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=o.setFilters[e];if(f)return f(a,c,b,d)}}},p=o.match.POS,q=function(a,b){return"\\"+(b-0+1)};for(var r in o.match)o.match[r]=new RegExp(o.match[r].source+/(?![^\[]*\])(?![^\(]*\))/.source),o.leftMatch[r]=new RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[r].source.replace(/\\(\d+)/g,q));o.match.globalPOS=p;var s=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(t){s=function(a,b){var c=0,d=b||[];if(g.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var e=a.length;c<e;c++)d.push(a[c]);else for(;a[c];c++)d.push(a[c]);return d}}var u,v;c.documentElement.compareDocumentPosition?u=function(a,b){if(a===b){h=!0;return 0}if(!a.compareDocumentPosition||!b.compareDocumentPosition)return a.compareDocumentPosition?-1:1;return a.compareDocumentPosition(b)&4?-1:1}:(u=function(a,b){if(a===b){h=!0;return 0}if(a.sourceIndex&&b.sourceIndex)return a.sourceIndex-b.sourceIndex;var c,d,e=[],f=[],g=a.parentNode,i=b.parentNode,j=g;if(g===i)return v(a,b);if(!g)return-1;if(!i)return 1;while(j)e.unshift(j),j=j.parentNode;j=i;while(j)f.unshift(j),j=j.parentNode;c=e.length,d=f.length;for(var k=0;k<c&&k<d;k++)if(e[k]!==f[k])return v(e[k],f[k]);return k===c?v(a,f[k],-1):v(e[k],b,1)},v=function(a,b,c){if(a===b)return c;var d=a.nextSibling;while(d){if(d===b)return-1;d=d.nextSibling}return 1}),function(){var a=c.createElement("div"),d="script"+(new Date).getTime(),e=c.documentElement;a.innerHTML="<a name='"+d+"'/>",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(o.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},o.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(o.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="<a href='#'></a>",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(o.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=m,b=c.createElement("div"),d="__sizzle__";b.innerHTML="<p class='TEST'></p>";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){m=function(b,e,f,g){e=e||c;if(!g&&!m.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return s(e.getElementsByTagName(b),f);if(h[2]&&o.find.CLASS&&e.getElementsByClassName)return s(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return s([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return s([],f);if(i.id===h[3])return s([i],f)}try{return s(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var k=e,l=e.getAttribute("id"),n=l||d,p=e.parentNode,q=/^\s*[+~]/.test(b);l?n=n.replace(/'/g,"\\$&"):e.setAttribute("id",n),q&&p&&(e=e.parentNode);try{if(!q||p)return s(e.querySelectorAll("[id='"+n+"'] "+b),f)}catch(r){}finally{l||k.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)m[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}m.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!m.isXML(a))try{if(e||!o.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return m(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="<div class='test e'></div><div class='test'></div>";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;o.order.splice(1,0,"CLASS"),o.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?m.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?m.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:m.contains=function(){return!1},m.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var y=function(a,b,c){var d,e=[],f="",g=b.nodeType?[b]:b;while(d=o.match.PSEUDO.exec(a))f+=d[0],a=a.replace(o.match.PSEUDO,"");a=o.relative[a]?a+"*":a;for(var h=0,i=g.length;h<i;h++)m(a,g[h],e,c);return m.filter(f,e)};m.attr=f.attr,m.selectors.attrMap={},f.find=m,f.expr=m.selectors,f.expr[":"]=f.expr.filters,f.unique=m.uniqueSort,f.text=m.getText,f.isXMLDoc=m.isXML,f.contains=m.contains}();var L=/Until$/,M=/^(?:parents|prevUntil|prevAll)/,N=/,/,O=/^.[^:#\[\.,]*$/,P=Array.prototype.slice,Q=f.expr.match.globalPOS,R={children:!0,contents:!0,next:!0,prev:!0};f.fn.extend({find:function(a){var b=this,c,d;if(typeof a!="string")return f(a).filter(function(){for(c=0,d=b.length;c<d;c++)if(f.contains(b[c],this))return!0});var e=this.pushStack("","find",a),g,h,i;for(c=0,d=this.length;c<d;c++){g=e.length,f.find(a,this[c],e);if(c>0)for(h=g;h<e.length;h++)for(i=0;i<g;i++)if(e[i]===e[h]){e.splice(h--,1);break}}return e},has:function(a){var b=f(a);return this.filter(function(){for(var a=0,c=b.length;a<c;a++)if(f.contains(this,b[a]))return!0})},not:function(a){return this.pushStack(T(this,a,!1),"not",a)},filter:function(a){return this.pushStack(T(this,a,!0),"filter",a)},is:function(a){return!!a&&(typeof a=="string"?Q.test(a)?f(a,this.context).index(this[0])>=0:f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h=1;while(g&&g.ownerDocument&&g!==b){for(d=0;d<a.length;d++)f(g).is(a[d])&&c.push({selector:a[d],elem:g,level:h});g=g.parentNode,h++}return c}var i=Q.test(a)||typeof a!="string"?f(a,b||this.context):0;for(d=0,e=this.length;d<e;d++){g=this[d];while(g){if(i?i.index(g)>-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a)return this[0]&&this[0].parentNode?this.prevAll().length:-1;if(typeof a=="string")return f.inArray(this[0],f(a));return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(S(c[0])||S(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c);L.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!R[a]?f.unique(e):e,(this.length>1||N.test(d))&&M.test(a)&&(e=e.reverse());return this.pushStack(e,a,P.call(arguments).join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var V="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",W=/ jQuery\d+="(?:\d+|null)"/g,X=/^\s+/,Y=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,Z=/<([\w:]+)/,$=/<tbody/i,_=/<|&#?\w+;/,ba=/<(?:script|style)/i,bb=/<(?:script|object|embed|option|style)/i,bc=new RegExp("<(?:"+V+")[\\s/>]","i"),bd=/checked\s*(?:[^=]|=\s*.checked.)/i,be=/\/(java|ecma)script/i,bf=/^\s*<!(?:\[CDATA\[|\-\-)/,bg={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]},bh=U(c);bg.optgroup=bg.option,bg.tbody=bg.tfoot=bg.colgroup=bg.caption=bg.thead,bg.th=bg.td,f.support.htmlSerialize||(bg._default=[1,"div<div>","</div>"]),f.fn.extend({text:function(a){return f.access(this,function(a){return a===b?f.text(this):this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a))},null,a,arguments.length)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=f.isFunction(a);return this.each(function(c){f(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f
-.clean(arguments);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f.clean(arguments));return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){return f.access(this,function(a){var c=this[0]||{},d=0,e=this.length;if(a===b)return c.nodeType===1?c.innerHTML.replace(W,""):null;if(typeof a=="string"&&!ba.test(a)&&(f.support.leadingWhitespace||!X.test(a))&&!bg[(Z.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Y,"<$1></$2>");try{for(;d<e;d++)c=this[d]||{},c.nodeType===1&&(f.cleanData(c.getElementsByTagName("*")),c.innerHTML=a);c=0}catch(g){}}c&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(a){if(this[0]&&this[0].parentNode){if(f.isFunction(a))return this.each(function(b){var c=f(this),d=c.html();c.replaceWith(a.call(this,b,d))});typeof a!="string"&&(a=f(a).detach());return this.each(function(){var b=this.nextSibling,c=this.parentNode;f(this).remove(),b?f(b).before(a):f(c).append(a)})}return this.length?this.pushStack(f(f.isFunction(a)?a():a),"replaceWith",a):this},detach:function(a){return this.remove(a,!0)},domManip:function(a,c,d){var e,g,h,i,j=a[0],k=[];if(!f.support.checkClone&&arguments.length===3&&typeof j=="string"&&bd.test(j))return this.each(function(){f(this).domManip(a,c,d,!0)});if(f.isFunction(j))return this.each(function(e){var g=f(this);a[0]=j.call(this,e,c?g.html():b),g.domManip(a,c,d)});if(this[0]){i=j&&j.parentNode,f.support.parentNode&&i&&i.nodeType===11&&i.childNodes.length===this.length?e={fragment:i}:e=f.buildFragment(a,this,k),h=e.fragment,h.childNodes.length===1?g=h=h.firstChild:g=h.firstChild;if(g){c=c&&f.nodeName(g,"tr");for(var l=0,m=this.length,n=m-1;l<m;l++)d.call(c?bi(this[l],g):this[l],e.cacheable||m>1&&l<n?f.clone(h,!0,!0):h)}k.length&&f.each(k,function(a,b){b.src?f.ajax({type:"GET",global:!1,url:b.src,async:!1,dataType:"script"}):f.globalEval((b.text||b.textContent||b.innerHTML||"").replace(bf,"/*$0*/")),b.parentNode&&b.parentNode.removeChild(b)})}return this}}),f.buildFragment=function(a,b,d){var e,g,h,i,j=a[0];b&&b[0]&&(i=b[0].ownerDocument||b[0]),i.createDocumentFragment||(i=c),a.length===1&&typeof j=="string"&&j.length<512&&i===c&&j.charAt(0)==="<"&&!bb.test(j)&&(f.support.checkClone||!bd.test(j))&&(f.support.html5Clone||!bc.test(j))&&(g=!0,h=f.fragments[j],h&&h!==1&&(e=h)),e||(e=i.createDocumentFragment(),f.clean(a,i,e,d)),g&&(f.fragments[j]=h?e:1);return{fragment:e,cacheable:g}},f.fragments={},f.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){f.fn[a]=function(c){var d=[],e=f(c),g=this.length===1&&this[0].parentNode;if(g&&g.nodeType===11&&g.childNodes.length===1&&e.length===1){e[b](this[0]);return this}for(var h=0,i=e.length;h<i;h++){var j=(h>0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d,e,g,h=f.support.html5Clone||f.isXMLDoc(a)||!bc.test("<"+a.nodeName+">")?a.cloneNode(!0):bo(a);if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bk(a,h),d=bl(a),e=bl(h);for(g=0;d[g];++g)e[g]&&bk(d[g],e[g])}if(b){bj(a,h);if(c){d=bl(a),e=bl(h);for(g=0;d[g];++g)bj(d[g],e[g])}}d=e=null;return h},clean:function(a,b,d,e){var g,h,i,j=[];b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);for(var k=0,l;(l=a[k])!=null;k++){typeof l=="number"&&(l+="");if(!l)continue;if(typeof l=="string")if(!_.test(l))l=b.createTextNode(l);else{l=l.replace(Y,"<$1></$2>");var m=(Z.exec(l)||["",""])[1].toLowerCase(),n=bg[m]||bg._default,o=n[0],p=b.createElement("div"),q=bh.childNodes,r;b===c?bh.appendChild(p):U(b).appendChild(p),p.innerHTML=n[1]+l+n[2];while(o--)p=p.lastChild;if(!f.support.tbody){var s=$.test(l),t=m==="table"&&!s?p.firstChild&&p.firstChild.childNodes:n[1]==="<table>"&&!s?p.childNodes:[];for(i=t.length-1;i>=0;--i)f.nodeName(t[i],"tbody")&&!t[i].childNodes.length&&t[i].parentNode.removeChild(t[i])}!f.support.leadingWhitespace&&X.test(l)&&p.insertBefore(b.createTextNode(X.exec(l)[0]),p.firstChild),l=p.childNodes,p&&(p.parentNode.removeChild(p),q.length>0&&(r=q[q.length-1],r&&r.parentNode&&r.parentNode.removeChild(r)))}var u;if(!f.support.appendChecked)if(l[0]&&typeof (u=l.length)=="number")for(i=0;i<u;i++)bn(l[i]);else bn(l);l.nodeType?j.push(l):j=f.merge(j,l)}if(d){g=function(a){return!a.type||be.test(a.type)};for(k=0;j[k];k++){h=j[k];if(e&&f.nodeName(h,"script")&&(!h.type||be.test(h.type)))e.push(h.parentNode?h.parentNode.removeChild(h):h);else{if(h.nodeType===1){var v=f.grep(h.getElementsByTagName("script"),g);j.splice.apply(j,[k+1,0].concat(v))}d.appendChild(h)}}}return j},cleanData:function(a){var b,c,d=f.cache,e=f.event.special,g=f.support.deleteExpando;for(var h=0,i;(i=a[h])!=null;h++){if(i.nodeName&&f.noData[i.nodeName.toLowerCase()])continue;c=i[f.expando];if(c){b=d[c];if(b&&b.events){for(var j in b.events)e[j]?f.event.remove(i,j):f.removeEvent(i,j,b.handle);b.handle&&(b.handle.elem=null)}g?delete i[f.expando]:i.removeAttribute&&i.removeAttribute(f.expando),delete d[c]}}}});var bp=/alpha\([^)]*\)/i,bq=/opacity=([^)]*)/,br=/([A-Z]|^ms)/g,bs=/^[\-+]?(?:\d*\.)?\d+$/i,bt=/^-?(?:\d*\.)?\d+(?!px)[^\d\s]+$/i,bu=/^([\-+])=([\-+.\de]+)/,bv=/^margin/,bw={position:"absolute",visibility:"hidden",display:"block"},bx=["Top","Right","Bottom","Left"],by,bz,bA;f.fn.css=function(a,c){return f.access(this,function(a,c,d){return d!==b?f.style(a,c,d):f.css(a,c)},a,c,arguments.length>1)},f.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=by(a,"opacity");return c===""?"1":c}return a.style.opacity}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":f.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!!a&&a.nodeType!==3&&a.nodeType!==8&&!!a.style){var g,h,i=f.camelCase(c),j=a.style,k=f.cssHooks[i];c=f.cssProps[i]||i;if(d===b){if(k&&"get"in k&&(g=k.get(a,!1,e))!==b)return g;return j[c]}h=typeof d,h==="string"&&(g=bu.exec(d))&&(d=+(g[1]+1)*+g[2]+parseFloat(f.css(a,c)),h="number");if(d==null||h==="number"&&isNaN(d))return;h==="number"&&!f.cssNumber[i]&&(d+="px");if(!k||!("set"in k)||(d=k.set(a,d))!==b)try{j[c]=d}catch(l){}}},css:function(a,c,d){var e,g;c=f.camelCase(c),g=f.cssHooks[c],c=f.cssProps[c]||c,c==="cssFloat"&&(c="float");if(g&&"get"in g&&(e=g.get(a,!0,d))!==b)return e;if(by)return by(a,c)},swap:function(a,b,c){var d={},e,f;for(f in b)d[f]=a.style[f],a.style[f]=b[f];e=c.call(a);for(f in b)a.style[f]=d[f];return e}}),f.curCSS=f.css,c.defaultView&&c.defaultView.getComputedStyle&&(bz=function(a,b){var c,d,e,g,h=a.style;b=b.replace(br,"-$1").toLowerCase(),(d=a.ownerDocument.defaultView)&&(e=d.getComputedStyle(a,null))&&(c=e.getPropertyValue(b),c===""&&!f.contains(a.ownerDocument.documentElement,a)&&(c=f.style(a,b))),!f.support.pixelMargin&&e&&bv.test(b)&&bt.test(c)&&(g=h.width,h.width=c,c=e.width,h.width=g);return c}),c.documentElement.currentStyle&&(bA=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;f==null&&g&&(e=g[b])&&(f=e),bt.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d));return f===""?"auto":f}),by=bz||bA,f.each(["height","width"],function(a,b){f.cssHooks[b]={get:function(a,c,d){if(c)return a.offsetWidth!==0?bB(a,b,d):f.swap(a,bw,function(){return bB(a,b,d)})},set:function(a,b){return bs.test(b)?b+"px":b}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return bq.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bp,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bp.test(g)?g.replace(bp,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){return f.swap(a,{display:"inline-block"},function(){return b?by(a,"margin-right"):a.style.marginRight})}})}),f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)}),f.each({margin:"",padding:"",border:"Width"},function(a,b){f.cssHooks[a+b]={expand:function(c){var d,e=typeof c=="string"?c.split(" "):[c],f={};for(d=0;d<4;d++)f[a+bx[d]+b]=e[d]||e[d-2]||e[0];return f}}});var bC=/%20/g,bD=/\[\]$/,bE=/\r?\n/g,bF=/#.*$/,bG=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bH=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bI=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bJ=/^(?:GET|HEAD)$/,bK=/^\/\//,bL=/\?/,bM=/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,bN=/^(?:select|textarea)/i,bO=/\s+/,bP=/([?&])_=[^&]*/,bQ=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bR=f.fn.load,bS={},bT={},bU,bV,bW=["*/"]+["*"];try{bU=e.href}catch(bX){bU=c.createElement("a"),bU.href="",bU=bU.href}bV=bQ.exec(bU.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bR)return bR.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("<div>").append(c.replace(bM,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bN.test(this.nodeName)||bH.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bE,"\r\n")}}):{name:b.name,value:c.replace(bE,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.on(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?b$(a,f.ajaxSettings):(b=a,a=f.ajaxSettings),b$(a,b);return a},ajaxSettings:{url:bU,isLocal:bI.test(bV[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded; charset=UTF-8",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":bW},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:bY(bS),ajaxTransport:bY(bT),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a>0?4:0;var o,r,u,w=c,x=l?ca(d,v,l):b,y,z;if(a>=200&&a<300||a===304){if(d.ifModified){if(y=v.getResponseHeader("Last-Modified"))f.lastModified[k]=y;if(z=v.getResponseHeader("Etag"))f.etag[k]=z}if(a===304)w="notmodified",o=!0;else try{r=cb(d,x),w="success",o=!0}catch(A){w="parsererror",u=A}}else{u=w;if(!w||a)w="error",a<0&&(a=0)}v.status=a,v.statusText=""+(c||w),o?h.resolveWith(e,[r,w,v]):h.rejectWith(e,[v,w,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.fireWith(e,[v,w]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f.Callbacks("once memory"),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bG.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.add,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bF,"").replace(bK,bV[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bO),d.crossDomain==null&&(r=bQ.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bV[1]&&r[2]==bV[2]&&(r[3]||(r[1]==="http:"?80:443))==(bV[3]||(bV[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),bZ(bS,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bJ.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bL.test(d.url)?"&":"?")+d.data,delete d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bP,"$1_="+x);d.url=y+(y===d.url?(bL.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", "+bW+"; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=bZ(bT,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){if(s<2)w(-1,z);else throw z}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)b_(g,a[g],c,e);return d.join("&").replace(bC,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var cc=f.now(),cd=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+cc++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=typeof b.data=="string"&&/^application\/x\-www\-form\-urlencoded/.test(b.contentType);if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(cd.test(b.url)||e&&cd.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(cd,l),b.url===j&&(e&&(k=k.replace(cd,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var ce=a.ActiveXObject?function(){for(var a in cg)cg[a](0,1)}:!1,cf=0,cg;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ch()||ci()}:ch,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,ce&&delete cg[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n);try{m.text=h.responseText}catch(a){}try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cf,ce&&(cg||(cg={},f(a).unload(ce)),cg[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var cj={},ck,cl,cm=/^(?:toggle|show|hide)$/,cn=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,co,cp=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cq;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(ct("show",3),a,b,c);for(var g=0,h=this.length;g<h;g++)d=this[g],d.style&&(e=d.style.display,!f._data(d,"olddisplay")&&e==="none"&&(e=d.style.display=""),(e===""&&f.css(d,"display")==="none"||!f.contains(d.ownerDocument.documentElement,d))&&f._data(d,"olddisplay",cu(d.nodeName)));for(g=0;g<h;g++){d=this[g];if(d.style){e=d.style.display;if(e===""||e==="none")d.style.display=f._data(d,"olddisplay")||""}}return this},hide:function(a,b,c){if(a||a===0)return this.animate(ct("hide",3),a,b,c);var d,e,g=0,h=this.length;for(;g<h;g++)d=this[g],d.style&&(e=f.css(d,"display"),e!=="none"&&!f._data(d,"olddisplay")&&f._data(d,"olddisplay",e));for(g=0;g<h;g++)this[g].style&&(this[g].style.display="none");return this},_toggle:f.fn.toggle,toggle:function(a,b,c){var d=typeof a=="boolean";f.isFunction(a)&&f.isFunction(b)?this._toggle.apply(this,arguments):a==null||d?this.each(function(){var b=d?a:f(this).is(":hidden");f(this)[b?"show":"hide"]()}):this.animate(ct("toggle",3),a,b,c);return this},fadeTo:function(a,b,c,d){return this.filter(":hidden").css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){function g(){e.queue===!1&&f._mark(this);var b=f.extend({},e),c=this.nodeType===1,d=c&&f(this).is(":hidden"),g,h,i,j,k,l,m,n,o,p,q;b.animatedProperties={};for(i in a){g=f.camelCase(i),i!==g&&(a[g]=a[i],delete a[i]);if((k=f.cssHooks[g])&&"expand"in k){l=k.expand(a[g]),delete a[g];for(i in l)i in a||(a[i]=l[i])}}for(g in a){h=a[g],f.isArray(h)?(b.animatedProperties[g]=h[1],h=a[g]=h[0]):b.animatedProperties[g]=b.specialEasing&&b.specialEasing[g]||b.easing||"swing";if(h==="hide"&&d||h==="show"&&!d)return b.complete.call(this);c&&(g==="height"||g==="width")&&(b.overflow=[this.style.overflow,this.style.overflowX,this.style.overflowY],f.css(this,"display")==="inline"&&f.css(this,"float")==="none"&&(!f.support.inlineBlockNeedsLayout||cu(this.nodeName)==="inline"?this.style.display="inline-block":this.style.zoom=1))}b.overflow!=null&&(this.style.overflow="hidden");for(i in a)j=new f.fx(this,b,i),h=a[i],cm.test(h)?(q=f._data(this,"toggle"+i)||(h==="toggle"?d?"show":"hide":0),q?(f._data(this,"toggle"+i,q==="show"?"hide":"show"),j[q]()):j[h]()):(m=cn.exec(h),n=j.cur(),m?(o=parseFloat(m[2]),p=m[3]||(f.cssNumber[i]?"":"px"),p!=="px"&&(f.style(this,i,(o||1)+p),n=(o||1)/j.cur()*n,f.style(this,i,n+p)),m[1]&&(o=(m[1]==="-="?-1:1)*o+n),j.custom(n,o,p)):j.custom(n,h,""));return!0}var e=f.speed(b,c,d);if(f.isEmptyObject(a))return this.each(e.complete,[!1]);a=f.extend({},a);return e.queue===!1?this.each(g):this.queue(e.queue,g)},stop:function(a,c,d){typeof a!="string"&&(d=c,c=a,a=b),c&&a!==!1&&this.queue(a||"fx",[]);return this.each(function(){function h(a,b,c){var e=b[c];f.removeData(a,c,!0),e.stop(d)}var b,c=!1,e=f.timers,g=f._data(this);d||f._unmark(!0,this);if(a==null)for(b in g)g[b]&&g[b].stop&&b.indexOf(".run")===b.length-4&&h(this,g,b);else g[b=a+".run"]&&g[b].stop&&h(this,g,b);for(b=e.length;b--;)e[b].elem===this&&(a==null||e[b].queue===a)&&(d?e[b](!0):e[b].saveState(),c=!0,e.splice(b,1));(!d||!c)&&f.dequeue(this,a)})}}),f.each({slideDown:ct("show",1),slideUp:ct("hide",1),slideToggle:ct("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){f.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),f.extend({speed:function(a,b,c){var d=a&&typeof a=="object"?f.extend({},a):{complete:c||!c&&b||f.isFunction(a)&&a,duration:a,easing:c&&b||b&&!f.isFunction(b)&&b};d.duration=f.fx.off?0:typeof d.duration=="number"?d.duration:d.duration in f.fx.speeds?f.fx.speeds[d.duration]:f.fx.speeds._default;if(d.queue==null||d.queue===!0)d.queue="fx";d.old=d.complete,d.complete=function(a){f.isFunction(d.old)&&d.old.call(this),d.queue?f.dequeue(this,d.queue):a!==!1&&f._unmark(this)};return d},easing:{linear:function(a){return a},swing:function(a){return-Math.cos(a*Math.PI)/2+.5}},timers:[],fx:function(a,b,c){this.options=b,this.elem=a,this.prop=c,b.orig=b.orig||{}}}),f.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this),(f.fx.step[this.prop]||f.fx.step._default)(this)},cur:function(){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];var a,b=f.css(this.elem,this.prop);return isNaN(a=parseFloat(b))?!b||b==="auto"?0:b:a},custom:function(a,c,d){function h(a){return e.step(a)}var e=this,g=f.fx;this.startTime=cq||cr(),this.end=c,this.now=this.start=a,this.pos=this.state=0,this.unit=d||this.unit||(f.cssNumber[this.prop]?"":"px"),h.queue=this.options.queue,h.elem=this.elem,h.saveState=function(){f._data(e.elem,"fxshow"+e.prop)===b&&(e.options.hide?f._data(e.elem,"fxshow"+e.prop,e.start):e.options.show&&f._data(e.elem,"fxshow"+e.prop,e.end))},h()&&f.timers.push(h)&&!co&&(co=setInterval(g.tick,g.interval))},show:function(){var a=f._data(this.elem,"fxshow"+this.prop);this.options.orig[this.prop]=a||f.style(this.elem,this.prop),this.options.show=!0,a!==b?this.custom(this.cur(),a):this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur()),f(this.elem).show()},hide:function(){this.options.orig[this.prop]=f._data(this.elem,"fxshow"+this.prop)||f.style(this.elem,this.prop),this.options.hide=!0,this.custom(this.cur(),0)},step:function(a){var b,c,d,e=cq||cr(),g=!0,h=this.elem,i=this.options;if(a||e>=i.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),i.animatedProperties[this.prop]=!0;for(b in i.animatedProperties)i.animatedProperties[b]!==!0&&(g=!1);if(g){i.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){h.style["overflow"+b]=i.overflow[a]}),i.hide&&f(h).hide();if(i.hide||i.show)for(b in i.animatedProperties)f.style(h,b,i.orig[b]),f.removeData(h,"fxshow"+b,!0),f.removeData(h,"toggle"+b,!0);d=i.complete,d&&(i.complete=!1,d.call(h))}return!1}i.duration==Infinity?this.now=e:(c=e-this.startTime,this.state=c/i.duration,this.pos=f.easing[i.animatedProperties[this.prop]](this.state,c,0,1,i.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a,b=f.timers,c=0;for(;c<b.length;c++)a=b[c],!a()&&b[c]===a&&b.splice(c--,1);b.length||f.fx.stop()},interval:13,stop:function(){clearInterval(co),co=null},speeds:{slow:600,fast:200,_default:400},step:{opacity:function(a){f.style(a.elem,"opacity",a.now)},_default:function(a){a.elem.style&&a.elem.style[a.prop]!=null?a.elem.style[a.prop]=a.now+a.unit:a.elem[a.prop]=a.now}}}),f.each(cp.concat.apply([],cp),function(a,b){b.indexOf("margin")&&(f.fx.step[b]=function(a){f.style(a.elem,b,Math.max(0,a.now)+a.unit)})}),f.expr&&f.expr.filters&&(f.expr.filters.animated=function(a){return f.grep(f.timers,function(b){return a===b.elem}).length});var cv,cw=/^t(?:able|d|h)$/i,cx=/^(?:body|html)$/i;"getBoundingClientRect"in c.documentElement?cv=function(a,b,c,d){try{d=a.getBoundingClientRect()}catch(e){}if(!d||!f.contains(c,a))return d?{top:d.top,left:d.left}:{top:0,left:0};var g=b.body,h=cy(b),i=c.clientTop||g.clientTop||0,j=c.clientLeft||g.clientLeft||0,k=h.pageYOffset||f.support.boxModel&&c.scrollTop||g.scrollTop,l=h.pageXOffset||f.support.boxModel&&c.scrollLeft||g.scrollLeft,m=d.top+k-i,n=d.left+l-j;return{top:m,left:n}}:cv=function(a,b,c){var d,e=a.offsetParent,g=a,h=b.body,i=b.defaultView,j=i?i.getComputedStyle(a,null):a.currentStyle,k=a.offsetTop,l=a.offsetLeft;while((a=a.parentNode)&&a!==h&&a!==c){if(f.support.fixedPosition&&j.position==="fixed")break;d=i?i.getComputedStyle(a,null):a.currentStyle,k-=a.scrollTop,l-=a.scrollLeft,a===e&&(k+=a.offsetTop,l+=a.offsetLeft,f.support.doesNotAddBorder&&(!f.support.doesAddBorderForTableAndCells||!cw.test(a.nodeName))&&(k+=parseFloat(d.borderTopWidth)||0,l+=parseFloat(d.borderLeftWidth)||0),g=e,e=a.offsetParent),f.support.subtractsBorderForOverflowNotVisible&&d.overflow!=="visible"&&(k+=parseFloat(d.borderTopWidth)||0,l+=parseFloat(d.borderLeftWidth)||0),j=d}if(j.position==="relative"||j.position==="static")k+=h.offsetTop,l+=h.offsetLeft;f.support.fixedPosition&&j.position==="fixed"&&(k+=Math.max(c.scrollTop,h.scrollTop),l+=Math.max(c.scrollLeft,h.scrollLeft));return{top:k,left:l}},f.fn.offset=function(a){if(arguments.length)return a===b?this:this.each(function(b){f.offset.setOffset(this,a,b)});var c=this[0],d=c&&c.ownerDocument;if(!d)return null;if(c===d.body)return f.offset.bodyOffset(c);return cv(c,d,d.documentElement)},f.offset={bodyOffset:function(a){var b=a.offsetTop,c=a.offsetLeft;f.support.doesNotIncludeMarginInBodyOffset&&(b+=parseFloat(f.css(a,"marginTop"))||0,c+=parseFloat(f.css(a,"marginLeft"))||0);return{top:b,left:c}},setOffset:function(a,b,c){var d=f.css(a,"position");d==="static"&&(a.style.position="relative");var e=f(a),g=e.offset(),h=f.css(a,"top"),i=f.css(a,"left"),j=(d==="absolute"||d==="fixed")&&f.inArray("auto",[h,i])>-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cx.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cx.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,c){var d=/Y/.test(c);f.fn[a]=function(e){return f.access(this,function(a,e,g){var h=cy(a);if(g===b)return h?c in h?h[c]:f.support.boxModel&&h.document.documentElement[e]||h.document.body[e]:a[e];h?h.scrollTo(d?f(h).scrollLeft():g,d?g:f(h).scrollTop()):a[e]=g},a,e,arguments.length,null)}}),f.each({Height:"height",Width:"width"},function(a,c){var d="client"+a,e="scroll"+a,g="offset"+a;f.fn["inner"+a]=function(){var a=this[0];return a?a.style?parseFloat(f.css(a,c,"padding")):this[c]():null},f.fn["outer"+a]=function(a){var b=this[0];return b?b.style?parseFloat(f.css(b,c,a?"margin":"border")):this[c]():null},f.fn[c]=function(a){return f.access(this,function(a,c,h){var i,j,k,l;if(f.isWindow(a)){i=a.document,j=i.documentElement[d];return f.support.boxModel&&j||i.body&&i.body[d]||j}if(a.nodeType===9){i=a.documentElement;if(i[d]>=i[e])return i[d];return Math.max(a.body[e],i[e],a.body[g],i[g])}if(h===b){k=f.css(a,c),l=parseFloat(k);return f.isNumeric(l)?l:k}f(a).css(c,h)},c,a,arguments.length,null)}}),a.jQuery=a.$=f,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return f})})(window); \ No newline at end of file
diff --git a/guides/assets/javascripts/responsive-tables.js b/guides/assets/javascripts/responsive-tables.js
index 8554a1343b..1c0f28c993 100755..100644
--- a/guides/assets/javascripts/responsive-tables.js
+++ b/guides/assets/javascripts/responsive-tables.js
@@ -1,43 +1,46 @@
-$(document).ready(function() {
+(function() {
+ "use strict";
+
var switched = false;
- $("table").not(".syntaxhighlighter").addClass("responsive");
+
var updateTables = function() {
- if (($(window).width() < 767) && !switched ){
+ if (document.documentElement.clientWidth < 767 && !switched) {
switched = true;
- $("table.responsive").each(function(i, element) {
- splitTable($(element));
- });
- return true;
- }
- else if (switched && ($(window).width() > 767)) {
+ each(document.querySelectorAll("table.responsive"), splitTable);
+ } else {
switched = false;
- $("table.responsive").each(function(i, element) {
- unsplitTable($(element));
- });
+ each(document.querySelectorAll(".table-wrapper table.responsive"), unsplitTable);
}
- };
-
- $(window).load(updateTables);
- $(window).bind("resize", updateTables);
-
-
- function splitTable(original)
- {
- original.wrap("<div class='table-wrapper' />");
-
- var copy = original.clone();
- copy.find("td:not(:first-child), th:not(:first-child)").css("display", "none");
- copy.removeClass("responsive");
-
- original.closest(".table-wrapper").append(copy);
- copy.wrap("<div class='pinned' />");
- original.wrap("<div class='scrollable' />");
- }
-
- function unsplitTable(original) {
- original.closest(".table-wrapper").find(".pinned").remove();
- original.unwrap();
- original.unwrap();
- }
-
-});
+ }
+
+ document.addEventListener("turbolinks:load", function() {
+ each(document.querySelectorAll(":not(.syntaxhighlighter)>table"), function(element) {
+ element.classList.add("responsive");
+ });
+ updateTables();
+ });
+
+ window.addEventListener("resize", updateTables);
+
+ var splitTable = function(original) {
+ wrap(original, createElement("div", "table-wrapper"));
+
+ var copy = original.cloneNode(true);
+ each(copy.querySelectorAll("td:not(:first-child), th:not(:first-child)"), function(element) {
+ element.style.display = "none";
+ });
+ copy.classList.remove("responsive");
+
+ original.parentNode.append(copy);
+ wrap(copy, createElement("div", "pinned"))
+ wrap(original, createElement("div", "scrollable"));
+ }
+
+ var unsplitTable = function(original) {
+ each(document.querySelectorAll(".table-wrapper .pinned"), function(element) {
+ element.parentNode.removeChild(element);
+ });
+ unwrap(original.parentNode);
+ unwrap(original);
+ }
+}).call(this);
diff --git a/guides/assets/javascripts/syntaxhighlighter.js b/guides/assets/javascripts/syntaxhighlighter.js
new file mode 100644
index 0000000000..584aaed716
--- /dev/null
+++ b/guides/assets/javascripts/syntaxhighlighter.js
@@ -0,0 +1,20 @@
+/*!
+ * SyntaxHighlighter
+ * https://github.com/syntaxhighlighter/syntaxhighlighter
+ *
+ * SyntaxHighlighter is donationware. If you are using it, please donate.
+ * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
+ *
+ * @version
+ * 4.0.1 (Sun, 03 Jul 2016 06:45:54 GMT)
+ *
+ * @copyright
+ * Copyright (C) 2004-2016 Alex Gorbatchev.
+ *
+ * @license
+ * Dual licensed under the MIT and GPL licenses.
+ */
+
+
+!function(e){function t(r){if(n[r])return n[r].exports;var i=n[r]={exports:{},id:r,loaded:!1};return e[r].call(i.exports,i,i.exports,t),i.loaded=!0,i.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){"use strict";function r(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[n]=e[n]);return t["default"]=e,t}function i(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(1);Object.keys(a).forEach(function(e){"default"!==e&&Object.defineProperty(t,e,{enumerable:!0,get:function(){return a[e]}})});var s=n(28),o=i(s),l=i(a),u=n(29),c=r(u);n(30),(0,o["default"])(function(){return l["default"].highlight(c.object(window.syntaxhighlighterConfig||{}))})},function(e,t,n){"use strict";function r(e){window.alert("SyntaxHighlighter\n\n"+e)}function i(e,t){var n=h.vars.discoveredBrushes,i=null;if(null==n){n={};for(var a in h.brushes){var s=h.brushes[a],o=s.aliases;if(null!=o){s.className=s.className||s.aliases[0],s.brushName=s.className||a.toLowerCase();for(var l=0,u=o.length;u>l;l++)n[o[l]]=a}}h.vars.discoveredBrushes=n}return i=h.brushes[n[e]],null==i&&t&&r(h.config.strings.noBrush+e),i}function a(e){var t="<![CDATA[",n="]]>",r=u.trim(e),i=!1,a=t.length,s=n.length;0==r.indexOf(t)&&(r=r.substring(a),i=!0);var o=r.length;return r.indexOf(n)==o-s&&(r=r.substring(0,o-s),i=!0),i?r:e}Object.defineProperty(t,"__esModule",{value:!0});var s=n(2),o=n(5),l=n(9)["default"],u=n(10),c=n(11),f=n(17),g=n(18),p=n(19),d=n(20),h={Match:o.Match,Highlighter:n(22),config:n(18),regexLib:n(3).commonRegExp,vars:{discoveredBrushes:null,highlighters:{}},brushes:{},findElements:function(e,t){var n=t?[t]:u.toArray(document.getElementsByTagName(h.config.tagName)),r=(h.config,[]);if(n=n.concat(f.getSyntaxHighlighterScriptTags()),0===n.length)return r;for(var i=0,a=n.length;a>i;i++){var o={target:n[i],params:s.defaults(s.parse(n[i].className),e)};null!=o.params.brush&&r.push(o)}return r},highlight:function(e,t){var n,r=h.findElements(e,t),u="innerHTML",m=null,x=h.config;if(0!==r.length)for(var v=0,y=r.length;y>v;v++){var m,w,b,t=r[v],E=t.target,S=t.params,C=S.brush;null!=C&&(m=i(C),m&&(S=s.defaults(S||{},p),S=s.defaults(S,g),1==S["html-script"]||1==p["html-script"]?(m=new d(i("xml"),m),C="htmlscript"):m=new m,b=E[u],x.useScriptTags&&(b=a(b)),""!=(E.title||"")&&(S.title=E.title),S.brush=C,b=c(b,S),w=o.applyRegexList(b,m.regexList,S),n=new l(b,w,S),t=f.create("div"),t.innerHTML=n.getHtml(),S.quickCode&&f.attachEvent(f.findElement(t,".code"),"dblclick",f.quickCodeHandler),""!=(E.id||"")&&(t.id=E.id),E.parentNode.replaceChild(t,E)))}}},m=0;t["default"]=h;var x=t.registerBrush=function(e){return h.brushes["brush"+m++]=e["default"]||e};t.clearRegisteredBrushes=function(){h.brushes={},m=0};x(n(23)),x(n(24)),x(n(25)),x(n(26)),x(n(27))},function(e,t,n){"use strict";function r(e){return e.replace(/-(\w+)/g,function(e,t){return t.charAt(0).toUpperCase()+t.substr(1)})}function i(e){var t=s[e];return null==t?e:t}var a=n(3).XRegExp,s={"true":!0,"false":!1};e.exports={defaults:function(e,t){for(var n in t||{})e.hasOwnProperty(n)||(e[n]=e[r(n)]=t[n]);return e},parse:function(e){for(var t,n={},s=a("^\\[(?<values>(.*?))\\]$"),o=0,l=a("(?<name>[\\w-]+)\\s*:\\s*(?<value>[\\w%#-]+|\\[.*?\\]|\".*?\"|'.*?')\\s*;?","g");null!=(t=a.exec(e,l,o));){var u=t.value.replace(/^['"]|['"]$/g,"");if(null!=u&&s.test(u)){var c=a.exec(u,s);u=c.values.length>0?c.values.split(/\s*,\s*/):[]}u=i(u),n[t.name]=n[r(t.name)]=u,o=t.index+t[0].length}return n}}},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(t,"__esModule",{value:!0}),t.commonRegExp=t.XRegExp=void 0;var i=n(4),a=r(i);t.XRegExp=a["default"];t.commonRegExp={multiLineCComments:(0,a["default"])("/\\*.*?\\*/","gs"),singleLineCComments:/\/\/.*$/gm,singleLinePerlComments:/#.*$/gm,doubleQuotedString:/"([^\\"\n]|\\.)*"/g,singleQuotedString:/'([^\\'\n]|\\.)*'/g,multiLineDoubleQuotedString:(0,a["default"])('"([^\\\\"]|\\\\.)*"',"gs"),multiLineSingleQuotedString:(0,a["default"])("'([^\\\\']|\\\\.)*'","gs"),xmlComments:(0,a["default"])("(&lt;|<)!--.*?--(&gt;|>)","gs"),url:/\w+:\/\/[\w-.\/?%&=:@;#]*/g,phpScriptTags:{left:/(&lt;|<)\?(?:=|php)?/g,right:/\?(&gt;|>)/g,eof:!0},aspScriptTags:{left:/(&lt;|<)%=?/g,right:/%(&gt;|>)/g},scriptScriptTags:{left:/(&lt;|<)\s*script.*?(&gt;|>)/gi,right:/(&lt;|<)\/\s*script\s*(&gt;|>)/gi}}},function(e,t){"use strict";function n(e,t,n,r,i){var a;if(e[b]={captureNames:t},i)return e;if(e.__proto__)e.__proto__=w.prototype;else for(a in w.prototype)e[a]=w.prototype[a];return e[b].source=n,e[b].flags=r?r.split("").sort().join(""):r,e}function r(e){return S.replace.call(e,/([\s\S])(?=[\s\S]*\1)/g,"")}function i(e,t){if(!w.isRegExp(e))throw new TypeError("Type RegExp expected");var i=e[b]||{},a=s(e),l="",u="",c=null,f=null;return t=t||{},t.removeG&&(u+="g"),t.removeY&&(u+="y"),u&&(a=S.replace.call(a,new RegExp("["+u+"]+","g"),"")),t.addG&&(l+="g"),t.addY&&(l+="y"),l&&(a=r(a+l)),t.isInternalOnly||(void 0!==i.source&&(c=i.source),null!=i.flags&&(f=l?r(i.flags+l):i.flags)),e=n(new RegExp(e.source,a),o(e)?i.captureNames.slice(0):null,c,f,t.isInternalOnly)}function a(e){return parseInt(e,16)}function s(e){return M?e.flags:S.exec.call(/\/([a-z]*)$/i,RegExp.prototype.toString.call(e))[1]}function o(e){return!(!e[b]||!e[b].captureNames)}function l(e){return parseInt(e,10).toString(16)}function u(e,t){var n,r=e.length;for(n=0;r>n;++n)if(e[n]===t)return n;return-1}function c(e,t){return $.call(e)==="[object "+t+"]"}function f(e,t,n){return S.test.call(n.indexOf("x")>-1?/^(?:\s+|#.*|\(\?#[^)]*\))*(?:[?*+]|{\d+(?:,\d*)?})/:/^(?:\(\?#[^)]*\))*(?:[?*+]|{\d+(?:,\d*)?})/,e.slice(t))}function g(e){for(;e.length<4;)e="0"+e;return e}function p(e,t){var n;if(r(t)!==t)throw new SyntaxError("Invalid duplicate regex flag "+t);for(e=S.replace.call(e,/^\(\?([\w$]+)\)/,function(e,n){if(S.test.call(/[gy]/,n))throw new SyntaxError("Cannot use flag g or y in mode modifier "+e);return t=r(t+n),""}),n=0;n<t.length;++n)if(!H[t.charAt(n)])throw new SyntaxError("Unknown regex flag "+t.charAt(n));return{pattern:e,flags:t}}function d(e){var t={};return c(e,"String")?(w.forEach(e,/[^\s,]+/,function(e){t[e]=!0}),t):e}function h(e){if(!/^[\w$]$/.test(e))throw new Error("Flag must be a single character A-Za-z0-9_$");H[e]=!0}function m(e,t,n,r,i){for(var a,s,o=L.length,l=e.charAt(n),u=null;o--;)if(s=L[o],!(s.leadChar&&s.leadChar!==l||s.scope!==r&&"all"!==s.scope||s.flag&&-1===t.indexOf(s.flag))&&(a=w.exec(e,s.regex,n,"sticky"))){u={matchLength:a[0].length,output:s.handler.call(i,a,r,t),reparse:s.reparse};break}return u}function x(e){E.astral=e}function v(e){RegExp.prototype.exec=(e?C:S).exec,RegExp.prototype.test=(e?C:S).test,String.prototype.match=(e?C:S).match,String.prototype.replace=(e?C:S).replace,String.prototype.split=(e?C:S).split,E.natives=e}function y(e){if(null==e)throw new TypeError("Cannot convert null or undefined to object");return e}function w(e,t){var r,a,s,o,l,u={hasNamedCapture:!1,captureNames:[]},c=R,f="",g=0;if(w.isRegExp(e)){if(void 0!==t)throw new TypeError("Cannot supply flags when copying a RegExp");return i(e)}if(e=void 0===e?"":String(e),t=void 0===t?"":String(t),w.isInstalled("astral")&&-1===t.indexOf("A")&&(t+="A"),k[e]||(k[e]={}),!k[e][t]){for(r=p(e,t),o=r.pattern,l=r.flags;g<o.length;){do r=m(o,l,g,c,u),r&&r.reparse&&(o=o.slice(0,g)+r.output+o.slice(g+r.matchLength));while(r&&r.reparse);r?(f+=r.output,g+=r.matchLength||1):(a=w.exec(o,_[c],g,"sticky")[0],f+=a,g+=a.length,"["===a&&c===R?c=T:"]"===a&&c===T&&(c=R))}k[e][t]={pattern:S.replace.call(f,/\(\?:\)(?:[*+?]|\{\d+(?:,\d*)?})?\??(?=\(\?:\))|^\(\?:\)(?:[*+?]|\{\d+(?:,\d*)?})?\??|\(\?:\)(?:[*+?]|\{\d+(?:,\d*)?})?\??$/g,""),flags:S.replace.call(l,/[^gimuy]+/g,""),captures:u.hasNamedCapture?u.captureNames:null}}return s=k[e][t],n(new RegExp(s.pattern,s.flags),s.captures,e,t)}var b="xregexp",E={astral:!1,natives:!1},S={exec:RegExp.prototype.exec,test:RegExp.prototype.test,match:String.prototype.match,replace:String.prototype.replace,split:String.prototype.split},C={},N={},k={},L=[],R="default",T="class",_={"default":/\\(?:0(?:[0-3][0-7]{0,2}|[4-7][0-7]?)?|[1-9]\d*|x[\dA-Fa-f]{2}|u(?:[\dA-Fa-f]{4}|{[\dA-Fa-f]+})|c[A-Za-z]|[\s\S])|\(\?[:=!]|[?*+]\?|{\d+(?:,\d*)?}\??|[\s\S]/,"class":/\\(?:[0-3][0-7]{0,2}|[4-7][0-7]?|x[\dA-Fa-f]{2}|u(?:[\dA-Fa-f]{4}|{[\dA-Fa-f]+})|c[A-Za-z]|[\s\S])|[\s\S]/},O=/\$(?:{([\w$]+)}|(\d\d?|[\s\S]))/g,j=void 0===S.exec.call(/()??/,"")[1],I=function(){var e=!0;try{new RegExp("","u")}catch(t){e=!1}return e}(),A=function(){var e=!0;try{new RegExp("","y")}catch(t){e=!1}return e}(),M=void 0!==/a/.flags,H={g:!0,i:!0,m:!0,u:I,y:A},$={}.toString;w.prototype=new RegExp,w.version="3.1.0-dev",w.addToken=function(e,t,n){n=n||{};var r,a=n.optionalFlags;if(n.flag&&h(n.flag),a)for(a=S.split.call(a,""),r=0;r<a.length;++r)h(a[r]);L.push({regex:i(e,{addG:!0,addY:A,isInternalOnly:!0}),handler:t,scope:n.scope||R,flag:n.flag,reparse:n.reparse,leadChar:n.leadChar}),w.cache.flush("patterns")},w.cache=function(e,t){return N[e]||(N[e]={}),N[e][t]||(N[e][t]=w(e,t))},w.cache.flush=function(e){"patterns"===e?k={}:N={}},w.escape=function(e){return S.replace.call(y(e),/[-[\]{}()*+?.,\\^$|#\s]/g,"\\$&")},w.exec=function(e,t,n,r){var a,s,o="g",l=!1;return l=A&&!!(r||t.sticky&&r!==!1),l&&(o+="y"),t[b]=t[b]||{},s=t[b][o]||(t[b][o]=i(t,{addG:!0,addY:l,removeY:r===!1,isInternalOnly:!0})),s.lastIndex=n=n||0,a=C.exec.call(s,e),r&&a&&a.index!==n&&(a=null),t.global&&(t.lastIndex=a?s.lastIndex:0),a},w.forEach=function(e,t,n){for(var r,i=0,a=-1;r=w.exec(e,t,i);)n(r,++a,e,t),i=r.index+(r[0].length||1)},w.globalize=function(e){return i(e,{addG:!0})},w.install=function(e){e=d(e),!E.astral&&e.astral&&x(!0),!E.natives&&e.natives&&v(!0)},w.isInstalled=function(e){return!!E[e]},w.isRegExp=function(e){return"[object RegExp]"===$.call(e)},w.match=function(e,t,n){var r,a,s=t.global&&"one"!==n||"all"===n,o=(s?"g":"")+(t.sticky?"y":"")||"noGY";return t[b]=t[b]||{},a=t[b][o]||(t[b][o]=i(t,{addG:!!s,addY:!!t.sticky,removeG:"one"===n,isInternalOnly:!0})),r=S.match.call(y(e),a),t.global&&(t.lastIndex="one"===n&&r?r.index+r[0].length:0),s?r||[]:r&&r[0]},w.matchChain=function(e,t){return function n(e,r){var i,a=t[r].regex?t[r]:{regex:t[r]},s=[],o=function(e){if(a.backref){if(!(e.hasOwnProperty(a.backref)||+a.backref<e.length))throw new ReferenceError("Backreference to undefined group: "+a.backref);s.push(e[a.backref]||"")}else s.push(e[0])};for(i=0;i<e.length;++i)w.forEach(e[i],a.regex,o);return r!==t.length-1&&s.length?n(s,r+1):s}([e],0)},w.replace=function(e,t,n,r){var a,s=w.isRegExp(t),o=t.global&&"one"!==r||"all"===r,l=(o?"g":"")+(t.sticky?"y":"")||"noGY",u=t;return s?(t[b]=t[b]||{},u=t[b][l]||(t[b][l]=i(t,{addG:!!o,addY:!!t.sticky,removeG:"one"===r,isInternalOnly:!0}))):o&&(u=new RegExp(w.escape(String(t)),"g")),a=C.replace.call(y(e),u,n),s&&t.global&&(t.lastIndex=0),a},w.replaceEach=function(e,t){var n,r;for(n=0;n<t.length;++n)r=t[n],e=w.replace(e,r[0],r[1],r[2]);return e},w.split=function(e,t,n){return C.split.call(y(e),t,n)},w.test=function(e,t,n,r){return!!w.exec(e,t,n,r)},w.uninstall=function(e){e=d(e),E.astral&&e.astral&&x(!1),E.natives&&e.natives&&v(!1)},w.union=function(e,t){var n,r,i,a,s=/(\()(?!\?)|\\([1-9]\d*)|\\[\s\S]|\[(?:[^\\\]]|\\[\s\S])*]/g,o=[],l=0,u=function(e,t,i){var a=r[l-n];if(t){if(++l,a)return"(?<"+a+">"}else if(i)return"\\"+(+i+n);return e};if(!c(e,"Array")||!e.length)throw new TypeError("Must provide a nonempty array of patterns to merge");for(a=0;a<e.length;++a)i=e[a],w.isRegExp(i)?(n=l,r=i[b]&&i[b].captureNames||[],o.push(S.replace.call(w(i.source).source,s,u))):o.push(w.escape(i));return w(o.join("|"),t)},C.exec=function(e){var t,n,r,a=this.lastIndex,s=S.exec.apply(this,arguments);if(s){if(!j&&s.length>1&&u(s,"")>-1&&(n=i(this,{removeG:!0,isInternalOnly:!0}),S.replace.call(String(e).slice(s.index),n,function(){var e,t=arguments.length;for(e=1;t-2>e;++e)void 0===arguments[e]&&(s[e]=void 0)})),this[b]&&this[b].captureNames)for(r=1;r<s.length;++r)t=this[b].captureNames[r-1],t&&(s[t]=s[r]);this.global&&!s[0].length&&this.lastIndex>s.index&&(this.lastIndex=s.index)}return this.global||(this.lastIndex=a),s},C.test=function(e){return!!C.exec.call(this,e)},C.match=function(e){var t;if(w.isRegExp(e)){if(e.global)return t=S.match.apply(this,arguments),e.lastIndex=0,t}else e=new RegExp(e);return C.exec.call(e,y(this))},C.replace=function(e,t){var n,r,i,a=w.isRegExp(e);return a?(e[b]&&(r=e[b].captureNames),n=e.lastIndex):e+="",i=c(t,"Function")?S.replace.call(String(this),e,function(){var n,i=arguments;if(r)for(i[0]=new String(i[0]),n=0;n<r.length;++n)r[n]&&(i[0][r[n]]=i[n+1]);return a&&e.global&&(e.lastIndex=i[i.length-2]+i[0].length),t.apply(void 0,i)}):S.replace.call(null==this?this:String(this),e,function(){var e=arguments;return S.replace.call(String(t),O,function(t,n,i){var a;if(n){if(a=+n,a<=e.length-3)return e[a]||"";if(a=r?u(r,n):-1,0>a)throw new SyntaxError("Backreference to undefined group "+t);return e[a+1]||""}if("$"===i)return"$";if("&"===i||0===+i)return e[0];if("`"===i)return e[e.length-1].slice(0,e[e.length-2]);if("'"===i)return e[e.length-1].slice(e[e.length-2]+e[0].length);if(i=+i,!isNaN(i)){if(i>e.length-3)throw new SyntaxError("Backreference to undefined group "+t);return e[i]||""}throw new SyntaxError("Invalid token "+t)})}),a&&(e.global?e.lastIndex=0:e.lastIndex=n),i},C.split=function(e,t){if(!w.isRegExp(e))return S.split.apply(this,arguments);var n,r=String(this),i=[],a=e.lastIndex,s=0;return t=(void 0===t?-1:t)>>>0,w.forEach(r,e,function(e){e.index+e[0].length>s&&(i.push(r.slice(s,e.index)),e.length>1&&e.index<r.length&&Array.prototype.push.apply(i,e.slice(1)),n=e[0].length,s=e.index+n)}),s===r.length?(!S.test.call(e,"")||n)&&i.push(""):i.push(r.slice(s)),e.lastIndex=a,i.length>t?i.slice(0,t):i},w.addToken(/\\([ABCE-RTUVXYZaeg-mopqyz]|c(?![A-Za-z])|u(?![\dA-Fa-f]{4}|{[\dA-Fa-f]+})|x(?![\dA-Fa-f]{2}))/,function(e,t){if("B"===e[1]&&t===R)return e[0];throw new SyntaxError("Invalid escape "+e[0])},{scope:"all",leadChar:"\\"}),w.addToken(/\\u{([\dA-Fa-f]+)}/,function(e,t,n){var r=a(e[1]);if(r>1114111)throw new SyntaxError("Invalid Unicode code point "+e[0]);if(65535>=r)return"\\u"+g(l(r));if(I&&n.indexOf("u")>-1)return e[0];throw new SyntaxError("Cannot use Unicode code point above \\u{FFFF} without flag u")},{scope:"all",leadChar:"\\"}),w.addToken(/\[(\^?)]/,function(e){return e[1]?"[\\s\\S]":"\\b\\B"},{leadChar:"["}),w.addToken(/\(\?#[^)]*\)/,function(e,t,n){return f(e.input,e.index+e[0].length,n)?"":"(?:)"},{leadChar:"("}),w.addToken(/\s+|#.*/,function(e,t,n){return f(e.input,e.index+e[0].length,n)?"":"(?:)"},{flag:"x"}),w.addToken(/\./,function(){return"[\\s\\S]"},{flag:"s",leadChar:"."}),w.addToken(/\\k<([\w$]+)>/,function(e){var t=isNaN(e[1])?u(this.captureNames,e[1])+1:+e[1],n=e.index+e[0].length;if(!t||t>this.captureNames.length)throw new SyntaxError("Backreference to undefined group "+e[0]);return"\\"+t+(n===e.input.length||isNaN(e.input.charAt(n))?"":"(?:)")},{leadChar:"\\"}),w.addToken(/\\(\d+)/,function(e,t){if(!(t===R&&/^[1-9]/.test(e[1])&&+e[1]<=this.captureNames.length)&&"0"!==e[1])throw new SyntaxError("Cannot use octal escape or backreference to undefined group "+e[0]);return e[0]},{scope:"all",leadChar:"\\"}),w.addToken(/\(\?P?<([\w$]+)>/,function(e){if(!isNaN(e[1]))throw new SyntaxError("Cannot use integer as capture name "+e[0]);if("length"===e[1]||"__proto__"===e[1])throw new SyntaxError("Cannot use reserved word as capture name "+e[0]);if(u(this.captureNames,e[1])>-1)throw new SyntaxError("Cannot use same name for multiple groups "+e[0]);return this.captureNames.push(e[1]),this.hasNamedCapture=!0,"("},{leadChar:"("}),w.addToken(/\((?!\?)/,function(e,t,n){return n.indexOf("n")>-1?"(?:":(this.captureNames.push(null),"(")},{optionalFlags:"n",leadChar:"("}),e.exports=w},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(6);Object.keys(r).forEach(function(e){"default"!==e&&Object.defineProperty(t,e,{enumerable:!0,get:function(){return r[e]}})});var i=n(7);Object.keys(i).forEach(function(e){"default"!==e&&Object.defineProperty(t,e,{enumerable:!0,get:function(){return i[e]}})})},function(e,t){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0});var r=function(){function e(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}return function(t,n,r){return n&&e(t.prototype,n),r&&e(t,r),t}}();t.Match=function(){function e(t,r,i){n(this,e),this.value=t,this.index=r,this.length=t.length,this.css=i,this.brushName=null}return r(e,[{key:"toString",value:function(){return this.value}}]),e}()},function(e,t,n){"use strict";function r(e,t){var n=[];t=t||[];for(var r=0,s=t.length;s>r;r++)"object"===i(t[r])&&(n=n.concat((0,a.find)(e,t[r])));return n=(0,a.sort)(n),n=(0,a.removeNested)(n),n=(0,a.compact)(n)}Object.defineProperty(t,"__esModule",{value:!0});var i="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol?"symbol":typeof e};t.applyRegexList=r;var a=n(8)},function(e,t,n){"use strict";function r(e,t){function n(e,t){return e[0]}for(var r=null,i=[],a=t.func?t.func:n,s=0;r=l.XRegExp.exec(e,t.regex,s);){var u=a(r,t);"string"==typeof u&&(u=[new o.Match(u,r.index,t.css)]),i=i.concat(u),s=r.index+r[0].length}return i}function i(e){function t(e,t){return e.index<t.index?-1:e.index>t.index?1:e.length<t.length?-1:e.length>t.length?1:0}return e.sort(t)}function a(e){var t,n,r=[];for(t=0,n=e.length;n>t;t++)e[t]&&r.push(e[t]);return r}function s(e){for(var t=0,n=e.length;n>t;t++)if(null!==e[t])for(var r=e[t],i=r.index+r.length,a=t+1,n=e.length;n>a&&null!==e[t];a++){var s=e[a];if(null!==s){if(s.index>i)break;s.index==r.index&&s.length>r.length?e[t]=null:s.index>=r.index&&s.index<i&&(e[a]=null)}}return e}Object.defineProperty(t,"__esModule",{value:!0}),t.find=r,t.sort=i,t.compact=a,t.removeNested=s;var o=n(6),l=n(3)},function(e,t){"use strict";function n(e,t){for(var n=e.toString();n.length<t;)n="0"+n;return n}function r(e){return e.split(/\r?\n/)}function i(e){var t,n,r,i={};for(t=e.highlight||[],"function"!=typeof t.push&&(t=[t]),r=0,n=t.length;n>r;r++)i[t[r]]=!0;return i}function a(e,t,n){var a=this;a.opts=n,a.code=e,a.matches=t,a.lines=r(e),a.linesToHighlight=i(n)}Object.defineProperty(t,"__esModule",{value:!0}),t["default"]=a,a.prototype={wrapLinesWithCode:function(e,t){if(null==e||0==e.length||"\n"==e||null==t)return e;var n,i,a,s,o,l=this,u=[];for(e=e.replace(/</g,"&lt;"),e=e.replace(/ {2,}/g,function(e){for(a="",s=0,o=e.length;o-1>s;s++)a+=l.opts.space;return a+" "}),n=r(e),s=0,o=n.length;o>s;s++)i=n[s],a="",i.length>0&&(i=i.replace(/^(&nbsp;| )+/,function(e){return a=e,""}),i=0===i.length?a:a+'<code class="'+t+'">'+i+"</code>"),u.push(i);return u.join("\n")},processUrls:function(e){var t=/(.*)((&gt;|&lt;).*)/,n=/\w+:\/\/[\w-.\/?%&=:@;#]*/g;return e.replace(n,function(e){var n="",r=null;return(r=t.exec(e))&&(e=r[1],n=r[2]),'<a href="'+e+'">'+e+"</a>"+n})},figureOutLineNumbers:function(e){var t,n,r=[],i=this.lines,a=parseInt(this.opts.firstLine||1);for(t=0,n=i.length;n>t;t++)r.push(t+a);return r},wrapLine:function(e,t,n){var r=["line","number"+t,"index"+e,"alt"+(t%2==0?1:2).toString()];return this.linesToHighlight[t]&&r.push("highlighted"),0==t&&r.push("break"),'<div class="'+r.join(" ")+'">'+n+"</div>"},renderLineNumbers:function(e,t){var r,i,a=this,s=a.opts,o="",l=a.lines.length,u=parseInt(s.firstLine||1),c=s.padLineNumbers;for(1==c?c=(u+l-1).toString().length:1==isNaN(c)&&(c=0),i=0;l>i;i++)r=t?t[i]:u+i,e=0==r?s.space:n(r,c),o+=a.wrapLine(i,r,e);return o},getCodeLinesHtml:function(e,t){for(var n=this,i=n.opts,a=r(e),s=(i.padLineNumbers,parseInt(i.firstLine||1)),o=i.brush,e="",l=0,u=a.length;u>l;l++){var c=a[l],f=/^(&nbsp;|\s)+/.exec(c),g=null,p=t?t[l]:s+l;null!=f&&(g=f[0].toString(),c=c.substr(g.length),g=g.replace(" ",i.space)),0==c.length&&(c=i.space),e+=n.wrapLine(l,p,(null!=g?'<code class="'+o+' spaces">'+g+"</code>":"")+c)}return e},getTitleHtml:function(e){return e?"<caption>"+e+"</caption>":""},getMatchesHtml:function(e,t){function n(e){var t=e?e.brushName||c:c;return t?t+" ":""}var r,i,a,s,o=this,l=0,u="",c=o.opts.brush||"";for(a=0,s=t.length;s>a;a++)r=t[a],null!==r&&0!==r.length&&(i=n(r),u+=o.wrapLinesWithCode(e.substr(l,r.index-l),i+"plain")+o.wrapLinesWithCode(r.value,i+r.css),l=r.index+r.length+(r.offset||0));return u+=o.wrapLinesWithCode(e.substr(l),n()+"plain")},getHtml:function(){var e,t,n,r=this,i=r.opts,a=r.code,s=r.matches,o=["syntaxhighlighter"];return i.collapse===!0&&o.push("collapsed"),t=i.gutter!==!1,t||o.push("nogutter"),o.push(i.className),o.push(i.brush),t&&(e=r.figureOutLineNumbers(a)),n=r.getMatchesHtml(a,s),n=r.getCodeLinesHtml(n,e),i.autoLinks&&(n=r.processUrls(n)),n='\n <div class="'+o.join(" ")+'">\n <table border="0" cellpadding="0" cellspacing="0">\n '+r.getTitleHtml(i.title)+"\n <tbody>\n <tr>\n "+(t?'<td class="gutter">'+r.renderLineNumbers(a)+"</td>":"")+'\n <td class="code">\n <div class="container">'+n+"</div>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n "}}},function(e,t){"use strict";function n(e){return e.split(/\r?\n/)}function r(e,t){for(var r=n(e),i=0,a=r.length;a>i;i++)r[i]=t(r[i],i);return r.join("\n")}function i(e){return(e||"")+Math.round(1e6*Math.random()).toString()}function a(e,t){var n,r={};for(n in e)r[n]=e[n];for(n in t)r[n]=t[n];return r}function s(e){return e.replace(/^\s+|\s+$/g,"")}function o(e){return Array.prototype.slice.apply(e)}function l(e){var t={"true":!0,"false":!1}[e];return null==t?e:t}e.exports={splitLines:n,eachLine:r,guid:i,merge:a,trim:s,toArray:o,toBoolean:l}},function(e,t,n){"use strict";var r=n(12),i=n(13),a=n(14),s=n(15),o=n(16);e.exports=function(e,t){e=r(e,t),e=i(e,t),e=a(e,t),e=s.unindent(e,t);var n=t["tab-size"];return e=t["smart-tabs"]===!0?o.smart(e,n):o.regular(e,n)}},function(e,t){"use strict";e.exports=function(e,t){return e.replace(/^[ ]*[\n]+|[\n]*[ ]*$/g,"").replace(/\r/g," ")}},function(e,t){"use strict";e.exports=function(e,t){var n=/<br\s*\/?>|&lt;br\s*\/?&gt;/gi;return t.bloggerMode===!0&&(e=e.replace(n,"\n")),e}},function(e,t){"use strict";e.exports=function(e,t){var n=/<br\s*\/?>|&lt;br\s*\/?&gt;/gi;return t.stripBrs===!0&&(e=e.replace(n,"")),e}},function(e,t){"use strict";function n(e){return/^\s*$/.test(e)}e.exports={unindent:function(e){var t,r,i,a,s=e.split(/\r?\n/),o=/^\s*/,l=1e3;for(i=0,a=s.length;a>i&&l>0;i++)if(t=s[i],!n(t)){if(r=o.exec(t),null==r)return e;l=Math.min(r[0].length,l)}if(l>0)for(i=0,a=s.length;a>i;i++)n(s[i])||(s[i]=s[i].substr(l));return s.join("\n")}}},function(e,t){"use strict";function n(e,t,n){return e.substr(0,t)+r.substr(0,n)+e.substr(t+1,e.length)}for(var r="",i=0;50>i;i++)r+=" ";e.exports={smart:function(e,t){var r,i,a,s,o=e.split(/\r?\n/),l=" ";for(a=0,s=o.length;s>a;a++)if(r=o[a],-1!==r.indexOf(l)){for(i=0;-1!==(i=r.indexOf(l));)r=n(r,i,t-i%t);o[a]=r}return o.join("\n")},regular:function(e,t){return e.replace(/\t/g,r.substr(0,t))}}},function(e,t){"use strict";function n(){for(var e=document.getElementsByTagName("script"),t=[],n=0;n<e.length;n++)("text/syntaxhighlighter"==e[n].type||"syntaxhighlighter"==e[n].type)&&t.push(e[n]);return t}function r(e,t){return-1!=e.className.indexOf(t)}function i(e,t){r(e,t)||(e.className+=" "+t)}function a(e,t){e.className=e.className.replace(t,"")}function s(e,t,n,r){function i(e){e=e||window.event,e.target||(e.target=e.srcElement,e.preventDefault=function(){this.returnValue=!1}),n.call(r||window,e)}e.attachEvent?e.attachEvent("on"+t,i):e.addEventListener(t,i,!1)}function o(e,t,n){if(null==e)return null;var r,i,a=1!=n?e.childNodes:[e.parentNode],s={"#":"id",".":"className"}[t.substr(0,1)]||"nodeName";if(r="nodeName"!=s?t.substr(1):t.toUpperCase(),-1!=(e[s]||"").indexOf(r))return e;for(var l=0,u=a.length;a&&u>l&&null==i;l++)i=o(a[l],t,n);return i}function l(e,t){return o(e,t,!0)}function u(e,t,n,r,i){var a=(screen.width-n)/2,s=(screen.height-r)/2;i+=", left="+a+", top="+s+", width="+n+", height="+r,i=i.replace(/^,/,"");var o=window.open(e,t,i);return o.focus(),o}function c(e){return document.getElementsByTagName(e)}function f(e){var t,n,r=c(e.tagName);if(e.useScriptTags)for(t=c("script"),n=0;n<t.length;n++)t[n].type.match(/^(text\/)?syntaxhighlighter$/)&&r.push(t[n]);return r}function g(e){return document.createElement(e)}function p(e){var t=e.target,n=l(t,".syntaxhighlighter"),r=l(t,".container"),u=document.createElement("textarea");if(r&&n&&!o(r,"textarea")){i(n,"source");for(var c=r.childNodes,f=[],g=0,p=c.length;p>g;g++)f.push(c[g].innerText||c[g].textContent);f=f.join("\r"),f=f.replace(/\u00a0/g," "),u.readOnly=!0,u.appendChild(document.createTextNode(f)),r.appendChild(u),u.focus(),u.select(),s(u,"blur",function(e){u.parentNode.removeChild(u),a(n,"source")})}}e.exports={quickCodeHandler:p,create:g,popup:u,hasClass:r,addClass:i,removeClass:a,attachEvent:s,findElement:o,findParentElement:l,getSyntaxHighlighterScriptTags:n,findElementsToHighlight:f}},function(e,t){"use strict";e.exports={space:"&nbsp;",useScriptTags:!0,bloggerMode:!1,stripBrs:!1,tagName:"pre"}},function(e,t){"use strict";e.exports={"class-name":"","first-line":1,"pad-line-numbers":!1,highlight:null,title:null,"smart-tabs":!0,"tab-size":4,gutter:!0,"quick-code":!0,collapse:!1,"auto-links":!0,unindent:!0,"html-script":!1}},function(e,t,n){(function(t){"use strict";function r(e,t){function n(e,t){for(var n=0,r=e.length;r>n;n++)e[n].index+=t}function r(e,r){function s(e){u=u.concat(e)}var o,l=e.code,u=[],c=a.regexList,f=e.index+e.left.length,g=a.htmlScript;o=i(l,c),n(o,f),s(o),null!=g.left&&null!=e.left&&(o=i(e.left,[g.left]),n(o,e.index),s(o)),null!=g.right&&null!=e.right&&(o=i(e.right,[g.right]),n(o,e.index+e[0].lastIndexOf(e.right)),s(o));for(var p=0,d=u.length;d>p;p++)u[p].brushName=t.brushName;return u}var a,s=new e;if(null!=t){if(a=new t,null==a.htmlScript)throw new Error("Brush wasn't configured for html-script option: "+t.brushName);s.regexList.push({regex:a.htmlScript.code,func:r}),this.regexList=s.regexList}}var i=n(5).applyRegexList;e.exports=r}).call(t,n(21))},function(e,t){"use strict";function n(){f&&u&&(f=!1,u.length?c=u.concat(c):g=-1,c.length&&r())}function r(){if(!f){var e=s(n);f=!0;for(var t=c.length;t;){for(u=c,c=[];++g<t;)u&&u[g].run();g=-1,t=c.length}u=null,f=!1,o(e)}}function i(e,t){this.fun=e,this.array=t}function a(){}var s,o,l=e.exports={};!function(){try{s=setTimeout}catch(e){s=function(){throw new Error("setTimeout is not defined")}}try{o=clearTimeout}catch(e){o=function(){throw new Error("clearTimeout is not defined")}}}();var u,c=[],f=!1,g=-1;l.nextTick=function(e){var t=new Array(arguments.length-1);if(arguments.length>1)for(var n=1;n<arguments.length;n++)t[n-1]=arguments[n];c.push(new i(e,t)),1!==c.length||f||s(r,0)},i.prototype.run=function(){this.fun.apply(null,this.array)},l.title="browser",l.browser=!0,l.env={},l.argv=[],l.version="",l.versions={},l.on=a,l.addListener=a,l.once=a,l.off=a,l.removeListener=a,l.removeAllListeners=a,l.emit=a,l.binding=function(e){throw new Error("process.binding is not supported")},l.cwd=function(){return"/"},l.chdir=function(e){throw new Error("process.chdir is not supported")},l.umask=function(){return 0}},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}function i(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}var a=function(){function e(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}return function(t,n,r){return n&&e(t.prototype,n),r&&e(t,r),t}}(),s=n(9),o=r(s),l=n(3),u=n(5);e.exports=function(){function e(){i(this,e)}return a(e,[{key:"getKeywords",value:function(e){var t=e.replace(/^\s+|\s+$/g,"").replace(/\s+/g,"|");return"\\b(?:"+t+")\\b"}},{key:"forHtmlScript",value:function(e){var t={end:e.right.source};e.eof&&(t.end="(?:(?:"+t.end+")|$)"),this.htmlScript={left:{regex:e.left,css:"script"},right:{regex:e.right,css:"script"},code:(0,l.XRegExp)("(?<left>"+e.left.source+")(?<code>.*?)(?<right>"+t.end+")","sgi")}}},{key:"getHtml",value:function(e){var t=arguments.length<=1||void 0===arguments[1]?{}:arguments[1],n=(0,u.applyRegexList)(e,this.regexList),r=new o["default"](e,n,t);return r.getHtml()}}]),e}()},function(e,t,n){"use strict";function r(){var e="break case catch class continue default delete do else enum export extends false for from as function if implements import in instanceof interface let new null package private protected static return super switch this throw true try typeof var while with yield";this.regexList=[{regex:a.multiLineDoubleQuotedString,css:"string"},{regex:a.multiLineSingleQuotedString,css:"string"},{regex:a.singleLineCComments,css:"comments"},{regex:a.multiLineCComments,css:"comments"},{regex:/\s*#.*/gm,css:"preprocessor"},{regex:new RegExp(this.getKeywords(e),"gm"),css:"keyword"}],this.forHtmlScript(a.scriptScriptTags)}var i=n(22),a=n(3).commonRegExp;r.prototype=new i,r.aliases=["js","jscript","javascript","json"],e.exports=r},function(e,t,n){"use strict";function r(){var e="alias and BEGIN begin break case class def define_method defined do each else elsif END end ensure false for if in module new next nil not or raise redo rescue retry return self super then throw true undef unless until when while yield",t="Array Bignum Binding Class Continuation Dir Exception FalseClass File::Stat File Fixnum Fload Hash Integer IO MatchData Method Module NilClass Numeric Object Proc Range Regexp String Struct::TMS Symbol ThreadGroup Thread Time TrueClass";this.regexList=[{regex:a.singleLinePerlComments,css:"comments"},{regex:a.doubleQuotedString,css:"string"},{regex:a.singleQuotedString,css:"string"},{regex:/\b[A-Z0-9_]+\b/g,css:"constants"},{regex:/:[a-z][A-Za-z0-9_]*/g,css:"color2"},{regex:/(\$|@@|@)\w+/g,css:"variable bold"},{regex:new RegExp(this.getKeywords(e),"gm"),css:"keyword"},{regex:new RegExp(this.getKeywords(t),"gm"),css:"color1"}],this.forHtmlScript(a.aspScriptTags)}var i=n(22),a=n(3).commonRegExp;r.prototype=new i,r.aliases=["ruby","rails","ror","rb"],e.exports=r},function(e,t,n){"use strict";function r(){function e(e,t){var n=e[0],r=s.exec(n,s("(&lt;|<)[\\s\\/\\?!]*(?<name>[:\\w-\\.]+)","xg")),i=[];if(null!=e.attributes)for(var a,l=0,u=s("(?<name> [\\w:.-]+)\\s*=\\s*(?<value> \".*?\"|'.*?'|\\w+)","xg");null!=(a=s.exec(n,u,l));)i.push(new o(a.name,e.index+a.index,"color1")),i.push(new o(a.value,e.index+a.index+a[0].indexOf(a.value),"string")),l=a.index+a[0].length;return null!=r&&i.push(new o(r.name,e.index+r[0].indexOf(r.name),"keyword")),i}this.regexList=[{regex:s("(\\&lt;|<)\\!\\[[\\w\\s]*?\\[(.|\\s)*?\\]\\](\\&gt;|>)","gm"),css:"color2"},{regex:a.xmlComments,css:"comments"},{regex:s("(&lt;|<)[\\s\\/\\?!]*(\\w+)(?<attributes>.*?)[\\s\\/\\?]*(&gt;|>)","sg"),func:e}]}var i=n(22),a=n(3).commonRegExp,s=n(3).XRegExp,o=n(5).Match;r.prototype=new i,r.aliases=["xml","xhtml","xslt","html","plist"],e.exports=r},function(e,t,n){"use strict";function r(){var e="abs avg case cast coalesce convert count current_timestamp current_user day isnull left lower month nullif replace right session_user space substring sum system_user upper user year",t="absolute action add after alter as asc at authorization begin bigint binary bit by cascade char character check checkpoint close collate column commit committed connect connection constraint contains continue create cube current current_date current_time cursor database date deallocate dec decimal declare default delete desc distinct double drop dynamic else end end-exec escape except exec execute false fetch first float for force foreign forward free from full function global goto grant group grouping having hour ignore index inner insensitive insert instead int integer intersect into is isolation key last level load local max min minute modify move name national nchar next no numeric of off on only open option order out output partial password precision prepare primary prior privileges procedure public read real references relative repeatable restrict return returns revoke rollback rollup rows rule schema scroll second section select sequence serializable set size smallint static statistics table temp temporary then time timestamp to top transaction translation trigger true truncate uncommitted union unique update values varchar varying view when where with work",n="all and any between cross in join like not null or outer some";
+ this.regexList=[{regex:/--(.*)$/gm,css:"comments"},{regex:/\/\*([^\*][\s\S]*?)?\*\//gm,css:"comments"},{regex:a.multiLineDoubleQuotedString,css:"string"},{regex:a.multiLineSingleQuotedString,css:"string"},{regex:new RegExp(this.getKeywords(e),"gmi"),css:"color2"},{regex:new RegExp(this.getKeywords(n),"gmi"),css:"color1"},{regex:new RegExp(this.getKeywords(t),"gmi"),css:"keyword"}]}var i=n(22),a=n(3).commonRegExp;r.prototype=new i,r.aliases=["sql"],e.exports=r},function(e,t,n){"use strict";function r(){this.regexList=[]}var i=n(22);n(3).commonRegExp;r.prototype=new i,r.aliases=["text","plain"],e.exports=r},function(e,t,n){"use strict";"function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol?"symbol":typeof e};!function(t,n){e.exports=n()}("domready",function(){var e,t=[],n=document,r=n.documentElement.doScroll,i="DOMContentLoaded",a=(r?/^loaded|^c/:/^loaded|^i|^c/).test(n.readyState);return a||n.addEventListener(i,e=function(){for(n.removeEventListener(i,e),a=1;e=t.shift();)e()}),function(e){a?setTimeout(e,0):t.push(e)}})},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=t.string=function(e){return e.replace(/^([A-Z])/g,function(e,t){return t.toLowerCase()}).replace(/([A-Z])/g,function(e,t){return"-"+t.toLowerCase()})};t.object=function(e){var t={};return Object.keys(e).forEach(function(r){return t[n(r)]=e[r]}),t}},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}var i=n(1),a=r(i);window.SyntaxHighlighter=a["default"],"undefined"==typeof window.XRegExp&&(window.XRegExp=n(3).XRegExp)}]);
diff --git a/guides/assets/javascripts/syntaxhighlighter/shBrushAS3.js b/guides/assets/javascripts/syntaxhighlighter/shBrushAS3.js
deleted file mode 100644
index 8aa3ed2732..0000000000
--- a/guides/assets/javascripts/syntaxhighlighter/shBrushAS3.js
+++ /dev/null
@@ -1,59 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-;(function()
-{
- // CommonJS
- typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null;
-
- function Brush()
- {
- // Created by Peter Atoria @ http://iAtoria.com
-
- var inits = 'class interface function package';
-
- var keywords = '-Infinity ...rest Array as AS3 Boolean break case catch const continue Date decodeURI ' +
- 'decodeURIComponent default delete do dynamic each else encodeURI encodeURIComponent escape ' +
- 'extends false final finally flash_proxy for get if implements import in include Infinity ' +
- 'instanceof int internal is isFinite isNaN isXMLName label namespace NaN native new null ' +
- 'Null Number Object object_proxy override parseFloat parseInt private protected public ' +
- 'return set static String super switch this throw true try typeof uint undefined unescape ' +
- 'use void while with'
- ;
-
- this.regexList = [
- { regex: SyntaxHighlighter.regexLib.singleLineCComments, css: 'comments' }, // one line comments
- { regex: SyntaxHighlighter.regexLib.multiLineCComments, css: 'comments' }, // multiline comments
- { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // double quoted strings
- { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // single quoted strings
- { regex: /\b([\d]+(\.[\d]+)?|0x[a-f0-9]+)\b/gi, css: 'value' }, // numbers
- { regex: new RegExp(this.getKeywords(inits), 'gm'), css: 'color3' }, // initializations
- { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' }, // keywords
- { regex: new RegExp('var', 'gm'), css: 'variable' }, // variable
- { regex: new RegExp('trace', 'gm'), css: 'color1' } // trace
- ];
-
- this.forHtmlScript(SyntaxHighlighter.regexLib.scriptScriptTags);
- };
-
- Brush.prototype = new SyntaxHighlighter.Highlighter();
- Brush.aliases = ['actionscript3', 'as3'];
-
- SyntaxHighlighter.brushes.AS3 = Brush;
-
- // CommonJS
- typeof(exports) != 'undefined' ? exports.Brush = Brush : null;
-})();
diff --git a/guides/assets/javascripts/syntaxhighlighter/shBrushAppleScript.js b/guides/assets/javascripts/syntaxhighlighter/shBrushAppleScript.js
deleted file mode 100644
index d40bbd7dd2..0000000000
--- a/guides/assets/javascripts/syntaxhighlighter/shBrushAppleScript.js
+++ /dev/null
@@ -1,75 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-;(function()
-{
- // CommonJS
- typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null;
-
- function Brush()
- {
- // AppleScript brush by David Chambers
- // http://davidchambersdesign.com/
- var keywords = 'after before beginning continue copy each end every from return get global in local named of set some that the then times to where whose with without';
- var ordinals = 'first second third fourth fifth sixth seventh eighth ninth tenth last front back middle';
- var specials = 'activate add alias AppleScript ask attachment boolean class constant delete duplicate empty exists false id integer list make message modal modified new no paragraph pi properties quit real record remove rest result reveal reverse run running save string true word yes';
-
- this.regexList = [
-
- { regex: /(--|#).*$/gm,
- css: 'comments' },
-
- { regex: /\(\*(?:[\s\S]*?\(\*[\s\S]*?\*\))*[\s\S]*?\*\)/gm, // support nested comments
- css: 'comments' },
-
- { regex: /"[\s\S]*?"/gm,
- css: 'string' },
-
- { regex: /(?:,|:|¬|'s\b|\(|\)|\{|\}|«|\b\w*»)/g,
- css: 'color1' },
-
- { regex: /(-)?(\d)+(\.(\d)?)?(E\+(\d)+)?/g, // numbers
- css: 'color1' },
-
- { regex: /(?:&(amp;|gt;|lt;)?|=|� |>|<|≥|>=|≤|<=|\*|\+|-|\/|÷|\^)/g,
- css: 'color2' },
-
- { regex: /\b(?:and|as|div|mod|not|or|return(?!\s&)(ing)?|equals|(is(n't| not)? )?equal( to)?|does(n't| not) equal|(is(n't| not)? )?(greater|less) than( or equal( to)?)?|(comes|does(n't| not) come) (after|before)|is(n't| not)?( in)? (back|front) of|is(n't| not)? behind|is(n't| not)?( (in|contained by))?|does(n't| not) contain|contain(s)?|(start|begin|end)(s)? with|((but|end) )?(consider|ignor)ing|prop(erty)?|(a )?ref(erence)?( to)?|repeat (until|while|with)|((end|exit) )?repeat|((else|end) )?if|else|(end )?(script|tell|try)|(on )?error|(put )?into|(of )?(it|me)|its|my|with (timeout( of)?|transaction)|end (timeout|transaction))\b/g,
- css: 'keyword' },
-
- { regex: /\b\d+(st|nd|rd|th)\b/g, // ordinals
- css: 'keyword' },
-
- { regex: /\b(?:about|above|against|around|at|below|beneath|beside|between|by|(apart|aside) from|(instead|out) of|into|on(to)?|over|since|thr(ough|u)|under)\b/g,
- css: 'color3' },
-
- { regex: /\b(?:adding folder items to|after receiving|choose( ((remote )?application|color|folder|from list|URL))?|clipboard info|set the clipboard to|(the )?clipboard|entire contents|display(ing| (alert|dialog|mode))?|document( (edited|file|nib name))?|file( (name|type))?|(info )?for|giving up after|(name )?extension|quoted form|return(ed)?|second(?! item)(s)?|list (disks|folder)|text item(s| delimiters)?|(Unicode )?text|(disk )?item(s)?|((current|list) )?view|((container|key) )?window|with (data|icon( (caution|note|stop))?|parameter(s)?|prompt|properties|seed|title)|case|diacriticals|hyphens|numeric strings|punctuation|white space|folder creation|application(s( folder)?| (processes|scripts position|support))?|((desktop )?(pictures )?|(documents|downloads|favorites|home|keychain|library|movies|music|public|scripts|sites|system|users|utilities|workflows) )folder|desktop|Folder Action scripts|font(s| panel)?|help|internet plugins|modem scripts|(system )?preferences|printer descriptions|scripting (additions|components)|shared (documents|libraries)|startup (disk|items)|temporary items|trash|on server|in AppleTalk zone|((as|long|short) )?user name|user (ID|locale)|(with )?password|in (bundle( with identifier)?|directory)|(close|open for) access|read|write( permission)?|(g|s)et eof|using( delimiters)?|starting at|default (answer|button|color|country code|entr(y|ies)|identifiers|items|name|location|script editor)|hidden( answer)?|open(ed| (location|untitled))?|error (handling|reporting)|(do( shell)?|load|run|store) script|administrator privileges|altering line endings|get volume settings|(alert|boot|input|mount|output|set) volume|output muted|(fax|random )?number|round(ing)?|up|down|toward zero|to nearest|as taught in school|system (attribute|info)|((AppleScript( Studio)?|system) )?version|(home )?directory|(IPv4|primary Ethernet) address|CPU (type|speed)|physical memory|time (stamp|to GMT)|replacing|ASCII (character|number)|localized string|from table|offset|summarize|beep|delay|say|(empty|multiple) selections allowed|(of|preferred) type|invisibles|showing( package contents)?|editable URL|(File|FTP|News|Media|Web) [Ss]ervers|Telnet hosts|Directory services|Remote applications|waiting until completion|saving( (in|to))?|path (for|to( (((current|frontmost) )?application|resource))?)|POSIX (file|path)|(background|RGB) color|(OK|cancel) button name|cancel button|button(s)?|cubic ((centi)?met(re|er)s|yards|feet|inches)|square ((kilo)?met(re|er)s|miles|yards|feet)|(centi|kilo)?met(re|er)s|miles|yards|feet|inches|lit(re|er)s|gallons|quarts|(kilo)?grams|ounces|pounds|degrees (Celsius|Fahrenheit|Kelvin)|print( (dialog|settings))?|clos(e(able)?|ing)|(de)?miniaturized|miniaturizable|zoom(ed|able)|attribute run|action (method|property|title)|phone|email|((start|end)ing|home) page|((birth|creation|current|custom|modification) )?date|((((phonetic )?(first|last|middle))|computer|host|maiden|related) |nick)?name|aim|icq|jabber|msn|yahoo|address(es)?|save addressbook|should enable action|city|country( code)?|formatte(r|d address)|(palette )?label|state|street|zip|AIM [Hh]andle(s)?|my card|select(ion| all)?|unsaved|(alpha )?value|entr(y|ies)|group|(ICQ|Jabber|MSN) handle|person|people|company|department|icon image|job title|note|organization|suffix|vcard|url|copies|collating|pages (across|down)|request print time|target( printer)?|((GUI Scripting|Script menu) )?enabled|show Computer scripts|(de)?activated|awake from nib|became (key|main)|call method|of (class|object)|center|clicked toolbar item|closed|for document|exposed|(can )?hide|idle|keyboard (down|up)|event( (number|type))?|launch(ed)?|load (image|movie|nib|sound)|owner|log|mouse (down|dragged|entered|exited|moved|up)|move|column|localization|resource|script|register|drag (info|types)|resigned (active|key|main)|resiz(e(d)?|able)|right mouse (down|dragged|up)|scroll wheel|(at )?index|should (close|open( untitled)?|quit( after last window closed)?|zoom)|((proposed|screen) )?bounds|show(n)?|behind|in front of|size (mode|to fit)|update(d| toolbar item)?|was (hidden|miniaturized)|will (become active|close|finish launching|hide|miniaturize|move|open|quit|(resign )?active|((maximum|minimum|proposed) )?size|show|zoom)|bundle|data source|movie|pasteboard|sound|tool(bar| tip)|(color|open|save) panel|coordinate system|frontmost|main( (bundle|menu|window))?|((services|(excluded from )?windows) )?menu|((executable|frameworks|resource|scripts|shared (frameworks|support)) )?path|(selected item )?identifier|data|content(s| view)?|character(s)?|click count|(command|control|option|shift) key down|context|delta (x|y|z)|key( code)?|location|pressure|unmodified characters|types|(first )?responder|playing|(allowed|selectable) identifiers|allows customization|(auto saves )?configuration|visible|image( name)?|menu form representation|tag|user(-| )defaults|associated file name|(auto|needs) display|current field editor|floating|has (resize indicator|shadow)|hides when deactivated|level|minimized (image|title)|opaque|position|release when closed|sheet|title(d)?)\b/g,
- css: 'color3' },
-
- { regex: new RegExp(this.getKeywords(specials), 'gm'), css: 'color3' },
- { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' },
- { regex: new RegExp(this.getKeywords(ordinals), 'gm'), css: 'keyword' }
- ];
- };
-
- Brush.prototype = new SyntaxHighlighter.Highlighter();
- Brush.aliases = ['applescript'];
-
- SyntaxHighlighter.brushes.AppleScript = Brush;
-
- // CommonJS
- typeof(exports) != 'undefined' ? exports.Brush = Brush : null;
-})();
diff --git a/guides/assets/javascripts/syntaxhighlighter/shBrushBash.js b/guides/assets/javascripts/syntaxhighlighter/shBrushBash.js
deleted file mode 100644
index 8c296969ff..0000000000
--- a/guides/assets/javascripts/syntaxhighlighter/shBrushBash.js
+++ /dev/null
@@ -1,59 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-;(function()
-{
- // CommonJS
- typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null;
-
- function Brush()
- {
- var keywords = 'if fi then elif else for do done until while break continue case function return in eq ne ge le';
- var commands = 'alias apropos awk basename bash bc bg builtin bzip2 cal cat cd cfdisk chgrp chmod chown chroot' +
- 'cksum clear cmp comm command cp cron crontab csplit cut date dc dd ddrescue declare df ' +
- 'diff diff3 dig dir dircolors dirname dirs du echo egrep eject enable env ethtool eval ' +
- 'exec exit expand export expr false fdformat fdisk fg fgrep file find fmt fold format ' +
- 'free fsck ftp gawk getopts grep groups gzip hash head history hostname id ifconfig ' +
- 'import install join kill less let ln local locate logname logout look lpc lpr lprint ' +
- 'lprintd lprintq lprm ls lsof make man mkdir mkfifo mkisofs mknod more mount mtools ' +
- 'mv netstat nice nl nohup nslookup open op passwd paste pathchk ping popd pr printcap ' +
- 'printenv printf ps pushd pwd quota quotacheck quotactl ram rcp read readonly renice ' +
- 'remsync rm rmdir rsync screen scp sdiff sed select seq set sftp shift shopt shutdown ' +
- 'sleep sort source split ssh strace su sudo sum symlink sync tail tar tee test time ' +
- 'times touch top traceroute trap tr true tsort tty type ulimit umask umount unalias ' +
- 'uname unexpand uniq units unset unshar useradd usermod users uuencode uudecode v vdir ' +
- 'vi watch wc whereis which who whoami Wget xargs yes'
- ;
-
- this.regexList = [
- { regex: /^#!.*$/gm, css: 'preprocessor bold' },
- { regex: /\/[\w-\/]+/gm, css: 'plain' },
- { regex: SyntaxHighlighter.regexLib.singleLinePerlComments, css: 'comments' }, // one line comments
- { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // double quoted strings
- { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // single quoted strings
- { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' }, // keywords
- { regex: new RegExp(this.getKeywords(commands), 'gm'), css: 'functions' } // commands
- ];
- }
-
- Brush.prototype = new SyntaxHighlighter.Highlighter();
- Brush.aliases = ['bash', 'shell'];
-
- SyntaxHighlighter.brushes.Bash = Brush;
-
- // CommonJS
- typeof(exports) != 'undefined' ? exports.Brush = Brush : null;
-})();
diff --git a/guides/assets/javascripts/syntaxhighlighter/shBrushCSharp.js b/guides/assets/javascripts/syntaxhighlighter/shBrushCSharp.js
deleted file mode 100644
index 079214efe1..0000000000
--- a/guides/assets/javascripts/syntaxhighlighter/shBrushCSharp.js
+++ /dev/null
@@ -1,65 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-;(function()
-{
- // CommonJS
- typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null;
-
- function Brush()
- {
- var keywords = 'abstract as base bool break byte case catch char checked class const ' +
- 'continue decimal default delegate do double else enum event explicit ' +
- 'extern false finally fixed float for foreach get goto if implicit in int ' +
- 'interface internal is lock long namespace new null object operator out ' +
- 'override params private protected public readonly ref return sbyte sealed set ' +
- 'short sizeof stackalloc static string struct switch this throw true try ' +
- 'typeof uint ulong unchecked unsafe ushort using virtual void while';
-
- function fixComments(match, regexInfo)
- {
- var css = (match[0].indexOf("///") == 0)
- ? 'color1'
- : 'comments'
- ;
-
- return [new SyntaxHighlighter.Match(match[0], match.index, css)];
- }
-
- this.regexList = [
- { regex: SyntaxHighlighter.regexLib.singleLineCComments, func : fixComments }, // one line comments
- { regex: SyntaxHighlighter.regexLib.multiLineCComments, css: 'comments' }, // multiline comments
- { regex: /@"(?:[^"]|"")*"/g, css: 'string' }, // @-quoted strings
- { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // strings
- { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // strings
- { regex: /^\s*#.*/gm, css: 'preprocessor' }, // preprocessor tags like #region and #endregion
- { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' }, // c# keyword
- { regex: /\bpartial(?=\s+(?:class|interface|struct)\b)/g, css: 'keyword' }, // contextual keyword: 'partial'
- { regex: /\byield(?=\s+(?:return|break)\b)/g, css: 'keyword' } // contextual keyword: 'yield'
- ];
-
- this.forHtmlScript(SyntaxHighlighter.regexLib.aspScriptTags);
- };
-
- Brush.prototype = new SyntaxHighlighter.Highlighter();
- Brush.aliases = ['c#', 'c-sharp', 'csharp'];
-
- SyntaxHighlighter.brushes.CSharp = Brush;
-
- // CommonJS
- typeof(exports) != 'undefined' ? exports.Brush = Brush : null;
-})();
-
diff --git a/guides/assets/javascripts/syntaxhighlighter/shBrushColdFusion.js b/guides/assets/javascripts/syntaxhighlighter/shBrushColdFusion.js
deleted file mode 100644
index 627dbb9b76..0000000000
--- a/guides/assets/javascripts/syntaxhighlighter/shBrushColdFusion.js
+++ /dev/null
@@ -1,100 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-;(function()
-{
- // CommonJS
- typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null;
-
- function Brush()
- {
- // Contributed by Jen
- // http://www.jensbits.com/2009/05/14/coldfusion-brush-for-syntaxhighlighter-plus
-
- var funcs = 'Abs ACos AddSOAPRequestHeader AddSOAPResponseHeader AjaxLink AjaxOnLoad ArrayAppend ArrayAvg ArrayClear ArrayDeleteAt ' +
- 'ArrayInsertAt ArrayIsDefined ArrayIsEmpty ArrayLen ArrayMax ArrayMin ArraySet ArraySort ArraySum ArraySwap ArrayToList ' +
- 'Asc ASin Atn BinaryDecode BinaryEncode BitAnd BitMaskClear BitMaskRead BitMaskSet BitNot BitOr BitSHLN BitSHRN BitXor ' +
- 'Ceiling CharsetDecode CharsetEncode Chr CJustify Compare CompareNoCase Cos CreateDate CreateDateTime CreateObject ' +
- 'CreateODBCDate CreateODBCDateTime CreateODBCTime CreateTime CreateTimeSpan CreateUUID DateAdd DateCompare DateConvert ' +
- 'DateDiff DateFormat DatePart Day DayOfWeek DayOfWeekAsString DayOfYear DaysInMonth DaysInYear DE DecimalFormat DecrementValue ' +
- 'Decrypt DecryptBinary DeleteClientVariable DeserializeJSON DirectoryExists DollarFormat DotNetToCFType Duplicate Encrypt ' +
- 'EncryptBinary Evaluate Exp ExpandPath FileClose FileCopy FileDelete FileExists FileIsEOF FileMove FileOpen FileRead ' +
- 'FileReadBinary FileReadLine FileSetAccessMode FileSetAttribute FileSetLastModified FileWrite Find FindNoCase FindOneOf ' +
- 'FirstDayOfMonth Fix FormatBaseN GenerateSecretKey GetAuthUser GetBaseTagData GetBaseTagList GetBaseTemplatePath ' +
- 'GetClientVariablesList GetComponentMetaData GetContextRoot GetCurrentTemplatePath GetDirectoryFromPath GetEncoding ' +
- 'GetException GetFileFromPath GetFileInfo GetFunctionList GetGatewayHelper GetHttpRequestData GetHttpTimeString ' +
- 'GetK2ServerDocCount GetK2ServerDocCountLimit GetLocale GetLocaleDisplayName GetLocalHostIP GetMetaData GetMetricData ' +
- 'GetPageContext GetPrinterInfo GetProfileSections GetProfileString GetReadableImageFormats GetSOAPRequest GetSOAPRequestHeader ' +
- 'GetSOAPResponse GetSOAPResponseHeader GetTempDirectory GetTempFile GetTemplatePath GetTickCount GetTimeZoneInfo GetToken ' +
- 'GetUserRoles GetWriteableImageFormats Hash Hour HTMLCodeFormat HTMLEditFormat IIf ImageAddBorder ImageBlur ImageClearRect ' +
- 'ImageCopy ImageCrop ImageDrawArc ImageDrawBeveledRect ImageDrawCubicCurve ImageDrawLine ImageDrawLines ImageDrawOval ' +
- 'ImageDrawPoint ImageDrawQuadraticCurve ImageDrawRect ImageDrawRoundRect ImageDrawText ImageFlip ImageGetBlob ImageGetBufferedImage ' +
- 'ImageGetEXIFTag ImageGetHeight ImageGetIPTCTag ImageGetWidth ImageGrayscale ImageInfo ImageNegative ImageNew ImageOverlay ImagePaste ' +
- 'ImageRead ImageReadBase64 ImageResize ImageRotate ImageRotateDrawingAxis ImageScaleToFit ImageSetAntialiasing ImageSetBackgroundColor ' +
- 'ImageSetDrawingColor ImageSetDrawingStroke ImageSetDrawingTransparency ImageSharpen ImageShear ImageShearDrawingAxis ImageTranslate ' +
- 'ImageTranslateDrawingAxis ImageWrite ImageWriteBase64 ImageXORDrawingMode IncrementValue InputBaseN Insert Int IsArray IsBinary ' +
- 'IsBoolean IsCustomFunction IsDate IsDDX IsDebugMode IsDefined IsImage IsImageFile IsInstanceOf IsJSON IsLeapYear IsLocalHost ' +
- 'IsNumeric IsNumericDate IsObject IsPDFFile IsPDFObject IsQuery IsSimpleValue IsSOAPRequest IsStruct IsUserInAnyRole IsUserInRole ' +
- 'IsUserLoggedIn IsValid IsWDDX IsXML IsXmlAttribute IsXmlDoc IsXmlElem IsXmlNode IsXmlRoot JavaCast JSStringFormat LCase Left Len ' +
- 'ListAppend ListChangeDelims ListContains ListContainsNoCase ListDeleteAt ListFind ListFindNoCase ListFirst ListGetAt ListInsertAt ' +
- 'ListLast ListLen ListPrepend ListQualify ListRest ListSetAt ListSort ListToArray ListValueCount ListValueCountNoCase LJustify Log ' +
- 'Log10 LSCurrencyFormat LSDateFormat LSEuroCurrencyFormat LSIsCurrency LSIsDate LSIsNumeric LSNumberFormat LSParseCurrency LSParseDateTime ' +
- 'LSParseEuroCurrency LSParseNumber LSTimeFormat LTrim Max Mid Min Minute Month MonthAsString Now NumberFormat ParagraphFormat ParseDateTime ' +
- 'Pi PrecisionEvaluate PreserveSingleQuotes Quarter QueryAddColumn QueryAddRow QueryConvertForGrid QueryNew QuerySetCell QuotedValueList Rand ' +
- 'Randomize RandRange REFind REFindNoCase ReleaseComObject REMatch REMatchNoCase RemoveChars RepeatString Replace ReplaceList ReplaceNoCase ' +
- 'REReplace REReplaceNoCase Reverse Right RJustify Round RTrim Second SendGatewayMessage SerializeJSON SetEncoding SetLocale SetProfileString ' +
- 'SetVariable Sgn Sin Sleep SpanExcluding SpanIncluding Sqr StripCR StructAppend StructClear StructCopy StructCount StructDelete StructFind ' +
- 'StructFindKey StructFindValue StructGet StructInsert StructIsEmpty StructKeyArray StructKeyExists StructKeyList StructKeyList StructNew ' +
- 'StructSort StructUpdate Tan TimeFormat ToBase64 ToBinary ToScript ToString Trim UCase URLDecode URLEncodedFormat URLSessionFormat Val ' +
- 'ValueList VerifyClient Week Wrap Wrap WriteOutput XmlChildPos XmlElemNew XmlFormat XmlGetNodeType XmlNew XmlParse XmlSearch XmlTransform ' +
- 'XmlValidate Year YesNoFormat';
-
- var keywords = 'cfabort cfajaximport cfajaxproxy cfapplet cfapplication cfargument cfassociate cfbreak cfcache cfcalendar ' +
- 'cfcase cfcatch cfchart cfchartdata cfchartseries cfcol cfcollection cfcomponent cfcontent cfcookie cfdbinfo ' +
- 'cfdefaultcase cfdirectory cfdiv cfdocument cfdocumentitem cfdocumentsection cfdump cfelse cfelseif cferror ' +
- 'cfexchangecalendar cfexchangeconnection cfexchangecontact cfexchangefilter cfexchangemail cfexchangetask ' +
- 'cfexecute cfexit cffeed cffile cfflush cfform cfformgroup cfformitem cfftp cffunction cfgrid cfgridcolumn ' +
- 'cfgridrow cfgridupdate cfheader cfhtmlhead cfhttp cfhttpparam cfif cfimage cfimport cfinclude cfindex ' +
- 'cfinput cfinsert cfinterface cfinvoke cfinvokeargument cflayout cflayoutarea cfldap cflocation cflock cflog ' +
- 'cflogin cfloginuser cflogout cfloop cfmail cfmailparam cfmailpart cfmenu cfmenuitem cfmodule cfNTauthenticate ' +
- 'cfobject cfobjectcache cfoutput cfparam cfpdf cfpdfform cfpdfformparam cfpdfparam cfpdfsubform cfpod cfpop ' +
- 'cfpresentation cfpresentationslide cfpresenter cfprint cfprocessingdirective cfprocparam cfprocresult ' +
- 'cfproperty cfquery cfqueryparam cfregistry cfreport cfreportparam cfrethrow cfreturn cfsavecontent cfschedule ' +
- 'cfscript cfsearch cfselect cfset cfsetting cfsilent cfslider cfsprydataset cfstoredproc cfswitch cftable ' +
- 'cftextarea cfthread cfthrow cftimer cftooltip cftrace cftransaction cftree cftreeitem cftry cfupdate cfwddx ' +
- 'cfwindow cfxml cfzip cfzipparam';
-
- var operators = 'all and any between cross in join like not null or outer some';
-
- this.regexList = [
- { regex: new RegExp('--(.*)$', 'gm'), css: 'comments' }, // one line and multiline comments
- { regex: SyntaxHighlighter.regexLib.xmlComments, css: 'comments' }, // single quoted strings
- { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // double quoted strings
- { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // single quoted strings
- { regex: new RegExp(this.getKeywords(funcs), 'gmi'), css: 'functions' }, // functions
- { regex: new RegExp(this.getKeywords(operators), 'gmi'), css: 'color1' }, // operators and such
- { regex: new RegExp(this.getKeywords(keywords), 'gmi'), css: 'keyword' } // keyword
- ];
- }
-
- Brush.prototype = new SyntaxHighlighter.Highlighter();
- Brush.aliases = ['coldfusion','cf'];
-
- SyntaxHighlighter.brushes.ColdFusion = Brush;
-
- // CommonJS
- typeof(exports) != 'undefined' ? exports.Brush = Brush : null;
-})();
diff --git a/guides/assets/javascripts/syntaxhighlighter/shBrushCpp.js b/guides/assets/javascripts/syntaxhighlighter/shBrushCpp.js
deleted file mode 100644
index 9f70d3aed6..0000000000
--- a/guides/assets/javascripts/syntaxhighlighter/shBrushCpp.js
+++ /dev/null
@@ -1,97 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-;(function()
-{
- // CommonJS
- typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null;
-
- function Brush()
- {
- // Copyright 2006 Shin, YoungJin
-
- var datatypes = 'ATOM BOOL BOOLEAN BYTE CHAR COLORREF DWORD DWORDLONG DWORD_PTR ' +
- 'DWORD32 DWORD64 FLOAT HACCEL HALF_PTR HANDLE HBITMAP HBRUSH ' +
- 'HCOLORSPACE HCONV HCONVLIST HCURSOR HDC HDDEDATA HDESK HDROP HDWP ' +
- 'HENHMETAFILE HFILE HFONT HGDIOBJ HGLOBAL HHOOK HICON HINSTANCE HKEY ' +
- 'HKL HLOCAL HMENU HMETAFILE HMODULE HMONITOR HPALETTE HPEN HRESULT ' +
- 'HRGN HRSRC HSZ HWINSTA HWND INT INT_PTR INT32 INT64 LANGID LCID LCTYPE ' +
- 'LGRPID LONG LONGLONG LONG_PTR LONG32 LONG64 LPARAM LPBOOL LPBYTE LPCOLORREF ' +
- 'LPCSTR LPCTSTR LPCVOID LPCWSTR LPDWORD LPHANDLE LPINT LPLONG LPSTR LPTSTR ' +
- 'LPVOID LPWORD LPWSTR LRESULT PBOOL PBOOLEAN PBYTE PCHAR PCSTR PCTSTR PCWSTR ' +
- 'PDWORDLONG PDWORD_PTR PDWORD32 PDWORD64 PFLOAT PHALF_PTR PHANDLE PHKEY PINT ' +
- 'PINT_PTR PINT32 PINT64 PLCID PLONG PLONGLONG PLONG_PTR PLONG32 PLONG64 POINTER_32 ' +
- 'POINTER_64 PSHORT PSIZE_T PSSIZE_T PSTR PTBYTE PTCHAR PTSTR PUCHAR PUHALF_PTR ' +
- 'PUINT PUINT_PTR PUINT32 PUINT64 PULONG PULONGLONG PULONG_PTR PULONG32 PULONG64 ' +
- 'PUSHORT PVOID PWCHAR PWORD PWSTR SC_HANDLE SC_LOCK SERVICE_STATUS_HANDLE SHORT ' +
- 'SIZE_T SSIZE_T TBYTE TCHAR UCHAR UHALF_PTR UINT UINT_PTR UINT32 UINT64 ULONG ' +
- 'ULONGLONG ULONG_PTR ULONG32 ULONG64 USHORT USN VOID WCHAR WORD WPARAM WPARAM WPARAM ' +
- 'char bool short int __int32 __int64 __int8 __int16 long float double __wchar_t ' +
- 'clock_t _complex _dev_t _diskfree_t div_t ldiv_t _exception _EXCEPTION_POINTERS ' +
- 'FILE _finddata_t _finddatai64_t _wfinddata_t _wfinddatai64_t __finddata64_t ' +
- '__wfinddata64_t _FPIEEE_RECORD fpos_t _HEAPINFO _HFILE lconv intptr_t ' +
- 'jmp_buf mbstate_t _off_t _onexit_t _PNH ptrdiff_t _purecall_handler ' +
- 'sig_atomic_t size_t _stat __stat64 _stati64 terminate_function ' +
- 'time_t __time64_t _timeb __timeb64 tm uintptr_t _utimbuf ' +
- 'va_list wchar_t wctrans_t wctype_t wint_t signed';
-
- var keywords = 'break case catch class const __finally __exception __try ' +
- 'const_cast continue private public protected __declspec ' +
- 'default delete deprecated dllexport dllimport do dynamic_cast ' +
- 'else enum explicit extern if for friend goto inline ' +
- 'mutable naked namespace new noinline noreturn nothrow ' +
- 'register reinterpret_cast return selectany ' +
- 'sizeof static static_cast struct switch template this ' +
- 'thread throw true false try typedef typeid typename union ' +
- 'using uuid virtual void volatile whcar_t while';
-
- var functions = 'assert isalnum isalpha iscntrl isdigit isgraph islower isprint' +
- 'ispunct isspace isupper isxdigit tolower toupper errno localeconv ' +
- 'setlocale acos asin atan atan2 ceil cos cosh exp fabs floor fmod ' +
- 'frexp ldexp log log10 modf pow sin sinh sqrt tan tanh jmp_buf ' +
- 'longjmp setjmp raise signal sig_atomic_t va_arg va_end va_start ' +
- 'clearerr fclose feof ferror fflush fgetc fgetpos fgets fopen ' +
- 'fprintf fputc fputs fread freopen fscanf fseek fsetpos ftell ' +
- 'fwrite getc getchar gets perror printf putc putchar puts remove ' +
- 'rename rewind scanf setbuf setvbuf sprintf sscanf tmpfile tmpnam ' +
- 'ungetc vfprintf vprintf vsprintf abort abs atexit atof atoi atol ' +
- 'bsearch calloc div exit free getenv labs ldiv malloc mblen mbstowcs ' +
- 'mbtowc qsort rand realloc srand strtod strtol strtoul system ' +
- 'wcstombs wctomb memchr memcmp memcpy memmove memset strcat strchr ' +
- 'strcmp strcoll strcpy strcspn strerror strlen strncat strncmp ' +
- 'strncpy strpbrk strrchr strspn strstr strtok strxfrm asctime ' +
- 'clock ctime difftime gmtime localtime mktime strftime time';
-
- this.regexList = [
- { regex: SyntaxHighlighter.regexLib.singleLineCComments, css: 'comments' }, // one line comments
- { regex: SyntaxHighlighter.regexLib.multiLineCComments, css: 'comments' }, // multiline comments
- { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // strings
- { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // strings
- { regex: /^ *#.*/gm, css: 'preprocessor' },
- { regex: new RegExp(this.getKeywords(datatypes), 'gm'), css: 'color1 bold' },
- { regex: new RegExp(this.getKeywords(functions), 'gm'), css: 'functions bold' },
- { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword bold' }
- ];
- };
-
- Brush.prototype = new SyntaxHighlighter.Highlighter();
- Brush.aliases = ['cpp', 'c'];
-
- SyntaxHighlighter.brushes.Cpp = Brush;
-
- // CommonJS
- typeof(exports) != 'undefined' ? exports.Brush = Brush : null;
-})();
diff --git a/guides/assets/javascripts/syntaxhighlighter/shBrushCss.js b/guides/assets/javascripts/syntaxhighlighter/shBrushCss.js
deleted file mode 100644
index 4297a9a648..0000000000
--- a/guides/assets/javascripts/syntaxhighlighter/shBrushCss.js
+++ /dev/null
@@ -1,91 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-;(function()
-{
- // CommonJS
- typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null;
-
- function Brush()
- {
- function getKeywordsCSS(str)
- {
- return '\\b([a-z_]|)' + str.replace(/ /g, '(?=:)\\b|\\b([a-z_\\*]|\\*|)') + '(?=:)\\b';
- };
-
- function getValuesCSS(str)
- {
- return '\\b' + str.replace(/ /g, '(?!-)(?!:)\\b|\\b()') + '\:\\b';
- };
-
- var keywords = 'ascent azimuth background-attachment background-color background-image background-position ' +
- 'background-repeat background baseline bbox border-collapse border-color border-spacing border-style border-top ' +
- 'border-right border-bottom border-left border-top-color border-right-color border-bottom-color border-left-color ' +
- 'border-top-style border-right-style border-bottom-style border-left-style border-top-width border-right-width ' +
- 'border-bottom-width border-left-width border-width border bottom cap-height caption-side centerline clear clip color ' +
- 'content counter-increment counter-reset cue-after cue-before cue cursor definition-src descent direction display ' +
- 'elevation empty-cells float font-size-adjust font-family font-size font-stretch font-style font-variant font-weight font ' +
- 'height left letter-spacing line-height list-style-image list-style-position list-style-type list-style margin-top ' +
- 'margin-right margin-bottom margin-left margin marker-offset marks mathline max-height max-width min-height min-width orphans ' +
- 'outline-color outline-style outline-width outline overflow padding-top padding-right padding-bottom padding-left padding page ' +
- 'page-break-after page-break-before page-break-inside pause pause-after pause-before pitch pitch-range play-during position ' +
- 'quotes right richness size slope src speak-header speak-numeral speak-punctuation speak speech-rate stemh stemv stress ' +
- 'table-layout text-align top text-decoration text-indent text-shadow text-transform unicode-bidi unicode-range units-per-em ' +
- 'vertical-align visibility voice-family volume white-space widows width widths word-spacing x-height z-index';
-
- var values = 'above absolute all always aqua armenian attr aural auto avoid baseline behind below bidi-override black blink block blue bold bolder '+
- 'both bottom braille capitalize caption center center-left center-right circle close-quote code collapse compact condensed '+
- 'continuous counter counters crop cross crosshair cursive dashed decimal decimal-leading-zero default digits disc dotted double '+
- 'embed embossed e-resize expanded extra-condensed extra-expanded fantasy far-left far-right fast faster fixed format fuchsia '+
- 'gray green groove handheld hebrew help hidden hide high higher icon inline-table inline inset inside invert italic '+
- 'justify landscape large larger left-side left leftwards level lighter lime line-through list-item local loud lower-alpha '+
- 'lowercase lower-greek lower-latin lower-roman lower low ltr marker maroon medium message-box middle mix move narrower '+
- 'navy ne-resize no-close-quote none no-open-quote no-repeat normal nowrap n-resize nw-resize oblique olive once open-quote outset '+
- 'outside overline pointer portrait pre print projection purple red relative repeat repeat-x repeat-y rgb ridge right right-side '+
- 'rightwards rtl run-in screen scroll semi-condensed semi-expanded separate se-resize show silent silver slower slow '+
- 'small small-caps small-caption smaller soft solid speech spell-out square s-resize static status-bar sub super sw-resize '+
- 'table-caption table-cell table-column table-column-group table-footer-group table-header-group table-row table-row-group teal '+
- 'text-bottom text-top thick thin top transparent tty tv ultra-condensed ultra-expanded underline upper-alpha uppercase upper-latin '+
- 'upper-roman url visible wait white wider w-resize x-fast x-high x-large x-loud x-low x-slow x-small x-soft xx-large xx-small yellow';
-
- var fonts = '[mM]onospace [tT]ahoma [vV]erdana [aA]rial [hH]elvetica [sS]ans-serif [sS]erif [cC]ourier mono sans serif';
-
- this.regexList = [
- { regex: SyntaxHighlighter.regexLib.multiLineCComments, css: 'comments' }, // multiline comments
- { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // double quoted strings
- { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // single quoted strings
- { regex: /\#[a-fA-F0-9]{3,6}/g, css: 'value' }, // html colors
- { regex: /(-?\d+)(\.\d+)?(px|em|pt|\:|\%|)/g, css: 'value' }, // sizes
- { regex: /!important/g, css: 'color3' }, // !important
- { regex: new RegExp(getKeywordsCSS(keywords), 'gm'), css: 'keyword' }, // keywords
- { regex: new RegExp(getValuesCSS(values), 'g'), css: 'value' }, // values
- { regex: new RegExp(this.getKeywords(fonts), 'g'), css: 'color1' } // fonts
- ];
-
- this.forHtmlScript({
- left: /(&lt;|<)\s*style.*?(&gt;|>)/gi,
- right: /(&lt;|<)\/\s*style\s*(&gt;|>)/gi
- });
- };
-
- Brush.prototype = new SyntaxHighlighter.Highlighter();
- Brush.aliases = ['css'];
-
- SyntaxHighlighter.brushes.CSS = Brush;
-
- // CommonJS
- typeof(exports) != 'undefined' ? exports.Brush = Brush : null;
-})();
diff --git a/guides/assets/javascripts/syntaxhighlighter/shBrushDelphi.js b/guides/assets/javascripts/syntaxhighlighter/shBrushDelphi.js
deleted file mode 100644
index e1060d4468..0000000000
--- a/guides/assets/javascripts/syntaxhighlighter/shBrushDelphi.js
+++ /dev/null
@@ -1,55 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-;(function()
-{
- // CommonJS
- typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null;
-
- function Brush()
- {
- var keywords = 'abs addr and ansichar ansistring array as asm begin boolean byte cardinal ' +
- 'case char class comp const constructor currency destructor div do double ' +
- 'downto else end except exports extended false file finalization finally ' +
- 'for function goto if implementation in inherited int64 initialization ' +
- 'integer interface is label library longint longword mod nil not object ' +
- 'of on or packed pansichar pansistring pchar pcurrency pdatetime pextended ' +
- 'pint64 pointer private procedure program property pshortstring pstring ' +
- 'pvariant pwidechar pwidestring protected public published raise real real48 ' +
- 'record repeat set shl shortint shortstring shr single smallint string then ' +
- 'threadvar to true try type unit until uses val var varirnt while widechar ' +
- 'widestring with word write writeln xor';
-
- this.regexList = [
- { regex: /\(\*[\s\S]*?\*\)/gm, css: 'comments' }, // multiline comments (* *)
- { regex: /{(?!\$)[\s\S]*?}/gm, css: 'comments' }, // multiline comments { }
- { regex: SyntaxHighlighter.regexLib.singleLineCComments, css: 'comments' }, // one line
- { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // strings
- { regex: /\{\$[a-zA-Z]+ .+\}/g, css: 'color1' }, // compiler Directives and Region tags
- { regex: /\b[\d\.]+\b/g, css: 'value' }, // numbers 12345
- { regex: /\$[a-zA-Z0-9]+\b/g, css: 'value' }, // numbers $F5D3
- { regex: new RegExp(this.getKeywords(keywords), 'gmi'), css: 'keyword' } // keyword
- ];
- };
-
- Brush.prototype = new SyntaxHighlighter.Highlighter();
- Brush.aliases = ['delphi', 'pascal', 'pas'];
-
- SyntaxHighlighter.brushes.Delphi = Brush;
-
- // CommonJS
- typeof(exports) != 'undefined' ? exports.Brush = Brush : null;
-})();
diff --git a/guides/assets/javascripts/syntaxhighlighter/shBrushDiff.js b/guides/assets/javascripts/syntaxhighlighter/shBrushDiff.js
deleted file mode 100644
index e9b14fc580..0000000000
--- a/guides/assets/javascripts/syntaxhighlighter/shBrushDiff.js
+++ /dev/null
@@ -1,41 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-;(function()
-{
- // CommonJS
- typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null;
-
- function Brush()
- {
- this.regexList = [
- { regex: /^\+\+\+.*$/gm, css: 'color2' },
- { regex: /^\-\-\-.*$/gm, css: 'color2' },
- { regex: /^\s.*$/gm, css: 'color1' },
- { regex: /^@@.*@@$/gm, css: 'variable' },
- { regex: /^\+[^\+]{1}.*$/gm, css: 'string' },
- { regex: /^\-[^\-]{1}.*$/gm, css: 'comments' }
- ];
- };
-
- Brush.prototype = new SyntaxHighlighter.Highlighter();
- Brush.aliases = ['diff', 'patch'];
-
- SyntaxHighlighter.brushes.Diff = Brush;
-
- // CommonJS
- typeof(exports) != 'undefined' ? exports.Brush = Brush : null;
-})();
diff --git a/guides/assets/javascripts/syntaxhighlighter/shBrushErlang.js b/guides/assets/javascripts/syntaxhighlighter/shBrushErlang.js
deleted file mode 100644
index 6ba7d9da87..0000000000
--- a/guides/assets/javascripts/syntaxhighlighter/shBrushErlang.js
+++ /dev/null
@@ -1,52 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-;(function()
-{
- // CommonJS
- typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null;
-
- function Brush()
- {
- // Contributed by Jean-Lou Dupont
- // http://jldupont.blogspot.com/2009/06/erlang-syntax-highlighter.html
-
- // According to: http://erlang.org/doc/reference_manual/introduction.html#1.5
- var keywords = 'after and andalso band begin bnot bor bsl bsr bxor '+
- 'case catch cond div end fun if let not of or orelse '+
- 'query receive rem try when xor'+
- // additional
- ' module export import define';
-
- this.regexList = [
- { regex: new RegExp("[A-Z][A-Za-z0-9_]+", 'g'), css: 'constants' },
- { regex: new RegExp("\\%.+", 'gm'), css: 'comments' },
- { regex: new RegExp("\\?[A-Za-z0-9_]+", 'g'), css: 'preprocessor' },
- { regex: new RegExp("[a-z0-9_]+:[a-z0-9_]+", 'g'), css: 'functions' },
- { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' },
- { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' },
- { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' }
- ];
- };
-
- Brush.prototype = new SyntaxHighlighter.Highlighter();
- Brush.aliases = ['erl', 'erlang'];
-
- SyntaxHighlighter.brushes.Erland = Brush;
-
- // CommonJS
- typeof(exports) != 'undefined' ? exports.Brush = Brush : null;
-})();
diff --git a/guides/assets/javascripts/syntaxhighlighter/shBrushGroovy.js b/guides/assets/javascripts/syntaxhighlighter/shBrushGroovy.js
deleted file mode 100644
index 6ec5c18521..0000000000
--- a/guides/assets/javascripts/syntaxhighlighter/shBrushGroovy.js
+++ /dev/null
@@ -1,67 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-;(function()
-{
- // CommonJS
- typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null;
-
- function Brush()
- {
- // Contributed by Andres Almiray
- // http://jroller.com/aalmiray/entry/nice_source_code_syntax_highlighter
-
- var keywords = 'as assert break case catch class continue def default do else extends finally ' +
- 'if in implements import instanceof interface new package property return switch ' +
- 'throw throws try while public protected private static';
- var types = 'void boolean byte char short int long float double';
- var constants = 'null';
- var methods = 'allProperties count get size '+
- 'collect each eachProperty eachPropertyName eachWithIndex find findAll ' +
- 'findIndexOf grep inject max min reverseEach sort ' +
- 'asImmutable asSynchronized flatten intersect join pop reverse subMap toList ' +
- 'padRight padLeft contains eachMatch toCharacter toLong toUrl tokenize ' +
- 'eachFile eachFileRecurse eachB yte eachLine readBytes readLine getText ' +
- 'splitEachLine withReader append encodeBase64 decodeBase64 filterLine ' +
- 'transformChar transformLine withOutputStream withPrintWriter withStream ' +
- 'withStreams withWriter withWriterAppend write writeLine '+
- 'dump inspect invokeMethod print println step times upto use waitForOrKill '+
- 'getText';
-
- this.regexList = [
- { regex: SyntaxHighlighter.regexLib.singleLineCComments, css: 'comments' }, // one line comments
- { regex: SyntaxHighlighter.regexLib.multiLineCComments, css: 'comments' }, // multiline comments
- { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // strings
- { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // strings
- { regex: /""".*"""/g, css: 'string' }, // GStrings
- { regex: new RegExp('\\b([\\d]+(\\.[\\d]+)?|0x[a-f0-9]+)\\b', 'gi'), css: 'value' }, // numbers
- { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' }, // goovy keyword
- { regex: new RegExp(this.getKeywords(types), 'gm'), css: 'color1' }, // goovy/java type
- { regex: new RegExp(this.getKeywords(constants), 'gm'), css: 'constants' }, // constants
- { regex: new RegExp(this.getKeywords(methods), 'gm'), css: 'functions' } // methods
- ];
-
- this.forHtmlScript(SyntaxHighlighter.regexLib.aspScriptTags);
- }
-
- Brush.prototype = new SyntaxHighlighter.Highlighter();
- Brush.aliases = ['groovy'];
-
- SyntaxHighlighter.brushes.Groovy = Brush;
-
- // CommonJS
- typeof(exports) != 'undefined' ? exports.Brush = Brush : null;
-})();
diff --git a/guides/assets/javascripts/syntaxhighlighter/shBrushJScript.js b/guides/assets/javascripts/syntaxhighlighter/shBrushJScript.js
deleted file mode 100644
index ff98daba16..0000000000
--- a/guides/assets/javascripts/syntaxhighlighter/shBrushJScript.js
+++ /dev/null
@@ -1,52 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-;(function()
-{
- // CommonJS
- typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null;
-
- function Brush()
- {
- var keywords = 'break case catch continue ' +
- 'default delete do else false ' +
- 'for function if in instanceof ' +
- 'new null return super switch ' +
- 'this throw true try typeof var while with'
- ;
-
- var r = SyntaxHighlighter.regexLib;
-
- this.regexList = [
- { regex: r.multiLineDoubleQuotedString, css: 'string' }, // double quoted strings
- { regex: r.multiLineSingleQuotedString, css: 'string' }, // single quoted strings
- { regex: r.singleLineCComments, css: 'comments' }, // one line comments
- { regex: r.multiLineCComments, css: 'comments' }, // multiline comments
- { regex: /\s*#.*/gm, css: 'preprocessor' }, // preprocessor tags like #region and #endregion
- { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' } // keywords
- ];
-
- this.forHtmlScript(r.scriptScriptTags);
- };
-
- Brush.prototype = new SyntaxHighlighter.Highlighter();
- Brush.aliases = ['js', 'jscript', 'javascript'];
-
- SyntaxHighlighter.brushes.JScript = Brush;
-
- // CommonJS
- typeof(exports) != 'undefined' ? exports.Brush = Brush : null;
-})();
diff --git a/guides/assets/javascripts/syntaxhighlighter/shBrushJava.js b/guides/assets/javascripts/syntaxhighlighter/shBrushJava.js
deleted file mode 100644
index d692fd6382..0000000000
--- a/guides/assets/javascripts/syntaxhighlighter/shBrushJava.js
+++ /dev/null
@@ -1,57 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-;(function()
-{
- // CommonJS
- typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null;
-
- function Brush()
- {
- var keywords = 'abstract assert boolean break byte case catch char class const ' +
- 'continue default do double else enum extends ' +
- 'false final finally float for goto if implements import ' +
- 'instanceof int interface long native new null ' +
- 'package private protected public return ' +
- 'short static strictfp super switch synchronized this throw throws true ' +
- 'transient try void volatile while';
-
- this.regexList = [
- { regex: SyntaxHighlighter.regexLib.singleLineCComments, css: 'comments' }, // one line comments
- { regex: /\/\*([^\*][\s\S]*)?\*\//gm, css: 'comments' }, // multiline comments
- { regex: /\/\*(?!\*\/)\*[\s\S]*?\*\//gm, css: 'preprocessor' }, // documentation comments
- { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // strings
- { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // strings
- { regex: /\b([\d]+(\.[\d]+)?|0x[a-f0-9]+)\b/gi, css: 'value' }, // numbers
- { regex: /(?!\@interface\b)\@[\$\w]+\b/g, css: 'color1' }, // annotation @anno
- { regex: /\@interface\b/g, css: 'color2' }, // @interface keyword
- { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' } // java keyword
- ];
-
- this.forHtmlScript({
- left : /(&lt;|<)%[@!=]?/g,
- right : /%(&gt;|>)/g
- });
- };
-
- Brush.prototype = new SyntaxHighlighter.Highlighter();
- Brush.aliases = ['java'];
-
- SyntaxHighlighter.brushes.Java = Brush;
-
- // CommonJS
- typeof(exports) != 'undefined' ? exports.Brush = Brush : null;
-})();
diff --git a/guides/assets/javascripts/syntaxhighlighter/shBrushJavaFX.js b/guides/assets/javascripts/syntaxhighlighter/shBrushJavaFX.js
deleted file mode 100644
index 1a150a6ad3..0000000000
--- a/guides/assets/javascripts/syntaxhighlighter/shBrushJavaFX.js
+++ /dev/null
@@ -1,58 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-;(function()
-{
- // CommonJS
- typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null;
-
- function Brush()
- {
- // Contributed by Patrick Webster
- // http://patrickwebster.blogspot.com/2009/04/javafx-brush-for-syntaxhighlighter.html
- var datatypes = 'Boolean Byte Character Double Duration '
- + 'Float Integer Long Number Short String Void'
- ;
-
- var keywords = 'abstract after and as assert at before bind bound break catch class '
- + 'continue def delete else exclusive extends false finally first for from '
- + 'function if import in indexof init insert instanceof into inverse last '
- + 'lazy mixin mod nativearray new not null on or override package postinit '
- + 'protected public public-init public-read replace return reverse sizeof '
- + 'step super then this throw true try tween typeof var where while with '
- + 'attribute let private readonly static trigger'
- ;
-
- this.regexList = [
- { regex: SyntaxHighlighter.regexLib.singleLineCComments, css: 'comments' },
- { regex: SyntaxHighlighter.regexLib.multiLineCComments, css: 'comments' },
- { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' },
- { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' },
- { regex: /(-?\.?)(\b(\d*\.?\d+|\d+\.?\d*)(e[+-]?\d+)?|0x[a-f\d]+)\b\.?/gi, css: 'color2' }, // numbers
- { regex: new RegExp(this.getKeywords(datatypes), 'gm'), css: 'variable' }, // datatypes
- { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' }
- ];
- this.forHtmlScript(SyntaxHighlighter.regexLib.aspScriptTags);
- };
-
- Brush.prototype = new SyntaxHighlighter.Highlighter();
- Brush.aliases = ['jfx', 'javafx'];
-
- SyntaxHighlighter.brushes.JavaFX = Brush;
-
- // CommonJS
- typeof(exports) != 'undefined' ? exports.Brush = Brush : null;
-})();
diff --git a/guides/assets/javascripts/syntaxhighlighter/shBrushPerl.js b/guides/assets/javascripts/syntaxhighlighter/shBrushPerl.js
deleted file mode 100644
index d94a2e0ec5..0000000000
--- a/guides/assets/javascripts/syntaxhighlighter/shBrushPerl.js
+++ /dev/null
@@ -1,72 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-;(function()
-{
- // CommonJS
- typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null;
-
- function Brush()
- {
- // Contributed by David Simmons-Duffin and Marty Kube
-
- var funcs =
- 'abs accept alarm atan2 bind binmode chdir chmod chomp chop chown chr ' +
- 'chroot close closedir connect cos crypt defined delete each endgrent ' +
- 'endhostent endnetent endprotoent endpwent endservent eof exec exists ' +
- 'exp fcntl fileno flock fork format formline getc getgrent getgrgid ' +
- 'getgrnam gethostbyaddr gethostbyname gethostent getlogin getnetbyaddr ' +
- 'getnetbyname getnetent getpeername getpgrp getppid getpriority ' +
- 'getprotobyname getprotobynumber getprotoent getpwent getpwnam getpwuid ' +
- 'getservbyname getservbyport getservent getsockname getsockopt glob ' +
- 'gmtime grep hex index int ioctl join keys kill lc lcfirst length link ' +
- 'listen localtime lock log lstat map mkdir msgctl msgget msgrcv msgsnd ' +
- 'oct open opendir ord pack pipe pop pos print printf prototype push ' +
- 'quotemeta rand read readdir readline readlink readpipe recv rename ' +
- 'reset reverse rewinddir rindex rmdir scalar seek seekdir select semctl ' +
- 'semget semop send setgrent sethostent setnetent setpgrp setpriority ' +
- 'setprotoent setpwent setservent setsockopt shift shmctl shmget shmread ' +
- 'shmwrite shutdown sin sleep socket socketpair sort splice split sprintf ' +
- 'sqrt srand stat study substr symlink syscall sysopen sysread sysseek ' +
- 'system syswrite tell telldir time times tr truncate uc ucfirst umask ' +
- 'undef unlink unpack unshift utime values vec wait waitpid warn write';
-
- var keywords =
- 'bless caller continue dbmclose dbmopen die do dump else elsif eval exit ' +
- 'for foreach goto if import last local my next no our package redo ref ' +
- 'require return sub tie tied unless untie until use wantarray while';
-
- this.regexList = [
- { regex: new RegExp('#[^!].*$', 'gm'), css: 'comments' },
- { regex: new RegExp('^\\s*#!.*$', 'gm'), css: 'preprocessor' }, // shebang
- { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' },
- { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' },
- { regex: new RegExp('(\\$|@|%)\\w+', 'g'), css: 'variable' },
- { regex: new RegExp(this.getKeywords(funcs), 'gmi'), css: 'functions' },
- { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' }
- ];
-
- this.forHtmlScript(SyntaxHighlighter.regexLib.phpScriptTags);
- }
-
- Brush.prototype = new SyntaxHighlighter.Highlighter();
- Brush.aliases = ['perl', 'Perl', 'pl'];
-
- SyntaxHighlighter.brushes.Perl = Brush;
-
- // CommonJS
- typeof(exports) != 'undefined' ? exports.Brush = Brush : null;
-})();
diff --git a/guides/assets/javascripts/syntaxhighlighter/shBrushPhp.js b/guides/assets/javascripts/syntaxhighlighter/shBrushPhp.js
deleted file mode 100644
index 95e6e4325b..0000000000
--- a/guides/assets/javascripts/syntaxhighlighter/shBrushPhp.js
+++ /dev/null
@@ -1,88 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-;(function()
-{
- // CommonJS
- typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null;
-
- function Brush()
- {
- var funcs = 'abs acos acosh addcslashes addslashes ' +
- 'array_change_key_case array_chunk array_combine array_count_values array_diff '+
- 'array_diff_assoc array_diff_key array_diff_uassoc array_diff_ukey array_fill '+
- 'array_filter array_flip array_intersect array_intersect_assoc array_intersect_key '+
- 'array_intersect_uassoc array_intersect_ukey array_key_exists array_keys array_map '+
- 'array_merge array_merge_recursive array_multisort array_pad array_pop array_product '+
- 'array_push array_rand array_reduce array_reverse array_search array_shift '+
- 'array_slice array_splice array_sum array_udiff array_udiff_assoc '+
- 'array_udiff_uassoc array_uintersect array_uintersect_assoc '+
- 'array_uintersect_uassoc array_unique array_unshift array_values array_walk '+
- 'array_walk_recursive atan atan2 atanh base64_decode base64_encode base_convert '+
- 'basename bcadd bccomp bcdiv bcmod bcmul bindec bindtextdomain bzclose bzcompress '+
- 'bzdecompress bzerrno bzerror bzerrstr bzflush bzopen bzread bzwrite ceil chdir '+
- 'checkdate checkdnsrr chgrp chmod chop chown chr chroot chunk_split class_exists '+
- 'closedir closelog copy cos cosh count count_chars date decbin dechex decoct '+
- 'deg2rad delete ebcdic2ascii echo empty end ereg ereg_replace eregi eregi_replace error_log '+
- 'error_reporting escapeshellarg escapeshellcmd eval exec exit exp explode extension_loaded '+
- 'feof fflush fgetc fgetcsv fgets fgetss file_exists file_get_contents file_put_contents '+
- 'fileatime filectime filegroup fileinode filemtime fileowner fileperms filesize filetype '+
- 'floatval flock floor flush fmod fnmatch fopen fpassthru fprintf fputcsv fputs fread fscanf '+
- 'fseek fsockopen fstat ftell ftok getallheaders getcwd getdate getenv gethostbyaddr gethostbyname '+
- 'gethostbynamel getimagesize getlastmod getmxrr getmygid getmyinode getmypid getmyuid getopt '+
- 'getprotobyname getprotobynumber getrandmax getrusage getservbyname getservbyport gettext '+
- 'gettimeofday gettype glob gmdate gmmktime ini_alter ini_get ini_get_all ini_restore ini_set '+
- 'interface_exists intval ip2long is_a is_array is_bool is_callable is_dir is_double '+
- 'is_executable is_file is_finite is_float is_infinite is_int is_integer is_link is_long '+
- 'is_nan is_null is_numeric is_object is_readable is_real is_resource is_scalar is_soap_fault '+
- 'is_string is_subclass_of is_uploaded_file is_writable is_writeable mkdir mktime nl2br '+
- 'parse_ini_file parse_str parse_url passthru pathinfo print readlink realpath rewind rewinddir rmdir '+
- 'round str_ireplace str_pad str_repeat str_replace str_rot13 str_shuffle str_split '+
- 'str_word_count strcasecmp strchr strcmp strcoll strcspn strftime strip_tags stripcslashes '+
- 'stripos stripslashes stristr strlen strnatcasecmp strnatcmp strncasecmp strncmp strpbrk '+
- 'strpos strptime strrchr strrev strripos strrpos strspn strstr strtok strtolower strtotime '+
- 'strtoupper strtr strval substr substr_compare';
-
- var keywords = 'abstract and array as break case catch cfunction class clone const continue declare default die do ' +
- 'else elseif enddeclare endfor endforeach endif endswitch endwhile extends final for foreach ' +
- 'function include include_once global goto if implements interface instanceof namespace new ' +
- 'old_function or private protected public return require require_once static switch ' +
- 'throw try use var while xor ';
-
- var constants = '__FILE__ __LINE__ __METHOD__ __FUNCTION__ __CLASS__';
-
- this.regexList = [
- { regex: SyntaxHighlighter.regexLib.singleLineCComments, css: 'comments' }, // one line comments
- { regex: SyntaxHighlighter.regexLib.multiLineCComments, css: 'comments' }, // multiline comments
- { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // double quoted strings
- { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // single quoted strings
- { regex: /\$\w+/g, css: 'variable' }, // variables
- { regex: new RegExp(this.getKeywords(funcs), 'gmi'), css: 'functions' }, // common functions
- { regex: new RegExp(this.getKeywords(constants), 'gmi'), css: 'constants' }, // constants
- { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' } // keyword
- ];
-
- this.forHtmlScript(SyntaxHighlighter.regexLib.phpScriptTags);
- };
-
- Brush.prototype = new SyntaxHighlighter.Highlighter();
- Brush.aliases = ['php'];
-
- SyntaxHighlighter.brushes.Php = Brush;
-
- // CommonJS
- typeof(exports) != 'undefined' ? exports.Brush = Brush : null;
-})();
diff --git a/guides/assets/javascripts/syntaxhighlighter/shBrushPlain.js b/guides/assets/javascripts/syntaxhighlighter/shBrushPlain.js
deleted file mode 100644
index 9f7d9e90c3..0000000000
--- a/guides/assets/javascripts/syntaxhighlighter/shBrushPlain.js
+++ /dev/null
@@ -1,33 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-;(function()
-{
- // CommonJS
- typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null;
-
- function Brush()
- {
- };
-
- Brush.prototype = new SyntaxHighlighter.Highlighter();
- Brush.aliases = ['text', 'plain'];
-
- SyntaxHighlighter.brushes.Plain = Brush;
-
- // CommonJS
- typeof(exports) != 'undefined' ? exports.Brush = Brush : null;
-})();
diff --git a/guides/assets/javascripts/syntaxhighlighter/shBrushPowerShell.js b/guides/assets/javascripts/syntaxhighlighter/shBrushPowerShell.js
deleted file mode 100644
index 0be1752968..0000000000
--- a/guides/assets/javascripts/syntaxhighlighter/shBrushPowerShell.js
+++ /dev/null
@@ -1,74 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-;(function()
-{
- // CommonJS
- typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null;
-
- function Brush()
- {
- // Contributes by B.v.Zanten, Getronics
- // http://confluence.atlassian.com/display/CONFEXT/New+Code+Macro
-
- var keywords = 'Add-Content Add-History Add-Member Add-PSSnapin Clear(-Content)? Clear-Item ' +
- 'Clear-ItemProperty Clear-Variable Compare-Object ConvertFrom-SecureString Convert-Path ' +
- 'ConvertTo-Html ConvertTo-SecureString Copy(-Item)? Copy-ItemProperty Export-Alias ' +
- 'Export-Clixml Export-Console Export-Csv ForEach(-Object)? Format-Custom Format-List ' +
- 'Format-Table Format-Wide Get-Acl Get-Alias Get-AuthenticodeSignature Get-ChildItem Get-Command ' +
- 'Get-Content Get-Credential Get-Culture Get-Date Get-EventLog Get-ExecutionPolicy ' +
- 'Get-Help Get-History Get-Host Get-Item Get-ItemProperty Get-Location Get-Member ' +
- 'Get-PfxCertificate Get-Process Get-PSDrive Get-PSProvider Get-PSSnapin Get-Service ' +
- 'Get-TraceSource Get-UICulture Get-Unique Get-Variable Get-WmiObject Group-Object ' +
- 'Import-Alias Import-Clixml Import-Csv Invoke-Expression Invoke-History Invoke-Item ' +
- 'Join-Path Measure-Command Measure-Object Move(-Item)? Move-ItemProperty New-Alias ' +
- 'New-Item New-ItemProperty New-Object New-PSDrive New-Service New-TimeSpan ' +
- 'New-Variable Out-Default Out-File Out-Host Out-Null Out-Printer Out-String Pop-Location ' +
- 'Push-Location Read-Host Remove-Item Remove-ItemProperty Remove-PSDrive Remove-PSSnapin ' +
- 'Remove-Variable Rename-Item Rename-ItemProperty Resolve-Path Restart-Service Resume-Service ' +
- 'Select-Object Select-String Set-Acl Set-Alias Set-AuthenticodeSignature Set-Content ' +
- 'Set-Date Set-ExecutionPolicy Set-Item Set-ItemProperty Set-Location Set-PSDebug ' +
- 'Set-Service Set-TraceSource Set(-Variable)? Sort-Object Split-Path Start-Service ' +
- 'Start-Sleep Start-Transcript Stop-Process Stop-Service Stop-Transcript Suspend-Service ' +
- 'Tee-Object Test-Path Trace-Command Update-FormatData Update-TypeData Where(-Object)? ' +
- 'Write-Debug Write-Error Write(-Host)? Write-Output Write-Progress Write-Verbose Write-Warning';
- var alias = 'ac asnp clc cli clp clv cpi cpp cvpa diff epal epcsv fc fl ' +
- 'ft fw gal gc gci gcm gdr ghy gi gl gm gp gps group gsv ' +
- 'gsnp gu gv gwmi iex ihy ii ipal ipcsv mi mp nal ndr ni nv oh rdr ' +
- 'ri rni rnp rp rsnp rv rvpa sal sasv sc select si sl sleep sort sp ' +
- 'spps spsv sv tee cat cd cp h history kill lp ls ' +
- 'mount mv popd ps pushd pwd r rm rmdir echo cls chdir del dir ' +
- 'erase rd ren type % \\?';
-
- this.regexList = [
- { regex: /#.*$/gm, css: 'comments' }, // one line comments
- { regex: /\$[a-zA-Z0-9]+\b/g, css: 'value' }, // variables $Computer1
- { regex: /\-[a-zA-Z]+\b/g, css: 'keyword' }, // Operators -not -and -eq
- { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // strings
- { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // strings
- { regex: new RegExp(this.getKeywords(keywords), 'gmi'), css: 'keyword' },
- { regex: new RegExp(this.getKeywords(alias), 'gmi'), css: 'keyword' }
- ];
- };
-
- Brush.prototype = new SyntaxHighlighter.Highlighter();
- Brush.aliases = ['powershell', 'ps'];
-
- SyntaxHighlighter.brushes.PowerShell = Brush;
-
- // CommonJS
- typeof(exports) != 'undefined' ? exports.Brush = Brush : null;
-})();
diff --git a/guides/assets/javascripts/syntaxhighlighter/shBrushPython.js b/guides/assets/javascripts/syntaxhighlighter/shBrushPython.js
deleted file mode 100644
index ce77462975..0000000000
--- a/guides/assets/javascripts/syntaxhighlighter/shBrushPython.js
+++ /dev/null
@@ -1,64 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-;(function()
-{
- // CommonJS
- typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null;
-
- function Brush()
- {
- // Contributed by Gheorghe Milas and Ahmad Sherif
-
- var keywords = 'and assert break class continue def del elif else ' +
- 'except exec finally for from global if import in is ' +
- 'lambda not or pass print raise return try yield while';
-
- var funcs = '__import__ abs all any apply basestring bin bool buffer callable ' +
- 'chr classmethod cmp coerce compile complex delattr dict dir ' +
- 'divmod enumerate eval execfile file filter float format frozenset ' +
- 'getattr globals hasattr hash help hex id input int intern ' +
- 'isinstance issubclass iter len list locals long map max min next ' +
- 'object oct open ord pow print property range raw_input reduce ' +
- 'reload repr reversed round set setattr slice sorted staticmethod ' +
- 'str sum super tuple type type unichr unicode vars xrange zip';
-
- var special = 'None True False self cls class_';
-
- this.regexList = [
- { regex: SyntaxHighlighter.regexLib.singleLinePerlComments, css: 'comments' },
- { regex: /^\s*@\w+/gm, css: 'decorator' },
- { regex: /(['\"]{3})([^\1])*?\1/gm, css: 'comments' },
- { regex: /"(?!")(?:\.|\\\"|[^\""\n])*"/gm, css: 'string' },
- { regex: /'(?!')(?:\.|(\\\')|[^\''\n])*'/gm, css: 'string' },
- { regex: /\+|\-|\*|\/|\%|=|==/gm, css: 'keyword' },
- { regex: /\b\d+\.?\w*/g, css: 'value' },
- { regex: new RegExp(this.getKeywords(funcs), 'gmi'), css: 'functions' },
- { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' },
- { regex: new RegExp(this.getKeywords(special), 'gm'), css: 'color1' }
- ];
-
- this.forHtmlScript(SyntaxHighlighter.regexLib.aspScriptTags);
- };
-
- Brush.prototype = new SyntaxHighlighter.Highlighter();
- Brush.aliases = ['py', 'python'];
-
- SyntaxHighlighter.brushes.Python = Brush;
-
- // CommonJS
- typeof(exports) != 'undefined' ? exports.Brush = Brush : null;
-})();
diff --git a/guides/assets/javascripts/syntaxhighlighter/shBrushRuby.js b/guides/assets/javascripts/syntaxhighlighter/shBrushRuby.js
deleted file mode 100644
index ff82130a7a..0000000000
--- a/guides/assets/javascripts/syntaxhighlighter/shBrushRuby.js
+++ /dev/null
@@ -1,55 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-;(function()
-{
- // CommonJS
- typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null;
-
- function Brush()
- {
- // Contributed by Erik Peterson.
-
- var keywords = 'alias and BEGIN begin break case class def define_method defined do each else elsif ' +
- 'END end ensure false for if in module new next nil not or raise redo rescue retry return ' +
- 'self super then throw true undef unless until when while yield';
-
- var builtins = 'Array Bignum Binding Class Continuation Dir Exception FalseClass File::Stat File Fixnum Fload ' +
- 'Hash Integer IO MatchData Method Module NilClass Numeric Object Proc Range Regexp String Struct::TMS Symbol ' +
- 'ThreadGroup Thread Time TrueClass';
-
- this.regexList = [
- { regex: SyntaxHighlighter.regexLib.singleLinePerlComments, css: 'comments' }, // one line comments
- { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // double quoted strings
- { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // single quoted strings
- { regex: /\b[A-Z0-9_]+\b/g, css: 'constants' }, // constants
- { regex: /:[a-z][A-Za-z0-9_]*/g, css: 'color2' }, // symbols
- { regex: /(\$|@@|@)\w+/g, css: 'variable bold' }, // $global, @instance, and @@class variables
- { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' }, // keywords
- { regex: new RegExp(this.getKeywords(builtins), 'gm'), css: 'color1' } // builtins
- ];
-
- this.forHtmlScript(SyntaxHighlighter.regexLib.aspScriptTags);
- };
-
- Brush.prototype = new SyntaxHighlighter.Highlighter();
- Brush.aliases = ['ruby', 'rails', 'ror', 'rb'];
-
- SyntaxHighlighter.brushes.Ruby = Brush;
-
- // CommonJS
- typeof(exports) != 'undefined' ? exports.Brush = Brush : null;
-})();
diff --git a/guides/assets/javascripts/syntaxhighlighter/shBrushSass.js b/guides/assets/javascripts/syntaxhighlighter/shBrushSass.js
deleted file mode 100644
index aa04da0996..0000000000
--- a/guides/assets/javascripts/syntaxhighlighter/shBrushSass.js
+++ /dev/null
@@ -1,94 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-;(function()
-{
- // CommonJS
- typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null;
-
- function Brush()
- {
- function getKeywordsCSS(str)
- {
- return '\\b([a-z_]|)' + str.replace(/ /g, '(?=:)\\b|\\b([a-z_\\*]|\\*|)') + '(?=:)\\b';
- };
-
- function getValuesCSS(str)
- {
- return '\\b' + str.replace(/ /g, '(?!-)(?!:)\\b|\\b()') + '\:\\b';
- };
-
- var keywords = 'ascent azimuth background-attachment background-color background-image background-position ' +
- 'background-repeat background baseline bbox border-collapse border-color border-spacing border-style border-top ' +
- 'border-right border-bottom border-left border-top-color border-right-color border-bottom-color border-left-color ' +
- 'border-top-style border-right-style border-bottom-style border-left-style border-top-width border-right-width ' +
- 'border-bottom-width border-left-width border-width border bottom cap-height caption-side centerline clear clip color ' +
- 'content counter-increment counter-reset cue-after cue-before cue cursor definition-src descent direction display ' +
- 'elevation empty-cells float font-size-adjust font-family font-size font-stretch font-style font-variant font-weight font ' +
- 'height left letter-spacing line-height list-style-image list-style-position list-style-type list-style margin-top ' +
- 'margin-right margin-bottom margin-left margin marker-offset marks mathline max-height max-width min-height min-width orphans ' +
- 'outline-color outline-style outline-width outline overflow padding-top padding-right padding-bottom padding-left padding page ' +
- 'page-break-after page-break-before page-break-inside pause pause-after pause-before pitch pitch-range play-during position ' +
- 'quotes right richness size slope src speak-header speak-numeral speak-punctuation speak speech-rate stemh stemv stress ' +
- 'table-layout text-align top text-decoration text-indent text-shadow text-transform unicode-bidi unicode-range units-per-em ' +
- 'vertical-align visibility voice-family volume white-space widows width widths word-spacing x-height z-index';
-
- var values = 'above absolute all always aqua armenian attr aural auto avoid baseline behind below bidi-override black blink block blue bold bolder '+
- 'both bottom braille capitalize caption center center-left center-right circle close-quote code collapse compact condensed '+
- 'continuous counter counters crop cross crosshair cursive dashed decimal decimal-leading-zero digits disc dotted double '+
- 'embed embossed e-resize expanded extra-condensed extra-expanded fantasy far-left far-right fast faster fixed format fuchsia '+
- 'gray green groove handheld hebrew help hidden hide high higher icon inline-table inline inset inside invert italic '+
- 'justify landscape large larger left-side left leftwards level lighter lime line-through list-item local loud lower-alpha '+
- 'lowercase lower-greek lower-latin lower-roman lower low ltr marker maroon medium message-box middle mix move narrower '+
- 'navy ne-resize no-close-quote none no-open-quote no-repeat normal nowrap n-resize nw-resize oblique olive once open-quote outset '+
- 'outside overline pointer portrait pre print projection purple red relative repeat repeat-x repeat-y rgb ridge right right-side '+
- 'rightwards rtl run-in screen scroll semi-condensed semi-expanded separate se-resize show silent silver slower slow '+
- 'small small-caps small-caption smaller soft solid speech spell-out square s-resize static status-bar sub super sw-resize '+
- 'table-caption table-cell table-column table-column-group table-footer-group table-header-group table-row table-row-group teal '+
- 'text-bottom text-top thick thin top transparent tty tv ultra-condensed ultra-expanded underline upper-alpha uppercase upper-latin '+
- 'upper-roman url visible wait white wider w-resize x-fast x-high x-large x-loud x-low x-slow x-small x-soft xx-large xx-small yellow';
-
- var fonts = '[mM]onospace [tT]ahoma [vV]erdana [aA]rial [hH]elvetica [sS]ans-serif [sS]erif [cC]ourier mono sans serif';
-
- var statements = '!important !default';
- var preprocessor = '@import @extend @debug @warn @if @for @while @mixin @include';
-
- var r = SyntaxHighlighter.regexLib;
-
- this.regexList = [
- { regex: r.multiLineCComments, css: 'comments' }, // multiline comments
- { regex: r.singleLineCComments, css: 'comments' }, // singleline comments
- { regex: r.doubleQuotedString, css: 'string' }, // double quoted strings
- { regex: r.singleQuotedString, css: 'string' }, // single quoted strings
- { regex: /\#[a-fA-F0-9]{3,6}/g, css: 'value' }, // html colors
- { regex: /\b(-?\d+)(\.\d+)?(px|em|pt|\:|\%|)\b/g, css: 'value' }, // sizes
- { regex: /\$\w+/g, css: 'variable' }, // variables
- { regex: new RegExp(this.getKeywords(statements), 'g'), css: 'color3' }, // statements
- { regex: new RegExp(this.getKeywords(preprocessor), 'g'), css: 'preprocessor' }, // preprocessor
- { regex: new RegExp(getKeywordsCSS(keywords), 'gm'), css: 'keyword' }, // keywords
- { regex: new RegExp(getValuesCSS(values), 'g'), css: 'value' }, // values
- { regex: new RegExp(this.getKeywords(fonts), 'g'), css: 'color1' } // fonts
- ];
- };
-
- Brush.prototype = new SyntaxHighlighter.Highlighter();
- Brush.aliases = ['sass', 'scss'];
-
- SyntaxHighlighter.brushes.Sass = Brush;
-
- // CommonJS
- typeof(exports) != 'undefined' ? exports.Brush = Brush : null;
-})();
diff --git a/guides/assets/javascripts/syntaxhighlighter/shBrushScala.js b/guides/assets/javascripts/syntaxhighlighter/shBrushScala.js
deleted file mode 100644
index 4b0b6f04d2..0000000000
--- a/guides/assets/javascripts/syntaxhighlighter/shBrushScala.js
+++ /dev/null
@@ -1,51 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-;(function()
-{
- // CommonJS
- typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null;
-
- function Brush()
- {
- // Contributed by Yegor Jbanov and David Bernard.
-
- var keywords = 'val sealed case def true trait implicit forSome import match object null finally super ' +
- 'override try lazy for var catch throw type extends class while with new final yield abstract ' +
- 'else do if return protected private this package false';
-
- var keyops = '[_:=><%#@]+';
-
- this.regexList = [
- { regex: SyntaxHighlighter.regexLib.singleLineCComments, css: 'comments' }, // one line comments
- { regex: SyntaxHighlighter.regexLib.multiLineCComments, css: 'comments' }, // multiline comments
- { regex: SyntaxHighlighter.regexLib.multiLineSingleQuotedString, css: 'string' }, // multi-line strings
- { regex: SyntaxHighlighter.regexLib.multiLineDoubleQuotedString, css: 'string' }, // double-quoted string
- { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // strings
- { regex: /0x[a-f0-9]+|\d+(\.\d+)?/gi, css: 'value' }, // numbers
- { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' }, // keywords
- { regex: new RegExp(keyops, 'gm'), css: 'keyword' } // scala keyword
- ];
- }
-
- Brush.prototype = new SyntaxHighlighter.Highlighter();
- Brush.aliases = ['scala'];
-
- SyntaxHighlighter.brushes.Scala = Brush;
-
- // CommonJS
- typeof(exports) != 'undefined' ? exports.Brush = Brush : null;
-})();
diff --git a/guides/assets/javascripts/syntaxhighlighter/shBrushSql.js b/guides/assets/javascripts/syntaxhighlighter/shBrushSql.js
deleted file mode 100644
index 5c2cd8806f..0000000000
--- a/guides/assets/javascripts/syntaxhighlighter/shBrushSql.js
+++ /dev/null
@@ -1,66 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-;(function()
-{
- // CommonJS
- typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null;
-
- function Brush()
- {
- var funcs = 'abs avg case cast coalesce convert count current_timestamp ' +
- 'current_user day isnull left lower month nullif replace right ' +
- 'session_user space substring sum system_user upper user year';
-
- var keywords = 'absolute action add after alter as asc at authorization begin bigint ' +
- 'binary bit by cascade char character check checkpoint close collate ' +
- 'column commit committed connect connection constraint contains continue ' +
- 'create cube current current_date current_time cursor database date ' +
- 'deallocate dec decimal declare default delete desc distinct double drop ' +
- 'dynamic else end end-exec escape except exec execute false fetch first ' +
- 'float for force foreign forward free from full function global goto grant ' +
- 'group grouping having hour ignore index inner insensitive insert instead ' +
- 'int integer intersect into is isolation key last level load local max min ' +
- 'minute modify move name national nchar next no numeric of off on only ' +
- 'open option order out output partial password precision prepare primary ' +
- 'prior privileges procedure public read real references relative repeatable ' +
- 'restrict return returns revoke rollback rollup rows rule schema scroll ' +
- 'second section select sequence serializable set size smallint static ' +
- 'statistics table temp temporary then time timestamp to top transaction ' +
- 'translation trigger true truncate uncommitted union unique update values ' +
- 'varchar varying view when where with work';
-
- var operators = 'all and any between cross in join like not null or outer some';
-
- this.regexList = [
- { regex: /--(.*)$/gm, css: 'comments' }, // one line and multiline comments
- { regex: SyntaxHighlighter.regexLib.multiLineDoubleQuotedString, css: 'string' }, // double quoted strings
- { regex: SyntaxHighlighter.regexLib.multiLineSingleQuotedString, css: 'string' }, // single quoted strings
- { regex: new RegExp(this.getKeywords(funcs), 'gmi'), css: 'color2' }, // functions
- { regex: new RegExp(this.getKeywords(operators), 'gmi'), css: 'color1' }, // operators and such
- { regex: new RegExp(this.getKeywords(keywords), 'gmi'), css: 'keyword' } // keyword
- ];
- };
-
- Brush.prototype = new SyntaxHighlighter.Highlighter();
- Brush.aliases = ['sql'];
-
- SyntaxHighlighter.brushes.Sql = Brush;
-
- // CommonJS
- typeof(exports) != 'undefined' ? exports.Brush = Brush : null;
-})();
-
diff --git a/guides/assets/javascripts/syntaxhighlighter/shBrushVb.js b/guides/assets/javascripts/syntaxhighlighter/shBrushVb.js
deleted file mode 100644
index be845dc0b3..0000000000
--- a/guides/assets/javascripts/syntaxhighlighter/shBrushVb.js
+++ /dev/null
@@ -1,56 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-;(function()
-{
- // CommonJS
- typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null;
-
- function Brush()
- {
- var keywords = 'AddHandler AddressOf AndAlso Alias And Ansi As Assembly Auto ' +
- 'Boolean ByRef Byte ByVal Call Case Catch CBool CByte CChar CDate ' +
- 'CDec CDbl Char CInt Class CLng CObj Const CShort CSng CStr CType ' +
- 'Date Decimal Declare Default Delegate Dim DirectCast Do Double Each ' +
- 'Else ElseIf End Enum Erase Error Event Exit False Finally For Friend ' +
- 'Function Get GetType GoSub GoTo Handles If Implements Imports In ' +
- 'Inherits Integer Interface Is Let Lib Like Long Loop Me Mod Module ' +
- 'MustInherit MustOverride MyBase MyClass Namespace New Next Not Nothing ' +
- 'NotInheritable NotOverridable Object On Option Optional Or OrElse ' +
- 'Overloads Overridable Overrides ParamArray Preserve Private Property ' +
- 'Protected Public RaiseEvent ReadOnly ReDim REM RemoveHandler Resume ' +
- 'Return Select Set Shadows Shared Short Single Static Step Stop String ' +
- 'Structure Sub SyncLock Then Throw To True Try TypeOf Unicode Until ' +
- 'Variant When While With WithEvents WriteOnly Xor';
-
- this.regexList = [
- { regex: /'.*$/gm, css: 'comments' }, // one line comments
- { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // strings
- { regex: /^\s*#.*$/gm, css: 'preprocessor' }, // preprocessor tags like #region and #endregion
- { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' } // vb keyword
- ];
-
- this.forHtmlScript(SyntaxHighlighter.regexLib.aspScriptTags);
- };
-
- Brush.prototype = new SyntaxHighlighter.Highlighter();
- Brush.aliases = ['vb', 'vbnet'];
-
- SyntaxHighlighter.brushes.Vb = Brush;
-
- // CommonJS
- typeof(exports) != 'undefined' ? exports.Brush = Brush : null;
-})();
diff --git a/guides/assets/javascripts/syntaxhighlighter/shBrushXml.js b/guides/assets/javascripts/syntaxhighlighter/shBrushXml.js
deleted file mode 100644
index 69d9fd0b1f..0000000000
--- a/guides/assets/javascripts/syntaxhighlighter/shBrushXml.js
+++ /dev/null
@@ -1,69 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-;(function()
-{
- // CommonJS
- typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null;
-
- function Brush()
- {
- function process(match, regexInfo)
- {
- var constructor = SyntaxHighlighter.Match,
- code = match[0],
- tag = new XRegExp('(&lt;|<)[\\s\\/\\?]*(?<name>[:\\w-\\.]+)', 'xg').exec(code),
- result = []
- ;
-
- if (match.attributes != null)
- {
- var attributes,
- regex = new XRegExp('(?<name> [\\w:\\-\\.]+)' +
- '\\s*=\\s*' +
- '(?<value> ".*?"|\'.*?\'|\\w+)',
- 'xg');
-
- while ((attributes = regex.exec(code)) != null)
- {
- result.push(new constructor(attributes.name, match.index + attributes.index, 'color1'));
- result.push(new constructor(attributes.value, match.index + attributes.index + attributes[0].indexOf(attributes.value), 'string'));
- }
- }
-
- if (tag != null)
- result.push(
- new constructor(tag.name, match.index + tag[0].indexOf(tag.name), 'keyword')
- );
-
- return result;
- }
-
- this.regexList = [
- { regex: new XRegExp('(\\&lt;|<)\\!\\[[\\w\\s]*?\\[(.|\\s)*?\\]\\](\\&gt;|>)', 'gm'), css: 'color2' }, // <![ ... [ ... ]]>
- { regex: SyntaxHighlighter.regexLib.xmlComments, css: 'comments' }, // <!-- ... -->
- { regex: new XRegExp('(&lt;|<)[\\s\\/\\?]*(\\w+)(?<attributes>.*?)[\\s\\/\\?]*(&gt;|>)', 'sg'), func: process }
- ];
- };
-
- Brush.prototype = new SyntaxHighlighter.Highlighter();
- Brush.aliases = ['xml', 'xhtml', 'xslt', 'html'];
-
- SyntaxHighlighter.brushes.Xml = Brush;
-
- // CommonJS
- typeof(exports) != 'undefined' ? exports.Brush = Brush : null;
-})();
diff --git a/guides/assets/javascripts/syntaxhighlighter/shCore.js b/guides/assets/javascripts/syntaxhighlighter/shCore.js
deleted file mode 100644
index b47b645472..0000000000
--- a/guides/assets/javascripts/syntaxhighlighter/shCore.js
+++ /dev/null
@@ -1,17 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-eval(function(p,a,c,k,e,d){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('K M;I(M)1S 2U("2a\'t 4k M 4K 2g 3l 4G 4H");(6(){6 r(f,e){I(!M.1R(f))1S 3m("3s 15 4R");K a=f.1w;f=M(f.1m,t(f)+(e||""));I(a)f.1w={1m:a.1m,19:a.19?a.19.1a(0):N};H f}6 t(f){H(f.1J?"g":"")+(f.4s?"i":"")+(f.4p?"m":"")+(f.4v?"x":"")+(f.3n?"y":"")}6 B(f,e,a,b){K c=u.L,d,h,g;v=R;5K{O(;c--;){g=u[c];I(a&g.3r&&(!g.2p||g.2p.W(b))){g.2q.12=e;I((h=g.2q.X(f))&&h.P===e){d={3k:g.2b.W(b,h,a),1C:h};1N}}}}5v(i){1S i}5q{v=11}H d}6 p(f,e,a){I(3b.Z.1i)H f.1i(e,a);O(a=a||0;a<f.L;a++)I(f[a]===e)H a;H-1}M=6(f,e){K a=[],b=M.1B,c=0,d,h;I(M.1R(f)){I(e!==1d)1S 3m("2a\'t 5r 5I 5F 5B 5C 15 5E 5p");H r(f)}I(v)1S 2U("2a\'t W 3l M 59 5m 5g 5x 5i");e=e||"";O(d={2N:11,19:[],2K:6(g){H e.1i(g)>-1},3d:6(g){e+=g}};c<f.L;)I(h=B(f,c,b,d)){a.U(h.3k);c+=h.1C[0].L||1}Y I(h=n.X.W(z[b],f.1a(c))){a.U(h[0]);c+=h[0].L}Y{h=f.3a(c);I(h==="[")b=M.2I;Y I(h==="]")b=M.1B;a.U(h);c++}a=15(a.1K(""),n.Q.W(e,w,""));a.1w={1m:f,19:d.2N?d.19:N};H a};M.3v="1.5.0";M.2I=1;M.1B=2;K C=/\\$(?:(\\d\\d?|[$&`\'])|{([$\\w]+)})/g,w=/[^5h]+|([\\s\\S])(?=[\\s\\S]*\\1)/g,A=/^(?:[?*+]|{\\d+(?:,\\d*)?})\\??/,v=11,u=[],n={X:15.Z.X,1A:15.Z.1A,1C:1r.Z.1C,Q:1r.Z.Q,1e:1r.Z.1e},x=n.X.W(/()??/,"")[1]===1d,D=6(){K f=/^/g;n.1A.W(f,"");H!f.12}(),y=6(){K f=/x/g;n.Q.W("x",f,"");H!f.12}(),E=15.Z.3n!==1d,z={};z[M.2I]=/^(?:\\\\(?:[0-3][0-7]{0,2}|[4-7][0-7]?|x[\\29-26-f]{2}|u[\\29-26-f]{4}|c[A-3o-z]|[\\s\\S]))/;z[M.1B]=/^(?:\\\\(?:0(?:[0-3][0-7]{0,2}|[4-7][0-7]?)?|[1-9]\\d*|x[\\29-26-f]{2}|u[\\29-26-f]{4}|c[A-3o-z]|[\\s\\S])|\\(\\?[:=!]|[?*+]\\?|{\\d+(?:,\\d*)?}\\??)/;M.1h=6(f,e,a,b){u.U({2q:r(f,"g"+(E?"y":"")),2b:e,3r:a||M.1B,2p:b||N})};M.2n=6(f,e){K a=f+"/"+(e||"");H M.2n[a]||(M.2n[a]=M(f,e))};M.3c=6(f){H r(f,"g")};M.5l=6(f){H f.Q(/[-[\\]{}()*+?.,\\\\^$|#\\s]/g,"\\\\$&")};M.5e=6(f,e,a,b){e=r(e,"g"+(b&&E?"y":""));e.12=a=a||0;f=e.X(f);H b?f&&f.P===a?f:N:f};M.3q=6(){M.1h=6(){1S 2U("2a\'t 55 1h 54 3q")}};M.1R=6(f){H 53.Z.1q.W(f)==="[2m 15]"};M.3p=6(f,e,a,b){O(K c=r(e,"g"),d=-1,h;h=c.X(f);){a.W(b,h,++d,f,c);c.12===h.P&&c.12++}I(e.1J)e.12=0};M.57=6(f,e){H 6 a(b,c){K d=e[c].1I?e[c]:{1I:e[c]},h=r(d.1I,"g"),g=[],i;O(i=0;i<b.L;i++)M.3p(b[i],h,6(k){g.U(d.3j?k[d.3j]||"":k[0])});H c===e.L-1||!g.L?g:a(g,c+1)}([f],0)};15.Z.1p=6(f,e){H J.X(e[0])};15.Z.W=6(f,e){H J.X(e)};15.Z.X=6(f){K e=n.X.1p(J,14),a;I(e){I(!x&&e.L>1&&p(e,"")>-1){a=15(J.1m,n.Q.W(t(J),"g",""));n.Q.W(f.1a(e.P),a,6(){O(K c=1;c<14.L-2;c++)I(14[c]===1d)e[c]=1d})}I(J.1w&&J.1w.19)O(K b=1;b<e.L;b++)I(a=J.1w.19[b-1])e[a]=e[b];!D&&J.1J&&!e[0].L&&J.12>e.P&&J.12--}H e};I(!D)15.Z.1A=6(f){(f=n.X.W(J,f))&&J.1J&&!f[0].L&&J.12>f.P&&J.12--;H!!f};1r.Z.1C=6(f){M.1R(f)||(f=15(f));I(f.1J){K e=n.1C.1p(J,14);f.12=0;H e}H f.X(J)};1r.Z.Q=6(f,e){K a=M.1R(f),b,c;I(a&&1j e.58()==="3f"&&e.1i("${")===-1&&y)H n.Q.1p(J,14);I(a){I(f.1w)b=f.1w.19}Y f+="";I(1j e==="6")c=n.Q.W(J,f,6(){I(b){14[0]=1f 1r(14[0]);O(K d=0;d<b.L;d++)I(b[d])14[0][b[d]]=14[d+1]}I(a&&f.1J)f.12=14[14.L-2]+14[0].L;H e.1p(N,14)});Y{c=J+"";c=n.Q.W(c,f,6(){K d=14;H n.Q.W(e,C,6(h,g,i){I(g)5b(g){24"$":H"$";24"&":H d[0];24"`":H d[d.L-1].1a(0,d[d.L-2]);24"\'":H d[d.L-1].1a(d[d.L-2]+d[0].L);5a:i="";g=+g;I(!g)H h;O(;g>d.L-3;){i=1r.Z.1a.W(g,-1)+i;g=1Q.3i(g/10)}H(g?d[g]||"":"$")+i}Y{g=+i;I(g<=d.L-3)H d[g];g=b?p(b,i):-1;H g>-1?d[g+1]:h}})})}I(a&&f.1J)f.12=0;H c};1r.Z.1e=6(f,e){I(!M.1R(f))H n.1e.1p(J,14);K a=J+"",b=[],c=0,d,h;I(e===1d||+e<0)e=5D;Y{e=1Q.3i(+e);I(!e)H[]}O(f=M.3c(f);d=f.X(a);){I(f.12>c){b.U(a.1a(c,d.P));d.L>1&&d.P<a.L&&3b.Z.U.1p(b,d.1a(1));h=d[0].L;c=f.12;I(b.L>=e)1N}f.12===d.P&&f.12++}I(c===a.L){I(!n.1A.W(f,"")||h)b.U("")}Y b.U(a.1a(c));H b.L>e?b.1a(0,e):b};M.1h(/\\(\\?#[^)]*\\)/,6(f){H n.1A.W(A,f.2S.1a(f.P+f[0].L))?"":"(?:)"});M.1h(/\\((?!\\?)/,6(){J.19.U(N);H"("});M.1h(/\\(\\?<([$\\w]+)>/,6(f){J.19.U(f[1]);J.2N=R;H"("});M.1h(/\\\\k<([\\w$]+)>/,6(f){K e=p(J.19,f[1]);H e>-1?"\\\\"+(e+1)+(3R(f.2S.3a(f.P+f[0].L))?"":"(?:)"):f[0]});M.1h(/\\[\\^?]/,6(f){H f[0]==="[]"?"\\\\b\\\\B":"[\\\\s\\\\S]"});M.1h(/^\\(\\?([5A]+)\\)/,6(f){J.3d(f[1]);H""});M.1h(/(?:\\s+|#.*)+/,6(f){H n.1A.W(A,f.2S.1a(f.P+f[0].L))?"":"(?:)"},M.1B,6(){H J.2K("x")});M.1h(/\\./,6(){H"[\\\\s\\\\S]"},M.1B,6(){H J.2K("s")})})();1j 2e!="1d"&&(2e.M=M);K 1v=6(){6 r(a,b){a.1l.1i(b)!=-1||(a.1l+=" "+b)}6 t(a){H a.1i("3e")==0?a:"3e"+a}6 B(a){H e.1Y.2A[t(a)]}6 p(a,b,c){I(a==N)H N;K d=c!=R?a.3G:[a.2G],h={"#":"1c",".":"1l"}[b.1o(0,1)]||"3h",g,i;g=h!="3h"?b.1o(1):b.5u();I((a[h]||"").1i(g)!=-1)H a;O(a=0;d&&a<d.L&&i==N;a++)i=p(d[a],b,c);H i}6 C(a,b){K c={},d;O(d 2g a)c[d]=a[d];O(d 2g b)c[d]=b[d];H c}6 w(a,b,c,d){6 h(g){g=g||1P.5y;I(!g.1F){g.1F=g.52;g.3N=6(){J.5w=11}}c.W(d||1P,g)}a.3g?a.3g("4U"+b,h):a.4y(b,h,11)}6 A(a,b){K c=e.1Y.2j,d=N;I(c==N){c={};O(K h 2g e.1U){K g=e.1U[h];d=g.4x;I(d!=N){g.1V=h.4w();O(g=0;g<d.L;g++)c[d[g]]=h}}e.1Y.2j=c}d=e.1U[c[a]];d==N&&b!=11&&1P.1X(e.13.1x.1X+(e.13.1x.3E+a));H d}6 v(a,b){O(K c=a.1e("\\n"),d=0;d<c.L;d++)c[d]=b(c[d],d);H c.1K("\\n")}6 u(a,b){I(a==N||a.L==0||a=="\\n")H a;a=a.Q(/</g,"&1y;");a=a.Q(/ {2,}/g,6(c){O(K d="",h=0;h<c.L-1;h++)d+=e.13.1W;H d+" "});I(b!=N)a=v(a,6(c){I(c.L==0)H"";K d="";c=c.Q(/^(&2s;| )+/,6(h){d=h;H""});I(c.L==0)H d;H d+\'<17 1g="\'+b+\'">\'+c+"</17>"});H a}6 n(a,b){a.1e("\\n");O(K c="",d=0;d<50;d++)c+=" ";H a=v(a,6(h){I(h.1i("\\t")==-1)H h;O(K g=0;(g=h.1i("\\t"))!=-1;)h=h.1o(0,g)+c.1o(0,b-g%b)+h.1o(g+1,h.L);H h})}6 x(a){H a.Q(/^\\s+|\\s+$/g,"")}6 D(a,b){I(a.P<b.P)H-1;Y I(a.P>b.P)H 1;Y I(a.L<b.L)H-1;Y I(a.L>b.L)H 1;H 0}6 y(a,b){6 c(k){H k[0]}O(K d=N,h=[],g=b.2D?b.2D:c;(d=b.1I.X(a))!=N;){K i=g(d,b);I(1j i=="3f")i=[1f e.2L(i,d.P,b.23)];h=h.1O(i)}H h}6 E(a){K b=/(.*)((&1G;|&1y;).*)/;H a.Q(e.3A.3M,6(c){K d="",h=N;I(h=b.X(c)){c=h[1];d=h[2]}H\'<a 2h="\'+c+\'">\'+c+"</a>"+d})}6 z(){O(K a=1E.36("1k"),b=[],c=0;c<a.L;c++)a[c].3s=="20"&&b.U(a[c]);H b}6 f(a){a=a.1F;K b=p(a,".20",R);a=p(a,".3O",R);K c=1E.4i("3t");I(!(!a||!b||p(a,"3t"))){B(b.1c);r(b,"1m");O(K d=a.3G,h=[],g=0;g<d.L;g++)h.U(d[g].4z||d[g].4A);h=h.1K("\\r");c.39(1E.4D(h));a.39(c);c.2C();c.4C();w(c,"4u",6(){c.2G.4E(c);b.1l=b.1l.Q("1m","")})}}I(1j 3F!="1d"&&1j M=="1d")M=3F("M").M;K e={2v:{"1g-27":"","2i-1s":1,"2z-1s-2t":11,1M:N,1t:N,"42-45":R,"43-22":4,1u:R,16:R,"3V-17":R,2l:11,"41-40":R,2k:11,"1z-1k":11},13:{1W:"&2s;",2M:R,46:11,44:11,34:"4n",1x:{21:"4o 1m",2P:"?",1X:"1v\\n\\n",3E:"4r\'t 4t 1D O: ",4g:"4m 4B\'t 51 O 1z-1k 4F: ",37:\'<!4T 1z 4S "-//4V//3H 4W 1.0 4Z//4Y" "1Z://2y.3L.3K/4X/3I/3H/3I-4P.4J"><1z 4I="1Z://2y.3L.3K/4L/5L"><3J><4N 1Z-4M="5G-5M" 6K="2O/1z; 6J=6I-8" /><1t>6L 1v</1t></3J><3B 1L="25-6M:6Q,6P,6O,6N-6F;6y-2f:#6x;2f:#6w;25-22:6v;2O-3D:3C;"><T 1L="2O-3D:3C;3w-32:1.6z;"><T 1L="25-22:6A-6E;">1v</T><T 1L="25-22:.6C;3w-6B:6R;"><T>3v 3.0.76 (72 73 3x)</T><T><a 2h="1Z://3u.2w/1v" 1F="38" 1L="2f:#3y">1Z://3u.2w/1v</a></T><T>70 17 6U 71.</T><T>6T 6X-3x 6Y 6D.</T></T><T>6t 61 60 J 1k, 5Z <a 2h="6u://2y.62.2w/63-66/65?64=5X-5W&5P=5O" 1L="2f:#3y">5R</a> 5V <2R/>5U 5T 5S!</T></T></3B></1z>\'}},1Y:{2j:N,2A:{}},1U:{},3A:{6n:/\\/\\*[\\s\\S]*?\\*\\//2c,6m:/\\/\\/.*$/2c,6l:/#.*$/2c,6k:/"([^\\\\"\\n]|\\\\.)*"/g,6o:/\'([^\\\\\'\\n]|\\\\.)*\'/g,6p:1f M(\'"([^\\\\\\\\"]|\\\\\\\\.)*"\',"3z"),6s:1f M("\'([^\\\\\\\\\']|\\\\\\\\.)*\'","3z"),6q:/(&1y;|<)!--[\\s\\S]*?--(&1G;|>)/2c,3M:/\\w+:\\/\\/[\\w-.\\/?%&=:@;]*/g,6a:{18:/(&1y;|<)\\?=?/g,1b:/\\?(&1G;|>)/g},69:{18:/(&1y;|<)%=?/g,1b:/%(&1G;|>)/g},6d:{18:/(&1y;|<)\\s*1k.*?(&1G;|>)/2T,1b:/(&1y;|<)\\/\\s*1k\\s*(&1G;|>)/2T}},16:{1H:6(a){6 b(i,k){H e.16.2o(i,k,e.13.1x[k])}O(K c=\'<T 1g="16">\',d=e.16.2x,h=d.2X,g=0;g<h.L;g++)c+=(d[h[g]].1H||b)(a,h[g]);c+="</T>";H c},2o:6(a,b,c){H\'<2W><a 2h="#" 1g="6e 6h\'+b+" "+b+\'">\'+c+"</a></2W>"},2b:6(a){K b=a.1F,c=b.1l||"";b=B(p(b,".20",R).1c);K d=6(h){H(h=15(h+"6f(\\\\w+)").X(c))?h[1]:N}("6g");b&&d&&e.16.2x[d].2B(b);a.3N()},2x:{2X:["21","2P"],21:{1H:6(a){I(a.V("2l")!=R)H"";K b=a.V("1t");H e.16.2o(a,"21",b?b:e.13.1x.21)},2B:6(a){a=1E.6j(t(a.1c));a.1l=a.1l.Q("47","")}},2P:{2B:6(){K a="68=0";a+=", 18="+(31.30-33)/2+", 32="+(31.2Z-2Y)/2+", 30=33, 2Z=2Y";a=a.Q(/^,/,"");a=1P.6Z("","38",a);a.2C();K b=a.1E;b.6W(e.13.1x.37);b.6V();a.2C()}}}},35:6(a,b){K c;I(b)c=[b];Y{c=1E.36(e.13.34);O(K d=[],h=0;h<c.L;h++)d.U(c[h]);c=d}c=c;d=[];I(e.13.2M)c=c.1O(z());I(c.L===0)H d;O(h=0;h<c.L;h++){O(K g=c[h],i=a,k=c[h].1l,j=3W 0,l={},m=1f M("^\\\\[(?<2V>(.*?))\\\\]$"),s=1f M("(?<27>[\\\\w-]+)\\\\s*:\\\\s*(?<1T>[\\\\w-%#]+|\\\\[.*?\\\\]|\\".*?\\"|\'.*?\')\\\\s*;?","g");(j=s.X(k))!=N;){K o=j.1T.Q(/^[\'"]|[\'"]$/g,"");I(o!=N&&m.1A(o)){o=m.X(o);o=o.2V.L>0?o.2V.1e(/\\s*,\\s*/):[]}l[j.27]=o}g={1F:g,1n:C(i,l)};g.1n.1D!=N&&d.U(g)}H d},1M:6(a,b){K c=J.35(a,b),d=N,h=e.13;I(c.L!==0)O(K g=0;g<c.L;g++){b=c[g];K i=b.1F,k=b.1n,j=k.1D,l;I(j!=N){I(k["1z-1k"]=="R"||e.2v["1z-1k"]==R){d=1f e.4l(j);j="4O"}Y I(d=A(j))d=1f d;Y 6H;l=i.3X;I(h.2M){l=l;K m=x(l),s=11;I(m.1i("<![6G[")==0){m=m.4h(9);s=R}K o=m.L;I(m.1i("]]\\>")==o-3){m=m.4h(0,o-3);s=R}l=s?m:l}I((i.1t||"")!="")k.1t=i.1t;k.1D=j;d.2Q(k);b=d.2F(l);I((i.1c||"")!="")b.1c=i.1c;i.2G.74(b,i)}}},2E:6(a){w(1P,"4k",6(){e.1M(a)})}};e.2E=e.2E;e.1M=e.1M;e.2L=6(a,b,c){J.1T=a;J.P=b;J.L=a.L;J.23=c;J.1V=N};e.2L.Z.1q=6(){H J.1T};e.4l=6(a){6 b(j,l){O(K m=0;m<j.L;m++)j[m].P+=l}K c=A(a),d,h=1f e.1U.5Y,g=J,i="2F 1H 2Q".1e(" ");I(c!=N){d=1f c;O(K k=0;k<i.L;k++)(6(){K j=i[k];g[j]=6(){H h[j].1p(h,14)}})();d.28==N?1P.1X(e.13.1x.1X+(e.13.1x.4g+a)):h.2J.U({1I:d.28.17,2D:6(j){O(K l=j.17,m=[],s=d.2J,o=j.P+j.18.L,F=d.28,q,G=0;G<s.L;G++){q=y(l,s[G]);b(q,o);m=m.1O(q)}I(F.18!=N&&j.18!=N){q=y(j.18,F.18);b(q,j.P);m=m.1O(q)}I(F.1b!=N&&j.1b!=N){q=y(j.1b,F.1b);b(q,j.P+j[0].5Q(j.1b));m=m.1O(q)}O(j=0;j<m.L;j++)m[j].1V=c.1V;H m}})}};e.4j=6(){};e.4j.Z={V:6(a,b){K c=J.1n[a];c=c==N?b:c;K d={"R":R,"11":11}[c];H d==N?c:d},3Y:6(a){H 1E.4i(a)},4c:6(a,b){K c=[];I(a!=N)O(K d=0;d<a.L;d++)I(1j a[d]=="2m")c=c.1O(y(b,a[d]));H J.4e(c.6b(D))},4e:6(a){O(K b=0;b<a.L;b++)I(a[b]!==N)O(K c=a[b],d=c.P+c.L,h=b+1;h<a.L&&a[b]!==N;h++){K g=a[h];I(g!==N)I(g.P>d)1N;Y I(g.P==c.P&&g.L>c.L)a[b]=N;Y I(g.P>=c.P&&g.P<d)a[h]=N}H a},4d:6(a){K b=[],c=2u(J.V("2i-1s"));v(a,6(d,h){b.U(h+c)});H b},3U:6(a){K b=J.V("1M",[]);I(1j b!="2m"&&b.U==N)b=[b];a:{a=a.1q();K c=3W 0;O(c=c=1Q.6c(c||0,0);c<b.L;c++)I(b[c]==a){b=c;1N a}b=-1}H b!=-1},2r:6(a,b,c){a=["1s","6i"+b,"P"+a,"6r"+(b%2==0?1:2).1q()];J.3U(b)&&a.U("67");b==0&&a.U("1N");H\'<T 1g="\'+a.1K(" ")+\'">\'+c+"</T>"},3Q:6(a,b){K c="",d=a.1e("\\n").L,h=2u(J.V("2i-1s")),g=J.V("2z-1s-2t");I(g==R)g=(h+d-1).1q().L;Y I(3R(g)==R)g=0;O(K i=0;i<d;i++){K k=b?b[i]:h+i,j;I(k==0)j=e.13.1W;Y{j=g;O(K l=k.1q();l.L<j;)l="0"+l;j=l}a=j;c+=J.2r(i,k,a)}H c},49:6(a,b){a=x(a);K c=a.1e("\\n");J.V("2z-1s-2t");K d=2u(J.V("2i-1s"));a="";O(K h=J.V("1D"),g=0;g<c.L;g++){K i=c[g],k=/^(&2s;|\\s)+/.X(i),j=N,l=b?b[g]:d+g;I(k!=N){j=k[0].1q();i=i.1o(j.L);j=j.Q(" ",e.13.1W)}i=x(i);I(i.L==0)i=e.13.1W;a+=J.2r(g,l,(j!=N?\'<17 1g="\'+h+\' 5N">\'+j+"</17>":"")+i)}H a},4f:6(a){H a?"<4a>"+a+"</4a>":""},4b:6(a,b){6 c(l){H(l=l?l.1V||g:g)?l+" ":""}O(K d=0,h="",g=J.V("1D",""),i=0;i<b.L;i++){K k=b[i],j;I(!(k===N||k.L===0)){j=c(k);h+=u(a.1o(d,k.P-d),j+"48")+u(k.1T,j+k.23);d=k.P+k.L+(k.75||0)}}h+=u(a.1o(d),c()+"48");H h},1H:6(a){K b="",c=["20"],d;I(J.V("2k")==R)J.1n.16=J.1n.1u=11;1l="20";J.V("2l")==R&&c.U("47");I((1u=J.V("1u"))==11)c.U("6S");c.U(J.V("1g-27"));c.U(J.V("1D"));a=a.Q(/^[ ]*[\\n]+|[\\n]*[ ]*$/g,"").Q(/\\r/g," ");b=J.V("43-22");I(J.V("42-45")==R)a=n(a,b);Y{O(K h="",g=0;g<b;g++)h+=" ";a=a.Q(/\\t/g,h)}a=a;a:{b=a=a;h=/<2R\\s*\\/?>|&1y;2R\\s*\\/?&1G;/2T;I(e.13.46==R)b=b.Q(h,"\\n");I(e.13.44==R)b=b.Q(h,"");b=b.1e("\\n");h=/^\\s*/;g=4Q;O(K i=0;i<b.L&&g>0;i++){K k=b[i];I(x(k).L!=0){k=h.X(k);I(k==N){a=a;1N a}g=1Q.4q(k[0].L,g)}}I(g>0)O(i=0;i<b.L;i++)b[i]=b[i].1o(g);a=b.1K("\\n")}I(1u)d=J.4d(a);b=J.4c(J.2J,a);b=J.4b(a,b);b=J.49(b,d);I(J.V("41-40"))b=E(b);1j 2H!="1d"&&2H.3S&&2H.3S.1C(/5s/)&&c.U("5t");H b=\'<T 1c="\'+t(J.1c)+\'" 1g="\'+c.1K(" ")+\'">\'+(J.V("16")?e.16.1H(J):"")+\'<3Z 5z="0" 5H="0" 5J="0">\'+J.4f(J.V("1t"))+"<3T><3P>"+(1u?\'<2d 1g="1u">\'+J.3Q(a)+"</2d>":"")+\'<2d 1g="17"><T 1g="3O">\'+b+"</T></2d></3P></3T></3Z></T>"},2F:6(a){I(a===N)a="";J.17=a;K b=J.3Y("T");b.3X=J.1H(a);J.V("16")&&w(p(b,".16"),"5c",e.16.2b);J.V("3V-17")&&w(p(b,".17"),"56",f);H b},2Q:6(a){J.1c=""+1Q.5d(1Q.5n()*5k).1q();e.1Y.2A[t(J.1c)]=J;J.1n=C(e.2v,a||{});I(J.V("2k")==R)J.1n.16=J.1n.1u=11},5j:6(a){a=a.Q(/^\\s+|\\s+$/g,"").Q(/\\s+/g,"|");H"\\\\b(?:"+a+")\\\\b"},5f:6(a){J.28={18:{1I:a.18,23:"1k"},1b:{1I:a.1b,23:"1k"},17:1f M("(?<18>"+a.18.1m+")(?<17>.*?)(?<1b>"+a.1b.1m+")","5o")}}};H e}();1j 2e!="1d"&&(2e.1v=1v);',62,441,'||||||function|||||||||||||||||||||||||||||||||||||return|if|this|var|length|XRegExp|null|for|index|replace|true||div|push|getParam|call|exec|else|prototype||false|lastIndex|config|arguments|RegExp|toolbar|code|left|captureNames|slice|right|id|undefined|split|new|class|addToken|indexOf|typeof|script|className|source|params|substr|apply|toString|String|line|title|gutter|SyntaxHighlighter|_xregexp|strings|lt|html|test|OUTSIDE_CLASS|match|brush|document|target|gt|getHtml|regex|global|join|style|highlight|break|concat|window|Math|isRegExp|throw|value|brushes|brushName|space|alert|vars|http|syntaxhighlighter|expandSource|size|css|case|font|Fa|name|htmlScript|dA|can|handler|gm|td|exports|color|in|href|first|discoveredBrushes|light|collapse|object|cache|getButtonHtml|trigger|pattern|getLineHtml|nbsp|numbers|parseInt|defaults|com|items|www|pad|highlighters|execute|focus|func|all|getDiv|parentNode|navigator|INSIDE_CLASS|regexList|hasFlag|Match|useScriptTags|hasNamedCapture|text|help|init|br|input|gi|Error|values|span|list|250|height|width|screen|top|500|tagName|findElements|getElementsByTagName|aboutDialog|_blank|appendChild|charAt|Array|copyAsGlobal|setFlag|highlighter_|string|attachEvent|nodeName|floor|backref|output|the|TypeError|sticky|Za|iterate|freezeTokens|scope|type|textarea|alexgorbatchev|version|margin|2010|005896|gs|regexLib|body|center|align|noBrush|require|childNodes|DTD|xhtml1|head|org|w3|url|preventDefault|container|tr|getLineNumbersHtml|isNaN|userAgent|tbody|isLineHighlighted|quick|void|innerHTML|create|table|links|auto|smart|tab|stripBrs|tabs|bloggerMode|collapsed|plain|getCodeLinesHtml|caption|getMatchesHtml|findMatches|figureOutLineNumbers|removeNestedMatches|getTitleHtml|brushNotHtmlScript|substring|createElement|Highlighter|load|HtmlScript|Brush|pre|expand|multiline|min|Can|ignoreCase|find|blur|extended|toLowerCase|aliases|addEventListener|innerText|textContent|wasn|select|createTextNode|removeChild|option|same|frame|xmlns|dtd|twice|1999|equiv|meta|htmlscript|transitional|1E3|expected|PUBLIC|DOCTYPE|on|W3C|XHTML|TR|EN|Transitional||configured|srcElement|Object|after|run|dblclick|matchChain|valueOf|constructor|default|switch|click|round|execAt|forHtmlScript|token|gimy|functions|getKeywords|1E6|escape|within|random|sgi|another|finally|supply|MSIE|ie|toUpperCase|catch|returnValue|definition|event|border|imsx|constructing|one|Infinity|from|when|Content|cellpadding|flags|cellspacing|try|xhtml|Type|spaces|2930402|hosted_button_id|lastIndexOf|donate|active|development|keep|to|xclick|_s|Xml|please|like|you|paypal|cgi|cmd|webscr|bin|highlighted|scrollbars|aspScriptTags|phpScriptTags|sort|max|scriptScriptTags|toolbar_item|_|command|command_|number|getElementById|doubleQuotedString|singleLinePerlComments|singleLineCComments|multiLineCComments|singleQuotedString|multiLineDoubleQuotedString|xmlComments|alt|multiLineSingleQuotedString|If|https|1em|000|fff|background|5em|xx|bottom|75em|Gorbatchev|large|serif|CDATA|continue|utf|charset|content|About|family|sans|Helvetica|Arial|Geneva|3em|nogutter|Copyright|syntax|close|write|2004|Alex|open|JavaScript|highlighter|July|02|replaceChild|offset|83'.split('|'),0,{}))
diff --git a/guides/assets/javascripts/turbolinks.js b/guides/assets/javascripts/turbolinks.js
new file mode 100644
index 0000000000..686283c7f0
--- /dev/null
+++ b/guides/assets/javascripts/turbolinks.js
@@ -0,0 +1,6 @@
+/*
+Turbolinks 5.1.1
+Copyright © 2018 Basecamp, LLC
+ */
+(function(){var t=this;(function(){(function(){this.Turbolinks={supported:function(){return null!=window.history.pushState&&null!=window.requestAnimationFrame&&null!=window.addEventListener}(),visit:function(t,r){return e.controller.visit(t,r)},clearCache:function(){return e.controller.clearCache()},setProgressBarDelay:function(t){return e.controller.setProgressBarDelay(t)}}}).call(this)}).call(t);var e=t.Turbolinks;(function(){(function(){var t,r,n,o=[].slice;e.copyObject=function(t){var e,r,n;r={};for(e in t)n=t[e],r[e]=n;return r},e.closest=function(e,r){return t.call(e,r)},t=function(){var t,e;return t=document.documentElement,null!=(e=t.closest)?e:function(t){var e;for(e=this;e;){if(e.nodeType===Node.ELEMENT_NODE&&r.call(e,t))return e;e=e.parentNode}}}(),e.defer=function(t){return setTimeout(t,1)},e.throttle=function(t){var e;return e=null,function(){var r;return r=1<=arguments.length?o.call(arguments,0):[],null!=e?e:e=requestAnimationFrame(function(n){return function(){return e=null,t.apply(n,r)}}(this))}},e.dispatch=function(t,e){var r,o,i,s,a,u;return a=null!=e?e:{},u=a.target,r=a.cancelable,o=a.data,i=document.createEvent("Events"),i.initEvent(t,!0,r===!0),i.data=null!=o?o:{},i.cancelable&&!n&&(s=i.preventDefault,i.preventDefault=function(){return this.defaultPrevented||Object.defineProperty(this,"defaultPrevented",{get:function(){return!0}}),s.call(this)}),(null!=u?u:document).dispatchEvent(i),i},n=function(){var t;return t=document.createEvent("Events"),t.initEvent("test",!0,!0),t.preventDefault(),t.defaultPrevented}(),e.match=function(t,e){return r.call(t,e)},r=function(){var t,e,r,n;return t=document.documentElement,null!=(e=null!=(r=null!=(n=t.matchesSelector)?n:t.webkitMatchesSelector)?r:t.msMatchesSelector)?e:t.mozMatchesSelector}(),e.uuid=function(){var t,e,r;for(r="",t=e=1;36>=e;t=++e)r+=9===t||14===t||19===t||24===t?"-":15===t?"4":20===t?(Math.floor(4*Math.random())+8).toString(16):Math.floor(15*Math.random()).toString(16);return r}}).call(this),function(){e.Location=function(){function t(t){var e,r;null==t&&(t=""),r=document.createElement("a"),r.href=t.toString(),this.absoluteURL=r.href,e=r.hash.length,2>e?this.requestURL=this.absoluteURL:(this.requestURL=this.absoluteURL.slice(0,-e),this.anchor=r.hash.slice(1))}var e,r,n,o;return t.wrap=function(t){return t instanceof this?t:new this(t)},t.prototype.getOrigin=function(){return this.absoluteURL.split("/",3).join("/")},t.prototype.getPath=function(){var t,e;return null!=(t=null!=(e=this.requestURL.match(/\/\/[^\/]*(\/[^?;]*)/))?e[1]:void 0)?t:"/"},t.prototype.getPathComponents=function(){return this.getPath().split("/").slice(1)},t.prototype.getLastPathComponent=function(){return this.getPathComponents().slice(-1)[0]},t.prototype.getExtension=function(){var t,e;return null!=(t=null!=(e=this.getLastPathComponent().match(/\.[^.]*$/))?e[0]:void 0)?t:""},t.prototype.isHTML=function(){return this.getExtension().match(/^(?:|\.(?:htm|html|xhtml))$/)},t.prototype.isPrefixedBy=function(t){var e;return e=r(t),this.isEqualTo(t)||o(this.absoluteURL,e)},t.prototype.isEqualTo=function(t){return this.absoluteURL===(null!=t?t.absoluteURL:void 0)},t.prototype.toCacheKey=function(){return this.requestURL},t.prototype.toJSON=function(){return this.absoluteURL},t.prototype.toString=function(){return this.absoluteURL},t.prototype.valueOf=function(){return this.absoluteURL},r=function(t){return e(t.getOrigin()+t.getPath())},e=function(t){return n(t,"/")?t:t+"/"},o=function(t,e){return t.slice(0,e.length)===e},n=function(t,e){return t.slice(-e.length)===e},t}()}.call(this),function(){var t=function(t,e){return function(){return t.apply(e,arguments)}};e.HttpRequest=function(){function r(r,n,o){this.delegate=r,this.requestCanceled=t(this.requestCanceled,this),this.requestTimedOut=t(this.requestTimedOut,this),this.requestFailed=t(this.requestFailed,this),this.requestLoaded=t(this.requestLoaded,this),this.requestProgressed=t(this.requestProgressed,this),this.url=e.Location.wrap(n).requestURL,this.referrer=e.Location.wrap(o).absoluteURL,this.createXHR()}return r.NETWORK_FAILURE=0,r.TIMEOUT_FAILURE=-1,r.timeout=60,r.prototype.send=function(){var t;return this.xhr&&!this.sent?(this.notifyApplicationBeforeRequestStart(),this.setProgress(0),this.xhr.send(),this.sent=!0,"function"==typeof(t=this.delegate).requestStarted?t.requestStarted():void 0):void 0},r.prototype.cancel=function(){return this.xhr&&this.sent?this.xhr.abort():void 0},r.prototype.requestProgressed=function(t){return t.lengthComputable?this.setProgress(t.loaded/t.total):void 0},r.prototype.requestLoaded=function(){return this.endRequest(function(t){return function(){var e;return 200<=(e=t.xhr.status)&&300>e?t.delegate.requestCompletedWithResponse(t.xhr.responseText,t.xhr.getResponseHeader("Turbolinks-Location")):(t.failed=!0,t.delegate.requestFailedWithStatusCode(t.xhr.status,t.xhr.responseText))}}(this))},r.prototype.requestFailed=function(){return this.endRequest(function(t){return function(){return t.failed=!0,t.delegate.requestFailedWithStatusCode(t.constructor.NETWORK_FAILURE)}}(this))},r.prototype.requestTimedOut=function(){return this.endRequest(function(t){return function(){return t.failed=!0,t.delegate.requestFailedWithStatusCode(t.constructor.TIMEOUT_FAILURE)}}(this))},r.prototype.requestCanceled=function(){return this.endRequest()},r.prototype.notifyApplicationBeforeRequestStart=function(){return e.dispatch("turbolinks:request-start",{data:{url:this.url,xhr:this.xhr}})},r.prototype.notifyApplicationAfterRequestEnd=function(){return e.dispatch("turbolinks:request-end",{data:{url:this.url,xhr:this.xhr}})},r.prototype.createXHR=function(){return this.xhr=new XMLHttpRequest,this.xhr.open("GET",this.url,!0),this.xhr.timeout=1e3*this.constructor.timeout,this.xhr.setRequestHeader("Accept","text/html, application/xhtml+xml"),this.xhr.setRequestHeader("Turbolinks-Referrer",this.referrer),this.xhr.onprogress=this.requestProgressed,this.xhr.onload=this.requestLoaded,this.xhr.onerror=this.requestFailed,this.xhr.ontimeout=this.requestTimedOut,this.xhr.onabort=this.requestCanceled},r.prototype.endRequest=function(t){return this.xhr?(this.notifyApplicationAfterRequestEnd(),null!=t&&t.call(this),this.destroy()):void 0},r.prototype.setProgress=function(t){var e;return this.progress=t,"function"==typeof(e=this.delegate).requestProgressed?e.requestProgressed(this.progress):void 0},r.prototype.destroy=function(){var t;return this.setProgress(1),"function"==typeof(t=this.delegate).requestFinished&&t.requestFinished(),this.delegate=null,this.xhr=null},r}()}.call(this),function(){var t=function(t,e){return function(){return t.apply(e,arguments)}};e.ProgressBar=function(){function e(){this.trickle=t(this.trickle,this),this.stylesheetElement=this.createStylesheetElement(),this.progressElement=this.createProgressElement()}var r;return r=300,e.defaultCSS=".turbolinks-progress-bar {\n position: fixed;\n display: block;\n top: 0;\n left: 0;\n height: 3px;\n background: #0076ff;\n z-index: 9999;\n transition: width "+r+"ms ease-out, opacity "+r/2+"ms "+r/2+"ms ease-in;\n transform: translate3d(0, 0, 0);\n}",e.prototype.show=function(){return this.visible?void 0:(this.visible=!0,this.installStylesheetElement(),this.installProgressElement(),this.startTrickling())},e.prototype.hide=function(){return this.visible&&!this.hiding?(this.hiding=!0,this.fadeProgressElement(function(t){return function(){return t.uninstallProgressElement(),t.stopTrickling(),t.visible=!1,t.hiding=!1}}(this))):void 0},e.prototype.setValue=function(t){return this.value=t,this.refresh()},e.prototype.installStylesheetElement=function(){return document.head.insertBefore(this.stylesheetElement,document.head.firstChild)},e.prototype.installProgressElement=function(){return this.progressElement.style.width=0,this.progressElement.style.opacity=1,document.documentElement.insertBefore(this.progressElement,document.body),this.refresh()},e.prototype.fadeProgressElement=function(t){return this.progressElement.style.opacity=0,setTimeout(t,1.5*r)},e.prototype.uninstallProgressElement=function(){return this.progressElement.parentNode?document.documentElement.removeChild(this.progressElement):void 0},e.prototype.startTrickling=function(){return null!=this.trickleInterval?this.trickleInterval:this.trickleInterval=setInterval(this.trickle,r)},e.prototype.stopTrickling=function(){return clearInterval(this.trickleInterval),this.trickleInterval=null},e.prototype.trickle=function(){return this.setValue(this.value+Math.random()/100)},e.prototype.refresh=function(){return requestAnimationFrame(function(t){return function(){return t.progressElement.style.width=10+90*t.value+"%"}}(this))},e.prototype.createStylesheetElement=function(){var t;return t=document.createElement("style"),t.type="text/css",t.textContent=this.constructor.defaultCSS,t},e.prototype.createProgressElement=function(){var t;return t=document.createElement("div"),t.className="turbolinks-progress-bar",t},e}()}.call(this),function(){var t=function(t,e){return function(){return t.apply(e,arguments)}};e.BrowserAdapter=function(){function r(r){this.controller=r,this.showProgressBar=t(this.showProgressBar,this),this.progressBar=new e.ProgressBar}var n,o,i;return i=e.HttpRequest,n=i.NETWORK_FAILURE,o=i.TIMEOUT_FAILURE,r.prototype.visitProposedToLocationWithAction=function(t,e){return this.controller.startVisitToLocationWithAction(t,e)},r.prototype.visitStarted=function(t){return t.issueRequest(),t.changeHistory(),t.loadCachedSnapshot()},r.prototype.visitRequestStarted=function(t){return this.progressBar.setValue(0),t.hasCachedSnapshot()||"restore"!==t.action?this.showProgressBarAfterDelay():this.showProgressBar()},r.prototype.visitRequestProgressed=function(t){return this.progressBar.setValue(t.progress)},r.prototype.visitRequestCompleted=function(t){return t.loadResponse()},r.prototype.visitRequestFailedWithStatusCode=function(t,e){switch(e){case n:case o:return this.reload();default:return t.loadResponse()}},r.prototype.visitRequestFinished=function(t){return this.hideProgressBar()},r.prototype.visitCompleted=function(t){return t.followRedirect()},r.prototype.pageInvalidated=function(){return this.reload()},r.prototype.showProgressBarAfterDelay=function(){return this.progressBarTimeout=setTimeout(this.showProgressBar,this.controller.progressBarDelay)},r.prototype.showProgressBar=function(){return this.progressBar.show()},r.prototype.hideProgressBar=function(){return this.progressBar.hide(),clearTimeout(this.progressBarTimeout)},r.prototype.reload=function(){return window.location.reload()},r}()}.call(this),function(){var t=function(t,e){return function(){return t.apply(e,arguments)}};e.History=function(){function r(e){this.delegate=e,this.onPageLoad=t(this.onPageLoad,this),this.onPopState=t(this.onPopState,this)}return r.prototype.start=function(){return this.started?void 0:(addEventListener("popstate",this.onPopState,!1),addEventListener("load",this.onPageLoad,!1),this.started=!0)},r.prototype.stop=function(){return this.started?(removeEventListener("popstate",this.onPopState,!1),removeEventListener("load",this.onPageLoad,!1),this.started=!1):void 0},r.prototype.push=function(t,r){return t=e.Location.wrap(t),this.update("push",t,r)},r.prototype.replace=function(t,r){return t=e.Location.wrap(t),this.update("replace",t,r)},r.prototype.onPopState=function(t){var r,n,o,i;return this.shouldHandlePopState()&&(i=null!=(n=t.state)?n.turbolinks:void 0)?(r=e.Location.wrap(window.location),o=i.restorationIdentifier,this.delegate.historyPoppedToLocationWithRestorationIdentifier(r,o)):void 0},r.prototype.onPageLoad=function(t){return e.defer(function(t){return function(){return t.pageLoaded=!0}}(this))},r.prototype.shouldHandlePopState=function(){return this.pageIsLoaded()},r.prototype.pageIsLoaded=function(){return this.pageLoaded||"complete"===document.readyState},r.prototype.update=function(t,e,r){var n;return n={turbolinks:{restorationIdentifier:r}},history[t+"State"](n,null,e)},r}()}.call(this),function(){e.Snapshot=function(){function t(t){var e,r;r=t.head,e=t.body,this.head=null!=r?r:document.createElement("head"),this.body=null!=e?e:document.createElement("body")}return t.wrap=function(t){return t instanceof this?t:this.fromHTML(t)},t.fromHTML=function(t){var e;return e=document.createElement("html"),e.innerHTML=t,this.fromElement(e)},t.fromElement=function(t){return new this({head:t.querySelector("head"),body:t.querySelector("body")})},t.prototype.clone=function(){return new t({head:this.head.cloneNode(!0),body:this.body.cloneNode(!0)})},t.prototype.getRootLocation=function(){var t,r;return r=null!=(t=this.getSetting("root"))?t:"/",new e.Location(r)},t.prototype.getCacheControlValue=function(){return this.getSetting("cache-control")},t.prototype.getElementForAnchor=function(t){try{return this.body.querySelector("[id='"+t+"'], a[name='"+t+"']")}catch(e){}},t.prototype.hasAnchor=function(t){return null!=this.getElementForAnchor(t)},t.prototype.isPreviewable=function(){return"no-preview"!==this.getCacheControlValue()},t.prototype.isCacheable=function(){return"no-cache"!==this.getCacheControlValue()},t.prototype.isVisitable=function(){return"reload"!==this.getSetting("visit-control")},t.prototype.getSetting=function(t){var e,r;return r=this.head.querySelectorAll("meta[name='turbolinks-"+t+"']"),e=r[r.length-1],null!=e?e.getAttribute("content"):void 0},t}()}.call(this),function(){var t=[].slice;e.Renderer=function(){function e(){}var r;return e.render=function(){var e,r,n,o;return n=arguments[0],r=arguments[1],e=3<=arguments.length?t.call(arguments,2):[],o=function(t,e,r){r.prototype=t.prototype;var n=new r,o=t.apply(n,e);return Object(o)===o?o:n}(this,e,function(){}),o.delegate=n,o.render(r),o},e.prototype.renderView=function(t){return this.delegate.viewWillRender(this.newBody),t(),this.delegate.viewRendered(this.newBody)},e.prototype.invalidateView=function(){return this.delegate.viewInvalidated()},e.prototype.createScriptElement=function(t){var e;return"false"===t.getAttribute("data-turbolinks-eval")?t:(e=document.createElement("script"),e.textContent=t.textContent,e.async=!1,r(e,t),e)},r=function(t,e){var r,n,o,i,s,a,u;for(i=e.attributes,a=[],r=0,n=i.length;n>r;r++)s=i[r],o=s.name,u=s.value,a.push(t.setAttribute(o,u));return a},e}()}.call(this),function(){e.HeadDetails=function(){function t(t){var e,r,i,s,a,u,l;for(this.element=t,this.elements={},l=this.element.childNodes,s=0,u=l.length;u>s;s++)i=l[s],i.nodeType===Node.ELEMENT_NODE&&(a=i.outerHTML,r=null!=(e=this.elements)[a]?e[a]:e[a]={type:o(i),tracked:n(i),elements:[]},r.elements.push(i))}var e,r,n,o;return t.prototype.hasElementWithKey=function(t){return t in this.elements},t.prototype.getTrackedElementSignature=function(){var t,e;return function(){var r,n;r=this.elements,n=[];for(t in r)e=r[t].tracked,e&&n.push(t);return n}.call(this).join("")},t.prototype.getScriptElementsNotInDetails=function(t){return this.getElementsMatchingTypeNotInDetails("script",t)},t.prototype.getStylesheetElementsNotInDetails=function(t){return this.getElementsMatchingTypeNotInDetails("stylesheet",t)},t.prototype.getElementsMatchingTypeNotInDetails=function(t,e){var r,n,o,i,s,a;o=this.elements,s=[];for(n in o)i=o[n],a=i.type,r=i.elements,a!==t||e.hasElementWithKey(n)||s.push(r[0]);return s},t.prototype.getProvisionalElements=function(){var t,e,r,n,o,i,s;r=[],n=this.elements;for(e in n)o=n[e],s=o.type,i=o.tracked,t=o.elements,null!=s||i?t.length>1&&r.push.apply(r,t.slice(1)):r.push.apply(r,t);return r},o=function(t){return e(t)?"script":r(t)?"stylesheet":void 0},n=function(t){return"reload"===t.getAttribute("data-turbolinks-track")},e=function(t){var e;return e=t.tagName.toLowerCase(),"script"===e},r=function(t){var e;return e=t.tagName.toLowerCase(),"style"===e||"link"===e&&"stylesheet"===t.getAttribute("rel")},t}()}.call(this),function(){var t=function(t,e){function n(){this.constructor=t}for(var o in e)r.call(e,o)&&(t[o]=e[o]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},r={}.hasOwnProperty;e.SnapshotRenderer=function(r){function n(t,r,n){this.currentSnapshot=t,this.newSnapshot=r,this.isPreview=n,this.currentHeadDetails=new e.HeadDetails(this.currentSnapshot.head),this.newHeadDetails=new e.HeadDetails(this.newSnapshot.head),this.newBody=this.newSnapshot.body}return t(n,r),n.prototype.render=function(t){return this.shouldRender()?(this.mergeHead(),this.renderView(function(e){return function(){return e.replaceBody(),e.isPreview||e.focusFirstAutofocusableElement(),t()}}(this))):this.invalidateView()},n.prototype.mergeHead=function(){return this.copyNewHeadStylesheetElements(),this.copyNewHeadScriptElements(),this.removeCurrentHeadProvisionalElements(),this.copyNewHeadProvisionalElements()},n.prototype.replaceBody=function(){return this.activateBodyScriptElements(),this.importBodyPermanentElements(),this.assignNewBody()},n.prototype.shouldRender=function(){return this.newSnapshot.isVisitable()&&this.trackedElementsAreIdentical()},n.prototype.trackedElementsAreIdentical=function(){return this.currentHeadDetails.getTrackedElementSignature()===this.newHeadDetails.getTrackedElementSignature()},n.prototype.copyNewHeadStylesheetElements=function(){var t,e,r,n,o;for(n=this.getNewHeadStylesheetElements(),o=[],e=0,r=n.length;r>e;e++)t=n[e],o.push(document.head.appendChild(t));return o},n.prototype.copyNewHeadScriptElements=function(){var t,e,r,n,o;for(n=this.getNewHeadScriptElements(),o=[],e=0,r=n.length;r>e;e++)t=n[e],o.push(document.head.appendChild(this.createScriptElement(t)));return o},n.prototype.removeCurrentHeadProvisionalElements=function(){var t,e,r,n,o;for(n=this.getCurrentHeadProvisionalElements(),o=[],e=0,r=n.length;r>e;e++)t=n[e],o.push(document.head.removeChild(t));return o},n.prototype.copyNewHeadProvisionalElements=function(){var t,e,r,n,o;for(n=this.getNewHeadProvisionalElements(),o=[],e=0,r=n.length;r>e;e++)t=n[e],o.push(document.head.appendChild(t));return o},n.prototype.importBodyPermanentElements=function(){var t,e,r,n,o,i;for(n=this.getNewBodyPermanentElements(),i=[],e=0,r=n.length;r>e;e++)o=n[e],(t=this.findCurrentBodyPermanentElement(o))?i.push(o.parentNode.replaceChild(t,o)):i.push(void 0);return i},n.prototype.activateBodyScriptElements=function(){var t,e,r,n,o,i;for(n=this.getNewBodyScriptElements(),i=[],e=0,r=n.length;r>e;e++)o=n[e],t=this.createScriptElement(o),i.push(o.parentNode.replaceChild(t,o));return i},n.prototype.assignNewBody=function(){return document.body=this.newBody},n.prototype.focusFirstAutofocusableElement=function(){var t;return null!=(t=this.findFirstAutofocusableElement())?t.focus():void 0},n.prototype.getNewHeadStylesheetElements=function(){return this.newHeadDetails.getStylesheetElementsNotInDetails(this.currentHeadDetails)},n.prototype.getNewHeadScriptElements=function(){return this.newHeadDetails.getScriptElementsNotInDetails(this.currentHeadDetails)},n.prototype.getCurrentHeadProvisionalElements=function(){return this.currentHeadDetails.getProvisionalElements()},n.prototype.getNewHeadProvisionalElements=function(){return this.newHeadDetails.getProvisionalElements()},n.prototype.getNewBodyPermanentElements=function(){return this.newBody.querySelectorAll("[id][data-turbolinks-permanent]")},n.prototype.findCurrentBodyPermanentElement=function(t){return document.body.querySelector("#"+t.id+"[data-turbolinks-permanent]")},n.prototype.getNewBodyScriptElements=function(){return this.newBody.querySelectorAll("script")},n.prototype.findFirstAutofocusableElement=function(){return document.body.querySelector("[autofocus]")},n}(e.Renderer)}.call(this),function(){var t=function(t,e){function n(){this.constructor=t}for(var o in e)r.call(e,o)&&(t[o]=e[o]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},r={}.hasOwnProperty;e.ErrorRenderer=function(e){function r(t){this.html=t}return t(r,e),r.prototype.render=function(t){return this.renderView(function(e){return function(){return e.replaceDocumentHTML(),e.activateBodyScriptElements(),t()}}(this))},r.prototype.replaceDocumentHTML=function(){return document.documentElement.innerHTML=this.html},r.prototype.activateBodyScriptElements=function(){var t,e,r,n,o,i;for(n=this.getScriptElements(),i=[],e=0,r=n.length;r>e;e++)o=n[e],t=this.createScriptElement(o),i.push(o.parentNode.replaceChild(t,o));return i},r.prototype.getScriptElements=function(){return document.documentElement.querySelectorAll("script")},r}(e.Renderer)}.call(this),function(){e.View=function(){function t(t){this.delegate=t,this.element=document.documentElement}return t.prototype.getRootLocation=function(){return this.getSnapshot().getRootLocation()},t.prototype.getElementForAnchor=function(t){return this.getSnapshot().getElementForAnchor(t)},t.prototype.getSnapshot=function(){return e.Snapshot.fromElement(this.element)},t.prototype.render=function(t,e){var r,n,o;return o=t.snapshot,r=t.error,n=t.isPreview,this.markAsPreview(n),null!=o?this.renderSnapshot(o,n,e):this.renderError(r,e)},t.prototype.markAsPreview=function(t){return t?this.element.setAttribute("data-turbolinks-preview",""):this.element.removeAttribute("data-turbolinks-preview")},t.prototype.renderSnapshot=function(t,r,n){return e.SnapshotRenderer.render(this.delegate,n,this.getSnapshot(),e.Snapshot.wrap(t),r)},t.prototype.renderError=function(t,r){return e.ErrorRenderer.render(this.delegate,r,t)},t}()}.call(this),function(){var t=function(t,e){return function(){return t.apply(e,arguments)}};e.ScrollManager=function(){function r(r){this.delegate=r,this.onScroll=t(this.onScroll,this),this.onScroll=e.throttle(this.onScroll)}return r.prototype.start=function(){return this.started?void 0:(addEventListener("scroll",this.onScroll,!1),this.onScroll(),this.started=!0)},r.prototype.stop=function(){return this.started?(removeEventListener("scroll",this.onScroll,!1),this.started=!1):void 0},r.prototype.scrollToElement=function(t){return t.scrollIntoView()},r.prototype.scrollToPosition=function(t){var e,r;return e=t.x,r=t.y,window.scrollTo(e,r)},r.prototype.onScroll=function(t){return this.updatePosition({x:window.pageXOffset,y:window.pageYOffset})},r.prototype.updatePosition=function(t){var e;return this.position=t,null!=(e=this.delegate)?e.scrollPositionChanged(this.position):void 0},r}()}.call(this),function(){e.SnapshotCache=function(){function t(t){this.size=t,this.keys=[],this.snapshots={}}var r;return t.prototype.has=function(t){var e;return e=r(t),e in this.snapshots},t.prototype.get=function(t){var e;if(this.has(t))return e=this.read(t),this.touch(t),e},t.prototype.put=function(t,e){return this.write(t,e),this.touch(t),e},t.prototype.read=function(t){var e;return e=r(t),this.snapshots[e]},t.prototype.write=function(t,e){var n;return n=r(t),this.snapshots[n]=e},t.prototype.touch=function(t){var e,n;return n=r(t),e=this.keys.indexOf(n),e>-1&&this.keys.splice(e,1),this.keys.unshift(n),this.trim()},t.prototype.trim=function(){var t,e,r,n,o;for(n=this.keys.splice(this.size),o=[],t=0,r=n.length;r>t;t++)e=n[t],o.push(delete this.snapshots[e]);return o},r=function(t){return e.Location.wrap(t).toCacheKey()},t}()}.call(this),function(){var t=function(t,e){return function(){return t.apply(e,arguments)}};e.Visit=function(){function r(r,n,o){this.controller=r,this.action=o,this.performScroll=t(this.performScroll,this),this.identifier=e.uuid(),this.location=e.Location.wrap(n),this.adapter=this.controller.adapter,this.state="initialized",this.timingMetrics={}}var n;return r.prototype.start=function(){return"initialized"===this.state?(this.recordTimingMetric("visitStart"),this.state="started",this.adapter.visitStarted(this)):void 0},r.prototype.cancel=function(){var t;return"started"===this.state?(null!=(t=this.request)&&t.cancel(),this.cancelRender(),this.state="canceled"):void 0},r.prototype.complete=function(){var t;return"started"===this.state?(this.recordTimingMetric("visitEnd"),this.state="completed","function"==typeof(t=this.adapter).visitCompleted&&t.visitCompleted(this),this.controller.visitCompleted(this)):void 0},r.prototype.fail=function(){var t;return"started"===this.state?(this.state="failed","function"==typeof(t=this.adapter).visitFailed?t.visitFailed(this):void 0):void 0},r.prototype.changeHistory=function(){var t,e;return this.historyChanged?void 0:(t=this.location.isEqualTo(this.referrer)?"replace":this.action,e=n(t),this.controller[e](this.location,this.restorationIdentifier),this.historyChanged=!0)},r.prototype.issueRequest=function(){return this.shouldIssueRequest()&&null==this.request?(this.progress=0,this.request=new e.HttpRequest(this,this.location,this.referrer),this.request.send()):void 0},r.prototype.getCachedSnapshot=function(){var t;return!(t=this.controller.getCachedSnapshotForLocation(this.location))||null!=this.location.anchor&&!t.hasAnchor(this.location.anchor)||"restore"!==this.action&&!t.isPreviewable()?void 0:t},r.prototype.hasCachedSnapshot=function(){return null!=this.getCachedSnapshot()},r.prototype.loadCachedSnapshot=function(){var t,e;return(e=this.getCachedSnapshot())?(t=this.shouldIssueRequest(),this.render(function(){var r;return this.cacheSnapshot(),this.controller.render({snapshot:e,isPreview:t},this.performScroll),"function"==typeof(r=this.adapter).visitRendered&&r.visitRendered(this),t?void 0:this.complete()})):void 0},r.prototype.loadResponse=function(){return null!=this.response?this.render(function(){var t,e;return this.cacheSnapshot(),this.request.failed?(this.controller.render({error:this.response},this.performScroll),"function"==typeof(t=this.adapter).visitRendered&&t.visitRendered(this),this.fail()):(this.controller.render({snapshot:this.response},this.performScroll),"function"==typeof(e=this.adapter).visitRendered&&e.visitRendered(this),this.complete())}):void 0},r.prototype.followRedirect=function(){return this.redirectedToLocation&&!this.followedRedirect?(this.location=this.redirectedToLocation,this.controller.replaceHistoryWithLocationAndRestorationIdentifier(this.redirectedToLocation,this.restorationIdentifier),this.followedRedirect=!0):void 0},r.prototype.requestStarted=function(){var t;return this.recordTimingMetric("requestStart"),"function"==typeof(t=this.adapter).visitRequestStarted?t.visitRequestStarted(this):void 0},r.prototype.requestProgressed=function(t){var e;return this.progress=t,"function"==typeof(e=this.adapter).visitRequestProgressed?e.visitRequestProgressed(this):void 0},r.prototype.requestCompletedWithResponse=function(t,r){return this.response=t,null!=r&&(this.redirectedToLocation=e.Location.wrap(r)),this.adapter.visitRequestCompleted(this)},r.prototype.requestFailedWithStatusCode=function(t,e){return this.response=e,this.adapter.visitRequestFailedWithStatusCode(this,t)},r.prototype.requestFinished=function(){var t;return this.recordTimingMetric("requestEnd"),"function"==typeof(t=this.adapter).visitRequestFinished?t.visitRequestFinished(this):void 0},r.prototype.performScroll=function(){return this.scrolled?void 0:("restore"===this.action?this.scrollToRestoredPosition()||this.scrollToTop():this.scrollToAnchor()||this.scrollToTop(),this.scrolled=!0)},r.prototype.scrollToRestoredPosition=function(){var t,e;return t=null!=(e=this.restorationData)?e.scrollPosition:void 0,null!=t?(this.controller.scrollToPosition(t),!0):void 0},r.prototype.scrollToAnchor=function(){return null!=this.location.anchor?(this.controller.scrollToAnchor(this.location.anchor),!0):void 0},r.prototype.scrollToTop=function(){return this.controller.scrollToPosition({x:0,y:0})},r.prototype.recordTimingMetric=function(t){var e;return null!=(e=this.timingMetrics)[t]?e[t]:e[t]=(new Date).getTime()},r.prototype.getTimingMetrics=function(){return e.copyObject(this.timingMetrics)},n=function(t){switch(t){case"replace":return"replaceHistoryWithLocationAndRestorationIdentifier";case"advance":case"restore":return"pushHistoryWithLocationAndRestorationIdentifier"}},r.prototype.shouldIssueRequest=function(){return"restore"===this.action?!this.hasCachedSnapshot():!0},r.prototype.cacheSnapshot=function(){return this.snapshotCached?void 0:(this.controller.cacheSnapshot(),this.snapshotCached=!0)},r.prototype.render=function(t){return this.cancelRender(),this.frame=requestAnimationFrame(function(e){return function(){return e.frame=null,t.call(e)}}(this))},r.prototype.cancelRender=function(){return this.frame?cancelAnimationFrame(this.frame):void 0},r}()}.call(this),function(){var t=function(t,e){return function(){return t.apply(e,arguments)}};e.Controller=function(){function r(){this.clickBubbled=t(this.clickBubbled,this),this.clickCaptured=t(this.clickCaptured,this),this.pageLoaded=t(this.pageLoaded,this),this.history=new e.History(this),this.view=new e.View(this),this.scrollManager=new e.ScrollManager(this),this.restorationData={},this.clearCache(),this.setProgressBarDelay(500)}return r.prototype.start=function(){return e.supported&&!this.started?(addEventListener("click",this.clickCaptured,!0),addEventListener("DOMContentLoaded",this.pageLoaded,!1),this.scrollManager.start(),this.startHistory(),this.started=!0,this.enabled=!0):void 0},r.prototype.disable=function(){return this.enabled=!1},r.prototype.stop=function(){return this.started?(removeEventListener("click",this.clickCaptured,!0),removeEventListener("DOMContentLoaded",this.pageLoaded,!1),this.scrollManager.stop(),this.stopHistory(),this.started=!1):void 0},r.prototype.clearCache=function(){return this.cache=new e.SnapshotCache(10)},r.prototype.visit=function(t,r){var n,o;return null==r&&(r={}),t=e.Location.wrap(t),this.applicationAllowsVisitingLocation(t)?this.locationIsVisitable(t)?(n=null!=(o=r.action)?o:"advance",this.adapter.visitProposedToLocationWithAction(t,n)):window.location=t:void 0},r.prototype.startVisitToLocationWithAction=function(t,r,n){var o;return e.supported?(o=this.getRestorationDataForIdentifier(n),this.startVisit(t,r,{restorationData:o})):window.location=t},r.prototype.setProgressBarDelay=function(t){return this.progressBarDelay=t},r.prototype.startHistory=function(){return this.location=e.Location.wrap(window.location),this.restorationIdentifier=e.uuid(),this.history.start(),this.history.replace(this.location,this.restorationIdentifier)},r.prototype.stopHistory=function(){return this.history.stop()},r.prototype.pushHistoryWithLocationAndRestorationIdentifier=function(t,r){return this.restorationIdentifier=r,this.location=e.Location.wrap(t),this.history.push(this.location,this.restorationIdentifier)},r.prototype.replaceHistoryWithLocationAndRestorationIdentifier=function(t,r){return this.restorationIdentifier=r,this.location=e.Location.wrap(t),this.history.replace(this.location,this.restorationIdentifier)},r.prototype.historyPoppedToLocationWithRestorationIdentifier=function(t,r){var n;return this.restorationIdentifier=r,this.enabled?(n=this.getRestorationDataForIdentifier(this.restorationIdentifier),this.startVisit(t,"restore",{restorationIdentifier:this.restorationIdentifier,restorationData:n,historyChanged:!0}),this.location=e.Location.wrap(t)):this.adapter.pageInvalidated()},r.prototype.getCachedSnapshotForLocation=function(t){var e;return e=this.cache.get(t),e?e.clone():void 0},r.prototype.shouldCacheSnapshot=function(){return this.view.getSnapshot().isCacheable()},r.prototype.cacheSnapshot=function(){var t;return this.shouldCacheSnapshot()?(this.notifyApplicationBeforeCachingSnapshot(),t=this.view.getSnapshot(),this.cache.put(this.lastRenderedLocation,t.clone())):void 0},r.prototype.scrollToAnchor=function(t){var e;return(e=this.view.getElementForAnchor(t))?this.scrollToElement(e):this.scrollToPosition({x:0,y:0})},r.prototype.scrollToElement=function(t){return this.scrollManager.scrollToElement(t)},r.prototype.scrollToPosition=function(t){return this.scrollManager.scrollToPosition(t)},r.prototype.scrollPositionChanged=function(t){var e;return e=this.getCurrentRestorationData(),e.scrollPosition=t},r.prototype.render=function(t,e){return this.view.render(t,e)},r.prototype.viewInvalidated=function(){return this.adapter.pageInvalidated()},r.prototype.viewWillRender=function(t){return this.notifyApplicationBeforeRender(t)},r.prototype.viewRendered=function(){return this.lastRenderedLocation=this.currentVisit.location,this.notifyApplicationAfterRender()},r.prototype.pageLoaded=function(){return this.lastRenderedLocation=this.location,this.notifyApplicationAfterPageLoad()},r.prototype.clickCaptured=function(){return removeEventListener("click",this.clickBubbled,!1),addEventListener("click",this.clickBubbled,!1)},r.prototype.clickBubbled=function(t){var e,r,n;return this.enabled&&this.clickEventIsSignificant(t)&&(r=this.getVisitableLinkForNode(t.target))&&(n=this.getVisitableLocationForLink(r))&&this.applicationAllowsFollowingLinkToLocation(r,n)?(t.preventDefault(),e=this.getActionForLink(r),
+this.visit(n,{action:e})):void 0},r.prototype.applicationAllowsFollowingLinkToLocation=function(t,e){var r;return r=this.notifyApplicationAfterClickingLinkToLocation(t,e),!r.defaultPrevented},r.prototype.applicationAllowsVisitingLocation=function(t){var e;return e=this.notifyApplicationBeforeVisitingLocation(t),!e.defaultPrevented},r.prototype.notifyApplicationAfterClickingLinkToLocation=function(t,r){return e.dispatch("turbolinks:click",{target:t,data:{url:r.absoluteURL},cancelable:!0})},r.prototype.notifyApplicationBeforeVisitingLocation=function(t){return e.dispatch("turbolinks:before-visit",{data:{url:t.absoluteURL},cancelable:!0})},r.prototype.notifyApplicationAfterVisitingLocation=function(t){return e.dispatch("turbolinks:visit",{data:{url:t.absoluteURL}})},r.prototype.notifyApplicationBeforeCachingSnapshot=function(){return e.dispatch("turbolinks:before-cache")},r.prototype.notifyApplicationBeforeRender=function(t){return e.dispatch("turbolinks:before-render",{data:{newBody:t}})},r.prototype.notifyApplicationAfterRender=function(){return e.dispatch("turbolinks:render")},r.prototype.notifyApplicationAfterPageLoad=function(t){return null==t&&(t={}),e.dispatch("turbolinks:load",{data:{url:this.location.absoluteURL,timing:t}})},r.prototype.startVisit=function(t,e,r){var n;return null!=(n=this.currentVisit)&&n.cancel(),this.currentVisit=this.createVisit(t,e,r),this.currentVisit.start(),this.notifyApplicationAfterVisitingLocation(t)},r.prototype.createVisit=function(t,r,n){var o,i,s,a,u;return i=null!=n?n:{},a=i.restorationIdentifier,s=i.restorationData,o=i.historyChanged,u=new e.Visit(this,t,r),u.restorationIdentifier=null!=a?a:e.uuid(),u.restorationData=e.copyObject(s),u.historyChanged=o,u.referrer=this.location,u},r.prototype.visitCompleted=function(t){return this.notifyApplicationAfterPageLoad(t.getTimingMetrics())},r.prototype.clickEventIsSignificant=function(t){return!(t.defaultPrevented||t.target.isContentEditable||t.which>1||t.altKey||t.ctrlKey||t.metaKey||t.shiftKey)},r.prototype.getVisitableLinkForNode=function(t){return this.nodeIsVisitable(t)?e.closest(t,"a[href]:not([target]):not([download])"):void 0},r.prototype.getVisitableLocationForLink=function(t){var r;return r=new e.Location(t.getAttribute("href")),this.locationIsVisitable(r)?r:void 0},r.prototype.getActionForLink=function(t){var e;return null!=(e=t.getAttribute("data-turbolinks-action"))?e:"advance"},r.prototype.nodeIsVisitable=function(t){var r;return(r=e.closest(t,"[data-turbolinks]"))?"false"!==r.getAttribute("data-turbolinks"):!0},r.prototype.locationIsVisitable=function(t){return t.isPrefixedBy(this.view.getRootLocation())&&t.isHTML()},r.prototype.getCurrentRestorationData=function(){return this.getRestorationDataForIdentifier(this.restorationIdentifier)},r.prototype.getRestorationDataForIdentifier=function(t){var e;return null!=(e=this.restorationData)[t]?e[t]:e[t]={}},r}()}.call(this),function(){!function(){var t,e;if((t=e=document.currentScript)&&!e.hasAttribute("data-turbolinks-suppress-warning"))for(;t=t.parentNode;)if(t===document.body)return console.warn("You are loading Turbolinks from a <script> element inside the <body> element. This is probably not what you meant to do!\n\nLoad your application\u2019s JavaScript bundle inside the <head> element instead. <script> elements in <body> are evaluated with each page change.\n\nFor more information, see: https://github.com/turbolinks/turbolinks#working-with-script-elements\n\n\u2014\u2014\nSuppress this warning by adding a `data-turbolinks-suppress-warning` attribute to: %s",e.outerHTML)}()}.call(this),function(){var t,r,n;e.start=function(){return r()?(null==e.controller&&(e.controller=t()),e.controller.start()):void 0},r=function(){return null==window.Turbolinks&&(window.Turbolinks=e),n()},t=function(){var t;return t=new e.Controller,t.adapter=new e.BrowserAdapter(t),t},n=function(){return window.Turbolinks===e},n()&&e.start()}.call(this)}).call(this),"object"==typeof module&&module.exports?module.exports=e:"function"==typeof define&&define.amd&&define(e)}).call(this);
diff --git a/guides/assets/stylesheets/main.css b/guides/assets/stylesheets/main.css
index ed558e4793..bdc3e21977 100644
--- a/guides/assets/stylesheets/main.css
+++ b/guides/assets/stylesheets/main.css
@@ -16,7 +16,6 @@
.large {font-size: larger;}
.hide {display: none;}
-li ul, li ol { margin:0 1.5em; }
ul, ol { margin: 0 1.5em 1.5em 1.5em; }
ul { list-style-type: disc; }
@@ -34,6 +33,13 @@ pre, code {
overflow: auto;
color: #222;
}
+
+p code {
+ background: #eee;
+ border-radius: 2px;
+ padding: 1px 3px;
+}
+
pre, tt, code {
white-space: pre-wrap; /* css-3 */
white-space: -moz-pre-wrap !important; /* Mozilla, since 1999 */
@@ -71,7 +77,7 @@ table {
}
table th, table td {
- padding: 0.25em 1em;
+ padding: 9px 10px;
border: 1px solid #CCC;
border-collapse: collapse;
}
@@ -80,7 +86,6 @@ table th {
border-bottom: 2px solid #CCC;
background: #EEE;
font-weight: bold;
- padding: 0.5em 1em;
}
img {
@@ -266,8 +271,6 @@ body {
}
}
-#extraCol {display: none;}
-
#footer {
padding: 2em 0;
background: #222 url(../images/footer_tile.gif) repeat-x;
@@ -280,8 +283,12 @@ body {
#header .wrapper, #topNav .wrapper, #feature .wrapper {padding-left: 1em; max-width: 960px;}
#feature .wrapper {max-width: 640px; padding-right: 23em; position: relative; z-index: 0;}
+@media screen and (max-width: 960px) {
+ #container .wrapper { padding-right: 23em; }
+}
+
@media screen and (max-width: 800px) {
- #feature .wrapper { padding-right: 0; }
+ #feature .wrapper, #container .wrapper { padding-right: 0; }
}
/* Links
@@ -295,6 +302,10 @@ a, a:link, a:visited {
#mainCol a, #subCol a, #feature a {color: #980905;}
#mainCol a code, #subCol a code, #feature a code {color: #980905;}
+#mainCol a.anchorlink, #mainCol a.anchorlink code {color: #333;}
+#mainCol a.anchorlink { text-decoration: none; }
+#mainCol a.anchorlink:hover { text-decoration: underline; }
+
/* Navigation
--------------------------------------- */
@@ -390,14 +401,10 @@ a, a:link, a:visited {
}
#guides {
- width: 27em;
+ width: 37em;
display: block;
background: #980905;
border-radius: 1em;
- -webkit-border-radius: 1em;
- -moz-border-radius: 1em;
- -webkit-box-shadow: 0.25em 0.25em 1em rgba(0,0,0,0.25);
- -moz-box-shadow: rgba(0,0,0,0.25) 0.25em 0.25em 1em;
color: #f1938c;
padding: 1.5em 2em;
position: absolute;
@@ -407,17 +414,48 @@ a, a:link, a:visited {
padding-top: 2em;
}
-#guides dt, #guides dd {
+#guides.visible {
+ display: block !important;
+}
+
+.guides-section dt, .guides-section dd {
font-weight: normal;
font-size: 0.722em;
margin: 0;
padding: 0;
}
-#guides dt {padding:0; margin: 0.5em 0 0;}
-#guides a {color: #FFF; background: none !important; text-decoration: none;}
-#guides a:hover {text-decoration: underline;}
-#guides .L, #guides .R {float: left; width: 50%; margin: 0; padding: 0;}
-#guides .R {float: right;}
+.guides-section dt {
+ margin: 0.5em 0 0;
+ padding:0;
+}
+#guides a {
+ background: none !important;
+ color: #FFF;
+ text-decoration: none;
+}
+#guides a:hover {
+ text-decoration: underline;
+}
+.guides-section-container {
+ display: flex;
+ flex-direction: column;
+ flex-wrap: wrap;
+ width: 100%;
+ max-height: 35em;
+}
+
+.guides-section {
+ min-width: 5em;
+ margin: 0 2em 0.5em 0;
+ flex: auto;
+ max-width: 12em;
+}
+
+.guides-section dd {
+ line-height: 1.3;
+ margin-bottom: 0.5em;
+}
+
#guides hr {
display: block;
border: none;
@@ -500,13 +538,26 @@ h6 {
#header h1 {
float: left;
- background: url(../images/rails_guides_logo.gif) no-repeat;
+ background: url(../images/rails_guides_logo_1x.png) no-repeat;
width: 297px;
text-indent: -9999em;
margin: 0;
padding: 0;
}
+@media
+only screen and (-webkit-min-device-pixel-ratio: 2),
+only screen and ( min--moz-device-pixel-ratio: 2),
+only screen and ( -o-min-device-pixel-ratio: 2/1),
+only screen and ( min-device-pixel-ratio: 2),
+only screen and ( min-resolution: 192dpi),
+only screen and ( min-resolution: 2dppx) {
+ #header h1 {
+ background: url(../images/rails_guides_logo_2x.png) no-repeat;
+ background-size: 160%;
+ }
+}
+
@media screen and (max-width: 480px) {
#header h1 {
float: none;
@@ -552,8 +603,6 @@ h6 {
font-size: 1.2857em;
padding: 0.125em 0 0.25em 0;
margin-bottom: 0;
- /*background: url(../images/book_icon.gif) no-repeat left top;
- padding: 0.125em 0 0.25em 28px;*/
}
@media screen and (max-width: 480px) {
@@ -602,6 +651,8 @@ h6 {
font-weight: normal;
}
+#subCol li ul, li ol { margin:0 1.5em; }
+
div.code_container {
background: #EEE url(../images/tab_grey.gif) no-repeat left top;
padding: 0.25em 1em 0.5em 48px;
@@ -628,7 +679,9 @@ div.code_container {
margin: 0.25em 0 1.5em 0;
}
-.note code, .info code, .todo code {border:none; background: none; padding: 0;}
+.note code, .info code, .todo code {
+ background: #fff;
+}
#mainCol ul li {
list-style:none;
@@ -660,10 +713,8 @@ div.code_container {
visibility: hidden;
}
-.clearfix {display: inline-block;}
* html .clearfix {height: 1%;}
.clearfix {display: block;}
-.clear { clear:both; }
/* Same bottom margin for special boxes than for regular paragraphs, this way
intermediate whitespace looks uniform. */
@@ -691,9 +742,6 @@ div.important p, div.caution p, div.warning p, div.note p, div.info p {
/* Foundation v2.1.4 http://foundation.zurb.com */
/* Artfully masterminded by ZURB */
-table th { font-weight: bold; }
-table td, table th { padding: 9px 10px; text-align: left; }
-
/* Mobile */
@media only screen and (max-width: 767px) {
table.responsive { margin-bottom: 0; }
diff --git a/guides/assets/stylesheets/main.rtl.css b/guides/assets/stylesheets/main.rtl.css
new file mode 100644
index 0000000000..ea31d6017c
--- /dev/null
+++ b/guides/assets/stylesheets/main.rtl.css
@@ -0,0 +1,762 @@
+/* Guides.rubyonrails.org */
+/* Main.css */
+/* Created January 30, 2009 */
+/* Modified February 8, 2009
+--------------------------------------- */
+
+/* General
+--------------------------------------- */
+
+.right {float: right; margin-left: 1em;}
+.left {float: left; margin-right: 1em;}
+@media screen and (max-width: 480px) {
+ .right, .left { float: none; }
+}
+.small {font-size: smaller;}
+.large {font-size: larger;}
+.hide {display: none;}
+
+ul, ol { margin: 0 1.5em 1.5em 1.5em; }
+
+ul { list-style-type: disc; }
+ol { list-style-type: decimal; }
+
+dl { margin: 0 0 1.5em 0; }
+dl dt { font-weight: bold; }
+dd { margin-right: 1.5em;}
+
+pre, code {
+ font-size: 1em;
+ font-family: "Anonymous Pro", "Inconsolata", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace;
+ line-height: 1.5;
+ margin: 1.5em 0;
+ overflow: auto;
+ color: #222;
+}
+
+p code {
+ background: #eee;
+ border-radius: 2px;
+ padding: 1px 3px;
+}
+
+pre, tt, code {
+ white-space: pre-wrap; /* css-3 */
+ white-space: -moz-pre-wrap !important; /* Mozilla, since 1999 */
+ white-space: -pre-wrap; /* Opera 4-6 */
+ white-space: -o-pre-wrap; /* Opera 7 */
+ word-wrap: break-word; /* Internet Explorer 5.5+ */
+}
+
+abbr, acronym { border-bottom: 1px dotted #666; }
+address { margin: 0 0 1.5em; font-style: italic; }
+del { color:#666; }
+
+blockquote { margin: 1.5em; color: #666; font-style: italic; }
+strong { font-weight: bold; }
+em, dfn { font-style: italic; }
+dfn { font-weight: bold; }
+sup, sub { line-height: 0; }
+p {margin: 0 0 1.5em;}
+
+label { font-weight: bold; }
+fieldset { padding:1.4em; margin: 0 0 1.5em 0; border: 1px solid #ccc; }
+legend { font-weight: bold; font-size:1.2em; }
+
+input.text, input.title,
+textarea, select {
+ margin:0.5em 0;
+ border:1px solid #bbb;
+}
+
+table {
+ margin: 0 0 1.5em;
+ border: 2px solid #CCC;
+ background: #FFF;
+ border-collapse: collapse;
+}
+
+table th, table td {
+ padding: 9px 10px;
+ border: 1px solid #CCC;
+ border-collapse: collapse;
+}
+
+table th {
+ border-bottom: 2px solid #CCC;
+ background: #EEE;
+ font-weight: bold;
+}
+
+img {
+ max-width: 100%;
+}
+
+
+/* Structure and Layout
+--------------------------------------- */
+
+body {
+ text-align: center;
+ font-family: Helvetica, Arial, sans-serif;
+ font-size: 87.5%;
+ line-height: 1.5em;
+ background: #fff;
+ color: #999;
+ direction: rtl;
+}
+
+.wrapper {
+ text-align: right;
+ margin: 0 auto;
+ max-width: 960px;
+ padding: 0 1em;
+}
+
+.red-button {
+ display: inline-block;
+ border-top: 1px solid rgba(255,255,255,.5);
+ background: #751913;
+ background: -webkit-gradient(linear, right top, right bottom, from(#c52f24), to(#751913));
+ background: -webkit-linear-gradient(top, #c52f24, #751913);
+ background: -moz-linear-gradient(top, #c52f24, #751913);
+ background: -ms-linear-gradient(top, #c52f24, #751913);
+ background: -o-linear-gradient(top, #c52f24, #751913);
+ padding: 9px 18px;
+ -webkit-border-radius: 11px;
+ -moz-border-radius: 11px;
+ border-radius: 11px;
+ -webkit-box-shadow: rgba(0,0,0,1) 0 1px 0;
+ -moz-box-shadow: rgba(0,0,0,1) 0 1px 0;
+ box-shadow: rgba(0,0,0,1) 0 1px 0;
+ text-shadow: rgba(0,0,0,.4) 0 1px 0;
+ color: white;
+ font-size: 15px;
+ font-family: Helvetica, Arial, Sans-Serif;
+ text-decoration: none;
+ vertical-align: middle;
+ cursor: pointer;
+}
+.red-button:active {
+ border-top: none;
+ padding-top: 10px;
+ background: -webkit-gradient(linear, right top, right bottom, from(#751913), to(#c52f24));
+ background: -webkit-linear-gradient(top, #751913, #c52f24);
+ background: -moz-linear-gradient(top, #751913, #c52f24);
+ background: -ms-linear-gradient(top, #751913, #c52f24);
+ background: -o-linear-gradient(top, #751913, #c52f24);
+}
+
+#topNav {
+ padding: 1em 0;
+ color: #565656;
+ background: #222;
+}
+
+.s-hidden {
+ display: none;
+}
+
+@media screen and (min-width: 1025px) {
+ .more-info-button {
+ display: none;
+ }
+ .more-info-links {
+ list-style: none;
+ display: inline;
+ margin: 0;
+ }
+
+ .more-info {
+ display: inline-block;
+ }
+ .more-info:after {
+ content: " |";
+ }
+
+ .more-info:last-child:after {
+ content: "";
+ }
+}
+
+@media screen and (max-width: 1024px) {
+ #topNav .wrapper { text-align: center; }
+ .more-info-button {
+ position: relative;
+ z-index: 25;
+ }
+
+ .more-info-label {
+ display: none;
+ }
+
+ .more-info-container {
+ position: absolute;
+ top: .5em;
+ z-index: 20;
+ margin: 0 auto;
+ right: 0;
+ left: 0;
+ width: 20em;
+ }
+
+ .more-info-links {
+ display: block;
+ list-style: none;
+ background-color: #c52f24;
+ border-radius: 5px;
+ padding-top: 5.25em;
+ border: 1px #980905 solid;
+ }
+ .more-info-links.s-hidden {
+ display: none;
+ }
+ .more-info {
+ padding: .75em;
+ border-top: 1px #980905 solid;
+ }
+ .more-info a, .more-info a:link, .more-info a:visited {
+ display: block;
+ color: white;
+ width: 100%;
+ height: 100%;
+ text-decoration: none;
+ text-transform: uppercase;
+ }
+}
+
+#header {
+ background: #c52f24 url(../images/header_tile.gif) repeat-x;
+ color: #FFF;
+ padding: 1.5em 0;
+ z-index: 99;
+}
+
+#feature {
+ background: #d5e9f6 url(../images/feature_tile.gif) repeat-x;
+ color: #333;
+ padding: 0.5em 0 1.5em;
+}
+
+#container {
+ color: #333;
+ padding: 0.5em 0 1.5em 0;
+}
+
+#mainCol {
+ max-width: 630px;
+ margin-right: 2em;
+}
+
+#subCol {
+ position: absolute;
+ z-index: 0;
+ top: 21px;
+ left: 0;
+ background: #FFF;
+ padding: 1em 1.5em 1em 1.25em;
+ width: 17em;
+ font-size: 0.9285em;
+ line-height: 1.3846em;
+ margin-left: 1em;
+}
+
+
+@media screen and (max-width: 800px) {
+ #subCol {
+ position: static;
+ width: inherit;
+ margin-right: -1em;
+ margin-left: 0;
+ padding-left: 1.25em;
+ }
+}
+
+#footer {
+ padding: 2em 0;
+ background: #222 url(../images/footer_tile.gif) repeat-x;
+}
+#footer .wrapper {
+ padding-right: 1em;
+ max-width: 960px;
+}
+
+#header .wrapper, #topNav .wrapper, #feature .wrapper {padding-right: 1em; max-width: 960px;}
+#feature .wrapper {max-width: 640px; padding-left: 23em; position: relative; z-index: 0;}
+
+@media screen and (max-width: 960px) {
+ #container .wrapper { padding-left: 23em; }
+}
+
+@media screen and (max-width: 800px) {
+ #feature .wrapper, #container .wrapper { padding-left: 0; }
+}
+
+/* Links
+--------------------------------------- */
+
+a, a:link, a:visited {
+ color: #ee3f3f;
+ text-decoration: underline;
+}
+
+#mainCol a, #subCol a, #feature a {color: #980905;}
+#mainCol a code, #subCol a code, #feature a code {color: #980905;}
+
+#mainCol a.anchorlink, #mainCol a.anchorlink code {color: #333;}
+#mainCol a.anchorlink { text-decoration: none; }
+#mainCol a.anchorlink:hover { text-decoration: underline; }
+
+/* Navigation
+--------------------------------------- */
+
+.nav {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ float: left;
+ margin-top: 1.5em;
+ font-size: 1.2857em;
+}
+
+.nav .nav-item {color: #FFF; text-decoration: none;}
+.nav .nav-item:hover {text-decoration: underline;}
+
+.guides-index-large, .guides-index-small .guides-index-item {
+ padding: 0.5em 1.5em;
+ border-radius: 1em;
+ -webkit-border-radius: 1em;
+ -moz-border-radius: 1em;
+ background: #980905;
+ position: relative;
+ color: white;
+}
+
+.guides-index .guides-index-item {
+ background: #980905 url(../images/nav_arrow.gif) no-repeat left top;
+ padding-left: 1em;
+ position: relative;
+ z-index: 15;
+ padding-bottom: 0.125em;
+}
+
+.guides-index:hover .guides-index-item, .guides-index .guides-index-item:hover {
+ background-position: left -81px;
+ text-decoration: underline !important;
+}
+
+@media screen and (min-width: 481px) {
+ .nav {
+ float: left;
+ margin-top: 1.5em;
+ font-size: 1.2857em;
+ }
+ .nav>li {
+ display: inline;
+ margin-right: 0.5em;
+ }
+ .guides-index.guides-index-small {
+ display: none;
+ }
+}
+
+@media screen and (max-width: 480px) {
+ .nav {
+ float: none;
+ width: 100%;
+ text-align: center;
+ }
+ .nav .nav-item {
+ display: block;
+ margin: 0;
+ width: 100%;
+ background-color: #980905;
+ border: solid 1px #620c04;
+ border-top: 0;
+ padding: 15px 0;
+ text-align: center;
+ }
+ .nav .nav-item, .nav-item.guides-index-item {
+ text-transform: uppercase;
+ }
+ .nav .nav-item:first-child, .nav-item.guides-index-small {
+ border-top: solid 1px #620c04;
+ }
+ .guides-index.guides-index-small {
+ display: block;
+ margin-top: 1.5em;
+ }
+ .guides-index.guides-index-large {
+ display: none;
+ }
+ .guides-index-small .guides-index-item {
+ font: inherit;
+ padding-right: .75em;
+ font-size: .95em;
+ background-position: 96% 16px;
+ -webkit-appearance: none;
+ }
+ .guides-index-small .guides-index-item:hover{
+ background-position: 96% -65px;
+ }
+}
+
+#guides {
+ width: 37em;
+ display: block;
+ background: #980905;
+ border-radius: 1em;
+ color: #f1938c;
+ padding: 1.5em 2em;
+ position: absolute;
+ z-index: 10;
+ top: -0.25em;
+ left: 0;
+ padding-top: 2em;
+}
+
+#guides.visible {
+ display: block !important;
+}
+
+.guides-section dt, .guides-section dd {
+ font-weight: normal;
+ font-size: 0.722em;
+ margin: 0;
+ padding: 0;
+}
+.guides-section dt {
+ margin: 0.5em 0 0;
+ padding:0;
+}
+#guides a {
+ background: none !important;
+ color: #FFF;
+ text-decoration: none;
+}
+#guides a:hover {
+ text-decoration: underline;
+}
+.guides-section-container {
+ display: flex;
+ flex-direction: column;
+ flex-wrap: wrap;
+ width: 100%;
+ max-height: 35em;
+}
+
+.guides-section {
+ min-width: 5em;
+ margin: 0 2em 0.5em 0;
+ flex: auto;
+ max-width: 12em;
+}
+
+.guides-section dd {
+ line-height: 1.3;
+ margin-bottom: 0.5em;
+}
+
+#guides hr {
+ display: block;
+ border: none;
+ height: 1px;
+ color: #f1938c;
+ background: #f1938c;
+}
+
+/* Headings
+--------------------------------------- */
+
+h1 {
+ font-size: 2.5em;
+ line-height: 1em;
+ margin: 0.6em 0 .2em;
+ font-weight: bold;
+}
+
+h2 {
+ font-size: 2.1428em;
+ line-height: 1em;
+ margin: 0.7em 0 .2333em;
+ font-weight: bold;
+}
+
+@media screen and (max-width: 480px) {
+ h2 {
+ font-size: 1.45em;
+ }
+}
+
+h3 {
+ font-size: 1.7142em;
+ line-height: 1.286em;
+ margin: 0.875em 0 0.2916em;
+ font-weight: bold;
+}
+
+@media screen and (max-width: 480px) {
+ h3 {
+ font-size: 1.45em;
+ }
+}
+
+h4 {
+ font-size: 1.2857em;
+ line-height: 1.2em;
+ margin: 1.6667em 0 .3887em;
+ font-weight: bold;
+}
+
+h5 {
+ font-size: 1em;
+ line-height: 1.5em;
+ margin: 1em 0 .5em;
+ font-weight: bold;
+}
+
+h6 {
+ font-size: 1em;
+ line-height: 1.5em;
+ margin: 1em 0 .5em;
+ font-weight: normal;
+}
+
+.section {
+ padding-bottom: 0.25em;
+ border-bottom: 1px solid #999;
+}
+
+/* Content
+--------------------------------------- */
+
+.pic {
+ margin: 0 2em 2em 0;
+}
+
+#topNav strong {color: #999; margin-left: 0.5em;}
+#topNav strong a {color: #FFF;}
+
+#header h1 {
+ float: right;
+ background: url(../images/rails_guides_logo_1x.png) no-repeat;
+ width: 297px;
+ text-indent: -9999em;
+ margin: 0;
+ padding: 0;
+}
+
+@media
+only screen and (-webkit-min-device-pixel-ratio: 2),
+only screen and ( min--moz-device-pixel-ratio: 2),
+only screen and ( -o-min-device-pixel-ratio: 2/1),
+only screen and ( min-device-pixel-ratio: 2),
+only screen and ( min-resolution: 192dpi),
+only screen and ( min-resolution: 2dppx) {
+ #header h1 {
+ background: url(../images/rails_guides_logo_2x.png) no-repeat;
+ background-size: 160%;
+ }
+}
+
+@media screen and (max-width: 480px) {
+ #header h1 {
+ float: none;
+ }
+}
+
+#header h1 a {
+ text-decoration: none;
+ display: block;
+ height: 77px;
+}
+
+#feature p {
+ font-size: 1.2857em;
+ margin-bottom: 0.75em;
+}
+
+@media screen and (max-width: 480px) {
+ #feature p {
+ font-size: 1em;
+ }
+}
+
+#feature ul {margin-right: 0;}
+#feature ul li {
+ list-style: none;
+ background: url(../images/check_bullet.gif) no-repeat right 0.5em;
+ padding: 0.5em 1.75em 0.5em 1.75em;
+ font-size: 1.1428em;
+ font-weight: bold;
+}
+
+#mainCol dd, #subCol dd {
+ padding: 0.25em 0 1em;
+ border-bottom: 1px solid #CCC;
+ margin-bottom: 1em;
+ margin-right: 0;
+ /*padding-right: 28px;*/
+ padding-right: 0;
+}
+
+#mainCol dt, #subCol dt {
+ font-size: 1.2857em;
+ padding: 0.125em 0 0.25em 0;
+ margin-bottom: 0;
+}
+
+@media screen and (max-width: 480px) {
+ #mainCol dt, #subCol dt {
+ font-size: 1em;
+ }
+}
+
+#mainCol dd.work-in-progress, #subCol dd.work-in-progress {
+ background: #fff9d8 url(../images/tab_yellow.gif) no-repeat left top;
+ border: none;
+ padding: 1.25em 1em 1.25em 48px;
+ margin-right: 0;
+ margin-top: 0.25em;
+}
+
+#mainCol dd.kindle, #subCol dd.kindle {
+ background: #d5e9f6 url(../images/tab_info.gif) no-repeat left top;
+ border: none;
+ padding: 1.25em 1em 1.25em 48px;
+ margin-right: 0;
+ margin-top: 0.25em;
+}
+
+#mainCol div.warning, #subCol dd.warning {
+ background: #f9d9d8 url(../images/tab_red.gif) no-repeat left top;
+ border: none;
+ padding: 1.25em 1.25em 0.25em 48px;
+ margin-right: 0;
+ margin-top: 0.25em;
+}
+
+#subCol .chapters {color: #980905;}
+#subCol .chapters a {font-weight: bold;}
+#subCol .chapters ul a {font-weight: normal;}
+#subCol .chapters li {margin-bottom: 0.75em;}
+#subCol h3.chapter {margin-top: 0.25em;}
+#subCol h3.chapter img {vertical-align: text-bottom;}
+#subCol .chapters ul {margin-right: 0; margin-top: 0.5em;}
+#subCol .chapters ul li {
+ list-style: none;
+ padding: 0 1em 0 0;
+ background: url(../images/bullet.gif) no-repeat right 0.45em;
+ margin-right: 0;
+ font-size: 1em;
+ font-weight: normal;
+}
+
+#subCol li ul, li ol { margin:0 1.5em; }
+
+div.code_container {
+ background: #EEE url(../images/tab_grey.gif) no-repeat right top;
+ padding: 0.25em 48px 0.5em 1em;
+}
+
+.note {
+ background: #fff9d8 url(../images/tab_note.gif) no-repeat right top;
+ border: none;
+ padding: 1em 48px 0.25em 1em;
+ margin: 0.25em 0 1.5em 0;
+}
+
+.info {
+ background: #d5e9f6 url(../images/tab_info.gif) no-repeat right top;
+ border: none;
+ padding: 1em 48px 0.25em 1em;
+ margin: 0.25em 0 1.5em 0;
+}
+
+#mainCol div.todo {
+ background: #fff9d8 url(../images/tab_yellow.gif) no-repeat right top;
+ border: none;
+ padding: 1em 48px 0.25em 1em;
+ margin: 0.25em 0 1.5em 0;
+}
+
+.note code, .info code, .todo code {
+ background: #fff;
+}
+
+#mainCol ul li {
+ list-style:none;
+ background: url(../images/grey_bullet.gif) no-repeat right 0.5em;
+ padding-right: 1em;
+ margin-right: 0;
+}
+
+#subCol .content {
+ font-size: 0.7857em;
+ line-height: 1.5em;
+}
+
+#subCol .content li {
+ font-weight: normal;
+ background: none;
+ padding: 0 0 1em;
+ font-size: 1.1667em;
+}
+
+/* Clearing
+--------------------------------------- */
+
+.clearfix:after {
+ content: ".";
+ display: block;
+ height: 0;
+ clear: both;
+ visibility: hidden;
+}
+
+* html .clearfix {height: 1%;}
+.clearfix {display: block;}
+
+/* Same bottom margin for special boxes than for regular paragraphs, this way
+intermediate whitespace looks uniform. */
+div.code_container, div.important, div.caution, div.warning, div.note, div.info {
+ margin-bottom: 1.5em;
+}
+
+/* Remove bottom margin of paragraphs in special boxes, otherwise they get a
+spurious blank area below with the box background. */
+div.important p, div.caution p, div.warning p, div.note p, div.info p {
+ margin-bottom: 1em;
+}
+
+/* Edge Badge
+--------------------------------------- */
+
+#edge-badge {
+ position: fixed;
+ right: 0px;
+ top: 0px;
+ z-index: 100;
+ border: none;
+}
+
+/* Foundation v2.1.4 http://foundation.zurb.com */
+/* Artfully masterminded by ZURB */
+
+/* Mobile */
+@media only screen and (max-width: 767px) {
+ table.responsive { margin-bottom: 0; }
+
+ .pinned { position: absolute; right: 0; top: 0; background: #fff; width: 35%; overflow: hidden; overflow-x: scroll; border-left: 1px solid #ccc; border-right: 1px solid #ccc; }
+ .pinned table { border-left: none; border-right: none; width: 100%; }
+ .pinned table th, .pinned table td { white-space: nowrap; }
+ .pinned td:last-child { border-bottom: 0; }
+
+ div.table-wrapper { position: relative; margin-bottom: 20px; overflow: hidden; border-left: 1px solid #ccc; }
+ div.table-wrapper div.scrollable table { margin-right: 35%; }
+ div.table-wrapper div.scrollable { overflow: scroll; overflow-y: hidden; }
+
+ table.responsive td, table.responsive th { position: relative; white-space: nowrap; overflow: hidden; }
+ table.responsive th:first-child, table.responsive td:first-child, table.responsive td:first-child, table.responsive.pinned td { display: none; }
+
+}
diff --git a/guides/assets/stylesheets/print.css b/guides/assets/stylesheets/print.css
index bdc8ec948d..6280422469 100644
--- a/guides/assets/stylesheets/print.css
+++ b/guides/assets/stylesheets/print.css
@@ -4,7 +4,7 @@
/* Modified January 31, 2009
--------------------------------------- */
-body, .wrapper, .note, .info, code, #topNav, .L, .R, #frame, #container, #header, #navigation, #footer, #feature, #mainCol, #subCol, #extraCol, .content {position: static; text-align: left; text-indent: 0; background: White; color: Black; border-color: Black; width: auto; height: auto; display: block; float: none; min-height: 0; margin: 0; padding: 0;}
+body, .wrapper, .note, .info, code, #topNav, .L, .R, #frame, #container, #header, #navigation, #footer, #feature, #mainCol, #subCol, .content {position: static; text-align: left; text-indent: 0; background: White; color: Black; border-color: Black; width: auto; height: auto; display: block; float: none; min-height: 0; margin: 0; padding: 0;}
body {
background: #FFF;
diff --git a/guides/assets/stylesheets/responsive-tables.css b/guides/assets/stylesheets/responsive-tables.css
deleted file mode 100755
index f5fbcbf948..0000000000
--- a/guides/assets/stylesheets/responsive-tables.css
+++ /dev/null
@@ -1,50 +0,0 @@
-/* Foundation v2.1.4 http://foundation.zurb.com */
-/* Artfully masterminded by ZURB */
-
-/* --------------------------------------------------
- Table of Contents
------------------------------------------------------
-:: Shared Styles
-:: Page Name 1
-:: Page Name 2
-*/
-
-
-/* -----------------------------------------
- Shared Styles
------------------------------------------ */
-
-table th { font-weight: bold; }
-table td, table th { padding: 9px 10px; text-align: left; }
-
-/* Mobile */
-@media only screen and (max-width: 767px) {
-
- table { margin-bottom: 0; }
-
- .pinned { position: absolute; left: 0; top: 0; background: #fff; width: 35%; overflow: hidden; overflow-x: scroll; border-right: 1px solid #ccc; border-left: 1px solid #ccc; }
- .pinned table { border-right: none; border-left: none; width: 100%; }
- .pinned table th, .pinned table td { white-space: nowrap; }
- .pinned td:last-child { border-bottom: 0; }
-
- div.table-wrapper { position: relative; margin-bottom: 20px; overflow: hidden; border-right: 1px solid #ccc; }
- div.table-wrapper div.scrollable table { margin-left: 35%; }
- div.table-wrapper div.scrollable { overflow: scroll; overflow-y: hidden; }
-
- table td, table th { position: relative; white-space: nowrap; overflow: hidden; }
- table th:first-child, table td:first-child, table td:first-child, table.pinned td { display: none; }
-
-}
-
-/* -----------------------------------------
- Page Name 1
------------------------------------------ */
-
-
-
-
-/* -----------------------------------------
- Page Name 2
------------------------------------------ */
-
-
diff --git a/guides/assets/stylesheets/style.css b/guides/assets/stylesheets/style.css
index 89b2ab885a..3dad5124f4 100644
--- a/guides/assets/stylesheets/style.css
+++ b/guides/assets/stylesheets/style.css
@@ -11,3 +11,4 @@ Import advanced style sheet
@import url("reset.css");
@import url("main.css");
+@import url("turbolinks.css");
diff --git a/guides/assets/stylesheets/syntaxhighlighter/shCore.css b/guides/assets/stylesheets/syntaxhighlighter/shCore.css
index 34f6864a15..7e1e199343 100644
--- a/guides/assets/stylesheets/syntaxhighlighter/shCore.css
+++ b/guides/assets/stylesheets/syntaxhighlighter/shCore.css
@@ -33,7 +33,7 @@
height: auto !important;
left: auto !important;
line-height: 1.1em !important;
- margin: 0 !important;
+ margin: 0 0 0.5px 0 !important;
outline: 0 !important;
overflow: visible !important;
padding: 0 !important;
diff --git a/guides/assets/stylesheets/syntaxhighlighter/shCoreDefault.css b/guides/assets/stylesheets/syntaxhighlighter/shCoreDefault.css
deleted file mode 100644
index 08f9e10e4e..0000000000
--- a/guides/assets/stylesheets/syntaxhighlighter/shCoreDefault.css
+++ /dev/null
@@ -1,328 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-.syntaxhighlighter a,
-.syntaxhighlighter div,
-.syntaxhighlighter code,
-.syntaxhighlighter table,
-.syntaxhighlighter table td,
-.syntaxhighlighter table tr,
-.syntaxhighlighter table tbody,
-.syntaxhighlighter table thead,
-.syntaxhighlighter table caption,
-.syntaxhighlighter textarea {
- -moz-border-radius: 0 0 0 0 !important;
- -webkit-border-radius: 0 0 0 0 !important;
- background: none !important;
- border: 0 !important;
- bottom: auto !important;
- float: none !important;
- height: auto !important;
- left: auto !important;
- line-height: 1.1em !important;
- margin: 0 !important;
- outline: 0 !important;
- overflow: visible !important;
- padding: 0 !important;
- position: static !important;
- right: auto !important;
- text-align: left !important;
- top: auto !important;
- vertical-align: baseline !important;
- width: auto !important;
- box-sizing: content-box !important;
- font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace !important;
- font-weight: normal !important;
- font-style: normal !important;
- font-size: 1em !important;
- min-height: inherit !important;
- min-height: auto !important;
-}
-
-.syntaxhighlighter {
- width: 100% !important;
- margin: 1em 0 1em 0 !important;
- position: relative !important;
- overflow: auto !important;
- font-size: 1em !important;
-}
-.syntaxhighlighter.source {
- overflow: hidden !important;
-}
-.syntaxhighlighter .bold {
- font-weight: bold !important;
-}
-.syntaxhighlighter .italic {
- font-style: italic !important;
-}
-.syntaxhighlighter .line {
- white-space: pre !important;
-}
-.syntaxhighlighter table {
- width: 100% !important;
-}
-.syntaxhighlighter table caption {
- text-align: left !important;
- padding: .5em 0 0.5em 1em !important;
-}
-.syntaxhighlighter table td.code {
- width: 100% !important;
-}
-.syntaxhighlighter table td.code .container {
- position: relative !important;
-}
-.syntaxhighlighter table td.code .container textarea {
- box-sizing: border-box !important;
- position: absolute !important;
- left: 0 !important;
- top: 0 !important;
- width: 100% !important;
- height: 100% !important;
- border: none !important;
- background: white !important;
- padding-left: 1em !important;
- overflow: hidden !important;
- white-space: pre !important;
-}
-.syntaxhighlighter table td.gutter .line {
- text-align: right !important;
- padding: 0 0.5em 0 1em !important;
-}
-.syntaxhighlighter table td.code .line {
- padding: 0 1em !important;
-}
-.syntaxhighlighter.nogutter td.code .container textarea, .syntaxhighlighter.nogutter td.code .line {
- padding-left: 0em !important;
-}
-.syntaxhighlighter.show {
- display: block !important;
-}
-.syntaxhighlighter.collapsed table {
- display: none !important;
-}
-.syntaxhighlighter.collapsed .toolbar {
- padding: 0.1em 0.8em 0em 0.8em !important;
- font-size: 1em !important;
- position: static !important;
- width: auto !important;
- height: auto !important;
-}
-.syntaxhighlighter.collapsed .toolbar span {
- display: inline !important;
- margin-right: 1em !important;
-}
-.syntaxhighlighter.collapsed .toolbar span a {
- padding: 0 !important;
- display: none !important;
-}
-.syntaxhighlighter.collapsed .toolbar span a.expandSource {
- display: inline !important;
-}
-.syntaxhighlighter .toolbar {
- position: absolute !important;
- right: 1px !important;
- top: 1px !important;
- width: 11px !important;
- height: 11px !important;
- font-size: 10px !important;
- z-index: 10 !important;
-}
-.syntaxhighlighter .toolbar span.title {
- display: inline !important;
-}
-.syntaxhighlighter .toolbar a {
- display: block !important;
- text-align: center !important;
- text-decoration: none !important;
- padding-top: 1px !important;
-}
-.syntaxhighlighter .toolbar a.expandSource {
- display: none !important;
-}
-.syntaxhighlighter.ie {
- font-size: .9em !important;
- padding: 1px 0 1px 0 !important;
-}
-.syntaxhighlighter.ie .toolbar {
- line-height: 8px !important;
-}
-.syntaxhighlighter.ie .toolbar a {
- padding-top: 0px !important;
-}
-.syntaxhighlighter.printing .line.alt1 .content,
-.syntaxhighlighter.printing .line.alt2 .content,
-.syntaxhighlighter.printing .line.highlighted .number,
-.syntaxhighlighter.printing .line.highlighted.alt1 .content,
-.syntaxhighlighter.printing .line.highlighted.alt2 .content {
- background: none !important;
-}
-.syntaxhighlighter.printing .line .number {
- color: #bbbbbb !important;
-}
-.syntaxhighlighter.printing .line .content {
- color: black !important;
-}
-.syntaxhighlighter.printing .toolbar {
- display: none !important;
-}
-.syntaxhighlighter.printing a {
- text-decoration: none !important;
-}
-.syntaxhighlighter.printing .plain, .syntaxhighlighter.printing .plain a {
- color: black !important;
-}
-.syntaxhighlighter.printing .comments, .syntaxhighlighter.printing .comments a {
- color: #008200 !important;
-}
-.syntaxhighlighter.printing .string, .syntaxhighlighter.printing .string a {
- color: blue !important;
-}
-.syntaxhighlighter.printing .keyword {
- color: #006699 !important;
- font-weight: bold !important;
-}
-.syntaxhighlighter.printing .preprocessor {
- color: gray !important;
-}
-.syntaxhighlighter.printing .variable {
- color: #aa7700 !important;
-}
-.syntaxhighlighter.printing .value {
- color: #009900 !important;
-}
-.syntaxhighlighter.printing .functions {
- color: #ff1493 !important;
-}
-.syntaxhighlighter.printing .constants {
- color: #0066cc !important;
-}
-.syntaxhighlighter.printing .script {
- font-weight: bold !important;
-}
-.syntaxhighlighter.printing .color1, .syntaxhighlighter.printing .color1 a {
- color: gray !important;
-}
-.syntaxhighlighter.printing .color2, .syntaxhighlighter.printing .color2 a {
- color: #ff1493 !important;
-}
-.syntaxhighlighter.printing .color3, .syntaxhighlighter.printing .color3 a {
- color: red !important;
-}
-.syntaxhighlighter.printing .break, .syntaxhighlighter.printing .break a {
- color: black !important;
-}
-
-.syntaxhighlighter {
- background-color: white !important;
-}
-.syntaxhighlighter .line.alt1 {
- background-color: white !important;
-}
-.syntaxhighlighter .line.alt2 {
- background-color: white !important;
-}
-.syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 {
- background-color: #e0e0e0 !important;
-}
-.syntaxhighlighter .line.highlighted.number {
- color: black !important;
-}
-.syntaxhighlighter table caption {
- color: black !important;
-}
-.syntaxhighlighter .gutter {
- color: #afafaf !important;
-}
-.syntaxhighlighter .gutter .line {
- border-right: 3px solid #6ce26c !important;
-}
-.syntaxhighlighter .gutter .line.highlighted {
- background-color: #6ce26c !important;
- color: white !important;
-}
-.syntaxhighlighter.printing .line .content {
- border: none !important;
-}
-.syntaxhighlighter.collapsed {
- overflow: visible !important;
-}
-.syntaxhighlighter.collapsed .toolbar {
- color: blue !important;
- background: white !important;
- border: 1px solid #6ce26c !important;
-}
-.syntaxhighlighter.collapsed .toolbar a {
- color: blue !important;
-}
-.syntaxhighlighter.collapsed .toolbar a:hover {
- color: red !important;
-}
-.syntaxhighlighter .toolbar {
- color: white !important;
- background: #6ce26c !important;
- border: none !important;
-}
-.syntaxhighlighter .toolbar a {
- color: white !important;
-}
-.syntaxhighlighter .toolbar a:hover {
- color: black !important;
-}
-.syntaxhighlighter .plain, .syntaxhighlighter .plain a {
- color: black !important;
-}
-.syntaxhighlighter .comments, .syntaxhighlighter .comments a {
- color: #008200 !important;
-}
-.syntaxhighlighter .string, .syntaxhighlighter .string a {
- color: blue !important;
-}
-.syntaxhighlighter .keyword {
- color: #006699 !important;
-}
-.syntaxhighlighter .preprocessor {
- color: gray !important;
-}
-.syntaxhighlighter .variable {
- color: #aa7700 !important;
-}
-.syntaxhighlighter .value {
- color: #009900 !important;
-}
-.syntaxhighlighter .functions {
- color: #ff1493 !important;
-}
-.syntaxhighlighter .constants {
- color: #0066cc !important;
-}
-.syntaxhighlighter .script {
- font-weight: bold !important;
- color: #006699 !important;
- background-color: none !important;
-}
-.syntaxhighlighter .color1, .syntaxhighlighter .color1 a {
- color: gray !important;
-}
-.syntaxhighlighter .color2, .syntaxhighlighter .color2 a {
- color: #ff1493 !important;
-}
-.syntaxhighlighter .color3, .syntaxhighlighter .color3 a {
- color: red !important;
-}
-
-.syntaxhighlighter .keyword {
- font-weight: bold !important;
-}
diff --git a/guides/assets/stylesheets/syntaxhighlighter/shCoreDjango.css b/guides/assets/stylesheets/syntaxhighlighter/shCoreDjango.css
deleted file mode 100644
index 1db1f70cb0..0000000000
--- a/guides/assets/stylesheets/syntaxhighlighter/shCoreDjango.css
+++ /dev/null
@@ -1,331 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-.syntaxhighlighter a,
-.syntaxhighlighter div,
-.syntaxhighlighter code,
-.syntaxhighlighter table,
-.syntaxhighlighter table td,
-.syntaxhighlighter table tr,
-.syntaxhighlighter table tbody,
-.syntaxhighlighter table thead,
-.syntaxhighlighter table caption,
-.syntaxhighlighter textarea {
- -moz-border-radius: 0 0 0 0 !important;
- -webkit-border-radius: 0 0 0 0 !important;
- background: none !important;
- border: 0 !important;
- bottom: auto !important;
- float: none !important;
- height: auto !important;
- left: auto !important;
- line-height: 1.1em !important;
- margin: 0 !important;
- outline: 0 !important;
- overflow: visible !important;
- padding: 0 !important;
- position: static !important;
- right: auto !important;
- text-align: left !important;
- top: auto !important;
- vertical-align: baseline !important;
- width: auto !important;
- box-sizing: content-box !important;
- font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace !important;
- font-weight: normal !important;
- font-style: normal !important;
- font-size: 1em !important;
- min-height: inherit !important;
- min-height: auto !important;
-}
-
-.syntaxhighlighter {
- width: 100% !important;
- margin: 1em 0 1em 0 !important;
- position: relative !important;
- overflow: auto !important;
- font-size: 1em !important;
-}
-.syntaxhighlighter.source {
- overflow: hidden !important;
-}
-.syntaxhighlighter .bold {
- font-weight: bold !important;
-}
-.syntaxhighlighter .italic {
- font-style: italic !important;
-}
-.syntaxhighlighter .line {
- white-space: pre !important;
-}
-.syntaxhighlighter table {
- width: 100% !important;
-}
-.syntaxhighlighter table caption {
- text-align: left !important;
- padding: .5em 0 0.5em 1em !important;
-}
-.syntaxhighlighter table td.code {
- width: 100% !important;
-}
-.syntaxhighlighter table td.code .container {
- position: relative !important;
-}
-.syntaxhighlighter table td.code .container textarea {
- box-sizing: border-box !important;
- position: absolute !important;
- left: 0 !important;
- top: 0 !important;
- width: 100% !important;
- height: 100% !important;
- border: none !important;
- background: white !important;
- padding-left: 1em !important;
- overflow: hidden !important;
- white-space: pre !important;
-}
-.syntaxhighlighter table td.gutter .line {
- text-align: right !important;
- padding: 0 0.5em 0 1em !important;
-}
-.syntaxhighlighter table td.code .line {
- padding: 0 1em !important;
-}
-.syntaxhighlighter.nogutter td.code .container textarea, .syntaxhighlighter.nogutter td.code .line {
- padding-left: 0em !important;
-}
-.syntaxhighlighter.show {
- display: block !important;
-}
-.syntaxhighlighter.collapsed table {
- display: none !important;
-}
-.syntaxhighlighter.collapsed .toolbar {
- padding: 0.1em 0.8em 0em 0.8em !important;
- font-size: 1em !important;
- position: static !important;
- width: auto !important;
- height: auto !important;
-}
-.syntaxhighlighter.collapsed .toolbar span {
- display: inline !important;
- margin-right: 1em !important;
-}
-.syntaxhighlighter.collapsed .toolbar span a {
- padding: 0 !important;
- display: none !important;
-}
-.syntaxhighlighter.collapsed .toolbar span a.expandSource {
- display: inline !important;
-}
-.syntaxhighlighter .toolbar {
- position: absolute !important;
- right: 1px !important;
- top: 1px !important;
- width: 11px !important;
- height: 11px !important;
- font-size: 10px !important;
- z-index: 10 !important;
-}
-.syntaxhighlighter .toolbar span.title {
- display: inline !important;
-}
-.syntaxhighlighter .toolbar a {
- display: block !important;
- text-align: center !important;
- text-decoration: none !important;
- padding-top: 1px !important;
-}
-.syntaxhighlighter .toolbar a.expandSource {
- display: none !important;
-}
-.syntaxhighlighter.ie {
- font-size: .9em !important;
- padding: 1px 0 1px 0 !important;
-}
-.syntaxhighlighter.ie .toolbar {
- line-height: 8px !important;
-}
-.syntaxhighlighter.ie .toolbar a {
- padding-top: 0px !important;
-}
-.syntaxhighlighter.printing .line.alt1 .content,
-.syntaxhighlighter.printing .line.alt2 .content,
-.syntaxhighlighter.printing .line.highlighted .number,
-.syntaxhighlighter.printing .line.highlighted.alt1 .content,
-.syntaxhighlighter.printing .line.highlighted.alt2 .content {
- background: none !important;
-}
-.syntaxhighlighter.printing .line .number {
- color: #bbbbbb !important;
-}
-.syntaxhighlighter.printing .line .content {
- color: black !important;
-}
-.syntaxhighlighter.printing .toolbar {
- display: none !important;
-}
-.syntaxhighlighter.printing a {
- text-decoration: none !important;
-}
-.syntaxhighlighter.printing .plain, .syntaxhighlighter.printing .plain a {
- color: black !important;
-}
-.syntaxhighlighter.printing .comments, .syntaxhighlighter.printing .comments a {
- color: #008200 !important;
-}
-.syntaxhighlighter.printing .string, .syntaxhighlighter.printing .string a {
- color: blue !important;
-}
-.syntaxhighlighter.printing .keyword {
- color: #006699 !important;
- font-weight: bold !important;
-}
-.syntaxhighlighter.printing .preprocessor {
- color: gray !important;
-}
-.syntaxhighlighter.printing .variable {
- color: #aa7700 !important;
-}
-.syntaxhighlighter.printing .value {
- color: #009900 !important;
-}
-.syntaxhighlighter.printing .functions {
- color: #ff1493 !important;
-}
-.syntaxhighlighter.printing .constants {
- color: #0066cc !important;
-}
-.syntaxhighlighter.printing .script {
- font-weight: bold !important;
-}
-.syntaxhighlighter.printing .color1, .syntaxhighlighter.printing .color1 a {
- color: gray !important;
-}
-.syntaxhighlighter.printing .color2, .syntaxhighlighter.printing .color2 a {
- color: #ff1493 !important;
-}
-.syntaxhighlighter.printing .color3, .syntaxhighlighter.printing .color3 a {
- color: red !important;
-}
-.syntaxhighlighter.printing .break, .syntaxhighlighter.printing .break a {
- color: black !important;
-}
-
-.syntaxhighlighter {
- background-color: #0a2b1d !important;
-}
-.syntaxhighlighter .line.alt1 {
- background-color: #0a2b1d !important;
-}
-.syntaxhighlighter .line.alt2 {
- background-color: #0a2b1d !important;
-}
-.syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 {
- background-color: #233729 !important;
-}
-.syntaxhighlighter .line.highlighted.number {
- color: white !important;
-}
-.syntaxhighlighter table caption {
- color: #f8f8f8 !important;
-}
-.syntaxhighlighter .gutter {
- color: #497958 !important;
-}
-.syntaxhighlighter .gutter .line {
- border-right: 3px solid #41a83e !important;
-}
-.syntaxhighlighter .gutter .line.highlighted {
- background-color: #41a83e !important;
- color: #0a2b1d !important;
-}
-.syntaxhighlighter.printing .line .content {
- border: none !important;
-}
-.syntaxhighlighter.collapsed {
- overflow: visible !important;
-}
-.syntaxhighlighter.collapsed .toolbar {
- color: #96dd3b !important;
- background: black !important;
- border: 1px solid #41a83e !important;
-}
-.syntaxhighlighter.collapsed .toolbar a {
- color: #96dd3b !important;
-}
-.syntaxhighlighter.collapsed .toolbar a:hover {
- color: white !important;
-}
-.syntaxhighlighter .toolbar {
- color: white !important;
- background: #41a83e !important;
- border: none !important;
-}
-.syntaxhighlighter .toolbar a {
- color: white !important;
-}
-.syntaxhighlighter .toolbar a:hover {
- color: #ffe862 !important;
-}
-.syntaxhighlighter .plain, .syntaxhighlighter .plain a {
- color: #f8f8f8 !important;
-}
-.syntaxhighlighter .comments, .syntaxhighlighter .comments a {
- color: #336442 !important;
-}
-.syntaxhighlighter .string, .syntaxhighlighter .string a {
- color: #9df39f !important;
-}
-.syntaxhighlighter .keyword {
- color: #96dd3b !important;
-}
-.syntaxhighlighter .preprocessor {
- color: #91bb9e !important;
-}
-.syntaxhighlighter .variable {
- color: #ffaa3e !important;
-}
-.syntaxhighlighter .value {
- color: #f7e741 !important;
-}
-.syntaxhighlighter .functions {
- color: #ffaa3e !important;
-}
-.syntaxhighlighter .constants {
- color: #e0e8ff !important;
-}
-.syntaxhighlighter .script {
- font-weight: bold !important;
- color: #96dd3b !important;
- background-color: none !important;
-}
-.syntaxhighlighter .color1, .syntaxhighlighter .color1 a {
- color: #eb939a !important;
-}
-.syntaxhighlighter .color2, .syntaxhighlighter .color2 a {
- color: #91bb9e !important;
-}
-.syntaxhighlighter .color3, .syntaxhighlighter .color3 a {
- color: #edef7d !important;
-}
-
-.syntaxhighlighter .comments {
- font-style: italic !important;
-}
-.syntaxhighlighter .keyword {
- font-weight: bold !important;
-}
diff --git a/guides/assets/stylesheets/syntaxhighlighter/shCoreEclipse.css b/guides/assets/stylesheets/syntaxhighlighter/shCoreEclipse.css
deleted file mode 100644
index a45de9fd8e..0000000000
--- a/guides/assets/stylesheets/syntaxhighlighter/shCoreEclipse.css
+++ /dev/null
@@ -1,339 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-.syntaxhighlighter a,
-.syntaxhighlighter div,
-.syntaxhighlighter code,
-.syntaxhighlighter table,
-.syntaxhighlighter table td,
-.syntaxhighlighter table tr,
-.syntaxhighlighter table tbody,
-.syntaxhighlighter table thead,
-.syntaxhighlighter table caption,
-.syntaxhighlighter textarea {
- -moz-border-radius: 0 0 0 0 !important;
- -webkit-border-radius: 0 0 0 0 !important;
- background: none !important;
- border: 0 !important;
- bottom: auto !important;
- float: none !important;
- height: auto !important;
- left: auto !important;
- line-height: 1.1em !important;
- margin: 0 !important;
- outline: 0 !important;
- overflow: visible !important;
- padding: 0 !important;
- position: static !important;
- right: auto !important;
- text-align: left !important;
- top: auto !important;
- vertical-align: baseline !important;
- width: auto !important;
- box-sizing: content-box !important;
- font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace !important;
- font-weight: normal !important;
- font-style: normal !important;
- font-size: 1em !important;
- min-height: inherit !important;
- min-height: auto !important;
-}
-
-.syntaxhighlighter {
- width: 100% !important;
- margin: 1em 0 1em 0 !important;
- position: relative !important;
- overflow: auto !important;
- font-size: 1em !important;
-}
-.syntaxhighlighter.source {
- overflow: hidden !important;
-}
-.syntaxhighlighter .bold {
- font-weight: bold !important;
-}
-.syntaxhighlighter .italic {
- font-style: italic !important;
-}
-.syntaxhighlighter .line {
- white-space: pre !important;
-}
-.syntaxhighlighter table {
- width: 100% !important;
-}
-.syntaxhighlighter table caption {
- text-align: left !important;
- padding: .5em 0 0.5em 1em !important;
-}
-.syntaxhighlighter table td.code {
- width: 100% !important;
-}
-.syntaxhighlighter table td.code .container {
- position: relative !important;
-}
-.syntaxhighlighter table td.code .container textarea {
- box-sizing: border-box !important;
- position: absolute !important;
- left: 0 !important;
- top: 0 !important;
- width: 100% !important;
- height: 100% !important;
- border: none !important;
- background: white !important;
- padding-left: 1em !important;
- overflow: hidden !important;
- white-space: pre !important;
-}
-.syntaxhighlighter table td.gutter .line {
- text-align: right !important;
- padding: 0 0.5em 0 1em !important;
-}
-.syntaxhighlighter table td.code .line {
- padding: 0 1em !important;
-}
-.syntaxhighlighter.nogutter td.code .container textarea, .syntaxhighlighter.nogutter td.code .line {
- padding-left: 0em !important;
-}
-.syntaxhighlighter.show {
- display: block !important;
-}
-.syntaxhighlighter.collapsed table {
- display: none !important;
-}
-.syntaxhighlighter.collapsed .toolbar {
- padding: 0.1em 0.8em 0em 0.8em !important;
- font-size: 1em !important;
- position: static !important;
- width: auto !important;
- height: auto !important;
-}
-.syntaxhighlighter.collapsed .toolbar span {
- display: inline !important;
- margin-right: 1em !important;
-}
-.syntaxhighlighter.collapsed .toolbar span a {
- padding: 0 !important;
- display: none !important;
-}
-.syntaxhighlighter.collapsed .toolbar span a.expandSource {
- display: inline !important;
-}
-.syntaxhighlighter .toolbar {
- position: absolute !important;
- right: 1px !important;
- top: 1px !important;
- width: 11px !important;
- height: 11px !important;
- font-size: 10px !important;
- z-index: 10 !important;
-}
-.syntaxhighlighter .toolbar span.title {
- display: inline !important;
-}
-.syntaxhighlighter .toolbar a {
- display: block !important;
- text-align: center !important;
- text-decoration: none !important;
- padding-top: 1px !important;
-}
-.syntaxhighlighter .toolbar a.expandSource {
- display: none !important;
-}
-.syntaxhighlighter.ie {
- font-size: .9em !important;
- padding: 1px 0 1px 0 !important;
-}
-.syntaxhighlighter.ie .toolbar {
- line-height: 8px !important;
-}
-.syntaxhighlighter.ie .toolbar a {
- padding-top: 0px !important;
-}
-.syntaxhighlighter.printing .line.alt1 .content,
-.syntaxhighlighter.printing .line.alt2 .content,
-.syntaxhighlighter.printing .line.highlighted .number,
-.syntaxhighlighter.printing .line.highlighted.alt1 .content,
-.syntaxhighlighter.printing .line.highlighted.alt2 .content {
- background: none !important;
-}
-.syntaxhighlighter.printing .line .number {
- color: #bbbbbb !important;
-}
-.syntaxhighlighter.printing .line .content {
- color: black !important;
-}
-.syntaxhighlighter.printing .toolbar {
- display: none !important;
-}
-.syntaxhighlighter.printing a {
- text-decoration: none !important;
-}
-.syntaxhighlighter.printing .plain, .syntaxhighlighter.printing .plain a {
- color: black !important;
-}
-.syntaxhighlighter.printing .comments, .syntaxhighlighter.printing .comments a {
- color: #008200 !important;
-}
-.syntaxhighlighter.printing .string, .syntaxhighlighter.printing .string a {
- color: blue !important;
-}
-.syntaxhighlighter.printing .keyword {
- color: #006699 !important;
- font-weight: bold !important;
-}
-.syntaxhighlighter.printing .preprocessor {
- color: gray !important;
-}
-.syntaxhighlighter.printing .variable {
- color: #aa7700 !important;
-}
-.syntaxhighlighter.printing .value {
- color: #009900 !important;
-}
-.syntaxhighlighter.printing .functions {
- color: #ff1493 !important;
-}
-.syntaxhighlighter.printing .constants {
- color: #0066cc !important;
-}
-.syntaxhighlighter.printing .script {
- font-weight: bold !important;
-}
-.syntaxhighlighter.printing .color1, .syntaxhighlighter.printing .color1 a {
- color: gray !important;
-}
-.syntaxhighlighter.printing .color2, .syntaxhighlighter.printing .color2 a {
- color: #ff1493 !important;
-}
-.syntaxhighlighter.printing .color3, .syntaxhighlighter.printing .color3 a {
- color: red !important;
-}
-.syntaxhighlighter.printing .break, .syntaxhighlighter.printing .break a {
- color: black !important;
-}
-
-.syntaxhighlighter {
- background-color: white !important;
-}
-.syntaxhighlighter .line.alt1 {
- background-color: white !important;
-}
-.syntaxhighlighter .line.alt2 {
- background-color: white !important;
-}
-.syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 {
- background-color: #c3defe !important;
-}
-.syntaxhighlighter .line.highlighted.number {
- color: white !important;
-}
-.syntaxhighlighter table caption {
- color: black !important;
-}
-.syntaxhighlighter .gutter {
- color: #787878 !important;
-}
-.syntaxhighlighter .gutter .line {
- border-right: 3px solid #d4d0c8 !important;
-}
-.syntaxhighlighter .gutter .line.highlighted {
- background-color: #d4d0c8 !important;
- color: white !important;
-}
-.syntaxhighlighter.printing .line .content {
- border: none !important;
-}
-.syntaxhighlighter.collapsed {
- overflow: visible !important;
-}
-.syntaxhighlighter.collapsed .toolbar {
- color: #3f5fbf !important;
- background: white !important;
- border: 1px solid #d4d0c8 !important;
-}
-.syntaxhighlighter.collapsed .toolbar a {
- color: #3f5fbf !important;
-}
-.syntaxhighlighter.collapsed .toolbar a:hover {
- color: #aa7700 !important;
-}
-.syntaxhighlighter .toolbar {
- color: #a0a0a0 !important;
- background: #d4d0c8 !important;
- border: none !important;
-}
-.syntaxhighlighter .toolbar a {
- color: #a0a0a0 !important;
-}
-.syntaxhighlighter .toolbar a:hover {
- color: red !important;
-}
-.syntaxhighlighter .plain, .syntaxhighlighter .plain a {
- color: black !important;
-}
-.syntaxhighlighter .comments, .syntaxhighlighter .comments a {
- color: #3f5fbf !important;
-}
-.syntaxhighlighter .string, .syntaxhighlighter .string a {
- color: #2a00ff !important;
-}
-.syntaxhighlighter .keyword {
- color: #7f0055 !important;
-}
-.syntaxhighlighter .preprocessor {
- color: #646464 !important;
-}
-.syntaxhighlighter .variable {
- color: #aa7700 !important;
-}
-.syntaxhighlighter .value {
- color: #009900 !important;
-}
-.syntaxhighlighter .functions {
- color: #ff1493 !important;
-}
-.syntaxhighlighter .constants {
- color: #0066cc !important;
-}
-.syntaxhighlighter .script {
- font-weight: bold !important;
- color: #7f0055 !important;
- background-color: none !important;
-}
-.syntaxhighlighter .color1, .syntaxhighlighter .color1 a {
- color: gray !important;
-}
-.syntaxhighlighter .color2, .syntaxhighlighter .color2 a {
- color: #ff1493 !important;
-}
-.syntaxhighlighter .color3, .syntaxhighlighter .color3 a {
- color: red !important;
-}
-
-.syntaxhighlighter .keyword {
- font-weight: bold !important;
-}
-.syntaxhighlighter .xml .keyword {
- color: #3f7f7f !important;
- font-weight: normal !important;
-}
-.syntaxhighlighter .xml .color1, .syntaxhighlighter .xml .color1 a {
- color: #7f007f !important;
-}
-.syntaxhighlighter .xml .string {
- font-style: italic !important;
- color: #2a00ff !important;
-}
diff --git a/guides/assets/stylesheets/syntaxhighlighter/shCoreEmacs.css b/guides/assets/stylesheets/syntaxhighlighter/shCoreEmacs.css
deleted file mode 100644
index 706c77a0a8..0000000000
--- a/guides/assets/stylesheets/syntaxhighlighter/shCoreEmacs.css
+++ /dev/null
@@ -1,324 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-.syntaxhighlighter a,
-.syntaxhighlighter div,
-.syntaxhighlighter code,
-.syntaxhighlighter table,
-.syntaxhighlighter table td,
-.syntaxhighlighter table tr,
-.syntaxhighlighter table tbody,
-.syntaxhighlighter table thead,
-.syntaxhighlighter table caption,
-.syntaxhighlighter textarea {
- -moz-border-radius: 0 0 0 0 !important;
- -webkit-border-radius: 0 0 0 0 !important;
- background: none !important;
- border: 0 !important;
- bottom: auto !important;
- float: none !important;
- height: auto !important;
- left: auto !important;
- line-height: 1.1em !important;
- margin: 0 !important;
- outline: 0 !important;
- overflow: visible !important;
- padding: 0 !important;
- position: static !important;
- right: auto !important;
- text-align: left !important;
- top: auto !important;
- vertical-align: baseline !important;
- width: auto !important;
- box-sizing: content-box !important;
- font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace !important;
- font-weight: normal !important;
- font-style: normal !important;
- font-size: 1em !important;
- min-height: inherit !important;
- min-height: auto !important;
-}
-
-.syntaxhighlighter {
- width: 100% !important;
- margin: 1em 0 1em 0 !important;
- position: relative !important;
- overflow: auto !important;
- font-size: 1em !important;
-}
-.syntaxhighlighter.source {
- overflow: hidden !important;
-}
-.syntaxhighlighter .bold {
- font-weight: bold !important;
-}
-.syntaxhighlighter .italic {
- font-style: italic !important;
-}
-.syntaxhighlighter .line {
- white-space: pre !important;
-}
-.syntaxhighlighter table {
- width: 100% !important;
-}
-.syntaxhighlighter table caption {
- text-align: left !important;
- padding: .5em 0 0.5em 1em !important;
-}
-.syntaxhighlighter table td.code {
- width: 100% !important;
-}
-.syntaxhighlighter table td.code .container {
- position: relative !important;
-}
-.syntaxhighlighter table td.code .container textarea {
- box-sizing: border-box !important;
- position: absolute !important;
- left: 0 !important;
- top: 0 !important;
- width: 100% !important;
- height: 100% !important;
- border: none !important;
- background: white !important;
- padding-left: 1em !important;
- overflow: hidden !important;
- white-space: pre !important;
-}
-.syntaxhighlighter table td.gutter .line {
- text-align: right !important;
- padding: 0 0.5em 0 1em !important;
-}
-.syntaxhighlighter table td.code .line {
- padding: 0 1em !important;
-}
-.syntaxhighlighter.nogutter td.code .container textarea, .syntaxhighlighter.nogutter td.code .line {
- padding-left: 0em !important;
-}
-.syntaxhighlighter.show {
- display: block !important;
-}
-.syntaxhighlighter.collapsed table {
- display: none !important;
-}
-.syntaxhighlighter.collapsed .toolbar {
- padding: 0.1em 0.8em 0em 0.8em !important;
- font-size: 1em !important;
- position: static !important;
- width: auto !important;
- height: auto !important;
-}
-.syntaxhighlighter.collapsed .toolbar span {
- display: inline !important;
- margin-right: 1em !important;
-}
-.syntaxhighlighter.collapsed .toolbar span a {
- padding: 0 !important;
- display: none !important;
-}
-.syntaxhighlighter.collapsed .toolbar span a.expandSource {
- display: inline !important;
-}
-.syntaxhighlighter .toolbar {
- position: absolute !important;
- right: 1px !important;
- top: 1px !important;
- width: 11px !important;
- height: 11px !important;
- font-size: 10px !important;
- z-index: 10 !important;
-}
-.syntaxhighlighter .toolbar span.title {
- display: inline !important;
-}
-.syntaxhighlighter .toolbar a {
- display: block !important;
- text-align: center !important;
- text-decoration: none !important;
- padding-top: 1px !important;
-}
-.syntaxhighlighter .toolbar a.expandSource {
- display: none !important;
-}
-.syntaxhighlighter.ie {
- font-size: .9em !important;
- padding: 1px 0 1px 0 !important;
-}
-.syntaxhighlighter.ie .toolbar {
- line-height: 8px !important;
-}
-.syntaxhighlighter.ie .toolbar a {
- padding-top: 0px !important;
-}
-.syntaxhighlighter.printing .line.alt1 .content,
-.syntaxhighlighter.printing .line.alt2 .content,
-.syntaxhighlighter.printing .line.highlighted .number,
-.syntaxhighlighter.printing .line.highlighted.alt1 .content,
-.syntaxhighlighter.printing .line.highlighted.alt2 .content {
- background: none !important;
-}
-.syntaxhighlighter.printing .line .number {
- color: #bbbbbb !important;
-}
-.syntaxhighlighter.printing .line .content {
- color: black !important;
-}
-.syntaxhighlighter.printing .toolbar {
- display: none !important;
-}
-.syntaxhighlighter.printing a {
- text-decoration: none !important;
-}
-.syntaxhighlighter.printing .plain, .syntaxhighlighter.printing .plain a {
- color: black !important;
-}
-.syntaxhighlighter.printing .comments, .syntaxhighlighter.printing .comments a {
- color: #008200 !important;
-}
-.syntaxhighlighter.printing .string, .syntaxhighlighter.printing .string a {
- color: blue !important;
-}
-.syntaxhighlighter.printing .keyword {
- color: #006699 !important;
- font-weight: bold !important;
-}
-.syntaxhighlighter.printing .preprocessor {
- color: gray !important;
-}
-.syntaxhighlighter.printing .variable {
- color: #aa7700 !important;
-}
-.syntaxhighlighter.printing .value {
- color: #009900 !important;
-}
-.syntaxhighlighter.printing .functions {
- color: #ff1493 !important;
-}
-.syntaxhighlighter.printing .constants {
- color: #0066cc !important;
-}
-.syntaxhighlighter.printing .script {
- font-weight: bold !important;
-}
-.syntaxhighlighter.printing .color1, .syntaxhighlighter.printing .color1 a {
- color: gray !important;
-}
-.syntaxhighlighter.printing .color2, .syntaxhighlighter.printing .color2 a {
- color: #ff1493 !important;
-}
-.syntaxhighlighter.printing .color3, .syntaxhighlighter.printing .color3 a {
- color: red !important;
-}
-.syntaxhighlighter.printing .break, .syntaxhighlighter.printing .break a {
- color: black !important;
-}
-
-.syntaxhighlighter {
- background-color: black !important;
-}
-.syntaxhighlighter .line.alt1 {
- background-color: black !important;
-}
-.syntaxhighlighter .line.alt2 {
- background-color: black !important;
-}
-.syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 {
- background-color: #2a3133 !important;
-}
-.syntaxhighlighter .line.highlighted.number {
- color: white !important;
-}
-.syntaxhighlighter table caption {
- color: #d3d3d3 !important;
-}
-.syntaxhighlighter .gutter {
- color: #d3d3d3 !important;
-}
-.syntaxhighlighter .gutter .line {
- border-right: 3px solid #990000 !important;
-}
-.syntaxhighlighter .gutter .line.highlighted {
- background-color: #990000 !important;
- color: black !important;
-}
-.syntaxhighlighter.printing .line .content {
- border: none !important;
-}
-.syntaxhighlighter.collapsed {
- overflow: visible !important;
-}
-.syntaxhighlighter.collapsed .toolbar {
- color: #ebdb8d !important;
- background: black !important;
- border: 1px solid #990000 !important;
-}
-.syntaxhighlighter.collapsed .toolbar a {
- color: #ebdb8d !important;
-}
-.syntaxhighlighter.collapsed .toolbar a:hover {
- color: #ff7d27 !important;
-}
-.syntaxhighlighter .toolbar {
- color: white !important;
- background: #990000 !important;
- border: none !important;
-}
-.syntaxhighlighter .toolbar a {
- color: white !important;
-}
-.syntaxhighlighter .toolbar a:hover {
- color: #9ccff4 !important;
-}
-.syntaxhighlighter .plain, .syntaxhighlighter .plain a {
- color: #d3d3d3 !important;
-}
-.syntaxhighlighter .comments, .syntaxhighlighter .comments a {
- color: #ff7d27 !important;
-}
-.syntaxhighlighter .string, .syntaxhighlighter .string a {
- color: #ff9e7b !important;
-}
-.syntaxhighlighter .keyword {
- color: aqua !important;
-}
-.syntaxhighlighter .preprocessor {
- color: #aec4de !important;
-}
-.syntaxhighlighter .variable {
- color: #ffaa3e !important;
-}
-.syntaxhighlighter .value {
- color: #009900 !important;
-}
-.syntaxhighlighter .functions {
- color: #81cef9 !important;
-}
-.syntaxhighlighter .constants {
- color: #ff9e7b !important;
-}
-.syntaxhighlighter .script {
- font-weight: bold !important;
- color: aqua !important;
- background-color: none !important;
-}
-.syntaxhighlighter .color1, .syntaxhighlighter .color1 a {
- color: #ebdb8d !important;
-}
-.syntaxhighlighter .color2, .syntaxhighlighter .color2 a {
- color: #ff7d27 !important;
-}
-.syntaxhighlighter .color3, .syntaxhighlighter .color3 a {
- color: #aec4de !important;
-}
diff --git a/guides/assets/stylesheets/syntaxhighlighter/shCoreFadeToGrey.css b/guides/assets/stylesheets/syntaxhighlighter/shCoreFadeToGrey.css
deleted file mode 100644
index 6101eba51f..0000000000
--- a/guides/assets/stylesheets/syntaxhighlighter/shCoreFadeToGrey.css
+++ /dev/null
@@ -1,328 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-.syntaxhighlighter a,
-.syntaxhighlighter div,
-.syntaxhighlighter code,
-.syntaxhighlighter table,
-.syntaxhighlighter table td,
-.syntaxhighlighter table tr,
-.syntaxhighlighter table tbody,
-.syntaxhighlighter table thead,
-.syntaxhighlighter table caption,
-.syntaxhighlighter textarea {
- -moz-border-radius: 0 0 0 0 !important;
- -webkit-border-radius: 0 0 0 0 !important;
- background: none !important;
- border: 0 !important;
- bottom: auto !important;
- float: none !important;
- height: auto !important;
- left: auto !important;
- line-height: 1.1em !important;
- margin: 0 !important;
- outline: 0 !important;
- overflow: visible !important;
- padding: 0 !important;
- position: static !important;
- right: auto !important;
- text-align: left !important;
- top: auto !important;
- vertical-align: baseline !important;
- width: auto !important;
- box-sizing: content-box !important;
- font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace !important;
- font-weight: normal !important;
- font-style: normal !important;
- font-size: 1em !important;
- min-height: inherit !important;
- min-height: auto !important;
-}
-
-.syntaxhighlighter {
- width: 100% !important;
- margin: 1em 0 1em 0 !important;
- position: relative !important;
- overflow: auto !important;
- font-size: 1em !important;
-}
-.syntaxhighlighter.source {
- overflow: hidden !important;
-}
-.syntaxhighlighter .bold {
- font-weight: bold !important;
-}
-.syntaxhighlighter .italic {
- font-style: italic !important;
-}
-.syntaxhighlighter .line {
- white-space: pre !important;
-}
-.syntaxhighlighter table {
- width: 100% !important;
-}
-.syntaxhighlighter table caption {
- text-align: left !important;
- padding: .5em 0 0.5em 1em !important;
-}
-.syntaxhighlighter table td.code {
- width: 100% !important;
-}
-.syntaxhighlighter table td.code .container {
- position: relative !important;
-}
-.syntaxhighlighter table td.code .container textarea {
- box-sizing: border-box !important;
- position: absolute !important;
- left: 0 !important;
- top: 0 !important;
- width: 100% !important;
- height: 100% !important;
- border: none !important;
- background: white !important;
- padding-left: 1em !important;
- overflow: hidden !important;
- white-space: pre !important;
-}
-.syntaxhighlighter table td.gutter .line {
- text-align: right !important;
- padding: 0 0.5em 0 1em !important;
-}
-.syntaxhighlighter table td.code .line {
- padding: 0 1em !important;
-}
-.syntaxhighlighter.nogutter td.code .container textarea, .syntaxhighlighter.nogutter td.code .line {
- padding-left: 0em !important;
-}
-.syntaxhighlighter.show {
- display: block !important;
-}
-.syntaxhighlighter.collapsed table {
- display: none !important;
-}
-.syntaxhighlighter.collapsed .toolbar {
- padding: 0.1em 0.8em 0em 0.8em !important;
- font-size: 1em !important;
- position: static !important;
- width: auto !important;
- height: auto !important;
-}
-.syntaxhighlighter.collapsed .toolbar span {
- display: inline !important;
- margin-right: 1em !important;
-}
-.syntaxhighlighter.collapsed .toolbar span a {
- padding: 0 !important;
- display: none !important;
-}
-.syntaxhighlighter.collapsed .toolbar span a.expandSource {
- display: inline !important;
-}
-.syntaxhighlighter .toolbar {
- position: absolute !important;
- right: 1px !important;
- top: 1px !important;
- width: 11px !important;
- height: 11px !important;
- font-size: 10px !important;
- z-index: 10 !important;
-}
-.syntaxhighlighter .toolbar span.title {
- display: inline !important;
-}
-.syntaxhighlighter .toolbar a {
- display: block !important;
- text-align: center !important;
- text-decoration: none !important;
- padding-top: 1px !important;
-}
-.syntaxhighlighter .toolbar a.expandSource {
- display: none !important;
-}
-.syntaxhighlighter.ie {
- font-size: .9em !important;
- padding: 1px 0 1px 0 !important;
-}
-.syntaxhighlighter.ie .toolbar {
- line-height: 8px !important;
-}
-.syntaxhighlighter.ie .toolbar a {
- padding-top: 0px !important;
-}
-.syntaxhighlighter.printing .line.alt1 .content,
-.syntaxhighlighter.printing .line.alt2 .content,
-.syntaxhighlighter.printing .line.highlighted .number,
-.syntaxhighlighter.printing .line.highlighted.alt1 .content,
-.syntaxhighlighter.printing .line.highlighted.alt2 .content {
- background: none !important;
-}
-.syntaxhighlighter.printing .line .number {
- color: #bbbbbb !important;
-}
-.syntaxhighlighter.printing .line .content {
- color: black !important;
-}
-.syntaxhighlighter.printing .toolbar {
- display: none !important;
-}
-.syntaxhighlighter.printing a {
- text-decoration: none !important;
-}
-.syntaxhighlighter.printing .plain, .syntaxhighlighter.printing .plain a {
- color: black !important;
-}
-.syntaxhighlighter.printing .comments, .syntaxhighlighter.printing .comments a {
- color: #008200 !important;
-}
-.syntaxhighlighter.printing .string, .syntaxhighlighter.printing .string a {
- color: blue !important;
-}
-.syntaxhighlighter.printing .keyword {
- color: #006699 !important;
- font-weight: bold !important;
-}
-.syntaxhighlighter.printing .preprocessor {
- color: gray !important;
-}
-.syntaxhighlighter.printing .variable {
- color: #aa7700 !important;
-}
-.syntaxhighlighter.printing .value {
- color: #009900 !important;
-}
-.syntaxhighlighter.printing .functions {
- color: #ff1493 !important;
-}
-.syntaxhighlighter.printing .constants {
- color: #0066cc !important;
-}
-.syntaxhighlighter.printing .script {
- font-weight: bold !important;
-}
-.syntaxhighlighter.printing .color1, .syntaxhighlighter.printing .color1 a {
- color: gray !important;
-}
-.syntaxhighlighter.printing .color2, .syntaxhighlighter.printing .color2 a {
- color: #ff1493 !important;
-}
-.syntaxhighlighter.printing .color3, .syntaxhighlighter.printing .color3 a {
- color: red !important;
-}
-.syntaxhighlighter.printing .break, .syntaxhighlighter.printing .break a {
- color: black !important;
-}
-
-.syntaxhighlighter {
- background-color: #121212 !important;
-}
-.syntaxhighlighter .line.alt1 {
- background-color: #121212 !important;
-}
-.syntaxhighlighter .line.alt2 {
- background-color: #121212 !important;
-}
-.syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 {
- background-color: #2c2c29 !important;
-}
-.syntaxhighlighter .line.highlighted.number {
- color: white !important;
-}
-.syntaxhighlighter table caption {
- color: white !important;
-}
-.syntaxhighlighter .gutter {
- color: #afafaf !important;
-}
-.syntaxhighlighter .gutter .line {
- border-right: 3px solid #3185b9 !important;
-}
-.syntaxhighlighter .gutter .line.highlighted {
- background-color: #3185b9 !important;
- color: #121212 !important;
-}
-.syntaxhighlighter.printing .line .content {
- border: none !important;
-}
-.syntaxhighlighter.collapsed {
- overflow: visible !important;
-}
-.syntaxhighlighter.collapsed .toolbar {
- color: #3185b9 !important;
- background: black !important;
- border: 1px solid #3185b9 !important;
-}
-.syntaxhighlighter.collapsed .toolbar a {
- color: #3185b9 !important;
-}
-.syntaxhighlighter.collapsed .toolbar a:hover {
- color: #d01d33 !important;
-}
-.syntaxhighlighter .toolbar {
- color: white !important;
- background: #3185b9 !important;
- border: none !important;
-}
-.syntaxhighlighter .toolbar a {
- color: white !important;
-}
-.syntaxhighlighter .toolbar a:hover {
- color: #96daff !important;
-}
-.syntaxhighlighter .plain, .syntaxhighlighter .plain a {
- color: white !important;
-}
-.syntaxhighlighter .comments, .syntaxhighlighter .comments a {
- color: #696854 !important;
-}
-.syntaxhighlighter .string, .syntaxhighlighter .string a {
- color: #e3e658 !important;
-}
-.syntaxhighlighter .keyword {
- color: #d01d33 !important;
-}
-.syntaxhighlighter .preprocessor {
- color: #435a5f !important;
-}
-.syntaxhighlighter .variable {
- color: #898989 !important;
-}
-.syntaxhighlighter .value {
- color: #009900 !important;
-}
-.syntaxhighlighter .functions {
- color: #aaaaaa !important;
-}
-.syntaxhighlighter .constants {
- color: #96daff !important;
-}
-.syntaxhighlighter .script {
- font-weight: bold !important;
- color: #d01d33 !important;
- background-color: none !important;
-}
-.syntaxhighlighter .color1, .syntaxhighlighter .color1 a {
- color: #ffc074 !important;
-}
-.syntaxhighlighter .color2, .syntaxhighlighter .color2 a {
- color: #4a8cdb !important;
-}
-.syntaxhighlighter .color3, .syntaxhighlighter .color3 a {
- color: #96daff !important;
-}
-
-.syntaxhighlighter .functions {
- font-weight: bold !important;
-}
diff --git a/guides/assets/stylesheets/syntaxhighlighter/shCoreMDUltra.css b/guides/assets/stylesheets/syntaxhighlighter/shCoreMDUltra.css
deleted file mode 100644
index 2923ce7367..0000000000
--- a/guides/assets/stylesheets/syntaxhighlighter/shCoreMDUltra.css
+++ /dev/null
@@ -1,324 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-.syntaxhighlighter a,
-.syntaxhighlighter div,
-.syntaxhighlighter code,
-.syntaxhighlighter table,
-.syntaxhighlighter table td,
-.syntaxhighlighter table tr,
-.syntaxhighlighter table tbody,
-.syntaxhighlighter table thead,
-.syntaxhighlighter table caption,
-.syntaxhighlighter textarea {
- -moz-border-radius: 0 0 0 0 !important;
- -webkit-border-radius: 0 0 0 0 !important;
- background: none !important;
- border: 0 !important;
- bottom: auto !important;
- float: none !important;
- height: auto !important;
- left: auto !important;
- line-height: 1.1em !important;
- margin: 0 !important;
- outline: 0 !important;
- overflow: visible !important;
- padding: 0 !important;
- position: static !important;
- right: auto !important;
- text-align: left !important;
- top: auto !important;
- vertical-align: baseline !important;
- width: auto !important;
- box-sizing: content-box !important;
- font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace !important;
- font-weight: normal !important;
- font-style: normal !important;
- font-size: 1em !important;
- min-height: inherit !important;
- min-height: auto !important;
-}
-
-.syntaxhighlighter {
- width: 100% !important;
- margin: 1em 0 1em 0 !important;
- position: relative !important;
- overflow: auto !important;
- font-size: 1em !important;
-}
-.syntaxhighlighter.source {
- overflow: hidden !important;
-}
-.syntaxhighlighter .bold {
- font-weight: bold !important;
-}
-.syntaxhighlighter .italic {
- font-style: italic !important;
-}
-.syntaxhighlighter .line {
- white-space: pre !important;
-}
-.syntaxhighlighter table {
- width: 100% !important;
-}
-.syntaxhighlighter table caption {
- text-align: left !important;
- padding: .5em 0 0.5em 1em !important;
-}
-.syntaxhighlighter table td.code {
- width: 100% !important;
-}
-.syntaxhighlighter table td.code .container {
- position: relative !important;
-}
-.syntaxhighlighter table td.code .container textarea {
- box-sizing: border-box !important;
- position: absolute !important;
- left: 0 !important;
- top: 0 !important;
- width: 100% !important;
- height: 100% !important;
- border: none !important;
- background: white !important;
- padding-left: 1em !important;
- overflow: hidden !important;
- white-space: pre !important;
-}
-.syntaxhighlighter table td.gutter .line {
- text-align: right !important;
- padding: 0 0.5em 0 1em !important;
-}
-.syntaxhighlighter table td.code .line {
- padding: 0 1em !important;
-}
-.syntaxhighlighter.nogutter td.code .container textarea, .syntaxhighlighter.nogutter td.code .line {
- padding-left: 0em !important;
-}
-.syntaxhighlighter.show {
- display: block !important;
-}
-.syntaxhighlighter.collapsed table {
- display: none !important;
-}
-.syntaxhighlighter.collapsed .toolbar {
- padding: 0.1em 0.8em 0em 0.8em !important;
- font-size: 1em !important;
- position: static !important;
- width: auto !important;
- height: auto !important;
-}
-.syntaxhighlighter.collapsed .toolbar span {
- display: inline !important;
- margin-right: 1em !important;
-}
-.syntaxhighlighter.collapsed .toolbar span a {
- padding: 0 !important;
- display: none !important;
-}
-.syntaxhighlighter.collapsed .toolbar span a.expandSource {
- display: inline !important;
-}
-.syntaxhighlighter .toolbar {
- position: absolute !important;
- right: 1px !important;
- top: 1px !important;
- width: 11px !important;
- height: 11px !important;
- font-size: 10px !important;
- z-index: 10 !important;
-}
-.syntaxhighlighter .toolbar span.title {
- display: inline !important;
-}
-.syntaxhighlighter .toolbar a {
- display: block !important;
- text-align: center !important;
- text-decoration: none !important;
- padding-top: 1px !important;
-}
-.syntaxhighlighter .toolbar a.expandSource {
- display: none !important;
-}
-.syntaxhighlighter.ie {
- font-size: .9em !important;
- padding: 1px 0 1px 0 !important;
-}
-.syntaxhighlighter.ie .toolbar {
- line-height: 8px !important;
-}
-.syntaxhighlighter.ie .toolbar a {
- padding-top: 0px !important;
-}
-.syntaxhighlighter.printing .line.alt1 .content,
-.syntaxhighlighter.printing .line.alt2 .content,
-.syntaxhighlighter.printing .line.highlighted .number,
-.syntaxhighlighter.printing .line.highlighted.alt1 .content,
-.syntaxhighlighter.printing .line.highlighted.alt2 .content {
- background: none !important;
-}
-.syntaxhighlighter.printing .line .number {
- color: #bbbbbb !important;
-}
-.syntaxhighlighter.printing .line .content {
- color: black !important;
-}
-.syntaxhighlighter.printing .toolbar {
- display: none !important;
-}
-.syntaxhighlighter.printing a {
- text-decoration: none !important;
-}
-.syntaxhighlighter.printing .plain, .syntaxhighlighter.printing .plain a {
- color: black !important;
-}
-.syntaxhighlighter.printing .comments, .syntaxhighlighter.printing .comments a {
- color: #008200 !important;
-}
-.syntaxhighlighter.printing .string, .syntaxhighlighter.printing .string a {
- color: blue !important;
-}
-.syntaxhighlighter.printing .keyword {
- color: #006699 !important;
- font-weight: bold !important;
-}
-.syntaxhighlighter.printing .preprocessor {
- color: gray !important;
-}
-.syntaxhighlighter.printing .variable {
- color: #aa7700 !important;
-}
-.syntaxhighlighter.printing .value {
- color: #009900 !important;
-}
-.syntaxhighlighter.printing .functions {
- color: #ff1493 !important;
-}
-.syntaxhighlighter.printing .constants {
- color: #0066cc !important;
-}
-.syntaxhighlighter.printing .script {
- font-weight: bold !important;
-}
-.syntaxhighlighter.printing .color1, .syntaxhighlighter.printing .color1 a {
- color: gray !important;
-}
-.syntaxhighlighter.printing .color2, .syntaxhighlighter.printing .color2 a {
- color: #ff1493 !important;
-}
-.syntaxhighlighter.printing .color3, .syntaxhighlighter.printing .color3 a {
- color: red !important;
-}
-.syntaxhighlighter.printing .break, .syntaxhighlighter.printing .break a {
- color: black !important;
-}
-
-.syntaxhighlighter {
- background-color: #222222 !important;
-}
-.syntaxhighlighter .line.alt1 {
- background-color: #222222 !important;
-}
-.syntaxhighlighter .line.alt2 {
- background-color: #222222 !important;
-}
-.syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 {
- background-color: #253e5a !important;
-}
-.syntaxhighlighter .line.highlighted.number {
- color: white !important;
-}
-.syntaxhighlighter table caption {
- color: lime !important;
-}
-.syntaxhighlighter .gutter {
- color: #38566f !important;
-}
-.syntaxhighlighter .gutter .line {
- border-right: 3px solid #435a5f !important;
-}
-.syntaxhighlighter .gutter .line.highlighted {
- background-color: #435a5f !important;
- color: #222222 !important;
-}
-.syntaxhighlighter.printing .line .content {
- border: none !important;
-}
-.syntaxhighlighter.collapsed {
- overflow: visible !important;
-}
-.syntaxhighlighter.collapsed .toolbar {
- color: #428bdd !important;
- background: black !important;
- border: 1px solid #435a5f !important;
-}
-.syntaxhighlighter.collapsed .toolbar a {
- color: #428bdd !important;
-}
-.syntaxhighlighter.collapsed .toolbar a:hover {
- color: lime !important;
-}
-.syntaxhighlighter .toolbar {
- color: #aaaaff !important;
- background: #435a5f !important;
- border: none !important;
-}
-.syntaxhighlighter .toolbar a {
- color: #aaaaff !important;
-}
-.syntaxhighlighter .toolbar a:hover {
- color: #9ccff4 !important;
-}
-.syntaxhighlighter .plain, .syntaxhighlighter .plain a {
- color: lime !important;
-}
-.syntaxhighlighter .comments, .syntaxhighlighter .comments a {
- color: #428bdd !important;
-}
-.syntaxhighlighter .string, .syntaxhighlighter .string a {
- color: lime !important;
-}
-.syntaxhighlighter .keyword {
- color: #aaaaff !important;
-}
-.syntaxhighlighter .preprocessor {
- color: #8aa6c1 !important;
-}
-.syntaxhighlighter .variable {
- color: aqua !important;
-}
-.syntaxhighlighter .value {
- color: #f7e741 !important;
-}
-.syntaxhighlighter .functions {
- color: #ff8000 !important;
-}
-.syntaxhighlighter .constants {
- color: yellow !important;
-}
-.syntaxhighlighter .script {
- font-weight: bold !important;
- color: #aaaaff !important;
- background-color: none !important;
-}
-.syntaxhighlighter .color1, .syntaxhighlighter .color1 a {
- color: red !important;
-}
-.syntaxhighlighter .color2, .syntaxhighlighter .color2 a {
- color: yellow !important;
-}
-.syntaxhighlighter .color3, .syntaxhighlighter .color3 a {
- color: #ffaa3e !important;
-}
diff --git a/guides/assets/stylesheets/syntaxhighlighter/shCoreMidnight.css b/guides/assets/stylesheets/syntaxhighlighter/shCoreMidnight.css
deleted file mode 100644
index e3733eed56..0000000000
--- a/guides/assets/stylesheets/syntaxhighlighter/shCoreMidnight.css
+++ /dev/null
@@ -1,324 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-.syntaxhighlighter a,
-.syntaxhighlighter div,
-.syntaxhighlighter code,
-.syntaxhighlighter table,
-.syntaxhighlighter table td,
-.syntaxhighlighter table tr,
-.syntaxhighlighter table tbody,
-.syntaxhighlighter table thead,
-.syntaxhighlighter table caption,
-.syntaxhighlighter textarea {
- -moz-border-radius: 0 0 0 0 !important;
- -webkit-border-radius: 0 0 0 0 !important;
- background: none !important;
- border: 0 !important;
- bottom: auto !important;
- float: none !important;
- height: auto !important;
- left: auto !important;
- line-height: 1.1em !important;
- margin: 0 !important;
- outline: 0 !important;
- overflow: visible !important;
- padding: 0 !important;
- position: static !important;
- right: auto !important;
- text-align: left !important;
- top: auto !important;
- vertical-align: baseline !important;
- width: auto !important;
- box-sizing: content-box !important;
- font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace !important;
- font-weight: normal !important;
- font-style: normal !important;
- font-size: 1em !important;
- min-height: inherit !important;
- min-height: auto !important;
-}
-
-.syntaxhighlighter {
- width: 100% !important;
- margin: 1em 0 1em 0 !important;
- position: relative !important;
- overflow: auto !important;
- font-size: 1em !important;
-}
-.syntaxhighlighter.source {
- overflow: hidden !important;
-}
-.syntaxhighlighter .bold {
- font-weight: bold !important;
-}
-.syntaxhighlighter .italic {
- font-style: italic !important;
-}
-.syntaxhighlighter .line {
- white-space: pre !important;
-}
-.syntaxhighlighter table {
- width: 100% !important;
-}
-.syntaxhighlighter table caption {
- text-align: left !important;
- padding: .5em 0 0.5em 1em !important;
-}
-.syntaxhighlighter table td.code {
- width: 100% !important;
-}
-.syntaxhighlighter table td.code .container {
- position: relative !important;
-}
-.syntaxhighlighter table td.code .container textarea {
- box-sizing: border-box !important;
- position: absolute !important;
- left: 0 !important;
- top: 0 !important;
- width: 100% !important;
- height: 100% !important;
- border: none !important;
- background: white !important;
- padding-left: 1em !important;
- overflow: hidden !important;
- white-space: pre !important;
-}
-.syntaxhighlighter table td.gutter .line {
- text-align: right !important;
- padding: 0 0.5em 0 1em !important;
-}
-.syntaxhighlighter table td.code .line {
- padding: 0 1em !important;
-}
-.syntaxhighlighter.nogutter td.code .container textarea, .syntaxhighlighter.nogutter td.code .line {
- padding-left: 0em !important;
-}
-.syntaxhighlighter.show {
- display: block !important;
-}
-.syntaxhighlighter.collapsed table {
- display: none !important;
-}
-.syntaxhighlighter.collapsed .toolbar {
- padding: 0.1em 0.8em 0em 0.8em !important;
- font-size: 1em !important;
- position: static !important;
- width: auto !important;
- height: auto !important;
-}
-.syntaxhighlighter.collapsed .toolbar span {
- display: inline !important;
- margin-right: 1em !important;
-}
-.syntaxhighlighter.collapsed .toolbar span a {
- padding: 0 !important;
- display: none !important;
-}
-.syntaxhighlighter.collapsed .toolbar span a.expandSource {
- display: inline !important;
-}
-.syntaxhighlighter .toolbar {
- position: absolute !important;
- right: 1px !important;
- top: 1px !important;
- width: 11px !important;
- height: 11px !important;
- font-size: 10px !important;
- z-index: 10 !important;
-}
-.syntaxhighlighter .toolbar span.title {
- display: inline !important;
-}
-.syntaxhighlighter .toolbar a {
- display: block !important;
- text-align: center !important;
- text-decoration: none !important;
- padding-top: 1px !important;
-}
-.syntaxhighlighter .toolbar a.expandSource {
- display: none !important;
-}
-.syntaxhighlighter.ie {
- font-size: .9em !important;
- padding: 1px 0 1px 0 !important;
-}
-.syntaxhighlighter.ie .toolbar {
- line-height: 8px !important;
-}
-.syntaxhighlighter.ie .toolbar a {
- padding-top: 0px !important;
-}
-.syntaxhighlighter.printing .line.alt1 .content,
-.syntaxhighlighter.printing .line.alt2 .content,
-.syntaxhighlighter.printing .line.highlighted .number,
-.syntaxhighlighter.printing .line.highlighted.alt1 .content,
-.syntaxhighlighter.printing .line.highlighted.alt2 .content {
- background: none !important;
-}
-.syntaxhighlighter.printing .line .number {
- color: #bbbbbb !important;
-}
-.syntaxhighlighter.printing .line .content {
- color: black !important;
-}
-.syntaxhighlighter.printing .toolbar {
- display: none !important;
-}
-.syntaxhighlighter.printing a {
- text-decoration: none !important;
-}
-.syntaxhighlighter.printing .plain, .syntaxhighlighter.printing .plain a {
- color: black !important;
-}
-.syntaxhighlighter.printing .comments, .syntaxhighlighter.printing .comments a {
- color: #008200 !important;
-}
-.syntaxhighlighter.printing .string, .syntaxhighlighter.printing .string a {
- color: blue !important;
-}
-.syntaxhighlighter.printing .keyword {
- color: #006699 !important;
- font-weight: bold !important;
-}
-.syntaxhighlighter.printing .preprocessor {
- color: gray !important;
-}
-.syntaxhighlighter.printing .variable {
- color: #aa7700 !important;
-}
-.syntaxhighlighter.printing .value {
- color: #009900 !important;
-}
-.syntaxhighlighter.printing .functions {
- color: #ff1493 !important;
-}
-.syntaxhighlighter.printing .constants {
- color: #0066cc !important;
-}
-.syntaxhighlighter.printing .script {
- font-weight: bold !important;
-}
-.syntaxhighlighter.printing .color1, .syntaxhighlighter.printing .color1 a {
- color: gray !important;
-}
-.syntaxhighlighter.printing .color2, .syntaxhighlighter.printing .color2 a {
- color: #ff1493 !important;
-}
-.syntaxhighlighter.printing .color3, .syntaxhighlighter.printing .color3 a {
- color: red !important;
-}
-.syntaxhighlighter.printing .break, .syntaxhighlighter.printing .break a {
- color: black !important;
-}
-
-.syntaxhighlighter {
- background-color: #0f192a !important;
-}
-.syntaxhighlighter .line.alt1 {
- background-color: #0f192a !important;
-}
-.syntaxhighlighter .line.alt2 {
- background-color: #0f192a !important;
-}
-.syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 {
- background-color: #253e5a !important;
-}
-.syntaxhighlighter .line.highlighted.number {
- color: #38566f !important;
-}
-.syntaxhighlighter table caption {
- color: #d1edff !important;
-}
-.syntaxhighlighter .gutter {
- color: #afafaf !important;
-}
-.syntaxhighlighter .gutter .line {
- border-right: 3px solid #435a5f !important;
-}
-.syntaxhighlighter .gutter .line.highlighted {
- background-color: #435a5f !important;
- color: #0f192a !important;
-}
-.syntaxhighlighter.printing .line .content {
- border: none !important;
-}
-.syntaxhighlighter.collapsed {
- overflow: visible !important;
-}
-.syntaxhighlighter.collapsed .toolbar {
- color: #428bdd !important;
- background: black !important;
- border: 1px solid #435a5f !important;
-}
-.syntaxhighlighter.collapsed .toolbar a {
- color: #428bdd !important;
-}
-.syntaxhighlighter.collapsed .toolbar a:hover {
- color: #1dc116 !important;
-}
-.syntaxhighlighter .toolbar {
- color: #d1edff !important;
- background: #435a5f !important;
- border: none !important;
-}
-.syntaxhighlighter .toolbar a {
- color: #d1edff !important;
-}
-.syntaxhighlighter .toolbar a:hover {
- color: #8aa6c1 !important;
-}
-.syntaxhighlighter .plain, .syntaxhighlighter .plain a {
- color: #d1edff !important;
-}
-.syntaxhighlighter .comments, .syntaxhighlighter .comments a {
- color: #428bdd !important;
-}
-.syntaxhighlighter .string, .syntaxhighlighter .string a {
- color: #1dc116 !important;
-}
-.syntaxhighlighter .keyword {
- color: #b43d3d !important;
-}
-.syntaxhighlighter .preprocessor {
- color: #8aa6c1 !important;
-}
-.syntaxhighlighter .variable {
- color: #ffaa3e !important;
-}
-.syntaxhighlighter .value {
- color: #f7e741 !important;
-}
-.syntaxhighlighter .functions {
- color: #ffaa3e !important;
-}
-.syntaxhighlighter .constants {
- color: #e0e8ff !important;
-}
-.syntaxhighlighter .script {
- font-weight: bold !important;
- color: #b43d3d !important;
- background-color: none !important;
-}
-.syntaxhighlighter .color1, .syntaxhighlighter .color1 a {
- color: #f8bb00 !important;
-}
-.syntaxhighlighter .color2, .syntaxhighlighter .color2 a {
- color: white !important;
-}
-.syntaxhighlighter .color3, .syntaxhighlighter .color3 a {
- color: #ffaa3e !important;
-}
diff --git a/guides/assets/stylesheets/syntaxhighlighter/shCoreRDark.css b/guides/assets/stylesheets/syntaxhighlighter/shCoreRDark.css
deleted file mode 100644
index d09368384d..0000000000
--- a/guides/assets/stylesheets/syntaxhighlighter/shCoreRDark.css
+++ /dev/null
@@ -1,324 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-.syntaxhighlighter a,
-.syntaxhighlighter div,
-.syntaxhighlighter code,
-.syntaxhighlighter table,
-.syntaxhighlighter table td,
-.syntaxhighlighter table tr,
-.syntaxhighlighter table tbody,
-.syntaxhighlighter table thead,
-.syntaxhighlighter table caption,
-.syntaxhighlighter textarea {
- -moz-border-radius: 0 0 0 0 !important;
- -webkit-border-radius: 0 0 0 0 !important;
- background: none !important;
- border: 0 !important;
- bottom: auto !important;
- float: none !important;
- height: auto !important;
- left: auto !important;
- line-height: 1.1em !important;
- margin: 0 !important;
- outline: 0 !important;
- overflow: visible !important;
- padding: 0 !important;
- position: static !important;
- right: auto !important;
- text-align: left !important;
- top: auto !important;
- vertical-align: baseline !important;
- width: auto !important;
- box-sizing: content-box !important;
- font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace !important;
- font-weight: normal !important;
- font-style: normal !important;
- font-size: 1em !important;
- min-height: inherit !important;
- min-height: auto !important;
-}
-
-.syntaxhighlighter {
- width: 100% !important;
- margin: 1em 0 1em 0 !important;
- position: relative !important;
- overflow: auto !important;
- font-size: 1em !important;
-}
-.syntaxhighlighter.source {
- overflow: hidden !important;
-}
-.syntaxhighlighter .bold {
- font-weight: bold !important;
-}
-.syntaxhighlighter .italic {
- font-style: italic !important;
-}
-.syntaxhighlighter .line {
- white-space: pre !important;
-}
-.syntaxhighlighter table {
- width: 100% !important;
-}
-.syntaxhighlighter table caption {
- text-align: left !important;
- padding: .5em 0 0.5em 1em !important;
-}
-.syntaxhighlighter table td.code {
- width: 100% !important;
-}
-.syntaxhighlighter table td.code .container {
- position: relative !important;
-}
-.syntaxhighlighter table td.code .container textarea {
- box-sizing: border-box !important;
- position: absolute !important;
- left: 0 !important;
- top: 0 !important;
- width: 100% !important;
- height: 100% !important;
- border: none !important;
- background: white !important;
- padding-left: 1em !important;
- overflow: hidden !important;
- white-space: pre !important;
-}
-.syntaxhighlighter table td.gutter .line {
- text-align: right !important;
- padding: 0 0.5em 0 1em !important;
-}
-.syntaxhighlighter table td.code .line {
- padding: 0 1em !important;
-}
-.syntaxhighlighter.nogutter td.code .container textarea, .syntaxhighlighter.nogutter td.code .line {
- padding-left: 0em !important;
-}
-.syntaxhighlighter.show {
- display: block !important;
-}
-.syntaxhighlighter.collapsed table {
- display: none !important;
-}
-.syntaxhighlighter.collapsed .toolbar {
- padding: 0.1em 0.8em 0em 0.8em !important;
- font-size: 1em !important;
- position: static !important;
- width: auto !important;
- height: auto !important;
-}
-.syntaxhighlighter.collapsed .toolbar span {
- display: inline !important;
- margin-right: 1em !important;
-}
-.syntaxhighlighter.collapsed .toolbar span a {
- padding: 0 !important;
- display: none !important;
-}
-.syntaxhighlighter.collapsed .toolbar span a.expandSource {
- display: inline !important;
-}
-.syntaxhighlighter .toolbar {
- position: absolute !important;
- right: 1px !important;
- top: 1px !important;
- width: 11px !important;
- height: 11px !important;
- font-size: 10px !important;
- z-index: 10 !important;
-}
-.syntaxhighlighter .toolbar span.title {
- display: inline !important;
-}
-.syntaxhighlighter .toolbar a {
- display: block !important;
- text-align: center !important;
- text-decoration: none !important;
- padding-top: 1px !important;
-}
-.syntaxhighlighter .toolbar a.expandSource {
- display: none !important;
-}
-.syntaxhighlighter.ie {
- font-size: .9em !important;
- padding: 1px 0 1px 0 !important;
-}
-.syntaxhighlighter.ie .toolbar {
- line-height: 8px !important;
-}
-.syntaxhighlighter.ie .toolbar a {
- padding-top: 0px !important;
-}
-.syntaxhighlighter.printing .line.alt1 .content,
-.syntaxhighlighter.printing .line.alt2 .content,
-.syntaxhighlighter.printing .line.highlighted .number,
-.syntaxhighlighter.printing .line.highlighted.alt1 .content,
-.syntaxhighlighter.printing .line.highlighted.alt2 .content {
- background: none !important;
-}
-.syntaxhighlighter.printing .line .number {
- color: #bbbbbb !important;
-}
-.syntaxhighlighter.printing .line .content {
- color: black !important;
-}
-.syntaxhighlighter.printing .toolbar {
- display: none !important;
-}
-.syntaxhighlighter.printing a {
- text-decoration: none !important;
-}
-.syntaxhighlighter.printing .plain, .syntaxhighlighter.printing .plain a {
- color: black !important;
-}
-.syntaxhighlighter.printing .comments, .syntaxhighlighter.printing .comments a {
- color: #008200 !important;
-}
-.syntaxhighlighter.printing .string, .syntaxhighlighter.printing .string a {
- color: blue !important;
-}
-.syntaxhighlighter.printing .keyword {
- color: #006699 !important;
- font-weight: bold !important;
-}
-.syntaxhighlighter.printing .preprocessor {
- color: gray !important;
-}
-.syntaxhighlighter.printing .variable {
- color: #aa7700 !important;
-}
-.syntaxhighlighter.printing .value {
- color: #009900 !important;
-}
-.syntaxhighlighter.printing .functions {
- color: #ff1493 !important;
-}
-.syntaxhighlighter.printing .constants {
- color: #0066cc !important;
-}
-.syntaxhighlighter.printing .script {
- font-weight: bold !important;
-}
-.syntaxhighlighter.printing .color1, .syntaxhighlighter.printing .color1 a {
- color: gray !important;
-}
-.syntaxhighlighter.printing .color2, .syntaxhighlighter.printing .color2 a {
- color: #ff1493 !important;
-}
-.syntaxhighlighter.printing .color3, .syntaxhighlighter.printing .color3 a {
- color: red !important;
-}
-.syntaxhighlighter.printing .break, .syntaxhighlighter.printing .break a {
- color: black !important;
-}
-
-.syntaxhighlighter {
- background-color: #1b2426 !important;
-}
-.syntaxhighlighter .line.alt1 {
- background-color: #1b2426 !important;
-}
-.syntaxhighlighter .line.alt2 {
- background-color: #1b2426 !important;
-}
-.syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 {
- background-color: #323e41 !important;
-}
-.syntaxhighlighter .line.highlighted.number {
- color: #b9bdb6 !important;
-}
-.syntaxhighlighter table caption {
- color: #b9bdb6 !important;
-}
-.syntaxhighlighter .gutter {
- color: #afafaf !important;
-}
-.syntaxhighlighter .gutter .line {
- border-right: 3px solid #435a5f !important;
-}
-.syntaxhighlighter .gutter .line.highlighted {
- background-color: #435a5f !important;
- color: #1b2426 !important;
-}
-.syntaxhighlighter.printing .line .content {
- border: none !important;
-}
-.syntaxhighlighter.collapsed {
- overflow: visible !important;
-}
-.syntaxhighlighter.collapsed .toolbar {
- color: #5ba1cf !important;
- background: black !important;
- border: 1px solid #435a5f !important;
-}
-.syntaxhighlighter.collapsed .toolbar a {
- color: #5ba1cf !important;
-}
-.syntaxhighlighter.collapsed .toolbar a:hover {
- color: #5ce638 !important;
-}
-.syntaxhighlighter .toolbar {
- color: white !important;
- background: #435a5f !important;
- border: none !important;
-}
-.syntaxhighlighter .toolbar a {
- color: white !important;
-}
-.syntaxhighlighter .toolbar a:hover {
- color: #e0e8ff !important;
-}
-.syntaxhighlighter .plain, .syntaxhighlighter .plain a {
- color: #b9bdb6 !important;
-}
-.syntaxhighlighter .comments, .syntaxhighlighter .comments a {
- color: #878a85 !important;
-}
-.syntaxhighlighter .string, .syntaxhighlighter .string a {
- color: #5ce638 !important;
-}
-.syntaxhighlighter .keyword {
- color: #5ba1cf !important;
-}
-.syntaxhighlighter .preprocessor {
- color: #435a5f !important;
-}
-.syntaxhighlighter .variable {
- color: #ffaa3e !important;
-}
-.syntaxhighlighter .value {
- color: #009900 !important;
-}
-.syntaxhighlighter .functions {
- color: #ffaa3e !important;
-}
-.syntaxhighlighter .constants {
- color: #e0e8ff !important;
-}
-.syntaxhighlighter .script {
- font-weight: bold !important;
- color: #5ba1cf !important;
- background-color: none !important;
-}
-.syntaxhighlighter .color1, .syntaxhighlighter .color1 a {
- color: #e0e8ff !important;
-}
-.syntaxhighlighter .color2, .syntaxhighlighter .color2 a {
- color: white !important;
-}
-.syntaxhighlighter .color3, .syntaxhighlighter .color3 a {
- color: #ffaa3e !important;
-}
diff --git a/guides/assets/stylesheets/syntaxhighlighter/shThemeDefault.css b/guides/assets/stylesheets/syntaxhighlighter/shThemeDefault.css
deleted file mode 100644
index 136541172d..0000000000
--- a/guides/assets/stylesheets/syntaxhighlighter/shThemeDefault.css
+++ /dev/null
@@ -1,117 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-.syntaxhighlighter {
- background-color: white !important;
-}
-.syntaxhighlighter .line.alt1 {
- background-color: white !important;
-}
-.syntaxhighlighter .line.alt2 {
- background-color: white !important;
-}
-.syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 {
- background-color: #e0e0e0 !important;
-}
-.syntaxhighlighter .line.highlighted.number {
- color: black !important;
-}
-.syntaxhighlighter table caption {
- color: black !important;
-}
-.syntaxhighlighter .gutter {
- color: #afafaf !important;
-}
-.syntaxhighlighter .gutter .line {
- border-right: 3px solid #6ce26c !important;
-}
-.syntaxhighlighter .gutter .line.highlighted {
- background-color: #6ce26c !important;
- color: white !important;
-}
-.syntaxhighlighter.printing .line .content {
- border: none !important;
-}
-.syntaxhighlighter.collapsed {
- overflow: visible !important;
-}
-.syntaxhighlighter.collapsed .toolbar {
- color: blue !important;
- background: white !important;
- border: 1px solid #6ce26c !important;
-}
-.syntaxhighlighter.collapsed .toolbar a {
- color: blue !important;
-}
-.syntaxhighlighter.collapsed .toolbar a:hover {
- color: red !important;
-}
-.syntaxhighlighter .toolbar {
- color: white !important;
- background: #6ce26c !important;
- border: none !important;
-}
-.syntaxhighlighter .toolbar a {
- color: white !important;
-}
-.syntaxhighlighter .toolbar a:hover {
- color: black !important;
-}
-.syntaxhighlighter .plain, .syntaxhighlighter .plain a {
- color: black !important;
-}
-.syntaxhighlighter .comments, .syntaxhighlighter .comments a {
- color: #008200 !important;
-}
-.syntaxhighlighter .string, .syntaxhighlighter .string a {
- color: blue !important;
-}
-.syntaxhighlighter .keyword {
- color: #006699 !important;
-}
-.syntaxhighlighter .preprocessor {
- color: gray !important;
-}
-.syntaxhighlighter .variable {
- color: #aa7700 !important;
-}
-.syntaxhighlighter .value {
- color: #009900 !important;
-}
-.syntaxhighlighter .functions {
- color: #ff1493 !important;
-}
-.syntaxhighlighter .constants {
- color: #0066cc !important;
-}
-.syntaxhighlighter .script {
- font-weight: bold !important;
- color: #006699 !important;
- background-color: none !important;
-}
-.syntaxhighlighter .color1, .syntaxhighlighter .color1 a {
- color: gray !important;
-}
-.syntaxhighlighter .color2, .syntaxhighlighter .color2 a {
- color: #ff1493 !important;
-}
-.syntaxhighlighter .color3, .syntaxhighlighter .color3 a {
- color: red !important;
-}
-
-.syntaxhighlighter .keyword {
- font-weight: bold !important;
-}
diff --git a/guides/assets/stylesheets/syntaxhighlighter/shThemeDjango.css b/guides/assets/stylesheets/syntaxhighlighter/shThemeDjango.css
deleted file mode 100644
index d8b4313433..0000000000
--- a/guides/assets/stylesheets/syntaxhighlighter/shThemeDjango.css
+++ /dev/null
@@ -1,120 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-.syntaxhighlighter {
- background-color: #0a2b1d !important;
-}
-.syntaxhighlighter .line.alt1 {
- background-color: #0a2b1d !important;
-}
-.syntaxhighlighter .line.alt2 {
- background-color: #0a2b1d !important;
-}
-.syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 {
- background-color: #233729 !important;
-}
-.syntaxhighlighter .line.highlighted.number {
- color: white !important;
-}
-.syntaxhighlighter table caption {
- color: #f8f8f8 !important;
-}
-.syntaxhighlighter .gutter {
- color: #497958 !important;
-}
-.syntaxhighlighter .gutter .line {
- border-right: 3px solid #41a83e !important;
-}
-.syntaxhighlighter .gutter .line.highlighted {
- background-color: #41a83e !important;
- color: #0a2b1d !important;
-}
-.syntaxhighlighter.printing .line .content {
- border: none !important;
-}
-.syntaxhighlighter.collapsed {
- overflow: visible !important;
-}
-.syntaxhighlighter.collapsed .toolbar {
- color: #96dd3b !important;
- background: black !important;
- border: 1px solid #41a83e !important;
-}
-.syntaxhighlighter.collapsed .toolbar a {
- color: #96dd3b !important;
-}
-.syntaxhighlighter.collapsed .toolbar a:hover {
- color: white !important;
-}
-.syntaxhighlighter .toolbar {
- color: white !important;
- background: #41a83e !important;
- border: none !important;
-}
-.syntaxhighlighter .toolbar a {
- color: white !important;
-}
-.syntaxhighlighter .toolbar a:hover {
- color: #ffe862 !important;
-}
-.syntaxhighlighter .plain, .syntaxhighlighter .plain a {
- color: #f8f8f8 !important;
-}
-.syntaxhighlighter .comments, .syntaxhighlighter .comments a {
- color: #336442 !important;
-}
-.syntaxhighlighter .string, .syntaxhighlighter .string a {
- color: #9df39f !important;
-}
-.syntaxhighlighter .keyword {
- color: #96dd3b !important;
-}
-.syntaxhighlighter .preprocessor {
- color: #91bb9e !important;
-}
-.syntaxhighlighter .variable {
- color: #ffaa3e !important;
-}
-.syntaxhighlighter .value {
- color: #f7e741 !important;
-}
-.syntaxhighlighter .functions {
- color: #ffaa3e !important;
-}
-.syntaxhighlighter .constants {
- color: #e0e8ff !important;
-}
-.syntaxhighlighter .script {
- font-weight: bold !important;
- color: #96dd3b !important;
- background-color: none !important;
-}
-.syntaxhighlighter .color1, .syntaxhighlighter .color1 a {
- color: #eb939a !important;
-}
-.syntaxhighlighter .color2, .syntaxhighlighter .color2 a {
- color: #91bb9e !important;
-}
-.syntaxhighlighter .color3, .syntaxhighlighter .color3 a {
- color: #edef7d !important;
-}
-
-.syntaxhighlighter .comments {
- font-style: italic !important;
-}
-.syntaxhighlighter .keyword {
- font-weight: bold !important;
-}
diff --git a/guides/assets/stylesheets/syntaxhighlighter/shThemeEclipse.css b/guides/assets/stylesheets/syntaxhighlighter/shThemeEclipse.css
deleted file mode 100644
index 77377d9533..0000000000
--- a/guides/assets/stylesheets/syntaxhighlighter/shThemeEclipse.css
+++ /dev/null
@@ -1,128 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-.syntaxhighlighter {
- background-color: white !important;
-}
-.syntaxhighlighter .line.alt1 {
- background-color: white !important;
-}
-.syntaxhighlighter .line.alt2 {
- background-color: white !important;
-}
-.syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 {
- background-color: #c3defe !important;
-}
-.syntaxhighlighter .line.highlighted.number {
- color: white !important;
-}
-.syntaxhighlighter table caption {
- color: black !important;
-}
-.syntaxhighlighter .gutter {
- color: #787878 !important;
-}
-.syntaxhighlighter .gutter .line {
- border-right: 3px solid #d4d0c8 !important;
-}
-.syntaxhighlighter .gutter .line.highlighted {
- background-color: #d4d0c8 !important;
- color: white !important;
-}
-.syntaxhighlighter.printing .line .content {
- border: none !important;
-}
-.syntaxhighlighter.collapsed {
- overflow: visible !important;
-}
-.syntaxhighlighter.collapsed .toolbar {
- color: #3f5fbf !important;
- background: white !important;
- border: 1px solid #d4d0c8 !important;
-}
-.syntaxhighlighter.collapsed .toolbar a {
- color: #3f5fbf !important;
-}
-.syntaxhighlighter.collapsed .toolbar a:hover {
- color: #aa7700 !important;
-}
-.syntaxhighlighter .toolbar {
- color: #a0a0a0 !important;
- background: #d4d0c8 !important;
- border: none !important;
-}
-.syntaxhighlighter .toolbar a {
- color: #a0a0a0 !important;
-}
-.syntaxhighlighter .toolbar a:hover {
- color: red !important;
-}
-.syntaxhighlighter .plain, .syntaxhighlighter .plain a {
- color: black !important;
-}
-.syntaxhighlighter .comments, .syntaxhighlighter .comments a {
- color: #3f5fbf !important;
-}
-.syntaxhighlighter .string, .syntaxhighlighter .string a {
- color: #2a00ff !important;
-}
-.syntaxhighlighter .keyword {
- color: #7f0055 !important;
-}
-.syntaxhighlighter .preprocessor {
- color: #646464 !important;
-}
-.syntaxhighlighter .variable {
- color: #aa7700 !important;
-}
-.syntaxhighlighter .value {
- color: #009900 !important;
-}
-.syntaxhighlighter .functions {
- color: #ff1493 !important;
-}
-.syntaxhighlighter .constants {
- color: #0066cc !important;
-}
-.syntaxhighlighter .script {
- font-weight: bold !important;
- color: #7f0055 !important;
- background-color: none !important;
-}
-.syntaxhighlighter .color1, .syntaxhighlighter .color1 a {
- color: gray !important;
-}
-.syntaxhighlighter .color2, .syntaxhighlighter .color2 a {
- color: #ff1493 !important;
-}
-.syntaxhighlighter .color3, .syntaxhighlighter .color3 a {
- color: red !important;
-}
-
-.syntaxhighlighter .keyword {
- font-weight: bold !important;
-}
-.syntaxhighlighter .xml .keyword {
- color: #3f7f7f !important;
- font-weight: normal !important;
-}
-.syntaxhighlighter .xml .color1, .syntaxhighlighter .xml .color1 a {
- color: #7f007f !important;
-}
-.syntaxhighlighter .xml .string {
- font-style: italic !important;
- color: #2a00ff !important;
-}
diff --git a/guides/assets/stylesheets/syntaxhighlighter/shThemeEmacs.css b/guides/assets/stylesheets/syntaxhighlighter/shThemeEmacs.css
deleted file mode 100644
index dae5053fea..0000000000
--- a/guides/assets/stylesheets/syntaxhighlighter/shThemeEmacs.css
+++ /dev/null
@@ -1,113 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-.syntaxhighlighter {
- background-color: black !important;
-}
-.syntaxhighlighter .line.alt1 {
- background-color: black !important;
-}
-.syntaxhighlighter .line.alt2 {
- background-color: black !important;
-}
-.syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 {
- background-color: #2a3133 !important;
-}
-.syntaxhighlighter .line.highlighted.number {
- color: white !important;
-}
-.syntaxhighlighter table caption {
- color: #d3d3d3 !important;
-}
-.syntaxhighlighter .gutter {
- color: #d3d3d3 !important;
-}
-.syntaxhighlighter .gutter .line {
- border-right: 3px solid #990000 !important;
-}
-.syntaxhighlighter .gutter .line.highlighted {
- background-color: #990000 !important;
- color: black !important;
-}
-.syntaxhighlighter.printing .line .content {
- border: none !important;
-}
-.syntaxhighlighter.collapsed {
- overflow: visible !important;
-}
-.syntaxhighlighter.collapsed .toolbar {
- color: #ebdb8d !important;
- background: black !important;
- border: 1px solid #990000 !important;
-}
-.syntaxhighlighter.collapsed .toolbar a {
- color: #ebdb8d !important;
-}
-.syntaxhighlighter.collapsed .toolbar a:hover {
- color: #ff7d27 !important;
-}
-.syntaxhighlighter .toolbar {
- color: white !important;
- background: #990000 !important;
- border: none !important;
-}
-.syntaxhighlighter .toolbar a {
- color: white !important;
-}
-.syntaxhighlighter .toolbar a:hover {
- color: #9ccff4 !important;
-}
-.syntaxhighlighter .plain, .syntaxhighlighter .plain a {
- color: #d3d3d3 !important;
-}
-.syntaxhighlighter .comments, .syntaxhighlighter .comments a {
- color: #ff7d27 !important;
-}
-.syntaxhighlighter .string, .syntaxhighlighter .string a {
- color: #ff9e7b !important;
-}
-.syntaxhighlighter .keyword {
- color: aqua !important;
-}
-.syntaxhighlighter .preprocessor {
- color: #aec4de !important;
-}
-.syntaxhighlighter .variable {
- color: #ffaa3e !important;
-}
-.syntaxhighlighter .value {
- color: #009900 !important;
-}
-.syntaxhighlighter .functions {
- color: #81cef9 !important;
-}
-.syntaxhighlighter .constants {
- color: #ff9e7b !important;
-}
-.syntaxhighlighter .script {
- font-weight: bold !important;
- color: aqua !important;
- background-color: none !important;
-}
-.syntaxhighlighter .color1, .syntaxhighlighter .color1 a {
- color: #ebdb8d !important;
-}
-.syntaxhighlighter .color2, .syntaxhighlighter .color2 a {
- color: #ff7d27 !important;
-}
-.syntaxhighlighter .color3, .syntaxhighlighter .color3 a {
- color: #aec4de !important;
-}
diff --git a/guides/assets/stylesheets/syntaxhighlighter/shThemeFadeToGrey.css b/guides/assets/stylesheets/syntaxhighlighter/shThemeFadeToGrey.css
deleted file mode 100644
index 8fbd871fb5..0000000000
--- a/guides/assets/stylesheets/syntaxhighlighter/shThemeFadeToGrey.css
+++ /dev/null
@@ -1,117 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-.syntaxhighlighter {
- background-color: #121212 !important;
-}
-.syntaxhighlighter .line.alt1 {
- background-color: #121212 !important;
-}
-.syntaxhighlighter .line.alt2 {
- background-color: #121212 !important;
-}
-.syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 {
- background-color: #2c2c29 !important;
-}
-.syntaxhighlighter .line.highlighted.number {
- color: white !important;
-}
-.syntaxhighlighter table caption {
- color: white !important;
-}
-.syntaxhighlighter .gutter {
- color: #afafaf !important;
-}
-.syntaxhighlighter .gutter .line {
- border-right: 3px solid #3185b9 !important;
-}
-.syntaxhighlighter .gutter .line.highlighted {
- background-color: #3185b9 !important;
- color: #121212 !important;
-}
-.syntaxhighlighter.printing .line .content {
- border: none !important;
-}
-.syntaxhighlighter.collapsed {
- overflow: visible !important;
-}
-.syntaxhighlighter.collapsed .toolbar {
- color: #3185b9 !important;
- background: black !important;
- border: 1px solid #3185b9 !important;
-}
-.syntaxhighlighter.collapsed .toolbar a {
- color: #3185b9 !important;
-}
-.syntaxhighlighter.collapsed .toolbar a:hover {
- color: #d01d33 !important;
-}
-.syntaxhighlighter .toolbar {
- color: white !important;
- background: #3185b9 !important;
- border: none !important;
-}
-.syntaxhighlighter .toolbar a {
- color: white !important;
-}
-.syntaxhighlighter .toolbar a:hover {
- color: #96daff !important;
-}
-.syntaxhighlighter .plain, .syntaxhighlighter .plain a {
- color: white !important;
-}
-.syntaxhighlighter .comments, .syntaxhighlighter .comments a {
- color: #696854 !important;
-}
-.syntaxhighlighter .string, .syntaxhighlighter .string a {
- color: #e3e658 !important;
-}
-.syntaxhighlighter .keyword {
- color: #d01d33 !important;
-}
-.syntaxhighlighter .preprocessor {
- color: #435a5f !important;
-}
-.syntaxhighlighter .variable {
- color: #898989 !important;
-}
-.syntaxhighlighter .value {
- color: #009900 !important;
-}
-.syntaxhighlighter .functions {
- color: #aaaaaa !important;
-}
-.syntaxhighlighter .constants {
- color: #96daff !important;
-}
-.syntaxhighlighter .script {
- font-weight: bold !important;
- color: #d01d33 !important;
- background-color: none !important;
-}
-.syntaxhighlighter .color1, .syntaxhighlighter .color1 a {
- color: #ffc074 !important;
-}
-.syntaxhighlighter .color2, .syntaxhighlighter .color2 a {
- color: #4a8cdb !important;
-}
-.syntaxhighlighter .color3, .syntaxhighlighter .color3 a {
- color: #96daff !important;
-}
-
-.syntaxhighlighter .functions {
- font-weight: bold !important;
-}
diff --git a/guides/assets/stylesheets/syntaxhighlighter/shThemeMDUltra.css b/guides/assets/stylesheets/syntaxhighlighter/shThemeMDUltra.css
deleted file mode 100755
index f4db39cd83..0000000000
--- a/guides/assets/stylesheets/syntaxhighlighter/shThemeMDUltra.css
+++ /dev/null
@@ -1,113 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-.syntaxhighlighter {
- background-color: #222222 !important;
-}
-.syntaxhighlighter .line.alt1 {
- background-color: #222222 !important;
-}
-.syntaxhighlighter .line.alt2 {
- background-color: #222222 !important;
-}
-.syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 {
- background-color: #253e5a !important;
-}
-.syntaxhighlighter .line.highlighted.number {
- color: white !important;
-}
-.syntaxhighlighter table caption {
- color: lime !important;
-}
-.syntaxhighlighter .gutter {
- color: #38566f !important;
-}
-.syntaxhighlighter .gutter .line {
- border-right: 3px solid #435a5f !important;
-}
-.syntaxhighlighter .gutter .line.highlighted {
- background-color: #435a5f !important;
- color: #222222 !important;
-}
-.syntaxhighlighter.printing .line .content {
- border: none !important;
-}
-.syntaxhighlighter.collapsed {
- overflow: visible !important;
-}
-.syntaxhighlighter.collapsed .toolbar {
- color: #428bdd !important;
- background: black !important;
- border: 1px solid #435a5f !important;
-}
-.syntaxhighlighter.collapsed .toolbar a {
- color: #428bdd !important;
-}
-.syntaxhighlighter.collapsed .toolbar a:hover {
- color: lime !important;
-}
-.syntaxhighlighter .toolbar {
- color: #aaaaff !important;
- background: #435a5f !important;
- border: none !important;
-}
-.syntaxhighlighter .toolbar a {
- color: #aaaaff !important;
-}
-.syntaxhighlighter .toolbar a:hover {
- color: #9ccff4 !important;
-}
-.syntaxhighlighter .plain, .syntaxhighlighter .plain a {
- color: lime !important;
-}
-.syntaxhighlighter .comments, .syntaxhighlighter .comments a {
- color: #428bdd !important;
-}
-.syntaxhighlighter .string, .syntaxhighlighter .string a {
- color: lime !important;
-}
-.syntaxhighlighter .keyword {
- color: #aaaaff !important;
-}
-.syntaxhighlighter .preprocessor {
- color: #8aa6c1 !important;
-}
-.syntaxhighlighter .variable {
- color: aqua !important;
-}
-.syntaxhighlighter .value {
- color: #f7e741 !important;
-}
-.syntaxhighlighter .functions {
- color: #ff8000 !important;
-}
-.syntaxhighlighter .constants {
- color: yellow !important;
-}
-.syntaxhighlighter .script {
- font-weight: bold !important;
- color: #aaaaff !important;
- background-color: none !important;
-}
-.syntaxhighlighter .color1, .syntaxhighlighter .color1 a {
- color: red !important;
-}
-.syntaxhighlighter .color2, .syntaxhighlighter .color2 a {
- color: yellow !important;
-}
-.syntaxhighlighter .color3, .syntaxhighlighter .color3 a {
- color: #ffaa3e !important;
-}
diff --git a/guides/assets/stylesheets/syntaxhighlighter/shThemeMidnight.css b/guides/assets/stylesheets/syntaxhighlighter/shThemeMidnight.css
deleted file mode 100644
index c49563cc9d..0000000000
--- a/guides/assets/stylesheets/syntaxhighlighter/shThemeMidnight.css
+++ /dev/null
@@ -1,113 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-.syntaxhighlighter {
- background-color: #0f192a !important;
-}
-.syntaxhighlighter .line.alt1 {
- background-color: #0f192a !important;
-}
-.syntaxhighlighter .line.alt2 {
- background-color: #0f192a !important;
-}
-.syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 {
- background-color: #253e5a !important;
-}
-.syntaxhighlighter .line.highlighted.number {
- color: #38566f !important;
-}
-.syntaxhighlighter table caption {
- color: #d1edff !important;
-}
-.syntaxhighlighter .gutter {
- color: #afafaf !important;
-}
-.syntaxhighlighter .gutter .line {
- border-right: 3px solid #435a5f !important;
-}
-.syntaxhighlighter .gutter .line.highlighted {
- background-color: #435a5f !important;
- color: #0f192a !important;
-}
-.syntaxhighlighter.printing .line .content {
- border: none !important;
-}
-.syntaxhighlighter.collapsed {
- overflow: visible !important;
-}
-.syntaxhighlighter.collapsed .toolbar {
- color: #428bdd !important;
- background: black !important;
- border: 1px solid #435a5f !important;
-}
-.syntaxhighlighter.collapsed .toolbar a {
- color: #428bdd !important;
-}
-.syntaxhighlighter.collapsed .toolbar a:hover {
- color: #1dc116 !important;
-}
-.syntaxhighlighter .toolbar {
- color: #d1edff !important;
- background: #435a5f !important;
- border: none !important;
-}
-.syntaxhighlighter .toolbar a {
- color: #d1edff !important;
-}
-.syntaxhighlighter .toolbar a:hover {
- color: #8aa6c1 !important;
-}
-.syntaxhighlighter .plain, .syntaxhighlighter .plain a {
- color: #d1edff !important;
-}
-.syntaxhighlighter .comments, .syntaxhighlighter .comments a {
- color: #428bdd !important;
-}
-.syntaxhighlighter .string, .syntaxhighlighter .string a {
- color: #1dc116 !important;
-}
-.syntaxhighlighter .keyword {
- color: #b43d3d !important;
-}
-.syntaxhighlighter .preprocessor {
- color: #8aa6c1 !important;
-}
-.syntaxhighlighter .variable {
- color: #ffaa3e !important;
-}
-.syntaxhighlighter .value {
- color: #f7e741 !important;
-}
-.syntaxhighlighter .functions {
- color: #ffaa3e !important;
-}
-.syntaxhighlighter .constants {
- color: #e0e8ff !important;
-}
-.syntaxhighlighter .script {
- font-weight: bold !important;
- color: #b43d3d !important;
- background-color: none !important;
-}
-.syntaxhighlighter .color1, .syntaxhighlighter .color1 a {
- color: #f8bb00 !important;
-}
-.syntaxhighlighter .color2, .syntaxhighlighter .color2 a {
- color: white !important;
-}
-.syntaxhighlighter .color3, .syntaxhighlighter .color3 a {
- color: #ffaa3e !important;
-}
diff --git a/guides/assets/stylesheets/syntaxhighlighter/shThemeRDark.css b/guides/assets/stylesheets/syntaxhighlighter/shThemeRDark.css
deleted file mode 100644
index 6305a10e4e..0000000000
--- a/guides/assets/stylesheets/syntaxhighlighter/shThemeRDark.css
+++ /dev/null
@@ -1,113 +0,0 @@
-/**
- * SyntaxHighlighter
- * http://alexgorbatchev.com/SyntaxHighlighter
- *
- * SyntaxHighlighter is donationware. If you are using it, please donate.
- * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
- *
- * @version
- * 3.0.83 (July 02 2010)
- *
- * @copyright
- * Copyright (C) 2004-2010 Alex Gorbatchev.
- *
- * @license
- * Dual licensed under the MIT and GPL licenses.
- */
-.syntaxhighlighter {
- background-color: #1b2426 !important;
-}
-.syntaxhighlighter .line.alt1 {
- background-color: #1b2426 !important;
-}
-.syntaxhighlighter .line.alt2 {
- background-color: #1b2426 !important;
-}
-.syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 {
- background-color: #323e41 !important;
-}
-.syntaxhighlighter .line.highlighted.number {
- color: #b9bdb6 !important;
-}
-.syntaxhighlighter table caption {
- color: #b9bdb6 !important;
-}
-.syntaxhighlighter .gutter {
- color: #afafaf !important;
-}
-.syntaxhighlighter .gutter .line {
- border-right: 3px solid #435a5f !important;
-}
-.syntaxhighlighter .gutter .line.highlighted {
- background-color: #435a5f !important;
- color: #1b2426 !important;
-}
-.syntaxhighlighter.printing .line .content {
- border: none !important;
-}
-.syntaxhighlighter.collapsed {
- overflow: visible !important;
-}
-.syntaxhighlighter.collapsed .toolbar {
- color: #5ba1cf !important;
- background: black !important;
- border: 1px solid #435a5f !important;
-}
-.syntaxhighlighter.collapsed .toolbar a {
- color: #5ba1cf !important;
-}
-.syntaxhighlighter.collapsed .toolbar a:hover {
- color: #5ce638 !important;
-}
-.syntaxhighlighter .toolbar {
- color: white !important;
- background: #435a5f !important;
- border: none !important;
-}
-.syntaxhighlighter .toolbar a {
- color: white !important;
-}
-.syntaxhighlighter .toolbar a:hover {
- color: #e0e8ff !important;
-}
-.syntaxhighlighter .plain, .syntaxhighlighter .plain a {
- color: #b9bdb6 !important;
-}
-.syntaxhighlighter .comments, .syntaxhighlighter .comments a {
- color: #878a85 !important;
-}
-.syntaxhighlighter .string, .syntaxhighlighter .string a {
- color: #5ce638 !important;
-}
-.syntaxhighlighter .keyword {
- color: #5ba1cf !important;
-}
-.syntaxhighlighter .preprocessor {
- color: #435a5f !important;
-}
-.syntaxhighlighter .variable {
- color: #ffaa3e !important;
-}
-.syntaxhighlighter .value {
- color: #009900 !important;
-}
-.syntaxhighlighter .functions {
- color: #ffaa3e !important;
-}
-.syntaxhighlighter .constants {
- color: #e0e8ff !important;
-}
-.syntaxhighlighter .script {
- font-weight: bold !important;
- color: #5ba1cf !important;
- background-color: none !important;
-}
-.syntaxhighlighter .color1, .syntaxhighlighter .color1 a {
- color: #e0e8ff !important;
-}
-.syntaxhighlighter .color2, .syntaxhighlighter .color2 a {
- color: white !important;
-}
-.syntaxhighlighter .color3, .syntaxhighlighter .color3 a {
- color: #ffaa3e !important;
-}
diff --git a/guides/assets/stylesheets/syntaxhighlighter/shThemeRailsGuides.css b/guides/assets/stylesheets/syntaxhighlighter/shThemeRailsGuides.css
index 6d2edb2eb8..bc7afd3898 100644
--- a/guides/assets/stylesheets/syntaxhighlighter/shThemeRailsGuides.css
+++ b/guides/assets/stylesheets/syntaxhighlighter/shThemeRailsGuides.css
@@ -90,7 +90,7 @@
}
.syntaxhighlighter .script {
color: #222 !important;
- background-color: none !important;
+ background-color: transparent !important;
}
.syntaxhighlighter .color1, .syntaxhighlighter .color1 a {
color: gray !important;
diff --git a/guides/assets/stylesheets/turbolinks.css b/guides/assets/stylesheets/turbolinks.css
new file mode 100644
index 0000000000..5cb598cef2
--- /dev/null
+++ b/guides/assets/stylesheets/turbolinks.css
@@ -0,0 +1,3 @@
+.turbolinks-progress-bar {
+ background-color: #c52f24;
+}
diff --git a/guides/bug_report_templates/action_controller_gem.rb b/guides/bug_report_templates/action_controller_gem.rb
index 11561c55f9..6c74200761 100644
--- a/guides/bug_report_templates/action_controller_gem.rb
+++ b/guides/bug_report_templates/action_controller_gem.rb
@@ -1,30 +1,29 @@
-begin
- require 'bundler/inline'
-rescue LoadError => e
- $stderr.puts 'Bundler version 1.10 or later is required. Please update your Bundler'
- raise e
-end
+# frozen_string_literal: true
+
+require "bundler/inline"
gemfile(true) do
- source 'https://rubygems.org'
+ source "https://rubygems.org"
+
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
+
# Activate the gem you are reporting the issue against.
- gem 'rails', '4.2.0'
+ gem "rails", "5.2.0"
end
-require 'rack/test'
-require 'action_controller/railtie'
+require "rack/test"
+require "action_controller/railtie"
class TestApp < Rails::Application
- config.root = File.dirname(__FILE__)
- config.session_store :cookie_store, key: 'cookie_store_key'
- secrets.secret_token = 'secret_token'
- secrets.secret_key_base = 'secret_key_base'
+ config.root = __dir__
+ config.session_store :cookie_store, key: "cookie_store_key"
+ secrets.secret_key_base = "secret_key_base"
config.logger = Logger.new($stdout)
Rails.logger = config.logger
routes.draw do
- get '/' => 'test#index'
+ get "/" => "test#index"
end
end
@@ -32,20 +31,17 @@ class TestController < ActionController::Base
include Rails.application.routes.url_helpers
def index
- render text: 'Home'
+ render plain: "Home"
end
end
-require 'minitest/autorun'
-
-# Ensure backward compatibility with Minitest 4
-Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test)
+require "minitest/autorun"
class BugTest < Minitest::Test
include Rack::Test::Methods
def test_returns_success
- get '/'
+ get "/"
assert last_response.ok?
end
diff --git a/guides/bug_report_templates/action_controller_master.rb b/guides/bug_report_templates/action_controller_master.rb
index 66887398b9..6d53e957d9 100644
--- a/guides/bug_report_templates/action_controller_master.rb
+++ b/guides/bug_report_templates/action_controller_master.rb
@@ -1,29 +1,26 @@
-begin
- require 'bundler/inline'
-rescue LoadError => e
- $stderr.puts 'Bundler version 1.10 or later is required. Please update your Bundler'
- raise e
-end
+# frozen_string_literal: true
+
+require "bundler/inline"
gemfile(true) do
- source 'https://rubygems.org'
- gem 'rails', github: 'rails/rails'
- gem 'arel', github: 'rails/arel'
+ source "https://rubygems.org"
+
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
+
+ gem "rails", github: "rails/rails"
end
-require 'action_controller/railtie'
+require "action_controller/railtie"
class TestApp < Rails::Application
- config.root = File.dirname(__FILE__)
- config.session_store :cookie_store, key: 'cookie_store_key'
- secrets.secret_token = 'secret_token'
- secrets.secret_key_base = 'secret_key_base'
+ config.root = __dir__
+ secrets.secret_key_base = "secret_key_base"
config.logger = Logger.new($stdout)
Rails.logger = config.logger
routes.draw do
- get '/' => 'test#index'
+ get "/" => "test#index"
end
end
@@ -31,18 +28,18 @@ class TestController < ActionController::Base
include Rails.application.routes.url_helpers
def index
- render text: 'Home'
+ render plain: "Home"
end
end
-require 'minitest/autorun'
-require 'rack/test'
+require "minitest/autorun"
+require "rack/test"
class BugTest < Minitest::Test
include Rack::Test::Methods
def test_returns_success
- get '/'
+ get "/"
assert last_response.ok?
end
diff --git a/guides/bug_report_templates/active_job_gem.rb b/guides/bug_report_templates/active_job_gem.rb
new file mode 100644
index 0000000000..eb9d1316e9
--- /dev/null
+++ b/guides/bug_report_templates/active_job_gem.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require "bundler/inline"
+
+gemfile(true) do
+ source "https://rubygems.org"
+
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
+
+ # Activate the gem you are reporting the issue against.
+ gem "activejob", "5.2.0"
+end
+
+require "minitest/autorun"
+require "active_job"
+
+class BuggyJob < ActiveJob::Base
+ def perform
+ puts "performed"
+ end
+end
+
+class BuggyJobTest < ActiveJob::TestCase
+ def test_stuff
+ assert_enqueued_with(job: BuggyJob) do
+ BuggyJob.perform_later
+ end
+ end
+end
diff --git a/guides/bug_report_templates/active_job_master.rb b/guides/bug_report_templates/active_job_master.rb
new file mode 100644
index 0000000000..ae3ef7752f
--- /dev/null
+++ b/guides/bug_report_templates/active_job_master.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require "bundler/inline"
+
+gemfile(true) do
+ source "https://rubygems.org"
+
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
+
+ gem "rails", github: "rails/rails"
+end
+
+require "active_job"
+require "minitest/autorun"
+
+class BuggyJob < ActiveJob::Base
+ def perform
+ puts "performed"
+ end
+end
+
+class BuggyJobTest < ActiveJob::TestCase
+ def test_stuff
+ assert_enqueued_with(job: BuggyJob) do
+ BuggyJob.perform_later
+ end
+ end
+end
diff --git a/guides/bug_report_templates/active_record_gem.rb b/guides/bug_report_templates/active_record_gem.rb
index 09d6e7b331..d88304a219 100644
--- a/guides/bug_report_templates/active_record_gem.rb
+++ b/guides/bug_report_templates/active_record_gem.rb
@@ -1,26 +1,23 @@
-begin
- require 'bundler/inline'
-rescue LoadError => e
- $stderr.puts 'Bundler version 1.10 or later is required. Please update your Bundler'
- raise e
-end
+# frozen_string_literal: true
+
+require "bundler/inline"
gemfile(true) do
- source 'https://rubygems.org'
+ source "https://rubygems.org"
+
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
+
# Activate the gem you are reporting the issue against.
- gem 'activerecord', '4.2.0'
- gem 'sqlite3'
+ gem "activerecord", "5.2.0"
+ gem "sqlite3"
end
-require 'active_record'
-require 'minitest/autorun'
-require 'logger'
-
-# Ensure backward compatibility with Minitest 4
-Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test)
+require "active_record"
+require "minitest/autorun"
+require "logger"
# This connection will do for database-independent bug reports.
-ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
+ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Schema.define do
diff --git a/guides/bug_report_templates/active_record_master.rb b/guides/bug_report_templates/active_record_master.rb
index 270dbe7df7..780456b7b6 100644
--- a/guides/bug_report_templates/active_record_master.rb
+++ b/guides/bug_report_templates/active_record_master.rb
@@ -1,30 +1,29 @@
-begin
- require 'bundler/inline'
-rescue LoadError => e
- $stderr.puts 'Bundler version 1.10 or later is required. Please update your Bundler'
- raise e
-end
+# frozen_string_literal: true
+
+require "bundler/inline"
gemfile(true) do
- source 'https://rubygems.org'
- gem 'rails', github: 'rails/rails'
- gem 'arel', github: 'rails/arel'
- gem 'sqlite3'
+ source "https://rubygems.org"
+
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
+
+ gem "rails", github: "rails/rails"
+ gem "sqlite3"
end
-require 'active_record'
-require 'minitest/autorun'
-require 'logger'
+require "active_record"
+require "minitest/autorun"
+require "logger"
# This connection will do for database-independent bug reports.
-ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
+ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Schema.define do
- create_table :posts, force: true do |t|
+ create_table :posts, force: true do |t|
end
- create_table :comments, force: true do |t|
+ create_table :comments, force: true do |t|
t.integer :post_id
end
end
diff --git a/guides/bug_report_templates/active_record_migrations_gem.rb b/guides/bug_report_templates/active_record_migrations_gem.rb
new file mode 100644
index 0000000000..5dfd49fb38
--- /dev/null
+++ b/guides/bug_report_templates/active_record_migrations_gem.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require "bundler/inline"
+
+gemfile(true) do
+ source "https://rubygems.org"
+
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
+
+ # Activate the gem you are reporting the issue against.
+ gem "activerecord", "5.2.0"
+ gem "sqlite3"
+end
+
+require "active_record"
+require "minitest/autorun"
+require "logger"
+
+# This connection will do for database-independent bug reports.
+ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
+ActiveRecord::Base.logger = Logger.new(STDOUT)
+
+ActiveRecord::Schema.define do
+ create_table :payments, force: true do |t|
+ t.decimal :amount, precision: 10, scale: 0, default: 0, null: false
+ end
+end
+
+class Payment < ActiveRecord::Base
+end
+
+class ChangeAmountToAddScale < ActiveRecord::Migration[5.2]
+ def change
+ reversible do |dir|
+ dir.up do
+ change_column :payments, :amount, :decimal, precision: 10, scale: 2, default: 0, null: false
+ end
+
+ dir.down do
+ change_column :payments, :amount, :decimal, precision: 10, scale: 0, default: 0, null: false
+ end
+ end
+ end
+end
+
+class BugTest < Minitest::Test
+ def test_migration_up
+ ChangeAmountToAddScale.migrate(:up)
+ Payment.reset_column_information
+
+ assert_equal "decimal(10,2)", Payment.columns.last.sql_type
+ end
+
+ def test_migration_down
+ ChangeAmountToAddScale.migrate(:down)
+ Payment.reset_column_information
+
+ assert_equal "decimal(10,0)", Payment.columns.last.sql_type
+ end
+end
diff --git a/guides/bug_report_templates/active_record_migrations_master.rb b/guides/bug_report_templates/active_record_migrations_master.rb
new file mode 100644
index 0000000000..b0fe3bc660
--- /dev/null
+++ b/guides/bug_report_templates/active_record_migrations_master.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require "bundler/inline"
+
+gemfile(true) do
+ source "https://rubygems.org"
+
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
+
+ gem "rails", github: "rails/rails"
+ gem "sqlite3"
+end
+
+require "active_record"
+require "minitest/autorun"
+require "logger"
+
+# This connection will do for database-independent bug reports.
+ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
+ActiveRecord::Base.logger = Logger.new(STDOUT)
+
+ActiveRecord::Schema.define do
+ create_table :payments, force: true do |t|
+ t.decimal :amount, precision: 10, scale: 0, default: 0, null: false
+ end
+end
+
+class Payment < ActiveRecord::Base
+end
+
+class ChangeAmountToAddScale < ActiveRecord::Migration[6.0]
+ def change
+ reversible do |dir|
+ dir.up do
+ change_column :payments, :amount, :decimal, precision: 10, scale: 2, default: 0, null: false
+ end
+
+ dir.down do
+ change_column :payments, :amount, :decimal, precision: 10, scale: 0, default: 0, null: false
+ end
+ end
+ end
+end
+
+class BugTest < Minitest::Test
+ def test_migration_up
+ ChangeAmountToAddScale.migrate(:up)
+ Payment.reset_column_information
+
+ assert_equal "decimal(10,2)", Payment.columns.last.sql_type
+ end
+
+ def test_migration_down
+ ChangeAmountToAddScale.migrate(:down)
+ Payment.reset_column_information
+
+ assert_equal "decimal(10,0)", Payment.columns.last.sql_type
+ end
+end
diff --git a/guides/bug_report_templates/benchmark.rb b/guides/bug_report_templates/benchmark.rb
new file mode 100644
index 0000000000..4a8ce787c7
--- /dev/null
+++ b/guides/bug_report_templates/benchmark.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require "bundler/inline"
+
+gemfile(true) do
+ source "https://rubygems.org"
+
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
+
+ gem "rails", github: "rails/rails"
+ gem "benchmark-ips"
+end
+
+require "active_support"
+require "active_support/core_ext/object/blank"
+
+# Your patch goes here.
+class String
+ def fast_blank?
+ true
+ end
+end
+
+# Enumerate some representative scenarios here.
+#
+# It is very easy to make an optimization that improves performance for a
+# specific scenario you care about but regresses on other common cases.
+# Therefore, you should test your change against a list of representative
+# scenarios. Ideally, they should be based on real-world scenarios extracted
+# from production applications.
+SCENARIOS = {
+ "Empty" => "",
+ "Single Space" => " ",
+ "Two Spaces" => " ",
+ "Mixed Whitspaces" => " \t\r\n",
+ "Very Long String" => " " * 100
+}
+
+SCENARIOS.each_pair do |name, value|
+ puts
+ puts " #{name} ".center(80, "=")
+ puts
+
+ Benchmark.ips do |x|
+ x.report("blank?") { value.blank? }
+ x.report("fast_blank?") { value.fast_blank? }
+ x.compare!
+ end
+end
diff --git a/guides/bug_report_templates/generic_gem.rb b/guides/bug_report_templates/generic_gem.rb
index a4fe51156d..3fd54437f7 100644
--- a/guides/bug_report_templates/generic_gem.rb
+++ b/guides/bug_report_templates/generic_gem.rb
@@ -1,21 +1,19 @@
-begin
- require 'bundler/inline'
-rescue LoadError => e
- $stderr.puts 'Bundler version 1.10 or later is required. Please update your Bundler'
- raise e
-end
+# frozen_string_literal: true
+
+require "bundler/inline"
gemfile(true) do
- source 'https://rubygems.org'
+ source "https://rubygems.org"
+
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
+
# Activate the gem you are reporting the issue against.
- gem 'activesupport', '4.2.0'
+ gem "activesupport", "5.2.0"
end
-require 'active_support/core_ext/object/blank'
-require 'minitest/autorun'
-
-# Ensure backward compatibility with Minitest 4
-Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test)
+require "active_support"
+require "active_support/core_ext/object/blank"
+require "minitest/autorun"
class BugTest < Minitest::Test
def test_stuff
diff --git a/guides/bug_report_templates/generic_master.rb b/guides/bug_report_templates/generic_master.rb
index b6b4562751..ec65fee292 100644
--- a/guides/bug_report_templates/generic_master.rb
+++ b/guides/bug_report_templates/generic_master.rb
@@ -1,22 +1,18 @@
-begin
- require 'bundler/inline'
-rescue LoadError => e
- $stderr.puts 'Bundler version 1.10 or later is required. Please update your Bundler'
- raise e
-end
+# frozen_string_literal: true
+
+require "bundler/inline"
gemfile(true) do
- source 'https://rubygems.org'
- gem 'rails', github: 'rails/rails'
- gem 'arel', github: 'rails/arel'
-end
+ source "https://rubygems.org"
-require 'active_support'
-require 'active_support/core_ext/object/blank'
-require 'minitest/autorun'
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
+
+ gem "rails", github: "rails/rails"
+end
-# Ensure backward compatibility with Minitest 4
-Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test)
+require "active_support"
+require "active_support/core_ext/object/blank"
+require "minitest/autorun"
class BugTest < Minitest::Test
def test_stuff
diff --git a/guides/rails_guides.rb b/guides/rails_guides.rb
index 367ed0b12e..b24c0205d5 100644
--- a/guides/rails_guides.rb
+++ b/guides/rails_guides.rb
@@ -1,17 +1,30 @@
-pwd = File.dirname(__FILE__)
-$:.unshift pwd
+# frozen_string_literal: true
-begin
- # Guides generation in the Rails repo.
- as_lib = File.join(pwd, "../activesupport/lib")
- ap_lib = File.join(pwd, "../actionpack/lib")
+$:.unshift __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
+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)
+$:.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"],
+ rtl: env_value["RTL"]
+).generate
diff --git a/guides/rails_guides/generator.rb b/guides/rails_guides/generator.rb
index 43f6f7eecf..3d8f54ab34 100644
--- a/guides/rails_guides/generator.rb
+++ b/guides/rails_guides/generator.rb
@@ -1,249 +1,216 @@
-# ---------------------------------------------------------------------------
-#
-# 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 ruby rails_guides.rb
-#
-# Separate many using commas:
-#
-# # generates only association_basics.html and migrations.html
-# ONLY=assoc,migrations ruby rails_guides.rb
-#
-# 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'
-
-require 'active_support/core_ext/string/output_safety'
-require 'active_support/core_ext/object/blank'
-require 'action_controller'
-require 'action_view'
-
-require 'rails_guides/markdown'
-require 'rails_guides/indexer'
-require 'rails_guides/helpers'
-require 'rails_guides/levenshtein'
+# frozen_string_literal: true
+
+require "set"
+require "fileutils"
+
+require "active_support/core_ext/string/output_safety"
+require "active_support/core_ext/object/blank"
+require "action_controller"
+require "action_view"
+
+require "rails_guides/markdown"
+require "rails_guides/indexer"
+require "rails_guides/helpers"
+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:, rtl: false)
+ @edge = edge
+ @version = version
+ @all = all
+ @only = only
+ @kindle = kindle
+ @language = language
+ @rtl = rtl
- 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
- end
+ 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
- if `which kindlegen`.blank?
- raise "Can't create a kindle version without `kindlegen`."
+ def check_for_kindlegen
+ if `which kindlegen`.blank?
+ raise "Can't create a kindle version without `kindlegen`."
+ end
end
- end
- def generate_mobi
- require 'rails_guides/kindle'
- out = "#{output_dir}/kindlegen.out"
- Kindle.generate(output_dir, mobi, out)
- puts "(kindlegen log at #{out})."
- end
+ def generate_mobi
+ require "rails_guides/kindle"
+ 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" : '')
- end
+ def mobi
+ 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</$>, '')
- end
+ def initialize_dirs
+ @guides_dir = File.expand_path("..", __dir__)
- def create_output_dir_if_needed
- FileUtils.mkdir_p(output_dir)
- end
+ @source_dir = "#{@guides_dir}/source"
+ @source_dir += "/#{@language}" if @language
- def generate_guides
- guides_to_generate.each do |guide|
- output_file = output_file_for(guide)
- generate_guide(guide, output_file) if generate?(guide, output_file)
+ @output_dir = "#{@guides_dir}/output"
+ @output_dir += "/kindle" if @kindle
+ @output_dir += "/#{@language}" if @language
end
- end
- def guides_to_generate
- guides = Dir.entries(source_dir).grep(GUIDES_RE)
+ def create_output_dir_if_needed
+ FileUtils.mkdir_p(@output_dir)
+ end
+
+ def initialize_markdown_renderer
+ Markdown::Renderer.edge = @edge
+ Markdown::Renderer.version = @version
+ end
- if kindle?
- Dir.entries("#{source_dir}/kindle").grep(GUIDES_RE).map do |entry|
- next if entry == 'KINDLE.md'
- guides << "kindle/#{entry}"
+ def generate_guides
+ guides_to_generate.each do |guide|
+ output_file = output_file_for(guide)
+ generate_guide(guide, output_file) if generate?(guide, output_file)
end
end
- ENV.key?('ONLY') ? select_only(guides) : guides
- end
+ def guides_to_generate
+ guides = Dir.entries(@source_dir).grep(GUIDES_RE)
+
+ if @kindle
+ Dir.entries("#{@source_dir}/kindle").grep(GUIDES_RE).map do |entry|
+ next if entry == "KINDLE.md"
+ guides << "kindle/#{entry}"
+ end
+ end
- def select_only(guides)
- prefixes = ENV['ONLY'].split(",").map(&:strip)
- guides.select do |guide|
- prefixes.any? { |p| guide.start_with?(p) || guide.start_with?("kindle") }
+ @only ? select_only(guides) : guides
end
- end
- def copy_assets
- FileUtils.cp_r(Dir.glob("#{guides_dir}/assets/*"), output_dir)
- end
+ def select_only(guides)
+ prefixes = @only.split(",").map(&:strip)
+ guides.select do |guide|
+ guide.start_with?("kindle", *prefixes)
+ end
+ end
- def output_file_for(guide)
- if guide.end_with?('.md')
- guide.sub(/md\z/, 'html')
- else
- guide.sub(/\.erb\z/, '')
+ def copy_assets
+ FileUtils.cp_r(Dir.glob("#{@guides_dir}/assets/*"), @output_dir)
+
+ if @rtl
+ FileUtils.rm(Dir.glob("#{@output_dir}/stylesheets/main.css"))
+ FileUtils.mv("#{@output_dir}/stylesheets/main.rtl.css", "#{@output_dir}/stylesheets/main.css")
+ end
end
- end
- def output_path_for(output_file)
- File.join(output_dir, File.basename(output_file))
- end
+ def output_file_for(guide)
+ if guide.end_with?(".md")
+ guide.sub(/md\z/, "html")
+ else
+ guide.sub(/\.erb\z/, "")
+ end
+ end
- def generate?(source_file, output_file)
- fin = File.join(source_dir, source_file)
- fout = output_path_for(output_file)
- all || !File.exist?(fout) || File.mtime(fout) < File.mtime(fin)
- end
+ def output_path_for(output_file)
+ File.join(@output_dir, File.basename(output_file))
+ 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'
+ def generate?(source_file, output_file)
+ fin = File.join(@source_dir, source_file)
+ fout = output_path_for(output_file)
+ @all || !File.exist?(fout) || File.mtime(fout) < File.mtime(fin)
+ end
- File.open(output_path, 'w') do |f|
- view = ActionView::Base.new(source_dir, :edge => @edge, :version => @version, :mobi => "kindle/#{mobi}")
+ def generate_guide(guide, output_file)
+ output_path = output_path_for(output_file)
+ puts "Generating #{guide} as #{output_file}"
+ layout = @kindle ? "kindle/layout" : "layout"
+
+ view = ActionView::Base.new(
+ @source_dir,
+ edge: @edge,
+ version: @version,
+ mobi: "kindle/#{mobi}",
+ language: @language
+ )
view.extend(Helpers)
if guide =~ /\.(\w+)\.erb$/
+ return if %w[_license _welcome layout].include?($`)
+
# Generate the special pages like the home.
# Passing a template handler in the template name is deprecated. So pass the file name without the extension.
- result = view.render(:layout => layout, :formats => [$1], :file => $`)
+ 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)
+ File.open(output_path, "w") do |f|
+ f.write(result)
+ end
end
- end
- def warn_about_broken_links(html)
- anchors = extract_anchors(html)
- check_fragment_identifiers(html, anchors)
- end
+ def warn_about_broken_links(html)
+ anchors = extract_anchors(html)
+ check_fragment_identifiers(html, anchors)
+ end
- def extract_anchors(html)
- # Markdown generates headers with IDs computed from titles.
- anchors = Set.new
- html.scan(/<h\d\s+id="([^"]+)/).flatten.each do |anchor|
- if anchors.member?(anchor)
- puts "*** DUPLICATE ID: #{anchor}, please make sure that there're no headings with the same name at the same level."
- else
- anchors << anchor
+ def extract_anchors(html)
+ # Markdown generates headers with IDs computed from titles.
+ anchors = Set.new
+ html.scan(/<h\d\s+id="([^"]+)/).flatten.each do |anchor|
+ if anchors.member?(anchor)
+ puts "*** DUPLICATE ID: #{anchor}, please make sure that there're no headings with the same name at the same level."
+ else
+ anchors << anchor
+ end
end
- end
- # 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
- end
+ # 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)
+ anchors
+ end
- def check_fragment_identifiers(html, anchors)
- html.scan(/<a\s+href="#([^"]+)/).flatten.each do |fragment_identifier|
- next if fragment_identifier == 'mainCol' # in layout, jumps to some DIV
- unless anchors.member?(fragment_identifier)
- guess = anchors.min { |a, b|
- Levenshtein.distance(fragment_identifier, a) <=> Levenshtein.distance(fragment_identifier, b)
- }
- puts "*** BROKEN LINK: ##{fragment_identifier}, perhaps you meant ##{guess}."
+ def check_fragment_identifiers(html, anchors)
+ html.scan(/<a\s+href="#([^"]+)/).flatten.each do |fragment_identifier|
+ next if fragment_identifier == "mainCol" # in layout, jumps to some DIV
+ unless anchors.member?(CGI.unescape(fragment_identifier))
+ guess = anchors.min { |a, b|
+ Levenshtein.distance(fragment_identifier, a) <=> Levenshtein.distance(fragment_identifier, b)
+ }
+ puts "*** BROKEN LINK: ##{fragment_identifier}, perhaps you meant ##{guess}."
+ end
end
end
- end
end
end
diff --git a/guides/rails_guides/helpers.rb b/guides/rails_guides/helpers.rb
index a78c2e9fca..5ab1388c29 100644
--- a/guides/rails_guides/helpers.rb
+++ b/guides/rails_guides/helpers.rb
@@ -1,13 +1,15 @@
-require 'yaml'
+# frozen_string_literal: true
+
+require "yaml"
module RailsGuides
module Helpers
def guide(name, url, options = {}, &block)
- link = content_tag(:a, :href => url) { name }
+ link = content_tag(:a, href: url) { name }
result = content_tag(:dt, link)
if options[:work_in_progress]
- result << content_tag(:dd, 'Work in progress', :class => 'work-in-progress')
+ result << content_tag(:dd, "Work in progress", class: "work-in-progress")
end
result << content_tag(:dd, capture(&block))
@@ -15,36 +17,27 @@ module RailsGuides
end
def documents_by_section
- @documents_by_section ||= YAML.load_file(File.expand_path('../../source/documents.yaml', __FILE__))
+ @documents_by_section ||= YAML.load_file(File.expand_path("../source/#{@language ? @language + '/' : ''}documents.yaml", __dir__))
end
def documents_flat
- documents_by_section.flat_map {|section| section['documents']}
+ documents_by_section.flat_map { |section| section["documents"] }
end
def finished_documents(documents)
- documents.reject { |document| document['work_in_progress'] }
+ documents.reject { |document| document["work_in_progress"] }
end
- def docs_for_menu(position=nil)
+ def docs_for_menu(position = nil)
if position.nil?
documents_by_section
- elsif position == 'L'
+ elsif position == "L"
documents_by_section.to(3)
else
documents_by_section.from(4)
end
end
- def author(name, nick, image = 'credits_pic_blank.gif', &block)
- image = "images/#{image}"
-
- result = tag(:img, :src => image, :class => 'left pic', :alt => name, :width => 91, :height => 91)
- result << content_tag(:h3, name)
- result << content_tag(:p, capture(&block))
- content_tag(:div, result, :class => 'clearfix', :id => nick)
- end
-
def code(&block)
c = capture(&block)
content_tag(:code, c)
diff --git a/guides/rails_guides/indexer.rb b/guides/rails_guides/indexer.rb
index 89fbccbb1d..c707464cdf 100644
--- a/guides/rails_guides/indexer.rb
+++ b/guides/rails_guides/indexer.rb
@@ -1,5 +1,7 @@
-require 'active_support/core_ext/object/blank'
-require 'active_support/core_ext/string/inflections'
+# frozen_string_literal: true
+
+require "active_support/core_ext/object/blank"
+require "active_support/core_ext/string/inflections"
module RailsGuides
class Indexer
@@ -17,52 +19,52 @@ module RailsGuides
private
- def process(string, current_level=3, counters=[1])
- s = StringScanner.new(string)
+ def process(string, current_level = 3, counters = [1])
+ s = StringScanner.new(string)
- level_hash = {}
+ level_hash = {}
- while !s.eos?
- re = %r{^h(\d)(?:\((#.*?)\))?\s*\.\s*(.*)$}
- s.match?(re)
- if matched = s.matched
- matched =~ re
- level, idx, title = $1.to_i, $2, $3.strip
+ while !s.eos?
+ re = %r{^h(\d)(?:\((#.*?)\))?\s*\.\s*(.*)$}
+ s.match?(re)
+ if matched = s.matched
+ matched =~ re
+ level, idx, title = $1.to_i, $2, $3.strip
- if level < current_level
- # This is needed. Go figure.
- return level_hash
- elsif level == current_level
- index = counters.join(".")
- idx ||= '#' + title_to_idx(title)
+ if level < current_level
+ # This is needed. Go figure.
+ return level_hash
+ elsif level == current_level
+ index = counters.join(".")
+ idx ||= "#" + title_to_idx(title)
- raise "Parsing Fail" unless @result.sub!(matched, "h#{level}(#{idx}). #{index} #{title}")
+ raise "Parsing Fail" unless @result.sub!(matched, "h#{level}(#{idx}). #{index} #{title}")
- key = {
- :title => title,
- :id => idx
- }
- # Recurse
- counters << 1
- level_hash[key] = process(s.post_match, current_level + 1, counters)
- counters.pop
+ key = {
+ title: title,
+ id: idx
+ }
+ # Recurse
+ counters << 1
+ level_hash[key] = process(s.post_match, current_level + 1, counters)
+ counters.pop
- # Increment the current level
- last = counters.pop
- counters << last + 1
+ # Increment the current level
+ last = counters.pop
+ counters << last + 1
+ end
end
+ s.getch
end
- s.getch
+ level_hash
end
- level_hash
- end
- def title_to_idx(title)
- idx = title.strip.parameterize.sub(/^\d+/, '')
- if warnings && idx.blank?
- puts "BLANK ID: please put an explicit ID for section #{title}, as in h5(#my-id)"
+ def title_to_idx(title)
+ idx = title.strip.parameterize.sub(/^\d+/, "")
+ if warnings && idx.blank?
+ puts "BLANK ID: please put an explicit ID for section #{title}, as in h5(#my-id)"
+ end
+ idx
end
- idx
- end
end
end
diff --git a/guides/rails_guides/kindle.rb b/guides/rails_guides/kindle.rb
index 32926622e3..8a0361ff4c 100644
--- a/guides/rails_guides/kindle.rb
+++ b/guides/rails_guides/kindle.rb
@@ -1,84 +1,81 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
-unless `which kindlerb`
- abort "Please gem install kindlerb"
-end
-
-require 'nokogiri'
-require 'fileutils'
-require 'yaml'
-require 'date'
+require "kindlerb"
+require "nokogiri"
+require "fileutils"
+require "yaml"
+require "date"
module Kindle
extend self
def generate(output_dir, mobi_outfile, logfile)
output_dir = File.absolute_path(output_dir)
- Dir.chdir output_dir do
+ Dir.chdir output_dir do
puts "=> Using output dir: #{output_dir}"
puts "=> Arranging html pages in document order"
toc = File.read("toc.ncx")
- doc = Nokogiri::XML(toc).xpath("//ncx:content", 'ncx' => "http://www.daisy.org/z3986/2005/ncx/")
- html_pages = doc.select {|c| c[:src]}.map {|c| c[:src]}.uniq
-
+ doc = Nokogiri::XML(toc).xpath("//ncx:content", "ncx" => "http://www.daisy.org/z3986/2005/ncx/")
+ html_pages = doc.select { |c| c[:src] }.map { |c| c[:src] }.uniq
+
generate_front_matter(html_pages)
generate_sections(html_pages)
generate_document_metadata(mobi_outfile)
- puts "Creating MOBI document with kindlegen. This make 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)}"
+ puts "Creating MOBI document with kindlegen. This may take a while."
+ if Kindlerb.run(output_dir)
+ puts "MOBI document generated at #{File.expand_path(mobi_outfile, output_dir)}"
+ end
end
end
def generate_front_matter(html_pages)
frontmatter = []
- html_pages.delete_if {|x|
- if x =~ /(toc|welcome|credits|copyright).html/
+ html_pages.delete_if { |x|
+ if /(toc|welcome|copyright).html/.match?(x)
frontmatter << x unless x =~ /toc/
true
end
}
- html = frontmatter.map {|x|
+ html = frontmatter.map { |x|
Nokogiri::HTML(File.open(x)).at("body").inner_html
}.join("\n")
fdoc = Nokogiri::HTML(html)
fdoc.search("h3").each do |h3|
- h3.name = 'h4'
+ h3.name = "h4"
end
- fdoc.search("h2").each do |h2|
- h2.name = 'h3'
- h2['id'] = h2.inner_text.gsub(/\s/, '-')
+ fdoc.search("h2").each do |h2|
+ h2.name = "h3"
+ h2["id"] = h2.inner_text.gsub(/\s/, "-")
end
add_head_section fdoc, "Front Matter"
- File.open("frontmatter.html",'w') {|f| f.puts fdoc.to_html}
+ File.open("frontmatter.html", "w") { |f| f.puts fdoc.to_html }
html_pages.unshift "frontmatter.html"
end
def generate_sections(html_pages)
- FileUtils::rm_rf("sections/")
+ FileUtils.rm_rf("sections/")
html_pages.each_with_index do |page, section_idx|
- FileUtils::mkdir_p("sections/%03d" % section_idx)
+ FileUtils.mkdir_p("sections/%03d" % section_idx)
doc = Nokogiri::HTML(File.open(page))
- title = doc.at("title").inner_text.gsub("Ruby on Rails Guides: ", '')
- title = page.capitalize.gsub('.html', '') if title.strip == ''
- File.open("sections/%03d/_section.txt" % section_idx, 'w') {|f| f.puts title}
- doc.xpath("//h3[@id]").each_with_index do |h3,item_idx|
+ title = doc.at("title").inner_text.gsub("Ruby on Rails Guides: ", "")
+ title = page.capitalize.gsub(".html", "") if title.strip == ""
+ File.open("sections/%03d/_section.txt" % section_idx, "w") { |f| f.puts title }
+ doc.xpath("//h3[@id]").each_with_index do |h3, item_idx|
subsection = h3.inner_text
- content = h3.xpath("./following-sibling::*").take_while {|x| x.name != "h3"}.map(&:to_html)
+ content = h3.xpath("./following-sibling::*").take_while { |x| x.name != "h3" }.map(&:to_html)
item = Nokogiri::HTML(h3.to_html + content.join("\n"))
- item_path = "sections/%03d/%03d.html" % [section_idx, item_idx]
+ item_path = "sections/%03d/%03d.html" % [section_idx, item_idx]
add_head_section(item, subsection)
item.search("img").each do |img|
- img['src'] = "#{Dir.pwd}/#{img['src']}"
+ img["src"] = "#{Dir.pwd}/#{img['src']}"
end
- item.xpath("//li/p").each {|p| p.swap(p.children); p.remove}
- File.open(item_path, 'w') {|f| f.puts item.to_html}
+ item.xpath("//li/p").each { |p| p.swap(p.children); p.remove }
+ File.open(item_path, "w") { |f| f.puts item.to_html }
end
end
end
@@ -87,21 +84,21 @@ module Kindle
puts "=> Generating _document.yml"
x = Nokogiri::XML(File.open("rails_guides.opf")).remove_namespaces!
cover_jpg = "#{Dir.pwd}/images/rails_guides_kindle_cover.jpg"
- cover_gif = cover_jpg.sub(/jpg$/, 'gif')
+ cover_gif = cover_jpg.sub(/jpg$/, "gif")
puts `convert #{cover_jpg} #{cover_gif}`
document = {
- 'doc_uuid' => x.at("package")['unique-identifier'],
- 'title' => x.at("title").inner_text.gsub(/\(.*$/, " v2"),
- 'publisher' => x.at("publisher").inner_text,
- 'author' => x.at("creator").inner_text,
- 'subject' => x.at("subject").inner_text,
- 'date' => x.at("date").inner_text,
- 'cover' => cover_gif,
- 'masthead' => nil,
- 'mobi_outfile' => mobi_outfile
+ "doc_uuid" => x.at("package")["unique-identifier"],
+ "title" => x.at("title").inner_text.gsub(/\(.*$/, " v2"),
+ "publisher" => x.at("publisher").inner_text,
+ "author" => x.at("creator").inner_text,
+ "subject" => x.at("subject").inner_text,
+ "date" => x.at("date").inner_text,
+ "cover" => cover_gif,
+ "masthead" => nil,
+ "mobi_outfile" => mobi_outfile
}
puts document.to_yaml
- File.open("_document.yml", 'w'){|f| f.puts document.to_yaml}
+ File.open("_document.yml", "w") { |f| f.puts document.to_yaml }
end
def add_head_section(doc, title)
@@ -110,9 +107,9 @@ module Kindle
title_node.content = title
title_node.parent = head
css = Nokogiri::XML::Node.new "link", doc
- css['rel'] = 'stylesheet'
- css['type'] = 'text/css'
- css['href'] = "#{Dir.pwd}/stylesheets/kindle.css"
+ css["rel"] = "stylesheet"
+ css["type"] = "text/css"
+ css["href"] = "#{Dir.pwd}/stylesheets/kindle.css"
css.parent = head
doc.at("body").before head
end
diff --git a/guides/rails_guides/levenshtein.rb b/guides/rails_guides/levenshtein.rb
index 049f633258..2213ef754d 100644
--- a/guides/rails_guides/levenshtein.rb
+++ b/guides/rails_guides/levenshtein.rb
@@ -1,15 +1,19 @@
+# frozen_string_literal: true
+
module RailsGuides
module Levenshtein
- # This code is based directly on the Text gem implementation
+ # 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 self.distance str1, str2
+ def self.distance(str1, str2)
s = str1
t = str2
n = s.length
m = t.length
- return m if (0 == n)
- return n if (0 == m)
+ return m if 0 == n
+ return n if 0 == m
d = (0..m).to_a
x = nil
@@ -18,12 +22,12 @@ module RailsGuides
str2_codepoint_enumerable = str2.each_codepoint
str1.each_codepoint.with_index do |char1, i|
- e = i+1
+ e = i + 1
str2_codepoint_enumerable.with_index do |char2, j|
cost = (char1 == char2) ? 0 : 1
x = [
- d[j+1] + 1, # insertion
+ d[j + 1] + 1, # insertion
e + 1, # deletion
d[j] + cost # substitution
].min
@@ -34,7 +38,7 @@ module RailsGuides
d[m] = x
end
- return x
+ x
end
end
end
diff --git a/guides/rails_guides/markdown.rb b/guides/rails_guides/markdown.rb
index 17035069d0..a98aa8fe66 100644
--- a/guides/rails_guides/markdown.rb
+++ b/guides/rails_guides/markdown.rb
@@ -1,17 +1,19 @@
-# encoding: utf-8
+# frozen_string_literal: true
-require 'redcarpet'
-require 'nokogiri'
-require 'rails_guides/markdown/renderer'
+require "redcarpet"
+require "nokogiri"
+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)
@@ -49,25 +51,25 @@ module RailsGuides
def dom_id_text(text)
escaped_chars = Regexp.escape('\\/`*_{}[]()#+-.!:,;|&<>^~=\'"')
- text.downcase.gsub(/\?/, '-questionmark')
- .gsub(/!/, '-bang')
- .gsub(/[#{escaped_chars}]+/, ' ').strip
- .gsub(/\s+/, '-')
+ text.downcase.gsub(/\?/, "-questionmark")
+ .gsub(/!/, "-bang")
+ .gsub(/[#{escaped_chars}]+/, " ").strip
+ .gsub(/\s+/, "-")
end
def engine
- @engine ||= Redcarpet::Markdown.new(Renderer, {
+ @engine ||= Redcarpet::Markdown.new(Renderer,
no_intra_emphasis: true,
fenced_code_blocks: true,
autolink: true,
strikethrough: true,
superscript: true,
tables: true
- })
+ )
end
def extract_raw_header_and_body
- if @raw_body =~ /^\-{40,}$/
+ if /^\-{40,}$/.match?(@raw_body)
@raw_header, _, @raw_body = @raw_body.partition(/^\-{40,}$/).map(&:strip)
end
end
@@ -87,31 +89,35 @@ module RailsGuides
hierarchy = []
doc.children.each do |node|
- if node.name =~ /^h[3-6]$/
+ if /^h[3-6]$/.match?(node.name)
case node.name
- when 'h3'
+ when "h3"
hierarchy = [node]
@headings_for_index << [1, node, node.inner_html]
- when 'h4'
+ when "h4"
hierarchy = hierarchy[0, 1] + [node]
@headings_for_index << [2, node, node.inner_html]
- when 'h5'
+ when "h5"
hierarchy = hierarchy[0, 2] + [node]
- when 'h6'
+ when "h6"
hierarchy = hierarchy[0, 3] + [node]
end
- node[:id] = dom_id(hierarchy)
+ node[:id] = dom_id(hierarchy) unless node[:id]
node.inner_html = "#{node_index(hierarchy)} #{node.inner_html}"
end
end
+
+ doc.css("h3, h4, h5, h6").each do |node|
+ node.inner_html = "<a class='anchorlink' href='##{node[:id]}'>#{node.inner_html}</a>"
+ end
end.to_html
end
end
def generate_index
if @headings_for_index.present?
- raw_index = ''
+ raw_index = ""
@headings_for_index.each do |level, node, label|
if level == 1
raw_index += "1. [#{label}](##{node[:id]})\n"
@@ -121,7 +127,7 @@ module RailsGuides
end
@index = Nokogiri::HTML.fragment(engine.render(raw_index)).tap do |doc|
- doc.at('ol')[:class] = 'chapters'
+ doc.at("ol")[:class] = "chapters"
end.to_html
@index = <<-INDEX.html_safe
@@ -161,7 +167,7 @@ module RailsGuides
@view.content_for(:header_section) { @header }
@view.content_for(:page_title) { @title }
@view.content_for(:index_section) { @index }
- @view.render(:layout => @layout, :text => @body)
+ @view.render(layout: @layout, html: @body.html_safe)
end
end
end
diff --git a/guides/rails_guides/markdown/renderer.rb b/guides/rails_guides/markdown/renderer.rb
index 554d94ad50..f186ac526f 100644
--- a/guides/rails_guides/markdown/renderer.rb
+++ b/guides/rails_guides/markdown/renderer.rb
@@ -1,9 +1,9 @@
+# frozen_string_literal: true
+
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,17 +15,34 @@ 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, so we can use h1, h2 heading in the document
+ # Always increase the heading level by 1, so we can use h1, h2 heading in the document
header_level += 1
- %(<h#{header_level}>#{text}</h#{header_level}>)
+ header_with_id = text.scan(/(.*){#(.*)}/)
+ unless header_with_id.empty?
+ %(<h#{header_level} id="#{header_with_id[0][1].strip}">#{header_with_id[0][0].strip}</h#{header_level}>)
+ else
+ %(<h#{header_level}>#{text}</h#{header_level}>)
+ end
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 /^(TIP|IMPORTANT|CAUTION|WARNING|NOTE|INFO|TODO)[.:]/.match?(text)
convert_notes(text)
- elsif text.include?('DO NOT READ THIS FILE ON GITHUB')
+ elsif text.include?("DO NOT READ THIS FILE ON GITHUB")
elsif text =~ /^\[<sup>(\d+)\]:<\/sup> (.+)$/
linkback = %(<a href="#footnote-#{$1}-ref"><sup>#{$1}</sup></a>)
%(<p class="footnote" id="footnote-#{$1}">#{linkback} #{$2}</p>)
@@ -46,14 +63,14 @@ HTML
def brush_for(code_type)
case code_type
- when 'ruby', 'sql', 'plain'
- code_type
- when 'erb', 'html+erb'
- 'ruby; html-script: true'
- when 'html'
- 'xml' # HTML is understood, but there are .xml rules in the CSS
- else
- 'plain'
+ when "ruby", "sql", "plain"
+ code_type
+ when "erb", "html+erb"
+ "ruby; html-script: true"
+ when "html"
+ "xml" # HTML is understood, but there are .xml rules in the CSS
+ else
+ "plain"
end
end
@@ -63,21 +80,49 @@ HTML
#
# It is important that we do not eat more than one newline
# because formatting may be wrong otherwise. For example,
- # if a bulleted list follows the first item is not rendered
+ # if a bulleted list follows, the first item is not rendered
# as a list item, but as a paragraph starting with a plain
# asterisk.
body.gsub(/^(TIP|IMPORTANT|CAUTION|WARNING|NOTE|INFO|TODO)[.:](.*?)(\n(?=\n)|\Z)/m) do
- css_class = case $1
- when 'CAUTION', 'IMPORTANT'
- 'warning'
- when 'TIP'
- 'info'
- else
- $1.downcase
- end
+ css_class = \
+ case $1
+ when "CAUTION", "IMPORTANT"
+ "warning"
+ when "TIP"
+ "info"
+ else
+ $1.downcase
+ end
%(<div class="#{css_class}"><p>#{$2.strip}</p></div>)
end
end
+
+ def github_file_url(file_path)
+ tree = version || edge
+
+ root = file_path[%r{(\w+)/}, 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 %r{http://api\.rubyonrails\.org/v\d+\.}.match?(url)
+ url
+ elsif edge
+ url.sub("api", "edgeapi")
+ else
+ url.sub(/(?<=\.org)/, "/#{version}")
+ end
+ end
end
end
end
diff --git a/guides/source/2_2_release_notes.md b/guides/source/2_2_release_notes.md
index be00087f63..78a7c64afc 100644
--- a/guides/source/2_2_release_notes.md
+++ b/guides/source/2_2_release_notes.md
@@ -1,11 +1,11 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Ruby on Rails 2.2 Release Notes
===============================
-Rails 2.2 delivers a number of new and improved features. This list covers the major upgrades, but doesn't include every little bug fix and change. If you want to see everything, check out the [list of commits](http://github.com/rails/rails/commits/2-2-stable) in the main Rails repository on GitHub.
+Rails 2.2 delivers a number of new and improved features. This list covers the major upgrades, but doesn't include every little bug fix and change. If you want to see everything, check out the [list of commits](https://github.com/rails/rails/commits/2-2-stable) in the main Rails repository on GitHub.
-Along with Rails, 2.2 marks the launch of the [Ruby on Rails Guides](http://guides.rubyonrails.org/), the first results of the ongoing [Rails Guides hackfest](http://hackfest.rubyonrails.org/guide). This site will deliver high-quality documentation of the major features of Rails.
+Along with Rails, 2.2 marks the launch of the [Ruby on Rails Guides](https://guides.rubyonrails.org/), the first results of the ongoing [Rails Guides hackfest](http://hackfest.rubyonrails.org/guide). This site will deliver high-quality documentation of the major features of Rails.
--------------------------------------------------------------------------------
@@ -21,8 +21,8 @@ Rails 2.2 supplies an easy system for internationalization (or i18n, for those o
* Lead Contributors: Rails i18 Team
* More information :
* [Official Rails i18 website](http://rails-i18n.org)
- * [Finally. Ruby on Rails gets internationalized](http://www.artweb-design.de/2008/7/18/finally-ruby-on-rails-gets-internationalized)
- * [Localizing Rails : Demo application](http://github.com/clemens/i18n_demo_app)
+ * [Finally. Ruby on Rails gets internationalized](https://web.archive.org/web/20140407075019/http://www.artweb-design.de/2008/7/18/finally-ruby-on-rails-gets-internationalized)
+ * [Localizing Rails : Demo application](https://github.com/clemens/i18n_demo_app)
### Compatibility with Ruby 1.9 and JRuby
@@ -31,10 +31,10 @@ Along with thread safety, a lot of work has been done to make Rails work well wi
Documentation
-------------
-The internal documentation of Rails, in the form of code comments, has been improved in numerous places. In addition, the [Ruby on Rails Guides](http://guides.rubyonrails.org/) project is the definitive source for information on major Rails components. In its first official release, the Guides page includes:
+The internal documentation of Rails, in the form of code comments, has been improved in numerous places. In addition, the [Ruby on Rails Guides](https://guides.rubyonrails.org/) project is the definitive source for information on major Rails components. In its first official release, the Guides page includes:
* [Getting Started with Rails](getting_started.html)
-* [Rails Database Migrations](migrations.html)
+* [Rails Database Migrations](active_record_migrations.html)
* [Active Record Associations](association_basics.html)
* [Active Record Query Interface](active_record_querying.html)
* [Layouts and Rendering in Rails](layouts_and_rendering.html)
@@ -45,7 +45,6 @@ The internal documentation of Rails, in the form of code comments, has been impr
* [A Guide to Testing Rails Applications](testing.html)
* [Securing Rails Applications](security.html)
* [Debugging Rails Applications](debugging_rails_applications.html)
-* [Performance Testing Rails Applications](performance_testing.html)
* [The Basics of Creating Rails Plugins](plugins.html)
All told, the Guides provide tens of thousands of words of guidance for beginning and intermediate Rails developers.
@@ -58,11 +57,10 @@ rake doc:guides
This will put the guides inside `Rails.root/doc/guides` and you may start surfing straight away by opening `Rails.root/doc/guides/index.html` in your favourite browser.
-* Lead Contributors: [Rails Documentation Team](credits.html)
-* Major contributions from [Xavier Noria":http://advogato.org/person/fxn/diary.html and "Hongli Lai](http://izumi.plan99.net/blog/.)
+* Major contributions from [Xavier Noria](http://advogato.org/person/fxn/diary.html) and [Hongli Lai](http://izumi.plan99.net/blog/).
* More information:
* [Rails Guides hackfest](http://hackfest.rubyonrails.org/guide)
- * [Help improve Rails documentation on Git branch](http://weblog.rubyonrails.org/2008/5/2/help-improve-rails-documentation-on-git-branch)
+ * [Help improve Rails documentation on Git branch](https://weblog.rubyonrails.org/2008/5/2/help-improve-rails-documentation-on-git-branch)
Better integration with HTTP : Out of the box ETag support
----------------------------------------------------------
@@ -114,7 +112,7 @@ config.threadsafe!
* More information :
* [Thread safety for your Rails](http://m.onkey.org/2008/10/23/thread-safety-for-your-rails)
- * [Thread safety project announcement](http://weblog.rubyonrails.org/2008/8/16/josh-peek-officially-joins-the-rails-core)
+ * [Thread safety project announcement](https://weblog.rubyonrails.org/2008/8/16/josh-peek-officially-joins-the-rails-core)
* [Q/A: What Thread-safe Rails Means](http://blog.headius.com/2008/08/qa-what-thread-safe-rails-means.html)
Active Record
@@ -126,7 +124,7 @@ There are two big additions to talk about here: transactional migrations and poo
Historically, multiple-step Rails migrations have been a source of trouble. If something went wrong during a migration, everything before the error changed the database and everything after the error wasn't applied. Also, the migration version was stored as having been executed, which means that it couldn't be simply rerun by `rake db:migrate:redo` after you fix the problem. Transactional migrations change this by wrapping migration steps in a DDL transaction, so that if any of them fail, the entire migration is undone. In Rails 2.2, transactional migrations are supported on PostgreSQL out of the box. The code is extensible to other database types in the future - and IBM has already extended it to support the DB2 adapter.
-* Lead Contributor: [Adam Wiggins](http://adam.heroku.com/)
+* Lead Contributor: [Adam Wiggins](http://about.adamwiggins.com/)
* More information:
* [DDL Transactions](http://adam.heroku.com/past/2008/9/3/ddl_transactions/)
* [A major milestone for DB2 on Rails](http://db2onrails.com/2008/11/08/a-major-milestone-for-db2-on-rails/)
@@ -146,7 +144,7 @@ development:
* Lead Contributor: [Nick Sieger](http://blog.nicksieger.com/)
* More information:
- * [What's New in Edge Rails: Connection Pools](http://ryandaigle.com/articles/2008/9/7/what-s-new-in-edge-rails-connection-pools)
+ * [What's New in Edge Rails: Connection Pools](http://archives.ryandaigle.com/articles/2008/9/7/what-s-new-in-edge-rails-connection-pools)
### Hashes for Join Table Conditions
@@ -166,7 +164,7 @@ Product.all(:joins => :photos, :conditions => { :photos => { :copyright => false
```
* More information:
- * [What's New in Edge Rails: Easy Join Table Conditions](http://ryandaigle.com/articles/2008/7/7/what-s-new-in-edge-rails-easy-join-table-conditions)
+ * [What's New in Edge Rails: Easy Join Table Conditions](http://archives.ryandaigle.com/articles/2008/7/7/what-s-new-in-edge-rails-easy-join-table-conditions)
### New Dynamic Finders
@@ -239,7 +237,7 @@ This will enable recognition of (among others) these routes:
* Lead Contributor: [S. Brent Faulkner](http://www.unwwwired.net/)
* More information:
* [Rails Routing from the Outside In](routing.html#nested-resources)
- * [What's New in Edge Rails: Shallow Routes](http://ryandaigle.com/articles/2008/9/7/what-s-new-in-edge-rails-shallow-routes)
+ * [What's New in Edge Rails: Shallow Routes](http://archives.ryandaigle.com/articles/2008/9/7/what-s-new-in-edge-rails-shallow-routes)
### Method Arrays for Member or Collection Routes
@@ -287,7 +285,7 @@ Action Mailer
Action Mailer now supports mailer layouts. You can make your HTML emails as pretty as your in-browser views by supplying an appropriately-named layout - for example, the `CustomerMailer` class expects to use `layouts/customer_mailer.html.erb`.
* More information:
- * [What's New in Edge Rails: Mailer Layouts](http://ryandaigle.com/articles/2008/9/7/what-s-new-in-edge-rails-mailer-layouts)
+ * [What's New in Edge Rails: Mailer Layouts](http://archives.ryandaigle.com/articles/2008/9/7/what-s-new-in-edge-rails-mailer-layouts)
Action Mailer now offers built-in support for GMail's SMTP servers, by turning on STARTTLS automatically. This requires Ruby 1.8.7 to be installed.
@@ -321,7 +319,7 @@ Other features of memoization include `unmemoize`, `unmemoize_all`, and `memoize
* Lead Contributor: [Josh Peek](http://joshpeek.com/)
* More information:
- * [What's New in Edge Rails: Easy Memoization](http://ryandaigle.com/articles/2008/7/16/what-s-new-in-edge-rails-memoization)
+ * [What's New in Edge Rails: Easy Memoization](http://archives.ryandaigle.com/articles/2008/7/16/what-s-new-in-edge-rails-memoization)
* [Memo-what? A Guide to Memoization](http://www.railway.at/articles/2008/09/20/a-guide-to-memoization)
### each_with_object
@@ -389,10 +387,10 @@ To avoid deployment issues and make Rails applications more self-contained, it's
You can unpack or install a single gem by specifying `GEM=_gem_name_` on the command line.
-* Lead Contributor: [Matt Jones](http://github.com/al2o3cr)
+* Lead Contributor: [Matt Jones](https://github.com/al2o3cr)
* More information:
- * [What's New in Edge Rails: Gem Dependencies](http://ryandaigle.com/articles/2008/4/1/what-s-new-in-edge-rails-gem-dependencies)
- * [Rails 2.1.2 and 2.2RC1: Update Your RubyGems](http://afreshcup.com/2008/10/25/rails-212-and-22rc1-update-your-rubygems/)
+ * [What's New in Edge Rails: Gem Dependencies](http://archives.ryandaigle.com/articles/2008/4/1/what-s-new-in-edge-rails-gem-dependencies)
+ * [Rails 2.1.2 and 2.2RC1: Update Your RubyGems](https://afreshcup.com/home/2008/10/25/rails-212-and-22rc1-update-your-rubygems)
* [Detailed discussion on Lighthouse](http://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/1128)
### Other Railties Changes
@@ -411,7 +409,7 @@ Deprecated
A few pieces of older code are deprecated in this release:
* `Rails::SecretKeyGenerator` has been replaced by `ActiveSupport::SecureRandom`
-* `render_component` is deprecated. There's a [render_components plugin](http://github.com/rails/render_component/tree/master) available if you need this functionality.
+* `render_component` is deprecated. There's a [render_components plugin](https://github.com/rails/render_component/tree/master) available if you need this functionality.
* Implicit local assignments when rendering partials has been deprecated.
```ruby
diff --git a/guides/source/2_3_release_notes.md b/guides/source/2_3_release_notes.md
index 0a62f34371..ee9a499953 100644
--- a/guides/source/2_3_release_notes.md
+++ b/guides/source/2_3_release_notes.md
@@ -1,16 +1,16 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Ruby on Rails 2.3 Release Notes
===============================
-Rails 2.3 delivers a variety of new and improved features, including pervasive Rack integration, refreshed support for Rails Engines, nested transactions for Active Record, dynamic and default scopes, unified rendering, more efficient routing, application templates, and quiet backtraces. This list covers the major upgrades, but doesn't include every little bug fix and change. If you want to see everything, check out the [list of commits](http://github.com/rails/rails/commits/2-3-stable) in the main Rails repository on GitHub or review the `CHANGELOG` files for the individual Rails components.
+Rails 2.3 delivers a variety of new and improved features, including pervasive Rack integration, refreshed support for Rails Engines, nested transactions for Active Record, dynamic and default scopes, unified rendering, more efficient routing, application templates, and quiet backtraces. This list covers the major upgrades, but doesn't include every little bug fix and change. If you want to see everything, check out the [list of commits](https://github.com/rails/rails/commits/2-3-stable) in the main Rails repository on GitHub or review the `CHANGELOG` files for the individual Rails components.
--------------------------------------------------------------------------------
Application Architecture
------------------------
-There are two major changes in the architecture of Rails applications: complete integration of the [Rack](http://rack.github.io/) modular web server interface, and renewed support for Rails Engines.
+There are two major changes in the architecture of Rails applications: complete integration of the [Rack](https://rack.github.io/) modular web server interface, and renewed support for Rails Engines.
### Rack Integration
@@ -52,9 +52,9 @@ After some versions without an upgrade, Rails 2.3 offers some new features for R
Documentation
-------------
-The [Ruby on Rails guides](http://guides.rubyonrails.org/) project has published several additional guides for Rails 2.3. In addition, a [separate site](http://edgeguides.rubyonrails.org/) maintains updated copies of the Guides for Edge Rails. Other documentation efforts include a relaunch of the [Rails wiki](http://newwiki.rubyonrails.org/) and early planning for a Rails Book.
+The [Ruby on Rails guides](https://guides.rubyonrails.org/) project has published several additional guides for Rails 2.3. In addition, a [separate site](https://edgeguides.rubyonrails.org/) maintains updated copies of the Guides for Edge Rails. Other documentation efforts include a relaunch of the [Rails wiki](http://newwiki.rubyonrails.org/) and early planning for a Rails Book.
-* More Information: [Rails Documentation Projects](http://weblog.rubyonrails.org/2009/1/15/rails-documentation-projects.)
+* More Information: [Rails Documentation Projects](https://weblog.rubyonrails.org/2009/1/15/rails-documentation-projects)
Ruby 1.9.1 Support
------------------
@@ -89,7 +89,7 @@ accepts_nested_attributes_for :author,
```
* Lead Contributor: [Eloy Duran](http://superalloy.nl/)
-* More Information: [Nested Model Forms](http://weblog.rubyonrails.org/2009/1/26/nested-model-forms)
+* More Information: [Nested Model Forms](https://weblog.rubyonrails.org/2009/1/26/nested-model-forms)
### Nested Transactions
@@ -125,14 +125,14 @@ Order.scoped_by_customer_id(12).scoped_by_status("open")
There's nothing to define to use dynamic scopes: they just work.
* Lead Contributor: [Yaroslav Markin](http://evilmartians.com/)
-* More Information: [What's New in Edge Rails: Dynamic Scope Methods](http://ryandaigle.com/articles/2008/12/29/what-s-new-in-edge-rails-dynamic-scope-methods.)
+* More Information: [What's New in Edge Rails: Dynamic Scope Methods](http://archives.ryandaigle.com/articles/2008/12/29/what-s-new-in-edge-rails-dynamic-scope-methods)
### Default Scopes
Rails 2.3 will introduce the notion of _default scopes_ similar to named scopes, but applying to all named scopes or find methods within the model. For example, you can write `default_scope :order => 'name ASC'` and any time you retrieve records from that model they'll come out sorted by name (unless you override the option, of course).
* Lead Contributor: Paweł Kondzior
-* More Information: [What's New in Edge Rails: Default Scoping](http://ryandaigle.com/articles/2008/11/18/what-s-new-in-edge-rails-default-scoping)
+* More Information: [What's New in Edge Rails: Default Scoping](http://archives.ryandaigle.com/articles/2008/11/18/what-s-new-in-edge-rails-default-scoping)
### Batch Processing
@@ -158,7 +158,7 @@ Note that you should only use this method for batch processing: for small number
* More Information (at that point the convenience method was called just `each`):
* [Rails 2.3: Batch Finding](http://afreshcup.com/2009/02/23/rails-23-batch-finding/)
- * [What's New in Edge Rails: Batched Find](http://ryandaigle.com/articles/2009/2/23/what-s-new-in-edge-rails-batched-find)
+ * [What's New in Edge Rails: Batched Find](http://archives.ryandaigle.com/articles/2009/2/23/what-s-new-in-edge-rails-batched-find)
### Multiple Conditions for Callbacks
@@ -179,7 +179,7 @@ developers = Developer.find(:all, :group => "salary",
:having => "sum(salary) > 10000", :select => "salary")
```
-* Lead Contributor: [Emilio Tagua](http://github.com/miloops)
+* Lead Contributor: [Emilio Tagua](https://github.com/miloops)
### Reconnecting MySQL Connections
@@ -231,11 +231,11 @@ Rails chooses between file, template, and action depending on whether there is a
### Application Controller Renamed
-If you're one of the people who has always been bothered by the special-case naming of `application.rb`, rejoice! It's been reworked to be application_controller.rb in Rails 2.3. In addition, there's a new rake task, `rake rails:update:application_controller` to do this automatically for you - and it will be run as part of the normal `rake rails:update` process.
+If you're one of the people who has always been bothered by the special-case naming of `application.rb`, rejoice! It's been reworked to be `application_controller.rb` in Rails 2.3. In addition, there's a new rake task, `rake rails:update:application_controller` to do this automatically for you - and it will be run as part of the normal `rake rails:update` process.
* More Information:
- * [The Death of Application.rb](http://afreshcup.com/2008/11/17/rails-2x-the-death-of-applicationrb/)
- * [What's New in Edge Rails: Application.rb Duality is no More](http://ryandaigle.com/articles/2008/11/19/what-s-new-in-edge-rails-application-rb-duality-is-no-more)
+ * [The Death of Application.rb](https://afreshcup.com/home/2008/11/17/rails-2x-the-death-of-applicationrb)
+ * [What's New in Edge Rails: Application.rb Duality is no More](http://archives.ryandaigle.com/articles/2008/11/19/what-s-new-in-edge-rails-application-rb-duality-is-no-more)
### HTTP Digest Authentication Support
@@ -261,7 +261,7 @@ end
```
* Lead Contributor: [Gregg Kellogg](http://www.kellogg-assoc.com/)
-* More Information: [What's New in Edge Rails: HTTP Digest Authentication](http://ryandaigle.com/articles/2009/1/30/what-s-new-in-edge-rails-http-digest-authentication)
+* More Information: [What's New in Edge Rails: HTTP Digest Authentication](http://archives.ryandaigle.com/articles/2009/1/30/what-s-new-in-edge-rails-http-digest-authentication)
### More Efficient Routing
@@ -304,7 +304,7 @@ Rails now keeps a per-request local cache of read from the remote cache stores,
Rails can now provide localized views, depending on the locale that you have set. For example, suppose you have a `Posts` controller with a `show` action. By default, this will render `app/views/posts/show.html.erb`. But if you set `I18n.locale = :da`, it will render `app/views/posts/show.da.html.erb`. If the localized template isn't present, the undecorated version will be used. Rails also includes `I18n#available_locales` and `I18n::SimpleBackend#available_locales`, which return an array of the translations that are available in the current Rails project.
-In addition, you can use the same scheme to localize the rescue files in the `public` directory: `public/500.da.html` or `public/404.en.html` work, for example.
+In addition, you can use the same scheme to localize the rescue files in the public directory: `public/500.da.html` or `public/404.en.html` work, for example.
### Partial Scoping for Translations
@@ -376,9 +376,9 @@ You can write this view in Rails 2.3:
* Lead Contributor: [Eloy Duran](http://superalloy.nl/)
* More Information:
- * [Nested Model Forms](http://weblog.rubyonrails.org/2009/1/26/nested-model-forms)
- * [complex-form-examples](http://github.com/alloy/complex-form-examples)
- * [What's New in Edge Rails: Nested Object Forms](http://ryandaigle.com/articles/2009/2/1/what-s-new-in-edge-rails-nested-attributes)
+ * [Nested Model Forms](https://weblog.rubyonrails.org/2009/1/26/nested-model-forms)
+ * [complex-form-examples](https://github.com/alloy/complex-form-examples)
+ * [What's New in Edge Rails: Nested Object Forms](http://archives.ryandaigle.com/articles/2009/2/1/what-s-new-in-edge-rails-nested-attributes)
### Smart Rendering of Partials
@@ -394,7 +394,7 @@ render @article
render @articles
```
-* More Information: [What's New in Edge Rails: render Stops Being High-Maintenance](http://ryandaigle.com/articles/2008/11/20/what-s-new-in-edge-rails-render-stops-being-high-maintenance)
+* More Information: [What's New in Edge Rails: render Stops Being High-Maintenance](http://archives.ryandaigle.com/articles/2008/11/20/what-s-new-in-edge-rails-render-stops-being-high-maintenance)
### Prompts for Date Select Helpers
@@ -421,7 +421,7 @@ You're likely familiar with Rails' practice of adding timestamps to static asset
Asset hosts get more flexible in edge Rails with the ability to declare an asset host as a specific object that responds to a call. This allows you to implement any complex logic you need in your asset hosting.
-* More Information: [asset-hosting-with-minimum-ssl](http://github.com/dhh/asset-hosting-with-minimum-ssl/tree/master)
+* More Information: [asset-hosting-with-minimum-ssl](https://github.com/dhh/asset-hosting-with-minimum-ssl/tree/master)
### grouped_options_for_select Helper Method
@@ -468,7 +468,7 @@ options_from_collection_for_select(@product.sizes, :name, :id, :disabled => lamb
```
* Lead Contributor: [Tekin Suleyman](http://tekin.co.uk/)
-* More Information: [New in rails 2.3 - disabled option tags and lambdas for selecting and disabling options from collections](http://tekin.co.uk/2009/03/new-in-rails-23-disabled-option-tags-and-lambdas-for-selecting-and-disabling-options-from-collections/)
+* More Information: [New in rails 2.3 - disabled option tags and lambdas for selecting and disabling options from collections](https://tekin.co.uk/2009/03/new-in-rails-23-disabled-option-tags-and-lambdas-for-selecting-and-disabling-options-from-collections)
### A Note About Template Loading
@@ -498,7 +498,7 @@ Active Support has a few interesting changes, including the introduction of `Obj
A lot of folks have adopted the notion of using try() to attempt operations on objects. It's especially helpful in views where you can avoid nil-checking by writing code like `<%= @person.try(:name) %>`. Well, now it's baked right into Rails. As implemented in Rails, it raises `NoMethodError` on private methods and always returns `nil` if the object is nil.
-* More Information: [try()](http://ozmm.org/posts/try.html.)
+* More Information: [try()](http://ozmm.org/posts/try.html)
### Object#tap Backport
@@ -533,7 +533,7 @@ If you look up the spec on the "json.org" site, you'll discover that all keys in
### Other Active Support Changes
* You can use `Enumerable#none?` to check that none of the elements match the supplied block.
-* If you're using Active Support [delegates](http://afreshcup.com/2008/10/19/coming-in-rails-22-delegate-prefixes/,) the new `:allow_nil` option lets you return `nil` instead of raising an exception when the target object is nil.
+* If you're using Active Support [delegates](https://afreshcup.com/home/2008/10/19/coming-in-rails-22-delegate-prefixes) the new `:allow_nil` option lets you return `nil` instead of raising an exception when the target object is nil.
* `ActiveSupport::OrderedHash`: now implements `each_key` and `each_value`.
* `ActiveSupport::MessageEncryptor` provides a simple way to encrypt information for storage in an untrusted location (like cookies).
* Active Support's `from_xml` no longer depends on XmlSimple. Instead, Rails now includes its own XmlMini implementation, with just the functionality that it requires. This lets Rails dispense with the bundled copy of XmlSimple that it's been carting around.
@@ -552,14 +552,14 @@ In addition to the Rack changes covered above, Railties (the core code of Rails
Rails Metal is a new mechanism that provides superfast endpoints inside of your Rails applications. Metal classes bypass routing and Action Controller to give you raw speed (at the cost of all the things in Action Controller, of course). This builds on all of the recent foundation work to make Rails a Rack application with an exposed middleware stack. Metal endpoints can be loaded from your application or from plugins.
* More Information:
- * [Introducing Rails Metal](http://weblog.rubyonrails.org/2008/12/17/introducing-rails-metal)
+ * [Introducing Rails Metal](https://weblog.rubyonrails.org/2008/12/17/introducing-rails-metal)
* [Rails Metal: a micro-framework with the power of Rails](http://soylentfoo.jnewland.com/articles/2008/12/16/rails-metal-a-micro-framework-with-the-power-of-rails-m)
* [Metal: Super-fast Endpoints within your Rails Apps](http://www.railsinside.com/deployment/180-metal-super-fast-endpoints-within-your-rails-apps.html)
- * [What's New in Edge Rails: Rails Metal](http://ryandaigle.com/articles/2008/12/18/what-s-new-in-edge-rails-rails-metal)
+ * [What's New in Edge Rails: Rails Metal](http://archives.ryandaigle.com/articles/2008/12/18/what-s-new-in-edge-rails-rails-metal)
### Application Templates
-Rails 2.3 incorporates Jeremy McAnally's [rg](http://github.com/jeremymcanally/rg/tree/master) application generator. What this means is that we now have template-based application generation built right into Rails; if you have a set of plugins you include in every application (among many other use cases), you can just set up a template once and use it over and over again when you run the `rails` command. There's also a rake task to apply a template to an existing application:
+Rails 2.3 incorporates Jeremy McAnally's [rg](https://github.com/jm/rg) application generator. What this means is that we now have template-based application generation built right into Rails; if you have a set of plugins you include in every application (among many other use cases), you can just set up a template once and use it over and over again when you run the `rails` command. There's also a rake task to apply a template to an existing application:
```
rake rails:template LOCATION=~/template.rb
@@ -572,11 +572,11 @@ This will layer the changes from the template on top of whatever code the projec
### Quieter Backtraces
-Building on Thoughtbot's [Quiet Backtrace](https://github.com/thoughtbot/quietbacktrace) plugin, which allows you to selectively remove lines from `Test::Unit` backtraces, Rails 2.3 implements `ActiveSupport::BacktraceCleaner` and `Rails::BacktraceCleaner` in core. This supports both filters (to perform regex-based substitutions on backtrace lines) and silencers (to remove backtrace lines entirely). Rails automatically adds silencers to get rid of the most common noise in a new application, and builds a `config/backtrace_silencers.rb` file to hold your own additions. This feature also enables prettier printing from any gem in the backtrace.
+Building on thoughtbot's [Quiet Backtrace](https://github.com/thoughtbot/quietbacktrace) plugin, which allows you to selectively remove lines from `Test::Unit` backtraces, Rails 2.3 implements `ActiveSupport::BacktraceCleaner` and `Rails::BacktraceCleaner` in core. This supports both filters (to perform regex-based substitutions on backtrace lines) and silencers (to remove backtrace lines entirely). Rails automatically adds silencers to get rid of the most common noise in a new application, and builds a `config/backtrace_silencers.rb` file to hold your own additions. This feature also enables prettier printing from any gem in the backtrace.
### Faster Boot Time in Development Mode with Lazy Loading/Autoload
-Quite a bit of work was done to make sure that bits of Rails (and its dependencies) are only brought into memory when they're actually needed. The core frameworks - Active Support, Active Record, Action Controller, Action Mailer and Action View - are now using `autoload` to lazy-load their individual classes. This work should help keep the memory footprint down and improve overall Rails performance.
+Quite a bit of work was done to make sure that bits of Rails (and its dependencies) are only brought into memory when they're actually needed. The core frameworks - Active Support, Active Record, Action Controller, Action Mailer, and Action View - are now using `autoload` to lazy-load their individual classes. This work should help keep the memory footprint down and improve overall Rails performance.
You can also specify (by using the new `preload_frameworks` option) whether the core libraries should be autoloaded at startup. This defaults to `false` so that Rails autoloads itself piece-by-piece, but there are some circumstances where you still need to bring in everything at once - Passenger and JRuby both want to see all of Rails loaded together.
@@ -592,7 +592,7 @@ The internals of the various <code>rake gem</code> tasks have been substantially
* Internal Rails testing has been switched from `Test::Unit::TestCase` to `ActiveSupport::TestCase`, and the Rails core requires Mocha to test.
* The default `environment.rb` file has been decluttered.
* The dbconsole script now lets you use an all-numeric password without crashing.
-* `Rails.root` now returns a `Pathname` object, which means you can use it directly with the `join` method to [clean up existing code](http://afreshcup.com/2008/12/05/a-little-rails_root-tidiness/) that uses `File.join`.
+* `Rails.root` now returns a `Pathname` object, which means you can use it directly with the `join` method to [clean up existing code](https://afreshcup.wordpress.com/2008/12/05/a-little-rails_root-tidiness/) that uses `File.join`.
* Various files in /public that deal with CGI and FCGI dispatching are no longer generated in every Rails application by default (you can still get them if you need them by adding `--with-dispatchers` when you run the `rails` command, or add them later with `rake rails:update:generate_dispatchers`).
* Rails Guides have been converted from AsciiDoc to Textile markup.
* Scaffolded views and controllers have been cleaned up a bit.
@@ -605,8 +605,8 @@ Deprecated
A few pieces of older code are deprecated in this release:
-* If you're one of the (fairly rare) Rails developers who deploys in a fashion that depends on the inspector, reaper, and spawner scripts, you'll need to know that those scripts are no longer included in core Rails. If you need them, you'll be able to pick up copies via the [irs_process_scripts](http://github.com/rails/irs_process_scripts/tree) plugin.
-* `render_component` goes from "deprecated" to "nonexistent" in Rails 2.3. If you still need it, you can install the [render_component plugin](http://github.com/rails/render_component/tree/master).
+* If you're one of the (fairly rare) Rails developers who deploys in a fashion that depends on the inspector, reaper, and spawner scripts, you'll need to know that those scripts are no longer included in core Rails. If you need them, you'll be able to pick up copies via the [irs_process_scripts](https://github.com/rails/irs_process_scripts) plugin.
+* `render_component` goes from "deprecated" to "nonexistent" in Rails 2.3. If you still need it, you can install the [render_component plugin](https://github.com/rails/render_component/tree/master).
* Support for Rails components has been removed.
* If you were one of the people who got used to running `script/performance/request` to look at performance based on integration tests, you need to learn a new trick: that script has been removed from core Rails now. There's a new request_profiler plugin that you can install to get the exact same functionality back.
* `ActionController::Base#session_enabled?` is deprecated because sessions are lazy-loaded now.
@@ -614,7 +614,7 @@ A few pieces of older code are deprecated in this release:
* Some integration test helpers have been removed. `response.headers["Status"]` and `headers["Status"]` will no longer return anything. Rack does not allow "Status" in its return headers. However you can still use the `status` and `status_message` helpers. `response.headers["cookie"]` and `headers["cookie"]` will no longer return any CGI cookies. You can inspect `headers["Set-Cookie"]` to see the raw cookie header or use the `cookies` helper to get a hash of the cookies sent to the client.
* `formatted_polymorphic_url` is deprecated. Use `polymorphic_url` with `:format` instead.
* The `:http_only` option in `ActionController::Response#set_cookie` has been renamed to `:httponly`.
-* The `:connector` and `:skip_last_comma` options of `to_sentence` have been replaced by `:words_connnector`, `:two_words_connector`, and `:last_word_connector` options.
+* The `:connector` and `:skip_last_comma` options of `to_sentence` have been replaced by `:words_connector`, `:two_words_connector`, and `:last_word_connector` options.
* Posting a multipart form with an empty `file_field` control used to submit an empty string to the controller. Now it submits a nil, due to differences between Rack's multipart parser and the old Rails one.
Credits
diff --git a/guides/source/3_0_release_notes.md b/guides/source/3_0_release_notes.md
index 696493a3cf..e936644daf 100644
--- a/guides/source/3_0_release_notes.md
+++ b/guides/source/3_0_release_notes.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Ruby on Rails 3.0 Release Notes
===============================
@@ -17,7 +17,7 @@ Even if you don't give a hoot about any of our internal cleanups, Rails 3.0 is g
On top of all that, we've tried our best to deprecate the old APIs with nice warnings. That means that you can move your existing application to Rails 3 without immediately rewriting all your old code to the latest best practices.
-These release notes cover the major upgrades, but don't include every little bug fix and change. Rails 3.0 consists of almost 4,000 commits by more than 250 authors! If you want to see everything, check out the [list of commits](http://github.com/rails/rails/commits/3-0-stable) in the main Rails repository on GitHub.
+These release notes cover the major upgrades, but don't include every little bug fix and change. Rails 3.0 consists of almost 4,000 commits by more than 250 authors! If you want to see everything, check out the [list of commits](https://github.com/rails/rails/commits/3-0-stable) in the main Rails repository on GitHub.
--------------------------------------------------------------------------------
@@ -38,7 +38,7 @@ If you're upgrading an existing application, it's a great idea to have good test
Rails 3.0 requires Ruby 1.8.7 or higher. Support for all of the previous Ruby versions has been dropped officially and you should upgrade as early as possible. Rails 3.0 is also compatible with Ruby 1.9.2.
-TIP: Note that Ruby 1.8.7 p248 and p249 have marshaling bugs that crash Rails 3.0. Ruby Enterprise Edition have these fixed since release 1.8.7-2010.02 though. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults on Rails 3.0, so if you want to use Rails 3 with 1.9.x jump on 1.9.2 for smooth sailing.
+TIP: Note that Ruby 1.8.7 p248 and p249 have marshalling bugs that crash Rails 3.0. Ruby Enterprise Edition have these fixed since release 1.8.7-2010.02 though. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults on Rails 3.0, so if you want to use Rails 3 with 1.9.x jump on 1.9.2 for smooth sailing.
### Rails Application object
@@ -63,7 +63,7 @@ The `config.gem` method is gone and has been replaced by using `bundler` and a `
### Upgrade Process
-To help with the upgrade process, a plugin named [Rails Upgrade](http://github.com/rails/rails_upgrade) has been created to automate part of it.
+To help with the upgrade process, a plugin named [Rails Upgrade](https://github.com/rails/rails_upgrade) has been created to automate part of it.
Simply install the plugin, then run `rake rails:upgrade:check` to check your app for pieces that need to be updated (with links to information on how to update them). It also offers a task to generate a `Gemfile` based on your current `config.gem` calls and a task to generate a new routes file from your current one. To get the plugin, simply run the following:
@@ -86,9 +86,9 @@ $ cd myapp
### Vendoring Gems
-Rails now uses a `Gemfile` in the application root to determine the gems you require for your application to start. This `Gemfile` is processed by the [Bundler](http://github.com/carlhuda/bundler,) which then installs all your dependencies. It can even install all the dependencies locally to your application so that it doesn't depend on the system gems.
+Rails now uses a `Gemfile` in the application root to determine the gems you require for your application to start. This `Gemfile` is processed by the [Bundler](https://github.com/bundler/bundler) which then installs all your dependencies. It can even install all the dependencies locally to your application so that it doesn't depend on the system gems.
-More information: - [bundler homepage](http://bundler.io/)
+More information: - [bundler homepage](https://bundler.io/)
### Living on the Edge
@@ -138,14 +138,14 @@ More Information: - [Rails Edge Architecture](http://yehudakatz.com/2009/06/11/r
### Arel Integration
-[Arel](http://github.com/brynary/arel) (or Active Relation) has been taken on as the underpinnings of Active Record and is now required for Rails. Arel provides an SQL abstraction that simplifies out Active Record and provides the underpinnings for the relation functionality in Active Record.
+[Arel](https://github.com/brynary/arel) (or Active Relation) has been taken on as the underpinnings of Active Record and is now required for Rails. Arel provides an SQL abstraction that simplifies out Active Record and provides the underpinnings for the relation functionality in Active Record.
More information: - [Why I wrote Arel](https://web.archive.org/web/20120718093140/http://magicscalingsprinkles.wordpress.com/2010/01/28/why-i-wrote-arel/)
### Mail Extraction
-Action Mailer ever since its beginnings has had monkey patches, pre parsers and even delivery and receiver agents, all in addition to having TMail vendored in the source tree. Version 3 changes that with all email message related functionality abstracted out to the [Mail](http://github.com/mikel/mail) gem. This again reduces code duplication and helps create definable boundaries between Action Mailer and the email parser.
+Action Mailer ever since its beginnings has had monkey patches, pre parsers and even delivery and receiver agents, all in addition to having TMail vendored in the source tree. Version 3 changes that with all email message related functionality abstracted out to the [Mail](https://github.com/mikel/mail) gem. This again reduces code duplication and helps create definable boundaries between Action Mailer and the email parser.
More information: - [New Action Mailer API in Rails 3](http://lindsaar.net/2010/1/26/new-actionmailer-api-in-rails-3)
@@ -153,15 +153,15 @@ More information: - [New Action Mailer API in Rails 3](http://lindsaar.net/2010/
Documentation
-------------
-The documentation in the Rails tree is being updated with all the API changes, additionally, the [Rails Edge Guides](http://edgeguides.rubyonrails.org/) are being updated one by one to reflect the changes in Rails 3.0. The guides at [guides.rubyonrails.org](http://guides.rubyonrails.org/) however will continue to contain only the stable version of Rails (at this point, version 2.3.5, until 3.0 is released).
+The documentation in the Rails tree is being updated with all the API changes, additionally, the [Rails Edge Guides](https://edgeguides.rubyonrails.org/) are being updated one by one to reflect the changes in Rails 3.0. The guides at [guides.rubyonrails.org](https://guides.rubyonrails.org/) however will continue to contain only the stable version of Rails (at this point, version 2.3.5, until 3.0 is released).
-More Information: - [Rails Documentation Projects](http://weblog.rubyonrails.org/2009/1/15/rails-documentation-projects.)
+More Information: - [Rails Documentation Projects](https://weblog.rubyonrails.org/2009/1/15/rails-documentation-projects)
Internationalization
--------------------
-A large amount of work has been done with I18n support in Rails 3, including the latest [I18n](http://github.com/svenfuchs/i18n) gem supplying many speed improvements.
+A large amount of work has been done with I18n support in Rails 3, including the latest [I18n](https://github.com/svenfuchs/i18n) gem supplying many speed improvements.
* I18n for any object - I18n behavior can be added to any object by including `ActiveModel::Translation` and `ActiveModel::Validations`. There is also an `errors.messages` fallback for translations.
* Attributes can have default translations.
@@ -174,7 +174,7 @@ More Information: - [Rails 3 I18n changes](http://blog.plataformatec.com.br/2010
Railties
--------
-With the decoupling of the main Rails frameworks, Railties got a huge overhaul so as to make linking up frameworks, engines or plugins as painless and extensible as possible:
+With the decoupling of the main Rails frameworks, Railties got a huge overhaul so as to make linking up frameworks, engines, or plugins as painless and extensible as possible:
* Each application now has its own name space, application is started with `YourAppName.boot` for example, makes interacting with other applications a lot easier.
* Anything under `Rails.root/app` is now added to the load path, so you can make `app/observers/user_observer.rb` and Rails will load it without any modifications.
@@ -213,8 +213,7 @@ Railties now deprecates:
More information:
* [Discovering Rails 3 generators](http://blog.plataformatec.com.br/2010/01/discovering-rails-3-generators)
-* [Making Generators for Rails 3 with Thor](http://caffeinedd.com/guides/331-making-generators-for-rails-3-with-thor)
-* [The Rails Module (in Rails 3)](http://litanyagainstfear.com/blog/2010/02/03/the-rails-module/)
+* [The Rails Module (in Rails 3)](http://quaran.to/blog/2010/02/03/the-rails-module/)
Action Pack
-----------
@@ -250,8 +249,8 @@ Deprecations:
More Information:
-* [Render Options in Rails 3](http://www.engineyard.com/blog/2010/render-options-in-rails-3/)
-* [Three reasons to love ActionController::Responder](http://weblog.rubyonrails.org/2009/8/31/three-reasons-love-responder)
+* [Render Options in Rails 3](https://blog.engineyard.com/2010/render-options-in-rails-3)
+* [Three reasons to love ActionController::Responder](https://weblog.rubyonrails.org/2009/8/31/three-reasons-love-responder)
### Action Dispatch
@@ -310,7 +309,7 @@ More Information:
Major re-write was done in the Action View helpers, implementing Unobtrusive JavaScript (UJS) hooks and removing the old inline AJAX commands. This enables Rails to use any compliant UJS driver to implement the UJS hooks in the helpers.
-What this means is that all previous `remote_<method>` helpers have been removed from Rails core and put into the [Prototype Legacy Helper](http://github.com/rails/prototype_legacy_helper). To get UJS hooks into your HTML, you now pass `:remote => true` instead. For example:
+What this means is that all previous `remote_<method>` helpers have been removed from Rails core and put into the [Prototype Legacy Helper](https://github.com/rails/prototype_legacy_helper). To get UJS hooks into your HTML, you now pass `:remote => true` instead. For example:
```ruby
form_for @post, :remote => true
@@ -423,7 +422,7 @@ More Information:
Active Record
-------------
-Active Record received a lot of attention in Rails 3.0, including abstraction into Active Model, a full update to the Query interface using Arel, validation updates and many enhancements and fixes. All of the Rails 2.x API will be usable through a compatibility layer that will be supported until version 3.1.
+Active Record received a lot of attention in Rails 3.0, including abstraction into Active Model, a full update to the Query interface using Arel, validation updates, and many enhancements and fixes. All of the Rails 2.x API will be usable through a compatibility layer that will be supported until version 3.1.
### Query Interface
@@ -576,7 +575,7 @@ The following methods have been removed because they are no longer used in the f
Action Mailer
-------------
-Action Mailer has been given a new API with TMail being replaced out with the new [Mail](http://github.com/mikel/mail) as the email library. Action Mailer itself has been given an almost complete re-write with pretty much every line of code touched. The result is that Action Mailer now simply inherits from Abstract Controller and wraps the Mail gem in a Rails DSL. This reduces the amount of code and duplication of other libraries in Action Mailer considerably.
+Action Mailer has been given a new API with TMail being replaced out with the new [Mail](https://github.com/mikel/mail) as the email library. Action Mailer itself has been given an almost complete re-write with pretty much every line of code touched. The result is that Action Mailer now simply inherits from Abstract Controller and wraps the Mail gem in a Rails DSL. This reduces the amount of code and duplication of other libraries in Action Mailer considerably.
* All mailers are now in `app/mailers` by default.
* Can now send email using new API with three methods: `attachments`, `headers` and `mail`.
diff --git a/guides/source/3_1_release_notes.md b/guides/source/3_1_release_notes.md
index 327495704a..d6981656ee 100644
--- a/guides/source/3_1_release_notes.md
+++ b/guides/source/3_1_release_notes.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Ruby on Rails 3.1 Release Notes
===============================
@@ -26,7 +26,7 @@ If you're upgrading an existing application, it's a great idea to have good test
Rails 3.1 requires Ruby 1.8.7 or higher. Support for all of the previous Ruby versions has been dropped officially and you should upgrade as early as possible. Rails 3.1 is also compatible with Ruby 1.9.2.
-TIP: Note that Ruby 1.8.7 p248 and p249 have marshaling bugs that crash Rails. Ruby Enterprise Edition have these fixed since release 1.8.7-2010.02 though. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults, so if you want to use 1.9.x jump on 1.9.2 for smooth sailing.
+TIP: Note that Ruby 1.8.7 p248 and p249 have marshalling bugs that crash Rails. Ruby Enterprise Edition have these fixed since release 1.8.7-2010.02 though. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults, so if you want to use 1.9.x jump on 1.9.2 for smooth sailing.
### What to update in your apps
@@ -99,7 +99,7 @@ gem 'jquery-rails'
# config.assets.manifest = YOUR_PATH
# Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added)
- # config.assets.precompile `= %w( search.js )
+ # config.assets.precompile `= %w( admin.js admin.css )
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
@@ -151,7 +151,7 @@ $ cd myapp
Rails now uses a `Gemfile` in the application root to determine the gems you require for your application to start. This `Gemfile` is processed by the [Bundler](https://github.com/carlhuda/bundler) gem, which then installs all your dependencies. It can even install all the dependencies locally to your application so that it doesn't depend on the system gems.
-More information: - [bundler homepage](http://bundler.io/)
+More information: - [bundler homepage](https://bundler.io/)
### Living on the Edge
@@ -558,4 +558,4 @@ Credits
See the [full list of contributors to Rails](http://contributors.rubyonrails.org/) for the many people who spent many hours making Rails, the stable and robust framework it is. Kudos to all of them.
-Rails 3.1 Release Notes were compiled by [Vijay Dev](https://github.com/vijaydev.)
+Rails 3.1 Release Notes were compiled by [Vijay Dev](https://github.com/vijaydev)
diff --git a/guides/source/3_2_release_notes.md b/guides/source/3_2_release_notes.md
index c52c39b705..d4c9bf357d 100644
--- a/guides/source/3_2_release_notes.md
+++ b/guides/source/3_2_release_notes.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Ruby on Rails 3.2 Release Notes
===============================
@@ -30,13 +30,13 @@ TIP: Note that Ruby 1.8.7 p248 and p249 have marshalling bugs that crash Rails.
### What to update in your apps
-* Update your Gemfile to depend on
+* Update your `Gemfile` to depend on
* `rails = 3.2.0`
* `sass-rails ~> 3.2.3`
* `coffee-rails ~> 3.2.1`
* `uglifier >= 1.0.3`
-* Rails 3.2 deprecates `vendor/plugins` and Rails 4.0 will remove them completely. You can start replacing these plugins by extracting them as gems and adding them in your Gemfile. If you choose not to make them gems, you can move them into, say, `lib/my_plugin/*` and add an appropriate initializer in `config/initializers/my_plugin.rb`.
+* Rails 3.2 deprecates `vendor/plugins` and Rails 4.0 will remove them completely. You can start replacing these plugins by extracting them as gems and adding them in your `Gemfile`. If you choose not to make them gems, you can move them into, say, `lib/my_plugin/*` and add an appropriate initializer in `config/initializers/my_plugin.rb`.
* There are a couple of new configuration changes you'd want to add in `config/environments/development.rb`:
@@ -81,7 +81,7 @@ $ cd myapp
Rails now uses a `Gemfile` in the application root to determine the gems you require for your application to start. This `Gemfile` is processed by the [Bundler](https://github.com/carlhuda/bundler) gem, which then installs all your dependencies. It can even install all the dependencies locally to your application so that it doesn't depend on the system gems.
-More information: [Bundler homepage](http://bundler.io/)
+More information: [Bundler homepage](https://bundler.io/)
### Living on the Edge
@@ -154,9 +154,9 @@ Railties
rails g scaffold Post title:string:index author:uniq price:decimal{7,2}
```
- will create indexes for `title` and `author` with the latter being an unique index. Some types such as decimal accept custom options. In the example, `price` will be a decimal column with precision and scale set to 7 and 2 respectively.
+ will create indexes for `title` and `author` with the latter being a unique index. Some types such as decimal accept custom options. In the example, `price` will be a decimal column with precision and scale set to 7 and 2 respectively.
-* Turn gem has been removed from default Gemfile.
+* Turn gem has been removed from default `Gemfile`.
* Remove old plugin generator `rails generate plugin` in favor of `rails plugin new` command.
@@ -295,7 +295,7 @@ Action Pack
```ruby
@items.each do |item|
content_tag_for(:li, item) do
- Title: <%= item.title %>
+ Title: <%= item.title %>
end
end
```
@@ -327,7 +327,7 @@ Active Record
* Implemented `ActiveRecord::Relation#explain`.
-* Implements `AR::Base.silence_auto_explain` which allows the user to selectively disable automatic EXPLAINs within a block.
+* Implements `ActiveRecord::Base.silence_auto_explain` which allows the user to selectively disable automatic EXPLAINs within a block.
* Implements automatic EXPLAIN logging for slow queries. A new configuration parameter `config.active_record.auto_explain_threshold_in_seconds` determines what's to be considered a slow query. Setting that to nil disables this feature. Defaults are 0.5 in development mode, and nil in test and production modes. Rails 3.2 supports this feature in SQLite, MySQL (mysql2 adapter), and PostgreSQL.
diff --git a/guides/source/4_0_release_notes.md b/guides/source/4_0_release_notes.md
index b9444510ea..c9bc7f937b 100644
--- a/guides/source/4_0_release_notes.md
+++ b/guides/source/4_0_release_notes.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Ruby on Rails 4.0 Release Notes
===============================
@@ -36,7 +36,7 @@ $ cd myapp
Rails now uses a `Gemfile` in the application root to determine the gems you require for your application to start. This `Gemfile` is processed by the [Bundler](https://github.com/carlhuda/bundler) gem, which then installs all your dependencies. It can even install all the dependencies locally to your application so that it doesn't depend on the system gems.
-More information: [Bundler homepage](http://bundler.io)
+More information: [Bundler homepage](https://bundler.io)
### Living on the Edge
@@ -55,22 +55,22 @@ $ ruby /path/to/rails/railties/bin/rails new myapp --dev
Major Features
--------------
-[![Rails 4.0](images/rails4_features.png)](http://guides.rubyonrails.org/images/rails4_features.png)
+[![Rails 4.0](images/4_0_release_notes/rails4_features.png)](https://guides.rubyonrails.org/images/4_0_release_notes/rails4_features.png)
### Upgrade
* **Ruby 1.9.3** ([commit](https://github.com/rails/rails/commit/a0380e808d3dbd2462df17f5d3b7fcd8bd812496)) - Ruby 2.0 preferred; 1.9.3+ required
-* **[New deprecation policy](http://www.youtube.com/watch?v=z6YgD6tVPQs)** - Deprecated features are warnings in Rails 4.0 and will be removed in Rails 4.1.
+* **[New deprecation policy](https://www.youtube.com/watch?v=z6YgD6tVPQs)** - Deprecated features are warnings in Rails 4.0 and will be removed in Rails 4.1.
* **ActionPack page and action caching** ([commit](https://github.com/rails/rails/commit/b0a7068564f0c95e7ef28fc39d0335ed17d93e90)) - Page and action caching are extracted to a separate gem. Page and action caching requires too much manual intervention (manually expiring caches when the underlying model objects are updated). Instead, use Russian doll caching.
* **ActiveRecord observers** ([commit](https://github.com/rails/rails/commit/ccecab3ba950a288b61a516bf9b6962e384aae0b)) - Observers are extracted to a separate gem. Observers are only needed for page and action caching, and can lead to spaghetti code.
* **ActiveRecord session store** ([commit](https://github.com/rails/rails/commit/0ffe19056c8e8b2f9ae9d487b896cad2ce9387ad)) - The ActiveRecord session store is extracted to a separate gem. Storing sessions in SQL is costly. Instead, use cookie sessions, memcache sessions, or a custom session store.
* **ActiveModel mass assignment protection** ([commit](https://github.com/rails/rails/commit/f8c9a4d3e88181cee644f91e1342bfe896ca64c6)) - Rails 3 mass assignment protection is deprecated. Instead, use strong parameters.
* **ActiveResource** ([commit](https://github.com/rails/rails/commit/f1637bf2bb00490203503fbd943b73406e043d1d)) - ActiveResource is extracted to a separate gem. ActiveResource was not widely used.
-* **vendor/plugins removed** ([commit](https://github.com/rails/rails/commit/853de2bd9ac572735fa6cf59fcf827e485a231c3)) - Use a Gemfile to manage installed gems.
+* **vendor/plugins removed** ([commit](https://github.com/rails/rails/commit/853de2bd9ac572735fa6cf59fcf827e485a231c3)) - Use a `Gemfile` to manage installed gems.
### ActionPack
-* **Strong parameters** ([commit](https://github.com/rails/rails/commit/a8f6d5c6450a7fe058348a7f10a908352bb6c7fc)) - Only allow whitelisted parameters to update model objects (`params.permit(:title, :text)`).
+* **Strong parameters** ([commit](https://github.com/rails/rails/commit/a8f6d5c6450a7fe058348a7f10a908352bb6c7fc)) - Only allow permitted parameters to update model objects (`params.permit(:title, :text)`).
* **Routing concerns** ([commit](https://github.com/rails/rails/commit/0dd24728a088fcb4ae616bb5d62734aca5276b1b)) - In the routing DSL, factor out common subroutes (`comments` from `/posts/1/comments` and `/videos/1/comments`).
* **ActionController::Live** ([commit](https://github.com/rails/rails/commit/af0a9f9eefaee3a8120cfd8d05cbc431af376da3)) - Stream JSON with `response.stream`.
* **Declarative ETags** ([commit](https://github.com/rails/rails/commit/ed5c938fa36995f06d4917d9543ba78ed506bb8d)) - Add controller-level etag additions that will be part of the action etag computation.
@@ -108,7 +108,7 @@ In Rails 4.0, several features have been extracted into gems. You can simply add
* Mass assignment protection in Active Record models ([GitHub](https://github.com/rails/protected_attributes), [Pull Request](https://github.com/rails/rails/pull/7251))
* ActiveRecord::SessionStore ([GitHub](https://github.com/rails/activerecord-session_store), [Pull Request](https://github.com/rails/rails/pull/7436))
* Active Record Observers ([GitHub](https://github.com/rails/rails-observers), [Commit](https://github.com/rails/rails/commit/39e85b3b90c58449164673909a6f1893cba290b2))
-* Active Resource ([GitHub](https://github.com/rails/activeresource), [Pull Request](https://github.com/rails/rails/pull/572), [Blog](http://yetimedia.tumblr.com/post/35233051627/activeresource-is-dead-long-live-activeresource))
+* Active Resource ([GitHub](https://github.com/rails/activeresource), [Pull Request](https://github.com/rails/rails/pull/572), [Blog](http://yetimedia-blog-blog.tumblr.com/post/35233051627/activeresource-is-dead-long-live-activeresource))
* Action Caching ([GitHub](https://github.com/rails/actionpack-action_caching), [Pull Request](https://github.com/rails/rails/pull/7833))
* Page Caching ([GitHub](https://github.com/rails/actionpack-page_caching), [Pull Request](https://github.com/rails/rails/pull/7833))
* Sprockets ([GitHub](https://github.com/rails/sprockets-rails))
@@ -196,7 +196,7 @@ Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/a
### Deprecations
-* Deprecate `ActiveSupport::TestCase#pending` method, use `skip` from MiniTest instead.
+* Deprecate `ActiveSupport::TestCase#pending` method, use `skip` from minitest instead.
* `ActiveSupport::Benchmarkable#silence` has been deprecated due to its lack of thread safety. It will be removed without replacement in Rails 4.1.
diff --git a/guides/source/4_1_release_notes.md b/guides/source/4_1_release_notes.md
index 6bf65757ec..b236f7ca24 100644
--- a/guides/source/4_1_release_notes.md
+++ b/guides/source/4_1_release_notes.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Ruby on Rails 4.1 Release Notes
===============================
@@ -274,7 +274,7 @@ for detailed changes.
* The [Spring application
preloader](https://github.com/rails/spring) is now installed
by default for new applications. It uses the development group of
- the Gemfile, so will not be installed in
+ the `Gemfile`, so will not be installed in
production. ([Pull Request](https://github.com/rails/rails/pull/12958))
* `BACKTRACE` environment variable to show unfiltered backtraces for test
@@ -719,7 +719,7 @@ for detailed changes.
responsibilities within a
class. ([Commit](https://github.com/rails/rails/commit/1eee0ca6de975b42524105a59e0521d18b38ab81))
-* Added `Object#presence_in` to simplify value whitelisting.
+* Added `Object#presence_in` to simplify adding values to a permitted list.
([Commit](https://github.com/rails/rails/commit/4edca106daacc5a159289eae255207d160f22396))
diff --git a/guides/source/4_2_release_notes.md b/guides/source/4_2_release_notes.md
index 684bd286bc..51d06bd07d 100644
--- a/guides/source/4_2_release_notes.md
+++ b/guides/source/4_2_release_notes.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Ruby on Rails 4.2 Release Notes
===============================
@@ -44,7 +44,7 @@ to their respective adapters. Active Job comes pre-configured with an inline
runner that executes jobs right away.
Jobs often need to take Active Record objects as arguments. Active Job passes
-object references as URIs (uniform resource identifiers) instead of marshaling
+object references as URIs (uniform resource identifiers) instead of marshalling
the object itself. The new [Global ID](https://github.com/rails/globalid)
library builds URIs and looks up the objects they reference. Passing Active
Record objects as job arguments just works by using Global ID internally.
@@ -179,7 +179,7 @@ change your code to use the explicit form (`render file: "foo/bar"`) instead.
`respond_with` and the corresponding class-level `respond_to` have been moved
to the [responders](https://github.com/plataformatec/responders) gem. Add
-`gem 'responders', '~> 2.0'` to your Gemfile to use it:
+`gem 'responders', '~> 2.0'` to your `Gemfile` to use it:
```ruby
# app/controllers/users_controller.rb
@@ -227,6 +227,17 @@ restore the old behavior.
If you do this, be sure to configure your firewall properly such that only
trusted machines on your network can access your development server.
+### Changed status option symbols for `render`
+
+Due to a [change in Rack](https://github.com/rack/rack/commit/be28c6a2ac152fe4adfbef71f3db9f4200df89e8), the symbols that the `render` method accepts for the `:status` option have changed:
+
+- 306: `:reserved` has been removed.
+- 413: `:request_entity_too_large` has been renamed to `:payload_too_large`.
+- 414: `:request_uri_too_long` has been renamed to `:uri_too_long`.
+- 416: `:requested_range_not_satisfiable` has been renamed to `:range_not_satisfiable`.
+
+Keep in mind that if calling `render` with an unknown symbol, the response status will default to 500.
+
### HTML Sanitizer
The HTML sanitizer has been replaced with a new, more robust, implementation
@@ -357,7 +368,7 @@ Please refer to the [Changelog][railties] for detailed changes.
### Notable changes
-* Introduced `web-console` in the default application Gemfile.
+* Introduced `web-console` in the default application `Gemfile`.
([Pull Request](https://github.com/rails/rails/pull/11667))
* Added a `required` option to the model generator for associations.
@@ -394,7 +405,7 @@ Please refer to the [Changelog][railties] for detailed changes.
url: http://localhost:3001
namespace: my_app_development
- # config/production.rb
+ # config/environments/production.rb
Rails.application.configure do
config.middleware.use ExceptionNotifier, config_for(:exception_notification)
end
@@ -435,7 +446,7 @@ Please refer to the [Changelog][action-pack] for detailed changes.
moved to the `responders` gem (version 2.0). Add `gem 'responders', '~> 2.0'`
to your `Gemfile` to continue using these features.
([Pull Request](https://github.com/rails/rails/pull/16526),
- [More Details](http://guides.rubyonrails.org/upgrading_ruby_on_rails.html#responders))
+ [More Details](https://guides.rubyonrails.org/upgrading_ruby_on_rails.html#responders))
* Removed deprecated `AbstractController::Helpers::ClassMethods::MissingHelperError`
in favor of `AbstractController::Helpers::MissingHelperError`.
@@ -534,7 +545,7 @@ Please refer to the [Changelog][action-pack] for detailed changes.
served if the client supports it and a pre-generated gzip file (`.gz`) is on disk.
By default the asset pipeline generates `.gz` files for all compressible assets.
Serving gzip files minimizes data transfer and speeds up asset requests. Always
- [use a CDN](http://guides.rubyonrails.org/asset_pipeline.html#cdns) if you are
+ [use a CDN](https://guides.rubyonrails.org/asset_pipeline.html#cdns) if you are
serving assets from your Rails server in production.
([Pull Request](https://github.com/rails/rails/pull/16466))
@@ -860,7 +871,7 @@ Please refer to the [Changelog][active-support] for detailed changes.
`module Foo; extend ActiveSupport::Concern; end` boilerplate.
([Commit](https://github.com/rails/rails/commit/b16c36e688970df2f96f793a759365b248b582ad))
-* New [guide](constant_autoloading_and_reloading.html) about constant autoloading and reloading.
+* New [guide](autoloading_and_reloading_constants.html) about constant autoloading and reloading.
Credits
-------
diff --git a/guides/source/5_0_release_notes.md b/guides/source/5_0_release_notes.md
new file mode 100644
index 0000000000..d63921507d
--- /dev/null
+++ b/guides/source/5_0_release_notes.md
@@ -0,0 +1,1096 @@
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
+
+Ruby on Rails 5.0 Release Notes
+===============================
+
+Highlights in Rails 5.0:
+
+* Action Cable
+* Rails API
+* Active Record Attributes API
+* Test Runner
+* Exclusive use of `rails` CLI over Rake
+* Sprockets 3
+* Turbolinks 5
+* Ruby 2.2.2+ required
+
+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-0-stable) in the main Rails
+repository on GitHub.
+
+--------------------------------------------------------------------------------
+
+Upgrading to Rails 5.0
+----------------------
+
+If you're upgrading an existing application, it's a great idea to have good test
+coverage before going in. You should also first upgrade to Rails 4.2 in case you
+haven't and make sure your application still runs as expected before attempting
+an update to Rails 5.0. A list of things to watch out for when upgrading is
+available in the
+[Upgrading Ruby on Rails](upgrading_ruby_on_rails.html#upgrading-from-rails-4-2-to-rails-5-0)
+guide.
+
+
+Major Features
+--------------
+
+### Action Cable
+
+Action Cable is a new framework in Rails 5. It seamlessly integrates
+[WebSockets](https://en.wikipedia.org/wiki/WebSocket) with the rest of your
+Rails application.
+
+Action Cable allows for real-time features to be written in Ruby in the
+same style and form as the rest of your Rails application, while still being
+performant and scalable. It's a full-stack offering that provides both a
+client-side JavaScript framework and a server-side Ruby framework. You have
+access to your full domain model written with Active Record or your ORM of
+choice.
+
+See the [Action Cable Overview](action_cable_overview.html) guide for more
+information.
+
+### API Applications
+
+Rails can now be used to create slimmed down API only applications.
+This is useful for creating and serving APIs similar to [Twitter](https://dev.twitter.com) or [GitHub](https://developer.github.com) API,
+that can be used to serve public facing, as well as, for custom applications.
+
+You can generate a new api Rails app using:
+
+```bash
+$ rails new my_api --api
+```
+
+This will do three main things:
+
+- Configure your application to start with a more limited set of middleware
+ than normal. Specifically, it will not include any middleware primarily useful
+ for browser applications (like cookies support) by default.
+- Make `ApplicationController` inherit from `ActionController::API` instead of
+ `ActionController::Base`. As with middleware, this will leave out any Action
+ Controller modules that provide functionalities primarily used by browser
+ applications.
+- Configure the generators to skip generating views, helpers, and assets when
+ you generate a new resource.
+
+The application provides a base for APIs,
+that can then be [configured to pull in functionality](api_app.html) as suitable for the application's needs.
+
+See the [Using Rails for API-only Applications](api_app.html) guide for more
+information.
+
+### Active Record attributes API
+
+Defines an attribute with a type on a model. It will override the type of existing attributes if needed.
+This allows control over how values are converted to and from SQL when assigned to a model.
+It also changes the behavior of values passed to `ActiveRecord::Base.where`, which lets use our domain objects across much of Active Record,
+without having to rely on implementation details or monkey patching.
+
+Some things that you can achieve with this:
+
+- The type detected by Active Record can be overridden.
+- A default can also be provided.
+- Attributes do not need to be backed by a database column.
+
+```ruby
+
+# db/schema.rb
+create_table :store_listings, force: true do |t|
+ t.decimal :price_in_cents
+ t.string :my_string, default: "original default"
+end
+
+# app/models/store_listing.rb
+class StoreListing < ActiveRecord::Base
+end
+
+store_listing = StoreListing.new(price_in_cents: '10.1')
+
+# before
+store_listing.price_in_cents # => BigDecimal.new(10.1)
+StoreListing.new.my_string # => "original default"
+
+class StoreListing < ActiveRecord::Base
+ attribute :price_in_cents, :integer # custom type
+ attribute :my_string, :string, default: "new default" # default value
+ attribute :my_default_proc, :datetime, default: -> { Time.now } # default value
+ attribute :field_without_db_column, :integer, array: true
+end
+
+# after
+store_listing.price_in_cents # => 10
+StoreListing.new.my_string # => "new default"
+StoreListing.new.my_default_proc # => 2015-05-30 11:04:48 -0600
+model = StoreListing.new(field_without_db_column: ["1", "2", "3"])
+model.attributes # => {field_without_db_column: [1, 2, 3]}
+```
+
+**Creating Custom Types:**
+
+You can define your own custom types, as long as they respond
+to the methods defined on the value type. The method `deserialize` or
+`cast` will be called on your type object, with raw input from the
+database or from your controllers. This is useful, for example, when doing custom conversion,
+like Money data.
+
+**Querying:**
+
+When `ActiveRecord::Base.where` is called, it will
+use the type defined by the model class to convert the value to SQL,
+calling `serialize` on your type object.
+
+This gives the objects ability to specify, how to convert values when performing SQL queries.
+
+**Dirty Tracking:**
+
+The type of an attribute is given the opportunity to change how dirty
+tracking is performed.
+
+See its
+[documentation](http://api.rubyonrails.org/v5.0.1/classes/ActiveRecord/Attributes/ClassMethods.html)
+for a detailed write up.
+
+
+### Test Runner
+
+A new test runner has been introduced to enhance the capabilities of running tests from Rails.
+To use this test runner simply type `bin/rails test`.
+
+Test Runner is inspired from `RSpec`, `minitest-reporters`, `maxitest` and others.
+It includes some of these notable advancements:
+
+- Run a single test using line number of test.
+- Run multiple tests pinpointing to line number of tests.
+- Improved failure messages, which also add ease of re-running failed tests.
+- Fail fast using `-f` option, to stop tests immediately on occurrence of failure,
+instead of waiting for the suite to complete.
+- Defer test output until the end of a full test run using the `-d` option.
+- Complete exception backtrace output using `-b` option.
+- Integration with minitest to allow options like `-s` for test seed data,
+`-n` for running specific test by name, `-v` for better verbose output and so forth.
+- Colored test output.
+
+Railties
+--------
+
+Please refer to the [Changelog][railties] for detailed changes.
+
+### Removals
+
+* Removed debugger support, use byebug instead. `debugger` is not supported by
+ Ruby
+ 2.2. ([commit](https://github.com/rails/rails/commit/93559da4826546d07014f8cfa399b64b4a143127))
+
+* Removed deprecated `test:all` and `test:all:db` tasks.
+ ([commit](https://github.com/rails/rails/commit/f663132eef0e5d96bf2a58cec9f7c856db20be7c))
+
+* Removed deprecated `Rails::Rack::LogTailer`.
+ ([commit](https://github.com/rails/rails/commit/c564dcb75c191ab3d21cc6f920998b0d6fbca623))
+
+* Removed deprecated `RAILS_CACHE` constant.
+ ([commit](https://github.com/rails/rails/commit/b7f856ce488ef8f6bf4c12bb549f462cb7671c08))
+
+* Removed deprecated `serve_static_assets` configuration.
+ ([commit](https://github.com/rails/rails/commit/463b5d7581ee16bfaddf34ca349b7d1b5878097c))
+
+* Removed the documentation tasks `doc:app`, `doc:rails`, and `doc:guides`.
+ ([commit](https://github.com/rails/rails/commit/cd7cc5254b090ccbb84dcee4408a5acede25ef2a))
+
+* Removed `Rack::ContentLength` middleware from the default
+ stack. ([Commit](https://github.com/rails/rails/commit/56903585a099ab67a7acfaaef0a02db8fe80c450))
+
+### Deprecations
+
+* Deprecated `config.static_cache_control` in favor of
+ `config.public_file_server.headers`.
+ ([Pull Request](https://github.com/rails/rails/pull/19135))
+
+* Deprecated `config.serve_static_files` in favor of `config.public_file_server.enabled`.
+ ([Pull Request](https://github.com/rails/rails/pull/22173))
+
+* Deprecated the tasks in the `rails` task namespace in favor of the `app` namespace.
+ (e.g. `rails:update` and `rails:template` tasks are renamed to `app:update` and `app:template`.)
+ ([Pull Request](https://github.com/rails/rails/pull/23439))
+
+### Notable changes
+
+* Added Rails test runner `bin/rails test`.
+ ([Pull Request](https://github.com/rails/rails/pull/19216))
+
+* Newly generated applications and plugins get a `README.md` in Markdown.
+ ([commit](https://github.com/rails/rails/commit/89a12c931b1f00b90e74afffcdc2fc21f14ca663),
+ [Pull Request](https://github.com/rails/rails/pull/22068))
+
+* Added `bin/rails restart` task to restart your Rails app by touching `tmp/restart.txt`.
+ ([Pull Request](https://github.com/rails/rails/pull/18965))
+
+* Added `bin/rails initializers` task to print out all defined initializers in
+ the order they are invoked by Rails.
+ ([Pull Request](https://github.com/rails/rails/pull/19323))
+
+* Added `bin/rails dev:cache` to enable or disable caching in development mode.
+ ([Pull Request](https://github.com/rails/rails/pull/20961))
+
+* Added `bin/update` script to update the development environment automatically.
+ ([Pull Request](https://github.com/rails/rails/pull/20972))
+
+* Proxy Rake tasks through `bin/rails`.
+ ([Pull Request](https://github.com/rails/rails/pull/22457),
+ [Pull Request](https://github.com/rails/rails/pull/22288))
+
+* New applications are generated with the evented file system monitor enabled
+ on Linux and 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))
+
+* Generate applications with an option to log to STDOUT in production
+ using the environment variable `RAILS_LOG_TO_STDOUT`.
+ ([Pull Request](https://github.com/rails/rails/pull/23734))
+
+* Enable HSTS with IncludeSudomains header for new applications.
+ ([Pull Request](https://github.com/rails/rails/pull/23852))
+
+* The application generator writes a new file `config/spring.rb`, which tells
+ Spring to watch additional common files.
+ ([commit](https://github.com/rails/rails/commit/b04d07337fd7bc17e88500e9d6bcd361885a45f8))
+
+* Added `--skip-action-mailer` to skip Action Mailer while generating new app.
+ ([Pull Request](https://github.com/rails/rails/pull/18288))
+
+* Removed `tmp/sessions` directory and the clear rake task associated with it.
+ ([Pull Request](https://github.com/rails/rails/pull/18314))
+
+* Changed `_form.html.erb` generated by scaffold generator to use local variables.
+ ([Pull Request](https://github.com/rails/rails/pull/13434))
+
+* Disabled autoloading of classes in production environment.
+ ([commit](https://github.com/rails/rails/commit/a71350cae0082193ad8c66d65ab62e8bb0b7853b))
+
+Action Pack
+-----------
+
+Please refer to the [Changelog][action-pack] for detailed changes.
+
+### Removals
+
+* Removed `ActionDispatch::Request::Utils.deep_munge`.
+ ([commit](https://github.com/rails/rails/commit/52cf1a71b393486435fab4386a8663b146608996))
+
+* Removed `ActionController::HideActions`.
+ ([Pull Request](https://github.com/rails/rails/pull/18371))
+
+* Removed `respond_to` and `respond_with` placeholder methods, this functionality
+ has been extracted to the
+ [responders](https://github.com/plataformatec/responders) gem.
+ ([commit](https://github.com/rails/rails/commit/afd5e9a7ff0072e482b0b0e8e238d21b070b6280))
+
+* Removed deprecated assertion files.
+ ([commit](https://github.com/rails/rails/commit/92e27d30d8112962ee068f7b14aa7b10daf0c976))
+
+* Removed deprecated usage of string keys in URL helpers.
+ ([commit](https://github.com/rails/rails/commit/34e380764edede47f7ebe0c7671d6f9c9dc7e809))
+
+* Removed deprecated `only_path` option on `*_path` helpers.
+ ([commit](https://github.com/rails/rails/commit/e4e1fd7ade47771067177254cb133564a3422b8a))
+
+* Removed deprecated `NamedRouteCollection#helpers`.
+ ([commit](https://github.com/rails/rails/commit/2cc91c37bc2e32b7a04b2d782fb8f4a69a14503f))
+
+* Removed deprecated support to define routes with `:to` option that doesn't contain `#`.
+ ([commit](https://github.com/rails/rails/commit/1f3b0a8609c00278b9a10076040ac9c90a9cc4a6))
+
+* Removed deprecated `ActionDispatch::Response#to_ary`.
+ ([commit](https://github.com/rails/rails/commit/4b19d5b7bcdf4f11bd1e2e9ed2149a958e338c01))
+
+* Removed deprecated `ActionDispatch::Request#deep_munge`.
+ ([commit](https://github.com/rails/rails/commit/7676659633057dacd97b8da66e0d9119809b343e))
+
+* Removed deprecated
+ `ActionDispatch::Http::Parameters#symbolized_path_parameters`.
+ ([commit](https://github.com/rails/rails/commit/7fe7973cd8bd119b724d72c5f617cf94c18edf9e))
+
+* Removed deprecated option `use_route` in controller tests.
+ ([commit](https://github.com/rails/rails/commit/e4cfd353a47369dd32198b0e67b8cbb2f9a1c548))
+
+* Removed `assigns` and `assert_template`. Both methods have been extracted
+ into the
+ [rails-controller-testing](https://github.com/rails/rails-controller-testing)
+ gem.
+ ([Pull Request](https://github.com/rails/rails/pull/20138))
+
+### Deprecations
+
+* Deprecated all `*_filter` callbacks in favor of `*_action` callbacks.
+ ([Pull Request](https://github.com/rails/rails/pull/18410))
+
+* Deprecated `*_via_redirect` integration test methods. Use `follow_redirect!`
+ manually after the request call for the same behavior.
+ ([Pull Request](https://github.com/rails/rails/pull/18693))
+
+* Deprecated `AbstractController#skip_action_callback` in favor of individual
+ skip_callback methods.
+ ([Pull Request](https://github.com/rails/rails/pull/19060))
+
+* Deprecated `:nothing` option for `render` method.
+ ([Pull Request](https://github.com/rails/rails/pull/20336))
+
+* Deprecated passing first parameter as `Hash` and default status code for
+ `head` method.
+ ([Pull Request](https://github.com/rails/rails/pull/20407))
+
+* Deprecated using strings or symbols for middleware class names. Use class
+ names instead.
+ ([commit](https://github.com/rails/rails/commit/83b767ce))
+
+* Deprecated accessing mime types via constants (eg. `Mime::HTML`). Use the
+ subscript operator with a symbol instead (eg. `Mime[:html]`).
+ ([Pull Request](https://github.com/rails/rails/pull/21869))
+
+* Deprecated `redirect_to :back` in favor of `redirect_back`, which accepts a
+ required `fallback_location` argument, thus eliminating the possibility of a
+ `RedirectBackError`.
+ ([Pull Request](https://github.com/rails/rails/pull/22506))
+
+* `ActionDispatch::IntegrationTest` and `ActionController::TestCase` deprecate positional arguments in favor of
+ keyword arguments. ([Pull Request](https://github.com/rails/rails/pull/18323))
+
+* Deprecated `:controller` and `:action` path parameters.
+ ([Pull Request](https://github.com/rails/rails/pull/23980))
+
+* Deprecated env method on controller instances.
+ ([commit](https://github.com/rails/rails/commit/05934d24aff62d66fc62621aa38dae6456e276be))
+
+* `ActionDispatch::ParamsParser` is deprecated and was removed from the
+ middleware stack. To configure the parameter parsers use
+ `ActionDispatch::Request.parameter_parsers=`.
+ ([commit](https://github.com/rails/rails/commit/38d2bf5fd1f3e014f2397898d371c339baa627b1),
+ [commit](https://github.com/rails/rails/commit/5ed38014811d4ce6d6f957510b9153938370173b))
+
+### Notable changes
+
+* Added `ActionController::Renderer` to render arbitrary templates
+ outside controller actions.
+ ([Pull Request](https://github.com/rails/rails/pull/18546))
+
+* Migrating to keyword arguments syntax in `ActionController::TestCase` and
+ `ActionDispatch::Integration` HTTP request methods.
+ ([Pull Request](https://github.com/rails/rails/pull/18323))
+
+* Added `http_cache_forever` to Action Controller, so we can cache a response
+ that never gets expired.
+ ([Pull Request](https://github.com/rails/rails/pull/18394))
+
+* Provide friendlier access to request variants.
+ ([Pull Request](https://github.com/rails/rails/pull/18939))
+
+* For actions with no corresponding templates, render `head :no_content`
+ instead of raising an error.
+ ([Pull Request](https://github.com/rails/rails/pull/19377))
+
+* Added the ability to override default form builder for a controller.
+ ([Pull Request](https://github.com/rails/rails/pull/19736))
+
+* Added support for API only apps.
+ `ActionController::API` is added as a replacement of
+ `ActionController::Base` for this kind of applications.
+ ([Pull Request](https://github.com/rails/rails/pull/19832))
+
+* Make `ActionController::Parameters` no longer inherits from
+ `HashWithIndifferentAccess`.
+ ([Pull Request](https://github.com/rails/rails/pull/20868))
+
+* Make it easier to opt in to `config.force_ssl` and `config.ssl_options` by
+ making them less dangerous to try and easier to disable.
+ ([Pull Request](https://github.com/rails/rails/pull/21520))
+
+* Added the ability of returning arbitrary headers to `ActionDispatch::Static`.
+ ([Pull Request](https://github.com/rails/rails/pull/19135))
+
+* Changed the `protect_from_forgery` prepend default to `false`.
+ ([commit](https://github.com/rails/rails/commit/39794037817703575c35a75f1961b01b83791191))
+
+* `ActionController::TestCase` will be moved to its own gem in Rails 5.1. Use
+ `ActionDispatch::IntegrationTest` instead.
+ ([commit](https://github.com/rails/rails/commit/4414c5d1795e815b102571425974a8b1d46d932d))
+
+* Rails generates weak ETags by default.
+ ([Pull Request](https://github.com/rails/rails/pull/17573))
+
+* Controller actions without an explicit `render` call and with no
+ corresponding templates will render `head :no_content` implicitly
+ instead of raising an error.
+ (Pull Request [1](https://github.com/rails/rails/pull/19377),
+ [2](https://github.com/rails/rails/pull/23827))
+
+* Added an option for per-form CSRF tokens.
+ ([Pull Request](https://github.com/rails/rails/pull/22275))
+
+* Added request encoding and response parsing to integration tests.
+ ([Pull Request](https://github.com/rails/rails/pull/21671))
+
+* Add `ActionController#helpers` to get access to the view context
+ at the controller level.
+ ([Pull Request](https://github.com/rails/rails/pull/24866))
+
+* Discarded flash messages get removed before storing into session.
+ ([Pull Request](https://github.com/rails/rails/pull/18721))
+
+* Added support for passing collection of records to `fresh_when` and
+ `stale?`.
+ ([Pull Request](https://github.com/rails/rails/pull/18374))
+
+* `ActionController::Live` became an `ActiveSupport::Concern`. That
+ means it can't be just included in other modules without extending
+ them with `ActiveSupport::Concern` or `ActionController::Live`
+ won't take effect in production. Some people may be using another
+ module to include some special `Warden`/`Devise` authentication
+ failure handling code as well since the middleware can't catch a
+ `:warden` thrown by a spawned thread which is the case when using
+ `ActionController::Live`.
+ ([More details in this issue](https://github.com/rails/rails/issues/25581))
+
+* Introduce `Response#strong_etag=` and `#weak_etag=` and analogous
+ options for `fresh_when` and `stale?`.
+ ([Pull Request](https://github.com/rails/rails/pull/24387))
+
+Action View
+-------------
+
+Please refer to the [Changelog][action-view] for detailed changes.
+
+### Removals
+
+* Removed deprecated `AbstractController::Base::parent_prefixes`.
+ ([commit](https://github.com/rails/rails/commit/34bcbcf35701ca44be559ff391535c0dd865c333))
+
+* Removed `ActionView::Helpers::RecordTagHelper`, this functionality
+ has been extracted to the
+ [record_tag_helper](https://github.com/rails/record_tag_helper) gem.
+ ([Pull Request](https://github.com/rails/rails/pull/18411))
+
+* Removed `:rescue_format` option for `translate` helper since it's no longer
+ supported by I18n.
+ ([Pull Request](https://github.com/rails/rails/pull/20019))
+
+### Notable Changes
+
+* Changed the default template handler from `ERB` to `Raw`.
+ ([commit](https://github.com/rails/rails/commit/4be859f0fdf7b3059a28d03c279f03f5938efc80))
+
+* Collection rendering can cache and fetches multiple partials at once.
+ ([Pull Request](https://github.com/rails/rails/pull/18948),
+ [commit](https://github.com/rails/rails/commit/e93f0f0f133717f9b06b1eaefd3442bd0ff43985))
+
+* Added wildcard matching to explicit dependencies.
+ ([Pull Request](https://github.com/rails/rails/pull/20904))
+
+* Make `disable_with` the default behavior for submit tags. Disables the
+ button on submit to prevent double submits.
+ ([Pull Request](https://github.com/rails/rails/pull/21135))
+
+* Partial template name no longer has to be a valid Ruby identifier.
+ ([commit](https://github.com/rails/rails/commit/da9038e))
+
+* The `datetime_tag` helper now generates an input tag with the type of
+ `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
+-------------
+
+Please refer to the [Changelog][action-mailer] for detailed changes.
+
+### Removals
+
+* Removed deprecated `*_path` helpers in email views.
+ ([commit](https://github.com/rails/rails/commit/d282125a18c1697a9b5bb775628a2db239142ac7))
+
+* Removed deprecated `deliver` and `deliver!` methods.
+ ([commit](https://github.com/rails/rails/commit/755dcd0691f74079c24196135f89b917062b0715))
+
+### Notable changes
+
+* Template lookup now respects default locale and I18n fallbacks.
+ ([commit](https://github.com/rails/rails/commit/ecb1981b))
+
+* Added `_mailer` suffix to mailers created via generator, following the same
+ naming convention used in controllers and jobs.
+ ([Pull Request](https://github.com/rails/rails/pull/18074))
+
+* Added `assert_enqueued_emails` and `assert_no_enqueued_emails`.
+ ([Pull Request](https://github.com/rails/rails/pull/18403))
+
+* Added `config.action_mailer.deliver_later_queue_name` configuration to set
+ the mailer queue name.
+ ([Pull Request](https://github.com/rails/rails/pull/18587))
+
+* Added support for fragment caching in Action Mailer views.
+ Added new config option `config.action_mailer.perform_caching` to determine
+ whether your templates should perform caching or not.
+ ([Pull Request](https://github.com/rails/rails/pull/22825))
+
+
+Active Record
+-------------
+
+Please refer to the [Changelog][active-record] for detailed changes.
+
+### Removals
+
+* Removed deprecated behavior allowing nested arrays to be passed as query
+ values. ([Pull Request](https://github.com/rails/rails/pull/17919))
+
+* Removed deprecated `ActiveRecord::Tasks::DatabaseTasks#load_schema`. This
+ method was replaced by `ActiveRecord::Tasks::DatabaseTasks#load_schema_for`.
+ ([commit](https://github.com/rails/rails/commit/ad783136d747f73329350b9bb5a5e17c8f8800da))
+
+* Removed deprecated `serialized_attributes`.
+ ([commit](https://github.com/rails/rails/commit/82043ab53cb186d59b1b3be06122861758f814b2))
+
+* Removed deprecated automatic counter caches on `has_many :through`.
+ ([commit](https://github.com/rails/rails/commit/87c8ce340c6c83342df988df247e9035393ed7a0))
+
+* Removed deprecated `sanitize_sql_hash_for_conditions`.
+ ([commit](https://github.com/rails/rails/commit/3a59dd212315ebb9bae8338b98af259ac00bbef3))
+
+* Removed deprecated `Reflection#source_macro`.
+ ([commit](https://github.com/rails/rails/commit/ede8c199a85cfbb6457d5630ec1e285e5ec49313))
+
+* Removed deprecated `symbolized_base_class` and `symbolized_sti_name`.
+ ([commit](https://github.com/rails/rails/commit/9013e28e52eba3a6ffcede26f85df48d264b8951))
+
+* Removed deprecated `ActiveRecord::Base.disable_implicit_join_references=`.
+ ([commit](https://github.com/rails/rails/commit/0fbd1fc888ffb8cbe1191193bf86933110693dfc))
+
+* Removed deprecated access to connection specification using a string accessor.
+ ([commit](https://github.com/rails/rails/commit/efdc20f36ccc37afbb2705eb9acca76dd8aabd4f))
+
+* Removed deprecated support to preload instance-dependent associations.
+ ([commit](https://github.com/rails/rails/commit/4ed97979d14c5e92eb212b1a629da0a214084078))
+
+* Removed deprecated support for PostgreSQL ranges with exclusive lower bounds.
+ ([commit](https://github.com/rails/rails/commit/a076256d63f64d194b8f634890527a5ed2651115))
+
+* Removed deprecation when modifying a relation with cached Arel.
+ This raises an `ImmutableRelation` error instead.
+ ([commit](https://github.com/rails/rails/commit/3ae98181433dda1b5e19910e107494762512a86c))
+
+* Removed `ActiveRecord::Serialization::XmlSerializer` from core. This feature
+ has been extracted into the
+ [activemodel-serializers-xml](https://github.com/rails/activemodel-serializers-xml)
+ 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 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))
+
+* Removed support for the `protected_attributes` gem.
+ ([commit](https://github.com/rails/rails/commit/f4fbc0301021f13ae05c8e941c8efc4ae351fdf9))
+
+* Removed support for PostgreSQL versions below 9.1.
+ ([Pull Request](https://github.com/rails/rails/pull/23434))
+
+* Removed support for `activerecord-deprecated_finders` gem.
+ ([commit](https://github.com/rails/rails/commit/78dab2a8569408658542e462a957ea5a35aa4679))
+
+* Removed `ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES` constant.
+ ([commit](https://github.com/rails/rails/commit/a502703c3d2151d4d3b421b29fefdac5ad05df61))
+
+### Deprecations
+
+* Deprecated passing a class as a value in a query. Users should pass strings
+ instead. ([Pull Request](https://github.com/rails/rails/pull/17916))
+
+* Deprecated returning `false` as a way to halt Active Record callback
+ chains. The recommended way is to
+ `throw(:abort)`. ([Pull Request](https://github.com/rails/rails/pull/17227))
+
+* Deprecated `ActiveRecord::Base.errors_in_transactional_callbacks=`.
+ ([commit](https://github.com/rails/rails/commit/07d3d402341e81ada0214f2cb2be1da69eadfe72))
+
+* Deprecated `Relation#uniq` use `Relation#distinct` instead.
+ ([commit](https://github.com/rails/rails/commit/adfab2dcf4003ca564d78d4425566dd2d9cd8b4f))
+
+* Deprecated the PostgreSQL `:point` type in favor of a new one which will return
+ `Point` objects instead of an `Array`
+ ([Pull Request](https://github.com/rails/rails/pull/20448))
+
+* Deprecated force association reload by passing a truthy argument to
+ association method.
+ ([Pull Request](https://github.com/rails/rails/pull/20888))
+
+* Deprecated the keys for association `restrict_dependent_destroy` errors in favor
+ of new key names.
+ ([Pull Request](https://github.com/rails/rails/pull/20668))
+
+* Synchronize behavior of `#tables`.
+ ([Pull Request](https://github.com/rails/rails/pull/21601))
+
+* Deprecated `SchemaCache#tables`, `SchemaCache#table_exists?` and
+ `SchemaCache#clear_table_cache!` in favor of their new data source
+ counterparts.
+ ([Pull Request](https://github.com/rails/rails/pull/21715))
+
+* Deprecated `connection.tables` on the SQLite3 and MySQL adapters.
+ ([Pull Request](https://github.com/rails/rails/pull/21601))
+
+* Deprecated passing arguments to `#tables` - the `#tables` method of some
+ adapters (mysql2, sqlite3) would return both tables and views while others
+ (postgresql) just return tables. To make their behavior consistent,
+ `#tables` will return only tables in the future.
+ ([Pull Request](https://github.com/rails/rails/pull/21601))
+
+* Deprecated `table_exists?` - The `#table_exists?` method would check both
+ tables and views. To make their behavior consistent with `#tables`,
+ `#table_exists?` will check only tables in the future.
+ ([Pull Request](https://github.com/rails/rails/pull/21601))
+
+* Deprecate sending the `offset` argument to `find_nth`. Please use the
+ `offset` method on relation instead.
+ ([Pull Request](https://github.com/rails/rails/pull/22053))
+
+* Deprecated `{insert|update|delete}_sql` in `DatabaseStatements`.
+ Use the `{insert|update|delete}` public methods instead.
+ ([Pull Request](https://github.com/rails/rails/pull/23086))
+
+* Deprecated `use_transactional_fixtures` in favor of
+ `use_transactional_tests` for more clarity.
+ ([Pull Request](https://github.com/rails/rails/pull/19282))
+
+* Deprecated passing a column to `ActiveRecord::Connection#quote`.
+ ([commit](https://github.com/rails/rails/commit/7bb620869725ad6de603f6a5393ee17df13aa96c))
+
+* Added an option `end` to `find_in_batches` that complements the `start`
+ parameter to specify where to stop batch processing.
+ ([Pull Request](https://github.com/rails/rails/pull/12257))
+
+
+### Notable changes
+
+* Added a `foreign_key` option to `references` while creating the table.
+ ([commit](https://github.com/rails/rails/commit/99a6f9e60ea55924b44f894a16f8de0162cf2702))
+
+* New attributes
+ API. ([commit](https://github.com/rails/rails/commit/8c752c7ac739d5a86d4136ab1e9d0142c4041e58))
+
+* Added `:_prefix`/`:_suffix` option to `enum` definition.
+ ([Pull Request](https://github.com/rails/rails/pull/19813),
+ [Pull Request](https://github.com/rails/rails/pull/20999))
+
+* Added `#cache_key` to `ActiveRecord::Relation`.
+ ([Pull Request](https://github.com/rails/rails/pull/20884))
+
+* Changed the default `null` value for `timestamps` to `false`.
+ ([commit](https://github.com/rails/rails/commit/a939506f297b667291480f26fa32a373a18ae06a))
+
+* Added `ActiveRecord::SecureToken` in order to encapsulate generation of
+ unique tokens for attributes in a model using `SecureRandom`.
+ ([Pull Request](https://github.com/rails/rails/pull/18217))
+
+* Added `:if_exists` option for `drop_table`.
+ ([Pull Request](https://github.com/rails/rails/pull/18597))
+
+* Added `ActiveRecord::Base#accessed_fields`, which can be used to quickly
+ discover which fields were read from a model when you are looking to only
+ select the data you need from the database.
+ ([commit](https://github.com/rails/rails/commit/be9b68038e83a617eb38c26147659162e4ac3d2c))
+
+* Added the `#or` method on `ActiveRecord::Relation`, allowing use of the OR
+ operator to combine WHERE or HAVING clauses.
+ ([commit](https://github.com/rails/rails/commit/b0b37942d729b6bdcd2e3178eda7fa1de203b3d0))
+
+* Added `ActiveRecord::Base.suppress` to prevent the receiver from being saved
+ during the given block.
+ ([Pull Request](https://github.com/rails/rails/pull/18910))
+
+* `belongs_to` will now trigger a validation error by default if the
+ association is not present. You can turn this off on a per-association basis
+ with `optional: true`. Also deprecate `required` option in favor of `optional`
+ for `belongs_to`.
+ ([Pull Request](https://github.com/rails/rails/pull/18937))
+
+* Added `config.active_record.dump_schemas` to configure the behavior of
+ `db:structure:dump`.
+ ([Pull Request](https://github.com/rails/rails/pull/19347))
+
+* Added `config.active_record.warn_on_records_fetched_greater_than` option.
+ ([Pull Request](https://github.com/rails/rails/pull/18846))
+
+* Added a native JSON data type support in MySQL.
+ ([Pull Request](https://github.com/rails/rails/pull/21110))
+
+* Added support for dropping indexes concurrently in PostgreSQL.
+ ([Pull Request](https://github.com/rails/rails/pull/21317))
+
+* Added `#views` and `#view_exists?` methods on connection adapters.
+ ([Pull Request](https://github.com/rails/rails/pull/21609))
+
+* Added `ActiveRecord::Base.ignored_columns` to make some columns
+ invisible from Active Record.
+ ([Pull Request](https://github.com/rails/rails/pull/21720))
+
+* Added `connection.data_sources` and `connection.data_source_exists?`.
+ These methods determine what relations can be used to back Active Record
+ models (usually tables and views).
+ ([Pull Request](https://github.com/rails/rails/pull/21715))
+
+* Allow fixtures files to set the model class in the YAML file itself.
+ ([Pull Request](https://github.com/rails/rails/pull/20574))
+
+* Added ability to default to `uuid` as primary key when generating database
+ migrations. ([Pull Request](https://github.com/rails/rails/pull/21762))
+
+* Added `ActiveRecord::Relation#left_joins` and
+ `ActiveRecord::Relation#left_outer_joins`.
+ ([Pull Request](https://github.com/rails/rails/pull/12071))
+
+* Added `after_{create,update,delete}_commit` callbacks.
+ ([Pull Request](https://github.com/rails/rails/pull/22516))
+
+* Version the API presented to migration classes, so we can change parameter
+ defaults without breaking existing migrations, or forcing them to be
+ rewritten through a deprecation cycle.
+ ([Pull Request](https://github.com/rails/rails/pull/21538))
+
+* `ApplicationRecord` is a new superclass for all app models, analogous to app
+ controllers subclassing `ApplicationController` instead of
+ `ActionController::Base`. This gives apps a single spot to configure app-wide
+ model behavior.
+ ([Pull Request](https://github.com/rails/rails/pull/22567))
+
+* Added ActiveRecord `#second_to_last` and `#third_to_last` methods.
+ ([Pull Request](https://github.com/rails/rails/pull/23583))
+
+* Added ability to annotate database objects (tables, columns, indexes)
+ with comments stored in database metadata for PostgreSQL & MySQL.
+ ([Pull Request](https://github.com/rails/rails/pull/22911))
+
+* Added prepared statements support to `mysql2` adapter, for mysql2 0.4.4+,
+ Previously this was only supported on the deprecated `mysql` legacy adapter.
+ To enable, set `prepared_statements: true` in `config/database.yml`.
+ ([Pull Request](https://github.com/rails/rails/pull/23461))
+
+* Added ability to call `ActionRecord::Relation#update` on relation objects
+ which will run validations on callbacks on all objects in the relation.
+ ([Pull Request](https://github.com/rails/rails/pull/11898))
+
+* Added `:touch` option to the `save` method so that records can be saved without
+ updating timestamps.
+ ([Pull Request](https://github.com/rails/rails/pull/18225))
+
+* Added expression indexes and operator classes support for PostgreSQL.
+ ([commit](https://github.com/rails/rails/commit/edc2b7718725016e988089b5fb6d6fb9d6e16882))
+
+* Added `:index_errors` option to add indexes to errors of nested attributes.
+ ([Pull Request](https://github.com/rails/rails/pull/19686))
+
+* Added support for bidirectional destroy dependencies.
+ ([Pull Request](https://github.com/rails/rails/pull/18548))
+
+* Added support for `after_commit` callbacks in transactional tests.
+ ([Pull Request](https://github.com/rails/rails/pull/18458))
+
+* Added `foreign_key_exists?` method to see if a foreign key exists on a table
+ or not.
+ ([Pull Request](https://github.com/rails/rails/pull/18662))
+
+* Added `:time` option to `touch` method to touch records with different time
+ than the current time.
+ ([Pull Request](https://github.com/rails/rails/pull/18956))
+
+* Change transaction callbacks to not swallow errors.
+ Before this change any errors raised inside a transaction callback
+ were getting rescued and printed in the logs, unless you used
+ the (newly deprecated) `raise_in_transactional_callbacks = true` option.
+
+ Now these errors are not rescued anymore and just bubble up, matching the
+ behavior of other callbacks.
+ ([commit](https://github.com/rails/rails/commit/07d3d402341e81ada0214f2cb2be1da69eadfe72))
+
+Active Model
+------------
+
+Please refer to the [Changelog][active-model] for detailed changes.
+
+### Removals
+
+* Removed deprecated `ActiveModel::Dirty#reset_#{attribute}` and
+ `ActiveModel::Dirty#reset_changes`.
+ ([Pull Request](https://github.com/rails/rails/commit/37175a24bd508e2983247ec5d011d57df836c743))
+
+* Removed XML serialization. This feature has been extracted into the
+ [activemodel-serializers-xml](https://github.com/rails/activemodel-serializers-xml) gem.
+ ([Pull Request](https://github.com/rails/rails/pull/21161))
+
+* Removed `ActionController::ModelNaming` module.
+ ([Pull Request](https://github.com/rails/rails/pull/18194))
+
+### Deprecations
+
+* Deprecated returning `false` as a way to halt Active Model and
+ `ActiveModel::Validations` callback chains. The recommended way is to
+ `throw(:abort)`. ([Pull Request](https://github.com/rails/rails/pull/17227))
+
+* Deprecated `ActiveModel::Errors#get`, `ActiveModel::Errors#set` and
+ `ActiveModel::Errors#[]=` methods that have inconsistent behavior.
+ ([Pull Request](https://github.com/rails/rails/pull/18634))
+
+* Deprecated the `:tokenizer` option for `validates_length_of`, in favor of
+ plain Ruby.
+ ([Pull Request](https://github.com/rails/rails/pull/19585))
+
+* Deprecated `ActiveModel::Errors#add_on_empty` and `ActiveModel::Errors#add_on_blank`
+ with no replacement.
+ ([Pull Request](https://github.com/rails/rails/pull/18996))
+
+### Notable changes
+
+* Added `ActiveModel::Errors#details` to determine what validator has failed.
+ ([Pull Request](https://github.com/rails/rails/pull/18322))
+
+* Extracted `ActiveRecord::AttributeAssignment` to `ActiveModel::AttributeAssignment`
+ allowing to use it for any object as an includable module.
+ ([Pull Request](https://github.com/rails/rails/pull/10776))
+
+* Added `ActiveModel::Dirty#[attr_name]_previously_changed?` and
+ `ActiveModel::Dirty#[attr_name]_previous_change` to improve access
+ to recorded changes after the model has been saved.
+ ([Pull Request](https://github.com/rails/rails/pull/19847))
+
+* Validate multiple contexts on `valid?` and `invalid?` at once.
+ ([Pull Request](https://github.com/rails/rails/pull/21069))
+
+* Change `validates_acceptance_of` to accept `true` as default value
+ apart from `1`.
+ ([Pull Request](https://github.com/rails/rails/pull/18439))
+
+Active Job
+-----------
+
+Please refer to the [Changelog][active-job] for detailed changes.
+
+### Notable changes
+
+* `ActiveJob::Base.deserialize` delegates to the job class. This allows jobs
+ to attach arbitrary metadata when they get serialized and read it back when
+ they get performed.
+ ([Pull Request](https://github.com/rails/rails/pull/18260))
+
+* Add ability to configure the queue adapter on a per job basis without
+ affecting each other.
+ ([Pull Request](https://github.com/rails/rails/pull/16992))
+
+* A generated job now inherits from `app/jobs/application_job.rb` by default.
+ ([Pull Request](https://github.com/rails/rails/pull/19034))
+
+* Allow `DelayedJob`, `Sidekiq`, `qu`, `que`, and `queue_classic` to report
+ the job id back to `ActiveJob::Base` as `provider_job_id`.
+ ([Pull Request](https://github.com/rails/rails/pull/20064),
+ [Pull Request](https://github.com/rails/rails/pull/20056),
+ [commit](https://github.com/rails/rails/commit/68e3279163d06e6b04e043f91c9470e9259bbbe0))
+
+* Implement a simple `AsyncJob` processor and associated `AsyncAdapter` that
+ queue jobs to a `concurrent-ruby` thread pool.
+ ([Pull Request](https://github.com/rails/rails/pull/21257))
+
+* Change the default adapter from inline to async. It's a better default as
+ tests will then not mistakenly come to rely on behavior happening
+ synchronously.
+ ([commit](https://github.com/rails/rails/commit/625baa69d14881ac49ba2e5c7d9cac4b222d7022))
+
+Active Support
+--------------
+
+Please refer to the [Changelog][active-support] for detailed changes.
+
+### Removals
+
+* Removed deprecated `ActiveSupport::JSON::Encoding::CircularReferenceError`.
+ ([commit](https://github.com/rails/rails/commit/d6e06ea8275cdc3f126f926ed9b5349fde374b10))
+
+* Removed deprecated methods `ActiveSupport::JSON::Encoding.encode_big_decimal_as_string=`
+ and `ActiveSupport::JSON::Encoding.encode_big_decimal_as_string`.
+ ([commit](https://github.com/rails/rails/commit/c8019c0611791b2716c6bed48ef8dcb177b7869c))
+
+* Removed deprecated `ActiveSupport::SafeBuffer#prepend`.
+ ([commit](https://github.com/rails/rails/commit/e1c8b9f688c56aaedac9466a4343df955b4a67ec))
+
+* Removed deprecated methods from `Kernel`. `silence_stderr`, `silence_stream`,
+ `capture` and `quietly`.
+ ([commit](https://github.com/rails/rails/commit/481e49c64f790e46f4aff3ed539ed227d2eb46cb))
+
+* Removed deprecated `active_support/core_ext/big_decimal/yaml_conversions`
+ file.
+ ([commit](https://github.com/rails/rails/commit/98ea19925d6db642731741c3b91bd085fac92241))
+
+* Removed deprecated methods `ActiveSupport::Cache::Store.instrument` and
+ `ActiveSupport::Cache::Store.instrument=`.
+ ([commit](https://github.com/rails/rails/commit/a3ce6ca30ed0e77496c63781af596b149687b6d7))
+
+* Removed deprecated `Class#superclass_delegating_accessor`.
+ Use `Class#class_attribute` instead.
+ ([Pull Request](https://github.com/rails/rails/pull/16938))
+
+* Removed deprecated `ThreadSafe::Cache`. Use `Concurrent::Map` instead.
+ ([Pull Request](https://github.com/rails/rails/pull/21679))
+
+* Removed `Object#itself` as it is implemented in Ruby 2.2.
+ ([Pull Request](https://github.com/rails/rails/pull/18244))
+
+### Deprecations
+
+* Deprecated `MissingSourceFile` in favor of `LoadError`.
+ ([commit](https://github.com/rails/rails/commit/734d97d2))
+
+* Deprecated `alias_method_chain` in favour of `Module#prepend` introduced in
+ Ruby 2.0.
+ ([Pull Request](https://github.com/rails/rails/pull/19434))
+
+* Deprecated `ActiveSupport::Concurrency::Latch` in favor of
+ `Concurrent::CountDownLatch` from concurrent-ruby.
+ ([Pull Request](https://github.com/rails/rails/pull/20866))
+
+* Deprecated `:prefix` option of `number_to_human_size` with no replacement.
+ ([Pull Request](https://github.com/rails/rails/pull/21191))
+
+* Deprecated `Module#qualified_const_` in favour of the builtin
+ `Module#const_` methods.
+ ([Pull Request](https://github.com/rails/rails/pull/17845))
+
+* Deprecated passing string to define callback.
+ ([Pull Request](https://github.com/rails/rails/pull/22598))
+
+* Deprecated `ActiveSupport::Cache::Store#namespaced_key`,
+ `ActiveSupport::Cache::MemCachedStore#escape_key`, and
+ `ActiveSupport::Cache::FileStore#key_file_path`.
+ Use `normalize_key` instead.
+ ([Pull Request](https://github.com/rails/rails/pull/22215),
+ [commit](https://github.com/rails/rails/commit/a8f773b0))
+
+* Deprecated `ActiveSupport::Cache::LocaleCache#set_cache_value` in favor of `write_cache_value`.
+ ([Pull Request](https://github.com/rails/rails/pull/22215))
+
+* Deprecated passing arguments to `assert_nothing_raised`.
+ ([Pull Request](https://github.com/rails/rails/pull/23789))
+
+* Deprecated `Module.local_constants` in favor of `Module.constants(false)`.
+ ([Pull Request](https://github.com/rails/rails/pull/23936))
+
+
+### Notable changes
+
+* Added `#verified` and `#valid_message?` methods to
+ `ActiveSupport::MessageVerifier`.
+ ([Pull Request](https://github.com/rails/rails/pull/17727))
+
+* Changed the way in which callback chains can be halted. The preferred method
+ to halt a callback chain from now on is to explicitly `throw(:abort)`.
+ ([Pull Request](https://github.com/rails/rails/pull/17227))
+
+* New config option
+ `config.active_support.halt_callback_chains_on_return_false` to specify
+ whether ActiveRecord, ActiveModel, and ActiveModel::Validations callback
+ chains can be halted by returning `false` in a 'before' callback.
+ ([Pull Request](https://github.com/rails/rails/pull/17227))
+
+* Changed the default test order from `:sorted` to `:random`.
+ ([commit](https://github.com/rails/rails/commit/5f777e4b5ee2e3e8e6fd0e2a208ec2a4d25a960d))
+
+* Added `#on_weekend?`, `#on_weekday?`, `#next_weekday`, `#prev_weekday` methods to `Date`,
+ `Time`, and `DateTime`.
+ ([Pull Request](https://github.com/rails/rails/pull/18335),
+ [Pull Request](https://github.com/rails/rails/pull/23687))
+
+* Added `same_time` option to `#next_week` and `#prev_week` for `Date`, `Time`,
+ and `DateTime`.
+ ([Pull Request](https://github.com/rails/rails/pull/18335))
+
+* Added `#prev_day` and `#next_day` counterparts to `#yesterday` and
+ `#tomorrow` for `Date`, `Time`, and `DateTime`.
+ ([Pull Request](https://github.com/rails/rails/pull/18335))
+
+* Added `SecureRandom.base58` for generation of random base58 strings.
+ ([commit](https://github.com/rails/rails/commit/b1093977110f18ae0cafe56c3d99fc22a7d54d1b))
+
+* Added `file_fixture` to `ActiveSupport::TestCase`.
+ It provides a simple mechanism to access sample files in your test cases.
+ ([Pull Request](https://github.com/rails/rails/pull/18658))
+
+* Added `#without` on `Enumerable` and `Array` to return a copy of an
+ enumerable without the specified elements.
+ ([Pull Request](https://github.com/rails/rails/pull/19157))
+
+* Added `ActiveSupport::ArrayInquirer` and `Array#inquiry`.
+ ([Pull Request](https://github.com/rails/rails/pull/18939))
+
+* Added `ActiveSupport::TimeZone#strptime` to allow parsing times as if
+ from a given timezone.
+ ([commit](https://github.com/rails/rails/commit/a5e507fa0b8180c3d97458a9b86c195e9857d8f6))
+
+* Added `Integer#positive?` and `Integer#negative?` query methods
+ in the vein of `Integer#zero?`.
+ ([commit](https://github.com/rails/rails/commit/e54277a45da3c86fecdfa930663d7692fd083daa))
+
+* Added a bang version to `ActiveSupport::OrderedOptions` get methods which will raise
+ an `KeyError` if the value is `.blank?`.
+ ([Pull Request](https://github.com/rails/rails/pull/20208))
+
+* Added `Time.days_in_year` to return the number of days in the given year, or the
+ current year if no argument is provided.
+ ([commit](https://github.com/rails/rails/commit/2f4f4d2cf1e4c5a442459fc250daf66186d110fa))
+
+* Added an evented file watcher to asynchronously detect changes in the
+ application source code, routes, locales, etc.
+ ([Pull Request](https://github.com/rails/rails/pull/22254))
+
+* Added thread_m/cattr_accessor/reader/writer suite of methods for declaring
+ class and module variables that live per-thread.
+ ([Pull Request](https://github.com/rails/rails/pull/22630))
+
+* Added `Array#second_to_last` and `Array#third_to_last` methods.
+ ([Pull Request](https://github.com/rails/rails/pull/23583))
+
+* Publish `ActiveSupport::Executor` and `ActiveSupport::Reloader` APIs to allow
+ components and libraries to manage, and participate in, the execution of
+ application code, and the application reloading process.
+ ([Pull Request](https://github.com/rails/rails/pull/23807))
+
+* `ActiveSupport::Duration` now supports ISO8601 formatting and parsing.
+ ([Pull Request](https://github.com/rails/rails/pull/16917))
+
+* `ActiveSupport::JSON.decode` now supports parsing ISO8601 local times when
+ `parse_json_times` is enabled.
+ ([Pull Request](https://github.com/rails/rails/pull/23011))
+
+* `ActiveSupport::JSON.decode` now return `Date` objects for date strings.
+ ([Pull Request](https://github.com/rails/rails/pull/23011))
+
+* Added ability to `TaggedLogging` to allow loggers to be instantiated multiple
+ times so that they don't share tags with each other.
+ ([Pull Request](https://github.com/rails/rails/pull/9065))
+
+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-0-stable/railties/CHANGELOG.md
+[action-pack]: https://github.com/rails/rails/blob/5-0-stable/actionpack/CHANGELOG.md
+[action-view]: https://github.com/rails/rails/blob/5-0-stable/actionview/CHANGELOG.md
+[action-mailer]: https://github.com/rails/rails/blob/5-0-stable/actionmailer/CHANGELOG.md
+[action-cable]: https://github.com/rails/rails/blob/5-0-stable/actioncable/CHANGELOG.md
+[active-record]: https://github.com/rails/rails/blob/5-0-stable/activerecord/CHANGELOG.md
+[active-model]: https://github.com/rails/rails/blob/5-0-stable/activemodel/CHANGELOG.md
+[active-support]: https://github.com/rails/rails/blob/5-0-stable/activesupport/CHANGELOG.md
+[active-job]: https://github.com/rails/rails/blob/5-0-stable/activejob/CHANGELOG.md
diff --git a/guides/source/5_1_release_notes.md b/guides/source/5_1_release_notes.md
new file mode 100644
index 0000000000..a5a7eb4b2e
--- /dev/null
+++ b/guides/source/5_1_release_notes.md
@@ -0,0 +1,659 @@
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://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
+----------------------
+
+If you're upgrading an existing application, it's a great idea to have good test
+coverage before going in. You should also first upgrade to Rails 5.0 in case you
+haven't and make sure your application still runs as expected before attempting
+an update to Rails 5.1. A list of things to watch out for when upgrading is
+available in the
+[Upgrading Ruby on Rails](upgrading_ruby_on_rails.html#upgrading-from-rails-5-0-to-rails-5-1)
+guide.
+
+
+Major Features
+--------------
+
+### Yarn Support
+
+[Pull Request](https://github.com/rails/rails/pull/26836)
+
+Rails 5.1 allows 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 jQuery 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 no longer need to 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 now allows management of application secrets in a secure way,
+inspired by 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 parameters used for all methods in a mailer class in
+order 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
+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 adds 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.
+
+Using just a URL:
+
+``` erb
+<%= form_with url: posts_path do |form| %>
+ <%= form.text_field :title %>
+<% end %>
+
+<%# Will generate %>
+
+<form action="/posts" method="post" data-remote="true">
+ <input type="text" name="title">
+</form>
+```
+
+Adding a scope prefixes the input field names:
+
+``` erb
+<%= form_with scope: :post, url: posts_path do |form| %>
+ <%= form.text_field :title %>
+<% end %>
+
+<%# Will generate %>
+
+<form action="/posts" method="post" data-remote="true">
+ <input type="text" name="post[title]">
+</form>
+```
+
+Using a model infers both the URL and scope:
+
+``` erb
+<%= form_with model: Post.new do |form| %>
+ <%= form.text_field :title %>
+<% end %>
+
+<%# Will generate %>
+
+<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:
+
+``` erb
+<%= form_with model: Post.first do |form| %>
+ <%= form.text_field :title %>
+<% end %>
+
+<%# Will generate %>
+
+<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>
+```
+
+Incompatibilities
+-----------------
+
+The following changes may require immediate action upon upgrade.
+
+### Transactional tests with multiple connections
+
+Transactional tests now wrap all Active Record connections in database
+transactions.
+
+When a test spawns additional threads, and those threads obtain database
+connections, those connections are now handled specially:
+
+The threads will share a single connection, which is inside the managed
+transaction. This ensures all threads see the database in the same
+state, ignoring the outermost transaction. Previously, such additional
+connections were unable to see the fixture rows, for example.
+
+When a thread enters a nested transaction, it will temporarily obtain
+exclusive use of the connection, to maintain isolation.
+
+If your tests currently rely on obtaining a separate,
+outside-of-transaction, connection in a spawned thread, you'll need to
+switch to more explicit connection management.
+
+If your tests spawn threads and those threads interact while also using
+explicit database transactions, this change may introduce a deadlock.
+
+The easy way to opt out of this new behavior is to disable transactional
+tests on any test cases it affects.
+
+Railties
+--------
+
+Please refer to the [Changelog][railties] for detailed changes.
+
+### Removals
+
+* Remove deprecated `config.static_cache_control`.
+ ([commit](https://github.com/rails/rails/commit/c861decd44198f8d7d774ee6a74194d1ac1a5a13))
+
+* Remove deprecated `config.serve_static_files`.
+ ([commit](https://github.com/rails/rails/commit/0129ca2eeb6d5b2ea8c6e6be38eeb770fe45f1fa))
+
+* Remove deprecated file `rails/rack/debugger`.
+ ([commit](https://github.com/rails/rails/commit/7563bf7b46e6f04e160d664e284a33052f9804b8))
+
+* Remove deprecated tasks: `rails:update`, `rails:template`, `rails:template:copy`,
+ `rails:update:configs` and `rails:update:bin`.
+ ([commit](https://github.com/rails/rails/commit/f7782812f7e727178e4a743aa2874c078b722eef))
+
+* Remove deprecated `CONTROLLER` environment variable for `routes` task.
+ ([commit](https://github.com/rails/rails/commit/f9ed83321ac1d1902578a0aacdfe55d3db754219))
+
+* Remove -j (--javascript) option from `rails new` command.
+ ([Pull Request](https://github.com/rails/rails/pull/28546))
+
+### Notable changes
+
+* Added a shared section to `config/secrets.yml` that will be loaded for all
+ environments.
+ ([commit](https://github.com/rails/rails/commit/e530534265d2c32b5c5f772e81cb9002dcf5e9cf))
+
+* The config file `config/secrets.yml` is now loaded in with all keys as symbols.
+ ([Pull Request](https://github.com/rails/rails/pull/26929))
+
+* Removed jquery-rails from default stack. rails-ujs, which is shipped
+ with Action View, is included as default UJS adapter.
+ ([Pull Request](https://github.com/rails/rails/pull/27113))
+
+* Add Yarn support in new apps with a yarn binstub and package.json.
+ ([Pull Request](https://github.com/rails/rails/pull/26836))
+
+* Add Webpack support in new apps via the `--webpack` option, which will delegate
+ to the rails/webpacker gem.
+ ([Pull Request](https://github.com/rails/rails/pull/27288))
+
+* Initialize Git repo when generating new app, if option `--skip-git` is not
+ provided.
+ ([Pull Request](https://github.com/rails/rails/pull/27632))
+
+* Add encrypted secrets in `config/secrets.yml.enc`.
+ ([Pull Request](https://github.com/rails/rails/pull/28038))
+
+* Display railtie class name in `rails initializers`.
+ ([Pull Request](https://github.com/rails/rails/pull/25257))
+
+Action Cable
+-----------
+
+Please refer to the [Changelog][action-cable] for detailed changes.
+
+### Notable changes
+
+* Added support for `channel_prefix` to Redis and evented Redis adapters
+ in `cable.yml` to avoid name collisions when using the same Redis server
+ with multiple applications.
+ ([Pull Request](https://github.com/rails/rails/pull/27425))
+
+* Add `ActiveSupport::Notifications` hook for broadcasting data.
+ ([Pull Request](https://github.com/rails/rails/pull/24988))
+
+Action Pack
+-----------
+
+Please refer to the [Changelog][action-pack] for detailed changes.
+
+### Removals
+
+* Removed support for non-keyword arguments in `#process`, `#get`, `#post`,
+ `#patch`, `#put`, `#delete`, and `#head` for the `ActionDispatch::IntegrationTest`
+ and `ActionController::TestCase` classes.
+ ([Commit](https://github.com/rails/rails/commit/98b8309569a326910a723f521911e54994b112fb),
+ [Commit](https://github.com/rails/rails/commit/de9542acd56f60d281465a59eac11e15ca8b3323))
+
+* Removed deprecated `ActionDispatch::Callbacks.to_prepare` and
+ `ActionDispatch::Callbacks.to_cleanup`.
+ ([Commit](https://github.com/rails/rails/commit/3f2b7d60a52ffb2ad2d4fcf889c06b631db1946b))
+
+* Removed deprecated methods related to controller filters.
+ ([Commit](https://github.com/rails/rails/commit/d7be30e8babf5e37a891522869e7b0191b79b757))
+
+* Removed deprecated support to `:text` and `:nothing` in `render`.
+ ([Commit](https://github.com/rails/rails/commit/79a5ea9eadb4d43b62afacedc0706cbe88c54496),
+ [Commit](https://github.com/rails/rails/commit/57e1c99a280bdc1b324936a690350320a1cd8111))
+
+* Removed deprecated support for calling `HashWithIndifferentAccess` methods on `ActionController::Parameters`.
+ ([Commit](https://github.com/rails/rails/pull/26746/commits/7093ceb480ad6a0a91b511832dad4c6a86981b93))
+
+### Deprecations
+
+* Deprecated `config.action_controller.raise_on_unfiltered_parameters`.
+ It doesn't have any effect in Rails 5.1.
+ ([Commit](https://github.com/rails/rails/commit/c6640fb62b10db26004a998d2ece98baede509e5))
+
+### Notable changes
+
+* Added the `direct` and `resolve` methods to the routing DSL.
+ ([Pull Request](https://github.com/rails/rails/pull/23138))
+
+* Added a new `ActionDispatch::SystemTestCase` class to write system tests in
+ your applications.
+ ([Pull Request](https://github.com/rails/rails/pull/26703))
+
+Action View
+-------------
+
+Please refer to the [Changelog][action-view] for detailed changes.
+
+### Removals
+
+* Removed deprecated `#original_exception` in `ActionView::Template::Error`.
+ ([commit](https://github.com/rails/rails/commit/b9ba263e5aaa151808df058f5babfed016a1879f))
+
+* Remove the option `encode_special_chars` misnomer from `strip_tags`.
+ ([Pull Request](https://github.com/rails/rails/pull/28061))
+
+### Deprecations
+
+* Deprecated Erubis ERB handler in favor of Erubi.
+ ([Pull Request](https://github.com/rails/rails/pull/27757))
+
+### Notable changes
+
+* Raw template handler (the default template handler in Rails 5) now outputs
+ HTML-safe strings.
+ ([commit](https://github.com/rails/rails/commit/1de0df86695f8fa2eeae6b8b46f9b53decfa6ec8))
+
+* Change `datetime_field` and `datetime_field_tag` to generate `datetime-local`
+ fields.
+ ([Pull Request](https://github.com/rails/rails/pull/25469))
+
+* New Builder-style syntax for HTML tags (`tag.div`, `tag.br`, etc.)
+ ([Pull Request](https://github.com/rails/rails/pull/25543))
+
+* Add `form_with` to unify `form_tag` and `form_for` usage.
+ ([Pull Request](https://github.com/rails/rails/pull/26976))
+
+* Add `check_parameters` option to `current_page?`.
+ ([Pull Request](https://github.com/rails/rails/pull/27549))
+
+Action Mailer
+-------------
+
+Please refer to the [Changelog][action-mailer] for detailed changes.
+
+### Notable changes
+
+* Allowed setting custom content type when attachments are included
+ and body is set inline.
+ ([Pull Request](https://github.com/rails/rails/pull/27227))
+
+* Allowed passing lambdas as values to the `default` method.
+ ([Commit](https://github.com/rails/rails/commit/1cec84ad2ddd843484ed40b1eb7492063ce71baf))
+
+* Added support for parameterized invocation of mailers to share before filters and defaults
+ between different mailer actions.
+ ([Commit](https://github.com/rails/rails/commit/1cec84ad2ddd843484ed40b1eb7492063ce71baf))
+
+* Passed the incoming arguments to the mailer action to `process.action_mailer` event under
+ an `args` key.
+ ([Pull Request](https://github.com/rails/rails/pull/27900))
+
+Active Record
+-------------
+
+Please refer to the [Changelog][active-record] for detailed changes.
+
+### Removals
+
+* Removed support for passing arguments and block at the same time to
+ `ActiveRecord::QueryMethods#select`.
+ ([Commit](https://github.com/rails/rails/commit/4fc3366d9d99a0eb19e45ad2bf38534efbf8c8ce))
+
+* Removed deprecated `activerecord.errors.messages.restrict_dependent_destroy.one` and
+ `activerecord.errors.messages.restrict_dependent_destroy.many` i18n scopes.
+ ([Commit](https://github.com/rails/rails/commit/00e3973a311))
+
+* Removed deprecated force reload argument in singular and collection association readers.
+ ([Commit](https://github.com/rails/rails/commit/09cac8c67af))
+
+* Removed deprecated support for passing a column to `#quote`.
+ ([Commit](https://github.com/rails/rails/commit/e646bad5b7c))
+
+* Removed deprecated `name` arguments from `#tables`.
+ ([Commit](https://github.com/rails/rails/commit/d5be101dd02214468a27b6839ffe338cfe8ef5f3))
+
+* Removed deprecated behavior of `#tables` and `#table_exists?` to return tables and views
+ to return only tables and not views.
+ ([Commit](https://github.com/rails/rails/commit/5973a984c369a63720c2ac18b71012b8347479a8))
+
+* Removed deprecated `original_exception` argument in `ActiveRecord::StatementInvalid#initialize`
+ and `ActiveRecord::StatementInvalid#original_exception`.
+ ([Commit](https://github.com/rails/rails/commit/bc6c5df4699d3f6b4a61dd12328f9e0f1bd6cf46))
+
+* Removed deprecated support of passing a class as a value in a query.
+ ([Commit](https://github.com/rails/rails/commit/b4664864c972463c7437ad983832d2582186e886))
+
+* Removed deprecated support to query using commas on LIMIT.
+ ([Commit](https://github.com/rails/rails/commit/fc3e67964753fb5166ccbd2030d7382e1976f393))
+
+* Removed deprecated `conditions` parameter from `#destroy_all`.
+ ([Commit](https://github.com/rails/rails/commit/d31a6d1384cd740c8518d0bf695b550d2a3a4e9b))
+
+* Removed deprecated `conditions` parameter from `#delete_all`.
+ ([Commit](https://github.com/rails/rails/pull/27503/commits/e7381d289e4f8751dcec9553dcb4d32153bd922b))
+
+* Removed deprecated method `#load_schema_for` in favor of `#load_schema`.
+ ([Commit](https://github.com/rails/rails/commit/419e06b56c3b0229f0c72d3e4cdf59d34d8e5545))
+
+* Removed deprecated `#raise_in_transactional_callbacks` configuration.
+ ([Commit](https://github.com/rails/rails/commit/8029f779b8a1dd9848fee0b7967c2e0849bf6e07))
+
+* Removed deprecated `#use_transactional_fixtures` configuration.
+ ([Commit](https://github.com/rails/rails/commit/3955218dc163f61c932ee80af525e7cd440514b3))
+
+### Deprecations
+
+* Deprecated `error_on_ignored_order_or_limit` flag in favor of
+ `error_on_ignored_order`.
+ ([Commit](https://github.com/rails/rails/commit/451437c6f57e66cc7586ec966e530493927098c7))
+
+* Deprecated `sanitize_conditions` in favor of `sanitize_sql`.
+ ([Pull Request](https://github.com/rails/rails/pull/25999))
+
+* Deprecated `supports_migrations?` on connection adapters.
+ ([Pull Request](https://github.com/rails/rails/pull/28172))
+
+* Deprecated `Migrator.schema_migrations_table_name`, use `SchemaMigration.table_name` instead.
+ ([Pull Request](https://github.com/rails/rails/pull/28351))
+
+* Deprecated using `#quoted_id` in quoting and type casting.
+ ([Pull Request](https://github.com/rails/rails/pull/27962))
+
+* Deprecated passing `default` argument to `#index_name_exists?`.
+ ([Pull Request](https://github.com/rails/rails/pull/26930))
+
+### Notable changes
+
+* Change Default Primary Keys to BIGINT.
+ ([Pull Request](https://github.com/rails/rails/pull/26266))
+
+* Virtual/generated column support for MySQL 5.7.5+ and MariaDB 5.2.0+.
+ ([Commit](https://github.com/rails/rails/commit/65bf1c60053e727835e06392d27a2fb49665484c))
+
+* Added support for limits in batch processing.
+ ([Commit](https://github.com/rails/rails/commit/451437c6f57e66cc7586ec966e530493927098c7))
+
+* Transactional tests now wrap all Active Record connections in database
+ transactions.
+ ([Pull Request](https://github.com/rails/rails/pull/28726))
+
+* Skipped comments in the output of `mysqldump` command by default.
+ ([Pull Request](https://github.com/rails/rails/pull/23301))
+
+* Fixed `ActiveRecord::Relation#count` to use Ruby's `Enumerable#count` for counting
+ records when a block is passed as argument instead of silently ignoring the
+ passed block.
+ ([Pull Request](https://github.com/rails/rails/pull/24203))
+
+* Pass `"-v ON_ERROR_STOP=1"` flag with `psql` command to not suppress SQL errors.
+ ([Pull Request](https://github.com/rails/rails/pull/24773))
+
+* Add `ActiveRecord::Base.connection_pool.stat`.
+ ([Pull Request](https://github.com/rails/rails/pull/26988))
+
+* Inheriting directly from `ActiveRecord::Migration` raises an error.
+ Specify the Rails version for which the migration was written for.
+ ([Commit](https://github.com/rails/rails/commit/249f71a22ab21c03915da5606a063d321f04d4d3))
+
+* An error is raised when `through` association has ambiguous reflection name.
+ ([Commit](https://github.com/rails/rails/commit/0944182ad7ed70d99b078b22426cbf844edd3f61))
+
+Active Model
+------------
+
+Please refer to the [Changelog][active-model] for detailed changes.
+
+### Removals
+
+* Removed deprecated methods in `ActiveModel::Errors`.
+ ([commit](https://github.com/rails/rails/commit/9de6457ab0767ebab7f2c8bc583420fda072e2bd))
+
+* Removed deprecated `:tokenizer` option in the length validator.
+ ([commit](https://github.com/rails/rails/commit/6a78e0ecd6122a6b1be9a95e6c4e21e10e429513))
+
+* Remove deprecated behavior that halts callbacks when the return value is false.
+ ([commit](https://github.com/rails/rails/commit/3a25cdca3e0d29ee2040931d0cb6c275d612dffe))
+
+### Notable changes
+
+* The original string assigned to a model attribute is no longer incorrectly
+ frozen.
+ ([Pull Request](https://github.com/rails/rails/pull/28729))
+
+Active Job
+-----------
+
+Please refer to the [Changelog][active-job] for detailed changes.
+
+### Removals
+
+* Removed deprecated support to passing the adapter class to `.queue_adapter`.
+ ([commit](https://github.com/rails/rails/commit/d1fc0a5eb286600abf8505516897b96c2f1ef3f6))
+
+* Removed deprecated `#original_exception` in `ActiveJob::DeserializationError`.
+ ([commit](https://github.com/rails/rails/commit/d861a1fcf8401a173876489d8cee1ede1cecde3b))
+
+### Notable changes
+
+* Added declarative exception handling via `ActiveJob::Base.retry_on` and `ActiveJob::Base.discard_on`.
+ ([Pull Request](https://github.com/rails/rails/pull/25991))
+
+* Yield the job instance so you have access to things like `job.arguments` on
+ the custom logic after retries fail.
+ ([commit](https://github.com/rails/rails/commit/a1e4c197cb12fef66530a2edfaeda75566088d1f))
+
+Active Support
+--------------
+
+Please refer to the [Changelog][active-support] for detailed changes.
+
+### Removals
+
+* Removed the `ActiveSupport::Concurrency::Latch` class.
+ ([Commit](https://github.com/rails/rails/commit/0d7bd2031b4054fbdeab0a00dd58b1b08fb7fea6))
+
+* Removed `halt_callback_chains_on_return_false`.
+ ([Commit](https://github.com/rails/rails/commit/4e63ce53fc25c3bc15c5ebf54bab54fa847ee02a))
+
+* Removed deprecated behavior that halts callbacks when the return is false.
+ ([Commit](https://github.com/rails/rails/commit/3a25cdca3e0d29ee2040931d0cb6c275d612dffe))
+
+### Deprecations
+
+* The top level `HashWithIndifferentAccess` class has been softly deprecated
+ in favor of the `ActiveSupport::HashWithIndifferentAccess` one.
+ ([Pull Request](https://github.com/rails/rails/pull/28157))
+
+* Deprecated passing string to `:if` and `:unless` conditional options on `set_callback` and `skip_callback`.
+ ([Commit](https://github.com/rails/rails/commit/0952552))
+
+### Notable changes
+
+* Fixed duration parsing and traveling to make it consistent across DST changes.
+ ([Commit](https://github.com/rails/rails/commit/8931916f4a1c1d8e70c06063ba63928c5c7eab1e),
+ [Pull Request](https://github.com/rails/rails/pull/26597))
+
+* Updated Unicode to version 9.0.0.
+ ([Pull Request](https://github.com/rails/rails/pull/27822))
+
+* Add Duration#before and #after as aliases for #ago and #since.
+ ([Pull Request](https://github.com/rails/rails/pull/27721))
+
+* Added `Module#delegate_missing_to` to delegate method calls not
+ defined for the current object to a proxy object.
+ ([Pull Request](https://github.com/rails/rails/pull/23930))
+
+* Added `Date#all_day` which returns a range representing the whole day
+ of the current date & time.
+ ([Pull Request](https://github.com/rails/rails/pull/24930))
+
+* Introduced the `assert_changes` and `assert_no_changes` methods for tests.
+ ([Pull Request](https://github.com/rails/rails/pull/25393))
+
+* The `travel` and `travel_to` methods now raise on nested calls.
+ ([Pull Request](https://github.com/rails/rails/pull/24890))
+
+* Update `DateTime#change` to support usec and nsec.
+ ([Pull Request](https://github.com/rails/rails/pull/28242))
+
+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/5_2_release_notes.md b/guides/source/5_2_release_notes.md
new file mode 100644
index 0000000000..c5b914fffc
--- /dev/null
+++ b/guides/source/5_2_release_notes.md
@@ -0,0 +1,861 @@
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
+
+Ruby on Rails 5.2 Release Notes
+===============================
+
+Highlights in Rails 5.2:
+
+* Active Storage
+* Redis Cache Store
+* HTTP/2 Early Hints
+* Credentials
+* Content Security Policy
+
+These release notes cover only the major changes. To learn about various bug
+fixes and changes, please refer to the change logs or check out the [list of
+commits](https://github.com/rails/rails/commits/5-2-stable) in the main Rails
+repository on GitHub.
+
+--------------------------------------------------------------------------------
+
+Upgrading to Rails 5.2
+----------------------
+
+If you're upgrading an existing application, it's a great idea to have good test
+coverage before going in. You should also first upgrade to Rails 5.1 in case you
+haven't and make sure your application still runs as expected before attempting
+an update to Rails 5.2. A list of things to watch out for when upgrading is
+available in the
+[Upgrading Ruby on Rails](upgrading_ruby_on_rails.html#upgrading-from-rails-5-1-to-rails-5-2)
+guide.
+
+Major Features
+--------------
+
+### Active Storage
+
+[Pull Request](https://github.com/rails/rails/pull/30020)
+
+[Active Storage](https://github.com/rails/rails/tree/5-2-stable/activestorage)
+facilitates uploading files to a cloud storage service like
+Amazon S3, Google Cloud Storage, or Microsoft Azure Storage and attaching
+those files to Active Record objects. It comes with a local disk-based service
+for development and testing and supports mirroring files to subordinate
+services for backups and migrations.
+You can read more about Active Storage in the
+[Active Storage Overview](active_storage_overview.html) guide.
+
+### Redis Cache Store
+
+[Pull Request](https://github.com/rails/rails/pull/31134)
+
+Rails 5.2 ships with built-in Redis cache store.
+You can read more about this in the
+[Caching with Rails: An Overview](caching_with_rails.html#activesupport-cache-rediscachestore)
+guide.
+
+### HTTP/2 Early Hints
+
+[Pull Request](https://github.com/rails/rails/pull/30744)
+
+Rails 5.2 supports [HTTP/2 Early Hints](https://tools.ietf.org/html/rfc8297).
+To start the server with Early Hints enabled pass `--early-hints`
+to `bin/rails server`.
+
+### Credentials
+
+[Pull Request](https://github.com/rails/rails/pull/30067)
+
+Added `config/credentials.yml.enc` file to store production app secrets.
+It allows saving any authentication credentials for third-party services
+directly in repository encrypted with a key in the `config/master.key` file or
+the `RAILS_MASTER_KEY` environment variable.
+This will eventually replace `Rails.application.secrets` and the encrypted
+secrets introduced in Rails 5.1.
+Furthermore, Rails 5.2
+[opens API underlying Credentials](https://github.com/rails/rails/pull/30940),
+so you can easily deal with other encrypted configurations, keys, and files.
+You can read more about this in the
+[Securing Rails Applications](security.html#custom-credentials)
+guide.
+
+### Content Security Policy
+
+[Pull Request](https://github.com/rails/rails/pull/31162)
+
+Rails 5.2 ships with a new DSL that allows you to configure a
+[Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy)
+for your application. You can configure a global default policy and then
+override it on a per-resource basis and even use lambdas to inject per-request
+values into the header such as account subdomains in a multi-tenant application.
+You can read more about this in the
+[Securing Rails Applications](security.html#content-security-policy)
+guide.
+
+Railties
+--------
+
+Please refer to the [Changelog][railties] for detailed changes.
+
+### Deprecations
+
+* Deprecate `capify!` method in generators and templates.
+ ([Pull Request](https://github.com/rails/rails/pull/29493))
+
+* Passing the environment's name as a regular argument to the
+ `rails dbconsole` and `rails console` commands is deprecated.
+ The `-e` option should be used instead.
+ ([Commit](https://github.com/rails/rails/commit/48b249927375465a7102acc71c2dfb8d49af8309))
+
+* Deprecate using subclass of `Rails::Application` to start the Rails server.
+ ([Pull Request](https://github.com/rails/rails/pull/30127))
+
+* Deprecate `after_bundle` callback in Rails plugin templates.
+ ([Pull Request](https://github.com/rails/rails/pull/29446))
+
+### Notable changes
+
+* Added a shared section to `config/database.yml` that will be loaded for
+ all environments.
+ ([Pull Request](https://github.com/rails/rails/pull/28896))
+
+* Add `railtie.rb` to the plugin generator.
+ ([Pull Request](https://github.com/rails/rails/pull/29576))
+
+* Clear screenshot files in `tmp:clear` task.
+ ([Pull Request](https://github.com/rails/rails/pull/29534))
+
+* Skip unused components when running `bin/rails app:update`.
+ If the initial app generation skipped Action Cable, Active Record etc.,
+ the update task honors those skips too.
+ ([Pull Request](https://github.com/rails/rails/pull/29645))
+
+* Allow passing a custom connection name to the `rails dbconsole`
+ command when using a 3-level database configuration.
+ Example: `bin/rails dbconsole -c replica`.
+ ([Commit](https://github.com/rails/rails/commit/1acd9a6464668d4d54ab30d016829f60b70dbbeb))
+
+* Properly expand shortcuts for environment's name running the `console`
+ and `dbconsole` commands.
+ ([Commit](https://github.com/rails/rails/commit/3777701f1380f3814bd5313b225586dec64d4104))
+
+* Add `bootsnap` to default `Gemfile`.
+ ([Pull Request](https://github.com/rails/rails/pull/29313))
+
+* Support `-` as a platform-agnostic way to run a script from stdin with
+ `rails runner`
+ ([Pull Request](https://github.com/rails/rails/pull/26343))
+
+* Add `ruby x.x.x` version to `Gemfile` and create `.ruby-version`
+ root file containing the current Ruby version when new Rails applications
+ are created.
+ ([Pull Request](https://github.com/rails/rails/pull/30016))
+
+* Add `--skip-action-cable` option to the plugin generator.
+ ([Pull Request](https://github.com/rails/rails/pull/30164))
+
+* Add `git_source` to `Gemfile` for plugin generator.
+ ([Pull Request](https://github.com/rails/rails/pull/30110))
+
+* Skip unused components when running `bin/rails` in Rails plugin.
+ ([Commit](https://github.com/rails/rails/commit/62499cb6e088c3bc32a9396322c7473a17a28640))
+
+* Optimize indentation for generator actions.
+ ([Pull Request](https://github.com/rails/rails/pull/30166))
+
+* Optimize routes indentation.
+ ([Pull Request](https://github.com/rails/rails/pull/30241))
+
+* Add `--skip-yarn` option to the plugin generator.
+ ([Pull Request](https://github.com/rails/rails/pull/30238))
+
+* Support multiple versions arguments for `gem` method of Generators.
+ ([Pull Request](https://github.com/rails/rails/pull/30323))
+
+* Derive `secret_key_base` from the app name in development and test
+ environments.
+ ([Pull Request](https://github.com/rails/rails/pull/30067))
+
+* Add `mini_magick` to default `Gemfile` as comment.
+ ([Pull Request](https://github.com/rails/rails/pull/30633))
+
+* `rails new` and `rails plugin new` get `Active Storage` by default.
+ Add ability to skip `Active Storage` with `--skip-active-storage`
+ and do so automatically when `--skip-active-record` is used.
+ ([Pull Request](https://github.com/rails/rails/pull/30101))
+
+Action Cable
+------------
+
+Please refer to the [Changelog][action-cable] for detailed changes.
+
+### Removals
+
+* Removed deprecated evented redis adapter.
+ ([Commit](https://github.com/rails/rails/commit/48766e32d31651606b9f68a16015ad05c3b0de2c))
+
+### Notable changes
+
+* Add support for `host`, `port`, `db` and `password` options in cable.yml
+ ([Pull Request](https://github.com/rails/rails/pull/29528))
+
+* Hash long stream identifiers when using PostgreSQL adapter.
+ ([Pull Request](https://github.com/rails/rails/pull/29297))
+
+Action Pack
+-----------
+
+Please refer to the [Changelog][action-pack] for detailed changes.
+
+### Removals
+
+* Remove deprecated `ActionController::ParamsParser::ParseError`.
+ ([Commit](https://github.com/rails/rails/commit/e16c765ac6dcff068ff2e5554d69ff345c003de1))
+
+### Deprecations
+
+* Deprecate `#success?`, `#missing?` and `#error?` aliases of
+ `ActionDispatch::TestResponse`.
+ ([Pull Request](https://github.com/rails/rails/pull/30104))
+
+### Notable changes
+
+* Add support for recyclable cache keys with fragment caching.
+ ([Pull Request](https://github.com/rails/rails/pull/29092))
+
+* Change the cache key format for fragments to make it easier to debug key
+ churn.
+ ([Pull Request](https://github.com/rails/rails/pull/29092))
+
+* AEAD encrypted cookies and sessions with GCM.
+ ([Pull Request](https://github.com/rails/rails/pull/28132))
+
+* Protect from forgery by default.
+ ([Pull Request](https://github.com/rails/rails/pull/29742))
+
+* Enforce signed/encrypted cookie expiry server side.
+ ([Pull Request](https://github.com/rails/rails/pull/30121))
+
+* Cookies `:expires` option supports `ActiveSupport::Duration` object.
+ ([Pull Request](https://github.com/rails/rails/pull/30121))
+
+* Use Capybara registered `:puma` server config.
+ ([Pull Request](https://github.com/rails/rails/pull/30638))
+
+* Simplify cookies middleware with key rotation support.
+ ([Pull Request](https://github.com/rails/rails/pull/29716))
+
+* Add ability to enable Early Hints for HTTP/2.
+ ([Pull Request](https://github.com/rails/rails/pull/30744))
+
+* Add headless chrome support to System Tests.
+ ([Pull Request](https://github.com/rails/rails/pull/30876))
+
+* Add `:allow_other_host` option to `redirect_back` method.
+ ([Pull Request](https://github.com/rails/rails/pull/30850))
+
+* Make `assert_recognizes` to traverse mounted engines.
+ ([Pull Request](https://github.com/rails/rails/pull/22435))
+
+* Add DSL for configuring Content-Security-Policy header.
+ ([Pull Request](https://github.com/rails/rails/pull/31162),
+ [Commit](https://github.com/rails/rails/commit/619b1b6353a65e1635d10b8f8c6630723a5a6f1a),
+ [Commit](https://github.com/rails/rails/commit/4ec8bf68ff92f35e79232fbd605012ce1f4e1e6e))
+
+* Register most popular audio/video/font mime types supported by modern
+ browsers.
+ ([Pull Request](https://github.com/rails/rails/pull/31251))
+
+* Changed the default system test screenshot output from `inline` to `simple`.
+ ([Commit](https://github.com/rails/rails/commit/9d6e288ee96d6241f864dbf90211c37b14a57632))
+
+* Add headless firefox support to System Tests.
+ ([Pull Request](https://github.com/rails/rails/pull/31365))
+
+* Add secure `X-Download-Options` and `X-Permitted-Cross-Domain-Policies` to
+ default headers set.
+ ([Commit](https://github.com/rails/rails/commit/5d7b70f4336d42eabfc403e9f6efceb88b3eff44))
+
+* Changed the system tests to set Puma as default server only when the
+ user haven't specified manually another server.
+ ([Pull Request](https://github.com/rails/rails/pull/31384))
+
+* Add `Referrer-Policy` header to default headers set.
+ ([Commit](https://github.com/rails/rails/commit/428939be9f954d39b0c41bc53d85d0d106b9d1a1))
+
+* Matches behavior of `Hash#each` in `ActionController::Parameters#each`.
+ ([Pull Request](https://github.com/rails/rails/pull/27790))
+
+* Add support for automatic nonce generation for Rails UJS.
+ ([Commit](https://github.com/rails/rails/commit/b2f0a8945956cd92dec71ec4e44715d764990a49))
+
+* Update the default HSTS max-age value to 31536000 seconds (1 year)
+ to meet the minimum max-age requirement for https://hstspreload.org/.
+ ([Commit](https://github.com/rails/rails/commit/30b5f469a1d30c60d1fb0605e84c50568ff7ed37))
+
+* Add alias method `to_hash` to `to_h` for `cookies`.
+ Add alias method `to_h` to `to_hash` for `session`.
+ ([Commit](https://github.com/rails/rails/commit/50a62499e41dfffc2903d468e8b47acebaf9b500))
+
+Action View
+-----------
+
+Please refer to the [Changelog][action-view] for detailed changes.
+
+### Removals
+
+* Remove deprecated Erubis ERB handler.
+ ([Commit](https://github.com/rails/rails/commit/7de7f12fd140a60134defe7dc55b5a20b2372d06))
+
+### Deprecations
+
+* Deprecate `image_alt` helper which used to add default alt text to
+ the images generated by `image_tag`.
+ ([Pull Request](https://github.com/rails/rails/pull/30213))
+
+### Notable changes
+
+* Add `:json` type to `auto_discovery_link_tag` to support
+ [JSON Feeds](https://jsonfeed.org/version/1).
+ ([Pull Request](https://github.com/rails/rails/pull/29158))
+
+* Add `srcset` option to `image_tag` helper.
+ ([Pull Request](https://github.com/rails/rails/pull/29349))
+
+* Fix issues with `field_error_proc` wrapping `optgroup` and
+ select divider `option`.
+ ([Pull Request](https://github.com/rails/rails/pull/31088))
+
+* Change `form_with` to generates ids by default.
+ ([Commit](https://github.com/rails/rails/commit/260d6f112a0ffdbe03e6f5051504cb441c1e94cd))
+
+* Add `preload_link_tag` helper.
+ ([Pull Request](https://github.com/rails/rails/pull/31251))
+
+* Allow the use of callable objects as group methods for grouped selects.
+ ([Pull Request](https://github.com/rails/rails/pull/31578))
+
+Action Mailer
+-------------
+
+Please refer to the [Changelog][action-mailer] for detailed changes.
+
+### Notable changes
+
+* Allow Action Mailer classes to configure their delivery job.
+ ([Pull Request](https://github.com/rails/rails/pull/29457))
+
+* Add `assert_enqueued_email_with` test helper.
+ ([Pull Request](https://github.com/rails/rails/pull/30695))
+
+Active Record
+-------------
+
+Please refer to the [Changelog][active-record] for detailed changes.
+
+### Removals
+
+* Remove deprecated `#migration_keys`.
+ ([Pull Request](https://github.com/rails/rails/pull/30337))
+
+* Remove deprecated support to `quoted_id` when typecasting
+ an Active Record object.
+ ([Commit](https://github.com/rails/rails/commit/82472b3922bda2f337a79cef961b4760d04f9689))
+
+* Remove deprecated argument `default` from `index_name_exists?`.
+ ([Commit](https://github.com/rails/rails/commit/8f5b34df81175e30f68879479243fbce966122d7))
+
+* Remove deprecated support to passing a class to `:class_name`
+ on associations.
+ ([Commit](https://github.com/rails/rails/commit/e65aff70696be52b46ebe57207ebd8bb2cfcdbb6))
+
+* Remove deprecated methods `initialize_schema_migrations_table` and
+ `initialize_internal_metadata_table`.
+ ([Commit](https://github.com/rails/rails/commit/c9660b5777707658c414b430753029cd9bc39934))
+
+* Remove deprecated method `supports_migrations?`.
+ ([Commit](https://github.com/rails/rails/commit/9438c144b1893f2a59ec0924afe4d46bd8d5ffdd))
+
+* Remove deprecated method `supports_primary_key?`.
+ ([Commit](https://github.com/rails/rails/commit/c56ff22fc6e97df4656ddc22909d9bf8b0c2cbb1))
+
+* Remove deprecated method
+ `ActiveRecord::Migrator.schema_migrations_table_name`.
+ ([Commit](https://github.com/rails/rails/commit/7df6e3f3cbdea9a0460ddbab445c81fbb1cfd012))
+
+* Remove deprecated argument `name` from `#indexes`.
+ ([Commit](https://github.com/rails/rails/commit/d6b779ecebe57f6629352c34bfd6c442ac8fba0e))
+
+* Remove deprecated arguments from `#verify!`.
+ ([Commit](https://github.com/rails/rails/commit/9c6ee1bed0292fc32c23dc1c68951ae64fc510be))
+
+* Remove deprecated configuration `.error_on_ignored_order_or_limit`.
+ ([Commit](https://github.com/rails/rails/commit/e1066f450d1a99c9a0b4d786b202e2ca82a4c3b3))
+
+* Remove deprecated method `#scope_chain`.
+ ([Commit](https://github.com/rails/rails/commit/ef7784752c5c5efbe23f62d2bbcc62d4fd8aacab))
+
+* Remove deprecated method `#sanitize_conditions`.
+ ([Commit](https://github.com/rails/rails/commit/8f5413b896099f80ef46a97819fe47a820417bc2))
+
+### Deprecations
+
+* Deprecate `supports_statement_cache?`.
+ ([Pull Request](https://github.com/rails/rails/pull/28938))
+
+* Deprecate passing arguments and block at the same time to
+ `count` and `sum` in `ActiveRecord::Calculations`.
+ ([Pull Request](https://github.com/rails/rails/pull/29262))
+
+* Deprecate delegating to `arel` in `Relation`.
+ ([Pull Request](https://github.com/rails/rails/pull/29619))
+
+* Deprecate `set_state` method in `TransactionState`.
+ ([Commit](https://github.com/rails/rails/commit/608ebccf8f6314c945444b400a37c2d07f21b253))
+
+* Deprecate `expand_hash_conditions_for_aggregates` without replacement.
+ ([Commit](https://github.com/rails/rails/commit/7ae26885d96daee3809d0bd50b1a440c2f5ffb69))
+
+### Notable changes
+
+* When calling the dynamic fixture accessor method with no arguments, it now
+ returns all fixtures of this type. Previously this method always returned
+ an empty array.
+ ([Pull Request](https://github.com/rails/rails/pull/28692))
+
+* Fix inconsistency with changed attributes when overriding
+ Active Record attribute reader.
+ ([Pull Request](https://github.com/rails/rails/pull/28661))
+
+* Support Descending Indexes for MySQL.
+ ([Pull Request](https://github.com/rails/rails/pull/28773))
+
+* Fix `bin/rails db:forward` first migration.
+ ([Commit](https://github.com/rails/rails/commit/b77d2aa0c336492ba33cbfade4964ba0eda3ef84))
+
+* Raise error `UnknownMigrationVersionError` on the movement of migrations
+ when the current migration does not exist.
+ ([Commit](https://github.com/rails/rails/commit/bb9d6eb094f29bb94ef1f26aa44f145f17b973fe))
+
+* Respect `SchemaDumper.ignore_tables` in rake tasks for
+ databases structure dump.
+ ([Pull Request](https://github.com/rails/rails/pull/29077))
+
+* Add `ActiveRecord::Base#cache_version` to support recyclable cache keys via
+ the new versioned entries in `ActiveSupport::Cache`. This also means that
+ `ActiveRecord::Base#cache_key` will now return a stable key that
+ does not include a timestamp any more.
+ ([Pull Request](https://github.com/rails/rails/pull/29092))
+
+* Prevent creation of bind param if casted value is nil.
+ ([Pull Request](https://github.com/rails/rails/pull/29282))
+
+* Use bulk INSERT to insert fixtures for better performance.
+ ([Pull Request](https://github.com/rails/rails/pull/29504))
+
+* Merging two relations representing nested joins no longer transforms
+ the joins of the merged relation into LEFT OUTER JOIN.
+ ([Pull Request](https://github.com/rails/rails/pull/27063))
+
+* Fix transactions to apply state to child transactions.
+ Previously, if you had a nested transaction and the outer transaction was
+ rolledback, the record from the inner transaction would still be marked
+ as persisted. It was fixed by applying the state of the parent
+ transaction to the child transaction when the parent transaction is
+ rolledback. This will correctly mark records from the inner transaction
+ as not persisted.
+ ([Commit](https://github.com/rails/rails/commit/0237da287eb4c507d10a0c6d94150093acc52b03))
+
+* Fix eager loading/preloading association with scope including joins.
+ ([Pull Request](https://github.com/rails/rails/pull/29413))
+
+* Prevent errors raised by `sql.active_record` notification subscribers
+ from being converted into `ActiveRecord::StatementInvalid` exceptions.
+ ([Pull Request](https://github.com/rails/rails/pull/29692))
+
+* Skip query caching when working with batches of records
+ (`find_each`, `find_in_batches`, `in_batches`).
+ ([Commit](https://github.com/rails/rails/commit/b83852e6eed5789b23b13bac40228e87e8822b4d))
+
+* Change sqlite3 boolean serialization to use 1 and 0.
+ SQLite natively recognizes 1 and 0 as true and false, but does not natively
+ recognize 't' and 'f' as was previously serialized.
+ ([Pull Request](https://github.com/rails/rails/pull/29699))
+
+* Values constructed using multi-parameter assignment will now use the
+ post-type-cast value for rendering in single-field form inputs.
+ ([Commit](https://github.com/rails/rails/commit/1519e976b224871c7f7dd476351930d5d0d7faf6))
+
+* `ApplicationRecord` is no longer generated when generating models. If you
+ need to generate it, it can be created with `rails g application_record`.
+ ([Pull Request](https://github.com/rails/rails/pull/29916))
+
+* `Relation#or` now accepts two relations who have different values for
+ `references` only, as `references` can be implicitly called by `where`.
+ ([Commit](https://github.com/rails/rails/commit/ea6139101ccaf8be03b536b1293a9f36bc12f2f7))
+
+* When using `Relation#or`, extract the common conditions and
+ put them before the OR condition.
+ ([Pull Request](https://github.com/rails/rails/pull/29950))
+
+* Add `binary` fixture helper method.
+ ([Pull Request](https://github.com/rails/rails/pull/30073))
+
+* Automatically guess the inverse associations for STI.
+ ([Pull Request](https://github.com/rails/rails/pull/23425))
+
+* Add new error class `LockWaitTimeout` which will be raised
+ when lock wait timeout exceeded.
+ ([Pull Request](https://github.com/rails/rails/pull/30360))
+
+* Update payload names for `sql.active_record` instrumentation to be
+ more descriptive.
+ ([Pull Request](https://github.com/rails/rails/pull/30619))
+
+* Use given algorithm while removing index from database.
+ ([Pull Request](https://github.com/rails/rails/pull/24199))
+
+* Passing a `Set` to `Relation#where` now behaves the same as passing
+ an array.
+ ([Commit](https://github.com/rails/rails/commit/9cf7e3494f5bd34f1382c1ff4ea3d811a4972ae2))
+
+* PostgreSQL `tsrange` now preserves subsecond precision.
+ ([Pull Request](https://github.com/rails/rails/pull/30725))
+
+* Raises when calling `lock!` in a dirty record.
+ ([Commit](https://github.com/rails/rails/commit/63cf15877bae859ff7b4ebaf05186f3ca79c1863))
+
+* Fixed a bug where column orders for an index weren't written to
+ `db/schema.rb` when using the sqlite adapter.
+ ([Pull Request](https://github.com/rails/rails/pull/30970))
+
+* Fix `bin/rails db:migrate` with specified `VERSION`.
+ `bin/rails db:migrate` with empty VERSION behaves as without `VERSION`.
+ Check a format of `VERSION`: Allow a migration version number
+ or name of a migration file. Raise error if format of `VERSION` is invalid.
+ Raise error if target migration doesn't exist.
+ ([Pull Request](https://github.com/rails/rails/pull/30714))
+
+* Add new error class `StatementTimeout` which will be raised
+ when statement timeout exceeded.
+ ([Pull Request](https://github.com/rails/rails/pull/31129))
+
+* `update_all` will now pass its values to `Type#cast` before passing them to
+ `Type#serialize`. This means that `update_all(foo: 'true')` will properly
+ persist a boolean.
+ ([Commit](https://github.com/rails/rails/commit/68fe6b08ee72cc47263e0d2c9ff07f75c4b42761))
+
+* Require raw SQL fragments to be explicitly marked when used in
+ relation query methods.
+ ([Commit](https://github.com/rails/rails/commit/a1ee43d2170dd6adf5a9f390df2b1dde45018a48),
+ [Commit](https://github.com/rails/rails/commit/e4a921a75f8702a7dbaf41e31130fe884dea93f9))
+
+* Add `#up_only` to database migrations for code that is only relevant when
+ migrating up, e.g. populating a new column.
+ ([Pull Request](https://github.com/rails/rails/pull/31082))
+
+* Add new error class `QueryCanceled` which will be raised
+ when canceling statement due to user request.
+ ([Pull Request](https://github.com/rails/rails/pull/31235))
+
+* Don't allow scopes to be defined which conflict with instance methods
+ on `Relation`.
+ ([Pull Request](https://github.com/rails/rails/pull/31179))
+
+* Add support for PostgreSQL operator classes to `add_index`.
+ ([Pull Request](https://github.com/rails/rails/pull/19090))
+
+* Log database query callers.
+ ([Pull Request](https://github.com/rails/rails/pull/26815),
+ [Pull Request](https://github.com/rails/rails/pull/31519),
+ [Pull Request](https://github.com/rails/rails/pull/31690))
+
+* Undefine attribute methods on descendants when resetting column information.
+ ([Pull Request](https://github.com/rails/rails/pull/31475))
+
+* Using subselect for `delete_all` with `limit` or `offset`.
+ ([Commit](https://github.com/rails/rails/commit/9e7260da1bdc0770cf4ac547120c85ab93ff3d48))
+
+* Fixed inconsistency with `first(n)` when used with `limit()`.
+ The `first(n)` finder now respects the `limit()`, making it consistent
+ with `relation.to_a.first(n)`, and also with the behavior of `last(n)`.
+ ([Pull Request](https://github.com/rails/rails/pull/27597))
+
+* Fix nested `has_many :through` associations on unpersisted parent instances.
+ ([Commit](https://github.com/rails/rails/commit/027f865fc8b262d9ba3ee51da3483e94a5489b66))
+
+* Take into account association conditions when deleting through records.
+ ([Commit](https://github.com/rails/rails/commit/ae48c65e411e01c1045056562319666384bb1b63))
+
+* Don't allow destroyed object mutation after `save` or `save!` is called.
+ ([Commit](https://github.com/rails/rails/commit/562dd0494a90d9d47849f052e8913f0050f3e494))
+
+* Fix relation merger issue with `left_outer_joins`.
+ ([Pull Request](https://github.com/rails/rails/pull/27860))
+
+* Support for PostgreSQL foreign tables.
+ ([Pull Request](https://github.com/rails/rails/pull/31549))
+
+* Clear the transaction state when an Active Record object is duped.
+ ([Pull Request](https://github.com/rails/rails/pull/31751))
+
+* Fix not expanded problem when passing an Array object as argument
+ to the where method using `composed_of` column.
+ ([Pull Request](https://github.com/rails/rails/pull/31724))
+
+* Make `reflection.klass` raise if `polymorphic?` not to be misused.
+ ([Commit](https://github.com/rails/rails/commit/63fc1100ce054e3e11c04a547cdb9387cd79571a))
+
+* Fix `#columns_for_distinct` of MySQL and PostgreSQL to make
+ `ActiveRecord::FinderMethods#limited_ids_for` use correct primary key values
+ even if `ORDER BY` columns include other table's primary key.
+ ([Commit](https://github.com/rails/rails/commit/851618c15750979a75635530200665b543561a44))
+
+* Fix `dependent: :destroy` issue for has_one/belongs_to relationship where
+ the parent class was getting deleted when the child was not.
+ ([Commit](https://github.com/rails/rails/commit/b0fc04aa3af338d5a90608bf37248668d59fc881))
+
+Active Model
+------------
+
+Please refer to the [Changelog][active-model] for detailed changes.
+
+### Notable changes
+
+* Fix methods `#keys`, `#values` in `ActiveModel::Errors`.
+ Change `#keys` to only return the keys that don't have empty messages.
+ Change `#values` to only return the not empty values.
+ ([Pull Request](https://github.com/rails/rails/pull/28584))
+
+* Add method `#merge!` for `ActiveModel::Errors`.
+ ([Pull Request](https://github.com/rails/rails/pull/29714))
+
+* Allow passing a Proc or Symbol to length validator options.
+ ([Pull Request](https://github.com/rails/rails/pull/30674))
+
+* Execute `ConfirmationValidator` validation when `_confirmation`'s value
+ is `false`.
+ ([Pull Request](https://github.com/rails/rails/pull/31058))
+
+* Models using the attributes API with a proc default can now be marshalled.
+ ([Commit](https://github.com/rails/rails/commit/0af36c62a5710e023402e37b019ad9982e69de4b))
+
+* Do not lose all multiple `:includes` with options in serialization.
+ ([Commit](https://github.com/rails/rails/commit/853054bcc7a043eea78c97e7705a46abb603cc44))
+
+Active Support
+--------------
+
+Please refer to the [Changelog][active-support] for detailed changes.
+
+### Removals
+
+* Remove deprecated `:if` and `:unless` string filter for callbacks.
+ ([Commit](https://github.com/rails/rails/commit/c792354adcbf8c966f274915c605c6713b840548))
+
+* Remove deprecated `halt_callback_chains_on_return_false` option.
+ ([Commit](https://github.com/rails/rails/commit/19fbbebb1665e482d76cae30166b46e74ceafe29))
+
+### Deprecations
+
+* Deprecate `Module#reachable?` method.
+ ([Pull Request](https://github.com/rails/rails/pull/30624))
+
+* Deprecate `secrets.secret_token`.
+ ([Commit](https://github.com/rails/rails/commit/fbcc4bfe9a211e219da5d0bb01d894fcdaef0a0e))
+
+### Notable changes
+
+* Add `fetch_values` for `HashWithIndifferentAccess`.
+ ([Pull Request](https://github.com/rails/rails/pull/28316))
+
+* Add support for `:offset` to `Time#change`.
+ ([Commit](https://github.com/rails/rails/commit/851b7f866e13518d900407c78dcd6eb477afad06))
+
+* Add support for `:offset` and `:zone`
+ to `ActiveSupport::TimeWithZone#change`.
+ ([Commit](https://github.com/rails/rails/commit/851b7f866e13518d900407c78dcd6eb477afad06))
+
+* Pass gem name and deprecation horizon to deprecation notifications.
+ ([Pull Request](https://github.com/rails/rails/pull/28800))
+
+* Add support for versioned cache entries. This enables the cache stores to
+ recycle cache keys, greatly saving on storage in cases with frequent churn.
+ Works together with the separation of `#cache_key` and `#cache_version`
+ in Active Record and its use in Action Pack's fragment caching.
+ ([Pull Request](https://github.com/rails/rails/pull/29092))
+
+* Add `ActiveSupport::CurrentAttributes` to provide a thread-isolated
+ attributes singleton. Primary use case is keeping all the per-request
+ attributes easily available to the whole system.
+ ([Pull Request](https://github.com/rails/rails/pull/29180))
+
+* `#singularize` and `#pluralize` now respect uncountables for
+ the specified locale.
+ ([Commit](https://github.com/rails/rails/commit/352865d0f835c24daa9a2e9863dcc9dde9e5371a))
+
+* Add default option to `class_attribute`.
+ ([Pull Request](https://github.com/rails/rails/pull/29270))
+
+* Add `Date#prev_occurring` and `Date#next_occurring` to return
+ specified next/previous occurring day of week.
+ ([Pull Request](https://github.com/rails/rails/pull/26600))
+
+* Add default option to module and class attribute accessors.
+ ([Pull Request](https://github.com/rails/rails/pull/29294))
+
+* Cache: `write_multi`.
+ ([Pull Request](https://github.com/rails/rails/pull/29366))
+
+* Default `ActiveSupport::MessageEncryptor` to use AES 256 GCM encryption.
+ ([Pull Request](https://github.com/rails/rails/pull/29263))
+
+* Add `freeze_time` helper which freezes time to `Time.now` in tests.
+ ([Pull Request](https://github.com/rails/rails/pull/29681))
+
+* Make the order of `Hash#reverse_merge!` consistent
+ with `HashWithIndifferentAccess`.
+ ([Pull Request](https://github.com/rails/rails/pull/28077))
+
+* Add purpose and expiry support to `ActiveSupport::MessageVerifier` and
+ `ActiveSupport::MessageEncryptor`.
+ ([Pull Request](https://github.com/rails/rails/pull/29892))
+
+* Update `String#camelize` to provide feedback when wrong option is passed.
+ ([Pull Request](https://github.com/rails/rails/pull/30039))
+
+* `Module#delegate_missing_to` now raises `DelegationError` if target is nil,
+ similar to `Module#delegate`.
+ ([Pull Request](https://github.com/rails/rails/pull/30191))
+
+* Add `ActiveSupport::EncryptedFile` and
+ `ActiveSupport::EncryptedConfiguration`.
+ ([Pull Request](https://github.com/rails/rails/pull/30067))
+
+* Add `config/credentials.yml.enc` to store production app secrets.
+ ([Pull Request](https://github.com/rails/rails/pull/30067))
+
+* Add key rotation support to `MessageEncryptor` and `MessageVerifier`.
+ ([Pull Request](https://github.com/rails/rails/pull/29716))
+
+* Return an instance of `HashWithIndifferentAccess` from
+ `HashWithIndifferentAccess#transform_keys`.
+ ([Pull Request](https://github.com/rails/rails/pull/30728))
+
+* `Hash#slice` now falls back to Ruby 2.5+'s built-in definition if defined.
+ ([Commit](https://github.com/rails/rails/commit/01ae39660243bc5f0a986e20f9c9bff312b1b5f8))
+
+* `IO#to_json` now returns the `to_s` representation, rather than
+ attempting to convert to an array. This fixes a bug where `IO#to_json`
+ would raise an `IOError` when called on an unreadable object.
+ ([Pull Request](https://github.com/rails/rails/pull/30953))
+
+* Add same method signature for `Time#prev_day` and `Time#next_day`
+ in accordance with `Date#prev_day`, `Date#next_day`.
+ Allows pass argument for `Time#prev_day` and `Time#next_day`.
+ ([Commit](https://github.com/rails/rails/commit/61ac2167eff741bffb44aec231f4ea13d004134e))
+
+* Add same method signature for `Time#prev_month` and `Time#next_month`
+ in accordance with `Date#prev_month`, `Date#next_month`.
+ Allows pass argument for `Time#prev_month` and `Time#next_month`.
+ ([Commit](https://github.com/rails/rails/commit/f2c1e3a793570584d9708aaee387214bc3543530))
+
+* Add same method signature for `Time#prev_year` and `Time#next_year`
+ in accordance with `Date#prev_year`, `Date#next_year`.
+ Allows pass argument for `Time#prev_year` and `Time#next_year`.
+ ([Commit](https://github.com/rails/rails/commit/ee9d81837b5eba9d5ec869ae7601d7ffce763e3e))
+
+* Fix acronym support in `humanize`.
+ ([Commit](https://github.com/rails/rails/commit/0ddde0a8fca6a0ca3158e3329713959acd65605d))
+
+* Allow `Range#include?` on TWZ ranges.
+ ([Pull Request](https://github.com/rails/rails/pull/31081))
+
+* Cache: Enable compression by default for values > 1kB.
+ ([Pull Request](https://github.com/rails/rails/pull/31147))
+
+* Redis cache store.
+ ([Pull Request](https://github.com/rails/rails/pull/31134),
+ [Pull Request](https://github.com/rails/rails/pull/31866))
+
+* Handle `TZInfo::AmbiguousTime` errors.
+ ([Pull Request](https://github.com/rails/rails/pull/31128))
+
+* MemCacheStore: Support expiring counters.
+ ([Commit](https://github.com/rails/rails/commit/b22ee64b5b30c6d5039c292235e10b24b1057f6d))
+
+* Make `ActiveSupport::TimeZone.all` return only time zones that are in
+ `ActiveSupport::TimeZone::MAPPING`.
+ ([Pull Request](https://github.com/rails/rails/pull/31176))
+
+* Changed default behaviour of `ActiveSupport::SecurityUtils.secure_compare`,
+ to make it not leak length information even for variable length string.
+ Renamed old `ActiveSupport::SecurityUtils.secure_compare` to
+ `fixed_length_secure_compare`, and started raising `ArgumentError` in
+ case of length mismatch of passed strings.
+ ([Pull Request](https://github.com/rails/rails/pull/24510))
+
+* Use SHA-1 to generate non-sensitive digests, such as the ETag header.
+ ([Pull Request](https://github.com/rails/rails/pull/31289),
+ [Pull Request](https://github.com/rails/rails/pull/31651))
+
+* `assert_changes` will always assert that the expression changes,
+ regardless of `from:` and `to:` argument combinations.
+ ([Pull Request](https://github.com/rails/rails/pull/31011))
+
+* Add missing instrumentation for `read_multi`
+ in `ActiveSupport::Cache::Store`.
+ ([Pull Request](https://github.com/rails/rails/pull/30268))
+
+* Support hash as first argument in `assert_difference`.
+ This allows to specify multiple numeric differences in the same assertion.
+ ([Pull Request](https://github.com/rails/rails/pull/31600))
+
+* Caching: MemCache and Redis `read_multi` and `fetch_multi` speedup.
+ Read from the local in-memory cache before consulting the backend.
+ ([Commit](https://github.com/rails/rails/commit/a2b97e4ffef971607a1be8fc7909f099b6840f36))
+
+Active Job
+----------
+
+Please refer to the [Changelog][active-job] for detailed changes.
+
+### Notable changes
+
+* Allow block to be passed to `ActiveJob::Base.discard_on` to allow custom
+ handling of discard jobs.
+ ([Pull Request](https://github.com/rails/rails/pull/30622))
+
+Ruby on Rails Guides
+--------------------
+
+Please refer to the [Changelog][guides] for detailed changes.
+
+### Notable changes
+
+* Add
+ [Threading and Code Execution in Rails](threading_and_code_execution.html)
+ Guide.
+ ([Pull Request](https://github.com/rails/rails/pull/27494))
+
+* Add [Active Storage Overview](active_storage_overview.html) Guide.
+ ([Pull Request](https://github.com/rails/rails/pull/31037))
+
+Credits
+-------
+
+See the
+[full list of contributors to Rails](http://contributors.rubyonrails.org/)
+for the many people who spent many hours making Rails, the stable and robust
+framework it is. Kudos to all of them.
+
+[railties]: https://github.com/rails/rails/blob/5-2-stable/railties/CHANGELOG.md
+[action-pack]: https://github.com/rails/rails/blob/5-2-stable/actionpack/CHANGELOG.md
+[action-view]: https://github.com/rails/rails/blob/5-2-stable/actionview/CHANGELOG.md
+[action-mailer]: https://github.com/rails/rails/blob/5-2-stable/actionmailer/CHANGELOG.md
+[action-cable]: https://github.com/rails/rails/blob/5-2-stable/actioncable/CHANGELOG.md
+[active-record]: https://github.com/rails/rails/blob/5-2-stable/activerecord/CHANGELOG.md
+[active-model]: https://github.com/rails/rails/blob/5-2-stable/activemodel/CHANGELOG.md
+[active-support]: https://github.com/rails/rails/blob/5-2-stable/activesupport/CHANGELOG.md
+[active-job]: https://github.com/rails/rails/blob/5-2-stable/activejob/CHANGELOG.md
+[guides]: https://github.com/rails/rails/blob/5-2-stable/guides/CHANGELOG.md
diff --git a/guides/source/6_0_release_notes.md b/guides/source/6_0_release_notes.md
new file mode 100644
index 0000000000..f3ed21dc45
--- /dev/null
+++ b/guides/source/6_0_release_notes.md
@@ -0,0 +1,175 @@
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
+
+Ruby on Rails 6.0 Release Notes
+===============================
+
+Highlights in Rails 6.0:
+
+* Parallel Testing
+
+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/6-0-stable) in the main Rails
+repository on GitHub.
+
+--------------------------------------------------------------------------------
+
+Upgrading to Rails 6.0
+----------------------
+
+If you're upgrading an existing application, it's a great idea to have good test
+coverage before going in. You should also first upgrade to Rails 5.2 in case you
+haven't and make sure your application still runs as expected before attempting
+an update to Rails 6.0. A list of things to watch out for when upgrading is
+available in the
+[Upgrading Ruby on Rails](upgrading_ruby_on_rails.html#upgrading-from-rails-5-2-to-rails-6-0)
+guide.
+
+Major Features
+--------------
+
+### Parallel Testing
+
+[Pull Request](https://github.com/rails/rails/pull/31900)
+
+[Parallel Testing](testing.html#parallel-testing) allows you to parallelize your
+test suite. While forking processes is the default method, threading is
+supported as well. Running tests in parallel reduces the time it takes
+your entire test suite to run.
+
+Railties
+--------
+
+Please refer to the [Changelog][railties] for detailed changes.
+
+### Removals
+
+### Deprecations
+
+### Notable changes
+
+Action Cable
+------------
+
+Please refer to the [Changelog][action-cable] for detailed changes.
+
+### Removals
+
+### Deprecations
+
+### Notable changes
+
+Action Pack
+-----------
+
+Please refer to the [Changelog][action-pack] for detailed changes.
+
+### Removals
+
+### Deprecations
+
+### Notable changes
+
+Action View
+-----------
+
+Please refer to the [Changelog][action-view] for detailed changes.
+
+### Removals
+
+### Deprecations
+
+### Notable changes
+
+Action Mailer
+-------------
+
+Please refer to the [Changelog][action-mailer] for detailed changes.
+
+### Removals
+
+### Deprecations
+
+### Notable changes
+
+Active Record
+-------------
+
+Please refer to the [Changelog][active-record] for detailed changes.
+
+### Removals
+
+### Deprecations
+
+### Notable changes
+
+Active Storage
+--------------
+
+Please refer to the [Changelog][active-storage] for detailed changes.
+
+### Removals
+
+### Deprecations
+
+### Notable changes
+
+Active Model
+------------
+
+Please refer to the [Changelog][active-model] for detailed changes.
+
+### Removals
+
+### Deprecations
+
+### Notable changes
+
+Active Support
+--------------
+
+Please refer to the [Changelog][active-support] for detailed changes.
+
+### Removals
+
+### Deprecations
+
+### Notable changes
+
+Active Job
+----------
+
+Please refer to the [Changelog][active-job] for detailed changes.
+
+### Removals
+
+### Deprecations
+
+### Notable changes
+
+Ruby on Rails Guides
+--------------------
+
+Please refer to the [Changelog][guides] for detailed changes.
+
+### Notable 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/6-0-stable/railties/CHANGELOG.md
+[action-pack]: https://github.com/rails/rails/blob/6-0-stable/actionpack/CHANGELOG.md
+[action-view]: https://github.com/rails/rails/blob/6-0-stable/actionview/CHANGELOG.md
+[action-mailer]: https://github.com/rails/rails/blob/6-0-stable/actionmailer/CHANGELOG.md
+[action-cable]: https://github.com/rails/rails/blob/6-0-stable/actioncable/CHANGELOG.md
+[active-record]: https://github.com/rails/rails/blob/6-0-stable/activerecord/CHANGELOG.md
+[active-storage]: https://github.com/rails/rails/blob/6-0-stable/activestorage/CHANGELOG.md
+[active-model]: https://github.com/rails/rails/blob/6-0-stable/activemodel/CHANGELOG.md
+[active-support]: https://github.com/rails/rails/blob/6-0-stable/activesupport/CHANGELOG.md
+[active-job]: https://github.com/rails/rails/blob/6-0-stable/activejob/CHANGELOG.md
+[guides]: https://github.com/rails/rails/blob/6-0-stable/guides/CHANGELOG.md
diff --git a/guides/source/_welcome.html.erb b/guides/source/_welcome.html.erb
index 67f5f1cdd5..bf00ee08e5 100644
--- a/guides/source/_welcome.html.erb
+++ b/guides/source/_welcome.html.erb
@@ -1,24 +1,29 @@
-<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
- <a href="http://guides.rubyonrails.org">http://guides.rubyonrails.org</a> instead.
+ <a href="https://guides.rubyonrails.org">https://guides.rubyonrails.org</a> instead.
</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.2 based on <a href="https://github.com/rails/rails/tree/<%= @version %>"><%= @version %></a>.
These guides are designed to make you immediately productive with Rails, and to help you understand how all of the pieces fit together.
</p>
<% end %>
<p>
The guides for earlier releases:
-<a href="http://guides.rubyonrails.org/v4.2.0/">Rails 4.2.0</a>,
-<a href="http://guides.rubyonrails.org/v4.1.8/">Rails 4.1.8</a>,
-<a href="http://guides.rubyonrails.org/v4.0.12/">Rails 4.0.12</a>,
-<a href="http://guides.rubyonrails.org/v3.2.21/">Rails 3.2.21</a> and
-<a href="http://guides.rubyonrails.org/v2.3.11/">Rails 2.3.11</a>.
+<a href="https://guides.rubyonrails.org/v5.2/">Rails 5.2</a>,
+<a href="https://guides.rubyonrails.org/v5.1/">Rails 5.1</a>,
+<a href="https://guides.rubyonrails.org/v5.0/">Rails 5.0</a>,
+<a href="https://guides.rubyonrails.org/v4.2/">Rails 4.2</a>,
+<a href="https://guides.rubyonrails.org/v4.1/">Rails 4.1</a>,
+<a href="https://guides.rubyonrails.org/v4.0/">Rails 4.0</a>,
+<a href="https://guides.rubyonrails.org/v3.2/">Rails 3.2</a>,
+<a href="https://guides.rubyonrails.org/v3.1/">Rails 3.1</a>,
+<a href="https://guides.rubyonrails.org/v3.0/">Rails 3.0</a>, and
+<a href="https://guides.rubyonrails.org/v2.3/">Rails 2.3</a>.
</p>
diff --git a/guides/source/action_cable_overview.md b/guides/source/action_cable_overview.md
new file mode 100644
index 0000000000..2f602c3e0a
--- /dev/null
+++ b/guides/source/action_cable_overview.md
@@ -0,0 +1,692 @@
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
+
+Action Cable Overview
+=====================
+
+In this guide, you will learn how Action Cable works and how to use WebSockets to
+incorporate real-time features into your Rails application.
+
+After reading this guide, you will know:
+
+* What Action Cable is and its integration backend and frontend
+* How to setup Action Cable
+* How to setup channels
+* Deployment and Architecture setup for running Action Cable
+
+--------------------------------------------------------------------------------
+
+Introduction
+------------
+
+Action Cable seamlessly integrates
+[WebSockets](https://en.wikipedia.org/wiki/WebSocket) with the rest of your
+Rails application. It allows for real-time features to be written in Ruby in the
+same style and form as the rest of your Rails application, while still being
+performant and scalable. It's a full-stack offering that provides both a
+client-side JavaScript framework and a server-side Ruby framework. You have
+access to your full domain model written with Active Record or your ORM of
+choice.
+
+What is Pub/Sub
+---------------
+
+[Pub/Sub](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern), or
+Publish-Subscribe, refers to a message queue paradigm whereby senders of
+information (publishers), send data to an abstract class of recipients
+(subscribers), without specifying individual recipients. Action Cable uses this
+approach to communicate between the server and many clients.
+
+## Server-Side Components
+
+### Connections
+
+*Connections* form the foundation of the client-server relationship. For every
+WebSocket accepted by the server, a connection object is instantiated. This
+object becomes the parent of all the *channel subscriptions* that are created
+from there on. The connection itself does not deal with any specific application
+logic beyond authentication and authorization. The client of a WebSocket
+connection is called the connection *consumer*. An individual user will create
+one consumer-connection pair per browser tab, window, or device they have open.
+
+Connections are instances of `ApplicationCable::Connection`. In this class, you
+authorize the incoming connection, and proceed to establish it if the user can
+be identified.
+
+#### Connection Setup
+
+```ruby
+# app/channels/application_cable/connection.rb
+module ApplicationCable
+ class Connection < ActionCable::Connection::Base
+ identified_by :current_user
+
+ def connect
+ self.current_user = find_verified_user
+ end
+
+ private
+ def find_verified_user
+ if verified_user = User.find_by(id: cookies.encrypted[:user_id])
+ verified_user
+ else
+ reject_unauthorized_connection
+ end
+ end
+ end
+end
+```
+
+Here `identified_by` is a connection identifier that can be used to find the
+specific connection later. Note that anything marked as an identifier will automatically
+create a delegate by the same name on any channel instances created off the connection.
+
+This example relies on the fact that you will already have handled authentication of the user
+somewhere else in your application, and that a successful authentication sets a signed
+cookie with the user ID.
+
+The cookie is then automatically sent to the connection instance when a new connection
+is attempted, and you use that to set the `current_user`. By identifying the connection
+by this same current user, you're also ensuring that you can later retrieve all open
+connections by a given user (and potentially disconnect them all if the user is deleted
+or unauthorized).
+
+### Channels
+
+A *channel* encapsulates a logical unit of work, similar to what a controller does in a
+regular MVC setup. By default, Rails creates a parent `ApplicationCable::Channel` class
+for encapsulating shared logic between your channels.
+
+#### Parent Channel Setup
+
+```ruby
+# app/channels/application_cable/channel.rb
+module ApplicationCable
+ class Channel < ActionCable::Channel::Base
+ end
+end
+```
+
+Then you would create your own channel classes. For example, you could have a
+`ChatChannel` and an `AppearanceChannel`:
+
+```ruby
+# app/channels/chat_channel.rb
+class ChatChannel < ApplicationCable::Channel
+end
+
+# app/channels/appearance_channel.rb
+class AppearanceChannel < ApplicationCable::Channel
+end
+```
+
+A consumer could then be subscribed to either or both of these channels.
+
+#### Subscriptions
+
+Consumers subscribe to channels, acting as *subscribers*. Their connection is
+called a *subscription*. Produced messages are then routed to these channel
+subscriptions based on an identifier sent by the cable consumer.
+
+```ruby
+# app/channels/chat_channel.rb
+class ChatChannel < ApplicationCable::Channel
+ # Called when the consumer has successfully
+ # become a subscriber to this channel.
+ def subscribed
+ end
+end
+```
+
+## Client-Side Components
+
+### Connections
+
+Consumers require an instance of the connection on their side. This can be
+established using the following JavaScript, which is generated by default by Rails:
+
+#### Connect Consumer
+
+```js
+// app/assets/javascripts/cable.js
+//= require action_cable
+//= require_self
+//= require_tree ./channels
+
+(function() {
+ this.App || (this.App = {});
+
+ App.cable = ActionCable.createConsumer();
+}).call(this);
+```
+
+This will ready a consumer that'll connect against `/cable` on your server by default.
+The connection won't be established until you've also specified at least one subscription
+you're interested in having.
+
+#### Subscriber
+
+A consumer becomes a subscriber by creating a subscription to a given channel:
+
+```coffeescript
+# app/assets/javascripts/cable/subscriptions/chat.coffee
+App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" }
+
+# app/assets/javascripts/cable/subscriptions/appearance.coffee
+App.cable.subscriptions.create { channel: "AppearanceChannel" }
+```
+
+While this creates the subscription, the functionality needed to respond to
+received data will be described later on.
+
+A consumer can act as a subscriber to a given channel any number of times. For
+example, a consumer could subscribe to multiple chat rooms at the same time:
+
+```coffeescript
+App.cable.subscriptions.create { channel: "ChatChannel", room: "1st Room" }
+App.cable.subscriptions.create { channel: "ChatChannel", room: "2nd Room" }
+```
+
+## Client-Server Interactions
+
+### Streams
+
+*Streams* provide the mechanism by which channels route published content
+(broadcasts) to their subscribers.
+
+```ruby
+# app/channels/chat_channel.rb
+class ChatChannel < ApplicationCable::Channel
+ def subscribed
+ stream_from "chat_#{params[:room]}"
+ end
+end
+```
+
+If you have a stream that is related to a model, then the broadcasting used
+can be generated from the model and channel. The following example would
+subscribe to a broadcasting like `comments:Z2lkOi8vVGVzdEFwcC9Qb3N0LzE`
+
+```ruby
+class CommentsChannel < ApplicationCable::Channel
+ def subscribed
+ post = Post.find(params[:id])
+ stream_for post
+ end
+end
+```
+
+You can then broadcast to this channel like this:
+
+```ruby
+CommentsChannel.broadcast_to(@post, @comment)
+```
+
+### Broadcasting
+
+A *broadcasting* is a pub/sub link where anything transmitted by a publisher
+is routed directly to the channel subscribers who are streaming that named
+broadcasting. Each channel can be streaming zero or more broadcastings.
+
+Broadcastings are purely an online queue and time-dependent. If a consumer is
+not streaming (subscribed to a given channel), they'll not get the broadcast
+should they connect later.
+
+Broadcasts are called elsewhere in your Rails application:
+
+```ruby
+WebNotificationsChannel.broadcast_to(
+ current_user,
+ title: 'New things!',
+ body: 'All the news fit to print'
+)
+```
+
+The `WebNotificationsChannel.broadcast_to` call places a message in the current
+subscription adapter's pubsub queue under a separate broadcasting name for each user.
+The default pubsub queue for Action Cable is `redis` in production and `async` in development and
+test environments. For a user with an ID of 1, the broadcasting name would be `web_notifications:1`.
+
+The channel has been instructed to stream everything that arrives at
+`web_notifications:1` directly to the client by invoking the `received`
+callback.
+
+### Subscriptions
+
+When a consumer is subscribed to a channel, they act as a subscriber. This
+connection is called a subscription. Incoming messages are then routed to
+these channel subscriptions based on an identifier sent by the cable consumer.
+
+```coffeescript
+# app/assets/javascripts/cable/subscriptions/chat.coffee
+# Assumes you've already requested the right to send web notifications
+App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" },
+ received: (data) ->
+ @appendLine(data)
+
+ appendLine: (data) ->
+ html = @createLine(data)
+ $("[data-chat-room='Best Room']").append(html)
+
+ createLine: (data) ->
+ """
+ <article class="chat-line">
+ <span class="speaker">#{data["sent_by"]}</span>
+ <span class="body">#{data["body"]}</span>
+ </article>
+ """
+```
+
+### Passing Parameters to Channels
+
+You can pass parameters from the client side to the server side when creating a
+subscription. For example:
+
+```ruby
+# app/channels/chat_channel.rb
+class ChatChannel < ApplicationCable::Channel
+ def subscribed
+ stream_from "chat_#{params[:room]}"
+ end
+end
+```
+
+An object passed as the first argument to `subscriptions.create` becomes the
+params hash in the cable channel. The keyword `channel` is required:
+
+```coffeescript
+# app/assets/javascripts/cable/subscriptions/chat.coffee
+App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" },
+ received: (data) ->
+ @appendLine(data)
+
+ appendLine: (data) ->
+ html = @createLine(data)
+ $("[data-chat-room='Best Room']").append(html)
+
+ createLine: (data) ->
+ """
+ <article class="chat-line">
+ <span class="speaker">#{data["sent_by"]}</span>
+ <span class="body">#{data["body"]}</span>
+ </article>
+ """
+```
+
+```ruby
+# Somewhere in your app this is called, perhaps
+# from a NewCommentJob.
+ActionCable.server.broadcast(
+ "chat_#{room}",
+ sent_by: 'Paul',
+ body: 'This is a cool chat app.'
+)
+```
+
+### Rebroadcasting a Message
+
+A common use case is to *rebroadcast* a message sent by one client to any
+other connected clients.
+
+```ruby
+# app/channels/chat_channel.rb
+class ChatChannel < ApplicationCable::Channel
+ def subscribed
+ stream_from "chat_#{params[:room]}"
+ end
+
+ def receive(data)
+ ActionCable.server.broadcast("chat_#{params[:room]}", data)
+ end
+end
+```
+
+```coffeescript
+# app/assets/javascripts/cable/subscriptions/chat.coffee
+App.chatChannel = App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" },
+ received: (data) ->
+ # data => { sent_by: "Paul", body: "This is a cool chat app." }
+
+App.chatChannel.send({ sent_by: "Paul", body: "This is a cool chat app." })
+```
+
+The rebroadcast will be received by all connected clients, _including_ the
+client that sent the message. Note that params are the same as they were when
+you subscribed to the channel.
+
+## Full-Stack Examples
+
+The following setup steps are common to both examples:
+
+ 1. [Setup your connection](#connection-setup).
+ 2. [Setup your parent channel](#parent-channel-setup).
+ 3. [Connect your consumer](#connect-consumer).
+
+### Example 1: User Appearances
+
+Here's a simple example of a channel that tracks whether a user is online or not
+and what page they're on. (This is useful for creating presence features like showing
+a green dot next to a user name if they're online).
+
+Create the server-side appearance channel:
+
+```ruby
+# app/channels/appearance_channel.rb
+class AppearanceChannel < ApplicationCable::Channel
+ def subscribed
+ current_user.appear
+ end
+
+ def unsubscribed
+ current_user.disappear
+ end
+
+ def appear(data)
+ current_user.appear(on: data['appearing_on'])
+ end
+
+ def away
+ current_user.away
+ end
+end
+```
+
+When a subscription is initiated the `subscribed` callback gets fired and we
+take that opportunity to say "the current user has indeed appeared". That
+appear/disappear API could be backed by Redis, a database, or whatever else.
+
+Create the client-side appearance channel subscription:
+
+```coffeescript
+# app/assets/javascripts/cable/subscriptions/appearance.coffee
+App.cable.subscriptions.create "AppearanceChannel",
+ # Called when the subscription is ready for use on the server.
+ connected: ->
+ @install()
+ @appear()
+
+ # Called when the WebSocket connection is closed.
+ disconnected: ->
+ @uninstall()
+
+ # Called when the subscription is rejected by the server.
+ rejected: ->
+ @uninstall()
+
+ appear: ->
+ # Calls `AppearanceChannel#appear(data)` on the server.
+ @perform("appear", appearing_on: $("main").data("appearing-on"))
+
+ away: ->
+ # Calls `AppearanceChannel#away` on the server.
+ @perform("away")
+
+
+ buttonSelector = "[data-behavior~=appear_away]"
+
+ install: ->
+ $(document).on "turbolinks:load.appearance", =>
+ @appear()
+
+ $(document).on "click.appearance", buttonSelector, =>
+ @away()
+ false
+
+ $(buttonSelector).show()
+
+ uninstall: ->
+ $(document).off(".appearance")
+ $(buttonSelector).hide()
+```
+
+##### Client-Server Interaction
+
+1. **Client** connects to the **Server** via `App.cable =
+ActionCable.createConsumer("ws://cable.example.com")`. (`cable.js`). The
+**Server** identifies this connection by `current_user`.
+
+2. **Client** subscribes to the appearance channel via
+`App.cable.subscriptions.create(channel: "AppearanceChannel")`. (`appearance.coffee`)
+
+3. **Server** recognizes a new subscription has been initiated for the
+appearance channel and runs its `subscribed` callback, calling the `appear`
+method on `current_user`. (`appearance_channel.rb`)
+
+4. **Client** recognizes that a subscription has been established and calls
+`connected` (`appearance.coffee`) which in turn calls `@install` and `@appear`.
+`@appear` calls `AppearanceChannel#appear(data)` on the server, and supplies a
+data hash of `{ appearing_on: $("main").data("appearing-on") }`. This is
+possible because the server-side channel instance automatically exposes all
+public methods declared on the class (minus the callbacks), so that these can be
+reached as remote procedure calls via a subscription's `perform` method.
+
+5. **Server** receives the request for the `appear` action on the appearance
+channel for the connection identified by `current_user`
+(`appearance_channel.rb`). **Server** retrieves the data with the
+`:appearing_on` key from the data hash and sets it as the value for the `:on`
+key being passed to `current_user.appear`.
+
+### Example 2: Receiving New Web Notifications
+
+The appearance example was all about exposing server functionality to
+client-side invocation over the WebSocket connection. But the great thing
+about WebSockets is that it's a two-way street. So now let's show an example
+where the server invokes an action on the client.
+
+This is a web notification channel that allows you to trigger client-side
+web notifications when you broadcast to the right streams:
+
+Create the server-side web notifications channel:
+
+```ruby
+# app/channels/web_notifications_channel.rb
+class WebNotificationsChannel < ApplicationCable::Channel
+ def subscribed
+ stream_for current_user
+ end
+end
+```
+
+Create the client-side web notifications channel subscription:
+
+```coffeescript
+# app/assets/javascripts/cable/subscriptions/web_notifications.coffee
+# Client-side which assumes you've already requested
+# the right to send web notifications.
+App.cable.subscriptions.create "WebNotificationsChannel",
+ received: (data) ->
+ new Notification data["title"], body: data["body"]
+```
+
+Broadcast content to a web notification channel instance from elsewhere in your
+application:
+
+```ruby
+# Somewhere in your app this is called, perhaps from a NewCommentJob
+WebNotificationsChannel.broadcast_to(
+ current_user,
+ title: 'New things!',
+ body: 'All the news fit to print'
+)
+```
+
+The `WebNotificationsChannel.broadcast_to` call places a message in the current
+subscription adapter's pubsub queue under a separate broadcasting name for each
+user. For a user with an ID of 1, the broadcasting name would be
+`web_notifications:1`.
+
+The channel has been instructed to stream everything that arrives at
+`web_notifications:1` directly to the client by invoking the `received`
+callback. The data passed as argument is the hash sent as the second parameter
+to the server-side broadcast call, JSON encoded for the trip across the wire
+and unpacked for the data argument arriving as `received`.
+
+### More Complete Examples
+
+See the [rails/actioncable-examples](https://github.com/rails/actioncable-examples)
+repository for a full example of how to setup Action Cable in a Rails app and adding channels.
+
+## Configuration
+
+Action Cable has two required configurations: a subscription adapter and allowed request origins.
+
+### Subscription Adapter
+
+By default, Action Cable looks for a configuration file in `config/cable.yml`.
+The file must specify an adapter for each Rails environment. See the
+[Dependencies](#dependencies) section for additional information on adapters.
+
+```yaml
+development:
+ adapter: async
+
+test:
+ adapter: async
+
+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
+
+The Redis adapter requires users to provide a URL pointing to the Redis server.
+Additionally, a `channel_prefix` may be provided to avoid channel name collisions
+when using the same Redis server for multiple applications. See the [Redis PubSub documentation](https://redis.io/topics/pubsub#database-amp-scoping) for more details.
+
+##### PostgreSQL Adapter
+
+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
+
+Action Cable will only accept requests from specified origins, which are
+passed to the server config as an array. The origins can be instances of
+strings or regular expressions, against which a check for the match will be performed.
+
+```ruby
+config.action_cable.allowed_request_origins = ['http://rubyonrails.com', %r{http://ruby.*}]
+```
+
+To disable and allow requests from any origin:
+
+```ruby
+config.action_cable.disable_request_forgery_protection = true
+```
+
+By default, Action Cable allows all requests from localhost:3000 when running
+in the development environment.
+
+### Consumer Configuration
+
+To configure the URL, add a call to `action_cable_meta_tag` in your HTML layout
+HEAD. This uses a URL or path typically set via `config.action_cable.url` in the
+environment configuration files.
+
+### Other Configurations
+
+The other common option to configure is the log tags applied to the
+per-connection logger. Here's an example that uses
+the user account id if available, else "no-account" while tagging:
+
+```ruby
+config.action_cable.log_tags = [
+ -> request { request.env['user_account_id'] || "no-account" },
+ :action_cable,
+ -> request { request.uuid }
+]
+```
+
+For a full list of all configuration options, see the
+`ActionCable::Server::Configuration` class.
+
+Also, note that your server must provide at least the same number of database
+connections as you have workers. The default worker pool size is set to 4, so
+that means you have to make at least that available. You can change that in
+`config/database.yml` through the `pool` attribute.
+
+## Running Standalone Cable Servers
+
+### In App
+
+Action Cable can run alongside your Rails application. For example, to
+listen for WebSocket requests on `/websocket`, specify that path to
+`config.action_cable.mount_path`:
+
+```ruby
+# config/application.rb
+class Application < Rails::Application
+ config.action_cable.mount_path = '/websocket'
+end
+```
+
+You can use `App.cable = ActionCable.createConsumer()` to connect to the cable
+server if `action_cable_meta_tag` is invoked in the layout. A custom path is
+specified as first argument to `createConsumer` (e.g. `App.cable =
+ActionCable.createConsumer("/websocket")`).
+
+For every instance of your server you create and for every worker your server
+spawns, you will also have a new instance of Action Cable, but the use of Redis
+keeps messages synced across connections.
+
+### Standalone
+
+The cable servers can be separated from your normal application server. It's
+still a Rack application, but it is its own Rack application. The recommended
+basic setup is as follows:
+
+```ruby
+# cable/config.ru
+require_relative '../config/environment'
+Rails.application.eager_load!
+
+run ActionCable.server
+```
+
+Then you start the server using a binstub in `bin/cable` ala:
+
+```
+#!/bin/bash
+bundle exec puma -p 28080 cable/config.ru
+```
+
+The above will start a cable server on port 28080.
+
+### Notes
+
+The WebSocket server doesn't have access to the session, but it has
+access to the cookies. This can be used when you need to handle
+authentication. You can see one way of doing that with Devise in this [article](https://greg.molnar.io/blog/actioncable-devise-authentication/).
+
+## Dependencies
+
+Action Cable provides a subscription adapter interface to process its
+pubsub internals. By default, asynchronous, inline, PostgreSQL, and Redis
+adapters are included. The default adapter
+in new Rails applications is the asynchronous (`async`) adapter.
+
+The Ruby side of things is built on top of [websocket-driver](https://github.com/faye/websocket-driver-ruby),
+[nio4r](https://github.com/celluloid/nio4r), and [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby).
+
+## Deployment
+
+Action Cable is powered by a combination of WebSockets and threads. Both the
+framework plumbing and user-specified channel work are handled internally by
+utilizing Ruby's native thread support. This means you can use all your regular
+Rails models with no problem, as long as you haven't committed any thread-safety sins.
+
+The Action Cable server implements the Rack socket hijacking API,
+thereby allowing the use of a multithreaded pattern for managing connections
+internally, irrespective of whether the application server is multi-threaded or not.
+
+Accordingly, Action Cable works with popular servers like Unicorn, Puma, and
+Passenger.
diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md
index 09fbdc0d32..aa746e4731 100644
--- a/guides/source/action_controller_overview.md
+++ b/guides/source/action_controller_overview.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Action Controller Overview
==========================
@@ -21,9 +21,9 @@ After reading this guide, you will know:
What Does a Controller Do?
--------------------------
-Action Controller is the C in MVC. After routing has determined which controller to use for a request, the controller is responsible for making sense of the request and producing the appropriate output. Luckily, Action Controller does most of the groundwork for you and uses smart conventions to make this as straightforward as possible.
+Action Controller is the C in [MVC](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller). After the router has determined which controller to use for a request, the controller is responsible for making sense of the request, and producing the appropriate output. Luckily, Action Controller does most of the groundwork for you and uses smart conventions to make this as straightforward as possible.
-For most conventional [RESTful](http://en.wikipedia.org/wiki/Representational_state_transfer) applications, the controller will receive the request (this is invisible to you as the developer), fetch or save data from a model and use a view to create HTML output. If your controller needs to do things a little differently, that's not a problem, this is just the most common way for a controller to work.
+For most conventional [RESTful](https://en.wikipedia.org/wiki/Representational_state_transfer) applications, the controller will receive the request (this is invisible to you as the developer), fetch or save data from a model, and use a view to create HTML output. If your controller needs to do things a little differently, that's not a problem, this is just the most common way for a controller to work.
A controller can thus be thought of as a middleman between models and views. It makes the model data available to the view so it can display that data to the user, and it saves or updates user data to the model.
@@ -51,7 +51,7 @@ class ClientsController < ApplicationController
end
```
-As an example, if a user goes to `/clients/new` in your application to add a new client, Rails will create an instance of `ClientsController` and call its `new` method. Note that the empty method from the example above would work just fine because Rails will by default render the `new.html.erb` view unless the action says otherwise. The `new` method could make available to the view a `@client` instance variable by creating a new `Client`:
+As an example, if a user goes to `/clients/new` in your application to add a new client, Rails will create an instance of `ClientsController` and call its `new` method. Note that the empty method from the example above would work just fine because Rails will by default render the `new.html.erb` view unless the action says otherwise. By creating a new `Client`, the `new` method can make a `@client` instance variable accessible in the view:
```ruby
def new
@@ -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.
@@ -145,7 +145,7 @@ So for example, if you are sending this JSON content:
Your controller will receive `params[:company]` as `{ "name" => "acme", "address" => "123 Carrot Street" }`.
-Also, if you've turned on `config.wrap_parameters` in your initializer or called `wrap_parameters` in your controller, you can safely omit the root element in the JSON parameter. In this case, the parameters will be cloned and wrapped with a key chosen based on your controller's name. So the above JSON POST can be written as:
+Also, if you've turned on `config.wrap_parameters` in your initializer or called `wrap_parameters` in your controller, you can safely omit the root element in the JSON parameter. In this case, the parameters will be cloned and wrapped with a key chosen based on your controller's name. So the above JSON request can be written as:
```json
{ "name": "acme", "address": "123 Carrot Street" }
@@ -166,7 +166,7 @@ NOTE: Support for parsing XML parameters has been extracted into a gem named `ac
The `params` hash will always contain the `:controller` and `:action` keys, but you should use the methods `controller_name` and `action_name` instead to access these values. Any other parameters defined by the routing, such as `:id`, will also be available. As an example, consider a listing of clients where the list can show either active or inactive clients. We can add a route which captures the `:status` parameter in a "pretty" URL:
```ruby
-get '/clients/:status' => 'clients#index', foo: 'bar'
+get '/clients/:status', to: 'clients#index', foo: 'bar'
```
In this case, when a user opens the URL `/clients/active`, `params[:status]` will be set to "active". When this route is used, `params[:foo]` will also be set to "bar", as if it were passed in the query string. Your controller will also receive `params[:action]` as "index" and `params[:controller]` as "clients".
@@ -193,17 +193,18 @@ In a given request, the method is not actually called for every single generated
With strong parameters, Action Controller parameters are forbidden to
be used in Active Model mass assignments until they have been
-whitelisted. This means that you'll have to make a conscious decision about
-which attributes to allow for mass update. This is a better security
+permitted. This means that you'll have to make a conscious decision about
+which attributes to permit for mass update. This is a better security
practice to help prevent accidentally allowing users to update sensitive
model attributes.
In addition, parameters can be marked as required and will flow through a
-predefined raise/rescue flow to end up as a 400 Bad Request.
+predefined raise/rescue flow that will result in a 400 Bad Request being
+returned if not all required parameters are passed in.
```ruby
class PeopleController < ActionController::Base
- # This will raise an ActiveModel::ForbiddenAttributes exception
+ # This will raise an ActiveModel::ForbiddenAttributesError exception
# because it's using mass assignment without an explicit permit
# step.
def create
@@ -213,8 +214,8 @@ class PeopleController < ActionController::Base
# This will pass with flying colors as long as there's a person key
# in the parameters, otherwise it'll raise a
# ActionController::ParameterMissing exception, which will get
- # caught by ActionController::Base and turned into that 400 Bad
- # Request reply.
+ # caught by ActionController::Base and turned into a 400 Bad
+ # Request error.
def update
person = current_account.people.find(params[:id])
person.update!(person_params)
@@ -240,7 +241,7 @@ Given
params.permit(:id)
```
-the key `:id` will pass the whitelisting if it appears in `params` and
+the key `:id` will be permitted for inclusion if it appears in `params` and
it has a permitted scalar value associated. Otherwise, the key is going
to be filtered out, so arrays, hashes, or any other objects cannot be
injected.
@@ -257,16 +258,28 @@ scalar values, map the key to an empty array:
params.permit(id: [])
```
-To whitelist an entire hash of parameters, the `permit!` method can be
+Sometimes it is not possible or convenient to declare the valid keys of
+a hash parameter or its internal structure. Just map to an empty hash:
+
+```ruby
+params.permit(preferences: {})
+```
+
+but 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.
+
+To permit an entire hash of parameters, the `permit!` method can be
used:
```ruby
params.require(:log_entry).permit!
```
-This will mark the `:log_entry` parameters hash and any sub-hash of it as
-permitted. Extreme care should be taken when using `permit!`, as it
-will allow all current and future model attributes to be mass-assigned.
+This marks the `:log_entry` parameters hash and any sub-hash of it as
+permitted and does not check for permitted scalars, anything is accepted.
+Extreme care should be taken when using `permit!`, as it will allow all current
+and future model attributes to be mass-assigned.
#### Nested Parameters
@@ -278,7 +291,7 @@ params.permit(:name, { emails: [] },
{ family: [ :name ], hobbies: [] }])
```
-This declaration whitelists the `name`, `emails`, and `friends`
+This declaration permits the `name`, `emails`, and `friends`
attributes. It is expected that `emails` will be an array of permitted
scalar values, and that `friends` will be an array of resources with
specific attributes: they should have a `name` attribute (any
@@ -313,7 +326,7 @@ parameters when you use `accepts_nested_attributes_for` in combination
with a `has_many` association:
```ruby
-# To whitelist the following data:
+# To permit the following data:
# {"book" => {"title" => "Some Book",
# "chapters_attributes" => { "1" => {"title" => "First Chapter"},
# "2" => {"title" => "Second Chapter"}}}}
@@ -321,26 +334,24 @@ with a `has_many` association:
params.require(:book).permit(:title, chapters_attributes: [:title])
```
-#### Outside the Scope of Strong Parameters
-
-The strong parameter API was designed with the most common use cases
-in mind. It is not meant as a silver bullet to handle all of your
-whitelisting problems. However, you can easily mix the API with your
-own code to adapt to your situation.
-
Imagine a scenario where you have parameters representing a product
name and a hash of arbitrary data associated with that product, and
-you want to whitelist the product name attribute and also the whole
-data hash. The strong parameters API doesn't let you directly
-whitelist the whole of a nested hash with any keys, but you can use
-the keys of your nested hash to declare what to whitelist:
+you want to permit the product name attribute and also the whole
+data hash:
```ruby
def product_params
- params.require(:product).permit(:name, data: params[:product][:data].try(:keys))
+ params.require(:product).permit(:name, data: {})
end
```
+#### Outside the Scope of Strong Parameters
+
+The strong parameter API was designed with the most common use cases
+in mind. It is not meant as a silver bullet to handle all of your
+parameter filtering problems. However, you can easily mix the API with your
+own code to adapt to your situation.
+
Session
-------
@@ -361,7 +372,7 @@ If your user sessions don't store critical data or don't need to be around for l
Read more about session storage in the [Security Guide](security.html).
-If you need a different session storage mechanism, you can change it in the `config/initializers/session_store.rb` file:
+If you need a different session storage mechanism, you can change it in an initializer:
```ruby
# Use the database for sessions instead of the cookie-based default,
@@ -370,7 +381,7 @@ If you need a different session storage mechanism, you can change it in the `con
# Rails.application.config.session_store :active_record_store
```
-Rails sets up a session key (the name of the cookie) when signing the session data. These can also be changed in `config/initializers/session_store.rb`:
+Rails sets up a session key (the name of the cookie) when signing the session data. These can also be changed in an initializer:
```ruby
# Be sure to restart your server when you modify this file.
@@ -384,34 +395,18 @@ You can also pass a `:domain` key and specify the domain name for the cookie:
Rails.application.config.session_store :cookie_store, key: '_your_app_session', domain: ".example.com"
```
-Rails sets up (for the CookieStore) a secret key used for signing the session data. This can be changed in `config/secrets.yml`
+Rails sets up (for the CookieStore) a secret key used for signing the session data in `config/credentials.yml.enc`. This can be changed with `rails credentials:edit`.
```ruby
-# Be sure to restart your server when you modify this file.
+# aws:
+# access_key_id: 123
+# secret_access_key: 345
-# Your secret key is used for verifying the integrity of signed cookies.
-# If you change this key, all old signed cookies will become invalid!
-
-# Make sure the secret is at least 30 characters and all random,
-# no regular words or you'll be exposed to dictionary attacks.
-# You can use `rake secret` to generate a secure secret key.
-
-# Make sure the secrets in this file are kept private
-# if you're sharing your code publicly.
-
-development:
- secret_key_base: a75d...
-
-test:
- secret_key_base: 492f...
-
-# Do not keep production secrets in the repository,
-# instead read values from the environment.
-production:
- secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
+# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.
+secret_key_base: 492f...
```
-NOTE: Changing the secret when using the `CookieStore` will invalidate all existing sessions.
+NOTE: Changing the secret_key_base when using the `CookieStore` will invalidate all existing sessions.
### Accessing the Session
@@ -453,14 +448,16 @@ class LoginsController < ApplicationController
end
```
-To remove something from the session, assign that key to be `nil`:
+To remove something from the session, delete the key/value pair:
```ruby
class LoginsController < ApplicationController
# "Delete" a login, aka "log the user out"
def destroy
# Remove the user id from the session
- @_current_user = session[:current_user_id] = nil
+ session.delete(:current_user_id)
+ # Clear the memoized current user
+ @_current_user = nil
redirect_to root_url
end
end
@@ -479,7 +476,7 @@ Let's use the act of logging out as an example. The controller can send a messag
```ruby
class LoginsController < ApplicationController
def destroy
- session[:current_user_id] = nil
+ session.delete(:current_user_id)
flash[:notice] = "You have successfully logged out."
redirect_to root_url
end
@@ -657,8 +654,8 @@ class UsersController < ApplicationController
@users = User.all
respond_to do |format|
format.html # index.html.erb
- format.xml { render xml: @users}
- format.json { render json: @users}
+ format.xml { render xml: @users }
+ format.json { render json: @users }
end
end
end
@@ -700,13 +697,16 @@ class LoginsController < ApplicationController
end
```
-Now, the `LoginsController`'s `new` and `create` actions will work as before without requiring the user to be logged in. The `:only` option is used to only skip this filter for these actions, and there is also an `:except` option which works the other way. These options can be used when adding filters too, so you can add a filter which only runs for selected actions in the first place.
+Now, the `LoginsController`'s `new` and `create` actions will work as before without requiring the user to be logged in. The `:only` option is used to skip this filter only for these actions, and there is also an `:except` option which works the other way. These options can be used when adding filters too, so you can add a filter which only runs for selected actions in the first place.
+
+NOTE: Calling the same filter multiple times with different options will not work,
+since the last filter definition will overwrite the previous ones.
### After Filters and Around Filters
In addition to "before" filters, you can also run filters after an action has been executed, or both before and after.
-"after" filters are similar to "before" filters, but because the action has already been run they have access to the response data that's about to be sent to the client. Obviously, "after" filters cannot stop the action from running.
+"after" filters are similar to "before" filters, but because the action has already been run they have access to the response data that's about to be sent to the client. Obviously, "after" filters cannot stop the action from running. Please note that "after" filters are executed only after a successful action, but not when an exception is raised in the request cycle.
"around" filters are responsible for running their associated actions by yielding, similar to how Rack middlewares work.
@@ -775,18 +775,18 @@ Again, this is not an ideal example for this filter, because it's not run in the
Request Forgery Protection
--------------------------
-Cross-site request forgery is a type of attack in which a site tricks a user into making requests on another site, possibly adding, modifying or deleting data on that site without the user's knowledge or permission.
+Cross-site request forgery is a type of attack in which a site tricks a user into making requests on another site, possibly adding, modifying, or deleting data on that site without the user's knowledge or permission.
-The first step to avoid this is to make sure all "destructive" actions (create, update and destroy) can only be accessed with non-GET requests. If you're following RESTful conventions you're already doing this. However, a malicious site can still send a non-GET request to your site quite easily, and that's where the request forgery protection comes in. As the name says, it protects from forged requests.
+The first step to avoid this is to make sure all "destructive" actions (create, update, and destroy) can only be accessed with non-GET requests. If you're following RESTful conventions you're already doing this. However, a malicious site can still send a non-GET request to your site quite easily, and that's where the request forgery protection comes in. As the name says, it protects from forged requests.
The way this is done is to add a non-guessable token which is only known to your server to each request. This way, if a request comes in without the proper token, it will be denied access.
If you generate a form like this:
```erb
-<%= form_for @user do |f| %>
- <%= f.text_field :username %>
- <%= f.text_field :password %>
+<%= form_with model: @user, local: true do |form| %>
+ <%= form.text_field :username %>
+ <%= form.text_field :password %>
<% end %>
```
@@ -810,11 +810,11 @@ The [Security Guide](security.html) has more about this and a lot of other secur
The Request and Response Objects
--------------------------------
-In every controller there are two accessor methods pointing to the request and the response objects associated with the request cycle that is currently in execution. The `request` method contains an instance of `AbstractRequest` and the `response` method returns a response object representing what is going to be sent back to the client.
+In every controller there are two accessor methods pointing to the request and the response objects associated with the request cycle that is currently in execution. The `request` method contains an instance of `ActionDispatch::Request` and the `response` method returns a response object representing what is going to be sent back to the client.
### The `request` Object
-The request object contains a lot of useful information about the request coming in from the client. To get a full list of the available methods, refer to the [API documentation](http://api.rubyonrails.org/classes/ActionDispatch/Request.html). Among the properties that you can access on this object are:
+The request object contains a lot of useful information about the request coming in from the client. To get a full list of the available methods, refer to the [Rails API documentation](http://api.rubyonrails.org/classes/ActionDispatch/Request.html) and [Rack Documentation](http://www.rubydoc.info/github/rack/rack/Rack/Request). Among the properties that you can access on this object are:
| Property of `request` | Purpose |
| ----------------------------------------- | -------------------------------------------------------------------------------- |
@@ -836,7 +836,7 @@ Rails collects all of the parameters sent along with the request in the `params`
### The `response` Object
-The response object is not usually used directly, but is built up during the execution of the action and rendering of the data that is being sent back to the user, but sometimes - like in an after filter - it can be useful to access the response directly. Some of these accessor methods also have setters, allowing you to change their values.
+The response object is not usually used directly, but is built up during the execution of the action and rendering of the data that is being sent back to the user, but sometimes - like in an after filter - it can be useful to access the response directly. Some of these accessor methods also have setters, allowing you to change their values. To get a full list of the available methods, refer to the [Rails API documentation](http://api.rubyonrails.org/classes/ActionDispatch/Response.html) and [Rack Documentation](http://www.rubydoc.info/github/rack/rack/Rack/Response).
| Property of `response` | Purpose |
| ---------------------- | --------------------------------------------------------------------------------------------------- |
@@ -855,7 +855,7 @@ If you want to set custom headers for a response then `response.headers` is the
response.headers["Content-Type"] = "application/pdf"
```
-Note: in the above case it would make more sense to use the `content_type` setter directly.
+NOTE: In the above case it would make more sense to use the `content_type` setter directly.
HTTP Authentications
--------------------
@@ -995,10 +995,6 @@ you would like in a response object. The `ActionController::Live` module allows
you to create a persistent connection with a browser. Using this module, you will
be able to send arbitrary data to the browser at specific points in time.
-NOTE: The default Rails server (WEBrick) is a buffering web server and does not
-support streaming. In order to use this feature, you'll need to use a non buffering
-server like [Puma](http://puma.io), [Rainbows](http://rainbows.bogomips.org)
-or [Passenger](https://www.phusionpassenger.com).
#### Incorporating Live Streaming
@@ -1029,7 +1025,7 @@ There are a couple of things to notice in the above example. We need to make
sure to close the response stream. Forgetting to close the stream will leave
the socket open forever. We also have to set the content type to `text/event-stream`
before we write to the response stream. This is because headers cannot be written
-after the response has been committed (when `response.committed` returns a truthy
+after the response has been committed (when `response.committed?` returns a truthy
value), which occurs when you `write` or `commit` the response stream.
#### Example Usage
@@ -1092,6 +1088,8 @@ You can filter out sensitive request parameters from your log files by appending
config.filter_parameters << :password
```
+NOTE: Provided parameters will be filtered out by partial matching regular expression. Rails adds default `:password` in the appropriate initializer (`initializers/filter_parameter_logging.rb`) and cares about typical application parameters `password` and `password_confirmation`.
+
### Redirects Filtering
Sometimes it's desirable to filter out from log files some sensitive locations your application is redirecting to.
@@ -1114,11 +1112,11 @@ Rescue
Most likely your application is going to contain bugs or otherwise throw an exception that needs to be handled. For example, if the user follows a link to a resource that no longer exists in the database, Active Record will throw the `ActiveRecord::RecordNotFound` exception.
-Rails' default exception handling displays a "500 Server Error" message for all exceptions. If the request was made locally, a nice traceback and some added information gets displayed so you can figure out what went wrong and deal with it. If the request was remote Rails will just display a simple "500 Server Error" message to the user, or a "404 Not Found" if there was a routing error or a record could not be found. Sometimes you might want to customize how these errors are caught and how they're displayed to the user. There are several levels of exception handling available in a Rails application:
+Rails default exception handling displays a "500 Server Error" message for all exceptions. If the request was made locally, a nice traceback and some added information gets displayed so you can figure out what went wrong and deal with it. If the request was remote Rails will just display a simple "500 Server Error" message to the user, or a "404 Not Found" if there was a routing error or a record could not be found. Sometimes you might want to customize how these errors are caught and how they're displayed to the user. There are several levels of exception handling available in a Rails application:
### The Default 500 and 404 Templates
-By default a production application will render either a 404 or a 500 error message. These messages are contained in static HTML files in the `public` folder, in `404.html` and `500.html` respectively. You can customize these files to add some extra information and layout, but remember that they are static; i.e. you can't use RHTML or layouts in them, just plain HTML.
+By default a production application will render either a 404 or a 500 error message, in the development environment all unhandled exceptions are raised. These messages are contained in static HTML files in the public folder, in `404.html` and `500.html` respectively. You can customize these files to add some extra information and style, but remember that they are static HTML; i.e. you can't use ERB, SCSS, CoffeeScript, or layouts for them.
### `rescue_from`
@@ -1150,7 +1148,7 @@ class ApplicationController < ActionController::Base
def user_not_authorized
flash[:error] = "You don't have access to this section."
- redirect_to :back
+ redirect_back(fallback_location: root_path)
end
end
@@ -1172,29 +1170,17 @@ class ClientsController < ApplicationController
end
```
-WARNING: You shouldn't do `rescue_from Exception` or `rescue_from StandardError` unless you have a particular reason as it will cause serious side-effects (e.g. you won't be able to see exception details and tracebacks during development).
+WARNING: Using `rescue_from` with `Exception` or `StandardError` would cause serious side-effects as it prevents Rails from handling exceptions properly. As such, it is not recommended to do so unless there is a strong reason.
-NOTE: Certain exceptions are only rescuable from the `ApplicationController` class, as they are raised before the controller gets initialized and the action gets executed. See Pratik Naik's [article](http://m.onkey.org/2008/7/20/rescue-from-dispatching) on the subject for more information.
+NOTE: When running in the production environment, all
+`ActiveRecord::RecordNotFound` errors render the 404 error page. Unless you need
+a custom behavior you don't need to handle this.
+
+NOTE: Certain exceptions are only rescuable from the `ApplicationController` class, as they are raised before the controller gets initialized and the action gets executed.
Force HTTPS protocol
--------------------
-Sometime you might want to force a particular controller to only be accessible via an HTTPS protocol for security reasons. You can use the `force_ssl` method in your controller to enforce that:
-
-```ruby
-class DinnerController
- force_ssl
-end
-```
-
-Just like the filter, you could also pass `:only` and `:except` to enforce the secure connection only to specific actions:
-
-```ruby
-class DinnerController
- force_ssl only: :cheeseburger
- # or
- force_ssl except: :cheeseburger
-end
-```
-
-Please note that if you find yourself adding `force_ssl` to many controllers, you may want to force the whole application to use HTTPS instead. In that case, you can set the `config.force_ssl` in your environment file.
+If you'd like to ensure that communication to your controller is only possible
+via HTTPS, you should do so by enabling the `ActionDispatch::SSL` middleware via
+`config.force_ssl` in your environment configuration.
diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md
index c31b50fcfc..1acb993cad 100644
--- a/guides/source/action_mailer_basics.md
+++ b/guides/source/action_mailer_basics.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Action Mailer Basics
====================
@@ -20,9 +20,18 @@ Introduction
------------
Action Mailer allows you to send emails from your application using mailer classes
-and views. Mailers work very similarly to controllers. They inherit from
-`ActionMailer::Base` and live in `app/mailers`, and they have associated views
-that appear in `app/views`.
+and views.
+
+#### Mailers are similar to controllers
+
+They inherit from `ActionMailer::Base` and live in `app/mailers`. Mailers also work
+very similarly to controllers. Some examples of similarities are enumerated below.
+Mailers have:
+
+* Actions, and also, associated views that appear in `app/views`.
+* Instance variables that are accessible in views.
+* The ability to utilise layouts and partials.
+* The ability to access a params hash.
Sending Emails
--------------
@@ -35,7 +44,7 @@ views.
#### Create the Mailer
```bash
-$ bin/rails generate mailer UserMailer
+$ rails generate mailer UserMailer
create app/mailers/user_mailer.rb
create app/mailers/application_mailer.rb
invoke erb
@@ -60,11 +69,10 @@ end
```
As you can see, you can generate mailers just like you use other generators with
-Rails. Mailers are conceptually similar to controllers, and so we get a mailer,
-a directory for views, and a test.
+Rails.
If you didn't want to use a generator, you could create your own file inside of
-app/mailers, just make sure that it inherits from `ActionMailer::Base`:
+`app/mailers`, just make sure that it inherits from `ActionMailer::Base`:
```ruby
class MyMailer < ActionMailer::Base
@@ -73,10 +81,9 @@ end
#### Edit the Mailer
-Mailers are very similar to Rails controllers. They also have methods called
-"actions" and use views to structure the content. Where a controller generates
-content like HTML to send back to the client, a Mailer creates a message to be
-delivered via email.
+Mailers have methods called "actions" and they use views to structure their content.
+Where a controller generates content like HTML to send back to the client, a Mailer
+creates a message to be delivered via email.
`app/mailers/user_mailer.rb` contains an empty mailer:
@@ -92,8 +99,8 @@ registered email address:
class UserMailer < ApplicationMailer
default from: 'notifications@example.com'
- def welcome_email(user)
- @user = user
+ def welcome_email
+ @user = params[:user]
@url = 'http://example.com/login'
mail(to: @user.email, subject: 'Welcome to My Awesome Site')
end
@@ -110,9 +117,6 @@ messages in this class. This can be overridden on a per-email basis.
* `mail` - The actual email message, we are passing the `:to` and `:subject`
headers in.
-Just like controllers, any instance variables we define in the method become
-available for use in the views.
-
#### Create a Mailer View
Create a file called `welcome_email.html.erb` in `app/views/user_mailer/`. This
@@ -160,8 +164,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.
@@ -169,14 +173,14 @@ Setting this up is painfully simple.
First, let's create a simple `User` scaffold:
```bash
-$ bin/rails generate scaffold user name email login
-$ bin/rake db:migrate
+$ rails generate scaffold user name email login
+$ rails db:migrate
```
Now that we have a user model to play with, we will just edit the
`app/controllers/users_controller.rb` make it instruct the `UserMailer` to deliver
an email to the newly created user by editing the create action and inserting a
-call to `UserMailer.welcome_email` right after the user is successfully saved.
+call to `UserMailer.with(user: @user).welcome_email` right after the user is successfully saved.
Action Mailer is nicely integrated with Active Job so you can send emails outside
of the request-response cycle, so the user doesn't have to wait on it:
@@ -191,7 +195,7 @@ class UsersController < ApplicationController
respond_to do |format|
if @user.save
# Tell the UserMailer to send a welcome email after save
- UserMailer.welcome_email(@user).deliver_later
+ UserMailer.with(user: @user).welcome_email.deliver_later
format.html { redirect_to(@user, notice: 'User was successfully created.') }
format.json { render json: @user, status: :created, location: @user }
@@ -204,10 +208,16 @@ class UsersController < ApplicationController
end
```
-NOTE: Active Job's default behavior is to execute jobs ':inline'. So, you can use
-`deliver_later` now to send emails, and when you later decide to start sending
-them from a background job, you'll only need to set up Active Job to use a queueing
-backend (Sidekiq, Resque, etc).
+NOTE: Active Job's default behavior is to execute jobs via the `:async` adapter. So, you can use
+`deliver_later` now to send emails asynchronously.
+Active Job's default adapter runs jobs with an in-process thread pool.
+It's well-suited for the development/test environments, since it doesn't require
+any external infrastructure, but it's a poor fit for production since it drops
+pending jobs on restart.
+If you need a persistent backend, you will need to use an Active Job adapter
+that has a persistent backend (Sidekiq, Resque, etc).
+
+NOTE: When calling `deliver_later` the job will be placed under `mailers` queue. Make sure Active Job adapter support it otherwise the job may be silently ignored preventing email delivery. You can change that by specifying `config.action_mailer.deliver_later_queue_name` option.
If you want to send emails right away (from a cronjob for example) just call
`deliver_now`:
@@ -216,16 +226,21 @@ If you want to send emails right away (from a cronjob for example) just call
class SendWeeklySummary
def run
User.find_each do |user|
- UserMailer.weekly_summary(user).deliver_now
+ UserMailer.with(user: user).weekly_summary.deliver_now
end
end
end
```
-The method `welcome_email` returns a `ActionMailer::MessageDelivery` object which
+Any key value pair passed to `with` just becomes the `params` for the mailer
+action. So `with(user: @user, account: @user.account)` makes `params[:user]` and
+`params[:account]` available in the mailer action. Just like controllers have
+params.
+
+The method `welcome_email` returns an `ActionMailer::MessageDelivery` object which
can then just be told `deliver_now` or `deliver_later` to send itself out. The
`ActionMailer::MessageDelivery` object is just a wrapper around a `Mail::Message`. If
-you want to inspect, alter or do anything else with the `Mail::Message` object you can
+you want to inspect, alter, or do anything else with the `Mail::Message` object you can
access it with the `message` method on the `ActionMailer::MessageDelivery` object.
### Auto encoding header values
@@ -257,7 +272,7 @@ Action Mailer makes it very easy to add attachments.
* Pass the file name and content and Action Mailer and the
[Mail gem](https://github.com/mikel/mail) will automatically guess the
- mime_type, set the encoding and create the attachment.
+ mime_type, set the encoding, and create the attachment.
```ruby
attachments['filename.jpg'] = File.read('/path/to/filename.jpg')
@@ -278,7 +293,7 @@ different, encode your content and pass in the encoded content and encoding in a
```ruby
encoded_content = SpecialEncode(File.read('/path/to/filename.jpg'))
attachments['filename.jpg'] = {
- mime_type: 'application/x-gzip',
+ mime_type: 'application/gzip',
encoding: 'SpecialEncoding',
content: encoded_content
}
@@ -326,8 +341,8 @@ key. The list of emails can be an array of email addresses or a single string
with the addresses separated by commas.
```ruby
-class AdminMailer < ActionMailer::Base
- default to: Proc.new { Admin.pluck(:email) },
+class AdminMailer < ApplicationMailer
+ default to: -> { Admin.pluck(:email) },
from: 'notification@example.com'
def new_registration(user)
@@ -347,8 +362,8 @@ address when they receive the email. The trick to doing that is to format the
email address in the format `"Full Name" <email>`.
```ruby
-def welcome_email(user)
- @user = user
+def welcome_email
+ @user = params[:user]
email_with_name = %("#{@user.name}" <#{@user.email}>)
mail(to: email_with_name, subject: 'Welcome to My Awesome Site')
end
@@ -368,8 +383,8 @@ To change the default mailer view for your action you do something like:
class UserMailer < ApplicationMailer
default from: 'notifications@example.com'
- def welcome_email(user)
- @user = user
+ def welcome_email
+ @user = params[:user]
@url = 'http://example.com/login'
mail(to: @user.email,
subject: 'Welcome to My Awesome Site',
@@ -390,13 +405,13 @@ templates or even render inline or text without using a template file:
class UserMailer < ApplicationMailer
default from: 'notifications@example.com'
- def welcome_email(user)
- @user = user
+ def welcome_email
+ @user = params[:user]
@url = 'http://example.com/login'
mail(to: @user.email,
subject: 'Welcome to My Awesome Site') do |format|
format.html { render 'another_template' }
- format.text { render text: 'Render text' }
+ format.text { render plain: 'Render text' }
end
end
end
@@ -407,6 +422,40 @@ use the rendered text for the text part. The render command is the same one used
inside of Action Controller, so you can use all the same options, such as
`:text`, `:inline` etc.
+If you would like to render a template located outside of the default `app/views/mailer_name/` directory, you can apply the `prepend_view_path`, like so:
+
+```ruby
+class UserMailer < ApplicationMailer
+ prepend_view_path "custom/path/to/mailer/view"
+
+ # This will try to load "custom/path/to/mailer/view/welcome_email" template
+ def welcome_email
+ # ...
+ end
+end
+```
+
+You can also consider using the [append_view_path](https://guides.rubyonrails.org/action_view_overview.html#view-paths) method.
+
+#### Caching mailer view
+
+You can perform fragment caching in mailer views like in application views using the `cache` method.
+
+```
+<% cache do %>
+ <%= @company.name %>
+<% end %>
+```
+
+And in order to use this feature, you need to configure your application with this:
+
+```
+ config.action_mailer.perform_caching = true
+```
+
+Fragment caching is also supported in multipart emails.
+Read more about caching in the [Rails caching guide](caching_with_rails.html).
+
### Action Mailer Layouts
Just like controller views, you can also have mailer layouts. The layout name
@@ -430,8 +479,8 @@ the format block to specify different layouts for different formats:
```ruby
class UserMailer < ApplicationMailer
- def welcome_email(user)
- mail(to: user.email) do |format|
+ def welcome_email
+ mail(to: params[:user].email) do |format|
format.html { render layout: 'my_layout' }
format.text
end
@@ -454,7 +503,7 @@ special URL that renders them. In the above example, the preview class for
```ruby
class UserMailerPreview < ActionMailer::Preview
def welcome_email
- UserMailer.welcome_email(User.first)
+ UserMailer.with(user: User.first).welcome_email
end
end
```
@@ -505,7 +554,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`.
@@ -530,8 +579,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/blob/master/actionview/app/assets/javascripts) 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
@@ -539,7 +589,7 @@ Unlike controllers, the mailer instance doesn't have any context about the
incoming request so you'll need to provide the `:asset_host` parameter yourself.
As the `:asset_host` usually is consistent across the application you can
-configure it globally in config/application.rb:
+configure it globally in `config/application.rb`:
```ruby
config.action_mailer.asset_host = 'http://example.com'
@@ -554,7 +604,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.
@@ -570,12 +620,12 @@ mailer action.
```ruby
class UserMailer < ApplicationMailer
- def welcome_email(user, company)
- @user = user
+ def welcome_email
+ @user = params[:user]
@url = user_url(@user)
- delivery_options = { user_name: company.smtp_user,
- password: company.smtp_password,
- address: company.smtp_host }
+ delivery_options = { user_name: params[:company].smtp_user,
+ password: params[:company].smtp_password,
+ address: params[:company].smtp_host }
mail(to: @user.email,
subject: "Please see the Terms and Conditions attached",
delivery_method_options: delivery_options)
@@ -592,9 +642,9 @@ will default to `text/plain` otherwise.
```ruby
class UserMailer < ApplicationMailer
- def welcome_email(user, email_body)
- mail(to: user.email,
- body: email_body,
+ def welcome_email
+ mail(to: params[:user].email,
+ body: params[:email_body],
content_type: "text/html",
subject: "Already rendered!")
end
@@ -653,24 +703,43 @@ Action Mailer allows for you to specify a `before_action`, `after_action` and
* You could use a `before_action` to populate the mail object with defaults,
delivery_method_options or insert default headers and attachments.
+```ruby
+class InvitationsMailer < ApplicationMailer
+ before_action { @inviter, @invitee = params[:inviter], params[:invitee] }
+ before_action { @account = params[:inviter].account }
+
+ default to: -> { @invitee.email_address },
+ from: -> { common_address(@inviter) },
+ reply_to: -> { @inviter.email_address_with_name }
+
+ def account_invitation
+ mail subject: "#{@inviter.name} invited you to their Basecamp (#{@account.name})"
+ end
+
+ def project_invitation
+ @project = params[:project]
+ @summarizer = ProjectInvitationSummarizer.new(@project.bucket)
+
+ mail subject: "#{@inviter.name.familiar} added you to a project in Basecamp (#{@account.name})"
+ end
+end
+```
+
* You could use an `after_action` to do similar setup as a `before_action` but
using instance variables set in your mailer action.
```ruby
class UserMailer < ApplicationMailer
+ before_action { @business, @user = params[:business], params[:user] }
+
after_action :set_delivery_options,
:prevent_delivery_to_guests,
:set_business_headers
- def feedback_message(business, user)
- @business = business
- @user = user
- mail
+ def feedback_message
end
- def campaign_message(business, user)
- @business = business
- @user = user
+ def campaign_message
end
private
@@ -714,11 +783,11 @@ files (environment.rb, production.rb, etc...)
| Configuration | Description |
|---------------|-------------|
|`logger`|Generates information on the mailing run if available. Can be set to `nil` for no logging. Compatible with both Ruby's own `Logger` and `Log4r` loggers.|
-|`smtp_settings`|Allows detailed configuration for `:smtp` delivery method:<ul><li>`:address` - Allows you to use a remote mail server. Just change it from its default `"localhost"` setting.</li><li>`:port` - On the off chance that your mail server doesn't run on port 25, you can change it.</li><li>`:domain` - If you need to specify a HELO domain, you can do it here.</li><li>`:user_name` - If your mail server requires authentication, set the username in this setting.</li><li>`:password` - If your mail server requires authentication, set the password in this setting.</li><li>`:authentication` - If your mail server requires authentication, you need to specify the authentication type here. This is a symbol and one of `:plain` (will send the password in the clear), `:login` (will send password Base64 encoded) or `:cram_md5` (combines a Challenge/Response mechanism to exchange information and a cryptographic Message Digest 5 algorithm to hash important information)</li><li>`:enable_starttls_auto` - Detects if STARTTLS is enabled in your SMTP server and starts to use it. Defaults to `true`.</li><li>`:openssl_verify_mode` - When using TLS, you can set how OpenSSL checks the certificate. This is really useful if you need to validate a self-signed and/or a wildcard certificate. You can use the name of an OpenSSL verify constant ('none', 'peer', 'client_once', 'fail_if_no_peer_cert') or directly the constant (`OpenSSL::SSL::VERIFY_NONE`, `OpenSSL::SSL::VERIFY_PEER`, ...).</li></ul>|
-|`sendmail_settings`|Allows you to override options for the `:sendmail` delivery method.<ul><li>`:location` - The location of the sendmail executable. Defaults to `/usr/sbin/sendmail`.</li><li>`:arguments` - The command line arguments to be passed to sendmail. Defaults to `-i -t`.</li></ul>|
+|`smtp_settings`|Allows detailed configuration for `:smtp` delivery method:<ul><li>`:address` - Allows you to use a remote mail server. Just change it from its default `"localhost"` setting.</li><li>`:port` - On the off chance that your mail server doesn't run on port 25, you can change it.</li><li>`:domain` - If you need to specify a HELO domain, you can do it here.</li><li>`:user_name` - If your mail server requires authentication, set the username in this setting.</li><li>`:password` - If your mail server requires authentication, set the password in this setting.</li><li>`:authentication` - If your mail server requires authentication, you need to specify the authentication type here. This is a symbol and one of `:plain` (will send the password in the clear), `:login` (will send password Base64 encoded) or `:cram_md5` (combines a Challenge/Response mechanism to exchange information and a cryptographic Message Digest 5 algorithm to hash important information)</li><li>`:enable_starttls_auto` - Detects if STARTTLS is enabled in your SMTP server and starts to use it. Defaults to `true`.</li><li>`:openssl_verify_mode` - When using TLS, you can set how OpenSSL checks the certificate. This is really useful if you need to validate a self-signed and/or a wildcard certificate. You can use the name of an OpenSSL verify constant ('none' or 'peer') or directly the constant (`OpenSSL::SSL::VERIFY_NONE` or `OpenSSL::SSL::VERIFY_PEER`).</li></ul>|
+|`sendmail_settings`|Allows you to override options for the `:sendmail` delivery method.<ul><li>`:location` - The location of the sendmail executable. Defaults to `/usr/sbin/sendmail`.</li><li>`:arguments` - The command line arguments to be passed to sendmail. Defaults to `-i`.</li></ul>|
|`raise_delivery_errors`|Whether or not errors should be raised if the email fails to be delivered. This only works if the external email server is configured for immediate delivery.|
|`delivery_method`|Defines a delivery method. Possible values are:<ul><li>`:smtp` (default), can be configured by using `config.action_mailer.smtp_settings`.</li><li>`:sendmail`, can be configured by using `config.action_mailer.sendmail_settings`.</li><li>`:file`: save emails to files; can be configured by using `config.action_mailer.file_settings`.</li><li>`:test`: save emails to `ActionMailer::Base.deliveries` array.</li></ul>See [API docs](http://api.rubyonrails.org/classes/ActionMailer/Base.html) for more info.|
-|`perform_deliveries`|Determines whether deliveries are actually carried out when the `deliver` method is invoked on the Mail message. By default they are, but this can be turned off to help functional testing.|
+|`perform_deliveries`|Determines whether deliveries are actually carried out when the `deliver` method is invoked on the Mail message. By default they are, but this can be turned off to help functional testing. If this value is `false`, `deliveries` array will not be populated even if `delivery_method` is `:test`.|
|`deliveries`|Keeps an array of all the emails sent out through the Action Mailer with delivery_method :test. Most useful for unit and functional testing.|
|`default_options`|Allows you to set default values for the `mail` method options (`:from`, `:reply_to`, etc.).|
@@ -736,7 +805,7 @@ config.action_mailer.delivery_method = :sendmail
# Defaults to:
# config.action_mailer.sendmail_settings = {
# location: '/usr/sbin/sendmail',
-# arguments: '-i -t'
+# arguments: '-i'
# }
config.action_mailer.perform_deliveries = true
config.action_mailer.raise_delivery_errors = true
@@ -757,8 +826,12 @@ config.action_mailer.smtp_settings = {
user_name: '<username>',
password: '<password>',
authentication: 'plain',
- enable_starttls_auto: true }
+ enable_starttls_auto: true }
```
+NOTE: As of July 15, 2014, Google increased [its security measures](https://support.google.com/accounts/answer/6010255) and now blocks attempts from apps it deems less secure.
+You can change your Gmail settings [here](https://www.google.com/settings/security/lesssecureapps) to allow the attempts. If your Gmail account has 2-factor authentication enabled,
+then you will need to set an [app password](https://myaccount.google.com/apppasswords) and use that instead of your regular password. Alternatively, you can
+use another ESP to send email by replacing 'smtp.gmail.com' above with the address of your provider.
Mailer Testing
--------------
@@ -766,13 +839,14 @@ Mailer Testing
You can find detailed instructions on how to test your mailers in the
[testing guide](testing.html#testing-your-mailers).
-Intercepting Emails
+Intercepting and Observing Emails
-------------------
-There are situations where you need to edit an email before it's
-delivered. Fortunately Action Mailer provides hooks to intercept every
-email. You can register an interceptor to make modifications to mail messages
-right before they are handed to the delivery agents.
+Action Mailer provides hooks into the Mail observer and interceptor methods. These allow you to register classes that are called during the mail delivery life cycle of every email sent.
+
+### Intercepting Emails
+
+Interceptors allow you to make modifications to emails before they are handed off to the delivery agents. An interceptor class must implement the `:delivering_email(message)` method which will be called before the email is sent.
```ruby
class SandboxEmailInterceptor
@@ -796,3 +870,21 @@ NOTE: The example above uses a custom environment called "staging" for a
production like server but for testing purposes. You can read
[Creating Rails environments](configuring.html#creating-rails-environments)
for more information about custom Rails environments.
+
+### Observing Emails
+
+Observers give you access to the email message after it has been sent. An observer class must implement the `:delivered_email(message)` method, which will be called after the email is sent.
+
+```ruby
+class EmailDeliveryObserver
+ def self.delivered_email(message)
+ EmailDelivery.log(message)
+ end
+end
+```
+Like interceptors, you need to register observers with the Action Mailer framework. You can do this in an initializer file
+`config/initializers/email_delivery_observer.rb`
+
+```ruby
+ActionMailer::Base.register_observer(EmailDeliveryObserver)
+```
diff --git a/guides/source/action_view_overview.md b/guides/source/action_view_overview.md
index 98c6cbd540..495ae9d267 100644
--- a/guides/source/action_view_overview.md
+++ b/guides/source/action_view_overview.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Action View Overview
====================
@@ -7,7 +7,7 @@ After reading this guide, you will know:
* What Action View is and how to use it with Rails.
* How best to use templates, partials, and layouts.
-* What helpers are provided by Action View and how to make your own.
+* What helpers are provided by Action View.
* How to use localized views.
--------------------------------------------------------------------------------
@@ -15,7 +15,7 @@ After reading this guide, you will know:
What is Action View?
--------------------
-Action View and Action Controller are the two major components of Action Pack. In Rails, web requests are handled by Action Pack, which splits the work into a controller part (performing the logic) and a view part (rendering a template). Typically, Action Controller will be concerned with communicating with the database and performing CRUD actions where necessary. Action View is then responsible for compiling the response.
+In Rails, web requests are handled by [Action Controller](action_controller_overview.html) and Action View. Typically, Action Controller is concerned with communicating with the database and performing CRUD actions where necessary. Action View is then responsible for compiling the response.
Action View templates are written using embedded Ruby in tags mingled with HTML. To avoid cluttering the templates with boilerplate code, a number of helper classes provide common behavior for forms, dates, and strings. It's also easy to add new helpers to your application as it evolves.
@@ -29,7 +29,7 @@ For each controller there is an associated directory in the `app/views` director
Let's take a look at what Rails does by default when creating a new resource using the scaffold generator:
```bash
-$ bin/rails generate scaffold article
+$ rails generate scaffold article
[...]
invoke scaffold_controller
create app/controllers/articles_controller.rb
@@ -48,7 +48,7 @@ For example, the index controller action of the `articles_controller.rb` will us
The complete HTML returned to the client is composed of a combination of this ERB file, a layout template that wraps it, and all the partials that the view may reference. Within this guide you will find more detailed documentation about each of these three components.
-Templates, Partials and Layouts
+Templates, Partials, and Layouts
-------------------------------
As mentioned, the final HTML output is a composition of three Rails elements: `Templates`, `Partials` and `Layouts`.
@@ -62,7 +62,7 @@ Rails supports multiple template systems and uses a file extension to distinguis
#### ERB
-Within an ERB template, Ruby code can be included using both `<% %>` and `<%= %>` tags. The `<% %>` tags are used to execute Ruby code that does not return anything, such as conditions, loops or blocks, and the `<%= %>` tags are used when you want output.
+Within an ERB template, Ruby code can be included using both `<% %>` and `<%= %>` tags. The `<% %>` tags are used to execute Ruby code that does not return anything, such as conditions, loops, or blocks, and the `<%= %>` tags are used when you want output.
Consider the following loop for names:
@@ -147,6 +147,39 @@ xml.rss("version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/") do
end
```
+#### Jbuilder
+[Jbuilder](https://github.com/rails/jbuilder) is a gem that's
+maintained by the Rails team and included in the default Rails `Gemfile`.
+It's similar to Builder, but is used to generate JSON, instead of XML.
+
+If you don't have it, you can add the following to your `Gemfile`:
+
+```ruby
+gem 'jbuilder'
+```
+
+A Jbuilder object named `json` is automatically made available to templates with
+a `.jbuilder` extension.
+
+Here is a basic example:
+
+```ruby
+json.name("Alex")
+json.email("alex@example.com")
+```
+
+would produce:
+
+```json
+{
+ "name": "Alex",
+ "email": "alex@example.com"
+}
+```
+
+See the [Jbuilder documentation](https://github.com/rails/jbuilder#jbuilder) for
+more examples and information.
+
#### Template Caching
By default, Rails will compile each template to a method in order to render it. When you alter a template, Rails will check the file's modification time and recompile it in development mode.
@@ -221,13 +254,7 @@ as if we had written:
<%= render partial: "product", locals: { product: @product } %>
```
-With the `as` option we can specify a different name for the local variable. For example, if we wanted it to be `item` instead of `product` we would do:
-
-```erb
-<%= render partial: "product", as: "item" %>
-```
-
-The `object` option can be used to directly specify which object is rendered into the partial; useful when the template's object is elsewhere (eg. in a different instance variable or in a local variable).
+The `object` option can be used to directly specify which object is rendered into the partial; useful when the template's object is elsewhere (e.g. in a different instance variable or in a local variable).
For example, instead of:
@@ -241,12 +268,18 @@ we would do:
<%= render partial: "product", object: @item %>
```
-The `object` and `as` options can also be used together:
+With the `as` option we can specify a different name for the said local variable. For example, if we wanted it to be `item` instead of `product` we would do:
```erb
<%= render partial: "product", object: @item, as: "item" %>
```
+This is equivalent to
+
+```erb
+<%= render partial: "product", locals: { item: @item } %>
+```
+
#### Rendering Collections
It is very common that a template will need to iterate over a collection and render a sub-template for each of the elements. This pattern has been implemented as a single method that accepts an array and renders a partial for each one of the elements in the array.
@@ -381,12 +414,12 @@ By default, Rails links to these assets on the current host in the public folder
```ruby
config.action_controller.asset_host = "assets.example.com"
-image_tag("rails.png") # => <img src="http://assets.example.com/images/rails.png" alt="Rails" />
+image_tag("rails.png") # => <img src="http://assets.example.com/images/rails.png" />
```
#### auto_discovery_link_tag
-Returns a link tag that browsers and feed readers can use to auto-detect an RSS or Atom feed.
+Returns a link tag that browsers and feed readers can use to auto-detect an RSS, Atom, or JSON feed.
```ruby
auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", { title: "RSS Feed" }) # =>
@@ -409,7 +442,7 @@ image_path("edit.png") # => /assets/edit-2d1a2db63fc738690021fedb5a65b68e.png
#### image_url
-Computes the url to an image asset in the `app/assets/images` directory. This will call `image_path` internally and merge with your current host or your asset host.
+Computes the URL to an image asset in the `app/assets/images` directory. This will call `image_path` internally and merge with your current host or your asset host.
```ruby
image_url("edit.png") # => http://www.example.com/assets/edit.png
@@ -420,7 +453,7 @@ image_url("edit.png") # => http://www.example.com/assets/edit.png
Returns an HTML image tag for the source. The source can be a full path or a file that exists in your `app/assets/images` directory.
```ruby
-image_tag("icon.png") # => <img src="/assets/icon.png" alt="Icon" />
+image_tag("icon.png") # => <img src="/assets/icon.png" />
```
#### javascript_include_tag
@@ -431,25 +464,6 @@ Returns an HTML script tag for each of the sources provided. You can pass in the
javascript_include_tag "common" # => <script src="/assets/common.js"></script>
```
-If the application does not use the asset pipeline, to include the jQuery JavaScript library in your application, pass `:defaults` as the source. When using `:defaults`, if an `application.js` file exists in your `app/assets/javascripts` directory, it will be included as well.
-
-```ruby
-javascript_include_tag :defaults
-```
-
-You can also include all JavaScript files in the `app/assets/javascripts` directory using `:all` as the source.
-
-```ruby
-javascript_include_tag :all
-```
-
-You can also cache multiple JavaScript files into one file, which requires less HTTP connections to download and can better be compressed by gzip (leading to faster transfers). Caching will only happen if `ActionController::Base.perform_caching` is set to true (which is the case by default for the Rails production environment, but not for the development environment).
-
-```ruby
-javascript_include_tag :all, cache: true # =>
- <script src="/javascripts/all.js"></script>
-```
-
#### javascript_path
Computes the path to a JavaScript asset in the `app/assets/javascripts` directory. If the source filename has no extension, `.js` will be appended. Full paths from the document root will be passed through. Used internally by `javascript_include_tag` to build the script path.
@@ -460,7 +474,7 @@ javascript_path "common" # => /assets/common.js
#### javascript_url
-Computes the url to a JavaScript asset in the `app/assets/javascripts` directory. This will call `javascript_path` internally and merge with your current host or your asset host.
+Computes the URL to a JavaScript asset in the `app/assets/javascripts` directory. This will call `javascript_path` internally and merge with your current host or your asset host.
```ruby
javascript_url "common" # => http://www.example.com/assets/common.js
@@ -474,22 +488,9 @@ Returns a stylesheet link tag for the sources specified as arguments. If you don
stylesheet_link_tag "application" # => <link href="/assets/application.css" media="screen" rel="stylesheet" />
```
-You can also include all styles in the stylesheet directory using `:all` as the source:
-
-```ruby
-stylesheet_link_tag :all
-```
-
-You can also cache multiple stylesheets into one file, which requires less HTTP connections and can better be compressed by gzip (leading to faster transfers). Caching will only happen if ActionController::Base.perform_caching is set to true (which is the case by default for the Rails production environment, but not for the development environment).
-
-```ruby
-stylesheet_link_tag :all, cache: true
-# => <link href="/assets/all.css" media="screen" rel="stylesheet" />
-```
-
#### stylesheet_path
-Computes the path to a stylesheet asset in the `app/assets/stylesheets` directory. If the source filename has no extension, `.css` will be appended. Full paths from the document root will be passed through. Used internally by stylesheet_link_tag to build the stylesheet path.
+Computes the path to a stylesheet asset in the `app/assets/stylesheets` directory. If the source filename has no extension, `.css` will be appended. Full paths from the document root will be passed through. Used internally by `stylesheet_link_tag` to build the stylesheet path.
```ruby
stylesheet_path "application" # => /assets/application.css
@@ -497,7 +498,7 @@ stylesheet_path "application" # => /assets/application.css
#### stylesheet_url
-Computes the url to a stylesheet asset in the `app/assets/stylesheets` directory. This will call `stylesheet_path` internally and merge with your current host or your asset host.
+Computes the URL to a stylesheet asset in the `app/assets/stylesheets` directory. This will call `stylesheet_path` internally and merge with your current host or your asset host.
```ruby
stylesheet_url "application" # => http://www.example.com/assets/application.css
@@ -566,7 +567,7 @@ This would add something like "Process data files (0.34523)" to the log, which y
#### cache
-A method for caching fragments of a view rather than an entire action or page. This technique is useful caching pieces like menus, lists of news topics, static HTML fragments, and so on. This method takes a block that contains the content you wish to cache. See `ActionController::Caching::Fragments` for more information.
+A method for caching fragments of a view rather than an entire action or page. This technique is useful for caching pieces like menus, lists of news topics, static HTML fragments, and so on. This method takes a block that contains the content you wish to cache. See `AbstractController::Caching::Fragments` for more information.
```erb
<% cache do %>
@@ -759,7 +760,7 @@ time_ago_in_words(3.minutes.from_now) # => 3 minutes
#### time_select
-Returns a set of select tags (one for hour, minute and optionally second) pre-selected for accessing a specified time-based attribute. The selects are prepared for multi-parameter assignment to an Active Record object.
+Returns a set of select tags (one for hour, minute, and optionally second) pre-selected for accessing a specified time-based attribute. The selects are prepared for multi-parameter assignment to an Active Record object.
```ruby
# Creates a time select tag that, when POSTed, will be stored in the order variable in the submitted attribute
@@ -806,20 +807,22 @@ The core method of this helper, `form_for`, gives you the ability to create a fo
The HTML generated for this would be:
```html
-<form action="/people/create" method="post">
- <input id="person_first_name" name="person[first_name]" type="text" />
- <input id="person_last_name" name="person[last_name]" type="text" />
- <input name="commit" type="submit" value="Create" />
+<form class="new_person" id="new_person" action="/people" accept-charset="UTF-8" method="post">
+ <input name="utf8" type="hidden" value="&#x2713;" />
+ <input type="hidden" name="authenticity_token" value="lTuvBzs7ANygT0NFinXj98tfw3Emfm65wwYLbUvoWsK2pngccIQSUorM2C035M9dZswXgWTvKwFS8W5TVblpYw==" />
+ <input type="text" name="person[first_name]" id="person_first_name" />
+ <input type="text" name="person[last_name]" id="person_last_name" />
+ <input type="submit" name="commit" value="Create" data-disable-with="Create" />
</form>
```
The params object created when this form is submitted would look like:
```ruby
-{ "action" => "create", "controller" => "people", "person" => { "first_name" => "William", "last_name" => "Smith" } }
+{"utf8" => "✓", "authenticity_token" => "lTuvBzs7ANygT0NFinXj98tfw3Emfm65wwYLbUvoWsK2pngccIQSUorM2C035M9dZswXgWTvKwFS8W5TVblpYw==", "person" => {"first_name" => "William", "last_name" => "Smith"}, "commit" => "Create", "controller" => "people", "action" => "create"}
```
-The params hash has a nested person value, which can therefore be accessed with params[:person] in the controller.
+The params hash has a nested person value, which can therefore be accessed with `params[:person]` in the controller.
#### check_box
@@ -957,11 +960,11 @@ Returns `select` and `option` tags for the collection of existing return values
Example object structure for use with this method:
```ruby
-class Article < ActiveRecord::Base
+class Article < ApplicationRecord
belongs_to :author
end
-class Author < ActiveRecord::Base
+class Author < ApplicationRecord
has_many :articles
def name_with_initial
"#{first_name.first}. #{last_name}"
@@ -993,11 +996,11 @@ Returns `radio_button` tags for the collection of existing return values of `met
Example object structure for use with this method:
```ruby
-class Article < ActiveRecord::Base
+class Article < ApplicationRecord
belongs_to :author
end
-class Author < ActiveRecord::Base
+class Author < ApplicationRecord
has_many :articles
def name_with_initial
"#{first_name.first}. #{last_name}"
@@ -1029,11 +1032,11 @@ Returns `check_box` tags for the collection of existing return values of `method
Example object structure for use with this method:
```ruby
-class Article < ActiveRecord::Base
+class Article < ApplicationRecord
has_and_belongs_to_many :authors
end
-class Author < ActiveRecord::Base
+class Author < ApplicationRecord
has_and_belongs_to_many :articles
def name_with_initial
"#{first_name.first}. #{last_name}"
@@ -1066,12 +1069,12 @@ Returns a string of `option` tags, like `options_from_collection_for_select`, bu
Example object structure for use with this method:
```ruby
-class Continent < ActiveRecord::Base
+class Continent < ApplicationRecord
has_many :countries
# attribs: id, name
end
-class Country < ActiveRecord::Base
+class Country < ApplicationRecord
belongs_to :continent
# attribs: id, name, continent_id
end
@@ -1099,7 +1102,7 @@ Possible output:
</optgroup>
```
-Note: Only the `optgroup` and `option` tags are returned, so you still have to wrap the output in an appropriate `select` tag.
+NOTE: Only the `optgroup` and `option` tags are returned, so you still have to wrap the output in an appropriate `select` tag.
#### options_for_select
@@ -1110,7 +1113,7 @@ options_for_select([ "VISA", "MasterCard" ])
# => <option>VISA</option> <option>MasterCard</option>
```
-Note: Only the `option` tags are returned, you have to wrap this call in a regular HTML `select` tag.
+NOTE: Only the `option` tags are returned, you have to wrap this call in a regular HTML `select` tag.
#### options_from_collection_for_select
@@ -1127,7 +1130,7 @@ options_from_collection_for_select(@project.people, "id", "name")
# => <option value="#{person.id}">#{person.name}</option>
```
-Note: Only the `option` tags are returned, you have to wrap this call in a regular HTML `select` tag.
+NOTE: Only the `option` tags are returned, you have to wrap this call in a regular HTML `select` tag.
#### select
@@ -1159,7 +1162,7 @@ Returns a string of option tags for pretty much any time zone in the world.
Returns select and option tags for the given object and method, using `time_zone_options_for_select` to generate the list of option tags.
```ruby
-time_zone_select( "user", "time_zone")
+time_zone_select("user", "time_zone")
```
#### date_field
@@ -1214,7 +1217,7 @@ file_field_tag 'attachment'
#### form_tag
-Starts a form tag that points the action to an url configured with `url_for_options` just like `ActionController::Base#url_for`.
+Starts a form tag that points the action to a URL configured with `url_for_options` just like `ActionController::Base#url_for`.
```html+erb
<%= form_tag '/articles' do %>
@@ -1264,8 +1267,8 @@ password_field_tag 'pass'
Creates a radio button; use groups of radio buttons named the same to allow users to select from a group of options.
```ruby
-radio_button_tag 'gender', 'male'
-# => <input id="gender_male" name="gender" type="radio" value="male" />
+radio_button_tag 'favorite_color', 'maroon'
+# => <input id="favorite_color_maroon" name="favorite_color" type="radio" value="maroon" />
```
#### select_tag
@@ -1386,7 +1389,7 @@ number_to_percentage(100, precision: 0) # => 100%
#### number_to_phone
-Formats a number into a US phone number.
+Formats a number into a phone number (US by default).
```ruby
number_to_phone(1235551234) # => 123-555-1234
@@ -1406,7 +1409,7 @@ Formats a number with the specified level of `precision`, which defaults to 3.
```ruby
number_with_precision(111.2345) # => 111.235
-number_with_precision(111.2345, 2) # => 111.23
+number_with_precision(111.2345, precision: 2) # => 111.23
```
### SanitizeHelper
@@ -1443,12 +1446,12 @@ Sanitizes a block of CSS code.
Strips all link tags from text leaving just the link text.
```ruby
-strip_links("<a href="http://rubyonrails.org">Ruby on Rails</a>")
+strip_links('<a href="http://rubyonrails.org">Ruby on Rails</a>')
# => Ruby on Rails
```
```ruby
-strip_links("emails to <a href="mailto:me@email.com">me@email.com</a>.")
+strip_links('emails to <a href="mailto:me@email.com">me@email.com</a>.')
# => emails to me@email.com.
```
@@ -1460,7 +1463,7 @@ strip_links('Blog: <a href="http://myblog.com/">Visit</a>.')
#### strip_tags(html)
Strips all HTML tags from the html, including comments.
-This uses the html-scanner tokenizer and so its HTML parsing ability is limited by that of html-scanner.
+This functionality is powered by the rails-html-sanitizer gem.
```ruby
strip_tags("Strip <i>these</i> tags!")
@@ -1491,7 +1494,7 @@ Localized Views
Action View has the ability to render different templates depending on the current locale.
-For example, suppose you have a `ArticlesController` with a show action. By default, calling this action will render `app/views/articles/show.html.erb`. But if you set `I18n.locale = :de`, then `app/views/articles/show.de.html.erb` will be rendered instead. If the localized template isn't present, the undecorated version will be used. This means you're not required to provide localized views for all cases, but they will be preferred and used if available.
+For example, suppose you have an `ArticlesController` with a show action. By default, calling this action will render `app/views/articles/show.html.erb`. But if you set `I18n.locale = :de`, then `app/views/articles/show.de.html.erb` will be rendered instead. If the localized template isn't present, the undecorated version will be used. This means you're not required to provide localized views for all cases, but they will be preferred and used if available.
You can use the same technique to localize the rescue files in your public directory. For example, setting `I18n.locale = :de` and creating `public/500.de.html` and `public/404.de.html` would allow you to have localized rescue pages.
diff --git a/guides/source/active_job_basics.md b/guides/source/active_job_basics.md
index dd545b56f5..39239852ca 100644
--- a/guides/source/active_job_basics.md
+++ b/guides/source/active_job_basics.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Active Job Basics
=================
@@ -34,8 +34,9 @@ Delayed Job and Resque. Picking your queuing backend becomes more of an operatio
concern, then. And you'll be able to switch between them without having to rewrite
your jobs.
-NOTE: Rails by default comes with an "immediate runner" queuing implementation.
-That means that each job that has been enqueued will run immediately.
+NOTE: Rails by default comes with an asynchronous queuing implementation that
+runs jobs with an in-process thread pool. Jobs will run asynchronously, but any
+jobs in the queue will be dropped upon restart.
Creating a Job
@@ -49,7 +50,7 @@ Active Job provides a Rails generator to create jobs. The following will create
job in `app/jobs` (with an attached test case under `test/jobs`):
```bash
-$ bin/rails generate job guests_cleanup
+$ rails generate job guests_cleanup
invoke test_unit
create test/jobs/guests_cleanup_job_test.rb
create app/jobs/guests_cleanup_job.rb
@@ -58,59 +59,68 @@ create app/jobs/guests_cleanup_job.rb
You can also create a job that will run on a specific queue:
```bash
-$ bin/rails generate job guests_cleanup --queue urgent
+$ rails generate job guests_cleanup --queue urgent
```
If you don't want to use a generator, you could create your own file inside of
-`app/jobs`, just make sure that it inherits from `ActiveJob::Base`.
+`app/jobs`, just make sure that it inherits from `ApplicationJob`.
Here's what a job looks like:
```ruby
-class GuestsCleanupJob < ActiveJob::Base
+class GuestsCleanupJob < ApplicationJob
queue_as :default
- def perform(*args)
+ def perform(*guests)
# Do something later
end
end
```
+Note that you can define `perform` with as many arguments as you want.
+
### Enqueue the Job
Enqueue a job like so:
```ruby
-# Enqueue a job to be performed as soon the queuing system is
+# Enqueue a job to be performed as soon as the queuing system is
# free.
-MyJob.perform_later record
+GuestsCleanupJob.perform_later guest
```
```ruby
# Enqueue a job to be performed tomorrow at noon.
-MyJob.set(wait_until: Date.tomorrow.noon).perform_later(record)
+GuestsCleanupJob.set(wait_until: Date.tomorrow.noon).perform_later(guest)
```
```ruby
# Enqueue a job to be performed 1 week from now.
-MyJob.set(wait: 1.week).perform_later(record)
+GuestsCleanupJob.set(wait: 1.week).perform_later(guest)
```
-That's it!
+```ruby
+# `perform_now` and `perform_later` will call `perform` under the hood so
+# you can pass as many arguments as defined in the latter.
+GuestsCleanupJob.perform_later(guest1, guest2, filter: 'some_filter')
+```
+That's it!
Job Execution
-------------
-For enqueuing and executing jobs you need to set up a queuing backend, that is to
-say you need to decide for a 3rd-party queuing library that Rails should use.
-Rails itself does not provide a sophisticated queuing system and just executes the
-job immediately if no adapter is set.
+For enqueuing and executing jobs in production you need to set up a queuing backend,
+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 backend. This may be fine for smaller apps or non-critical jobs, but most
+production apps will need to pick a persistent backend.
### Backends
Active Job has built-in adapters for multiple queuing backends (Sidekiq,
-Resque, Delayed Job and others). To get an up-to-date list of the adapters
+Resque, Delayed Job, and others). To get an up-to-date list of the adapters
see the API Documentation for [ActiveJob::QueueAdapters](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html).
### Setting the Backend
@@ -129,10 +139,32 @@ module YourApp
end
```
-NOTE: Since jobs run in parallel to your Rails application, most queuing libraries
+You can also configure your backend on a per job basis.
+
+```ruby
+class GuestsCleanupJob < ApplicationJob
+ self.queue_adapter = :resque
+ #....
+end
+
+# Now your job will use `resque` as its backend queue adapter overriding what
+# was configured in `config.active_job.queue_adapter`.
+```
+
+### Starting the Backend
+
+Since jobs run in parallel to your Rails application, most queuing libraries
require that you start a library-specific queuing service (in addition to
-starting your Rails app) for the job processing to work. For information on
-how to do that refer to the documentation of your respective library.
+starting your Rails app) for the job processing to work. Refer to library
+documentation for instructions on starting your queue backend.
+
+Here is a noncomprehensive list of documentation:
+
+- [Sidekiq](https://github.com/mperham/sidekiq/wiki/Active-Job)
+- [Resque](https://github.com/resque/resque/wiki/ActiveJob)
+- [Sneakers](https://github.com/jondot/sneakers/wiki/How-To:-Rails-Background-Jobs-with-ActiveJob)
+- [Sucker Punch](https://github.com/brandonhilkert/sucker_punch#active-job)
+- [Queue Classic](https://github.com/QueueClassic/queue_classic#active-job)
Queues
------
@@ -141,7 +173,7 @@ Most of the adapters support multiple queues. With Active Job you can schedule
the job to run on a specific queue:
```ruby
-class GuestsCleanupJob < ActiveJob::Base
+class GuestsCleanupJob < ApplicationJob
queue_as :low_priority
#....
end
@@ -158,8 +190,8 @@ module YourApp
end
end
-# app/jobs/guests_cleanup.rb
-class GuestsCleanupJob < ActiveJob::Base
+# app/jobs/guests_cleanup_job.rb
+class GuestsCleanupJob < ApplicationJob
queue_as :low_priority
#....
end
@@ -181,8 +213,8 @@ module YourApp
end
end
-# app/jobs/guests_cleanup.rb
-class GuestsCleanupJob < ActiveJob::Base
+# app/jobs/guests_cleanup_job.rb
+class GuestsCleanupJob < ApplicationJob
queue_as :low_priority
#....
end
@@ -204,7 +236,7 @@ block will be executed in the job context (so you can access `self.arguments`)
and you must return the queue name:
```ruby
-class ProcessVideoJob < ActiveJob::Base
+class ProcessVideoJob < ApplicationJob
queue_as do
video = self.arguments.first
if video.owner.premium?
@@ -229,40 +261,48 @@ backends you need to specify the queues to listen to.
Callbacks
---------
-Active Job provides hooks during the life cycle of a job. Callbacks allow you to
-trigger logic during the life cycle of a job.
-
-### Available callbacks
-
-* `before_enqueue`
-* `around_enqueue`
-* `after_enqueue`
-* `before_perform`
-* `around_perform`
-* `after_perform`
-
-### Usage
+Active Job provides hooks to trigger logic during the life cycle of a job. Like
+other callbacks in Rails, you can implement the callbacks as ordinary methods
+and use a macro-style class method to register them as callbacks:
```ruby
-class GuestsCleanupJob < ActiveJob::Base
+class GuestsCleanupJob < ApplicationJob
queue_as :default
- before_enqueue do |job|
- # Do something with the job instance
- end
-
- around_perform do |job, block|
- # Do something before perform
- block.call
- # Do something after perform
- end
+ around_perform :around_cleanup
def perform
# Do something later
end
+
+ private
+ def around_cleanup
+ # Do something before perform
+ yield
+ # Do something after perform
+ end
end
```
+The macro-style class methods can also receive a block. Consider using this
+style if the code inside your block is so short that it fits in a single line.
+For example, you could send metrics for every job enqueued:
+
+```ruby
+class ApplicationJob < ActiveJob::Base
+ before_enqueue { |job| $statsd.increment "#{job.class.name.underscore}.enqueue" }
+end
+```
+
+### Available callbacks
+
+* `before_enqueue`
+* `around_enqueue`
+* `after_enqueue`
+* `before_perform`
+* `around_perform`
+* `after_perform`
+
Action Mailer
------------
@@ -279,16 +319,50 @@ UserMailer.welcome(@user).deliver_now
UserMailer.welcome(@user).deliver_later
```
+NOTE: Using the asynchronous queue from a Rake task (for example, to
+send an email using `.deliver_later`) will generally not work because Rake will
+likely end, causing the in-process thread pool to be deleted, before any/all
+of the `.deliver_later` emails are processed. To avoid this problem, use
+`.deliver_now` or run a persistent queue in development.
+
+
+Internationalization
+--------------------
+
+Each job uses the `I18n.locale` set when the job was created. Useful if you send
+emails asynchronously:
+
+```ruby
+I18n.locale = :eo
+
+UserMailer.welcome(@user).deliver_later # Email will be localized to Esperanto.
+```
+
+
+Supported types for arguments
+----------------------------
+
+ActiveJob supports the following types of arguments by default:
+
+ - Basic types (`NilClass`, `String`, `Integer`, `Float`, `BigDecimal`, `TrueClass`, `FalseClass`)
+ - `Symbol`
+ - `Date`
+ - `Time`
+ - `DateTime`
+ - `ActiveSupport::TimeWithZone`
+ - `ActiveSupport::Duration`
+ - `Hash` (Keys should be of `String` or `Symbol` type)
+ - `ActiveSupport::HashWithIndifferentAccess`
+ - `Array`
-GlobalID
---------
+### GlobalID
Active Job supports GlobalID for parameters. This makes it possible to pass live
Active Record objects to your job instead of class/id pairs, which you then have
to manually deserialize. Before, jobs would look like this:
```ruby
-class TrashableCleanupJob < ActiveJob::Base
+class TrashableCleanupJob < ApplicationJob
def perform(trashable_class, trashable_id, depth)
trashable = trashable_class.constantize.find(trashable_id)
trashable.cleanup(depth)
@@ -299,7 +373,7 @@ end
Now you can simply do:
```ruby
-class TrashableCleanupJob < ActiveJob::Base
+class TrashableCleanupJob < ApplicationJob
def perform(trashable, depth)
trashable.cleanup(depth)
end
@@ -309,6 +383,39 @@ end
This works with any class that mixes in `GlobalID::Identification`, which
by default has been mixed into Active Record classes.
+### Serializers
+
+You can extend the list of supported argument types. You just need to define your own serializer:
+
+```ruby
+class MoneySerializer < ActiveJob::Serializers::ObjectSerializer
+ # Checks if an argument should be serialized by this serializer.
+ def serialize?(argument)
+ argument.is_a? Money
+ end
+
+ # Converts an object to a simpler representative using supported object types.
+ # The recommended representative is a Hash with a specific key. Keys can be of basic types only.
+ # You should call `super` to add the custom serializer type to the hash.
+ def serialize(money)
+ super(
+ "amount" => money.amount,
+ "currency" => money.currency
+ )
+ end
+
+ # Converts serialized value into a proper object.
+ def deserialize(hash)
+ Money.new(hash["amount"], hash["currency"])
+ end
+end
+```
+
+and add this serializer to the list:
+
+```ruby
+Rails.application.config.active_job.custom_serializers << MoneySerializer
+```
Exceptions
----------
@@ -317,11 +424,11 @@ Active Job provides a way to catch exceptions raised during the execution of the
job:
```ruby
-class GuestsCleanupJob < ActiveJob::Base
+class GuestsCleanupJob < ApplicationJob
queue_as :default
rescue_from(ActiveRecord::RecordNotFound) do |exception|
- # Do something with the exception
+ # Do something with the exception
end
def perform
@@ -330,6 +437,25 @@ class GuestsCleanupJob < ActiveJob::Base
end
```
+### Retrying or Discarding failed jobs
+
+It's also possible to retry or discard a job if an exception is raised during execution.
+For example:
+
+```ruby
+class RemoteServiceJob < ApplicationJob
+ retry_on CustomAppException # defaults to 3s wait, 5 attempts
+
+ discard_on ActiveJob::DeserializationError
+
+ def perform(*args)
+ # Might raise CustomAppException or ActiveJob::DeserializationError
+ end
+end
+```
+
+To get more details see the API Documentation for [ActiveJob::Exceptions](http://api.rubyonrails.org/classes/ActiveJob/Exceptions/ClassMethods.html).
+
### Deserialization
GlobalID allows serializing full Active Record objects passed to `#perform`.
diff --git a/guides/source/active_model_basics.md b/guides/source/active_model_basics.md
index 4b2bfaee2f..2e1bb1a23d 100644
--- a/guides/source/active_model_basics.md
+++ b/guides/source/active_model_basics.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Active Model Basics
===================
@@ -8,12 +8,12 @@ classes. Active Model allows for Action Pack helpers to interact with
plain Ruby objects. Active Model also helps build custom ORMs for use
outside of the Rails framework.
-After reading this guide, you will be able to add to plain Ruby objects:
+After reading this guide, you will know:
-* The ability to behave like an Active Record model.
-* Callbacks and validations like Active Record.
-* Serializers.
-* Integration with the Rails internationalization (i18n) framework.
+* How an Active Record model behaves.
+* How Callbacks and validations work.
+* How serializers work.
+* How Active Model integrates with the Rails internationalization (i18n) framework.
--------------------------------------------------------------------------------
@@ -61,7 +61,7 @@ person.age_highest? # => false
`ActiveModel::Callbacks` gives Active Record style callbacks. This provides an
ability to define callbacks which run at appropriate times.
-After defining callbacks, you can wrap them with before, after and around
+After defining callbacks, you can wrap them with before, after, and around
custom methods.
```ruby
@@ -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 if any attribute has changed.
+# 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
-`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
@@ -292,8 +293,8 @@ objects.
### Serialization
-`ActiveModel::Serialization` provides a basic serialization for your object.
-You need to declare an attributes hash which contains the attributes you want to
+`ActiveModel::Serialization` provides basic serialization for your object.
+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,14 +320,14 @@ person.serializable_hash # => {"name"=>"Bob"}
#### ActiveModel::Serializers
-Rails provides two serializers `ActiveModel::Serializers::JSON` and
-`ActiveModel::Serializers::Xml`. Both of these modules 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
@@ -340,7 +341,8 @@ class Person
end
```
-With the `as_json` 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
@@ -349,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
@@ -370,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
@@ -379,62 +381,6 @@ person.from_json(json) # => #<Person:0x00000100c773f0 @name="Bob">
person.name # => "Bob"
```
-##### ActiveModel::Serializers::Xml
-
-To use the `ActiveModel::Serializers::Xml` you only need to change from
-`ActiveModel::Serialization` to `ActiveModel::Serializers::Xml`.
-
-```ruby
-class Person
- include ActiveModel::Serializers::Xml
-
- attr_accessor :name
-
- def attributes
- {'name' => nil}
- end
-end
-```
-
-With the `to_xml` you have an XML representing the model.
-
-```ruby
-person = Person.new
-person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person>\n <name nil=\"true\"/>\n</person>\n"
-person.name = "Bob"
-person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person>\n <name>Bob</name>\n</person>\n"
-```
-
-From an XML string you define the attributes of the model.
-You need to have the `attributes=` method defined on your class:
-
-```ruby
-class Person
- include ActiveModel::Serializers::Xml
-
- attr_accessor :name
-
- def attributes=(hash)
- hash.each do |key, value|
- send("#{key}=", value)
- end
- end
-
- def attributes
- {'name' => nil}
- end
-end
-```
-
-Now it is possible to create an instance of person and set the attributes using `from_xml`.
-
-```ruby
-xml = { name: 'Bob' }.to_xml
-person = Person.new
-person.from_xml(xml) # => #<Person:0x00000100c773f0 @name="Bob">
-person.name # => "Bob"
-```
-
### Translation
`ActiveModel::Translation` provides integration between your object and the Rails
@@ -446,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
@@ -465,19 +411,18 @@ Person.human_attribute_name('name') # => "Nome"
### Lint Tests
-`ActiveModel::Lint::Tests` allow you to test whether an object is compliant with
+`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
include ActiveModel::Model
-
end
```
-* test/models/person_test.rb
+* `test/models/person_test.rb`
```ruby
require 'test_helper'
@@ -485,14 +430,14 @@ the Active Model API.
class PersonTest < ActiveSupport::TestCase
include ActiveModel::Lint::Tests
- def setup
+ setup do
@model = Person.new
end
end
```
```bash
-$ rake test
+$ rails test
Run options: --seed 14596
@@ -512,20 +457,21 @@ 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 by default.
#### Requirements
-`ActiveModel::SecurePassword` depends on the [`bcrypt`](https://github.com/codahale/bcrypt-ruby 'BCrypt'),
-so include this gem in your Gemfile to use `ActiveModel::SecurePassword` correctly.
-In order to make this work, the model must have an accessor named `password_digest`.
-The `has_secure_password` will add the following validations on the `password` accessor:
+`ActiveModel::SecurePassword` depends on [`bcrypt`](https://github.com/codahale/bcrypt-ruby 'BCrypt'),
+so include this gem in your `Gemfile` to use `ActiveModel::SecurePassword` correctly.
+In order to make this work, the model must have an accessor named `XXX_digest`.
+Where `XXX` is the attribute name of your desired password.
+The following validations are added automatically:
1. Password should be present.
-2. Password should be equal to its confirmation.
-3. This maximum length of a password is 72 (required by `bcrypt` on which ActiveModel::SecurePassword depends)
+2. Password should be equal to its confirmation (provided `XXX_confirmation` is passed along).
+3. The maximum length of a password is 72 (required by `bcrypt` on which ActiveModel::SecurePassword depends)
#### Examples
@@ -533,7 +479,9 @@ The `has_secure_password` will add the following validations on the `password` a
class Person
include ActiveModel::SecurePassword
has_secure_password
- attr_accessor :password_digest
+ has_secure_password :recovery_password, validations: false
+
+ attr_accessor :password_digest, :recovery_password_digest
end
person = Person.new
@@ -546,11 +494,28 @@ person.password = 'aditya'
person.password_confirmation = 'nomatch'
person.valid? # => false
-# When the length of password, exceeds 72.
+# When the length of password exceeds 72.
person.password = person.password_confirmation = 'a' * 100
person.valid? # => false
+# When only password is supplied with no password_confirmation.
+person.password = 'aditya'
+person.valid? # => true
+
# When all validations are passed.
person.password = person.password_confirmation = 'aditya'
person.valid? # => true
+
+person.recovery_password = "42password"
+
+person.authenticate('aditya') # => person
+person.authenticate('notright') # => false
+person.authenticate_password('aditya') # => person
+person.authenticate_password('notright') # => false
+
+person.authenticate_recovery_password('42password') # => person
+person.authenticate_recovery_password('notright') # => false
+
+person.password_digest # => "$2a$04$gF8RfZdoXHvyTjHhiU4ZsO.kQqV9oonYZu31PRE4hLQn3xM2qkpIy"
+person.recovery_password_digest # => "$2a$04$iOfhwahFymCs5weB3BNH/uXkTG65HR.qpW.bNhEjFP3ftli3o5DQC"
```
diff --git a/guides/source/active_record_basics.md b/guides/source/active_record_basics.md
index a227b54040..b9e24099b1 100644
--- a/guides/source/active_record_basics.md
+++ b/guides/source/active_record_basics.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Active Record Basics
====================
@@ -13,14 +13,14 @@ After reading this guide, you will know:
* How to use Active Record models to manipulate data stored in a relational
database.
* Active Record schema naming conventions.
-* The concepts of database migrations, validations and callbacks.
+* The concepts of database migrations, validations, and callbacks.
--------------------------------------------------------------------------------
What is Active Record?
----------------------
-Active Record is the M in [MVC](http://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) - the
+Active Record is the M in [MVC](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) - the
model - which is the layer of the system responsible for representing business
data and logic. Active Record facilitates the creation and use of business
objects whose data requires persistent storage to a database. It is an
@@ -38,13 +38,15 @@ object on how to write to and read from the database.
### Object Relational Mapping
-Object Relational Mapping, commonly referred to as its abbreviation ORM, is
+[Object Relational Mapping](https://en.wikipedia.org/wiki/Object-relational_mapping), commonly referred to as its abbreviation ORM, is
a technique that connects the rich objects of an application to tables in
a relational database management system. Using ORM, the properties and
relationships of the objects in an application can be easily stored and
retrieved from a database without writing SQL statements directly and with less
overall database access code.
+NOTE: If you are not familiar enough with relational database management systems (RDBMS) or structured query language (SQL), please go through [this tutorial](https://www.w3schools.com/sql/default.asp) (or [this one](http://www.sqlcourse.com/)) or study them by other means. Understanding how relational databases work is crucial to understanding Active Records and Rails in general.
+
### Active Record as an ORM Framework
Active Record gives us several mechanisms, the most important being the ability
@@ -80,9 +82,9 @@ of two or more words, the model class name should follow the Ruby conventions,
using the CamelCase form, while the table name must contain the words separated
by underscores. Examples:
-* Database Table - Plural with underscores separating words (e.g., `book_clubs`).
* Model Class - Singular with the first letter of each word capitalized (e.g.,
`BookClub`).
+* Database Table - Plural with underscores separating words (e.g., `book_clubs`).
| Model / Class | Table / Schema |
| ---------------- | -------------- |
@@ -104,7 +106,7 @@ depending on the purpose of these columns.
your models.
* **Primary keys** - By default, Active Record will use an integer column named
`id` as the table's primary key. When using [Active Record
- Migrations](migrations.html) to create your tables, this column will be
+ Migrations](active_record_migrations.html) to create your tables, this column will be
automatically created.
There are also some optional column names that will add additional features
@@ -113,7 +115,7 @@ to Active Record instances:
* `created_at` - Automatically gets set to the current date and time when the
record is first created.
* `updated_at` - Automatically gets set to the current date and time whenever
- the record is updated.
+ the record is created or updated.
* `lock_version` - Adds [optimistic
locking](http://api.rubyonrails.org/classes/ActiveRecord/Locking.html) to
a model.
@@ -132,17 +134,17 @@ Creating Active Record Models
-----------------------------
It is very easy to create Active Record models. All you have to do is to
-subclass the `ActiveRecord::Base` class and you're good to go:
+subclass the `ApplicationRecord` class and you're good to go:
```ruby
-class Product < ActiveRecord::Base
+class Product < ApplicationRecord
end
```
This will create a `Product` model, mapped to a `products` table at the
database. By doing this you'll also have the ability to map the columns of each
row in that table with the attributes of the instances of your model. Suppose
-that the `products` table was created using an SQL sentence like:
+that the `products` table was created using an SQL (or one of its extensions) statement like:
```sql
CREATE TABLE products (
@@ -152,8 +154,9 @@ CREATE TABLE products (
);
```
-Following the table schema above, you would be able to write code like the
-following:
+Schema above declares a table with two columns: `id` and `name`. Each row of
+this table represents a certain product with these two parameters. Thus, you
+would be able to write code like the following:
```ruby
p = Product.new
@@ -168,11 +171,12 @@ What if you need to follow a different naming convention or need to use your
Rails application with a legacy database? No problem, you can easily override
the default conventions.
-You can use the `ActiveRecord::Base.table_name=` method to specify the table
-name that should be used:
+`ApplicationRecord` inherits from `ActiveRecord::Base`, which defines a
+number of helpful methods. You can use the `ActiveRecord::Base.table_name=`
+method to specify the table name that should be used:
```ruby
-class Product < ActiveRecord::Base
+class Product < ApplicationRecord
self.table_name = "my_products"
end
```
@@ -193,7 +197,7 @@ It's also possible to override the column that should be used as the table's
primary key using the `ActiveRecord::Base.primary_key=` method:
```ruby
-class Product < ActiveRecord::Base
+class Product < ApplicationRecord
self.primary_key = "product_id"
end
```
@@ -207,7 +211,7 @@ to allow an application to read and manipulate data stored within its tables.
### Create
-Active Record objects can be created from a hash, a block or have their
+Active Record objects can be created from a hash, a block, or have their
attributes manually set after creation. The `new` method will return a new
object while `create` will return the object and save it to the database.
@@ -260,7 +264,7 @@ david = User.find_by(name: 'David')
```ruby
# find all users named David who are Code Artists and sort by created_at in reverse chronological order
-users = User.where(name: 'David', occupation: 'Code Artist').order('created_at DESC')
+users = User.where(name: 'David', occupation: 'Code Artist').order(created_at: :desc)
```
You can learn more about querying an Active Record model in the [Active Record
@@ -303,24 +307,35 @@ user = User.find_by(name: 'David')
user.destroy
```
+If you'd like to delete several records in bulk, you may use `destroy_all`
+method:
+
+```ruby
+# find and delete all users named David
+User.where(name: 'David').destroy_all
+
+# delete all users
+User.destroy_all
+```
+
Validations
-----------
Active Record allows you to validate the state of a model before it gets written
into the database. There are several methods that you can use to check your
models and validate that an attribute value is not empty, is unique and not
-already in the database, follows a specific format and many more.
+already in the database, follows a specific format, and many more.
Validation is a very important issue to consider when persisting to the database, so
the methods `save` and `update` take it into account when
-running: they return `false` when validation fails and they didn't actually
-perform any operation on the database. All of these have a bang counterpart (that
+running: they return `false` when validation fails and they don't actually
+perform any operations on the database. All of these have a bang counterpart (that
is, `save!` and `update!`), which are stricter in that
they raise the exception `ActiveRecord::RecordInvalid` if validation fails.
A quick example to illustrate:
```ruby
-class User < ActiveRecord::Base
+class User < ApplicationRecord
validates :name, presence: true
end
@@ -338,7 +353,7 @@ Callbacks
Active Record callbacks allow you to attach code to certain events in the
life-cycle of your models. This enables you to add behavior to your models by
transparently executing code when those events occur, like when you create a new
-record, update it, destroy it and so on. You can learn more about callbacks in
+record, update it, destroy it, and so on. You can learn more about callbacks in
the [Active Record Callbacks guide](active_record_callbacks.html).
Migrations
@@ -350,7 +365,7 @@ database that Active Record supports using `rake`. Here's a migration that
creates a table:
```ruby
-class CreatePublications < ActiveRecord::Migration
+class CreatePublications < ActiveRecord::Migration[5.0]
def change
create_table :publications do |t|
t.string :title
@@ -360,7 +375,7 @@ class CreatePublications < ActiveRecord::Migration
t.string :publisher_type
t.boolean :single_issue
- t.timestamps null: false
+ t.timestamps
end
add_index :publications, :publication_type_id
end
@@ -368,9 +383,9 @@ end
```
Rails keeps track of which files have been committed to the database and
-provides rollback features. To actually create the table, you'd run `rake db:migrate`
-and to roll it back, `rake db:rollback`.
+provides rollback features. To actually create the table, you'd run `rails db:migrate`
+and to roll it back, `rails db:rollback`.
Note that the above code is database-agnostic: it will run in MySQL,
-PostgreSQL, Oracle and others. You can learn more about migrations in the
-[Active Record Migrations guide](migrations.html).
+PostgreSQL, Oracle, and others. You can learn more about migrations in the
+[Active Record Migrations guide](active_record_migrations.html).
diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md
index 13989a3b33..ebdee446f9 100644
--- a/guides/source/active_record_callbacks.md
+++ b/guides/source/active_record_callbacks.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Active Record Callbacks
=======================
@@ -31,12 +31,12 @@ Callbacks are methods that get called at certain moments of an object's life cyc
In order to use the available callbacks, you need to register them. You can implement the callbacks as ordinary methods and use a macro-style class method to register them as callbacks:
```ruby
-class User < ActiveRecord::Base
+class User < ApplicationRecord
validates :login, :email, presence: true
before_validation :ensure_login_has_a_value
- protected
+ private
def ensure_login_has_a_value
if login.nil?
self.login = email unless email.blank?
@@ -48,7 +48,7 @@ end
The macro-style class methods can also receive a block. Consider using this style if the code inside your block is so short that it fits in a single line:
```ruby
-class User < ActiveRecord::Base
+class User < ApplicationRecord
validates :login, :email, presence: true
before_create do
@@ -60,13 +60,13 @@ end
Callbacks can also be registered to only fire on certain life cycle events:
```ruby
-class User < ActiveRecord::Base
+class User < ApplicationRecord
before_validation :normalize_name, on: :create
# :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 < ActiveRecord::Base
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
-------------------
@@ -117,6 +117,10 @@ Here is a list with all the available Active Record callbacks, listed in the sam
WARNING. `after_save` runs both on create and update, but always _after_ the more specific callbacks `after_create` and `after_update`, no matter the order in which the macro calls were executed.
+NOTE: `before_destroy` callbacks should be placed before `dependent: :destroy`
+associations (or use the `prepend: true` option), to ensure they execute before
+the records are deleted by `dependent: :destroy`.
+
### `after_initialize` and `after_find`
The `after_initialize` callback will be called whenever an Active Record object is instantiated, either by directly using `new` or when a record is loaded from the database. It can be useful to avoid the need to directly override your Active Record `initialize` method.
@@ -126,7 +130,7 @@ The `after_find` callback will be called whenever Active Record loads a record f
The `after_initialize` and `after_find` callbacks have no `before_*` counterparts, but they can be registered just like the other Active Record callbacks.
```ruby
-class User < ActiveRecord::Base
+class User < ApplicationRecord
after_initialize do |user|
puts "You have initialized an object!"
end
@@ -151,7 +155,7 @@ You have initialized an object!
The `after_touch` callback will be called whenever an Active Record object is touched.
```ruby
-class User < ActiveRecord::Base
+class User < ApplicationRecord
after_touch do |user|
puts "You have touched an object"
end
@@ -168,21 +172,21 @@ You have touched an object
It can be used along with `belongs_to`:
```ruby
-class Employee < ActiveRecord::Base
+class Employee < ApplicationRecord
belongs_to :company, touch: true
after_touch do
puts 'An Employee was touched'
end
end
-class Company < ActiveRecord::Base
+class Company < ApplicationRecord
has_many :employees
after_touch :log_when_employees_or_company_touched
private
- def log_when_employees_or_company_touched
- puts 'Employee/Company was touched'
- end
+ def log_when_employees_or_company_touched
+ puts 'Employee/Company was touched'
+ end
end
>> @employee = Employee.last
@@ -190,8 +194,8 @@ end
# triggers @employee.company.touch
>> @employee.touch
-Employee/Company was touched
An Employee was touched
+Employee/Company was touched
=> true
```
@@ -202,15 +206,14 @@ The following methods trigger callbacks:
* `create`
* `create!`
-* `decrement!`
* `destroy`
* `destroy!`
* `destroy_all`
-* `increment!`
* `save`
* `save!`
* `save(validate: false)`
* `toggle!`
+* `touch`
* `update_attribute`
* `update`
* `update!`
@@ -243,7 +246,6 @@ Just as with validations, it is also possible to skip callbacks by using the fol
* `increment`
* `increment_counter`
* `toggle`
-* `touch`
* `update_column`
* `update_columns`
* `update_all`
@@ -256,9 +258,13 @@ Halting Execution
As you start registering new callbacks for your models, they will be queued for execution. This queue will include all your model's validations, the registered callbacks, and the database operation to be executed.
-The whole callback chain is wrapped in a transaction. If any _before_ callback method returns exactly `false` or raises an exception, the execution chain gets halted and a ROLLBACK is issued; _after_ callbacks can only accomplish that by raising an exception.
+The whole callback chain is wrapped in a transaction. If any callback raises an exception, the execution chain gets halted and a ROLLBACK is issued. To intentionally stop a chain use:
+
+```ruby
+throw :abort
+```
-WARNING. Any exception that is not `ActiveRecord::Rollback` will be re-raised by Rails after the callback chain is halted. Raising an exception other than `ActiveRecord::Rollback` may break code that does not expect methods like `save` and `update_attributes` (which normally try to return `true` or `false`) to raise an exception.
+WARNING. Any exception that is not `ActiveRecord::Rollback` or `ActiveRecord::RecordInvalid` will be re-raised by Rails after the callback chain is halted. Raising an exception other than `ActiveRecord::Rollback` or `ActiveRecord::RecordInvalid` may break code that does not expect methods like `save` and `update` (which normally try to return `true` or `false`) to raise an exception.
Relational Callbacks
--------------------
@@ -266,11 +272,11 @@ Relational Callbacks
Callbacks work through model relationships, and can even be defined by them. Suppose an example where a user has many articles. A user's articles should be destroyed if the user is destroyed. Let's add an `after_destroy` callback to the `User` model by way of its relationship to the `Article` model:
```ruby
-class User < ActiveRecord::Base
+class User < ApplicationRecord
has_many :articles, dependent: :destroy
end
-class Article < ActiveRecord::Base
+class Article < ApplicationRecord
after_destroy :log_destroy_action
def log_destroy_action
@@ -290,36 +296,34 @@ 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`
You can associate the `:if` and `:unless` options with a symbol corresponding to the name of a predicate method that will get called right before the callback. When using the `:if` option, the callback won't be executed if the predicate method returns false; when using the `:unless` option, the callback won't be executed if the predicate method returns true. This is the most common option. Using this form of registration it is also possible to register several different predicates that should be called to check if the callback should be executed.
```ruby
-class Order < ActiveRecord::Base
+class Order < ApplicationRecord
before_save :normalize_card_number, if: :paid_with_card?
end
```
-### Using `:if` and `:unless` with a String
+### Using `:if` and `:unless` with a `Proc`
-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:
+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:
```ruby
-class Order < ActiveRecord::Base
- before_save :normalize_card_number, if: "paid_with_card?"
+class Order < ApplicationRecord
+ before_save :normalize_card_number,
+ if: Proc.new { |order| order.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:
+As the proc is evaluated in the context of the object, it is also possible to write this as:
```ruby
-class Order < ActiveRecord::Base
- before_save :normalize_card_number,
- if: Proc.new { |order| order.paid_with_card? }
+class Order < ApplicationRecord
+ before_save :normalize_card_number, if: Proc.new { paid_with_card? }
end
```
@@ -328,7 +332,7 @@ end
When writing conditional callbacks, it is possible to mix both `:if` and `:unless` in the same callback declaration:
```ruby
-class Comment < ActiveRecord::Base
+class Comment < ApplicationRecord
after_create :send_email_to_author, if: :author_wants_emails?,
unless: Proc.new { |comment| comment.article.ignore_comments? }
end
@@ -354,7 +358,7 @@ end
When declared inside a class, as above, the callback methods will receive the model object as a parameter. We can now use the callback class in the model:
```ruby
-class PictureFile < ActiveRecord::Base
+class PictureFile < ApplicationRecord
after_destroy PictureFileCallbacks.new
end
```
@@ -374,7 +378,7 @@ end
If the callback method is declared this way, it won't be necessary to instantiate a `PictureFileCallbacks` object.
```ruby
-class PictureFile < ActiveRecord::Base
+class PictureFile < ApplicationRecord
after_destroy PictureFileCallbacks
end
```
@@ -398,8 +402,8 @@ end
By using the `after_commit` callback we can account for this case.
```ruby
-class PictureFile < ActiveRecord::Base
- after_commit :delete_picture_file_from_disk, on: [:destroy]
+class PictureFile < ApplicationRecord
+ after_commit :delete_picture_file_from_disk, on: :destroy
def delete_picture_file_from_disk
if File.exist?(filepath)
@@ -409,7 +413,57 @@ class PictureFile < ActiveRecord::Base
end
```
-NOTE: the `:on` option specifies when a callback will be fired. If you
+NOTE: The `:on` option specifies when a callback will be fired. If you
don't supply the `:on` option the callback will fire for every action.
-WARNING. The `after_commit` and `after_rollback` callbacks are guaranteed to be called for all models created, updated, or destroyed within a transaction block. If any exceptions are raised within one of these callbacks, they will be ignored so that they don't interfere with the other callbacks. As such, if your callback code could raise an exception, you'll need to rescue it and handle it appropriately within the callback.
+Since using `after_commit` callback only on create, update, or delete is
+common, there are aliases for those operations:
+
+* `after_create_commit`
+* `after_update_commit`
+* `after_destroy_commit`
+
+```ruby
+class PictureFile < ApplicationRecord
+ after_destroy_commit :delete_picture_file_from_disk
+
+ def delete_picture_file_from_disk
+ if File.exist?(filepath)
+ File.delete(filepath)
+ end
+ end
+end
+```
+
+WARNING. When a transaction completes, the `after_commit` or `after_rollback` callbacks are called for all models created, updated, or destroyed within that transaction. However, if an exception is raised within one of these callbacks, the exception will bubble up and any remaining `after_commit` or `after_rollback` methods will _not_ be executed. As such, if your callback code could raise an exception, you'll need to rescue it and handle it within the callback in order to allow other callbacks to run.
+
+WARNING. The code executed within `after_commit` or `after_rollback` callbacks is itself not enclosed within a transaction.
+
+WARNING. Using both `after_create_commit` and `after_update_commit` in the same model will only allow the last callback defined to take effect, and will override all others.
+
+```ruby
+class User < ApplicationRecord
+ after_create_commit :log_user_saved_to_db
+ after_update_commit :log_user_saved_to_db
+
+ private
+ def log_user_saved_to_db
+ puts 'User was saved to database'
+ end
+end
+
+# prints nothing
+>> @user = User.create
+
+# updating @user
+>> @user.save
+=> User was saved to database
+```
+
+To register callbacks for both create and update actions, use `after_commit` instead.
+
+```ruby
+class User < ApplicationRecord
+ after_commit :log_user_saved_to_db, on: [:create, :update]
+end
+```
diff --git a/guides/source/active_record_migrations.md b/guides/source/active_record_migrations.md
index ce605c912e..4d195988f8 100644
--- a/guides/source/active_record_migrations.md
+++ b/guides/source/active_record_migrations.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Active Record Migrations
========================
@@ -12,7 +12,7 @@ After reading this guide, you will know:
* The generators you can use to create them.
* The methods Active Record provides to manipulate your database.
-* The Rake tasks that manipulate migrations and your schema.
+* The rails commands that manipulate migrations and your schema.
* How migrations relate to `schema.rb`.
--------------------------------------------------------------------------------
@@ -21,7 +21,7 @@ Migration Overview
------------------
Migrations are a convenient way to
-[alter your database schema over time](http://en.wikipedia.org/wiki/Schema_migration)
+[alter your database schema over time](https://en.wikipedia.org/wiki/Schema_migration)
in a consistent and easy way. They use a Ruby DSL so that you don't have to
write SQL by hand, allowing your schema and changes to be database independent.
@@ -35,13 +35,13 @@ history to the latest version. Active Record will also update your
Here's an example of a migration:
```ruby
-class CreateProducts < ActiveRecord::Migration
+class CreateProducts < ActiveRecord::Migration[5.0]
def change
create_table :products do |t|
t.string :name
t.text :description
- t.timestamps null: false
+ t.timestamps
end
end
end
@@ -72,7 +72,7 @@ If you wish for a migration to do something that Active Record doesn't know how
to reverse, you can use `reversible`:
```ruby
-class ChangeProductsPrice < ActiveRecord::Migration
+class ChangeProductsPrice < ActiveRecord::Migration[5.0]
def change
reversible do |dir|
change_table :products do |t|
@@ -87,7 +87,7 @@ end
Alternatively, you can use `up` and `down` instead of `change`:
```ruby
-class ChangeProductsPrice < ActiveRecord::Migration
+class ChangeProductsPrice < ActiveRecord::Migration[5.0]
def up
change_table :products do |t|
t.change :price, :string
@@ -123,13 +123,13 @@ Of course, calculating timestamps is no fun, so Active Record provides a
generator to handle making it for you:
```bash
-$ bin/rails generate migration AddPartNumberToProducts
+$ rails generate migration AddPartNumberToProducts
```
This will create an empty but appropriately named migration:
```ruby
-class AddPartNumberToProducts < ActiveRecord::Migration
+class AddPartNumberToProducts < ActiveRecord::Migration[5.0]
def change
end
end
@@ -140,13 +140,13 @@ followed by a list of column names and types then a migration containing the
appropriate `add_column` and `remove_column` statements will be created.
```bash
-$ bin/rails generate migration AddPartNumberToProducts part_number:string
+$ rails generate migration AddPartNumberToProducts part_number:string
```
will generate
```ruby
-class AddPartNumberToProducts < ActiveRecord::Migration
+class AddPartNumberToProducts < ActiveRecord::Migration[5.0]
def change
add_column :products, :part_number, :string
end
@@ -156,13 +156,13 @@ end
If you'd like to add an index on the new column, you can do that as well:
```bash
-$ bin/rails generate migration AddPartNumberToProducts part_number:string:index
+$ rails generate migration AddPartNumberToProducts part_number:string:index
```
will generate
```ruby
-class AddPartNumberToProducts < ActiveRecord::Migration
+class AddPartNumberToProducts < ActiveRecord::Migration[5.0]
def change
add_column :products, :part_number, :string
add_index :products, :part_number
@@ -174,13 +174,13 @@ end
Similarly, you can generate a migration to remove a column from the command line:
```bash
-$ bin/rails generate migration RemovePartNumberFromProducts part_number:string
+$ rails generate migration RemovePartNumberFromProducts part_number:string
```
generates
```ruby
-class RemovePartNumberFromProducts < ActiveRecord::Migration
+class RemovePartNumberFromProducts < ActiveRecord::Migration[5.0]
def change
remove_column :products, :part_number, :string
end
@@ -190,13 +190,13 @@ end
You are not limited to one magically generated column. For example:
```bash
-$ bin/rails generate migration AddDetailsToProducts part_number:string price:decimal
+$ rails generate migration AddDetailsToProducts part_number:string price:decimal
```
generates
```ruby
-class AddDetailsToProducts < ActiveRecord::Migration
+class AddDetailsToProducts < ActiveRecord::Migration[5.0]
def change
add_column :products, :part_number, :string
add_column :products, :price, :decimal
@@ -209,13 +209,13 @@ followed by a list of column names and types then a migration creating the table
XXX with the columns listed will be generated. For example:
```bash
-$ bin/rails generate migration CreateProducts name:string part_number:string
+$ rails generate migration CreateProducts name:string part_number:string
```
generates
```ruby
-class CreateProducts < ActiveRecord::Migration
+class CreateProducts < ActiveRecord::Migration[5.0]
def change
create_table :products do |t|
t.string :name
@@ -229,35 +229,36 @@ As always, what has been generated for you is just a starting point. You can add
or remove from it as you see fit by editing the
`db/migrate/YYYYMMDDHHMMSS_add_details_to_products.rb` file.
-Also, the generator accepts column type as `references`(also available as
+Also, the generator accepts column type as `references` (also available as
`belongs_to`). For instance:
```bash
-$ bin/rails generate migration AddUserRefToProducts user:references
+$ rails generate migration AddUserRefToProducts user:references
```
generates
```ruby
-class AddUserRefToProducts < ActiveRecord::Migration
+class AddUserRefToProducts < ActiveRecord::Migration[5.0]
def change
- add_reference :products, :user, index: true, foreign_key: true
+ add_reference :products, :user, foreign_key: true
end
end
```
This migration will create a `user_id` column and appropriate index.
+For more `add_reference` options, visit the [API documentation](http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_reference).
There is also a generator which will produce join tables if `JoinTable` is part of the name:
```bash
-$ bin/rails g migration CreateJoinTableCustomerProduct customer product
+$ rails g migration CreateJoinTableCustomerProduct customer product
```
will produce the following migration:
```ruby
-class CreateJoinTableCustomerProduct < ActiveRecord::Migration
+class CreateJoinTableCustomerProduct < ActiveRecord::Migration[5.0]
def change
create_join_table :customers, :products do |t|
# t.index [:customer_id, :product_id]
@@ -275,19 +276,19 @@ relevant table. If you tell Rails what columns you want, then statements for
adding these columns will also be created. For example, running:
```bash
-$ bin/rails generate model Product name:string description:text
+$ rails generate model Product name:string description:text
```
will create a migration that looks like this
```ruby
-class CreateProducts < ActiveRecord::Migration
+class CreateProducts < ActiveRecord::Migration[5.0]
def change
create_table :products do |t|
t.string :name
t.text :description
- t.timestamps null: false
+ t.timestamps
end
end
end
@@ -303,16 +304,16 @@ the command line. They are enclosed by curly braces and follow the field type:
For instance, running:
```bash
-$ bin/rails generate migration AddDetailsToProducts 'price:decimal{5,2}' supplier:references{polymorphic}
+$ rails generate migration AddDetailsToProducts 'price:decimal{5,2}' supplier:references{polymorphic}
```
will produce a migration that looks like this
```ruby
-class AddDetailsToProducts < ActiveRecord::Migration
+class AddDetailsToProducts < ActiveRecord::Migration[5.0]
def change
add_column :products, :price, :decimal, precision: 5, scale: 2
- add_reference :products, :supplier, polymorphic: true, index: true
+ add_reference :products, :supplier, polymorphic: true
end
end
```
@@ -352,13 +353,19 @@ create_table :products, options: "ENGINE=BLACKHOLE" do |t|
end
```
-will append `ENGINE=BLACKHOLE` to the SQL statement used to create the table
-(when using MySQL, the default is `ENGINE=InnoDB`).
+will append `ENGINE=BLACKHOLE` to the SQL statement used to create the table.
+
+Also you can pass the `:comment` option with any description for the table
+that will be stored in database itself and can be viewed with database administration
+tools, such as MySQL Workbench or PgAdmin III. It's highly recommended to specify
+comments in migrations for applications with large databases as it helps people
+to understand data model and generate documentation.
+Currently only the MySQL and PostgreSQL adapters support comments.
### Creating a Join Table
-Migration method `create_join_table` creates an HABTM join table. A typical use
-would be:
+The migration method `create_join_table` creates an HABTM (has and belongs to
+many) join table. A typical use would be:
```ruby
create_join_table :products, :categories
@@ -367,23 +374,21 @@ create_join_table :products, :categories
which creates a `categories_products` table with two columns called
`category_id` and `product_id`. These columns have the option `:null` set to
`false` by default. This can be overridden by specifying the `:column_options`
-option.
+option:
```ruby
-create_join_table :products, :categories, column_options: {null: true}
+create_join_table :products, :categories, column_options: { null: true }
```
-will create the `product_id` and `category_id` with the `:null` option as
-`true`.
-
-You can pass the option `:table_name` when you want to customize the table
-name. For example:
+By default, the name of the join table comes from the union of the first two
+arguments provided to create_join_table, in alphabetical order.
+To customize the name of the table, provide a `:table_name` option:
```ruby
create_join_table :products, :categories, table_name: :categorization
```
-will create a `categorization` table.
+creates a `categorization` table.
`create_join_table` also accepts a block, which you can use to add indices
(which are not created by default) or additional columns:
@@ -437,7 +442,7 @@ change_column_default :products, :approved, from: true, to: false
This sets `:name` field on products to a `NOT NULL` column and the default
value of the `:approved` field from true to false.
-Note: You could also write the above `change_column_default` migration as
+NOTE: You could also write the above `change_column_default` migration as
`change_column_default :products, :approved, false`, but unlike the previous
example, this would make your migration irreversible.
@@ -456,12 +461,13 @@ number of digits after the decimal point.
are using a dynamic value (such as a date), the default will only be calculated
the first time (i.e. on the date the migration is applied).
* `index` Adds an index for the column.
-* `required` Adds `required: true` for `belongs_to` associations and
-`null: false` to the column in the migration.
+* `comment` Adds a comment for the column.
Some adapters may support additional options; see the adapter specific API docs
for further information.
+NOTE: `null` and `default` cannot be specified via command line.
+
### Foreign Keys
While it's not required you might want to add foreign key constraints to
@@ -485,6 +491,9 @@ NOTE: Active Record only supports single column foreign keys. `execute` and
`structure.sql` are required to use composite foreign keys. See
[Schema Dumping and You](#schema-dumping-and-you).
+NOTE: The SQLite3 adapter doesn't support `add_foreign_key` since SQLite supports
+only [a limited subset of ALTER TABLE](https://www.sqlite.org/lang_altertable.html).
+
Removing a foreign key is easy as well:
```ruby
@@ -524,20 +533,27 @@ majority of cases, where Active Record knows how to reverse the migration
automatically. Currently, the `change` method supports only these migration
definitions:
-* `add_column`
-* `add_index`
-* `add_reference`
-* `add_timestamps`
-* `add_foreign_key`
-* `create_table`
-* `create_join_table`
-* `drop_table` (must supply a block)
-* `drop_join_table` (must supply a block)
-* `remove_timestamps`
-* `rename_column`
-* `rename_index`
-* `remove_reference`
-* `rename_table`
+* add_column
+* add_foreign_key
+* add_index
+* add_reference
+* add_timestamps
+* change_column_default (must supply a :from and :to option)
+* change_column_null
+* create_join_table
+* create_table
+* disable_extension
+* drop_join_table
+* drop_table (must supply a block)
+* enable_extension
+* remove_column (must supply a type)
+* remove_foreign_key (must supply a second table)
+* remove_index
+* remove_reference
+* remove_timestamps
+* rename_column
+* rename_index
+* rename_table
`change_table` is also reversible, as long as the block does not call `change`,
`change_default` or `remove`.
@@ -547,7 +563,7 @@ argument. Provide the original column options too, otherwise Rails can't
recreate the column exactly when rolling back:
```ruby
-remove_column :posts, :slug, :string, null: false, default: '', index: true
+remove_column :posts, :slug, :string, null: false, default: ''
```
If you're going to need to use any other methods, you should use `reversible`
@@ -560,7 +576,7 @@ to reverse. You can use `reversible` to specify what to do when running a
migration and what else to do when reverting it. For example:
```ruby
-class ExampleMigration < ActiveRecord::Migration
+class ExampleMigration < ActiveRecord::Migration[5.0]
def change
create_table :distributors do |t|
t.string :zipcode
@@ -613,7 +629,7 @@ is wise to perform the transformations in precisely the reverse order they were
made in the `up` method. The example in the `reversible` section is equivalent to:
```ruby
-class ExampleMigration < ActiveRecord::Migration
+class ExampleMigration < ActiveRecord::Migration[5.0]
def up
create_table :distributors do |t|
t.string :zipcode
@@ -654,9 +670,9 @@ can't be done.
You can use Active Record's ability to rollback migrations using the `revert` method:
```ruby
-require_relative '2012121212_example_migration'
+require_relative '20121212123456_example_migration'
-class FixupExampleMigration < ActiveRecord::Migration
+class FixupExampleMigration < ActiveRecord::Migration[5.0]
def change
revert ExampleMigration
@@ -674,7 +690,7 @@ is later decided it would be best to use Active Record validations,
in place of the `CHECK` constraint, to verify the zipcode.
```ruby
-class DontUseConstraintForZipcodeValidationMigration < ActiveRecord::Migration
+class DontUseConstraintForZipcodeValidationMigration < ActiveRecord::Migration[5.0]
def change
revert do
# copy-pasted code from ExampleMigration
@@ -714,15 +730,15 @@ you will have to use `structure.sql` as dump method. See
Running Migrations
------------------
-Rails provides a set of Rake tasks to run certain sets of migrations.
+Rails provides a set of rails commands to run certain sets of migrations.
-The very first migration related Rake task you will use will probably be
-`rake db:migrate`. In its most basic form it just runs the `change` or `up`
+The very first migration related rails command you will use will probably be
+`rails db:migrate`. In its most basic form it just runs the `change` or `up`
method for all the migrations that have not yet been run. If there are
no such migrations, it exits. It will run these migrations in order based
on the date of the migration.
-Note that running the `db:migrate` task also invokes the `db:schema:dump` task, which
+Note that running the `db:migrate` command also invokes the `db:schema:dump` command, which
will update your `db/schema.rb` file to match the structure of your database.
If you specify a target version, Active Record will run the required migrations
@@ -731,7 +747,7 @@ is the numerical prefix on the migration's filename. For example, to migrate
to version 20080906120000 run:
```bash
-$ bin/rake db:migrate VERSION=20080906120000
+$ rails db:migrate VERSION=20080906120000
```
If version 20080906120000 is greater than the current version (i.e., it is
@@ -748,7 +764,7 @@ mistake in it and wish to correct it. Rather than tracking down the version
number associated with the previous migration you can run:
```bash
-$ bin/rake db:rollback
+$ rails db:rollback
```
This will rollback the latest migration, either by reverting the `change`
@@ -756,63 +772,63 @@ method or by running the `down` method. If you need to undo
several migrations you can provide a `STEP` parameter:
```bash
-$ bin/rake db:rollback STEP=3
+$ rails db:rollback STEP=3
```
will revert the last 3 migrations.
-The `db:migrate:redo` task is a shortcut for doing a rollback and then migrating
-back up again. As with the `db:rollback` task, you can use the `STEP` parameter
+The `db:migrate:redo` command is a shortcut for doing a rollback and then migrating
+back up again. As with the `db:rollback` command, you can use the `STEP` parameter
if you need to go more than one version back, for example:
```bash
-$ bin/rake db:migrate:redo STEP=3
+$ rails db:migrate:redo STEP=3
```
-Neither of these Rake tasks do anything you could not do with `db:migrate`. They
+Neither of these rails commands do anything you could not do with `db:migrate`. They
are simply more convenient, since you do not need to explicitly specify the
version to migrate to.
### Setup the Database
-The `rake db:setup` task will create the database, load the schema and initialize
+The `rails db:setup` command will create the database, load the schema, and initialize
it with the seed data.
### Resetting the Database
-The `rake db:reset` task will drop the database and set it up again. This is
-functionally equivalent to `rake db:drop db:setup`.
+The `rails db:reset` command will drop the database and set it up again. This is
+functionally equivalent to `rails db:drop db:setup`.
NOTE: This is not the same as running all the migrations. It will only use the
-contents of the current `schema.rb` file. If a migration can't be rolled back,
-`rake db:reset` may not help you. To find out more about dumping the schema see
+contents of the current `db/schema.rb` or `db/structure.sql` file. If a migration can't be rolled back,
+`rails db:reset` may not help you. To find out more about dumping the schema see
[Schema Dumping and You](#schema-dumping-and-you) section.
### Running Specific Migrations
If you need to run a specific migration up or down, the `db:migrate:up` and
-`db:migrate:down` tasks will do that. Just specify the appropriate version and
+`db:migrate:down` commands will do that. Just specify the appropriate version and
the corresponding migration will have its `change`, `up` or `down` method
invoked, for example:
```bash
-$ bin/rake db:migrate:up VERSION=20080906120000
+$ rails db:migrate:up VERSION=20080906120000
```
will run the 20080906120000 migration by running the `change` method (or the
-`up` method). This task will
+`up` method). This command will
first check whether the migration is already performed and will do nothing if
Active Record believes that it has already been run.
### Running Migrations in Different Environments
-By default running `rake db:migrate` will run in the `development` environment.
+By default running `rails db:migrate` will run in the `development` environment.
To run migrations against another environment you can specify it using the
`RAILS_ENV` environment variable while running the command. For example to run
migrations against the `test` environment you could run:
```bash
-$ bin/rake db:migrate RAILS_ENV=test
+$ rails db:migrate RAILS_ENV=test
```
### Changing the Output of Running Migrations
@@ -838,13 +854,13 @@ Several methods are provided in migrations that allow you to control all this:
For example, this migration:
```ruby
-class CreateProducts < ActiveRecord::Migration
+class CreateProducts < ActiveRecord::Migration[5.0]
def change
suppress_messages do
create_table :products do |t|
t.string :name
t.text :description
- t.timestamps null: false
+ t.timestamps
end
end
@@ -873,18 +889,18 @@ generates the following output
== CreateProducts: migrated (10.0054s) =======================================
```
-If you want Active Record to not output anything, then running `rake db:migrate
+If you want Active Record to not output anything, then running `rails db:migrate
VERBOSE=false` will suppress all output.
Changing Existing Migrations
----------------------------
Occasionally you will make a mistake when writing a migration. If you have
-already run the migration then you cannot just edit the migration and run the
+already run the migration, then you cannot just edit the migration and run the
migration again: Rails thinks it has already run the migration and so will do
-nothing when you run `rake db:migrate`. You must rollback the migration (for
-example with `rake db:rollback`), edit your migration and then run
-`rake db:migrate` to run the corrected version.
+nothing when you run `rails db:migrate`. You must rollback the migration (for
+example with `rails db:rollback`), edit your migration, and then run
+`rails db:migrate` to run the corrected version.
In general, editing existing migrations is not a good idea. You will be
creating extra work for yourself and your co-workers and cause major headaches
@@ -904,35 +920,30 @@ Schema Dumping and You
### What are Schema Files for?
Migrations, mighty as they may be, are not the authoritative source for your
-database schema. That role falls to either `db/schema.rb` or an SQL file which
-Active Record generates by examining the database. They are not designed to be
-edited, they just represent the current state of the database.
-
-There is no need (and it is error prone) to deploy a new instance of an app by
-replaying the entire migration history. It is much simpler and faster to just
-load into the database a description of the current schema.
+database schema. Your database remains the authoritative source. By default,
+Rails generates `db/schema.rb` which attempts to capture the current state of
+your database schema.
-For example, this is how the test database is created: the current development
-database is dumped (either to `db/schema.rb` or `db/structure.sql`) and then
-loaded into the test database.
+It tends to be faster and less error prone to create a new instance of your
+application's database by loading the schema file via `rails db:schema:load`
+than it is to replay the entire migration history.
+[Old migrations](#old-migrations) may fail to apply correctly if those
+migrations use changing external dependencies or rely on application code which
+evolves separately from your migrations.
Schema files are also useful if you want a quick look at what attributes an
Active Record object has. This information is not in the model's code and is
frequently spread across several migrations, but the information is nicely
-summed up in the schema file. The
-[annotate_models](https://github.com/ctran/annotate_models) gem automatically
-adds and updates comments at the top of each model summarizing the schema if
-you desire that functionality.
+summed up in the schema file.
### Types of Schema Dumps
-There are two ways to dump the schema. This is set in `config/application.rb`
-by the `config.active_record.schema_format` setting, which may be either `:sql`
-or `:ruby`.
+The format of the schema dump generated by Rails is controlled by the
+`config.active_record.schema_format` setting in `config/application.rb`. By
+default, the format is `:ruby`, but can also be set to `:sql`.
-If `:ruby` is selected then the schema is stored in `db/schema.rb`. If you look
-at this file you'll find that it looks an awful lot like one very big
-migration:
+If `:ruby` is selected, then the schema is stored in `db/schema.rb`. If you look
+at this file you'll find that it looks an awful lot like one very big migration:
```ruby
ActiveRecord::Schema.define(version: 20080906171750) do
@@ -944,46 +955,42 @@ ActiveRecord::Schema.define(version: 20080906171750) do
create_table "products", force: true do |t|
t.string "name"
- t.text "description"
+ t.text "description"
t.datetime "created_at"
t.datetime "updated_at"
- t.string "part_number"
+ t.string "part_number"
end
end
```
In many ways this is exactly what it is. This file is created by inspecting the
database and expressing its structure using `create_table`, `add_index`, and so
-on. Because this is database-independent, it could be loaded into any database
-that Active Record supports. This could be very useful if you were to
-distribute an application that is able to run against multiple databases.
-
-There is however a trade-off: `db/schema.rb` cannot express database specific
-items such as triggers, stored procedures or check constraints. While in a
-migration you can execute custom SQL statements, the schema dumper cannot
-reconstitute those statements from the database. If you are using features like
-this, then you should set the schema format to `:sql`.
-
-Instead of using Active Record's schema dumper, the database's structure will
-be dumped using a tool specific to the database (via the `db:structure:dump`
-Rake task) into `db/structure.sql`. For example, for PostgreSQL, the `pg_dump`
-utility is used. For MySQL, this file will contain the output of
-`SHOW CREATE TABLE` for the various tables.
-
-Loading these schemas is simply a question of executing the SQL statements they
-contain. By definition, this will create a perfect copy of the database's
-structure. Using the `:sql` schema format will, however, prevent loading the
-schema into a RDBMS other than the one used to create it.
+on.
+
+`db/schema.rb` cannot express everything your database may support such as
+triggers, sequences, stored procedures, check constraints, etc. While migrations
+may use `execute` to create database constructs that are not supported by the
+Ruby migration DSL, these constructs may not be able to be reconstituted by the
+schema dumper. If you are using features like these, you should set the schema
+format to `:sql` in order to get an accurate schema file that is useful to
+create new database instances.
+
+When the schema format is set to `:sql`, the database structure will be dumped
+using a tool specific to the database into `db/structure.sql`. For example, for
+PostgreSQL, the `pg_dump` utility is used. For MySQL and MariaDB, this file will
+contain the output of `SHOW CREATE TABLE` for the various tables.
+
+To load the schema from `db/structure.sql`, run `rails db:structure:load`.
+Loading this file is done by executing the SQL statements it contains. By
+definition, this will create a perfect copy of the database's structure.
### Schema Dumps and Source Control
-Because schema dumps are the authoritative source for your database schema, it
-is strongly recommended that you check them into source control.
+Because schema files are commonly used to create new databases, it is strongly
+recommended that you check your schema file into source control.
-`db/schema.rb` contains the current version number of the database. This
-ensures conflicts are going to happen in the case of a merge where both
-branches touched the schema. When that happens, solve conflicts manually,
-keeping the highest version number of the two.
+Merge conflicts can occur in your schema file when two branches modify schema.
+To resolve these conflicts run `rails db:migrate` to regenerate the schema file.
Active Record and Referential Integrity
---------------------------------------
@@ -1006,10 +1013,13 @@ such features, the `execute` method can be used to execute arbitrary SQL.
Migrations and Seed Data
------------------------
-Some people use migrations to add data to the database:
+The main purpose of Rails' migration feature is to issue commands that modify the
+schema using a consistent process. Migrations can also be used
+to add or modify data. This is useful in an existing database that can't be destroyed
+and recreated, such as a production database.
```ruby
-class AddInitialProducts < ActiveRecord::Migration
+class AddInitialProducts < ActiveRecord::Migration[5.0]
def up
5.times do |i|
Product.create(name: "Product ##{i}", description: "A product.")
@@ -1022,9 +1032,11 @@ class AddInitialProducts < ActiveRecord::Migration
end
```
-However, Rails has a 'seeds' feature that should be used for seeding a database
-with initial data. It's a really simple feature: just fill up `db/seeds.rb`
-with some Ruby code, and run `rake db:seed`:
+To add initial data after a database is created, Rails has a built-in
+'seeds' feature that makes the process quick and easy. This is especially
+useful when reloading the database frequently in development and test environments.
+It's easy to get started with this feature: just fill up `db/seeds.rb` with some
+Ruby code, and run `rails db:seed`:
```ruby
5.times do |i|
@@ -1034,3 +1046,21 @@ end
This is generally a much cleaner way to set up the database of a blank
application.
+
+Old Migrations
+--------------
+
+The `db/schema.rb` or `db/structure.sql` is a snapshot of the current state of your
+database and is the authoritative source for rebuilding that database. This
+makes it possible to delete old migration files.
+
+When you delete migration files in the `db/migrate/` directory, any environment
+where `rails db:migrate` was run when those files still existed will hold a reference
+to the migration timestamp specific to them inside an internal Rails database
+table named `schema_migrations`. This table is used to keep track of whether
+migrations have been executed in a specific environment.
+
+If you run the `rails db:migrate:status` command, which displays the status
+(up or down) of each migration, you should see `********** NO FILE **********`
+displayed next to any deleted migration file which was once executed on a
+specific environment but can no longer be found in the `db/migrate/` directory.
diff --git a/guides/source/active_record_postgresql.md b/guides/source/active_record_postgresql.md
index fe112a4708..16c1567c69 100644
--- a/guides/source/active_record_postgresql.md
+++ b/guides/source/active_record_postgresql.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Active Record and PostgreSQL
============================
@@ -14,7 +14,7 @@ After reading this guide, you will know:
--------------------------------------------------------------------------------
-In order to use the PostgreSQL adapter you need to have at least version 8.2
+In order to use the PostgreSQL adapter you need to have at least version 9.1
installed. Older versions are not supported.
To get started with PostgreSQL have a look at the
@@ -29,8 +29,8 @@ that are supported by the PostgreSQL adapter.
### Bytea
-* [type definition](http://www.postgresql.org/docs/current/static/datatype-binary.html)
-* [functions and operators](http://www.postgresql.org/docs/current/static/functions-binarystring.html)
+* [type definition](https://www.postgresql.org/docs/current/static/datatype-binary.html)
+* [functions and operators](https://www.postgresql.org/docs/current/static/functions-binarystring.html)
```ruby
# db/migrate/20140207133952_create_documents.rb
@@ -39,7 +39,7 @@ create_table :documents do |t|
end
# app/models/document.rb
-class Document < ActiveRecord::Base
+class Document < ApplicationRecord
end
# Usage
@@ -49,8 +49,8 @@ Document.create payload: data
### Array
-* [type definition](http://www.postgresql.org/docs/current/static/arrays.html)
-* [functions and operators](http://www.postgresql.org/docs/current/static/functions-array.html)
+* [type definition](https://www.postgresql.org/docs/current/static/arrays.html)
+* [functions and operators](https://www.postgresql.org/docs/current/static/functions-array.html)
```ruby
# db/migrate/20140207133952_create_books.rb
@@ -63,7 +63,7 @@ add_index :books, :tags, using: 'gin'
add_index :books, :ratings, using: 'gin'
# app/models/book.rb
-class Book < ActiveRecord::Base
+class Book < ApplicationRecord
end
# Usage
@@ -83,9 +83,10 @@ Book.where("array_length(ratings, 1) >= 3")
### Hstore
-* [type definition](http://www.postgresql.org/docs/current/static/hstore.html)
+* [type definition](https://www.postgresql.org/docs/current/static/hstore.html)
+* [functions and operators](https://www.postgresql.org/docs/current/static/hstore.html#id-1.11.7.26.5)
-NOTE: you need to enable the `hstore` extension to use hstore.
+NOTE: You need to enable the `hstore` extension to use hstore.
```ruby
# db/migrate/20131009135255_create_profiles.rb
@@ -97,7 +98,7 @@ ActiveRecord::Schema.define do
end
# app/models/profile.rb
-class Profile < ActiveRecord::Base
+class Profile < ApplicationRecord
end
# Usage
@@ -108,21 +109,29 @@ profile.settings # => {"color"=>"blue", "resolution"=>"800x600"}
profile.settings = {"color" => "yellow", "resolution" => "1280x1024"}
profile.save!
+
+Profile.where("settings->'color' = ?", "yellow")
+# => #<ActiveRecord::Relation [#<Profile id: 1, settings: {"color"=>"yellow", "resolution"=>"1280x1024"}>]>
```
-### JSON
+### JSON and JSONB
-* [type definition](http://www.postgresql.org/docs/current/static/datatype-json.html)
-* [functions and operators](http://www.postgresql.org/docs/current/static/functions-json.html)
+* [type definition](https://www.postgresql.org/docs/current/static/datatype-json.html)
+* [functions and operators](https://www.postgresql.org/docs/current/static/functions-json.html)
```ruby
# db/migrate/20131220144913_create_events.rb
+# ... for json datatype:
create_table :events do |t|
t.json 'payload'
end
+# ... or for jsonb datatype:
+create_table :events do |t|
+ t.jsonb 'payload'
+end
# app/models/event.rb
-class Event < ActiveRecord::Base
+class Event < ApplicationRecord
end
# Usage
@@ -138,8 +147,8 @@ Event.where("payload->>'kind' = ?", "user_renamed")
### Range Types
-* [type definition](http://www.postgresql.org/docs/current/static/rangetypes.html)
-* [functions and operators](http://www.postgresql.org/docs/current/static/functions-range.html)
+* [type definition](https://www.postgresql.org/docs/current/static/rangetypes.html)
+* [functions and operators](https://www.postgresql.org/docs/current/static/functions-range.html)
This type is mapped to Ruby [`Range`](http://www.ruby-doc.org/core-2.2.2/Range.html) objects.
@@ -150,7 +159,7 @@ create_table :events do |t|
end
# app/models/event.rb
-class Event < ActiveRecord::Base
+class Event < ApplicationRecord
end
# Usage
@@ -173,7 +182,7 @@ event.ends_at # => Thu, 13 Feb 2014
### Composite Types
-* [type definition](http://www.postgresql.org/docs/current/static/rowtypes.html)
+* [type definition](https://www.postgresql.org/docs/current/static/rowtypes.html)
Currently there is no special support for composite types. They are mapped to
normal text columns:
@@ -200,7 +209,7 @@ create_table :contacts do |t|
end
# app/models/contact.rb
-class Contact < ActiveRecord::Base
+class Contact < ApplicationRecord
end
# Usage
@@ -213,22 +222,33 @@ contact.save!
### Enumerated Types
-* [type definition](http://www.postgresql.org/docs/current/static/datatype-enum.html)
+* [type definition](https://www.postgresql.org/docs/current/static/datatype-enum.html)
Currently there is no special support for enumerated types. They are mapped as
normal text columns:
```ruby
# db/migrate/20131220144913_create_articles.rb
-execute <<-SQL
- CREATE TYPE article_status AS ENUM ('draft', 'published');
-SQL
-create_table :articles do |t|
- t.column :status, :article_status
+def up
+ execute <<-SQL
+ CREATE TYPE article_status AS ENUM ('draft', 'published');
+ SQL
+ create_table :articles do |t|
+ t.column :status, :article_status
+ end
+end
+
+# NOTE: It's important to drop table before dropping enum.
+def down
+ drop_table :articles
+
+ execute <<-SQL
+ DROP TYPE article_status;
+ SQL
end
# app/models/article.rb
-class Article < ActiveRecord::Base
+class Article < ApplicationRecord
end
# Usage
@@ -240,23 +260,50 @@ article.status = "published"
article.save!
```
+To add a new value before/after existing one you should use [ALTER TYPE](https://www.postgresql.org/docs/current/static/sql-altertype.html):
+
+```ruby
+# db/migrate/20150720144913_add_new_state_to_articles.rb
+# NOTE: ALTER TYPE ... ADD VALUE cannot be executed inside of a transaction block so here we are using disable_ddl_transaction!
+disable_ddl_transaction!
+
+def up
+ execute <<-SQL
+ ALTER TYPE article_status ADD VALUE IF NOT EXISTS 'archived' AFTER 'published';
+ SQL
+end
+```
+
+NOTE: ENUM values can't be dropped currently. You can read why [here](https://www.postgresql.org/message-id/29F36C7C98AB09499B1A209D48EAA615B7653DBC8A@mail2a.alliedtesting.com).
+
+Hint: to show all the values of the all enums you have, you should call this query in `rails db` or `psql` console:
+
+```sql
+SELECT n.nspname AS enum_schema,
+ t.typname AS enum_name,
+ e.enumlabel AS enum_value
+ FROM pg_type t
+ JOIN pg_enum e ON t.oid = e.enumtypid
+ JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
+```
+
### UUID
-* [type definition](http://www.postgresql.org/docs/current/static/datatype-uuid.html)
-* [pgcrypto generator function](http://www.postgresql.org/docs/current/static/pgcrypto.html#AEN159361)
-* [uuid-ossp generator functions](http://www.postgresql.org/docs/current/static/uuid-ossp.html)
+* [type definition](https://www.postgresql.org/docs/current/static/datatype-uuid.html)
+* [pgcrypto generator function](https://www.postgresql.org/docs/current/static/pgcrypto.html#id-1.11.7.35.7)
+* [uuid-ossp generator functions](https://www.postgresql.org/docs/current/static/uuid-ossp.html)
-NOTE: you need to enable the `pgcrypto` (only PostgreSQL >= 9.4) or `uuid-ossp`
+NOTE: You need to enable the `pgcrypto` (only PostgreSQL >= 9.4) or `uuid-ossp`
extension to use uuid.
```ruby
# db/migrate/20131220144913_create_revisions.rb
create_table :revisions do |t|
- t.column :identifier, :uuid
+ t.uuid :identifier
end
# app/models/revision.rb
-class Revision < ActiveRecord::Base
+class Revision < ApplicationRecord
end
# Usage
@@ -279,12 +326,12 @@ create_table :comments, id: :uuid, default: 'gen_random_uuid()' do |t|
end
# app/models/post.rb
-class Post < ActiveRecord::Base
+class Post < ApplicationRecord
has_many :comments
end
# app/models/comment.rb
-class Comment < ActiveRecord::Base
+class Comment < ApplicationRecord
belongs_to :post
end
```
@@ -293,8 +340,8 @@ See [this section](#uuid-primary-keys) for more details on using UUIDs as primar
### Bit String Types
-* [type definition](http://www.postgresql.org/docs/current/static/datatype-bit.html)
-* [functions and operators](http://www.postgresql.org/docs/current/static/functions-bitstring.html)
+* [type definition](https://www.postgresql.org/docs/current/static/datatype-bit.html)
+* [functions and operators](https://www.postgresql.org/docs/current/static/functions-bitstring.html)
```ruby
# db/migrate/20131220144913_create_users.rb
@@ -302,8 +349,8 @@ create_table :users, force: true do |t|
t.column :settings, "bit(8)"
end
-# app/models/device.rb
-class User < ActiveRecord::Base
+# app/models/user.rb
+class User < ApplicationRecord
end
# Usage
@@ -317,7 +364,7 @@ user.save!
### Network Address Types
-* [type definition](http://www.postgresql.org/docs/current/static/datatype-net-types.html)
+* [type definition](https://www.postgresql.org/docs/current/static/datatype-net-types.html)
The types `inet` and `cidr` are mapped to Ruby
[`IPAddr`](http://www.ruby-doc.org/stdlib-2.2.2/libdoc/ipaddr/rdoc/IPAddr.html)
@@ -332,7 +379,7 @@ create_table(:devices, force: true) do |t|
end
# app/models/device.rb
-class Device < ActiveRecord::Base
+class Device < ApplicationRecord
end
# Usage
@@ -352,7 +399,7 @@ macbook.address
### Geometric Types
-* [type definition](http://www.postgresql.org/docs/current/static/datatype-geometric.html)
+* [type definition](https://www.postgresql.org/docs/current/static/datatype-geometric.html)
All geometric types, with the exception of `points` are mapped to normal text.
A point is casted to an array containing `x` and `y` coordinates.
@@ -361,7 +408,7 @@ A point is casted to an array containing `x` and `y` coordinates.
UUID Primary Keys
-----------------
-NOTE: you need to enable the `pgcrypto` (only PostgreSQL >= 9.4) or `uuid-ossp`
+NOTE: You need to enable the `pgcrypto` (only PostgreSQL >= 9.4) or `uuid-ossp`
extension to generate random UUIDs.
```ruby
@@ -372,7 +419,7 @@ create_table :devices, id: :uuid, default: 'gen_random_uuid()' do |t|
end
# app/models/device.rb
-class Device < ActiveRecord::Base
+class Device < ApplicationRecord
end
# Usage
@@ -380,7 +427,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
@@ -393,10 +440,10 @@ create_table :documents do |t|
t.string 'body'
end
-execute "CREATE INDEX documents_idx ON documents USING gin(to_tsvector('english', title || ' ' || body));"
+add_index :documents, "to_tsvector('english', title || ' ' || body)", using: :gin, name: 'documents_idx'
# app/models/document.rb
-class Document < ActiveRecord::Base
+class Document < ApplicationRecord
end
# Usage
@@ -410,7 +457,7 @@ Document.where("to_tsvector('english', title || ' ' || body) @@ to_tsquery(?)",
Database Views
--------------
-* [view creation](http://www.postgresql.org/docs/current/static/sql-createview.html)
+* [view creation](https://www.postgresql.org/docs/current/static/sql-createview.html)
Imagine you need to work with a legacy database containing the following table:
@@ -446,7 +493,7 @@ CREATE VIEW articles AS
SQL
# app/models/article.rb
-class Article < ActiveRecord::Base
+class Article < ApplicationRecord
self.primary_key = "id"
def archive!
update_attribute :archived, true
@@ -461,9 +508,9 @@ second = Article.create! title: "Brace yourself",
status: "draft",
published_at: 1.month.ago
-Article.count # => 1
-first.archive!
Article.count # => 2
+first.archive!
+Article.count # => 1
```
NOTE: This application only cares about non-archived `Articles`. A view also
diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md
index e3cfabb327..02055e59f0 100644
--- a/guides/source/active_record_querying.md
+++ b/guides/source/active_record_querying.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Active Record Query Interface
=============================
@@ -10,8 +10,8 @@ After reading this guide, you will know:
* How to find records using a variety of methods and conditions.
* How to specify the order, retrieved attributes, grouping, and other properties of the found records.
* How to use eager loading to reduce the number of database queries needed for data retrieval.
-* How to use dynamic finders methods.
-* How to use method chaining to use multiple ActiveRecord methods together.
+* How to use dynamic finder methods.
+* How to use method chaining to use multiple Active Record methods together.
* How to check for the existence of particular records.
* How to perform various calculations on Active Record models.
* How to run EXPLAIN on relations.
@@ -25,7 +25,7 @@ Code examples throughout this guide will refer to one or more of the following m
TIP: All of the following models use `id` as the primary key, unless specified otherwise.
```ruby
-class Client < ActiveRecord::Base
+class Client < ApplicationRecord
has_one :address
has_many :orders
has_and_belongs_to_many :roles
@@ -33,24 +33,24 @@ end
```
```ruby
-class Address < ActiveRecord::Base
+class Address < ApplicationRecord
belongs_to :client
end
```
```ruby
-class Order < ActiveRecord::Base
+class Order < ApplicationRecord
belongs_to :client, counter_cache: true
end
```
```ruby
-class Role < ActiveRecord::Base
+class Role < ApplicationRecord
has_and_belongs_to_many :clients
end
```
-Active Record will perform queries on the database for you and is compatible with most database systems (MySQL, PostgreSQL and SQLite to name a few). Regardless of which database system you're using, the Active Record method format will always be the same.
+Active Record will perform queries on the database for you and is compatible with most database systems, including MySQL, MariaDB, PostgreSQL, and SQLite. Regardless of which database system you're using, the Active Record method format will always be the same.
Retrieving Objects from the Database
------------------------------------
@@ -59,7 +59,7 @@ To retrieve objects from the database, Active Record provides several finder met
The methods are:
-* `bind`
+* `find`
* `create_with`
* `distinct`
* `eager_load`
@@ -69,6 +69,7 @@ The methods are:
* `having`
* `includes`
* `joins`
+* `left_outer_joins`
* `limit`
* `lock`
* `none`
@@ -80,10 +81,9 @@ The methods are:
* `reorder`
* `reverse_order`
* `select`
-* `uniq`
* `where`
-All of the above methods return an instance of `ActiveRecord::Relation`.
+Finder methods that return a collection, such as `where` and `group`, return an instance of `ActiveRecord::Relation`. Methods that find a single entity, such as `find` and `first`, return a single instance of the model.
The primary operation of `Model.find(options)` can be summarized as:
@@ -118,7 +118,7 @@ You can also use this method to query for multiple objects. Call the `find` meth
```ruby
# Find the clients with primary keys 1 and 10.
-client = Client.find([1, 10]) # Or even Client.find(1, 10)
+clients = Client.find([1, 10]) # Or even Client.find(1, 10)
# => [#<Client id: 1, first_name: "Lifo">, #<Client id: 10, first_name: "Ryan">]
```
@@ -150,11 +150,11 @@ The `take` method returns `nil` if no record is found and no exception will be r
You can pass in a numerical argument to the `take` method to return up to that number of results. For example
```ruby
-client = Client.take(2)
+clients = Client.take(2)
# => [
- #<Client id: 1, first_name: "Lifo">,
- #<Client id: 220, first_name: "Sara">
-]
+# #<Client id: 1, first_name: "Lifo">,
+# #<Client id: 220, first_name: "Sara">
+# ]
```
The SQL equivalent of the above is:
@@ -169,7 +169,7 @@ TIP: The retrieved record may vary depending on the database engine.
#### `first`
-The `first` method finds the first record ordered by the primary key. For example:
+The `first` method finds the first record ordered by primary key (default). For example:
```ruby
client = Client.first
@@ -184,15 +184,17 @@ SELECT * FROM clients ORDER BY clients.id ASC LIMIT 1
The `first` method returns `nil` if no matching record is found and no exception will be raised.
+If your [default scope](active_record_querying.html#applying-a-default-scope) contains an order method, `first` will return the first record according to this ordering.
+
You can pass in a numerical argument to the `first` method to return up to that number of results. For example
```ruby
-client = Client.first(3)
+clients = Client.first(3)
# => [
- #<Client id: 1, first_name: "Lifo">,
- #<Client id: 2, first_name: "Fifo">,
- #<Client id: 3, first_name: "Filo">
-]
+# #<Client id: 1, first_name: "Lifo">,
+# #<Client id: 2, first_name: "Fifo">,
+# #<Client id: 3, first_name: "Filo">
+# ]
```
The SQL equivalent of the above is:
@@ -201,11 +203,24 @@ The SQL equivalent of the above is:
SELECT * FROM clients ORDER BY clients.id ASC LIMIT 3
```
+On a collection that is ordered using `order`, `first` will return the first record ordered by the specified attribute for `order`.
+
+```ruby
+client = Client.order(:first_name).first
+# => #<Client id: 2, first_name: "Fifo">
+```
+
+The SQL equivalent of the above is:
+
+```sql
+SELECT * FROM clients ORDER BY clients.first_name ASC LIMIT 1
+```
+
The `first!` method behaves exactly like `first`, except that it will raise `ActiveRecord::RecordNotFound` if no matching record is found.
#### `last`
-The `last` method finds the last record ordered by the primary key. For example:
+The `last` method finds the last record ordered by primary key (default). For example:
```ruby
client = Client.last
@@ -220,15 +235,17 @@ SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1
The `last` method returns `nil` if no matching record is found and no exception will be raised.
+If your [default scope](active_record_querying.html#applying-a-default-scope) contains an order method, `last` will return the last record according to this ordering.
+
You can pass in a numerical argument to the `last` method to return up to that number of results. For example
```ruby
-client = Client.last(3)
+clients = Client.last(3)
# => [
- #<Client id: 219, first_name: "James">,
- #<Client id: 220, first_name: "Sara">,
- #<Client id: 221, first_name: "Russel">
-]
+# #<Client id: 219, first_name: "James">,
+# #<Client id: 220, first_name: "Sara">,
+# #<Client id: 221, first_name: "Russel">
+# ]
```
The SQL equivalent of the above is:
@@ -237,6 +254,19 @@ The SQL equivalent of the above is:
SELECT * FROM clients ORDER BY clients.id DESC LIMIT 3
```
+On a collection that is ordered using `order`, `last` will return the last record ordered by the specified attribute for `order`.
+
+```ruby
+client = Client.order(:first_name).last
+# => #<Client id: 220, first_name: "Sara">
+```
+
+The SQL equivalent of the above is:
+
+```sql
+SELECT * FROM clients ORDER BY clients.first_name DESC LIMIT 1
+```
+
The `last!` method behaves exactly like `last`, except that it will raise `ActiveRecord::RecordNotFound` if no matching record is found.
#### `find_by`
@@ -283,7 +313,7 @@ We often need to iterate over a large set of records, as when we send a newslett
This may appear straightforward:
```ruby
-# This is very inefficient when the users table has thousands of rows.
+# This may consume too much memory if the table is big.
User.all.each do |user|
NewsMailer.weekly(user).deliver_now
end
@@ -297,7 +327,7 @@ TIP: The `find_each` and `find_in_batches` methods are intended for use in the b
#### `find_each`
-The `find_each` method retrieves a batch of records and then yields _each_ record to the block individually as a model. In the following example, `find_each` will retrieve 1000 records (the current default for both `find_each` and `find_in_batches`) and then yield each record individually to the block as a model. This process is repeated until all of the records have been processed:
+The `find_each` method retrieves records in batches and then yields _each_ one to the block. In the following example, `find_each` retrieves users in batches of 1000 and yields them to the block one by one:
```ruby
User.find_each do |user|
@@ -305,7 +335,9 @@ User.find_each do |user|
end
```
-To add conditions to a `find_each` operation you can chain other Active Record methods such as `where`:
+This process is repeated, fetching more batches as needed, until all of the records have been processed.
+
+`find_each` works on model classes, as seen above, and also on relations:
```ruby
User.where(weekly_subscriber: true).find_each do |user|
@@ -313,11 +345,16 @@ User.where(weekly_subscriber: true).find_each do |user|
end
```
-##### Options for `find_each`
+as long as they have no ordering, since the method needs to force an order
+internally to iterate.
-The `find_each` method accepts most of the options allowed by the regular `find` method, except for `:order` and `:limit`, which are reserved for internal use by `find_each`.
+If an order is present in the receiver the behaviour depends on the flag
+`config.active_record.error_on_ignored_order`. If true, `ArgumentError` is
+raised, otherwise the order is ignored and a warning issued, which is the
+default. This can be overridden with the option `:error_on_ignore`, explained
+below.
-Three additional options, `:batch_size`, `:begin_at` and `:end_at`, are available as well.
+##### Options for `find_each`
**`:batch_size`**
@@ -329,47 +366,65 @@ User.find_each(batch_size: 5000) do |user|
end
```
-**`:begin_at`**
+**`:start`**
-By default, records are fetched in ascending order of the primary key, which must be an integer. The `:begin_at` option allows you to configure the first ID of the sequence whenever the lowest ID is not the one you need. This would be useful, for example, if you wanted to resume an interrupted batch process, provided you saved the last processed ID as a checkpoint.
+By default, records are fetched in ascending order of the primary key. The `:start` option allows you to configure the first ID of the sequence whenever the lowest ID is not the one you need. This would be useful, for example, if you wanted to resume an interrupted batch process, provided you saved the last processed ID as a checkpoint.
-For example, to send newsletters only to users with the primary key starting from 2000, and to retrieve them in batches of 5000:
+For example, to send newsletters only to users with the primary key starting from 2000:
```ruby
-User.find_each(begin_at: 2000, batch_size: 5000) do |user|
+User.find_each(start: 2000) do |user|
NewsMailer.weekly(user).deliver_now
end
```
-Another example would be if you wanted multiple workers handling the same processing queue. You could have each worker handle 10000 records by setting the appropriate `:begin_at` option on each worker.
+**`:finish`**
-**`:end_at`**
+Similar to the `:start` option, `:finish` allows you to configure the last ID of the sequence whenever the highest ID is not the one you need.
+This would be useful, for example, if you wanted to run a batch process using a subset of records based on `:start` and `:finish`.
-Similar to the `:begin_at` option, `:end_at` allows you to configure the last ID of the sequence whenever the highest ID is not the one you need.
-This would be useful, for example, if you wanted to run a batch process, using a subset of records based on `:begin_at` and `:end_at`
-
-For example, to send newsletters only to users with the primary key starting from 2000 up to 10000 and to retrieve them in batches of 5000:
+For example, to send newsletters only to users with the primary key starting from 2000 up to 10000:
```ruby
-User.find_each(begin_at: 2000, end_at: 10000, batch_size: 5000) do |user|
+User.find_each(start: 2000, finish: 10000) do |user|
NewsMailer.weekly(user).deliver_now
end
```
+Another example would be if you wanted multiple workers handling the same
+processing queue. You could have each worker handle 10000 records by setting the
+appropriate `:start` and `:finish` options on each worker.
+
+**`:error_on_ignore`**
+
+Overrides the application config to specify if an error should be raised when an
+order is present in the relation.
+
#### `find_in_batches`
The `find_in_batches` method is similar to `find_each`, since both retrieve batches of records. The difference is that `find_in_batches` yields _batches_ to the block as an array of models, instead of individually. The following example will yield to the supplied block an array of up to 1000 invoices at a time, with the final block containing any remaining invoices:
```ruby
-# Give add_invoices an array of 1000 invoices at a time
+# Give add_invoices an array of 1000 invoices at a time.
Invoice.find_in_batches do |invoices|
export.add_invoices(invoices)
end
```
+`find_in_batches` works on model classes, as seen above, and also on relations:
+
+```ruby
+Invoice.pending.find_in_batches do |invoices|
+ pending_invoices_export.add_invoices(invoices)
+end
+```
+
+as long as they have no ordering, since the method needs to force an order
+internally to iterate.
+
##### Options for `find_in_batches`
-The `find_in_batches` method accepts the same `:batch_size`, `:begin_at` and `:end_at` options as `find_each`.
+The `find_in_batches` method accepts the same options as `find_each`.
Conditions
----------
@@ -390,7 +445,7 @@ Now what if that number could vary, say as an argument from somewhere? The find
Client.where("orders_count = ?", params[:orders])
```
-Active Record will go through the first element in the conditions value and any additional elements will replace the question marks `(?)` in the first element.
+Active Record will take the first argument as the conditions string and any additional arguments will replace the question marks `(?)` in it.
If you want to specify multiple conditions:
@@ -418,7 +473,7 @@ TIP: For more information on the dangers of SQL injection, see the [Ruby on Rail
#### Placeholder Conditions
-Similar to the `(?)` replacement style of params, you can also specify keys/values hash in your array conditions:
+Similar to the `(?)` replacement style of params, you can also specify keys in your conditions string along with a corresponding keys/values hash:
```ruby
Client.where("created_at >= :start_date AND created_at <= :end_date",
@@ -429,9 +484,9 @@ This makes for clearer readability if you have a large number of variable condit
### Hash Conditions
-Active Record also allows you to pass in hash conditions which can increase the readability of your conditions syntax. With hash conditions, you pass in a hash with keys of the fields you want conditionalised and the values of how you want to conditionalise them:
+Active Record also allows you to pass in hash conditions which can increase the readability of your conditions syntax. With hash conditions, you pass in a hash with keys of the fields you want qualified and the values of how you want to qualify them:
-NOTE: Only equality, range and subset checking are possible with Hash conditions.
+NOTE: Only equality, range, and subset checking are possible with Hash conditions.
#### Equality Conditions
@@ -439,6 +494,12 @@ NOTE: Only equality, range and subset checking are possible with Hash conditions
Client.where(locked: true)
```
+This will generate SQL like this:
+
+```sql
+SELECT * FROM clients WHERE (clients.locked = 1)
+```
+
The field name can also be a string:
```ruby
@@ -452,8 +513,6 @@ Article.where(author: author)
Author.joins(:articles).where(articles: { author: author })
```
-NOTE: The values cannot be symbols. For example, you cannot do `Client.where(status: :active)`.
-
#### Range Conditions
```ruby
@@ -484,13 +543,30 @@ SELECT * FROM clients WHERE (clients.orders_count IN (1,3,5))
### NOT Conditions
-`NOT` SQL queries can be built by `where.not`.
+`NOT` SQL queries can be built by `where.not`:
+
+```ruby
+Client.where.not(locked: true)
+```
+
+In other words, this query can be generated by calling `where` with no argument, then immediately chain with `not` passing `where` conditions. This will generate SQL like this:
+
+```sql
+SELECT * FROM clients WHERE (clients.locked != 1)
+```
+
+### OR Conditions
+
+`OR` conditions between two relations can be built by calling `or` on the first
+relation, and passing the second one as an argument.
```ruby
-Article.where.not(author: author)
+Client.where(locked: true).or(Client.where(orders_count: [1,3,5]))
```
-In other words, this query can be generated by calling `where` with no argument, then immediately chain with `not` passing `where` conditions.
+```sql
+SELECT * FROM clients WHERE (clients.locked = 1 OR clients.orders_count IN (1,3,5))
+```
Ordering
--------
@@ -529,12 +605,13 @@ Client.order("orders_count ASC, created_at DESC")
Client.order("orders_count ASC", "created_at DESC")
```
-If you want to call `order` multiple times e.g. in different context, new order will append previous one:
+If you want to call `order` multiple times, subsequent orders will be appended to the first:
```ruby
Client.order("orders_count ASC").order("created_at DESC")
# SELECT * FROM clients ORDER BY orders_count ASC, created_at DESC
```
+WARNING: If you are using **MySQL 5.7.5** and above, then on selecting fields from a result set using methods like `select`, `pluck` and `ids`; the `order` method will raise an `ActiveRecord::StatementInvalid` exception unless the field(s) used in `order` clause are included in the select list. See the next section for selecting fields from the result set.
Selecting Specific Fields
-------------------------
@@ -617,9 +694,9 @@ SELECT * FROM clients LIMIT 5 OFFSET 30
Group
-----
-To apply a `GROUP BY` clause to the SQL fired by the finder, you can specify the `group` method on the find.
+To apply a `GROUP BY` clause to the SQL fired by the finder, you can use the `group` method.
-For example, if you want to find a collection of the dates orders were created on:
+For example, if you want to find a collection of the dates on which orders were created:
```ruby
Order.select("date(created_at) as ordered_date, sum(price) as total_price").group("date(created_at)")
@@ -637,7 +714,7 @@ GROUP BY date(created_at)
### Total of grouped items
-To get the total of grouped items on a single query call `count` after the `group`.
+To get the total of grouped items on a single query, call `count` after the `group`.
```ruby
Order.group(:status).count
@@ -673,7 +750,7 @@ GROUP BY date(created_at)
HAVING sum(price) > 100
```
-This will return single order objects for each day, but only those that are ordered more than $100 in a day.
+This returns the date and total price for each order object, grouped by the day they were ordered and where the price is more than $100.
Overriding Conditions
---------------------
@@ -703,8 +780,7 @@ Article.where(id: 10, trashed: false).unscope(where: :id)
# SELECT "articles".* FROM "articles" WHERE trashed = 0
```
-A relation which has used `unscope` will affect any relation it is
-merged in to:
+A relation which has used `unscope` will affect any relation into which it is merged:
```ruby
Article.order('id asc').merge(Article.unscope(:order))
@@ -725,7 +801,7 @@ The SQL that would be executed:
SELECT * FROM articles WHERE id > 10 ORDER BY id DESC
# Original query without `only`
-SELECT "articles".* FROM "articles" WHERE (id > 10) ORDER BY id desc LIMIT 20
+SELECT * FROM articles WHERE id > 10 ORDER BY id DESC LIMIT 20
```
@@ -734,7 +810,7 @@ SELECT "articles".* FROM "articles" WHERE (id > 10) ORDER BY id desc LIMIT 20
The `reorder` method overrides the default scope order. For example:
```ruby
-class Article < ActiveRecord::Base
+class Article < ApplicationRecord
has_many :comments, -> { order('posted_at DESC') }
end
@@ -744,14 +820,14 @@ Article.find(10).comments.reorder('name')
The SQL that would be executed:
```sql
-SELECT * FROM articles WHERE id = 10
+SELECT * FROM articles WHERE id = 10 LIMIT 1
SELECT * FROM comments WHERE article_id = 10 ORDER BY name
```
-In case the `reorder` clause is not used, the SQL executed would be:
+In the case where the `reorder` clause is not used, the SQL executed would be:
```sql
-SELECT * FROM articles WHERE id = 10
+SELECT * FROM articles WHERE id = 10 LIMIT 1
SELECT * FROM comments WHERE article_id = 10 ORDER BY posted_at DESC
```
@@ -837,7 +913,7 @@ end
Readonly Objects
----------------
-Active Record provides `readonly` method on a relation to explicitly disallow modification of any of the returned objects. Any attempt to alter a readonly record will not succeed, raising an `ActiveRecord::ReadOnlyRecord` exception.
+Active Record provides the `readonly` method on a relation to explicitly disallow modification of any of the returned objects. Any attempt to alter a readonly record will not succeed, raising an `ActiveRecord::ReadOnlyRecord` exception.
```ruby
client = Client.readonly.first
@@ -883,7 +959,7 @@ This behavior can be turned off by setting `ActiveRecord::Base.lock_optimistical
To override the name of the `lock_version` column, `ActiveRecord::Base` provides a class attribute called `locking_column`:
```ruby
-class Client < ActiveRecord::Base
+class Client < ApplicationRecord
self.locking_column = :lock_client_column
end
```
@@ -934,58 +1010,63 @@ end
Joining Tables
--------------
-Active Record provides a finder method called `joins` for specifying `JOIN` clauses on the resulting SQL. There are multiple ways to use the `joins` method.
+Active Record provides two finder methods for specifying `JOIN` clauses on the
+resulting SQL: `joins` and `left_outer_joins`.
+While `joins` should be used for `INNER JOIN` or custom queries,
+`left_outer_joins` is used for queries using `LEFT OUTER JOIN`.
-### Using a String SQL Fragment
+### `joins`
+
+There are multiple ways to use the `joins` method.
+
+#### Using a String SQL Fragment
You can just supply the raw SQL specifying the `JOIN` clause to `joins`:
```ruby
-Client.joins('LEFT OUTER JOIN addresses ON addresses.client_id = clients.id')
+Author.joins("INNER JOIN posts ON posts.author_id = authors.id AND posts.published = 't'")
```
This will result in the following SQL:
```sql
-SELECT clients.* FROM clients LEFT OUTER JOIN addresses ON addresses.client_id = clients.id
+SELECT authors.* FROM authors INNER JOIN posts ON posts.author_id = authors.id AND posts.published = 't'
```
-### Using Array/Hash of Named Associations
-
-WARNING: This method only works with `INNER JOIN`.
+#### Using Array/Hash of Named Associations
Active Record lets you use the names of the [associations](association_basics.html) defined on the model as a shortcut for specifying `JOIN` clauses for those associations when using the `joins` method.
For example, consider the following `Category`, `Article`, `Comment`, `Guest` and `Tag` models:
```ruby
-class Category < ActiveRecord::Base
+class Category < ApplicationRecord
has_many :articles
end
-class Article < ActiveRecord::Base
+class Article < ApplicationRecord
belongs_to :category
has_many :comments
has_many :tags
end
-class Comment < ActiveRecord::Base
+class Comment < ApplicationRecord
belongs_to :article
has_one :guest
end
-class Guest < ActiveRecord::Base
+class Guest < ApplicationRecord
belongs_to :comment
end
-class Tag < ActiveRecord::Base
+class Tag < ApplicationRecord
belongs_to :article
end
```
Now all of the following will produce the expected join queries using `INNER JOIN`:
-#### Joining a Single Association
+##### Joining a Single Association
```ruby
Category.joins(:articles)
@@ -998,7 +1079,7 @@ SELECT categories.* FROM categories
INNER JOIN articles ON articles.category_id = categories.id
```
-Or, in English: "return a Category object for all categories with articles". Note that you will see duplicate categories if more than one article has the same category. If you want unique categories, you can use `Category.joins(:articles).uniq`.
+Or, in English: "return a Category object for all categories with articles". Note that you will see duplicate categories if more than one article has the same category. If you want unique categories, you can use `Category.joins(:articles).distinct`.
#### Joining Multiple Associations
@@ -1010,13 +1091,13 @@ This produces:
```sql
SELECT articles.* FROM articles
- INNER JOIN categories ON articles.category_id = categories.id
+ INNER JOIN categories ON categories.id = articles.category_id
INNER JOIN comments ON comments.article_id = articles.id
```
Or, in English: "return all articles that have a category and at least one comment". Note again that articles with multiple comments will show up multiple times.
-#### Joining Nested Associations (Single Level)
+##### Joining Nested Associations (Single Level)
```ruby
Article.joins(comments: :guest)
@@ -1032,7 +1113,7 @@ SELECT articles.* FROM articles
Or, in English: "return all articles that have a comment made by a guest."
-#### Joining Nested Associations (Multiple Level)
+##### Joining Nested Associations (Multiple Level)
```ruby
Category.joins(articles: [{ comments: :guest }, :tags])
@@ -1048,9 +1129,11 @@ SELECT categories.* FROM categories
INNER JOIN tags ON tags.article_id = articles.id
```
-### Specifying Conditions on the Joined Tables
+Or, in English: "return all categories that have articles, where those articles have a comment made by a guest, and where those articles also have a tag."
+
+#### Specifying Conditions on the Joined Tables
-You can specify conditions on the joined tables using the regular [Array](#array-conditions) and [String](#pure-string-conditions) conditions. [Hash conditions](#hash-conditions) provides a special syntax for specifying conditions for the joined tables:
+You can specify conditions on the joined tables using the regular [Array](#array-conditions) and [String](#pure-string-conditions) conditions. [Hash conditions](#hash-conditions) provide a special syntax for specifying conditions for the joined tables:
```ruby
time_range = (Time.now.midnight - 1.day)..Time.now.midnight
@@ -1066,6 +1149,26 @@ Client.joins(:orders).where(orders: { created_at: time_range })
This will find all clients who have orders that were created yesterday, again using a `BETWEEN` SQL expression.
+### `left_outer_joins`
+
+If you want to select a set of records whether or not they have associated
+records you can use the `left_outer_joins` method.
+
+```ruby
+Author.left_outer_joins(:posts).distinct.select('authors.*, COUNT(posts.*) AS posts_count').group('authors.id')
+```
+
+Which produces:
+
+```sql
+SELECT DISTINCT authors.*, COUNT(posts.*) AS posts_count FROM "authors"
+LEFT OUTER JOIN posts ON posts.author_id = authors.id GROUP BY authors.id
+```
+
+Which means: "return all authors with their count of posts, whether or not they
+have any posts at all"
+
+
Eager Loading Associations
--------------------------
@@ -1089,7 +1192,7 @@ This code looks fine at the first sight. But the problem lies within the total n
Active Record lets you specify in advance all the associations that are going to be loaded. This is possible by specifying the `includes` method of the `Model.find` call. With `includes`, Active Record ensures that all of the specified associations are loaded using the minimum possible number of queries.
-Revisiting the above case, we could rewrite `Client.limit(10)` to use eager load addresses:
+Revisiting the above case, we could rewrite `Client.limit(10)` to eager load addresses:
```ruby
clients = Client.includes(:address).limit(10)
@@ -1158,7 +1261,8 @@ articles, all the articles would still be loaded. By using `joins` (an INNER
JOIN), the join conditions **must** match, otherwise no records will be
returned.
-
+NOTE: If an association is eager loaded as part of a join, any fields from a custom select clause will not be present on the loaded models.
+This is because it is ambiguous whether they should appear on the parent record, or the child.
Scopes
------
@@ -1168,25 +1272,15 @@ Scoping allows you to specify commonly-used queries which can be referenced as m
To define a simple scope, we use the `scope` method inside the class, passing the query that we'd like to run when this scope is called:
```ruby
-class Article < ActiveRecord::Base
+class Article < ApplicationRecord
scope :published, -> { where(published: true) }
end
```
-This is exactly the same as defining a class method, and which you use is a matter of personal preference:
-
-```ruby
-class Article < ActiveRecord::Base
- def self.published
- where(published: true)
- end
-end
-```
-
Scopes are also chainable within scopes:
```ruby
-class Article < ActiveRecord::Base
+class Article < ApplicationRecord
scope :published, -> { where(published: true) }
scope :published_and_commented, -> { published.where("comments_count > 0") }
end
@@ -1210,7 +1304,7 @@ category.articles.published # => [published articles belonging to this category]
Your scope can take arguments:
```ruby
-class Article < ActiveRecord::Base
+class Article < ApplicationRecord
scope :created_before, ->(time) { where("created_at < ?", time) }
end
```
@@ -1224,7 +1318,7 @@ Article.created_before(Time.zone.now)
However, this is just duplicating the functionality that would be provided to you by a class method.
```ruby
-class Article < ActiveRecord::Base
+class Article < ApplicationRecord
def self.created_before(time)
where("created_at < ?", time)
end
@@ -1237,13 +1331,35 @@ Using a class method is the preferred way to accept arguments for scopes. These
category.articles.created_before(time)
```
+### Using conditionals
+
+Your scope can utilize conditionals:
+
+```ruby
+class Article < ApplicationRecord
+ scope :created_before, ->(time) { where("created_at < ?", time) if time.present? }
+end
+```
+
+Like the other examples, this will behave similarly to a class method.
+
+```ruby
+class Article < ApplicationRecord
+ def self.created_before(time)
+ where("created_at < ?", time) if time.present?
+ end
+end
+```
+
+However, there is one important caveat: A scope will always return an `ActiveRecord::Relation` object, even if the conditional evaluates to `false`, whereas a class method, will return `nil`. This can cause `NoMethodError` when chaining class methods with conditionals, if any of the conditionals return `false`.
+
### Applying a default scope
If we wish for a scope to be applied across all queries to the model we can use the
`default_scope` method within the model itself.
```ruby
-class Client < ActiveRecord::Base
+class Client < ApplicationRecord
default_scope { where("removed_at IS NULL") }
end
```
@@ -1259,19 +1375,43 @@ If you need to do more complex things with a default scope, you can alternativel
define it as a class method:
```ruby
-class Client < ActiveRecord::Base
+class Client < ApplicationRecord
def self.default_scope
# Should return an ActiveRecord::Relation.
end
end
```
+NOTE: The `default_scope` is also applied while creating/building a record
+when the scope arguments are given as a `Hash`. It is not applied while
+updating a record. E.g.:
+
+```ruby
+class Client < ApplicationRecord
+ default_scope { where(active: true) }
+end
+
+Client.new # => #<Client id: nil, active: true>
+Client.unscoped.new # => #<Client id: nil, active: nil>
+```
+
+Be aware that, when given in the `Array` format, `default_scope` query arguments
+cannot be converted to a `Hash` for default attribute assignment. E.g.:
+
+```ruby
+class Client < ApplicationRecord
+ default_scope { where("active = ?", true) }
+end
+
+Client.new # => #<Client id: nil, active: nil>
+```
+
### Merging of scopes
Just like `where` clauses scopes are merged using `AND` conditions.
```ruby
-class User < ActiveRecord::Base
+class User < ApplicationRecord
scope :active, -> { where state: 'active' }
scope :inactive, -> { where state: 'inactive' }
end
@@ -1300,7 +1440,7 @@ One important caveat is that `default_scope` will be prepended in
`scope` and `where` conditions.
```ruby
-class User < ActiveRecord::Base
+class User < ApplicationRecord
default_scope { where state: 'pending' }
scope :active, -> { where state: 'active' }
scope :inactive, -> { where state: 'inactive' }
@@ -1331,8 +1471,15 @@ Client.unscoped.load
This method removes all scoping and will do a normal query on the table.
-Note that chaining `unscoped` with a `scope` does not work. In these cases, it is
-recommended that you use the block form of `unscoped`:
+```ruby
+Client.unscoped.all
+# SELECT "clients".* FROM "clients"
+
+Client.where(published: false).unscoped.all
+# SELECT "clients".* FROM "clients"
+```
+
+`unscoped` can also accept a block.
```ruby
Client.unscoped {
@@ -1349,10 +1496,40 @@ You can specify an exclamation point (`!`) on the end of the dynamic finders to
If you want to find both by name and locked, you can chain these finders together by simply typing "`and`" between the fields. For example, `Client.find_by_first_name_and_locked("Ryan", true)`.
+Enums
+-----
+
+The `enum` macro maps an integer column to a set of possible values.
+
+```ruby
+class Book < ApplicationRecord
+ enum availability: [:available, :unavailable]
+end
+```
+
+This will automatically create the corresponding [scopes](#scopes) to query the
+model. Methods to transition between states and query the current state are also
+added.
+
+```ruby
+# Both examples below query just available books.
+Book.available
+# or
+Book.where(availability: :available)
+
+book = Book.new(availability: :available)
+book.available? # => true
+book.unavailable! # => true
+book.available? # => false
+```
+
+Read the full documentation about enums
+[in the Rails API docs](http://api.rubyonrails.org/classes/ActiveRecord/Enum.html).
+
Understanding The Method Chaining
---------------------------------
-The Active Record pattern implements [Method Chaining](http://en.wikipedia.org/wiki/Method_chaining),
+The Active Record pattern implements [Method Chaining](https://en.wikipedia.org/wiki/Method_chaining),
which allow us to use multiple Active Record methods together in a simple and straightforward way.
You can chain methods in a statement when the previous method called returns an
@@ -1380,7 +1557,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
@@ -1414,7 +1591,7 @@ It's common that you need to find a record or create it if it doesn't exist. You
### `find_or_create_by`
-The `find_or_create_by` method checks whether a record with the attributes exists. If it doesn't, then `create` is called. Let's see an example.
+The `find_or_create_by` method checks whether a record with the specified attributes exists. If it doesn't, then `create` is called. Let's see an example.
Suppose you want to find a client named 'Andy', and if there's none, create one. You can do so by running:
@@ -1483,7 +1660,7 @@ now want the client named 'Nick':
```ruby
nick = Client.find_or_initialize_by(first_name: 'Nick')
-# => <Client id: nil, first_name: "Nick", orders_count: 0, locked: true, created_at: "2011-08-30 06:09:27", updated_at: "2011-08-30 06:09:27">
+# => #<Client id: nil, first_name: "Nick", orders_count: 0, locked: true, created_at: "2011-08-30 06:09:27", updated_at: "2011-08-30 06:09:27">
nick.persisted?
# => false
@@ -1515,24 +1692,24 @@ Client.find_by_sql("SELECT * FROM clients
INNER JOIN orders ON clients.id = orders.client_id
ORDER BY clients.created_at desc")
# => [
- #<Client id: 1, first_name: "Lucas" >,
- #<Client id: 2, first_name: "Jan" >,
- # ...
-]
+# #<Client id: 1, first_name: "Lucas" >,
+# #<Client id: 2, first_name: "Jan" >,
+# ...
+# ]
```
`find_by_sql` provides you with a simple way of making custom calls to the database and retrieving instantiated objects.
### `select_all`
-`find_by_sql` has a close relative called `connection#select_all`. `select_all` will retrieve objects from the database using custom SQL just like `find_by_sql` but will not instantiate them. Instead, you will get an array of hashes where each hash indicates a record.
+`find_by_sql` has a close relative called `connection#select_all`. `select_all` will retrieve objects from the database using custom SQL just like `find_by_sql` but will not instantiate them. This method will return an instance of `ActiveRecord::Result` class and calling `to_hash` on this object would return you an array of hashes where each hash indicates a record.
```ruby
-Client.connection.select_all("SELECT first_name, created_at FROM clients WHERE id = '1'")
+Client.connection.select_all("SELECT first_name, created_at FROM clients WHERE id = '1'").to_hash
# => [
- {"first_name"=>"Rafael", "created_at"=>"2012-11-10 23:23:45.281189"},
- {"first_name"=>"Eileen", "created_at"=>"2013-12-09 11:22:35.221282"}
-]
+# {"first_name"=>"Rafael", "created_at"=>"2012-11-10 23:23:45.281189"},
+# {"first_name"=>"Eileen", "created_at"=>"2013-12-09 11:22:35.221282"}
+# ]
```
### `pluck`
@@ -1577,7 +1754,7 @@ a large or often-running query. However, any model method overrides will
not be available. For example:
```ruby
-class Client < ActiveRecord::Base
+class Client < ApplicationRecord
def name
"I am #{super}"
end
@@ -1590,6 +1767,12 @@ Client.pluck(:name)
# => ["David", "Jeremy", "Jose"]
```
+You are not limited to querying fields from a single table, you can query multiple tables as well.
+
+```
+Client.joins(:comments, :categories).pluck("clients.email, comments.title, categories.name")
+```
+
Furthermore, unlike `select` and other `Relation` scopes, `pluck` triggers an immediate
query, and thus cannot be chained with any further scopes, although it can work with
scopes already constructed earlier:
@@ -1612,7 +1795,7 @@ Person.ids
```
```ruby
-class Person < ActiveRecord::Base
+class Person < ApplicationRecord
self.primary_key = "person_id"
end
@@ -1684,14 +1867,14 @@ All calculation methods work directly on a model:
```ruby
Client.count
-# SELECT count(*) AS count_all FROM clients
+# SELECT COUNT(*) FROM clients
```
Or on a relation:
```ruby
Client.where(first_name: 'Ryan').count
-# SELECT count(*) AS count_all FROM clients WHERE (first_name = 'Ryan')
+# SELECT COUNT(*) FROM clients WHERE (first_name = 'Ryan')
```
You can also use various finder methods on a relation for performing complex calculations:
@@ -1703,9 +1886,9 @@ Client.includes("orders").where(first_name: 'Ryan', orders: { status: 'received'
Which will execute:
```sql
-SELECT count(DISTINCT clients.id) AS count_all FROM clients
- LEFT OUTER JOIN orders ON orders.client_id = client.id WHERE
- (clients.first_name = 'Ryan' AND orders.status = 'received')
+SELECT COUNT(DISTINCT clients.id) FROM clients
+ LEFT OUTER JOIN orders ON orders.client_id = clients.id
+ WHERE (clients.first_name = 'Ryan' AND orders.status = 'received')
```
### Count
@@ -1785,7 +1968,7 @@ EXPLAIN for: SELECT `users`.* FROM `users` INNER JOIN `articles` ON `articles`.`
2 rows in set (0.00 sec)
```
-under MySQL.
+under MySQL and MariaDB.
Active Record performs a pretty printing that emulates that of the
corresponding database shell. So, the same query running with the
@@ -1845,7 +2028,7 @@ EXPLAIN for: SELECT `articles`.* FROM `articles` WHERE `articles`.`user_id` IN
1 row in set (0.00 sec)
```
-under MySQL.
+under MySQL and MariaDB.
### Interpreting EXPLAIN
@@ -1854,6 +2037,8 @@ following pointers may be helpful:
* SQLite3: [EXPLAIN QUERY PLAN](http://www.sqlite.org/eqp.html)
-* MySQL: [EXPLAIN Output Format](http://dev.mysql.com/doc/refman/5.6/en/explain-output.html)
+* MySQL: [EXPLAIN Output Format](http://dev.mysql.com/doc/refman/5.7/en/explain-output.html)
+
+* MariaDB: [EXPLAIN](https://mariadb.com/kb/en/mariadb/explain/)
-* PostgreSQL: [Using EXPLAIN](http://www.postgresql.org/docs/current/static/using-explain.html)
+* PostgreSQL: [Using EXPLAIN](https://www.postgresql.org/docs/current/static/using-explain.html)
diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md
index 71ca7a0f66..0fda7c5cfd 100644
--- a/guides/source/active_record_validations.md
+++ b/guides/source/active_record_validations.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Active Record Validations
=========================
@@ -20,7 +20,7 @@ Validations Overview
Here's an example of a very simple validation:
```ruby
-class Person < ActiveRecord::Base
+class Person < ApplicationRecord
validates :name, presence: true
end
@@ -80,14 +80,14 @@ method to determine whether an object is already in the database or not.
Consider the following simple Active Record class:
```ruby
-class Person < ActiveRecord::Base
+class Person < ApplicationRecord
end
```
We can see how it works by looking at some `rails console` output:
```ruby
-$ bin/rails console
+$ rails console
>> p = Person.new(name: "John Doe")
=> #<Person id: nil, name: "John Doe", created_at: nil, updated_at: nil>
>> p.new_record?
@@ -149,13 +149,15 @@ false` as an argument. This technique should be used with caution.
### `valid?` and `invalid?`
-To verify whether or not an object is valid, Rails uses the `valid?` method.
-You can also use this method on your own. `valid?` triggers your validations
+Before saving an Active Record object, Rails runs your validations.
+If these validations produce any errors, Rails does not save the object.
+
+You can also run these validations on your own. `valid?` triggers your validations
and returns true if no errors were found in the object, and false otherwise.
As you saw above:
```ruby
-class Person < ActiveRecord::Base
+class Person < ApplicationRecord
validates :name, presence: true
end
@@ -168,11 +170,12 @@ through the `errors.messages` instance method, which returns a collection of err
By definition, an object is valid if this collection is empty after running
validations.
-Note that an object instantiated with `new` will not report errors even if it's
-technically invalid, because validations are not run when using `new`.
+Note that an object instantiated with `new` will not report errors
+even if it's technically invalid, because validations are automatically run
+only when the object is saved, such as with the `create` or `save` methods.
```ruby
-class Person < ActiveRecord::Base
+class Person < ApplicationRecord
validates :name, presence: true
end
@@ -218,7 +221,7 @@ it doesn't verify the validity of the object as a whole. It only checks to see
whether there are errors found on an individual attribute of the object.
```ruby
-class Person < ActiveRecord::Base
+class Person < ApplicationRecord
validates :name, presence: true
end
@@ -236,13 +239,13 @@ To check which validations failed on an invalid attribute, you can use
key to get the symbol of the validator:
```ruby
-class Person < ActiveRecord::Base
+class Person < ApplicationRecord
validates :name, presence: true
end
>> person = Person.new
>> person.valid?
->> person.errors.details[:name] #=> [{error: :blank}]
+>> person.errors.details[:name] # => [{error: :blank}]
```
Using `details` with custom validators is covered in the [Working with
@@ -273,28 +276,41 @@ available helpers.
This method validates that a checkbox on the user interface was checked when a
form was submitted. This is typically used when the user needs to agree to your
application's terms of service, confirm that some text is read, or any similar
-concept. This validation is very specific to web applications and this
-'acceptance' does not need to be recorded anywhere in your database (if you
-don't have a field for it, the helper will just create a virtual attribute).
+concept.
```ruby
-class Person < ActiveRecord::Base
+class Person < ApplicationRecord
validates :terms_of_service, acceptance: true
end
```
This check is performed only if `terms_of_service` is not `nil`.
The default error message for this helper is _"must be accepted"_.
+You can also pass custom message via the `message` option.
+
+```ruby
+class Person < ApplicationRecord
+ validates :terms_of_service, acceptance: { message: 'must be abided' }
+end
+```
-It can receive an `:accept` option, which determines the value that will be
-considered acceptance. It defaults to "1" and can be easily changed.
+It can also receive an `:accept` option, which determines the allowed values
+that will be considered as accepted. It defaults to `['1', true]` and can be
+easily changed.
```ruby
-class Person < ActiveRecord::Base
+class Person < ApplicationRecord
validates :terms_of_service, acceptance: { accept: 'yes' }
+ validates :eula, acceptance: { accept: ['TRUE', 'accepted'] }
end
```
+This validation is very specific to web applications and this
+'acceptance' does not need to be recorded anywhere in your database. If you
+don't have a field for it, the helper will just create a virtual attribute. If
+the field does exist in your database, the `accept` option must be set to
+or include `true` or else the validation will not run.
+
### `validates_associated`
You should use this helper when your model has associations with other models
@@ -302,7 +318,7 @@ and they also need to be validated. When you try to save your object, `valid?`
will be called upon each one of the associated objects.
```ruby
-class Library < ActiveRecord::Base
+class Library < ApplicationRecord
has_many :books
validates_associated :books
end
@@ -325,7 +341,7 @@ or a password. This validation creates a virtual attribute whose name is the
name of the field that has to be confirmed with "_confirmation" appended.
```ruby
-class Person < ActiveRecord::Base
+class Person < ApplicationRecord
validates :email, confirmation: true
end
```
@@ -342,12 +358,22 @@ confirmation, make sure to add a presence check for the confirmation attribute
(we'll take a look at `presence` later on in this guide):
```ruby
-class Person < ActiveRecord::Base
+class Person < ApplicationRecord
validates :email, confirmation: true
validates :email_confirmation, presence: true
end
```
+There is also a `:case_sensitive` option that you can use to define whether the
+confirmation constraint will be case sensitive or not. This option defaults to
+true.
+
+```ruby
+class Person < ApplicationRecord
+ validates :email, confirmation: { case_sensitive: false }
+end
+```
+
The default error message for this helper is _"doesn't match confirmation"_.
### `exclusion`
@@ -356,7 +382,7 @@ This helper validates that the attributes' values are not included in a given
set. In fact, this set can be any enumerable object.
```ruby
-class Account < ActiveRecord::Base
+class Account < ApplicationRecord
validates :subdomain, exclusion: { in: %w(www us ca jp),
message: "%{value} is reserved." }
end
@@ -366,7 +392,8 @@ The `exclusion` helper has an option `:in` that receives the set of values that
will not be accepted for the validated attributes. The `:in` option has an
alias called `:within` that you can use for the same purpose, if you'd like to.
This example uses the `:message` option to show how you can include the
-attribute's value.
+attribute's value. For full options to the message argument please see the
+[message documentation](#message).
The default error message is _"is reserved"_.
@@ -376,7 +403,7 @@ This helper validates the attributes' values by testing whether they match a
given regular expression, which is specified using the `:with` option.
```ruby
-class Product < ActiveRecord::Base
+class Product < ApplicationRecord
validates :legacy_code, format: { with: /\A[a-zA-Z]+\z/,
message: "only allows letters" }
end
@@ -392,7 +419,7 @@ This helper validates that the attributes' values are included in a given set.
In fact, this set can be any enumerable object.
```ruby
-class Coffee < ActiveRecord::Base
+class Coffee < ApplicationRecord
validates :size, inclusion: { in: %w(small medium large),
message: "%{value} is not a valid size" }
end
@@ -401,7 +428,8 @@ end
The `inclusion` helper has an option `:in` that receives the set of values that
will be accepted. The `:in` option has an alias called `:within` that you can
use for the same purpose, if you'd like to. The previous example uses the
-`:message` option to show how you can include the attribute's value.
+`:message` option to show how you can include the attribute's value. For full
+options please see the [message documentation](#message).
The default error message for this helper is _"is not included in the list"_.
@@ -411,7 +439,7 @@ This helper validates the length of the attributes' values. It provides a
variety of options, so you can specify length constraints in different ways:
```ruby
-class Person < ActiveRecord::Base
+class Person < ApplicationRecord
validates :name, length: { minimum: 2 }
validates :bio, length: { maximum: 500 }
validates :password, length: { in: 6..20 }
@@ -434,27 +462,12 @@ number corresponding to the length constraint being used. You can still use the
`:message` option to specify an error message.
```ruby
-class Person < ActiveRecord::Base
+class Person < ApplicationRecord
validates :bio, length: { maximum: 1000,
too_long: "%{count} characters is the maximum allowed" }
end
```
-This helper counts characters by default, but you can split the value in a
-different way using the `:tokenizer` option:
-
-```ruby
-class Essay < ActiveRecord::Base
- validates :content, length: {
- minimum: 300,
- maximum: 400,
- tokenizer: lambda { |str| str.split(/\s+/) },
- too_short: "must have at least %{count} words",
- too_long: "must have at most %{count} words"
- }
-end
-```
-
Note that the default error messages are plural (e.g., "is too short (minimum
is %{count} characters)"). For this reason, when `:minimum` is 1 you should
provide a personalized message or use `presence: true` instead. When
@@ -477,11 +490,8 @@ 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 < ActiveRecord::Base
+class Player < ApplicationRecord
validates :points, numericality: true
validates :games_played, numericality: { only_integer: true }
end
@@ -503,6 +513,8 @@ constraints to acceptable values:
* `:less_than_or_equal_to` - Specifies the value must be less than or equal to
the supplied value. The default error message for this option is _"must be
less than or equal to %{count}"_.
+* `:other_than` - Specifies the value must be other than the supplied value.
+ The default error message for this option is _"must be other than %{count}"_.
* `:odd` - Specifies the value must be an odd number if set to true. The
default error message for this option is _"must be odd"_.
* `:even` - Specifies the value must be an even number if set to true. The
@@ -519,17 +531,18 @@ This helper validates that the specified attributes are not empty. It uses the
is, a string that is either empty or consists of whitespace.
```ruby
-class Person < ActiveRecord::Base
+class Person < ApplicationRecord
validates :name, :login, :email, presence: true
end
```
If you want to be sure that an association is present, you'll need to test
whether the associated object itself is present, and not the foreign key used
-to map the association.
+to map the association. This way, it is not only checked that the foreign key
+is not empty but also that the referenced object exists.
```ruby
-class LineItem < ActiveRecord::Base
+class LineItem < ApplicationRecord
belongs_to :order
validates :order, presence: true
end
@@ -539,7 +552,7 @@ In order to validate associated records whose presence is required, you must
specify the `:inverse_of` option for the association:
```ruby
-class Order < ActiveRecord::Base
+class Order < ApplicationRecord
has_many :line_items, inverse_of: :order
end
```
@@ -566,7 +579,7 @@ This helper validates that the specified attributes are absent. It uses the
is, a string that is either empty or consists of whitespace.
```ruby
-class Person < ActiveRecord::Base
+class Person < ApplicationRecord
validates :name, :login, :email, absence: true
end
```
@@ -576,7 +589,7 @@ whether the associated object itself is absent, and not the foreign key used
to map the association.
```ruby
-class LineItem < ActiveRecord::Base
+class LineItem < ApplicationRecord
belongs_to :order
validates :order, absence: true
end
@@ -586,7 +599,7 @@ In order to validate associated records whose absence is required, you must
specify the `:inverse_of` option for the association:
```ruby
-class Order < ActiveRecord::Base
+class Order < ApplicationRecord
has_many :line_items, inverse_of: :order
end
```
@@ -609,7 +622,7 @@ with the same value for a column that you intend to be unique. To avoid that,
you must create a unique index on that column in your database.
```ruby
-class Account < ActiveRecord::Base
+class Account < ApplicationRecord
validates :email, uniqueness: true
end
```
@@ -621,19 +634,19 @@ There is a `:scope` option that you can use to specify one or more attributes th
are used to limit the uniqueness check:
```ruby
-class Holiday < ActiveRecord::Base
+class Holiday < ApplicationRecord
validates :name, uniqueness: { scope: :year,
message: "should happen once per year" }
end
```
-Should you wish to create a database constraint to prevent possible violations of a uniqueness validation using the `:scope` option, you must create a unique index on both columns in your database. See [the MySQL manual](http://dev.mysql.com/doc/refman/5.6/en/multiple-column-indexes.html) for more details about multiple column indexes or [the PostgreSQL manual](http://www.postgresql.org/docs/current/static/ddl-constraints.html) for examples of unique constraints that refer to a group of columns.
+Should you wish to create a database constraint to prevent possible violations of a uniqueness validation using the `:scope` option, you must create a unique index on both columns in your database. See [the MySQL manual](http://dev.mysql.com/doc/refman/5.7/en/multiple-column-indexes.html) for more details about multiple column indexes or [the PostgreSQL manual](https://www.postgresql.org/docs/current/static/ddl-constraints.html) for examples of unique constraints that refer to a group of columns.
There is also a `:case_sensitive` option that you can use to define whether the
uniqueness constraint will be case sensitive or not. This option defaults to
true.
```ruby
-class Person < ActiveRecord::Base
+class Person < ApplicationRecord
validates :name, uniqueness: { case_sensitive: false }
end
```
@@ -656,7 +669,7 @@ class GoodnessValidator < ActiveModel::Validator
end
end
-class Person < ActiveRecord::Base
+class Person < ApplicationRecord
validates_with GoodnessValidator
end
```
@@ -684,7 +697,7 @@ class GoodnessValidator < ActiveModel::Validator
end
end
-class Person < ActiveRecord::Base
+class Person < ApplicationRecord
validates_with GoodnessValidator, fields: [:first_name, :last_name]
end
```
@@ -697,7 +710,7 @@ If your validator is complex enough that you want instance variables, you can
easily use a plain old Ruby object instead:
```ruby
-class Person < ActiveRecord::Base
+class Person < ApplicationRecord
validate do |person|
GoodnessValidator.new(person).validate
end
@@ -726,14 +739,14 @@ passed to `validates_each` will be tested against it. In the following example,
we don't want names and surnames to begin with lower case.
```ruby
-class Person < ActiveRecord::Base
+class Person < ApplicationRecord
validates_each :name, :surname do |record, attr, value|
record.errors.add(attr, 'must start with upper case') if value =~ /\A[[:lower:]]/
end
end
```
-The block receives the record, the attribute's name and the attribute's value.
+The block receives the record, the attribute's name, and the attribute's value.
You can do anything you like to check for valid data within the block. If your
validation fails, you should add an error message to the model, therefore
making it invalid.
@@ -749,12 +762,15 @@ The `:allow_nil` option skips the validation when the value being validated is
`nil`.
```ruby
-class Coffee < ActiveRecord::Base
+class Coffee < ApplicationRecord
validates :size, inclusion: { in: %w(small medium large),
message: "%{value} is not a valid size" }, allow_nil: true
end
```
+For full options to the message argument please see the
+[message documentation](#message).
+
### `:allow_blank`
The `:allow_blank` option is similar to the `:allow_nil` option. This option
@@ -762,7 +778,7 @@ will let validation pass if the attribute's value is `blank?`, like `nil` or an
empty string for example.
```ruby
-class Topic < ActiveRecord::Base
+class Topic < ApplicationRecord
validates :title, length: { is: 5 }, allow_blank: true
end
@@ -775,7 +791,37 @@ Topic.create(title: nil).valid? # => true
As you've already seen, the `:message` option lets you specify the message that
will be added to the `errors` collection when validation fails. When this
option is not used, Active Record will use the respective default error message
-for each validation helper.
+for each validation helper. The `:message` option accepts a `String` or `Proc`.
+
+A `String` `:message` value can optionally contain any/all of `%{value}`,
+`%{attribute}`, and `%{model}` which will be dynamically replaced when
+validation fails. This replacement is done using the I18n gem, and the
+placeholders must match exactly, no spaces are allowed.
+
+A `Proc` `:message` value is given two arguments: the object being validated, and
+a hash with `:model`, `:attribute`, and `:value` key-value pairs.
+
+```ruby
+class Person < ApplicationRecord
+ # Hard-coded message
+ validates :name, presence: { message: "must be given please" }
+
+ # Message with dynamic attribute value. %{value} will be replaced with
+ # the actual value of the attribute. %{attribute} and %{model} also
+ # available.
+ validates :age, numericality: { message: "%{value} seems wrong" }
+
+ # Proc
+ validates :username,
+ uniqueness: {
+ # object = person object being validated
+ # data = { model: "Person", attribute: "Username", value: <username> }
+ message: ->(object, data) do
+ "Hey #{object.name}!, #{data[:value]} is taken already! Try again #{Time.zone.tomorrow}"
+ end
+ }
+end
+```
### `:on`
@@ -787,7 +833,7 @@ new record is created or `on: :update` to run the validation only when a record
is updated.
```ruby
-class Person < ActiveRecord::Base
+class Person < ApplicationRecord
# it will be possible to update email with a duplicated value
validates :email, uniqueness: true, on: :create
@@ -799,6 +845,43 @@ class Person < ActiveRecord::Base
end
```
+You can also use `on:` to define custom contexts. Custom contexts need to be
+triggered explicitly by passing the name of the context to `valid?`,
+`invalid?`, or `save`.
+
+```ruby
+class Person < ApplicationRecord
+ validates :email, uniqueness: true, on: :account_setup
+ validates :age, numericality: true, on: :account_setup
+end
+
+person = Person.new(age: 'thirty-three')
+person.valid? # => true
+person.valid?(:account_setup) # => false
+person.errors.messages
+ # => {:email=>["has already been taken"], :age=>["is not a number"]}
+```
+
+`person.valid?(:account_setup)` executes both the validations without saving
+the model. `person.save(context: :account_setup)` validates `person` in the
+`account_setup` context before saving.
+
+When triggered by an explicit context, validations are run for that context,
+as well as any validations _without_ a context.
+
+```ruby
+class Person < ApplicationRecord
+ validates :email, uniqueness: true, on: :account_setup
+ validates :age, numericality: true, on: :account_setup
+ validates :name, presence: true
+end
+
+person = Person.new
+person.valid?(:account_setup) # => false
+person.errors.messages
+ # => {:email=>["has already been taken"], :age=>["is not a number"], :name=>["can't be blank"]}
+```
+
Strict Validations
------------------
@@ -806,7 +889,7 @@ You can also specify validations to be strict and raise
`ActiveModel::StrictValidationFailed` when the object is invalid.
```ruby
-class Person < ActiveRecord::Base
+class Person < ApplicationRecord
validates :name, presence: { strict: true }
end
@@ -816,7 +899,7 @@ Person.new.valid? # => ActiveModel::StrictValidationFailed: Name can't be blank
There is also the ability to pass a custom exception to the `:strict` option.
```ruby
-class Person < ActiveRecord::Base
+class Person < ApplicationRecord
validates :token, presence: true, uniqueness: true, strict: TokenGenerationException
end
@@ -828,7 +911,7 @@ Conditional Validation
Sometimes it will make sense to validate an object only when a given predicate
is satisfied. You can do that by using the `:if` and `:unless` options, which
-can take a symbol, a string, a `Proc` or an `Array`. You may use the `:if`
+can take a symbol, a `Proc` or an `Array`. You may use the `:if`
option when you want to specify when the validation **should** happen. If you
want to specify when the validation **should not** happen, then you may use the
`:unless` option.
@@ -840,7 +923,7 @@ to the name of a method that will get called right before validation happens.
This is the most commonly used option.
```ruby
-class Order < ActiveRecord::Base
+class Order < ApplicationRecord
validates :card_number, presence: true, if: :paid_with_card?
def paid_with_card?
@@ -849,18 +932,6 @@ class Order < ActiveRecord::Base
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 < ActiveRecord::Base
- 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
@@ -869,19 +940,26 @@ inline condition instead of a separate method. This option is best suited for
one-liners.
```ruby
-class Account < ActiveRecord::Base
+class Account < ApplicationRecord
validates :password, confirmation: true,
unless: Proc.new { |a| a.password.blank? }
end
```
+As `Lambdas` are a type of `Proc`, they can also be used to write inline
+conditions in a shorter way.
+
+```ruby
+validates :password, confirmation: true, unless: -> { password.blank? }
+```
+
### Grouping Conditional validations
Sometimes it is useful to have multiple validations use one condition. It can
be easily achieved using `with_options`.
```ruby
-class User < ActiveRecord::Base
+class User < ApplicationRecord
with_options if: :is_admin? do |admin|
admin.validates :password, length: { minimum: 10 }
admin.validates :email, presence: true
@@ -899,9 +977,9 @@ should happen, an `Array` can be used. Moreover, you can apply both `:if` and
`:unless` to the same validation.
```ruby
-class Computer < ActiveRecord::Base
+class Computer < ApplicationRecord
validates :mouse, presence: true,
- if: ["market.retail?", :desktop?],
+ if: [Proc.new { |c| c.market.retail? }, :desktop?],
unless: Proc.new { |c| c.trackpad.present? }
end
```
@@ -953,7 +1031,7 @@ class EmailValidator < ActiveModel::EachValidator
end
end
-class Person < ActiveRecord::Base
+class Person < ApplicationRecord
validates :email, presence: true, email: true
end
```
@@ -972,8 +1050,12 @@ class method, passing in the symbols for the validation methods' names.
You can pass more than one symbol for each class method and the respective
validations will be run in the same order as they were registered.
+The `valid?` method will verify that the errors collection is empty,
+so your custom validation methods should add errors to it when you
+wish validation to fail:
+
```ruby
-class Invoice < ActiveRecord::Base
+class Invoice < ApplicationRecord
validate :expiration_date_cannot_be_in_the_past,
:discount_cannot_be_greater_than_total_value
@@ -991,12 +1073,13 @@ class Invoice < ActiveRecord::Base
end
```
-By default such validations will run every time you call `valid?`. It is also
-possible to control when to run these custom validations by giving an `:on`
-option to the `validate` method, with either: `:create` or `:update`.
+By default, such validations will run every time you call `valid?`
+or save the object. But it is also possible to control when to run these
+custom validations by giving an `:on` option to the `validate` method,
+with either: `:create` or `:update`.
```ruby
-class Invoice < ActiveRecord::Base
+class Invoice < ApplicationRecord
validate :active_customer, on: :create
def active_customer
@@ -1017,7 +1100,7 @@ The following is a list of the most commonly used methods. Please refer to the `
Returns an instance of the class `ActiveModel::Errors` containing all errors. Each key is the attribute name and the value is an array of strings with all errors.
```ruby
-class Person < ActiveRecord::Base
+class Person < ApplicationRecord
validates :name, presence: true, length: { minimum: 3 }
end
@@ -1036,7 +1119,7 @@ person.errors.messages # => {}
`errors[]` is used when you want to check the error messages for a specific attribute. It returns an array of strings with all error messages for the given attribute, each string with one error message. If there are no errors related to the attribute, it returns an empty array.
```ruby
-class Person < ActiveRecord::Base
+class Person < ApplicationRecord
validates :name, presence: true, length: { minimum: 3 }
end
@@ -1061,7 +1144,7 @@ The `add` method lets you add an error message related to a particular attribute
The `errors.full_messages` method (or its equivalent, `errors.to_a`) returns the error messages in a user-friendly format, with the capitalized attribute name prepended to each message, as shown in the examples below.
```ruby
-class Person < ActiveRecord::Base
+class Person < ApplicationRecord
def a_method_used_for_validation_purposes
errors.add(:name, "cannot contain the characters !@#%*()_-+=")
end
@@ -1076,31 +1159,13 @@ person.errors.full_messages
# => ["Name cannot contain the characters !@#%*()_-+="]
```
-An equivalent to `errors#add` is to use `<<` to append a message to the `errors.messages` array for an attribute:
-
-```ruby
- class Person < ActiveRecord::Base
- def a_method_used_for_validation_purposes
- errors.messages[:name] << "cannot contain the characters !@#%*()_-+="
- end
- end
-
- person = Person.create(name: "!@#")
-
- person.errors[:name]
- # => ["cannot contain the characters !@#%*()_-+="]
-
- person.errors.to_a
- # => ["Name cannot contain the characters !@#%*()_-+="]
-```
-
### `errors.details`
You can specify a validator type to the returned error details hash using the
`errors.add` method.
```ruby
-class Person < ActiveRecord::Base
+class Person < ApplicationRecord
def a_method_used_for_validation_purposes
errors.add(:name, :invalid_characters)
end
@@ -1116,7 +1181,7 @@ To improve the error details to contain the unallowed characters set for instanc
you can pass additional keys to `errors.add`.
```ruby
-class Person < ActiveRecord::Base
+class Person < ApplicationRecord
def a_method_used_for_validation_purposes
errors.add(:name, :invalid_characters, not_allowed: "!@#%*()_-+=")
end
@@ -1136,7 +1201,7 @@ validator type.
You can add error messages that are related to the object's state as a whole, instead of being related to a specific attribute. You can use this method when you want to say that the object is invalid, no matter the values of its attributes. Since `errors[:base]` is an array, you can simply add a string to it and it will be used as an error message.
```ruby
-class Person < ActiveRecord::Base
+class Person < ApplicationRecord
def a_method_used_for_validation_purposes
errors[:base] << "This person is invalid because ..."
end
@@ -1148,7 +1213,7 @@ end
The `clear` method is used when you intentionally want to clear all the messages in the `errors` collection. Of course, calling `errors.clear` upon an invalid object won't actually make it valid: the `errors` collection will now be empty, but the next time you call `valid?` or any method that tries to save this object to the database, the validations will run again. If any of the validations fail, the `errors` collection will be filled again.
```ruby
-class Person < ActiveRecord::Base
+class Person < ApplicationRecord
validates :name, presence: true, length: { minimum: 3 }
end
@@ -1160,9 +1225,9 @@ person.errors[:name]
person.errors.clear
person.errors.empty? # => true
-p.save # => false
+person.save # => false
-p.errors[:name]
+person.errors[:name]
# => ["can't be blank", "is too short (minimum is 3 characters)"]
```
@@ -1171,7 +1236,7 @@ p.errors[:name]
The `size` method returns the total number of error messages for the object.
```ruby
-class Person < ActiveRecord::Base
+class Person < ApplicationRecord
validates :name, presence: true, length: { minimum: 3 }
end
diff --git a/guides/source/active_storage_overview.md b/guides/source/active_storage_overview.md
new file mode 100644
index 0000000000..d5387219f5
--- /dev/null
+++ b/guides/source/active_storage_overview.md
@@ -0,0 +1,765 @@
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
+
+Active Storage Overview
+=======================
+
+This guide covers how to attach files to your Active Record models.
+
+After reading this guide, you will know:
+
+* How to attach one or many files to a record.
+* How to delete an attached file.
+* How to link to an attached file.
+* How to use variants to transform images.
+* How to generate an image representation of a non-image file, such as a PDF or a video.
+* How to send file uploads directly from browsers to a storage service,
+ bypassing your application servers.
+* How to clean up files stored during testing.
+* How to implement support for additional storage services.
+
+--------------------------------------------------------------------------------
+
+What is Active Storage?
+-----------------------
+
+Active Storage facilitates uploading files to a cloud storage service like
+Amazon S3, Google Cloud Storage, or Microsoft Azure Storage and attaching those
+files to Active Record objects. It comes with a local disk-based service for
+development and testing and supports mirroring files to subordinate services for
+backups and migrations.
+
+Using Active Storage, an application can transform image uploads with
+[ImageMagick](https://www.imagemagick.org), generate image representations of
+non-image uploads like PDFs and videos, and extract metadata from arbitrary
+files.
+
+## Setup
+
+Active Storage uses two tables in your application’s database named
+`active_storage_blobs` and `active_storage_attachments`. After creating a new
+application (or upgrading your application to Rails 5.2), run
+`rails active_storage:install` to generate a migration that creates these
+tables. Use `rails db:migrate` to run the migration.
+
+Declare Active Storage services in `config/storage.yml`. For each service your
+application uses, provide a name and the requisite configuration. The example
+below declares three services named `local`, `test`, and `amazon`:
+
+```yaml
+local:
+ service: Disk
+ root: <%= Rails.root.join("storage") %>
+
+test:
+ service: Disk
+ root: <%= Rails.root.join("tmp/storage") %>
+
+amazon:
+ service: S3
+ access_key_id: ""
+ secret_access_key: ""
+ bucket: ""
+ region: "" # e.g. 'us-east-1'
+```
+
+Tell Active Storage which service to use by setting
+`Rails.application.config.active_storage.service`. Because each environment will
+likely use a different service, it is recommended to do this on a
+per-environment basis. To use the disk service from the previous example in the
+development environment, you would add the following to
+`config/environments/development.rb`:
+
+```ruby
+# Store files locally.
+config.active_storage.service = :local
+```
+
+To use the Amazon S3 service in production, you add the following to
+`config/environments/production.rb`:
+
+```ruby
+# Store files on Amazon S3.
+config.active_storage.service = :amazon
+```
+
+To use the test service when testing, you add the following to
+`config/environments/test.rb`:
+
+```ruby
+# Store uploaded files on the local file system in a temporary directory.
+config.active_storage.service = :test
+```
+
+Continue reading for more information on the built-in service adapters (e.g.
+`Disk` and `S3`) and the configuration they require.
+
+### Disk Service
+
+Declare a Disk service in `config/storage.yml`:
+
+```yaml
+local:
+ service: Disk
+ root: <%= Rails.root.join("storage") %>
+```
+
+### Amazon S3 Service
+
+Declare an S3 service in `config/storage.yml`:
+
+```yaml
+amazon:
+ service: S3
+ access_key_id: ""
+ secret_access_key: ""
+ region: ""
+ bucket: ""
+```
+
+Add the [`aws-sdk-s3`](https://github.com/aws/aws-sdk-ruby) gem to your `Gemfile`:
+
+```ruby
+gem "aws-sdk-s3", require: false
+```
+
+NOTE: The core features of Active Storage require the following permissions: `s3:ListBucket`, `s3:PutObject`, `s3:GetObject`, and `s3:DeleteObject`. If you have additional upload options configured such as setting ACLs then additional permissions may be required.
+
+NOTE: If you want to use environment variables, standard SDK configuration files, profiles,
+IAM instance profiles or task roles, you can omit the `access_key_id`, `secret_access_key`,
+and `region` keys in the example above. The Amazon S3 Service supports all of the
+authentication options described in the [AWS SDK documentation]
+(https://docs.aws.amazon.com/sdk-for-ruby/v3/developer-guide/setup-config.html).
+
+
+### Microsoft Azure Storage Service
+
+Declare an Azure Storage service in `config/storage.yml`:
+
+```yaml
+azure:
+ service: AzureStorage
+ storage_account_name: ""
+ storage_access_key: ""
+ container: ""
+```
+
+Add the [`azure-storage`](https://github.com/Azure/azure-storage-ruby) gem to your `Gemfile`:
+
+```ruby
+gem "azure-storage", require: false
+```
+
+### Google Cloud Storage Service
+
+Declare a Google Cloud Storage service in `config/storage.yml`:
+
+```yaml
+google:
+ service: GCS
+ credentials: <%= Rails.root.join("path/to/keyfile.json") %>
+ project: ""
+ bucket: ""
+```
+
+Optionally provide a Hash of credentials instead of a keyfile path:
+
+```yaml
+google:
+ service: GCS
+ credentials:
+ type: "service_account"
+ project_id: ""
+ private_key_id: <%= Rails.application.credentials.dig(:gcs, :private_key_id) %>
+ private_key: <%= Rails.application.credentials.dig(:gcs, :private_key).dump %>
+ client_email: ""
+ client_id: ""
+ auth_uri: "https://accounts.google.com/o/oauth2/auth"
+ token_uri: "https://accounts.google.com/o/oauth2/token"
+ auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs"
+ client_x509_cert_url: ""
+ project: ""
+ bucket: ""
+```
+
+Add the [`google-cloud-storage`](https://github.com/GoogleCloudPlatform/google-cloud-ruby/tree/master/google-cloud-storage) gem to your `Gemfile`:
+
+```ruby
+gem "google-cloud-storage", "~> 1.11", require: false
+```
+
+### Mirror Service
+
+You can keep multiple services in sync by defining a mirror service. When a file
+is uploaded or deleted, it's done across all the mirrored services. Mirrored
+services can be used to facilitate a migration between services in production.
+You can start mirroring to the new service, copy existing files from the old
+service to the new, then go all-in on the new service. Define each of the
+services you'd like to use as described above and reference them from a mirrored
+service.
+
+```yaml
+s3_west_coast:
+ service: S3
+ access_key_id: ""
+ secret_access_key: ""
+ region: ""
+ bucket: ""
+
+s3_east_coast:
+ service: S3
+ access_key_id: ""
+ secret_access_key: ""
+ region: ""
+ bucket: ""
+
+production:
+ service: Mirror
+ primary: s3_east_coast
+ mirrors:
+ - s3_west_coast
+```
+
+NOTE: Files are served from the primary service.
+
+NOTE: This is not compatible with the [direct uploads](#direct-uploads) feature.
+
+Attaching Files to Records
+--------------------------
+
+### `has_one_attached`
+
+The `has_one_attached` macro sets up a one-to-one mapping between records and
+files. Each record can have one file attached to it.
+
+For example, suppose your application has a `User` model. If you want each user to
+have an avatar, define the `User` model like this:
+
+```ruby
+class User < ApplicationRecord
+ has_one_attached :avatar
+end
+```
+
+You can create a user with an avatar:
+
+```erb
+<%= form.file_field :avatar %>
+```
+
+```ruby
+class SignupController < ApplicationController
+ def create
+ user = User.create!(user_params)
+ session[:user_id] = user.id
+ redirect_to root_path
+ end
+
+ private
+ def user_params
+ params.require(:user).permit(:email_address, :password, :avatar)
+ end
+end
+```
+
+Call `avatar.attach` to attach an avatar to an existing user:
+
+```ruby
+user.avatar.attach(params[:avatar])
+```
+
+Call `avatar.attached?` to determine whether a particular user has an avatar:
+
+```ruby
+user.avatar.attached?
+```
+
+### `has_many_attached`
+
+The `has_many_attached` macro sets up a one-to-many relationship between records
+and files. Each record can have many files attached to it.
+
+For example, suppose your application has a `Message` model. If you want each
+message to have many images, define the `Message` model like this:
+
+```ruby
+class Message < ApplicationRecord
+ has_many_attached :images
+end
+```
+
+You can create a message with images:
+
+```ruby
+class MessagesController < ApplicationController
+ def create
+ message = Message.create!(message_params)
+ redirect_to message
+ end
+
+ private
+ def message_params
+ params.require(:message).permit(:title, :content, images: [])
+ end
+end
+```
+
+Call `images.attach` to add new images to an existing message:
+
+```ruby
+@message.images.attach(params[:images])
+```
+
+Call `images.attached?` to determine whether a particular message has any images:
+
+```ruby
+@message.images.attached?
+```
+
+### Attaching File/IO Objects
+
+Sometimes you need to attach a file that doesn’t arrive via an HTTP request.
+For example, you may want to attach a file you generated on disk or downloaded
+from a user-submitted URL. You may also want to attach a fixture file in a
+model test. To do that, provide a Hash containing at least an open IO object
+and a filename:
+
+```ruby
+@message.image.attach(io: File.open('/path/to/file'), filename: 'file.pdf')
+```
+
+When possible, provide a content type as well. Active Storage attempts to
+determine a file’s content type from its data. It falls back to the content
+type you provide if it can’t do that.
+
+```ruby
+@message.image.attach(io: File.open('/path/to/file'), filename: 'file.pdf', content_type: 'application/pdf')
+```
+
+You can bypass the content type inference from the data by passing in
+`identify: false` along with the `content_type`.
+
+```ruby
+@message.image.attach(
+ io: File.open('/path/to/file'),
+ filename: 'file.pdf',
+ content_type: 'application/pdf',
+ identify: false
+)
+```
+
+If you don’t provide a content type and Active Storage can’t determine the
+file’s content type automatically, it defaults to application/octet-stream.
+
+
+Removing Files
+--------------
+
+To remove an attachment from a model, call `purge` on the attachment. Removal
+can be done in the background if your application is setup to use Active Job.
+Purging deletes the blob and the file from the storage service.
+
+```ruby
+# Synchronously destroy the avatar and actual resource files.
+user.avatar.purge
+
+# Destroy the associated models and actual resource files async, via Active Job.
+user.avatar.purge_later
+```
+
+Linking to Files
+----------------
+
+Generate a permanent URL for the blob that points to the application. Upon
+access, a redirect to the actual service endpoint is returned. This indirection
+decouples the public URL from the actual one, and allows, for example, mirroring
+attachments in different services for high-availability. The redirection has an
+HTTP expiration of 5 min.
+
+```ruby
+url_for(user.avatar)
+```
+
+To create a download link, use the `rails_blob_{path|url}` helper. Using this
+helper allows you to set the disposition.
+
+```ruby
+rails_blob_path(user.avatar, disposition: "attachment")
+```
+
+If you need to create a link from outside of controller/view context (Background
+jobs, Cronjobs, etc.), you can access the rails_blob_path like this:
+
+```
+Rails.application.routes.url_helpers.rails_blob_path(user.avatar, only_path: true)
+```
+
+Downloading Files
+-----------------
+
+Sometimes you need to process a blob after it’s uploaded—for example, to convert
+it to a different format. Use `ActiveStorage::Blob#download` to read a blob’s
+binary data into memory:
+
+```ruby
+binary = user.avatar.download
+```
+
+You might want to download a blob to a file on disk so an external program (e.g.
+a virus scanner or media transcoder) can operate on it. Use
+`ActiveStorage::Blob#open` to download a blob to a tempfile on disk:
+
+```ruby
+message.video.open do |file|
+ system '/path/to/virus/scanner', file.path
+ # ...
+end
+```
+
+Transforming Images
+-------------------
+
+To create a variation of the image, call `variant` on the `Blob`. You can pass
+any transformation to the method supported by the processor. The default
+processor is [MiniMagick](https://github.com/minimagick/minimagick), but you
+can also use [Vips](http://www.rubydoc.info/gems/ruby-vips/Vips/Image).
+
+To enable variants, add the `image_processing` gem to your `Gemfile`:
+
+```ruby
+gem 'image_processing', '~> 1.2'
+```
+
+When the browser hits the variant URL, Active Storage will lazily transform the
+original blob into the specified format and redirect to its new service
+location.
+
+```erb
+<%= image_tag user.avatar.variant(resize_to_fit: [100, 100]) %>
+```
+
+To switch to the Vips processor, you would add the following to
+`config/application.rb`:
+
+```ruby
+# Use Vips for processing variants.
+config.active_storage.variant_processor = :vips
+```
+
+Previewing Files
+----------------
+
+Some non-image files can be previewed: that is, they can be presented as images.
+For example, a video file can be previewed by extracting its first frame. Out of
+the box, Active Storage supports previewing videos and PDF documents.
+
+```erb
+<ul>
+ <% @message.files.each do |file| %>
+ <li>
+ <%= image_tag file.preview(resize_to_limit: [100, 100]) %>
+ </li>
+ <% end %>
+</ul>
+```
+
+WARNING: Extracting previews requires third-party applications, FFmpeg for
+video and muPDF for PDFs, and on macOS also XQuartz and Poppler.
+These libraries are not provided by Rails. You must install them yourself to
+use the built-in previewers. Before you install and use third-party software,
+make sure you understand the licensing implications of doing so.
+
+
+Direct Uploads
+--------------
+
+Active Storage, with its included JavaScript library, supports uploading
+directly from the client to the cloud.
+
+### Direct upload installation
+
+1. Include `activestorage.js` in your application's JavaScript bundle.
+
+ Using the asset pipeline:
+
+ ```js
+ //= require activestorage
+
+ ```
+
+ Using the npm package:
+
+ ```js
+ import * as ActiveStorage from "activestorage"
+ ActiveStorage.start()
+ ```
+
+2. Annotate file inputs with the direct upload URL.
+
+ ```erb
+ <%= form.file_field :attachments, multiple: true, direct_upload: true %>
+ ```
+3. That's it! Uploads begin upon form submission.
+
+### Direct upload JavaScript events
+
+| Event name | Event target | Event data (`event.detail`) | Description |
+| --- | --- | --- | --- |
+| `direct-uploads:start` | `<form>` | None | A form containing files for direct upload fields was submitted. |
+| `direct-upload:initialize` | `<input>` | `{id, file}` | Dispatched for every file after form submission. |
+| `direct-upload:start` | `<input>` | `{id, file}` | A direct upload is starting. |
+| `direct-upload:before-blob-request` | `<input>` | `{id, file, xhr}` | Before making a request to your application for direct upload metadata. |
+| `direct-upload:before-storage-request` | `<input>` | `{id, file, xhr}` | Before making a request to store a file. |
+| `direct-upload:progress` | `<input>` | `{id, file, progress}` | As requests to store files progress. |
+| `direct-upload:error` | `<input>` | `{id, file, error}` | An error occurred. An `alert` will display unless this event is canceled. |
+| `direct-upload:end` | `<input>` | `{id, file}` | A direct upload has ended. |
+| `direct-uploads:end` | `<form>` | None | All direct uploads have ended. |
+
+### Example
+
+You can use these events to show the progress of an upload.
+
+![direct-uploads](https://user-images.githubusercontent.com/5355/28694528-16e69d0c-72f8-11e7-91a7-c0b8cfc90391.gif)
+
+To show the uploaded files in a form:
+
+```js
+// direct_uploads.js
+
+addEventListener("direct-upload:initialize", event => {
+ const { target, detail } = event
+ const { id, file } = detail
+ target.insertAdjacentHTML("beforebegin", `
+ <div id="direct-upload-${id}" class="direct-upload direct-upload--pending">
+ <div id="direct-upload-progress-${id}" class="direct-upload__progress" style="width: 0%"></div>
+ <span class="direct-upload__filename">${file.name}</span>
+ </div>
+ `)
+})
+
+addEventListener("direct-upload:start", event => {
+ const { id } = event.detail
+ const element = document.getElementById(`direct-upload-${id}`)
+ element.classList.remove("direct-upload--pending")
+})
+
+addEventListener("direct-upload:progress", event => {
+ const { id, progress } = event.detail
+ const progressElement = document.getElementById(`direct-upload-progress-${id}`)
+ progressElement.style.width = `${progress}%`
+})
+
+addEventListener("direct-upload:error", event => {
+ event.preventDefault()
+ const { id, error } = event.detail
+ const element = document.getElementById(`direct-upload-${id}`)
+ element.classList.add("direct-upload--error")
+ element.setAttribute("title", error)
+})
+
+addEventListener("direct-upload:end", event => {
+ const { id } = event.detail
+ const element = document.getElementById(`direct-upload-${id}`)
+ element.classList.add("direct-upload--complete")
+})
+```
+
+Add styles:
+
+```css
+/* direct_uploads.css */
+
+.direct-upload {
+ display: inline-block;
+ position: relative;
+ padding: 2px 4px;
+ margin: 0 3px 3px 0;
+ border: 1px solid rgba(0, 0, 0, 0.3);
+ border-radius: 3px;
+ font-size: 11px;
+ line-height: 13px;
+}
+
+.direct-upload--pending {
+ opacity: 0.6;
+}
+
+.direct-upload__progress {
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ opacity: 0.2;
+ background: #0076ff;
+ transition: width 120ms ease-out, opacity 60ms 60ms ease-in;
+ transform: translate3d(0, 0, 0);
+}
+
+.direct-upload--complete .direct-upload__progress {
+ opacity: 0.4;
+}
+
+.direct-upload--error {
+ border-color: red;
+}
+
+input[type=file][data-direct-upload-url][disabled] {
+ display: none;
+}
+```
+
+### Integrating with Libraries or Frameworks
+
+If you want to use the Direct Upload feature from a JavaScript framework, or
+you want to integrate custom drag and drop solutions, you can use the
+`DirectUpload` class for this purpose. Upon receiving a file from your library
+of choice, instantiate a DirectUpload and call its create method. Create takes
+a callback to invoke when the upload completes.
+
+```js
+import { DirectUpload } from "activestorage"
+
+const input = document.querySelector('input[type=file]')
+
+// Bind to file drop - use the ondrop on a parent element or use a
+// library like Dropzone
+const onDrop = (event) => {
+ event.preventDefault()
+ const files = event.dataTransfer.files;
+ Array.from(files).forEach(file => uploadFile(file))
+}
+
+// Bind to normal file selection
+input.addEventListener('change', (event) => {
+ Array.from(input.files).forEach(file => uploadFile(file))
+ // you might clear the selected files from the input
+ input.value = null
+})
+
+const uploadFile = (file) {
+ // your form needs the file_field direct_upload: true, which
+ // provides data-direct-upload-url
+ const url = input.dataset.directUploadUrl
+ const upload = new DirectUpload(file, url)
+
+ upload.create((error, blob) => {
+ if (error) {
+ // Handle the error
+ } else {
+ // Add an appropriately-named hidden input to the form with a
+ // value of blob.signed_id so that the blob ids will be
+ // transmitted in the normal upload flow
+ const hiddenField = document.createElement('input')
+ hiddenField.setAttribute("type", "hidden");
+ hiddenField.setAttribute("value", blob.signed_id);
+ hiddenField.name = input.name
+ document.querySelector('form').appendChild(hiddenField)
+ }
+ })
+}
+```
+
+If you need to track the progress of the file upload, you can pass a third
+parameter to the `DirectUpload` constructor. During the upload, DirectUpload
+will call the object's `directUploadWillStoreFileWithXHR` method. You can then
+bind your own progress handler on the XHR.
+
+```js
+import { DirectUpload } from "activestorage"
+
+class Uploader {
+ constructor(file, url) {
+ this.upload = new DirectUpload(this.file, this.url, this)
+ }
+
+ upload(file) {
+ this.upload.create((error, blob) => {
+ if (error) {
+ // Handle the error
+ } else {
+ // Add an appropriately-named hidden input to the form
+ // with a value of blob.signed_id
+ }
+ })
+ }
+
+ directUploadWillStoreFileWithXHR(request) {
+ request.upload.addEventListener("progress",
+ event => this.directUploadDidProgress(event))
+ }
+
+ directUploadDidProgress(event) {
+ // Use event.loaded and event.total to update the progress bar
+ }
+}
+```
+
+Discarding Files Stored During System Tests
+-------------------------------------------
+
+System tests clean up test data by rolling back a transaction. Because destroy
+is never called on an object, the attached files are never cleaned up. If you
+want to clear the files, you can do it in an `after_teardown` callback. Doing it
+here ensures that all connections created during the test are complete and
+you won't receive an error from Active Storage saying it can't find a file.
+
+```ruby
+class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
+ driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
+
+ def remove_uploaded_files
+ FileUtils.rm_rf("#{Rails.root}/storage_test")
+ end
+
+ def after_teardown
+ super
+ remove_uploaded_files
+ end
+end
+```
+
+If your system tests verify the deletion of a model with attachments and you're
+using Active Job, set your test environment to use the inline queue adapter so
+the purge job is executed immediately rather at an unknown time in the future.
+
+You may also want to use a separate service definition for the test environment
+so your tests don't delete the files you create during development.
+
+```ruby
+# Use inline job processing to make things happen immediately
+config.active_job.queue_adapter = :inline
+
+# Separate file storage in the test environment
+config.active_storage.service = :local_test
+```
+
+Discarding Files Stored During Integration Tests
+-------------------------------------------
+
+Similarly to System Tests, files uploaded during Integration Tests will not be
+automatically cleaned up. If you want to clear the files, you can do it in an
+`after_teardown` callback. Doing it here ensures that all connections created
+during the test are complete and you won't receive an error from Active Storage
+saying it can't find a file.
+
+```ruby
+module ActionDispatch
+ class IntegrationTest
+ def remove_uploaded_files
+ FileUtils.rm_rf(Rails.root.join('tmp', 'storage'))
+ end
+
+ def after_teardown
+ super
+ remove_uploaded_files
+ end
+ end
+end
+```
+
+Implementing Support for Other Cloud Services
+---------------------------------------------
+
+If you need to support a cloud service other than these, you will need to
+implement the Service. Each service extends
+[`ActiveStorage::Service`](https://github.com/rails/rails/blob/master/activestorage/lib/active_storage/service.rb)
+by implementing the methods necessary to upload and download files to the cloud.
diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md
index 01bf928407..6b0554bb5f 100644
--- a/guides/source/active_support_core_extensions.md
+++ b/guides/source/active_support_core_extensions.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Active Support Core Extensions
==============================
@@ -135,44 +135,42 @@ 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:
+As of Ruby 2.5, most objects can be duplicated via `dup` or `clone`:
```ruby
-1.object_id # => 3
-Math.cos(0).to_i.object_id # => 3
+"foo".dup # => "foo"
+"".dup # => ""
+Rational(1).dup # => (1/1)
+Complex(0).dup # => (0+0i)
+1.method(:+).dup # => TypeError (allocator undefined for Method)
```
-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? # => true
+Complex(1).duplicable? # => true
+1.method(:+).duplicable? # => false
```
-Some numbers which are not singletons are not duplicable either:
+`duplicable?` matches the current Ruby version's `dup` behavior,
+so results will vary according the version of Ruby you're using.
+In Ruby 2.4, for example, Complex and Rational are not duplicable:
```ruby
-0.0.clone # => allocator undefined for Float
-(2**1024).clone # => allocator undefined for Bignum
+Rational(1).duplicable? # => false
+Complex(1).duplicable? # => false
```
-Active Support provides `duplicable?` to programmatically query an object about this property:
-
-```ruby
-"foo".duplicable? # => true
-"".duplicable? # => true
-0.0.duplicable? # => false
-false.duplicable? # => false
-```
-
-By definition all objects are `duplicable?` except `nil`, `false`, `true`, symbols, numbers, class, module, and method objects.
-
WARNING: Any class can disallow duplication by removing `dup` and `clone` or raising exceptions from them. Thus only `rescue` can tell whether a given arbitrary object is duplicable. `duplicable?` depends on the hard-coded list above, but it is much faster than `rescue`. Use it only if you know the hard-coded list is enough in your use case.
NOTE: Defined in `active_support/core_ext/object/duplicable.rb`.
### `deep_dup`
-The `deep_dup` method returns deep copy of a given object. Normally, when you `dup` an object that contains other objects, Ruby does not `dup` them, so it creates a shallow copy of the object. If you have an array with a string, for example, it will look like this:
+The `deep_dup` method returns a deep copy of a given object. Normally, when you `dup` an object that contains other objects, Ruby does not `dup` them, so it creates a shallow copy of the object. If you have an array with a string, for example, it will look like this:
```ruby
array = ['string']
@@ -248,6 +246,13 @@ end
@person.try { |p| "#{p.first_name} #{p.last_name}" }
```
+Note that `try` will swallow no-method errors, returning nil instead. If you want to protect against typos, use `try!` instead:
+
+```ruby
+@number.try(:nest) # => nil
+@number.try!(:nest) # NoMethodError: undefined method `nest' for 1:Integer
+```
+
NOTE: Defined in `active_support/core_ext/object/try.rb`.
### `class_eval(*args, &block)`
@@ -361,7 +366,7 @@ account.to_query('company[name]')
so its output is ready to be used in a query string.
-Arrays return the result of applying `to_query` to each element with `_key_[]` as key, and join the result with "&":
+Arrays return the result of applying `to_query` to each element with `key[]` as key, and join the result with "&":
```ruby
[3.4, -45.6].to_query('sample')
@@ -390,7 +395,7 @@ The method `with_options` provides a way to factor out common options in a serie
Given a default options hash, `with_options` yields a proxy object to a block. Within the block, methods called on the proxy are forwarded to the receiver with their options merged. For example, you get rid of the duplication in:
```ruby
-class Account < ActiveRecord::Base
+class Account < ApplicationRecord
has_many :customers, dependent: :destroy
has_many :products, dependent: :destroy
has_many :invoices, dependent: :destroy
@@ -401,7 +406,7 @@ end
this way:
```ruby
-class Account < ActiveRecord::Base
+class Account < ApplicationRecord
with_options dependent: :destroy do |assoc|
assoc.has_many :customers
assoc.has_many :products
@@ -453,7 +458,7 @@ NOTE: Defined in `active_support/core_ext/object/instance_variables.rb`.
#### `instance_variable_names`
-The method `instance_variable_names` returns an array. Each name includes the "@" sign.
+The method `instance_variable_names` returns an array. Each name includes the "@" sign.
```ruby
class C
@@ -504,56 +509,6 @@ NOTE: Defined in `active_support/core_ext/object/inclusion.rb`.
Extensions to `Module`
----------------------
-### `alias_method_chain`
-
-**This method is deprecated in favour of using Module#prepend.**
-
-Using plain Ruby you can wrap methods with other methods, that's called _alias chaining_.
-
-For example, let's say you'd like params to be strings in functional tests, as they are in real requests, but still want the convenience of assigning integers and other kind of values. To accomplish that you could wrap `ActionController::TestCase#process` this way in `test/test_helper.rb`:
-
-```ruby
-ActionController::TestCase.class_eval do
- # save a reference to the original process method
- alias_method :original_process, :process
-
- # now redefine process and delegate to original_process
- def process(action, params=nil, session=nil, flash=nil, http_method='GET')
- params = Hash[*params.map {|k, v| [k, v.to_s]}.flatten]
- original_process(action, params, session, flash, http_method)
- end
-end
-```
-
-That's the method `get`, `post`, etc., delegate the work to.
-
-That technique has a risk, it could be the case that `:original_process` was taken. To try to avoid collisions people choose some label that characterizes what the chaining is about:
-
-```ruby
-ActionController::TestCase.class_eval do
- def process_with_stringified_params(...)
- params = Hash[*params.map {|k, v| [k, v.to_s]}.flatten]
- process_without_stringified_params(action, params, session, flash, http_method)
- end
- alias_method :process_without_stringified_params, :process
- alias_method :process, :process_with_stringified_params
-end
-```
-
-The method `alias_method_chain` provides a shortcut for that pattern:
-
-```ruby
-ActionController::TestCase.class_eval do
- def process_with_stringified_params(...)
- params = Hash[*params.map {|k, v| [k, v.to_s]}.flatten]
- process_without_stringified_params(action, params, session, flash, http_method)
- end
- alias_method_chain :process, :stringified_params
-end
-```
-
-NOTE: Defined in `active_support/core_ext/module/aliasing.rb`.
-
### Attributes
#### `alias_attribute`
@@ -561,7 +516,7 @@ NOTE: Defined in `active_support/core_ext/module/aliasing.rb`.
Model attributes have a reader, a writer, and a predicate. You can alias a model attribute having the corresponding three methods defined for you in one shot. As in other aliasing methods, the new name is the first argument, and the old name is the second (one mnemonic is that they go in the same order as if you did an assignment):
```ruby
-class User < ActiveRecord::Base
+class User < ApplicationRecord
# You can refer to the email column as "login".
# This can be meaningful for authentication code.
alias_attribute :login, :email
@@ -625,8 +580,6 @@ module ActiveSupport
mattr_accessor :load_once_paths
mattr_accessor :autoloaded_constants
mattr_accessor :explicitly_unloadable_constants
- mattr_accessor :logger
- mattr_accessor :log_activity
mattr_accessor :constant_watch_stack
mattr_accessor :constant_watch_stack_mutex
end
@@ -637,9 +590,9 @@ NOTE: Defined in `active_support/core_ext/module/attribute_accessors.rb`.
### Parents
-#### `parent`
+#### `module_parent`
-The `parent` method on a nested named module returns the module that contains its corresponding constant:
+The `module_parent` method on a nested named module returns the module that contains its corresponding constant:
```ruby
module X
@@ -650,19 +603,19 @@ module X
end
M = X::Y::Z
-X::Y::Z.parent # => X::Y
-M.parent # => X::Y
+X::Y::Z.module_parent # => X::Y
+M.module_parent # => X::Y
```
-If the module is anonymous or belongs to the top-level, `parent` returns `Object`.
+If the module is anonymous or belongs to the top-level, `module_parent` returns `Object`.
-WARNING: Note that in that case `parent_name` returns `nil`.
+WARNING: Note that in that case `module_parent_name` returns `nil`.
NOTE: Defined in `active_support/core_ext/module/introspection.rb`.
-#### `parent_name`
+#### `module_parent_name`
-The `parent_name` method on a nested named module returns the fully-qualified name of the module that contains its corresponding constant:
+The `module_parent_name` method on a nested named module returns the fully qualified name of the module that contains its corresponding constant:
```ruby
module X
@@ -673,19 +626,19 @@ module X
end
M = X::Y::Z
-X::Y::Z.parent_name # => "X::Y"
-M.parent_name # => "X::Y"
+X::Y::Z.module_parent_name # => "X::Y"
+M.module_parent_name # => "X::Y"
```
-For top-level or anonymous modules `parent_name` returns `nil`.
+For top-level or anonymous modules `module_parent_name` returns `nil`.
-WARNING: Note that in that case `parent` returns `Object`.
+WARNING: Note that in that case `module_parent` returns `Object`.
NOTE: Defined in `active_support/core_ext/module/introspection.rb`.
-#### `parents`
+#### `module_parents`
-The method `parents` calls `parent` on the receiver and upwards until `Object` is reached. The chain is returned in an array, from bottom to top:
+The method `module_parents` calls `module_parent` on the receiver and upwards until `Object` is reached. The chain is returned in an array, from bottom to top:
```ruby
module X
@@ -696,131 +649,12 @@ module X
end
M = X::Y::Z
-X::Y::Z.parents # => [X::Y, X, Object]
-M.parents # => [X::Y, X, Object]
-```
-
-NOTE: Defined in `active_support/core_ext/module/introspection.rb`.
-
-### Constants
-
-The method `local_constants` returns the names of the constants that have been
-defined in the receiver module:
-
-```ruby
-module X
- X1 = 1
- X2 = 2
- module Y
- Y1 = :y1
- X1 = :overrides_X1_above
- end
-end
-
-X.local_constants # => [:X1, :X2, :Y]
-X::Y.local_constants # => [:Y1, :X1]
+X::Y::Z.module_parents # => [X::Y, X, Object]
+M.module_parents # => [X::Y, X, Object]
```
-The names are returned as symbols.
-
NOTE: Defined in `active_support/core_ext/module/introspection.rb`.
-#### Qualified Constant Names
-
-The standard methods `const_defined?`, `const_get`, and `const_set` accept
-bare constant names. Active Support extends this API to be able to pass
-relative qualified constant names.
-
-The new methods are `qualified_const_defined?`, `qualified_const_get`, and
-`qualified_const_set`. Their arguments are assumed to be qualified constant
-names relative to their receiver:
-
-```ruby
-Object.qualified_const_defined?("Math::PI") # => true
-Object.qualified_const_get("Math::PI") # => 3.141592653589793
-Object.qualified_const_set("Math::Phi", 1.618034) # => 1.618034
-```
-
-Arguments may be bare constant names:
-
-```ruby
-Math.qualified_const_get("E") # => 2.718281828459045
-```
-
-These methods are analogous to their built-in counterparts. In particular,
-`qualified_constant_defined?` accepts an optional second argument to be
-able to say whether you want the predicate to look in the ancestors.
-This flag is taken into account for each constant in the expression while
-walking down the path.
-
-For example, given
-
-```ruby
-module M
- X = 1
-end
-
-module N
- class C
- include M
- end
-end
-```
-
-`qualified_const_defined?` behaves this way:
-
-```ruby
-N.qualified_const_defined?("C::X", false) # => false
-N.qualified_const_defined?("C::X", true) # => true
-N.qualified_const_defined?("C::X") # => true
-```
-
-As the last example implies, the second argument defaults to true,
-as in `const_defined?`.
-
-For coherence with the built-in methods only relative paths are accepted.
-Absolute qualified constant names like `::Math::PI` raise `NameError`.
-
-NOTE: Defined in `active_support/core_ext/module/qualified_const.rb`.
-
-### Reachable
-
-A named module is reachable if it is stored in its corresponding constant. It means you can reach the module object via the constant.
-
-That is what ordinarily happens, if a module is called "M", the `M` constant exists and holds it:
-
-```ruby
-module M
-end
-
-M.reachable? # => true
-```
-
-But since constants and modules are indeed kind of decoupled, module objects can become unreachable:
-
-```ruby
-module M
-end
-
-orphan = Object.send(:remove_const, :M)
-
-# The module object is orphan now but it still has a name.
-orphan.name # => "M"
-
-# You cannot reach it via the constant M because it does not even exist.
-orphan.reachable? # => false
-
-# Let's define a module called "M" again.
-module M
-end
-
-# The constant M exists now again, and it stores a module
-# object called "M", but it is a new instance.
-orphan.reachable? # => false
-```
-
-NOTE: Defined in `active_support/core_ext/module/reachable.rb`.
-
### Anonymous
A module may or may not have a name:
@@ -854,7 +688,6 @@ end
m = Object.send(:remove_const, :M)
-m.reachable? # => false
m.anonymous? # => false
```
@@ -864,12 +697,14 @@ NOTE: Defined in `active_support/core_ext/module/anonymous.rb`.
### Method Delegation
+#### `delegate`
+
The macro `delegate` offers an easy way to forward methods.
Let's imagine that users in some application have login information in the `User` model but name and other data in a separate `Profile` model:
```ruby
-class User < ActiveRecord::Base
+class User < ApplicationRecord
has_one :profile
end
```
@@ -877,7 +712,7 @@ end
With that configuration you get a user's name via their profile, `user.profile.name`, but it could be handy to still be able to access such attribute directly:
```ruby
-class User < ActiveRecord::Base
+class User < ApplicationRecord
has_one :profile
def name
@@ -889,7 +724,7 @@ end
That is what `delegate` does for you:
```ruby
-class User < ActiveRecord::Base
+class User < ApplicationRecord
has_one :profile
delegate :name, to: :profile
@@ -944,15 +779,46 @@ delegate :size, to: :attachment, prefix: :avatar
In the previous example the macro generates `avatar_size` rather than `size`.
+The option `:private` changes methods scope:
+
+```ruby
+delegate :date_of_birth, to: :profile, private: true
+```
+
+The delegated methods are public by default. Pass `private: true` to change that.
+
NOTE: Defined in `active_support/core_ext/module/delegation.rb`
+#### `delegate_missing_to`
+
+Imagine you would like to delegate everything missing from the `User` object,
+to the `Profile` one. The `delegate_missing_to` macro lets you implement this
+in a breeze:
+
+```ruby
+class User < ApplicationRecord
+ has_one :profile
+
+ delegate_missing_to :profile
+end
+```
+
+The target can be anything callable within the object, e.g. instance variables,
+methods, constants, etc. Only the public methods of the target are delegated.
+
+NOTE: Defined in `active_support/core_ext/module/delegation.rb`.
+
### Redefining Methods
There are cases where you need to define a method with `define_method`, but don't know whether a method with that name already exists. If it does, a warning is issued if they are enabled. No big deal, but not clean either.
The method `redefine_method` prevents such a potential warning, removing the existing method before if needed.
-NOTE: Defined in `active_support/core_ext/module/remove_method.rb`
+You can also use `silence_redefinition_of_method` if you need to define
+the replacement method yourself (because you're using `delegate`, for
+example).
+
+NOTE: Defined in `active_support/core_ext/module/redefine_method.rb`.
Extensions to `Class`
---------------------
@@ -1015,8 +881,7 @@ The generation of the writer instance method can be prevented by setting the opt
```ruby
module ActiveRecord
class Base
- class_attribute :table_name_prefix, instance_writer: false
- self.table_name_prefix = ""
+ class_attribute :table_name_prefix, instance_writer: false, default: "my"
end
end
```
@@ -1030,7 +895,8 @@ class A
class_attribute :x, instance_reader: false
end
-A.new.x = 1 # NoMethodError
+A.new.x = 1
+A.new.x # NoMethodError
```
For convenience `class_attribute` also defines an instance predicate which is the double negation of what the instance reader returns. In the examples above it would be called `x?`.
@@ -1039,7 +905,7 @@ When `:instance_reader` is `false`, the instance predicate returns a `NoMethodEr
If you do not want the instance predicate, pass `instance_predicate: false` and it will not be defined.
-NOTE: Defined in `active_support/core_ext/class/attribute.rb`
+NOTE: Defined in `active_support/core_ext/class/attribute.rb`.
#### `cattr_reader`, `cattr_writer`, and `cattr_accessor`
@@ -1049,7 +915,15 @@ The macros `cattr_reader`, `cattr_writer`, and `cattr_accessor` are analogous to
class MysqlAdapter < AbstractAdapter
# Generates class methods to access @@emulate_booleans.
cattr_accessor :emulate_booleans
- self.emulate_booleans = true
+end
+```
+
+Also, you can pass a block to `cattr_*` to set up the attribute with a default value:
+
+```ruby
+class MysqlAdapter < AbstractAdapter
+ # Generates class methods to access @@emulate_booleans with default value of true.
+ cattr_accessor :emulate_booleans, default: true
end
```
@@ -1058,23 +932,13 @@ Instance methods are created as well for convenience, they are just proxies to t
```ruby
module ActionView
class Base
- cattr_accessor :field_error_proc
- @@field_error_proc = Proc.new{ ... }
+ cattr_accessor :field_error_proc, default: Proc.new { ... }
end
end
```
we can access `field_error_proc` in views.
-Also, you can pass a block to `cattr_*` to set up the attribute with a default value:
-
-```ruby
-class MysqlAdapter < AbstractAdapter
- # Generates class methods to access @@emulate_booleans with default value of true.
- cattr_accessor(:emulate_booleans) { true }
-end
-```
-
The generation of the reader instance method can be prevented by setting `:instance_reader` to `false` and the generation of the writer instance method can be prevented by setting `:instance_writer` to `false`. Generation of both methods can be prevented by setting `:instance_accessor` to `false`. In all cases, the value must be exactly `false` and not any false value.
```ruby
@@ -1679,19 +1543,6 @@ Given a string with a qualified constant reference expression, `deconstantize` r
"Admin::Hotel::ReservationUtils".deconstantize # => "Admin::Hotel"
```
-Active Support for example uses this method in `Module#qualified_const_set`:
-
-```ruby
-def qualified_const_set(path, value)
- QualifiedConstUtils.raise_if_absolute(path)
-
- const_name = path.demodulize
- mod_name = path.deconstantize
- mod = mod_name.empty? ? self : qualified_const_get(mod_name)
- mod.const_set(const_name, value)
-end
-```
-
NOTE: Defined in `active_support/core_ext/string/inflections.rb`.
#### `parameterize`
@@ -1703,6 +1554,20 @@ The method `parameterize` normalizes its receiver in a way that can be used in p
"Kurt Gödel".parameterize # => "kurt-godel"
```
+To preserve the case of the string, set the `preserve_case` argument to true. By default, `preserve_case` is set to false.
+
+```ruby
+"John Smith".parameterize(preserve_case: true) # => "John-Smith"
+"Kurt Gödel".parameterize(preserve_case: true) # => "Kurt-Godel"
+```
+
+To use a custom separator, override the `separator` argument.
+
+```ruby
+"John Smith".parameterize(separator: "_") # => "john\_smith"
+"Kurt Gödel".parameterize(separator: "_") # => "kurt\_godel"
+```
+
In fact, the result string is wrapped in an instance of `ActiveSupport::Multibyte::Chars`.
NOTE: Defined in `active_support/core_ext/string/inflections.rb`.
@@ -1746,7 +1611,7 @@ NOTE: Defined in `active_support/core_ext/string/inflections.rb`.
The method `constantize` resolves the constant reference expression in its receiver:
```ruby
-"Fixnum".constantize # => Fixnum
+"Integer".constantize # => Integer
module M
X = 1
@@ -1798,7 +1663,7 @@ Specifically performs these transformations:
* Capitalizes the first word.
The capitalization of the first word can be turned off by setting the
-+:capitalize+ option to false (default is true).
+`:capitalize` option to false (default is true).
```ruby
"name".humanize # => "Name"
@@ -1865,18 +1730,18 @@ The methods `to_date`, `to_time`, and `to_datetime` are basically convenience wr
```ruby
"2010-07-27".to_date # => Tue, 27 Jul 2010
-"2010-07-27 23:37:00".to_time # => Tue Jul 27 23:37:00 UTC 2010
+"2010-07-27 23:37:00".to_time # => 2010-07-27 23:37:00 +0200
"2010-07-27 23:37:00".to_datetime # => Tue, 27 Jul 2010 23:37:00 +0000
```
`to_time` receives an optional argument `:utc` or `:local`, to indicate which time zone you want the time in:
```ruby
-"2010-07-27 23:42:00".to_time(:utc) # => Tue Jul 27 23:42:00 UTC 2010
-"2010-07-27 23:42:00".to_time(:local) # => Tue Jul 27 23:42:00 +0200 2010
+"2010-07-27 23:42:00".to_time(:utc) # => 2010-07-27 23:42:00 UTC
+"2010-07-27 23:42:00".to_time(:local) # => 2010-07-27 23:42:00 +0200
```
-Default is `:utc`.
+Default is `:local`.
Please refer to the documentation of `Date._parse` for further details.
@@ -1920,7 +1785,7 @@ NOTE: Defined in `active_support/core_ext/numeric/bytes.rb`.
### Time
-Enables the use of time calculations and declarations, like `45.minutes + 2.hours + 4.years`.
+Enables the use of time calculations and declarations, like `45.minutes + 2.hours + 4.weeks`.
These methods use Time#advance for precise date calculations when using from_now, ago, etc.
as well as adding or subtracting their results from a Time object. For example:
@@ -1929,14 +1794,16 @@ as well as adding or subtracting their results from a Time object. For example:
# equivalent to Time.current.advance(months: 1)
1.month.from_now
-# equivalent to Time.current.advance(years: 2)
-2.years.from_now
+# equivalent to Time.current.advance(weeks: 2)
+2.weeks.from_now
-# equivalent to Time.current.advance(months: 4, years: 5)
-(4.months + 5.years).from_now
+# equivalent to Time.current.advance(months: 4, weeks: 5)
+(4.months + 5.weeks).from_now
```
-NOTE: Defined in `active_support/core_ext/numeric/time.rb`
+WARNING. For other durations please refer to the time extensions to `Integer`.
+
+NOTE: Defined in `active_support/core_ext/numeric/time.rb`.
### Formatting
@@ -2003,12 +1870,14 @@ Produce a string representation of a number rounded to a precision:
Produce a string representation of a number as a human-readable number of bytes:
```ruby
-123.to_s(:human_size) # => 123 Bytes
-1234.to_s(:human_size) # => 1.21 KB
-12345.to_s(:human_size) # => 12.1 KB
-1234567.to_s(:human_size) # => 1.18 MB
-1234567890.to_s(:human_size) # => 1.15 GB
-1234567890123.to_s(:human_size) # => 1.12 TB
+123.to_s(:human_size) # => 123 Bytes
+1234.to_s(:human_size) # => 1.21 KB
+12345.to_s(:human_size) # => 12.1 KB
+1234567.to_s(:human_size) # => 1.18 MB
+1234567890.to_s(:human_size) # => 1.15 GB
+1234567890123.to_s(:human_size) # => 1.12 TB
+1234567890123456.to_s(:human_size) # => 1.1 PB
+1234567890123456789.to_s(:human_size) # => 1.07 EB
```
Produce a string representation of a number in human-readable words:
@@ -2069,34 +1938,48 @@ The method `ordinalize` returns the ordinal string corresponding to the receiver
NOTE: Defined in `active_support/core_ext/integer/inflections.rb`.
-Extensions to `BigDecimal`
---------------------------
-### `to_s`
+### Time
-The method `to_s` is aliased to `to_formatted_s`. This provides a convenient way to display a BigDecimal value in floating-point notation:
+Enables the use of time calculations and declarations, like `4.months + 5.years`.
+
+These methods use Time#advance for precise date calculations when using from_now, ago, etc.
+as well as adding or subtracting their results from a Time object. For example:
```ruby
-BigDecimal.new(5.00, 6).to_s # => "5.0"
+# equivalent to Time.current.advance(months: 1)
+1.month.from_now
+
+# equivalent to Time.current.advance(years: 2)
+2.years.from_now
+
+# equivalent to Time.current.advance(months: 4, years: 5)
+(4.months + 5.years).from_now
```
-### `to_formatted_s`
+WARNING. For other durations please refer to the time extensions to `Numeric`.
-Te method `to_formatted_s` provides a default specifier of "F". This means that a simple call to `to_formatted_s` or `to_s` will result in floating point representation instead of engineering notation:
+NOTE: Defined in `active_support/core_ext/integer/time.rb`.
+
+Extensions to `BigDecimal`
+--------------------------
+### `to_s`
+
+The method `to_s` provides a default specifier of "F". This means that a simple call to `to_s` will result in floating point representation instead of engineering notation:
```ruby
-BigDecimal.new(5.00, 6).to_formatted_s # => "5.0"
+BigDecimal(5.00, 6).to_s # => "5.0"
```
and that symbol specifiers are also supported:
```ruby
-BigDecimal.new(5.00, 6).to_formatted_s(:db) # => "5.0"
+BigDecimal(5.00, 6).to_s(:db) # => "5.0"
```
Engineering notation is still supported:
```ruby
-BigDecimal.new(5.00, 6).to_formatted_s("e") # => "0.5E1"
+BigDecimal(5.00, 6).to_s("e") # => "0.5E1"
```
Extensions to `Enumerable`
@@ -2116,7 +1999,7 @@ Addition only assumes the elements respond to `+`:
```ruby
[[1, 2], [2, 3], [3, 4]].sum # => [1, 2, 2, 3, 3, 4]
%w(foo bar baz).sum # => "foobarbaz"
-{a: 1, b: 2, c: 3}.sum # => [:b, 2, :c, 3, :a, 1]
+{a: 1, b: 2, c: 3}.sum # => [:b, 2, :c, 3, :a, 1]
```
The sum of an empty collection is zero by default, but this is customizable:
@@ -2156,6 +2039,21 @@ WARNING. Keys should normally be unique. If the block returns the same value for
NOTE: Defined in `active_support/core_ext/enumerable.rb`.
+### `index_with`
+
+The method `index_with` generates a hash with the elements of an enumerable as keys. The value
+is either a passed default or returned in a block.
+
+```ruby
+%i( title body created_at ).index_with { |attr_name| post.public_send(attr_name) }
+# => { title: "hey", body: "what's up?", … }
+
+WEEKDAYS.index_with(Interval.all_day)
+# => { monday: [ 0, 1440 ], … }
+```
+
+NOTE: Defined in `active_support/core_ext/enumerable.rb`.
+
### `many?`
The method `many?` is shorthand for `collection.size > 1`:
@@ -2225,7 +2123,7 @@ Similarly, `from` returns the tail from the element at the passed index to the e
[].from(0) # => []
```
-The methods `second`, `third`, `fourth`, and `fifth` return the corresponding element (`first` is built-in). Thanks to social wisdom and positive constructiveness all around, `forty_two` is also available.
+The methods `second`, `third`, `fourth`, and `fifth` return the corresponding element, as do `second_to_last` and `third_to_last` (`first` and `last` are built-in). Thanks to social wisdom and positive constructiveness all around, `forty_two` is also available.
```ruby
%w(a b c d).third # => "c"
@@ -2258,6 +2156,19 @@ This method is an alias of `Array#<<`.
NOTE: Defined in `active_support/core_ext/array/prepend_and_append.rb`.
+### Extracting
+
+The method `extract!` removes and returns the elements for which the block returns a true value.
+If no block is given, an Enumerator is returned instead.
+
+```ruby
+numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
+odd_numbers = numbers.extract! { |number| number.odd? } # => [1, 3, 5, 7, 9]
+numbers # => [0, 2, 4, 6, 8]
+```
+
+NOTE: Defined in `active_support/core_ext/array/extract.rb`.
+
### Options Extraction
When the last argument in a method call is a hash, except perhaps for a `&block` argument, Ruby allows you to omit the brackets:
@@ -2359,7 +2270,7 @@ Contributor.limit(2).order(:rank).to_xml
To do so it sends `to_xml` to every item in turn, and collects the results under a root node. All items must respond to `to_xml`, an exception is raised otherwise.
-By default, the name of the root element is the underscorized and dasherized plural of the name of the class of the first item, provided the rest of elements belong to that type (checked with `is_a?`) and they are not hashes. In the example above that's "contributors".
+By default, the name of the root element is the underscored and dasherized plural of the name of the class of the first item, provided the rest of elements belong to that type (checked with `is_a?`) and they are not hashes. In the example above that's "contributors".
If there's any element that does not belong to the type of the first one the root node becomes "objects":
@@ -2454,7 +2365,7 @@ This method is similar in purpose to `Kernel#Array`, but there are some differen
* If the argument responds to `to_ary` the method is invoked. `Kernel#Array` moves on to try `to_a` if the returned value is `nil`, but `Array.wrap` returns an array with the argument as its single element right away.
* If the returned value from `to_ary` is neither `nil` nor an `Array` object, `Kernel#Array` raises an exception, while `Array.wrap` does not, it just returns the value.
-* It does not call `to_a` on the argument, if the argument does not respond to +to_ary+ it returns an array with the argument as its single element.
+* It does not call `to_a` on the argument, if the argument does not respond to `to_ary` it returns an array with the argument as its single element.
The last point is particularly worth comparing for some enumerables:
@@ -2621,8 +2532,7 @@ To do so, the method loops over the pairs and builds nodes that depend on the _v
```ruby
XML_TYPE_NAMES = {
"Symbol" => "symbol",
- "Fixnum" => "integer",
- "Bignum" => "integer",
+ "Integer" => "integer",
"BigDecimal" => "decimal",
"Float" => "float",
"TrueClass" => "boolean",
@@ -2742,7 +2652,7 @@ The method `transform_keys` accepts a block and returns a hash that has applied
```ruby
{nil => nil, 1 => 1, a: :a}.transform_keys { |key| key.to_s.upcase }
-# => {"" => nil, "A" => :a, "1" => 1}
+# => {"" => nil, "1" => 1, "A" => :a}
```
In case of key collision, one of the values will be chosen. The chosen value may not always be the same given the same hash:
@@ -2784,7 +2694,7 @@ The method `stringify_keys` returns a hash that has a stringified version of the
```ruby
{nil => nil, 1 => 1, a: :a}.stringify_keys
-# => {"" => nil, "a" => :a, "1" => 1}
+# => {"" => nil, "1" => 1, "a" => :a}
```
In case of key collision, one of the values will be chosen. The chosen value may not always be the same given the same hash:
@@ -2826,7 +2736,7 @@ The method `symbolize_keys` returns a hash that has a symbolized version of the
```ruby
{nil => nil, 1 => 1, "a" => "a"}.symbolize_keys
-# => {1=>1, nil=>nil, :a=>"a"}
+# => {nil=>nil, 1=>1, :a=>"a"}
```
WARNING. Note in the previous example only one key was symbolized.
@@ -2883,27 +2793,13 @@ Active Record does not accept unknown options when building associations, for ex
NOTE: Defined in `active_support/core_ext/hash/keys.rb`.
-### Working with Values
-
-#### `transform_values` && `transform_values!`
-
-The method `transform_values` accepts a block and returns a hash that has applied the block operations to each of the values in the receiver.
-
-```ruby
-{ nil => nil, 1 => 1, :x => :a }.transform_values { |value| value.to_s.upcase }
-# => {nil=>"", 1=>"1", :x=>"A"}
-```
-There's also the bang variant `transform_values!` that applies the block operations to values in the very receiver.
-
-NOTE: Defined in `active_support/core_ext/hash/transform_values.rb`.
-
### Slicing
Ruby has built-in support for taking slices out of strings and arrays. Active Support extends slicing to hashes:
```ruby
{a: 1, b: 2, c: 3}.slice(:a, :c)
-# => {:c=>3, :a=>1}
+# => {:a=>1, :c=>3}
{a: 1, b: 2, c: 3}.slice(:b, :X)
# => {:b=>2} # non-existing keys are ignored
@@ -2958,16 +2854,6 @@ The method `with_indifferent_access` returns an `ActiveSupport::HashWithIndiffer
NOTE: Defined in `active_support/core_ext/hash/indifferent_access.rb`.
-### Compacting
-
-The methods `compact` and `compact!` return a Hash without items with `nil` value.
-
-```ruby
-{a: 1, b: 2, c: nil}.compact # => {a: 1, b: 2}
-```
-
-NOTE: Defined in `active_support/core_ext/hash/compact.rb`.
-
Extensions to `Regexp`
----------------------
@@ -3016,9 +2902,9 @@ As the example depicts, the `:db` format generates a `BETWEEN` SQL clause. That
NOTE: Defined in `active_support/core_ext/range/conversions.rb`.
-### `include?`
+### `===`, `include?`, and `cover?`
-The methods `Range#include?` and `Range#===` say whether some value falls between the ends of a given instance:
+The methods `Range#===`, `Range#include?`, and `Range#cover?` say whether some value falls between the ends of a given instance:
```ruby
(2..3).include?(Math::E) # => true
@@ -3027,18 +2913,23 @@ The methods `Range#include?` and `Range#===` say whether some value falls betwee
Active Support extends these methods so that the argument may be another range in turn. In that case we test whether the ends of the argument range belong to the receiver themselves:
```ruby
+(1..10) === (3..7) # => true
+(1..10) === (0..7) # => false
+(1..10) === (3..11) # => false
+(1...9) === (3..9) # => false
+
(1..10).include?(3..7) # => true
(1..10).include?(0..7) # => false
(1..10).include?(3..11) # => false
(1...9).include?(3..9) # => false
-(1..10) === (3..7) # => true
-(1..10) === (0..7) # => false
-(1..10) === (3..11) # => false
-(1...9) === (3..9) # => false
+(1..10).cover?(3..7) # => true
+(1..10).cover?(0..7) # => false
+(1..10).cover?(3..11) # => false
+(1...9).cover?(3..9) # => false
```
-NOTE: Defined in `active_support/core_ext/range/include_range.rb`.
+NOTE: Defined in `active_support/core_ext/range/compare_range.rb`.
### `overlaps?`
@@ -3057,79 +2948,17 @@ Extensions to `Date`
### Calculations
-NOTE: All the following methods are defined in `active_support/core_ext/date/calculations.rb`.
-
INFO: The following calculation methods have edge cases in October 1582, since days 5..14 just do not exist. This guide does not document their behavior around those days for brevity, but it is enough to say that they do what you would expect. That is, `Date.new(1582, 10, 4).tomorrow` returns `Date.new(1582, 10, 15)` and so on. Please check `test/core_ext/date_ext_test.rb` in the Active Support test suite for expected behavior.
#### `Date.current`
-Active Support defines `Date.current` to be today in the current time zone. That's like `Date.today`, except that it honors the user time zone, if defined. It also defines `Date.yesterday` and `Date.tomorrow`, and the instance predicates `past?`, `today?`, and `future?`, all of them relative to `Date.current`.
+Active Support defines `Date.current` to be today in the current time zone. That's like `Date.today`, except that it honors the user time zone, if defined. It also defines `Date.yesterday` and `Date.tomorrow`, and the instance predicates `past?`, `today?`, `future?`, `on_weekday?` and `on_weekend?`, all of them relative to `Date.current`.
When making Date comparisons using methods which honor the user time zone, make sure to use `Date.current` and not `Date.today`. There are cases where the user time zone might be in the future compared to the system time zone, which `Date.today` uses by default. This means `Date.today` may equal `Date.yesterday`.
-#### Named dates
-
-##### `prev_year`, `next_year`
-
-In Ruby 1.9 `prev_year` and `next_year` return a date with the same day/month in the last or next year:
-
-```ruby
-d = Date.new(2010, 5, 8) # => Sat, 08 May 2010
-d.prev_year # => Fri, 08 May 2009
-d.next_year # => Sun, 08 May 2011
-```
-
-If date is the 29th of February of a leap year, you obtain the 28th:
-
-```ruby
-d = Date.new(2000, 2, 29) # => Tue, 29 Feb 2000
-d.prev_year # => Sun, 28 Feb 1999
-d.next_year # => Wed, 28 Feb 2001
-```
-
-`prev_year` is aliased to `last_year`.
-
-##### `prev_month`, `next_month`
-
-In Ruby 1.9 `prev_month` and `next_month` return the date with the same day in the last or next month:
-
-```ruby
-d = Date.new(2010, 5, 8) # => Sat, 08 May 2010
-d.prev_month # => Thu, 08 Apr 2010
-d.next_month # => Tue, 08 Jun 2010
-```
-
-If such a day does not exist, the last day of the corresponding month is returned:
-
-```ruby
-Date.new(2000, 5, 31).prev_month # => Sun, 30 Apr 2000
-Date.new(2000, 3, 31).prev_month # => Tue, 29 Feb 2000
-Date.new(2000, 5, 31).next_month # => Fri, 30 Jun 2000
-Date.new(2000, 1, 31).next_month # => Tue, 29 Feb 2000
-```
-
-`prev_month` is aliased to `last_month`.
-
-##### `prev_quarter`, `next_quarter`
-
-Same as `prev_month` and `next_month`. It returns the date with the same day in the previous or next quarter:
-
-```ruby
-t = Time.local(2010, 5, 8) # => Sat, 08 May 2010
-t.prev_quarter # => Mon, 08 Feb 2010
-t.next_quarter # => Sun, 08 Aug 2010
-```
-
-If such a day does not exist, the last day of the corresponding month is returned:
-
-```ruby
-Time.local(2000, 7, 31).prev_quarter # => Sun, 30 Apr 2000
-Time.local(2000, 5, 31).prev_quarter # => Tue, 29 Feb 2000
-Time.local(2000, 10, 31).prev_quarter # => Mon, 30 Oct 2000
-Time.local(2000, 11, 31).next_quarter # => Wed, 28 Feb 2001
-```
+NOTE: Defined in `active_support/core_ext/date/calculations.rb`.
-`prev_quarter` is aliased to `last_quarter`.
+#### Named dates
##### `beginning_of_week`, `end_of_week`
@@ -3148,6 +2977,8 @@ d.end_of_week(:sunday) # => Sat, 08 May 2010
`beginning_of_week` is aliased to `at_beginning_of_week` and `end_of_week` is aliased to `at_end_of_week`.
+NOTE: Defined in `active_support/core_ext/date_and_time/calculations.rb`.
+
##### `monday`, `sunday`
The methods `monday` and `sunday` return the dates for the previous Monday and
@@ -3165,6 +2996,8 @@ d = Date.new(2012, 9, 16) # => Sun, 16 Sep 2012
d.sunday # => Sun, 16 Sep 2012
```
+NOTE: Defined in `active_support/core_ext/date_and_time/calculations.rb`.
+
##### `prev_week`, `next_week`
The method `next_week` receives a symbol with a day name in English (default is the thread local `Date.beginning_of_week`, or `config.beginning_of_week`, or `:monday`) and it returns the date corresponding to that day.
@@ -3187,6 +3020,8 @@ d.prev_week(:friday) # => Fri, 30 Apr 2010
Both `next_week` and `prev_week` work as expected when `Date.beginning_of_week` or `config.beginning_of_week` are set.
+NOTE: Defined in `active_support/core_ext/date_and_time/calculations.rb`.
+
##### `beginning_of_month`, `end_of_month`
The methods `beginning_of_month` and `end_of_month` return the dates for the beginning and end of the month:
@@ -3199,6 +3034,8 @@ d.end_of_month # => Mon, 31 May 2010
`beginning_of_month` is aliased to `at_beginning_of_month`, and `end_of_month` is aliased to `at_end_of_month`.
+NOTE: Defined in `active_support/core_ext/date_and_time/calculations.rb`.
+
##### `beginning_of_quarter`, `end_of_quarter`
The methods `beginning_of_quarter` and `end_of_quarter` return the dates for the beginning and end of the quarter of the receiver's calendar year:
@@ -3211,6 +3048,8 @@ d.end_of_quarter # => Wed, 30 Jun 2010
`beginning_of_quarter` is aliased to `at_beginning_of_quarter`, and `end_of_quarter` is aliased to `at_end_of_quarter`.
+NOTE: Defined in `active_support/core_ext/date_and_time/calculations.rb`.
+
##### `beginning_of_year`, `end_of_year`
The methods `beginning_of_year` and `end_of_year` return the dates for the beginning and end of the year:
@@ -3223,6 +3062,8 @@ d.end_of_year # => Fri, 31 Dec 2010
`beginning_of_year` is aliased to `at_beginning_of_year`, and `end_of_year` is aliased to `at_end_of_year`.
+NOTE: Defined in `active_support/core_ext/date_and_time/calculations.rb`.
+
#### Other Date Computations
##### `years_ago`, `years_since`
@@ -3248,6 +3089,10 @@ Date.new(2012, 2, 29).years_ago(3) # => Sat, 28 Feb 2009
Date.new(2012, 2, 29).years_since(3) # => Sat, 28 Feb 2015
```
+`last_year` is short-hand for `#years_ago(1)`.
+
+NOTE: Defined in `active_support/core_ext/date_and_time/calculations.rb`.
+
##### `months_ago`, `months_since`
The methods `months_ago` and `months_since` work analogously for months:
@@ -3264,6 +3109,10 @@ Date.new(2010, 4, 30).months_ago(2) # => Sun, 28 Feb 2010
Date.new(2009, 12, 31).months_since(2) # => Sun, 28 Feb 2010
```
+`last_month` is short-hand for `#months_ago(1)`.
+
+NOTE: Defined in `active_support/core_ext/date_and_time/calculations.rb`.
+
##### `weeks_ago`
The method `weeks_ago` works analogously for weeks:
@@ -3273,6 +3122,8 @@ Date.new(2010, 5, 24).weeks_ago(1) # => Mon, 17 May 2010
Date.new(2010, 5, 24).weeks_ago(2) # => Mon, 10 May 2010
```
+NOTE: Defined in `active_support/core_ext/date_and_time/calculations.rb`.
+
##### `advance`
The most generic way to jump to other days is `advance`. This method receives a hash with keys `:years`, `:months`, `:weeks`, `:days`, and returns a date advanced as much as the present keys indicate:
@@ -3301,6 +3152,8 @@ Date.new(2010, 2, 28).advance(days: 1).advance(months: 1)
# => Thu, 01 Apr 2010
```
+NOTE: Defined in `active_support/core_ext/date/calculations.rb`.
+
#### Changing Components
The method `change` allows you to get a new date which is the same as the receiver except for the given year, month, or day:
@@ -3317,6 +3170,8 @@ Date.new(2010, 1, 31).change(month: 2)
# => ArgumentError: invalid date
```
+NOTE: Defined in `active_support/core_ext/date/calculations.rb`.
+
#### Durations
Durations can be added to and subtracted from dates:
@@ -3359,6 +3214,8 @@ date.end_of_day # => Mon Jun 07 23:59:59 +0200 2010
`beginning_of_day` is aliased to `at_beginning_of_day`, `midnight`, `at_midnight`.
+NOTE: Defined in `active_support/core_ext/date/calculations.rb`.
+
##### `beginning_of_hour`, `end_of_hour`
The method `beginning_of_hour` returns a timestamp at the beginning of the hour (hh:00:00):
@@ -3377,6 +3234,8 @@ date.end_of_hour # => Mon Jun 07 19:59:59 +0200 2010
`beginning_of_hour` is aliased to `at_beginning_of_hour`.
+NOTE: Defined in `active_support/core_ext/date_time/calculations.rb`.
+
##### `beginning_of_minute`, `end_of_minute`
The method `beginning_of_minute` returns a timestamp at the beginning of the minute (hh:mm:00):
@@ -3397,6 +3256,8 @@ date.end_of_minute # => Mon Jun 07 19:55:59 +0200 2010
INFO: `beginning_of_hour`, `end_of_hour`, `beginning_of_minute` and `end_of_minute` are implemented for `Time` and `DateTime` but **not** `Date` as it does not make sense to request the beginning or end of an hour or minute on a `Date` instance.
+NOTE: Defined in `active_support/core_ext/date_time/calculations.rb`.
+
##### `ago`, `since`
The method `ago` receives a number of seconds as argument and returns a timestamp those many seconds ago from midnight:
@@ -3413,6 +3274,8 @@ date = Date.current # => Fri, 11 Jun 2010
date.since(1) # => Fri, 11 Jun 2010 00:00:01 EDT -04:00
```
+NOTE: Defined in `active_support/core_ext/date/calculations.rb`.
+
#### Other Time Computations
### Conversions
@@ -3424,35 +3287,7 @@ WARNING: `DateTime` is not aware of DST rules and so some of these methods have
### Calculations
-NOTE: All the following methods are defined in `active_support/core_ext/date_time/calculations.rb`.
-
-The class `DateTime` is a subclass of `Date` so by loading `active_support/core_ext/date/calculations.rb` you inherit these methods and their aliases, except that they will always return datetimes:
-
-```ruby
-yesterday
-tomorrow
-beginning_of_week (at_beginning_of_week)
-end_of_week (at_end_of_week)
-monday
-sunday
-weeks_ago
-prev_week (last_week)
-next_week
-months_ago
-months_since
-beginning_of_month (at_beginning_of_month)
-end_of_month (at_end_of_month)
-prev_month (last_month)
-next_month
-beginning_of_quarter (at_beginning_of_quarter)
-end_of_quarter (at_end_of_quarter)
-beginning_of_year (at_beginning_of_year)
-end_of_year (at_end_of_year)
-years_ago
-years_since
-prev_year (last_year)
-next_year
-```
+The class `DateTime` is a subclass of `Date` so by loading `active_support/core_ext/date/calculations.rb` you inherit these methods and their aliases, except that they will always return datetimes.
The following methods are reimplemented so you do **not** need to load `active_support/core_ext/date/calculations.rb` for these ones:
@@ -3478,6 +3313,8 @@ end_of_hour
Active Support defines `DateTime.current` to be like `Time.now.to_datetime`, except that it honors the user time zone, if defined. It also defines `DateTime.yesterday` and `DateTime.tomorrow`, and the instance predicates `past?`, and `future?` relative to `DateTime.current`.
+NOTE: Defined in `active_support/core_ext/date_time/calculations.rb`.
+
#### Other Extensions
##### `seconds_since_midnight`
@@ -3489,6 +3326,8 @@ now = DateTime.current # => Mon, 07 Jun 2010 20:26:36 +0000
now.seconds_since_midnight # => 73596
```
+NOTE: Defined in `active_support/core_ext/date_time/calculations.rb`.
+
##### `utc`
The method `utc` gives you the same datetime in the receiver expressed in UTC.
@@ -3500,6 +3339,8 @@ now.utc # => Mon, 07 Jun 2010 23:27:52 +0000
This method is also aliased as `getutc`.
+NOTE: Defined in `active_support/core_ext/date_time/calculations.rb`.
+
##### `utc?`
The predicate `utc?` says whether the receiver has UTC as its time zone:
@@ -3510,6 +3351,8 @@ now.utc? # => false
now.utc.utc? # => true
```
+NOTE: Defined in `active_support/core_ext/date_time/calculations.rb`.
+
##### `advance`
The most generic way to jump to another datetime is `advance`. This method receives a hash with keys `:years`, `:months`, `:weeks`, `:days`, `:hours`, `:minutes`, and `:seconds`, and returns a datetime advanced as much as the present keys indicate.
@@ -3541,6 +3384,8 @@ d.advance(seconds: 1).advance(months: 1)
WARNING: Since `DateTime` is not DST-aware you can end up in a non-existing point in time with no warning or error telling you so.
+NOTE: Defined in `active_support/core_ext/date_time/calculations.rb`.
+
#### Changing Components
The method `change` allows you to get a new datetime which is the same as the receiver except for the given options, which may include `:year`, `:month`, `:day`, `:hour`, `:min`, `:sec`, `:offset`, `:start`:
@@ -3573,6 +3418,8 @@ DateTime.current.change(month: 2, day: 30)
# => ArgumentError: invalid date
```
+NOTE: Defined in `active_support/core_ext/date_time/calculations.rb`.
+
#### Durations
Durations can be added to and subtracted from datetimes:
@@ -3598,48 +3445,6 @@ Extensions to `Time`
### Calculations
-NOTE: All the following methods are defined in `active_support/core_ext/time/calculations.rb`.
-
-Active Support adds to `Time` many of the methods available for `DateTime`:
-
-```ruby
-past?
-today?
-future?
-yesterday
-tomorrow
-seconds_since_midnight
-change
-advance
-ago
-since (in)
-beginning_of_day (midnight, at_midnight, at_beginning_of_day)
-end_of_day
-beginning_of_hour (at_beginning_of_hour)
-end_of_hour
-beginning_of_week (at_beginning_of_week)
-end_of_week (at_end_of_week)
-monday
-sunday
-weeks_ago
-prev_week (last_week)
-next_week
-months_ago
-months_since
-beginning_of_month (at_beginning_of_month)
-end_of_month (at_end_of_month)
-prev_month (last_month)
-next_month
-beginning_of_quarter (at_beginning_of_quarter)
-end_of_quarter (at_end_of_quarter)
-beginning_of_year (at_beginning_of_year)
-end_of_year (at_end_of_year)
-years_ago
-years_since
-prev_year (last_year)
-next_year
-```
-
They are analogous. Please refer to their documentation above and take into account the following differences:
* `change` accepts an additional `:usec` option.
@@ -3664,6 +3469,8 @@ Active Support defines `Time.current` to be today in the current time zone. That
When making Time comparisons using methods which honor the user time zone, make sure to use `Time.current` instead of `Time.now`. There are cases where the user time zone might be in the future compared to the system time zone, which `Time.now` uses by default. This means `Time.now.to_date` may equal `Date.yesterday`.
+NOTE: Defined in `active_support/core_ext/time/calculations.rb`.
+
#### `all_day`, `all_week`, `all_month`, `all_quarter` and `all_year`
The method `all_day` returns a range representing the whole day of the current time.
@@ -3692,6 +3499,84 @@ now.all_year
# => Fri, 01 Jan 2010 00:00:00 UTC +00:00..Fri, 31 Dec 2010 23:59:59 UTC +00:00
```
+NOTE: Defined in `active_support/core_ext/date_and_time/calculations.rb`.
+
+#### `prev_day`, `next_day`
+
+In Ruby 1.9 `prev_day` and `next_day` return the date in the last or next day:
+
+```ruby
+d = Date.new(2010, 5, 8) # => Sat, 08 May 2010
+d.prev_day # => Fri, 07 May 2010
+d.next_day # => Sun, 09 May 2010
+```
+
+NOTE: Defined in `active_support/core_ext/date_and_time/calculations.rb`.
+
+#### `prev_month`, `next_month`
+
+In Ruby 1.9 `prev_month` and `next_month` return the date with the same day in the last or next month:
+
+```ruby
+d = Date.new(2010, 5, 8) # => Sat, 08 May 2010
+d.prev_month # => Thu, 08 Apr 2010
+d.next_month # => Tue, 08 Jun 2010
+```
+
+If such a day does not exist, the last day of the corresponding month is returned:
+
+```ruby
+Date.new(2000, 5, 31).prev_month # => Sun, 30 Apr 2000
+Date.new(2000, 3, 31).prev_month # => Tue, 29 Feb 2000
+Date.new(2000, 5, 31).next_month # => Fri, 30 Jun 2000
+Date.new(2000, 1, 31).next_month # => Tue, 29 Feb 2000
+```
+
+NOTE: Defined in `active_support/core_ext/date_and_time/calculations.rb`.
+
+#### `prev_year`, `next_year`
+
+In Ruby 1.9 `prev_year` and `next_year` return a date with the same day/month in the last or next year:
+
+```ruby
+d = Date.new(2010, 5, 8) # => Sat, 08 May 2010
+d.prev_year # => Fri, 08 May 2009
+d.next_year # => Sun, 08 May 2011
+```
+
+If date is the 29th of February of a leap year, you obtain the 28th:
+
+```ruby
+d = Date.new(2000, 2, 29) # => Tue, 29 Feb 2000
+d.prev_year # => Sun, 28 Feb 1999
+d.next_year # => Wed, 28 Feb 2001
+```
+
+NOTE: Defined in `active_support/core_ext/date_and_time/calculations.rb`.
+
+#### `prev_quarter`, `next_quarter`
+
+`prev_quarter` and `next_quarter` return the date with the same day in the previous or next quarter:
+
+```ruby
+t = Time.local(2010, 5, 8) # => 2010-05-08 00:00:00 +0300
+t.prev_quarter # => 2010-02-08 00:00:00 +0200
+t.next_quarter # => 2010-08-08 00:00:00 +0300
+```
+
+If such a day does not exist, the last day of the corresponding month is returned:
+
+```ruby
+Time.local(2000, 7, 31).prev_quarter # => 2000-04-30 00:00:00 +0300
+Time.local(2000, 5, 31).prev_quarter # => 2000-02-29 00:00:00 +0200
+Time.local(2000, 10, 31).prev_quarter # => 2000-07-31 00:00:00 +0300
+Time.local(2000, 11, 31).next_quarter # => 2001-03-01 00:00:00 +0200
+```
+
+`prev_quarter` is aliased to `last_quarter`.
+
+NOTE: Defined in `active_support/core_ext/date_and_time/calculations.rb`.
+
### Time Constructors
Active Support defines `Time.current` to be `Time.zone.now` if there's a user time zone defined, with fallback to `Time.now`:
@@ -3715,7 +3600,7 @@ Durations can be added to and subtracted from time objects:
now = Time.current
# => Mon, 09 Aug 2010 23:20:05 UTC +00:00
now + 1.year
-# => Tue, 09 Aug 2011 23:21:11 UTC +00:00
+# => Tue, 09 Aug 2011 23:21:11 UTC +00:00
now - 1.week
# => Mon, 02 Aug 2010 23:21:11 UTC +00:00
```
@@ -3778,9 +3663,9 @@ Extensions to `NameError`
Active Support adds `missing_name?` to `NameError`, which tests whether the exception was raised because of the name passed as argument.
-The name may be given as a symbol or string. A symbol is tested against the bare constant name, a string is against the fully-qualified constant name.
+The name may be given as a symbol or string. A symbol is tested against the bare constant name, a string is against the fully qualified constant name.
-TIP: A symbol can represent a fully-qualified constant name as in `:"ActiveRecord::Base"`, so the behavior for symbols is defined for convenience, not because it has to be that way technically.
+TIP: A symbol can represent a fully qualified constant name as in `:"ActiveRecord::Base"`, so the behavior for symbols is defined for convenience, not because it has to be that way technically.
For example, when an action of `ArticlesController` is called Rails tries optimistically to use `ArticlesHelper`. It is OK that the helper module does not exist, so if an exception for that constant name is raised it should be silenced. But it could be the case that `articles_helper.rb` raises a `NameError` due to an actual unknown constant. That should be reraised. The method `missing_name?` provides a way to distinguish both cases:
diff --git a/guides/source/active_support_instrumentation.md b/guides/source/active_support_instrumentation.md
index e49abc41f4..64db141381 100644
--- a/guides/source/active_support_instrumentation.md
+++ b/guides/source/active_support_instrumentation.md
@@ -1,9 +1,9 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Active Support Instrumentation
==============================
-Active Support is a part of core Rails that provides Ruby language extensions, utilities and other things. One of the things it includes is an instrumentation API that can be used inside an application to measure certain actions that occur within Ruby code, such as that inside a Rails application or the framework itself. It is not limited to Rails, however. It can be used independently in other Ruby scripts if it is so desired.
+Active Support is a part of core Rails that provides Ruby language extensions, utilities, and other things. One of the things it includes is an instrumentation API that can be used inside an application to measure certain actions that occur within Ruby code, such as that inside a Rails application or the framework itself. It is not limited to Rails, however. It can be used independently in other Ruby scripts if it is so desired.
In this guide, you will learn how to use the instrumentation API inside of Active Support to measure events inside of Rails and other Ruby code.
@@ -112,6 +112,7 @@ Action Controller
| `:controller` | The controller name |
| `:action` | The action |
| `:params` | Hash of request parameters without any filtered parameter |
+| `:headers` | Request headers |
| `:format` | html/js/json/xml etc |
| `:method` | HTTP request verb |
| `:path` | Request path |
@@ -121,6 +122,7 @@ Action Controller
controller: "PostsController",
action: "new",
params: { "action" => "new", "controller" => "posts" },
+ headers: #<ActionDispatch::Http::Headers:0x0055a67a519b88>,
format: :html,
method: "GET",
path: "/posts/new"
@@ -134,6 +136,7 @@ Action Controller
| `:controller` | The controller name |
| `:action` | The action |
| `:params` | Hash of request parameters without any filtered parameter |
+| `:headers` | Request headers |
| `:format` | html/js/json/xml etc |
| `:method` | HTTP request verb |
| `:path` | Request path |
@@ -146,6 +149,7 @@ Action Controller
controller: "PostsController",
action: "index",
params: {"action" => "index", "controller" => "posts"},
+ headers: #<ActionDispatch::Http::Headers:0x0055a67a519b88>,
format: :html,
method: "GET",
path: "/posts",
@@ -165,7 +169,7 @@ INFO. Additional keys may be added by the caller.
### send_data.action_controller
-`ActionController` does not had any specific information to the payload. All options are passed through to the payload.
+`ActionController` does not add any specific information to the payload. All options are passed through to the payload.
### redirect_to.action_controller
@@ -193,6 +197,12 @@ INFO. Additional keys may be added by the caller.
}
```
+### unpermitted_parameters.action_controller
+
+| Key | Value |
+| ------- | ---------------- |
+| `:keys` | Unpermitted keys |
+
Action View
-----------
@@ -222,16 +232,36 @@ Action View
}
```
+### render_collection.action_view
+
+| Key | Value |
+| ------------- | ------------------------------------- |
+| `:identifier` | Full path to template |
+| `:count` | Size of collection |
+| `:cache_hits` | Number of partials fetched from cache |
+
+`:cache_hits` is only included if the collection is rendered with `cached: true`.
+
+```ruby
+{
+ identifier: "/Users/adam/projects/notifications/app/views/posts/_post.html.erb",
+ count: 3,
+ cache_hits: 0
+}
+```
+
Active Record
------------
### sql.active_record
-| Key | Value |
-| ---------------- | --------------------- |
-| `:sql` | SQL statement |
-| `:name` | Name of the operation |
-| `:connection_id` | `self.object_id` |
+| Key | Value |
+| ---------------- | ---------------------------------------- |
+| `:sql` | SQL statement |
+| `:name` | Name of the operation |
+| `:connection_id` | `self.object_id` |
+| `:binds` | Bind parameters |
+| `:cached` | `true` is added when cached queries used |
INFO. The adapters will add their own data as well.
@@ -244,14 +274,6 @@ INFO. The adapters will add their own data as well.
}
```
-### identity.active_record
-
-| Key | Value |
-| ---------------- | ----------------------------------------- |
-| `:line` | Primary Key of object in the identity map |
-| `:name` | Record's class |
-| `:connection_id` | `self.object_id` |
-
### instantiation.active_record
| Key | Value |
@@ -288,7 +310,7 @@ Action Mailer
mailer: "Notification",
message_id: "4f5b5491f1774_181b23fc3d4434d38138e5@mba.local.mail",
subject: "Rails Guides",
- to: ["users@rails.com", "ddh@rails.com"],
+ to: ["users@rails.com", "dhh@rails.com"],
from: ["me@rails.com"],
date: Sat, 10 Mar 2012 14:18:09 +0100,
mail: "..." # omitted for brevity
@@ -297,27 +319,45 @@ Action Mailer
### deliver.action_mailer
-| Key | Value |
-| ------------- | -------------------------------------------- |
-| `:mailer` | Name of the mailer class |
-| `:message_id` | ID of the message, generated by the Mail gem |
-| `:subject` | Subject of the mail |
-| `:to` | To address(es) of the mail |
-| `:from` | From address of the mail |
-| `:bcc` | BCC addresses of the mail |
-| `:cc` | CC addresses of the mail |
-| `:date` | Date of the mail |
-| `:mail` | The encoded form of the mail |
+| Key | Value |
+| --------------------- | ---------------------------------------------------- |
+| `:mailer` | Name of the mailer class |
+| `:message_id` | ID of the message, generated by the Mail gem |
+| `:subject` | Subject of the mail |
+| `:to` | To address(es) of the mail |
+| `:from` | From address of the mail |
+| `:bcc` | BCC addresses of the mail |
+| `:cc` | CC addresses of the mail |
+| `:date` | Date of the mail |
+| `:mail` | The encoded form of the mail |
+| `:perform_deliveries` | Whether delivery of this message is performed or not |
```ruby
{
mailer: "Notification",
message_id: "4f5b5491f1774_181b23fc3d4434d38138e5@mba.local.mail",
subject: "Rails Guides",
- to: ["users@rails.com", "ddh@rails.com"],
+ to: ["users@rails.com", "dhh@rails.com"],
from: ["me@rails.com"],
date: Sat, 10 Mar 2012 14:18:09 +0100,
- mail: "..." # omitted for brevity
+ mail: "...", # omitted for brevity
+ perform_deliveries: true
+}
+```
+
+### process.action_mailer
+
+| Key | Value |
+| ------------- | ------------------------ |
+| `:mailer` | Name of the mailer class |
+| `:action` | The action |
+| `:args` | The arguments |
+
+```ruby
+{
+ mailer: "Notification",
+ action: "welcome_email",
+ args: []
}
```
@@ -403,6 +443,156 @@ INFO. Cache stores may add their own keys
}
```
+Active Job
+--------
+
+### enqueue_at.active_job
+
+| Key | Value |
+| ------------ | -------------------------------------- |
+| `:adapter` | QueueAdapter object processing the job |
+| `:job` | Job object |
+
+### enqueue.active_job
+
+| Key | Value |
+| ------------ | -------------------------------------- |
+| `:adapter` | QueueAdapter object processing the job |
+| `:job` | Job object |
+
+### enqueue_retry.active_job
+
+| Key | Value |
+| ------------ | -------------------------------------- |
+| `:job` | Job object |
+| `:adapter` | QueueAdapter object processing the job |
+| `:error` | The error that caused the retry |
+| `:wait` | The delay of the retry |
+
+### perform_start.active_job
+
+| Key | Value |
+| ------------ | -------------------------------------- |
+| `:adapter` | QueueAdapter object processing the job |
+| `:job` | Job object |
+
+### perform.active_job
+
+| Key | Value |
+| ------------ | -------------------------------------- |
+| `:adapter` | QueueAdapter object processing the job |
+| `:job` | Job object |
+
+### retry_stopped.active_job
+
+| Key | Value |
+| ------------ | -------------------------------------- |
+| `:adapter` | QueueAdapter object processing the job |
+| `:job` | Job object |
+| `:error` | The error that caused the retry |
+
+### discard.active_job
+
+| Key | Value |
+| ------------ | -------------------------------------- |
+| `:adapter` | QueueAdapter object processing the job |
+| `:job` | Job object |
+| `:error` | The error that caused the discard |
+
+Action Cable
+------------
+
+### perform_action.action_cable
+
+| Key | Value |
+| ---------------- | ------------------------- |
+| `:channel_class` | Name of the channel class |
+| `:action` | The action |
+| `:data` | A hash of data |
+
+### transmit.action_cable
+
+| Key | Value |
+| ---------------- | ------------------------- |
+| `:channel_class` | Name of the channel class |
+| `:data` | A hash of data |
+| `:via` | Via |
+
+### transmit_subscription_confirmation.action_cable
+
+| Key | Value |
+| ---------------- | ------------------------- |
+| `:channel_class` | Name of the channel class |
+
+### transmit_subscription_rejection.action_cable
+
+| Key | Value |
+| ---------------- | ------------------------- |
+| `:channel_class` | Name of the channel class |
+
+### broadcast.action_cable
+
+| Key | Value |
+| --------------- | -------------------- |
+| `:broadcasting` | A named broadcasting |
+| `:message` | A hash of message |
+| `:coder` | The coder |
+
+Active Storage
+--------------
+
+### service_upload.active_storage
+
+| Key | Value |
+| ------------ | ---------------------------- |
+| `:key` | Secure token |
+| `:service` | Name of the service |
+| `:checksum` | Checksum to ensure integrity |
+
+### service_streaming_download.active_storage
+
+| Key | Value |
+| ------------ | ------------------- |
+| `:key` | Secure token |
+| `:service` | Name of the service |
+
+### service_download.active_storage
+
+| Key | Value |
+| ------------ | ------------------- |
+| `:key` | Secure token |
+| `:service` | Name of the service |
+
+### service_delete.active_storage
+
+| Key | Value |
+| ------------ | ------------------- |
+| `:key` | Secure token |
+| `:service` | Name of the service |
+
+### service_delete_prefixed.active_storage
+
+| Key | Value |
+| ------------ | ------------------- |
+| `:prefix` | Key prefix |
+| `:service` | Name of the service |
+
+### service_exist.active_storage
+
+| Key | Value |
+| ------------ | --------------------------- |
+| `:key` | Secure token |
+| `:service` | Name of the service |
+| `:exist` | File or blob exists or not |
+
+### service_url.active_storage
+
+| Key | Value |
+| ------------ | ------------------- |
+| `:key` | Secure token |
+| `:service` | Name of the service |
+| `:url` | Generated url |
+
Railties
--------
@@ -433,7 +623,7 @@ The block receives the following arguments:
* The name of the event
* Time when it started
* Time when it finished
-* An unique ID for this event
+* A unique ID for the instrumenter that fired the event
* The payload (described in previous sections)
```ruby
@@ -482,7 +672,8 @@ Creating custom events
Adding your own events is easy as well. `ActiveSupport::Notifications` will take care of
all the heavy lifting for you. Simply call `instrument` with a `name`, `payload` and a block.
The notification will be sent after the block returns. `ActiveSupport` will generate the start and end times
-as well as the unique ID. All data passed into the `instrument` call will make it into the payload.
+and add the instrumenter's unique ID. All data passed into the `instrument` call will make
+it into the payload.
Here's an example:
@@ -501,4 +692,4 @@ end
```
You should follow Rails conventions when defining your own events. The format is: `event.library`.
-If you application is sending Tweets, you should create an event named `tweet.twitter`.
+If your application is sending Tweets, you should create an event named `tweet.twitter`.
diff --git a/guides/source/api_app.md b/guides/source/api_app.md
index 29ca872254..85367c50e7 100644
--- a/guides/source/api_app.md
+++ b/guides/source/api_app.md
@@ -1,5 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
-
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Using Rails for API-only Applications
=====================================
@@ -8,35 +7,34 @@ In this guide you will learn:
* What Rails provides for API-only applications
* How to configure Rails to start without any browser features
-* How to decide which middlewares you will want to include
+* How to decide which middleware you will want to include
* How to decide which modules to use in your controller
--------------------------------------------------------------------------------
-What is an API app?
--------------------
+What is an API Application?
+---------------------------
Traditionally, when people said that they used Rails as an "API", they meant
providing a programmatically accessible API alongside their web application.
-For example, GitHub provides [an API](http://developer.github.com) that you
+For example, GitHub provides [an API](https://developer.github.com) that you
can use from your own custom clients.
With the advent of client-side frameworks, more developers are using Rails to
build a back-end that is shared between their web application and other native
applications.
-For example, Twitter uses its [public API](https://dev.twitter.com) in its web
+For example, Twitter uses its [public API](https://developer.twitter.com/) in its web
application, which is built as a static site that consumes JSON resources.
-Instead of using Rails to generate dynamic HTML that will communicate with the
-server through forms and links, many developers are treating their web application
-as just another client, delivered as static HTML, CSS and JavaScript consuming
-a simple JSON API.
+Instead of using Rails to generate HTML that communicates with the server
+through forms and links, many developers are treating their web application as
+just an API client delivered as HTML with JavaScript that consumes a JSON API.
This guide covers building a Rails application that serves JSON resources to an
-API client **or** a client-side framework.
+API client, including client-side frameworks.
-Why use Rails for JSON APIs?
+Why Use Rails for JSON APIs?
----------------------------
The first question a lot of people have when thinking about building a JSON API
@@ -44,11 +42,11 @@ using Rails is: "isn't using Rails to spit out some JSON overkill? Shouldn't I
just use something like Sinatra?".
For very simple APIs, this may be true. However, even in very HTML-heavy
-applications, most of an application's logic is actually outside of the view
+applications, most of an application's logic lives outside of the view
layer.
The reason most people use Rails is that it provides a set of defaults that
-allows us to get up and running quickly without having to make a lot of trivial
+allows developers to get up and running quickly, without having to make a lot of trivial
decisions.
Let's take a look at some of the things that Rails provides out of the box that are
@@ -67,26 +65,24 @@ Handled at the middleware layer:
about the request environment, database queries, and basic performance
information.
- Security: Rails detects and thwarts [IP spoofing
- attacks](http://en.wikipedia.org/wiki/IP_address_spoofing) and handles
+ attacks](https://en.wikipedia.org/wiki/IP_address_spoofing) and handles
cryptographic signatures in a [timing
- attack](http://en.wikipedia.org/wiki/Timing_attack) aware way. Don't know what
+ attack](https://en.wikipedia.org/wiki/Timing_attack) aware way. Don't know what
an IP spoofing attack or a timing attack is? Exactly.
- Parameter Parsing: Want to specify your parameters as JSON instead of as a
URL-encoded String? No problem. Rails will decode the JSON for you and make
it available in `params`. Want to use nested URL-encoded parameters? That
works too.
-- Conditional GETs: Rails handles conditional `GET`, (`ETag` and `Last-Modified`),
+- Conditional GETs: Rails handles conditional `GET` (`ETag` and `Last-Modified`)
processing request headers and returning the correct response headers and status
code. All you need to do is use the
[`stale?`](http://api.rubyonrails.org/classes/ActionController/ConditionalGet.html#method-i-stale-3F)
check in your controller, and Rails will handle all of the HTTP details for you.
-- Caching: If you use `dirty?` with public cache control, Rails will automatically
- cache your responses. You can easily configure the cache store.
- HEAD requests: Rails will transparently convert `HEAD` requests into `GET` ones,
and return just the headers on the way out. This makes `HEAD` work reliably in
all Rails APIs.
-While you could obviously build these up in terms of existing Rack middlewares,
+While you could obviously build these up in terms of existing Rack middleware,
this list demonstrates that the default Rails middleware stack provides a lot
of value, even if you're "just generating JSON".
@@ -97,28 +93,28 @@ Handled at the Action Pack layer:
means not having to spend time thinking about how to model your API in terms
of HTTP.
- URL Generation: The flip side of routing is URL generation. A good API based
- on HTTP includes URLs (see [the GitHub gist API](http://developer.github.com/v3/gists/)
+ on HTTP includes URLs (see [the GitHub Gist API](https://developer.github.com/v3/gists/)
for an example).
- Header and Redirection Responses: `head :no_content` and
`redirect_to user_url(current_user)` come in handy. Sure, you could manually
add the response headers, but why?
-- Caching: Rails provides page, action and fragment caching. Fragment caching
+- Caching: Rails provides page, action, and fragment caching. Fragment caching
is especially helpful when building up a nested JSON object.
-- Basic, Digest and Token Authentication: Rails comes with out-of-the-box support
+- Basic, Digest, and Token Authentication: Rails comes with out-of-the-box support
for three kinds of HTTP authentication.
-- Instrumentation: Rails has an instrumentation API that will trigger registered
+- Instrumentation: Rails has an instrumentation API that triggers registered
handlers for a variety of events, such as action processing, sending a file or
data, redirection, and database queries. The payload of each event comes with
relevant information (for the action processing event, the payload includes
- the controller, action, parameters, request format, request method and the
+ the controller, action, parameters, request format, request method, and the
request's full path).
-- Generators: This may be passé for advanced Rails users, but it can be nice to
- generate a resource and get your model, controller, test stubs, and routes
- created for you in a single command.
+- Generators: It is often handy to generate a resource and get your model,
+ controller, test stubs, and routes created for you in a single command for
+ further tweaking. Same for migrations and others.
- Plugins: Many third-party libraries come with support for Rails that reduce
or eliminate the cost of setting up and gluing together the library and the
web framework. This includes things like overriding default generators, adding
- rake tasks, and honoring Rails choices (like the logger and cache back-end).
+ Rake tasks, and honoring Rails choices (like the logger and cache back-end).
Of course, the Rails boot process also glues together all registered components.
For example, the Rails boot process is what uses your `config/database.yml` file
@@ -126,7 +122,7 @@ when configuring Active Record.
**The short version is**: you may not have thought about which parts of Rails
are still applicable even if you remove the view layer, but the answer turns out
-to be "most of it".
+to be most of it.
The Basic Configuration
-----------------------
@@ -135,6 +131,8 @@ If you're building a Rails application that will be an API server first and
foremost, you can start with a more limited subset of Rails and add in features
as needed.
+### Creating a new application
+
You can generate a new api Rails app:
```bash
@@ -143,16 +141,18 @@ $ rails new my_api --api
This will do three main things for you:
-- Configure your application to start with a more limited set of middlewares
+- Configure your application to start with a more limited set of middleware
than normal. Specifically, it will not include any middleware primarily useful
for browser applications (like cookies support) by default.
- Make `ApplicationController` inherit from `ActionController::API` instead of
- `ActionController::Base`. As with middlewares, this will leave out any Action
+ `ActionController::Base`. As with middleware, this will leave out any Action
Controller modules that provide functionalities primarily used by browser
applications.
-- Configure the generators to skip generating views, helpers and assets when
+- Configure the generators to skip generating views, helpers, and assets when
you generate a new resource.
+### Changing an existing application
+
If you want to take an existing application and make it an API one, read the
following steps.
@@ -163,6 +163,23 @@ class definition:
config.api_only = true
```
+In `config/environments/development.rb`, set `config.debug_exception_response_format`
+to configure the format used in responses when errors occur in development mode.
+
+To render an HTML page with debugging information, use the value `:default`.
+
+```ruby
+config.debug_exception_response_format = :default
+```
+
+To render debugging information preserving the response format, use the value `:api`.
+
+```ruby
+config.debug_exception_response_format = :api
+```
+
+By default, `config.debug_exception_response_format` is set to `:api`, when `config.api_only` is set to true.
+
Finally, inside `app/controllers/application_controller.rb`, instead of:
```ruby
@@ -177,39 +194,39 @@ class ApplicationController < ActionController::API
end
```
-Choosing Middlewares
+Choosing Middleware
--------------------
-An API application comes with the following middlewares by default:
+An API application comes with the following middleware by default:
- `Rack::Sendfile`
- `ActionDispatch::Static`
-- `Rack::Lock`
+- `ActionDispatch::Executor`
- `ActiveSupport::Cache::Strategy::LocalCache::Middleware`
+- `Rack::Runtime`
- `ActionDispatch::RequestId`
+- `ActionDispatch::RemoteIp`
- `Rails::Rack::Logger`
-- `Rack::Runtime`
- `ActionDispatch::ShowExceptions`
- `ActionDispatch::DebugExceptions`
-- `ActionDispatch::RemoteIp`
- `ActionDispatch::Reloader`
- `ActionDispatch::Callbacks`
-- `ActionDispatch::ParamsParser`
+- `ActiveRecord::Migration::CheckPending`
- `Rack::Head`
- `Rack::ConditionalGet`
- `Rack::ETag`
-See the [internal middlewares](rails_on_rack.html#internal-middleware-stack)
+See the [internal middleware](rails_on_rack.html#internal-middleware-stack)
section of the Rack guide for further information on them.
-Other plugins, including Active Record, may add additional middlewares. In
-general, these middlewares are agnostic to the type of application you are
+Other plugins, including Active Record, may add additional middleware. In
+general, these middleware are agnostic to the type of application you are
building, and make sense in an API-only Rails application.
-You can get a list of all middlewares in your application via:
+You can get a list of all middleware in your application via:
```bash
-$ rake middleware
+$ rails middleware
```
### Using the Cache Middleware
@@ -222,7 +239,7 @@ For instance, using the `stale?` method:
```ruby
def show
- @post = Post.find(params[:id])
+ @post = Post.find(params[:id])
if stale?(last_modified: @post.updated_at)
render json: @post
@@ -241,7 +258,7 @@ cross-client caching in the call to `stale?`:
```ruby
def show
- @post = Post.find(params[:id])
+ @post = Post.find(params[:id])
if stale?(last_modified: @post.updated_at, public: true)
render json: @post
@@ -255,9 +272,6 @@ subsequent inbound requests for the same URL.
Think of it as page caching using HTTP semantics.
-NOTE: This middleware is always outside of the `Rack::Lock` mutex, even in
-single-threaded applications.
-
### Using Rack::Sendfile
When you use the `send_file` method inside a Rails controller, it sets the
@@ -275,7 +289,7 @@ You can learn more about how to use `Rack::Sendfile` with popular
front-ends in [the Rack::Sendfile
documentation](http://rubydoc.info/github/rack/rack/master/Rack/Sendfile).
-Here are some values for popular servers, once they are configured, to support
+Here are some values for this header for some popular servers, once these servers are configured to support
accelerated file sending:
```ruby
@@ -289,12 +303,9 @@ config.action_dispatch.x_sendfile_header = "X-Accel-Redirect"
Make sure to configure your server to support these options following the
instructions in the `Rack::Sendfile` documentation.
-NOTE: The `Rack::Sendfile` middleware is always outside of the `Rack::Lock`
-mutex, even in single-threaded applications.
-
-### Using ActionDispatch::ParamsParser
+### Using ActionDispatch::Request
-`ActionDispatch::ParamsParser` will take parameters from the client in the JSON
+`ActionDispatch::Request#params` will take parameters from the client in the JSON
format and make them available in your controller inside `params`.
To use this, your client will need to make a request with JSON-encoded parameters
@@ -313,33 +324,33 @@ jQuery.ajax({
});
```
-`ActionDispatch::ParamsParser` will see the `Content-Type` and your parameters
+`ActionDispatch::Request` will see the `Content-Type` and your parameters
will be:
```ruby
{ :person => { :firstName => "Yehuda", :lastName => "Katz" } }
```
-### Other Middlewares
+### Other Middleware
-Rails ships with a number of other middlewares that you might want to use in an
+Rails ships with a number of other middleware that you might want to use in an
API application, especially if one of your API clients is the browser:
- `Rack::MethodOverride`
- `ActionDispatch::Cookies`
- `ActionDispatch::Flash`
-- For sessions management
+- For session management
* `ActionDispatch::Session::CacheStore`
* `ActionDispatch::Session::CookieStore`
* `ActionDispatch::Session::MemCacheStore`
-Any of these middlewares can be added via:
+Any of these middleware can be added via:
```ruby
config.middleware.use Rack::MethodOverride
```
-### Removing Middlewares
+### Removing Middleware
If you don't want to use a middleware that is included by default in the API-only
middleware set, you can remove it with:
@@ -357,34 +368,38 @@ Choosing Controller Modules
An API application (using `ActionController::API`) comes with the following
controller modules by default:
-- `ActionController::UrlFor`: Makes `url_for` and friends available.
+- `ActionController::UrlFor`: Makes `url_for` and similar helpers available.
- `ActionController::Redirecting`: Support for `redirect_to`.
-- `ActionController::Rendering`: Basic support for rendering.
+- `AbstractController::Rendering` and `ActionController::ApiRendering`: Basic support for rendering.
- `ActionController::Renderers::All`: Support for `render :json` and friends.
- `ActionController::ConditionalGet`: Support for `stale?`.
-- `ActionController::ForceSSL`: Support for `force_ssl`.
-- `ActionController::RackDelegation`: Support for the `request` and `response`
- methods returning `ActionDispatch::Request` and `ActionDispatch::Response`
- objects.
+- `ActionController::BasicImplicitRender`: Makes sure to return an empty response, if there isn't an explicit one.
+- `ActionController::StrongParameters`: Support for parameters white-listing in combination with Active Model mass assignment.
- `ActionController::DataStreaming`: Support for `send_file` and `send_data`.
-- `AbstractController::Callbacks`: Support for `before_filter` and friends.
+- `AbstractController::Callbacks`: Support for `before_action` and
+ similar helpers.
+- `ActionController::Rescue`: Support for `rescue_from`.
- `ActionController::Instrumentation`: Support for the instrumentation
hooks defined by Action Controller (see [the instrumentation
- guide](active_support_instrumentation.html#action-controller)).
-- `ActionController::Rescue`: Support for `rescue_from`.
-- `ActionController::BasicImplicitRender`: Makes sure to return an empty response
- if there's not an explicit one.
-- `ActionController::StrongParameters`: Support for parameters white-listing in
- combination with Active Model mass assignment.
-- `ActionController::ParamsWrapper`: Wraps the parameters hash into a nested hash
- so you don't have to specify root elements sending POST requests for instance.
+ guide](active_support_instrumentation.html#action-controller) for
+more information regarding this).
+- `ActionController::ParamsWrapper`: Wraps the parameters hash into a nested hash,
+ so that you don't have to specify root elements sending POST requests for instance.
+- `ActionController::Head`: Support for returning a response with no content, only headers
Other plugins may add additional modules. You can get a list of all modules
included into `ActionController::API` in the rails console:
```bash
-$ bin/rails c
+$ rails c
>> ActionController::API.ancestors - ActionController::Metal.ancestors
+=> [ActionController::API,
+ ActiveRecord::Railties::ControllerRuntime,
+ ActionDispatch::Routing::RouteSet::MountedHelpers,
+ ActionController::ParamsWrapper,
+ ... ,
+ AbstractController::Rendering,
+ ActionView::ViewPaths]
```
### Adding Other Modules
@@ -397,12 +412,14 @@ Some common modules you might want to add:
- `AbstractController::Translation`: Support for the `l` and `t` localization
and translation methods.
-- `ActionController::HTTPAuthentication::Basic` (or `Digest` or `Token`): Support
- for basic, digest or token HTTP authentication.
-- `AbstractController::Layouts`: Support for layouts when rendering.
+- Support for basic, digest, or token HTTP authentication:
+ * `ActionController::HttpAuthentication::Basic::ControllerMethods`,
+ * `ActionController::HttpAuthentication::Digest::ControllerMethods`,
+ * `ActionController::HttpAuthentication::Token::ControllerMethods`
+- `ActionView::Layouts`: Support for layouts when rendering.
- `ActionController::MimeResponds`: Support for `respond_to`.
- `ActionController::Cookies`: Support for `cookies`, which includes
support for signed and encrypted cookies. This requires the cookies middleware.
-The best place to add a module is in your `ApplicationController` but you can
+The best place to add a module is in your `ApplicationController`, but you can
also add modules to individual controllers.
diff --git a/guides/source/api_documentation_guidelines.md b/guides/source/api_documentation_guidelines.md
index 46c9013087..b6ee7354f9 100644
--- a/guides/source/api_documentation_guidelines.md
+++ b/guides/source/api_documentation_guidelines.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
API Documentation Guidelines
============================
@@ -16,7 +16,7 @@ RDoc
----
The [Rails API documentation](http://api.rubyonrails.org) is generated with
-[RDoc](http://docs.seattlerb.org/rdoc/). To generate it, make sure you are
+[RDoc](https://ruby.github.io/rdoc/). To generate it, make sure you are
in the rails root directory, run `bundle install` and execute:
```bash
@@ -26,9 +26,9 @@ in the rails root directory, run `bundle install` and execute:
Resulting HTML files can be found in the ./doc/rdoc directory.
Please consult the RDoc documentation for help with the
-[markup](http://docs.seattlerb.org/rdoc/RDoc/Markup.html),
+[markup](https://ruby.github.io/rdoc/RDoc/Markup.html),
and also take into account these [additional
-directives](http://docs.seattlerb.org/rdoc/RDoc/Parser/Ruby.html).
+directives](https://ruby.github.io/rdoc/RDoc/Parser/Ruby.html).
Wording
-------
@@ -53,7 +53,7 @@ Documentation has to be concise but comprehensive. Explore and document edge cas
The proper names of Rails components have a space in between the words, like "Active Support". `ActiveRecord` is a Ruby module, whereas Active Record is an ORM. All Rails documentation should consistently refer to Rails components by their proper name, and if in your next blog post or presentation you remember this tidbit and take it into account that'd be phenomenal.
-Spell names correctly: Arel, Test::Unit, RSpec, HTML, MySQL, JavaScript, ERB. When in doubt, please have a look at some authoritative source like their official documentation.
+Spell names correctly: Arel, minitest, RSpec, HTML, MySQL, JavaScript, ERB. When in doubt, please have a look at some authoritative source like their official documentation.
Use the article "an" for "SQL", as in "an SQL statement". Also "an SQLite database".
@@ -82,7 +82,13 @@ used. Instead of:
English
-------
-Please use American English (*color*, *center*, *modularize*, etc). See [a list of American and British English spelling differences here](http://en.wikipedia.org/wiki/American_and_British_English_spelling_differences).
+Please use American English (*color*, *center*, *modularize*, etc). See [a list of American and British English spelling differences here](https://en.wikipedia.org/wiki/American_and_British_English_spelling_differences).
+
+Oxford Comma
+------------
+
+Please use the [Oxford comma](https://en.wikipedia.org/wiki/Serial_comma)
+("red, white, and blue", instead of "red, white and blue").
Example Code
------------
@@ -114,7 +120,7 @@ On the other hand, big chunks of structured documentation may have a separate "E
The results of expressions follow them and are introduced by "# => ", vertically aligned:
```ruby
-# For checking if a fixnum is even or odd.
+# For checking if an integer is even or odd.
#
# 1.even? # => false
# 1.odd? # => true
@@ -234,7 +240,7 @@ You can quickly test the RDoc output with the following command:
```
$ echo "+:to_param+" | rdoc --pipe
-#=> <p><code>:to_param</code></p>
+# => <p><code>:to_param</code></p>
```
### Regular Font
@@ -275,7 +281,7 @@ Methods created with `(module|class)_eval(STRING)` have a comment by their side
```ruby
for severity in Severity.constants
- class_eval <<-EOT, __FILE__, __LINE__
+ class_eval <<-EOT, __FILE__, __LINE__ + 1
def #{severity.downcase}(message = nil, progname = nil, &block) # def debug(message = nil, progname = nil, &block)
add(#{severity}, message, progname, &block) # add(DEBUG, message, progname, &block)
end # end
@@ -327,10 +333,6 @@ As a contributor, it's important to think about whether this API is meant for en
A class or module is marked with `:nodoc:` to indicate that all methods are internal API and should never be used directly.
-If you come across an existing `:nodoc:` you should tread lightly. Consider asking someone from the core team or author of the code before removing it. This should almost always happen through a pull request instead of the docrails project.
-
-A `:nodoc:` should never be added simply because a method or class is missing documentation. There may be an instance where an internal public method wasn't given a `:nodoc:` by mistake, for example when switching a method from private to public visibility. When this happens it should be discussed over a PR on a case-by-case basis and never committed directly to docrails.
-
To summarize, the Rails team uses `:nodoc:` to mark publicly visible methods and classes for internal use; changes to the visibility of API should be considered carefully and discussed over a pull request first.
Regarding the Rails Stack
@@ -348,7 +350,7 @@ into account, one such example is
```ruby
# image_tag("icon.png")
-# # => <img alt="Icon" src="/assets/icon.png" />
+# # => <img src="/assets/icon.png" />
```
Although the default behavior for `#image_tag` is to always return
diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md
index 4a610e8458..500e230ff9 100644
--- a/guides/source/asset_pipeline.md
+++ b/guides/source/asset_pipeline.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
The Asset Pipeline
==================
@@ -20,23 +20,21 @@ What is the Asset Pipeline?
The asset pipeline provides a framework to concatenate and minify or compress
JavaScript and CSS assets. It also adds the ability to write these assets in
-other languages and pre-processors such as CoffeeScript, Sass and ERB.
+other languages and pre-processors such as CoffeeScript, Sass, and ERB.
+It allows assets in your application to be automatically combined with assets
+from other gems.
-The asset pipeline is technically no longer a core feature of Rails 4, it has
-been extracted out of the framework into the
-[sprockets-rails](https://github.com/rails/sprockets-rails) gem.
-
-The asset pipeline is enabled by default.
-
-You can disable the asset pipeline while creating a new application by
+The asset pipeline is implemented by the
+[sprockets-rails](https://github.com/rails/sprockets-rails) gem,
+and is enabled by default. You can disable it while creating a new application by
passing the `--skip-sprockets` option.
```bash
rails new appname --skip-sprockets
```
-Rails 4 automatically adds the `sass-rails`, `coffee-rails` and `uglifier`
-gems to your Gemfile, which are used by Sprockets for asset compression:
+Rails automatically adds the `sass-rails`, `coffee-rails` and `uglifier`
+gems to your `Gemfile`, which are used by Sprockets for asset compression:
```ruby
gem 'sass-rails'
@@ -44,9 +42,9 @@ gem 'uglifier'
gem 'coffee-rails'
```
-Using the `--skip-sprockets` option will prevent Rails 4 from adding
-`sass-rails` and `uglifier` to Gemfile, so if you later want to enable
-the asset pipeline you will have to add those gems to your Gemfile. Also,
+Using the `--skip-sprockets` option will prevent Rails from adding
+them to your `Gemfile`, so if you later want to enable
+the asset pipeline you will have to add those gems to your `Gemfile`. Also,
creating an application with the `--skip-sprockets` option will generate
a slightly different `config/application.rb` file, with a require statement
for the sprockets railtie that is commented-out. You will have to remove
@@ -66,7 +64,7 @@ config.assets.js_compressor = :uglifier
```
NOTE: The `sass-rails` gem is automatically used for CSS compression if included
-in Gemfile and no `config.assets.css_compressor` option is set.
+in the `Gemfile` and no `config.assets.css_compressor` option is set.
### Main Features
@@ -79,9 +77,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,
@@ -107,7 +105,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`
```
@@ -155,7 +153,7 @@ environments. You can enable or disable it in your configuration through the
More reading:
-* [Optimize caching](http://code.google.com/speed/page-speed/docs/caching.html)
+* [Optimize caching](https://developers.google.com/speed/docs/insights/LeverageBrowserCaching)
* [Revving Filenames: don't use querystring](http://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/)
@@ -169,7 +167,7 @@ directory. Files in this directory are served by the Sprockets middleware.
Assets can still be placed in the `public` hierarchy. Any assets under `public`
will be served as static files by the application or web server when
-`config.serve_static_files` is set to true. You should use `app/assets` for
+`config.public_file_server.enabled` is set to true. You should use `app/assets` for
files that must undergo some pre-processing before they are served.
In production, Rails precompiles these files to `public/assets` by default. The
@@ -182,13 +180,12 @@ When you generate a scaffold or a controller, Rails also generates a JavaScript
file (or CoffeeScript file if the `coffee-rails` gem is in the `Gemfile`) and a
Cascading Style Sheet file (or SCSS file if `sass-rails` is in the `Gemfile`)
for that controller. Additionally, when generating a scaffold, Rails generates
-the file scaffolds.css (or scaffolds.scss if `sass-rails` is in the
+the file `scaffolds.css` (or `scaffolds.scss` if `sass-rails` is in the
`Gemfile`.)
For example, if you generate a `ProjectsController`, Rails will also add a new
-file at `app/assets/javascripts/projects.coffee` and another at
-`app/assets/stylesheets/projects.scss`. By default these files will be ready
-to use by your application immediately using the `require_tree` directive. See
+file at `app/assets/stylesheets/projects.scss`. By default these files will be
+ready to use by your application immediately using the `require_tree` directive. See
[Manifest Files and Directives](#manifest-files-and-directives) for more details
on require_tree.
@@ -203,12 +200,12 @@ will result in your assets being included more than once.
WARNING: When using asset precompilation, you will need to ensure that your
controller assets will be precompiled when loading them on a per page basis. By
-default .coffee and .scss files will not be precompiled on their own. See
+default `.coffee` and `.scss` files will not be precompiled on their own. See
[Precompiling Assets](#precompiling-assets) for more information on how
precompiling works.
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
@@ -226,7 +223,7 @@ Pipeline assets can be placed inside an application in one of three locations:
`app/assets`, `lib/assets` or `vendor/assets`.
* `app/assets` is for assets that are owned by the application, such as custom
-images, JavaScript files or stylesheets.
+images, JavaScript files, or stylesheets.
* `lib/assets` is for your own libraries' code that doesn't really fit into the
scope of the application or those libraries which are shared across applications.
@@ -236,11 +233,6 @@ code for JavaScript plugins and CSS frameworks. Keep in mind that third party
code with references to other files also processed by the asset Pipeline (images,
stylesheets, etc.), will need to be rewritten to use helpers like `asset_path`.
-WARNING: If you are upgrading from Rails 3, please take into account that assets
-under `lib/assets` or `vendor/assets` are available for inclusion via the
-application manifests but no longer part of the precompile array. See
-[Precompiling Assets](#precompiling-assets) for guidance.
-
#### Search Paths
When a file is referenced from a manifest or a helper, Sprockets searches the
@@ -284,10 +276,10 @@ You can view the search path by inspecting
`Rails.application.config.assets.paths` in the Rails console.
Besides the standard `assets/*` paths, additional (fully qualified) paths can be
-added to the pipeline in `config/application.rb`. For example:
+added to the pipeline in `config/initializers/assets.rb`. For example:
```ruby
-config.assets.paths << Rails.root.join("lib", "videoplayer", "flash")
+Rails.application.config.assets.paths << Rails.root.join("lib", "videoplayer", "flash")
```
Paths are traversed in the order they occur in the search path. By default,
@@ -327,16 +319,16 @@ familiar `javascript_include_tag` and `stylesheet_link_tag`:
<%= javascript_include_tag "application" %>
```
-If using the turbolinks gem, which is included by default in Rails 4, then
+If using the turbolinks gem, which is included by default in Rails, then
include the 'data-turbolinks-track' option which causes turbolinks to check if
an asset has been updated and if so loads it into the page:
```erb
-<%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %>
-<%= javascript_include_tag "application", "data-turbolinks-track" => true %>
+<%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => "reload" %>
+<%= javascript_include_tag "application", "data-turbolinks-track" => "reload" %>
```
-In regular views you can access images in the `public/assets/images` directory
+In regular views you can access images in the `app/assets/images` directory
like this:
```erb
@@ -347,9 +339,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`,
@@ -384,7 +376,7 @@ it would make sense to have an image in one of the asset load paths, such as
already available in `public/assets` as a fingerprinted file, then that path is
referenced.
-If you want to use a [data URI](http://en.wikipedia.org/wiki/Data_URI_scheme) -
+If you want to use a [data URI](https://en.wikipedia.org/wiki/Data_URI_scheme) -
a method of embedding the image data directly into the CSS file - you can use
the `asset_data_uri` helper.
@@ -403,13 +395,13 @@ When using the asset pipeline, paths to assets must be re-written and
underscored in Ruby) for the following asset classes: image, font, video, audio,
JavaScript and stylesheet.
-* `image-url("rails.png")` becomes `url(/assets/rails.png)`
-* `image-path("rails.png")` becomes `"/assets/rails.png"`.
+* `image-url("rails.png")` returns `url(/assets/rails.png)`
+* `image-path("rails.png")` returns `"/assets/rails.png"`
The more generic form can also be used:
-* `asset-url("rails.png")` becomes `url(/assets/rails.png)`
-* `asset-path("rails.png")` becomes `"/assets/rails.png"`
+* `asset-url("rails.png")` returns `url(/assets/rails.png)`
+* `asset-path("rails.png")` returns `"/assets/rails.png"`
#### JavaScript/CoffeeScript and ERB
@@ -436,27 +428,27 @@ Sprockets uses manifest files to determine which assets to include and serve.
These manifest files contain _directives_ - instructions that tell Sprockets
which files to require in order to build a single CSS or JavaScript file. With
these directives, Sprockets loads the files specified, processes them if
-necessary, concatenates them into one single file and then compresses them (if
-`Rails.application.config.assets.compress` is true). By serving one file rather
-than many, the load time of pages can be greatly reduced because the browser
-makes fewer requests. Compression also reduces file size, enabling the
-browser to download them faster.
+necessary, concatenates them into one single file, and then compresses them
+(based on value of `Rails.application.config.assets.js_compressor`). By serving
+one file rather than many, the load time of pages can be greatly reduced because
+the browser makes fewer requests. Compression also reduces file size, enabling
+the browser to download them faster.
-For example, a new Rails 4 application includes a default
+For example, a new Rails application includes a default
`app/assets/javascripts/application.js` file containing the following lines:
```js
// ...
-//= require jquery
-//= require jquery_ujs
+//= require rails-ujs
+//= require turbolinks
//= require_tree .
```
In JavaScript files, Sprockets directives begin with `//=`. In the above case,
the file is using the `require` and the `require_tree` directives. The `require`
directive is used to tell Sprockets the files you wish to require. Here, you are
-requiring the files `jquery.js` and `jquery_ujs.js` that are available somewhere
+requiring the files `rails-ujs.js` and `turbolinks.js` that are available somewhere
in the search path for Sprockets. You need not supply the extensions explicitly.
Sprockets assumes you are requiring a `.js` file when done from within a `.js`
file.
@@ -484,9 +476,9 @@ which contains these lines:
*/
```
-Rails 4 creates both `app/assets/javascripts/application.js` and
+Rails creates both `app/assets/javascripts/application.js` and
`app/assets/stylesheets/application.css` regardless of whether the
---skip-sprockets option is used when creating a new rails application. This is
+--skip-sprockets option is used when creating a new Rails application. This is
so you can easily add asset pipelining later if you like.
The directives that work in JavaScript files also work in stylesheets
@@ -573,19 +565,18 @@ would generate this HTML:
The `body` param is required by Sprockets.
-### Runtime Error Checking
+### Raise an Error When an Asset is Not Found
-By default the asset pipeline will check for potential errors in development mode during
-runtime. To disable this behavior you can set:
+If you are using sprockets-rails >= 3.2.0 you can configure what happens
+when an asset lookup is performed and nothing is found. If you turn off "asset fallback"
+then an error will be raised when an asset cannot be found.
```ruby
-config.assets.raise_runtime_errors = false
+config.assets.unknown_asset_fallback = false
```
-When this option is true, the asset pipeline will check if all the assets loaded
-in your application are included in the `config.assets.precompile` list.
-If `config.assets.digest` is also true, the asset pipeline will require that
-all requests for assets include digests.
+If "asset fallback" is enabled then when an asset cannot be found the path will be
+output instead and no error raised. The asset fallback behavior is enabled by default.
### Turning Digests Off
@@ -642,7 +633,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.
@@ -662,12 +653,12 @@ generates something like this:
rel="stylesheet" />
```
-Note: with the Asset Pipeline the :cache and :concat options aren't used
+NOTE: with the Asset Pipeline the `:cache` and `:concat` options aren't used
anymore, delete these options from the `javascript_include_tag` and
`stylesheet_link_tag`.
The fingerprinting behavior is controlled by the `config.assets.digest`
-initialization option (which defaults to `true` for production and development).
+initialization option (which defaults to `true`).
NOTE: Under normal circumstances the default `config.assets.digest` option
should not be changed. If there are no digests in the filenames, and far-future
@@ -676,20 +667,20 @@ content changes.
### Precompiling Assets
-Rails comes bundled with a rake task to compile the asset manifests and other
+Rails comes bundled with a command to compile the asset manifests and other
files in the pipeline.
Compiled assets are written to the location specified in `config.assets.prefix`.
By default, this is the `/assets` directory.
-You can call this task on the server during deployment to create compiled
+You can call this command on the server during deployment to create compiled
versions of your assets directly on the server. See the next section for
information on compiling locally.
-The rake task is:
+The command is:
```bash
-$ RAILS_ENV=production bin/rake assets:precompile
+$ RAILS_ENV=production rails assets:precompile
```
Capistrano (v2.15.1 and above) includes a recipe to handle this in deployment.
@@ -701,7 +692,7 @@ load 'deploy/assets'
This links the folder specified in `config.assets.prefix` to `shared/assets`.
If you already use this shared folder you'll need to write your own deployment
-task.
+command.
It is important that this folder is shared between deployments so that remotely
cached pages referencing the old compiled assets still work for the life of
@@ -725,28 +716,30 @@ If you have other manifests or individual stylesheets and JavaScript files to
include, you can add them to the `precompile` array in `config/initializers/assets.rb`:
```ruby
-Rails.application.config.assets.precompile += ['admin.js', 'admin.css', 'swfObject.js']
+Rails.application.config.assets.precompile += %w( admin.js admin.css )
```
-NOTE. Always specify an expected compiled filename that ends with .js or .css,
+NOTE. Always specify an expected compiled filename that ends with `.js` or `.css`,
even if you want to add Sass or CoffeeScript files to the precompile array.
-The rake 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 command also generates a `.sprockets-manifest-randomhex.json` (where `randomhex` is
+a 16-byte random hex string) that contains a list with all your assets and their respective
+fingerprints. This is used by the Rails helper methods to avoid handing the
+mapping requests back to Sprockets. A typical manifest file looks like:
```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
@@ -786,7 +779,6 @@ location ~ ^/assets/ {
add_header Cache-Control public;
add_header ETag "";
- break;
}
```
@@ -839,7 +831,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
@@ -847,22 +839,22 @@ signals all caches between your server and the client browser that this content
number of requests for this asset from your server; the asset has a good chance
of being in the local browser cache or some intermediate cache.
-This mode uses more memory, performs more poorly than the default and is not
+This mode uses more memory, performs more poorly than the default, and is not
recommended.
If you are deploying a production application to a system without any
-pre-existing JavaScript runtimes, you may want to add one to your Gemfile:
+pre-existing JavaScript runtimes, you may want to add one to your `Gemfile`:
```ruby
group :production do
- gem 'therubyracer'
+ gem 'mini_racer'
end
```
### CDNs
CDN stands for [Content Delivery
-Network](http://en.wikipedia.org/wiki/Content_delivery_network), they are
+Network](https://en.wikipedia.org/wiki/Content_delivery_network), they are
primarily designed to cache assets all over the world so that when a browser
requests the asset, a cached copy will be geographically close to that browser.
If you are serving assets directly from your Rails server in production, the
@@ -898,7 +890,7 @@ your CDN server, you need to tell browsers to use your CDN to grab assets
instead of your Rails server directly. You can do this by configuring Rails to
set your CDN as the asset host instead of using a relative path. To set your
asset host in Rails, you need to set `config.action_controller.asset_host` in
-`config/production.rb`:
+`config/environments/production.rb`:
```ruby
config.action_controller.asset_host = 'mycdnsubdomain.fictional-cdn.com'
@@ -910,7 +902,7 @@ domain, you do not need to specify a protocol or "scheme" such as `http://` or
that is generated will match how the webpage is accessed by default.
You can also set this value through an [environment
-variable](http://en.wikipedia.org/wiki/Environment_variable) to make running a
+variable](https://en.wikipedia.org/wiki/Environment_variable) to make running a
staging copy of your site easier:
```
@@ -919,7 +911,7 @@ config.action_controller.asset_host = ENV['CDN_HOST']
-Note: You would need to set `CDN_HOST` on your server to `mycdnsubdomain
+NOTE: You would need to set `CDN_HOST` on your server to `mycdnsubdomain
.fictional-cdn.com` for this to work.
Once you have configured your server and your CDN when you serve a webpage that
@@ -1021,15 +1013,17 @@ header](http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9) is a W3C
specification that describes how a request can be cached. When no CDN is used, a
browser will use this information to cache contents. This is very helpful for
assets that are not modified so that a browser does not need to re-download a
-website's CSS or javascript on every request. Generally we want our Rails server
+website's CSS or JavaScript on every request. Generally we want our Rails server
to tell our CDN (and browser) that the asset is "public", that means any cache
can store the request. Also we commonly want to set `max-age` which is how long
the cache will store the object before invalidating the cache. The `max-age`
value is set to seconds with a maximum possible value of `31536000` which is one
-year. You can do this in your rails application by setting
+year. You can do this in your Rails application by setting
```
-config.static_cache_control = "public, max-age=31536000"
+config.public_file_server.headers = {
+ 'Cache-Control' => 'public, max-age=31536000'
+}
```
Now when your application serves an asset in production, the CDN will store the
@@ -1068,7 +1062,7 @@ Customizing the Pipeline
### CSS Compression
One of the options for compressing CSS is YUI. The [YUI CSS
-compressor](http://yui.github.io/yuicompressor/css.html) provides
+compressor](https://yui.github.io/yuicompressor/css.html) provides
minification.
The following line enables YUI compression, and requires the `yui-compressor`
@@ -1089,7 +1083,7 @@ Possible options for JavaScript compression are `:closure`, `:uglifier` and
`:yui`. These require the use of the `closure-compiler`, `uglifier` or
`yui-compressor` gems, respectively.
-The default Gemfile includes [uglifier](https://github.com/lautis/uglifier).
+The default `Gemfile` includes [uglifier](https://github.com/lautis/uglifier).
This gem wraps [UglifyJS](https://github.com/mishoo/UglifyJS) (written for
NodeJS) in Ruby. It compresses your code by removing white space and comments,
shortening local variable names, and performing other micro-optimizations such
@@ -1102,14 +1096,20 @@ 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.
-NOTE: The `config.assets.compress` initialization option is no longer used in
-Rails 4 to enable either CSS or JavaScript compression. Setting it will have no
-effect on the application. Instead, setting `config.assets.css_compressor` and
-`config.assets.js_compressor` will control compression of CSS and JavaScript
-assets.
+
+
+### Serving GZipped version of assets
+
+By default, gzipped version of compiled assets will be generated, along with
+the non-gzipped version of assets. Gzipped assets help reduce the transmission
+of data over the wire. You can configure this by setting the `gzip` flag.
+
+```ruby
+config.assets.gzip = false # disable gzipped assets generation
+```
### Using Your Own Compressor
@@ -1175,19 +1175,14 @@ TIP: For further details have a look at the docs of your production web server:
Assets Cache Store
------------------
-The default Rails cache store will be used by Sprockets to cache assets in
-development and production. This can be changed by setting
-`config.assets.cache_store`:
+By default, Sprockets caches assets in `tmp/cache/assets` in development
+and production environments. This can be changed as follows:
```ruby
-config.assets.cache_store = :memory_store
-```
-
-The options accepted by the assets cache store are the same as the application's
-cache store.
-
-```ruby
-config.assets.cache_store = :memory_store, { size: 32.megabytes }
+config.assets.configure do |env|
+ env.cache = ActiveSupport::Cache.lookup_store(:memory_store,
+ { size: 32.megabytes })
+end
```
To disable the assets cache store:
@@ -1203,103 +1198,34 @@ Adding Assets to Your Gems
Assets can also come from external sources in the form of gems.
-A good example of this is the `jquery-rails` gem which comes with Rails as the
-standard JavaScript library gem. This gem contains an engine class which
-inherits from `Rails::Engine`. By doing this, Rails is informed that the
-directory for this gem may contain assets and the `app/assets`, `lib/assets` and
+A good example of this is the `jquery-rails` gem.
+This gem contains an engine class which inherits from `Rails::Engine`.
+By doing this, Rails is informed that the directory for this
+gem may contain assets and the `app/assets`, `lib/assets` and
`vendor/assets` directories of this engine are added to the search path of
Sprockets.
Making Your Library or Gem a Pre-Processor
------------------------------------------
-As Sprockets uses [Tilt](https://github.com/rtomayko/tilt) as a generic
-interface to different templating engines, your gem should just implement the
-Tilt template protocol. Normally, you would subclass `Tilt::Template` and
-reimplement the `prepare` method, which initializes your template, and the
-`evaluate` method, which returns the processed source. The original source is
-stored in `data`. Have a look at
-[`Tilt::Template`](https://github.com/rtomayko/tilt/blob/master/lib/tilt/template.rb)
-sources to learn more.
+Sprockets uses Processors, Transformers, Compressors, and Exporters to extend
+Sprockets functionality. Have a look at
+[Extending Sprockets](https://github.com/rails/sprockets/blob/master/guides/extending_sprockets.md)
+to learn more. Here we registered a preprocessor to add a comment to the end
+of text/css (`.css`) files.
```ruby
-module BangBang
- class Template < ::Tilt::Template
- def prepare
- # Do any initialization here
- end
-
- # Adds a "!" to original template.
- def evaluate(scope, locals, &block)
- "#{data}!"
- end
+module AddComment
+ def self.call(input)
+ { data: input[:data] + "/* Hello From my sprockets extension */" }
end
end
```
-Now that you have a `Template` class, it's time to associate it with an
-extension for template files:
-
-```ruby
-Sprockets.register_engine '.bang', BangBang::Template
-```
-
-Upgrading from Old Versions of Rails
-------------------------------------
-
-There are a few issues when upgrading from Rails 3.0 or Rails 2.x. The first is
-moving the files from `public/` to the new locations. See [Asset
-Organization](#asset-organization) above for guidance on the correct locations
-for different file types.
-
-Next will be avoiding duplicate JavaScript files. Since jQuery is the default
-JavaScript library from Rails 3.1 onwards, you don't need to copy `jquery.js`
-into `app/assets` and it will be included automatically.
-
-The third is updating the various environment files with the correct default
-options.
-
-In `application.rb`:
+Now that you have a module that modifies the input data, it's time to register
+it as a preprocessor for your mime type.
```ruby
-# Version of your assets, change this if you want to expire all your assets
-config.assets.version = '1.0'
-
-# Change the path that assets are served from config.assets.prefix = "/assets"
+Sprockets.register_preprocessor 'text/css', AddComment
```
-In `development.rb`:
-
-```ruby
-# Expands the lines which load the assets
-config.assets.debug = true
-```
-
-And in `production.rb`:
-
-```ruby
-# Choose the compressors to use (if any) config.assets.js_compressor =
-# :uglifier config.assets.css_compressor = :yui
-
-# Don't fallback to assets pipeline if a precompiled asset is missed
-config.assets.compile = false
-
-# Generate digests for assets URLs. This is planned for deprecation.
-config.assets.digest = true
-
-# Precompile additional assets (application.js, application.css, and all
-# non-JS/CSS are already added) config.assets.precompile += %w( search.js )
-```
-
-Rails 4 no longer sets default config values for Sprockets in `test.rb`, so
-`test.rb` now requires Sprockets configuration. The old defaults in the test
-environment are: `config.assets.compile = true`, `config.assets.compress = false`,
-`config.assets.debug = false` and `config.assets.digest = false`.
-
-The following should also be added to `Gemfile`:
-
-```ruby
-gem 'sass-rails', "~> 3.2.3"
-gem 'coffee-rails', "~> 3.2.1"
-gem 'uglifier'
-```
diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md
index c0fa3cfd04..4f3e8b2cff 100644
--- a/guides/source/association_basics.md
+++ b/guides/source/association_basics.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Active Record Associations
==========================
@@ -16,54 +16,54 @@ After reading this guide, you will know:
Why Associations?
-----------------
-Why do we need associations between models? Because they make common operations simpler and easier in your code. For example, consider a simple Rails application that includes a model for customers and a model for orders. Each customer can have many orders. Without associations, the model declarations would look like this:
+In Rails, an _association_ is a connection between two Active Record models. Why do we need associations between models? Because they make common operations simpler and easier in your code. For example, consider a simple Rails application that includes a model for authors and a model for books. Each author can have many books. Without associations, the model declarations would look like this:
```ruby
-class Customer < ActiveRecord::Base
+class Author < ApplicationRecord
end
-class Order < ActiveRecord::Base
+class Book < ApplicationRecord
end
```
-Now, suppose we wanted to add a new order for an existing customer. We'd need to do something like this:
+Now, suppose we wanted to add a new book for an existing author. We'd need to do something like this:
```ruby
-@order = Order.create(order_date: Time.now, customer_id: @customer.id)
+@book = Book.create(published_at: Time.now, author_id: @author.id)
```
-Or consider deleting a customer, and ensuring that all of its orders get deleted as well:
+Or consider deleting an author, and ensuring that all of its books get deleted as well:
```ruby
-@orders = Order.where(customer_id: @customer.id)
-@orders.each do |order|
- order.destroy
+@books = Book.where(author_id: @author.id)
+@books.each do |book|
+ book.destroy
end
-@customer.destroy
+@author.destroy
```
-With Active Record associations, we can streamline these - and other - operations by declaratively telling Rails that there is a connection between the two models. Here's the revised code for setting up customers and orders:
+With Active Record associations, we can streamline these - and other - operations by declaratively telling Rails that there is a connection between the two models. Here's the revised code for setting up authors and books:
```ruby
-class Customer < ActiveRecord::Base
- has_many :orders, dependent: :destroy
+class Author < ApplicationRecord
+ has_many :books, dependent: :destroy
end
-class Order < ActiveRecord::Base
- belongs_to :customer
+class Book < ApplicationRecord
+ belongs_to :author
end
```
-With this change, creating a new order for a particular customer is easier:
+With this change, creating a new book for a particular author is easier:
```ruby
-@order = @customer.orders.create(order_date: Time.now)
+@book = @author.books.create(published_at: Time.now)
```
-Deleting a customer and all of its orders is *much* easier:
+Deleting an author and all of its books is *much* easier:
```ruby
-@customer.destroy
+@author.destroy
```
To learn more about the different types of associations, read the next section of this guide. That's followed by some tips and tricks for working with associations, and then by a complete reference to the methods and options for associations in Rails.
@@ -71,7 +71,7 @@ To learn more about the different types of associations, read the next section o
The Types of Associations
-------------------------
-In Rails, an _association_ is a connection between two Active Record models. Associations are implemented using macro-style calls, so that you can declaratively add features to your models. For example, by declaring that one model `belongs_to` another, you instruct Rails to maintain Primary Key-Foreign Key information between instances of the two models, and you also get a number of utility methods added to your model. Rails supports six types of associations:
+Rails supports six types of associations:
* `belongs_to`
* `has_one`
@@ -80,36 +80,38 @@ In Rails, an _association_ is a connection between two Active Record models. Ass
* `has_one :through`
* `has_and_belongs_to_many`
+Associations are implemented using macro-style calls, so that you can declaratively add features to your models. For example, by declaring that one model `belongs_to` another, you instruct Rails to maintain [Primary Key](https://en.wikipedia.org/wiki/Unique_key)-[Foreign Key](https://en.wikipedia.org/wiki/Foreign_key) information between instances of the two models, and you also get a number of utility methods added to your model.
+
In the remainder of this guide, you'll learn how to declare and use the various forms of associations. But first, a quick introduction to the situations where each association type is appropriate.
### The `belongs_to` Association
-A `belongs_to` association sets up a one-to-one connection with another model, such that each instance of the declaring model "belongs to" one instance of the other model. For example, if your application includes customers and orders, and each order can be assigned to exactly one customer, you'd declare the order model this way:
+A `belongs_to` association sets up a one-to-one connection with another model, such that each instance of the declaring model "belongs to" one instance of the other model. For example, if your application includes authors and books, and each book can be assigned to exactly one author, you'd declare the book model this way:
```ruby
-class Order < ActiveRecord::Base
- belongs_to :customer
+class Book < ApplicationRecord
+ belongs_to :author
end
```
-![belongs_to Association Diagram](images/belongs_to.png)
+![belongs_to Association Diagram](images/association_basics/belongs_to.png)
-NOTE: `belongs_to` associations _must_ use the singular term. If you used the pluralized form in the above example for the `customer` association in the `Order` model, you would be told that there was an "uninitialized constant Order::Customers". This is because Rails automatically infers the class name from the association name. If the association name is wrongly pluralized, then the inferred class will be wrongly pluralized too.
+NOTE: `belongs_to` associations _must_ use the singular term. If you used the pluralized form in the above example for the `author` association in the `Book` model and tried to create the instance by `Book.create(authors: @author)`, you would be told that there was an "uninitialized constant Book::Authors". This is because Rails automatically infers the class name from the association name. If the association name is wrongly pluralized, then the inferred class will be wrongly pluralized too.
The corresponding migration might look like this:
```ruby
-class CreateOrders < ActiveRecord::Migration
+class CreateBooks < ActiveRecord::Migration[5.0]
def change
- create_table :customers do |t|
+ create_table :authors do |t|
t.string :name
- t.timestamps null: false
+ t.timestamps
end
- create_table :orders do |t|
- t.belongs_to :customer, index: true
- t.datetime :order_date
- t.timestamps null: false
+ create_table :books do |t|
+ t.belongs_to :author
+ t.datetime :published_at
+ t.timestamps
end
end
end
@@ -120,27 +122,27 @@ end
A `has_one` association also sets up a one-to-one connection with another model, but with somewhat different semantics (and consequences). This association indicates that each instance of a model contains or possesses one instance of another model. For example, if each supplier in your application has only one account, you'd declare the supplier model like this:
```ruby
-class Supplier < ActiveRecord::Base
+class Supplier < ApplicationRecord
has_one :account
end
```
-![has_one Association Diagram](images/has_one.png)
+![has_one Association Diagram](images/association_basics/has_one.png)
The corresponding migration might look like this:
```ruby
-class CreateSuppliers < ActiveRecord::Migration
+class CreateSuppliers < ActiveRecord::Migration[5.0]
def change
create_table :suppliers do |t|
t.string :name
- t.timestamps null: false
+ t.timestamps
end
create_table :accounts do |t|
- t.belongs_to :supplier, index: true
+ t.belongs_to :supplier
t.string :account_number
- t.timestamps null: false
+ t.timestamps
end
end
end
@@ -152,39 +154,39 @@ 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
```
### The `has_many` Association
-A `has_many` association indicates a one-to-many connection with another model. You'll often find this association on the "other side" of a `belongs_to` association. This association indicates that each instance of the model has zero or more instances of another model. For example, in an application containing customers and orders, the customer model could be declared like this:
+A `has_many` association indicates a one-to-many connection with another model. You'll often find this association on the "other side" of a `belongs_to` association. This association indicates that each instance of the model has zero or more instances of another model. For example, in an application containing authors and books, the author model could be declared like this:
```ruby
-class Customer < ActiveRecord::Base
- has_many :orders
+class Author < ApplicationRecord
+ has_many :books
end
```
NOTE: The name of the other model is pluralized when declaring a `has_many` association.
-![has_many Association Diagram](images/has_many.png)
+![has_many Association Diagram](images/association_basics/has_many.png)
The corresponding migration might look like this:
```ruby
-class CreateCustomers < ActiveRecord::Migration
+class CreateAuthors < ActiveRecord::Migration[5.0]
def change
- create_table :customers do |t|
+ create_table :authors do |t|
t.string :name
- t.timestamps null: false
+ t.timestamps
end
- create_table :orders do |t|
- t.belongs_to :customer, index: true
- t.datetime :order_date
- t.timestamps null: false
+ create_table :books do |t|
+ t.belongs_to :author
+ t.datetime :published_at
+ t.timestamps
end
end
end
@@ -195,73 +197,75 @@ end
A `has_many :through` association is often used to set up a many-to-many connection with another model. This association indicates that the declaring model can be matched with zero or more instances of another model by proceeding _through_ a third model. For example, consider a medical practice where patients make appointments to see physicians. The relevant association declarations could look like this:
```ruby
-class Physician < ActiveRecord::Base
+class Physician < ApplicationRecord
has_many :appointments
has_many :patients, through: :appointments
end
-class Appointment < ActiveRecord::Base
+class Appointment < ApplicationRecord
belongs_to :physician
belongs_to :patient
end
-class Patient < ActiveRecord::Base
+class Patient < ApplicationRecord
has_many :appointments
has_many :physicians, through: :appointments
end
```
-![has_many :through Association Diagram](images/has_many_through.png)
+![has_many :through Association Diagram](images/association_basics/has_many_through.png)
The corresponding migration might look like this:
```ruby
-class CreateAppointments < ActiveRecord::Migration
+class CreateAppointments < ActiveRecord::Migration[5.0]
def change
create_table :physicians do |t|
t.string :name
- t.timestamps null: false
+ t.timestamps
end
create_table :patients do |t|
t.string :name
- t.timestamps null: false
+ t.timestamps
end
create_table :appointments do |t|
- t.belongs_to :physician, index: true
- t.belongs_to :patient, index: true
+ t.belongs_to :physician
+ t.belongs_to :patient
t.datetime :appointment_date
- t.timestamps null: false
+ t.timestamps
end
end
end
```
-The collection of join models can be managed via the API. For example, if you assign
+The collection of join models can be managed via the [`has_many` association methods](#has-many-association-reference).
+For example, if you assign:
```ruby
physician.patients = patients
```
-new join models are created for newly associated objects, and if some are gone their rows are deleted.
+Then new join models are automatically created for the newly associated objects.
+If some that existed previously are now missing, then their join rows are automatically deleted.
WARNING: Automatic deletion of join models is direct, no destroy callbacks are triggered.
The `has_many :through` association is also useful for setting up "shortcuts" through nested `has_many` associations. For example, if a document has many sections, and a section has many paragraphs, you may sometimes want to get a simple collection of all paragraphs in the document. You could set that up this way:
```ruby
-class Document < ActiveRecord::Base
+class Document < ApplicationRecord
has_many :sections
has_many :paragraphs, through: :sections
end
-class Section < ActiveRecord::Base
+class Section < ApplicationRecord
belongs_to :document
has_many :paragraphs
end
-class Paragraph < ActiveRecord::Base
+class Paragraph < ApplicationRecord
belongs_to :section
end
```
@@ -280,43 +284,43 @@ For example, if each supplier has one account, and each account is associated wi
supplier model could look like this:
```ruby
-class Supplier < ActiveRecord::Base
+class Supplier < ApplicationRecord
has_one :account
has_one :account_history, through: :account
end
-class Account < ActiveRecord::Base
+class Account < ApplicationRecord
belongs_to :supplier
has_one :account_history
end
-class AccountHistory < ActiveRecord::Base
+class AccountHistory < ApplicationRecord
belongs_to :account
end
```
-![has_one :through Association Diagram](images/has_one_through.png)
+![has_one :through Association Diagram](images/association_basics/has_one_through.png)
The corresponding migration might look like this:
```ruby
-class CreateAccountHistories < ActiveRecord::Migration
+class CreateAccountHistories < ActiveRecord::Migration[5.0]
def change
create_table :suppliers do |t|
t.string :name
- t.timestamps null: false
+ t.timestamps
end
create_table :accounts do |t|
- t.belongs_to :supplier, index: true
+ t.belongs_to :supplier
t.string :account_number
- t.timestamps null: false
+ t.timestamps
end
create_table :account_histories do |t|
- t.belongs_to :account, index: true
+ t.belongs_to :account
t.integer :credit_rating
- t.timestamps null: false
+ t.timestamps
end
end
end
@@ -327,35 +331,35 @@ end
A `has_and_belongs_to_many` association creates a direct many-to-many connection with another model, with no intervening model. For example, if your application includes assemblies and parts, with each assembly having many parts and each part appearing in many assemblies, you could declare the models this way:
```ruby
-class Assembly < ActiveRecord::Base
+class Assembly < ApplicationRecord
has_and_belongs_to_many :parts
end
-class Part < ActiveRecord::Base
+class Part < ApplicationRecord
has_and_belongs_to_many :assemblies
end
```
-![has_and_belongs_to_many Association Diagram](images/habtm.png)
+![has_and_belongs_to_many Association Diagram](images/association_basics/habtm.png)
The corresponding migration might look like this:
```ruby
-class CreateAssembliesAndParts < ActiveRecord::Migration
+class CreateAssembliesAndParts < ActiveRecord::Migration[5.0]
def change
create_table :assemblies do |t|
t.string :name
- t.timestamps null: false
+ t.timestamps
end
create_table :parts do |t|
t.string :part_number
- t.timestamps null: false
+ t.timestamps
end
create_table :assemblies_parts, id: false do |t|
- t.belongs_to :assembly, index: true
- t.belongs_to :part, index: true
+ t.belongs_to :assembly
+ t.belongs_to :part
end
end
end
@@ -368,11 +372,11 @@ If you want to set up a one-to-one relationship between two models, you'll need
The distinction is in where you place the foreign key (it goes on the table for the class declaring the `belongs_to` association), but you should give some thought to the actual meaning of the data as well. The `has_one` relationship says that one of something is yours - that is, that something points back to you. For example, it makes more sense to say that a supplier owns an account than that an account owns a supplier. This suggests that the correct relationships are like this:
```ruby
-class Supplier < ActiveRecord::Base
+class Supplier < ApplicationRecord
has_one :account
end
-class Account < ActiveRecord::Base
+class Account < ApplicationRecord
belongs_to :supplier
end
```
@@ -380,17 +384,17 @@ end
The corresponding migration might look like this:
```ruby
-class CreateSuppliers < ActiveRecord::Migration
+class CreateSuppliers < ActiveRecord::Migration[5.0]
def change
create_table :suppliers do |t|
- t.string :name
- t.timestamps null: false
+ t.string :name
+ t.timestamps
end
create_table :accounts do |t|
t.integer :supplier_id
t.string :account_number
- t.timestamps null: false
+ t.timestamps
end
add_index :accounts, :supplier_id
@@ -405,11 +409,11 @@ NOTE: Using `t.integer :supplier_id` makes the foreign key naming obvious and ex
Rails offers two different ways to declare a many-to-many relationship between models. The simpler way is to use `has_and_belongs_to_many`, which allows you to make the association directly:
```ruby
-class Assembly < ActiveRecord::Base
+class Assembly < ApplicationRecord
has_and_belongs_to_many :parts
end
-class Part < ActiveRecord::Base
+class Part < ApplicationRecord
has_and_belongs_to_many :assemblies
end
```
@@ -417,17 +421,17 @@ end
The second way to declare a many-to-many relationship is to use `has_many :through`. This makes the association indirectly, through a join model:
```ruby
-class Assembly < ActiveRecord::Base
+class Assembly < ApplicationRecord
has_many :manifests
has_many :parts, through: :manifests
end
-class Manifest < ActiveRecord::Base
+class Manifest < ApplicationRecord
belongs_to :assembly
belongs_to :part
end
-class Part < ActiveRecord::Base
+class Part < ApplicationRecord
has_many :manifests
has_many :assemblies, through: :manifests
end
@@ -435,22 +439,22 @@ end
The simplest rule of thumb is that you should set up a `has_many :through` relationship if you need to work with the relationship model as an independent entity. If you don't need to do anything with the relationship model, it may be simpler to set up a `has_and_belongs_to_many` relationship (though you'll need to remember to create the joining table in the database).
-You should use `has_many :through` if you need validations, callbacks or extra attributes on the join model.
+You should use `has_many :through` if you need validations, callbacks, or extra attributes on the join model.
### Polymorphic Associations
A slightly more advanced twist on associations is the _polymorphic association_. With polymorphic associations, a model can belong to more than one other model, on a single association. For example, you might have a picture model that belongs to either an employee model or a product model. Here's how this could be declared:
```ruby
-class Picture < ActiveRecord::Base
+class Picture < ApplicationRecord
belongs_to :imageable, polymorphic: true
end
-class Employee < ActiveRecord::Base
+class Employee < ApplicationRecord
has_many :pictures, as: :imageable
end
-class Product < ActiveRecord::Base
+class Product < ApplicationRecord
has_many :pictures, as: :imageable
end
```
@@ -462,13 +466,13 @@ Similarly, you can retrieve `@product.pictures`.
If you have an instance of the `Picture` model, you can get to its parent via `@picture.imageable`. To make this work, you need to declare both a foreign key column and a type column in the model that declares the polymorphic interface:
```ruby
-class CreatePictures < ActiveRecord::Migration
+class CreatePictures < ActiveRecord::Migration[5.0]
def change
create_table :pictures do |t|
t.string :name
t.integer :imageable_id
t.string :imageable_type
- t.timestamps null: false
+ t.timestamps
end
add_index :pictures, [:imageable_type, :imageable_id]
@@ -479,29 +483,29 @@ end
This migration can be simplified by using the `t.references` form:
```ruby
-class CreatePictures < ActiveRecord::Migration
+class CreatePictures < ActiveRecord::Migration[5.0]
def change
create_table :pictures do |t|
t.string :name
- t.references :imageable, polymorphic: true, index: true
- t.timestamps null: false
+ t.references :imageable, polymorphic: true
+ t.timestamps
end
end
end
```
-![Polymorphic Association Diagram](images/polymorphic.png)
+![Polymorphic Association Diagram](images/association_basics/polymorphic.png)
### Self Joins
In designing a data model, you will sometimes find a model that should have a relation to itself. For example, you may want to store all employees in a single database model, but be able to trace relationships such as between manager and subordinates. This situation can be modeled with self-joining associations:
```ruby
-class Employee < ActiveRecord::Base
+class Employee < ApplicationRecord
has_many :subordinates, class_name: "Employee",
foreign_key: "manager_id"
- belongs_to :manager, class_name: "Employee"
+ belongs_to :manager, class_name: "Employee", optional: true
end
```
@@ -510,11 +514,11 @@ With this setup, you can retrieve `@employee.subordinates` and `@employee.manage
In your migrations/schema, you will add a references column to the model itself.
```ruby
-class CreateEmployees < ActiveRecord::Migration
+class CreateEmployees < ActiveRecord::Migration[5.0]
def change
create_table :employees do |t|
- t.references :manager, index: true
- t.timestamps null: false
+ t.references :manager
+ t.timestamps
end
end
end
@@ -536,18 +540,18 @@ Here are a few things you should know to make efficient use of Active Record ass
All of the association methods are built around caching, which keeps the result of the most recent query available for further operations. The cache is even shared across methods. For example:
```ruby
-customer.orders # retrieves orders from the database
-customer.orders.size # uses the cached copy of orders
-customer.orders.empty? # uses the cached copy of orders
+author.books # retrieves books from the database
+author.books.size # uses the cached copy of books
+author.books.empty? # uses the cached copy of books
```
-But what if you want to reload the cache, because data might have been changed by some other part of the application? Just pass `true` to the association call:
+But what if you want to reload the cache, because data might have been changed by some other part of the application? Just call `reload` on the association:
```ruby
-customer.orders # retrieves orders from the database
-customer.orders.size # uses the cached copy of orders
-customer.orders(true).empty? # discards the cached copy of orders
- # and goes back to the database
+author.books # retrieves books from the database
+author.books.size # uses the cached copy of books
+author.books.reload.empty? # discards the cached copy of books
+ # and goes back to the database
```
### Avoiding Name Collisions
@@ -563,43 +567,51 @@ Associations are extremely useful, but they are not magic. You are responsible f
When you declare a `belongs_to` association, you need to create foreign keys as appropriate. For example, consider this model:
```ruby
-class Order < ActiveRecord::Base
- belongs_to :customer
+class Book < ApplicationRecord
+ belongs_to :author
end
```
-This declaration needs to be backed up by the proper foreign key declaration on the orders table:
+This declaration needs to be backed up by a corresponding foreign key column in the books table. For a brand new table, the migration might look something like this:
```ruby
-class CreateOrders < ActiveRecord::Migration
+class CreateBooks < ActiveRecord::Migration[5.0]
def change
- create_table :orders do |t|
- t.datetime :order_date
- t.string :order_number
- t.integer :customer_id
+ create_table :books do |t|
+ t.datetime :published_at
+ t.string :book_number
+ t.references :author
end
+ end
+end
+```
- add_index :orders, :customer_id
+Whereas for an existing table, it might look like this:
+
+```ruby
+class AddAuthorToBooks < ActiveRecord::Migration[5.0]
+ def change
+ add_reference :books, :author
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.
+NOTE: If you wish to [enforce referential integrity at the database level](/active_record_migrations.html#foreign-keys), add the `foreign_key: true` option to the ‘reference’ column declarations above.
#### Creating Join Tables for `has_and_belongs_to_many` Associations
-If you create a `has_and_belongs_to_many` association, you need to explicitly create the joining table. Unless the name of the join table is explicitly specified by using the `:join_table` option, Active Record creates the name by using the lexical order of the class names. So a join between customer and order models will give the default join table name of "customers_orders" because "c" outranks "o" in lexical ordering.
+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 order 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.
WARNING: The precedence between model names is calculated using the `<=>` operator for `String`. This means that if the strings are of different lengths, and the strings are equal when compared up to the shortest length, then the longer string is considered of higher lexical precedence than the shorter one. For example, one would expect the tables "paper_boxes" and "papers" to generate a join table name of "papers_paper_boxes" because of the length of the name "paper_boxes", but it in fact generates a join table name of "paper_boxes_papers" (because the underscore '\_' is lexicographically _less_ than 's' in common encodings).
Whatever the name, you must manually generate the join table with an appropriate migration. For example, consider these associations:
```ruby
-class Assembly < ActiveRecord::Base
+class Assembly < ApplicationRecord
has_and_belongs_to_many :parts
end
-class Part < ActiveRecord::Base
+class Part < ApplicationRecord
has_and_belongs_to_many :assemblies
end
```
@@ -607,7 +619,7 @@ end
These need to be backed up by a migration to create the `assemblies_parts` table. This table should be created without a primary key:
```ruby
-class CreateAssembliesPartsJoinTable < ActiveRecord::Migration
+class CreateAssembliesPartsJoinTable < ActiveRecord::Migration[5.0]
def change
create_table :assemblies_parts, id: false do |t|
t.integer :assembly_id
@@ -622,6 +634,19 @@ end
We pass `id: false` to `create_table` because that table does not represent a model. That's required for the association to work properly. If you observe any strange behavior in a `has_and_belongs_to_many` association like mangled model IDs, or exceptions about conflicting IDs, chances are you forgot that bit.
+You can also use the method `create_join_table`
+
+```ruby
+class CreateAssembliesPartsJoinTable < ActiveRecord::Migration[5.0]
+ def change
+ create_join_table :assemblies, :parts do |t|
+ t.index :assembly_id
+ t.index :part_id
+ end
+ end
+end
+```
+
### Controlling Association Scope
By default, associations look for objects only within the current module's scope. This can be important when you declare Active Record models within a module. For example:
@@ -629,12 +654,12 @@ By default, associations look for objects only within the current module's scope
```ruby
module MyApplication
module Business
- class Supplier < ActiveRecord::Base
- has_one :account
+ class Supplier < ApplicationRecord
+ has_one :account
end
- class Account < ActiveRecord::Base
- belongs_to :supplier
+ class Account < ApplicationRecord
+ belongs_to :supplier
end
end
end
@@ -645,14 +670,14 @@ This will work fine, because both the `Supplier` and the `Account` class are def
```ruby
module MyApplication
module Business
- class Supplier < ActiveRecord::Base
- has_one :account
+ class Supplier < ApplicationRecord
+ has_one :account
end
end
module Billing
- class Account < ActiveRecord::Base
- belongs_to :supplier
+ class Account < ApplicationRecord
+ belongs_to :supplier
end
end
end
@@ -663,15 +688,15 @@ To associate a model with a model in a different namespace, you must specify the
```ruby
module MyApplication
module Business
- class Supplier < ActiveRecord::Base
- has_one :account,
+ class Supplier < ApplicationRecord
+ has_one :account,
class_name: "MyApplication::Billing::Account"
end
end
module Billing
- class Account < ActiveRecord::Base
- belongs_to :supplier,
+ class Account < ApplicationRecord
+ belongs_to :supplier,
class_name: "MyApplication::Business::Supplier"
end
end
@@ -683,64 +708,73 @@ end
It's normal for associations to work in two directions, requiring declaration on two different models:
```ruby
-class Customer < ActiveRecord::Base
- has_many :orders
+class Author < ApplicationRecord
+ has_many :books
end
-class Order < ActiveRecord::Base
- belongs_to :customer
+class Book < ApplicationRecord
+ belongs_to :author
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
-c = Customer.first
-o = c.orders.first
-c.first_name == o.customer.first_name # => true
-c.first_name = 'Manny'
-c.first_name == o.customer.first_name # => false
+a = Author.first
+b = a.books.first
+a.first_name == b.author.first_name # => true
+a.first_name = 'David'
+a.first_name == b.author.first_name # => true
```
-This happens because `c` and `o.customer` 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 a scope or any of the following options:
+
+* `:through`
+* `:foreign_key`
+
+For example, consider the following model declarations:
```ruby
-class Customer < ActiveRecord::Base
- has_many :orders, inverse_of: :customer
+class Author < ApplicationRecord
+ has_many :books
end
-class Order < ActiveRecord::Base
- belongs_to :customer, inverse_of: :orders
+class Book < ApplicationRecord
+ belongs_to :writer, class_name: 'Author', foreign_key: 'author_id'
end
```
-With these changes, Active Record will only load one copy of the customer object, preventing inconsistencies and making your application more efficient:
+Active Record will no longer automatically recognize the bi-directional association:
```ruby
-c = Customer.first
-o = c.orders.first
-c.first_name == o.customer.first_name # => true
-c.first_name = 'Manny'
-c.first_name == o.customer.first_name # => true
+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 # => false
```
-There are a few limitations to `inverse_of` support:
+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
-* 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.
+class Book < ApplicationRecord
+ belongs_to :writer, class_name: 'Author', foreign_key: 'author_id'
+end
+```
-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:
+By including the `:inverse_of` option in the `has_many` association declaration, Active Record will now recognize the bi-directional association:
-* `:conditions`
-* `:through`
-* `:polymorphic`
-* `:foreign_key`
+```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
+```
Detailed Association Reference
------------------------------
@@ -753,50 +787,56 @@ The `belongs_to` association creates a one-to-one match with another model. In d
#### Methods Added by `belongs_to`
-When you declare a `belongs_to` association, the declaring class automatically gains five methods related to the association:
+When you declare a `belongs_to` association, the declaring class automatically gains 6 methods related to the association:
-* `association(force_reload = false)`
+* `association`
* `association=(associate)`
* `build_association(attributes = {})`
* `create_association(attributes = {})`
* `create_association!(attributes = {})`
+* `reload_association`
In all of these methods, `association` is replaced with the symbol passed as the first argument to `belongs_to`. For example, given the declaration:
```ruby
-class Order < ActiveRecord::Base
- belongs_to :customer
+class Book < ApplicationRecord
+ belongs_to :author
end
```
-Each instance of the `Order` model will have these methods:
+Each instance of the `Book` model will have these methods:
```ruby
-customer
-customer=
-build_customer
-create_customer
-create_customer!
+author
+author=
+build_author
+create_author
+create_author!
+reload_author
```
NOTE: When initializing a new `has_one` or `belongs_to` association you must use the `build_` prefix to build the association, rather than the `association.build` method that would be used for `has_many` or `has_and_belongs_to_many` associations. To create one, use the `create_` prefix.
-##### `association(force_reload = false)`
+##### `association`
The `association` method returns the associated object, if any. If no associated object is found, it returns `nil`.
```ruby
-@customer = @order.customer
+@author = @book.author
```
-If the associated object has already been retrieved from the database for this object, the cached version will be returned. To override this behavior (and force a database read), pass `true` as the `force_reload` argument.
+If the associated object has already been retrieved from the database for this object, the cached version will be returned. To override this behavior (and force a database read), call `#reload_association` on the parent object.
+
+```ruby
+@author = @book.reload_author
+```
##### `association=(associate)`
The `association=` method assigns an associated object to this object. Behind the scenes, this means extracting the primary key from the associated object and setting this object's foreign key to the same value.
```ruby
-@order.customer = @customer
+@book.author = @author
```
##### `build_association(attributes = {})`
@@ -804,8 +844,8 @@ The `association=` method assigns an associated object to this object. Behind th
The `build_association` method returns a new object of the associated type. This object will be instantiated from the passed attributes, and the link through this object's foreign key will be set, but the associated object will _not_ yet be saved.
```ruby
-@customer = @order.build_customer(customer_number: 123,
- customer_name: "John Doe")
+@author = @book.build_author(author_number: 123,
+ author_name: "John Doe")
```
##### `create_association(attributes = {})`
@@ -813,8 +853,8 @@ The `build_association` method returns a new object of the associated type. This
The `create_association` method returns a new object of the associated type. This object will be instantiated from the passed attributes, the link through this object's foreign key will be set, and, once it passes all of the validations specified on the associated model, the associated object _will_ be saved.
```ruby
-@customer = @order.create_customer(customer_number: 123,
- customer_name: "John Doe")
+@author = @book.create_author(author_number: 123,
+ author_name: "John Doe")
```
##### `create_association!(attributes = {})`
@@ -827,8 +867,8 @@ Does the same as `create_association` above, but raises `ActiveRecord::RecordInv
While Rails uses intelligent defaults that will work well in most situations, there may be times when you want to customize the behavior of the `belongs_to` association reference. Such customizations can easily be accomplished by passing options and scope blocks when you create the association. For example, this association uses two such options:
```ruby
-class Order < ActiveRecord::Base
- belongs_to :customer, dependent: :destroy,
+class Book < ApplicationRecord
+ belongs_to :author, touch: :books_updated_at,
counter_cache: true
end
```
@@ -849,15 +889,15 @@ The `belongs_to` association supports these options:
##### `:autosave`
-If you set the `:autosave` option to `true`, Rails will save any loaded members and destroy members that are marked for destruction whenever you save the parent object.
+If you set the `:autosave` option to `true`, Rails will save any loaded association members and destroy members that are marked for destruction whenever you save the parent object. Setting `:autosave` to `false` is not the same as not setting the `:autosave` option. If the `:autosave` option is not present, then new associated objects will be saved, but updated associated objects will not be saved.
##### `:class_name`
-If the name of the other model cannot be derived from the association name, you can use the `:class_name` option to supply the model name. For example, if an order belongs to a customer, but the actual name of the model containing customers is `Patron`, you'd set things up this way:
+If the name of the other model cannot be derived from the association name, you can use the `:class_name` option to supply the model name. For example, if a book belongs to an author, but the actual name of the model containing authors is `Patron`, you'd set things up this way:
```ruby
-class Order < ActiveRecord::Base
- belongs_to :customer, class_name: "Patron"
+class Book < ApplicationRecord
+ belongs_to :author, class_name: "Patron"
end
```
@@ -866,22 +906,22 @@ end
The `:counter_cache` option can be used to make finding the number of belonging objects more efficient. Consider these models:
```ruby
-class Order < ActiveRecord::Base
- belongs_to :customer
+class Book < ApplicationRecord
+ belongs_to :author
end
-class Customer < ActiveRecord::Base
- has_many :orders
+class Author < ApplicationRecord
+ has_many :books
end
```
-With these declarations, asking for the value of `@customer.orders.size` requires making a call to the database to perform a `COUNT(*)` query. To avoid this call, you can add a counter cache to the _belonging_ model:
+With these declarations, asking for the value of `@author.books.size` requires making a call to the database to perform a `COUNT(*)` query. To avoid this call, you can add a counter cache to the _belonging_ model:
```ruby
-class Order < ActiveRecord::Base
- belongs_to :customer, counter_cache: true
+class Book < ApplicationRecord
+ belongs_to :author, counter_cache: true
end
-class Customer < ActiveRecord::Base
- has_many :orders
+class Author < ApplicationRecord
+ has_many :books
end
```
@@ -890,22 +930,22 @@ With this declaration, Rails will keep the cache value up to date, and then retu
Although the `:counter_cache` option is specified on the model that includes
the `belongs_to` declaration, the actual column must be added to the
_associated_ (`has_many`) model. In the case above, you would need to add a
-column named `orders_count` to the `Customer` model.
+column named `books_count` to the `Author` model.
You can override the default column name by specifying a custom column name in
the `counter_cache` declaration instead of `true`. For example, to use
-`count_of_orders` instead of `orders_count`:
+`count_of_books` instead of `books_count`:
```ruby
-class Order < ActiveRecord::Base
- belongs_to :customer, counter_cache: :count_of_orders
+class Book < ApplicationRecord
+ belongs_to :author, counter_cache: :count_of_books
end
-class Customer < ActiveRecord::Base
- has_many :orders
+class Author < ApplicationRecord
+ has_many :books
end
```
-NOTE: You only need to specify the :counter_cache option on the `belongs_to`
+NOTE: You only need to specify the `:counter_cache` option on the `belongs_to`
side of the association.
Counter cache columns are added to the containing model's list of read-only attributes through `attr_readonly`.
@@ -925,8 +965,8 @@ WARNING: You should not specify this option on a `belongs_to` association that i
By convention, Rails assumes that the column used to hold the foreign key on this model is the name of the association with the suffix `_id` added. The `:foreign_key` option lets you set the name of the foreign key directly:
```ruby
-class Order < ActiveRecord::Base
- belongs_to :customer, class_name: "Patron",
+class Book < ApplicationRecord
+ belongs_to :author, class_name: "Patron",
foreign_key: "patron_id"
end
```
@@ -941,11 +981,11 @@ of its tables. The `:primary_key` option allows you to specify a different colum
For example, given we have a `users` table with `guid` as the primary key. If we want a separate `todos` table to hold the foreign key `user_id` in the `guid` column, then we can use `primary_key` to achieve this like so:
```ruby
-class User < ActiveRecord::Base
+class User < ApplicationRecord
self.primary_key = 'guid' # primary key is guid and not id
end
-class Todo < ActiveRecord::Base
+class Todo < ApplicationRecord
belongs_to :user, primary_key: 'guid'
end
```
@@ -955,15 +995,15 @@ When we execute `@user.todos.create` then the `@todo` record will have its
##### `:inverse_of`
-The `:inverse_of` option specifies the name of the `has_many` or `has_one` association that is the inverse of this association. Does not work in combination with the `:polymorphic` options.
+The `:inverse_of` option specifies the name of the `has_many` or `has_one` association that is the inverse of this association.
```ruby
-class Customer < ActiveRecord::Base
- has_many :orders, inverse_of: :customer
+class Author < ApplicationRecord
+ has_many :books, inverse_of: :author
end
-class Order < ActiveRecord::Base
- belongs_to :customer, inverse_of: :orders
+class Book < ApplicationRecord
+ belongs_to :author, inverse_of: :books
end
```
@@ -976,20 +1016,20 @@ Passing `true` to the `:polymorphic` option indicates that this is a polymorphic
If you set the `:touch` option to `true`, then the `updated_at` or `updated_on` timestamp on the associated object will be set to the current time whenever this object is saved or destroyed:
```ruby
-class Order < ActiveRecord::Base
- belongs_to :customer, touch: true
+class Book < ApplicationRecord
+ belongs_to :author, touch: true
end
-class Customer < ActiveRecord::Base
- has_many :orders
+class Author < ApplicationRecord
+ has_many :books
end
```
-In this case, saving or destroying an order will update the timestamp on the associated customer. You can also specify a particular timestamp attribute to update:
+In this case, saving or destroying a book will update the timestamp on the associated author. You can also specify a particular timestamp attribute to update:
```ruby
-class Order < ActiveRecord::Base
- belongs_to :customer, touch: :orders_updated_at
+class Book < ApplicationRecord
+ belongs_to :author, touch: :books_updated_at
end
```
@@ -1007,9 +1047,8 @@ object won't be validated. By default, this option is set to `false`.
There may be times when you wish to customize the query used by `belongs_to`. Such customizations can be achieved via a scope block. For example:
```ruby
-class Order < ActiveRecord::Base
- belongs_to :customer, -> { where active: true },
- dependent: :destroy
+class Book < ApplicationRecord
+ belongs_to :author, -> { where active: true }
end
```
@@ -1025,8 +1064,8 @@ You can use any of the standard [querying methods](active_record_querying.html)
The `where` method lets you specify the conditions that the associated object must meet.
```ruby
-class Order < ActiveRecord::Base
- belongs_to :customer, -> { where active: true }
+class Book < ApplicationRecord
+ belongs_to :author, -> { where active: true }
end
```
@@ -1035,38 +1074,38 @@ end
You can use the `includes` method to specify second-order associations that should be eager-loaded when this association is used. For example, consider these models:
```ruby
-class LineItem < ActiveRecord::Base
- belongs_to :order
+class Chapter < ApplicationRecord
+ belongs_to :book
end
-class Order < ActiveRecord::Base
- belongs_to :customer
- has_many :line_items
+class Book < ApplicationRecord
+ belongs_to :author
+ has_many :chapters
end
-class Customer < ActiveRecord::Base
- has_many :orders
+class Author < ApplicationRecord
+ has_many :books
end
```
-If you frequently retrieve customers directly from line items (`@line_item.order.customer`), then you can make your code somewhat more efficient by including customers in the association from line items to orders:
+If you frequently retrieve authors directly from chapters (`@chapter.book.author`), then you can make your code somewhat more efficient by including authors in the association from chapters to books:
```ruby
-class LineItem < ActiveRecord::Base
- belongs_to :order, -> { includes :customer }
+class Chapter < ApplicationRecord
+ belongs_to :book, -> { includes :author }
end
-class Order < ActiveRecord::Base
- belongs_to :customer
- has_many :line_items
+class Book < ApplicationRecord
+ belongs_to :author
+ has_many :chapters
end
-class Customer < ActiveRecord::Base
- has_many :orders
+class Author < ApplicationRecord
+ has_many :books
end
```
-NOTE: There's no need to use `includes` for immediate associations - that is, if you have `Order belongs_to :customer`, then the customer is eager-loaded automatically when it's needed.
+NOTE: There's no need to use `includes` for immediate associations - that is, if you have `Book belongs_to :author`, then the author is eager-loaded automatically when it's needed.
##### `readonly`
@@ -1083,8 +1122,8 @@ TIP: If you use the `select` method on a `belongs_to` association, you should al
You can see if any associated objects exist by using the `association.nil?` method:
```ruby
-if @order.customer.nil?
- @msg = "No customer found for this order"
+if @book.author.nil?
+ @msg = "No author found for this book"
end
```
@@ -1098,18 +1137,19 @@ The `has_one` association creates a one-to-one match with another model. In data
#### Methods Added by `has_one`
-When you declare a `has_one` association, the declaring class automatically gains five methods related to the association:
+When you declare a `has_one` association, the declaring class automatically gains 6 methods related to the association:
-* `association(force_reload = false)`
+* `association`
* `association=(associate)`
* `build_association(attributes = {})`
* `create_association(attributes = {})`
* `create_association!(attributes = {})`
+* `reload_association`
In all of these methods, `association` is replaced with the symbol passed as the first argument to `has_one`. For example, given the declaration:
```ruby
-class Supplier < ActiveRecord::Base
+class Supplier < ApplicationRecord
has_one :account
end
```
@@ -1122,11 +1162,12 @@ account=
build_account
create_account
create_account!
+reload_account
```
NOTE: When initializing a new `has_one` or `belongs_to` association you must use the `build_` prefix to build the association, rather than the `association.build` method that would be used for `has_many` or `has_and_belongs_to_many` associations. To create one, use the `create_` prefix.
-##### `association(force_reload = false)`
+##### `association`
The `association` method returns the associated object, if any. If no associated object is found, it returns `nil`.
@@ -1134,7 +1175,11 @@ The `association` method returns the associated object, if any. If no associated
@account = @supplier.account
```
-If the associated object has already been retrieved from the database for this object, the cached version will be returned. To override this behavior (and force a database read), pass `true` as the `force_reload` argument.
+If the associated object has already been retrieved from the database for this object, the cached version will be returned. To override this behavior (and force a database read), call `#reload_association` on the parent object.
+
+```ruby
+@account = @supplier.reload_account
+```
##### `association=(associate)`
@@ -1169,7 +1214,7 @@ Does the same as `create_association` above, but raises `ActiveRecord::RecordInv
While Rails uses intelligent defaults that will work well in most situations, there may be times when you want to customize the behavior of the `has_one` association reference. Such customizations can easily be accomplished by passing options when you create the association. For example, this association uses two such options:
```ruby
-class Supplier < ActiveRecord::Base
+class Supplier < ApplicationRecord
has_one :account, class_name: "Billing", dependent: :nullify
end
```
@@ -1194,14 +1239,14 @@ Setting the `:as` option indicates that this is a polymorphic association. Polym
##### `:autosave`
-If you set the `:autosave` option to `true`, Rails will save any loaded members and destroy members that are marked for destruction whenever you save the parent object.
+If you set the `:autosave` option to `true`, Rails will save any loaded association members and destroy members that are marked for destruction whenever you save the parent object. Setting `:autosave` to `false` is not the same as not setting the `:autosave` option. If the `:autosave` option is not present, then new associated objects will be saved, but updated associated objects will not be saved.
##### `:class_name`
If the name of the other model cannot be derived from the association name, you can use the `:class_name` option to supply the model name. For example, if a supplier has an account, but the actual name of the model containing accounts is `Billing`, you'd set things up this way:
```ruby
-class Supplier < ActiveRecord::Base
+class Supplier < ApplicationRecord
has_one :account, class_name: "Billing"
end
```
@@ -1213,7 +1258,7 @@ Controls what happens to the associated object when its owner is destroyed:
* `:destroy` causes the associated object to also be destroyed
* `:delete` causes the associated object to be deleted directly from the database (so callbacks will not execute)
* `:nullify` causes the foreign key to be set to `NULL`. Callbacks are not executed.
-* `:restrict_with_exception` causes an exception to be raised if there is an associated record
+* `:restrict_with_exception` causes an `ActiveRecord::DeleteRestrictionError` exception to be raised if there is an associated record
* `:restrict_with_error` causes an error to be added to the owner if there is an associated object
It's necessary not to set or leave `:nullify` option for those associations
@@ -1227,7 +1272,7 @@ unallowed `NULL` value.
By convention, Rails assumes that the column used to hold the foreign key on the other model is the name of this model with the suffix `_id` added. The `:foreign_key` option lets you set the name of the foreign key directly:
```ruby
-class Supplier < ActiveRecord::Base
+class Supplier < ApplicationRecord
has_one :account, foreign_key: "supp_id"
end
```
@@ -1236,14 +1281,14 @@ TIP: In any case, Rails will not create foreign key columns for you. You need to
##### `:inverse_of`
-The `:inverse_of` option specifies the name of the `belongs_to` association that is the inverse of this association. Does not work in combination with the `:through` or `:as` options.
+The `:inverse_of` option specifies the name of the `belongs_to` association that is the inverse of this association.
```ruby
-class Supplier < ActiveRecord::Base
+class Supplier < ApplicationRecord
has_one :account, inverse_of: :supplier
end
-class Account < ActiveRecord::Base
+class Account < ApplicationRecord
belongs_to :supplier, inverse_of: :account
end
```
@@ -1273,7 +1318,7 @@ If you set the `:validate` option to `true`, then associated objects will be val
There may be times when you wish to customize the query used by `has_one`. Such customizations can be achieved via a scope block. For example:
```ruby
-class Supplier < ActiveRecord::Base
+class Supplier < ApplicationRecord
has_one :account, -> { where active: true }
end
```
@@ -1290,7 +1335,7 @@ You can use any of the standard [querying methods](active_record_querying.html)
The `where` method lets you specify the conditions that the associated object must meet.
```ruby
-class Supplier < ActiveRecord::Base
+class Supplier < ApplicationRecord
has_one :account, -> { where "confirmed = 1" }
end
```
@@ -1300,16 +1345,16 @@ end
You can use the `includes` method to specify second-order associations that should be eager-loaded when this association is used. For example, consider these models:
```ruby
-class Supplier < ActiveRecord::Base
+class Supplier < ApplicationRecord
has_one :account
end
-class Account < ActiveRecord::Base
+class Account < ApplicationRecord
belongs_to :supplier
belongs_to :representative
end
-class Representative < ActiveRecord::Base
+class Representative < ApplicationRecord
has_many :accounts
end
```
@@ -1317,16 +1362,16 @@ end
If you frequently retrieve representatives directly from suppliers (`@supplier.account.representative`), then you can make your code somewhat more efficient by including representatives in the association from suppliers to accounts:
```ruby
-class Supplier < ActiveRecord::Base
+class Supplier < ApplicationRecord
has_one :account, -> { includes :representative }
end
-class Account < ActiveRecord::Base
+class Account < ApplicationRecord
belongs_to :supplier
belongs_to :representative
end
-class Representative < ActiveRecord::Base
+class Representative < ApplicationRecord
has_many :accounts
end
```
@@ -1357,7 +1402,7 @@ If either of these saves fails due to validation errors, then the assignment sta
If the parent object (the one declaring the `has_one` association) is unsaved (that is, `new_record?` returns `true`) then the child objects are not saved. They will automatically when the parent object is saved.
-If you want to assign an object to a `has_one` association without saving the object, use the `association.build` method.
+If you want to assign an object to a `has_one` association without saving the object, use the `build_association` method.
### `has_many` Association Reference
@@ -1365,9 +1410,9 @@ The `has_many` association creates a one-to-many relationship with another model
#### Methods Added by `has_many`
-When you declare a `has_many` association, the declaring class automatically gains 16 methods related to the association:
+When you declare a `has_many` association, the declaring class automatically gains 17 methods related to the association:
-* `collection(force_reload = false)`
+* `collection`
* `collection<<(object, ...)`
* `collection.delete(object, ...)`
* `collection.destroy(object, ...)`
@@ -1383,42 +1428,44 @@ When you declare a `has_many` association, the declaring class automatically gai
* `collection.build(attributes = {}, ...)`
* `collection.create(attributes = {})`
* `collection.create!(attributes = {})`
+* `collection.reload`
In all of these methods, `collection` is replaced with the symbol passed as the first argument to `has_many`, and `collection_singular` is replaced with the singularized version of that symbol. For example, given the declaration:
```ruby
-class Customer < ActiveRecord::Base
- has_many :orders
+class Author < ApplicationRecord
+ has_many :books
end
```
-Each instance of the `Customer` model will have these methods:
+Each instance of the `Author` model will have these methods:
```ruby
-orders(force_reload = false)
-orders<<(object, ...)
-orders.delete(object, ...)
-orders.destroy(object, ...)
-orders=(objects)
-order_ids
-order_ids=(ids)
-orders.clear
-orders.empty?
-orders.size
-orders.find(...)
-orders.where(...)
-orders.exists?(...)
-orders.build(attributes = {}, ...)
-orders.create(attributes = {})
-orders.create!(attributes = {})
+books
+books<<(object, ...)
+books.delete(object, ...)
+books.destroy(object, ...)
+books=(objects)
+book_ids
+book_ids=(ids)
+books.clear
+books.empty?
+books.size
+books.find(...)
+books.where(...)
+books.exists?(...)
+books.build(attributes = {}, ...)
+books.create(attributes = {})
+books.create!(attributes = {})
+books.reload
```
-##### `collection(force_reload = false)`
+##### `collection`
-The `collection` method returns an array of all of the associated objects. If there are no associated objects, it returns an empty array.
+The `collection` method returns a Relation of all of the associated objects. If there are no associated objects, it returns an empty Relation.
```ruby
-@orders = @customer.orders
+@books = @author.books
```
##### `collection<<(object, ...)`
@@ -1426,7 +1473,7 @@ The `collection` method returns an array of all of the associated objects. If th
The `collection<<` method adds one or more objects to the collection by setting their foreign keys to the primary key of the calling model.
```ruby
-@customer.orders << @order1
+@author.books << @book1
```
##### `collection.delete(object, ...)`
@@ -1434,7 +1481,7 @@ The `collection<<` method adds one or more objects to the collection by setting
The `collection.delete` method removes one or more objects from the collection by setting their foreign keys to `NULL`.
```ruby
-@customer.orders.delete(@order1)
+@author.books.delete(@book1)
```
WARNING: Additionally, objects will be destroyed if they're associated with `dependent: :destroy`, and deleted if they're associated with `dependent: :delete_all`.
@@ -1444,33 +1491,33 @@ WARNING: Additionally, objects will be destroyed if they're associated with `dep
The `collection.destroy` method removes one or more objects from the collection by running `destroy` on each object.
```ruby
-@customer.orders.destroy(@order1)
+@author.books.destroy(@book1)
```
WARNING: Objects will _always_ be removed from the database, ignoring the `:dependent` option.
##### `collection=(objects)`
-The `collection=` method makes the collection contain only the supplied objects, by adding and deleting as appropriate.
+The `collection=` method makes the collection contain only the supplied objects, by adding and deleting as appropriate. The changes are persisted to the database.
##### `collection_singular_ids`
The `collection_singular_ids` method returns an array of the ids of the objects in the collection.
```ruby
-@order_ids = @customer.order_ids
+@book_ids = @author.book_ids
```
##### `collection_singular_ids=(ids)`
-The `collection_singular_ids=` method makes the collection contain only the objects identified by the supplied primary key values, by adding and deleting as appropriate.
+The `collection_singular_ids=` method makes the collection contain only the objects identified by the supplied primary key values, by adding and deleting as appropriate. The changes are persisted to the database.
##### `collection.clear`
The `collection.clear` method removes all objects from the collection according to the strategy specified by the `dependent` option. If no option is given, it follows the default strategy. The default strategy for `has_many :through` associations is `delete_all`, and for `has_many` associations is to set the foreign keys to `NULL`.
```ruby
-@customer.orders.clear
+@author.books.clear
```
WARNING: Objects will be deleted if they're associated with `dependent: :destroy`,
@@ -1481,8 +1528,8 @@ just like `dependent: :delete_all`.
The `collection.empty?` method returns `true` if the collection does not contain any associated objects.
```erb
-<% if @customer.orders.empty? %>
- No Orders Found
+<% if @author.books.empty? %>
+ No Books Found
<% end %>
```
@@ -1491,15 +1538,16 @@ The `collection.empty?` method returns `true` if the collection does not contain
The `collection.size` method returns the number of objects in the collection.
```ruby
-@order_count = @customer.orders.size
+@book_count = @author.books.size
```
##### `collection.find(...)`
-The `collection.find` method finds objects within the collection. It uses the same syntax and options as `ActiveRecord::Base.find`.
+The `collection.find` method finds objects within the collection. It uses the same syntax and options as
+[`ActiveRecord::Base.find`](http://api.rubyonrails.org/classes/ActiveRecord/FinderMethods.html#method-i-find).
```ruby
-@open_orders = @customer.orders.find(1)
+@available_book = @author.books.find(1)
```
##### `collection.where(...)`
@@ -1507,8 +1555,8 @@ The `collection.find` method finds objects within the collection. It uses the sa
The `collection.where` method finds objects within the collection based on the conditions supplied but the objects are loaded lazily meaning that the database is queried only when the object(s) are accessed.
```ruby
-@open_orders = @customer.orders.where(open: true) # No query yet
-@open_order = @open_orders.first # Now the database will be queried
+@available_books = @author.books.where(available: true) # No query yet
+@available_book = @available_books.first # Now the database will be queried
```
##### `collection.exists?(...)`
@@ -1522,12 +1570,12 @@ conditions exists in the collection. It uses the same syntax and options as
The `collection.build` method returns a single or array of new objects of the associated type. The object(s) will be instantiated from the passed attributes, and the link through their foreign key will be created, but the associated objects will _not_ yet be saved.
```ruby
-@order = @customer.orders.build(order_date: Time.now,
- order_number: "A12345")
+@book = @author.books.build(published_at: Time.now,
+ book_number: "A12345")
-@orders = @customer.orders.build([
- { order_date: Time.now, order_number: "A12346" },
- { order_date: Time.now, order_number: "A12347" }
+@books = @author.books.build([
+ { published_at: Time.now, book_number: "A12346" },
+ { published_at: Time.now, book_number: "A12347" }
])
```
@@ -1536,12 +1584,12 @@ The `collection.build` method returns a single or array of new objects of the as
The `collection.create` method returns a single or array of new objects of the associated type. The object(s) will be instantiated from the passed attributes, the link through its foreign key will be created, and, once it passes all of the validations specified on the associated model, the associated object _will_ be saved.
```ruby
-@order = @customer.orders.create(order_date: Time.now,
- order_number: "A12345")
+@book = @author.books.create(published_at: Time.now,
+ book_number: "A12345")
-@orders = @customer.orders.create([
- { order_date: Time.now, order_number: "A12346" },
- { order_date: Time.now, order_number: "A12347" }
+@books = @author.books.create([
+ { published_at: Time.now, book_number: "A12346" },
+ { published_at: Time.now, book_number: "A12347" }
])
```
@@ -1549,13 +1597,21 @@ The `collection.create` method returns a single or array of new objects of the a
Does the same as `collection.create` above, but raises `ActiveRecord::RecordInvalid` if the record is invalid.
+##### `collection.reload`
+
+The `collection.reload` method returns a Relation of all of the associated objects, forcing a database read. If there are no associated objects, it returns an empty Relation.
+
+```ruby
+@books = @author.books.reload
+```
+
#### Options for `has_many`
While Rails uses intelligent defaults that will work well in most situations, there may be times when you want to customize the behavior of the `has_many` association reference. Such customizations can easily be accomplished by passing options when you create the association. For example, this association uses two such options:
```ruby
-class Customer < ActiveRecord::Base
- has_many :orders, dependent: :delete_all, validate: false
+class Author < ApplicationRecord
+ has_many :books, dependent: :delete_all, validate: false
end
```
@@ -1580,15 +1636,15 @@ Setting the `:as` option indicates that this is a polymorphic association, as di
##### `:autosave`
-If you set the `:autosave` option to `true`, Rails will save any loaded members and destroy members that are marked for destruction whenever you save the parent object.
+If you set the `:autosave` option to `true`, Rails will save any loaded association members and destroy members that are marked for destruction whenever you save the parent object. Setting `:autosave` to `false` is not the same as not setting the `:autosave` option. If the `:autosave` option is not present, then new associated objects will be saved, but updated associated objects will not be saved.
##### `:class_name`
-If the name of the other model cannot be derived from the association name, you can use the `:class_name` option to supply the model name. For example, if a customer has many orders, but the actual name of the model containing orders is `Transaction`, you'd set things up this way:
+If the name of the other model cannot be derived from the association name, you can use the `:class_name` option to supply the model name. For example, if an author has many books, but the actual name of the model containing books is `Transaction`, you'd set things up this way:
```ruby
-class Customer < ActiveRecord::Base
- has_many :orders, class_name: "Transaction"
+class Author < ApplicationRecord
+ has_many :books, class_name: "Transaction"
end
```
@@ -1603,16 +1659,18 @@ Controls what happens to the associated objects when their owner is destroyed:
* `:destroy` causes all the associated objects to also be destroyed
* `:delete_all` causes all the associated objects to be deleted directly from the database (so callbacks will not execute)
* `:nullify` causes the foreign keys to be set to `NULL`. Callbacks are not executed.
-* `:restrict_with_exception` causes an exception to be raised if there are any associated records
+* `:restrict_with_exception` causes an `ActiveRecord::DeleteRestrictionError` exception to be raised if there are any associated records
* `:restrict_with_error` causes an error to be added to the owner if there are any associated objects
+The `:destroy` and `:delete_all` options also affect the semantics of the `collection.delete` and `collection=` methods by causing them to destroy associated objects when they are removed from the collection.
+
##### `:foreign_key`
By convention, Rails assumes that the column used to hold the foreign key on the other model is the name of this model with the suffix `_id` added. The `:foreign_key` option lets you set the name of the foreign key directly:
```ruby
-class Customer < ActiveRecord::Base
- has_many :orders, foreign_key: "cust_id"
+class Author < ApplicationRecord
+ has_many :books, foreign_key: "cust_id"
end
```
@@ -1620,15 +1678,15 @@ TIP: In any case, Rails will not create foreign key columns for you. You need to
##### `:inverse_of`
-The `:inverse_of` option specifies the name of the `belongs_to` association that is the inverse of this association. Does not work in combination with the `:through` or `:as` options.
+The `:inverse_of` option specifies the name of the `belongs_to` association that is the inverse of this association.
```ruby
-class Customer < ActiveRecord::Base
- has_many :orders, inverse_of: :customer
+class Author < ApplicationRecord
+ has_many :books, inverse_of: :author
end
-class Order < ActiveRecord::Base
- belongs_to :customer, inverse_of: :orders
+class Book < ApplicationRecord
+ belongs_to :author, inverse_of: :books
end
```
@@ -1642,7 +1700,7 @@ hold the `guid` column value as the foreign key and not `id`
value. This can be achieved like this:
```ruby
-class User < ActiveRecord::Base
+class User < ApplicationRecord
has_many :todos, primary_key: :guid
end
```
@@ -1672,8 +1730,8 @@ If you set the `:validate` option to `false`, then associated objects will not b
There may be times when you wish to customize the query used by `has_many`. Such customizations can be achieved via a scope block. For example:
```ruby
-class Customer < ActiveRecord::Base
- has_many :orders, -> { where processed: true }
+class Author < ApplicationRecord
+ has_many :books, -> { where processed: true }
end
```
@@ -1695,22 +1753,22 @@ You can use any of the standard [querying methods](active_record_querying.html)
The `where` method lets you specify the conditions that the associated object must meet.
```ruby
-class Customer < ActiveRecord::Base
- has_many :confirmed_orders, -> { where "confirmed = 1" },
- class_name: "Order"
+class Author < ApplicationRecord
+ has_many :confirmed_books, -> { where "confirmed = 1" },
+ class_name: "Book"
end
```
You can also set conditions via a hash:
```ruby
-class Customer < ActiveRecord::Base
- has_many :confirmed_orders, -> { where confirmed: true },
- class_name: "Order"
+class Author < ApplicationRecord
+ has_many :confirmed_books, -> { where confirmed: true },
+ class_name: "Book"
end
```
-If you use a hash-style `where` option, then record creation via this association will be automatically scoped using the hash. In this case, using `@customer.confirmed_orders.create` or `@customer.confirmed_orders.build` will create orders where the confirmed column has the value `true`.
+If you use a hash-style `where` option, then record creation via this association will be automatically scoped using the hash. In this case, using `@author.confirmed_books.create` or `@author.confirmed_books.build` will create books where the confirmed column has the value `true`.
##### `extending`
@@ -1721,9 +1779,9 @@ The `extending` method specifies a named module to extend the association proxy.
The `group` method supplies an attribute name to group the result set by, using a `GROUP BY` clause in the finder SQL.
```ruby
-class Customer < ActiveRecord::Base
- has_many :line_items, -> { group 'orders.id' },
- through: :orders
+class Author < ApplicationRecord
+ has_many :chapters, -> { group 'books.id' },
+ through: :books
end
```
@@ -1732,34 +1790,34 @@ end
You can use the `includes` method to specify second-order associations that should be eager-loaded when this association is used. For example, consider these models:
```ruby
-class Customer < ActiveRecord::Base
- has_many :orders
+class Author < ApplicationRecord
+ has_many :books
end
-class Order < ActiveRecord::Base
- belongs_to :customer
- has_many :line_items
+class Book < ApplicationRecord
+ belongs_to :author
+ has_many :chapters
end
-class LineItem < ActiveRecord::Base
- belongs_to :order
+class Chapter < ApplicationRecord
+ belongs_to :book
end
```
-If you frequently retrieve line items directly from customers (`@customer.orders.line_items`), then you can make your code somewhat more efficient by including line items in the association from customers to orders:
+If you frequently retrieve chapters directly from authors (`@author.books.chapters`), then you can make your code somewhat more efficient by including chapters in the association from authors to books:
```ruby
-class Customer < ActiveRecord::Base
- has_many :orders, -> { includes :line_items }
+class Author < ApplicationRecord
+ has_many :books, -> { includes :chapters }
end
-class Order < ActiveRecord::Base
- belongs_to :customer
- has_many :line_items
+class Book < ApplicationRecord
+ belongs_to :author
+ has_many :chapters
end
-class LineItem < ActiveRecord::Base
- belongs_to :order
+class Chapter < ApplicationRecord
+ belongs_to :book
end
```
@@ -1768,10 +1826,10 @@ end
The `limit` method lets you restrict the total number of objects that will be fetched through an association.
```ruby
-class Customer < ActiveRecord::Base
- has_many :recent_orders,
- -> { order('order_date desc').limit(100) },
- class_name: "Order",
+class Author < ApplicationRecord
+ has_many :recent_books,
+ -> { order('published_at desc').limit(100) },
+ class_name: "Book"
end
```
@@ -1784,8 +1842,8 @@ The `offset` method lets you specify the starting offset for fetching objects vi
The `order` method dictates the order in which associated objects will be received (in the syntax used by an SQL `ORDER BY` clause).
```ruby
-class Customer < ActiveRecord::Base
- has_many :orders, -> { order "date_confirmed DESC" }
+class Author < ApplicationRecord
+ has_many :books, -> { order "date_confirmed DESC" }
end
```
@@ -1805,7 +1863,7 @@ Use the `distinct` method to keep the collection free of duplicates. This is
mostly useful together with the `:through` option.
```ruby
-class Person < ActiveRecord::Base
+class Person < ApplicationRecord
has_many :readings
has_many :articles, through: :readings
end
@@ -1815,7 +1873,7 @@ article = Article.create(name: 'a1')
person.articles << article
person.articles << article
person.articles.inspect # => [#<Article id: 5, name: "a1">, #<Article id: 5, name: "a1">]
-Reading.all.inspect # => [#<Reading id: 12, person_id: 5, article_id: 5>, #<Reading id: 13, person_id: 5, article_id: 5>]
+Reading.all.inspect # => [#<Reading id: 12, person_id: 5, article_id: 5>, #<Reading id: 13, person_id: 5, article_id: 5>]
```
In the above case there are two readings and `person.articles` brings out both of
@@ -1834,7 +1892,7 @@ article = Article.create(name: 'a1')
person.articles << article
person.articles << article
person.articles.inspect # => [#<Article id: 7, name: "a1">]
-Reading.all.inspect # => [#<Reading id: 16, person_id: 7, article_id: 7>, #<Reading id: 17, person_id: 7, article_id: 7>]
+Reading.all.inspect # => [#<Reading id: 16, person_id: 7, article_id: 7>, #<Reading id: 17, person_id: 7, article_id: 7>]
```
In the above case there are still two readings. However `person.articles` shows
@@ -1844,11 +1902,21 @@ If you want to make sure that, upon insertion, all of the records in the
persisted association are distinct (so that you can be sure that when you
inspect the association that you will never find duplicate records), you should
add a unique index on the table itself. For example, if you have a table named
-`person_articles` and you want to make sure all the articles are unique, you could
-add the following in a migration:
+`readings` and you want to make sure the articles can only be added to a person once,
+you could add the following in a migration:
```ruby
-add_index :person_articles, :article, unique: true
+add_index :readings, [:person_id, :article_id], unique: true
+```
+
+Once you have this unique index, attempting to add the article to a person twice
+will raise an `ActiveRecord::RecordNotUnique` error:
+
+```ruby
+person = Person.create(name: 'Honda')
+article = Article.create(name: 'a1')
+person.articles << article
+person.articles << article # => ActiveRecord::RecordNotUnique
```
Note that checking for uniqueness using something like `include?` is subject
@@ -1877,9 +1945,9 @@ The `has_and_belongs_to_many` association creates a many-to-many relationship wi
#### Methods Added by `has_and_belongs_to_many`
-When you declare a `has_and_belongs_to_many` association, the declaring class automatically gains 16 methods related to the association:
+When you declare a `has_and_belongs_to_many` association, the declaring class automatically gains 17 methods related to the association:
-* `collection(force_reload = false)`
+* `collection`
* `collection<<(object, ...)`
* `collection.delete(object, ...)`
* `collection.destroy(object, ...)`
@@ -1895,11 +1963,12 @@ When you declare a `has_and_belongs_to_many` association, the declaring class au
* `collection.build(attributes = {})`
* `collection.create(attributes = {})`
* `collection.create!(attributes = {})`
+* `collection.reload`
In all of these methods, `collection` is replaced with the symbol passed as the first argument to `has_and_belongs_to_many`, and `collection_singular` is replaced with the singularized version of that symbol. For example, given the declaration:
```ruby
-class Part < ActiveRecord::Base
+class Part < ApplicationRecord
has_and_belongs_to_many :assemblies
end
```
@@ -1907,7 +1976,7 @@ end
Each instance of the `Part` model will have these methods:
```ruby
-assemblies(force_reload = false)
+assemblies
assemblies<<(object, ...)
assemblies.delete(object, ...)
assemblies.destroy(object, ...)
@@ -1923,6 +1992,7 @@ assemblies.exists?(...)
assemblies.build(attributes = {}, ...)
assemblies.create(attributes = {})
assemblies.create!(attributes = {})
+assemblies.reload
```
##### Additional Column Methods
@@ -1932,9 +2002,9 @@ If the join table for a `has_and_belongs_to_many` association has additional col
WARNING: The use of extra attributes on the join table in a `has_and_belongs_to_many` association is deprecated. If you require this sort of complex behavior on the table that joins two models in a many-to-many relationship, you should use a `has_many :through` association instead of `has_and_belongs_to_many`.
-##### `collection(force_reload = false)`
+##### `collection`
-The `collection` method returns an array of all of the associated objects. If there are no associated objects, it returns an empty array.
+The `collection` method returns a Relation of all of the associated objects. If there are no associated objects, it returns an empty Relation.
```ruby
@assemblies = @part.assemblies
@@ -1958,11 +2028,9 @@ The `collection.delete` method removes one or more objects from the collection b
@part.assemblies.delete(@assembly1)
```
-WARNING: This does not trigger callbacks on the join records.
-
##### `collection.destroy(object, ...)`
-The `collection.destroy` method removes one or more objects from the collection by running `destroy` on each record in the join table, including running callbacks. This does not destroy the objects.
+The `collection.destroy` method removes one or more objects from the collection by deleting records in the join table. This does not destroy the objects.
```ruby
@part.assemblies.destroy(@assembly1)
@@ -1970,7 +2038,7 @@ The `collection.destroy` method removes one or more objects from the collection
##### `collection=(objects)`
-The `collection=` method makes the collection contain only the supplied objects, by adding and deleting as appropriate.
+The `collection=` method makes the collection contain only the supplied objects, by adding and deleting as appropriate. The changes are persisted to the database.
##### `collection_singular_ids`
@@ -1982,7 +2050,7 @@ The `collection_singular_ids` method returns an array of the ids of the objects
##### `collection_singular_ids=(ids)`
-The `collection_singular_ids=` method makes the collection contain only the objects identified by the supplied primary key values, by adding and deleting as appropriate.
+The `collection_singular_ids=` method makes the collection contain only the objects identified by the supplied primary key values, by adding and deleting as appropriate. The changes are persisted to the database.
##### `collection.clear`
@@ -2008,7 +2076,8 @@ The `collection.size` method returns the number of objects in the collection.
##### `collection.find(...)`
-The `collection.find` method finds objects within the collection. It uses the same syntax and options as `ActiveRecord::Base.find`. It also adds the additional condition that the object must be in the collection.
+The `collection.find` method finds objects within the collection. It uses the same syntax and options as
+[`ActiveRecord::Base.find`](http://api.rubyonrails.org/classes/ActiveRecord/FinderMethods.html#method-i-find).
```ruby
@assembly = @part.assemblies.find(1)
@@ -2016,7 +2085,7 @@ The `collection.find` method finds objects within the collection. It uses the sa
##### `collection.where(...)`
-The `collection.where` method finds objects within the collection based on the conditions supplied but the objects are loaded lazily meaning that the database is queried only when the object(s) are accessed. It also adds the additional condition that the object must be in the collection.
+The `collection.where` method finds objects within the collection based on the conditions supplied but the objects are loaded lazily meaning that the database is queried only when the object(s) are accessed.
```ruby
@new_assemblies = @part.assemblies.where("created_at > ?", 2.days.ago)
@@ -2048,12 +2117,20 @@ The `collection.create` method returns a new object of the associated type. This
Does the same as `collection.create`, but raises `ActiveRecord::RecordInvalid` if the record is invalid.
+##### `collection.reload`
+
+The `collection.reload` method returns a Relation of all of the associated objects, forcing a database read. If there are no associated objects, it returns an empty Relation.
+
+```ruby
+@assemblies = @part.assemblies.reload
+```
+
#### Options for `has_and_belongs_to_many`
While Rails uses intelligent defaults that will work well in most situations, there may be times when you want to customize the behavior of the `has_and_belongs_to_many` association reference. Such customizations can easily be accomplished by passing options when you create the association. For example, this association uses two such options:
```ruby
-class Parts < ActiveRecord::Base
+class Parts < ApplicationRecord
has_and_belongs_to_many :assemblies, -> { readonly },
autosave: true
end
@@ -2075,7 +2152,7 @@ By convention, Rails assumes that the column in the join table used to hold the
TIP: The `:foreign_key` and `:association_foreign_key` options are useful when setting up a many-to-many self-join. For example:
```ruby
-class User < ActiveRecord::Base
+class User < ApplicationRecord
has_and_belongs_to_many :friends,
class_name: "User",
foreign_key: "this_user_id",
@@ -2085,14 +2162,14 @@ end
##### `:autosave`
-If you set the `:autosave` option to `true`, Rails will save any loaded members and destroy members that are marked for destruction whenever you save the parent object.
+If you set the `:autosave` option to `true`, Rails will save any loaded association members and destroy members that are marked for destruction whenever you save the parent object. Setting `:autosave` to `false` is not the same as not setting the `:autosave` option. If the `:autosave` option is not present, then new associated objects will be saved, but updated associated objects will not be saved.
##### `:class_name`
If the name of the other model cannot be derived from the association name, you can use the `:class_name` option to supply the model name. For example, if a part has many assemblies, but the actual name of the model containing assemblies is `Gadget`, you'd set things up this way:
```ruby
-class Parts < ActiveRecord::Base
+class Parts < ApplicationRecord
has_and_belongs_to_many :assemblies, class_name: "Gadget"
end
```
@@ -2102,7 +2179,7 @@ end
By convention, Rails assumes that the column in the join table used to hold the foreign key pointing to this model is the name of this model with the suffix `_id` added. The `:foreign_key` option lets you set the name of the foreign key directly:
```ruby
-class User < ActiveRecord::Base
+class User < ApplicationRecord
has_and_belongs_to_many :friends,
class_name: "User",
foreign_key: "this_user_id",
@@ -2123,7 +2200,7 @@ If you set the `:validate` option to `false`, then associated objects will not b
There may be times when you wish to customize the query used by `has_and_belongs_to_many`. Such customizations can be achieved via a scope block. For example:
```ruby
-class Parts < ActiveRecord::Base
+class Parts < ApplicationRecord
has_and_belongs_to_many :assemblies, -> { where active: true }
end
```
@@ -2139,14 +2216,14 @@ You can use any of the standard [querying methods](active_record_querying.html)
* `order`
* `readonly`
* `select`
-* `uniq`
+* `distinct`
##### `where`
The `where` method lets you specify the conditions that the associated object must meet.
```ruby
-class Parts < ActiveRecord::Base
+class Parts < ApplicationRecord
has_and_belongs_to_many :assemblies,
-> { where "factory = 'Seattle'" }
end
@@ -2155,7 +2232,7 @@ end
You can also set conditions via a hash:
```ruby
-class Parts < ActiveRecord::Base
+class Parts < ApplicationRecord
has_and_belongs_to_many :assemblies,
-> { where factory: 'Seattle' }
end
@@ -2172,7 +2249,7 @@ The `extending` method specifies a named module to extend the association proxy.
The `group` method supplies an attribute name to group the result set by, using a `GROUP BY` clause in the finder SQL.
```ruby
-class Parts < ActiveRecord::Base
+class Parts < ApplicationRecord
has_and_belongs_to_many :assemblies, -> { group "factory" }
end
```
@@ -2186,7 +2263,7 @@ You can use the `includes` method to specify second-order associations that shou
The `limit` method lets you restrict the total number of objects that will be fetched through an association.
```ruby
-class Parts < ActiveRecord::Base
+class Parts < ApplicationRecord
has_and_belongs_to_many :assemblies,
-> { order("created_at DESC").limit(50) }
end
@@ -2201,7 +2278,7 @@ The `offset` method lets you specify the starting offset for fetching objects vi
The `order` method dictates the order in which associated objects will be received (in the syntax used by an SQL `ORDER BY` clause).
```ruby
-class Parts < ActiveRecord::Base
+class Parts < ApplicationRecord
has_and_belongs_to_many :assemblies,
-> { order "assembly_name ASC" }
end
@@ -2215,9 +2292,9 @@ If you use the `readonly` method, then the associated objects will be read-only
The `select` method lets you override the SQL `SELECT` clause that is used to retrieve data about the associated objects. By default, Rails retrieves all columns.
-##### `uniq`
+##### `distinct`
-Use the `uniq` method to remove duplicates from the collection.
+Use the `distinct` method to remove duplicates from the collection.
#### When are Objects Saved?
@@ -2243,10 +2320,10 @@ Association callbacks are similar to normal callbacks, but they are triggered by
You define association callbacks by adding options to the association declaration. For example:
```ruby
-class Customer < ActiveRecord::Base
- has_many :orders, before_add: :check_credit_limit
+class Author < ApplicationRecord
+ has_many :books, before_add: :check_credit_limit
- def check_credit_limit(order)
+ def check_credit_limit(book)
...
end
end
@@ -2257,15 +2334,15 @@ Rails passes the object being added or removed to the callback.
You can stack callbacks on a single event by passing them as an array:
```ruby
-class Customer < ActiveRecord::Base
- has_many :orders,
+class Author < ApplicationRecord
+ has_many :books,
before_add: [:check_credit_limit, :calculate_shipping_charges]
- def check_credit_limit(order)
+ def check_credit_limit(book)
...
end
- def calculate_shipping_charges(order)
+ def calculate_shipping_charges(book)
...
end
end
@@ -2278,10 +2355,10 @@ If a `before_add` callback throws an exception, the object does not get added to
You're not limited to the functionality that Rails automatically builds into association proxy objects. You can also extend these objects through anonymous modules, adding new finders, creators, or other methods. For example:
```ruby
-class Customer < ActiveRecord::Base
- has_many :orders do
- def find_by_order_prefix(order_number)
- find_by(region_id: order_number[0..2])
+class Author < ApplicationRecord
+ has_many :books do
+ def find_by_book_prefix(book_number)
+ find_by(category_id: book_number[0..2])
end
end
end
@@ -2296,11 +2373,11 @@ module FindRecentExtension
end
end
-class Customer < ActiveRecord::Base
- has_many :orders, -> { extending FindRecentExtension }
+class Author < ApplicationRecord
+ has_many :books, -> { extending FindRecentExtension }
end
-class Supplier < ActiveRecord::Base
+class Supplier < ApplicationRecord
has_many :deliveries, -> { extending FindRecentExtension }
end
```
@@ -2315,7 +2392,7 @@ Single Table Inheritance
------------------------
Sometimes, you may want to share fields and behavior between different models.
-Let's say we have Car, Motorcycle and Bicycle models. We will want to share
+Let's say we have Car, Motorcycle, and Bicycle models. We will want to share
the `color` and `price` fields and some methods for all of them, but having some
specific behavior for each, and separated controllers too.
diff --git a/guides/source/autoloading_and_reloading_constants.md b/guides/source/autoloading_and_reloading_constants.md
index 2b6d7e4044..b3f923a017 100644
--- a/guides/source/autoloading_and_reloading_constants.md
+++ b/guides/source/autoloading_and_reloading_constants.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Autoloading and Reloading Constants
===================================
@@ -8,7 +8,7 @@ This guide documents how constant autoloading and reloading works.
After reading this guide, you will know:
* Key aspects of Ruby constants
-* What is `autoload_paths`
+* What are the `autoload_paths` and how does eager loading work in production?
* How constant autoloading works
* What is `require_dependency`
* How constant reloading works
@@ -181,14 +181,14 @@ constant.
That is,
```ruby
-class Project < ActiveRecord::Base
+class Project < ApplicationRecord
end
```
performs a constant assignment equivalent to
```ruby
-Project = Class.new(ActiveRecord::Base)
+Project = Class.new(ApplicationRecord)
```
including setting the name of the class as a side-effect:
@@ -230,10 +230,12 @@ is not entirely equivalent to the one of the body of the definitions using the
`class` and `module` keywords. But both idioms result in the same constant
assignment.
-Thus, when one informally says "the `String` class", that really means: the
-class object stored in the constant called "String" in the class object stored
-in the `Object` constant. `String` is otherwise an ordinary Ruby constant and
-everything related to constants such as resolution algorithms applies to it.
+Thus, an informal expression like "the `String` class" technically means the
+class object stored in the constant called "String". That constant, in turn,
+belongs to the class object stored in the constant called "Object".
+
+`String` is an ordinary constant, and everything related to them such as
+resolution algorithms applies to it.
Likewise, in the controller
@@ -330,11 +332,17 @@ its resolution next. Let's define *parent* to be that qualifying class or module
object, that is, `Billing` in the example above. The algorithm for qualified
constants goes like this:
-1. The constant is looked up in the parent and its ancestors.
+1. The constant is looked up in the parent and its ancestors. In Ruby >= 2.5,
+`Object` is skipped if present among the ancestors. `Kernel` and `BasicObject`
+are still checked though.
2. If the lookup fails, `const_missing` is invoked in the parent. The default
implementation of `const_missing` raises `NameError`, but it can be overridden.
+INFO. In Ruby < 2.5 `String::Hash` evaluates to `Hash` and the interpreter
+issues a warning: "toplevel constant Hash referenced by String::Hash". Starting
+with 2.5, `String::Hash` raises `NameError` because `Object` is skipped.
+
As you see, this algorithm is simpler than the one for relative constants. In
particular, the nesting plays no role here, and modules are not special-cased,
if neither they nor their ancestors have the constants, `Object` is **not**
@@ -402,7 +410,7 @@ Rails is always able to autoload provided its environment is in place. For
example the `runner` command autoloads:
```
-$ bin/rails runner 'p User.column_names'
+$ rails runner 'p User.column_names'
["id", "email", "created_at", "updated_at"]
```
@@ -424,8 +432,8 @@ if `House` is still unknown when `app/models/beach_house.rb` is being eager
loaded, Rails autoloads it.
-autoload_paths
---------------
+autoload_paths and eager_load_paths
+-----------------------------------
As you probably know, when `require` gets a relative file name:
@@ -445,41 +453,56 @@ the idea is that when a constant like `Post` is hit and missing, if there's a
`post.rb` file for example in `app/models` Rails is going to find it, evaluate
it, and have `Post` defined as a side-effect.
-Alright, Rails has a collection of directories similar to `$LOAD_PATH` in which
+All right, Rails has a collection of directories similar to `$LOAD_PATH` in which
to look up `post.rb`. That collection is called `autoload_paths` and by
default it contains:
-* All subdirectories of `app` in the application and engines. For example,
- `app/controllers`. They do not need to be the default ones, any custom
- directories like `app/workers` belong automatically to `autoload_paths`.
+* All subdirectories of `app` in the application and engines present at boot
+ time. For example, `app/controllers`. They do not need to be the default
+ ones, any custom directories like `app/workers` belong automatically to
+ `autoload_paths`.
* Any existing second level directories called `app/*/concerns` in the
application and engines.
* The directory `test/mailers/previews`.
-Also, this collection is configurable via `config.autoload_paths`. For example,
-`lib` was in the list years ago, but no longer is. An application can opt-in
-by adding this to `config/application.rb`:
+`eager_load_paths` is initially the `app` paths above
-```ruby
-config.autoload_paths << "#{Rails.root}/lib"
-```
+How files are autoloaded depends on `eager_load` and `cache_classes` config settings which typically vary in development, production, and test modes:
+
+ * In **development**, you want quicker startup with incremental loading of application code. So `eager_load` should be set to `false`, and Rails will autoload files as needed (see [Autoloading Algorithms](#autoloading-algorithms) below) -- and then reload them when they change (see [Constant Reloading](#constant-reloading) below).
+ * In **production**, however, you want consistency and thread-safety and can live with a longer boot time. So `eager_load` is set to `true`, and then during boot (before the app is ready to receive requests) Rails loads all files in the `eager_load_paths` and then turns off auto loading (NB: autoloading may be needed during eager loading). Not autoloading after boot is a `good thing`, as autoloading can cause the app to be have thread-safety problems.
+ * In **test**, for speed of execution (of individual tests) `eager_load` is `false`, so Rails follows development behaviour.
+
+What is described above are the defaults with a newly generated Rails app. There are multiple ways this can be configured differently (see [Configuring Rails Applications](configuring.html#rails-general-configuration).
+). But using `autoload_paths` on its own in the past (before Rails 5) developers might configure `autoload_paths` to add in extra locations (e.g. `lib` which used to be an autoload path list years ago, but no longer is). However this is now discouraged for most purposes, as it is likely to lead to production-only errors. It is possible to add new locations to both `config.eager_load_paths` and `config.autoload_paths` but use at your own risk.
+
+See also [Autoloading in the Test Environment](#autoloading-in-the-test-environment).
`config.autoload_paths` is not changeable from environment-specific configuration files.
-The value of `autoload_paths` can be inspected. In a just generated application
+The value of `autoload_paths` can be inspected. In a just-generated application
it is (edited):
```
-$ bin/rails r 'puts ActiveSupport::Dependencies.autoload_paths'
+$ rails r 'puts ActiveSupport::Dependencies.autoload_paths'
.../app/assets
+.../app/channels
.../app/controllers
+.../app/controllers/concerns
.../app/helpers
+.../app/jobs
.../app/mailers
.../app/models
-.../app/controllers/concerns
.../app/models/concerns
+.../activestorage/app/assets
+.../activestorage/app/controllers
+.../activestorage/app/javascript
+.../activestorage/app/jobs
+.../activestorage/app/models
+.../actioncable/app/assets
+.../actionview/app/assets
.../test/mailers/previews
```
@@ -524,7 +547,7 @@ On the contrary, if `ApplicationController` is unknown, the constant is
considered missing and an autoload is going to be attempted by Rails.
In order to load `ApplicationController`, Rails iterates over `autoload_paths`.
-First checks if `app/assets/application_controller.rb` exists. If it does not,
+First it checks if `app/assets/application_controller.rb` exists. If it does not,
which is normally the case, it continues and finds
`app/controllers/application_controller.rb`.
@@ -624,7 +647,7 @@ file is loaded. If the file actually defines `Post` all is fine, otherwise
### Qualified References
When a qualified constant is missing Rails does not look for it in the parent
-namespaces. But there is a caveat: When a constant is missing, Rails is
+namespaces. But there is a caveat: when a constant is missing, Rails is
unable to tell if the trigger was a relative reference or a qualified one.
For example, consider
@@ -685,7 +708,7 @@ to trigger the heuristic is defined in the conflicting place.
### Automatic Modules
When a module acts as a namespace, Rails does not require the application to
-defines a file for it, a directory matching the namespace is enough.
+define a file for it, a directory matching the namespace is enough.
Suppose an application has a back office whose controllers are stored in
`app/controllers/admin`. If the `Admin` module is not yet loaded when
@@ -790,7 +813,7 @@ Constant Reloading
When `config.cache_classes` is false Rails is able to reload autoloaded
constants.
-For example, in you're in a console session and edit some file behind the
+For example, if you're in a console session and edit some file behind the
scenes, the code can be reloaded with the `reload!` command:
```
@@ -912,7 +935,7 @@ these classes:
```ruby
# app/models/polygon.rb
-class Polygon < ActiveRecord::Base
+class Polygon < ApplicationRecord
end
# app/models/triangle.rb
@@ -944,7 +967,7 @@ to work on some subclass, things get interesting.
While working with `Polygon` you do not need to be aware of all its descendants,
because anything in the table is by definition a polygon, but when working with
subclasses Active Record needs to be able to enumerate the types it is looking
-for. Let’s see an example.
+for. Let's see an example.
`Rectangle.all` only loads rectangles by adding a type constraint to the query:
@@ -953,7 +976,7 @@ SELECT "polygons".* FROM "polygons"
WHERE "polygons"."type" IN ("Rectangle")
```
-Let’s introduce now a subclass of `Rectangle`:
+Let's introduce now a subclass of `Rectangle`:
```ruby
# app/models/square.rb
@@ -968,7 +991,7 @@ SELECT "polygons".* FROM "polygons"
WHERE "polygons"."type" IN ("Rectangle", "Square")
```
-But there’s a caveat here: How does Active Record know that the class `Square`
+But there's a caveat here: How does Active Record know that the class `Square`
exists at all?
Even if the file `app/models/square.rb` exists and defines the `Square` class,
@@ -982,20 +1005,19 @@ WHERE "polygons"."type" IN ("Rectangle")
That is not a bug, the query includes all *known* descendants of `Rectangle`.
A way to ensure this works correctly regardless of the order of execution is to
-load the leaves of the tree by hand at the bottom of the file that defines the
-root class:
+manually load the direct subclasses at the bottom of the file that defines each
+intermediate class:
```ruby
-# app/models/polygon.rb
-class Polygon < ActiveRecord::Base
+# app/models/rectangle.rb
+class Rectangle < Polygon
end
-require_dependency ‘square’
+require_dependency 'square'
```
-Only the leaves that are **at least grandchildren** need to be loaded this
-way. Direct subclasses do not need to be preloaded. If the hierarchy is
-deeper, intermediate classes will be autoloaded recursively from the bottom
-because their constant will appear in the class definitions as superclass.
+This needs to happen for every intermediate (non-root and non-leaf) class. The
+root class does not scope the query by type, and therefore does not necessarily
+have to know all its descendants.
### Autoloading and `require`
@@ -1040,7 +1062,7 @@ end
The purpose of this setup would be that the application uses the class that
corresponds to the environment via `AUTH_SERVICE`. In development mode
-`MockedAuthService` gets autoloaded when the initializer runs. Let’s suppose
+`MockedAuthService` gets autoloaded when the initializer runs. Let's suppose
we do some requests, change its implementation, and hit the application again.
To our surprise the changes are not reflected. Why?
@@ -1169,6 +1191,8 @@ end
#### Qualified References
+WARNING. This gotcha is only possible in Ruby < 2.5.
+
Given
```ruby
@@ -1196,7 +1220,7 @@ been loaded but `app/models/hotel/image.rb` hasn't, Ruby does not find `Image`
in `Hotel`, but it does in `Object`:
```
-$ bin/rails r 'Image; p Hotel::Image' 2>/dev/null
+$ rails r 'Image; p Hotel::Image' 2>/dev/null
Image # NOT Hotel::Image!
```
@@ -1312,3 +1336,48 @@ class C < BasicObject
end
end
```
+
+### Autoloading in the Test Environment
+
+When configuring the `test` environment for autoloading you might consider multiple factors.
+
+For example it might be worth running your tests with an identical setup to production (`config.eager_load = true`, `config.cache_classes = true`) in order to catch any problems before they hit production (this is compensation for the lack of dev-prod parity). However this will slow down the boot time for individual tests on a dev machine (and is not immediately compatible with spring see below). So one possibility is to do this on a
+[CI](https://en.wikipedia.org/wiki/Continuous_integration) machine only (which should run without spring).
+
+On a development machine you can then have your tests running with whatever is fastest (ideally `config.eager_load = false`).
+
+With the [Spring](https://github.com/rails/spring) pre-loader (included with new Rails apps), you ideally keep `config.eager_load = false` as per development. Sometimes you may end up with a hybrid configuration (`config.eager_load = true`, `config.cache_classes = true` AND `config.enable_dependency_loading = true`), see [spring issue](https://github.com/rails/spring/issues/519#issuecomment-348324369). However it might be simpler to keep the same configuration as development, and work out whatever it is that is causing autoloading to fail (perhaps by the results of your CI test results).
+
+Occasionally you may need to explicitly eager_load by using `Rails
+.application.eager_load!` in the setup of your tests -- this might occur if your [tests involve multithreading](https://stackoverflow.com/questions/25796409/in-rails-how-can-i-eager-load-all-code-before-a-specific-rspec-test).
+
+## Troubleshooting
+
+### Tracing Autoloads
+
+Active Support is able to report constants as they are autoloaded. To enable these traces in a Rails application, put the following two lines in some initializer:
+
+```ruby
+ActiveSupport::Dependencies.logger = Rails.logger
+ActiveSupport::Dependencies.verbose = true
+```
+
+### Where is a Given Autoload Triggered?
+
+If constant `Foo` is being autoloaded, and you'd like to know where is that autoload coming from, just throw
+
+```ruby
+puts caller
+```
+
+at the top of `foo.rb` and inspect the printed stack trace.
+
+### Which Constants Have Been Autoloaded?
+
+At any given time,
+
+```ruby
+ActiveSupport::Dependencies.autoloaded_constants
+```
+
+has the collection of constants that have been autoloaded so far.
diff --git a/guides/source/caching_with_rails.md b/guides/source/caching_with_rails.md
index d6ad8f7db3..67b097f2ae 100644
--- a/guides/source/caching_with_rails.md
+++ b/guides/source/caching_with_rails.md
@@ -1,14 +1,26 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Caching with Rails: An Overview
===============================
-This guide is an introduction to speeding up your Rails app with caching.
+This guide is an introduction to speeding up your Rails application with caching.
+
+Caching means to store content generated during the request-response cycle and
+to reuse it when responding to similar requests.
+
+Caching is often the most effective way to boost an application's performance.
+Through caching, web sites running on a single server with a single database
+can sustain a load of thousands of concurrent users.
+
+Rails provides a set of caching features out of the box. This guide will teach
+you the scope and purpose of each one of them. Master these techniques and your
+Rails applications can serve millions of views without exorbitant response times
+or server bills.
After reading this guide, you will know:
-* Page and action caching.
* Fragment and Russian doll caching.
+* How to manage the caching dependencies.
* Alternative cache stores.
* Conditional GET support.
@@ -20,7 +32,7 @@ Basic Caching
This is an introduction to three types of caching techniques: page, action and
fragment caching. By default Rails provides fragment caching. In order to use
page and action caching you will need to add `actionpack-page_caching` and
-`actionpack-action_caching` to your Gemfile.
+`actionpack-action_caching` to your `Gemfile`.
By default, caching is only enabled in your production environment. To play
around with caching locally you'll want to enable caching in your local
@@ -31,6 +43,11 @@ the relevant `config/environments/*.rb` file:
config.action_controller.perform_caching = true
```
+NOTE: Changing the value of `config.action_controller.perform_caching` will
+only have an effect on the caching provided by the Action Controller component.
+For instance, it will not impact low-level caching, that we address
+[below](#low-level-caching).
+
### Page Caching
Page caching is a Rails mechanism which allows the request for a generated page
@@ -83,9 +100,9 @@ called key-based expiration.
Cache fragments will also be expired when the view fragment changes (e.g., the
HTML in the view changes). The string of characters at the end of the key is a
-template tree digest. It is an md5 hash computed based on the contents of the
-view fragment you are caching. If you change the view fragment, the md5 hash
-will change, expiring the existing file.
+template tree digest. It is a hash digest computed based on the contents of the
+view fragment you are caching. If you change the view fragment, the digest will
+change, expiring the existing file.
TIP: Cache stores like Memcached will automatically delete old cache files.
@@ -98,6 +115,21 @@ If you want to cache a fragment under certain conditions, you can use
<% end %>
```
+#### Collection caching
+
+The `render` helper can also cache individual templates rendered for a collection.
+It can even one up the previous example with `each` by reading all cache
+templates at once instead of one by one. This is done by passing `cached: true` when rendering the collection:
+
+```html+erb
+<%= render partial: 'products/product', collection: @products, cached: true %>
+```
+
+All cached templates from previous renders will be fetched at once with much
+greater speed. Additionally, the templates that haven't yet been cached will be
+written to cache and multi fetched on the next render.
+
+
### Russian Doll Caching
You may want to nest cached fragments inside other cached fragments. This is
@@ -134,29 +166,134 @@ your app will serve stale data. To fix this, we tie the models together with
the `touch` method:
```ruby
-class Product < ActiveRecord::Base
+class Product < ApplicationRecord
has_many :games
end
-class Game < ActiveRecord::Base
+class Game < ApplicationRecord
belongs_to :product, touch: true
end
```
-With `touch` set to true, any action which changes `updated_at` for a game
+With `touch` set to `true`, any action which changes `updated_at` for a game
record will also change it for the associated product, thereby expiring the
cache.
+### Shared Partial Caching
+
+It is possible to share partials and associated caching between files with different mime types. For example shared partial caching allows template writers to share a partial between HTML and JavaScript files. When templates are collected in the template resolver file paths they only include the template language extension and not the mime type. Because of this templates can be used for multiple mime types. Both HTML and JavaScript requests will respond to the following code:
+
+```ruby
+render(partial: 'hotels/hotel', collection: @hotels, cached: true)
+```
+
+Will load a file named `hotels/hotel.erb`.
+
+Another option is to include the full filename of the partial to render.
+
+```ruby
+render(partial: 'hotels/hotel.html.erb', collection: @hotels, cached: true)
+```
+
+Will load a file named `hotels/hotel.html.erb` in any file mime type, for example you could include this partial in a JavaScript file.
+
+### Managing dependencies
+
+In order to correctly invalidate the cache, you need to properly define the
+caching dependencies. Rails is clever enough to handle common cases so you don't
+have to specify anything. However, sometimes, when you're dealing with custom
+helpers for instance, you need to explicitly define them.
+
+#### Implicit dependencies
+
+Most template dependencies can be derived from calls to `render` in the template
+itself. Here are some examples of render calls that `ActionView::Digestor` knows
+how to decode:
+
+```ruby
+render partial: "comments/comment", collection: commentable.comments
+render "comments/comments"
+render 'comments/comments'
+render('comments/comments')
+
+render "header" translates to render("comments/header")
+
+render(@topic) translates to render("topics/topic")
+render(topics) translates to render("topics/topic")
+render(message.topics) translates to render("topics/topic")
+```
+
+On the other hand, some calls need to be changed to make caching work properly.
+For instance, if you're passing a custom collection, you'll need to change:
+
+```ruby
+render @project.documents.where(published: true)
+```
+
+to:
+
+```ruby
+render partial: "documents/document", collection: @project.documents.where(published: true)
+```
+
+#### Explicit dependencies
+
+Sometimes you'll have template dependencies that can't be derived at all. This
+is typically the case when rendering happens in helpers. Here's an example:
+
+```html+erb
+<%= render_sortable_todolists @project.todolists %>
+```
+
+You'll need to use a special comment format to call those out:
+
+```html+erb
+<%# Template Dependency: todolists/todolist %>
+<%= render_sortable_todolists @project.todolists %>
+```
+
+In some cases, like a single table inheritance setup, you might have a bunch of
+explicit dependencies. Instead of writing every template out, you can use a
+wildcard to match any template in a directory:
+
+```html+erb
+<%# Template Dependency: events/* %>
+<%= render_categorizable_events @person.events %>
+```
+
+As for collection caching, if the partial template doesn't start with a clean
+cache call, you can still benefit from collection caching by adding a special
+comment format anywhere in the template, like:
+
+```html+erb
+<%# Template Collection: notification %>
+<% my_helper_that_calls_cache(some_arg, notification) do %>
+ <%= notification.name %>
+<% end %>
+```
+
+#### External dependencies
+
+If you use a helper method, for example, inside a cached block and you then update
+that helper, you'll have to bump the cache as well. It doesn't really matter how
+you do it, but the MD5 of the template file must change. One recommendation is to
+simply be explicit in a comment, like:
+
+```html+erb
+<%# Helper Dependency Updated: Jul 28, 2015 at 7pm %>
+<%= some_helper_method(person) %>
+```
+
### Low-Level Caching
Sometimes you need to cache a particular value or query result instead of caching view fragments. Rails' caching mechanism works great for storing __any__ kind of information.
-The most efficient way to implement low-level caching is using the `Rails.cache.fetch` method. This method does both reading and writing to the cache. When passed only a single argument, the key is fetched and value from the cache is returned. If a block is passed, the result of the block will be cached to the given key and the result is returned.
+The most efficient way to implement low-level caching is using the `Rails.cache.fetch` method. This method does both reading and writing to the cache. When passed only a single argument, the key is fetched and value from the cache is returned. If a block is passed, that block will be executed in the event of a cache miss. The return value of the block will be written to the cache under the given cache key, and that return value will be returned. In case of cache hit, the cached value will be returned without executing the block.
-Consider the following example. An application has a `Product` model with an instance method that looks up the product’s price on a competing website. The data returned by this method would be perfect for low-level caching:
+Consider the following example. An application has a `Product` model with an instance method that looks up the product's price on a competing website. The data returned by this method would be perfect for low-level caching:
```ruby
-class Product < ActiveRecord::Base
+class Product < ApplicationRecord
def competing_price
Rails.cache.fetch("#{cache_key}/competing_price", expires_in: 12.hours) do
Competitor::API.find_price(id)
@@ -165,7 +302,7 @@ class Product < ActiveRecord::Base
end
```
-NOTE: Notice that in this example we used the `cache_key` method, so the resulting cache-key will be something like `products/233-20140225082222765838000/competing_price`. `cache_key` generates a string based on the model’s `id` and `updated_at` attributes. This is a common convention and has the benefit of invalidating the cache whenever the product is updated. In general, when you use low-level caching for instance level information, you need to generate a cache key.
+NOTE: Notice that in this example we used the `cache_key_with_version` method, so the resulting cache key will be something like `products/233-20140225082222765838000/competing_price`. `cache_key_with_version` generates a string based on the model's `id` and `updated_at` attributes. This is a common convention and has the benefit of invalidating the cache whenever the product is updated. In general, when you use low-level caching for instance level information, you need to generate a cache key.
### SQL Caching
@@ -202,16 +339,17 @@ persistent fashion, you can with low level caching.
Cache Stores
------------
-Rails provides different stores for the cached data created by **action** and **fragment** caches.
-
-TIP: Page caches are always stored on disk.
+Rails provides different stores for the cached data (apart from SQL and page
+caching).
### Configuration
-You can set up your application's default cache store by calling `config.cache_store=` in the Application definition inside your `config/application.rb` file or in an Application.configure block in an environment specific configuration file (i.e. `config/environments/*.rb`). The first argument will be the cache store to use and the rest of the argument will be passed as arguments to the cache store constructor.
+You can set up your application's default cache store by setting the
+`config.cache_store` configuration option. Other parameters can be passed as
+arguments to the cache store's constructor:
```ruby
-config.cache_store = :memory_store
+config.cache_store = :memory_store, { size: 64.megabytes }
```
NOTE: Alternatively, you can call `ActionController::Base.cache_store` outside of a configuration block.
@@ -224,18 +362,31 @@ This class provides the foundation for interacting with the cache in Rails. This
The main methods to call are `read`, `write`, `delete`, `exist?`, and `fetch`. The fetch method takes a block and will either return an existing value from the cache, or evaluate the block and write the result to the cache if no value exists.
-There are some common options used by all cache implementations. These can be passed to the constructor or the various methods to interact with entries.
+There are some common options that can be used by all cache implementations. These can be passed to the constructor or the various methods to interact with entries.
* `:namespace` - This option can be used to create a namespace within the cache store. It is especially useful if your application shares a cache with other applications.
-* `:compress` - This option can be used to indicate that compression should be used in the cache. This can be useful for transferring large cache entries over a slow network.
+* `:compress` - Enabled by default. Compresses cache entries so more data can be stored in the same memory footprint, leading to fewer cache evictions and higher hit rates.
-* `:compress_threshold` - This option is used in conjunction with the `:compress` option to indicate a threshold under which cache entries should not be compressed. This defaults to 16 kilobytes.
+* `:compress_threshold` - Defaults to 1kB. Cache entries larger than this threshold, specified in bytes, are compressed.
-* `:expires_in` - This option sets an expiration time in seconds for the cache entry when it will be automatically removed from the cache.
+* `:expires_in` - This option sets an expiration time in seconds for the cache entry, if the cache store supports it, when it will be automatically removed from the cache.
* `:race_condition_ttl` - This option is used in conjunction with the `:expires_in` option. It will prevent race conditions when cache entries expire by preventing multiple processes from simultaneously regenerating the same entry (also known as the dog pile effect). This option sets the number of seconds that an expired entry can be reused while a new value is being regenerated. It's a good practice to set this value if you use the `:expires_in` option.
+#### Custom Cache Stores
+
+You can create your own custom cache store by simply extending
+`ActiveSupport::Cache::Store` and implementing the appropriate methods. This way,
+you can swap in any number of caching technologies into your Rails application.
+
+To use a custom cache store, simply set the cache store to a new instance of your
+custom class.
+
+```ruby
+config.cache_store = MyCacheStore.new
+```
+
### ActiveSupport::Cache::MemoryStore
This cache store keeps entries in memory in the same Ruby process. The cache
@@ -248,12 +399,17 @@ config.cache_store = :memory_store, { size: 64.megabytes }
```
If you're running multiple Ruby on Rails server processes (which is the case
-if you're using mongrel_cluster or Phusion Passenger), then your Rails server
+if you're using Phusion Passenger or puma clustered mode), then your Rails server
process instances won't be able to share cache data with each other. This cache
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.
@@ -263,14 +419,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
@@ -287,57 +444,93 @@ The `write` and `fetch` methods on this cache accept two additional options that
config.cache_store = :mem_cache_store, "cache-1.example.com", "cache-2.example.com"
```
-### ActiveSupport::Cache::EhcacheStore
+### ActiveSupport::Cache::RedisCacheStore
-If you are using JRuby you can use Terracotta's Ehcache as the cache store for your application. Ehcache is an open source Java cache that also offers an enterprise version with increased scalability, management, and commercial support. You must first install the jruby-ehcache-rails3 gem (version 1.1.0 or later) to use this cache store.
+The Redis cache store takes advantage of Redis support for automatic eviction
+when it reaches max memory, allowing it to behave much like a Memcached cache server.
-```ruby
-config.cache_store = :ehcache_store
-```
+Deployment note: Redis doesn't expire keys by default, so take care to use a
+dedicated Redis cache server. Don't fill up your persistent-Redis server with
+volatile cache data! Read the
+[Redis cache server setup guide](https://redis.io/topics/lru-cache) in detail.
+
+For a cache-only Redis server, set `maxmemory-policy` to one of the variants of allkeys.
+Redis 4+ supports least-frequently-used eviction (`allkeys-lfu`), an excellent
+default choice. Redis 3 and earlier should use least-recently-used eviction (`allkeys-lru`).
-When initializing the cache, you may use the `:ehcache_config` option to specify the Ehcache config file to use (where the default is "ehcache.xml" in your Rails config directory), and the :cache_name option to provide a custom name for your cache (the default is rails_cache).
+Set cache read and write timeouts relatively low. Regenerating a cached value
+is often faster than waiting more than a second to retrieve it. Both read and
+write timeouts default to 1 second, but may be set lower if your network is
+consistently low-latency.
-In addition to the standard `:expires_in` option, the `write` method on this cache can also accept the additional `:unless_exist` option, which will cause the cache store to use Ehcache's `putIfAbsent` method instead of `put`, and therefore will not overwrite an existing entry. Additionally, the `write` method supports all of the properties exposed by the [Ehcache Element class](http://ehcache.org/apidocs/net/sf/ehcache/Element.html) , including:
+By default, the cache store will not attempt to reconnect to Redis if the
+connection fails during a request. If you experience frequent disconnects you
+may wish to enable reconnect attempts.
-| Property | Argument Type | Description |
-| --------------------------- | ------------------- | ----------------------------------------------------------- |
-| elementEvictionData | ElementEvictionData | Sets this element's eviction data instance. |
-| eternal | boolean | Sets whether the element is eternal. |
-| timeToIdle, tti | int | Sets time to idle |
-| timeToLive, ttl, expires_in | int | Sets time to Live |
-| version | long | Sets the version attribute of the ElementAttributes object. |
+Cache reads and writes never raise exceptions; they just return `nil` instead,
+behaving as if there was nothing in the cache. To gauge whether your cache is
+hitting exceptions, you may provide an `error_handler` to report to an
+exception gathering service. It must accept three keyword arguments: `method`,
+the cache store method that was originally called; `returning`, the value that
+was returned to the user, typically `nil`; and `exception`, the exception that
+was rescued.
-These options are passed to the `write` method as Hash options using either camelCase or underscore notation, as in the following examples:
+To get started, add the redis gem to your Gemfile:
```ruby
-Rails.cache.write('key', 'value', time_to_idle: 60.seconds, timeToLive: 600.seconds)
-caches_action :index, expires_in: 60.seconds, unless_exist: true
+gem 'redis'
```
-For more information about Ehcache, see [http://ehcache.org/](http://ehcache.org/) .
-For more information about Ehcache for JRuby and Rails, see [http://ehcache.org/documentation/jruby.html](http://ehcache.org/documentation/jruby.html)
+You can enable support for the faster [hiredis](https://github.com/redis/hiredis)
+connection library by additionally adding its ruby wrapper to your Gemfile:
-### ActiveSupport::Cache::NullStore
+```ruby
+gem 'hiredis'
+```
-This cache store implementation is meant to be used only in development or test environments and it never stores anything. This can be very useful in development when you have code that interacts directly with `Rails.cache` but caching may interfere with being able to see the results of code changes. With this cache store, all `fetch` and `read` operations will result in a miss.
+Redis cache store will automatically require & use hiredis if available. No further
+configuration is needed.
+
+Finally, add the configuration in the relevant `config/environments/*.rb` file:
```ruby
-config.cache_store = :null_store
+config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'] }
```
-### Custom Cache Stores
+A more complex, production Redis cache store may look something like this:
-You can create your own custom cache store by simply extending `ActiveSupport::Cache::Store` and implementing the appropriate methods. In this way, you can swap in any number of caching technologies into your Rails application.
+```ruby
+cache_servers = %w(redis://cache-01:6379/0 redis://cache-02:6379/0)
+config.cache_store = :redis_cache_store, { url: cache_servers,
+
+ connect_timeout: 30, # Defaults to 20 seconds
+ read_timeout: 0.2, # Defaults to 1 second
+ write_timeout: 0.2, # Defaults to 1 second
+ reconnect_attempts: 1, # Defaults to 0
+
+ error_handler: -> (method:, returning:, exception:) {
+ # Report errors to Sentry as warnings
+ Raven.capture_exception exception, level: 'warning',
+ tags: { method: method, returning: returning }
+ }
+}
+```
-To use a custom cache store, simply set the cache store to a new instance of the class.
+### ActiveSupport::Cache::NullStore
+
+This cache store implementation is meant to be used only in development or test environments and it never stores anything. This can be very useful in development when you have code that interacts directly with `Rails.cache` but caching may interfere with being able to see the results of code changes. With this cache store, all `fetch` and `read` operations will result in a miss.
```ruby
-config.cache_store = MyCacheStore.new
+config.cache_store = :null_store
```
-### Cache Keys
+Cache Keys
+----------
-The keys used in a cache can be any object that responds to either `:cache_key` or `:to_param`. You can implement the `:cache_key` method on your classes if you need to generate custom keys. Active Record will generate keys based on the class name and record id.
+The keys used in a cache can be any object that responds to either `cache_key` or
+`to_param`. You can implement the `cache_key` method on your classes if you need
+to generate custom keys. Active Record will generate keys based on the class name
+and record id.
You can use Hashes and Arrays of values as cache keys.
@@ -346,7 +539,12 @@ You can use Hashes and Arrays of values as cache keys.
Rails.cache.read(site: "mysite", owners: [owner_1, owner_2])
```
-The keys you use on `Rails.cache` will not be the same as those actually used with the storage engine. They may be modified with a namespace or altered to fit technology backend constraints. This means, for instance, that you can't save values with `Rails.cache` and then try to pull them out with the `memcache-client` gem. However, you also don't need to worry about exceeding the memcached size limit or violating syntax rules.
+The keys you use on `Rails.cache` will not be the same as those actually used with
+the storage engine. They may be modified with a namespace or altered to fit
+technology backend constraints. This means, for instance, that you can't save
+values with `Rails.cache` and then try to pull them out with the `dalli` gem.
+However, you also don't need to worry about exceeding the memcached size limit or
+violating syntax rules.
Conditional GET support
-----------------------
@@ -365,7 +563,7 @@ class ProductsController < ApplicationController
# If the request is stale according to the given timestamp and etag value
# (i.e. it needs to be processed again) then execute this block
- if stale?(last_modified: @product.updated_at.utc, etag: @product.cache_key)
+ if stale?(last_modified: @product.updated_at.utc, etag: @product.cache_key_with_version)
respond_to do |wants|
# ... normal response processing
end
@@ -379,7 +577,7 @@ class ProductsController < ApplicationController
end
```
-Instead of an options hash, you can also simply pass in a model. Rails will use the `updated_at` and `cache_key` methods for setting `last_modified` and `etag`:
+Instead of an options hash, you can also simply pass in a model. Rails will use the `updated_at` and `cache_key_with_version` methods for setting `last_modified` and `etag`:
```ruby
class ProductsController < ApplicationController
@@ -410,6 +608,78 @@ class ProductsController < ApplicationController
end
```
+Sometimes we want to cache response, for example a static page, that never gets
+expired. To achieve this, we can use `http_cache_forever` helper and by doing
+so browser and proxies will cache it indefinitely.
+
+By default cached responses will be private, cached only on the user's web
+browser. To allow proxies to cache the response, set `public: true` to indicate
+that they can serve the cached response to all users.
+
+Using this helper, `last_modified` header is set to `Time.new(2011, 1, 1).utc`
+and `expires` header is set to a 100 years.
+
+WARNING: Use this method carefully as browser/proxy won't be able to invalidate
+the cached response unless browser cache is forcefully cleared.
+
+```ruby
+class HomeController < ApplicationController
+ def index
+ http_cache_forever(public: true) do
+ render
+ end
+ end
+end
+```
+
+### Strong v/s Weak ETags
+
+Rails generates weak ETags by default. Weak ETags allow semantically equivalent
+responses to have the same ETags, even if their bodies do not match exactly.
+This is useful when we don't want the page to be regenerated for minor changes in
+response body.
+
+Weak ETags have a leading `W/` to differentiate them from strong ETags.
+
+```
+ W/"618bbc92e2d35ea1945008b42799b0e7" → Weak ETag
+ "618bbc92e2d35ea1945008b42799b0e7" → Strong ETag
+```
+
+Unlike weak ETag, strong ETag implies that response should be exactly the same
+and byte by byte identical. Useful when doing Range requests within a
+large video or PDF file. Some CDNs support only strong ETags, like Akamai.
+If you absolutely need to generate a strong ETag, it can be done as follows.
+
+```ruby
+ class ProductsController < ApplicationController
+ def show
+ @product = Product.find(params[:id])
+ fresh_when last_modified: @product.published_at.utc, strong_etag: @product
+ end
+ end
+```
+
+You can also set the strong ETag directly on the response.
+
+```ruby
+ response.strong_etag = response.body # => "618bbc92e2d35ea1945008b42799b0e7"
+```
+
+Caching in Development
+----------------------
+
+It's common to want to test the caching strategy of your application
+in development mode. Rails provides the rails command `dev:cache` to
+easily toggle caching on/off.
+
+```bash
+$ rails dev:cache
+Development mode is now being cached.
+$ 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 0f5a9e4e39..bbebf97c3f 100644
--- a/guides/source/command_line.md
+++ b/guides/source/command_line.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
The Rails Command Line
======================
@@ -21,12 +21,51 @@ There are a few commands that are absolutely critical to your everyday usage of
* `rails console`
* `rails server`
-* `rake`
+* `rails test`
* `rails generate`
+* `rails db:migrate`
+* `rails db:create`
+* `rails routes`
* `rails dbconsole`
* `rails new app_name`
-All commands can run with `-h` or `--help` to list more information.
+You can get a list of rails commands available to you, which will often depend on your current directory, by typing `rails --help`. Each command has a description, and should help you find the thing you need.
+
+```bash
+$ rails --help
+Usage: rails COMMAND [ARGS]
+
+The most common rails commands are:
+ generate Generate new code (short-cut alias: "g")
+ console Start the Rails console (short-cut alias: "c")
+ server Start the Rails server (short-cut alias: "s")
+ ...
+
+All commands can be run with -h (or --help) for more information.
+
+In addition to those commands, there are:
+ about List versions of all Rails ...
+ assets:clean[keep] Remove old compiled assets
+ assets:clobber Remove compiled assets
+ assets:environment Load asset compile environment
+ assets:precompile Compile all the assets ...
+ ...
+ 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.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 ...
+ db:structure:dump Dumps the database structure ...
+ db:structure:load Recreates the databases ...
+ db:version Retrieves the current schema ...
+ ...
+ restart Restart app by touching ...
+ tmp:create Creates tmp directories ...
+```
Let's create a simple Rails application to step through each of these commands in context.
@@ -39,7 +78,7 @@ INFO: You can install the rails gem by typing `gem install rails`, if you don't
```bash
$ rails new commandsapp
create
- create README.rdoc
+ create README.md
create Rakefile
create config.ru
create .gitignore
@@ -55,20 +94,22 @@ Rails will set you up with what seems like a huge amount of stuff for such a tin
### `rails server`
-The `rails server` command launches a small web server named WEBrick which comes bundled with Ruby. You'll use this any time you want to access your application through a web browser.
+The `rails server` command launches a web server named Puma which comes bundled with Rails. You'll use this any time you want to access your application through a web browser.
With no further work, `rails server` will run our new shiny Rails app:
```bash
$ cd commandsapp
-$ bin/rails server
-=> Booting WEBrick
-=> Rails 5.0.0 application starting in development on http://localhost:3000
+$ rails server
+=> Booting Puma
+=> Rails 5.1.0 application starting in development on http://0.0.0.0:3000
=> Run `rails server -h` for more startup options
-=> Ctrl-C to shutdown server
-[2013-08-07 02:00:01] INFO WEBrick 1.3.1
-[2013-08-07 02:00:01] INFO ruby 2.2.2 (2015-06-27) [x86_64-darwin11.2.0]
-[2013-08-07 02:00:01] INFO WEBrick::HTTPServer#start: pid=69680 port=3000
+Puma starting in single mode...
+* Version 3.0.2 (ruby 2.3.0-p0), codename: Plethora of Penguin Pinatas
+* Min threads: 5, max threads: 5
+* Environment: development
+* Listening on tcp://localhost:3000
+Use Ctrl-C to stop
```
With just three commands we whipped up a Rails server listening on port 3000. Go to your browser and open [http://localhost:3000](http://localhost:3000), you will see a basic Rails app running.
@@ -78,7 +119,7 @@ INFO: You can also use the alias "s" to start the server: `rails s`.
The server can be run on a different port using the `-p` option. The default development environment can be changed using `-e`.
```bash
-$ bin/rails server -e production -p 4000
+$ rails server -e production -p 4000
```
The `-b` option binds Rails to the specified IP, by default it is localhost. You can run a server as a daemon by passing a `-d` option.
@@ -90,7 +131,7 @@ The `rails generate` command uses templates to create a whole lot of things. Run
INFO: You can also use the alias "g" to invoke the generator command: `rails g`.
```bash
-$ bin/rails generate
+$ rails generate
Usage: rails generate GENERATOR [args] [options]
...
@@ -100,6 +141,7 @@ Please choose a generator below.
Rails:
assets
+ channel
controller
generator
...
@@ -115,7 +157,7 @@ Let's make our own controller with the controller generator. But what command sh
INFO: All Rails console utilities have help text. As with most *nix utilities, you can try adding `--help` or `-h` to the end, for example `rails server --help`.
```bash
-$ bin/rails generate controller
+$ rails generate controller
Usage: rails generate controller NAME [action action] [options]
...
@@ -141,9 +183,9 @@ Example:
The controller generator is expecting parameters in the form of `generate controller ControllerName action1 action2`. Let's make a `Greetings` controller with an action of **hello**, which will say something nice to us.
```bash
-$ bin/rails generate controller Greetings hello
+$ rails generate controller Greetings hello
create app/controllers/greetings_controller.rb
- route get "greetings/hello"
+ route get 'greetings/hello'
invoke erb
create app/views/greetings
create app/views/greetings/hello.html.erb
@@ -151,14 +193,13 @@ $ bin/rails generate controller Greetings hello
create test/controllers/greetings_controller_test.rb
invoke helper
create app/helpers/greetings_helper.rb
+ invoke test_unit
invoke assets
- invoke coffee
- create app/assets/javascripts/greetings.coffee
invoke scss
create app/assets/stylesheets/greetings.scss
```
-What all did this generate? It made sure a bunch of directories were in our application, and created a controller file, a view file, a functional test file, a helper for the view, a JavaScript file and a stylesheet file.
+What all did this generate? It made sure a bunch of directories were in our application, and created a controller file, a view file, a functional test file, a helper for the view, a JavaScript file, and a stylesheet file.
Check out the controller and modify it a little (in `app/controllers/greetings_controller.rb`):
@@ -180,8 +221,8 @@ Then the view, to display our message (in `app/views/greetings/hello.html.erb`):
Fire up your server using `rails server`.
```bash
-$ bin/rails server
-=> Booting WEBrick...
+$ rails server
+=> Booting Puma...
```
The URL will be [http://localhost:3000/greetings/hello](http://localhost:3000/greetings/hello).
@@ -191,7 +232,7 @@ INFO: With a normal, plain-old Rails application, your URLs will generally follo
Rails comes with a generator for data models too.
```bash
-$ bin/rails generate model
+$ rails generate model
Usage:
rails generate model NAME [field[:type][:index] field[:type][:index]] [options]
@@ -207,14 +248,14 @@ Description:
Create rails files for model generator.
```
-NOTE: For a list of available field types, refer to the [API documentation](http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/TableDefinition.html#method-i-column) for the column method for the `TableDefinition` class.
+NOTE: For a list of available field types for the `type` parameter, refer to the [API documentation](http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_column) for the add_column method for the `SchemaStatements` module. The `index` parameter generates a corresponding index for the column.
But instead of generating a model directly (which we'll be doing later), let's set up a scaffold. A **scaffold** in Rails is a full set of model, database migration for that model, controller to manipulate it, views to view and manipulate the data, and a test suite for each of the above.
We will set up a simple resource called "HighScore" that will keep track of our highest score on video games we play.
```bash
-$ bin/rails generate scaffold HighScore game:string score:integer
+$ rails generate scaffold HighScore game:string score:integer
invoke active_record
create db/migrate/20130717151933_create_high_scores.rb
create app/models/high_score.rb
@@ -239,6 +280,8 @@ $ bin/rails generate scaffold HighScore game:string score:integer
invoke jbuilder
create app/views/high_scores/index.json.jbuilder
create app/views/high_scores/show.json.jbuilder
+ invoke test_unit
+ create test/system/high_scores_test.rb
invoke assets
invoke coffee
create app/assets/javascripts/high_scores.coffee
@@ -250,22 +293,28 @@ $ bin/rails generate scaffold HighScore game:string score:integer
The generator checks that there exist the directories for models, controllers, helpers, layouts, functional and unit tests, stylesheets, creates the views, controller, model and database migration for HighScore (creating the `high_scores` table and fields), takes care of the route for the **resource**, and new tests for everything.
-The migration requires that we **migrate**, that is, run some Ruby code (living in that `20130717151933_create_high_scores.rb`) to modify the schema of our database. Which database? The SQLite3 database that Rails will create for you when we run the `rake db:migrate` command. We'll talk more about Rake in-depth in a little while.
+The migration requires that we **migrate**, that is, run some Ruby code (living in that `20130717151933_create_high_scores.rb`) to modify the schema of our database. Which database? The SQLite3 database that Rails will create for you when we run the `rails db:migrate` command. We'll talk more about that command below.
```bash
-$ bin/rake db:migrate
+$ rails db:migrate
== CreateHighScores: migrating ===============================================
-- create_table(:high_scores)
-> 0.0017s
== CreateHighScores: migrated (0.0019s) ======================================
```
-INFO: Let's talk about unit tests. Unit tests are code that tests and makes assertions about code. In unit testing, we take a little part of code, say a method of a model, and test its inputs and outputs. Unit tests are your friend. The sooner you make peace with the fact that your quality of life will drastically increase when you unit test your code, the better. Seriously. We'll make one in a moment.
+INFO: Let's talk about unit tests. Unit tests are code that tests and makes assertions
+about code. In unit testing, we take a little part of code, say a method of a model,
+and test its inputs and outputs. Unit tests are your friend. The sooner you make
+peace with the fact that your quality of life will drastically increase when you unit
+test your code, the better. Seriously. Please visit
+[the testing guide](https://guides.rubyonrails.org/testing.html) for an in-depth
+look at unit testing.
Let's see the interface Rails created for us.
```bash
-$ bin/rails server
+$ rails server
```
Go to your browser and open [http://localhost:3000/high_scores](http://localhost:3000/high_scores), now we can create new high scores (55,160 on Space Invaders!)
@@ -279,14 +328,14 @@ INFO: You can also use the alias "c" to invoke the console: `rails c`.
You can specify the environment in which the `console` command should operate.
```bash
-$ bin/rails console staging
+$ rails console -e staging
```
If you wish to test out some code without changing any data, you can do that by invoking `rails console --sandbox`.
```bash
-$ bin/rails console --sandbox
-Loading development environment in sandbox (Rails 5.0.0)
+$ rails console --sandbox
+Loading development environment in sandbox (Rails 5.1.0)
Any modifications you make will be rolled back on exit
irb(main):001:0>
```
@@ -318,7 +367,7 @@ With the `helper` method it is possible to access Rails and your application's h
### `rails dbconsole`
-`rails dbconsole` figures out which database you're using and drops you into whichever command line interface you would use with it (and figures out the command line parameters to give to it, too!). It supports MySQL, PostgreSQL, SQLite and SQLite3.
+`rails dbconsole` figures out which database you're using and drops you into whichever command line interface you would use with it (and figures out the command line parameters to give to it, too!). It supports MySQL (including MariaDB), PostgreSQL, and SQLite3.
INFO: You can also use the alias "db" to invoke the dbconsole: `rails db`.
@@ -327,7 +376,7 @@ INFO: You can also use the alias "db" to invoke the dbconsole: `rails db`.
`runner` runs Ruby code in the context of Rails non-interactively. For instance:
```bash
-$ bin/rails runner "Model.long_running_method"
+$ rails runner "Model.long_running_method"
```
INFO: You can also use the alias "r" to invoke the runner: `rails r`.
@@ -335,13 +384,13 @@ INFO: You can also use the alias "r" to invoke the runner: `rails r`.
You can specify the environment in which the `runner` command should operate using the `-e` switch.
```bash
-$ bin/rails runner -e staging "Model.long_running_method"
+$ rails runner -e staging "Model.long_running_method"
```
You can even execute ruby code written in a file with runner.
```bash
-$ bin/rails runner lib/code_to_be_run.rb
+$ rails runner lib/code_to_be_run.rb
```
### `rails destroy`
@@ -351,7 +400,7 @@ Think of `destroy` as the opposite of `generate`. It'll figure out what generate
INFO: You can also use the alias "d" to invoke the destroy command: `rails d`.
```bash
-$ bin/rails generate model Oops
+$ rails generate model Oops
invoke active_record
create db/migrate/20120528062523_create_oops.rb
create app/models/oops.rb
@@ -360,7 +409,7 @@ $ bin/rails generate model Oops
create test/fixtures/oops.yml
```
```bash
-$ bin/rails destroy model Oops
+$ rails destroy model Oops
invoke active_record
remove db/migrate/20120528062523_create_oops.rb
remove app/models/oops.rb
@@ -369,151 +418,158 @@ $ bin/rails destroy model Oops
remove test/fixtures/oops.yml
```
-Rake
-----
-
-Rake is Ruby Make, a standalone Ruby utility that replaces the Unix utility 'make', and uses a 'Rakefile' and `.rake` files to build up a list of tasks. In Rails, Rake is used for common administration tasks, especially sophisticated ones that build off of each other.
-
-You can get a list of Rake tasks available to you, which will often depend on your current directory, by typing `rake --tasks`. Each task has a description, and should help you find the thing you need.
-
-To get the full backtrace for running rake task you can pass the option `--trace` to command line, for example `rake db:create --trace`.
-
-```bash
-$ bin/rake --tasks
-rake about # List versions of all Rails frameworks and the environment
-rake assets:clean # Remove old compiled assets
-rake assets:clobber # Remove compiled assets
-rake assets:precompile # Compile all the assets named in config.assets.precompile
-rake db:create # Create the database from config/database.yml for the current Rails.env
-...
-rake log:clear # Truncates all *.log files in log/ to zero bytes (specify which logs with LOGS=test,development)
-rake middleware # Prints out your Rack middleware stack
-...
-rake tmp:clear # Clear cache and socket files from tmp/ (narrow w/ tmp:cache:clear, tmp:sockets:clear)
-rake tmp:create # Creates tmp directories for cache, sockets, and pids
-```
-INFO: You can also use `rake -T` to get the list of tasks.
-
-### `about`
+### `rails about`
-`rake about` gives information about version numbers for Ruby, RubyGems, Rails, the Rails subcomponents, your application's folder, the current Rails environment name, your app's database adapter, and schema version. It is useful when you need to ask for help, check if a security patch might affect you, or when you need some stats for an existing Rails installation.
+`rails about` gives information about version numbers for Ruby, RubyGems, Rails, the Rails subcomponents, your application's folder, the current Rails environment name, your app's database adapter, and schema version. It is useful when you need to ask for help, check if a security patch might affect you, or when you need some stats for an existing Rails installation.
```bash
-$ bin/rake about
+$ rails about
About your application's environment
-Rails version 5.0.0
-Ruby version 2.2.2 (x86_64-linux)
-RubyGems version 2.4.6
-Rack version 1.6
+Rails version 6.0.0
+Ruby version 2.5.0 (x86_64-linux)
+RubyGems version 2.7.3
+Rack version 2.0.4
JavaScript Runtime Node.js (V8)
-Middleware Rack::Sendfile, ActionDispatch::Static, Rack::Lock, #<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, ActiveRecord::ConnectionAdapters::ConnectionManagement, ActiveRecord::QueryCache, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, ActionDispatch::ParamsParser, 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
-Database schema version 20110805173523
+Database schema version 20180205173523
```
-### `assets`
+### `rails assets:`
-You can precompile the assets in `app/assets` using `rake assets:precompile`, and remove older compiled assets using `rake assets:clean`. The `assets:clean` task allows for rolling deploys that may still be linking to an old asset while the new assets are being built.
+You can precompile the assets in `app/assets` using `rails assets:precompile`, and remove older compiled assets using `rails assets:clean`. The `assets:clean` command allows for rolling deploys that may still be linking to an old asset while the new assets are being built.
-If you want to clear `public/assets` completely, you can use `rake assets:clobber`.
+If you want to clear `public/assets` completely, you can use `rails assets:clobber`.
-### `db`
+### `rails db:`
-The most common tasks of the `db:` Rake namespace are `migrate` and `create`, and it will pay off to try out all of the migration rake tasks (`up`, `down`, `redo`, `reset`). `rake db:version` is useful when troubleshooting, telling you the current version of the database.
+The most common commands of the `db:` rails namespace are `migrate` and `create`, and it will pay off to try out all of the migration rails commands (`up`, `down`, `redo`, `reset`). `rails db:version` is useful when troubleshooting, telling you the current version of the database.
More information about migrations can be found in the [Migrations](active_record_migrations.html) guide.
-### `notes`
+### `rails notes`
-`rake notes` will search through your code for comments beginning with FIXME, OPTIMIZE or TODO. The search is done in files with extension `.builder`, `.rb`, `.rake`, `.yml`, `.yaml`, `.ruby`, `.css`, `.js` and `.erb` for both default and custom annotations.
+`rails notes` searches through your code for comments beginning with a specific keyword. You can refer to `rails notes --help` for information about usage.
+
+By default, it will search in `app`, `config`, `db`, `lib`, and `test` directories for FIXME, OPTIMIZE, and TODO annotations in files with extension `.builder`, `.rb`, `.rake`, `.yml`, `.yaml`, `.ruby`, `.css`, `.js`, and `.erb`.
```bash
-$ bin/rake notes
-(in /home/foobar/commandsapp)
+$ rails notes
app/controllers/admin/users_controller.rb:
* [ 20] [TODO] any other way to do this?
* [132] [FIXME] high priority for next deploy
-app/models/school.rb:
+lib/school.rb:
* [ 13] [OPTIMIZE] refactor this code to make it faster
* [ 17] [FIXME]
```
-You can add support for new file extensions using `config.annotations.register_extensions` option, which receives a list of the extensions with its corresponding regex to match it up.
-
-```ruby
-config.annotations.register_extensions("scss", "sass", "less") { |annotation| /\/\/\s*(#{annotation}):?\s*(.*)$/ }
-```
+#### Annotations
-If you are looking for a specific annotation, say FIXME, you can use `rake notes:fixme`. Note that you have to lower case the annotation's name.
+You can pass specific annotations by using the `--annotations` argument. By default, it will search for FIXME, OPTIMIZE, and TODO.
+Note that annotations are case sensitive.
```bash
-$ bin/rake notes:fixme
-(in /home/foobar/commandsapp)
+$ rails notes --annotations FIXME RELEASE
app/controllers/admin/users_controller.rb:
- * [132] high priority for next deploy
+ * [101] [RELEASE] We need to look at this before next release
+ * [132] [FIXME] high priority for next deploy
-app/models/school.rb:
- * [ 17]
+lib/school.rb:
+ * [ 17] [FIXME]
```
-You can also use custom annotations in your code and list them using `rake notes:custom` by specifying the annotation using an environment variable `ANNOTATION`.
+#### Directories
+
+You can add more default directories to search from by using `config.annotations.register_directories`. It receives a list of directory names.
+
+```ruby
+config.annotations.register_directories("spec", "vendor")
+```
```bash
-$ bin/rake notes:custom ANNOTATION=BUG
-(in /home/foobar/commandsapp)
-app/models/article.rb:
- * [ 23] Have to fix this one before pushing!
+$ rails notes
+app/controllers/admin/users_controller.rb:
+ * [ 20] [TODO] any other way to do this?
+ * [132] [FIXME] high priority for next deploy
+
+lib/school.rb:
+ * [ 13] [OPTIMIZE] Refactor this code to make it faster
+ * [ 17] [FIXME]
+
+spec/models/user_spec.rb:
+ * [122] [TODO] Verify the user that has a subscription works
+
+vendor/tools.rb:
+ * [ 56] [TODO] Get rid of this dependency
```
-NOTE. When using specific annotations and custom annotations, the annotation name (FIXME, BUG etc) is not displayed in the output lines.
+#### Extensions
+
+You can add more default file extensions to search from by using `config.annotations.register_extensions`. It receives a list of extensions with its corresponding regex to match it up.
-By default, `rake notes` will look in the `app`, `config`, `db`, `lib` and `test` directories. If you would like to search other directories, you can provide them as a comma separated list in an environment variable `SOURCE_ANNOTATION_DIRECTORIES`.
+```ruby
+config.annotations.register_extensions("scss", "sass") { |annotation| /\/\/\s*(#{annotation}):?\s*(.*)$/ }
+```
```bash
-$ export SOURCE_ANNOTATION_DIRECTORIES='spec,vendor'
-$ bin/rake notes
-(in /home/foobar/commandsapp)
-app/models/user.rb:
- * [ 35] [FIXME] User should have a subscription at this point
+$ rails notes
+app/controllers/admin/users_controller.rb:
+ * [ 20] [TODO] any other way to do this?
+ * [132] [FIXME] high priority for next deploy
+
+app/assets/stylesheets/application.css.sass:
+ * [ 34] [TODO] Use pseudo element for this class
+
+app/assets/stylesheets/application.css.scss:
+ * [ 1] [TODO] Split into multiple components
+
+lib/school.rb:
+ * [ 13] [OPTIMIZE] Refactor this code to make it faster
+ * [ 17] [FIXME]
+
spec/models/user_spec.rb:
* [122] [TODO] Verify the user that has a subscription works
+
+vendor/tools.rb:
+ * [ 56] [TODO] Get rid of this dependency
```
-### `routes`
+### `rails routes`
-`rake routes` will list all of your defined routes, which is useful for tracking down routing problems in your app, or giving you a good overview of the URLs in an app you're trying to get familiar with.
+`rails routes` will list all of your defined routes, which is useful for tracking down routing problems in your app, or giving you a good overview of the URLs in an app you're trying to get familiar with.
-### `test`
+### `rails test`
INFO: A good description of unit testing in Rails is given in [A Guide to Testing Rails Applications](testing.html)
-Rails comes with a test suite called Minitest. Rails owes its stability to the use of tests. The tasks available in the `test:` namespace helps in running the different tests you will hopefully write.
+Rails comes with a test framework called minitest. Rails owes its stability to the use of tests. The commands available in the `test:` namespace helps in running the different tests you will hopefully write.
-### `tmp`
+### `rails tmp:`
The `Rails.root/tmp` directory is, like the *nix /tmp directory, the holding place for temporary files like process id files and cached actions.
-The `tmp:` namespaced tasks will help you clear and create the `Rails.root/tmp` directory:
+The `tmp:` namespaced commands will help you clear and create the `Rails.root/tmp` directory:
-* `rake tmp:cache:clear` clears `tmp/cache`.
-* `rake tmp:sockets:clear` clears `tmp/sockets`.
-* `rake tmp:clear` clears all cache and sockets files.
-* `rake tmp:create` creates tmp directories for cache, sockets and pids.
+* `rails tmp:cache:clear` clears `tmp/cache`.
+* `rails tmp:sockets:clear` clears `tmp/sockets`.
+* `rails tmp:screenshots:clear` clears `tmp/screenshots`.
+* `rails tmp:clear` clears all cache, sockets, and screenshot files.
+* `rails tmp:create` creates tmp directories for cache, sockets, and pids.
### Miscellaneous
-* `rake stats` is great for looking at statistics on your code, displaying things like KLOCs (thousands of lines of code) and your code to test ratio.
-* `rake secret` will give you a pseudo-random key to use for your session secret.
-* `rake time:zones:all` lists all the timezones Rails knows about.
+* `rails stats` is great for looking at statistics on your code, displaying things like KLOCs (thousands of lines of code) and your code to test ratio.
+* `rails secret` will give you a pseudo-random key to use for your session secret.
+* `rails time:zones:all` lists all the timezones Rails knows about.
### Custom Rake Tasks
Custom rake tasks have a `.rake` extension and are placed in
`Rails.root/lib/tasks`. You can create these custom rake tasks with the
-`bin/rails generate task` command.
+`rails generate task` command.
```ruby
desc "I am short, but comprehensive description for my cool task"
@@ -545,12 +601,12 @@ end
Invocation of the tasks will look like:
```bash
-$ bin/rake task_name
-$ bin/rake "task_name[value 1]" # entire argument string should be quoted
-$ bin/rake db:nothing
+$ rails task_name
+$ rails "task_name[value 1]" # entire argument string should be quoted
+$ rails db:nothing
```
-NOTE: If your need to interact with your application models, perform database queries and so on, your task should depend on the `environment` task, which will load your application code.
+NOTE: If your need to interact with your application models, perform database queries, and so on, your task should depend on the `environment` task, which will load your application code.
The Rails Advanced Command Line
-------------------------------
@@ -578,8 +634,8 @@ $ rails new . --git --database=postgresql
create tmp/pids
create Rakefile
add 'Rakefile'
- create README.rdoc
-add 'README.rdoc'
+ create README.md
+add 'README.md'
create app/controllers/application_controller.rb
add 'app/controllers/application_controller.rb'
create app/helpers/application_helper.rb
@@ -592,13 +648,13 @@ We had to create the **gitapp** directory and initialize an empty git repository
```bash
$ cat config/database.yml
-# PostgreSQL. Versions 8.2 and up are supported.
+# PostgreSQL. Versions 9.1 and up are supported.
#
# Install the pg driver:
# gem install pg
-# On OS X with Homebrew:
+# On macOS with Homebrew:
# gem install pg -- --with-pg-config=/usr/local/bin/pg_config
-# On OS X with MacPorts:
+# On macOS with MacPorts:
# gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config
# On Windows:
# gem install pg
@@ -608,17 +664,20 @@ $ cat config/database.yml
# Configure Using Gemfile
# gem 'pg'
#
-development:
+default: &default
adapter: postgresql
encoding: unicode
+ # For details on connection pooling, see Rails configuration guide
+ # https://guides.rubyonrails.org/configuring.html#database-pooling
+ pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
+
+development:
+ <<: *default
database: gitapp_development
- pool: 5
- username: gitapp
- password:
...
...
```
-It also generated some lines in our database.yml configuration corresponding to our choice of PostgreSQL for database.
+It also generated some lines in our `database.yml` configuration corresponding to our choice of PostgreSQL for database.
NOTE. The only catch with using the SCM options is that you have to make your application's directory first, then initialize your SCM, then you can run the `rails new` command to generate the basis of your app.
diff --git a/guides/source/configuring.md b/guides/source/configuring.md
index 79a80de3cc..de6766e12e 100644
--- a/guides/source/configuring.md
+++ b/guides/source/configuring.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Configuring Rails Applications
==============================
@@ -32,10 +32,10 @@ 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.autoload_paths += %W(#{config.root}/extras)
+config.time_zone = 'Central Time (US & Canada)'
```
This is a setting for Rails itself. If you want to pass settings to individual Rails components, you can do so via the same `config` object in `config/application.rb`:
@@ -60,22 +60,20 @@ These configuration methods are to be called on a `Rails::Railtie` object, such
* `config.asset_host` sets the host for the assets. Useful when CDNs are used for hosting assets, or when you want to work around the concurrency constraints built-in in browsers using different domain aliases. Shorter version of `config.action_controller.asset_host`.
-* `config.autoload_once_paths` accepts an array of paths from which Rails will autoload constants that won't be wiped per request. Relevant if `config.cache_classes` is false, which is the case in development mode by default. Otherwise, all autoloading happens only once. All elements of this array must also be in `autoload_paths`. Default is an empty array.
+* `config.autoload_once_paths` accepts an array of paths from which Rails will autoload constants that won't be wiped per request. Relevant if `config.cache_classes` is `false`, which is the case in development mode by default. Otherwise, all autoloading happens only once. All elements of this array must also be in `autoload_paths`. Default is an empty array.
-* `config.autoload_paths` accepts an array of paths from which Rails will autoload constants. Default is all directories under `app`.
+* `config.autoload_paths` accepts an array of paths from which Rails will autoload constants. Default is all directories under `app`. It is no longer recommended to adjust this. See [Autoloading and Reloading Constants](autoloading_and_reloading_constants.html#autoload-paths-and-eager-load-paths)
-* `config.cache_classes` controls whether or not application classes and modules should be reloaded on each request. Defaults to false in development mode, and true in test and production modes.
-
-* `config.action_view.cache_template_loading` controls whether or not templates should be reloaded on each request. Defaults to whatever is set for `config.cache_classes`.
+* `config.cache_classes` controls whether or not application classes and modules should be reloaded on each request. Defaults to `false` in development mode, and `true` in test and production modes.
* `config.beginning_of_week` sets the default beginning of week for the
application. Accepts a valid week day symbol (e.g. `:monday`).
-* `config.cache_store` configures which cache store to use for Rails caching. Options include one of the symbols `:memory_store`, `:file_store`, `:mem_cache_store`, `:null_store`, or an object that implements the cache API. Defaults to `:file_store` if the directory `tmp/cache` exists, and to `:memory_store` otherwise.
+* `config.cache_store` configures which cache store to use for Rails caching. Options include one of the symbols `:memory_store`, `:file_store`, `:mem_cache_store`, `:null_store`, `:redis_cache_store`, or an object that implements the cache API. Defaults to `:file_store`.
-* `config.colorize_logging` specifies whether or not to use ANSI color codes when logging information. Defaults to true.
+* `config.colorize_logging` specifies whether or not to use ANSI color codes when logging information. Defaults to `true`.
-* `config.consider_all_requests_local` is a flag. If true then any error will cause detailed debugging information to be dumped in the HTTP response, and the `Rails::Info` controller will show the application runtime context in `/rails/info/properties`. True by default in development and test environments, and false in production mode. For finer-grained control, set this to false and implement `local_request?` in controllers to specify which requests should provide debugging information on errors.
+* `config.consider_all_requests_local` is a flag. If `true` then any error will cause detailed debugging information to be dumped in the HTTP response, and the `Rails::Info` controller will show the application runtime context in `/rails/info/properties`. `true` by default in development and test environments, and `false` in production mode. For finer-grained control, set this to `false` and implement `local_request?` in controllers to specify which requests should provide debugging information on errors.
* `config.console` allows you to set class that will be used as console you run `rails console`. It's best to run it in `console` block:
@@ -88,25 +86,29 @@ application. Accepts a valid week day symbol (e.g. `:monday`).
end
```
-* `config.eager_load` when true, eager loads all registered `config.eager_load_namespaces`. This includes your application, engines, Rails frameworks and any other registered namespace.
+* `config.eager_load` when `true`, eager loads all registered `config.eager_load_namespaces`. This includes your application, engines, Rails frameworks, and any other registered namespace.
-* `config.eager_load_namespaces` registers namespaces that are eager loaded when `config.eager_load` is true. All namespaces in the list must respond to the `eager_load!` method.
+* `config.eager_load_namespaces` registers namespaces that are eager loaded when `config.eager_load` is `true`. All namespaces in the list must respond to the `eager_load!` method.
* `config.eager_load_paths` accepts an array of paths from which Rails will eager load on boot if cache classes is enabled. Defaults to every folder in the `app` directory of the application.
+* `config.enable_dependency_loading`: when true, enables autoloading, even if the application is eager loaded and `config.cache_classes` is set as true. Defaults to false.
+
* `config.encoding` sets up the application-wide encoding. Defaults to UTF-8.
* `config.exceptions_app` sets the exceptions application invoked by the ShowException middleware when an exception happens. Defaults to `ActionDispatch::PublicExceptions.new(Rails.public_path)`.
-* `config.file_watcher` the class used to detect file updates in the filesystem when `config.reload_classes_only_on_change` is true. Must conform to `ActiveSupport::FileUpdateChecker` API.
+* `config.debug_exception_response_format` sets the format used in responses when errors occur in development mode. Defaults to `:api` for API only apps and `:default` for normal apps.
+
+* `config.file_watcher` is the class used to detect file updates in the file system when `config.reload_classes_only_on_change` is `true`. Rails ships with `ActiveSupport::FileUpdateChecker`, the default, and `ActiveSupport::EventedFileUpdateChecker` (this one depends on the [listen](https://github.com/guard/listen) gem). Custom classes must conform to the `ActiveSupport::FileUpdateChecker` API.
* `config.filter_parameters` used for filtering out the parameters that
you don't want shown in the logs, such as passwords or credit card
-numbers. New applications filter out passwords by adding the following `config.filter_parameters+=[:password]` in `config/initializers/filter_parameter_logging.rb`.
+numbers. It also filters out sensitive values of database columns when call `#inspect` on an Active Record object. 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 under HTTPS protocol by using `ActionDispatch::SSL` middleware.
+* `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 except production, where it defaults to `Logger::Formatter`.
+* `config.log_formatter` defines the formatter of the Rails logger. This option defaults to an instance of `ActiveSupport::Logger::SimpleFormatter` for all modes. If you are setting a value for `config.logger` you must manually pass the value of your formatter to your logger before it is wrapped in an `ActiveSupport::TaggedLogging` instance, Rails will not do it for you.
* `config.log_level` defines the verbosity of the Rails logger. This option
defaults to `:debug` for all environments. The available log levels are: `:debug`,
@@ -114,17 +116,30 @@ defaults to `:debug` for all environments. The available log levels are: `:debug
* `config.log_tags` accepts a list of: methods that the `request` object responds to, a `Proc` that accepts the `request` object, or something that responds to `to_s`. This makes it easy to tag log lines with debug information like subdomain and request id - both very helpful in debugging multi-user production applications.
-* `config.logger` accepts a logger conforming to the interface of Log4r or the default Ruby `Logger` class. Defaults to an instance of `ActiveSupport::Logger`.
+* `config.logger` is the logger that will be used for `Rails.logger` and any related Rails logging such as `ActiveRecord::Base.logger`. It defaults to an instance of `ActiveSupport::TaggedLogging` that wraps an instance of `ActiveSupport::Logger` which outputs a log to the `log/` directory. You can supply a custom logger, to get full compatibility you must follow these guidelines:
+ * To support a formatter, you must manually assign a formatter from the `config.log_formatter` value to the logger.
+ * To support tagged logs, the log instance must be wrapped with `ActiveSupport::TaggedLogging`.
+ * To support silencing, the logger must include `ActiveSupport::LoggerSilence` module. The `ActiveSupport::Logger` class already includes these modules.
+
+ ```ruby
+ class MyLogger < ::Logger
+ include ActiveSupport::LoggerSilence
+ end
+
+ mylogger = MyLogger.new(STDOUT)
+ mylogger.formatter = config.log_formatter
+ config.logger = ActiveSupport::TaggedLogging.new(mylogger)
+ ```
* `config.middleware` allows you to configure the application's middleware. This is covered in depth in the [Configuring Middleware](#configuring-middleware) section below.
-* `config.reload_classes_only_on_change` enables or disables reloading of classes only when tracked files change. By default tracks everything on autoload paths and is set to true. If `config.cache_classes` is true, this option is ignored.
+* `config.reload_classes_only_on_change` enables or disables reloading of classes only when tracked files change. By default tracks everything on autoload paths and is set to `true`. If `config.cache_classes` is `true`, this option is ignored.
-* `secrets.secret_key_base` is used for specifying a key which allows sessions for the application to be verified against a known secure key to prevent tampering. Applications get `secrets.secret_key_base` initialized to a random key present in `config/secrets.yml`.
+* `secret_key_base` is used for specifying a key which allows sessions for the application to be verified against a known secure key to prevent tampering. Applications get a random generated key in test and development environments, other environments should set one in `config/credentials.yml.enc`.
-* `config.serve_static_files` configures Rails to serve static files. This option defaults to true, but in the production environment it is set to false because the server software (e.g. NGINX or Apache) used to run the application should serve static files instead. If you are running or testing your app in production mode using WEBrick (it is not recommended to use WEBrick in production) set the option to true. Otherwise, you won't be able to use page caching and request for files that exist under the public directory.
+* `config.public_file_server.enabled` configures Rails to serve static files from the public directory. This option defaults to `true`, but in the production environment it is set to `false` because the server software (e.g. NGINX or Apache) used to run the application should serve static files instead. If you are running or testing your app in production mode using WEBrick (it is not recommended to use WEBrick in production) set the option to `true.` Otherwise, you won't be able to use page caching and request for files that exist under the public directory.
-* `config.session_store` is usually set up in `config/initializers/session_store.rb` and specifies what class to use to store the session. Possible values are `:cookie_store` which is the default, `:mem_cache_store`, and `:disabled`. The last one tells Rails not to deal with sessions. Custom session stores can also be specified:
+* `config.session_store` specifies what class to use to store the session. Possible values are `:cookie_store` which is the default, `:mem_cache_store`, and `:disabled`. The last one tells Rails not to deal with sessions. Defaults to a cookie store with application name as the session key. Custom session stores can also be specified:
```ruby
config.session_store :my_custom_store
@@ -137,33 +152,35 @@ defaults to `:debug` for all environments. The available log levels are: `:debug
### Configuring Assets
* `config.assets.enabled` a flag that controls whether the asset
-pipeline is enabled. It is set to true by default.
-
-* `config.assets.raise_runtime_errors` Set this flag to `true` to enable additional runtime error checking. Recommended in `config/environments/development.rb` to minimize unexpected behavior when deploying to `production`.
-
-* `config.assets.compress` a flag that enables the compression of compiled assets. It is explicitly set to true in `config/environments/production.rb`.
+pipeline is enabled. It is set to `true` by default.
* `config.assets.css_compressor` defines the CSS compressor to use. It is set by default by `sass-rails`. The unique alternative value at the moment is `:yui`, which uses the `yui-compressor` gem.
* `config.assets.js_compressor` defines the JavaScript compressor to use. Possible values are `:closure`, `:uglifier` and `:yui` which require the use of the `closure-compiler`, `uglifier` or `yui-compressor` gems respectively.
+* `config.assets.gzip` a flag that enables the creation of gzipped version of compiled assets, along with non-gzipped assets. Set to `true` by default.
+
* `config.assets.paths` contains the paths which are used to look for assets. Appending paths to this configuration option will cause those paths to be used in the search for assets.
* `config.assets.precompile` allows you to specify additional assets (other than `application.css` and `application.js`) which are to be precompiled when `rake assets:precompile` is run.
+* `config.assets.unknown_asset_fallback` allows you to modify the behavior of the asset pipeline when an asset is not in the pipeline, if you use sprockets-rails 3.2.0 or newer. Defaults to `false`.
+
* `config.assets.prefix` defines the prefix where assets are served from. Defaults to `/assets`.
* `config.assets.manifest` defines the full path to be used for the asset precompiler's manifest file. Defaults to a file named `manifest-<random>.json` in the `config.assets.prefix` directory within the public folder.
-* `config.assets.digest` enables the use of MD5 fingerprints in asset names. Set to `true` by default in `production.rb` and `development.rb`.
+* `config.assets.digest` enables the use of SHA256 fingerprints in asset names. Set to `true` by default.
* `config.assets.debug` disables the concatenation and compression of assets. Set to `true` by default in `development.rb`.
-* `config.assets.cache_store` defines the cache store that Sprockets will use. The default is the Rails file store.
+* `config.assets.version` is an option string that is used in SHA256 hash generation. This can be changed to force all files to be recompiled.
* `config.assets.compile` is a boolean that can be used to turn on live Sprockets compilation in production.
-* `config.assets.logger` accepts a logger conforming to the interface of Log4r or the default Ruby `Logger` class. Defaults to the same configured at `config.logger`. Setting `config.assets.logger` to false will turn off served assets logging.
+* `config.assets.logger` accepts a logger conforming to the interface of Log4r or the default Ruby `Logger` class. Defaults to the same configured at `config.logger`. Setting `config.assets.logger` to `false` will turn off served assets logging.
+
+* `config.assets.quiet` disables logging of assets requests. Set to `true` by default in `development.rb`.
### Configuring Generators
@@ -182,8 +199,7 @@ The full set of methods that can be used in this block are as follows:
* `force_plural` allows pluralized model names. Defaults to `false`.
* `helper` defines whether or not to generate helpers. Defaults to `true`.
* `integration_tool` defines which integration tool to use to generate integration tests. Defaults to `:test_unit`.
-* `javascripts` turns on the hook for JavaScript files in generators. Used in Rails for when the `scaffold` generator is run. Defaults to `true`.
-* `javascript_engine` configures the engine to be used (for eg. coffee) when generating assets. Defaults to `:js`.
+* `system_tests` defines which integration tool to use to generate system tests. Defaults to `:test_unit`.
* `orm` defines which orm to use. Defaults to `false` and will use Active Record by default.
* `resource_controller` defines which generator to use for generating a controller when using `rails generate resource`. Defaults to `:controller`.
* `resource_route` defines whether a resource route definition should be generated
@@ -191,16 +207,17 @@ The full set of methods that can be used in this block are as follows:
* `scaffold_controller` different from `resource_controller`, defines which generator to use for generating a _scaffolded_ controller when using `rails generate scaffold`. Defaults to `:scaffold_controller`.
* `stylesheets` turns on the hook for stylesheets in generators. Used in Rails for when the `scaffold` generator is run, but this hook can be used in other generates as well. Defaults to `true`.
* `stylesheet_engine` configures the stylesheet engine (for eg. sass) to be used when generating assets. Defaults to `:css`.
-* `test_framework` defines which test framework to use. Defaults to `false` and will use Minitest by default.
+* `scaffold_stylesheet` creates `scaffold.css` when generating a scaffolded resource. Defaults to `true`.
+* `test_framework` defines which test framework to use. Defaults to `false` and will use minitest by default.
* `template_engine` defines which template engine to use, such as ERB or Haml. Defaults to `:erb`.
### Configuring Middleware
Every Rails application comes with a standard set of middleware which it uses in this order in the development environment:
-* `ActionDispatch::SSL` forces every request to be under HTTPS protocol. Will be available if `config.force_ssl` is set to `true`. Options passed to this can be configured by using `config.ssl_options`.
-* `ActionDispatch::Static` is used to serve static assets. Disabled if `config.serve_static_files` is `false`. Set `config.static_index` if you need to serve a static directory index file that is not named `index`. For example, to serve `main.html` instead of `index.html` for directory requests, set `config.static_index` to `"main"`.
-* `Rack::Lock` wraps the app in mutex so it can only be called by a single thread at a time. Only enabled when `config.cache_classes` is `false`.
+* `ActionDispatch::SSL` forces every request to be served using HTTPS. Enabled if `config.force_ssl` is set to `true`. Options passed to this can be configured by setting `config.ssl_options`.
+* `ActionDispatch::Static` is used to serve static assets. Disabled if `config.public_file_server.enabled` is `false`. Set `config.public_file_server.index_name` if you need to serve a static directory index file that is not named `index`. For example, to serve `main.html` instead of `index.html` for directory requests, set `config.public_file_server.index_name` to `"main"`.
+* `ActionDispatch::Executor` allows thread safe code reloading. Disabled if `config.allow_concurrency` is `false`, which causes `Rack::Lock` to be loaded. `Rack::Lock` wraps the app in mutex so it can only be called by a single thread at a time.
* `ActiveSupport::Cache::Strategy::LocalCache` serves as a basic memory backed cache. This cache is not thread safe and is intended only for serving as a temporary memory cache for a single thread.
* `Rack::Runtime` sets an `X-Runtime` header, containing the time (in seconds) taken to execute the request.
* `Rails::Rack::Logger` notifies the logs that the request has begun. After request is complete, flushes all the logs.
@@ -209,12 +226,9 @@ Every Rails application comes with a standard set of middleware which it uses in
* `ActionDispatch::RemoteIp` checks for IP spoofing attacks and gets valid `client_ip` from request headers. Configurable with the `config.action_dispatch.ip_spoofing_check`, and `config.action_dispatch.trusted_proxies` options.
* `Rack::Sendfile` intercepts responses whose body is being served from a file and replaces it with a server specific X-Sendfile header. Configurable with `config.action_dispatch.x_sendfile_header`.
* `ActionDispatch::Callbacks` runs the prepare callbacks before serving the request.
-* `ActiveRecord::ConnectionAdapters::ConnectionManagement` cleans active connections after each request, unless the `rack.test` key in the request environment is set to `true`.
-* `ActiveRecord::QueryCache` caches all SELECT queries generated in a request. If any INSERT or UPDATE takes place then the cache is cleaned.
* `ActionDispatch::Cookies` sets cookies for the request.
* `ActionDispatch::Session::CookieStore` is responsible for storing the session in cookies. An alternate middleware can be used for this by changing the `config.action_controller.session_store` to an alternate value. Additionally, options passed to this can be configured by using `config.action_controller.session_options`.
* `ActionDispatch::Flash` sets up the `flash` keys. Only available if `config.action_controller.session_store` is set to a value.
-* `ActionDispatch::ParamsParser` parses out parameters from the request into `params`.
* `Rack::MethodOverride` allows the method to be overridden if `params[:_method]` is set. This is the middleware which supports the PATCH, PUT, and DELETE HTTP method types.
* `Rack::Head` converts HEAD requests to GET requests and serves them as so.
@@ -230,6 +244,12 @@ This will put the `Magical::Unicorns` middleware on the end of the stack. You ca
config.middleware.insert_before Rack::Head, Magical::Unicorns
```
+Or you can insert a middleware to exact position by using indexes. For example, if you want to insert `Magical::Unicorns` middleware on top of the stack, you can do it, like so:
+
+```ruby
+config.middleware.insert_before 0, Magical::Unicorns
+```
+
There's also `insert_after` which will insert a middleware after another:
```ruby
@@ -245,14 +265,14 @@ config.middleware.swap ActionController::Failsafe, Lifo::Failsafe
They can also be removed from the stack completely:
```ruby
-config.middleware.delete "Rack::MethodOverride"
+config.middleware.delete Rack::MethodOverride
```
### Configuring i18n
All these configuration options are delegated to the `I18n` library.
-* `config.i18n.available_locales` whitelists the available locales for the app. Defaults to all locale keys found in locale files, usually only `:en` on a new application.
+* `config.i18n.available_locales` defines the permitted available locales for the app. Defaults to all locale keys found in locale files, usually only `:en` on a new application.
* `config.i18n.default_locale` sets the default locale of an application used for i18n. Defaults to `:en`.
@@ -260,6 +280,32 @@ All these configuration options are delegated to the `I18n` library.
* `config.i18n.load_path` sets the path Rails uses to look for locale files. Defaults to `config/locales/*.{yml,rb}`.
+* `config.i18n.fallbacks` sets fallback behavior for missing translations. Here are 3 usage examples for this option:
+
+ * You can set the option to `true` for using default locale as fallback, like so:
+
+ ```ruby
+ config.i18n.fallbacks = true
+ ```
+
+ * Or you can set an array of locales as fallback, like so:
+
+ ```ruby
+ config.i18n.fallbacks = [:tr, :en]
+ ```
+
+ * Or you can set different fallbacks for locales individually. For example, if you want to use `:tr` for `:az` and `:de`, `:en` for `:da` as fallbacks, you can do it, like so:
+
+ ```ruby
+ config.i18n.fallbacks = { az: :tr, da: [:de, :en] }
+ #or
+ config.i18n.fallbacks.map = { az: :tr, da: [:de, :en] }
+ ```
+
+### Configuring Active Model
+
+* `config.active_model.i18n_full_message` is a boolean value which controls whether the `full_message` error format can be overridden at the attribute or model level in the locale files. This is `false` by default.
+
### Configuring Active Record
`config.active_record` includes a variety of configuration options:
@@ -267,8 +313,8 @@ All these configuration options are delegated to the `I18n` library.
* `config.active_record.logger` accepts a logger conforming to the interface of Log4r or the default Ruby Logger class, which is then passed on to any new database connections made. You can retrieve this logger by calling `logger` on either an Active Record model class or an Active Record model instance. Set to `nil` to disable logging.
* `config.active_record.primary_key_prefix_type` lets you adjust the naming for primary key columns. By default, Rails assumes that primary key columns are named `id` (and this configuration option doesn't need to be set.) There are two other choices:
-** `:table_name` would make the primary key for the Customer class `customerid`
-** `:table_name_with_underscore` would make the primary key for the Customer class `customer_id`
+ * `:table_name` would make the primary key for the Customer class `customerid`.
+ * `:table_name_with_underscore` would make the primary key for the Customer class `customer_id`.
* `config.active_record.table_name_prefix` lets you set a global string to be prepended to table names. If you set this to `northwest_`, then the Customer class will look for `northwest_customers` as its table. The default is an empty string.
@@ -276,15 +322,21 @@ All these configuration options are delegated to the `I18n` library.
* `config.active_record.schema_migrations_table_name` lets you set a string to be used as the name of the schema migrations table.
-* `config.active_record.pluralize_table_names` specifies whether Rails will look for singular or plural table names in the database. If set to true (the default), then the Customer class will use the `customers` table. If set to false, then the Customer class will use the `customer` table.
+* `config.active_record.internal_metadata_table_name` lets you set a string to be used as the name of the internal metadata table.
+
+* `config.active_record.protected_environments` lets you set an array of names of environments where destructive actions should be prohibited.
+
+* `config.active_record.pluralize_table_names` specifies whether Rails will look for singular or plural table names in the database. If set to `true` (the default), then the Customer class will use the `customers` table. If set to false, then the Customer class will use the `customer` table.
* `config.active_record.default_timezone` determines whether to use `Time.local` (if set to `:local`) or `Time.utc` (if set to `:utc`) when pulling dates and times from the database. The default is `:utc`.
* `config.active_record.schema_format` controls the format for dumping the database schema to a file. The options are `:ruby` (the default) for a database-independent version that depends on migrations, or `:sql` for a set of (potentially database-dependent) SQL statements.
-* `config.active_record.timestamped_migrations` controls whether migrations are numbered with serial integers or with timestamps. The default is true, to use timestamps, which are preferred if there are multiple developers working on the same application.
+* `config.active_record.error_on_ignored_order` specifies if an error should be raised if the order of a query is ignored during a batch query. The options are `true` (raise error) or `false` (warn). Default is `false`.
+
+* `config.active_record.timestamped_migrations` controls whether migrations are numbered with serial integers or with timestamps. The default is `true`, to use timestamps, which are preferred if there are multiple developers working on the same application.
-* `config.active_record.lock_optimistically` controls whether Active Record will use optimistic locking and is true by default.
+* `config.active_record.lock_optimistically` controls whether Active Record will use optimistic locking and is `true` by default.
* `config.active_record.cache_timestamp_format` controls the format of the timestamp value in the cache key. Default is `:nsec`.
@@ -292,33 +344,80 @@ All these configuration options are delegated to the `I18n` library.
* `config.active_record.partial_writes` is a boolean value and controls whether or not partial writes are used (i.e. whether updates only set attributes that are dirty). Note that when using partial writes, you should also use optimistic locking `config.active_record.lock_optimistically` since concurrent updates may write attributes based on a possibly stale read state. The default value is `true`.
-* `config.active_record.maintain_test_schema` is a boolean value which controls whether Active Record should try to keep your test database schema up-to-date with `db/schema.rb` (or `db/structure.sql`) when you run your tests. The default is true.
+* `config.active_record.maintain_test_schema` is a boolean value which controls whether Active Record should try to keep your test database schema up-to-date with `db/schema.rb` (or `db/structure.sql`) when you run your tests. The default is `true`.
* `config.active_record.dump_schema_after_migration` is a flag which
controls whether or not schema dump should happen (`db/schema.rb` or
- `db/structure.sql`) when you run migrations. This is set to false in
+ `db/structure.sql`) when you run migrations. This is set to `false` in
`config/environments/production.rb` which is generated by Rails. The
- default value is true if this configuration is not set.
+ 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 controls whether `belongs_to` association is required by default.
+* `config.active_record.belongs_to_required_by_default` is a boolean value and
+ controls whether a record fails validation if `belongs_to` association is not
+ present.
* `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.
+ Defaults to `false`.
+
+* `config.active_record.use_schema_cache_dump` enables users to get schema cache information
+ from `db/schema_cache.yml` (generated by `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::MysqlAdapter.emulate_booleans` controls whether Active Record will consider all `tinyint(1)` columns in a MySQL database to be booleans and is true by default.
+* `ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans` controls whether Active Record will consider all `tinyint(1)` columns as booleans. Defaults to `true`.
-The schema dumper adds one additional configuration option:
+The PostgreSQL adapter adds one additional configuration option:
-* `ActiveRecord::SchemaDumper.ignore_tables` accepts an array of tables that should _not_ be included in any generated schema file. This setting is ignored unless `config.active_record.schema_format == :ruby`.
+* `ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables`
+ controls whether database tables created should be "unlogged," which can speed
+ up performance but adds a risk of data loss if the database crashes. It is
+ highly recommended that you do not enable this in a production environment.
+ Defaults to `false` in all environments.
+
+The SQLite3Adapter adapter adds one additional configuration option:
+
+* `ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer`
+indicates whether boolean values are stored in sqlite3 databases as 1 and 0 or
+'t' and 'f'. Leaving `ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer`
+set to false is deprecated. SQLite databases have used 't' and 'f' to serialize
+boolean values and must have old data converted to 1 and 0 (its native boolean
+serialization) before setting this flag to true. Conversion can be accomplished
+by setting up a Rake task which runs
+
+ ```ruby
+ ExampleModel.where("boolean_column = 't'").update_all(boolean_column: 1)
+ ExampleModel.where("boolean_column = 'f'").update_all(boolean_column: 0)
+ ```
+
+ for all models and all boolean columns, after which the flag must be set to true
+by adding the following to your `application.rb` file:
+
+ ```ruby
+ Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true
+ ```
+
+The schema dumper adds two additional configuration options:
+
+* `ActiveRecord::SchemaDumper.ignore_tables` accepts an array of tables that should _not_ be included in any generated schema file.
+
+* `ActiveRecord::SchemaDumper.fk_ignore_pattern` allows setting a different regular
+ expression that will be used to decide whether a foreign key's name should be
+ dumped to db/schema.rb or not. By default, foreign key names starting with
+ `fk_rails_` are not exported to the database schema dump.
+ Defaults to `/^fk_rails_[0-9a-f]{10}$/`.
### Configuring Action Controller
@@ -326,13 +425,11 @@ The schema dumper adds one additional configuration option:
* `config.action_controller.asset_host` sets the host for the assets. Useful when CDNs are used for hosting assets rather than the application server itself.
-* `config.action_controller.perform_caching` configures whether the application should perform caching or not. Set to false in development mode, true in production.
+* `config.action_controller.perform_caching` configures whether the application should perform the caching features provided by the Action Controller component or not. Set to `false` in development mode, `true` in production.
* `config.action_controller.default_static_extension` configures the extension used for cached pages. Defaults to `.html`.
-* `config.action_controller.default_charset` specifies the default character set for all renders. The default is "utf-8".
-
-* `config.action_controller.include_all_helpers` configures whether all view helpers are available everywhere or are scoped to the corresponding controller. If set to `false`, `UsersHelper` methods are only available for views rendered as part of `UsersController`. If `true`, `UsersHelper` methods are available everywhere. The default is `true`.
+* `config.action_controller.include_all_helpers` configures whether all view helpers are available everywhere or are scoped to the corresponding controller. If set to `false`, `UsersHelper` methods are only available for views rendered as part of `UsersController`. If `true`, `UsersHelper` methods are available everywhere. The default configuration behavior (when this option is not explicitly set to `true` or `false`) is that all view helpers are available to each controller.
* `config.action_controller.logger` accepts a logger conforming to the interface of Log4r or the default Ruby Logger class, which is then used to log information from Action Controller. Set to `nil` to disable logging.
@@ -340,13 +437,35 @@ The schema dumper adds one additional configuration option:
* `config.action_controller.allow_forgery_protection` enables or disables CSRF protection. By default this is `false` in test mode and `true` in all other modes.
+* `config.action_controller.forgery_protection_origin_check` configures whether the HTTP `Origin` header should be checked against the site's origin as an additional CSRF defense.
+
+* `config.action_controller.per_form_csrf_tokens` configures whether CSRF tokens are only valid for the method/action they were generated for.
+
+* `config.action_controller.default_protect_from_forgery` determines whether forgery protection is added on `ActionController:Base`. This is false by default, but enabled when loading defaults for Rails 5.2.
+
* `config.action_controller.relative_url_root` can be used to tell Rails that you are [deploying to a subdirectory](configuring.html#deploy-to-a-subdirectory-relative-url-root). The default is `ENV['RAILS_RELATIVE_URL_ROOT']`.
* `config.action_controller.permit_all_parameters` sets all the parameters for mass assignment to be permitted by default. The default value is `false`.
* `config.action_controller.action_on_unpermitted_parameters` enables logging or raising an exception if parameters that are not explicitly permitted are found. Set to `:log` or `:raise` to enable. The default value is `:log` in development and test environments, and `false` in all other environments.
-* `config.action_controller.always_permitted_parameters` sets a list of whitelisted parameters that are permitted by default. The default values are `['controller', 'action']`.
+* `config.action_controller.always_permitted_parameters` sets a list of permitted parameters that are permitted by default. The default values are `['controller', 'action']`.
+
+* `config.action_controller.enable_fragment_cache_logging` determines whether to log fragment cache reads and writes in verbose format as follows:
+
+ ```
+ 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]
+ ```
+
+ By default it is set to `false` which results in following output:
+
+ ```
+ Rendered messages/_message.html.erb in 1.2 ms [cache hit]
+ Rendered recordings/threads/_thread.html.erb in 1.5 ms [cache miss]
+ ```
### Configuring Action Dispatch
@@ -358,12 +477,21 @@ The schema dumper adds one additional configuration option:
config.action_dispatch.default_headers = {
'X-Frame-Options' => 'SAMEORIGIN',
'X-XSS-Protection' => '1; mode=block',
- 'X-Content-Type-Options' => 'nosniff'
+ 'X-Content-Type-Options' => 'nosniff',
+ 'X-Download-Options' => 'noopen',
+ 'X-Permitted-Cross-Domain-Policies' => 'none',
+ 'Referrer-Policy' => 'strict-origin-when-cross-origin'
}
```
+* `config.action_dispatch.default_charset` specifies the default character set for all renders. Defaults to `nil`.
+
* `config.action_dispatch.tld_length` sets the TLD (top-level domain) length for the application. Defaults to `1`.
+* `config.action_dispatch.ignore_accept_header` is used to determine whether to ignore accept headers from a request. Defaults to `false`.
+
+* `config.action_dispatch.x_sendfile_header` specifies server specific X-Sendfile header. This is useful for accelerated file sending from server. For example it can be set to 'X-Sendfile' for Apache.
+
* `config.action_dispatch.http_auth_salt` sets the HTTP Auth salt value. Defaults
to `'http authentication'`.
@@ -371,34 +499,56 @@ to `'http authentication'`.
Defaults to `'signed cookie'`.
* `config.action_dispatch.encrypted_cookie_salt` sets the encrypted cookies salt
-value. Defaults to `'encrypted cookie'`.
+ value. Defaults to `'encrypted cookie'`.
* `config.action_dispatch.encrypted_signed_cookie_salt` sets the signed
-encrypted cookies salt value. Defaults to `'signed encrypted cookie'`.
+ encrypted cookies salt value. Defaults to `'signed encrypted cookie'`.
+
+* `config.action_dispatch.authenticated_encrypted_cookie_salt` sets the
+ authenticated encrypted cookie salt. Defaults to `'authenticated encrypted
+ cookie'`.
+
+* `config.action_dispatch.encrypted_cookie_cipher` sets the cipher to be
+ used for encrypted cookies. This defaults to `"aes-256-gcm"`.
+
+* `config.action_dispatch.signed_cookie_digest` sets the digest to be
+ used for signed cookies. This defaults to `"SHA1"`.
+
+* `config.action_dispatch.cookies_rotations` allows rotating
+ secrets, ciphers, and digests for encrypted and signed cookies.
+
+* `config.action_dispatch.use_authenticated_cookie_encryption` controls whether
+ signed and encrypted cookies use the AES-256-GCM cipher or
+ the older AES-256-CBC cipher. It defaults to `true`.
+
+* `config.action_dispatch.use_cookies_with_metadata` enables writing
+ cookies with the purpose and expiry metadata embedded. It defaults to `true`.
* `config.action_dispatch.perform_deep_munge` configures whether `deep_munge`
method should be performed on the parameters. See [Security Guide](security.html#unsafe-query-generation)
- for more information. It defaults to true.
+ for more information. It defaults to `true`.
* `config.action_dispatch.rescue_responses` configures what exceptions are assigned to an HTTP status. It accepts a hash and you can specify pairs of exception/status. By default, this is defined as:
```ruby
config.action_dispatch.rescue_responses = {
- 'ActionController::RoutingError' => :not_found,
- 'AbstractController::ActionNotFound' => :not_found,
- 'ActionController::MethodNotAllowed' => :method_not_allowed,
- 'ActionController::UnknownHttpMethod' => :method_not_allowed,
- 'ActionController::NotImplemented' => :not_implemented,
- 'ActionController::UnknownFormat' => :not_acceptable,
- 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity,
- 'ActionController::InvalidCrossOriginRequest' => :unprocessable_entity,
- 'ActionDispatch::ParamsParser::ParseError' => :bad_request,
- 'ActionController::BadRequest' => :bad_request,
- 'ActionController::ParameterMissing' => :bad_request,
- 'ActiveRecord::RecordNotFound' => :not_found,
- 'ActiveRecord::StaleObjectError' => :conflict,
- 'ActiveRecord::RecordInvalid' => :unprocessable_entity,
- 'ActiveRecord::RecordNotSaved' => :unprocessable_entity
+ 'ActionController::RoutingError' => :not_found,
+ 'AbstractController::ActionNotFound' => :not_found,
+ 'ActionController::MethodNotAllowed' => :method_not_allowed,
+ 'ActionController::UnknownHttpMethod' => :method_not_allowed,
+ 'ActionController::NotImplemented' => :not_implemented,
+ 'ActionController::UnknownFormat' => :not_acceptable,
+ 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity,
+ 'ActionController::InvalidCrossOriginRequest' => :unprocessable_entity,
+ 'ActionDispatch::Http::Parameters::ParseError' => :bad_request,
+ 'ActionController::BadRequest' => :bad_request,
+ 'ActionController::ParameterMissing' => :bad_request,
+ 'Rack::QueryParser::ParameterTypeError' => :bad_request,
+ 'Rack::QueryParser::InvalidParameterError' => :bad_request,
+ 'ActiveRecord::RecordNotFound' => :not_found,
+ 'ActiveRecord::StaleObjectError' => :conflict,
+ 'ActiveRecord::RecordInvalid' => :unprocessable_entity,
+ 'ActiveRecord::RecordNotSaved' => :unprocessable_entity
}
```
@@ -406,15 +556,15 @@ encrypted cookies salt value. Defaults to `'signed encrypted cookie'`.
* `ActionDispatch::Callbacks.before` takes a block of code to run before the request.
-* `ActionDispatch::Callbacks.to_prepare` takes a block to run after `ActionDispatch::Callbacks.before`, but before the request. Runs for every request in `development` mode, but only once for `production` or environments with `cache_classes` set to `true`.
-
* `ActionDispatch::Callbacks.after` takes a block of code to run after the request.
### Configuring Action View
`config.action_view` includes a small number of configuration settings:
-* `config.action_view.field_error_proc` provides an HTML generator for displaying errors that come from Active Record. The default is
+* `config.action_view.cache_template_loading` controls whether or not templates should be reloaded on each request. Defaults to whatever is set for `config.cache_classes`.
+
+* `config.action_view.field_error_proc` provides an HTML generator for displaying errors that come from Active Model. The default is
```ruby
Proc.new do |html_tag, instance|
@@ -433,7 +583,7 @@ encrypted cookies salt value. Defaults to `'signed encrypted cookie'`.
* `config.action_view.embed_authenticity_token_in_remote_forms` allows you to
set the default behavior for `authenticity_token` in forms with `remote:
- true`. By default it's set to false, which means that remote forms will not
+ true`. By default it's set to `false`, which means that remote forms will not
include `authenticity_token`, which is helpful when you're fragment-caching
the form. Remote forms get the authenticity from the `meta` tag, so embedding
is unnecessary unless you support browsers without JavaScript. In such case
@@ -451,6 +601,24 @@ encrypted cookies salt value. Defaults to `'signed encrypted cookie'`.
* `config.action_view.raise_on_missing_translations` determines whether an
error should be raised for missing translations.
+* `config.action_view.automatically_disable_submit_tag` determines whether
+ `submit_tag` should automatically disable on click, this defaults to `true`.
+
+* `config.action_view.debug_missing_translation` determines whether to wrap the missing translations key in a `<span>` tag or not. This defaults to `true`.
+
+* `config.action_view.form_with_generates_remote_forms` determines whether `form_with` generates remote forms or not. This defaults to `true`.
+
+* `config.action_view.form_with_generates_ids` determines whether `form_with` generates ids on inputs. This defaults to `true`.
+
+* `config.action_view.default_enforce_utf8` determines whether forms are generated with a hidden tag that forces older versions of Internet Explorer to submit forms encoded in UTF-8. This defaults to `false`.
+
+* `config.action_view.finalize_compiled_template_methods` determines
+ whether the methods on `ActionView::CompiledTemplates` that templates
+ compile themselves to are removed when template instances are
+ destroyed by the garbage collector. This helps prevent memory leaks in
+ development mode, but for large test suites, disabling this option in
+ the test environment can improve performance. This defaults to `true`.
+
### Configuring Action Mailer
There are a number of settings available on `config.action_mailer`:
@@ -464,16 +632,19 @@ There are a number of settings available on `config.action_mailer`:
* `:user_name` - If your mail server requires authentication, set the username in this setting.
* `:password` - If your mail server requires authentication, set the password in this setting.
* `:authentication` - If your mail server requires authentication, you need to specify the authentication type here. This is a symbol and one of `:plain`, `:login`, `:cram_md5`.
+ * `:enable_starttls_auto` - Detects if STARTTLS is enabled in your SMTP server and starts to use it. It defaults to `true`.
+ * `:openssl_verify_mode` - When using TLS, you can set how OpenSSL checks the certificate. This is useful if you need to validate a self-signed and/or a wildcard certificate. This can be one of the OpenSSL verify constants, `:none` or `:peer` -- or the constant directly `OpenSSL::SSL::VERIFY_NONE` or `OpenSSL::SSL::VERIFY_PEER`, respectively.
+ * `:ssl/:tls` - Enables the SMTP connection to use SMTP/TLS (SMTPS: SMTP over direct TLS connection).
* `config.action_mailer.sendmail_settings` allows detailed configuration for the `sendmail` delivery method. It accepts a hash of options, which can include any of these options:
* `:location` - The location of the sendmail executable. Defaults to `/usr/sbin/sendmail`.
- * `:arguments` - The command line arguments. Defaults to `-i -t`.
+ * `:arguments` - The command line arguments. Defaults to `-i`.
-* `config.action_mailer.raise_delivery_errors` specifies whether to raise an error if email delivery cannot be completed. It defaults to true.
+* `config.action_mailer.raise_delivery_errors` specifies whether to raise an error if email delivery cannot be completed. It defaults to `true`.
-* `config.action_mailer.delivery_method` defines the delivery method and defaults to `:smtp`. See the [configuration section in the Action Mailer guide](http://guides.rubyonrails.org/action_mailer_basics.html#action-mailer-configuration) for more info.
+* `config.action_mailer.delivery_method` defines the delivery method and defaults to `:smtp`. See the [configuration section in the Action Mailer guide](action_mailer_basics.html#action-mailer-configuration) for more info.
-* `config.action_mailer.perform_deliveries` specifies whether mail will actually be delivered and is true by default. It can be convenient to set it to false for testing.
+* `config.action_mailer.perform_deliveries` specifies whether mail will actually be delivered and is true by default. It can be convenient to set it to `false` for testing.
* `config.action_mailer.default_options` configures Action Mailer defaults. Use to set options like `from` or `reply_to` for every mailer. These default to:
@@ -504,6 +675,12 @@ There are a number of settings available on `config.action_mailer`:
config.action_mailer.interceptors = ["MailInterceptor"]
```
+* `config.action_mailer.preview_interceptors` registers interceptors which will be called before mail is previewed.
+
+ ```ruby
+ config.action_mailer.preview_interceptors = ["MyPreviewMailInterceptor"]
+ ```
+
* `config.action_mailer.preview_path` specifies the location of mailer previews.
```ruby
@@ -519,13 +696,16 @@ There are a number of settings available on `config.action_mailer`:
* `config.action_mailer.deliver_later_queue_name` specifies the queue name for
mailers. By default this is `mailers`.
+* `config.action_mailer.perform_caching` specifies whether the mailer templates should perform fragment caching or not. By default this is `false` in all environments.
+
+
### Configuring Active Support
There are a few configuration options available in Active Support:
* `config.active_support.bare` enables or disables the loading of `active_support/all` when booting Rails. Defaults to `nil`, which means `active_support/all` is loaded.
-* `config.active_support.test_order` sets the order that test cases are executed. Possible values are `:random` and `:sorted`. This option is set to `:random` in `config/environments/test.rb` in newly-generated applications. If you have an application that does not specify a `test_order`, it will default to `:sorted`, *until* Rails 5.0, when the default will become `:random`.
+* `config.active_support.test_order` sets the order in which the test cases are executed. Possible values are `:random` and `:sorted`. Defaults to `:random`.
* `config.active_support.escape_html_entities_in_json` enables or disables the escaping of HTML entities in JSON serialization. Defaults to `true`.
@@ -533,7 +713,9 @@ There are a few configuration options available in Active Support:
* `config.active_support.time_precision` sets the precision of JSON encoded time values. Defaults to `3`.
-* `config.active_support.halt_callback_chains_on_return_false` specifies whether ActiveRecord, ActiveModel and ActiveModel::Validations callback chains can be halted by returning `false` in a 'before' callback. Defaults to `true`.
+* `config.active_support.use_sha1_digests` specifies whether to use SHA-1 instead of MD5 to generate non-sensitive digests, such as the ETag header. Defaults to false.
+
+* `config.active_support.use_authenticated_message_encryption` specifies whether to use AES-256-GCM authenticated encryption as the default cipher for encrypting messages instead of AES-256-CBC. This is false by default, but enabled when loading defaults for Rails 5.2.
* `ActiveSupport::Logger.silencer` is set to `false` to disable the ability to silence logging in a block. The default is `true`.
@@ -549,7 +731,7 @@ There are a few configuration options available in Active Support:
`config.active_job` provides the following configuration options:
-* `config.active_job.queue_adapter` sets the adapter for the queueing backend. The default adapter is `:inline` which will perform jobs immediately. For an up-to-date list of built-in adapters see the [ActiveJob::QueueAdapters API documentation](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html).
+* `config.active_job.queue_adapter` sets the adapter for the queuing backend. The default adapter is `:async`. For an up-to-date list of built-in adapters see the [ActiveJob::QueueAdapters API documentation](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html).
```ruby
# Be sure to have the adapter's gem in your Gemfile
@@ -598,6 +780,71 @@ There are a few configuration options available in Active Support:
* `config.active_job.logger` accepts a logger conforming to the interface of Log4r or the default Ruby Logger class, which is then used to log information from Active Job. You can retrieve this logger by calling `logger` on either an Active Job class or an Active Job instance. Set to `nil` to disable logging.
+* `config.active_job.custom_serializers` allows to set custom argument serializers. Defaults to `[]`.
+
+### Configuring Action Cable
+
+* `config.action_cable.url` accepts a string for the URL for where
+ you are hosting your Action Cable server. You would use this option
+if you are running Action Cable servers that are separated from your
+main application.
+* `config.action_cable.mount_path` accepts a string for where to mount Action
+ Cable, as part of the main server process. Defaults to `/cable`.
+You can set this as nil to not mount Action Cable as part of your
+normal Rails server.
+
+
+### Configuring Active Storage
+
+`config.active_storage` provides the following configuration options:
+
+* `config.active_storage.variant_processor` accepts a symbol `:mini_magick` or `:vips`, specifying whether variant transformations will be performed with MiniMagick or ruby-vips. The default is `:mini_magick`.
+
+* `config.active_storage.analyzers` accepts an array of classes indicating the analyzers available for Active Storage blobs. The default is `[ActiveStorage::Analyzer::ImageAnalyzer, ActiveStorage::Analyzer::VideoAnalyzer]`. The former can extract width and height of an image blob; the latter can extract width, height, duration, angle, and aspect ratio of a video blob.
+
+* `config.active_storage.previewers` accepts an array of classes indicating the image previewers available in Active Storage blobs. The default is `[ActiveStorage::Previewer::PDFPreviewer, ActiveStorage::Previewer::VideoPreviewer]`. The former can generate a thumbnail from the first page of a PDF blob; the latter from the relevant frame of a video blob.
+
+* `config.active_storage.paths` accepts a hash of options indicating the locations of previewer/analyzer commands. The default is `{}`, meaning the commands will be looked for in the default path. Can include any of these options:
+ * `:ffprobe` - The location of the ffprobe executable.
+ * `:mutool` - The location of the mutool executable.
+ * `:ffmpeg` - The location of the ffmpeg executable.
+
+ ```ruby
+ config.active_storage.paths[:ffprobe] = '/usr/local/bin/ffprobe'
+ ```
+
+* `config.active_storage.variable_content_types` accepts an array of strings indicating the content types that Active Storage can transform through ImageMagick. The default is `%w(image/png image/gif image/jpg image/jpeg image/vnd.adobe.photoshop image/vnd.microsoft.icon)`.
+
+* `config.active_storage.content_types_to_serve_as_binary` accepts an array of strings indicating the content types that Active Storage will always serve as an attachment, rather than inline. The default is `%w(text/html
+text/javascript image/svg+xml application/postscript application/x-shockwave-flash text/xml application/xml application/xhtml+xml)`.
+
+* `config.active_storage.queue` can be used to set the name of the Active Job queue used to perform jobs like analyzing the content of a blob or purging a blog.
+
+ ```ruby
+ config.active_storage.queue = :low_priority
+ ```
+
+* `config.active_storage.logger` can be used to set the logger used by Active Storage. Accepts a logger conforming to the interface of Log4r or the default Ruby Logger class.
+
+ ```ruby
+ config.active_storage.logger = ActiveSupport::Logger.new(STDOUT)
+ ```
+
+* `config.active_storage.service_urls_expire_in` determines the default expiry of URLs generated by:
+ * `ActiveStorage::Blob#service_url`
+ * `ActiveStorage::Blob#service_url_for_direct_upload`
+ * `ActiveStorage::Variant#service_url`
+
+ The default is 5 minutes.
+
+* `config.active_storage.routes_prefix` can be used to set the route prefix for the routes served by Active Storage. Accepts a string that will be prepended to the generated routes.
+
+ ```ruby
+ config.active_storage.routes_prefix = '/files'
+ ```
+
+ The default is `/rails/active_storage`
+
### Configuring a Database
Just about every Rails application will interact with a database. You can connect to the database by setting an environment variable `ENV['DATABASE_URL']` or by using a configuration file called `config/database.yml`.
@@ -639,7 +886,7 @@ TIP: You don't have to update the database configurations manually. If you look
### Connection Preference
-Since there are two ways to set your connection, via environment variable it is important to understand how the two can interact.
+Since there are two ways to configure your connection (using `config/database.yml` or using an environment variable) it is important to understand how they can interact.
If you have an empty `config/database.yml` file but your `ENV['DATABASE_URL']` is present, then Rails will connect to the database via your environment variable:
@@ -676,8 +923,16 @@ development:
$ echo $DATABASE_URL
postgresql://localhost/my_database
-$ bin/rails runner 'puts ActiveRecord::Base.configurations'
-{"development"=>{"adapter"=>"postgresql", "host"=>"localhost", "database"=>"my_database"}}
+$ rails runner 'puts ActiveRecord::Base.configurations'
+#<ActiveRecord::DatabaseConfigurations:0x00007fd50e209a28>
+
+$ rails runner 'puts ActiveRecord::Base.configurations.inspect'
+#<ActiveRecord::DatabaseConfigurations:0x00007fc8eab02880 @configurations=[
+ #<ActiveRecord::DatabaseConfigurations::UrlConfig:0x00007fc8eab020b0
+ @env_name="development", @spec_name="primary",
+ @config={"adapter"=>"postgresql", "database"=>"my_database", "host"=>"localhost"}
+ @url="postgresql://localhost/my_database">
+ ]
```
Here the adapter, host, and database match the information in `ENV['DATABASE_URL']`.
@@ -693,8 +948,16 @@ development:
$ echo $DATABASE_URL
postgresql://localhost/my_database
-$ bin/rails runner 'puts ActiveRecord::Base.configurations'
-{"development"=>{"adapter"=>"postgresql", "host"=>"localhost", "database"=>"my_database", "pool"=>5}}
+$ rails runner 'puts ActiveRecord::Base.configurations'
+#<ActiveRecord::DatabaseConfigurations:0x00007fd50e209a28>
+
+$ rails runner 'puts ActiveRecord::Base.configurations.inspect'
+#<ActiveRecord::DatabaseConfigurations:0x00007fc8eab02880 @configurations=[
+ #<ActiveRecord::DatabaseConfigurations::UrlConfig:0x00007fc8eab020b0
+ @env_name="development", @spec_name="primary",
+ @config={"adapter"=>"postgresql", "database"=>"my_database", "host"=>"localhost", "pool"=>5}
+ @url="postgresql://localhost/my_database">
+ ]
```
Since pool is not in the `ENV['DATABASE_URL']` provided connection information its information is merged in. Since `adapter` is duplicate, the `ENV['DATABASE_URL']` connection information wins.
@@ -709,8 +972,16 @@ development:
$ echo $DATABASE_URL
postgresql://localhost/my_database
-$ bin/rails runner 'puts ActiveRecord::Base.configurations'
-{"development"=>{"adapter"=>"sqlite3", "database"=>"NOT_my_database"}}
+$ rails runner 'puts ActiveRecord::Base.configurations'
+#<ActiveRecord::DatabaseConfigurations:0x00007fd50e209a28>
+
+$ rails runner 'puts ActiveRecord::Base.configurations.inspect'
+#<ActiveRecord::DatabaseConfigurations:0x00007fc8eab02880 @configurations=[
+ #<ActiveRecord::DatabaseConfigurations::UrlConfig:0x00007fc8eab020b0
+ @env_name="development", @spec_name="primary",
+ @config={"adapter"=>"sqlite3", "database"=>"NOT_my_database"}
+ @url="sqlite3:NOT_my_database">
+ ]
```
Here the connection information in `ENV['DATABASE_URL']` is ignored, note the different adapter and database name.
@@ -739,16 +1010,16 @@ development:
timeout: 5000
```
-NOTE: Rails uses an SQLite3 database for data storage by default because it is a zero configuration database that just works. Rails also supports MySQL and PostgreSQL "out of the box", and has plugins for many database systems. If you are using a database in a production environment Rails most likely has an adapter for it.
+NOTE: Rails uses an SQLite3 database for data storage by default because it is a zero configuration database that just works. Rails also supports MySQL (including MariaDB) and PostgreSQL "out of the box", and has plugins for many database systems. If you are using a database in a production environment Rails most likely has an adapter for it.
-#### Configuring a MySQL Database
+#### Configuring a MySQL or MariaDB Database
-If you choose to use MySQL instead of the shipped SQLite3 database, your `config/database.yml` will look a little different. Here's the development section:
+If you choose to use MySQL or MariaDB instead of the shipped SQLite3 database, your `config/database.yml` will look a little different. Here's the development section:
```yaml
development:
adapter: mysql2
- encoding: utf8
+ encoding: utf8mb4
database: blog_development
pool: 5
username: root
@@ -756,7 +1027,17 @@ development:
socket: /tmp/mysql.sock
```
-If your development computer's MySQL installation includes a root user with an empty password, this configuration should work for you. Otherwise, change the username and password in the `development` section as appropriate.
+If your development database has a root user with an empty password, this configuration should work for you. Otherwise, change the username and password in the `development` section as appropriate.
+
+NOTE: If your MySQL version is 5.5 or 5.6 and want to use the `utf8mb4` character set by default, please configure your MySQL server to support the longer key prefix by enabling `innodb_large_prefix` system variable.
+
+Advisory Locks are enabled by default on MySQL and are used to make database migrations concurrent safe. You can disable advisory locks by setting `advisory_locks` to `false`:
+
+```yaml
+production:
+ adapter: mysql2
+ advisory_locks: false
+```
#### Configuring a PostgreSQL Database
@@ -770,12 +1051,13 @@ development:
pool: 5
```
-Prepared Statements are enabled by default on PostgreSQL. You can disable prepared statements by setting `prepared_statements` to `false`:
+By default Active Record uses database features like prepared statements and advisory locks. You might need to disable those features if you're using an external connection pooler like PgBouncer:
```yaml
production:
adapter: postgresql
prepared_statements: false
+ advisory_locks: false
```
If enabled, Active Record will create up to `1000` prepared statements per database connection by default. To modify this behavior you can set `statement_limit` to a different value:
@@ -798,9 +1080,9 @@ development:
database: db/development.sqlite3
```
-#### Configuring a MySQL Database for JRuby Platform
+#### Configuring a MySQL or MariaDB Database for JRuby Platform
-If you choose to use MySQL and are using JRuby, your `config/database.yml` will look a little different. Here's the development section:
+If you choose to use MySQL or MariaDB and are using JRuby, your `config/database.yml` will look a little different. Here's the development section:
```yaml
development:
@@ -831,7 +1113,7 @@ By default Rails ships with three environments: "development", "test", and "prod
Imagine you have a server which mirrors the production environment but is only used for testing. Such a server is commonly called a "staging server". To define an environment called "staging" for this server, just create a file called `config/environments/staging.rb`. Please use the contents of any existing file in `config/environments` as a starting point and make the necessary changes from there.
-That environment is no different than the default ones, start a server with `rails server -e staging`, a console with `rails console staging`, `Rails.env.staging?` works, etc.
+That environment is no different than the default ones, start a server with `rails server -e staging`, a console with `rails console -e staging`, `Rails.env.staging?` works, etc.
### Deploy to a subdirectory (relative url root)
@@ -853,7 +1135,7 @@ Rails will now prepend "/app1" when generating links.
#### Using Passenger
-Passenger makes it easy to run your application in a subdirectory. You can find the relevant configuration in the [Passenger manual](http://www.modrails.com/documentation/Users%20guide%20Apache.html#deploying_rails_to_sub_uri).
+Passenger makes it easy to run your application in a subdirectory. You can find the relevant configuration in the [Passenger manual](https://www.phusionpassenger.com/library/deploy/apache/deploy/ruby/#deploying-an-app-to-a-sub-uri-or-subdirectory).
#### Using a Reverse Proxy
@@ -861,17 +1143,17 @@ Deploying your application using a reverse proxy has definite advantages over tr
Many modern web servers can be used as a proxy server to balance third-party elements such as caching servers or application servers.
-One such application server you can use is [Unicorn](http://unicorn.bogomips.org/) to run behind a reverse proxy.
+One such application server you can use is [Unicorn](https://bogomips.org/unicorn/) to run behind a reverse proxy.
In this case, you would need to configure the proxy server (NGINX, Apache, etc) to accept connections from your application server (Unicorn). By default Unicorn will listen for TCP connections on port 8080, but you can change the port or configure it to use sockets instead.
-You can find more information in the [Unicorn readme](http://unicorn.bogomips.org/README.html) and understand the [philosophy](http://unicorn.bogomips.org/PHILOSOPHY.html) behind it.
+You can find more information in the [Unicorn readme](https://bogomips.org/unicorn/README.html) and understand the [philosophy](https://bogomips.org/unicorn/PHILOSOPHY.html) behind it.
Once you've configured the application server, you must proxy requests to it by configuring your web server appropriately. For example your NGINX config may include:
```
upstream application_server {
- server 0.0.0.0:8080
+ server 0.0.0.0:8080;
}
server {
@@ -893,7 +1175,7 @@ server {
}
```
-Be sure to read the [NGINX documentation](http://nginx.org/en/docs/) for the most up-to-date information.
+Be sure to read the [NGINX documentation](https://nginx.org/en/docs/) for the most up-to-date information.
Rails Environment Settings
@@ -915,7 +1197,7 @@ After loading the framework and any gems in your application, Rails turns to loa
NOTE: You can use subfolders to organize your initializers if you like, because Rails will look into the whole file hierarchy from the initializers folder on down.
-TIP: If you have any ordering dependency in your initializers, you can control the load order through naming. Initializer files are loaded in alphabetical order by their path. For example, `01_critical.rb` will be loaded before `02_normal.rb`.
+TIP: While Rails supports numbering of initializer file names for load ordering purposes, a better technique is to place any code that need to load in a specific order within the same file. This reduces file name churn, makes dependencies more explicit, and can help surface new concepts within your application.
Initialization events
---------------------
@@ -978,98 +1260,110 @@ Because `Rails::Application` inherits from `Rails::Railtie` (indirectly), you ca
Below is a comprehensive list of all the initializers found in Rails in the order that they are defined (and therefore run in, unless otherwise stated).
-* `load_environment_hook` Serves as a placeholder so that `:load_environment_config` can be defined to run before it.
+* `load_environment_hook`: Serves as a placeholder so that `:load_environment_config` can be defined to run before it.
+
+* `load_active_support`: Requires `active_support/dependencies` which sets up the basis for Active Support. Optionally requires `active_support/all` if `config.active_support.bare` is un-truthful, which is the default.
+
+* `initialize_logger`: Initializes the logger (an `ActiveSupport::Logger` object) for the application and makes it accessible at `Rails.logger`, provided that no initializer inserted before this point has defined `Rails.logger`.
+
+* `initialize_cache`: If `Rails.cache` isn't set yet, initializes the cache by referencing the value in `config.cache_store` and stores the outcome as `Rails.cache`. If this object responds to the `middleware` method, its middleware is inserted before `Rack::Runtime` in the middleware stack.
+
+* `set_clear_dependencies_hook`: This initializer - which runs only if `cache_classes` is set to `false` - uses `ActionDispatch::Callbacks.after` to remove the constants which have been referenced during the request from the object space so that they will be reloaded during the following request.
+
+* `initialize_dependency_mechanism`: If `config.cache_classes` is true, configures `ActiveSupport::Dependencies.mechanism` to `require` dependencies rather than `load` them.
+
+* `bootstrap_hook`: Runs all configured `before_initialize` blocks.
-* `load_active_support` Requires `active_support/dependencies` which sets up the basis for Active Support. Optionally requires `active_support/all` if `config.active_support.bare` is un-truthful, which is the default.
+* `i18n.callbacks`: In the development environment, sets up a `to_prepare` callback which will call `I18n.reload!` if any of the locales have changed since the last request. In production mode this callback will only run on the first request.
-* `initialize_logger` Initializes the logger (an `ActiveSupport::Logger` object) for the application and makes it accessible at `Rails.logger`, provided that no initializer inserted before this point has defined `Rails.logger`.
+* `active_support.deprecation_behavior`: Sets up deprecation reporting for environments, defaulting to `:log` for development, `:notify` for production, and `:stderr` for test. If a value isn't set for `config.active_support.deprecation` then this initializer will prompt the user to configure this line in the current environment's `config/environments` file. Can be set to an array of values.
-* `initialize_cache` If `Rails.cache` isn't set yet, initializes the cache by referencing the value in `config.cache_store` and stores the outcome as `Rails.cache`. If this object responds to the `middleware` method, its middleware is inserted before `Rack::Runtime` in the middleware stack.
+* `active_support.initialize_time_zone`: Sets the default time zone for the application based on the `config.time_zone` setting, which defaults to "UTC".
-* `set_clear_dependencies_hook` Provides a hook for `active_record.set_dispatch_hooks` to use, which will run before this initializer. This initializer - which runs only if `cache_classes` is set to `false` - uses `ActionDispatch::Callbacks.after` to remove the constants which have been referenced during the request from the object space so that they will be reloaded during the following request.
+* `active_support.initialize_beginning_of_week`: Sets the default beginning of week for the application based on `config.beginning_of_week` setting, which defaults to `:monday`.
-* `initialize_dependency_mechanism` If `config.cache_classes` is true, configures `ActiveSupport::Dependencies.mechanism` to `require` dependencies rather than `load` them.
+* `active_support.set_configs`: Sets up Active Support by using the settings in `config.active_support` by `send`'ing the method names as setters to `ActiveSupport` and passing the values through.
-* `bootstrap_hook` Runs all configured `before_initialize` blocks.
+* `action_dispatch.configure`: Configures the `ActionDispatch::Http::URL.tld_length` to be set to the value of `config.action_dispatch.tld_length`.
-* `i18n.callbacks` In the development environment, sets up a `to_prepare` callback which will call `I18n.reload!` if any of the locales have changed since the last request. In production mode this callback will only run on the first request.
+* `action_view.set_configs`: Sets up Action View by using the settings in `config.action_view` by `send`'ing the method names as setters to `ActionView::Base` and passing the values through.
-* `active_support.deprecation_behavior` Sets up deprecation reporting for environments, defaulting to `:log` for development, `:notify` for production and `:stderr` for test. If a value isn't set for `config.active_support.deprecation` then this initializer will prompt the user to configure this line in the current environment's `config/environments` file. Can be set to an array of values.
+* `action_controller.assets_config`: Initializes the `config.actions_controller.assets_dir` to the app's public directory if not explicitly configured.
-* `active_support.initialize_time_zone` Sets the default time zone for the application based on the `config.time_zone` setting, which defaults to "UTC".
+* `action_controller.set_helpers_path`: Sets Action Controller's `helpers_path` to the application's `helpers_path`.
-* `active_support.initialize_beginning_of_week` Sets the default beginning of week for the application based on `config.beginning_of_week` setting, which defaults to `:monday`.
+* `action_controller.parameters_config`: Configures strong parameters options for `ActionController::Parameters`.
-* `action_dispatch.configure` Configures the `ActionDispatch::Http::URL.tld_length` to be set to the value of `config.action_dispatch.tld_length`.
+* `action_controller.set_configs`: Sets up Action Controller by using the settings in `config.action_controller` by `send`'ing the method names as setters to `ActionController::Base` and passing the values through.
-* `action_view.set_configs` Sets up Action View by using the settings in `config.action_view` by `send`'ing the method names as setters to `ActionView::Base` and passing the values through.
+* `action_controller.compile_config_methods`: Initializes methods for the config settings specified so that they are quicker to access.
-* `action_controller.logger` Sets `ActionController::Base.logger` - if it's not already set - to `Rails.logger`.
+* `active_record.initialize_timezone`: Sets `ActiveRecord::Base.time_zone_aware_attributes` to `true`, as well as setting `ActiveRecord::Base.default_timezone` to UTC. When attributes are read from the database, they will be converted into the time zone specified by `Time.zone`.
-* `action_controller.initialize_framework_caches` Sets `ActionController::Base.cache_store` - if it's not already set - to `Rails.cache`.
+* `active_record.logger`: Sets `ActiveRecord::Base.logger` - if it's not already set - to `Rails.logger`.
-* `action_controller.set_configs` Sets up Action Controller by using the settings in `config.action_controller` by `send`'ing the method names as setters to `ActionController::Base` and passing the values through.
+* `active_record.migration_error`: Configures middleware to check for pending migrations.
-* `action_controller.compile_config_methods` Initializes methods for the config settings specified so that they are quicker to access.
+* `active_record.check_schema_cache_dump`: Loads the schema cache dump if configured and available.
-* `active_record.initialize_timezone` Sets `ActiveRecord::Base.time_zone_aware_attributes` to true, as well as setting `ActiveRecord::Base.default_timezone` to UTC. When attributes are read from the database, they will be converted into the time zone specified by `Time.zone`.
+* `active_record.warn_on_records_fetched_greater_than`: Enables warnings when queries return large numbers of records.
-* `active_record.logger` Sets `ActiveRecord::Base.logger` - if it's not already set - to `Rails.logger`.
+* `active_record.set_configs`: Sets up Active Record by using the settings in `config.active_record` by `send`'ing the method names as setters to `ActiveRecord::Base` and passing the values through.
-* `active_record.set_configs` Sets up Active Record by using the settings in `config.active_record` by `send`'ing the method names as setters to `ActiveRecord::Base` and passing the values through.
+* `active_record.initialize_database`: Loads the database configuration (by default) from `config/database.yml` and establishes a connection for the current environment.
-* `active_record.initialize_database` Loads the database configuration (by default) from `config/database.yml` and establishes a connection for the current environment.
+* `active_record.log_runtime`: Includes `ActiveRecord::Railties::ControllerRuntime` which is responsible for reporting the time taken by Active Record calls for the request back to the logger.
-* `active_record.log_runtime` Includes `ActiveRecord::Railties::ControllerRuntime` which is responsible for reporting the time taken by Active Record calls for the request back to the logger.
+* `active_record.set_reloader_hooks`: Resets all reloadable connections to the database if `config.cache_classes` is set to `false`.
-* `active_record.set_dispatch_hooks` Resets all reloadable connections to the database if `config.cache_classes` is set to `false`.
+* `active_record.add_watchable_files`: Adds `schema.rb` and `structure.sql` files to watchable files.
-* `active_job.logger` Sets `ActiveJob::Base.logger` - if it's not already set -
+* `active_job.logger`: Sets `ActiveJob::Base.logger` - if it's not already set -
to `Rails.logger`.
-* `active_job.set_configs` Sets up Active Job by using the settings in `config.active_job` by `send`'ing the method names as setters to `ActiveJob::Base` and passing the values through.
+* `active_job.set_configs`: Sets up Active Job by using the settings in `config.active_job` by `send`'ing the method names as setters to `ActiveJob::Base` and passing the values through.
-* `action_mailer.logger` Sets `ActionMailer::Base.logger` - if it's not already set - to `Rails.logger`.
+* `action_mailer.logger`: Sets `ActionMailer::Base.logger` - if it's not already set - to `Rails.logger`.
-* `action_mailer.set_configs` Sets up Action Mailer by using the settings in `config.action_mailer` by `send`'ing the method names as setters to `ActionMailer::Base` and passing the values through.
+* `action_mailer.set_configs`: Sets up Action Mailer by using the settings in `config.action_mailer` by `send`'ing the method names as setters to `ActionMailer::Base` and passing the values through.
-* `action_mailer.compile_config_methods` Initializes methods for the config settings specified so that they are quicker to access.
+* `action_mailer.compile_config_methods`: Initializes methods for the config settings specified so that they are quicker to access.
-* `set_load_path` This initializer runs before `bootstrap_hook`. Adds the `vendor`, `lib`, all directories of `app` and any paths specified by `config.load_paths` to `$LOAD_PATH`.
+* `set_load_path`: This initializer runs before `bootstrap_hook`. Adds paths specified by `config.load_paths` and all autoload paths to `$LOAD_PATH`.
-* `set_autoload_paths` This initializer runs before `bootstrap_hook`. Adds all sub-directories of `app` and paths specified by `config.autoload_paths` to `ActiveSupport::Dependencies.autoload_paths`.
+* `set_autoload_paths`: This initializer runs before `bootstrap_hook`. Adds all sub-directories of `app` and paths specified by `config.autoload_paths`, `config.eager_load_paths` and `config.autoload_once_paths` to `ActiveSupport::Dependencies.autoload_paths`.
-* `add_routing_paths` Loads (by default) all `config/routes.rb` files (in the application and railties, including engines) and sets up the routes for the application.
+* `add_routing_paths`: Loads (by default) all `config/routes.rb` files (in the application and railties, including engines) and sets up the routes for the application.
-* `add_locales` Adds the files in `config/locales` (from the application, railties and engines) to `I18n.load_path`, making available the translations in these files.
+* `add_locales`: Adds the files in `config/locales` (from the application, railties, and engines) to `I18n.load_path`, making available the translations in these files.
-* `add_view_paths` Adds the directory `app/views` from the application, railties and engines to the lookup path for view files for the application.
+* `add_view_paths`: Adds the directory `app/views` from the application, railties, and engines to the lookup path for view files for the application.
-* `load_environment_config` Loads the `config/environments` file for the current environment.
+* `load_environment_config`: Loads the `config/environments` file for the current environment.
-* `prepend_helpers_path` Adds the directory `app/helpers` from the application, railties and engines to the lookup path for helpers for the application.
+* `prepend_helpers_path`: Adds the directory `app/helpers` from the application, railties, and engines to the lookup path for helpers for the application.
-* `load_config_initializers` Loads all Ruby files from `config/initializers` in the application, railties and engines. The files in this directory can be used to hold configuration settings that should be made after all of the frameworks are loaded.
+* `load_config_initializers`: Loads all Ruby files from `config/initializers` in the application, railties, and engines. The files in this directory can be used to hold configuration settings that should be made after all of the frameworks are loaded.
-* `engines_blank_point` Provides a point-in-initialization to hook into if you wish to do anything before engines are loaded. After this point, all railtie and engine initializers are run.
+* `engines_blank_point`: Provides a point-in-initialization to hook into if you wish to do anything before engines are loaded. After this point, all railtie and engine initializers are run.
-* `add_generator_templates` Finds templates for generators at `lib/templates` for the application, railties and engines and adds these to the `config.generators.templates` setting, which will make the templates available for all generators to reference.
+* `add_generator_templates`: Finds templates for generators at `lib/templates` for the application, railties, and engines and adds these to the `config.generators.templates` setting, which will make the templates available for all generators to reference.
-* `ensure_autoload_once_paths_as_subset` Ensures that the `config.autoload_once_paths` only contains paths from `config.autoload_paths`. If it contains extra paths, then an exception will be raised.
+* `ensure_autoload_once_paths_as_subset`: Ensures that the `config.autoload_once_paths` only contains paths from `config.autoload_paths`. If it contains extra paths, then an exception will be raised.
-* `add_to_prepare_blocks` The block for every `config.to_prepare` call in the application, a railtie or engine is added to the `to_prepare` callbacks for Action Dispatch which will be run per request in development, or before the first request in production.
+* `add_to_prepare_blocks`: The block for every `config.to_prepare` call in the application, a railtie, or engine is added to the `to_prepare` callbacks for Action Dispatch which will be run per request in development, or before the first request in production.
-* `add_builtin_route` If the application is running under the development environment then this will append the route for `rails/info/properties` to the application routes. This route provides the detailed information such as Rails and Ruby version for `public/index.html` in a default Rails application.
+* `add_builtin_route`: If the application is running under the development environment then this will append the route for `rails/info/properties` to the application routes. This route provides the detailed information such as Rails and Ruby version for `public/index.html` in a default Rails application.
-* `build_middleware_stack` Builds the middleware stack for the application, returning an object which has a `call` method which takes a Rack environment object for the request.
+* `build_middleware_stack`: Builds the middleware stack for the application, returning an object which has a `call` method which takes a Rack environment object for the request.
-* `eager_load!` If `config.eager_load` is true, runs the `config.before_eager_load` hooks and then calls `eager_load!` which will load all `config.eager_load_namespaces`.
+* `eager_load!`: If `config.eager_load` is `true`, runs the `config.before_eager_load` hooks and then calls `eager_load!` which will load all `config.eager_load_namespaces`.
-* `finisher_hook` Provides a hook for after the initialization of process of the application is complete, as well as running all the `config.after_initialize` blocks for the application, railties and engines.
+* `finisher_hook`: Provides a hook for after the initialization of process of the application is complete, as well as running all the `config.after_initialize` blocks for the application, railties, and engines.
-* `set_routes_reloader` Configures Action Dispatch to reload the routes file using `ActionDispatch::Callbacks.to_prepare`.
+* `set_routes_reloader_hook`: Configures Action Dispatch to reload the routes file using `ActiveSupport::Callbacks.to_run`.
-* `disable_dependency_loading` Disables the automatic dependency loading if the `config.eager_load` is set to true.
+* `disable_dependency_loading`: Disables the automatic dependency loading if the `config.eager_load` is set to `true`.
Database pooling
----------------
@@ -1084,33 +1378,37 @@ development:
timeout: 5000
```
-Since the connection pooling is handled inside of Active Record by default, all application servers (Thin, mongrel, Unicorn etc.) should behave the same. Initially, the database connection pool is empty and it will create additional connections as the demand for them increases, until it reaches the connection pool limit.
+Since the connection pooling is handled inside of Active Record by default, all application servers (Thin, Puma, Unicorn etc.) should behave the same. The database connection pool is initially empty. As demand for connections increases it will create them until it reaches the connection pool limit.
-Any one request will check out a connection the first time it requires access to the database, after which it will check the connection back in, at the end of the request, meaning that the additional connection slot will be available again for the next request in the queue.
+Any one request will check out a connection the first time it requires access to the database. At the end of the request it will check the connection back in. This means that the additional connection slot will be available again for the next request in the queue.
If you try to use more connections than are available, Active Record will block
-and wait for a connection from the pool. When it cannot get connection, a timeout
-error similar to given below will be thrown.
+you and wait for a connection from the pool. If it cannot get a connection, a
+timeout error similar to that given below will be thrown.
```ruby
-ActiveRecord::ConnectionTimeoutError - could not obtain a database connection within 5 seconds. The max pool size is currently 5; consider increasing it:
+ActiveRecord::ConnectionTimeoutError - could not obtain a database connection within 5.000 seconds (waited 5.000 seconds)
```
-If you get the above error, you might want to increase the size of connection
-pool by incrementing the `pool` option in `database.yml`
+If you get the above error, you might want to increase the size of the
+connection pool by incrementing the `pool` option in `database.yml`
-NOTE. If you are running in a multi-threaded environment, there could be a chance that several threads may be accessing multiple connections simultaneously. So depending on your current request load, you could very well have multiple threads contending for a limited amount of connections.
+NOTE. If you are running in a multi-threaded environment, there could be a chance that several threads may be accessing multiple connections simultaneously. So depending on your current request load, you could very well have multiple threads contending for a limited number of connections.
Custom configuration
--------------------
-You can configure your own code through the Rails configuration object with custom configuration. It works like this:
+You can configure your own code through the Rails configuration object with
+custom configuration under either the `config.x` namespace, or `config` directly.
+The key difference between these two is that you should be using `config.x` if you
+are defining _nested_ configuration (ex: `config.x.nested.nested.hi`), and just
+`config` for _single level_ configuration (ex: `config.hello`).
```ruby
config.x.payment_processing.schedule = :daily
config.x.payment_processing.retries = 3
- config.x.super_debugger = true
+ config.super_debugger = true
```
These configuration points are then available through the configuration object:
@@ -1118,21 +1416,48 @@ These configuration points are then available through the configuration object:
```ruby
Rails.configuration.x.payment_processing.schedule # => :daily
Rails.configuration.x.payment_processing.retries # => 3
- Rails.configuration.x.super_debugger # => true
- Rails.configuration.x.super_debugger.not_set # => nil
+ Rails.configuration.x.payment_processing.not_set # => nil
+ Rails.configuration.super_debugger # => true
+ ```
+
+You can also use `Rails::Application.config_for` to load whole configuration files:
+
+ ```ruby
+ # config/payment.yml:
+ production:
+ environment: production
+ merchant_id: production_merchant_id
+ public_key: production_public_key
+ private_key: production_private_key
+ development:
+ environment: sandbox
+ merchant_id: development_merchant_id
+ public_key: development_public_key
+ private_key: development_private_key
+
+ # config/application.rb
+ module MyApp
+ class Application < Rails::Application
+ config.payment = config_for(:payment)
+ end
+ end
+ ```
+
+ ```ruby
+ Rails.configuration.payment['merchant_id'] # => production_merchant_id or development_merchant_id
```
Search Engines Indexing
-----------------------
Sometimes, you may want to prevent some pages of your application to be visible
-on search sites like Google, Bing, Yahoo or Duck Duck Go. The robots that index
+on search sites like Google, Bing, Yahoo, or Duck Duck Go. The robots that index
these sites will first analyze the `http://your-site.com/robots.txt` file to
know which pages it is allowed to index.
Rails creates this file for you inside the `/public` folder. By default, it allows
search engines to index all pages of your application. If you want to block
-indexing on all pages of you application, use this:
+indexing on all pages of your application, use this:
```
User-agent: *
@@ -1141,3 +1466,25 @@ Disallow: /
To block just specific pages, it's necessary to use a more complex syntax. Learn
it on the [official documentation](http://www.robotstxt.org/robotstxt.html).
+
+Evented File System Monitor
+---------------------------
+
+If the [listen gem](https://github.com/guard/listen) is loaded Rails uses an
+evented file system monitor to detect changes when `config.cache_classes` is
+`false`:
+
+```ruby
+group :development do
+ gem 'listen', '>= 3.0.5', '< 3.2'
+end
+```
+
+Otherwise, in every request Rails walks the application tree to check if
+anything has changed.
+
+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).
+
+Note that [some setups are unsupported](https://github.com/guard/listen#issues--limitations).
diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md
index 2e86eee587..709a5146e9 100644
--- a/guides/source/contributing_to_ruby_on_rails.md
+++ b/guides/source/contributing_to_ruby_on_rails.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Contributing to Ruby on Rails
=============================
@@ -13,16 +13,19 @@ After reading this guide, you will know:
* How to contribute to the Ruby on Rails documentation.
* How to contribute to the Ruby on Rails code.
-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.
+Ruby on Rails is not "someone else's framework." Over the years, thousands 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'
+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/).
--------------------------------------------------------------------------------
Reporting an Issue
------------------
-Ruby on Rails uses [GitHub Issue Tracking](https://github.com/rails/rails/issues) to track issues (primarily bugs and contributions of new code). If you've found a bug in Ruby on Rails, this is the place to start. You'll need to create a (free) GitHub account in order to submit an issue, to comment on them or to create pull requests.
+Ruby on Rails uses [GitHub Issue Tracking](https://github.com/rails/rails/issues) to track issues (primarily bugs and contributions of new code). If you've found a bug in Ruby on Rails, this is the place to start. You'll need to create a (free) GitHub account in order to submit an issue, to comment on them, or to create pull requests.
-NOTE: Bugs in the most recent released version of Ruby on Rails are likely to get the most attention. Also, the Rails core team is always interested in feedback from those who can take the time to test _edge Rails_ (the code for the version of Rails that is currently under development). Later in this guide you'll find out how to get edge Rails for testing.
+NOTE: Bugs in the most recent released version of Ruby on Rails are likely to get the most attention. Also, the Rails core team is always interested in feedback from those who can take the time to test _edge Rails_ (the code for the version of Rails that is currently under development). Later in this guide, you'll find out how to get edge Rails for testing.
### Creating a Bug Report
@@ -34,10 +37,12 @@ Then, don't get your hopes up! Unless you have a "Code Red, Mission Critical, th
### Create an Executable Test Case
-Having a way to reproduce your issue will be very helpful for others to help confirm, investigate and ultimately fix your issue. You can do this by providing an executable test case. To make this process easier, we have prepared several bug report templates for you to use as a starting point:
+Having a way to reproduce your issue will be very helpful for others to help confirm, investigate, and ultimately fix your issue. You can do this by providing an executable test case. To make this process easier, we have prepared several bug report templates for you to use as a starting point:
* Template for Active Record (models, database) issues: [gem](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_gem.rb) / [master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_master.rb)
+* Template for testing Active Record (migration) issues: [gem](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_migrations_gem.rb) / [master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_migrations_master.rb)
* Template for Action Pack (controllers, routing) issues: [gem](https://github.com/rails/rails/blob/master/guides/bug_report_templates/action_controller_gem.rb) / [master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/action_controller_master.rb)
+* Template for Active Job issues: [gem](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_job_gem.rb) / [master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_job_master.rb)
* Generic template for other issues: [gem](https://github.com/rails/rails/blob/master/guides/bug_report_templates/generic_gem.rb) / [master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/generic_master.rb)
These templates include the boilerplate code to set up a test case against either a released version of Rails (`*_gem.rb`) or edge Rails (`*_master.rb`).
@@ -55,14 +60,14 @@ WARNING: Please do not report security vulnerabilities with public GitHub issue
Please don't put "feature request" items into GitHub Issues. If there's a new
feature that you want to see added to Ruby on Rails, you'll need to write the
code yourself - or convince someone else to partner with you to write the code.
-Later in this guide you'll find detailed instructions for proposing a patch to
+Later in this guide, you'll find detailed instructions for proposing a patch to
Ruby on Rails. If you enter a wish list item in GitHub Issues with no code, you
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.
@@ -79,7 +84,9 @@ discussions new features require.
Helping to Resolve Existing Issues
----------------------------------
-As a next step beyond reporting issues, you can help the core team resolve existing issues. If you check the [issues list](https://github.com/rails/rails/issues) in GitHub Issues, you'll find lots of issues already requiring attention. What can you do for these? Quite a bit, actually:
+As a next step beyond reporting issues, you can help the core team resolve existing ones by providing feedback about them. If you are new to Rails core development, that might be a great way to walk your first steps, you'll get familiar with the code base and the processes.
+
+If you check the [issues list](https://github.com/rails/rails/issues) in GitHub Issues, you'll find lots of issues already requiring attention. What can you do for these? Quite a bit, actually:
### Verifying Bug Reports
@@ -87,22 +94,22 @@ For starters, it helps just to verify bug reports. Can you reproduce the reporte
If an issue is very vague, can you help narrow it down to something more specific? Maybe you can provide additional information to help reproduce a bug, or help by eliminating needless steps that aren't required to demonstrate the problem.
-If you find a bug report without a test, it's very useful to contribute a failing test. This is also a great way to get started exploring the source code: looking at the existing test files will teach you how to write more tests. New tests are best contributed in the form of a patch, as explained later on in the "Contributing to the Rails Code" section.
+If you find a bug report without a test, it's very useful to contribute a failing test. This is also a great way to get started exploring the source code: looking at the existing test files will teach you how to write more tests. New tests are best contributed in the form of a patch, as explained later on in the "[Contributing to the Rails Code](#contributing-to-the-rails-code)" section.
Anything you can do to make bug reports more succinct or easier to reproduce helps folks trying to write code to fix those bugs - whether you end up writing the code yourself or not.
### 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 git://github.com/JohnSmith/rails.git
+$ git remote add JohnSmith https://github.com/JohnSmith/rails.git
$ git pull JohnSmith orange
```
@@ -125,36 +132,62 @@ Contributing to the Rails Documentation
Ruby on Rails has two main sets of documentation: the guides, which help you
learn about Ruby on Rails, and the API, which serves as a reference.
-You can help improve the Rails guides by making them more coherent, consistent or readable, adding missing information, correcting factual errors, fixing typos, or bringing it up to date with the latest edge Rails. To get involved in the translation of Rails guides, please see [Translating Rails Guides](https://wiki.github.com/rails/docrails/translating-rails-guides).
+You can help improve the Rails guides by making them more coherent, consistent, or readable, adding missing information, correcting factual errors, fixing typos, or bringing them up to date with the latest edge Rails.
-You can either open a pull request to [Rails](http://github.com/rails/rails) or
-ask the [Rails core team](http://rubyonrails.org/core) for commit access on
-[docrails](http://github.com/rails/docrails) if you contribute regularly.
-Please do not open pull requests in docrails, if you'd like to get feedback on your
-change, ask for it in [Rails](http://github.com/rails/rails) instead.
+To do so, make changes to Rails guides source files (located [here](https://github.com/rails/rails/tree/master/guides/source) on GitHub). Then open a pull request to apply your
+changes to the master branch.
-Docrails is merged with master regularly, so you are effectively editing the Ruby on Rails documentation.
+When working with documentation, please take into account the [API Documentation Guidelines](api_documentation_guidelines.html) and the [Ruby on Rails Guides Guidelines](ruby_on_rails_guides_guidelines.html).
-If you are unsure of the documentation changes, you can create an issue in the [Rails](https://github.com/rails/rails/issues) issues tracker on GitHub.
+NOTE: To help our CI servers you should add [ci skip] to your documentation commit message to skip build on that commit. Please remember to use it for commits containing only documentation changes.
-When working with documentation, please take into account the [API Documentation Guidelines](api_documentation_guidelines.html) and the [Ruby on Rails Guides Guidelines](ruby_on_rails_guides_guidelines.html).
+Translating Rails Guides
+------------------------
-NOTE: As explained earlier, ordinary code patches should have proper documentation coverage. Docrails is only used for isolated documentation improvements.
+We are happy to have people volunteer to translate the Rails guides. Just follow these steps:
-NOTE: To help our CI servers you should add [ci skip] to your documentation commit message to skip build on that commit. Please remember to use it for commits containing only documentation changes.
+* Fork https://github.com/rails/rails.
+* Add a source folder for your own language, for example: *guides/source/it-IT* for Italian.
+* Copy the contents of *guides/source* into your own language directory and translate them.
+* Do NOT translate the HTML files, as they are automatically generated.
+
+Note that translations are not submitted to the Rails repository. As detailed above, your work happens in a fork. This is so because in practice documentation maintenance via patches is only sustainable in English.
-WARNING: Docrails has a very strict policy: no code can be touched whatsoever, no matter how trivial or small the change. Only RDoc and guides can be edited via docrails. Also, CHANGELOGs should never be edited in docrails.
+To generate the guides in HTML format cd into the *guides* directory then run (eg. for it-IT):
+
+```bash
+$ bundle install
+$ bundle exec rake guides:generate:html GUIDES_LANGUAGE=it-IT
+```
+
+This will generate the guides in an *output* directory.
+
+NOTE: The instructions are for Rails > 4. The Redcarpet Gem doesn't work with JRuby.
+
+Translation efforts we know about (various versions):
+
+* **Italian**: [https://github.com/rixlabs/docrails](https://github.com/rixlabs/docrails)
+* **Spanish**: [https://github.com/gramos/docrails/wiki](https://github.com/gramos/docrails/wiki)
+* **Polish**: [https://github.com/apohllo/docrails](https://github.com/apohllo/docrails)
+* **French** : [https://github.com/railsfrance/docrails](https://github.com/railsfrance/docrails)
+* **Czech** : [https://github.com/rubyonrails-cz/docrails/tree/czech](https://github.com/rubyonrails-cz/docrails/tree/czech)
+* **Turkish** : [https://github.com/ujk/docrails](https://github.com/ujk/docrails)
+* **Korean** : [https://github.com/rorlakr/rails-guides](https://github.com/rorlakr/rails-guides)
+* **Simplified Chinese** : [https://github.com/ruby-china/guides](https://github.com/ruby-china/guides)
+* **Traditional Chinese** : [https://github.com/docrails-tw/guides](https://github.com/docrails-tw/guides)
+* **Russian** : [https://github.com/morsbox/rusrails](https://github.com/morsbox/rusrails)
+* **Japanese** : [https://github.com/yasslab/railsguides.jp](https://github.com/yasslab/railsguides.jp)
Contributing to the Rails Code
------------------------------
### Setting Up a Development Environment
-To move on from submitting bugs to helping resolve existing issues or contributing your own code to Ruby on Rails, you _must_ be able to run its test suite. In this section of the guide you'll learn how to setup the tests on your own computer.
+To move on from submitting bugs to helping resolve existing issues or contributing your own code to Ruby on Rails, you _must_ be able to run its test suite. In this section of the guide, you'll learn how to setup the tests on your own computer.
#### The Easy Way
-The easiest and recommended way to get a development environment ready to hack is to use the [Rails development box](https://github.com/rails/rails-dev-box).
+The easiest and recommended way to get a development environment ready to hack is to use the [rails-dev-box](https://github.com/rails/rails-dev-box).
#### The Hard Way
@@ -165,7 +198,7 @@ In case you can't use the Rails development box, see [this other guide](developm
To be able to contribute code, you need to clone the Rails repository:
```bash
-$ git clone git://github.com/rails/rails.git
+$ git clone https://github.com/rails/rails.git
```
and create a dedicated branch:
@@ -206,7 +239,6 @@ Now get busy and add/edit code. You're on your branch now, so you can write what
* Include tests that fail without your code, and pass with it.
* Update the (surrounding) documentation, examples elsewhere, and the guides: whatever is affected by your contribution.
-
TIP: Changes that are cosmetic in nature and do not add anything substantial to the stability, functionality, or testability of Rails will generally not be accepted (read more about [our rationales behind this decision](https://github.com/rails/rails/pull/13771#issuecomment-32746700)).
#### Follow the Coding Conventions
@@ -221,48 +253,51 @@ Rails follows a simple set of coding style conventions:
* Prefer class << self over self.method for class methods.
* Use `my_method(my_arg)` not `my_method( my_arg )` or `my_method my_arg`.
* Use `a = b` and not `a=b`.
-* Use assert_not methods instead of refute.
+* Use assert\_not methods instead of refute.
* Prefer `method { do_stuff }` instead of `method{do_stuff}` for single-line blocks.
* Follow the conventions in the source you see used already.
The above are guidelines - please use your best judgment in using them.
-### Benchmark Your Code
-
-If your change has an impact on the performance of Rails, please use the
-[benchmark-ips](https://github.com/evanphx/benchmark-ips) gem to provide
-benchmark results for comparison.
-
-Here's an example of using benchmark-ips:
+Additionally, we have [RuboCop](https://www.rubocop.org/) rules defined to codify some of our coding conventions. You can run RuboCop locally against the file that you have modified before submitting a pull request:
-```ruby
-require 'benchmark/ips'
+```bash
+$ rubocop actionpack/lib/action_controller/metal/strong_parameters.rb
+Inspecting 1 file
+.
-Benchmark.ips do |x|
- x.report('addition') { 1 + 2 }
- x.report('addition with send') { 1.send(:+, 2) }
-end
+1 file inspected, no offenses detected
```
-This will generate a report with the following information:
+For `rails-ujs` CoffeeScript and JavaScript files, you can run `npm run lint` in `actionview` folder.
-```
-Calculating -------------------------------------
- addition 132.013k i/100ms
- addition with send 125.413k i/100ms
--------------------------------------------------
- addition 9.677M (± 1.7%) i/s - 48.449M
- addition with send 6.794M (± 1.1%) i/s - 33.987M
-```
+### Benchmark Your Code
-Please see the benchmark/ips [README](https://github.com/evanphx/benchmark-ips/blob/master/README.md) for more information.
+For changes that might have an impact on performance, please benchmark your
+code and measure the impact. Please share the benchmark script you used as well
+as the results. You should consider including this information in your commit
+message, which allows future contributors to easily verify your findings and
+determine if they are still relevant. (For example, future optimizations in the
+Ruby VM might render certain optimizations unnecessary.)
+
+It is very easy to make an optimization that improves performance for a
+specific scenario you care about but regresses on other common cases.
+Therefore, you should test your change against a list of representative
+scenarios. Ideally, they should be based on real-world scenarios extracted
+from production applications.
+
+You can use the [benchmark template](https://github.com/rails/rails/blob/master/guides/bug_report_templates/benchmark.rb)
+as a starting point. It includes the boilerplate code to setup a benchmark
+using the [benchmark-ips](https://github.com/evanphx/benchmark-ips) gem. The
+template is designed for testing relatively self-contained changes that can be
+inlined into the script.
### Running Tests
It is not customary in Rails to run the full test suite before pushing
-changes. The railties test suite in particular takes a long time, and even
-more if the source code is mounted in `/vagrant` as happens in the recommended
-workflow with the [rails-dev-box](https://github.com/rails/rails-dev-box).
+changes. The railties test suite in particular takes a long time, and takes an
+especially long time if the source code is mounted in `/vagrant` as happens in
+the recommended workflow with the [rails-dev-box](https://github.com/rails/rails-dev-box).
As a compromise, test what your code obviously affects, and if the change is
not in railties, run the whole test suite of the affected component. If all
@@ -289,6 +324,26 @@ $ cd actionmailer
$ bundle exec rake test
```
+#### For a Specific Directory
+
+If you want to run the tests located in a specific directory use the `TEST_DIR`
+environment variable. For example, this will run the tests in the
+`railties/test/generators` directory only:
+
+```bash
+$ cd railties
+$ TEST_DIR=generators bundle exec rake test
+```
+
+#### For a Specific File
+
+You can run the tests for a particular file by using:
+
+```bash
+$ cd actionpack
+$ bundle exec ruby -w -Itest test/template/form_helper_test.rb
+```
+
#### Running a Single Test
You can run a single test through ruby. For instance:
@@ -298,15 +353,36 @@ $ cd actionmailer
$ bundle exec ruby -w -Itest test/mail_layout_test.rb -n test_explicit_class_layout
```
-The `-n` option allows you to run a single method instead of the whole
-file.
+The `-n` option allows you to run a single method instead of the whole file.
+
+#### Running tests with a specific seed
+
+Test execution is randomized with a randomization seed. If you are experiencing random
+test failures you can more accurately reproduce a failing test scenario by specifically
+setting the randomization seed.
+
+Running all tests for a component:
+
+```bash
+$ cd actionmailer
+$ SEED=15002 bundle exec rake test
+```
+
+Running a single test file:
+
+```bash
+$ cd actionmailer
+$ SEED=15002 bundle exec ruby -w -Itest test/mail_layout_test.rb
+```
#### 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:
@@ -315,10 +391,9 @@ $ cd activerecord
$ bundle exec rake test:sqlite3
```
-You can now run the tests as you did for `sqlite3`. The tasks are respectively
+You can now run the tests as you did for `sqlite3`. The tasks are respectively:
```bash
-test:mysql
test:mysql2
test:postgresql
```
@@ -329,12 +404,12 @@ Finally,
$ bundle exec rake test
```
-will now run the four of them in turn.
+will now run the three of them in turn.
You can also run any single test separately:
```bash
-$ ARCONN=sqlite3 ruby -Itest test/cases/associations/has_many_associations_test.rb
+$ ARCONN=sqlite3 bundle exec ruby -Itest test/cases/associations/has_many_associations_test.rb
```
To run a single test against all adapters, use:
@@ -349,17 +424,11 @@ You can invoke `test_jdbcmysql`, `test_jdbcsqlite3` or `test_jdbcpostgresql` als
The test suite runs with warnings enabled. Ideally, Ruby on Rails should issue no warnings, but there may be a few, as well as some from third-party libraries. Please ignore (or fix!) them, if any, and submit patches that do not issue new warnings.
-If you are sure about what you are doing and would like to have a more clear output, there's a way to override the flag:
-
-```bash
-$ RUBYOPT=-W0 bundle exec rake test
-```
-
### Updating the CHANGELOG
The CHANGELOG is an important part of every release. It keeps the list of changes for every Rails version.
-You should add an entry **to the top** of the CHANGELOG of the framework that you modified if you're adding or removing a feature, committing a bug fix or adding deprecation notices. Refactorings and documentation changes generally should not go to the CHANGELOG.
+You should add an entry **to the top** of the CHANGELOG of the framework that you modified if you're adding or removing a feature, committing a bug fix, or adding deprecation notices. Refactorings and documentation changes generally should not go to the CHANGELOG.
A CHANGELOG entry should summarize what was changed and should end with the author's name. You can use multiple lines if you need more space and you can attach code examples indented with 4 spaces. If a change is related to a specific issue, you should attach the issue's number. Here is an example CHANGELOG entry:
@@ -373,7 +442,7 @@ A CHANGELOG entry should summarize what was changed and should end with the auth
end
end
- You can continue after the code example and you can attach issue number. GH#1234
+ You can continue after the code example and you can attach issue number. Fixes #1234.
*Your Name*
```
@@ -385,16 +454,6 @@ examples or multiple paragraphs. Otherwise, it's best to make a new paragraph.
Some changes require the dependencies to be upgraded. In these cases make sure you run `bundle update` to get the right version of the dependency and commit the `Gemfile.lock` file within your changes.
-### Sanity Check
-
-You should not be the only person who looks at the code before you submit it.
-If you know someone else who uses Rails, try asking them if they'll check out
-your work. If you don't know anyone else using Rails, try hopping into the IRC
-room or posting about your idea to the rails-core mailing list. Doing this in
-private before you push a patch out publicly is the "smoke test" for a patch:
-if you can't convince one other developer of the beauty of your code, you’re
-unlikely to convince the core team either.
-
### Commit Your Changes
When you're happy with the code on your computer, you need to commit the changes to Git:
@@ -468,21 +527,13 @@ Navigate to the Rails [GitHub repository](https://github.com/rails/rails) and pr
Add the new remote to your local repository on your local machine:
```bash
-$ git remote add mine git@github.com:<your user name>/rails.git
-```
-
-Push to your remote:
-
-```bash
-$ git push mine my_new_branch
+$ git remote add fork https://github.com/<your user name>/rails.git
```
-You might have cloned your forked repository into your machine and might want to add the original Rails repository as a remote instead, if that's the case here's what you have to do.
-
-In the directory you cloned your fork:
+You may have cloned your local repository from rails/rails or you may have cloned from your forked repository. To avoid ambiguity the following git commands assume that you have made a "rails" remote that points to rails/rails.
```bash
-$ git remote add rails git://github.com/rails/rails.git
+$ git remote add rails https://github.com/rails/rails.git
```
Download new commits and branches from the official repository:
@@ -496,23 +547,17 @@ Merge the new content:
```bash
$ git checkout master
$ git rebase rails/master
+$ git checkout my_new_branch
+$ git rebase rails/master
```
Update your fork:
```bash
-$ git push origin master
+$ git push fork master
+$ git push fork my_new_branch
```
-If you want to update another branch:
-
-```bash
-$ git checkout branch_name
-$ git rebase rails/branch_name
-$ git push origin branch_name
-```
-
-
### Issue a Pull Request
Navigate to the Rails repository you just pushed to (e.g.
@@ -542,7 +587,7 @@ is the open source life.
If it's been over a week, and you haven't heard anything, you might want to try
and nudge things along. You can use the [rubyonrails-core mailing
-list](http://groups.google.com/group/rubyonrails-core/) for this. You can also
+list](https://groups.google.com/forum/#!forum/rubyonrails-core) for this. You can also
leave another comment on the pull request.
While you're waiting for feedback on your pull request, open up a few other
@@ -562,35 +607,21 @@ branches, squashing makes it easier to revert bad commits, and the git history
can be a bit easier to follow. Rails is a large project, and a bunch of
extraneous commits can add a lot of noise.
-In order to do this, you'll need to have a git remote that points at the main
-Rails repository. This is useful anyway, but just in case you don't have it set
-up, make sure that you do this first:
-
```bash
-$ git remote add upstream https://github.com/rails/rails.git
-```
-
-You can call this remote whatever you'd like, but if you don't use `upstream`,
-then change the name to your own in the instructions below.
-
-Given that your remote branch is called `my_pull_request`, then you can do the
-following:
-
-```bash
-$ git fetch upstream
-$ git checkout my_pull_request
-$ git rebase -i upstream/master
+$ git fetch rails
+$ git checkout my_new_branch
+$ git rebase -i rails/master
< Choose 'squash' for all of your commits except the first one. >
< Edit the commit message to make sense, and describe all your changes. >
-$ git push origin my_pull_request -f
+$ git push fork my_new_branch --force-with-lease
```
You should be able to refresh the pull request on GitHub and see that it has
been updated.
-#### Updating pull request
+#### Updating a pull request
Sometimes you will be asked to make some changes to the code you have
already committed. This can include amending existing commits. In this
@@ -600,19 +631,20 @@ you can force push to your branch on GitHub as described earlier in
squashing commits section:
```bash
-$ git push origin my_pull_request -f
+$ git push fork my_new_branch --force-with-lease
```
-This will update the branch and pull request on GitHub with your new code. Do
-note that using force push may result in commits being lost on the remote branch; use it with care.
-
+This will update the branch and pull request on GitHub with your new code.
+By force pushing with `--force-with-lease`, git will more safely update
+the remote than with a typical `-f`, which can delete work from the remote
+that you don't already have.
### Older Versions of Ruby on Rails
If you want to add a fix to older versions of Ruby on Rails, you'll need to set up and switch to your own local tracking branch. Here is an example to switch to the 4-0-stable branch:
```bash
-$ git branch --track 4-0-stable origin/4-0-stable
+$ git branch --track 4-0-stable rails/4-0-stable
$ git checkout 4-0-stable
```
@@ -624,7 +656,7 @@ Changes that are merged into master are intended for the next major release of R
For simple fixes, the easiest way to backport your changes is to [extract a diff from your changes in master and apply them to the target branch](http://ariejan.net/2009/10/26/how-to-create-and-apply-a-patch-with-git).
-First make sure your changes are the only difference between your current branch and master:
+First, make sure your changes are the only difference between your current branch and master:
```bash
$ git log master..HEAD
@@ -639,7 +671,7 @@ $ git format-patch master --stdout > ~/my_changes.patch
Switch over to the target branch and apply your changes:
```bash
-$ git checkout -b my_backport_branch 3-2-stable
+$ git checkout -b my_backport_branch 4-2-stable
$ git apply ~/my_changes.patch
```
@@ -652,4 +684,4 @@ And then... think about your next contribution!
Rails Contributors
------------------
-All contributions, either via master or docrails, get credit in [Rails Contributors](http://contributors.rubyonrails.org).
+All contributions get credit in [Rails Contributors](http://contributors.rubyonrails.org).
diff --git a/guides/source/credits.html.erb b/guides/source/credits.html.erb
deleted file mode 100644
index 61ea0b44ef..0000000000
--- a/guides/source/credits.html.erb
+++ /dev/null
@@ -1,80 +0,0 @@
-<% content_for :page_title do %>
-Ruby on Rails Guides: Credits
-<% end %>
-
-<% content_for :header_section do %>
-<h2>Credits</h2>
-
-<p>We'd like to thank the following people for their tireless contributions to this project.</p>
-
-<% end %>
-
-<h3 class="section">Rails Guides Reviewers</h3>
-
-<%= author('Vijay Dev', 'vijaydev', 'vijaydev.jpg') do %>
- Vijayakumar, found as Vijay Dev on the web, is a web applications developer and an open source enthusiast who lives in Chennai, India. He started using Rails in 2009 and began actively contributing to Rails documentation in late 2010. He <a href="https://twitter.com/vijay_dev">tweets</a> a lot and also <a href="http://vijaydev.wordpress.com">blogs</a>.
-<% end %>
-
-<%= author('Xavier Noria', 'fxn', 'fxn.png') do %>
- Xavier Noria has been into Ruby on Rails since 2005. He is a Rails core team member and enjoys combining his passion for Rails and his past life as a proofreader of math textbooks. Xavier is currently an independent Ruby on Rails consultant. Oh, he also <a href="http://twitter.com/fxn">tweets</a> and can be found everywhere as &quot;fxn&quot;.
-<% end %>
-
-<h3 class="section">Rails Guides Designers</h3>
-
-<%= author('Jason Zimdars', 'jz') do %>
- Jason Zimdars is an experienced creative director and web designer who has lead UI and UX design for numerous websites and web applications. You can see more of his design and writing at <a href="http://www.thinkcage.com/">Thinkcage.com</a> or follow him on <a href="http://twitter.com/JZ">Twitter</a>.
-<% end %>
-
-<h3 class="section">Rails Guides Authors</h3>
-
-<%= author('Ryan Bigg', 'radar', 'radar.png') do %>
- Ryan Bigg works as the Community Manager at <a href="http://spreecommerce.com">Spree Commerce</a> and has been working with Rails since 2006. He's the author of <a href="https://leanpub.com/multi-tenancy-rails">Multi Tenancy With Rails</a> and co-author of <a href="http://manning.com/bigg2">Rails 4 in Action</a>. He's written many gems which can be seen on <a href="https://github.com/radar">his GitHub page</a> and he also tweets prolifically as <a href="http://twitter.com/ryanbigg">@ryanbigg</a>.
-<% end %>
-
-<%= author('Oscar Del Ben', 'oscardelben', 'oscardelben.jpg') do %>
-Oscar Del Ben is a software engineer at <a href="http://www.wildfireapp.com/">Wildfire</a>. He's a regular open source contributor (<a href="https://github.com/oscardelben">GitHub account</a>) and tweets regularly at <a href="https://twitter.com/oscardelben">@oscardelben</a>.
- <% end %>
-
-<%= author('Frederick Cheung', 'fcheung') do %>
- Frederick Cheung is Chief Wizard at Texperts where he has been using Rails since 2006. He is based in Cambridge (UK) and when not consuming fine ales he blogs at <a href="http://www.spacevatican.org">spacevatican.org</a>.
-<% end %>
-
-<%= author('Tore Darell', 'toretore') do %>
- Tore Darell is an independent developer based in Menton, France who specialises in cruft-free web applications using Ruby, Rails and unobtrusive JavaScript. You can follow him on <a href="http://twitter.com/toretore">Twitter</a>.
-<% end %>
-
-<%= author('Jeff Dean', 'zilkey') do %>
- Jeff Dean is a software engineer with <a href="http://pivotallabs.com">Pivotal Labs</a>.
-<% end %>
-
-<%= author('Mike Gunderloy', 'mgunderloy') do %>
- Mike Gunderloy is a consultant with <a href="http://www.actionrails.com">ActionRails</a>. He brings 25 years of experience in a variety of languages to bear on his current work with Rails. His near-daily links and other blogging can be found at <a href="http://afreshcup.com">A Fresh Cup</a> and he <a href="http://twitter.com/MikeG1">twitters</a> too much.
-<% end %>
-
-<%= author('Mikel Lindsaar', 'raasdnil') do %>
- Mikel Lindsaar has been working with Rails since 2006 and is the author of the Ruby <a href="https://github.com/mikel/mail">Mail gem</a> and core contributor (he helped re-write Action Mailer's API). Mikel is the founder of <a href="http://rubyx.com/">RubyX</a>, has a <a href="http://lindsaar.net/">blog</a> and <a href="http://twitter.com/raasdnil">tweets</a>.
-<% end %>
-
-<%= author('Cássio Marques', 'cmarques') do %>
- Cássio Marques is a Brazilian software developer working with different programming languages such as Ruby, JavaScript, CPP and Java, as an independent consultant. He blogs at <a href="http://cassiomarques.wordpress.com">/* CODIFICANDO */</a>, which is mainly written in Portuguese, but will soon get a new section for posts with English translation.
-<% end %>
-
-<%= author('James Miller', 'bensie') do %>
- James Miller is a software developer for <a href="http://www.jk-tech.com">JK Tech</a> in San Diego, CA. You can find James on GitHub, Gmail, Twitter, and Freenode as &quot;bensie&quot;.
-<% end %>
-
-<%= author('Pratik Naik', 'lifo') do %>
- Pratik Naik is a Ruby on Rails developer at <a href="https://basecamp.com/">Basecamp</a> and also a member of the <a href="http://rubyonrails.org/core">Rails core team</a>. He maintains a blog at <a href="http://m.onkey.org">has_many :bugs, :through =&gt; :rails</a> and has a semi-active <a href="http://twitter.com/lifo">twitter account</a>.
-<% end %>
-
-<%= author('Emilio Tagua', 'miloops') do %>
- Emilio Tagua &mdash;a.k.a. miloops&mdash; is an Argentinian entrepreneur, developer, open source contributor and Rails evangelist. Cofounder of <a href="http://eventioz.com">Eventioz</a>. He has been using Rails since 2006 and contributing since early 2008. Can be found at gmail, twitter, freenode, everywhere as &quot;miloops&quot;.
-<% end %>
-
-<%= author('Heiko Webers', 'hawe') do %>
- Heiko Webers is the founder of <a href="http://www.bauland42.de">bauland42</a>, a German web application security consulting and development company focused on Ruby on Rails. He blogs at the <a href="http://www.rorsecurity.info">Ruby on Rails Security Project</a>. After 10 years of desktop application development, Heiko has rarely looked back.
-<% end %>
-
-<%= author('Akshay Surve', 'startupjockey', 'akshaysurve.jpg') do %>
- Akshay Surve is the Founder at <a href="http://www.deltax.com">DeltaX</a>, hackathon specialist, a midnight code junkie and occasionally writes prose. You can connect with him on <a href="https://twitter.com/akshaysurve">Twitter</a>, <a href="http://www.linkedin.com/in/akshaysurve">Linkedin</a>, <a href="http://www.akshaysurve.com/">Personal Blog</a> or <a href="http://www.quora.com/Akshay-Surve">Quora</a>.
-<% end %>
diff --git a/guides/source/debugging_rails_applications.md b/guides/source/debugging_rails_applications.md
index dc1df8f229..3a383cbd4d 100644
--- a/guides/source/debugging_rails_applications.md
+++ b/guides/source/debugging_rails_applications.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Debugging Rails Applications
============================
@@ -109,18 +109,18 @@ It can also be useful to save information to log files at runtime. Rails maintai
Rails makes use of the `ActiveSupport::Logger` class to write log information. Other loggers, such as `Log4r`, may also be substituted.
-You can specify an alternative logger in `environment.rb` or any other environment file, for example:
+You can specify an alternative logger in `config/application.rb` or any other environment file, for example:
```ruby
-Rails.logger = Logger.new(STDOUT)
-Rails.logger = Log4r::Logger.new("Application Log")
+config.logger = Logger.new(STDOUT)
+config.logger = Log4r::Logger.new("Application Log")
```
Or in the `Initializer` section, add _any_ of the following
```ruby
-config.logger = Logger.new(STDOUT)
-config.logger = Log4r::Logger.new("Application Log")
+Rails.logger = Logger.new(STDOUT)
+Rails.logger = Log4r::Logger.new("Application Log")
```
TIP: By default, each log is created under `Rails.root/log/` and the log file is named after the environment in which the application is running.
@@ -147,7 +147,7 @@ TIP: The default Rails log level is `debug` in all environments.
### Sending Messages
-To write in the current log use the `logger.(debug|info|warn|error|fatal)` method from within a controller, model or mailer:
+To write in the current log use the `logger.(debug|info|warn|error|fatal)` method from within a controller, model, or mailer:
```ruby
logger.debug "Person attributes hash: #{@person.attributes.inspect}"
@@ -162,45 +162,82 @@ class ArticlesController < ApplicationController
# ...
def create
- @article = Article.new(params[:article])
+ @article = Article.new(article_params)
logger.debug "New article: #{@article.attributes.inspect}"
logger.debug "Article should be valid: #{@article.valid?}"
if @article.save
- flash[:notice] = 'Article was successfully created.'
logger.debug "The article was saved and now the user is going to be redirected..."
- redirect_to(@article)
+ redirect_to @article, notice: 'Article was successfully created.'
else
- render action: "new"
+ render :new
end
end
# ...
+
+ private
+ def article_params
+ params.require(:article).permit(:title, :body, :published)
+ end
end
```
Here's an example of the log generated when this controller action is executed:
```
-Processing ArticlesController#create (for 127.0.0.1 at 2008-09-08 11:52:54) [POST]
- Session ID: BAh7BzoMY3NyZl9pZCIlMDY5MWU1M2I1ZDRjODBlMzkyMWI1OTg2NWQyNzViZjYiCmZsYXNoSUM6J0FjdGl
-vbkNvbnRyb2xsZXI6OkZsYXNoOjpGbGFzaEhhc2h7AAY6CkB1c2VkewA=--b18cd92fba90eacf8137e5f6b3b06c4d724596a4
- Parameters: {"commit"=>"Create", "article"=>{"title"=>"Debugging Rails",
- "body"=>"I'm learning how to print in logs!!!", "published"=>"0"},
- "authenticity_token"=>"2059c1286e93402e389127b1153204e0d1e275dd", "action"=>"create", "controller"=>"articles"}
-New article: {"updated_at"=>nil, "title"=>"Debugging Rails", "body"=>"I'm learning how to print in logs!!!",
- "published"=>false, "created_at"=>nil}
+Started POST "/articles" for 127.0.0.1 at 2018-10-18 20:09:23 -0400
+Processing by ArticlesController#create as HTML
+ Parameters: {"utf8"=>"✓", "authenticity_token"=>"XLveDrKzF1SwaiNRPTaMtkrsTzedtebPPkmxEFIU0ordLjICSnXsSNfrdMa4ccyBjuGwnnEiQhEoMN6H1Gtz3A==", "article"=>{"title"=>"Debugging Rails", "body"=>"I'm learning how to print in logs.", "published"=>"0"}, "commit"=>"Create Article"}
+New article: {"id"=>nil, "title"=>"Debugging Rails", "body"=>"I'm learning how to print in logs.", "published"=>false, "created_at"=>nil, "updated_at"=>nil}
Article should be valid: true
- Article Create (0.000443) INSERT INTO "articles" ("updated_at", "title", "body", "published",
- "created_at") VALUES('2008-09-08 14:52:54', 'Debugging Rails',
- 'I''m learning how to print in logs!!!', 'f', '2008-09-08 14:52:54')
+ (0.0ms) begin transaction
+ ↳ app/controllers/articles_controller.rb:31
+ Article Create (0.5ms) INSERT INTO "articles" ("title", "body", "published", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) [["title", "Debugging Rails"], ["body", "I'm learning how to print in logs."], ["published", 0], ["created_at", "2018-10-19 00:09:23.216549"], ["updated_at", "2018-10-19 00:09:23.216549"]]
+ ↳ app/controllers/articles_controller.rb:31
+ (2.3ms) commit transaction
+ ↳ app/controllers/articles_controller.rb:31
The article was saved and now the user is going to be redirected...
-Redirected to # Article:0x20af760>
-Completed in 0.01224 (81 reqs/sec) | DB: 0.00044 (3%) | 302 Found [http://localhost/articles]
+Redirected to http://localhost:3000/articles/1
+Completed 302 Found in 4ms (ActiveRecord: 0.8ms)
```
Adding extra logging like this makes it easy to search for unexpected or unusual behavior in your logs. If you add extra logging, be sure to make sensible use of log levels to avoid filling your production logs with useless trivia.
+### Verbose Query Logs
+
+When looking at database query output in logs, it may not be immediately clear why multiple database queries are triggered when a single method is called:
+
+```
+irb(main):001:0> Article.pamplemousse
+ Article Load (0.4ms) SELECT "articles".* FROM "articles"
+ Comment Load (0.2ms) SELECT "comments".* FROM "comments" WHERE "comments"."article_id" = ? [["article_id", 1]]
+ Comment Load (0.1ms) SELECT "comments".* FROM "comments" WHERE "comments"."article_id" = ? [["article_id", 2]]
+ Comment Load (0.1ms) SELECT "comments".* FROM "comments" WHERE "comments"."article_id" = ? [["article_id", 3]]
+=> #<Comment id: 2, author: "1", body: "Well, actually...", article_id: 1, created_at: "2018-10-19 00:56:10", updated_at: "2018-10-19 00:56:10">
+```
+
+After running `ActiveRecord::Base.verbose_query_logs = true` in the `rails console` session to enable verbose query logs and running the method again, it becomes obvious what single line of code is generating all these discrete database calls:
+
+```
+irb(main):003:0> Article.pamplemousse
+ Article Load (0.2ms) SELECT "articles".* FROM "articles"
+ ↳ app/models/article.rb:5
+ Comment Load (0.1ms) SELECT "comments".* FROM "comments" WHERE "comments"."article_id" = ? [["article_id", 1]]
+ ↳ app/models/article.rb:6
+ Comment Load (0.1ms) SELECT "comments".* FROM "comments" WHERE "comments"."article_id" = ? [["article_id", 2]]
+ ↳ app/models/article.rb:6
+ Comment Load (0.1ms) SELECT "comments".* FROM "comments" WHERE "comments"."article_id" = ? [["article_id", 3]]
+ ↳ app/models/article.rb:6
+=> #<Comment id: 2, author: "1", body: "Well, actually...", article_id: 1, created_at: "2018-10-19 00:56:10", updated_at: "2018-10-19 00:56:10">
+```
+
+Below each database statement you can see arrows pointing to the specific source filename (and line number) of the method that resulted in a database call. This can help you identify and address performance problems caused by N+1 queries: single database queries that generates multiple additional queries.
+
+Verbose query logs are enabled by default in the development environment logs after Rails 5.2.
+
+WARNING: We recommend against using this setting in production environments. It relies on Ruby's `Kernel#caller` method which tends to allocate a lot of memory in order to generate stacktraces of method calls.
+
### Tagged Logging
When running multi-user, multi-account applications, it's often useful
@@ -255,7 +292,8 @@ is your best companion.
The debugger can also help you if you want to learn about the Rails source code
but don't know where to start. Just debug any request to your application and
-use this guide to learn how to move from the code you have written into the underlying Rails code.
+use this guide to learn how to move from the code you have written into the
+underlying Rails code.
### Setup
@@ -311,16 +349,15 @@ processing the entire request.
For example:
```bash
-=> Booting WEBrick
-=> Rails 5.0.0 application starting in development on http://0.0.0.0:3000
+=> Booting Puma
+=> Rails 5.1.0 application starting in development on http://0.0.0.0:3000
=> Run `rails server -h` for more startup options
-=> Notice: server is listening on all interfaces (0.0.0.0). Consider using 127.0.0.1 (--binding option)
-=> Ctrl-C to shutdown server
-[2014-04-11 13:11:47] INFO WEBrick 1.3.1
-[2014-04-11 13:11:47] INFO ruby 2.2.2 (2015-04-13) [i686-linux]
-[2014-04-11 13:11:47] INFO WEBrick::HTTPServer#start: pid=6370 port=3000
-
-
+Puma starting in single mode...
+* Version 3.4.0 (ruby 2.3.1-p112), codename: Owl Bowl Brawl
+* Min threads: 5, max threads: 5
+* Environment: development
+* Listening on tcp://localhost:3000
+Use Ctrl-C to stop
Started GET "/" for 127.0.0.1 at 2014-04-11 13:11:48 +0200
ActiveRecord::SchemaMigration Load (0.2ms) SELECT "schema_migrations".* FROM "schema_migrations"
Processing by ArticlesController#index as HTML
@@ -336,7 +373,6 @@ Processing by ArticlesController#index as HTML
10: respond_to do |format|
11: format.html # index.html.erb
12: format.json { render json: @articles }
-
(byebug)
```
@@ -346,23 +382,47 @@ by asking the debugger for help. Type: `help`
```
(byebug) help
-byebug 2.7.0
+ break -- Sets breakpoints in the source code
+ catch -- Handles exception catchpoints
+ condition -- Sets conditions on breakpoints
+ continue -- Runs until program ends, hits a breakpoint or reaches a line
+ debug -- Spawns a subdebugger
+ delete -- Deletes breakpoints
+ disable -- Disables breakpoints or displays
+ display -- Evaluates expressions every time the debugger stops
+ down -- Moves to a lower frame in the stack trace
+ edit -- Edits source files
+ enable -- Enables breakpoints or displays
+ finish -- Runs the program until frame returns
+ frame -- Moves to a frame in the call stack
+ help -- Helps you using byebug
+ history -- Shows byebug's history of commands
+ info -- Shows several informations about the program being debugged
+ interrupt -- Interrupts the program
+ irb -- Starts an IRB session
+ kill -- Sends a signal to the current process
+ list -- Lists lines of source code
+ method -- Shows methods of an object, class or module
+ next -- Runs one or more lines of code
+ pry -- Starts a Pry session
+ quit -- Exits byebug
+ restart -- Restarts the debugged program
+ save -- Saves current byebug session to a file
+ set -- Modifies byebug settings
+ show -- Shows byebug settings
+ source -- Restores a previously saved byebug session
+ step -- Steps into blocks or methods one or more times
+ thread -- Commands to manipulate threads
+ tracevar -- Enables tracing of a global variable
+ undisplay -- Stops displaying all or some expressions when program stops
+ untracevar -- Stops tracing a global variable
+ up -- Moves to a higher frame in the stack trace
+ var -- Shows variables and its values
+ where -- Displays the backtrace
-Type 'help <command-name>' for help on a specific command
-
-Available commands:
-backtrace delete enable help list pry next restart source up
-break disable eval info method ps save step var
-catch display exit interrupt next putl set thread
-condition down finish irb p quit show trace
-continue edit frame kill pp reload skip undisplay
+(byebug)
```
-TIP: To view the help menu for any command use `help <command-name>` at the
-debugger prompt. For example: _`help list`_. You can abbreviate any debugging
-command by supplying just enough letters to distinguish them from other
-commands. For example, you can use `l` for the `list` command.
-
To see the previous ten lines you should type `list-` (or `l-`).
```
@@ -378,13 +438,12 @@ To see the previous ten lines you should type `list-` (or `l-`).
7 byebug
8 @articles = Article.find_recent
9
- 10 respond_to do |format|
-
+ 10 respond_to do |format|
```
-This way you can move inside the file and see the code above
-the line where you added the `byebug` call. Finally, to see where you are in
-the code again you can type `list=`
+This way you can move inside the file and see the code above the line where you
+added the `byebug` call. Finally, to see where you are in the code again you can
+type `list=`
```
(byebug) list=
@@ -400,7 +459,6 @@ the code again you can type `list=`
10: respond_to do |format|
11: format.html # index.html.erb
12: format.json { render json: @articles }
-
(byebug)
```
@@ -422,59 +480,58 @@ then `backtrace` will supply the answer.
```
(byebug) where
--> #0 ArticlesController.index
- at /PathTo/project/test_app/app/controllers/articles_controller.rb:8
- #1 ActionController::ImplicitRender.send_action(method#String, *args#Array)
- at /PathToGems/actionpack-5.0.0/lib/action_controller/metal/implicit_render.rb:4
+ at /PathToProject/app/controllers/articles_controller.rb:8
+ #1 ActionController::BasicImplicitRender.send_action(method#String, *args#Array)
+ 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:189
- #3 ActionController::Rendering.process_action(action#NilClass, *args#NilClass)
- at /PathToGems/actionpack-5.0.0/lib/action_controller/metal/rendering.rb:10
+ at /PathToGems/actionpack-5.1.0/lib/abstract_controller/base.rb:181
+ #3 ActionController::Rendering.process_action(action, *args)
+ at /PathToGems/actionpack-5.1.0/lib/action_controller/metal/rendering.rb:30
...
```
The current frame is marked with `-->`. You can move anywhere you want in this
-trace (thus changing the context) by using the `frame _n_` command, where _n_ is
+trace (thus changing the context) by using the `frame n` command, where _n_ is
the specified frame number. If you do that, `byebug` will display your new
context.
```
(byebug) frame 2
-[184, 193] in /PathToGems/actionpack-5.0.0/lib/abstract_controller/base.rb
- 184: # is the intended way to override action dispatching.
- 185: #
- 186: # Notice that the first argument is the method to be dispatched
- 187: # which is *not* necessarily the same as the action name.
- 188: def process_action(method_name, *args)
-=> 189: send_action(method_name, *args)
- 190: end
- 191:
- 192: # Actually call the method associated with the action. Override
- 193: # this method if you wish to change how action methods are called,
-
+[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
+ 179: # which is *not* necessarily the same as the action name.
+ 180: def process_action(method_name, *args)
+=> 181: send_action(method_name, *args)
+ 182: end
+ 183:
+ 184: # Actually call the method associated with the action. Override
+ 185: # this method if you wish to change how action methods are called,
(byebug)
```
The available variables are the same as if you were running the code line by
line. After all, that's what debugging is.
-You can also use `up [n]` (`u` for abbreviated) and `down [n]` commands in order
-to change the context _n_ frames up or down the stack respectively. _n_ defaults
-to one. Up in this case is towards higher-numbered stack frames, and down is
-towards lower-numbered stack frames.
+You can also use `up [n]` and `down [n]` commands in order to change the context
+_n_ frames up or down the stack respectively. _n_ defaults to one. Up in this
+case is towards higher-numbered stack frames, and down is towards lower-numbered
+stack frames.
### Threads
-The debugger can list, stop, resume and switch between running threads by using
+The debugger can list, stop, resume, and switch between running threads by using
the `thread` command (or the abbreviated `th`). This command has a handful of
options:
-* `thread` shows the current thread.
-* `thread list` is used to list all threads and their statuses. The plus +
-character and the number indicates the current thread of execution.
-* `thread stop _n_` stop thread _n_.
-* `thread resume _n_` resumes thread _n_.
-* `thread switch _n_` switches the current thread context to _n_.
+* `thread`: shows the current thread.
+* `thread list`: is used to list all threads and their statuses. The current
+thread is marked with a plus (+) sign.
+* `thread stop n`: stops thread _n_.
+* `thread resume n`: resumes thread _n_.
+* `thread switch n`: switches the current thread context to _n_.
This command is very helpful when you are debugging concurrent threads and need
to verify that there are no race conditions in your code.
@@ -501,9 +558,9 @@ current context:
12: format.json { render json: @articles }
(byebug) instance_variables
-[:@_action_has_layout, :@_routes, :@_headers, :@_status, :@_request,
- :@_response, :@_env, :@_prefixes, :@_lookup_context, :@_action_name,
- :@_response_body, :@marked_for_same_origin_verification, :@_config]
+[:@_action_has_layout, :@_routes, :@_request, :@_response, :@_lookup_context,
+ :@_action_name, :@_response_body, :@marked_for_same_origin_verification,
+ :@_config]
```
As you may have figured out, all of the variables that you can access from a
@@ -513,14 +570,15 @@ command later in this guide).
```
(byebug) next
+
[5, 14] in /PathTo/project/app/controllers/articles_controller.rb
5 # GET /articles.json
6 def index
7 byebug
8 @articles = Article.find_recent
9
-=> 10 respond_to do |format|
- 11 format.html # index.html.erb
+=> 10 respond_to do |format|
+ 11 format.html # index.html.erb
12 format.json { render json: @articles }
13 end
14 end
@@ -532,29 +590,35 @@ And then ask again for the instance_variables:
```
(byebug) instance_variables
-[:@_action_has_layout, :@_routes, :@_headers, :@_status, :@_request,
- :@_response, :@_env, :@_prefixes, :@_lookup_context, :@_action_name,
- :@_response_body, :@marked_for_same_origin_verification, :@_config,
- :@articles]
+[:@_action_has_layout, :@_routes, :@_request, :@_response, :@_lookup_context,
+ :@_action_name, :@_response_body, :@marked_for_same_origin_verification,
+ :@_config, :@articles]
```
-Now `@articles` is included in the instance variables, because the line defining it
-was executed.
+Now `@articles` is included in the instance variables, because the line defining
+it was executed.
TIP: You can also step into **irb** mode with the command `irb` (of course!).
-This will start an irb session within the context you invoked it. But
-be warned: this is an experimental feature.
+This will start an irb session within the context you invoked it.
The `var` method is the most convenient way to show variables and their values.
Let's have `byebug` help us with it.
```
(byebug) help var
-v[ar] cl[ass] show class variables of self
-v[ar] const <object> show constants of object
-v[ar] g[lobal] show global variables
-v[ar] i[nstance] <object> show instance variables of object
-v[ar] l[ocal] show local variables
+
+ [v]ar <subcommand>
+
+ Shows variables and its values
+
+
+ var all -- Shows local, global and instance variables of self.
+ var args -- Information about arguments of the current scope
+ var const -- Shows constants of an object.
+ var global -- Shows global variables.
+ var instance -- Shows instance variables of self or a specific object.
+ var local -- Shows local variables in current scope.
+
```
This is a great way to inspect the values of the current context variables. For
@@ -572,16 +636,16 @@ You can also inspect for an object method this way:
@_start_transaction_state = {}
@aggregation_cache = {}
@association_cache = {}
-@attributes = {"id"=>nil, "created_at"=>nil, "updated_at"=>nil}
-@attributes_cache = {}
-@changed_attributes = nil
-...
+@attributes = #<ActiveRecord::AttributeSet:0x007fd0682a9b18 @attributes={"id"=>#<ActiveRecord::Attribute::FromDatabase:0x007fd0682a9a00 @name="id", @value_be...
+@destroyed = false
+@destroyed_by_association = nil
+@marked_for_destruction = false
+@new_record = true
+@readonly = false
+@transaction_state = nil
```
-TIP: The commands `p` (print) and `pp` (pretty print) can be used to evaluate
-Ruby expressions and display the value of variables to the console.
-
-You can use also `display` to start watching variables. This is a good way of
+You can also use `display` to start watching variables. This is a good way of
tracking the values of a variable while the execution goes on.
```
@@ -590,7 +654,7 @@ tracking the values of a variable while the execution goes on.
```
The variables inside the displayed list will be printed with their values after
-you move in the stack. To stop displaying a variable use `undisplay _n_` where
+you move in the stack. To stop displaying a variable use `undisplay n` where
_n_ is the variable number (1 in the last example).
### Step by Step
@@ -600,45 +664,35 @@ available variables. But let's continue and move on with the application
execution.
Use `step` (abbreviated `s`) to continue running your program until the next
-logical stopping point and return control to the debugger.
-
-You may also use `next` which is similar to step, but function or method calls
-that appear within the line of code are executed without stopping.
-
-TIP: You can also use `step n` or `next n` to move forwards `n` steps at once.
-
-The difference between `next` and `step` is that `step` stops at the next line
-of code executed, doing just a single step, while `next` moves to the next line
-without descending inside methods.
+logical stopping point and return control to the debugger. `next` is similar to
+`step`, but while `step` stops at the next line of code executed, doing just a
+single step, `next` moves to the next line without descending inside methods.
For example, consider the following situation:
-```ruby
+```
Started GET "/" for 127.0.0.1 at 2014-04-11 13:39:23 +0200
Processing by ArticlesController#index as HTML
-[1, 8] in /home/davidr/Proyectos/test_app/app/models/article.rb
- 1: class Article < ActiveRecord::Base
- 2:
- 3: def self.find_recent(limit = 10)
- 4: byebug
-=> 5: where('created_at > ?', 1.week.ago).limit(limit)
- 6: end
- 7:
- 8: end
+[1, 6] in /PathToProject/app/models/article.rb
+ 1: class Article < ApplicationRecord
+ 2: def self.find_recent(limit = 10)
+ 3: byebug
+=> 4: where('created_at > ?', 1.week.ago).limit(limit)
+ 5: end
+ 6: end
(byebug)
```
-If we use `next`, we want go deep inside method calls. Instead, byebug will go
-to the next line within the same context. In this case, this is the last line of
-the method, so `byebug` will jump to next next line of the previous frame.
+If we use `next`, we won't go deep inside method calls. Instead, `byebug` will
+go to the next line within the same context. In this case, it is the last line
+of the current method, so `byebug` will return to the next line of the caller
+method.
```
(byebug) next
-Next went up a frame because previous frame finished
-
-[4, 13] in /PathTo/project/test_app/app/controllers/articles_controller.rb
+[4, 13] in /PathToProject/app/controllers/articles_controller.rb
4: # GET /articles
5: # GET /articles.json
6: def index
@@ -653,29 +707,30 @@ Next went up a frame because previous frame finished
(byebug)
```
-If we use `step` in the same situation, we will literally go to the next Ruby
-instruction to be executed. In this case, Active Support's `week` method.
+If we use `step` in the same situation, `byebug` will literally go to the next
+Ruby instruction to be executed -- in this case, Active Support's `week` method.
```
(byebug) step
-[50, 59] in /PathToGems/activesupport-5.0.0/lib/active_support/core_ext/numeric/time.rb
- 50: ActiveSupport::Duration.new(self * 24.hours, [[:days, self]])
- 51: end
- 52: alias :day :days
- 53:
- 54: def weeks
-=> 55: ActiveSupport::Duration.new(self * 7.days, [[:days, self * 7]])
- 56: end
- 57: alias :week :weeks
- 58:
- 59: def fortnights
-
+[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.weeks(self)
+ 55: end
+ 56: alias :week :weeks
+ 57:
+ 58: # Returns a Duration instance matching the number of fortnights provided.
(byebug)
```
This is one of the best ways to find bugs in your code.
+TIP: You can also use `step n` or `next n` to move forward `n` steps at once.
+
### Breakpoints
A breakpoint makes your application stop whenever a certain point in the program
@@ -684,19 +739,18 @@ is reached. The debugger shell is invoked in that line.
You can add breakpoints dynamically with the command `break` (or just `b`).
There are 3 possible ways of adding breakpoints manually:
-* `break line`: set breakpoint in the _line_ in the current source file.
-* `break file:line [if expression]`: set breakpoint in the _line_ number inside
-the _file_. If an _expression_ is given it must evaluated to _true_ to fire up
-the debugger.
+* `break n`: set breakpoint in line number _n_ in the current source file.
+* `break file:n [if expression]`: set breakpoint in line number _n_ inside
+file named _file_. If an _expression_ is given it must evaluated to _true_ to
+fire up the debugger.
* `break class(.|\#)method [if expression]`: set breakpoint in _method_ (. and
\# for class and instance method respectively) defined in _class_. The
-_expression_ works the same way as with file:line.
-
+_expression_ works the same way as with file:n.
For example, in the previous situation
```
-[4, 13] in /PathTo/project/app/controllers/articles_controller.rb
+[4, 13] in /PathToProject/app/controllers/articles_controller.rb
4: # GET /articles
5: # GET /articles.json
6: def index
@@ -709,20 +763,20 @@ For example, in the previous situation
13: end
(byebug) break 11
-Created breakpoint 1 at /PathTo/project/app/controllers/articles_controller.rb:11
+Successfully created breakpoint with id 1
```
-Use `info breakpoints _n_` or `info break _n_` to list breakpoints. If you
-supply a number, it lists that breakpoint. Otherwise it lists all breakpoints.
+Use `info breakpoints` to list breakpoints. If you supply a number, it lists
+that breakpoint. Otherwise it lists all breakpoints.
```
(byebug) info breakpoints
Num Enb What
-1 y at /PathTo/project/app/controllers/articles_controller.rb:11
+1 y at /PathToProject/app/controllers/articles_controller.rb:11
```
-To delete breakpoints: use the command `delete _n_` to remove the breakpoint
+To delete breakpoints: use the command `delete n` to remove the breakpoint
number _n_. If no number is specified, it deletes all breakpoints that are
currently active.
@@ -734,10 +788,11 @@ No breakpoints.
You can also enable or disable breakpoints:
-* `enable breakpoints`: allow a _breakpoints_ list or all of them if no list is
-specified, to stop your program. This is the default state when you create a
+* `enable breakpoints [n [m [...]]]`: allows a specific breakpoint list or all
+breakpoints to stop your program. This is the default state when you create a
breakpoint.
-* `disable breakpoints`: the _breakpoints_ will have no effect on your program.
+* `disable breakpoints [n [m [...]]]`: make certain (or all) breakpoints have
+no effect on your program.
### Catching Exceptions
@@ -752,29 +807,27 @@ To list all active catchpoints use `catch`.
There are two ways to resume execution of an application that is stopped in the
debugger:
-* `continue` [line-specification] \(or `c`): resume program execution, at the
-address where your script last stopped; any breakpoints set at that address are
-bypassed. The optional argument line-specification allows you to specify a line
-number to set a one-time breakpoint which is deleted when that breakpoint is
-reached.
-* `finish` [frame-number] \(or `fin`): execute until the selected stack frame
-returns. If no frame number is given, the application will run until the
-currently selected frame returns. The currently selected frame starts out the
-most-recent frame or 0 if no frame positioning (e.g up, down or frame) has been
-performed. If a frame number is given it will run until the specified frame
-returns.
+* `continue [n]`: resumes program execution at the address where your script last
+stopped; any breakpoints set at that address are bypassed. The optional argument
+`n` allows you to specify a line number to set a one-time breakpoint which is
+deleted when that breakpoint is reached.
+* `finish [n]`: execute until the selected stack frame returns. If no frame
+number is given, the application will run until the currently selected frame
+returns. The currently selected frame starts out the most-recent frame or 0 if
+no frame positioning (e.g up, down, or frame) has been performed. If a frame
+number is given it will run until the specified frame returns.
### Editing
Two commands allow you to open code from the debugger into an editor:
-* `edit [file:line]`: edit _file_ using the editor specified by the EDITOR
-environment variable. A specific _line_ can also be given.
+* `edit [file:n]`: edit file named _file_ using the editor specified by the
+EDITOR environment variable. A specific line _n_ can also be given.
### Quitting
-To exit the debugger, use the `quit` command (abbreviated `q`), or its alias
-`exit`.
+To exit the debugger, use the `quit` command (abbreviated to `q`). Or, type `q!`
+to bypass the `Really quit? (y/n)` prompt and exit unconditionally.
A simple quit tries to terminate all threads in effect. Therefore your server
will be stopped and you will have to start it again.
@@ -783,21 +836,43 @@ will be stopped and you will have to start it again.
`byebug` has a few available options to tweak its behavior:
-* `set autoreload`: Reload source code when changed (defaults: true).
-* `set autolist`: Execute `list` command on every breakpoint (defaults: true).
-* `set listsize _n_`: Set number of source lines to list by default to _n_
-(defaults: 10)
-* `set forcestep`: Make sure the `next` and `step` commands always move to a new
-line.
+```
+(byebug) help set
+
+ set <setting> <value>
+
+ Modifies byebug settings
+
+ Boolean values take "on", "off", "true", "false", "1" or "0". If you
+ don't specify a value, the boolean setting will be enabled. Conversely,
+ you can use "set no<setting>" to disable them.
-You can see the full list by using `help set`. Use `help set _subcommand_` to
-learn about a particular `set` command.
+ You can see these environment settings with the "show" command.
+
+ List of supported settings:
+
+ autosave -- Automatically save command history record on exit
+ autolist -- Invoke list command on every stop
+ width -- Number of characters per line in byebug's output
+ autoirb -- Invoke IRB on every stop
+ basename -- <file>:<line> information after every stop uses short paths
+ linetrace -- Enable line execution tracing
+ autopry -- Invoke Pry on every stop
+ stack_on_error -- Display stack trace when `eval` raises an exception
+ fullpath -- Display full file names in backtraces
+ histfile -- File where cmd history is saved to. Default: ./.byebug_history
+ listsize -- Set number of source lines to list by default
+ post_mortem -- Enable/disable post-mortem mode
+ callstyle -- Set how you want method call parameters to be displayed
+ histsize -- Maximum number of commands that can be stored in byebug history
+ savefile -- File where settings are saved to. Default: ~/.byebug_save
+```
TIP: You can save these settings in an `.byebugrc` file in your home directory.
The debugger reads these global settings when it starts. For example:
```bash
-set forcestep
+set callstyle short
set listsize 25
```
@@ -837,7 +912,7 @@ location of the `console` call; it won't be rendered on the spot of its
invocation but next to your HTML content.
The console executes pure Ruby code: You can define and instantiate
-custom classes, create new models and inspect variables.
+custom classes, create new models, and inspect variables.
NOTE: Only one console can be rendered per request. Otherwise `web-console`
will raise an error on the second `console` invocation.
@@ -869,8 +944,8 @@ such as Valgrind.
### Valgrind
-[Valgrind](http://valgrind.org/) is a Linux-only application for detecting
-C-based memory leaks and race conditions.
+[Valgrind](http://valgrind.org/) is an application for detecting C-based memory
+leaks and race conditions.
There are Valgrind tools that can automatically detect many memory management
and threading bugs, and profile your programs in detail. For example, if a C
@@ -907,16 +982,10 @@ development that will end your tailing of development.log. Have all information
about your Rails app requests in the browser — in the Developer Tools panel.
Provides insight to db/rendering/total times, parameter list, rendered views and
more.
+* [Pry](https://github.com/pry/pry) An IRB alternative and runtime developer console.
References
----------
-* [ruby-debug Homepage](http://bashdb.sourceforge.net/ruby-debug/home-page.html)
-* [debugger Homepage](https://github.com/cldwalker/debugger)
* [byebug Homepage](https://github.com/deivid-rodriguez/byebug)
* [web-console Homepage](https://github.com/rails/web-console)
-* [Article: Debugging a Rails application with ruby-debug](http://www.sitepoint.com/debug-rails-app-ruby-debug/)
-* [Ryan Bates' debugging ruby (revised) screencast](http://railscasts.com/episodes/54-debugging-ruby-revised)
-* [Ryan Bates' stack trace screencast](http://railscasts.com/episodes/24-the-stack-trace)
-* [Ryan Bates' logger screencast](http://railscasts.com/episodes/56-the-logger)
-* [Debugging with ruby-debug](http://bashdb.sourceforge.net/ruby-debug.html)
diff --git a/guides/source/development_dependencies_install.md b/guides/source/development_dependencies_install.md
index 3c670a1221..d52946be08 100644
--- a/guides/source/development_dependencies_install.md
+++ b/guides/source/development_dependencies_install.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Development Dependencies Install
================================
@@ -8,8 +8,6 @@ This guide covers how to setup an environment for Ruby on Rails core development
After reading this guide, you will know:
* How to set up your machine for Rails development
-* How to run specific groups of unit tests from the Rails test suite
-* How the Active Record portion of the Rails test suite operates
--------------------------------------------------------------------------------
@@ -21,271 +19,241 @@ The easiest and recommended way to get a development environment ready to hack i
The Hard Way
------------
-In case you can't use the Rails development box, see section below, these are the steps to manually build a development box for Ruby on Rails core development.
+In case you can't use the Rails development box, see the steps below to manually
+build a development box for Ruby on Rails core development.
### Install Git
-Ruby on Rails uses Git for source code control. The [Git homepage](http://git-scm.com/) has installation instructions. There are a variety of resources on the net that will help you get familiar with Git:
+Ruby on Rails uses Git for source code control. The [Git homepage](https://git-scm.com/) has installation instructions. There are a variety of resources on the net that will help you get familiar with Git:
-* [Try Git course](http://try.github.io/) is an interactive course that will teach you the basics.
-* The [official Documentation](http://git-scm.com/documentation) is pretty comprehensive and also contains some videos with the basics of Git.
-* [Everyday Git](http://schacon.github.io/git/everyday.html) will teach you just enough about Git to get by.
-* The [PeepCode screencast](https://peepcode.com/products/git) on Git is easier to follow.
-* [GitHub](http://help.github.com) offers links to a variety of Git resources.
-* [Pro Git](http://git-scm.com/book) is an entire book about Git with a Creative Commons license.
+* [Try Git course](https://try.github.io/) is an interactive course that will teach you the basics.
+* The [official Documentation](https://git-scm.com/documentation) is pretty comprehensive and also contains some videos with the basics of Git.
+* [Everyday Git](https://schacon.github.io/git/everyday.html) will teach you just enough about Git to get by.
+* [GitHub](https://help.github.com/) offers links to a variety of Git resources.
+* [Pro Git](https://git-scm.com/book) is an entire book about Git with a Creative Commons license.
### Clone the Ruby on Rails Repository
Navigate to the folder where you want the Ruby on Rails source code (it will create its own `rails` subdirectory) and run:
```bash
-$ git clone git://github.com/rails/rails.git
+$ git clone https://github.com/rails/rails.git
$ cd rails
```
-### Set up and Run the Tests
+### Install Additional Tools and Services
-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.
+Some Rails tests depend on additional tools that you need to install before running those specific tests.
-Install first SQLite3 and its development files for the `sqlite3` gem. Mac OS X
-users are done with:
+Here's the list of each gems' additional dependencies:
-```bash
-$ brew install sqlite3
-```
+* Action Cable depends on Redis
+* Active Record depends on SQLite3, MySQL and PostgreSQL
+* Active Storage depends on Yarn (additionally Yarn depends on
+ [Node.js](https://nodejs.org/)), ImageMagick, FFmpeg, muPDF, and on macOS
+ also XQuartz and Poppler.
+* Active Support depends on memcached and Redis
+* Railties depend on a JavaScript runtime environment, such as having
+ [Node.js](https://nodejs.org/) installed.
-In Ubuntu you're done with just:
+Install all the services you need to properly test the full gem you'll be
+making changes to.
-```bash
-$ sudo apt-get install sqlite3 libsqlite3-dev
-```
+NOTE: Redis' documentation discourage installations with package managers as those are usually outdated. Installing from source and bringing the server up is straight forward and well documented on [Redis' documentation](https://redis.io/download#installation).
-If you are on Fedora or CentOS, you're done with
+NOTE: Active Record tests _must_ pass for at least MySQL, PostgreSQL, and SQLite3. Subtle differences between the various adapters have been behind the rejection of many patches that looked OK when tested only against single adapter.
-```bash
-$ sudo yum install sqlite3 sqlite3-devel
-```
+Below you can find instructions on how to install all of the additional
+tools for different OSes.
-If you are on Arch Linux, you will need to run:
+#### macOS
-```bash
-$ sudo pacman -S sqlite
-```
+On macOS you can use [Homebrew](https://brew.sh/) to install all of the
+additional tools.
-For FreeBSD users, you're done with:
+To install all run:
```bash
-# pkg install sqlite3
+$ brew bundle
```
-Or compile the `databases/sqlite3` port.
-
-Get a recent version of [Bundler](http://bundler.io/)
+You'll also need to start each of the installed services. To list all
+available services run:
```bash
-$ gem install bundler
-$ gem update bundler
+$ brew services list
```
-and run:
+You can then start each of the services one by one like this:
```bash
-$ bundle install --without db
+$ brew services start mysql
```
-This command will install all dependencies except the MySQL and PostgreSQL Ruby drivers. We will come back to these soon.
+Replace `mysql` with the name of the service you want to start.
-NOTE: If you would like to run the tests that use memcached, you need to ensure that you have it installed and running.
+#### Ubuntu
-You can use [Homebrew](http://brew.sh/) to install memcached on OS X:
+To install all run:
```bash
-$ brew install memcached
-```
-
-On Ubuntu you can install it with apt-get:
-
-```bash
-$ sudo apt-get install memcached
-```
-
-Or use yum on Fedora or CentOS:
-
-```bash
-$ sudo yum install memcached
-```
-
-If you are running on Arch Linux:
-
-```bash
-$ sudo pacman -S memcached
-```
-
-For FreeBSD users, you're done with:
-
-```bash
-# pkg install memcached
+$ sudo apt-get update
+$ sudo apt-get install sqlite3 libsqlite3-dev
+ mysql-server libmysqlclient-dev
+ postgresql postgresql-client postgresql-contrib libpq-dev
+ redis-server memcached imagemagick ffmpeg mupdf mupdf-tools
+
+# Install Yarn
+$ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
+$ echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
+$ sudo apt-get install yarn
```
-Alternatively, you can compile the `databases/memcached` port.
+#### Fedora or CentOS
-With the dependencies now installed, you can run the test suite with:
+To install all run:
```bash
-$ bundle exec rake test
+$ sudo dnf install sqlite-devel sqlite-libs
+ mysql-server mysql-devel
+ postgresql-server postgresql-devel
+ redis memcached imagemagick ffmpeg mupdf
+
+# Install Yarn
+# Use this command if you do not have Node.js installed
+$ curl --silent --location https://rpm.nodesource.com/setup_8.x | sudo bash -
+# If you have Node.js installed, use this command instead
+$ curl --silent --location https://dl.yarnpkg.com/rpm/yarn.repo | sudo tee /etc/yum.repos.d/yarn.repo
+$ sudo dnf install yarn
```
-You can also run tests for a specific component, like Action Pack, by going into its directory and executing the same command:
+#### Arch Linux
-```bash
-$ cd actionpack
-$ bundle exec rake test
-```
-
-If you want to run the tests located in a specific directory use the `TEST_DIR` environment variable. For example, this will run the tests in the `railties/test/generators` directory only:
+To install all run:
```bash
-$ cd railties
-$ TEST_DIR=generators bundle exec rake test
+$ sudo pacman -S sqlite
+ mariadb libmariadbclient mariadb-clients
+ postgresql postgresql-libs
+ redis memcached imagemagick ffmpeg mupdf mupdf-tools poppler
+ yarn
+$ sudo systemctl start redis
```
-You can run the tests for a particular file by using:
+NOTE: If you are running Arch Linux, MySQL isn't supported anymore so you will need to
+use MariaDB instead (see [this announcement](https://www.archlinux.org/news/mariadb-replaces-mysql-in-repositories/)).
-```bash
-$ cd actionpack
-$ bundle exec ruby -Itest test/template/form_helper_test.rb
-```
+#### FreeBSD
-Or, you can run a single test in a particular file:
+To install all run:
```bash
-$ cd actionpack
-$ bundle exec ruby -Itest path/to/test.rb -n test_name
+# pkg install sqlite3
+ mysql80-client mysql80-server
+ postgresql11-client postgresql11-server
+ memcached imagemagick ffmpeg mupdf
+ yarn
+# portmaster databases/redis
```
-### Active Record Setup
+Or install everyting through ports (these packages are located under the
+`databases` folder).
-The test suite of Active Record attempts to run four times: once for SQLite3, once for each of the two MySQL gems (`mysql` and `mysql2`), and once for PostgreSQL. We are going to see now how to set up the environment for them.
+NOTE: If you run into troubles during the installation of MySQL, please see
+[the MySQL documentation](https://dev.mysql.com/doc/refman/8.0/en/freebsd-installation.html).
-WARNING: If you're working with Active Record code, you _must_ ensure that the tests pass for at least MySQL, PostgreSQL, and SQLite3. Subtle differences between the various adapters have been behind the rejection of many patches that looked OK when tested only against MySQL.
+### Database Configuration
-#### Database Configuration
+There are couple of additional steps required to configure database engines
+required for running Active Record tests.
-The Active Record test suite requires a custom config file: `activerecord/test/config.yml`. An example is provided in `activerecord/test/config.example.yml` which can be copied and used as needed for your environment.
-
-#### MySQL and PostgreSQL
-
-To be able to run the suite for MySQL and PostgreSQL we need their gems. Install
-first the servers, their client libraries, and their development files.
-
-On OS X, you can run:
-
-```bash
-$ brew install mysql
-$ brew install postgresql
-```
-
-Follow the instructions given by Homebrew to start these.
-
-In Ubuntu just run:
+In order to be able to run the test suite against MySQL you need to create a user named `rails` with privileges on the test databases:
```bash
-$ sudo apt-get install mysql-server libmysqlclient15-dev
-$ sudo apt-get install postgresql postgresql-client postgresql-contrib libpq-dev
-```
-
-On Fedora or CentOS, just run:
+$ mysql -uroot -p
-```bash
-$ sudo yum install mysql-server mysql-devel
-$ sudo yum install postgresql-server postgresql-devel
+mysql> CREATE USER 'rails'@'localhost';
+mysql> GRANT ALL PRIVILEGES ON activerecord_unittest.*
+ to 'rails'@'localhost';
+mysql> GRANT ALL PRIVILEGES ON activerecord_unittest2.*
+ to 'rails'@'localhost';
+mysql> GRANT ALL PRIVILEGES ON inexistent_activerecord_unittest.*
+ to 'rails'@'localhost';
```
-If you are running Arch Linux, MySQL isn't supported anymore so you will need to
-use MariaDB instead (see [this announcement](https://www.archlinux.org/news/mariadb-replaces-mysql-in-repositories/)):
+PostgreSQL's authentication works differently. To setup the development environment
+with your development account, on Linux or BSD, you just have to run:
```bash
-$ sudo pacman -S mariadb libmariadbclient mariadb-clients
-$ sudo pacman -S postgresql postgresql-libs
+$ sudo -u postgres createuser --superuser $USER
```
-FreeBSD users will have to run the following:
+and for macOS:
```bash
-# pkg install mysql56-client mysql56-server
-# pkg install postgresql94-client postgresql94-server
+$ createuser --superuser $USER
```
-Or install them through ports (they are located under the `databases` folder).
-If you run into troubles during the installation of MySQL, please see
-[the MySQL documentation](http://dev.mysql.com/doc/refman/5.1/en/freebsd-installation.html).
-
-After that, run:
+Then, you need to create the test databases for both MySQL and PostgreSQL with:
```bash
-$ rm .bundle/config
-$ bundle install
+$ cd activerecord
+$ bundle exec rake db:create
```
-First, we need to delete `.bundle/config` because Bundler remembers in that file that we didn't want to install the "db" group (alternatively you can edit the file).
+NOTE: You'll see the following warning (or localized warning) during activating HStore extension in PostgreSQL 9.1.x or earlier: "WARNING: => is deprecated as an operator".
-In order to be able to run the test suite against MySQL you need to create a user named `rails` with privileges on the test databases:
+You can also create test databases for each database engine separately:
```bash
-$ mysql -uroot -p
-
-mysql> CREATE USER 'rails'@'localhost';
-mysql> GRANT ALL PRIVILEGES ON activerecord_unittest.*
- to 'rails'@'localhost';
-mysql> GRANT ALL PRIVILEGES ON activerecord_unittest2.*
- to 'rails'@'localhost';
-mysql> GRANT ALL PRIVILEGES ON inexistent_activerecord_unittest.*
- to 'rails'@'localhost';
+$ cd activerecord
+$ bundle exec rake db:mysql:build
+$ bundle exec rake db:postgresql:build
```
-and create the test databases:
+and you can drop the databases using:
```bash
$ cd activerecord
-$ bundle exec rake db:mysql:build
+$ bundle exec rake db:drop
```
-PostgreSQL's authentication works differently. To setup the development environment
-with your development account, on Linux or BSD, you just have to run:
+NOTE: Using the Rake task to create the test databases ensures they have the correct character set and collation.
-```bash
-$ sudo -u postgres createuser --superuser $USER
-```
+If you're using another database, check the file `activerecord/test/config.yml` or `activerecord/test/config.example.yml` for default connection information. You can edit `activerecord/test/config.yml` to provide different credentials on your machine if you must, but obviously you should not push any such changes back to Rails.
-and for OS X:
+### Install JavaScript dependencies
+
+If you installed Yarn, you will need to install the javascript dependencies:
```bash
-$ createuser --superuser $USER
+$ cd activestorage
+$ yarn install
```
-Then you need to create the test databases with
+### Install Bundler gem
+
+Get a recent version of [Bundler](https://bundler.io/)
```bash
-$ cd activerecord
-$ bundle exec rake db:postgresql:build
+$ gem install bundler
+$ gem update bundler
```
-It is possible to build databases for both PostgreSQL and MySQL with
+and run:
```bash
-$ cd activerecord
-$ bundle exec rake db:create
+$ bundle install
```
-You can cleanup the databases using
+or:
```bash
-$ cd activerecord
-$ bundle exec rake db:drop
+$ bundle install --without db
```
-NOTE: Using the rake task to create the test databases ensures they have the correct character set and collation.
+if you don't need to run Active Record tests.
-NOTE: You'll see the following warning (or localized warning) during activating HStore extension in PostgreSQL 9.1.x or earlier: "WARNING: => is deprecated as an operator".
+### Contribute to Rails
-If you're using another database, check the file `activerecord/test/config.yml` or `activerecord/test/config.example.yml` for default connection information. You can edit `activerecord/test/config.yml` to provide different credentials on your machine if you must, but obviously you should not push any such changes back to Rails.
+After you've setup everything, read how you can start [contributing](contributing_to_ruby_on_rails.html#running-an-application-against-your-local-branch).
diff --git a/guides/source/documents.yaml b/guides/source/documents.yaml
index 9145aee009..25c159d471 100644
--- a/guides/source/documents.yaml
+++ b/guides/source/documents.yaml
@@ -65,17 +65,13 @@
url: routing.html
description: This guide covers the user-facing features of Rails routing. If you want to understand how to use routing in your own Rails applications, start here.
-
- name: Digging Deeper
+ name: Other Components
documents:
-
name: Active Support Core Extensions
url: active_support_core_extensions.html
description: This guide documents the Ruby core extensions defined in Active Support.
-
- name: Rails Internationalization API
- url: i18n.html
- description: This guide covers how to add internationalization to your applications. Your application will be able to translate content to different languages, change pluralization rules, use correct date formats for each country, and so on.
- -
name: Action Mailer Basics
url: action_mailer_basics.html
description: This guide describes how to use Action Mailer to send and receive emails.
@@ -84,10 +80,25 @@
url: active_job_basics.html
description: This guide provides you with all you need to get started creating, enqueuing, and executing background jobs.
-
+ name: Active Storage Overview
+ url: active_storage_overview.html
+ description: This guide covers how to attach files to your Active Record models.
+ -
+ name: Action Cable Overview
+ url: action_cable_overview.html
+ description: This guide explains how Action Cable works, and how to use WebSockets to create real-time features.
+
+-
+ name: Digging Deeper
+ documents:
+ -
+ name: Rails Internationalization (I18n) API
+ url: i18n.html
+ description: This guide covers how to add internationalization to your applications. Your application will be able to translate content to different languages, change pluralization rules, use correct date formats for each country, and so on.
+ -
name: Testing Rails Applications
- work_in_progress: true
url: testing.html
- description: This is a rather comprehensive guide to the various testing facilities in Rails. It covers everything from 'What is a test?' to the testing APIs. Enjoy.
+ description: This is a rather comprehensive guide to the various testing facilities in Rails. It covers everything from 'What is a test?' to Integration Testing. Enjoy.
-
name: Securing Rails Applications
url: security.html
@@ -101,11 +112,11 @@
url: configuring.html
description: This guide covers the basic configuration settings for a Rails application.
-
- name: Rails Command Line Tools and Rake Tasks
+ name: The Rails Command Line
url: command_line.html
- description: This guide covers the command line tools and rake tasks provided by Rails.
+ description: This guide covers the command line tools provided by Rails.
-
- name: Asset Pipeline
+ name: The Asset Pipeline
url: asset_pipeline.html
description: This guide documents the asset pipeline.
-
@@ -116,21 +127,24 @@
name: The Rails Initialization Process
work_in_progress: true
url: initialization.html
- description: This guide explains the internals of the Rails initialization process as of Rails 4.
+ description: This guide explains the internals of the Rails initialization process.
-
name: Autoloading and Reloading Constants
url: autoloading_and_reloading_constants.html
description: This guide documents how autoloading and reloading constants work.
-
+ name: "Caching with Rails: An Overview"
+ url: caching_with_rails.html
+ description: This guide is an introduction to speeding up your Rails application with caching.
+ -
name: Active Support Instrumentation
work_in_progress: true
url: active_support_instrumentation.html
description: This guide explains how to use the instrumentation API inside of Active Support to measure events inside of Rails and other Ruby code.
-
- name: Profiling Rails Applications
- work_in_progress: true
- url: profiling.html
- description: This guide explains how to profile your Rails applications to improve performance.
+ name: Using Rails for API-only Applications
+ url: api_app.html
+ description: This guide explains how to effectively use Rails to develop a JSON API application.
-
name: Extending Rails
@@ -145,7 +159,7 @@
url: rails_on_rack.html
description: This guide covers Rails integration with Rack and interfacing with other Rack components.
-
- name: Creating and Customizing Rails Generators
+ name: Creating and Customizing Rails Generators & Templates
url: generators.html
description: This guide covers the process of adding a brand new generator to your extension or providing an alternative to an element of a built-in Rails generator (such as providing alternative test stubs for the scaffold generator).
-
@@ -153,8 +167,13 @@
url: engines.html
description: This guide explains how to write a mountable engine.
work_in_progress: true
+ -
+ name: Threading and Code Execution in Rails
+ url: threading_and_code_execution.html
+ description: This guide describes the considerations needed and tools available when working directly with concurrency in a Rails application.
+ work_in_progress: true
-
- name: Contributing to Ruby on Rails
+ name: Contributions
documents:
-
name: Contributing to Ruby on Rails
@@ -165,11 +184,11 @@
url: api_documentation_guidelines.html
description: This guide documents the Ruby on Rails API documentation guidelines.
-
- name: Ruby on Rails Guides Guidelines
+ name: Guides Guidelines
url: ruby_on_rails_guides_guidelines.html
description: This guide documents the Ruby on Rails guides guidelines.
-
- name: Maintenance Policy
+ name: Policies
documents:
-
name: Maintenance Policy
@@ -183,34 +202,51 @@
url: upgrading_ruby_on_rails.html
description: This guide helps in upgrading applications to latest Ruby on Rails versions.
-
- name: Ruby on Rails 4.2 Release Notes
+ name: 6.0 Release Notes
+ work_in_progress: true
+ url: 6_0_release_notes.html
+ description: Release notes for Rails 6.0.
+ -
+ name: Version 5.2 - April 2018
+ url: 5_2_release_notes.html
+ description: Release notes for Rails 5.2.
+ -
+ name: Version 5.1 - April 2017
+ url: 5_1_release_notes.html
+ description: Release notes for Rails 5.1.
+ -
+ name: Version 5.0 - June 2016
+ url: 5_0_release_notes.html
+ description: Release notes for Rails 5.0.
+ -
+ name: Version 4.2 - December 2014
url: 4_2_release_notes.html
description: Release notes for Rails 4.2.
-
- name: Ruby on Rails 4.1 Release Notes
+ name: Version 4.1 - April 2014
url: 4_1_release_notes.html
description: Release notes for Rails 4.1.
-
- name: Ruby on Rails 4.0 Release Notes
+ name: Version 4.0 - June 2013
url: 4_0_release_notes.html
description: Release notes for Rails 4.0.
-
- name: Ruby on Rails 3.2 Release Notes
+ name: Version 3.2 - January 2012
url: 3_2_release_notes.html
description: Release notes for Rails 3.2.
-
- name: Ruby on Rails 3.1 Release Notes
+ name: Version 3.1 - August 2011
url: 3_1_release_notes.html
description: Release notes for Rails 3.1.
-
- name: Ruby on Rails 3.0 Release Notes
+ name: Version 3.0 - August 2010
url: 3_0_release_notes.html
description: Release notes for Rails 3.0.
-
- name: Ruby on Rails 2.3 Release Notes
+ name: Version 2.3 - March 2009
url: 2_3_release_notes.html
description: Release notes for Rails 2.3.
-
- name: Ruby on Rails 2.2 Release Notes
+ name: Version 2.2 - November 2008
url: 2_2_release_notes.html
description: Release notes for Rails 2.2.
diff --git a/guides/source/engines.md b/guides/source/engines.md
index 3b1588b75a..1e93a19c84 100644
--- a/guides/source/engines.md
+++ b/guides/source/engines.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Getting Started with Engines
============================
@@ -11,9 +11,10 @@ After reading this guide, you will know:
* What makes an engine.
* How to generate an engine.
-* Building features for the engine.
-* Hooking the engine into an application.
-* Overriding engine functionality in the application.
+* How to build features for the engine.
+* How to hook the engine into an application.
+* How to override engine functionality in the application.
+* Avoid loading Rails frameworks with Load and Configuration Hooks
--------------------------------------------------------------------------------
@@ -25,7 +26,7 @@ their host applications. A Rails application is actually just a "supercharged"
engine, with the `Rails::Application` class inheriting a lot of its behavior
from `Rails::Engine`.
-Therefore, engines and applications can be thought of almost the same thing,
+Therefore, engines and applications can be thought of as almost the same thing,
just with subtle differences, as you'll see throughout this guide. Engines and
applications also share a common structure.
@@ -46,7 +47,7 @@ see how to hook it into an application.
Engines can also be isolated from their host applications. This means that an
application is able to have a path provided by a routing helper such as
-`articles_path` and use an engine also that provides a path also called
+`articles_path` and use an engine that also provides a path also called
`articles_path`, and the two would not clash. Along with this, controllers, models
and table names are also namespaced. You'll see how to do this later in this
guide.
@@ -59,10 +60,10 @@ 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.
+[Refinery CMS](https://github.com/refinery/refinerycms), a CMS engine.
Finally, engines would not have been possible without the work of James Adam,
Piotr Sarnacki, the Rails Core Team, and a number of other people. If you ever
@@ -150,7 +151,7 @@ When you include the engine into an application later on, you will do so with
this line in the Rails application's `Gemfile`:
```ruby
-gem 'blorgh', path: "vendor/engines/blorgh"
+gem 'blorgh', path: 'engines/blorgh'
```
Don't forget to run `bundle install` as usual. By specifying it as a gem within
@@ -184,10 +185,10 @@ end
By inheriting from the `Rails::Engine` class, this gem notifies Rails that
there's an engine at the specified path, and will correctly mount the engine
inside the application, performing tasks such as adding the `app` directory of
-the engine to the load path for models, mailers, controllers and views.
+the engine to the load path for models, mailers, controllers, and views.
The `isolate_namespace` method here deserves special notice. This call is
-responsible for isolating the controllers, models, routes and other things into
+responsible for isolating the controllers, models, routes, and other things into
their own namespace, away from similar components inside the application.
Without this, there is a possibility that the engine's components could "leak"
into the application, causing unwanted disruption, or that important engine
@@ -201,7 +202,7 @@ within the `Engine` class definition. Without it, classes generated in an engine
**may** conflict with an application.
What this isolation of the namespace means is that a model generated by a call
-to `bin/rails g model`, such as `bin/rails g model article`, won't be called `Article`, but
+to `rails g model`, such as `rails g model article`, won't be called `Article`, but
instead be namespaced and called `Blorgh::Article`. In addition, the table for the
model is namespaced, becoming `blorgh_articles`, rather than simply `articles`.
Similar to the model namespacing, a controller called `ArticlesController` becomes
@@ -239,6 +240,27 @@ NOTE: The `ApplicationController` class inside an engine is named just like a
Rails application in order to make it easier for you to convert your
applications into engines.
+NOTE: Because of the way that Ruby does constant lookup you may run into a situation
+where your engine controller is inheriting from the main application controller and
+not your engine's application controller. Ruby is able to resolve the `ApplicationController` constant, and therefore the autoloading mechanism is not triggered. See the section [When Constants Aren't Missed](autoloading_and_reloading_constants.html#when-constants-aren-t-missed) of the [Autoloading and Reloading Constants](autoloading_and_reloading_constants.html) guide for further details. The best way to prevent this from
+happening is to use `require_dependency` to ensure that the engine's application
+controller is loaded. For example:
+
+``` ruby
+# app/controllers/blorgh/articles_controller.rb:
+require_dependency "blorgh/application_controller"
+
+module Blorgh
+ class ArticlesController < ApplicationController
+ ...
+ end
+end
+```
+
+WARNING: Don't use `require` because it will break the automatic reloading of classes
+in the development environment - using `require_dependency` ensures that classes are
+loaded and unloaded in the correct manner.
+
Lastly, the `app/views` directory contains a `layouts` folder, which contains a
file at `blorgh/application.html.erb`. This file allows you to specify a layout
for the engine. If this engine is to be used as a stand-alone engine, then you
@@ -291,13 +313,16 @@ The engine that this guide covers provides submitting articles and commenting
functionality and follows a similar thread to the [Getting Started
Guide](getting_started.html), with some new twists.
+NOTE: For this section, make sure to run the commands in the root of the
+`blorgh` engine's directory.
+
### Generating an Article Resource
The first thing to generate for a blog engine is the `Article` model and related
controller. To quickly generate this, you can use the Rails scaffold generator.
```bash
-$ bin/rails generate scaffold article title:string text:text
+$ rails generate scaffold article title:string text:text
```
This command will output this information:
@@ -324,6 +349,9 @@ invoke test_unit
create test/controllers/blorgh/articles_controller_test.rb
invoke helper
create app/helpers/blorgh/articles_helper.rb
+invoke test_unit
+create test/application_system_test_case.rb
+create test/system/articles_test.rb
invoke assets
invoke js
create app/assets/javascripts/blorgh/articles.js
@@ -381,8 +409,8 @@ module Blorgh
end
```
-NOTE: The `ApplicationController` class being inherited from here is the
-`Blorgh::ApplicationController`, not an application's `ApplicationController`.
+NOTE: The `ArticlesController` class inherits from
+`Blorgh::ApplicationController`, not the application's `ApplicationController`.
The helper inside `app/helpers/blorgh/articles_helper.rb` is also namespaced:
@@ -402,7 +430,7 @@ Finally, the assets for this resource are generated in two files:
`app/assets/stylesheets/blorgh/articles.css`. You'll see how to use these a little
later.
-You can see what the engine has so far by running `rake db:migrate` at the root
+You can see what the engine has so far by running `rails db:migrate` at the root
of our engine to run the migration generated by the scaffold generator, and then
running `rails server` in `test/dummy`. When you open
`http://localhost:3000/blorgh/articles` you will see the default scaffold that has
@@ -436,15 +464,15 @@ rather than visiting `/articles`. This means that instead of
Now that the engine can create new articles, it only makes sense to add
commenting functionality as well. To do this, you'll need to generate a comment
-model, a comment controller and then modify the articles scaffold to display
+model, a comment controller, and then modify the articles scaffold to display
comments and allow people to create new ones.
From the application root, run the model generator. Tell it to generate a
-`Comment` model, with the related table having two columns: a `article_id` integer
+`Comment` model, with the related table having two columns: an `article_id` integer
and `text` text column.
```bash
-$ bin/rails generate model Comment article_id:integer text:text
+$ rails generate model Comment article_id:integer text:text
```
This will output the following:
@@ -464,7 +492,7 @@ called `Blorgh::Comment`. Now run the migration to create our blorgh_comments
table:
```bash
-$ rake db:migrate
+$ rails db:migrate
```
To show the comments on an article, edit `app/views/blorgh/articles/show.html.erb` and
@@ -487,7 +515,7 @@ Turning the model into this:
```ruby
module Blorgh
- class Article < ActiveRecord::Base
+ class Article < ApplicationRecord
has_many :comments
end
end
@@ -512,12 +540,12 @@ directory at `app/views/blorgh/comments` and in it a new file called
```html+erb
<h3>New comment</h3>
-<%= form_for [@article, @article.comments.build] do |f| %>
+<%= form_with(model: [@article, @article.comments.build], local: true) do |form| %>
<p>
- <%= f.label :text %><br>
- <%= f.text_area :text %>
+ <%= form.label :text %><br>
+ <%= form.text_area :text %>
</p>
- <%= f.submit %>
+ <%= form.submit %>
<% end %>
```
@@ -538,7 +566,7 @@ The route now exists, but the controller that this route goes to does not. To
create it, run this command from the application root:
```bash
-$ bin/rails g controller comments
+$ rails g controller comments
```
This will generate the following things:
@@ -628,7 +656,7 @@ there isn't an application handy to test this out in, generate one using the
$ rails new unicorn
```
-Usually, specifying the engine inside the Gemfile would be done by specifying it
+Usually, specifying the engine inside the `Gemfile` would be done by specifying it
as a normal, everyday gem.
```ruby
@@ -639,7 +667,7 @@ However, because you are developing the `blorgh` engine on your local machine,
you will need to specify the `:path` option in your `Gemfile`:
```ruby
-gem 'blorgh', path: "/path/to/blorgh"
+gem 'blorgh', path: 'engines/blorgh'
```
Then run `bundle` to install the gem.
@@ -670,17 +698,17 @@ pre-defined path which may be customizable.
The engine contains migrations for the `blorgh_articles` and `blorgh_comments`
table which need to be created in the application's database so that the
engine's models can query them correctly. To copy these migrations into the
-application use this command:
+application run the following command from the application's root:
```bash
-$ rake blorgh:install:migrations
+$ rails blorgh:install:migrations
```
If you have multiple engines that need migrations copied over, use
`railties:install:migrations` instead:
```bash
-$ rake railties:install:migrations
+$ rails railties:install:migrations
```
This command, when run for the first time, will copy over all the migrations
@@ -698,7 +726,7 @@ timestamp (`[timestamp_2]`) will be the current time plus a second. The reason
for this is so that the migrations for the engine are run after any existing
migrations in the application.
-To run these migrations within the context of the application, simply run `rake
+To run these migrations within the context of the application, simply run `rails
db:migrate`. When accessing the engine through `http://localhost:3000/blog`, the
articles will be empty. This is because the table created inside the application is
different from the one created within the engine. Go ahead, play around with the
@@ -709,14 +737,14 @@ If you would like to run migrations only from one engine, you can do it by
specifying `SCOPE`:
```bash
-rake db:migrate SCOPE=blorgh
+rails db:migrate SCOPE=blorgh
```
This may be useful if you want to revert engine's migrations before removing it.
To revert all migrations from blorgh engine you can run code such as:
```bash
-rake db:migrate SCOPE=blorgh VERSION=0
+rails db:migrate SCOPE=blorgh VERSION=0
```
### Using a Class Provided by the Application
@@ -743,7 +771,7 @@ application:
rails g model user name:string
```
-The `rake db:migrate` command needs to be run here to ensure that our
+The `rails db:migrate` command needs to be run here to ensure that our
application has the `users` table for future use.
Also, to keep it simple, the articles form will have a new text field called
@@ -758,8 +786,8 @@ added above the `title` field with this code:
```html+erb
<div class="field">
- <%= f.label :author_name %><br>
- <%= f.text_field :author_name %>
+ <%= form.label :author_name %><br>
+ <%= form.text_field :author_name %>
</div>
```
@@ -778,7 +806,7 @@ before the article is saved. It will also need to have an `attr_accessor` set up
for this field, so that the setter and getter methods are defined for it.
To do all this, you'll need to add the `attr_accessor` for `author_name`, the
-association for the author and the `before_save` call into
+association for the author and the `before_validation` call into
`app/models/blorgh/article.rb`. The `author` association will be hard-coded to the
`User` class for the time being.
@@ -786,7 +814,7 @@ association for the author and the `before_save` call into
attr_accessor :author_name
belongs_to :author, class_name: "User"
-before_save :set_author
+before_validation :set_author
private
def set_author
@@ -803,7 +831,7 @@ of associating the records in the `blorgh_articles` table with the records in th
To generate this new column, run this command within the engine:
```bash
-$ bin/rails g migration add_author_id_to_blorgh_articles author_id:integer
+$ rails g migration add_author_id_to_blorgh_articles author_id:integer
```
NOTE: Due to the migration's name and the column specification after it, Rails
@@ -815,7 +843,7 @@ This migration will need to be run on the application. To do that, it must first
be copied using this command:
```bash
-$ rake blorgh:install:migrations
+$ rails blorgh:install:migrations
```
Notice that only _one_ migration was copied over here. This is because the first
@@ -830,7 +858,7 @@ Copied migration [timestamp]_add_author_id_to_blorgh_articles.blorgh.rb from blo
Run the migration using:
```bash
-$ rake db:migrate
+$ rails db:migrate
```
Now with all the pieces in place, an action will take place that will associate
@@ -843,28 +871,10 @@ above the "Title" output inside `app/views/blorgh/articles/show.html.erb`:
```html+erb
<p>
<b>Author:</b>
- <%= @article.author %>
+ <%= @article.author.name %>
</p>
```
-By outputting `@article.author` using the `<%=` tag, the `to_s` method will be
-called on the object. By default, this will look quite ugly:
-
-```
-#<User:0x00000100ccb3b0>
-```
-
-This is undesirable. It would be much better to have the user's name there. To
-do this, add a `to_s` method to the `User` class within the application:
-
-```ruby
-def to_s
- name
-end
-```
-
-Now instead of the ugly Ruby object output, the author's name will be displayed.
-
#### Using a Controller Provided by the Application
Because Rails controllers generally share code for things like authentication
@@ -914,7 +924,7 @@ engine:
mattr_accessor :author_class
```
-This method works like its brothers, `attr_accessor` and `cattr_accessor`, but
+This method works like its siblings, `attr_accessor` and `cattr_accessor`, but
provides a setter and getter method on the module with the specified name. To
use it, it must be referenced using `Blorgh.author_class`.
@@ -975,7 +985,7 @@ Blorgh.author_class = "User"
WARNING: It's very important here to use the `String` version of the class,
rather than the class itself. If you were to use the class, Rails would attempt
to load that class and then reference the related table. This could lead to
-problems if the table wasn't already existing. Therefore, a `String` should be
+problems if the table didn't already exist. Therefore, a `String` should be
used and then converted to a class using `constantize` in the engine later on.
Go ahead and try to create a new article. You will see that it works exactly in the
@@ -991,7 +1001,7 @@ some sort of identifier by which it can be referenced.
#### General Engine Configuration
Within an engine, there may come a time where you wish to use things such as
-initializers, internationalization or other configuration options. The great
+initializers, internationalization, or other configuration options. The great
news is that these things are entirely possible, because a Rails engine shares
much the same functionality as a Rails application. In fact, a Rails
application's functionality is actually a superset of what is provided by
@@ -1013,11 +1023,11 @@ Testing an engine
When an engine is generated, there is a smaller dummy application created inside
it at `test/dummy`. This application is used as a mounting point for the engine,
to make testing the engine extremely simple. You may extend this application by
-generating controllers, models or views from within the directory, and then use
+generating controllers, models, or views from within the directory, and then use
those to test your engine.
The `test` directory should be treated like a typical Rails testing environment,
-allowing for unit, functional and integration tests.
+allowing for unit, functional, and integration tests.
### Functional Tests
@@ -1030,9 +1040,11 @@ typical `GET` to a controller in a controller's functional test like this:
```ruby
module Blorgh
- class FooControllerTest < ActionController::TestCase
+ class FooControllerTest < ActionDispatch::IntegrationTest
+ include Engine.routes.url_helpers
+
def test_index
- get :index
+ get foos_url
...
end
end
@@ -1046,13 +1058,15 @@ in your setup code:
```ruby
module Blorgh
- class FooControllerTest < ActionController::TestCase
+ class FooControllerTest < ActionDispatch::IntegrationTest
+ include Engine.routes.url_helpers
+
setup do
@routes = Engine.routes
end
def test_index
- get :index
+ get foos_url
...
end
end
@@ -1126,7 +1140,7 @@ end
```ruby
# Blorgh/app/models/article.rb
-class Article < ActiveRecord::Base
+class Article < ApplicationRecord
has_many :comments
end
```
@@ -1147,7 +1161,7 @@ end
```ruby
# Blorgh/app/models/article.rb
-class Article < ActiveRecord::Base
+class Article < ApplicationRecord
has_many :comments
def summary
"#{title}"
@@ -1168,7 +1182,7 @@ classes at run time allowing you to significantly modularize your code.
```ruby
# MyApp/app/models/blorgh/article.rb
-class Blorgh::Article < ActiveRecord::Base
+class Blorgh::Article < ApplicationRecord
include Blorgh::Concerns::Models::Article
def time_since_created
@@ -1184,7 +1198,7 @@ end
```ruby
# Blorgh/app/models/article.rb
-class Article < ActiveRecord::Base
+class Article < ApplicationRecord
include Blorgh::Concerns::Models::Article
end
```
@@ -1202,7 +1216,7 @@ module Blorgh::Concerns::Models::Article
attr_accessor :author_name
belongs_to :author, class_name: "User"
- before_save :set_author
+ before_validation :set_author
private
def set_author
@@ -1311,7 +1325,7 @@ engine.
Assets within an engine work in an identical way to a full application. Because
the engine class inherits from `Rails::Engine`, the application will know to
-look up assets in the engine's 'app/assets' and 'lib/assets' directories.
+look up assets in the engine's `app/assets` and `lib/assets` directories.
Like all of the other components of an engine, the assets should be namespaced.
This means that if you have an asset called `style.css`, it should be placed at
@@ -1350,14 +1364,14 @@ that only exists for your engine. In this case, the host application doesn't
need to require `admin.css` or `admin.js`. Only the gem's admin layout needs
these assets. It doesn't make sense for the host app to include
`"blorgh/admin.css"` in its stylesheets. In this situation, you should
-explicitly define these assets for precompilation. This tells sprockets to add
-your engine assets when `rake assets:precompile` is triggered.
+explicitly define these assets for precompilation. This tells Sprockets to add
+your engine assets when `rails assets:precompile` is triggered.
You can define assets for precompilation in `engine.rb`:
```ruby
initializer "blorgh.assets.precompile" do |app|
- app.config.assets.precompile += %w(admin.css admin.js)
+ app.config.assets.precompile += %w( admin.js admin.css )
end
```
@@ -1403,3 +1417,115 @@ module MyEngine
end
end
```
+
+Active Support On Load Hooks
+----------------------------
+
+Active Support is the Ruby on Rails component responsible for providing Ruby language extensions, utilities, and other transversal utilities.
+
+Rails code can often be referenced on load of an application. Rails is responsible for the load order of these frameworks, so when you load frameworks, such as `ActiveRecord::Base`, prematurely you are violating an implicit contract your application has with Rails. Moreover, by loading code such as `ActiveRecord::Base` on boot of your application you are loading entire frameworks which may slow down your boot time and could cause conflicts with load order and boot of your application.
+
+On Load hooks are the API that allow you to hook into this initialization process without violating the load contract with Rails. This will also mitigate boot performance degradation and avoid conflicts.
+
+## What are `on_load` hooks?
+
+Since Ruby is a dynamic language, some code will cause different Rails frameworks to load. Take this snippet for instance:
+
+```ruby
+ActiveRecord::Base.include(MyActiveRecordHelper)
+```
+
+This snippet means that when this file is loaded, it will encounter `ActiveRecord::Base`. This encounter causes Ruby to look for the definition of that constant and will require it. This causes the entire Active Record framework to be loaded on boot.
+
+`ActiveSupport.on_load` is a mechanism that can be used to defer the loading of code until it is actually needed. The snippet above can be changed to:
+
+```ruby
+ActiveSupport.on_load(:active_record) { include MyActiveRecordHelper }
+```
+
+This new snippet will only include `MyActiveRecordHelper` when `ActiveRecord::Base` is loaded.
+
+## How does it work?
+
+In the Rails framework these hooks are called when a specific library is loaded. For example, when `ActionController::Base` is loaded, the `:action_controller_base` hook is called. This means that all `ActiveSupport.on_load` calls with `:action_controller_base` hooks will be called in the context of `ActionController::Base` (that means `self` will be an `ActionController::Base`).
+
+## Modifying code to use `on_load` hooks
+
+Modifying code is generally straightforward. If you have a line of code that refers to a Rails framework such as `ActiveRecord::Base` you can wrap that code in an `on_load` hook.
+
+### Example 1
+
+```ruby
+ActiveRecord::Base.include(MyActiveRecordHelper)
+```
+
+becomes
+
+```ruby
+ActiveSupport.on_load(:active_record) { include MyActiveRecordHelper } # self refers to ActiveRecord::Base here, so we can simply #include
+```
+
+### Example 2
+
+```ruby
+ActionController::Base.prepend(MyActionControllerHelper)
+```
+
+becomes
+
+```ruby
+ActiveSupport.on_load(:action_controller_base) { prepend MyActionControllerHelper } # self refers to ActionController::Base here, so we can simply #prepend
+```
+
+### Example 3
+
+```ruby
+ActiveRecord::Base.include_root_in_json = true
+```
+
+becomes
+
+```ruby
+ActiveSupport.on_load(:active_record) { self.include_root_in_json = true } # self refers to ActiveRecord::Base here
+```
+
+## Available Hooks
+
+These are the hooks you can use in your own code.
+
+To hook into the initialization process of one of the following classes use the available hook.
+
+| Class | Available Hooks |
+| --------------------------------- | ------------------------------------ |
+| `ActionCable` | `action_cable` |
+| `ActionController::API` | `action_controller_api` |
+| `ActionController::API` | `action_controller` |
+| `ActionController::Base` | `action_controller_base` |
+| `ActionController::Base` | `action_controller` |
+| `ActionController::TestCase` | `action_controller_test_case` |
+| `ActionDispatch::IntegrationTest` | `action_dispatch_integration_test` |
+| `ActionDispatch::SystemTestCase` | `action_dispatch_system_test_case` |
+| `ActionMailer::Base` | `action_mailer` |
+| `ActionMailer::TestCase` | `action_mailer_test_case` |
+| `ActionView::Base` | `action_view` |
+| `ActionView::TestCase` | `action_view_test_case` |
+| `ActiveJob::Base` | `active_job` |
+| `ActiveJob::TestCase` | `active_job_test_case` |
+| `ActiveRecord::Base` | `active_record` |
+| `ActiveSupport::TestCase` | `active_support_test_case` |
+| `i18n` | `i18n` |
+
+## Configuration hooks
+
+These are the available configuration hooks. They do not hook into any particular framework, but instead they run in context of the entire application.
+
+| Hook | Use Case |
+| ---------------------- | ---------------------------------------------------------------------------------- |
+| `before_configuration` | First configurable block to run. Called before any initializers are run. |
+| `before_initialize` | Second configurable block to run. Called before frameworks initialize. |
+| `before_eager_load` | Third configurable block to run. Does not run if `config.eager_load` set to false. |
+| `after_initialize` | Last configurable block to run. Called after frameworks initialize. |
+
+### Example
+
+`config.before_configuration { puts 'I am called before any initializers' }`
diff --git a/guides/source/form_helpers.md b/guides/source/form_helpers.md
index 8f7d97844e..b5e2c49487 100644
--- a/guides/source/form_helpers.md
+++ b/guides/source/form_helpers.md
@@ -1,7 +1,7 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
-Form Helpers
-============
+Action View Form Helpers
+========================
Forms in web applications are an essential interface for user input. However, form markup can quickly become tedious to write and maintain because of the need to handle form control naming and its numerous attributes. Rails does away with this complexity by providing view helpers for generating form markup. However, since these helpers have different use cases, developers need to know the differences between the helper methods before putting them to use.
@@ -22,25 +22,25 @@ NOTE: This guide is not intended to be a complete documentation of available for
Dealing with Basic Forms
------------------------
-The most basic form helper is `form_tag`.
+The main form helper is `form_with`.
```erb
-<%= form_tag do %>
+<%= form_with do %>
Form contents
<% end %>
```
-When called without arguments like this, it creates a `<form>` tag which, when submitted, will POST to the current page. For instance, assuming the current page is `/home/index`, the generated HTML will look like this (some line breaks added for readability):
+When called without arguments like this, it creates a form tag which, when submitted, will POST to the current page. For instance, assuming the current page is a home page, the generated HTML will look like this:
```html
-<form accept-charset="UTF-8" action="/" method="post">
- <input name="utf8" type="hidden" value="&#x2713;" />
+<form accept-charset="UTF-8" action="/" data-remote="true" method="post">
<input name="authenticity_token" type="hidden" value="J7CBxfHalt49OSHp27hblqK20c9PgwJ108nDHX/8Cts=" />
Form contents
</form>
```
-You'll notice that the HTML contains `input` element with type `hidden`. This `input` is important, because the form cannot be successfully submitted without it. The hidden input element has name attribute of `utf8` enforces browsers to properly respect your form's character encoding and is generated for all forms whether their actions are "GET" or "POST". The second input element with name `authenticity_token` is a security feature of Rails called **cross-site request forgery protection**, and form helpers generate it for every non-GET form (provided that this security feature is enabled). You can read more about this in the [Security Guide](security.html#cross-site-request-forgery-csrf).
+You'll notice that the HTML contains an `input` element with type `hidden`. This `input` is important, because non-GET form cannot be successfully submitted without it.
+The hidden input element with the name `authenticity_token` is a security feature of Rails called **cross-site request forgery protection**, and form helpers generate it for every non-GET form (provided that this security feature is enabled). You can read more about this in the [Securing Rails Applications](security.html#cross-site-request-forgery-csrf) guide.
### A Generic Search Form
@@ -51,10 +51,10 @@ One of the most basic forms you see on the web is a search form. This form conta
* a text input element, and
* a submit element.
-To create this form you will use `form_tag`, `label_tag`, `text_field_tag`, and `submit_tag`, respectively. Like this:
+To create this form you will use `form_with`, `label_tag`, `text_field_tag`, and `submit_tag`, respectively. Like this:
```erb
-<%= form_tag("/search", method: "get") do %>
+<%= form_with(url: "/search", method: "get") do %>
<%= label_tag(:q, "Search for:") %>
<%= text_field_tag(:q) %>
<%= submit_tag("Search") %>
@@ -64,37 +64,18 @@ To create this form you will use `form_tag`, `label_tag`, `text_field_tag`, and
This will generate the following HTML:
```html
-<form accept-charset="UTF-8" action="/search" method="get">
- <input name="utf8" type="hidden" value="&#x2713;" />
+<form accept-charset="UTF-8" action="/search" data-remote="true" method="get">
<label for="q">Search for:</label>
<input id="q" name="q" type="text" />
- <input name="commit" type="submit" value="Search" />
+ <input name="commit" type="submit" value="Search" data-disable-with="Search" />
</form>
```
-TIP: For every form input, an ID attribute is generated from its name (`"q"` in above example). These IDs can be very useful for CSS styling or manipulation of form controls with JavaScript.
-
-Besides `text_field_tag` and `submit_tag`, there is a similar helper for _every_ form control in HTML.
-
-IMPORTANT: Always use "GET" as the method for search forms. This allows users to bookmark a specific search and get back to it. More generally Rails encourages you to use the right HTTP verb for an action.
-
-### Multiple Hashes in Form Helper Calls
-
-The `form_tag` helper accepts 2 arguments: the path for the action and an options hash. This hash specifies the method of form submission and HTML options such as the form element's class.
+TIP: Passing `url: my_specified_path` to `form_with` tells the form where to make the request. However, as explained below, you can also pass ActiveRecord objects to the form.
-As with the `link_to` helper, the path argument doesn't have to be a string; it can be a hash of URL parameters recognizable by Rails' routing mechanism, which will turn the hash into a valid URL. However, since both arguments to `form_tag` are hashes, you can easily run into a problem if you would like to specify both. For instance, let's say you write this:
-
-```ruby
-form_tag(controller: "people", action: "search", method: "get", class: "nifty_form")
-# => '<form accept-charset="UTF-8" action="/people/search?method=get&class=nifty_form" method="post">'
-```
+TIP: For every form input, an ID attribute is generated from its name (`"q"` in above example). These IDs can be very useful for CSS styling or manipulation of form controls with JavaScript.
-Here, `method` and `class` are appended to the query string of the generated URL because even though you mean to write two hashes, you really only specified one. So you need to tell Ruby which is which by delimiting the first hash (or both) with curly brackets. This will generate the HTML you expect:
-
-```ruby
-form_tag({controller: "people", action: "search"}, method: "get", class: "nifty_form")
-# => '<form accept-charset="UTF-8" action="/people/search" method="get" class="nifty_form">'
-```
+IMPORTANT: Use "GET" as the method for search forms. This allows users to bookmark a specific search and get back to it. More generally Rails encourages you to use the right HTTP verb for an action.
### Helpers for Generating Form Elements
@@ -103,12 +84,12 @@ checkboxes, text fields, and radio buttons. These basic helpers, with names
ending in `_tag` (such as `text_field_tag` and `check_box_tag`), generate just a
single `<input>` element. The first parameter to these is always the name of the
input. When the form is submitted, the name will be passed along with the form
-data, and will make its way to the `params` hash in the controller with the
-value entered by the user for that field. For example, if the form contains `<%=
-text_field_tag(:query) %>`, then you would be able to get the value of this
+data, and will make its way to the `params` in the controller with the
+value entered by the user for that field. For example, if the form contains
+`<%= text_field_tag(:query) %>`, then you would be able to get the value of this
field in the controller with `params[:query]`.
-When naming inputs, Rails uses certain conventions that make it possible to submit parameters with non-scalar values such as arrays or hashes, which will also be accessible in `params`. You can read more about them in [chapter 7 of this guide](#understanding-parameter-naming-conventions). For details on the precise usage of these helpers, please refer to the [API documentation](http://api.rubyonrails.org/classes/ActionView/Helpers/FormTagHelper.html).
+When naming inputs, Rails uses certain conventions that make it possible to submit parameters with non-scalar values such as arrays or hashes, which will also be accessible in `params`. You can read more about them in chapter [Understanding Parameter Naming Conventions](#understanding-parameter-naming-conventions) of this guide. For details on the precise usage of these helpers, please refer to the [API documentation](http://api.rubyonrails.org/classes/ActionView/Helpers/FormTagHelper.html).
#### Checkboxes
@@ -140,7 +121,7 @@ Radio buttons, while similar to checkboxes, are controls that specify a set of o
<%= radio_button_tag(:age, "child") %>
<%= label_tag(:age_child, "I am younger than 21") %>
<%= radio_button_tag(:age, "adult") %>
-<%= label_tag(:age_adult, "I'm over 21") %>
+<%= label_tag(:age_adult, "I am over 21") %>
```
Output:
@@ -149,7 +130,7 @@ Output:
<input id="age_child" name="age" type="radio" value="child" />
<label for="age_child">I am younger than 21</label>
<input id="age_adult" name="age" type="radio" value="adult" />
-<label for="age_adult">I'm over 21</label>
+<label for="age_adult">I am over 21</label>
```
As with `check_box_tag`, the second parameter to `radio_button_tag` is the value of the input. Because these two radio buttons share the same name (`age`), the user will only be able to select one of them, and `params[:age]` will contain either `"child"` or `"adult"`.
@@ -162,8 +143,8 @@ make it easier for users to click the inputs.
Other form controls worth mentioning are textareas, password fields,
hidden fields, search fields, telephone fields, date fields, time fields,
-color fields, datetime fields, datetime-local fields, month fields, week fields,
-URL fields, email fields, number fields and range fields:
+color fields, datetime-local fields, month fields, week fields,
+URL fields, email fields, number fields, and range fields:
```erb
<%= text_area_tag(:message, "Hi, nice site", size: "24x6") %>
@@ -172,7 +153,6 @@ URL fields, email fields, number fields and range fields:
<%= search_field(:user, :name) %>
<%= telephone_field(:user, :phone) %>
<%= date_field(:user, :born_on) %>
-<%= datetime_field(:user, :meeting_time) %>
<%= datetime_local_field(:user, :graduation_day) %>
<%= month_field(:user, :birthday_month) %>
<%= week_field(:user, :birthday_week) %>
@@ -193,7 +173,6 @@ Output:
<input id="user_name" name="user[name]" type="search" />
<input id="user_phone" name="user[phone]" type="tel" />
<input id="user_born_on" name="user[born_on]" type="date" />
-<input id="user_meeting_time" name="user[meeting_time]" type="datetime" />
<input id="user_graduation_day" name="user[graduation_day]" type="datetime-local" />
<input id="user_birthday_month" name="user[birthday_month]" type="month" />
<input id="user_birthday_week" name="user[birthday_week]" type="week" />
@@ -208,15 +187,14 @@ Output:
Hidden inputs are not shown to the user but instead hold data like any textual input. Values inside them can be changed with JavaScript.
IMPORTANT: The search, telephone, date, time, color, datetime, datetime-local,
-month, week, URL, email, number and range inputs are HTML5 controls.
+month, week, URL, email, number, and range inputs are HTML5 controls.
If you require your app to have a consistent experience in older browsers,
you will need an HTML5 polyfill (provided by CSS and/or JavaScript).
-There is definitely [no shortage of solutions for this](https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills), although a couple of popular tools at the moment are
-[Modernizr](http://www.modernizr.com/) and [yepnope](http://yepnopejs.com/),
-which provide a simple way to add functionality based on the presence of
+There is definitely [no shortage of solutions for this](https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills), although a popular tool at the moment is
+[Modernizr](https://modernizr.com/), which provides a simple way to add functionality based on the presence of
detected HTML5 features.
-TIP: If you're using password input fields (for any purpose), you might want to configure your application to prevent those parameters from being logged. You can learn about this in the [Security Guide](security.html#logging).
+TIP: If you're using password input fields (for any purpose), you might want to configure your application to prevent those parameters from being logged. You can learn about this in the [Securing Rails Applications](security.html#logging) guide.
Dealing with Model Objects
--------------------------
@@ -234,10 +212,10 @@ For these helpers the first argument is the name of an instance variable and the
will produce output similar to
```erb
-<input id="person_name" name="person[name]" type="text" value="Henry"/>
+<input id="person_name" name="person[name]" type="text" value="Henry" />
```
-Upon form submission the value entered by the user will be stored in `params[:person][:name]`. The `params[:person]` hash is suitable for passing to `Person.new` or, if `@person` is an instance of Person, `@person.update`. While the name of an attribute is the most common second parameter to these helpers this is not compulsory. In the example above, as long as person objects have a `name` and a `name=` method Rails will be happy.
+Upon form submission the value entered by the user will be stored in `params[:person][:name]`.
WARNING: You must pass the name of an instance variable, i.e. `:person` or `"person"`, not an actual instance of your model object.
@@ -245,7 +223,7 @@ Rails provides helpers for displaying the validation errors associated with a mo
### Binding a Form to an Object
-While this is an increase in comfort it is far from perfect. If `Person` has many attributes to edit then we would be repeating the name of the edited object many times. What we want to do is somehow bind a form to a model object, which is exactly what `form_for` does.
+While this is an increase in comfort it is far from perfect. If `Person` has many attributes to edit then we would be repeating the name of the edited object many times. What we want to do is somehow bind a form to a model object, which is exactly what `form_with` with `:model` does.
Assume we have a controller for dealing with articles `app/controllers/articles_controller.rb`:
@@ -255,10 +233,10 @@ def new
end
```
-The corresponding view `app/views/articles/new.html.erb` using `form_for` looks like this:
+The corresponding view `app/views/articles/new.html.erb` using `form_with` looks like this:
```erb
-<%= form_for @article, url: {action: "create"}, html: {class: "nifty_form"} do |f| %>
+<%= form_with model: @article, class: "nifty_form" do |f| %>
<%= f.text_field :title %>
<%= f.text_area :body, size: "60x12" %>
<%= f.submit "Create" %>
@@ -268,30 +246,34 @@ The corresponding view `app/views/articles/new.html.erb` using `form_for` looks
There are a few things to note here:
* `@article` is the actual object being edited.
-* There is a single hash of options. Routing options are passed in the `:url` hash, HTML options are passed in the `:html` hash. Also you can provide a `:namespace` option for your form to ensure uniqueness of id attributes on form elements. The namespace attribute will be prefixed with underscore on the generated HTML id.
-* The `form_for` method yields a **form builder** object (the `f` variable).
+* There is a single hash of options. HTML options (except `id` and `class`) are passed in the `:html` hash. Also you can provide a `:namespace` option for your form to ensure uniqueness of id attributes on form elements. The scope attribute will be prefixed with underscore on the generated HTML id.
+* The `form_with` method yields a **form builder** object (the `f` variable).
+* If you wish to direct your form request to a particular url, you would use `form_with url: my_nifty_url_path` instead. To see more in depth options on what `form_with` accepts be sure to [check out the API documentation](https://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_with).
* Methods to create form controls are called **on** the form builder object `f`.
The resulting HTML is:
```html
-<form accept-charset="UTF-8" action="/articles" method="post" class="nifty_form">
- <input id="article_title" name="article[title]" type="text" />
- <textarea id="article_body" name="article[body]" cols="60" rows="12"></textarea>
- <input name="commit" type="submit" value="Create" />
+<form class="nifty_form" action="/articles" accept-charset="UTF-8" data-remote="true" method="post">
+ <input type="hidden" name="authenticity_token" value="NRkFyRWxdYNfUg7vYxLOp2SLf93lvnl+QwDWorR42Dp6yZXPhHEb6arhDOIWcqGit8jfnrPwL781/xlrzj63TA==" />
+ <input type="text" name="article[title]" id="article_title" />
+ <textarea name="article[body]" id="article_body" cols="60" rows="12"></textarea>
+ <input type="submit" name="commit" value="Create" data-disable-with="Create" />
</form>
```
-The name passed to `form_for` controls the key used in `params` to access the form's values. Here the name is `article` and so all the inputs have names of the form `article[attribute_name]`. Accordingly, in the `create` action `params[:article]` will be a hash with keys `:title` and `:body`. You can read more about the significance of input names in the [parameter_names section](#understanding-parameter-naming-conventions).
+The object passed as `:model` in `form_with` controls the key used in `params` to access the form's values. Here the name is `article` and so all the inputs have names of the form `article[attribute_name]`. Accordingly, in the `create` action `params[:article]` will be a hash with keys `:title` and `:body`. You can read more about the significance of input names in chapter [Understanding Parameter Naming Conventions](#understanding-parameter-naming-conventions) of this guide.
+
+TIP: Conventionally your inputs will mirror model attributes. However, they don't have to! If there is other information you need you can include it in your form just as with attributes and access it via `params[:article][:my_nifty_non_attribute_input]`.
The helper methods called on the form builder are identical to the model object helpers except that it is not necessary to specify which object is being edited since this is already managed by the form builder.
You can create a similar binding without actually creating `<form>` tags with the `fields_for` helper. This is useful for editing additional model objects with the same form. For example, if you had a `Person` model with an associated `ContactDetail` model, you could create a form for creating both like so:
```erb
-<%= form_for @person, url: {action: "create"} do |person_form| %>
+<%= form_with model: @person do |person_form| %>
<%= person_form.text_field :name %>
- <%= fields_for @person.contact_detail do |contact_detail_form| %>
+ <%= fields_for :contact_detail, @person.contact_detail do |contact_detail_form| %>
<%= contact_detail_form.text_field :phone_number %>
<% end %>
<% end %>
@@ -300,13 +282,14 @@ You can create a similar binding without actually creating `<form>` tags with th
which produces the following output:
```html
-<form accept-charset="UTF-8" action="/people" class="new_person" id="new_person" method="post">
- <input id="person_name" name="person[name]" type="text" />
- <input id="contact_detail_phone_number" name="contact_detail[phone_number]" type="text" />
+<form action="/people" accept-charset="UTF-8" data-remote="true" method="post">
+ <input type="hidden" name="authenticity_token" value="bL13x72pldyDD8bgtkjKQakJCpd4A8JdXGbfksxBDHdf1uC0kCMqe2tvVdUYfidJt0fj3ihC4NxiVHv8GVYxJA==" />
+ <input type="text" name="person[name]" id="person_name" />
+ <input type="text" name="contact_detail[phone_number]" id="contact_detail_phone_number" />
</form>
```
-The object yielded by `fields_for` is a form builder like the one yielded by `form_for` (in fact `form_for` calls `fields_for` internally).
+The object yielded by `fields_for` is a form builder like the one yielded by `form_with`.
### Relying on Record Identification
@@ -316,68 +299,67 @@ The Article model is directly available to users of the application, so - follow
resources :articles
```
-TIP: Declaring a resource has a number of side-affects. See [Rails Routing From the Outside In](routing.html#resource-routing-the-rails-default) for more information on setting up and using resources.
+TIP: Declaring a resource has a number of side effects. See [Rails Routing from the Outside In](routing.html#resource-routing-the-rails-default) guide for more information on setting up and using resources.
-When dealing with RESTful resources, calls to `form_for` can get significantly easier if you rely on **record identification**. In short, you can just pass the model instance and have Rails figure out model name and the rest:
+When dealing with RESTful resources, calls to `form_with` can get significantly easier if you rely on **record identification**. In short, you can just pass the model instance and have Rails figure out model name and the rest:
```ruby
## Creating a new article
# long-style:
-form_for(@article, url: articles_path)
-# same thing, short-style (record identification gets used):
-form_for(@article)
+form_with(model: @article, url: articles_path)
+short-style:
+form_with(model: @article)
## Editing an existing article
# long-style:
-form_for(@article, url: article_path(@article), html: {method: "patch"})
+form_with(model: @article, url: article_path(@article), method: "patch")
# short-style:
-form_for(@article)
+form_with(model: @article)
```
-Notice how the short-style `form_for` invocation is conveniently the same, regardless of the record being new or existing. Record identification is smart enough to figure out if the record is new by asking `record.new_record?`. It also selects the correct path to submit to and the name based on the class of the object.
+Notice how the short-style `form_with` invocation is conveniently the same, regardless of the record being new or existing. Record identification is smart enough to figure out if the record is new by asking `record.new_record?`. It also selects the correct path to submit to, and the name based on the class of the object.
-Rails will also automatically set the `class` and `id` of the form appropriately: a form creating an article would have `id` and `class` `new_article`. If you were editing the article with id 23, the `class` would be set to `edit_article` and the id to `edit_article_23`. These attributes will be omitted for brevity in the rest of this guide.
-
-WARNING: When you're using STI (single-table inheritance) with your models, you can't rely on record identification on a subclass if only their parent class is declared a resource. You will have to specify the model name, `:url`, and `:method` explicitly.
+WARNING: When you're using STI (single-table inheritance) with your models, you can't rely on record identification on a subclass if only their parent class is declared a resource. You will have to specify `:url`, and `:scope` (the model name) explicitly.
#### Dealing with Namespaces
-If you have created namespaced routes, `form_for` has a nifty shorthand for that too. If your application has an admin namespace then
+If you have created namespaced routes, `form_with` has a nifty shorthand for that too. If your application has an admin namespace then
```ruby
-form_for [:admin, @article]
+form_with model: [:admin, @article]
```
will create a form that submits to the `ArticlesController` inside the admin namespace (submitting to `admin_article_path(@article)` in the case of an update). If you have several levels of namespacing then the syntax is similar:
```ruby
-form_for [:admin, :management, @article]
+form_with model: [:admin, :management, @article]
```
-For more information on Rails' routing system and the associated conventions, please see the [routing guide](routing.html).
+For more information on Rails' routing system and the associated conventions, please see [Rails Routing from the Outside In](routing.html) guide.
### How do forms with PATCH, PUT, or DELETE methods work?
-The Rails framework encourages RESTful design of your applications, which means you'll be making a lot of "PATCH" and "DELETE" requests (besides "GET" and "POST"). However, most browsers _don't support_ methods other than "GET" and "POST" when it comes to submitting forms.
+The Rails framework encourages RESTful design of your applications, which means you'll be making a lot of "PATCH", "PUT", and "DELETE" requests (besides "GET" and "POST"). However, most browsers _don't support_ methods other than "GET" and "POST" when it comes to submitting forms.
Rails works around this issue by emulating other methods over POST with a hidden input named `"_method"`, which is set to reflect the desired method:
```ruby
-form_tag(search_path, method: "patch")
+form_with(url: search_path, method: "patch")
```
-output:
+Output:
```html
-<form accept-charset="UTF-8" action="/search" method="post">
+<form accept-charset="UTF-8" action="/search" data-remote="true" method="post">
<input name="_method" type="hidden" value="patch" />
- <input name="utf8" type="hidden" value="&#x2713;" />
<input name="authenticity_token" type="hidden" value="f755bb0ed134b76c432144748a6d4b7a7ddf2b71" />
...
</form>
```
-When parsing POSTed data, Rails will take into account the special `_method` parameter and acts as if the HTTP method was the one specified inside it ("PATCH" in this example).
+When parsing POSTed data, Rails will take into account the special `_method` parameter and act as if the HTTP method was the one specified inside it ("PATCH" in this example).
+
+IMPORTANT: All forms using `form_with` implement `remote: true` by default. These forms will submit data using an XHR (Ajax) request. To disable this include `local: true`. To dive deeper see [Working with JavaScript in Rails](working_with_javascript_in_rails.html#remote-elements) guide.
Making Select Boxes with Ease
-----------------------------
@@ -390,8 +372,7 @@ Here is what the markup might look like:
<select name="city_id" id="city_id">
<option value="1">Lisbon</option>
<option value="2">Madrid</option>
- ...
- <option value="12">Berlin</option>
+ <option value="3">Berlin</option>
</select>
```
@@ -402,19 +383,21 @@ Here you have a list of cities whose names are presented to the user. Internally
The most generic helper is `select_tag`, which - as the name implies - simply generates the `SELECT` tag that encapsulates an options string:
```erb
-<%= select_tag(:city_id, '<option value="1">Lisbon</option>...') %>
+<%= select_tag(:city_id, raw('<option value="1">Lisbon</option><option value="2">Madrid</option><option value="3">Berlin</option>')) %>
```
This is a start, but it doesn't dynamically create the option tags. You can generate option tags with the `options_for_select` helper:
```html+erb
-<%= options_for_select([['Lisbon', 1], ['Madrid', 2], ...]) %>
+<%= options_for_select([['Lisbon', 1], ['Madrid', 2], ['Berlin', 3]]) %>
+```
-output:
+Output:
+```html
<option value="1">Lisbon</option>
<option value="2">Madrid</option>
-...
+<option value="3">Berlin</option>
```
The first argument to `options_for_select` is a nested array where each element has two elements: option text (city name) and option value (city id). The option value is what will be submitted to your controller. Often this will be the id of a corresponding database object but this does not have to be the case.
@@ -428,50 +411,61 @@ Knowing this, you can combine `select_tag` and `options_for_select` to achieve t
`options_for_select` allows you to pre-select an option by passing its value.
```html+erb
-<%= options_for_select([['Lisbon', 1], ['Madrid', 2], ...], 2) %>
+<%= options_for_select([['Lisbon', 1], ['Madrid', 2], ['Berlin', 3]], 2) %>
+```
-output:
+Output:
+```html
<option value="1">Lisbon</option>
<option value="2" selected="selected">Madrid</option>
-...
+<option value="3">Berlin</option>
```
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:
```html+erb
<%= options_for_select(
[
['Lisbon', 1, { 'data-size' => '2.8 million' }],
- ['Madrid', 2, { 'data-size' => '3.2 million' }]
+ ['Madrid', 2, { 'data-size' => '3.2 million' }],
+ ['Berlin', 3, { 'data-size' => '3.4 million' }]
], 2
) %>
+```
-output:
+Output:
+```html
<option value="1" data-size="2.8 million">Lisbon</option>
<option value="2" selected="selected" data-size="3.2 million">Madrid</option>
-...
+<option value="3" data-size="3.4 million">Berlin</option>
```
-### Select Boxes for Dealing with Models
+### Select Boxes for Dealing with Model Objects
+
+In most cases form controls will be tied to a specific model and as you might expect Rails provides helpers tailored for that purpose. Consistent with other form helpers, when dealing with a model object drop the `_tag` suffix from `select_tag`:
-In most cases form controls will be tied to a specific database model and as you might expect Rails provides helpers tailored for that purpose. Consistent with other form helpers, when dealing with models you drop the `_tag` suffix from `select_tag`:
+If your controller has defined `@person` and that person's city_id is 2:
```ruby
-# controller:
@person = Person.new(city_id: 2)
```
```erb
-# view:
-<%= select(:person, :city_id, [['Lisbon', 1], ['Madrid', 2], ...]) %>
+<%= select(:person, :city_id, [['Lisbon', 1], ['Madrid', 2], ['Berlin', 3]]) %>
+```
+
+will produce output similar to
+
+```html
+<select name="person[city_id]" id="person_city_id">
+ <option value="1">Lisbon</option>
+ <option value="2" selected="selected">Madrid</option>
+ <option value="3">Berlin</option>
+</select>
```
Notice that the third parameter, the options array, is the same kind of argument you pass to `options_for_select`. One advantage here is that you don't have to worry about pre-selecting the correct city if the user already has one - Rails will do this for you by reading from the `@person.city_id` attribute.
@@ -479,21 +473,26 @@ Notice that the third parameter, the options array, is the same kind of argument
As with other helpers, if you were to use the `select` helper on a form builder scoped to the `@person` object, the syntax would be:
```erb
-# select on a form builder
-<%= f.select(:city_id, ...) %>
+<%= form_with model: @person do |person_form| %>
+ <%= person_form.select(:city_id, [['Lisbon', 1], ['Madrid', 2], ['Berlin', 3]]) %>
+<% end %>
```
You can also pass a block to `select` helper:
```erb
-<%= f.select(:city_id) do %>
- <% [['Lisbon', 1], ['Madrid', 2]].each do |c| -%>
- <%= content_tag(:option, c.first, value: c.last) %>
+<%= form_with model: @person do |person_form| %>
+ <%= person_form.select(:city_id) do %>
+ <% [['Lisbon', 1], ['Madrid', 2], ['Berlin', 3]].each do |c| %>
+ <%= content_tag(:option, c.first, value: c.last) %>
+ <% end %>
<% end %>
<% end %>
```
-WARNING: If you are using `select` (or similar helpers such as `collection_select`, `select_tag`) to set a `belongs_to` association you must pass the name of the foreign key (in the example above `city_id`), not the name of association itself. If you specify `city` instead of `city_id` Active Record will raise an error along the lines of `ActiveRecord::AssociationTypeMismatch: City(#17815740) expected, got String(#1138750)` when you pass the `params` hash to `Person.new` or `update`. Another way of looking at this is that form helpers only edit attributes. You should also be aware of the potential security ramifications of allowing users to edit foreign keys directly.
+WARNING: If you are using `select` or similar helpers to set a `belongs_to` association you must pass the name of the foreign key (in the example above `city_id`), not the name of association itself.
+
+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.
### Option Tags from a Collection of Arbitrary Objects
@@ -510,7 +509,7 @@ This is a perfectly valid solution, but Rails provides a less verbose alternativ
<%= options_from_collection_for_select(City.all, :id, :name) %>
```
-As the name implies, this only generates option tags. To generate a working select box you would need to use it in conjunction with `select_tag`, just as you would with `options_for_select`. When working with model objects, just as `select` combines `select_tag` and `options_for_select`, `collection_select` combines `select_tag` with `options_from_collection_for_select`.
+As the name implies, this only generates option tags. To generate a working select box you would need to use `collection_select`:
```erb
<%= collection_select(:person, :city_id, City.all, :id, :name) %>
@@ -519,38 +518,38 @@ As the name implies, this only generates option tags. To generate a working sele
As with other helpers, if you were to use the `collection_select` helper on a form builder scoped to the `@person` object, the syntax would be:
```erb
-<%= f.collection_select(:city_id, City.all, :id, :name) %>
+<%= form_with model: @person do |person_form| %>
+ <%= person_form.collection_select(:city_id, City.all, :id, :name) %>
+<% end %>
```
-To recap, `options_from_collection_for_select` is to `collection_select` what `options_for_select` is to `select`.
-
-NOTE: Pairs passed to `options_for_select` should have the name first and the id second, however with `options_from_collection_for_select` the first argument is the value method and the second the text method.
+NOTE: Pairs passed to `options_for_select` should have the text first and the value second, however with `options_from_collection_for_select` should have the value method first and the text method second.
### Time Zone and Country Select
-To leverage time zone support in Rails, you have to ask your users what time zone they are in. Doing so would require generating select options from a list of pre-defined TimeZone objects using `collection_select`, but you can simply use the `time_zone_select` helper that already wraps this:
+To leverage time zone support in Rails, you have to ask your users what time zone they are in. Doing so would require generating select options from a list of pre-defined [`ActiveSupport::TimeZone`](http://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html) objects using `collection_select`, but you can simply use the `time_zone_select` helper that already wraps this:
```erb
<%= 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).
+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).
Using Date and Time Form Helpers
--------------------------------
You can choose not to use the form helpers generating HTML5 date and time input fields and use the alternative date and time helpers. These date and time helpers differ from all the other form helpers in two important respects:
-* Dates and times are not representable by a single input element. Instead you have several, one for each component (year, month, day etc.) and so there is no single value in your `params` hash with your date or time.
+* Dates and times are not representable by a single input element. Instead, you have several, one for each component (year, month, day etc.) and so there is no single value in your `params` hash with your date or time.
* Other helpers use the `_tag` suffix to indicate whether a helper is a barebones helper or one that operates on model objects. With dates and times, `select_date`, `select_time` and `select_datetime` are the barebones helpers, `date_select`, `time_select` and `datetime_select` are the equivalent model object helpers.
Both of these families of helpers will create a series of select boxes for the different components (year, month, day etc.).
### Barebones Helpers
-The `select_*` family of helpers take as their first argument an instance of `Date`, `Time` or `DateTime` that is used as the currently selected value. You may omit this parameter, in which case the current date is used. For example:
+The `select_*` family of helpers take as their first argument an instance of `Date`, `Time`, or `DateTime` that is used as the currently selected value. You may omit this parameter, in which case the current date is used. For example:
```erb
<%= select_date Date.today, prefix: :start_date %>
@@ -559,12 +558,15 @@ The `select_*` family of helpers take as their first argument an instance of `Da
outputs (with actual option values omitted for brevity)
```html
-<select id="start_date_year" name="start_date[year]"> ... </select>
-<select id="start_date_month" name="start_date[month]"> ... </select>
-<select id="start_date_day" name="start_date[day]"> ... </select>
+<select id="start_date_year" name="start_date[year]">
+</select>
+<select id="start_date_month" name="start_date[month]">
+</select>
+<select id="start_date_day" name="start_date[day]">
+</select>
```
-The above inputs would result in `params[:start_date]` being a hash with keys `:year`, `:month`, `:day`. To get an actual `Date`, `Time` or `DateTime` object you would have to extract these values and pass them to the appropriate constructor, for example:
+The above inputs would result in `params[:start_date]` being a hash with keys `:year`, `:month`, `:day`. To get an actual `Date`, `Time`, or `DateTime` object you would have to extract these values and pass them to the appropriate constructor, for example:
```ruby
Date.civil(params[:start_date][:year].to_i, params[:start_date][:month].to_i, params[:start_date][:day].to_i)
@@ -584,9 +586,12 @@ The model object helpers for dates and times submit parameters with special name
outputs (with actual option values omitted for brevity)
```html
-<select id="person_birth_date_1i" name="person[birth_date(1i)]"> ... </select>
-<select id="person_birth_date_2i" name="person[birth_date(2i)]"> ... </select>
-<select id="person_birth_date_3i" name="person[birth_date(3i)]"> ... </select>
+<select id="person_birth_date_1i" name="person[birth_date(1i)]">
+</select>
+<select id="person_birth_date_2i" name="person[birth_date(2i)]">
+</select>
+<select id="person_birth_date_3i" name="person[birth_date(3i)]">
+</select>
```
which results in a `params` hash like
@@ -603,68 +608,60 @@ Both families of helpers use the same core set of functions to generate the indi
As a rule of thumb you should be using `date_select` when working with model objects and `select_date` in other cases, such as a search form which filters results by date.
-NOTE: In many cases the built-in date pickers are clumsy as they do not aid the user in working out the relationship between the date and the day of the week.
-
### Individual Components
Occasionally you need to display just a single date component such as a year or a month. Rails provides a series of helpers for this, one for each component `select_year`, `select_month`, `select_day`, `select_hour`, `select_minute`, `select_second`. These helpers are fairly straightforward. By default they will generate an input field named after the time component (for example, "year" for `select_year`, "month" for `select_month` etc.) although this can be overridden with the `:field_name` option. The `:prefix` option works in the same way that it does for `select_date` and `select_time` and has the same default value.
-The first parameter specifies which value should be selected and can either be an instance of a `Date`, `Time` or `DateTime`, in which case the relevant component will be extracted, or a numerical value. For example:
+The first parameter specifies which value should be selected and can either be an instance of a `Date`, `Time`, or `DateTime`, in which case the relevant component will be extracted, or a numerical value. For example:
```erb
<%= select_year(2009) %>
-<%= select_year(Time.now) %>
+<%= select_year(Time.new(2009)) %>
```
-will produce the same output if the current year is 2009 and the value chosen by the user can be retrieved by `params[:date][:year]`.
+will produce the same output and the value chosen by the user can be retrieved by `params[:date][:year]`.
Uploading Files
---------------
-A common task is uploading some sort of file, whether it's a picture of a person or a CSV file containing data to process. The most important thing to remember with file uploads is that the rendered form's encoding **MUST** be set to "multipart/form-data". If you use `form_for`, this is done automatically. If you use `form_tag`, you must set it yourself, as per the following example.
+A common task is uploading some sort of file, whether it's a picture of a person or a CSV file containing data to process. The most important thing to remember with file uploads is that the rendered form's enctype attribute **must** be set to "multipart/form-data". If you use `form_with` with `:model`, this is done automatically. If you use `form_with` without `:model`, you must set it yourself, as per the following example.
The following two forms both upload a file.
```erb
-<%= form_tag({action: :upload}, multipart: true) do %>
+<%= form_with(url: {action: :upload}, multipart: true) do %>
<%= file_field_tag 'picture' %>
<% end %>
-<%= form_for @person do |f| %>
+<%= form_with model: @person do |f| %>
<%= f.file_field :picture %>
<% end %>
```
-Rails provides the usual pair of helpers: the barebones `file_field_tag` and the model oriented `file_field`. The only difference with other helpers is that you cannot set a default value for file inputs as this would have no meaning. As you would expect in the first case the uploaded file is in `params[:picture]` and in the second case in `params[:person][:picture]`.
+Rails provides the usual pair of helpers: the barebones `file_field_tag` and the model oriented `file_field`. As you would expect in the first case the uploaded file is in `params[:picture]` and in the second case in `params[:person][:picture]`.
### What Gets Uploaded
-The object in the `params` hash is an instance of a subclass of `IO`. Depending on the size of the uploaded file it may in fact be a `StringIO` or an instance of `File` backed by a temporary file. In both cases the object will have an `original_filename` attribute containing the name the file had on the user's computer and a `content_type` attribute containing the MIME type of the uploaded file. The following snippet saves the uploaded content in `#{Rails.root}/public/uploads` under the same name as the original file (assuming the form was the one in the previous example).
+The object in the `params` hash is an instance of [`ActionDispatch::Http::UploadedFile`](http://api.rubyonrails.org/classes/ActionDispatch/Http/UploadedFile.html). The following snippet saves the uploaded file in `#{Rails.root}/public/uploads` under the same name as the original file.
```ruby
def upload
- uploaded_io = params[:person][:picture]
- File.open(Rails.root.join('public', 'uploads', uploaded_io.original_filename), 'wb') do |file|
- file.write(uploaded_io.read)
+ uploaded_file = params[:picture]
+ File.open(Rails.root.join('public', 'uploads', uploaded_file.original_filename), 'wb') do |file|
+ file.write(uploaded_file.read)
end
end
```
-Once a file has been uploaded, there are a multitude of potential tasks, ranging from where to store the files (on disk, Amazon S3, etc) and associating them with models to resizing image files and generating thumbnails. The intricacies of this are beyond the scope of this guide, but there are several libraries designed to assist with these. Two of the better known ones are [CarrierWave](https://github.com/jnicklas/carrierwave) and [Paperclip](https://github.com/thoughtbot/paperclip).
-
-NOTE: If the user has not selected a file the corresponding parameter will be an empty string.
-
-### Dealing with Ajax
-
-Unlike other forms making an asynchronous file upload form is not as simple as providing `form_for` with `remote: true`. With an Ajax form the serialization is done by JavaScript running inside the browser and since JavaScript cannot read files from your hard drive the file cannot be uploaded. The most common workaround is to use an invisible iframe that serves as the target for the form submission.
+Once a file has been uploaded, there are a multitude of potential tasks, ranging from where to store the files (on Disk, Amazon S3, etc), associating them with models, resizing image files, and generating thumbnails, etc. [Active Storage](active_storage_overview.html) is designed to assist with these tasks.
Customizing Form Builders
-------------------------
-As mentioned previously the object yielded by `form_for` and `fields_for` is an instance of `FormBuilder` (or a subclass thereof). Form builders encapsulate the notion of displaying form elements for a single object. While you can of course write helpers for your forms in the usual way, you can also subclass `FormBuilder` and add the helpers there. For example:
+The object yielded by `form_with` and `fields_for` is an instance of [`ActionView::Helpers::FormBuilder`](http://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html). Form builders encapsulate the notion of displaying form elements for a single object. While you can write helpers for your forms in the usual way, you can also create subclass `ActionView::Helpers::FormBuilder` and add the helpers there. For example:
```erb
-<%= form_for @person do |f| %>
+<%= form_with model: @person do |f| %>
<%= text_field_with_label f, :first_name %>
<% end %>
```
@@ -672,7 +669,7 @@ As mentioned previously the object yielded by `form_for` and `fields_for` is an
can be replaced with
```erb
-<%= form_for @person, builder: LabellingFormBuilder do |f| %>
+<%= form_with model: @person, builder: LabellingFormBuilder do |f| %>
<%= f.text_field :first_name %>
<% end %>
```
@@ -687,12 +684,12 @@ class LabellingFormBuilder < ActionView::Helpers::FormBuilder
end
```
-If you reuse this frequently you could define a `labeled_form_for` helper that automatically applies the `builder: LabellingFormBuilder` option:
+If you reuse this frequently you could define a `labeled_form_with` helper that automatically applies the `builder: LabellingFormBuilder` option:
```ruby
-def labeled_form_for(record, options = {}, &block)
+def labeled_form_with(model: nil, scope: nil, url: nil, format: nil, **options, &block)
options.merge! builder: LabellingFormBuilder
- form_for record, options, &block
+ form_with model: model, scope: scope, url: url, format: format, **options, &block
end
```
@@ -702,23 +699,15 @@ The form builder used also determines what happens when you do
<%= render partial: f %>
```
-If `f` is an instance of `FormBuilder` then this will render the `form` partial, setting the partial's object to the form builder. If the form builder is of class `LabellingFormBuilder` then the `labelling_form` partial would be rendered instead.
+If `f` is an instance of `ActionView::Helpers::FormBuilder` then this will render the `form` partial, setting the partial's object to the form builder. If the form builder is of class `LabellingFormBuilder` then the `labelling_form` partial would be rendered instead.
Understanding Parameter Naming Conventions
------------------------------------------
-As you've seen in the previous sections, values from forms can be at the top level of the `params` hash or nested in another hash. For example, in a standard `create`
-action for a Person model, `params[:person]` would usually be a hash of all the attributes for the person to create. The `params` hash can also contain arrays, arrays of hashes and so on.
+Values from forms can be at the top level of the `params` hash or nested in another hash. For example, in a standard `create` action for a Person model, `params[:person]` would usually be a hash of all the attributes for the person to create. The `params` hash can also contain arrays, arrays of hashes, and so on.
Fundamentally HTML forms don't know about any sort of structured data, all they generate is name-value pairs, where pairs are just plain strings. The arrays and hashes you see in your application are the result of some parameter naming conventions that Rails uses.
-TIP: You may find you can try out examples in this section faster by using the console to directly invoke Rack's parameter parser. For example,
-
-```ruby
-Rack::Utils.parse_query "name=fred&phone=0123456789"
-# => {"name"=>"fred", "phone"=>"0123456789"}
-```
-
### Basic Structures
The two basic structures are arrays and hashes. Hashes mirror the syntax used for accessing the value in `params`. For example, if a form contains:
@@ -762,28 +751,31 @@ This would result in `params[:person][:phone_number]` being an array containing
We can mix and match these two concepts. One element of a hash might be an array as in the previous example, or you can have an array of hashes. For example, a form might let you create any number of addresses by repeating the following form fragment
```html
-<input name="addresses[][line1]" type="text"/>
-<input name="addresses[][line2]" type="text"/>
-<input name="addresses[][city]" type="text"/>
+<input name="person[addresses][][line1]" type="text"/>
+<input name="person[addresses][][line2]" type="text"/>
+<input name="person[addresses][][city]" type="text"/>
+<input name="person[addresses][][line1]" type="text"/>
+<input name="person[addresses][][line2]" type="text"/>
+<input name="person[addresses][][city]" type="text"/>
```
-This would result in `params[:addresses]` being an array of hashes with keys `line1`, `line2` and `city`. Rails decides to start accumulating values in a new hash whenever it encounters an input name that already exists in the current hash.
+This would result in `params[:person][:addresses]` being an array of hashes with keys `line1`, `line2`, and `city`.
-There's a restriction, however, while hashes can be nested arbitrarily, only one level of "arrayness" is allowed. Arrays can usually be replaced by hashes; for example, instead of having an array of model objects, one can have a hash of model objects keyed by their id, an array index or some other parameter.
+There's a restriction, however, while hashes can be nested arbitrarily, only one level of "arrayness" is allowed. Arrays can usually be replaced by hashes; for example, instead of having an array of model objects, one can have a hash of model objects keyed by their id, an array index, or some other parameter.
-WARNING: Array parameters do not play well with the `check_box` helper. According to the HTML specification unchecked checkboxes submit no value. However it is often convenient for a checkbox to always submit a value. The `check_box` helper fakes this by creating an auxiliary hidden input with the same name. If the checkbox is unchecked only the hidden input is submitted and if it is checked then both are submitted but the value submitted by the checkbox takes precedence. When working with array parameters this duplicate submission will confuse Rails since duplicate input names are how it decides when to start a new array element. It is preferable to either use `check_box_tag` or to use hashes instead of arrays.
+WARNING: Array parameters do not play well with the `check_box` helper. According to the HTML specification unchecked checkboxes submit no value. However it is often convenient for a checkbox to always submit a value. The `check_box` helper fakes this by creating an auxiliary hidden input with the same name. If the checkbox is unchecked only the hidden input is submitted and if it is checked then both are submitted but the value submitted by the checkbox takes precedence.
### Using Form Helpers
-The previous sections did not use the Rails form helpers at all. While you can craft the input names yourself and pass them directly to helpers such as `text_field_tag` Rails also provides higher level support. The two tools at your disposal here are the name parameter to `form_for` and `fields_for` and the `:index` option that helpers take.
+The previous sections did not use the Rails form helpers at all. While you can craft the input names yourself and pass them directly to helpers such as `text_field_tag` Rails also provides higher level support. The two tools at your disposal here are the name parameter to `form_with` and `fields_for` and the `:index` option that helpers take.
You might want to render a form with a set of edit fields for each of a person's addresses. For example:
```erb
-<%= form_for @person do |person_form| %>
+<%= form_with model: @person do |person_form| %>
<%= person_form.text_field :name %>
<% @person.addresses.each do |address| %>
- <%= person_form.fields_for address, index: address.id do |address_form|%>
+ <%= person_form.fields_for address, index: address.id do |address_form| %>
<%= address_form.text_field :city %>
<% end %>
<% end %>
@@ -793,7 +785,8 @@ You might want to render a form with a set of edit fields for each of a person's
Assuming the person had two addresses, with ids 23 and 45 this would create output similar to this:
```html
-<form accept-charset="UTF-8" action="/people/1" class="edit_person" id="edit_person_1" method="post">
+<form accept-charset="UTF-8" action="/people/1" data-remote="true" method="post">
+ <input name="_method" type="hidden" value="patch" />
<input id="person_name" name="person[name]" type="text" />
<input id="person_address_23_city" name="person[address][23][city]" type="text" />
<input id="person_address_45_city" name="person[address][45][city]" type="text" />
@@ -818,7 +811,7 @@ To create more intricate nestings, you can specify the first part of the input
name (`person[address]` in the previous example) explicitly:
```erb
-<%= fields_for 'person[address][primary]', address, index: address do |address_form| %>
+<%= fields_for 'person[address][primary]', address, index: address.id do |address_form| %>
<%= address_form.text_field :city %>
<% end %>
```
@@ -826,12 +819,12 @@ name (`person[address]` in the previous example) explicitly:
will create inputs like
```html
-<input id="person_address_primary_1_city" name="person[address][primary][1][city]" type="text" value="bologna" />
+<input id="person_address_primary_1_city" name="person[address][primary][1][city]" type="text" value="Bologna" />
```
-As a general rule the final input name is the concatenation of the name given to `fields_for`/`form_for`, the index value and the name of the attribute. You can also pass an `:index` option directly to helpers such as `text_field`, but it is usually less repetitive to specify this at the form builder level rather than on individual input controls.
+As a general rule the final input name is the concatenation of the name given to `fields_for`/`form_with`, the index value, and the name of the attribute. You can also pass an `:index` option directly to helpers such as `text_field`, but it is usually less repetitive to specify this at the form builder level rather than on individual input controls.
-As a shortcut you can append [] to the name and omit the `:index` option. This is the same as specifying `index: address` so
+As a shortcut you can append [] to the name and omit the `:index` option. This is the same as specifying `index: address.id` so
```erb
<%= fields_for 'person[address][primary][]', address do |address_form| %>
@@ -844,10 +837,10 @@ produces exactly the same output as the previous example.
Forms to External Resources
---------------------------
-Rails' form helpers can also be used to build a form for posting data to an external resource. However, at times it can be necessary to set an `authenticity_token` for the resource; this can be done by passing an `authenticity_token: 'your_external_token'` parameter to the `form_tag` options:
+Rails' form helpers can also be used to build a form for posting data to an external resource. However, at times it can be necessary to set an `authenticity_token` for the resource; this can be done by passing an `authenticity_token: 'your_external_token'` parameter to the `form_with` options:
```erb
-<%= form_tag 'http://farfar.away/form', authenticity_token: 'external_token' do %>
+<%= form_with url: 'http://farfar.away/form', authenticity_token: 'external_token' do %>
Form contents
<% end %>
```
@@ -855,23 +848,7 @@ Rails' form helpers can also be used to build a form for posting data to an exte
Sometimes when submitting data to an external resource, like a payment gateway, the fields that can be used in the form are limited by an external API and it may be undesirable to generate an `authenticity_token`. To not send a token, simply pass `false` to the `:authenticity_token` option:
```erb
-<%= form_tag 'http://farfar.away/form', authenticity_token: false do %>
- Form contents
-<% end %>
-```
-
-The same technique is also available for `form_for`:
-
-```erb
-<%= form_for @invoice, url: external_url, authenticity_token: 'external_token' do |f| %>
- Form contents
-<% end %>
-```
-
-Or if you don't want to render an `authenticity_token` field:
-
-```erb
-<%= form_for @invoice, url: external_url, authenticity_token: false do |f| %>
+<%= form_with url: 'http://farfar.away/form', authenticity_token: false do %>
Form contents
<% end %>
```
@@ -879,31 +856,31 @@ Or if you don't want to render an `authenticity_token` field:
Building Complex Forms
----------------------
-Many apps grow beyond simple forms editing a single object. For example, when creating a `Person` you might want to allow the user to (on the same form) create multiple address records (home, work, etc.). When later editing that person the user should be able to add, remove or amend addresses as necessary.
+Many apps grow beyond simple forms editing a single object. For example, when creating a `Person` you might want to allow the user to (on the same form) create multiple address records (home, work, etc.). When later editing that person the user should be able to add, remove, or amend addresses as necessary.
### Configuring the Model
Active Record provides model level support via the `accepts_nested_attributes_for` method:
```ruby
-class Person < ActiveRecord::Base
- has_many :addresses
+class Person < ApplicationRecord
+ has_many :addresses, inverse_of: :person
accepts_nested_attributes_for :addresses
end
-class Address < ActiveRecord::Base
+class Address < ApplicationRecord
belongs_to :person
end
```
-This creates an `addresses_attributes=` method on `Person` that allows you to create, update and (optionally) destroy addresses.
+This creates an `addresses_attributes=` method on `Person` that allows you to create, update, and (optionally) destroy addresses.
### Nested Forms
The following form allows a user to create a `Person` and its associated addresses.
```html+erb
-<%= form_for @person do |f| %>
+<%= form_with model: @person do |f| %>
Addresses:
<ul>
<%= f.fields_for :addresses do |addresses_form| %>
@@ -926,7 +903,7 @@ When an association accepts nested attributes `fields_for` renders its block onc
```ruby
def new
@person = Person.new
- 2.times { @person.addresses.build}
+ 2.times { @person.addresses.build }
end
```
@@ -954,12 +931,12 @@ The `fields_for` yields a form builder. The parameters' name will be what
The keys of the `:addresses_attributes` hash are unimportant, they need merely be different for each address.
-If the associated object is already saved, `fields_for` autogenerates a hidden input with the `id` of the saved record. You can disable this by passing `include_id: false` to `fields_for`. You may wish to do this if the autogenerated input is placed in a location where an input tag is not valid HTML or when using an ORM where children do not have an `id`.
+If the associated object is already saved, `fields_for` autogenerates a hidden input with the `id` of the saved record. You can disable this by passing `include_id: false` to `fields_for`.
### The Controller
As usual you need to
-[whitelist the parameters](action_controller_overview.html#strong-parameters) in
+[declare the permitted parameters](action_controller_overview.html#strong-parameters) in
the controller before you pass them to the model:
```ruby
@@ -979,23 +956,23 @@ private
You can allow users to delete associated objects by passing `allow_destroy: true` to `accepts_nested_attributes_for`
```ruby
-class Person < ActiveRecord::Base
+class Person < ApplicationRecord
has_many :addresses
accepts_nested_attributes_for :addresses, allow_destroy: true
end
```
-If the hash of attributes for an object contains the key `_destroy` with a value
-of `1` or `true` then the object will be destroyed. This form allows users to
-remove addresses:
+If the hash of attributes for an object contains the key `_destroy` with a value that
+evaluates to `true` (eg. 1, '1', true, or 'true') then the object will be destroyed.
+This form allows users to remove addresses:
```erb
-<%= form_for @person do |f| %>
+<%= form_with model: @person do |f| %>
Addresses:
<ul>
<%= f.fields_for :addresses do |addresses_form| %>
<li>
- <%= addresses_form.check_box :_destroy%>
+ <%= addresses_form.check_box :_destroy %>
<%= addresses_form.label :kind %>
<%= addresses_form.text_field :kind %>
...
@@ -1005,7 +982,7 @@ remove addresses:
<% end %>
```
-Don't forget to update the whitelisted params in your controller to also include
+Don't forget to update the permitted params in your controller to also include
the `_destroy` field:
```ruby
@@ -1020,7 +997,7 @@ end
It is often useful to ignore sets of fields that the user has not filled in. You can control this by passing a `:reject_if` proc to `accepts_nested_attributes_for`. This proc will be called with each hash of attributes submitted by the form. If the proc returns `false` then Active Record will not build an associated object for that hash. The example below only tries to build an address if the `kind` attribute is set.
```ruby
-class Person < ActiveRecord::Base
+class Person < ApplicationRecord
has_many :addresses
accepts_nested_attributes_for :addresses, reject_if: lambda {|attributes| attributes['kind'].blank?}
end
@@ -1030,4 +1007,9 @@ As a convenience you can instead pass the symbol `:all_blank` which will create
### Adding Fields on the Fly
-Rather than rendering multiple sets of fields ahead of time you may wish to add them only when a user clicks on an 'Add new address' button. Rails does not provide any built-in support for this. When generating new sets of fields you must ensure the key of the associated array is unique - the current JavaScript date (milliseconds after the epoch) is a common choice.
+Rather than rendering multiple sets of fields ahead of time you may wish to add them only when a user clicks on an 'Add new address' button. Rails does not provide any built-in support for this. When generating new sets of fields you must ensure the key of the associated array is unique - the current JavaScript date (milliseconds since the [epoch](https://en.wikipedia.org/wiki/Unix_time)) is a common choice.
+
+Using form_for and form_tag
+---------------------------
+
+Before `form_with` was introduced in Rails 5.1 its functionality used to be split between `form_tag` and `form_for`. Both are now soft-deprecated. Documentation on their usage can be found in [older versions of this guide](https://guides.rubyonrails.org/v5.2/form_helpers.html).
diff --git a/guides/source/generators.md b/guides/source/generators.md
index 32bbdc554a..88ce4be8da 100644
--- a/guides/source/generators.md
+++ b/guides/source/generators.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Creating and Customizing Rails Generators & Templates
=====================================================
@@ -26,13 +26,13 @@ When you create an application using the `rails` command, you are in fact using
```bash
$ rails new myapp
$ cd myapp
-$ bin/rails generate
+$ rails generate
```
You will get a list of all generators that comes with Rails. If you need a detailed description of the helper generator, for example, you can simply do:
```bash
-$ bin/rails generate helper --help
+$ rails generate helper --help
```
Creating Your First Generator
@@ -57,13 +57,13 @@ Our new generator is quite simple: it inherits from `Rails::Generators::Base` an
To invoke our new generator, we just need to do:
```bash
-$ bin/rails generate initializer
+$ rails generate initializer
```
Before we go on, let's see our brand new generator description:
```bash
-$ bin/rails generate initializer --help
+$ rails generate initializer --help
```
Rails is usually able to generate good descriptions if a generator is namespaced, as `ActiveRecord::Generators::ModelGenerator`, but not in this particular case. We can solve this problem in two ways. The first one is calling `desc` inside our generator:
@@ -85,18 +85,20 @@ Creating Generators with Generators
Generators themselves have a generator:
```bash
-$ bin/rails generate generator initializer
+$ rails generate generator initializer
create lib/generators/initializer
create lib/generators/initializer/initializer_generator.rb
create lib/generators/initializer/USAGE
create lib/generators/initializer/templates
+ invoke test_unit
+ create test/lib/generators/initializer_generator_test.rb
```
This is the generator just created:
```ruby
class InitializerGenerator < Rails::Generators::NamedBase
- source_root File.expand_path("../templates", __FILE__)
+ source_root File.expand_path('templates', __dir__)
end
```
@@ -105,7 +107,7 @@ First, notice that we are inheriting from `Rails::Generators::NamedBase` instead
We can see that by invoking the description of this new generator (don't forget to delete the old generator file):
```bash
-$ bin/rails generate initializer --help
+$ rails generate initializer --help
Usage:
rails generate initializer NAME [options]
```
@@ -122,7 +124,7 @@ And now let's change the generator to copy this template when invoked:
```ruby
class InitializerGenerator < Rails::Generators::NamedBase
- source_root File.expand_path("../templates", __FILE__)
+ source_root File.expand_path('templates', __dir__)
def copy_initializer_file
copy_file "initializer.rb", "config/initializers/#{file_name}.rb"
@@ -133,7 +135,7 @@ end
And let's execute our generator:
```bash
-$ bin/rails generate initializer core_extensions
+$ rails generate initializer core_extensions
```
We can see that now an initializer named core_extensions was created at `config/initializers/core_extensions.rb` with the contents of our template. That means that `copy_file` copied a file in our source root to the destination path we gave. The method `file_name` is automatically created when we inherit from `Rails::Generators::NamedBase`.
@@ -172,7 +174,7 @@ end
Before we customize our workflow, let's first see what our scaffold looks like:
```bash
-$ bin/rails generate scaffold User name:string
+$ rails generate scaffold User name:string
invoke active_record
create db/migrate/20130924151154_create_users.rb
create app/models/user.rb
@@ -197,9 +199,10 @@ $ bin/rails generate scaffold User name:string
invoke jbuilder
create app/views/users/index.json.jbuilder
create app/views/users/show.json.jbuilder
+ invoke test_unit
+ create test/application_system_test_case.rb
+ create test/system/users_test.rb
invoke assets
- invoke coffee
- create app/assets/javascripts/users.coffee
invoke scss
create app/assets/stylesheets/users.scss
invoke scss
@@ -208,7 +211,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 and test fixture files for scaffolds altogether. We can achieve that by changing our configuration to the following:
```ruby
config.generators do |g|
@@ -216,20 +227,21 @@ config.generators do |g|
g.template_engine :erb
g.test_framework :test_unit, fixture: false
g.stylesheets false
- g.javascripts false
end
```
-If we generate another resource with the scaffold generator, we can see that stylesheet, JavaScript and fixture files are not created anymore. If you want to customize it further, for example to use DataMapper and RSpec instead of Active Record and TestUnit, it's just a matter of adding their gems to your application and configuring your generators.
+If we generate another resource with the scaffold generator, we can see that stylesheet, JavaScript, and fixture files are not created anymore. If you want to customize it further, for example to use DataMapper and RSpec instead of Active Record and TestUnit, it's just a matter of adding their gems to your application and configuring your generators.
To demonstrate this, we are going to create a new helper generator that simply adds some instance variable readers. First, we create a generator within the rails namespace, as this is where rails searches for generators used as hooks:
```bash
-$ bin/rails generate generator rails/my_helper
+$ rails generate generator rails/my_helper
create lib/generators/rails/my_helper
create lib/generators/rails/my_helper/my_helper_generator.rb
create lib/generators/rails/my_helper/USAGE
create lib/generators/rails/my_helper/templates
+ invoke test_unit
+ create test/lib/generators/rails/my_helper_generator_test.rb
```
After that, we can delete both the `templates` directory and the `source_root`
@@ -252,7 +264,7 @@ end
We can try out our new generator by creating a helper for products:
```bash
-$ bin/rails generate my_helper products
+$ rails generate my_helper products
create app/helpers/products_helper.rb
```
@@ -272,7 +284,6 @@ config.generators do |g|
g.template_engine :erb
g.test_framework :test_unit, fixture: false
g.stylesheets false
- g.javascripts false
g.helper :my_helper
end
```
@@ -280,7 +291,7 @@ end
and see it in action when invoking the generator:
```bash
-$ bin/rails generate scaffold Article body:text
+$ rails generate scaffold Article body:text
[...]
invoke my_helper
create app/helpers/articles_helper.rb
@@ -337,7 +348,6 @@ config.generators do |g|
g.template_engine :erb
g.test_framework :test_unit, fixture: false
g.stylesheets false
- g.javascripts false
end
```
@@ -372,7 +382,6 @@ config.generators do |g|
g.template_engine :erb
g.test_framework :shoulda, fixture: false
g.stylesheets false
- g.javascripts false
# Add a fallback!
g.fallbacks[:shoulda] = :test_unit
@@ -382,7 +391,7 @@ end
Now, if you create a Comment scaffold, you will see that the shoulda generators are being invoked, and at the end, they are just falling back to TestUnit generators:
```bash
-$ bin/rails generate scaffold Comment body:text
+$ rails generate scaffold Comment body:text
invoke active_record
create db/migrate/20130924143118_create_comments.rb
create app/models/comment.rb
@@ -407,10 +416,12 @@ $ bin/rails generate scaffold Comment body:text
invoke jbuilder
create app/views/comments/index.json.jbuilder
create app/views/comments/show.json.jbuilder
+ invoke test_unit
+ create test/application_system_test_case.rb
+ create test/system/comments_test.rb
invoke assets
- invoke coffee
- create app/assets/javascripts/comments.coffee
invoke scss
+ create app/assets/stylesheets/scaffolds.scss
```
Fallbacks allow your generators to have a single responsibility, increasing code reuse and reducing the amount of duplication.
@@ -418,7 +429,7 @@ Fallbacks allow your generators to have a single responsibility, increasing code
Application Templates
---------------------
-Now that you've seen how generators can be used _inside_ an application, did you know they can also be used to _generate_ applications too? This kind of generator is referred as a "template". This is a brief overview of the Templates API. For detailed documentation see the [Rails Application Templates guide](rails_application_templates.html).
+Now that you've seen how generators can be used _inside_ an application, did you know they can also be used to _generate_ applications too? This kind of generator is referred to as a "template". This is a brief overview of the Templates API. For detailed documentation see the [Rails Application Templates guide](rails_application_templates.html).
```ruby
gem "rspec-rails", group: "test"
@@ -451,6 +462,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
-----------------
@@ -476,13 +507,13 @@ Available options are:
Any additional options passed to this method are put on the end of the line:
```ruby
-gem "devise", git: "git://github.com/plataformatec/devise", branch: "master"
+gem "devise", git: "https://github.com/plataformatec/devise.git", branch: "master"
```
The above code will put the following line into `Gemfile`:
```ruby
-gem "devise", git: "git://github.com/plataformatec/devise", branch: "master"
+gem "devise", git: "https://github.com/plataformatec/devise.git", branch: "master"
```
### `gem_group`
@@ -599,7 +630,7 @@ This method also takes a block:
```ruby
lib "super_special.rb" do
- puts "Super special!"
+ "puts 'Super special!'"
end
```
@@ -608,7 +639,7 @@ end
Creates a Rake file in the `lib/tasks` directory of the application.
```ruby
-rakefile "test.rake", "hello there"
+rakefile "test.rake", 'task(:hello) { puts "Hello, there" }'
```
This method also takes a block:
@@ -661,14 +692,6 @@ Available options are:
* `:env` - Specifies the environment in which to run this rake task.
* `:sudo` - Whether or not to run this task using `sudo`. Defaults to `false`.
-### `capify!`
-
-Runs the `capify` command from Capistrano at the root of the application which generates Capistrano configuration.
-
-```ruby
-capify!
-```
-
### `route`
Adds text to the `config/routes.rb` file:
diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md
index 400383cfb5..e2f558d74c 100644
--- a/guides/source/getting_started.md
+++ b/guides/source/getting_started.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Getting Started with Rails
==========================
@@ -20,14 +20,7 @@ Guide Assumptions
This guide is designed for beginners who want to get started with a Rails
application from scratch. It does not assume that you have any prior experience
-with Rails. However, to get the most out of it, you need to have some
-prerequisites installed:
-
-* The [Ruby](https://www.ruby-lang.org/en/downloads) language version 2.2.2 or newer.
-* Right version of [Development Kit](http://rubyinstaller.org/downloads/), if you are using Windows
-* The [RubyGems](https://rubygems.org) packaging system, which is installed with Ruby
- versions 1.9 and later. To learn more about RubyGems, please read the [RubyGems Guides](http://guides.rubygems.org).
-* A working installation of the [SQLite3 Database](https://www.sqlite.org).
+with Rails.
Rails is a web application framework running on the Ruby programming language.
If you have no prior experience with Ruby, you will find a very steep learning
@@ -35,7 +28,7 @@ curve diving straight into Rails. There are several curated lists of online reso
for learning Ruby:
* [Official Ruby Programming Language website](https://www.ruby-lang.org/en/documentation/)
-* [reSRC's List of Free Programming Books](http://resrc.io/list/10/list-of-free-programming-books/#ruby)
+* [List of Free Programming Books](https://github.com/vhf/free-programming-books/blob/master/free-programming-books.md#ruby)
Be aware that some resources, while still excellent, cover versions of Ruby as old as
1.6, and commonly 1.8, and will not include some syntax that you will see in day-to-day
@@ -44,7 +37,7 @@ development with Rails.
What is Rails?
--------------
-Rails is a web application development framework written in the Ruby language.
+Rails is a web application development framework written in the Ruby programming language.
It is designed to make programming web applications easier by making assumptions
about what every developer needs to get started. It allows you to write less
code while accomplishing more than many other languages and frameworks.
@@ -66,14 +59,13 @@ The Rails philosophy includes two major guiding principles:
again, our code is more maintainable, more extensible, and less buggy.
* **Convention Over Configuration:** Rails has opinions about the best way to do many
things in a web application, and defaults to this set of conventions, rather than
- require that you specify every minutiae through endless configuration files.
+ require that you specify minutiae through endless configuration files.
Creating a New Rails Project
----------------------------
-
-The best way to use this guide is to follow each step as it happens, no code or
-step needed to make this example application has been left out, so you can
-literally follow along step by step.
+The best way to read this guide is to follow it step by step. All steps are
+essential to run this example application and no additional code or steps are
+needed.
By following along with this guide, you'll create a Rails project called
`blog`, a (very) simple weblog. Before you can start building the application,
@@ -85,22 +77,30 @@ 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
+Before you install Rails, you should check to make sure that your system has the
+proper prerequisites installed. These include Ruby and SQLite3.
+
+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:
-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).
-For more installation methods for most Operating Systems take a look at
-[ruby-lang.org](https://www.ruby-lang.org/en/documentation/installation/).
-
```bash
$ ruby -v
-ruby 2.2.2p95
+ruby 2.5.0
```
+Rails requires Ruby version 2.4.1 or later. If the version number returned is
+less than that number, you'll need to install a fresh copy of Ruby.
+
+TIP: To quickly install Ruby and Ruby on Rails on your system in Windows, you can use
+[Rails Installer](http://railsinstaller.org). For more installation methods for most
+Operating Systems take a look at [ruby-lang.org](https://www.ruby-lang.org/en/documentation/installation/).
+
+If you are working on Windows, you should also install the
+[Ruby Installer Development Kit](https://rubyinstaller.org/downloads/).
+
+You will also need an installation of the SQLite3 database.
Many popular UNIX-like OSes ship with an acceptable version of SQLite3.
On Windows, if you installed Rails through Rails Installer, you
already have SQLite installed. Others can find installation instructions
@@ -126,7 +126,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.2.1", you are ready to continue.
### Creating the Blog Application
@@ -147,6 +147,10 @@ This will create a Rails application called Blog in a `blog` directory and
install the gem dependencies that are already mentioned in `Gemfile` using
`bundle install`.
+NOTE: If you're using Windows Subsystem for Linux then there are currently some
+limitations on file system notifications that mean you should disable the `spring`
+and `listen` gems which you can do by running `rails new blog --skip-spring --skip-listen`.
+
TIP: You can see all of the command line options that the Rails application
builder accepts by running `rails new -h`.
@@ -163,20 +167,24 @@ of the files and folders that Rails created by default:
| File/Folder | Purpose |
| ----------- | ------- |
-|app/|Contains the controllers, models, views, helpers, mailers and assets for your application. You'll focus on this folder for the remainder of this guide.|
-|bin/|Contains the rails script that starts your app and can contain other scripts you use to setup, deploy or run your application.|
+|app/|Contains the controllers, models, views, helpers, mailers, channels, jobs, and assets for your application. You'll focus on this folder for the remainder of this guide.|
+|bin/|Contains the rails script that starts your app and can contain other scripts you use to setup, update, deploy, or run your application.|
|config/|Configure your application's routes, database, and more. This is covered in more detail in [Configuring Rails Applications](configuring.html).|
-|config.ru|Rack configuration for Rack based servers used to start the application.|
+|config.ru|Rack configuration for Rack based servers used to start the application. For more information about Rack, see the [Rack website](https://rack.github.io/).|
|db/|Contains your current database schema, as well as the database migrations.|
-|Gemfile<br>Gemfile.lock|These files allow you to specify what gem dependencies are needed for your Rails application. These files are used by the Bundler gem. For more information about Bundler, see the [Bundler website](http://bundler.io).|
+|Gemfile<br>Gemfile.lock|These files allow you to specify what gem dependencies are needed for your Rails application. These files are used by the Bundler gem. For more information about Bundler, see the [Bundler website](https://bundler.io).|
|lib/|Extended modules for your application.|
|log/|Application log files.|
+|package.json|This file allows you to specify what npm dependencies are needed for your Rails application. This file is used by Yarn. For more information about Yarn, see the [Yarn website](https://yarnpkg.com/lang/en/).|
|public/|The only folder seen by the world as-is. Contains static files and compiled assets.|
-|Rakefile|This file locates and loads tasks that can be run from the command line. The task definitions are defined throughout the components of Rails. Rather than changing Rakefile, you should add your own tasks by adding files to the lib/tasks directory of your application.|
-|README.rdoc|This is a brief instruction manual for your application. You should edit this file to tell others what your application does, how to set it up, and so on.|
+|Rakefile|This file locates and loads tasks that can be run from the command line. The task definitions are defined throughout the components of Rails. Rather than changing `Rakefile`, you should add your own tasks by adding files to the `lib/tasks` directory of your application.|
+|README.md|This is a brief instruction manual for your application. You should edit this file to tell others what your application does, how to set it up, and so on.|
+|storage/|Active Storage files for Disk Service. This is covered in [Active Storage Overview](active_storage_overview.html).|
|test/|Unit tests, fixtures, and other test apparatus. These are covered in [Testing Rails Applications](testing.html).|
|tmp/|Temporary files (like cache and pid files).|
|vendor/|A place for all third-party code. In a typical Rails application this includes vendored gems.|
+|.gitignore|This file tells git which files (or patterns) it should ignore. See [GitHub - Ignoring files](https://help.github.com/articles/ignoring-files) for more info about ignoring files.
+|.ruby-version|This file contains the default Ruby version.|
Hello, Rails!
-------------
@@ -191,7 +199,7 @@ start a web server on your development machine. You can do this by running the
following in the `blog` directory:
```bash
-$ bin/rails server
+$ rails server
```
TIP: If you are using Windows, you have to pass the scripts under the `bin`
@@ -200,14 +208,14 @@ 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.
-Rails adds the `therubyracer` gem to the generated `Gemfile` in a
+Usually macOS and Windows come with a JavaScript runtime installed.
+Rails adds the `mini_racer` 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
default to the `Gemfile` in apps generated under JRuby. You can investigate
all the supported runtimes at [ExecJS](https://github.com/rails/execjs#readme).
-This will fire up WEBrick, a web server distributed with Ruby by default. To see
+This will fire up Puma, a web server distributed with Rails by default. To see
your application in action, open a browser window and navigate to
<http://localhost:3000>. You should see the Rails default information page:
@@ -215,15 +223,14 @@ 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.
The "Welcome aboard" page is the _smoke test_ for a new Rails application: it
makes sure that you have your software configured correctly enough to serve a
-page. You can also click on the _About your application's environment_ link to
-see a summary of your application's environment.
+page.
### Say "Hello", Rails
@@ -244,11 +251,11 @@ Ruby) which is processed by the request cycle in Rails before being sent to the
user.
To create a new controller, you will need to run the "controller" generator and
-tell it you want a controller called "welcome" with an action called "index",
+tell it you want a controller called "Welcome" with an action called "index",
just like this:
```bash
-$ bin/rails generate controller welcome index
+$ rails generate controller Welcome index
```
Rails will create several files and a route for you.
@@ -263,9 +270,8 @@ invoke test_unit
create test/controllers/welcome_controller_test.rb
invoke helper
create app/helpers/welcome_helper.rb
+invoke test_unit
invoke assets
-invoke coffee
-create app/assets/javascripts/welcome.coffee
invoke scss
create app/assets/stylesheets/welcome.scss
```
@@ -297,35 +303,32 @@ Open the file `config/routes.rb` in your editor.
Rails.application.routes.draw do
get 'welcome/index'
- # The priority is based upon order of creation:
- # first created -> highest priority.
- # See how all your routes lay out with "rake routes".
- #
- # You can have the root of your site routed with "root"
- # root 'welcome#index'
- #
- # ...
+ # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
+end
```
This is your application's _routing file_ which holds entries in a special
-[DSL (domain-specific language)](http://en.wikipedia.org/wiki/Domain-specific_language)
+[DSL (domain-specific language)](https://en.wikipedia.org/wiki/Domain-specific_language)
that tells Rails how to connect incoming requests to
-controllers and actions. This file contains many sample routes on commented
-lines, and one of them actually shows you how to connect the root of your site
-to a specific controller and action. Find the line beginning with `root` and
-uncomment it. It should look something like the following:
+controllers and actions.
+Edit this file by adding the line of code `root 'welcome#index'`.
+It should look something like the following:
```ruby
-root 'welcome#index'
+Rails.application.routes.draw do
+ get 'welcome/index'
+
+ root 'welcome#index'
+end
```
`root 'welcome#index'` tells Rails to map requests to the root of the
application to the welcome controller's index action and `get 'welcome/index'`
tells Rails to map requests to <http://localhost:3000/welcome/index> to the
welcome controller's index action. This was created earlier when you ran the
-controller generator (`bin/rails generate controller welcome index`).
+controller generator (`rails generate controller Welcome index`).
-Launch the web server again if you stopped it to generate the controller (`bin/rails
+Launch the web server again if you stopped it to generate the controller (`rails
server`) and navigate to <http://localhost:3000> in your browser. You'll see the
"Hello, Rails!" message you put into `app/views/welcome/index.html.erb`,
indicating that this new route is indeed going to `WelcomeController`'s `index`
@@ -336,21 +339,22 @@ TIP: For more information about routing, refer to [Rails Routing from the Outsid
Getting Up and Running
----------------------
-Now that you've seen how to create a controller, an action and a view, let's
+Now that you've seen how to create a controller, an action, and a view, let's
create something with a bit more substance.
In the Blog application, you will now create a new _resource_. A resource is the
-term used for a collection of similar objects, such as articles, people or
+term used for a collection of similar objects, such as articles, people, or
animals.
-You can create, read, update and destroy items for a resource and these
+You can create, read, update, and destroy items for a resource and these
operations are referred to as _CRUD_ operations.
Rails provides a `resources` method which can be used to declare a standard REST
resource. You need to add the _article resource_ to the
-`config/routes.rb` as follows:
+`config/routes.rb` so the file will look as follows:
```ruby
Rails.application.routes.draw do
+ get 'welcome/index'
resources :articles
@@ -358,23 +362,24 @@ Rails.application.routes.draw do
end
```
-If you run `bin/rake routes`, you'll see that it has defined routes for all the
+If you run `rails routes`, you'll see that it has defined routes for all the
standard RESTful actions. The meaning of the prefix column (and other columns)
will be seen later, but for now notice that Rails has inferred the
singular form `article` and makes meaningful use of the distinction.
```bash
-$ bin/rake routes
- Prefix Verb URI Pattern Controller#Action
- articles GET /articles(.:format) articles#index
- POST /articles(.:format) articles#create
- new_article GET /articles/new(.:format) articles#new
-edit_article GET /articles/:id/edit(.:format) articles#edit
- article GET /articles/:id(.:format) articles#show
- PATCH /articles/:id(.:format) articles#update
- PUT /articles/:id(.:format) articles#update
- DELETE /articles/:id(.:format) articles#destroy
- root GET / welcome#index
+$ rails routes
+ Prefix Verb URI Pattern Controller#Action
+welcome_index GET /welcome/index(.:format) welcome#index
+ articles GET /articles(.:format) articles#index
+ POST /articles(.:format) articles#create
+ new_article GET /articles/new(.:format) articles#new
+ edit_article GET /articles/:id/edit(.:format) articles#edit
+ article GET /articles/:id(.:format) articles#show
+ PATCH /articles/:id(.:format) articles#update
+ PUT /articles/:id(.:format) articles#update
+ DELETE /articles/:id(.:format) articles#destroy
+ root GET / welcome#index
```
In the next section, you will add the ability to create new articles in your
@@ -386,7 +391,7 @@ create and read. The form for doing this will look like this:
It will look a little basic for now, but that's ok. We'll look at improving the
styling for it afterwards.
-### Laying down the ground work
+### Laying down the groundwork
Firstly, you need a place within the application to create a new article. A
great place for that would be at `/articles/new`. With the route already
@@ -402,7 +407,7 @@ a controller called `ArticlesController`. You can do this by running this
command:
```bash
-$ bin/rails generate controller articles
+$ rails generate controller Articles
```
If you open up the newly generated `app/controllers/articles_controller.rb`
@@ -454,10 +459,9 @@ You're getting this error now because Rails expects plain actions like this one
to have views associated with them to display their information. With no view
available, Rails will raise an exception.
-In the above image, the bottom line has been truncated. Let's see what the full
-error message looks like:
+Let's look at the full error message again:
->Missing template articles/new, application/new with {locale:[:en], formats:[:html], handlers:[:erb, :builder, :coffee]}. Searched in: * "/path/to/blog/app/views"
+>ArticlesController#new is missing a template for this request format and variant. request.formats: ["text/html"] request.variant: [] NOTE! For XHR/Ajax or API requests, this action would normally respond with 204 No Content: an empty white screen. Since you're loading it in a web browser, we assume that you expected to actually render a template, not… nothing, so we're showing an error to be extra-clear. If you expect 204 No Content, carry on. That's what you'll get from an XHR or API request. Give it a shot.
That's quite a lot of text! Let's quickly go through and understand what each
part of it means.
@@ -467,27 +471,24 @@ The first part identifies which template is missing. In this case, it's the
then it will attempt to load a template called `application/new`. It looks for
one here because the `ArticlesController` inherits from `ApplicationController`.
-The next part of the message contains a hash. The `:locale` key in this hash
-simply indicates which spoken language template should be retrieved. By default,
-this is the English - or "en" - template. The next key, `:formats` specifies the
-format of template to be served in response. The default format is `:html`, and
-so Rails is looking for an HTML template. The final key, `:handlers`, is telling
-us what _template handlers_ could be used to render our template. `:erb` is most
-commonly used for HTML templates, `:builder` is used for XML templates, and
-`:coffee` uses CoffeeScript to build JavaScript templates.
-
-The final part of this message tells us where Rails has looked for the templates.
-Templates within a basic Rails application like this are kept in a single
-location, but in more complex applications it could be many different paths.
+The next part of the message contains `request.formats` which specifies
+the format of template to be served in response. It is set to `text/html` as we
+requested this page via browser, so Rails is looking for an HTML template.
+`request.variant` specifies what kind of physical devices would be served by
+the response and helps Rails determine which template to use in the response.
+It is empty because no information has been provided.
The simplest template that would work in this case would be one located at
`app/views/articles/new.html.erb`. The extension of this file name is important:
the first extension is the _format_ of the template, and the second extension
-is the _handler_ that will be used. Rails is attempting to find a template
-called `articles/new` within `app/views` for the application. The format for
-this template can only be `html` and the handler must be one of `erb`,
-`builder` or `coffee`. Because you want to create a new HTML form, you will be
-using the `ERB` language which is designed to embed Ruby in HTML.
+is the _handler_ that will be used to render the template. Rails is attempting
+to find a template called `articles/new` within `app/views` for the
+application. The format for this template can only be `html` and the default
+handler for HTML is `erb`. Rails uses other handlers for other formats.
+`builder` handler is used to build XML templates and `coffee` handler uses
+CoffeeScript to build JavaScript templates. Since you want to create a new
+HTML form, you will be using the `ERB` language which is designed to embed Ruby
+in HTML.
Therefore the file should be called `articles/new.html.erb` and needs to be
located inside the `app/views` directory of the application.
@@ -500,43 +501,43 @@ write this content in it:
```
When you refresh <http://localhost:3000/articles/new> you'll now see that the
-page has a title. The route, controller, action and view are now working
+page has a title. The route, controller, action, and view are now working
harmoniously! It's time to create the form for a new article.
### The first form
To create a form within this template, you will use a *form
builder*. The primary form builder for Rails is provided by a helper
-method called `form_for`. To use this method, add this code into
+method called `form_with`. To use this method, add this code into
`app/views/articles/new.html.erb`:
```html+erb
-<%= form_for :article do |f| %>
+<%= form_with scope: :article, local: true do |form| %>
<p>
- <%= f.label :title %><br>
- <%= f.text_field :title %>
+ <%= form.label :title %><br>
+ <%= form.text_field :title %>
</p>
<p>
- <%= f.label :text %><br>
- <%= f.text_area :text %>
+ <%= form.label :text %><br>
+ <%= form.text_area :text %>
</p>
<p>
- <%= f.submit %>
+ <%= form.submit %>
</p>
<% end %>
```
-If you refresh the page now, you'll see the exact same form as in the example.
+If you refresh the page now, you'll see the exact same form from our example above.
Building forms in Rails is really just that easy!
-When you call `form_for`, you pass it an identifying object for this
-form. In this case, it's the symbol `:article`. This tells the `form_for`
+When you call `form_with`, you pass it an identifying scope for this
+form. In this case, it's the symbol `:article`. This tells the `form_with`
helper what this form is for. Inside the block for this method, the
-`FormBuilder` object - represented by `f` - is used to build two labels and two
+`FormBuilder` object - represented by `form` - is used to build two labels and two
text fields, one each for the title and text of an article. Finally, a call to
-`submit` on the `f` object will create a submit button for the form.
+`submit` on the `form` object will create a submit button for the form.
There's one problem with this form though. If you inspect the HTML that is
generated, by viewing the source of the page, you will see that the `action`
@@ -545,33 +546,34 @@ this route goes to the very page that you're on right at the moment, and that
route should only be used to display the form for a new article.
The form needs to use a different URL in order to go somewhere else.
-This can be done quite simply with the `:url` option of `form_for`.
+This can be done quite simply with the `:url` option of `form_with`.
Typically in Rails, the action that is used for new form submissions
like this is called "create", and so the form should be pointed to that action.
-Edit the `form_for` line inside `app/views/articles/new.html.erb` to look like
+Edit the `form_with` line inside `app/views/articles/new.html.erb` to look like
this:
```html+erb
-<%= form_for :article, url: articles_path do |f| %>
+<%= form_with scope: :article, url: articles_path, local: true do |form| %>
```
In this example, the `articles_path` helper is passed to the `:url` option.
To see what Rails will do with this, we look back at the output of
-`bin/rake routes`:
+`rails routes`:
```bash
-$ bin/rake routes
+$ rails routes
Prefix Verb URI Pattern Controller#Action
- articles GET /articles(.:format) articles#index
- POST /articles(.:format) articles#create
- new_article GET /articles/new(.:format) articles#new
-edit_article GET /articles/:id/edit(.:format) articles#edit
- article GET /articles/:id(.:format) articles#show
- PATCH /articles/:id(.:format) articles#update
- PUT /articles/:id(.:format) articles#update
- DELETE /articles/:id(.:format) articles#destroy
- root GET / welcome#index
+welcome_index GET /welcome/index(.:format) welcome#index
+ articles GET /articles(.:format) articles#index
+ POST /articles(.:format) articles#create
+ new_article GET /articles/new(.:format) articles#new
+ edit_article GET /articles/:id/edit(.:format) articles#edit
+ article GET /articles/:id(.:format) articles#show
+ PATCH /articles/:id(.:format) articles#update
+ PUT /articles/:id(.:format) articles#update
+ DELETE /articles/:id(.:format) articles#destroy
+ root GET / welcome#index
```
The `articles_path` helper tells Rails to point the form to the URI Pattern
@@ -590,6 +592,10 @@ familiar error:
You now need to create the `create` action within the `ArticlesController` for
this to work.
+NOTE: By default `form_with` submits forms using Ajax thereby skipping full page
+redirects. To make this guide easier to get into we've disabled that with
+`local: true` for now.
+
### Creating articles
To make the "Unknown action" go away, you can define a `create` action within
@@ -606,9 +612,11 @@ class ArticlesController < ApplicationController
end
```
-If you re-submit the form now, you'll see another familiar error: a template is
-missing. That's ok, we can ignore that for now. What the `create` action should
-be doing is saving our new article to the database.
+If you re-submit the form now, you may not see any change on the page. Don't worry!
+This is because Rails by default returns `204 No Content` response for an action if
+we don't specify what the response should be. We just added the `create` action
+but didn't specify anything about how the response should be. In this case, the
+`create` action should save our new article to the database.
When a form is submitted, the fields of the form are sent to Rails as
_parameters_. These parameters can then be referenced inside the controller
@@ -624,17 +632,16 @@ end
The `render` method here is taking a very simple hash with a key of `:plain` and
value of `params[:article].inspect`. The `params` method is the object which
represents the parameters (or fields) coming in from the form. The `params`
-method returns an `ActiveSupport::HashWithIndifferentAccess` object, which
+method returns an `ActionController::Parameters` object, which
allows you to access the keys of the hash using either strings or symbols. In
this situation, the only parameters that matter are the ones from the form.
TIP: Ensure you have a firm grasp of the `params` method, as you'll use it fairly regularly. Let's consider an example URL: **http://www.example.com/?username=dhh&email=dhh@email.com**. In this URL, `params[:username]` would equal "dhh" and `params[:email]` would equal "dhh@email.com".
-If you re-submit the form one more time you'll now no longer get the missing
-template error. Instead, you'll see something that looks like the following:
+If you re-submit the form one more time, you'll see something that looks like the following:
```ruby
-{"title"=>"First article!", "text"=>"This is my first article."}
+<ActionController::Parameters {"title"=>"First Article!", "text"=>"This is my first article."} permitted: false>
```
This action is now displaying the parameters for the article that are coming in
@@ -649,10 +656,10 @@ Rails developers tend to use when creating new models. To create the new model,
run this command in your terminal:
```bash
-$ bin/rails generate model Article title:string text:text
+$ rails generate model Article title:string text:text
```
-With that command we told Rails that we want a `Article` model, together
+With that command we told Rails that we want an `Article` model, together
with a _title_ attribute of type string, and a _text_ attribute
of type text. Those attributes are automatically added to the `articles`
table in the database and mapped to the `Article` model.
@@ -668,7 +675,7 @@ models, as that will be done automatically by Active Record.
### Running a Migration
-As we've just seen, `bin/rails generate model` created a _database migration_ file
+As we've just seen, `rails generate model` created a _database migration_ file
inside the `db/migrate` directory. Migrations are Ruby classes that are
designed to make it simple to create and modify database tables. Rails uses
rake commands to run migrations, and it's possible to undo a migration after
@@ -679,13 +686,13 @@ If you look in the `db/migrate/YYYYMMDDHHMMSS_create_articles.rb` file
(remember, yours will have a slightly different name), here's what you'll find:
```ruby
-class CreateArticles < ActiveRecord::Migration
+class CreateArticles < ActiveRecord::Migration[5.0]
def change
create_table :articles do |t|
t.string :title
t.text :text
- t.timestamps null: false
+ t.timestamps
end
end
end
@@ -698,13 +705,13 @@ in case you want to reverse it later. When you run this migration it will create
an `articles` table with one string column and a text column. It also creates
two timestamp fields to allow Rails to track article creation and update times.
-TIP: For more information about migrations, refer to [Rails Database Migrations]
-(migrations.html).
+TIP: For more information about migrations, refer to [Active Record Migrations]
+(active_record_migrations.html).
-At this point, you can use a rake command to run the migration:
+At this point, you can use a rails command to run the migration:
```bash
-$ bin/rake db:migrate
+$ rails db:migrate
```
Rails will execute this migration command and tell you it created the Articles
@@ -721,7 +728,7 @@ NOTE. Because you're working in the development environment by default, this
command will apply to the database defined in the `development` section of your
`config/database.yml` file. If you would like to execute migrations in another
environment, for instance in production, you must explicitly pass it when
-invoking the command: `bin/rake db:migrate RAILS_ENV=production`.
+invoking the command: `rails db:migrate RAILS_ENV=production`.
### Saving data in the controller
@@ -766,14 +773,15 @@ Why do you have to bother? The ability to grab and automatically assign all
controller parameters to your model in one shot makes the programmer's job
easier, but this convenience also allows malicious use. What if a request to
the server was crafted to look like a new article form submit but also included
-extra fields with values that violated your applications integrity? They would
+extra fields with values that violated your application's integrity? They would
be 'mass assigned' into your model and then into the database along with the
good stuff - potentially breaking your application or worse.
-We have to whitelist our controller parameters to prevent wrongful mass
+We have to define our permitted controller parameters to prevent wrongful mass
assignment. In this case, we want to both allow and require the `title` and
`text` parameters for valid use of `create`. The syntax for this introduces
-`require` and `permit`. The change will involve one line in the `create` action:
+`require` and `permit`. The change will involve one line in the `create`
+action:
```ruby
@article = Article.new(params.require(:article).permit(:title, :text))
@@ -800,7 +808,7 @@ private
TIP: For more information, refer to the reference above and
[this blog article about Strong Parameters]
-(http://weblog.rubyonrails.org/2012/3/21/strong-parameters/).
+(https://weblog.rubyonrails.org/2012/3/21/strong-parameters/).
### Showing Articles
@@ -808,7 +816,7 @@ If you submit the form again now, Rails will complain about not finding the
`show` action. That's not very useful though, so let's add the `show` action
before proceeding.
-As we have seen in the output of `bin/rake routes`, the route for `show` action is
+As we have seen in the output of `rails routes`, the route for `show` action is
as follows:
```
@@ -825,7 +833,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:
@@ -870,7 +878,7 @@ Visit <http://localhost:3000/articles/new> and give it a try!
### Listing all articles
We still need a way to list all our articles, so let's do that.
-The route for this as per output of `bin/rake routes` is:
+The route for this as per output of `rails routes` is:
```
articles GET /articles(.:format) articles#index
@@ -907,6 +915,7 @@ And then finally, add the view for this action, located at
<tr>
<th>Title</th>
<th>Text</th>
+ <th></th>
</tr>
<% @articles.each do |article| %>
@@ -952,7 +961,7 @@ Now, add another link in `app/views/articles/new.html.erb`, underneath the
form, to go back to the `index` action:
```erb
-<%= form_for :article, url: articles_path do |f| %>
+<%= form_with scope: :article, url: articles_path, local: true do |form| %>
...
<% end %>
@@ -990,21 +999,22 @@ and restart the web server when a change is made.
The model file, `app/models/article.rb` is about as simple as it can get:
```ruby
-class Article < ActiveRecord::Base
+class Article < ApplicationRecord
end
```
There isn't much to this file - but note that the `Article` class inherits from
-`ActiveRecord::Base`. Active Record supplies a great deal of functionality to
-your Rails models for free, including basic database CRUD (Create, Read, Update,
-Destroy) operations, data validation, as well as sophisticated search support
-and the ability to relate multiple models to one another.
+`ApplicationRecord`. `ApplicationRecord` inherits from `ActiveRecord::Base`
+which supplies a great deal of functionality to your Rails models for free,
+including basic database CRUD (Create, Read, Update, Destroy) operations, data
+validation, as well as sophisticated search support and the ability to relate
+multiple models to one another.
Rails includes methods to help you validate the data that you send to models.
Open the `app/models/article.rb` file and edit it:
```ruby
-class Article < ActiveRecord::Base
+class Article < ApplicationRecord
validates :title, presence: true,
length: { minimum: 5 }
end
@@ -1062,7 +1072,7 @@ something went wrong. To do that, you'll modify
`app/views/articles/new.html.erb` to check for error messages:
```html+erb
-<%= form_for :article, url: articles_path do |f| %>
+<%= form_with scope: :article, url: articles_path, local: true do |form| %>
<% if @article.errors.any? %>
<div id="error_explanation">
@@ -1079,17 +1089,17 @@ something went wrong. To do that, you'll modify
<% end %>
<p>
- <%= f.label :title %><br>
- <%= f.text_field :title %>
+ <%= form.label :title %><br>
+ <%= form.text_field :title %>
</p>
<p>
- <%= f.label :text %><br>
- <%= f.text_area :text %>
+ <%= form.label :text %><br>
+ <%= form.text_area :text %>
</p>
<p>
- <%= f.submit %>
+ <%= form.submit %>
</p>
<% end %>
@@ -1110,10 +1120,10 @@ that otherwise `@article` would be `nil` in our view, and calling
`@article.errors.any?` would throw an error.
TIP: Rails automatically wraps fields that contain an error with a div
-with class `field_with_errors`. You can define a css rule to make them
+with class `field_with_errors`. You can define a CSS rule to make them
standout.
-Now you'll get a nice error message when saving an article without title when
+Now you'll get a nice error message when saving an article without a title when
you attempt to do just that on the new article form
<http://localhost:3000/articles/new>:
@@ -1152,9 +1162,9 @@ new articles. Create a file called `app/views/articles/edit.html.erb` and make
it look as follows:
```html+erb
-<h1>Editing article</h1>
+<h1>Edit article</h1>
-<%= form_for :article, url: article_path(@article), method: :patch do |f| %>
+<%= form_with(model: @article, local: true) do |form| %>
<% if @article.errors.any? %>
<div id="error_explanation">
@@ -1171,17 +1181,17 @@ it look as follows:
<% end %>
<p>
- <%= f.label :title %><br>
- <%= f.text_field :title %>
+ <%= form.label :title %><br>
+ <%= form.text_field :title %>
</p>
<p>
- <%= f.label :text %><br>
- <%= f.text_area :text %>
+ <%= form.label :text %><br>
+ <%= form.text_area :text %>
</p>
<p>
- <%= f.submit %>
+ <%= form.submit %>
</p>
<% end %>
@@ -1192,16 +1202,17 @@ 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
-via the `PATCH` HTTP method which is the HTTP method you're expected to use to
-**update** resources according to the REST protocol.
+Passing the article object to the `form_with` method will automatically set the URL for
+submitting the edited article form. This option tells Rails that we want this
+form to be submitted via the `PATCH` HTTP method, which is the HTTP method you're
+expected to use to **update** resources according to the REST protocol.
-The 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.
-More details can be found in [form_for documentation]
-(http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_for).
+Also, passing a model object to `form_with`, like `model: @article` in the edit
+view above, will cause form helpers to fill in form fields with the corresponding
+values of the object. Passing in a symbol scope such as `scope: :article`, as
+was done in the new view, only creates empty form fields.
+More details can be found in [form_with documentation]
+(http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_with).
Next, we need to create the `update` action in
`app/controllers/articles_controller.rb`.
@@ -1298,7 +1309,7 @@ Create a new file `app/views/articles/_form.html.erb` with the following
content:
```html+erb
-<%= form_for @article do |f| %>
+<%= form_with model: @article, local: true do |form| %>
<% if @article.errors.any? %>
<div id="error_explanation">
@@ -1315,29 +1326,29 @@ content:
<% end %>
<p>
- <%= f.label :title %><br>
- <%= f.text_field :title %>
+ <%= form.label :title %><br>
+ <%= form.text_field :title %>
</p>
<p>
- <%= f.label :text %><br>
- <%= f.text_area :text %>
+ <%= form.label :text %><br>
+ <%= form.text_area :text %>
</p>
<p>
- <%= f.submit %>
+ <%= form.submit %>
</p>
<% end %>
```
-Everything except for the `form_for` declaration remained the same.
-The reason we can use this shorter, simpler `form_for` declaration
+Everything except for the `form_with` declaration remained the same.
+The reason we can use this shorter, simpler `form_with` declaration
to stand in for either of the other forms is that `@article` is a *resource*
corresponding to a full set of RESTful routes, and Rails is able to infer
which URI and method to use.
-For more information about this use of `form_for`, see [Resource-oriented style]
-(http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_for-label-Resource-oriented+style).
+For more information about this use of `form_with`, see [Resource-oriented style]
+(http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_with-label-Resource-oriented+style).
Now, let's update the `app/views/articles/new.html.erb` view to use this new
partial, rewriting it completely:
@@ -1364,7 +1375,7 @@ Then do the same for the `app/views/articles/edit.html.erb` view:
We're now ready to cover the "D" part of CRUD, deleting articles from the
database. Following the REST convention, the route for
-deleting articles as per output of `bin/rake routes` is:
+deleting articles as per output of `rails routes` is:
```ruby
DELETE /articles/:id(.:format) articles#destroy
@@ -1484,17 +1495,17 @@ second argument, and then the options as another argument. The `method: :delete`
and `data: { confirm: 'Are you sure?' }` options are used as HTML5 attributes so
that when the link is clicked, Rails will first show a confirm dialog to the
user, and then submit the link with method `delete`. This is done via the
-JavaScript file `jquery_ujs` which is automatically included in your
+JavaScript file `rails-ujs` which is automatically included in your
application's layout (`app/views/layouts/application.html.erb`) when you
generated the application. Without this file, the confirmation dialog box won't
appear.
![Confirm Dialog](images/getting_started/confirm_dialog.png)
-TIP: Learn more about jQuery Unobtrusive Adapter (jQuery UJS) on
+TIP: Learn more about Unobtrusive JavaScript on
[Working With JavaScript in Rails](working_with_javascript_in_rails.html) guide.
-Congratulations, you can now create, show, list, update and destroy
+Congratulations, you can now create, show, list, update, and destroy
articles.
TIP: In general, Rails encourages using resources objects instead of
@@ -1510,11 +1521,11 @@ comments on articles.
### Generating a Model
We're going to see the same generator that we used before when creating
-the `Article` model. This time we'll create a `Comment` model to hold
+the `Article` model. This time we'll create a `Comment` model to hold a
reference to an article. Run this command in your terminal:
```bash
-$ bin/rails generate model Comment commenter:string body:text article:references
+$ rails generate model Comment commenter:string body:text article:references
```
This command will generate four files:
@@ -1529,7 +1540,7 @@ This command will generate four files:
First, take a look at `app/models/comment.rb`:
```ruby
-class Comment < ActiveRecord::Base
+class Comment < ApplicationRecord
belongs_to :article
end
```
@@ -1538,18 +1549,23 @@ This is very similar to the `Article` model that you saw earlier. The difference
is the line `belongs_to :article`, which sets up an Active Record _association_.
You'll learn a little about associations in the next section of this guide.
+The (`:references`) keyword used in the bash command is a special data type for models.
+It creates a new column on your database table with the provided model name appended with an `_id`
+that can hold integer values. To get a better understanding, analyze the
+`db/schema.rb` file after running the migration.
+
In addition to the model, Rails has also made a migration to create the
corresponding database table:
```ruby
-class CreateComments < ActiveRecord::Migration
+class CreateComments < ActiveRecord::Migration[5.0]
def change
create_table :comments do |t|
t.string :commenter
t.text :body
- t.references :article, index: true, foreign_key: true
+ t.references :article, foreign_key: true
- t.timestamps null: false
+ t.timestamps
end
end
end
@@ -1560,7 +1576,7 @@ for it, and a foreign key constraint that points to the `id` column of the `arti
table. Go ahead and run the migration:
```bash
-$ bin/rake db:migrate
+$ rails db:migrate
```
Rails is smart enough to only execute the migrations that have not already been
@@ -1587,7 +1603,7 @@ association. You've already seen the line of code inside the `Comment` model
(app/models/comment.rb) that makes each comment belong to an Article:
```ruby
-class Comment < ActiveRecord::Base
+class Comment < ApplicationRecord
belongs_to :article
end
```
@@ -1596,7 +1612,7 @@ You'll need to edit `app/models/article.rb` to add the other side of the
association:
```ruby
-class Article < ActiveRecord::Base
+class Article < ApplicationRecord
has_many :comments
validates :title, presence: true,
length: { minimum: 5 }
@@ -1636,7 +1652,7 @@ With the model in hand, you can turn your attention to creating a matching
controller. Again, we'll use the same generator we used before:
```bash
-$ bin/rails generate controller Comments
+$ rails generate controller Comments
```
This creates five files and one empty directory:
@@ -1647,8 +1663,7 @@ This creates five files and one empty directory:
| app/views/comments/ | Views of the controller are stored here |
| test/controllers/comments_controller_test.rb | The test for the controller |
| app/helpers/comments_helper.rb | A view helper file |
-| app/assets/javascripts/comment.coffee | CoffeeScript for the controller |
-| app/assets/stylesheets/comment.scss | Cascading style sheet for the controller |
+| app/assets/stylesheets/comments.scss | Cascading style sheet for the controller |
Like with any blog, our readers will create their comments directly after
reading the article, and once they have added their comment, will be sent back
@@ -1671,17 +1686,17 @@ So first, we'll wire up the Article show template
</p>
<h2>Add a comment:</h2>
-<%= form_for([@article, @article.comments.build]) do |f| %>
+<%= form_with(model: [ @article, @article.comments.build ], local: true) do |form| %>
<p>
- <%= f.label :commenter %><br>
- <%= f.text_field :commenter %>
+ <%= form.label :commenter %><br>
+ <%= form.text_field :commenter %>
</p>
<p>
- <%= f.label :body %><br>
- <%= f.text_area :body %>
+ <%= form.label :body %><br>
+ <%= form.text_area :body %>
</p>
<p>
- <%= f.submit %>
+ <%= form.submit %>
</p>
<% end %>
@@ -1690,7 +1705,7 @@ So first, we'll wire up the Article show template
```
This adds a form on the `Article` show page that creates a new comment by
-calling the `CommentsController` `create` action. The `form_for` call here uses
+calling the `CommentsController` `create` action. The `form_with` call here uses
an array, which will build a nested route, such as `/articles/1/comments`.
Let's wire up the `create` in `app/controllers/comments_controller.rb`:
@@ -1752,17 +1767,17 @@ add that to the `app/views/articles/show.html.erb`.
<% end %>
<h2>Add a comment:</h2>
-<%= form_for([@article, @article.comments.build]) do |f| %>
+<%= form_with(model: [ @article, @article.comments.build ], local: true) do |form| %>
<p>
- <%= f.label :commenter %><br>
- <%= f.text_field :commenter %>
+ <%= form.label :commenter %><br>
+ <%= form.text_field :commenter %>
</p>
<p>
- <%= f.label :body %><br>
- <%= f.text_area :body %>
+ <%= form.label :body %><br>
+ <%= form.text_area :body %>
</p>
<p>
- <%= f.submit %>
+ <%= form.submit %>
</p>
<% end %>
@@ -1818,17 +1833,17 @@ following:
<%= render @article.comments %>
<h2>Add a comment:</h2>
-<%= form_for([@article, @article.comments.build]) do |f| %>
+<%= form_with(model: [ @article, @article.comments.build ], local: true) do |form| %>
<p>
- <%= f.label :commenter %><br>
- <%= f.text_field :commenter %>
+ <%= form.label :commenter %><br>
+ <%= form.text_field :commenter %>
</p>
<p>
- <%= f.label :body %><br>
- <%= f.text_area :body %>
+ <%= form.label :body %><br>
+ <%= form.text_area :body %>
</p>
<p>
- <%= f.submit %>
+ <%= form.submit %>
</p>
<% end %>
@@ -1840,7 +1855,7 @@ This will now render the partial in `app/views/comments/_comment.html.erb` once
for each comment that is in the `@article.comments` collection. As the `render`
method iterates over the `@article.comments` collection, it assigns each
comment to a local variable named the same as the partial, in this case
-`comment` which is then available in the partial for us to show.
+`comment`, which is then available in the partial for us to show.
### Rendering a Partial Form
@@ -1848,17 +1863,17 @@ Let us also move that new comment section out to its own partial. Again, you
create a file `app/views/comments/_form.html.erb` containing:
```html+erb
-<%= form_for([@article, @article.comments.build]) do |f| %>
+<%= form_with(model: [ @article, @article.comments.build ], local: true) do |form| %>
<p>
- <%= f.label :commenter %><br>
- <%= f.text_field :commenter %>
+ <%= form.label :commenter %><br>
+ <%= form.text_field :commenter %>
</p>
<p>
- <%= f.label :body %><br>
- <%= f.text_area :body %>
+ <%= form.label :body %><br>
+ <%= form.text_area :body %>
</p>
<p>
- <%= f.submit %>
+ <%= form.submit %>
</p>
<% end %>
```
@@ -1962,7 +1977,7 @@ you to use the `dependent` option of an association to achieve this. Modify the
Article model, `app/models/article.rb`, as follows:
```ruby
-class Article < ActiveRecord::Base
+class Article < ApplicationRecord
has_many :comments, dependent: :destroy
validates :title, presence: true,
length: { minimum: 5 }
@@ -2043,13 +2058,13 @@ What's Next?
Now that you've seen your first Rails application, you should feel free to
update it and experiment on your own.
-Remember you don't have to do everything without help. As you need assistance
+Remember, you don't have to do everything without help. As you need assistance
getting up and running with Rails, feel free to consult these support
resources:
* The [Ruby on Rails Guides](index.html)
-* The [Ruby on Rails Tutorial](http://railstutorial.org/book)
-* The [Ruby on Rails mailing list](http://groups.google.com/group/rubyonrails-talk)
+* The [Ruby on Rails Tutorial](https://www.railstutorial.org/book)
+* The [Ruby on Rails mailing list](https://groups.google.com/group/rubyonrails-talk)
* The [#rubyonrails](irc://irc.freenode.net/#rubyonrails) channel on irc.freenode.net
diff --git a/guides/source/i18n.md b/guides/source/i18n.md
index 272a0e3623..10b1a6de7e 100644
--- a/guides/source/i18n.md
+++ b/guides/source/i18n.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Rails Internationalization (I18n) API
=====================================
@@ -11,7 +11,7 @@ So, in the process of _internationalizing_ your Rails application you have to:
* Ensure you have support for i18n.
* Tell Rails where to find locale dictionaries.
-* Tell Rails how to set, preserve and switch locales.
+* Tell Rails how to set, preserve, and switch locales.
In the process of _localizing_ your application you'll probably want to do the following three things:
@@ -25,7 +25,7 @@ After reading this guide, you will know:
* How I18n works in Ruby on Rails
* How to correctly use I18n into a RESTful application in various ways
-* How to use I18n to translate ActiveRecord errors or ActionMailer E-mail subjects
+* How to use I18n to translate Active Record errors or Action Mailer E-mail subjects
* Some other tools to go further with the translation process of your application
--------------------------------------------------------------------------------
@@ -42,6 +42,8 @@ Internationalization is a complex problem. Natural languages differ in so many w
As part of this solution, **every static string in the Rails framework** - e.g. Active Record validation messages, time and date formats - **has been internationalized**. _Localization_ of a Rails application means defining translated values for these strings in desired languages.
+To localize store and update _content_ in your application (e.g. translate blog posts), see the [Translating model content](#translating-model-content) section.
+
### The Overall Architecture of the Library
Thus, the Ruby I18n gem is split into two parts:
@@ -51,7 +53,7 @@ Thus, the Ruby I18n gem is split into two parts:
As a user you should always only access the public methods on the I18n module, but it is useful to know about the capabilities of the backend.
-NOTE: It is possible (or even desirable) to swap the shipped Simple backend with a more powerful one, which would store translation data in a relational database, GetText dictionary, or similar. See section [Using different backends](#using-different-backends) below.
+NOTE: It is possible to swap the shipped Simple backend with a more powerful one, which would store translation data in a relational database, GetText dictionary, or similar. See section [Using different backends](#using-different-backends) below.
### The Public I18n API
@@ -72,11 +74,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 # Permitted locales available for the application
+enforce_available_locales # Enforce locale permission (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!
@@ -103,21 +107,20 @@ This means, that in the `:en` locale, the key _hello_ will map to the _Hello wor
The I18n library will use **English** as a **default locale**, i.e. if a different locale is not set, `:en` will be used for looking up translations.
-NOTE: The i18n library takes a **pragmatic approach** to locale keys (after [some discussion](http://groups.google.com/group/rails-i18n/browse_thread/thread/14dede2c7dbe9470/80eec34395f64f3c?hl=en)), including only the _locale_ ("language") part, like `:en`, `:pl`, not the _region_ part, like `:en-US` or `:en-GB`, which are traditionally used for separating "languages" and "regional setting" or "dialects". Many international applications use only the "language" element of a locale such as `:cs`, `:th` or `:es` (for Czech, Thai and Spanish). However, there are also regional differences within different language groups that may be important. For instance, in the `:en-US` locale you would have $ as a currency symbol, while in `:en-GB`, you would have £. Nothing stops you from separating regional and other settings in this way: you just have to provide full "English - United Kingdom" locale in a `:en-GB` dictionary. Few gems such as [Globalize3](https://github.com/globalize/globalize) may help you implement it.
+NOTE: The i18n library takes a **pragmatic approach** to locale keys (after [some discussion](https://groups.google.com/forum/#!topic/rails-i18n/FN7eLH2-lHA)), including only the _locale_ ("language") part, like `:en`, `:pl`, not the _region_ part, like `:en-US` or `:en-GB`, which are traditionally used for separating "languages" and "regional setting" or "dialects". Many international applications use only the "language" element of a locale such as `:cs`, `:th`, or `:es` (for Czech, Thai, and Spanish). However, there are also regional differences within different language groups that may be important. For instance, in the `:en-US` locale you would have $ as a currency symbol, while in `:en-GB`, you would have £. Nothing stops you from separating regional and other settings in this way: you just have to provide full "English - United Kingdom" locale in a `:en-GB` dictionary.
The **translations load path** (`I18n.load_path`) is an array of paths to files that will be loaded automatically. Configuring this path allows for customization of translations directory structure and file naming scheme.
NOTE: The backend lazy-loads these translations when a translation is looked up for the first time. This backend can be swapped with something else even after translations have already been announced.
-The default `application.rb` file has instructions on how to add locales from another directory and how to set a different default locale.
+You can change the default locale as well as configure the translations load paths in `config/application.rb` as follows:
```ruby
-# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
-# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
-# config.i18n.default_locale = :de
+ config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}')]
+ config.i18n.default_locale = :de
```
-The load path must be specified before any translations are looked up. To change the default locale from an initializer instead of `application.rb`:
+The load path must be specified before any translations are looked up. To change the default locale from an initializer instead of `config/application.rb`:
```ruby
# config/initializers/locale.rb
@@ -125,23 +128,29 @@ 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}')]
+# Permitted locales available for the application
+I18n.available_locales = [:en, :pt]
+
# Set default locale to something other than :en
I18n.default_locale = :pt
```
+Note that appending directly to `I18n.load_paths` instead of to the application's configured i18n will _not_ override translations from external gems.
+
### Managing the Locale across Requests
The default locale is used for all translations unless `I18n.locale` is explicitly set.
A localized application will likely need to provide support for multiple locales. To accomplish this, the locale should be set at the beginning of each request so that all strings are translated using the desired locale during the lifetime of that request.
-The locale can be set in a `before_action` in the `ApplicationController`:
+The locale can be set in an `around_action` in the `ApplicationController`:
```ruby
-before_action :set_locale
+around_action :switch_locale
-def set_locale
- I18n.locale = params[:locale] || I18n.default_locale
+def switch_locale(&action)
+ locale = params[:locale] || I18n.default_locale
+ I18n.with_locale(locale, &action)
end
```
@@ -161,13 +170,14 @@ One option you have is to set the locale from the domain name where your applica
You can implement it like this in your `ApplicationController`:
```ruby
-before_action :set_locale
+around_action :switch_locale
-def set_locale
- I18n.locale = extract_locale_from_tld || I18n.default_locale
+def switch_locale(&action)
+ locale = extract_locale_from_tld || I18n.default_locale
+ I18n.with_locale(locale, &action)
end
-# Get locale from top-level domain or return nil if such locale is not available
+# Get locale from top-level domain or return +nil+ if such locale is not available
# You have to put something like:
# 127.0.0.1 application.com
# 127.0.0.1 application.it
@@ -204,7 +214,7 @@ This solution has aforementioned advantages, however, you may not be able or may
#### Setting the Locale from URL Params
-The most usual way of setting (and passing) the locale would be to include it in URL params, as we did in the `I18n.locale = params[:locale]` _before_action_ in the first example. We would like to have URLs like `www.example.com/books?locale=ja` or `www.example.com/ja/books` in this case.
+The most usual way of setting (and passing) the locale would be to include it in URL params, as we did in the `I18n.with_locale(params[:locale], &action)` _around_action_ in the first example. We would like to have URLs like `www.example.com/books?locale=ja` or `www.example.com/ja/books` in this case.
This approach has almost the same set of advantages as setting the locale from the domain name: namely that it's RESTful and in accord with the rest of the World Wide Web. It does require a little bit more work to implement, though.
@@ -249,7 +259,7 @@ end
With this approach you will not get a `Routing Error` when accessing your resources such as `http://localhost:3001/books` without a locale. This is useful for when you want to use the default locale when one is not specified.
-Of course, you need to take special care of the root URL (usually "homepage" or "dashboard") of your application. An URL like `http://localhost:3001/nl` will not work automatically, because the `root to: "books#index"` declaration in your `routes.rb` doesn't take locale into account. (And rightly so: there's only one "root" URL.)
+Of course, you need to take special care of the root URL (usually "homepage" or "dashboard") of your application. A URL like `http://localhost:3001/nl` will not work automatically, because the `root to: "books#index"` declaration in your `routes.rb` doesn't take locale into account. (And rightly so: there's only one "root" URL.)
You would probably need to map URLs like these:
@@ -267,8 +277,11 @@ NOTE: Have a look at various gems which simplify working with routes: [routing_f
An application with authenticated users may allow users to set a locale preference through the application's interface. With this approach, a user's selected locale preference is persisted in the database and used to set the locale for authenticated requests by that user.
```ruby
-def set_locale
- I18n.locale = current_user.try(:locale) || I18n.default_locale
+around_action :switch_locale
+
+def switch_locale(&action)
+ locale = current_user.try(:locale) || I18n.default_locale
+ I18n.with_locale(locale, &action)
end
```
@@ -283,10 +296,11 @@ The `Accept-Language` HTTP header indicates the preferred language for request's
A trivial implementation of using an `Accept-Language` header would be:
```ruby
-def set_locale
+def switch_locale(&action)
logger.debug "* Accept-Language: #{request.env['HTTP_ACCEPT_LANGUAGE']}"
- I18n.locale = extract_locale_from_accept_language_header
+ locale = extract_locale_from_accept_language_header
logger.debug "* Locale set to '#{I18n.locale}'"
+ I18n.with_locale(locale, &action)
end
private
@@ -306,10 +320,10 @@ In general, this approach is far less reliable than using the language header an
#### Storing the Locale from the Session or Cookies
-WARNING: You may be tempted to store the chosen locale in a _session_ or a *cookie*. However, **do not do this**. The locale should be transparent and a part of the URL. This way you won't break people's basic assumptions about the web itself: if you send a URL to a friend, they should see the same page and content as you. A fancy word for this would be that you're being [*RESTful*](http://en.wikipedia.org/wiki/Representational_State_Transfer). Read more about the RESTful approach in [Stefan Tilkov's articles](http://www.infoq.com/articles/rest-introduction). Sometimes there are exceptions to this rule and those are discussed below.
+WARNING: You may be tempted to store the chosen locale in a _session_ or a *cookie*. However, **do not do this**. The locale should be transparent and a part of the URL. This way you won't break people's basic assumptions about the web itself: if you send a URL to a friend, they should see the same page and content as you. A fancy word for this would be that you're being [*RESTful*](https://en.wikipedia.org/wiki/Representational_State_Transfer). Read more about the RESTful approach in [Stefan Tilkov's articles](https://www.infoq.com/articles/rest-introduction). Sometimes there are exceptions to this rule and those are discussed below.
Internationalization and Localization
------------------------------------
+-------------------------------------
OK! Now you've initialized I18n support for your Ruby on Rails application and told it which locale to use and how to preserve it between requests.
@@ -327,10 +341,12 @@ end
```ruby
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
- before_action :set_locale
- def set_locale
- I18n.locale = params[:locale] || I18n.default_locale
+ around_action :switch_locale
+
+ def switch_locale(&action)
+ locale = params[:locale] || I18n.default_locale
+ I18n.with_locale(locale, &action)
end
end
```
@@ -367,7 +383,7 @@ end
```html+erb
# app/views/home/index.html.erb
-<h1><%=t :hello_world %></h1>
+<h1><%= t :hello_world %></h1>
<p><%= flash[:notice] %></p>
```
@@ -405,6 +421,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:
+
+```yaml
+# 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
@@ -469,8 +514,8 @@ OK! Now let's add a timestamp to the view, so we can demo the **date/time locali
```erb
# app/views/home/index.html.erb
-<h1><%=t :hello_world %></h1>
-<p><%= flash[:notice] %></p
+<h1><%= t :hello_world %></h1>
+<p><%= flash[:notice] %></p>
<p><%= l Time.now, format: :short %></p>
```
@@ -561,7 +606,7 @@ Covered are features like these:
### Looking up Translations
-#### Basic Lookup, Scopes and Nested Keys
+#### Basic Lookup, Scopes, and Nested Keys
Translations are looked up by keys which can be both Symbols or Strings, so these calls are equivalent:
@@ -627,6 +672,26 @@ I18n.t 'activerecord.errors.messages'
# => {:inclusion=>"is not included in the list", :exclusion=> ... }
```
+If you want to perform interpolation on a bulk hash of translations, you need to pass `deep_interpolation: true` as a parameter. When you have the following dictionary:
+
+```yaml
+en:
+ welcome:
+ title: "Welcome!"
+ content: "Welcome to the %{app_name}"
+```
+
+then the nested interpolation will be ignored without the setting:
+
+```ruby
+I18n.t 'welcome', app_name: 'book store'
+# => {:title=>"Welcome!", :content=>"Welcome to the %{app_name}"}
+
+I18n.t 'welcome', deep_interpolation: true, app_name: 'book store'
+# => {:title=>"Welcome!", :content=>"Welcome to the book store"}
+```
+
+
#### "Lazy" Lookup
Rails implements a convenient way to look up the locale inside _views_. When you have the following dictionary:
@@ -668,12 +733,15 @@ 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 many languages — including English — there are only two forms, a singular and a plural, 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:
+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 in the
+pluralization backend. By default, only the English pluralization rules are applied.
```ruby
I18n.backend.store_translations :en, inbox: {
+ zero: 'no messages', # optional
one: 'one message',
other: '%{count} messages'
}
@@ -682,18 +750,39 @@ 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.
+#### Locale-specific rules
+
+The I18n gem provides a Pluralization backend that can be used to enable locale-specific rules. Include it
+to the Simple backend, then add the localized pluralization algorithms to translation store, as `i18n.plural.rule`.
+
+```ruby
+I18n::Backend::Simple.include(I18n::Backend::Pluralization)
+I18n.backend.store_translations :pt, i18n: { plural: { rule: lambda { |n| [0, 1].include?(n) ? :one : :other } } }
+I18n.backend.store_translations :pt, apples: { one: 'one or none', other: 'more than one' }
+
+I18n.t :apples, count: 0, locale: :pt
+# => 'one or none'
+```
+
+Alternatively, the separate gem [rails-i18n](https://github.com/svenfuchs/rails-i18n) can be used to provide a fuller set of locale-specific pluralization rules.
+
### Setting and Passing a Locale
The locale can be either set pseudo-globally to `I18n.locale` (which uses `Thread.current` like, e.g., `Time.zone`) or can be passed as an option to `#translate` and `#localize`.
@@ -770,14 +859,14 @@ For example when you add the following translations:
en:
activerecord:
models:
- user: Dude
+ user: Customer
attributes:
user:
login: "Handle"
# will translate User attribute "login" as "Handle"
```
-Then `User.model_name.human` will return "Dude" and `User.human_attribute_name("login")` will return "Handle".
+Then `User.model_name.human` will return "Customer" and `User.human_attribute_name("login")` will return "Handle".
You can also set a plural form for model names, adding as following:
@@ -786,11 +875,11 @@ en:
activerecord:
models:
user:
- one: Dude
- other: Dudes
+ one: Customer
+ other: Customers
```
-Then `User.model_name.human(count: 2)` will return "Dudes". With `count: 1` or without params will return "Dude".
+Then `User.model_name.human(count: 2)` will return "Customers". With `count: 1` or without params will return "Customer".
In the event you need to access nested attributes within a given model, you should nest these under `model/attribute` at the model level of your translation file:
@@ -798,12 +887,14 @@ In the event you need to access nested attributes within a given model, you shou
en:
activerecord:
attributes:
- user/gender:
- female: "Female"
- male: "Male"
+ user/role:
+ admin: "Admin"
+ contributor: "Contributor"
```
-Then `User.human_attribute_name("gender.female")` will return "Female".
+Then `User.human_attribute_name("role.admin")` will return "Admin".
+
+NOTE: If you are using a class which includes `ActiveModel` and does not inherit from `ActiveRecord::Base`, replace `activerecord` with `activemodel` in the above key paths.
#### Error Message Scopes
@@ -814,7 +905,7 @@ This gives you quite powerful means to flexibly adjust your messages to your app
Consider a User model with a validation for the name attribute like this:
```ruby
-class User < ActiveRecord::Base
+class User < ApplicationRecord
validates :name, presence: true
end
```
@@ -865,7 +956,7 @@ This way you can provide special translations for various error messages at diff
#### Error Message Interpolation
-The translated model name, translated attribute name, and value are always available for interpolation.
+The translated model name, translated attribute name, and value are always available for interpolation as `model`, `attribute` and `value` respectively.
So, for example, instead of the default error message `"cannot be blank"` you could use the attribute name like this : `"Please fill in your %{attribute}"`.
@@ -887,6 +978,7 @@ So, for example, instead of the default error message `"cannot be blank"` you co
| inclusion | - | :inclusion | - |
| exclusion | - | :exclusion | - |
| associated | - | :invalid | - |
+| non-optional association | - | :required | - |
| numericality | - | :not_a_number | - |
| numericality | :greater_than | :greater_than | count |
| numericality | :greater_than_or_equal_to | :greater_than_or_equal_to | count |
@@ -917,7 +1009,7 @@ en:
```
NOTE: In order to use this helper, you need to install [DynamicForm](https://github.com/joelmoss/dynamic_form)
-gem by adding this line to your Gemfile: `gem 'dynamic_form'`.
+gem by adding this line to your `Gemfile`: `gem 'dynamic_form'`.
### Translations for Action Mailer E-Mail Subjects
@@ -990,7 +1082,7 @@ The Simple backend shipped with Active Support allows you to store translations
For example a Ruby Hash providing translations can look like this:
-```yaml
+```ruby
{
pt: {
foo: {
@@ -1039,13 +1131,11 @@ Customize your I18n Setup
For several reasons the Simple backend shipped with Active Support only does the "simplest thing that could possibly work" _for Ruby on Rails_[^3] ... which means that it is only guaranteed to work for English and, as a side effect, languages that are very similar to English. Also, the simple backend is only capable of reading translations but cannot dynamically store them to any format.
-That does not mean you're stuck with these limitations, though. The Ruby I18n gem makes it very easy to exchange the Simple backend implementation with something else that fits better for your needs. E.g. you could exchange it with Globalize's Static backend:
+That does not mean you're stuck with these limitations, though. The Ruby I18n gem makes it very easy to exchange the Simple backend implementation with something else that fits better for your needs, by passing a backend instance to the `I18n.backend=` setter.
-```ruby
-I18n.backend = Globalize::Backend::Static.new
-```
+For example, you can replace the Simple backend with the Chain backend to chain multiple backends together. This is useful when you want to use standard translations with a Simple backend but store custom application translations in a database or other backends.
-You can also use the Chain backend to chain multiple backends together. This is useful when you want to use standard translations with a Simple backend but store custom application translations in a database or other backends. For example, you could use the Active Record backend and fall back to the (default) Simple backend:
+With the Chain backend, you could use the Active Record backend and fall back to the (default) Simple backend:
```ruby
I18n.backend = I18n::Backend::Chain.new(I18n::Backend::ActiveRecord.new, I18n.backend)
@@ -1106,32 +1196,39 @@ To do so, the helper forces `I18n#translate` to raise exceptions no matter what
I18n.t :foo, raise: true # always re-raises exceptions from the backend
```
+Translating Model Content
+-------------------------
+
+The I18n API described in this guide is primarily intended for translating interface strings. If you are looking to translate model content (e.g. blog posts), you will need a different solution to help with this.
+
+Several gems can help with this:
+
+* [Globalize](https://github.com/globalize/globalize): Store translations on separate translation tables, one for each translated model
+* [Mobility](https://github.com/shioyama/mobility): Provides support for storing translations in many formats, including translation tables, json columns (Postgres), etc.
+* [Traco](https://github.com/barsoom/traco): Translatable columns for Rails 3 and 4, stored in the model table itself
+
Conclusion
----------
At this point you should have a good overview about how I18n support in Ruby on Rails works and are ready to start translating your project.
-If you find anything missing or wrong in this guide, please file a ticket on our [issue tracker](http://i18n.lighthouseapp.com/projects/14948-rails-i18n/overview). If you want to discuss certain portions or have questions, please sign up to our [mailing list](http://groups.google.com/group/rails-i18n).
-
Contributing to Rails I18n
--------------------------
I18n support in Ruby on Rails was introduced in the release 2.2 and is still evolving. The project follows the good Ruby on Rails development tradition of evolving solutions in gems and real applications first, and only then cherry-picking the best-of-breed of most widely useful features for inclusion in the core.
-Thus we encourage everybody to experiment with new ideas and features in gems or other libraries and make them available to the community. (Don't forget to announce your work on our [mailing list](http://groups.google.com/group/rails-i18n!))
+Thus we encourage everybody to experiment with new ideas and features in gems or other libraries and make them available to the community. (Don't forget to announce your work on our [mailing list](https://groups.google.com/forum/#!forum/rails-i18n)!)
-If you find your own locale (language) missing from our [example translations data](https://github.com/svenfuchs/rails-i18n/tree/master/rails/locale) repository for Ruby on Rails, please [_fork_](https://github.com/guides/fork-a-project-and-submit-your-modifications) the repository, add your data and send a [pull request](https://github.com/guides/pull-requests).
+If you find your own locale (language) missing from our [example translations data](https://github.com/svenfuchs/rails-i18n/tree/master/rails/locale) repository for Ruby on Rails, please [_fork_](https://github.com/guides/fork-a-project-and-submit-your-modifications) the repository, add your data, and send a [pull request](https://help.github.com/articles/about-pull-requests/).
Resources
---------
-* [Google group: rails-i18n](http://groups.google.com/group/rails-i18n) - The project's mailing list.
-* [GitHub: rails-i18n](https://github.com/svenfuchs/rails-i18n/tree/master) - Code repository for the rails-i18n project. Most importantly you can find lots of [example translations](https://github.com/svenfuchs/rails-i18n/tree/master/rails/locale) for Rails that should work for your application in most cases.
-* [GitHub: i18n](https://github.com/svenfuchs/i18n/tree/master) - Code repository for the i18n gem.
-* [Lighthouse: rails-i18n](http://i18n.lighthouseapp.com/projects/14948-rails-i18n/overview) - Issue tracker for the rails-i18n project.
-* [Lighthouse: i18n](http://i18n.lighthouseapp.com/projects/14947-ruby-i18n/overview) - Issue tracker for the i18n gem.
+* [Google group: rails-i18n](https://groups.google.com/forum/#!forum/rails-i18n) - The project's mailing list.
+* [GitHub: rails-i18n](https://github.com/svenfuchs/rails-i18n) - Code repository and issue tracker for the rails-i18n project. Most importantly you can find lots of [example translations](https://github.com/svenfuchs/rails-i18n/tree/master/rails/locale) for Rails that should work for your application in most cases.
+* [GitHub: i18n](https://github.com/svenfuchs/i18n) - Code repository and issue tracker for the i18n gem.
Authors
@@ -1143,7 +1240,7 @@ Authors
Footnotes
---------
-[^1]: Or, to quote [Wikipedia](http://en.wikipedia.org/wiki/Internationalization_and_localization): _"Internationalization is the process of designing a software application so that it can be adapted to various languages and regions without engineering changes. Localization is the process of adapting software for a specific region or language by adding locale-specific components and translating text."_
+[^1]: Or, to quote [Wikipedia](https://en.wikipedia.org/wiki/Internationalization_and_localization): _"Internationalization is the process of designing a software application so that it can be adapted to various languages and regions without engineering changes. Localization is the process of adapting software for a specific region or language by adding locale-specific components and translating text."_
[^2]: Other backends might allow or require to use other formats, e.g. a GetText backend might allow to read GetText files.
diff --git a/guides/source/index.html.erb b/guides/source/index.html.erb
index 2fdf18a2e9..76f01fea0a 100644
--- a/guides/source/index.html.erb
+++ b/guides/source/index.html.erb
@@ -10,7 +10,9 @@ Ruby on Rails Guides
<div id="subCol">
<dl>
<dt></dt>
- <dd class="kindle">Rails Guides are also available for <%= link_to 'Kindle', @mobi %>.</dd>
+ <% unless @edge -%>
+ <dd class="kindle">Rails Guides are also available for <%= link_to 'Kindle', @mobi %>.</dd>
+ <% end -%>
<dd class="work-in-progress">Guides marked with this icon are currently being worked on and will not be available in the Guides Index menu. While still useful, they may contain incomplete information and even errors. You can help by reviewing them and posting your comments and corrections.</dd>
</dl>
</div>
diff --git a/guides/source/initialization.md b/guides/source/initialization.md
index 43083ebb86..c41eae18cf 100644
--- a/guides/source/initialization.md
+++ b/guides/source/initialization.md
@@ -1,10 +1,10 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
The Rails Initialization Process
================================
-This guide explains the internals of the initialization process in Rails
-as of Rails 4. It is an extremely in-depth guide and recommended for advanced Rails developers.
+This guide explains the internals of the initialization process in Rails.
+It is an extremely in-depth guide and recommended for advanced Rails developers.
After reading this guide, you will know:
@@ -16,7 +16,7 @@ After reading this guide, you will know:
--------------------------------------------------------------------------------
This guide goes through every method call that is
-required to boot up the Ruby on Rails stack for a default Rails 4
+required to boot up the Ruby on Rails stack for a default Rails
application, explaining each part in detail along the way. For this
guide, we will be focusing on what happens when you execute `rails server`
to boot your app.
@@ -45,7 +45,7 @@ load Gem.bin_path('railties', 'rails', version)
```
If you try out this command in a Rails console, you would see that this loads
-`railties/exe/rails`. A part of the file `railties/exe/rails.rb` has the
+`railties/exe/rails`. A part of the file `railties/exe/rails` has the
following code:
```ruby
@@ -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,37 +86,36 @@ The `APP_PATH` constant will be used later in `rails/commands`. The `config/boot
`config/boot.rb` contains:
```ruby
-# Set up gems listed in the Gemfile.
-ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
-require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
+require 'bundler/setup' # Set up gems listed in the Gemfile.
```
In a standard Rails application, there's a `Gemfile` which declares all
dependencies of the application. `config/boot.rb` sets
-`ENV['BUNDLE_GEMFILE']` to the location of this file. If the Gemfile
+`ENV['BUNDLE_GEMFILE']` to the location of this file. If the `Gemfile`
exists, then `bundler/setup` is required. The require is used by Bundler to
configure the load path for your Gemfile's dependencies.
A standard Rails application depends on several gems, specifically:
+* actioncable
* actionmailer
* actionpack
* actionview
+* activejob
* activemodel
* activerecord
+* activestorage
* activesupport
-* activejob
* arel
* builder
* bundler
-* erubis
+* erubi
* i18n
* mail
* mime-types
* rack
-* rack-cache
-* rack-mount
* rack-test
* rails
* railties
@@ -132,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_relative "command"
aliases = {
"g" => "generate",
@@ -140,38 +139,44 @@ aliases = {
"c" => "console",
"s" => "server",
"db" => "dbconsole",
- "r" => "runner"
+ "r" => "runner",
+ "t" => "test"
}
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/command_tasks.rb`
+### `rails/command.rb`
-When one types an incorrect rails command, the `run_command` is responsible for
-throwing an error message. If the command is valid, a method of the same name
-is called.
+When one types a Rails command, `invoke` tries to lookup a command for the given
+namespace and executes the command if found.
+
+If Rails doesn't recognize the command, it hands the reins over to Rake
+to run a task of the same name.
+
+As shown, `Rails::Command` displays the help output automatically if the `args`
+are empty.
```ruby
-COMMAND_WHITELIST = %w(plugin generate destroy console server dbconsole application runner new version help)
+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
-def run_command!(command)
- command = parse_command(command)
- if COMMAND_WHITELIST.include?(command)
- send(command)
- else
- write_error_message(command)
+ if command = find_by_namespace(namespace)
+ command.perform(namespace, args, config)
+ else
+ find_by_namespace("rake").perform(namespace, args, config)
+ end
+ end
end
end
```
@@ -179,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(*)
@@ -251,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
@@ -288,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
```
@@ -310,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 [mongrel, 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`
@@ -344,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
@@ -351,12 +347,9 @@ def start
end
private
-
def print_boot_information
...
puts "=> Run `rails server -h` for more startup options"
- ...
- puts "=> Ctrl-C to shutdown server" unless options[:daemonize]
end
def create_tmp_directories
@@ -365,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`.
@@ -465,7 +467,7 @@ The `options[:config]` value defaults to `config.ru` which contains this:
```ruby
# This file is used by Rack-based servers to start the application.
-require ::File.expand_path('../config/environment', __FILE__)
+require_relative 'config/environment'
run <%= app_const %>
```
@@ -486,7 +488,7 @@ end
The `initialize` method of `Rack::Builder` will take the block here and execute it within an instance of `Rack::Builder`. This is where the majority of the initialization process of Rails happens. The `require` line for `config/environment.rb` in `config.ru` is the first to run:
```ruby
-require ::File.expand_path('../config/environment', __FILE__)
+require_relative 'config/environment'
```
### `config/environment.rb`
@@ -496,7 +498,7 @@ This file is the common file required by `config.ru` (`rails server`) and Passen
This file begins with requiring `config/application.rb`:
```ruby
-require File.expand_path('../application', __FILE__)
+require_relative 'application'
```
### `config/application.rb`
@@ -504,7 +506,7 @@ require File.expand_path('../application', __FILE__)
This file requires `config/boot.rb`:
```ruby
-require File.expand_path('../boot', __FILE__)
+require_relative 'boot'
```
But only if it hasn't been required before, which would be the case in `rails server`
@@ -529,16 +531,18 @@ This file is responsible for requiring all the individual frameworks of Rails:
require "rails"
%w(
- active_record
- action_controller
- action_view
- action_mailer
- active_job
- rails/test_unit
- sprockets
-).each do |framework|
+ active_record/railtie
+ active_storage/engine
+ action_controller/railtie
+ action_view/railtie
+ action_mailer/railtie
+ active_job/railtie
+ action_cable/engine
+ rails/test_unit/railtie
+ sprockets/railtie
+).each do |railtie|
begin
- require "#{framework}/railtie"
+ require railtie
rescue LoadError
end
end
@@ -663,7 +667,7 @@ DEFAULT_OPTIONS = {
}
def self.run(app, options = {})
- options = DEFAULT_OPTIONS.merge(options)
+ options = DEFAULT_OPTIONS.merge(options)
if options[:Verbose]
app = Rack::CommonLogger.new(app, STDOUT)
diff --git a/guides/source/kindle/layout.html.erb b/guides/source/kindle/layout.html.erb
index f0a286210b..fd8746776b 100644
--- a/guides/source/kindle/layout.html.erb
+++ b/guides/source/kindle/layout.html.erb
@@ -14,12 +14,12 @@
<% if content_for? :header_section %>
<%= yield :header_section %>
- <div class="pagebreak">
+ <div class="pagebreak"></div>
<% end %>
<% if content_for? :index_section %>
<%= yield :index_section %>
- <div class="pagebreak">
+ <div class="pagebreak"></div>
<% end %>
<%= yield.html_safe %>
diff --git a/guides/source/kindle/rails_guides.opf.erb b/guides/source/kindle/rails_guides.opf.erb
index 547abcbc19..1882ec1005 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>
@@ -26,7 +26,7 @@
<item id="<%= document['url'] %>" media-type="text/html" href="<%= document['url'] %>" />
<% end %>
- <% %w{toc.html credits.html welcome.html copyright.html}.each do |url| %>
+ <% %w{toc.html welcome.html copyright.html}.each do |url| %>
<item id="<%= url %>" media-type="text/html" href="<%= url %>" />
<% end %>
@@ -38,7 +38,6 @@
<spine toc="toc">
<itemref idref="toc.html" />
<itemref idref="welcome.html" />
- <itemref idref="credits.html" />
<itemref idref="copyright.html" />
<% documents_flat.each do |document| %>
<itemref idref="<%= document['url'] %>" />
diff --git a/guides/source/kindle/toc.html.erb b/guides/source/kindle/toc.html.erb
index f310edd3a1..b77ac2e99d 100644
--- a/guides/source/kindle/toc.html.erb
+++ b/guides/source/kindle/toc.html.erb
@@ -14,11 +14,10 @@ Ruby on Rails Guides
<% if document['work_in_progress']%>(WIP)<% end %>
</li>
<% end %>
- </ul>
+ </ul>
<% end %>
<hr />
<ul>
- <li><a href="credits.html">Credits</a></li>
<li><a href="copyright.html">Copyright &amp; License</a></li>
</ul>
</div>
diff --git a/guides/source/kindle/toc.ncx.erb b/guides/source/kindle/toc.ncx.erb
index 2c6d8e3bdf..9b73bc9bea 100644
--- a/guides/source/kindle/toc.ncx.erb
+++ b/guides/source/kindle/toc.ncx.erb
@@ -30,14 +30,10 @@
</navLabel>
<content src="welcome.html"/>
</navPoint>
- <navPoint class="article" id="credits" playOrder="3">
- <navLabel><text>Credits</text></navLabel>
- <content src="credits.html">
- </navPoint>
<navPoint class="article" id="copyright" playOrder="4">
<navLabel><text>Copyright &amp; License</text></navLabel>
- <content src="copyright.html">
- </navPoint>
+ <content src="copyright.html"/>
+ </navPoint>
</navPoint>
<% play_order = 4 %>
@@ -47,7 +43,7 @@
<text><%= section['name'] %></text>
</navLabel>
<content src="<%=section['documents'].first['url'] %>"/>
-
+
<% section['documents'].each_with_index do |document, document_no| %>
<navPoint class="article" id="_<%=section_no+1%>.<%=document_no+1%>" playOrder="<%=play_order +=1 %>">
<navLabel>
diff --git a/guides/source/layout.html.erb b/guides/source/layout.html.erb
index 1005057ca9..1f42d72756 100644
--- a/guides/source/layout.html.erb
+++ b/guides/source/layout.html.erb
@@ -1,20 +1,19 @@
<!DOCTYPE html>
-
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<html lang="en">
<head>
-<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
-<meta name="viewport" content="width=device-width, initial-scale=1"/>
-
-<title><%= yield(:page_title) || 'Ruby on Rails Guides' %></title>
-<link rel="stylesheet" type="text/css" href="stylesheets/style.css" />
-<link rel="stylesheet" type="text/css" href="stylesheets/print.css" media="print" />
-
-<link rel="stylesheet" type="text/css" href="stylesheets/syntaxhighlighter/shCore.css" />
-<link rel="stylesheet" type="text/css" href="stylesheets/syntaxhighlighter/shThemeRailsGuides.css" />
-
-<link rel="stylesheet" type="text/css" href="stylesheets/fixes.css" />
-
-<link href="images/favicon.ico" rel="shortcut icon" type="image/x-icon" />
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title><%= yield(:page_title) || 'Ruby on Rails Guides' %></title>
+ <link rel="stylesheet" type="text/css" href="stylesheets/style.css" data-turbolinks-track="reload">
+ <link rel="stylesheet" type="text/css" href="stylesheets/print.css" media="print">
+ <link rel="stylesheet" type="text/css" href="stylesheets/syntaxhighlighter/shCore.css" data-turbolinks-track="reload">
+ <link rel="stylesheet" type="text/css" href="stylesheets/syntaxhighlighter/shThemeRailsGuides.css" data-turbolinks-track="reload">
+ <link rel="stylesheet" type="text/css" href="stylesheets/fixes.css" data-turbolinks-track="reload">
+ <link href="images/favicon.ico" rel="shortcut icon" type="image/x-icon" />
+ <script src="javascripts/syntaxhighlighter.js" data-turbolinks-track="reload"></script>
+ <script src="javascripts/turbolinks.js" data-turbolinks-track="reload"></script>
+ <script src="javascripts/guides.js" data-turbolinks-track="reload"></script>
+ <script src="javascripts/responsive-tables.js" data-turbolinks-track="reload"></script>
</head>
<body class="guide">
<% if @edge %>
@@ -29,14 +28,11 @@
More Ruby on Rails
</span>
<ul class="more-info-links s-hidden">
- <li class="more-info"><a href="http://rubyonrails.org/">Overview</a></li>
- <li class="more-info"><a href="http://rubyonrails.org/download">Download</a></li>
- <li class="more-info"><a href="http://rubyonrails.org/deploy">Deploy</a></li>
- <li class="more-info"><a href="https://github.com/rails/rails">Code</a></li>
- <li class="more-info"><a href="http://rubyonrails.org/screencasts">Screencasts</a></li>
- <li class="more-info"><a href="http://rubyonrails.org/documentation">Documentation</a></li>
- <li class="more-info"><a href="http://rubyonrails.org/community">Community</a></li>
- <li class="more-info"><a href="http://weblog.rubyonrails.org/">Blog</a></li>
+ <li class="more-info"><a href="https://weblog.rubyonrails.org/">Blog</a></li>
+ <li class="more-info"><a href="https://guides.rubyonrails.org/">Guides</a></li>
+ <li class="more-info"><a href="http://api.rubyonrails.org/">API</a></li>
+ <li class="more-info"><a href="https://stackoverflow.com/questions/tagged/ruby-on-rails">Ask for help</a></li>
+ <li class="more-info"><a href="https://github.com/rails/rails">Contribute on GitHub</a></li>
</ul>
</div>
</div>
@@ -49,20 +45,19 @@
<a href="index.html" id="guidesMenu" class="guides-index-item nav-item">Guides Index</a>
<div id="guides" class="clearfix" style="display: none;">
<hr />
- <% ['L', 'R'].each do |position| %>
- <dl class="<%= position %>">
- <% docs_for_menu(position).each do |section| %>
- <dt><%= section['name'] %></dt>
- <% finished_documents(section['documents']).each do |document| %>
- <dd><a href="<%= document['url'] %>"><%= document['name'] %></a></dd>
- <% end %>
+ <div class="guides-section-container">
+ <% documents_by_section.each do |section| %>
+ <div class="guides-section">
+ <dt><%= section['name'] %></dt>
+ <% finished_documents(section['documents']).each do |document| %>
+ <dd><a href="<%= document['url'] %>"><%= document['name'] %></a></dd>
+ <% end %>
+ </div>
<% end %>
- </dl>
- <% end %>
+ </div>
</div>
</li>
<li><a class="nav-item" href="contributing_to_ruby_on_rails.html">Contribute</a></li>
- <li><a class="nav-item" href="credits.html">Credits</a></li>
<li class="guides-index guides-index-small">
<select class="guides-index-item nav-item">
<option value="index.html">Guides Index</option>
@@ -91,7 +86,7 @@
<div id="container">
<div class="wrapper">
<div id="mainCol">
- <%= yield.html_safe %>
+ <%= yield %>
<h3>Feedback</h3>
<p>
@@ -99,12 +94,12 @@
</p>
<p>
Please contribute if you see any typos or factual errors.
- To get started, you can read our <%= link_to 'documentation contributions', 'http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation' %> section.
+ To get started, you can read our <%= link_to 'documentation contributions', 'https://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation' %> section.
</p>
<p>
- You may also find incomplete content, or stuff that is not up to date.
+ You may also find incomplete content or stuff that is not up to date.
Please do add any missing documentation for master. Make sure to check
- <%= link_to 'Edge Guides','http://edgeguides.rubyonrails.org' %> first to verify
+ <%= link_to 'Edge Guides', 'https://edgeguides.rubyonrails.org' %> first to verify
if the issues are already fixed or not on the master branch.
Check the <%= link_to 'Ruby on Rails Guides Guidelines', 'ruby_on_rails_guides_guidelines.html' %>
for style and conventions.
@@ -114,7 +109,7 @@
<%= link_to 'open an issue', 'https://github.com/rails/rails/issues' %>.
</p>
<p>And last but not least, any kind of discussion regarding Ruby on Rails
- documentation is very welcome in the <%= link_to 'rubyonrails-docs mailing list', 'http://groups.google.com/group/rubyonrails-docs' %>.
+ documentation is very welcome on the <%= link_to 'rubyonrails-docs mailing list', 'https://groups.google.com/forum/#!forum/rubyonrails-docs' %>.
</p>
</div>
</div>
@@ -126,18 +121,5 @@
<%= render 'license' %>
</div>
</div>
-
- <script type="text/javascript" src="javascripts/jquery.min.js"></script>
- <script type="text/javascript" src="javascripts/responsive-tables.js"></script>
- <script type="text/javascript" src="javascripts/guides.js"></script>
- <script type="text/javascript" src="javascripts/syntaxhighlighter/shCore.js"></script>
- <script type="text/javascript" src="javascripts/syntaxhighlighter/shBrushRuby.js"></script>
- <script type="text/javascript" src="javascripts/syntaxhighlighter/shBrushXml.js"></script>
- <script type="text/javascript" src="javascripts/syntaxhighlighter/shBrushSql.js"></script>
- <script type="text/javascript" src="javascripts/syntaxhighlighter/shBrushPlain.js"></script>
- <script type="text/javascript">
- SyntaxHighlighter.all();
- $(guidesIndex.bind);
- </script>
</body>
</html>
diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md
index 94cd7297e2..ad08e5a5a9 100644
--- a/guides/source/layouts_and_rendering.md
+++ b/guides/source/layouts_and_rendering.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Layouts and Rendering in Rails
==============================
@@ -71,23 +71,25 @@ If we want to display the properties of all the books in our view, we can do so
<h1>Listing Books</h1>
<table>
- <tr>
- <th>Title</th>
- <th>Summary</th>
- <th></th>
- <th></th>
- <th></th>
- </tr>
-
-<% @books.each do |book| %>
- <tr>
- <td><%= book.title %></td>
- <td><%= book.content %></td>
- <td><%= link_to "Show", book %></td>
- <td><%= link_to "Edit", edit_book_path(book) %></td>
- <td><%= link_to "Remove", book, method: :delete, data: { confirm: "Are you sure?" } %></td>
- </tr>
-<% end %>
+ <thead>
+ <tr>
+ <th>Title</th>
+ <th>Content</th>
+ <th colspan="3"></th>
+ </tr>
+ </thead>
+
+ <tbody>
+ <% @books.each do |book| %>
+ <tr>
+ <td><%= book.title %></td>
+ <td><%= book.content %></td>
+ <td><%= link_to "Show", book %></td>
+ <td><%= link_to "Edit", edit_book_path(book) %></td>
+ <td><%= link_to "Destroy", book, method: :delete, data: { confirm: "Are you sure?" } %></td>
+ </tr>
+ <% end %>
+ </tbody>
</table>
<br>
@@ -95,7 +97,7 @@ If we want to display the properties of all the books in our view, we can do so
<%= link_to "New book", new_book_path %>
```
-NOTE: The actual rendering is done by subclasses of `ActionView::TemplateHandlers`. This guide does not dig into that process, but it's important to know that the file extension on your view controls the choice of template handler. Beginning with Rails 2, the standard extensions are `.erb` for ERB (HTML with embedded Ruby), and `.builder` for Builder (XML generator).
+NOTE: The actual rendering is done by nested classes of the module [`ActionView::Template::Handlers`](http://api.rubyonrails.org/classes/ActionView/Template/Handlers.html). This guide does not dig into that process, but it's important to know that the file extension on your view controls the choice of template handler.
### Using `render`
@@ -149,27 +151,26 @@ render template: "products/show"
#### Rendering an Arbitrary File
-The `render` method can also use a view that's entirely outside of your application (perhaps you're sharing views between two Rails applications):
-
-```ruby
-render "/u/apps/warehouse_app/current/app/views/products/show"
-```
-
-Rails determines that this is a file render because of the leading slash character. To be explicit, you can use the `:file` option (which was required on Rails 2.2 and earlier):
+The `render` method can also use a view that's entirely outside of your application:
```ruby
render file: "/u/apps/warehouse_app/current/app/views/products/show"
```
-The `:file` option takes an absolute file-system path. Of course, you need to have rights to the view that you're using to render the content.
+The `:file` option takes an absolute file-system path. Of course, you need to have rights
+to the view that you're using to render the content.
+
+NOTE: Using the `:file` option in combination with users input can lead to security problems
+since an attacker could use this action to access security sensitive files in your file system.
NOTE: By default, the file is rendered using the current layout.
-TIP: If you're running Rails on Microsoft Windows, you should use the `:file` option to render a file, because Windows filenames do not have the same format as Unix filenames.
+TIP: If you're running Rails on Microsoft Windows, you should use the `:file` option to
+render a file, because Windows filenames do not have the same format as Unix filenames.
#### Wrapping it up
-The above three ways of rendering (rendering another template within the controller, rendering a template within another controller and rendering an arbitrary file on the file system) are actually variants of the same action.
+The above three ways of rendering (rendering another template within the controller, rendering a template within another controller, and rendering an arbitrary file on the file system) are actually variants of the same action.
In fact, in the BooksController class, inside of the update action where we want to render the edit template if the book does not update successfully, all of the following render calls would all render the `edit.html.erb` template in the `views/books` directory:
@@ -222,7 +223,7 @@ service requests that are expecting something other than proper HTML.
NOTE: By default, if you use the `:plain` option, the text is rendered without
using the current layout. If you want Rails to put the text into the current
-layout, you need to add the `layout: true` option and use the `.txt.erb`
+layout, you need to add the `layout: true` option and use the `.text.erb`
extension for the layout file.
#### Rendering HTML
@@ -231,14 +232,14 @@ You can send an HTML string back to the browser by using the `:html` option to
`render`:
```ruby
-render html: "<strong>Not Found</strong>".html_safe
+render html: helpers.tag.strong('Not Found')
```
TIP: This is useful when you're rendering a small snippet of HTML code.
However, you might want to consider moving it to a template file if the markup
is complex.
-NOTE: This option will escape HTML entities if the string is not HTML safe.
+NOTE: When using `html:` option, HTML entities will be escaped if the string is not composed with `html_safe`-aware APIs.
#### Rendering JSON
@@ -280,11 +281,11 @@ render body: "raw"
```
TIP: This option should be used only if you don't care about the content type of
-the response. Using `:plain` or `:html` might be more appropriate in most of the
+the response. Using `:plain` or `:html` might be more appropriate most of the
time.
NOTE: Unless overridden, your response returned from this render option will be
-`text/html`, as that is the default content type of Action Dispatch response.
+`text/plain`, as that is the default content type of Action Dispatch response.
#### Options for `render`
@@ -360,7 +361,6 @@ Rails understands both numeric status codes and the corresponding symbols shown
| | 303 | :see_other |
| | 304 | :not_modified |
| | 305 | :use_proxy |
-| | 306 | :reserved |
| | 307 | :temporary_redirect |
| | 308 | :permanent_redirect |
| **Client Error** | 400 | :bad_request |
@@ -376,11 +376,12 @@ Rails understands both numeric status codes and the corresponding symbols shown
| | 410 | :gone |
| | 411 | :length_required |
| | 412 | :precondition_failed |
-| | 413 | :request_entity_too_large |
-| | 414 | :request_uri_too_long |
+| | 413 | :payload_too_large |
+| | 414 | :uri_too_long |
| | 415 | :unsupported_media_type |
-| | 416 | :requested_range_not_satisfiable |
+| | 416 | :range_not_satisfiable |
| | 417 | :expectation_failed |
+| | 421 | :misdirected_request |
| | 422 | :unprocessable_entity |
| | 423 | :locked |
| | 424 | :failed_dependency |
@@ -388,6 +389,7 @@ Rails understands both numeric status codes and the corresponding symbols shown
| | 428 | :precondition_required |
| | 429 | :too_many_requests |
| | 431 | :request_header_fields_too_large |
+| | 451 | :unavailable_for_legal_reasons |
| **Server Error** | 500 | :internal_server_error |
| | 501 | :not_implemented |
| | 502 | :bad_gateway |
@@ -401,7 +403,7 @@ Rails understands both numeric status codes and the corresponding symbols shown
| | 511 | :network_authentication_required |
NOTE: If you try to render content along with a non-content status code
-(100-199, 204, 205 or 304), it will be dropped from the response.
+(100-199, 204, 205, or 304), it will be dropped from the response.
##### The `:formats` Option
@@ -413,6 +415,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.
@@ -556,7 +560,7 @@ class Admin::ProductsController < AdminController
end
```
-The lookup order for a `admin/products#index` action will be:
+The lookup order for an `admin/products#index` action will be:
* `app/views/admin/products/`
* `app/views/admin/`
@@ -623,12 +627,17 @@ Another way to handle returning responses to an HTTP request is with `redirect_t
redirect_to photos_url
```
-You can use `redirect_to` with any arguments that you could use with `link_to` or `url_for`. There's also a special redirect that sends the user back to the page they just came from:
+You can use `redirect_back` to return the user to the page they just came from.
+This location is pulled from the `HTTP_REFERER` header which is not guaranteed
+to be set by the browser, so you must provide the `fallback_location`
+to use in this case.
```ruby
-redirect_to :back
+redirect_back(fallback_location: root_path)
```
+NOTE: `redirect_to` and `redirect_back` do not halt and return immediately from method execution, but simply set HTTP responses. Statements occurring after them in a method will be executed. You can halt by an explicit `return` or some other halting mechanism, if needed.
+
#### Getting a Different Redirect Status Code
Rails uses HTTP status code 302, a temporary redirect, when you call `redirect_to`. If you'd like to use a different status code, perhaps 301, a permanent redirect, you can use the `:status` option:
@@ -698,7 +707,7 @@ This would detect that there are no books with the specified ID, populate the `@
### Using `head` To Build Header-Only Responses
-The `head` method can be used to send responses with only headers to the browser. It provides a more obvious alternative to calling `render :nothing`. The `head` method accepts a number or symbol (see [reference table](#the-status-option)) representing a HTTP status code. The options argument is interpreted as a hash of header names and values. For example, you can return only an error header:
+The `head` method can be used to send responses with only headers to the browser. The `head` method accepts a number or symbol (see [reference table](#the-status-option)) representing an HTTP status code. The options argument is interpreted as a hash of header names and values. For example, you can return only an error header:
```ruby
head :bad_request
@@ -748,7 +757,7 @@ When Rails renders a view as a response, it does so by combining the view with t
### Asset Tag Helpers
-Asset tag helpers provide methods for generating HTML that link views to feeds, JavaScript, stylesheets, images, videos and audios. There are six asset tag helpers available in Rails:
+Asset tag helpers provide methods for generating HTML that link views to feeds, JavaScript, stylesheets, images, videos, and audios. There are six asset tag helpers available in Rails:
* `auto_discovery_link_tag`
* `javascript_include_tag`
@@ -763,7 +772,7 @@ WARNING: The asset tag helpers do _not_ verify the existence of the assets at th
#### Linking to Feeds with the `auto_discovery_link_tag`
-The `auto_discovery_link_tag` helper builds HTML that most browsers and feed readers can use to detect the presence of RSS or Atom feeds. It takes the type of the link (`:rss` or `:atom`), a hash of options that are passed through to url_for, and a hash of options for the tag:
+The `auto_discovery_link_tag` helper builds HTML that most browsers and feed readers can use to detect the presence of RSS, Atom, or JSON feeds. It takes the type of the link (`:rss`, `:atom`, or `:json`), a hash of options that are passed through to url_for, and a hash of options for the tag:
```erb
<%= auto_discovery_link_tag(:rss, {action: "feed"},
@@ -782,7 +791,7 @@ The `javascript_include_tag` helper returns an HTML `script` tag for each source
If you are using Rails with the [Asset Pipeline](asset_pipeline.html) enabled, this helper will generate a link to `/assets/javascripts/` rather than `public/javascripts` which was used in earlier versions of Rails. This link is then served by the asset pipeline.
-A JavaScript file within a Rails application or Rails engine goes in one of three locations: `app/assets`, `lib/assets` or `vendor/assets`. These locations are explained in detail in the [Asset Organization section in the Asset Pipeline Guide](asset_pipeline.html#asset-organization)
+A JavaScript file within a Rails application or Rails engine goes in one of three locations: `app/assets`, `lib/assets` or `vendor/assets`. These locations are explained in detail in the [Asset Organization section in the Asset Pipeline Guide](asset_pipeline.html#asset-organization).
You can specify a full path relative to the document root, or a URL, if you prefer. For example, to link to a JavaScript file that is inside a directory called `javascripts` inside of one of `app/assets`, `lib/assets` or `vendor/assets`, you would do this:
@@ -1079,7 +1088,7 @@ definitions for several similar resources:
* `shared/_search_filters.html.erb`
```html+erb
- <%= form_for(@q) do |f| %>
+ <%= form_for(search) do |f| %>
<h1>Search form:</h1>
<fieldset>
<%= yield f %>
@@ -1152,23 +1161,21 @@ 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
- <%= content_tag_for :article, article do |article| %>
- <h2><%= article.title %></h2>
+ <h2><%= article.title %></h2>
- <% if local_assigns[:full] %>
- <%= simple_format article.body %>
- <% else %>
- <%= truncate article.body %>
- <% end %>
+ <% if local_assigns[:full] %>
+ <%= simple_format article.body %>
+ <% else %>
+ <%= truncate article.body %>
<% end %>
```
This way it is possible to use the partial without the need to declare all local variables.
-Every partial also has a local variable with the same name as the partial (minus the underscore). You can pass an object in to this local variable via the `:object` option:
+Every partial also has a local variable with the same name as the partial (minus the leading underscore). You can pass an object in to this local variable via the `:object` option:
```erb
<%= render partial: "customer", object: @new_customer %>
@@ -1203,7 +1210,7 @@ Partials are very useful in rendering collections. When you pass a collection to
When a partial is called with a pluralized collection, then the individual instances of the partial have access to the member of the collection being rendered via a variable named after the partial. In this case, the partial is `_product`, and within the `_product` partial, you can refer to `product` to get the instance that is being rendered.
-There is also a shorthand for this. Assuming `@products` is a collection of `product` instances, you can simply write this in the `index.html.erb` to produce the same result:
+There is also a shorthand for this. Assuming `@products` is a collection of `Product` instances, you can simply write this in the `index.html.erb` to produce the same result:
```html+erb
<h1>Products</h1>
@@ -1259,7 +1266,7 @@ You can also pass in arbitrary local variables to any partial you are rendering
In this case, the partial will have access to a local variable `title` with the value "Products Page".
-TIP: Rails also makes a counter variable available within a partial called by the collection, named after the member of the collection followed by `_counter`. For example, if you're rendering `@products`, within the partial you can refer to `product_counter` to tell you how many times the partial has been rendered. This does not work in conjunction with the `as: :value` option.
+TIP: Rails also makes a counter variable available within a partial called by the collection, named after the title of the partial followed by `_counter`. For example, when rendering a collection `@products` the partial `_product.html.erb` can access the variable `product_counter` which indexes the number of times it has been rendered within the enclosing view. Note that it also applies for when the partial name was changed by using the `as:` option. For example, the counter variable for the code above would be `item_counter`.
You can also specify a second partial to be rendered between instances of the main partial by using the `:spacer_template` option:
@@ -1279,7 +1286,7 @@ When rendering collections it is also possible to use the `:layout` option:
<%= render partial: "product", collection: @products, layout: "special_layout" %>
```
-The layout will be rendered together with the partial for each item in the collection. The current object and object_counter variables will be available in the layout as well, the same way they do within the partial.
+The layout will be rendered together with the partial for each item in the collection. The current object and object_counter variables will be available in the layout as well, the same way they are within the partial.
### Using Nested Layouts
diff --git a/guides/source/maintenance_policy.md b/guides/source/maintenance_policy.md
index 50308f505a..b14b7a2c90 100644
--- a/guides/source/maintenance_policy.md
+++ b/guides/source/maintenance_policy.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Maintenance Policy for Ruby on Rails
====================================
@@ -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:** `4.2.Z`, `4.1.Z` (Supported by Rafael França).
+**Currently included series:** `5.2.Z`.
Security Issues
---------------
@@ -59,16 +59,16 @@ be built from 1.2.2, and then added to the end of 1-2-stable. This means that
security releases are easy to upgrade to if you're running the latest version
of Rails.
-**Currently included series:** `4.2.Z`, `4.1.Z`.
+**Currently included series:** `5.2.Z`, `5.1.Z`.
Severe Security Issues
----------------------
-For severe security issues we will provide new versions as above, and also the
+For severe security issues all releases in the current major series, and also the
last major release series will receive patches and new versions. The
classification of the security issue is judged by the core team.
-**Currently included series:** `4.2.Z`, `4.1.Z`, `3.2.Z`.
+**Currently included series:** `5.2.Z`, `5.1.Z`, `5.0.Z`, `4.2.Z`.
Unsupported Release Series
--------------------------
diff --git a/guides/source/nested_model_forms.md b/guides/source/nested_model_forms.md
deleted file mode 100644
index 121cf2b185..0000000000
--- a/guides/source/nested_model_forms.md
+++ /dev/null
@@ -1,230 +0,0 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
-
-Rails Nested Model Forms
-========================
-
-Creating a form for a model _and_ its associations can become quite tedious. Therefore Rails provides helpers to assist in dealing with the complexities of generating these forms _and_ the required CRUD operations to create, update, and destroy associations.
-
-After reading this guide, you will know:
-
-* do stuff.
-
---------------------------------------------------------------------------------
-
-NOTE: This guide assumes the user knows how to use the [Rails form helpers](form_helpers.html) in general. Also, it's **not** an API reference. For a complete reference please visit [the Rails API documentation](http://api.rubyonrails.org/).
-
-
-Model setup
------------
-
-To be able to use the nested model functionality in your forms, the model will need to support some basic operations.
-
-First of all, it needs to define a writer method for the attribute that corresponds to the association you are building a nested model form for. The `fields_for` form helper will look for this method to decide whether or not a nested model form should be built.
-
-If the associated object is an array, a form builder will be yielded for each object, else only a single form builder will be yielded.
-
-Consider a Person model with an associated Address. When asked to yield a nested FormBuilder for the `:address` attribute, the `fields_for` form helper will look for a method on the Person instance named `address_attributes=`.
-
-### ActiveRecord::Base model
-
-For an ActiveRecord::Base model and association this writer method is commonly defined with the `accepts_nested_attributes_for` class method:
-
-#### has_one
-
-```ruby
-class Person < ActiveRecord::Base
- has_one :address
- accepts_nested_attributes_for :address
-end
-```
-
-#### belongs_to
-
-```ruby
-class Person < ActiveRecord::Base
- belongs_to :firm
- accepts_nested_attributes_for :firm
-end
-```
-
-#### has_many / has_and_belongs_to_many
-
-```ruby
-class Person < ActiveRecord::Base
- has_many :projects
- accepts_nested_attributes_for :projects
-end
-```
-
-NOTE: For greater detail on associations see [Active Record Associations](association_basics.html).
-For a complete reference on associations please visit the API documentation for [ActiveRecord::Associations::ClassMethods](http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html).
-
-### Custom model
-
-As you might have inflected from this explanation, you _don't_ necessarily need an ActiveRecord::Base model to use this functionality. The following examples are sufficient to enable the nested model form behavior:
-
-#### Single associated object
-
-```ruby
-class Person
- def address
- Address.new
- end
-
- def address_attributes=(attributes)
- # ...
- end
-end
-```
-
-#### Association collection
-
-```ruby
-class Person
- def projects
- [Project.new, Project.new]
- end
-
- def projects_attributes=(attributes)
- # ...
- end
-end
-```
-
-NOTE: See (TODO) in the advanced section for more information on how to deal with the CRUD operations in your custom model.
-
-Views
------
-
-### Controller code
-
-A nested model form will _only_ be built if the associated object(s) exist. This means that for a new model instance you would probably want to build the associated object(s) first.
-
-Consider the following typical RESTful controller which will prepare a new Person instance and its `address` and `projects` associations before rendering the `new` template:
-
-```ruby
-class PeopleController < ApplicationController
- def new
- @person = Person.new
- @person.build_address
- 2.times { @person.projects.build }
- end
-
- def create
- @person = Person.new(params[:person])
- if @person.save
- # ...
- end
- end
-end
-```
-
-NOTE: Obviously the instantiation of the associated object(s) can become tedious and not DRY, so you might want to move that into the model itself. ActiveRecord::Base provides an `after_initialize` callback which is a good way to refactor this.
-
-### Form code
-
-Now that you have a model instance, with the appropriate methods and associated object(s), you can start building the nested model form.
-
-#### Standard form
-
-Start out with a regular RESTful form:
-
-```erb
-<%= form_for @person do |f| %>
- <%= f.text_field :name %>
-<% end %>
-```
-
-This will generate the following html:
-
-```html
-<form action="/people" class="new_person" id="new_person" method="post">
- <input id="person_name" name="person[name]" type="text" />
-</form>
-```
-
-#### Nested form for a single associated object
-
-Now add a nested form for the `address` association:
-
-```erb
-<%= form_for @person do |f| %>
- <%= f.text_field :name %>
-
- <%= f.fields_for :address do |af| %>
- <%= af.text_field :street %>
- <% end %>
-<% end %>
-```
-
-This generates:
-
-```html
-<form action="/people" class="new_person" id="new_person" method="post">
- <input id="person_name" name="person[name]" type="text" />
-
- <input id="person_address_attributes_street" name="person[address_attributes][street]" type="text" />
-</form>
-```
-
-Notice that `fields_for` recognized the `address` as an association for which a nested model form should be built by the way it has namespaced the `name` attribute.
-
-When this form is posted the Rails parameter parser will construct a hash like the following:
-
-```ruby
-{
- "person" => {
- "name" => "Eloy Duran",
- "address_attributes" => {
- "street" => "Nieuwe Prinsengracht"
- }
- }
-}
-```
-
-That's it. The controller will simply pass this hash on to the model from the `create` action. The model will then handle building the `address` association for you and automatically save it when the parent (`person`) is saved.
-
-#### Nested form for a collection of associated objects
-
-The form code for an association collection is pretty similar to that of a single associated object:
-
-```erb
-<%= form_for @person do |f| %>
- <%= f.text_field :name %>
-
- <%= f.fields_for :projects do |pf| %>
- <%= pf.text_field :name %>
- <% end %>
-<% end %>
-```
-
-Which generates:
-
-```html
-<form action="/people" class="new_person" id="new_person" method="post">
- <input id="person_name" name="person[name]" type="text" />
-
- <input id="person_projects_attributes_0_name" name="person[projects_attributes][0][name]" type="text" />
- <input id="person_projects_attributes_1_name" name="person[projects_attributes][1][name]" type="text" />
-</form>
-```
-
-As you can see it has generated 2 `project name` inputs, one for each new `project` that was built in the controller's `new` action. Only this time the `name` attribute of the input contains a digit as an extra namespace. This will be parsed by the Rails parameter parser as:
-
-```ruby
-{
- "person" => {
- "name" => "Eloy Duran",
- "projects_attributes" => {
- "0" => { "name" => "Project 1" },
- "1" => { "name" => "Project 2" }
- }
- }
-}
-```
-
-You can basically see the `projects_attributes` hash as an array of attribute hashes, one for each model instance.
-
-NOTE: The reason that `fields_for` constructed a hash instead of an array is that it won't work for any form nested deeper than one level deep.
-
-TIP: You _can_ however pass an array to the writer method generated by `accepts_nested_attributes_for` if you're using plain Ruby or some other API access. See (TODO) for more info and example.
diff --git a/guides/source/plugins.md b/guides/source/plugins.md
index 4e630a39f3..7c9784dfe3 100644
--- a/guides/source/plugins.md
+++ b/guides/source/plugins.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
The Basics of Creating Rails Plugins
====================================
@@ -17,7 +17,7 @@ After reading this guide, you will know:
This guide describes how to build a test-driven plugin that will:
* Extend core Ruby classes like Hash and String.
-* Add methods to `ActiveRecord::Base` in the tradition of the `acts_as` plugins.
+* Add methods to `ApplicationRecord` in the tradition of the `acts_as` plugins.
* Give you information about where to put generators in your plugin.
For the purpose of this guide pretend for a moment that you are an avid bird watcher.
@@ -30,14 +30,14 @@ Setup
-----
Currently, Rails plugins are built as gems, _gemified plugins_. They can be shared across
-different rails applications using RubyGems and Bundler if desired.
+different Rails applications using RubyGems and Bundler if desired.
### Generate a gemified plugin.
Rails ships with a `rails plugin new` command which creates a
skeleton for developing any kind of Rails extension with the ability
-to run integration tests using a dummy Rails application. Create your
+to run integration tests using a dummy Rails application. Create your
plugin with the command:
```bash
@@ -54,7 +54,7 @@ Testing Your Newly Generated Plugin
-----------------------------------
You can navigate to the directory that contains the plugin, run the `bundle install` command
- and run the one generated test using the `rake` command.
+ and run the one generated test using the `bin/test` command.
You should see:
@@ -67,14 +67,14 @@ This will tell you that everything got generated properly and you are ready to s
Extending Core Classes
----------------------
-This section will explain how to add a method to String that will be available anywhere in your rails application.
+This section will explain how to add a method to String that will be available anywhere in your Rails application.
In this example you will add a method to String named `to_squawk`. To begin, create a new test file with a few assertions:
```ruby
# yaffle/test/core_ext_test.rb
-require 'test_helper'
+require "test_helper"
class CoreExtTest < ActiveSupport::TestCase
def test_to_squawk_prepends_the_word_squawk
@@ -83,25 +83,37 @@ class CoreExtTest < ActiveSupport::TestCase
end
```
-Run `rake` to run the test. This test should fail because we haven't implemented the `to_squawk` method:
+Run `bin/test` to run the test. This test should fail because we haven't implemented the `to_squawk` method:
```bash
- 1) Error:
- CoreExtTest#test_to_squawk_prepends_the_word_squawk:
- NoMethodError: undefined method `to_squawk' for "Hello World":String
- /path/to/yaffle/test/core_ext_test.rb:5:in `test_to_squawk_prepends_the_word_squawk'
+E
+
+Error:
+CoreExtTest#test_to_squawk_prepends_the_word_squawk:
+NoMethodError: undefined method `to_squawk' for "Hello World":String
+
+
+bin/test /path/to/yaffle/test/core_ext_test.rb:4
+
+.
+
+Finished in 0.003358s, 595.6483 runs/s, 297.8242 assertions/s.
+
+2 runs, 1 assertions, 0 failures, 1 errors, 0 skips
```
Great - now you are ready to start development.
-In `lib/yaffle.rb`, add `require 'yaffle/core_ext'`:
+In `lib/yaffle.rb`, add `require "yaffle/core_ext"`:
```ruby
# yaffle/lib/yaffle.rb
-require 'yaffle/core_ext'
+require "yaffle/railtie"
+require "yaffle/core_ext"
module Yaffle
+ # Your code goes here...
end
```
@@ -110,23 +122,23 @@ Finally, create the `core_ext.rb` file and add the `to_squawk` method:
```ruby
# yaffle/lib/yaffle/core_ext.rb
-String.class_eval do
+class String
def to_squawk
"squawk! #{self}".strip
end
end
```
-To test that your method does what it says it does, run the unit tests with `rake` from your plugin directory.
+To test that your method does what it says it does, run the unit tests with `bin/test` from your plugin directory.
```bash
2 runs, 2 assertions, 0 failures, 0 errors, 0 skips
```
-To see this in action, change to the test/dummy directory, fire up a console and start squawking:
+To see this in action, change to the `test/dummy` directory, fire up a console, and start squawking:
```bash
-$ bin/rails console
+$ rails console
>> "Hello World".to_squawk
=> "squawk! Hello World"
```
@@ -142,7 +154,7 @@ To begin, set up your files so that you have:
```ruby
# yaffle/test/acts_as_yaffle_test.rb
-require 'test_helper'
+require "test_helper"
class ActsAsYaffleTest < ActiveSupport::TestCase
end
@@ -151,10 +163,12 @@ end
```ruby
# yaffle/lib/yaffle.rb
-require 'yaffle/core_ext'
-require 'yaffle/acts_as_yaffle'
+require "yaffle/railtie"
+require "yaffle/core_ext"
+require "yaffle/acts_as_yaffle"
module Yaffle
+ # Your code goes here...
end
```
@@ -163,7 +177,6 @@ end
module Yaffle
module ActsAsYaffle
- # your code will go here
end
end
```
@@ -179,10 +192,9 @@ To start out, write a failing test that shows the behavior you'd like:
```ruby
# yaffle/test/acts_as_yaffle_test.rb
-require 'test_helper'
+require "test_helper"
class ActsAsYaffleTest < ActiveSupport::TestCase
-
def test_a_hickwalls_yaffle_text_field_should_be_last_squawk
assert_equal "last_squawk", Hickwall.yaffle_text_field
end
@@ -190,34 +202,47 @@ class ActsAsYaffleTest < ActiveSupport::TestCase
def test_a_wickwalls_yaffle_text_field_should_be_last_tweet
assert_equal "last_tweet", Wickwall.yaffle_text_field
end
-
end
```
-When you run `rake`, you should see the following:
+When you run `bin/test`, you should see the following:
```
- 1) Error:
- ActsAsYaffleTest#test_a_hickwalls_yaffle_text_field_should_be_last_squawk:
- NameError: uninitialized constant ActsAsYaffleTest::Hickwall
- /path/to/yaffle/test/acts_as_yaffle_test.rb:6:in `test_a_hickwalls_yaffle_text_field_should_be_last_squawk'
+# Running:
+
+..E
+
+Error:
+ActsAsYaffleTest#test_a_wickwalls_yaffle_text_field_should_be_last_tweet:
+NameError: uninitialized constant ActsAsYaffleTest::Wickwall
+
+
+bin/test /path/to/yaffle/test/acts_as_yaffle_test.rb:8
+
+E
+
+Error:
+ActsAsYaffleTest#test_a_hickwalls_yaffle_text_field_should_be_last_squawk:
+NameError: uninitialized constant ActsAsYaffleTest::Hickwall
- 2) Error:
- ActsAsYaffleTest#test_a_wickwalls_yaffle_text_field_should_be_last_tweet:
- NameError: uninitialized constant ActsAsYaffleTest::Wickwall
- /path/to/yaffle/test/acts_as_yaffle_test.rb:10:in `test_a_wickwalls_yaffle_text_field_should_be_last_tweet'
- 4 runs, 2 assertions, 0 failures, 2 errors, 0 skips
+bin/test /path/to/yaffle/test/acts_as_yaffle_test.rb:4
+
+
+
+Finished in 0.004812s, 831.2949 runs/s, 415.6475 assertions/s.
+
+4 runs, 2 assertions, 0 failures, 2 errors, 0 skips
```
This tells us that we don't have the necessary models (Hickwall and Wickwall) that we are trying to test.
We can easily generate these models in our "dummy" Rails application by running the following commands from the
-test/dummy directory:
+`test/dummy` directory:
```bash
$ cd test/dummy
-$ bin/rails generate model Hickwall last_squawk:string
-$ bin/rails generate model Wickwall last_squawk:string last_tweet:string
+$ rails generate model Hickwall last_squawk:string
+$ rails generate model Wickwall last_squawk:string last_tweet:string
```
Now you can create the necessary database tables in your testing database by navigating to your dummy app
@@ -225,7 +250,7 @@ and migrating the database. First, run:
```bash
$ cd test/dummy
-$ bin/rake db:migrate
+$ rails db:migrate
```
While you are here, change the Hickwall and Wickwall models so that they know that they are supposed to act
@@ -234,57 +259,70 @@ like yaffles.
```ruby
# test/dummy/app/models/hickwall.rb
-class Hickwall < ActiveRecord::Base
+class Hickwall < ApplicationRecord
acts_as_yaffle
end
# test/dummy/app/models/wickwall.rb
-class Wickwall < ActiveRecord::Base
+class Wickwall < ApplicationRecord
acts_as_yaffle yaffle_text_field: :last_tweet
end
-
```
We will also add code to define the `acts_as_yaffle` method.
```ruby
# yaffle/lib/yaffle/acts_as_yaffle.rb
+
module Yaffle
module ActsAsYaffle
extend ActiveSupport::Concern
- included do
- end
-
- module ClassMethods
+ class_methods do
def acts_as_yaffle(options = {})
- # your code will go here
end
end
end
end
-ActiveRecord::Base.include(Yaffle::ActsAsYaffle)
+# test/dummy/app/models/application_record.rb
+
+class ApplicationRecord < ActiveRecord::Base
+ include Yaffle::ActsAsYaffle
+
+ self.abstract_class = true
+end
```
-You can then return to the root directory (`cd ../..`) of your plugin and rerun the tests using `rake`.
+You can then return to the root directory (`cd ../..`) of your plugin and rerun the tests using `bin/test`.
```
- 1) Error:
- ActsAsYaffleTest#test_a_hickwalls_yaffle_text_field_should_be_last_squawk:
- NoMethodError: undefined method `yaffle_text_field' for #<Class:0x007fd105e3b218>
- activerecord (4.1.5) lib/active_record/dynamic_matchers.rb:26:in `method_missing'
- /path/to/yaffle/test/acts_as_yaffle_test.rb:6:in `test_a_hickwalls_yaffle_text_field_should_be_last_squawk'
+# Running:
+
+.E
- 2) Error:
- ActsAsYaffleTest#test_a_wickwalls_yaffle_text_field_should_be_last_tweet:
- NoMethodError: undefined method `yaffle_text_field' for #<Class:0x007fd105e409c0>
- activerecord (4.1.5) lib/active_record/dynamic_matchers.rb:26:in `method_missing'
- /path/to/yaffle/test/acts_as_yaffle_test.rb:10:in `test_a_wickwalls_yaffle_text_field_should_be_last_tweet'
+Error:
+ActsAsYaffleTest#test_a_hickwalls_yaffle_text_field_should_be_last_squawk:
+NoMethodError: undefined method `yaffle_text_field' for #<Class:0x0055974ebbe9d8>
- 4 runs, 2 assertions, 0 failures, 2 errors, 0 skips
+bin/test /path/to/yaffle/test/acts_as_yaffle_test.rb:4
+
+E
+
+Error:
+ActsAsYaffleTest#test_a_wickwalls_yaffle_text_field_should_be_last_tweet:
+NoMethodError: undefined method `yaffle_text_field' for #<Class:0x0055974eb8cfc8>
+
+
+bin/test /path/to/yaffle/test/acts_as_yaffle_test.rb:8
+
+.
+
+Finished in 0.008263s, 484.0999 runs/s, 242.0500 assertions/s.
+
+4 runs, 2 assertions, 0 failures, 2 errors, 0 skips
```
Getting closer... Now we will implement the code of the `acts_as_yaffle` method to make the tests pass.
@@ -294,24 +332,26 @@ Getting closer... Now we will implement the code of the `acts_as_yaffle` method
module Yaffle
module ActsAsYaffle
- extend ActiveSupport::Concern
-
- included do
- end
+ extend ActiveSupport::Concern
- module ClassMethods
+ class_methods do
def acts_as_yaffle(options = {})
- cattr_accessor :yaffle_text_field
- self.yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s
+ cattr_accessor :yaffle_text_field, default: (options[:yaffle_text_field] || :last_squawk).to_s
end
end
end
end
-ActiveRecord::Base.include(Yaffle::ActsAsYaffle)
+# test/dummy/app/models/application_record.rb
+
+class ApplicationRecord < ActiveRecord::Base
+ include Yaffle::ActsAsYaffle
+
+ self.abstract_class = true
+end
```
-When you run `rake`, you should see the tests all pass:
+When you run `bin/test`, you should see the tests all pass:
```bash
4 runs, 4 assertions, 0 failures, 0 errors, 0 skips
@@ -319,17 +359,16 @@ When you run `rake`, you should see the tests all pass:
### Add an Instance Method
-This plugin will add a method named 'squawk' to any Active Record object that calls 'acts_as_yaffle'. The 'squawk'
+This plugin will add a method named 'squawk' to any Active Record object that calls `acts_as_yaffle`. The 'squawk'
method will simply set the value of one of the fields in the database.
To start out, write a failing test that shows the behavior you'd like:
```ruby
# yaffle/test/acts_as_yaffle_test.rb
-require 'test_helper'
+require "test_helper"
class ActsAsYaffleTest < ActiveSupport::TestCase
-
def test_a_hickwalls_yaffle_text_field_should_be_last_squawk
assert_equal "last_squawk", Hickwall.yaffle_text_field
end
@@ -353,7 +392,7 @@ end
```
Run the test to make sure the last two tests fail with an error that contains "NoMethodError: undefined method `squawk'",
-then update 'acts_as_yaffle.rb' to look like this:
+then update `acts_as_yaffle.rb` to look like this:
```ruby
# yaffle/lib/yaffle/acts_as_yaffle.rb
@@ -363,29 +402,29 @@ module Yaffle
extend ActiveSupport::Concern
included do
- end
-
- module ClassMethods
- def acts_as_yaffle(options = {})
- cattr_accessor :yaffle_text_field
- self.yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s
-
- include Yaffle::ActsAsYaffle::LocalInstanceMethods
+ def squawk(string)
+ write_attribute(self.class.yaffle_text_field, string.to_squawk)
end
end
- module LocalInstanceMethods
- def squawk(string)
- write_attribute(self.class.yaffle_text_field, string.to_squawk)
+ class_methods do
+ def acts_as_yaffle(options = {})
+ cattr_accessor :yaffle_text_field, default: (options[:yaffle_text_field] || :last_squawk).to_s
end
end
end
end
-ActiveRecord::Base.include(Yaffle::ActsAsYaffle)
+# test/dummy/app/models/application_record.rb
+
+class ApplicationRecord < ActiveRecord::Base
+ include Yaffle::ActsAsYaffle
+
+ self.abstract_class = true
+end
```
-Run `rake` one final time and you should see:
+Run `bin/test` one final time and you should see:
```
6 runs, 6 assertions, 0 failures, 0 errors, 0 skips
@@ -400,23 +439,23 @@ send("#{self.class.yaffle_text_field}=", string.to_squawk)
Generators
----------
-Generators can be included in your gem simply by creating them in a lib/generators directory of your plugin. More information about
-the creation of generators can be found in the [Generators Guide](generators.html)
+Generators can be included in your gem simply by creating them in a `lib/generators` directory of your plugin. More information about
+the creation of generators can be found in the [Generators Guide](generators.html).
Publishing Your Gem
-------------------
Gem plugins currently in development can easily be shared from any Git repository. To share the Yaffle gem with others, simply
-commit the code to a Git repository (like GitHub) and add a line to the Gemfile of the application in question:
+commit the code to a Git repository (like GitHub) and add a line to the `Gemfile` of the application in question:
```ruby
-gem 'yaffle', git: 'git://github.com/yaffle_watcher/yaffle.git'
+gem "yaffle", git: "https://github.com/rails/yaffle.git"
```
After running `bundle install`, your gem functionality will be available to the application.
-When the gem is ready to be shared as a formal release, it can be published to [RubyGems](http://www.rubygems.org).
-For more information about publishing gems to RubyGems, see: [Creating and Publishing Your First Ruby Gem](http://blog.thepete.net/2010/11/creating-and-publishing-your-first-ruby.html).
+When the gem is ready to be shared as a formal release, it can be published to [RubyGems](https://rubygems.org).
+For more information about publishing gems to RubyGems, see: [Publishing your gem](https://guides.rubygems.org/publishing).
RDoc Documentation
------------------
@@ -430,7 +469,7 @@ The first step is to update the README file with detailed information about how
* How to add the functionality to the app (several examples of common use cases)
* Warnings, gotchas or tips that might help users and save them time
-Once your README is solid, go through and add rdoc comments to all of the methods that developers will use. It's also customary to add '#:nodoc:' comments to those parts of the code that are not included in the public API.
+Once your README is solid, go through and add rdoc comments to all of the methods that developers will use. It's also customary to add `#:nodoc:` comments to those parts of the code that are not included in the public API.
Once your comments are good to go, navigate to your plugin directory and run:
@@ -442,5 +481,4 @@ $ bundle exec rake rdoc
* [Developing a RubyGem using Bundler](https://github.com/radar/guides/blob/master/gem-development.md)
* [Using .gemspecs as Intended](http://yehudakatz.com/2010/04/02/using-gemspecs-as-intended/)
-* [Gemspec Reference](http://guides.rubygems.org/specification-reference/)
-* [GemPlugins: A Brief Introduction to the Future of Rails Plugins](http://www.intridea.com/blog/2008/6/11/gemplugins-a-brief-introduction-to-the-future-of-rails-plugins)
+* [Gemspec Reference](https://guides.rubygems.org/specification-reference/)
diff --git a/guides/source/profiling.md b/guides/source/profiling.md
deleted file mode 100644
index ce093f78ba..0000000000
--- a/guides/source/profiling.md
+++ /dev/null
@@ -1,16 +0,0 @@
-*DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
-
-A Guide to Profiling Rails Applications
-=======================================
-
-This guide covers built-in mechanisms in Rails for profiling your application.
-
-After reading this guide, you will know:
-
-* Rails profiling terminology.
-* How to write benchmark tests for your application.
-* Other benchmarking approaches and plugins.
-
---------------------------------------------------------------------------------
-
-
diff --git a/guides/source/rails_application_templates.md b/guides/source/rails_application_templates.md
index b7364536c3..bc68a555c5 100644
--- a/guides/source/rails_application_templates.md
+++ b/guides/source/rails_application_templates.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Rails Application Templates
===========================
@@ -15,18 +15,18 @@ After reading this guide, you will know:
Usage
-----
-To apply a template, you need to provide the Rails generator with the location of the template you wish to apply using the -m option. This can either be a path to a file or a URL.
+To apply a template, you need to provide the Rails generator with the location of the template you wish to apply using the `-m` option. This can either be a path to a file or a URL.
```bash
$ rails new blog -m ~/template.rb
$ rails new blog -m http://example.com/template.rb
```
-You can use the rake task `rails:template` to apply templates to an existing Rails application. The location of the template needs to be passed in to an environment variable named LOCATION. Again, this can either be path to a file or a URL.
+You can use the `app:template` rails command to apply templates to an existing Rails application. The location of the template needs to be passed in via the LOCATION environment variable. Again, this can either be path to a file or a URL.
```bash
-$ bin/rake rails:template LOCATION=~/template.rb
-$ bin/rake rails:template LOCATION=http://example.com/template.rb
+$ rails app:template LOCATION=~/template.rb
+$ rails app:template LOCATION=http://example.com/template.rb
```
Template API
@@ -38,7 +38,7 @@ The Rails templates API is easy to understand. Here's an example of a typical Ra
# template.rb
generate(:scaffold, "person name:string")
route "root to: 'people#index'"
-rake("db:migrate")
+rails_command("db:migrate")
after_bundle do
git :init
@@ -78,7 +78,7 @@ gem_group :development, :test do
end
```
-### add_source(source, options = {})
+### add_source(source, options={}, &block)
Adds the given source to the generated application's `Gemfile`.
@@ -88,6 +88,14 @@ For example, if you need to source a gem from `"http://code.whytheluckystiff.net
add_source "http://code.whytheluckystiff.net"
```
+If block is given, gem entries in block are wrapped into the source group.
+
+```ruby
+add_source "http://gems.github.com/" do
+ gem "rspec-rails"
+end
+```
+
### environment/application(data=nil, options={}, &block)
Adds a line inside the `Application` class for `config/application.rb`.
@@ -167,18 +175,24 @@ Executes an arbitrary command. Just like the backticks. Let's say you want to re
run "rm README.rdoc"
```
-### rake(command, options = {})
+### rails_command(command, options = {})
+
+Runs the supplied command in the Rails application. Let's say you want to migrate the database:
+
+```ruby
+rails_command "db:migrate"
+```
-Runs the supplied rake tasks in the Rails application. Let's say you want to migrate the database:
+You can also run commands with a different Rails environment:
```ruby
-rake "db:migrate"
+rails_command "db:migrate", env: 'production'
```
-You can also run rake tasks with a different Rails environment:
+You can also run commands as a super-user:
```ruby
-rake "db:migrate", env: 'production'
+rails_command "log:clear", sudo: true
```
### route(routing_code)
@@ -218,7 +232,7 @@ CODE
These methods let you ask questions from templates and decide the flow based on the user's answer. Let's say you want to Freeze Rails only if the user wants to:
```ruby
-rake("rails:freeze:gems") if yes?("Freeze rails gems?")
+rails_command("rails:freeze:gems") if yes?("Freeze rails gems?")
# no?(question) acts just the opposite.
```
@@ -263,6 +277,6 @@ relative paths to your template's location.
```ruby
def source_paths
- [File.expand_path(File.dirname(__FILE__))]
+ [__dir__]
end
```
diff --git a/guides/source/rails_on_rack.md b/guides/source/rails_on_rack.md
index 1e2fe94010..c33851a0f9 100644
--- a/guides/source/rails_on_rack.md
+++ b/guides/source/rails_on_rack.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Rails on Rack
=============
@@ -13,16 +13,16 @@ After reading this guide, you will know:
--------------------------------------------------------------------------------
-WARNING: This guide assumes a working knowledge of Rack protocol and Rack concepts such as middlewares, url maps and `Rack::Builder`.
+WARNING: This guide assumes a working knowledge of Rack protocol and Rack concepts such as middlewares, url maps, and `Rack::Builder`.
Introduction to Rack
--------------------
-Rack provides a minimal, modular and adaptable interface for developing web applications in Ruby. By wrapping HTTP requests and responses in the simplest way possible, it unifies and distills the API for web servers, web frameworks, and software in between (the so-called middleware) into a single method call.
+Rack provides a minimal, modular, and adaptable interface for developing web applications in Ruby. By wrapping HTTP requests and responses in the simplest way possible, it unifies and distills the API for web servers, web frameworks, and software in between (the so-called middleware) into a single method call.
-* [Rack API Documentation](http://rack.github.io/)
-
-Explaining Rack is not really in the scope of this guide. In case you are not familiar with Rack's basics, you should check out the [Resources](#resources) section below.
+Explaining how Rack works is not really in the scope of this guide. In case you
+are not familiar with Rack's basics, you should check out the [Resources](#resources)
+section below.
Rails on Rack
-------------
@@ -64,7 +64,7 @@ To use `rackup` instead of Rails' `rails server`, you can put the following insi
```ruby
# Rails.root/config.ru
-require ::File.expand_path('../config/environment', __FILE__)
+require_relative 'config/environment'
run Rails.application
```
@@ -74,7 +74,7 @@ And start the server:
$ rackup config.ru
```
-To find out more about different `rackup` options:
+To find out more about different `rackup` options, you can run:
```bash
$ rackup --help
@@ -89,14 +89,15 @@ Action Dispatcher Middleware Stack
Many of Action Dispatcher's internal components are implemented as Rack middlewares. `Rails::Application` uses `ActionDispatch::MiddlewareStack` to combine various internal and external middlewares to form a complete Rails Rack application.
-NOTE: `ActionDispatch::MiddlewareStack` is Rails equivalent of `Rack::Builder`, but built for better flexibility and more features to meet Rails' requirements.
+NOTE: `ActionDispatch::MiddlewareStack` is Rails' equivalent of `Rack::Builder`,
+but is built for better flexibility and more features to meet Rails' requirements.
### Inspecting Middleware Stack
-Rails has a handy rake task for inspecting the middleware stack in use:
+Rails has a handy command for inspecting the middleware stack in use:
```bash
-$ bin/rake middleware
+$ rails middleware
```
For a freshly generated Rails application, this might produce something like:
@@ -104,35 +105,36 @@ For a freshly generated Rails application, this might produce something like:
```ruby
use Rack::Sendfile
use ActionDispatch::Static
-use Rack::Lock
-use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x000000029a0838>
+use ActionDispatch::Executor
+use ActiveSupport::Cache::Strategy::LocalCache::Middleware
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
+use ActionDispatch::RemoteIp
+use Sprockets::Rails::QuietAssets
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
+use WebConsole::Middleware
use ActionDispatch::DebugExceptions
-use ActionDispatch::RemoteIp
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::Migration::CheckPending
-use ActiveRecord::ConnectionAdapters::ConnectionManagement
-use ActiveRecord::QueryCache
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
-use ActionDispatch::ParamsParser
+use ActionDispatch::ContentSecurityPolicy::Middleware
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
-run Rails.application.routes
+use Rack::TempfileReaper
+run MyApp::Application.routes
```
The default middlewares shown here (and some others) are each summarized in the [Internal Middlewares](#internal-middleware-stack) section, below.
### Configuring Middleware Stack
-Rails provides a simple configuration interface `config.middleware` for adding, removing and modifying the middlewares in the middleware stack via `application.rb` or the environment specific configuration file `environments/<environment>.rb`.
+Rails provides a simple configuration interface `config.middleware` for adding, removing, and modifying the middlewares in the middleware stack via `application.rb` or the environment specific configuration file `environments/<environment>.rb`.
#### Adding a Middleware
@@ -150,9 +152,9 @@ You can add a new middleware to the middleware stack using any of the following
# Push Rack::BounceFavicon at the bottom
config.middleware.use Rack::BounceFavicon
-# Add Lifo::Cache after ActiveRecord::QueryCache.
+# Add Lifo::Cache after ActionDispatch::Executor.
# Pass { page_cache: false } argument to Lifo::Cache.
-config.middleware.insert_after ActiveRecord::QueryCache, Lifo::Cache, page_cache: false
+config.middleware.insert_after ActionDispatch::Executor, Lifo::Cache, page_cache: false
```
#### Swapping a Middleware
@@ -172,18 +174,17 @@ Add the following lines to your application configuration:
```ruby
# config/application.rb
-config.middleware.delete "Rack::Lock"
+config.middleware.delete Rack::Runtime
```
-And now if you inspect the middleware stack, you'll find that `Rack::Lock` is
+And now if you inspect the middleware stack, you'll find that `Rack::Runtime` is
not a part of it.
```bash
-$ bin/rake middleware
+$ rails middleware
(in /Users/lifo/Rails/blog)
use ActionDispatch::Static
use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x00000001c304c8>
-use Rack::Runtime
...
run Rails.application.routes
```
@@ -192,16 +193,16 @@ If you want to remove session related middleware, do the following:
```ruby
# config/application.rb
-config.middleware.delete "ActionDispatch::Cookies"
-config.middleware.delete "ActionDispatch::Session::CookieStore"
-config.middleware.delete "ActionDispatch::Flash"
+config.middleware.delete ActionDispatch::Cookies
+config.middleware.delete ActionDispatch::Session::CookieStore
+config.middleware.delete ActionDispatch::Flash
```
And to remove browser related middleware,
```ruby
# config/application.rb
-config.middleware.delete "Rack::MethodOverride"
+config.middleware.delete Rack::MethodOverride
```
### Internal Middleware Stack
@@ -214,12 +215,16 @@ Much of Action Controller's functionality is implemented as Middlewares. The fol
**`ActionDispatch::Static`**
-* Used to serve static files. Disabled if `config.serve_static_files` is `false`.
+* Used to serve static files from the public directory. Disabled if `config.public_file_server.enabled` is `false`.
**`Rack::Lock`**
* Sets `env["rack.multithread"]` flag to `false` and wraps the application within a Mutex.
+**`ActionDispatch::Executor`**
+
+* Used for thread safe code reloading during development.
+
**`ActiveSupport::Cache::Strategy::LocalCache::Middleware`**
* Used for memory caching. This cache is not thread safe.
@@ -236,9 +241,17 @@ Much of Action Controller's functionality is implemented as Middlewares. The fol
* Makes a unique `X-Request-Id` header available to the response and enables the `ActionDispatch::Request#request_id` method.
+**`ActionDispatch::RemoteIp`**
+
+* Checks for IP spoofing attacks.
+
+**`Sprockets::Rails::QuietAssets`**
+
+* Suppresses logger output for asset requests.
+
**`Rails::Rack::Logger`**
-* Notifies the logs that the request has began. After request is complete, flushes all the logs.
+* Notifies the logs that the request has begun. After the request is complete, flushes all the logs.
**`ActionDispatch::ShowExceptions`**
@@ -248,10 +261,6 @@ Much of Action Controller's functionality is implemented as Middlewares. The fol
* Responsible for logging exceptions and showing a debugging page in case the request is local.
-**`ActionDispatch::RemoteIp`**
-
-* Checks for IP spoofing attacks.
-
**`ActionDispatch::Reloader`**
* Provides prepare and cleanup callbacks, intended to assist with code reloading during development.
@@ -264,14 +273,6 @@ Much of Action Controller's functionality is implemented as Middlewares. The fol
* Checks pending migrations and raises `ActiveRecord::PendingMigrationError` if any migrations are pending.
-**`ActiveRecord::ConnectionAdapters::ConnectionManagement`**
-
-* Cleans active connections after each request, unless the `rack.test` key in the request environment is set to `true`.
-
-**`ActiveRecord::QueryCache`**
-
-* Enables the Active Record query cache.
-
**`ActionDispatch::Cookies`**
* Sets cookies for the request.
@@ -284,9 +285,9 @@ Much of Action Controller's functionality is implemented as Middlewares. The fol
* Sets up the flash keys. Only available if `config.action_controller.session_store` is set to a value.
-**`ActionDispatch::ParamsParser`**
+**`ActionDispatch::ContentSecurityPolicy::Middleware`**
-* Parses out parameters from the request into `params`.
+* Provides a DSL to configure a Content-Security-Policy header.
**`Rack::Head`**
@@ -294,12 +295,16 @@ Much of Action Controller's functionality is implemented as Middlewares. The fol
**`Rack::ConditionalGet`**
-* Adds support for "Conditional `GET`" so that server responds with nothing if page wasn't changed.
+* Adds support for "Conditional `GET`" so that server responds with nothing if the page wasn't changed.
**`Rack::ETag`**
* Adds ETag header on all String bodies. ETags are used to validate cache.
+**`Rack::TempfileReaper`**
+
+* Cleans up tempfiles used to buffer multipart requests.
+
TIP: It's possible to use any of the above middlewares in your custom Rack stack.
Resources
@@ -307,7 +312,7 @@ Resources
### Learning Rack
-* [Official Rack Website](http://rack.github.io)
+* [Official Rack Website](https://rack.github.io)
* [Introducing Rack](http://chneukirchen.org/blog/archive/2007/02/introducing-rack.html)
### Understanding Middlewares
diff --git a/guides/source/routing.md b/guides/source/routing.md
index 52f11f92bd..84de727c11 100644
--- a/guides/source/routing.md
+++ b/guides/source/routing.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Rails Routing from the Outside In
=================================
@@ -7,18 +7,18 @@ This guide covers the user-facing features of Rails routing.
After reading this guide, you will know:
-* How to interpret the code in `routes.rb`.
+* How to interpret the code in `config/routes.rb`.
* How to construct your own routes, using either the preferred resourceful style or the `match` method.
-* What parameters to expect an action to receive.
+* How to declare route parameters, which are passed onto controller actions.
* How to automatically create paths and URLs using route helpers.
-* Advanced techniques such as constraints and Rack endpoints.
+* Advanced techniques such as creating constraints and mounting Rack endpoints.
--------------------------------------------------------------------------------
The Purpose of the Rails Router
-------------------------------
-The Rails router recognizes URLs and dispatches them to a controller's action. It can also generate paths and URLs, avoiding the need to hardcode strings in your views.
+The Rails router recognizes URLs and dispatches them to a controller's action, or to a Rack application. It can also generate paths and URLs, avoiding the need to hardcode strings in your views.
### Connecting URLs to Code
@@ -36,6 +36,8 @@ get '/patients/:id', to: 'patients#show'
the request is dispatched to the `patients` controller's `show` action with `{ id: '17' }` in `params`.
+NOTE: Rails uses snake_case for controller names here, if you have a multiple word controller like `MonsterTrucksController`, you want to use `monster_trucks#show` for example.
+
### Generating Paths and URLs from Code
You can also generate paths and URLs. If the route above is modified to be:
@@ -47,7 +49,7 @@ get '/patients/:id', to: 'patients#show', as: 'patient'
and your application contains this code in the controller:
```ruby
-@patient = Patient.find(17)
+@patient = Patient.find(params[:id])
```
and this in the corresponding view:
@@ -58,6 +60,26 @@ and this in the corresponding view:
then the router will generate the path `/patients/17`. This reduces the brittleness of your view and makes your code easier to understand. Note that the id does not need to be specified in the route helper.
+### Configuring the Rails Router
+
+The routes for your application or engine live in the file `config/routes.rb` and typically looks like this:
+
+```ruby
+Rails.application.routes.draw do
+ resources :brands, only: [:index, :show] do
+ resources :products, only: [:index, :show]
+ end
+
+ resource :basket, only: [:show, :update, :destroy]
+
+ resolve("Basket") { route_for(:basket) }
+end
+```
+
+Since this is a regular Ruby source file you can use all of its features to help you define your routes but be careful with variable names as they can clash with the DSL methods of the router.
+
+NOTE: The `Rails.application.routes.draw do ... end` block that wraps your route definitions is required to establish the scope for the router DSL and must not be deleted.
+
Resource Routing: the Rails Default
-----------------------------------
@@ -79,11 +101,13 @@ it asks the router to map it to a controller action. If the first matching route
resources :photos
```
-Rails would dispatch that request to the `destroy` method on the `photos` controller with `{ id: '17' }` in `params`.
+Rails would dispatch that request to the `destroy` action on the `photos` controller with `{ id: '17' }` in `params`.
### CRUD, Verbs, and Actions
-In Rails, a resourceful route provides a mapping between HTTP verbs and URLs to controller actions. By convention, each action also maps to particular CRUD operations in a database. A single entry in the routing file, such as:
+In Rails, a resourceful route provides a mapping between HTTP verbs and URLs to
+controller actions. By convention, each action also maps to a specific CRUD
+operation in a database. A single entry in the routing file, such as:
```ruby
resources :photos
@@ -114,7 +138,7 @@ Creating a resourceful route will also expose a number of helpers to the control
* `edit_photo_path(:id)` returns `/photos/:id/edit` (for instance, `edit_photo_path(10)` returns `/photos/10/edit`)
* `photo_path(:id)` returns `/photos/:id` (for instance, `photo_path(10)` returns `/photos/10`)
-Each of these helpers has a corresponding `_url` helper (such as `photos_url`) which returns the same path prefixed with the current host, port and path prefix.
+Each of these helpers has a corresponding `_url` helper (such as `photos_url`) which returns the same path prefixed with the current host, port, and path prefix.
### Defining Multiple Resources at the Same Time
@@ -140,16 +164,17 @@ Sometimes, you have a resource that clients always look up without referencing a
get 'profile', to: 'users#show'
```
-Passing a `String` to `get` will expect a `controller#action` format, while passing a `Symbol` will map directly to an action:
+Passing a `String` to `to:` will expect a `controller#action` format. When using a `Symbol`, the `to:` option should be replaced with `action:`. When using a `String` without a `#`, the `to:` option should be replaced with `controller:`:
```ruby
-get 'profile', to: :show
+get 'profile', action: :show, controller: 'users'
```
This resourceful route:
```ruby
resource :geocoder
+resolve('Geocoder') { [:geocoder] }
```
creates six different routes in your application, all mapping to the `Geocoders` controller:
@@ -171,15 +196,7 @@ A singular resourceful route generates these helpers:
* `edit_geocoder_path` returns `/geocoder/edit`
* `geocoder_path` returns `/geocoder`
-As with plural resources, the same helpers ending in `_url` will also include the host, port and path prefix.
-
-WARNING: A [long-standing bug](https://github.com/rails/rails/issues/1769) prevents `form_for` from working automatically with singular resources. As a workaround, specify the URL for the form directly, like so:
-
-```ruby
-form_for @geocoder, url: geocoder_path do |f|
-
-# snippet for brevity
-```
+As with plural resources, the same helpers ending in `_url` will also include the host, port, and path prefix.
### Controller Namespaces and Routing
@@ -250,11 +267,11 @@ TIP: _If you need to use a different controller namespace inside a `namespace` b
It's common to have resources that are logically children of other resources. For example, suppose your application includes these models:
```ruby
-class Magazine < ActiveRecord::Base
+class Magazine < ApplicationRecord
has_many :ads
end
-class Ad < ActiveRecord::Base
+class Ad < ApplicationRecord
belongs_to :magazine
end
```
@@ -390,7 +407,7 @@ The comments resource here will have the following routes generated for it:
### Routing concerns
-Routing Concerns allows you to declare common routes that can be reused inside other resources and routes. To define a concern:
+Routing concerns allow you to declare common routes that can be reused inside other resources and routes. To define a concern:
```ruby
concern :commentable do
@@ -423,7 +440,7 @@ resources :articles do
end
```
-Also you can use them in any place that you want inside the routes, for example in a scope or namespace call:
+Also you can use them in any place that you want inside the routes, for example in a `scope` or `namespace` call:
```ruby
namespace :articles do
@@ -543,7 +560,7 @@ TIP: If you find yourself adding many extra actions to a resourceful route, it's
Non-Resourceful Routes
----------------------
-In addition to resource routing, Rails has powerful support for routing arbitrary URLs to actions. Here, you don't get groups of routes automatically generated by resourceful routing. Instead, you set up each route within your application separately.
+In addition to resource routing, Rails has powerful support for routing arbitrary URLs to actions. Here, you don't get groups of routes automatically generated by resourceful routing. Instead, you set up each route separately within your application.
While you should usually use resourceful routing, there are still many places where the simpler routing is more appropriate. There's no need to try to shoehorn every last piece of your application into a resourceful framework if that's not a good fit.
@@ -551,29 +568,23 @@ In particular, simple routing makes it very easy to map legacy URLs to new Rails
### Bound Parameters
-When you set up a regular route, you supply a series of symbols that Rails maps to parts of an incoming HTTP request. Two of these symbols are special: `:controller` maps to the name of a controller in your application, and `:action` maps to the name of an action within that controller. For example, consider this route:
+When you set up a regular route, you supply a series of symbols that Rails maps to parts of an incoming HTTP request. For example, consider this route:
```ruby
-get ':controller(/:action(/:id))'
+get 'photos(/:id)', to: 'photos#display'
```
-If an incoming request of `/photos/show/1` is processed by this route (because it hasn't matched any previous route in the file), then the result will be to invoke the `show` action of the `PhotosController`, and to make the final parameter `"1"` available as `params[:id]`. This route will also route the incoming request of `/photos` to `PhotosController#index`, since `:action` and `:id` are optional parameters, denoted by parentheses.
+If an incoming request of `/photos/1` is processed by this route (because it hasn't matched any previous route in the file), then the result will be to invoke the `display` action of the `PhotosController`, and to make the final parameter `"1"` available as `params[:id]`. This route will also route the incoming request of `/photos` to `PhotosController#display`, since `:id` is an optional parameter, denoted by parentheses.
### Dynamic Segments
-You can set up as many dynamic segments within a regular route as you like. Anything other than `:controller` or `:action` will be available to the action as part of `params`. If you set up this route:
+You can set up as many dynamic segments within a regular route as you like. Any segment will be available to the action as part of `params`. If you set up this route:
```ruby
-get ':controller/:action/:id/:user_id'
+get 'photos/:id/:user_id', to: 'photos#show'
```
-An incoming path of `/photos/show/1/2` will be dispatched to the `show` action of the `PhotosController`. `params[:id]` will be `"1"`, and `params[:user_id]` will be `"2"`.
-
-NOTE: You can't use `:namespace` or `:module` with a `:controller` path segment. If you need to do this then use a constraint on :controller that matches the namespace you require. e.g:
-
-```ruby
-get ':controller(/:action(/:id))', controller: /admin\/[^\/]+/
-```
+An incoming path of `/photos/1/2` will be dispatched to the `show` action of the `PhotosController`. `params[:id]` will be `"1"`, and `params[:user_id]` will be `"2"`.
TIP: By default, dynamic segments don't accept dots - this is because the dot is used as a separator for formatted routes. If you need to use a dot within a dynamic segment, add a constraint that overrides this – for example, `id: /[^\/]+/` allows anything except a slash.
@@ -582,38 +593,40 @@ TIP: By default, dynamic segments don't accept dots - this is because the dot is
You can specify static segments when creating a route by not prepending a colon to a fragment:
```ruby
-get ':controller/:action/:id/with_user/:user_id'
+get 'photos/:id/with_user/:user_id', to: 'photos#show'
```
-This route would respond to paths such as `/photos/show/1/with_user/2`. In this case, `params` would be `{ controller: 'photos', action: 'show', id: '1', user_id: '2' }`.
+This route would respond to paths such as `/photos/1/with_user/2`. In this case, `params` would be `{ controller: 'photos', action: 'show', id: '1', user_id: '2' }`.
### The Query String
The `params` will also include any parameters from the query string. For example, with this route:
```ruby
-get ':controller/:action/:id'
+get 'photos/:id', to: 'photos#show'
```
-An incoming path of `/photos/show/1?user_id=2` will be dispatched to the `show` action of the `Photos` controller. `params` will be `{ controller: 'photos', action: 'show', id: '1', user_id: '2' }`.
+An incoming path of `/photos/1?user_id=2` will be dispatched to the `show` action of the `Photos` controller. `params` will be `{ controller: 'photos', action: 'show', id: '1', user_id: '2' }`.
### Defining Defaults
-You do not need to explicitly use the `:controller` and `:action` symbols within a route. You can supply them as defaults:
+You can define defaults in a route by supplying a hash for the `:defaults` option. This even applies to parameters that you do not specify as dynamic segments. For example:
```ruby
-get 'photos/:id', to: 'photos#show'
+get 'photos/:id', to: 'photos#show', defaults: { format: 'jpg' }
```
-With this route, Rails will match an incoming path of `/photos/12` to the `show` action of `PhotosController`.
+Rails would match `photos/12` to the `show` action of `PhotosController`, and set `params[:format]` to `"jpg"`.
-You can also define other defaults in a route by supplying a hash for the `:defaults` option. This even applies to parameters that you do not specify as dynamic segments. For example:
+You can also use `defaults` in a block format to define the defaults for multiple items:
```ruby
-get 'photos/:id', to: 'photos#show', defaults: { format: 'jpg' }
+defaults format: :json do
+ resources :photos
+end
```
-Rails would match `photos/12` to the `show` action of `PhotosController`, and set `params[:format]` to `"jpg"`.
+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
@@ -631,7 +644,7 @@ You can also use this to override routing methods defined by resources, like thi
get ':username', to: 'users#show', as: :user
```
-This will define a `user_path` method that will be available in controllers, helpers and views that will go to a route such as `/bob`. Inside the `show` action of `UsersController`, `params[:username]` will contain the username for the user. Change `:username` in the route definition if you do not want your parameter name to be `:username`.
+This will define a `user_path` method that will be available in controllers, helpers, and views that will go to a route such as `/bob`. Inside the `show` action of `UsersController`, `params[:username]` will contain the username for the user. Change `:username` in the route definition if you do not want your parameter name to be `:username`.
### HTTP Verb Constraints
@@ -649,7 +662,7 @@ match 'photos', to: 'photos#show', via: :all
NOTE: Routing both `GET` and `POST` requests to a single action has security implications. In general, you should avoid routing all verbs to an action unless you have a good reason to.
-NOTE: 'GET' in Rails won't check for CSRF token. You should never write to the database from 'GET' requests, for more information see the [security guide](security.html#csrf-countermeasures) on CSRF countermeasures.
+NOTE: `GET` in Rails won't check for CSRF token. You should never write to the database from `GET` requests, for more information see the [security guide](security.html#csrf-countermeasures) on CSRF countermeasures.
### Segment Constraints
@@ -702,14 +715,16 @@ end
NOTE: Request constraints work by calling a method on the [Request object](action_controller_overview.html#the-request-object) with the same name as the hash key and then compare the return value with the hash value. Therefore, constraint values should match the corresponding Request object method return type. For example: `constraints: { subdomain: 'api' }` will match an `api` subdomain as expected, however using a symbol `constraints: { subdomain: :api }` will not, because `request.subdomain` returns `'api'` as a String.
+NOTE: There is an exception for the `format` constraint: while it's a method on the Request object, it's also an implicit optional parameter on every path. Segment constraints take precedence and the `format` constraint is only applied as such when enforced through a hash. For example, `get 'foo', constraints: { format: 'json' }` will match `GET /foo` because the format is optional by default. However, you can [use a lambda](#advanced-constraints) like in `get 'foo', constraints: lambda { |req| req.format == :json }` and the route will only match explicit JSON requests.
+
### Advanced Constraints
-If you have a more advanced constraint, you can provide an object that responds to `matches?` that Rails should use. Let's say you wanted to route all users on a blacklist to the `BlacklistController`. You could do:
+If you have a more advanced constraint, you can provide an object that responds to `matches?` that Rails should use. Let's say you wanted to route all users on a restricted list to the `RestrictedListController`. You could do:
```ruby
-class BlacklistConstraint
+class RestrictedListConstraint
def initialize
- @ips = Blacklist.retrieve_ips
+ @ips = RestrictedList.retrieve_ips
end
def matches?(request)
@@ -718,8 +733,8 @@ class BlacklistConstraint
end
Rails.application.routes.draw do
- get '*path', to: 'blacklist#index',
- constraints: BlacklistConstraint.new
+ get '*path', to: 'restricted_list#index',
+ constraints: RestrictedListConstraint.new
end
```
@@ -727,8 +742,8 @@ You can also specify constraints as a lambda:
```ruby
Rails.application.routes.draw do
- get '*path', to: 'blacklist#index',
- constraints: lambda { |request| Blacklist.retrieve_ips.include?(request.remote_ip) }
+ get '*path', to: 'restricted_list#index',
+ constraints: lambda { |request| RestrictedList.retrieve_ips.include?(request.remote_ip) }
end
```
@@ -793,7 +808,11 @@ get '/stories/:name', to: redirect { |path_params, req| "/articles/#{path_params
get '/stories', to: redirect { |path_params, req| "/articles/#{req.subdomain}" }
```
-Please note that this redirection is a 301 "Moved Permanently" redirect. Keep in mind that some web browsers or proxy servers will cache this type of redirect, making the old page inaccessible.
+Please note that default redirection is a 301 "Moved Permanently" redirect. Keep in mind that some web browsers or proxy servers will cache this type of redirect, making the old page inaccessible. You can use the `:status` option to change the response status:
+
+```ruby
+get '/stories/:name', to: redirect('/articles/%{name}', status: 302)
+```
In all of these cases, if you don't provide the leading host (`http://www.example.com`), Rails will take those details from the current request.
@@ -802,23 +821,23 @@ In all of these cases, if you don't provide the leading host (`http://www.exampl
Instead of a String like `'articles#index'`, which corresponds to the `index` action in the `ArticlesController`, you can specify any [Rack application](rails_on_rack.html) as the endpoint for a matcher:
```ruby
-match '/application.js', to: Sprockets, via: :all
+match '/application.js', to: MyRackApp, via: :all
```
-As long as `Sprockets` responds to `call` and returns a `[status, headers, body]`, the router won't know the difference between the Rack application and an action. This is an appropriate use of `via: :all`, as you will want to allow your Rack application to handle all verbs as it considers appropriate.
+As long as `MyRackApp` responds to `call` and returns a `[status, headers, body]`, the router won't know the difference between the Rack application and an action. This is an appropriate use of `via: :all`, as you will want to allow your Rack application to handle all verbs as it considers appropriate.
NOTE: For the curious, `'articles#index'` actually expands out to `ArticlesController.action(:index)`, which returns a valid Rack application.
If you specify a Rack application as the endpoint for a matcher, remember that
the route will be unchanged in the receiving application. With the following
-route your Rack application should expect the route to be '/admin':
+route your Rack application should expect the route to be `/admin`:
```ruby
match '/admin', to: AdminApp, via: :all
```
If you would prefer to have your Rack application receive requests at the root
-path instead, use mount:
+path instead, use `mount`:
```ruby
mount AdminApp, at: '/admin'
@@ -855,6 +874,49 @@ You can specify unicode character routes directly. For example:
get 'こんにちは', to: 'welcome#index'
```
+### Direct routes
+
+You can create custom URL helpers directly. For example:
+
+```ruby
+direct :homepage do
+ "http://www.rubyonrails.org"
+end
+
+# >> homepage_url
+# => "http://www.rubyonrails.org"
+```
+
+The return value of the block must be a valid argument for the `url_for` method. So, you can pass a valid string URL, Hash, Array, an Active Model instance, or an Active Model class.
+
+```ruby
+direct :commentable do |model|
+ [ model, anchor: model.dom_id ]
+end
+
+direct :main do
+ { controller: 'pages', action: 'index', subdomain: 'www' }
+end
+```
+
+### Using `resolve`
+
+The `resolve` method allows customizing polymorphic mapping of models. For example:
+
+``` ruby
+resource :basket
+
+resolve("Basket") { [:basket] }
+```
+
+``` erb
+<%= form_for @basket do |form| %>
+ <!-- basket form -->
+<% end %>
+```
+
+This will generate the singular URL `/basket` instead of the usual `/baskets/:id`.
+
Customizing Resourceful Routes
------------------------------
@@ -999,7 +1061,7 @@ scope ':username' do
end
```
-This will provide you with URLs such as `/bob/articles/1` and will allow you to reference the `username` part of the path as `params[:username]` in controllers, helpers and views.
+This will provide you with URLs such as `/bob/articles/1` and will allow you to reference the `username` part of the path as `params[:username]` in controllers, helpers, and views.
### Restricting the Routes Created
@@ -1077,16 +1139,30 @@ resources :videos, param: :identifier
```
```
- videos GET /videos(.:format) videos#index
- POST /videos(.:format) videos#create
- new_videos GET /videos/new(.:format) videos#new
-edit_videos GET /videos/:identifier/edit(.:format) videos#edit
+ videos GET /videos(.:format) videos#index
+ POST /videos(.:format) videos#create
+ new_video GET /videos/new(.:format) videos#new
+edit_video GET /videos/:identifier/edit(.:format) videos#edit
```
```ruby
Video.find_by(identifier: params[:identifier])
```
+You can override `ActiveRecord::Base#to_param` of a related model to construct
+a URL:
+
+```ruby
+class Video < ApplicationRecord
+ def to_param
+ identifier
+ end
+end
+
+video = Video.find_by(identifier: "Roman-Holiday")
+edit_video_path(video) # => "/videos/Roman-Holiday/edit"
+```
+
Inspecting and Testing Routes
-----------------------------
@@ -1094,16 +1170,16 @@ Rails offers facilities for inspecting and testing your routes.
### Listing Existing Routes
-To get a complete list of the available routes in your application, visit `http://localhost:3000/rails/info/routes` in your browser while your server is running in the **development** environment. You can also execute the `rake routes` command in your terminal to produce the same output.
+To get a complete list of the available routes in your application, visit `http://localhost:3000/rails/info/routes` in your browser while your server is running in the **development** environment. You can also execute the `rails routes` command in your terminal to produce the same output.
-Both methods will list all of your routes, in the same order that they appear in `routes.rb`. For each route, you'll see:
+Both methods will list all of your routes, in the same order that they appear in `config/routes.rb`. For each route, you'll see:
* The route name (if any)
* The HTTP verb used (if the route doesn't respond to all verbs)
* The URL pattern to match
* The routing parameters for the route
-For example, here's a small section of the `rake routes` output for a RESTful route:
+For example, here's a small section of the `rails routes` output for a RESTful route:
```
users GET /users(.:format) users#index
@@ -1112,13 +1188,24 @@ For example, here's a small section of the `rake routes` output for a RESTful ro
edit_user GET /users/:id/edit(.:format) users#edit
```
-You may restrict the listing to the routes that map to a particular controller setting the `CONTROLLER` environment variable:
+You can search through your routes with the grep option: -g. This outputs any routes that partially match the URL helper method name, the HTTP verb, or the URL path.
+
+```
+$ rails routes -g new_comment
+$ rails routes -g POST
+$ rails routes -g admin
+```
+
+If you only want to see the routes that map to a specific controller, there's the -c option.
-```bash
-$ CONTROLLER=users bin/rake routes
+```
+$ rails routes -c users
+$ rails routes -c admin/users
+$ rails routes -c Comments
+$ rails routes -c Articles::CommentsController
```
-TIP: You'll find that the output from `rake routes` is much more readable if you widen your terminal window until the output lines don't wrap.
+TIP: You'll find that the output from `rails routes` is much more readable if you widen your terminal window until the output lines don't wrap. You can also use --expanded option to turn on the expanded table formatting mode.
### Testing Routes
diff --git a/guides/source/ruby_on_rails_guides_guidelines.md b/guides/source/ruby_on_rails_guides_guidelines.md
index 50866350f8..4b56cf6296 100644
--- a/guides/source/ruby_on_rails_guides_guidelines.md
+++ b/guides/source/ruby_on_rails_guides_guidelines.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Ruby on Rails Guides Guidelines
===============================
@@ -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
----------------------------
@@ -65,8 +107,8 @@ HTML Guides
-----------
Before generating the guides, make sure that you have the latest version of
-Bundler installed on your system. As of this writing, you must install Bundler
-1.3.5 or later on your device.
+Bundler installed on your system. You can find the latest Bundler version
+[here](https://rubygems.org/gems/bundler). As of this writing, it's v1.17.1.
To install the latest version of Bundler, run `gem install bundler`.
@@ -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 485b108d12..dbec3cdd2d 100644
--- a/guides/source/security.md
+++ b/guides/source/security.md
@@ -1,7 +1,7 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
-Ruby on Rails Security Guide
-============================
+Securing Rails Applications
+===========================
This manual describes common security problems in web applications and how to avoid them with Rails.
@@ -21,13 +21,13 @@ Introduction
Web application frameworks are made to help developers build web applications. Some of them also help you with securing the web application. In fact one framework is not more secure than another: If you use it correctly, you will be able to build secure apps with many frameworks. Ruby on Rails has some clever helper methods, for example against SQL injection, so that this is hardly a problem.
-In general there is no such thing as plug-n-play security. Security depends on the people using the framework, and sometimes on the development method. And it depends on all layers of a web application environment: The back-end storage, the web server and the web application itself (and possibly other layers or applications).
+In general there is no such thing as plug-n-play security. Security depends on the people using the framework, and sometimes on the development method. And it depends on all layers of a web application environment: The back-end storage, the web server, and the web application itself (and possibly other layers or applications).
-The Gartner Group however estimates that 75% of attacks are at the web application layer, and found out "that out of 300 audited sites, 97% are vulnerable to attack". This is because web applications are relatively easy to attack, as they are simple to understand and manipulate, even by the lay person.
+The Gartner Group, however, estimates that 75% of attacks are at the web application layer, and found out "that out of 300 audited sites, 97% are vulnerable to attack". This is because web applications are relatively easy to attack, as they are simple to understand and manipulate, even by the lay person.
-The threats against web applications include user account hijacking, bypass of access control, reading or modifying sensitive data, or presenting fraudulent content. Or an attacker might be able to install a Trojan horse program or unsolicited e-mail sending software, aim at financial enrichment or cause brand name damage by modifying company resources. In order to prevent attacks, minimize their impact and remove points of attack, first of all, you have to fully understand the attack methods in order to find the correct countermeasures. That is what this guide aims at.
+The threats against web applications include user account hijacking, bypass of access control, reading or modifying sensitive data, or presenting fraudulent content. Or an attacker might be able to install a Trojan horse program or unsolicited e-mail sending software, aim at financial enrichment, or cause brand name damage by modifying company resources. In order to prevent attacks, minimize their impact and remove points of attack, first of all, you have to fully understand the attack methods in order to find the correct countermeasures. That is what this guide aims at.
-In order to develop secure web applications you have to keep up to date on all layers and know your enemies. To keep up to date subscribe to security mailing lists, read security blogs and make updating and security checks a habit (check the [Additional Resources](#additional-resources) chapter). It is done manually because that's how you find the nasty logical security problems.
+In order to develop secure web applications you have to keep up to date on all layers and know your enemies. To keep up to date subscribe to security mailing lists, read security blogs, and make updating and security checks a habit (check the [Additional Resources](#additional-resources) chapter). It is done manually because that's how you find the nasty logical security problems.
Sessions
--------
@@ -41,28 +41,28 @@ NOTE: _HTTP is a stateless protocol. Sessions make it stateful._
Most applications need to keep track of certain state of a particular user. This could be the contents of a shopping basket or the user id of the currently logged in user. Without the idea of sessions, the user would have to identify, and probably authenticate, on every request.
Rails will create a new session automatically if a new user accesses the application. It will load an existing session if the user has already used the application.
-A session usually consists of a hash of values and a session id, usually a 32-character string, to identify the hash. Every cookie sent to the client's browser includes the session id. And the other way round: the browser will send it to the server on every request from the client. In Rails you can save and retrieve values using the session method:
+A session usually consists of a hash of values and a session ID, usually a 32-character string, to identify the hash. Every cookie sent to the client's browser includes the session ID. And the other way round: the browser will send it to the server on every request from the client. In Rails you can save and retrieve values using the session method:
```ruby
session[:user_id] = @current_user.id
User.find(session[:user_id])
```
-### Session id
+### Session ID
-NOTE: _The session id is a 32 byte long MD5 hash value._
+NOTE: _The session ID is a 32-character random hex string._
-A session id consists of the hash value of a random string. The random string is the current time, a random number between 0 and 1, the process id number of the Ruby interpreter (also basically a random number) and a constant string. Currently it is not feasible to brute-force Rails' session ids. To date MD5 is uncompromised, but there have been collisions, so it is theoretically possible to create another input text with the same hash value. But this has had no security impact to date.
+The session ID is generated using `SecureRandom.hex` which generates a random hex string using platform specific methods (such as OpenSSL, /dev/urandom or Win32 CryptoAPI) for generating cryptographically secure random numbers. Currently it is not feasible to brute-force Rails' session IDs.
### Session Hijacking
-WARNING: _Stealing a user's session id lets an attacker use the web application in the victim's name._
+WARNING: _Stealing a user's session ID lets an attacker use the web application in the victim's name._
-Many web applications have an authentication system: a user provides a user name and password, the web application checks them and stores the corresponding user id in the session hash. From now on, the session is valid. On every request the application will load the user, identified by the user id in the session, without the need for new authentication. The session id in the cookie identifies the session.
+Many web applications have an authentication system: a user provides a user name and password, the web application checks them and stores the corresponding user id in the session hash. From now on, the session is valid. On every request the application will load the user, identified by the user id in the session, without the need for new authentication. The session ID in the cookie identifies the session.
Hence, the cookie serves as temporary authentication for the web application. Anyone who seizes a cookie from someone else, may use the web application as this user - with possibly severe consequences. Here are some ways to hijack a session, and their countermeasures:
-* Sniff the cookie in an insecure network. A wireless LAN can be an example of such a network. In an unencrypted wireless LAN it is especially easy to listen to the traffic of all connected clients. For the web application builder this means to _provide a secure connection over SSL_. In Rails 3.1 and later, this could be accomplished by always forcing SSL connection in your application config file:
+* Sniff the cookie in an insecure network. A wireless LAN can be an example of such a network. In an unencrypted wireless LAN, it is especially easy to listen to the traffic of all connected clients. For the web application builder this means to _provide a secure connection over SSL_. In Rails 3.1 and later, this could be accomplished by always forcing SSL connection in your application config file:
```ruby
config.force_ssl = true
@@ -74,7 +74,7 @@ Hence, the cookie serves as temporary authentication for the web application. An
* Instead of stealing a cookie unknown to the attacker, they fix a user's session identifier (in the cookie) known to them. Read more about this so-called session fixation later.
-The main objective of most attackers is to make money. The underground prices for stolen bank login accounts range from $10-$1000 (depending on the available amount of funds), $0.40-$20 for credit card numbers, $1-$8 for online auction site accounts and $4-$30 for email passwords, according to the [Symantec Global Internet Security Threat Report](http://eval.symantec.com/mktginfo/enterprise/white_papers/b-whitepaper_internet_security_threat_report_xiii_04-2008.en-us.pdf).
+The main objective of most attackers is to make money. The underground prices for stolen bank login accounts range from 0.5%-10% of account balance, $0.5-$30 for credit card numbers ($20-$60 with full details), $0.1-$1.5 for identities (Name, SSN & DOB), $20-$50 for retailer accounts, and $6-$10 for cloud service provider accounts, according to the [Symantec Internet Security Threat Report (2017)](https://www.symantec.com/content/dam/symantec/docs/reports/istr-22-2017-en.pdf).
### Session Guidelines
@@ -85,32 +85,117 @@ This will also be a good idea, if you modify the structure of an object and old
* _Critical data should not be stored in session_. If the user clears their cookies or closes the browser, they will be lost. And with a client-side session storage, the user can read the data.
-### Session Storage
+### Encrypted Session Storage
NOTE: _Rails provides several storage mechanisms for the session hashes. The most important is `ActionDispatch::Session::CookieStore`._
-Rails 2 introduced a new default session storage, CookieStore. CookieStore saves the session hash directly in a cookie on the client-side. The server retrieves the session hash from the cookie and eliminates the need for a session id. That will greatly increase the speed of the application, but it is a controversial storage option and you have to think about the security implications of it:
+The `CookieStore` saves the session hash directly in a cookie on the
+client-side. The server retrieves the session hash from the cookie and
+eliminates the need for a session ID. That will greatly increase the
+speed of the application, but it is a controversial storage option and
+you have to think about the security implications and storage
+limitations of it:
+
+* Cookies imply a strict size limit of 4kB. This is fine as you should
+ not store large amounts of data in a session anyway, as described
+ before. Storing the current user's database id in a session is common
+ practice.
+
+* Session cookies do not invalidate themselves and can be maliciously
+ reused. It may be a good idea to have your application invalidate old
+ session cookies using a stored timestamp.
+
+The `CookieStore` uses the
+[encrypted](http://api.rubyonrails.org/classes/ActionDispatch/Cookies/ChainedCookieJars.html#method-i-encrypted)
+cookie jar to provide a secure, encrypted location to store session
+data. Cookie-based sessions thus provide both integrity as well as
+confidentiality to their contents. The encryption key, as well as the
+verification key used for
+[signed](http://api.rubyonrails.org/classes/ActionDispatch/Cookies/ChainedCookieJars.html#method-i-signed)
+cookies, is derived from the `secret_key_base` configuration value.
+
+As of Rails 5.2 encrypted cookies and sessions are protected using AES
+GCM encryption. This form of encryption is a type of Authenticated
+Encryption and couples authentication and encryption in single step
+while also producing shorter ciphertexts as compared to other
+algorithms previously used. The key for cookies encrypted with AES GCM
+are derived using a salt value defined by the
+`config.action_dispatch.authenticated_encrypted_cookie_salt`
+configuration value.
+
+Prior to this version, encrypted cookies were secured using AES in CBC
+mode with HMAC using SHA1 for authentication. The keys for this type of
+encryption and for HMAC verification were derived via the salts defined
+by `config.action_dispatch.encrypted_cookie_salt` and
+`config.action_dispatch.encrypted_signed_cookie_salt` respectively.
+
+Prior to Rails version 4 in both versions 2 and 3, session cookies were
+protected using only HMAC verification. As such, these session cookies
+only provided integrity to their content because the actual session data
+was stored in plaintext encoded as base64. This is how `signed` cookies
+work in the current version of Rails. These kinds of cookies are still
+useful for protecting the integrity of certain client-stored data and
+information.
+
+__Do not use a trivial secret for the `secret_key_base`, i.e. a word
+from a dictionary, or one which is shorter than 30 characters! Instead
+use `rails secret` to generate secret keys!__
+
+It is also important to use different salt values for encrypted and
+signed cookies. Using the same value for different salt configuration
+values may lead to the same derived key being used for different
+security features which in turn may weaken the strength of the key.
+
+In test and development applications get a `secret_key_base` derived from the app name. Other environments must use a random key present in `config/credentials.yml.enc`, shown here in its decrypted state:
+
+ secret_key_base: 492f...
-* Cookies imply a strict size limit of 4kB. This is fine as you should not store large amounts of data in a session anyway, as described before. _Storing the current user's database id in a session is usually ok_.
+If you have received an application where the secret was exposed (e.g. an application whose source was shared), strongly consider changing the secret.
-* The client can see everything you store in a session, because it is stored in clear-text (actually Base64-encoded, so not encrypted). So, of course, _you don't want to store any secrets here_. To prevent session hash tampering, a digest is calculated from the session with a server-side secret and inserted into the end of the cookie.
+### Rotating Encrypted and Signed Cookies Configurations
-That means the security of this storage depends on this secret (and on the digest algorithm, which defaults to SHA1, for compatibility). So _don't use a trivial secret, i.e. a word from a dictionary, or one which is shorter than 30 characters_.
+Rotation is ideal for changing cookie configurations and ensuring old cookies
+aren't immediately invalid. Your users then have a chance to visit your site,
+get their cookie read with an old configuration and have it rewritten with the
+new change. The rotation can then be removed once you're comfortable enough
+users have had their chance to get their cookies upgraded.
-`secrets.secret_key_base` is used for specifying a key which allows sessions for the application to be verified against a known secure key to prevent tampering. Applications get `secrets.secret_key_base` initialized to a random key present in `config/secrets.yml`, e.g.:
+It's possible to rotate the ciphers and digests used for encrypted and signed cookies.
- development:
- secret_key_base: a75d...
+For instance to change the digest used for signed cookies from SHA1 to SHA256,
+you would first assign the new configuration value:
- test:
- secret_key_base: 492f...
+```ruby
+Rails.application.config.action_dispatch.signed_cookie_digest = "SHA256"
+```
- production:
- secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
+Now add a rotation for the old SHA1 digest so existing cookies are
+seamlessly upgraded to the new SHA256 digest.
-Older versions of Rails use CookieStore, which uses `secret_token` instead of `secret_key_base` that is used by EncryptedCookieStore. Read the upgrade documentation for more information.
+```ruby
+Rails.application.config.action_dispatch.cookies_rotations.tap do |cookies|
+ cookies.rotate :signed, digest: "SHA1"
+end
+```
-If you have received an application where the secret was exposed (e.g. an application whose source was shared), strongly consider changing the secret.
+Then any written signed cookies will be digested with SHA256. Old cookies
+that were written with SHA1 can still be read, and if accessed will be written
+with the new digest so they're upgraded and won't be invalid when you remove the
+rotation.
+
+Once users with SHA1 digested signed cookies should no longer have a chance to
+have their cookies rewritten, remove the rotation.
+
+While you can setup as many rotations as you'd like it's not common to have many
+rotations going at any one time.
+
+For more details on key rotation with encrypted and signed messages as
+well as the various options the `rotate` method accepts, please refer to
+the
+[MessageEncryptor API](http://api.rubyonrails.org/classes/ActiveSupport/MessageEncryptor.html)
+and
+[MessageVerifier API](http://api.rubyonrails.org/classes/ActiveSupport/MessageVerifier.html)
+documentation.
### Replay Attacks for CookieStore Sessions
@@ -124,22 +209,22 @@ It works like this:
* The user takes the cookie from the first step (which they previously copied) and replaces the current cookie in the browser.
* The user has their original credit back.
-Including a nonce (a random value) in the session solves replay attacks. A nonce is valid only once, and the server has to keep track of all the valid nonces. It gets even more complicated if you have several application servers (mongrels). Storing nonces in a database table would defeat the entire purpose of CookieStore (avoiding accessing the database).
+Including a nonce (a random value) in the session solves replay attacks. A nonce is valid only once, and the server has to keep track of all the valid nonces. It gets even more complicated if you have several application servers. Storing nonces in a database table would defeat the entire purpose of CookieStore (avoiding accessing the database).
The best _solution against it is not to store this kind of data in a session, but in the database_. In this case store the credit in the database and the logged_in_user_id in the session.
### Session Fixation
-NOTE: _Apart from stealing a user's session id, the attacker may fix a session id known to them. This is called session fixation._
+NOTE: _Apart from stealing a user's session ID, the attacker may fix a session ID known to them. This is called session fixation._
-![Session fixation](images/session_fixation.png)
+![Session fixation](images/security/session_fixation.png)
-This attack focuses on fixing a user's session id known to the attacker, and forcing the user's browser into using this id. It is therefore not necessary for the attacker to steal the session id afterwards. Here is how this attack works:
+This attack focuses on fixing a user's session ID known to the attacker, and forcing the user's browser into using this ID. It is therefore not necessary for the attacker to steal the session ID afterwards. Here is how this attack works:
-* The attacker creates a valid session id: They load the login page of the web application where they want to fix the session, and take the session id in the cookie from the response (see number 1 and 2 in the image).
+* The attacker creates a valid session ID: They load the login page of the web application where they want to fix the session, and take the session ID in the cookie from the response (see number 1 and 2 in the image).
* They maintain the session by accessing the web application periodically in order to keep an expiring session alive.
-* The attacker forces the user's browser into using this session id (see number 3 in the image). As you may not change a cookie of another domain (because of the same origin policy), the attacker has to run a JavaScript from the domain of the target web application. Injecting the JavaScript code into the application by XSS accomplishes this attack. Here is an example: `<script>document.cookie="_session_id=16d5b78abb28e3d6206b60f22a03c8d9";</script>`. Read more about XSS and injection later on.
-* The attacker lures the victim to the infected page with the JavaScript code. By viewing the page, the victim's browser will change the session id to the trap session id.
+* The attacker forces the user's browser into using this session ID (see number 3 in the image). As you may not change a cookie of another domain (because of the same origin policy), the attacker has to run a JavaScript from the domain of the target web application. Injecting the JavaScript code into the application by XSS accomplishes this attack. Here is an example: `<script>document.cookie="_session_id=16d5b78abb28e3d6206b60f22a03c8d9";</script>`. Read more about XSS and injection later on.
+* The attacker lures the victim to the infected page with the JavaScript code. By viewing the page, the victim's browser will change the session ID to the trap session ID.
* As the new trap session is unused, the web application will require the user to authenticate.
* From now on, the victim and the attacker will co-use the web application with the same session: The session became valid and the victim didn't notice the attack.
@@ -153,18 +238,18 @@ The most effective countermeasure is to _issue a new session identifier_ and dec
reset_session
```
-If you use the popular RestfulAuthentication plugin for user management, add reset_session to the SessionsController#create action. Note that this removes any value from the session, _you have to transfer them to the new session_.
+If you use the popular [Devise](https://rubygems.org/gems/devise) gem for user management, it will automatically expire sessions on sign in and sign out for you. If you roll your own, remember to expire the session after your sign in action (when the session is created). This will remove values from the session, therefore _you will have to transfer them to the new session_.
Another countermeasure is to _save user-specific properties in the session_, verify them every time a request comes in, and deny access, if the information does not match. Such properties could be the remote IP address or the user agent (the web browser name), though the latter is less user-specific. When saving the IP address, you have to bear in mind that there are Internet service providers or large organizations that put their users behind proxies. _These might change over the course of a session_, so these users will not be able to use your application, or only in a limited way.
### Session Expiry
-NOTE: _Sessions that never expire extend the time-frame for attacks such as cross-site request forgery (CSRF), session hijacking and session fixation._
+NOTE: _Sessions that never expire extend the time-frame for attacks such as cross-site request forgery (CSRF), session hijacking, and session fixation._
-One possibility is to set the expiry time-stamp of the cookie with the session id. However the client can edit cookies that are stored in the web browser so expiring sessions on the server is safer. Here is an example of how to _expire sessions in a database table_. Call `Session.sweep("20 minutes")` to expire sessions that were used longer than 20 minutes ago.
+One possibility is to set the expiry time-stamp of the cookie with the session ID. However the client can edit cookies that are stored in the web browser so expiring sessions on the server is safer. Here is an example of how to _expire sessions in a database table_. Call `Session.sweep("20 minutes")` to expire sessions that were used longer than 20 minutes ago.
```ruby
-class Session < ActiveRecord::Base
+class Session < ApplicationRecord
def self.sweep(time = 1.hour)
if time.is_a?(String)
time = time.split.inject { |count, unit| count.to_i.send(unit) }
@@ -175,7 +260,7 @@ class Session < ActiveRecord::Base
end
```
-The section about session fixation introduced the problem of maintained sessions. An attacker maintaining a session every five minutes can keep the session alive forever, although you are expiring sessions. A simple solution for this would be to add a created_at column to the sessions table. Now you can delete sessions that were created a long time ago. Use this line in the sweep method above:
+The section about session fixation introduced the problem of maintained sessions. An attacker maintaining a session every five minutes can keep the session alive forever, although you are expiring sessions. A simple solution for this would be to add a `created_at` column to the sessions table. Now you can delete sessions that were created a long time ago. Use this line in the sweep method above:
```ruby
delete_all "updated_at < '#{time.ago.to_s(:db)}' OR
@@ -187,18 +272,17 @@ Cross-Site Request Forgery (CSRF)
This attack method works by including malicious code or a link in a page that accesses a web application that the user is believed to have authenticated. If the session for that web application has not timed out, an attacker may execute unauthorized commands.
-![](images/csrf.png)
+![](images/security/csrf.png)
-In the [session chapter](#sessions) you have learned that most Rails applications use cookie-based sessions. Either they store the session id in the cookie and have a server-side session hash, or the entire session hash is on the client-side. In either case the browser will automatically send along the cookie on every request to a domain, if it can find a cookie for that domain. The controversial point is, that it will also send the cookie, if the request comes from a site of a different domain. Let's start with an example:
+In the [session chapter](#sessions) you have learned that most Rails applications use cookie-based sessions. Either they store the session ID in the cookie and have a server-side session hash, or the entire session hash is on the client-side. In either case the browser will automatically send along the cookie on every request to a domain, if it can find a cookie for that domain. The controversial point is that if the request comes from a site of a different domain, it will also send the cookie. Let's start with an example:
-* Bob browses a message board and views a post from a hacker where there is a crafted HTML image element. The element references a command in Bob's project management application, rather than an image file.
-* `<img src="http://www.webapp.com/project/1/destroy">`
-* Bob's session at www.webapp.com is still alive, because he didn't log out a few minutes ago.
-* By viewing the post, the browser finds an image tag. It tries to load the suspected image from www.webapp.com. As explained before, it will also send along the cookie with the valid session id.
-* The web application at www.webapp.com verifies the user information in the corresponding session hash and destroys the project with the ID 1. It then returns a result page which is an unexpected result for the browser, so it will not display the image.
+* Bob browses a message board and views a post from a hacker where there is a crafted HTML image element. The element references a command in Bob's project management application, rather than an image file: `<img src="http://www.webapp.com/project/1/destroy">`
+* Bob's session at `www.webapp.com` is still alive, because he didn't log out a few minutes ago.
+* By viewing the post, the browser finds an image tag. It tries to load the suspected image from `www.webapp.com`. As explained before, it will also send along the cookie with the valid session ID.
+* The web application at `www.webapp.com` verifies the user information in the corresponding session hash and destroys the project with the ID 1. It then returns a result page which is an unexpected result for the browser, so it will not display the image.
* Bob doesn't notice the attack - but a few days later he finds out that project number one is gone.
-It is important to notice that the actual crafted image or link doesn't necessarily have to be situated in the web application's domain, it can be anywhere - in a forum, blog post or email.
+It is important to notice that the actual crafted image or link doesn't necessarily have to be situated in the web application's domain, it can be anywhere - in a forum, blog post, or email.
CSRF appears very rarely in CVE (Common Vulnerabilities and Exposures) - less than 0.1% in 2006 - but it really is a 'sleeping giant' [Grossman]. This is in stark contrast to the results in many security contract works - _CSRF is an important security issue_.
@@ -206,7 +290,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:**
@@ -218,9 +302,9 @@ 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_. Here is an example for a link which displays www.harmless.com as destination in the browser's status bar. In fact it dynamically creates a new form that sends a POST request.
+_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.
```html
<a href="http://www.harmless.com/" onclick="
@@ -239,9 +323,11 @@ Or the attacker places the code into the onmouseover event handler of an image:
<img src="http://www.harmless.com/img" width="400" height="400" onmouseover="..." />
```
-There are many other possibilities, like using a `<script>` tag to make a cross-site request to a URL with a JSONP or JavaScript response. The response is executable code that the attacker can find a way to run, possibly extracting sensitive data. To protect against this data leakage, we disallow cross-site `<script>` tags. Only Ajax requests may have JavaScript responses since `XMLHttpRequest` is subject to the browser Same-Origin policy - meaning only your site can initiate the request.
+There are many other possibilities, like using a `<script>` tag to make a cross-site request to a URL with a JSONP or JavaScript response. The response is executable code that the attacker can find a way to run, possibly extracting sensitive data. To protect against this data leakage, we must disallow cross-site `<script>` tags. Ajax requests, however, obey the browser's same-origin policy (only your own site is allowed to initiate `XmlHttpRequest`) so we can safely allow them to return JavaScript responses.
+
+NOTE: We can't distinguish a `<script>` tag's origin—whether it's a tag on your own site or on some other malicious site—so we must block all `<script>` across the board, even if it's actually a safe same-origin script served from your own site. In these cases, explicitly skip CSRF protection on actions that serve JavaScript meant for a `<script>` tag.
-To protect against all other forged requests, we introduce a _required security token_ that our site knows but other sites don't know. We include the security token in requests and verify it on the server. This is a one-liner in your application controller, and is the default for newly created rails applications:
+To protect against all other forged requests, we introduce a _required security token_ that our site knows but other sites don't know. We include the security token in requests and verify it on the server. This is a one-liner in your application controller, and is the default for newly created Rails applications:
```ruby
protect_from_forgery with: :exception
@@ -249,13 +335,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/blob/master/actionview/app/assets/javascripts),
+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:
@@ -279,7 +364,7 @@ Another class of security vulnerabilities surrounds the use of redirection and f
WARNING: _Redirection in a web application is an underestimated cracker tool: Not only can the attacker forward the user to a trap web site, they may also create a self-contained attack._
-Whenever the user is allowed to pass (parts of) the URL for redirection, it is possibly vulnerable. The most obvious attack would be to redirect users to a fake web application which looks and feels exactly as the original one. This so-called phishing attack works by sending an unsuspicious link in an email to the users, injecting the link by XSS in the web application or putting the link into an external site. It is unsuspicious, because the link starts with the URL to the web application and the URL to the malicious site is hidden in the redirection parameter: http://www.example.com/site/redirect?to= www.attacker.com. Here is an example of a legacy action:
+Whenever the user is allowed to pass (parts of) the URL for redirection, it is possibly vulnerable. The most obvious attack would be to redirect users to a fake web application which looks and feels exactly as the original one. This so-called phishing attack works by sending an unsuspicious link in an email to the users, injecting the link by XSS in the web application or putting the link into an external site. It is unsuspicious, because the link starts with the URL to the web application and the URL to the malicious site is hidden in the redirection parameter: http://www.example.com/site/redirect?to=www.attacker.com. Here is an example of a legacy action:
```ruby
def legacy
@@ -293,7 +378,7 @@ This will redirect the user to the main action if they tried to access a legacy
http://www.example.com/site/legacy?param1=xy&param2=23&host=www.attacker.com
```
-If it is at the end of the URL it will hardly be noticed and redirects the user to the attacker.com host. A simple countermeasure would be to _include only the expected parameters in a legacy action_ (again a whitelist approach, as opposed to removing unexpected parameters). _And if you redirect to an URL, check it with a whitelist or a regular expression_.
+If it is at the end of the URL it will hardly be noticed and redirects the user to the attacker.com host. A simple countermeasure would be to _include only the expected parameters in a legacy action_ (again a permitted list approach, as opposed to removing unexpected parameters). _And if you redirect to a URL, check it with a permitted list or a regular expression_.
#### Self-contained XSS
@@ -307,9 +392,9 @@ This example is a Base64 encoded JavaScript which displays a simple message box.
NOTE: _Make sure file uploads don't overwrite important files, and process media files asynchronously._
-Many web applications allow users to upload files. _File names, which the user may choose (partly), should always be filtered_ as an attacker could use a malicious file name to overwrite any file on the server. If you store file uploads at /var/www/uploads, and the user enters a file name like "../../../etc/passwd", it may overwrite an important file. Of course, the Ruby interpreter would need the appropriate permissions to do so - one more reason to run web servers, database servers and other programs as a less privileged Unix user.
+Many web applications allow users to upload files. _File names, which the user may choose (partly), should always be filtered_ as an attacker could use a malicious file name to overwrite any file on the server. If you store file uploads at /var/www/uploads, and the user enters a file name like "../../../etc/passwd", it may overwrite an important file. Of course, the Ruby interpreter would need the appropriate permissions to do so - one more reason to run web servers, database servers, and other programs as a less privileged Unix user.
-When filtering user input file names, _don't try to remove malicious parts_. Think of a situation where the web application removes all "../" in a file name and an attacker uses a string such as "....//" - the result will be "../". It is best to use a whitelist approach, which _checks for the validity of a file name with a set of accepted characters_. This is opposed to a blacklist approach which attempts to remove not allowed characters. In case it isn't a valid file name, reject it (or replace not accepted characters), but don't remove them. Here is the file name sanitizer from the [attachment_fu plugin](https://github.com/technoweenie/attachment_fu/tree/master):
+When filtering user input file names, _don't try to remove malicious parts_. Think of a situation where the web application removes all "../" in a file name and an attacker uses a string such as "....//" - the result will be "../". It is best to use a permitted list approach, which _checks for the validity of a file name with a set of accepted characters_. This is opposed to a restricted list approach which attempts to remove not allowed characters. In case it isn't a valid file name, reject it (or replace not accepted characters), but don't remove them. Here is the file name sanitizer from the [attachment_fu plugin](https://github.com/technoweenie/attachment_fu/tree/master):
```ruby
def sanitize_filename(filename)
@@ -334,7 +419,7 @@ WARNING: _Source code in uploaded files may be executed when placed in specific
The popular Apache web server has an option called DocumentRoot. This is the home directory of the web site, everything in this directory tree will be served by the web server. If there are files with a certain file name extension, the code in it will be executed when requested (might require some options to be set). Examples for this are PHP and CGI files. Now think of a situation where an attacker uploads a file "file.cgi" with code in it, which will be executed when someone downloads the file.
-_If your Apache DocumentRoot points to Rails' /public directory, do not put file uploads in it_, store files at least one level downwards.
+_If your Apache DocumentRoot points to Rails' /public directory, do not put file uploads in it_, store files at least one level upwards.
### File Downloads
@@ -349,7 +434,7 @@ send_file('/var/www/uploads/' + params[:filename])
Simply pass a file name like "../../../etc/passwd" to download the server's login information. A simple solution against this, is to _check that the requested file is in the expected directory_:
```ruby
-basename = File.expand_path(File.join(File.dirname(__FILE__), '../../files'))
+basename = File.expand_path('../../files', __dir__)
filename = File.expand_path(File.join(basename, @file.public_filename))
raise if basename !=
File.expand_path(File.join(File.dirname(filename), '../../../'))
@@ -369,15 +454,15 @@ 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.
-A real-world example is a [router reconfiguration by CSRF](http://www.h-online.com/security/news/item/Symantec-reports-first-active-attack-on-a-DSL-router-735883.html). The attackers sent a malicious e-mail, with CSRF in it, to Mexican users. The e-mail claimed there was an e-card waiting for them, but it also contained an image tag that resulted in a HTTP-GET request to reconfigure the user's router (which is a popular model in Mexico). The request changed the DNS-settings so that requests to a Mexico-based banking site would be mapped to the attacker's site. Everyone who accessed the banking site through that router saw the attacker's fake web site and had their credentials stolen.
+A real-world example is a [router reconfiguration by CSRF](http://www.h-online.com/security/news/item/Symantec-reports-first-active-attack-on-a-DSL-router-735883.html). The attackers sent a malicious e-mail, with CSRF in it, to Mexican users. The e-mail claimed there was an e-card waiting for the user, but it also contained an image tag that resulted in an HTTP-GET request to reconfigure the user's router (which is a popular model in Mexico). The request changed the DNS-settings so that requests to a Mexico-based banking site would be mapped to the attacker's site. Everyone who accessed the banking site through that router saw the attacker's fake web site and had their credentials stolen.
-Another example changed Google Adsense's e-mail address and password by. If the victim was logged into Google Adsense, the administration interface for Google advertisements campaigns, an attacker could change their credentials.

+Another example changed Google Adsense's e-mail address and password. If the victim was logged into Google Adsense, the administration interface for Google advertisement campaigns, an attacker could change the credentials of the victim.

-Another popular attack is to spam your web application, your blog or forum to propagate malicious XSS. Of course, the attacker has to know the URL structure, but most Rails URLs are quite straightforward or they will be easy to find out, if it is an open-source application's admin interface. The attacker may even do 1,000 lucky guesses by just including malicious IMG-tags which try every possible combination.
+Another popular attack is to spam your web application, your blog, or forum to propagate malicious XSS. Of course, the attacker has to know the URL structure, but most Rails URLs are quite straightforward or they will be easy to find out, if it is an open-source application's admin interface. The attacker may even do 1,000 lucky guesses by just including malicious IMG-tags which try every possible combination.
For _countermeasures against CSRF in administration interfaces and Intranet applications, refer to the countermeasures in the CSRF section_.
@@ -389,7 +474,7 @@ The common admin interface works like this: it's located at www.example.com/admi
* Does the admin really have to access the interface from everywhere in the world? Think about _limiting the login to a bunch of source IP addresses_. Examine request.remote_ip to find out about the user's IP address. This is not bullet-proof, but a great barrier. Remember that there might be a proxy in use, though.
-* _Put the admin interface to a special sub-domain_ such as admin.application.com and make it a separate application with its own user management. This makes stealing an admin cookie from the usual domain, www.application.com, impossible. This is because of the same origin policy in your browser: An injected (XSS) script on www.application.com may not read the cookie for admin.application.com and vice-versa.
+* _Put the admin interface to a special subdomain_ such as admin.application.com and make it a separate application with its own user management. This makes stealing an admin cookie from the usual domain, www.application.com, impossible. This is because of the same origin policy in your browser: An injected (XSS) script on www.application.com may not read the cookie for admin.application.com and vice-versa.
User Management
---------------
@@ -398,7 +483,7 @@ NOTE: _Almost every web application has to deal with authorization and authentic
There are a number of authentication plug-ins for Rails available. Good ones, such as the popular [devise](https://github.com/plataformatec/devise) and [authlogic](https://github.com/binarylogic/authlogic), store only encrypted passwords, not plain-text passwords. In Rails 3.1 you can use the built-in `has_secure_password` method which has similar features.
-Every new user gets an activation code to activate their account when they get an e-mail with a link in it. After activating the account, the activation_code columns will be set to NULL in the database. If someone requested an URL like these, they would be logged in as the first activated user found in the database (and chances are that this is the administrator):
+Every new user gets an activation code to activate their account when they get an e-mail with a link in it. After activating the account, the activation_code columns will be set to NULL in the database. If someone requested a URL like these, they would be logged in as the first activated user found in the database (and chances are that this is the administrator):
```
http://localhost:3006/user/activate
@@ -417,7 +502,7 @@ If the parameter was nil, the resulting SQL query will be
SELECT * FROM users WHERE (users.activation_code IS NULL) LIMIT 1
```
-And thus it found the first user in the database, returned it and logged them in. You can find out more about it in [this blog post](http://www.rorsecurity.info/2007/10/28/restful_authentication-login-security/). _It is advisable to update your plug-ins from time to time_. Moreover, you can review your application to find more flaws like this.
+And thus it found the first user in the database, returned it, and logged them in. You can find out more about it in [this blog post](http://www.rorsecurity.info/2007/10/28/restful_authentication-login-security/). _It is advisable to update your plug-ins from time to time_. Moreover, you can review your application to find more flaws like this.
### Brute-Forcing Accounts
@@ -445,20 +530,20 @@ However, the attacker may also take over the account by changing the e-mail addr
#### Other
-Depending on your web application, there may be more ways to hijack the user's account. In many cases CSRF and XSS will help to do so. For example, as in a CSRF vulnerability in [Google Mail](http://www.gnucitizen.org/blog/google-gmail-e-mail-hijack-technique/). In this proof-of-concept attack, the victim would have been lured to a web site controlled by the attacker. On that site is a crafted IMG-tag which results in a HTTP GET request that changes the filter settings of Google Mail. If the victim was logged in to Google Mail, the attacker would change the filters to forward all e-mails to their e-mail address. This is nearly as harmful as hijacking the entire account. As a countermeasure, _review your application logic and eliminate all XSS and CSRF vulnerabilities_.
+Depending on your web application, there may be more ways to hijack the user's account. In many cases CSRF and XSS will help to do so. For example, as in a CSRF vulnerability in [Google Mail](http://www.gnucitizen.org/blog/google-gmail-e-mail-hijack-technique/). In this proof-of-concept attack, the victim would have been lured to a web site controlled by the attacker. On that site is a crafted IMG-tag which results in an HTTP GET request that changes the filter settings of Google Mail. If the victim was logged in to Google Mail, the attacker would change the filters to forward all e-mails to their e-mail address. This is nearly as harmful as hijacking the entire account. As a countermeasure, _review your application logic and eliminate all XSS and CSRF vulnerabilities_.
### CAPTCHAs
INFO: _A CAPTCHA is a challenge-response test to determine that the response is not generated by a computer. It is often used to protect registration forms from attackers and comment forms from automatic spam bots by asking the user to type the letters of a distorted image. This is the positive CAPTCHA, but there is also the negative CAPTCHA. The idea of a negative CAPTCHA is not for a user to prove that they are human, but reveal that a robot is a robot._
-A popular positive CAPTCHA API is [reCAPTCHA](http://recaptcha.net/) which displays two distorted images of words from old books. It also adds an angled line, rather than a distorted background and high levels of warping on the text as earlier CAPTCHAs did, because the latter were broken. As a bonus, using reCAPTCHA helps to digitize old books. [ReCAPTCHA](https://github.com/ambethia/recaptcha/) is also a Rails plug-in with the same name as the API.
+A popular positive CAPTCHA API is [reCAPTCHA](https://developers.google.com/recaptcha/) which displays two distorted images of words from old books. It also adds an angled line, rather than a distorted background and high levels of warping on the text as earlier CAPTCHAs did, because the latter were broken. As a bonus, using reCAPTCHA helps to digitize old books. [ReCAPTCHA](https://github.com/ambethia/recaptcha/) is also a Rails plug-in with the same name as the API.
You will get two keys from the API, a public and a private key, which you have to put into your Rails environment. After that you can use the recaptcha_tags method in the view, and the verify_recaptcha method in the controller. Verify_recaptcha will return false if the validation fails.
The problem with CAPTCHAs is that they have a negative impact on the user experience. Additionally, some visually impaired users have found certain kinds of distorted CAPTCHAs difficult to read. Still, positive CAPTCHAs are one of the best methods to prevent all kinds of bots from submitting forms.
Most bots are really dumb. They crawl the web and put their spam into every form's field they can find. Negative CAPTCHAs take advantage of that and include a "honeypot" field in the form which will be hidden from the human user by CSS or JavaScript.
-Note that negative CAPTCHAs are only effective against dumb bots and won't suffice to protect critical applications from targeted bots. Still, the negative and positive CAPTCHAs can be combined to increase the performance, e.g., if the "honeypot" field is not empty (bot detected), you won't need to verify the positive CAPTCHA, which would require a HTTPS request to Google ReCaptcha before computing the response.
+Note that negative CAPTCHAs are only effective against dumb bots and won't suffice to protect critical applications from targeted bots. Still, the negative and positive CAPTCHAs can be combined to increase the performance, e.g., if the "honeypot" field is not empty (bot detected), you won't need to verify the positive CAPTCHA, which would require an HTTPS request to Google ReCaptcha before computing the response.
Here are some ideas how to hide honeypot fields by JavaScript and/or CSS:
@@ -466,7 +551,7 @@ Here are some ideas how to hide honeypot fields by JavaScript and/or CSS:
* make the elements very small or color them the same as the background of the page
* leave the fields displayed, but tell humans to leave them blank
-The most simple negative CAPTCHA is one hidden honeypot field. On the server side, you will check the value of the field: If it contains any text, it must be a bot. Then, you can either ignore the post or return a positive result, but not saving the post to the database. This way the bot will be satisfied and moves on. You can do this with annoying users, too.
+The most simple negative CAPTCHA is one hidden honeypot field. On the server side, you will check the value of the field: If it contains any text, it must be a bot. Then, you can either ignore the post or return a positive result, but not saving the post to the database. This way the bot will be satisfied and moves on.
You can find more sophisticated negative CAPTCHAs in Ned Batchelder's [blog post](http://nedbatchelder.com/text/stopbots.html):
@@ -486,17 +571,7 @@ By default, Rails logs all requests being made to the web application. But log f
config.filter_parameters << :password
```
-### Good Passwords
-
-INFO: _Do you find it hard to remember all your passwords? Don't write them down, but use the initial letters of each word in an easy to remember sentence._
-
-Bruce Schneier, a security technologist, [has analyzed](http://www.schneier.com/blog/archives/2006/12/realworld_passw.html) 34,000 real-world user names and passwords from the MySpace phishing attack mentioned [below](#examples-from-the-underground). It turns out that most of the passwords are quite easy to crack. The 20 most common passwords are:
-
-password1, abc123, myspace1, password, blink182, qwerty1, ****you, 123abc, baseball1, football1, 123456, soccer, monkey1, liverpool1, princess1, jordan23, slipknot1, superman1, iloveyou1, and monkey.
-
-It is interesting that only 4% of these passwords were dictionary words and the great majority is actually alphanumeric. However, password cracker dictionaries contain a large number of today's passwords, and they try out all kinds of (alphanumerical) combinations. If an attacker knows your user name and you use a weak password, your account will be easily cracked.
-
-A good password is a long alphanumeric combination of mixed cases. As this is quite hard to remember, it is advisable to enter only the _first letters of a sentence that you can easily remember_. For example "The quick brown fox jumps over the lazy dog" will be "Tqbfjotld". Note that this is just an example, you should not use well known phrases like these, as they might appear in cracker dictionaries, too.
+NOTE: Provided parameters will be filtered out by partial matching regular expression. Rails adds default `:password` in the appropriate initializer (`initializers/filter_parameter_logging.rb`) and cares about typical application parameters `password` and `password_confirmation`.
### Regular Expressions
@@ -557,28 +632,28 @@ This is alright for some web applications, but certainly not if the user is not
Depending on your web application, there will be many more parameters the user can tamper with. As a rule of thumb, _no user input data is secure, until proven otherwise, and every parameter from the user is potentially manipulated_.
-Don't be fooled by security by obfuscation and JavaScript security. The Web Developer Toolbar for Mozilla Firefox lets you review and change every form's hidden fields. _JavaScript can be used to validate user input data, but certainly not to prevent attackers from sending malicious requests with unexpected values_. The Live Http Headers plugin for Mozilla Firefox logs every request and may repeat and change them. That is an easy way to bypass any JavaScript validations. And there are even client-side proxies that allow you to intercept any request and response from and to the Internet.
+Don't be fooled by security by obfuscation and JavaScript security. Developer tools let you review and change every form's hidden fields. _JavaScript can be used to validate user input data, but certainly not to prevent attackers from sending malicious requests with unexpected values_. The Firebug addon for Mozilla Firefox logs every request and may repeat and change them. That is an easy way to bypass any JavaScript validations. And there are even client-side proxies that allow you to intercept any request and response from and to the Internet.
Injection
---------
INFO: _Injection is a class of attacks that introduce malicious code or parameters into a web application in order to run it within its security context. Prominent examples of injection are cross-site scripting (XSS) and SQL injection._
-Injection is very tricky, because the same code or parameter can be malicious in one context, but totally harmless in another. A context can be a scripting, query or programming language, the shell or a Ruby/Rails method. The following sections will cover all important contexts where injection attacks may happen. The first section, however, covers an architectural decision in connection with Injection.
+Injection is very tricky, because the same code or parameter can be malicious in one context, but totally harmless in another. A context can be a scripting, query, or programming language, the shell, or a Ruby/Rails method. The following sections will cover all important contexts where injection attacks may happen. The first section, however, covers an architectural decision in connection with Injection.
-### Whitelists versus Blacklists
+### Permitted lists versus Restricted lists
-NOTE: _When sanitizing, protecting or verifying something, prefer whitelists over blacklists._
+NOTE: _When sanitizing, protecting, or verifying something, prefer permitted lists over restricted lists._
-A blacklist can be a list of bad e-mail addresses, non-public actions or bad HTML tags. This is opposed to a whitelist which lists the good e-mail addresses, public actions, good HTML tags and so on. Although sometimes it is not possible to create a whitelist (in a SPAM filter, for example), _prefer to use whitelist approaches_:
+A restricted list can be a list of bad e-mail addresses, non-public actions or bad HTML tags. This is opposed to a permitted list which lists the good e-mail addresses, public actions, good HTML tags, and so on. Although sometimes it is not possible to create a permitted list (in a SPAM filter, for example), _prefer to use permitted list approaches_:
* Use before_action except: [...] instead of only: [...] for security-related actions. This way you don't forget to enable security checks for newly added actions.
* Allow &lt;strong&gt; instead of removing &lt;script&gt; against Cross-Site Scripting (XSS). See below for details.
-* Don't try to correct user input by blacklists:
+* Don't try to correct user input using restricted lists:
* This will make the attack work: "&lt;sc&lt;script&gt;ript&gt;".gsub("&lt;script&gt;", "")
* But reject malformed input
-Whitelists are also a good approach against the human factor of forgetting something in the blacklist.
+Permitted lists are also a good approach against the human factor of forgetting something in the restricted list.
### SQL Injection
@@ -605,7 +680,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:
@@ -643,7 +718,7 @@ Also, the second query renames some columns with the AS statement so that the we
#### Countermeasures
-Ruby on Rails has a built-in filter for special SQL characters, which will escape ' , " , NULL character and line breaks. *Using `Model.find(id)` or `Model.find_by_some thing(something)` automatically applies this countermeasure*. But in SQL fragments, especially *in conditions fragments (`where("...")`), the `connection.execute()` or `Model.find_by_sql()` methods, it has to be applied manually*.
+Ruby on Rails has a built-in filter for special SQL characters, which will escape ' , " , NULL character, and line breaks. *Using `Model.find(id)` or `Model.find_by_some thing(something)` automatically applies this countermeasure*. But in SQL fragments, especially *in conditions fragments (`where("...")`), the `connection.execute()` or `Model.find_by_sql()` methods, it has to be applied manually*.
Instead of passing a string to the conditions option, you can pass an array to sanitize tainted strings like this:
@@ -667,13 +742,11 @@ INFO: _The most widespread, and one of the most devastating security vulnerabili
An entry point is a vulnerable URL and its parameters where an attacker can start an attack.
-The most common entry points are message posts, user comments, and guest books, but project titles, document names and search result pages have also been vulnerable - just about everywhere where the user can input data. But the input does not necessarily have to come from input boxes on web sites, it can be in any URL parameter - obvious, hidden or internal. Remember that the user may intercept any traffic. Applications, such as the [Live HTTP Headers Firefox plugin](http://livehttpheaders.mozdev.org/), or client-site proxies make it easy to change requests.
+The most common entry points are message posts, user comments, and guest books, but project titles, document names, and search result pages have also been vulnerable - just about everywhere where the user can input data. But the input does not necessarily have to come from input boxes on web sites, it can be in any URL parameter - obvious, hidden or internal. Remember that the user may intercept any traffic. Applications or client-site proxies make it easy to change requests. There are also other attack vectors like banner advertisements.
XSS attacks work like this: An attacker injects some code, the web application saves it and displays it on a page, later presented to a victim. Most XSS examples simply display an alert box, but it is more powerful than that. XSS can steal the cookie, hijack the session, redirect the victim to a fake website, display advertisements for the benefit of the attacker, change elements on the web site to get confidential information or install malicious software through security holes in the web browser.
-During the second half of 2007, there were 88 vulnerabilities reported in Mozilla browsers, 22 in Safari, 18 in IE, and 12 in Opera. The [Symantec Global Internet Security threat report](http://eval.symantec.com/mktginfo/enterprise/white_papers/b-whitepaper_internet_security_threat_report_xiii_04-2008.en-us.pdf) also documented 239 browser plug-in vulnerabilities in the last six months of 2007. [Mpack](http://pandalabs.pandasecurity.com/mpack-uncovered/) is a very active and up-to-date attack framework which exploits these vulnerabilities. For criminal hackers, it is very attractive to exploit an SQL-Injection vulnerability in a web application framework and insert malicious code in every textual table column. In April 2008 more than 510,000 sites were hacked like this, among them the British government, United Nations, and many more high targets.
-
-A relatively new, and unusual, form of entry points are banner advertisements. In earlier 2008, malicious code appeared in banner ads on popular sites, such as MySpace and Excite, according to [Trend Micro](http://blog.trendmicro.com/myspace-excite-and-blick-serve-up-malicious-banner-ads/).
+During the second half of 2007, there were 88 vulnerabilities reported in Mozilla browsers, 22 in Safari, 18 in IE, and 12 in Opera. The [Symantec Global Internet Security threat report](http://eval.symantec.com/mktginfo/enterprise/white_papers/b-whitepaper_internet_security_threat_report_xiii_04-2008.en-us.pdf) also documented 239 browser plug-in vulnerabilities in the last six months of 2007. [Mpack](http://pandalabs.pandasecurity.com/mpack-uncovered/) is a very active and up-to-date attack framework which exploits these vulnerabilities. For criminal hackers, it is very attractive to exploit an SQL-Injection vulnerability in a web application framework and insert malicious code in every textual table column. In April 2008 more than 510,000 sites were hacked like this, among them the British government, United Nations, and many more high profile targets.
#### HTML/JavaScript Injection
@@ -712,11 +785,11 @@ The log files on www.attacker.com will read like this:
GET http://www.attacker.com/_app_session=836c1c25278e5b321d6bea4f19cb57e2
```
-You can mitigate these attacks (in the obvious way) by adding the **httpOnly** flag to cookies, so that document.cookie may not be read by JavaScript. Http only cookies can be used from IE v6.SP1, Firefox v2.0.0.5 and Opera 9.5. Safari is still considering, it ignores the option. But other, older browsers (such as WebTV and IE 5.5 on Mac) can actually cause the page to fail to load. Be warned that cookies [will still be visible using Ajax](https://www.owasp.org/index.php/HTTPOnly#Browsers_Supporting_HttpOnly), though.
+You can mitigate these attacks (in the obvious way) by adding the **httpOnly** flag to cookies, so that document.cookie may not be read by JavaScript. HTTP only cookies can be used from IE v6.SP1, Firefox v2.0.0.5, Opera 9.5, Safari 4, and Chrome 1.0.154 onwards. But other, older browsers (such as WebTV and IE 5.5 on Mac) can actually cause the page to fail to load. Be warned that cookies [will still be visible using Ajax](https://www.owasp.org/index.php/HTTPOnly#Browsers_Supporting_HttpOnly), though.
##### Defacement
-With web page defacement an attacker can do a lot of things, for example, present false information or lure the victim on the attackers web site to steal the cookie, login credentials or other sensitive data. The most popular way is to include code from external sources by iframes:
+With web page defacement an attacker can do a lot of things, for example, present false information or lure the victim on the attackers web site to steal the cookie, login credentials, or other sensitive data. The most popular way is to include code from external sources by iframes:
```html
<iframe name="StatPage" src="http://58.xx.xxx.xxx" width=5 height=5 style="display:none"></iframe>
@@ -737,15 +810,15 @@ http://www.cbsnews.com/stories/2002/02/15/weather_local/main501644.shtml?zipcode
_It is very important to filter malicious input, but it is also important to escape the output of the web application_.
-Especially for XSS, it is important to do _whitelist input filtering instead of blacklist_. Whitelist filtering states the values allowed as opposed to the values not allowed. Blacklists are never complete.
+Especially for XSS, it is important to do _permitted input filtering instead of restricted_. Permitted list filtering states the values allowed as opposed to the values not allowed. Restricted lists are never complete.
-Imagine a blacklist deletes "script" from the user input. Now the attacker injects "&lt;scrscriptipt&gt;", and after the filter, "&lt;script&gt;" remains. Earlier versions of Rails used a blacklist approach for the strip_tags(), strip_links() and sanitize() method. So this kind of injection was possible:
+Imagine a restricted list deletes "script" from the user input. Now the attacker injects "&lt;scrscriptipt&gt;", and after the filter, "&lt;script&gt;" remains. Earlier versions of Rails used a restricted list approach for the strip_tags(), strip_links() and sanitize() method. So this kind of injection was possible:
```ruby
strip_tags("some<<b>script>alert('hello')<</b>/script>")
```
-This returned "some&lt;script&gt;alert('hello')&lt;/script&gt;", which makes an attack work. That's why a whitelist approach is better, using the updated Rails 2 method sanitize():
+This returned "some&lt;script&gt;alert('hello')&lt;/script&gt;", which makes an attack work. That's why a permitted list approach is better, using the updated Rails 2 method sanitize():
```ruby
tags = %w(a acronym b strong i em li ul ol h1 h2 h3 h4 h5 h6 blockquote br cite sub sup ins p)
@@ -754,7 +827,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;, &gt; by their uninterpreted representations in HTML (`&amp;`, `&quot;`, `&lt`;, and `&gt;`). However, it can easily happen that the programmer forgets to use it, so _it is recommended to use the SafeErb gem. SafeErb reminds you to escape strings from external sources.
+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
@@ -779,21 +852,19 @@ The following is an excerpt from the [Js.Yamanner@m](http://www.symantec.com/sec
var IDList = ''; var CRumb = ''; function makeRequest(url, Func, Method,Param) { ...
```
-The worms exploits a hole in Yahoo's HTML/JavaScript filter, which usually filters all target and onload attributes from tags (because there can be JavaScript). The filter is applied only once, however, so the onload attribute with the worm code stays in place. This is a good example why blacklist filters are never complete and why it is hard to allow HTML/JavaScript in a web application.
+The worms exploit a hole in Yahoo's HTML/JavaScript filter, which usually filters all targets and onload attributes from tags (because there can be JavaScript). The filter is applied only once, however, so the onload attribute with the worm code stays in place. This is a good example why restricted list filters are never complete and why it is hard to allow HTML/JavaScript in a web application.
Another proof-of-concept webmail worm is Nduja, a cross-domain worm for four Italian webmail services. Find more details on [Rosario Valotta's paper](http://www.xssed.com/news/37/Nduja_Connection_A_cross_webmail_worm_XWW/). Both webmail worms have the goal to harvest email addresses, something a criminal hacker could make money with.
In December 2006, 34,000 actual user names and passwords were stolen in a [MySpace phishing attack](http://news.netcraft.com/archives/2006/10/27/myspace_accounts_compromised_by_phishers.html). The idea of the attack was to create a profile page named "login_home_index_html", so the URL looked very convincing. Specially-crafted HTML and CSS was used to hide the genuine MySpace content from the page and instead display its own login form.
-The MySpace Samy worm will be discussed in the CSS Injection section.
-
### CSS Injection
-INFO: _CSS Injection is actually JavaScript injection, because some browsers (IE, some versions of Safari and others) allow JavaScript in CSS. Think twice about allowing custom CSS in your web application._
+INFO: _CSS Injection is actually JavaScript injection, because some browsers (IE, some versions of Safari, and others) allow JavaScript in CSS. Think twice about allowing custom CSS in your web application._
-CSS Injection is explained best by a well-known worm, the [MySpace Samy worm](http://namb.la/popular/tech.html). This worm automatically sent a friend request to Samy (the attacker) simply by visiting his profile. Within several hours he had over 1 million friend requests, but it creates too much traffic on MySpace, so that the site goes offline. The following is a technical explanation of the worm.
+CSS Injection is explained best by the well-known [MySpace Samy worm](https://samy.pl/myspace/tech.html). This worm automatically sent a friend request to Samy (the attacker) simply by visiting his profile. Within several hours he had over 1 million friend requests, which created so much traffic that MySpace went offline. The following is a technical explanation of that worm.
-MySpace blocks many tags, however it allows CSS. So the worm's author put JavaScript into CSS like this:
+MySpace blocked many tags, but allowed CSS. So the worm's author put JavaScript into CSS like this:
```html
<div style="background:url('javascript:alert(1)')">
@@ -805,7 +876,7 @@ So the payload is in the style attribute. But there are no quotes allowed in the
<div id="mycode" expr="alert('hah!')" style="background:url('javascript:eval(document.all.mycode.expr)')">
```
-The eval() function is a nightmare for blacklist input filters, as it allows the style attribute to hide the word "innerHTML":
+The eval() function is a nightmare for restricted list input filters, as it allows the style attribute to hide the word "innerHTML":
```
alert(eval('document.body.inne' + 'rHTML'));
@@ -817,7 +888,7 @@ The next problem was MySpace filtering the word "javascript", so the author used
<div id="mycode" expr="alert('hah!')" style="background:url('java↵
script:eval(document.all.mycode.expr)')">
```
-Another problem for the worm's author were CSRF security tokens. Without them he couldn't send a friend request over POST. He got around it by sending a GET to the page right before adding a user and parsing the result for the CSRF token.
+Another problem for the worm's author was the [CSRF security tokens](#cross-site-request-forgery-csrf). Without them he couldn't send a friend request over POST. He got around it by sending a GET to the page right before adding a user and parsing the result for the CSRF token.
In the end, he got a 4 KB worm, which he injected into his profile page.
@@ -825,7 +896,7 @@ The [moz-binding](http://www.securiteam.com/securitynews/5LP051FHPE.html) CSS pr
#### Countermeasures
-This example, again, showed that a blacklist filter is never complete. However, as custom CSS in web applications is a quite rare feature, it may be hard to find a good whitelist CSS filter. _If you want to allow custom colors or images, you can allow the user to choose them and build the CSS in the web application_. Use Rails' `sanitize()` method as a model for a whitelist CSS filter, if you really need one.
+This example, again, showed that a restricted list filter is never complete. However, as custom CSS in web applications is a quite rare feature, it may be hard to find a good permitted CSS filter. _If you want to allow custom colors or images, you can allow the user to choose them and build the CSS in the web application_. Use Rails' `sanitize()` method as a model for a permitted CSS filter, if you really need one.
### Textile Injection
@@ -854,7 +925,7 @@ RedCloth.new("<a href='javascript:alert(1)'>hello</a>", [:filter_html]).to_html
#### Countermeasures
-It is recommended to _use RedCloth in combination with a whitelist input filter_, as described in the countermeasures against XSS section.
+It is recommended to _use RedCloth in combination with a permitted input filter_, as described in the countermeasures against XSS section.
### Ajax Injection
@@ -878,9 +949,9 @@ system("/bin/echo","hello; rm *")
### Header Injection
-WARNING: _HTTP headers are dynamically generated and under certain circumstances user input may be injected. This can lead to false redirection, XSS or HTTP response splitting._
+WARNING: _HTTP headers are dynamically generated and under certain circumstances user input may be injected. This can lead to false redirection, XSS, or HTTP response splitting._
-HTTP request headers have a Referer, User-Agent (client software), and Cookie field, among others. Response headers for example have a status code, Cookie and Location (redirection target URL) field. All of them are user-supplied and may be manipulated with more or less effort. _Remember to escape these header fields, too._ For example when you display the user agent in an administration area.
+HTTP request headers have a Referer, User-Agent (client software), and Cookie field, among others. Response headers for example have a status code, Cookie, and Location (redirection target URL) field. All of them are user-supplied and may be manipulated with more or less effort. _Remember to escape these header fields, too._ For example when you display the user agent in an administration area.
Besides that, it is _important to know what you are doing when building response headers partly based on user input._ For example you want to redirect the user back to a specific page. To do that you introduced a "referer" field in a form to redirect to the given address:
@@ -959,7 +1030,7 @@ When `params[:token]` is one of: `[nil]`, `[nil, nil, ...]` or
`['foo', nil]` it will bypass the test for `nil`, but `IS NULL` or
`IN ('foo', NULL)` where clauses still will be added to the SQL query.
-To keep rails secure by default, `deep_munge` replaces some of the values with
+To keep Rails secure by default, `deep_munge` replaces some of the values with
`nil`. Below table shows what the parameters look like based on `JSON` sent in
request:
@@ -987,7 +1058,10 @@ Every HTTP response from your Rails application receives the following default s
config.action_dispatch.default_headers = {
'X-Frame-Options' => 'SAMEORIGIN',
'X-XSS-Protection' => '1; mode=block',
- 'X-Content-Type-Options' => 'nosniff'
+ 'X-Content-Type-Options' => 'nosniff',
+ 'X-Download-Options' => 'noopen',
+ 'X-Permitted-Cross-Domain-Policies' => 'none',
+ 'Referrer-Policy' => 'strict-origin-when-cross-origin'
}
```
@@ -1008,53 +1082,170 @@ config.action_dispatch.default_headers.clear
Here is a list of common headers:
-* X-Frame-Options
-_'SAMEORIGIN' in Rails by default_ - allow framing on same domain. Set it to 'DENY' to deny framing at all or 'ALLOWALL' if you want to allow framing for all website.
-* X-XSS-Protection
-_'1; mode=block' in Rails by default_ - use XSS Auditor and block page if XSS attack is detected. Set it to '0;' if you want to switch XSS Auditor off(useful if response contents scripts from request parameters)
-* X-Content-Type-Options
-_'nosniff' in Rails by default_ - stops the browser from guessing the MIME type of a file.
-* X-Content-Security-Policy
-[A powerful mechanism for controlling which sites certain content types can be loaded from](http://w3c.github.io/webappsec/specs/content-security-policy/csp-specification.dev.html)
-* Access-Control-Allow-Origin
-Used to control which sites are allowed to bypass same origin policies and send cross-origin requests.
-* Strict-Transport-Security
-[Used to control if the browser is allowed to only access a site over a secure connection](http://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security)
+* **X-Frame-Options:** _'SAMEORIGIN' in Rails by default_ - allow framing on same domain. Set it to 'DENY' to deny framing at all or 'ALLOWALL' if you want to allow framing for all website.
+* **X-XSS-Protection:** _'1; mode=block' in Rails by default_ - use XSS Auditor and block page if XSS attack is detected. Set it to '0;' if you want to switch XSS Auditor off(useful if response contents scripts from request parameters)
+* **X-Content-Type-Options:** _'nosniff' in Rails by default_ - stops the browser from guessing the MIME type of a file.
+* **X-Content-Security-Policy:** [A powerful mechanism for controlling which sites certain content types can be loaded from](http://w3c.github.io/webappsec/specs/content-security-policy/csp-specification.dev.html)
+* **Access-Control-Allow-Origin:** Used to control which sites are allowed to bypass same origin policies and send cross-origin requests.
+* **Strict-Transport-Security:** [Used to control if the browser is allowed to only access a site over a secure connection](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security)
+
+### Content Security Policy
+
+Rails provides a DSL that allows you to configure a
+[Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy)
+for your application. You can configure a global default policy and then
+override it on a per-resource basis and even use lambdas to inject per-request
+values into the header such as account subdomains in a multi-tenant application.
+
+Example global policy:
+
+```ruby
+# config/initializers/content_security_policy.rb
+Rails.application.config.content_security_policy do |policy|
+ policy.default_src :self, :https
+ policy.font_src :self, :https, :data
+ policy.img_src :self, :https, :data
+ policy.object_src :none
+ policy.script_src :self, :https
+ policy.style_src :self, :https
+
+ # Specify URI for violation reports
+ policy.report_uri "/csp-violation-report-endpoint"
+end
+```
+
+Example controller overrides:
+
+```ruby
+# Override policy inline
+class PostsController < ApplicationController
+ content_security_policy do |p|
+ p.upgrade_insecure_requests true
+ end
+end
+
+# Using literal values
+class PostsController < ApplicationController
+ content_security_policy do |p|
+ p.base_uri "https://www.example.com"
+ end
+end
+
+# Using mixed static and dynamic values
+class PostsController < ApplicationController
+ content_security_policy do |p|
+ p.base_uri :self, -> { "https://#{current_user.domain}.example.com" }
+ end
+end
+
+# Disabling the global CSP
+class LegacyPagesController < ApplicationController
+ content_security_policy false, only: :index
+end
+```
+
+Use the `content_security_policy_report_only`
+configuration attribute to set
+[Content-Security-Policy-Report-Only](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only)
+in order to report only content violations for migrating
+legacy content
+
+```ruby
+# config/initializers/content_security_policy.rb
+Rails.application.config.content_security_policy_report_only = true
+```
+
+```ruby
+# Controller override
+class PostsController < ApplicationController
+ content_security_policy_report_only only: :index
+end
+```
+
+You can enable automatic nonce generation:
+
+```ruby
+# config/initializers/content_security_policy.rb
+Rails.application.config.content_security_policy do |policy|
+ policy.script_src :self, :https
+end
+
+Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
+```
+
+Then you can add an automatic nonce value by passing `nonce: true`
+as part of `html_options`. Example:
+
+```html+erb
+<%= javascript_tag nonce: true do -%>
+ alert('Hello, World!');
+<% end -%>
+```
+
+The same works with `javascript_include_tag`:
+
+```html+erb
+<%= javascript_include_tag "script", nonce: true %>
+```
+
+Use [`csp_meta_tag`](http://api.rubyonrails.org/classes/ActionView/Helpers/CspHelper.html#method-i-csp_meta_tag)
+helper to create a meta tag "csp-nonce" with the per-session nonce value
+for allowing inline `<script>` tags.
+
+```html+erb
+<head>
+ <%= csp_meta_tag %>
+</head>
+```
+
+This is used by the Rails UJS helper to create dynamically
+loaded inline `<script>` elements.
Environmental Security
----------------------
It is beyond the scope of this guide to inform you on how to secure your application code and environments. However, please secure your database configuration, e.g. `config/database.yml`, and your server-side secret, e.g. stored in `config/secrets.yml`. You may want to further restrict access, using environment-specific versions of these files and any others that may contain sensitive information.
-### Custom secrets
+### Custom credentials
+
+Rails generates a `config/credentials.yml.enc` to store third-party credentials
+within the repo. This is only viable because Rails encrypts the file with a master
+key that's generated into a version control ignored `config/master.key` — Rails
+will also look for that key in `ENV["RAILS_MASTER_KEY"]`. Rails also requires the
+key to boot in production, so the credentials can be read.
-Rails generates a `config/secrets.yml`. By default, this file contains the
-application's `secret_key_base`, but it could also be used to store other
-secrets such as access keys for external APIs.
+To edit stored credentials use `rails credentials:edit`.
-The secrets added to this file are accessible via `Rails.application.secrets`.
-For example, with the following `config/secrets.yml`:
+By default, this file contains the application's
+`secret_key_base`, but it could also be used to store other credentials such as
+access keys for external APIs.
- development:
- secret_key_base: 3b7cd727ee24e8444053437c36cc66c3
- some_api_key: SOMEKEY
+The credentials added to this file are accessible via `Rails.application.credentials`.
+For example, with the following decrypted `config/credentials.yml.enc`:
-`Rails.application.secrets.some_api_key` returns `SOMEKEY` in the development
-environment.
+ secret_key_base: 3b7cd727ee24e8444053437c36cc66c3
+ some_api_key: SOMEKEY
+
+`Rails.application.credentials.some_api_key` returns `SOMEKEY` in any environment.
If you want an exception to be raised when some key is blank, use the bang
version:
```ruby
-Rails.application.secrets.some_api_key! # => raises KeyError
+Rails.application.credentials.some_api_key! # => raises KeyError: :some_api_key is blank
```
+Dependency Management and CVEs
+------------------------------
+
+We don’t bump dependencies just to encourage use of new versions, including for security issues. This is because application owners need to manually update their gems regardless of our efforts. Use `bundle update --conservative gem_name` to safely update vulnerable dependencies.
+
Additional Resources
--------------------
The security landscape shifts and it is important to keep up to date, because missing a new vulnerability can be catastrophic. You can find additional resources about (Rails) security here:
-* Subscribe to the Rails security [mailing list](http://groups.google.com/group/rubyonrails-security)
-* [Keep up to date on the other application layers](http://secunia.com/) (they have a weekly newsletter, too)
-* A [good security blog](https://www.owasp.org) including the [Cross-Site scripting Cheat Sheet](https://www.owasp.org/index.php/DOM_based_XSS_Prevention_Cheat_Sheet)
-
+* Subscribe to the Rails security [mailing list](https://groups.google.com/forum/#!forum/rubyonrails-security).
+* [Brakeman - Rails Security Scanner](https://brakemanscanner.org/) - To perform static security analysis for Rails applications.
+* [Keep up to date on the other application layers](http://secunia.com/) (they have a weekly newsletter, too).
+* A [good security blog](https://www.owasp.org) including the [Cross-Site scripting Cheat Sheet](https://www.owasp.org/index.php/DOM_based_XSS_Prevention_Cheat_Sheet).
diff --git a/guides/source/testing.md b/guides/source/testing.md
index 2fd54a48fc..8c21ccfba6 100644
--- a/guides/source/testing.md
+++ b/guides/source/testing.md
@@ -1,14 +1,14 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
-A Guide to Testing Rails Applications
-=====================================
+Testing Rails Applications
+==========================
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.
--------------------------------------------------------------------------------
@@ -18,22 +18,14 @@ Why Write Tests for your Rails Applications?
Rails makes it super easy to write your tests. It starts by producing skeleton test code while you are creating your models and controllers.
-By simply running your Rails tests you can ensure your code adheres to the desired functionality even after some major code refactoring.
+By running your Rails tests you can ensure your code adheres to the desired functionality even after some major code refactoring.
Rails tests can also simulate browser requests and thus you can test your application's response without having to test it through your browser.
Introduction to Testing
-----------------------
-Testing support was woven into the Rails fabric from the beginning. It wasn't an "oh! let's bolt on support for running tests because they're new and cool" epiphany. Just about every Rails application interacts heavily with a database and, as a result, your tests will need a database to interact with as well. To write efficient tests, you'll need to understand how to set up this database and populate it with sample data.
-
-### The Test Environment
-
-By default, every Rails application has three environments: development, test, and production. The database for each one of them is configured in `config/database.yml`.
-
-A dedicated test database allows you to set up and interact with test data in isolation. This way your tests can mangle test data with confidence, without worrying about the data in the development or production databases.
-
-Also, each environment's configuration can be modified similarly. In this case, we can modify our test environment by changing the options found in `config/environments/test.rb`.
+Testing support was woven into the Rails fabric from the beginning. It wasn't an "oh! let's bolt on support for running tests because they're new and cool" epiphany.
### Rails Sets up for Testing from the Word Go
@@ -41,136 +33,44 @@ 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/
+application_system_test_case.rb fixtures/ integration/ models/ test_helper.rb
+controllers/ helpers/ mailers/ system/
```
-The `models` directory is meant to hold tests for your models, the `controllers` directory is meant to hold tests for your controllers and the `integration` directory is meant to hold tests that involve any number of controllers interacting. There is also a directory for testing your mailers and one for testing view helpers.
-
-Fixtures are a way of organizing test data; they reside in the `fixtures` directory.
-
-The `test_helper.rb` file holds the default configuration for your tests.
+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 Low-Down on Fixtures
+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.
-For good tests, you'll need to give some thought to setting up test data.
-In Rails, you can handle this by defining and customizing fixtures.
-You can find comprehensive documentation in the [Fixtures API documentation](http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html).
-
-#### What Are Fixtures?
-
-_Fixtures_ is a fancy word for sample data. Fixtures allow you to populate your testing database with predefined data before your tests run. Fixtures are database independent and written in YAML. There is one file per model.
-
-You'll find fixtures under your `test/fixtures` directory. When you run `rails generate model` to create a new model, Rails automatically creates fixture stubs in this directory.
-
-#### YAML
-
-YAML-formatted fixtures are a human-friendly way to describe your sample data. These types of fixtures have the **.yml** file extension (as in `users.yml`).
-
-Here's a sample YAML fixture file:
-
-```yaml
-# lo & behold! I am a YAML comment!
-david:
- name: David Heinemeier Hansson
- birthday: 1979-10-15
- profession: Systems development
-
-steve:
- name: Steve Ross Kellock
- birthday: 1974-09-27
- profession: guy with keyboard
-```
-
-Each fixture is given a name followed by an indented list of colon-separated key/value pairs. Records are typically separated by a blank line. You can place comments in a fixture file by using the # character in the first column.
-
-If you are working with [associations](/association_basics.html), you can simply
-define a reference node between two different fixtures. Here's an example with
-a `belongs_to`/`has_many` association:
-
-```yaml
-# In fixtures/categories.yml
-about:
- name: About
-
-# In fixtures/articles.yml
-one:
- title: Welcome to Rails!
- body: Hello world!
- category: about
-```
-
-Notice the `category` key of the `one` article found in `fixtures/articles.yml` has a value of `about`. This tells Rails to load the category `about` found in `fixtures/categories.yml`.
-
-NOTE: For associations to reference one another by name, you cannot specify the `id:` attribute on the associated fixtures. Rails will auto assign a primary key to be consistent between runs. For more information on this association behavior please read the [Fixtures API documentation](http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html).
-
-#### ERB'in It Up
-
-ERB allows you to embed Ruby code within templates. The YAML fixture format is pre-processed with ERB when Rails loads fixtures. This allows you to use Ruby to help you generate some sample data. For example, the following code generates a thousand users:
-
-```erb
-<% 1000.times do |n| %>
-user_<%= n %>:
- username: <%= "user#{n}" %>
- email: <%= "user#{n}@example.com" %>
-<% end %>
-```
-
-#### Fixtures in Action
-
-Rails by default automatically loads all fixtures from the `test/fixtures` directory for your models and controllers test. Loading involves three steps:
-
-1. Remove any existing data from the table corresponding to the fixture
-2. Load the fixture data into the table
-3. Dump the fixture data into a method in case you want to access it directly
-
-TIP: In order to remove existing data from the database, Rails tries to disable referential integrity triggers (like foreign keys and check constraints). If you are getting annoying permission errors on running tests, make sure the database user has privilege to disable these triggers in testing environment. (In PostgreSQL, only superusers can disable all triggers. Read more about PostgreSQL permissions [here](http://blog.endpoint.com/2012/10/postgres-system-triggers-error.html))
-
-#### Fixtures are Active Record objects
-
-Fixtures are instances of Active Record. As mentioned in point #3 above, you can access the object directly because it is automatically available as a method whose scope is local of the test case. For example:
-
-```ruby
-# this will return the User object for the fixture named david
-users(:david)
-
-# this will return the property for david called id
-users(:david).id
+Fixtures are a way of organizing test data; they reside in the `fixtures` directory.
-# one can also access methods available on the User class
-email(david.partner.email, david.location_tonight)
-```
+A `jobs` directory will also be created when an associated test is first generated.
-To get multiple fixtures at once, you can pass in a list of fixture names. For example:
+The `test_helper.rb` file holds the default configuration for your tests.
-```ruby
-# this will return an array containing the fixtures david and steve
-users(:david, :steve)
-```
+The `application_system_test_case.rb` holds the default configuration for your system
+tests.
-### Console Tasks for Running your Tests
-Rails comes with a CLI command to run tests.
-Here are some examples of how to use it:
+### The Test Environment
-```bash
-$ bin/rails test # run all tests in the `test` directory
-$ bin/rails test test/controllers # run all tests from specific directory
-$ bin/rails test test/models/post_test.rb # run specific test
-$ bin/rails test test/models/post_test.rb:44 # run specific test and line
-```
+By default, every Rails application has three environments: development, test, and production.
-We will cover each of types Rails tests listed above in this guide.
+Each environment's configuration can be modified similarly. In this case, we can modify our test environment by changing the options found in `config/environments/test.rb`.
-Model Testing
-------------------------
+NOTE: Your tests are run under `RAILS_ENV=test`.
-For this guide we will be using the application we built in the [Getting Started with Rails](getting_started.html) guide.
+### Rails meets Minitest
-If you remember when you used the `rails generate scaffold` command from earlier. We created our first resource among other things it created a test stub in the `test/models` directory:
+If you remember, we used the `rails generate model` command in the
+[Getting Started with Rails](getting_started.html) guide. We created our first
+model, and among other things it created test stubs in the `test` directory:
```bash
-$ bin/rails generate scaffold article title:string body:text
+$ rails generate model article title:string body:text
...
create app/models/article.rb
create test/models/article_test.rb
@@ -196,16 +96,16 @@ A line by line examination of this file will help get you oriented to Rails test
require 'test_helper'
```
-By requiring this file, `test_helper.rb` the default configuration to run our tests is loaded. We will include this with all the tests we write, so any methods added to this file are available to all your tests.
+By requiring this file, `test_helper.rb` the default configuration to run our tests is loaded. We will include this with all the tests we write, so any methods added to this file are available to all our tests.
```ruby
class ArticleTest < ActiveSupport::TestCase
```
-The `ArticleTest` class defines a _test case_ because it inherits from `ActiveSupport::TestCase`. `ArticleTest` thus has all the methods available from `ActiveSupport::TestCase`. Later in this guide, you'll see some of the methods it gives you.
+The `ArticleTest` class defines a _test case_ because it inherits from `ActiveSupport::TestCase`. `ArticleTest` thus has all the methods available from `ActiveSupport::TestCase`. Later in this guide, we'll see some of the methods it gives us.
Any method defined within a class inherited from `Minitest::Test`
-(which is the superclass of `ActiveSupport::TestCase`) that begins with `test_` (case sensitive) is simply called a test. So, methods defined as `test_password` and `test_valid_password` are legal test names and are run automatically when the test case is run.
+(which is the superclass of `ActiveSupport::TestCase`) that begins with `test_` is simply called a test. So, methods defined as `test_password` and `test_valid_password` are legal test names and are run automatically when the test case is run.
Rails also adds a `test` method that takes a test name and a block. It generates a normal `Minitest::Unit` test with method names prefixed with `test_`. So you don't have to worry about naming the methods, and you can write something like:
@@ -223,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.
@@ -240,48 +140,7 @@ An assertion is a line of code that evaluates an object (or expression) for expe
* does this line of code throw an exception?
* is the user's password greater than 5 characters?
-Every test must contain at least one assertion, with no restriction as to how many assertions are allowed. Only when all the assertions are successful will the test pass.
-
-### Maintaining the test database schema
-
-In order to run your tests, your test database will need to have the current
-structure. The test helper checks whether your test database has any pending
-migrations. If so, it will try to load your `db/schema.rb` or `db/structure.sql`
-into the test database. If migrations are still pending, an error will be
-raised. Usually this indicates that your schema is not fully migrated. Running
-the migrations against the development database (`bin/rake db:migrate`) will
-bring the schema up to date.
-
-NOTE: If existing migrations required modifications, the test database needs to
-be rebuilt. This can be done by executing `bin/rake db:test:prepare`.
-
-### Running Tests
-
-Running a test is as simple as invoking the file containing the test cases through `rails test` command.
-
-```bash
-$ bin/rails test test/models/article_test.rb
-.
-
-Finished tests in 0.009262s, 107.9680 tests/s, 107.9680 assertions/s.
-
-1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
-```
-
-This will run all test methods from the test case.
-
-You can also run a particular test method from the test case by running the test and providing the `test method name`.
-
-```bash
-$ bin/rails test test/models/article_test.rb test_the_truth
-.
-
-Finished tests in 0.009064s, 110.3266 tests/s, 110.3266 assertions/s.
-
-1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
-```
-
-The `.` (dot) above indicates a passing test. When a test fails you see an `F`; when a test throws an error you see an `E` in its place. The last line of the output is the summary.
+Every test may contain one or more assertions, with no restriction as to how many assertions are allowed. Only when all the assertions are successful will the test pass.
#### Your first failing test
@@ -297,19 +156,29 @@ end
Let us run this newly added test (where `6` is the number of line where the test is defined).
```bash
-$ bin/rails test test/models/article_test.rb:6
+$ rails test test/models/article_test.rb:6
+Run options: --seed 44656
+
+# Running:
+
F
-Finished tests in 0.044632s, 22.4054 tests/s, 22.4054 assertions/s.
+Failure:
+ArticleTest#test_should_not_save_article_without_title [/path/to/blog/test/models/article_test.rb:6]:
+Expected true to be nil or false
- 1) Failure:
-test_should_not_save_article_without_title(ArticleTest) [test/models/article_test.rb:6]:
-Failed assertion, no message given.
-1 tests, 1 assertions, 1 failures, 0 errors, 0 skips
+rails test test/models/article_test.rb:6
+
+
+
+Finished in 0.023918s, 41.8090 runs/s, 41.8090 assertions/s.
+
+1 runs, 1 assertions, 1 failures, 0 errors, 0 skips
+
```
-In the output, `F` denotes a failure. You can see the corresponding trace shown under `1)` along with the name of the failing test. The next few lines contain the stack trace followed by a message which mentions the actual value and the expected value by the assertion. The default assertion messages provide just enough information to help pinpoint the error. To make the assertion failure message more readable, every assertion provides an optional message parameter, as shown here:
+In the output, `F` denotes a failure. You can see the corresponding trace shown under `Failure` along with the name of the failing test. The next few lines contain the stack trace followed by a message that mentions the actual value and the expected value by the assertion. The default assertion messages provide just enough information to help pinpoint the error. To make the assertion failure message more readable, every assertion provides an optional message parameter, as shown here:
```ruby
test "should not save article without title" do
@@ -321,15 +190,15 @@ end
Running this test shows the friendlier assertion message:
```bash
- 1) Failure:
-test_should_not_save_article_without_title(ArticleTest) [test/models/article_test.rb:6]:
+Failure:
+ArticleTest#test_should_not_save_article_without_title [/path/to/blog/test/models/article_test.rb:6]:
Saved the article without a title
```
Now to get this test to pass we can add a model level validation for the _title_ field.
```ruby
-class Article < ActiveRecord::Base
+class Article < ApplicationRecord
validates :title, presence: true
end
```
@@ -337,12 +206,16 @@ end
Now the test should pass. Let us verify by running the test again:
```bash
-$ bin/rails test test/models/article_test.rb:6
+$ rails test test/models/article_test.rb:6
+Run options: --seed 31252
+
+# Running:
+
.
-Finished tests in 0.047721s, 20.9551 tests/s, 20.9551 assertions/s.
+Finished in 0.027476s, 36.3952 runs/s, 36.3952 assertions/s.
-1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
+1 runs, 1 assertions, 0 failures, 0 errors, 0 skips
```
Now, if you noticed, we first wrote a test which fails for a desired
@@ -366,17 +239,26 @@ end
Now you can see even more output in the console from running the tests:
```bash
-$ bin/rails test test/models/article_test.rb
-E
+$ rails test test/models/article_test.rb
+Run options: --seed 1808
+
+# Running:
+
+.E
+
+Error:
+ArticleTest#test_should_report_error:
+NameError: undefined local variable or method 'some_undefined_variable' for #<ArticleTest:0x007fee3aa71798>
+ test/models/article_test.rb:11:in 'block in <class:ArticleTest>'
+
-Finished tests in 0.030974s, 32.2851 tests/s, 0.0000 assertions/s.
+rails test test/models/article_test.rb:9
- 1) Error:
-test_should_report_error(ArticleTest):
-NameError: undefined local variable or method `some_undefined_variable' for #<ArticleTest:0x007fe32e24afe0>
- test/models/article_test.rb:10:in `block in <class:ArticleTest>'
-1 tests, 0 assertions, 0 failures, 1 errors, 0 skips
+
+Finished in 0.040609s, 49.2500 runs/s, 24.6250 assertions/s.
+
+2 runs, 1 assertions, 0 failures, 1 errors, 0 skips
```
Notice the 'E' in the output. It denotes a test with error.
@@ -391,10 +273,10 @@ When a test fails you are presented with the corresponding backtrace. By default
Rails filters that backtrace and will only print lines relevant to your
application. This eliminates the framework noise and helps to focus on your
code. However there are situations when you want to see the full
-backtrace. Simply set the `-b` (or `--backtrace`) argument to enable this behavior:
+backtrace. Set the `-b` (or `--backtrace`) argument to enable this behavior:
```bash
-$ bin/rails test -b test/models/article_test.rb
+$ rails test -b test/models/article_test.rb
```
If we want this test to pass we can modify it to use `assert_raises` like so:
@@ -417,7 +299,7 @@ By now you've caught a glimpse of some of the assertions that are available. Ass
Here's an extract of the assertions you can use with
[`Minitest`](https://github.com/seattlerb/minitest), the default testing library
used by Rails. The `[msg]` parameter is an optional string message you can
-specify to make your test failure messages clearer. It's not required.
+specify to make your test failure messages clearer.
| Assertion | Purpose |
| ---------------------------------------------------------------- | ------- |
@@ -435,11 +317,12 @@ specify to make your test failure messages clearer. It's not required.
| `assert_no_match( regexp, string, [msg] )` | Ensures that a string doesn't match the regular expression.|
| `assert_includes( collection, obj, [msg] )` | Ensures that `obj` is in `collection`.|
| `assert_not_includes( collection, obj, [msg] )` | Ensures that `obj` is not in `collection`.|
-| `assert_in_delta( expecting, actual, [delta], [msg] )` | Ensures that the numbers `expected` and `actual` are within `delta` of each other.|
-| `assert_not_in_delta( expecting, actual, [delta], [msg] )` | Ensures that the numbers `expected` and `actual` are not within `delta` of each other.|
+| `assert_in_delta( expected, actual, [delta], [msg] )` | Ensures that the numbers `expected` and `actual` are within `delta` of each other.|
+| `assert_not_in_delta( expected, actual, [delta], [msg] )` | Ensures that the numbers `expected` and `actual` are not within `delta` of each other.|
+| `assert_in_epsilon ( expected, actual, [epsilon], [msg] )` | Ensures that the numbers `expected` and `actual` have a relative error less than `epsilon`.|
+| `assert_not_in_epsilon ( expected, actual, [epsilon], [msg] )` | Ensures that the numbers `expected` and `actual` don't have a relative error less than `epsilon`.|
| `assert_throws( symbol, [msg] ) { block }` | Ensures that the given block throws the symbol.|
| `assert_raises( exception1, exception2, ... ) { block }` | Ensures that the given block raises one of the given exceptions.|
-| `assert_nothing_raised( exception1, exception2, ... ) { block }` | Ensures that the given block doesn't raise one of the given exceptions.|
| `assert_instance_of( class, obj, [msg] )` | Ensures that `obj` is an instance of `class`.|
| `assert_not_instance_of( class, obj, [msg] )` | Ensures that `obj` is not an instance of `class`.|
| `assert_kind_of( class, obj, [msg] )` | Ensures that `obj` is an instance of `class` or is descending from it.|
@@ -450,13 +333,12 @@ specify to make your test failure messages clearer. It's not required.
| `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. 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 &
more up-to-date list, please check
[Minitest API documentation](http://docs.seattlerb.org/minitest/), specifically
-[`Minitest::Assertions`](http://docs.seattlerb.org/minitest/Minitest/Assertions.html)
+[`Minitest::Assertions`](http://docs.seattlerb.org/minitest/Minitest/Assertions.html).
Because of the modular nature of the testing framework, it is possible to create your own assertions. In fact, that's exactly what Rails does. It includes some specialized assertions to make your life easier.
@@ -468,56 +350,721 @@ Rails adds some custom assertions of its own to the `minitest` framework:
| Assertion | Purpose |
| --------------------------------------------------------------------------------- | ------- |
-| `assert_difference(expressions, difference = 1, message = nil) {...}` | Test numeric difference between the return value of an expression as a result of what is evaluated in the yielded block.|
-| `assert_no_difference(expressions, message = nil, &block)` | Asserts that the numeric result of evaluating an expression is not changed before and after invoking the passed in block.|
-| `assert_recognizes(expected_options, path, extras={}, message=nil)` | Asserts that the routing of the given path was handled correctly and that the parsed options (given in the expected_options hash) match path. Basically, it asserts that Rails recognizes the route given by expected_options.|
-| `assert_generates(expected_path, options, defaults={}, extras = {}, message=nil)` | Asserts that the provided options can be used to generate the provided path. This is the inverse of assert_recognizes. The extras parameter is used to tell the request the names and values of additional request parameters that would be in a query string. The message parameter allows you to specify a custom error message for assertion failures.|
-| `assert_response(type, message = nil)` | Asserts that the response comes with a specific status code. You can specify `:success` to indicate 200-299, `:redirect` to indicate 300-399, `:missing` to indicate 404, or `:error` to match the 500-599 range. You can also pass an explicit status number or its symbolic equivalent. For more information, see [full list of status codes](http://rubydoc.info/github/rack/rack/master/Rack/Utils#HTTP_STATUS_CODES-constant) and how their [mapping](http://rubydoc.info/github/rack/rack/master/Rack/Utils#SYMBOL_TO_STATUS_CODE-constant) works.|
-| `assert_redirected_to(options = {}, message=nil)` | Assert that the redirection options passed in match those of the redirect called in the latest action. This match can be partial, such that `assert_redirected_to(controller: "weblog")` will also match the redirection of `redirect_to(controller: "weblog", action: "show")` and so on. You can also pass named routes such as `assert_redirected_to root_path` and Active Record objects such as `assert_redirected_to @article`.|
+| [`assert_difference(expressions, difference = 1, message = nil) {...}`](http://api.rubyonrails.org/classes/ActiveSupport/Testing/Assertions.html#method-i-assert_difference) | Test numeric difference between the return value of an expression as a result of what is evaluated in the yielded block.|
+| [`assert_no_difference(expressions, message = nil, &block)`](http://api.rubyonrails.org/classes/ActiveSupport/Testing/Assertions.html#method-i-assert_no_difference) | Asserts that the numeric result of evaluating an expression is not changed before and after invoking the passed in block.|
+| [`assert_changes(expressions, message = nil, from:, to:, &block)`](http://api.rubyonrails.org/classes/ActiveSupport/Testing/Assertions.html#method-i-assert_changes) | Test that the result of evaluating an expression is changed after invoking the passed in block.|
+| [`assert_no_changes(expressions, message = nil, &block)`](http://api.rubyonrails.org/classes/ActiveSupport/Testing/Assertions.html#method-i-assert_no_changes) | Test the result of evaluating an expression is not changed after invoking the passed in block.|
+| [`assert_nothing_raised { block }`](http://api.rubyonrails.org/classes/ActiveSupport/Testing/Assertions.html#method-i-assert_nothing_raised) | Ensures that the given block doesn't raise any exceptions.|
+| [`assert_recognizes(expected_options, path, extras={}, message=nil)`](http://api.rubyonrails.org/classes/ActionDispatch/Assertions/RoutingAssertions.html#method-i-assert_recognizes) | Asserts that the routing of the given path was handled correctly and that the parsed options (given in the expected_options hash) match path. Basically, it asserts that Rails recognizes the route given by expected_options.|
+| [`assert_generates(expected_path, options, defaults={}, extras = {}, message=nil)`](http://api.rubyonrails.org/classes/ActionDispatch/Assertions/RoutingAssertions.html#method-i-assert_generates) | Asserts that the provided options can be used to generate the provided path. This is the inverse of assert_recognizes. The extras parameter is used to tell the request the names and values of additional request parameters that would be in a query string. The message parameter allows you to specify a custom error message for assertion failures.|
+| [`assert_response(type, message = nil)`](http://api.rubyonrails.org/classes/ActionDispatch/Assertions/ResponseAssertions.html#method-i-assert_response) | Asserts that the response comes with a specific status code. You can specify `:success` to indicate 200-299, `:redirect` to indicate 300-399, `:missing` to indicate 404, or `:error` to match the 500-599 range. You can also pass an explicit status number or its symbolic equivalent. For more information, see [full list of status codes](http://rubydoc.info/github/rack/rack/master/Rack/Utils#HTTP_STATUS_CODES-constant) and how their [mapping](http://rubydoc.info/github/rack/rack/master/Rack/Utils#SYMBOL_TO_STATUS_CODE-constant) works.|
+| [`assert_redirected_to(options = {}, message=nil)`](http://api.rubyonrails.org/classes/ActionDispatch/Assertions/ResponseAssertions.html#method-i-assert_redirected_to) | Asserts that the redirection options passed in match those of the redirect called in the latest action. This match can be partial, such that `assert_redirected_to(controller: "weblog")` will also match the redirection of `redirect_to(controller: "weblog", action: "show")` and so on. You can also pass named routes such as `assert_redirected_to root_path` and Active Record objects such as `assert_redirected_to @article`.|
You'll see the usage of some of these assertions in the next chapter.
-### A Brief Note About Minitest
+### A Brief Note About Test Cases
All the basic assertions such as `assert_equal` defined in `Minitest::Assertions` are also available in the classes we use in our own test cases. In fact, Rails provides the following classes for you to inherit from:
-* `ActiveSupport::TestCase`
-* `ActionController::TestCase`
-* `ActionMailer::TestCase`
-* `ActionView::TestCase`
-* `ActionDispatch::IntegrationTest`
-* `ActiveJob::TestCase`
+* [`ActiveSupport::TestCase`](http://api.rubyonrails.org/classes/ActiveSupport/TestCase.html)
+* [`ActionMailer::TestCase`](http://api.rubyonrails.org/classes/ActionMailer/TestCase.html)
+* [`ActionView::TestCase`](http://api.rubyonrails.org/classes/ActionView/TestCase.html)
+* [`ActiveJob::TestCase`](http://api.rubyonrails.org/classes/ActiveJob/TestCase.html)
+* [`ActionDispatch::IntegrationTest`](http://api.rubyonrails.org/classes/ActionDispatch/IntegrationTest.html)
+* [`ActionDispatch::SystemTestCase`](http://api.rubyonrails.org/classes/ActionDispatch/SystemTestCase.html)
+* [`Rails::Generators::TestCase`](http://api.rubyonrails.org/classes/Rails/Generators/TestCase.html)
Each of these classes include `Minitest::Assertions`, allowing us to use all of the basic assertions in our tests.
-NOTE: For more information on `Minitest`, refer to [Minitest](http://docs.seattlerb.org/minitest)
+NOTE: For more information on `Minitest`, refer to [its
+documentation](http://docs.seattlerb.org/minitest).
+
+### The Rails Test Runner
+
+We can run all of our tests at once by using the `rails test` command.
+
+Or we can run a single test file by passing the `rails test` command the filename containing the test cases.
+
+```bash
+$ rails test test/models/article_test.rb
+Run options: --seed 1559
+
+# Running:
+
+..
+
+Finished in 0.027034s, 73.9810 runs/s, 110.9715 assertions/s.
+
+2 runs, 3 assertions, 0 failures, 0 errors, 0 skips
+```
+
+This will run all test methods from the test case.
+
+You can also run a particular test method from the test case by providing the
+`-n` or `--name` flag and the test's method name.
+
+```bash
+$ rails test test/models/article_test.rb -n test_the_truth
+Run options: -n test_the_truth --seed 43583
+
+# Running:
+
+.
+
+Finished tests in 0.009064s, 110.3266 tests/s, 110.3266 assertions/s.
+
+1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
+```
+
+You can also run a test at a specific line by providing the line number.
+
+```bash
+$ rails test test/models/article_test.rb:6 # run specific test and line
+```
+
+You can also run an entire directory of tests by providing the path to the directory.
+
+```bash
+$ rails test test/controllers # run all tests from specific directory
+```
+
+The test runner also provides a lot of other features like failing fast, deferring test output
+at the end of test run and so on. Check the documentation of the test runner as follows:
+
+```bash
+$ rails test -h
+Usage: rails test [options] [files or directories]
+
+You can run a single test by appending a line number to a filename:
+
+ rails test test/models/user_test.rb:27
+
+You can run multiple files and directories at the same time:
+
+ rails test test/controllers test/integration/login_test.rb
+
+By default test failures and errors are reported inline during a run.
+
+minitest options:
+ -h, --help Display this help.
+ --no-plugins Bypass minitest plugin auto-loading (or set $MT_NO_PLUGINS).
+ -s, --seed SEED Sets random seed. Also via env. Eg: SEED=n rake
+ -v, --verbose Verbose. Show progress processing files.
+ -n, --name PATTERN Filter run on /regexp/ or string.
+ --exclude PATTERN Exclude /regexp/ or string from run.
+
+Known extensions: rails, pride
+ -w, --warnings Run with Ruby warnings enabled
+ -e, --environment ENV Run tests in the ENV environment
+ -b, --backtrace Show the complete backtrace
+ -d, --defer-output Output test failures and errors after the test run
+ -f, --fail-fast Abort test run on first failure or error
+ -c, --[no-]color Enable color in the output
+ -p, --pride Pride. Show your testing pride!
+```
+
+Parallel Testing
+----------------
+
+Parallel testing allows you to parallelize your test suite. While forking processes is the
+default method, threading is supported as well. Running tests in parallel reduces the time it
+takes your entire test suite to run.
+
+### Parallel testing with processes
+
+The default parallelization method is to fork processes using Ruby's DRb system. The processes
+are forked based on the number of workers provided. The default is 2, but can be changed by the
+number passed to the parallelize method. Active Record automatically handles creating and
+migrating a new database for each worker to use.
+
+To enable parallelization add the following to your `test_helper.rb`:
+
+```
+class ActiveSupport::TestCase
+ parallelize(workers: 2)
+end
+```
+
+The number of workers passed is the number of times the process will be forked. You may want to
+parallelize your local test suite differently from your CI, so an environment variable is provided
+to be able to easily change the number of workers a test run should use:
+
+```
+PARALLEL_WORKERS=15 rails test
+```
+
+When parallelizing tests, Active Record automatically handles creating and migrating a database for each
+process. The databases will be suffixed with the number corresponding to the worker. For example, if you
+have 2 workers the tests will create `test-database-0` and `test-database-1` respectively.
+
+If the number of workers passed is 1 or fewer the processes will not be forked and the tests will not
+be parallelized and the tests will use the original `test-database` database.
+
+Two hooks are provided, one runs when the process is forked, and one runs before the processes are closed.
+These can be useful if your app uses multiple databases or perform other tasks that depend on the number of
+workers.
+
+The `parallelize_setup` method is called right after the processes are forked. The `parallelize_teardown` method
+is called right before the processes are closed.
+
+```
+class ActiveSupport::TestCase
+ parallelize_setup do |worker|
+ # setup databases
+ end
+
+ parallelize_teardown do |worker|
+ # cleanup database
+ end
+
+ parallelize(workers: 2)
+end
+```
+
+These methods are not needed or available when using parallel testing with threads.
+
+### Parallel testing with threads
+
+If you prefer using threads or are using JRuby, a threaded parallelization option is provided. The threaded
+parallelizer is backed by Minitest's `Parallel::Executor`.
+
+To change the parallelization method to use threads over forks put the following in your `test_helper.rb`
+
+```
+class ActiveSupport::TestCase
+ parallelize(workers: 2, with: :threads)
+end
+```
+
+Rails applications generated from JRuby will automatically include the `with: :threads` option.
+
+The number of workers passed to `parallelize` determines the number of threads the tests will use. You may
+want to parallelize your local test suite differently from your CI, so an environment variable is provided
+to be able to easily change the number of workers a test run should use:
+
+```
+PARALLEL_WORKERS=15 rails test
+```
+
+The Test Database
+-----------------
+
+Just about every Rails application interacts heavily with a database and, as a result, your tests will need a database to interact with as well. To write efficient tests, you'll need to understand how to set up this database and populate it with sample data.
+
+By default, every Rails application has three environments: development, test, and production. The database for each one of them is configured in `config/database.yml`.
+
+A dedicated test database allows you to set up and interact with test data in isolation. This way your tests can mangle test data with confidence, without worrying about the data in the development or production databases.
+
+
+### Maintaining the test database schema
+
+In order to run your tests, your test database will need to have the current
+structure. The test helper checks whether your test database has any pending
+migrations. It will try to load your `db/schema.rb` or `db/structure.sql`
+into the test database. If migrations are still pending, an error will be
+raised. Usually this indicates that your schema is not fully migrated. Running
+the migrations against the development database (`rails db:migrate`) will
+bring the schema up to date.
+
+NOTE: If there were modifications to existing migrations, the test database needs to
+be rebuilt. This can be done by executing `rails db:test:prepare`.
+
+### The Low-Down on Fixtures
+
+For good tests, you'll need to give some thought to setting up test data.
+In Rails, you can handle this by defining and customizing fixtures.
+You can find comprehensive documentation in the [Fixtures API documentation](http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html).
+
+#### What Are Fixtures?
+
+_Fixtures_ is a fancy word for sample data. Fixtures allow you to populate your testing database with predefined data before your tests run. Fixtures are database independent and written in YAML. There is one file per model.
+
+NOTE: Fixtures are not designed to create every object that your tests need, and are best managed when only used for default data that can be applied to the common case.
+
+You'll find fixtures under your `test/fixtures` directory. When you run `rails generate model` to create a new model, Rails automatically creates fixture stubs in this directory.
+
+#### YAML
+
+YAML-formatted fixtures are a human-friendly way to describe your sample data. These types of fixtures have the **.yml** file extension (as in `users.yml`).
+
+Here's a sample YAML fixture file:
+
+```yaml
+# lo & behold! I am a YAML comment!
+david:
+ name: David Heinemeier Hansson
+ birthday: 1979-10-15
+ profession: Systems development
+
+steve:
+ name: Steve Ross Kellock
+ birthday: 1974-09-27
+ profession: guy with keyboard
+```
+
+Each fixture is given a name followed by an indented list of colon-separated key/value pairs. Records are typically separated by a blank line. You can place comments in a fixture file by using the # character in the first column.
+
+If you are working with [associations](/association_basics.html), you can
+define a reference node between two different fixtures. Here's an example with
+a `belongs_to`/`has_many` association:
+
+```yaml
+# In fixtures/categories.yml
+about:
+ name: About
+
+# In fixtures/articles.yml
+first:
+ title: Welcome to Rails!
+ body: Hello world!
+ category: about
+```
+
+Notice the `category` key of the `first` article found in `fixtures/articles.yml` has a value of `about`. This tells Rails to load the category `about` found in `fixtures/categories.yml`.
+
+NOTE: For associations to reference one another by name, you can use the fixture name instead of specifying the `id:` attribute on the associated fixtures. Rails will auto assign a primary key to be consistent between runs. For more information on this association behavior please read the [Fixtures API documentation](http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html).
+
+#### ERB'in It Up
+
+ERB allows you to embed Ruby code within templates. The YAML fixture format is pre-processed with ERB when Rails loads fixtures. This allows you to use Ruby to help you generate some sample data. For example, the following code generates a thousand users:
+
+```erb
+<% 1000.times do |n| %>
+user_<%= n %>:
+ username: <%= "user#{n}" %>
+ email: <%= "user#{n}@example.com" %>
+<% end %>
+```
+
+#### Fixtures in Action
+
+Rails automatically loads all fixtures from the `test/fixtures` directory by
+default. Loading involves three steps:
+
+1. Remove any existing data from the table corresponding to the fixture
+2. Load the fixture data into the table
+3. Dump the fixture data into a method in case you want to access it directly
+
+TIP: In order to remove existing data from the database, Rails tries to disable referential integrity triggers (like foreign keys and check constraints). If you are getting annoying permission errors on running tests, make sure the database user has privilege to disable these triggers in testing environment. (In PostgreSQL, only superusers can disable all triggers. Read more about PostgreSQL permissions [here](http://blog.endpoint.com/2012/10/postgres-system-triggers-error.html)).
+
+#### Fixtures are Active Record objects
+
+Fixtures are instances of Active Record. As mentioned in point #3 above, you can access the object directly because it is automatically available as a method whose scope is local of the test case. For example:
+
+```ruby
+# this will return the User object for the fixture named david
+users(:david)
+
+# this will return the property for david called id
+users(:david).id
+
+# one can also access methods available on the User class
+david = users(:david)
+david.call(david.partner)
+```
+
+To get multiple fixtures at once, you can pass in a list of fixture names. For example:
+
+```ruby
+# this will return an array containing the fixtures david and steve
+users(:david, :steve)
+```
+
+
+Model Testing
+-------------
+
+Model tests are used to test the various models of your application.
+
+Rails model tests are stored under the `test/models` directory. Rails provides
+a generator to create a model test skeleton for you.
+
+```bash
+$ rails generate test_unit:model article title:string body:text
+create test/models/article_test.rb
+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 allow you to test user interactions with your application, running tests
+in either a real or a headless browser. System tests use Capybara under the hood.
+
+For creating Rails system tests, you use the `test/system` directory in your
+application. Rails provides a generator to create a system test skeleton for you.
+
+```bash
+$ rails generate system_test users
+ invoke test_unit
+ create test/system/users_test.rb
+```
+
+Here's what a freshly generated system test looks like:
+
+```ruby
+require "application_system_test_case"
+
+class UsersTest < ApplicationSystemTestCase
+ # test "visiting the index" do
+ # visit users_url
+ #
+ # assert_selector "h1", text: "Users"
+ # 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 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 by Selenium), `:screen_size` to change the size of the screen for
+screenshots, and `:options` which can be used to set options supported by the
+driver.
+
+```ruby
+require "test_helper"
+
+class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
+ driven_by :selenium, using: :firefox
+end
+```
+
+If you want to use a headless browser, you could use Headless Chrome or Headless Firefox by adding
+`headless_chrome` or `headless_firefox` in the `:using` argument.
+
+```ruby
+require "test_helper"
+
+class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
+ driven_by :selenium, using: :headless_chrome
+end
+```
+
+If your Capybara configuration requires more setup than provided by Rails, this
+additional configuration could be added into the `application_system_test_case.rb`
+file.
+
+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 was automatically
+created for you. If you didn't use the scaffold generator, start by creating a
+system test skeleton.
+
+```bash
+$ 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
+rails test:system
+```
+
+NOTE: By default, running `rails test` won't run your system tests.
+Make sure to run `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 articles index page and there we assert
+that the text from the new article's title is on the articles index page.
+
+#### Testing for multiple screen sizes
+If you want to test for mobile sizes on top of testing for desktop,
+you can create another class that inherits from SystemTestCase and use in your
+test suite. In this example a file called `mobile_system_test_case.rb` is created
+in the `/test` directory with the following configuration.
+
+```ruby
+require "test_helper"
+
+class MobileSystemTestCase < ActionDispatch::SystemTestCase
+ driven_by :selenium, using: :chrome, screen_size: [375, 667]
+end
+```
+To use this configuration, create a test inside `test/system` that inherits from `MobileSystemTestCase`.
+Now you can test your app using multiple different configurations.
+
+```ruby
+require "mobile_system_test_case"
+
+class PostsTest < MobileSystemTestCase
+
+ test "visiting the index" do
+ visit posts_url
+ assert_selector "h1", text: "Posts"
+ end
+end
+```
+
+#### Taking it further
+
+The beauty of system testing is that it is similar to integration testing in
+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
+-------------------
+
+Integration tests are used to test how various parts of your application interact. They are generally used to test important workflows within our application.
+
+For creating Rails integration tests, we use the `test/integration` directory for our application. Rails provides a generator to create an integration test skeleton for us.
+
+```bash
+$ rails generate integration_test user_flows
+ exists test/integration/
+ create test/integration/user_flows_test.rb
+```
+
+Here's what a freshly generated integration test looks like:
+
+```ruby
+require 'test_helper'
+
+class UserFlowsTest < ActionDispatch::IntegrationTest
+ # test "the truth" do
+ # assert true
+ # end
+end
+```
+
+Here the test is inheriting from `ActionDispatch::IntegrationTest`. This makes some additional helpers available for us to use in our integration tests.
+
+### Helpers Available for Integration Tests
+
+In addition to the standard testing helpers, inheriting from `ActionDispatch::IntegrationTest` comes with some additional helpers available when writing integration tests. Let's get briefly introduced to the three categories of helpers we get to choose from.
+
+For dealing with the integration test runner, see [`ActionDispatch::Integration::Runner`](http://api.rubyonrails.org/classes/ActionDispatch/Integration/Runner.html).
+
+When performing requests, we will have [`ActionDispatch::Integration::RequestHelpers`](http://api.rubyonrails.org/classes/ActionDispatch/Integration/RequestHelpers.html) available for our use.
+
+If we need to modify the session, or state of our integration test, take a look at [`ActionDispatch::Integration::Session`](http://api.rubyonrails.org/classes/ActionDispatch/Integration/Session.html) to help.
+
+### Implementing an integration test
+
+Let's add an integration test to our blog application. We'll start with a basic workflow of creating a new blog article, to verify that everything is working properly.
+
+We'll start by generating our integration test skeleton:
+
+```bash
+$ rails generate integration_test blog_flow
+```
+
+It should have created a test file placeholder for us. With the output of the
+previous command we should see:
+
+```bash
+ invoke test_unit
+ create test/integration/blog_flow_test.rb
+```
+
+Now let's open that file and write our first assertion:
+
+```ruby
+require 'test_helper'
+
+class BlogFlowTest < ActionDispatch::IntegrationTest
+ test "can see the welcome page" do
+ get "/"
+ assert_select "h1", "Welcome#index"
+ end
+end
+```
+
+We will take a look at `assert_select` to query the resulting HTML of a request in the "Testing Views" section below. It is used for testing the response of our request by asserting the presence of key HTML elements and their content.
+
+When we visit our root path, we should see `welcome/index.html.erb` rendered for the view. So this assertion should pass.
+
+#### Creating articles integration
+
+How about testing our ability to create a new article in our blog and see the resulting article.
+
+```ruby
+test "can create an article" do
+ get "/articles/new"
+ assert_response :success
+
+ post "/articles",
+ params: { article: { title: "can create", body: "article successfully." } }
+ assert_response :redirect
+ follow_redirect!
+ assert_response :success
+ assert_select "p", "Title:\n can create"
+end
+```
+
+Let's break this test down so we can understand it.
+
+We start by calling the `:new` action on our Articles controller. This response should be successful.
+
+After this we make a post request to the `:create` action of our Articles controller:
+
+```ruby
+post "/articles",
+ params: { article: { title: "can create", body: "article successfully." } }
+assert_response :redirect
+follow_redirect!
+```
+
+The two lines following the request are to handle the redirect we setup when creating a new article.
+
+NOTE: Don't forget to call `follow_redirect!` if you plan to make subsequent requests after a redirect is made.
+
+Finally we can assert that our response was successful and our new article is readable on the page.
+
+#### Taking it further
+
+We were able to successfully test a very small workflow for visiting our blog and creating a new article. If we wanted to take this further we could add tests for commenting, removing articles, or editing comments. Integration tests are a great place to experiment with all kinds of use-cases for our applications.
+
Functional Tests for Your Controllers
-------------------------------------
-In Rails, testing the various actions of a controller is a form of writing functional tests. Remember your controllers handle the incoming web requests to your application and eventually respond with a rendered view. When writing functional tests, you're testing how your actions handle the requests and the expected result, or response in some cases an HTML view.
+In Rails, testing the various actions of a controller is a form of writing functional tests. Remember your controllers handle the incoming web requests to your application and eventually respond with a rendered view. When writing functional tests, you are testing how your actions handle the requests and the expected result or response, in some cases an HTML view.
-### What to Include in your Functional Tests
+### What to include in your Functional Tests
You should test for things such as:
* was the web request successful?
* was the user redirected to the right page?
* was the user successfully authenticated?
-* was the correct object stored in the response template?
* was the appropriate message displayed to the user in the view?
+* was the correct information displayed in the response?
-Now that we have used Rails scaffold generator for our `Article` resource, it has already created the controller code and tests. You can take look at the file `articles_controller_test.rb` in the `test/controllers` directory.
+The easiest way to see functional tests in action is to generate a controller using the scaffold generator:
-Let me take you through one such test, `test_should_get_index` from the file `articles_controller_test.rb`.
+```bash
+$ rails generate scaffold_controller article title:string body:text
+...
+create app/controllers/articles_controller.rb
+...
+invoke test_unit
+create test/controllers/articles_controller_test.rb
+...
+```
+
+This will generate the controller code and tests for an `Article` resource.
+You can take a look at the file `articles_controller_test.rb` in the `test/controllers` directory.
+
+If you already have a controller and just want to generate the test scaffold code for
+each of the seven default actions, you can use the following command:
+
+```bash
+$ rails generate test_unit:scaffold article
+...
+invoke test_unit
+create test/controllers/articles_controller_test.rb
+...
+```
+
+Let's take a look at one such test, `test_should_get_index` from the file `articles_controller_test.rb`.
```ruby
# articles_controller_test.rb
-class ArticlesControllerTest < ActionController::TestCase
+class ArticlesControllerTest < ActionDispatch::IntegrationTest
test "should get index" do
- get :index
+ get articles_url
assert_response :success
- assert_includes @response.body, 'Articles'
end
end
```
@@ -525,30 +1072,29 @@ end
In the `test_should_get_index` test, Rails simulates a request on the action called `index`, making sure the request was successful
and also ensuring that the right response body has been generated.
-The `get` method kicks off the web request and populates the results into the response. It accepts 4 arguments:
-
-* The action of the controller you are requesting.
- This can be in the form of a string or a symbol.
+The `get` method kicks off the web request and populates the results into the `@response`. It can accept up to 6 arguments:
+* The URI of the controller action you are requesting.
+ This can be in the form of a string or a route helper (e.g. `articles_url`).
* `params`: option with a hash of request parameters to pass into the action
(e.g. query string parameters or article variables).
+* `headers`: for setting the headers that will be passed with the request.
+* `env`: for customizing the request environment as needed.
+* `xhr`: whether the request is Ajax request or not. Can be set to true for marking the request as Ajax.
+* `as`: for encoding the request with different content type. Supports `:json` by default.
-* `session`: option with a hash of session variables to pass along with the request.
-
-* `flash`: option with a hash of flash values.
-
-All the keyword arguments are optional.
+All of these keyword arguments are optional.
-Example: Calling the `:show` action, passing an `id` of 12 as the `params` and setting a `user_id` of 5 in the session:
+Example: Calling the `:show` action for the first `Article`, passing in an `HTTP_REFERER` header:
```ruby
-get(:show, params: { id: 12 }, session: { user_id: 5 })
+get article_url(Article.first), headers: { "HTTP_REFERER" => "http://example.com/home" }
```
-Another example: Calling the `:view` action, passing an `id` of 12 as the `params`, this time with no session, but with a flash message.
+Another example: Calling the `:update` action for the last `Article`, passing in new text for the `title` in `params`, as an Ajax request:
```ruby
-get(:view, params: { id: 12 }, flash: { message: 'booya!' })
+patch article_url(Article.last), params: { article: { title: "updated" } }, xhr: true
```
NOTE: If you try running `test_should_create_article` test from `articles_controller_test.rb` it will fail on account of the newly added model level validation and rightly so.
@@ -558,7 +1104,7 @@ Let us modify `test_should_create_article` test in `articles_controller_test.rb`
```ruby
test "should create article" do
assert_difference('Article.count') do
- post :create, params: { article: { title: 'Some title' } }
+ post articles_url, params: { article: { body: 'Rails is awesome!', title: 'Hello Rails' } }
end
assert_redirected_to article_path(Article.last)
@@ -567,6 +1113,12 @@ 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 authorization to every request header to get all the tests passing:
+
+```ruby
+post articles_url, params: { article: { body: 'Rails is awesome!', title: 'Hello Rails' } }, 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:
@@ -578,31 +1130,32 @@ If you're familiar with the HTTP protocol, you'll know that `get` is a type of r
* `head`
* `delete`
-All of request types have equivalent methods that you can use. In a typical C.R.U.D. application you'll be using `get`, `post`, `put` and `delete` more often.
+All of request types have equivalent methods that you can use. In a typical C.R.U.D. application you'll be using `get`, `post`, `put`, and `delete` more often.
NOTE: Functional tests do not verify whether the specified request type is accepted by the action, we're more concerned with the result. Request tests exist for this use case to make your tests more purposeful.
### Testing XHR (AJAX) requests
To test AJAX requests, you can specify the `xhr: true` option to `get`, `post`,
-`patch`, `put`, and `delete` methods:
+`patch`, `put`, and `delete` methods. For example:
```ruby
test "ajax request" do
- get :show, params: { id: articles(:first).id }, xhr: true
+ article = articles(:one)
+ get article_url(article), xhr: true
assert_equal 'hello world', @response.body
assert_equal "text/javascript", @response.content_type
end
```
-### The Four Hashes of the Apocalypse
+### The Three Hashes of the Apocalypse
-After a request has been made and processed, you will have 4 Hash objects ready for use:
+After a request has been made and processed, you will have 3 Hash objects ready for use:
-* `cookies` - Any cookies that are set.
-* `flash` - Any objects living in the flash.
-* `session` - Any object living in session variables.
+* `cookies` - Any cookies that are set
+* `flash` - Any objects living in the flash
+* `session` - Any object living in session variables
As is the case with normal Hash objects, you can access the values by referencing the keys by string. You can also reference them by symbol name. For example:
@@ -614,32 +1167,43 @@ cookies["are_good_for_u"] cookies[:are_good_for_u]
### Instance Variables Available
-You also have access to three instance variables in your functional tests:
+You also have access to three instance variables in your functional tests, after a request is made:
* `@controller` - The controller processing the request
* `@request` - The request object
* `@response` - The response object
+
+```ruby
+class ArticlesControllerTest < ActionDispatch::IntegrationTest
+ test "should get index" do
+ get articles_url
+
+ assert_equal "index", @controller.action_name
+ assert_equal "application/x-www-form-urlencoded", @request.media_type
+ assert_match "Articles", @response.body
+ end
+end
+```
+
### Setting Headers and CGI variables
-[HTTP headers](http://tools.ietf.org/search/rfc2616#section-5.3)
+[HTTP headers](https://tools.ietf.org/search/rfc2616#section-5.3)
and
-[CGI variables](http://tools.ietf.org/search/rfc3875#section-4.1)
-can be set directly on the `@request` instance variable:
+[CGI variables](https://tools.ietf.org/search/rfc3875#section-4.1)
+can be passed as headers:
```ruby
-# setting a HTTP Header
-@request.headers["Accept"] = "text/plain, text/html"
-get :index # simulate the request with custom header
+# setting an HTTP Header
+get articles_url, headers: { "Content-Type": "text/plain" } # simulate the request with custom header
# setting a CGI variable
-@request.headers["HTTP_REFERER"] = "http://example.com/home"
-post :create # simulate the request with custom env variable
+get articles_url, headers: { "HTTP_REFERER": "http://example.com/home" } # simulate the request with custom env variable
```
### Testing `flash` notices
-If you remember from earlier one of the Four Hashes of the Apocalypse was `flash`.
+If you remember from earlier, one of the Three Hashes of the Apocalypse was `flash`.
We want to add a `flash` message to our blog application whenever someone
successfully creates a new Article.
@@ -649,7 +1213,7 @@ Let's start by adding this assertion to our `test_should_create_article` test:
```ruby
test "should create article" do
assert_difference('Article.count') do
- post :create, params: { article: { title: 'Some title' } }
+ post article_url, params: { article: { title: 'Some title' } }
end
assert_redirected_to article_path(Article.last)
@@ -660,7 +1224,7 @@ end
If we run our test now, we should see a failure:
```bash
-$ bin/rails test test/controllers/articles_controller_test.rb test_should_create_article
+$ rails test test/controllers/articles_controller_test.rb -n test_should_create_article
Run options: -n test_should_create_article --seed 32266
# Running:
@@ -670,7 +1234,7 @@ F
Finished in 0.114870s, 8.7055 runs/s, 34.8220 assertions/s.
1) Failure:
-ArticlesControllerTest#test_should_create_article [/Users/zzak/code/bench/sharedapp/test/controllers/articles_controller_test.rb:16]:
+ArticlesControllerTest#test_should_create_article [/test/controllers/articles_controller_test.rb:16]:
--- expected
+++ actual
@@ -1 +1 @@
@@ -698,7 +1262,7 @@ end
Now if we run our tests, we should see it pass:
```bash
-$ bin/rails test test/controllers/articles_controller_test.rb test_should_create_article
+$ rails test test/controllers/articles_controller_test.rb -n test_should_create_article
Run options: -n test_should_create_article --seed 18981
# Running:
@@ -719,12 +1283,12 @@ Let's write a test for the `:show` action:
```ruby
test "should show article" do
article = articles(:one)
- get :show, params: { id: article.id }
+ get article_url(article)
assert_response :success
end
```
-Remember from our discussion earlier on fixtures the `articles()` method will give us access to our Articles fixtures.
+Remember from our discussion earlier on fixtures, the `articles()` method will give us access to our Articles fixtures.
How about deleting an existing Article?
@@ -732,7 +1296,7 @@ How about deleting an existing Article?
test "should destroy article" do
article = articles(:one)
assert_difference('Article.count', -1) do
- delete :destroy, params: { id: article.id }
+ delete article_url(article)
end
assert_redirected_to articles_path
@@ -744,47 +1308,56 @@ We can also add a test for updating an existing Article.
```ruby
test "should update article" do
article = articles(:one)
- patch :update, params: { id: article.id, article: { title: "updated" } }
+
+ patch article_url(article), params: { article: { title: "updated" } }
+
assert_redirected_to article_path(article)
+ # Reload association to fetch updated data and assert that title is updated.
+ article.reload
+ assert_equal "updated", article.title
end
```
Notice we're starting to see some duplication in these three tests, they both access the same Article fixture data. We can D.R.Y. this up by using the `setup` and `teardown` methods provided by `ActiveSupport::Callbacks`.
-Our test should now look something like this, disregard the other tests we're leaving them out for brevity.
+Our test should now look something as what follows. Disregard the other tests for now, we're leaving them out for brevity.
```ruby
require 'test_helper'
-class ArticlesControllerTest < ActionController::TestCase
+class ArticlesControllerTest < ActionDispatch::IntegrationTest
# called before every single test
- def setup
+ setup do
@article = articles(:one)
end
# called after every single test
- def teardown
+ teardown do
# when controller is using cache it may be a good idea to reset it afterwards
Rails.cache.clear
end
test "should show article" do
# Reuse the @article instance variable from setup
- get :show, params: { id: @article.id }
+ get article_url(@article)
assert_response :success
end
test "should destroy article" do
assert_difference('Article.count', -1) do
- delete :destroy, params: { id: @article.id }
+ delete article_url(@article)
end
assert_redirected_to articles_path
end
test "should update article" do
- patch :update, params: { id: @article.id, article: { title: "updated" } }
+ patch article_url(@article), params: { article: { title: "updated" } }
+
assert_redirected_to article_path(@article)
+ # Reload association to fetch updated data and assert that title is updated.
+ @article.reload
+ assert_equal "updated", @article.title
end
end
```
@@ -797,15 +1370,15 @@ To avoid code duplication, you can add your own test helpers.
Sign in helper can be a good example:
```ruby
-test/test_helper.rb
+# test/test_helper.rb
module SignInHelper
- def sign_in(user)
- session[:user_id] = user.id
+ def sign_in_as(user)
+ post sign_in_url(email: user.email, password: user.password)
end
end
-class ActionController::TestCase
+class ActionDispatch::IntegrationTest
include SignInHelper
end
```
@@ -813,13 +1386,13 @@ end
```ruby
require 'test_helper'
-class ProfileControllerTest < ActionController::TestCase
+class ProfileControllerTest < ActionDispatch::IntegrationTest
test "should show profile" do
# helper is now reusable from any controller test case
- sign_in users(:david)
+ sign_in_as users(:david)
- get :show
+ get profile_url
assert_response :success
end
end
@@ -828,33 +1401,9 @@ end
Testing Routes
--------------
-Like everything else in your Rails application, it is recommended that you test your routes. Below are example tests for the routes of default `show` and `create` action of `Articles` controller above and it should look like:
-
-```ruby
-class ArticleRoutesTest < ActionController::TestCase
- test "should route to article" do
- assert_routing '/articles/1', { controller: "articles", action: "show", id: "1" }
- end
-
- test "should route to create article" do
- assert_routing({ method: 'post', path: '/articles' }, { controller: "articles", action: "create" })
- end
-end
-```
-
-I've added this file here `test/controllers/articles_routes_test.rb` and if we run the test we should see:
-
-```bash
-$ bin/rails test test/controllers/articles_routes_test.rb
+Like everything else in your Rails application, you can test your routes. Route tests reside in `test/controllers/` or are part of controller tests.
-# Running:
-
-..
-
-Finished in 0.069381s, 28.8263 runs/s, 86.4790 assertions/s.
-
-2 runs, 6 assertions, 0 failures, 0 errors, 0 skips
-```
+NOTE: If your application has complex routes, Rails provides a number of useful helpers to test them.
For more information on routing assertions available in Rails, see the API documentation for [`ActionDispatch::Assertions::RoutingAssertions`](http://api.rubyonrails.org/classes/ActionDispatch/Assertions/RoutingAssertions.html).
@@ -902,7 +1451,7 @@ assert_select "ol" do
end
```
-This assertion is quite powerful. For more advanced usage, refer to its [documentation](http://www.rubydoc.info/github/rails/rails-dom-testing).
+This assertion is quite powerful. For more advanced usage, refer to its [documentation](https://github.com/rails/rails-dom-testing/blob/master/lib/rails/dom/testing/assertions/selector_assertions.rb).
#### Additional View-Based Assertions
@@ -925,29 +1474,31 @@ end
Testing Helpers
---------------
+A helper is just a simple module where you can define methods which are
+available in your views.
+
In order to test helpers, all you need to do is check that the output of the
helper method matches what you'd expect. Tests related to the helpers are
located under the `test/helpers` directory.
-A helper test looks like so:
+Given we have the following helper:
```ruby
-require 'test_helper'
-
-class UserHelperTest < ActionView::TestCase
+module UsersHelper
+ def link_to_user(user)
+ link_to "#{user.first_name} #{user.last_name}", user
+ end
end
```
-A helper is just a simple module where you can define methods which are
-available into your views. To test the output of the helper's methods, you just
-have to use a mixin like this:
+We can test the output of this method like this:
```ruby
-class UserHelperTest < ActionView::TestCase
- include UserHelper
+class UsersHelperTest < ActionView::TestCase
+ test "should return the user's full name" do
+ user = users(:david)
- test "should return the user name" do
- # ...
+ assert_dom_equal %{<a href="/user/#{user.id}">David Heinemeier Hansson</a>}, link_to_user(user)
end
end
```
@@ -955,118 +1506,6 @@ end
Moreover, since the test class extends from `ActionView::TestCase`, you have
access to Rails' helper methods such as `link_to` or `pluralize`.
-Integration Testing
--------------------
-
-Integration tests are used to test how various parts of your application interact. They are generally used to test important work flows within your application.
-
-For creating Rails integration tests, we use the 'test/integration' directory for your application. Rails provides a generator to create an integration test skeleton for you.
-
-```bash
-$ bin/rails generate integration_test user_flows
- exists test/integration/
- create test/integration/user_flows_test.rb
-```
-
-Here's what a freshly-generated integration test looks like:
-
-```ruby
-require 'test_helper'
-
-class UserFlowsTest < ActionDispatch::IntegrationTest
- # test "the truth" do
- # assert true
- # end
-end
-```
-
-Inheriting from `ActionDispatch::IntegrationTest` comes with some advantages. This makes available some additional helpers to use in your integration tests.
-
-### Helpers Available for Integration Tests
-
-In addition to the standard testing helpers, inheriting `ActionDispatch::IntegrationTest` comes with some additional helpers available when writing integration tests. Let's briefly introduce you to the three categories of helpers you get to choose from.
-
-For dealing with the integration test runner, see [`ActionDispatch::Integration::Runner`](http://api.rubyonrails.org/classes/ActionDispatch/Integration/Runner.html).
-
-When performing requests, you will have [`ActionDispatch::Integration::RequestHelpers`](http://api.rubyonrails.org/classes/ActionDispatch/Integration/RequestHelpers.html) available for your use.
-
-If you'd like to modify the session, or state of your integration test you should look for [`ActionDispatch::Integration::Session`](http://api.rubyonrails.org/classes/ActionDispatch/Integration/Session.html) to help.
-
-### Implementing an integration test
-
-Let's add an integration test to our blog application. We'll start with a basic workflow of creating a new blog article, to verify that everything is working properly.
-
-We'll start by generating our integration test skeleton:
-
-```bash
-$ bin/rails generate integration_test blog_flow
-```
-
-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/integration/blog_flow_test.rb
-```
-
-Now let's open that file and write our first assertion:
-
-```ruby
-require 'test_helper'
-
-class BlogFlowTest < ActionDispatch::IntegrationTest
- test "can see the welcome page" do
- get "/"
- assert_select "h1", "Welcome#index"
- end
-end
-```
-
-If you remember from earlier in the "Testing Views" section we covered `assert_select` to query the resulting HTML of a request.
-
-When visit our root path, we should see `welcome/index.html.erb` rendered for the view. So this assertion should pass.
-
-#### Creating articles integration
-
-How about testing our ability to create a new article in our blog and see the resulting article.
-
-```ruby
-test "can create an article" do
- get "/articles/new"
- assert_response :success
-
- post "/articles",
- params: { article: { title: "can create", body: "article successfully." } }
- assert_response :redirect
- follow_redirect!
- assert_response :success
- assert_select "p", "Title:\n can create"
-end
-```
-
-Let's break this test down so we can understand it.
-
-We start by calling the `:new` action on our Articles controller. This response should be successful, and we can verify the correct template is rendered including the form partial.
-
-After this we make a post request to the `:create` action of our Articles controller:
-
-```ruby
-post "/articles",
- params: { article: { title: "can create", body: "article successfully." } }
-assert_response :redirect
-follow_redirect!
-```
-
-The two lines following the request are to handle the redirect we setup when creating a new article.
-
-NOTE: Don't forget to call `follow_redirect!` if you plan to make subsequent requests after a redirect is made.
-
-Finally we can assert that our response was successful, template was rendered, and our new article is readable on the page.
-
-#### Taking it further
-
-We were able to successfully test a very small workflow for visiting our blog and creating a new article. If we wanted to take this further we could add tests for commenting, removing articles, or editting comments. Integration tests are a great place to experiment with all kinds of use-cases for our applications.
-
Testing Your Mailers
--------------------
@@ -1094,7 +1533,7 @@ In order to test that your mailer is working as expected, you can use unit tests
For the purposes of unit testing a mailer, fixtures are used to provide an example of how the output _should_ look. Because these are example emails, and not Active Record data like the other fixtures, they are kept in their own subdirectory apart from the other fixtures. The name of the directory within `test/fixtures` directly corresponds to the name of the mailer. So, for a mailer named `UserMailer`, the fixtures should reside in `test/fixtures/user_mailer` directory.
-When you generated your mailer, the generator creates stub fixtures for each of the mailers actions. If you didn't use the generator you'll have to make those files yourself.
+If you generated your mailer, the generator does not create stub fixtures for the mailers actions. You'll have to create those files yourself as described above.
#### The Basic Test Case
@@ -1105,10 +1544,13 @@ require 'test_helper'
class UserMailerTest < ActionMailer::TestCase
test "invite" do
+ # Create the email and store it for further assertions
+ email = UserMailer.create_invite('me@example.com',
+ 'friend@example.com', Time.now)
+
# Send the email, then test that it got queued
assert_emails 1 do
- email = UserMailer.create_invite('me@example.com',
- 'friend@example.com', Time.now).deliver_now
+ email.deliver_now
end
# Test the body of the sent email contains what we expect it to
@@ -1120,11 +1562,15 @@ class UserMailerTest < ActionMailer::TestCase
end
```
-In the test we send the email and store the returned object in the `email`
+In the test we create the email and store the returned object in the `email`
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:
```
@@ -1143,27 +1589,27 @@ testing) but instead it will be appended to an array
(`ActionMailer::Base.deliveries`).
NOTE: The `ActionMailer::Base.deliveries` array is only reset automatically in
-`ActionMailer::TestCase` tests. If you want to have a clean slate outside Action
-Mailer tests, you can reset it manually with:
-`ActionMailer::Base.deliveries.clear`
+`ActionMailer::TestCase` and `ActionDispatch::IntegrationTest` tests.
+If you want to have a clean slate outside these test cases, you can reset it
+manually with: `ActionMailer::Base.deliveries.clear`
### Functional Testing
-Functional testing for mailers involves more than just checking that the email body, recipients and so forth are correct. In functional mail tests you call the mail deliver methods and check that the appropriate emails have been appended to the delivery list. It is fairly safe to assume that the deliver methods themselves do their job. You are probably more interested in whether your own business logic is sending emails when you expect them to go out. For example, you can check that the invite friend operation is sending an email appropriately:
+Functional testing for mailers involves more than just checking that the email body, recipients, and so forth are correct. In functional mail tests you call the mail deliver methods and check that the appropriate emails have been appended to the delivery list. It is fairly safe to assume that the deliver methods themselves do their job. You are probably more interested in whether your own business logic is sending emails when you expect them to go out. For example, you can check that the invite friend operation is sending an email appropriately:
```ruby
require 'test_helper'
-class UserControllerTest < ActionController::TestCase
+class UsersControllerTest < ActionDispatch::IntegrationTest
test "invite friend" do
assert_difference 'ActionMailer::Base.deliveries.size', +1 do
- post :invite_friend, params: { email: 'friend@example.com' }
+ post invite_friend_url, params: { email: 'friend@example.com' }
end
invite_email = ActionMailer::Base.deliveries.last
assert_equal "You have been invited by me@example.com", invite_email.subject
assert_equal 'friend@example.com', invite_email.to[0]
- assert_match(/Hi friend@example.com/, invite_email.body.to_s)
+ assert_match(/Hi friend@example\.com/, invite_email.body.to_s)
end
end
```
@@ -1172,7 +1618,7 @@ Testing Jobs
------------
Since your custom jobs can be queued at different levels inside your application,
-you'll need to test both jobs themselves (their behavior when they get enqueued)
+you'll need to test both the jobs themselves (their behavior when they get enqueued)
and that other entities correctly enqueue them.
### A Basic Test Case
@@ -1191,7 +1637,7 @@ class BillingJobTest < ActiveJob::TestCase
end
```
-This test is pretty simple and only asserts that the job get the work done
+This test is pretty simple and only asserts that the job got the work done
as expected.
By default, `ActiveJob::TestCase` will set the queue adapter to `:test` so that
@@ -1220,15 +1666,25 @@ class ProductTest < ActiveJob::TestCase
end
```
-Other Testing Approaches
-------------------------
+Additional Testing Resources
+----------------------------
+
+### Testing Time-Dependent Code
-The built-in `minitest` based testing is not the only way to test Rails applications. Rails developers have come up with a wide variety of other approaches and aids for testing, including:
+Rails provides built-in helper methods that enable you to assert that your time-sensitive code works as expected.
+
+Here is an example using the [`travel_to`](http://api.rubyonrails.org/classes/ActiveSupport/Testing/TimeHelpers.html#method-i-travel_to) helper:
+
+```ruby
+# Lets say that a user is eligible for gifting a month after they register.
+user = User.create(name: 'Gaurish', activation_date: Date.new(2004, 10, 24))
+assert_not user.applicable_for_gifting?
+travel_to Date.new(2004, 11, 24) do
+ assert_equal Date.new(2004, 10, 24), user.activation_date # inside the `travel_to` block `Date.current` is mocked
+ assert user.applicable_for_gifting?
+end
+assert_equal Date.new(2004, 10, 24), user.activation_date # The change was visible only inside the `travel_to` block.
+```
-* [NullDB](http://avdi.org/projects/nulldb/), a way to speed up testing by avoiding database use.
-* [Factory Girl](https://github.com/thoughtbot/factory_girl/tree/master), a replacement for fixtures.
-* [Fixture Builder](https://github.com/rdy/fixture_builder), a tool that compiles Ruby factories into fixtures before a test run.
-* [MiniTest::Spec Rails](https://github.com/metaskills/minitest-spec-rails), use the MiniTest::Spec DSL within your rails tests.
-* [Shoulda](http://www.thoughtbot.com/projects/shoulda), an extension to `test/unit` with additional helpers, macros, and assertions.
-* [RSpec](http://relishapp.com/rspec), a behavior-driven development framework
-* [Capybara](http://jnicklas.github.com/capybara/), Acceptance test framework for web applications
+Please see [`ActiveSupport::Testing::TimeHelpers` API Documentation](http://api.rubyonrails.org/classes/ActiveSupport/Testing/TimeHelpers.html)
+for in-depth information about the available time helpers.
diff --git a/guides/source/threading_and_code_execution.md b/guides/source/threading_and_code_execution.md
new file mode 100644
index 0000000000..d3a81fe6a8
--- /dev/null
+++ b/guides/source/threading_and_code_execution.md
@@ -0,0 +1,324 @@
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
+
+Threading and Code Execution in Rails
+=====================================
+
+After reading this guide, you will know:
+
+* What code Rails will automatically execute concurrently
+* How to integrate manual concurrency with Rails internals
+* How to wrap all application code
+* How to affect application reloading
+
+--------------------------------------------------------------------------------
+
+Automatic Concurrency
+---------------------
+
+Rails automatically allows various operations to be performed at the same time.
+
+When using a threaded web server, such as the default Puma, multiple HTTP
+requests will be served simultaneously, with each request provided its own
+controller instance.
+
+Threaded Active Job adapters, including the built-in Async, will likewise
+execute several jobs at the same time. Action Cable channels are managed this
+way too.
+
+These mechanisms all involve multiple threads, each managing work for a unique
+instance of some object (controller, job, channel), while sharing the global
+process space (such as classes and their configurations, and global variables).
+As long as your code doesn't modify any of those shared things, it can mostly
+ignore that other threads exist.
+
+The rest of this guide describes the mechanisms Rails uses to make it "mostly
+ignorable", and how extensions and applications with special needs can use them.
+
+Executor
+--------
+
+The Rails Executor separates application code from framework code: any time the
+framework invokes code you've written in your application, it will be wrapped by
+the Executor.
+
+The Executor consists of two callbacks: `to_run` and `to_complete`. The Run
+callback is called before the application code, and the Complete callback is
+called after.
+
+### Default callbacks
+
+In a default Rails application, the Executor callbacks are used to:
+
+* track which threads are in safe positions for autoloading and reloading
+* enable and disable the Active Record query cache
+* return acquired Active Record connections to the pool
+* constrain internal cache lifetimes
+
+Prior to Rails 5.0, some of these were handled by separate Rack middleware
+classes (such as `ActiveRecord::ConnectionAdapters::ConnectionManagement`), or
+directly wrapping code with methods like
+`ActiveRecord::Base.connection_pool.with_connection`. The Executor replaces
+these with a single more abstract interface.
+
+### Wrapping application code
+
+If you're writing a library or component that will invoke application code, you
+should wrap it with a call to the executor:
+
+```ruby
+Rails.application.executor.wrap do
+ # call application code here
+end
+```
+
+TIP: If you repeatedly invoke application code from a long-running process, you
+may want to wrap using the Reloader instead.
+
+Each thread should be wrapped before it runs application code, so if your
+application manually delegates work to other threads, such as via `Thread.new`
+or Concurrent Ruby features that use thread pools, you should immediately wrap
+the block:
+
+```ruby
+Thread.new do
+ Rails.application.executor.wrap do
+ # your code here
+ end
+end
+```
+
+NOTE: Concurrent Ruby uses a `ThreadPoolExecutor`, which it sometimes configures
+with an `executor` option. Despite the name, it is unrelated.
+
+The Executor is safely re-entrant; if it is already active on the current
+thread, `wrap` is a no-op.
+
+If it's impractical to wrap the application code in a block (for
+example, the Rack API makes this problematic), you can also use the `run!` /
+`complete!` pair:
+
+```ruby
+Thread.new do
+ execution_context = Rails.application.executor.run!
+ # your code here
+ensure
+ execution_context.complete! if execution_context
+end
+```
+
+### Concurrency
+
+The Executor will put the current thread into `running` mode in the Load
+Interlock. This operation will block temporarily if another thread is currently
+either autoloading a constant or unloading/reloading the application.
+
+Reloader
+--------
+
+Like the Executor, the Reloader also wraps application code. If the Executor is
+not already active on the current thread, the Reloader will invoke it for you,
+so you only need to call one. This also guarantees that everything the Reloader
+does, including all its callback invocations, occurs wrapped inside the
+Executor.
+
+```ruby
+Rails.application.reloader.wrap do
+ # call application code here
+end
+```
+
+The Reloader is only suitable where a long-running framework-level process
+repeatedly calls into application code, such as for a web server or job queue.
+Rails automatically wraps web requests and Active Job workers, so you'll rarely
+need to invoke the Reloader for yourself. Always consider whether the Executor
+is a better fit for your use case.
+
+### Callbacks
+
+Before entering the wrapped block, the Reloader will check whether the running
+application needs to be reloaded -- for example, because a model's source file has
+been modified. If it determines a reload is required, it will wait until it's
+safe, and then do so, before continuing. When the application is configured to
+always reload regardless of whether any changes are detected, the reload is
+instead performed at the end of the block.
+
+The Reloader also provides `to_run` and `to_complete` callbacks; they are
+invoked at the same points as those of the Executor, but only when the current
+execution has initiated an application reload. When no reload is deemed
+necessary, the Reloader will invoke the wrapped block with no other callbacks.
+
+### Class Unload
+
+The most significant part of the reloading process is the Class Unload, where
+all autoloaded classes are removed, ready to be loaded again. This will occur
+immediately before either the Run or Complete callback, depending on the
+`reload_classes_only_on_change` setting.
+
+Often, additional reloading actions need to be performed either just before or
+just after the Class Unload, so the Reloader also provides `before_class_unload`
+and `after_class_unload` callbacks.
+
+### Concurrency
+
+Only long-running "top level" processes should invoke the Reloader, because if
+it determines a reload is needed, it will block until all other threads have
+completed any Executor invocations.
+
+If this were to occur in a "child" thread, with a waiting parent inside the
+Executor, it would cause an unavoidable deadlock: the reload must occur before
+the child thread is executed, but it cannot be safely performed while the parent
+thread is mid-execution. Child threads should use the Executor instead.
+
+Framework Behavior
+------------------
+
+The Rails framework components use these tools to manage their own concurrency
+needs too.
+
+`ActionDispatch::Executor` and `ActionDispatch::Reloader` are Rack middlewares
+that wraps the request with a supplied Executor or Reloader, respectively. They
+are automatically included in the default application stack. The Reloader will
+ensure any arriving HTTP request is served with a freshly-loaded copy of the
+application if any code changes have occurred.
+
+Active Job also wraps its job executions with the Reloader, loading the latest
+code to execute each job as it comes off the queue.
+
+Action Cable uses the Executor instead: because a Cable connection is linked to
+a specific instance of a class, it's not possible to reload for every arriving
+websocket message. Only the message handler is wrapped, though; a long-running
+Cable connection does not prevent a reload that's triggered by a new incoming
+request or job. Instead, Action Cable uses the Reloader's `before_class_unload`
+callback to disconnect all its connections. When the client automatically
+reconnects, it will be speaking to the new version of the code.
+
+The above are the entry points to the framework, so they are responsible for
+ensuring their respective threads are protected, and deciding whether a reload
+is necessary. Other components only need to use the Executor when they spawn
+additional threads.
+
+### Configuration
+
+The Reloader only checks for file changes when `cache_classes` is false and
+`reload_classes_only_on_change` is true (which is the default in the
+`development` environment).
+
+When `cache_classes` is true (in `production`, by default), the Reloader is only
+a pass-through to the Executor.
+
+The Executor always has important work to do, like database connection
+management. When `cache_classes` and `eager_load` are both true (`production`),
+no autoloading or class reloading will occur, so it does not need the Load
+Interlock. If either of those are false (`development`), then the Executor will
+use the Load Interlock to ensure constants are only loaded when it is safe.
+
+Load Interlock
+--------------
+
+The Load Interlock allows autoloading and reloading to be enabled in a
+multi-threaded runtime environment.
+
+When one thread is performing an autoload by evaluating the class definition
+from the appropriate file, it is important no other thread encounters a
+reference to the partially-defined constant.
+
+Similarly, it is only safe to perform an unload/reload when no application code
+is in mid-execution: after the reload, the `User` constant, for example, may
+point to a different class. Without this rule, a poorly-timed reload would mean
+`User.new.class == User`, or even `User == User`, could be false.
+
+Both of these constraints are addressed by the Load Interlock. It keeps track of
+which threads are currently running application code, loading a class, or
+unloading autoloaded constants.
+
+Only one thread may load or unload at a time, and to do either, it must wait
+until no other threads are running application code. If a thread is waiting to
+perform a load, it doesn't prevent other threads from loading (in fact, they'll
+cooperate, and each perform their queued load in turn, before all resuming
+running together).
+
+### `permit_concurrent_loads`
+
+The Executor automatically acquires a `running` lock for the duration of its
+block, and autoload knows when to upgrade to a `load` lock, and switch back to
+`running` again afterwards.
+
+Other blocking operations performed inside the Executor block (which includes
+all application code), however, can needlessly retain the `running` lock. If
+another thread encounters a constant it must autoload, this can cause a
+deadlock.
+
+For example, assuming `User` is not yet loaded, the following will deadlock:
+
+```ruby
+Rails.application.executor.wrap do
+ th = Thread.new do
+ Rails.application.executor.wrap do
+ User # inner thread waits here; it cannot load
+ # User while another thread is running
+ end
+ end
+
+ th.join # outer thread waits here, holding 'running' lock
+end
+```
+
+To prevent this deadlock, the outer thread can `permit_concurrent_loads`. By
+calling this method, the thread guarantees it will not dereference any
+possibly-autoloaded constant inside the supplied block. The safest way to meet
+that promise is to put it as close as possible to the blocking call:
+
+```ruby
+Rails.application.executor.wrap do
+ th = Thread.new do
+ Rails.application.executor.wrap do
+ User # inner thread can acquire the 'load' lock,
+ # load User, and continue
+ end
+ end
+
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
+ th.join # outer thread waits here, but has no lock
+ end
+end
+```
+
+Another example, using Concurrent Ruby:
+
+```ruby
+Rails.application.executor.wrap do
+ futures = 3.times.collect do |i|
+ Concurrent::Future.execute do
+ Rails.application.executor.wrap do
+ # do work here
+ end
+ end
+ end
+
+ values = ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
+ futures.collect(&:value)
+ end
+end
+```
+
+
+### ActionDispatch::DebugLocks
+
+If your application is deadlocking and you think the Load Interlock may be
+involved, you can temporarily add the ActionDispatch::DebugLocks middleware to
+`config/application.rb`:
+
+```ruby
+config.middleware.insert_before Rack::Sendfile,
+ ActionDispatch::DebugLocks
+```
+
+If you then restart the application and re-trigger the deadlock condition,
+`/rails/locks` will show a summary of all threads currently known to the
+interlock, which lock level they are holding or awaiting, and their current
+backtrace.
+
+Generally a deadlock will be caused by the interlock conflicting with some other
+external lock or blocking I/O call. Once you find it, you can wrap it with
+`permit_concurrent_loads`.
+
diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md
index 17309d4b47..a0553c1ccc 100644
--- a/guides/source/upgrading_ruby_on_rails.md
+++ b/guides/source/upgrading_ruby_on_rails.md
@@ -1,7 +1,7 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
-A Guide for Upgrading Ruby on Rails
-===================================
+Upgrading Ruby on Rails
+=======================
This guide provides steps to be followed when you upgrade your applications to a newer version of Ruby on Rails. These steps are also available in individual release guides.
@@ -16,26 +16,42 @@ Before attempting to upgrade an existing application, you should be sure you hav
The best way to be sure that your application still works after upgrading is to have good test coverage before you start the process. If you don't have automated tests that exercise the bulk of your application, you'll need to spend time manually exercising all the parts that have changed. In the case of a Rails upgrade, that will mean every single piece of functionality in the application. Do yourself a favor and make sure your test coverage is good _before_ you start an upgrade.
+### The Upgrade Process
+
+When changing Rails versions, it's best to move slowly, one minor version at a time, in order to make good use of the deprecation warnings. Rails version numbers are in the form Major.Minor.Patch. Major and Minor versions are allowed to make changes to the public API, so this may cause errors in your application. Patch versions only include bug fixes, and don't change any public API.
+
+The process should go as follows:
+
+1. Write tests and make sure they pass.
+2. Move to the latest patch version after your current version.
+3. Fix tests and deprecated features.
+4. Move to the latest patch version of the next minor version.
+
+Repeat this process until you reach your target Rails version. Each time you move versions, you will need to change the Rails version number in the `Gemfile` (and possibly other gem versions) and run `bundle update`. Then run the Update task mentioned below to update configuration files, then run your tests.
+
+You can find a list of all released Rails versions [here](https://rubygems.org/gems/rails/versions).
+
### Ruby Versions
Rails generally stays close to the latest released Ruby version when it's released:
+* Rails 6 requires Ruby 2.4.1 or newer.
* Rails 5 requires Ruby 2.2.2 or newer.
* Rails 4 prefers Ruby 2.0 and requires 1.9.3 or newer.
* Rails 3.2.x is the last branch to support Ruby 1.8.7.
* Rails 3 and above require Ruby 1.8.7 or higher. Support for all of the previous Ruby versions has been dropped officially. You should upgrade as early as possible.
-TIP: Ruby 1.8.7 p248 and p249 have marshaling bugs that crash Rails. Ruby Enterprise Edition has these fixed since the release of 1.8.7-2010.02. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults, so if you want to use 1.9.x, jump straight to 1.9.3 for smooth sailing.
+TIP: Ruby 1.8.7 p248 and p249 have marshalling bugs that crash Rails. Ruby Enterprise Edition has these fixed since the release of 1.8.7-2010.02. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults, so if you want to use 1.9.x, jump straight to 1.9.3 for smooth sailing.
-### The Rake Task
+### The Update Task
-Rails provides the `rails:update` rake task. After updating the Rails version
-in the Gemfile, run this rake task.
+Rails provides the `app:update` command (`rake rails:update` on 4.2 and earlier). After updating the Rails version
+in the `Gemfile`, run this command.
This will help you with the creation of new files and changes of old files in an
interactive session.
```bash
-$ rake rails:update
+$ rails app:update
identical config/boot.rb
exist config
conflict config/routes.rb
@@ -50,37 +66,155 @@ 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.
+### Configure Framework Defaults
+
+The new Rails version might have different configuration defaults than the previous version. However, after following the steps described above, your application would still run with configuration defaults from the *previous* Rails version. That's because the value for `config.load_defaults` in `config/application.rb` has not been changed yet.
+
+To allow you to upgrade to new defaults one by one, the update task has created a file `config/initializers/new_framework_defaults.rb`. Once your application is ready to run with new defaults, you can remove this file and flip the `config.load_defaults` value.
+
+
+Upgrading from Rails 5.2 to Rails 6.0
+-------------------------------------
+
+For more information on changes made to Rails 6.0 please see the [release notes](6_0_release_notes.html).
+
+### Force SSL
+
+The `force_ssl` method on controllers has been deprecated and will be removed in
+Rails 6.1. You are encouraged to enable `config.force_ssl` to enforce HTTPS
+connections throughout your application. If you need to exempt certain endpoints
+from redirection, you can use `config.ssl_options` to configure that behavior.
+
+### Purpose in signed or encrypted cookie is now embedded in the cookies values
+
+To improve security, Rails now embeds the purpose information in encrypted or signed cookies value.
+Rails can now thwart attacks that attempt to copy signed/encrypted value
+of a cookie and use it as the value of another cookie.
+
+This new embed information make those cookies incompatible with versions of Rails older than 6.0.
+
+If you require your cookies to be read by 5.2 and older, or you are still validating your 6.0 deploy and want
+to allow you to rollback set
+`Rails.application.config.action_dispatch.use_cookies_with_metadata` to `false`.
+
+Upgrading from Rails 5.1 to Rails 5.2
+-------------------------------------
+
+For more information on changes made to Rails 5.2 please see the [release notes](5_2_release_notes.html).
+
+### Bootsnap
+
+Rails 5.2 adds bootsnap gem in the [newly generated app's Gemfile](https://github.com/rails/rails/pull/29313).
+The `app:update` command sets it up in `boot.rb`. If you want to use it, then add it in the Gemfile,
+otherwise change the `boot.rb` to not use bootsnap.
+
+### Expiry in signed or encrypted cookie is now embedded in the cookies values
+
+To improve security, Rails now embeds the expiry information also in encrypted or signed cookies value.
+
+This new embed information make those cookies incompatible with versions of Rails older than 5.2.
+
+If you require your cookies to be read by 5.1 and older, or you are still validating your 5.2 deploy and want
+to allow you to rollback set
+`Rails.application.config.action_dispatch.use_authenticated_cookie_encryption` to `false`.
+
+Upgrading from Rails 5.0 to Rails 5.1
+-------------------------------------
+
+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 top-level `HashWithIndifferentAccess` class, you
+should slowly move your code to instead use `ActiveSupport::HashWithIndifferentAccess`.
+
+It is only soft-deprecated, which means that your code will not break at the
+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.
+
+### `application.secrets` now loaded with all keys as symbols
+
+If your application stores nested configuration in `config/secrets.yml`, all keys
+are now loaded as symbols, so access using strings should be changed.
+
+From:
+
+```ruby
+Rails.application.secrets[:smtp_settings]["address"]
+```
+
+To:
+
+```ruby
+Rails.application.secrets[:smtp_settings][:address]
+```
+
Upgrading from Rails 4.2 to Rails 5.0
-------------------------------------
-### Halting callback chains by returning `false`
+For more information on changes made to Rails 5.0 please see the [release notes](5_0_release_notes.html).
+
+### Ruby 2.2.2+ required
+
+From Ruby on Rails 5.0 onwards, Ruby 2.2.2+ is the only supported Ruby version.
+Make sure you are on Ruby 2.2.2 version or greater, before you proceed.
+
+### Active Record Models Now Inherit from ApplicationRecord by Default
+
+In Rails 4.2, an Active Record model inherits from `ActiveRecord::Base`. In Rails 5.0,
+all models inherit from `ApplicationRecord`.
+
+`ApplicationRecord` is a new superclass for all app models, analogous to app
+controllers subclassing `ApplicationController` instead of
+`ActionController::Base`. This gives apps a single spot to configure app-wide
+model behavior.
+
+When upgrading from Rails 4.2 to Rails 5.0, you need to create an
+`application_record.rb` file in `app/models/` and add the following content:
-In Rails 4.2, when a 'before' callback returns `false` in ActiveRecord,
-ActiveModel and ActiveModel::Validations, then the entire callback chain
-is halted. In other words, successive 'before' callbacks are not executed,
-and neither is the action wrapped in callbacks.
+```
+class ApplicationRecord < ActiveRecord::Base
+ self.abstract_class = true
+end
+```
+
+Then make sure that all your models inherit from it.
+
+### Halting Callback Chains via `throw(:abort)`
-In Rails 5.0, returning `false` in a callback will not have this side effect
-of halting the callback chain. Instead, callback chains must be explicitly
-halted by calling `throw(:abort)`.
+In Rails 4.2, when a 'before' callback returns `false` in Active Record
+and Active Model, then the entire callback chain is halted. In other words,
+successive 'before' callbacks are not executed, and neither is the action wrapped
+in callbacks.
-When you upgrade from Rails 4.2 to Rails 5.0, returning `false` in a callback
-will still halt the callback chain, but you will receive a deprecation warning
-about this upcoming change.
+In Rails 5.0, returning `false` in an Active Record or Active Model callback
+will not have this side effect of halting the callback chain. Instead, callback
+chains must be explicitly halted by calling `throw(:abort)`.
+
+When you upgrade from Rails 4.2 to Rails 5.0, returning `false` in those kind of
+callbacks will still halt the callback chain, but you will receive a deprecation
+warning about this upcoming change.
When you are ready, you can opt into the new behavior and remove the deprecation
warning by adding the following configuration to your `config/application.rb`:
- config.active_support.halt_callback_chains_on_return_false = false
+ ActiveSupport.halt_callback_chains_on_return_false = false
+
+Note that this option will not affect Active Support callbacks since they never
+halted the chain when any value was returned.
See [#17227](https://github.com/rails/rails/pull/17227) for more details.
-### ActiveJob jobs now inherent from ApplicationJob by default
+### ActiveJob Now Inherits from ApplicationJob by Default
-In Rails 4.2 an ActiveJob inherits from `ActiveJob::Base`. In Rails 5.0 this
+In Rails 4.2, an Active Job inherits from `ActiveJob::Base`. In Rails 5.0, this
behavior has changed to now inherit from `ApplicationJob`.
-When upgrading from Rails 4.2 to Rails 5.0 you need to create an
+When upgrading from Rails 4.2 to Rails 5.0, you need to create an
`application_job.rb` file in `app/jobs/` and add the following content:
```
@@ -92,16 +226,251 @@ Then make sure that all your job classes inherit from it.
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`.
+
+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
+default.
+
+Eager loading the application is part of the boot process, so top-level
+constants are fine and are still autoloaded, no need to require their files.
+
+Constants in deeper places only executed at runtime, like regular method bodies,
+are also fine because the file defining them will have been eager loaded while booting.
+
+For the vast majority of applications this change needs no action. But in the
+very rare event that your application needs autoloading while running in
+production mode, set `Rails.application.config.enable_dependency_loading` to
+true.
+
+### XML Serialization
+
+`ActiveModel::Serializers::Xml` has been extracted from Rails to the `activemodel-serializers-xml`
+gem. To continue using XML serialization in your application, add `gem 'activemodel-serializers-xml'`
+to your `Gemfile`.
+
+### Removed Support for Legacy `mysql` Database Adapter
+
+Rails 5 removes support for the legacy `mysql` database adapter. Most users should be able to
+use `mysql2` instead. It will be converted to a separate gem when we find someone to maintain
+it.
+
+### Removed Support for Debugger
+
+`debugger` is not supported by Ruby 2.2 which is required by Rails 5. Use `byebug` instead.
+
+### Use `rails` for running tasks and tests
+
+Rails 5 adds the ability to run tasks and tests through `bin/rails` instead of rake. Generally
+these changes are in parallel with rake, but some were ported over altogether. As the `rails`
+command already looks for and runs `bin/rails`, we recommend you to use the shorter `rails`
+over `bin/rails.
+
+To use the new test runner simply type `rails test`.
+
+`rake dev:cache` is now `rails dev:cache`.
+
+Run `rails` inside your application's directory to see the list of commands available.
+
+### `ActionController::Parameters` No Longer Inherits from `HashWithIndifferentAccess`
+
+Calling `params` in your application will now return an object instead of a hash. If your
+parameters are already permitted, then you will not need to make any changes. If you are using `map`
+and other methods that depend on being able to read the hash regardless of `permitted?` you will
+need to upgrade your application to first permit and then convert to a hash.
+
+ params.permit([:proceed_to, :return_to]).to_h
+
+### `protect_from_forgery` Now Defaults to `prepend: false`
+
+`protect_from_forgery` defaults to `prepend: false` which means that it will be inserted into
+the callback chain at the point in which you call it in your application. If you want
+`protect_from_forgery` to always run first, then you should change your application to use
+`protect_from_forgery prepend: true`.
+
+### Default Template Handler is Now RAW
+
+Files without a template handler in their extension will be rendered using the raw handler.
+Previously Rails would render files using the ERB template handler.
+
+If you do not want your file to be handled via the raw handler, you should add an extension
+to your file that can be parsed by the appropriate template handler.
+
+### Added Wildcard Matching for Template Dependencies
+
+You can now use wildcard matching for your template dependencies. For example, if you were
+defining your templates as such:
+
+```erb
+<% # Template Dependency: recordings/threads/events/subscribers_changed %>
+<% # Template Dependency: recordings/threads/events/completed %>
+<% # Template Dependency: recordings/threads/events/uncompleted %>
+```
+
+You can now just call the dependency once with a wildcard.
+
+```erb
+<% # Template Dependency: recordings/threads/events/* %>
+```
+
+### `ActionView::Helpers::RecordTagHelper` moved to external gem (record_tag_helper)
+
+`content_tag_for` and `div_for` have been removed in favor of just using `content_tag`. To continue using the older methods, add the `record_tag_helper` gem to your `Gemfile`:
+
+```ruby
+gem 'record_tag_helper', '~> 1.0'
+```
+
+See [#18411](https://github.com/rails/rails/pull/18411) for more details.
+
+### Removed Support for `protected_attributes` Gem
+
+The `protected_attributes` gem is no longer supported in Rails 5.
+
+### Removed support for `activerecord-deprecated_finders` gem
+
+The `activerecord-deprecated_finders` gem is no longer supported in Rails 5.
+
+### `ActiveSupport::TestCase` Default Test Order is Now Random
+
+When tests are run in your application, the default order is now `:random`
+instead of `:sorted`. Use the following config option to set it back to `:sorted`.
+
+```ruby
+# config/environments/test.rb
+Rails.application.configure do
+ config.active_support.test_order = :sorted
+end
+```
+
+### `ActionController::Live` became a `Concern`
+
+If you include `ActionController::Live` in another module that is included in your controller, then you
+should also extend the module with `ActiveSupport::Concern`. Alternatively, you can use the `self.included` hook
+to include `ActionController::Live` directly to the controller once the `StreamingSupport` is included.
+
+This means that if your application used to have its own streaming module, the following code
+would break in production mode:
+
+```ruby
+# This is a work-around for streamed controllers performing authentication with Warden/Devise.
+# See https://github.com/plataformatec/devise/issues/2332
+# Authenticating in the router is another solution as suggested in that issue
+class StreamingSupport
+ include ActionController::Live # this won't work in production for Rails 5
+ # extend ActiveSupport::Concern # unless you uncomment this line.
+
+ def process(name)
+ super(name)
+ rescue ArgumentError => e
+ if e.message == 'uncaught throw :warden'
+ throw :warden
+ else
+ raise e
+ end
+ end
+end
+```
+
+### New Framework Defaults
+
+#### Active Record `belongs_to` Required by Default Option
+
+`belongs_to` will now trigger a validation error by default if the association is not present.
+
+This can be turned off per-association with `optional: true`.
+
+This default will be automatically configured in new applications. If existing application
+want to add this feature it will need to be turned on in an initializer.
+
+ config.active_record.belongs_to_required_by_default = true
+
+#### Per-form CSRF Tokens
+
+Rails 5 now supports per-form CSRF tokens to mitigate against code-injection attacks with forms
+created by JavaScript. With this option turned on, forms in your application will each have their
+own CSRF token that is specified to the action and method for that form.
+
+ config.action_controller.per_form_csrf_tokens = true
+
+#### Forgery Protection with Origin Check
+
+You can now configure your application to check if the HTTP `Origin` header should be checked
+against the site's origin as an additional CSRF defense. Set the following in your config to
+true:
+
+ config.action_controller.forgery_protection_origin_check = true
+
+#### Allow Configuration of Action Mailer Queue Name
+
+The default mailer queue name is `mailers`. This configuration option allows you to globally change
+the queue name. Set the following in your config:
+
+ config.action_mailer.deliver_later_queue_name = :new_queue_name
+
+#### Support Fragment Caching in Action Mailer Views
+
+Set `config.action_mailer.perform_caching` in your config to determine whether your Action Mailer views
+should support caching.
+
+ config.action_mailer.perform_caching = true
+
+#### Configure the Output of `db:structure:dump`
+
+If you're using `schema_search_path` or other PostgreSQL extensions, you can control how the schema is
+dumped. Set to `:all` to generate all dumps, or to `:schema_search_path` to generate from schema search path.
+
+ config.active_record.dump_schemas = :all
+
+#### Configure SSL Options to Enable HSTS with Subdomains
+
+Set the following in your config to enable HSTS when using subdomains:
+
+ config.ssl_options = { hsts: { subdomains: true } }
+
+#### Preserve Timezone of the Receiver
+
+When using Ruby 2.4, you can preserve the timezone of the receiver when calling `to_time`.
+
+ ActiveSupport.to_time_preserves_timezone = false
+
+### Changes with JSON/JSONB serialization
+
+In Rails 5.0, how JSON/JSONB attributes are serialized and deserialized changed. Now, if
+you set a column equal to a `String`, Active Record will no longer turn that string
+into a `Hash`, and will instead only return the string. This is not limited to code
+interacting with models, but also affects `:default` column settings in `db/schema.rb`.
+It is recommended that you do not set columns equal to a `String`, but pass a `Hash`
+instead, which will be converted to and from a JSON string automatically.
+
Upgrading from Rails 4.1 to Rails 4.2
-------------------------------------
### Web Console
-First, add `gem 'web-console', '~> 2.0'` to the `:development` group in your Gemfile and run `bundle install` (it won't have been included when you upgraded Rails). Once it's been installed, you can simply drop a reference to the console helper (i.e., `<%= console %>`) into any view you want to enable it for. A console will also be provided on any error page you view in your development environment.
+First, add `gem 'web-console', '~> 2.0'` to the `:development` group in your `Gemfile` and run `bundle install` (it won't have been included when you upgraded Rails). Once it's been installed, you can simply drop a reference to the console helper (i.e., `<%= console %>`) into any view you want to enable it for. A console will also be provided on any error page you view in your development environment.
### Responders
-`respond_with` and the class-level `respond_to` methods have been extracted to the `responders` gem. To use them, simply add `gem 'responders', '~> 2.0'` to your Gemfile. Calls to `respond_with` and `respond_to` (again, at the class level) will no longer work without having included the `responders` gem in your dependencies:
+`respond_with` and the class-level `respond_to` methods have been extracted to the `responders` gem. To use them, simply add `gem 'responders', '~> 2.0'` to your `Gemfile`. Calls to `respond_with` and `respond_to` (again, at the class level) will no longer work without having included the `responders` gem in your dependencies:
```ruby
# app/controllers/users_controller.rb
@@ -142,7 +511,7 @@ the logs. In the next version, these errors will no longer be suppressed.
Instead, the errors will propagate normally just like in other Active
Record callbacks.
-When you define a `after_rollback` or `after_commit` callback, you
+When you define an `after_rollback` or `after_commit` callback, you
will receive a deprecation warning about this upcoming change. When
you are ready, you can opt into the new behavior and remove the
deprecation warning by adding following configuration to your
@@ -245,7 +614,7 @@ Read the [gem's readme](https://github.com/rails/rails-html-sanitizer) for more
The documentation for `PermitScrubber` and `TargetScrubber` explains how you
can gain complete control over when and how elements should be stripped.
-If your application needs to use the old sanitizer implementation, include `rails-deprecated_sanitizer` in your Gemfile:
+If your application needs to use the old sanitizer implementation, include `rails-deprecated_sanitizer` in your `Gemfile`:
```ruby
gem 'rails-deprecated_sanitizer'
@@ -253,7 +622,7 @@ gem 'rails-deprecated_sanitizer'
### Rails DOM Testing
-The [`TagAssertions` module](http://api.rubyonrails.org/classes/ActionDispatch/Assertions/TagAssertions.html) (containing methods such as `assert_tag`), [has been deprecated](https://github.com/rails/rails/blob/6061472b8c310158a2a2e8e9a6b81a1aef6b60fe/actionpack/lib/action_dispatch/testing/assertions/dom.rb) in favor of the `assert_select` methods from the `SelectorAssertions` module, which has been extracted into the [rails-dom-testing gem](https://github.com/rails/rails-dom-testing).
+The [`TagAssertions` module](http://api.rubyonrails.org/v4.1/classes/ActionDispatch/Assertions/TagAssertions.html) (containing methods such as `assert_tag`), [has been deprecated](https://github.com/rails/rails/blob/6061472b8c310158a2a2e8e9a6b81a1aef6b60fe/actionpack/lib/action_dispatch/testing/assertions/dom.rb) in favor of the `assert_select` methods from the `SelectorAssertions` module, which has been extracted into the [rails-dom-testing gem](https://github.com/rails/rails-dom-testing).
### Masked Authenticity Tokens
@@ -303,7 +672,7 @@ migration DSL counterpart.
The migration procedure is as follows:
-1. remove `gem "foreigner"` from the Gemfile.
+1. remove `gem "foreigner"` from the `Gemfile`.
2. run `bundle install`.
3. run `bin/rake db:schema:dump`.
4. make sure that `db/schema.rb` contains every foreign key definition with
@@ -314,11 +683,11 @@ Upgrading from Rails 4.0 to Rails 4.1
### CSRF protection from remote `<script>` tags
-Or, "whaaat my tests are failing!!!?"
+Or, "whaaat my tests are failing!!!?" or "my `<script>` widget is busted!!"
Cross-site request forgery (CSRF) protection now covers GET requests with
-JavaScript responses, too. This prevents a third-party site from referencing
-your JavaScript URL and attempting to run it to extract sensitive data.
+JavaScript responses, too. This prevents a third-party site from remotely
+referencing your JavaScript with a `<script>` tag to extract sensitive data.
This means that your functional and integration tests that use
@@ -334,8 +703,9 @@ xhr :get, :index, format: :js
to explicitly test an `XmlHttpRequest`.
-If you really mean to load JavaScript from remote `<script>` tags, skip CSRF
-protection on that action.
+NOTE: Your own `<script>` tags are treated as cross-origin and blocked by
+default, too. If you really mean to load JavaScript from `<script>` tags,
+you must now explicitly skip CSRF protection on those actions.
### Spring
@@ -452,9 +822,9 @@ 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
+1. Add 'multi_json' to your `Gemfile`. Note that this might cease to work in the future
2. Migrate away from MultiJSON by using `obj.to_json`, and `JSON.parse(str)` instead.
@@ -495,7 +865,7 @@ part of the rewrite, the following features have been removed from the encoder:
If your application depends on one of these features, you can get them back by
adding the [`activesupport-json_encoder`](https://github.com/rails/activesupport-json_encoder)
-gem to your Gemfile.
+gem to your `Gemfile`.
#### JSON representation of Time objects
@@ -784,14 +1154,14 @@ being used, you can update your form to use the `PUT` method instead:
<%= form_for [ :update_name, @user ], method: :put do |f| %>
```
-For more on PATCH and why this change was made, see [this post](http://weblog.rubyonrails.org/2012/2/26/edge-rails-patch-is-the-new-primary-http-method-for-updates/)
+For more on PATCH and why this change was made, see [this post](https://weblog.rubyonrails.org/2012/2/26/edge-rails-patch-is-the-new-primary-http-method-for-updates/)
on the Rails blog.
#### A note about media types
The errata for the `PATCH` verb [specifies that a 'diff' media type should be
used with `PATCH`](http://www.rfc-editor.org/errata_search.php?rfc=5789). One
-such format is [JSON Patch](http://tools.ietf.org/html/rfc6902). While Rails
+such format is [JSON Patch](https://tools.ietf.org/html/rfc6902). While Rails
does not support JSON Patch natively, it's easy enough to add support:
```
@@ -820,8 +1190,8 @@ full support for the last few changes in the specification.
### Gemfile
-Rails 4.0 removed the `assets` group from Gemfile. You'd need to remove that
-line from your Gemfile when upgrading. You should also update your application
+Rails 4.0 removed the `assets` group from `Gemfile`. You'd need to remove that
+line from your `Gemfile` when upgrading. You should also update your application
file (in `config/application.rb`):
```ruby
@@ -832,13 +1202,13 @@ Bundler.require(*Rails.groups)
### vendor/plugins
-Rails 4.0 no longer supports loading plugins from `vendor/plugins`. You must replace any plugins by extracting them to gems and adding them to your Gemfile. If you choose not to make them gems, you can move them into, say, `lib/my_plugin/*` and add an appropriate initializer in `config/initializers/my_plugin.rb`.
+Rails 4.0 no longer supports loading plugins from `vendor/plugins`. You must replace any plugins by extracting them to gems and adding them to your `Gemfile`. If you choose not to make them gems, you can move them into, say, `lib/my_plugin/*` and add an appropriate initializer in `config/initializers/my_plugin.rb`.
### Active Record
* Rails 4.0 has removed the identity map from Active Record, due to [some inconsistencies with associations](https://github.com/rails/rails/commit/302c912bf6bcd0fa200d964ec2dc4a44abe328a6). If you have manually enabled it in your application, you will have to remove the following config that has no effect anymore: `config.active_record.identity_map`.
-* The `delete` method in collection associations can now receive `Fixnum` or `String` arguments as record ids, besides records, pretty much like the `destroy` method does. Previously it raised `ActiveRecord::AssociationTypeMismatch` for such arguments. From Rails 4.0 on `delete` automatically tries to find the records matching the given ids before deleting them.
+* The `delete` method in collection associations can now receive `Integer` or `String` arguments as record ids, besides records, pretty much like the `destroy` method does. Previously it raised `ActiveRecord::AssociationTypeMismatch` for such arguments. From Rails 4.0 on `delete` automatically tries to find the records matching the given ids before deleting them.
* In Rails 4.0 when a column or a table is renamed the related indexes are also renamed. If you have migrations which rename the indexes, they are no longer needed.
@@ -895,11 +1265,11 @@ CatalogProduct < ActiveRecord::Base
end
```
-* Note that the the prefix takes scopes into account as well, so relations between `Catalog::Category` and `Catalog::Product` or `Catalog::Category` and `CatalogProduct` need to be updated similarly.
+* Note that the prefix takes scopes into account as well, so relations between `Catalog::Category` and `Catalog::Product` or `Catalog::Category` and `CatalogProduct` need to be updated similarly.
### Active Resource
-Rails 4.0 extracted Active Resource to its own gem. If you still need the feature you can add the [Active Resource gem](https://github.com/rails/activeresource) in your Gemfile.
+Rails 4.0 extracted Active Resource to its own gem. If you still need the feature you can add the [Active Resource gem](https://github.com/rails/activeresource) in your `Gemfile`.
### Active Model
@@ -936,11 +1306,11 @@ Please read [Pull Request #9978](https://github.com/rails/rails/pull/9978) for d
* Rails 4.0 has deprecated `ActionController::Base.page_cache_extension` option. Use `ActionController::Base.default_static_extension` instead.
-* Rails 4.0 has removed Action and Page caching from Action Pack. You will need to add the `actionpack-action_caching` gem in order to use `caches_action` and the `actionpack-page_caching` to use `caches_pages` in your controllers.
+* Rails 4.0 has removed Action and Page caching from Action Pack. You will need to add the `actionpack-action_caching` gem in order to use `caches_action` and the `actionpack-page_caching` to use `caches_page` in your controllers.
* Rails 4.0 has removed the XML parameters parser. You will need to add the `actionpack-xml_parser` gem if you require this feature.
-* Rails 4.0 changes the default `layout` lookup set using symbols or procs that return nil. To get the "no layout" behavior, return false instead of nil.
+* Rails 4.0 changes the default `layout` lookup set using symbols or procs that return nil. To get the "no layout" behavior, return false instead of nil.
* Rails 4.0 changes the default memcached client from `memcache-client` to `dalli`. To upgrade, simply add `gem 'dalli'` to your `Gemfile`.
@@ -995,7 +1365,7 @@ get 'こんにちは', controller: 'welcome', action: 'index'
get '/' => 'root#index'
```
-* Rails 4.0 has removed `ActionDispatch::BestStandardsSupport` middleware, `<!DOCTYPE html>` already triggers standards mode per http://msdn.microsoft.com/en-us/library/jj676915(v=vs.85).aspx and ChromeFrame header has been moved to `config.action_dispatch.default_headers`.
+* Rails 4.0 has removed `ActionDispatch::BestStandardsSupport` middleware, `<!DOCTYPE html>` already triggers standards mode per https://msdn.microsoft.com/en-us/library/jj676915(v=vs.85).aspx and ChromeFrame header has been moved to `config.action_dispatch.default_headers`.
Remember you must also remove any references to the middleware from your application code, for example:
@@ -1006,6 +1376,17 @@ config.middleware.insert_before(Rack::Lock, ActionDispatch::BestStandardsSupport
Also check your environment settings for `config.action_dispatch.best_standards_support` and remove it if present.
+* Rails 4.0 allows configuration of HTTP headers by setting `config.action_dispatch.default_headers`. The defaults are as follows:
+
+```ruby
+ config.action_dispatch.default_headers = {
+ 'X-Frame-Options' => 'SAMEORIGIN',
+ 'X-XSS-Protection' => '1; mode=block'
+ }
+```
+
+Please note that if your application is dependent on loading certain pages in a `<frame>` or `<iframe>`, then you may need to explicitly set `X-Frame-Options` to `ALLOW-FROM ...` or `ALLOWALL`.
+
* In Rails 4.0, precompiling assets no longer automatically copies non-JS/CSS assets from `vendor/assets` and `lib/assets`. Rails application and engine developers should put these assets in `app/assets` or configure `config.assets.precompile`.
* In Rails 4.0, `ActionController::UnknownFormat` is raised when the action doesn't handle the request format. By default, the exception is handled by responding with 406 Not Acceptable, but you can override that now. In Rails 3, 406 Not Acceptable was always returned. No overrides.
@@ -1027,6 +1408,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](https://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.
@@ -1086,7 +1471,7 @@ config.active_record.auto_explain_threshold_in_seconds = 0.5
### config/environments/test.rb
-The `mass_assignment_sanitizer` configuration setting should also be be added to `config/environments/test.rb`:
+The `mass_assignment_sanitizer` configuration setting should also be added to `config/environments/test.rb`:
```ruby
# Raise exception on mass assignment protection for Active Record models
@@ -1095,7 +1480,7 @@ config.active_record.mass_assignment_sanitizer = :strict
### vendor/plugins
-Rails 3.2 deprecates `vendor/plugins` and Rails 4.0 will remove them completely. While it's not strictly necessary as part of a Rails 3.2 upgrade, you can start replacing any plugins by extracting them to gems and adding them to your Gemfile. If you choose not to make them gems, you can move them into, say, `lib/my_plugin/*` and add an appropriate initializer in `config/initializers/my_plugin.rb`.
+Rails 3.2 deprecates `vendor/plugins` and Rails 4.0 will remove them completely. While it's not strictly necessary as part of a Rails 3.2 upgrade, you can start replacing any plugins by extracting them to gems and adding them to your `Gemfile`. If you choose not to make them gems, you can move them into, say, `lib/my_plugin/*` and add an appropriate initializer in `config/initializers/my_plugin.rb`.
### Active Record
@@ -1175,7 +1560,7 @@ config.assets.digest = true
# config.assets.manifest = YOUR_PATH
# Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added)
-# config.assets.precompile += %w( search.js )
+# config.assets.precompile += %w( admin.js admin.css )
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
# config.force_ssl = true
@@ -1187,8 +1572,10 @@ You can help test performance with these additions to your test environment:
```ruby
# Configure static asset server for tests with Cache-Control for performance
-config.serve_static_files = true
-config.static_cache_control = 'public, max-age=3600'
+config.public_file_server.enabled = true
+config.public_file_server.headers = {
+ 'Cache-Control' => 'public, max-age=3600'
+}
```
### config/initializers/wrap_parameters.rb
diff --git a/guides/source/working_with_javascript_in_rails.md b/guides/source/working_with_javascript_in_rails.md
index f3d3a83afc..c36b3faa6c 100644
--- a/guides/source/working_with_javascript_in_rails.md
+++ b/guides/source/working_with_javascript_in_rails.md
@@ -1,4 +1,4 @@
-**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
Working with JavaScript in Rails
================================
@@ -24,11 +24,11 @@ In order to understand Ajax, you must first understand what a web browser does
normally.
When you type `http://localhost:3000` into your browser's address bar and hit
-'Go,' the browser (your 'client') makes a request to the server. It parses the
+'Go', the browser (your 'client') makes a request to the server. It parses the
response, then fetches all associated assets, like JavaScript files,
stylesheets and images. It then assembles the page. If you click a link, it
does the same process: fetch the page, fetch the assets, put it all together,
-show you the results. This is called the 'request response cycle.'
+show you the results. This is called the 'request response cycle'.
JavaScript can also make requests to the server, and parse the response. It
also has the ability to update information on the page. Combining these two
@@ -57,7 +57,7 @@ will show you how Rails can help you write websites in this way, but it's
all built on top of this fairly simple technique.
Unobtrusive JavaScript
--------------------------------------
+----------------------
Rails uses a technique called "Unobtrusive JavaScript" to handle attaching
JavaScript to the DOM. This is generally considered to be a best-practice
@@ -81,7 +81,7 @@ Awkward, right? We could pull the function definition out of the click handler,
and turn it into CoffeeScript:
```coffeescript
-paintIt = (element, backgroundColor, textColor) ->
+@paintIt = (element, backgroundColor, textColor) ->
element.style.backgroundColor = backgroundColor
if textColor?
element.style.color = textColor
@@ -107,7 +107,7 @@ attribute to our link, and then bind a handler to the click event of every link
that has that attribute:
```coffeescript
-paintIt = (element, backgroundColor, textColor) ->
+@paintIt = (element, backgroundColor, textColor) ->
element.style.backgroundColor = backgroundColor
if textColor?
element.style.color = textColor
@@ -139,7 +139,9 @@ JavaScript) in this style, and you can expect that many libraries will also
follow this pattern.
Built-in Helpers
-----------------------
+----------------
+
+### Remote elements
Rails provides a bunch of view helper methods written in Ruby to assist you
in generating HTML. Sometimes, you want to add a little Ajax to those elements,
@@ -148,19 +150,23 @@ and Rails has got your back in those cases.
Because of Unobtrusive JavaScript, the Rails "Ajax helpers" are actually in two
parts: the JavaScript half and the Ruby half.
-[rails.js](https://github.com/rails/jquery-ujs/blob/master/src/rails.js)
+Unless you have disabled the Asset Pipeline,
+[rails-ujs](https://github.com/rails/rails/tree/master/actionview/app/assets/javascripts)
provides the JavaScript half, and the regular Ruby view helpers add appropriate
-tags to your DOM. The CoffeeScript in rails.js then listens for these
-attributes, and attaches appropriate handlers.
+tags to your DOM.
-### form_for
+You can read below about the different events that are fired dealing with
+remote elements inside your application.
-[`form_for`](http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_for)
-is a helper that assists with writing forms. `form_for` takes a `:remote`
-option. It works like this:
+#### form_with
+
+[`form_with`](http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_with)
+is a helper that assists with writing forms. By default, `form_with` assumes that
+your form will be using Ajax. You can opt out of this behavior by
+passing the `:local` option `form_with`.
```erb
-<%= form_for(@article, remote: true) do |f| %>
+<%= form_with(model: @article) do |f| %>
...
<% end %>
```
@@ -168,7 +174,7 @@ option. It works like this:
This will generate the following HTML:
```html
-<form accept-charset="UTF-8" action="/articles" class="new_article" data-remote="true" id="new_article" method="post">
+<form action="/articles" accept-charset="UTF-8" method="post" data-remote="true">
...
</form>
```
@@ -182,39 +188,21 @@ bind to the `ajax:success` event. On failure, use `ajax:error`. Check it out:
```coffeescript
$(document).ready ->
- $("#new_article").on("ajax:success", (e, data, status, xhr) ->
+ $("#new_article").on("ajax:success", (event) ->
+ [data, status, xhr] = event.detail
$("#new_article").append xhr.responseText
- ).on "ajax:error", (e, xhr, status, error) ->
+ ).on "ajax:error", (event) ->
$("#new_article").append "<p>ERROR</p>"
```
Obviously, you'll want to be a bit more sophisticated than that, but it's a
-start. You can see more about the events [in the jquery-ujs wiki](https://github.com/rails/jquery-ujs/wiki/ajax).
-
-### form_tag
-
-[`form_tag`](http://api.rubyonrails.org/classes/ActionView/Helpers/FormTagHelper.html#method-i-form_tag)
-is very similar to `form_for`. It has a `:remote` option that you can use like
-this:
-
-```erb
-<%= form_tag('/articles', remote: true) do %>
- ...
-<% end %>
-```
-
-This will generate the following HTML:
+start.
-```html
-<form accept-charset="UTF-8" action="/articles" data-remote="true" method="post">
- ...
-</form>
-```
+NOTE: As of Rails 5.1 and the new `rails-ujs`, the parameters `data, status, xhr`
+have been bundled into `event.detail`. For information about the previously used
+`jquery-ujs` in Rails 5 and earlier, read the [`jquery-ujs` wiki](https://github.com/rails/jquery-ujs/wiki/ajax).
-Everything else is the same as `form_for`. See its documentation for full
-details.
-
-### link_to
+#### link_to
[`link_to`](http://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-link_to)
is a helper that assists with generating links. It has a `:remote` option you
@@ -230,7 +218,7 @@ which generates
<a href="/articles/1" data-remote="true">an article</a>
```
-You can bind to the same Ajax events as `form_for`. Here's an example. Let's
+You can bind to the same Ajax events as `form_with`. Here's an example. Let's
assume that we have a list of articles that can be deleted with just one
click. We would generate some HTML like this:
@@ -242,11 +230,11 @@ and write some CoffeeScript like this:
```coffeescript
$ ->
- $("a[data-remote]").on "ajax:success", (e, data, status, xhr) ->
+ $("a[data-remote]").on "ajax:success", (event) ->
alert "The article was deleted."
```
-### button_to
+#### button_to
[`button_to`](http://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-button_to) is a helper that helps you create buttons. It has a `:remote` option that you can call like this:
@@ -258,11 +246,154 @@ this generates
```html
<form action="/articles/1" class="button_to" data-remote="true" method="post">
- <div><input type="submit" value="An article"></div>
+ <input type="submit" value="An article" />
</form>
```
-Since it's just a `<form>`, all of the information on `form_for` also applies.
+Since it's just a `<form>`, all of the information on `form_with` also applies.
+
+### Customize remote elements
+
+It is possible to customize the behavior of elements with a `data-remote`
+attribute without writing a line of JavaScript. You can specify extra `data-`
+attributes to accomplish this.
+
+#### `data-method`
+
+Activating hyperlinks always results in an HTTP GET request. However, if your
+application is [RESTful](https://en.wikipedia.org/wiki/Representational_State_Transfer),
+some links are in fact actions that change data on the server, and must be
+performed with non-GET requests. This attribute allows marking up such links
+with an explicit method such as "post", "put" or "delete".
+
+The way it works is that, when the link is activated, it constructs a hidden form
+in the document with the "action" attribute corresponding to "href" value of the
+link, and the method corresponding to `data-method` value, and submits that form.
+
+NOTE: Because submitting forms with HTTP methods other than GET and POST isn't
+widely supported across browsers, all other HTTP methods are actually sent over
+POST with the intended method indicated in the `_method` parameter. Rails
+automatically detects and compensates for this.
+
+#### `data-url` and `data-params`
+
+Certain elements of your page aren't actually referring to any URL, but you may want
+them to trigger Ajax calls. Specifying the `data-url` attribute along with
+the `data-remote` one will trigger an Ajax call to the given URL. You can also
+specify extra parameters through the `data-params` attribute.
+
+This can be useful to trigger an action on check-boxes for instance:
+
+```html
+<input type="checkbox" data-remote="true"
+ data-url="/update" data-params="id=10" data-method="put">
+```
+
+#### `data-type`
+
+It is also possible to define the Ajax `dataType` explicitly while performing
+requests for `data-remote` elements, by way of the `data-type` attribute.
+
+### Confirmations
+
+You can ask for an extra confirmation of the user by adding a `data-confirm`
+attribute on links and forms. The user will be presented a JavaScript `confirm()`
+dialog containing the attribute's text. If the user chooses to cancel, the action
+doesn't take place.
+
+Adding this attribute on links will trigger the dialog on click, and adding it
+on forms will trigger it on submit. For example:
+
+```erb
+<%= link_to "Dangerous zone", dangerous_zone_path,
+ data: { confirm: 'Are you sure?' } %>
+```
+
+This generates:
+
+```html
+<a href="..." data-confirm="Are you sure?">Dangerous zone</a>
+```
+
+The attribute is also allowed on form submit buttons. This allows you to customize
+the warning message depending on the button which was activated. In this case,
+you should **not** have `data-confirm` on the form itself.
+
+The default confirmation uses a JavaScript confirm dialog, but you can customize
+this by listening to the `confirm` event, which is fired just before the confirmation
+window appears to the user. To cancel this default confirmation, have the confirm
+handler to return `false`.
+
+### Automatic disabling
+
+It is also possible to automatically disable an input while the form is submitting
+by using the `data-disable-with` attribute. This is to prevent accidental
+double-clicks from the user, which could result in duplicate HTTP requests that
+the backend may not detect as such. The value of the attribute is the text that will
+become the new value of the button in its disabled state.
+
+This also works for links with `data-method` attribute.
+
+For example:
+
+```erb
+<%= form_with(model: @article.new) do |f| %>
+ <%= f.submit data: { "disable-with": "Saving..." } %>
+<%= end %>
+```
+
+This generates a form with:
+
+```html
+<input data-disable-with="Saving..." type="submit">
+```
+
+### Rails-ujs event handlers
+
+Rails 5.1 introduced rails-ujs and dropped jQuery as a dependency.
+As a result the Unobtrusive JavaScript (UJS) driver has been rewritten to operate without jQuery.
+These introductions cause small changes to `custom events` fired during the request:
+
+NOTE: Signature of calls to UJS's event handlers has changed.
+Unlike the version with jQuery, all custom events return only one parameter: `event`.
+In this parameter, there is an additional attribute `detail` which contains an array of extra parameters.
+
+| Event name | Extra parameters (event.detail) | Fired |
+|---------------------|---------------------------------|-------------------------------------------------------------|
+| `ajax:before` | | Before the whole ajax business. |
+| `ajax:beforeSend` | [xhr, options] | Before the request is sent. |
+| `ajax:send` | [xhr] | When the request is sent. |
+| `ajax:stopped` | | When the request is stopped. |
+| `ajax:success` | [response, status, xhr] | After completion, if the response was a success. |
+| `ajax:error` | [response, status, xhr] | After completion, if the response was an error. |
+| `ajax:complete` | [xhr, status] | After the request has been completed, no matter the outcome.|
+
+Example usage:
+
+```html
+document.body.addEventListener('ajax:success', function(event) {
+ var detail = event.detail;
+ var data = detail[0], status = detail[1], xhr = detail[2];
+})
+```
+
+NOTE: As of Rails 5.1 and the new `rails-ujs`, the parameters `data, status, xhr`
+have been bundled into `event.detail`. For information about the previously used
+`jquery-ujs` in Rails 5 and earlier, read the [`jquery-ujs` wiki](https://github.com/rails/jquery-ujs/wiki/ajax).
+
+### Stoppable events
+You can stop execution of the Ajax request by running `event.preventDefault()`
+from the handlers methods `ajax:before` or `ajax:beforeSend`.
+The `ajax:before` event can manipulate form data before serialization and the
+`ajax:beforeSend` event is useful for adding custom request headers.
+
+If you stop the `ajax:aborted:file` event, the default behavior of allowing the
+browser to submit the form via normal means (i.e. non-Ajax submission) will be
+canceled and the form will not be submitted at all. This is useful for
+implementing your own Ajax file upload workaround.
+
+Note, you should use `return false` to prevent event for `jquery-ujs` and
+`e.preventDefault()` for `rails-ujs`
Server-Side Concerns
--------------------
@@ -297,7 +428,7 @@ The index view (`app/views/users/index.html.erb`) contains:
<br>
-<%= form_for(@user, remote: true) do |f| %>
+<%= form_with(model: @user) do |f| %>
<%= f.label :name %><br>
<%= f.text_field :name %>
<%= f.submit %>
@@ -328,7 +459,7 @@ this:
respond_to do |format|
if @user.save
format.html { redirect_to @user, notice: 'User was successfully created.' }
- format.js {}
+ format.js
format.json { render json: @user, status: :created, location: @user }
else
format.html { render action: "new" }
@@ -338,7 +469,7 @@ this:
end
```
-Notice the format.js in the `respond_to` block; that allows the controller to
+Notice the `format.js` in the `respond_to` block: that allows the controller to
respond to your Ajax request. You then have a corresponding
`app/views/users/create.js.erb` view file that generates the actual JavaScript
code that will be sent and executed on the client side.
@@ -350,28 +481,24 @@ $("<%= escape_javascript(render @user) %>").appendTo("#users");
Turbolinks
----------
-Rails 4 ships with the [Turbolinks gem](https://github.com/rails/turbolinks).
-This gem uses Ajax to speed up page rendering in most applications.
+Rails ships with the [Turbolinks library](https://github.com/turbolinks/turbolinks),
+which uses Ajax to speed up page rendering in most applications.
### How Turbolinks Works
-Turbolinks attaches a click handler to all `<a>` on the page. If your browser
+Turbolinks attaches a click handler to all `<a>` tags on the page. If your browser
supports
-[PushState](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Manipulating_the_browser_history#The_pushState()_method),
+[PushState](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Manipulating_the_browser_history#The_pushState%28%29_method),
Turbolinks will make an Ajax request for the page, parse the response, and
replace the entire `<body>` of the page with the `<body>` of the response. It
will then use PushState to change the URL to the correct one, preserving
refresh semantics and giving you pretty URLs.
-The only thing you have to do to enable Turbolinks is have it in your Gemfile,
-and put `//= require turbolinks` in your CoffeeScript manifest, which is usually
-`app/assets/javascripts/application.js`.
-
-If you want to disable Turbolinks for certain links, add a `data-no-turbolink`
+If you want to disable Turbolinks for certain links, add a `data-turbolinks="false"`
attribute to the tag:
```html
-<a href="..." data-no-turbolink>No turbolinks here</a>.
+<a href="..." data-turbolinks="false">No turbolinks here</a>.
```
### Page Change Events
@@ -385,17 +512,17 @@ $(document).ready ->
```
However, because Turbolinks overrides the normal page loading process, the
-event that this relies on will not be fired. If you have code that looks like
+event that this relies upon will not be fired. If you have code that looks like
this, you must change your code to do this instead:
```coffeescript
-$(document).on "page:change", ->
+$(document).on "turbolinks:load", ->
alert "page has loaded!"
```
For more details, including other events you can bind to, check out [the
Turbolinks
-README](https://github.com/rails/turbolinks/blob/master/README.md).
+README](https://github.com/turbolinks/turbolinks/blob/master/README.md).
Other Resources
---------------
diff --git a/guides/w3c_validator.rb b/guides/w3c_validator.rb
index 71f044b9c4..f38b6c2639 100644
--- a/guides/w3c_validator.rb
+++ b/guides/w3c_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# ---------------------------------------------------------------------------
#
# This script validates the generated guides against the W3C Validator.
@@ -21,19 +23,19 @@
#
# Separate many using commas:
#
-# # validates only association_basics.html and migrations.html
-# rake guides:validate ONLY=assoc,migrations
+# # validates only association_basics.html and command_line.html
+# rake guides:validate ONLY=assoc,command
#
# ---------------------------------------------------------------------------
-require 'w3c_validators'
+require "w3c_validators"
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 = {}
@@ -45,11 +47,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
@@ -57,40 +59,39 @@ module RailsGuides
end
private
- def guides_to_validate
- guides = Dir["./output/*.html"]
- guides.delete("./output/layout.html")
- guides.delete("./output/_license.html")
- guides.delete("./output/_welcome.html")
- ENV.key?('ONLY') ? select_only(guides) : guides
- end
+ def guides_to_validate
+ guides = Dir["./output/*.html"]
+ guides.delete("./output/layout.html")
+ guides.delete("./output/_license.html")
+ guides.delete("./output/_welcome.html")
+ ENV.key?("ONLY") ? select_only(guides) : guides
+ end
- def select_only(guides)
- prefixes = ENV['ONLY'].split(",").map(&:strip)
- guides.select do |guide|
- prefixes.any? {|p| guide.start_with?("./output/#{p}")}
+ def select_only(guides)
+ prefixes = ENV["ONLY"].split(",").map(&:strip)
+ guides.select do |guide|
+ prefixes.any? { |p| guide.start_with?("./output/#{p}") }
+ end
end
- end
- def show_results(error_list)
- if error_list.size == 0
- puts "\n\nAll checked guides validate OK!"
- else
- error_summary = error_detail = ""
+ def show_results(error_list)
+ if error_list.size == 0
+ puts "\n\nAll checked guides validate OK!"
+ else
+ error_summary = error_detail = ""
- error_list.each_pair do |name, errors|
- error_summary += "\n #{name}"
- error_detail += "\n\n #{name} has #{errors.size} validation error(s):\n"
- errors.each do |error|
- error_detail += "\n "+error.to_s.delete("\n")
+ error_list.each_pair do |name, errors|
+ error_summary += "\n #{name}"
+ error_detail += "\n\n #{name} has #{errors.size} validation error(s):\n"
+ errors.each do |error|
+ error_detail += "\n " + error.to_s.delete("\n")
+ end
end
- end
- puts "\n\nThere are #{error_list.size} guides with validation errors:\n" + error_summary
- puts "\nHere are the detailed errors for each guide:" + error_detail
+ puts "\n\nThere are #{error_list.size} guides with validation errors:\n" + error_summary
+ puts "\nHere are the detailed errors for each guide:" + error_detail
+ end
end
- end
-
end
end
diff --git a/install.rb b/install.rb
deleted file mode 100644
index 933e5ee9a2..0000000000
--- a/install.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-version = ARGV.pop
-
-if version.nil?
- puts "Usage: ruby install.rb version"
- exit(64)
-end
-
-%w( activesupport activemodel activerecord actionpack actionview actionmailer railties activejob ).each do |framework|
- puts "Installing #{framework}..."
- `cd #{framework} && gem build #{framework}.gemspec && gem install #{framework}-#{version}.gem --no-document && rm #{framework}-#{version}.gem`
-end
-
-puts "Installing rails..."
-`gem build rails.gemspec`
-`gem install rails-#{version}.gem --no-document `
-`rm rails-#{version}.gem`
diff --git a/load_paths.rb b/load_paths.rb
deleted file mode 100644
index 0ad8fcfeda..0000000000
--- a/load_paths.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-# bust gem prelude
-require 'bundler'
-Bundler.setup
diff --git a/package.json b/package.json
new file mode 100644
index 0000000000..88029de141
--- /dev/null
+++ b/package.json
@@ -0,0 +1,14 @@
+{
+ "private": true,
+ "workspaces": [
+ "actioncable",
+ "activestorage",
+ "actionview",
+ "tmp/templates/app_template",
+ "railties/test/fixtures/tmp/bukkits/**/test/dummy",
+ "railties/test/fixtures/tmp/bukkits/**/spec/dummy"
+ ],
+ "dependencies": {
+ "webpack": "^4.17.1"
+ }
+}
diff --git a/rails.gemspec b/rails.gemspec
index 52112680b2..709ce642f3 100644
--- a/rails.gemspec
+++ b/rails.gemspec
@@ -1,32 +1,36 @@
-version = File.read(File.expand_path('../RAILS_VERSION', __FILE__)).strip
+# frozen_string_literal: true
+
+version = File.read(File.expand_path("RAILS_VERSION", __dir__)).strip
Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
- s.name = 'rails'
+ s.name = "rails"
s.version = version
- s.summary = 'Full-stack web application framework.'
- s.description = 'Ruby on Rails is a full-stack web framework optimized for programmer happiness and sustainable productivity. It encourages beautiful code by favoring convention over configuration.'
+ s.summary = "Full-stack web application framework."
+ s.description = "Ruby on Rails is a full-stack web framework optimized for programmer happiness and sustainable productivity. It encourages beautiful code by favoring convention over configuration."
- s.required_ruby_version = '>= 2.2.2'
- s.required_rubygems_version = '>= 1.8.11'
+ s.required_ruby_version = ">= 2.4.1"
+ s.required_rubygems_version = ">= 1.8.11"
- s.license = 'MIT'
+ s.license = "MIT"
- s.author = 'David Heinemeier Hansson'
- s.email = 'david@loudthinking.com'
- s.homepage = 'http://www.rubyonrails.org'
+ s.author = "David Heinemeier Hansson"
+ s.email = "david@loudthinking.com"
+ s.homepage = "http://rubyonrails.org"
- s.files = ['README.md']
+ s.files = ["README.md"]
- s.add_dependency 'activesupport', version
- s.add_dependency 'actionpack', version
- s.add_dependency 'actionview', version
- s.add_dependency 'activemodel', version
- s.add_dependency 'activerecord', version
- s.add_dependency 'actionmailer', version
- s.add_dependency 'activejob', version
- s.add_dependency 'railties', version
+ s.add_dependency "activesupport", version
+ s.add_dependency "actionpack", version
+ s.add_dependency "actionview", version
+ s.add_dependency "activemodel", version
+ s.add_dependency "activerecord", version
+ s.add_dependency "actionmailer", version
+ s.add_dependency "activejob", version
+ s.add_dependency "actioncable", version
+ s.add_dependency "activestorage", version
+ s.add_dependency "railties", version
- s.add_dependency 'bundler', '>= 1.3.0', '< 2.0'
- s.add_dependency 'sprockets-rails'
+ s.add_dependency "bundler", ">= 1.3.0"
+ s.add_dependency "sprockets-rails", ">= 2.0.0"
end
diff --git a/railties/.gitignore b/railties/.gitignore
index 80dd262d2f..c08562e016 100644
--- a/railties/.gitignore
+++ b/railties/.gitignore
@@ -1 +1,5 @@
-log/
+/log/
+/test/500.html
+/test/fixtures/tmp/
+/test/initializer/root/log/
+/tmp/
diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md
index 573b1f8f69..448fd48f10 100644
--- a/railties/CHANGELOG.md
+++ b/railties/CHANGELOG.md
@@ -1,212 +1,192 @@
-* Fix `NoMethodError` when generating a scaffold inside a full engine.
+* Remove `app/assets` and `app/javascript` from `eager_load_paths` and `autoload_paths`.
- *Yuji Yaginuma*
-
-* Adding support for passing a block to the `add_source` action of a custom generator
-
- *Mike Dalton*, *Hirofumi Wakasugi*
+ *Gannon McGibbon*
-* `assert_file` understands paths with special characters
- (eg. `v0.1.4~alpha+nightly`).
+* Add JSON support to rails properties route (`/rails/info/properties`).
- *Diego Carrion*
+ Now, `Rails::Info` properties may be accessed in JSON format at `/rails/info/properties.json`.
-* Remove ContentLength middleware from the defaults. If you want it, just
- add it as a middleware in your config.
+ *Yoshiyuki Hirano*
- *Egg McMuffin*
+* Use Ids instead of memory addresses when displaying references in scaffold views.
-* Make it possible to customize the executable inside rerun snippets.
+ Fixes #29200.
- *Yves Senn*
+ *Rasesh Patel*
-* Add support for API only apps.
- Middleware stack was slimmed down and it has only the needed
- middleware for API apps & generators generates the right files,
- folders and configurations.
+* Adds support for multiple databases to `rails db:migrate:status`.
+ Subtasks are also added to get the status of individual databases (eg. `rails db:migrate:status:animals`).
- *Santiago Pastorino & Jorge Bejar*
-
-* Make generated scaffold functional tests work inside engines.
-
- *Yuji Yaginuma*
+ *Gannon McGibbon*
-* Generator a `.keep` file in the `tmp` folder by default as many scripts
- assume the existence of this folder and most would fail if it is absent.
+* Use Webpacker by default to manage app-level JavaScript through the new app/javascript directory.
+ Sprockets is now solely in charge, by default, of compiling CSS and other static assets.
+ Action Cable channel generators will create ES6 stubs rather than use CoffeeScript.
+ Active Storage, Action Cable, Turbolinks, and Rails-UJS are loaded by a new application.js pack.
+ Generators no longer generate JavaScript stubs.
- See #20299.
+ *DHH*, *Lachlan Sylvester*
- *Yoong Kang Lim*, *Sunny Juneja*
+* Add `database` (aliased as `db`) option to model generator to allow
+ setting the database. This is useful for applications that use
+ multiple databases and put migrations per database in their own directories.
-* `config.static_index` configures directory `index.html` filename
+ ```
+ bin/rails g model Room capacity:integer --database=kingston
+ invoke active_record
+ create db/kingston_migrate/20180830151055_create_rooms.rb
+ ```
- Set `config.static_index` to serve a static directory index file not named
- `index`. E.g. to serve `main.html` instead of `index.html` for directory
- requests, set `config.static_index` to `"main"`.
+ Because rails scaffolding uses the model generator, you can
+ also specify a database with the scaffold generator.
- *Eliot Sykes*
+ *Gannon McGibbon*
-* `bin/setup` uses built-in rake tasks (`log:clear`, `tmp:clear`).
+* Raise an error when "recyclable cache keys" are being used by a cache store
+ that does not explicitly support it. Custom cache keys that do support this feature
+ can bypass this error by implementing the `supports_cache_versioning?` method on their
+ class and returning a truthy value.
- *Mohnish Thallavajhula*
+ *Richard Schneeman*
-* Fix mailer previews with attachments by using the mail gem's own API to
- locate the first part of the correct mime type.
+* Support environment specific credentials file.
- Fixes #14435.
+ For `production` environment look first for `config/credentials/production.yml.enc` file that can be decrypted by
+ `ENV["RAILS_MASTER_KEY"]` or `config/credentials/production.key` master key.
+ Edit given environment credentials file by command `rails credentials:edit --environment production`.
+ Default paths can be overwritten by setting `config.credentials.content_path` and `config.credentials.key_path`.
- *Andrew White*
+ *Wojciech Wnętrzak*
-* Remove sqlite support from `rails dbconsole`.
+* Make `ActiveSupport::Cache::NullStore` the default cache store in the test environment.
- *Andrew White*
+ *Michael C. Nelson*
-* Rename `railties/bin` to `railties/exe` to match the new Bundler executables
- convention.
+* Emit warning for unknown inflection rule when generating model.
- *Islam Wazery*
+ *Yoshiyuki Kinjo*
-* Print `bundle install` output in `rails new` as soon as it's available.
+* Add `database` (aliased as `db`) option to migration generator.
- Running `rails new` will now print the output of `bundle install` as
- it is available, instead of waiting until all gems finish installing.
+ If you're using multiple databases and have a folder for each database
+ for migrations (ex db/migrate and db/new_db_migrate) you can now pass the
+ `--database` option to the generator to make sure the the migration
+ is inserted into the correct folder.
- *Max Holder*
+ ```
+ rails g migration CreateHouses --database=kingston
+ invoke active_record
+ create db/kingston_migrate/20180830151055_create_houses.rb
+ ```
-* Respect `pluralize_table_names` when generating fixture file.
+ *Eileen M. Uchitelle*
- Fixes #19519.
+* Deprecate `rake routes` in favor of `rails routes`.
*Yuji Yaginuma*
-* Add a new-line to the end of route method generated code.
-
- We need to add a `\n`, because we cannot have two routes
- in the same line.
-
- *arthurnn*
-
-* Add `rake initializers`.
-
- This task prints out all defined initializers in the order they are invoked
- by Rails. This is helpful for debugging issues related to the initialization
- process.
-
- *Naoto Kaneko*
+* Deprecate `rake initializers` in favor of `rails initializers`.
-* Created rake restart task. Restarts your Rails app by touching the
- `tmp/restart.txt`.
+ *Annie-Claude Côté*
- Fixes #18876.
+* Deprecate `rake dev:cache` in favor of `rails dev:cache`.
- *Hyonjee Joo*
+ *Annie-Claude Côté*
-* Add `config/initializers/active_record_belongs_to_required_by_default.rb`.
+* Deprecate `rails notes` subcommands in favor of passing an `annotations` argument to `rails notes`.
- Newly generated Rails apps have a new initializer called
- `active_record_belongs_to_required_by_default.rb` which sets the value of
- the configuration option `config.active_record.belongs_to_required_by_default`
- to `true` when ActiveRecord is not skipped.
+ The following subcommands are replaced by passing `--annotations` or `-a` to `rails notes`:
+ - `rails notes:custom ANNOTATION=custom` is deprecated in favor of using `rails notes -a custom`.
+ - `rails notes:optimize` is deprecated in favor of using `rails notes -a OPTIMIZE`.
+ - `rails notes:todo` is deprecated in favor of using`rails notes -a TODO`.
+ - `rails notes:fixme` is deprecated in favor of using `rails notes -a FIXME`.
- As a result, new Rails apps require `belongs_to` association on model
- to be valid.
+ *Annie-Claude Côté*
- This initializer is *not* added when running `rake rails:update`, so
- old apps ported to Rails 5 will work without any change.
+* Deprecate `SOURCE_ANNOTATION_DIRECTORIES` environment variable used by `rails notes`
+ through `Rails::SourceAnnotationExtractor::Annotation` in favor of using `config.annotations.register_directories`.
- *Josef Šimánek*
+ *Annie-Claude Côté*
-* `delete` operations in configurations are run last in order to eliminate
- 'No such middleware' errors when `insert_before` or `insert_after` are added
- after the `delete` operation for the middleware being deleted.
+* Deprecate `rake notes` in favor of `rails notes`.
- Fixes #16433.
+ *Annie-Claude Côté*
- *Guo Xiang Tan*
+* Don't generate unused files in `app:update` task.
-* Newly generated applications get a `README.md` in Markdown.
+ Skip the assets' initializer when sprockets isn't loaded.
- *Xavier Noria*
+ Skip `config/spring.rb` when spring isn't loaded.
-* Remove the documentation tasks `doc:app`, `doc:rails`, and `doc:guides`.
+ Skip yarn's contents when yarn integration isn't used.
- *Xavier Noria*
+ *Tsukuru Tanimichi*
-* Force generated routes to be inserted into `config/routes.rb`.
+* Make the master.key file read-only for the owner upon generation on
+ POSIX-compliant systems.
- *Andrew White*
+ Previously:
-* Don't remove all line endings from `config/routes.rb` when revoking scaffold.
+ $ ls -l config/master.key
+ -rw-r--r-- 1 owner group 32 Jan 1 00:00 master.key
- Fixes #15913.
+ Now:
- *Andrew White*
+ $ ls -l config/master.key
+ -rw------- 1 owner group 32 Jan 1 00:00 master.key
-* Rename `--skip-test-unit` option to `--skip-test` in app generator
+ Fixes #32604.
- *Melanie Gilman*
+ *Jose Luis Duran*
-* Add the `method_source` gem to the default Gemfile for apps.
+* Deprecate support for using the `HOST` environment to specify the server IP.
- *Sean Griffin*
+ The `BINDING` environment should be used instead.
-* Drop old test locations from `rake stats`:
+ Fixes #29516.
- - test/functional
- - test/unit
-
- *Ravil Bayramgalin*
-
-* Update `rake stats` to correctly count declarative tests
- as methods in `_test.rb` files.
-
- *Ravil Bayramgalin*
-
-* Remove deprecated `test:all` and `test:all:db` tasks.
-
- *Rafael Mendonça França*
-
-* Remove deprecated `Rails::Rack::LogTailer`.
-
- *Rafael Mendonça França*
-
-* Remove deprecated `RAILS_CACHE` constant.
-
- *Rafael Mendonça França*
-
-* Remove deprecated `serve_static_assets` configuration.
+ *Yuji Yaginuma*
- *Rafael Mendonça França*
+* Deprecate passing Rack server name as a regular argument to `rails server`.
-* Use local variables in `_form.html.erb` partial generated by scaffold.
+ Previously:
- *Andrew Kozlov*
+ $ bin/rails server thin
-* Add `config/initializers/callback_terminator.rb`.
+ There wasn't an explicit option for the Rack server to use, now we have the
+ `--using` option with the `-u` short switch.
- Newly generated Rails apps have a new initializer called
- `callback_terminator.rb` which sets the value of the configuration option
- `config.active_support.halt_callback_chains_on_return_false` to `false`.
+ Now:
- As a result, new Rails apps do not halt callback chains when a callback
- returns `false`; only when they are explicitly halted with `throw(:abort)`.
+ $ bin/rails server -u thin
- The terminator is *not* added when running `rake rails:update`, so returning
- `false` will still work on old apps ported to Rails 5, displaying a
- deprecation warning to prompt users to update their code to the new syntax.
+ This change also improves the error message if a missing or mistyped rack
+ server is given.
- *claudiob*
+ *Genadi Samokovarov*
-* Generated fixtures won't use the id when generated with references attributes.
+* Add "rails routes --expanded" option to output routes in expanded mode like
+ "psql --expanded". Result looks like:
- *Pablo Olmos de Aguilera Corradini*
+ ```
+ $ rails routes --expanded
+ --[ Route 1 ]------------------------------------------------------------
+ Prefix | high_scores
+ Verb | GET
+ URI | /high_scores(.:format)
+ Controller#Action | high_scores#index
+ --[ Route 2 ]------------------------------------------------------------
+ Prefix | new_high_score
+ Verb | GET
+ URI | /high_scores/new(.:format)
+ Controller#Action | high_scores#new
+ ```
-* Add `--skip-action-mailer` option to the app generator.
+ *Benoit Tigeot*
- *claudiob*
+* Rails 6 requires Ruby 2.4.1 or newer.
-* Autoload any second level directories called `app/*/concerns`.
+ *Jeremy Daer*
- *Alex Robbin*
-Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/railties/CHANGELOG.md) for previous changes.
+Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/railties/CHANGELOG.md) for previous changes.
diff --git a/railties/MIT-LICENSE b/railties/MIT-LICENSE
index 7c2197229d..cce00cbc3a 100644
--- a/railties/MIT-LICENSE
+++ b/railties/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2015 David Heinemeier Hansson
+Copyright (c) 2004-2018 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
@@ -17,4 +17,4 @@ 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. \ No newline at end of file
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/railties/RDOC_MAIN.rdoc b/railties/RDOC_MAIN.rdoc
index 8d847eaa1c..89fc6bcbce 100644
--- a/railties/RDOC_MAIN.rdoc
+++ b/railties/RDOC_MAIN.rdoc
@@ -1,73 +1,98 @@
-== Welcome to \Rails
-
-\Rails is a web-application framework that includes everything needed to create
-database-backed web applications according to the {Model-View-Controller (MVC)}[http://en.wikipedia.org/wiki/Model-view-controller] pattern.
-
-Understanding the MVC pattern is key to understanding \Rails. MVC divides your application
-into three layers, each with a specific responsibility.
-
-The View layer is composed of "templates" that are responsible for providing
-appropriate representations of your application's resources. Templates
-can come in a variety of formats, but most view templates are \HTML with embedded Ruby
-code (.erb files).
-
-The Model layer represents your domain model (such as Account, Product, Person, Post)
-and encapsulates the business logic that is specific to your application. In \Rails,
-database-backed model classes are derived from ActiveRecord::Base. Active Record allows
-you to present the data from database rows as objects and embellish these data objects
-with business logic methods. Although most \Rails models are backed by a database, models
-can also be ordinary Ruby classes, or Ruby classes that implement a set of interfaces as
-provided by the ActiveModel module. You can read more about Active Record in its
-{README}[link:files/activerecord/README_rdoc.html].
-
-The Controller layer is responsible for handling incoming HTTP requests and providing a
-suitable response. Usually this means returning \HTML, but \Rails controllers can also
-generate XML, JSON, PDFs, mobile-specific views, and more. Controllers manipulate models
-and render view templates in order to generate the appropriate HTTP response.
-
-In \Rails, the Controller and View layers are handled together by Action Pack.
-These two layers are bundled in a single package due to their heavy interdependence.
-This is unlike the relationship between Active Record and Action Pack, which are
-independent. Each of these packages can be used independently outside of \Rails. You
-can read more about Action Pack in its {README}[link:files/actionpack/README_rdoc.html].
+= Welcome to \Rails
+
+== What's \Rails
+
+\Rails is a web-application framework that includes everything needed to
+create database-backed web applications according to the
+{Model-View-Controller (MVC)}[http://en.wikipedia.org/wiki/Model-view-controller]
+pattern.
+
+Understanding the MVC pattern is key to understanding \Rails. MVC divides your
+application into three layers: Model, View, and Controller, each with a specific responsibility.
+
+== Model layer
+
+The <em><b>Model layer</b></em> represents the domain model (such as Account, Product,
+Person, Post, etc.) and encapsulates the business logic specific to
+your application. In \Rails, database-backed model classes are derived from
+<tt>ActiveRecord::Base</tt>. {Active Record}[link:files/activerecord/README_rdoc.html] allows you to present the data from
+database rows as objects and embellish these data objects with business logic
+methods. Although most \Rails models are backed by a database, models can also be ordinary
+Ruby classes, or Ruby classes that implement a set of interfaces as provided by
+the {Active Model}[link:files/activemodel/README_rdoc.html] module.
+
+== Controller layer
+
+The <em><b>Controller layer</b></em> is responsible for handling incoming HTTP requests and
+providing a suitable response. Usually this means returning \HTML, but \Rails controllers
+can also generate XML, JSON, PDFs, mobile-specific views, and more. Controllers load and
+manipulate models, and render view templates in order to generate the appropriate HTTP response.
+In \Rails, incoming requests are routed by Action Dispatch to an appropriate controller, and
+controller classes are derived from <tt>ActionController::Base</tt>. Action Dispatch and Action Controller
+are bundled together in {Action Pack}[link:files/actionpack/README_rdoc.html].
+
+== View layer
+
+The <em><b>View layer</b></em> is composed of "templates" that are responsible for providing
+appropriate representations of your application's resources. Templates can
+come in a variety of formats, but most view templates are \HTML with embedded
+Ruby code (ERB files). Views are typically rendered to generate a controller response,
+or to generate the body of an email. In \Rails, View generation is handled by {Action View}[link:files/actionview/README_rdoc.html].
+
+== Frameworks and libraries
+
+{Active Record}[link:files/activerecord/README_rdoc.html], {Active Model}[link:files/activemodel/README_rdoc.html],
+{Action Pack}[link:files/actionpack/README_rdoc.html], and {Action View}[link:files/actionview/README_rdoc.html] can each be used independently outside \Rails.
+In addition to that, \Rails also comes with {Action Mailer}[link:files/actionmailer/README_rdoc.html], a library
+to generate and send emails; {Active Job}[link:files/activejob/README_md.html], a
+framework for declaring jobs and making them run on a variety of queueing
+backends; {Action Cable}[link:files/actioncable/README_md.html], a framework to
+integrate WebSockets with a \Rails application; {Active Storage}[link:files/activestorage/README_md.html],
+a library to attach cloud and local files to \Rails applications;
+and {Active Support}[link:files/activesupport/README_rdoc.html], a collection
+of utility classes and standard library extensions that are useful for \Rails,
+and may also be used independently outside \Rails.
== Getting Started
1. Install \Rails at the command prompt if you haven't yet:
- gem install rails
+ $ gem install rails
2. At the command prompt, create a new \Rails application:
- rails new myapp
+ $ rails new myapp
where "myapp" is the application name.
3. Change directory to +myapp+ and start the web server:
- cd myapp; rails server
+ $ cd myapp
+ $ rails server
Run with <tt>--help</tt> or <tt>-h</tt> for options.
-4. Go to http://localhost:3000 and you'll see:
-
- "Welcome aboard: You're riding Ruby on Rails!"
+4. Go to <tt>http://localhost:3000</tt> and you'll see: "Yay! You’re on \Rails!"
5. Follow the guidelines to start developing your application. You may find the following resources handy:
-* The \README file created within your application.
-* {Getting Started with \Rails}[http://guides.rubyonrails.org/getting_started.html].
-* {Ruby on \Rails Tutorial}[http://ruby.railstutorial.org/ruby-on-rails-tutorial-book].
-* {Ruby on \Rails Guides}[http://guides.rubyonrails.org].
-* {The API Documentation}[http://api.rubyonrails.org].
+ * The \README file created within your application.
+ * {Getting Started with \Rails}[https://guides.rubyonrails.org/getting_started.html].
+ * {Ruby on \Rails Guides}[https://guides.rubyonrails.org].
+ * {The API Documentation}[http://api.rubyonrails.org].
+ * {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 Rails
-guide}[http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html] for guidelines about how
-to proceed. {Join us}[http://contributors.rubyonrails.org]!
+We encourage you to contribute to Ruby on \Rails! Please check out the
+{Contributing to Ruby on \Rails guide}[https://guides.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/].
== License
-Ruby on \Rails is released under the {MIT License}[http://www.opensource.org/licenses/MIT].
+Ruby on \Rails is released under the {MIT License}[https://opensource.org/licenses/MIT].
diff --git a/railties/README.rdoc b/railties/README.rdoc
index a25658668c..2715ccac3f 100644
--- a/railties/README.rdoc
+++ b/railties/README.rdoc
@@ -23,7 +23,7 @@ Source code can be downloaded as part of the Rails project on GitHub
Railties is released under the MIT license:
-* http://www.opensource.org/licenses/MIT
+* https://opensource.org/licenses/MIT
== Support
@@ -38,4 +38,3 @@ Bug reports can be filed for the Ruby on Rails project here:
Feature requests should be discussed on the rails-core mailing list here:
* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core
-
diff --git a/railties/Rakefile b/railties/Rakefile
index 4393f45790..445f6217b3 100644
--- a/railties/Rakefile
+++ b/railties/Rakefile
@@ -1,31 +1,83 @@
-require 'rake/testtask'
+# frozen_string_literal: true
-task :default => :test
+require "rake/testtask"
+
+task default: :test
+
+task :package
desc "Run all unit tests"
-task :test => 'test:isolated'
+task test: "test:isolated"
namespace :test do
task :isolated do
+ dash_i = [
+ "test",
+ "lib",
+ "../activesupport/lib",
+ "../actionpack/lib",
+ "../actionview/lib",
+ "../activemodel/lib"
+ ].map { |dir| File.expand_path(dir, __dir__) }
+
+ dash_i.reverse_each do |x|
+ $:.unshift(x) unless $:.include?(x)
+ end
+ $-w = true
+
+ require "bundler/setup" unless defined?(Bundler)
+ require "active_support"
+
+ # Only generate the template app once.
+ require_relative "test/isolation/abstract_unit"
+
+ failing_files = []
+
dirs = (ENV["TEST_DIR"] || ENV["TEST_DIRS"] || "**").split(",")
+ test_options = ENV["TESTOPTS"].to_s.split(/[\s]+/)
+
test_files = dirs.map { |dir| "test/#{dir}/*_test.rb" }
Dir[*test_files].each do |file|
- next true if file.include?("fixtures")
- dash_i = [
- 'test',
- 'lib',
- "#{File.dirname(__FILE__)}/../activesupport/lib",
- "#{File.dirname(__FILE__)}/../actionpack/lib",
- "#{File.dirname(__FILE__)}/../activemodel/lib"
- ]
- ruby "-w", "-I#{dash_i.join ':'}", file
+ next true if file.start_with?("test/fixtures/")
+
+ fake_command = Shellwords.join([
+ FileUtils::RUBY,
+ "-w",
+ *dash_i.map { |dir| "-I#{Pathname.new(dir).relative_path_from(Pathname.pwd)}" },
+ file,
+ ])
+ puts fake_command
+
+ # We could run these in parallel, but pretty much all of the
+ # railties tests already run in parallel, so ¯\_(⊙︿⊙)_/¯
+ Process.waitpid fork {
+ ARGV.clear.concat test_options
+ Rake.application = nil
+
+ load file
+ }
+
+ unless $?.success?
+ failing_files << file
+ end
+ end
+
+ unless failing_files.empty?
+ puts
+ puts "Failed in:"
+ failing_files.each do |file|
+ puts " #{file}"
+ end
+ puts
+
+ raise "Failure in isolated test runner"
end
end
end
-Rake::TestTask.new('test:regular') do |t|
- t.libs << 'test' << "#{File.dirname(__FILE__)}/../activesupport/lib"
- t.pattern = 'test/**/*_test.rb'
+Rake::TestTask.new("test:regular") do |t|
+ t.libs << "test" << "#{__dir__}/../activesupport/lib"
+ t.pattern = "test/**/*_test.rb"
t.warning = true
t.verbose = true
t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
diff --git a/railties/bin/test b/railties/bin/test
new file mode 100755
index 0000000000..c53377cc97
--- /dev/null
+++ b/railties/bin/test
@@ -0,0 +1,5 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+COMPONENT_ROOT = File.expand_path("..", __dir__)
+require_relative "../../tools/test"
diff --git a/railties/exe/rails b/railties/exe/rails
index 82c17cabce..79af4db6b6 100755
--- a/railties/exe/rails
+++ b/railties/exe/rails
@@ -1,9 +1,10 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
-git_path = File.expand_path('../../../.git', __FILE__)
+git_path = File.expand_path("../../.git", __dir__)
if File.exist?(git_path)
- railties_path = File.expand_path('../../lib', __FILE__)
+ railties_path = File.expand_path("../lib", __dir__)
$:.unshift(railties_path)
end
require "rails/cli"
diff --git a/railties/lib/minitest/rails_plugin.rb b/railties/lib/minitest/rails_plugin.rb
new file mode 100644
index 0000000000..6486fa1798
--- /dev/null
+++ b/railties/lib/minitest/rails_plugin.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/attribute_accessors"
+require "rails/test_unit/reporter"
+require "rails/test_unit/runner"
+
+module Minitest
+ class SuppressedSummaryReporter < SummaryReporter
+ # Disable extra failure output after a run if output is inline.
+ def aggregated_results(*)
+ super unless options[:output_inline]
+ end
+ end
+
+ def self.plugin_rails_options(opts, options)
+ ::Rails::TestUnit::Runner.attach_before_load_options(opts)
+
+ opts.on("-b", "--backtrace", "Show the complete backtrace") do
+ options[:full_backtrace] = true
+ end
+
+ opts.on("-d", "--defer-output", "Output test failures and errors after the test run") do
+ options[:output_inline] = false
+ end
+
+ opts.on("-f", "--fail-fast", "Abort test run on first failure or error") do
+ options[:fail_fast] = true
+ end
+
+ opts.on("-c", "--[no-]color", "Enable color in the output") do |value|
+ options[:color] = value
+ end
+
+ options[:color] = true
+ options[:output_inline] = true
+ end
+
+ # Owes great inspiration to test runner trailblazers like RSpec,
+ # minitest-reporters, maxitest and others.
+ def self.plugin_rails_init(options)
+ unless options[:full_backtrace] || ENV["BACKTRACE"]
+ # Plugin can run without Rails loaded, check before filtering.
+ Minitest.backtrace_filter = ::Rails.backtrace_cleaner if ::Rails.respond_to?(:backtrace_cleaner)
+ end
+
+ # Suppress summary reports when outputting inline rerun snippets.
+ if reporter.reporters.reject! { |reporter| reporter.kind_of?(SummaryReporter) }
+ reporter << SuppressedSummaryReporter.new(options[:io], options)
+ end
+
+ # Replace progress reporter for colors.
+ if reporter.reporters.reject! { |reporter| reporter.kind_of?(ProgressReporter) }
+ reporter << ::Rails::TestUnitReporter.new(options[:io], options)
+ end
+ end
+
+ # Backwardscompatibility with Rails 5.0 generated plugin test scripts
+ mattr_reader :run_via, default: {}
+end
diff --git a/railties/lib/rails.rb b/railties/lib/rails.rb
index fe789f3c2a..092105d502 100644
--- a/railties/lib/rails.rb
+++ b/railties/lib/rails.rb
@@ -1,18 +1,21 @@
-require 'rails/ruby_version_check'
+# frozen_string_literal: true
-require 'pathname'
+require "rails/ruby_version_check"
-require 'active_support'
-require 'active_support/dependencies/autoload'
-require 'active_support/core_ext/kernel/reporting'
-require 'active_support/core_ext/module/delegation'
-require 'active_support/core_ext/array/extract_options'
+require "pathname"
-require 'rails/application'
-require 'rails/version'
+require "active_support"
+require "active_support/dependencies/autoload"
+require "active_support/core_ext/kernel/reporting"
+require "active_support/core_ext/module/delegation"
+require "active_support/core_ext/array/extract_options"
+require "active_support/core_ext/object/blank"
-require 'active_support/railtie'
-require 'action_dispatch/railtie'
+require "rails/application"
+require "rails/version"
+
+require "active_support/railtie"
+require "action_dispatch/railtie"
# UTF-8 is the default internal and external encoding.
silence_warnings do
@@ -46,14 +49,14 @@ module Rails
def backtrace_cleaner
@backtrace_cleaner ||= begin
- # Relies on Active Support, so we have to lazy load to postpone definition until AS has been loaded
- require 'rails/backtrace_cleaner'
+ # Relies on Active Support, so we have to lazy load to postpone definition until Active Support has been loaded
+ require "rails/backtrace_cleaner"
Rails::BacktraceCleaner.new
end
end
- # Returns a Pathname object of the current rails project,
- # otherwise it returns nil if there is no project:
+ # Returns a Pathname object of the current Rails project,
+ # otherwise it returns +nil+ if there is no project:
#
# Rails.root
# # => #<Pathname:/Users/someuser/some/path/project>
@@ -67,7 +70,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.
@@ -77,7 +80,7 @@ module Rails
@_env = ActiveSupport::StringInquirer.new(environment)
end
- # Returns all rails groups for loading based on:
+ # Returns all Rails groups for loading based on:
#
# * The Rails environment;
# * The environment variable RAILS_GROUPS;
@@ -86,8 +89,8 @@ module Rails
# groups assets: [:development, :test]
#
# # Returns
- # # => [:default, :development, :assets] for Rails.env == "development"
- # # => [:default, :production] for Rails.env == "production"
+ # # => [:default, "development", :assets] for Rails.env == "development"
+ # # => [:default, "production"] for Rails.env == "production"
def groups(*groups)
hash = groups.extract_options!
env = Rails.env
@@ -100,7 +103,7 @@ module Rails
end
# Returns a Pathname object of the public folder of the current
- # rails project, otherwise it returns nil if there is no project:
+ # Rails project, otherwise it returns +nil+ if there is no project:
#
# Rails.public_path
# # => #<Pathname:/Users/someuser/some/path/project/public>
diff --git a/railties/lib/rails/all.rb b/railties/lib/rails/all.rb
index 45361fca83..f5dccd2381 100644
--- a/railties/lib/rails/all.rb
+++ b/railties/lib/rails/all.rb
@@ -1,16 +1,20 @@
+# frozen_string_literal: true
+
require "rails"
%w(
- active_record
- action_controller
- action_view
- action_mailer
- active_job
- rails/test_unit
- sprockets
-).each do |framework|
+ active_record/railtie
+ active_storage/engine
+ action_controller/railtie
+ action_view/railtie
+ action_mailer/railtie
+ active_job/railtie
+ action_cable/engine
+ rails/test_unit/railtie
+ sprockets/railtie
+).each do |railtie|
begin
- require "#{framework}/railtie"
+ require railtie
rescue LoadError
end
end
diff --git a/railties/lib/rails/api/generator.rb b/railties/lib/rails/api/generator.rb
new file mode 100644
index 0000000000..126d4d0438
--- /dev/null
+++ b/railties/lib/rails/api/generator.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require "sdoc"
+require "active_support/core_ext/array/extract"
+
+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 and remove
+ # Active Storage duplicated classes that are at the top level
+ # since they aren't nested under a definition of the `ActiveStorage` module.
+ if visited.empty?
+ classes = classes.reject { |klass| active_storage?(klass) }
+ core_exts = classes.extract! { |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
+
+ def active_storage?(klass)
+ klass.name != "ActiveStorage" && klass.in_files.all? { |file| file.absolute_name.include?("active_storage") }
+ end
+end
diff --git a/railties/lib/rails/api/task.rb b/railties/lib/rails/api/task.rb
index a082932632..e7f0557584 100644
--- a/railties/lib/rails/api/task.rb
+++ b/railties/lib/rails/api/task.rb
@@ -1,33 +1,35 @@
-require 'rdoc/task'
+# frozen_string_literal: true
+
+require "rdoc/task"
+require "rails/api/generator"
module Rails
module API
class Task < RDoc::Task
RDOC_FILES = {
- 'activesupport' => {
- :include => %w(
+ "activesupport" => {
+ include: %w(
README.rdoc
lib/active_support/**/*.rb
- ),
- :exclude => 'lib/active_support/vendor/*'
+ )
},
- 'activerecord' => {
- :include => %w(
+ "activerecord" => {
+ include: %w(
README.rdoc
lib/active_record/**/*.rb
)
},
- 'activemodel' => {
- :include => %w(
+ "activemodel" => {
+ include: %w(
README.rdoc
lib/active_model/**/*.rb
)
},
- 'actionpack' => {
- :include => %w(
+ "actionpack" => {
+ include: %w(
README.rdoc
lib/abstract_controller/**/*.rb
lib/action_controller/**/*.rb
@@ -35,34 +37,53 @@ module Rails
)
},
- 'actionview' => {
- :include => %w(
+ "actionview" => {
+ include: %w(
README.rdoc
lib/action_view/**/*.rb
),
- :exclude => 'lib/action_view/vendor/*'
+ exclude: "lib/action_view/vendor/*"
},
- 'actionmailer' => {
- :include => %w(
+ "actionmailer" => {
+ include: %w(
README.rdoc
lib/action_mailer/**/*.rb
)
},
- 'activejob' => {
- :include => %w(
+ "activejob" => {
+ include: %w(
README.md
lib/active_job/**/*.rb
)
},
- 'railties' => {
- :include => %w(
+ "actioncable" => {
+ include: %w(
+ README.md
+ lib/action_cable/**/*.rb
+ )
+ },
+
+ "activestorage" => {
+ include: %w(
+ README.md
+ app/**/active_storage/**/*.rb
+ lib/active_storage/**/*.rb
+ )
+ },
+
+ "railties" => {
+ include: %w(
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
+ )
}
}
@@ -73,7 +94,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
@@ -84,20 +105,15 @@ module Rails
# no-op
end
- def load_and_configure_sdoc
- require 'sdoc'
-
- self.title = 'Ruby on Rails API'
+ def configure_sdoc
+ self.title = "Ruby on Rails API"
self.rdoc_dir = api_dir
- options << '-m' << api_main
- options << '-e' << 'UTF-8'
+ options << "-m" << api_main
+ options << "-e" << "UTF-8"
- options << '-f' << 'sdoc'
- 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
+ options << "-f" << "api"
+ options << "-T" << "rails"
end
def configure_rdoc_files
@@ -114,22 +130,35 @@ module Rails
rdoc_files.exclude("#{cdr}/#{pattern}")
end
end
+
+ # Only generate documentation for files that have been
+ # changed since the API was generated.
+ if Dir.exist?("doc/rdoc") && !ENV["ALL"]
+ last_generation = DateTime.rfc2822(File.open("doc/rdoc/created.rid", &:readline))
+
+ rdoc_files.keep_if do |file|
+ File.mtime(file).to_datetime > last_generation
+ end
+
+ # Nothing to do
+ exit(0) if rdoc_files.empty?
+ end
end
def setup_horo_variables
- ENV['HORO_PROJECT_NAME'] = 'Ruby on Rails'
- ENV['HORO_PROJECT_VERSION'] = rails_version
+ ENV["HORO_PROJECT_NAME"] = "Ruby on Rails"
+ ENV["HORO_PROJECT_VERSION"] = rails_version
end
def api_main
- component_root_dir('railties') + '/RDOC_MAIN.rdoc'
+ component_root_dir("railties") + "/RDOC_MAIN.rdoc"
end
end
class RepoTask < Task
- def load_and_configure_sdoc
+ def configure_sdoc
super
- options << '-g' # link to GitHub, SDoc flag
+ options << "-g" # link to GitHub, SDoc flag
end
def component_root_dir(component)
@@ -137,7 +166,7 @@ module Rails
end
def api_dir
- 'doc/rdoc'
+ "doc/rdoc"
end
end
@@ -149,7 +178,7 @@ module Rails
class StableTask < RepoTask
def rails_version
- File.read('RAILS_VERSION').strip
+ File.read("RAILS_VERSION").strip
end
end
end
diff --git a/railties/lib/rails/app_loader.rb b/railties/lib/rails/app_loader.rb
index a9fe21824e..aabcc5970c 100644
--- a/railties/lib/rails/app_loader.rb
+++ b/railties/lib/rails/app_loader.rb
@@ -1,22 +1,35 @@
-require 'pathname'
-require 'rails/version'
+# frozen_string_literal: true
+
+require "pathname"
+require "rails/version"
module Rails
module AppLoader # :nodoc:
extend self
RUBY = Gem.ruby
- EXECUTABLES = ['bin/rails', 'script/rails']
+ EXECUTABLES = ["bin/rails", "script/rails"]
BUNDLER_WARNING = <<EOS
-Looks like your app's ./bin/rails is a stub that was generated by Bundler.
+Beginning in Rails 4, Rails ships with a `rails` binstub at ./bin/rails that
+should be used instead of the Bundler-generated `rails` binstub.
+
+If you are seeing this message, your binstub at ./bin/rails was generated by
+Bundler instead of Rails.
+
+You might need to regenerate your `rails` binstub locally and add it to source
+control:
+
+ rails app:update:bin # Bear in mind this generates other binstubs
+ # too that you may or may not want (like yarn)
-In Rails #{Rails::VERSION::MAJOR}, your app's bin/ directory contains executables that are versioned
-like any other source code, rather than stubs that are generated on demand.
+If you already have Rails binstubs in source control, you might be
+inadverently overwriting them during deployment by using bundle install
+with the --binstubs option.
-Here's how to upgrade:
+If your application was created prior to Rails 4, here's how to upgrade:
bundle config --delete bin # Turn off Bundler's stub generator
- rake rails:update:bin # Use the new Rails 4 executables
+ rails app:update:bin # Use the new Rails executables
git add bin # Add bin/ to source control
You may need to remove bin/ from your .gitignore as well.
@@ -36,24 +49,24 @@ EOS
if exe = find_executable
contents = File.read(exe)
- if contents =~ /(APP|ENGINE)_PATH/
+ if /(APP|ENGINE)_PATH/.match?(contents)
exec RUBY, exe, *ARGV
break # non reachable, hack to be able to stub exec in the test suite
- elsif exe.end_with?('bin/rails') && contents.include?('This file was generated by Bundler')
+ elsif exe.end_with?("bin/rails") && contents.include?("This file was generated by Bundler")
$stderr.puts(BUNDLER_WARNING)
- Object.const_set(:APP_PATH, File.expand_path('config/application', Dir.pwd))
- require File.expand_path('../boot', APP_PATH)
- require 'rails/commands'
+ Object.const_set(:APP_PATH, File.expand_path("config/application", Dir.pwd))
+ require File.expand_path("../boot", APP_PATH)
+ require "rails/commands"
break
end
end
# If we exhaust the search there is no executable, this could be a
# call to generate a new application, so restore the original cwd.
- Dir.chdir(original_cwd) and return if Pathname.new(Dir.pwd).root?
+ Dir.chdir(original_cwd) && return if Pathname.new(Dir.pwd).root?
# Otherwise keep moving upwards in search of an executable.
- Dir.chdir('..')
+ Dir.chdir("..")
end
end
diff --git a/railties/lib/rails/app_updater.rb b/railties/lib/rails/app_updater.rb
new file mode 100644
index 0000000000..19d136e041
--- /dev/null
+++ b/railties/lib/rails/app_updater.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require "rails/generators"
+require "rails/generators/rails/app/app_generator"
+
+module Rails
+ class AppUpdater # :nodoc:
+ class << self
+ def invoke_from_app_generator(method)
+ app_generator.send(method)
+ end
+
+ def app_generator
+ @app_generator ||= begin
+ gen = Rails::Generators::AppGenerator.new ["rails"], generator_options, destination_root: Rails.root
+ File.exist?(Rails.root.join("config", "application.rb")) ? gen.send(:app_const) : gen.send(:valid_const?)
+ gen
+ end
+ end
+
+ private
+ def generator_options
+ options = { api: !!Rails.application.config.api_only, update: true }
+ options[:skip_javascript] = !File.exist?(Rails.root.join("bin", "yarn"))
+ options[:skip_active_record] = !defined?(ActiveRecord::Railtie)
+ options[:skip_active_storage] = !defined?(ActiveStorage::Engine) || !defined?(ActiveRecord::Railtie)
+ options[:skip_action_mailer] = !defined?(ActionMailer::Railtie)
+ options[:skip_action_cable] = !defined?(ActionCable::Engine)
+ options[:skip_sprockets] = !defined?(Sprockets::Railtie)
+ options[:skip_puma] = !defined?(Puma)
+ options[:skip_bootsnap] = !defined?(Bootsnap)
+ options[:skip_spring] = !defined?(Spring)
+ options
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb
index 8075068b3f..656786246d 100644
--- a/railties/lib/rails/application.rb
+++ b/railties/lib/rails/application.rb
@@ -1,10 +1,14 @@
-require 'fileutils'
-require 'yaml'
-require 'active_support/core_ext/hash/keys'
-require 'active_support/core_ext/object/blank'
-require 'active_support/key_generator'
-require 'active_support/message_verifier'
-require 'rails/engine'
+# frozen_string_literal: true
+
+require "yaml"
+require "active_support/core_ext/hash/keys"
+require "active_support/core_ext/object/blank"
+require "active_support/key_generator"
+require "active_support/message_verifier"
+require "active_support/encrypted_configuration"
+require "active_support/deprecation"
+require "rails/engine"
+require "rails/secrets"
module Rails
# An Engine with the responsibility of coordinating the whole boot process.
@@ -73,22 +77,22 @@ 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
- # application other than +Rails.application+, then you must run those
- # these manually.
+ # 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'
- autoload :Configuration, 'rails/application/configuration'
- autoload :DefaultMiddlewareStack, 'rails/application/default_middleware_stack'
- autoload :Finisher, 'rails/application/finisher'
- autoload :Railties, 'rails/engine/railties'
- autoload :RoutesReloader, 'rails/application/routes_reloader'
+ autoload :Bootstrap, "rails/application/bootstrap"
+ autoload :Configuration, "rails/application/configuration"
+ autoload :DefaultMiddlewareStack, "rails/application/default_middleware_stack"
+ autoload :Finisher, "rails/application/finisher"
+ autoload :Railties, "rails/engine/railties"
+ autoload :RoutesReloader, "rails/application/routes_reloader"
class << self
def inherited(base)
super
Rails.app_class = base
add_lib_to_load_path!(find_root(base.called_from))
+ ActiveSupport.run_load_hooks(:before_configuration, base)
end
def instance
@@ -113,7 +117,7 @@ module Rails
attr_accessor :assets, :sandbox
alias_method :sandbox?, :sandbox
- attr_reader :reloaders
+ attr_reader :reloaders, :reloader, :executor
delegate :default_url_options, :default_url_options=, to: :routes
@@ -131,6 +135,10 @@ module Rails
@message_verifiers = {}
@ran_load_hooks = false
+ @executor = Class.new(ActiveSupport::Executor)
+ @reloader = Class.new(ActiveSupport::Reloader)
+ @reloader.executor = @executor
+
# are these actually used?
@initial_variable_values = initial_variable_values
@block = block
@@ -144,7 +152,6 @@ module Rails
def run_load_hooks! # :nodoc:
return self if @ran_load_hooks
@ran_load_hooks = true
- ActiveSupport.run_load_hooks(:before_configuration, self)
@initial_variable_values.each do |variable_name, value|
if INITIAL_VARIABLES.include?(variable_name)
@@ -156,28 +163,20 @@ module Rails
self
end
- # Implements call according to the Rack API. It simply
- # dispatches the request to the underlying middleware stack.
- def call(env)
- req = ActionDispatch::Request.new env
- env["ORIGINAL_FULLPATH"] = req.fullpath
- env["ORIGINAL_SCRIPT_NAME"] = req.script_name
- super(env)
- end
-
# Reload application routes regardless if they changed or not.
def reload_routes!
routes_reloader.reload!
end
- # Return the application's KeyGenerator
+ # Returns the application's KeyGenerator
def key_generator
# number of iterations selected based on consultation with the google security
# team. Details at https://github.com/rails/rails/pull/6952#issuecomment-7661220
@caching_key_generator ||=
- if secrets.secret_key_base
- key_generator = ActiveSupport::KeyGenerator.new(secrets.secret_key_base, iterations: 1000)
- ActiveSupport::CachingKeyGenerator.new(key_generator)
+ if secret_key_base
+ ActiveSupport::CachingKeyGenerator.new(
+ ActiveSupport::KeyGenerator.new(secret_key_base, iterations: 1000)
+ )
else
ActiveSupport::LegacyKeyGenerator.new(secrets.secret_token)
end
@@ -220,16 +219,23 @@ module Rails
# url: http://localhost:3001
# namespace: my_app_development
#
- # # config/production.rb
+ # # config/environments/production.rb
# Rails.application.configure do
# config.middleware.use ExceptionNotifier, config_for(:exception_notification)
# end
- def config_for(name)
- yaml = Pathname.new("#{paths["config"].existent.first}/#{name}.yml")
+ def config_for(name, env: Rails.env)
+ if name.is_a?(Pathname)
+ yaml = name
+ else
+ yaml = Pathname.new("#{paths["config"].existent.first}/#{name}.yml")
+ end
if yaml.exist?
require "erb"
- (YAML.load(ERB.new(yaml.read).result) || {})[Rails.env] || {}
+ require "active_support/ordered_options"
+
+ config = (YAML.load(ERB.new(yaml.read).result) || {})[env] || {}
+ ActiveSupport::InheritableOptions.new(config.deep_symbolize_keys)
else
raise "Could not load configuration. No such file - #{yaml}"
end
@@ -243,13 +249,11 @@ module Rails
# will be used by middlewares and engines to configure themselves.
def env_config
@app_env_config ||= begin
- validate_secret_key_config!
-
- super.merge({
+ super.merge(
"action_dispatch.parameter_filter" => config.filter_parameters,
"action_dispatch.redirect_filter" => config.filter_redirect,
"action_dispatch.secret_token" => secrets.secret_token,
- "action_dispatch.secret_key_base" => secrets.secret_key_base,
+ "action_dispatch.secret_key_base" => secret_key_base,
"action_dispatch.show_exceptions" => config.action_dispatch.show_exceptions,
"action_dispatch.show_detailed_exceptions" => config.consider_all_requests_local,
"action_dispatch.logger" => Rails.logger,
@@ -259,14 +263,23 @@ module Rails
"action_dispatch.signed_cookie_salt" => config.action_dispatch.signed_cookie_salt,
"action_dispatch.encrypted_cookie_salt" => config.action_dispatch.encrypted_cookie_salt,
"action_dispatch.encrypted_signed_cookie_salt" => config.action_dispatch.encrypted_signed_cookie_salt,
+ "action_dispatch.authenticated_encrypted_cookie_salt" => config.action_dispatch.authenticated_encrypted_cookie_salt,
+ "action_dispatch.use_authenticated_cookie_encryption" => config.action_dispatch.use_authenticated_cookie_encryption,
+ "action_dispatch.encrypted_cookie_cipher" => config.action_dispatch.encrypted_cookie_cipher,
+ "action_dispatch.signed_cookie_digest" => config.action_dispatch.signed_cookie_digest,
"action_dispatch.cookies_serializer" => config.action_dispatch.cookies_serializer,
- "action_dispatch.cookies_digest" => config.action_dispatch.cookies_digest
- })
+ "action_dispatch.cookies_digest" => config.action_dispatch.cookies_digest,
+ "action_dispatch.cookies_rotations" => config.action_dispatch.cookies_rotations,
+ "action_dispatch.use_cookies_with_metadata" => config.action_dispatch.use_cookies_with_metadata,
+ "action_dispatch.content_security_policy" => config.content_security_policy,
+ "action_dispatch.content_security_policy_report_only" => config.content_security_policy_report_only,
+ "action_dispatch.content_security_policy_nonce_generator" => config.content_security_policy_nonce_generator
+ )
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
@@ -274,7 +287,7 @@ module Rails
# Sends the initializers to the +initializer+ method defined in the
# Rails::Initializable module. Each Rails::Application class has its own
# set of initializers, as defined by the Initializable module.
- def initializer(name, opts={}, &block)
+ def initializer(name, opts = {}, &block)
self.class.initializer(name, opts, &block)
end
@@ -317,7 +330,7 @@ module Rails
# Rails application, you will need to add lib to $LOAD_PATH on your own in case
# you need to load files in lib/ during the application configuration as well.
def self.add_lib_to_load_path!(root) #:nodoc:
- path = File.join root, 'lib'
+ path = File.join root, "lib"
if File.exist?(path) && !$LOAD_PATH.include?(path)
$LOAD_PATH.unshift(path)
end
@@ -347,7 +360,7 @@ module Rails
# Initialize the application passing the given group. By default, the
# group is :default
- def initialize!(group=:default) #:nodoc:
+ def initialize!(group = :default) #:nodoc:
raise "Application has been already initialized." if @initialized
run_initializers(group, self)
@initialized = true
@@ -364,9 +377,7 @@ module Rails
@config ||= Application::Configuration.new(self.class.find_root(self.class.called_from))
end
- def config=(configuration) #:nodoc:
- @config = configuration
- end
+ attr_writer :config
# Returns secrets added to config/secrets.yml.
#
@@ -385,25 +396,90 @@ 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) || {}
- env_secrets = all_secrets[Rails.env]
- secrets.merge!(env_secrets.symbolize_keys) if env_secrets
- end
+ files = config.paths["config/secrets"].existent
+ files = files.reject { |path| path.end_with?(".enc") } unless config.read_encrypted_secrets
+ secrets.merge! Rails::Secrets.parse(files, 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
# Fallback to config.secret_token if secrets.secret_token isn't set
secrets.secret_token ||= config.secret_token
+ if secrets.secret_token.present?
+ ActiveSupport::Deprecation.warn(
+ "`secrets.secret_token` is deprecated in favor of `secret_key_base` and will be removed in Rails 6.0."
+ )
+ end
+
secrets
end
end
- def secrets=(secrets) #:nodoc:
- @secrets = secrets
+ attr_writer :secrets
+
+ # The secret_key_base is used as the input secret to the application's key generator, which in turn
+ # is used to create all MessageVerifiers/MessageEncryptors, including the ones that sign and encrypt cookies.
+ #
+ # In test and development, this is simply derived as a MD5 hash of the application's name.
+ #
+ # In all other environments, we look for it first in ENV["SECRET_KEY_BASE"],
+ # then credentials.secret_key_base, and finally secrets.secret_key_base. For most applications,
+ # the correct place to store it is in the encrypted credentials file.
+ def secret_key_base
+ if Rails.env.test? || Rails.env.development?
+ secrets.secret_key_base || Digest::MD5.hexdigest(self.class.name)
+ else
+ validate_secret_key_base(
+ ENV["SECRET_KEY_BASE"] || credentials.secret_key_base || secrets.secret_key_base
+ )
+ end
+ end
+
+ # Decrypts the credentials hash as kept in +config/credentials.yml.enc+. This file is encrypted with
+ # the Rails master key, which is either taken from <tt>ENV["RAILS_MASTER_KEY"]</tt> or from loading
+ # +config/master.key+.
+ # If specific credentials file exists for current environment, it takes precedence, thus for +production+
+ # environment look first for +config/credentials/production.yml.enc+ with master key taken
+ # from <tt>ENV["RAILS_MASTER_KEY"]</tt> or from loading +config/credentials/production.key+.
+ # Default behavior can be overwritten by setting +config.credentials.content_path+ and +config.credentials.key_path+.
+ def credentials
+ @credentials ||= encrypted(config.credentials.content_path, key_path: config.credentials.key_path)
+ end
+
+ # Shorthand to decrypt any encrypted configurations or files.
+ #
+ # For any file added with <tt>rails encrypted:edit</tt> call +read+ to decrypt
+ # the file with the master key.
+ # The master key is either stored in +config/master.key+ or <tt>ENV["RAILS_MASTER_KEY"]</tt>.
+ #
+ # Rails.application.encrypted("config/mystery_man.txt.enc").read
+ # # => "We've met before, haven't we?"
+ #
+ # It's also possible to interpret encrypted YAML files with +config+.
+ #
+ # Rails.application.encrypted("config/credentials.yml.enc").config
+ # # => { next_guys_line: "I don't think so. Where was it you think we met?" }
+ #
+ # Any top-level configs are also accessible directly on the return value:
+ #
+ # Rails.application.encrypted("config/credentials.yml.enc").next_guys_line
+ # # => "I don't think so. Where was it you think we met?"
+ #
+ # The files or configs can also be encrypted with a custom key. To decrypt with
+ # a key in the +ENV+, use:
+ #
+ # Rails.application.encrypted("config/special_tokens.yml.enc", env_key: "SPECIAL_TOKENS")
+ #
+ # Or to decrypt with a file, that should be version control ignored, relative to +Rails.root+:
+ #
+ # Rails.application.encrypted("config/special_tokens.yml.enc", key_path: "config/special_tokens.key")
+ def encrypted(path, key_path: "config/master.key", env_key: "RAILS_MASTER_KEY")
+ ActiveSupport::EncryptedConfiguration.new(
+ config_path: Rails.root.join(path),
+ key_path: Rails.root.join(key_path),
+ env_key: env_key,
+ raise_if_missing_key: config.require_master_key
+ )
end
def to_app #:nodoc:
@@ -504,15 +580,27 @@ module Rails
default_stack.build_stack
end
- def validate_secret_key_config! #:nodoc:
- if secrets.secret_key_base.blank?
- 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?
- raise "Missing `secret_key_base` for '#{Rails.env}' environment, set this value in `config/secrets.yml`"
- end
+ def validate_secret_key_base(secret_key_base)
+ if secret_key_base.is_a?(String) && secret_key_base.present?
+ secret_key_base
+ elsif secret_key_base
+ raise ArgumentError, "`secret_key_base` for #{Rails.env} environment must be a type of String`"
+ elsif secrets.secret_token.blank?
+ raise ArgumentError, "Missing `secret_key_base` for '#{Rails.env}' environment, set this string with `rails credentials:edit`"
end
end
+
+ private
+
+ def build_request(env)
+ req = super
+ env["ORIGINAL_FULLPATH"] = req.fullpath
+ env["ORIGINAL_SCRIPT_NAME"] = req.script_name
+ req
+ end
+
+ def build_middleware
+ config.app_middleware + super
+ end
end
end
diff --git a/railties/lib/rails/application/bootstrap.rb b/railties/lib/rails/application/bootstrap.rb
index 0f4d932749..e3c0759f95 100644
--- a/railties/lib/rails/application/bootstrap.rb
+++ b/railties/lib/rails/application/bootstrap.rb
@@ -1,6 +1,10 @@
+# frozen_string_literal: true
+
+require "fileutils"
require "active_support/notifications"
require "active_support/dependencies"
require "active_support/descendants_tracker"
+require "rails/secrets"
module Rails
class Application
@@ -35,7 +39,7 @@ INFO
FileUtils.mkdir_p File.dirname path
end
- f = File.open path, 'a'
+ f = File.open path, "a"
f.binmode
f.sync = config.autoflush_log # if true make sure every write flushes
@@ -47,8 +51,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
@@ -63,7 +67,7 @@ INFO
Rails.cache = ActiveSupport::Cache.lookup_store(config.cache_store)
if Rails.cache.respond_to?(:middleware)
- config.middleware.insert_before("Rack::Runtime", Rails.cache.middleware)
+ config.middleware.insert_before(::Rack::Runtime, Rails.cache.middleware)
end
end
end
@@ -76,6 +80,10 @@ 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
+ end
end
end
end
diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb
index 4fc7a1db62..d6f8c4f47c 100644
--- a/railties/lib/rails/application/configuration.rb
+++ b/railties/lib/rails/application/configuration.rb
@@ -1,7 +1,9 @@
-require 'active_support/core_ext/kernel/reporting'
-require 'active_support/file_update_checker'
-require 'rails/engine/configuration'
-require 'rails/source_annotation_extractor'
+# frozen_string_literal: true
+
+require "active_support/core_ext/kernel/reporting"
+require "active_support/file_update_checker"
+require "rails/engine/configuration"
+require "rails/source_annotation_extractor"
module Rails
class Application
@@ -11,46 +13,125 @@ module Rails
:eager_load, :exceptions_app, :file_watcher, :filter_parameters,
:force_ssl, :helpers_paths, :logger, :log_formatter, :log_tags,
:railties_order, :relative_url_root, :secret_key_base, :secret_token,
- :serve_static_files, :ssl_options, :static_cache_control, :static_index,
+ :ssl_options, :public_file_server,
:session_options, :time_zone, :reload_classes_only_on_change,
- :beginning_of_week, :filter_redirect, :x
+ :beginning_of_week, :filter_redirect, :x, :enable_dependency_loading,
+ :read_encrypted_secrets, :log_level, :content_security_policy_report_only,
+ :content_security_policy_nonce_generator, :require_master_key, :credentials
- attr_writer :log_level
- attr_reader :encoding, :api_only
+ attr_reader :encoding, :api_only, :loaded_config_version
def initialize(*)
super
- self.encoding = "utf-8"
- @allow_concurrency = nil
- @consider_all_requests_local = false
- @filter_parameters = []
- @filter_redirect = []
- @helpers_paths = []
- @serve_static_files = true
- @static_cache_control = nil
- @static_index = "index"
- @force_ssl = false
- @ssl_options = {}
- @session_store = :cookie_store
- @session_options = {}
- @time_zone = "UTC"
- @beginning_of_week = :monday
- @log_level = nil
- @middleware = app_middleware
- @generators = app_generators
- @cache_store = [ :file_store, "#{root}/tmp/cache/" ]
- @railties_order = [:all]
- @relative_url_root = ENV["RAILS_RELATIVE_URL_ROOT"]
- @reload_classes_only_on_change = true
- @file_watcher = ActiveSupport::FileUpdateChecker
- @exceptions_app = nil
- @autoflush_log = true
- @log_formatter = ActiveSupport::Logger::SimpleFormatter.new
- @eager_load = nil
- @secret_token = nil
- @secret_key_base = nil
- @api_only = false
- @x = Custom.new
+ self.encoding = Encoding::UTF_8
+ @allow_concurrency = nil
+ @consider_all_requests_local = false
+ @filter_parameters = []
+ @filter_redirect = []
+ @helpers_paths = []
+ @public_file_server = ActiveSupport::OrderedOptions.new
+ @public_file_server.enabled = true
+ @public_file_server.index_name = "index"
+ @force_ssl = false
+ @ssl_options = {}
+ @session_store = nil
+ @time_zone = "UTC"
+ @beginning_of_week = :monday
+ @log_level = :debug
+ @generators = app_generators
+ @cache_store = [ :file_store, "#{root}/tmp/cache/" ]
+ @railties_order = [:all]
+ @relative_url_root = ENV["RAILS_RELATIVE_URL_ROOT"]
+ @reload_classes_only_on_change = true
+ @file_watcher = ActiveSupport::FileUpdateChecker
+ @exceptions_app = nil
+ @autoflush_log = true
+ @log_formatter = ActiveSupport::Logger::SimpleFormatter.new
+ @eager_load = nil
+ @secret_token = nil
+ @secret_key_base = nil
+ @api_only = false
+ @debug_exception_response_format = nil
+ @x = Custom.new
+ @enable_dependency_loading = false
+ @read_encrypted_secrets = false
+ @content_security_policy = nil
+ @content_security_policy_report_only = false
+ @content_security_policy_nonce_generator = nil
+ @require_master_key = false
+ @loaded_config_version = nil
+ @credentials = ActiveSupport::OrderedOptions.new
+ @credentials.content_path = default_credentials_content_path
+ @credentials.key_path = default_credentials_key_path
+ end
+
+ 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
+
+ ActiveSupport.to_time_preserves_timezone = true
+
+ if respond_to?(:active_record)
+ active_record.belongs_to_required_by_default = true
+ end
+
+ self.ssl_options = { hsts: { subdomains: true } }
+ when "5.1"
+ load_defaults "5.0"
+
+ if respond_to?(:assets)
+ assets.unknown_asset_fallback = false
+ end
+
+ if respond_to?(:action_view)
+ action_view.form_with_generates_remote_forms = true
+ end
+ when "5.2"
+ load_defaults "5.1"
+
+ if respond_to?(:active_record)
+ active_record.cache_versioning = true
+ # Remove the temporary load hook from SQLite3Adapter when this is removed
+ ActiveSupport.on_load(:active_record_sqlite3adapter) do
+ ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = true
+ end
+ end
+
+ if respond_to?(:action_dispatch)
+ action_dispatch.use_authenticated_cookie_encryption = true
+ end
+
+ if respond_to?(:active_support)
+ active_support.use_authenticated_message_encryption = true
+ active_support.use_sha1_digests = true
+ end
+
+ if respond_to?(:action_controller)
+ action_controller.default_protect_from_forgery = true
+ end
+
+ if respond_to?(:action_view)
+ action_view.form_with_generates_ids = true
+ end
+ when "6.0"
+ load_defaults "5.2"
+
+ if respond_to?(:action_view)
+ action_view.default_enforce_utf8 = false
+ end
+
+ if respond_to?(:action_dispatch)
+ action_dispatch.use_cookies_with_metadata = true
+ end
+ else
+ raise "Unknown version #{target_version.to_s.inspect}"
+ end
+
+ @loaded_config_version = target_version
end
def encoding=(value)
@@ -64,13 +145,21 @@ module Rails
def api_only=(value)
@api_only = value
generators.api_only = value
+
+ @debug_exception_response_format ||= :api
+ end
+
+ def debug_exception_response_format
+ @debug_exception_response_format || :default
end
+ attr_writer :debug_exception_response_format
+
def paths
@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"
@@ -83,7 +172,7 @@ module Rails
end
# Loads and returns the entire raw configuration of database from
- # values stored in `config/database.yml`.
+ # values stored in <tt>config/database.yml</tt>.
def database_configuration
path = paths["config/database"].existent.first
yaml = Pathname.new(path) if path
@@ -91,8 +180,15 @@ module Rails
config = if yaml && yaml.exist?
require "yaml"
require "erb"
- YAML.load(ERB.new(yaml.read).result) || {}
- elsif ENV['DATABASE_URL']
+ loaded_yaml = YAML.load(ERB.new(yaml.read).result) || {}
+ shared = loaded_yaml.delete("shared")
+ if shared
+ loaded_yaml.each do |_k, values|
+ values.reverse_merge!(shared)
+ end
+ end
+ Hash.new(shared).merge(loaded_yaml)
+ elsif ENV["DATABASE_URL"]
# Value from ENV['DATABASE_URL'] is set to default database connection
# by Active Record.
{}
@@ -106,11 +202,7 @@ module Rails
"Please note that YAML must be consistently indented using spaces. Tabs are not allowed. " \
"Error: #{e.message}"
rescue => e
- raise e, "Cannot load `Rails.application.database_configuration`:\n#{e.message}", e.backtrace
- end
-
- def log_level
- @log_level ||= (Rails.env.production? ? :info : :debug)
+ raise e, "Cannot load database configuration:\n#{e.message}", e.backtrace
end
def colorize_logging
@@ -119,50 +211,90 @@ module Rails
def colorize_logging=(val)
ActiveSupport::LogSubscriber.colorize_logging = val
- self.generators.colorize_logging = val
+ generators.colorize_logging = val
end
- def session_store(*args)
- if args.empty?
- case @session_store
- when :disabled
- nil
- when :active_record_store
+ def session_store(new_session_store = nil, **options)
+ if new_session_store
+ if new_session_store == :active_record_store
begin
ActionDispatch::Session::ActiveRecordStore
rescue NameError
raise "`ActiveRecord::SessionStore` is extracted out of Rails into a gem. " \
"Please add `activerecord-session_store` to your Gemfile to use it."
end
+ end
+
+ @session_store = new_session_store
+ @session_options = options || {}
+ else
+ case @session_store
+ when :disabled
+ nil
+ when :active_record_store
+ ActionDispatch::Session::ActiveRecordStore
when Symbol
ActionDispatch::Session.const_get(@session_store.to_s.camelize)
else
@session_store
end
- else
- @session_store = args.shift
- @session_options = args.shift || {}
end
end
+ def session_store? #:nodoc:
+ @session_store
+ end
+
def annotations
- SourceAnnotationExtractor::Annotation
+ Rails::SourceAnnotationExtractor::Annotation
+ end
+
+ def content_security_policy(&block)
+ if block_given?
+ @content_security_policy = ActionDispatch::ContentSecurityPolicy.new(&block)
+ else
+ @content_security_policy
+ end
+ end
+
+ class Custom #:nodoc:
+ def initialize
+ @configurations = Hash.new
+ end
+
+ def method_missing(method, *args)
+ if method =~ /=$/
+ @configurations[$`.to_sym] = args.first
+ else
+ @configurations.fetch(method) {
+ @configurations[method] = ActiveSupport::OrderedOptions.new
+ }
+ end
+ end
+
+ def respond_to_missing?(symbol, *)
+ true
+ end
end
private
- class Custom #:nodoc:
- def initialize
- @configurations = Hash.new
- end
-
- def method_missing(method, *args)
- if method =~ /=$/
- @configurations[$`.to_sym] = args.first
- else
- @configurations.fetch(method) {
- @configurations[method] = ActiveSupport::OrderedOptions.new
- }
- end
+ def credentials_available_for_current_env?
+ File.exist?("#{root}/config/credentials/#{Rails.env}.yml.enc")
+ end
+
+ def default_credentials_content_path
+ if credentials_available_for_current_env?
+ File.join(root, "config", "credentials", "#{Rails.env}.yml.enc")
+ else
+ File.join(root, "config", "credentials.yml.enc")
+ end
+ end
+
+ def default_credentials_key_path
+ if credentials_available_for_current_env?
+ File.join(root, "config", "credentials", "#{Rails.env}.key")
+ else
+ File.join(root, "config", "master.key")
end
end
end
diff --git a/railties/lib/rails/application/default_middleware_stack.rb b/railties/lib/rails/application/default_middleware_stack.rb
index 88eade5c5a..433a7ab41f 100644
--- a/railties/lib/rails/application/default_middleware_stack.rb
+++ b/railties/lib/rails/application/default_middleware_stack.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Rails
class Application
class DefaultMiddlewareStack
@@ -10,15 +12,17 @@ module Rails
end
def build_stack
- ActionDispatch::MiddlewareStack.new.tap do |middleware|
+ ActionDispatch::MiddlewareStack.new do |middleware|
if config.force_ssl
middleware.use ::ActionDispatch::SSL, config.ssl_options
end
middleware.use ::Rack::Sendfile, config.action_dispatch.x_sendfile_header
- if config.serve_static_files
- middleware.use ::ActionDispatch::Static, paths["public"].first, config.static_cache_control, index: config.static_index
+ if config.public_file_server.enabled
+ headers = config.public_file_server.headers || {}
+
+ middleware.use ::ActionDispatch::Static, paths["public"].first, index: config.public_file_server.index_name, headers: headers
end
if rack_cache = load_rack_cache
@@ -31,68 +35,56 @@ module Rails
# handling: presumably their code is not threadsafe
middleware.use ::Rack::Lock
-
- elsif config.allow_concurrency == :unsafe
- # Do nothing, even if we know this is dangerous. This is the
- # historical behaviour for true.
-
- else
- # Default concurrency setting: enabled, but safe
-
- unless config.cache_classes && config.eager_load
- # Without cache_classes + eager_load, the load interlock
- # is required for proper operation
-
- middleware.use ::ActionDispatch::LoadInterlock
- end
end
+ middleware.use ::ActionDispatch::Executor, app.executor
+
middleware.use ::Rack::Runtime
middleware.use ::Rack::MethodOverride unless config.api_only
middleware.use ::ActionDispatch::RequestId
+ 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
- middleware.use ::ActionDispatch::RemoteIp, config.action_dispatch.ip_spoofing_check, config.action_dispatch.trusted_proxies
+ middleware.use ::ActionDispatch::DebugExceptions, app, config.debug_exception_response_format
unless config.cache_classes
- middleware.use ::ActionDispatch::Reloader, lambda { reload_dependencies? }
+ middleware.use ::ActionDispatch::Reloader, app.reloader
end
middleware.use ::ActionDispatch::Callbacks
middleware.use ::ActionDispatch::Cookies unless config.api_only
if !config.api_only && config.session_store
- if config.force_ssl && !config.session_options.key?(:secure)
+ if config.force_ssl && config.ssl_options.fetch(:secure_cookies, true) && !config.session_options.key?(:secure)
config.session_options[:secure] = true
end
middleware.use config.session_store, config.session_options
middleware.use ::ActionDispatch::Flash
end
- middleware.use ::ActionDispatch::ParamsParser
+ unless config.api_only
+ middleware.use ::ActionDispatch::ContentSecurityPolicy::Middleware
+ end
+
middleware.use ::Rack::Head
middleware.use ::Rack::ConditionalGet
middleware.use ::Rack::ETag, "no-cache"
+
+ middleware.use ::Rack::TempfileReaper unless config.api_only
end
end
private
- def reload_dependencies?
- config.reload_classes_only_on_change != true || app.reloaders.map(&:updated?).any?
- end
-
def load_rack_cache
rack_cache = config.action_dispatch.rack_cache
return unless rack_cache
begin
- require 'rack/cache'
+ require "rack/cache"
rescue LoadError => error
- error.message << ' Be sure to add rack-cache to your Gemfile'
+ error.message << " Be sure to add rack-cache to your Gemfile"
raise
end
diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb
index f8f92792a7..04aaf6dd9a 100644
--- a/railties/lib/rails/application/finisher.rb
+++ b/railties/lib/rails/application/finisher.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Rails
class Application
module Finisher
@@ -21,15 +23,26 @@ module Rails
initializer :add_builtin_route do |app|
if Rails.env.development?
+ app.routes.prepend do
+ get "/rails/info/properties" => "rails/info#properties", internal: true
+ get "/rails/info/routes" => "rails/info#routes", internal: true
+ get "/rails/info" => "rails/info#index", internal: true
+ end
+
app.routes.append do
- get '/rails/info/properties' => "rails/info#properties"
- get '/rails/info/routes' => "rails/info#routes"
- get '/rails/info' => "rails/info#index"
- get '/' => "rails/welcome#index"
+ get "/" => "rails/welcome#index", internal: true
end
end
end
+ # Setup default session store if not already set in config/application.rb
+ initializer :setup_default_session_store, before: :build_middleware_stack do |app|
+ unless app.config.session_store?
+ app_name = app.class.name ? app.railtie_name.chomp("_application") : ""
+ app.config.session_store :cookie_store, key: "_#{app_name}_session"
+ end
+ end
+
initializer :build_middleware_stack do
build_middleware_stack
end
@@ -38,16 +51,16 @@ module Rails
app.routes.define_mounted_helper(:main_app)
end
- initializer :add_to_prepare_blocks do
+ initializer :add_to_prepare_blocks do |app|
config.to_prepare_blocks.each do |block|
- ActionDispatch::Reloader.to_prepare(&block)
+ app.reloader.to_prepare(&block)
end
end
# This needs to happen before eager load so it happens
- # in exactly the same point regardless of config.cache_classes
- initializer :run_prepare_callbacks do
- ActionDispatch::Reloader.prepare!
+ # in exactly the same point regardless of config.eager_load
+ initializer :run_prepare_callbacks do |app|
+ app.reloader.prepare!
end
initializer :eager_load! do
@@ -62,13 +75,61 @@ module Rails
ActiveSupport.run_load_hooks(:after_initialize, self)
end
+ class MutexHook
+ def initialize(mutex = Mutex.new)
+ @mutex = mutex
+ end
+
+ def run
+ @mutex.lock
+ end
+
+ def complete(_state)
+ @mutex.unlock
+ end
+ end
+
+ module InterlockHook
+ def self.run
+ ActiveSupport::Dependencies.interlock.start_running
+ end
+
+ def self.complete(_state)
+ ActiveSupport::Dependencies.interlock.done_running
+ end
+ end
+
+ initializer :configure_executor_for_concurrency do |app|
+ if config.allow_concurrency == false
+ # User has explicitly opted out of concurrent request
+ # handling: presumably their code is not threadsafe
+
+ app.executor.register_hook(MutexHook.new, outer: true)
+
+ elsif config.allow_concurrency == :unsafe
+ # Do nothing, even if we know this is dangerous. This is the
+ # historical behavior for true.
+
+ else
+ # Default concurrency setting: enabled, but safe
+
+ unless config.cache_classes && config.eager_load
+ # Without cache_classes + eager_load, the load interlock
+ # is required for proper operation
+
+ app.executor.register_hook(InterlockHook, outer: true)
+ end
+ end
+ end
+
# Set routes reload after the finisher hook to ensure routes added in
# the hook are taken into account.
- initializer :set_routes_reloader_hook do
+ initializer :set_routes_reloader_hook do |app|
reloader = routes_reloader
- reloader.execute_if_updated
- self.reloaders << reloader
- ActionDispatch::Reloader.to_prepare do
+ reloader.eager_load = app.config.eager_load
+ reloader.execute
+ reloaders << reloader
+ app.reloader.to_run do
# We configure #execute rather than #execute_if_updated because if
# autoloaded constants are cleared we need to reload routes also in
# case any was used there, as in
@@ -78,42 +139,55 @@ module Rails
# This means routes are also reloaded if i18n is updated, which
# might not be necessary, but in order to be more precise we need
# some sort of reloaders dependency support, to be added.
+ require_unload_lock!
reloader.execute
end
end
# Set clearing dependencies after the finisher hook to ensure paths
# added in the hook are taken into account.
- initializer :set_clear_dependencies_hook, group: :all do
+ initializer :set_clear_dependencies_hook, group: :all do |app|
callback = lambda do
- ActiveSupport::Dependencies.interlock.attempt_loading do
- ActiveSupport::DescendantsTracker.clear
- ActiveSupport::Dependencies.clear
+ ActiveSupport::DescendantsTracker.clear
+ ActiveSupport::Dependencies.clear
+ end
+
+ if config.cache_classes
+ app.reloader.check = lambda { false }
+ elsif config.reload_classes_only_on_change
+ app.reloader.check = lambda do
+ app.reloaders.map(&:updated?).any?
end
+ else
+ app.reloader.check = lambda { true }
end
if config.reload_classes_only_on_change
reloader = config.file_watcher.new(*watchable_args, &callback)
- self.reloaders << reloader
+ reloaders << reloader
# Prepend this callback to have autoloaded constants cleared before
# any other possible reloading, in case they need to autoload fresh
# constants.
- ActionDispatch::Reloader.to_prepare(prepend: true) do
+ app.reloader.to_run(prepend: true) do
# In addition to changes detected by the file watcher, if routes
# or i18n have been updated we also need to clear constants,
# that's why we run #execute rather than #execute_if_updated, this
# callback has to clear autoloaded constants after any update.
- reloader.execute
+ class_unload! do
+ reloader.execute
+ end
end
else
- ActionDispatch::Reloader.to_cleanup(&callback)
+ app.reloader.to_complete do
+ class_unload!(&callback)
+ end
end
end
# Disable dependency loading during request cycle
initializer :disable_dependency_loading do
- if config.eager_load && config.cache_classes
+ if config.eager_load && config.cache_classes && !config.enable_dependency_loading
ActiveSupport::Dependencies.unhook!
end
end
diff --git a/railties/lib/rails/application/routes_reloader.rb b/railties/lib/rails/application/routes_reloader.rb
index cf0a4e128f..3ecb8e264e 100644
--- a/railties/lib/rails/application/routes_reloader.rb
+++ b/railties/lib/rails/application/routes_reloader.rb
@@ -1,20 +1,25 @@
+# frozen_string_literal: true
+
require "active_support/core_ext/module/delegation"
module Rails
class Application
class RoutesReloader
attr_reader :route_sets, :paths
+ attr_accessor :eager_load
delegate :execute_if_updated, :execute, :updated?, to: :updater
def initialize
@paths = []
@route_sets = []
+ @eager_load = false
end
def reload!
clear!
load_paths
finalize!
+ route_sets.each(&:eager_load!) if eager_load
ensure
revert
end
@@ -22,11 +27,7 @@ module Rails
private
def updater
- @updater ||= begin
- updater = ActiveSupport::FileUpdateChecker.new(paths) { reload! }
- updater.execute
- updater
- end
+ @updater ||= ActiveSupport::FileUpdateChecker.new(paths) { reload! }
end
def clear!
diff --git a/railties/lib/rails/application_controller.rb b/railties/lib/rails/application_controller.rb
index 9a29ec21cf..b3fe822218 100644
--- a/railties/lib/rails/application_controller.rb
+++ b/railties/lib/rails/application_controller.rb
@@ -1,16 +1,29 @@
+# frozen_string_literal: true
+
class Rails::ApplicationController < ActionController::Base # :nodoc:
- self.view_paths = File.expand_path('../templates', __FILE__)
- layout 'application'
+ self.view_paths = File.expand_path("templates", __dir__)
+ layout "application"
- protected
+ before_action :disable_content_security_policy_nonce!
- def require_local!
- unless local_request?
- render text: '<p>For security purposes, this information is only available to local requests.</p>', status: :forbidden
- end
+ content_security_policy do |policy|
+ policy.script_src :unsafe_inline
+ policy.style_src :unsafe_inline
end
- def local_request?
- Rails.application.config.consider_all_requests_local || request.local?
- end
+ private
+
+ def require_local!
+ unless local_request?
+ render html: "<p>For security purposes, this information is only available to local requests.</p>".html_safe, status: :forbidden
+ end
+ end
+
+ def local_request?
+ Rails.application.config.consider_all_requests_local || request.local?
+ end
+
+ def disable_content_security_policy_nonce!
+ request.content_security_policy_nonce_generator = nil
+ end
end
diff --git a/railties/lib/rails/backtrace_cleaner.rb b/railties/lib/rails/backtrace_cleaner.rb
index 5276eb33c9..7c2eb1dc42 100644
--- a/railties/lib/rails/backtrace_cleaner.rb
+++ b/railties/lib/rails/backtrace_cleaner.rb
@@ -1,32 +1,22 @@
-require 'active_support/backtrace_cleaner'
+# frozen_string_literal: true
+
+require "active_support/backtrace_cleaner"
module Rails
class BacktraceCleaner < ActiveSupport::BacktraceCleaner
- APP_DIRS_PATTERN = /^\/?(app|config|lib|test)/
- RENDER_TEMPLATE_PATTERN = /:in `_render_template_\w*'/
- EMPTY_STRING = ''.freeze
- SLASH = '/'.freeze
- DOT_SLASH = './'.freeze
+ APP_DIRS_PATTERN = /^\/?(app|config|lib|test|\(\w*\))/
+ RENDER_TEMPLATE_PATTERN = /:in `.*_\w+_{2,3}\d+_\d+'/
+ EMPTY_STRING = ""
+ SLASH = "/"
+ DOT_SLASH = "./"
def initialize
super
- @root = "#{Rails.root}/".freeze
+ @root = "#{Rails.root}/"
add_filter { |line| line.sub(@root, EMPTY_STRING) }
add_filter { |line| line.sub(RENDER_TEMPLATE_PATTERN, EMPTY_STRING) }
add_filter { |line| line.sub(DOT_SLASH, SLASH) } # for tests
-
- add_gem_filters
- add_silencer { |line| line !~ APP_DIRS_PATTERN }
+ add_silencer { |line| !APP_DIRS_PATTERN.match?(line) }
end
-
- private
- def add_gem_filters
- gems_paths = (Gem.path | [Gem.default_dir]).map { |p| Regexp.escape(p) }
- return if gems_paths.empty?
-
- gems_regexp = %r{(#{gems_paths.join('|')})/gems/([^/]+)-([\w.]+)/(.*)}
- gems_result = '\2 (\3) \4'.freeze
- add_filter { |line| line.sub(gems_regexp, gems_result) }
- end
end
end
diff --git a/railties/lib/rails/cli.rb b/railties/lib/rails/cli.rb
index a8794bc0de..e56e604fdc 100644
--- a/railties/lib/rails/cli.rb
+++ b/railties/lib/rails/cli.rb
@@ -1,15 +1,19 @@
-require 'rails/app_loader'
+# frozen_string_literal: true
+
+require "rails/app_loader"
# If we are inside a Rails application this method performs an exec and thus
# the rest of this script is not run.
Rails::AppLoader.exec_app
-require 'rails/ruby_version_check'
+require "rails/ruby_version_check"
Signal.trap("INT") { puts; exit(1) }
-if ARGV.first == 'plugin'
+require "rails/command"
+
+if ARGV.first == "plugin"
ARGV.shift
- require 'rails/commands/plugin'
+ Rails::Command.invoke :plugin, ARGV
else
- require 'rails/commands/application'
+ Rails::Command.invoke :application, ARGV
end
diff --git a/railties/lib/rails/code_statistics.rb b/railties/lib/rails/code_statistics.rb
index fd352dc9b7..19d331ff30 100644
--- a/railties/lib/rails/code_statistics.rb
+++ b/railties/lib/rails/code_statistics.rb
@@ -1,13 +1,18 @@
-require 'rails/code_statistics_calculator'
+# frozen_string_literal: true
+
+require "rails/code_statistics_calculator"
+require "active_support/core_ext/enumerable"
class CodeStatistics #:nodoc:
+ TEST_TYPES = ["Controller tests",
+ "Helper tests",
+ "Model tests",
+ "Mailer tests",
+ "Job tests",
+ "Integration tests",
+ "System tests"]
- TEST_TYPES = ['Controller tests',
- 'Helper tests',
- 'Model tests',
- 'Mailer tests',
- 'Job tests',
- 'Integration tests']
+ HEADERS = { lines: " Lines", code_lines: " LOC", classes: "Classes", methods: "Methods" }
def initialize(*pairs)
@pairs = pairs
@@ -30,10 +35,10 @@ class CodeStatistics #:nodoc:
private
def calculate_statistics
- Hash[@pairs.map{|pair| [pair.first, calculate_directory_statistics(pair.last)]}]
+ Hash[@pairs.map { |pair| [pair.first, calculate_directory_statistics(pair.last)] }]
end
- def calculate_directory_statistics(directory, pattern = /.*\.(rb|js|coffee)$/)
+ def calculate_directory_statistics(directory, pattern = /^(?!\.).*?\.(rb|js|coffee|rake)$/)
stats = CodeStatisticsCalculator.new
Dir.foreach(directory) do |file_name|
@@ -41,7 +46,7 @@ class CodeStatistics #:nodoc:
if File.directory?(path) && (/^\./ !~ file_name)
stats.add(calculate_directory_statistics(path, pattern))
- elsif file_name =~ pattern
+ elsif file_name&.match?(pattern)
stats.add_by_file_path(path)
end
end
@@ -67,34 +72,44 @@ class CodeStatistics #:nodoc:
test_loc
end
+ def width_for(label)
+ [@statistics.values.sum { |s| s.send(label) }.to_s.size, HEADERS[label].length].max
+ end
+
def print_header
print_splitter
- puts "| Name | Lines | LOC | Classes | Methods | M/C | LOC/M |"
+ print "| Name "
+ HEADERS.each do |k, v|
+ print " | #{v.rjust(width_for(k))}"
+ end
+ puts " | M/C | LOC/M |"
print_splitter
end
def print_splitter
- puts "+----------------------+--------+--------+---------+---------+-----+-------+"
+ print "+----------------------"
+ HEADERS.each_key do |k|
+ print "+#{'-' * (width_for(k) + 2)}"
+ end
+ puts "+-----+-------+"
end
def print_line(name, statistics)
m_over_c = (statistics.methods / statistics.classes) rescue m_over_c = 0
loc_over_m = (statistics.code_lines / statistics.methods) - 2 rescue loc_over_m = 0
- puts "| #{name.ljust(20)} " \
- "| #{statistics.lines.to_s.rjust(6)} " \
- "| #{statistics.code_lines.to_s.rjust(6)} " \
- "| #{statistics.classes.to_s.rjust(7)} " \
- "| #{statistics.methods.to_s.rjust(7)} " \
- "| #{m_over_c.to_s.rjust(3)} " \
- "| #{loc_over_m.to_s.rjust(5)} |"
+ print "| #{name.ljust(20)} "
+ HEADERS.each_key do |k|
+ print "| #{statistics.send(k).to_s.rjust(width_for(k))} "
+ end
+ puts "| #{m_over_c.to_s.rjust(3)} | #{loc_over_m.to_s.rjust(5)} |"
end
def print_code_test_stats
code = calculate_code
tests = calculate_tests
- puts " Code LOC: #{code} Test LOC: #{tests} Code to Test Ratio: 1:#{sprintf("%.1f", tests.to_f/code)}"
+ puts " Code LOC: #{code} Test LOC: #{tests} Code to Test Ratio: 1:#{sprintf("%.1f", tests.to_f / code)}"
puts ""
end
end
diff --git a/railties/lib/rails/code_statistics_calculator.rb b/railties/lib/rails/code_statistics_calculator.rb
index a142236dbe..85f86bdbd0 100644
--- a/railties/lib/rails/code_statistics_calculator.rb
+++ b/railties/lib/rails/code_statistics_calculator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CodeStatisticsCalculator #:nodoc:
attr_reader :lines, :code_lines, :classes, :methods
@@ -25,6 +27,7 @@ class CodeStatisticsCalculator #:nodoc:
}
PATTERNS[:minitest] = PATTERNS[:rb].merge method: /^\s*(def|test)\s+['"_a-z]/
+ PATTERNS[:rake] = PATTERNS[:rb]
def initialize(lines = 0, code_lines = 0, classes = 0, methods = 0)
@lines = lines
@@ -42,7 +45,7 @@ class CodeStatisticsCalculator #:nodoc:
def add_by_file_path(file_path)
File.open(file_path) do |f|
- self.add_by_io(f, file_type(file_path))
+ add_by_io(f, file_type(file_path))
end
end
@@ -76,10 +79,10 @@ class CodeStatisticsCalculator #:nodoc:
private
def file_type(file_path)
- if file_path.end_with? '_test.rb'
+ if file_path.end_with? "_test.rb"
:minitest
else
- File.extname(file_path).sub(/\A\./, '').downcase.to_sym
+ File.extname(file_path).sub(/\A\./, "").downcase.to_sym
end
end
end
diff --git a/railties/lib/rails/command.rb b/railties/lib/rails/command.rb
new file mode 100644
index 0000000000..f09aa3ae0d
--- /dev/null
+++ b/railties/lib/rails/command.rb
@@ -0,0 +1,114 @@
+# frozen_string_literal: true
+
+require "active_support"
+require "active_support/dependencies/autoload"
+require "active_support/core_ext/enumerable"
+require "active_support/core_ext/object/blank"
+
+require "thor"
+
+module Rails
+ module Command
+ extend ActiveSupport::Autoload
+
+ autoload :Spellchecker
+ autoload :Behavior
+ autoload :Base
+
+ include Behavior
+
+ HELP_MAPPINGS = %w(-h -? --help)
+
+ class << self
+ def hidden_commands # :nodoc:
+ @hidden_commands ||= []
+ end
+
+ def environment # :nodoc:
+ ENV["RAILS_ENV"].presence || ENV["RACK_ENV"].presence || "development"
+ end
+
+ # Receives a namespace, arguments and the behavior to invoke the command.
+ 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)
+
+ 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(full_namespace, args, config)
+ end
+ end
+
+ # 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.
+ #
+ # find_by_namespace :webrat, :rails, :integration
+ #
+ # Will search for the following commands:
+ #
+ # "rails:webrat", "webrat:integration", "webrat"
+ #
+ # 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(namespace, command_name = nil) # :nodoc:
+ lookups = [ namespace ]
+ lookups << "#{namespace}:#{command_name}" if command_name
+ lookups.concat lookups.map { |lookup| "rails:#{lookup}" }
+
+ lookup(lookups)
+
+ namespaces = subclasses.index_by(&:namespace)
+ namespaces[(lookups & namespaces.keys).first]
+ end
+
+ # Returns the root of the Rails engine or app running the command.
+ def root
+ if defined?(ENGINE_ROOT)
+ Pathname.new(ENGINE_ROOT)
+ elsif defined?(APP_PATH)
+ Pathname.new(File.expand_path("../..", APP_PATH))
+ end
+ end
+
+ def print_commands # :nodoc:
+ commands.each { |command| puts(" #{command}") }
+ end
+
+ private
+ COMMANDS_IN_USAGE = %w(generate console server test test:system dbconsole new)
+ private_constant :COMMANDS_IN_USAGE
+
+ def commands
+ lookup!
+
+ visible_commands = (subclasses - hidden_commands).flat_map(&:printing_commands)
+
+ (visible_commands - COMMANDS_IN_USAGE).sort
+ end
+
+ def command_type # :doc:
+ @command_type ||= "command"
+ end
+
+ def lookup_paths # :doc:
+ @lookup_paths ||= %w( rails/commands commands )
+ end
+
+ def file_lookup_paths # :doc:
+ @file_lookup_paths ||= [ "{#{lookup_paths.join(',')}}", "**", "*_command.rb" ]
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/command/actions.rb b/railties/lib/rails/command/actions.rb
new file mode 100644
index 0000000000..cbb743346b
--- /dev/null
+++ b/railties/lib/rails/command/actions.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module Rails
+ module Command
+ module Actions
+ # Change to the application's path if there is no <tt>config.ru</tt> file in current directory.
+ # This allows us to run <tt>rails server</tt> from other directories, but still get
+ # the main <tt>config.ru</tt> and properly set the <tt>tmp</tt> directory.
+ def set_application_directory!
+ Dir.chdir(File.expand_path("../..", APP_PATH)) unless File.exist?(File.expand_path("config.ru"))
+ end
+
+ 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
+ end
+
+ def load_generators
+ engine = ::Rails::Engine.find(ENGINE_ROOT)
+ Rails::Generators.namespace = engine.railtie_namespace
+ engine.load_generators
+ end
+ else
+ def load_tasks
+ Rails.application.load_tasks
+ end
+
+ def load_generators
+ Rails.application.load_generators
+ end
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/command/base.rb b/railties/lib/rails/command/base.rb
new file mode 100644
index 0000000000..766872de8a
--- /dev/null
+++ b/railties/lib/rails/command/base.rb
@@ -0,0 +1,157 @@
+# frozen_string_literal: true
+
+require "thor"
+require "erb"
+
+require "active_support/core_ext/string/filters"
+require "active_support/core_ext/string/inflections"
+
+require "rails/command/actions"
+
+module Rails
+ module Command
+ class Base < Thor
+ class Error < Thor::Error # :nodoc:
+ end
+
+ include Actions
+
+ class << self
+ # Returns true when the app is a Rails engine.
+ def engine?
+ defined?(ENGINE_ROOT)
+ end
+
+ # Tries to get the description from a USAGE file one folder above the command
+ # root.
+ def desc(usage = nil, description = nil, options = {})
+ if usage
+ super
+ else
+ @desc ||= ERB.new(File.read(usage_path)).result(binding) if usage_path
+ end
+ end
+
+ # Convenience method to get the namespace from the class name. It's the
+ # same as Thor default except that the Command at the end of the class
+ # is removed.
+ def namespace(name = nil)
+ if name
+ super
+ else
+ @namespace ||= super.chomp("_command").sub(/:command:/, ":")
+ end
+ end
+
+ # Convenience method to hide this command from the available ones when
+ # running rails command.
+ def hide_command!
+ Rails::Command.hidden_commands << self
+ end
+
+ def inherited(base) #:nodoc:
+ super
+
+ if base.name && base.name !~ /Base$/
+ Rails::Command.subclasses << base
+ end
+ end
+
+ def perform(command, args, config) # :nodoc:
+ if Rails::Command::HELP_MAPPINGS.include?(args.first)
+ command, args = "help", []
+ end
+
+ dispatch(command, args.dup, nil, config)
+ end
+
+ def printing_commands
+ namespaced_commands
+ end
+
+ def executable
+ "rails #{command_name}"
+ end
+
+ # Use Rails' default banner.
+ def banner(*)
+ "#{executable} #{arguments.map(&:usage).join(' ')} [options]".squish
+ end
+
+ # Sets the base_name taking into account the current class namespace.
+ #
+ # Rails::Command::TestCommand.base_name # => 'rails'
+ def base_name
+ @base_name ||= begin
+ if base = name.to_s.split("::").first
+ base.underscore
+ end
+ end
+ end
+
+ # Return command name without namespaces.
+ #
+ # Rails::Command::TestCommand.command_name # => 'test'
+ def command_name
+ @command_name ||= begin
+ if command = name.to_s.split("::").last
+ command.chomp!("Command")
+ command.underscore
+ end
+ end
+ end
+
+ # Path to lookup a USAGE description in a file.
+ def usage_path
+ if default_command_root
+ path = File.join(default_command_root, "USAGE")
+ path if File.exist?(path)
+ end
+ end
+
+ # Default file root to place extra files a command might need, placed
+ # one folder above the command file.
+ #
+ # For a Rails::Command::TestCommand placed in <tt>rails/command/test_command.rb</tt>
+ # would return <tt>rails/test</tt>.
+ def default_command_root
+ path = File.expand_path(File.join("../commands", command_root_namespace), __dir__)
+ path if File.exist?(path)
+ end
+
+ private
+ # Allow the command method to be called perform.
+ def create_command(meth)
+ if meth == "perform"
+ alias_method command_name, meth
+ else
+ # Prevent exception about command without usage.
+ # Some commands define their documentation differently.
+ @usage ||= ""
+ @desc ||= ""
+
+ 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
+end
diff --git a/railties/lib/rails/command/behavior.rb b/railties/lib/rails/command/behavior.rb
new file mode 100644
index 0000000000..718e2d9ab2
--- /dev/null
+++ b/railties/lib/rails/command/behavior.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+require "active_support"
+
+module Rails
+ module Command
+ module Behavior #:nodoc:
+ extend ActiveSupport::Concern
+
+ class_methods do
+ # Remove the color from output.
+ def no_color!
+ Thor::Base.shell = Thor::Shell::Basic
+ end
+
+ # Track all command subclasses.
+ def subclasses
+ @subclasses ||= []
+ end
+
+ private
+ # Prints a list of generators.
+ def print_list(base, namespaces)
+ return if namespaces.empty?
+ puts "#{base.camelize}:"
+
+ namespaces.each do |namespace|
+ puts(" #{namespace}")
+ end
+
+ puts
+ end
+
+ # Receives namespaces in an array and tries to find matching generators
+ # in the load path.
+ def lookup(namespaces)
+ paths = namespaces_to_paths(namespaces)
+
+ paths.each do |raw_path|
+ lookup_paths.each do |base|
+ path = "#{base}/#{raw_path}_#{command_type}"
+
+ begin
+ require path
+ return
+ rescue LoadError => e
+ raise unless e.message =~ /#{Regexp.escape(path)}$/
+ rescue Exception => e
+ warn "[WARNING] Could not load #{command_type} #{path.inspect}. Error: #{e.message}.\n#{e.backtrace.join("\n")}"
+ end
+ end
+ end
+ end
+
+ # This will try to load any command in the load path to show in help.
+ def lookup!
+ $LOAD_PATH.each do |base|
+ Dir[File.join(base, *file_lookup_paths)].each do |path|
+ begin
+ path = path.sub("#{base}/", "")
+ require path
+ rescue Exception
+ # No problem
+ end
+ end
+ end
+ end
+
+ # 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)
+ paths = []
+ namespaces.each do |namespace|
+ pieces = namespace.split(":")
+ paths << pieces.dup.push(pieces.last).join("/")
+ paths << pieces.join("/")
+ end
+ paths.uniq!
+ paths
+ end
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/command/environment_argument.rb b/railties/lib/rails/command/environment_argument.rb
new file mode 100644
index 0000000000..5dc98b113d
--- /dev/null
+++ b/railties/lib/rails/command/environment_argument.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require "active_support"
+
+module Rails
+ module Command
+ module EnvironmentArgument #:nodoc:
+ extend ActiveSupport::Concern
+
+ included do
+ argument :environment, optional: true, banner: "environment"
+
+ class_option :environment, aliases: "-e", type: :string,
+ desc: "Specifies the environment to run this console under (test/development/production)."
+ end
+
+ private
+ def extract_environment_option_from_argument
+ if environment
+ self.options = options.merge(environment: acceptable_environment(environment))
+
+ ActiveSupport::Deprecation.warn "Passing the environment's name as a " \
+ "regular argument is deprecated and " \
+ "will be removed in the next Rails " \
+ "version. Please, use the -e option " \
+ "instead."
+ elsif options[:environment]
+ self.options = options.merge(environment: acceptable_environment(options[:environment]))
+ else
+ self.options = options.merge(environment: Rails::Command.environment)
+ end
+ end
+
+ def acceptable_environment(env = nil)
+ if available_environments.include? env
+ env
+ else
+ %w( production development test ).detect { |e| e =~ /^#{env}/ } || env
+ end
+ end
+
+ def available_environments
+ Dir["config/environments/*.rb"].map { |fname| File.basename(fname, ".*") }
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/command/helpers/editor.rb b/railties/lib/rails/command/helpers/editor.rb
new file mode 100644
index 0000000000..6191d97672
--- /dev/null
+++ b/railties/lib/rails/command/helpers/editor.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require "active_support/encrypted_file"
+
+module Rails
+ module Command
+ module Helpers
+ module Editor
+ private
+ def ensure_editor_available(command:)
+ if ENV["EDITOR"].to_s.empty?
+ say "No $EDITOR to open file in. Assign one like this:"
+ say ""
+ say %(EDITOR="mate --wait" #{command})
+ say ""
+ say "For editors that fork and exit immediately, it's important to pass a wait flag,"
+ say "otherwise the credentials will be saved immediately with no chance to edit."
+
+ false
+ else
+ true
+ end
+ end
+
+ def catch_editing_exceptions
+ yield
+ rescue Interrupt
+ say "Aborted changing file: nothing saved."
+ rescue ActiveSupport::EncryptedFile::MissingKeyError => error
+ say error.message
+ end
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/command/spellchecker.rb b/railties/lib/rails/command/spellchecker.rb
new file mode 100644
index 0000000000..085d5b16df
--- /dev/null
+++ b/railties/lib/rails/command/spellchecker.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+module Rails
+ module Command
+ module Spellchecker # :nodoc:
+ class << self
+ def suggest(word, from:)
+ if defined?(DidYouMean::SpellChecker)
+ DidYouMean::SpellChecker.new(dictionary: from.map(&:to_s)).correct(word).first
+ else
+ from.sort_by { |w| levenshtein_distance(word, w) }.first
+ end
+ end
+
+ private
+
+ # This code is based directly on the Text gem implementation.
+ # Copyright (c) 2006-2013 Paul Battley, Michael Neumann, Tim Fletcher.
+ #
+ # Returns a value representing the "cost" of transforming str1 into str2.
+ def levenshtein_distance(str1, str2) # :doc:
+ s = str1
+ t = str2
+ n = s.length
+ m = t.length
+
+ return m if 0 == n
+ return n if 0 == m
+
+ d = (0..m).to_a
+ x = nil
+
+ # avoid duplicating an enumerable object in the loop
+ str2_codepoint_enumerable = str2.each_codepoint
+
+ str1.each_codepoint.with_index do |char1, i|
+ e = i + 1
+
+ str2_codepoint_enumerable.with_index do |char2, j|
+ cost = (char1 == char2) ? 0 : 1
+ x = [
+ d[j + 1] + 1, # insertion
+ e + 1, # deletion
+ d[j] + cost # substitution
+ ].min
+ d[j] = e
+ e = x
+ end
+
+ d[m] = x
+ end
+
+ x
+ end
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/commands.rb b/railties/lib/rails/commands.rb
index 12bd73db24..77961a0292 100644
--- a/railties/lib/rails/commands.rb
+++ b/railties/lib/rails/commands.rb
@@ -1,4 +1,6 @@
-ARGV << '--help' if ARGV.empty?
+# frozen_string_literal: true
+
+require "rails/command"
aliases = {
"g" => "generate",
@@ -7,12 +9,10 @@ aliases = {
"s" => "server",
"db" => "dbconsole",
"r" => "runner",
- "t" => "test",
+ "t" => "test"
}
command = ARGV.shift
command = aliases[command] || command
-require 'rails/commands/commands_tasks'
-
-Rails::CommandsTasks.new(ARGV).run_command!(command)
+Rails::Command.invoke command, ARGV
diff --git a/railties/lib/rails/commands/application.rb b/railties/lib/rails/commands/application.rb
deleted file mode 100644
index c998e6b6a8..0000000000
--- a/railties/lib/rails/commands/application.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-require 'rails/generators'
-require 'rails/generators/rails/app/app_generator'
-
-module Rails
- module Generators
- class AppGenerator # :nodoc:
- # We want to exit on failure to be kind to other libraries
- # This is only when accessing via CLI
- def self.exit_on_failure?
- true
- end
- end
- end
-end
-
-args = Rails::Generators::ARGVScrubber.new(ARGV).prepare!
-Rails::Generators::AppGenerator.start args
diff --git a/railties/lib/rails/commands/application/application_command.rb b/railties/lib/rails/commands/application/application_command.rb
new file mode 100644
index 0000000000..f77553b830
--- /dev/null
+++ b/railties/lib/rails/commands/application/application_command.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require "rails/generators"
+require "rails/generators/rails/app/app_generator"
+
+module Rails
+ module Generators
+ class AppGenerator # :nodoc:
+ # We want to exit on failure to be kind to other libraries
+ # This is only when accessing via CLI
+ def self.exit_on_failure?
+ true
+ end
+ end
+ end
+
+ module Command
+ class ApplicationCommand < Base # :nodoc:
+ hide_command!
+
+ def help
+ perform # Punt help output to the generator.
+ end
+
+ def perform(*args)
+ Rails::Generators::AppGenerator.start \
+ Rails::Generators::ARGVScrubber.new(args).prepare!
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/commands/commands_tasks.rb b/railties/lib/rails/commands/commands_tasks.rb
deleted file mode 100644
index 685d55eea8..0000000000
--- a/railties/lib/rails/commands/commands_tasks.rb
+++ /dev/null
@@ -1,185 +0,0 @@
-module Rails
- # This is a class which takes in a rails command and initiates the appropriate
- # initiation sequence.
- #
- # Warning: This class mutates ARGV because some commands require manipulating
- # it before they are run.
- class CommandsTasks # :nodoc:
- attr_reader :argv
-
- HELP_MESSAGE = <<-EOT
-Usage: rails COMMAND [ARGS]
-
-The most common rails commands are:
- generate Generate new code (short-cut alias: "g")
- console Start the Rails console (short-cut alias: "c")
- server Start the Rails server (short-cut alias: "s")
- test Run tests (short-cut alias: "t")
- dbconsole Start a console for the database specified in config/database.yml
- (short-cut alias: "db")
- new Create a new Rails application. "rails new my_app" creates a
- new application called MyApp in "./my_app"
-
-In addition to those, there are:
- destroy Undo code generated with "generate" (short-cut alias: "d")
- plugin new Generates skeleton for developing a Rails plugin
- runner Run a piece of code in the application environment (short-cut alias: "r")
-
-All commands can be run with -h (or --help) for more information.
-EOT
-
- COMMAND_WHITELIST = %w(plugin generate destroy console server dbconsole runner new version help test)
-
- def initialize(argv)
- @argv = argv
- end
-
- def run_command!(command)
- command = parse_command(command)
- if COMMAND_WHITELIST.include?(command)
- send(command)
- else
- write_error_message(command)
- end
- end
-
- def plugin
- require_command!("plugin")
- end
-
- def generate
- generate_or_destroy(:generate)
- end
-
- def destroy
- generate_or_destroy(:destroy)
- end
-
- def console
- require_command!("console")
- options = Rails::Console.parse_arguments(argv)
-
- # RAILS_ENV needs to be set before config/application is required
- ENV['RAILS_ENV'] = options[:environment] if options[:environment]
-
- # shift ARGV so IRB doesn't freak
- shift_argv!
-
- require_application_and_environment!
- Rails::Console.start(Rails.application, options)
- 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
- end
- end
-
- def test
- require_command!("test")
- end
-
- def dbconsole
- require_command!("dbconsole")
- Rails::DBConsole.start
- end
-
- def runner
- require_command!("runner")
- end
-
- def new
- if %w(-h --help).include?(argv.first)
- require_command!("application")
- else
- exit_with_initialization_warning!
- end
- end
-
- def version
- argv.unshift '--version'
- require_command!("application")
- end
-
- def help
- write_help_message
- end
-
- private
-
- def exit_with_initialization_warning!
- puts "Can't initialize a new Rails application within the directory of another, please change to a non-Rails directory first.\n"
- puts "Type 'rails' for help."
- exit(1)
- end
-
- def shift_argv!
- argv.shift if argv.first && argv.first[0] != '-'
- end
-
- def require_command!(command)
- require "rails/commands/#{command}"
- end
-
- def generate_or_destroy(command)
- require 'rails/generators'
- require_application_and_environment!
- Rails.application.load_generators
- require_command!(command)
- end
-
- # Change to the application's path if there is no config.ru file in current directory.
- # This allows us to run `rails server` from other directories, but still get
- # the main config.ru and properly set the tmp directory.
- def set_application_directory!
- Dir.chdir(File.expand_path('../../', APP_PATH)) unless File.exist?(File.expand_path("config.ru"))
- end
-
- def require_application_and_environment!
- require APP_PATH
- Rails.application.require_environment!
- end
-
- def write_help_message
- puts HELP_MESSAGE
- end
-
- # Output an error message stating that the attempted command is not a valid rails command.
- # Run the attempted command as a rake command with the --dry-run flag. If successful, suggest
- # to the user that they possibly meant to run the given rails command as a rake command.
- # Append the help message.
- #
- # Example:
- # $ rails db:migrate
- # Error: Command 'db:migrate' not recognized
- # Did you mean: `$ rake db:migrate` ?
- # (Help message output)
- #
- def write_error_message(command)
- puts "Error: Command '#{command}' not recognized"
- if %x{rake #{command} --dry-run 2>&1 } && $?.success?
- puts "Did you mean: `$ rake #{command}` ?\n\n"
- end
- write_help_message
- exit(1)
- end
-
- def parse_command(command)
- case command
- when '--version', '-v'
- 'version'
- when '--help', '-h'
- 'help'
- else
- command
- end
- end
- end
-end
diff --git a/railties/lib/rails/commands/console.rb b/railties/lib/rails/commands/console.rb
deleted file mode 100644
index ea5d20ea24..0000000000
--- a/railties/lib/rails/commands/console.rb
+++ /dev/null
@@ -1,68 +0,0 @@
-require 'optparse'
-require 'irb'
-require 'irb/completion'
-require 'rails/commands/console_helper'
-
-module Rails
- class Console
- include ConsoleHelper
-
- class << self
- def parse_arguments(arguments)
- options = {}
-
- OptionParser.new do |opt|
- opt.banner = "Usage: rails console [environment] [options]"
- opt.on('-s', '--sandbox', 'Rollback database modifications on exit.') { |v| options[:sandbox] = v }
- opt.on("-e", "--environment=name", String,
- "Specifies the environment to run this console under (test/development/production).",
- "Default: development") { |v| options[:environment] = v.strip }
- opt.parse!(arguments)
- end
-
- set_options_env(arguments, options)
- end
- end
-
- attr_reader :options, :app, :console
-
- def initialize(app, options={})
- @app = app
- @options = options
-
- app.sandbox = sandbox?
- app.load_console
-
- @console = app.config.console || IRB
- end
-
- def sandbox?
- options[:sandbox]
- end
-
- def environment
- options[:environment] ||= super
- end
- alias_method :environment?, :environment
-
- def set_environment!
- Rails.env = environment
- end
-
- def start
- set_environment! if environment?
-
- if sandbox?
- puts "Loading #{Rails.env} environment in sandbox (Rails #{Rails.version})"
- puts "Any modifications you make will be rolled back on exit"
- else
- puts "Loading #{Rails.env} environment (Rails #{Rails.version})"
- end
-
- if defined?(console::ExtendCommandBundle)
- console::ExtendCommandBundle.include(Rails::ConsoleMethods)
- end
- console.start
- end
- end
-end
diff --git a/railties/lib/rails/commands/console/console_command.rb b/railties/lib/rails/commands/console/console_command.rb
new file mode 100644
index 0000000000..e35faa5b01
--- /dev/null
+++ b/railties/lib/rails/commands/console/console_command.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+require "irb"
+require "irb/completion"
+
+require "rails/command/environment_argument"
+
+module Rails
+ class Console
+ module BacktraceCleaner
+ def filter_backtrace(bt)
+ if result = super
+ Rails.backtrace_cleaner.filter([result]).first
+ end
+ end
+ end
+
+ def self.start(*args)
+ new(*args).start
+ end
+
+ attr_reader :options, :app, :console
+
+ def initialize(app, options = {})
+ @app = app
+ @options = options
+
+ app.sandbox = sandbox?
+ app.load_console
+
+ @console = app.config.console || IRB
+
+ if @console == IRB
+ IRB::WorkSpace.prepend(BacktraceCleaner)
+ end
+ end
+
+ def sandbox?
+ options[:sandbox]
+ end
+
+ def environment
+ options[:environment]
+ end
+ alias_method :environment?, :environment
+
+ def set_environment!
+ Rails.env = environment
+ end
+
+ def start
+ set_environment! if environment?
+
+ if sandbox?
+ puts "Loading #{Rails.env} environment in sandbox (Rails #{Rails.version})"
+ puts "Any modifications you make will be rolled back on exit"
+ else
+ puts "Loading #{Rails.env} environment (Rails #{Rails.version})"
+ end
+
+ if defined?(console::ExtendCommandBundle)
+ console::ExtendCommandBundle.include(Rails::ConsoleMethods)
+ end
+ console.start
+ end
+ end
+
+ module Command
+ class ConsoleCommand < Base # :nodoc:
+ include EnvironmentArgument
+
+ class_option :sandbox, aliases: "-s", type: :boolean, default: false,
+ desc: "Rollback database modifications on exit."
+
+ def initialize(args = [], local_options = {}, config = {})
+ console_options = []
+
+ # For the same behavior as OptionParser, leave only options after "--" in ARGV.
+ termination = local_options.find_index("--")
+ if termination
+ console_options = local_options[termination + 1..-1]
+ local_options = local_options[0...termination]
+ end
+
+ ARGV.replace(console_options)
+ super(args, local_options, config)
+ end
+
+ def perform
+ extract_environment_option_from_argument
+
+ # RAILS_ENV needs to be set before config/application is required.
+ ENV["RAILS_ENV"] = options[:environment]
+
+ require_application_and_environment!
+ Rails::Console.start(Rails.application, options)
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/commands/console_helper.rb b/railties/lib/rails/commands/console_helper.rb
deleted file mode 100644
index 8ee0b60012..0000000000
--- a/railties/lib/rails/commands/console_helper.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-require 'active_support/concern'
-
-module Rails
- module ConsoleHelper # :nodoc:
- extend ActiveSupport::Concern
-
- module ClassMethods
- def start(*args)
- new(*args).start
- end
-
- private
- def set_options_env(arguments, options)
- if arguments.first && arguments.first[0] != '-'
- env = arguments.first
- if available_environments.include? env
- options[:environment] = env
- else
- options[:environment] = %w(production development test).detect { |e| e =~ /^#{env}/ } || env
- end
- end
- options
- end
-
- def available_environments
- Dir['config/environments/*.rb'].map { |fname| File.basename(fname, '.*') }
- end
- end
-
- def environment
- ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
- end
- end
-end \ No newline at end of file
diff --git a/railties/lib/rails/commands/credentials/USAGE b/railties/lib/rails/commands/credentials/USAGE
new file mode 100644
index 0000000000..6b33d1ab74
--- /dev/null
+++ b/railties/lib/rails/commands/credentials/USAGE
@@ -0,0 +1,49 @@
+=== Storing Encrypted Credentials in Source Control
+
+The Rails `credentials` commands provide access to encrypted credentials,
+so you can safely store access tokens, database passwords, and the like
+safely inside the app without relying on a mess of ENVs.
+
+This also allows for atomic deploys: no need to coordinate key changes
+to get everything working as the keys are shipped with the code.
+
+=== Setup
+
+Applications after Rails 5.2 automatically have a basic credentials file generated
+that just contains the secret_key_base used by MessageVerifiers/MessageEncryptors, like the ones
+signing and encrypting cookies.
+
+For applications created prior to Rails 5.2, we'll automatically generate a new
+credentials file in `config/credentials.yml.enc` the first time you run `rails credentials:edit`.
+If you didn't have a master key saved in `config/master.key`, that'll be created too.
+
+Don't lose this master 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
+credentials.
+
+Don't commit the key! Add `config/master.key` to your source control's
+ignore file. If you use Git, Rails handles this for you.
+
+Rails also looks for the master 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="very-secret-and-secure" server.start
+
+=== Editing Credentials
+
+This will open a temporary file in `$EDITOR` with the decrypted contents to edit
+the encrypted credentials.
+
+When the temporary file is next saved the contents are encrypted and written to
+`config/credentials.yml.enc` while the file itself is destroyed to prevent credentials
+from leaking.
+
+=== Environment Specific Credentials
+
+It is possible to have credentials for each environment. If the file for current environment exists it will take
+precedence over `config/credentials.yml.enc`, thus for `production` environment first look for
+`config/credentials/production.yml.enc` that can be decrypted using master key taken from `ENV["RAILS_MASTER_KEY"]`
+or stored in `config/credentials/production.key`.
+To edit given file use command `rails credentials:edit --environment production`
+Default paths can be overwritten by setting `config.credentials.content_path` and `config.credentials.key_path`.
diff --git a/railties/lib/rails/commands/credentials/credentials_command.rb b/railties/lib/rails/commands/credentials/credentials_command.rb
new file mode 100644
index 0000000000..4b30d208e0
--- /dev/null
+++ b/railties/lib/rails/commands/credentials/credentials_command.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+require "active_support"
+require "rails/command/helpers/editor"
+
+module Rails
+ module Command
+ class CredentialsCommand < Rails::Command::Base # :nodoc:
+ include Helpers::Editor
+
+ class_option :environment, aliases: "-e", type: :string,
+ desc: "Uses credentials from config/credentials/:environment.yml.enc encrypted by config/credentials/:environment.key key"
+
+ no_commands do
+ def help
+ say "Usage:\n #{self.class.banner}"
+ say ""
+ say self.class.desc
+ end
+ end
+
+ def edit
+ require_application_and_environment!
+
+ ensure_editor_available(command: "bin/rails credentials:edit") || (return)
+
+ encrypted = Rails.application.encrypted(content_path, key_path: key_path)
+
+ ensure_encryption_key_has_been_added(key_path) if encrypted.key.nil?
+ ensure_encrypted_file_has_been_added(content_path, key_path)
+
+ catch_editing_exceptions do
+ change_encrypted_file_in_system_editor(content_path, key_path)
+ end
+
+ say "File encrypted and saved."
+ rescue ActiveSupport::MessageEncryptor::InvalidMessage
+ say "Couldn't decrypt #{content_path}. Perhaps you passed the wrong key?"
+ end
+
+ def show
+ require_application_and_environment!
+
+ encrypted = Rails.application.encrypted(content_path, key_path: key_path)
+
+ say encrypted.read.presence || missing_encrypted_message(key: encrypted.key, key_path: key_path, file_path: content_path)
+ end
+
+ private
+ def content_path
+ options[:environment] ? "config/credentials/#{options[:environment]}.yml.enc" : "config/credentials.yml.enc"
+ end
+
+ def key_path
+ options[:environment] ? "config/credentials/#{options[:environment]}.key" : "config/master.key"
+ end
+
+
+ def ensure_encryption_key_has_been_added(key_path)
+ encryption_key_file_generator.add_key_file(key_path)
+ encryption_key_file_generator.ignore_key_file(key_path)
+ end
+
+ def ensure_encrypted_file_has_been_added(file_path, key_path)
+ encrypted_file_generator.add_encrypted_file_silently(file_path, key_path)
+ end
+
+ def change_encrypted_file_in_system_editor(file_path, key_path)
+ Rails.application.encrypted(file_path, key_path: key_path).change do |tmp_path|
+ system("#{ENV["EDITOR"]} #{tmp_path}")
+ end
+ end
+
+
+ def encryption_key_file_generator
+ require "rails/generators"
+ require "rails/generators/rails/encryption_key_file/encryption_key_file_generator"
+
+ Rails::Generators::EncryptionKeyFileGenerator.new
+ end
+
+ def encrypted_file_generator
+ require "rails/generators"
+ require "rails/generators/rails/encrypted_file/encrypted_file_generator"
+
+ Rails::Generators::EncryptedFileGenerator.new
+ end
+
+ def missing_encrypted_message(key:, key_path:, file_path:)
+ if key.nil?
+ "Missing '#{key_path}' to decrypt credentials. See `rails credentials:help`"
+ else
+ "File '#{file_path}' does not exist. Use `rails credentials:edit` to change that."
+ end
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/commands/dbconsole.rb b/railties/lib/rails/commands/dbconsole.rb
deleted file mode 100644
index dca60f948f..0000000000
--- a/railties/lib/rails/commands/dbconsole.rb
+++ /dev/null
@@ -1,173 +0,0 @@
-require 'erb'
-require 'yaml'
-require 'optparse'
-require 'rails/commands/console_helper'
-
-module Rails
- class DBConsole
- include ConsoleHelper
-
- attr_reader :arguments
-
- class << self
- def parse_arguments(arguments)
- options = {}
-
- OptionParser.new do |opt|
- opt.banner = "Usage: rails dbconsole [environment] [options]"
- opt.on("-p", "--include-password", "Automatically provide the password from database.yml") do |v|
- options['include_password'] = true
- end
-
- opt.on("--mode [MODE]", ['html', 'list', 'line', 'column'],
- "Automatically put the sqlite3 database in the specified mode (html, list, line, column).") do |mode|
- options['mode'] = mode
- end
-
- opt.on("--header") do |h|
- options['header'] = h
- end
-
- opt.on("-h", "--help", "Show this help message.") do
- puts opt
- exit
- end
-
- opt.on("-e", "--environment=name", String,
- "Specifies the environment to run this console under (test/development/production).",
- "Default: development"
- ) { |v| options[:environment] = v.strip }
-
- opt.parse!(arguments)
- abort opt.to_s unless (0..1).include?(arguments.size)
- end
-
- set_options_env(arguments, options)
- end
- end
-
- def initialize(arguments = ARGV)
- @arguments = arguments
- end
-
- def start
- options = self.class.parse_arguments(arguments)
- ENV['RAILS_ENV'] = options[:environment] || environment
-
- case config["adapter"]
- when /^(jdbc)?mysql/
- args = {
- 'host' => '--host',
- 'port' => '--port',
- 'socket' => '--socket',
- 'username' => '--user',
- 'encoding' => '--default-character-set',
- 'sslca' => '--ssl-ca',
- 'sslcert' => '--ssl-cert',
- 'sslcapath' => '--ssl-capath',
- 'sslcipher' => '--ssh-cipher',
- 'sslkey' => '--ssl-key'
- }.map { |opt, arg| "#{arg}=#{config[opt]}" if config[opt] }.compact
-
- if config['password'] && options['include_password']
- args << "--password=#{config['password']}"
- elsif config['password'] && !config['password'].to_s.empty?
- args << "-p"
- end
-
- args << config['database']
-
- find_cmd_and_exec(['mysql', 'mysql5'], *args)
-
- when /^postgres|^postgis/
- ENV['PGUSER'] = config["username"] if config["username"]
- ENV['PGHOST'] = config["host"] if config["host"]
- ENV['PGPORT'] = config["port"].to_s if config["port"]
- ENV['PGPASSWORD'] = config["password"].to_s if config["password"] && options['include_password']
- find_cmd_and_exec('psql', config["database"])
-
- when "sqlite3"
- args = []
-
- args << "-#{options['mode']}" if options['mode']
- args << "-header" if options['header']
- args << File.expand_path(config['database'], Rails.respond_to?(:root) ? Rails.root : nil)
-
- find_cmd_and_exec('sqlite3', *args)
-
- when "oracle", "oracle_enhanced"
- logon = ""
-
- if config['username']
- logon = config['username']
- logon << "/#{config['password']}" if config['password'] && options['include_password']
- logon << "@#{config['database']}" if config['database']
- end
-
- find_cmd_and_exec('sqlplus', logon)
-
- when "sqlserver"
- args = []
-
- args += ["-D", "#{config['database']}"] if config['database']
- args += ["-U", "#{config['username']}"] if config['username']
- args += ["-P", "#{config['password']}"] if config['password']
-
- if config['host']
- host_arg = "#{config['host']}"
- host_arg << ":#{config['port']}" if config['port']
- args += ["-S", host_arg]
- end
-
- find_cmd_and_exec("sqsh", *args)
-
- else
- abort "Unknown command-line client for #{config['database']}."
- end
- end
-
- def config
- @config ||= begin
- if configurations[environment].blank?
- raise ActiveRecord::AdapterNotSpecified, "'#{environment}' database is not configured. Available configuration: #{configurations.inspect}"
- else
- configurations[environment]
- end
- end
- end
-
- def environment
- Rails.respond_to?(:env) ? Rails.env : super
- end
-
- protected
- def configurations
- require APP_PATH
- ActiveRecord::Base.configurations = Rails.application.config.database_configuration
- ActiveRecord::Base.configurations
- end
-
- def find_cmd_and_exec(commands, *args)
- commands = Array(commands)
-
- dirs_on_path = ENV['PATH'].to_s.split(File::PATH_SEPARATOR)
- unless (ext = RbConfig::CONFIG['EXEEXT']).empty?
- commands = commands.map{|cmd| "#{cmd}#{ext}"}
- end
-
- full_path_command = nil
- found = commands.detect do |cmd|
- dirs_on_path.detect do |path|
- full_path_command = File.join(path, cmd)
- File.file?(full_path_command) && File.executable?(full_path_command)
- end
- end
-
- if found
- exec full_path_command, *args
- else
- abort("Couldn't find database client: #{commands.join(', ')}. Check your $PATH and try again.")
- end
- end
- end
-end
diff --git a/railties/lib/rails/commands/dbconsole/dbconsole_command.rb b/railties/lib/rails/commands/dbconsole/dbconsole_command.rb
new file mode 100644
index 0000000000..0fac7d34a0
--- /dev/null
+++ b/railties/lib/rails/commands/dbconsole/dbconsole_command.rb
@@ -0,0 +1,170 @@
+# frozen_string_literal: true
+
+require "rails/command/environment_argument"
+
+module Rails
+ class DBConsole
+ def self.start(*args)
+ new(*args).start
+ end
+
+ def initialize(options = {})
+ @options = options
+ end
+
+ def start
+ ENV["RAILS_ENV"] ||= @options[:environment] || environment
+
+ case config["adapter"]
+ when /^(jdbc)?mysql/
+ args = {
+ "host" => "--host",
+ "port" => "--port",
+ "socket" => "--socket",
+ "username" => "--user",
+ "encoding" => "--default-character-set",
+ "sslca" => "--ssl-ca",
+ "sslcert" => "--ssl-cert",
+ "sslcapath" => "--ssl-capath",
+ "sslcipher" => "--ssl-cipher",
+ "sslkey" => "--ssl-key"
+ }.map { |opt, arg| "#{arg}=#{config[opt]}" if config[opt] }.compact
+
+ if config["password"] && @options["include_password"]
+ args << "--password=#{config['password']}"
+ elsif config["password"] && !config["password"].to_s.empty?
+ args << "-p"
+ end
+
+ args << config["database"]
+
+ find_cmd_and_exec(["mysql", "mysql5"], *args)
+
+ when /^postgres|^postgis/
+ ENV["PGUSER"] = config["username"] if config["username"]
+ ENV["PGHOST"] = config["host"] if config["host"]
+ ENV["PGPORT"] = config["port"].to_s if config["port"]
+ ENV["PGPASSWORD"] = config["password"].to_s if config["password"] && @options["include_password"]
+ find_cmd_and_exec("psql", config["database"])
+
+ when "sqlite3"
+ args = []
+
+ args << "-#{@options['mode']}" if @options["mode"]
+ args << "-header" if @options["header"]
+ args << File.expand_path(config["database"], Rails.respond_to?(:root) ? Rails.root : nil)
+
+ find_cmd_and_exec("sqlite3", *args)
+
+ when "oracle", "oracle_enhanced"
+ logon = ""
+
+ if config["username"]
+ logon = config["username"].dup
+ logon << "/#{config['password']}" if config["password"] && @options["include_password"]
+ logon << "@#{config['database']}" if config["database"]
+ end
+
+ find_cmd_and_exec("sqlplus", logon)
+
+ when "sqlserver"
+ args = []
+
+ args += ["-D", "#{config['database']}"] if config["database"]
+ args += ["-U", "#{config['username']}"] if config["username"]
+ args += ["-P", "#{config['password']}"] if config["password"]
+
+ if config["host"]
+ host_arg = +"#{config['host']}"
+ host_arg << ":#{config['port']}" if config["port"]
+ args += ["-S", host_arg]
+ end
+
+ find_cmd_and_exec("sqsh", *args)
+
+ else
+ abort "Unknown command-line client for #{config['database']}."
+ end
+ end
+
+ def config
+ @config ||= begin
+ # We need to check whether the user passed the connection the
+ # first time around to show a consistent error message to people
+ # relying on 2-level database configuration.
+ if @options["connection"] && configurations[connection].blank?
+ raise ActiveRecord::AdapterNotSpecified, "'#{connection}' connection is not configured. Available configuration: #{configurations.inspect}"
+ elsif configurations[environment].blank? && configurations[connection].blank?
+ raise ActiveRecord::AdapterNotSpecified, "'#{environment}' database is not configured. Available configuration: #{configurations.inspect}"
+ else
+ configurations[connection] || configurations[environment].presence
+ end
+ end
+ end
+
+ def environment
+ Rails.respond_to?(:env) ? Rails.env : Rails::Command.environment
+ end
+
+ def connection
+ @options.fetch(:connection, "primary")
+ end
+
+ 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) # :doc:
+ commands = Array(commands)
+
+ dirs_on_path = ENV["PATH"].to_s.split(File::PATH_SEPARATOR)
+ unless (ext = RbConfig::CONFIG["EXEEXT"]).empty?
+ commands = commands.map { |cmd| "#{cmd}#{ext}" }
+ end
+
+ full_path_command = nil
+ found = commands.detect do |cmd|
+ dirs_on_path.detect do |path|
+ full_path_command = File.join(path, cmd)
+ File.file?(full_path_command) && File.executable?(full_path_command)
+ end
+ end
+
+ if found
+ exec full_path_command, *args
+ else
+ abort("Couldn't find database client: #{commands.join(', ')}. Check your $PATH and try again.")
+ end
+ end
+ end
+
+ module Command
+ class DbconsoleCommand < Base # :nodoc:
+ include EnvironmentArgument
+
+ class_option :include_password, aliases: "-p", type: :boolean,
+ desc: "Automatically provide the password from database.yml"
+
+ class_option :mode, enum: %w( html list line column ), type: :string,
+ desc: "Automatically put the sqlite3 database in the specified mode (html, list, line, column)."
+
+ class_option :header, type: :boolean
+
+ class_option :connection, aliases: "-c", type: :string,
+ desc: "Specifies the connection to use."
+
+ def perform
+ extract_environment_option_from_argument
+
+ # RAILS_ENV needs to be set before config/application is required.
+ ENV["RAILS_ENV"] = options[:environment]
+
+ require_application_and_environment!
+ Rails::DBConsole.start(options)
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/commands/destroy.rb b/railties/lib/rails/commands/destroy.rb
deleted file mode 100644
index ce26cc3fde..0000000000
--- a/railties/lib/rails/commands/destroy.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-require 'rails/generators'
-
-#if no argument/-h/--help is passed to rails destroy command, then
-#it generates the help associated.
-if [nil, "-h", "--help"].include?(ARGV.first)
- Rails::Generators.help 'destroy'
- exit
-end
-
-name = ARGV.shift
-Rails::Generators.invoke name, ARGV, behavior: :revoke, destination_root: Rails.root
diff --git a/railties/lib/rails/commands/destroy/destroy_command.rb b/railties/lib/rails/commands/destroy/destroy_command.rb
new file mode 100644
index 0000000000..dd432d28fd
--- /dev/null
+++ b/railties/lib/rails/commands/destroy/destroy_command.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require "rails/generators"
+
+module Rails
+ module Command
+ class DestroyCommand < Base # :nodoc:
+ no_commands do
+ def help
+ require_application_and_environment!
+ load_generators
+
+ Rails::Generators.help self.class.command_name
+ end
+ end
+
+ def perform(*)
+ generator = args.shift
+ return help unless generator
+
+ require_application_and_environment!
+ load_generators
+
+ Rails::Generators.invoke generator, args, behavior: :revoke, destination_root: Rails::Command.root
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/commands/dev/dev_command.rb b/railties/lib/rails/commands/dev/dev_command.rb
new file mode 100644
index 0000000000..a3f02f3172
--- /dev/null
+++ b/railties/lib/rails/commands/dev/dev_command.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require "rails/dev_caching"
+
+module Rails
+ module Command
+ class DevCommand < Base # :nodoc:
+ def help
+ say "rails dev:cache # Toggle development mode caching on/off."
+ end
+
+ def cache
+ Rails::DevCaching.enable_by_file
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/commands/encrypted/encrypted_command.rb b/railties/lib/rails/commands/encrypted/encrypted_command.rb
new file mode 100644
index 0000000000..8d5947652a
--- /dev/null
+++ b/railties/lib/rails/commands/encrypted/encrypted_command.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require "pathname"
+require "active_support"
+require "rails/command/helpers/editor"
+
+module Rails
+ module Command
+ class EncryptedCommand < Rails::Command::Base # :nodoc:
+ include Helpers::Editor
+
+ class_option :key, aliases: "-k", type: :string,
+ default: "config/master.key", desc: "The Rails.root relative path to the encryption key"
+
+ no_commands do
+ def help
+ say "Usage:\n #{self.class.banner}"
+ say ""
+ end
+ end
+
+ def edit(file_path)
+ require_application_and_environment!
+ encrypted = Rails.application.encrypted(file_path, key_path: options[:key])
+
+ ensure_editor_available(command: "bin/rails encrypted:edit") || (return)
+ ensure_encryption_key_has_been_added(options[:key]) if encrypted.key.nil?
+ ensure_encrypted_file_has_been_added(file_path, options[:key])
+
+ catch_editing_exceptions do
+ change_encrypted_file_in_system_editor(file_path, options[:key])
+ end
+
+ say "File encrypted and saved."
+ rescue ActiveSupport::MessageEncryptor::InvalidMessage
+ say "Couldn't decrypt #{file_path}. Perhaps you passed the wrong key?"
+ end
+
+ def show(file_path)
+ require_application_and_environment!
+ encrypted = Rails.application.encrypted(file_path, key_path: options[:key])
+
+ say encrypted.read.presence || missing_encrypted_message(key: encrypted.key, key_path: options[:key], file_path: file_path)
+ end
+
+ private
+ def ensure_encryption_key_has_been_added(key_path)
+ encryption_key_file_generator.add_key_file(key_path)
+ encryption_key_file_generator.ignore_key_file(key_path)
+ end
+
+ def ensure_encrypted_file_has_been_added(file_path, key_path)
+ encrypted_file_generator.add_encrypted_file_silently(file_path, key_path)
+ end
+
+ def change_encrypted_file_in_system_editor(file_path, key_path)
+ Rails.application.encrypted(file_path, key_path: key_path).change do |tmp_path|
+ system("#{ENV["EDITOR"]} #{tmp_path}")
+ end
+ end
+
+
+ def encryption_key_file_generator
+ require "rails/generators"
+ require "rails/generators/rails/encryption_key_file/encryption_key_file_generator"
+
+ Rails::Generators::EncryptionKeyFileGenerator.new
+ end
+
+ def encrypted_file_generator
+ require "rails/generators"
+ require "rails/generators/rails/encrypted_file/encrypted_file_generator"
+
+ Rails::Generators::EncryptedFileGenerator.new
+ end
+
+ def missing_encrypted_message(key:, key_path:, file_path:)
+ if key.nil?
+ "Missing '#{key_path}' to decrypt data. See `rails encrypted:help`"
+ else
+ "File '#{file_path}' does not exist. Use `rails encrypted:edit #{file_path}` to change that."
+ end
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/commands/generate.rb b/railties/lib/rails/commands/generate.rb
deleted file mode 100644
index 926c36b967..0000000000
--- a/railties/lib/rails/commands/generate.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-require 'rails/generators'
-
-#if no argument/-h/--help is passed to rails generate command, then
-#it generates the help associated.
-if [nil, "-h", "--help"].include?(ARGV.first)
- Rails::Generators.help 'generate'
- exit
-end
-
-name = ARGV.shift
-
-root = defined?(ENGINE_ROOT) ? ENGINE_ROOT : Rails.root
-Rails::Generators.invoke name, ARGV, behavior: :invoke, destination_root: root
diff --git a/railties/lib/rails/commands/generate/generate_command.rb b/railties/lib/rails/commands/generate/generate_command.rb
new file mode 100644
index 0000000000..93d7a0ce3a
--- /dev/null
+++ b/railties/lib/rails/commands/generate/generate_command.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require "rails/generators"
+
+module Rails
+ module Command
+ class GenerateCommand < Base # :nodoc:
+ no_commands do
+ def help
+ require_application_and_environment!
+ load_generators
+
+ Rails::Generators.help self.class.command_name
+ end
+ end
+
+ def perform(*)
+ generator = args.shift
+ return help unless generator
+
+ require_application_and_environment!
+ load_generators
+
+ ARGV.shift
+
+ Rails::Generators.invoke generator, args, behavior: :invoke, destination_root: Rails::Command.root
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/commands/help/USAGE b/railties/lib/rails/commands/help/USAGE
new file mode 100644
index 0000000000..8eb98319d2
--- /dev/null
+++ b/railties/lib/rails/commands/help/USAGE
@@ -0,0 +1,16 @@
+The most common rails commands are:
+ generate Generate new code (short-cut alias: "g")
+ console Start the Rails console (short-cut alias: "c")
+ server Start the Rails server (short-cut alias: "s")
+ test Run tests except system tests (short-cut alias: "t")
+ test:system Run system tests
+ 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.
+In addition to those commands, there are:
+
diff --git a/railties/lib/rails/commands/help/help_command.rb b/railties/lib/rails/commands/help/help_command.rb
new file mode 100644
index 0000000000..9df34e9b79
--- /dev/null
+++ b/railties/lib/rails/commands/help/help_command.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Rails
+ module Command
+ class HelpCommand < Base # :nodoc:
+ hide_command!
+
+ def help(*)
+ say self.class.desc
+
+ Rails::Command.print_commands
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/commands/initializers/initializers_command.rb b/railties/lib/rails/commands/initializers/initializers_command.rb
new file mode 100644
index 0000000000..33596177af
--- /dev/null
+++ b/railties/lib/rails/commands/initializers/initializers_command.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Rails
+ module Command
+ class InitializersCommand < Base # :nodoc:
+ desc "initializers", "Print out all defined initializers in the order they are invoked by Rails."
+ def perform
+ require_application_and_environment!
+
+ Rails.application.initializers.tsort_each do |initializer|
+ say "#{initializer.context_class}.#{initializer.name}"
+ end
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/commands/new/new_command.rb b/railties/lib/rails/commands/new/new_command.rb
new file mode 100644
index 0000000000..a4f2081510
--- /dev/null
+++ b/railties/lib/rails/commands/new/new_command.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Rails
+ module Command
+ class NewCommand < Base # :nodoc:
+ no_commands do
+ def help
+ Rails::Command.invoke :application, [ "--help" ]
+ end
+ end
+
+ def perform(*)
+ say "Can't initialize a new Rails application within the directory of another, please change to a non-Rails directory first.\n"
+ say "Type 'rails' for help."
+ exit 1
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/commands/notes/notes_command.rb b/railties/lib/rails/commands/notes/notes_command.rb
new file mode 100644
index 0000000000..64b339b3cd
--- /dev/null
+++ b/railties/lib/rails/commands/notes/notes_command.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require "rails/source_annotation_extractor"
+
+module Rails
+ module Command
+ class NotesCommand < Base # :nodoc:
+ class_option :annotations, aliases: "-a", desc: "Filter by specific annotations, e.g. Foobar TODO", type: :array, default: %w(OPTIMIZE FIXME TODO)
+
+ def perform(*)
+ require_application_and_environment!
+
+ deprecation_warning
+ display_annotations
+ end
+
+ private
+ def display_annotations
+ annotations = options[:annotations]
+ tag = (annotations.length > 1)
+
+ Rails::SourceAnnotationExtractor.enumerate annotations.join("|"), tag: tag, dirs: directories
+ end
+
+ def directories
+ Rails::SourceAnnotationExtractor::Annotation.directories + source_annotation_directories
+ end
+
+ def deprecation_warning
+ return if source_annotation_directories.empty?
+ ActiveSupport::Deprecation.warn("`SOURCE_ANNOTATION_DIRECTORIES` is deprecated and will be removed in Rails 6.1. You can add default directories by using config.annotations.register_directories instead.")
+ end
+
+ def source_annotation_directories
+ ENV["SOURCE_ANNOTATION_DIRECTORIES"].to_s.split(",")
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/commands/plugin.rb b/railties/lib/rails/commands/plugin.rb
deleted file mode 100644
index 52d8966ead..0000000000
--- a/railties/lib/rails/commands/plugin.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-if ARGV.first != "new"
- ARGV[0] = "--help"
-else
- ARGV.shift
- unless ARGV.delete("--no-rc")
- customrc = ARGV.index{ |x| x.include?("--rc=") }
- railsrc = if customrc
- File.expand_path(ARGV.delete_at(customrc).gsub(/--rc=/, ""))
- else
- File.join(File.expand_path("~"), '.railsrc')
- end
- if File.exist?(railsrc)
- extra_args_string = File.read(railsrc)
- extra_args = extra_args_string.split(/\n+/).flat_map(&:split)
- puts "Using #{extra_args.join(" ")} from #{railsrc}"
- ARGV.insert(1, *extra_args)
- end
- end
-end
-
-require 'rails/generators'
-require 'rails/generators/rails/plugin/plugin_generator'
-Rails::Generators::PluginGenerator.start
diff --git a/railties/lib/rails/commands/plugin/plugin_command.rb b/railties/lib/rails/commands/plugin/plugin_command.rb
new file mode 100644
index 0000000000..96187aa952
--- /dev/null
+++ b/railties/lib/rails/commands/plugin/plugin_command.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Rails
+ module Command
+ class PluginCommand < Base # :nodoc:
+ hide_command!
+
+ def help
+ run_plugin_generator %w( --help )
+ end
+
+ def self.banner(*) # :nodoc:
+ "#{executable} new [options]"
+ end
+
+ 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."
+
+ def perform(type = nil, *plugin_args)
+ plugin_args << "--help" unless type == "new"
+
+ unless options.key?("no_rc") # Thor's not so indifferent access hash.
+ railsrc = File.expand_path(options[:rc])
+
+ if File.exist?(railsrc)
+ extra_args = File.read(railsrc).split(/\n+/).flat_map(&:split)
+ say "Using #{extra_args.join(" ")} from #{railsrc}"
+ plugin_args.insert(1, *extra_args)
+ end
+ end
+
+ run_plugin_generator plugin_args
+ end
+
+ private
+ def run_plugin_generator(plugin_args)
+ require "rails/generators"
+ require "rails/generators/rails/plugin/plugin_generator"
+ Rails::Generators::PluginGenerator.start plugin_args
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/commands/rake/rake_command.rb b/railties/lib/rails/commands/rake/rake_command.rb
new file mode 100644
index 0000000000..535df0c430
--- /dev/null
+++ b/railties/lib/rails/commands/rake/rake_command.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module Rails
+ module Command
+ class RakeCommand < Base # :nodoc:
+ extend Rails::Command::Actions
+
+ namespace "rake"
+
+ class << self
+ def printing_commands
+ formatted_rake_tasks.map(&:first)
+ end
+
+ def perform(task, *)
+ require_rake
+
+ ARGV.unshift(task) # Prepend the task, so Rake knows how to run it.
+
+ Rake.application.standard_exception_handling do
+ Rake.application.init("rails")
+ Rake.application.load_rakefile
+ Rake.application.top_level
+ end
+ end
+
+ private
+ def rake_tasks
+ require_rake
+
+ return @rake_tasks if defined?(@rake_tasks)
+
+ require_application_and_environment!
+
+ Rake::TaskManager.record_task_metadata = true
+ Rake.application.instance_variable_set(:@name, "rails")
+ load_tasks
+ @rake_tasks = Rake.application.tasks.select(&:comment)
+ end
+
+ def formatted_rake_tasks
+ rake_tasks.map { |t| [ t.name_with_args, t.comment ] }
+ end
+
+ def require_rake
+ require "rake" # Defer booting Rake until we know it's needed.
+ end
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/commands/routes/routes_command.rb b/railties/lib/rails/commands/routes/routes_command.rb
new file mode 100644
index 0000000000..b592a5212f
--- /dev/null
+++ b/railties/lib/rails/commands/routes/routes_command.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require "rails/command"
+
+module Rails
+ module Command
+ class RoutesCommand < Base # :nodoc:
+ class_option :controller, aliases: "-c", desc: "Filter by a specific controller, e.g. PostsController or Admin::PostsController."
+ class_option :grep, aliases: "-g", desc: "Grep routes by a specific pattern."
+ class_option :expanded, type: :boolean, aliases: "-E", desc: "Print routes expanded vertically with parts explained."
+
+ def perform(*)
+ require_application_and_environment!
+ require "action_dispatch/routing/inspector"
+
+ say inspector.format(formatter, routes_filter)
+ end
+
+ private
+ def inspector
+ ActionDispatch::Routing::RoutesInspector.new(Rails.application.routes.routes)
+ end
+
+ def formatter
+ if options.key?("expanded")
+ ActionDispatch::Routing::ConsoleFormatter::Expanded.new
+ else
+ ActionDispatch::Routing::ConsoleFormatter::Sheet.new
+ end
+ end
+
+ def routes_filter
+ options.symbolize_keys.slice(:controller, :grep)
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/commands/runner.rb b/railties/lib/rails/commands/runner.rb
deleted file mode 100644
index 86bce9b2fe..0000000000
--- a/railties/lib/rails/commands/runner.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-require 'optparse'
-
-options = { environment: (ENV['RAILS_ENV'] || ENV['RACK_ENV'] || "development").dup }
-code_or_file = nil
-
-if ARGV.first.nil?
- ARGV.push "-h"
-end
-
-ARGV.clone.options do |opts|
- opts.banner = "Usage: rails runner [options] [<'Some.ruby(code)'> | <filename.rb>]"
-
- opts.separator ""
-
- opts.on("-e", "--environment=name", String,
- "Specifies the environment for the runner to operate under (test/development/production).",
- "Default: development") { |v| options[:environment] = v }
-
- opts.separator ""
-
- opts.on("-h", "--help",
- "Show this help message.") { $stdout.puts opts; exit }
-
- opts.separator ""
- opts.separator "Examples: "
-
- opts.separator " rails runner 'puts Rails.env'"
- opts.separator " This runs the code `puts Rails.env` after loading the app"
- opts.separator ""
- opts.separator " rails runner path/to/filename.rb"
- opts.separator " This runs the Ruby file located at `path/to/filename.rb` after loading the app"
-
- if RbConfig::CONFIG['host_os'] !~ /mswin|mingw/
- opts.separator ""
- opts.separator "You can also use runner as a shebang line for your executables:"
- opts.separator " -------------------------------------------------------------"
- opts.separator " #!/usr/bin/env #{File.expand_path($0)} runner"
- opts.separator ""
- opts.separator " Product.all.each { |p| p.price *= 2 ; p.save! }"
- opts.separator " -------------------------------------------------------------"
- end
-
- opts.order! { |o| code_or_file ||= o } rescue retry
-end
-
-ARGV.delete(code_or_file)
-
-ENV["RAILS_ENV"] = options[:environment]
-
-require APP_PATH
-Rails.application.require_environment!
-Rails.application.load_runner
-
-if code_or_file.nil?
- $stderr.puts "Run '#{$0} -h' for help."
- exit 1
-elsif File.exist?(code_or_file)
- $0 = code_or_file
- Kernel.load code_or_file
-else
- eval(code_or_file, binding, __FILE__, __LINE__)
-end
diff --git a/railties/lib/rails/commands/runner/USAGE b/railties/lib/rails/commands/runner/USAGE
new file mode 100644
index 0000000000..24b60037f0
--- /dev/null
+++ b/railties/lib/rails/commands/runner/USAGE
@@ -0,0 +1,20 @@
+Examples:
+
+Run `puts Rails.env` after loading the app:
+
+ <%= executable %> 'puts Rails.env'
+
+Run the Ruby file located at `path/to/filename.rb` after loading the app:
+
+ <%= executable %> path/to/filename.rb
+
+Run the Ruby script read from stdin after loading the app:
+ <%= executable %> -
+
+<% 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) %>
+
+ Product.all.each { |p| p.price *= 2 ; p.save! }
+<% end %>
diff --git a/railties/lib/rails/commands/runner/runner_command.rb b/railties/lib/rails/commands/runner/runner_command.rb
new file mode 100644
index 0000000000..cb693bcf34
--- /dev/null
+++ b/railties/lib/rails/commands/runner/runner_command.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module Rails
+ module Command
+ class RunnerCommand < Base # :nodoc:
+ class_option :environment, aliases: "-e", type: :string,
+ default: Rails::Command.environment.dup,
+ desc: "The environment for the runner to operate under (test/development/production)"
+
+ no_commands do
+ def help
+ super
+ say self.class.desc
+ end
+ end
+
+ def self.banner(*)
+ "#{super} [<'Some.ruby(code)'> | <filename.rb> | -]"
+ end
+
+ def perform(code_or_file = nil, *command_argv)
+ unless code_or_file
+ help
+ exit 1
+ end
+
+ ENV["RAILS_ENV"] = options[:environment]
+
+ require_application_and_environment!
+ Rails.application.load_runner
+
+ ARGV.replace(command_argv)
+
+ if code_or_file == "-"
+ eval($stdin.read, TOPLEVEL_BINDING, "stdin")
+ elsif File.exist?(code_or_file)
+ $0 = code_or_file
+ Kernel.load code_or_file
+ else
+ begin
+ eval(code_or_file, TOPLEVEL_BINDING, __FILE__, __LINE__)
+ rescue SyntaxError, NameError => e
+ error "Please specify a valid ruby command or the path of a script to run."
+ error "Run '#{self.class.executable} -h' for help."
+ error ""
+ error e
+ exit 1
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/commands/secrets/USAGE b/railties/lib/rails/commands/secrets/USAGE
new file mode 100644
index 0000000000..e205cdc001
--- /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 `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. `rails secrets:setup`
+inserts this into the production environment by default.
+
+=== Editing Secrets
+
+After `rails secrets:setup`, run `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..2eebc0f35f
--- /dev/null
+++ b/railties/lib/rails/commands/secrets/secrets_command.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+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
+ deprecate_in_favor_of_credentials_and_exit
+ 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" 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|
+ system("#{ENV["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
+ rescue Errno::ENOENT => error
+ if /secrets\.yml\.enc/.match?(error.message)
+ deprecate_in_favor_of_credentials_and_exit
+ else
+ raise
+ end
+ end
+
+ def show
+ say Rails::Secrets.read
+ end
+
+ private
+ def deprecate_in_favor_of_credentials_and_exit
+ say "Encrypted secrets is deprecated in favor of credentials. Run:"
+ say "rails credentials:help"
+
+ exit 1
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/commands/server.rb b/railties/lib/rails/commands/server.rb
deleted file mode 100644
index d1e445ac70..0000000000
--- a/railties/lib/rails/commands/server.rb
+++ /dev/null
@@ -1,120 +0,0 @@
-require 'fileutils'
-require 'optparse'
-require 'action_dispatch'
-require 'rails'
-
-module Rails
- class Server < ::Rack::Server
- class Options
- 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
-
- private
-
- def option_parser(options)
- OptionParser.new do |opts|
- opts.banner = "Usage: rails server [mongrel, thin etc] [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.separator ""
-
- opts.on("-h", "--help", "Shows this help message.") { puts opts; exit }
- end
- end
- end
-
- def initialize(*)
- super
- set_environment
- end
-
- # TODO: this is no longer required but we keep it for the moment to support older config.ru files.
- def app
- @app ||= begin
- app = super
- app.respond_to?(:to_app) ? app.to_app : app
- end
- end
-
- def opt_parser
- Options.new
- end
-
- def set_environment
- ENV["RAILS_ENV"] ||= options[:environment]
- end
-
- def start
- print_boot_information
- trap(:INT) { exit }
- create_tmp_directories
- log_to_stdout if options[:log_stdout]
-
- super
- ensure
- # The '-h' option calls exit before @options is set.
- # If we call 'options' with it unset, we get double help banners.
- puts 'Exiting' unless @options && options[:daemonize]
- end
-
- def middleware
- Hash.new([])
- end
-
- def default_options
- super.merge({
- Port: 3000,
- DoNotReverseLookup: true,
- environment: (ENV['RAILS_ENV'] || ENV['RACK_ENV'] || "development").dup,
- daemonize: false,
- pid: File.expand_path("tmp/pids/server.pid")
- })
- end
-
- private
-
- def print_boot_information
- url = "#{options[:SSLEnable] ? 'https' : 'http'}://#{options[:Host]}:#{options[:Port]}"
- puts "=> Booting #{ActiveSupport::Inflector.demodulize(server)}"
- puts "=> Rails #{Rails.version} application starting in #{Rails.env} on #{url}"
- puts "=> Run `rails server -h` for more startup options"
-
- puts "=> Ctrl-C to shutdown server" unless options[:daemonize]
- end
-
- def create_tmp_directories
- %w(cache pids sockets).each do |dir_to_make|
- FileUtils.mkdir_p(File.join(Rails.root, 'tmp', dir_to_make))
- end
- end
-
- def log_to_stdout
- wrapped_app # touch the app so the logger is set up
-
- console = ActiveSupport::Logger.new($stdout)
- console.formatter = Rails.logger.formatter
- console.level = Rails.logger.level
-
- Rails.logger.extend(ActiveSupport::Logger.broadcast(console))
- end
- end
-end
diff --git a/railties/lib/rails/commands/server/server_command.rb b/railties/lib/rails/commands/server/server_command.rb
new file mode 100644
index 0000000000..70789e0303
--- /dev/null
+++ b/railties/lib/rails/commands/server/server_command.rb
@@ -0,0 +1,322 @@
+# frozen_string_literal: true
+
+require "fileutils"
+require "action_dispatch"
+require "rails"
+require "active_support/deprecation"
+require "active_support/core_ext/string/filters"
+require "rails/dev_caching"
+
+module Rails
+ class Server < ::Rack::Server
+ class Options
+ def parse!(args)
+ Rails::Command::ServerCommand.new([], args).server_options
+ end
+ end
+
+ def initialize(options = nil)
+ @default_options = options || {}
+ super(@default_options)
+ set_environment
+ end
+
+ def app
+ @app ||= begin
+ app = super
+ if app.is_a?(Class)
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Using `Rails::Application` subclass to start the server is deprecated and will be removed in Rails 6.0.
+ Please change `run #{app}` to `run Rails.application` in config.ru.
+ MSG
+ end
+ app.respond_to?(:to_app) ? app.to_app : app
+ end
+ end
+
+ def opt_parser
+ Options.new
+ end
+
+ def set_environment
+ ENV["RAILS_ENV"] ||= options[:environment]
+ end
+
+ def start(after_stop_callback = nil)
+ trap(:INT) { exit }
+ create_tmp_directories
+ setup_dev_caching
+ log_to_stdout if options[:log_stdout]
+
+ super()
+ ensure
+ after_stop_callback.call if after_stop_callback
+ end
+
+ def serveable? # :nodoc:
+ server
+ true
+ rescue LoadError, NameError
+ false
+ end
+
+ def middleware
+ Hash.new([])
+ end
+
+ def default_options
+ super.merge(@default_options)
+ end
+
+ def served_url
+ "#{options[:SSLEnable] ? 'https' : 'http'}://#{options[:Host]}:#{options[:Port]}" unless use_puma?
+ end
+
+ private
+ def setup_dev_caching
+ if options[:environment] == "development"
+ Rails::DevCaching.enable_by_argument(options[:caching])
+ end
+ end
+
+ def create_tmp_directories
+ %w(cache pids sockets).each do |dir_to_make|
+ FileUtils.mkdir_p(File.join(Rails.root, "tmp", dir_to_make))
+ end
+ end
+
+ def log_to_stdout
+ wrapped_app # touch the app so the logger is set up
+
+ console = ActiveSupport::Logger.new(STDOUT)
+ console.formatter = Rails.logger.formatter
+ console.level = Rails.logger.level
+
+ unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDOUT)
+ Rails.logger.extend(ActiveSupport::Logger.broadcast(console))
+ end
+ end
+
+ def use_puma?
+ server.to_s == "Rack::Handler::Puma"
+ end
+ end
+
+ module Command
+ class ServerCommand < Base # :nodoc:
+ # Hard-coding a bunch of handlers here as we don't have a public way of
+ # querying them from the Rack::Handler registry.
+ RACK_SERVERS = %w(cgi fastcgi webrick lsws scgi thin puma unicorn)
+
+ DEFAULT_PORT = 3000
+ DEFAULT_PID_PATH = "tmp/pids/server.pid"
+
+ argument :using, optional: true
+
+ class_option :port, aliases: "-p", type: :numeric,
+ desc: "Runs Rails on the specified port - defaults to 3000.", banner: :port
+ class_option :binding, aliases: "-b", type: :string,
+ 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 :using, aliases: "-u", type: :string,
+ desc: "Specifies the Rack server used to run the application (thin/puma/webrick).", banner: :name
+ class_option :pid, aliases: "-P", type: :string, default: DEFAULT_PID_PATH,
+ desc: "Specifies the PID file."
+ class_option :dev_caching, aliases: "-C", type: :boolean, default: nil,
+ desc: "Specifies whether to perform caching in development."
+ class_option :restart, type: :boolean, default: nil, hide: true
+ class_option :early_hints, type: :boolean, default: nil, desc: "Enables HTTP/2 early hints."
+ class_option :log_to_stdout, type: :boolean, default: nil, optional: true,
+ desc: "Whether to log to stdout. Enabled by default in development when not daemonized."
+
+ def initialize(args, local_options, *)
+ super
+
+ @original_options = local_options - %w( --restart )
+ deprecate_positional_rack_server_and_rewrite_to_option(@original_options)
+ end
+
+ def perform
+ set_application_directory!
+ prepare_restart
+
+ Rails::Server.new(server_options).tap do |server|
+ # Require application after server sets environment to propagate
+ # the --environment option.
+ require APP_PATH
+ Dir.chdir(Rails.application.root)
+
+ if server.serveable?
+ print_boot_information(server.server, server.served_url)
+ after_stop_callback = -> { say "Exiting" unless options[:daemon] }
+ server.start(after_stop_callback)
+ else
+ say rack_server_suggestion(using)
+ end
+ end
+ end
+
+ no_commands do
+ def server_options
+ {
+ user_supplied_options: user_supplied_options,
+ server: using,
+ log_stdout: log_to_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,
+ early_hints: early_hints
+ }
+ end
+ end
+
+ private
+ def user_supplied_options
+ @user_supplied_options ||= begin
+ # Convert incoming options array to a hash of flags
+ # ["-p3001", "-C", "--binding", "127.0.0.1"] # => {"-p"=>true, "-C"=>true, "--binding"=>true}
+ user_flag = {}
+ @original_options.each do |command|
+ if command.to_s.start_with?("--")
+ option = command.split("=")[0]
+ user_flag[option] = true
+ elsif command =~ /\A(-.)/
+ user_flag[Regexp.last_match[0]] = true
+ end
+ end
+
+ # 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"] || ENV["BINDING"]
+ user_supplied_options << :Port if ENV["PORT"]
+ user_supplied_options.uniq
+ end
+ end
+
+ def port
+ options[:port] || ENV.fetch("PORT", DEFAULT_PORT).to_i
+ end
+
+ def host
+ if options[:binding]
+ options[:binding]
+ else
+ default_host = environment == "development" ? "localhost" : "0.0.0.0"
+
+ if ENV["HOST"] && !ENV["BINDING"]
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Using the `HOST` environment to specify the IP is deprecated and will be removed in Rails 6.1.
+ Please use `BINDING` environment instead.
+ MSG
+
+ return ENV["HOST"]
+ end
+
+ ENV.fetch("BINDING", default_host)
+ end
+ end
+
+ def environment
+ options[:environment] || Rails::Command.environment
+ end
+
+ def restart_command
+ "bin/rails server #{@original_options.join(" ")} --restart"
+ end
+
+ def early_hints
+ options[:early_hints]
+ end
+
+ def log_to_stdout?
+ options.fetch(:log_to_stdout) do
+ options[:daemon].blank? && environment == "development"
+ end
+ end
+
+ def pid
+ File.expand_path(options[:pid])
+ end
+
+ def self.banner(*)
+ "rails server [thin/puma/webrick] [options]"
+ end
+
+ def prepare_restart
+ FileUtils.rm_f(options[:pid]) if options[:restart]
+ end
+
+ def deprecate_positional_rack_server_and_rewrite_to_option(original_options)
+ if using
+ ActiveSupport::Deprecation.warn(<<~MSG)
+ Passing the Rack server name as a regular argument is deprecated
+ and will be removed in the next Rails version. Please, use the -u
+ option instead.
+ MSG
+
+ original_options.concat [ "-u", using ]
+ else
+ # Use positional internally to get around Thor's immutable options.
+ # TODO: Replace `using` occurrences with `options[:using]` after deprecation removal.
+ @using = options[:using]
+ end
+ end
+
+ def rack_server_suggestion(server)
+ if server.in?(RACK_SERVERS)
+ <<~MSG
+ Could not load server "#{server}". Maybe you need to the add it to the Gemfile?
+
+ gem "#{server}"
+
+ Run `rails server --help` for more options.
+ MSG
+ else
+ suggestion = Rails::Command::Spellchecker.suggest(server, from: RACK_SERVERS)
+
+ <<~MSG
+ Could not find server "#{server}". Maybe you meant #{suggestion.inspect}?
+ Run `rails server --help` for more options.
+ MSG
+ end
+ end
+
+ def print_boot_information(server, url)
+ say <<~MSG
+ => Booting #{ActiveSupport::Inflector.demodulize(server)}
+ => Rails #{Rails.version} application starting in #{Rails.env} #{url}
+ => Run `rails server --help` for more startup options
+ MSG
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/commands/test.rb b/railties/lib/rails/commands/test.rb
deleted file mode 100644
index fe5307788a..0000000000
--- a/railties/lib/rails/commands/test.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-require "rails/test_unit/minitest_plugin"
-
-$: << File.expand_path("../../test", APP_PATH)
-
-exit Minitest.run(ARGV)
diff --git a/railties/lib/rails/commands/test/test_command.rb b/railties/lib/rails/commands/test/test_command.rb
new file mode 100644
index 0000000000..00ea9ac4a6
--- /dev/null
+++ b/railties/lib/rails/commands/test/test_command.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require "rails/command"
+require "rails/test_unit/runner"
+require "rails/test_unit/reporter"
+
+module Rails
+ module Command
+ class TestCommand < Base # :nodoc:
+ no_commands do
+ def help
+ say "Usage: #{Rails::TestUnitReporter.executable} [options] [files or directories]"
+ say ""
+ say "You can run a single test by appending a line number to a filename:"
+ say ""
+ say " #{Rails::TestUnitReporter.executable} test/models/user_test.rb:27"
+ say ""
+ say "You can run multiple files and directories at the same time:"
+ say ""
+ say " #{Rails::TestUnitReporter.executable} test/controllers test/integration/login_test.rb"
+ say ""
+ say "By default test failures and errors are reported inline during a run."
+ say ""
+
+ Minitest.run(%w(--help))
+ end
+ end
+
+ def perform(*)
+ $LOAD_PATH << Rails::Command.root.join("test").to_s
+
+ Rails::TestUnit::Runner.parse_options(ARGV)
+ Rails::TestUnit::Runner.run(ARGV)
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/commands/version/version_command.rb b/railties/lib/rails/commands/version/version_command.rb
new file mode 100644
index 0000000000..3e2112b6d4
--- /dev/null
+++ b/railties/lib/rails/commands/version/version_command.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Rails
+ module Command
+ class VersionCommand < Base # :nodoc:
+ def perform
+ Rails::Command.invoke :application, [ "--version" ]
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/configuration.rb b/railties/lib/rails/configuration.rb
index d99d27a756..e8741a50ba 100644
--- a/railties/lib/rails/configuration.rb
+++ b/railties/lib/rails/configuration.rb
@@ -1,7 +1,9 @@
-require 'active_support/ordered_options'
-require 'active_support/core_ext/object'
-require 'rails/paths'
-require 'rails/rack'
+# frozen_string_literal: true
+
+require "active_support/ordered_options"
+require "active_support/core_ext/object"
+require "rails/paths"
+require "rails/rack"
module Rails
module Configuration
@@ -33,9 +35,9 @@ module Rails
# config.middleware.delete ActionDispatch::Flash
#
class MiddlewareStackProxy
- def initialize
- @operations = []
- @delete_operations = []
+ def initialize(operations = [], delete_operations = [])
+ @operations = operations
+ @delete_operations = delete_operations
end
def insert_before(*args, &block)
@@ -71,6 +73,13 @@ module Rails
other
end
+
+ def +(other) # :nodoc:
+ MiddlewareStackProxy.new(@operations + other.operations, @delete_operations + other.delete_operations)
+ end
+
+ protected
+ attr_reader :operations, :delete_operations
end
class Generators #:nodoc:
@@ -78,8 +87,8 @@ module Rails
attr_reader :hidden_namespaces
def initialize
- @aliases = Hash.new { |h,k| h[k] = {} }
- @options = Hash.new { |h,k| h[k] = {} }
+ @aliases = Hash.new { |h, k| h[k] = {} }
+ @options = Hash.new { |h, k| h[k] = {} }
@fallbacks = {}
@templates = []
@colorize_logging = true
@@ -99,7 +108,7 @@ module Rails
end
def method_missing(method, *args)
- method = method.to_s.sub(/=$/, '').to_sym
+ method = method.to_s.sub(/=$/, "").to_sym
return @options[method] if args.empty?
diff --git a/railties/lib/rails/console/app.rb b/railties/lib/rails/console/app.rb
index ac5836a588..c37583ce9a 100644
--- a/railties/lib/rails/console/app.rb
+++ b/railties/lib/rails/console/app.rb
@@ -1,11 +1,13 @@
-require 'active_support/all'
-require 'action_controller'
+# frozen_string_literal: true
+
+require "active_support/all"
+require "action_controller"
module Rails
module ConsoleMethods
# reference the global "app" instance, created on demand. To recreate the
# instance, pass a non-false value as the parameter.
- def app(create=false)
+ def app(create = false)
@app_integration_instance = nil if create
@app_integration_instance ||= new_session do |sess|
sess.host! "www.example.com"
@@ -27,10 +29,9 @@ module Rails
end
# reloads the environment
- def reload!(print=true)
+ def reload!(print = true)
puts "Reloading..." if print
- ActionDispatch::Reloader.cleanup!
- ActionDispatch::Reloader.prepare!
+ Rails.application.reloader.reload!
true
end
end
diff --git a/railties/lib/rails/console/helpers.rb b/railties/lib/rails/console/helpers.rb
index b775f1ff8d..39fbc55606 100644
--- a/railties/lib/rails/console/helpers.rb
+++ b/railties/lib/rails/console/helpers.rb
@@ -1,10 +1,12 @@
+# frozen_string_literal: true
+
module Rails
module ConsoleMethods
# Gets the helper methods available to the controller.
#
# This method assumes an +ApplicationController+ exists, and it extends +ActionController::Base+
def helper
- @helper ||= ApplicationController.helpers
+ ApplicationController.helpers
end
# Gets a new instance of a controller object.
diff --git a/railties/lib/rails/dev_caching.rb b/railties/lib/rails/dev_caching.rb
new file mode 100644
index 0000000000..ff629b2527
--- /dev/null
+++ b/railties/lib/rails/dev_caching.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require "fileutils"
+
+module Rails
+ module DevCaching # :nodoc:
+ class << self
+ FILE = "tmp/caching-dev.txt"
+
+ def enable_by_file
+ FileUtils.mkdir_p("tmp")
+
+ if File.exist?(FILE)
+ delete_cache_file
+ puts "Development mode is no longer being cached."
+ else
+ create_cache_file
+ puts "Development mode is now being cached."
+ end
+
+ FileUtils.touch "tmp/restart.txt"
+ end
+
+ def enable_by_argument(caching)
+ FileUtils.mkdir_p("tmp")
+
+ if caching
+ create_cache_file
+ elsif caching == false && File.exist?(FILE)
+ delete_cache_file
+ end
+ end
+
+ private
+ def create_cache_file
+ FileUtils.touch FILE
+ end
+
+ def delete_cache_file
+ File.delete FILE
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb
index 1dede32dd4..6a13a84108 100644
--- a/railties/lib/rails/engine.rb
+++ b/railties/lib/rails/engine.rb
@@ -1,7 +1,10 @@
-require 'rails/railtie'
-require 'rails/engine/railties'
-require 'active_support/core_ext/module/delegation'
-require 'pathname'
+# frozen_string_literal: true
+
+require "rails/railtie"
+require "rails/engine/railties"
+require "active_support/core_ext/module/delegation"
+require "pathname"
+require "thread"
module Rails
# <tt>Rails::Engine</tt> allows you to wrap a specific Rails application or subset of
@@ -39,7 +42,7 @@ module Rails
#
# class MyEngine < Rails::Engine
# # Add a load path for this specific Engine
- # config.autoload_paths << File.expand_path("../lib/some/path", __FILE__)
+ # config.autoload_paths << File.expand_path("lib/some/path", __dir__)
#
# initializer "my_engine.add_middleware" do |app|
# app.middleware.use MyEngine::Middleware
@@ -108,7 +111,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:
@@ -127,7 +130,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
@@ -218,7 +221,7 @@ module Rails
# The next thing that changes in isolated engines is the behavior of routes.
# Normally, when you namespace your controllers, you also need to namespace
# the related routes. With an isolated engine, the engine's namespace is
- # automatically applied, so you don't need to specify it explicity in your
+ # automatically applied, so you don't need to specify it explicitly in your
# routes:
#
# MyEngine::Engine.routes.draw do
@@ -336,7 +339,7 @@ module Rails
# == Loading priority
#
# In order to change engine's priority you can use +config.railties_order+ in the main application.
- # It will affect the priority of loading views, helpers, assets and all the other files
+ # It will affect the priority of loading views, helpers, assets, and all the other files
# related to engine or application.
#
# # load Blog::Engine with highest priority, followed by application and other railties
@@ -357,12 +360,7 @@ module Rails
Rails::Railtie::Configuration.eager_load_namespaces << base
base.called_from = begin
- call_stack = if Kernel.respond_to?(:caller_locations)
- caller_locations.map { |l| l.absolute_path || l.path }
- else
- # Remove the line number from backtraces making sure we don't leave anything behind
- caller.map { |p| p.sub(/:\d+.*/, '') }
- end
+ call_stack = caller_locations.map { |l| l.absolute_path || l.path }
File.dirname(call_stack.detect { |p| p !~ %r[railties[\w.-]*/lib/rails|rack[\w.-]*/lib/rack] })
end
@@ -384,7 +382,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)
@@ -406,7 +404,7 @@ module Rails
end
unless mod.respond_to?(:railtie_routes_url_helpers)
- define_method(:railtie_routes_url_helpers) {|include_path_helpers = true| railtie.routes.url_helpers(include_path_helpers) }
+ define_method(:railtie_routes_url_helpers) { |include_path_helpers = true| railtie.routes.url_helpers(include_path_helpers) }
end
end
end
@@ -434,12 +432,13 @@ module Rails
@env_config = nil
@helpers = nil
@routes = nil
+ @app_build_lock = Mutex.new
super
end
# Load console and invoke the registered hooks.
# Check <tt>Rails::Railtie.console</tt> for more info.
- def load_console(app=self)
+ def load_console(app = self)
require "rails/console/app"
require "rails/console/helpers"
run_console_blocks(app)
@@ -448,14 +447,14 @@ module Rails
# Load Rails runner and invoke the registered hooks.
# Check <tt>Rails::Railtie.runner</tt> for more info.
- def load_runner(app=self)
+ def load_runner(app = self)
run_runner_blocks(app)
self
end
# Load Rake, railties tasks and invoke the registered hooks.
# Check <tt>Rails::Railtie.rake_tasks</tt> for more info.
- def load_tasks(app=self)
+ def load_tasks(app = self)
require "rake"
run_tasks_blocks(app)
self
@@ -463,7 +462,7 @@ module Rails
# Load Rails generators and invoke the registered hooks.
# Check <tt>Rails::Railtie.generators</tt> for more info.
- def load_generators(app=self)
+ def load_generators(app = self)
require "rails/generators"
run_generators_blocks(app)
Rails::Generators.configure!(app.config.generators)
@@ -502,12 +501,15 @@ 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 ||= begin
- config.middleware = config.middleware.merge_into(default_middleware_stack)
- config.middleware.build(endpoint)
- end
+ @app || @app_build_lock.synchronize {
+ @app ||= begin
+ stack = default_middleware_stack
+ config.middleware = build_middleware.merge_into(stack)
+ config.middleware.build(endpoint)
+ end
+ }
end
# Returns the endpoint for this engine. If none is registered,
@@ -518,18 +520,13 @@ module Rails
# Define the Rack API for this engine.
def call(env)
- env.merge!(env_config)
- if env['SCRIPT_NAME']
- env[routes.env_key] = env['SCRIPT_NAME'].dup
- end
- app.call(env)
+ req = build_request env
+ app.call req.env
end
# Defines additional Rack env configuration that is added on each call.
def env_config
- @env_config ||= {
- 'action_dispatch.routes' => routes
- }
+ @env_config ||= {}
end
# Defines the routes for this engine. If a block is given to
@@ -554,7 +551,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)
@@ -578,7 +575,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)
@@ -589,14 +586,14 @@ module Rails
# I18n load paths are a special case since the ones added
# later have higher priority.
initializer :add_locales do
- config.i18n.railties_load_path.concat(paths["config/locales"].existent)
+ config.i18n.railties_load_path << paths["config/locales"]
end
initializer :add_view_paths do
views = paths["app/views"].existent
unless views.empty?
- ActiveSupport.on_load(:action_controller){ prepend_view_path(views) if respond_to?(:prepend_view_path) }
- ActiveSupport.on_load(:action_mailer){ prepend_view_path(views) }
+ ActiveSupport.on_load(:action_controller) { prepend_view_path(views) if respond_to?(:prepend_view_path) }
+ ActiveSupport.on_load(:action_mailer) { prepend_view_path(views) }
end
end
@@ -624,7 +621,7 @@ module Rails
end
rake_tasks do
- next if self.is_a?(Rails::Application)
+ next if is_a?(Rails::Application)
next unless has_migrations?
namespace railtie_name do
@@ -648,48 +645,61 @@ module Rails
protected
- def load_config_initializer(initializer)
- ActiveSupport::Notifications.instrument('load_config_initializer.railties', initializer: initializer) do
- load(initializer)
+ def run_tasks_blocks(*) #:nodoc:
+ super
+ paths["lib/tasks"].existent.sort.each { |ext| load(ext) }
end
- end
- def run_tasks_blocks(*) #:nodoc:
- super
- paths["lib/tasks"].existent.sort.each { |ext| load(ext) }
- end
+ private
- def has_migrations? #:nodoc:
- paths["db/migrate"].existent.any?
- end
+ def load_config_initializer(initializer) # :doc:
+ ActiveSupport::Notifications.instrument("load_config_initializer.railties", initializer: initializer) do
+ load(initializer)
+ end
+ end
- def self.find_root_with_flag(flag, root_path, default=nil) #:nodoc:
+ 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
+ end
+
+ root = File.exist?("#{root_path}/#{flag}") ? root_path : default
+ raise "Could not find root path for #{self}" unless root
- while root_path && File.directory?(root_path) && !File.exist?("#{root_path}/#{flag}")
- parent = File.dirname(root_path)
- root_path = parent != root_path && parent
+ Pathname.new File.realpath root
end
- root = File.exist?("#{root_path}/#{flag}") ? root_path : default
- raise "Could not find root path for #{self}" unless root
+ def default_middleware_stack
+ ActionDispatch::MiddlewareStack.new
+ end
- Pathname.new File.realpath root
- end
+ def _all_autoload_once_paths
+ config.autoload_once_paths
+ end
- def default_middleware_stack #:nodoc:
- ActionDispatch::MiddlewareStack.new
- end
+ def _all_autoload_paths
+ @_all_autoload_paths ||= (config.autoload_paths + config.eager_load_paths + config.autoload_once_paths).uniq
+ end
- def _all_autoload_once_paths #:nodoc:
- config.autoload_once_paths
- end
+ def _all_load_paths
+ @_all_load_paths ||= (config.paths.load_paths + _all_autoload_paths).uniq
+ end
- def _all_autoload_paths #:nodoc:
- @_all_autoload_paths ||= (config.autoload_paths + config.eager_load_paths + config.autoload_once_paths).uniq
- end
+ def build_request(env)
+ env.merge!(env_config)
+ req = ActionDispatch::Request.new env
+ req.routes = routes
+ req.engine_script_name = req.script_name
+ req
+ end
- def _all_load_paths #:nodoc:
- @_all_load_paths ||= (config.paths.load_paths + _all_autoload_paths).uniq
- end
+ def build_middleware
+ config.middleware
+ end
end
end
diff --git a/railties/lib/rails/engine/commands.rb b/railties/lib/rails/engine/commands.rb
index f39f926109..05218640c6 100644
--- a/railties/lib/rails/engine/commands.rb
+++ b/railties/lib/rails/engine/commands.rb
@@ -1,41 +1,9 @@
-ARGV << '--help' if ARGV.empty?
+# frozen_string_literal: true
-aliases = {
- "g" => "generate",
- "d" => "destroy"
-}
-
-command = ARGV.shift
-command = aliases[command] || command
-
-require ENGINE_PATH
-engine = ::Rails::Engine.find(ENGINE_ROOT)
-
-case command
-when 'generate', 'destroy'
- require 'rails/generators'
- Rails::Generators.namespace = engine.railtie_namespace
- engine.load_generators
- require "rails/commands/#{command}"
-
-when '--version', '-v'
- ARGV.unshift '--version'
- require 'rails/commands/application'
-
-else
- puts "Error: Command not recognized" unless %w(-h --help).include?(command)
- puts <<-EOT
-Usage: rails COMMAND [ARGS]
-
-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")
-
-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 `rails server` or `rails console`,
-you should do it from application's directory (typically test/dummy).
- EOT
- exit(1)
+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
+
+require "rails/commands"
diff --git a/railties/lib/rails/engine/configuration.rb b/railties/lib/rails/engine/configuration.rb
index 62a4139d07..4143b3c881 100644
--- a/railties/lib/rails/engine/configuration.rb
+++ b/railties/lib/rails/engine/configuration.rb
@@ -1,20 +1,19 @@
-require 'rails/railtie/configuration'
+# frozen_string_literal: true
+
+require "rails/railtie/configuration"
module Rails
class Engine
class Configuration < ::Rails::Railtie::Configuration
attr_reader :root
- attr_writer :middleware, :eager_load_paths, :autoload_once_paths, :autoload_paths
+ attr_accessor :middleware
+ attr_writer :eager_load_paths, :autoload_once_paths, :autoload_paths
- def initialize(root=nil)
+ def initialize(root = nil)
super()
@root = root
@generators = app_generators.dup
- end
-
- # Returns the middleware stack for the engine.
- def middleware
- @middleware ||= Rails::Configuration::MiddlewareStackProxy.new
+ @middleware = Rails::Configuration::MiddlewareStackProxy.new
end
# Holds generators configuration:
@@ -29,7 +28,7 @@ module Rails
#
# config.generators.colorize_logging = false
#
- def generators #:nodoc:
+ def generators
@generators ||= Rails::Configuration::Generators.new
yield(@generators) if block_given?
@generators
@@ -39,9 +38,12 @@ module Rails
@paths ||= begin
paths = Rails::Paths::Root.new(@root)
- paths.add "app", eager_load: true, glob: "{*,*/concerns}"
+ paths.add "app", eager_load: true,
+ glob: "{*,*/concerns}",
+ exclude: %w(assets javascript)
paths.add "app/assets", glob: "*"
paths.add "app/controllers", eager_load: true
+ paths.add "app/channels", eager_load: true, glob: "**/*_channel.rb"
paths.add "app/helpers", eager_load: true
paths.add "app/models", eager_load: true
paths.add "app/mailers", eager_load: true
diff --git a/railties/lib/rails/engine/railties.rb b/railties/lib/rails/engine/railties.rb
index 9969a1475d..052b74c880 100644
--- a/railties/lib/rails/engine/railties.rb
+++ b/railties/lib/rails/engine/railties.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Rails
class Engine < Railtie
class Railties
diff --git a/railties/lib/rails/engine/updater.rb b/railties/lib/rails/engine/updater.rb
new file mode 100644
index 0000000000..be7a47124a
--- /dev/null
+++ b/railties/lib/rails/engine/updater.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+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 7d74b1bfe5..54bfbdd516 100644
--- a/railties/lib/rails/gem_version.rb
+++ b/railties/lib/rails/gem_version.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Rails
# Returns the version of the currently loaded Rails as a <tt>Gem::Version</tt>
def self.gem_version
@@ -5,7 +7,7 @@ module Rails
end
module VERSION
- MAJOR = 5
+ MAJOR = 6
MINOR = 0
TINY = 0
PRE = "alpha"
diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb
index b430cf1909..5e8cebc50a 100644
--- a/railties/lib/rails/generators.rb
+++ b/railties/lib/rails/generators.rb
@@ -1,45 +1,49 @@
-activesupport_path = File.expand_path('../../../../activesupport/lib', __FILE__)
+# frozen_string_literal: true
+
+activesupport_path = File.expand_path("../../../activesupport/lib", __dir__)
$:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path)
-require 'thor/group'
+require "thor/group"
+require "rails/command"
-require 'active_support'
-require 'active_support/core_ext/object/blank'
-require 'active_support/core_ext/kernel/singleton_class'
-require 'active_support/core_ext/array/extract_options'
-require 'active_support/core_ext/hash/deep_merge'
-require 'active_support/core_ext/module/attribute_accessors'
-require 'active_support/core_ext/string/inflections'
+require "active_support"
+require "active_support/core_ext/object/blank"
+require "active_support/core_ext/kernel/singleton_class"
+require "active_support/core_ext/array/extract_options"
+require "active_support/core_ext/hash/deep_merge"
+require "active_support/core_ext/module/attribute_accessors"
+require "active_support/core_ext/string/indent"
+require "active_support/core_ext/string/inflections"
module Rails
module Generators
- autoload :Actions, 'rails/generators/actions'
- autoload :ActiveModel, 'rails/generators/active_model'
- autoload :Base, 'rails/generators/base'
- autoload :Migration, 'rails/generators/migration'
- autoload :NamedBase, 'rails/generators/named_base'
- autoload :ResourceHelpers, 'rails/generators/resource_helpers'
- autoload :TestCase, 'rails/generators/test_case'
+ include Rails::Command::Behavior
+
+ autoload :Actions, "rails/generators/actions"
+ autoload :ActiveModel, "rails/generators/active_model"
+ autoload :Base, "rails/generators/base"
+ autoload :Migration, "rails/generators/migration"
+ autoload :NamedBase, "rails/generators/named_base"
+ autoload :ResourceHelpers, "rails/generators/resource_helpers"
+ autoload :TestCase, "rails/generators/test_case"
mattr_accessor :namespace
DEFAULT_ALIASES = {
rails: {
- actions: '-a',
- orm: '-o',
- javascripts: '-j',
- javascript_engine: '-je',
- resource_controller: '-c',
- scaffold_controller: '-c',
- stylesheets: '-y',
- stylesheet_engine: '-se',
- scaffold_stylesheet: '-ss',
- template_engine: '-e',
- test_framework: '-t'
+ actions: "-a",
+ orm: "-o",
+ resource_controller: "-c",
+ scaffold_controller: "-c",
+ stylesheets: "-y",
+ stylesheet_engine: "-se",
+ scaffold_stylesheet: "-ss",
+ template_engine: "-e",
+ test_framework: "-t"
},
test_unit: {
- fixture_replacement: '-r',
+ fixture_replacement: "-r",
}
}
@@ -50,8 +54,6 @@ module Rails
force_plural: false,
helper: true,
integration_tool: nil,
- javascripts: true,
- javascript_engine: :js,
orm: false,
resource_controller: :controller,
resource_route: true,
@@ -59,335 +61,258 @@ 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
-
- def self.templates_path #:nodoc:
- @templates_path ||= []
- end
-
- def self.aliases #:nodoc:
- @aliases ||= DEFAULT_ALIASES.dup
- end
-
- def self.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
-
- # Configure generators for API only applications. It basically hides
- # everything that is usually browser related, such as assets and session
- # migration generators, and completely disable views, helpers and assets
- # 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
- )
- end
-
- # Remove the color from output.
- def self.no_color!
- Thor::Base.shell = Thor::Shell::Basic
- end
-
- # Track all generators subclasses.
- def self.subclasses
- @subclasses ||= []
- 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
- # 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}"
+ def templates_path #:nodoc:
+ @templates_path ||= []
end
- lookup(lookups)
+ def aliases #:nodoc:
+ @aliases ||= DEFAULT_ALIASES.dup
+ end
- namespaces = Hash[subclasses.map { |klass| [klass.namespace, klass] }]
+ def options #:nodoc:
+ @options ||= DEFAULT_OPTIONS.dup
+ end
- lookups.each do |namespace|
- klass = namespaces[namespace]
- return klass if klass
+ # 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
- invoke_fallbacks_for(name, base) || invoke_fallbacks_for(context, name)
- 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 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][:template_engine] = :erb
+ end
+ 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 ") }\n"
- msg << "Run `rails generate --help` for more options."
- puts msg
+ # Remove the color from output.
+ def no_color!
+ Thor::Base.shell = Thor::Shell::Basic
end
- 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",
- "#{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
+ # 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
- def self.public_namespaces
- lookup!
- subclasses.map(&:namespace)
- end
+ print_generators
+ end
- def self.print_generators
- sorted_groups.each { |b, n| print_list(b, n) }
- end
+ def public_namespaces
+ lookup!
+ subclasses.map(&:namespace)
+ end
- def self.sorted_groups
- namespaces = public_namespaces
- namespaces.sort!
- 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")
- hidden_namespaces.each { |n| groups.delete(n.to_s) }
+ def sorted_groups
+ namespaces = public_namespaces
+ namespaces.sort!
- [["rails", rails]] + groups.sort.to_a
- end
+ groups = Hash.new { |h, k| h[k] = [] }
+ namespaces.each do |namespace|
+ base = namespace.split(":").first
+ groups[base] << namespace
+ end
- protected
-
- # This code is based directly on the Text gem implementation
- # Returns a value representing the "cost" of transforming str1 into str2
- def self.levenshtein_distance str1, str2
- s = str1
- t = str2
- n = s.length
- m = t.length
-
- return m if (0 == n)
- return n if (0 == m)
-
- d = (0..m).to_a
- x = nil
-
- # avoid duplicating an enumerable object in the loop
- str2_codepoint_enumerable = str2.each_codepoint
-
- str1.each_codepoint.with_index do |char1, i|
- e = i+1
-
- str2_codepoint_enumerable.with_index do |char2, j|
- cost = (char1 == char2) ? 0 : 1
- x = [
- d[j+1] + 1, # insertion
- e + 1, # deletion
- d[j] + cost # substitution
- ].min
- d[j] = e
- e = x
- end
+ rails = groups.delete("rails")
+ rails.map! { |n| n.sub(/^rails:/, "") }
+ rails.delete("app")
+ rails.delete("plugin")
+ rails.delete("encrypted_secrets")
+ rails.delete("encrypted_file")
+ rails.delete("encryption_key_file")
+ rails.delete("master_key")
+ rails.delete("credentials")
- d[m] = x
- end
+ hidden_namespaces.each { |n| groups.delete(n.to_s) }
- x
+ [[ "rails", rails ]] + groups.sort.to_a
end
- # Prints a list of generators.
- def self.print_list(base, namespaces) #:nodoc:
- namespaces = namespaces.reject do |n|
- hidden_namespaces.include?(n)
+ # 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
- return if namespaces.empty?
- puts "#{base.camelize}:"
+ lookup(lookups)
- namespaces.each do |namespace|
- puts(" #{namespace}")
+ namespaces = Hash[subclasses.map { |klass| [klass.namespace, klass] }]
+ lookups.each do |namespace|
+ klass = namespaces[namespace]
+ return klass if klass
end
- puts
+ invoke_fallbacks_for(name, base) || invoke_fallbacks_for(context, name)
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 = []
+ # 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)
+ suggestion = Rails::Command::Spellchecker.suggest(namespace.to_s, from: options)
+ puts <<~MSG
+ Could not find generator '#{namespace}'. Maybe you meant #{suggestion.inspect}?
+ Run `rails generate --help` for more options.
+ MSG
+ end
+ end
- 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 = []
+
+ Array(fallbacks[base.to_sym]).each do |fallback|
+ next if invoked_fallbacks.include?(fallback)
+ invoked_fallbacks << fallback
- # Receives namespaces in an array and tries to find matching generators
- # in the load path.
- def self.lookup(namespaces) #:nodoc:
- paths = namespaces_to_paths(namespaces)
-
- paths.each do |raw_path|
- ["rails/generators", "generators"].each do |base|
- path = "#{base}/#{raw_path}_generator"
-
- begin
- require path
- return
- rescue LoadError => e
- raise unless e.message =~ /#{Regexp.escape(path)}$/
- rescue Exception => e
- warn "[WARNING] Could not load generator #{path.inspect}. Error: #{e.message}.\n#{e.backtrace.join("\n")}"
- end
+ klass = find_by_namespace(name, fallback)
+ return klass if klass
end
+
+ nil
end
- end
- # This will try to load any generator in the load path to show in help.
- def self.lookup! #:nodoc:
- $LOAD_PATH.each do |base|
- Dir[File.join(base, "{rails/generators,generators}", "**", "*_generator.rb")].each do |path|
- begin
- path = path.sub("#{base}/", "")
- require path
- rescue Exception
- # No problem
- end
- end
+ def command_type # :doc:
+ @command_type ||= "generator"
end
- end
- # 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 self.namespaces_to_paths(namespaces) #:nodoc:
- paths = []
- namespaces.each do |namespace|
- pieces = namespace.split(":")
- paths << pieces.dup.push(pieces.last).join("/")
- paths << pieces.join("/")
+ def lookup_paths # :doc:
+ @lookup_paths ||= %w( rails/generators generators )
end
- paths.uniq!
- paths
- 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 560a553789..78d2471890 100644
--- a/railties/lib/rails/generators/actions.rb
+++ b/railties/lib/rails/generators/actions.rb
@@ -1,11 +1,13 @@
-require 'open-uri'
+# frozen_string_literal: true
+
+require "active_support/core_ext/string/strip"
module Rails
module Generators
module Actions
def initialize(*) # :nodoc:
super
- @in_group = nil
+ @indentation = 0
@after_bundle_callbacks = []
end
@@ -13,29 +15,32 @@ module Rails
#
# gem "rspec", group: :test
# gem "technoweenie-restful-authentication", lib: "restful-authentication", source: "http://gems.github.com/"
- # gem "rails", "3.0", git: "git://github.com/rails/rails"
+ # gem "rails", "3.0", git: "https://github.com/rails/rails"
+ # gem "RedCloth", ">= 4.1.0", "< 4.2.0"
def gem(*args)
options = args.extract_options!
- name, version = args
+ name, *versions = args
# Set the message to be shown in logs. Uses the git repo if one is given,
# otherwise use name (version).
- parts, message = [ quote(name) ], name
- if version ||= options.delete(:version)
- parts << quote(version)
- message << " (#{version})"
+ parts, message = [ quote(name) ], name.dup
+
+ if versions = versions.any? ? versions : options.delete(:version)
+ _versions = Array(versions)
+ _versions.each do |version|
+ parts << quote(version)
+ end
+ message << " (#{_versions.join(", ")})"
end
message = options[:git] if options[:git]
log :gemfile, message
- options.each do |option, value|
- parts << "#{option}: #{quote(value)}"
- end
+ parts << quote(options) unless options.empty?
in_root do
str = "gem #{parts.join(", ")}"
- str = " " + str if @in_group
+ str = indentation + str
str = "\n" + str
append_file "Gemfile", str, verbose: false
end
@@ -47,17 +52,29 @@ module Rails
# gem "rspec-rails"
# end
def gem_group(*names, &block)
- name = names.map(&:inspect).join(", ")
- log :gemfile, "group #{name}"
+ options = names.extract_options!
+ str = names.map(&:inspect)
+ str << quote(options) unless options.empty?
+ str = str.join(", ")
+ log :gemfile, "group #{str}"
in_root do
- append_file "Gemfile", "\ngroup #{name} do", force: true
+ append_file "Gemfile", "\ngroup #{str} do", force: true
+ with_indentation(&block)
+ append_file "Gemfile", "\nend\n", force: true
+ end
+ end
- @in_group = true
- instance_eval(&block)
- @in_group = false
+ def github(repo, options = {}, &block)
+ str = [quote(repo)]
+ str << quote(options) unless options.empty?
+ str = str.join(", ")
+ log :github, "github #{str}"
- append_file "Gemfile", "\nend\n", force: true
+ in_root do
+ append_file "Gemfile", "\n#{indentation}github #{str} do", force: true
+ with_indentation(&block)
+ append_file "Gemfile", "\n#{indentation}end", force: true
end
end
@@ -70,15 +87,13 @@ module Rails
# add_source "http://gems.github.com/" do
# gem "rspec-rails"
# end
- def add_source(source, options={}, &block)
+ def add_source(source, options = {}, &block)
log :source, source
in_root do
if block
- append_file "Gemfile", "source #{quote(source)} do", force: true
- @in_group = true
- instance_eval(&block)
- @in_group = false
+ append_file "Gemfile", "\nsource #{quote(source)} do", force: true
+ with_indentation(&block)
append_file "Gemfile", "\nend\n", force: true
else
prepend_file "Gemfile", "source #{quote(source)}\n", verbose: false
@@ -92,23 +107,23 @@ module Rails
# file in <tt>config/environments</tt>.
#
# environment do
- # "config.autoload_paths += %W(#{config.root}/extras)"
+ # "config.action_controller.asset_host = 'cdn.provider.com'"
# end
#
# environment(nil, env: "development") do
- # "config.autoload_paths += %W(#{config.root}/extras)"
+ # "config.action_controller.asset_host = 'localhost:3000'"
# end
- def environment(data=nil, options={})
- sentinel = /class [a-z_:]+ < Rails::Application/i
- env_file_sentinel = /Rails\.application\.configure do/
- data = yield if !data && block_given?
+ def environment(data = nil, options = {})
+ sentinel = "class Application < Rails::Application\n"
+ env_file_sentinel = "Rails.application.configure do\n"
+ data ||= yield if block_given?
in_root do
if options[:env].nil?
- inject_into_file 'config/application.rb', "\n #{data}", after: sentinel, verbose: false
+ inject_into_file "config/application.rb", optimize_indentation(data, 4), after: sentinel, verbose: false
else
Array(options[:env]).each do |env|
- inject_into_file "config/environments/#{env}.rb", "\n #{data}", after: env_file_sentinel, verbose: false
+ inject_into_file "config/environments/#{env}.rb", optimize_indentation(data, 2), after: env_file_sentinel, verbose: false
end
end
end
@@ -120,7 +135,7 @@ module Rails
# git :init
# git add: "this.file that.rb"
# git add: "onefile.rb", rm: "badfile.cxx"
- def git(commands={})
+ def git(commands = {})
if commands.is_a?(Symbol)
run "git #{commands}"
else
@@ -139,12 +154,13 @@ module Rails
# end
#
# vendor("foreign.rb", "# Foreign code is fun")
- def vendor(filename, data=nil, &block)
+ def vendor(filename, data = nil)
log :vendor, filename
- create_file("vendor/#{filename}", data, verbose: false, &block)
+ data ||= yield if block_given?
+ create_file("vendor/#{filename}", optimize_indentation(data), verbose: false)
end
- # Create a new file in the lib/ directory. Code can be specified
+ # Create a new file in the <tt>lib/</tt> directory. Code can be specified
# in a block or a data string can be given.
#
# lib("crypto.rb") do
@@ -152,9 +168,10 @@ module Rails
# end
#
# lib("foreign.rb", "# Foreign code is fun")
- def lib(filename, data=nil, &block)
+ def lib(filename, data = nil)
log :lib, filename
- create_file("lib/#{filename}", data, verbose: false, &block)
+ data ||= yield if block_given?
+ create_file("lib/#{filename}", optimize_indentation(data), verbose: false)
end
# Create a new +Rakefile+ with the provided code (either in a block or a string).
@@ -172,9 +189,10 @@ module Rails
# end
#
# rakefile('seed.rake', 'puts "Planting seeds"')
- def rakefile(filename, data=nil, &block)
+ def rakefile(filename, data = nil)
log :rakefile, filename
- create_file("lib/tasks/#{filename}", data, verbose: false, &block)
+ data ||= yield if block_given?
+ create_file("lib/tasks/#{filename}", optimize_indentation(data), verbose: false)
end
# Create a new initializer with the provided code (either in a block or a string).
@@ -190,9 +208,10 @@ module Rails
# end
#
# initializer("api.rb", "API_KEY = '123456'")
- def initializer(filename, data=nil, &block)
+ def initializer(filename, data = nil)
log :initializer, filename
- create_file("config/initializers/#{filename}", data, verbose: false, &block)
+ data ||= yield if block_given?
+ create_file("config/initializers/#{filename}", optimize_indentation(data), verbose: false)
end
# Generate something using a generator from Rails or a plugin.
@@ -207,22 +226,31 @@ module Rails
in_root { run_ruby_script("bin/rails generate #{what} #{argument}", verbose: false) }
end
- # Runs the supplied rake task
+ # Runs the supplied rake task (invoked with 'rake ...')
#
# rake("db:migrate")
# rake("db:migrate", env: "production")
# rake("gems:install", sudo: true)
- def rake(command, options={})
- log :rake, command
- env = options[:env] || ENV["RAILS_ENV"] || 'development'
- sudo = options[:sudo] && RbConfig::CONFIG['host_os'] !~ /mswin|mingw/ ? 'sudo ' : ''
- in_root { run("#{sudo}#{extify(:rake)} #{command} RAILS_ENV=#{env}", verbose: false) }
+ # rake("gems:install", capture: true)
+ def rake(command, options = {})
+ execute_command :rake, command, options
+ end
+
+ # Runs the supplied rake task (invoked with 'rails ...')
+ #
+ # rails_command("db:migrate")
+ # rails_command("db:migrate", env: "production")
+ # rails_command("gems:install", sudo: true)
+ # rails_command("gems:install", capture: true)
+ def rails_command(command, options = {})
+ execute_command :rails, command, options
end
# Just run the capify command in root
#
# capify!
def capify!
+ ActiveSupport::Deprecation.warn("`capify!` is deprecated and will be removed in the next version of Rails.")
log :capify, ""
in_root { run("#{extify(:capify)} .", verbose: false) }
end
@@ -235,7 +263,7 @@ module Rails
sentinel = /\.routes\.draw do\s*\n/m
in_root do
- inject_into_file 'config/routes.rb', " #{routing_code}\n", { after: sentinel, verbose: false, force: true }
+ inject_into_file "config/routes.rb", optimize_indentation(routing_code, 2), after: sentinel, verbose: false, force: false
end
end
@@ -256,23 +284,36 @@ 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 = {}) # :doc:
+ log executor, command
+ env = options[:env] || ENV["RAILS_ENV"] || "development"
+ sudo = options[:sudo] && !Gem.win_platform? ? "sudo " : ""
+ config = { verbose: false }
+
+ config[:capture] = options[:capture] if options[:capture]
+
+ in_root { run("#{sudo}#{extify(executor)} #{command} RAILS_ENV=#{env}", config) }
+ end
+
# Add an extension to the given name based on the platform.
- def extify(name)
- if RbConfig::CONFIG['host_os'] =~ /mswin|mingw/
+ def extify(name) # :doc:
+ if Gem.win_platform?
"#{name}.bat"
else
name
@@ -281,7 +322,12 @@ 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:
+ if value.respond_to? :each_pair
+ return value.map do |k, v|
+ "#{k}: #{quote(v)}"
+ end.join(", ")
+ end
return value.inspect unless value.is_a? String
if value.include?("'")
@@ -290,6 +336,30 @@ module Rails
"'#{value}'"
end
end
+
+ # Returns optimized string with indentation
+ def optimize_indentation(value, amount = 0) # :doc:
+ return "#{value}\n" unless value.is_a?(String)
+
+ if value.lines.size > 1
+ value.strip_heredoc.indent(amount)
+ else
+ "#{value.strip.indent(amount)}\n"
+ end
+ end
+
+ # Indent the +Gemfile+ to the depth of @indentation
+ def indentation # :doc:
+ " " * @indentation
+ end
+
+ # Manage +Gemfile+ indentation for a DSL action block
+ def with_indentation(&block) # :doc:
+ @indentation += 1
+ instance_eval(&block)
+ ensure
+ @indentation -= 1
+ end
end
end
end
diff --git a/railties/lib/rails/generators/actions/create_migration.rb b/railties/lib/rails/generators/actions/create_migration.rb
index cffdef6ec9..05bc242447 100644
--- a/railties/lib/rails/generators/actions/create_migration.rb
+++ b/railties/lib/rails/generators/actions/create_migration.rb
@@ -1,10 +1,12 @@
-require 'thor/actions'
+# frozen_string_literal: true
+
+require "fileutils"
+require "thor/actions"
module Rails
module Generators
module Actions
- class CreateMigration < Thor::Actions::CreateFile
-
+ class CreateMigration < Thor::Actions::CreateFile #:nodoc:
def migration_dir
File.dirname(@destination)
end
@@ -37,32 +39,32 @@ module Rails
end
alias :exists? :existing_migration
- protected
+ private
- def on_conflict_behavior
- options = base.options.merge(config)
- if identical?
- say_status :identical, :blue, relative_existing_migration
- elsif options[:force]
- say_status :remove, :green, relative_existing_migration
- say_status :create, :green
- unless pretend?
- ::FileUtils.rm_rf(existing_migration)
- yield
+ def on_conflict_behavior # :doc:
+ options = base.options.merge(config)
+ if identical?
+ say_status :identical, :blue, relative_existing_migration
+ elsif options[:force]
+ say_status :remove, :green, relative_existing_migration
+ say_status :create, :green
+ unless pretend?
+ ::FileUtils.rm_rf(existing_migration)
+ yield
+ end
+ elsif options[:skip]
+ 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 " \
+ "or --skip to ignore conflicted file."
end
- elsif options[:skip]
- 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 " +
- "or --skip to ignore conflicted file."
end
- end
- def say_status(status, color, message = relative_destination)
- base.shell.say_status(status, message, color) if config[:verbose]
- end
+ def say_status(status, color, message = relative_destination) # :doc:
+ base.shell.say_status(status, message, color) if config[:verbose]
+ end
end
end
end
diff --git a/railties/lib/rails/generators/active_model.rb b/railties/lib/rails/generators/active_model.rb
index 6183944bb0..8df8eb2438 100644
--- a/railties/lib/rails/generators/active_model.rb
+++ b/railties/lib/rails/generators/active_model.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Rails
module Generators
# ActiveModel is a class to be implemented by each ORM to allow Rails to
@@ -39,13 +41,13 @@ module Rails
# GET edit
# PATCH/PUT update
# DELETE destroy
- def self.find(klass, params=nil)
+ def self.find(klass, params = nil)
"#{klass}.find(#{params})"
end
# GET new
# POST create
- def self.build(klass, params=nil)
+ def self.build(klass, params = nil)
if params
"#{klass}.new(#{params})"
else
@@ -59,7 +61,7 @@ module Rails
end
# PATCH/PUT update
- def update(params=nil)
+ def update(params = nil)
"#{name}.update(#{params})"
end
diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb
index 249fe96772..4dc4d27a46 100644
--- a/railties/lib/rails/generators/app_base.rb
+++ b/railties/lib/rails/generators/app_base.rb
@@ -1,15 +1,17 @@
-require 'digest/md5'
-require 'active_support/core_ext/string/strip'
-require 'rails/version' unless defined?(Rails::VERSION)
-require 'open-uri'
-require 'uri'
-require 'rails/generators'
-require 'active_support/core_ext/array/extract_options'
+# frozen_string_literal: true
+
+require "fileutils"
+require "digest/md5"
+require "rails/version" unless defined?(Rails::VERSION)
+require "open-uri"
+require "uri"
+require "rails/generators"
+require "active_support/core_ext/array/extract_options"
module Rails
module Generators
class AppBase < Base # :nodoc:
- DATABASES = %w( mysql oracle postgresql sqlite3 frontbase ibm_db sqlserver )
+ DATABASES = %w( mysql postgresql sqlite3 oracle frontbase ibm_db sqlserver )
JDBC_DATABASES = %w( jdbcmysql jdbcsqlite3 jdbcpostgresql jdbc )
DATABASES.concat(JDBC_DATABASES)
@@ -23,63 +25,75 @@ module Rails
end
def self.add_shared_options_for(name)
- class_option :template, type: :string, aliases: '-m',
- desc: "Path to some #{name} template (can be a filesystem path or URL)"
+ class_option :template, type: :string, aliases: "-m",
+ desc: "Path to some #{name} template (can be a filesystem path or URL)"
+
+ class_option :database, type: :string, aliases: "-d", default: "sqlite3",
+ desc: "Preconfigure for selected database (options: #{DATABASES.join('/')})"
+
+ class_option :skip_gemfile, type: :boolean, default: false,
+ desc: "Don't create a Gemfile"
+
+ class_option :skip_git, type: :boolean, aliases: "-G", default: false,
+ desc: "Skip .gitignore file"
- class_option :skip_gemfile, type: :boolean, default: false,
- desc: "Don't create a Gemfile"
+ class_option :skip_keeps, type: :boolean, default: false,
+ desc: "Skip source control .keep files"
- class_option :skip_bundle, type: :boolean, aliases: '-B', default: false,
- desc: "Don't run bundle install"
+ class_option :skip_action_mailer, type: :boolean, aliases: "-M",
+ default: false,
+ desc: "Skip Action Mailer files"
- class_option :skip_git, type: :boolean, aliases: '-G', default: false,
- desc: 'Skip .gitignore file'
+ class_option :skip_active_record, type: :boolean, aliases: "-O", default: false,
+ desc: "Skip Active Record files"
- class_option :skip_keeps, type: :boolean, default: false,
- desc: 'Skip source control .keep files'
+ class_option :skip_active_storage, type: :boolean, default: false,
+ desc: "Skip Active Storage files"
- class_option :skip_action_mailer, type: :boolean, aliases: "-M",
- default: false,
- desc: "Skip Action Mailer files"
+ class_option :skip_puma, type: :boolean, aliases: "-P", default: false,
+ desc: "Skip Puma related files"
- class_option :skip_active_record, type: :boolean, aliases: '-O', default: false,
- desc: 'Skip Active Record files'
+ class_option :skip_action_cable, type: :boolean, aliases: "-C", default: false,
+ desc: "Skip Action Cable files"
- class_option :skip_sprockets, type: :boolean, aliases: '-S', default: false,
- desc: 'Skip Sprockets files'
+ class_option :skip_sprockets, type: :boolean, aliases: "-S", default: false,
+ desc: "Skip Sprockets files"
- class_option :skip_spring, type: :boolean, default: false,
- desc: "Don't install Spring application preloader"
+ class_option :skip_spring, type: :boolean, default: false,
+ desc: "Don't install Spring application preloader"
- class_option :database, type: :string, aliases: '-d', default: 'sqlite3',
- desc: "Preconfigure for selected database (options: #{DATABASES.join('/')})"
+ class_option :skip_listen, type: :boolean, default: false,
+ desc: "Don't generate configuration that depends on the listen gem"
- class_option :javascript, type: :string, aliases: '-j', default: 'jquery',
- desc: 'Preconfigure for selected JavaScript library'
+ class_option :skip_javascript, type: :boolean, aliases: "-J", default: name == "plugin",
+ desc: "Skip JavaScript files"
- class_option :skip_javascript, type: :boolean, aliases: '-J', default: false,
- desc: 'Skip JavaScript files'
+ class_option :skip_turbolinks, type: :boolean, default: false,
+ desc: "Skip turbolinks gem"
- class_option :dev, type: :boolean, default: false,
- desc: "Setup the #{name} with Gemfile pointing to your Rails checkout"
+ class_option :skip_test, type: :boolean, aliases: "-T", default: false,
+ desc: "Skip test files"
- class_option :edge, type: :boolean, default: false,
- desc: "Setup the #{name} with Gemfile pointing to Rails repository"
+ class_option :skip_system_test, type: :boolean, default: false,
+ desc: "Skip system test files"
- class_option :skip_turbolinks, type: :boolean, default: false,
- desc: 'Skip turbolinks gem'
+ class_option :skip_bootsnap, type: :boolean, default: false,
+ desc: "Skip bootsnap gem"
- class_option :skip_test, type: :boolean, aliases: '-T', default: false,
- desc: 'Skip test files'
+ class_option :dev, type: :boolean, default: false,
+ desc: "Setup the #{name} with Gemfile pointing to your Rails checkout"
- class_option :rc, type: :string, default: false,
- desc: "Path to file containing extra configuration options for rails command"
+ class_option :edge, type: :boolean, default: false,
+ desc: "Setup the #{name} with Gemfile pointing to Rails repository"
- class_option :no_rc, type: :boolean, default: false,
- desc: 'Skip loading of extra configuration options from .railsrc file'
+ class_option :rc, type: :string, default: nil,
+ desc: "Path to file containing extra configuration options for rails command"
- class_option :help, type: :boolean, aliases: '-h', group: :rails,
- desc: 'Show this help message and quit'
+ class_option :no_rc, type: :boolean, default: false,
+ desc: "Skip loading of extra configuration options from .railsrc file"
+
+ class_option :help, type: :boolean, aliases: "-h", group: :rails,
+ desc: "Show this help message and quit"
end
def initialize(*args)
@@ -89,9 +103,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]
@@ -107,23 +121,26 @@ 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,
+ cable_gemfile_entry,
@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)
@@ -131,55 +148,85 @@ 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 '.'
+ 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]
+ self.rails_template = \
+ case options[:template]
when /^https?:\/\//
options[:template]
when String
File.expand_path(options[:template], Dir.pwd)
else
options[:template]
- end
+ end
end
- def database_gemfile_entry
+ def database_gemfile_entry # :doc:
return [] if options[:skip_active_record]
- GemfileEntry.version gem_for_database, nil,
+ gem_name, gem_version = gem_for_database
+ GemfileEntry.version gem_name, gem_version,
"Use #{options[:database]} as the database for Active Record"
end
- def include_all_railties?
- options.values_at(:skip_active_record, :skip_action_mailer, :skip_test, :skip_sprockets).none?
+ def webserver_gemfile_entry # :doc:
+ return [] if options[:skip_puma]
+ comment = "Use Puma as the app server"
+ GemfileEntry.new("puma", "~> 3.11", comment)
+ end
+
+ def include_all_railties? # :doc:
+ [
+ options.values_at(
+ :skip_active_record,
+ :skip_action_mailer,
+ :skip_test,
+ :skip_sprockets,
+ :skip_action_cable
+ ),
+ skip_active_storage?
+ ].flatten.none?
end
- def comment_if(value)
- options[value] ? '# ' : ''
+ def comment_if(value) # :doc:
+ question = "#{value}?"
+
+ comment =
+ if respond_to?(question, true)
+ send(question)
+ else
+ options[value]
+ end
+
+ comment ? "# " : ""
end
- def keeps?
+ def keeps? # :doc:
!options[:skip_keeps]
end
- def sqlite3?
- !options[:skip_active_record] && options[:database] == 'sqlite3'
+ def sqlite3? # :doc:
+ !options[:skip_active_record] && options[:database] == "sqlite3"
+ end
+
+ def skip_active_storage? # :doc:
+ options[:skip_active_storage] || options[:skip_active_record]
end
class GemfileEntry < Struct.new(:name, :version, :comment, :options, :commented_out)
@@ -202,117 +249,119 @@ module Rails
def self.path(name, path, comment = nil)
new(name, nil, comment, path: path)
end
+
+ def version
+ version = super
+
+ if version.is_a?(Array)
+ version.join("', '")
+ else
+ version
+ end
+ end
end
def rails_gemfile_entry
if options.dev?
[
- GemfileEntry.path('rails', Rails::Generators::RAILS_DEV_PATH),
- GemfileEntry.github('sprockets-rails', 'rails/sprockets-rails'),
- GemfileEntry.github('arel', 'rails/arel')
+ GemfileEntry.path("rails", Rails::Generators::RAILS_DEV_PATH)
]
elsif options.edge?
[
- GemfileEntry.github('rails', 'rails/rails'),
- GemfileEntry.github('sprockets-rails', 'rails/sprockets-rails'),
- GemfileEntry.github('arel', 'rails/arel')
+ GemfileEntry.github("rails", "rails/rails")
]
else
- [GemfileEntry.version('rails',
- Rails::VERSION::STRING,
+ [GemfileEntry.version("rails",
+ rails_version_specifier,
"Bundle edge Rails instead: gem 'rails', github: 'rails/rails'")]
end
end
+ def rails_version_specifier(gem_version = Rails.gem_version)
+ 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
+ end
+
def gem_for_database
- # %w( mysql oracle postgresql sqlite3 frontbase ibm_db sqlserver jdbcmysql jdbcsqlite3 jdbcpostgresql )
+ # %w( mysql postgresql sqlite3 oracle frontbase ibm_db sqlserver jdbcmysql jdbcsqlite3 jdbcpostgresql )
case options[:database]
- when "oracle" then "ruby-oci8"
- when "postgresql" then "pg"
- when "frontbase" then "ruby-frontbase"
- when "mysql" then "mysql2"
- when "sqlserver" then "activerecord-sqlserver-adapter"
- when "jdbcmysql" then "activerecord-jdbcmysql-adapter"
- when "jdbcsqlite3" then "activerecord-jdbcsqlite3-adapter"
- when "jdbcpostgresql" then "activerecord-jdbcpostgresql-adapter"
- when "jdbc" then "activerecord-jdbc-adapter"
- else options[:database]
+ when "mysql" then ["mysql2", [">= 0.4.4"]]
+ when "postgresql" then ["pg", [">= 0.18", "< 2.0"]]
+ when "oracle" then ["activerecord-oracle_enhanced-adapter", nil]
+ when "frontbase" then ["ruby-frontbase", nil]
+ when "sqlserver" then ["activerecord-sqlserver-adapter", nil]
+ when "jdbcmysql" then ["activerecord-jdbcmysql-adapter", nil]
+ when "jdbcsqlite3" then ["activerecord-jdbcsqlite3-adapter", nil]
+ when "jdbcpostgresql" then ["activerecord-jdbcpostgresql-adapter", nil]
+ when "jdbc" then ["activerecord-jdbc-adapter", nil]
+ else [options[:database], nil]
end
end
def convert_database_option_for_jruby
if defined?(JRUBY_VERSION)
- case options[:database]
- when "oracle" then options[:database].replace "jdbc"
- when "postgresql" then options[:database].replace "jdbcpostgresql"
- when "mysql" then options[:database].replace "jdbcmysql"
- when "sqlite3" then options[:database].replace "jdbcsqlite3"
+ opt = options.dup
+ case opt[:database]
+ when "postgresql" then opt[:database] = "jdbcpostgresql"
+ when "mysql" then opt[:database] = "jdbcmysql"
+ when "sqlite3" then opt[:database] = "jdbcsqlite3"
end
+ self.options = opt.freeze
end
end
def assets_gemfile_entry
return [] if options[:skip_sprockets]
- gems = []
- gems << GemfileEntry.version('sass-rails', '~> 5.0',
- 'Use SCSS for stylesheets')
-
- gems << GemfileEntry.version('uglifier',
- '>= 1.3.0',
- 'Use Uglifier as compressor for JavaScript assets')
-
- gems
+ GemfileEntry.version("sass-rails", "~> 5.0", "Use SCSS for stylesheets")
end
- def jbuilder_gemfile_entry
- return [] if options[:api]
-
- comment = 'Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder'
- GemfileEntry.version('jbuilder', '~> 2.0', comment)
- end
+ def webpacker_gemfile_entry
+ return [] if options[:skip_javascript]
- def coffee_gemfile_entry
- comment = 'Use CoffeeScript for .coffee assets and views'
if options.dev? || options.edge?
- GemfileEntry.github 'coffee-rails', 'rails/coffee-rails', nil, comment
+ GemfileEntry.github "webpacker", "rails/webpacker", nil, "Use development version of Webpacker"
else
- GemfileEntry.version 'coffee-rails', '~> 4.1.0', comment
+ GemfileEntry.new "webpacker", nil, "Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker"
end
end
- def javascript_gemfile_entry
- if options[:skip_javascript]
- []
- else
- gems = [coffee_gemfile_entry, javascript_runtime_gemfile_entry]
- gems << GemfileEntry.version("#{options[:javascript]}-rails", nil,
- "Use #{options[:javascript]} as the JavaScript library")
-
- unless options[:skip_turbolinks]
- gems << GemfileEntry.version("turbolinks", nil,
- "Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks")
- end
-
- gems
- 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]
end
- def javascript_runtime_gemfile_entry
- comment = 'See https://github.com/rails/execjs#readme for more supported runtimes'
- if defined?(JRUBY_VERSION)
- GemfileEntry.version 'therubyrhino', nil, comment
+ def javascript_gemfile_entry
+ if options[:skip_javascript] || options[:skip_turbolinks]
+ []
else
- GemfileEntry.new 'therubyracer', nil, comment, { platforms: :ruby }, true
+ [ GemfileEntry.version("turbolinks", "~> 5",
+ "Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks") ]
end
end
def psych_gemfile_entry
return [] unless defined?(Rubinius)
- comment = 'Use Psych as the YAML engine, instead of Syck, so serialized ' \
- 'data can be read safely from different rubies (see http://git.io/uuLVag)'
- GemfileEntry.new('psych', '~> 2.0', comment, platforms: :rbx)
+ comment = "Use Psych as the YAML engine, instead of Syck, so serialized " \
+ "data can be read safely from different rubies (see http://git.io/uuLVag)"
+ GemfileEntry.new("psych", "~> 2.0", comment, platforms: :rbx)
+ end
+
+ def cable_gemfile_entry
+ return [] if options[:skip_action_cable]
+ comment = "Use Redis adapter to run Action Cable in production"
+ gems = []
+ gems << GemfileEntry.new("redis", "~> 4.0", comment, {}, true)
+ gems
end
def bundle_command(command)
@@ -326,9 +375,9 @@ module Rails
# We unset temporary bundler variables to load proper bundler and Gemfile.
#
# Thanks to James Tucker for the Gem tricks involved in this call.
- _bundle_command = Gem.bin_path('bundler', 'bundle')
+ _bundle_command = Gem.bin_path("bundler", "bundle")
- require 'bundler'
+ require "bundler"
Bundler.with_clean_env do
full_command = %Q["#{Gem.ruby}" "#{_bundle_command}" #{command}]
if options[:quiet]
@@ -347,8 +396,41 @@ module Rails
!options[:skip_spring] && !options.dev? && Process.respond_to?(:fork) && !RUBY_PLATFORM.include?("cygwin")
end
+ def webpack_install?
+ !(options[:skip_javascript] || options[:skip_webpack_install])
+ 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
+
+ def depend_on_bootsnap?
+ !options[:skip_bootsnap] && !options[:dev] && !defined?(JRUBY_VERSION)
+ end
+
+ def os_supports_listen_out_of_the_box?
+ RbConfig::CONFIG["host_os"] =~ /darwin|linux/
+ end
+
def run_bundle
- bundle_command('install') if bundle_install?
+ bundle_command("install") if bundle_install?
+ end
+
+ def run_webpack
+ if webpack_install?
+ rails_command "webpacker:install"
+ rails_command "webpacker:install:#{options[:webpack]}" if options[:webpack] && options[:webpack] != "webpack"
+ end
+ end
+
+ def generate_bundler_binstub
+ if bundle_install?
+ bundle_command("binstubs bundler")
+ end
end
def generate_spring_binstubs
diff --git a/railties/lib/rails/generators/base.rb b/railties/lib/rails/generators/base.rb
index 813b8b629e..5523a3f659 100644
--- a/railties/lib/rails/generators/base.rb
+++ b/railties/lib/rails/generators/base.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
begin
- require 'thor/group'
+ require "thor/group"
rescue LoadError
puts "Thor is not available.\nIf you ran this command from a git checkout " \
"of Rails, please make sure thor is installed,\nand run this command " \
@@ -16,18 +18,21 @@ module Rails
include Thor::Actions
include Rails::Generators::Actions
+ class_option :skip_namespace, type: :boolean, default: false,
+ desc: "Skip namespace (affects only isolated applications)"
+
add_runtime_options!
strict_args_position!
# Returns the source root for this generator using default_source_root as default.
- def self.source_root(path=nil)
+ def self.source_root(path = nil)
@_source_root = path if path
@_source_root ||= default_source_root
end
# Tries to get the description from a USAGE file one folder above the source
# root otherwise uses a default description.
- def self.desc(description=nil)
+ def self.desc(description = nil)
return super if description
@desc ||= if usage_path
@@ -40,15 +45,15 @@ module Rails
# Convenience method to get the namespace from the class name. It's the
# same as Thor default except that the Generator at the end of the class
# is removed.
- def self.namespace(name=nil)
+ def self.namespace(name = nil)
return super if name
- @namespace ||= super.sub(/_generator$/, '').sub(/:generators:/, ':')
+ @namespace ||= super.sub(/_generator$/, "").sub(/:generators:/, ":")
end
# Convenience method to hide this generator from the available ones when
# running rails generator command.
def self.hide!
- Rails::Generators.hide_namespace self.namespace
+ Rails::Generators.hide_namespace(namespace)
end
# Invoke a generator based on the value supplied by the user to the
@@ -103,12 +108,12 @@ module Rails
# hook_for :test_framework, as: :controller
# end
#
- # And now it will lookup at:
+ # And now it will look up at:
#
# "test_unit:controller", "test_unit"
#
- # Similarly, if you want it to also lookup in the rails namespace, you just
- # need to provide the :in value:
+ # Similarly, if you want it to also look up in the rails namespace, you
+ # just need to provide the :in value:
#
# class AwesomeGenerator < Rails::Generators::Base
# hook_for :test_framework, in: :rails, as: :controller
@@ -168,7 +173,7 @@ module Rails
names.each do |name|
unless class_options.key?(name)
defaults = if options[:type] == :boolean
- { }
+ {}
elsif [true, false].include?(default_value_for_option(name, options))
{ banner: "" }
else
@@ -195,7 +200,7 @@ module Rails
end
# Make class option aware of Rails::Generators.options and Rails::Generators.aliases.
- def self.class_option(name, options={}) #:nodoc:
+ def self.class_option(name, options = {}) #:nodoc:
options[:desc] = "Indicates when to generate #{name.to_s.humanize.downcase}" unless options.key?(:desc)
options[:aliases] = default_aliases_for_option(name, options)
options[:default] = default_value_for_option(name, options)
@@ -208,14 +213,14 @@ module Rails
def self.default_source_root
return unless base_name && generator_name
return unless default_generator_root
- path = File.join(default_generator_root, 'templates')
+ path = File.join(default_generator_root, "templates")
path if File.exist?(path)
end
# Returns the base root for a common set of generators. This is used to dynamically
# guess the default source root.
def self.base_root
- File.dirname(__FILE__)
+ __dir__
end
# Cache source root and add lib/generators/base/generator/templates to
@@ -230,7 +235,7 @@ module Rails
Rails::Generators.subclasses << base
Rails::Generators.templates_path.each do |path|
- if base.name.include?('::')
+ if base.name.include?("::")
base.source_paths << File.join(path, base.base_name, base.generator_name)
else
base.source_paths << File.join(path, base.generator_name)
@@ -239,11 +244,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|
@@ -251,35 +256,69 @@ module Rails
next if class_name.strip.empty?
# Split the class from its module nesting
- nesting = class_name.split('::')
+ nesting = class_name.split("::")
last_name = nesting.pop
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)
end
end
+ # Wrap block with namespace of current application
+ # if namespace exists and is not skipped
+ def module_namespacing(&block) # :doc:
+ content = capture(&block)
+ content = wrap_with_namespace(content) if namespaced?
+ concat(content)
+ end
+
+ 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) # :doc:
+ content = indent(content).chomp
+ "module #{namespace.name}\n#{content}\nend\n"
+ end
+
+ def namespace # :doc:
+ Rails::Generators.namespace
+ end
+
+ def namespaced? # :doc:
+ !options[:skip_namespace] && namespace
+ end
+
+ def namespace_dirs
+ @namespace_dirs ||= namespace.name.split("::").map(&:underscore)
+ end
+
+ def namespaced_path # :doc:
+ @namespaced_path ||= namespace_dirs.join("/")
+ end
+
# Use Rails default banner.
- def self.banner
- "rails generate #{namespace.sub(/^rails:/,'')} #{self.arguments.map(&:usage).join(' ')} [options]".gsub(/\s+/, ' ')
+ 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
+ if base = name.to_s.split("::").first
base.underscore
end
end
@@ -287,10 +326,10 @@ 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$/, '')
+ if generator = name.to_s.split("::").last
+ generator.sub!(/Generator$/, "")
generator.underscore
end
end
@@ -298,21 +337,21 @@ 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
- # Return default aliases for the option name given doing a lookup in
+ # 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
- # Return default for the option name given doing a lookup in config.
- def self.default_for_option(config, name, options, default)
- if generator_name and c = config[generator_name.to_sym] and c.key?(name)
+ # Returns default for the option name given doing a lookup in config.
+ 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 and c = config[base_name.to_sym] and c.key?(name)
+ elsif base_name && (c = config[base_name.to_sym]) && c.key?(name)
c[name]
elsif config[:rails].key?(name)
config[:rails][name]
@@ -331,7 +370,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 +382,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 +400,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,11 +408,10 @@ 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
-
end
end
end
diff --git a/railties/lib/rails/generators/css/assets/assets_generator.rb b/railties/lib/rails/generators/css/assets/assets_generator.rb
index e4a305f4b3..f657d1e50f 100644
--- a/railties/lib/rails/generators/css/assets/assets_generator.rb
+++ b/railties/lib/rails/generators/css/assets/assets_generator.rb
@@ -1,12 +1,14 @@
+# frozen_string_literal: true
+
require "rails/generators/named_base"
module Css # :nodoc:
module Generators # :nodoc:
class AssetsGenerator < Rails::Generators::NamedBase # :nodoc:
- source_root File.expand_path("../templates", __FILE__)
+ source_root File.expand_path("templates", __dir__)
def copy_stylesheet
- copy_file "stylesheet.css", File.join('app/assets/stylesheets', class_path, "#{file_name}.css")
+ copy_file "stylesheet.css", File.join("app/assets/stylesheets", class_path, "#{file_name}.css")
end
end
end
diff --git a/railties/lib/rails/generators/css/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/css/scaffold/scaffold_generator.rb
index cf534030f9..89c560f382 100644
--- a/railties/lib/rails/generators/css/scaffold/scaffold_generator.rb
+++ b/railties/lib/rails/generators/css/scaffold/scaffold_generator.rb
@@ -1,15 +1,17 @@
+# frozen_string_literal: true
+
require "rails/generators/named_base"
module Css # :nodoc:
module Generators # :nodoc:
class ScaffoldGenerator < Rails::Generators::NamedBase # :nodoc:
+ source_root Rails::Generators::ScaffoldGenerator.source_root
+
# In order to allow the Sass generators to pick up the default Rails CSS and
# transform it, we leave it in a standard location for the CSS stylesheet
# generators to handle. For the simple, default case, just copy it over.
def copy_stylesheet
- dir = Rails::Generators::ScaffoldGenerator.source_root
- file = File.join(dir, "scaffold.css")
- create_file "app/assets/stylesheets/scaffold.css", File.read(file)
+ copy_file "scaffold.css", "app/assets/stylesheets/scaffold.css"
end
end
end
diff --git a/railties/lib/rails/generators/erb.rb b/railties/lib/rails/generators/erb.rb
index 0755ac335c..ba20bcd32a 100644
--- a/railties/lib/rails/generators/erb.rb
+++ b/railties/lib/rails/generators/erb.rb
@@ -1,25 +1,27 @@
-require 'rails/generators/named_base'
+# frozen_string_literal: true
+
+require "rails/generators/named_base"
module Erb # :nodoc:
module Generators # :nodoc:
class Base < Rails::Generators::NamedBase #:nodoc:
- protected
+ private
- def formats
- [format]
- end
+ def formats
+ [format]
+ end
- def format
- :html
- end
+ def format
+ :html
+ end
- def handler
- :erb
- end
+ def handler
+ :erb
+ end
- def filename_with_extensions(name, format = self.format)
- [name, format, handler].compact.join(".")
- end
+ def filename_with_extensions(name, file_format = format)
+ [name, file_format, handler].compact.join(".")
+ end
end
end
end
diff --git a/railties/lib/rails/generators/erb/controller/controller_generator.rb b/railties/lib/rails/generators/erb/controller/controller_generator.rb
index 94c1b835d1..8e13744b2a 100644
--- a/railties/lib/rails/generators/erb/controller/controller_generator.rb
+++ b/railties/lib/rails/generators/erb/controller/controller_generator.rb
@@ -1,4 +1,6 @@
-require 'rails/generators/erb'
+# frozen_string_literal: true
+
+require "rails/generators/erb"
module Erb # :nodoc:
module Generators # :nodoc:
diff --git a/railties/lib/rails/generators/erb/controller/templates/view.html.erb b/railties/lib/rails/generators/erb/controller/templates/view.html.erb.tt
index cd54d13d83..cd54d13d83 100644
--- a/railties/lib/rails/generators/erb/controller/templates/view.html.erb
+++ b/railties/lib/rails/generators/erb/controller/templates/view.html.erb.tt
diff --git a/railties/lib/rails/generators/erb/mailer/mailer_generator.rb b/railties/lib/rails/generators/erb/mailer/mailer_generator.rb
index 65563aa6db..997602cb8c 100644
--- a/railties/lib/rails/generators/erb/mailer/mailer_generator.rb
+++ b/railties/lib/rails/generators/erb/mailer/mailer_generator.rb
@@ -1,4 +1,6 @@
-require 'rails/generators/erb'
+# frozen_string_literal: true
+
+require "rails/generators/erb"
module Erb # :nodoc:
module Generators # :nodoc:
@@ -6,13 +8,13 @@ module Erb # :nodoc:
argument :actions, type: :array, default: [], banner: "method method"
def copy_view_files
- view_base_path = File.join("app/views", class_path, file_name + '_mailer')
+ 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", filename_with_extensions("mailer", format))
- template filename_with_extensions(:layout, format), layout_path
+ layout_path = File.join("app/views/layouts", class_path, filename_with_extensions("mailer", format))
+ template filename_with_extensions(:layout, format), layout_path unless File.exist?(layout_path)
end
end
@@ -26,15 +28,15 @@ module Erb # :nodoc:
end
end
- protected
+ private
- def formats
- [:text, :html]
- end
+ def formats
+ [:text, :html]
+ end
- def file_name
- @_file_name ||= super.gsub(/\_mailer/i, '')
- end
+ def file_name
+ @_file_name ||= super.sub(/_mailer\z/i, "")
+ end
end
end
end
diff --git a/railties/lib/rails/generators/erb/mailer/templates/layout.html.erb b/railties/lib/rails/generators/erb/mailer/templates/layout.html.erb
deleted file mode 100644
index 93110e74ad..0000000000
--- a/railties/lib/rails/generators/erb/mailer/templates/layout.html.erb
+++ /dev/null
@@ -1,5 +0,0 @@
-<html>
- <body>
- <%%= yield %>
- </body>
-</html>
diff --git a/railties/lib/rails/generators/erb/mailer/templates/layout.html.erb.tt b/railties/lib/rails/generators/erb/mailer/templates/layout.html.erb.tt
new file mode 100644
index 0000000000..55f3675d49
--- /dev/null
+++ b/railties/lib/rails/generators/erb/mailer/templates/layout.html.erb.tt
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <style>
+ /* Email styles need to be inline */
+ </style>
+ </head>
+
+ <body>
+ <%%= yield %>
+ </body>
+</html>
diff --git a/railties/lib/rails/generators/erb/mailer/templates/layout.text.erb b/railties/lib/rails/generators/erb/mailer/templates/layout.text.erb.tt
index 6363733e6e..6363733e6e 100644
--- a/railties/lib/rails/generators/erb/mailer/templates/layout.text.erb
+++ b/railties/lib/rails/generators/erb/mailer/templates/layout.text.erb.tt
diff --git a/railties/lib/rails/generators/erb/mailer/templates/view.html.erb b/railties/lib/rails/generators/erb/mailer/templates/view.html.erb.tt
index b5045671b3..b5045671b3 100644
--- a/railties/lib/rails/generators/erb/mailer/templates/view.html.erb
+++ b/railties/lib/rails/generators/erb/mailer/templates/view.html.erb.tt
diff --git a/railties/lib/rails/generators/erb/mailer/templates/view.text.erb b/railties/lib/rails/generators/erb/mailer/templates/view.text.erb.tt
index 342285df19..342285df19 100644
--- a/railties/lib/rails/generators/erb/mailer/templates/view.text.erb
+++ b/railties/lib/rails/generators/erb/mailer/templates/view.text.erb.tt
diff --git a/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb
index c94829a0ae..2fc04e4094 100644
--- a/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb
+++ b/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb
@@ -1,5 +1,7 @@
-require 'rails/generators/erb'
-require 'rails/generators/resource_helpers'
+# frozen_string_literal: true
+
+require "rails/generators/erb"
+require "rails/generators/resource_helpers"
module Erb # :nodoc:
module Generators # :nodoc:
@@ -21,7 +23,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
deleted file mode 100644
index d99b27cb2d..0000000000
--- a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb
+++ /dev/null
@@ -1,34 +0,0 @@
-<%%= form_for(<%= singular_table_name %>) do |f| %>
- <%% 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>
-
- <ul>
- <%% <%= singular_table_name %>.errors.full_messages.each do |message| %>
- <li><%%= message %></li>
- <%% end %>
- </ul>
- </div>
- <%% end %>
-
-<% attributes.each do |attribute| -%>
- <div class="field">
-<% if attribute.password_digest? -%>
- <%%= f.label :password %><br>
- <%%= f.password_field :password %>
- </div>
-
- <div class="field">
- <%%= f.label :password_confirmation %><br>
- <%%= f.password_field :password_confirmation %>
-<% else -%>
- <%%= f.label :<%= attribute.column_name %> %><br>
- <%%= f.<%= attribute.field_type %> :<%= attribute.column_name %> %>
-<% end -%>
- </div>
-
-<% end -%>
- <div class="actions">
- <%%= f.submit %>
- </div>
-<%% end %>
diff --git a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb.tt b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb.tt
new file mode 100644
index 0000000000..518cb1121e
--- /dev/null
+++ b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb.tt
@@ -0,0 +1,34 @@
+<%%= form_with(model: <%= model_resource_name %>, local: true) do |form| %>
+ <%% if <%= singular_table_name %>.errors.any? %>
+ <div id="error_explanation">
+ <h2><%%= pluralize(<%= singular_table_name %>.errors.count, "error") %> prohibited this <%= singular_table_name %> from being saved:</h2>
+
+ <ul>
+ <%% <%= singular_table_name %>.errors.full_messages.each do |message| %>
+ <li><%%= message %></li>
+ <%% end %>
+ </ul>
+ </div>
+ <%% end %>
+
+<% attributes.each do |attribute| -%>
+ <div class="field">
+<% if attribute.password_digest? -%>
+ <%%= form.label :password %>
+ <%%= form.password_field :password %>
+ </div>
+
+ <div class="field">
+ <%%= form.label :password_confirmation %>
+ <%%= form.password_field :password_confirmation %>
+<% else -%>
+ <%%= form.label :<%= attribute.column_name %> %>
+ <%%= form.<%= attribute.field_type %> :<%= attribute.column_name %> %>
+<% end -%>
+ </div>
+
+<% end -%>
+ <div class="actions">
+ <%%= form.submit %>
+ </div>
+<%% end %>
diff --git a/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb.tt
index 81329473d9..81329473d9 100644
--- a/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb
+++ b/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb.tt
diff --git a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb
deleted file mode 100644
index c3b8ef1181..0000000000
--- a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb
+++ /dev/null
@@ -1,31 +0,0 @@
-<p id="notice"><%%= notice %></p>
-
-<h1><%= plural_table_name.titleize %></h1>
-
-<table>
- <thead>
- <tr>
-<% attributes.reject(&:password_digest?).each do |attribute| -%>
- <th><%= attribute.human_name %></th>
-<% end -%>
- <th colspan="3"></th>
- </tr>
- </thead>
-
- <tbody>
- <%% @<%= plural_table_name %>.each do |<%= singular_table_name %>| %>
- <tr>
-<% attributes.reject(&:password_digest?).each do |attribute| -%>
- <td><%%= <%= singular_table_name %>.<%= attribute.name %> %></td>
-<% end -%>
- <td><%%= link_to 'Show', <%= singular_table_name %> %></td>
- <td><%%= link_to 'Edit', edit_<%= singular_table_name %>_path(<%= singular_table_name %>) %></td>
- <td><%%= link_to 'Destroy', <%= singular_table_name %>, method: :delete, data: { confirm: 'Are you sure?' } %></td>
- </tr>
- <%% end %>
- </tbody>
-</table>
-
-<br>
-
-<%%= link_to 'New <%= human_name %>', new_<%= singular_table_name %>_path %>
diff --git a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb.tt b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb.tt
new file mode 100644
index 0000000000..2cf4e5c9d0
--- /dev/null
+++ b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb.tt
@@ -0,0 +1,31 @@
+<p id="notice"><%%= notice %></p>
+
+<h1><%= plural_table_name.titleize %></h1>
+
+<table>
+ <thead>
+ <tr>
+<% attributes.reject(&:password_digest?).each do |attribute| -%>
+ <th><%= attribute.human_name %></th>
+<% end -%>
+ <th colspan="3"></th>
+ </tr>
+ </thead>
+
+ <tbody>
+ <%% @<%= plural_table_name %>.each do |<%= singular_table_name %>| %>
+ <tr>
+<% attributes.reject(&:password_digest?).each do |attribute| -%>
+ <td><%%= <%= singular_table_name %>.<%= attribute.column_name %> %></td>
+<% end -%>
+ <td><%%= link_to 'Show', <%= model_resource_name %> %></td>
+ <td><%%= link_to 'Edit', edit_<%= singular_route_name %>_path(<%= singular_table_name %>) %></td>
+ <td><%%= link_to 'Destroy', <%= model_resource_name %>, method: :delete, data: { confirm: 'Are you sure?' } %></td>
+ </tr>
+ <%% end %>
+ </tbody>
+</table>
+
+<br>
+
+<%%= link_to 'New <%= singular_table_name.titleize %>', new_<%= singular_route_name %>_path %>
diff --git a/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb.tt
index 9b2b2f4875..9b2b2f4875 100644
--- a/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb
+++ b/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb.tt
diff --git a/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb
deleted file mode 100644
index 5e634153be..0000000000
--- a/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb
+++ /dev/null
@@ -1,11 +0,0 @@
-<p id="notice"><%%= notice %></p>
-
-<% attributes.reject(&:password_digest?).each do |attribute| -%>
-<p>
- <strong><%= attribute.human_name %>:</strong>
- <%%= @<%= singular_table_name %>.<%= attribute.name %> %>
-</p>
-
-<% end -%>
-<%%= link_to 'Edit', edit_<%= singular_table_name %>_path(@<%= singular_table_name %>) %> |
-<%%= link_to 'Back', <%= index_helper %>_path %>
diff --git a/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb.tt b/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb.tt
new file mode 100644
index 0000000000..7deba07926
--- /dev/null
+++ b/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb.tt
@@ -0,0 +1,11 @@
+<p id="notice"><%%= notice %></p>
+
+<% attributes.reject(&:password_digest?).each do |attribute| -%>
+<p>
+ <strong><%= attribute.human_name %>:</strong>
+ <%%= @<%= singular_table_name %>.<%= attribute.column_name %> %>
+</p>
+
+<% end -%>
+<%%= link_to 'Edit', edit_<%= singular_table_name %>_path(@<%= singular_table_name %>) %> |
+<%%= link_to 'Back', <%= index_helper %>_path %>
diff --git a/railties/lib/rails/generators/generated_attribute.rb b/railties/lib/rails/generators/generated_attribute.rb
index 8145a26e22..3f20f5a718 100644
--- a/railties/lib/rails/generators/generated_attribute.rb
+++ b/railties/lib/rails/generators/generated_attribute.rb
@@ -1,4 +1,6 @@
-require 'active_support/time'
+# frozen_string_literal: true
+
+require "active_support/time"
module Rails
module Generators
@@ -12,7 +14,7 @@ module Rails
class << self
def parse(column_definition)
- name, type, has_index = column_definition.split(':')
+ name, type, has_index = column_definition.split(":")
# if user provided "name:index" instead of "name:string:index"
# type should be set blank so GeneratedAttribute's constructor
@@ -23,8 +25,9 @@ module Rails
type = type.to_sym if type
if type && reference?(type)
- references_index = UNIQ_INDEX_OPTIONS.include?(has_index) ? { unique: true } : true
- attr_options[:index] = references_index
+ if UNIQ_INDEX_OPTIONS.include?(has_index)
+ attr_options[:index] = { unique: true }
+ end
end
new(name, type, has_index, attr_options)
@@ -55,7 +58,7 @@ module Rails
end
end
- def initialize(name, type=nil, index_type=false, attr_options={})
+ def initialize(name, type = nil, index_type = false, attr_options = {})
@name = name
@type = type || :string
@has_index = INDEX_OPTIONS.include?(index_type)
@@ -65,40 +68,40 @@ module Rails
def field_type
@field_type ||= case type
- when :integer then :number_field
- when :float, :decimal then :text_field
- when :time then :time_select
- when :datetime, :timestamp then :datetime_select
- when :date then :date_select
- when :text then :text_area
- when :boolean then :check_box
- else
- :text_field
+ when :integer then :number_field
+ when :float, :decimal then :text_field
+ when :time then :time_select
+ when :datetime, :timestamp then :datetime_select
+ when :date then :date_select
+ when :text then :text_area
+ when :boolean then :check_box
+ else
+ :text_field
end
end
def default
@default ||= case type
- when :integer then 1
- when :float then 1.5
- when :decimal then "9.99"
- when :datetime, :timestamp, :time then Time.now.to_s(:db)
- when :date then Date.today.to_s(:db)
- when :string then name == "type" ? "" : "MyString"
- when :text then "MyText"
- when :boolean then false
- when :references, :belongs_to then nil
- else
- ""
+ when :integer then 1
+ when :float then 1.5
+ when :decimal then "9.99"
+ when :datetime, :timestamp, :time then Time.now.to_s(:db)
+ when :date then Date.today.to_s(:db)
+ when :string then name == "type" ? "" : "MyString"
+ when :text then "MyText"
+ when :boolean then false
+ when :references, :belongs_to then nil
+ else
+ ""
end
end
def plural_name
- name.sub(/_id$/, '').pluralize
+ name.sub(/_id$/, "").pluralize
end
def singular_name
- name.sub(/_id$/, '').singularize
+ name.sub(/_id$/, "").singularize
end
def human_name
@@ -126,11 +129,11 @@ module Rails
end
def polymorphic?
- self.attr_options[:polymorphic]
+ attr_options[:polymorphic]
end
def required?
- self.attr_options[:required]
+ attr_options[:required]
end
def has_index?
@@ -142,7 +145,7 @@ module Rails
end
def password_digest?
- name == 'password' && type == :digest
+ name == "password" && type == :digest
end
def token?
@@ -150,7 +153,7 @@ module Rails
end
def inject_options
- "".tap { |s| options_for_migration.each { |k,v| s << ", #{k}: #{v.inspect}" } }
+ (+"").tap { |s| options_for_migration.each { |k, v| s << ", #{k}: #{v.inspect}" } }
end
def inject_index_options
diff --git a/railties/lib/rails/generators/js/assets/assets_generator.rb b/railties/lib/rails/generators/js/assets/assets_generator.rb
deleted file mode 100644
index 1e925b2cd2..0000000000
--- a/railties/lib/rails/generators/js/assets/assets_generator.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-require "rails/generators/named_base"
-
-module Js # :nodoc:
- module Generators # :nodoc:
- class AssetsGenerator < Rails::Generators::NamedBase # :nodoc:
- source_root File.expand_path("../templates", __FILE__)
-
- def copy_javascript
- copy_file "javascript.js", File.join('app/assets/javascripts', class_path, "#{file_name}.js")
- end
- end
- end
-end
diff --git a/railties/lib/rails/generators/js/assets/templates/javascript.js b/railties/lib/rails/generators/js/assets/templates/javascript.js
deleted file mode 100644
index dee720facd..0000000000
--- a/railties/lib/rails/generators/js/assets/templates/javascript.js
+++ /dev/null
@@ -1,2 +0,0 @@
-// Place all the behaviors and hooks related to the matching controller here.
-// All this logic will automatically be available in application.js.
diff --git a/railties/lib/rails/generators/migration.rb b/railties/lib/rails/generators/migration.rb
index 51e6d68bf0..5081060895 100644
--- a/railties/lib/rails/generators/migration.rb
+++ b/railties/lib/rails/generators/migration.rb
@@ -1,5 +1,7 @@
-require 'active_support/concern'
-require 'rails/generators/actions/create_migration'
+# frozen_string_literal: true
+
+require "active_support/concern"
+require "rails/generators/actions/create_migration"
module Rails
module Generators
@@ -10,22 +12,22 @@ module Rails
extend ActiveSupport::Concern
attr_reader :migration_number, :migration_file_name, :migration_class_name
- module ClassMethods
- def migration_lookup_at(dirname) #:nodoc:
+ module ClassMethods #:nodoc:
+ def migration_lookup_at(dirname)
Dir.glob("#{dirname}/[0-9]*_*.rb")
end
- def migration_exists?(dirname, file_name) #:nodoc:
+ def migration_exists?(dirname, file_name)
migration_lookup_at(dirname).grep(/\d+_#{file_name}.rb$/).first
end
- def current_migration_number(dirname) #:nodoc:
+ def current_migration_number(dirname)
migration_lookup_at(dirname).collect do |file|
File.basename(file).split("_").first.to_i
end.max.to_i
end
- def next_migration_number(dirname) #:nodoc:
+ def next_migration_number(dirname)
raise NotImplementedError
end
end
@@ -35,11 +37,11 @@ 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)
- @migration_file_name = File.basename(destination, '.rb')
+ @migration_file_name = File.basename(destination, ".rb")
@migration_class_name = @migration_file_name.camelize
end
@@ -52,16 +54,21 @@ module Rails
#
# migration_template "migration.rb", "db/migrate/add_foo_to_bar.rb"
def migration_template(source, destination, config = {})
- source = File.expand_path(find_in_source_paths(source.to_s))
+ source = File.expand_path(find_in_source_paths(source.to_s))
set_migration_assigns!(destination)
- context = instance_eval('binding')
+ context = instance_eval("binding")
dir, base = File.split(destination)
- numbered_destination = File.join(dir, ["%migration_number%", base].join('_'))
+ numbered_destination = File.join(dir, ["%migration_number%", base].join("_"))
create_migration numbered_destination, nil, config do
- ERB.new(::File.binread(source), nil, '-', '@output_buffer').result(context)
+ match = ERB.version.match(/\Aerb\.rb \[(?<version>[^ ]+) /)
+ if match && match[:version] >= "2.2.0" # Ruby 2.6+
+ ERB.new(::File.binread(source), trim_mode: "-", eoutvar: "@output_buffer").result(context)
+ else
+ ERB.new(::File.binread(source), nil, "-", "@output_buffer").result(context)
+ end
end
end
end
diff --git a/railties/lib/rails/generators/model_helpers.rb b/railties/lib/rails/generators/model_helpers.rb
index 42c646543e..3676432d5c 100644
--- a/railties/lib/rails/generators/model_helpers.rb
+++ b/railties/lib/rails/generators/model_helpers.rb
@@ -1,14 +1,20 @@
-require 'rails/generators/active_model'
+# frozen_string_literal: true
+
+require "rails/generators/active_model"
module Rails
module Generators
module ModelHelpers # :nodoc:
PLURAL_MODEL_NAME_WARN_MESSAGE = "[WARNING] The model name '%s' was recognized as a plural, using the singular '%s' instead. " \
"Override with --force-plural or setup custom inflection rules for this noun before running the generator."
+ IRREGULAR_MODEL_NAME_WARN_MESSAGE = <<~WARNING
+ [WARNING] Rails cannot recover singular form from its plural form '%s'.
+ Please setup custom inflection rules for this noun before running the generator in config/initializers/inflections.rb.
+ WARNING
mattr_accessor :skip_warn
def self.included(base) #:nodoc:
- base.class_option :force_plural, type: :boolean, default: false, desc: 'Forces the use of the given model name'
+ base.class_option :force_plural, type: :boolean, default: false, desc: "Forces the use of the given model name"
end
def initialize(args, *_options)
@@ -17,11 +23,14 @@ module Rails
singular = name.singularize
unless ModelHelpers.skip_warn
say PLURAL_MODEL_NAME_WARN_MESSAGE % [name, singular]
- ModelHelpers.skip_warn = true
end
name.replace singular
assign_names!(name)
end
+ if name.singularize != name.pluralize.singularize && ! ModelHelpers.skip_warn
+ say IRREGULAR_MODEL_NAME_WARN_MESSAGE % [name.pluralize]
+ end
+ ModelHelpers.skip_warn = true
end
end
end
diff --git a/railties/lib/rails/generators/named_base.rb b/railties/lib/rails/generators/named_base.rb
index 243694f38e..d6732f8ff1 100644
--- a/railties/lib/rails/generators/named_base.rb
+++ b/railties/lib/rails/generators/named_base.rb
@@ -1,20 +1,19 @@
-require 'active_support/core_ext/module/introspection'
-require 'rails/generators/base'
-require 'rails/generators/generated_attribute'
+# frozen_string_literal: true
+
+require "rails/generators/base"
+require "rails/generators/generated_attribute"
module Rails
module Generators
class NamedBase < Base
argument :name, type: :string
- class_option :skip_namespace, type: :boolean, default: false,
- desc: "Skip namespace (affects only isolated applications)"
def initialize(args, *options) #:nodoc:
@inside_template = nil
# Unfreeze name in case it's given as a frozen string
args[0] = args[0].dup if args[0].is_a?(String) && args[0].frozen?
super
- assign_names!(self.name)
+ assign_names!(name)
parse_attributes! if respond_to?(:attributes)
end
@@ -26,164 +25,179 @@ module Rails
super
end
end
+
+ def js_template(source, destination)
+ template(source + ".js", destination + ".js")
+ end
end
- protected
+ private
attr_reader :file_name
# FIXME: We are avoiding to use alias because a bug on thor that make
# this method public and add it to the task list.
- 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)
- content = capture(&block)
- content = wrap_with_namespace(content) if namespaced?
- concat(content)
- end
-
- def indent(content, multiplier = 2)
- spaces = " " * multiplier
- content.each_line.map {|line| line.blank? ? line : "#{spaces}#{line}" }.join
- end
-
- def wrap_with_namespace(content)
- 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
- Rails::Generators.namespace
- end
-
- def namespaced?
- !options[:skip_namespace] && namespace
- end
-
- def file_path
- @file_path ||= (class_path + [file_name]).join('/')
+ 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("/")
- end
-
- def namespaced_class_path
- @namespaced_class_path ||= [namespaced_path] + @class_path
+ def namespaced_class_path # :doc:
+ @namespaced_class_path ||= namespace_dirs + @class_path
end
- def namespaced_path
- @namespaced_path ||= namespace.name.split("::").first.underscore
+ def class_name # :doc:
+ (class_path + [file_name]).map!(&:camelize).join("::")
end
- def class_name
- (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
- @i18n_scope ||= file_path.tr('/', '.')
+ 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('_')
+ (class_path + [base]).join("_")
end
end
- def uncountable?
+ def uncountable? # :doc:
singular_name == plural_name
end
- def index_helper
- uncountable? ? "#{plural_table_name}_index" : plural_table_name
+ def index_helper # :doc:
+ uncountable? ? "#{plural_route_name}_index" : plural_route_name
end
- def singular_table_name
+ def show_helper # :doc:
+ "#{singular_route_name}_url(@#{singular_table_name})"
+ end
+
+ def edit_helper # :doc:
+ "edit_#{show_helper}"
+ end
+
+ def new_helper # :doc:
+ "new_#{singular_route_name}_url"
+ 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
- @route_url ||= class_path.collect {|dname| "/" + dname }.join + "/" + plural_file_name
+ def route_url # :doc:
+ @route_url ||= class_path.collect { |dname| "/" + dname }.join + "/" + plural_file_name
+ end
+
+ 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
+ Rails.application.class.name.split("::").first.underscore
else
"application"
end
end
- def assign_names!(name) #:nodoc:
- @class_path = name.include?('/') ? name.split('/') : name.split('::')
+ def redirect_resource_name # :doc:
+ model_resource_name(prefix: "@")
+ end
+
+ def model_resource_name(prefix: "") # :doc:
+ resource_name = "#{prefix}#{singular_table_name}"
+ if options[:model_name]
+ "[#{controller_class_path.map { |name| ":" + name }.join(", ")}, #{resource_name}]"
+ else
+ resource_name
+ end
+ end
+
+ def singular_route_name # :doc:
+ if options[:model_name]
+ "#{controller_class_path.join('_')}_#{singular_table_name}"
+ else
+ singular_table_name
+ end
+ end
+
+ def plural_route_name # :doc:
+ if options[:model_name]
+ "#{controller_class_path.join('_')}_#{plural_table_name}"
+ else
+ plural_table_name
+ end
+ end
+
+ def assign_names!(name)
+ @class_path = name.include?("/") ? name.split("/") : name.split("::")
@class_path.map!(&:underscore)
@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?
+ names << "password_confirmation" if a.password_digest?
names << "#{a.name}_type" if a.polymorphic?
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
@@ -197,9 +211,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 ResourceHelpers
controller_class_name
else
class_name
diff --git a/railties/lib/rails/generators/rails/app/USAGE b/railties/lib/rails/generators/rails/app/USAGE
index 691095f33f..28df6ebf44 100644
--- a/railties/lib/rails/generators/rails/app/USAGE
+++ b/railties/lib/rails/generators/rails/app/USAGE
@@ -12,4 +12,3 @@ Example:
rails new ~/Code/Ruby/weblog
This generates a skeletal Rails installation in ~/Code/Ruby/weblog.
- See the README in the newly created application to get going.
diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb
index b813b083f4..33002790d4 100644
--- a/railties/lib/rails/generators/rails/app/app_generator.rb
+++ b/railties/lib/rails/generators/rails/app/app_generator.rb
@@ -1,4 +1,6 @@
-require 'rails/generators/app_base'
+# frozen_string_literal: true
+
+require "rails/generators/app_base"
module Rails
module ActionMethods # :nodoc:
@@ -19,7 +21,6 @@ module Rails
RUBY
end
- # TODO: Remove once this is fully in place
def method_missing(meth, *args, &block)
@generator.send(meth, *args, &block)
end
@@ -32,6 +33,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"
@@ -41,6 +50,10 @@ module Rails
copy_file "README.md", "README.md"
end
+ def ruby_version
+ template "ruby-version", ".ruby-version"
+ end
+
def gemfile
template "Gemfile"
end
@@ -53,15 +66,23 @@ module Rails
template "gitignore", ".gitignore"
end
+ def version_control
+ if !options[:skip_git] && !options[:pretend]
+ run "git init", capture: options[:quiet]
+ end
+ end
+
+ def package_json
+ template "package.json"
+ end
+
def app
- directory 'app'
+ directory "app"
- keep_file 'app/assets/images'
- keep_file 'app/mailers'
- keep_file 'app/models'
+ keep_file "app/assets/images"
- keep_file 'app/controllers/concerns'
- keep_file 'app/models/concerns'
+ keep_file "app/controllers/concerns"
+ keep_file "app/models/concerns"
end
def bin
@@ -71,6 +92,14 @@ module Rails
chmod "bin", 0755 & ~File.umask, verbose: false
end
+ def bin_when_updating
+ bin
+
+ if options[:skip_javascript]
+ remove_file "bin/yarn"
+ end
+ end
+
def config
empty_directory "config"
@@ -78,7 +107,10 @@ module Rails
template "routes.rb"
template "application.rb"
template "environment.rb"
- template "secrets.yml"
+ template "cable.yml" unless options[:skip_action_cable]
+ template "puma.rb" unless options[:skip_puma]
+ template "spring.rb" if spring_install?
+ template "storage.yml" unless skip_active_storage?
directory "environments"
directory "initializers"
@@ -87,25 +119,64 @@ module Rails
end
def config_when_updating
- cookie_serializer_config_exist = File.exist?('config/initializers/cookies_serializer.rb')
- callback_terminator_config_exist = File.exist?('config/initializers/callback_terminator.rb')
- active_record_belongs_to_required_by_default_config_exist = File.exist?('config/initializers/active_record_belongs_to_required_by_default.rb')
+ cookie_serializer_config_exist = File.exist?("config/initializers/cookies_serializer.rb")
+ action_cable_config_exist = File.exist?("config/cable.yml")
+ active_storage_config_exist = File.exist?("config/storage.yml")
+ rack_cors_config_exist = File.exist?("config/initializers/cors.rb")
+ assets_config_exist = File.exist?("config/initializers/assets.rb")
+ csp_config_exist = File.exist?("config/initializers/content_security_policy.rb")
+
+ @config_target_version = Rails.application.config.loaded_config_version || "5.0"
config
- unless callback_terminator_config_exist
- remove_file 'config/initializers/callback_terminator.rb'
+ unless cookie_serializer_config_exist
+ gsub_file "config/initializers/cookies_serializer.rb", /json(?!,)/, "marshal"
+ end
+
+ if !options[:skip_action_cable] && !action_cable_config_exist
+ template "config/cable.yml"
end
- unless cookie_serializer_config_exist
- gsub_file 'config/initializers/cookies_serializer.rb', /json/, 'marshal'
+ if !skip_active_storage? && !active_storage_config_exist
+ template "config/storage.yml"
end
- unless active_record_belongs_to_required_by_default_config_exist
- remove_file 'config/initializers/active_record_belongs_to_required_by_default.rb'
+ if options[:skip_sprockets] && !assets_config_exist
+ remove_file "config/initializers/assets.rb"
+ end
+
+ 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 csp_config_exist
+ remove_file "config/initializers/content_security_policy.rb"
+ end
end
end
+ def master_key
+ return if options[:pretend] || options[:dummy_app]
+
+ require "rails/generators/rails/master_key/master_key_generator"
+ master_key_generator = Rails::Generators::MasterKeyGenerator.new([], quiet: options[:quiet], force: options[:force])
+ master_key_generator.add_master_key_file_silently
+ master_key_generator.ignore_master_key_file_silently
+ end
+
+ def credentials
+ return if options[:pretend] || options[:dummy_app]
+
+ require "rails/generators/rails/credentials/credentials_generator"
+ Rails::Generators::CredentialsGenerator.new([], quiet: options[:quiet]).add_credentials_file_silently
+ end
+
def database_yml
template "config/databases/#{options[:database]}.yml", "config/database.yml"
end
@@ -115,29 +186,40 @@ module Rails
end
def lib
- empty_directory 'lib'
- empty_directory_with_keep_file 'lib/tasks'
- empty_directory_with_keep_file 'lib/assets'
+ empty_directory "lib"
+ empty_directory_with_keep_file "lib/tasks"
+ empty_directory_with_keep_file "lib/assets"
end
def log
- empty_directory_with_keep_file 'log'
+ empty_directory_with_keep_file "log"
end
def public_directory
directory "public", "public", recursive: false
end
+ def storage
+ empty_directory_with_keep_file "storage"
+ empty_directory_with_keep_file "tmp/storage"
+ end
+
def test
- empty_directory_with_keep_file 'test/fixtures'
- empty_directory_with_keep_file 'test/fixtures/files'
- empty_directory_with_keep_file 'test/controllers'
- empty_directory_with_keep_file 'test/mailers'
- empty_directory_with_keep_file 'test/models'
- empty_directory_with_keep_file 'test/helpers'
- empty_directory_with_keep_file 'test/integration'
-
- template 'test/test_helper.rb'
+ empty_directory_with_keep_file "test/fixtures"
+ empty_directory_with_keep_file "test/fixtures/files"
+ empty_directory_with_keep_file "test/controllers"
+ empty_directory_with_keep_file "test/mailers"
+ empty_directory_with_keep_file "test/models"
+ empty_directory_with_keep_file "test/helpers"
+ empty_directory_with_keep_file "test/integration"
+
+ 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
@@ -147,51 +229,53 @@ module Rails
end
def vendor
- vendor_javascripts
- vendor_stylesheets
+ empty_directory_with_keep_file "vendor"
end
- def vendor_javascripts
- unless options[:skip_javascript]
- empty_directory_with_keep_file 'vendor/assets/javascripts'
- end
- end
-
- def vendor_stylesheets
- empty_directory_with_keep_file 'vendor/assets/stylesheets'
+ def config_target_version
+ defined?(@config_target_version) ? @config_target_version : Rails::VERSION::STRING.to_f
end
end
module Generators
# We need to store the RAILS_DEV_PATH in a constant, otherwise the path
# can change in Ruby 1.8.7 when we FileUtils.cd.
- RAILS_DEV_PATH = File.expand_path("../../../../../..", File.dirname(__FILE__))
+ RAILS_DEV_PATH = File.expand_path("../../../../../..", __dir__)
RESERVED_NAMES = %w[application destroy plugin runner test]
class AppGenerator < AppBase # :nodoc:
+ WEBPACKS = %w( react vue angular elm stimulus )
+
add_shared_options_for "application"
- # Add bin/rails options
+ # Add rails command options
class_option :version, type: :boolean, aliases: "-v", group: :rails,
desc: "Show Rails version number and quit"
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"
+
+ class_option :webpack, type: :string, aliases: "--webpacker", default: nil,
+ desc: "Preconfigure Webpack with a particular framework (options: #{WEBPACKS.join(", ")})"
+
+ class_option :skip_webpack_install, type: :boolean, default: false,
+ desc: "Don't run Webpack 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).freeze
+ end
end
public_task :set_default_accessors!
@@ -200,9 +284,12 @@ module Rails
def create_root_files
build(:readme)
build(:rakefile)
+ build(:ruby_version)
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)
+ build(:package_json) unless options[:skip_javascript]
end
def create_app_files
@@ -213,6 +300,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
@@ -222,6 +314,19 @@ module Rails
end
remove_task :update_config_files
+ def create_master_key
+ build(:master_key)
+ end
+
+ def create_credentials
+ build(:credentials)
+ end
+
+ def display_upgrade_guide_info
+ say "\nAfter this, check Rails upgrade guide at https://guides.rubyonrails.org/upgrading_ruby_on_rails.html for more details about upgrading your app."
+ end
+ remove_task :display_upgrade_guide_info
+
def create_boot_file
template "config/boot.rb"
end
@@ -232,6 +337,7 @@ module Rails
end
def create_db_files
+ return if options[:skip_active_record]
build(:db)
end
@@ -247,10 +353,6 @@ module Rails
build(:public_directory)
end
- def create_test_files
- build(:test) unless options[:skip_test]
- end
-
def create_tmp_files
build(:tmp)
end
@@ -259,77 +361,140 @@ module Rails
build(:vendor)
end
+ def create_test_files
+ build(:test) unless options[:skip_test]
+ end
+
+ def create_system_test_files
+ build(:system_test) if depends_on_system_test?
+ end
+
+ def create_storage_files
+ build(:storage) unless skip_active_storage?
+ end
+
def delete_app_assets_if_api_option
if options[:api]
- remove_dir 'app/assets'
- remove_dir 'lib/assets'
- remove_dir 'tmp/cache/assets'
- remove_dir 'vendor/assets'
+ remove_dir "app/assets"
+ remove_dir "lib/assets"
+ remove_dir "tmp/cache/assets"
end
end
def delete_app_helpers_if_api_option
if options[:api]
- remove_dir 'app/helpers'
- remove_dir 'test/helpers'
+ remove_dir "app/helpers"
+ remove_dir "test/helpers"
end
end
def delete_app_views_if_api_option
if options[:api]
- remove_dir 'app/views'
+ if options[:skip_action_mailer]
+ remove_dir "app/views"
+ else
+ remove_file "app/views/layouts/application.html.erb"
+ end
+ end
+ end
+
+ def delete_public_files_if_api_option
+ if options[:api]
+ remove_file "public/404.html"
+ remove_file "public/422.html"
+ remove_file "public/500.html"
+ remove_file "public/apple-touch-icon-precomposed.png"
+ remove_file "public/apple-touch-icon.png"
+ remove_file "public/favicon.ico"
end
end
def delete_js_folder_skipping_javascript
if options[:skip_javascript]
- remove_dir 'app/assets/javascripts'
+ remove_dir "app/javascript"
end
end
def delete_assets_initializer_skipping_sprockets
if options[:skip_sprockets]
- remove_file 'config/initializers/assets.rb'
+ remove_file "config/initializers/assets.rb"
end
end
- def delete_active_record_initializers_skipping_active_record
+ def delete_application_record_skipping_active_record
if options[:skip_active_record]
- remove_file 'config/initializers/active_record_belongs_to_required_by_default.rb'
+ remove_file "app/models/application_record.rb"
+ end
+ end
+
+ def delete_action_mailer_files_skipping_action_mailer
+ if options[:skip_action_mailer]
+ remove_file "app/views/layouts/mailer.html.erb"
+ remove_file "app/views/layouts/mailer.text.erb"
+ remove_dir "app/mailers"
+ remove_dir "test/mailers"
+ end
+ end
+
+ def delete_action_cable_files_skipping_action_cable
+ if options[:skip_action_cable]
+ remove_dir "app/javascript/channels"
+ remove_dir "app/channels"
end
end
def delete_non_api_initializers_if_api_option
if options[:api]
- remove_file 'config/initializers/session_store.rb'
- remove_file 'config/initializers/cookies_serializer.rb'
+ remove_file "config/initializers/cookies_serializer.rb"
+ remove_file "config/initializers/content_security_policy.rb"
+ end
+ end
+
+ def delete_api_initializers
+ unless options[:api]
+ remove_file "config/initializers/cors.rb"
end
end
+ def delete_new_framework_defaults
+ unless options[:update]
+ remove_file "config/initializers/new_framework_defaults_6_0.rb"
+ end
+ end
+
+ def delete_bin_yarn
+ remove_file "bin/yarn" if options[:skip_javascript]
+ end
+
def finish_template
build(:leftovers)
end
public_task :apply_rails_template, :run_bundle
- public_task :generate_spring_binstubs
+ public_task :generate_bundler_binstub, :generate_spring_binstubs
+ public_task :run_webpack
def run_after_bundle_callbacks
@after_bundle_callbacks.each(&:call)
end
- protected
-
def self.banner
- "rails new #{self.arguments.map(&:usage).join(' ')} [options]"
+ "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)
end
def app_name
- @app_name ||= (defined_app_const_base? ? defined_app_name : File.basename(destination_root)).tr('\\', '').tr(". ", "_")
+ @app_name ||= original_app_name.tr("-", "_")
+ end
+
+ def original_app_name
+ @original_app_name ||= (defined_app_const_base? ? defined_app_name : File.basename(destination_root)).tr('\\', "").tr(". ", "_")
end
def defined_app_name
@@ -344,7 +509,7 @@ module Rails
alias :defined_app_const_base? :defined_app_const_base
def app_const_base
- @app_const_base ||= defined_app_const_base || app_name.gsub(/\W/, '_').squeeze('_').camelize
+ @app_const_base ||= defined_app_const_base || app_name.gsub(/\W/, "_").squeeze("_").camelize
end
alias :camelized :app_const_base
@@ -353,21 +518,17 @@ module Rails
end
def valid_const?
- if app_const =~ /^\d/
- raise Error, "Invalid application name #{app_name}. Please give a name which does not start with numbers."
- elsif RESERVED_NAMES.include?(app_name)
- raise Error, "Invalid application name #{app_name}. Please give a " \
+ if /^\d/.match?(app_const)
+ raise Error, "Invalid application name #{original_app_name}. Please give a name which does not start with numbers."
+ elsif RESERVED_NAMES.include?(original_app_name)
+ raise Error, "Invalid application name #{original_app_name}. Please give a " \
"name which does not match one of the reserved rails " \
"words: #{RESERVED_NAMES.join(", ")}"
elsif Object.const_defined?(app_const_base)
- raise Error, "Invalid application name #{app_name}, constant #{app_const_base} is already in use. Please choose another application name."
+ raise Error, "Invalid application name #{original_app_name}, constant #{app_const_base} is already in use. Please choose another application name."
end
end
- def app_secret
- SecureRandom.hex(64)
- end
-
def mysql_socket
@mysql_socket ||= [
"/tmp/mysql.sock", # default
@@ -379,7 +540,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
@@ -407,14 +568,14 @@ module Rails
end
def self.default_rc_file
- File.expand_path('~/.railsrc')
+ File.expand_path("~/.railsrc")
end
private
def handle_version_request!(argument)
- if ['--version', '-v'].include?(argument)
- require 'rails/version'
+ if ["--version", "-v"].include?(argument)
+ require "rails/version"
puts "Rails #{Rails::VERSION::STRING}"
exit(0)
end
@@ -424,20 +585,20 @@ module Rails
if argument == "new"
yield
else
- ['--help'] + argv.drop(1)
+ ["--help"] + argv.drop(1)
end
end
def handle_rails_rc!(argv)
- if argv.find { |arg| arg == '--no-rc' }
- argv.reject { |arg| arg == '--no-rc' }
+ if argv.find { |arg| arg == "--no-rc" }
+ argv.reject { |arg| arg == "--no-rc" }
else
railsrc(argv) { |rc_argv, rc| insert_railsrc_into_argv!(rc_argv, rc) }
end
end
def railsrc(argv)
- if (customrc = argv.index{ |x| x.include?("--rc=") })
+ if (customrc = argv.index { |x| x.include?("--rc=") })
fname = File.expand_path(argv[customrc].gsub(/--rc=/, ""))
yield(argv.take(customrc) + argv.drop(customrc + 1), fname)
else
diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile
deleted file mode 100644
index b083381255..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/Gemfile
+++ /dev/null
@@ -1,55 +0,0 @@
-source 'https://rubygems.org'
-
-<% gemfile_entries.each do |gem| -%>
-<% if gem.comment -%>
-
-# <%= gem.comment %>
-<% end -%>
-<%= gem.commented_out ? '# ' : '' %>gem '<%= gem.name %>'<%= %(, '#{gem.version}') if gem.version -%>
-<% if gem.options.any? -%>
-, <%= gem.options.map { |k,v|
- "#{k}: #{v.inspect}" }.join(', ') %>
-<% end -%>
-<% end -%>
-
-# Use ActiveModel has_secure_password
-# gem 'bcrypt', '~> 3.1.7'
-
-# Use Unicorn as the app server
-# gem 'unicorn'
-
-# Use Capistrano for deployment
-# gem 'capistrano-rails', group: :development
-
-<%- if options.api? -%>
-# Use ActiveModelSerializers to serialize JSON responses
-gem 'active_model_serializers', '~> 0.10.0.rc2'
-
-# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible
-# gem 'rack-cors'
-
-<%- 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'
-end
-
-group :development do
- # Access an IRB console on exception pages or by using <%%= console %> in views
- <%- if options.dev? || options.edge? -%>
- gem 'web-console', github: 'rails/web-console'
- <%- else -%>
- gem 'web-console', '~> 2.0'
- <%- end -%>
-<%- if spring_install? %>
- # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
- gem 'spring'
-<% end -%>
-end
-<% end -%>
-<% if RUBY_PLATFORM.match(/bccwin|cygwin|emx|mingw|mswin|wince|java/) -%>
-
-# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
-gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
-<% end -%>
diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile.tt b/railties/lib/rails/generators/rails/app/templates/Gemfile.tt
new file mode 100644
index 0000000000..fb264935bd
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/Gemfile.tt
@@ -0,0 +1,81 @@
+source 'https://rubygems.org'
+git_source(:github) { |repo| "https://github.com/#{repo}.git" }
+
+ruby <%= "'#{RUBY_VERSION}'" -%>
+
+<% unless gemfile_entries.first.comment -%>
+
+<% end -%>
+<% gemfile_entries.each do |gem| -%>
+<% if gem.comment -%>
+
+# <%= gem.comment %>
+<% end -%>
+<%= gem.commented_out ? '# ' : '' %>gem '<%= gem.name %>'<%= %(, '#{gem.version}') if gem.version -%>
+<% if gem.options.any? -%>
+, <%= gem.options.map { |k,v|
+ "#{k}: #{v.inspect}" }.join(', ') %>
+<% end -%>
+<% end -%>
+
+# Use ActiveModel has_secure_password
+# gem 'bcrypt', '~> 3.1.7'
+<% unless skip_active_storage? -%>
+
+# Use ActiveStorage variant
+# gem 'image_processing', '~> 1.2'
+<% end -%>
+
+# Use Capistrano for deployment
+# gem 'capistrano-rails', group: :development
+
+<% if depend_on_bootsnap? -%>
+# Reduces boot times through caching; required in config/boot.rb
+gem 'bootsnap', '>= 1.1.0', require: false
+
+<%- end -%>
+<%- if options.api? -%>
+# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible
+# gem 'rack-cors'
+
+<%- 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', platforms: [:mri, :mingw, :x64_mingw]
+end
+
+<% end -%>
+group :development do
+<%- unless options.api? -%>
+ # Access an interactive console on exception pages or by calling 'console' anywhere in the code.
+ <%- if options.dev? || options.edge? -%>
+ gem 'web-console', github: 'rails/web-console'
+ <%- else -%>
+ gem 'web-console', '>= 3.3.0'
+ <%- end -%>
+<%- end -%>
+<% if depend_on_listen? -%>
+ gem 'listen', '>= 3.0.5', '< 3.2'
+<% end -%>
+<% if spring_install? -%>
+ # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
+ gem 'spring'
+<% if depend_on_listen? -%>
+ gem 'spring-watcher-listen', '~> 2.0.0'
+<% end -%>
+<% end -%>
+end
+
+<%- if depends_on_system_test? -%>
+group :test do
+ # Adds support for Capybara system testing and selenium driver
+ gem 'capybara', '>= 2.15'
+ gem 'selenium-webdriver'
+ # Easy installation and use of chromedriver to run system tests with Chrome
+ gem 'chromedriver-helper'
+end
+<%- end -%>
+
+# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
+gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
diff --git a/railties/lib/rails/generators/rails/app/templates/README.md b/railties/lib/rails/generators/rails/app/templates/README.md
deleted file mode 100644
index 55e144da18..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/README.md
+++ /dev/null
@@ -1,24 +0,0 @@
-## README
-
-This README would normally document whatever steps are necessary to get the
-application up and running.
-
-Things you may want to cover:
-
-* Ruby version
-
-* System dependencies
-
-* Configuration
-
-* Database creation
-
-* Database initialization
-
-* How to run the test suite
-
-* Services (job queues, cache servers, search engines, etc.)
-
-* Deployment instructions
-
-* ...
diff --git a/railties/lib/rails/generators/rails/app/templates/README.md.tt b/railties/lib/rails/generators/rails/app/templates/README.md.tt
new file mode 100644
index 0000000000..7db80e4ca1
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/README.md.tt
@@ -0,0 +1,24 @@
+# README
+
+This README would normally document whatever steps are necessary to get the
+application up and running.
+
+Things you may want to cover:
+
+* Ruby version
+
+* System dependencies
+
+* Configuration
+
+* Database creation
+
+* Database initialization
+
+* How to run the test suite
+
+* Services (job queues, cache servers, search engines, etc.)
+
+* Deployment instructions
+
+* ...
diff --git a/railties/lib/rails/generators/rails/app/templates/Rakefile b/railties/lib/rails/generators/rails/app/templates/Rakefile
deleted file mode 100644
index ba6b733dd2..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/Rakefile
+++ /dev/null
@@ -1,6 +0,0 @@
-# Add your own tasks in files placed in lib/tasks ending in .rake,
-# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
-
-require File.expand_path('../config/application', __FILE__)
-
-Rails.application.load_tasks
diff --git a/railties/lib/rails/generators/rails/app/templates/Rakefile.tt b/railties/lib/rails/generators/rails/app/templates/Rakefile.tt
new file mode 100644
index 0000000000..e85f913914
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/Rakefile.tt
@@ -0,0 +1,6 @@
+# Add your own tasks in files placed in lib/tasks ending in .rake,
+# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
+
+require_relative 'config/application'
+
+Rails.application.load_tasks
diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/config/manifest.js.tt b/railties/lib/rails/generators/rails/app/templates/app/assets/config/manifest.js.tt
new file mode 100644
index 0000000000..591819335f
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/app/assets/config/manifest.js.tt
@@ -0,0 +1,2 @@
+//= link_tree ../images
+//= link_directory ../stylesheets .css
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
deleted file mode 100644
index cb86978d4c..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt
+++ /dev/null
@@ -1,20 +0,0 @@
-// 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.
-//
-// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
-// compiled file.
-//
-// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
-// about supported directives.
-//
-<% unless options[:skip_javascript] -%>
-//= require <%= options[:javascript] %>
-//= require <%= options[:javascript] %>_ujs
-<% if gemfile_entries.any? { |m| m.name == "turbolinks" } -%>
-//= require turbolinks
-<% end -%>
-<% end -%>
-//= require_tree .
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
deleted file mode 100644
index 0cdd2788d0..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css
+++ /dev/null
@@ -1,14 +0,0 @@
-/*
- * 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.
- *
- * 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
- * files in this directory. It is generally better to create a new file per style scope.
- *
- *= require_tree .
- *= require_self
- */
diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css.tt b/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css.tt
new file mode 100644
index 0000000000..d05ea0f511
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css.tt
@@ -0,0 +1,15 @@
+/*
+ * 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, 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
+ * files in this directory. Styles in this file should be added after the last require_* statement.
+ * It is generally better to create a new file per style scope.
+ *
+ *= require_tree .
+ *= require_self
+ */
diff --git a/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/channel.rb.tt b/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/channel.rb.tt
new file mode 100644
index 0000000000..d672697283
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/channel.rb.tt
@@ -0,0 +1,4 @@
+module ApplicationCable
+ class Channel < ActionCable::Channel::Base
+ end
+end
diff --git a/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/connection.rb.tt b/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/connection.rb.tt
new file mode 100644
index 0000000000..0ff5442f47
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/connection.rb.tt
@@ -0,0 +1,4 @@
+module ApplicationCable
+ class Connection < ActionCable::Connection::Base
+ end
+end
diff --git a/railties/lib/rails/generators/rails/app/templates/app/controllers/application_controller.rb.tt b/railties/lib/rails/generators/rails/app/templates/app/controllers/application_controller.rb.tt
index f726fd6305..938eff8ed0 100644
--- a/railties/lib/rails/generators/rails/app/templates/app/controllers/application_controller.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/app/controllers/application_controller.rb.tt
@@ -1,7 +1,2 @@
-class ApplicationController < ActionController::<%= options[:api] ? "API" : "Base" %>
-<%- unless options[:api] -%>
- # Prevent CSRF attacks by raising an exception.
- # For APIs, you may want to use :null_session instead.
- protect_from_forgery with: :exception
-<%- end -%>
+class ApplicationController < ActionController::<%= options.api? ? "API" : "Base" %>
end
diff --git a/railties/lib/rails/generators/rails/app/templates/app/helpers/application_helper.rb b/railties/lib/rails/generators/rails/app/templates/app/helpers/application_helper.rb.tt
index de6be7945c..de6be7945c 100644
--- a/railties/lib/rails/generators/rails/app/templates/app/helpers/application_helper.rb
+++ b/railties/lib/rails/generators/rails/app/templates/app/helpers/application_helper.rb.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/app/javascript/channels/consumer.js b/railties/lib/rails/generators/rails/app/templates/app/javascript/channels/consumer.js
new file mode 100644
index 0000000000..76ca3d0f2f
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/app/javascript/channels/consumer.js
@@ -0,0 +1,6 @@
+// Action Cable provides the framework to deal with WebSockets in Rails.
+// You can generate new channels where WebSocket features live using the `rails generate channel` command.
+
+import ActionCable from "actioncable"
+
+export default ActionCable.createConsumer()
diff --git a/railties/lib/rails/generators/rails/app/templates/app/javascript/channels/index.js b/railties/lib/rails/generators/rails/app/templates/app/javascript/channels/index.js
new file mode 100644
index 0000000000..0cfcf74919
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/app/javascript/channels/index.js
@@ -0,0 +1,5 @@
+// Load all the channels within this directory and all subdirectories.
+// Channel files must be named *_channel.js.
+
+const channels = require.context('.', true, /_channel\.js$/)
+channels.keys().forEach(channels)
diff --git a/railties/lib/rails/generators/rails/app/templates/app/javascript/packs/application.js.tt b/railties/lib/rails/generators/rails/app/templates/app/javascript/packs/application.js.tt
new file mode 100644
index 0000000000..4d7a145cd6
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/app/javascript/packs/application.js.tt
@@ -0,0 +1,21 @@
+// This file is automatically compiled by Webpack, along with any other files
+// present in this directory. You're encouraged to place your actual application logic in
+// a relevant structure within app/javascript and only use these pack files to reference
+// that code so it'll be compiled.
+
+import Rails from "rails-ujs"
+Rails.start()
+<%- unless options[:skip_turbolinks] -%>
+
+import Turbolinks from "turbolinks"
+Turbolinks.start()
+<%- end -%>
+<%- unless skip_active_storage? -%>
+
+import * as ActiveStorage from "activestorage"
+ActiveStorage.start()
+<%- end -%>
+<%- unless options[:skip_action_cable] -%>
+
+import "channels"
+<%- end -%>
diff --git a/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb b/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb
deleted file mode 100644
index a009ace51c..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-class ApplicationJob < ActiveJob::Base
-end
diff --git a/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb.tt b/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb.tt
new file mode 100644
index 0000000000..d394c3d106
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb.tt
@@ -0,0 +1,7 @@
+class ApplicationJob < ActiveJob::Base
+ # Automatically retry jobs that encountered a deadlock
+ # retry_on ActiveRecord::Deadlocked
+
+ # Most jobs are safe to ignore if the underlying records are no longer available
+ # discard_on ActiveJob::DeserializationError
+end
diff --git a/railties/lib/rails/generators/rails/app/templates/app/mailers/application_mailer.rb.tt b/railties/lib/rails/generators/rails/app/templates/app/mailers/application_mailer.rb.tt
new file mode 100644
index 0000000000..286b2239d1
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/app/mailers/application_mailer.rb.tt
@@ -0,0 +1,4 @@
+class ApplicationMailer < ActionMailer::Base
+ default from: 'from@example.com'
+ layout 'mailer'
+end
diff --git a/railties/lib/rails/generators/rails/app/templates/app/models/application_record.rb.tt b/railties/lib/rails/generators/rails/app/templates/app/models/application_record.rb.tt
new file mode 100644
index 0000000000..10a4cba84d
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/app/models/application_record.rb.tt
@@ -0,0 +1,3 @@
+class ApplicationRecord < ActiveRecord::Base
+ self.abstract_class = true
+end
diff --git a/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt
index 75ea52828e..9a7267c783 100644
--- a/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt
@@ -1,23 +1,24 @@
<!DOCTYPE html>
<html>
-<head>
- <title><%= camelized %></title>
- <%- if options[:skip_javascript] -%>
- <%%= stylesheet_link_tag 'application', media: 'all' %>
- <%- else -%>
- <%- if gemfile_entries.any? { |m| m.name == 'turbolinks' } -%>
- <%%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
- <%%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
+ <head>
+ <title><%= camelized %></title>
+ <%%= csrf_meta_tags %>
+ <%%= csp_meta_tag %>
+
+ <%- if options[:skip_javascript] -%>
+ <%%= stylesheet_link_tag 'application', media: 'all' %>
<%- else -%>
- <%%= stylesheet_link_tag 'application', media: 'all' %>
- <%%= javascript_include_tag 'application' %>
+ <%- unless options[:skip_turbolinks] -%>
+ <%%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
+ <%%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
+ <%- else -%>
+ <%%= stylesheet_link_tag 'application', media: 'all' %>
+ <%%= javascript_pack_tag 'application' %>
+ <%- end -%>
<%- end -%>
- <%- end -%>
- <%%= csrf_meta_tags %>
-</head>
-<body>
-
-<%%= yield %>
+ </head>
-</body>
+ <body>
+ <%%= yield %>
+ </body>
</html>
diff --git a/railties/lib/rails/generators/rails/app/templates/app/views/layouts/mailer.html.erb.tt b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/mailer.html.erb.tt
new file mode 100644
index 0000000000..55f3675d49
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/mailer.html.erb.tt
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <style>
+ /* Email styles need to be inline */
+ </style>
+ </head>
+
+ <body>
+ <%%= yield %>
+ </body>
+</html>
diff --git a/railties/lib/rails/generators/rails/app/templates/app/views/layouts/mailer.text.erb.tt b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/mailer.text.erb.tt
new file mode 100644
index 0000000000..6363733e6e
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/mailer.text.erb.tt
@@ -0,0 +1 @@
+<%%= yield %>
diff --git a/railties/lib/rails/generators/rails/app/templates/bin/bundle b/railties/lib/rails/generators/rails/app/templates/bin/bundle
deleted file mode 100644
index 1123dcf501..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/bin/bundle
+++ /dev/null
@@ -1,2 +0,0 @@
-ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
-load Gem.bin_path('bundler', 'bundle')
diff --git a/railties/lib/rails/generators/rails/app/templates/bin/rails b/railties/lib/rails/generators/rails/app/templates/bin/rails
deleted file mode 100644
index 80ec8080ab..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/bin/rails
+++ /dev/null
@@ -1,3 +0,0 @@
-APP_PATH = File.expand_path('../../config/application', __FILE__)
-require_relative '../config/boot'
-require 'rails/commands'
diff --git a/railties/lib/rails/generators/rails/app/templates/bin/rails.tt b/railties/lib/rails/generators/rails/app/templates/bin/rails.tt
new file mode 100644
index 0000000000..513a2e0183
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/bin/rails.tt
@@ -0,0 +1,3 @@
+APP_PATH = File.expand_path('../config/application', __dir__)
+require_relative '../config/boot'
+require 'rails/commands'
diff --git a/railties/lib/rails/generators/rails/app/templates/bin/rake b/railties/lib/rails/generators/rails/app/templates/bin/rake.tt
index d14fc8395b..d14fc8395b 100644
--- a/railties/lib/rails/generators/rails/app/templates/bin/rake
+++ b/railties/lib/rails/generators/rails/app/templates/bin/rake.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/bin/setup b/railties/lib/rails/generators/rails/app/templates/bin/setup
deleted file mode 100644
index 0d41f2fe4c..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/bin/setup
+++ /dev/null
@@ -1,29 +0,0 @@
-require 'pathname'
-require 'fileutils'
-include FileUtils
-
-# path to your application root.
-APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
-
-chdir APP_ROOT do
- # This script is a starting point to setup your application.
- # Add necessary setup steps to this file.
-
- puts '== Installing dependencies =='
- system 'gem install bundler --conservative'
- system('bundle check') or system('bundle install')
-
- # puts "\n== Copying sample files =="
- # unless File.exist?('config/database.yml')
- # cp 'config/database.yml.sample', 'config/database.yml'
- # end
-
- puts "\n== Preparing database =="
- system 'ruby bin/rake db:setup'
-
- puts "\n== Removing old logs and tempfiles =="
- system 'ruby bin/rake log:clear tmp:clear'
-
- puts "\n== Restarting application server =="
- system 'ruby bin/rake restart'
-end
diff --git a/railties/lib/rails/generators/rails/app/templates/bin/setup.tt b/railties/lib/rails/generators/rails/app/templates/bin/setup.tt
new file mode 100644
index 0000000000..3f73bae3da
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/bin/setup.tt
@@ -0,0 +1,38 @@
+require 'fileutils'
+
+# path to your application root.
+APP_ROOT = File.expand_path('..', __dir__)
+
+def system!(*args)
+ system(*args) || abort("\n== Command #{args} failed ==")
+end
+
+FileUtils.chdir APP_ROOT do
+ # This script is a starting point to setup your application.
+ # Add necessary setup steps to this file.
+
+ puts '== Installing dependencies =='
+ system! 'gem install bundler --conservative'
+ system('bundle check') || system!('bundle install')
+<% unless options.skip_javascript? -%>
+
+ # Install JavaScript dependencies
+ # system('bin/yarn')
+<% end -%>
+<% unless options.skip_active_record? -%>
+
+ # puts "\n== Copying sample files =="
+ # unless File.exist?('config/database.yml')
+ # FileUtils.cp 'config/database.yml.sample', 'config/database.yml'
+ # end
+
+ puts "\n== Preparing database =="
+ system! 'bin/rails db:setup'
+<% end -%>
+
+ puts "\n== Removing old logs and tempfiles =="
+ system! 'bin/rails log:clear tmp:clear'
+
+ puts "\n== Restarting application server =="
+ system! 'bin/rails restart'
+end
diff --git a/railties/lib/rails/generators/rails/app/templates/bin/update.tt b/railties/lib/rails/generators/rails/app/templates/bin/update.tt
new file mode 100644
index 0000000000..03b77d0d46
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/bin/update.tt
@@ -0,0 +1,33 @@
+require 'fileutils'
+
+# path to your application root.
+APP_ROOT = File.expand_path('..', __dir__)
+
+def system!(*args)
+ system(*args) || abort("\n== Command #{args} failed ==")
+end
+
+FileUtils.chdir APP_ROOT do
+ # This script is a way to update your development environment automatically.
+ # Add necessary update steps to this file.
+
+ puts '== Installing dependencies =='
+ system! 'gem install bundler --conservative'
+ system('bundle check') || system!('bundle install')
+<% unless options.skip_javascript? -%>
+
+ # Install JavaScript dependencies
+ # system('bin/yarn')
+<% end -%>
+<% unless options.skip_active_record? -%>
+
+ puts "\n== Updating database =="
+ system! 'rails db:migrate'
+<% end -%>
+
+ puts "\n== Removing old logs and tempfiles =="
+ system! 'rails log:clear tmp:clear'
+
+ puts "\n== Restarting application server =="
+ system! 'rails restart'
+end
diff --git a/railties/lib/rails/generators/rails/app/templates/bin/yarn.tt b/railties/lib/rails/generators/rails/app/templates/bin/yarn.tt
new file mode 100644
index 0000000000..90ddcc520e
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/bin/yarn.tt
@@ -0,0 +1,10 @@
+APP_ROOT = File.expand_path('..', __dir__)
+Dir.chdir(APP_ROOT) do
+ begin
+ exec "yarnpkg", *ARGV
+ rescue Errno::ENOENT
+ $stderr.puts "Yarn executable was not detected in the system."
+ $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install"
+ exit 1
+ end
+end
diff --git a/railties/lib/rails/generators/rails/app/templates/config.ru b/railties/lib/rails/generators/rails/app/templates/config.ru
deleted file mode 100644
index bd83b25412..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config.ru
+++ /dev/null
@@ -1,4 +0,0 @@
-# This file is used by Rack-based servers to start the application.
-
-require ::File.expand_path('../config/environment', __FILE__)
-run Rails.application
diff --git a/railties/lib/rails/generators/rails/app/templates/config.ru.tt b/railties/lib/rails/generators/rails/app/templates/config.ru.tt
new file mode 100644
index 0000000000..f7ba0b527b
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config.ru.tt
@@ -0,0 +1,5 @@
+# This file is used by Rack-based servers to start the application.
+
+require_relative 'config/environment'
+
+run Rails.application
diff --git a/railties/lib/rails/generators/rails/app/templates/config/application.rb b/railties/lib/rails/generators/rails/app/templates/config/application.rb
deleted file mode 100644
index 6b7d7abd0b..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/application.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-require File.expand_path('../boot', __FILE__)
-
-<% if include_all_railties? -%>
-require 'rails/all'
-<% else -%>
-require "rails"
-# Pick the frameworks you want:
-require "active_model/railtie"
-require "active_job/railtie"
-<%= comment_if :skip_active_record %>require "active_record/railtie"
-require "action_controller/railtie"
-<%= comment_if :skip_action_mailer %>require "action_mailer/railtie"
-require "action_view/railtie"
-<%= comment_if :skip_sprockets %>require "sprockets/railtie"
-<%= comment_if :skip_test %>require "rails/test_unit/railtie"
-<% end -%>
-
-# Require the gems listed in Gemfile, including any gems
-# you've limited to :test, :development, or :production.
-Bundler.require(*Rails.groups)
-
-module <%= app_const_base %>
- class Application < Rails::Application
- # 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.
-
- # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
- # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
- # config.time_zone = 'Central Time (US & Canada)'
-
- # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
- # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
- # config.i18n.default_locale = :de
-<%- if options[:api] -%>
-
- # Only loads a smaller set of middleware suitable for API only apps.
- # Middleware like session, flash, cookies can be added back manually.
- # Skip views, helpers and assets when generating a new resource.
- config.api_only = true
-<%- end -%>
- end
-end
diff --git a/railties/lib/rails/generators/rails/app/templates/config/application.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/application.rb.tt
new file mode 100644
index 0000000000..9a427113c7
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/application.rb.tt
@@ -0,0 +1,45 @@
+require_relative 'boot'
+
+<% if include_all_railties? -%>
+require 'rails/all'
+<% else -%>
+require "rails"
+# Pick the frameworks you want:
+require "active_model/railtie"
+require "active_job/railtie"
+<%= comment_if :skip_active_record %>require "active_record/railtie"
+<%= comment_if :skip_active_storage %>require "active_storage/engine"
+require "action_controller/railtie"
+<%= comment_if :skip_action_mailer %>require "action_mailer/railtie"
+require "action_view/railtie"
+<%= comment_if :skip_action_cable %>require "action_cable/engine"
+<%= comment_if :skip_sprockets %>require "sprockets/railtie"
+<%= comment_if :skip_test %>require "rails/test_unit/railtie"
+<% end -%>
+
+# Require the gems listed in Gemfile, including any gems
+# you've limited to :test, :development, or :production.
+Bundler.require(*Rails.groups)
+
+module <%= app_const_base %>
+ class Application < Rails::Application
+ # Initialize configuration defaults for originally generated Rails version.
+ config.load_defaults <%= build(:config_target_version) %>
+
+ # Settings in config/environments/* take precedence over those specified here.
+ # Application configuration can go into files in config/initializers
+ # -- all .rb files in that directory are automatically loaded after loading
+ # the framework and any gems in your application.
+<%- if options.api? -%>
+
+ # Only loads a smaller set of middleware suitable for API only apps.
+ # Middleware like session, flash, cookies can be added back manually.
+ # Skip views, helpers and assets when generating a new resource.
+ config.api_only = true
+<%- elsif !depends_on_system_test? -%>
+
+ # Don't generate system test files.
+ config.generators.system_tests = nil
+<%- end -%>
+ end
+end
diff --git a/railties/lib/rails/generators/rails/app/templates/config/boot.rb b/railties/lib/rails/generators/rails/app/templates/config/boot.rb
deleted file mode 100644
index 6b750f00b1..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/boot.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
-
-require 'bundler/setup' # Set up gems listed in the Gemfile.
diff --git a/railties/lib/rails/generators/rails/app/templates/config/boot.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/boot.rb.tt
new file mode 100644
index 0000000000..42d46b8175
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/boot.rb.tt
@@ -0,0 +1,6 @@
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
+
+require 'bundler/setup' # Set up gems listed in the Gemfile.
+<% if depend_on_bootsnap? -%>
+require 'bootsnap/setup' # Speed up boot time by caching expensive operations.
+<%- end -%>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/cable.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/cable.yml.tt
new file mode 100644
index 0000000000..f69dc91b92
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/cable.yml.tt
@@ -0,0 +1,10 @@
+development:
+ adapter: async
+
+test:
+ adapter: test
+
+production:
+ adapter: redis
+ url: <%%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
+ channel_prefix: <%= app_name %>_production
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml
deleted file mode 100644
index 34fc0e3465..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml
+++ /dev/null
@@ -1,49 +0,0 @@
-# FrontBase versions 4.x
-#
-# Get the bindings:
-# gem install ruby-frontbase
-#
-# Configure Using Gemfile
-# gem 'ruby-frontbase'
-#
-default: &default
- adapter: frontbase
- host: localhost
- username: <%= app_name %>
- password: ''
-
-development:
- <<: *default
- database: <%= app_name %>_development
-
-# Warning: The database defined as "test" will be erased and
-# re-generated from your development database when you run "rake".
-# Do not set this db to the same as development or production.
-test:
- <<: *default
- database: <%= app_name %>_test
-
-# As with config/secrets.yml, you never want to store sensitive information,
-# like your database password, in your source code. If your source code is
-# ever seen by anyone, they now have access to your database.
-#
-# Instead, provide the password as a unix environment variable when you boot
-# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
-# for a full rundown on how to provide these environment variables in a
-# production deployment.
-#
-# On Heroku and other platform providers, you may have a full connection URL
-# available as an environment variable. For example:
-#
-# DATABASE_URL="frontbase://myuser:mypass@localhost/somedatabase"
-#
-# You can use this database configuration with:
-#
-# production:
-# url: <%%= ENV['DATABASE_URL'] %>
-#
-production:
- <<: *default
- database: <%= app_name %>_production
- username: <%= app_name %>
- password: <%%= ENV['<%= app_name.upcase %>_DATABASE_PASSWORD'] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml.tt
new file mode 100644
index 0000000000..33f422c622
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml.tt
@@ -0,0 +1,50 @@
+# FrontBase versions 4.x
+#
+# Get the bindings:
+# gem install ruby-frontbase
+#
+# Configure Using Gemfile
+# gem 'ruby-frontbase'
+#
+default: &default
+ adapter: frontbase
+ pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
+ host: localhost
+ username: <%= app_name %>
+ password: ''
+
+development:
+ <<: *default
+ database: <%= app_name %>_development
+
+# Warning: The database defined as "test" will be erased and
+# re-generated from your development database when you run "rake".
+# Do not set this db to the same as development or production.
+test:
+ <<: *default
+ database: <%= app_name %>_test
+
+# As with config/secrets.yml, you never want to store sensitive information,
+# like your database password, in your source code. If your source code is
+# ever seen by anyone, they now have access to your database.
+#
+# Instead, provide the password as a unix environment variable when you boot
+# the app. Read https://guides.rubyonrails.org/configuring.html#configuring-a-database
+# for a full rundown on how to provide these environment variables in a
+# production deployment.
+#
+# On Heroku and other platform providers, you may have a full connection URL
+# available as an environment variable. For example:
+#
+# DATABASE_URL="frontbase://myuser:mypass@localhost/somedatabase"
+#
+# You can use this database configuration with:
+#
+# production:
+# url: <%%= ENV['DATABASE_URL'] %>
+#
+production:
+ <<: *default
+ database: <%= app_name %>_production
+ username: <%= app_name %>
+ password: <%%= ENV['<%= app_name.upcase %>_DATABASE_PASSWORD'] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml
deleted file mode 100644
index 187ff01bac..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml
+++ /dev/null
@@ -1,85 +0,0 @@
-# IBM Dataservers
-#
-# Home Page
-# https://github.com/dparnell/ibm_db
-#
-# To install the ibm_db gem:
-#
-# On Linux:
-# . /home/db2inst1/sqllib/db2profile
-# export IBM_DB_INCLUDE=/opt/ibm/db2/V9.7/include
-# export IBM_DB_LIB=/opt/ibm/db2/V9.7/lib32
-# gem install ibm_db
-#
-# On Mac OS X 10.5:
-# . /home/db2inst1/sqllib/db2profile
-# export IBM_DB_INCLUDE=/opt/ibm/db2/V9.7/include
-# export IBM_DB_LIB=/opt/ibm/db2/V9.7/lib32
-# export ARCHFLAGS="-arch i386"
-# gem install ibm_db
-#
-# On Mac OS X 10.6:
-# . /home/db2inst1/sqllib/db2profile
-# export IBM_DB_INCLUDE=/opt/ibm/db2/V9.7/include
-# export IBM_DB_LIB=/opt/ibm/db2/V9.7/lib64
-# export ARCHFLAGS="-arch x86_64"
-# gem install ibm_db
-#
-# On Windows:
-# Issue the command: gem install ibm_db
-#
-# Configure Using Gemfile
-# gem 'ibm_db'
-#
-#
-default: &default
- adapter: ibm_db
- username: db2inst1
- password:
- #schema: db2inst1
- #host: localhost
- #port: 50000
- #account: my_account
- #app_user: my_app_user
- #application: my_application
- #workstation: my_workstation
- #security: SSL
- #timeout: 10
- #authentication: SERVER
- #parameterized: false
-
-development:
- <<: *default
- database: <%= app_name[0,4] %>_dev
-
-# Warning: The database defined as "test" will be erased and
-# re-generated from your development database when you run "rake".
-# Do not set this db to the same as development or production.
-test:
- <<: *default
- database: <%= app_name[0,4] %>_tst
-
-# As with config/secrets.yml, you never want to store sensitive information,
-# like your database password, in your source code. If your source code is
-# ever seen by anyone, they now have access to your database.
-#
-# Instead, provide the password as a unix environment variable when you boot
-# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
-# for a full rundown on how to provide these environment variables in a
-# production deployment.
-#
-# On Heroku and other platform providers, you may have a full connection URL
-# available as an environment variable. For example:
-#
-# DATABASE_URL="ibm-db://myuser:mypass@localhost/somedatabase"
-#
-# You can use this database configuration with:
-#
-# production:
-# url: <%%= ENV['DATABASE_URL'] %>
-#
-production:
- <<: *default
- database: <%= app_name %>_production
- username: <%= app_name %>
- password: <%%= ENV['<%= app_name.upcase %>_DATABASE_PASSWORD'] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml.tt
new file mode 100644
index 0000000000..681c765e93
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml.tt
@@ -0,0 +1,86 @@
+# IBM Dataservers
+#
+# Home Page
+# https://github.com/dparnell/ibm_db
+#
+# To install the ibm_db gem:
+#
+# On Linux:
+# . /home/db2inst1/sqllib/db2profile
+# export IBM_DB_INCLUDE=/opt/ibm/db2/V9.7/include
+# export IBM_DB_LIB=/opt/ibm/db2/V9.7/lib32
+# gem install ibm_db
+#
+# On Mac OS X 10.5:
+# . /home/db2inst1/sqllib/db2profile
+# export IBM_DB_INCLUDE=/opt/ibm/db2/V9.7/include
+# export IBM_DB_LIB=/opt/ibm/db2/V9.7/lib32
+# export ARCHFLAGS="-arch i386"
+# gem install ibm_db
+#
+# On Mac OS X 10.6:
+# . /home/db2inst1/sqllib/db2profile
+# export IBM_DB_INCLUDE=/opt/ibm/db2/V9.7/include
+# export IBM_DB_LIB=/opt/ibm/db2/V9.7/lib64
+# export ARCHFLAGS="-arch x86_64"
+# gem install ibm_db
+#
+# On Windows:
+# Issue the command: gem install ibm_db
+#
+# Configure Using Gemfile
+# gem 'ibm_db'
+#
+#
+default: &default
+ adapter: ibm_db
+ pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
+ username: db2inst1
+ password:
+ #schema: db2inst1
+ #host: localhost
+ #port: 50000
+ #account: my_account
+ #app_user: my_app_user
+ #application: my_application
+ #workstation: my_workstation
+ #security: SSL
+ #timeout: 10
+ #authentication: SERVER
+ #parameterized: false
+
+development:
+ <<: *default
+ database: <%= app_name[0,4] %>_dev
+
+# Warning: The database defined as "test" will be erased and
+# re-generated from your development database when you run "rake".
+# Do not set this db to the same as development or production.
+test:
+ <<: *default
+ database: <%= app_name[0,4] %>_tst
+
+# As with config/secrets.yml, you never want to store sensitive information,
+# like your database password, in your source code. If your source code is
+# ever seen by anyone, they now have access to your database.
+#
+# Instead, provide the password as a unix environment variable when you boot
+# the app. Read https://guides.rubyonrails.org/configuring.html#configuring-a-database
+# for a full rundown on how to provide these environment variables in a
+# production deployment.
+#
+# On Heroku and other platform providers, you may have a full connection URL
+# available as an environment variable. For example:
+#
+# DATABASE_URL="ibm-db://myuser:mypass@localhost/somedatabase"
+#
+# You can use this database configuration with:
+#
+# production:
+# url: <%%= ENV['DATABASE_URL'] %>
+#
+production:
+ <<: *default
+ database: <%= app_name %>_production
+ username: <%= app_name %>
+ password: <%%= ENV['<%= app_name.upcase %>_DATABASE_PASSWORD'] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml
deleted file mode 100644
index db0a429753..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml
+++ /dev/null
@@ -1,68 +0,0 @@
-# If you are using mssql, derby, hsqldb, or h2 with one of the
-# ActiveRecord JDBC adapters, install the appropriate driver, e.g.,:
-# gem install activerecord-jdbcmssql-adapter
-#
-# Configure using Gemfile:
-# gem 'activerecord-jdbcmssql-adapter'
-#
-# development:
-# adapter: mssql
-# username: <%= app_name %>
-# password:
-# host: localhost
-# database: <%= app_name %>_development
-#
-# Warning: The database defined as "test" will be erased and
-# re-generated from your development database when you run "rake".
-# Do not set this db to the same as development or production.
-#
-# test:
-# adapter: mssql
-# username: <%= app_name %>
-# password:
-# host: localhost
-# database: <%= app_name %>_test
-#
-# production:
-# adapter: mssql
-# username: <%= app_name %>
-# password:
-# host: localhost
-# database: <%= app_name %>_production
-
-# If you are using oracle, db2, sybase, informix or prefer to use the plain
-# JDBC adapter, configure your database setting as the example below (requires
-# you to download and manually install the database vendor's JDBC driver .jar
-# file). See your driver documentation for the appropriate driver class and
-# connection string:
-
-default: &default
- adapter: jdbc
- username: <%= app_name %>
- password:
- driver:
-
-development:
- <<: *default
- url: jdbc:db://localhost/<%= app_name %>_development
-
-# Warning: The database defined as "test" will be erased and
-# re-generated from your development database when you run "rake".
-# Do not set this db to the same as development or production.
-test:
- <<: *default
- url: jdbc:db://localhost/<%= app_name %>_test
-
-# As with config/secrets.yml, you never want to store sensitive information,
-# like your database password, in your source code. If your source code is
-# ever seen by anyone, they now have access to your database.
-#
-# Instead, provide the password as a unix environment variable when you boot
-# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
-# for a full rundown on how to provide these environment variables in a
-# production deployment.
-#
-production:
- url: jdbc:db://localhost/<%= app_name %>_production
- username: <%= app_name %>
- password: <%%= ENV['<%= app_name.upcase %>_DATABASE_PASSWORD'] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml.tt
new file mode 100644
index 0000000000..af69f12059
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml.tt
@@ -0,0 +1,69 @@
+# If you are using mssql, derby, hsqldb, or h2 with one of the
+# ActiveRecord JDBC adapters, install the appropriate driver, e.g.,:
+# gem install activerecord-jdbcmssql-adapter
+#
+# Configure using Gemfile:
+# gem 'activerecord-jdbcmssql-adapter'
+#
+# development:
+# adapter: mssql
+# username: <%= app_name %>
+# password:
+# host: localhost
+# database: <%= app_name %>_development
+#
+# Warning: The database defined as "test" will be erased and
+# re-generated from your development database when you run "rake".
+# Do not set this db to the same as development or production.
+#
+# test:
+# adapter: mssql
+# username: <%= app_name %>
+# password:
+# host: localhost
+# database: <%= app_name %>_test
+#
+# production:
+# adapter: mssql
+# username: <%= app_name %>
+# password:
+# host: localhost
+# database: <%= app_name %>_production
+
+# If you are using oracle, db2, sybase, informix or prefer to use the plain
+# JDBC adapter, configure your database setting as the example below (requires
+# you to download and manually install the database vendor's JDBC driver .jar
+# file). See your driver documentation for the appropriate driver class and
+# connection string:
+
+default: &default
+ adapter: jdbc
+ pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
+ username: <%= app_name %>
+ password:
+ driver:
+
+development:
+ <<: *default
+ url: jdbc:db://localhost/<%= app_name %>_development
+
+# Warning: The database defined as "test" will be erased and
+# re-generated from your development database when you run "rake".
+# Do not set this db to the same as development or production.
+test:
+ <<: *default
+ url: jdbc:db://localhost/<%= app_name %>_test
+
+# As with config/secrets.yml, you never want to store sensitive information,
+# like your database password, in your source code. If your source code is
+# ever seen by anyone, they now have access to your database.
+#
+# Instead, provide the password as a unix environment variable when you boot
+# the app. Read https://guides.rubyonrails.org/configuring.html#configuring-a-database
+# for a full rundown on how to provide these environment variables in a
+# production deployment.
+#
+production:
+ url: jdbc:db://localhost/<%= app_name %>_production
+ username: <%= app_name %>
+ password: <%%= ENV['<%= app_name.upcase %>_DATABASE_PASSWORD'] %>
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
deleted file mode 100644
index f5b62e8fb3..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml
+++ /dev/null
@@ -1,52 +0,0 @@
-# MySQL. Versions 4.1 and 5.0 are recommended.
-#
-# Install the MySQL driver:
-# gem install activerecord-jdbcmysql-adapter
-#
-# Configure Using Gemfile
-# gem 'activerecord-jdbcmysql-adapter'
-#
-# And be sure to use new-style password hashing:
-# http://dev.mysql.com/doc/refman/5.6/en/old-client.html
-#
-default: &default
- adapter: mysql
- username: root
- password:
- host: localhost
-
-development:
- <<: *default
- database: <%= app_name %>_development
-
-# Warning: The database defined as "test" will be erased and
-# re-generated from your development database when you run "rake".
-# Do not set this db to the same as development or production.
-test:
- <<: *default
- database: <%= app_name %>_test
-
-# As with config/secrets.yml, you never want to store sensitive information,
-# like your database password, in your source code. If your source code is
-# ever seen by anyone, they now have access to your database.
-#
-# Instead, provide the password as a unix environment variable when you boot
-# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
-# for a full rundown on how to provide these environment variables in a
-# production deployment.
-#
-# On Heroku and other platform providers, you may have a full connection URL
-# available as an environment variable. For example:
-#
-# DATABASE_URL="mysql://myuser:mypass@localhost/somedatabase"
-#
-# You can use this database configuration with:
-#
-# production:
-# url: <%%= ENV['DATABASE_URL'] %>
-#
-production:
- <<: *default
- database: <%= app_name %>_production
- username: <%= app_name %>
- password: <%%= ENV['<%= app_name.upcase %>_DATABASE_PASSWORD'] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml.tt
new file mode 100644
index 0000000000..f39593372c
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml.tt
@@ -0,0 +1,53 @@
+# MySQL. Versions 5.5.8 and up are supported.
+#
+# Install the MySQL driver:
+# gem install activerecord-jdbcmysql-adapter
+#
+# Configure Using Gemfile
+# gem 'activerecord-jdbcmysql-adapter'
+#
+# And be sure to use new-style password hashing:
+# https://dev.mysql.com/doc/refman/5.7/en/password-hashing.html
+#
+default: &default
+ adapter: mysql
+ pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
+ username: root
+ password:
+ host: localhost
+
+development:
+ <<: *default
+ database: <%= app_name %>_development
+
+# Warning: The database defined as "test" will be erased and
+# re-generated from your development database when you run "rake".
+# Do not set this db to the same as development or production.
+test:
+ <<: *default
+ database: <%= app_name %>_test
+
+# As with config/secrets.yml, you never want to store sensitive information,
+# like your database password, in your source code. If your source code is
+# ever seen by anyone, they now have access to your database.
+#
+# Instead, provide the password as a unix environment variable when you boot
+# the app. Read https://guides.rubyonrails.org/configuring.html#configuring-a-database
+# for a full rundown on how to provide these environment variables in a
+# production deployment.
+#
+# On Heroku and other platform providers, you may have a full connection URL
+# available as an environment variable. For example:
+#
+# DATABASE_URL="mysql://myuser:mypass@localhost/somedatabase"
+#
+# You can use this database configuration with:
+#
+# production:
+# url: <%%= ENV['DATABASE_URL'] %>
+#
+production:
+ <<: *default
+ database: <%= app_name %>_production
+ username: <%= app_name %>
+ password: <%%= ENV['<%= app_name.upcase %>_DATABASE_PASSWORD'] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml
deleted file mode 100644
index 9e99264d33..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml
+++ /dev/null
@@ -1,68 +0,0 @@
-# PostgreSQL. Versions 8.2 and up are supported.
-#
-# Configure Using Gemfile
-# gem 'activerecord-jdbcpostgresql-adapter'
-#
-default: &default
- adapter: postgresql
- encoding: unicode
-
-development:
- <<: *default
- database: <%= app_name %>_development
-
- # The specified database role being used to connect to postgres.
- # To create additional roles in postgres see `$ createuser --help`.
- # When left blank, postgres will use the default role. This is
- # the same name as the operating system user that initialized the database.
- #username: <%= app_name %>
-
- # The password associated with the postgres role (username).
- #password:
-
- # Connect on a TCP socket. Omitted by default since the client uses a
- # domain socket that doesn't need configuration. Windows does not have
- # domain sockets, so uncomment these lines.
- #host: localhost
- #port: 5432
-
- # Schema search path. The server defaults to $user,public
- #schema_search_path: myapp,sharedapp,public
-
- # Minimum log levels, in increasing order:
- # debug5, debug4, debug3, debug2, debug1,
- # log, notice, warning, error, fatal, and panic
- # Defaults to warning.
- #min_messages: notice
-
-# Warning: The database defined as "test" will be erased and
-# re-generated from your development database when you run "rake".
-# Do not set this db to the same as development or production.
-test:
- <<: *default
- database: <%= app_name %>_test
-
-# As with config/secrets.yml, you never want to store sensitive information,
-# like your database password, in your source code. If your source code is
-# ever seen by anyone, they now have access to your database.
-#
-# Instead, provide the password as a unix environment variable when you boot
-# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
-# for a full rundown on how to provide these environment variables in a
-# production deployment.
-#
-# On Heroku and other platform providers, you may have a full connection URL
-# available as an environment variable. For example:
-#
-# DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase"
-#
-# You can use this database configuration with:
-#
-# production:
-# url: <%%= ENV['DATABASE_URL'] %>
-#
-production:
- <<: *default
- database: <%= app_name %>_production
- username: <%= app_name %>
- password: <%%= ENV['<%= app_name.upcase %>_DATABASE_PASSWORD'] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml.tt
new file mode 100644
index 0000000000..df8a6ad627
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml.tt
@@ -0,0 +1,69 @@
+# PostgreSQL. Versions 9.1 and up are supported.
+#
+# Configure Using Gemfile
+# gem 'activerecord-jdbcpostgresql-adapter'
+#
+default: &default
+ adapter: postgresql
+ encoding: unicode
+ pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
+
+development:
+ <<: *default
+ database: <%= app_name %>_development
+
+ # The specified database role being used to connect to postgres.
+ # To create additional roles in postgres see `$ createuser --help`.
+ # When left blank, postgres will use the default role. This is
+ # the same name as the operating system user that initialized the database.
+ #username: <%= app_name %>
+
+ # The password associated with the postgres role (username).
+ #password:
+
+ # Connect on a TCP socket. Omitted by default since the client uses a
+ # domain socket that doesn't need configuration. Windows does not have
+ # domain sockets, so uncomment these lines.
+ #host: localhost
+ #port: 5432
+
+ # Schema search path. The server defaults to $user,public
+ #schema_search_path: myapp,sharedapp,public
+
+ # Minimum log levels, in increasing order:
+ # debug5, debug4, debug3, debug2, debug1,
+ # log, notice, warning, error, fatal, and panic
+ # Defaults to warning.
+ #min_messages: notice
+
+# Warning: The database defined as "test" will be erased and
+# re-generated from your development database when you run "rake".
+# Do not set this db to the same as development or production.
+test:
+ <<: *default
+ database: <%= app_name %>_test
+
+# As with config/secrets.yml, you never want to store sensitive information,
+# like your database password, in your source code. If your source code is
+# ever seen by anyone, they now have access to your database.
+#
+# Instead, provide the password as a unix environment variable when you boot
+# the app. Read https://guides.rubyonrails.org/configuring.html#configuring-a-database
+# for a full rundown on how to provide these environment variables in a
+# production deployment.
+#
+# On Heroku and other platform providers, you may have a full connection URL
+# available as an environment variable. For example:
+#
+# DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase"
+#
+# You can use this database configuration with:
+#
+# production:
+# url: <%%= ENV['DATABASE_URL'] %>
+#
+production:
+ <<: *default
+ database: <%= app_name %>_production
+ username: <%= app_name %>
+ password: <%%= ENV['<%= app_name.upcase %>_DATABASE_PASSWORD'] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml
deleted file mode 100644
index 28c36eb82f..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml
+++ /dev/null
@@ -1,23 +0,0 @@
-# SQLite version 3.x
-# gem 'activerecord-jdbcsqlite3-adapter'
-#
-# Configure Using Gemfile
-# gem 'activerecord-jdbcsqlite3-adapter'
-#
-default: &default
- adapter: sqlite3
-
-development:
- <<: *default
- database: db/development.sqlite3
-
-# Warning: The database defined as "test" will be erased and
-# re-generated from your development database when you run "rake".
-# Do not set this db to the same as development or production.
-test:
- <<: *default
- database: db/test.sqlite3
-
-production:
- <<: *default
- database: db/production.sqlite3
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml.tt
new file mode 100644
index 0000000000..371415e6a8
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml.tt
@@ -0,0 +1,24 @@
+# SQLite version 3.x
+# gem 'activerecord-jdbcsqlite3-adapter'
+#
+# Configure Using Gemfile
+# gem 'activerecord-jdbcsqlite3-adapter'
+#
+default: &default
+ adapter: sqlite3
+ pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
+
+development:
+ <<: *default
+ database: db/development.sqlite3
+
+# Warning: The database defined as "test" will be erased and
+# re-generated from your development database when you run "rake".
+# Do not set this db to the same as development or production.
+test:
+ <<: *default
+ database: db/test.sqlite3
+
+production:
+ <<: *default
+ database: db/production.sqlite3
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
deleted file mode 100644
index b0767bd93a..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml
+++ /dev/null
@@ -1,58 +0,0 @@
-# MySQL. Versions 5.0+ are recommended.
-#
-# Install the MySQL driver
-# gem install mysql2
-#
-# Ensure the MySQL gem is defined in your Gemfile
-# gem 'mysql2'
-#
-# And be sure to use new-style password hashing:
-# http://dev.mysql.com/doc/refman/5.6/en/old-client.html
-#
-default: &default
- adapter: mysql2
- encoding: utf8
- pool: 5
- username: root
- password:
-<% if mysql_socket -%>
- socket: <%= mysql_socket %>
-<% else -%>
- host: localhost
-<% end -%>
-
-development:
- <<: *default
- database: <%= app_name %>_development
-
-# Warning: The database defined as "test" will be erased and
-# re-generated from your development database when you run "rake".
-# Do not set this db to the same as development or production.
-test:
- <<: *default
- database: <%= app_name %>_test
-
-# As with config/secrets.yml, you never want to store sensitive information,
-# like your database password, in your source code. If your source code is
-# ever seen by anyone, they now have access to your database.
-#
-# Instead, provide the password as a unix environment variable when you boot
-# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
-# for a full rundown on how to provide these environment variables in a
-# production deployment.
-#
-# On Heroku and other platform providers, you may have a full connection URL
-# available as an environment variable. For example:
-#
-# DATABASE_URL="mysql2://myuser:mypass@localhost/somedatabase"
-#
-# You can use this database configuration with:
-#
-# production:
-# url: <%%= ENV['DATABASE_URL'] %>
-#
-production:
- <<: *default
- database: <%= app_name %>_production
- username: <%= app_name %>
- password: <%%= ENV['<%= app_name.upcase %>_DATABASE_PASSWORD'] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml.tt
new file mode 100644
index 0000000000..b6c2e7448a
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml.tt
@@ -0,0 +1,58 @@
+# MySQL. Versions 5.5.8 and up are supported.
+#
+# Install the MySQL driver
+# gem install mysql2
+#
+# Ensure the MySQL gem is defined in your Gemfile
+# gem 'mysql2'
+#
+# And be sure to use new-style password hashing:
+# https://dev.mysql.com/doc/refman/5.7/en/password-hashing.html
+#
+default: &default
+ adapter: mysql2
+ encoding: utf8mb4
+ pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
+ username: root
+ password:
+<% if mysql_socket -%>
+ socket: <%= mysql_socket %>
+<% else -%>
+ host: localhost
+<% end -%>
+
+development:
+ <<: *default
+ database: <%= app_name %>_development
+
+# Warning: The database defined as "test" will be erased and
+# re-generated from your development database when you run "rake".
+# Do not set this db to the same as development or production.
+test:
+ <<: *default
+ database: <%= app_name %>_test
+
+# As with config/secrets.yml, you never want to store sensitive information,
+# like your database password, in your source code. If your source code is
+# ever seen by anyone, they now have access to your database.
+#
+# Instead, provide the password as a unix environment variable when you boot
+# the app. Read https://guides.rubyonrails.org/configuring.html#configuring-a-database
+# for a full rundown on how to provide these environment variables in a
+# production deployment.
+#
+# On Heroku and other platform providers, you may have a full connection URL
+# available as an environment variable. For example:
+#
+# DATABASE_URL="mysql2://myuser:mypass@localhost/somedatabase"
+#
+# You can use this database configuration with:
+#
+# production:
+# url: <%%= ENV['DATABASE_URL'] %>
+#
+production:
+ <<: *default
+ database: <%= app_name %>_production
+ username: <%= app_name %>
+ password: <%%= ENV['<%= app_name.upcase %>_DATABASE_PASSWORD'] %>
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
deleted file mode 100644
index 9aedcc15cb..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml
+++ /dev/null
@@ -1,58 +0,0 @@
-# Oracle/OCI 8i, 9, 10g
-#
-# Requires Ruby/OCI8:
-# https://github.com/kubo/ruby-oci8
-#
-# Specify your database using any valid connection syntax, such as a
-# tnsnames.ora service name, or an SQL connect string of the form:
-#
-# //host:[port][/service name]
-#
-# By default prefetch_rows (OCI_ATTR_PREFETCH_ROWS) is set to 100. And
-# until true bind variables are supported, cursor_sharing is set by default
-# to 'similar'. Both can be changed in the configuration below; the defaults
-# are equivalent to specifying:
-#
-# prefetch_rows: 100
-# cursor_sharing: similar
-#
-default: &default
- adapter: oracle
- username: <%= app_name %>
- password:
-
-development:
- <<: *default
- database: <%= app_name %>_development
-
-# Warning: The database defined as "test" will be erased and
-# re-generated from your development database when you run "rake".
-# Do not set this db to the same as development or production.
-test:
- <<: *default
- database: <%= app_name %>_test
-
-# As with config/secrets.yml, you never want to store sensitive information,
-# like your database password, in your source code. If your source code is
-# ever seen by anyone, they now have access to your database.
-#
-# Instead, provide the password as a unix environment variable when you boot
-# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
-# for a full rundown on how to provide these environment variables in a
-# production deployment.
-#
-# 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"
-#
-# You can use this database configuration with:
-#
-# production:
-# url: <%%= ENV['DATABASE_URL'] %>
-#
-production:
- <<: *default
- database: <%= app_name %>_production
- username: <%= app_name %>
- password: <%%= ENV['<%= app_name.upcase %>_DATABASE_PASSWORD'] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml.tt
new file mode 100644
index 0000000000..8d9d33ba6c
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml.tt
@@ -0,0 +1,61 @@
+# Oracle/OCI 11g or higher recommended
+#
+# Requires Ruby/OCI8:
+# https://github.com/kubo/ruby-oci8
+#
+# Specify your database using any valid connection syntax, such as a
+# tnsnames.ora service name, or an SQL connect string of the form:
+#
+# //host:[port][/service name]
+#
+# By default prefetch_rows (OCI_ATTR_PREFETCH_ROWS) is set to 100. And
+# until true bind variables are supported, cursor_sharing is set by default
+# to 'similar'. Both can be changed in the configuration below; the defaults
+# are equivalent to specifying:
+#
+# prefetch_rows: 100
+# cursor_sharing: similar
+#
+default: &default
+ adapter: oracle_enhanced
+ pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
+ username: <%= app_name %>
+ password:
+
+development:
+ <<: *default
+ database: <%= app_name %>_development
+
+# Warning: The database defined as "test" will be erased and
+# re-generated from your development database when you run "rake".
+# Do not set this db to the same as development or production.
+test:
+ <<: *default
+ database: <%= app_name %>_test
+
+# As with config/secrets.yml, you never want to store sensitive information,
+# like your database password, in your source code. If your source code is
+# ever seen by anyone, they now have access to your database.
+#
+# Instead, provide the password as a unix environment variable when you boot
+# the app. Read https://guides.rubyonrails.org/configuring.html#configuring-a-database
+# for a full rundown on how to provide these environment variables in a
+# production deployment.
+#
+# On Heroku and other platform providers, you may have a full connection URL
+# available as an environment variable. For example:
+#
+# 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:
+#
+# production:
+# url: <%%= ENV['DATABASE_URL'] %>
+#
+production:
+ <<: *default
+ database: <%= app_name %>_production
+ username: <%= app_name %>
+ password: <%%= ENV['<%= app_name.upcase %>_DATABASE_PASSWORD'] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml
deleted file mode 100644
index feb25bbc6b..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml
+++ /dev/null
@@ -1,85 +0,0 @@
-# PostgreSQL. Versions 8.2 and up are supported.
-#
-# Install the pg driver:
-# gem install pg
-# On OS X with Homebrew:
-# gem install pg -- --with-pg-config=/usr/local/bin/pg_config
-# On OS X with MacPorts:
-# gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config
-# On Windows:
-# gem install pg
-# Choose the win32 build.
-# Install PostgreSQL and put its /bin directory on your path.
-#
-# Configure Using Gemfile
-# gem 'pg'
-#
-default: &default
- adapter: postgresql
- encoding: unicode
- # For details on connection pooling, see rails configuration guide
- # http://guides.rubyonrails.org/configuring.html#database-pooling
- pool: 5
-
-development:
- <<: *default
- database: <%= app_name %>_development
-
- # The specified database role being used to connect to postgres.
- # To create additional roles in postgres see `$ createuser --help`.
- # When left blank, postgres will use the default role. This is
- # the same name as the operating system user that initialized the database.
- #username: <%= app_name %>
-
- # The password associated with the postgres role (username).
- #password:
-
- # Connect on a TCP socket. Omitted by default since the client uses a
- # domain socket that doesn't need configuration. Windows does not have
- # domain sockets, so uncomment these lines.
- #host: localhost
-
- # The TCP port the server listens on. Defaults to 5432.
- # If your server runs on a different port number, change accordingly.
- #port: 5432
-
- # Schema search path. The server defaults to $user,public
- #schema_search_path: myapp,sharedapp,public
-
- # Minimum log levels, in increasing order:
- # debug5, debug4, debug3, debug2, debug1,
- # log, notice, warning, error, fatal, and panic
- # Defaults to warning.
- #min_messages: notice
-
-# Warning: The database defined as "test" will be erased and
-# re-generated from your development database when you run "rake".
-# Do not set this db to the same as development or production.
-test:
- <<: *default
- database: <%= app_name %>_test
-
-# As with config/secrets.yml, you never want to store sensitive information,
-# like your database password, in your source code. If your source code is
-# ever seen by anyone, they now have access to your database.
-#
-# Instead, provide the password as a unix environment variable when you boot
-# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
-# for a full rundown on how to provide these environment variables in a
-# production deployment.
-#
-# On Heroku and other platform providers, you may have a full connection URL
-# available as an environment variable. For example:
-#
-# DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase"
-#
-# You can use this database configuration with:
-#
-# production:
-# url: <%%= ENV['DATABASE_URL'] %>
-#
-production:
- <<: *default
- database: <%= app_name %>_production
- username: <%= app_name %>
- password: <%%= ENV['<%= app_name.upcase %>_DATABASE_PASSWORD'] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml.tt
new file mode 100644
index 0000000000..dcd57425e2
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml.tt
@@ -0,0 +1,85 @@
+# PostgreSQL. Versions 9.1 and up are supported.
+#
+# Install the pg driver:
+# gem install pg
+# On macOS with Homebrew:
+# gem install pg -- --with-pg-config=/usr/local/bin/pg_config
+# On macOS with MacPorts:
+# gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config
+# On Windows:
+# gem install pg
+# Choose the win32 build.
+# Install PostgreSQL and put its /bin directory on your path.
+#
+# Configure Using Gemfile
+# gem 'pg'
+#
+default: &default
+ adapter: postgresql
+ encoding: unicode
+ # For details on connection pooling, see Rails configuration guide
+ # https://guides.rubyonrails.org/configuring.html#database-pooling
+ pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
+
+development:
+ <<: *default
+ database: <%= app_name %>_development
+
+ # The specified database role being used to connect to postgres.
+ # To create additional roles in postgres see `$ createuser --help`.
+ # When left blank, postgres will use the default role. This is
+ # the same name as the operating system user that initialized the database.
+ #username: <%= app_name %>
+
+ # The password associated with the postgres role (username).
+ #password:
+
+ # Connect on a TCP socket. Omitted by default since the client uses a
+ # domain socket that doesn't need configuration. Windows does not have
+ # domain sockets, so uncomment these lines.
+ #host: localhost
+
+ # The TCP port the server listens on. Defaults to 5432.
+ # If your server runs on a different port number, change accordingly.
+ #port: 5432
+
+ # Schema search path. The server defaults to $user,public
+ #schema_search_path: myapp,sharedapp,public
+
+ # Minimum log levels, in increasing order:
+ # debug5, debug4, debug3, debug2, debug1,
+ # log, notice, warning, error, fatal, and panic
+ # Defaults to warning.
+ #min_messages: notice
+
+# Warning: The database defined as "test" will be erased and
+# re-generated from your development database when you run "rake".
+# Do not set this db to the same as development or production.
+test:
+ <<: *default
+ database: <%= app_name %>_test
+
+# As with config/secrets.yml, you never want to store sensitive information,
+# like your database password, in your source code. If your source code is
+# ever seen by anyone, they now have access to your database.
+#
+# Instead, provide the password as a unix environment variable when you boot
+# the app. Read https://guides.rubyonrails.org/configuring.html#configuring-a-database
+# for a full rundown on how to provide these environment variables in a
+# production deployment.
+#
+# On Heroku and other platform providers, you may have a full connection URL
+# available as an environment variable. For example:
+#
+# DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase"
+#
+# You can use this database configuration with:
+#
+# production:
+# url: <%%= ENV['DATABASE_URL'] %>
+#
+production:
+ <<: *default
+ database: <%= app_name %>_production
+ username: <%= app_name %>
+ password: <%%= ENV['<%= app_name.upcase %>_DATABASE_PASSWORD'] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml
deleted file mode 100644
index 1c1a37ca8d..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml
+++ /dev/null
@@ -1,25 +0,0 @@
-# SQLite version 3.x
-# gem install sqlite3
-#
-# Ensure the SQLite 3 gem is defined in your Gemfile
-# gem 'sqlite3'
-#
-default: &default
- adapter: sqlite3
- pool: 5
- timeout: 5000
-
-development:
- <<: *default
- database: db/development.sqlite3
-
-# Warning: The database defined as "test" will be erased and
-# re-generated from your development database when you run "rake".
-# Do not set this db to the same as development or production.
-test:
- <<: *default
- database: db/test.sqlite3
-
-production:
- <<: *default
- database: db/production.sqlite3
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml.tt
new file mode 100644
index 0000000000..9510568124
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml.tt
@@ -0,0 +1,25 @@
+# SQLite version 3.x
+# gem install sqlite3
+#
+# Ensure the SQLite 3 gem is defined in your Gemfile
+# gem 'sqlite3'
+#
+default: &default
+ adapter: sqlite3
+ pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
+ timeout: 5000
+
+development:
+ <<: *default
+ database: db/development.sqlite3
+
+# Warning: The database defined as "test" will be erased and
+# re-generated from your development database when you run "rake".
+# Do not set this db to the same as development or production.
+test:
+ <<: *default
+ database: db/test.sqlite3
+
+production:
+ <<: *default
+ database: db/production.sqlite3
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
deleted file mode 100644
index 30b0df34a8..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml
+++ /dev/null
@@ -1,68 +0,0 @@
-# SQL Server (2005 or higher recommended)
-#
-# Install the adapters and driver
-# gem install tiny_tds
-# gem install activerecord-sqlserver-adapter
-#
-# Ensure the activerecord adapter and db driver gems are defined in your Gemfile
-# 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
- reconnect: false
- username: <%= app_name %>
- password:
- timeout: 25
- dataserver: from_freetds.conf
-
-development:
- <<: *default
- database: <%= app_name %>_development
-
-# Warning: The database defined as "test" will be erased and
-# re-generated from your development database when you run "rake".
-# Do not set this db to the same as development or production.
-test:
- <<: *default
- database: <%= app_name %>_test
-
-# As with config/secrets.yml, you never want to store sensitive information,
-# like your database password, in your source code. If your source code is
-# ever seen by anyone, they now have access to your database.
-#
-# Instead, provide the password as a unix environment variable when you boot
-# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
-# for a full rundown on how to provide these environment variables in a
-# production deployment.
-#
-# On Heroku and other platform providers, you may have a full connection URL
-# available as an environment variable. For example:
-#
-# DATABASE_URL="sqlserver://myuser:mypass@localhost/somedatabase"
-#
-# You can use this database configuration with:
-#
-# production:
-# url: <%%= ENV['DATABASE_URL'] %>
-#
-production:
- <<: *default
- database: <%= app_name %>_production
- username: <%= app_name %>
- password: <%%= ENV['<%= app_name.upcase %>_DATABASE_PASSWORD'] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml.tt
new file mode 100644
index 0000000000..0246fb0d02
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml.tt
@@ -0,0 +1,52 @@
+# SQL Server (2012 or higher required)
+#
+# Install the adapters and driver
+# gem install tiny_tds
+# gem install activerecord-sqlserver-adapter
+#
+# Ensure the activerecord adapter and db driver gems are defined in your Gemfile
+# gem 'tiny_tds'
+# gem 'activerecord-sqlserver-adapter'
+#
+default: &default
+ adapter: sqlserver
+ encoding: utf8
+ username: sa
+ password: <%%= ENV['SA_PASSWORD'] %>
+ host: localhost
+
+development:
+ <<: *default
+ database: <%= app_name %>_development
+
+# Warning: The database defined as "test" will be erased and
+# re-generated from your development database when you run "rake".
+# Do not set this db to the same as development or production.
+test:
+ <<: *default
+ database: <%= app_name %>_test
+
+# As with config/secrets.yml, you never want to store sensitive information,
+# like your database password, in your source code. If your source code is
+# ever seen by anyone, they now have access to your database.
+#
+# Instead, provide the password as a unix environment variable when you boot
+# the app. Read https://guides.rubyonrails.org/configuring.html#configuring-a-database
+# for a full rundown on how to provide these environment variables in a
+# production deployment.
+#
+# On Heroku and other platform providers, you may have a full connection URL
+# available as an environment variable. For example:
+#
+# DATABASE_URL="sqlserver://myuser:mypass@localhost/somedatabase"
+#
+# You can use this database configuration with:
+#
+# production:
+# url: <%%= ENV['DATABASE_URL'] %>
+#
+production:
+ <<: *default
+ database: <%= app_name %>_production
+ username: <%= app_name %>
+ password: <%%= ENV['<%= app_name.upcase %>_DATABASE_PASSWORD'] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/environment.rb b/railties/lib/rails/generators/rails/app/templates/config/environment.rb
deleted file mode 100644
index ee8d90dc65..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/environment.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# Load the Rails application.
-require File.expand_path('../application', __FILE__)
-
-# Initialize the Rails application.
-Rails.application.initialize!
diff --git a/railties/lib/rails/generators/rails/app/templates/config/environment.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environment.rb.tt
new file mode 100644
index 0000000000..426333bb46
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/environment.rb.tt
@@ -0,0 +1,5 @@
+# Load the Rails application.
+require_relative 'application'
+
+# Initialize the Rails application.
+Rails.application.initialize!
diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt
index ecb5d4170f..3807c8a9aa 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt
@@ -9,13 +9,34 @@ Rails.application.configure do
# Do not eager load code on boot.
config.eager_load = false
- # Show full error reports and disable caching.
- config.consider_all_requests_local = true
- config.action_controller.perform_caching = false
+ # Show full error reports.
+ 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
+
+ config.cache_store = :memory_store
+ config.public_file_server.headers = {
+ 'Cache-Control' => "public, max-age=#{2.days.to_i}"
+ }
+ else
+ config.action_controller.perform_caching = false
+
+ config.cache_store = :null_store
+ end
+ <%- unless skip_active_storage? -%>
+
+ # Store uploaded files on the local file system (see config/storage.yml for options).
+ config.active_storage.service = :local
+ <%- end -%>
<%- unless options.skip_action_mailer? -%>
# Don't care if the mailer can't send.
config.action_mailer.raise_delivery_errors = false
+
+ config.action_mailer.perform_caching = false
<%- end -%>
# Print deprecation notices to the Rails logger.
@@ -25,6 +46,9 @@ Rails.application.configure do
# Raise an error on page load if there are pending migrations.
config.active_record.migration_error = :page_load
+ # Highlight code that triggered database queries in logs.
+ config.active_record.verbose_query_logs = true
+
<%- end -%>
<%- unless options.skip_sprockets? -%>
# Debug mode disables concatenation and preprocessing of assets.
@@ -32,16 +56,14 @@ Rails.application.configure do
# number of complex assets.
config.assets.debug = true
- # Asset digests allow you to set far-future HTTP expiration dates on all assets,
- # yet still be able to expire them through the digest params.
- config.assets.digest = true
-
- # Adds additional error checking when serving assets at runtime.
- # Checks for improperly declared sprockets dependencies.
- # Raises helpful error messages.
- config.assets.raise_runtime_errors = true
+ # Suppress logger output for asset requests.
+ config.assets.quiet = true
<%- end -%>
- # Raises error for missing translations
+ # Raises error for missing translations.
# config.action_view.raise_on_missing_translations = true
+
+ # Use an evented file watcher to asynchronously detect changes in source code,
+ # routes, locales, etc. This feature depends on the listen gem.
+ <%= '# ' unless depend_on_listen? %>config.file_watcher = ActiveSupport::EventedFileUpdateChecker
end
diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt
index 8c09396fc1..08befd9196 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,25 +14,22 @@ Rails.application.configure do
config.consider_all_requests_local = false
config.action_controller.perform_caching = true
+ # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
+ # or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
+ # config.require_master_key = true
+
# Disable serving static files from the `/public` folder by default since
# Apache or NGINX already handles this.
- config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present?
+ config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
<%- unless options.skip_sprockets? -%>
- # Compress JavaScripts and CSS.
- config.assets.js_compressor = :uglifier
+ # Compress CSS using a preprocessor.
# config.assets.css_compressor = :sass
# Do not fallback to assets pipeline if a precompiled asset is missed.
config.assets.compile = false
- # Asset digests allow you to set far-future HTTP expiration dates on all assets,
- # yet still be able to expire them through the digest params.
- config.assets.digest = true
-
- # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
<%- end -%>
-
# Enable serving of images, stylesheets, and JavaScripts from an asset server.
# config.action_controller.asset_host = 'http://assets.example.com'
@@ -40,6 +37,18 @@ Rails.application.configure do
# config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
+ <%- unless skip_active_storage? -%>
+ # Store uploaded files on the local file system (see config/storage.yml for options).
+ config.active_storage.service = :local
+
+ <%- end -%>
+ <%- unless options[:skip_action_cable] -%>
+ # Mount Action Cable outside main process or domain.
+ # config.action_cable.mount_path = nil
+ # config.action_cable.url = 'wss://example.com/cable'
+ # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
+
+ <%- end -%>
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
# config.force_ssl = true
@@ -48,24 +57,23 @@ Rails.application.configure do
config.log_level = :debug
# Prepend all log lines with the following tags.
- # config.log_tags = [ :subdomain, :request_id ]
-
- # Use a different logger for distributed setups.
- # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new)
+ config.log_tags = [ :request_id ]
# Use a different cache store in production.
# config.cache_store = :mem_cache_store
- # Use a real queuing backend for Active Job (and separate queues per environment)
+ # Use a real queuing backend for Active Job (and separate queues per environment).
# config.active_job.queue_adapter = :resque
- # config.active_job.queue_name_prefix = "<%= app_name %>_#{Rails.env}"
+ # config.active_job.queue_name_prefix = "<%= app_name %>_production"
+
<%- unless options.skip_action_mailer? -%>
+ config.action_mailer.perform_caching = false
# Ignore bad email addresses and do not raise email delivery errors.
# Set this to true and configure the email server for immediate delivery to raise delivery errors.
# config.action_mailer.raise_delivery_errors = false
- <%- end -%>
+ <%- end -%>
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
# the I18n.default_locale when a translation cannot be found).
config.i18n.fallbacks = true
@@ -75,6 +83,16 @@ Rails.application.configure do
# Use default logging formatter so that PID and timestamp are not suppressed.
config.log_formatter = ::Logger::Formatter.new
+
+ # Use a different logger for distributed setups.
+ # require 'syslog/logger'
+ # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
+
+ if ENV["RAILS_LOG_TO_STDOUT"].present?
+ logger = ActiveSupport::Logger.new(STDOUT)
+ logger.formatter = config.log_formatter
+ config.logger = ActiveSupport::TaggedLogging.new(logger)
+ end
<%- unless options.skip_active_record? -%>
# Do not dump schema after migrations.
diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt
index 0306deb18c..223aa56187 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt
@@ -12,33 +12,43 @@ Rails.application.configure do
# preloads Rails for running tests, you may have to set it to true.
config.eager_load = false
- # Configure static file server for tests with Cache-Control for performance.
- config.serve_static_files = true
- config.static_cache_control = 'public, max-age=3600'
+ # Configure public file server for tests with Cache-Control for performance.
+ config.public_file_server.enabled = true
+ config.public_file_server.headers = {
+ 'Cache-Control' => "public, max-age=#{1.hour.to_i}"
+ }
# Show full error reports and disable caching.
config.consider_all_requests_local = true
config.action_controller.perform_caching = false
+ config.cache_store = :null_store
# Raise exceptions instead of rendering exception templates.
config.action_dispatch.show_exceptions = false
# Disable request forgery protection in test environment.
config.action_controller.allow_forgery_protection = false
+
+ <%- unless skip_active_storage? -%>
+ # Store uploaded files on the local file system in a temporary directory.
+ config.active_storage.service = :test
+
+ <%- end -%>
<%- unless options.skip_action_mailer? -%>
+ config.action_mailer.perform_caching = false
# Tell Action Mailer not to deliver emails to the real world.
# The :test delivery method accumulates sent emails in the
# ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test
- <%- end -%>
-
- # Randomize the order test cases are executed.
- config.active_support.test_order = :random
+ <%- end -%>
# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr
- # Raises error for missing translations
+ # Raises error for missing translations.
# config.action_view.raise_on_missing_translations = true
+
+ # Prevent expensive template finalization at end of test suite runs.
+ config.action_view.finalize_compiled_template_methods = false
end
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/active_record_belongs_to_required_by_default.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/active_record_belongs_to_required_by_default.rb
deleted file mode 100644
index 30c4f89792..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/active_record_belongs_to_required_by_default.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-# Be sure to restart your server when you modify this file.
-
-# Require `belongs_to` associations by default.
-Rails.application.config.active_record.belongs_to_required_by_default = true
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/application_controller_renderer.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/application_controller_renderer.rb
deleted file mode 100644
index ea930f54da..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/application_controller_renderer.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-## Change renderer defaults here.
-#
-# ApplicationController.renderer.defaults.merge!(
-# http_host: 'example.org',
-# https: false
-# )
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/application_controller_renderer.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/application_controller_renderer.rb.tt
new file mode 100644
index 0000000000..89d2efab2b
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/application_controller_renderer.rb.tt
@@ -0,0 +1,8 @@
+# Be sure to restart your server when you modify this file.
+
+# ActiveSupport::Reloader.to_prepare do
+# ApplicationController.renderer.defaults.merge!(
+# http_host: 'example.org',
+# https: false
+# )
+# end
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 01ef3e6630..fe48fc34ee 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,9 +3,10 @@
# 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
# Precompile additional assets.
-# application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
-# Rails.application.config.assets.precompile += %w( search.js )
+# application.js, application.css, and all non-JS/CSS in the app/assets
+# folder are already added.
+# Rails.application.config.assets.precompile += %w( admin.js admin.css )
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/backtrace_silencers.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/backtrace_silencers.rb.tt
index 59385cdf37..59385cdf37 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/backtrace_silencers.rb
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/backtrace_silencers.rb.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/callback_terminator.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/callback_terminator.rb
deleted file mode 100644
index a70a1b9cde..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/callback_terminator.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-# Be sure to restart your server when you modify this file.
-
-# Do not halt callback chains when a callback returns false.
-ActiveSupport.halt_callback_chains_on_return_false = false
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/content_security_policy.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/content_security_policy.rb.tt
new file mode 100644
index 0000000000..c517b0f96b
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/content_security_policy.rb.tt
@@ -0,0 +1,29 @@
+# Be sure to restart your server when you modify this file.
+
+# Define an application-wide content security policy
+# For further information see the following documentation
+# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
+
+# Rails.application.config.content_security_policy do |policy|
+# policy.default_src :self, :https
+# policy.font_src :self, :https, :data
+# policy.img_src :self, :https, :data
+# policy.object_src :none
+# policy.script_src :self, :https
+# policy.style_src :self, :https
+<%- unless options[:skip_javascript] -%>
+# # If you are using webpack-dev-server then specify webpack-dev-server host
+# policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035" if Rails.env.development?
+<%- end -%>
+
+# # Specify URI for violation reports
+# # policy.report_uri "/csp-violation-report-endpoint"
+# end
+
+# If you are using UJS then enable automatic nonce generation
+# Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
+
+# Report CSP violations to a specified URI
+# For further information see the following documentation:
+# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
+# Rails.application.config.content_security_policy_report_only = true
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb
deleted file mode 100644
index 7f70458dee..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-# Be sure to restart your server when you modify this file.
-
-Rails.application.config.action_dispatch.cookies_serializer = :json
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb.tt
new file mode 100644
index 0000000000..5a6a32d371
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb.tt
@@ -0,0 +1,5 @@
+# Be sure to restart your server when you modify this file.
+
+# Specify a serializer for the signed and encrypted cookie jars.
+# Valid options are :json, :marshal, and :hybrid.
+Rails.application.config.action_dispatch.cookies_serializer = :json
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/cors.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/cors.rb
deleted file mode 100644
index 45c44d24f8..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/cors.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# Avoid CORS issues when API is called from the frontend app
-# Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests
-
-# Read more: https://github.com/cyu/rack-cors
-
-# Rails.application.config.middleware.insert_before 0, "Rack::Cors" do
-# allow do
-# origins 'example.com'
-#
-# resource '*',
-# headers: :any,
-# methods: [:get, :post, :put, :patch, :delete, :options, :head]
-# end
-# end
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/cors.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/cors.rb.tt
new file mode 100644
index 0000000000..3b1c1b5ed1
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/cors.rb.tt
@@ -0,0 +1,16 @@
+# Be sure to restart your server when you modify this file.
+
+# Avoid CORS issues when API is called from the frontend app.
+# Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests.
+
+# Read more: https://github.com/cyu/rack-cors
+
+# Rails.application.config.middleware.insert_before 0, Rack::Cors do
+# allow do
+# origins 'example.com'
+#
+# resource '*',
+# headers: :any,
+# methods: [:get, :post, :put, :patch, :delete, :options, :head]
+# end
+# end
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb.tt
index 4a994e1e7b..4a994e1e7b 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb.tt
index ac033bf9dc..ac033bf9dc 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb.tt
index dc1899682b..dc1899682b 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt
new file mode 100644
index 0000000000..54eb0cb1d2
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt
@@ -0,0 +1,17 @@
+# Be sure to restart your server when you modify this file.
+#
+# This file contains migration options to ease your Rails 6.0 upgrade.
+#
+# Once upgraded flip defaults one by one to migrate to the new default.
+#
+# Read the Guide for Upgrading Ruby on Rails for more info on each option.
+
+# Don't force requests from old versions of IE to be UTF-8 encoded
+# Rails.application.config.action_view.default_enforce_utf8 = false
+
+# Embed purpose and expiry metadata inside signed and encrypted
+# cookies for increased security.
+#
+# This option is not backwards compatible with earlier Rails versions.
+# It's best enabled when your entire app is migrated and stable on 6.0.
+# Rails.application.config.action_dispatch.use_cookies_with_metadata = true
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt
deleted file mode 100644
index 2bb9b82c61..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt
+++ /dev/null
@@ -1,3 +0,0 @@
-# Be sure to restart your server when you modify this file.
-
-Rails.application.config.session_store :cookie_store, key: <%= "'_#{app_name}_session'" %>
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..cf9b342d0a 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,8 +16,18 @@
#
# 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.
+# available at https://guides.rubyonrails.org/i18n.html.
en:
hello: "Hello world"
diff --git a/railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt
new file mode 100644
index 0000000000..f6146e7259
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt
@@ -0,0 +1,35 @@
+# Puma can serve each request in a thread from an internal thread pool.
+# 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.
+#
+max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
+min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
+threads min_threads_count, max_threads_count
+
+# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
+#
+port ENV.fetch("PORT") { 3000 }
+
+# Specifies the `environment` that Puma will run in.
+#
+environment ENV.fetch("RAILS_ENV") { "development" }
+
+# Specifies the number of `workers` to boot in clustered mode.
+# Workers are forked webserver processes. If using threads and workers together
+# the concurrency of the application would be max `threads` * `workers`.
+# Workers do not work on JRuby or Windows (both of which do not support
+# processes).
+#
+# workers ENV.fetch("WEB_CONCURRENCY") { 2 }
+
+# Use the `preload_app!` method when specifying a `workers` number.
+# This directive tells Puma to first boot the application and load code
+# before forking the application. This takes advantage of Copy On Write
+# process behavior so workers use less memory.
+#
+# preload_app!
+
+# Allow puma to be restarted by `rails restart` command.
+plugin :tmp_restart
diff --git a/railties/lib/rails/generators/rails/app/templates/config/routes.rb b/railties/lib/rails/generators/rails/app/templates/config/routes.rb
deleted file mode 100644
index 3f66539d54..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/routes.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-Rails.application.routes.draw do
- # The priority is based upon order of creation: first created -> highest priority.
- # See how all your routes lay out with "rake routes".
-
- # You can have the root of your site routed with "root"
- # root 'welcome#index'
-
- # Example of regular route:
- # get 'products/:id' => 'catalog#view'
-
- # Example of named route that can be invoked with purchase_url(id: product.id)
- # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase
-
- # Example resource route (maps HTTP verbs to controller actions automatically):
- # resources :products
-
- # Example resource route with options:
- # resources :products do
- # member do
- # get 'short'
- # post 'toggle'
- # end
- #
- # collection do
- # get 'sold'
- # end
- # end
-
- # Example resource route with sub-resources:
- # resources :products do
- # resources :comments, :sales
- # resource :seller
- # end
-
- # Example resource route with more complex sub-resources:
- # resources :products do
- # resources :comments
- # resources :sales do
- # get 'recent', on: :collection
- # end
- # end
-
- # Example resource route with concerns:
- # concern :toggleable do
- # post 'toggle'
- # end
- # resources :posts, concerns: :toggleable
- # resources :photos, concerns: :toggleable
-
- # Example resource route within a namespace:
- # namespace :admin do
- # # Directs /admin/products/* to Admin::ProductsController
- # # (app/controllers/admin/products_controller.rb)
- # resources :products
- # end
-end
diff --git a/railties/lib/rails/generators/rails/app/templates/config/routes.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/routes.rb.tt
new file mode 100644
index 0000000000..c06383a172
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/routes.rb.tt
@@ -0,0 +1,3 @@
+Rails.application.routes.draw do
+ # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
+end
diff --git a/railties/lib/rails/generators/rails/app/templates/config/secrets.yml b/railties/lib/rails/generators/rails/app/templates/config/secrets.yml
deleted file mode 100644
index b2669a0f79..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/secrets.yml
+++ /dev/null
@@ -1,22 +0,0 @@
-# Be sure to restart your server when you modify this file.
-
-# Your secret key is used for verifying the integrity of signed cookies.
-# If you change this key, all old signed cookies will become invalid!
-
-# Make sure the secret is at least 30 characters and all random,
-# no regular words or you'll be exposed to dictionary attacks.
-# You can use `rake secret` to generate a secure secret key.
-
-# Make sure the secrets in this file are kept private
-# if you're sharing your code publicly.
-
-development:
- secret_key_base: <%= app_secret %>
-
-test:
- secret_key_base: <%= app_secret %>
-
-# Do not keep production secrets in the repository,
-# instead read values from the environment.
-production:
- secret_key_base: <%%= ENV["SECRET_KEY_BASE"] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/spring.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/spring.rb.tt
new file mode 100644
index 0000000000..db5bf1307a
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/spring.rb.tt
@@ -0,0 +1,6 @@
+Spring.watch(
+ ".ruby-version",
+ ".rbenv-vars",
+ "tmp/restart.txt",
+ "tmp/caching-dev.txt"
+)
diff --git a/railties/lib/rails/generators/rails/app/templates/config/storage.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/storage.yml.tt
new file mode 100644
index 0000000000..7207c75086
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/storage.yml.tt
@@ -0,0 +1,34 @@
+test:
+ service: Disk
+ root: <%%= Rails.root.join("tmp/storage") %>
+
+local:
+ service: Disk
+ root: <%%= Rails.root.join("storage") %>
+
+# Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
+# amazon:
+# service: S3
+# access_key_id: <%%= Rails.application.credentials.dig(:aws, :access_key_id) %>
+# secret_access_key: <%%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
+# region: us-east-1
+# bucket: your_own_bucket
+
+# Remember not to checkin your GCS keyfile to a repository
+# google:
+# service: GCS
+# project: your_project
+# credentials: <%%= Rails.root.join("path/to/gcs.keyfile") %>
+# bucket: your_own_bucket
+
+# Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
+# microsoft:
+# service: AzureStorage
+# storage_account_name: your_account_name
+# storage_access_key: <%%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
+# container: your_container_name
+
+# mirror:
+# service: Mirror
+# primary: local
+# mirrors: [ amazon, google, microsoft ]
diff --git a/railties/lib/rails/generators/rails/app/templates/db/seeds.rb.tt b/railties/lib/rails/generators/rails/app/templates/db/seeds.rb.tt
index 4edb1e857e..1beea2accd 100644
--- a/railties/lib/rails/generators/rails/app/templates/db/seeds.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/db/seeds.rb.tt
@@ -1,7 +1,7 @@
# This file should contain all the record creation needed to seed the database with its default values.
-# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
+# The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup).
#
# Examples:
#
-# cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }])
-# Mayor.create(name: 'Emanuel', city: cities.first)
+# movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }])
+# Character.create(name: 'Luke', movie: movies.first)
diff --git a/railties/lib/rails/generators/rails/app/templates/gitignore b/railties/lib/rails/generators/rails/app/templates/gitignore
deleted file mode 100644
index 1b8cf8a9fa..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/gitignore
+++ /dev/null
@@ -1,22 +0,0 @@
-# See https://help.github.com/articles/ignoring-files for more about ignoring files.
-#
-# If you find yourself ignoring temporary files generated by your text editor
-# or operating system, you probably want to add a global ignore instead:
-# git config --global core.excludesfile '~/.gitignore_global'
-
-# Ignore bundler config.
-/.bundle
-
-<% if sqlite3? -%>
-# Ignore the default SQLite database.
-/db/*.sqlite3
-/db/*.sqlite3-journal
-
-<% end -%>
-# Ignore all logfiles and tempfiles.
-/log/*
-/tmp/*
-<% if keeps? -%>
-!/log/.keep
-!/tmp/.keep
-<% end -%>
diff --git a/railties/lib/rails/generators/rails/app/templates/gitignore.tt b/railties/lib/rails/generators/rails/app/templates/gitignore.tt
new file mode 100644
index 0000000000..860baa1595
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/gitignore.tt
@@ -0,0 +1,35 @@
+# See https://help.github.com/articles/ignoring-files for more about ignoring files.
+#
+# If you find yourself ignoring temporary files generated by your text editor
+# or operating system, you probably want to add a global ignore instead:
+# git config --global core.excludesfile '~/.gitignore_global'
+
+# Ignore bundler config.
+/.bundle
+
+<% if sqlite3? -%>
+# Ignore the default SQLite database.
+/db/*.sqlite3
+/db/*.sqlite3-journal
+
+<% end -%>
+# Ignore all logfiles and tempfiles.
+/log/*
+/tmp/*
+<% if keeps? -%>
+!/log/.keep
+!/tmp/.keep
+<% end -%>
+
+<% unless skip_active_storage? -%>
+# Ignore uploaded files in development.
+/storage/*
+<% if keeps? -%>
+!/storage/.keep
+<% end -%>
+<% end -%>
+<% unless options.api? -%>
+
+/public/assets
+<% end -%>
+.byebug_history
diff --git a/railties/lib/rails/generators/rails/app/templates/package.json.tt b/railties/lib/rails/generators/rails/app/templates/package.json.tt
new file mode 100644
index 0000000000..7174116989
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/package.json.tt
@@ -0,0 +1,11 @@
+{
+ "name": "<%= app_name %>",
+ "private": true,
+ "dependencies": {
+ "rails-ujs": ">=5.2.1"<% unless options[:skip_turbolinks] %>,
+ "turbolinks": "5.1.1"<% end -%><% unless skip_active_storage? %>,
+ "activestorage": ">=5.2.1"<% end -%><% unless options[:skip_action_cable] %>,
+ "actioncable": ">=5.2.1"<% end %>
+ },
+ "version": "0.1.0"
+}
diff --git a/railties/lib/rails/generators/rails/app/templates/public/404.html b/railties/lib/rails/generators/rails/app/templates/public/404.html
index b612547fc2..2be3af26fc 100644
--- a/railties/lib/rails/generators/rails/app/templates/public/404.html
+++ b/railties/lib/rails/generators/rails/app/templates/public/404.html
@@ -4,7 +4,7 @@
<title>The page you were looking for doesn't exist (404)</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
- body {
+ .rails-default-error-page {
background-color: #EFEFEF;
color: #2E2F30;
text-align: center;
@@ -12,13 +12,13 @@
margin: 0;
}
- div.dialog {
+ .rails-default-error-page div.dialog {
width: 95%;
max-width: 33em;
margin: 4em auto 0;
}
- div.dialog > div {
+ .rails-default-error-page div.dialog > div {
border: 1px solid #CCC;
border-right-color: #999;
border-left-color: #999;
@@ -31,13 +31,13 @@
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
}
- h1 {
+ .rails-default-error-page h1 {
font-size: 100%;
color: #730E15;
line-height: 1.5em;
}
- div.dialog > p {
+ .rails-default-error-page div.dialog > p {
margin: 0 0 1em;
padding: 1em;
background-color: #F7F7F7;
@@ -54,7 +54,7 @@
</style>
</head>
-<body>
+<body class="rails-default-error-page">
<!-- This file lives in public/404.html -->
<div class="dialog">
<div>
diff --git a/railties/lib/rails/generators/rails/app/templates/public/422.html b/railties/lib/rails/generators/rails/app/templates/public/422.html
index a21f82b3bd..c08eac0d1d 100644
--- a/railties/lib/rails/generators/rails/app/templates/public/422.html
+++ b/railties/lib/rails/generators/rails/app/templates/public/422.html
@@ -4,7 +4,7 @@
<title>The change you wanted was rejected (422)</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
- body {
+ .rails-default-error-page {
background-color: #EFEFEF;
color: #2E2F30;
text-align: center;
@@ -12,13 +12,13 @@
margin: 0;
}
- div.dialog {
+ .rails-default-error-page div.dialog {
width: 95%;
max-width: 33em;
margin: 4em auto 0;
}
- div.dialog > div {
+ .rails-default-error-page div.dialog > div {
border: 1px solid #CCC;
border-right-color: #999;
border-left-color: #999;
@@ -31,13 +31,13 @@
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
}
- h1 {
+ .rails-default-error-page h1 {
font-size: 100%;
color: #730E15;
line-height: 1.5em;
}
- div.dialog > p {
+ .rails-default-error-page div.dialog > p {
margin: 0 0 1em;
padding: 1em;
background-color: #F7F7F7;
@@ -54,7 +54,7 @@
</style>
</head>
-<body>
+<body class="rails-default-error-page">
<!-- This file lives in public/422.html -->
<div class="dialog">
<div>
diff --git a/railties/lib/rails/generators/rails/app/templates/public/500.html b/railties/lib/rails/generators/rails/app/templates/public/500.html
index 061abc587d..78a030af22 100644
--- a/railties/lib/rails/generators/rails/app/templates/public/500.html
+++ b/railties/lib/rails/generators/rails/app/templates/public/500.html
@@ -4,7 +4,7 @@
<title>We're sorry, but something went wrong (500)</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
- body {
+ .rails-default-error-page {
background-color: #EFEFEF;
color: #2E2F30;
text-align: center;
@@ -12,13 +12,13 @@
margin: 0;
}
- div.dialog {
+ .rails-default-error-page div.dialog {
width: 95%;
max-width: 33em;
margin: 4em auto 0;
}
- div.dialog > div {
+ .rails-default-error-page div.dialog > div {
border: 1px solid #CCC;
border-right-color: #999;
border-left-color: #999;
@@ -31,13 +31,13 @@
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
}
- h1 {
+ .rails-default-error-page h1 {
font-size: 100%;
color: #730E15;
line-height: 1.5em;
}
- div.dialog > p {
+ .rails-default-error-page div.dialog > p {
margin: 0 0 1em;
padding: 1em;
background-color: #F7F7F7;
@@ -54,7 +54,7 @@
</style>
</head>
-<body>
+<body class="rails-default-error-page">
<!-- This file lives in public/500.html -->
<div class="dialog">
<div>
diff --git a/railties/lib/rails/generators/rails/app/templates/public/apple-touch-icon-precomposed.png b/railties/lib/rails/generators/rails/app/templates/public/apple-touch-icon-precomposed.png
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/public/apple-touch-icon-precomposed.png
diff --git a/railties/lib/rails/generators/rails/app/templates/public/apple-touch-icon.png b/railties/lib/rails/generators/rails/app/templates/public/apple-touch-icon.png
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/public/apple-touch-icon.png
diff --git a/railties/lib/rails/generators/rails/app/templates/public/robots.txt b/railties/lib/rails/generators/rails/app/templates/public/robots.txt
index 3c9c7c01f3..37b576a4a0 100644
--- a/railties/lib/rails/generators/rails/app/templates/public/robots.txt
+++ b/railties/lib/rails/generators/rails/app/templates/public/robots.txt
@@ -1,5 +1 @@
# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
-#
-# To ban all spiders from the entire site uncomment the next two lines:
-# User-agent: *
-# Disallow: /
diff --git a/railties/lib/rails/generators/rails/app/templates/ruby-version.tt b/railties/lib/rails/generators/rails/app/templates/ruby-version.tt
new file mode 100644
index 0000000000..bac1339923
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/ruby-version.tt
@@ -0,0 +1 @@
+<%= ENV["RBENV_VERSION"] || ENV["rvm_ruby_string"] || "#{RUBY_ENGINE}-#{RUBY_ENGINE_VERSION}" -%>
diff --git a/railties/lib/rails/generators/rails/app/templates/test/application_system_test_case.rb.tt b/railties/lib/rails/generators/rails/app/templates/test/application_system_test_case.rb.tt
new file mode 100644
index 0000000000..d19212abd5
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/test/application_system_test_case.rb.tt
@@ -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/app/templates/test/test_helper.rb b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb
deleted file mode 100644
index 87b8fe3516..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-ENV['RAILS_ENV'] ||= 'test'
-require File.expand_path('../../config/environment', __FILE__)
-require 'rails/test_help'
-
-class ActiveSupport::TestCase
-<% unless options[:skip_active_record] -%>
- # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
- fixtures :all
-
-<% end -%>
- # Add more helper methods to be used by all tests here...
-end
diff --git a/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb.tt b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb.tt
new file mode 100644
index 0000000000..c06cd525d7
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb.tt
@@ -0,0 +1,19 @@
+ENV['RAILS_ENV'] ||= 'test'
+require_relative '../config/environment'
+require 'rails/test_help'
+
+class ActiveSupport::TestCase
+ # Run tests in parallel with specified workers
+<% if defined?(JRUBY_VERSION) || Gem.win_platform? -%>
+ parallelize(workers: 2, with: :threads)
+<%- else -%>
+ parallelize(workers: 2)
+<% end -%>
+
+<% unless options[:skip_active_record] -%>
+ # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
+ fixtures :all
+
+<% end -%>
+ # Add more helper methods to be used by all tests here...
+end
diff --git a/railties/lib/rails/generators/rails/application_record/application_record_generator.rb b/railties/lib/rails/generators/rails/application_record/application_record_generator.rb
new file mode 100644
index 0000000000..f6b6e76b1d
--- /dev/null
+++ b/railties/lib/rails/generators/rails/application_record/application_record_generator.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Rails
+ module Generators
+ class ApplicationRecordGenerator < Base # :nodoc:
+ hook_for :orm, required: true, desc: "ORM to be invoked"
+ end
+ end
+end
diff --git a/railties/lib/rails/generators/rails/assets/USAGE b/railties/lib/rails/generators/rails/assets/USAGE
index d2e5ed4482..ee73d05808 100644
--- a/railties/lib/rails/generators/rails/assets/USAGE
+++ b/railties/lib/rails/generators/rails/assets/USAGE
@@ -5,16 +5,13 @@ Description:
To create an asset within a folder, specify the asset's name as a
path like 'parent/name'.
- This generates a JavaScript stub in app/assets/javascripts and a stylesheet
- stub in app/assets/stylesheets.
+ This generates a stylesheet stub in app/assets/stylesheets.
- If CoffeeScript is available, JavaScripts will be generated with the .coffee extension.
If Sass 3 is available, stylesheets will be generated with the .scss extension.
Example:
`rails generate assets posts`
Posts assets.
- JavaScript: app/assets/javascripts/posts.js
Stylesheet: app/assets/stylesheets/posts.css
diff --git a/railties/lib/rails/generators/rails/assets/assets_generator.rb b/railties/lib/rails/generators/rails/assets/assets_generator.rb
index 6f4b86e708..9ce8570172 100644
--- a/railties/lib/rails/generators/rails/assets/assets_generator.rb
+++ b/railties/lib/rails/generators/rails/assets/assets_generator.rb
@@ -1,25 +1,19 @@
+# frozen_string_literal: true
+
module Rails
module Generators
class AssetsGenerator < NamedBase # :nodoc:
- class_option :javascripts, type: :boolean, desc: "Generate JavaScripts"
class_option :stylesheets, type: :boolean, desc: "Generate Stylesheets"
-
- class_option :javascript_engine, desc: "Engine for JavaScripts"
class_option :stylesheet_engine, desc: "Engine for Stylesheets"
- protected
-
- def asset_name
- file_name
- end
-
- hook_for :javascript_engine do |javascript_engine|
- invoke javascript_engine, [name] if options[:javascripts]
- end
+ private
+ def asset_name
+ file_name
+ end
- hook_for :stylesheet_engine do |stylesheet_engine|
- invoke stylesheet_engine, [name] if options[:stylesheets]
- end
+ hook_for :stylesheet_engine do |stylesheet_engine|
+ invoke stylesheet_engine, [name] if options[:stylesheets]
+ end
end
end
end
diff --git a/railties/lib/rails/generators/rails/assets/templates/javascript.js b/railties/lib/rails/generators/rails/assets/templates/javascript.js
deleted file mode 100644
index dee720facd..0000000000
--- a/railties/lib/rails/generators/rails/assets/templates/javascript.js
+++ /dev/null
@@ -1,2 +0,0 @@
-// Place all the behaviors and hooks related to the matching controller here.
-// All this logic will automatically be available in application.js.
diff --git a/railties/lib/rails/generators/rails/assets/templates/stylesheet.css b/railties/lib/rails/generators/rails/assets/templates/stylesheet.css
index 7594abf268..afad32db02 100644
--- a/railties/lib/rails/generators/rails/assets/templates/stylesheet.css
+++ b/railties/lib/rails/generators/rails/assets/templates/stylesheet.css
@@ -1,4 +1,4 @@
-/*
+/*
Place all the styles related to the matching controller here.
They will automatically be included in application.css.
*/
diff --git a/railties/lib/rails/generators/rails/controller/controller_generator.rb b/railties/lib/rails/generators/rails/controller/controller_generator.rb
index 0a4c509a31..eb75e7e661 100644
--- a/railties/lib/rails/generators/rails/controller/controller_generator.rb
+++ b/railties/lib/rails/generators/rails/controller/controller_generator.rb
@@ -1,59 +1,76 @@
+# frozen_string_literal: true
+
module Rails
module Generators
class ControllerGenerator < NamedBase # :nodoc:
argument :actions, type: :array, default: [], banner: "action action"
class_option :skip_routes, type: :boolean, desc: "Don't add routes to config/routes.rb."
+ class_option :helper, type: :boolean
+ class_option :assets, type: :boolean
check_class_collision suffix: "Controller"
def create_controller_files
- template 'controller.rb', File.join('app/controllers', class_path, "#{file_name}_controller.rb")
+ template "controller.rb", File.join("app/controllers", class_path, "#{file_name}_controller.rb")
end
def add_routes
- 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]
- end
- end
+ return if options[:skip_routes]
+ return if actions.empty?
+ route generate_routing_code
end
- hook_for :template_engine, :test_framework
- hook_for :helper, :assets, hide: true
+ hook_for :template_engine, :test_framework, :helper, :assets do |generator|
+ invoke generator, [ remove_possible_suffix(name), actions ]
+ end
private
+ def file_name
+ @_file_name ||= remove_possible_suffix(super)
+ end
+
+ def remove_possible_suffix(name)
+ name.sub(/_?controller$/i, "")
+ end
+
# This method creates nested route entry for namespaced resources.
- # For eg. rails g controller foo/bar/baz index
+ # For eg. rails g controller foo/bar/baz index show
# Will generate -
# namespace :foo do
# namespace :bar do
# get 'baz/index'
+ # get 'baz/show'
# end
# end
- def generate_routing_code(action)
- depth = regular_class_path.length
+ def generate_routing_code
+ 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)
+ # get 'baz/show'
+ actions.each do |action|
+ lines << indent(%{get '#{file_name}/#{action}'\n}, depth * 2)
+ end
# 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/controller/templates/controller.rb b/railties/lib/rails/generators/rails/controller/templates/controller.rb.tt
index 633e0b3177..633e0b3177 100644
--- a/railties/lib/rails/generators/rails/controller/templates/controller.rb
+++ b/railties/lib/rails/generators/rails/controller/templates/controller.rb.tt
diff --git a/railties/lib/rails/generators/rails/credentials/credentials_generator.rb b/railties/lib/rails/generators/rails/credentials/credentials_generator.rb
new file mode 100644
index 0000000000..99b935aa6a
--- /dev/null
+++ b/railties/lib/rails/generators/rails/credentials/credentials_generator.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require "rails/generators/base"
+require "rails/generators/rails/master_key/master_key_generator"
+require "active_support/encrypted_configuration"
+
+module Rails
+ module Generators
+ class CredentialsGenerator < Base # :nodoc:
+ def add_credentials_file
+ unless credentials.content_path.exist?
+ template = credentials_template
+
+ say "Adding #{credentials.content_path} to store encrypted credentials."
+ say ""
+ say "The following content has been encrypted with the Rails master key:"
+ say ""
+ say template, :on_green
+ say ""
+
+ add_credentials_file_silently(template)
+
+ say "You can edit encrypted credentials with `rails credentials:edit`."
+ say ""
+ end
+ end
+
+ def add_credentials_file_silently(template = nil)
+ unless credentials.content_path.exist?
+ credentials.write(credentials_template)
+ end
+ end
+
+ private
+ def credentials
+ ActiveSupport::EncryptedConfiguration.new(
+ config_path: "config/credentials.yml.enc",
+ key_path: "config/master.key",
+ env_key: "RAILS_MASTER_KEY",
+ raise_if_missing_key: true
+ )
+ end
+
+ def credentials_template
+ <<~YAML
+ # aws:
+ # access_key_id: 123
+ # secret_access_key: 345
+
+ # Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.
+ secret_key_base: #{SecureRandom.hex(64)}
+ YAML
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/generators/rails/encrypted_file/encrypted_file_generator.rb b/railties/lib/rails/generators/rails/encrypted_file/encrypted_file_generator.rb
new file mode 100644
index 0000000000..867e28c6db
--- /dev/null
+++ b/railties/lib/rails/generators/rails/encrypted_file/encrypted_file_generator.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require "rails/generators/base"
+require "active_support/encrypted_file"
+
+module Rails
+ module Generators
+ class EncryptedFileGenerator < Base # :nodoc:
+ def add_encrypted_file_silently(file_path, key_path, template = encrypted_file_template)
+ unless File.exist?(file_path)
+ setup = { content_path: file_path, key_path: key_path, env_key: "RAILS_MASTER_KEY", raise_if_missing_key: true }
+ ActiveSupport::EncryptedFile.new(setup).write(template)
+ end
+ end
+
+ private
+ def encrypted_file_template
+ <<~YAML
+ # aws:
+ # access_key_id: 123
+ # secret_access_key: 345
+
+ YAML
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/generators/rails/encryption_key_file/encryption_key_file_generator.rb b/railties/lib/rails/generators/rails/encryption_key_file/encryption_key_file_generator.rb
new file mode 100644
index 0000000000..e2359e9ded
--- /dev/null
+++ b/railties/lib/rails/generators/rails/encryption_key_file/encryption_key_file_generator.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require "pathname"
+require "rails/generators/base"
+require "active_support/encrypted_file"
+
+module Rails
+ module Generators
+ class EncryptionKeyFileGenerator < Base # :nodoc:
+ def add_key_file(key_path)
+ key_path = Pathname.new(key_path)
+
+ unless key_path.exist?
+ key = ActiveSupport::EncryptedFile.generate_key
+
+ log "Adding #{key_path} to store the encryption key: #{key}"
+ log ""
+ log "Save this in a password manager your team can access."
+ log ""
+ log "If you lose the key, no one, including you, can access anything encrypted with it."
+
+ log ""
+ add_key_file_silently(key_path, key)
+ log ""
+ end
+ end
+
+ def add_key_file_silently(key_path, key = nil)
+ create_file key_path, key || ActiveSupport::EncryptedFile.generate_key
+ key_path.chmod 0600
+ end
+
+ def ignore_key_file(key_path, ignore: key_ignore(key_path))
+ if File.exist?(".gitignore")
+ unless File.read(".gitignore").include?(ignore)
+ log "Ignoring #{key_path} so it won't end up in Git history:"
+ log ""
+ append_to_file ".gitignore", ignore
+ log ""
+ end
+ else
+ log "IMPORTANT: Don't commit #{key_path}. Add this to your ignore file:"
+ log ignore, :on_green
+ log ""
+ end
+ end
+
+ def ignore_key_file_silently(key_path, ignore: key_ignore(key_path))
+ append_to_file ".gitignore", ignore if File.exist?(".gitignore")
+ end
+
+ private
+ def key_ignore(key_path)
+ [ "", "/#{key_path}", "" ].join("\n")
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/generators/rails/generator/generator_generator.rb b/railties/lib/rails/generators/rails/generator/generator_generator.rb
index 15d88f06ac..747acd68d1 100644
--- a/railties/lib/rails/generators/rails/generator/generator_generator.rb
+++ b/railties/lib/rails/generators/rails/generator/generator_generator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Rails
module Generators
class GeneratorGenerator < NamedBase # :nodoc:
@@ -7,12 +9,12 @@ module Rails
desc: "Namespace generator under lib/generators/name"
def create_generator_files
- directory '.', generator_dir
+ directory ".", generator_dir
end
hook_for :test_framework
- protected
+ private
def generator_dir
if options[:namespace]
@@ -21,7 +23,6 @@ module Rails
File.join("lib", "generators", regular_class_path)
end
end
-
end
end
end
diff --git a/railties/lib/rails/generators/rails/generator/templates/%file_name%_generator.rb.tt b/railties/lib/rails/generators/rails/generator/templates/%file_name%_generator.rb.tt
index d0575772bc..178d5c3f9f 100644
--- a/railties/lib/rails/generators/rails/generator/templates/%file_name%_generator.rb.tt
+++ b/railties/lib/rails/generators/rails/generator/templates/%file_name%_generator.rb.tt
@@ -1,3 +1,3 @@
class <%= class_name %>Generator < Rails::Generators::NamedBase
- source_root File.expand_path('../templates', __FILE__)
+ source_root File.expand_path('templates', __dir__)
end
diff --git a/railties/lib/rails/generators/rails/helper/helper_generator.rb b/railties/lib/rails/generators/rails/helper/helper_generator.rb
index 5ff38e4111..3837c10ca0 100644
--- a/railties/lib/rails/generators/rails/helper/helper_generator.rb
+++ b/railties/lib/rails/generators/rails/helper/helper_generator.rb
@@ -1,10 +1,12 @@
+# frozen_string_literal: true
+
module Rails
module Generators
class HelperGenerator < NamedBase # :nodoc:
check_class_collision suffix: "Helper"
def create_helper_files
- template 'helper.rb', File.join('app/helpers', class_path, "#{file_name}_helper.rb")
+ template "helper.rb", File.join("app/helpers", class_path, "#{file_name}_helper.rb")
end
hook_for :test_framework
diff --git a/railties/lib/rails/generators/rails/helper/templates/helper.rb b/railties/lib/rails/generators/rails/helper/templates/helper.rb.tt
index b4173151b4..b4173151b4 100644
--- a/railties/lib/rails/generators/rails/helper/templates/helper.rb
+++ b/railties/lib/rails/generators/rails/helper/templates/helper.rb.tt
diff --git a/railties/lib/rails/generators/rails/integration_test/integration_test_generator.rb b/railties/lib/rails/generators/rails/integration_test/integration_test_generator.rb
index 70770ddcb8..975dd8b90c 100644
--- a/railties/lib/rails/generators/rails/integration_test/integration_test_generator.rb
+++ b/railties/lib/rails/generators/rails/integration_test/integration_test_generator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Rails
module Generators
class IntegrationTestGenerator < NamedBase # :nodoc:
diff --git a/railties/lib/rails/generators/rails/master_key/master_key_generator.rb b/railties/lib/rails/generators/rails/master_key/master_key_generator.rb
new file mode 100644
index 0000000000..21664ea86d
--- /dev/null
+++ b/railties/lib/rails/generators/rails/master_key/master_key_generator.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require "pathname"
+require "rails/generators/base"
+require "rails/generators/rails/encryption_key_file/encryption_key_file_generator"
+require "active_support/encrypted_file"
+
+module Rails
+ module Generators
+ class MasterKeyGenerator < Base # :nodoc:
+ MASTER_KEY_PATH = Pathname.new("config/master.key")
+
+ def add_master_key_file
+ unless MASTER_KEY_PATH.exist?
+ key = ActiveSupport::EncryptedFile.generate_key
+
+ log "Adding #{MASTER_KEY_PATH} to store the master encryption key: #{key}"
+ log ""
+ log "Save this in a password manager your team can access."
+ log ""
+ log "If you lose the key, no one, including you, can access anything encrypted with it."
+
+ log ""
+ add_master_key_file_silently(key)
+ log ""
+ end
+ end
+
+ def add_master_key_file_silently(key = nil)
+ unless MASTER_KEY_PATH.exist?
+ key_file_generator.add_key_file_silently(MASTER_KEY_PATH, key)
+ end
+ end
+
+ def ignore_master_key_file
+ key_file_generator.ignore_key_file(MASTER_KEY_PATH, ignore: key_ignore)
+ end
+
+ def ignore_master_key_file_silently
+ key_file_generator.ignore_key_file_silently(MASTER_KEY_PATH, ignore: key_ignore)
+ end
+
+ private
+ def key_file_generator
+ EncryptionKeyFileGenerator.new([], options)
+ end
+
+ def key_ignore
+ [ "", "# Ignore master key for decrypting credentials and more.", "/#{MASTER_KEY_PATH}", "" ].join("\n")
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/generators/rails/migration/migration_generator.rb b/railties/lib/rails/generators/rails/migration/migration_generator.rb
index fca2a8fef4..c331c135e3 100644
--- a/railties/lib/rails/generators/rails/migration/migration_generator.rb
+++ b/railties/lib/rails/generators/rails/migration/migration_generator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Rails
module Generators
class MigrationGenerator < NamedBase # :nodoc:
diff --git a/railties/lib/rails/generators/rails/model/USAGE b/railties/lib/rails/generators/rails/model/USAGE
index 11daa5c3cb..025bcf4774 100644
--- a/railties/lib/rails/generators/rails/model/USAGE
+++ b/railties/lib/rails/generators/rails/model/USAGE
@@ -8,14 +8,14 @@ Description:
As a special case, specifying 'password:digest' will generate a
password_digest field of string type, and configure your generated model and
- tests for use with ActiveModel has_secure_password (assuming the default ORM
+ tests for use with Active Model has_secure_password (assuming the default ORM
and test framework are being used).
You don't have to think up every attribute up front, but it helps to
sketch out a few so you can start working with the model immediately.
This generator invokes your configured ORM and test framework, which
- defaults to ActiveRecord and TestUnit.
+ defaults to Active Record and TestUnit.
Finally, if --parent option is given, it's used as superclass of the
created model. This allows you create Single Table Inheritance models.
@@ -91,7 +91,7 @@ Available field types:
Examples:
`rails generate model account`
- For ActiveRecord and TestUnit it creates:
+ For Active Record and TestUnit it creates:
Model: app/models/account.rb
Test: test/models/account_test.rb
@@ -104,7 +104,7 @@ Examples:
`rails generate model admin/account`
- For ActiveRecord and TestUnit it creates:
+ For Active Record and TestUnit it creates:
Module: app/models/admin.rb
Model: app/models/admin/account.rb
diff --git a/railties/lib/rails/generators/rails/model/model_generator.rb b/railties/lib/rails/generators/rails/model/model_generator.rb
index ec78fd855d..de4de2cae2 100644
--- a/railties/lib/rails/generators/rails/model/model_generator.rb
+++ b/railties/lib/rails/generators/rails/model/model_generator.rb
@@ -1,4 +1,6 @@
-require 'rails/generators/model_helpers'
+# frozen_string_literal: true
+
+require "rails/generators/model_helpers"
module Rails
module Generators
diff --git a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb
index 66111004aa..239b3a5739 100644
--- a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb
+++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb
@@ -1,6 +1,7 @@
-require 'active_support/core_ext/hash/slice'
+# frozen_string_literal: true
+
require "rails/generators/rails/app/app_generator"
-require 'date'
+require "date"
module Rails
# The plugin builder allows you to override elements of the plugin
@@ -17,20 +18,27 @@ module Rails
def app
if mountable?
- directory 'app'
- empty_directory_with_keep_file "app/assets/images/#{namespaced_name}"
+ if api?
+ directory "app", exclude_pattern: %r{app/(views|helpers)}
+ else
+ directory "app"
+ empty_directory_with_keep_file "app/assets/images/#{namespaced_name}"
+ end
elsif full?
- empty_directory_with_keep_file 'app/models'
- empty_directory_with_keep_file 'app/controllers'
- empty_directory_with_keep_file 'app/views'
- empty_directory_with_keep_file 'app/helpers'
- empty_directory_with_keep_file 'app/mailers'
- empty_directory_with_keep_file "app/assets/images/#{namespaced_name}"
+ empty_directory_with_keep_file "app/models"
+ empty_directory_with_keep_file "app/controllers"
+ empty_directory_with_keep_file "app/mailers"
+
+ unless api?
+ empty_directory_with_keep_file "app/assets/images/#{namespaced_name}"
+ empty_directory_with_keep_file "app/helpers"
+ empty_directory_with_keep_file "app/views"
+ end
end
end
def readme
- template "README.rdoc"
+ template "README.md"
end
def gemfile
@@ -53,7 +61,12 @@ module Rails
template "lib/%namespaced_name%.rb"
template "lib/tasks/%namespaced_name%_tasks.rake"
template "lib/%namespaced_name%/version.rb"
- template "lib/%namespaced_name%/engine.rb" if engine?
+
+ if engine?
+ template "lib/%namespaced_name%/engine.rb"
+ else
+ template "lib/%namespaced_name%/railtie.rb"
+ end
end
def config
@@ -64,8 +77,8 @@ module Rails
template "test/test_helper.rb"
template "test/%namespaced_name%_test.rb"
append_file "Rakefile", <<-EOF
-#{rakefile_test_tasks}
+#{rakefile_test_tasks}
task default: :test
EOF
if engine?
@@ -74,14 +87,19 @@ task default: :test
end
PASSTHROUGH_OPTIONS = [
- :skip_active_record, :skip_action_mailer, :skip_javascript, :database,
- :javascript, :quiet, :pretend, :force, :skip
+ :skip_active_record, :skip_active_storage, :skip_action_mailer, :skip_javascript, :skip_action_cable, :skip_sprockets, :database,
+ :api, :quiet, :pretend, :skip
]
def generate_test_dummy(force = false)
- opts = (options || {}).slice(*PASSTHROUGH_OPTIONS)
+ opts = (options.dup || {}).keep_if { |k, _| PASSTHROUGH_OPTIONS.map(&:to_s).include?(k) }
opts[:force] = force
opts[:skip_bundle] = true
+ opts[:skip_listen] = true
+ opts[:skip_git] = true
+ opts[:skip_turbolinks] = true
+ opts[:skip_webpack_install] = true
+ opts[:dummy_app] = true
invoke Rails::Generators::AppGenerator,
[ File.expand_path(dummy_path, destination_root) ], opts
@@ -96,24 +114,27 @@ task default: :test
end
def test_dummy_assets
- template "rails/javascripts.js", "#{dummy_path}/app/assets/javascripts/application.js", force: true
- template "rails/stylesheets.css", "#{dummy_path}/app/assets/stylesheets/application.css", force: true
+ template "rails/javascripts.js", "#{dummy_path}/app/javascript/packs/application.js", force: true
+ template "rails/stylesheets.css", "#{dummy_path}/app/assets/stylesheets/application.css", force: true
+ template "rails/dummy_manifest.js", "#{dummy_path}/app/assets/config/manifest.js", force: true
end
def test_dummy_clean
inside dummy_path do
- remove_file ".gitignore"
remove_file "db/seeds.rb"
- remove_file "doc"
remove_file "Gemfile"
remove_file "lib/tasks"
remove_file "public/robots.txt"
- remove_file "README"
+ remove_file "README.md"
remove_file "test"
remove_file "vendor"
end
end
+ def assets_manifest
+ template "rails/engine_manifest.js", "app/assets/config/#{underscored_name}_manifest.js"
+ end
+
def stylesheets
if mountable?
copy_file "rails/stylesheets.css",
@@ -135,9 +156,8 @@ task default: :test
end
def bin(force = false)
- return unless engine?
-
- directory "bin", force: force do |content|
+ bin_file = engine? ? "bin/rails.tt" : "bin/test.tt"
+ template bin_file, force: force do |content|
"#{shebang}\n" + content
end
chmod "bin", 0755, verbose: false
@@ -148,7 +168,7 @@ task default: :test
gemfile_in_app_path = File.join(rails_app_path, "Gemfile")
if File.exist? gemfile_in_app_path
- entry = "gem '#{name}', path: '#{relative_path}'"
+ entry = "\ngem '#{name}', path: '#{relative_path}'"
append_file gemfile_in_app_path, entry
end
end
@@ -173,16 +193,15 @@ 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,
+ desc: "Generate a smaller stack for API application plugins"
+
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!
@@ -209,16 +228,16 @@ task default: :test
build(:lib)
end
- def create_public_stylesheets_files
- build(:stylesheets)
+ def create_assets_manifest_file
+ build(:assets_manifest) if !api? && engine?
end
- def create_javascript_files
- build(:javascripts)
+ def create_public_stylesheets_files
+ build(:stylesheets) unless api?
end
- def create_images_directory
- build(:images)
+ def create_javascript_files
+ build(:javascripts) unless api?
end
def create_bin_files
@@ -242,14 +261,24 @@ task default: :test
build(:leftovers)
end
- public_task :apply_rails_template, :run_bundle
+ public_task :apply_rails_template
+
+ def run_after_bundle_callbacks
+ unless @after_bundle_callbacks.empty?
+ ActiveSupport::Deprecation.warn("`after_bundle` is deprecated and will be removed in the next version of Rails. ")
+ end
+
+ @after_bundle_callbacks.each do |callback|
+ callback.call
+ end
+ end
def name
@name ||= begin
# same as ActiveSupport::Inflector#underscore except not replacing '-'
underscored = original_name.dup
- underscored.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
- underscored.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
+ underscored.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
+ underscored.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
underscored.downcase!
underscored
@@ -261,14 +290,10 @@ task default: :test
end
def namespaced_name
- @namespaced_name ||= name.gsub('-', '/')
+ @namespaced_name ||= name.tr("-", "/")
end
- protected
-
- def app_templates_dir
- "../../app/templates"
- end
+ private
def create_dummy_app(path = nil)
dummy_path(path) if path
@@ -286,7 +311,7 @@ task default: :test
end
def engine?
- full? || mountable?
+ full? || mountable? || options[:engine]
end
def full?
@@ -302,11 +327,15 @@ task default: :test
end
def with_dummy_app?
- options[:skip_test].blank? || options[:dummy_path] != 'test/dummy'
+ options[:skip_test].blank? || options[:dummy_path] != "test/dummy"
+ end
+
+ def api?
+ options[:api]
end
def self.banner
- "rails plugin new #{self.arguments.map(&:usage).join(' ')} [options]"
+ "rails plugin new #{arguments.map(&:usage).join(' ')} [options]"
end
def original_name
@@ -318,7 +347,7 @@ task default: :test
end
def wrap_in_modules(unwrapped_code)
- unwrapped_code = "#{unwrapped_code}".strip.gsub(/\W$\n/, '')
+ unwrapped_code = "#{unwrapped_code}".strip.gsub(/\s$\n/, "")
modules.reverse.inject(unwrapped_code) do |content, mod|
str = "module #{mod}\n"
str += content.lines.map { |line| " #{line}" }.join
@@ -335,7 +364,7 @@ task default: :test
end
def camelized
- @camelized ||= name.gsub(/\W/, '_').squeeze('_').camelize
+ @camelized ||= name.gsub(/\W/, "_").squeeze("_").camelize
end
def author
@@ -357,11 +386,11 @@ task default: :test
end
def valid_const?
- if original_name =~ /-\d/
+ if /-\d/.match?(original_name)
raise Error, "Invalid plugin name #{original_name}. Please give a name which does not contain a namespace starting with numeric characters."
- elsif original_name =~ /[^\w-]+/
+ elsif /[^\w-]+/.match?(original_name)
raise Error, "Invalid plugin name #{original_name}. Please give a name which uses only alphabetic, numeric, \"_\" or \"-\" characters."
- elsif camelized =~ /^\d/
+ elsif /^\d/.match?(camelized)
raise Error, "Invalid plugin name #{original_name}. Please give a name which does not start with numbers."
elsif RESERVED_NAMES.include?(name)
raise Error, "Invalid plugin name #{original_name}. Please give a " \
@@ -393,7 +422,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
@@ -415,12 +443,12 @@ 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
return unless inside_application?
- app_path.sub(/^#{rails_app_path}\//, '')
+ app_path.sub(/^#{rails_app_path}\//, "")
end
end
end
diff --git a/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec b/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec
deleted file mode 100644
index f8ece4fe73..0000000000
--- a/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec
+++ /dev/null
@@ -1,27 +0,0 @@
-$:.push File.expand_path("../lib", __FILE__)
-
-# Maintain your gem's version:
-require "<%= namespaced_name %>/version"
-
-# Describe your gem and declare its dependencies:
-Gem::Specification.new do |s|
- s.name = "<%= name %>"
- s.version = <%= camelized_modules %>::VERSION
- s.authors = ["<%= author %>"]
- s.email = ["<%= email %>"]
- s.homepage = "TODO"
- s.summary = "TODO: Summary of <%= camelized_modules %>."
- s.description = "TODO: Description of <%= camelized_modules %>."
- s.license = "MIT"
-
- s.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.rdoc"]
-<% unless options.skip_test? -%>
- s.test_files = Dir["test/**/*"]
-<% end -%>
-
- <%= '# ' if options.dev? || options.edge? -%>s.add_dependency "rails", "~> <%= Rails::VERSION::STRING %>"
-<% unless options[:skip_active_record] -%>
-
- s.add_development_dependency "<%= gem_for_database %>"
-<% end -%>
-end
diff --git a/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec.tt b/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec.tt
new file mode 100644
index 0000000000..405642c850
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec.tt
@@ -0,0 +1,33 @@
+$:.push File.expand_path("lib", __dir__)
+
+# Maintain your gem's version:
+require "<%= namespaced_name %>/version"
+
+# Describe your gem and declare its dependencies:
+Gem::Specification.new do |spec|
+ spec.name = "<%= name %>"
+ spec.version = <%= camelized_modules %>::VERSION
+ spec.authors = ["<%= author %>"]
+ spec.email = ["<%= email %>"]
+ spec.homepage = "TODO"
+ spec.summary = "TODO: Summary of <%= camelized_modules %>."
+ spec.description = "TODO: Description of <%= camelized_modules %>."
+ spec.license = "MIT"
+
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
+ if spec.respond_to?(:metadata)
+ spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
+ else
+ raise "RubyGems 2.0 or newer is required to protect against " \
+ "public gem pushes."
+ end
+
+ spec.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"]
+
+ <%= '# ' if options.dev? || options.edge? -%>spec.add_dependency "rails", "<%= Array(rails_version_specifier).join('", "') %>"
+<% unless options[:skip_active_record] -%>
+
+ spec.add_development_dependency "<%= gem_for_database[0] %>"
+<% end -%>
+end
diff --git a/railties/lib/rails/generators/rails/plugin/templates/Gemfile b/railties/lib/rails/generators/rails/plugin/templates/Gemfile
deleted file mode 100644
index 2c91c6a0ea..0000000000
--- a/railties/lib/rails/generators/rails/plugin/templates/Gemfile
+++ /dev/null
@@ -1,47 +0,0 @@
-source 'https://rubygems.org'
-
-<% if options[:skip_gemspec] -%>
-<%= '# ' if options.dev? || options.edge? -%>gem 'rails', '~> <%= Rails::VERSION::STRING %>'
-<% else -%>
-# Declare your gem's dependencies in <%= name %>.gemspec.
-# Bundler will treat runtime dependencies like base dependencies, and
-# development dependencies will be added by default to the :development group.
-gemspec
-<% end -%>
-
-<% if options[:skip_gemspec] -%>
-group :development do
- gem '<%= gem_for_database %>'
-end
-<% else -%>
-# Declare any dependencies that are still in development here instead of in
-# your gemspec. These might include edge Rails or gems from your path or
-# Git. Remember to move these dependencies to your gemspec before releasing
-# your gem to rubygems.org.
-<% end -%>
-
-<% if options.dev? || options.edge? -%>
-# Your gem is dependent on dev or edge Rails. Once you can lock this
-# dependency down to a specific version, move it to your gemspec.
-<% max_width = gemfile_entries.map { |g| g.name.length }.max -%>
-<% gemfile_entries.each do |gem| -%>
-<% if gem.comment -%>
-
-# <%= gem.comment %>
-<% end -%>
-<%= gem.commented_out ? '# ' : '' %>gem '<%= gem.name %>'<%= %(, '#{gem.version}') if gem.version -%>
-<% if gem.options.any? -%>
-, <%= gem.options.map { |k,v|
- "#{k}: #{v.inspect}" }.join(', ') %>
-<% end -%>
-<% end -%>
-
-<% end -%>
-<% if RUBY_ENGINE == 'ruby' -%>
-# To use a debugger
-# gem 'byebug', group: [:development, :test]
-<% end -%>
-<% if RUBY_PLATFORM.match(/bccwin|cygwin|emx|mingw|mswin|wince|java/) -%>
-
-gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
-<% end -%>
diff --git a/railties/lib/rails/generators/rails/plugin/templates/Gemfile.tt b/railties/lib/rails/generators/rails/plugin/templates/Gemfile.tt
new file mode 100644
index 0000000000..290259b4db
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/Gemfile.tt
@@ -0,0 +1,48 @@
+source 'https://rubygems.org'
+git_source(:github) { |repo| "https://github.com/#{repo}.git" }
+
+<% if options[:skip_gemspec] -%>
+<%= '# ' if options.dev? || options.edge? -%>gem 'rails', '<%= Array(rails_version_specifier).join("', '") %>'
+<% else -%>
+# Declare your gem's dependencies in <%= name %>.gemspec.
+# Bundler will treat runtime dependencies like base dependencies, and
+# development dependencies will be added by default to the :development group.
+gemspec
+<% end -%>
+
+<% if options[:skip_gemspec] -%>
+group :development do
+ gem '<%= gem_for_database[0] %>'
+end
+<% else -%>
+# Declare any dependencies that are still in development here instead of in
+# your gemspec. These might include edge Rails or gems from your path or
+# Git. Remember to move these dependencies to your gemspec before releasing
+# your gem to rubygems.org.
+<% end -%>
+
+<% if options.dev? || options.edge? -%>
+# Your gem is dependent on dev or edge Rails. Once you can lock this
+# dependency down to a specific version, move it to your gemspec.
+<% max_width = gemfile_entries.map { |g| g.name.length }.max -%>
+<% gemfile_entries.each do |gem| -%>
+<% if gem.comment -%>
+
+# <%= gem.comment %>
+<% end -%>
+<%= gem.commented_out ? '# ' : '' %>gem '<%= gem.name %>'<%= %(, '#{gem.version}') if gem.version -%>
+<% if gem.options.any? -%>
+, <%= gem.options.map { |k,v|
+ "#{k}: #{v.inspect}" }.join(', ') %>
+<% end -%>
+<% end -%>
+
+<% end -%>
+<% if RUBY_ENGINE == 'ruby' -%>
+# To use a debugger
+# gem 'byebug', group: [:development, :test]
+<% end -%>
+<% if RUBY_PLATFORM.match(/bccwin|cygwin|emx|mingw|mswin|wince|java/) -%>
+
+gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
+<% end -%>
diff --git a/railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE b/railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE.tt
index ff2fb3ba4e..ff2fb3ba4e 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE
+++ b/railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE.tt
diff --git a/railties/lib/rails/generators/rails/plugin/templates/README.md.tt b/railties/lib/rails/generators/rails/plugin/templates/README.md.tt
new file mode 100644
index 0000000000..1632409bea
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/README.md.tt
@@ -0,0 +1,28 @@
+# <%= camelized_modules %>
+Short description and motivation.
+
+## Usage
+How to use my plugin.
+
+## Installation
+Add this line to your application's Gemfile:
+
+```ruby
+gem '<%= name %>'
+```
+
+And then execute:
+```bash
+$ bundle
+```
+
+Or install it yourself as:
+```bash
+$ gem install <%= name %>
+```
+
+## Contributing
+Contribution directions go here.
+
+## License
+The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
diff --git a/railties/lib/rails/generators/rails/plugin/templates/README.rdoc b/railties/lib/rails/generators/rails/plugin/templates/README.rdoc
deleted file mode 100644
index 25983ca5da..0000000000
--- a/railties/lib/rails/generators/rails/plugin/templates/README.rdoc
+++ /dev/null
@@ -1,3 +0,0 @@
-= <%= camelized_modules %>
-
-This project rocks and uses MIT-LICENSE. \ No newline at end of file
diff --git a/railties/lib/rails/generators/rails/plugin/templates/Rakefile b/railties/lib/rails/generators/rails/plugin/templates/Rakefile
deleted file mode 100644
index bda55bae29..0000000000
--- a/railties/lib/rails/generators/rails/plugin/templates/Rakefile
+++ /dev/null
@@ -1,29 +0,0 @@
-begin
- require 'bundler/setup'
-rescue LoadError
- puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
-end
-
-require 'rdoc/task'
-
-RDoc::Task.new(:rdoc) do |rdoc|
- rdoc.rdoc_dir = 'rdoc'
- rdoc.title = '<%= camelized_modules %>'
- rdoc.options << '--line-numbers'
- rdoc.rdoc_files.include('README.rdoc')
- rdoc.rdoc_files.include('lib/**/*.rb')
-end
-
-<% if engine? && !options[:skip_active_record] && with_dummy_app? -%>
-APP_RAKEFILE = File.expand_path("../<%= dummy_path -%>/Rakefile", __FILE__)
-load 'rails/tasks/engine.rake'
-<% end %>
-
-<% if engine? -%>
-load 'rails/tasks/statistics.rake'
-<% end %>
-
-<% unless options[:skip_gemspec] -%>
-
-Bundler::GemHelper.install_tasks
-<% end %>
diff --git a/railties/lib/rails/generators/rails/plugin/templates/Rakefile.tt b/railties/lib/rails/generators/rails/plugin/templates/Rakefile.tt
new file mode 100644
index 0000000000..f3efe21cf1
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/Rakefile.tt
@@ -0,0 +1,28 @@
+begin
+ require 'bundler/setup'
+rescue LoadError
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
+end
+
+require 'rdoc/task'
+
+RDoc::Task.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = '<%= camelized_modules %>'
+ rdoc.options << '--line-numbers'
+ rdoc.rdoc_files.include('README.md')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
+<% if engine? && !options[:skip_active_record] && with_dummy_app? -%>
+
+APP_RAKEFILE = File.expand_path("<%= dummy_path -%>/Rakefile", __dir__)
+load 'rails/tasks/engine.rake'
+<% end -%>
+<% if engine? -%>
+
+load 'rails/tasks/statistics.rake'
+<% end -%>
+<% unless options[:skip_gemspec] -%>
+
+require 'bundler/gem_tasks'
+<% end -%>
diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt
index 7157e48c42..b86ef0f2f8 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt
@@ -1,5 +1,6 @@
-<%= wrap_in_modules <<-rb.strip_heredoc
- class ApplicationController < ActionController::Base
+<%= wrap_in_modules <<~rb
+ class ApplicationController < ActionController::#{api? ? "API" : "Base"}
+ #{ api? ? '# ' : '' }protect_from_forgery with: :exception
end
rb
%>
diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/helpers/%namespaced_name%/application_helper.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/helpers/%namespaced_name%/application_helper.rb.tt
index 25d692732d..be078f36de 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/app/helpers/%namespaced_name%/application_helper.rb.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/app/helpers/%namespaced_name%/application_helper.rb.tt
@@ -1,4 +1,4 @@
-<%= wrap_in_modules <<-rb.strip_heredoc
+<%= wrap_in_modules <<~rb
module ApplicationHelper
end
rb
diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/jobs/%namespaced_name%/application_job.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/jobs/%namespaced_name%/application_job.rb.tt
new file mode 100644
index 0000000000..846863bc13
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/app/jobs/%namespaced_name%/application_job.rb.tt
@@ -0,0 +1,5 @@
+<%= wrap_in_modules <<~rb
+ class ApplicationJob < ActiveJob::Base
+ end
+rb
+%>
diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/mailers/%namespaced_name%/application_mailer.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/mailers/%namespaced_name%/application_mailer.rb.tt
new file mode 100644
index 0000000000..246e274348
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/app/mailers/%namespaced_name%/application_mailer.rb.tt
@@ -0,0 +1,7 @@
+<%= wrap_in_modules <<~rb
+ class ApplicationMailer < ActionMailer::Base
+ default from: 'from@example.com'
+ layout 'mailer'
+ end
+rb
+%>
diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/models/%namespaced_name%/application_record.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/models/%namespaced_name%/application_record.rb.tt
new file mode 100644
index 0000000000..21465278be
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/app/models/%namespaced_name%/application_record.rb.tt
@@ -0,0 +1,6 @@
+<%= wrap_in_modules <<~rb
+ class ApplicationRecord < ActiveRecord::Base
+ self.abstract_class = true
+ end
+rb
+%>
diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/views/layouts/%namespaced_name%/application.html.erb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/views/layouts/%namespaced_name%/application.html.erb.tt
index 6bc480161d..6e54a1ce9d 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/app/views/layouts/%namespaced_name%/application.html.erb.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/app/views/layouts/%namespaced_name%/application.html.erb.tt
@@ -2,9 +2,13 @@
<html>
<head>
<title><%= humanized %></title>
+ <%%= csrf_meta_tags %>
+ <%%= csp_meta_tag %>
+
<%%= stylesheet_link_tag "<%= namespaced_name %>/application", media: "all" %>
+ <%- unless options[:skip_javascript] -%>
<%%= javascript_include_tag "<%= namespaced_name %>/application" %>
- <%%= csrf_meta_tags %>
+ <%- end -%>
</head>
<body>
diff --git a/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt b/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt
index 3edaac35c9..ee8e469da2 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt
@@ -1,11 +1,30 @@
-# This command will automatically be run when you run "rails" with Rails 4 gems installed from the root of your application.
+# This command will automatically be run when you run "rails" with Rails gems
+# installed from the root of your application.
-ENGINE_ROOT = File.expand_path('../..', __FILE__)
-ENGINE_PATH = File.expand_path('../../lib/<%= namespaced_name -%>/engine', __FILE__)
+ENGINE_ROOT = File.expand_path('..', __dir__)
+ENGINE_PATH = File.expand_path('../lib/<%= namespaced_name -%>/engine', __dir__)
+<% if with_dummy_app? -%>
+APP_PATH = File.expand_path('../<%= dummy_path -%>/config/application', __dir__)
+<% end -%>
# Set up gems listed in the Gemfile.
-ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
+<% if include_all_railties? -%>
require 'rails/all'
+<% else -%>
+require "rails"
+# Pick the frameworks you want:
+require "active_model/railtie"
+require "active_job/railtie"
+<%= comment_if :skip_active_record %>require "active_record/railtie"
+<%= comment_if :skip_active_storage %>require "active_storage/engine"
+require "action_controller/railtie"
+<%= comment_if :skip_action_mailer %>require "action_mailer/railtie"
+require "action_view/railtie"
+<%= comment_if :skip_action_cable %>require "action_cable/engine"
+<%= comment_if :skip_sprockets %>require "sprockets/railtie"
+<%= comment_if :skip_test %>require "rails/test_unit/railtie"
+<% end -%>
require 'rails/engine/commands'
diff --git a/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt b/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt
new file mode 100644
index 0000000000..8e7d321626
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt
@@ -0,0 +1,4 @@
+$: << File.expand_path("../test", __dir__)
+
+require "bundler/setup"
+require "rails/plugin/test"
diff --git a/railties/lib/rails/generators/rails/plugin/templates/config/routes.rb b/railties/lib/rails/generators/rails/plugin/templates/config/routes.rb.tt
index 154452bfe5..154452bfe5 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/config/routes.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/config/routes.rb.tt
diff --git a/railties/lib/rails/generators/rails/plugin/templates/gitignore b/railties/lib/rails/generators/rails/plugin/templates/gitignore
deleted file mode 100644
index d524fcbc4e..0000000000
--- a/railties/lib/rails/generators/rails/plugin/templates/gitignore
+++ /dev/null
@@ -1,10 +0,0 @@
-.bundle/
-log/*.log
-pkg/
-<% unless options[:skip_test] && options[:dummy_path] == 'test/dummy' -%>
-<%= dummy_path %>/db/*.sqlite3
-<%= dummy_path %>/db/*.sqlite3-journal
-<%= dummy_path %>/log/*.log
-<%= dummy_path %>/tmp/
-<%= dummy_path %>/.sass-cache
-<% end -%>
diff --git a/railties/lib/rails/generators/rails/plugin/templates/gitignore.tt b/railties/lib/rails/generators/rails/plugin/templates/gitignore.tt
new file mode 100644
index 0000000000..0aabf09252
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/gitignore.tt
@@ -0,0 +1,18 @@
+.bundle/
+log/*.log
+pkg/
+<% if with_dummy_app? -%>
+<% if sqlite3? -%>
+<%= dummy_path %>/db/*.sqlite3
+<%= dummy_path %>/db/*.sqlite3-journal
+<% end -%>
+<%= dummy_path %>/log/*.log
+<% unless options[:skip_javascript] -%>
+<%= dummy_path %>/node_modules/
+<%= dummy_path %>/yarn-error.log
+<% end -%>
+<% unless skip_active_storage? -%>
+<%= dummy_path %>/storage/
+<% end -%>
+<%= dummy_path %>/tmp/
+<% end -%>
diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%.rb b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%.rb
deleted file mode 100644
index 40b1c4cee7..0000000000
--- a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-<% if engine? -%>
-require "<%= namespaced_name %>/engine"
-
-<% end -%>
-<%= wrap_in_modules "# Your code goes here..." %>
diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%.rb.tt
new file mode 100644
index 0000000000..3285055eb7
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%.rb.tt
@@ -0,0 +1,7 @@
+<% if engine? -%>
+require "<%= namespaced_name %>/engine"
+<% else -%>
+require "<%= namespaced_name %>/railtie"
+<% end -%>
+
+<%= wrap_in_modules "# Your code goes here..." %>
diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb
deleted file mode 100644
index 17afd52177..0000000000
--- a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-<%= wrap_in_modules <<-rb.strip_heredoc
- class Engine < ::Rails::Engine
- #{mountable? ? ' isolate_namespace ' + camelized_modules : ' '}
- end
-rb
-%>
diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb.tt
new file mode 100644
index 0000000000..4ec1804940
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb.tt
@@ -0,0 +1,7 @@
+<%= wrap_in_modules <<~rb
+ class Engine < ::Rails::Engine
+ #{mountable? ? ' isolate_namespace ' + camelized_modules : ' '}
+ #{api? ? " config.generators.api_only = true" : ' '}
+ end
+rb
+%>
diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/railtie.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/railtie.rb.tt
new file mode 100644
index 0000000000..b853fabcc3
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/railtie.rb.tt
@@ -0,0 +1,5 @@
+<%= wrap_in_modules <<~rb
+ class Railtie < ::Rails::Railtie
+ end
+rb
+%>
diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb
deleted file mode 100644
index d257295988..0000000000
--- a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb
+++ /dev/null
@@ -1 +0,0 @@
-<%= wrap_in_modules 'VERSION = "0.0.1"' %>
diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb.tt
new file mode 100644
index 0000000000..b08f4ef9ae
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb.tt
@@ -0,0 +1 @@
+<%= wrap_in_modules "VERSION = '0.1.0'" %>
diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/tasks/%namespaced_name%_tasks.rake b/railties/lib/rails/generators/rails/plugin/templates/lib/tasks/%namespaced_name%_tasks.rake.tt
index 88a2c4120f..88a2c4120f 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/lib/tasks/%namespaced_name%_tasks.rake
+++ b/railties/lib/rails/generators/rails/plugin/templates/lib/tasks/%namespaced_name%_tasks.rake.tt
diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb b/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb
deleted file mode 100644
index b1038c839e..0000000000
--- a/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-require File.expand_path('../boot', __FILE__)
-
-<% if include_all_railties? -%>
-require 'rails/all'
-<% else -%>
-# Pick the frameworks you want:
-<%= comment_if :skip_active_record %>require "active_record/railtie"
-require "action_controller/railtie"
-<%= comment_if :skip_action_mailer %>require "action_mailer/railtie"
-require "action_view/railtie"
-<%= comment_if :skip_sprockets %>require "sprockets/railtie"
-<%= comment_if :skip_test %>require "rails/test_unit/railtie"
-<% end -%>
-
-Bundler.require(*Rails.groups)
-require "<%= namespaced_name %>"
-
-<%= application_definition %>
diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb.tt
new file mode 100644
index 0000000000..06ffe2f1ed
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb.tt
@@ -0,0 +1,23 @@
+require_relative 'boot'
+
+<% if include_all_railties? -%>
+require 'rails/all'
+<% else -%>
+require "rails"
+# Pick the frameworks you want:
+require "active_model/railtie"
+require "active_job/railtie"
+<%= comment_if :skip_active_record %>require "active_record/railtie"
+<%= comment_if :skip_active_storage %>require "active_storage/engine"
+require "action_controller/railtie"
+<%= comment_if :skip_action_mailer %>require "action_mailer/railtie"
+require "action_view/railtie"
+<%= comment_if :skip_action_cable %>require "action_cable/engine"
+<%= comment_if :skip_sprockets %>require "sprockets/railtie"
+<%= comment_if :skip_test %>require "rails/test_unit/railtie"
+<% end -%>
+
+Bundler.require(*Rails.groups)
+require "<%= namespaced_name %>"
+
+<%= application_definition %>
diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb b/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb
deleted file mode 100644
index 6266cfc509..0000000000
--- a/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# Set up gems listed in the Gemfile.
-ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__)
-
-require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
-$LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__)
diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb.tt
new file mode 100644
index 0000000000..c9aef85d40
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb.tt
@@ -0,0 +1,5 @@
+# Set up gems listed in the Gemfile.
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__)
+
+require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
+$LOAD_PATH.unshift File.expand_path('../../../lib', __dir__)
diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/dummy_manifest.js.tt b/railties/lib/rails/generators/rails/plugin/templates/rails/dummy_manifest.js.tt
new file mode 100644
index 0000000000..03937cf8ff
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/rails/dummy_manifest.js.tt
@@ -0,0 +1,10 @@
+<% unless api? -%>
+//= link_tree ../images
+<% end -%>
+<% unless options.skip_javascript -%>
+//= link_directory ../javascripts .js
+<% end -%>
+//= link_directory ../stylesheets .css
+<% if mountable? && !api? -%>
+//= link <%= underscored_name %>_manifest.js
+<% end -%>
diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/engine_manifest.js.tt b/railties/lib/rails/generators/rails/plugin/templates/rails/engine_manifest.js.tt
new file mode 100644
index 0000000000..2f23844f5e
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/rails/engine_manifest.js.tt
@@ -0,0 +1,6 @@
+<% if mountable? -%>
+<% if !options.skip_javascript -%>
+//= link_directory ../javascripts/<%= namespaced_name %> .js
+<% end -%>
+//= link_directory ../stylesheets/<%= namespaced_name %> .css
+<% end -%>
diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js b/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js
deleted file mode 100644
index 8913b40f69..0000000000
--- a/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js
+++ /dev/null
@@ -1,13 +0,0 @@
-// 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.
-//
-// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
-// compiled file.
-//
-// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
-// about supported directives.
-//
-//= require_tree .
diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js.tt b/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js.tt
new file mode 100644
index 0000000000..51049826bf
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js.tt
@@ -0,0 +1,17 @@
+// 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.
+//
+// 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.
+//
+// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
+// about supported directives.
+//
+//= require rails-ujs
+<% unless skip_active_storage? -%>
+//= require activestorage
+<% end -%>
+//= require_tree .
diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb b/railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb
deleted file mode 100644
index 673de44108..0000000000
--- a/railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-Rails.application.routes.draw do
-
- mount <%= camelized_modules %>::Engine => "/<%= name %>"
-end
diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb.tt
new file mode 100644
index 0000000000..694510edc0
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb.tt
@@ -0,0 +1,3 @@
+Rails.application.routes.draw do
+ mount <%= camelized_modules %>::Engine => "/<%= name %>"
+end
diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/stylesheets.css b/railties/lib/rails/generators/rails/plugin/templates/rails/stylesheets.css
index 0cdd2788d0..0ebd7fe829 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/rails/stylesheets.css
+++ b/railties/lib/rails/generators/rails/plugin/templates/rails/stylesheets.css
@@ -7,7 +7,8 @@
*
* 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
- * files in this directory. It is generally better to create a new file per style scope.
+ * files in this directory. Styles in this file should be added after the last require_* statement.
+ * It is generally better to create a new file per style scope.
*
*= require_tree .
*= require_self
diff --git a/railties/lib/rails/generators/rails/plugin/templates/test/%namespaced_name%_test.rb b/railties/lib/rails/generators/rails/plugin/templates/test/%namespaced_name%_test.rb.tt
index 1ee05d7871..1ee05d7871 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/test/%namespaced_name%_test.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/test/%namespaced_name%_test.rb.tt
diff --git a/railties/lib/rails/generators/rails/plugin/templates/test/application_system_test_case.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/test/application_system_test_case.rb.tt
new file mode 100644
index 0000000000..d19212abd5
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/test/application_system_test_case.rb.tt
@@ -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/plugin/templates/test/integration/navigation_test.rb b/railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb
deleted file mode 100644
index f5d1ec2046..0000000000
--- a/railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-require 'test_helper'
-
-class NavigationTest < ActionDispatch::IntegrationTest
- # test "the truth" do
- # assert true
- # end
-end
-
diff --git a/railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb.tt
new file mode 100644
index 0000000000..29e59d8407
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb.tt
@@ -0,0 +1,7 @@
+require 'test_helper'
+
+class NavigationTest < ActionDispatch::IntegrationTest
+ # test "the truth" do
+ # assert true
+ # end
+end
diff --git a/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb b/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb
deleted file mode 100644
index 95adcc06ff..0000000000
--- a/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# Configure Rails Environment
-ENV["RAILS_ENV"] = "test"
-
-require File.expand_path("../../<%= options[:dummy_path] -%>/config/environment.rb", __FILE__)
-<% unless options[:skip_active_record] -%>
-ActiveRecord::Migrator.migrations_paths = [File.expand_path("../../<%= options[:dummy_path] -%>/db/migrate", __FILE__)]
-<% if options[:mountable] -%>
-ActiveRecord::Migrator.migrations_paths << File.expand_path('../../db/migrate', __FILE__)
-<% end -%>
-<% end -%>
-require "rails/test_help"
-
-# Filter out Minitest backtrace while allowing backtrace from other libraries
-# to be shown.
-Minitest.backtrace_filter = Minitest::BacktraceFilter.new
-
-# Load support files
-Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
-
-# Load fixtures from the engine
-if ActiveSupport::TestCase.respond_to?(:fixture_path=)
- ActiveSupport::TestCase.fixture_path = File.expand_path("../fixtures", __FILE__)
- ActiveSupport::TestCase.file_fixture_path = ActiveSupport::TestCase.fixture_path + "/files"
- ActiveSupport::TestCase.fixtures :all
-end
diff --git a/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb.tt
new file mode 100644
index 0000000000..4f7a8d3d6e
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb.tt
@@ -0,0 +1,29 @@
+# Configure Rails Environment
+ENV["RAILS_ENV"] = "test"
+
+require_relative "<%= File.join('..', options[:dummy_path], 'config/environment') -%>"
+<% unless options[:skip_active_record] -%>
+ActiveRecord::Migrator.migrations_paths = [File.expand_path("../<%= options[:dummy_path] -%>/db/migrate", __dir__)]
+<% if options[:mountable] -%>
+ActiveRecord::Migrator.migrations_paths << File.expand_path('../db/migrate', __dir__)
+<% end -%>
+<% end -%>
+require "rails/test_help"
+
+# Filter out the backtrace from minitest while preserving the one from other libraries.
+Minitest.backtrace_filter = Minitest::BacktraceFilter.new
+
+<% unless engine? -%>
+require "rails/test_unit/reporter"
+Rails::TestUnitReporter.executable = 'bin/test'
+<% end -%>
+
+<% unless options[:skip_active_record] -%>
+# Load fixtures from the engine
+if ActiveSupport::TestCase.respond_to?(:fixture_path=)
+ ActiveSupport::TestCase.fixture_path = File.expand_path("fixtures", __dir__)
+ ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path
+ ActiveSupport::TestCase.file_fixture_path = ActiveSupport::TestCase.fixture_path + "/files"
+ ActiveSupport::TestCase.fixtures :all
+end
+<% end -%>
diff --git a/railties/lib/rails/generators/rails/resource/USAGE b/railties/lib/rails/generators/rails/resource/USAGE
index e359cd574f..66d0ee546a 100644
--- a/railties/lib/rails/generators/rails/resource/USAGE
+++ b/railties/lib/rails/generators/rails/resource/USAGE
@@ -1,6 +1,6 @@
Description:
Stubs out a new resource including an empty model and controller suitable
- for a restful, resource-oriented application. Pass the singular model name,
+ for a RESTful, resource-oriented application. Pass the singular model name,
either CamelCased or under_scored, as the first argument, and an optional
list of attribute pairs.
diff --git a/railties/lib/rails/generators/rails/resource/resource_generator.rb b/railties/lib/rails/generators/rails/resource/resource_generator.rb
index 3acf21df13..3ba25ef0fe 100644
--- a/railties/lib/rails/generators/rails/resource/resource_generator.rb
+++ b/railties/lib/rails/generators/rails/resource/resource_generator.rb
@@ -1,5 +1,7 @@
-require 'rails/generators/resource_helpers'
-require 'rails/generators/rails/model/model_generator'
+# frozen_string_literal: true
+
+require "rails/generators/resource_helpers"
+require "rails/generators/rails/model/model_generator"
module Rails
module Generators
diff --git a/railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb b/railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb
index 42705107ae..9a92991efe 100644
--- a/railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb
+++ b/railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Rails
module Generators
class ResourceRouteGenerator < NamedBase # :nodoc:
@@ -15,37 +17,32 @@ module Rails
def add_resource_route
return if options[:actions].present?
- # iterates over all namespaces and opens up blocks
- regular_class_path.each_with_index do |namespace, index|
- write("namespace :#{namespace} do", index + 1)
+ depth = 0
+ lines = []
+
+ # Create 'namespace' ladder
+ # namespace :foo do
+ # namespace :bar do
+ regular_class_path.each do |ns|
+ lines << indent("namespace :#{ns} do\n", depth * 2)
+ depth += 1
end
# inserts the primary resource
- write("resources :#{file_name.pluralize}", route_length + 1)
+ # Create route
+ # resources 'products'
+ lines << indent(%{resources :#{file_name.pluralize}\n}, depth * 2)
- # ends blocks
- regular_class_path.each_index do |index|
- write("end", route_length - index)
+ # Create `end` ladder
+ # end
+ # end
+ until depth.zero?
+ depth -= 1
+ lines << indent("end\n", depth * 2)
end
- # route prepends two spaces onto the front of the string that is passed, this corrects that.
- # Also it adds a \n to the end of each line, as route already adds that
- # we need to correct that too.
- route route_string[2..-2]
+ route lines.join
end
-
- private
- def route_string
- @route_string ||= ""
- end
-
- def write(str, indent)
- route_string << "#{" " * indent}#{str}\n"
- end
-
- def route_length
- regular_class_path.length
- end
end
end
end
diff --git a/railties/lib/rails/generators/rails/scaffold/USAGE b/railties/lib/rails/generators/rails/scaffold/USAGE
index d2e495758d..c9283eda87 100644
--- a/railties/lib/rails/generators/rails/scaffold/USAGE
+++ b/railties/lib/rails/generators/rails/scaffold/USAGE
@@ -16,7 +16,7 @@ Description:
As a special case, specifying 'password:digest' will generate a
password_digest field of string type, and configure your generated model,
- controller, views, and test suite for use with ActiveModel
+ controller, views, and test suite for use with Active Model
has_secure_password (assuming they are using Rails defaults).
Timestamps are added by default, so you don't have to specify them by hand
diff --git a/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb
index 17c32bfdb3..8beb7416c0 100644
--- a/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb
+++ b/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb
@@ -1,4 +1,6 @@
-require 'rails/generators/rails/resource/resource_generator'
+# frozen_string_literal: true
+
+require "rails/generators/rails/resource/resource_generator"
module Rails
module Generators
@@ -6,6 +8,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
diff --git a/railties/lib/rails/generators/rails/scaffold/templates/scaffold.css b/railties/lib/rails/generators/rails/scaffold/templates/scaffold.css
index b7818883d1..cd4f3de38d 100644
--- a/railties/lib/rails/generators/rails/scaffold/templates/scaffold.css
+++ b/railties/lib/rails/generators/rails/scaffold/templates/scaffold.css
@@ -1,13 +1,13 @@
body {
background-color: #fff;
color: #333;
+ margin: 33px;
}
body, p, ol, ul, td {
font-family: verdana, arial, helvetica, sans-serif;
font-size: 13px;
line-height: 18px;
- margin: 33px;
}
pre {
@@ -34,9 +34,7 @@ th {
}
td {
- padding-bottom: 7px;
- padding-left: 5px;
- padding-right: 5px;
+ padding: 0 5px 7px;
}
div.field,
@@ -57,8 +55,7 @@ div.actions {
#error_explanation {
width: 450px;
border: 2px solid red;
- padding: 7px;
- padding-bottom: 0;
+ padding: 7px 7px 0;
margin-bottom: 20px;
background-color: #f0f0f0;
}
@@ -68,8 +65,7 @@ div.actions {
font-weight: bold;
padding: 5px 5px 5px 15px;
font-size: 12px;
- margin: -7px;
- margin-bottom: 0;
+ margin: -7px -7px 0;
background-color: #c00;
color: #fff;
}
@@ -78,3 +74,7 @@ div.actions {
font-size: 12px;
list-style: square;
}
+
+label {
+ display: block;
+}
diff --git a/railties/lib/rails/generators/rails/scaffold_controller/USAGE b/railties/lib/rails/generators/rails/scaffold_controller/USAGE
index 8ba4c5ccbc..28f229510b 100644
--- a/railties/lib/rails/generators/rails/scaffold_controller/USAGE
+++ b/railties/lib/rails/generators/rails/scaffold_controller/USAGE
@@ -12,7 +12,7 @@ Description:
Example:
`rails generate scaffold_controller CreditCard`
- Credit card controller with URLs like /credit_card/debit.
+ Credit card controller with URLs like /credit_cards.
Controller: app/controllers/credit_cards_controller.rb
Test: test/controllers/credit_cards_controller_test.rb
Views: app/views/credit_cards/index.html.erb [...]
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 d0b8cad896..7030561a33 100644
--- a/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb
+++ b/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb
@@ -1,4 +1,6 @@
-require 'rails/generators/resource_helpers'
+# frozen_string_literal: true
+
+require "rails/generators/resource_helpers"
module Rails
module Generators
@@ -17,10 +19,14 @@ module Rails
def create_controller_files
template_file = options.api? ? "api_controller.rb" : "controller.rb"
- template template_file, File.join('app/controllers', controller_class_path, "#{controller_file_name}_controller.rb")
+ template template_file, File.join("app/controllers", controller_class_path, "#{controller_file_name}_controller.rb")
+ end
+
+ hook_for :template_engine, as: :scaffold do |template_engine|
+ invoke template_engine unless options.api?
end
- hook_for :template_engine, :test_framework, as: :scaffold
+ 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/scaffold_controller/templates/api_controller.rb b/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb
deleted file mode 100644
index bc3c9b3f6b..0000000000
--- a/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb
+++ /dev/null
@@ -1,61 +0,0 @@
-<% if namespaced? -%>
-require_dependency "<%= namespaced_file_path %>/application_controller"
-
-<% end -%>
-<% module_namespacing do -%>
-class <%= controller_class_name %>Controller < ApplicationController
- before_action :set_<%= singular_table_name %>, only: [:show, :update, :destroy]
-
- # GET <%= route_url %>
- def index
- @<%= plural_table_name %> = <%= orm_class.all(class_name) %>
-
- render json: <%= "@#{plural_table_name}" %>
- end
-
- # GET <%= route_url %>/1
- def show
- render json: <%= "@#{singular_table_name}" %>
- end
-
- # POST <%= route_url %>
- def create
- @<%= singular_table_name %> = <%= orm_class.build(class_name, "#{singular_table_name}_params") %>
-
- if @<%= orm_instance.save %>
- render json: <%= "@#{singular_table_name}" %>, status: :created, location: <%= "@#{singular_table_name}" %>
- else
- render json: <%= "@#{orm_instance.errors}" %>, status: :unprocessable_entity
- end
- end
-
- # PATCH/PUT <%= route_url %>/1
- def update
- if @<%= orm_instance.update("#{singular_table_name}_params") %>
- render json: <%= "@#{singular_table_name}" %>
- else
- render json: <%= "@#{orm_instance.errors}" %>, status: :unprocessable_entity
- end
- end
-
- # DELETE <%= route_url %>/1
- def destroy
- @<%= orm_instance.destroy %>
- end
-
- private
- # Use callbacks to share common setup or constraints between actions.
- def set_<%= singular_table_name %>
- @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %>
- end
-
- # Only allow a trusted parameter "white list" through.
- def <%= "#{singular_table_name}_params" %>
- <%- if attributes_names.empty? -%>
- params[:<%= singular_table_name %>]
- <%- else -%>
- params.require(:<%= singular_table_name %>).permit(<%= attributes_names.map { |name| ":#{name}" }.join(', ') %>)
- <%- end -%>
- end
-end
-<% end -%>
diff --git a/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb.tt b/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb.tt
new file mode 100644
index 0000000000..400afec6dc
--- /dev/null
+++ b/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb.tt
@@ -0,0 +1,61 @@
+<% if namespaced? -%>
+require_dependency "<%= namespaced_path %>/application_controller"
+
+<% end -%>
+<% module_namespacing do -%>
+class <%= controller_class_name %>Controller < ApplicationController
+ before_action :set_<%= singular_table_name %>, only: [:show, :update, :destroy]
+
+ # GET <%= route_url %>
+ def index
+ @<%= plural_table_name %> = <%= orm_class.all(class_name) %>
+
+ render json: <%= "@#{plural_table_name}" %>
+ end
+
+ # GET <%= route_url %>/1
+ def show
+ render json: <%= "@#{singular_table_name}" %>
+ end
+
+ # POST <%= route_url %>
+ def create
+ @<%= singular_table_name %> = <%= orm_class.build(class_name, "#{singular_table_name}_params") %>
+
+ if @<%= orm_instance.save %>
+ render json: <%= "@#{singular_table_name}" %>, status: :created, location: <%= "@#{singular_table_name}" %>
+ else
+ render json: <%= "@#{orm_instance.errors}" %>, status: :unprocessable_entity
+ end
+ end
+
+ # PATCH/PUT <%= route_url %>/1
+ def update
+ if @<%= orm_instance.update("#{singular_table_name}_params") %>
+ render json: <%= "@#{singular_table_name}" %>
+ else
+ render json: <%= "@#{orm_instance.errors}" %>, status: :unprocessable_entity
+ end
+ end
+
+ # DELETE <%= route_url %>/1
+ def destroy
+ @<%= orm_instance.destroy %>
+ end
+
+ private
+ # Use callbacks to share common setup or constraints between actions.
+ def set_<%= singular_table_name %>
+ @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %>
+ end
+
+ # Only allow a trusted parameter "white list" through.
+ def <%= "#{singular_table_name}_params" %>
+ <%- if attributes_names.empty? -%>
+ params.fetch(:<%= singular_table_name %>, {})
+ <%- else -%>
+ params.require(:<%= singular_table_name %>).permit(<%= attributes_names.map { |name| ":#{name}" }.join(', ') %>)
+ <%- end -%>
+ end
+end
+<% end -%>
diff --git a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb
deleted file mode 100644
index 2c3b04043f..0000000000
--- a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb
+++ /dev/null
@@ -1,68 +0,0 @@
-<% if namespaced? -%>
-require_dependency "<%= namespaced_file_path %>/application_controller"
-
-<% end -%>
-<% module_namespacing do -%>
-class <%= controller_class_name %>Controller < ApplicationController
- before_action :set_<%= singular_table_name %>, only: [:show, :edit, :update, :destroy]
-
- # GET <%= route_url %>
- def index
- @<%= plural_table_name %> = <%= orm_class.all(class_name) %>
- end
-
- # GET <%= route_url %>/1
- def show
- end
-
- # GET <%= route_url %>/new
- def new
- @<%= singular_table_name %> = <%= orm_class.build(class_name) %>
- end
-
- # GET <%= route_url %>/1/edit
- def edit
- end
-
- # POST <%= route_url %>
- def create
- @<%= singular_table_name %> = <%= orm_class.build(class_name, "#{singular_table_name}_params") %>
-
- if @<%= orm_instance.save %>
- redirect_to @<%= singular_table_name %>, notice: <%= "'#{human_name} was successfully created.'" %>
- else
- render :new
- end
- end
-
- # PATCH/PUT <%= route_url %>/1
- def update
- if @<%= orm_instance.update("#{singular_table_name}_params") %>
- redirect_to @<%= singular_table_name %>, notice: <%= "'#{human_name} was successfully updated.'" %>
- else
- render :edit
- end
- end
-
- # DELETE <%= route_url %>/1
- def destroy
- @<%= orm_instance.destroy %>
- redirect_to <%= index_helper %>_url, notice: <%= "'#{human_name} was successfully destroyed.'" %>
- end
-
- private
- # Use callbacks to share common setup or constraints between actions.
- def set_<%= singular_table_name %>
- @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %>
- end
-
- # Only allow a trusted parameter "white list" through.
- def <%= "#{singular_table_name}_params" %>
- <%- if attributes_names.empty? -%>
- params[:<%= singular_table_name %>]
- <%- else -%>
- params.require(:<%= singular_table_name %>).permit(<%= attributes_names.map { |name| ":#{name}" }.join(', ') %>)
- <%- end -%>
- end
-end
-<% end -%>
diff --git a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb.tt b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb.tt
new file mode 100644
index 0000000000..05f1c2b2d3
--- /dev/null
+++ b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb.tt
@@ -0,0 +1,68 @@
+<% if namespaced? -%>
+require_dependency "<%= namespaced_path %>/application_controller"
+
+<% end -%>
+<% module_namespacing do -%>
+class <%= controller_class_name %>Controller < ApplicationController
+ before_action :set_<%= singular_table_name %>, only: [:show, :edit, :update, :destroy]
+
+ # GET <%= route_url %>
+ def index
+ @<%= plural_table_name %> = <%= orm_class.all(class_name) %>
+ end
+
+ # GET <%= route_url %>/1
+ def show
+ end
+
+ # GET <%= route_url %>/new
+ def new
+ @<%= singular_table_name %> = <%= orm_class.build(class_name) %>
+ end
+
+ # GET <%= route_url %>/1/edit
+ def edit
+ end
+
+ # POST <%= route_url %>
+ def create
+ @<%= singular_table_name %> = <%= orm_class.build(class_name, "#{singular_table_name}_params") %>
+
+ if @<%= orm_instance.save %>
+ redirect_to <%= redirect_resource_name %>, notice: <%= "'#{human_name} was successfully created.'" %>
+ else
+ render :new
+ end
+ end
+
+ # PATCH/PUT <%= route_url %>/1
+ def update
+ if @<%= orm_instance.update("#{singular_table_name}_params") %>
+ redirect_to <%= redirect_resource_name %>, notice: <%= "'#{human_name} was successfully updated.'" %>
+ else
+ render :edit
+ end
+ end
+
+ # DELETE <%= route_url %>/1
+ def destroy
+ @<%= orm_instance.destroy %>
+ redirect_to <%= index_helper %>_url, notice: <%= "'#{human_name} was successfully destroyed.'" %>
+ end
+
+ private
+ # Use callbacks to share common setup or constraints between actions.
+ def set_<%= singular_table_name %>
+ @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %>
+ end
+
+ # Only allow a trusted parameter "white list" through.
+ def <%= "#{singular_table_name}_params" %>
+ <%- if attributes_names.empty? -%>
+ params.fetch(:<%= singular_table_name %>, {})
+ <%- else -%>
+ params.require(:<%= singular_table_name %>).permit(<%= attributes_names.map { |name| ":#{name}" }.join(', ') %>)
+ <%- end -%>
+ end
+end
+<% end -%>
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..7169e1bd3b
--- /dev/null
+++ b/railties/lib/rails/generators/rails/system_test/system_test_generator.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Rails
+ module Generators
+ class SystemTestGenerator < NamedBase # :nodoc:
+ hook_for :system_tests, as: :system
+ end
+ end
+end
diff --git a/railties/lib/rails/generators/rails/task/task_generator.rb b/railties/lib/rails/generators/rails/task/task_generator.rb
index 754824ca0c..b7290a7447 100644
--- a/railties/lib/rails/generators/rails/task/task_generator.rb
+++ b/railties/lib/rails/generators/rails/task/task_generator.rb
@@ -1,12 +1,13 @@
+# frozen_string_literal: true
+
module Rails
module Generators
class TaskGenerator < NamedBase # :nodoc:
argument :actions, type: :array, default: [], banner: "action action"
def create_task_files
- template 'task.rb', File.join('lib/tasks', "#{file_name}.rake")
+ template "task.rb", File.join("lib/tasks", "#{file_name}.rake")
end
-
end
end
end
diff --git a/railties/lib/rails/generators/rails/task/templates/task.rb b/railties/lib/rails/generators/rails/task/templates/task.rb.tt
index 1e3ed5f158..1e3ed5f158 100644
--- a/railties/lib/rails/generators/rails/task/templates/task.rb
+++ b/railties/lib/rails/generators/rails/task/templates/task.rb.tt
diff --git a/railties/lib/rails/generators/resource_helpers.rb b/railties/lib/rails/generators/resource_helpers.rb
index 9c2037783e..5675faff70 100644
--- a/railties/lib/rails/generators/resource_helpers.rb
+++ b/railties/lib/rails/generators/resource_helpers.rb
@@ -1,12 +1,13 @@
-require 'rails/generators/active_model'
-require 'rails/generators/model_helpers'
+# frozen_string_literal: true
+
+require "rails/generators/active_model"
+require "rails/generators/model_helpers"
module Rails
module Generators
# Deal with controller names on scaffold and add some helpers to deal with
# ActiveModel.
module ResourceHelpers # :nodoc:
-
def self.included(base) #:nodoc:
base.include(Rails::Generators::ModelHelpers)
base.class_option :model_name, type: :string, desc: "ModelName to be used"
@@ -18,14 +19,13 @@ module Rails
controller_name = name
if options[:model_name]
self.name = options[:model_name]
- assign_names!(self.name)
+ assign_names!(name)
end
assign_controller_names!(controller_name.pluralize)
end
- protected
-
+ private
attr_reader :controller_name, :controller_file_name
def controller_class_path
@@ -38,25 +38,25 @@ module Rails
def assign_controller_names!(name)
@controller_name = name
- @controller_class_path = name.include?('/') ? name.split('/') : name.split('::')
+ @controller_class_path = name.include?("/") ? name.split("/") : name.split("::")
@controller_class_path.map!(&:underscore)
@controller_file_name = @controller_class_path.pop
end
def controller_file_path
- @controller_file_path ||= (controller_class_path + [controller_file_name]).join('/')
+ @controller_file_path ||= (controller_class_path + [controller_file_name]).join("/")
end
def controller_class_name
- (controller_class_path + [controller_file_name]).map!(&:camelize).join('::')
+ (controller_class_path + [controller_file_name]).map!(&:camelize).join("::")
end
def controller_i18n_scope
- @controller_i18n_scope ||= controller_file_path.tr('/', '.')
+ @controller_i18n_scope ||= controller_file_path.tr("/", ".")
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
@@ -74,7 +74,7 @@ module Rails
end
# Initialize ORM::Generators::ActiveModel to access instance methods.
- def orm_instance(name=singular_table_name)
+ def orm_instance(name = singular_table_name)
@orm_instance ||= orm_class.new(name)
end
end
diff --git a/railties/lib/rails/generators/test_case.rb b/railties/lib/rails/generators/test_case.rb
index 58592b4f8e..5c71bf0be9 100644
--- a/railties/lib/rails/generators/test_case.rb
+++ b/railties/lib/rails/generators/test_case.rb
@@ -1,8 +1,10 @@
-require 'rails/generators'
-require 'rails/generators/testing/behaviour'
-require 'rails/generators/testing/setup_and_teardown'
-require 'rails/generators/testing/assertions'
-require 'fileutils'
+# frozen_string_literal: true
+
+require "rails/generators"
+require "rails/generators/testing/behaviour"
+require "rails/generators/testing/setup_and_teardown"
+require "rails/generators/testing/assertions"
+require "fileutils"
module Rails
module Generators
@@ -14,7 +16,7 @@ module Rails
#
# class AppGeneratorTest < Rails::Generators::TestCase
# tests AppGenerator
- # destination File.expand_path("../tmp", File.dirname(__FILE__))
+ # destination File.expand_path("../tmp", __dir__)
# end
#
# If you want to ensure your destination root is clean before running each test,
@@ -22,7 +24,7 @@ module Rails
#
# class AppGeneratorTest < Rails::Generators::TestCase
# tests AppGenerator
- # destination File.expand_path("../tmp", File.dirname(__FILE__))
+ # destination File.expand_path("../tmp", __dir__)
# setup :prepare_destination
# end
class TestCase < ActiveSupport::TestCase
@@ -30,7 +32,6 @@ module Rails
include Rails::Generators::Testing::SetupAndTeardown
include Rails::Generators::Testing::Assertions
include FileUtils
-
end
end
end
diff --git a/railties/lib/rails/generators/test_unit.rb b/railties/lib/rails/generators/test_unit.rb
index fe45c9e15d..1005ac557c 100644
--- a/railties/lib/rails/generators/test_unit.rb
+++ b/railties/lib/rails/generators/test_unit.rb
@@ -1,4 +1,6 @@
-require 'rails/generators/named_base'
+# frozen_string_literal: true
+
+require "rails/generators/named_base"
module TestUnit # :nodoc:
module Generators # :nodoc:
diff --git a/railties/lib/rails/generators/test_unit/controller/controller_generator.rb b/railties/lib/rails/generators/test_unit/controller/controller_generator.rb
index b5aa581769..1a9ac6bf2a 100644
--- a/railties/lib/rails/generators/test_unit/controller/controller_generator.rb
+++ b/railties/lib/rails/generators/test_unit/controller/controller_generator.rb
@@ -1,4 +1,6 @@
-require 'rails/generators/test_unit'
+# frozen_string_literal: true
+
+require "rails/generators/test_unit"
module TestUnit # :nodoc:
module Generators # :nodoc:
@@ -7,8 +9,8 @@ module TestUnit # :nodoc:
check_class_collision suffix: "ControllerTest"
def create_test_files
- template 'functional_test.rb',
- File.join('test/controllers', class_path, "#{file_name}_controller_test.rb")
+ template "functional_test.rb",
+ File.join("test/controllers", class_path, "#{file_name}_controller_test.rb")
end
end
end
diff --git a/railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb b/railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb
deleted file mode 100644
index 5a8a3ca5e0..0000000000
--- a/railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-require 'test_helper'
-
-<% module_namespacing do -%>
-class <%= class_name %>ControllerTest < ActionController::TestCase
-<% if mountable_engine? -%>
- setup do
- @routes = Engine.routes
- end
-
-<% end -%>
-<% if actions.empty? -%>
- # test "the truth" do
- # assert true
- # end
-<% else -%>
-<% actions.each do |action| -%>
- test "should get <%= action %>" do
- get :<%= action %>
- assert_response :success
- end
-
-<% end -%>
-<% end -%>
-end
-<% end -%>
diff --git a/railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb.tt b/railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb.tt
new file mode 100644
index 0000000000..ff41fef9e9
--- /dev/null
+++ b/railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb.tt
@@ -0,0 +1,23 @@
+require 'test_helper'
+
+<% module_namespacing do -%>
+class <%= class_name %>ControllerTest < ActionDispatch::IntegrationTest
+<% if mountable_engine? -%>
+ include Engine.routes.url_helpers
+
+<% end -%>
+<% if actions.empty? -%>
+ # test "the truth" do
+ # assert true
+ # end
+<% else -%>
+<% actions.each do |action| -%>
+ test "should get <%= action %>" do
+ get <%= url_helper_prefix %>_<%= action %>_url
+ assert_response :success
+ end
+
+<% end -%>
+<% end -%>
+end
+<% end -%>
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 d7307398ce..19be4f2f51 100644
--- a/railties/lib/rails/generators/test_unit/generator/generator_generator.rb
+++ b/railties/lib/rails/generators/test_unit/generator/generator_generator.rb
@@ -1,4 +1,6 @@
-require 'rails/generators/test_unit'
+# frozen_string_literal: true
+
+require "rails/generators/test_unit"
module TestUnit # :nodoc:
module Generators # :nodoc:
@@ -9,10 +11,10 @@ module TestUnit # :nodoc:
desc: "Namespace generator under lib/generators/name"
def create_generator_files
- template 'generator_test.rb', File.join('test/lib/generators', class_path, "#{file_name}_generator_test.rb")
+ 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/generator/templates/generator_test.rb b/railties/lib/rails/generators/test_unit/generator/templates/generator_test.rb.tt
index a7f1fc4fba..a7f1fc4fba 100644
--- a/railties/lib/rails/generators/test_unit/generator/templates/generator_test.rb
+++ b/railties/lib/rails/generators/test_unit/generator/templates/generator_test.rb.tt
diff --git a/railties/lib/rails/generators/test_unit/helper/helper_generator.rb b/railties/lib/rails/generators/test_unit/helper/helper_generator.rb
index bde4e88915..77308dcf7d 100644
--- a/railties/lib/rails/generators/test_unit/helper/helper_generator.rb
+++ b/railties/lib/rails/generators/test_unit/helper/helper_generator.rb
@@ -1,4 +1,6 @@
-require 'rails/generators/test_unit'
+# frozen_string_literal: true
+
+require "rails/generators/test_unit"
module TestUnit # :nodoc:
module Generators # :nodoc:
diff --git a/railties/lib/rails/generators/test_unit/integration/integration_generator.rb b/railties/lib/rails/generators/test_unit/integration/integration_generator.rb
index e004835bd5..ae307c5cd9 100644
--- a/railties/lib/rails/generators/test_unit/integration/integration_generator.rb
+++ b/railties/lib/rails/generators/test_unit/integration/integration_generator.rb
@@ -1,4 +1,6 @@
-require 'rails/generators/test_unit'
+# frozen_string_literal: true
+
+require "rails/generators/test_unit"
module TestUnit # :nodoc:
module Generators # :nodoc:
@@ -6,7 +8,7 @@ module TestUnit # :nodoc:
check_class_collision suffix: "Test"
def create_test_files
- template 'integration_test.rb', File.join('test/integration', class_path, "#{file_name}_test.rb")
+ template "integration_test.rb", File.join("test/integration", class_path, "#{file_name}_test.rb")
end
end
end
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
deleted file mode 100644
index dea7e22196..0000000000
--- a/railties/lib/rails/generators/test_unit/integration/templates/integration_test.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-require 'test_helper'
-
-class <%= class_name %>Test < ActionDispatch::IntegrationTest
- # test "the truth" do
- # assert true
- # end
-end
diff --git a/railties/lib/rails/generators/test_unit/integration/templates/integration_test.rb.tt b/railties/lib/rails/generators/test_unit/integration/templates/integration_test.rb.tt
new file mode 100644
index 0000000000..118e0f1271
--- /dev/null
+++ b/railties/lib/rails/generators/test_unit/integration/templates/integration_test.rb.tt
@@ -0,0 +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/job/job_generator.rb b/railties/lib/rails/generators/test_unit/job/job_generator.rb
index 566b61ca66..1dae3cb6a5 100644
--- a/railties/lib/rails/generators/test_unit/job/job_generator.rb
+++ b/railties/lib/rails/generators/test_unit/job/job_generator.rb
@@ -1,13 +1,20 @@
-require 'rails/generators/test_unit'
+# frozen_string_literal: true
+
+require "rails/generators/test_unit"
module TestUnit # :nodoc:
module Generators # :nodoc:
class JobGenerator < Base # :nodoc:
- check_class_collision suffix: 'JobTest'
+ check_class_collision suffix: "JobTest"
def create_test_file
- template 'unit_test.rb.erb', File.join('test/jobs', class_path, "#{file_name}_job_test.rb")
+ template "unit_test.rb", File.join("test/jobs", class_path, "#{file_name}_job_test.rb")
end
+
+ private
+ def file_name
+ @_file_name ||= super.sub(/_job\z/i, "")
+ end
end
end
end
diff --git a/railties/lib/rails/generators/test_unit/job/templates/unit_test.rb.erb b/railties/lib/rails/generators/test_unit/job/templates/unit_test.rb.tt
index f5351d0ec6..f5351d0ec6 100644
--- a/railties/lib/rails/generators/test_unit/job/templates/unit_test.rb.erb
+++ b/railties/lib/rails/generators/test_unit/job/templates/unit_test.rb.tt
diff --git a/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb b/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb
index 343c8a3949..ab8331f31c 100644
--- a/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb
+++ b/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb
@@ -1,4 +1,6 @@
-require 'rails/generators/test_unit'
+# frozen_string_literal: true
+
+require "rails/generators/test_unit"
module TestUnit # :nodoc:
module Generators # :nodoc:
@@ -10,16 +12,16 @@ module TestUnit # :nodoc:
end
def create_test_files
- template "functional_test.rb", File.join('test/mailers', class_path, "#{file_name}_mailer_test.rb")
+ template "functional_test.rb", File.join("test/mailers", class_path, "#{file_name}_mailer_test.rb")
end
def create_preview_files
- template "preview.rb", File.join('test/mailers/previews', class_path, "#{file_name}_mailer_preview.rb")
+ 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, '')
+ @_file_name ||= super.sub(/_mailer\z/i, "")
end
end
end
diff --git a/railties/lib/rails/generators/test_unit/mailer/templates/functional_test.rb b/railties/lib/rails/generators/test_unit/mailer/templates/functional_test.rb.tt
index a2f2d30de5..a2f2d30de5 100644
--- a/railties/lib/rails/generators/test_unit/mailer/templates/functional_test.rb
+++ b/railties/lib/rails/generators/test_unit/mailer/templates/functional_test.rb.tt
diff --git a/railties/lib/rails/generators/test_unit/mailer/templates/preview.rb b/railties/lib/rails/generators/test_unit/mailer/templates/preview.rb.tt
index b063cbc47b..b063cbc47b 100644
--- a/railties/lib/rails/generators/test_unit/mailer/templates/preview.rb
+++ b/railties/lib/rails/generators/test_unit/mailer/templates/preview.rb.tt
diff --git a/railties/lib/rails/generators/test_unit/model/model_generator.rb b/railties/lib/rails/generators/test_unit/model/model_generator.rb
index 086588750e..02d7502592 100644
--- a/railties/lib/rails/generators/test_unit/model/model_generator.rb
+++ b/railties/lib/rails/generators/test_unit/model/model_generator.rb
@@ -1,9 +1,10 @@
-require 'rails/generators/test_unit'
+# frozen_string_literal: true
+
+require "rails/generators/test_unit"
module TestUnit # :nodoc:
module Generators # :nodoc:
class ModelGenerator < Base # :nodoc:
-
RESERVED_YAML_KEYWORDS = %w(y yes n no true false on off null)
argument :attributes, type: :array, default: [], banner: "field:type field:type"
@@ -12,14 +13,14 @@ module TestUnit # :nodoc:
check_class_collision suffix: "Test"
def create_test_file
- template 'unit_test.rb', File.join('test/models', class_path, "#{file_name}_test.rb")
+ template "unit_test.rb", File.join("test/models", class_path, "#{file_name}_test.rb")
end
hook_for :fixture_replacement
def create_fixture_file
if options[:fixture] && options[:fixture_replacement].nil?
- template 'fixtures.yml', File.join('test/fixtures', class_path, "#{fixture_file_name}.yml")
+ template "fixtures.yml", File.join("test/fixtures", class_path, "#{fixture_file_name}.yml")
end
end
diff --git a/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml b/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml
deleted file mode 100644
index 50ca61a35b..0000000000
--- a/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml
+++ /dev/null
@@ -1,29 +0,0 @@
-# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
-<% unless attributes.empty? -%>
-<% %w(one two).each do |name| %>
-<%= name %>:
-<% attributes.each do |attribute| -%>
- <%- if attribute.password_digest? -%>
- password_digest: <%%= BCrypt::Password.create('secret') %>
- <%- elsif attribute.reference? -%>
- <%= yaml_key_value(attribute.column_name.sub(/_id$/, ''), attribute.default) %>
- <%- else -%>
- <%= yaml_key_value(attribute.column_name, attribute.default) %>
- <%- end -%>
- <%- if attribute.polymorphic? -%>
- <%= yaml_key_value("#{attribute.name}_type", attribute.human_name) %>
- <%- end -%>
-<% end -%>
-<% end -%>
-<% else -%>
-
-# This model initially had no columns defined. If you add columns to the
-# model remove the '{}' from the fixture names and add the columns immediately
-# below each fixture, per the syntax in the comments below
-#
-one: {}
-# column: value
-#
-two: {}
-# column: value
-<% end -%>
diff --git a/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml.tt b/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml.tt
new file mode 100644
index 0000000000..0681780c97
--- /dev/null
+++ b/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml.tt
@@ -0,0 +1,29 @@
+# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
+<% unless attributes.empty? -%>
+<% %w(one two).each do |name| %>
+<%= name %>:
+<% attributes.each do |attribute| -%>
+ <%- if attribute.password_digest? -%>
+ password_digest: <%%= BCrypt::Password.create('secret') %>
+ <%- elsif attribute.reference? -%>
+ <%= yaml_key_value(attribute.column_name.sub(/_id$/, ''), attribute.default || name) %>
+ <%- else -%>
+ <%= yaml_key_value(attribute.column_name, attribute.default) %>
+ <%- end -%>
+ <%- if attribute.polymorphic? -%>
+ <%= yaml_key_value("#{attribute.name}_type", attribute.human_name) %>
+ <%- end -%>
+<% end -%>
+<% end -%>
+<% else -%>
+
+# This model initially had no columns defined. If you add columns to the
+# model remove the '{}' from the fixture names and add the columns immediately
+# below each fixture, per the syntax in the comments below
+#
+one: {}
+# column: value
+#
+two: {}
+# column: value
+<% end -%>
diff --git a/railties/lib/rails/generators/test_unit/model/templates/unit_test.rb b/railties/lib/rails/generators/test_unit/model/templates/unit_test.rb.tt
index c9bc7d5b90..c9bc7d5b90 100644
--- a/railties/lib/rails/generators/test_unit/model/templates/unit_test.rb
+++ b/railties/lib/rails/generators/test_unit/model/templates/unit_test.rb.tt
diff --git a/railties/lib/rails/generators/test_unit/plugin/plugin_generator.rb b/railties/lib/rails/generators/test_unit/plugin/plugin_generator.rb
index b5d4f38444..0657bc2389 100644
--- a/railties/lib/rails/generators/test_unit/plugin/plugin_generator.rb
+++ b/railties/lib/rails/generators/test_unit/plugin/plugin_generator.rb
@@ -1,4 +1,6 @@
-require 'rails/generators/test_unit'
+# frozen_string_literal: true
+
+require "rails/generators/test_unit"
module TestUnit # :nodoc:
module Generators # :nodoc:
@@ -6,7 +8,7 @@ module TestUnit # :nodoc:
check_class_collision suffix: "Test"
def create_test_files
- directory '.', 'test'
+ directory ".", "test"
end
end
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 0171da7cc7..e2e8b18eab 100644
--- a/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb
+++ b/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb
@@ -1,5 +1,7 @@
-require 'rails/generators/test_unit'
-require 'rails/generators/resource_helpers'
+# frozen_string_literal: true
+
+require "rails/generators/test_unit"
+require "rails/generators/resource_helpers"
module TestUnit # :nodoc:
module Generators # :nodoc:
@@ -11,18 +13,25 @@ module TestUnit # :nodoc:
class_option :api, type: :boolean,
desc: "Generates API functional tests"
+ class_option :system_tests, type: :string,
+ desc: "Skip system test files"
+
argument :attributes, type: :array, default: [], banner: "field:type field:type"
def create_test_files
template_file = options.api? ? "api_functional_test.rb" : "functional_test.rb"
template template_file,
File.join("test/controllers", controller_class_path, "#{controller_file_name}_controller_test.rb")
+
+ if !options.api? && options[:system_tests]
+ template "system_test.rb", File.join("test/system", class_path, "#{file_name.pluralize}_test.rb")
+ end
end
def fixture_name
@fixture_name ||=
if mountable_engine?
- "%s_%s" % [namespaced_path, table_name]
+ (namespace_dirs + [table_name]).join("_")
else
table_name
end
@@ -30,16 +39,20 @@ module TestUnit # :nodoc:
private
+ def attributes_string
+ attributes_hash.map { |k, v| "#{k}: #{v}" }.join(", ")
+ end
+
def attributes_hash
- return if attributes_names.empty?
+ return {} if attributes_names.empty?
attributes_names.map do |name|
if %w(password password_confirmation).include?(name) && attributes.any?(&:password_digest?)
- "#{name}: 'secret'"
+ ["#{name}", "'secret'"]
else
- "#{name}: @#{singular_table_name}.#{name}"
+ ["#{name}", "@#{singular_table_name}.#{name}"]
end
- end.sort.join(', ')
+ end.sort.to_h
end
end
end
diff --git a/railties/lib/rails/generators/test_unit/scaffold/templates/api_functional_test.rb b/railties/lib/rails/generators/test_unit/scaffold/templates/api_functional_test.rb
deleted file mode 100644
index 896b38bc8f..0000000000
--- a/railties/lib/rails/generators/test_unit/scaffold/templates/api_functional_test.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-require 'test_helper'
-
-<% module_namespacing do -%>
-class <%= controller_class_name %>ControllerTest < ActionController::TestCase
- setup do
- @<%= singular_table_name %> = <%= table_name %>(:one)
- end
-
- test "should get index" do
- get :index
- assert_response :success
- end
-
- test "should create <%= singular_table_name %>" do
- assert_difference('<%= class_name %>.count') do
- post :create, params: { <%= "#{singular_table_name}: { #{attributes_hash} }" %> }
- end
-
- assert_response 201
- end
-
- test "should show <%= singular_table_name %>" do
- get :show, params: { id: <%= "@#{singular_table_name}" %> }
- assert_response :success
- end
-
- test "should update <%= singular_table_name %>" do
- patch :update, params: { id: <%= "@#{singular_table_name}" %>, <%= "#{singular_table_name}: { #{attributes_hash} }" %> }
- assert_response 200
- end
-
- test "should destroy <%= singular_table_name %>" do
- assert_difference('<%= class_name %>.count', -1) do
- delete :destroy, params: { id: <%= "@#{singular_table_name}" %> }
- end
-
- assert_response 204
- end
-end
-<% end -%>
diff --git a/railties/lib/rails/generators/test_unit/scaffold/templates/api_functional_test.rb.tt b/railties/lib/rails/generators/test_unit/scaffold/templates/api_functional_test.rb.tt
new file mode 100644
index 0000000000..f21861d8e6
--- /dev/null
+++ b/railties/lib/rails/generators/test_unit/scaffold/templates/api_functional_test.rb.tt
@@ -0,0 +1,44 @@
+require 'test_helper'
+
+<% module_namespacing do -%>
+class <%= controller_class_name %>ControllerTest < ActionDispatch::IntegrationTest
+ <%- if mountable_engine? -%>
+ include Engine.routes.url_helpers
+
+ <%- end -%>
+ setup do
+ @<%= singular_table_name %> = <%= fixture_name %>(:one)
+ end
+
+ test "should get index" do
+ get <%= index_helper %>_url, as: :json
+ assert_response :success
+ end
+
+ test "should create <%= singular_table_name %>" do
+ assert_difference('<%= class_name %>.count') do
+ post <%= index_helper %>_url, params: { <%= "#{singular_table_name}: { #{attributes_string} }" %> }, as: :json
+ end
+
+ assert_response 201
+ end
+
+ test "should show <%= singular_table_name %>" do
+ get <%= show_helper %>, as: :json
+ assert_response :success
+ end
+
+ test "should update <%= singular_table_name %>" do
+ patch <%= show_helper %>, params: { <%= "#{singular_table_name}: { #{attributes_string} }" %> }, as: :json
+ assert_response 200
+ end
+
+ test "should destroy <%= singular_table_name %>" do
+ assert_difference('<%= class_name %>.count', -1) do
+ delete <%= show_helper %>, as: :json
+ end
+
+ assert_response 204
+ end
+end
+<% end -%>
diff --git a/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb b/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb
deleted file mode 100644
index 50b98b2631..0000000000
--- a/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-require 'test_helper'
-
-<% module_namespacing do -%>
-class <%= controller_class_name %>ControllerTest < ActionController::TestCase
- setup do
- @<%= singular_table_name %> = <%= fixture_name %>(:one)
-<% if mountable_engine? -%>
- @routes = Engine.routes
-<% end -%>
- end
-
- test "should get index" do
- get :index
- assert_response :success
- end
-
- test "should get new" do
- get :new
- assert_response :success
- end
-
- test "should create <%= singular_table_name %>" do
- assert_difference('<%= class_name %>.count') do
- post :create, params: { <%= "#{singular_table_name}: { #{attributes_hash} }" %> }
- end
-
- assert_redirected_to <%= singular_table_name %>_path(<%= class_name %>.last)
- end
-
- test "should show <%= singular_table_name %>" do
- get :show, params: { id: <%= "@#{singular_table_name}" %> }
- assert_response :success
- end
-
- test "should get edit" do
- get :edit, params: { id: <%= "@#{singular_table_name}" %> }
- assert_response :success
- end
-
- test "should update <%= singular_table_name %>" do
- patch :update, params: { id: <%= "@#{singular_table_name}" %>, <%= "#{singular_table_name}: { #{attributes_hash} }" %> }
- assert_redirected_to <%= singular_table_name %>_path(<%= "@#{singular_table_name}" %>)
- end
-
- test "should destroy <%= singular_table_name %>" do
- assert_difference('<%= class_name %>.count', -1) do
- delete :destroy, params: { id: <%= "@#{singular_table_name}" %> }
- end
-
- assert_redirected_to <%= index_helper %>_path
- end
-end
-<% end -%>
diff --git a/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb.tt b/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb.tt
new file mode 100644
index 0000000000..195d60be20
--- /dev/null
+++ b/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb.tt
@@ -0,0 +1,54 @@
+require 'test_helper'
+
+<% module_namespacing do -%>
+class <%= controller_class_name %>ControllerTest < ActionDispatch::IntegrationTest
+ <%- if mountable_engine? -%>
+ include Engine.routes.url_helpers
+
+ <%- end -%>
+ setup do
+ @<%= singular_table_name %> = <%= fixture_name %>(:one)
+ end
+
+ test "should get index" do
+ get <%= index_helper %>_url
+ assert_response :success
+ end
+
+ test "should get new" do
+ get <%= new_helper %>
+ assert_response :success
+ end
+
+ test "should create <%= singular_table_name %>" do
+ assert_difference('<%= class_name %>.count') do
+ post <%= index_helper %>_url, params: { <%= "#{singular_table_name}: { #{attributes_string} }" %> }
+ end
+
+ assert_redirected_to <%= singular_table_name %>_url(<%= class_name %>.last)
+ end
+
+ test "should show <%= singular_table_name %>" do
+ get <%= show_helper %>
+ assert_response :success
+ end
+
+ test "should get edit" do
+ get <%= edit_helper %>
+ assert_response :success
+ end
+
+ test "should update <%= singular_table_name %>" do
+ patch <%= show_helper %>, params: { <%= "#{singular_table_name}: { #{attributes_string} }" %> }
+ assert_redirected_to <%= singular_table_name %>_url(<%= "@#{singular_table_name}" %>)
+ end
+
+ test "should destroy <%= singular_table_name %>" do
+ assert_difference('<%= class_name %>.count', -1) do
+ delete <%= show_helper %>
+ end
+
+ assert_redirected_to <%= index_helper %>_url
+ end
+end
+<% end -%>
diff --git a/railties/lib/rails/generators/test_unit/scaffold/templates/system_test.rb.tt b/railties/lib/rails/generators/test_unit/scaffold/templates/system_test.rb.tt
new file mode 100644
index 0000000000..15bd7956b6
--- /dev/null
+++ b/railties/lib/rails/generators/test_unit/scaffold/templates/system_test.rb.tt
@@ -0,0 +1,49 @@
+require "application_system_test_case"
+
+<% module_namespacing do -%>
+class <%= class_name.pluralize %>Test < ApplicationSystemTestCase
+ setup do
+ @<%= singular_table_name %> = <%= fixture_name %>(:one)
+ end
+
+ test "visiting the index" do
+ visit <%= plural_table_name %>_url
+ assert_selector "h1", text: "<%= class_name.pluralize.titleize %>"
+ end
+
+ test "creating a <%= human_name %>" do
+ visit <%= plural_table_name %>_url
+ click_on "New <%= class_name.titleize %>"
+
+ <%- attributes_hash.each do |attr, value| -%>
+ fill_in "<%= attr.humanize %>", with: <%= value %>
+ <%- end -%>
+ click_on "Create <%= human_name %>"
+
+ assert_text "<%= human_name %> was successfully created"
+ click_on "Back"
+ end
+
+ test "updating a <%= human_name %>" do
+ visit <%= plural_table_name %>_url
+ click_on "Edit", match: :first
+
+ <%- attributes_hash.each do |attr, value| -%>
+ fill_in "<%= attr.humanize %>", with: <%= value %>
+ <%- end -%>
+ click_on "Update <%= human_name %>"
+
+ assert_text "<%= human_name %> was successfully updated"
+ click_on "Back"
+ end
+
+ test "destroying a <%= human_name %>" do
+ visit <%= plural_table_name %>_url
+ page.accept_confirm do
+ click_on "Destroy", match: :first
+ end
+
+ assert_text "<%= human_name %> was successfully destroyed"
+ end
+end
+<% 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..08504d4124
--- /dev/null
+++ b/railties/lib/rails/generators/test_unit/system/system_generator.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+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", class_path, "#{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.tt b/railties/lib/rails/generators/test_unit/system/templates/application_system_test_case.rb.tt
new file mode 100644
index 0000000000..d19212abd5
--- /dev/null
+++ b/railties/lib/rails/generators/test_unit/system/templates/application_system_test_case.rb.tt
@@ -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.tt b/railties/lib/rails/generators/test_unit/system/templates/system_test.rb.tt
new file mode 100644
index 0000000000..b5ce2ba5c8
--- /dev/null
+++ b/railties/lib/rails/generators/test_unit/system/templates/system_test.rb.tt
@@ -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/assertions.rb b/railties/lib/rails/generators/testing/assertions.rb
index 17af6eddfa..c4cff9090b 100644
--- a/railties/lib/rails/generators/testing/assertions.rb
+++ b/railties/lib/rails/generators/testing/assertions.rb
@@ -1,4 +1,4 @@
-require 'shellwords'
+# frozen_string_literal: true
module Rails
module Generators
@@ -31,10 +31,10 @@ module Rails
contents.each do |content|
case content
- when String
- assert_equal content, read
- when Regexp
- assert_match content, read
+ when String
+ assert_equal content, read
+ when Regexp
+ assert_match content, read
end
end
end
@@ -115,7 +115,11 @@ module Rails
#
# assert_field_default_value :string, "MyString"
def assert_field_default_value(attribute_type, value)
- assert_equal(value, create_generated_attribute(attribute_type).default)
+ if value.nil?
+ assert_nil(create_generated_attribute(attribute_type).default)
+ else
+ assert_equal(value, create_generated_attribute(attribute_type).default)
+ end
end
end
end
diff --git a/railties/lib/rails/generators/testing/behaviour.rb b/railties/lib/rails/generators/testing/behaviour.rb
index c9700e1cd7..ec29ad12ba 100644
--- a/railties/lib/rails/generators/testing/behaviour.rb
+++ b/railties/lib/rails/generators/testing/behaviour.rb
@@ -1,10 +1,12 @@
-require 'active_support/core_ext/class/attribute'
-require 'active_support/core_ext/module/delegation'
-require 'active_support/core_ext/hash/reverse_merge'
-require 'active_support/core_ext/kernel/reporting'
-require 'active_support/testing/stream'
-require 'active_support/concern'
-require 'rails/generators'
+# frozen_string_literal: true
+
+require "active_support/core_ext/class/attribute"
+require "active_support/core_ext/module/delegation"
+require "active_support/core_ext/hash/reverse_merge"
+require "active_support/core_ext/kernel/reporting"
+require "active_support/testing/stream"
+require "active_support/concern"
+require "rails/generators"
module Rails
module Generators
@@ -14,12 +16,12 @@ module Rails
include ActiveSupport::Testing::Stream
included do
- class_attribute :destination_root, :current_path, :generator_class, :default_arguments
-
# Generators frequently change the current path using +FileUtils.cd+.
# So we need to store the path at file load and revert back to it after each test.
- self.current_path = File.expand_path(Dir.pwd)
- self.default_arguments = []
+ class_attribute :current_path, default: File.expand_path(Dir.pwd)
+ class_attribute :default_arguments, default: []
+ class_attribute :destination_root
+ class_attribute :generator_class
end
module ClassMethods
@@ -40,7 +42,7 @@ module Rails
# Sets the destination of generator files:
#
- # destination File.expand_path("../tmp", File.dirname(__FILE__))
+ # destination File.expand_path("../tmp", __dir__)
def destination(path)
self.destination_root = path
end
@@ -51,7 +53,7 @@ module Rails
#
# class AppGeneratorTest < Rails::Generators::TestCase
# tests AppGenerator
- # destination File.expand_path("../tmp", File.dirname(__FILE__))
+ # destination File.expand_path("../tmp", __dir__)
# setup :prepare_destination
#
# test "database.yml is not created when skipping Active Record" do
@@ -62,47 +64,50 @@ module Rails
#
# You can provide a configuration hash as second argument. This method returns the output
# printed by the generator.
- def run_generator(args=self.default_arguments, config={})
+ def run_generator(args = default_arguments, config = {})
capture(:stdout) do
- args += ['--skip-bundle'] unless args.include? '--dev'
- self.generator_class.start(args, config.reverse_merge(destination_root: destination_root))
+ args += ["--skip-bundle"] unless args.include? "--dev"
+ args |= ["--skip-bootsnap"] unless args.include? "--no-skip-bootsnap"
+ args |= ["--skip-webpack-install"] unless args.include? "--no-skip-webpack-install"
+
+ generator_class.start(args, config.reverse_merge(destination_root: destination_root))
end
end
# Instantiate the generator.
- def generator(args=self.default_arguments, options={}, config={})
- @generator ||= self.generator_class.new(args, options, config.reverse_merge(destination_root: destination_root))
+ def generator(args = default_arguments, options = {}, config = {})
+ @generator ||= generator_class.new(args, options, config.reverse_merge(destination_root: destination_root))
end
# Create a Rails::Generators::GeneratedAttribute by supplying the
# attribute type and, optionally, the attribute name:
#
# create_generated_attribute(:string, 'name')
- def create_generated_attribute(attribute_type, name = 'test', index = nil)
- Rails::Generators::GeneratedAttribute.parse([name, attribute_type, index].compact.join(':'))
+ def create_generated_attribute(attribute_type, name = "test", index = nil)
+ 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
- def prepare_destination # :nodoc:
+ # Clears all files and directories in 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$/, '')
+ dirname, file_name = File.dirname(absolute), File.basename(absolute).sub(/\.rb$/, "")
Dir.glob("#{dirname}/[0-9]*_*.rb").grep(/\d+_#{file_name}.rb$/).first
end
-
end
end
end
diff --git a/railties/lib/rails/generators/testing/setup_and_teardown.rb b/railties/lib/rails/generators/testing/setup_and_teardown.rb
index 73102a283f..4374aa5b8c 100644
--- a/railties/lib/rails/generators/testing/setup_and_teardown.rb
+++ b/railties/lib/rails/generators/testing/setup_and_teardown.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Rails
module Generators
module Testing
diff --git a/railties/lib/rails/info.rb b/railties/lib/rails/info.rb
index 5909446b66..b8173c8d11 100644
--- a/railties/lib/rails/info.rb
+++ b/railties/lib/rails/info.rb
@@ -1,12 +1,15 @@
+# frozen_string_literal: true
+
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 = [])
+ mattr_accessor :properties, default: []
+
+ class << @@properties
def names
map(&:first)
end
@@ -38,65 +41,69 @@ module Rails
alias inspect to_s
def to_html
- '<table>'.tap do |table|
+ (+"<table>").tap do |table|
properties.each do |(name, value)|
table << %(<tr><td class="name">#{CGI.escapeHTML(name.to_s)}</td>)
formatted_value = if value.kind_of?(Array)
- "<ul>" + value.map { |v| "<li>#{CGI.escapeHTML(v.to_s)}</li>" }.join + "</ul>"
- else
- CGI.escapeHTML(value.to_s)
- end
+ "<ul>" + value.map { |v| "<li>#{CGI.escapeHTML(v.to_s)}</li>" }.join + "</ul>"
+ else
+ CGI.escapeHTML(value.to_s)
+ end
table << %(<td class="value">#{formatted_value}</td></tr>)
end
- table << '</table>'
+ table << "</table>"
end
end
+
+ def to_json
+ Hash[properties].to_json
+ end
end
# The Rails version.
- property 'Rails version' do
+ property "Rails version" do
Rails.version.to_s
end
# The Ruby version and platform, e.g. "2.0.0-p247 (x86_64-darwin12.4.0)".
- property 'Ruby version' do
+ property "Ruby version" do
"#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL} (#{RUBY_PLATFORM})"
end
# The RubyGems version, if it's installed.
- property 'RubyGems version' do
+ property "RubyGems version" do
Gem::RubyGemsVersion
end
- property 'Rack version' do
+ property "Rack version" do
::Rack.release
end
- property 'JavaScript Runtime' do
+ property "JavaScript Runtime" do
ExecJS.runtime.name
end
- property 'Middleware' do
+ property "Middleware" do
Rails.configuration.middleware.map(&:inspect)
end
# The application's location on the filesystem.
- property 'Application root' do
+ property "Application root" do
File.expand_path(Rails.root)
end
# The current Rails environment (development, test, or production).
- property 'Environment' do
+ property "Environment" do
Rails.env
end
# The name of the database adapter for the current environment.
- property 'Database adapter' do
- ActiveRecord::Base.configurations[Rails.env]['adapter']
+ property "Database adapter" do
+ ActiveRecord::Base.configurations[Rails.env]["adapter"]
end
- property 'Database schema version' do
- ActiveRecord::Migrator.current_version rescue nil
+ property "Database schema version" do
+ ActiveRecord::Base.connection.migration_context.current_version rescue nil
end
end
end
diff --git a/railties/lib/rails/info_controller.rb b/railties/lib/rails/info_controller.rb
index 778105c5f7..50fe176946 100644
--- a/railties/lib/rails/info_controller.rb
+++ b/railties/lib/rails/info_controller.rb
@@ -1,9 +1,11 @@
-require 'rails/application_controller'
-require 'action_dispatch/routing/inspector'
+# frozen_string_literal: true
+
+require "rails/application_controller"
+require "action_dispatch/routing/inspector"
class Rails::InfoController < Rails::ApplicationController # :nodoc:
prepend_view_path ActionDispatch::DebugExceptions::RESCUES_TEMPLATE_PATH
- layout -> { request.xhr? ? false : 'application' }
+ layout -> { request.xhr? ? false : "application" }
before_action :require_local!
@@ -12,8 +14,16 @@ class Rails::InfoController < Rails::ApplicationController # :nodoc:
end
def properties
- @info = Rails::Info.to_html
- @page_title = 'Properties'
+ respond_to do |format|
+ format.html do
+ @info = Rails::Info.to_html
+ @page_title = "Properties"
+ end
+
+ format.json do
+ render json: Rails::Info.to_json
+ end
+ end
end
def routes
@@ -21,24 +31,24 @@ class Rails::InfoController < Rails::ApplicationController # :nodoc:
path = URI.parser.escape path
normalized_path = with_leading_slash path
render json: {
- exact: match_route {|it| it.match normalized_path },
- fuzzy: match_route {|it| it.spec.to_s.match path }
+ exact: match_route { |it| it.match normalized_path },
+ fuzzy: match_route { |it| it.spec.to_s.match path }
}
else
@routes_inspector = ActionDispatch::Routing::RoutesInspector.new(_routes.routes)
- @page_title = 'Routes'
+ @page_title = "Routes"
end
end
private
- def match_route
- _routes.routes.select {|route|
- yield route.path
- }.map {|route| route.path.spec.to_s }
- end
+ def match_route
+ _routes.routes.select { |route|
+ yield route.path
+ }.map { |route| route.path.spec.to_s }
+ end
- def with_leading_slash(path)
- ('/' + path).squeeze('/')
- end
+ def with_leading_slash(path)
+ ("/" + path).squeeze("/")
+ end
end
diff --git a/railties/lib/rails/initializable.rb b/railties/lib/rails/initializable.rb
index 1a0b6d1e1a..5410e17153 100644
--- a/railties/lib/rails/initializable.rb
+++ b/railties/lib/rails/initializable.rb
@@ -1,4 +1,6 @@
-require 'tsort'
+# frozen_string_literal: true
+
+require "tsort"
module Rails
module Initializable
@@ -34,6 +36,10 @@ module Rails
return self if @context
Initializer.new(@name, context, @options, &block)
end
+
+ def context_class
+ @context.class
+ end
end
class Collection < Array
@@ -49,7 +55,7 @@ module Rails
end
end
- def run_initializers(group=:default, *args)
+ def run_initializers(group = :default, *args)
return if instance_variable_defined?(:@ran)
initializers.tsort_each do |initializer|
initializer.run(*args) if initializer.belongs_to?(group)
diff --git a/railties/lib/rails/mailers_controller.rb b/railties/lib/rails/mailers_controller.rb
index 41422a656c..e2d36d7654 100644
--- a/railties/lib/rails/mailers_controller.rb
+++ b/railties/lib/rails/mailers_controller.rb
@@ -1,10 +1,16 @@
-require 'rails/application_controller'
+# frozen_string_literal: true
+
+require "rails/application_controller"
class Rails::MailersController < Rails::ApplicationController # :nodoc:
prepend_view_path ActionDispatch::DebugExceptions::RESCUES_TEMPLATE_PATH
- before_action :require_local!
- before_action :find_preview, only: :preview
+ before_action :require_local!, unless: :show_previews?
+ before_action :find_preview, :set_locale, only: :preview
+
+ helper_method :part_query, :locale_query
+
+ content_security_policy(false)
def index
@previews = ActionMailer::Preview.all
@@ -14,25 +20,25 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc:
def preview
if params[:path] == @preview.preview_name
@page_title = "Mailer Previews for #{@preview.preview_name}"
- render action: 'mailer'
+ render action: "mailer"
else
@email_action = File.basename(params[:path])
if @preview.email_exists?(@email_action)
- @email = @preview.call(@email_action)
+ @email = @preview.call(@email_action, params)
if params[:part]
part_type = Mime::Type.lookup(params[:part])
if part = find_part(part_type)
response.content_type = part_type
- render text: part.respond_to?(:decoded) ? part.decoded : part
+ render plain: part.respond_to?(:decoded) ? part.decoded : part
else
raise AbstractController::ActionNotFound, "Email part '#{part_type}' not found in #{@preview.name}##{@email_action}"
end
else
- @part = find_preferred_part(request.format, Mime::HTML, Mime::TEXT)
- render action: 'email', layout: false, formats: %w[html]
+ @part = find_preferred_part(request.format, Mime[:html], Mime[:text])
+ render action: "email", layout: false, formats: %w[html]
end
else
raise AbstractController::ActionNotFound, "Email '#{@email_action}' not found in #{@preview.name}"
@@ -40,11 +46,15 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc:
end
end
- protected
- def find_preview
+ private
+ def show_previews? # :doc:
+ ActionMailer::Base.show_previews
+ end
+
+ def find_preview # :doc:
candidates = []
- params[:path].to_s.scan(%r{/|$}){ candidates << $` }
- preview = candidates.detect{ |candidate| ActionMailer::Preview.exists?(candidate) }
+ params[:path].to_s.scan(%r{/|$}) { candidates << $` }
+ preview = candidates.detect { |candidate| ActionMailer::Preview.exists?(candidate) }
if preview
@preview = ActionMailer::Preview.find(preview)
@@ -53,23 +63,35 @@ 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
end
end
- if formats.any?{ |f| @email.mime_type == f }
+ if formats.any? { |f| @email.mime_type == f }
@email
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
@email
end
end
+
+ def part_query(mime_type)
+ request.query_parameters.merge(part: mime_type).to_query
+ end
+
+ def locale_query(locale)
+ request.query_parameters.merge(locale: locale).to_query
+ end
+
+ def set_locale
+ I18n.locale = params[:locale] || I18n.default_locale
+ end
end
diff --git a/railties/lib/rails/paths.rb b/railties/lib/rails/paths.rb
index ebcaaaba46..8367ac8980 100644
--- a/railties/lib/rails/paths.rb
+++ b/railties/lib/rails/paths.rb
@@ -1,13 +1,15 @@
+# frozen_string_literal: true
+
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"]
@@ -30,7 +32,7 @@ module Rails
# root["config/routes"].inspect # => ["config/routes.rb"]
#
# The +add+ method accepts the following options as arguments:
- # eager_load, autoload, autoload_once and glob.
+ # eager_load, autoload, autoload_once, and glob.
#
# Finally, the +Path+ object also provides a few helpers:
#
@@ -45,7 +47,6 @@ module Rails
attr_accessor :path
def initialize(path)
- @current = nil
@path = path
@root = {}
end
@@ -112,10 +113,11 @@ module Rails
attr_accessor :glob
def initialize(root, current, paths, options = {})
- @paths = paths
- @current = current
- @root = root
- @glob = options[:glob]
+ @paths = paths
+ @current = current
+ @root = root
+ @glob = options[:glob]
+ @exclude = options[:exclude]
options[:autoload_once] ? autoload_once! : skip_autoload_once!
options[:eager_load] ? eager_load! : skip_eager_load!
@@ -123,6 +125,10 @@ module Rails
options[:load_path] ? load_path! : skip_load_path!
end
+ def absolute_current # :nodoc:
+ File.expand_path(@current, @root.path)
+ end
+
def children
keys = @root.keys.find_all { |k|
k.start_with?(@current) && k != @current
@@ -175,18 +181,20 @@ module Rails
@paths
end
+ def extensions # :nodoc:
+ $1.split(",") if @glob =~ /\{([\S]+)\}/
+ end
+
# Expands all paths against the root and return all unique values.
def expanded
raise "You need to set a path root" unless @root.path
result = []
- each do |p|
- path = File.expand_path(p, @root.path)
+ each do |path|
+ path = File.expand_path(path, @root.path)
if @glob && File.directory?(path)
- Dir.chdir(path) do
- result.concat(Dir.glob(@glob).map { |file| File.join path, file }.sort)
- end
+ result.concat files_in(path)
else
result << path
end
@@ -198,7 +206,14 @@ module Rails
# Returns all expanded paths but only if they exist in the filesystem.
def existent
- expanded.select { |f| File.exist?(f) }
+ expanded.select do |f|
+ does_exist = File.exist?(f)
+
+ if !does_exist && File.symlink?(f)
+ raise "File #{f.inspect} is a symlink that does not point to a valid file"
+ end
+ does_exist
+ end
end
def existent_directories
@@ -206,6 +221,17 @@ module Rails
end
alias to_a expanded
+
+ private
+
+ def files_in(path)
+ Dir.chdir(path) do
+ files = Dir.glob(@glob)
+ files -= @exclude if @exclude
+ files.map! { |file| File.join(path, file) }
+ files.sort
+ end
+ end
end
end
end
diff --git a/railties/lib/rails/plugin/test.rb b/railties/lib/rails/plugin/test.rb
new file mode 100644
index 0000000000..18b6fd1757
--- /dev/null
+++ b/railties/lib/rails/plugin/test.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require "rails/test_unit/runner"
+require "rails/test_unit/reporter"
+
+Rails::TestUnitReporter.executable = "bin/test"
+
+Rails::TestUnit::Runner.parse_options(ARGV)
+Rails::TestUnit::Runner.run(ARGV)
diff --git a/railties/lib/rails/rack.rb b/railties/lib/rails/rack.rb
index a4c4527a72..579fb25cc4 100644
--- a/railties/lib/rails/rack.rb
+++ b/railties/lib/rails/rack.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Rails
module Rack
autoload :Logger, "rails/rack/logger"
diff --git a/railties/lib/rails/rack/debugger.rb b/railties/lib/rails/rack/debugger.rb
deleted file mode 100644
index 1fde3db070..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 12676b18bc..3a95b55811 100644
--- a/railties/lib/rails/rack/logger.rb
+++ b/railties/lib/rails/rack/logger.rb
@@ -1,8 +1,10 @@
-require 'active_support/core_ext/time/conversions'
-require 'active_support/core_ext/object/blank'
-require 'active_support/log_subscriber'
-require 'action_dispatch/http/request'
-require 'rack/body_proxy'
+# frozen_string_literal: true
+
+require "active_support/core_ext/time/conversions"
+require "active_support/core_ext/object/blank"
+require "active_support/log_subscriber"
+require "action_dispatch/http/request"
+require "rack/body_proxy"
module Rails
module Rack
@@ -27,64 +29,52 @@ module Rails
end
end
- protected
+ private
- def call_app(request, env)
- # Put some space between requests in development logs.
- if development?
- logger.debug ''
- logger.debug ''
+ def call_app(request, env) # :doc:
+ instrumenter = ActiveSupport::Notifications.instrumenter
+ instrumenter.start "request.action_dispatch", request: request
+ logger.info { started_request_message(request) }
+ status, headers, body = @app.call(env)
+ body = ::Rack::BodyProxy.new(body) { finish(request) }
+ [status, headers, body]
+ rescue Exception
+ finish(request)
+ raise
+ ensure
+ ActiveSupport::LogSubscriber.flush_all!
end
- 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.remote_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
- instrumenter.finish 'request.action_dispatch', request: request
- end
-
- def development?
- Rails.env.development?
- end
+ def finish(request)
+ instrumenter = ActiveSupport::Notifications.instrumenter
+ instrumenter.finish "request.action_dispatch", request: request
+ end
- def logger
- Rails.logger
- end
+ def logger
+ Rails.logger
+ end
end
end
end
diff --git a/railties/lib/rails/railtie.rb b/railties/lib/rails/railtie.rb
index 8c24d1d56d..a67b90e285 100644
--- a/railties/lib/rails/railtie.rb
+++ b/railties/lib/rails/railtie.rb
@@ -1,38 +1,39 @@
-require 'rails/initializable'
-require 'rails/configuration'
-require 'active_support/inflector'
-require 'active_support/core_ext/module/introspection'
-require 'active_support/core_ext/module/delegation'
+# frozen_string_literal: true
+
+require "rails/initializable"
+require "active_support/inflector"
+require "active_support/core_ext/module/introspection"
+require "active_support/core_ext/module/delegation"
module Rails
- # Railtie is the core of the Rails framework and provides several hooks to extend
- # Rails and/or modify the initialization process.
+ # <tt>Rails::Railtie</tt> is the core of the Rails framework and provides
+ # several hooks to extend Rails and/or modify the initialization process.
#
- # Every major component of Rails (Action Mailer, Action Controller,
- # Action View and Active Record) is a Railtie. Each of
- # them is responsible for their own initialization. This makes Rails itself
- # absent of any component hooks, allowing other components to be used in
- # place of any of the Rails defaults.
+ # Every major component of Rails (Action Mailer, Action Controller, Active
+ # Record, etc.) implements a railtie. Each of them is responsible for their
+ # own initialization. This makes Rails itself absent of any component hooks,
+ # allowing other components to be used in place of any of the Rails defaults.
#
- # Developing a Rails extension does _not_ require any implementation of
- # Railtie, but if you need to interact with the Rails framework during
- # or after boot, then Railtie is needed.
+ # Developing a Rails extension does _not_ require implementing a railtie, but
+ # if you need to interact with the Rails framework during or after boot, then
+ # a railtie is needed.
#
- # For example, an extension doing any of the following would require Railtie:
+ # For example, an extension doing any of the following would need a railtie:
#
# * creating initializers
# * configuring a Rails framework for the application, like setting a generator
# * adding <tt>config.*</tt> keys to the environment
- # * setting up a subscriber with ActiveSupport::Notifications
- # * adding rake tasks
+ # * setting up a subscriber with <tt>ActiveSupport::Notifications</tt>
+ # * adding Rake tasks
#
- # == Creating your Railtie
+ # == Creating a Railtie
#
- # To extend Rails using Railtie, create a Railtie class which inherits
- # from Rails::Railtie within your extension's namespace. This class must be
- # loaded during the Rails boot process.
+ # To extend Rails using a railtie, create a subclass of <tt>Rails::Railtie</tt>.
+ # This class must be loaded during the Rails boot process, and is conventionally
+ # called <tt>MyNamespace::Railtie</tt>.
#
- # The following example demonstrates an extension which can be used with or without Rails.
+ # The following example demonstrates an extension which can be used with or
+ # without Rails.
#
# # lib/my_gem/railtie.rb
# module MyGem
@@ -45,8 +46,8 @@ module Rails
#
# == Initializers
#
- # To add an initialization step from your Railtie to Rails boot process, you just need
- # to create an initializer block:
+ # To add an initialization step to the Rails boot process from your railtie, just
+ # define the initialization code with the +initializer+ macro:
#
# class MyRailtie < Rails::Railtie
# initializer "my_railtie.configure_rails_initialization" do
@@ -55,7 +56,7 @@ module Rails
# end
#
# If specified, the block can also receive the application object, in case you
- # need to access some application specific configuration, like middleware:
+ # need to access some application-specific configuration, like middleware:
#
# class MyRailtie < Rails::Railtie
# initializer "my_railtie.configure_rails_initialization" do |app|
@@ -63,54 +64,57 @@ module Rails
# end
# end
#
- # Finally, you can also pass <tt>:before</tt> and <tt>:after</tt> as option to initializer,
- # in case you want to couple it with a specific step in the initialization process.
+ # Finally, you can also pass <tt>:before</tt> and <tt>:after</tt> as options to
+ # +initializer+, in case you want to couple it with a specific step in the
+ # initialization process.
#
# == Configuration
#
- # Inside the Railtie class, you can access a config object which contains configuration
- # shared by all railties and the application:
+ # Railties can access a config object which contains configuration shared by all
+ # railties and the application:
#
# class MyRailtie < Rails::Railtie
# # Customize the ORM
# config.app_generators.orm :my_railtie_orm
#
# # Add a to_prepare block which is executed once in production
- # # and before each request in development
+ # # and before each request in development.
# config.to_prepare do
# MyRailtie.setup!
# end
# end
#
- # == Loading rake tasks and generators
+ # == Loading Rake Tasks and Generators
#
- # If your railtie has rake tasks, you can tell Rails to load them through the method
- # rake_tasks:
+ # If your railtie has Rake tasks, you can tell Rails to load them through the method
+ # +rake_tasks+:
#
# class MyRailtie < Rails::Railtie
# rake_tasks do
- # load "path/to/my_railtie.tasks"
+ # load 'path/to/my_railtie.tasks'
# end
# end
#
# By default, Rails loads generators from your load path. However, if you want to place
- # your generators at a different location, you can specify in your Railtie a block which
+ # your generators at a different location, you can specify in your railtie a block which
# will load them during normal generators lookup:
#
# class MyRailtie < Rails::Railtie
# generators do
- # require "path/to/my_railtie_generator"
+ # require 'path/to/my_railtie_generator'
# end
# end
#
+ # Since filenames on the load path are shared across gems, be sure that files you load
+ # through a railtie have unique names.
+ #
# == Application and Engine
#
- # A Rails::Engine is nothing more than a Railtie with some initializers already set.
- # And since Rails::Application is an engine, the same configuration described here
- # can be used in both.
+ # An engine is nothing more than a railtie with some initializers already set. And since
+ # <tt>Rails::Application</tt> is an engine, the same configuration described here can be
+ # used in both.
#
# Be sure to look at the documentation of those specific classes for more information.
- #
class Railtie
autoload :Configuration, "rails/railtie/configuration"
@@ -133,27 +137,19 @@ module Rails
end
def rake_tasks(&blk)
- @rake_tasks ||= []
- @rake_tasks << blk if blk
- @rake_tasks
+ register_block_for(:rake_tasks, &blk)
end
def console(&blk)
- @load_console ||= []
- @load_console << blk if blk
- @load_console
+ register_block_for(:load_console, &blk)
end
def runner(&blk)
- @load_runner ||= []
- @load_runner << blk if blk
- @load_runner
+ register_block_for(:runner, &blk)
end
def generators(&blk)
- @generators ||= []
- @generators << blk if blk
- @generators
+ register_block_for(:generators, &blk)
end
def abstract_railtie?
@@ -171,10 +167,6 @@ module Rails
@instance ||= new
end
- def respond_to_missing?(*args)
- instance.respond_to?(*args) || super
- end
-
# Allows you to configure the railtie. This is the same method seen in
# Railtie::Configurable, but this module is no longer required for all
# subclasses of Railtie so we provide the class method here.
@@ -182,11 +174,15 @@ module Rails
instance.configure(&block)
end
- protected
+ private
def generate_railtie_name(string)
ActiveSupport::Inflector.underscore(string).tr("/", "_")
end
+ def respond_to_missing?(name, _)
+ instance.respond_to?(name) || super
+ end
+
# If the class method does not have a method, then send the method call
# to the Railtie instance.
def method_missing(name, *args, &block)
@@ -196,55 +192,69 @@ module Rails
super
end
end
+
+ # 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)`
+ def register_block_for(type, &blk)
+ var_name = "@#{type}"
+ blocks = instance_variable_defined?(var_name) ? instance_variable_get(var_name) : instance_variable_set(var_name, [])
+ blocks << blk if blk
+ blocks
+ end
end
delegate :railtie_name, to: :class
- def initialize
+ def initialize #:nodoc:
if self.class.abstract_railtie?
raise "#{self.class.name} is abstract, you cannot instantiate it directly."
end
end
- def configure(&block)
+ def configure(&block) #:nodoc:
instance_eval(&block)
end
+ # This is used to create the <tt>config</tt> object on Railties, an instance of
+ # Railtie::Configuration, that is used by Railties and Application to store
+ # related configuration.
def config
@config ||= Railtie::Configuration.new
end
- def railtie_namespace
- @railtie_namespace ||= self.class.parents.detect { |n| n.respond_to?(:railtie_namespace) }
+ def railtie_namespace #:nodoc:
+ @railtie_namespace ||= self.class.module_parents.detect { |n| n.respond_to?(:railtie_namespace) }
end
protected
- def run_console_blocks(app) #:nodoc:
- each_registered_block(:console) { |block| block.call(app) }
- end
+ def run_console_blocks(app) #:nodoc:
+ each_registered_block(:console) { |block| block.call(app) }
+ end
- def run_generators_blocks(app) #:nodoc:
- each_registered_block(:generators) { |block| block.call(app) }
- end
+ def run_generators_blocks(app) #:nodoc:
+ each_registered_block(:generators) { |block| block.call(app) }
+ end
- def run_runner_blocks(app) #:nodoc:
- each_registered_block(:runner) { |block| block.call(app) }
- end
+ def run_runner_blocks(app) #:nodoc:
+ each_registered_block(:runner) { |block| block.call(app) }
+ end
- def run_tasks_blocks(app) #:nodoc:
- extend Rake::DSL
- each_registered_block(:rake_tasks) { |block| instance_exec(app, &block) }
- end
+ def run_tasks_blocks(app) #:nodoc:
+ extend Rake::DSL
+ each_registered_block(:rake_tasks) { |block| instance_exec(app, &block) }
+ end
private
- def each_registered_block(type, &block)
- klass = self.class
- while klass.respond_to?(type)
- klass.public_send(type).each(&block)
- klass = klass.superclass
+ # run `&block` in every registered block in `#register_block_for`
+ def each_registered_block(type, &block)
+ klass = self.class
+ while klass.respond_to?(type)
+ klass.public_send(type).each(&block)
+ klass = klass.superclass
+ end
end
- end
end
end
diff --git a/railties/lib/rails/railtie/configurable.rb b/railties/lib/rails/railtie/configurable.rb
index 1572af0b2a..7f42fae10a 100644
--- a/railties/lib/rails/railtie/configurable.rb
+++ b/railties/lib/rails/railtie/configurable.rb
@@ -1,4 +1,6 @@
-require 'active_support/concern'
+# frozen_string_literal: true
+
+require "active_support/concern"
module Rails
class Railtie
@@ -9,7 +11,7 @@ module Rails
delegate :config, to: :instance
def inherited(base)
- raise "You cannot inherit from a #{self.superclass.name} child"
+ raise "You cannot inherit from a #{superclass.name} child"
end
def instance
@@ -24,11 +26,11 @@ module Rails
class_eval(&block)
end
- protected
+ private
- def method_missing(*args, &block)
- instance.send(*args, &block)
- end
+ def method_missing(*args, &block)
+ instance.send(*args, &block)
+ end
end
end
end
diff --git a/railties/lib/rails/railtie/configuration.rb b/railties/lib/rails/railtie/configuration.rb
index eb3b2d8ef4..70274b948c 100644
--- a/railties/lib/rails/railtie/configuration.rb
+++ b/railties/lib/rails/railtie/configuration.rb
@@ -1,4 +1,6 @@
-require 'rails/configuration'
+# frozen_string_literal: true
+
+require "rails/configuration"
module Rails
class Railtie
@@ -53,7 +55,7 @@ module Rails
ActiveSupport.on_load(:before_configuration, yield: true, &block)
end
- # Third configurable block to run. Does not run if +config.cache_classes+
+ # Third configurable block to run. Does not run if +config.eager_load+
# set to false.
def before_eager_load(&block)
ActiveSupport.on_load(:before_eager_load, yield: true, &block)
diff --git a/railties/lib/rails/ruby_version_check.rb b/railties/lib/rails/ruby_version_check.rb
index 67a19d8a94..b2d44d9b8e 100644
--- a/railties/lib/rails/ruby_version_check.rb
+++ b/railties/lib/rails/ruby_version_check.rb
@@ -1,13 +1,15 @@
-if RUBY_VERSION < '2.2.2' && RUBY_ENGINE == 'ruby'
+# frozen_string_literal: true
+
+if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.4.1") && RUBY_ENGINE == "ruby"
desc = defined?(RUBY_DESCRIPTION) ? RUBY_DESCRIPTION : "ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE})"
abort <<-end_message
- Rails 5 requires Ruby 2.2.2 or newer.
+ Rails 6 requires Ruby 2.4.1 or newer.
You're running
#{desc}
- Please upgrade to Ruby 2.2.2 or newer to continue.
+ Please upgrade to Ruby 2.4.1 or newer to continue.
end_message
end
diff --git a/railties/lib/rails/secrets.rb b/railties/lib/rails/secrets.rb
new file mode 100644
index 0000000000..747cf31d7a
--- /dev/null
+++ b/railties/lib/rails/secrets.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+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"
+ @root = File # Wonky, but ensures `join` uses the current directory.
+
+ class << self
+ attr_writer :root
+
+ 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 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(&block)
+ writing(read, &block)
+ 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")
+ decrypt(IO.binread(path))
+ else
+ IO.read(path)
+ end
+ end
+
+ def writing(contents)
+ tmp_file = "#{File.basename(path)}.#{Process.pid}"
+ tmp_path = File.join(Dir.tmpdir, tmp_file)
+ IO.binwrite(tmp_path, contents)
+
+ yield tmp_path
+
+ updated_contents = IO.binread(tmp_path)
+
+ write(updated_contents) if updated_contents != contents
+ ensure
+ FileUtils.rm(tmp_path) if File.exist?(tmp_path)
+ 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 9b058a1848..d7170e6282 100644
--- a/railties/lib/rails/source_annotation_extractor.rb
+++ b/railties/lib/rails/source_annotation_extractor.rb
@@ -1,133 +1,149 @@
-# Implements the logic behind the rake tasks for annotations like
-#
-# rake notes
-# rake notes:optimize
-#
-# and friends. See <tt>rake -T notes</tt> and <tt>railties/lib/tasks/annotations.rake</tt>.
-#
-# Annotation objects are triplets <tt>:line</tt>, <tt>:tag</tt>, <tt>:text</tt> that
-# represent the line where the annotation lives, its tag, and its text. Note
-# the filename is not stored.
-#
-# Annotations are looked for in comments and modulus whitespace they have to
-# start with the tag optionally followed by a colon. Everything up to the end
-# of the line (or closing ERB comment tag) is considered to be their text.
-class SourceAnnotationExtractor
- class Annotation < Struct.new(:line, :tag, :text)
- def self.directories
- @@directories ||= %w(app config db lib test) + (ENV['SOURCE_ANNOTATION_DIRECTORIES'] || '').split(',')
- end
+# frozen_string_literal: true
- def self.extensions
- @@extensions ||= {}
- end
+require "active_support/deprecation"
- # Registers new Annotations File Extensions
- # SourceAnnotationExtractor::Annotation.register_extensions("css", "scss", "sass", "less", "js") { |tag| /\/\/\s*(#{tag}):?\s*(.*)$/ }
- def self.register_extensions(*exts, &block)
- extensions[/\.(#{exts.join("|")})$/] = block
- end
+# Remove this deprecated class in the next minor version
+#:nodoc:
+SourceAnnotationExtractor = ActiveSupport::Deprecation::DeprecatedConstantProxy.
+ new("SourceAnnotationExtractor", "Rails::SourceAnnotationExtractor")
- register_extensions("builder", "rb", "rake", "yml", "yaml", "ruby") { |tag| /#\s*(#{tag}):?\s*(.*)$/ }
- register_extensions("css", "js") { |tag| /\/\/\s*(#{tag}):?\s*(.*)$/ }
- register_extensions("erb") { |tag| /<%\s*#\s*(#{tag}):?\s*(.*?)\s*%>/ }
+module Rails
+ # Implements the logic behind <tt>Rails::Command::NotesCommand</tt>. See <tt>rails notes --help</tt> for usage information.
+ #
+ # Annotation objects are triplets <tt>:line</tt>, <tt>:tag</tt>, <tt>:text</tt> that
+ # represent the line where the annotation lives, its tag, and its text. Note
+ # the filename is not stored.
+ #
+ # Annotations are looked for in comments and modulus whitespace they have to
+ # start with the tag optionally followed by a colon. Everything up to the end
+ # of the line (or closing ERB comment tag) is considered to be their text.
+ class SourceAnnotationExtractor
+ class Annotation < Struct.new(:line, :tag, :text)
+ def self.directories
+ @@directories ||= %w(app config db lib test)
+ end
- # Returns a representation of the annotation that looks like this:
+ # Registers additional directories to be included
+ # Rails::SourceAnnotationExtractor::Annotation.register_directories("spec", "another")
+ def self.register_directories(*dirs)
+ directories.push(*dirs)
+ end
+
+ def self.extensions
+ @@extensions ||= {}
+ end
+
+ # Registers new Annotations File Extensions
+ # Rails::SourceAnnotationExtractor::Annotation.register_extensions("css", "scss", "sass", "less", "js") { |tag| /\/\/\s*(#{tag}):?\s*(.*)$/ }
+ def self.register_extensions(*exts, &block)
+ extensions[/\.(#{exts.join("|")})$/] = block
+ end
+
+ register_extensions("builder", "rb", "rake", "yml", "yaml", "ruby") { |tag| /#\s*(#{tag}):?\s*(.*)$/ }
+ register_extensions("css", "js") { |tag| /\/\/\s*(#{tag}):?\s*(.*)$/ }
+ register_extensions("erb") { |tag| /<%\s*#\s*(#{tag}):?\s*(.*?)\s*%>/ }
+
+ # Returns a representation of the annotation that looks like this:
+ #
+ # [126] [TODO] This algorithm is simple and clearly correct, make it faster.
+ #
+ # If +options+ has a flag <tt>:tag</tt> the tag is shown as in the example above.
+ # Otherwise the string contains just line and text.
+ def to_s(options = {})
+ s = +"[#{line.to_s.rjust(options[:indent])}] "
+ s << "[#{tag}] " if options[:tag]
+ s << text
+ end
+
+ # Used in annotations.rake
+ #:nodoc:
+ def self.notes_task_deprecation_warning
+ ActiveSupport::Deprecation.warn("This rake task is deprecated and will be removed in Rails 6.1. \nRefer to `rails notes --help` for more information.\n")
+ puts "\n"
+ end
+ end
+
+ # Prints all annotations with tag +tag+ under the root directories +app+,
+ # +config+, +db+, +lib+, and +test+ (recursively).
+ #
+ # Specific directories can be explicitly set using the <tt>:dirs</tt> key in +options+.
+ #
+ # Rails::SourceAnnotationExtractor.enumerate 'TODO|FIXME', dirs: %w(app lib), tag: true
+ #
+ # If +options+ has a <tt>:tag</tt> flag, it will be passed to each annotation's +to_s+.
#
- # [126] [TODO] This algorithm is simple and clearly correct, make it faster.
+ # See <tt>#find_in</tt> for a list of file extensions that will be taken into account.
#
- # If +options+ has a flag <tt>:tag</tt> the tag is shown as in the example above.
- # Otherwise the string contains just line and text.
- def to_s(options={})
- s = "[#{line.to_s.rjust(options[:indent])}] "
- s << "[#{tag}] " if options[:tag]
- s << text
+ # This class method is the single entry point for the `rails notes` command.
+ def self.enumerate(tag, options = {})
+ extractor = new(tag)
+ dirs = options.delete(:dirs) || Annotation.directories
+ extractor.display(extractor.find(dirs), options)
end
- end
- # Prints all annotations with tag +tag+ under the root directories +app+,
- # +config+, +db+, +lib+, and +test+ (recursively).
- #
- # Additional directories may be added using a comma-delimited list set using
- # <tt>ENV['SOURCE_ANNOTATION_DIRECTORIES']</tt>.
- #
- # Directories may also be explicitly set using the <tt>:dirs</tt> key in +options+.
- #
- # SourceAnnotationExtractor.enumerate 'TODO|FIXME', dirs: %w(app lib), tag: true
- #
- # If +options+ has a <tt>:tag</tt> flag, it will be passed to each annotation's +to_s+.
- #
- # See <tt>#find_in</tt> for a list of file extensions that will be taken into account.
- #
- # This class method is the single entry point for the rake tasks.
- def self.enumerate(tag, options={})
- extractor = new(tag)
- dirs = options.delete(:dirs) || Annotation.directories
- extractor.display(extractor.find(dirs), options)
- end
+ attr_reader :tag
- attr_reader :tag
+ def initialize(tag)
+ @tag = tag
+ end
- def initialize(tag)
- @tag = tag
- end
+ # Returns a hash that maps filenames under +dirs+ (recursively) to arrays
+ # with their annotations.
+ def find(dirs)
+ dirs.inject({}) { |h, dir| h.update(find_in(dir)) }
+ end
- # Returns a hash that maps filenames under +dirs+ (recursively) to arrays
- # with their annotations.
- def find(dirs)
- dirs.inject({}) { |h, dir| h.update(find_in(dir)) }
- end
+ # Returns a hash that maps filenames under +dir+ (recursively) to arrays
+ # with their annotations. Files with extensions registered in
+ # <tt>Rails::SourceAnnotationExtractor::Annotation.extensions</tt> are
+ # taken into account. Only files with annotations are included.
+ def find_in(dir)
+ results = {}
- # Returns a hash that maps filenames under +dir+ (recursively) to arrays
- # with their annotations. Only files with annotations are included. Files
- # with extension +.builder+, +.rb+, +.rake+, +.yml+, +.yaml+, +.ruby+,
- # +.css+, +.js+ and +.erb+ are taken into account.
- def find_in(dir)
- results = {}
-
- Dir.glob("#{dir}/*") do |item|
- next if File.basename(item)[0] == ?.
-
- if File.directory?(item)
- results.update(find_in(item))
- else
- extension = Annotation.extensions.detect do |regexp, _block|
- regexp.match(item)
- end
+ Dir.glob("#{dir}/*") do |item|
+ next if File.basename(item)[0] == ?.
- if extension
- pattern = extension.last.call(tag)
- results.update(extract_annotations_from(item, pattern)) if pattern
+ if File.directory?(item)
+ results.update(find_in(item))
+ else
+ extension = Annotation.extensions.detect do |regexp, _block|
+ regexp.match(item)
+ end
+
+ if extension
+ pattern = extension.last.call(tag)
+ results.update(extract_annotations_from(item, pattern)) if pattern
+ end
end
end
- end
- results
- end
+ results
+ end
- # If +file+ is the filename of a file that contains annotations this method returns
- # a hash with a single entry that maps +file+ to an array of its annotations.
- # Otherwise it returns an empty hash.
- def extract_annotations_from(file, pattern)
- lineno = 0
- result = File.readlines(file).inject([]) do |list, line|
- lineno += 1
- next list unless line =~ pattern
- list << Annotation.new(lineno, $1, $2)
+ # If +file+ is the filename of a file that contains annotations this method returns
+ # a hash with a single entry that maps +file+ to an array of its annotations.
+ # Otherwise it returns an empty hash.
+ def extract_annotations_from(file, pattern)
+ lineno = 0
+ result = File.readlines(file, encoding: Encoding::BINARY).inject([]) do |list, line|
+ lineno += 1
+ next list unless line =~ pattern
+ list << Annotation.new(lineno, $1, $2)
+ end
+ result.empty? ? {} : { file => result }
end
- result.empty? ? {} : { file => result }
- end
- # Prints the mapping from filenames to annotations in +results+ ordered by filename.
- # The +options+ hash is passed to each annotation's +to_s+.
- def display(results, options={})
- options[:indent] = results.flat_map { |f, a| a.map(&:line) }.max.to_s.size
- results.keys.sort.each do |file|
- puts "#{file}:"
- results[file].each do |note|
- puts " * #{note.to_s(options)}"
+ # Prints the mapping from filenames to annotations in +results+ ordered by filename.
+ # The +options+ hash is passed to each annotation's +to_s+.
+ def display(results, options = {})
+ options[:indent] = results.flat_map { |f, a| a.map(&:line) }.max.to_s.size
+ results.keys.sort.each do |file|
+ puts "#{file}:"
+ results[file].each do |note|
+ puts " * #{note.to_s(options)}"
+ end
+ puts
end
- puts
end
end
end
diff --git a/railties/lib/rails/tasks.rb b/railties/lib/rails/tasks.rb
index 2c3d278eca..2f644a20c9 100644
--- a/railties/lib/rails/tasks.rb
+++ b/railties/lib/rails/tasks.rb
@@ -1,8 +1,11 @@
-require 'rake'
+# frozen_string_literal: true
+
+require "rake"
# Load Rails Rakefile extensions
%w(
annotations
+ dev
framework
initializers
log
@@ -10,8 +13,10 @@ require 'rake'
misc
restart
routes
- statistics
tmp
-).each do |task|
+ yarn
+).tap { |arr|
+ arr << "statistics" if Rake.application.current_scope.empty?
+}.each do |task|
load "rails/tasks/#{task}.rake"
end
diff --git a/railties/lib/rails/tasks/annotations.rake b/railties/lib/rails/tasks/annotations.rake
index 386ecf44be..3a78de418a 100644
--- a/railties/lib/rails/tasks/annotations.rake
+++ b/railties/lib/rails/tasks/annotations.rake
@@ -1,20 +1,22 @@
-require 'rails/source_annotation_extractor'
+# frozen_string_literal: true
-desc "Enumerate all annotations (use notes:optimize, :fixme, :todo for focus)"
-task :notes do
- SourceAnnotationExtractor.enumerate "OPTIMIZE|FIXME|TODO", tag: true
+require "rails/source_annotation_extractor"
+
+task notes: :environment do
+ Rails::SourceAnnotationExtractor::Annotation.notes_task_deprecation_warning
+ Rails::Command.invoke :notes
end
namespace :notes do
["OPTIMIZE", "FIXME", "TODO"].each do |annotation|
- # desc "Enumerate all #{annotation} annotations"
- task annotation.downcase.intern do
- SourceAnnotationExtractor.enumerate annotation
+ task annotation.downcase.intern => :environment do
+ Rails::SourceAnnotationExtractor::Annotation.notes_task_deprecation_warning
+ Rails::Command.invoke :notes, ["--annotations", annotation]
end
end
- desc "Enumerate a custom annotation, specify with ANNOTATION=CUSTOM"
- task :custom do
- SourceAnnotationExtractor.enumerate ENV['ANNOTATION']
+ task custom: :environment do
+ Rails::SourceAnnotationExtractor::Annotation.notes_task_deprecation_warning
+ Rails::Command.invoke :notes, ["--annotations", ENV["ANNOTATION"]]
end
-end \ No newline at end of file
+end
diff --git a/railties/lib/rails/tasks/dev.rake b/railties/lib/rails/tasks/dev.rake
new file mode 100644
index 0000000000..716fb6a331
--- /dev/null
+++ b/railties/lib/rails/tasks/dev.rake
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require "rails/command"
+require "active_support/deprecation"
+
+namespace :dev do
+ task cache: :environment do
+ ActiveSupport::Deprecation.warn("Using `bin/rake dev:cache` is deprecated and will be removed in Rails 6.1. Use `bin/rails dev:cache` instead.\n")
+ Rails::Command.invoke "dev:cache"
+ end
+end
diff --git a/railties/lib/rails/tasks/engine.rake b/railties/lib/rails/tasks/engine.rake
index 16ad1bfc84..8d77904210 100644
--- a/railties/lib/rails/tasks/engine.rake
+++ b/railties/lib/rails/tasks/engine.rake
@@ -1,11 +1,24 @@
+# frozen_string_literal: true
+
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"
+ task environment: "app:environment"
- if !defined?(ENGINE_PATH) || !ENGINE_PATH
- ENGINE_PATH = find_engine_path(APP_RAKEFILE)
+ if !defined?(ENGINE_ROOT) || !ENGINE_ROOT
+ ENGINE_ROOT = find_engine_path(APP_RAKEFILE)
end
end
@@ -26,11 +39,11 @@ namespace :db do
desc "Display status of migrations"
app_task "migrate:status"
- desc 'Create the database from config/database.yml for the current Rails.env (use db:create:all to create all databases in the config)'
+ desc "Create the database from config/database.yml for the current Rails.env (use db:create:all to create all databases in the config)"
app_task "create"
app_task "create:all"
- desc 'Drops the database for the current Rails.env (use db:drop:all to drop all databases)'
+ desc "Drops the database for the current Rails.env (use db:drop:all to drop all databases)"
app_task "drop"
app_task "drop:all"
@@ -40,7 +53,7 @@ namespace :db do
desc "Rolls the schema back to the previous version (specify steps w/ STEP=n)."
app_task "rollback"
- desc "Create a db/schema.rb file that can be portably used against any DB supported by AR"
+ desc "Create a db/schema.rb file that can be portably used against any database supported by Active Record"
app_task "schema:dump"
desc "Load a schema.rb file into the database"
@@ -49,7 +62,7 @@ namespace :db do
desc "Load the seed data from db/seeds.rb"
app_task "seed"
- desc "Create the database, load the schema, and initialize with the seed data (use db:reset to also drop the db first)"
+ desc "Create the database, load the schema, and initialize with the seed data (use db:reset to also drop the database first)"
app_task "setup"
desc "Dump the database structure to an SQL file"
@@ -57,6 +70,9 @@ namespace :db do
desc "Retrieves the current schema version number"
app_task "version"
+
+ # desc 'Load the test schema'
+ app_task "test:prepare"
end
def find_engine_path(path)
@@ -65,7 +81,7 @@ def find_engine_path(path)
if Rails::Engine.find(path)
path
else
- find_engine_path(File.expand_path('..', path))
+ find_engine_path(File.expand_path("..", path))
end
end
diff --git a/railties/lib/rails/tasks/framework.rake b/railties/lib/rails/tasks/framework.rake
index 904b9d9ad6..1a3711c446 100644
--- a/railties/lib/rails/tasks/framework.rake
+++ b/railties/lib/rails/tasks/framework.rake
@@ -1,22 +1,24 @@
-namespace :rails do
+# frozen_string_literal: true
+
+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" ]
+ task update: [ "update:configs", "update:bin", "update:upgrade_guide_info" ]
desc "Applies the template supplied by LOCATION=(/path/to/template) or URL"
task template: :environment do
template = ENV["LOCATION"]
raise "No LOCATION value given. Please set LOCATION either as path to a file or a URL" if template.blank?
template = File.expand_path(template) if template !~ %r{\A[A-Za-z][A-Za-z0-9+\-\.]*://}
- require 'rails/generators'
- require 'rails/generators/rails/app/app_generator'
- generator = Rails::Generators::AppGenerator.new [Rails.root], {}, destination_root: Rails.root
+ require "rails/generators"
+ require "rails/generators/rails/app/app_generator"
+ generator = Rails::Generators::AppGenerator.new [Rails.root], {}, { destination_root: Rails.root }
generator.apply template, verbose: false
end
namespace :templates do
# desc "Copy all the templates from rails to the application directory for customization. Already existing local copies will be overwritten"
task :copy do
- generators_lib = File.expand_path("../../generators", __FILE__)
+ generators_lib = File.expand_path("../generators", __dir__)
project_templates = "#{Rails.root}/lib/templates"
default_templates = { "erb" => %w{controller mailer scaffold},
@@ -24,45 +26,33 @@ namespace :rails do
default_templates.each do |type, names|
local_template_type_dir = File.join(project_templates, type)
- FileUtils.mkdir_p local_template_type_dir
+ mkdir_p local_template_type_dir, verbose: false
names.each do |name|
dst_name = File.join(local_template_type_dir, name)
src_name = File.join(generators_lib, type, name, "templates")
- FileUtils.cp_r src_name, dst_name
+ cp_r src_name, dst_name, verbose: false
end
end
end
end
namespace :update do
- class RailsUpdate
- def self.invoke_from_app_generator(method)
- app_generator.send(method)
- end
-
- def self.app_generator
- @app_generator ||= begin
- require 'rails/generators'
- require 'rails/generators/rails/app/app_generator'
- gen = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true },
- destination_root: Rails.root
- File.exist?(Rails.root.join("config", "application.rb")) ?
- gen.send(:app_const) : gen.send(:valid_const?)
- gen
- end
- end
- end
+ require "rails/app_updater"
- # desc "Update config/boot.rb from your current rails install"
+ # desc "Update config files from your current rails install"
task :configs do
- RailsUpdate.invoke_from_app_generator :create_boot_file
- RailsUpdate.invoke_from_app_generator :update_config_files
+ Rails::AppUpdater.invoke_from_app_generator :create_boot_file
+ Rails::AppUpdater.invoke_from_app_generator :update_config_files
end
# desc "Adds new executables to the application bin/ directory"
task :bin do
- RailsUpdate.invoke_from_app_generator :create_bin_files
+ Rails::AppUpdater.invoke_from_app_generator :update_bin_files
+ end
+
+ task :upgrade_guide_info do
+ Rails::AppUpdater.invoke_from_app_generator :display_upgrade_guide_info
end
end
end
diff --git a/railties/lib/rails/tasks/initializers.rake b/railties/lib/rails/tasks/initializers.rake
index 2968b5cb53..f108517d1d 100644
--- a/railties/lib/rails/tasks/initializers.rake
+++ b/railties/lib/rails/tasks/initializers.rake
@@ -1,6 +1,9 @@
-desc "Print out all defined initializers in the order they are invoked by Rails."
+# frozen_string_literal: true
+
+require "rails/command"
+require "active_support/deprecation"
+
task initializers: :environment do
- Rails.application.initializers.tsort_each do |initializer|
- puts initializer.name
- end
+ ActiveSupport::Deprecation.warn("Using `bin/rake initializers` is deprecated and will be removed in Rails 6.1. Use `bin/rails initializers` instead.\n")
+ Rails::Command.invoke "initializers"
end
diff --git a/railties/lib/rails/tasks/log.rake b/railties/lib/rails/tasks/log.rake
index 877f175ef3..ec56957204 100644
--- a/railties/lib/rails/tasks/log.rake
+++ b/railties/lib/rails/tasks/log.rake
@@ -1,5 +1,13 @@
+# frozen_string_literal: true
+
namespace :log do
- desc "Truncates all *.log files in log/ to zero bytes (specify which logs with LOGS=test,development)"
+ ##
+ # Truncates all/specified log files
+ # ENV['LOGS']
+ # - 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)"
task :clear do
log_files.each do |file|
clear_log_file(file)
@@ -7,17 +15,27 @@ namespace :log do
end
def log_files
- if ENV['LOGS']
- ENV['LOGS'].split(',')
- .map { |file| "log/#{file.strip}.log" }
- .select { |file| File.exist?(file) }
- else
+ if ENV["LOGS"] == "all"
FileList["log/*.log"]
+ elsif ENV["LOGS"]
+ log_files_to_truncate(ENV["LOGS"])
+ else
+ log_files_to_truncate(all_environments.join(","))
end
end
+ def log_files_to_truncate(envs)
+ envs.split(",")
+ .map { |file| "log/#{file.strip}.log" }
+ .select { |file| File.exist?(file) }
+ end
+
def clear_log_file(file)
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/middleware.rake b/railties/lib/rails/tasks/middleware.rake
index 31e961b483..3a7f86fdcb 100644
--- a/railties/lib/rails/tasks/middleware.rake
+++ b/railties/lib/rails/tasks/middleware.rake
@@ -1,4 +1,6 @@
-desc 'Prints out your Rack middleware stack'
+# frozen_string_literal: true
+
+desc "Prints out your Rack middleware stack"
task middleware: :environment do
Rails.configuration.middleware.each do |middleware|
puts "use #{middleware.inspect}"
diff --git a/railties/lib/rails/tasks/misc.rake b/railties/lib/rails/tasks/misc.rake
index 4195106961..e7786aa622 100644
--- a/railties/lib/rails/tasks/misc.rake
+++ b/railties/lib/rails/tasks/misc.rake
@@ -1,43 +1,62 @@
-desc 'Generate a cryptographically secure secret key (this is typically used to generate a secret for cookie sessions).'
+# frozen_string_literal: true
+
+desc "Generate a cryptographically secure secret key (this is typically used to generate a secret for cookie sessions)."
task :secret do
- require 'securerandom'
+ require "securerandom"
puts SecureRandom.hex(64)
end
-desc 'List versions of all Rails frameworks and the environment'
+desc "List versions of all Rails frameworks and the environment"
task about: :environment do
puts Rails::Info
end
namespace :time do
+ desc "List all time zones, list by two-letter country code (`rails time:zones[US]`), or list by UTC offset (`rails time:zones[-8]`)"
+ task :zones, :country_or_offset do |t, args|
+ zones, offset = ActiveSupport::TimeZone.all, nil
+
+ if country_or_offset = args[:country_or_offset]
+ begin
+ zones = ActiveSupport::TimeZone.country_zones(country_or_offset)
+ rescue TZInfo::InvalidCountryCode
+ offset = country_or_offset
+ end
+ end
+
+ build_time_zone_list zones, offset
+ end
+
namespace :zones do
- desc 'Displays all time zones, also available: time:zones:us, time:zones:local -- filter with OFFSET parameter, e.g., OFFSET=-6'
+ # desc 'Displays all time zones, also available: time:zones:us, time:zones:local -- filter with OFFSET parameter, e.g., OFFSET=-6'
task :all do
- build_time_zone_list(:all)
+ build_time_zone_list ActiveSupport::TimeZone.all
end
# desc 'Displays names of US time zones recognized by the Rails TimeZone class, grouped by offset. Results can be filtered with optional OFFSET parameter, e.g., OFFSET=-6'
task :us do
- build_time_zone_list(:us_zones)
+ build_time_zone_list ActiveSupport::TimeZone.us_zones
end
# desc 'Displays names of time zones recognized by the Rails TimeZone class with the same offset as the system local time'
task :local do
- require 'active_support'
- require 'active_support/time'
+ require "active_support"
+ require "active_support/time"
+
jan_offset = Time.now.beginning_of_year.utc_offset
jul_offset = Time.now.beginning_of_year.change(month: 7).utc_offset
offset = jan_offset < jul_offset ? jan_offset : jul_offset
- build_time_zone_list(:all, offset)
+
+ build_time_zone_list(ActiveSupport::TimeZone.all, offset)
end
# to find UTC -06:00 zones, OFFSET can be set to either -6, -6:00 or 21600
- def build_time_zone_list(method, offset = ENV['OFFSET'])
- require 'active_support'
- require 'active_support/time'
+ def build_time_zone_list(zones, offset = ENV["OFFSET"])
+ require "active_support"
+ require "active_support/time"
if offset
offset = if offset.to_s.match(/(\+|-)?(\d+):(\d+)/)
- sign = $1 == '-' ? -1 : 1
+ sign = $1 == "-" ? -1 : 1
hours, minutes = $2.to_f, $3.to_f
((hours * 3600) + (minutes.to_f * 60)) * sign
elsif offset.to_f.abs <= 13
@@ -47,7 +66,7 @@ namespace :time do
end
end
previous_offset = nil
- ActiveSupport::TimeZone.__send__(method).each do |zone|
+ zones.each do |zone|
if offset.nil? || offset == zone.utc_offset
puts "\n* UTC #{zone.formatted_offset} *" unless zone.utc_offset == previous_offset
puts zone.name
diff --git a/railties/lib/rails/tasks/restart.rake b/railties/lib/rails/tasks/restart.rake
index f36c86d81b..074e3e89a1 100644
--- a/railties/lib/rails/tasks/restart.rake
+++ b/railties/lib/rails/tasks/restart.rake
@@ -1,5 +1,9 @@
+# frozen_string_literal: true
+
desc "Restart app by touching tmp/restart.txt"
task :restart do
- FileUtils.mkdir_p('tmp')
- FileUtils.touch('tmp/restart.txt')
+ verbose(false) do
+ mkdir_p "tmp"
+ touch "tmp/restart.txt"
+ end
end
diff --git a/railties/lib/rails/tasks/routes.rake b/railties/lib/rails/tasks/routes.rake
index 1815c2fdc7..21ce900a8c 100644
--- a/railties/lib/rails/tasks/routes.rake
+++ b/railties/lib/rails/tasks/routes.rake
@@ -1,7 +1,9 @@
-desc 'Print out all defined routes in match order, with names. Target specific controller with CONTROLLER=x.'
+# frozen_string_literal: true
+
+require "rails/command"
+require "active_support/deprecation"
+
task routes: :environment do
- all_routes = Rails.application.routes.routes
- require 'action_dispatch/routing/inspector'
- inspector = ActionDispatch::Routing::RoutesInspector.new(all_routes)
- puts inspector.format(ActionDispatch::Routing::ConsoleFormatter.new, ENV['CONTROLLER'])
+ ActiveSupport::Deprecation.warn("Using `bin/rake routes` is deprecated and will be removed in Rails 6.1. Use `bin/rails routes` instead.\n")
+ Rails::Command.invoke "routes"
end
diff --git a/railties/lib/rails/tasks/statistics.rake b/railties/lib/rails/tasks/statistics.rake
index 735c36eb3a..8cacf4a49f 100644
--- a/railties/lib/rails/tasks/statistics.rake
+++ b/railties/lib/rails/tasks/statistics.rake
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# While global constants are bad, many 3rd party tools depend on this one (e.g
# rspec-rails & cucumber-rails). So a deprecation warning is needed if we want
# to remove it.
@@ -7,7 +9,9 @@ STATS_DIRECTORIES = [
%w(Jobs app/jobs),
%w(Models app/models),
%w(Mailers app/mailers),
- %w(Javascripts app/assets/javascripts),
+ %w(Channels app/channels),
+ %w(JavaScripts app/assets/javascripts),
+ %w(JavaScript app/javascript),
%w(Libraries lib/),
%w(APIs app/apis),
%w(Controller\ tests test/controllers),
@@ -16,12 +20,13 @@ 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) }
desc "Report code statistics (KLOCs, etc) from the application or engine"
task :stats do
- require 'rails/code_statistics'
+ require "rails/code_statistics"
CodeStatistics.new(*STATS_DIRECTORIES).to_s
end
diff --git a/railties/lib/rails/tasks/tmp.rake b/railties/lib/rails/tasks/tmp.rake
index 9162ef234a..7340b41ee4 100644
--- a/railties/lib/rails/tasks/tmp.rake
+++ b/railties/lib/rails/tasks/tmp.rake
@@ -1,13 +1,13 @@
+# frozen_string_literal: true
+
namespace :tmp do
- desc "Clear cache and socket files from tmp/ (narrow w/ tmp:cache:clear, tmp:sockets:clear)"
- task clear: ["tmp:cache:clear", "tmp:sockets:clear"]
+ desc "Clear cache, socket and screenshot files from tmp/ (narrow w/ tmp:cache:clear, tmp:sockets:clear, tmp:screenshots:clear)"
+ task clear: ["tmp:cache:clear", "tmp:sockets:clear", "tmp:screenshots:clear"]
- tmp_dirs = [ 'tmp/cache',
- 'tmp/sockets',
- 'tmp/pids',
- 'tmp/cache/assets/development',
- 'tmp/cache/assets/test',
- 'tmp/cache/assets/production' ]
+ tmp_dirs = [ "tmp/cache",
+ "tmp/sockets",
+ "tmp/pids",
+ "tmp/cache/assets" ]
tmp_dirs.each { |d| directory d }
@@ -17,21 +17,28 @@ namespace :tmp do
namespace :cache do
# desc "Clears all files and directories in tmp/cache"
task :clear do
- FileUtils.rm_rf(Dir['tmp/cache/[^.]*'])
+ rm_rf Dir["tmp/cache/[^.]*"], verbose: false
end
end
namespace :sockets do
# desc "Clears all files in tmp/sockets"
task :clear do
- FileUtils.rm(Dir['tmp/sockets/[^.]*'])
+ rm Dir["tmp/sockets/[^.]*"], verbose: false
end
end
namespace :pids do
# desc "Clears all files in tmp/pids"
task :clear do
- FileUtils.rm(Dir['tmp/pids/[^.]*'])
+ rm Dir["tmp/pids/[^.]*"], verbose: false
+ end
+ end
+
+ namespace :screenshots do
+ # desc "Clears all files in tmp/screenshots"
+ task :clear do
+ rm Dir["tmp/screenshots/[^.]*"], verbose: false
end
end
end
diff --git a/railties/lib/rails/tasks/yarn.rake b/railties/lib/rails/tasks/yarn.rake
new file mode 100644
index 0000000000..4fb8586b69
--- /dev/null
+++ b/railties/lib/rails/tasks/yarn.rake
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+namespace :yarn do
+ desc "Install all JavaScript dependencies as specified via Yarn"
+ task :install do
+ # Install only production deps when for not usual envs.
+ valid_node_envs = %w[test development production]
+ node_env = ENV.fetch("NODE_ENV") do
+ rails_env = ENV["RAILS_ENV"]
+ valid_node_envs.include?(rails_env) ? rails_env : "production"
+ end
+ system({ "NODE_ENV" => node_env }, "#{Rails.root}/bin/yarn install --no-progress --frozen-lockfile")
+ 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/templates/rails/mailers/email.html.erb b/railties/lib/rails/templates/rails/mailers/email.html.erb
index bb6e49966d..e46364ba8a 100644
--- a/railties/lib/rails/templates/rails/mailers/email.html.erb
+++ b/railties/lib/rails/templates/rails/mailers/email.html.erb
@@ -88,15 +88,32 @@
<% unless @email.attachments.nil? || @email.attachments.empty? %>
<dt>Attachments:</dt>
<dd>
- <%= @email.attachments.map { |a| a.respond_to?(:original_filename) ? a.original_filename : a.filename }.join(', ') %>
+ <% @email.attachments.each do |a| %>
+ <% filename = a.respond_to?(:original_filename) ? a.original_filename : a.filename %>
+ <%= link_to filename, "data:application/octet-stream;charset=utf-8;base64,#{Base64.encode64(a.body.to_s)}", download: filename %>
+ <% end %>
</dd>
<% end %>
+ <dt>Format:</dt>
<% if @email.multipart? %>
<dd>
- <select onchange="document.getElementsByName('messageBody')[0].src=this.options[this.selectedIndex].value;">
- <option <%= request.format == Mime::HTML ? 'selected' : '' %> value="?part=text%2Fhtml">View as HTML email</option>
- <option <%= request.format == Mime::TEXT ? 'selected' : '' %> value="?part=text%2Fplain">View as plain-text email</option>
+ <select id="part" onchange="refreshBody(false);">
+ <option <%= request.format == Mime[:html] ? 'selected' : '' %> value="<%= part_query('text/html') %>">View as HTML email</option>
+ <option <%= request.format == Mime[:text] ? 'selected' : '' %> value="<%= part_query('text/plain') %>">View as plain-text email</option>
+ </select>
+ </dd>
+ <% else %>
+ <dd id="mime_type" data-mime-type="<%= part_query(@email.mime_type) %>"><%= @email.mime_type == 'text/html' ? 'HTML email' : 'plain-text email' %></dd>
+ <% end %>
+
+ <% if I18n.available_locales.count > 1 %>
+ <dt>Locale:</dt>
+ <dd>
+ <select id="locale" onchange="refreshBody(true);">
+ <% I18n.available_locales.each do |locale| %>
+ <option <%= I18n.locale == locale ? 'selected' : '' %> value="<%= locale_query(locale) %>"><%= locale %></option>
+ <% end %>
</select>
</dd>
<% end %>
@@ -104,7 +121,7 @@
</header>
<% if @part && @part.mime_type %>
- <iframe seamless name="messageBody" src="?part=<%= Rack::Utils.escape(@part.mime_type) %>"></iframe>
+ <iframe seamless name="messageBody" src="?<%= part_query(@part.mime_type) %>"></iframe>
<% else %>
<p>
You are trying to preview an email that does not have any content.
@@ -112,5 +129,34 @@
</p>
<% end %>
+<script>
+ function refreshBody(reload) {
+ var part_select = document.querySelector('select#part');
+ var locale_select = document.querySelector('select#locale');
+ var iframe = document.getElementsByName('messageBody')[0];
+ var part_param = part_select ?
+ part_select.options[part_select.selectedIndex].value :
+ document.querySelector('#mime_type').dataset.mimeType;
+ var locale_param = locale_select ? locale_select.options[locale_select.selectedIndex].value : null;
+ var fresh_location;
+ if (locale_param) {
+ fresh_location = '?' + part_param + '&' + locale_param;
+ } else {
+ fresh_location = '?' + part_param;
+ }
+ iframe.contentWindow.location = fresh_location;
+
+ var url = location.pathname.replace(/\.(txt|html)$/, '');
+ var format = /html/.test(part_param) ? '.html' : '.txt';
+ var state_to_replace = locale_param ? (url + format + '?' + locale_param) : (url + format);
+
+ if (reload) {
+ location.href = state_to_replace;
+ } else if (history.replaceState) {
+ window.history.replaceState({}, '', state_to_replace);
+ }
+ }
+</script>
+
</body>
</html>
diff --git a/railties/lib/rails/templates/rails/welcome/index.html.erb b/railties/lib/rails/templates/rails/welcome/index.html.erb
index acf04af416..b6823457c0 100644
--- a/railties/lib/rails/templates/rails/welcome/index.html.erb
+++ b/railties/lib/rails/templates/rails/welcome/index.html.erb
@@ -1,268 +1,74 @@
<!DOCTYPE html>
<html>
- <head>
- <title>Ruby on Rails: Welcome aboard</title>
- <style media="screen">
- body {
- margin: 0;
- margin-bottom: 25px;
- padding: 0;
- background-color: #f0f0f0;
- font-family: "Lucida Grande", "Bitstream Vera Sans", "Verdana";
- font-size: 13px;
- color: #333;
- }
-
- h1 {
- font-size: 28px;
- color: #000;
- }
-
- a {
- color: #03c;
- }
-
- a:hover {
- background-color: #03c;
- color: white;
- text-decoration: none;
- }
-
- #page {
- background-color: #f0f0f0;
- width: 750px;
- margin: 0;
- margin-left: auto;
- margin-right: auto;
- }
-
- #content {
- float: left;
- background-color: white;
- border: 3px solid #aaa;
- border-top: none;
- padding: 25px;
- width: 500px;
- }
-
- #sidebar {
- float: right;
- width: 175px;
- }
-
- #footer {
- clear: both;
- }
-
- #header, #about, #getting-started {
- padding-left: 75px;
- padding-right: 30px;
- }
-
- #header {
- background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAABACAYAAABY1SR7AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAGZhJREFUeNqsWwmUXGWV/t5Sr9aurl6qO0l3Z9/DEoJh18gZQGAUxPHIyQHH7eioZ8bjnAFHZ0RndNxxRBhGcUbxoKIHBkTEcUYREIHIGpKQjUDS6U660/tSVV3Lq/fefPf/Xy2dBFGYx3npqvde/e/e/97v3u/e/8e4Lt2L8DCCAFcGwF8ZBjYbgM1rAZoO+WLwZhDMu9y4+YcOozbAqzwXNA3GdzX/5hV+KnKO2+GXFj/AvzmW8e72iG202CYiphbY403f9/k3QHZtJ9oWtyCQe7wGX79TKVb7rP9pXJPDVxf0Rz+oyxm4HNWrahFNixdk3EAJbERMWOm4ulctVODNVeEVK0DeRVDgb1wfJgcqUo6duaKnFOH7bm6JmH+5LOEgZprwRIHAV3JYfLjKM55Noz3bBqdcgt0Wg52Kq/cHHkXns0qIukKBlltk9rU2QaiouiefPQ+RdBuseAJeqYTK1CTH8mE4NsyIpRWu8nssCs+xULWpjGVwTvieKl/sV6mIXzOib/OftzuG8d6l8SiVMODyRb46oazg8YPP2Wnvy9ISNqplzsxYAW6hjGhHEmYiBoPC+hRMfFMrESgrBC5n0KS+lq1nPahZh2OXymg9bSNWX/u3FKyKI//7Exx96B4Y8RiCEseq8t0VznyxjMDidFIJ8QSf3hJEOFbZEAHVhIkFTX54fxtnIW5pJUQIeZ8ooZShkInuDOLpFIX1ldtCBix7KI/k4E7OwbTjcNIdiCQzsONp2LEk7GgUnZsuQN9lW2En45xlukrUghWzeZq8FsXsi8+gND6MSCqD9k3nwulIUShKZxt0LYPWortRSY0NXreC8J6pZNDChEDh53PT1NIPLaEnLbQKNTETEaR7sycA0jD1INXZAnzObjTbiWh7Vr1A3Knn4nciu+lCvstUig09cp96cVCtcELoFpEIFUjjyIM/osWIg+IMXS3DcfNwZ3NQHmmKU9OqroX2jWdgatduuPkpmA4ViZrK9RqKABEBtg9tDeW+oUIyTIYuFaX7eCG4aqbU+hhKocD3UBoZISBLiC9cpAQKyq5SQo6OjVswtec5VHLTiHUuIN4WonXlqUj2riS0DIUXwZlERFHSK+SQGzqI3MHdmNm7CzMvvowF527B8qvejZ3/+iXk9vVTao5tiTKN0OUHISZEGS/8W6UbRdoTSHe3E1f+CRaR3xhBLVJSIQ7qleZQGBigZYoYdR+ElUjBaW3H6JMPIrV0Hdo2bEayZ7my0KsdLctPBS64EuWZMYw/9wTGnvod0mtzWH71Vuz66o10bVpK8FIx6orUMejpCKYBTvfM9HXBJtA8z3/1BKDivaksVJmaYsgsYPDnd6LzzAuw8I1XUIGleC1HtDWLnguv5BiX4+jDD2D4sQeV1bQvNXBi6vAb1MGtrEEHjRPgqfZ0qMRJElYYSudfq12nmzAvtJ2yib69iRadRGnySD0Uv5bDtCPou/gqnPY3N6DnLRczgtHxCf4aVnUeUdgw6i6FqM1w292Ujo/TJdB5wHcJ2iDCaBTRmVfw4rkw4yksuvQyJJf0YvrgNiayvBLESS9AYuFqJLLLCPb4SQWulosojhxmeCeoDeaQSoVuy8lPtSKxYKnC2Bmf+DwtvBgv3/qfTI6uEtGuJV7PCBTIq5zNtt5uxBgyvap30pf55TISfX1Y/PatGPrVvcgPvEyAJ1GenaPZLSy//G2IL+qki43CNCMwk620iovy9FGUJgYwm8gwpK/guRJOS5dyD688h+n9z2L28F4Ujx2ia04jEl8Ad3oGVTePaGcnQ3sKLb1rkD3nIqx594dRIh733n6PmmrrvGj671sjVlxczRWAkxZ0r+rTrhfMJ0uEM8xKUYXONR+5nr57BdpP24TCsX6M/f5F5AYLWPauK9F11htUwjOIL8GNZH1qpKwiyVGELk0OoDj2EtziFOaODSN3aC/v24xmZzAU51TgcJKd/DktHo9jyRXvg0Or7PvejTj22KPKiyafew6zg8MYypVLNsLkJ2bxaZXM4i5EmCBPsEaoWJUUpfeSK7DgvEtQmh4ihTDQdf5FOHDHr7HqPVeh99KL4OVzpE50N18CtqnCdBCY6rsEcTsqIGUGD6rY9e3bMPzIHmTWLsbqa7ai84wL6YrTqEyOqEmwonEExSoO//R7dLcJWiWCueF+7P7mjZAUY8YdJZqySMo24j5zQSybQdeyhdrX5imho4NhEEnkRbkDQyjSRVJLeziCgef/6avIrFuOtR95P2lJNSSshg4l6rdm+Ht9inWsqIOX7voN+u/eRoEM5PvHMbbjGcwcfg7jO3YxbCcRiaaYQOXnpEaFGeahGQaMCidJRidt8RghS6Q344XQIowmFq2QXdLNdwsx8zUFqCOQNIECVqdp8pESB53Fvhdux9T2FxBb1AWX4XbjDX/HFzjEmgedB4XYKT5D4T0VTLRCtIiTwOBvfovpvS8T+Bm4MyW6jw13tIIDt/9G/TTWk8HKvzgbmd4+YldYQIdixgHJYkC82Ul6UDnQSbEGdsFGZlEWyUyLyiEyYwajRVAoAXNlEjR+pjUCUmiDQcKOORwwgpFfP4cg5mPzTZ9FoqePdGVWuZRPYQNPcgrd0/dCpqpdy3DIsQ4fxtiTu7Hxkx8iRXkcB+94iM86/K0Jx4opi5aOzGJs14toWeLAdYXWxFQCtJlkA+LUq+bI7QR3mj3YoqVNgGcXd5NWUOiZAk9GH86S4jK25jWBLVREl1uK5Voywz6WXf1WLHjTm0lPigSyxoUpnEqU8c26Wyk/Y24RMjhw/yMoj+cQbWvH0isuwuijL6BwaJwcyq7XUTaBP7N3HOU3ke7HSONJb8RTBGoGKZPFyTE8saTZyCPtrC2coxOoTuY5+x4UTzHNsNjR6d6Qa8JJ5BIV8ksVtKzpwcr3v5dyOrzHKMWXizsZAnK6k1ImPDmAqjOmdr9AwXcodzr4kwfQfuY6VKbzyhpGU96S75WxIqb2DaPnvNWKklQD4WSuzB+sVILjOYjm/VARSWKTBQQzlZCFmErYeubzVJJR14SlQtVQMjO0xrXvoulXkq3OKnxAXqSsoSmNUbOM/BV35RjDDz9JrBXpnnEM3vsYjj38LLyZihI8QNAgQhITOCmTO46i+6w1MPm86RVIiC09/RJUGcECCe2UU0G6QIyUjEC5hGaCNd4RqHKU6VuDylQlI2N8hfXDWibEdyhCKXREuZUVUX8lyhh2+Jl5Q/6akSgT4izGn3wBFu+JwYOKj8qwtsbJaYmJuYEZ5AYmFOWXPCN1jTodzeuqM0WtSI1rzXrV0LSNKRFuZLYQ2EYVPjEQVuQUMsCya65GvL1HWUwJS+FNUcBsUiZUQv7aLGlndr+I8ug4XUMVAJw4U7FmI8SFETTmUaGK2gas1SeeP8znoizIEso9DaUIy2FWkNU5V0VYs/azWXKncuCHqgQq1CHiY831H8TGr34erRvXKdD6LD3b+HnRn12qGgdqlmxHZe2aRcy6NbQScl8y8dSOfWQE1yK9YYmqXYww3xhNObemUI2IWraF2d1HMTeeh83MbkUiylKiiMdy2wjzXBjxWYdRiSkhfDVVKGSstxM9l16JxZe/E2+848c49bPXK9D2vPUyEsBOVZMINmpCW6HgEOuIQjXF6FYuAV2aHsWyrVfj9C9er5SR5Kms0PTf8QoZtIo7WSJW+mmRJLGSpDK2ipzV2bK6X6fxtWOCicYVqyhGXkXn+WeTcfape5ZDsPGM91C5iy8LI0s445bd9FkrAFHICt1N8DE+gdyeQczs34+uzeei68LNLGfdea50st6VbiyYmHq+nxTFRSSRVsD3ii7xyeQbdt/M5h/MERMT4i6GjlAWeUxh6HCN8+LIz+5H5zlUbtHSOnVp4MCa51JaIQ16i0kwP9CP0uExPP+JL2DggfuYN8jTJClYxnH4aNimdpp0r7nDkyx9h5gE0+RqSVTyZXXTsMz5FaJyMJrrGLNopyWUIImj//1LjPzuUZLCC5gzVqMwPIglV7/rxCaihFaCPCDOxDUl1EoylFP4mUlFCgPDStLKWB47PnUjrSSsNqrJsa/zR02ZwGjYRoVkEZh0ZHzbfmTPXE85SWrnKip6GeFE2I1iKVBCzNK9pmiVhS1x+Axx7myRJesvgHvvR3rNKmQ3n/OKPVGND1MVXTqHiFK6qVFiwlXgTVDhkq+ChhnyJCW9GeaoIGQOdV0M9YhYZWbvUXrIJJ+rKL6lJ9CYj5Fai0iKqyPkx0HcUsJYrBbtREIJ2H72GxTI/2CL1zAbLkZ8WIxYgUvsKebq6Zl3rEZvymx6echo1N+au9XcS3oHsxWMPrGTFH+CLhsmbhMNRWrNB4SZVSwyJ5WDFRb3DAAmaXf2rPP+6BpbkmStkBLAWwkHmdNWKfYqFaZRp2GGdo+mhpv6bBkNhepRzERpdASeW1aKSZ5RidpoUsRAvQ+NJCnJHHl+bcZ80vjkij661vo/rWMQSitWskgnNv7LP+MNN38NadYuCPtYCItIFTjMRgfeqClkhkFZ+FXCQmpFuyKXii7xNI93LT9szdrUMsNZnJkuwZX6zlKdaqRXrESiq/e19kBC3NisLt+Gc/7jW0gtZ51Bl1MCmUaoM//aRv0aapnF0l362KIUnI6EyuhCUOuWrIVfAZcRAj5NJWJ0C5epP19y1awJLWhdt/a1t3KcGF8Yxb5bbsLItoeYmxZRkRWq46IrR9StX/tcw4oKsYH+nlrZpmbcZQ7R1tDPBvMbdIwofLpVKIfcJy5nCa5WRhnDFkVOx+s5kr29GPzpfUxsuxg0zlQUxSZudG/CqNOSIJxYCclGCA7fDRDpiCK6gIVfidVmWXrHRh0fmBd+eSYIIEcWdRhdJJsWp+aQT1vI9nYjnl3wuhSJLuhAJJ1WQWDisadUELCi0bD1WlscMpq6lrV1Ft0riC9tVcFD8odfDVS9bod5pNGgC3+XFnxsXA2rsw25/gHMTcwiRxdbvLgPsY7s61IktWSZinw6l8SbupNGvUlphB1yZY3aIhfZtRmz4XS3oMoA5JP6BywdvBIr24ytMdzsWjHaMcnI0nXRG5FkdCrnS6gy6QzccxeMZDsJW+r1KbJ4pbKAVy6huXoyauVUaAUjRK5WjN9cH05PCiZl84VfsXaSVTKf191C6F61qCXjtjAORtvTSPb0sgYoEi/UmEmnMj6JkpXA6z2cTAbxxV26GdEEZB12DVVV63BrIuwYaWpCGZyuJBWSFSxPLTB5PH1+rhDDKlQbuvajNUzE+UVyRTTdQt+zWIrGWIJOozo8hjmashq8PkXsZAoty1Yqi/gVnq6ru+p1pUKFTM3dENJzu421TiqKKq3hhUp45apSyM1VGMH0xOi+liz0yOxUyijs2w2DlRjI+8tHB3XUIP+fGBxA9+LFr1kRgwV769p1fPkEQ+9KRq+dKE9MsGKc1BmxltEC7W6CEdW0aUtocIvw0tcSt5JGu3R4OA+zIxW1uKoUOUZzFxmxRp/ai+iz+xi9CK5EVJGdqBNBlG4xdvBlRq9eTQteawhm0MgPLsSGj92gVqjKk8ew/TOfxPjjz8BKxhvLFGHjWUBuJh0Cu6pqD7WCTGz4BDqKpE30rIlj05rw6sKFxuCXPP9O8MEjxQqOTuQwNjJLa1mItaRRGB3GLHnO6znaNmxC/nA/cocPKNoS61iEZVdfEy5LBHVKUieCLY5eeKIiXp6RapJuNVJFMCamYGnOUFyslBo0Xronai0dIfXmnZIqtKhgNIaj/F3ULSLx4j60dnXXy8s/OZe0dyGW7cLOL34arevXI9rayWgYhZPtoJtNqsTbyPKUgwzamyCw867MtG5NBUF9bSBXLCkeKOzDroUutaZODax52yUk5sfgsyrL897+PXtQHTmK7vWnomPpCkSTf3pI7j7/Qmz/5HWY3r5LNziYeC3WPlYsovOJJ7VKVbuPENcgXEyvuV3IbKXpPlcqqh0acqGe2S1oq1jzqmZ+b0mGDJNaM2bnjrHuPnYUifZOtDMKda9ah1RnZ30F99WO9jM2MzouZw0vLdJIuCsiUInOz0vbiVNa9DSBtITyWo3VAV/XG/KmPEuBKrmard7rNxKiyCoN7EBnpXlLCiYTmfibuEHSSSkLV4uzGNr5NEYP7EZb31J0rd6AzMIevtf+g4oIg+7e8iYM3H03J5muw9n3ZquqfwU3aGDdMBqdztr+lXBbhyg+R2xYTb5jN7YG6SKnyh870r8Ki6Py0CiO3fcTNWaCBU3E8FVDr7ZPRjbcDLHO30N/TmazdLk+JFMxVoZh6errUrcmnDQp5o4MocrI4o3N6dmXhp1hoHkOFV2R5CXtVwm3Qc0aBip8Z6lY0HtRpJ8GYz5pVFgxgkaHiaCuDE1gfOAhFdNbJIKxplCKNJqqyoqi0CT9tp9/IyyPE2SryYyDKD9LVKxKUqXbuFOM+yVDN/Rq+0ia1mLmtYNqK8rhTiSpLLNbLkDLuZvQ0X8QBoG+//5fIMjP4AQ/kJkuM+vW+sS1wkgiVSTi0Fq2XqoLFfFYMMkyHSFL2mOpHQmy+aU4xXHoLk6rrIkYiE1JNpZOJjO1ivduOLSkZeuk6/YBwR54jaVv6chXpmZQmJnEssveQjwVcPCXv1IWt4//sUVB7K4WpGTREqhvJCrO5MhtGLMTKWU5pUSpDKs1glhbB4W3VCSpTM6gOl2GQzxJt+RQUMFcOoENrXG0FEhESSvMmIVIZ6uaHL9QZn6Y067VNJueV4bdmYDdktJ7pAJNKKfG+pG/cz8GH/gfGLIARF4o9fs8RWSrUmZxN7Z+9za0sooTPiRuI22bsUMHsevWW1B+iFnYdOgqFWTPPxWnXPdxtK5eV8fB9IH92Pn1m1hz7MQh00Xm/C34+K23MiOXsPvLX8bgbXej5bz1OPs7tzIhduHgnXfghX/8OplEsr6U4ZtV9G69HMvf8wEkKUfgaUeWbs4zX/8Sxm/+AbzxCRVF1VpFM9hrvS2ZmZbuRUh2LpxPw7t60EWK8vgHPoCZ5w4i1pvBps99Bu2nbJ73XLyzB4kvLcAPt27F2LFR9MTjSKbb1L1h4mIq4iNL14u2ZRFJysazZCNHqA0DZXRcuBGnf+bz6v4JLDqVgk3r247DnMdJDkOzffJtDfoY2b0dg08/gbZlq7BiyyWk+MuQ2bAGU9v2snTtQnxBj3pu9OnfYXr/Hiq1EZ0bz0ZsUS+sFUvgDB+DFfh1v3X9Kg4xknfLRNZ21h2/RYTX29avU0pUSwUcuP07KLw0oBZrA5bGozt2MlA5updgzGuJnYyp6rt7778HP37fX+OJW77ZaKzKoo5eOdfRhMehO3+EbXzu8H/dXW/SOTwj0gZqeoVck+h3xES9LDjpVp3QXeRdqSVLkDllrepy5oeHMPH0brq2qdteRmNJwj7pYKFVlr75YrztKw6ya9aFTzF8Tk+pBZrmXRGRdCsSLMiQbKlfE7PLrjarCcSSA0QZvQQevGKncnrfXpVwZTde3+XvqN9b8d4PYfuNn8O+b9zO56K6oGpOiMYreNfSc7eoE+FO00P334XJx3fQzM685zd8/Hqs/uCHGGEy9QEslaT0Cm9t7rVyYqnGWogEGIl+nqUTmyxwTj62HTs/91ks3XqN2u8VBLKZoVt14pe/42oc/O6dzB2+qnEMNGHEPHHbSfiSqloakGP7D7+Dpz79BfT6cRXu5rHatk51Nh9aEaOJu2mOZIf36uDu6EDi3PVoIQGV5efiwSG1Rjny8COY3P4sI1WM2HKx4bpPYdEFlzA9RMOlhCAsLJssYqGxRbcZI8//9MfIrliDvjPOwqqL/xwD996P6rY9zGHWPNMNPf8UJl9+Cdm169G9YWOdapjB8auShsJMc85YdekVWL7lQgroKHd68qMfRcAEu+lrX1GdSdmBKjQn0aOrU9lso5bK53uSLiyscNu10tAy66FganAQD9zwD6jM5ZBe2IeLbvoGWs5YofZQyfKXxbpejl133omfXfth7P/Fz8NRLbXgb0nGNe26GhGST5MzFmEYll2oCl+sd2IZCcWtTKxd6rokwdYVpyK9fB1z1KnIrD0NDt1WiNGB738X3kxJVapiWVmR5pCurc2iSaIkmNJ0Hr+9+WYkMu0YfHI7Dv9+J+766Eew8vSNiFP4WGsGBanhh6bw1K3fRjSdwfSel5FikTT67At4+t9vgVssojA0Rp6VwOyhfjx9262qABrfw1KaJW15YprXvsVcEG1sT5eCji40fXSURVyAvTd9TSmv6nTVifQx/uwzmHiU7kb3Clu+GC27MsY247p07+SihN0m/Kgc6EXRIjmMgDvCF9mcsXJxDgniZSnN3xFLIcc6Yormd1mhCX2QpWc7SteolNUpNUQkIUvJpDkUrsrfqy1L8ZjaFSTrJKLsCbvz6BqxaBwdBReWbJmF3kTa2NYRVYFGHEYKqqFKFXtzMg6uUhaJyzZyQ/d/FdUm8LwmAuYwO/vhQBU+m+ddmy+NpBKNWpIzF7EdRSxrOygMMl6LruUw2tQXOTy1akNFk/XtU/V70H3g6YyNNk5GtOIp/DYvlKp9LoJLWuIl2fADfJ/X71PQ8Jo2Vzbv620OAFI9jtIqCQ7tnfC/JxhNT4dShds4UKvB66s1ftPnRqOh/l13hDDqWGhxqUgTsIV1Fzg5Y7TEpKsK+B/w+sdqUWuqv1CxUN8K/MqHLMnhj/g/J/4/juDky9VSg0kh/zQj322897Pao/8nwAC+AZicLeuzngAAAABJRU5ErkJggg==);
- background-repeat: no-repeat;
- background-position: top left;
- height: 64px;
- }
-
- #header h1,
- #header h2 {
- margin: 0;
- }
-
- #header h2 {
- color: #888;
- font-weight: normal;
- font-size: 16px;
- }
-
- #about h3 {
- margin: 0;
- margin-bottom: 10px;
- font-size: 14px;
- }
-
- #about-content {
- background-color: #ffd;
- border: 1px solid #fc0;
- margin-left: -55px;
- margin-right: -10px;
- }
-
- #about-content table {
- margin-top: 10px;
- margin-bottom: 10px;
- font-size: 11px;
- border-collapse: collapse;
- }
-
- #about-content td {
- padding: 10px;
- padding-top: 3px;
- padding-bottom: 3px;
- }
-
- #about-content td.name {
- font-weight: bold;
- vertical-align: top;
- color: #555;
- }
-
- #about-content td.value {
- color: #000;
- }
-
- #about-content ul {
- padding: 0;
- list-style-type: none;
- }
-
- #about-content.failure {
- background-color: #fcc;
- border: 1px solid #f00;
- }
-
- #about-content.failure p {
- margin: 0;
- padding: 10px;
- }
-
- #getting-started {
- border-top: 1px solid #ccc;
- margin-top: 25px;
- padding-top: 15px;
- }
-
- #getting-started h1 {
- margin: 0;
- font-size: 20px;
- }
-
- #getting-started h2 {
- margin: 0;
- font-size: 14px;
- font-weight: normal;
- color: #333;
- margin-bottom: 25px;
- }
-
- #getting-started ol {
- margin-left: 0;
- padding-left: 0;
- }
-
- #getting-started li {
- font-size: 18px;
- color: #888;
- margin-bottom: 25px;
- }
-
- #getting-started li h2 {
- margin: 0;
- font-weight: normal;
- font-size: 18px;
- color: #333;
- }
-
- #getting-started li p {
- color: #555;
- font-size: 13px;
- }
-
- #sidebar ul {
- margin-left: 0;
- padding-left: 0;
- }
-
- #sidebar ul h3 {
- margin-top: 25px;
- font-size: 16px;
- padding-bottom: 10px;
- border-bottom: 1px solid #ccc;
- }
-
- #sidebar li {
- list-style-type: none;
- }
-
- #sidebar ul.links li {
- margin-bottom: 5px;
- }
-
- .filename {
- font-style: italic;
- }
- </style>
- <script>
- function about() {
- var info = document.getElementById('about-content'),
- xhr;
-
- if (info.innerHTML === '') {
- xhr = new XMLHttpRequest();
- xhr.open("GET", "/rails/info/properties", false);
- xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
- xhr.send("");
- info.innerHTML = xhr.responseText;
- }
-
- info.style.display = info.style.display === 'none' ? 'block' : 'none';
- }
- </script>
- </head>
- <body>
- <div id="page">
- <div id="sidebar">
- <ul id="sidebar-items">
- <li>
- <h3>Browse the documentation</h3>
- <ul class="links">
- <li><a href="http://guides.rubyonrails.org/">Rails Guides</a></li>
- <li><a href="http://api.rubyonrails.org/">Rails API</a></li>
- <li><a href="http://www.ruby-doc.org/core/">Ruby core</a></li>
- <li><a href="http://www.ruby-doc.org/stdlib/">Ruby standard library</a></li>
- </ul>
- </li>
- </ul>
- </div>
-
- <div id="content">
- <div id="header">
- <h1>Welcome aboard</h1>
- <h2>You&rsquo;re riding Ruby on Rails!</h2>
- </div>
-
- <div id="about">
- <h3><a href="/rails/info/properties" onclick="about(); return false">About your application&rsquo;s environment</a></h3>
- <div id="about-content" style="display: none"></div>
- </div>
-
- <div id="getting-started">
- <h1>Getting started</h1>
- <h2>Here&rsquo;s how to get rolling:</h2>
-
- <ol>
- <li>
- <h2>Use <code>bin/rails generate</code> to create your models and controllers</h2>
- <p>To see all available options, run it without parameters.</p>
- </li>
-
- <li>
- <h2>Set up a root route to replace this page</h2>
- <p>You're seeing this page because you're running in development mode and you haven't set a root route yet.</p>
- <p>Routes are set up in <span class="filename">config/routes.rb</span>.</p>
- </li>
-
- <li>
- <h2>Configure your database</h2>
- <p>If you're not using SQLite (the default), edit <span class="filename">config/database.yml</span> with your username and password.</p>
- </li>
- </ol>
- </div>
- </div>
-
- <div id="footer">&nbsp;</div>
- </div>
- </body>
+<head>
+ <title>Ruby on Rails</title>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <style type="text/css" media="screen" charset="utf-8">
+ body {
+ font-family: Georgia, sans-serif;
+ line-height: 2rem;
+ font-size: 1.3rem;
+ background-color: white;
+ margin: 0;
+ padding: 0;
+ color: #000;
+ }
+
+ h1 {
+ font-weight: normal;
+ line-height: 2.8rem;
+ font-size: 2.5rem;
+ letter-spacing: -1px;
+ color: black;
+ }
+
+ p { font-family: monospace; }
+
+ .container {
+ max-width: 960px;
+ margin: 0 auto 40px;
+ overflow: hidden;
+ }
+
+ section {
+ margin: 0 auto 2rem;
+ padding: 1rem 0 0;
+ text-align: center;
+ }
+
+ @media only screen and (max-width: 500px) {
+ h1 { font-size: 2rem; }
+
+ .version { font-size: 1.1rem; }
+ }
+
+ .welcome {
+ width: 600px;
+ max-width: 100%;
+ height: auto;
+ }
+ </style>
+</head>
+
+<body>
+ <div class="container">
+ <section>
+ <p>
+ <a href="http://rubyonrails.org">
+ <img width="130" height="46" alt="Ruby on Rails" border="0" src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHZpZXdCb3g9IjAgMCA0MDAgMTQwIiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCA0MDAgMTQwIiB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxzdHlsZT4uYXtmaWxsOiNjMDA7fTwvc3R5bGU+DQo8dGl0bGU+cmFpbHMtbG9nbzwvdGl0bGU+DQo8Zz4NCgk8cGF0aCBjbGFzcz0iYSIgZD0iTTM0Ni42LDEyMS41djE4LjFjMCwwLDIzLjQsMCwzMi43LDBjNi43LDAsMTguMi00LjksMTguNi0xOC42YzAtMC42LDAtNi41LDAtN2MwLTExLjctOS42LTE4LjYtMTguNi0xOC42DQoJCWMtNC4yLDAtMTYuMywwLTE2LjMsMFY4N2wzMi4zLDBWNjguOGMwLDAtMjIuMiwwLTMxLDBjLTgsMC0xOC43LDYuNi0xOC43LDE4LjljMCwxLjIsMCw1LjIsMCw2LjNjMCwxMi4zLDEwLjYsMTguNiwxOC43LDE4LjYNCgkJYzIyLjUsMC4xLTUuNCwwLDE1LjQsMGMwLDguOCwwLDguOCwwLDguOCIvPg0KCTxwYXRoIGNsYXNzPSJhIiBkPSJNMTcxLjQsMTE3LjFjMCwwLDE3LjUtMS41LDE3LjUtMjQuMXMtMjEuMi0yNC43LTIxLjItMjQuN2gtMzguMnY3MS4zaDE5LjJ2LTE3LjJsMTYuNiwxNy4yaDI4LjQNCgkJTDE3MS40LDExNy4xeiBNMTY0LDEwMi41aC0xNS4zVjg2LjJoMTUuNGMwLDAsNC4zLDEuNiw0LjMsOC4xUzE2NCwxMDIuNSwxNjQsMTAyLjV6Ii8+DQoJPHBhdGggY2xhc3M9ImEiIGQ9Ik0yMzYuMyw2OC44Yy00LjksMC01LjYsMC0xOS41LDBjLTEzLjksMC0xOC42LDEyLjYtMTguNiwxOC42YzAsMTMsMCw1Mi4yLDAsNTIuMmgxOS41di0xMi41SDIzNnYxMi41aDE4LjkNCgkJYzAsMCwwLTM4LjUsMC01Mi4yQzI1NC45LDcyLjIsMjQxLjEsNjguOCwyMzYuMyw2OC44eiBNMjM2LDEwNi45aC0xOC40Vjg5LjZjMCwwLDAtMy45LDYuMS0zLjljNS42LDAsMSwwLDYuNywwDQoJCWM1LjQsMCw1LjUsMy45LDUuNSwzLjlWMTA2Ljl6Ii8+DQoJPHJlY3QgeD0iMjYzLjgiIHk9IjY4LjgiIGNsYXNzPSJhIiB3aWR0aD0iMjAuMyIgaGVpZ2h0PSI3MC44Ii8+DQoJPHBvbHlnb24gY2xhc3M9ImEiIHBvaW50cz0iMzEyLjYsMTIxLjMgMzEyLjYsNjguOCAyOTIuNCw2OC44IDI5Mi40LDEyMS4zIDI5Mi40LDEzOS42IDMxMi42LDEzOS42IDMzOS45LDEzOS42IDMzOS45LDEyMS4zIAkNCgkJIi8+DQoJPHBhdGggY2xhc3M9ImEiIGQ9Ik05LDEzOS42aDc5YzAsMC0xNS4xLTY4LjksMzQuOS05Ni44YzEwLjktNS4zLDQ1LjYtMjUuMSwxMDIuNCwxNi45YzEuOC0xLjUsMy41LTIuNywzLjUtMi43DQoJCVMxNzYuOCw1LjEsMTE4LjksMTAuOUM4OS44LDEzLjUsNTQsNDAsMzMsNzVTOSwxMzkuNiw5LDEzOS42eiIvPg0KCTxwYXRoIGNsYXNzPSJhIiBkPSJNOSwxMzkuNmg3OWMwLDAtMTUuMS02OC45LDM0LjktOTYuOGMxMC45LTUuMyw0NS42LTI1LjEsMTAyLjQsMTYuOWMxLjgtMS41LDMuNS0yLjcsMy41LTIuNw0KCQlTMTc2LjgsNS4xLDExOC45LDEwLjlDODkuOCwxMy41LDU0LDQwLDMzLDc1UzksMTM5LjYsOSwxMzkuNnoiLz4NCgk8cGF0aCBjbGFzcz0iYSIgZD0iTTksMTM5LjZoNzljMCwwLTE1LjEtNjguOSwzNC45LTk2LjhjMTAuOS01LjMsNDUuNi0yNS4xLDEwMi40LDE2LjljMS44LTEuNSwzLjUtMi43LDMuNS0yLjcNCgkJUzE3Ni44LDUuMSwxMTguOSwxMC45Qzg5LjcsMTMuNSw1My45LDQwLDMyLjksNzVTOSwxMzkuNiw5LDEzOS42eiIvPg0KCTxwYXRoIGNsYXNzPSJhIiBkPSJNMTczLjYsMTYuNWwwLjQtNi43Yy0wLjktMC41LTMuNC0xLjctOS43LTMuNWwtMC40LDYuNkMxNjcuMiwxNCwxNzAuNCwxNS4yLDE3My42LDE2LjV6Ii8+DQoJPHBhdGggY2xhc3M9ImEiIGQ9Ik0xNjQuMSwzNy43bC0wLjQsNi4zYzMuMywwLjEsNi42LDAuNSw5LjksMS4ybDAuNC02LjJDMTcwLjYsMzguMywxNjcuMywzNy45LDE2NC4xLDM3Ljd6Ii8+DQoJPHBhdGggY2xhc3M9ImEiIGQ9Ik0xMjcuMSw2LjVjMC4zLDAsMC43LDAsMSwwbC0yLTYuMWMtMy4xLDAtNi4zLDAuMi05LjYsMC42bDEuOSw1LjlDMTIxLjMsNi42LDEyNC4yLDYuNSwxMjcuMSw2LjV6Ii8+DQoJPHBhdGggY2xhc3M9ImEiIGQ9Ik0xMzEuOSw0My4zbDIuMyw2LjljMi45LTEuNCw1LjgtMi42LDguNy0zLjVsLTIuMi02LjZDMTM3LjMsNDEuMSwxMzQuNCw0Mi4yLDEzMS45LDQzLjN6Ii8+DQoJPHBhdGggY2xhc3M9ImEiIGQ9Ik04Ni41LDE3TDgyLDEwLjFjLTIuNSwxLjMtNS4xLDIuNy03LjgsNC4zbDQuNiw3QzgxLjQsMTkuOCw4My45LDE4LjMsODYuNSwxN3oiLz4NCgk8cGF0aCBjbGFzcz0iYSIgZD0iTTEwNyw2Mmw0LjgsNy4yYzEuNy0yLjUsMy43LTQuOCw1LjktNy4xbC00LjUtNi44QzExMC45LDU3LjQsMTA4LjgsNTkuNywxMDcsNjJ6Ii8+DQoJPHBhdGggY2xhc3M9ImEiIGQ9Ik05Mi41LDk0LjJsOC4xLDYuNGMwLjQtMy45LDEuMS03LjgsMi4xLTExLjdsLTcuMi01LjdDOTQuMiw4Ni45LDkzLjMsOTAuNiw5Mi41LDk0LjJ6Ii8+DQoJPHBhdGggY2xhc3M9ImEiIGQ9Ik00OC43LDQ2LjdsLTcuMS02LjJjLTIuNiwyLjUtNS4xLDUtNy40LDcuNWw3LjcsNi42QzQ0LDUxLjksNDYuMyw0OS4yLDQ4LjcsNDYuN3oiLz4NCgk8cGF0aCBjbGFzcz0iYSIgZD0iTTE4LjUsOTEuNEw3LDg3LjJjLTEuOSw0LjMtNCw5LjMtNSwxMmwxMS41LDQuMkMxNC44LDEwMCwxNi45LDk1LjEsMTguNSw5MS40eiIvPg0KCTxwYXRoIGNsYXNzPSJhIiBkPSJNOTEsMTE5LjZjMC4yLDUuMywwLjcsOS42LDEuMiwxMi42bDEyLDQuM2MtMC45LTMuOS0xLjgtOC4zLTIuNC0xM0w5MSwxMTkuNnoiLz4NCjwvZz4NCjwvc3ZnPg0K" />
+ </a>
+ </p>
+
+ <h1>Yay! You&rsquo;re on Rails!</h1>
+
+ <img alt="Welcome" class="welcome" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABIIAAAMGCAIAAAABcbh7AAAAA3NCSVQICAjb4U/gAAAAinpUWHRSYXcgcHJvZmlsZSB0eXBlIGV4aWYAAHjaVY7RDcMwCET/PUVGwIAPGKeKEqkbdPxAnNbq+4DTyXqmHZ/32baiEzcd5giAEg0NfmVwmghRZ+q1c06eLT0Tr7oJz4BwI10P9em/DIHjNDXDwI6d086HsHjNFJWV6oxYEudbmd/+94T7th/tAkOgLCrTUorzAAAKCGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNC40LjAtRXhpdjIiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iCiAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgZXhpZjpQaXhlbFhEaW1lbnNpb249IjExNTQiCiAgIGV4aWY6UGl4ZWxZRGltZW5zaW9uPSI3NzQiCiAgIHRpZmY6SW1hZ2VXaWR0aD0iMTE1NCIKICAgdGlmZjpJbWFnZUhlaWdodD0iNzc0IgogICB0aWZmOk9yaWVudGF0aW9uPSIxIi8+CiA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgCjw/eHBhY2tldCBlbmQ9InciPz5VvFxpAAR4KklEQVR42ux9B5gsRdX27iXnDBKVIEEyGAkiggkBQQUBAUWC8CGIKCiIApKUpCKgApJBQUDJknO+m3POOe9snND91Zk6VT27O91V1WF29t7zPvvz+H93uru6u7r7vHXOed8Cm0AgEAgEAoFAIBAIOUQBXQICgUAgEAgEAoFAIBpGIBAIBAKBQCAQCETDCAQCgUAgEAgEAoFANIxAIBAIBAKBQCAQiIYRCAQCgUAgEAgEAoFoGIFAIBAIBAKBQCAQDSMQCAQCgUAgEAgEomEEAoFAIBAIBAKBQCAaRiAQCAQCgUAgEAhEwwgEAoFAIBAIBAKBQDSMQCAQCAQCgUAgEIiGEQgEAoFAIBAIBAKBaBiBQCAQCAQCgUAgEA0jEAgEAoFAIBAIBKJhBAKBQCDkFyx79Bm78dtW43H2bBtdDgKBQCAQDSMQCAQCIWLMNNnLl9nLC+Cv+wq6HgQCgUAgGkYgEAgEQsSIvYEcbHmB1fht20rSJSEQCAQC0TACgUAgEKJEctSu+TwysYqdbGuWLgmBQCAQiIYRCAQCgRAxBu9DGla9Wyo5TdeDQCAQCETDCAQCgUCIGBMf2ssLgYaVbZyc6aPrQSAQCASiYQQCgUAgRIyZJrt4LaBhxWvbsy10PQgEAoFANIxAIBAIhIgR77ZLNkjTsDWBkhEIBAKBQDSMQCAQCIRokRy3y7YCGla6GVAyAoFAIBCIhhEIBAJhERDvscdesGcaVw4aNoo0rGoP24rTzScQCAQC0TACgUAgLAaaTwZaUrKhPVO/4p9sataq2A3Pl/FPAoFAIBCIhhEIBAIh10gM2qWbo4b76NMrwQlbdvV+aRq2PtEwAoFAIBANIxAIBMJiIDVhl2+HNKz/tpXilFvPTJ9voT3xLt1/AoFAIBANIxAIBELuYdm1X0Qa1n3lSnHG3Vfj+Q4/kr+D7L3Bjr1tJ/ppghIIBAKBaBiBQCCsiOj7I9KSltNWivPtvxXPd+DOvOTFM3bnpTjC6n3s1BTNUAKBQCAQDSMQCIQVDrG3Meiv+ORKEfQPPYDn23tjXt6ON3B4oKq/CSjsEwgEAoFANIxAIBBWNCRH7IodhaPxSiBbP/Y8kpyuy/NxeAP/cGhY319oehIIBAKBaBiBQCCsoOj4Bcb9Yy+s+Cc7/hqebOO383F4bT/G4ZWsR71hBAKBQCAaRiAQCCsuem8WYol/W/FPNt4LxX7sZGsPyMfh1X8F70Xdl2liEggEAoFoGIFAIKy4GH0OQ//Oi6M5gGVZyYjGbk18ZM+2m2yQtKt2h5Ot2guEIvMLll29r2hdu54mJoFAIBCIhhEIBMKKi9kOu3gtCP07LoqGhCVTiWjEP/pvAyPmsq2hpUofjcemq/42sGea8+tGTH5kF62+BPT0CQQCgUA0jEAgEAhBkZoGlY6igmTD8fk8TCuVmJO/So7ZZR9ztN31IfuvRp7MrzOUKo7LC6zRF2hiEggEAoFoGIFAIKzQ6L8dov+eG/KbhsXZ/8v4/yehsJDzluaTDXbUc52gYY9HzG8Nc4CDd+PAyrezk8M0KwkEAoFANIxAIBBWaCTHrJFnl9qYR+2yLYW4yO0GGw49JLa6LcLh9d4AjV6jJld16EEcWM3naUoSCAQCgWgYgUAgEPIP0NK2Dpbwjb1isOHYC/bywrR12GVRjW26zi5eO92Btr5+Xsvq/bNdVMD+UlX7zsn7EQgEAoFANIxAIBAIeYHY25g7Ktvamu022HCmCSVJOi+Namz9t+LYGBmbbdOlYYMPUDaMQCAQCETDCAQCgZDHGLwPSUvtgWYbjr9sL18GG7adE9nY7sexFa1uzzTobtVzjVAc2de2KRtGIBAIBKJhBAKBQMg3jDwpLLYMlUWSI3b5tmmL5EOjGlv/33BspZvaiSHdrfpuwa2q9ratBN1hAoFAIBANIxAIBEKeQZKWiQ+Mt607BDas2AHE+iOlYfVfNdhq9KkMGpZc1Itr0fwiEAgEomEEAoFAICykYX/G5isfLsxNx6Vr//aPiO3MNpwOYhvsEK1n6G+V6rsDt6raw7biZodMxezpWtuaDWH0U6V2zRfg8lJhJIFAIBANIxAIBAJhDmYa7fJt7IYjzbe0QAMD7Lk+DuwlAqQ6fouEqucag80G78VsWOVuZoQq9o5dsRP0oQ09GHToo0/bJRtkuLFRWoxAIBCIhhEIBAKBkInZTnu23c+GdV9Oq8mvZ8e7IxkYIzOcyXReYrDVxAeChu1iRsPa/g83bDgi6Mg7f4m7wmEkaZYRCAQC0TACgUAgEMJA9xVpGcM1DGQMjTD8GDKZ1jMNtor3gqQHSPBvCToi+mg9XRzujKAj77nWoWHtF9BMIRAIBKJhBAKBQMg/tP/Erj3IHn9liQ176J+oJj9dG8n+pURH+/kGW1mzdsUn0wNbxZ6qNNiw5TRxuPOCjrzrcoeGDfydJjiBQCAQDSMQCARCnmHgLqHstwdkcpYQeBeWKdvRx9iLeGWavmewVWrartg+rTuyplmaTtKwnt8HHXlmNiz2Ds1xAoFAIBpGIBAIhDxD/eFOyG7UBLXoGHkiPexCe/z1SPYf74TyQvY38qTBVlbSrtoT+eF0lQkNOxXvAqOXAdF+Hu4KqGATzXECgUAgGkYgEAiEPMPQQ8BkeNTe8bOlNPLxVyJP+DAO40P/o/lkwQ9fNdnqJDydvj8GHXbrjzKMpwdojhMIBALRMAKBQCDkGSY/spcvw6i99iBjq6vFHHkJDnuyKL8G1vFzHNjwvw22avmhyIbdExoNK9vSTgzRHCcQCASiYQQCgUDIM0zX2MVrOe1hS0jcfKoCCv+KCpKDT+TXwDovxus59JAJDRNFiQP/CHZ4y67/mnNDyb6ZQCAQiIYRCAQCIe+QmkJJCfZX84WlZPUb77RL1odh9/81vwbWewO/nlbnbwy2kr5hYy8GY2EJu/rTQlzkOJrgBAKBQDSMQCAQCHmJui+J5MleSyl5MttiF6+blha8Nr8GNvos0rCmEwy26rwE78L4y0EH0HIK7qr7SprdBAKBQDSMQCAQCHkJKaxX/7WlNOxEv12yUVrT4i95RsOew+vZfJLBVr3Xi1LGh4MOoOEo3FX/bTS7CQQCgWgYgUAgEPISnb/CqL3xW0uKhg3apRvDsLsuz6+Bxd62i1aFgVXva1DkOfB3oZT4p6ADaPy2qG98nmY3gUAgEA0jEAgEQl6i/69LOxumaXdmJe3ZtpwMrM8u2SCtPHmAAQ0bfgTvQu+NQQdQ/3VR3/g6zW4CgUAgGkYgEAiEvMRUBYolgs3U0tE3Tw7bZVsY0LC2s4Eddf06d/zQSCFj7HmRDbsl0NEZ25QSHeOv+d/PZInd9Vt74E4QcclvWFaK/dFzTCAQiIYRCAQCYUnBitsVO6Zp2CZLiYaBRMfaurwl9hYyE3amUXujWTN2xSfhWHWHGmzVc63oDXsg6ABqviBo2Kv+T6FqbxQaqT3MyncmZi0lhU8CgUAgGkYgEAgERO0B6Zh7GfQ1LRUkBjAb1vFz9Y+FAIZVsVMu0js1n0HKl5rW3WTkSeROIwFt0Cy75rNBixJnGuyi1YV+5u5kPhYQqeQ0+6PrQCAQiIYRCAQCYS7qDuUxd6r7xqUzaAszNh0Xqn/begaSioZv5mJoNZ+HY5VvZ6di2meTAIkUttXEh0EviyxKjL3pl4Y1YaYRcnqH0PMRdKamEuyPrgOBQCAaRiAQCIS56Po1xtw6lCZPkJrAWsqm49U/bjnNj4i8bzR9N13kuTH0iRmc0aQ9/JhBAs1lL3b1fnD0olWg688fpmvsojWEfua36fkgEAgEomEEAoGwQmOq0h7+l50cy/VxB+4KTSxxptlqOMpqOw/6i5SYeM9qPtXuvsYX3Zi2aw9Mj/nr6h/LbFjrj3JxPdvOhmMVr2vPtoazw+S4nRyFPyUY8eM6/iC4MuDzcBMf2EWrCWb+c3ouCQQCgWgYgUAgrLhITWJ6R58qWCFVOsXetJcXwqHLPgYRfxCwwWvK9M22w+GwfO4dP8dipJFtW/MZdfNSx89ySiraz08frtCeKg1nh9PVdvX+UFE526KaRRN2+TbQBVf+cfjfPpcDKpzesO6r6dEkEAgEomEEAoGw4oLRkuI10z1Fn1CnklLTdutZVtX+9uhTwY9sTZaDPgckcNaBYfjfUcKu2gvD95ZTFD+e+AB/yf7azzM/VhzUI8AleX+1UB7yIkYqrsjFrey+UuhtPBnaPieL7XiXmnDONNkl68GhK3fzrwk52+b0hg0/Ro8mgUAgEA0jEAiEFRfjryAXqvyUOs3V/lOMkotW9ZlKmkfqqvdJ7201e7o20H7Y4DV7iqykXbUH/rjpe34OxyUBwSVZBSkH335BLm5l7414uIG7cj2LQF1jLWOdxvlT8XWciuyP0T8CgUAgEA0jEAiEFRY91whvq0/a1qzix7UHO6mkgTtCOHrDN3BvAZX6ui4TNOwY1U8t5H6+BQzrvpQuSvyC+pexN3Iq0TF4t7g1/8j1LEoOQ1cYFCXuCGWu/jD8CI6/dJN0Co5AIBAIRMMIBAJhRYVModQdpq49k2wHysYeCeHozSeINq03Au2n788GzEoaDbf/xM+xGo9O97NtaSdHFL+E9A6/tl/Kxa0c+Q8eru8vuZ5F8V67ZENdduo6u36D42dsn0AgEAhEwwgEAmFFRvdVQkbiZ+ofT1c5Wnb9t4dw9PYLcG+9NwXaT+vpuJ+az6tbtuq/gj/uvNjPsZqOT9OwLdQ0DDQnVhONZNGbEY8+h+fF+EyuaViXXbK+KG312xsmE7M+evYIBAKBQDSMQCAQvDBZYrf80B55IvIDTddo/azrtwZqfjPN2AIEshNXhjDIoX+GY9fLOCHfDxAeFTp/JcL9831RvrN0adjEuygFGaRjyoCGPS10Sn6Y61k904BCLzo02A19f8TxDz24RB7m1FIYpGUTCAQC0TACgUCA2j/QBlzbV/eLXkSVmgBn5KLV7YYjQX1OQRXeA8td6F86QSPabnJoWO/NYYTvzUKncVv/QucMsbdFh9v2au17WcHYeoafY3F7rtLN1AZZ7AcVO6XP7uNBFfl1MPZ8TlvRstKwIBZwMjE7eF++P8WzrXbzieBYPXBXaP4NkfDEKavucLv3ei0/PQKBQDSMQCAQVmTUHiREKd4z2GqyyG46zq7e1+7+nXINPtV/l9PB1XCUitnF7cpdMGOjlOhIjtilm4fp7MSOyIkKo4JTZf73M1mMeScdNzBJw1p+4OdYHb9Ib77MnirXuN1fTLPutdTWW8Ex8T4qDeqoOCrvzNjLdt9t9sjjWr+erkXLryBtXW3nCIZ/Y74/xZ2XZjRJPpq/4+y9mQwACAQC0TACgUCw7Zl6p7dKm4Ylxz+0S9eXYZ81pdB2Tw0/aRcVOGoHVlJxAMZGIGOznbpwLt5pl2wgUi4nhHNNZKfWwJ3+dzLb6lhOjfxH8eP+2wIV73VeIkTVl6t/XPM5tFRmRDpqjL2EXJRxv+Bo/4mTW0vFVDS4BBkgO1/fxK/5ZNHbdnkQ/pgTenODQ8Naz/S/n/HXIxxkYsAu2wqTsezNQyAQCETDCATCyovksF21t4jLdZ2RkqOvObSqeJVkrERFljpAxw9/vyYUjHlj4l2QQNTSKrSg+Qel4b8VzjVpOTUEfb/kmF22tdAOuU1Fw0QjGYv7faD7CkGk31X/mMsqsj/GkaKGkw0LQ2lQjlwn4TNVgYduONI/f2r8Nh6u5w/5/iB3X51xccwTTVYc9Ej4AkTziVEVrA78A/ZfsoE9VUmvXgKBQDSMQCCs9OCdRexv9GnNLVITFU7MV7SGmlZlchvN2jl9SE3Cpu+Gs8POX2bwugCpDO6qXFSQ7FY1rQ09KLIuv/VzoMF7BLN6Uf3jukNE6qwk8qk124Kde0oPay0adowz64YeUEzRsdfsojQNazre/xHZjMLDPZTvTzHv8ASSs54d7zbePNGHDZn8b/zl8EdoJXHFhM1AAoFAIBpGIBAIdt8tQi39Uu3wuk3UtvHep7fUm3T/TtC2VeyJD8IcP3ZGFdj1h4ezQ2naW7G9WkjATcYjNQ3203C+ham+uxQ76f9boOI3KeinoyTReoZdtCo0BGoKObATYTG6P0zXAUsPi4YNP6pv1R0fesEuDqwO0n7+otlPGyEVc7LNlbupi36z0LB+u2QjYbHwmUiyYZPLsUK16zJ66RIIBKJhBAKBkO5i4uVbY/8z2Gqm3i5eRxRB/VsjIq/GiBx+/68wx9/+U9wtaBvGQtjhVClmBko3VmgPdl5sl38iewrLiqMYRtGqlrIEixO/ooJU44l+Bjz6FN5BLSJtWVNVUIyq8Uu751ogk+w6NJ8IbXjGNKwGdTLqDg3hvmT2Mfb/VfHj2Bv4y7ZzA0wt0Y029nw+P8HWTEYjor/zBYFQ2cr4eCSjHLzf4HVBIBCIhhEIBMJKAAscuip20AvNBVKT2G0PnTPXaPw+5vRK6RiC6WPoYcGaNlcbZ+mAXYeyj6V50WpeTSyxt0S321rZ80WMe3A7L6UTAEhZpMVOGo/zRaTbMIbuvirkicETemkyCUTadPuJErw1tQeFMJyJD5Bt6kyh4ccCOWLz8Tcen7s+ukAENcM9z99Q5eUqXteO90YySFlTmucXk0AgEA0jEAiEnEJpOTU/RJ21675s5gol21daTgtz5LPtGIOC+EdjGOwjCUL8GDK6ZwhHn8kQHVl4XMuaqoEsio5p9ehzgcQeZSrDn/uzK3OesCt3xjRd5d4+2uSsyTLgb2HRsJH/ivan9dUZlZEnBGG7cMGNSVp6ZXtW27lIjwcfzuuHV8pCFq1hT1f5mfLd1wgh0wOj8oCWz1S4NckEAoFoGIFAIKx0GHkc46qqvbXaUXpkqHdAmCrejBBWfkpIC7wWzi6bTlR3W0lWULF9IKNnm9scp9tm2s7xs3m8B+oGQRXwiFBvsIXGWUUFVt/tfm7Z2AtCNf4LIQyn93pB409R/9jJhl3i/4hcrKWoIDnwYF4/ib03Om1d/p4sNnNQHfSPkYxQmgHCmkUzvTsJBALRMAKBQAgAsE7eNF0NuCnosysx04C5keJ17NmOMEfScJSmgJ7umTWfgYr8Hm1sQw/jQcs+ZlbPmeXKNAlFQV+a+6kpoILAh3f3I8/ggY4LA/XzTNfheUGOJTAY++ISFLE31T+WqidKtwAPdAkngL6bFCwjObGYT6K0//bnz5YYRBofkUYiw+RHWJ6quWQTCRWctQkEAtEwAoFAWAqwbCul+AELr7kGvY6id2rSKvsEj/ZSY69GEobqiz0qgnjhp9xzretvBu4U0iAfDyoNwkhp8bqwq+r9/GQzrAQQMMjL7aQ2vDZC+wVYlTf6rC96KXqW6r8ewmBGn7Kr99FVy+i5LrjIYbL7JmTjbkYC7MrH3rYbj7Wr99RyC4gIA3cIiwVfipSjT4tqzw2Ni5M10XuzUBA5ZxGuz1Ql2MdV7qpvjUggEIiGEQgEwqIhGY8l4yrd6p7fi2qoz2q1lDR/H3/ffXWYY4WivoIQm6OSPX/h8bfVcLTrj9p+LGoy91Dr2nsD0hHpvGLVXj5zBZwPV+wAmbEQ0XER0ubx93yGvzz/GQoNS1N53R/2/MHUDS8LyeqVTgD3Zl+GkJ57cO/2DK3UdvgxsNgavEf39zIx2/IDP4eTM7nl1KjeJry6Fa7k/Tl/k8nVogKoayUQCETDCAQCYSlAFVbKWqPiNUH4XglZKtZ4dJjDnCzC3qogJlGZpz0o/JRrD3b9UddvhK7GiYEPmIIgnos9Job87IDLn4SeDev6NSpG+hJ+SKunrOtfeiQIWs8Q3uL1AabBA8hCO36V5Z/jvY5nA3Y8hoHxV1Hlv2R9XSPm/tuFHsnPfE2eQwVLuTEyInRA+ow2AIOyHCP2rnOPRp/J0UHZyzDg0gyBQDSMQCAQCAo0HWfQPgRNUGumO3x2DkdcHmlYMY6h7kvh7BAsqtIuZ5W7uIZTk8sxXAYZwMBpkIYj07Rhdeig87158C61eeDGWSx6jvf4pMecpTd8I9fTsvlEnGYLO4IYz2k9HU4tOeq9j0T3LZAULSqI12czEkiOQT2qDPFbfhjOyBnz13apFs/g9/xrwSf67NJNIrdH47cjrMfTgADOWrWCZJZsFPLT4bb00HoGPDJQIGrZBAKBaBiBQCBEBdki1fRd9Y9T03b5dvh7FqOHBRZP84C4ZEOfhGEe4p2Yxinbyk66VGbGu+2S9YRSYuBSwJZThZz3u36izfq0KVPx2uAhFiYN+ynmlFhw6QMT7y5aRxB3U6jae34d43Q13rWigmTfPYrZOvYatsZl1WaMd9mlmzmUKZRUUrwXIni5Tx0xEhbrV+6WzjAf6+eI03XqFYfgGH8NJCtDfOT13wyMfWHW+vu5OCIXkuGvDitO3wcCgWgYgUCI9Es/DD0wVmIlPX2Z8Sj/hFYYJ8OUvj+HOYz6rwka834Ie0vFkNd5WEIzqsYDeg+qpo+OXwSRlADfZ5SgDJWGDf9bVJD68pKSNav+pCMC3D8gYJB++fJ8xiLnSfGq1tjriid75GWeDUsxlpvlIFNzMlehuBLHexzaUPMF3UZBdpuaT/KpPjr8KB6u9fQV8O0UewtnoO+lBCOM/U84jC8L+f1GIBANIxAIhCyxeOVuoENQ/emotJ7zHCwYxdKsZXbsDY1I5QUM+8JdfRdiEhBWBgcj1dyLDGiYixY/Izw8jcBO3FcKaw4G78bxd/3a/+mzeThVEerNnQD5h8Sg/6eDM9WGI3P7VPYimWn/yfx/YoRQO0GXGngIlRLrD8/+i/5bHRo2/noYQ7dApmW5yiwhRHReKsQz7lkB3068qnZ5gdV2dqQlglYqnib5B2Gj7PBjuTtHK2UTCETDCATCygjpEhvEO9Xgi5uX1jf1XzWgEIzVlG0laq7eCG0MA/8QY/hNONEw49VAbFaxJ7KLBCZnhzAHBX5lDwU94OizgVQWGr6Bo50qz6OJMVWGuYi6Q3N63OkqVGjs+EX261zxSS3pjvGXUPql7jDFmgL7G3kyJObwU6t8W7vzV3ZqMhfXquUHQoWlZkV7OUvPaCDJr0R6qGRiIjVRahenZ3v913J5lqnElE0gEA0jEAgrI7qvyqBhn402om0+yWJRRW7WyI3Q/1e8AoyP6UAW4PXfHtoYYu9gOVDTd8LZYeMxiqQEC/L6/hTcnCojoE9H/E3H+9lcCqWMPpdHE4NRHa7IUvOF3NKwOlRP6bgoG7l6RVeBcLYNtRDBzy1bzqH7SrzsRWvAExpaUD+WqyslNAzLtlAKliw9yHppdnb+BEiN0Pw9nAmxt+jDSCAQDSMQCDkINBvhG89Dser9I8yGSepSvW/+XYR6TD6Ub6tVwDbypKBt4S0bx7swYoacZBhoP1+d4pO/6b4i8PiFKAjoLpoDleVXzS+D2ukapEM5SBRnYuI95LQBfaKSwyjCwf6bVWSv42fiqfz0kpTFs5LgesetDlY8gXUj9aCgyyjPiGz2RaHts/9W+CMQiIYRCASCK1p+iB/g0k0jXHOVPS3Fa4W57h5OMJcAcoilWY9rRMkfCgnp9YB+hDOGGbtqd4yYffcyZe6v5zohRH6K64/GXxW20T8NejyZePFnP8UJYb6Vls22Ym8YWBvnsINF8vzu3wWd2HWHeDmn8Yo+aH47Ykm+u1LTYPmd+xsUCuBl6znmjp8LNaBbIh8Mfz+XbRmaJn7v9dibmorRN5ZANIxAIBBcwL7xjlra/6I6SvV+GW5Cd+XdRei9CcfWealGdJvEXiZdSW49NJ8g+kBeC4GG9d2O8gzeBXWjT9t9fwxBGCPehYyl5nN+Nu+6HMVC8iobBn2AHwszRamJ0edwJjAuHXRSfX+BBGVG1ksaH+cg3xIFZhpQZib3jl6B2OMk6DqyqVW9P6hxuv2mYkd0vfPnxaePqXLwigjR/7rvzzivyj9ONIxANIxAIBA8v8EoUlwQmUixBdG5pGFdv827i8DYFL8ImmIMkra1nqGryq3EwN9F8P37EPY29GAuak2dqHECBSf9ZcOG/pkebWF+0TAWQZZt7b/S0jfGXxaLAr8KuisW7mM2jDNty3KE6Syg6FiK9vMl+e4aFaV0XZctpWHHO51XbtuPs/8GClN5Nvu0yMfTelZa+nW3EIwr7HQOVtqCt51LH1gC0TACgUBwR2IAeqKwCOrqqI4ilQDBhelbeXcRkjGrbDuDshxZNsb+spZ7+YkpRQ4EXKoCEyehO2I1n5QTxjKNi/dVe/ngpcneOzB3N/5qPoXLPWhGXP/1nB535D+BZCczwYJ41D55esEtm3TC5XD0OXMOTKKGZPOQMww9gL1/We8LR8+1IebGvTC5HIVAwlLmYANGZZGtIUlOIBANIxAIBC9I/Yzm70cWpk+BEiM6bu2aj27RUq+i748avGHc4a4ezVdGkPLooDcQDyuUtxqOzcnlE6mViu39KJV3XyOi0mfyaEqwSVu5c7oo8fM5PWyfkO5sPSvovpq+53phY2+InEyhPfnhEnxtCZlENslHn19KA+cpSp6Aylqzx54g7vtXvU8IrwKvSziLj23wmYYjj+HIlxw3JhCIhhEIhMXB2PP44QQCEBlBkkV3RaumJsvz7iLI4TV8U+v3nb8U0iabhNP/kByzy2VGbiTo3rp+I9ozPhHC3nTAG43YFEqZGwHJhYC88jOwZsChi0fDuTxs/12hlafWfwV3NbGAaMXeQhpWuqkd71+SL65abje8SmqycikNm00nzHZemP0H09WhpUO9wUuXi1a3Z5rC2aFM5FbumiPjOAKBaBiBQFjaYN9g3uletCoYWPmNHiH0r94PxMdTE9liiyqU/15eYI29mHcXQTZjQMytURPIQiUucx+i2xXqSRaCZVBANJ8kpSmtmZac0LBD/GfDOi8WV/KpPJoS0PC2nfDdyqGee/fVodlq85sCNOzd+f80Vcqzr1b59tZSjJihDnYnYRo2tmSGnRzBWtDiNV1NmftvFW/jt6Ok+7PQOAotiBeHts/OS3C+9d5E31UC0TACgUDQY1D8ewztYVf6DoswdQC9NF+Bb/xCSIHBnmvzLzwahzQU1+vTCX+tuFW5t1kCTR1/CzvdwbuD7kre0NLN7HhvLi5g24/Twm4b2ok+4217RFHi+Ov59FjMwqJ+7u2bpZ3XWOBaO+nivZCGjb2I/5RjAZKwILN5S0omMTXwMG+DTNUcoKCXtQdHO5SBO9JX77DQKiCspPMJyCutHQKBaBiBQMhrSALgWz+Dxaw8evD4DHdeKgSyj8/HiyDNzfS4qNX4PccMbbo2hAH0/02sJd8cdFe9N2DuceDvuZpCV/mnYdypFgT9qvNseWKfRciGtZ4p7BACJ0MavunqrNB9hegIPXlJvrJk/Vv7BUtp2IP3pad6QbLhuOw/gNqE9JPbcmqEw2DvK/aolqwHSdGwMFWBI6/aM/syHIFANIxAIBCyYLYdfZ/AXtlvo4VkWW4R5OizWMhX+SlovMk39Pxe1CV+Ontd5fxw+SznfEPpRx96OMySnthb9sgTubt6jDryXKIPL7WOC7EKayqfmnwgG7ZLerru4iesnCyB6jLjbrcUJCgwGxa4drfpO8hvF1rDdf1a0JjzluQra/AOkTq+N4S9Jfrt6broZ1TCqQhws0/ELGUhvA2iGkYcxD9Dl8aV78PgxbQEAtEwAoGwckHWL/Vc43MPUE4jilI6L8kehVTsgD+Yyj+VDhaKlW6Owxt5Uv17rhAAMhjb2vHuEAYAhlFpJeu2/8ubYPceCBx1agXH/uffnpvrnRSvac805tF8kOVhoDsybbx57YHpk1rbTgya0bCqvUSJ5itBT6HlVJFmXJCt7blOcP4bl9aLykrFreS03XZGmKbzvTfbpRtDPjzShYDhx5xS4eSoyy07JT1t1oXXUUTgmkzFa4X5uM00oLsD221uqqCjml3JnKa+CUTDCAQCYU6IEESeu/VHDjPJ0vef0YQWvO4uCjQcqRAxy8TIEyBzz1hKcEUNhwdunEftLowqF6+Deaq+P6k45KvIIYceMKdhv8IMwMR7+RSRJVB9m/EiO2W2LQuyufUzpAcNawu5hjhwp5qAPBLKKTHgXiCFJ9UpdRwa8i5YTtq1+yHLne0IurfR55BC+Ju9+mj/KR6l7rDsP5iuw7q+2i8aTzl9NB6r9QY2KliQDnXsFUo0hkA0jEAgEAxjthiIm0N7z0b+lzP7b5eq9Nl9jTHgLoBQYP5XP7X43+/Bu8Vy9eYRrka7xu7jdvk2afG3rVwXy3OJ7t85VZftP1H8WEpHdl1mfKABUWAWvIoy3KaU6n1FDa2hjEG8C6t8fTxNvCGtZH3wjw52LZCGMSq4sAWo4QhBPB5eei8rdkk5cao7NDhdSTadhu7hkTrXMbrILc7Z7Rh+LPtvem8U3PiWKF9xhZDJ935Suq+yq3YHwVWPF1FiCCdPvNuZ7aF0yRIIRMMIBMJKh6bvBo1F4p2Yz4Fw/LdZfhB7WxjL7DzPmdSaqk5Ody7yFZj4AFeji9e0Z1sWYQA8bmYhZmJg8efD8CMODWNhmTegQT9tP912jnloeL8oaLwj0ICHHoA+Lkb1QwGjXiwSRT89w1ZGxuFZSAqZtD3MeMJMI2gn+BY7mctIrcrdBQ0rmzvP34VsW1h6jK78ZiQq9+GZBnhCQV/kxBD21vIDsXi0WvbFo1DAdWiwTnUo+2+kz1uIyhlzIPRsvTO0I0+KC7KGR7l1ov9hu7gAeNfIEyF72RMIRMMIBMJKh/6/isKSo/zvRIqzNWWTAktNYXtY8Vrzq4msZDohttiQ448uPPVA+09EJdsbi38pZurRUE7HSZbdTZ6gyNoW6A3plBWEQTHmwyYVz2SGYqgt5yooJabML12azzceY0h9HxXrFJ8KymFSU1Z5Ov1SvI492zbnn0afVmStQ+Hw5dsAj+24MHQyZkkxm85LA3PFMVE+GnFPJpdLYX+tZ7qk/VNA2rkjvFkqXntyMpK/vNCu+Sy8bD2mPVemwaG6Ij74tF25NZRk1wl7upzpshIIRMMIBMIKBiv2AX5NIRvjdyVe+vlAe0PWcETk3JTtRovDRf8WWoSXNXaZGU6l3MuBoMPKr9BF6ACNiu11ixLB7Hgbn0Lbg/cGNpSz7NbTHf+A2fYQTh8C9I/59A1zVjSONNuQXQFs0fxM4Od5BiVGiteFNHUmpFl5xY4QdkeBTB3RsJVXrK4rQ/P7Hnspw3YiMrHEmQaQ5cBmtjaXhYx2bMVsPslgz71/sqr21m21ZVNi7HlFnr/3BrwgZVvBsL0f+plWUPFBA7qD7WA+4Gl5DAKBaBiBQFg5wQKyxm+J+OZZ/7F7+bZey/lDD4k+9UPz8SLIsKxq92hKqiyvFriJD5HEdlyUF1ejTqxzd/1GeeOxk6r2QOMeP7jmaXmP9p/6v6pSCrxyZy2/AZ3HgbPQ6k+bkxDh/dX8fbMNR58RT8eXg44/MYAVwsBL50beUo+n9oCoZk7XZU5h20KBkKAcT1zeifddHjFt9P1F2FTsG+FzJAVR2APlhqEHMT85/G/d3TI6zciSpqSQ1ipRL7699fvTpL/CwJ30CSUQDSMQCIQAABrA47OD/O+El99U7JidxrDICXNu60PtVr4hOYqBSNEqi2BjNdsG6+VQDvSjvLgaXFRNixamUFvCh67gTCNWP3oWQSnQ8weRgDpCM+JMdV3lFWs6vmG7GRPy2i+K/N7vzTbs+6NXTa9ZSN0FjxgvqpxXhNZ5MR6l/qsmj8aIASuWSW9YzgjbJJCvFrEnZWGeLTFo1XwO1GWSY3ovKzHO/lsjfI7kfHC1B7DQPh5M6vSSQslhhzKNPB7OONv+T6RJt9dSCZLVrYzERtQHSCAQDSMQCCsLWOhZf3hQF87paiip6vhF9n9N9NtVe2rnWBYDLALGyOz2HF/9xFSbXbZeUNuAENF1uS63AZnH7dJeBdvB/zajYc3Y1tV6uv+hzjShRogmtZCr+G5SgYw8VO4siISJUiLbUHp/mQqgy7QJIxIBMVOPOhatZ8yP+LkmvoGmggWW4uXbwHzQKWKc7cDVhChkG6D8FeiHVbqllYzNZyby4WXMR8nEUjGsOy1ZL0K3q/HXUBClZEO3clkrNZ0s2w4EG+ffLM8lG2Ts3w2HAkE9uagjHbxP7yE6FDN4OtaCBALRMAKBQFBg9FkReX8jqkNI/5zoDhEEUna/7ewc0zArMQ5d70DDPpcfNOw36mIqGalzw+Lly5ITht7cLHrmtVWNRwcb7W8hqnbj//NCWCnp6aYLIpUSq/Y2y++Bfvf6Ipy93+xKNJ6C4umacbAXDWtAcrtQXkV6tXdfrbWrsRecAH3sJfXvp0qREkfxEE2V4c5bz5p/XyY+cMZZ83m16bY1iy4dtQdHuazzHXWDZWLILtsSZq+BG3XK7r0eTjMsTdem74lU2Ce1comM53NVz4odI3Q5IxCIhhEIhJUIkATYNb3Gufp8nevQmJ5ogCnbOi8MsubHeRVi9XqjIOawVob2oxV7xxp+TKMIU2QqWHSoWVUVKQb+YSAix2LZ9I+To4Yyj6lJi0fDVXsENf5iF01nDyP/VfMKaeNmKlrIxsA3ZGxhssRo+InKzyENCy6VOfZ8egwFqeYFBa71XxMs8V6tXfVc41wuFvor57AjAb+GHXsn5DmJCcPCharrqZkOu2Rd4Uyop1HZ92e7+WQT8uPjLqT7HhnLmm31umgzTXZicNGe9HgXKDRiieN/dJ5ZKNZdug7gBALRMAKBEDKSw3bszRAWJnuuE8u3F0QzzjFQFUctkOfy7jKmpqyyj+Pwhh/1vZvpsaZUEtbjkyPP28Wroh1Z56UKnsCCQhQ2aFz8SzH+upgJGuIZjUeLMO4xwxCwF122Iq0Ny4TUqPCQCpSC9VV76rbr4PQewekNyuMmgXViAGvkgBwGJgachi0vsDrnOfgJMRWomXxQ43RGQaREygnqTMuaz+LvGR1VpqQMH06QFeG1rwv2bMX70TUB6OLNefEykSmmSNXwg0MKjbIZqLMAxKia4wBJCocEomEEAmFl52Bj0JEFQfATQXclWztAY60tktE2n6CrhL4oaDlV1y/LI6iexWgmNfgQpjiWayQ6uHwCCIRULP51kFVejd9S/5jHxx7dVh68BdNHq6YmSnNxXixGVwoSOkWJhqIj4N2cLtYq39bMwSwxZJdumg6Ft/Cio/Eue/BuddeZNNca+qcr2eu9QX1zptvs4tXx94xYJoc1Hp9ThHLD/mZtdTqXiDP2rPXM469CnxKMc1OtcUYOy+mDHbwnj78dwzhjoV/3t1qb1AkN1Z7r6NtLIBANIxBWerDoigcooRCb9vNF9HB3NHHwTU4Lh77AtDWTajnXajgWFPymyo0PyijlxPtaum1SSq5ih4BmOOlb0+dk/5Q1PzIVGXo1lw9MFmFJlY58f8sPRYbzGeMDNRyFqZuxV3NxXv23CXmD73hMNmiS8VGUKHvDQGLRhITMNGI3F3Sjuc3hZrQbLloNWra8zvFWkdGdm5xMjjqGxf1/1Vjeec9p9KreTy3RkRyHKxaRBo/0k8gqcdl5iVg1ONrOB4y9iJeufBsgz3kLRr0wFbaFlnN07A301WAkMxRziCUE9ioYegj6YGs+C6ImBALRMAKBIGLZb6Y/+R8PwZJVql1HpBXBPt48xi1aHZQVNZHZgl93iNkR2beT01SNDEA6floViwND6YAfuMsZubcEpTQympfEWBxu3ycSO9uo9Q+7r/JPwyTvHX4kF+clm536/uw+RSdtp2PNpOxqpgklClmgZoThfwmR973d1iaSA/90MqsdP/faW9s5oodqbrAIxZY74h7GX1aPavw1pOKaluuxdzIayW4K+cbJ2rmsxs3yX/OkIrH9vKVRkcjYNZ8qmooystIyuJDMkkMq5qj7sEeMQCAaRiAQRBx8hYiuwpAP5skN6AaJxt2r9QyxZH6ZdvR8nRPhAds0yVM1HCGaGT6llRBjoTAmr54I4WSnq5yUQtYIUmLkSYOQN/KYQ4TsjIwxSuYNuaZuWpTIOEf39UK776pcnFfHhcixs/v/pjHbIZJau5plw2TGxiPVln2l4EHlEkN84L8ODev5g+fze1r2JsPxV5ypOPGhelRTJbBWwl8FOpo9Yy8KfY5VoD8tRLDHlhOG0k3seM8ChjqCRaSgjLI8D5YwBp2VptGn8/erAcYG6xo4PbDngpfO+vs0GLjP5eflahZmDMu0VjEIRMMIBMLKgqlKlPjTJzYekKvaPddGMtqx/zmESqcSxs6QWOR/kx+ZsMpTdbuzkCWeacwSvT7eTehQrPJatSaKMErWUcWIHlbdV9BrW1lV1f83/0647JqgfPwvVdHtkD38iNXxC3v43/7Piqcpyrb0KqliJL9iez/2zcJI2uo1VJDr+rXw2jrVdVD9dzs0zLtgmOfGWcg4T6APhOzXRrun6So1E5+sxt4wzTB95AnRSLapWWucEvFOHHntgVn+tfdGkUvcIy9EI0afc0Qs8lnPXa5PjfxX6/eyrbfhSMM1nUlYmCjbCvKrOYapk6EH+m4R5fSfoYiDQDSMQCBkhsyzfP3Vqv50CAJlUJCWthKu/1o0n8bRDInkJ7XZ1A8dNjVsosjXdq5Qe1tTq6+s4+dhfm6Tw0572MA/FFEvJ2z54W1ttZyJK/rKlW9Jkhmd8E1Tla5fQ/903A5852m5YH3tFz34lZWMWWXbpsPoXcxk9LnZWlFBousPZqOSOvKM0Ko4Hjzmo+65puQYWrFBNqxhzj/NtmHNJAjwtKgnQHLarv4kpEM9RjXnBj2QEaZbYc5FqW/Zclq2VYBb1VcvIBirnO3Q5eRNx+N46g8P+TqECOncAGsNGtx1shhX+nystkBHWYGQHk3k7hzZccu3gwxzcP+V1BS8DfAtdzlFHASiYQQCYS7SEnAWCIWFUfvBfZZZ0Ma+vlFA2sgO3KG7yXQVdoezv+bvGxyLkR/e7sU211G/gAqxQswnTNcZMZfs/2fZuOJtswPsNy263X5eXswo7NoqtGKqAjYRByebz/S6OIxxLQxkQTciTRuaT1YcZfBuh4f7XlZnUSC7C/FOT/oxkSrbzk9RYuuPFMbQbkPifn3eZatsn1jyV5Aad62otCZKcfaWbjy/mhTIzDLX0r6sYBcq3qsVOrNQvv0n+jKMZuj5vXhd3JV1AYNdeav+KHvivSiWI8DVnUXzxeuC54R6DWvA6SDq+0v+fi+E15/WzbJmHSvq2gONO5AZb8EFlK3CTE8peGMRqrCG4r8iq+LZF3a2ncINAtEwAmGxMVVpoDCRA4B17MfTfflvhHF25Vgg13xiNKQRO4JSTSdrb5OC4ihZfaRf7cNiiPK0G1jpZhAkqY8Tc3S9vUU1NGnYwJ2isftcr63Z1714nXQy4Zt5MaNkEQ4LQ70x8SEnulbjd1wnZ+0XIQnTcdGCqz2FFYDKfipGnzCruXYQc22NCTBpYVGiSTYMtGd29JGttabrRQvWml60n106mQyMd7vfi/dExe8C0fzOX2IyrXz7ELR85t3hqVa7eA19GUazx6rpOCHqU7MID4JIg1hl21nKrlT2UeBvzrItoYw2P8EeWJ4Xrd5fq3qC/d4xPTfv+pPuc61n5e4cpZoI489Bypj5PS0W5uA9v6fYh0A0jEBYbAw9DHEnCyvzyoOYu+j2XBNG5BPHLvOIhDrGX86QrdfGwB2O966O2AZGiKNIq6r31a0RqvuS0I24wn/sKCHbn6r29GKPLI7ndFGzFSdqyFYf5UWYrsWCpfJPZO+5ar8Ad1Wy/vxuQDbTKndL3519FFmXwftELLgs2iYTNkJeNAuaLto1VDFheM3eDCa9i9bwo4JfbenVUsXFRaAw7xSv3Y29ICbb7vOLzaSgZeMx4V80KFpbVUTqL4TK8EaxrLdsqxA8JEwRewMrtBkNK9ncSoyqnhrhblz7xfz9fsm8paYrCXeW569QozJdeDnU4CoD6OJ8kKMTnFzuqNEE1/6RmlIV2+e0qJJANIxAIGSLm2adMnFY8uzL3ZH77/TSQmSxaduPQxNx6r1BfMZ+F0GkO4iRLgtx9Gs8Ym+LSHc1yEbqXrUk2hlBq4ZeDo2dMirXHRZCk/3E+6JObBOvPAY7UNVefnT2IsJsCwoGskml5i2bin6k5gWnNZuq2FnQsA0WCH5YQMBQAHNCi2AUrxNCs4dX3D+OdZLV++vw9lQynU+YKsVws3Td+ESDweG6LnPKvVwnm5WqPtirP8q5SkL/pu7Q+f8kuyuVbXg+gCr56WRduAbK7PHh3YPNJyzCUyDb0uBBOFs9H5pPFGmTa+z8BPtglW+LryOdfN3ocw6l6fuj8eGkEWXrj3L1mUw4aiLsXRHQd4S9DdBifrWgWTUC0TACgRDGZ6xfCNfm0KmGcQmptT3blpPTHMJF6PLtwi/oT01b5TuIwr8HtLeagHJErPt61OBwfbcAl5gnHOcBmb+C7qCg2msggehoPBZ73WKuu81oST509rNohmdE676s/nH91zERtFAJHc5rjwym4bItlIwOeh2C8Rze2sSou2dzV2AaNgZutpBQ2kvn7ltWmjvF3sRotWSd1LTJE9rxM9HBcr7Xz2oPEi+cGxVBM6a8vj3/n2RDZufF4V80mTBp+UHIex74h38dzuDI1GhV9p7J1SXIiJZEOKrpOkhW61cEZEI69Wl2TNUeINqiNplvgaDzKPEFGsiRvpSjWyZX60JZcZBZ6HxObxKIhhEIKxMsu/tKp1gcChVmIj9mahoyb6a8JSC4ARHovL/p+pvZDqAWppUqdkYSoPFbJjGEECrQaZf3T0EHMBCHxdS2BWF3MpkwKI6y4r2YXYFQ8javnzZ8Q+iJRaK7nZrtszsvhAV7LXslCwgYZyPKlKAoO7QGH/D41zQNW8AwuZQlyPd58uTxVzKM4yaipGHDGDuaFCXO9tyDgvLl2xkMjwXuYm5YI09p0TBP2wN79FnX5EPdIYFLbd3BnmJT0R1NtJ6FjWGRpkAXgj0j3b+zm09yCh8YqfDG4L2ORWF0UvXsW8DzM5NF5m+2fqyxLN0MxFe0vgKnYOOipq59JhhxxU65raN9ZjMXfbhhA6+RDihYxeguey9ptsgSiIYRCITcIfa2kxPzUarhhxSJmqLqT+foHAf+Lhbgb8r+g7az0wHrMrvhKOMEDpjkridMV7XjCdl6wWJxZVQUBLVf9FyDNzxZHsooMx5c6hrKuiLQE0tNWfVHZFSOaQQovCkCCkdViUTRtpfsuc2Lclfvl+XSYadKoULHUtZ2MoasI7XiG/FOnJn6zYQM3VeLc9zfYKupCjwpRsNi73lQeZFRXOa1JmJndNDNKyWFwlFhnBB2R2tqps0u3RCzQFOloUbVsxBM41UNdcdTlV7qFOzpkEsn+toM9YdnlC9GRcIcyZ/xV4y3FjItCgfwOS/qdih0n671M1iuuKt874VLnjF3t3FQmV924lV7i0WNMyjkIRANIxDyDCP/xaUyECWbjvxwQw85SnG5EWmE7uo1RFfVwkhl1JFmhpSgeUKs5rPGqS12UK5jATm6tyM8d1m6E0qRlbRU8m6Q4I0lxWtm6bAK4W5WOjFlUUFySMOxTeYex/6n+OXwv7ziFak1z+74QvT9RdCDZ7wOMdOAs7FkfWs2yqJEKC3bWNAwbUhfL8al9SGTV1Dz9q577D1tV+wgCjI9kxgDd4nAd64JODi8r4JELux6OWv8VTMTKn3I/FLLD8PcLdf2bPyW61tLXkbH+f1tjWmzWYSyRviw/BlLc7Ml6lUU5SN8ghghj3QhAx/YemwuNZacNZt9VuaUaxYCicFNL+UTXbJhjhoBCETDCASCGeQyfw4sYtiXgFfKKdXSQgT2kxRmUT9LTaLUOC9x8dGXLyXIaw82SCC0npmLFVaIj5cJcbDAkeVUmdAS/LiHVrjVeg6mFKarIghXEnbDETKsTA3/x4D5K+uRpH5GazbrMCmMmbW+cfRpLS9vRk3T+WerbFsrSvchS64+GGXDGF1Hd1d9923LajjaCfQ9vMXBjWpTUfEY06Jh3VfOvXpNQkFk83CsBeeQJZGCq/tSyHtu/r5o8rkozN1y64vKnbMXnTq5xwJH4VPptCYbSiHlG9kCAZuT6J1oLljSc22EGi0L0X2FqFzYLjohq3SJOK6BWjONdul64UiksDeMvNS5qXYhEA0jEAjmH4EZlNuu2CkXh5M2poz25KbUXmY5snRwpZx0FlSUveUnquBS8kau02MvOg0YRu66ZqS3HVOdbITBg34pd1G0ikeLi9VxsZk5D5sDww8biEZKBlu9j9b8kaoYfbcoBhL7QESH2YzgGL+S68oLl+FHn/Iy5104GIieo+zGnG3Dzs+qvQ06fGTvloEUgWXVfsmJ3T0WMuJdWCeptDLrvz177CgZdclGC8Qqgy/WfFtLxdEH5FUNsZByshgLQd1ctiHjNDcV1nCEerfc3Ax4+K8jIzZXZ2TnDP0hocxS2CFOvB/5h2PyI0ecY96KQHSoP9SR0wj4aRDOlmCOkptPLYFoGIFA8AMpyMu+vtEoK8zlButgNO9dweVNHfWRimFvBuMkC8N9WWsHZScP+hlMw5F4OgZllpbdeCxyv4W6fGEBemk2Fu0uejyH8QQPBb/6r6l9bKRQ/tD9eKZehytPNp4EshDl2+oG1v23mrU6SM+feRVuC2/JZAXaRmUVXGEXk6vSgzjbAmIjS9o6LvQ6RuwdQb938VMBa3TrGVcxzYbVHuinlX/4MUjx1XzWy7jZ5gaya+Fyj2f9c7L7ZlQKyWTOYy9ifg9zrbUhX7Hq/SPJG4C1904igxdeI6i0gXKrApXagPKv/TzlWNG8u2i1CCvGY2/hU6YjXurGLas/HeHqlUNKj89gjC4rdDMNdtN3QZcoORrCEeVqDnTNvRpoV2xzYRZnZMVOIBpGICwRTC6HNEukqtM5Q2oSPIj5Kzv0+CZLDPEjV0FqHQLDPiqVn4Jtpyp0N3IMxK7O8h2VljJGCvISbWeLAO4Wk8/ka5gYic6LxkpgnhNMTu/T+P0sFDKVbgZC0tkv401OYZ5bJC1/o1NRI1ffYYT3a52U7LRRWoHh/W2CRjUNGgbmPFyxJqskPfyg1e65LvuJs2iYZycaj/a6wNDdtLpoQIrSRFUudujTMOmcphSUX/hIzjSrl29ib+AlUqloWl2XCzb410zG7uisgkRed8hXrOZzkYi4sjvOH/Osvak+I+zXnCxH1iw3PPifmkvDloEykOKLVoTz37PqOAQM3AGV8KadXVB3sDUKTrq9oMJ8ecahPFWKo2Y13U7FUF2zZP1waFjNZ0Rz5ncCZcsTQ/CU4bt671xQVgLRMAIhh6Rlwgkfc+bnGDWmayDX0XxS9u9NuGBBGw9GK3YwPpzMJ0CZzZG6W0GULLIQ88Jo9inly9W+XX3kGq2ZK0sKsytQ5DYY1aWu+YIByWFzgIeMbvr78V4hVbfMtYNfSlnoWPpIEREDGna/0ES5RO8yT0EFIJzUsSrq0iEK+fYylupmDI3Tnvqve4V2YL+2DOv3Ik07S62FrIIi2Qc343QTRWFvBVZIhUIl1ZMZ1h2SfVmEsbKs0h2hxNzIWwqNK+UUlOmV8LtApepM6+nqayjfe8opLXfrOYcXDZKcQxdu9JgsUZutyQJLxrGDr6qwB4QvCBavpe7iU7wk73Hyxt7arQQC0TDC0gMLRnlZBZeUjS6GXoEhk2/D/zKkcI2Y3ECxLL2LDypt6b4mFmcv7LRuP098Tb/i51xGn8XPp6n9mmxzmvwoqussE4/qkiTOJdZ1bX/i4D7FkDR4OPsPRv5r0E0xWYSEXD8DI3m4fuEc4zwoF+4ZiSaHsfOE3UfT1QEWNpVsoCZ7cL6rKpNmISA5jmLltQf4eSo1XXGNIPwAXJONErJNa56auRW3mk6ClQVGmMNlYdMNOA/Z/A+360xG6iFaNknNofGXXX/Tez3kyhj70qmV5ZAlqTnQajJGSvDkZbnyVjnNkSvMumLC/o98fQdUf8OQI5I9hAEFEpOjdtXuos31+xRrEIiGEVY4WAknXjFTFSMISIFvYD6GBlYgpLZMVL5da8h5lkFJzzxIDUD2X29HIzc0fcePqDHI66VPpO0cObc8r4a5m6pMILAR6sQ6PEEHUhP/cLmMp4uawP9zYRrLkWm0nqXxKM1AVxjSsOu1zghWqQtNmuYtSL8sL7BKt7SUhUPpXwIL1bSFzTyRyl3V3gBQIZnuj6o7JGIaFkN7XCOvKpk71YnaTdFxEdqyNajU8Dt+Ltom381yNyNAYqLWLl5NKASG6lmM7ZSFXlL+ZmPt19MESsGyghRpVKaahx7Gx4qxnVDq68LF8CMGQiPBAf5+qoZh+M2y0KiOPMGi1fWM6T0Wqt50Bj/2IsUaBKJhhBUR4Ey/rrDAWjcSi6QVnMomHXNhH95ZI//lq9epyj21N3kS44ysSmh1h4lSt4v9nI50ZG74hhmfL99OS2WRzbem4+y6Q411HRxpvk9p+cI1HCU6CvbM3lEgVezc7LBZDMc9CTRFIJtPMMjX4RkZKlbz7AHjP7Oq55T3ZoAxtHkkWnswZp88+mrYfcRy3B2jtekDZYjtjQsspSaBG8cOAum6q2zqExlXa+zlHL2OYq9jQlu/zlnrLsSwncm02yo14fZWnG27CvVLqvdRFMKxp0/aNyt1gDDDVgAl1vkG9qRIo0V/7btm36aEk/Ov2An6rLI/LN8RBDuwZiMk1nYLzVxOPsX1X6GuMALRMMKKi96bnTUnTbUAQiakuJy/q1e1N9oET3yg9fvEIJaNlW6ysK8d9fqM+7vk9mPYNAUabnUGG8q6xMF7vD7SMk1hKqkv434QctQQX8FERPoqZY0/ksOwwM9umRtRYeEm16WEet1+9RGlgYFmKQ6UDm5httDL7bB05CKbTxLnbl5pXPfl9FHWAHkMN7BomK8FVHwyWsF6uEpb6sgSzkHXryPsDpISmkqJSz7biwqS47lqa5FZIz+iQRpPX90hhtfqSlg9yVYeaTX/QLeCeqYZtfJgPns+ibNtmDtlr9PQtU+CQ4rZNhwR7VPDIf0DPWQzJ0uwgCIUl7nhf+Ph2DMb8PqPveSUiigN6wkEomGEpQ1Z92/UgEEQEQU6kLJYwYc+skzLDN6tHY6f5vpxjb2J1WJsMCwo8QEUoC8Ax099xHtQSgGqoVwKrlITuKYOybpfGobjo86KuI4GRv/fHBc1A/OoueClfSB7XaMRgvzL2DGWRbfjLxv0xLedK3ICKsbOW+nYHDBWQLUgV8lpmAcPZyya0zAW70aaQocWyrWECo52Hqb9gqgsjO0MpwGFWIWFvXz61nPBIR2W2FQJET3XKUp83ZZdeB/UAiVYa6raLt1QPM73KnYz8qxuIZ80ZKs7LKKyT/+YeA8FPCt3DlPx3wMym8Re6W6sr+3HWnbtWkg57gJdlwfb06STVWs7myIMAtEwwooOFh+XbpIOqtYxEE8nSIw84b8IKjEIOgdVn0rNtOtuMlUmdOo+neVf5QdsYfOY1rmIusSaz5htyDMwHnwS9EW2579JVe5umRazSX9qnQ/zxLu4yuttDuaNhm8KGqbRuS7bMKAUcDySaSY11pTlQ9x7AEZubtvQ9F2Rc3N/FbB/ctwR9Mx8pkrhMTE1l2OvJl7HBYL12kWJ7DHEyqgfhH8XRG+YQl02U2zdVL/H31s8PuwIhau4jRnqv6LL/xc+ESAWMl8uL9F7JybtGbtW1ifLt2v7T3QfEB2TiRyDv0xyNrbpakd/y61anvFkrima7R6ZL0/chocr3zboC3DyI6eWwcNInUAgGkZYcdB6lmN8TDAF6MutF0iDGFcr9VZwQbt8F8xFLKxPk3Ro9Gk/I0n0gbQD5FI2MhN4GH8FExf1X3WfZujWalXtayx0zoIwZ6lbIyKXuYjWM33eFFkEqEPDpDwj4ycRrWUM3KmbXcFrtQyKjkzBixLZrZxpcv3N6LNOvZNOYjM5JpzH17RHnzPhFtNoxQsGZdr9hDIppGmNbQTRG2Z1XuR5yiOYH4Yn8bkcvIQS40XIbRj9nioPa7fWZCm+ENhu3dwdsk/C89OrEh9bWPdrybyu0n0Bpv0/tL5NQCr2DPTqiw6xt/DdyKbEQoXbKND+0wy7LbcHJ4VShHWHBp58g06lg1HKNOuoZB7Pw8mAQCAaRlihMNOIRALCrwa6Hsbg5IeF4AN/z8lX9gK3FWJrthP6/bp/59/QSdaWsGjbCLzPzaMeEjyX/GoZS7/j4nW0xLilaalvuQKeF2IB99AjGsFDzLHlHX89kpsuNfT7/6b4ZcfP/CsBcMkZRik95NRBG1OKmGnUfIKuTIGfWy+FUti11S/lkkbk7CkIHTwbVlRgDdzjsXRiTVU5Hga5KUqcrsIUZc3nQizJS/bdi+zOK6DPFkzztHy2RRmr6USRGvqDek9dl2lZt0MD2xp52hiGGu65Eqkff81JVnu/w1t+CNnmeYYKfr5H52uwPs33/H1OPXlArUUC0TACYSlButxo+skS5vDYZkgfwbL9rrk4HBTdrWFma2vwTRUrqSwAMgKqWheAbHF2WHbziVb5tn5WTKVfE1h5aih8SEU7FsHPNPq5Dr03Yglll46mfMqRqTSS+9cHGBKsotW2J2UkfJTD8XYRZU5v+N+gstB9jVZv2+hTDg1rPtFgMIkRqxTIrVWyqRXv1z6Fc7T0Qife99OlI+dV/01es2GyHJ9QmA//zMU7QWqFt5wS5m67LhGdqH8y2Er6gGdTIrUavyvm5781PkxHa+n6SFWepu+GLNaf5QRmtWR7OHquzWlux4o79dug8ur5eKYmQ6ighhLl1ZE4Bewxm652smqO/QmBQDSMsDIAgrw10G7Iox5p6WKyOFp9Ki5TVrSaf00Ik48t+jhD6WBPyPse+5+o8v+EQoB+HsZfxY4sb7+mZMzX7Stylng7LlL/XraUQAR5m58jDj1oZj8l7bMjsuCbqsR+D2UdoCCQXsKVrjTjVyKn92poI5/twGS7KbdPxqyydDVjyfraKY6U0yLV/1fX3ww9ABez/nBjii41bPo9J5U0wmZPhI/SUD+rJ2L69d0S5m6bjsNMjpEhx+Ddoo4x26eE1xmWbQUfHcXNnMYWu+I17dkW99ehkEqHq/1R5JeaUb6yLbQcAtmk5cocmhKvwTH+WoYX6GW5OKJcmGg+KeiuZAddw5Fm1ggEAtEwwooAuYq84pnW996QjiTeiPAQ7CtbunE6Q/WFoIUZRjcr3KiLRT6z/XbVrmIh/wGzjflCLLRADIV8vokBDGg09bhBza8AFcN7fNUCTbwnHoeTtX4PRYOFgbrRvAGygWu6WsZloucaFQ9xR/cV4Xc0ydpC0+5T9hzxNkiwpNNs1k+ByVj6vqcGXCxrE/14JWEuHWN2Lrx3Tpnjmm23i9dW84fQlmWkG8QyBQ8ZfjTZfIZ2naSFlcZlW0PZrcFVOgRlftxKo8df1xIgnanHpQf2YvGosh5/BR89N5tvRueMVUPdGOa9qJDU9xcDbtx5aY6+dGxUUt8i9BW6rKwPV1gKzRRcsu6Kr+Jl6yckEIiGEVYCzDRjJ7GyEH/JgZtUskglUsjCmNFnIj8j2Z/jo89Kia5f+dQLZtFGdIVYLacaiMKzmIBHZjrNVNmJ3JvCyOFAPdom3bSiMTUGvYqdRCWqZ9lVz/UKvyAPNBwZfkdTvBvSWT6IvTWDWV9GwzSJfXIcxRWLClNDLjVvjNFJ/YyGo8zORcoGehfUyXa4sq0M1N7iXZa/1tx4F0bDRWt41TKAkRcQVCCTWmsfg8ifGRnT7zeTFFQnX6R4y4ly1vaferIdod+T9YjTVVArzq4PI4c+nPTmnRojCegnrrogjEPyjCj7b8Dj6q53jID4JNZAnpWLI8rMsz+nSuflFnOURSNaxiIQDSMQlgBafuCspRnpYuU5sN9pmYFbrg9ItXeltnIIX9xhDAiK11EUVlmzxnIdMpVUspHZWv5Uqa7Jjw8wYoAqHWuqLdrYNZEaCf7MZ6AMclWhzagXs5Zvmy7m3MYsdWAa9IC/nxcNS/X9FcVFes1pmBRNNeoFUpCpNuwUMg3NIRu2K1ZKa0rMgVTDamJ5/l3Xn8lWWDNjBsuu3kfLEKLzYiFasKd2q1IKCHbxumAFbjxXP+Jz1YJC4ons03P8I7tkNbNUCawsmBthD/wdt2JPUFgLWx7LgtKgLOsRGZPnqjOcoypLW5NjkP12a6qUJakjj6unSuMxQjwwJ7pNmZerZH2tZOM89P8N1EQajtLdFjJvy0S0EEDZa7bNoXN1h0LHGoFANIywkoJ9paTSd8WOXhauSwsQpqQ7ixq/FeFR2Ce/et80T1jbj5WzKWQbj7dmAwusWXhnNB4Wi0j/MSM5DUY/uFAHo0DhqWaLydnrtBgpu56sBDoR+7bxhUX9NHmo+bxWKkA2MrFQOKLGBi7eCNk5z/GwGJHTsC5zqUAp76HTgKc7K4T9l2maXaqQ69Mw6MbZTC1cLtmmToFrJlPiRXoQiD+h9WxClkCPhkGObhN88Zqum4w+g3e8cne3bVOjL6Hmob7vtnQm8E5GzXvuGGeD0HzjEEyfGo5QM7qxl5AMVGyfJYKPveH0SjEKraYWf0Kl0IUiHPFOcYM0zMRlb2rVXtH2JDvvqxZHFcbHwtNkiXOhdGoZ2BXgZhLB/Zql0GLFJ0OrHSUQDSMQlixjKXbUisq3W3EMnflCJgvRoigYk5C66jmorBh/XYR6B80LheZ8XHlyACx6TGSsOy70+UWXCh8dvwib5c5iiRokVW40oKlVe2gJ+i0k1ZU7IwfQSR0AZ+CS/WslJ6Jpx285RYSbnlGgLOVq+aHxISARWug/hZidYIxjnpCFyx4Zqqy0h2efvAX0504Sq2JXdWuczFQYmSaxKF+Gnt55denyrG0hnRp51jE1NqXx0lyr4ZuuF2bgfifOBh82jSeCy2bClbxddySM3fEsdPCKgMSQXbo5cMuybb2K+qRxcNZLPfyYc9YDd2o88p8SkjAL+EDr6bp2Z7PtmMVldHfkvzn6wMluYfBerzRff7lKXKhlWmnMvj+LststAjkE9P8NHRfhmfofxV8EomEEQrqFXRZysC/xioGJDyBRA90yVrQHqv+qWIx/KtoDMVrCk2+gD+ZiseIsyu5upuM8/KhP/at4F35Wq/fzQ368ISMhHdkMWRzlu1mLyx6AmV6z1u9lTW9EYjDc/xoUULxaTZKxYqRSUL5ougpTgrmFEHV6wNJqVYzwlOJ4c2Z4XAjlreNqRjd/k4STyGWRojJmrT/chIZNYXse4wbj73jGxP9nWp9sdf5GLH5tYywjzntfPWs+LcgaFRo0lEJHn0g+j7+sO5LR53y6Drq9gooKEk2ebU7SJW/owSz3S/oHwimr3vxT5Vg0sfD6TNdhw1vJhuoiEbmGxcaWGzDiJ1Nhmsqu8xnvJgZrNzIxGLADkCtq5lLXkUA0jEBYGkgOQ6VBzzVUJGAMaVIMiiARUz52j7Au8ersPxh7welRMap0mq7GNFrN5w2FHy0s9AfvqcqQz1cmG6FFXhkRPuUo76f81AWl6r6BBZaafZLD/9JuHfEFlFhQXFhrshSpVNXu5m2BYvY2Hh1ejCh6w0o3NjBcsjOUEvVpGEy/L6gtEBu/JaLzr5idC1/4gDK5j7TGoJO25Zs0fMP/qkHtweqJxwbsiNZoZLfkqwMqQrUVJkCTlq9EvBl02jQdrz4p9nnC2o3CLIUbmertOpZWMsMzr8ou3uW0n/Vcq+JyZeghyf6bs/7qoQec/jcfypzyxQUpKQ2J1PrDnc+K7wLs2Q5H/FZpwkEgEA0jEAh6DHYMK5eKVo3cK2ayWPS8Hav4QeWuZrmpRJ/QXlvN2Eeu5/f+fau8AaLGq4iMkKpZiIUjfAGbhSa+xJStltPNsgEgrrjMp1K8Dnh+DzyIqjwpdC0WhpVuYuwcIDNXIEwSkg0uaDzuIMZjQsMg+7Q9NlvOtmpzkoMwYdXkbhUtiwY7fm42nvJPoHKjRyGWU4SprdJpxZ2CW2hhMlm+sWawg45NDA93r0FRlMguqU7XlsyHM+Y5n8y7D4/XzWr2nnmj5nN46z12NfRPUZh9QJb3m2x0rP+qlgY6Z7PsMk68P5eRviQY6QaKdcnEgJx+Wor23mAsbvgxdfKNPRplW/o375ppcBLITceqF26G/y06DNdRCNV4fblKnC50uHdJm0AgGkYgEOZ+U/shSmOkggVJRpCrqkqLp6AB7hQuBrPPcNY4A8ps0lF1yXqWZncNBlqzYMKDbOp+w1C+LipBFGBW6PhkxVRdRlBps6m6Ps2LT14n6p0e1vo91HFtFGFnYN8fceF//HXPhYBh0EnHgPI9s0OADspGogMtpC5KoGE7Yl7RaGFC8jd9iQ5bmFaxGeJRV9l3ix8aBpYe6yAz8br+4xgWs0dvXkDvfqpod7ZcW03euWVSrX51L8E69i7COvNztXbrtLedMufFkIonZgYVtBl8xiaCzZkYKsF6G7t1Xy2SVAvkJVOTDhkeelDjrVWDixcLfU0Yl9ayN0hBDtkphQgGWM7bXkvMs/VHTi7LWJ0yQ9GxdAOtbwTn/OwtpHNVs7+Z+xxhffaK9l5UIhCIhhEIKym6LsNPxVSpIVvIUOiO1DPazuhyySqyPNMkddtT42+b7RlV/n20CaXQ5VYnZ5UltLoCFsLbfpyFWDJmxYMzHVMscJ0SnTytvgQDRPiV6r9T6/fgMb25z6YsHUgjgaGHPC//jFUhaqhMFQJA1XBbUUDYF86wE4PIh8u3M+p6spJTqfIdQbgCJBO65wfZw/+GCjEWrc6DtPZqP09NsPU1AG3uJrwGSrp5yN9NVeJiAWNHjNZqomp3oe5zhiEN60RbtuK1XHOGICC5ua5SBYfsN+v6rcH1wcbCk4POGRBpTJdQdv/Oi6pxEZ2s9EO25zHaoDOTZUNX780LeM5ZWqQ93iMatArVr/3pakWaS3I/9jLU4kVpWU7TFUP2+PBlF7jRUBloWSnVqAqD8szmE0Q+bW2DtkMCgWgYgbBSwao9xNEfM62akMvtmq5T/kPzdzD0ydp2n5p2LFnqDzc7C9A9L9QVep4HKY/BroMZ9b3cWdllsfJCSOsenVbyhqN4l3+q9jA/eiE9f+CbJzuv1t2EV1JBe08E7jfQ3qPRcWSlUmzqctm9gbsMw7KMgrqJD8MZNhRJriYqY03CRFCuk71h7XPDdCHR3vGrBUGzSA40HOm6Z6miyX6sT1Gnm+2StYUps3uRm+y00e/yYpRS2twNP2J2eWWRGGO5boZ1ckhQLtioR8OOM7asYNMS5+dNwWaMWMeBdK570rv/r47+0MLHDa0FlsEbUr1wJiynGYdfmMfjlZZlH1OQ6sH7RGnfcYrDjb8C5Ifdi9hbLky+FGkz+423fiCYdxUipfEhawySKsuEhrCqy2viPdCPgR9v7t8UTiYw2bA9LCUIBKJhBMLKDh6FY3fQq8abMwJmtPzskyzOYr8KCPpla+KChqUCdc3SQsR7YJ88CDb9wCf6IJ1iKgjO0Ha2c82zNrxJfWpQYlSwSqvlzAyZgSHjayv9uLsu1d2EOx155CUC8RlRN6Uw9bKcjJBprwgUg20dciKXxbW8KDGrs5P33Eb75vWh9C4ToinIaj5lwRQ6V2063Huzj+tjTdWJprtNvVQrJMfTdz2WiwtAGwzFLRjTRhn6nV1ZrtTw1Kx4ZPupPcBY8VWW5LEHJwjinfjmAasJ92e85YdC4CHb49l6FhSFapaFN5/oVYA9eA+UGcfeVjw4jAbzSlQ33Vq5KiGLpd32yd0XgdEd73mbkrhO4c8gJDVtV39aV9SRHUuWqXub5nktGTzivN4bvkEhBoFoGIFA8IxFpKKah+qaaxAvxOJrPmsUs1qTZWYHknWJbuvW0v/H6PPJTl8KvvnQnOCRX9EqiqBkHqTRNjdQWlj6Jd3SilZJTSrIYar5NGFZu8weNycVcnm781e6m2AOodD/arEHGLXjxa4qb2WrT1Q01R1iOOeTYA8dSjCdCZ4kBJ5goroplRJLN7eTI3P+iQWdbu060g3WIykhs7Xavl429Nw9j9OJjcrjRKStQsspuruW0TDUMXab3S9u6u295MHYJuYf9HTkMxXMhx/VHAqG6frKom4AhcN0hqf5RI+HW+T5C7M3QKamoOlIpwgWCgoK8bb6tsBqv0DXS0NKYroVzYL1YqHWyxMEdVb3L0srl5nY5kpJFamGYtROOedwTwrjivRzF5HHPYFoGIFAWIGY2CyQKF6zYSoYCLpq24hvZLle/PEKtDMVr2WWiJBePW5Lp4xQ9f0ZMiSGyo1Ww9FO14Exh7lHkMM7jI7pcD+IQhaQmdS00xCiXKcfelAuvqZ6bzE+BTZyhfvTAsk4SQ/GXgp/Nkqti8ZvK/ja8Gt2sTQqmDU7CguzdDrQTMbNXZhTlbubFSXK8wWJxYE5/8S1+7MmRdnFwWyqu0IML9IrKkg1nmAwnLHXkIZ5G4LLVIamWr00CkfCOWryuDgNkG7dkpaVSFXuAyOHJG271m5B9kO0DGkuo0xVYGcUY0dBhO/Yts3fFw+4e9FaYhBS3Jy4mr6Z5y+diC64/lt97mHsRSy7Ld1UoRcPvVXLcH3ErWBV8jSllZZ8VMHy21BQBwQ298CUMgicqMQ52SNTsgF8RExN7ThmGlE3iJc5mL6UCASiYQTCSgrZkKNKQWSBXJjXdHGVJXlGUnsggbAZckUdKWqD8V/g2NEYFTTaGSrPLaeabdh7vVO4MvpMlihNpg6yNo9lIjnmfPt9LOKyAJTHTK2n624iZV0G741gUSCJQg4KxTYeRq8vlDYGzY4iYztThUwPGpZu1LHKd7CSJkWJcPs+hjWl89pywIlhtewa4rwu1LsAT/ZTuTk9ZEXsDUfD3SNs5enEooJEv56UXHLEqVJjgzfyCWDbyhk+eHf2WZMYtUq2SDePfVxXwFCajJVtqesx0H2VT4mReZh4Vyz9HOgVrI89LzzKDwr2ei92jI99VJ5zyGo9b38CxnzwZrm3RTFKWbxu+spvoWhFk8+4iUv4XELIE7DrJKe13EGtICYEdYdklN2+RWEFgWgYgUDQRvMJ8FE0VTuwM5T9ilbV6hQHF85l2EVjmTgONxwpeMuzYZ74TKMjsGa6VJwcxWaJyk+ZrX1mpLCy1O2wOFIazrBPuxJSp7v2YOPTl4qX+sG6Pwk+bR6GIT7kuDzTSuxfeYJlobiF+hT+4CuN6TlsfssgYjZxxALN8U9kJ5Og6LhN9vpALMDzJKsODTNxqR5/RYjWuPclytRWydqpSb06MSgtWwPFYLpvNlzs+J9YKFnNtQ5WthRWf1p3t2iNkFYn0oRsyeu5JtBkcdR9/uR5OFFo7a1or1wgkEoktV90FTjxxlQZCmMCy51U3axlntQxQ/Le+/TtDEcBtk/TNTJ4srbDbXUdxgNg5ElHlqPtHAooCETDCASCIRipMOJFErK/CGIgVQzKYk3em27aci29WZUy7qaQuZHybY0DeqkBbbT8CYoO24uV2rWs6QVLsEMPO1LUyiK37ivwx4wPm6p0SB+kip10Q7TOS5xiOSPKobsicLKuLzMnbMXrGt81SVGUXfv6N5RFqJhEMkr1iGzYQm0PYMjrIg2bt8/em9RM2KFhJtZ24MK3StoY2t3CQcrH6yeQhx6S6w7W0ONm1xbaqAoUpt7yFVR/uPYcEFIKXt1Z8+bbweJhf9P/VLESTmviZIn7xBhHEg4eeq/6P5yULyrd1G8dgQX8DXspH1cwH/lac1ssk5UXDUeolV2lBG7T94xfNaNPO0WwAR3eNJ5/R3NYWb9AIBANIxAWB93X2PVftTt/Ob/EaKkDOsS2w+Vqpd4g+/RyjXU3AXrXALFCOCYfE/L4+293clOm66Zg/uNL4ER2WBUVpEYXhFkTHzoZAOUldRZiC4y9jGVmo/wTusHK4N0ZsnUmOcDUtJaSOLLKZbAG7w20fVsGsZ0RpPm4qgNNFyB8tzYWJRopJUKwWJj1+lux9wRXvHD+Vj3XCp/is93jb+HAxt45BlH7G5izqv+e+8n2QJ0kEmA9w3Q5RYuWJWPFhqs8dzurJAst1Pi16rzMuKxaPvWaTy7jMLyuErKvHf6nCvgcrqqmYYxw8rRSyUZuZ623qCEMrNp+7HMPw4+K530XxTpd58Xqx0omcpUlzSBltIYQEe0xHnbdl0RBwZfMVkb8ENWkVX8UNg366ysjEIiGEQhRv6mdRvPSzVJTNSvU2clUlU4FP1QorSYCRO01Tsbf+OIotPiPhTl4CD0LDTXTBBIDYHTLO0yMhAdmGkQ12qb2xAcLdtvn9MMovWVZMMevp3vzjBd4Hxpk0gb0+HAZGhAVr23NtJkxz+K1oBnPm7zJjjuVNVOyVaQih/5pSLxv9aEi6DmUUX43LTCgM4n5Jt7FxQUw4Jqrq8ZYWdevIWmwMLCTZrvdV3nOilWMXWhlqV7dIRo0bG2FWoNDrYXcCHhOGLbfsPmvTLZLxYuhB3R3KxRiU31/03tghbG1Ts7f646/j/elen+vAj9Z+uubPtkZGT92p4zUXJ23bhwqrvHd+Jjn9WnGMofSTVwFRYCCroEvPaXUpOza9bHuNl2LtdYV2xuv0fgDe1rH/uez5pNAIBpGIJiBfU58ZLTqD88wjPr2CnVB4r0g5wUx3+e0fs/VvYtWt6erDY7SealWQGD8BZ1EtTpYnzYXYe/6jShHud4wdh+DVhw3FWa5jF25m3qFVUov+gjaeKkV3Is6bea2L+ZMYtqxXbxL6IMXKmoIoT3J3Ssp8761/VSQz3vMTlnmMFmIGZaoNLpab29WAcVmMhaUbm2wji49str+z/U37CLzR7L2QJNH7GK1cKikYdpzxmo92+EDs21mF7b9PJUQjiWMjAsUzldzaJhQm/RwT56z+lCB6am2cwPNE6lN2nOtJ/nZVYhqvO7zQJnqlP4V2J9QG4VzyMY5DyYsfQ6Uq11sjnFNJlBkfd542LL9DAQSCQSiYQQCD5uSM5E0k+QYVgKCrdLNIV40glzg5L3O/hqx8hYdP8P+DR3ZA1kRpC/QZ3N5D3PxNx3I1XRTE2fbERu0qj8f5vTOnC2qQMRq+FaGDJ0pDTtQ9Pk06m5S/1XRp/dnbb7xL6eFfeJDzzj1TkxOtv5IQWNbfoLq6qZKGzLB4iP/6R35Fa9rtkATexNL1ICGaed4heiC1eC+mgO+WJtByWv9UQbjaT1D7cvs6NepbqWcn+0ib7kw6acfUnf+0uW7EoN6RdMWTd7vpO8AJusqPeiTwaum0MuxQ1K+8m3mu8kZTK238TkqXtP2LQAoL/74y4pFFq7+X/MZ13dg7414Ut6O1fNeyOzpMB28rG5gnH96iVSdsI+ID1c0AtEwAsEIiZkh20ot9bOwuq/D4I+FXEaVHiz+kB1BOsVmWY8OH7CM7xyLIdrOge4RU5WC0MGiz7IthOZbn+LHsgGdfZL1qQvbLcrWbxKoPSNL6H8HNjsZNfaI+yq0ItY0lvPy/jA7WiaqxWzOgSGa38q4Eb/uMCGy/I7uJiJYTzUer7sJJKAKdSrHrMSYVbqVCNc8yxdl6sbN1NvtEB2XZDyGl4Vzv3iRG3gTd5m8EwfQG0pfad12bJcsDyNd6GUCCVDLqAOT66OwrTrcO6bYq4ZrhzDqrplB5akSxgmr9rNSccMLK5jA6HMuT8pHGN8Xr6PrrwVvkk3Fy0pPrV46c/iQk3Vuyggm3qGv0vU6JNt/jZ+YIEWz0tvNtw7NdDWW9rH/erzSwV/7O1gu7mHbKKpAQXPFGxPvOe+K1rOMh910vGZVc75g7CWYh+zz0XZ29GoiBKJhBMJSp2GNx2fUFhqWrbMvlvw6gsq5sQcoBDGZVLbllAwtqcVG+0/ULSsSSF3WsWdbfYSJ4Tk+yW/hi/7rfyQLCrUGJlH7dYzGIIfgyQ9lE4iP9I5cGtBf5m88FoN1fRU+cH7bWEsm3pq1KnZFlUjvPqveG/ypk8WHX8cL66/kKft8FrWdRlQcqnk3xN48zd7C5Bh2FQKH/I374oAQ7GY02+ARvkDIyrtr1bBQm6vDA+fXK6QUFYAWVEgaCvpz8T2QA2l1jWKlhodmFMvYI09CVu2u+xKWkg8ehstKSKcKzwcH9R6Asbzi80Cy7LZ0M12emeVle5LWMpCUwezztI8feRxs7nTy5/VfyShhNVxeBGGP1bAuQ7NxcXExXeMst7G/EBfyCETDCIQVE7E3RZdLumTCqLuJgX0b+BJj0RohfCfqDnHch5XKclEDVjGXibVeVXAj1eoYhTCIYx4KRDsjKgSVRkBG4uDKwfb+0dFL9K4YZCGO/JB3X212GKn3oJ+eheRhIcoM6EbVll3zeZ0Cp1RyKlm2M9Ak6LOa9tpl5y/VbMRtMEP/hLK0rst8LIVkR9flgte9YPI26MAOLqBhekWJiT4sAPP20pVWBHWHGoxHdp15SA5Krzl9W2EZ0Js+IIkBTICDO4JqHaFiB8WEkRh/VbxGvqtHfYfxYkICsNr/JJHrNV4rESloB8U64XpfL46ko63ffaXPl890PXqFMZLgwYUYx+OLApW7+ColyPYRkWbTPgYv28+aT4hcIDEUsBkoX92lmy4N6kggGkYgLDLYu5Lry/lrFaj7suh2uDiEjxYP40Cl8PzFvzK8XIqxU6VyOgs6eYBlJKgd78YYtGIntaHWQvLA+GHnz8M/63gvuj/puF0Zxeg83gXHnidVBLhQU9liPmROqfUM3U2kKhrjh25WTgshO8oYl/YGr0ODlYVyz5FfL5Qqzo1qPuvLZgzcJajRX01usaA0JRvoVjOyB0dqrg497B7MT2P9mxENq/+6uutvshgnm77xMS9a86EVAWRyJ0GxprIzavkq1i+/bDkVs3NdWmsWib57MX1aujG8gvwhMSjkTwu9elClA7KpB52zhxcdyyxlfbhydca7MFLqGQ7/K4yVpziUIqMw5v7GebzJIszTVu463ww9PwG5u1XEisB30pX2S795nkA0jEDIBaQ7qlGUwxF723n5jjwRdCRyGb5828V3JJMmPzrxVvOJWJfo1vWRnekdiUzPw3UnS/A6gq1rRQWJ8fLwT7zt7AiqJVNOiOntbsS4H+eBPjrcJj5QqzLMp52dDvkffUZ3K6n9qIzYZE7Gu2JQ2iREpDs68ji0bOm1zFk9N6gtlbMu6KD6/1q6fU0Qqu4p0oC/9voZT6qAwHrWs/tPmsVZWTbxlgyVE0az3BHMyj/p01pgugarB+EsXDgJY4NK3UgXGqYr7iIF9+u+7D9QHn8dd1LzGa+0vJRw1Cntzk56v6fSllS+LYVJWtFqXq9ZcC3fDts4w9BqTw086FQLsy+sIYcTba7mmj2LgtS0k7RkvNE3vScQDSMQVkaAPNfHhU2KeT23DDSDm0uymDvdix+m3oBvsLCydCPsG1Gu7kOTSbqOv2pvg9qwzkvUFVlZiEo/BhZAff8T/onHXsd4MdwmPZlGUDoB1B7gFLcY1U1NvK+2ispCHtbUtWF1aNjJuiIHfbcIGvaS188gbyCNfSNwTa35rIEVFbsOTjWUNkD8fUNsZZnSWx1gITJfUPCmYQzV++HDtZA5sBCwdGPg0pkiKOx/S510jysvNQM1aZjwtk6rw79v+Fi9o2Da1qzFT9NDStFjKmq2g7b+KIQWXLly1/0799dUn2ihLPSp8ifLtsu38d9oJCdz0/Faa0+hvFEH73cWd+q/bvxEyxZBn0pLOYf0xYbFrGcppCIQDSMQTCPvtzAYZZTDtJM4OQoS1TpawDqQITh4TI0s7lVJdFyBK5o6JZctp+kWqjlfLyFHwYIAIzQeI2jA3yOg5ZPg4MzTdL4b67PEf2c5XYjenmaSsIFEmImDGdCwQiEEotdaA4JvOyn067KM8Hu6mVIZSnqHd+OvZcQxT4d9Ry2kMa1n6l3GD7CWTD+paGdIdOg3AoEh2PoopNHzF/fhC+8p9l8rsXAVyar7kt19xdw1IAsd4diJeEw23pJXVJBq0EtCshnCrwzQsA/MbsLEu6qHPWVX7WVGgMGTahNRYdijNRNk+tFo6Wceui5DfjXyuOuRWoRhQM1n/TSyThY7ZcyD9wX4oHxRzcbHnhcWcweFUEqXmnKMwmo+Y8zBoB3ui36qghcLjCHzRGIQKUsC0TACYWUHC9F8VCJxsGBUZ0lbBywe5akYH1a2oUevE8VIwxgtUaoOSPta/YKiqVKharCFgdWSndEM3X5euGcMLgKpGbvpSP+TwfVrXY8JQ6UMI4vt5BxoOcVk8ohaKZDE0PZ0kpVgo0/pbiJ9eJUTHnIgaWbY9yfPoPMj55RZ2Bqy1rOFYp6aJnVAqDYwpmEge7itmdK9lJeAKXGDV2DK+UnpJlZ2dwcry9oQGnAtg4Dem04wGtakN82kkgqco2HVtPQ5cJszIAi5jZH+h9UvDJRBJjGhwXvbUKDfyJcsC7c5CMv83LVhrZqDA7Vasavk+FImfI6TvWD5O4fxBDc6lBzGXLFRPtwD3Vc5s3rkv8abSwnKsq2WRleYFN40cqogEA0jEAhzA8FilHUq+5iZX5DNRcDS9SfV+0AMF5QQnu5UrxmQEyt8J7dUzPnGDN6tCl578CJU7qKbx7OSlkzFGDViSUtQFp76jlE8MPG+6GPZJzSRLhZlVu2tG2UyTlt3GChhGGVEHbG+LXQ100H+YQfjchpZTaqkYVICxFvILt6NHfkYIr8Z8g3lTkc6brN82nM+wK6M/vUHCYodhERHr/Y7ZxW1MxLQsPTMKVkLVO+0CN7LTupVmslCTmbuZG77P3SNa9FLR8tmJ6PaY47+W8XSxgUunKFMrlNYmoKZ4CNsslwCPsiBI2YwW1sHXw5uJXNjz+OdZf/V91J37njcefGy6+b3jSPNwT2Wq5LDT+NaG+j9BrastGadtxy7PqYvZ6jkFGX5A3cugbABhG2Xob+F/jIWgWgYXQICIQvazvFfFiVF6vRzQa6fokGnTsy96GX+5y8VTyWnw78mMnBpOEojOr/UsK/asmRrR81nTK72jU7IPv5q+GfNyAmvMyndVJfP6EA2SrHdhijD6FzOGZw5+gkZFjZJ7RD9TnqZFVEqBwCl2VZD/yblODibKsVr8YejUW5OxxEbaM/uGEPr9+ZBO9bO6Yu/oa4unAzooZ2s1H3PCRSd05827A2GedGdnLwoO5d5tEGqKfbeoN4nm1HSL675RONb0HONKFp+0OWhvl5OAKvp+1r7rDvEjLdz2aGiglTF3v7tDaSlhEeptmwY1qyDnX8pbnZK030vA8lmPJBzdG1WTA48gjSser8QlrTY884ruuFTeI7p+8txLNB3gVtESOtwKEe8kAIoAtEwwoqLKDIeCzG53H/3NgvceehWvFagchf8DN/kP9zRxHQtlMbJlXLXL41I9JVspBaAmmnEXpeqPecIBnhALtuDqWubMTmMTuJcSnXp6weqr3mVk/NRilv4A2+CAivtDt1nq+kE4wqi1jO0nxTRpJTJB9zAeGDzCRCKuT3v8U5jbwO8m18Wl0VvvZ+PGTxntR3JGQfmNIw9L+yp0Zr8r2KdHlhFe6a5GIlF36cWrT2DyEGhgv2yiyzVFHX6OaV4OqRobje+Bd1XK6ZZ+/kZJZoa3iHJcVSNh0UHPW06Fitzmld/pP8mKOkY5uY8ARmhPbSUadzezFLfYuAfAZYejtHy4ov3wC/Lt7MnPgzlUw0Ldrwu2rRTeqbB6TzUbzBeNA42ALYKuNixo39nbQLRMAIhr5GKgfYDYzjNJ0VOxtjns/0n/j8DUMm2uiAh8UAjma7GD1LpppGo37IPHvcwLd1Evd7vlMdoNEz3/Um4Ff1Jl2Dzz7ZZL1wK2zOwLnE2/EskM3v1XwlxhjnGx7VfjMRYpu4QXP+eeFd3k67famnKz4mYxWNStrWKcliQ58QcUYBOj8QQlGgykt96lp/8QNPxaRqzpq7iHO+XAy+1OoOFGP5M6bOC0acyVFuKPcd/nFpvY14oz99FMM3c4u9u1BRhtGTwYY15crnjL8/2b3oDW3+JWZfY29nJgPRpUDZP4lrMG+INsKdu5oS9wTBJdbrf79EkyqWwqegmCiLt7OsO8yPO0XisKOrbz39GCJzB18NFAR2RjBDlSWc7IO3vg5ZIOR9917hFC0umnUxs+bY+vbkJRMMIhCUA0OYq0DJcCuv1yleIwVXGHLLIx0dr8jxI4UF9K159SGksHbHBkf9gnwPoaCk/58OoXZZV1c37FjefbHAK0v8HArt3wr9E46+ieGbJRrrJDR3IEK10szDLHSXEer+BMICsFuu4SHcTNr0lf1CJcVuN3xGZpTb/58Uouj8Rf6RV3xCOXs16l/FnSHv0pxabJLwcS5+GTXzgCJNkJScODRPSlJMf6d2gx51FCjfWyhgm74aFo7+hWkCIQ6sq52xl29hJ48Yqq+1cMWGyZeBZ1M7N3PVT0FITiHFU3bWVX6qF5pW3DIu0v+n6Banex78YI8jzrIFLBkE6JOXaSv5TGpwfCczkLy80M59cFMglCaiKf43CNALRMMLiITmmLmwLgtkWVC3jkWuiL/IzGrjDfzXa8KPYZV7xyaCGJ/FeUXe+TMvdODVlsADJYgWZlqk7RPWBTGLHESQTNNrNZcio+X1i84ev3BvZZ7MYRYawwfvxskIKiIUoiw9abWupJez807CLRCXSb3U3kfr4jUfrJuhkGkej7CrRfC6mQbQbHbPFPUI4jlEd44V2CyPjTL0Kb7T/VLDZR3QfqYkSXK0A3q73moIqaD39d6SR2tm8nusEWzjC9Tcg3SlomNKYITEoaZLVcIyfLE3LKXzaWxPLXVYobgF1n4odoCRbZwVHan703qw3C5LOE23qPe1c2D8oUveyHKB4XePlIanFwpujfIMRbF4czsagnxVfXAw9EK2Be4gYvM/RvO2+kmJAAtEwwqKi4xcQ3DQcqdX77vOtdz/2OUBweXn0xHIcWQc4d5mXashGc02xL68vU0aRhrLKsflkKDFqOl5XqZwFIqg9vaG6YYbXdGkG97G3DN1vLSEEt74ZzZaiDpW7RlLgJ9NEjLKG1S8e73IaP6L4hMsUAcTfetek/qsYXpdubiX0hAGlEDbQsP+p1jXuxP13BMhmS08I4Irm4Oeor2EoTd70deoYUeHrJmVb6743QBtwFa1qQ85hilbXpZGSjXdf7T4Ve8QKV6G6nRWK3Db0P2+tpFWTdkQsKkyNuxNOa9bAaEHIPGonnSy75gv+W7bsdMaGpwQZH3arVuVdiP6KmaHlVXzp2IfVN+RCWPv5SyOKmK7G2VW8Tr4X+MlyBjYH/ItYEoiGEQihgMWmlbs4ksGp6agOJL9tjcfk4rykUJWPDjH2qeYsDoryA1PT2oN1g10RTOuKEExXYdGdTt3j4H2iDn4bK66h1caLM4vXsqcqtAbD9cQhwrvChJ/f65TGBZdFyR56biQMglpCIvljjpIYqGZPhTxmp2FmL10a9v/sXQd4FMfZBmwnsZ1enPxO4jSn98RpTnHyp7f/T37HBmw6AhUQIBBFIAGm995FB9MxxdiAwfQO6r333stJurb3f3MzO7OS7nZn9vZOEt7vuScPkfdud2dnZ773K+9L0SzgB2cL11copTtPR5+sCebM+KV+2jdaBaSDPsclC81xMw1KZQuFdRTgl/GTTXqaV2dCSdmirhZAnxEniStlnFPp54R3k4DA/tr59rZklnxWV+L2MqJS6ncMTgLLRZICNWxZL8iEKFk6FwScyk79lueXC22I35Lb25YK/z6rvO2vnzADnhThfXmUq4yiN1jpDMOEN/1qKA8mv7D1R4yDdut4W6lNM2GYaaZ19sbaGbW6sQtTV08ujux/8R/UI8Mi7Cs3k/IVAJmcjqnSanbI0HS8r1dSd4DsqflaDM7Fk4S3f1o/k/gpjZ4ZcDFTvooPdtRyPOXmi6TeibNeiBa5pXxJQKkJACfmpkPJnz84bX6ggMdE55w3As538UTkHap3BmLWbOxsGd5XAD9Ia6I4+6BowwwqjeMrYULu5jcJ93epVoLUVoo1c6XkL0u6IzWAz0kBaoj4lxXpVm+0Cl2eZFUsKaTknMBoMWwm1Pw8Wuds/fwyV9qNPiO+NI5E8yEqldXt6XJKkwMaNb8rT5IBiDJRGMBUEwaOuEckS4oB87w9g3D9o9HmjnbhSuzEp1x2XQEypCzcX033D4UnvkBebX7CGzpLqWIkijXoTe+zpuLRrj5hjkbiRcAM8S/foOQTyxcqm5QlwnT3FnrcK0nbZ5zLNBOGmWaasDWdZ71b+gLVvLvLcGEiAUMWx8pl4ujUgoANLoLyPSGGieY0SRdYkeGHNfpM2HW2EccRERuu5d3dOUsNs//oTvg8w8VvoWzbEGJyh4NJQqyfo2a/8dOAQqas32ofXH9UIXpjV5seSZ+WK5f+Ymw5pWRJluO13AH1xrcU5ancLBoyob9UoJVKRWmiT5PsaHuazhvTwSPCLkDOaiL5Zj5STZqX4JYDsrfmuOLfT9hHOTkhEWU8SZ44Sheqvt2vC3H/SFlybtzywHts64GcNnlERVeKGCNleb+OzLC96TaBtQDIORXtNObDfEYnyPtu2LGmiJT4tKSPHUezNhuBQ7nEoPmK+Cz9qK907Zb7clnjADUlul5lVACgdIZfzyO1paMaAX06k03nWKyKR2SP0+AZ0XoiflIl00wYZpppnYyW8KEegxt+w0WX5NKpp31lv+Basx2kkQBQjQ7fEYEid+Qs70Vf24osCSSzlPpNtQ6xjjyC/XDNDGe1W81W2Zt5TiNS2J6Jcxq8OsvUb+Pskataz6sI3Omu85mApj+SseBXYbcGBd2beQdTgwVRcmU+Lzu1jxjNcCOxQlZOHgKYn2VRrry/SkUTBTAhzRNq8pc6LVKynPNpeEOvryb3hhVPEv6u3JyG5H04lSQo6uM+na2t1JX4QXIW3lpTJyJb51GIonQUmsXJLiwk/XXtEDtdUXnk0ejribJhonkel63mBIFh/MLi3obM4R7b4jBFryBfpSsicX2K8Cfp4JGH14SKZHjLB1IeRZRwqxVeavA6j0pn9VKz0p5A2AJ6v/yxS9FlmvARfqlDT5P5svbaUn+YMOiInsgST2aOsTLN8CIkfVYuUP+igEShaSYMM+1hsOZ3XbX7jInEI5L3b7CiMn/oXKGztBJN1YDREyG5Ur6CQI9GydZ8D/LRzhCVhnvlYajGaSOfa9LIEmLqyRPEl/hVsl/y1IXSHjnEWMBRo2WrIDAy5ctihaDtWWiEKxbxVoIJGeJr+QpvmxBNSGr6teAx0CcloJYm6IpVrRP3Nbl7t6jYrmae0NkhJculy6Uzdd4UZajjZyenBu8vKdMaxeuyo2JgQf30jjySCUl/TuQrT8jlqarJk6rVJPdYs5PrVcI0MOrMilQDnSd9R5Vq4/rZG94VfgSUuAjWcN/kHx029+JgK0WpLZgMTm7qfFslKdxAWhriSoOtd0hkDUVkGr1uhVj1Swc/R/4rilmqb2jqUfUBnk7cDJ89aTCMtNajLFrvjzS4isNRPAvuXQVfwTyhIQ+hvruGk0zRDjC/gSojlGMm9Ws+iXmYZsIw0/qewRqNQ1A6Ku48g7qLhCHX2HBRd+hI3Nwnjals0TSi+vqkpjiSB0PMaR8nzpCPrdKojO2zJMiqkpOhriruauN0dyhcBAdCY9sIYXQsXHhArjZpvc11PK071cdj5ierXC6XGn5HI4hef4SlhdWbbcBdoK3e4GEba4C+9Hg2ghEZGRgg70cjOy25CkbIqc6hOm+KQtzMX4o70PfQ1M39l4DLTh8lPzEjSi71F0gXgzWeYROmLUkVhS4mnXhlHJ2fCOS7ryTlWTW8QedJwse0W+bKYuTx/6nkEG+Xpa19BjYsoacp6ZlCsKrrCQQs4uIghfW5do+e/YKm2nSojZFBXiA/o1/3ET/kGKO61dGDjQIZOQxcqfPc8Nd9KA0RWT3BCsg5c+lcUM1JIGj849qqfaaZMMy0h82oOAm4ULq5y7pYaRTDSJy9SaIGi2DWb2XndX0gBqojlzSC61Omqt7kq/dJLX+INkQBDwDcHcJ+1g/xXvAYSkM9RfYD9ZgcQAscD4bjeagO0PH9BciXaaNX5Ype9LJY4niVOhmNAUf1Xf1RBK2z/6BHjFgzyIIzJ+m/8DHzoHqzlxlxs2Z2lFaoeuM20PahX9fPA67DkEiGkOiCC6ldibK5IsoHmepTnVOU1mTyZDhpOCPnr3xA+vPajmNbIsoCwYxtvqgrnvUfsQiOP4yVJ+hIyUqsCrco2PhrQ1UJnyNFmx15Pv0CSrDv7ht+CI3OwEapxx9wMGp+XC7rbegA48kUU5zdle7wzR1U3UNjcJytEM2X0AuVP1ijLVzqIEk2gN+mmTDMtPecNRxnUVjYVHQUync3exWS39UdseY0awkqocEtW5xEcL7Cy+nET9JB0+xoQss3RjjqGR5AIOqOe3saufGUr2gUvNGoKpKQ5iOmo7FqcDpVHHfUHfFLue9oHxdsxsQbnNwMyLN3w7a07xsWHfAAOyuRzB1Me84OAbhrTDeiiQEcDazWn0exR4cqHZf3cBujZYlfwEpPhCJbjhMP0A670CJMTbzh9QWR+7ty/hmIt5525MNU4bTyeXKy5T/ca8tM5kGqM9GzCMVK7Z+lTO7qV0ILBVHmnKNIz14nwGLqzQkun99De56TNWTqEJ9seofw9cP/cub2xUInR5i7b0CcQnL1fqtcTiKGad8WSFN3wfZUvZDEOr3ceOEouYL6v7XxNv4ReB9lvXJ0hZy0NLCnUO5TjaZfCQFCWNb0oW7TTBhmWp832tphYA0Yyn5Q8vpj/rpyGhguGheIgUI8b0+7a41+qkcYjdYdea/dclhyXfGPaJNr1WyTb1w1NddyVcHWdZnvHmsIxkM9/ar1k9WbFd4wx2ZPufs50omSo0VK+LTcKnPJXw805++sdIez0J/y6QPwsJWqHVk4JqBJG88AKYdUPHLLZOlya9tRWzl+WJrCTSzuo5cFjlIXoveoxe9jSJcy/gpDSiXKnWxpL5jjipeJK9T9PJpqq1ishe0bWCtL9p/VjqR1hgDb/Oq40yY0REiT1DMgrC3HFf8Y0QDUkX+mDCUoQuSHsaIvCMq+6vp93JOmT68s8GarZNRK+oowYc2hUtcAk2AAvUWTUVOffGTDKY09yOlwOqySvdGV+gwrmOTXyMn9PxkTjvBjJYJpJgwz7WEwFOP/gwLPGLK1OBHNnWhPhajBRk5LBXQo2OgwSgVZOl0X4g1Xj8I6m67L+asvqxGcwDaT/hwhfVZvPaIeIaqP4ksroXB7f21quPYspuLacpNjmtkJxQWSBtYu6pAKQ8Vgno5pj/OTQr3aMPI0nK9e8EMVpXuw1ASwJekk/Lg/dXicrvQfyDEXLT4AywPSJ4kGcI+es1kLSels3ot+zJSyRSaR1N+m/5BvMFqZhB23EquzehfhD0z/sfpsdxaOI0eq6IAREJ5H6qg1Y/+w7LNmJz8PJr9OtJ+2uya5YBiGWgeFIFX0yh/ih4uzsTpSTl7Z7tgbJ2Hg0YsViTh7Zo2iIYCcf+ihSwEUR/TZ3G+oSoU8bFiUgwowEucDKQxhbZOcPMn2Whbahmfhv/iXaSYMM+3hMVhfsKuBdve3jflN6najDrF7/rpyVBD1OIlNBoC83tFMSBqRmnO307XeQoXgKiWFCD79WE0mFTwnWv+gTsdH0zKZv1E7TClywllCA64Abi1ALCCqZOvZsiRRUSjXL9OmQZ6OL2sBE88xak522r/LGZcMKpBL5v1iuaxTlPV7tcNarjHyKx3uhYY7m4o6c3jaxInA0QAuqKz3VWcsZzzM+G0pCBUUjNTPCdZy3Zk3FAknBGZtxPVO6Dly1GwrOVfUqeeVVrGAs9JSypNp9DS7+eEAxtPzTTU/m1Yv+xuGwSQkWiOf1VnW6LsxIbgper6Oi6thSljuGx/PsGQQjK2bNIuSc2T/QewdBlQJC1rN1oA+C5Sud1PzA5TSp+deMpnRSqmTfOa9xOQ0OZNaVUuYjqKmrDnd4um2GPcIahI2zTQThpnGt3zLTUHgHhlltBu7dJofr7xsluwCHgjEQFEC6+7Yg/ATDkDg0JtVb5GDqa94PoD2aaQ8q6a46mwlrO6wjalEPQEMZ/xclC7fmfV3LoVcCkjiP8BFiwzXiTddFPLnMECYtK7DeOzgQCwRZL98n0C1CW3eSP2GWuKi6QJx3+Pfj+qgDDTw/3DhKA/dC2Vf0Eye+BJwIcEFbnWEPmRUUyv9B3z0aE5S7AquG7/MYPEk3nay/EG8URVUlPi0IiXrfa6mfc/vlQvYaGWydmeO34yKeehINzmaET0Jgg2f0wkb1K3prOz3f0RtE1ExGFgduyGthIT12VYZuGdRtZa/Ut3L1i8XGbZcVx3Yd1hwhLNWU1nBWBPLe0k0mdnb+KVMM2GYab3d7FWkmCHx44apB7beIs0J/CLCOsxWQbyNtG8L8xDU7kGlTYA8+S/P0USCx/FPdoFJztI52mpm9hpS1Rb/Qc+deI5G1rOU+Em1fBQgOlzho45SGk4o6FL4Kk/aksmmlfRZtbodwAN0o0LKAVqUieDFwkzA987TGUJb4FAXUKvxM4fS0yGvZT/vt9rTybDHP66GkykN8YP+zpYHRl52zj9YEk8zz0wdrPJ5flw9qECZDknlXm5IM/0x7bRzlxlSEtFlhkuSQ3J6f5WKQjWiM9RI7KA/FwktvPvZf0JLnApJAFwn7SAtHu/fwQSUHpi0m5qjLNdp1+4U/i7V8oYl2h8Gw0KgwiKdGy4uFEeCZtzJRsnKCq2z/+gXeOlt4uHSD1hIdWBOytYLq7G6fCgqhJbbz9J/xNWpBWNCQ0v8ELHhFNsQM37hlz3LNBOGmfYwW3smWcS1ulcFjJKG+ZVWnqaYyueK3S9TzhXZkpvOkaYLBPyYUrCz7igpKUn8FIK1Xs+bQXYF+BGPaMRex+rxwK9VKWnDoXFUU5ekBvywVilP6w41WrKvohOtbK8CV5VnK6U/W7mKYy90oOJPIYoRIUMlMY8Jc0+jNNr/cLmtOBsMg29c/ZVkLWFz40E/qSBE4wtUkIe7HUKP1R+VByT8YVsVYVbjFC6ir/Cblc3mre/N+SdvUSKXOVnQBxFd3vbvYNLtQF9BoCEG/jEfSUO3AFkDaUpE6ZHtxl9Y8xUy02DFUKf/8WaZvxZGcY5mpqOF8rfpgXsQNHGnYzI46lG1CE/oB55a6jfpgunMfYXr92lcCQCqYotXXShyWTgj8RMBYm82zYRhpj1sVrcP1b63JRr2g4ABcKMR/K+BqvPdnQlMzQzrIH+TD+o+kvFJ5vNiJywYKuO3XQof2ca4ClTbmRxFUwhg81aDBLCQQsQOryVtzqotXKWktLoPcCOnJgEisHqKlKm0Z3k9jF5n+ve5oozgXuAIKGeJV2GQTw3rGpimg+3Qad8X6NenQX3U0uA95GmJR7x2Fcsl47q0nZZ0JgcX18+eqQWuUGvT+3yld9d8UvWHmLDpQ2aoDu2zPmlvwDSrWu/K/Yurain4x1L5Iqn7TKNKVoDHNJzX38tMgylGzKd2Bi3Uex0NMUoXLqYnbqjRbiLR5i4YcFox6A/CG8qtp4+qviOPvOmAprhRnLNkBttoOEUdDTGqu5D6LT0kFrTzFkU8a9SOVJI2I2KYOI6g0mH5QX9IwBeinGRIc+Jtl2mmmTDMtN5iFfMF2vd1G6Ws4Jf3cSlUleMeEyumhy2ckNf/rJMj3vim3OPxkioCLHKlfZe4U96SSFimGf2O96YOe62rcAQp81PpbkKauQOEOR5pjlE9xdF03ln6mtPCjX4psuLhK6c7Iow2j060qIdM+RjhcfBzhaFCtffLabqAN2HXH2XkIlm/04qDVJNYgxDOZHcah7IlAFbVpU4pDIMjuRqovGGeJlRHV7UG3WMvMZR+/JD+dqaabWjkqSOY97Jn+ES5CjXDDYROYwC/gyhJTvh4+49u5dwBKIDlRxIXuqYN8kE32SCDN6JiEYo2ijZB0ZCT+tquzxAP/gCfeopoV1L+q9zLn0NK+6Ewh5MBoY1GsnsCbuSpre0KOLOZJJdmMyosR/mDybl4CPFpuQG4BA3HeS+p9Q7jDS4cbTp9ppkwzLReZPb6C3Lm5yX/suJSkqim8/z7EGF+xzFIzvIDAi8Xe6ipcDQzPu7K5ar77jqGG72V4wOo00wh0t6VwjFcDhB/jRyqZvwo6TRQYc8XtaZ3BPgAbGWsBs+TAJ13/5LTtdojP/3viU3O9B/pqYM1ymhVZPIXNToQJCsp7ATHRUfXB2VeVvebKQxTTw9qmbNoKivrFXoZ/WeAmnD6ERx3sZtpY+84Gv8vuQrHeg2p0FC6ZjaMZrbVpQW7YC11yQdwhQMQTXBaWPJZXaI6AMZZFMAuvlW++P7+KJCWqMxg0mf09FEjit2PCAeGShWpsKLgwHWF0a0z4+d6vk6bY3k4ijDqq1iKWgm4vAiZqwzpYXAbpWH0Lgrq27vTjqLYLTdd1mLTpTRhmGmmCW4wzg5X3r9kPjp/6sRLNiIjlvlLgZA8JadC3QInxbwK7IuDl6b0KijpGco/WNV8I+pMqwM2TYP7xcOrUpBJ6S7AjeAPe+e/wkWZKGQ0FJrwMbUOOmpFIUz6rFs+x9Ze53R06L+Y9iyS60BN7SJFs5SfM+WrHFuj0bpnTedZaaJmODnrBVkXWJwgnjLRF45SHUaZ1R1eQB/Y+R25wxhhtzG9Tz6bJYG03aOCXj4ZVhgBWFUA2Cd+EoU/CoajrLs6qqRRkiKtZj+s8YieZkkf2wlQSOVDMrlrbh+7eCqTmPBhrlVLeA3/jU9q7zQEiUTG+VYbJEzyRQV4CBQGs9exXBY/MVL3jQzxfhkNeCwPiP4Nz7raaZOiFDuD/eHZsPUBCZFVuUwzYZhppom7MjgI+pZ/T0Q1c9VTQ13h0L/lzMZ8sdN15JPOiuTPsxQTQIWyaOySSuquUlsiWfQBkPgiZmq5T/wbcNG84U/YZXP/xSrZOGFqWyrZquE6DRR/o1kCnvIb2Bq9iyw7bK12q289hySv1V/M72dCtP3QHqk+wySHr1k7D+74Hznruxw5gwi20QFscCeSZvMkYvd5H3FSfelZot7Mg36I8KY3GFXfglcMgAQP3Gi44Sqf46rbK9BBRHtNs36rEfzJ+jMuSpRabvWxXcBaTIpp4X8DyYpuiFGqffQiGB1VAZROyFefRHqAwut/HNlH4Ec4cz4uBXE/wEtPhQZ+McmKskx6EYuj+a4r5Yv6iS41kHYmY5yCHUrwyhDLZd7LfmmAh12Ytbf1mviUaSYMM60vGSCTjJ+gaj0V3mRjTmSXU0OPCuR8rEVoS8j4uR5BWNp0Xn9IsSjXo7oLHlcD7e79DWBALpujzUDYdF6xlHMPDq0h1GxDEtjwskgjBJIz0jIAkFTXqDDI+DlDO+PhkfEbPFwMfUlBY8CNdhNphs/LXxMu1u1+lrTvqrWWUTpBnrI6FaO8juiFOiL68vsHPBQSWlTU368h4Wq3NqPcrI4ePNy+gjXTVQuAHWnPE1Dt75CW4YZ0zP5LprjoaxF9us5Xbzb+xylfvD7dttLpjIGJ05wtqH7Y8IVd0ypXsnluLRD9tjPrr4x33vAGBxgHw0s/DDAnI0clZZxOl2kmDDPNNGGzVXDGkn21lhskMJ/6LQOJwr2a5T6ixyCtZboiYRhjxD/OJaXl1QGsIy5O/BNIPcYbRqWQI/8V3l8GnxIzC8OoGkVnjLpEvibHxTm6zpDA1wC55MZoo5xdQp33Ugdyeqi8m4G9c5xGdZmTPqvBFUZpTgCrC7tNq9hZVEqAOnLQxPNF8ghb623Gxd9L2MYQtychROFIQEk60SDtLUGl0SdUncUXuA7rhYaEsPvLJXAtfeziaXWuDkoJjQ3rKkmFxT0mkMtSGm1vruZWhaE9sfBE+F80eGq+9CZJNhIkRYz/2zxtpnEoMegtnV5/UB6oR/TkDDX28ThCN5/6DeG+Qf8ZjBjtVcMPi19I2jQThplmWo8ZVacJDC1y5TKfGLTq9rEuI1965wrHyo3LI9TcZaqUxV/bAPu04YXvVAiVx2t3tiPXjRSGGc2XCAg28ROydyjQHO/MeZHkJeL6Oat3SzpyIL5s0M3XiFOLWi7VNGqklhv65ycKajxCqPlqd6vBD5qx9IX2GlF6EuZSyZLaKxYTR7OECevRK3PNX2ehvR9arZvOtJ+TWWeguqOPIRXeeMdWRfZYcvUds9e/64ofIBNUphj6GltRfkMHJwS1prMkEQ2zFH7NyQEhHA1Mm5ifYQhd6k9RSALgtM6NUk6FJX/eA5FPnYyyEj7oaE3zgACphqRo74D2HG5VkMec7kUzD9ZSisFSv4YqU3zhoTXNhGGmmRaobbOW0OulPBsIJXsACXiHQPmiNF1O2KsGcN3SJpakT3vlAwCYkfIVmaGRV0hKas+W6R8H6IzXdre2ZLnK4hc8l8AE2YynMpeIJxT/uBCJhUQLQeP62fLGBNizlOyNUuLTxHFXpZaxtWS44h8jG7kOEjbqsanjK8oxoI/Ynczh6wRbcjdi+X+grShATjjoLvrrLDQbpi7F7nJK6T/sRS0izVdc6d+RODuLaNpZHz9eD1rFcsXSWm/kL7feUYhN6aoypfPBHX1zWjK0FyJKeoQeBPeqVbvXp6pj1AgtJ8zrD3vYmFK/xTLh7d1EXGhaHu6XkymH3wpGKKRZek2AoD1dLod214TrU/Q2zYRhppnWM9bwBvHn8l4ORC1187skJJn9Jz1fdzQihm5U3vYJH4RBJQbnVGKcAGMw5VfcI8jr5d2Dd8m74I+NKSgCb4bW+fDIs9D+h4yfGf/4SqbKVT1bBL7VdEFBR/4Vl3EazZyPW8p7hQsgUZVqFCbwrsRtuY/emu44DVBB4ie1ibBpd1Pad1y6E4OWOAUM6zVuB52o3d1Hwxx9mcK7eLxqxKdNwTLnSYaxao3x1AVeIcRt5CbG9ZOy+QI6VIewYFjv2zAkhBIBDOe96FZW7LxllETI/By/Mfi0cu+lhBJE4rx/8M7ifQc2Ds5WK9hfqGA3j5YxWevOEhAV/zgqqRA1OGnKs/JiFeHhAGXpXemMruMPEA6T66IG1wsGPwJWzPJiL2q7ar7IOgbh3v1KMW2aCcNMM80vRokBhXSZdRtp8XpSp/tI23B9Ia9HvOHvI91KKgTZVC4m4+e8HjP4f+pl/ToMACEhmfgLl4NOOygM5GzERqsuhTI59irSFojQ6XM90O5SuUIuFh2icSQlPPSWz+mQE55FoR7iweD6FIVoBKGpqwpOnm7JL4B8uGgWfkS94S2gMOznelC6qJW/hiCWeusReOoys4JU3o1Rhlaf1u4N6PTj7NikMAzutLeZvY5lHuApKKe6kmBdXcgeRZeapKoNdivf/GcVv6rUSmp7nNzrWzKZ9ytU5TnrBd7Mj6ORtPLqfgVgC1bRFYTfBxRK9d+7XxXdeuB+jbW6fSQoiUoheo0kV+ObLHMY/wHDKlBMM2GYaab5ag2nEEVS9h8QiTnPJod31qRPB4JBgba1aPrEHs1aSLZkuFpfFIEo5Xf1Ju/OXDtTjOGhjMdGxQCQjq0RlRvoMj5PCrF4ijmpzBoAbGOtLZUwPqd9R+yLVesQ4oXB7JFoZfNl3sJOWvPmhSzRVjSbCXbp006A+UbK6h7R3z3CihI/1ot0sWB4SQBiu39P5GjWUF2z15LMJMCw0phuSDjSJ8I9UaMk+zl/5Tq+NEpec1b2up0FpZUeU3QuKUS0YO+g0RZ1HnN4cVK+4sr4geTkkM5rS0JLPTnj0/Z68SRPyzW5ruFR3rwW2mUeJTWQ/C8pzRfB3emgr+jIdSV/zisbreRw5f6vHMH5lIfKiOrN8tmfNbgi1F5FuoLRhS3pLVMRJZkfZ1PRD1rhppkwzLQ+brAQN54WqGcwzJwIAFC5SR7KY4BtgeTqyPuPHhloahULyR4J25Lu6gjJRoh31Vmhm84RtwOQKqfbLdlR2or0qScKX1V7hqdbXiKDxg0CQBfcFx2FMeqW9VtSttd6R3gv76m8DXhymEASQIt6oIFWq3p5EaSy+Qqm+EO6ENRVkolFZ9Gb7ujIJ3xlvUrhN0smJyxf0MNXYqtA7zUReg7zGqcon+f3K4GnQ68k/Qdcmt1UMIrnZQ+w0cYnhLXWd1p+lTBMpX0LvGd4DTmFBGp3sspA9JUz4pthK+vyzfo97wJOK0RQ4SV3ZIQqc+irgygcI6/bn/GwVFYsUhuH1ntkn0r4MIqrGonB6liNAEr1O3rFPGy9xZBhypeM3+ZMM2GYaQ+D4bB33CMBZ0x2suog5OrN4vpSzj/IIu6LPjKnwSnwnoHaiHUt6yVT5I1hnP7LAGeRB3xSr6hgKPflTeXqYOli7WkoywR4r3utlLWA+O65/+Z4/m2Mz6p2l8HPDitu88+r3mBKtS51IThaMeiNLLH+MKuP6u7fcxoRwhYpkeru85Fs2Ed6QANA860MTDRHPZxBne+yztkwWHAolVzxJL8zDbSlsPSRuqYcNZqor1rd614l+oIgEa3OITBae/lggJpwHK4qRz2NHHKRjBgTXrdgXXuN3EWJon5vcH3FWsgqqHnnm5XJcuT8Tc91Nr5JuU89MOM7monOCqoieVXt1TM8W1UuB55Sv9Ep+dmDZq9mxZkpXxaOdZpmwjDT3itGu5hg9w3w+gW+Jt294p/gqmQDVxWn+FO/pr9lhd/AAfKFxwzFmD9ENi3d2YCOPHLLgHBUlvLmS8SRSvo0r241+F64vATRe/DeoFT2GqPc7cK3C94bFr1JeEJqjdf+rYKRvE0ayiFtOKntGzVfITgEPKS+Yig38lE5w7BR7UjUhud22sBT90iWiEhiPilDtZd1hklyBzGPSl8YAmA5FulK/3EvapeXnWZn5breARgGoBHu2mMjubL/zOLo/ia2Bp8eswohWLWGB0BKaTLlZu0eP15Y6x1U+SZUrCF1sABf09mu/5VSgKK+Xy9qGUjYsL+AHiNdx1AeW5cCB0URSL2d702hZcP8+oG0HDHpM6hyW9QAZYGToEIjRMuq0W6e5QFq4vUt+QuoHNfISXKXigEioNgrMFgdq6ZJfkbPaJtmwjDT3itGA4eI/WJfoM+Oytj6i1VW1GwlHjYsc/6uPUAJivfJVAe6jHZd+1JZxNklQr2BzOd5f7nxLWG++3yZDhhVmnVTuMLiwnH9bDkcSTlwklTqWzyc20p4kDUlsygOSf9hnxE1kpzOtOdITxfyxrxfdnsaKV8EFO1RwsFpoTwEzux/6ryemp2yU/VBnZ1dks2Z9Ay6I8OlgXwKPP2dtGOVRPX8xcCT8tYZ25FDoFH6c36HYcpdgKfQwNmiILs7zzkbhK+KIo34Dwjk3FDZ4ZMkutRVod7JOgMzf+V1VHP+Sr5uiec6Y8tNhE9g1dXHvoDe1q8QsQrOM7ZnkKAbLMKcqipws7SFWF9XJOWCSv2m57JVKtfePSXoaFIIRbxr5NS13GcVodWbe8UKo1Ryi3sUDYtpJgwzzTSv1pbMmKMQgW/AfVbaAoHo6fhIftO+Y4w4I+wN4JMVDFejQ8z8NfFEO7L1nIJqTyd+XD9nNyzrxOkZoNZ40HqHBQVRnSHPo3SS8U/4mLpqsMI7zGf17t37jiQ7cSkQ84emM5HJQHjFQo5T5xFUDGBMPWYMXgJW4EG74L0+8zJmvcCIMVTEhTtySc9Vwkc9JwYR9wN5RlJhkM6LAeiFu2h0C+hJTql0NmrVEBFw8zfYZURtHrm2e5U1nHJVLA0EuwldpuKf4KKYQ0ynjwnIr8GqlfZ9dC9CVjiKgUMeGQxybQ8UTW6dgRaqkh2gXpLqbL7tSvyIsB4anEg3VC4KkxftibxfyR8iXDCMIm79iTC0jhkldaBgmXoZKqy68Jhg6+lOcUQfJfzD2CgGvarCsb1mGf8dW8b9SsdqmgnDTHtYkFgqWpp7qtDfkkCaofkTYq23ZEHnr3KxLHp1PuRSEMBa3tx61GbzGOmc5ulc72K2chJbxchEtz9auZwJbalcBvjulIy46R2uX64/ItzVQPUxPTYAkKBpf+3kqr2KNcCk/0h7eJGD5fai0r6tXbpTs01P51tPAgS5pNOdTnRWeW+gt9exHgxv/DG02Fg3DIPHQbVuejHJsiQJRY4kphumT7j2oTQASIyCnCMWhurJZUU4nvBNzp+IV2otFMEnIQqmGW6d99a76O2AT/dCa6oaHP8BFH/0OD/KF/urYdXrNvQayucgCUe+pgDaoJX4Ka7WNRxSgY3DlwWBtnXBQi0q5gG7El23DSxHhIlKC0Dg7npDSxi8C2WzWIVq7W6xr7enSXkvSgEo8zHNhGGm9TqjEu9Ix+luoM/edF5mVHuUl9qu8bS82H1Ev0IILXpBm653mVRKkqGvQwws99+8XB0NJ5x5ryJ82H0hpkxQ6jlACqsQsOFYzZGG2PNEM5RzfwWsziouus0WyUbAFSqm0roACi9JdkLicv6Q6qhWchIcFIzVYTv0QxthR0d7Zkaqw+EAGOB0OgTBgLen/wajcVNPQGX/SSOCXrPdp158bLSRRhNRw+MQJaU0yhlzCnWdSaxeSCXkBIOf83f02vaSdn9/Q9ncl8XkK+peZ/rvmm9KY5wr8X3yWi0Cw/IGsiproRCYvQYRJHSfKtkvkqJflYbJ9kyUuMv934C6wrBYORp4D856QTgVVrVeoT9xRfjy6Cqd/HleXelO0ZxnZeibYtyUtTGiyMxfcjEt+9sscUStEReLinKeORpZcM3UdzZhmGnvRaNlA7CoBd7ojsuv00UjuPmD9XpwrQSBEFp5L2YtIigRLlKftaUSro7kL6rt7uA6UNpocHS6gtULcmn+t9CS7XU1r2d1GpyU3M2XBLrzXYoeAPSVtR4OwKKiAOw1d8fKleynkj6t4YsgJsb3C6h4U/bIuoOGT9iS4gL4uNzZGMNgGMYzuf+WCsZoiOoUT9AgS6R9d0g8TSc9hkSDzRWLNK4ZF832ifxS5q9I11yhdw5JSiKqg3y8Ty7+Y8RSx4VjtZdNOosazjB9eSHATPOWultzlT/XUeRKeYaLZL8X5yKcthpX4ocIXTA/+2jhaH7Y7MFoUxk/HQg1GsQULUnV+FlFGW1bUs8/GNi7adAq4WPC6waqmvkbS9X2BlRpmgnDTAu02UpJPRt4ujw+rrHWkUMKI1EXFjepIG7cinsUlQ7qM4RA5ASL11idRAoLYX3U3d9VMEwbGiE5zvcr6iS7e4eRXNWbtbtlWc9+KJCp7aHYSMMMACEuqgyFZE32Hz0Eqqs3krpEzYggpvQggOHb6JfVr5OV1ozRONiliNkDcDWa5KCxsSEp4X5zc1PPvK301rJe8IL8kwiTTeJTOguBJLuU+d9y9uBFtSMBfREH4nGdTHGBNJix7rJPR94Ir9M76wVyTMUa10NvUgerPuVc+dN/IK9mHOQrlCPeYw2z11jPPXkR628IqYOjaIasad4fNYn10WdVvlTBqchtNAOsA4Y1nCRRyOTPCa8kzVcQTEII/CdGjkLLNdIFDUucj/3hhhh4ESlfYryIovnG1lskYYghHCIakVymmTDMtPeiNb0jrwUfFYi0WUtQtJK/psKblc5US7B4Xo6vE86GpP/SSaEBRmOuKr4mLfjWzflGi8QINJI8472icax6pHvbT0ceIU8D31olIaZEfQ8GcDX21B0Uc5UA6ZGp8qSjvVvDt72KbJOIU0sVLFnus2FBDDFaRtvZ4aNZ4ARzGGcXYTANLZkrLipYuXTuuLGvzJo+fu2qha2tLYF+VZGo3SNk13c0e5hJbVnEAYKPTpISSSqdxuW9NZ5RaDHF9fZVjnYceYx04NdQ1kyTqrS41+qPuCvcnK6+a4iA9GsCMEyysSbG7kl7D29LuB6htuot8kr4Pv1l50qjGSHd1RM9boij6FE99FTF4xVcuyIuvr2aFdrpYJugY25gcykSrXnSs9pejxj4PzQqAU9H9E5hhKlCIww1p2qcaSYMM814c7ZK7Tk9v51T3VtYQHkBRqxXIREha0skjTFp3/ashuTRKhay4itOosWuiGKPzNc3wKuzjhTAPiqzGOsiBQGYisnWSeePF2kvWxnTfMz5h6cNVZYyq9mq6qbHsQ076TMcSmJOkmhCNPQchen2OlKp4g2xZ/2Wq7ceXEC4o4KRqE+Ppymx5Sp5WKh5g6NdhPqL6kpcIlZVWR4VGQYYjH7y83ICvly0EYJTmJAdOZ6xhEwoKjVd1umfV65RVDaqEOhnkWgITDkD2z/8ZHCFWP4IySJ7mZNUH0kdZsBSgMMNnHQ4vdPgXaYqc/XHtI9HAndy7XTlKu3jAfYQ0k4RaWNaJJ/9J+28N49VLkNZHdhcev8U9WY0Tpr9Z0GoUIhyWRk/FU5n1e6Vxeue1Qj8dTfa6Zr1gmEj4GhCK79o/4JKQAH8lubLPv0I7dmGl6K7pLXGq1dFa6SR+6HCjmuaCcNM83NApQDFI2GTKBjew1cCyxzRMBmAlmAuILSI5KOkDl/Pjugl3E52yRTuldTKarKLwnSel8ZrVdgCURmYm+4p9RsafTtet8KZclWMd4I7MFjK8SDEPYZIKbruxOfknfgPGnHNsjkM9aV8VXsTrVonpr6CaXnB8/YYq0YSw/14iTrEJskhN3dCq9iTNYgoubyspAsGg8/WTatwb5hhHWIc8x45VcQVXuH5EPpe8IgBeHb7zrKMhEodF8o6flSGYX4XKi0rK9m6ceXmDcvramv0Dl4HujVvORZnCyIM9CbJ0Gl2je9jbJyecWkyIYOFlZ+HzAZWPyKH0F+76xI8XUA+qGB1gK2B38t0okQ6iTfFGhW6QNE036s2etCoIrAKoZTXp9YgzFQELwgV9ebB511eIhwnSnyqm3qbD0bfOMBgvvsbuMgFJjN/7U+ne2x1Zf9FoalwStjrS36GiyTMNBOGmeZ3g+2BMfMe6+GLaX5XTpE/xbVwt6W4O5r6C9Ozet5pXiYgkF+qCJXqfdInBWq033xcW92L1n9XLNHj+7XcYzCs5YbaobROMufvHnZTUkE0AFX0qQNU2vXLVcJXRpzp1G955BnrNk8uSWnf95plclpIugzmhhA9mrHWcFwGol/yfdsGlLV961qKvsLGDA6T/33/3s1A3xolS0z6jGeMTQs4OXUguhtKc8nyUPVH1I4kYlz9fRKQ4FkecrMiwkfiMV84dxpAMuPPgeR0v8RVTJX/ChflQy83VA4gt1DyWOsdEpCKf1K7FRBJdcnBfn6IDos/7pIFYN9n+7gMtta7JDyH0HJzIM5IS2O8lu9yfFeTHJjfGt8kJR6oh9lnEouqtUy1Up+QTLG8wMIiyVOd28VwGZEo6aVpJgwzzS8GGz+tWAMXXIc+lbFGY05V67mOx/HOzOdRfYuviDSfhIhEalGkqg1kSU39us5roK0IKp0DNduIV5r8OV7Blk5X2YFay+C+CkZqMEbUHZDLfgagLqCu+8caheC1aldSezras+M/6CoYyjWY1LMvncZ3R3YuMFmzvecCHNmINwIjBJ+VGKwdHbOmjZMx2CsTQl4dH/wq/r9JiQ8Ce2MSa0jwJqFbLb8UKvTcmuEJzPCpGeCgNajNl/x3z/V1tYvnRynzkDtj1xt/GrhrufnE2ayKAahOK+f70juteKLsMYfwuY9b5dDGV7SrxymrEHz4+wapEw+nMJpcp68aZTrRndwW80laWcegaMOS5QHBS7CVdxhUsG2JJ82uCPPsM2wwMVAUrHqVrKVStizMCCukvtB55TKmx+BsMSe4CcNM62lrPE167ntEQ7nbMkP4xJBfxcFSVbmKLLuc9WwaiGizGG2Xe9NANCEER72i87w8rmTa98kxBSP8+whKoxRqWt3AT87feTVGAeGj4iu+YjlHPSlJTfueAZWEdJsB2NmDRmv3vUls8U8yp5PCgJCgwXcPzls6K3jsqEEAxuLu3w70fVGyyrj3eez9k9rSSctW2nf1rgM2pq+d8zc1Z0WekFKDH4nLVi17rUs56Fun/VA70J5Bxg3R21zoNAHsbZLyvaByF/CPvms4Z460+PjUiihXOMxATVOyePOHrjAFrkFU9Q+DwazDsU79jDuCVj5XUdDeJHapdGkS7ZXyuvK20U5XAQYvr6GBCIbBdFDwo7X3/xTFhHqLgAC84ZCutcSc4CYMM613GLjUJCH2i55nLFUKdPJomDaeQREmQ4I6iD/9eZn/MJ/3W6ha75ukCk6bkcKTIfqHARruBcowyEyGfu3Lt8TJNMHPeBh/yk2X+Stj+tepEQoQT1k4YXc2k8Qv039g8EUKWfYfGJ26zz3QSxbMBO8/NGjwlHFDbXc3H1s/bfTIgfCXg/t3BPq+6FSE+/JU9ulsvk2yYcmf152mZsK+6CzeCeswJVpcP6cXhndJ8pV86Orld7pgMPjcuPau8QOLqqwfkZFJfue7cHRamSkPuG+aSA67vbGxIT8vuwfeDrgjXIiR+CleDgbKElQ6U2RT+ynn7iDZ61lRqHop7HvHUJem+11O/CRXxbivu088YyMUfQT0iaPuZYMox6g3glwjg5ZNVCbwH10objKLf9X4tuw3vqXRWWCaCcNMC6w5CW0OUmbsafkdp4WRnvvOgihqrbdJLZlQPVXjabYBOHQJOuHMScJH1OSzsv7ba+OWsUbze92dPEA1NMzM30THNfJ3Cflb3kADEmKUirf53Z6ZxsjLlHv94/rZs//j4+8dPbwXwbAxgyeEvFp7eW3mqSVjRiEYFhUZZgkwbT3uvotzRys8OrgdOUyGDtw4fQghfyxpaARErQLDaPI2+0+iISQeXhNrR0d3ZhT4vHPOD8k31IQzQA7oZKmO/zO+NIu63Kybbxzdv2jejKkRY+F2Thw/AHcqMAWcPru5sE7C/MEpU873PfffAgT0tlJX6XSpZCp/+42jJUGeck8KhOEebpPZJl3lr/l/YWknOpmkLEKkRaL5CuuyNoqZoyOP5OSTPoPwoS8G7ynFYFm/0yOoSJOEgIrNGIFpJgx7CK0thUShjPKtyxegUnJ9oA58O1j4SEjscKCHomIxScs0viUCXWTdrbyX9FAyNJwkQccq77KtlvtykfojaoSHvhtmGcay0d2ZD2p3ivXv8Rt1s3zXgKKblm6WCN9DG/mD2Nab+38+/lxmegoGAKNHDty7YnLz9fURYUNCgwbDX3JzPORgHQ5HRnpyZkZqUuKDU28cArfbyJuzxDtr9kpN573iBNrdAW+TPqOKeQhLe5clrZBVZdO+LxoC54Fhb795nLTkubvyKAzbsmGF8VOGUoyqizfQqL8PMGzNivldgOXKpXMvv3tOE4y1WSx7d21e+Nr0rZtW3bpxWf/NwjPFK17Gz/nwMxNCQCITfjB73TkCw4QUigMR0+lAO6mxvK9cOCSX7DgoFVbj99NRji5wRVquCg2Q3A88QKShQHPN/aVc/rfXp99BKb7HGamGDoSvJNXwJndhmgnDTHsYkFjTBWPW+pYbsrL753SKpbTeQzX9mJtIVDaELc5WxHHUliToP1tcSU8TCWB+x87RwApadDSqwaXikyY/o1bHlfs/AorDvhiChV6EYtrT5TKVp/Q/Go8G3pVRPV2UXj/z+R5rtXe2uNJ/SGpI9CaFmO9dUz1p3HBwl8eMGrQ8OsQRt231nDCcEDty0EOTQJdSugVzp5UUFwTOb8yUM7fFE3QOXtUWBYmr91hMzTYF257BBdWNjQ14zDEv5eRxQykSO3n8oPGjVrmKkfupiOQWjmEuXXuGjvNkZaaFhwztnuKDz67YDeqZrk3rlimPP3PqqL57dVSsJZincCzXF5A4wccM7vzpMuXqT/dYFYbavpyMdiJYbDsCLhJYuTKgTDCUsoVzSrAhSiRbEkqJaz7mVpe1TPsw2sdVMMKnyvaOPDZv4SJVwqzerP6QoqMs2mWaaSYMe0+bvd5RewT1Jqlv/w2nGCUreKL61I1pJTSiLtRF+W15QFI6RWFcbWbU6g6Q6qDK5QLfar5I+EJSvqrnlhveIF9XASEUHfk7KgYbD+4/if+gh0ZemmsqmWrkSRGU/QrBLZxd+yqwFstbZf6qJ8mgWu+hlgAjuNRbWpqnThoDju+oES8fXTfVlbn78NpI+Df8JXJSUGXnZNc7505PCO3qZ+/dtTlg9+1svulK+byb9Vsvnz44nTgSr04dBG8Nj8KYLtu9YxMeOoC7O5dO2rIwHDAw/suenZuM12qjFBTqjm/tbrlf5ef6zpOUcJ+JH4x9JXj04NAxg+lfVi17rTv9Zlub5cG9W4DBKEUn/WzbvKqivFT4IkjRAXe+2pLA9hSxVAm/v3tQFsRb7uo9hqUvvKql++8dbiO8UHBq3/t1edAmLkoXFiV3EqLdxI9rX2fjm2hfS/yERuGirZQwtQL69ZGEuekCe6l1hBdhKLAUNaoLXWB6oKaZMOw9b4g5HRdZ/Y+6F4wcdAYYwvWcy1Yui3X2Q2E5HUbZh3X0slPtI6ECOUokyEnB7PGksBt5qx23FhOohvlq/dozTWuf8l/18F+z/yxHps8YetK9hm05gIoD09IQEGtpboqcGISLEgESuNJ3XdoVM3zYS9gVXrd6ERvCmuqJYcO7JzpmR01sbm4K3BW3pfhErQaYHFfGohjwbK8uWMs99o7XGMlWAlAWj1vw6EEzJo5IfWPRpNAhoX7NhpXFMBoedZEDWF5gbdRbsB13/zadFeHBr86NHDl1/NDgIIbEYKbV17El6Mql81QvgaomACKl4G1ezBSr1VpYkJuZwa3QBbcgVDvQlkpCY/7jisB9UDD4PdVQ6nlWzCaEVQFmG0IqbTiQ+iPDGC9ULOsFnViFFslXrtI4su4g6VmFKaTSLiHZmDiyDlWurgv3DcK7k/oN4ZYw2h2AG8IDX5VqmgnDTOtdJtmkTJmDGxZNrYNdOf9krQ6tt3TtBLdJhAyVdV0Q/jqsYmnfldlvv4yqEfitPYsE4+Eu+DNp2PXXLWQE96tOBSZZGaUyauRL9+Pj7shjOsjd/enqjYzQ0kCDocYJMaTb45vqMSKKeFQnL3DvMwBXUyaMApc3aOTALQvDXcnbm29sWDB9zFg5P3P1Mgkh37h2ibrLACGUiY683Ky+gzuvKjrr/tfrMtOeTYg9kUO/xcDzz58zFeMNGMN7h+aXnF8VEjQYFyUCSmluajT+lh3NKPaR+Suuhjod+oGyPbh3C8+HsaMHrZg1Jv1ozLWd0wGJBY0cRGXBZ04bl5aSCAcDuMITj35gRk0eN2TD3OBp4cPC5Nm1aN4MnIDdvmUNV3KMkBb0560wbL5KsmFIBcE/qADT5CKepNre8hYgIhN3m3TW74W2agNOXb5A3oyi/A815QAEDL4n8lXv23Qaef3Tf6hB6QHPFHwAnmxb4Vi5DMegFsHmK6h1nF9DnLzgZazDFiCco8H0QHu1dyw5JGdPKg2aMOw9MtHsUsVyFq/VLDcCl5pKJ+X8VedJKQkhLEk6SAhbbrCcfskUse/SmmyBgmyJiVbBBYuWJsJGgtvQEz/ute3KXo3oblOeRRE7R6N/n7itnHCyJXwM6RF3cRkx0RkCaYZKV6GKVlnd0qfpakWbtL3qIXjzbDbrskXRxAkOGhwZPqzh6jpXyo76q+umhg8LcecxJoePAr9ZkqTYLWuokz17yqipCl854FrPPljjmwyGJX7Ka2kQPGXqrBhH51OQn4NBxeiRA89sjXKl7zq0JjLIrRAAnw1rl/Tp6VRSXIBrC2GGrIwek3YkJu1I9NUd02IXhtFGOPhMGjcCZh3ujqNJMJhLMCa7Fo8rPDV324JQWqWp/GxYs9hh10rd4FIF8KH5GAvsBeNIIQZqi/UDDEOAx92dCytwD6pcdDFUVz9AQGbKWoKo1dN/rLMlm71WHYzo1a/6KCheli+T1A9w1e0XRM6/IoUhHbkaAQ7cpotGMkZ1qa0gDkP6c4FgJVExylGZ+ElUsWmaaSYMM829nDUil4hfRxg1eDwuK9DrpTCi6xGPamd3o2ozgCjE9leJFBkmfQYViwvsDc/rr5KizG8AAr1el02s1c0Xo7QB3cWpadSQl+uMc9RtCGTi4kxTX9JtVy6dU7rCIUGD0k8udiVudyXvOL99FibqgM/ShdEdHe1zoyOQyvPowTMmjqi7tv7gmimj3S1k4HkXFxX0HehZSRiocRTAG5xGMEwWejaoNwxA7/LFsykvZfLxRa60nWvmhlEY1gNabcY6eHk5NFm6eMbo5EPRSQdnpR6Ozjg2e/nMoJEjBio5IemsCw4aPN79j52Lx2UcjbmxGyXQQhWljMoP4FiNi6haTxxoWxnfovprnzSXNA1pZz9GqNJ7XDyTGtWtUtkOOsUPhsu781DfgiCnmYayv3ODVHMZHrFYwO4ESZBq3myd3PUH2FKdsYmKtXijgQ2MVSxkQajqzS7TTDNhmGnMyl8jqwOAE55UA2XaiP+gGJhhblGZXCHdn3c36gQF81nZUu0use9aCxBVI8rm/U3gW7RaHXZ00YgaAF1cCoiqLDp6/nGDN4zrQpF8U2cFno48V+LHSMmoj8HXrhvz//ZGyrIeMkmScIEc/Ywa8fKBVVMAg7kSYmsvr52IepYG4/RF/IM7M6einAZAr0u7Y1wZuwA/YJz2WswUABh96c7zXmLMgV5pTiQUt9bktRexUycOUQy2acF4Z0Js+93Ny6NDaP3njMjQ40f29bHBVCKajFTKtAHg6sCK8LQjMYkHZ6UcRjmx+dNGTQh5VYnE4N/hwa8C+rqyfdrFbVNTj0QfXT0xUsZgmOQjJGhwiAKSXTj3psZFVG+RGYCKOVZFOVXlP/6MwiC5cONvvQiGyR10UgtHVX/jW4yuRkhtxQNSH8xbWl93ANXt6+axQElvmXkFYBW/wY6DyzHin9So93O2st4EFbIfdC/7ZKrGUT350GngGNdjO5p7y2wEB6A9y2WaCcNM62FDOEFuGy0co328s401vOpLZ7nchWqYmgIAlWiNNRgtFEz6rHAhX+1e0mLLr4Bkq0CFBKR16jlh8Fm336/yOOKuQLCCR7iTg+IomUsqhfJe5AEU3Mg5iyRdAQMbC/D6oF2/erELPV2wu9qw7fYmV2Jsy/UNkeOHhQQNwvkuXEqHVZ5rLq1xpe2MXTxhtDuNM292pMPRp5q8S2cqOOu9x1+onnijgKRye3v7nVvXkpPiGhrqlX+vr6+dHI5aoQBUwMBWvYvGMO2NxTQVRj+Z6X11ZsItR4SPxHfhzpoOi3t9ZvKhWYDEkg+hnNjepePHjmaYCv69eMbo9KMx8IFjVseMhRmFQRcgtNAxrwByO7t5SuzCMIrEtOWtLfFoXYWPtYBjQchjobSWa34YEidqv/FNcFzkbBZX4Wgk41u7W4N3Iecf7rK0T2grcALYyPgFV92dNjR6m9SwZPxMY9F2tqBuLrQ5LtI1Di2k8MEbEZSKE5L2bVISrxl8KYlU9DV477CCeYVrI5O/KNZGbqwpuc2qN/SaWKAdpRzhWcOAFwzrRVW7ppkw7D1q9cfYStF6hyNwlUTqPVDE65TOk1LaQ32RKkxfzl9k3yk6NUGYKJkCP1wIIdRPj8ji3YXsyZ/3LxcipzW9o+AdudwJbzbedSU8SuKv2nwhAjFmqUgec1j0jbM2i6WwIDctJfH82VPn3j4Zd/92SnI8eKV2u63Xvm3792zrXvQFfvDdg/NcydtdCbFH103tAhLGjh40b2qQ48HWuqvrYiaPxM7x0oXR6pJQvc7a0wg/Acwxlf5D3B/yoJ9UuZr/t48e2oPHasqE0SeOs3rp7Kx0QlI/cuCcKaOk+1tdKTtv7p87ekRXGPbWm8f76PpttVpnTR+vaPd65Z2tkalHogFiwSfz2Ox9y8Yrm75gdu1eMj7zWEzqkZglUUFBIwcp6mMH71sWDt+Fb93fHzVJTszC8HItLLw0icksZ9KW6Acv00ZgWNyjGl1GmmaJcxaGq7Up2usYeRWs8Go1ck6cxnGm/5QDW59gVSc6BILZUDiYTDbsYmoHWlgNpL4SvuotLH0nNOy4opVHltDRzGS7arZ5X2oy5cP6C0VzDLZqhViij63RRr4dVlf+EIU/8/Uek+I0zYRhpjF/GrAQK+Hg8O1ohgfeYX2dr/At0gQyAFUyiBrSg/4QFxm0B7elyJX8BXeh/Jd4eZyc7agika5cohFcykNYtVZ8pJwocVe5AuUhjYqElUaxx2e53+m/VixiLCyaq7PlHgoA12mTfTtbk5DwFGYr4Slb8vY7Tmd5Wcm9OzdiN69euXTuTDfvdnhwp4aWaRFj50ZPXrtywcXzZyrKS43Xg/LB4PrhsrvDsFEjXt6/crIrabsrMbb20toJIUOUjIgIhk0LcsZvKzq7ArvL8L9rVszveytNe5pUtc7WcE01ZvEDOWPGi4ssllaKQ/Bn+5Y1jY0oUv72m8dxOhEQRcLRhVL8NhjkLQvDu2fD3ji639Vn7dzbJ+mNAOLaPD8kw53pSj4UfW9fVHTECJrsAsA/ZdzQG7umpx+NWT8nmNJFuifhwG0LQ+GLSYdmwX99e9Pk8cGkmvHY4b3G7TYOKe8VltDwUcfJG8zD1RawaPvQdivVHSalEwkf9rrLYDVLKqqpvnvm/tuda+KghKE5H5TC8sHs1STBlfBRdWjkrFjJlNP17TWUwaskQvCLsjq8OncFLA6Fo+XGs+e9MmbB36nSjKFRP7FNtigYwVGir7OQK2opdSDPCu4RHrruALe2N7KZzdjsPwjq10moPjYAunMmDDOH4D1nqErkSVWCsm6LCG34gcVXX7kzpS5EJITi637BSDmENlE4q05527L/wsvTVbVawSyfIYw5MWgE+Cc6VkhgB5eA/p9hjxv1ZvwXyTl00XVpz2S3qdqQ4OwoIb1kD/o5S+cIYFFdpPPgVb91+tjCudPCQ4Z6ZBHw+JkQOnT18nnpab2FmaqpqYHShYcGDR41/GXsH4NzvGPJRMTSEb/Nfndz9OSRYxXpC3CsF0eNdcRtbbu5MWriCFyyuGr5aw/hQmSvZeqCgPD57PLFs90ffUIc8psXzJ3m7qwbuHdFhCt1pys+1nJzw9ypowHZdjn+yqXzfXfYrFbrjCkhMgH9K5PDhlzbOT3lcHTakZgzGyePcd8sJqaPXRh6fRf6T+vmBMMgUAwWNGrQhteC41+fCf8p6/hs+FZ48Cs4FjBr2rj6egN5HZzOlG/JGOMnfhmOmm2KumsffOnqrYqKMi/yCYi2Si6wzB+kFTCIQzE1nuQDHIk5FRt8S9I2vytToQxUvVUbYyit2a7rRBdJhjPu/bw0LeRhbZev8EUNrNKRz6UfUzqd8T/raHkwxACIkmt4FLWo8X5rMr1BJCbkFwy2iYHDtG8LeyO4kijly6bomQnDTPOD0cKAlGc9JbgkD8gNk09ghKCDf8LZiqh4dRc3ttwk6o2ILKRM+OuUm6RyBd+ebEVRTFi5ioL1rEG0CBNAo+i6SYkN2pKMW443uDeJfvasf3UNyFGOhMxfq3gMzrZMEirGQFpzTNrTSIhahEGrsrL8xrVL27eunREZyo++un/OnD6mTbrtf8vOSseNYYDBoiaNOLF5BkCC4KDBY91Ay/lgqytuG4Cx89tnKdM1o0a8fGZLlCtjd/rJJcEyftBu1+mLhmqovi0E151Ox5IFM7s/8bWrFqalJpIBHP7y2diZrpQdruQdJzZOHz3y5e7H3719vU+PHNzs4vkzKW5fHTMWMFjqkeit80NxYxj8cf3c4IJTc5MPRa+bGzxaMcGCRw9eM3ss/D39SEzCgVmHVk6YPI4IQE+NGFtSXGjoI7Yy8vSMn/mlIIoyUuiggFJOreLppFcWPpZ4L5GVCwwbGCsu33pPpz6n0mheqP6YapRLpgNJ+74e/QB7LWsTKB4v8EXYFHCAMvEp7SqJsllypcaf3BPJ03XCj9A4DspBBdyc7ajHnk4JfnWcutfZfhr/hK+kLJ59vHXswlAV0tsir20Hq5TJ/R8ThpkwzDT/GM34c5III87Dz5Ov2Mr1nNFWQYr4kz6th0gXkFj6DxApvD5vAJMmAzbgr4r0pZmVtlwLba40LIrqy0MNe9a2StRTHvc+DwHjugNssVbp3OjIJTCYAOljmhuUlPE8UTvgIMFLS0kET3rSuBHekNXEsOFLF0YfPbw3/sGdtNSkWzev790du3Lp3PlzpmJWhi6fRfNmpKZwV1NIDqclxXCpmVs3LlNkdXB1pCtv3/V9c0aPeBnA1ZzIUfa7mxEMi9vmiNu2ak4oZkREBXVhQ6ovrXGl7gQYRtM4KcnxD+ESZK9TcOit5IRhXZgn6YcmHgHT3j80H3FRxm9bGROCB7aLFvadW9f6+uCdPnGYJsQmhr56a8+MO3ujJoS8GjqGyDRPnzBs8YzRUROHK1kQYUbNmTISMFjW8dkn10XMmDgMUBllrodX6fiRfTA4hhX3oiKI/saSYXaeEBZSc47Y88t9+SVWPIkoXr2AE9o2nPy5rtyzPW4124lnn/WC2s4FSBj2AlI2v07PiSghe+InxQYh+89yN9oZjSNpS3NcP6nlOmAwp6O9e2iQdpbqqv83wsB3Itf5mIDyIfgwtN8+/TnhchsuVH+HMY2hazsq9nXah5L+I2EBVdNMGGYatw9UhSrISWczX1twRx5agn2RwqBF8Pr4oHwJpsI94oUp7XuBYJOnsSiAuwI36HBl/pIlKiUjabWdlnQP6JcqfalrZKPKxk8zGJb5PU2M6qxYS6LL2X9QibmWFBesXbnAG/qKnh5+6PWdBfk51VWe2cYcDkdtTfWtG1e6d2FNnxxcXVXJNeb5g9BbAONgMRLt1NZWR04KwqmJ5dHBrqTthW+vAJcXIMGMiSPabm0EnOB6sNWVsuPdXTGj3PpgYW6axIqLq11pu87Gzholi4YVFnA3wTsapb5CUGktINFxbnkDgAdUCxuDqy4yWfCXuVNHW+9sRkSUNzZMn4BACGCPmMmjYMwpEkuIv9f7h8fpsEqS1xenqamBIlK4waVRQff2Rc2cxEAX3Cz8XYnB8Fi9vWly3sk5F7dFTg4bgqthw7q9d5s3LDcmn0y54+Le7xP5hNddrFpW7P2hT1T1bckoRUPg4kXvsUs59VEU1rvmCuwUNEhauUrtyPYsFunToSwMq2XGT/SQ1DdfIufN/Zv8KntPxFGgCP/wttHQ+kb06AM/4HbWYw/3xa8ySrszsLS9P8SdwU+j4S2vjSeqBtMbhcufNkrO0TQThpnmxXCtGuptHRmgM6K1mNIr5QX6fhvfIikdWECdLf49l61MZor7sFiapeUqiR/Hf4CXU8RXuLBH4Sp5d/drdzFi6MT32VoyNfwe2LCzfo9KkgD3eiK2rigvPX/2FO1yUX4WzJ0Wu3n1zeuXWls9PybJaZecXTF5UuKDZYtilL+zeH5UR4dW1z7yYJ5hgqfGQXRLa8vUiLEYG7w2NUiK21b2zqrx7iYc+Evi0YWIpQNgWNL2zJNLAHHB38FRBs84/cRiV8buExunYxg2adzw7Kx03rMWhaKXqwsXS6+1jJ8JCMq7jaozw3CtnhM6edzQMEWaK2jkwK0Lw9HAJm1PO4Go6uETPXlk7eW1y2YF0x68pIQ+Mj6qVlpSFDkxiJDXBw2OmTxiChqNTq8SjBJFYoDKls0Mur1nxub5IVPkQkSE892HdfmiMUNUPl9W8fY1W+X53a09QGI9mc/7NpSyvkL8416vsy3FlfIVfzLv+2CI0PhRcv3q9X4AGEix3z/1FJs1nJQLGr8rVtCY9VsZvB11hxjaPSS4CLSuISgC1jFvXWGWOBLBge215aZvwaAS4bIXcB4o0UjyFwRyTbYK4hVg/iqVwhxAy6130TIuem32OkYwBi5EuS5up5ptaK9vueYyzYRhpvndKGtt2ewAMZnSnlpYlwNfc0xjUdl/8vu5aPGGaO0HXeIDIIODjeqfqisKdOTQinaJpw7EaUGTCnBO573EarW+feaNiWHDu6CvmVPDjhzck5Od4XTqnBhOp/PwgV3K3zywb7t2hVXeQEWft2GppLY2C87RIZHcoEGACuqvrMUuL+CrnUsnof4ld4eY9d6WrYsmwN+HDf3PtkUTpPhY+E+H10ZiGBYVGdZm4awMkQWRNcmge4nR3G/OX7luT5JobxgM18qYkC4wDEbsyNqpSAwgflvLzY2zIlCZa9bppZUXV2N6dzyezU2NrofCbl6/RO9dSYRIcVdE2JCoicPx32EeTgodAp8xo1iJ5sgRA3cuHrd7yTg4eHww++7B/TuMeL6/YYF/w2kSYVWh88eX9JSjmeGr0hleD6s7yAgwelu3DN1Vy+dqbS4yw2HjGT0ngn2TJ+fWNbZ4SS6Y/C12MyTJIXkZQ8lWS5Qn4aF4jPR15Mupv/4IFvpiZTEIzqFdjxtSAkqkYqqp30Qojt9y/sbqOdUrLygpmuh+VDCM7WW+tJxJVpdpJgwzTY6LpEmWOH/9uLWYlSg0vROI24FVjLbV9kjMnvYxN7/r3xPZyhHLEKEPFiGKpSSzmmRcRlnDca6EGEIsL7GqcZceJauC/JwVS+YoncXxwa/OmTnp/NunmpubDLmbLkisuKhA4wsAd+VWBGflBhUXQdSU7WFX9sxuvrFhVgSSAgsOGhw1cUTN5bWuBHddopsyseydVXcOzGu8tt6VECslxMYunoipOxbOnSaASzERDpIxbekDixuTZ/0mTxjIZrPNmx2pUpQI43xoTSRJMybEwpCiCs/kHXWX1wIgwdhj5rRx7e1trofF3jj2useCXphjq2LG3t4z4/L2aXDjoUGvyJLNBICFucWdV8wac3dfVPKhWXDY2tljaTtifNxdAy6Oeu3+gGG2UlKRiHIsb/i0JWEuWbhIh+oSVH/MVTxJD0eUvw1zZoDn3Z6pHqciEhEAA3QwHrclykRZT/APgsNa60r/KknOcPJOVSxFgM0bq0Tuv+RY6h99mz9lZP6g0eDeeqhwHAxFK3dts7MVzRxaKFg6U3U/usAKNIToH2HcGIXMXJdpJgwzzQCDhQ+vFDqI2jkNkfa8X67DDkiQr2qNHAL/ew8Macs15vm1+rlFhBL7ComrgAONg2Fxj/HodBmDGGVSew0NsY5stkPUHRA9z9XL70wa1zUJBn+Ju3/byNiC1apEetu3rrVYWtUjHbIwXT9n/minwzAfvaGhHsudAaBCLB1pu+KOLMDgCgBD7OIJrsRYBBgwa2JCLMIP8dvg03ZrY9SkESFu1rvZURO1Syup5b8iM20m94H1rf6onF8d4Go6x/ONIwf3KPM/wUGdyOhHj3j59VWTCQxzIzH0Sdye99YyVHfnPmbl0rkOx0PF/ZWcFNedXHT3knHJhxCLfdKh6B2LwkLHoB5FJWqF0duxaFzqkWh82F2F5tiObesMGCLYsDJ+LtewfY+32gJAEadP7LS4kj5rgOoxjkNl/0XHgtYrjBbbp35DI4mBuEm/JwuiCLbSwXez/6CDIFEqjRHq/9Swmh1kxUBCoCU+Tc7SGXJ682Xeb5XJ95L4lJhUdFEoA0gFw9ViZAD1KYkiAE7+rFTFYnaK4nAznWXCMNMMMiqKot467KOVTtPQSzHWEC3EV4nv5TtLrx7kuV8Og33c7y1quOAt/kkxtxi8CqztlvgJv2ieekCMV9jqr04mSVsKk57mj4m2NDfFblnjjYojclLQ2bdOGFgqBqeLnhFOfz8xXj3v6iQ5UoBhaT82NhixM3Y9rqAbH/xq7eW1DdfWTQodAs4ufKaMH9ZyfYMrXkZi9JMYW/j2ivHBr9Byu6wM7pho+Ty5faUvdFe3ZxHSMJh7fLnxM6eOkoq7UYM2zh+/e3nEGIXq2qjhLx9YPQUVJSrHM3nHqU0zcIUnfM6/fcr10FltbfWmdctYj9yoQbELwzKOIVnnpINIoPnMxskLp4+OHE+axzCjPRyA5Zvv7YuaNWk4ToXBy9jS0mzANSnoVaWUr3PJRbZnoHhQ/qtcIKEtlfw+6lPiOL71jptl3vmwPXtaZ1g0TnvvrT8ipf+cX6av0wZBE5u2St5vwQZBVDS/iFjBfLSmcySsmfBhV/Nln36K7mKoDohPRZDyZMKsa7khcK7K5QoN5b+oHWl5wBiJ8/4jEHmv2cq0y8w8mAnDTDPSwHdn9HS/9IkMSsVgfcRNscmfN5y2W2NF44+SGmuFYwNU+GeJJ45mwVCxL9Iib10KyHqMhq4zf6U6W2oYV0dRMM8Pl5WVKGvJMAlH92Kq2VET3zl3uqS4oK3NcyuUEI/2hfNv0l++cO5N3gmZ8EFnu5amDXhyDcddFi5C/AP7Ymm93OnNM1yZu09umj5i2EtDh/4nZsooy+1NhC+xM2y4c2CeUvAqP4+7qJV2P/Ill3rB+iYXRdfu4vkGzBCa+Dq8NjLtxGIqigWodczIgRknl7Aco7vg03JrY3TESCrC1idoEnWYw25/cO+WO9s8BGDYgRXhaUcRDMMf+HfakZj5U0chhvoxr4QHv3plx7SMozHwObd5SnTEcCw4Bp+L588Yc0G01hdgWOq3XU4O8hvsTSY/gxSZtKOHMxVKhloOa1sK6TjiW7L6klHxPd62AifX8HZeellfEzwjfqNNa74QLBNEV06eoD7p0U5RuuskyomFpHnibsUTFSWF0wXOVaOQBQcvS2XX6MhHrI+UIpKfKR4QcvwTjHnFNBOGmWY0YlGEUoTUEkVjXVhwPeu/ucKWvi6pFaiowAjZTb0XUCrTMT3ia5uv5p6X+i33Evxf6K4FtoprJD2V9BmXoz4QY9LwBov2qYv81B9TEKBpJMTycrNmTR+vhFvrVi2sqUbx1PNnT1GqN2WrGOCxg/t33LtzvaqyvL29Xd/d1NRUYb54+MyNnqzRDoTQslwdZ9Fy06tWE1Iyjpjo8SP7aSNTzORRrbc3OeK2Ang4t31WwdvLOwEGBQy7uDN6tMxWf/TQHpuNu8LEEkduhFOpvGet/ijz1PnCDelpyeEhQ/B4LpgeVHd57fQJw4cPfQlQ7sjhL8cunuh0q7F1Si2eXUHpK+ZGR1ha+0LXnF4rLS2uqSy9vm928qHopEOzlDBs79LxuOYQgOuuxeNyT8y5vWcG/BFrOsP/Tggb9vaZNwy7lLYktnNx1lngaEjad7VhlbOV8WpULNEOUNLsR+InArScBsbArceIIvWbAg1Oeh6lOw2V+nUBLtm2ZFfCR0h4V2jv82hU6dRHRU3LfUbuD96Oo4FjL9mh0werP8KyWwUjkdfhzepeZyATBpk/34jiCx+X46e/8alQ0zQThpnm3ZN9SaECccRfZ6F1z7BjBcCocHDy5/SoOftuTWcJyW/yM/5VEqvbp1BBEanYZgHIHQEaEyqMk/m82qU62wn7iJYEXGFB7vTOlPS3b3YCeCXFBd0lv5SqzXNmRZw4fiA5Ka5FnMDj2OG99KeuXNKqPKF0Aqhyybs5GlkEoXan5jUAklyzYr6cwBkIOIH0gCXvQP/ojsHiUAJn5ezQILfu8PEj+8TuuSMPtcKjkG1UL1zJnPZmp02BgqhOKLcGa2NjQ0T4SFznOTF0SNO19VlvLj28NnLXsknX9s5xuNNfncYzZcfbsTNpxsywVE9vNau148o7J4+tn3Z8zSSKwZIPzbq5e/r0CcNCRg8GxLUmZuy9/VH7l4dPCx8WJDeMzZwaBhDXyEsBKFW1GgWhcv/FWwpYFOym//mxJqySWu4y1SYNXgq35f5b3m6e6Zntxk+WL6tO66gz5Dfa2lQyWeBblO6v/phhIcKUr/qENq3FTFMr/nGulkJLHJNCLp4ocK72DKaLCC6cypSGRRuLuBISxQf8C6or7fuM/Dkw/QummTDsvWj2KsaLkPYdf1Xx2Url2NXnAlSaSGFGT7mMZXPkSNVQ/QWf8DhKZyJXo3S65/4WcNxTvyZXUxzXg98yfiFeRsKuT00os/suhUsokVu8RhVerJfdoP7o356suanxtZgpyjSXx7Ycu9321uljs9xUFiqfmdPG7d25WaBLyuVKS02kXwewp3E07cNUJ+CyFhKcg9o1r/BcRn1dLWbnD3WrMze5uRA9ADBFAmffysmYyePtN4+LPW3UXeAOLuT8rReuZJKjw+lQhDyovK8me5hiUk0OH0VVws7FznKl7kSY1q0V1ikP5oZkHXc2vzZ1NO16KiszOFoMs7etzQLvmMPhKCzIPXHsQElxQW5OZkpyPMzqivJSoUpana+3gkjzrTePwzQLGjkoNGjwpdipKYejE929YZvnhcAf0SREDIpjlkYFweiFytJhK5bMgcv2z/XZeJdW2OZwQgAgk9DaqFlURlUcUZBlzkPkGNSyTIgQpZ6Qtd6SpSyfcLVzCxhS4JT5G58v4A5qBsMX0Jao/3cczUyoEPUjvMIBc1pY6iz9xygHy+9NgatGWYXVHaqicWwzrXvd+5veeZ5LHQweg+fmB10+00wYZpoyXpLNIitCtHtCBksAbQ8NgFkLCFUG+LUB08jqvJKhXRzfsm7WRHB86coOHrBHdQH4cVw6gmRGud0yRwMrvBEVH1PeoxDCpEWwsLKrBAthDwC4To78MCIb7GzlZSULX5vO+r5mTszLzVIbRUtrcVHB9asXAS+Fhwz1BsYAy23bvOqdc6fTUhKrKssbGuqtVqvT6QFnZqanzI2OoF+MGD+itqZa9RXLJxAUEa9le99fK10JH8M37qzhTVWdPnGYEkscXTfVGb8NZb28wbCk7fEyoeKObYLPHR4ELoPhp//qQaMNkNy8qfC4Vy17jQ7mwhljWm5u7Iq+FIB2xxLC+w+fIwcNyBh0dLQDyrpw7s2dsevXrVo4L2bKzGnjFs+Pmj9n6oSwYXAW5eydNG7Eonkz3r3wVkZ6cllpsc1mfATNDcPI/AdAOHNqGBUN27F4XNoRN0vHoei3N03GhPW4BDFY7gSDF+rg/h0CVa/+M1rqzFOeSmmlSqZoH1y+gCpS2GuOuB4ao0Ijad8WaCUSMmcb0cBAZTKbBL6Y+k2SceLJVaoYwEsMzmED9bHfNe9FBVXGHzV0rt1BI+RrUblqBzeDlL0aQS+Sff0iitypWO0eBTIcouYO2C2dfCcqXwbbVsMbLtNMGGaa3616CxNHb8/w11nyB8v9vhcCcVPl8xUEJD1hiHxJrhjUR6KlbIFABRiHPR8G7gKhsLsq8OPF4XLQ96cCDH6wDdirfYCmP5E1Opep7mqKWtmGE532bqdzwdxpSviUlioQxQQIB0Br49qlUyaMUsmPAbKKnBg0Z1YE4D1wzfft3nrm1NGsTAQI4x/cmdG5GBI+WrT4TiRWQxzBaO8wrIyqFUk12znvqKgwD/xd7AePGTkw6/RSV+J27zBsx+0Dr+E6OnDuPYJM74++huS0+wQVQUcOy/Nzt93TJOfYUQOXzBzrvL9V8jiMCbH1V9ZNChsSEmQM+YTdbgM4PWfmJPW0rbdPeMiQ3Ts2+XU4AZItWxRNdcOiJg6Pf30m7hBLOxK9cPqokcMHdlF5BiTp73wdr9Gq+OJJ2gdjpSye1mLJzooRAIY19AUGUU7L+Yce0gghq1qnINPi3oBqd8tlJiN8vQCa8EEiy75s9O8o2M5+wiWrSOmOwONqviRwLqpslvRZbclQutuCC8TZHAHYmIaP4594ODCY3dosSQ+VjogJwx5SK56gEJTwz5S115IsR+rXA8HVIdmQUBUu8a9caeRN1e6V8gZxsPw7Gcd38Xg9BZ9OC6MMVtl4UGmH+M7U+La8oH+G83FIjmaUc0v8lFdAyI+NwddRsdZ7pP4NgfazCgzmUAooT58cfPWyTmXw2tpq+O6GtUs0SxbpZ0LYsM0blnv8T1s2aLFWVCyUb/wnXlOIsFPSSCd3oyZ4unAXlDLx+r45rpSdXmFYQmzFxdUTQl4NHTM4clJQY4MHRgGH3W61Wr2Ayd+5+wm39401zVaOSnn5yjvJrKipnjppDFa+mhqOSf+3eUyFbV/MUmEwkpWVOut2mpubdsau78L2qe9zcP+O6qoK/w0nbUQcO2rQilljcFEifOAfl2Knro4ZC9hsUuiQMPl65kZHtFksPT8NYBVNf45sBJqA3F4nK4Y9oc1JUL2JLc7pPzZWT6mkuLCyoofUnC0JTA+g6bK/HkrKs8K9Z6jT6cMyM4dvlXI1O0gFePIX1SoUtNffDsYnmfwFycpxVe1p6KSipSgwu2ggO+4RlQpDdhbSEvZx3rQhPJT8IWxK8xHM9gWTXO9VM2FYXzNw4gk91CJ/nYKSSql3BxlljiYE+Yzq5UXrVIdUOIpR+fEog+X+j1CDiofoFC2vgvXUYxECuAuYwi7uMTHNk+Jw1FXMvdpKTVdZ83pbkp7bac8gJZSwBXbkqvqnV1ClBwBpBd8UACfqdE6LGFtaUmSAy2Fp3b9nqw6vF8CPUspZg4SAtkqrd28DXKfkV9x24dybtKNp17JJ9jhWlyjd7wrDqi6uAV851K0b5rGY0wlg1+6FTQ78FURM53xYl0CYDNHTw7H41arZoVLcVg8Vnik7sk8vDR49CGuvjQ9+9fbNq/pOp84io+MTET7SSELCzhZ3/zY+CwzOjkVh6QrOekBiaUdikg/NWj177FiF2Fp2VnrPP9SW64wcSNNoY2rGLzS8N0c9AWw6WnO9W0py/OYNy7dsXBEeMjQqMqy4qKAHRoyWZSZ+EnU9+cPAx6Aled65KyXJ4bRWedhMfaQTg12S7JiP+qQvaq9i9EsJjztbOYRGbBWs5F6oybZiKZtsiMBTcy17gGg/YLlufpfvXurkaAVuHhloesQmDDOtJ2JglPGJh2tVX+gIc6wnfspl9c2H5kwu0W019RsGpOCc7aQwnTIIaQZakNLo+4j/rW/Ft1craJFe9DQUDpTu09G1LFnFhC9bb5OOal+4fTN/yTYhEXaQjo52ZebqzKmjBk7MnOyMs2+d2BW7gfbhKHvGuju79+7c6AILtVut8v4j95xM9XoMfY4pX+FX2Lx08SyVtwJ8VX5hlWe2ekQssdV2d/OC6WMwscSF82+ay14XwwnP0SMG3tw/t6tYM2L83152cXX05JFjZa2wA/t05gZv3biMM2/KmTZnVsThA7sA8Bw/sg98cZjk5946kZaaeOPapayM1Du3rx3cv2PKhNHqYOzi+TPwshg+MiXFhbj2dYJbGYxmw/An6dCsuNdnzpgwjBZqwkg6HL2gFqjlqoBnmfkrOWo2TdufpstyypcMqe9IT02aFjG2i/r8yeMHvakd+me4rjGioKIwv5zCWkDKm2E3UYWvTke71Cp3RFNud8S16wP/cEc+IkXEZ/dF0ga23aIQJsTCg8Nhw8UFBahV4fcC9IOWOMXOO84vVGp0e4J7KZ0hoO9smgnDTDM0RrVYLk38t7+Y1lvvEIKgrN/qj7RVb5LSfsSlcouCT8/oocT1ZrV7O/drcSTZKGkbXIl6T63XX4hkaajOvVIKB+J5coBf1XULhrNkIGzYOqwmlo2eyKXu271V2XbS3OwXKRubzVpRXpoQdxdcolNvHJodNbELKlu5dC5NfMGRlDUBjtRgI6BuBDht3sIctDMeNaDzZhLA2T1x/ICcqXBT/CV650tM3L59yUQsHQb30iPdO72lZcjTSGL2l5CgQTlvLuvaZZe0vezC6qiJI2g5IhzspYBTKzpUWd4F3q9duSAnO8PaobHq3r97c/qUkKkRY5MT49698FYX3Tz6WbJgpuG++41r7+LGsOiIEQkHZimlw+CTegSJNeMsK3wmh4+C2+kdT1VCQhE5/9BsnZXac9F7hx10TeG+9iwSX0M7S6TvV1lZWd4Fg9EPvN2BGioHC5MBIvWHDwCnoPUdPIyCdLSxmAfKX931JZLqyh+ko+LAkyegIMCoWs1xZguiTSa51p8L0HLARkD5tAC8GV6M0JbM8KQoXYppJgwzzfgdi1K98wtZOJrU1AO7W+ObpPOHh4qqu1Fh1oyfcBWIw+5LqNIHeKZ9F1vG21lpBE7r8SSU8l6WWwie0xPKkqyM3Db1m56pqxpO+Z3byuWmWKRlGEhwU1c7BNvpf8kZRaaaTvCZFzNFn+8rZBnpyQvmTlP6yssXz87Py+6M2Wzz50ylB2hQ3iPCd1lws9FLGspR70p8Si7N38N/tXBqSp8wNXxY+62Nnpua3Fgi6dhCXDkGvnJDQyAEZwF3pSTFgze5Y9u6pQtnrVgyOzUlIQAPUdTy83LgiePGsObr613xCjSbsK311sZ504JGjxjIW4nq3TatW6bkgwGEo/mV3OxMGDrSphg6tKIcLbltFgvgsf17tlE9cfpZs2I+PsYow3LhMDgxESNw+ksJw9KOxJzZODlYToXFbl7t6msmtaXL/ULPaCfqa7bLEZMnu7O56rBbN64oH19o0CD44H/Dcw/MCFirT7ri5K2tzj/Yj7JWAojlr2wnbd79UL2AL0aFZFK/JsAR78GpuMb6u7L+mwsaUZHopKe12TWo2atRIQ+lRjRcmM5aRNoEAtktou5fmWbCsPe6USINeDltfC3CNVvRV2piBc6S/kP5FOKNtkVhbNVI+y6XuDtDQT8yJqFPudfVue+UkTAa0ILh0uMhDmJnrDvoxRmXCx4aTvpxhrSlKK5kn55fwFKqPJSJsu3dtZk6KO9eeMuvb4DDbscep/Kzd+dmj3wDh17fqeRI0PhpSoSjUpdIsppivPBNTQ0z3RWbYWNeCR49KPHYQiR45Z3lb2LoqzhxocXx6OtI5uZkbly3dNG8Gd1j/K/FTFm7auHRw3vv3bmekhzfGxa/zPQUXJH4+qopSCtMqXyduvPkxukjhr1Er3/h3Gn6iu5Qea0ii5WcFNflUba3e/BFlDQe0dPDS4oLlEnF+rraXds3dBnhGZGh5capmR3Ytx1T0i+cPir5UHTSwa4w7NzmKbQiUftd6IWW/7IAVX3pdDka+DNDTn739nUlBps2cdT0iNEYiY0PfjUw9cPOynUEhuX8018toIVBcsXN/3Ejt8OkJA/xG/lgrbdYArPRh/FEyiIfYVsYT/NV5Sqdm2bBSFYIo80KJhp4UNB+IOG713r4BaxYjOCxZiLaNBOGPfxGCaA4ay0omx8/wGg4waofRZf7pgudygJhNdesnejIZ6WJRgX5aKFg/Ae4mr6azpJkSNLTYslDskO2MYp5ALEeU3BU1zLjp/7loqQMkDwKpx62w9ukQ5qjS9DpdCrZEWOiJlgsrf67M/CAlZAPPgAhACd43V4rymiabvXyeRq/TtkCYA44vBRVVm9UqEcIMBxQBoWxowbNnTq6xVtCLG6b7f6WlTGhY0YN9FO9k6W15fbNK7tiN/ATAIKjuXvHxquX3+nZbqJ7d264YdjLV/bM7oRjE2KPr5+GZbLxBW/duLK6qlLfWQDS0xvfv2ebspY1KeH+zKlh0TPCuzyXM6eO4sQs/O+7F95qbm6yWq3dyw6VzYp09hqV8Ny9YxOeXevmBKcp+DloUeL6ucG4ZS48ZKhR8D5w9auwCmEWPuRYa/m7kg3JQ5HatqGGnB9eGYrBZkWOPXd4/TtHNsyQkVjE+BGAtP0+CLW7CBGfENWTAIApZ2rXTef5VpN4V9Kn3bXcz/qksAw7ZvIX5Lq7DT7Elhpd2X9mbVTeQqKdlv2brsRPuJf0x1x1IgKDVetJ3RD8b+1eg5+FvZbJqOASTR1bec02V9lsI/bdNLfSdH85u2iaCcPe4+ZoIEQUsBy3cuymSASjv1yhxy0qRV35GsG4qaOeNVLzs1RR8gPEdWsErTNq0ZZDYqlftDdzhPMpPVTGT/S0wFpL5NaFfkhgsfuiCc4BXVhFStrEMaGF7KbguNh0OaM055P0WfWka5vFEjmRFVzpZqjXnlYOx41r7y5UiJKFhww5fmS/JjDYsGaxoj3MpoGlU76sQfZlLSSVUXH9HOWrBHZVuw3nWMLcvnLB28s75XM6c/0BqBjlbg/btX2DgWOYm5O5btXCqMgw3Vx/8+dMPbh/R35eTo+sfO+cO40JJx8cns/4ORK3Z59eCqiVpnoAYcJo63x1nE6aG5wQOrSmmr0+yYlxdBw2rlvKsFnig0njhlP4R/+o9Mubmki34RtHuyZyDx/YZcjgYFLHMaMGrfUEw5IPRc+NHIm1m+EGDVtpnIGi5bQWk9UVfGtNgm9YaXGrknFxvZTkeIy0g0cNXL0w8tqpbddPx25aHjV21Ms8CcaK8tKG+jqfF8EmxL0E26ufjBbmoR4nPqNZR6Fym6731cywkxA/YffnrizG4VGIsdwnMPJBP2e+SDda3esKMrA/GL2Dt6LfpGAS8J4OWEuHom6/b3G7BObYIHacGS7TTBhmGgqGYV8w7ftcRBqY/1C92qq74YLvxKeEcRHsFmnf66Rbr4lqUPPxr2W5xjHGjBItTYzr5yyK4FrEaZ03rOA6yqBpjy9qbvYUJ6ZSkuk/8mdCzEluJP4JnTCsIx8hsZRnNWOilZXl1AeFT2FBrj/uB7zYHdvWKZ3XiWHDr1ziitdSzV/wohLi72mMG523Wb/zPGnhXcAR0Kxf2JpThe7i2pULlLn+4JpIrzAsafu1vXMwz8SMyNAWI8hOWltbzpw+pnxS3rJec6Mnq2tn43TK2pULigrzA0nmgfk5wtx1d4yfI36bK3Xn7mURtCVs9fJ5vnS1FRcVUFqX6BnhLS3NFGnQLNnc6IjaWhLPctjttCUM4I2llYjDwgGYz6O5qXHTumWRE4NSkuJx2OL4kf3KnsaI8SNuXr/k+/hg+eaxowftXjIu7UgnGJZ8OPrW7ukRYUQIIWC9TIYuARfIe5f2He0CjY4cWXjjg74KWMlWVlaCn1rIaJQNu/TG5qsnt76xezlmQMXPsaqbPB1M2qSE+wf2bQdIP3PauNMnDhvwOovyJKNWZI73tOUGqqzDXWGc9XVtSSi2S0pIfFBRowU+qV/TuWFhyxuoUIp7DkWEtQHGB+VG6B842rlvoeU646tEnRdFRk518IVy/infSH/UbK8jIgyeIR2K5M/pdzbsta6cv7PCSyPYbvxjfVJ8zIRhfdzAoceF1Ln/4jj4nKy32x+FrzinbFsK+ZYOzqLGM50iKDyCwogj4RFykZxiGppoBCmhDSBlAy0c8kGW+2RfQeG9bXqCWBVLUQm1N65b8OxplKtwrB+XJNiQkv4LqRL7shk4tD0G8CCVrAPG34kkXb54FtBIF9o6fsUe8IMph/jmDctVkYOEJF/kSSvVn/T4BKXqbSjBK56ztdms82KmuOuaBk8MHdJwdZ0rIdYjWWLumWVu5St0zdev+tRyAJjk6uV3YqIm8CS7ANxmpqeAp5iSHI+512lJpwcVrPEjAPPEP7jjJ1bMLoYbw7DLW3p+JWKbdBdwnt48IzzkVfz32TMnNvpW42e32zCewdkwGAScbgUPe3I4QacA7OnxVy6do9A0K7MrFQRczOL5M/EByqjBvTvXYaiVg8nDAqJitTXVmAUEMOrJ9RFdYBj831PrIyg/x4olXHVKDrf1lv2OUuAWjtY+mIo48WiR8ZnF0oqz2SFBg6Imj3n3+CZAYvDZvGJmiKyOkJwY12XlURZs8ypnGGslUxF1RMZPNQgMHc2ogp1TDIAGpCgNhi8leU3nSTuAECmIhzuNZP5GYZA2WIW9HlAfrVzlp0a01zCQk/VbY4p32P7Swdrz9NGktd5m0mcY2Ta+paegEaxuP+sWiX8SdVWYZsIw0zpZ7v8KUD7U7mGUO/wUEZm/kcNjl/QsB4mfZGV+PFm7shjWXmWUlS+QY0Jf4FICoUS3sNQKyXZxWkcOaXIAZGhsFK3TttokUICq19rb25T9RQBydDfkeJ5BrS3bt65VOjGTxg0/efygUBLG6XQunh+Fvz510phWOV/hNfiHdZwf9HOWLTB8xM6+dQJfyeiRA8/GznSl7PDI0lFzyS3i7Pabt25c6UvSCYarEy3ElJCtm1Z1oeyj44N1t6lsdGNjQ11tTUZ6MkDB7lx/ygYnONLfk+3EsQM42/Pa1KC225tQHix+265lk0YMe4m2hBmiR3zrxmV6awDJ8B/PnD6G/7Jlwwrl4zh+ZB9VSmAvn91eUV5qs9mogPjShdFdiD2WLYpRjqGP1bw52Rm4NQ7g/fVd05M7i4alH43ZvihsjCzcDM+Lp5gQAHwvgmE0YVLDIQRHifvyhxh1fhgxvNaFjUHjfHDbwisnt149tfXojqWUMlGptw5Pf/P65d1flsiJQYGhP0VWuZJ2+UrqhW0oFYabgT/Bu3FQJqes3+nnC4H1Nu27MuSI0H+nVWs7EYNpP852FKYk4i4fEYBSzjZE+kK++GFUK2ugWQuRp8Q4OeYJ/4Kj/v/Zew+wOLbsWtgzDs9hZv6xPc728//Gfs/j8DzO73lsj9N4nMPM3CvQFTnT0ORMk0EgQOTc5CRAQoAQCIkkIRRA5JxB5CRyhu5++/SpPlU0TXdV041072V/9ekD1F3xVNVeZ6+9Fs2rB3A7bc/B/UxuRnLKhKE4/fOSradXGfcVDLsKRTctnqvo/Q1Wj86lGJl4IGtSCmAGTG+Dp/OeavVnsej4VKkN8B6p3Q//tWrVRPGRbPblC6ys6Nk9OFHdiXor32ClxEjENhB61MIrk2jKs3cdoCBiFmIzzvlqq0WbY2xurstZ3EKyDml99cP7b1peDA/1Q3autlPt9NvJYEYnGJJDzEqem1XntVcjAz98Kz05UXtFOdy/MSzFNazPC/sP+0DEJJBkYtcZamI7QmIJQbaYl+jtbqvC8ez8aG5qcHe2oowEfF1evXi6sb4mEp1ERQSQswo/r6wsPay4yxT6I6Q7Er09HaUl+Y31NZBcynm14Tn+yQktNoytrizjkWZidK05zx/B1/Y08ZsUfxdT4tRccif74huC4UpODmD+yvISDJ+wbTQsb1pfMD8fGeZ71jwKcBpgGKLn6eVms7Ymr98gJ5xYXJh1EbCNBWBgUDnx9F7leMjBsP4Sn5JoewtTmVp9SgybbandX6eVmLaVaeg9VPEOgbcMVlzQlBelLGCAUe5/xtfiwtybKtIay5Kr78Q62BjgYuyDcqqhdKC/h1BVzy6X1FoJCKfjJ+mM/GBc2YeJV9hbG1Yr322jZnW7vqa+HwAkMITwBqDoSF3/BshMyJH2/DpS21IxRA7olKD7FzlQb+B1ACkEkWhio//BIU/ZpifWATyvZnKftmyhNK7RdfkZNY1DJVI3auIWjRXF9nokV3EFw65CcRCXj/GP2d3n3+esBgsPXMzr6/0fKsGe6OQAoNjpBCqXlspggzoIBIJlV1My2SI6t4bXueqP7yB3L4quYKj5q7b9AtkrU4JUtRy+uJJGvwOUv1YvJSCZiwoPUEJyc7E3E3jwHz64NzU5zqmbH5J+ppKEq6NF62v1dXKlPs56bBURIKvo/GnqPO+0avyMAUylCmJGOhECK1TVaT9TEOvJeJrti02cHWyMmLPs7KPuSRXzWjBNtCDDxooOlKaFVAikprqckD/Dgr2UsPtgf+TgN2blsWeKcg1IcLEplivfYKMpHpE5O9J2mhO8HI0szbD6n96GJooMFfeLyBFVP7yPgdnhwQEmpNla3oCRTD48PNRPurzetJyCZ8tLi3jIwQcU1uiqZOU1igDJM2DKgXCNnq522BDP/BM7yxvPMt16z1TDcsNtLWTVMI20ol12jP0XNT23qaJsKN4fpIwoO79yIZLbmRga6KVUOkx1grxtG8tSGu4n15cmebtYYF6in5cDfKyjvYV5ZeHDPFm1FpsBylEGRMe7YrEWqo5LcQySnlIC/FYDjdbYYJKTDSTdxMXRRNGExxjhHUjVjNU1fQZMNfzXMuzxNVZYbiVVtt2fRF1eHGbRZGydji9peCYUaXL8IzeXHfmk4hlDk+wb6tCXcGxU05gWWyOItlmc0nSE1nY7JFdxBcM+j4GdJdp/BN0/LB5aiN3BtceX0A8WQtWaXHkj6f4qMjxh098J+0aI18jyRUNKXPB07v5lap6JzcMC3t+kjqf2rJKSIMqQnORfkVzYl7jZFWg5sEw2myUkwD1TGF9eeqfl1fP2N6/GRofmZqchcd/d3WFyn7ACPkFNUkUEp7dTF8KcPd3tdtb6pCgkEqlIekRD36H8eVDjpYa14Agv0dLsupeD8SHm18m3hwnHq8Ktza/j9jA15MWlUhN6TOahXElt7d0qU3YS67yNjQyRv0SG+Srh+MEVOes2lhwfoSW0j7GruYlumJeFFLimSbrS8yKdcMEQcQUTIy9evUHTCrJSGGxxd2cbThoscB6IhAkTasK4Isf+svlUWkbcfiNCFTdiwWqZDXsAts9WzNhHR9trDFM97Qxa8zzlTMMAhhVH2ZGy4acPhgFKGfy/0qf3j6qWSVyvoOYNu36OFROedcAw4FtRMMzPwxpgGGCwporUqGAXrJfoYGPY3FTvL3DE59nKVAfgWUK4h6ezOSEuwu0vf3CiY/m5Sw2csQNJ32/RhSbltsJT5rI5Sj6LNR/TRZve32CVoytO96voXH/1AnXsFSHD8qtA9eff5dFzoJxUNGc9GDbKmm7wWwihVz5tz1mo+WAUoSAKg/2W+i7SRCyak1PZwQiFAIe+JRGfSK7iCoZ97gLuASzO2/OrksNJrWziaJ56psPNBphKjRWsPRUtxLJlYMOLduCPZQ8Cf40dxfZzapa059dZzfnN+cleNl/XfBMXUkP6N6rvjo2gP50+36WkWTq/onm/SO5RU13OVDNPiA3z83JgCsEpFdxDOMHLlQdfTEuOmp+bWV5ajI4IZH4mJSHyIoY86+tr9+8VONoaMYs2szMqrqaYUFbQdOC/qTa+4xKAFnDLEGoyMbs+UnlLgWRiV/rwg1uQVfPUMtvd2dlmth4BVFYIUWamJ5nsRFxzw3UngjrqHp/LAVtanM/JTAJ0zbxe6amxGhDmlrtxt7ewKYKpsc6DZE90ujrSDl8ne9obYQV2wDDvVlc0MKM1PkKw6+uXaPJlb293c3OdYCrIsxfm6en27s43Cpu7AOfDjYD/Xlpyrlp0fHQo81QzlT+4BiYlArD3dzbuLvKGBcBYe4EXgWGFkXwMwy64ofcGwwb+SAbDVFHgCJ9C00nh9NQYHh4ArgSulnX3EpFeYkVqRKCThYmO3MMN/gKfyU8ObixLdncwIfZiGr87znnwldJPsOVEpXdXE92rvMdC+nU5he6M2nmt7kNwlSGPYaT+YS7eputybORY0M5/gbONquS0/gdiumoUNi+GU5kJwmB2nL8OKQFh4ULitNen5m7AcZFjBOjORlZNIpV/6/k12m9TtCO5iisY9nkMeJLiTlw1JA3ZbqKZUk0c+S5XbVCx+OTkcIPb5rYaNUBXUDBzliabKP151chKtEt3D6MmMU2LELwrpLWMOKULy8nUUaBq4XuOyYlRAroATYlEoqOjw7GRoYqyotTE2wCx2HtSRYT6yn2+7knVRRpmers7fL3sz26orVWVnfdSPGNS8AtIvl+jMdDXjfM5M2OdxCCbY6lrsxwMGygPJZoKXGXrSRMLbi1bXTmXS3xwsC9w5+NPAirAf8zOSGSeLoVUQ3JdTo6P5SqiMZFBSraoRgwN9uExBqervywUwbDu9N77oUhMUqacqZENEVhFyIdw7E8bHhOmohPfhKmvIC2PULCt9jHN9N7d2SaKJg/KiuXOmMLLBMvdYvXl5ujeMBu917kevcWCVzkebfleMu9mQYyvBSYl8q30mUhS6XNbfJmGBCoC0+lhUZFlimnPiXlVwq37w5wc2Pt7O/E4BEzlam/yuCQeYFhTRdrtIGcL42uniIgmOgGevMr86PyUYD8Pa1IKu1ece0mni/T2qHQAG/seB3rF8Qqdc8/5qI2q6d1r/zH1VS6IrAiWp1epRYFkk3+ImxokNa31kiidsOr+4AbuHRhH8QecleU3n9CS1P3fVN+egRBTEaD9Ntv7ArWc/DT9xbH/vErGr2DY5zgIb5Cr2zL7GPkHGR08itP3RMe7h7scnw4H4/TtPfBHGjwIMTKD/yG2kHWnlX7GcTxqFrsisylr/1H0oOfwxRNU0EPzkT+tWvVEy7G3t0u4Vc52ptvbp1hAW1ubHW2vC/OEoUGeSkyrzlbP3Bwt1GDi0fnV/j6zyUduUa1KB/kZ1lBm30/IMbCOOc/8Ot/qxmpjrLxyfXvqSUdacjCfkO7kWo+UxKsXT5k9dSr1AMpKC8nnCZYg8hKwpCVHyfX1QXbOVF4B1EEa3qhS0itN8nhhbbLGMEOk8t8lPGpNDnIzI81OT2oeaGZDL5tIpxb2gIJDm51529zUQAbqzPQUPauzvEhG9cvmp3jgHR0drqwsEY+Ekjs558Gw1tfNcvI2amvo5eekYRjmbKsPMKynWIBqYgxSYrSPBTldcrrqnw4YtpqJUvaBP1bRnyzaodW618uUffJkA30SHu87bJ8z3Z1tBGUlRng2VaTWlyY9K08tSAkGoMWTXUcrU93wAEdAaKF+9qaGHxE5e0e+8UUK+xzi8C2aZ6QUTZTeGvv91Oxq7/+U2oupeJPT8Kn36xwU3uVivZxBe1PX4+Rkk24J6/yqag0JgBnkVT5lwiUVGaNrTahwp9E6GCktotnY30HMJm7vuSEaFSMu4gUYAYQAMvJdNkY1KNZKTjEYYQf2hyVXcQXDPr8hPqRUeiF9VIs3yGImbJniCsIm1C58c5ieqZc9N7+ItAE1FKKjTXHPb1JNYstJLN4ZFdRMWNfPqE+5Pi/e5cv0nX6NWxsD6RVmw+bXctzJzyBJ6nlJP+Rzy0sLb6fGu7vaHj+qyEiLS0+JiYoIEKbE1D2pInJzeCkqyFQ7Xzk+PgKU5et5qggWGeYHoI5sRbVKx9E87e6gMpVRKzD4wbzEoYowBbzEnoy24iACw+CMsdE46e/rcrAxIliip1t1wn1ycpIpjCfnCn8FrheAZ/LHxNhbyjPy3d0duJTMPkC43Jo6V+nSNZub6N70tBC3o8a56UeRfEvKK0zgzt/f14wTenNTPUFEeEIBjnphfnZ4sI8cGtMBnImjsIQMvkZjo3SLXWzUud5921ubXm42zIE6OjKo3p7DVrBpmJ+zEaAvZm8Y4LG2fK8Qd1NM4IRRIUfKhWM8OT5WeM9+WO84SDpVJoiIKSCjUShPzeF/OfLe4dlFAS0z3ax4fwBgAMMQEqtIjQ9ztzLVkXIRr4X5O7x8mJ5y29v8dIksISb0kk4UKfv0/56KM7b9TGbsWaJ6tbOe9CNRDVNN2bCSDP0Foy9aPVQjoqujbJSfd9touDKug5Iltq+TVbq4Ckj1RHMGiaI9ZOlGqxH+OWdYuxRNa3IM/IHqnkkV86ldyDBgk51tBpwHGAxkprLzK6iUqg13nysYdhWfsth8RD9otAWNnlKNSUN/qXHpAgVBXDg1qZoo7WfFsvioL4uFhRpRTRz5B40rmFM9D6jrN5rLIUxQs5idP615cMgxGusekVTjbAO6yqh6cI8pgP6w4q7ae9LT1c60wEKUPDebu8W5OKEkVqq3QrxVp5iTxqiTe+jbF2ofPz8wiwwpsBteK0twV2AgJnUPs7WkpO39vB0V5sqn3qS7dGXS3clyaKCX7St4d5fobRD/q5XlRYEHn5xJlboOM9NTzDOfFBeukRO1sbGOLaRNjK49TPEEdAqQNSfCkQBUohJ+8RgfG8aFWXueIal6wQ8ld3LIEB3oo8X34BQFSP24nfgmM9OT9Pnc2yUTAe7OVkrsrbEZmhyWUyPSkqMwDAtwMe4pEjBhWG+xoEHoijE/rsnI7Q/cCx+WNv0FclvJ6L/QtQXlb6iVDA5GZNJ40/KC1Lu8nM3rpA7OAMOelqVUFkQj0qzRx/bW+vBzc6UwzM+e2TCWkRYnxxTQYiDJ/q9Jun9BtdDryTqSuGCjFghZPtGsmtBVvxR2+JZaT8dPIFahmjgziU4M2HBJ+n6bhivsHWhE25LxH8i6nn4BybhrMJiNWH3fQDkJp9jtpC8HjPnjd5d3kwEGI637+Px/AD3qVzDsKj6MEB8jdVGKQRehra3MemlA3Yjtc3CHEsjSeNvbWjE1k9T5FdUwZvcNzZB8y9PwMRINemScMsf6Wp/QnP7VnPc77jbW10jSCYk7+9LE4uJ8YuwtpngGQDL19mF7a5PpfIVm/a31C/OETLPmx48qKHaQrZHqWg28g9kTPkW7qNOPS8M6nCUsVGhpqhvoZnbcmiKvl9ieetKelhJC8RLdHC3m52aUn4HwmwK17YD7e7twm5OznQlRERgbGYKLQvQqlStMHh0dZgrjAcCQq9nZroGuzvraKmk3znVHnj5ib3YJxV3pWeEOptLTgsxwNad5sLKyREiGRFoD/tjV8YZInjxtqBkZHoDT1d/XBbAf4HSWMKGx7tG71ZXDQ3qWfWJ8FLeHAexR0ovFJH+qza6E7WKOq7mJbqyvRX+JD1Mmsa9Y0JjuCngewzDAjbu7OwSAKR7OopMPrhTGatruJi3AMPinKj489JcUJ4K1ZQgmx2L5jfAAx6cVVDWsXorHkiK83B1NE8M9n1Wkwq8+bpa4PsYzvw6DCobQpZ4KeCls1mgutThCRtjEHFk1fVFpEo98k7/Aqv6meNKom1a67/wyoqsoTyFIxoI2Wsz6kA+QWgZNiHio0auTSzMkh/6CW3ecaB/N2Hb/Is0hFB9e3rg6mqWt3nBbOye1yau4gmGf/ThZR72q1BRFo3ag0Z5k5J8o1sf6fa0fEbxLtMQNW4xkeDqrUsgglUZUtorV6Pncp23vJw04fBGpxH6V8nNTWzVYQ/FQZoLkYMNWCmxleZE4C+OsnWXLytlofd3MNBnDRlhzZ0ALKUDB0tH2WpN3xPDfyxRTOCSvZHLdFNd5zvo496SPVoYTIQolooUQ94pzydFBUn50xLm+QaiJTNJmVWUpWW15qWq7UjjJpNMvMuyiXGIAq37ejhhgBLiaiaRcze7Sm0R7PSE2TIPDeH+fFiyJiaS7VgDTnnWsxgvATie+ibOdKWB7Py8HQGspCZFwAiFlvxnoAf9bW6PMpJHQIGHx9bRfX19TA/9srK9hJUk4Szm3bAfuysOweqErT1YNg+NSuYnj46NPJQybD6Cf0uPXlE7bNFPadH2/zT6LHRsZQuZsUmRVLAx9Wk7DMFQTK0c2Yo1lKfDDvaxwcln9vOyn304yPTk+hfiWQUu5OLrbfi7m5JbJjJ0WismCMpCfUu3UPOPIeHFz4Zusl2lLGpEAWjz8uHoqEB9zXMi9TIF4QI/Y9YfilP6NSiPZq7iCYZ/LQPb2X6Jmrbi6T7AFe2vSCS2pA722CcGAUkiJr//3NTnxg1T4ZZq5s14qPnz8jrZhUVe1/9ygO8R+iRugmvOV+Z/ceb+DbqCvm6QdQ4Oq+wYhKUlOiGDmsn29nept+nF1BanYYJmQhjrF7+bhoX6NUB8V4GGKnvEjkr1e9t/b2931kAJRKzNdR57e2rM4eaGODuH6szj4Lyuz69gaS0neTDQhIVNUT91kfGyY6KPs7VET3gDngvxcyWXa2FCtF1qQKyTXgqUo37kDfG4G1RNQ/UH3Rb4/6qDryciJcDQxunZWn1AjQbRG4GDHRqmO8+GBHmuGAy9PqhcCUBD+xQsgHPgXPoN/Jp8EUIelPs4LLK1BTntFWZEa+Ac2gYuQcJZyw+VhWH+JT2aoDQGu1ZWlks9q7HVR3i2oX0jpFMCMs6y31oZD/jw+iqtbthaf3M+OALjFhGGoJoZbxcpTC1NDrEx1eUi40mB+cuhTf2KH/5Y2ChO/P/7q0SKtXYxcTFVNwaxm0dXR0X/mMEd2NIc6wSj1v+9xlYY+NyB7WQil9x/w5MEYl7xrU6qb/8ULK1WqFdP2pwQ53lprslPuCoZdxWct5v1l1MTb2trExgOqSWzkO1o/HNEOonRTE1oxmlzz/jAS3sDUFJWGjJBhE3f53q9L9gc1thsAa7u+Jt2NHxJzOsDDKQpyT5mrePrDSBj/CL2WNGqBRZ/I/T1SRijME6rMrVMTbzPdxtTDYACr4qTKBLTJWGLk5MS5JHuABERePCo8QGPz/ScbkoE/VMeLRiZZjtP6gXJFQh3d6aXxbmbSJhMlWhRDDA2Jkjs56h0aYONbId5MoQ4cD8qKzzMpVrwzA72kIBZ+U1BbU4nF37nG4cGBTJxDJ9LHCpE2ezJGK8NxOQKv/4IwT342ob/nVoiAmGs/rCjZfje/Od7S9lBobY6h13VzE0ivP/FyMAxxN/WyN3TjG3jaG9pb34AfHKz1/NwtPZ0tbC1p42w/b0dCAjwbcqTEYH83Na7d4sIcLtZZml5/kOAoR0qEX2N9LYj5gRJL7s9CwLN0xgl5/SlhCMNnsJpi+4+J9/rZrxsGG3aBtzTVyUsKevEwXQ6G4aWpIi0r3t/SRAdGqb+3/ae+7058QoHbzq9oXi6Yw5Vdp1u82n8UYTDlhaB3hbTKPGAw9h3UgMEwuwFhsP/Q2P7DDhA0iy22VKo7nprsG6ezIMhD5gMv78zvtp8SROn8/5ClquTDqJbvdV/BsKu4+ATPrGROIM2PNfewRhWkf6Y0BreeamvPIfXXUrvU2SBmlN2/pH5zsOJHTActN6RS/pGI3eNi48G4xnaDMCQH/4hbxQ93OMAzXQm3YfMxg1LyREuXqKggkzRlKUmOAYO5OpiTvNPd2WpxkbPVydHRUXFhFsFUeD2N9SrYMhsb68TNycHGaENdcXBFr3xZPROwLpcYGxkiQh0PU70lvZlnYVjv/ZsYhtnzDM8TkGSqGl4kz75/r+Aslj48PPR2t7Xh4tAlTI5mogsAY+rMJklLYQiGGevUZQokfZnrz+P9XU0xogCkV1ZaqLxdjRMEZQr3A+Kys7rRVBAyWOqPkUyCv6WzjX5BJD/jJu9RsnNbvldfsaC9wKs1zxN+bsp0hx+as9w7i/xaCgP8XEyJOjzxxVYYXR1v5DTrt7Y4zzFLW9qMMAwrjXU4C8Ng58lJUzJP8bmZpgzGd6u49xucXrsz01OUfbOZrq+bVXZCQN0ZDNZwP7n2bqLA1RLr1GvK0e794jDJcop4wkB9RQ2NpDQk34CsRuX88lYdLXXb/SvcuHNElgPWoCkFdsCQpMsdK+yvcylKH68gBiC1V1/S3ktc0QvbDzXgkT0f+UfEt/oQQrSHPNxgMCyEfCiY8AqGfVqD9D69y9fkauG5g3mDPb+qed9h6smyQTNANNgNrPhFcEyTBFYzNbzyiRtsZZSYfHG0J3kKHw+oLWHGkfNrhnSILadwAQAFMsXhovOhZqea/Hgu0dneQrLJ0hKFZwbJqQf6upCP3Qz0UAODra4upyZFyckh9nS1QyZ9dHR4JA2RSHR4oKDux5TxqOEu6nj+vbBJzdT2/y6nV8K71RWsCWFhqhviZXWCiIhpcjCsvTjYXArDIIdW2D63v79PGIm+nvbwq9rHQXr80lNiGKD3kIh/wIYUnli5KC+9w7xAfCv9wYEerjsDX8FeWHZWeisNMQDDhh/cIgKJQX6uGhy9vT0dTAzmYW/4NNOd4JnuIvRvS65nf4lg4K5PX4kA/wX+RbKERUiNEH7olbp1Dd7zvRdjjyucBLtubW6cj6BoP72UhEg1qmGkFqq4N6zEJ1JgjmGhnbX+3Nx7dhp8/zH4f7hK1eN42lDDGCS6lqY6dzNuyVETG+8nPyqKc7Q1xCzW6ofc26cPJyVDf8VJ7+czHkip688YSOCfWECpa/Tn11jzzyHNWM2hxTNWhBo7BCYXsfsXEXuW08uFKBN2/zJ7m7sLJ10nNHeXfRc9+zgYlwx9i1tJ8BQ+FNDnRLT3KR3aVzDsA4FhtbIhfl3DayaCvNrTr19JpVnO+1rmuhC80fXzmixD4ZmqKRPayURJWQnA0qQho7KkqM8Y4BDVb+bJrTdvIUyd2Tt4AOGmtd7fOL9PT0RPJXb9HGqK00Ksr73DOgE4Wd/dOdXkdnh4CCkmMzVPS47a3OQ2QQDgqqHuEZYIP53l67k7Wbo7WwEI8Rc4whIS4O7n7Xj7ln9yQkRWegIsBbnC16+a6murmV+ED+Rlp66uaKLPGF++9h/mKo2DpcaR2rWxTkmsm3x7WKdwqS6ab0V5ZJ1VpACMRJiZgNMuKE64uDCH6ypEJxAH8cKGPH5tjarInRwfA+Tb3trc29slbs6YNgk4XO4aebhYj41ya5LBjETIdz3sjLabEyS9mU+zfQkMq6ku1+Dofdb4hOyqpdn1jISb/WU3MdaikNgdhLiY8EbJ0lcseJrh5u9ijK26ML5SSE47OjrCGiRkYkKNalh/Xxe154pJiYLbAMOkxRmAfJqlcX76Yq+P4pbDvxzfIzPTk+QRh2Xrc5OCnlWkylXDaorjnO2MrKUg/GnDY46576Fk7D+lAnrf+vTO8Ws4yJsRnxY2768pM+plt5rFZUMhNOtPgxgMwUId2iB0lwsDf6+b5iIO/73WsywSW3W09wMuwS1Fa3JCfzmR6sxf5/4YP16VTDswSos/fSHf6isYdhU0G63rZzSfH+OHkXb8Z6n8nkw7jfyj1pt3x/5DBiw/1vjMD3q+s/GCRK4A/4qybXiIKJTTYHpccnJUBEBF9mE1lwsAKqM8xJCg0zlxNIucT/DKp0y1dH2Y6vMPGdLzYrE4Iy2OmZTHRAYBfuC08qXFeaZBsAaXWyECDXDbDmco4cqx73P6HtHKszTV9XQwPnydfKog1p520pYa7WeNC2Le7rZ7u6fUoplKhsH+bhc8iLnZadz9AniV/BGwVm1NJdmKMDkaluLCLLimAg++l5sN7BVgicgw37BgL0DCcD6Z2SrDRJuDcOL6+houE5ka66SE8FHXXEdauLclPg/OdiYrK5oUByJa5Ahq2phMjfYO3Q9mCboULsOlvsIQHunIgh1WWBDb3t6SE/mcmhzjuvO93R3YFgwRKTPdeosFcugx0NUEA0K+lb4a6/9MxVKc7IX7s1wdF0UikZ+XAw3XTXVyEwPlYNjT8pRSqUwiXA5byxucz/bhFKXB0PeN965/+0EEpOmkxavnv6NSIaup1TXxhB43K5eVDFoHf06g4aPYfk7ZfKkUA5PDQoQE1PsbGm7HUBIblVTnP+m/4FS+Uwmjdkck7VLplKFvq6PxNml8qkbH6ZRewbCrUDTb0UtTb1WqRHAe78uUB3zv1zVcQaI3sUS7GyuBARqJ3TfUK6r9vyHhWg2vvJN63AMeVtn3CVftPI+vzSf0A4Ij6QXpz1IdYn/G7Ysj36FOixLhkBUhbZ/CzW+R7aQss9ElOYF2rrtblMNMNEtL8jhhMMh+hgZ6XR0ttIHBNElQxFMS8LpVrH8lVngmFxfmPFysZX5Eum3FQcihmFkQ682oTPHE2oB2PIPV1VO1O2w+hh202GhUKo/urjZi1gwbKsxLj4u+CciKSZzjtHi58nB5DXvKsb/uDTJPcGPDa68LAyQDWetNcW52hpjpFxHqq9m7v7Ojlexzemrc2vyEXE2J69KL3bpkMvFwHoj4JJ6YwORDGNjM0xXg46zSpPtsYCcG2JC99Y3mLPdTMKzIu6PQW+BohM+bq4P55ZkI4/kfFIeSDydIf2/3r6gh9dbR3uLEN8EXy9pMtyAlWA6GPStPLZDKJGLeLOf5HSINrz3nz09XMJ2ClRDvLxirmZSBAZpH+08Nk9wOZ1AmdjCKGvXZQynUtf5lGgjBr5cRIkREJHAUbfqbyHFbs7FVT+UhajSz0C2C0oRnPkiTXgJXMOzzG+TJ2/U1tpM97GO/n7qZ+35bWyYPAEj6fkcrLocKMt2PZE+H30OUcc3G2l0ZEvua+tM/4mPJ4J/T/MlDLp0YR4u0XshiKIcvEokI5TNDhHs5+q9auj5ErNzd2WpnZ/vk5ASLAZJFufnV2VheWrh9y58o7+FUtbK8BOABYKfO9paXzY11T6pqayqrHtzLzUrOz0mLjQqJiwqJiQzydOFhIW/InOys9QFL3Arxjr0dfBYtwPqbmxo0MImI9ZGn7Th97/WrJnyAlqa6Xg5GG8/jJR0MamJPxpN0bwzD5NrDIP8mAEmYoo6IKKSJ42PDzxqfwGWCNfgLnIjDNSzs4ZZUq13XzFiHCLvDqX79sgnGwIPyEnKS37S+YLNX8C13J0vcMufnYrr1MvG4AzlZm8oYiY+rKzQ7bqsf3ieNYR1vXiz2P+tmTUFUuHRLIRApQwF+ZrZBikRU6rC/v0fOOaJuOlsRpu7C/CxL1m7Lq+fSS/CJg/WNF9lnYZiXlwMFXwHw7+xoscayt4voqbBMv52Ee1M6ohx9veyzMxIH+rphpLW+bp6defs+xQOn+TTxTK23baVsPMOAzzlTDWuqSEu57W0hrdlmCuM53o27kt7flE2ofaYFLVm9SY9QVQoTPVDl5K+0ZcCDJKy+yPA50HBaL95p4yTIKX2hF6LkgXpZ/4vWbYFwbDfRBEh0g/wEqhCerGl+Q0cLksUYzt2Pov1TvWoj39EiLL+CYZ+/x80JclLHY0uzhlQ4luK1Xq1aTqHtCEW7WjxXRzNUcV8r1EQpZZlYE6r90CdnAzEAzbh9lxTcO39cdDDH9lsHY0hNGIvJHowqm+vChOyeX9OScv3Y6BDJKetrq7PSE5jJ+oOyYk5rg88725ky15CRFgdpHNv5xN0dyH0nxkdWV5YhqV1eWpQmvvvJCRGQ5RcXZhGTKExN1MBUYv83qYIqF7Y6gFWisWFseK0y2UPSw1Cu70ofLA+DnA+3hz2peUCwCimjce2VmpwYfdrwGCBraJAnZiGyA1rXASgCNDI30QHEhRcLs0+CAjziooJK72Q0VhfHhAngk3DVSI/TLmAqmU93+E0fNvUBgMSUgKTRteY8f8lwDvxrZPAx4V5eRIZEAXzeWPd2s8ErjwgLQPMVTzMvgsHwMnDX526MPW7KAggqp42xsrI0NIBc5gCZMM9zVEQAQOKSOzkAsCPDfNnY/j5/VkdIic9PV8N6igUtuZ5ONnrW0qIcZ2DAMn/b2hzo78nJTILT6OtpL3DnKxlUgEgDfJzPM/fTerzLo7sAjt+psYKR4QGKlGiikxTpBTCMqZfYWJbi72FtaYpg2COuNfb9YWoekCsb4jMZq7kMWYtfQvaM2ojdNlppDF6+H0Js1tL1qPGP2PZ6bDWieUC1+wmXE+i2BQx6Nyo/oMGwP0gnyVrVO7iCYZ/fQIPsW5K3VlpprxLtSQb+hBJg3WY1G43eTwuhqA+SJaY62aRLQHAUWo15P/pufFeg8ZOFVDqoleepvxpAvBSa+go3aaPDGUnPr8s6xNI4fJEQGpWj05F/UJ8PwC6jdbYzIXIOzPSrrLSQ/XrW1lbl5BD9BU6Q6mlqP3FNYGJ8lNidwW6fJwfPISZ0qauw/YzT98gUOwCPohgXSW+GnFBHdrgjlq0nuvzra+9IKczBxkjlzovFYvhKR3tLQmyYatCFEZeJLp9nHB4WEBkedL8k70n1/e62l4O97VNjAwNdr9uaH9/Pje979Whtqmtv4sXReINksqok0dvUSAdwF2mFgu3eDPQga+7tUc2uiZWKjgDqc7czXG+KF3enx/jziDjHy2YNm3AwhVsS4yOPD/eHym+SalJ/iQ8T2LBf4ItlcQ4WMpUOplL8u9UVrMyBQfWd/AxmvZe5sKkftr5+biNjPz5Jc+lj7C38fCfSjmg21tdWaeqkLS7MLczPwtXMzUrGpUtOC99Kr7mpnsi9XOKk5wHyvZ00RMwotaJKqiZqbX7diW/8qiavra6g6YGw4X4SgLGGMiST6MAzwDMmA30cTY0Wb8tMpfmf94wIQBeWesZdfJx0j+ASv7VBJl0q2XQ7rUi3kLw3Nc6vUSO26mg4NPqvrBLCkzVK1gXJLCeplVMFn+q2mjTS7mQ61zicPmUZN/SXmudJXsGwq9B6HC9Jev5/im7Hpklsq4GubsGjik0cLdCS64iwq5VXqBjrpeJWKKr4o+met70+WoFHuVOK+Hz2gmif9gnhygBcjJKd/P/FQR8W+Qf8JiXWp6SmSlSn+n9XS04G6amx8pQ/i08g0WS/hqantaR+gisJgFJ2tUOmystOIRsaHbmwJfdSNHV6F25x+t7qyrKXK08q06fr5WC81ZxwSqijN+N5rh/AG6y5h7/CFPdTSdJrfd0cGuRF2lrkFlRI4RlZW+jZ2ZikJsc8b2oYGuiZnZ5YWpjdxrw4uPOO9kXbi6KVYdHMa8l4jWS4VNJfKOnJlfRmIf2MTiEs+6+SfJ0pGt7M9BTZOmya6S6gnBeHO50wIn2e5y8ZyFqqj7GVWTZD+g5gUrNjoFHWh8az+CQ/J3VtbqSv2Af3d3UUeFXEOz5Oce45H4nBxwBxdSuCYUzfZOYtEBd9E/8xKz1BIqUmkskLuQUQrEoaIUA1wqisTnLqKxHI6XMQKMh05b5IPH5UYcczYFlHVbJ4OFtxxirvO4oLs/DOuzpa7G5vik6OjvZ3dtYX1+dHF8faa0ozcGMYKghvctFU2O2k2gfgvaMdMdtPTYi2JQN/xJhsLeT2ddkcqHhTKc8cQNfAH8oe1yEfxIG/K0B8VAqK27OdlGdS9Qb/lPuUsSP9daQtmf1hDYb1ilNlOk6yK1cw7Co+rCBaPcN/r7pyLT6k2XHDf8eWLY3A2xcpedNjbUxzymDYXruk/Yu0iqvGX1or6RQ5ZOBP1F/JzktqJZ1fQRiVfcCHu3+ZAlScHDOJCMfwX5/rAQ17RVSnxFppnW+sr5HDYPHRISyFB1aWFx+UFcuJFmg1USM1NxcHMw3k99tN1C2A7jJuQdQIIWnuKwtF2IbRHvZISLWHebrwkOjBwQEpMYUFK2sInJwYLSrIPJvlowqJlaGVuV66MBHwJ6BcyClhvdSddnwg3l6SrAxJ5lokEzWSwWJJdxZChu3SRc5jWlaym6+NsrWk8BKzdCmnk/m0QVklNiLUF7fJ2VvrvXsaK+nOyI5wxAKJWKdRDREL5XH7lj+Rqu9uf7XY19gtBVfNWe6+TkaGBtdSAq3kzLiYzMOGdNecMFuF1bDMUBtMSmSyXglqgqXpad3x8RGAMVINsza/DsiNKN1j4E16yRRGW+tLIlhfmeDEFBcZYHg3s6ytqXjGb25kCuNV4itcp3WwMfL1snd1MPfxtIOT7Otpr5Cj+OrFU8mnJAAwR4b5Mdzw5EVxKu4X4fGfzrVXk1DZPy2MxLdWiDK3p4WHM+lhxg9SDl5VYsTikbaTifv/UNkLbvcNjcGG/vKDOJ9LsfRRj/4zW3ohJD+kbIhtctjHWhESuGYamn0g1sxMiEiaUBDTivcZy8qvYNjnL8isCUuD4LHvyZrKXFg/QGUS+dO26vdWsZgEOp6wpW/Oi2zrvJjzvnBlT0zJVKqhXDJtS78hODVxwUuLsp7MOBdgv7VGykvLyVoaZZAlM9NuyD5ZwhvAb458Y2YRLC87ZWNDYyU7yHen30729nQAruvpaofkD/LXxDhKZD8+OvTimxDv9sj0pv8XV6dLSHAhW8UGYknBfEkXsz1MOFUdSYhnr182vZ2aICcqPyf1nJRxv7Qk342hMAkJIsAbX4FLUWHO8GDvwf7e9ibqwBYd7p1szkpWhyXzrZLxRwh0dWWei7jQ3xXBsC7hdM1tviVlcQZjgDgRb6yvMXmJSjqU4LjseYZwmIBGXhYESPoy918mOdnoY1qdlytP46Wwd6srZNQBXJke659uyu4uQqWkYDcTYyMdF1t9OflBuvOqSHAv2t6Rp2dqrFMe5yAnrtgrdQ+TnhBKynJzcx3uDmJ8B39fWVroanuJjw5+hR2wt76REmR908OUicSUC9tg+2aslCjfG1aEjsJS1p8GA/4i9/XE+MhZ1z4sgRMZ5hsVHlByJ6e7801Pd3t3V9uL5w3LS4sA72Fg414+uPsUci8Bz1Q9uDczPfnhv0KZYrCBPs7ELo9EfEwYPtV9vVzsoVB/+F9Kp95+RPvCA5qwIyMllDkNa5bSxpvYTJWTxtV6Oa3gtT987sdEO+gNSIHe/6vsk5cWK2ky0t2PIUYlS3rk/gDdMQUjZ/TfkTMN2y1mMNQ4fhylHPtDau78Zg2q3U0anSsfrV5sVJ2iSkIWqiWNlisYdhUXSy2XOKjoiLYpG4rOr7K6YeAzHV+i7oF1dtJkJ5uUmzB3XhY+HPHY98V9v6vYGZl5KIcLkr6v07foRpWmz6yIVg3i5Ld4KqmUiSPDS4tTkJoV1yr82l2Zzcj/VOZDrWWHNyZIYDMrvLe3SyQWiVJfR9vri+8J5H8T46M1VWUpCZGQQZ7NAp3tTOx4BvBDbmby0dGFTwu8ySjHlS9I1ku5fhv2E9ep3OwMd5oTaCvnjrTNpnhAI9gT1tvNhqAaOCJIeRXMh0yOERs3SNAhEU+Ii0CZ8SJ144uP9yUbU5KFNsnYA0lfDoAoxTWuMxjsuEXqbNahAKGJ2tNu+1AWZ3C2mYJ4pGKD8ldfxdM6hwcH4TcFGIgmBNpIejJO2tPuxbtZygpK+TlpGh+rzOqrt6fj/OTAcFkQRlAImJnqBroa9xULuhVxEV9ke9haohqambFuRihPrmIGqKwqyYn0ZUWG+YlEJ0uL83wrPVz18nHjve2s7XsQ5cY3MJEyTqN9LOqFrsOlvoADb3maESQmcOfv7p6bmWHvOFghIMamTDfYDdg0QEQskyiQySTeCvEmwJhTzM68FSZHB/m5MmdJqDPmblv7uJJJQFU+0aDEdsLOWv9hxV2Nlzo1G8w2QkCPcnJBY6NDWF/Uy80GBjOXyYA8Gj9oNfZ60XuN5Tys4hfj3inR8J2Xmtw9wBVdP0s7ZXFKuw/fUjrDyJh7jNUc6+D/UV+bHiElTQDa41Xa3JlT1vSuAPWYUBjsh5FaMts935Vynb5IC3RvqVuOBgQ79p+0muXCTc1hsEqkZUDMoy/SqH8Fwz7XwamvVJ2RWo3q0d2/wlZ4QyJ1bMD3zPhHrD6/nEwz6/b62D4dsGw3PBP3OBoZkefjwB+q/vDuG6R2RVWN/kINExhVx55As0SOFtVZw8EERfefuMH5VTf0V+pwVMisKqcapsYhrOgkOjKYJCurK8vKkzzCCiPmznOz0xfZgcXF+cePKoQpMUSB8LwFYAzBZuWldy6c5fTR7yTuulsry4tYGdLCVDf1pp24PZWqO7WnnrxJDfEwJ+wysgBuOZtb7+xs2/MMLUx0AR54e9rnZae2tyJ2q+hoX7TxVjL/SjJaIenKIlWsnReJuy8SFSKrU0t3eufdYC9H49s+Vgt10TRKZHwgI8wey8r7eNodHtKkoL3dXdLvByc8S5hw9vBfvXiGP2Bk8HF9po+kL3P7eYK9tR5BMhpB5qdmjU5OiFomwJjhwd7FkRYAMLC8zvVwstEzNLh208NUYSls8K5PQSQfXxFAYvdj5athfcU+T1JdeNKWMyITCogFjzc4KB8nY0xrrE5yvuVl9jjFBdaAO7uG7vmURNmRyw1fmZ87tyyQHB8hO4RPHHl6Mb4WsMKWXM+RUl84CgdrPVyOS0mMVOMUDQ32eTAaNZnLnfwMhS5kAL/PCjziUSonnXp2Ya+D+l5iZWUJl6xxbVauYbW0JB//FzzQOCBeND0qE+sDhKPVwO447V8Qbz5T97WYzChQOGkGilDjZoVSmsWS/VyrgmP/ISOwBCv72Go2lZ+gyWXOM2XIvwve5oN/ii7ZWxsN1GeIJBg6n86oq5wl5sSdC4hM+AuSddaanOIDpIBPttj5VfVNho7mUGsDs2Clxsy7wkDmq1+g+//XiiWf0biCYeelzqMaeLsfLEvG/l1K/0vU1n4eL9Pufl0/x6FiM+fH6mlFp8nutGIES4YAYT+i2Sb20tJi+gHR8eOsLIZXUhnTcq0aPsMIC/0FfSDqwbylOPHQ36GWIc6P+xbG2+I+lwE8LlN/4mrTrKHTdoLmFxsbaklqpaT3o762mimzBuAhITZsoL9nbGRooK/77dT4+Njw8FD/xPgI/LCxsb61uaGkZrW+9u7F8wZIN+Wm7SENlYqtX4dUm295w85KsSqdq6MFV1uzMyd/TFYNk74dOTFqqEwuD2tFGBtde5HvTyvX96TXZgj09T4ixlx4edb4RMEc5f5ehjDhZcPD2eH2lbE374afbw48kYxVoP4uOZ5hl7ClKMiFb+DvYrqJ/MqUIrFOYWwAz1D/Y4BJ3fdCTnWvyWBYUYwL7mEDPLm4MCdhWBUDGmTuOSp9MJL1ra3NID9XXApLDLI9eoOU+jea4h15+viQA31dNF4qWVqcJyDc2dFKLBLNvCgkGoMPE50A0mCaHwAkwEs9MjMx+EuD0NXRWg/r+Hs7GHYUenUXnaYsFgte5Xg48PTw/mOBx+WlRdw3JYVhRp1SMUZYW590YZbaYP18S4qGCvfF8tKCQmADQYi1pLUMVu5hbwhALivUhgwYpqM6hyxxqP88yJSaeHtifJS5MyKRCIm5KLpD8QfghCukNTKnYGD0fsgJAu5dxNaIchRZYXK0Os4ci5G0lZlWCXKQ22A6TMcPHaw+UW8d4jGZdefIdzVpsXW8hFZIqh+c3lzwap40kLUP2Sjbq/lAmuCD2qi47r8YATDaPviH1ZyfJVAKEB259OxlnwH/ELza9fMcLOa2X5ySfQcUrbZRLUAjpowKngrn1AN/bqmgjhIbw835ez2Sz25cwTBFsVGNyj4XxvQHyw/hSScVQvhVLjiEU7a7g7ABuQcG/oCt8B3yiPwNasKJzU0Iqx3+W1q9h9XD6pDyp0KFID01Z9r6fks1Q3qvi6Gyqq/JmTmyftL/ytUB7KIhPur/W0k7pmf8JjdBSOK4MvJP7+MuQldhdWXFxd6MkoNTVPqAlPpOfsbZ2hSmbClc3BwtXB3MQwLcw2/6JMbeykpPiI4IjIu+mZEWB3lPVWUpUZ/HSIayujK7bm99Q+BodMvTLMLLzImnZ38ODMPSApXlJReYgFlHtzw1gL+hhvcloBFvd1vYE1Oja4BGaK5ge9r+66T7ie4utgYksYYToiRnPZp9IxksQGCpS6igciUzJYuTIisLU93Jqghl1MSOtJ0XiZDcW5joAo5F4hmKqmF5t50wDCPVRQLDFuZnMdBiOsuRvcWsVJ45gsrTNZEIf3YKS+PdiDhHRZnme2b6+7qYgPb4cH+wLFhOArGvRNCWj/QSs8NsSOcV4LGX2e6OPFSpMzPWTfC3lCuF4c+05nm62Rngal6j1CkLri+MYSzEEuJO1dkAicGHMRiDf9GviPGI1o9hmIezFfFhOxtREQFnRzJsFEl9mNGg/fYtfzVO0cvmxlO9hSY6PMZEANywcPeNjXDoKunt6SDQF243Py8HuT2HO1phke0DiZSESIop6sGXuyhEvKSjvYXDGuEpTRJZrQZp9h78E/XflaP/Jpv3fKXRffs+/SpfSec4myLTpx39d2XIav0+o+vshjo7SRwFsHI6qtepexoBscALgqxtKZbD/DvBP10/x9YEWXyEXBBgnwnqY7/Fs7FZc0o5Y9oOcQhFmpg9WU5CttHELI77POYVDPv0B7b9gcHKshXqvDG/WkQl0LDArautya1xmpWHKsKsNQY2H1FjnaUL3n4/UtGlurDYmfqtZtI7xt6iilnig4eUysmq41W6LA6oUrMdojgQx1L2yOPiyasBoD3jS59DlqVL6pt7aJIMax+zbx3UdBBVbkgit0/nK5C+nNW119RibXYdy9O58g38nI3j/S3zI/hpwdahHqZO0tIE73SVDD6Mk12SGq6vr6l/2GvFaA5i+G/UnsbDBTHYMdj/HaZyPfwwmJ0V7mAqc9BSYQMwVa+616tTmBzMx8Bpuua2MhjWmfauEV2yGzd+kBrCF3UoEuroSa9M8SS7F+LvJrdHgMSYMNvZzrS7E5kr7O7uYC1H2JMoHyvceyZ+k+rnYkqUBocHz2U4A8ybm51WLieoMMhEAJzthfmZ1fGO7iLBWTH6CG9zU2NdY0OdcC8z+EC3TCMx1s/SzFiHb3mjNtWlr0SBlGL/XQF8BR9CRCgSM9jb28X9UXCW8sP5g/cA5vl03vEGwNac5V6b5tIgdIVfAfu1F3h52lMQLiTAXQnJLToikImUHB3MzMyvW52umqLqjZNlXnbq+BiHesvi4jyBSbBmgE/+XoigaG50zczwY2szXXLLNNbXzExPMS8BkrU9OVG424SE7O1uOzjQw3QhpzyswwM+2JoY6XI8C2ux3x2cJdbWF2I000cy2hWh1mbGjiXz/oxW6gs482Kqy+CfcpUgUhbbz+lWpdF/4fz1+SBK6O+82pRo92Q+WtIly2HaviDZ5e7cgKg3sp3s+KkLKamg7ix9mnTHHnbCdSSMoa6vobIBK8i3eIqICCjuIoqIe720uzQkGJDpaSpIGwh2V1fiu3MFwz7LsfkYVcNwWXzzkfrr2WlhsG+/rD77VmXA7UT8B+GWOJxi+0XMEYdl7S7bI8L3HpyfTXZ8hoUQmp3FXtaWGB/Dy4kNl2/Wk7Y401LhcfivZf3T//tSUc1uJwNjczQ2WbgpK4j94/u6mYgCOyw426ZeZytLclURjSxSIQpUAfC0Nyy6bVeT4pwRauPrZAS5LCRGkArD/2K4hatkuPEGPpwSZA25MrPt6sFFCmLoLXtwkW8DZPUXOGJ6Xm2GQNKdQQiEI5W37Kxu4GpYaJCXiv6Ttw0qSIbtaSdtqRECK1Oja3BmFhW2ezE+vP8qqaf0ZmO272pjrOJPdgoBqjlY62EpEXueYX9vl/zNNNTPvPqOfOPW1897u1GFxNRIJ8aft/VS2qXWKVx/FufGp+Ql4qJClJBR4Tww5UBYxsvmp1idBWn9OVhura9M1qfKidG/yvGI9DbH0B3vyctsD1kJS/A61/NxivOzDLeeIgXNY91F3gCo/JyNcEnq4YN7KCM6Ogz0deFZ6CWFOvdISYxxfpbejoZufAN7a6pGdNPdFDDY8yx3uNZ4xMZEBim51kSYFHkK8wwXXr+a7WyPC/K0PoPEYIGTz75tickjtTDWKb1za22heqCroLo8OjHGw8XeGMAYKY7BFQS4WJgn7Ovt3FA6kVH3pIo0jsLTYGNjvb62Sm4/fT3tax9XSj68mBgfUQjDRKITMrDZyiQiRatv0Jx/7XnmwmuLgL23lhda1ckW4v9rhH5GpfXddJsTnAQ11gz51dj3UCf2ec+HCT16Wpzhjn2y0yc6Yschmg+g2eZomr78AsfbJxn4YzqjYN9OcrJB85IQQWaEHbZJpJU80NW3Rr2IaoP5pXh6bSP/xIEPqXo/kxjKjT/JzarnCoZ91mIlXWas9Kvql0TFhzThG89bnGiNZbFWQm9o7Pus9/CAegGgyi+7rui1YroudMLOmHLsv2QW9Tpsdwy9MGTs7aFvqy6IwTPlLQ9hUe1NJcJzk9Topm0vbygiD+g/o/jfXNUa4QLhghiqRj56L3fS+NgwSaqKCqg5s/39PcjVyN9dHMzgv+7fK3jW+KSjvQXSl86O1o62129aXlRXlt6/m5+VnpCSGAm5XXFhVnZGUrC/21nWIuArWCD7BzQV62dZGMlHqa2DIdZOsJby3OADFia6uG3GyUbfw94wK8ymId21o9BrpNS3LM6BZ3GdpK2wiaanterJymkkcFYKMCw+wIYCPO2o1hTnzzOVUf6UVIdw9DQWvyoIOCV8f3bpyajLFOjp/SAh0PawNeVcTzAKiSHaIaILnofu2lNFbalhXrRnsUJf6bnZaabzr7OdSYCPM1wXR57ezOMo2CW8Y2WJ7oTf+KiqTOMnOSzYi6CX5qdPdteXeqSVrr5iAQCwvhIklhjoYmxqrMOT+pjBHmaH2TARF/wMH+s9x9kZ/v48k4ZStTWVI8MDkWF+gDy9PRy66ws7C71gxML60SiV9i5aSYcr/CXKx6ImxZlokxTknvtwW11d9nKzwSxcfeOPirxdJBlZksI7HTERVozCL1ngXmN/igiusDTR8XQxW5gqX1+s3lp5vL36ZHftyWhfUXiIA/wXgDErVFK+zgR7dY8fwvEqvIlePG84O0HzpOaBXFnM3cnyA6yJPW2gfBHhtDMlOgB54jqnE98EsCXb1RF/zikTLe70fr/sRfwxNwcUbcfxO8nov9ITr3tasYg87vl9Gob1/BqZrhWf7Kqu6R2vSJXDvkDrN6rR6c3MJch7uetnOGDO41VEuSTlLJYT7rOetGQUHMKs14U0RYgdEfYmPdnQ2BU6WUNSc0Sy/90dyecjrmDYuckvQ2Hizy8w1MSSOQGjc8lYk/2sp7ZzgmZ3yIbYa+Zs1VEqiMPfgpWITg5VH9G4rD13zo/VJg4nJf2/K/uKD9szAMCSzDwtJ7B7SC1rkiOhYJeKqHMFyznKsKKTA4lY05cYnrbr99UkWy6Eyp7af6x5GUk2r/79fVzVwfSz3d2d4+OjvOwUkmb5C5ymJtn2vC0vLdZUl4cGeZLGEshuEZ/Q4hPIZYUhvOww27wIvo+TEU9qAAUpLOEcOvD0IJ++LTDPCLWpTnJ+nevRWeg9XOrbL82hu6UcsMoEJ1uLG0x24gXVGi8S6+trkIMijy9nEyQQD+ioJ6OtKMjanNJsAAihnIMHp8vZ0SpcYClWURBLPXqdPFoZTsnQs9GsV770ZNRmCEyNrylREIFoefWcVKLwAtijMdtXMpCFK28HLckBrhSdT+DBx8ZTGozXr5rIDthaGx4e7C92VGK2YW2qy4MEx9Qga0xWxMPM29GoIt4R4Fm3IsR1HgxrTHe1lbm9YWUOYrRVmxMU6GpiZoIGMKwfRqy7nYGLLUKnxoY6MFbvxdgTwfrzRG7W1lYxIxHAm4WZbqaHw15ykiQ9XZSS4sc3tTQ/pauZn5OqEroz42Vzoz2P2mdz42sNNQk77568m68iy9ZyzdpCddvLzMQYD09nUzhMwGOAyqwZDWnhN33qa6u6u9qYV3B0ZJCcjeR4WjiEVMnIcnG/aY1HlpASewwJcBeJ6JdOb3cHYVRyecLPoKpC19fYNvmoCUSWUCHlrZW2CCNqpi5H4qG/P+XUrKUgFBvUhRXPLbkiOQ8q1v0O4k+qHYDfBv5EVgf7JjcZEtLvPfY9BMlU471euhUQndu/Q1SviwTKZr9AawRoFsxvPpKB5F9nS7a6gmGf/Rj/geymjbvQeqYsGLo0rlrcYSK50/3LbKtbEtqEUbzITsv4ZF3SK3PrmvVi9ZXdTrrnkrX2iWjo76jpK6RzsPlBDAlyKYe+pRBPik72NQ/DLjjRSGaYlqLfyy6QmWM7a/3GukfYFYpYIS0uzrNZSU93e0ZaHN+Krp9YmqG6AeSsEd7mpbEOObdsEPqSpsuQAvJk+gTwgxvfAPLplzkePUWU2B38O3jXpyXP82m6G4CxVzmIYwYL/DE50Ip0IsFyrzj3PV49rLtgYaL7ssBfMpj1qjDASnrUeN9GhlWwQeprqwHFuWP/MVXURFQxO68O1ilE5SlY5FbSnqqYl9id3nUvhMCwmEgF7ue4QtL6upnpIgVnPiWEv9oUh/rTBrMHysPMZD1mGienzUxPMkFg1cP7x4cHQ2XBMDZSgqwtpdAddwzCYmd1IzXYWuoDJuhijcGwdXJrvieMQCuzU+RAgF756bGvy+KktS9UxYKBlxlq05Tp9ibf63GKc5inaVWSU6CrMWYzwshXeKdsb21i+zhLKQarC/SRpGdIhMKjlJRibxcmBvPzdlRYljwvDg727xbnkvkOC+Nrwb42gLvWF6uZMAwWgGHbq483l2sWpip62/PyMwN9PS2d+IamBgiPWTLupsgw3/6+rvm5meWlBRgARPadKaN/eHgITwwmXlVP3VGrQSigsbdP9evCeD57RGxDgxy/T1EAyMFqYZjxAchBW1Pr+6gdblKfQ/889aRwofO3/t+/UPP5RhU9uTz0bRXOZgpm5ioQGRIOQaUSm/gYTfoTSlHHT11Uc058RNdsKYVJTQecjf7fQ1zErYbP1R1wBcOUBowGotbCkoN73i2BeWVYQ+J8+vJFA7AKeaLNurF+PG2jCR7MB2DZtrv9goJV7T/M1jR5RSh7IvwEy7ZL0VoFzSIA/KM1oCLeZT09fDj9fqiJF4l3+ZekwXVO7OxsYyMsvpUe0zrZxd5scEC1ggVkbNkZSQwxbinQMr8e5GaSHWYjDLGGpNnL3hDXvohatyVS6za4LTCvTnJuy/ciJS/UriNNpgsi+M42+vbWej6ORg48vTg/y9e5HkP3fF5kudvIWoBw+js3996Umpqe1uIaUVwAT9KXmRhkSxh6If5uTEsuBU+vrc2IUF84G448PcWShiyXTuGrgoA7MS4Pkj32XybS6vltqSdtqQ9TPJcbYiQdQjkF/Oma2442lEo7XHSFMnpYer6r4w0Tn5ib6MB1yQp36CoPSwqyxcxGSN+ZNYeLx/b2FtailJXC9Pf3djfmhmGQtOZ5utrqM9UFYbzlhduO3vcDlM4UrFe+ALzvYsh4EM0SDPDc7Qwa7kR1lQTB1YH/Sgq0grGHV45YjsUC2FxNsrMFo1nxZXPj2QNJiguXYjBdJyv9/ohQiTBdkpomSU/P83QyNPmY0BGLC7O4UvvKS+8wr4uJ/g8eV8burtUS9CWlJtYAANtYesT4y2P4y8rsw5Heooq7EYDHvN3MLYx1TA0+AiCHKYu+XvaLCwhSEru2ID/Xvd1TPVGhQZ7MDrGjo8MP6pl6/y5lDgbHsrdH7zlphc1KT5BcBctYikGJROdXP8T8+3CSbqgDMHORPijImiD9IzLxajB3AAuxseQ5mpWMfIeGTD2/dtGWBMhgJ/QYbXX2mtejJgfISQ76CoZ9LuItj9Z+uAgLFoA+seId+rZ63Fwxm0Rku4kCSF0/ixQ1WMZeNzVxAo/CQ3YsLFIfRz737N6R49for7B8nBE3Rjgo9lqLHB6y04gwCSuHJxfLIH2Daojqvpc4WkQzTDQp9D1EWnKUHNEo0MeZ2A2dOws5PZWeEuPEN2FKGkKKDJApwts81MNU4GiECYfW0pKChSn6GZL4aB+L+7EOHYVeWGoc583w78BdXAoTJAehkhehLGLk5mKr/yDBEb6SGmyN/0gAz8HB+6HxAFCBHBT2xJVvEOPHAzzDk529tTUVpJSR4QEbGRduXKkMvfiNCobhy4KAGzd+YGjwcZiXxWDlLaomJi15xfjz9l4ny5fR2tMOXyf7uZiSSggcxfNndQBoYWF2CsGJheXF84bIMD+mzLqZsY65MVVIASSssu6nxtQAswcpNjoM/rjYWQX4p17oCmiBSUyFXwE13fI0g3GVGcoDuI5l5eUswuSWN/le7QVeuIBWleRkZ3XDXGqlHeBqfC/GAf4L1tBTLHic4lIe5wirYhIde5EUvsctLzMyHQBpvZx0J8AqrI1uZX7d0ky359ZNSUaGJDUVMNh09G0YKmQAl5UWchUvGejvIVxEtAlTXS8Xs6nhexhxAdwCPLY8U9nXkd/6PH1y6C78fXO5hlkfg18Bjy3NVC5MVbxoTC0viYgMdeRb3TAz/NhY7wfDgx2wlZI7WZi76OJgtrFxaoJ/cmKU6SWYkRb3Hrs0zwZ2VjjrIlD3+CH+e40W+hg/y7Hbwbk0dDkBCRVpqLtIBrIYTgtNI8Nr7YT4AClGYl84Mn/NXrNNcXLYQ+uTQSKxUXk1Wq9g2CXnQev0mL6ouBDDemslVYv7vJxIi4Kwn7whkoaogY1dEOt3lmy3o3laW3/gjxBfTvVj5QSptJNmVo0/qQnMHvkuWzCJcgRD2n5ee7VNTR6mJa1XKb7seWWRSITn7JkLpFnK9ndqApAbli8nqbADTy/WzyLW1yLIzcTBWo/gKEsp+dDB+kaQq0lBBP9VrgdAKViYWXJPkffLHI+0YOt7MfaAtaR+YvK6BShFdjHB361KdHLl6xO977onVe/r0uVkJslKfDqkuNHdpUJreHdnOz46FH/Y2PBaZbIHJXqhEIYpr4a1px21pgS5ox4tI4OPb3qYi9ulJMb2tM2m+INXSQrojh1pW8/jnW315Zh4AKj8BY5MtcOjo0OMEA4PD7HS99lFG7xQ2AfiIGxrpT8xNiw6ORp6EI7h+m1vc2MjHebOo7Yrae8WLK62+v4uxjm3bOWwk/IOsedZ7kVRdndu89vyvQbv0oOzr0Qg5zaGybHBbiakgAYLgFi5/ccOwlbmupbmuk3BfoC+EAYTClfiY71sjCxkOvKPH3G2XRkbGSJzH7JS2Ee56QF763Wrc1WArwCAlZeEBwissW6Hg41+TLjLSG8RE4kRPAaYbXv1yd46KqON9hU9uBdZkBW0tvDoaKdNmBRkLq3uAmicmpR/tsMhM4vnnR2tH84DlZASQ/zdmBC3urIU//1JzYOr7OmzEOIjyWo2UvBTP/HblIqwy1qqFqO0uLdzPqfMlGccL7rCzUdIjI1QRrUxFX4Fw65OAYvpkGYZxfYLrCrCyuZgX9J2WCpdiS8S07Z0UyZLSQzkBC3zhgcgx+pwWikFHvakzZU0hmCJIbsd26Pl+OErmhXhmLjOeGY5s54f6kL8BKq+JODwSD+cFb8X2HY4Re/whX3JuUaVLDXBiz3P8EFZsRLMVl9b5cg3PpW+W37i62QEC4JGUi9mXMWCnJhvdQP+nhhg2ZTp1itVt1PIGYNkN8rH3NhQB5JpW9I2ZmXg5+NiafYJj+H1fDfaHj48ct83McCK5MGwz0MDve/l8dPf1yUnC1lcmKXivX9ykhAbxtC9uFaf6aMEhilekOOzjH/Ym1Ec62JidA1OSHqYPV1YQ75hitrJuoSzj6P4lpTYCaAvckEdbY12d3fOgUaH7W9eYf4qc+FkcsUyFhfmSJ+hwNMB/rK/vsA0XM6P4HvZG1pKHQ5wkxgMGIz8cQHWyFAHIH3/XR/2Wh24GHt6dkCATclwayL8L/zbnOUOY8/W8hNSkfN2t11ZWRKJTjAzEyArhgEwXOEkvwgJQP1ggMHS0o5TU247WpmYUsxVH0+7wwPObfSvXjyTM4GwMtVte5m5vYL4hzNj9yNvOhrr/cBCZuJsbXYdfvX3tgZ4drZzTK5EtvPuyfbqY/gZfijMDjYz/Bhvpelp7dk9CZRBZSR6EREg+WCi4n6Rwt4wAs/u382/Sp2uAsWsG8P5M1CLGzqap5vBIDGbD7rY7OmOZNKIJmT2/ZZkX93n8PGKFkXCr2DY5yUWo2iW7QULMnMyN96Rf9BiXQKwCnGlWGZNUj8YRxo1+B5mqRY67UDzDFkq+y+nUDNDnV9hpfYjOe1bP2WmyRO11YCcE8nKV3PYfpEYXMBzarOW3cTSEzSx1PllZD/yHl4GXgwhzYpL2+zBwb6HsxXJpW7f8p8/v9VqoL8Hi1IwF0BN9lY3sFMW5llZSnW9PewN0oJ5gL4gr8X5q5IkGMEwgTnW3oA02svBMCvcobPpwdbqwkDX66iIQGImBulvvdB18J5Po9DVgadHSiJwFDs725d/3Z4/q2PCsLvFucrZWctLi8QYl3Rbdd8LQbCKCwwbKAsbfxiOkFhPhrgjLcbPGnAIwLDcSMdT/Eb4ufuMtoeUx0hAbGlJ3tq71cb6GmwnpVDdEcbJ8GBfWWmhHAzzcuVpQ68cBiGBYe6uNmKRaHd5iomOYAC8yfesTnIqj3csjXUAxJVx0ybS29zdzgAPIQD/DUJX5aNOJTCrTXXxdzZODbYG6AWjriCCH+Rq4mKrj6VliKwFFudYh5O4snx0dJSSEIm5iMam19rCgiUZmQiDSUthGe72pqbUab8Z6LGyvMj1zMDZZvZl4XpXSrzXcM+d/Y26k92npXfCDD/5PikSEkVEU4OP4b/kdBSVLADGXj0TWsgGSd1jBb6apAULV8zOk9y8/KipLsd7lZ1xasqS3Hqtrz8XlkfvMXq7O4oLs+CJ0d/X9eHu5VIMbXN8QVykOpl5Snt5qeFMLTf3PfJdhs+YDcJ4aoPDgT9AieVFKopXMOwqUEGJNCmN/tuF1nS8QqtosLQ/Vy82qmji3DZrwd/tZsokvue/s6s7iSRT5rJSlQHrJ6jsDIxfY4cqj9GDgEzzHIxq8kTtdaGDpcDk/+Ay8/RTtLncyRoLyGpLOz9qxxpF6WF2009VOEzRJSGKgb5ukkiF+Lsx29lPjYiejlshAuW+zLiEZSkFUUVRdp2F3v1S9MWGGNZf4hPnZ4n1HgCMpYfwRu/7D9zzHSwLnn6Wtb2xmiFToLYw0S2Lc8CCihXxjpj3iP+ruan+ki/am9YXzJOQnhpL3xOKwFh3VxtWnwvwcSbdVgDDujjBsE7hamMsHDUAYPji5JOovNtOsBI4FaZGOqOV4fSquoQTVRGtdwKPWk63h/VkNOf5Exh2tjIAoGt8bBiA2cT4KIyQ+toqf4GTwut+VztKlZMTo4Tw5sA3OTo63JofwnZhfTI1lx7pz5ikKhPYFLTlewH6epTsDMPvPAzGUsNjqNQ3K9TGyPAaDEg7qxt8S9Q8Zml6nWm4bGctrxBTmJeOMRhcoEo/T3FaGoXBMjKHIsLMzSj8Fhbspd6sQXNTw5lb77qPh4W3q3luesDjBzERNx0AOyFJUuNrdtZ6Djb61lICJPwa4G29vlgtVxDbWnksrYA92VqpWV98xIRhLc/TCQxreaVYBLy+tprsCeDPDyQhKLtXKINhp5JLIjrCyRXgKjhFW+vLyDBf5hD9oAirjCmxZPqdu6j9oSvaEa0/QtPcF7EFQzMxQwxJuR9RvwEe8tux79HJ1azH1dC9gmEXi+NVlLwSD/KLxM5rlIjjbFg9ipqYXb/1YgSFqXq/zkGCgsj0s6TbHS8hfXyqSSyG1VeQ7qrMgIt9aWjKVIaE//WiDxr5/SmjRSzgYSHaZfWt1RxafHb031V7aOy2owIgJ/ypwUBiRzIGZseXLk0cmeQrkPUqnLbcWF+rLC+R84+ClJRv+QlTmx5yUy97wxhfi6pEp/YCLzly13kL6t4p8sZZtZ+zMWYz2lvdeJ7lTnJoSJpXRluHB3tx+mtheh17Q0lraD6ZoTZENM+eZwhw8fIwWMsL5mkJ8nNlZuRnYRigRCyr4MQ3gU+SM29mrNN6J4gDDOsSztTcxlbF1tJF6nmN0uWyJA8x6gqj0NpyQ6yLrYG+3kcZYfYnWL+eIjFm1mYIiKgjse3GsfZuFWusK18ASAB+07hXGPUM3tkmSolwmO9WFtff9sAVz7jJC/Mwu+luGuJu8ibf6yygguGEoZrC4dcn5Ra25nmqHJz9d306Cr0S/C2xLRjP/BOeuYKTIKf08OrFU1yDsjTTbQkNlNbBpDBMmD4eFenBM8QK9Y584472FjVOy8nJCVMrhSnRAYuJ/kemBh9j0AX/lhTcHOsvnhgsuenPt0QExU8sTXSryqIAbmGgtbH0CDBYa3MG/LG5IaWvPX92ogzQF8Zp8LHe9jwM86Ql03NZfKmJt4nx4OzMW8kHEA/KS84qIsLZC/R1USINehUXjM3N9bzs1LPjMyEm9DJ34+Rw42h/WcWHIBeiMNgXPx1SXhKpgfKcN12+G/gTbrZmp56wLXR2hPtWdlquBvAVDLtwbD2lzchX0i60qllPWe/W33BrdtptQ2zGvt9h2ys5H0j7nbOMw0lak30xnN3TsYZm6O33s9uxYIbeoJDdZM+2pP9/a8sCCzWtfZHGVMcrrL5FKKaI0Mhi2n7SQFai/Ipk67JLK5KNh5RBXPeva7c1kRFMWtGrF8/k/re3u8OdQVmUATZUhyEADJe/yuMdO6VFLTntjfPQV5+006az0BuS3ZZcz8Z0V3tr1KoEKayDtR7yEGOUMibrU9dXFnhWFN8s0BVScE+M39ryvZxt9UmBIjoi8BJOGkCs4cE+JgbLzUxWWdxorHuEKHbOVp3SFBxSE5yyO/L0157GcRCsb087bE1JCLQxNryGNQPhB19nk4EHtyS9GTTW6k5Pu2kHWMvY6FpupKOYwDyppMdNTwssth4VHiCnNT8xPqIcgAHgfFxdoVWN8o2NdRd7M1LtGRsZXB5te5ntAWPPzITqBEsNtu5j5xKG+7uG7vnWpbmGepi62xk0M3D+2cHZXyIojOS7wriSInxmBxRZSkvymE1xIpEI/oIvqInptcYgX6ofTNoStp+c7Mc3IXTE+lo1FWX6e7vO7gkgLnOjawC0pIYTlIdYeIgDACpAWQebdc+eJJkZXpP+XUfgZrE0/UCqzPFo8W3F3YJQS1MdAG/wX3Cw/t7Wb5oz4O+AxDaXa0Z679jzqJOgpOz5rPEJk9Ws3KfhkmBYWTHeH4CI9Jvz8NDXy15q0m20urL8XnZMLI3PXvJ1cnwMzwQvNxuFT4zQIK/L3Zmdo73z5zEheZjm09KCa0WfjlO8XoZ4PXQbWzDb6X75w19FqpIkh8Sqb0eLkqu4gmGaibdWRLVPzLJ7SvFIfUfX1lhWkHCQilD3L6EylOqn8hHV7sVeeEMirfMQoww2W5EwtPgG/phVzxvs2NC3OHPkSJMY7Nhup4Yv7mom/eAY+092yHCfJlgCsloIlewPKvv89vNTz6ZLDgDYgOGHvi1VmrmkV3Xr6+dMo7Dt7S2SLjysuCsnxXFqAh6pluv6OBpVxDti6XlW5S+pTVNbgVe90DXnlo2vk5G3g6GTjZ4bXx9nkLBagaMhwDPm2obuB26vLwcFUC0xlqbXq5Oc+2CLKGkWBLmZmMvsmyATVUN6jmskJ0QQkADLnfwMll+EhBVru5+cnACYIdWwljuB3HrDOoQHrcnCUDsAYKZG1yJ9rBbrYxAGI27OA1n1WT6WyN34uonhtc67wfT6ezIGykKJ7fJZkUm49MWFWUwRPOLlnZ2RVPu4kqu6uhoByDA5PoK2FQ4PfPkgHXC+sw0FufG/T1Jdhkt9MaDqVkQ+hME2eM/neZZ7VZJTWjAPm4AZGV6rTHCS0z+ku87u+tyNtocxhquspSX5+/t7xMYaTktURMDTBvlZtuqH9zEXEXasPtBHIhSKMQaT0hGHI28RaUQAKnLS9mzfLZPjZ9Ncc+NrN/35b15ktDSlj/QVBQh4gMcAld0KtgegBXBra6Wm502utJp3Henau5qtzj3cW6+F/woN4JsafMSTTWHgD8BfctL9sdvY5NBdZ74hhmE5mee2jqwsL7o6mJNdGh7ql7zvyMtOIQ14BPbAfmLX6bP6+5cWANc/ezBscXGeqbWLa7PMUZqaFPWhHDUyif1TBgYr/nScYpTyfZE2Z1ovV3M9BxPIgpmp1jhlrs2U5i2iUF7BsM9XAKYf/mu68rPdpP6qtp9Rxd/2/yZZL2X7rb0ums43zWf1lc0nFGkQHgrsW5IAUVD0OR3Ur8kGkJD2OZatqAdjtJMGSz8r2Aptv/ZX6FfNBuw5VyGQGedTD52BP1RR3iQFsfYfvXzRwsuPjfU1ZtuPMDl6aLAP8hWSxygEYAB7kIpGqM2bPE8l/ENcsEJCc1LyGPwK+Ko4yk7gaAR5IW4kww5gREgAfnax1Ye8mVnoGHkQtru1HuDnhvUSUT1B6Io/gKXGXfkGRKvDiW+iVa0O0v1PJeXhATPTk2rAjBB/Nymk1A12N19EDstpXF2bRW9S+spCu0tvnrSm0LIc3envnsUBBnO2MbA00zUz0QG0diQtgkkwL7FTmBrCJzBMIYtsbnaaecU9nK2qKkvfra5c5siEs0pmAQAUhbibNghd3e0MCOSGK+5sq58Xzn+R5YF5raRVDKOvlzked27zU4KsAbxh1wRLqQ5nlI/F69xT5VaCwWB8CkN4uOHQzlofwBWday7Mdba3Tr+dPJtYw8lxsDGCkWlmpvMsyBdw10lKiig1BbuETURH+tiaYDqin5cD08aKQz5zcCDw4J9tyISl9Xn64Vb9wWZ9Z0s231oP0BTAsAf3IgFKYQ+xldmHvp6WUl7idQcb/UcV0S3P0x+VR1tJUfpZfqM9T3+4twi+vjBV4eNuaSUt4uXnKDNxYRbV01NiFKq8XGYQJaFMYTz5I1w7rPsCTzymK4MaMTY61NzUsDA/K/ncBwBa4i2BkbzA1TJYYEuGFkD0s3fNewvCP+r4CcnavU9DTjuLGjFIAjP+MduZdwVvnR1J/zfpVY38A1Io0BYAm0b9LJBAjnzn0uaUr2DYBxPH75BeItVH5HmxB0w1xXLs+CkE61kGESdEIh8PWX1lq47aUN9vsaXboVfBf1FbmbjOijm584omQ28/Z7WJlVQak2w1sPoK6qz7ETXaXsVidm/ud4WSpThkWcj+cdD9S/QV6f9dFQ4BMH6G/0ZWU/1ZZCryWY/5uRk5+TvIKc/T4cC+TJCqInulewrU5yEVRmIJkAdLu2ueZbhlh9mUxzn2Sd2WKuIdzYyRzgFPBrqwvLhUbZx6bcOvAS7GyD+3BOXW8K2RVw9K7uTwZGUQJxt9ZhoNuwGAkGTnsIyODGrjREHilZIYyTwhalfehof6cbnJ1FgnM9yBLmQpbgZDcoi0PD2DnYhqXEQIESBWX+bys/jbvtYG+h9ZSyGuA09vsymeCdKmqiOtEcy4jilbCmepl5cWmaZwkEjJCVFczqR+lkyXBZ0oI528cFu47n7ORmSoYF9vJ55ekKuJv7NxtI8FLPkR/Nxw21g/C3e+gZnUSYx0FfItP3mY6KTQNQEGW1uhj4+TMVJBNL8eGOA+xkKIH9AR6YwyMvm41MeN8geT0RG3kxJ9bI1NZQr15wldqATtWH3xDB3xOkCm8YGSrRXEP4wJd8H68vBvzYOY3bVa3OLV9iLTzlqPzHRYGF8DnEa0N+Cmhs9jWiPOpOGTgOh2Vp+szj0M8rHBn1Te3vP8WR2To6vS+V3bQTromC1tU5PjWNTU291W7Z7Gk+PjO/kZeOUAqtvfvHrT+qL1dfPI8MD42HB9bXV2RlJMZFBedmpGWlx2RqIwJSYvOwV+iI0Kef2yqb62quRONpwu4jgPsPzit9Lh4eElQN/NzfXZmbdMGaelxXnM88Sj0cpMNzVK0FSRdsvfAQ8nWKoefDBoZ9JIRlb6FaR29uEHpJ2ELQVp27z/RVYmmvaQtBMM9h2t7bQYtbGQpGvwzy/fCvUKhn0AsV6GVM4h4b6gKii6b41lkxA/YP2c3kCSG8RQD35lE7Pu3GpoEmkfGpYSQWZTIay+MuctQyPfZOUUIdqjPaC7f4EtFkXSI/grv6iac7wYJZ7QU8EVvGDstDAmkz5S/fm9Prp7laV52qc8SEe7kgWyWMjYQtxNn2e5nwVguOoFy91o+zu3kQ0uoK8IbzNsuwz/Nqa7wVcAXxFVevjByUbP29HwtsA855ZtnJ8FUUGA7Dkt2BpLKTzP9shODifFLsAtNz1MmY09PUXegNm8HAyJVofavTdKZnwb6h4xeXqujhavXzURTEKSKpbR292B12ZuohMhsFLs7iVdRG2pgxVhBVFOSE2xC4MuBZUxSU86/Fub5ePrTNkKI4hirPMs14+mI3akHbYkB7mZWcggqxLLLwAMzKsfFxXCTBaJR5ZWo+7xQ4asv26Cv+V4uV9LroeHnQEcI2kIRJIYUg4hcQ/DP+Axw5MCe1g87Q2qk5wH7irmIo41F9/Pi0cVM/PrKSkxEnaZccmdHLwJU1OdQDvTtYQ4iVBIw7CMjGdBvsYmFAYL9HFWr077uLqCOfDcpPRIXLnydDGbmyhbmn6wMlN5058PEAvDsMwUn/2NOoBh+xu1+ZmBJvo/UHhTW8LxmuikJXhHh7v4e1tjcG5u9HFVWdTeWu3i2weY5YgbAgGBKEU+tDieemhTUwH7SRi/Fffpzp+1tVVcX3Xim2yoxQvFqOksX5frAmsA9JIcHwFQzdOFFxrklZWecP9u/oOy4vv3CgCtwQK4jj1zEgCS9no14cZvrK+BXfVy5cGee7vzuzvfoPfk5mpyfDgeMzAUAerfSQt5VZ1RmBpibUYXWt87JpfNLcl0ETu/fFGD2cuJ/X662R5QDXs97fNmmBdSJV1fRrPkU2bqm4wpj4Nxyei/MJT0LSUn65JPeVzBMLVnEWbY6ukpj6M5urloNZvtt7abUMmbos8ZivcnWQEeCrx9kYOP+3o5xYHs+JJk9w2rrwz9hay96vvsplD26CYx+IFVK6dYMvGJbNLlu8qoiXCiSJFKqxLt7/KR+OGsF1sxfdJKh3thP+sBKXXT01pICBQXwaSssFueZnVpLtjH9qwEwtMMN8iSQz1MccUM58GWMjV5SIg97A38nI3wlDz86srXL7pt9yrHAwuLj933e5Htbi81HyMbtbfWC3IzcbczsJB1GsA6vR0M64Qufaf3AXLr0lgHAtVCAtwvSDo69WY52McEQuYCm3jT+uJpQ012RmJkmJ+vp/2d/IxHVWWLC3OsUoKlRVxyhON1tjlfogNr01t8YqD/MRxdfKBNd+lNRF9E3s1C9BX4tydj5vHt3nKAas5mxrT/tamRTnmiByqjMUQ7SuJciU59dETgycmJktyrIFfILIhdfjsNJJdkrh0PpLI4h5FSXxgqaSE8B2s9QPjkoisoFklrZfDFYDeTezH2HYXeCiU9JpvydxbRY6GkpCA5JXqgt+t4b/dkelpCLTMS+mfp8vatZGZmb3wsNzMZNgFXx8T0WqHX/2PvPaAbWbbrUD29r2AFf0tW+pZkS5YlWcGWLAdJX7JsS/6Wv2xL8nt3huSdYQSYc84555wAMOeccxxyOIEc5hyG5DDnnFP7VBdQaIIACIAAZ+59PKsWFwg0Gt3VVd1n1zlnb9uzpCSKw8BgXO52bLSdsZYRPaSzMziKuf7d714TCTVogwMfSGSMrvXSBxi2uVS9v1EPcIul9RWOaFkYvezpSjnba9lYrAJ8RUITmNOF+fXO5sSDzYbjncaZ0QJMyAEoLifN9/yg5cObVFLkAy44KRwVa6PDAySKXl9b/nnvZoTqsyhfKDV5enqCEzsRRceWghQdLU01D8Rgsjc7SzYnMaK5obrzVXN/7/s3r9s+S3/2fXgnEoN1dzQZ6ixLifHE4wresbPQLU4NeVOTkpngY270ktzGAWp+EVVhy2586eTeH6N2Cr/05/HVHvJAiNbzx+8rIaLAf5hNUSfDqjrs9Vghi+Po76PEpW+FPcGwL8B2S/jFkQM/LweN+Ho08eNvlmULJR/3CSK5P0ztN8j6Q0Ttavp/ybpcMfiL/K/ISFG6UywseJv5u/uZ39GT8FiYGiqFWB/xHwrQzmrgF3TRr/ap8T+WW2/tG24H+3tvu9pLCrMwrzquPMGwKtSVjeNdYiW/GpLssKoSYAATBngTqSjD0SqUVWjyEmDbRDEiV8CsiS1ceycLLZFKFRzlECZT6alHeRj05riMFoqp6nmf5Qy7JY//3p63SuwWazMdGT0nT1frQxmKf+bnPpI1dcBFZfGO4ik6epOveznpIVYsWgkK/kI3hrgacgIteEGWWeE22eG2GWE2nnZ6AMAwvjKjOT/gb2+hHwJsJM42yFuoDyfkFtAwWYgUA9hAWOOxR6Ww86oUFxDOHS5xR6rDdKnnVKlHG88+JcDU2VKb5LUasfk5rvhfW9OX0Z6G1fE2WEAcqSMIlMfQ3wK38eqI/WXRCvKbi4ulnCxHE21vc1aEjXGzj/t4WNB2bMw1oCweDzUu7yQxIdjKwACJgGmw2Wql7g4oCMbEYBzOZXJyuLURW8COqFgd0e7uDqEqhVnZ0oSkLAE/E0Xm1CSPw62GnVUk8zXQnYGLczDEsrXQiQ6zh0YwGI6VWQjUxgGzpXM8Lw5b4ev7G3WfpoptECGHOsCwopyAk92mreXqAG9znJQYHXF/wgUBP9ILyVRtl5cXko6EyDffO/glWWxk4KPBsLsNkH9/76MSix8c7MM97U55sHpkgL2zrT5WR2DrPONEuL2tTW0oiLUx0zYUjHlfT3uFo47KNMIc1vuj1EOY2x7HTiep8T8R+kULVt8A1+FyE0mQCYNgJrJmgT3BsCeT1WBU8auwXsj6lZsLavRfC+SDf03WEqPdCkE248/LKot+tYsqyvC3Fu1kQ26C6rXJv5K1epJZ8AbzTaZ7X4SwFlYSa+JWpnC3vT/2IEoVpdt2DjXyWyi79aD9B2qwY843WhyM7/tGuhv0SxC9HSpwe5vhZG+mZSgo10EMijQeMzV4YWzwwkDAOMdMbswJMx8v8mCU5bgn+BiRKI3YmjSAFkFOLMz5IUkAOiXAhMTNmErKD7Sbmxui+ipLu8v7f9eGBnsZHaLubqt3g2vAxNHTX/dxEvzMtDQRqZ0JHRJk0aCLTTeWznMSEaLZU9S87VmDpQE0eT1HWEXWz431MSWdHBMhUxrzzNQEs+YHfND9/UeNiZ2cHDOhII6jJvsZt/LsAYxNlnj05bpWxVkXR1mmB5m6WGm7WmkHObNSA03Loq1epzkC+hqhoT6MXkzmCfgfvp7kazzztuLy7E7GxPExVVmZ7WKjrvM9FktNn60Gfw311a2NNH0t2HVerqOhQZtxsfF2ZizWc5TuyFbPcLYGYEYRmWZBKCzT2UZPUBKmGCcEdDUz06+sJBePRoIxAIYVZAUcbzdiHbCd1drB7gw7Sx2AW2aGfCJ7fYE6HIAxd0fD5tqYEH+s74zGTP+79IPNBqwhNj/Jh2EYnp3uNm8t1xAYBiD83mmCNRgwP6FK9QzuNQK3eEm3iI4JglVYYDA7gyN21sM0cbA2CPZ3LchNy8nkwt/CvIy+D++GB/uy0pPgeEIC3MKCPL3dbQHV+LjbWpvrMmeWudFLC2NNZthTSoORsLH+SMKSGakJZFnNiK3ORGKC1AZ1b2eThsLY9vKkEG9rgvmtTLVnplXAknc2RR29kYP14XQcPcTlJaP+XAaeElkl7/vxh7IbPBIGW0f0znwKtD/6BgQbn2DYN9KuT5FHjsbZd+SItB73CjGGjLlwN1fUgoV8SYPoRjMmDKPJwmR69lGYMykji8bFKqNU9Ifo+6AMNvlfBJG6v5Fwvuco2Nj3k4IE6H+CBAq/qOv+WDLKn9GuLi/nZqdfd7TOz82cnp640qTYAKuyQszaePYtXPt+Gm5JAj/54RaEHsMQVeBog5ubG2bewnVozA75ND2yub6alZFMAJW54QtwhUlWIaJPzHP1ttMlQE5YuMJC0TCMrCLcURxM0mHgvET4UXIk4BIxS8kfuiS8v9faXMdk5SbOk4ezJaFlww6oLFR4OztbTDUqOM3iWIeDN/GSUhMPXsfxgiyxRJjxnZghvI8UnHXVnCx1bM20FurDqZHUW3sYSnmf68NmAN3R4QEZzx0cShHWxEdej5+eGhcJq+rpqMEwK4q0bEhCWbJTJR7Tpah9yHGBkQDYDAYDNJxAO0JHcXuynStjrWGYodRWOlw2Mtx/F/NRhYWAoAaC/QF0OZnqsFlqRjT0hU4GxIUgGVvdSpBzBZ8Wu9GcHCIYjJcyFhrEZquZ8EWN9RTgcJ/9OMWMQgAKwhmkuzvbmD0Fc9D3vU3DOIqGYTXHO009r1OsTDVx4Is0Pc2vokLsAKdtLlW72usbsVBJmK+76eZyNRZr3t+oHx/IMTd6QWctIkLF5FiX1ro44lVHhHjfe8ydr5qFA2xk4LPdtgU0pHfRI5wFfr+tuU6RpbmtTaJUAVeBqVpRXJApS4Ho5eXF+fk5YPKd7a31tRW48Q4OfJj9OL28vLi6srSyvAiwLYUTExLgLlatjpmvOD42dHfnvT1vlagnsby0QIL2YUEeOWmxInVxgMHsLXSbiuK6qnlpsV5k1NlbG6ikJGxOi++6yEg0v1tKDfycIN0m6Et/EgNKJNzU039JnQ5T3wjbSOCzuG0kKp8c+wmGPRnDbyokimSyVmFRNFlF309Q0/9DDq6Yy22hsMOSzKKH21nCMBrs4f7tcxjbb8m2Lj2CGEeIfPvNpQxA9APqLr5QvWQN6EU7RiGWz9NYezTred+Vm8X1E7gslibaQX4upA6nlWs/UcwPJkgRxvVz0MMICvzjUFf2h2zn8WLkBGPKxLmW5MO1mfOzUxtLfUEp1EvEc8iIrY0WuRdHWerRGXcMdPF1kq9xTYJNWYwVoEEAYGLDccyQWnmMlRGj9EWJeYnECXvb9aqtpb6yrAAa7B+cJwB7THL/uzJcUjr/lqOs89zVWne1JUo8EuvjUoO8sfKgzDAbG1NN2BgwFf5rbfISQEWgi0Fvkf/h67i9jljR7w7yLnqSvO1Z+gKXOj4mWHaCDfAXCeEBbo42hosL82IWVVRWBNL9rlPE/yN1X0HO7Op4m4pY6/pEWxiNXemOVXE2fHHwPNfONMfGZLtwN31HCy0kNc7iD480XpwoWgYMVloqSClE6YUHCfGFbvbWRi9ZrOfQAFYBEjPURw3rg+noPUMyzSkpInGw3bhYZ1MdzFBPE8bUKnDKhXnpTOhL5IYBnuGuwHhpqCfzYLMewzDcTnabAD5ZmWqxtL4CEAWeMbyIj3TcWKhEWy7X+Hog8kN4MzvV52S3mf+tnaYPb1IJfaIxW8NAT42kL0KDG8W9xzzY30O2B/T+Ge9soYEeZFmEiY7ImoJih8dMd8zPSYW7gYWxJgEeRQWZK7cJRR+4OjY9NV5SmFWYlxEV5ns3M9DcSFOEYgfQXd+Hd9KZVOSysuJc/ngweLG6ND/cVW1m+JKZOm7IUksMcwEMVpIWgjG8yq7+tbDgXMYSBqQC+h0aIXwX5SUqy8CXO3wth7aQjAYoUaAMdjP519eX35ykvpMBFLjbTKa+pfYEw74kIwpUw7+BxBxknLMn43KTT5xNoTxGvnh0pKw3qan/KkzMlcWI1LWU2i3RxTF3hrqxDuIvudeITEfvj0ljmZ/TFkgT/rZMAO/JHvhMu76qrSqRzG3wta2pZle6k5ToEwE/ke4GOGBlwNIAh5iZbYjbdE3o5cW5j5cjDvv42OuJACqAeX05Llx/EwdzLRNB0AwgWXmM9WSJB5Idu+8wcHnYSG1cBi+GBIv8vR0foZZpbXWZMBPYWbJlV4U62N9jppxB09b6qirRmRpOlcZcP5yyUB/+Ltenr8ivgefanOq+1R4zWRl8/CYeZSH2cUT1xwa42x2xqcGW+gIPG8D25saaXOd4enpaVpJ7azHeit3T3SWCwZTo/901GK5OdsZ3s1UxuMI0np62unZmmjBy4r0Nk32NPWx0LIwwHaWQyQPc5ab6qlve2uHBytxHqqz8FprCYIzL246NfhfgU+zukGBv7kOHyCwNXzqYaFsbaVoYvhgMDrjFUJ/Mga9kOlvrCtIRk+MV8f9mpieIYACAro72JvLR6MiAACmp21poL86U7m/cgmF0nVjj5FBebISDq4O+j7tJbXkkTjuE9nGskM48RCVkrvb68O/pXvPRduPmUnVxbpA+Hc0AZ5qtI6SwJ6rTsgxpQvMDw2weevUzGRPEMg+jrqYMv5nGjVMMGpFFK/gJis7atRcQV0KzNtetqSrZ2d5S+hkBmISxJKIsIkVTWym/GOzvipc88rI4O6uztblRFsZCGAYj0MFKr60ssaOSE+hhSUJhjfWVSl6RudqhFm2ENQ5b6fc7CecLwuru9RglAbBLFPDBxSZITrZUObu93KTmWYKz+ylq2RH90M1nVt6Tz477ZaIMeIJhT6YEW/EWKt+pFC2cjgpCT9+h9ptkW5MY5Ee0e78rk970zTk1/Ot8/lYZVfyuj4VEi6hSTl2Ge/lHauhXBDVy/5Q6n5ew2Sw18i+fAmKPY5eXF8wKB5TSRqdpEQADri24sBggSQc/gJESfYxwKiB8vSre5i6Nx6dXyF/hw7Dbex5kILHxIveOVAd7c01ccgD7jPc2EksvLrb157vufxrZ6nzlZKJjJDiRnvddqu7M+OhgRsSAJ9d3W5vrblfNqfs4sM/eJYqn62AgK0RPP0iLidE89QieiVV/HuQtNIQ7WGizBAVC4NO/7VKw1hH8S2ZYDHYFxy+Fa1Hptrm5nhgbKrGAkB5dAokwIVEniY5iLMEUQFta/AQuo52NoZXRy4P42FscG0wwhpg5Uigu9yop6Tghfi068jA+bismei8uFt4RSUes8XQmJWHmRi8VEK4d7O9xEtByQAP8yfx0aKCXyVlXkOW/s1oL0AvXhh1uNQAqO9hEf6GtL1TC+zSHR+3WcjXAsLGBHCtTLTzF2NrPokJse9+klheGOtux8ZvgZMNhJ0Y7BftZutizic8tY7Flfk4qo0jys1XVErAK58K84oTxJSk+TIHdAr4ibD0fut+QYZmbxWUCJBtzPRhpMFZftTWODPdPTY5trK/u7mw//LwODvY5iRHMYT82OqSiPmxurBHw06p1tdaMdBQVpgQzuZcApQd7WdXlR3s5GZNxkpPJVfJxHDQKq99ROmKRDNepmO8+9f4oLTmjDEhzfSjkCMA5eDI6ZtJXz+F0SPYTuED79U/+yRMMe7J7Fyr/TpAx6KDaH9rJF0pGyKjpvN8olE6WRUlsI0mQmvhzsmqCXSwjUWMcOpexruyom0G9+gyRl4hHnhN8ta6+fyCHWPaTyW9zs9NM1x+eoKmBJsl+xphp0IitoaP9PNiZPXJfDGpQINhlJCCmb0y2G78DnFY+VMKPernb4Z+LdDfA0a1hOs8QEzBOFHv0ZDt72uoydaJSA0xkh2GDpb5Xe1s3GdlOJtoYhlmZ6mxvbap2jjLcMoAlOzvyrYKfnp4wpbHwWUd7mXxqjEBZiFKQmCxtgHfWnRTubqynIywTykxLfMj5npwcuztbMA84KS7sYF98/oyK9GSLCzJxTqDspCkwwsFLJuG78/Pzrs7WsCBPM6OXAJlgV9Wezld3AZgYSMZBDZEichA8Ey0J4/UH+ZkYCI+tvERWWrYb2ihaoY5JywkgXyTGCJ49Q/VLLZPnvbtWBzAMgbGVmuLcoIhg2w9vUjH0Atx1tN14vNMEDbAZILHjncacNF/tr79H9mCg+xwavMBZjvBRVorP6V7zyW5Tc20sYfjABCH3WkVZPrNc6nPd4si0glnJrKEaHPhAkhUVGJ+LC/M4I9TCWJOkifJXaJcXg/3dJI1AK1NtRxvD5ISIovwMQP4wFAGewfSX69dhhMDwIMrUBE+qIhkYOieFEyOgqFUrz016XcWtzo2yt9DlU3Gy1R2t9Coyw+EviYPR1CxKq0y7Pp252i6n+n9CIAT8x9Th/exHKDJDqs23c5S0LD52i71w4GeVU2lGdFzBbVt2U47G0v2j6Orefr/ZrZJJV/YJhj3ZZwolbAlXLzZ5Kvyhi7Wbme8JRYevZbtlk+w+mNgHbffNyEtq5u8F+mb6sh7YWjhatpn8zyiEJaPNvmAIKH9fYiCRkO+fzz0NNNXZx5lJ7EwYszX8HPSwpu1UiUdZDCJGdzDXivIwaOHay5IKOFLoFu/Nj4YBGHOz1u5IdWQGxEYK3Ddm+hbmZ035asXqOWHmmD6hiWPna68X5MyGPaQHmcKRGDBYOpBcr8zRsH5a/el6bOSGw020N2Oz1fBCuNLLw25N0ItzUhUG/VlfW66AP3R5eSFCw6it9SzMzQiFvBQGYL0cajhluzM2xsuEyT8ZE+EvCTLJheEJIR5ubo7mtdWl62srjzaAR/o/JMeFAYLy9rK3sdbXo/kMMYWGHqrjUoMBAC+g2dkaRYX7v+t6hfMwR0cGKkrzMfkBbGNh+CLP1W4hKvyW5rJijcubjwwzA2BDl4SBm975qlkuDxv819WVpUBfZ9Kx0eF+d1kfhOU6bA0rU625iWJBImJDeWGo7svvs7S+Ake5ujQccBR8RAe7QiqKQgd7MgGVARgb7M5wddAH3GUsILvj4zGExL6Oj3RcmSvH+C2N48nWfoY36H4nkyJzSWEWOX4/Lwd5Zc2VZVOTY+QwmKSI83Mz+NYHg/b0VO5jA/hE0o/vRreOjw7rasoAOd/NnhUvbuFiBcCsIDctPSU+jRdXVV4IY6atpf7N63aAka87Wgb6euAO1t7akJmWGBMZ4O/lcFfakZcUpQoYBjc3LzcbwsORHuf9qiK5o5KTGObK1nlGS8M/i/S3y+cGMjNXS4tzlHkQ8zpUr8BtGP0DmRZnAYON/K581Rn398WS0OXjax8/mAHysJOa/v+FgbWttC/FOTh6Q039d7pWxf3JU3qCYV+wnQwjZgs8f06GVPtbC+aCDEANmba/2qHG/72QePBeSkPYflQQ8T98LbPXIOeKF9KD/31GXoEEmiOAmqv+KKPg28i38+XYyvKiOV18D7CnKt5mqsST5AeWRlu9z3IeL5KpHAsnJdYm2pJomAFL3c5ME3N7AIKCTwGG1ZWkBvi5EnBVEm0F78NPJPoYaWk9h2OAN3EKGbM4TUf7OWAzGWFYX77L3lQvVVpG8VI6/TxZAuEalcrIMikEC/MyFN4PuKohAe5M8gkLoxeL9eHUaBrKPOyVE4PBV/qSyxOcAE5jakTENK2v7ulq/Wl+VlnnDv6WiDvo5mReXJAJvuPRkQp12IcH+1qa6w6npi6mp+faW1c62pcb61/zEkuDvIOtDCsCvTvio2rDArO9nNuTYpsSojZ63h/Ozx2sLNdUFAX6uvAhh766PlvNx9poNDyYSklVAgZLTr5K5sTZmZJ0ROgNBc6uurKY9GdEiPfdaMnJsTAaCZPOwVpvYjB37VPlzmotoKZMnheWbwYYZmH8sqwgxN/TDF7Dm3qaCJu97+Cd7jUf7zQBbAsPsrE00YTNAGjpaX7f2ZbdWB091p+ztVy9v1GHa8mC/SxxtZiDtYGM9YRrq8uAcIhslIoiorLDMABdTNr0tbUVa3NdTDMjexkn/1F5eenv7Yh3GxwgraD6YH+vobYiKswX+g1rMKqowf5FWDqUaLzkaBJMLuAFvipPai1NaClJTApHN3M/V7P28qTEMBcmJ6etBUs55bgXa9RqsDC1Z/ifU+cycJ/sVQrjYJN/qZx4zukYv3ZDiXSLmzwhI+LQr8gU4nscW3YTnunYv33ylJ5g2Jdt61ECWvm/V+0PXW7zhcl7v4vuCLIYER/DudEn9xAHny7GUH30xhN/psI6y/1Gxgz/Q+Wkaz+ZQra0+AnDMEOWRmWcNQlewYuaeJtmjp0UeS6xZInJfsZ6OmoYiQGasjJ5mexrnBNm3pBkB1iLaHkh+V3Tl28zEE3iSIFbY7IdZvkjtT24koetq25u9CI10LQn25kUp+H0RYnHUBl0sbpAcVIAhr329yIwLCYyQEV9CF4pqQZxsjM+Pj56yN5mP07fYupnq7tY6+ZE2r7P88GSX/fgLlwhBi9GUpaaIuN9zfR0nhvSHWtioGGkr+FiqmttrgcHzFSzfcgiOrikTH5IZvPxsFMFM8rFxQVOeDNgqyfam5+urlCnp6hki8dDXIUpqR/DQ7lOVoXuDsOhQTAGeoP9B0ICS9wdg6wM7Iy18ACDgaHLeu5hptcf7H+B0gt5DwdgmB1xPToSSXUJ0hHzslP4K0sy4xA4QUL/IInn8/z8PCSAn/kGsMrFXn9xphSAE677WpguAWCGc8bAddbXeY71wfDEBLgVHWo3N1E02JPxroM32JO5Ol8xNpBTXxkFbXI472S3+WCjHhAdDq/BC1o07Dlmu5F9tIwM9+OIEyAQBUrjlGKTE6P4GOAvU6n56uoKUw5amWpvbq7Lt/p6cuxib4IHkoxhH0B662srbc11KZyYuKhADxdLwqyolCZXxFVey0pPJpHS5HDXjorklpIEQGKvKpJL0kMbC+M6KjlFqcF4sJHcS+XoCu5VCb2Fvp+SSbrzuE9Y+zD118rBYJcbQvVXFF4zVQ4Gw/yNCF7+C8TH9oXYorXwTJHGWhz1ZE8w7Iu260Nq5m8FlBK+qv2tvWqq/x/Sk/Y3qUNZKAeuqY/PpVLA33qg3lxsUqO/I2BNdFXhiWzE86u/sJLYxZqqfmg9FgmMHLQ+jVPx99uFeaKkHOVhMFLoNkinF2aHmmNCOU9b3RauvYyRKABIH7JdItz1CdM9JhPHfOLCrCdaAaw4ypKgvhFEL+4U6sJm0xsLVq+/jnQ3qEmwmSh2JxgMXrzJcOpIdRRLGdKf77Ix3H79oQdzJLT7ehAYFuzvpooOBBfZ08WKOEMtTRIZbmT3wnu6u0gWEF8dS/s5S1ct1tt061UMYuMY5PGJEAe4QmDWyxksDujK9lpujvxQ4JcWam1rpkmKwdhslJ6X52K7ERMFUMTYAF2LifERwbHdKhIg5UlM6+psLS7IBNwu9pjBwRUhDCDoV7msieBu4mw9AKgAw9LTkvgfjI8Tbow6LxcNne/hUyZNj/UcvmJM15IZ6aunOFq2+3keJySIEft6SOOl1HgJmTkS40LljbSsrS4nxITIQqNSIyA4xb7vSG8WBk7QjrabctJ8dTW/f0tWGFHbCXlKLE0QDACEZm2q1VgdfbLbdLzdeLTdCCiOSbeI1ZztLHWNacVeuQgtTo6PCcUIgaOSTEUEmxvrqwTw9DFk7mDMY5oZ6GR540i7uzuYvpLFet4WH0W9fUdNT1P7clxoQNGrK0sTY8OVZQUxEf7cpCimgrNcDcDk2MigSh8TtdWlhJXel459AQzDrb0sqbU0sbOSk8cJIBgMurSmqkRJP36DfJjeH0Jr0PsN92++nSeQTqXJlq/2lXEIV4iJjV8M9o+ROtbNw+iIro8Qqxmpxl8NkshY9sgGyHD6fzKo115Q+3VPC+VPMOwbgsSIorGMYoKKz5MUQZ7hLyMm1vufhIPCSbVoI8NK0gc+QIK/p5MqPBFAX+TAlt1UdGGoERpV9v/Ml3Kb+8JsZ3sLc6yDb2pjqtmd5Txa6NaT7WJvromFlQAU2ZtpNibbjcqMxMaLPZJ8jQE2ANYi5OA4wIU0iFgaLlba+eEWIrQf+N+cMPPMYLOmZDtoDUm2WHNMuPMCt/eZzg7mWlbGL1+nOQ7fUQ/rL/Y8X12g0rNwiU53oK8+W51w1iu99z50v2FqWEnXU5Ir4gTXxcPNRl8g+8sP4Oiq2Ztr5UfZTVQF73XEnr6Jn6sN3WqLRvT0Y2lLjREGeuosHTVzwxf0i+ek88FRi3Sy7Avyo7jcm+RkVzNdfbpkzsHaQIxyMZ0beRc0Ykp9O0t2ZXkhuH3gYcMZHR0dAmygaA2ru9mJUsSyxCI96XZxcZEt0P5ms9T8LfXrstMODxlL3S0tmDwDwJWPBVtT9yt9WuAL/+VjWjoeOB4ahMJfvBRlAjBBYVicnSlbAP6Z4RdZrL21npm6Jp1rrr+vW1g/qfO8sSoaQBTGTvsbCDu5OuiTPDHYIDnWpbk21oDPRI8mI/ab4R0bc+3l2bLdNQTA4O/BZj0BY/sb9SO92UQJKtjfTfYLd3V15S2QHnaxN5FORAG7lZepQqaly71dRxtDseQiRMG5oixfvtvmzpaNOR+GwU0GDSS49CkpVFUVNTBAra9T8oeXP83P9n14B/NxZnqiqaGKkxgBEyo9JaEwL72msjgjNSEpLiw82AswW093F5OFsrqiSNWPCZjsWEYSYJi3swkThmEkBhjMWsC6CQ0uujJ//ujd9SxLpiqsTY7QqRj5XVmThu6576wKMVj/T1NHD64xPptF5fT8Hf4Mwo2Sps/lyc3N49HPIsTFDILJJXL7BMOe7PPbbhlSZ0aT/7dkpdBQcGHmnJp9KUfA6uYSbT/4i9Tkf0I3FJkeCIaCNMu/o1R3FyAq2GhB6BdkZYCUr68uqdF/JSBKyv1WjjsF3FkRI5X0hmyNUBf9oXzXvlxXVysdUqAFwAlgTxPHTsaY2GC+W3+ua26YhbedLkAmXPFF0zAiCTJ/R70P2S5idzWYj5IhMfQCVHaXnhE+ApAGe2Pr0cG02zvpz3P99KboarCPSuJgevHlqAjAJNh9NDfSHBrsVWLPD/T12FmxiTMUFx30cNILpm2tr5alJtJ6qRrMyBiL1rYGzOxqjSpbnCx1CmLsm9PcIz1NcCfDp9D09fgS2B7uNuvLi1RZGZ13h8j9qj2dkba1gBtgb3fn3oM5PT11cTBlgitnO5NAX2c3J3NbC1ZyQgR0r6TVerE5dTBo5SoWurg4T4zjM9TrsZ4HWhmc1t8hdD45ofLycGbganRkgat9qLUhoLUwG6MAS30zQbmdtZHmVmy0gmVgmB2RyxWP31JSRkMCAfKZCCKBcs3NsdEhZkjE3tpAOt/m/v4uoGI+LtV+lprsAfBpa7kaw6fjnaa37RwL45fYOTam68e6WpO93UyMBNm/gM1wjAteNNfGYN2wlbny4Q9ZH8cKdtdQbO1ws6H3TaqxAMfCOJdjGez62p+RXanqoI0kA/QilrAxjRuH30/hyCcn1fmqGUN6BxOtvThx8gZFRVRHBzU7S+3uUsojDCSrJEwun0fI9oRfxIsshnpqfm5mIjCss5Lj5WSsLwD8MIaZ6naPZ3u1whKysT+iLpRBFARuycSfC8vSHo7BDruQWg/e4cR/vC8RUdzdA7ClKuijj3uEYgBwpmuh32L5rycY9u01omi85KjiX7rmT5j+n5Z1Ql5uycGlgYJ7As3o1QAVngVhEIF2Nq2SHyEMk6rOF/1MdnV1dXn5oMf8+dmZl5s1XwCXpdGQZDtZ4tFdlRQS6GnESCO0Nn5Zl2g7IludGAAqgFLDBW7vs5wrYq0TfIwCHFnV8TY92c44YiZjsZlI4dnrNEcAhEZ0iUt9ku3IbV2y3gKX45kRKjuPVOnMRoQaC/CG7ETbsjiXIhLGpUXZkkCySL6fHL9yfEwVFbf6umPGP0y7R+TdjJFUMRIYQCmLOs9ZOs8BdxnSgm9mhl87WmiHuhl52OrBZsZGL2pyUq9LS5lM6z4WbBIgKshNkwEFXeBBIm+TZeeyrDWkpyQQDBZib3pQWECJRXFzc+Tq42DXTXISvJgKCybIE/ZQ5+XKj2DIWfd1GB8HAG89OgqphIkgMQ53Lz7ez1KfBGDlSnU7Ojp0dTQTYTaPDvfrfvf64GBfhGYQNoZZPz/3kegFA7IClLU8W7a/wUBi202vGhP0NL+PVyJgGytTLaYsWFKMs7uTIabuSE1yX/xYmpXiDRuAt+1iz16cKd1dq4MdjvZlE63eyjL50j3qBSrJ0JgMGY9mMFtxJOcuMO7teYvf93C2PDmRgyI8PiaYDoWp8Rwtb2t2ixkzVHY2itNOTYnNWhwZGXj/tlOuM5ocHyFdCnjs4kK1IYvNzXUiLs/S/io22LGjgkMw2Kvy5AJuoLmRUMpZ3tCiklat0hBNGh9F/AZ1ua6c3YJ/NfjLfIr8i+WHPar3EQc1AYpz2nLTj8EBzBug0xz9Pbmp0aTbepTwwAZ+9qmI4wmGfWMNZiwuDO37CZVXW25lMALHF8rfP4AWflHsjyOSVhXZ0XuUNAhgcpMjfuHn4bZgyT8RePFtNHgGgz1wJ12drUQ6LMSFDfBmvil+b2t19uOUt4c9ITZ0t9HpZ+g4y0LdAYhrtBDRJMLGAMzg33tloCU1AIe1CTY0E6OGudGLV6kOIgcwUh54/q6THwqjHaDNmGhrI01ClsBLinp4h4NvzU2KYvjKOk31VXeDHuAllxRmB/g4xUUHKVDxAp7WZnEh9vCGQwIznG2cTXXYNBs7OR22rhpL0HS1UQqiq7VuX5HfclPkQWccNZRSmegECA3gR1JkwElerhA28HiVnk6kfgkOUpZDigj1lh19gVvm5mReUZp/d3DCO8w4GAC8e0NGH7rf8L1AlloRJ+4oO4taXpaE2Kja2tuuMAcHxywEcVE48fcBPvd4z+L86cWoCHczPXPDF2YGL4KtDE8SEm4Re/B4FR6OOnp8VndArXL5xzvbW5KY9JzsjD1drFI4MdWVxc2NNWncOHjHx92WqSqGUVZOms/CdAkWaKbTC5Gac266n57mV/jcESwXYDBLE83KolAPGobhUjEAaWydZ/CvgS7SEBsfzMVEHRuLVZ4uRpiOvK2lXt6RTI4QDv7x75AwumIi/EmAkcneCXOZlI3Jnj56eLCPaehhIDX5uMmB53HWYl8ftbZGFhG2Nzf6+rrlOiMyHWhd7Feq7j2MwWDAGLLUAtzN6/Kj20oTMQbroEvC8LgiVWE4S/lRbclJuJ4LqOlIqdokl9uIU/6BuO7qQJiI2Ptj1Gqg/HvYE3KEzOko01/abxL23vh/QGUsT/YEw75Au7mRLX9mK52/qDD5lypPq120EVR82Sl/55ebiD6VH0fyU+FZXO2rVhnsZIga/1OE9PYbFfn6cQ+6SR12SK1SvVEVhpSl/66ulKIVg9d3cdpbhJtBfaLtfHPi0fbyxtqyOe0dwvvmhl93piHwM0hnHrbxHBQDVPK2QRrylcVYOVpo0RQLX4NL3XEbhvXnuyx2Fl5XlCG3m+EZV3k6k0IdWqdV8SRbQFOv2hqZvq+5kSa8Ixwsx0erK0tDg70tTTWk7IR2ldrlGLAnx+kpCYC4Up2sbrAuMAAGHu8gPm4kNLDKy8XBXNcIhbxepAZbcYMseUGW6SHWuZG2/UX+Z+8TEXvHABexdwylFMXYAwyDXTXkZ94UFzEz644T4j3N9XCEDXbV0lR7L2aoKi8kZySiG0vwg62FHqCFyYnRpcVPYut8oP+Pjw6ZIxbekT6Ax0aHsNdrwFaPDvLcryhHbBzS3ORDKidHJJPwKD7OwUQL+g1O2dVMdwclknHk5d547e/1UvcrTPIBLniqo9VBQjw/uZGX8ikyjA64aeBUz8UF+YpRkVizua68wUYRJKbz4nsVRaEnu02Eq2NvrW5vvS4n1UcfADlLXeTrJCkR+9nYmQa4BXisJC8YsBxSgl6tXV+o9HTmw7C+D+/kPS/CIwpjQxUkHPdaTiaXTFiYocwlAEL639pcJ+PeYEDyF63Y6qjIkKcQx2ZhIfXqFTU2drmyci1Pn5yeCjMSYeYuLsyptOuwFAfG8Nwoj1flyW1l/IzE9vKk+oIYZ1u2IUsoFwaXWBXaZdJsO0fINwhA5bCT+tLsbJaa+AtBMdg/pPblW8hA2ZXL7kIMNvybD43L3VoKfydkNJnXQ9whT/YEw75cIHZzKVPc6eP3BVwdqg/N8wufflglkmWEC6TvJ+VQZ/4SL90FdfZRwe/OafI74bhXKWNIicNRuZ10eLBPiunZukjy60OOy1SZ99bc0PzsdGiIL3jYWSFmQ/kIFOEaLUOWRn64xXxzwoBCSYayhMUG6Xga/FxFrDX8HC5XM9BTj/MyxGhQKBdW6HY02kOlpAtjIDQMAz+JJIl5OFs+JHLY0d4k4sWCG+TmaB4Z6kPrria4OpiKLZHKTE2U8ScAwGD2Nl2951G2JjfMmhOMx9LS83xc9fRQIGu9OYoaSUW4C/HUp1ADNH0iIa8f4CYHWLB1EUlgY34mcvtug4qx0EAW67kJI7tJepFJe2sDA4Z5pHHjykvyXne0vH/bub628u5tx5vX7TIqSsmxDHJ0iAvwjA00goM8N7s6qUEZFmtXVm6VfnE4h/Fx9sYIhgF8qvd2RXEJ+TMSV6IiHBgBKy3dr9KdrGmi/JTzstKcQC+sFQ6jYnpqXIGThZ4M8nPBQ0gEX91tME56e94ODnxgvklDKfXi3EAm2+HuWu3BZkN1abiNuTZb+xnBXWKbvs4zRM/4inu234KxHOxqbqLY1kIHvmhlqrO1uSHvnYos8TjaGsmV+6csIzMXgC6z3A6ODSYvv+Ytih+g2Nxc/zgzKeUGi1WzjfTVHU20YVyJKQyTr9qQS+XmoqzFyUnArPcP7eVFJieQSjFPc2MNCbRGBzq8ruLScmEJNFt9ImAwLydjA73nBOJ2vmq+uLh4vOt6uXEz/bfCSI5y8YmybLdUiHNGf4/akz8gPP2/hOcI7qVyq+gBeuEA3Yr3k5f/BMO+eGd+JQRl0M2+uCfh8LhHWCSq8hleJpicX6nghC+p8T8RcnX8YNp+PT+8uRb2ZY3GBzNz3LWZ6QmmcLCXre6bdKeZMu/9FTTgJ3uax2lKDABFOaHmmIk+0Mfu6vJ8d3FkpNRfriovTFUPf8WCMQB7QwUIfQ0ivhCXGE9Dc8MXOBkPkJi1yct3GU63QmF5rtPNnMvONpSRiKIcdEUQF4GW0ZBA7Bw/HIZl0oQZCjQZCQDAwXW2Q2JE+mw1W2PN+chwERqJ88QEeKfA3QFgGPTGXG0ogl5iBcR6k696kgOc9PVZ6nD6H9qaqPx8kQDRVXIyz9GSxVjJdrI1kpKatbqyRCqRfORnQru6vFQgEtJQV8E/PAu96Y42qqmZkpHYo7mZ6eluxkRZGaGqQjODF0tR4QrLNIPbHWhlgMvqaC1sjTcxEefLyyvzs2aCWFZiXOhDpuHsx+nhwb6trY2hwd7E2NDwYC8YP6nc2OSEiNjIQED7gH6XFj/l56TCa6JDLZy5bKQSNtCdcbBZT5AY1nSeHS9KiHKyMtViaX2FlMT01Az11ACY4QbvwPsBXuZDHzIPtxrIdwHCjfZlY4xna6G3tyefGNT19VWAjxNDxPmhnE+jwwN52SkVZfk721uyfmVkQFL4qKWJjzQcbQxPT0/X1lbwalREiPfGupg1hdPTE0eagh/GgK+F/vXdEsEHqR3wqMpKqreXWl2VNM5fd7QKa+2mVFhrNzc7jWlg4VaTn839NPKmrTSpuZgPw7qqeFEB9mydZ+RgGmorHvspuGDBYPn6WcUXW1XoPzQIFcyWXFDVvby25CiM9S2YK587fr+RmvpvMqmx3bWTkZtPJtQ8mzrqpn4w7AmGfVY7X6D6f1wgdv5r93Cn8mkhvvMYATGSmqiK1MHTMWHhpsp5R75Um/xL/kW/2pWMiR5VWEMVGAwbZgAjBImOFlpJvsYfcty2hhtmmxKG8t0mij3qEm1xjqKxgcZoewv+4tnB1seOTBkZ7V+lOMR6Gbrb6MR7G/XmuIwUoIIxeB+g1zCNvj5ku3SlO6YHmXrb6TpbaqOCFswxQGOwu0whvfnOOx+aqYxscInOk5LSnKy8LVgpjpZvA325jpaEi8LT1fohi7Xv33bKArosTLTqa8qS48PJO1hCB8XTJZOp9PW+xxwYcLROJtqzEWHMZKezxISb5OQbOopV5uGoy3puwFIfKwtEcTCxMKyPc9AZa2uqiQg8WM/HX78ShWE0ErvmcACJ6TPCI3ZWbCl+NslFBP9MXnI5wGDyuuCHhwd8l9dA401J/k1LK7WzI+uX9/cRNYIAhm3FRlsbvTTUV7c20tyOjVY8gsFLSXawIGmugMQc7Ixh1vj5Oj2Oc4wN0LKUDEZ9nWedLUmne81M+S9MPQ/4amIoLzXJIzrULtDbws3RIDnOJSXRvbwwpLU+rq0+fn2hkonBAL/trdcnx7nq0wJ0Xm428hJC0MVFngTqHB8dPuTEayqLrUz5AcnCvAwZv9XT3UXGrUhS5cryIuE7jY8OJtT2WM6hMC999zaPKFJCE6TI+ikdhjFbQQHV2Eh1d1OLi0idXIBpPd35ioKRoT6qy/Dc3dkm4LkgL50/pdY+9rcWNBUndFRwshJ9LY01CS3Hq7ZGuYhPlWB7tUIMNvTPFAQSqrOrHUSngdmzwYlSYBn3au+W6Oui7Zd0ejeISnFEoDE7/qdPMOzJHmHUnQmpz5GqnYZUzDaP5CBQtegvKbL+IR8+/ET1/QN6qn8XiTsrf8HJSnjWsogn3kJx49R6DFosURZt0WcxuIHi099KlXZXekQMBg881eWitLc2EC0scOLZuuoAhEqjrQAavctwauU5uFppAwbQZ6sBwqGysqm+fsGBXS8N1suSZOhjr6errWbI0mDpqMV4GnZnOQMY68l2xrrMuWHm8Iv2Zpr6euqwjZCtEXEDapREWQIUvM1T7zJeG3nxqhmFwri8ybBgAB4ALVgsNXCXDRgYAxy4jfXVh3ROW3MdzhmzMNa0s2QzORUAfTnbmRQXZH6aRxm80eF+hKyClAnhq4bpE5lXENwp7PQA2oRjbvFxp1JSmT7ZRWLijQAGNPsg7kS2rtq7XB9qSFI0jHPZneTryIYrBRsPNNbymdzvcAYMhwSQaCH2U6WQpDNZ7+SteVPAMJQFqGNs+GIzJ5va3JLv+/PzzHxCc8MXGIbtxMYADLtRiK3+KikpwNLA4HZeHyFNgVZVXvgINwHpgVlDPTVfD9OpkXzMOC8WjMHftU+VGHRBO6KFm6Hd+QqqDfNxNzHQVcPiewrcefr7uskt5UP3G9lxe1/v+8qygpam2rqasomx4U/zH5mnWSiAB/darUDnWixdarC/m5TOdHUwTeHEAJDDJ/5xZhKfC4wBf0v9G5L/rNKWmYmiu5OTW5MThoIDq1TlSKsoyyc3hO0tYRbc9dXF0khHWUaYhdFLIxwT1teoqy77DM/lOS1BtdU/+uJYJa52kT4QIX9XACKC3zj5XxjCqu5f0NldrCAHmKkwNvbvnmDYkz2KLZjdGnnSZZpXvAVxZDOVH9huBZ+qdeS3lC/zBTskN5SJ/xdlKsrhBrH5X5z8K+VoqV0sIWbIRRuV6GZIMsCQ9JrW9cxzVeweYyrZnZu7TrzSrbqymKlKbETjH0cLLTszTZz1ZGKgAV7IAsqaoyM2lZWI+wsPxoXhoWJv6TDMw0bHQE9IhQ+Iy8lCy9Fcy8b0pYXRCyz6TKi3jBFDFwJjgM2q421u5yK6DFUELvfXnW8sIUBIZ/V0+XsRDsC79H1ycYiLNfAFwa1cW13e3dmG1wBFZj9Og4+4urJ0fMyvbz45OXYVqGwBPLsrzCVy+TLTEmm8oQHoEVUuSSn656W0+3oAwtTTeV4QbS8xGkbXhiX6m+PasML4SFHWCkESVJuvB47twBW3MtWBSy/99BNjQ0l/+nk5PDC4IcVmP04RdsQ0N7ubmZm7EwfcdF5ydH5O6vzcjPi9vHpFZMQs+DDs5TaCYRzFYNhFUqIvg+tfpPm42z5QPUK2hZJ6QnohqbG1n3k6G20tV2NFZrENEJdYnMZsgMFmRgtszLXxfFQMhu1sb5HYnezYCa4s84wCfJwAwjFPPJUbSzbe29sls++uwTghN7S7TI/JCRFELDspPkxSlwb6OjfWVwIkI8MyxdFS4exWxVIWh0MDDWjMb2GsubK8qKIBNjYySPTrEuNCRa748dG+q4MRpnsxZKkFets9dhwMGyaLHvhZ6vAV9UXZbglizCfFYKejimCwmb8XupqrQV/KqV0dIMbpoX92yxNGRxj8BMOe7FHs+ggBgN4fFWi0S8U8F6vC6NlmisqP7ZORIHJtr8wzvjy8uTq6OZtBC058VGkhx/f3G4QTda9SGa7ZCwEp7T+hToZV0ZE3N1didOu3Uqk5zZudIhXBsNPTU9kJD69oUykMg51/mp/1ETB2EDBmTAt2oaevvnq1l/MVMxsHXjc1Y2Gcs/2NmfZUSUhspNCtNNrSkKVuyABaWHGY7J9kfOnrqduYarrb6BRHWfbluowSobAij4+vc47XZq/PTq7X16iaGkHcgzcWGgSHZyJ5bVuSxpcCvSTpo4baCmZhmPRkPMJAyGI9b/Vxv4f/mpfSJoBhjTxXxMwhoTaMGuJlhtmwdFE8MMrN9jo1VUz2FI9X5+WCYZi7s8XSIlrdODzYz87g5GbxxPJtbKyvMdkjvN1tYXtVjEBMB4eJB/c6O+5uw6TtBu9ZwhrKJeI/oKULrIw0AeV6mOudJSYonkjG5cbbm7EZBXXMBgBJ1Xf6vg/vCMc6swE+YSbUIQ1APbWm6ph9mnFeOtaSjMGQBnRpfjBL6yu826z0ZMWOmaCg0uKce7cHQCWCwXBdGUJ0O1uYIQNakJ8LjJPzszOYYvZW+m5O5hNj4h8Kh4cHTnR2K7SSwiyRT992vSLEMwDnBvt74BbBXG4Qn/nJVouwMUaz9dGQGC/lTYA3LuYMCXBTEfg5ONgnitvB/m4Ht2c3dDg3OQZjciO2uoudwepM/2dzyfbrqOM+pTo8xzc3D8vzRLkz3xEqg13tyr2H0wnEGi+Mg3l8KQ4wwF2ShUhyEdejKeQXqR6H31zfKD3G8ATDvqm2aCsQf/juPcswe1UCwPY71I2KWXqvdhAs5KcOKs0VuD7fujgYoJ2vOOHcO2iWeeZcUaN/ICARUUYoiQhkI8G0//mtGVNnZ6cyls3AU/Di4lzVMIyPPbc25udm0nhxohwA+hqWhi/342JviSbhlp5ODfBJOz92ZklCYqOFblkhZrammgYsPtWBCVElplmzDViI/8NY/2t/B71Wrv1QgdtIgdtQPgp/DZb6LvZWXR4j5+BmcZGqqL6dFMS5SeaEWhvps0UdZT8vB7KUzk2KemB2onRc7eFsSXSKxPK2ExsfG8JetS7rOcfBArl00hECh9vu68FmIxiWF2lLDafyQVcfB/HUI7Z6HspU7Oee9iSXxzuy6VwygB9d/l5irhePR5ISyaEODfTytW4j/MW6eq87WpgdW1+j/JQkGHj8qA5LbaxK/P4nxkeIc+8thTJkchK69IBmSgRE6mCsjYauYrVhdDSMKF8H+7t2vmoGXEEKn5ROFClio8MDpDJKpI0M94+ODJAyJzpqrW5u9GJiMPdgs+HeqNfddrBZv7ded7zTmJXizdLiMzH0vO9S4LCnJseEQuf3wTC4vxGxYNyiI/yrygvHRvk3lqb6KoEymw11m1jIyc74QMKiQH5OKn8hxtEMkNstz3lthWQX9/e+Fw6ciVHAY1am4ikrTegAcrmH02p0FBpOPB6aX2hcqSpH8YbDibMzxbNVMTwsi71qayTn2H9b0Gx9bQV+l+D84AC3mcHX15fn35oH8fXl0c31A1y19Vihf/LJVCEQ3CyMpKHil6+/FAb5FR8h1wguxttKV7lby/RwLw6vLg6eYNhnMNXA32tEZriZIrZg6frq5PpKqrT5xRo1/u8FiwF/LE0H/WqfGvpVwZKGp8o766gbMcujXORfp85mlH0lLlD6Lz8S9YvUyYCsX9yrRlyoKHngF5RQIXb0ll/zim4Ev6LyurvPM+alJSheXV4CZnscGEYMF0QhcnYnc1yeDm59qbvjjaRl4MZmQJZX5ycjlaGSkNh4sTvgK1crbUMWPwgGuMvM8GvAZvZmmoFOLH9HVn64BQA2zMbRX+g+Xh+7Pfnu+uzk5uwU+dal5ZJSdyo9nUi9E4CcyrKC9tb6/f1d4opBS4gJIV367k0Hs/7hgRiMsDyj6vbcNCmXEvw8TI0Ibr27md5WbMy9K+tXyckZztYATlCWppXOVls04qzv4xx2xS3Uh41XBE1Xh1QnOUd7m7jZ6DJW7hG5tpgoEJc3Ex6C+h9zedPUc7Mfp8kX46ODxYqxwnkRCAQu7JvXSi4SK8zLwDt3cTS/lMAJAQeGMYmRvkaIm92llPTIltbt2BgbI01DfXX4u/sAGHaSEO9sqmNEJ4aFBPALNgC4lpXkwtVU7QLgwpyNuZ5YSEACvDNTE8x6RWO2hrujYbCv5cfxwrP9FsBUTCJ7Se1wswGQ2/Rofl6GX3lhSDrHk03zc0BvyyuGRnAOSXJraqiScuv7NP+RWfYG3yrKF+XhIDRCfl4O1O2MR4BMuzvbYncOPWNu9FKsivTR0aGjIFZWVpx79+DbWxsiQr3vSgiY0KmJloYvomxN6rxcZiPDtmKicdhKdIA9nMaDy52LCMVTFXfjxcUFDLnud53KpYQJDeRj4OyMZJH7FdbSEERf2WKZJH9wbS1EgFK+gzCYAlUYl5vUwM8zFpr/B/UFxH+oyy1qxVd4VL3/F0qY/AJJKZ9gmOqcUhXA6gOUT4z9+DuBHQT87h364P2ThEPpax57tUKmwUdgTUQ6hrhi8t8of+c7xcKpuOQgxxeXPfja9kop6GLeEbZzvvThKz+lIfjxgLIk3hIv4eF7rjqmROaR4xdbmxvpKfHgc4MLCC5OKjcWJ+SEWhveSFn3LSqmTk8ujnZHqkKl0Nb35rhUxVnXJdrWJdrwAkwak+3eZTp1ZzkP5SPKRATA8l0HSn2W+moujpCizs3mJtXaRqVmSPeVjxPi7U20sDYxAMjRYf6qAXRsSWEWKT3HHiHOa1JWhs/E+AjxVLzcrE9PTyVdZcLhAcfpZqa7h6KLMmCDlNRqT2dc/Kan8zw12KojyyvM3cjRUhtArCFdVsfSUWPrqsFrcNoA4CE+D5ZajZfL9V1fkJ+th5jc7cx1T2geQuilmspiBq+JThov7m1Xu0jVE+lJjMSUKCP7aX4W86DA8dfVSWTBJlE7AJkwGqmKSkoScdzl5VpOFtZ6sjZ6uYWYEhVOSuSlO1nhxDBA+GIxqioMpqG/t6NYDCaSkNnV2SpC18HSeubjblJZFJaT5gt47GS3GWcq7q7V7awixg5SPwYvED19fzZsCcMJizsTvA2YXEEHVQCYpcMwkhxIiAp7e96Keaju7thaIDhqZ8nGMe2YyIB7o2GAItyczAUAgyNyr6uuKMIfuTmab22JF0bb3FgbHxvKSk8WSQpFtDpsxAYEL8wNX8TZmfUF+2/ERNFCfyl01iIPS008DIbxRkICSZD/48wkYG8yQ3OzuDij+IEGEBdfbuh8EYpImOBMIp93bzuekBfDLfERYjBpVF5S7XxOmNDY/zMI/8hrgI5W/ZVJH78WdgsZjvzuF1eJ9wTDvpnI7oKa/huB1sQ/po7eKzRh5tGIxLNOOj8hKWca+qcKRm/OZqhlN4Ti7o0mXR9TI/9SUDQZoOR+g50T+tTeH5FjNl5u3izYoB5Tip1NC5Ht1H/9Zg29exkObwR2LzpSNQY7PjpcW1sB7wSHv/CjN9DXBa/Hg9sRZ2d6T0ihEs2L0921wVJfKQrOo4XuI4Vu0MaK3OnMQ9SQHHO+y0hN+NbU++sLOoNoZQUF2WR2Wco8HNkCHoWYCH/mJSCCrXezbh5uzBJ/sU4kRZdXFeSmMd04AI2r0ZEyhWi4XK6jkDAdgS5dNX09FBwzZCP6SvgIpSyynmOoFmhlkORg/jbAW3y6I5e7HhOFuStsjDQvFhcI2gef+25xzmB/z8KnOeLpEllemsDAZXhIOXUa6SkJJPd1qaNdErgi9Hcs1vNMZ2uUEtbZKfGGvbBgYfQSVSHqa0yFBYvJz5S5PqfCw5FwwJBMOZXazs6Wq6OZWAyWyo0VYba8uDgn9DAiYEz35fc9nA1TktwHujMAcS19LNtbqxvrz1mcKT3ZbTreadpcri7MDjBha9Aqzxp0pqWRqcGLe0O70o2pMQ1YS9JmhCoD82HMfpyScBe94iZFYfC/vbUJNytSy5TGi5NyGGncOJLCKiJCPTTYS3763mE8NztdXJBpY8E21v/a5HZwDKkpstWgWRi+THawqPVyeRfoU+hm723BGg0NpLgpsuJ/mK00fmMG+QdDArC2hLuzxenpCS6eZEpNVJYXPiQtdnVlCeNbaN3vXjM/AowXEiDkk7wbolS1XZ7tnB8/sjTzNar1kGndyFA5ZBWIHfE/o+yh4V+nNjmK7GHyr2j37EdvdpSRKH74ms/4zXe3/lrJytFPMOwH2q4OqDldAZXN71M3CiU3HzQL9vAH0vZwsUSN/DZ/S8UkgD9+JciB/JP7k3FPhhF9BS5d285V9n3pkC+ihVIff0OO1ETl2uzXtwtYr78p4+4Rolj34kDZtZsiQrwl8rCx1RLtze9f332NKkn2l8YH8t1kF3cG2Pbpfcnhp7Hrs9Nr8JaGR6myCjmTx7jH8fG2xlqYSdzDxZIpF7a4MOfjYcdf4I9RJsUTs6wiPNjrblefnp6+ed1GkC3x3gzY6ggp8Xj3UqUNBQcA3DK5vRiPGDtYz22NNYOsDUOsDWNsTfNc7V75eSxFRVwnIVAq8UpxuAdxsQ4m2kZ05HC8+y0TK96lScBo3MfdFs703duOooJMkU8lIU+51uPJGEuCMZaSigSUenqojVt+8/T0BI6Yoagg63mRmz2f2mR6Wvx98ejQwUrfhN7th0Df+7taMgyr8XLBMIzWAp5X9ZyFMRMR6k2oPmHokvAUzFCxylH9ve9FfHQmHtPT/MrKRNPP08zJlhXkY2FtpuXtalyQ5V+SF+TjbkpHwGgAZmsEbv3rjhb8c/B3ZlrB5LdP87Nii6+YNjU5Ri4oDDDpItEnJ8etzXUjw4gcYmZamG1YW10q5VtvXreTfhCRKQf0QjIbZUSb5+dn21vrhXnpYcHepoaaJncKaPFqCPxl0SsjFoYv+oL8rrBGswjEujPNF6PC3wR4D4UEXCUlkTfLPBxxGDY3iwcHMDE2HOjrIpIqGRLgrtgjBr5FVl44iRHMj46ODpmLMnA7hf5/9Efn1aMyNGylU2N/hOov1sLv2XLJQeCN/DAiDrh5WKUcOHinEwqindNxgppuNh+Gk69PqSWnW5VgKz4ogPEDb08w7I4d91MbCSiSq8g4OxKEs2h1c8UWS2bV+XuY15O2IRwkSatVIPg2/qfC+SDL11E51o/wt1e6nsbJCLrdCHXrZz7Ddb9YvkUlpFg88wdz/eHqSkb94smJUbGEbCSPzt1M9zwx8f7F3ZY29FCb6b4XffXnuYzVRiyPdl6dndycn1FTU1R5peL17lwuOPG4Qgy8tOXb5M6D/T0EVDycwp7sk1TzO9oYihSbQc+D44gFmkUai/UcUJNMwICX8j7AR1vvmQlfSE1dR+8ZuHepTlYAujZj6Fw71LiCbCju/ReIy0t3toZjgGva19pIfDL8AocdZG/2VvoPobCH77rYm5Buaff1uMUbWVODpMBoCwsThjTB2S0kMCwzkzoW4yMeHOxjgpaHw7BXfp4sQTRsempctY+4o0MSw+HzVQhyWaEN9PVIXxSQRC+BWHB01YxY6ga6CHTRsbKvWFrP4AXhDNzcRMkXMGjxO4BSpEMjKQa9RJBST7cYkg/4LXtrA77GuouVXD9ESjFh0h3RY29magLeFOHhoGjCRjLrmTAM5ubZ2SlZdQr0db4HgJ2dRYb59r+uPd5Z4Tvtm2tVFUUebrZSpgYS3tBHis9Fbva9QX4z4SGCrEUeTe/BJTeuNl8PU4MXuqxnBmz1pagIPIvPEhPczHRxonVLUy2ZpxvrqyKTtLqiSAEk1v2Or00P8JtJhQ+7io7wZ+5f7BX89hiAvUVrRnTLX9rG8CnZ8hEIsaXY5RY19oeM0MIDINPVPjXzPeF5zfztUxDsCYZJHngLflTvDyH+lo9qivDJAH4b/CV+YuF+o4JDnzABSmEzvzkTpkFO/Bn6Vy5jFmWN/4lMXyGxvo9f3RNAOxlCqX1zWohhX9a1TU3h8UDPU58jvHO1g5I8vzRG18dKGlT1Ee7t7ogABnjSMxdEwaWwM9aiGedkgEk9vbDP5eHmfgnoqzffebo9de/T8M3F+fXO9k3XGyo94+Hkzk0+7ixBKYWI1OnB/p6QNTEx8uG9Cl4+qfIHJw98Guanhwf7hGEMe7SwTUJMCHicWIAIwNU9PPWCsrezpCSAl/AVaL4W7GwX2/HQIColRQwrgMwd1eDtqst6bmKgUXiHfu3q8rKhruJelSpmm59TvHp7cWGeSQg+hk9N5IAbGvurypjScACKspxthMiqrk7M0s3FORbIho3bRNCdnN1V5emsK/j18pI81U3V6+trolIFkMmYDdMwkiyO+Hs5nJ/fs/Te1/s+JMAtMy1REr+iJMKPIwGWLinMJtzlCtdPNjdUk53XiBOmY1aF4VCP7La0+Al/MSrMl6LZ/BxoRBcbGShSzQiolfwKwEvmLRGs530X/sjGXA9DULEGgC0y1Acuh5ONfktJQl9L3sLwq/2N+Zuba/i5htoKC2NtAZeghglD15vEvbGsvKG+RoK9WXegL6CyRm9XOieZg6vInE11cA0YjNU3KEiegqPWtsaamBumvrZc5JZelJ/xEF11uEGREL2IcmBuFve2ZHYG9e02cGmIe7NoI41pYzWAUS3v+DmPGZw3wqCGDttO8V2djiGtM6ZkmYyZmU8w7AfTbhYZYdNPRorsAkAIzhgc/GXEwqfIqJ3kE4wCGpSyh6tdauLPhSSk8tqctvBM5w2k0TPy8eE6CqkLI3WSmfc+OSAoKw+/6tX5xg1BlWgRiPd5Lv92lpAy8Wz2aTooEacxF+CD/FywGs/wUB/TnwCvfS4iTKYaGy6XGkZ72Jh4Pfcmb6qNN1YXNVwRNFDiPV4fuzX2+nz10+XK4k3vhweFv0ToyHgpPbmZRkYvSJqTCP5saqjC2Vbg10oU/5VxPef8PDk+nMlXMTM9AT+3srw4MzVRUZbv5mhO6jfamuu2tzYXF+ZgA1ylxmar9QX5yRqf4XDOExOnw0MGgwNOEuIRSnlw6f9kWDBmYHOwNdrfFxOIWF5aaGmqiYsOuteDT4oPk0RVJ4stLy+SjDt9tjpKNRQJ6HG5C1HhTgKuQhINq/R0voWs3ry5u3NcyQa9LSvolZAw9jbAmwjTgZOquplISBHA6be30n3dltNQW0r6B+sgz36cmhwfmZocg6uTyo3Ny05pqq9icvzgzFjAKsODfV2drfk5qXaWbPC5AZiZG70kBIbW5rpebjaBvi6D/bcibBmpCXgDEoFRwACsEtrSifGRuxskxglFuoYGe+VdPIJjK8hNw7OYuVokUigFnUDONz0lQfRJfnpKdCZwuqNYa2+tNxUIbBSnhrSVJTUVJ7SWJg625b1tLgoL8jDRRwDMykwnN5NTUZrvYGtsfBuMkVsoIvagUxZhAFsYvoi2NY23M/O31DczIFNALczaCIe4YbLbGWvhYf+6o0XkqGDSMRkyHW2NDg9lZfde+DQHkBV/kZcURQQ2ri4vmTQ89lb6Sq+k/bIMRYEEosm9P0atRUh+Rl4iAVU+cfT/g9KdPhel4fUxtd+EVueJP7biR10qegfezkHeFGHj+OIp0J5g2BdgB623tOSWnBTZyW6FMPaqmO0UCvgJ/5200NBhl5AGB45cPtf4gpr534ww8f++P+h8sYw44vkz01eiB7nCofoEYEbmMN3NxRY19AvC6fpZAmKXW0JR6Tmtp9mgLFtfWyGuHnhmJLmuTZCeREp35AAP0EZGhYv8p8dXu1tnK/NXG6s3H2eomjoq6aESqNdJSbtxMfSKMjjuyTf5BfmcWAMBSwf4f6MjAyKOHdGnjg73k8JOea+Njw2J+Fjg1Ab4OJFaF9yc7UzA42Euq2PeBXC2hoID5OhJetUcbc9RikIR5zo5yddCH/eV9Oqa0eEBcPQJZR/JNAN4CR48dOMDmQNhaDCZAMA9bcF61uRkeSm5Lrbaes9uZ3Wq8RwtRVcEPokSx+HcLdhns4+b4jCMy12MDCe60uD0qygADo41TpMDnx5gWO+b1MWP9c52xiTAAlBKEnfiXYVipsE12tvdWVtbAVgCrz/OTA4N9G6sr15eXohUmh0fH5Eqyo72JoWXdQiVCxzzyZ2UUUCSJFgHc+RIXFIrDMvoCH+RILOYZ+zhAYncujmZi5QwMe9srg6md1lMMTcM5moXe1nh4LFGNo50FacGt5cntZQktJUlJoa7GtGah4YsNStTrer8hPWFycuLM+jVtuYaQ7aGgR7KAjURB8lMBdwecFMltywM1cwNXoRbGwVZGSTYm5kbvjARLI2tLC/29b7vfNUM16W3521OJpecGrTQQI+rS1k1ncKDvYQ8k/t7IoCTNADkiodYTk+GB/vgIN+/7fxyn3x7tUIXa8FcsuOxTc1qCLnKDto+2wFfblJT//2WD7wWquCuLlZvcVBP/40iPI1PMOwHct3+EvFk9P4oI4QaqMj4w4ycfT+uoPDxzbmQuGLRVtqWhGxj5LflHuUAqyb+QjD5f+h6M0uG20oln1QQbhZHbySsf2QL9vmdm2V/OY4HUCt9JJcTf/vZBsA8m3/wfT+FwpIPMxmLpr71RiSbRKrVmfwT2PdFLnJqmhxgoKiEKiikcvKpzGwqPRPxznN4ytE25fKWoyKcTXWibU1WoiLgqGYKcj3cbZh183fpttNT4smnn+YVD6iKyBmLbeAxY1UuYosL8wBjELuagQYqFOFyVaT6Kgu0SHG0wuyL2NmSDi2Ojw77e99jlo6x0SFwB8F7UxYa2dnZys9JxVELgDrg2mY4W4+GBSOugtTUk4R4mlBEA4NnLGQEzmuEjbFoB5aUiFAsZmdwcKJXRVgAGreK9tVSVLiZAIZFhHqrYg4CQHJ3tuCvd+g8S4h2OttvmRlvigrzkyWrEHpPKTp44DqTfSrMCQlIjwRqYMCv3K7SBGMywUjKecMEpHBesx+npfwW4Z0XG1WDOzwRhrYw1rwrewXjmXSgWNp6OBecFApwy85cpy4/pq00sb0sqSwjzIitrq/7HP46WrOK00I6Kznw/vu69LmBltXZkY8TAzMTQzNTo687mnlJUU52piYGLyRBMpGgmT6Nzdi39egJpaHYVlSQKfslxnMNrhGzShYAp6erMC+dmxgpcvuS3Zgp2Q7WBrKH6R7btvOE7pkkpo2zj9TQrwnTcPabPtvRXu3eCoIpzACHHMUqocItNPBmL1aeHKEnGCaPLbvxZYtx20qTew/H/Xx2wYFfQK8VceHXENPovUl6lxtC1sSZ78m/VnxKAlyyEpIuuQhC578kXrbrfJHElG4m5eF/hy/OvkQwb8Xvs116gKYEAEP/35urKfk5ERPhD0+dkAA3EWXPb+HahQR3Gb//ofsNWZmGZyeulccfiegR4fKw94E+50mJD8okfHij8/QCrQwwNRmAsWZfdztTbaM7Xo5I2cnc7DRxEBPjQhXuUnBZEmNDJblEIQHuvT1vj49Fi1dHhvtxN1oYvljBhfifqwN5KcVuDrjaysvN+vOuR0xNjtVWlTjaGpno49zXrwEf6rPVA6z0a7xdi9zsDWiWAg9ny/OzM6zHDX5qOMCwu6VxdXUUY7TjjQ31NTqb668HBhSOJV5yOJnO1tgt9na3PTs7VW4PAL7FIRcEGrWfRQTbbq/Q6l6bfVXl+UYsdVmQWGZa4sOPhCQTujqYKnyagKuJ6jTcW9bXVkTmDimqhCaSEkksISaEn/UaJ9HXfP+2kxTOpXBixG7T3togAISaImSJyB3d2yWkJmJp6ycnRvGnBnpqQV6WryqSUSisNLEmN8rBUs/VziA70be5OB4AGA6RtZQkNhXDi6TuxsyhjpLp3ubNpenz06Orq8ujw/3ud6+z0jlJ8RGebrZWZjpyceFIafAskySedheD2Vmy70paA+QmqwDQKkoVlDy9urzMy07BCvXMSF33u86+3vdlxbkPodeXYQEgAoWtdgpkzda52r9eT7pZDaOOe8VvcNBGDf+mUJr1fOEzPcIvUNEaroUhB6NYUG6Tg75L9gNe4m7ZN4h6+gmGfUl2MkAN/3MBkfqvK1IptJ0roND4CwVT7BCe+b9p3b2flqaRddiJ0o4xvemh/AF6AJlzmtSWzGWy1yfUxH8UzNU/RDjwrpE1FcBj8oqjn4xQV3tyQUklX3qAzb3f5R+/QhAaHhVYkpi09tb6RxizgG3ulRFThUn5UXifxyDdWl1ZEvbS1RXvDmkeQAhtvWf9wf6Kp3gpKRRWI1A0piV6UXqP2HoM8L1EFrmZzBkP0YA6OTmOjvD3cLEERxM8GzsrtpujeUxkQG11qSRUA4jXlC7Z97Fgo1AP5/NBWR4vz9VOl870g+OXJF/7CHZ+fo4TNVE6luELQGJGbA2Mx2hug+dEvhbHaTGBBC0mjkpobu6e2ocPZOd4Y9hV93ua6q2kRGHU2h/kj2EYwHilu5KYSgSTy1sYvxwfzD3cagAkdnY0tr+3HRUewEw/A4RjbvQSvlJTVcJkzIN3xPfwmRwEUaRkKDkhQvG1sotzwvfj6WIlQitCOEtxeFMS6UhDXQXexs/LQezta39/195Kn1RGSaLrZHLAVFcU3b03kvxJscw9JO4NYDgj3gfDMGitpQl1+dHw4lV5cmtpIo3BkmrzohEkK0fbNPNbIrz/tjZ1urt2Z2n8/ER4kPt7u/193bzkGDcnCxt5GHFEGoCcuys+Yg1u704CAFxalE3e393dYerOPWRdpqO9iUmuiwPIzPaQcXXf6rwr9gquB3/15loZCyVH74UiWgM/99lyEcHdYsqUDfw8Ymu8ll8/4GwGpW4xg2lDv6ogP4LEHntLbSQqn6z7CYZ9uXb0DmkxEyQmr0d+c4YIA/HXFWNNRKsvIQKdu/8mrc6KpNJN/X9KEzWWNt+mEDLk03WwxGywV4USMvlMJ8Yqu33sUwtWiGh+t0zJe4bexge/HqPAt0keC2ZwxrXId8sGVITE7n3nEX6U6TARunB4+pJOuLy8yMtOEfvUZ7HUhFzhnw9FpDpZ6eo9l+SagGtF6LDB1WPyp+3t7hBfJNDXWYRaTd6OBTC2sb66t7cLTuG90QNwfTARBR3J4XzOiCKXOxcRakWrGzMllR5f5m5jfY2QKFTGWjdx7DxtdQGPodIaVFfDZGxDBBUVpflo5uojKHspFspyudQ2v2CdcK8XFdOl5zMzCo+39wE+Ai0EzaXFT0rsgbdd7ThCa8xG59tUE3O03QgYDNr5MT9nbPbjNCcxIiM14W3Xq52dLTgAzMOB+O4Eg9nOks1cRtnd2W5vre/qbAUgFBLgDl+HSZGeknAXQ15dXg4N9rY1183NTpM6KymayxIXJo6PcaERTApyVD7utiJAi0nEl8qNlbS3YoFCHdyc77LIwLSNiwok2XozUxL1zWBLovUMt4K7yn7v33YKAnc6MJdFPsV5rSb6GgB9K7IicNQLt7bSRAzAMAbLTvS1NNF0smEX8IJw/RhpCI8Vo78dlZyxrvKtuYGj3bXL81My6eAEez+8m5udGRkZbG2pf93Z+v5tR0VZfkiAG/wugHA4R4DfHi6Wvp72YUGe4cFeABqD/V1TODGHssXB4NKQwsL46GC485OrX1tdyrx5jo8NKXw/JHWeJoDAzfXMUG3bLSTm7W6rMP2mVIjJ4JH/ZKKEHZ6OChf6R35X7tVqJXq5o/+KkT34Z4ooBl0fIU4R4i1jAAY9JjtXtixYccGCvz6+k//QvV0s3px9or4Me4Jh9wZG+oRSYDDI5NU6OJ3kD83Rf410ihWzBXMBK4bkmgGYw4Q8AwDSWoTKewYVidFTou8nxYcKAX3xj+dnUIKlKrz/ow/CaX/co8xdb2UIQuq/iBQM5brmpye4Bh0AWLCfpYu9PkZiWelJXw71PLhTgBYe4Yc+zc+SnJyWJmGOSn1tOfPZGejrUlaci+kZTOhlzhZfdySz+/miYd2BPvq3RY35Tpu1QVN9FVxKIi4ETaRMnFnzphTyehmtIDcNM0aUuDt8ZhxLR3hSHa2wHFbP+8+mCzQ00Eu7uV+bGX79KsVhstijN8elK92xOMoyyJllZfKSIDHMFI/1o4WEcuIrEosoejn/fXeXaG7qmzeKwbAPgX6ESmF4sE9Zpw/Ih8l1/qYt+XinCWOww62Gxbn32RnJ0rOmYbSTwUzKqABOiEg/MSn1AJvxl7Mmx8CPx+V2uD5K4UAxDCEXexOAgplpiTvbWwTOJcWFidxXCT8EppiXcJc+BchB0iPvagcDtHNzMsfRtrGRe1bfMXTH7a5mIOyKMPdgqM8EqHDrQw8LtrqzjX4zHQRj4ivccKkYBtL6es/tzHXqC2IIQhNpgMeaihMAp8He0hKCy4uzT/Y3pQAbwE4AZkZHBqBX4VAVIxZiZj24OJgSThQ4QbhAIjolCj8HAfnjZFSYnv6W+ufJyZ3+nkY0IytTCx7HJCfGRzram5SzorEaKPQ0hv8FWv99kF0jX46UvUz+J/EpRao28GbXwoWJP70/Qq24K0LPeNBOjfwOI5j2c9R6FCozUyZWfC+UcRr/Ezmzpe70/tkCnOn1/pfC7PIEw2Sw80+Iep4f+dGXe5huxAuY93QVXQbYRWAAE54ef5C4GSJXFIggf/iOggVpctns14KJ8adipDBQRiUdMRv9PVUx5ACyxQV4iMjEWql3qC10N+GLAbyU66vwMLMy1QaPB54HcxNFr1uS9HWe4UXu3d0vQi6jprLYwlgTnI+7JMVKxsk3N5mpicQDY64EE7+EX0YVG3p5eUH8J/BH7Yw15yPDhFgCy5Ii5jrOowXEitwcRKrYA3ycSEAATocI47g5mjOr88H5IE4eNBFCRdUZuNQ4nJjuZC0T77+KOzDXxRbDsLzsFFlGiyr6pL+vm3ZzNVystAGADea7QhsucBsrcu/LdXGy0IKPMAbDmVdvu9rxCPSVntj5qgM2zstLx5eYkyhY+QJ4VlAgfy0idzMmylIQPGxvbVDW6ePqNWi6L7+fEOUE0GtntRbDsLP9lqKcUD3Nr6zNdKRU/qwIGP/h77s36Kyvr69CAz2kZ7JlpCZUVxZLUoe7G8K6d9mIKVPm7+Ug1DrzdhRh8EtOiBAWNUUGiH+qC1AWplI8ESfPPTM1AacgRe+LGJPXVCyVBYmaerpaM8c5vA70deYXhnlavipPFousXlUkZyX4GtKENzBCLI01a3Kj2sqSxG5MvuLnZs7SeQbfSon2GHldsjL57nR//ebmVqTo+OgIxxVfd7Q+/M6DBwkTYJcV5zKve0tTzd1ooeyG00wAdOmynr8L8KZSUmGGepqzDPU1oFvYgrxxeLSRZb6EmJCHTqHDLiFn2/BvoKKJhy5hVwlBy8SfK75A/6Dg0gE183fCw+j/RygsJv8THvldgN+YFWUnQ0o+1INmqu8nhPzVCmRLih71Z1ICeIJhD5s2tQJRZoXi0SQuBJBMwVWgCv5YH/kdaQIOTHrQj2oPUj2XcSaP/wcBVlEXH1OCaXM6ocJjmPprRhHatDL3/PG5gPjk7+QNNAHkMNB9Huhtvr9et/apwtVe31APrXM31Fao9IJMjo+UFmWDQzk48GFudnpp8dPe7o5IUhz8yyyVTowL7epsZZbuDA32NjdUN9VX9XR3TYyPPIQeDeUOCYCKn5cDeQB3v3vNrEXBSSzX19cz0xNMpV1PS/Y84plAxOLHCfFd/l4Dwf43IlpeqkQRM+EhJEABKBp8souLC/BEAS7iLCYizwoNHA7mucO1FpYrxIc/wl0Kuhdn7Oixnhe42n8J0bASdz5LR1xU4Oe6eWOBXQOWeqAza6TQjWh8w+vGZDvM2AEomiR89ve+x2UnHuZ6F4mJEgcbl3s+Md5QU449v7AgT+FPrq3JIRUgpPhPDrA0wONNJGaisAH+x2QV4OUHeJsvTJfsrdeRUNj7Tp6F8UtjtgYAEikrRGury9bm/4e994CKq2vPQ/3Hjkvs2LGdxNeOE5dcO47jFV8n9rVz7djXSRxft8S/fwnQJ1FnaAMDQ29D7723GToIUUUTXaKpIwGiSiA6iA5D78O57z57Zs9hGnOGoq9or71YMJw5c+acXd7nLc9jTrhhAJURaKd7B9SUGBtCZr12DQPVBua7pjNjhWUmsAFIrJB5sDfreaNcnQJrIE7fvTyVDvlQooDnJrDc2FB2O8JqTBQFYXEmrwPGcxVw8QNKjvQghWFqYZgNDcOsLQwjAwSaAJvi+PRgWzqYD8eLE/w6qjORFlll2tvHxdODT9fnP9CQ7Ex6eupOjxAmgS3bRsrb3J2sYG0nr7/pfs4sOHzxrP0yN3ludkoWCrM0qvH3OhOJAYa9pnMWYA662Bq3B/t52placo3Oa0NfdipJpwUyHdSBX2YtC6TatpoVHt5pS+p4/jOsiVtNKD9LkYj4Z9TOM/lI1hGinFGSSurj3zGoOH4R0UKeHV3xpW6UyIIQWL7s29i+wDCd216vIibGlsJeuivLGOz9Kf2ZcEjt48e/1Ub4MW3BQGI/uHZHy1aL4uPWiz4HQq5XXMAn4VWe+XACeaoA9x4Ms90tYMe1NDNIiHLdXm3a22htrU+CP+VQ5Loog3Z3dwjmIY5J2BcDfV2yMhMBVpH0jBB5JQPpro7cxLjQjfW1leVF1X+BcV9ZVri0xJpwdmJ8lBhwzOL1+wVikuAnN+9kz679cSOx1bgcQx++xWpqCpWbV+Hrftf8B4DN4p1tAZLdDBI7FWWmuNpZyokT9vf2AOhi5S7sPIZXCPlBfHSQ0tcntSVgC+pNzawH6OVwDB8jaazPHw3rjwzFJIRaGA6uu+G6FIBhMT5cAsP6H/gMlAhDPTjWHASiaqtKyfE41xRgmD+fow2GQS+63/agAMdL46LOZ4x3drJl5jxIS/PnW1jTRuSVcBIyVexg/XnzLHtfIktHlCw1rMzVBfvacU3RutTUUK39PEzhNSVac08Xm6yMhJGhfpgahIxRtZDy0/wsHENe6Ww/Vyw9OfFROwkEM+tPqedmpShPhL09EunCMxcWHxgGiwvzO9tb1ZUPAs+H4rXUfVG0JntcdODa6spFQLGBrLrwZZX+e3x8TJ6FInBK+7xkbBMcw7zkwC4NMKy9KqOmMM4eJbobAmyuyo9Wqg1j9rZKlKwodLPC0TN05pQAAvBQCRnd4feBrorJga5gX2c4Jyxf+u1Nr152kRX75fMO8jpsNySACdBUlUOSbcMiBGgL8HQ4g8VNLN5JSw1xtMREO2IPRyo3vzPEX4AUO+4QrUXVHFHW8+hgHFGRffwbfYqmVO0lzKbW+xOfB1ScblKTdxVis4iFm71YPGCtObdzZWAzttdCSSDdp979nEz5ST8J3y8w7NvWdrtlU2jgV1gzHypYE/+YOtUrM+1kmRr6LbmYg+bSr+0n55hqpi2v956cnVBzroraucOxm34o0gOFX2fe/VrOz9Ya3tvzcuVZmRtEBDuAubO10jQzVunMN7GhDb4Lywz0bjPTk9od0gAGCnLSBwd6nz9tw8lCSiGpmAj/ro5WpReZ7sziwixWHNM9b16SjESS23NwsB8gdMb84ITrOTYygLyr+9VTkssENm6os82byFAvOzNcA2BqcbsVSeVeCcagdZmho8CRSG08p1zoRvgSH9VWgLFCW6Kcvp7X+GpfPu8k1h5TSZmik5rI3auvrbiB2ZAYG4JvWlew/+dPShSJTjMyAh04YCfxbe4x2R1usuE6PYBhcb6Ww2W+JBoGMCzA1RzDMKb5iIWe4JoDEUWHVhiWld0eGoCHR2ToeR/QwQF1/z4rRpOlxHiayPEOzn29vL8G5pGcKMggyJe3PFsLyxEJhQ28KcA1q/BoVHkjVGCYr9o1ATAYkyz+xbN2QDjR4X6uAi5MB7DCvd3tHpYX4UohAlT8vB2JV2J1ZSk1MRLH2aYmNaYzEDoNpQ5LlmroCVq2KEn1YLgkJaJzJUI/1YZDqbokMgDMIDAMhpDqAYSRiFk6Rej74bmXZoVrAldtqGYsPdSXD4eF+zloqgqThcKqM+9nhNjQeJ5niZIYK3OjNJw5vaU81cuZCwcjV9EGa1fRh5FBUu+XHB9O8i2Pjo6Yzj5NXP8s3OB7suRJWNxehQfRCeriLA9HPPtgtg5HhaNNITvnTXgwdpzBfpEZEbC6fBV16VcS5Fm/T/X9NF1d8qM05f2NN0A143+vMA57f5Jay2FviK5T498/F0k7/nR99iUinxv6bRRC/Pa2LzCMZVuKQWwteoxd5B3ykie5fV9PjvWjOWr4d2TzR1ORGOAiIuiMUvV++irJatR/4rGCV/D9H3yGROeVdCz6TK1mf02GSUJMsJW5YYi/3cZiA0ZiohQfLl0hhnMkLlMGc3p6uinZWFpa2N7aPD05gQ0PW2wLn+bIjgimQFSYEExD6O7yiBPpYAYBBMLI6nFLfXlJPk6MwTYZhmFgsuSIk9NTooUe/HOSwQHu9XWVOoKxx82PCMoiX5not8KtmBgfJaYJwDOGBanIWrS2NOIyyOLh9wQXnkYYJqKRlU6xMgQSFhPjJ2Kjn4UGqDG44c/8gvIQXw5HmS+RmebEZAsozMs4t+tJT0l5uoezNSsccnR4qMcgwSgRrJD+yLDPHw2jmU7AVOLQXvm+3u7PMhkxbYmlhVF6kM1IuS+JhkH3dzG35t5R0v/FEtg4KfEoPU07DHsRGoi/HdOPIGsLC1Q2i7xQqVic7emIY2sBQqfLEGxCW1tdwXMcEfRzjHpe5GKGehwK25e0tjel4hB9Xnaa9pEG/yWVP+dpdbyWFj+pjltkcK6twgIFEGuXEQLFcsnMabKyvMTU8y3IUR8D7Ot5TYLqSl1pxin2hOVFwoyvpaclRWlZysD0J5eH6Vu0NMT54eVIErBVBYVnpifIQlFbXYZfLC7MktMk3q0uiNVS7gXQq7Ui9dH9+McVqRfAsJrMzDghLki2NDcI8+W3MRgXVYJssUjw3fIO3GG2MKy/7w2Jd90vEGMSS4zBcsUp5CZ7u/FUZa/ZNllMG1VsWh5lZMAiv5eW6mVnhnYHDpL4k+J5mpWd7+UEr8B+Af+yt7oLO8vTzsef3yZYTpTFoMAk26j4DBcASAYgE7EMJ430KeJaFSlUzkb/O7VWcPVZiNjIOd48O8XE4NJr+ogvMOw72aQH1OifXyzHfMFcamSkJmraNg+pod9UzLdLVpTqQgp0OE70mmkJ6RvmAzyjVtKkk2YoYPj1aLC52nKNBPYm48OlWyuNYAC96863odmxvVx5bNMzNjclkxNjz7qePCjKBjsb9ng3JytAXGDZB/m6+HsLwoI846IDA31dMNMg7u1PZEplW1uS4cF3sE2qTRnCcQAwQwmbmRLR897uztTkx4a6SjcGnAvyc4XvODR4AQ0M8UlXVSrqpuB3AmZg58bQEa4cviPzvcRoUxUWc7S+NxIdoSbaIxIdpqd9SoiDn8hdqh2MZWU99PWglWfURdjE4tPc3Gc5ogBvgRJZoq+Xg5LpRnSBUD4Sw6Cn6Go9JmkBCxh2dKQfDOMhN7DRWEzk54+G0VZRR7Af9ljDKCUc1jfZkuLDMAxLY8CwgRLh6wIvV3tjGE7OfAvJxjrTggfjElebbKYka4VhWa/CgjAMc3Xkqimv6utjFRDLdHfg0mcD6/+S94rEmbmmt2PDnTeXGzEzB/yyOv8oPdHLyd7ElotohJRCuGobgAocaCWKyQBuidmtS4MzkJRpEixipimixM7oQLXxKGzr25xXF8C9To5nVNvkxEctAAzOCTBSlaee2Rrrqy7UgFaFCrir5iVSDApHQCZ4tGB2QUBBjjzMupGuCYN1Vmd21Yo6qjPUUikqHdxcmuzrbgMYzENgAUBLY5DtYXpTSZIL30yXkaAkNQH7C1HQhgfHDN7WVCkySN0EllfCVYiHH8y1p6EBaG3PymoL9oWFBRW/cY16IoLR6k1X89rTnjuYvBECKxvaeRce7P253fexCntMUvkZLmD9PtXzI4pr0IPPTLpPTZkqzoDoCreu73qlJ7tn0pPvCDL4BsCw06N16cn+t+R+I1r5f0GXM/4StT+oJ+TArBg9P6zNp7LzHMXNYOLBz+MF/S94+zE18G9Qst/ZRQKd896MIrHC7zjiftr5GO0ZJreqy6L2NlolSw1LM7XeiKjDEKvH9L69mJVoeOhdTIR/ZKgPWHh6aG7CTq+Ua7S2utLSVJsQExwZKgTwxpSspehCfMyzr5ZwAo2F7a1ccQoWIFKQYmnOsdzb2yWRNELDBdYYrpFwsDUGOwlsTWKcve1+wXx7Z3uLplITc4vb+V5OqtGeM5FI5O5gyTWMEli/iwzTVkImFk/GRcP1YxiGt+0tsLnpkgMqN2/n/v2iyEAO/FdFsjk9Wbmgf3pqnDyjtseNSgExsFEISdq11kednp4CFOdZ3eFb311A1Cbiz5WLqOjZ2Z8S45B6GF2tATfqhmfiwcEBLhMCGJYTbkdg2GCpsCPbHas5g6XIVEbCrHcAw5xsjNeTkyiRWAsMG44OJ3zZatSQjo5YpCaKxcnyQsRLltLNzU5jGh4wrwV2xsO9RTgUBguRZKlRnCo0v/cDWzmTgY4UeasrS4Bb4LSAJT6MsN68iHYW38Z4blZm7i8vLRA1C7URxYH+HrzgWJkb+HraCuxMlJIMtRMItTbVwZoG14x9PTDfocO9FWckAEzSTtm3uDCPVb9x+oAuqo+wYpNrU1tuV19bQQ7IykyE1Q+z1eNoWE0B4KV0tRmJLeUpRenB6bE+LWUp2sk5mKGzrAS/+geJHVojbI0lic72pjBOXAVcpjNCeyNpDjgXkXlzRj8Me8uVmuHhXr4kjKKp52l9MFio78zFx9KZ5FlpbnwuxxB6pMCa5tHN2ktLE9qbW3GNzCxuP/L3agr0wQ4guNWf0xpYy5dDoH9ErWTcuAEtoZaiGelR/1Qf8+xwHFG+MVGc9Ntik3+BYbrB4p2zrxm/5KXaXi/SncBI7ECv4tG9t1Tfz8jKFnc0q/GcbqI42CW13gnhhxbJMtnHbcu+Fw67Xys7onpf3enXZ5zs7+3BFs41vZ2R7H203Qamz/5Ga2NNvIXxLWLWgymgPUtNLJdhIZgHYAmY8mBqBwidAoTOAKVcHDjwEzpxTJ4T8HG21rQL4koGsE4wAzUBTsWFYgBIESHeSluyVCp90/0cbAslBmotuTr9796SKwe7EL9ItGUImMGGCE5rUbb8VpeVSEdIQAxM5MXEeKX41aeEOFwSgHfiTHeHU03s9llZPREhHK4i25DLNRR7OGykpiwkxj9LTcxOisJRDtVOwozMRgo/wOhXipUNDyqMs6L8zOvTjgPjmE8znjvbGEtStOKHKwZdYtTFWdhLfZCWepyevpmSDH0pMWE2ITbIgWNNE3XAXboMXbUejVaPMMOiYU9EbkOlMoqO4TLfulRnTFXv7mTFpGvHpHY8ephNxEZpCyqKxIDT4G7jjNkutblPi4u64uGsrBehgTgaBtesC0+6es/70gIhqOCY3KoqjTyQPMYY7HDrycOSSHPjHzDHsy5aAiQegnMO9WiEmTDY3w1HTmAkxET4M69EiU5TItnAXwQwmDjVZ2G2I1sUj+vZcAcwoItvAq55afHT9NTEzPQkdF0SqtfWVphVZI90K+zc31dQgyTFharO9JOTY1wIRxK8sfsGwTBrlJTYUZ3RXJoMWIsJlrroJEOu2W1LcwMvZ25FbtTTWhETXD2uSNUUQNOevthelVF3P97BFiUlwsKuY1Ii7AIk7SIjJYYZtu3rec0sLSbycZdsedmpONc6ztkWZSSKxPtpqZ50RiL0Z6EBVHbOclJivLMtLOOw+Ic6Wh5kpBd6OWtMGL45DJYr47ju/SdIUuiG29G0glAAse3/BrXTxfok6/cVfPE9P4b+/NK+azDsW9i2HyuqG/XL31vNVCgJUtJrvNRZe9kHDf9Hnb4Xkdd4/18uDqB9e2EYtPSUaGsLw4Ro15ed4oWp6pW5un1Ja06mHxhGOCaGmR6YKEipKdVjwPYG0Kuro1VmXpycgHmxKdk4otv62urIUD8gmaqKYiLVpTaoRdr4xw9qcSC8qGpslWsokddiCVWWFcq4FuV+VrhOwpXf2lSHDyOJi5GhPqo2OphNNQ9L4BvBf4vyRXFRssgS7K8id4djOu0NO0epnNxmufsTQTWrO9CXlaAaI1luPiHO3uou4dTChQdutiYA8Mw5Bhg2qHZ4CmDPqX5ZUsrvZG+mlOkEfxJyOR93u+Pj65KRaGmqxSjUk2e2f01kkhh0IcRFgy6RGBCXJDlpLTlxMCqsJVBY6O3sz+cEOXDdeCbuPBMH67v2NJ4hNxAQvh6Um3q3yQmEqRA05d17me85UCKDYSPlvtlhPEtaRgLGJNOdv7K85MhDSYkCm3srSQna0KxYvJqU6ERDXytLo5YSDW7m1691J5bEdNsoT1JfjcH+PtlQhHWGb3N3fKR0a6VJsoSSEhuqEQuILfdcgFe/WMH21qbukAzmNSEUJTpOzBQ+VaHztdUVGCo4FxHl+k5W7W31v3rRQWBYVkaCfvent+d1ZlqcUtRaqTU3KtQmYPnSnW2PKVOhthjyw8igGr8S18hDwGktT+2gi7Ue3Y9nZh4CNqvIicLq29YceKb3MmJ96ori4OCuGlFnTSa8US0Su7ADTivKCMFxUUD+F2qTHBzsM7lP4Jkya//29naZapCXp+UgH+rhYsOjPWWjMZF45XkfHQHLNQCzEEdLxK6UnX3f29nY/BaPlusYiAyjcvPbgn0xDAO4q58m9WUbqgeTQyDAYzeNAAuogV9VCMmO/X/6kOOvFykw2NC/o3a+LpLHX2DYl3ZJxHBMTdyWjexZB0SqwbpJFYoNWlgTL992nim0+ZZiWMC2GyBp/No+3rOzjrYmnFAHGyfYDV6uXD8v69rKGLCE6qvi3ATmuDgel5Ro2vwG+3sjQ4VuAkslGFD2IA8Q1IURAN1LGnRpoYEeCgkgnkmwvxsOza1o5qEi6j152WnET49FV/k2xsSyIXEkJMN60WZ5eHhAMifBYE104w+kJS1mpH2MjZqMiwmQR10IrCrycj7MVFHgFYv30tMTXXiYSx1v8DivzIaWAT2nd8QzKS/JB9uRpNlsqrOPt7e3sCMcHtCb7nMxagDMRFMITKjLEyhranXVZTisl+TKu+JQGCI+QdYPgK7VpIShqPBXYUFtwX7pbvwAPsfZ1hgQC9xtMICgwwXATzCAOHShPBPoqkoXXHfD0BQuw9Xe+HWBFxOGVSYIME1igNCJiY0/0VQ3OBo2flE0DCCogIZhFhzDAh8Xal4dadjJCVVTo0tSIoA6fDa4AF1KttQ2Um7kYHuvuS5xe7WJJgpqzEjytjC+hTFYeLAXYbCoUCc3rKWNDPWX3M/xcuXBGXTkEYExT+InWDWL8Ao68c2J0DmBYQBgcGIbwh7cOzXl0esL9bMTTw/29/x9nOlH5qwf8ebmpoQErLRkY+J6QrUxugtM37UVkp8MJ1Et8INXyh7kqwnv25nUFycAsmosSYJfcBTrMQMvpcd4AwaTgTEEsO+F+fJzkvxzUwLgLa16wTBAcYnh7lYWBhht7mxrq/aBTYcsgziISvxNMH1gomH+W6IDeVWFoJjwE1b7WGfbU3otOs7MjHO2hVfMLQyqfD2o7ByYOzHONpZ0KCzfy+kM5ShmE69coK/LJQlv2Btohyj/sPcn6AjSj1DLCTdthaykKVjph36L2mC/5J5unlM/Gv1/L1Xe8qV9gWFfP1P9UEH6CWNdj3Y0p1C12267xkudtpLTm/4Ukue7YOpuIZl5MnW/e76TmemJpLhQuY/zDu6wa0I3/er7tRUx0v3OscGSAB9bgsRU1UWZiA6M/oF3PbVVpWAsMnfujNQYVTIuhak0PJCVmajd48uqFReKmVAQLIwP74e07G37+3vebjwlrmfCoA37IinprqooJhpi29sXV/0ODfaRvBck3Glr7MI3x7a+jUodl6nF7TfhwVRuniIxTCQ6ycxMcrHDOzRY2w7Wd8sjg/w97JWoOFwcOOWlBdjae/Wii+QaacKKgDY1CRkRoxOD0uvQjoOhAqAdazdXCN0vq92MCSdRpFGmnT0cHVHl5xHkwHW0vkcL9Rhi0IW7mcVt+GlJE1q62Br78y0iXXmZIb4JUYExEX5MDA93lWSo3kDDIVkb7h03e+PuQq/+Eh+SlFiT7GRFwzAlPoz1tVXMy8dFhJOh2ggnRaKD9LRgBy6+IVkejlRpGaU2RrS0pAur5GhMpK280kyXjDs1LpiNNUxowTW9nRLvcbj1BNPTN9YkkIzo8GDvd71vCOWGdtEwTbcUl3fqmMY2OCAjR/VwsTk4OIB3kbBJzcMSMjtgbNRUlYBNT1KXOSa3UuM9jrafpCV4OvDMXjzrWF1Z6n71bH9vT7/x8PJ5B7N6lsnOygSNhI4IvqOWhAW1jfhcoKsySVJ0lR2TSwlHw9wczZtLk9sfpreWp9QVxeNUw6bSJBzmakMxsUyATNZINwwz0aPIGCAo2FnKsiO0FIBpj4ZFBQisLAyVyGxVW9mDPOYFN9VXMf/7gUFEhPNOr0omUSLZwCgdJuPrMJqnPisbpglGXD725jupqQDDnocGWNPUiM42xnv0K9AbA7xxNCwsyPP6UsHVtOOFs+Hfk1tNP47Eiq7EYtSREgMOm+YqbLBZPnXC/lkczSDWaxJJm/ekpHvUl/YFhn3bmvRAkba7GKHPGQj9zuCvI9fFNbXDj4qoNHRNRPkKA/ydgtJ05PevN2fya9ZammqxAWfDNQIzyMnexNH2Huy4znxTniVSUI0OE2ytNO2utSxMVYcF8HF2YsOjhxfvRhvrA/09RA7Y7hKGmp5gfGpciUrR18thZnpS0/Efx96T+A8xu0lCC5jm5Mg33c9Jwf2KbhovAEqZVwIYjEf/VM32gY05ENCUr8d0XIwMV2RlZXs4cuV1X/DG5+0tZ8fH21ubnR2tgGSiwoSpSZFgdhBBJNjCC/MyZFZjVYmmqyJQDUCmUuYhqcXHFG2b+uabaW+YC4TDMXiGycT0indh6LWbmjoTH9Me4l/s7RIpsPa2MyPBLtzBvsFGD4CQFFc7OKwtLHA0O3O5tvqw5+3Z9DS1uXkmx6tS6SnR/yWVdXu7O/C4tYv2Xr6V3M/BomFhHpxBeWEY9KEyYUqAjRWd4UaLqiug9dzsFKaFgO/YEex3wZ3Myi7xcUUxQI5hhhsfjbHHGtixu7oufAS7qSlCe3Mc0S0uFOvxfYkOFaw/lQ8idtdbJEuNgMT8PK2tzGVjvry0gFRqQe9oa2L1EeS9AF91Ia6AVpiXyYx35eekESnC2Zmp+bmZc5IYgR74/sPyCEvo5Puynpe5cPG23DsRIZelvCNKhlhvXbVODFZapsJHamIk24941tWmVtmC2YiEGoFhrg7mTaXJiFm+Mg1Ft8pTMBJrKUvBUsu41qswLdjJzsTS3IBklgIYC/C0pQ9QT8IBCE0tvyK8WJkbZS+XqtNUInhycswM38GjedJazzwAVjOScYr3BVYUmtpbb89rHC20t7q7mpQAq/dOWmog7fgwtzCo8/ekcvImYqMESGHiDkzY5kAfmJKDUeHbqSnxKGJmKC9HvLGkRCk1ZcLgRXx4Bac8XqLG/oIa/LWzrYsc7kez1Mh/Uny6fgrR220KZzqigvscEmdfYNiXdkNt9yUSIMeS6hfCGzVuj23FhJ+8e43XuRSFCHZ0pzr95MuQp/jqO4LExsc+YB+nDcfI1dG8tDB06kP5xPuy0YEHohRvK3MDsCSChDwwiTYWkW7Ps7ZMHBDLTI3VclqAOplpcQ48E4GdmRJzYIDQ6fTkukhdAXsonfzo8LD/3Vuw80jxuqer7ZvX6kliiD8bjCq8BUokGyRdh0nssbGx5kHrckJXSufT0gDzeMrfpdpTEiNI8BCMWnM66ybHU9AdEVLo5Yxgmzxu5uVux/ya8LvqLcWEK4RjQ9MlEfZtsGwmJ5SFaEnRCPwX52VdbYOng3kpwfIYjNJZNAxBU1TodZyR/j464nlowENfD7G7oydtB2PEZa4Idhn58y0y3R2q/TzfxEfP3S/ce9x69u4d9fEjNT9P7exQmqN8YOsrZaiC9fa2+4XeqXc6GUTSU8wIamlhVBjFJzSJOBoW72tlRdeG0UNUyoBh0zh1Fr5yvb/3hTCswtcdjrSxvOPBM91JTUHpoG/UZQIDMteemigSHaSl+dpbYBgGk50tXF9bXcHZyLCwRAQ7bizWAwY73HoMeIxgsCA/181NCQlBQx/s72X1KTERfqzw2/raKt/GWPZZA73wcYS3sJfWQGeqnJMOqyjf5u7bFzknu+0PSyI5JrewWvQlcXuOOJlkF6vNBm9triPXIPTk60G2zqyAhYVI08hMiAnGNwHDMBe+GaCvdjoXEaBXS3kKAy9ldtbI2BGf1oqq8mOCvO2c7U2taV8S/Az2sVMbDWuvSodzBvvYp8V445CaEvOHKE5oJS9UVvs0JyfGSDkuLm0dGT5HB/qu9w0z1l1yP0dydT4m2IOw7iKXY5jv5XRE10+W+rjCeg4wLFxgdZCRcSwSxTrZcOhcaFpPMvtNeDDgMbo81RRnmMOtvqFoGKCgUYY212rmpZewHWrKTOHXXtSaH7v5iBr69/Ji/v+gDwI8O0bZT4Tafvh3qa2WL3b6Fxh2hVGd8a/jkNpqlQ36d/9cHzU9QGJEsGuTZWxkfxDhKx2ThjfradfI96iVVB2cN/NIPYwsRouh34Xxhf3QmCG6+2nWvuTx9krTxEhZarwHvAh2P9hGYE9g5ujd9Za2xhSssKmFx+lp52M3FfFlhcxOVOCVVx6DJdfX2w1gIzJUGBLgnpuVcr9ArETaBn/i/DdSiq2Unbi/v0cYDkXpstpFMLiJC1wpLY0YduVsylTgMspLzlVZAFgC9NVIJ8wMDfYpVV/APs2lC5aYyYeRoT4XT7KTExIJTFRHgCZzquzukFSrjrZmpf8S0WpVUvurcRu/fYUd2w7WdxcS47XVhuHAF51tuJyU0B7sJ/ZwDOBzbFCBkwGYOBh0wS/wE87px7fI9RS8jIscL8w7aW+j3r9HKXb77AiLUaafvdl1k5QotbHREQwAbLh3mjNcAHoxomG+CX5yGIaiYQoYBigF04EiGBbgdSEMwwrOtpZynQB859fUJQJJJFpTHBHvopOcdxE6W7JvHPrjWaJQWMujpH1J68pcXUlBCLxCgicwZSgGc7qXK4+VaO/a2gq+OTAwtFeokjbQ38MUeyBCzLC84AMIpSozOmRlbtDenApfYW+j5X5eMMfk9uXDGgAqCNErnRd9qoKUj2EwkMsAjKHfBxFpREeeqZKQoGKYSTZgTcClvzx5bZiqblhHdUZOUkBmnLC9ShbRAsTVWZ356H680M2aa37bwvQH6THeXTUiNaVftSJ4o4XJLUtzg8L0YILlZGeuygjysrOW5wWoflkYfq4ChVYKjByYGswD6AxYM2Z08WpLsHCuI4/mW5qMi0a55SKRL98C5wCjmH9OHsw4W7qg18rSaCgqHOZjoAPHkoti9WQe3RBTIpiXCrLon70C2R4wWd//ocKO6vlhxLOtxVdOjvz416hohW0D0Djy+4xisP9GHU5+wUhfYNjVtY0SmWCXpPp6P+h0i2LL10fmz4c/oU7Ze5LWCpAeBQJyP8+GJv6M+vhXcu6NKN1WmSbp1hNdT3+yimjr5bnFZ1Mc9VLom3VooZm8g7w+3/D24lm7vJzgnq+ndai/fbCfnTPflGt224a2KnzcrRanazaXGzEMK5bbFnnZ6pFtZ3uLElcEWPkZKTGtTXUT46NdHa1XxTiHK9AAG6QmRioxgshkc7wclZSaJZKNzNRYHDSgNaA7mf/d2dkmgTtCQ0+0UOFsSoY4qaYDdMf24vv73vS8eQmWHBiFAIRIltT+3h7TolLLeVhTVaJjGiSBnbniFC2HFeWLNAXNDg72iYpaQU76lQ8/GA8YbTrbGG+oVbvCOYeZmYfp6eOx0TmejhECKzhYnmRogEM6jtb3PO1M493t6yOCe3NEksct0pFhhLsOLiV9cXR05CNXE+p+9exmpmRpcS72jHg4mHQXeg3IC8P6oZf4BLiaY4oOpdIRRCRDh2toGHZhNCyrNyLEkqZ4AZyPChEx0KqpodS6SNrbtcDjw7xcoVxzFsbn1ORH3b9s96unGHMCBstO991ebYZFZm78IY4skRAQwHX4siHyqZEUH8bqlpLyTt2lF0hGIrMDGCPiY2TllOUJ0xQUgB531lo2Fhu2VpqSYtxw4oCm4JJOjkTJhgcjfq7WNGfWcMKidBn8r4vANHPpg2dUnBmqjJQQRyIqF4RNJDXKi6kY1lGd2VyanBThHujFq8qPaVeJhrU9TH9ckSp0tYIzW1kYRgc5M0/eVonwnreLJR4b8DgWzsNF2H0IYIahSIhtSQMQW5CbzlxOr0Smmbkr4eRVAFQBfM5OaspAdHhzoA9e5fjWd9eSEqnc/CdBvpgWKNXVHqbqalICUSkkHa7z2tea3VdIYVVGE/0H1G73ZU8oqaL6/5UCFA3/jkYKAOketRyvOHLKmHUdF9hmK+kMvPcj1JzLTScxnR2ebVSjuMIl28k6tddzrdLSX2CYfibAlCKnbui3qOujNd95Rg38MjXye9RmLbs3LoQweN7Z8wtNW8qLxP4tdaKj2swZNfKfGdHzrKu/G8yJPfjr6gkhCf+HjlDwa9zAeiOF12jnMzekuwFgLTeBeYCPLbx4PzcYzCNcNJ8S58Glo2HR4X5KnA2SjfXMtDgmo/Srl12rK0tXnuAO56ytKgUzlDhlwWYFi4djcotkMSnRTDMbkWT19XJgOkppl7kFNvvI7k4Ec1Tpg4lt5+rIVWJ7v0zb29ttePTQ31vAlLKRlbV4C1iFpIjAEVy8lmAO4DpCNwI3gWlSQCMgzd3J6kJuaLYNcw9Yo7xBzlF6OoMcUoQ5/Udjomr8PMMcrYT25jy6hIzLMcT06GDTxPu59RcXrHW0HY6NSgF0bW5Sh1csOJGeHC2raWypv5kpmUjbuFYco3BP7lCZojCMTkoUomgYbYPGRwcx30Wyi+EWtQQKL4Rh3eHBXLoEhcMxfBrirzi+tVUtHqUqK9Wcp6CAev/+aGcnQE4E6sy30L3GZnZmCisHwiLj4WQB6Asw2PxEVVqiF5FpDvZ3W1le2tnZhrFHghjaM6JVW2WZrDBMRwVnWLKYovBqMyHJwJARnJob1lbEwPUDBttcbpwZq3RxMMMKb2xJHZmN1KDiglV4yqqXSjRCHHmmanUpdGzMajft1BdkxQDkmRrt1aUMwzJLs8Lxsgw/K3OjmEgMgBYcT0fJ1IiDPa0VFaYHyxIXLQzFCb5MGAYAryo/Gil20Jgf1kMSyILdh8lK7+3GU43KMu8VxmBsiUxU2+LCPJwkV5ySkhDe0dbE1JRzsL7rbYdqJi0ZlLaF3s5zifFBDlx4HZBYa5CQyiuAJc6c5l5i0jUpeQmvvgFk6v1JBUG09JL1rmfUJx8FQzXGdSca9outZsQjL4NPP4r45dk2gHDkDDgIttd308bTTifCmfDpy/qKHJwdI0m0sb+k3v1LxCkyzf0Cw75mbX9IMcgQ64vHtXzK4SQCG7K41h+zfvvHv5ZPYyvW7wXoT5IAdR9/H/4rQ1v9p/UpTrv4I/5Uvo78oQbzQSA7AOCrHmF0dS40wANMMZObbLAzKUj8zAy4prc9XTjVZVH9b/KDhDyTO99PinHbl7SCebGx0BDiZ4ehjpcrj0n89Wl+loRNwFwg7N4A1S6f4P5x7H1zY83gQC82N3ExOuzENFxERGoCO+P4KBe45soHETzGTgaISwM+8ZN7x0XkxcmJMaXoFs3j5yPLpYxWlgKfm50mH6RjphOLeXl48OH9ELE74QHBY1LLkKalgbWqC1BkKjWTIgqMwSgGbcll8p2028dgpsS72MowmAhRkpxlZrYH++V7OVnTHBuAu8BqoRlNvor0c2sqv//u5dO1uZmTQ/2DXTvbW0dHR6ury2DAgfEKNujo+yEYY3A3+t+9hW89Nfmxt+d1RmoMkQ67KjJr7Q3zglhaGKUF2TALw3BtWEqANdYNU4p7EKeAFRenOWVph2HvkNiXIY6eIVuQCdsG1OWZr6won7O+HuFeWmuYZPM68EyGh97p+E2JQ8TC+FZRThAsMtsrTYnRbmZ3/4GcbaC/hziMyHTQFIrXtLpifkUwzXXkKgTbmsTM1SpKLctFLGTxOntzB9t7k+/L9zZa1z7Vb682DfUWERoJoprI2hN7eMhUU8xIUSO+0tRQrT1Wpnvb3t7y83LUJRozNzuFvz4iS3QwbylPYWKq9ofpDQ8SBTwTHCH097DFRB0XUiC2V6E3ejnLgl0AxgrTQ5gwDH7PTvInCpYkQfRt9wtmza1aDEYxmGBwh8XnMs6ylqbaB0XZmJdFbefRsEqJyRamJ9/6LrwO3dXWZDctdSMl2ZNnil8B2OZAi7DD4Lk+jRDUNkpkxPQ4G/DssmmZZ3sDDHuVLvs/XtRg+EkUCApJM7NHm8wyflT573zDcq+oIWmyH5ddwEqKPmc4nj9nzSIL/P/RU5L3Cwy7ribdod7/keIJvfs5VEl59YD+meIjen8SKfexCrACCBn5v+TFVJHsB+KCQqpvLU83dNqPwI9itt+5+ntysoJIRN7/AbUq0uCJSVJcwNj/VJk5uk4kqfQUjL/a6rKEmGAPFxvYPFqb68ACuFGaWtrOxjAMMFhmsndbU8rSTM3OWnNCtCuYR4BwasqjP/QX7663jLy7DxAL+zjheDBbiSeeuX/D2YSefLAmg3xd/L0FoQHuiXGh6cnRDY8eHrBJEtvZ2e5oayK0GQI709LiXFx1BliRb3M3ItgRrLe3z3PGh0s3lxuPtp/MT1Q52ZuAceBkb0YoGcFMVAIhpDrLxYFDkiTBfCQOeLgnACDhAkiRm6pLG74LSSC8Js/l45Z6GVebXqqvTfKMSngiWip2AP9jmn6+jTFh6SCDcOHTHEHphMT/aiM/gARKfFypnFwkQpWc1BniHwX4i1ERB5cHKKjsQV7/u579XdZeWzBn19dWAWsBxAK48rC8KCk+DJBDoK8LjBMckFGNPeI8IsCBpFrjavOX1C9ve3tYe8qKY1QW76gEwwZLhaHuFjgpUaniCPAzsvut5LVeYrF2GNYTHiKPhhm0KzEr5udTam9yT4/sgJISalRhI06MjzGLbeBh6ZKXKJFsYPCGJQqnRyv2N1qHegqxh0U1Na79cSP5CDC7db+lK8tLmA1fxyyvvd2dXHGK0kiAdY/JsNr96hlTywFGl5cbLyyQPzb4ABYiWCpfdopxRiI8FB1DcGpM5Y015rBUJRaCe0iSZuFILSIiOjYYReTj3J2sNHkGYenzln8uzI68lEBmeiHAsMaSJGd7GdWEDdcoO9GvuiD2QgzWVSsK9rG3MjcgbPhNcv4PeSQtTehmbSMvDBOlxwEcam2qYz4pwGaqTrGN9TUidYAJ/TvbW/TeZ+H84cFeWlLHtXd8W2D2Vfl5wExMcbWDOWhtaeRrbzEUHe5kYwyzGKbGddGxgtHFpIYf/we9NGBVYNjhpALXjf0PjTatpIYa/o8KN/eR8op6Jj3RZkFtNaGTK5IefxdViNx0O6NWs5ESkiwQ99+RxcguArGBNNkIhYmiOu5vv0HMcN+Z2jDpLjXvzoAcRtfyKaN/fm4oLLD0qJ1uKpCYHsHlvT5Z7iWAwO12nd6CiTdkAbF/9hnk+c4OFUrW/b+gX4UYbANgxBMyLsZ+bxwZKmx/0jQ5MXYz3wZjEitzw5R4j+2VptO9djAjMpK8LExuWVsY3s8N9nTh+HnZ7G20VJVGYe4vbBxg9/Dh4QFh5Luwq5JAaHR4zc8qaY4RWTPYp2PDnfq787dWmuCqADHCLxuLiMtxfaE+mI7X+Xk74l0WoBSgXFcBl4mUCD0g9Pq6SthW4bv4+5z7uKJ80aZkAzM0qLWBKIZ6WFbGtShdEghEmPpYtVcvuogZpz0zKirMl6R7KeVewg0kJhczIHD5tr29hQObALdehQcuJSdluDsIbO5xUNqhIYlB9b59xVZwCfDJ4sL8m+7nxYVZyfHhvl4O5DligWMweqy4KFkIK4lx5MLN0BEnChddAHRH63sBfA5tGH2l91Ng1bAvgGf5lcD23rNcDyZbfX+JT98DH18nWaqbUjga8LM97WV345nuIuZDkXaqyZn4GHtaLwG+fpNqLVlVFaVKZwoDo6qa6u4+OzoiDo662gpnezOlSaoLbT1AKZnKlunt9CSvvY1WmMjJce5c09v4dRiTzKEIE1A/0QvCpaFFtkHpEeAqOyYZPTMODFdFuM6T4kJxXlyA0Nn0q+9XFIfDF9lda6mtiMEwDAae3koPTMKemAh/1WDso5pyBY9ISvSVjEDCygjDSVM2ATRxejxZkB1tjRseJDKRWEd1RpivA1ZYliFtZ66mmBhKU6wVwRngLeTrcM1uJ4S5w+vMc1YXxJAYI6B92CMS5VVqpKZXlTN2e2uTyU2vh6gabvNzM2UP8lISI9S6bJTiYLC8WMkTa9V2WF66w4NxkRhNbf/VQmL8q7Ag7BzRQ3JANxfzOiohkZtPZ/N+l4+DMdzT8YjtY8YGxXnUNiYn/sQPNBhOZxqvfKOM6vsZud31i8hLrl8QDE51pm9ew8kqunLyLea92LMqxFFDv3nO5B79M+lK1pnkkf5X9QWGXbPFf6yohur54WuB/ns9VP8vKcbEwL+mjj+xdOH2y3KCB35Fn2pFSbVMOh0gjaZA9jkjaw9NQqbS3823oxkUn5TVj7FbyHrevIyNDNAFtJSXFlx5NY5qw8KdgLjAdFiaqRnsKYwJd4Y/wYzwduWOD5e6OJjBny87xWGBfPN7P4gJc0qIdoX/ujhwPn2aAyuEWUTu4mAR7O+Gw0SOPFOlHYtQX1xoOQFwwqplxCzDWz5cCdg6gLvAaAPoBbiL2QFAFmQFwFvAvt+jXblgsRG6M2bMgcjsFOZlEqpAJTxcX1tBCufej6jJ1Hr+VCa2A8bW8tLVuwPAzsDnBySpz+JxdkZ4mbUjKBJqAANUiViMYtAVgOlzhT5aUu0DYCDBhefJM8UWCbE7wVLXXdvg6PCwr+c1AGOAxDACwdjCiAuMIbBssGgYVmoGcOVjZx4usIpzthV7OD7wcX3k79UZ4t8R4tcW7NsbEfI2PHggKqw/MmwpMf5YJOoK8ce2kasj96oEXjU1zFYHQMtHYNpX7EOEm0k0LMbHEteGCexMmYV8mNMS6VDbGG+mJGnjnMRq4BnpgQ4cjEXT3OylqrDt+XO1AFcRBJsYI7pqSl1t+pzaZQfFQk1ulReFn+y2v2gXweyGWY8xwItnHczjiQwUTEwl6h3tjcSER4b6dXyLUoBFKReXAEi4SFgWtrYkGLoAnqwuiwIYJllqhKUSwzAfD/sDfXliCNSBrhY5MEHI6IfhKxmBS4ufiHMQ1kxNIaOXzzvI2m7DNcpJCjiXPVidWZwZCk8TV3kBcIIe5GVXmRsFaArAFRzcVYMY7TuqM5tKksqywoVu1piDF0EUcwN3R4tH9+OZ0A4OTonyJBmJsOkwSee1VG8SmiXca6tK9VhIS4tzlcA5ktk0u02nxJsRUROMweyt7oY6WsIKw2NEwHgqUI1P30C65NWwxs+Tys2HnxZ0kdi18HOcLKPCE4Vb3+DseOWKP0J6oDGCtBDIoKFmSVqz++oc8wfYmQej6p7ShYhIiqJ/734esS2w5SOBky9GUoO/pmCVXIphd4ajKYRRmQDs/f9NLQRR0n3qG9i+Y7phh+MKRtGBX72W4M/RHDVtwQA29qzPsBguj6v+jT7EibOOsrfP2OnkXVjJUBSD9v4T6nDiMzyXGWsEjBeCWK3mz7qeKCETa0sjWj8E5T4xK3QJa0JjbcXqyjK753l4ODjQC3jv6Ohi/wrh00NynI5mADzAgLAyN7DhGDXXJgz3FWH8ExnsiNHa2xc5va/y4L9gMGWmxWEjXpQeJ85IANyysrwEpvPx8fHkxFjDo4fMrSs82IuUHq2uLg+864G7oZQsJ5VKMVMcZspKinErygnCaZB0HMywrChsb6NFstSgBMAIDHuCKPUNwArHKWSE5FopoY44fQHhkFCJp6ttUlwo2VOjwnyJRaIWhk1OfCTf7joy1o6PjzCgBWNuf39PjzPUVZfhy0uO1ybeMjE+qloeRtrDivsX2jp6tOmpCYJyAQ/gwe/Mt8jLTu1+9ezClCGwbleWF8EmbmmqzctOw8FMnEaIg1pg37jYGgfRSs0Vvu7twX4AribjopeTEvbSUs9wHRpiYpQLQMt6lqKLxVR29nBUOInO1WplkLu8BYzT56w4d8I9zwk3ExgW5c21klMIYgEr3AAqyGCYrfF+WupFCmwiQGIRAitLrhFWC0AEbqrIbXxcHRCTfvgwBJOUUNLhckoYJKSQMiUhXPs3hYmDvykY3AlRLpj+JzHajfhcMlJjzn/oKUkDC/R10T2dDBYiLHXlJrDUnUdHsrFOJn7NwxImjtrb3cGqbhgJb29tHhzs48IkjsntqtLI/Y1WRJMY64ZBBVytfvocMBgIASzWj1YOLUx+JKtrekq0VHpl6UywmF9Y9QpfiqQP2HAMhW5WSqwb8GdWoq+nM5eQy2MEFRfimp3kX5QRkpPkHx/qGubr4Mo3g60H14PR67xRiNC+tiiOGTqDsz0qTnC0NeapbJGAgnKzUtRmK8DTKS/JJ3fJz8uxtbmObYUnbGSweCqxYnJMbyGgWJ4D2B5254H+HlIrCDiqTOgmzc55Hx2BA2KwEDna3LNXEzRD2AzWK4HNPaTdl52T7eHIoW+XjpFbNt72N+fozWYdbq4M6XhREUF693O6lp+QtlGhMIB7f5yaMqcOP+pzGfsDiI9RFtL4IekiGxAFRjhYtopSlL9gh+K22xDyBPhHzjDy+6g87+yE+sa27558Mww7zFyPmD3Nr+tTFoLlY/TH0BBh2wj96Nj/YI3vT9YUFV9LurFgDf0WmVEnSzmf4aGcbqNAIptGStKJ6QlLdpSPU1Nmcn1SjKP1PS87M5wTpeQ5g21exxzFxYV5gENCTxlbBoCNC7dn5lXBFghoB376e9sM9RQebD4GIwm7da0tjCyMb5UUhu5LWreWGyOCHOB1uLAjDdx0YDkR3arCvExSSAZbI0BEkufm4sAhSTsA4bAFACBQYGfc8ihxZw3lHMZHunBMb4OJVloIGKwVJx+q7durzX2v82yRcfnVk9Z6EudR1SbGHH04TETMHYxAXjxrx1AZbiPGCZoKpsGwI7f6YXnRdYwynPcIF6CkWqZjAyhOKj20AEUwLklmqaoMGqBlZnKULthelwbDgDnIXQVcGLowgLXZErs7YG8BBgC07ONuh+vjsboazeRhCIjC397ioZ9HX2ToVFz0ZkrSaUYGpv2QQSwxDa5Q8Eeko1T0cmK8vbyi40KAcZmGZyIO4tWnOSvRJCLdsFJhrNASU3RAf9LaQN6LCS3xutEUKFxKStCWlygWf4yJwpQnYAvCt5tPiFVTTlZYiJXWtrY2T09OFhc/iUVJwSqCCiEB7jhI2P6kSe5w8da+7BDqQpjpr59mSfc7O1rScFEQNqyZ9VQAuna2t8gk1TGijtvOzjamP2UbTwY7GJav+toK5Z3q5JjAMBiBOC80KkwIpjmsFSN9RYDBdtdb6qviMKQEqKYf1wLh14EOK5KqgyaJEQq7Wh8Q0qyXS29p0booLsxibhwArp7ViZlI7GmtuOFBorvAgmt+G1NfItIOjqGVhSGALkBl8Av8JOpwiPDW3CAxHOUiKik7d1ZnMENhinzRAHdNhYjwLaLD/ZgHq3WlaYvB7O7AtGK6TXk0Cz9Mz/zkgIlnpYeLsghkw6OHJOoFiOtTQhyVm/fI3wsWJR7taR2LiQRsxuEYqkViL8MCYXVaT07yoLk6WGXv69RW0hV8EgO/Qm2UUjfWDkYV8A+M2O3HbEwsCTVxi0FJ/6NIsVY/+3nKFPnrFVQL//JsRzcBEkBKi5Gy1CeU/fR/UpJKdp8OH0QKyWQ8CLnXyHz+BYapf4pXM5Ey5GPxH2uUYrgsrpBQH/9O4XXQsVLrnFNd7vNYjtdnpSDR3oP3OqyRrxA9ht4fd+Pt4ODAz9uRJCRwOAb+fItaP8/djHQqLx8sxeWkhK3UZHglztmWRzvVmMExsEIuTDvp73vDzI7AnOy6+GKZFFI47vShv3hzuTE51p3sfIDBAnxsl2drJUuoBr2pNgHXiVVVFmu3tMBoJtZ8VJivqsbX8OA7DBgwEoAPchOY973KO5A8RvSMiw3woU+fZLzqFEuWGjAGg18AngEkw7VhcD2EKnpipAwgHHwFHLgoLhRjb71SeltDXSWpGMHJLUF+rrjMA8w+nKYIIJN4UjVt9lh8VlXH6araKC0GCh3uuD5QR66/fKGgEwAMTaJMYNC4M/S4L0OKzWzEaoc73/v2lWRjXQOel/b2vK55WAIQnWBFGzn0suIaOdkYp7raV/t59kaGwCQ6Tk+XIy4abol0g1ua+2lmZhIMB9qOFHrwr4kv8ejwEDvdYei62BnTimEqMKzMtyHNhSQ41ddVkpgP4chBUmC0LvPb8BAqJ+/8TRDJhLCzsgu9nHH6E87SRNGwbAZShd9zcuC9h01Nk2Pvhd4CeEbuKprsgHBg+pM4Law/5HWlpwmrEJkdsBLCuoTDI0IPq7VPjz4OlcCUJ9QLOeLkc1vo2dnWloRIGLMqDIOhi9+oqomnbdOmL1VtQiy8SJZxrBkAH+HhbA1PzcneZOpDOaboeNKYjL1XHi42SjryOnnPAXYy7rbqzGVCoIgQb/0CbrqAQL6N8cC7Hk0jltRcwa4BK0xsiEtreSqTVKOjOqO+OCE22MXVwczKwgA6M5yFkxURJOMYAgBztDUWJ/ipctkDJGutSHWyMyEZyzANBwd6R4b6AWarvTa4Y0qp5gCoWM3cw8MDJqsHBmCoejbGu78lf2u4UTLYsDbYeHaM/BTiDBlXPky9DDc+zKO9tLQgBy5O+k1w4cErD3xcORwDpRkEx9/3dqGyc+CA12FBFvIDnnY+vrJnuV4k02iVFfAX35zRA5/V/38o7Lqdp2ws56NzVVjv/0gfuxds2hkbRUUZ6ZKHuhltI4g2g7xr6N/pZJoy21oeqrVRBMH+kz4Rji8w7FJtreBs+HeQLMCV6LJN3lGkJh5dD2eXdFeRHwjT5pAlSwQcP/ArNIr7CcRpwxKwIhkunGo49Ns6kc9sPkJ6CxO3v/666WBKkkwPLCRSLnTbT09Ddg8xE7HBBGaQSDQdH/Mk2M/TzoyJxLxcNVKQLS8tPKopV0p3jAz10cKPpzy4JsbAigqUb105GX5Bvjzs0IVt0sfdsqM5dWq0YmulEaOgtYX6mHAnMDWc7M1UC2bmZqdwHCk63I+i08+IYxLOhpIe5ejO31swPvahsqzQwRZhJ9jqstKFk+/LwJQhMS4Mukgx2NZK08pcHQCzkoKQB/khohSfPLE/wEL0r+VG+Je/lw1YeGDM0YzzQjmt3Dn3PK4ZAyNjYnwUJ90xVZizMhMxdCG3VJNLu7aqlNRN6Zc3eMEY35RgaM1WspaYI6QwXbvLHOeCasKTSfHhPDmcuyomZZLrqDbnanpqvL62oiA3HWej4ap3ohvmZWeW5sZvCvQZiAqTpCTL410k0pV5lT0razIumuaeRpOxr7dbi+2ud2uTl+dZWRhlBtsOl/kqYTDM0tFz39vTwQQHEAgxDIFwCvOOaxjqaNke4r8sztwrLpZWVlINjWeNjVRLi7St7eTp0+GKUq6cQsDK8s5U+5PdkeHJrvZPr1++rSqvSYiqjY/K9HHxtbdQqyEeE+HX2d6iJJ5Lwpuobm11RcmoxfcHfsGeEfqbGj5pTDnafgKrjfm9H5DzqzK8z0xPkvxVtelnmtriwjxOL9SjIkhTI8Vm1ZUP8MoJNwQxVfCMx0dKMQwrzA7EMvcAOPWIHhOxPizAeLB/LrsE7i2JVsG3U80ivmRbWlpg8hWpVV/E7f3IgBJQSQx3b69KV8pO7KrJbCpNFif4Rgc5CXgmALpsUDeCZwp/Ct2sAjxto4OcH+ZFd9WKlDFYdUZzaXK4nyPBbwBQtYgiAE6GZ83kvoIthi1ZJaAgkmGL+ULggpPD3QcfFwAA2xxqXH1Xh/veNFoNHtVW8GT8h0aZQlcqv6A/MhRjKkwDe5yZ6ce3gK1fBYYZ5XoKzkQo+bkxwBu/BTZ6TfCSpWtnVqFxivnQN+tuKBFOenCuGAxsV92z+OAK13JQ2p4sJPAT1GIoa3Fn5Kl/QX34k/O1WH+I2O1Xsy9+7/4ANWGIGOCIBtr6AxbEB9IdFHIkcQJstMPnSg+ob0v75sCwSSNFJuHlRz9gJJhIRK5OM0vMmfTk7DJBTzi5gouTZUn6eiGSM8eS03qMOSJENuem0/EX6j7D7N1/93lHv2RjXSxnXbe2vOPOM20OFCKXs5YyerAmc3K7w4NMzG+RuhRcI6TKkbC7uxMR4q2qcqOHizQjJYYIiJE4GI/2vg+8LTjYfEygEYAiwi7NVN/CDTMNgHXSUFfZ0daM60DkmTyWSTFu2Rm+tlwjOCAjNQYjJZydUlYUtr3avL3apDbncGOxYW+9dfJDebCfnSUtGgYdsCL8BDOuOC8EbCBEtR/lCi8mxATPTE/I70YKiRvQP49wsM7TxQasQ6wA5iaw3NmWuUuUqrpdHCxWV5bU3rHhwXcYqqmanlfVsIXtzLdYWV5i+164bEImHhXme6RZ3bi5sUamwCvgHssMx7OTg529tZn18de1eVGYJ/0K1cMI8GPmcwImH3jXk5+ThscMTunBuT3BDtwHPq6vw4OHosIRGaCYhl7a59GVdJFoMyXJxdYY+0SUYjVX1QjrpqWFUXWykxJVPYFhvcXe3gJTTJZYkKOo4ydyCwoKAUvEghji55oaG9pUXvy+p3tmfGx3a3NpfnZxfm5Lsi6wlxV32dvce9rR2tpc5+PjFBTkZWdvSpNGGgCWUzUcQwPcX73sUos5aS14mflLkpCVGjMFGhaZ3ld5MNkDhbbELxMgdFIlxhzo7yFcOBsbLLYksL/x9GSbkKalEepFHG/fWF9z4ptjBer5iYewBO2ttzRUx2Mflh5BcpikJO8xyM9VFcUxtcK0JA3q3cZGR5hPHFCBFrLHwrwMBhJDYl/5qUFP65SzCmnVZhH0uqL4ovSQooyQwvTgmsLYhuKEtsp0QG6dtKazEoki4LfWitQgbzuumYKoqf1Jk5YQlpI/Anpnewurr08SubF2mYXJrdgg55e1os2hBiYAI/1YgpwRUREBiGDGxnh0qP8MZkJONuyM5hYG1lyjTwlxU3HRlnKvB7PiwJZOCV5JSqCycwq8nHC4rLKs8AqeIuKg/lkGHd+fI4bAm2mnm+e4QMb+gl0O3pzrOV3mffYzF3OM9/yoAshNc6ndl7qWw+08Z1Sj/SQKD7CqsjmcPJdL+fYfIXXsk2Xq29W+GTDsTHpwNvAbVxwLPp5XBHkXAjV/9NGlYNhWk+KyZ+xYv33eU14G6sj6vdsdMtZElAfcdAV3DHmDvoco9beaP9dIIOYmIuGwujMcG4kSfi62/0SnGRmtQb6xzja2VoqY2IOi7IODAyIZtL21SdjGGVI2qVoMbi3txbOOcwQhAkvsFASDKSLYcXOpkcmNAYAnJ8MPWxuf5meZoT8iqMUEYGCpeLtZzo4/BDh3Py8Y3ggWEmZZtDI38HC2eNUl3tto0VT6JVlqBBOnsyUNjoTjYcvHBQbwky5pQ2QDI333D7eeNNUmgFUH1kNqUiTm4h98h8gMWhprA31dEmNDwMrBqUqerrZwo8DswxEnkvbZ19vNvA/+Pk6aKPtIhtXV0lcwW0ebLHmv+9VT1lN5S8KkUtCCoEhGma3lV4NdDxfePBxtShmsDMGqwZ3Z7vbWX2EWO7D+mYpVejcybp92PgZTdW1tJS0pipRM0JTxSLJZaG9e5ecxGRctpQNTcuglul7opewWycpHRpIhro67jqeM9fFgiXC1N36R56nKz4FgGI3EAlzMMCSGwbwnV3aCGwj4ECY+k7ZUtuZgukiOIfy0tTW2orWw3d1sSXxJ7mq5o4my1YFnkhQfVpCbPjM9oSWzC4me0QJ0KNNYQ7yi7EEewWDRoYLV+UdbK01hgXxcGAYLgpIEHywme3u7BHgoiVZf2GppihpnvgUr8HZBFLdclm79sOI+RScQugq4sP44800/Dpfi2rCWR0k4KRGWGrZMiYT4B57dK5UpD/efZEVChydy5UNxsL+XOTaga6GBhYHHRGJwzQKecZA3rzQrvKsWcFdm+/noFmCtTpojEeMuOnSmXkasozozO9HPQ8CxZpRUhQS4axqBszNTShTE8OejmnJWi9XE+ChRgoblzsfF8kF6yEpf7c77pjUVAIb7+mDj4dr00eHB/OwUXANBhHMtTZHOtlW+HrBkDUWFAySDNQ3PPkuGgwN+7wj2A5MANnrscsVZ+peOacYw2OFv6RNN0rsROR+kS/a/WcC/g1FFzhfmEWFrx54dozDUyO8xYoD/FVF56/ju031qxuSclO5aPjv4txxP9f204gyDv/EZLc8vMAw9E2qag8j0ZEWBP0/D8Uu3jTIFQ+DO0+u6dhg6soTa71FLLAVJYCySaaAHlIJZhN0Y/b+AAlmXbAq6/3/MbkZdyQiQSutrK7AdjLKqLI26I0OREamzG57Ky5+IjXKyuWcpp5LDmfE4Mwf2SyVxLQBOA/09el/wyclxTVUJABjAUfDLyvISvIJ3WbAqHpZEbq82M2kJXz/NwtYGs24eNuai/EzmVfl42Ls5Wdly74ClMj9RNfWhHKcgEoMsNsL502QVMxFRFYMtTteA3WYtr+oGFOcmMA/y5Xm6cNwczeFPkzvf72hJ25c8XpiuBqiGqbfgYH8vm7WF1pPd7vhIV1zPxrQvMScEzpkk4lpgdmD6BwImNVFHAIojmZzXxNIxNjqCPfqsqmKIe5ipmdNQp7G8eHpqAn8KmPgVCeeEgwESPM/zcOLds7WUnUdvRVrS4H4SgkqA+nCRmEoBwACXtroiBNYvw4LmEuIP0tJk0CvzZqHXubzE7DfhwRzaSIIx80FDqOcyjxhXIVpaGKUH2bxXFwojLB00WaJs7qhmHQeqU1/Qr4M9mpIQDiuYjgwQyP8i5xBXC/hhspADLE1vtzelnu6197zIJWJQsOyoxo729/eISHF5Cbs1vCAnHfMAkUD35RshX22qr8KXB+ATYJgL33R8pHRrGcGwtqYUohvGtjYMwzzAM4FejpKPY2efPlHyfjY/31xdbi3fCKoqi6+QIFE1MItv3YOibO2JFXANcAwT0ltZGDjY3AsV8rMT/asLYjurAXGlq1Vtbnt4LoORDpplPq/LqsqPEbpZ29KVY0xlcE0ZAX293ViHnTASAQBj+8VXV5awpDj+FnybuyPtRXsfWtYHHq1qwGC4r/U/2pt4treiUhyxskI1NsI+fpiR0RDgHezArRC6w0oSKbCG37HjA4BZuMCqPyrMAcmv08nte1cBmdYKkIkI9idbXvVLtk9+ChDyyY+NC/4xqjEh1vIy+6SD3ReKVEZUiPWfUfBDRzEu6eHRYs7p0B9SPXJ1KACBh+MsPn0lTUFnj0Nwq+IbRb9fYJgmdI3ql5gVfqeSKzjrrL1isO71Xde1S6oUBDUwvlm1zTqFM0CPuNz4/5LT3//dpVDQ4Seqj8FRg6rdbrSKTLKxToqLrK2+qgkSsrAmRaIzWrPI2cbYgmMgtDf34JkSRcjcrBQmnxXuZQ/yVpYXL3/ZOzvbzODP2toK7HAYNb1/V0yQGECjpZnaAB9bK3NDZkYfOgOjxDwqzBfuQ2piJJzBwfbe5PvyDwMPMJrCITKBncnE+WIw1b6zhsSjze/9gGeJQmdwV3NF/gDnVucfAX6bHq1orkuor4qbHUdJQcuzdUIPK3x+wF3FecGAzXbWmjua08DWd7A1Zd60/j5kL2KomSj3tSu5nKG/fN6h6gbGv8RE+Gsit7iStra6gtmQUxIj9Hj7m+7nDF3dLI2byPIiRp6ABFICrEcr/QZKhES3arjMNznA2krO0VfzsOTyX8rpvOwv5q1BLGReTvOJ8bgo61pqvfTJSxRvJCcKbO5hkgDAEpjN5coNX7j5xTF8tRmJvcUoIxH+lRpoQ8gSlQj0YU0gxqiHs3VCTHCQnyu8cqHgLK4yEnryi/JFjfVV/e/eQted4Z20vOw0OYujmsjw+toqRpuwFAh4xtNjFTArI4IdMWLBlKqq7yKkpqQcS3enEiZrjY0M0F2A7uK4JV04SmrYYB0IC/LETOvvXufB8ghL2eOGZExYDwCSVTQMrjNA6Iyr+/JDfancPMX4F4l2UlMImR7f5t51ZEHDWs0sDNORGQiwKDPqTkgROaa3YoKcW8tTHt2PBzDWVSPCcbAu+U+AXs1lyfAvJO5ck9lYkiSK902P8fZytiTMmTK6eW9HTe6PpoZq5ggP9HVhZmfo3uKiA8lJOGa3ne1Mp19WbAzWa8dgirBY/6OdqdcnuyrBn5ERTHhDk+VgkYzspyEBhDgRAJirrQldffpVTnrClT3Lw6mz3b6bNHjOheA++ehsqO1QswKEW2TSzP+KOvjA7nOPPyGjkWQh9v0zFJXSXdB1sx5Vf/X+kAyD9f4UtdXIBv69pkb/7FwR2oc/pfbeUt/q9k0jrF8rUMgNw1i5vFyDdB+FemXQ7rdQISbbaBUMu63Wi4fpnAsjlMdS7W7yK3ZVXuf2zzVq4N/ICRv1p4U8O9mkxv/63PSYvIeilDfmGJqfxWYHwKcggZVUpLNNKRYfZWYWeDmZWdw25xiEC6wkKckrSYmozJdGYgAnhB58ZrgmW5R0HUx9uOFKKq7pbVGKD00ZL0NH8HtjdTwuRieMiLg97XwM1ok4PX6XzpsC3Igd3kO9RWCmAKZysjexMEY1XVWlkXAebL5A31ppUi0JA8gXGmBvaW5gbYHI9Pte5e3R+jz4X7gsHuuJwe+T78uc+aYYN8I1tzxK3KMx3r6kdenTh5XlJZImCvcQC2TjxD/4kyRzKtUY9LzRGMrGwkTYUMC1Z1fbDg8PcJInyq1iLx88PvaB2CgA1DW7tE/Dg71leXF2xs2Zrq8KPMH0x9lxYP2XxDoQ658VabjaBhYV03ICoxOskFRX+9HYKBmnfObXAH2dR2Lpbnys4wwW8JX4O5Qse1w08ljkpkpVD72vGHWMh8mDUArAbsqJAVHk89FDbNbDmJmdmRoZ6h8eetf79lVnewvMTfhvZVkhDPtnXW0AJwYHehcX5i/PMUPwpNqR9ub1c0JN7sgzHuop7HudR0JhqsGuM7rFRSmMY1b8HIRfUUs1kR6NZL4RSWX8Ctfsdmt9El7EWuuTuTS2jArzZXXyRzXlRH7qvq/bOf23rKyeiBBSKpyWFHUdobBH8lgf5vWZmdbVcQmATVMk1tneVGBnEhfiKooTFqWjqjBxvG9uSiD8EhPsHOjNqymMLc4MjQ1xcReghHMrCwNbrhFzd0uKC1WNEcFwHR58RxQgCcOQlko2LY3olGDyRj936wpxuGSoQUcMxuw7cwOnR7sqYbEmhSZEVnZDgDds7sQDhf078EvZlRSGfZa2nKgwtD7+lc7G3jKSYCURpPF/YM0Mt/cGkbq9lSOo0f9GHegsZX68gIgbFJz4P4yoEXUXIpLuoW/NFATr/yXEav4daN9A3TBmtdVO5xWcEB4/KYIc+58IWeneiNTyvIcOB0cwlMtHWHzK6QYKheH3rrI32gB94XzOvp+l9i+XAQWn6v1J+f3/HrtvcbkGK7vMtcYxaA70QblVuhWibKelRDtZw04MGKzQy/kES81m5zwO8jW1uK3qxgYb61q/CGx4oYEe2Fp60Z65s9ZCOAyXZmr9PK2x5/Lj2Hu18SJin1lbGPa8zN2lKebHR0pzMvzqKmN21poPNh93tqRlJns/LIkc6bvPTH2UC4I1xUY43zP8e4BVb1/kHG490Rw3a+59lYtDYcgja2H4rjsfc35Ilp+Q2OyzridN9VWk5mFtbUVgZwbbMEk+ZJL4a/Lu49b96hlhkBsavBbvI+Z1ZFp+ujcwSohIANiLWo4syE0nBR721l858e55OZpWJwkAiUFvz3JzsLmLy8MIR5/erbfnNTFBrC2NQhwtu8ODZWzpXyv0xchL7I8M5cqNYE0s3no0QErYfkXc8bZ3n+d5qlLVM2vDgt0sCF1Kfk4a81QT46ME3L5+qU/KOqDxy3hznj9tIyWsqv9lEgDy6CIiAGM8S8Usa26sUV559vY8nK1JkuSuvBZOlwb4E/PTXC2XIHG7kDGAXTawNDXWJGAY1lyXiEN8Qg++RGdUAGiZTAo3Z+uPmannpoNYnOjCIyNQRxZcVlCNKeMOA0nLoqfeoj45puvKjJV2KKRFiTIVZYphNhyUCmFDSzajjriacCqjIRN9yeQEHbnzczNqxyRZr0j+ZG1V6R6bEaJwla+tkHI4a45hmShsfaBeMqgPBpOlKQ407Mz2S0/OVWhL5+epBw+wT2cxMR5HwJhlde52ZmtzM9TNts1Nyej7IbgD+p/i7FChOgt9+Hd1JRXcakSxL/LGjTJ2n3s0jVRqCR3/u39O7bNJF18vpgb/LaOC7jaSQWIVXyHwj5CRHM1S3432TZRvlioEvPt/kV3KqUYfwFvF+Ju1Z/FGQF9EHExSdfHxhPMUhimrtlEiT038NXZAEbcZG0XE73LJnGezLoqpshh5Y0+9sqwQl7sgtaW4GDUCqerYsSWpKeGOVqYWtwC85Xk6KWR/ROLNlOQgB64SfZkz32Jp8dN1f5dROnwB26eTvcnYYMmOHCmBzfGsLQN2U9hHwRzRVA/d2lyHSTX6XuXh926tNB1vtx1tP3nfX/wgPxjebmF8y8LkFt/mLpxQCYnBwd1Ps4pygl52itc+PSLhOLVsiqvzdaH+9qZ3/wGRXEU44xcRQpNow0iYMYJI4gKyZd5kwFqa3ghmFikky0i9lkT83rev1Eoq6TT45az9MkEnDUYhmC/MsAM8LDALLMwNo7y5w2W+A6XCl/mejjZ3sddW6Mm/TNwPBklKYoQMg3GNHvq6n8EI/9oCMHk0bDUpgS8P3ZQ9uLJCU5IOasO9A7j3TdE5xTD4fagMZYfiFwEPB7tbWMkLZlKTzq1mnz7NEWtycKBXr0cjvQwMG/0wjD9drVxy96unytY595wghyrKevm8kwBLJjOkLg1zXcC9vcLkPeZsgll5LhpmerswOxDWQ1i7YNnBMAyg45JmfgulRjj6YFJECZ2leXlMOZP5+Fi+lWz4BQid2DJ/6NKedbUxhbb0HgO9Pa+ZJakXdqaYGNO9WFyY1dfzWvUj+t+9ZSqn4URE3QN3qo1EcTlmtxNCXLaHm9b761fU4auFt9UrfbU6grGNkSeHG+dEHc7296muLvRAs7Nr/DyZSmIAsBOFLtQ15FNoaW+6n2M9QACx4x8/6HkWUoSCMNh/oI51Y/RdL5IJFCFv+88gRnhWTVKtQEH9v0AthKDkQF2DYPPU+PflETCaimOFzdpysqwQdpJ96985g5vwLeKj/1bCMDo6ROjmR35fJ12sC9uqSIbEAFDpri9O0BEOwmpmXJSPuTXk3pDhPQE7DbT3f6Tg6mFL2X+8oIinfRJeCgSf7lFjfyoHdb95Mx4L2LDTk6NxRmKgA1eakXFxnpU4azUlKdbZ9p75LYH1vRZUSybPY6TBWLWvhzvP1FZl0/LxsK+tKh3s773Wb1TzsAQzagDIWZ6rkyw1EiT2oCCEQ1MmajL+6qrLsFjnwJsCDLG2V5uaaxNiwp0FdsYck1tkJ4bfY8KdAHdJlhrgGEJeD6/srbcQ9TAtHY780F/cUB33ol00R1eL4ddPDtUnkmHjD3O4gfWAy0gQARpDC1tLUQq8ncgKe7raXiEnG2mEdztA6KyHDkFSXOiF2ZW5WSmqlpClhVFygPX7Cl+sWBXkZmEtt5uVqpJYtYODfXzHwPJIc7P/DMyH+uk4i0SZ7rK8RKViyMsgUsJnY8W5U5kgYBaGvS/3bc5wDffkxAktmzNcAIMBJE7ytyYwzM/Lken+JzpOAjvTZZ2t/ytsc7NTWtTnVFn1CR7jaWC4yRUrhiVbZpSujlamLPvV+FOlpwRgvO1GJdP7e3s4PxwWxscNybv0GnU/NxhTyAb7u+kOa+toXkc6e8KwPMj7nGMiO7terit1haIR50ILh4c4LRlTXOiX2kcazI762gqmZ0eXDtArNNCDb3MvJsJP01aiBMBgYSy5n6Oq48IiKLK2ihk+Ua6srfHg48ItdcT0uOuOwdDB9M+t8ZeHm+e3nqkpqrpmKyXZ2daYpCPaWN7pTk+mpDdXNNHR1oSrjgkNqT4Ey6fbSJVYZk/+GLXdrpNp98lPUcr1/g/ZkbFhUWZmDGpH91xlKYJ/g79OAmhnE3d0umZiDM/yFUVGOBkN7HDpDvUdaz/0Tb3wozmZurF+FVNq22KYjOF94Fd1VpeTKpJxcd/uuHjcEzGxj3+lRbJMxUR9pQjZffxbFsMdt61GxfS+JGviditj5vwli/LNSyzuOCWGyzHMdHe4OBQmzlpJSvSxMzOxuO1mazIWGwVbL/HEw9ubAnw4HAOSxhAV5ktoneR72D2wPK6vQgwAQFQY8gRbGP+gtDBsX9JK8gCH+4pwjQdsomrjJO20Oq21heHrp1lYi/l5e6aVuSHH5DbmmsekHWDKgPni4WyxPFsLMGyotwg64ChNLPZakBjGbBgrIjGfjcfHR4q1ElAWmDL1dZWZqbGeLjaRoULAzPAV4B5iUVqm2xtrAGgC2xQjFws6/H7ldx5MPS9XHo586pE90v6kiZm6o5a6TVVvB+fJePBNOrPdAQAAJBCF2JKqpIgQ78sYfJgGAMzKUqGrrsm6X4O8xBehgcQUxlb45ecUpmSwtfzK0ebes1wPQlUPiKs+zdnV3phjbmhqYpARZAMIDfr9aD55Cv7egm0GGnza+fjCsOd1L3qY8VLoyVc16Y6Pj1SHGcz9ID+BKgUObkTJ0Nvdjq2pLU6PhzfGRwdd7XcEhICt/6nJj/Dn/NwMjkDCwtVQFY+LVJ80pmAYFiB00j1ujIWtwSLn2xpPJ8UrYJhIvJ6cRMg5vBHtx/6VP7uOtmZduHzYtsmJj8+6nsRFB6YmRkaG+sDAIAofSh0OgIOlUunE+KhaVvr9/T0lDAan0j+GI2+kVJhrdjsrTrg90nQZ6KWJvWNv5s3p3gZz51h59YIURlpyjaKcrKn6hhubqkrqcJgDVh/svfNUbuD9ja5JfaN/zmC0/wd24rQ7zxBsI1EEXSprSDucoJMYCZv8L5+sVbF4u6QKlX4pKPd+m1rLo76r7Ye+wdd+MEL1/VNZauLJFWVKTJmypqYBWDX8O4rxhBgXLyp1OJqmhv69PrEpmJzkgxZCWH+7CUO5y+S/XBY7MSUpdL9X+m8/Y3h1M+cYvMxKp3JytVt4nxLivO3MwMiz4hqhOpnsHEKQdZiRUR/ghUVscQfMcLC/L6LtDKVecj/n+r4UQBfYOWw4Rm4C80kGveH2KhJNxnk4sJWqvhHn21jSVewHm4/nxx96unCw6pcN18jB9p6fl3WQkAcngV+SYtw+TVbFhDvRaUtGxXnBNBKr10qlqFH0GeDcxEjZ0mzr2upiZlpCVmZidLifu5zIUbU/65LhKFF6nC6aodg+APNCC9vb5RupSOloY8030N39nKkZGh7sDZYHU2B3oL9HibdQkSpjYZgfaQ/WP6CCgkh7UpXkJrDU218OBrqflyOGYY+DfL8xMEycNR0XY0sTmmln/2exs2+sY9xixbkT5sEhGGygRPimyMvTwQQjLkRfGYiI7OFBZDLA8KPaCubZiN4dgPbLxAf0bmdnZ5hOxsHWWK3Gw/0CMXN0WXOQhnvXE41KD0RKTg+aUJwrCMDvKjfwgwMc+wLohen4iLQ04K6a8uh9SevGYkNMmBMul0UPQudqJczUB+t/iIutVCxSZE9kZQ1EhpEEtmxR0pU/OACTTI7NyyT4qXf9ypPVDw/hFh7Mzkw9aa0ve5Df+/bV8NC74cF3M9MT2uP8sINg3kvSszISNOmIsMTV/piz19OJM/e6cmOgXg+gtdhTvfC26oLDAIwtjkpPZAjz5PhY6CMgjCw5HgLq9eubmaerq8vY+8PsSBudPQXU6Volyusb+0udKkeOZhiBrO9Rq2z2yuMl+r1yt/6HP0aQTNeF6Zj65HuuDm3aijpZ1TkIto5EpYn6FH67dJ/6Drcf+mZf/pyb/EFaXs2DPFlGVPiyIJuTzuviDgJFZFTpAnIOxxTi0TAKdQy+oYDYJcDP0Sw18K8VPJPSS9gWMOtIMLrnx6hrzuU9Pj4K8nNFGSZcw3ftrafFxVq46btCApxtETE93/puH1NbLCt7ODoc+0ExAIMDqsqKTujEuZWVJQ9XnqW5gSWSM1ZkKsI+Lb223IaXzzsdeaZW5oZCD6uhnkIMfgCGwe/YtxcVJlS1PLAAK+C0tsaU4522nhc5cAau2e2wQH7/m4KxoZLludq1T48Abq3M1S5O1yREu+JMRbBmAPItzdQylaNVgVbPy9zel7kkT5J0OOHc+EMvV25EiBdWyNWoUWtrjGktsSorNCKGowtjGPZk40DE5jUEInCGFXQxe3qMve0tPw8+T6n2XcAlgZTW5jpNt8WKYxTqwRkqQywdrwu8XOxkWTREck2PBm8kDKJ9ESHn6OC+1l10lJ4e4sjFuhFgx2hSMWLlk8bppnCfI7y4SCRAno5YFM3nmhuRtD13vsmrAq/hcl94HCQpUYlEgUCCq8qZ1KMlxYdpSn+FZYHwbcD0tzS77e5k4cgzduabxscEq6YOgsnuTSuGAeZhm3ENdj+22svlYoBX0ra2JDhXGdYEXJFLyuEAhpUWhh5uPfkwUMyXaxsE+broyJWPJBxozncOTct0blKIxTV+noQjUUudqn4N8E9KQjjJ8dM7vrS0tPCotuLKVfWgve1+cS4J391ODxFFtQ3GlWxAWhi0lCSqhsJ07Eu9NToGzdaHWvYXR6SnJ9LTUyHtjcIZib2wEk5N38wkJYnQqGBbnmcOj356ir0a+Mka4irUxUUOFlffzyqkjDYqdHfvoJL+/l9QSOauZrNwygOIGvsLRtThX1ArqTrvnb3UhME5/Db+93T62Bn13W7fcBgG0GtULjJwVdoC+wOIJQafczmRTajq7xQ1jrrAKsBU5IN01OYD4PThT2RiDvoJiu++VKQRr14uWWK98Fw25tBvsqipYw3DjoNoAjRYYe/npB81NqgvgMnKbg/2NecYwC4LvSvEn46DiWSUiakpvvbmWGCEZ3XH2tKot0qhSnl6cjzysr4uPyYqQIDz+q4jq0S1VZQW4CKuIF8e5ojHSYOBQlscEFMl9Cun3wKWSuv/z957QMW1demBDm2729Nte7rXeDw93bbH7ekJnrHbbffMLHvc4zWe1A6r+/+fhNATsQJQBVXkXOSccyyiyFnkKAQSCAQiZyEEIuciIxBwZ597qk5dqgqohJ70fp11xUJF1a0bzj17fzt8X308wLCh3hyW8Q9IH3O44HSvDXeCwU521xAJfveLNBats2zFNjR5/Iui3MDj3ZabsmHwp96XYrbJQ09n7uZSrUL5IuDD4Te5cGUEPBN3WQ0nYIBgf7eMtLjXXS/m3s8MDbwBl3F1ZQlXvJDw+fBgPynkuzO3UFyQRS7+ffAlfph7h700JnxSxxu9XFh4W1nq6WDJkyF5YnRXVqTt4y1NNTfBMB73R3ue0ats14lSr+4cVxqG0aFTgbkWoVM8Fj/Ow7fzLB7bWhltxseqRV3z1dQllomcSV2idmyEzEFYKwBo1SY6wkUepukQBws9vBzMLBkMFlyWYVGk8EWGC3aeVDK8tzZJ4bTITQAY5iexb4QOsV4pWwjwm/BtBHhZ11ZEjr3Nc7AxxRK9yiLRpNUNJpumDHhIVpHGS20t9Xo8u82NNVxqTqIt05NjBIY118bC8iVZb4yPcsErob+3k5rNnETfD6zA2xBGbCI9/SotzV8oBf9wKfQe5dnaXCf3RetkPsBRnNfF9B6womq9PiinblwdrUjGJiczSS9JMIou9g4L8sRPn9DK6EN3qfoqYaT7a3NIG9h2Mte1vfzO28MOt0z7iuz3W5oB5X+BJ5S05sIy4sA3CnBm8WTrCTxx9/Wtm6lywurJP9KgFuxsCSEfeUfJ/4ccQg0WguprfIbDv6MunT1870YMitczPcYVL3X1oLUPihxQq77UkgNCj99h2D2O3TJq4p+iAtlL/dWNAEDHTWLDv41QmbpTfB5hwrE/QLQz6ocHcFhi4K9Rm4nqxTLOqa1s6nRG+7Nb8ZFl7f4E5bW1HnAkRBsab5vJ93STwXHHlW9IDKQw+3RsDLV4KdGvHSUluVubcTiPwEtGGIwu0DpPSQEMNhcdEWpnAVbZhsZy8LM4T3x+Jl0FLk4PDt937403HEw2bY/UDTRm+brx2GYPmDmxezq1vT0Jru/nmhvkZ/kDDAMQhWmaLVBeToWQMUY48P5Xz1MBd32YKvXz5JUVhACIUoBVsJ+nGb6A8cA5g/c8b0wAZLWxiHJlKvNgKx+qArytWcYPXOzNVz48U0iIgVdUVxWF+zTI5upgCUhA+bxGhhDuApOPfT6axR5R2CXGhij7UljUiPx3emqcKFa/6my7j8tOiiSHBu8S8fv0iVpfP+3tnc/JFCEYbwAYHqYQzDEBQ60LMAB+e1VF4c0kZgDYfnyR4Uz4IXBFnC4wDNALDk+4WZseJyd9E/wcBIZ1BPgQ3VWt2eTIwFeevshPOugePIBh46WixhRHlE7hXrsRDtZGInszJkMPfJy5N8w/CVtkqM/99YiqGY8Qp8SohFWWrEe+Hjx4nE8kbfDT290Kx4/ev5tWeL5IOhrAjKbkAeCpY+Z0/eaOJidGCRkPllkDuIsbHWGRqauMOt5pOZG0lheGsOlAkq/IQWWbk4qZQJP1wc114BlLEuPlxiI9/SQZ2QjcGBYXFaj3O9tUX0WYObRO8DLJ7gl4TkmIKC3Kqa4shr+qbElVJ5pJhNoCfJz1Wy35vLVeCn3NDZLDXHdH68GSXuOdp9u6dGwMu2nbn2hMDnfjmCPb1Nxc+8WeUCJ/YsE2jPG2qE92xKXmQp7x8n3Q5YOXu+p/LfatJkfa5TG1HiWvwAIEtaKZCh/SOiJkjIhA4e+rS08gqURpA6ajqAW1gTbTfV2euDudpr7i8e3DMJTQvNR/WnM9XMb++Vt3s25cm+4axk33mxHBqJT/PeSLXLFLORvPxP+ka5yA2SF6+PK+jvjykvTzBAR4HCwtytu9ZPaVysgQuwhZdCqsO8iXypC/YSA00NHKGPcDgG0OcOQtjg4TD+V0fWZ7RB63A+MBYGz17bOsWC+26QPiqNXqQGd3+9je2gS4Aq4h4J/GZzGAdnDqSZwsAncEfMoPc9dEGNOSo3F2a7AnG962u1a/vVJPOAwV2DUigu25ZgbgnAX7CUb7n4Jz42rPGniddbitqCcGiC4r1QuOAd6fEu+uIP0M+A1eCfa1wX0aCiV54B8oEP1vba5j3IVx1M72Fu6YampQEaRQgGEUo+/lntrzCJ91sL+baqKOy0tqcZHq6PiclzcYGujMM0E5UlreAHw4oaVRZ6BPiJ2FhUzwoLlBKtNUU1WicH3AIcMJTLjFAqsnnVku4yWi0WLElo5tNrzh6EhLeijs/cCBedqYnyYnf0swTCzeiY+zs5SWnPl5OeroE4NXLb3IDBg2VeaVEczn0HAXHiWSqYAvteJcY0llypcdHh6QpIFK2a4vM2amJ/ABhwUplqBjLT62ycPip0FYfv3ju3JnO3NYFmA6KSScCXkJbJ0vWjQ9DBxS0a/CG8VQoHZ34mMYdniw70ZfdlhkejvFh9tIN6y9KVFKWO/EUzOPh9MyYAiSvZyo6mqqslK6VVev5WYTde9WffvrS4vzpC9Ul2lzeXlRXVVM1MNViDgLWCqDX7cPoqWGYZgWPLE3jc+fz308pK1ZYGv66jIUKhJ3Rut2x+rX3j6DX/SOwQDgwc6jAxy45gbaCUJqbbjJPbLkGDamOBLipZTECP3HbgBxTf6R3N2a/lNEk3jNE77hG8HHm/oT+QcXBShnoOn4YMjAYP9ALZq3vVrqw4/UwK9dA2BH6rIxXV3p0AxyOomAorS9yOkrr3v8GcCwextEE2z095E2wv0NQGKDv07nxH4NpW6/wDgeoIb+lpz+Xl1aSFVjp0iaOfzwRAPWR80HIdAT2LL3Vleop3nMPhPAYE2+nmB3TVkPCzwcZVUoiJi+xscd6V3KtCz5lkbzU9JM+tXl56PFIZUru2S8YX2wOj7Yma6yMLw/4j482mnyQ/xFb19nYabm4b5c8CnB+w8J8GA2RQAixd1rE4P5BzRYuon8EEBaUqyb6ZNfYthG0yc+NHr05y+aEwkjCN5OJK0j/bkOAlTUZGH+qK8r8+g6ToPjedGchJkY4QBwKzZzc7bjMqvhz8+lhhmr4n76dIq5KMvVay8hJWGhgR5qNoRoNCSSXWJB4chnmS0cO7tUfz9VXr6bmNDq7+UnZAPIgY0vLXMyBKjf6OtBZedkudnhZA54RSS03NHepHBlwCGTKyxzH4O1nnvmUx5rS2CAdtT5eOTlpEqpCGy5l2mp3xIMo5FYkpMNjo/Y8k3VVNG9aVSU5mPFMFehSX+++0gx0mgGMBbqzsFwNyxIRFTgFbbnrQ3MW4CaUekqaLq0LFW/c29ifLjgqXhv7+4ufBLLAKdZQUWwTFbMXFYQciJpg60gR0rsriwI/v7dNBECJgpd6g9CGqnf/AnJJHgiukIUviT9eOBPd7enHW0j3bCxgTy62Aw9p/v7aly0rQ3MkAHPbJVSEKevo5XYAr3wczIHs5pad9bBiBBv3Mtnw8i6kypohdicOgMXwOOPd718rscTX0Uie8YYg9nyjWdfFTMrEndH6z/2lscFOcF62V+fuT/RqGcYNlIHW7BIYMEyUE5r398gETcOyzBaxB0q9HQWGONVXf/Rw+1sOTc4IhTgqCt3dDwo79wZ/rvalixdyUkTpv5XVGR495zwv5YBG/o7iI5OkyyFujAM3E7lssyPfCltwUfe18//8R2G3XJ3z6j3fy6dQ/PG96tmsFsujRkM/FUUQvgCYz1C/oTM/FtE3qht5A515R123vfxkvYwS4vH78eGqbo6JusauMUcziPwiWMceMcpydIqlIyMnmBEim0lI2RzFLJaZO3I5/vrkunO2xf3g8mm2qeRlrSeMtYPVaj20ePAwAMcqbBAWzqRhaoT/UR8HAlmds6EB8OrSK11oCdbOamlQKrxfqIkLMDWkmXINn0IO0dVjpHOi7MVpOBwf7PpcLulp1PsKDTFdIuuDmymRBjeD3zEUWiGO08AVBzs7w0NvGmoq0xPiSZegpe7kEntiBECotumiz+rq4qxK6xOmHBk+C1JJR0f3cujFxIoFfax5D6uLSs4X1ujRkY+19a+j41q8ROlOAsceSYwo8Bp40vfZggYzMuG1Rnoe0VnX3Pc7TGE8PawJVTakt0dN1kihdQ9ks4cwAORntyWVCcnG2MCwxS6kjQKQmPuBA77Ubo6Qg5fXV1ixqtAX3KFw4O9tBHbkSV8cMEVj/sYrm3vU7fRYhHmqSfXGetEA+LCLUk00aW7mxNPueQY9oZZClGes7Fa44NRChwAiAJ4MDzYT9K8S4vz6pxUIH0YgFEVtMvaWuoxDMvL9Dvda4OlANxfWBMAts29VyxZJ/2KzjdXyt3yVGIWcic7jn47qWpk0l6IBOUQhQKJ5DEseq/aUo52UDbseaM0G+YoZMPDdeduF+bnyKP3Wikx8vRpOknB6VeWEHAR4YzNyUzWhdgJPpstTsTrqr3AHGAwIMaG2or+vu7R4QG4m1oELJjNhH5ejno8cZjteTlpdATE0FloDkBLgSARzGh9XpSZ0S/NTX6IDnDEtlW/MAx+BolsLOiKXK2XU00HTrriUvPWdKf2DGcsoS7km+gT4YMjuh51rRARMcurkd4BfIIkxWQdWXMP7ybxvmVsxCNSkAUWqk68w1dbQbRz5IAH/ybqzvo0ey8u+kYsKuma+GeKbWYno4g68nSS+hbGdxh2+3xalccA3v0/GosmazQQieevSXNi6neXaT8uEfoij8rkH+sqJnb/A/vxgKkqi3LPBgcI1voQHcHjIplOTxvzo+QkKTViRiZgM0u6h4dvgRbHtISIdezKXF2dbsyoub4fTjXnJ/tzzB4SyZ2d7a37OLvT0xPcBw9g6WmGLy0I1vKiOQmnUMBT2dqU+k9Ps1Pw21ITPBSSWiqR2N5648u2lORYt8gQ+/7uTFoBrAE3g8Hv8zNl5YUhGICBFTF78kvUlXG9zezT3vPG6hi2CboI4BcqeJmAyoj/4epgiSWA8OtYPQzXzwBys7cxQ+xhakBZgF5Y3esWGWsdR05mEi4pBAD/ItivxV8UYmvhJ2DDhAHoDi8SWTkr+hUHK+P2AO9PqanSXGtGJoB8Dp0NA/zJZHGYGBvGTNx0Z5H3+fnZwcE+4bCmM5w/8hgVcasrS9qdAlxSnNNDomGezl85TeJFagotvH6tlvgiNdVXwCYJivzcNJwY0dgiX13humW4ffZ8IwzDpsq9UgKsCCU90ScACDTQ3zMzNQ7oaH9fouwxA1YB5COdfjfzCgJ4KCt5CvCvKD+ztCgnPFgUFiSKCPEGbAwHA+cSHx0EHiq8AUAdwX7SkjD1crwZaXH4IyND/QoPnWxlMA/2EwisnuAZpdLDJtgPjuSWC3jTnzD5u4+nvR61m8/OzogSupuj1cnxMTO9ALir63nq8U7L/mZTTJgjhmHwlKmD0utrykmSTbnWlwgYArbU73qCo05YB1xHThe0VNIcQnhLS46GJQWWEYDl2sUpYLJliRPIDvWb4CUxJrCSpenBp+9aFczo4WRTRWYo2+whLj+e6SyUjDXcxn+o1FemDgwL9LTB2bB6fahf3DlGRwYwrIXlJSOY/67SO9KTi2lXb3nKNB77LYh5W17U92fUxZ5aH/w0J3fwEB2i7tIv4DbdHTlC+IdJ4zH5L+4rr/B5h3r/Cxnj4i++abrF7zDsTnMxL9f4mnt0v9+1ESOnAT27f7rVk5FrrZMT/+R+cabuC9+wtGQlJNjrZGkRwS2x+CQl2UfAwjpg76PCCQbrDvID7xlTKaCMRL10Lbj4dHQ4/0blar6ravXfHatf7qt0sWVZyrgEcrOS76llH0e4MbN8a338wVbTiaQ1LcED1xplixGJy/TkWLC/G34bj2P4+kXa2f7zO8SXd1s/7T+Hn0d0yzv8AigLd6ABuLKzNgEvB5ND2lj+WJIXtL1Sx6xyRIrSg/ku9iycClMpDru0uBAoyx442XEW5t9TtKSVhzOftKOAy4vfo8w3oAqUSvm1YauuLL4P1Hvc11fm42ZJAwAnngmd7HpkKcvMSBuv6RJEN2uzZGfBfHQkEgFPlwsQPff34tDM13BHFKaEZHdnaLAPfGXsuQKEJm49k50PgDfMaq2n09bWBnbu4SCff/WiYVf0plyXGO/IN2XJeV9Il53GYZrKYtz2A498c6rT+yqfgQIPd1sTnA3z8bDDKRd1BuafxMcz+27qpreRaj2NtvSU6M2NNU1TRq+7OhSOkNBCWjA0NohSH3MQxTD4ao09+AspWz1Mcj1KdxwfH3l72Eq5KF2lXJREUBhWpKw0b1gAJesNIX5C3I+qDmUl4C685iAGjugghb++7Xst5BnfR4U5IRdRvlNaDFg9lKeNi72Fp6tNUlyYFncBrjZpWnMQ6FkFhNQ6cs0Nup6lHkw0KfNnvKpKMTP+wcz4l0Eim63hmptQ1t54A7x5qa9yY6hmRxPNMfhgaqQ7x/whH0Vbmu/bFVmYn3O242LyWzue0Zs894pYWwuZDiRG+PDggEFvqKusqyknBE4aOoRx1xqr5s3UJaJDFYyyzqixP6COvpCEGhrT/4ahbRtwX40qiJlPptY79o+pT++/aZDxHYap47HNUIO/Ib3l6+H3+12kVnDin34JJLbfxNDR+8tIs+IrHpsb69LSdoH5xvwcVV5+npaaSPeWANxCtBwyDDYdGcqjMRgqZbExm3olrZn8vLcsmWi9CYPN95ThX1RYkcoU3J+AD6Bcr/o5Co6dwMoIvgtMWltD/NlB+8xoEXa23Bytlpc+Et8FF+I725l3tiYDvrqJogM+/qI5afTt0+UPVXNTpbMTxRVFoYU5AXmZfgHe1hbmj6zodi+WyQMHgUlPp/h0r00Zg9nbmGAGNvDnboriSyS7aUlR+MCC/Fyxw4RZE6LCfHHjDeayQ2rFdzXGwLf4yZpzCp6K9bd871LDw1Rzy1p6aoOfZ7yTNZ4kREeYoC82+xFgmyBbbkegz0FSIqr3Uyj5Y/Ctg4t/e6IArgbRpGZuOlLPAULAgFxo+WQ5NvrbK0qkYdhSbHSonQVbJuWktT4E6cqz5DyO87HICbP2dpBzIWpEmdD7+qW0E9XKGCsLqw5knRzDbnESW7mBh7nZWZsCjKmrLpuZntDopJ63NshSec2KMMbdVuFbmuqrVLhkW5skWd3W1qhx0Pnzua8IAYzoCD89LnRXV1eEi5Ku6T2jGCyjHNOHSbFuh1tIR54Q1jsKWXcWJY6NDpKrkZIQcRMc1S/r4+XlBSGihFVafbR/04CrUfuslHDWMzdbvqkWRdpDA2/ArMgKJpP0eB8BE4bS1d04EDD1okA507UzUrfSX1WaHhzqLXzbkLk33ngTlOqtFccFO7nYspLD3dYHq9XJie2Mot6Bs9nndXlRHLOHgT5OOztb9+2KZKbH44vJNn8U4cGZqfBO8JXS3sI6MNDfMzLUj3PI8vpYjcD5ToFUmghvH56gtJhazuokg5L+L6NqwGs0Hl8Ahv0rWrTpd7URtlXLZW1BKsHkynzkobzfNz6+wzD1xl6NXKhhK+N+v2veTFYG+X9pZd8+I8bF2f9IrapnNT9ay+f01w3Dzs/PcWUFuFYVRbmHL9orvVzNWQbm7IfV3m4oU0H3iW3Fx7nwTaTKMDbmi7OoU+Lq4vxoaeimpVwy3vCqKsWWbxwT4Lj0phL+q1DzANjM19WKLStNtLcxU6e1Q7vRUFeJIZarA2txtvJE0paX6W/64y/BIyR1ekwkZsU2rKuMWv9YDS7LNQS11dzVngZWgW3yEGywiz3LztrEwcYEXBy2yQN4EX8WKzsXPw2cnShRKHEEFLe1XAtoDafj4KxXbi2fA48NABiTfxwzRwv5JpiN8N3MpPqUa4RTHvapK6nX8TH17t3nZ88uxemzURHprkKcQeLKauFwFsWSC+jLAF6PsLfqDvZ7GxJwmpx0LQN2va9pOCzIggYPzmqQU6cmRSpTd+iYVsVqbHAWIXYWUn28b0G4WfGVrGwqXZzmIoBnFrzMibHhufczzY3VzQ3VdTXl6rN+wwfJtbXiPAaviGiFacofPTTYR9KVd9JCSCS78x9mlxYXRkcGwNkCvPSyoxV+lpc8BVwBj3Nf76v19VXt7jWRGm9tUqztef9u2lOWMYZflCWeFSAlzJPJly8ouvxPgyDk6QnOL6UkRuhxlbv4/Nnf24nkz3FchuikwYLT8CzmeAcl7SuKQnE5NCxlHxfucLmI8rudtZmycFNpUY528+H2sbqyRCZeTVWJvnYLiy1gxZLCbIKg8KKhQNaizsgWJ5I9TIzrs/ugqeEZSYWlRnpsD9cpY6dN2sICPHtdk7bQU6ayIhHTbAR62pga/cKSbcgyedCtKrGmjNyW+io7K5KrssIAvPE4hnZ8E5GrIDUxsubemI1hdcI5VThOf2dWd47raLFntJcF5gGCWUrYfZhbdLjaUYxld7lLNvDXUE5J3ZDJtpy5evT3EVP8lx+7pdR6JPX5fpDwZrI8PTjw1xELyM9ifIdhGiWOaOHjwd/UTPNOYwO1J2cXXRRorHCHRM8IM6kQ8Mddj+4G9dEKkZFspnz99bWHB/vYJxDamFfnpAstn3DYj7xszI+TaVoOsfgsNTXKwQoTJ9hYGmFmi4tPR3uzXTct5QCxPnSX2lubcFkGHLOHIieLqY6C3eumQjJWP9iU7WonLcyDLT466D4Y/Gi/5xR3uViYG4T4C7dX6mAryQsCEIUjjq4OltfVqB5zzQxcHdidrSmS9UYCxsCDyUn3MTP6JX4Pbv2y4qCfNGWiAcv4gYPAND3Rs+9VBrz54DpD/eF288ZiDR2KfqhSWEnlAHcTy6Dh9xOq655ulJAEZwu3SCn7lMoDHHHC66XSZwK/9o5OifNzanWV6uj4lJ39NjTQX8gJEKLEBZv9iCRJMIsmQC/4xUvAynazh3eepaYg9JWRcRvxoFj8MSYKS4HDz+mp8TvCHQtzpD0M/Cq9MJVhlMvhPEp2tpGmgr+2DS4gejAzUMFkRoY0X0d+wovp4tGw4KHw4BcB3nAlbayM46KDALeT6d2gdqfHyfExM1fM3F48b9LowtZVl+EPqskoc9MAXNHboxN9NsyTWwQzwCMsLsjqftUuubnMDId18DzpC/Gn8vKp2lqqt5ean6eOjqi7FjF49DBbo36zKLAUAPoiqUIMtjEXCF76ejrFRzRhfdHTQKwbZi8wV+ApUbQOhwckUBUeLFJ+A+lGg3lydqYf6ViYHvm5adLOYWe+OgSYmg5YOlqaaqori193vdDi47BIEriuXyL18/MznJJFUopsw5HWXJUsiLtj9atvn0X42ZkZ/zI2yGl7RAVFx95441BzDi7NoNvMDDrKkxRY7xWDp2MNK/1V0QEO5iY/gOHmMWJqeKssL9Ca8uemQXLLuPi5Nc3pXaV3e4Yzs9Rc5ebmaHWwf1db104hNfkvGbzwv69uEgxFgypQX4m0I+tfakNJ/zWPiwNq2UN+ZUZ+F3F0/1zGdximyZBUy5DYb6BJf49zbhels6RQyk5DpPLyGk/oAvdndhMwmxa45iJ3ITjQAssnqGlHjDzm09QUmv/6EQYezY01yFRsvtsZv201ByPxvqtEyDPCPe5mxj805McoG4CjqeaBxiwAMMSDvz9iXDAeOKzLNnmYEO0CGKm6LMLGEp0UmPz9fcnsu6nM9HhS7o/TYvAGHw+rvEy/vQ0AY83gwbxsSwGsZUGDLg7NlIgJhX3creIinQtzAiaHC04krYdbzQoSYaeStsmhAl8PHi4Hwh07akZhV1eWcJkWukTlhQqh9KS4MPhvUnzYnd4ACjrK3PG05OibfCAVr9K5r6umpqnkhBof9xRngYe1uSXXEGYLTcr3WEaQiCoP7ayMAoXcXHf7d1HhCH0BVLgdfTH4Od9HheNqRoGVsTo0G+9mJivL8luba7VsFVAIO+5uY34OLscwyoH3mcCbrweAZWSep6TsJsQD0Krwcin0dEpwsg62tZiNDKeyspdjoxv9POMc+Vbcx3AjLLmGKt2Xp9kp6l+Thfk5MveYfBiaXluYrsRt1S5ejmk5dKeCIHk5TZEkGVhmEDuOtpZG7QFectHFrCyqqAgpa3V1UbOz1P4+pfRAdcso/uFZ1uMSB09uqIyq1MPFGuuGjY0MymDYo57OdFxrHUkrH2IYdkuBKEWnJTFiVMnLD9+Iqe2whrW+6EbgkMhM01GIDJb97e1NvUf3iNYLbLhrV38RwxOpzhvLIMrfXiUFIryyPlgd6W8PSAnsF/zsepZK0BoqKZxogvfsjNQt9VW6O3A4NJMHvHP8ed7e+I1MHvD+jcGauCAnlskP8nCkOQqkMsvLCSuPXsbW5jrpAOSyDEPc2GMlovFSLwBjd8IwFKqbvJlj8OoTNW9Ovf0rcnr3j3xEOaimu/iRJ//gZsr90np/4YEUq/1QA5jcp7VQwVD/HYb9Co0lJ7mI+OnUPX7R2bycP2Ndk75qlExjUOsM/FXq07uf0x0AKxUdjqquebQ/3RXoiwniLtPSEp2szVkGMmbwRx1t9UdLw3fqRSI7MVDt746o4XkcJK7V35C5pxTV2xmth6W/7mkUWXDBLdCa406NQCMq8MOUehnJIqEVoERDX5E9s7t6eelj18vnpHiP5vZ4xDZ5IE4SzU+XAb4CQJWZ4uXpYhERZJcc61ZZHAbOzcxo0cZizf5m0xFNRKbcTgafqioJx9Qd0oxEnWblDT3dnTJCcA9MIw6wGXP4YlYDZ3uLEzWKozA3wN02TDozLqjlFaqzcyE5sSvIL8Leist5hDkPieoXpj2EV8zZBiIbc4ABm/FxlzSmwkheI5ixn5jgxDOBHQqsjJYWF77wg7C+vkqqlSy4hrGOfEliIp1fSv/J272oTATA3oT4h9lZOvCMAf2a09We9JV/GCDkVHi72loZwdMKr/Bv8FochazcrGRl+vXbx4e5WULSgFnOtVC7igz11gIEEmeaOW/bNW/HYg6i3NDSVKNN5BDp48lbjOBqe9uwVuNi0TxRBu2wkJaVwRNEvXuHuihpKojWlnp8g0r12hALoAgWB3KPdncQdzwRVeeYPmyujTuWIBoh3MKK26JuKYre2tpIig8jZ6pcNH6wv+cs65FLTYzU14n09khrPn087ACW6LKr1KRIexuzyFAf9Qtx7xxMKpTwYJEWBY23BL9IvSvL9MHTRN+jqWZl8wqIq7dWjMkz8DsrMkIPp5rBnh5MNi33Vb6sTFnprwIrvDfe2FOT7iQwg/fEBt7Baw+fLU0PNqcxGNh6TGWcFOaaGeNF5LlhS4wL1SOsJbpwluzHDtZGr7JdxxEME0WLuFy2PIrEu64LL+UQdrQ6uqmp79PsNdrq8T9EfIPqht275Gzew79D7dV9tZ7bp4OZi4O3d9dnMQdch5l/x0gP/kNEzvGzG99hmKYg4IBaYMsSo38PKS/f39gtlfJnDPx16kCTEqbz1Wup7Y9WP7ObAPBDyDNmsw1K/T2lkd2MzPnoCFof7DEuwMMyJuWZobdz45KKiJHWXEcBSi4FedqsvK1SAG+7Y/UbdMcw/IS3WclYE8FkqhlV1aIU5N3MJKzdgMTAC4Gf4Fze1JA2PTWel5NGiny4Zg8dbExK8oIWZsox5QbJjx1uNwPQwoT1ZIP/0uph6A2r88+KnwaZG/1Ayi814jbAA64JmHzMcACngNkLwEnCiQJcladOb0ZOZjIxYyEB7ky1nJOT4326xuP8+Oh8bW2xq3MkLfltaGCCkzVOc3Gv16jglAvgATdrM3gPALDj5CSZM5quXbbnMCnRmYZhAJLvLErU+3jd1cGkhQAP241v2hHoc5Sc/NMUKAL8o6HsWlwMHEaonSXuvsOKEYBX7ayM+AQJM+pCFTaYM8H+bjCf11aXtbsyk+MjviJ7Nyeeq4Nlfq4GTM3YSQXogmnQtOPTOzw8cGUIx73QDYbBvNKFuaSjvVnh8sIjYG9llOlmRxcRiG+cKvCnsrKrFy/iAzzggYKblavXokTAXQQf+ns7YV+5sb6KwLD0JE9atKM5M8UL96Z6utrcgnMAb2AqEUwGiHGdQmDLV+RAZCT0xfpI2Bq0w8mMINIFUbmApb6s5Ckslbr2xFLUi+dNemclwZcOfoL5w0FJHsfwRVmiyuQV4KXKzDDcVm3BehTuY/u+q+RwsmnpTeVo69PYICejH/+iuSh2n24Dk4w3LPSUvakTL/VV7t4aPwWTnRDiAjDMkvXI19UqzMe2qyoVdgtfF+ottJBFY2HDcim6j48Lc5jtFp4FgeWT+mSHiTKvYVogPtCFZSEzl3A13G1NhVbXCHvA9qlua7w8QZpgw78tC5r/Gqp+UjPVc3lK7TUgVm382el/RZ1OUF/xuLw4PZd0Xn7eV/cDiJHhbzK47//5z4CN4zsM02MgOlwmyPCb1MELjQJJ1Ok0kgW7UI8xdrdcOhGH/g51rkmEDJDY2D+SP9tfN/eGNtalrRE8ubKYUNTqkJk1GxXhYW2G6pq4P9pY/ChyskAM7OxHrnaslf6qHbUIl+rnukpmOgtxZE65eSzYS+Dvzp9/XVYmDiHk9Xov8yCjuaEaLDEpKMKiyXemRwhXIdhF8F3sbUwzkkXTI4UHNOcYlkMFz+Zgswn+C78c77bCz7WF6unRor5XGVmp3i725lxzqQ2zF5iD5daul+D97DQRSsJowctdCG7uyfExLhtTxydYWVli6ufAVlGaDy+ODg8E+bk62nLSYkOC3YTg31vC+bIfARThMrIrfBn6ghcBL9X6uI+FhxwnJeF6OV2zRunivcR4Rytj+AqYeNOvOr7wI0AyNjyZKgMcBlwBT2uzsYgQurryC2bGMjIv09JKRc6Btly4HWw68UW01+DwfARsuAX8G6AXwJ6QAI/62oqF+bmlxXnd21fOzs6Ojw4PDvY12hWGYctLHwm+7X2tTWdXWXEuoYLQGkziMTM9oYvWE4nfMzceeigMABunuwjhiZD27DGnCn4FdT9G2lg84dPgLdjJmmpro0ZHYaGhznWt6JucGCXHQ+MrRKz6pucV6Q0L9LbeWa0/lbQ1PIth0zAMnOD1m3vDED+QTDMDflFGWefnZwSnhQd76aVFantrE5dB3n5sasbpmFEnPHkCfJx14fyA+U/oNOGU9Sj7hh8TnI23ZBmCwQW7uTFUoxKG1eVFsUwf4HoNO74xmNHkcDcvZ0srjiEAM3NjpOksGWvYllnb/YnGO2tYdsfqJ18UeDpy44Od1wer98cbAQRu0xplxWmBeMJgowP4XPeThSvp42FHWsIq4uymyhEGwzDM39kcwzAu21Bkb5Ydas3M8KOMnEo4fdAu5RUknBMb8Wqnluaoqf+FQQ3vr1mW6WvPduxTHy3lJZpwZRZYP7NCxO8wTA/AHtEYkjZK9QHSXr2U+372P6n7kXkT6Rctu2t0iOdradSg7Cmd+Xc/v3uQnZHobMttzRWPV5W5WJuDqw0AjG32MCfee7AxC3AIrPL21iaAoHbVUyCBlR2MgbIBAEOSl+hnavQL2PmbOvH2SC2gO4LEMPmEHsf5+Tlx4+RdB03qdh2Af0O8DQzGbPnG0WGOcZHOpfnBLXXxvS8zJocKxgbyul+k1VdFi5M8Ra4WQp6RFRu9mSTBwoI8mdknpqs6NjrY+aLlTpldwI1MyVpwbfG1wmWKakoY4V4yhWwJk56ESMMxvUwLGpXBn5x4JmUil74Q/31EOq9235e6MCzBkWeM5Z4nqsq/2MwHjNH1sh0TfsC3O/NN7a2MyEWAB0Fg+STVRfA2NBCO8H7FxDDNRkbmZGRohL0li0a8WK/PhW+a625f4OHIkARQxGDgaMJTPPd+RiLZvSctPi1GW3MdOcJXnW1a7AEeHOyhwgmur63ocjBEGzcuKlC7RfKmThU+TRYKKCvDzZYuzU2V0qikpw+HBc1HR1CZWQvRkTCX4N6ZsR4mOllfpqdJH5+CAqqhgRoaQhQ4Wsltk543LAiGqRSwJjXuDQvwssZEQbliH8wSBIvJLUwkgIKIJhiSmFdyfGnIJF2OGlUx+2sxiEbCTc2rGq78Z+KUGELkQ7Y7uTpvXIHf9jJSYa/09YDgR5X0T3LMHob72hEcpUykAXgJDLEVXZ7A50prCMHQ4FYxMKbtZYkquT1u5eeoP33XWpQSMNiUfTzdAkZ5kwaBh1PNpeJgNo36tH5qlC0yyXlyWYbutqYjRZ4jxZ4Yho2XipL8rNjmiJcVMJg930ho9YT0iT2rKAIIp3QFzxDhhFwr6C9R03+qQaz8fFXeeDL2B9RmKvVzGnBxwD2WJ8H+6GuutPwOw6iry8/w7yfC67vU9L+WTxQ1mylX/eWSDmoSyqOwx/8m/chaiIYpiT+Tz+aNhJ/Z3K2qKOTTrHe2tmy+rB8s0NMG1uKJ9nxU+8Q2dLQxW+gpwzBsb7wBtoOJplvKFFGvsBIMg1VeHOUJBgO2tpJ4WP376jOEMsc3LSlKv+cF9lLBDJdp2JWxJ9mtKM0jLQE8DqJSlLJ00PYPEBFsFqxHbBlvB1MVDbyi+ppyrOSDBzjK4cGihJgQ2DPxn5rucmV6e15iDgknOw5OL2C9NYCUuCFEooaKKHw1Kba8acMacVjsC7NuBAi5+e4Os9ERCIfoF33JYVjaRWpqoC0XEy02hAdSeuJeu9NXw0SaUh+I/ajFT9QfEgCOMmG54NH9b6ashzGO/A+xUdLaM/1eBDqjuBwb3eovSnUR2tBPIs7IgeMOcynC3qrFX+QtYKksPhTyTWCKKlOKfw2DJCVs+aZbm9p06SwtLhB2GZWxDI1yDhhd+Hk5atHl0t7WqChxwTW0ZNTrwt0B5GxraRRkyy0WOZd4Oic728DkcQQAGRdL5eQ+9/cyMn8QKORuJsThLlzFLTeXqq+n+vqoqSlqeRmxL6oTLRofISlHon5OBC1gUcpM8TraaTnYakqJc+OYGuDbMTE+fJP0MEnOoCpHL8dzpXzd2uoywWl5OWm6zxO4Hbi9zc7a9O7OVbXHx4U5OLwscYKXu9DZjluUn3mnaPVNg6iZOdtbaI3lVMIwgM24whOsiacjd/x53vpANWAhrL+sYEAPp5pai+O5svYwnL3nmhtYsAzSozyX3lTcQsWhcoOvmOsqSQ53Q3ZEYFqVFUa0nuFPsYFOpKBDi4p65VFdVUxkMJxtjGsTHUaLPYcKPcZLRMM0Hhsq8iyOElbG2TWnOgksf7xdq/Dq8oRaspc7ZoO/Sa14o/yPmmmi7SzUIoU/O/sfqLPFnxsokVTL6Eb+c8RTcnlM/dzHNw7DUB5WHzHU87WrU815LGB+zPwfMoFzc7WSwvC8MWkM1RR2gGePaKLv5GtwhMdD8uLa2T/7pu81eJ+S3R1myBwTdTA3jrlBYqjr6bu2N3ViCxpaAMB4UZ6IaQ8Hm7LF0aLmwlgAabtj9SqZ6+HnSn8VM3uG2sZach1sTHEFRW9t+tFU88fecvwKVha6m4hWk0HowsgWGuihKVEBRdNYdbQ3gy1nys7geCQNuhSdY/DzwPz3dHcodxI/b60nTd6EbSwx9ragQHVlMWn8AOSGq+qLC7IoGdelOmWWMBY/zt8OwLAT6W5tBn5ktptdqotgKTb6CidqxLoCD5QiuBWKRDvwcPVdfUbS1fn5F3gQ3s9OK1wBV77pQGjA8wAvHwFbITdoQbdmiV2F76LCpTJo6WK99ICdp6ZWe7sB4jWjaTYwYyTP4rGfkP0y0CdAyIH7gnXVVd44cUrMV7vUdL5oIdpT21vaVMKsrCxhj19gpWtRIsnhuDvxMZ2gZh/f3iRk5XRXnmEALdtAt+2pkG3ANCq4mtfN2izfwzHHza7E0+ksLW0lLmYfJVfvajvMzqaqqqieHmpujjo4uAVekrQ2gWFYjhzXuXm5Wa4tVJ/utRVkB2DCeloD98VNRPPMZVPl0kTyipg8U3eyCsKeoqwTrek4OT6GtRo2JuKC33XBThcXF0TAqklP2T8y4MBwuTjb9MHTBF/AUfOvSyVjDY0FMdU5EQrlJxiepdOhTIBetnxjIc8o3NfuVVXKdEchqkBRo2uAWbQCnwLsxzL5AVVDcAxNjX7ZUZ60N9EIGGygMcsGhYGkE1u7MApzvO17jetOedwfbSx/bE13mir3Giz0AABWFWeXH2FTEm07ViJ6V+k9WiLycjDD6mE4E65y9bg86KUGGLJgu2qXUZyvUjP/pywJ9o+pQ72lN5HY7FYaddT3Vay/x/3osoz/D9TRa+pXY3wvSqSpQif/OTX469Sqr+bgYBXR2kiRmBmazbePox55OyZ6lv4b6kw9qr29BlkN5O+pm3nDY/pPZULsht/uLbr4/BlwCLhHzBfB+CkGetmPfFyttkfrYJkW0NkeWKDLxCEnM63vXhYJrJ6AzcC0uTW5kcyg3fZIHUC19cHqvCS/2CAnOrBXJxVuHmsI9hLgVBigOLABO6N1m0M1YEVg/6TyAaDFx4W58/NzgA1aBy+ly/TlRXVVcXpKdFV5IZHBEfJNZrQigQBM9XHhQ11NOTi+YUEib3dbMCrO9hbgnAGmgi1bnPjiedP05NgtRYbEPVKAbTcVklVVFBKd69WVpcvLS9x9jktESFtIRendMQW4mOHBXszv5XIMCdODC98U0MVmfNxJctLn1BQp7voyvO2ZmVRrW2lcOIdWcC7IE3+ZZ+HgYF9BxRvzQEY58DoDvZOdbYR0koHZIIf5MMDtno0Kv8REGhkZGlYeZkibzTIyLlNT1xPikp0FpqyHcAts6APgovwJyv/EOPIAEvMt7uBuHhsd/GpXm7aWeiL1c3ykDfVzT3cH3oOLvcXh4YEuB7O3J8EpZV+Rw7lWOB8AT0ZqLE49mbMNanzcP6WltfiLIuytAEXD03QTVKb7Kg1g8xWwM9zsRDbmMLsW46KljxhdjCrdbgp2wDNSUUG9fIkg2d4exejXmpb1vOFI1tbWBsVohOPTJaxDvTnnB+0lecEsYykMGx7svxMU3ZSFgOtA3hAV5qv7PHmanaKptN1NI0ucgHeVEBOiL0EzZtnnir4ZfV91tuEZZWFu0FYSvzfeuPq2qvtZKmo7NH3wpi4DDCVWZCaF/X11GfARwGmzL4vfvyoGG3o01dxXnzHW9nTxTYX6SOxwsrk0Pdj4yS9s6FODr/N35890Fh1Pt8CXhnoLidBlYlwos6ZD8yj/VV9vlzzIyzIsiBRMlklbwiZKvYJcWA58o/pkh5kK7+5s10AXFsFgSMhbpX705SE19ccM+jRLtYFvi9zVHPgb1EGbfm7k5Qm17CptURv8za+FVftkTF32hO8w7GcyJBXyp0ILKouTYWrkv5JnnC5P734/SW2hji83db8IUCL+yMQ/0yATffAcPV2Dv4H6Qb/ZAb749tamAk1WaVGOot/AMXS1Yy31VcCi31mRbMszTghxWemv2htveN9V4mLLwvkrlgnqCSYwDEyFZKyhqSDG05HLNTcwN/4BwBhOoKFC8/QgC5YBj/MYTAgYjP3xRqwhVpwWRArQSe8TuEoCKyPAJ/m5aUMDb3QPuO7ubJPuDgBOYPx03OfJyfHmxvr+vkRTbuXu7g5bazNcT4L9fkCGo8MDyoC5MC8D65jxOIZCnvHmxhq8jrmkPVyswS7C3cRAwtmO+352Wh3gUfBU7O1hm5oYCQAvL9xfQHsA4PrHOPCpjCyU4UEywel66fiS7u0mQFJVRfX3Ux8+IJElinpWVYwhR35u+hd7HDDpv0KfD24Js7U0KvBwzHC1ZdOtcaQdC4eHPW3MA4Qc+OvrYD90mqhtLF2e41KpvJyRuRgTNRga+DYksEzk/NTdIdCWY2v5xAJ3etAYD155E+IPu8VsJcx0HEwAwP+FeeL6mnKi6QTIX188dfcxMtLiyBOnRQLq9PSUcEXAKeuYDYPJj+Xa4WC05kOHneDchSXHcCQsWIqd0tJ2EuP7QwLc+GYkCXYTHuPQwm5w0wHkh9pbprkIK7xc2gO8uoP8mvw89xLjpcln8c2PD/yptJRqaUGJstnZiVcdXI40jAUnuL2N8gaAM3HjKL1KP8jL9Ds7eD45XGDHN8YT+Ba+CvjT7X1QsHOSyY8M9daRxxzuMg4tAVZXZmXUdOTlpJGDDw/2glWuv69bx6WetDOh1ju91ksDPkmOD8fl7rY8o7muEjCgH3vLvV0sOWaoxD1YZDP/ugzAFWAzjMT2Jhrzk/xKxcFgWOHNGKRh+a/VgWdgWD90l+JMlxpdYQ0A5DwcOGCpWaYP4oKdl95UHM+0vKkTJ4a6MNmztEtlk/Gyo5Wsrmxzw1B3Dl2CiAoRx0tE9cmO0SKLoihhf757XoSNo7UR4ayHRQ+WO9W372IfBd8R5vnPUGRcHbBxdcZoZvlL1AcjvTGuAeiaeyjf8/BvI+r87+M7DPsJBsxpeCTwRNSuF/DojRyJratRnwBIjBDKD/0dDXKvcwbybjS1FfoujmeuTiZ/fveNsBszJYz93fmbQzU7o0iEBMzDxmA1Xtnhv711YkBKgA3AwD9N9MMwDN658vYZLOWAtVAdI91w3FgQczDRtD/R1PUsFcw/GBt4sTg1kGg6wy/5yf5sWdTtpi3Q16XzRYsuATmK5gUGEEL2mRATItnd+Uku+Mb6altTzbOy/J6ONnAK+TIOemV/wsLcIMRf6ONhZcU2xDlM/Dop4CRaZ4VqJ5GIQ3w5Nxck68gq8XTWEwVFOqaG20tKzHN3mI0Mu5ZSy8hAErdDQ5REsUaor69bnRJNvY+O9ib4RoVufr6MTNlHwH4fFV7p5YoVqwkusmLUnmW72Q2FBV/hs0sXHyEKE1mKgy5c/JScvJUQPxUZ6sI3Nadr2Fh0bsRCxkEPv3A5hnGO/LW4GHDKLTiGCn58SIDHhzlpePXjwgdcHIt6An+iCazmIJMzwMdZC8bw3d1tXIKLhRm0UC27Dh7OcJOnsw6JtZXlRQGtFgBQagbN7QzpnKdh9m5CfF9IQE+wnxvflDlV2DLCFb5iGTDiv6EnA2rFhLkB8D7SgVcqcl6Ojd5JiDvAxDBIDULGwSiF9Cihup+YsJEYn4eUeh8p5wzjogJlqhsGFUWhx7tIOsxPxLOkSw/EqbE3nSOTE7KtWbUdryqXZ+l1ZIXZkDGCJMaF6j7lAKsTMn2y6dJVeHx85CITSWtrqddz4BqJ0bGJwV0frN4ZrV8feJYV642Z4s2Mf9lWmrD0phIMKJ0TQ7HO8oyQ8edPlYEWKjlBH68GJDbRnr+tRloMrPlMR+Hz0oTkcLfClAAw9DW5EXAwpCVMd+Hmvt4uXDSL6Pi5jxN9LQcKPMboZjDcD9aV4wqvjBR5FkcLrTiPLWVaYXbWpiPDb2/b9dkCavI/VI/Z6+AFqtViSjPra+zky5VpcbXUmW4p06MeSlJFfR/fYZiWg3RMHr3Rdk4XIQoNXOyrDusoTNmhvy3r0fwNdTXpLo8QAJNSlPp+05f85PgYHPTuV+2HB/vaEemWFGYrdgpxHttbm8y+KsZrPQm50ZUMTRWZoSZGvwCsVZMbSeolJGMN714WYawFP81NfkiL9ACrQFdZPPN2tgS7AlgCFmKwEETTGSBcRoyIcxcMk8p6etq/7XutS2gT3IW6mnJwHQgds7KFBkMODkFeTqpe+HmVsSB1dn51cvJ5c/N8dvZiaCjTzx08MEchCye7KFouCefBOKYPvdwstpbrnmb4sk0e+Hk5wl8baiuk5fJ02HtmegIbucz0eI0P5uKiJjqULpd6VOTpqFl9nWqqiYzLtDTwINNchE48E2PzB6nOAuQ+ZmdTtbXUyAiqp7phkD6i+JjgL/8QwU2HO05ErhiEJYbV3m5r8XGvAn3ARRZaPuEqYSRwggE4hdlZ5rjbi11t3flmKc6CRj/PFj9RsrMgxM7C24aFlL5o+pPrRaGPwAu3AZRiy4U353s4iGzMOYwgNCZl6XrZzkw4kBSTv7fT15wKO9jfI6pfoYEeWuyBST4Bm3LGWNMoDM6tac0XQklFuth8GqIvY8oWhQxwZtanlGSY/LjHDyaGh7VZjY9blAMPY7ObChdJWSzdCgiz4gnMGZgPZSKXWh+32cjwjfjYvcQEAGYA9lr9RIWejvBXzL5IPu4gYBG2HkKHYMl+1PtSfCJpXflQ5ebAxgyut9CrNjdUkx3exF5LyP3srM02dOOXJ9z65Qz+JMDMcPxhQSItdA7W11aYUZUgP9dzHXpNMQ2StD+KLvjU4xgdGcAzHCxgepTnwUTT5jAKfYJJBVQGFhZeTwl3P5hsGm7OWegph9dxZkyiioqDFm5uAIO70l8FgGpzuHZzqFYdJPa2MTPSzx5bYZgtzG5nXTTczs7OsjMSMcUOYDBz00dpgbzpcu/RYikGw9tkmVdLmpM938iSI2e3AqMG657eLjSK7/+unOxaj6pZe7VyOnjE36Zbp+7nHeojjxr6W2hX+43U9/EdhsnHfiu1FoZSunc7dvtX+x2ofu9Kh0KFRdtrMg53Pwl1DGGEv0GdqsfBcDIqw29/hdrO/UbvzNmnT/HRQaQiBTx1gBCaVu8Qx44JwxwFpu9flSiH3MAAvG3I8nPntxbHMWVJUDasv8rdgWNm/AMs5cWpgesD1Wezz3trxa52LFp8zNDexhT+y9wn7CEr1hsbADBIT7NSYiMDmhqewVZalBMT4c/0w/B7oiP8dKwMAeiFiblwlB0wD7NiilzPoUHdGm3hICUSanGRGhujXr9GHGhVVVRJCZWXR2VlSyuOMjKnI0NxzRsc0uuujqL8TCxQwzYBDGY5O1FydtDe9TyV5mD8EVyT8bEhfNjilJjIUG9/L0d8idwcreDqDQ/2T06MqiD2vWEMdXdiMsB4R2stOsGu6A3ht8zM45TkTppSArAKLuSz4D4eaqih1tao47sL0qanxjGeBN/rp3qa9vYkHe3NuHSNbHAucEa57vYHSUlbifGJTjbYUVaoPePS9JKY3J9Dk2rg+jR4HZcX8q9XpsFfYx35g2FBkqSE7YT4HDd7eIUwNGIHNz83TZmlnRSb1deUf81L0+bGOuHTy81K1mIPleUFzBuhI1H48fERaQWEZ0TLnRwdujnx6Fv5WKrarCj7lvEy0IcrI+foDPCRJCXipq+jpMSF2OhiTycLGb8ilkdHZYoI21/LlcF/MWbDGVf4xdbSyJlnApsDz5hNzx9lhYnwYBFB5vV0vAZHc2orIgGGba/UBXpbW5g/uj1qMz05dkdbzvVq3tddL3SEIng/sLJRNDlt96t2WAR02f/qytLC/HtYLYP93VZ16+bCwsqwRYf7YTEAPQ6iCc41NyhI9peMNawPVtO6YdU1uRG4+5rHMexvyNwfb8SN1u9eFikLcmL6YngxNcK9KiuM2OXNoRrY4e1MiTOdhXxEt/iQVFyTrbfnpdanBiaVaG9acR4LLH/MDbMeKPBQwGBjJSJ4xc+JxWXJlz5/b6eVZT3xFl7sU+vR1NBvSf1D8C0vT/V5CzfipXse+Xuo60wnN7uZGvtv5QTgO3nfYdV3GEYCgOXS9NRO0Rf6xs/b1IfHciS2V6vBw6BRIm5LLEtS/61vVFGBRCWZGyCZU00kaHC7EXOzQFXpAhyWU0mEuDmE133FNf1Zdniot7CvPuNwqhmMytuGTOR1mD3k0ebkdU3a0XQLM3oHPwM9rTFFB0AL5cAqQCZxaiyhjMcb+BAn4FVp1fQvC41fgCeBC0Jw0VdbSz1GL4D98IuaReM+f6a2tlAD/du31PPnVGUlglvqAJuMjAwXW3OWwbUeErahvxd/ZqzoaKdFst6wsVjj6cK1pMPYFaV5xLu9aQNIBi6sOo0WHxc+2NDtSf5CzufU1Nu7wq5USl2JxV1BvkWeTt4CFim0w4C5UxOpKHAfwfrSDAr251+EKfGWNE5hnpiJ/7GgWbKzzVFKMpz4h+iInmD/NBehFU3XoYCybpKWQgpsHFR45sI3iXeyhj3ApdtMiKv39fC0VkyCwVZdWazy2EjKbmToq5aSf93VIRdMf9ur+RN6SWS1ZZkZnXS9SVEibM9bG7TeDyAEuN12VkYHiVJ8RT81pDNQHOVghROkIXYW13hu4PesbABpWJAA3hDjyM/zcEx1EfgK2Bh6YbVuuir1GsTiy8QklKEXc2MqU8sFmswMOlqSj3dbt5ZrSVHiLeogTD76nMwkle9pqKskX1qvG68G+OtPs1LwHYHfSYCMbIB/tHcldNNZhoUIL0qw1ek76iGR7JIJCRYwIcRlY7AagNPGYA0ArfddJXZ8E5qP/mFSmOvuWP1URwERZT6YbIKfuDcMfh5NN891lUT42pmb/GBvbYKIOmQUi5uqlKAJcoOfUf72lqxri0+AjzM8eiWF2bqcHYkCYF7Eyjg7Og/mycRgGJKJg/hMQg64Jqv64kE5fot4AolnuHYP5e4XEmrRhtrOpk6nddrP2SI19o/k/PIb8dT38R2GEWtITfzPcmbPM50fj6vPaNZuxFL7TXdQ5M8byyblb6nFirFbQk3+MTraz5q0+a4GyAlPD55/W/cGVjqFZBHZUhIicDPG+vrqh7l3bc11YJWHB/tVFvEnxoZgv5kUwWMNE+Zqrlz/QH4HtAZWAVPr0iwd9SczCGtVZIZicS1rWgy6Ojv8UNYSRlqEF3vL3ezYmPADVv+b0OPZ2dnM9ATJA+DFOjxYpGNvzMT4MO4OJ0QCvT0v83PT8FX183Jsbqy+LfN2fExtblIzM9SLF1Rhoda6vVsJcR7WZnKtKs5jW77x4mwFHcCu31mtP5G0lReGELJpNTdXB0txSkzXy+ebG2s3XdjtrU0nOw6PVmdGJNpqkXOk4/LF2ajwV0G+YldbhC5Y1zI5sjC2Bn4zTFeYALjlaW9P8pM/XB3tTY5ClgK/gq3lk3QX4WFyEpWTS4kzRsKCgmy53jYseytjDMlwmgI3AuECM0w3D250gJADbneDr8cxfDwz61Na2vMABDPoS6fCsf4wN3tLigmQ9tdwlW4Z4FtLpZbsuHsSjdm6EBiQKYbh7cXzJh0PKTUpkkjBareH7e1NH097DLkrvFyW42IkiQmHSYnSCEVGJvzuzDPBWawSkTOVlSNHaHTMpcVPBJMEaxJMR4aixLg44zwlZTk2eiE6stHPEyB6sC1C2rgCFreNcWkMz6UVHcg0w+XETC1vZisXLmzG2bBcsS8sJstzlS72LB692N6ikgznSMq2n7eq7oZamH9P6PsBJulLMVxZYh4bpoH+Hj24MpeXJ8fHGh0qLj3Ax6CjbJ3yANMsa5p6LLAymu4sBFwEGIw2qfUAyfzc+DhACTCpOid8sCm7OieiIT+6KDUwK9arqTDm3csiQF/vOosGGrMCPPgskwcs0wchXgLUY3ZXYxjY64WespQId9yY7e7Ehwe2r/cVeAs6cq7AKC95SpA8h2WYHWI9U+HNBGAjxagWcajQM9iVzWXJy7xDAz30s6xdfaJ2CqiR/1KWqvqvv/Zuq9k/Y7i7HdT38R2GXRvL7vJwwoqPzo/HGTX6D2Qco/w7ABvJiQ3+ulrZKti5FhJ1Sw7yIMRh17dyW7pftTNtlZ21mUJnS+/rl7lZyQquTFx0kIJ7B2YJ85iDSXjb99rT1Ua6epo9bC6KPbgOnJQ3MBhLbypai+M/dJceTbcA0AID8LYhU+RkgavMYTMz/iEhxAUx1DNsAy2B0vg00Y8j6wYGlHWnjQRbSKK8OO+nY3vM8dFha1MtUxDMXmBOdHhQzmHkekfKp0/Uygqi+KuqQi1P+mFsz+qLiQBPC+4gmEPUysx5DLgLANjuWv3eRuP2Sl1MmCOuJiJtcuBIVZTmlxRmg09ZlJ+ZkRoLMEalQLOdtam3u63K5Amm6uLT4fYFlUVWStvn1NSOAO8we0seTcOtnMbRLgdyeXlBaPH07vFoN7Y210MC3BW6xcD99bJhDYYF0dWYiCDxNDlJkpDwLjJsPDzkZaBPvodDjCPPhW/qI2BHOfDiHPlt/l4AWS8Q5EbMCiNhwbnu9o48E1y3RnYOt16cGnu73jHRnbult+eriN5dXgb7uxEJYC2SEoTsXl9UATAyZJe3rrpMuz0MDrxh9gTaWPzowDN24ZsAcAqxs6j2dqv2duXSOAfurLu1WU+w/yXuGZPBMLj7OIMqsmEB+pIGPnDSjJ4e8MtlWurHmMi12Oj+kICOAB+xi22YnWW8Iz/agZfgZA0/89wdKr1cYF7BL/ZWUvJDeMwBHZFDJdLkAMPqKqOOd1s3Fmu83S1x6uOW+bO/LyHxuMqy/DsxLXyvXqhiULrS3Za0BjEXYR1FomE2wkIHk1Ajkif4CFFthoup92cEYBgWssOd2HPdJUQlbGOoZm+isSQ9CEwnX5Yug7sMP63YhlxzA7bpAwuWgZBnBB+05RlbcQzB2gKgChLZfOwtl9xFkwhvgO8K8LBm0aG9tKQoPcZ0ZqbGmdz0Hnamb/PdmbWI8PtAgUd6IM/d1tSKcy0C9bKjVR8ZqgPEgki81jkD6nyd+srHRgLKc0z/KeoA0ss4+4iYGuaNUSrlOwz75sfVObUokE7o6X+tPq+g6vF5AxXRSp+Qv4JUvG4fANWkHKD/BeJFvJcTvJCqPdBRk6sjtUt9ri6vLj/f34W/hYsC/hQa6EECdeC/zn+YBXxFzOctG7hHCjXuxLebHB8B515WJmHQ/Sz1Thi2N96QGetl/ONfAO6qz49+WZkc7mOL5Z7hJ8ccZcMyY7zWBp4ptJntTzS1lyWCRSH16GpWfSwvfSQ5wIgQb70EYqcmR+HESY0i0+3uxDH4nR1qYoJqbUWlhvrSy3r6lKqrQ7SBa2uXJydzs9M7O1u4WwBxnBj9UJQbeHbQLllvSI51BV+KHFV8TLDKLgXwM/Ykuy/aGgE8KChN43mSnZGo3DaWl5NKd0AZ9AT73U2WmJn1NiTAxPwB93pnFOwc4HFTwzMSBBW5Cg7VblHDIyczGX+2ve1r6U4mAtkKPArmrIfPvF1PUlOkYAwLiEkFwaSNQGcpyVLW/qxstNEedneQrzmdx1DgzQMIvbKytL29ib1Pe4H57q6KrD5pt1CQ/vvaxvvZafKQRof7aUGrQxTzbtew0mgQ1grtetUomqKDrLGYPJNL1xBy6F5B+En6vjASg3vdE+R3Rop4s3OGw4Jw2aGvgI0E+lTnn6V0o9IZhfgY0+ViElLBOimDYri9JSasj48OYkIaHFCDlQRuxORwweF289ZyrY+7FYZhni7WN2EnWFFJ9OGWFjImk8fQwBvd5wysXYRaIzzYCywaQWU6oiBiIwD5wBMND1F/X/fdzvznzz4edrf3yOkycEE4Tnb5uFiBidy+FqasW337LDXCnW36kEdjFT4WdqdJsAB3YTYstNGxTrCkod62S28q9hgynio3LOyZFukBppmPqg9YWiSrb7HOJE1qyXkc72PZ+9SN8CJiDDZZ5pXga2lu9oiJwZLiwqYnx/RwBNs51Pj/KMdgi7bfjKeto2vNHJIqKeX48N/Vcy/cdxj2U461YOm01rFD7NMHOak90lD+h3cIPlzsU+/+b1lV5B8gXo37GKcT1Oy/x99yNfjbl4dqIbErgGFXF/o9EPBcwUKUFuUmx4cDXmptqh0ZfgtLm8LbmIKShXkZROZ4ZXmR1LLfsinQH5FGYdgtIDEcooOVva0kXjLesH1LYcNY/YfuUlueMTIMbEOO2UMwBjgJxgMfhfUo1FvYWZGMUmRKxY37E40JIS6EI5F/MyuXwmiQFZ2Dw3oHoa2mc3x1ubK8gETxMThpjI+knlXrLKKVjvBbRQXV2Eh1dFADAyilpgSl4D4SuhSuGaKqfzdWHBliD7/zuYgGDZx1mBh3SoSB2wGYvKQw28/LkVB+4y0syFOB+Lu5EblTLLYB+Gi3wbCSEqq9/WxkuDQ1jugUYfwAXzRNK2IzETIWmNZokIaTep1VXPU4JbBOlDI+t7H40Y1vmufuMB8bdZKWeglOc1YWSpAC4srMRD/p3z+npo6Hh/SFBFR5uwbbWdjQn1XIVRbmibEyz9z7GVJzqKx3TOs1iXAXJTPv8RWO/jfd5AQrSrXpNceYHM60uCALr0iRoT46xlzwVIctNSlS6500Ps3AWCvYlustYIlszO2tjGDDGs2Yl4W0C8JmY/HER8AKtbNIdrZpD/Qp9nRCzBycR42+HnRFolh7mb709IvUVH8hx4KuB65lQAWYHngiwbLsbGu+OFuxt9F4sNWUn+UHiwkRnbvpHEn5t5sT7yZy/53tLUJmI06JOTs7GxsdHHzb21hfpR2VxenpCSkRx9WVMHOILLUyV42mDzIsU4S6U8g3aW9rvD1Ju729Sd7f9VL/DQuAM3GwDKyns9D8Y2/57nUruUvnrPKT/OANbNMHYCstWAYCyyf21iaudmwkO8lGVIqwmZv84O1iSQt73oHBwOyOP8+LDXJiyUrcwSLo64z29yW4sgbTcrgJTQYKPCZKvZikiH157pEiLuZOJPeiqrxQayk/xvq4Ko/aIy2if4latqgr6ldqnM1T8+byi7CZ/Ct19r8ChPVrYVeT/4I61ble6HiAev8X8oky+x/VyFb9Gxls+wfUxd69nN3VZ6TmLM2J/Q5q7vzi4+BgPz4mWFVRmRmYWMJIcXl5GR3hRyroFBwyZfb5xLjQqcnR/Nw0JoUDszRxeLCfafzA9GIYBjiqoyLpcKr55orEuuW+Sg9HjoX5NYYJMAxCK6MX5Um7Yw0q82lHU81vGzLpkkUZOZLIYX//7jsLflhKQgQBALqwdNwSBG2qrwLDj8EJuE0aC2qB/w2gpaGBevUK0SR+/IiSaXexOwKSCaSr8hCzOdsQcKyLPctBYIrdptBAD3BENBVfAsd9dWWpKD8Tnw7eHIWszhctxKN92/famtYOTsHk8grnUlpK9fZSGxuUrPhzcPgt0dYE88kkmQQYRrJw6pSYKgxC6qCd434vAcrLS4CyhFSTmWPE4lHY4Q52so50FeZGBs6+ed1VVpju716bnfa6MLcsKjjUkYfzJJi/hNnJg1t0mM8veLEyzhh35au3LlNYcrLj6DGAfR9jZKhf3tOleW4T5jnO9jsIWJMTo7jcGq6JjkdFCh1hcmq9k4bqMsSfYfHjamH+eU7OaXLybkL8dkLcTGTYq0AfANuxjnxPa3M7Wl4MFhBztoEZ66E5jdBY9BzAibJIe6sXgT4rcTEnyUnMisRrvWR3wbCzlBQfARu3Zfa96SLRHFKRCGu4rydva7lud63haKelIDuA5NXB/d3aVM3AHh4skhfH3szSDkCOdNWWlTxlUihpB5iJ1hmswPBfwHUkrKNF+pccQ/+bbgVhQLypTDiTUfOslBjZufczen9GSOMZoCwHG9P5nrJdpWDlzkjd/nhjf31GeUZoU0FMS1HcxPP8mc6i+delg03ZvbXip4m+OfE+rcXxs6+KJWMNtzeDHc+0jLbm2vGNiVBnSICHvirAZ6YnSB6Mx/3Rgm1Yl+RAMBhuBiuJFroKTVAzGFeeBFuY1wd9vKQChemJVzlvitrD1BnrsVe7lT8TF/24X15rNvr71EYc9Ss2fjV0w670VIMHyIoJ2Vfuoqg+W6BGf0/65iXn+zq7k1Fq9O/LJvHvIWX0Lzj29yWkzlDlJnITAEhLiAmBjbxYVVGoaBozEpmfqiwvwK9//nxOrCZsAL3IR0huDctTEgUnrrmBnxt/beDZLbXme+MNQ03Z4J6yTB7A+wGPWbIeBXra9NamA9ZSSbS4P9G49KYi2EtgIaMHBFv7+pVatIQb66ukfRzcCB8Pu57uDh3psFQO3HrH5TyKduDJWztuq9bLRNDr9WtqdRVRd2jYsQY2zM7aFEevAYaJXCzsbUxw2QnGYDpK1gBAUuCdI+0xAAPoAhJUJXWJyRLhXMrLqTdvEPej0omcn5/BDIyO8FO232efPvl42pPaV01dsdFhKXV1fm7a17PmkccNPLn+vu6wIE9lFkTwg3F9Go9nbIEFwaQazdeq1JgbOLjNjdUK3wXeHvY7VbL2k3AJXFvd2+jvddTKXFjt9L4IibmboxWcNW5wDfJz1bERtLfnJd4tPAta7wTXqVpwHy9/mKVOTqiFBfTUl1cg1haZZjdgs634uIWYyDZ/UZ67Q7yjNYAlBytjS66UC5EnJXQxsLF44m5tluFqW+fj/jrIbzEm6lLaKiYrR0y/OV0mFsO32FkZI6lxnjHhl4Mn1FdkTxrDMpJFh9vNO6v1AMOelYYzy5tv0gAg/beOQvbBzdGxsuJcEqHwcOYzS5S1ExMjUUhMEwqTnDxupUXaiMqAi5+TmaTyAUxLjr59gSLKJRlp+nFncWkufOnp6Ylkd2egv0e6gNAydNMdhXvjDTdJex1MNqFtokky3oA7u+BFRJk4gV5EFFm39oPtjNYvvqkozwhxFJgRXsQAH2cdc4zMQdoZAGLBFuLG7kctYdJCxPESUU6oNVJnlpEiwgJYmJehh56CqzNE9Y61tnD3yk6+Wp+T1F3NGdJlVv9IXcz2NY+jXlRchi/C+B9+Yff1Owz7ZgegL/XJPw67qOHfkb75/Z+jYsV7QmKEXWfyX+iBGVLNKMbRIVMpBZwPMD9OthyFijLl7f07xfq09rZG8tfnrfVMx4Ws+wq0Y8T9rSqXgjqweQSJRfrZrw9W747egsQah1tyqnMikkJd/d35DfnRYBL2VVWoAyo7mWl9XprgxDAGyiYWlubz8/OLiwvlNXp3d5vZun1/FSPgyoCvZsU1FAlYJ6jJJ/3GUr1Xr6j5eYDRlFY+okSym5IQgSvfwENysjUb6s0pLwwFVwZ3zVWU5mtXsAHoGlwQApbOzs6YpWKkiun4+AgQPv6u1fpaJHR2cEBpZR3hfpFyJi06eT4uzGEQkhgX+vWsUjUMRDE48AYuY1tLPWGy0XSDSw2XKC8nbfbdlPJ3vZ+dVmYeJ6OjvenOcrKvYYDvS7Kv8LRubmjcH19TVUKY04ZkrBioQO5Ap2WfKGJFR2jPgd7T3Yl38obZXwTPy+EhShqPj6MO0vx8BJ+wokMGglLnqSmSxIR3UeHPA7xiHfmIV4OOfZAsGQbtgMoCbbnh9la57vaNvp7DYUG7iQlSClOM8XALIoJniLB0PjoCRwG8bTmfJVKWBbhKhPbdzOiHZ6URx7stAMMAjA29ycHUtdj7b6gtUT5BAD8kIIgah24mbyBABXByXFQgLsIkpfIUnZeD9QfupprXtrQoR1rlmBqLX3lWUUR4XyUaZoAH3/YyK4p9PO2Z9nRyfOSWz36YmyWf1bEPk1gxWGlfd3UkxoZ4uQsdBCzmsXHMHwJGOmaouehr26TzYADwUsLdTI1+aSWLCqmUh9F6kKWJT8Ow6gR7LAiGlMFoQo4wd44FW178AvdXoTZey7FXT43/d1JFJURe8KfUp7uqtQFxHXZScw8ZBVn//tuGYeAMzz1AXN9YOHcz9b5KxqTz+evl/PgOw7QahNBm8Dfv7vuSVDA6L4XqfsVBB2Kh+byl9oNdK/+W8f+e2ky5L8jHCJIRe4Yjtbh5emtzHRZK8PkIJ5XC5ihk7+8r2siV5UVcgBHs76bwp7EReY0HkytsQ8ac6ytyAFNBMTq8UWGeyYOceB+wELfUPMAqj4TCaAVJeNtNamM7o/XNRXF21iZcRhEjYE4mb8QVPWhWq3NKFU8JGHWSECPXYXNjTe/35WVHKx32NhwLD0YJIlJwWFaGlMFGRlB/18WFbivaFa4vBZcI6zVPjRS+akuhixKlGEzrnfvT1XTilBjmizCdcKcNs7e+rqYc9zvpWKAClpX438UFWZp+fH1tBRfd/YQKziqg0Ts520RTwzNypgE+zh4u1ncGSsgGj1hMhP/tZbQkUAL+qPJfC/PEyrzkX+GYnBhl+r7MslX1IiDnhDMzOyPx5PjYgy52srM21TEnTKo6Ra4CjZQVr62io4N36zEw+VSzsqTdoSTHlZ6+nYByZYOhgTnu9uH2lgLLJzh9in+SzYr7GACbI88k2dmmwsul2c9zKCwIsNn7qHCEzcQZM5FhXDoNCzuhVqVr4Mz0hLQi0fxRiL9wea5qb6MRYJhkvWF7pc7Pk0c4Vx2ErJ3tLWUYRphgAPzc1BtG0RJqxKeffTfV1lTrbMvBSAyQBlxhsDLqCAGD0cGdkFXlUl4WwCr4T7AikUrgvl4NeIyb6qsUqkLGx4ZwxYE6Ca7W5lry2aXFeR2fCNhbfU05GBQFgRkB34QvrUs0tOMb9zdkHtxFsKHptj/RONdVEh/sbMVIy7s58TS6mHcGkUkulG3+KNKTO1HmNULnwSZKvWBL9rOC1/k3k4RphT0OqA8/ynWZB3+D2n6qVifYu/9X7uCh8kWzb4BH8ZZxOkHN/Ft5IeL9c31/h2E/u3G+Jqe1Gf9DNKVuHxtxcnWFfbX4i68WuNKmMvX19ZZdrj2oW/dbIoWdYFKrrdI/GBnqBx+OLOKujlbZ4sQZmhdBeayuLHW0N62tLiu8zpTiYbI/Hx7sk15kcDKIL4W/DndwZcZ6ve8qub36XEFPTMEYbAzVZMV6sUwfMO1BbGTATTQbe3uSuKhAcMi6GfWK05NjDbUVix/nFaApyePpcYD/IXITcNiPyoK8z1++RHFucK2Oj6krvXX9Yjo4AF2WrEc56T4HW01jb/NQbxjHEPx7MN6Kpkdt1AdzAMMtZaqMnu4OZxnbG/wC4Gd56aNUT0mVZLBGAQUShifVsBpc8IN93AXk7W6rBzutpyGR7JK4NZNg7dOnU0AIG+urAJXhrOEJglsGLiO8melsgcPX0905OjwAd+TOVkZSD6zy6uFSMdi5fgLJ9zaYjNXKVdN3jv19CeF+wO47bs+Da6ujQ0woZJxsOQf7ezqe3U2aWsrrCDU7i4h5AJIRnlWcK8MEm2lpa3ExfSEBld6uoXYWIhtzN/rJ5dD6YEyhMJZM3JnHfexhbZbgZJ3rZo8IP9gGBR6OSMCQHkcyt5hrZtDTmf5pvw0WlqOdluOdltO9NnGiJ9tEXpeoMtUzIuv/vB36tjTVyEUCaYKlV3lZpAp3YX4uIsSbpMvGRgZvXjcucdUGPEoKegxwLrhXGS/yHxfmAMxUVxZXluUD0FqYfw/3dHtrkwn1P38+J/APPy84NZcULxUlg4tDSK1UDlh8CNUEYAat1eRxHgyOkEn7RDYXR6vtzfWocD++TKsTDnWwKfvo5mZsjbbtkVoAdZMv8kVOFmzTBwQFwRoLBlR/TvkVqU215Dx2sjF+keE8XoryYPCzIs420IVFGBE9XW3e9LzS+noysMcMgk/EQxv7A7W8wbMFCnuDRAZpPfzb9p9PxuSiz7P/QbNCxJ0CatmD+jT7c8IT32GYtuPzplzpfOKf3J0dJg/SwK9Rkuq7FomLy8k/oQbwNP1Pai8tF9T0/y5/XO+vG02WcsEbQI7bW4EBI4F9etXZdkuEkulzgA8UHe5XUpjd19sF5hbL46rkBH/e2oCdcqaHR9g+sPaXvzsfE81raAzqDqeapzsKwD/nmD0kZQnSqNjNxBWEXdrf2wmM9MHBPlFVAnsMdtTdic+kmwMLrfe7k5KI6EDcXfhHalxwTQeuvALrK7B60loXf37YPtib7Wxrjss1J8YU5RnglsEdVNP5Az/+FrUluFbE0wVXaXd3G3NDA5bQse6LdJXApdO0kwcsOm4FAYebNLr85AOgL2mr63rZftORA3jY3FgDnLCzvRUoS+aovI93TgmVTTt7exIMnr09bO+jGVJ9t/LOQYSDrdWmP2UO0gIHDzXuXSGTSkdi9K3NdRyboNvMtMxjwy3GtQP16glsXHcEz5Dg+8gIVV9P5eZeo6fH3PRpaWcpycdJSYux0a+D/JKcbAKEHE9rc6zRh6WcZdjMkEsz5tMtrHTGvq9PuvIvL4JDj4gfBKY15ZERwXYpce61FVF1ldGVxWHrH2t8PXlYFxgW9qz0WOXDfP9umuSgbqERv/j8GXOBwFWdGEfzvEx268Hpl2xvvWipJ8p4twhGMyJE0oLPnMxk+bS/roeprMMB3w62AFBW/5vutdVlksrDSTmc4Z//IC8yvLP1dHdnm2S5mxuqdZlyYN9JiBNXHGCSHkDUae52ZyPDq+/fWcvKEyxYBh6O3Mn2/CNZaYkW6GtnFBWk7E807o03TrTnu9qxmLUncKEmJ/RGNw3GgrSEcViG3g5mb/Lcx2l6+olS0cssF7g78Lo0Me5hp2M2m4bsp9RakDwJhtJZJigzdndgMoAa/m35p5D3+Ozb9pwvj1H6C+Uk/rbG57KZKmul+50v1nrzHYZ93eNsUZ5XXXa7483nK0i+jGSi75R1XmDLn719tTm7ttLln1qPuqfzBpvhKGSR6h3lCkOtB1gdZRUssnk48xVIzwEOgR0F83Mt5HR6Slq9MelWTrzPdEfhwWTTTZWHCgCMfmd9d3Wak8CMY/6QeQylRbm3e/wkM4A7DRQYJrIzEj/MzTKLOWkpZ33qBxTlZxIVrM6OVv3eegDGmCMLYFhHc/Lnwxcbi7XuThzcQE9UU3GJJv4FSxG42FvgwtHbB+Ycc7LlnBwf34n/k+LCCOTWpdEO4AHpDRO5CtQ5zutB8Qvwj9Xp3PjCIz83XSPuEMJTBzdLuejrloGZLcB3UU50k1R2alLk1dVPw8KszvcCwicurHZwmsgYhgZ64FeIq9f7+qV6E+nyhmP7gJ/oIF8Xrdk+wJXEWE5l4agG4/QUcah2dyMdCyZDqbR8kQZmYvFlaupJctL/z957AMe1bdeBo3EoWbbL1kilUbk8I0u2ZI8tzY/Sl5VVI+nrf8lfPzwSwGNA6G400Mg555xzzokEkRMRCBIgQDCBRM4AEQgQJHLO6c6+d3efvuhw+3YDDO89njqFAsEON5x79l47rDUdHdEa4N0e6FPi6RTnIImyN7czu2FscgXFDwCVPQv2pyulmfFEBmawv1RkdBV2FZhG137h6iDYXGpsb04m3LYAKdmsueRGE7YM7uq1nZ3tgf7u16+nyLZmLiWtMVitrV7q63F2NDdjiBy93W2wQAPAm7pmVxKGAEzF3irZRK/ck+zYtJlztiDtTw2yGkV4iOY1rcmJ8RHMmsJPnTkSwXrCNs4W+xaJ9O0YAG9ocsVJcnM5IY7Kyj7ufN5cXWomO2xAYnYWN2vzIsfaboEN3R5p2uCNx8Aow+vfdFaAje67l5cR7QEPIGkFxOr9S+TkoFmLEyMIPb2DxY2aRLuRUpoaERvDoj1FpjIMFh3up1yeo/XYbKRG/vhcpdLUNV66WIA0uv6ZXK721c8ZLvuv8jg7PZl1pRMMIz+gW934j6M38poyev4SLfT8GYZ9HvTY66MpbnBlvAvTtAQP5B2W/b9F03RyRWw6aLRGOtC27vM9pMXYs6E/oGYtL9gbtruz3d/bNTzUPzoy2PqgEX5/NT6KWSDSf0xXA6qv2dBhKNPWk5mblaROuFN5gBHylxF2IxM97OwVWaET7bex1BAn2Alm0tBrbUBK4rTUU/OiPis90l1kcpUUIpqJDJKSIod5CDXu7e1yW1/kdSRd3TC7XjzlH7PnGBsb6/nZKQqa15dIjg+Lwd7KBD2k8tth+xv31xcaUuJckZseACo5BXAWye/ZGQl4MICxuT9/YeEtusIcPBngJRDvFqkj0POIiw66CB8dycLpwJQIryfcMBdpirv0USYj4w4P9uLzelKlAxdBq4uJdCDg5SuDN9Lr0t/X9Slv5EMDvezIyIn2zZPEt8NaMopFBaFDiaPCc43BqYv0hr2Ze42+PqGRuISxvk6NjtKQrKGBKirC/rHzqCxTTtHBKDu/iY0u83J2lRiZMaoJ3aFBVGUV1kv3s3jeJSyROvy9/0X++kK9t0zHmclYqmhyI5pdWp3mfm9PoK0ItRlmoiMAZkympThJDM2ZFFB9bTnAgEBfZx8Pu25ViU0CwwJ8nI6OpIUSR0dHhH+VcJb6eTmAdXC0VWsg/L0dBwd68BPezs8RRTJ4ljWeBcFsTnamulUHwBZNyrNpdW+hXqKrTWdk6GpC/Ex0ZLO/51xMlBR7p2fs3muqykoWiWQmUqiP+atIP7sHJQn9zfmL3TVgT9cH61XKgmHT9fZwIyC3WykBXk5isNHmQlrQ2VxWDQhbSlF+xtjo0GUt2NPTk+qKYoLBnKxuPM1zRXr6nlseLwrdY7xMRTIMBs/vRZldz07k0rUEgPGGHycrpdKSqO5fpjZqLnG721hfAx9J24DjZcCw47Pxf6SzBVr42P20jBjKOpM+oKWUrxOM+FrDsNO9y19FZ/Agn68/3Hooj1hoJE48O6RG/khGafhtDRGRzUYWEvvXdEXs8SLPtX6RcxwdHijITfNys1Y2EsVF2eCfwc5IKh9OTi4zk0N6nUlYFMwS+Dc93Z3afhRp+CZlFQDGbCU3fF3Mi5L8uxtzHpYnveuqogFY/11AYhOPinvv5ebEefu6mJkJwKJckcjVkPXuaVPJAyiCuxwFbTlRdgKriTK4FxmHBwdEvgYu2l2ZWvSjtvvcEIL0J4DJgX8eHOyDxwx3Fg5yZnqSlF+CI4L6KkLDK1kpXjur9w42H9ytiBIweppwj9SlIMiN0Jiwaqyvwvb6XU3QkQ1iiVLq9gUqMJ8/fUSYV3TwYAhHRV72J6Q7CfcR/Vpw4vlkt0hVXlpSlFZYFDcEgNAKlDOwV6CUk52lkULK+lMbhHYVJn+KPLnDdHxMMvAoHsW+noS5QbcBCxIdd/BKdb6MW7IPiQz1eR8X8OzoaGP29cHzZ1RtHU0IpE4hgwFm4XZi2FTtzG5sJMRT+QWoCD8yMqhABSHXo7/5ReWdsOOd1rQEd8Jcj9Er5W0fQzmwWfGFrC0tVFbWoyBfkVA/1NZ0KymJboHLzEp2soKDxNtHOBiz0uNVmK3yW8odWRPjIyyem+uw+y0vLx4fH4EHvLm5jkyM7EJE1IVjb2KAM4mB4BNNIxpoESG8hA0UnvGR4X5cIbQsgalBhLvd4/AgKW0m4c9k5z8zMvczMvxsTQ1N5LaS0e28CqYWPiTEyyo9yj0u2InuHBtt2hpqJBT2G4P1r5+WvX1Z1VAUY29paHLzF2KBHtPObcAm/VLXRq7zIGZRIvpSLDCoiLPFPBjqg4W5CQXG+qTM5xK2rOMVOoYu9fe+oy2UOtsbpImve3+DWkq6xIsAtgBLVFwczNS1uOsyjhbozjfNlATaRDn3x6iB/8Lqi/vXdLbjPTj2n2HY+xkHr6iV3PfwuWcqGFcWIuQLZU9TwgSgFEFi04YaXrx5j1555MPH/4ZXLvsC49XEKEdRO5gT8NrRXYb5Zk7HvPDgQA9s+sp/J5EqEpa+yLmQzmk2GDM10WN6vQzgd383SZi3TYSfHRgMMAYSBqqJTeTkSLQxiPQf63y619pCrWixKZexVEFhy2t90IhtbEQ2FFyr+fk58hcFYkAdRm5WMrlHKK2LTWgYiQeX5UnHw+bGWrBDz562A8qCI8zJSAS/AQ4mLjoIPIAAH6eQAHdPV6tz0VkvB8DAvd0v8NMEN69EBNuipmpnR5aN5AZAVlsLw/k3s+oj5mtIEXnnVg73cxUS4MbzUoCvQ9ro5U35S7ozRxEeOfB4dGBBICR70eF+n84WuLu748z0ZcFJ8akcJqlUrVj7AWthSSfcAgWSd6ZrjoZhfl4On7hi2KO2+wrZaa0GPMukroy8ncgux8cEX+TY9nZ3XZlGHWvJzalJHXV1Dg8PMbKmmwY0oaPgGM86H5+cMHd5Z4d69YqWxCgpUdLMSD9NTQu2EQHCcZYY7iQlAgQ6XaMp3dNSYtTZHZHR1bbm5JPdh7np3gSGwY6q8jAA8OALeJWTPX1K1MzGIsM2EuNRihoOMtBGJFJSz0tNjFT+DKLRx35wwFAC4sWCQ3WxMNg22f1gCgdMKEYJ8wf3IAKbKQkR2t7fzmcdyMcoEurZWhrdjw7dz8KuP05V7szMoYiQolB/2GckCrKEMlMrMPzCwdKwINGvKNm/914u4K5bKQFNt2O9nMSudgJAX+z6QzYM46L01GnMzU4TkidTgX64u7C/2LOPwWDwS2Oqg435NSyXtbMy1tm3UYp/tFBzdmerxXQIXpcHb/dyGRE3NtbZCq6XkxsHXPQuiCboxuLJvUvp4julZm2p3l+Xe78AR7cffS3BytcWhp2dHui47s8ZwD6+lCyLMXLZrl1NdXona3KZryl9OnPN9dzcPbcWF6Lf30Xr7+0i3LiE+Rc8ddL3HBnqDa4VWHQwgW2t93TruX/8qAV2W0vxNdgO8nNS2N3wsFGSrjMEMFr1qCiPvOxkNiUGe7tHO2FqcpWWbzahCyHYNkBaCZmbegbOR08PbZtzc6lnzyh+Edbl5UXilhEwCc464UoGeDM1OcF2/p4/1XGL2WAkvEg8dWJ8BO5Rc1Otu5MELjJALLDiuqlFKUwzgZ6Xq3hmvGx3rflOQRDgVXPGTamuLOZ04E6Q+8HHw45jwTx70o6pQmVNOdXobm1VAYlpLHrkGABccZEzpClaE/qtra6g3IKTregSWyUvHjHCKD5cVT6c/oRSovV+g1bfgk1lcAEVLh1cCoyvhwV5fOL24i6L93WUR+2xwmCzLJKuJFKrdkElA3iCCE2RSuoanjgKie/gYbzcEgayDJaXFhWTqLB5rq5Sk5NUVxfV2EgzLqanbyclOklugrvvYWl8kJwMW+vZa9rlbWioZvdHNdVXE3IjgGEvHmetva33dhOTokR1rD+HBweZqbGwmEl9oOqxvU0fkkKmTgYaz9IzCtzs4SDposS6ClLiDjZL0UfY2yWhq9Lic0qSK8tLjXcrOdq0jo6OYN+LjvAryE1TEDojnLEw2/m1+BLNaK3CKGDNC/PSpNdZqO9tZTIeG0XLFaRzAjCWGDdVUFQR5HPd+AuJGsMBtlVgdAUgGXaRwe9gcwF9wZScL1exMb9hZSbNiGKgFhwAdX3C2g6SnTYVGHjZGXXfckeJsJEyr9xQSzOhVB8MdksddoCvxNja3GATnmGC/UKVOADAwPUltOGYsOJut9E4AGvNe55jm+v5VWrOUXOG4zMM+6THdtvZq6vUrI127VJv/WlpuZ5/y1PdXE5FOvj/aNahW8lmcebc0HBgWw+k3DIwF+Pen9OGFUQEPABIgE0Q/j43O3OrIAN8/Qum6Y+PDmdGX6REewoMv2DvBWAGiAbLq4lRouahc5/GuSu9sqTgr2sAGwy0iAr3ldOBdHXLrU5pKbW2xudiEtYHurGhV9oYs76+RoqXAOLOTE+SfmjYH3XDYATa+Xrao1QoWz1G3US8DT+d7UzDg70iQ33AS4uPCS4uyq6vLe9of0Biq+zSoKKcgKOtljeTlQ7WciVrjax6JFPX2616gz45PsbIMWBm/qLPy0sLzrLo5gVlqd69fUP0tXVQIaO1ExgKR8Del9hNfvFBnmg+oWXUggMvRFtiaKyGgguoEEKGz0FwS8L5H4ulA7+agwuHNBz6eNjpoDowONBDqCNIZnhosJdwyV6EhocNw9jCidoOhNmujuaX5ddqPfb3qdm5rUft9lZGQhQNQ1+/v1/mKEvzP1iFTmCYqfHVwmz/yjvhJBV2QVYemvsRdnJOdHHwqL04Lz0uOgjWAyFoTY5X5AoncgLYRXZZl2qgr5ucKVv7RG3EeHeXBBzhMvJfWnnZ0s3Z2ORqsI1oOzkJqS9Z5CtYjpip9lqlZ6wlJvS13u/r7bqjVC6uEP0890/G2mLGDK5hZXbYs9p0W8kNgGRgJeHxgafSzsoYfh8e6r8gkRUgXnyIAGvZmF27l+Y4VOLZc4uWCCuNsYFvJA1pTQ3V1NdxzM1Ok9pa9mTLmWj5OA/JC7uQvXAhnGHO0HmTP6P9265/fq6bbvj7mhWhPsOwT30AIhr+nqyo72+pA1784Ge7PSyS0H9BZ5b5fFH/f5S+ZUak+fULkfKmsmljDS8+mKJ2nlI7T97fdSLFY9gQfBFTvb291d/XBa6JvI7l7Gx1YfZZc/HDytSq/Eh3BxHZ9Yj3A1YEtt2erufsujgbieEFE2Iwhgf7nOxMAQO0td4jBlXlDPBxKClMr60sPof9xsfPWZ2cHGpoSKMMV1/vS5Ja9HaXK0pNjI8QDmJ/Lwf4J1IDgwcPqExnhOloK8Q8Eruxm7SGhwS4J8aGAFBpf9j8oLkekMbS4rvpqQn4SVqh2C4ybNkohC0yuopFI2BBTW5+8ehB6t568+xEha3FDfw7Hz2ovp4XRF9OZYia1GdqFceFMToySLKOiXGhOnv5cHfIqnvxXGuzRDjr4WpcWjXLZQxSlwsXFg6SI6JBVJvgFLTNK2K6Vbk3DJYZeqiYLyX8mR8Rialz0cjdz85I0OGTwVGWPuluNqSdkqTI4MM5JC60gmEaBXw5BhIgOdoIMVhzUbt6cqLb3Tw6PHS0F4uF+jHeTictLdTt23RloPRMT1sfND5sacJ/smlmFaoVYEfd1Zl8aHubqqrSkOR59IjAGzhNIswNO5iysSOlbkS+8uIDnkG5cXS31VgjANs+CSQ1N9by/BbSBSAQXA2xMV2Ij1WEWxkZKwlxDwO8e8OCqPQ0DiQGN/Hs9DTpvDAmPP54VOZCfcJ3RZM8GV/FhJizrUl1bkR9YfSDkoSjyZbitCBMmoEJYzPma6F3p2Z0PpN2/4oE+iGugmGmJQx+NqU6WDJ0HTKOpYKvpRe8sPCWrFK2+g7dy1qvEwP+zjN5SRetq/SPPF1rrjHvcQ6A0VQcSZq1oD7DsK/A2B+h2S3JrR36fT7tVSdbz6meX2EJ7f0utcujl3F/jC5glco7VPCAPpVyEo43zh/3OpH2GPDa+bMUgBECZ4uIXw0N9hbmpWNYztRED9tsluanOh+UPChPxglIrLUiJdjb2kygpzFpA/u4tuBEYYAzBBCFkJVtbq4DKnj6+GFqYmRV+e2mhupgf9e0pKi6mrKR4QHV3tK7dyoMT00NXW/DObDQTqFxH1FHTIQ/CZ8TIAHXjb+MGJsNEn7BJEznsw6SICLkxQCJ+TtM8EpAawjIwV4W5wfdq4tzsqVxY31VzPpCA8Cw3bXmrBQvkxtfgG8En6+xhg38SACBeFQLMi5m9jfmZiUhPYYOCi2EMAaOROfOGTiG6HA/nRkaCBS5YFLu0gd4tET8dGtrk6O9Z29vl/j66nTG1A2s9nGwNtk4798T8YYLqhi977Eoo+jUTTEMBpwgcdPJszY1OUH4Yy4SS6J52GWkixcp73zY0sgu99JhwCIBeHC3piwixJvN6afVQH0wOthXyoh9Hx+rq/QmigvoO7LdR+UlCldpd3dHQyEijNev5WrUKmdeHjU1pQyzcZeuq1XB1YRakdaSmyq3r52dbc1HhbFWljrz0dER9soSBWFu8EwWG//7C+sBbwRgsFxX24PUVCUMlvk8JMBafN3Y5KpIqD8WGcaVE8vJLUuINBFcZZtvV0fzR233bS2MPRxEXk5isBfgFYBZiQl0ANzVey938nHJ9kjT1nAj/JzsuGNncVMhPssunwOTXV9bzq1hrW4gkSksIQBddUn2gyWeQyVeVQl21mbXEIOBIdahKfSrkYk4OSEEmHCyNubniHBqqkq0jQtRbwPlGmg9/55mMrwILdzJBl2qNvKHrAzYd6i1O9TJOvXNGF9ZGAZQausBL/07ijqnXE4zywfxetd69Tm43/trvIgKYYESsfNtHnIxbH30yS8+liYd2DDCs8eTSuv4+CgrPR57x8Frr60qKchNJc+2WKD3uP3B1sbq0Iv7BICR2VGbkRXn42xvRnxfjqmz04BYBTCYr6c9diWpzAZohiibmzThsrLhyc6mBga44QdRlAJXjF2EfXhwgDVgChMwIR+Cr5LbuaQSBjxsTF801leRP8JZk9/VNSeopBbc291FdmyxiV6Qj+WT1jQ/TwkYTmvz68M9RWvvwA5GdT7K3FoGXBltZUbLrcI5auwSBDCMYVFlZa3lpQX8L90UjcDBJRnOixQFsRNHOrwdkSQtnlZW9Olsk+CtyilMOCHu7OtphCLwc252RqtvQYJT5Q60TFlR2WXycb2HwWar141YnzCw5WfLmZTXVldI3ltnHSccSXFhMu0B3dvMXjx/zKGpRUtp9XW/6HwMhwpb7tv5udGRQbihfT0veuHPzzoa7lYq0Ofq1s4KH45vr+LsKaVYVZ0KE4wOmzkQjhPsCOycbo6S5ibORNDGBt3iy4HBcnKoRdWPSWlxXmJC+Pq6itDb/PxcXXWpyoQVbIxwYLGRARp5XHOzkr3dbdhwnWwp0oeoh6vlhhSiw5JbWdHc7fOk4yEaCFOhfqKzlVRd4Dz9xnxsNL5Awki2TMbHcMGwjIyxpDjAXcjyT0JjznamKYmRczOvclLCTG5+kRrh1lGVujfWfDhxH37ujNIKY6gfM/qwyFJMpz3FrJ4x5ZmaFKltU/ro8ABubgBCXK1vdt9yHyv3LomxsRDJ82CXzgii0ee6QPGedoNwqokFBgm+4tIYG3LWtLaBtgzDk3qshNV/10JLSeU4enuusrH7X1Gvfqq5qeczDPv4Y/uxVA5v8hc0k4xmxLBITRnQFC54pwFc8WwS23lKY3154vV/8ciQntHSENIc2u9ohlUHk7SGGDvttvcRFGABnwBWwSfTz8uBzzb3pKNVXT+uo62o5V7N/PTI47u5yhistTK1piDa3soItoDykoKHLY3ZGQmFeWnpKdHEa5FaFEuj6opinfWg4I2o5tnWeo/gIl0KaQ4PuQKobW10lYuaQWohlP17cHwJSEOJW54wgKQ4mH4wO4Sp4EoS3HXnVk6grIpGgQRZFmo9BKsD3gxgBoVOGLj13m42MlHR6wDGsPhQaHglJ80nLdHD8MufmZrotTenUAft5bdDUTRMQVZbJSLFui84HoXuL4QKgMR0dlWJmXF3kuisk0Y+JD9HF00SwvGQk5H46eyUgASISBG3iPDGhpROA/YBbVkNSUuPgl+OIVi4szq0233IUS8DUWFBnjo0hsGWkhQfxlYFJJsqJhhp4pmJ0YscIXj5OuvakUEa2JQpH2CL8GMJLSoICitPeJZrqkr4t3GyBxGH4NPcBd9CtjKSFVHgKSFUKEhKyXV9lpe5MFhpKbWwwHGXZW7oJMV7kRC5ee5qUtiNlYUH+3pfshn8X3Y+4UZx/KOosEmS0JVIqF/g7rCSmHCQkiznRUxPP0lLT3KyFDIkJWamBhG+LlsZ6Rp4O/Lzh1uag0O9nc/XEzrZipLiw10dJWYCvdhAh+LUwIaimMfVaU9r0gGS9TXnwc93L6umn5R4OIrEpgap8cF+3o4ioZ5YRGuXmQiuwk82MOt++Yz/etvd3SFdc2KhgZvNzb7bnk/zXH0djE0Z02Zldl3HwryvwmDTgAmM9RtSHFIDzYk2mqeLlXY73nqF3Fl9bcY3EaL2ecygE1/kA0f+J+1yf/PGVxOGvQtiFRn+AbXVyutd04byd73lTdq73XYuJ7bFI2RyukON/7VspVrw2Ce65E1ldJL3l0/exnzgKwqIhVi7sCAPjY1hpNyI1t8w0TNniAfNhfrwu5uT+b3qgmf3ilorU5QxGBYllueEmwn1sdafzWM+PTVRWXYrLioQrCkY10WlAjatBinCuYR2nfJyLvOTm0v19akz3qQvFmyqQt/Rwrt5rNieGB8B7x9fVpDLpbzBxnUAX1H1q6K0kDQG1NdVEB3PrPR4dKnBI7zfVBcbGVCUn5GWHM3WFVUWJCU3lz3hzqbEudlbGQLMBkgW7Gd1tNXy4nEWwjA+fjYh8WfXfuzubKOr6uporrM07fybWdKGB9dHtw8h3mFOpi44ilCtMHwMp5/OZgn+n7RjhDNRcHCw7+1ug1Qx2nbDI9O9AtjY2trEVDnc2d3L0xB/H4MUAgAQXdB+zwGnlii2EzIeHEg8o1vDIXsQuQv6Yuoqugp3Bz/kXmONchgu6DzaUQvAXKzAZ71I53CdLGAxNcmrBRHAHoCxWwWZsN1VVxSr3MxnpifhsbWzMtagDQAeZ16e6j28tZXiE30YHaUzQnfvnvELVRASQjCvHDiB9EEpyHWwSW45wkOwyRMUHRHirfH5PTw8JBFAem8X0dmn7pgIKjtHCrQyMut83IyZCkMzkUF6YuTmi85zomGqO8TSTzs6ENX39L7083WSqLIjAsMrQpqdWE/MMBWD22B4/Wf5ib7bE82z3Q1DT9uP3s6/q6uJtjd3tTCq8nap9nZNcLSgfQxZns3fy4F/38TkqzFWkY7+7SjrtmxnG1ktojotuE9lrFfR7Sq6cg+y20oBeqUHmncVuXvZGZFzB+dEu7AOITBcybvQeR3OUlPX5U5v/3/i1cXzGYZ9QmP70bkiw97foA55VNEcr1Kjfy5j3fjnWtz1tTvnEmJ84qP743L6zh0ekZuFcBYjyP928u5DB9TBtpHuCO6oG8UQr2OUDptug72tHSwNrcTXnKyNAzws796Ob6tOa1EFwMhsqUgJ97MjVHvaVyfzApbIVXhxrkV6NDRopu5tb6dUtZaxw1FujhIF+wEOBPIHgjMHSKm2qoQIgCpvqXnZySRvBmgZO9lIUx/NmHwnH/6CHhUgE1LiApae0B8rTJXS2A+a78KhOtmK/L0d4V7DnQr0sXz5ONvK7BrD9U+3XFcUh7U2JWGujLtgBgdANWV1YCJymhgbonOYHyAEYddkV4Vpi+WQ1i8syFOHIyHaqQ7WAgXu6Y87CE1zye08PgZbB1o/zAXBOmFXM66vrWJyW2M95McdG+trbNqeJx2t2n7C3Oy0OppN4karrAPkP+rrKpR7z7QdS4sLGK0oY3YJxSjl1ERqYiTiSbAFvp52wf6u8HUkIQOP2NBg78VZFnFBOutQDcUB7apLS4vzPJwtKstvcUX34dKpJEh89IjiFzo5rK6aiAwfjAgdryjtaH+QFBd2uzCrMC99Sw0qIInitORoju0L+ZCUSyGqym/zUdEkdxZLu/msELD4JE9OVx6K9CP9XEdKbp/k5lI5uZNREWYifSwvDAvz2VxdpiorefHXZ2eT2pCjw8Pa6lJ/fxeJpZGJ4Kop4y3AT/gdpkCoLxTpm5oa2FgZmplfa6lMPxx/eTbQBxCXTsox1Y/7yUmo9w2z1seN3XimTopNeYCBI6mwACeT7iL3MDchSQeBmdu4DMaaSx9nB6/pai/0CV/9nOe7Op89gq2GLAC4y2jUzE2vx/lJhko8O3JcbMyvkzbLFm20SZhtvZwa/+FFJXnhQ/r+g9zjnXOiNZy+weMr2xu2epsa+0tWTuxbvMgMj+apiR/JwNv/QW3zM41nRzTFIoF8x6s83nJMJ9xIU9mWpuqLw9f08Uhh2L+kaUU+7Bhl6d48fayY8QO3sqO9pbG+CjDD1uYGBtLMhQb2Voa30oLaqtLqimKq86OaShJo+o3K1AecGIypS6QTZV5OchEYHbwf7gHuAoYYL0LxLB+PH/MyQiUltG6pYjR3ny2YqHP9QxNLVycmwh/VisHtQxMOLi+mO8ARwddUlt1iuR9nWCFmZ2lEXEY07eqq+OCmr6+vTU2Ogx8muHmlviq2pCgYfiFZUFNjPVuLm5ZivqS38Glo+MHJI90Lm5vrmITRjRiDQAiimcMRddYQ29newuyNbg7iwrt54kvB0/Tp7JSAvvgEPmnOfeb0wfnWKptHlha46ewQw7u3bzCyw1M8+mMNhQYkB2uT8pLC+011XS+eYqpZ4yAVZQDnFIIssJcqM/ToMEg/lZebtc5JYwDDGIvJzUpW95rXM1NDA70LC28PDw/hzsIVIAEOWB46fzV7ZDAyzQCZLuXTwCQRynWpeHFiBNeNq69XhA3DvCpm9/f2+l4+i3G1BSAhEuqZnld2jgjxHh5S0cPc1/sS/stacvNuTRnHhyOJpXI2NTEulHwFR7Xh/JtZEkXlz69DGg7lOTHJzZQA97aI4GQnS9RMk4ivT70aO93aorkQ+VhAmM3N1Js31Ozc6ezs3sz00mD/6IN77VmpofbmN42vRDhKHiTFVIf6P83N6i3MGsiOmy/PnivLPqwto/ILqZRUhfQaaTzbTUn2tRLgUWGTJJ9t6ujoCEU7JIw0WVOqQ2qAucBYXpJ3kc7z9zWOl87e+p/1/yd5bwtn/9Xc7DSAUlhCxM0gvbjYXmgmNPB2EvXd8R0r984LsyQQVLcu6AsNgFuL8XJeuoH/TC3GUt/48ZVmSjyTQx1McPHRdzvdl3cEDvwuX4IXwG8MTDrr/bWzY97AfSGSRe+hCbwtZ1HD3z4b+j612fzhLyU8kKQlQKGNAXxc0vkAHiqWWUtEBmKB3u304I7aDIRVAMBaKlI0AjAy26pSK3MjrOnAjAHSM+rWHK9uvBofxTiugqqVjvIj/f18jRCmxc5HZAG7Ellq8PV1CL+Be0c+AW4HWiD4HLwd4F2NMa4/+I4kV6ZAz/Wy88m9xhpAOwSGgTemkUdblsK6XlcZ6+ogMD/vfCCrFXy7AlO5OrBENNPezs+xAUxdTRmfhAl8y8T4SEf7g+rKYjhTmARDYl0cHgwRbtJuN6FJ5z0vkrtAedyLKhpd9gA/Ax8E8AU5qnxXlpfQRwdHXysusr3dXRRIADDGfriI8FGAj9P70Au+rFHHEm5WmAAVosJ8Acc+bGmCxfZ6ZhLwJAEPGxvrcKHW19dI2Q8AXYUP7+56Tto1L3KQ4OITqhWd5VZ3d7Yx0MCRV1EehEWJ5uI/uATyaOzX9fd2vPiqgNvBVg4kRgoZelWPlhb5Rl1URLPg8hiTr8Z8Pe3NRAbIVyExpQvkzM+rYHE4tbOvp9mRHVhL9bXl7NgE3BrYsVvuNygESvBaSRlNym+r+/yKsiKWevgjPltxye1ckg0zE+iLjK/SkgBMtkogy1zRkp5F2Uw44akW5o8NomS5LCo7ZzU+rj3QZz85mS59zMqSapHRWa90eioAMOWZmTkRFU4LfIkMpA8CPyYSVleYYc9tDz9HY7HAALP3rdrmgj7A2B+lW6TYRPCc/AIz068w33Vu43Kx3NvbBZOK9QiAu+J9xCNlXv13PMnpw661vrb6QU8NABjBljSzw5VveBLs6wHDmLFWQvX8O+l95ZndOnxNdf8bWe+WhO8XvQs+G/wfZ2tals+9+qmMnjGUF7Dkzft5dnp8dqa1GQMfYmx0CB4/dt0UbMo+7rZEAlgBqICfwW4UJnqaYb62LXR5YTJ/6KUwH9WkJ4a7ioyvEJqKtbWVy1oXLzofI4OFQisFsXNg+bSgc5ia0s4C3bmjwLh1l+Xt1WpfhMmWhUFgCU4MCWdiLeLqyrKTrYiDmQ22ZvAsSacBn9qV3u4XCM7dHE3P6fa42WDpo4uDGX+1nPq6Ch8Pu8K8NJ4kELBcR0cG79+729RQnZESQ3TtyCQKOYAwdahXURhEcZsoF+kQ5n9PRba6B6tY3YnZGQnqwhAAcbGuKcjPRatsGPiXSK2pQELQ39t1cXK/DzBSEiMkLD1ZQg1H/smejjZCT1eryFBvQOxw1t7uNq4sNgJlbd/+PulFSEmIuMhBdr98RpJ1Oqt+7e/vuTOPP2GO5fMAYrIaq1UvRfYN1cDcnSQX7xgkJc241RfmpYNzqYEN5dEjzK5Q9+9TW3yT3jmZieD9Az6BKRIyvdCABMyuC4V6AoGUPYKnTw9YC+1palIk+3oqxz7gkSQ9hzA55AGQjZ0/4x87A2xqouftJI4LdrKzuCkW6LH54u1sBGBTzo6OqPx8XWCYMioD6MVN8qEJid32cBTKEmJsOhx1Wx9pgQPskRNq8TDLGS4+luSlp0R/Uk28ND3gUiLV++ssDPZjjUDlrpooEsBsNGcShg2yIcVhrNy7Oc2RbGsfVKX6ZJOac5CT5HX9C+qN22f09TWCYbRD/ZKa96SWUrWgbdmolxNvzJhSPPHMmfYSnIdz0ipYWHmLcR/xIsGO09P1HBP0YMud7Ezjo4PK7uSDB0+8T5UuLBjvpPhwduRPbKIX7GVFagt1ni0VKfdKEj0cTcUyATGOgJ92T/3JCSrtgLekIvC/slRclO3pYuXhYgmbES/fYmlJa5uRk0MHEWWx85XVZYJmAdUo5Og0w3lZez1WjYLNzkqPl7pHfi6Hh4dwf1Nl0pkqNSjB6SHqQ6XFeTylbGZfT+Nhs80zrUXW23V0dMRIxk1p5Req63wj4/GjFji1yFCf6HA/BfY25ZmZGqvsWCi0ufMfRP1J2Z/mM152PiFZkU/KwBOePVf1dWU7O9uYWNCWoqOv9yUGCG4VZKj0D9jFsR9/HB7Sz/LwMPXkyevqyucZyc4SQ9jczEQGnpbGxR6O8Euio4Wd2Q0r8TX0s7GVxYRxwYUCPZgC2YQ/mrE2RnisFL4Nng58fMKDvS5y1CvLSxjY5k5patwVMXHn427L8/Hf2twg6SbyrF0YhvkgG8rFS1XBNiXEhLg4mD1orkcYoyG9D1v9yAg1NKRR8lFx+301dqcouyLQuyXAeygiZDYmaj42ei0xvjXA+46HI6Zo4Ly4d/XtrU12YTlMbmUINlVSsL8rx6lNT02QWgnYCTW4SzJWJJr/ltFKfl6XuTXc+OpRcU9Tbka0B+kUeIaJtd7eS8BglzIzMvaTk1wtjPChg2XMTagDTwpm+MVCA28H4+FSr9xQeUnepfdBXGxfmqPG/0YOwMb/jhaV1QQyF97NkzAQnckUGJgpqa7BX3JCLeHcXxS6e9oZiZkX2EgML4G3jO/mlU8N/jd5x83431M7zz9Dr68dDNNt7A1Sfb8pXRzzHu/xi7bbaQyGX7Re+VHOFYBHSkKERhYsdyeJSvPc/6LNy1kMW7a5kK5ecLET3GPawC6CwaSlidVplXmR1mbXSaaFT5eRZpS9sY77b2pipLIRIqVxSFbLp7YBvFS6i0AHy1FcTLt9zK5ZV1fO5uLXah/saH9A3gvmh9S/udiLMfFFmBhCAtxUttED8pS+3cOOv+4K2H4nVtkP0lgzXSJSrurEuFA4l0vshgKHlWOJ2loYwrnnZ6cE+ToD7BwdHiAuDjsvoaxOxmcQshOA6Do0rgAgIVdpfn7u09nqCFM5zZaxpJqSe3VlGT05T1crnj1ROFrvN6jk3yOY/4LsFJc5pqaowkJw5hbj45IZMm7AUYR+DXCXg/kN8GYCrGnNPW9LkxQnq44gX3C1K71cwNsucndIdbaCmeliU+Bmn+9mV+frHusgIRVcyjGsd2/fIEaFq3qRVqiN9TXiZ/NXeFd+lrHqgT/SJsjtIhykCseAsRWAYVu8ye44Bl0XyrO2CmBMYyO30iNXHPPoiCq8RRfUZeccpaSsJ8YfpjD1dTl5PZmpYcGeEmZn5qjbZGfsNT4asMGSPKRyab2Kvauvu+R2nkZ6LYpVhWsm0A/0sGgtTdwYbFjpq9sYrF8frA/ysEQYFh0ZQAcoj4/p0s1PBIYxSCzFxUYoi9tyywAQYhuAXlkhkrFy70BnEySp9+enyvPBguTU2F/LMdisNc+VT0SGML8HZwdAi61yDqDLx8Gk747naJl3fpilUAZBwWp/iNPaeUIXVcoZHH6fl5TuZxj2zRobd6mXvyTtK9tseo9ftBhLdf0zRu3u9+jmtA+MN3d3CYEB98zNSpJvDLJw/sby/KOa9NrCmBBvGwcro0BPy7u3Yh9W8cVgrZoaxuDD40NdTE2ukrAfz0gtx2Ca0Q2VWzLArhBJWTJV5o5UmHAO6TDuCZ7fzg5+OyGwxiwBzyacsdEh4oQpSOhgpcr42DDR3lXZGdXT3YlReYCdBLpoHHOz07u7O+B+ebhYwncV5qWh9hcAOUxqvZ2fU0eBrRm09LygFVFjQyrKitiIlDQiYuw/JMD9dmFWQW7aw5YmOHJAEehBKjeWsK9tdISftuJXFMMrRTKWKgkkucfm5jrhM6jWJE37IQfhClPmAGDFGbaxYg2WkDqopnKQdkRSIIqDqJOz9Ql0GJdSCEf7tUuLr1OS4hwkkfbmALdILRl7IiQzY0rOJMzPXFe7V9ERdCVVdi7tc9PdLBnMlDa9RNmbIW1ARIi3MnxdWHiL1KZ2lkZsZV5tB7yXaCrqHPKgYRijVOHqaM6zHhuWNOkg0kqsiQtdMLKEF2He13G8eEHvxl26diDPzcF9n4uNznG1dbMwshZfc7cwirE37wjyOyzIjwpwg2UDuzRHio9ddaKRp47NrxsS4HZZ3ZWwY2MJMRhcdwfR25dV2yNNy721MDeHGp/WpGNlCljPqWmGa+rVq08IgzF1ieWB3oQykYNrHhwYMC7SHmaRwb00x44cFytZRaKydN7HHEcLcoKDWUv+7yN9m2KBQUaQZKLCpzLeFnEmm6Qe8GdblrON+TVzGUKDpXhZ+6oajDhJTX4hdXppwvBfoQvWTj5doqbPMOyjjnlvOXHizpP3+EXTxjLu0Z/QwQ+tHJHT44t8c2pipMLuDwCguqKYtLKgQ19XU4b4B5wwQC9Bfi7g+C6+nUEJ5oeVqTDvlSS28uNCRJlmQGsNxXHc1B3wsrqiGBvJDZIQ01nGlwwiFXK3tlwhuOvlZq3ciK8ZDoEVLC7WxWyUlNCaoayoNsnFASICVKPRSYXbwSYr82fV6SHpImAwTP052YpUVqRsbKwTFPf0cRvPawh3HzAJdkmB07a0+G5tbQW7s1ISInATJ+mjBi2J4B4/amHfAnsrEwIOh4f6/b0d05Kj4VzAi+WPppLjw9mfOaa9t7q/v096CXKzknQoLITDJuptn84mtyArzuGg7FtfX0NHH266VpeOZMPYIY+x0SHSOK5t/e3lh5pPT3MyEgOdLO3Nb5jIenukCQGRgVikj44amexVJBTQnHheliaF7g4zMZE0XxxNLZBBA7P09NPU1ABrIWbDFKSr5TCGae+5CLUGPoCEh1NnWHt6eoJZdNgNeGoqkGzeJfJ/Ym8YwLCLc9+DwQIrBtsRL3onpFx/pGtO72VXva87OPGwhGDNmNMrxwDAPCwhP2sBhm8COcUe0pKi2EsL3vJ6ZkqFB3uwf6sgg827wNZ0vhgG3scFIDbRc7Y1GWopXB+oRwzGwLCG+GAnIdOqXVkh6w6oqfnwQIsJcCB7h/L/Zg1UlVnIQhJgu9XVJcLjhrUb5kIDJ6sbfcUet6OssSIR/v72U6pWoM4OqLcBNB03H1la1kCBUFiT1mbXHuW4ANwqjbEhMIzRlTFozXQaL/cujLAiqTA06+8Lhi3G0SfS86tyYPnqZ9Tep0dH+RmGfUoPwDH1WixHYrtd7+uLjt7J9cIB+2kXD9Y9EgZeLEYfCd9xPQuZDA/2ZaXHg/FgpyOK8jPI41qcEdpWlcbu5uJfbVieE+7vbmFrcbMoNYg7e9ZamcLuECtVJWuj1SD0AODuK/xXQW6qclqJV4KIW8FZ5WxspPb2FD7m/r277K/2cbfldkdqq0oIFfvKyhLYD293GxuJIYCutdUVuhjP0RxfoI6an9BgKvAEwEasDuSQwhV22czCu3k0bOkpUg2cFpkLzp+lgzqfFQQ7in4eu99at3IREvuUInBOhmh1o1GmfA2LX4fCwr6eF+iQsRPLn8IgLqC6+PHe3i7pxHv2RIvSEdIDxv5kMPOkPvPjejzwjGRnJCDiQoglYX4XMRWJrhJDT0tj+MVafM3O7Iat2XX4idJGAMDEQn0kYwC3W8AE4KPszR8G+ryLizlISaZJtJOTHM1vYqdKR/uD8bFhhTWzvrZqbyWNgAz29+hups7OAmXyyhcp246J8EdMuLTIK+E5PTWBeMDpkmS+YMPx93bEauqLU3TA40ZCWnD9uYppYY/NzaXBc4Ou5HitD2MdJDeMv2BoObBXEFsE6V+kxcx06emeug8gepskI6Ey0PN6ZlKZGCYzLe5R2/0L3gIwc8hyjP1gJA8Gc2f0XndjjoWI/l9ahx0joXDRdCvF13WepKY+C/Zv8vModLcfiwxTweeRmTX+4J69zORx9MKRbmGAJaGugpEy72gvEcIwOMGpyfGvuusKiwepfc2EBs5WNzsL3UZKvWoS7EQMDGNaxfQTfcV9tz1Gy7wT/cxIU5xuBFQ8DmiXBpNsRd8pfZ2Fpz/DsG8aEjuiRv9Cum7G/pI63eP3Lu2h0eE0LQFBf9EvUZsfiCmV2FHYejJTY1VaPnZxyNHRIeFVE5voxQY7sWEY30LEytSq/EgnG2OR0VWYgV5W3B8C/5sY7kpgmKeL1QXbBpAsAU58oK9bOUhG4soqqzHVjupq7YzK06eUqoAT3BGFb+eo9jk9PcH8DJwLoCnAUQE+Ts+fPlpafLe+vgY+TU5mIhGuUemFgGuI2Mn9fOAQbnRBblpIgFtb6z32Gw8PDx801xN1MjajxrysBJHkPRCxA47i7jVXGMtLUrFRB2sBGEuEkRfH3iR8QCTRdEhnkTyqVplDMghtIPy8SBHapQ+iHhbs76ouDkpSeXAl+X9ydWWxTCNBTodTXVFM/OOPBcPgGWluqmUHoQBNgfcMyMpFYhhuJ77j6bSZlLiVlNAe6DMfG72aELcUH7uaEN8XFlzl7ZriZOVrJQB4BoANIBkmyvB3K/E1DwvjRCfLaHtzSxnzGEYT4DFkX154spDNH17z5EnbRU4nVsZdrjMRKIy4qECskOTVEMsqjYM3XspNoakXmTuibe0rd4AJ5+zrabXwb/7N29jonaREqkLXNHV1zZMgP2eJYaaLTbO/Z5mXc4WXc56bXbqLdZCNyIzJqcK2xkH2+2buNTlU2MYnxkdU4m0iBK88o8P9NDJwqBsbG+vYSmRi+EVhkv8OC4NtDDb0Nee5O4jQBIcFe0nX8Acm50jPWI6PgyfF2OSqkckVO7MbawnxinplmVlj95scnMxl24vaKA8JDwFKqU6wAygS4CRtDEtPiX6/JXkfxm89O8OIBmbD2rJdhku9WjOdbc2vA+ISGOulBUoG7ngOlXg9z3d1tb6J7B2+nnaXIjuh4KfQjOUD/0UOwAb/O7V6i/o8PsMwLcbRW2rgt2VI7K80My6+C6aG/l/qXZjWX7TzVPotPb9K7X2IWp3CvHQiJcHn8QOc5iKjOoBNOTrIUQcY1l6dnhLpjixMZkJ9N3vR/bIkdZm0+2U0bGsojrezuInhanDd+OhQcQyUMIZTVtkWj76IQlGc5j7vBw/4mpO8PGp6mmP3JCSH3DUngCKILfF2tyGUZQCT8AWkDA/QlEotMnC2sNIMnB4F+83GG+HBXqi0s721GSdjVkiICWFjsKnJ8cy0OPwvQG74R0xAgV+lkf/wHAxbXsTQAJxRZKgPRogvXnhzt7acfVUBfC5ozyl3cLCPYsR0dlH7fBrcXEw40F1YfV2fzg4Hl1edtpXCU6MtlzGpbQYYRpwbsu3Aun0PVl/zAJeXTS3DICh9b0uTSm+XiajwneQkqahReoaUR5uuM5RNLIvKyDxOTXkXF9MTFpTqbGXHrFiBQA+QGA3GhHrgLBIxWZxOtqK+3pcKHh72b8Abu6rLqdlZamZG+/n69PXM7fgIrH7UoQ+TLE5s2OPfqEb0ypRJIHUbOzvbKHWoriRPq22Ezefu5WbNIRdWX34LELWbhdFyZjp1qH3v8fY2LXWVnk7TcmBhKr1ImMK57Jw6HzdjWbcSB0kGIDSEoB7OFirpLuEFSGXMrpVQVovRLZuByopg0x2tjSY77pByxM2hhvH222B8TY2voqUYQ3x4kXZoXcsRnwf7Ix89dmYCcqYfzHMUHZkrXS+jIv0lsubhhXfzKs+3qvw2QhQr8Zft2S7dRR5OljfQwdCh7/cTHHDi6BLASdlLrj/Nc+0v9gTc1ZLp5OdonOxn1lfsAf/su+0R5CIglYp89Ay0zC68Pscv0vebNCQ7O6Q+j88wTBeAROpZ5xy5XnmyQfX8e6aI8depU+2bjOHDpbwx36LlFN7zILK2YPx4EqA1yOqyzAR6nk7i1ooUHWBYTJCTiNnWYT+1ldy4eyuWo6MMEBpMb2czsWyz0KrITXnUMIV8NBmXKsNMMkjnSas09Qw8f87LlhQVgTnVsHEdHmIcixTmqWzVgH1WJhYkYFO3Y4qP0BmDL7K2qjoEW1l2S52S1cryEvv0I0K8AfoSyvtiRrXz6OgQ/LDG+iqFrobWB43S/AnzdwBs2rqD7L5EhQ4iAJ/v3r7hEmBVM549aUf3Dq4t+i4lt3N1WDxwdnhI8dFBOrz9VkGGzupw72/MzU4jVwS9WtY0rBatOv1qKu+QPBtJP+ZlJ+Mf/T4SI9mdWzlsDGZp+mWGs802+NAoGstTvIiGagwqS89YS4ifiYms93WPtDfztxY6S256WBqzJYNh+akMhWQwnEAmgqutAd5UTh42lZHuMtqnR8KPjEypZ49yt9KZKYWIOblF7g4mTLKiuvKOzjAMq5g4cggKg+ieJcWFXcp92d/fx+5ceEK1SqErD0CGchlDdxuOMktY8Pa2QqxKfZsQz18uTD6IbqSqMrn2QB8jkytEJY+jPWxzcx1FwFX+LxGWkO4/McGHBwfTUxMK3d0+HlonNAAcwgWH0zc10WuvSN4abkQMBr+8flbm5yZBDEarfRBZgrGxD4O+TlNTYSIMu0Uv8quo3edhaSQNl5yHYQvPnwYFeRAYpjJiC7cAAa256EtAXy8L3R9mOlmKaVRGN0aufx1Ug4k+CtxWO8n15/luAMN6b9PQC34ZBABW7DFc6lUSY0PKEZ1sRRdvyGRtKIfURh018J/lGOy1mDqYel8nfDhHfU3HZxjGRmLP5EtqJUfty44WqJ5/K32ZDonXs2NaPlxK1/Hz9x02yM1KIp1FPNmWjo6OkFDLXKjvYidoLk1s0RKJPaxKK0wNNBPqAwYTm+jBLt9SoUHoGZBbapRcsSQsyEMlbcYWuOfra2CZlpcXV1eWV1aWlE8KfEGMQNM5LlUbbn5OijIM83az0dAvMTKi2ai0toKl1XiFAWYoRD1VKlE2N9YSISysJwETgiVJz58+QmpEmLcLs9R5PMjh7u5soWz44Sp1tD+ASxEk6zkhWVBr8xvlJQWZqbG4DJRnd5dU9wNpJwtyU/mtq8P2h809zHuXlxYIvzycCCA9ch/h0+AA4Ku1pTRobpJerqb6amykAfiqg8UdHxsmvDUcZU4aURxTFXnyiextYH3JBVdZDUWxkqtaZSbvN9WRxhjk3wN3n52EvwhRu25jfW2V6JgLhXqpzlZrSYkn4OopRNa1rJiSQbL049TUraTExfhYezNpfD01KVLdwRTkpqFbGW1v3hbku5GYsJOUOBcTtZ2UeJSSshQfC79PR0cMhYd0hvh3hwY2+nmUejqVezlXeruUML+kO1vnutkF2gjFDKHInfQEnWFYRIi3Vr1hHe1SKh2e2sQ8YNgeVmnaWRqpCx7xCyvMEOpI2LjgmeV4McbdBAK9WAfJGbj1795xb86wgcDOds6yzM1xYPWdlORER0uSGk1Pid7Rsu0N9meFyFRooPs2qzJ/8tVY14unsC3rQDJ5fHyEWTjAYJXZYQSDbQzWD9wv8HISi4yvkIbYsbEhfA9NLvUhYFg6PAg7SUkYlYAbBJdRwhTx9oYFqWDpyMhc7e6KjgokNKdTkxPKp7yxsY7LQyw08HU0HinzLoqSohGwpByOEDg/cNnBPL2emXrY0gjWFszKw5amgb7uJx2tsLeD/Roa6B0Z7l9cePtxy87BPiLxEpxXnI/pAIO7AIbB7GN+wl+e5bsCCsVyRNgS1dHk6rCXUJv3qOHvyQDY/05N/C9q68H7OtXlDFpUDbzu5azPMOwbMJbT5Trfam/5GTX+Q7n0sw4DgBxqOsNcznivJ0T42d2dJPxbool2k0T0ZVl2GJtg42Flalt1mkZgBm8J97MTGH5hanK1KDWovTpNUztZSl1RDPjfhC+RXd0BnhzY7JrKO4AowLEDawEeNuyzYMuD/FyS4sLY9S0AMFDrRl3PUmJcKGmSJrkLmBVlRZyWf06DRenjpVUFGIb4iGSqlJkOCXAj/dnYeVJymy4NAjuBhT14CuqawrtfPuPGaRRDHUlq8Linh7MFqVe8cysHK68QTEaGemsstQfYjKk2QJLY+Pd2fi4xNgQ8JAWGfQILk+K1C8ATLhPwV0gr1LOnWguVAFzEK8996dSNF52PZTlMEx1yeu9pkEwIR88beS7U9YbBh4BLp6DURJLn4BPg3+E1RNDmUlqAtN7Fl6R6FZiGAlRDZWXzzYDxzJJlZs3HRlvKCA8UhDHYg5C+YGeap6UxTHijk+Smr5XA1uy6JRPPBpxGa0Oz1KKNTaQ/jemfV+DtlgyWyPF0pPci7QdsjLLwhAlPyXVSFD03O30ptwYWD4pe6BYikds1WYE02IKnj7nqrMCBhtdITA3MRPqjkaG0Zz8xofESIZo6IEknLEpUtx6ysp8H+5O6RJiwrfE/F9gPUxIVVT0Bhqks2nz2pF1bDljkWxIZX/VzNV/pq1vtrwMMBj/hd383iYnhF2xmfGnkaHr6w5PRn6SmBtmI4EGA6WctPFOTe5ztaPfxdSJZaJVFiYQHy1SgH+ttOlLmFeNlioV5yfHh5GXgEfV2vwB7MTzUX1FaWHYnHzYuF3sx7B7KtaDKCpZgmuHBb26sBVR2KQQ2Wo3R4QHwCmg6RJFBU6rDUIkXYjAy4S9x3qYkFdbTdUmiyXsD1MgfyzNg/f83tdn4vk5yMZ4a/RP5d03qfYZh34wx7ynTOvhlaqNeTUxvjI4E9P8WtV6laygjW1ZK+39SO++xUhnjZ9pGNInDLRbo5Sf7t8lAVHt1emVeZH5yQOOdeG4kBv/bXJaUFOGWm+ALEIsnsYe/mwUh6iAdUODBg2tOYp8qJ3h7YDWHBnsx506ghTKLNLiSxMnuaG/Z3d3xYOSSEGlwpexXV9V6ctnZ1KBm07i3t1tTVaJyfwfH6N3bN+ci+utriNbgXBBVWktuIqFlvawJCj5qemqC7UOwI6B4E+2tTDgY/5YWF9jMyGxVa9jiwWGKjQwA+ETzffs6E+wHc2aa1pDFrKO3uw2H1NvKylJb6z12ow536Lqe1eLV0a5FgA2LlJATYmpyAq9zTIS/Dk8NIWHnFgJSF6fHS2ptfkPhnn5cGEZki9RdVYJd4XGbmX61s7MNE240QFlwa8DhANTt7+UAt9vRVgirq7ykEJ4X0j4EFxwrS+FdRGxKYYkqj8PDQ3Dx+/u6Jl+NgSsDzyNPMT2ukz05ifZzRQJDgeBqgZs9Lfl1iTCM9ryz6n3dkSJPYvrl/ea76g6G1A4RMAaTQQUGyHtOpkhI8zEiGANPVMLk0KzE15wlhjCtmfUMfwHwdpybo7HyWeXA3jAOFW/FPFJGIj5TKtUIdRjbW5u4Fbg6mO3oypQIjxUS4WCqn6PqFZAeLkW4U7U+bnSFJ9y7nh71ybp9QjwL7rg8wgVb6507HLB8LyXF31rI7hUsuZ2rMQoDj0/7w2Z2wblCqyH3VslngB2BMzIT6NmY3+hvztsYrGcw2N3tkabSjGCR8VX2N9aTauS2tg+vyzwTHYl5MGN8ZjOzVBLWv2prcZKRbKnjfSVF0WKhQW2i3fMCN2szqWIYSfU/6XhIlGAuPsE41lWXfrAC7BPY4hjjKxLopwaYq8RgOSEWpgIDQu5yGVbkiFrOlNeCofzSwfvhnDzdpxYiz/EuDn+X2h/6DMO+MeNdKIvCXs2WfbpLN4ldZEFP/FjGKvN7NEeIaiflNTV55WzkB2fzXrp9T1NDNXuzmEFNRk2D1BSJTfQyYr0AfSFMyozzBpMsNLri6WTaXJqkEYnBG/kzfADYK0yhSxlJyzV4wCvLS+w2Ko0TfMSuF08JB4Yy/97hwQFWxYB5xrJy4nPTPVQcfRf7+zT3hrJhqK0FW8cnCabA0OjjbpuSEEF4KcFyEI8ZjQQ5JNTVpVsCmObyeBlI5hCnejU+KkOzdzmOSoG2EZBDZlocoKa0pCjwQgCDAYzx9bQnXrWCb41MZXB4KgvPAOOB704cJpxwN7EYCY7wbk2Zcp8SGBhAYlhyCYtNWXJA3YCjxfgoYmks8mEyoloH8gE6EqEt/gcgc9hO8Eh4NRx+wJEhS4znZ6eofAFhmccrDwvAmQWeVc4AH6fOZx1IxUmE7F50PmbHGuCRhF2op+v58vKiwo1GH5S82MXBDLwZAP8hAe4A/jNTY6sriwHmwSLhqXMlB9Ky0lCAOv7WgsdBfrtJSQzLQsaluIwLcbGEqh6Q0kv1Wl4ATdny0AwDPv3TjMmAMZwfNOKyEV+PsDO74+H4MjRgMDxkIjJsMT52OibybWz0UETIXGz0bQ8HRH3w84G/F1VWTh1p7fOhUAQ8XDzZa3Ajhdcvas92o9qgHRxggZy6inE+42FLI7mkcIQcqXjEwGKRgYelMa0xgFC8XW2G/PXMFAlLKWbCGxo46SWyngb5MQD7S3bHIGwFcLTKSpjPnrTDCtcIA2D7gn2Mg32Re6yuLAf7u9JLTmTQVp60PUKXI6701S311OQn+pqaXJWwVPLgMZyZmZRauvz8Dy8X1hcWJGQ0/azF115FhavTDesvL5FYGnIQTsB6wDINwF02Ztee5rnWJtlhYR5pGB4a7GVHFS9rhgV5RoX5XlYFL8d49rQdSSBdrG++KHTrv+PJxmDDpV4F4ZZigT4iT1hFvCR5NBjFfJr/kICi8b9hhHbfD+HkUso53sUZAd0x9PWl/fgMw9SMyavSFTDw29TBxPuC+yN/KG9tVIXV6OYxshbXtWbaLcxLIx1EGlXnyRYGFprUoogFehF+dgDAAE2V5YQDRjIXGpgL9e0tDRvvJLRoz97BXZfYcDvOlqXjPNDXze7DJmGnQF9nwDBxUYEAElQml4hWDziItwoywS8cGx06ONiHs3s1MYoeNniHxH4TDxXcSg7/WkVAtL6e0tQtDX45oEHiqsKemBgXer+pDqP+E+MjeAqAT9jpLGQZsbUwBBiAFVY5GYmIbRAUweco+LXsm4hFO/DJgwNcakUAdOE6sC2xk52pq4OZyhQZajfDTIwNwUPFoCNNV3XeSwMYA5eU3AVy4wCVYfHG7u4OuiC5WckqD4wwZcOXKqsOqByY5ASMjf3rpCZTNwExAqXitCfqqCgrkpn8vE9nSyOcLuo469mAgVYQElwzF1yTCK9ZiKQJZ3hwYIUAWCJVtVYio/HRkUAfF7ZkNtxTlT4KAHJ4EJYW3+EkV0njhIXkaCMMD/YCz7jhbiUse41sQ+trq2S5AuAxMrmS7GTZGx5CU2BnZEgnocGgGTIypKyJ6KmTX9S4jC9DAhAUgeftYyXYm32tPuH8Do+Exlpm1wG8OZjfcJYYelmaJDpavAgNHI0IXYiPBei1lhi/mhC3n5I8Hxs9EB7cHx78IMArz83OzuyGp6Wxu4UR9qHBN7pZGK0lJpy1PFhUQxOnZiM6RfVemrCeX+MlxlkAkF9WzdW6LD0Fi0HbPLNyvACWItbBqlzPx8dHGMKDu5/ubE3faLJpa4o/wmpX5PR/+FADkCgpeVpVKlZTzAZb5dzsdGX5rd7uF8VF2UQUmz0BJL8aH+1ob8HyBzJvFejYuYBiEgLDL8oyQwhD/d5Y84OSBKMbPwc7mxQfhglPGjTai6XFIENDH74iER7AW+4OQkYQIs5BIr9ZSo/ebV83FPGDZalS2IZotIgFBgFOJrSaVqKdmMkLwfYFzyNJySpPeCNsNXD3vd1swHA0N9Y2NVTDT3hwsLoEAF59XQX3fgUfchFVCT6lDUlxYVhymRdmCSfIxmD9xZ5dRe7utoaE8OyiqbD1KmrGhOr+V+eoON7TOF6WC/mi+vNChFbX5jMM++TH2TFfva/TXTkEGvw96njxvRzPzhOq599JW9EOJhUP9nT/bPB/yFfk0Lfo4+c9SHc1e5bxUGcCL8rB2kTWG2ZgaXqtMi+yozYjN9EPe3nBG7C1uFl/O46QKAJOAxD1sDL1IjCshZmeTmIz2fbReLey5X4DAVo+Hnb3GmvAeAOwIZVLYNsG+rvBadZYzA0bK2ym7rISRPAphwf7kGmdyD0jSaBaHqraWrbZoLkTNfVEbW1upKdEkw8Hv0EhQwLngkUpsPuzQ/6Y8gKc2fXiKZs//UHzXRJ4U6eL9e7tG4KjODJmOJaXFgpy05S11CxkVFQAn+C6gQMB7i+CE9IMRjrrFNjwSUcilnoCbAMoxS7RWVtbwSpTjqJBksgFr7G2qoS73mN3ZxspKACi47GRyqUgX2cdhGJhbSD61bYhHkbnsw5y7jw5zQ4PD9nFeDs723gWgDfgum2sr8Hp4AQfYouZa6srC+/mwQ1dBZ968R14jfNvZmFOTY4Dtn/2pB3mq4lROJEnHa0vOh8TIkSaQVSV+yItBxV9aW5y3crsZliUVWaeX93dW60tDfAJsA7BSQX3sfV+Q093Z3Fhtqebxfhk9/HJ4fT0aEFuOqwivEcqKXDY9cM4dQ45w8rxdLVKTYxsqq8eHRkErwumcroM/giOGnkXSn4BCop3tEhyskxytCzzcq7xdgVANRIROhcTtRQfu5GYsJ2UCADsKCWF1phiOBJVFqGtJ8Y7mN0AgAeOI3wgNaa2Mmdm+hX63PDKjkDftaSkJj/PKm+X7rCg5yH+PWFB9/09q31cQ21NAW5Zi6/Zm98AwMbuEwPcZWxyFf5pyaps9LA0BhTnaCPIyUqCB5MPrII15uNui2QqPJclbIbSPq6N9UuxePPzc+giw9Oqm3wznAWAB4JbNGJIsFaAe0cjw+TZlbIylfv20dEh2QZVqA5yMOXm5dEgjcEwnedrUBVSWxyrGm5KX+9LciTTUxNkF2VYJbVO6cOu5etpLxbo+biYvX1ZtdZ/F+nph1oKvJxM4e/wHAHqw9o2mNlYjnFyQpWWfmgMlp6+m5TkZmEEC1uIyV6VMCw9fS85ydPSWMyotME9UuPAJJHGsHB34Vi5d2W8LYoaR4b6wm4ZL5PgYwMnMGrtD5vnZmdgv83OSIC/hAS4DQ/1w9YKrwdvAa0JvOzk+PhhS2NYkAeYBrCM/l4ObOEElq5A43tSJwPrgB4a0xXmOFgiT4X1FXvAP5P8zEQyJwrWlXIyVpvn7fW5KsSB32byAe8H7RyMU6N/es7j3aj9JuCSbwwM2+mkxv+WGv4OLezNf0zflC6IaeP3dWDvguSK48pjb/DcMzB1nScSUy42g5/gRZEXbG9vgQ93qEpBpaK0UF5FIzKwMb9RVxTTXp0GYMzJ2thcSFc42FncbCiOQ6L5RzUZCaEu7g6i4oyQtuq0iyAxeHt6jCdpDwMvH/xIgq+61Ff+YCyfZJx4TvhklEgC95HNCqj2i0jFfG4ureejaQD2YMMb8B5U1vaQJn7Syba+voab/t3acqKEC64wO02UkRLDEYAnbi7PmjrY2euqS0kCAVBcfnYKIMa383Ngzok5Qc4xogTQ0/VcxvpwrjiEmHZYcio7QJi6ES92elalxWprvUfuflx0kLrsH8XoROEr2R3YpPGdMOxrNUiHISAZ7Z5pFgy+31SHfxkZ7ocleremDK7qs6ftgH4BgYOBhEe15X4DYEWYYO+jI/zAwLs7ScAJAD8DjCjYeFgM4MPB8oafcPFhOtmK7K1MACSDPQZ3Cm433Dumo+86n5Wv8oIAoPN2dpIIr9k7fRFR8rfp936eUmYcEm7l42kNeNLJzpTcC1uJkb2VyMnpxq3en2U/+cekFN+J8VE2gtV4AHSqzfg6TDMjeuIvWEKj+vUADuH1JvLsnMKEqwSLE5AkIAe4yLAyAZfCZVdg+0TghBNBDjZiAcKxNbuOeaooe3MfKwH8UuzhCNhMRVqM4a9HmkT4hEovF+rFS/UP44KU0EyoV+hmn+Nqy9QlStEUOJ0Ea2Gloojh6hAwfxQwUtFwVGnOViG2pqgRLENiBkJZJxKsBFgGJbfzAIFzP+OIS2Fp8XQQsUyAl6wivzE02EvYIHRzUmFTJReB9A8rj9nX07hcjQVXm/09qawc+e3Lz1cpHcZUJN5QawLUEbiDUTjfUVxRVqQxJsie4MrDclXJtATPKe7J3LZP5QablhwNllpkfLWrIXtriC5HXB+sH2u/7WxrgpKecCXJ7XBzlEg36olXHyMVljkcEQoPCKx/eO52lXnqZbw4szFRYlmxjLoOAiyLgB0DnrK6JPuRMq+WDCcL8ZdCY72qkty0pEhlN0BBio2djVS+leReYLAMUBk8HS86H7PrSkjz+fvwGTE2jarNj3JcBlgVicOlXkURVkhGwmwy13Qg+2VFJt5RA78j9z/n7C7UicM9Ttao/v8oZ0xYLfrmiI99Y2DYtNE5uhWeel/7w/SCwHfNWusSAwDUdDBJnagPJR6vUkO/L/2KxTgVL1jJP4fEXpvz+VoiFwauW+ezDnBSmxtriUM8NzsDWwb4cOAKg3eo8N7sjAQ2DJOpfgHcSs9L8odtHSbgsebSJMBmNQXRYb424CvAH2FrACTGplXUGoZVpcEnmMvKuMElBf+evf2BrQJg0Nf7sig/A6wd7Efg5RBzDqcJGBJ2Z9Khq3GC44vvfdLxkLCAgB8Me6uKy/rypTSYuqrBKZmbnb5VkMkOf3LUiw/0dxMZZQzBjg4PSNmNujtJgSju/oBL8Z+ujubqSrMAVZK2b3Dl1VEpKg9CPo5Fm8gIohxgplv8GUQ0Mz3JBhtkgFcBtpCjLY0OzDP+MTGl6nwyuMUEXcNJqQvsEbZ6BKs4UEkME6E60KajODVNoVmqnbT00dGhr6cd6Q+Bh1E5XArIinCysR63azQ+MZFCFMVpovhPCVM0SIMTfJfJdfkUXONANZlEHUghBzj1KLr2L9O7vxtZ/efiGzfFN26Ib94wM5SCJek0vA5fain+0szwhq3tVWsrffh2gKzsBC9WOoHTnxATUpCbppAHcLAU3y5JLCwJj0uz84/9wj/th8H5/59/2t/Z2OjRKItdGMmcHXyprfUNn/Crrl76tja0Y4RHAt/L8WjDFQ4NdCeRBQ2w0FTKk8EAIT0ARfDLNaNfZLnYMlkUmYAY6n1lZC7GxViZXkMY9jg0gAOGbchCKpZMQszI5ArCMAkTzMYOMYIJ4TWuFkbR9uZ5bnY1Pq5doYEz0ZErCXHggDb6edw0+UIiP+AvVZYNp6dEq4NMGxvrWE0aEeLNcyVjbxj/IkaNo6/nhZQGxs9FNxhWdiefnO/YqNqWfSwvhMtrZ3Z9JT7unFYB/L6pPhvMhJlUZP8WFxWBQXU1Na+6KHRifITIuGsMBXIzH85Mv0pNitQ2G4YBMrFAvywrbKWPJkUEGLY90pgc7mpy8wuiMkIU21OSoqTvrKnBsztLS/2QjWFF7vYCJu6Q7GQpV8+joVcmlZNDFRYel5bu3r3be6vAQnwdF7+6ensM8JkJDVytb3YVuaOIVnGUdaKvuCzB2dbCiL0xgnFRKHoH06aSDwxeCTcLEJo6s7u+vtbb/YIt8ubjbtv57BF4I5cVxWAbJjhBJ8sbLwrc+oqlMGywxLMjx8XG/BrTDaghVsvDdz2km7KkuOg/0Czi73WAo0uoF/cGL/ppB1Na1Y59hmHajM0GaksnFfA3zudIVxZ5S6/sPKcG/5ssGOCgHRLb66VG/4yuqX31U64owlopLbxAczP+Ci2Hpzy2WumqRVIsuz/C/bWwrRO+ZndnCwWVjIV388RHVEknSPgtEIZZm18HrNVamQr4qjQr1EZyw91BlBrlDsCsuTQx3M9OaHQFu7lExlciA+zbL5AQg68ozwknTgbsYuB2k8ROfk5Kcny4gksHLgK4gODpAs4cZdkz7ILAdi/yYrBnVeW3SZYDnGM2LX5hXhp5pWqFjbExulFbUz3P+NgwOzAWGerDHcsEr51QU6KAGLoacBM3N9dJjxOWF25vbRLiBDgXjpgx4dXgeJnyABhDSDU8XawUuP6a6qsxV4YIDX6SW6McjuX4lt3dHTyLGh4ax3A9iU8D16S2qkShpAowM95lMJ9sMSKAXmQZkIIf/oP0AMCHqMwbcwy2fDBHhockhWDC71YSAwdHfSePX7gH/swt4KeuAf/kFvQT18B/cvH9qVvgT5y9fwY/3UN+4uJH/5eT18/sHK/YWOs5uP3C3vkLR7dfOHr8HP4IL4Pp4PILa4vrrg5m8BApNOnRJGx2psp1iQu7vZUTehm938no+05sw586uv8cPscj/B994n/kl/JDmICXAjL/NiDj7yKr/iKu6U/C7vy1T8KPEDvBmmF7J3CDOp91zM/PvZ6ZBDTOLkEEiBiTZdw86Vwy8rOc/j/J6PtuZv93Mge/nfr8D+FcEGpifoy+3bZX4TDgpIJy/zaz/7sZXT9IfvTHMfV/FpD+d94xP4bTx1eSCwhHwgE+VTKCxkUFwv0ljCznbpCF4bOSW9SzZ2fPn293Pl998Xyvr/d0ePjs1aulgX4bSyMJk9GqK8o5Vb88iFQAQAJ/ayFALGvxNcBdIqbI0EViGGgjSnK0rPB2afT1eBMbJU0FZMrUnLFpLTNzIjLcWWIIX4fVlZhJEzIiS8o8ASqJDRcX3uLmCRsdz2V8qyDzcrNhhFcTNmEdYNj+/h7ZzwFSqmMihVWHsRu4RDXerio4996qqErAVD8+7yq/W8pZX1hIPXhA2wLO49/e3nrUdv9F52PYG9nFsVidyGZHVMkzcZGxtbkR6OtsaqIX4m292n93beAuKjU/LE9Gug6ACmBHjo6OUEob5iSqby0tfYRUWHr6ZmKCl6UJLGlAYp0h/oeVlc+jQuv8PDJdbe8E+zwuLmivrWiquNNUUxbg74LpKbi/KmlmYFHhni8W0IphgMFQRAuQ2EipV4irwFSgz74XBbnnngW4ZUgho8xaOTLcD2BY44Owu7OtrLcZFuShrcyA2gjmwUGQnwujAqcPwHJIVpGIomGhbkI8QQBpxQluB3vbOn/R6cE8NfJ9qudXaaKE9yfKLD+xGWpGSL31vahM8/ES3V0GvvT431CH09RXYXx1YNjZCZ0SfflL9Nx5qvXbARGxWwz7fpPa7uD73t1uqutfyvSaC7RJwRmyyF5EXBCOdCX2/oZqEE8I7mFO/AN3hxupW6ML4eLCjliEWrS0cYSfwh5RXlKgHExSzoYh+SH8DuirrSqtrTotJdIdyxvgZaYmdJasIDmgnYeqGAe5YuOdBEdrI3OGL9HFwWxpcUErVtn46CDc78AKgr/+4vlj2DeJj4XgigAbBXPLLnfJzkhQmWbivuHwXQqJuNjIgCMehGbkkPy9HRn12zRkSwe4Mvt6WoG5i42T4Y/qNIIZMhKp+wufz3/ZwjcimSTmkXq6O0mbBGmia3/YjF4RKkTxb4JSyNfxVAoGpAqWjH1V2XCLHBW41ApvhAWAZjsy1FvbPWPy1RipSNGKaWpvd1dZCEjRxRdeszQ3sHf6wtX/n7xjfxxe8ldRVX+Z8PCPU5/9IO3l9zJ6vyub34GZ3kP/kt7N/KUP//ld+GfK8+8nP/6jtBffS+38PrwLJvwRZ+rz7+d3/nRtfamvu0ulY6Eg4jy/1ZnV/af4IfTn9Hw3rYv+NPg6Giadn/BHPJKc0W/5JvxIbHgDM2yAdRfezcN1g1lXUwaL+VxJj+hL8c0bLv7/lNFDHyH5LjzslKd/CLCKfup9fwpIL+zOX8XU/2nykz/Eg6FPv4s5mIFvZw19O3vkW1nD34L/hZeFl/6Vb+Lfuwf/xN7lCysLfStzA2k60eQaDxhGU/g8fdwGixxwI6xGmBWlhYV56cVF2YuL78i2+aSjtba2nPQ3AgCwMJN+fgVnmOPgYB+XOgpJUxmZ87HR3aGB/WHBczFRKwlxp6gNjeF/zLap8Va3kxJHI0M7An2fB/sPhAdXerskOFpITA1ESmAMvlF52yF5pLTkaJ4rGQvUFaIbWhhtJaBCOl0BK+oAw0iZAFOynqnuZchvBLjXzcpkh4a1Sg1+44rVm/PzcyRaAUZT9ee2tFDd3ZT2eXXAPNNTE/caa+AZuVWQMTY6RLKCMNta7/H5EMCWLzuf8OlTxWAZwLAnNemAvpaYlrCxttsOVkZigR4tP8BImMDeKI1U+jpLO2+7uj4CDMvMuufnYUzX5Rp4Wpoc19cf7O6aWRpiwbCAKdCFX8RCfTMWr6NCKzU75YthRJFAPy1QzuQ+WOL5IMNJwgS/2FSWbDUXNgMnkt0DpJHI4lb8S9MfNNerTKYpy+foMEpu50o7Hi2uP8tzJRyJY2XeBeGWQmN9PHJXm5vLExcXCjs923//AOzSgNxrmkoEfHviKi9EfyUO/CsFwwZ+S9Ym+Lt0IanWMaL7dLqTrTp3xJtmCm6nVEzs3zA0nWxLc3x6oiYUOvzdcym4PfWe3MkG1f9/SfXIl9WEKmdt5R+1kstxsIQSTbmmnE3OTiY4r4T/AMwtO3ZOU3SIv6zMjSClhoDHsCUMfnexFZgJ9ZnXXIsLcc5L8rtfltRakUL/1KQVxtDZpylUMOLHujuIkLbeztJoeXmR3fFvxhQO8QFjpIqGNIWTFqb+vi7SvcoGD4yWsRvh0tC2gxyMKyqTksqW+tpynkIi4DORKO/S4jvwBZFEBIwKXAH0ZeEFWCr5anyUHbyvrihW97FYQ4jBvNczk/zP5e38HDte29wk7ZSFq4pfHSSz3FjWAqaduy9FeSTHhzNMiUk8Xw+XAtxHdnyR5HMI0eVAf7e64CgctrY6wiTJRrKUPAeh6FA3ASQ4e/8sse0HgKMQz+AEvAFgA/APG6KonQCTuhg8Q/9C/1M6X9L/zBr6Vk7nj9NTwy1EqrvF2CmRhe2+wv4fKX++xmOAg4+u+zM6ByUrJlTuz8S2LjMjRkvNWs8n4UeZA99R8VE934279ydRVX8Bn0kjvf7vINSUnhEAy87vR9/9s/DSvwTcFVX753HN/zOx/QfwRwahfQ9gZ3LHHyXBbP9B2J2/dvX/qbPPzwDokkYyCVPiqO6OgJvl62kH8P5F5+PRkUE2Rj04OAjwdwO0Q+eFbATRUYF3ivNKZZCGqPlxZEVQigrAUqSdGQ0JsL6RZLrS0rWQjc4gWTLpz5ehgYE2InBhrViI10ZiuLqyrHAkxMuEVa2O3UdhIK2LMhUqXydO6VtIkTl/KHjOorLaDqcmJ9SlI7AjVyTUjwbcm5UlZ78kV1JJOmxkuF9uEKOD3hO5gvLOjJXnGuLAuzu5WcnY9qmRgg92RTA9QqMr4b62q0w54vpg/fSTUnd7IQAzdh04YapoQyFBbm209zezsmt93EwYxbwsF5uJlubm5rskgIgoCDvSFRhNVFYoYEgO4da9NDl9xUgpreBMtIxxtrA6BdibNqAvoYl+mJswI0hiJgSXQ7qzDQ/187m5JNagMMG7uOCyoZ0ZyU16bZvow+mwQWZtop2N2TUpmarQ4HFN5leUNlCXcbJJF7j1/to5fxvmWtlX4vC/SkWJx+tt1NQV6fUd+SNaQ1nbsfuCGv72OSaWE948vIsx8jLZLQxfndG9XwdrJ0dqAlSrhdTAfz4ndccxVm/Lyw5Xi1S/Zkpf9lE/4/ikglx5cZ27swUJGk2+GiPuO0AvdsAei1jW11YVKtph+7O3NKxn2DgUmriKUgPNGQwGP9NjvJ7czYJZnhPu42ru6SiuK4ppVU+cCP/VXJoYFeiYGefNflkLA+Hc7UVEPQyZ0MiO7GVlEhHkmZoc/aSjFf4OthNO6vnTRxVlRVFhvmwWWkARGDhkIxy0YQvv5nEvA8C5cV67hh1tVdSNUR/xhXdlZySw2RFCAtxfz2gXRiKxSUDR6P0A+gLsBMePiAiOGYEEKmKxyzLVGfKDg31CZuhgLUDNZZ4DEBfJosCFJTUVCBHJxZRnorT0XVCKQCs6ePh8QlhC6D0G+rrxyqs7AAwfYv2Jtt4VaTmD54hbbRY+eWlxAZZiU3010YJTPUX0jKr+86zBb9Og6+X3LncievFL/ns7hysKwAMeBJKeqq6gvbGtg/nWae+Mrj/W/ev6vhN256+wiUuKeURIt3gN0ZelmYGL30/9Un4Y0/CnCa1/7J/6Q4VUGDsFBwev4u/d30169EcOrr8gfB50LtHMwEqib2Nz1cHlF3aOV2xtr9o7XnHx/6lb8E+8ov/BK+ofvON+TBdMiq5h7tHR7Reu/v/EgcQUOnZgJ0xJiCgvLSxg1SqrDHLDI1ZclA33fXl5cWx0aG5WkbwHhbMBhkXQMCz90r3Yw5SU8ajwYBuRqWzbhCdCOX9LWiUDfZ0VKtXVDZJA49bg5j9IiDApPkyHt5NT8HC22NtV0aID54VaHciA4m1pshzP5BvT0k5TU7ENic46Pn6sHHgiG7iPu+3R0XvU4QWkRAq/Aflvq6ItPRfgGxkk640bhp2enhTmpZkJ9B2sjMbabm0M1q8yKmERvrZYtwLQC/fAqclxZNuzsTCSUh+NT3yUisSd5CQfKwFKrtmIYbuQtn4BaBQZX4VzgV/sLG5am58LJ6WRZrbzAw2EufBLR8sbz/Pd+pm+qcESr/pkByvxuYplsJuoXHd6evr0cRu6BJhoAgBWGW/bc8tjtMy7MMKKDiQx8AbAIYBYjffr8OCA9BWzJ7z9gvpdKBeGQLEiznaYoaqHc3xZ6Abni2LN8F9erhZ7W2vUN2Ecr9B1jEN/cI7jfvC/0imQOQfq7OArcRJftd6w032624rkxE73tP+Ebbqoj9yzpWQt3jsjlH3179CfwyvN8fYc1Qz3170Llr3yl+h+MOVBd5EhUYeE42PaHzazIQEgBMBXNH0zw+hNmpsBjZCXIfMBuB2KYXuhvrONyb3SRAUY9rAyNcDDEvZHTIWVZIU2lSTEhzpbMrunwPCL3EQ/dU1iiLUi/e2Nbvzc1U7A/mT4HT7HycYYixLh8BYX3q6uLksY31Eg0Gvw9dibmFDnB29urvd2vyCBRjhl/C8CODuYsB9su4BC0dkaGjjHtE5LzbCyQOoCrvJQ99ZmYlyogsYRuBrathJRDIc7ttDkZ6egtwGHh+E6pI1GG0wI6wFUYEIJX6lAkkEGgAdyRvHRQeoqGFWOgf5ugtsBh6D294ZM+SckwA2u5P7+PtHB1KroAulA1JEOc+HVzseYsAXvFv5J2urUkaCAW0xSNNp6k/8/e+8B3lTWpgn+PfP0dvf0bs/MzvTs9PTMM9szs9O7M9vbVRQUUFBUzoEqggM4S5Zk2bLlIMu25JxzTpID2OCMDQ5gMpgcnA02wWQTDcY2YDC2td+539XRtZIlY6D+as5z0GOke6+urq7O+d7zvd/7UmxMqss6zxiGU3BN4BbaWqaGq0FXcM13kbODIunHop73FxyAYU4p+8CykISfuAoWcG67WhpQz6CmcrOWbuqrKkwqO/2rKVBkFRJL3vYJoD6JaANJvkGsI9wQELImOPbn0PQfMvcuZxNcDLGw4IzVx4e98k4sgYsGsArQl05i0ZkoiDBSIk7kD/K3o9CBKIvgS2y1GFMw5uVh7+1l5+lujiaKVEYP7V6mNmZEUIgsipgBnJwwywXXLOAinzh2CG62nc31169dQRYx8fmAAS0rgySyXh2MqVA1pPhxTnZ9qBwAnjdEmaYrfunCB0oBWXj/0yWMV9Jb4zT4seBCQFLcfEiJFGKhwoRh4xLyceXOT+QY6+Oe5CuMZgrztob4745U7slJgzMZGXnIHevoXkRM30qPCqta//keuhrS090+5/Zc0QjzhWRYzgpT8O7KrHHGKOzpwJ591dkwI+P9iYuDMG5T0f9o6iJYX/9WGIkdCTHoAybSStegPnNrRXpbfd7JJjU83jy1LTs+kM/45RjV5qUNtUlEfHuF1LmjIgQrpvpqlNEyN71UWFoy66a1p7VRR0TkkR27mewZ7gtIbGuyhO9mRw2glXJJT1e7eRI+rcM0VMW0agrmNojicCp3J1VhIhRI7GbOsCbdB43R4DE1wvvu0DXNP4X2rHtWWgUA2DWe5lnvzNTTP65M4B+hUuL0E46OvON8RC2nn+rQc9dfk9JAy3ekPuLwfVvY7hdwmJD/kSBJM40qOhrNdwGoI7fdnxDlevOA8eplLkEOABiXagiwBBllNE0EceSli/10QQgCayzFATgkAxhWow/DDu8oLM4KF7qxisneIgcfsaOIB0OVnZBn6+fl3FyRcdBEhRjAsyp1vIhnC7ur0kMPby/kJNkKqosSKO3QS+Rw/+6d6Zcvk0KkQnc7Id8u1Mt1urZWMxejBj7LwPleqnZIQ0+qJEEV1Q313KleFnRVvknmzOTk5O6dO2gNFWKhUnWOqapxS1phbioGARBM4CCO7sY0RwcvYazP0IqmIFbgCuKbmtFv3bxO0RQc0MKFcHYhduAc12kNa8wKtE5oiHxo6BMW4mMhCZNOfnQutKrB7Q1wdPjB/evXBin4ua+t5DFstJquuqLUquAPvs0g7eJFbXUZ7Ds+PgZR0UB/HxyKuhibiukNpfzIM+4bUhs/Buiy8DCMVHN9AOAHoAgtfujl3BXwu2ArP5kyLV/ZutwjH1oHw9o/oPxJknbTlpAByso/sSRj18qk2s8yWldAJ1t2sVVk1nIdjSExcjR4i+z9yxKrP0uo+Cyh8rOowm8iC76JUn0dkftteNZ3ioSfZKG/BkWv9pevk0pt4NN5S2wBekk8mfBLyAqisCk1rc4k4iiU6ZdH/uIftBZ2hx3JXlo6Jebf2I2FpHotIISk4ADaeQnt4Th0G8M7ISjAgw7Fbjyb7aHyFyrVVEEBUwmmVYRD/2js6BzNdiwV03LqMJPDvPSyoOB2ZvreKGWQpwsc1p1vx8Vg8Js1pErSBQtT/t2GDSnH3h6OrzKscRslhMv9hE+sZH3DwEXFRbmCqLqVrIfDhmWQHowWJVwf5lHnBACxviLQMzFWWZiXtq1mC7VrR0rn/Xt3X18IQxcKYfqwRMH19Mkj9NzMeECNjDzy9XIRuNlEhXiiOuLjvl0X26oU/gL0gKF6SAc4tQl1dQz15vbtt4DBGNXKfJmEz7OFr2lrbJjcxx0VRCGiGDxW/ezC3tFzuwBJXj5aJZe6UY4MTN+m0pVYlwVoJNzfFVFKX7Vyd4G/1+yqMMzHwkEu9PdRiEuKwdzsCqI9znMMkeEIvVXK2nQfP08HAc9ezLEhhTmIa/ao10y52M9bkYVqJgMgbMnzg5Mk51atLEvywk/nwbcPlrqN3bmo+W22BZQunH6muR2l6fhLLQD755pL3+mVC72DYa/763yhGVKwX8CFT4g0irXt+RWd/mH/hySzaWF72kmycCwSE8DtYNFej5t1hYPEH8z0Xi8f6MwTbgYYh4LPujQTA3O+JwAMoz5aEJnRrLpRPTeJyOHyxQHMA2A2bK+xbFjTljQAHgiZxIz+kpBnB/tuyo6Alw7Um3QGa9qaDjjN3dUm2N+dlpnRV/NTFBTdBcg8ZgBxXbnakRjLYyYSPt/2RFykIbPffKMVBdQsmA6RhtVEXLVxRBqGy1cnT7TpEc9gFr98ceAV7+sBLfkkNTEChUkQbkHMxI0SAC3jZNzStI0iZ6xaNkVZ6e48Q5EYgHCr1Nvv3r2t0CIxAMa1FSV3bt3wYxTYZcy9BFeMSoGVFuVYePDOjtMYpAKwmd/lgiNQbk/5pgIz8PLB/bsUTVklmfjixQvqvu0ncYuPDoYA16iqHqWolZXkJ8aERoR5h8SsS9+5MizrO5GrA8VgEN+n71wBiMJozoeoa7QvojIbpGxMi3ZU7XNjGJIKO7iMFERp8UBUeMDRY63VlSUQ+eVkxOOA4MGUaYVlfJ/bthR2sQqDFZxenFj9aXIdURPJ2P0RPMKTgI7gDIv73iMCHr3vFxHo9f784NYcYKyDXAoC7XqMC4eQi9ZJqsjglApOLSalYkc+zDm0LL1lZeaej+AxruyL0PTvAbYFhv/iF0jgFn4vAgen4JjVmKKE3WFH2Ctj1woAe7GbvgyJ+xkRHXyV/sFrERbmn1yctW951t7lKQ2rIvO/CUn8CbZB2wAG5m30nJ0rw+SMj9Ah1Mstxoe/IyyoNEhapQjYG6m8mJJ0NS1lKCPtflbGaE429Bf5eS/y8+9lZYzn5hDpcJXqcU52b1L8weiwDH9xip8o0pvnLdwIiEJkUCsLQarRMkgqzgRgxsIl+dad23HAuTev2jDDtn1bJV2ysUrUB4dHXJyCeccwrT0x8YxK4L56z81MnHfWYo4V/KdPKXkeXSvnbJcu9uP2ocHeZuJ+VHrku6zfV501fr71cd/O7r2b/b1ccD4F1EE/EdWRIleS4Thomlveil3YYGoSeuWlyDwfXBwIZGY9gZttRJDHvY4dDxmd/bHzrfCJeC66VBhX31ivYUjj7maXHSEEoAIo5XBxoFTs4GGQ2YaBOilOSQdzgDGlCZ4HimRnt5AcGnSqfgFo51xN6Kny4KxwISAxzDtRMAa3yoljh0dGHhnckBPcRVJuznwefsp3bt9CuAgfTR0rxqqwvppQwGDwDIZhAM92ba/4LUbsU481g2s1l35agCTVzCTx2qVBOPTz789HtO8dDFuYdjuC/RoGVpLvxto20a/p+rfsEW5ZUzr59AwR6mCVWFIt3etWEMf7y9Ns7rmBQ2LMeZUrVKSVLpiti6jTpoOQ1HADZF3jS4TMI9hQV5pk6AYGz2QnyAWuNh5MeZjAzUYqdtqUE3m0SW2qKgx22VOTHREkBgzmwbffUhCjZ/d8eHtBrFJCYVhgkGR6clLT0DBZUBDr487YO9oFiZ3HNm/WWJPTgBgCA9BwhS/O/bTCvnlHramhnHYuVANMwhWKwHhifgpIHWdPwqXubNfJGcEUi3oSMOBipgWmSaQCcivIYTLWcEiMgA24ovymdLcAoVEmzKbiXKuCjJs3riESIwYGwo1DO5sLokP4DDAGlAKHovlGrNEy5azCbUPaYgw9yT5Ll8Omp8NDpFR1DaCg+e2btUuJc5b8wfQJ06S6IKNEnQ2zrBknVjF/o8xHGBep2F5XnZUaj0sYO+qrz/TsquxcX9y1JK1pZUDIGsyJid1J9VRS3SdFfe8Zwpui3vcANqBoB0CInLaluUeWAgwA8AAdwBVgAwakfWCunIxIdCwKUKxBPQxjbLqNImcHedQvac0rAXJYS0eEXeIrPidkPz7R1icBupcdfEBfP5ug6NUROd+FZ30HmKTwDJsfY1Jh77866ZHLfmQPyxyZKC7qiS6206wgc60Q0Hai4CR5xN3xs8MlzTv2IaDi1O2r4rd8nrrjY1YRpP0DuhcL8DoX5TL6HynbV2XtX87mANuZbTpZMUnYGL412CClHlDZt/7ydZ5C+P1q82nU5I3PLLrz7TAnwzzaYGEM3GkA0mQeTjKxU4SEFy7hSUUOwZ4ucT7uyb4iudgZhePcGbF7Id/OqFhRUWGmKfkNymGOUPrO+XthqQEMyc2UKt08Gl3UsDwjR1tfbyddhzJMfT8eecRdH4Ez37enBT6pITcMUFBkqD/+rs38us+cOvY6wpbr1wbpW1iYY6SyinQZ0QiN5solxGAFySGPeluGu5ue9O8uzgh1dVyLEk1cpXXKcs9iKvRmxsZm+aq9QXGObaGBrvAT4Nue2Lrp6eioB9oMuKyvKogB9AUYjNidnW+tLoylMAymP1OUUbjzcb0SkElFigRgWH9d2I5sXz06IvVOpDcMABjobSXygbpQxGAdW0OObQqievddTN4JekOWNNTPBX7CFPxQRa6s9DiYfO/eGTp25ADM7E3ba/a2Nhm9tRJjFVall+GXgk6wEI9JhBv2q2UoPQLnE+7vSs2aK0uzp17+9syyRraRVAdEs31/P59AndvG9mkGPuYInv8tEbSbntD8kbc/avvmGc2Fz7QpJod5aSfu13T8Gdm981+TFJPl7WG5Tjjx6RlLz/aGjy6F+ninuW0vfM4RRdw87wv05Mk4jDVYEgazEcymJ4+3cWljU1NTVC6JslmwIIpq4gncbIuzwtt2qAxLvA41FJRmR0DoKfNxzU8J2VWVZca7+fD2wqYtaXIpDw4IPSNOplc8hgk0RYC7SDus5OUkPz1/DksgdkcqIApBYs+h6DDN5ctW5XMQhgVp9Q8pHdyoYPqD+7NU8nMzE0dGHgFsgFBez4c3ISbEWv09DPT37W7GUwIExQ1HzpxmBdZp9FCqzsGBmFbuwUvwteJ8A/PrrZvXh27doPkowGaGltxcwoZW7lltNFyD+OzyxYHG7TUAUOGxpbHuXF8X4Fh4EsEezJo7I0JuZKT5ezjiPASgbnx8jJuvy0qLnZOdCKeNMKy+bqvVyeznE1yobMbFlTbqNA13uN7K5ZXBiyeOHQJkDggNPoVSLjETnBHcGxVcVb65vrry2rXLT56wqbzLl897C10xAeLtbatM+VGmXIMVRJQOF7/1cyMlYUyKKb78c1norxKxHVGb8LMBeOPjbcuEiYQFB5gHXoUDhsT/nH9qiardnD5H8rZPJJ526O+sA2CMv7Nf4Lqw9O8B782nMo15UxliPIat58G6OTuyj0yHl/wC1gdFrY4r+yKp9lPAe4BeFiozBgeMLvo6bvMXaU0fpxGxxI+y9i2H47NAS8uWtPDtEKehUiWDoxbNkYjrYN0CTG2AevpwqOK+9wrOLM7auxwgdGLVZ5EF3wSG/QqXReprA18xXCIdidGdzZKJtRbSIqYDygJsRv2dmRUocwqxAC3gxoZRxczyCqrPW+XZhTwoGFvG5pIlsLChljo6c1gbMh46sJvmE4aH9Skwjx4OU3aZ1NMZcePExLPbQzcH+vtoKRQyk2GUu3H96uVLAzAKcf12ub1EnW3qKsGYz8paWN+oO2VacqSFa2F0Camm0rhCMlxJiPtFfLsQf/ehM9ufDuwZPbfr7M5iGD1QYLCmcjMXq2BpNBFpxHrX3t63Is7xPD8/ypsP9zZMJSOXySAsJojITu7jduPktkc9LQjD4LMUaq1xzGtswnWA+4pL2+usUDApLDszvx0Bz17q4VCZ6o2JL4LBKkJi5YRnUZniTdUIWYpjjbKzUlGX4ZMW6g57iVDUfrYftG5i6u/T82jlEl9hYrXwnoH7DedKAaPCj5zJc7WhewsDmK8Yf1D+4wv0I12w9uIm4QrSUPZ25PwPNdHPsBC1plPtf0bKdp7/8Yjp/35hmIYIoVx11cpm/DfNxDlrDzA1XDfD7v53pODPiiRxrLbc6//UPLe47p8isXP/05yh8/PLmv7l2hvufyGKi6/Q4McJMfTNG1eNer+MjT5GI10IQHc211PlwMnJSXQhBFCkCBDsq8szdAMD4ATgqrE8rbU6q61RZUakHlAcgDSAWAI3G6GbbVSIpx4dEWvGCtOUIu2IGRDgMQ4nU1uLQ/Zkfn6olytGJAlSAfHQtJhZB3M2stfoMvCpE0cMZbu5jQora2UG3fRWVQHcdnednQdx5eSJNpwqmPPxG5jt6gjAGC87jOYIBmC8xi9udHSElmnRnpoYAQADgiQ4GeoZDWfb0rTNKMo6uF9njbK1TG14bmgNachTTY4PqygvgmgGosMICU9TVtbTdsBDO+UAlIJLQcnrKMllPlIB5IY3Xliwj1W6ZHA1UhLC6RtZSOyBRqVKIAyFKRA+LADspLhQo7XUWEAV5OsVHhzgI3bhPnnk8N5Zc83Uk4fPLl58sCs+34YtIkKU4oZJsA0ATnx8bBOrPzVK1YPoP6N1BUAaD9ScYNQmRE6OLLxhlDYIiiOyEA5CR8fw7O/M15XBuwA4CY77GZAbS5BzdQAAELv5S0wozRsUqTsXReZ/gxKIAcFrmRKpNQDtiIu01Aa1K7BQSuDgJNjoxKhlOMkjV1Pd+VfJg6XuWIVID68tvBGEIF4ie8Cr6GcdVfgNYDOSrWJycarXwIo0A9IIR3H/8tw2YvtGWJpwDr3vsa5rve+hk1v+8SU5h5YBNI3f8nlIwk9S5qKxuTKzoiB6Ko7wA/eTuNVVl+1qaUiKUwK2gXF7TlUJKjMIPx8LfzJowAXd1MqOta2xoZqjRmhdXTdFI0btpO/cvkVdwvOzk/VepSMGDLncRARMDaYqPA3LhrENXr4QFOChkHtZkvM3XHuiSzx7Whst3IsuOZmq/kX1WhHPNlbhtTkrvL215NbpernUDUvC4G7hrhUOP7iPF0oscngMlxHG3vLytyLO0Rwe7MqzAeiVydyQ1YweDN91fUFKCE2FDXc1PeptSQjzdne1wVVLqrxl2OAbgdmESRltPFwc2F2ljJK5UQ6h0XEecFREgGtbiRxLrTDFdKg4EAvMhHz7qlRvqnpPjZIBm0E/WirfmeeXFMJHM2WSH+PPWivJyYiH6Ym7aEtlcnBmn9OrQPv9luOpyr2dTpcT+UfogDBjAt0QYUK0YJUM8mtvk0OauylEVJxNV/wFwWBT86o+AAB2Q6Jp/1MdnLvwieZZj+Z31P7we/gQFIkBtrFexX7muqdO/9CqMjMqH9/395ZWl03eJqkw3GtIYXbBf1DT+S+1ude/MYfZXrnBtHT5kpG6JpjgtfJBtlsNCIRc9Xkzfs2A0w5tLyjLi/KXuADEgsFU5uO6uzrbkLh4pFEVrfCijERVQcYLgB/qIsojPxAd6s4jRqUAxjoSYjRnrSjyQbFEmHFxxYjqcBQVZprKtxw6sJurmqjXa6vLrL3OECdRAWjUCns8YiSFiwXcgHwo6MKEmIZxDOOKgujWwqXu0PVKAetMnCGgL7oNujAjpYcWjmNROzKg+DxbwF10ewg+4FUvwYbJAyRGSUyKoC+hmgicKn0mOECsZ1un17CWA+ISo9fBBKXnSmiwN32Lhrq5qfAAR2G2a9peYxRh6uEuCPEVAdLCrMyWxobOdta3uq+3w8vdmVp+ba1LujK6p+Oeat9VecOAY3nP16qzS7BmKSL3W5TyI2iBgVUo156556NiQy6iFjVl7Foh5hNEAaF5dNFXMSVML/4qOGZ1cOzPEi87UnTEcNsEG5xD4n6aU96DJeO1f5BQ8XlkwTfJ2z7JO7ZkAZAJw8SDs40r+wKQBla4EXRxkrD7AEzCG6Xu+DilYVX81s/DMr4Pif/JT7ZOkfRj4ZkPXhWGdS1Kb1kJl4hVxmcKuqhYIqI+eMR0HFy3hMrP4JTeDBJDPf2AkLWewg3eXrYAUAGXhmd9B6gM8RjcGwjGkLtIk2/walrTSmXyj4HhvwAk85awiTJEzgRqGkNlcj9hd9fZmzeuWivHSge9yFB/PcqTqV12NFRpNWMXpu4f65e09WnTVu1LpYAAVxiOGPv3tlCEo7c08+jhMEVoegklSs+T+4soyRnzaSMmBiWsK4aYfh48CBhvKa/SctVWOnkZFTW5f+8OSs8zkbqdq9NambdrdryMzqSHDrQaXYkLUUjJ/8+deyupsPHcHLnYGSeX/r6uqakpZFu4Oa8ry4lEmUfoj/t2du3ejHx4+FLM+9fBpYALi5UUR0rlu/L9aR4sIyW6ljP50nowH9FGQFNcTQ74uzxJgjxG2L0m3YebEON2AGzwEjweKJJty/DJixQFSpy8uOVnpfnwldEkbWKsUm8ldMtm1ZzfPtzqqK8Gp5SscD+nVeZIDxXwtWTLLIt9X2Zmpqenns9Mv07u4uOdJDmhow7+B829rHkl024QO+aOP+cc6t8zVmDTmt9X+13AsJnnmsurdV/56G5r99cM2mqlF52s+I7hfS9+q5XT8CUG0xYtiAWzu3T+lWZ0j9m7uZnkXqkw48ybZv1SV1wY0GNDJaZgmBmDZsBaJVnhYXIRYdfw7eA4oYHC7WUphw24i7BlbUmir6cTkijc+XZ97admjh7hDtwvCwvjfARYF6Hwcr1bkKcZsbRiAeEBDIiYWaJeZBANmCl6NuXDiNw2y6fhkUcPGxuquQgqLyvJcEGXpqQQnORmJlJk1bi9BqOlu3eGUhLC6Lyrt0xeX7dVwcmYAZKEif/UiSPdnWe4ymlINMdVtDOnjwEOR99qscAeoJevyDFOKsiVeZUGSQsDveFSAxijlkTwR6lcOjX84OmTJ+GhftwTUOen9/V26lnPlaiz9+5u6ulqH35wX481ce3qIGC/lqZtltCTJidfnDl1jGb8sMbaYBq+29vTAb2nux1CMQCcORnxgL7Mkwy1gZfrwQM7h+5cnp6Zxth05OnNW2MnL4/s6rhWpYxwwugfQmTASMZJfe0YkS8FlAJROITXoak/pDWvxCom87oXgG2yDiyDzTBYx46FT3DA3LalAGzgmICpMNtjEYewXVdMpV9A1f4BYgOdeTStcepmcYKe+gWt9QI8WdL/j6zZtLaz6ILJ/7AbM5CD1Im1f7BQpMTsg8vSWlam1K/K2P1RWtPHhKNY/FVEzrehaT/A1Q6KWR0QssbHx0bo6OS+wTmp5tOi7vffAAwjev31n8CbojIHKmQSRqhwg9RvvTzil6Co1dHqr2EbQJJEO+T0Yno98esubF8EoDHv6IfwoZQpP+Jn8Q9e6+Vhj3RWPRnGQF8B/GCD/EX1tVvgF2eJyA3ErzQJD0AOfiCWoDi6lg+ob0HmFBjTrBXNp40yyY3WqtHEHdeqRC8FB9gJK1FxLB28fAFZEgDSrgxeopgTPQkN0Sk8AyMVqjXCxbdW6fHh8ANkOuixBOdsODhHKH0Nmd4wKsZq1SO5NjOYO8IaPL2sXUFuCvJgmxrrZqamNFXVbyUVtj8q1I3Rqc9MjUEEhdkqOPm2+vzRc7sQho2db22tSEdGIgQkc5CbGCwn4tuHSJ3bt4bEB/GEDAyDef/mjWvUfYFr06yK8dBDWfDfxhxf2JHvaufv6XhmS0hPlcIoDEOaIuIxUodWG3qiLCjExxml7QE0os0DVY7x9XJBBU66WGmJoTPM6bgxwLCyRC+AYXCG27PhDO3pMo2pgnDjcSsR/Xot8jOkduuGN5HyZnXI/w3RQZi8Y8W5TY3OPDunGS7W3AqZZbrb8580N6QkyfZ7bH/4vXyQGc11kY7F97Tdur2nn2r6l5rTJzSZs7qi0/l4aLFGDVWlB9BoPs31qIp8HNzYanj5qg0meNR0Ij4eYkci1MFJYR2szwdgZkqV/tD2giONqoJUhbubjZBnC2Mrz3l9tMIL82NGnMS25Qb6uIl47AKeTO41duO6pqRUb+w+GReFFWLObuu3hvhpjh61itcBky7O3wAJqMagobIzbZcvDegV9ZZvKqD/PWDCqEqvwRDJhUZwDi2NdWaX9O5iVR7gB25JAy1jm56eBgQInwjTZRKRA4z4MP2glAggLr0CNhqCAOaBSGJo6CZVysLd8fSI1Iq7fYUi4G5WxjQjJUwEstVFT/JyD8dExPq445UHGHyzjjj/FjAoHWY4rt2cTOpeUV6UlR5nqNIJW8r9RQDS0PGstCgHgsijbQf0yjyMRj/dXWdRvISe8476KtgRcPKJY4fgUgDcgvelBXJG5TRoIsX4BjzHmobMvuEtB2+ENFy233LuC1IsxOCH4nPvJVZ9xuYoXB1Stq8yk49CRpwOuhj1I25fhNAIpRExHNcVHZ3Rh3b0mLM2mzNL04lyi7NgFZWYz9q3HKBdRusKADMJlZ/Bf5PqPkmo+Dyx5lPo8ExM8VeRed8CpITNEuG/JV/BRcho/Si18ePkuk8y93wEgDDn8FLAD0RT5PBSonhxfAmc/CwghykgDopjpDJYaQ0qiWFpNRetAeua9bm0oh3kaLlHP4TzT972CZzPPMXxO3SVZqjPMccZEtXEJek7V4Smfy/xtAPshFxQQqF0cmRvG+aRBGe+xG86JP4nuKqAKuG6Ee0TuEq0d+tU/uEKp25flVj9KeB5H6mNiGGuGnIXgwI8MlKid7U0AJYwygQeON9ruHADw8vWMnX/+R4zcIJK71y/tgCUJ/jB0gw/DAWjo9bJfnCt2/ftadF7ldamRij9uCRngC5UxZ5rFg+ACrakhbLwDHeZ7MJsrjgn58aW1yJ4sKqhpdWck45eG7p1A9MpaUmRBnn+qTqDDI9e39lcP2tp9/EIUsE94Du9evnluXML7yduQZ9RqZJ8hTCVeAk3nu8jxSDo7ihws41TSh72ELV9ZCSO9LakRPoiqjQjkEj5JgCPhXz7yADX3QUBHu6svjx8a/BqltawgeMSZgeIyzDZBbBqS5IEsFxDprS3WmkKgPVVK7urFKjH2KUtKpN7OyEMo3wZmO/0FgjO9XVZCMOuX7uCa4hwzCAfp9NbCCMR8F5WuBDzdfDqq2rJjO7RDHw0Dx6ZQbBao0NNRDnvI81zK1Po0xOk4ovLPyTpin9FUhcv72l+v+0Pv6tPQ9mJXf9O89DKuv8X1zQ9/1mrf5hkxY5U2LD7by0tThs7oLvJxucS2aOYreuvyY5vttHY1911fUacDJCVNnmVv7MyozwveldVFqEdEl5i3gHU7dheAM80bE5Jjw0A9AUQDsZWX0+nrAT5rqrMgw0F++pyDYmLFYWxYgG7uiMWOw10tb88sN9whgB4EEOKeu1E7vYBHk6PSoo0lqkeI6EFBnpc1p2YeEYXJg1teXVf1Ngoas5CfI/8OoAE1AU7OT7M+MQ5dPPe3dvwFjDa0rwT7aY05WfzXnIROEH8xJW+qK3azK0AefpkHMIjeLv79+5wF0rhxKhANtb9U00nQEEIkLDKjj4P15PPsz0aG6EpLmZUs1SzjGKLip/k56b7eRAOiXDj6MijtoN7xNoCM7gm6C/EFTWBjxAW4mPUMsGw8AxnYv2lzZcv4QICxDLUofb1ckmKUxpFm3AL6eyhnB0hzopSyrPTEmtrSpuaKuoaCr3cHU3iNN7GjF0m1OQ7FwH2CAhaGxC8FmJuS9lu7R/QLBMLGBjxPThCSsOqnLal+ceXMMy0j7P3LzMngTgfA7FFpFTp6Idxm7+IzP8mWv11DJM4CopeHRS1Whb6K5FfZwQVsfbMS2hPPbUohJhVpcY8euLldSGpHi+RPaAOHx9bX7/1cDT/4LWAMWSKNcqUH5SpP4RnEeFEeN+ErZ8DqIPTSK7/JK2ZCMdn7GalNfKOLSG0RkZZUU3RVLcRMXodjDSfCWxnpTLmXQIHu2cfWpbWtDJl+8epOz5OqPgMTpjIVLazEh3a73HRrHsA7dS63s85tDT7wDK4VaIKv4GLEBSzGi4RmsjphEyYP+AZAGwSsX1g2K+xJV/C/QDQEd6Lnj8HypJHgLtwTMBvrIe1mwMtKtOzjy9R51y5MjA7nWJOyV0R6FlalFOsympsqO7t7oCx6+6dIbqEhNtcutgPI8yVwYswDM7bQ4zLCoPzfDj8wKrdt2xWcdhchXpjBaUc6znCA/aj1oInT7TR548fPURTi4OXL0xMTMCoQjNIT41B05GRRxRGmrIPNtMo6zsvK8nyomJ6ntu3Veq91NVxxvzQCt/s2OhjvaVVFoEIHZ48HplpanobjET1w+xMXxFReAqRiREzI/GE57J+c3bEeD9bGPawp+XW6foQP76IZ+fv7fbo0RylHwAyYTM4rELqHB/EQziUGMsahesJIJNSK4nTWRPJLoBYnRUKvaqwWWZi1cryJC/AexlhguZcP0JQrCVJKnxTb7ETXbmgyVi6fPDo4TDyDP29eeZ/BbjsSwoBhBsPFsngXQbqwppyfL2YyR2XhudNIpu+FaEZWMHCHstFv41M1aOam/46TyboNyTWOfrCxiPbNec/mAXAIIy/Ha15cV3ze2+/Bxg283JkZoqJTV8O6zQGO/6CYZFa0yYukCoskk/7U+vyaXcSiIE3uW/+rUWKi3DPXfyK5SW+uDnHxi8faC58oXN/flD8Jq8tnYZhHAyTiw7W5x9gCITNWzMCfdyEbsSpOTnSd0dZKia1dldnAwBLjvKFUQNeRSJiYoR0Z2Um0fAwljpr26GqUsd7CjYiHRHgwdbctOeHDhHrUmNkhjPx0egh5sqzqVXKNEct8uyjllytO7fjMxXlRdyiJiPfEjN2Dz+4v7NpG1dI4/TJIyhbhOrnetkbiFdgOvcRO3Pts7m9pWnbnGcLYQGugWWmxY48eshNBEWFBZw6ccQUi298fAxiHUz0CRixNTLWezjBnAf/RbVrGNP5fFtAsyfjo1N8RQJG+dpH6HA4JkxXiWds7pzet2+g/fSlSwMd7aekTMgSExFIl9J7utpjOPSYIH9RblYixHYQPMH5wzUxxQxUyiVc+enr166cOHa4olxNOVTmE1wInyAk9eK7RIT4qwvTmlsq9h2q3ne0/HB72fHerW19hW39eUcGU/ddCm26KFDEr/bQGnlhdgsjWvJxir8CaGQq0McMmIW6f6xKRMeizL3LAX7Eln4Zrfo6Iuc7CKMViT/5B61DDzGpn42v/3r0dIZY3CgCnI+kYfsHAIEIxPWyo6w2WkkldCQdszRc3AXngMhKIrZDqUY4Qwj6iZex2B5eIqVZ7uSyExgAh2UU2Cm6YAQ5tGVa2k6oeloRRZZZJ0QDOntycKkNwFr/oLVBUb+Epv0Qlv59aDqhFwJcjFJ9A6gDa+TCMr8HJAn/jd30JfE6e51Uw6Lu9+EE4GwZYiG5dHCqfoHrZMo1gJfgJfJVqr9O37lSxy3s0gmQsBRNXfZvEaAy2Dih4nM4f/hc8ohf0PfZY7bIJGM4Ri4sbBAS9zPcJEk1n+YcXlZwajHee7QDHgOATW6qTV/CKcE35YH+0QDJtFkyiceG9MzgzSV5J48fhhHJKIfZVIexCwaQlISw2qrNVEUQxkAks6Eqo7ogA8aZo237Tx5va95Re/zoQfj72JED5/u64fdrSlaRuqizsuPWkPooG9Cowi3V9TWs/qJiSzKpO5fKmJYUyY2M4eS5o7qJVFgLPYH8nGSr9JlgnMQ0FAyDVtlGUWEVPVsUmHdo8bapnp0er3c0+JpYUoZMNDJ4WbNp81vRqW+NVCAjkWJLVBlxd7XZX5NN9TlGz+063VKEQiN66NrUlI2uDEQYiYFDEpEj9ZvhqmWwyDbLt89E3Rdmt7pN0BEBnuVHewjciEyii7PNlmRJP1NdFilzRRpkLuMEgA2mS3prHW07gGlMOqcPmMi7Ii8G7xn4LPlRIixFa833l3mxCTfo6GdjZdLpmeZeOsE87RzMczd5/mHiDanuOL3/1brA++VDkvY4//4sAHbVWfNA9XulIP5OYdj0c50dwdQjgqHZ9NT/QWC6Ve1+nk7t4+VDaxJx2pxV/3KLiriedWuuiy29Xydva/r+h45y+QZzYmOjj7VrhMSguUodd3h74eEdhWW5UTzndWLiGGYPQEsi3KgIEChlQpmPK9aSMW7OtjD65CYFH2LSZSbkPQgwiwrxFGjriV3cbHpSEwFumRjEVTMqVXaAJ59HFJwDxc7D2VmaB3Ovqt69M4SZGSpcAaEDCwOCJIbyx+YbLXaHPnTrhtElT262Jys9LjFWSefvucfJ6enEWAV7/KGbz54+1SNURIfLGrZVQITRcfbkgwf3ujvP7GltVOWnIf0GYBUgrgSpoCcp/kZ66khO1oPsrP1RoYWBkgx/sRdMaf7ip3l5cJHhD3cixWGzJ0qpTwHl9i1bNINEHHbyxYst5WoarunFExAMcREjzoJwqvHRwTmZCRDVlZXkV1eUNtRVwIwIHcKyvbubILyAyam1peHg/lYI+0zWcblvQAyA4MHXg58YH5SZGZ6SrMgrjKhvTd9/VnXiQumxy7l7LymaLvGr+n/cfG4F170KoVFq48dEdZCBBEExjK46w8FL2b6qqNuiZIsl3LnsQ8vCMr6HKJlAF46YhBAlBB0d2f86sbw1d3vn4LifFwRgqDtJHsyDyXGxVEytdRVAIEAUcFbwXvKo1YAJE7Z+Hr/lC3gElEhSVXsJzzDn0FKAoxDuk4TV8SXwmH1wGRwza9/yjF0r0lpWwh8oGQ//TWFYc3AZAZxEAdTM/RawijLlx+CYn+WRv/gFrCdwy9sW0B0K9+uQG5Nnw0tB/6tFJpzEkcssrOjjY5PW+LGqc9Hrg2Fxm76c9dbOrIKl7gydyUgCaAo+KQDFjN0f4WVnFfMZoikXmHH5kyiZCBcTbjnYHdCdn2ydBHmM9E2Zt0BzNv/AdYBUYcvMPR+h4iL1sEbNFcClKQ2rAOORm83Dnkrho1udUWsseMnHwzXAWyDVClcsYIe3k/uLYLgrUWe3Hdp7+OCe0yePwhgF2CxQa1uMRMo5pR0No1Jc24qNlOtxL+nwDr2D48QIjVKmuQm0a1cHMR0RrvBFNKXOT6fDmik9Emp6xgbQ53stpdo8f05TeUH+ookJK5yOkBkBl06Pw0ml/810PakSmCOwFg6mzub66rF9e42vdb5mcY4nebkxPu4Cvl1wgJhWCwPKgpgBes++ssd9O2lh2N6qLL7relPWMkY5nFhKZyjBdef2LSqWSNQUGXEOU5xD6D1VrDeXIWXxcHGgt2gjNU0GaAQw7NimIHgSl0KaOXUHDP2Vpd5QkXpKcjFV1vXixQtMYgv59oESR0zNnS4PlkkcBVpljpyMeKtDupEds/yQ0HXp0tevhHmorDccynIiIqBBkkP7D7ozaf/nmiv2mpF6zT+x9off4We6LtR9r/PIHV1zY/e99INmymLy+sSArojLvATi/NrEBZI6Y+/1P9c8OfmGIO7MDGW4CdxsEsN9SD3Y9oLtZakh/u6ItbB4TMSzg+7BpFYwDwbArGFzSlujCg3BjNSPNRTA0VKi/Wg9MZ9vuyMsaJKBWybHcXXRtbQUEUO0g+0TpcKXu+au0aI0ADouU3qGJbW/hqCOMlgOzq4Q276tkgvAYiICsSi8t6eDsnEsKQygbstYSAZBDJedqFdzNas+mxDi7beFBr4oIMlDwjBUMV1NCr1mCgsfZGXOkCXJ4t6kOMyYycXOT/NyjBQJwDP19ZqeXs1Ttsj7uHZp1rDqgPIkAU1x7aR1hipiJzhVmJAglooK9U9JCINwTe4nNKkXr50vmYSVI+wu9XIKlLnkqOVl9WGNR+OPDmbsvhBc2+W6tfvnree/KO75gNWNaJ8DSqU2rZQp1kTkfKtTSmAeF0Y3r3MRRMzeEjshF1cwJT1SXxt5+C+KpB/D0r8HuBKe9V38FkLYAwATmvZDSv0nC4Uu8k8uCYpaDbABLh3gH0XiT5F53ybXE85b/qnFrFkWx56YEibVVHWjY3bXSXGQvbSGyO9za6i4eh6YCILN4O1yjxDxCUB38O4MeFsZv/VzgG2R+d/AScK5EQNo//VMzo0UViHvETOETDmfgw5GCja4b3ACTKLueb3aG0hHhDNEVOkvXweQFR6J6xeTREUUzUIy4Qb/oLXBsT+HJJAUFimWO8nWyOFV1Uufqqg+Ctyu7QSVAcqF2wDuB7ga3l526C2GwIxF7I6O3p52cDL6hFgtCRMBHqA7gNOBob/CxSTO0caIi9kZsf0XOmA8eT7x7MH9uxf6z22rroBBHuABhH0KuVeAD5+y+Baqw2gJYwLNCSBai4sKwqId8+3K4KWLF85DvN7T3Y4KPWWl+to8nR2n6ZE7OTDsyZNxwHuGaQeKzTCQnZh4RuvHTGVdhoZucu2h8TT0nDbGx8cGL1+4cf2qXqKPK/VUmGsF+wtODM8/Quk3NaVLvsFb+HAgdGiw94ljh/QY4DDGclWgYNKhM4Ubz+bCkcOarRVvRZyjVilzcbNBAzd2nXlyEj4gBA/+EpfBo9WPeluocXN5TiTqc+zf22LhRaO1WEq5hCujRYENinNEBrh2VoaYyXcVx4nl3k71mVJDDY+WXD93JusFGMxHtBEA2PnaUMBmEiELw04cO8w9JeqXQIExXWatr91igozKLhMDDNuSRKyoB7aFVad5UytquAGsowfPvCAmtGf/GUf34j8SDcOnHdNPz71SmAhhdvufEsuo0b2Wnsl42ywj5s5/rbn4DTHy/SfZfo8wDL5g+u1e95jPEYaUWiT2oxX6hGMHdcj+hmThP9fIDh3S6/yXmtsRb5iXyIT4dptyIqgyR21JYkSQB4AoAGMCgF4MDIP5NUYpqVTFoVCHaSexwvpNycoAgVhbngRzQ7xUMMXw3+Yayouaw4Opm/OuiBDNrVtzclpQfZ5al8IMR/XoDR20LOGKIEyKjZRzSTgTExNnTx/fVJzXdmgvzNx0nr5+bRC3Bzhxz6zqLs0sIbcQTpjO8XXVZcEBYnOghZHQOBkXRQCY8dprFYPKSLnX9jC5K8+Gx7OtVgbqpx+3bdP09GgePtRwKJfHjh2iTMvUxAgzOtcQMRB7tFB/PYhoUdemvFBxzktkHxS9On7LV3n7P88/9Lnq+KdlvV9s6lvOIi6tMruV6ufvI2/QOL2w/QPUD8TKLhaEoJ4EK6GxGKlipuiIEIgHBK+FD+IjtYUgHgL62NIvIbInMbQ2Z6InQjinfbAV/QyKRixOrPyMIbYthSOz6GhOjLrgYu7tOiDH6mp0zdILwe8CrgygtexDJOeWvnNl6o5V0NN3rgDYRuTvt3wO2FUW+isgn8CwX+ETWaQV+QqsTr1yvoIzi8nj6cU5h5ZltK5IrPqMVNlFrQawjTV16PYGHTAPwzVdD5gNYFVUwTfJDLpGDUkjN2o7i8ooZsttWwrvAp8ddowv/wLuH7/AdYDN4EfhbucM95JJ620tuoNHOEjWPuIcHZb+g0y5hhAXGRALMWKgH7+pPeT8/W1PJ2eRCHCkglFxbPTx/Xt3IdDv6Wqni00wduEaCoTO5ZsKSGWmlO8ncZNJ3SHYBYgFMAb+Nr+kYrTDSAJgzKhz4PT0FBW452bS4G89NXkqcghjJoy69HmqiBuu8OUMxVfoecIwDs8c3LdrTnGO1p3bDWkOcNj05KgSdXaxKisrPQ4rx+BawSh9tE0XUzZpVRzh9KzydwLwCdcZ3U3ok+fP9czKK/qLzvd1v3j+nKsiCzhTzyYUs2q4TpebFjfa0vwWUmFq9f2sDKnIAc4BPhedB7HIje+6PjdRTjUSh7ub77bvCJMJcakXVzPnbHAonGelns6Gkh4NdRXMFdgg9XBoKw40mgrrqVKSfFeJXCLayHO19fd0PFUWTLdEpiJ6drk6k8WOekbGo782rD5LKuCxGolYGDY5+QL6y5eTkdpIo76O1SxIigvV1kTkGqWwYq0jwMVkBZ94mlUpciNFPh4szIPPOKdgiT73r3/ZrCQYADCrCF9zLBict1RFA+Jzmj1D3tndVKvUFN/BsD+GNjWiGVzLmm5ZK9TBgvXnOmbj7WgrdnxYoU1Y/aXVKjEW4cMwTir5z2fuF7yBy3n/3h1KaxG42UQrvA5vLySCHIxSIgCqvOTg0EBhVkJgaoy/UiZoLE8jaS6yTb4pn7GjTerirHBvDwfqbcLj2YR4utzOTGf0IeYmNswUquJ83FGrA4b1J7tb5/wgMFniChkqNQOKoMUG8OSzp1Z7cdKcjyXqwwCiUMcPJu8rg3M7xgCcw4HY28OR6+oG8QcEBFSKECIhzPKxkYGn80l1nmlKJwNx4QqXl2uamxtT4uCyC9ztL6UkaYpKNGXkSc2JE5pr1zSzLX0gsqHuBTCjQFRhHknCh4X4Bi6LGeM1fV0N5M45OzDoxQagV0LlZ2lNK6kbL5XXWzARCy1CQNBFQRE8Qyyw6j8BVJC55yNCyTuwDB7zTyyhsI2o8JktDMs7viRr73J4pKgDiZFvEv+g6MXr4+8tpKBIuy7nRjNss7Jt3YCFPlCm/JC5+6OiV0uFzULUlpfbaXEOuVu0sBkAYRap/VsVlvE98bBmqunYFJYTSzqFG1se8Utk/jeJ1Z9aIt6ohaxUaZOAQAaYrQRYRZQVLaHFdiyi2jDE8+Doh2Hp3xMkxjB7vb1t4YSTNq3dfzn8xpODky8nRh493FFfBfhk1rrfyCNu8urM6WNcjfvbQzcHL1+A6J/afwGEGx6+DyDhwL5dNZWbAF9FhQUgC27ObkjNmnr50pDgTfvli7M0SNKSI2najYvQKP4pzEszpPP5iJ1R9YHmTwDSmKJKonkXXAcuBDLTuSXH2enxNG1lVUXZgwf3MDMJMI/yU7jEb5mvgOqpUAUUmNcMraVpaZ/Y3T4kQHwpM40I4b7pqrDilohgV7f13FSYhoivkOvPd1m/uzJznFMYdrJJjdUKMI9Y4q8wOjpCZ8OUhDBDVI8zPt+V6NT314bqpb8G6sIOFsn2Fgacq1GeKg/2FRO46O5mp4r16K8Lxe33qQL6akJ7q5THNweVxHu2FvifqwntqVKc2BwU7uciZEq2cjIT6D0M3xdgKpn2nqHUSnpLRCj9DEmqVAAT3r0hy3ewIbwy1RswIZXUgl+rFZHKi+tEgZArffFW8k5Pz+gcnkj/Z6Qg7XU64r6DYVa26fGZZxcW8oCTtzXjr6DjObqL5FiRrno3hQAzC9vgOq2n8/9YeCQ2M8X9Oc30/GfNy0dv4MvhKgWL+HbqjNC2HYXo2nxAK1t/pFG1pya7eWu6UXsxRtiD2bIhv2lLenYCWWoSaa2oeDzbGB/325kZVkwMRcVHYyJc3NZ7Mbs3RSo09+ZYjGltacBqJazmYkqcQ+nkbWhEM2drP3MCmSEKudec1j3UnhL6ntZGM6kk2vbtadGO3eWGr0IYdOP61YfDD2Cqphipjhnlp2/f1rS1kYxWbZ1mW72mYbumsYlArH37Jrq7ng8NPb49ND7ysKwkD7Nn7XVV0/fuzcyeDCCounHz+tEjByACoJqESXFKU4zK+/fu3rxx7cSxQzDBGNUwNMI2dNuIhD0vD3spQK+oXyKyv0tr/JhYA1PWVueihXKdIsikS8vE62X0No6wZDkAXURBIfdbRdKPgeG/khVTNweIqokhLyNQ4e1lFxCyFt14LY31FxA0vusMl49A8Ve4pAi5iUMXwOMeY45q8zigFu3knViS07YU7iiAW2GZ38uUa3xl69F3W+jghBWA8vBfCk4tttpUul1XYDa/s8VsW0jCTyIXR9S2wd+dj4+NPPKX2CSPQF/CfPMRuT1+rFsgh1C+MCeD/mDh160NZ6evXb2ckhAeGerf3XkGENHZ08fbDu2FwLqmcnNXx5k7t2+NjT6GRzgC4BwYE04eb9u/dycVpzXsAVI+d3UfhjWKrAw7gCVuqgfCa0o2A5gEJ0ARC63I5Yatmdo6WyQ30gUyPWygF98ja1Eichg435uVHmf0xGRSd5o/rK1i1+ZevpykakOojG95o47VFKaiGCB9R+4B4YInxirSkiL7z/eYz+bx+bbxUsFkQcEbVatXqSby8yMkPCEz71MnOviaACICOCSSuQe3jvTupIzEzdnhPIaRqMfxg0s6OamPygCnwURJXWEM1Xd7utsxGQj46hCTCuus0JV77czzL0+WHCgK3KsK2FMYsLvAPy3UXcAjkvewS2aYsDHHtyReHBPoBtCru0rRw3iF9TFZMvijNN4T0B2+e/NsQ5oXz59TLwRqtFBfu8WovwK0e3dvI+tEwLOLC+IBPmwrkRNMqJXlgBnZEqdN+vvQDK7n6Mh/rHly6k2H9xMXNHfTdN5OHX+pGbQhJmPv2m8Lhl33JIy7obDf0LWZHNL0/hf2vrkltzgXN0o0QCn19vmlBT6rh1tmVTROXHgDVwJm09TECLqW5sG3S48N2F2d3VqdBbCK4jH0az4EneEi4h9tjSpGzj5/Z2VmXkqIIkDgI3YUuNkgF1HMgKhICe9eVoZ1i3Mq1XhebpQ3H8Z0xBK9JSq9HI5eo8VgVGuY+tnD5G1Gtt5Mo5wZU+Jas2kheXS10pJhlJlxeeguMm32oyEM8/ZwhAG6pIQj/GgA9mCqhhk9MNBTLvdK1xabNc8Wb3z48MGundsBW3KL+2W+AogJDE8bJr/OjtMAvaSezpa4JGMI6Mlz9hY7yEJswtJ/jN/yZdb+5YSwx2a9FihrRLXLu1ijYXiLjN0r0ppXkrKfvG9lijW0Dkfk7CjY6ORu7yzYQCQ0UNaP1ZBgKqxIMO3kmNr08SwxwzPv0NHrQVwdC49g4QbI2rc8KGa1tzdRaAzP+o6YpFV/SqA1c3ssyDnTSi3iyn1qce7RDxMqPw+O/ZksYDk5wlsz9/l8Dq7LqXYuMsmnNdaRQIvcXWXqD0IGEBJqE1ODR4vH4Fb3l6/b3R8yNH6K+HBqW3dHR4kqt+2gLma6oFWdNWuG7uwjdgYUhH61lFgRHS7zlbjmZiYCLipRZ3Oth2GsoyVVXGNDVFEiOzIIBxDX7aGbevxtSn7m6gaNPHqIiAXGJTTS1TBWkFiiBrvAjvDMmVPHKMnQlIYh9YYO9BU8fz4ByOHCwLmayk2ARWHUhTeFIXr/3hZAazmZCawkb0sDvVw4MMI7WqXRD++SnhyFAzv9yIA5aaERvOn4bFFKM6t78NG4l9SNZ7MlxI8UDL8xaqJaXRHijwKJ8L1T7PHg/l24gIRoE+I53N08rGUkwmN0sFjgZis3EDUBDAbfgj7K6mqnn85wOqaYXMizr0336a8jqS2EYYDB2kpIWZebiy2gnQNqGXRAZafKgoN8nARMDRg8AiRzd7ODHuDlyMjcK7nVYpWp3shINASN6GaGL1VtLcEnO86epAsQjzn5W7hpkUICv1B4LziNwYbwVKU7X1sSBjDbGgymIS5KNGI8v4h45L5RAHaeZMDa/0x3DgDAnnW/g16/SRhG3ZOHQn9Dl2d0jw7wDFtsez95S9P397q1h4U1LH85rBlYpftRvSY3dEN2xP27SFJnBTn4dn5eztBjlZLmrRkAxlqrs+ERENe20qS6ksTDOwqr1fHwanaCPEYpAfTl6+lEpDt4tmKOVxVgsEpFwERBvkVcRAPd2wPRoU5sQswmJ8BTc8mceOvdu7cxeaXKZwkqw8O6+VsR6Dk0dNPay3L3zhAai8mk7k/m0l/e09pI5/LHIxalMSktp7e7w+Qd8XIyNNibO8U2NdY9fDj8khmsAb89GR/v7m7fuWtHTnYSCnhQ7Xu2cELp27CtElBiUWEmxEay2awb+G9L0zZTni1th/YaU0tzVMp8oxRBJYV5B/a05qSneLixsvKBcqfUIre87Xa5B1epzi5heVOd8+GJqTu1UhOz9SEwGiZuwkc/TGtZiSrhgWG/Eml4BgQSrLXBmWjfQQzK20g8r4hm+uqIvG+jCokwerT666Raov4Xu+nLqIJvAkLW+HjbBkWvhqj6XYLrDXAUcw4vJRlRhpS4UIeFmwTgN3zvqERPkDbz7Uv9iOBh7pEPkfW6oExLHXxKaVgl9V2vTPlBZbFXNReDwQWJLvoqofKz9JaVaAeHJY46Ifuu9+es+mPA26K48i8AFqIUCvm1Ckn39VuP4iJYoLjtvEPH7eJRE2YqAAks8QOkUWZ97RaqFTE1NYXgRxtPT9ZWl9Hlm/JNBbDB3tYmbo1ZVnocDD7w/L27twFEGVaRwcEpFY3rh0tNcpVyCY3aKbODuj5SAhichtFU/80bVym1Us+vTMNIX8AsgPgH3gVdJQEx0nwdsjCQQD5iwbA/wzT8G0FdTEQgdxkO3hE+xe5dO6z1v6b2A7SKGJDYRH7+m2AnqtSPc7J9RY4iZvbv6+2kZ4WOAnyX9TXqOCpVP9K3c+BQBQBjiBYskaqHBgiH5iS5yhwsGB44B98vTH/eoo0ny4K4IOp8TagqxgNxDt/VLlnhfmFbGJoy7ykIANAFLzEwjHS+m63My/F0eTD3CH01yvggHgK2sGAfPWoM3LdU3KWhroJGDvRJWvYG9w9lnAJczIkQwpGrUr093FmXMLiTTblBmI4YH2gGVpIItuvfEVA0dyJhTPOsV6c9Pn+C2xOSvej633UArO//1gxvege6fsMwjFpjEXlD9Zt+95f3CbyZNiYj+7CCCN8jk/WexXp6EwNscRqR6/C2QufDIph3h0jT3I54w4sKnR2nuekODyYthtZhoYHCAG/XqGBxjJJNoUQGiwF3ubvaCBgBD6LeoUVfZIhxtwPgJBU6EDEJlPKbF8nhcW52FHFztkXmQF9+tmbS5PDx4vlzhCuRof6U0sC1BC0ryZ/HZWlprGORUk+H+S3hBGhS0ZLyMGT6YQRAoaOR4W562tAkGvYKU/jGx4REh8sUWstR2nMy4k8cO4zVDmZK5wF9dXed5cJLiIFgzti/t6WsND8tORKr1+AbT0kI276t6tD+ve1nTvV2d924fmVqSnfPX751KjLe1cfHJqbk68KTK4t7PkDoZXk8ymV/YfQJTxIj4NYV8JhY9VlCxeeAnQirMPEnAF2y0F99A9ZJxPaY5hIwllZkwVtsHxj+a2T+N7Gbv4zf8nnytk/Sd67MbVuK2RI9uT+tee57+Uxa44+gyOp3kAfrXJR9cJnE007K1AcCDF4o3IsKlsRMmbdR4mHPCh66OKCLGsBsmXJNTPFX5oVSUOxEZb3SCezIFoZZz7CFd4zb/KW7nbOIqTTzEtkTv7WQNSEJxIMObnvmNl6Rf3IxEm5ZLRajGbN21oKM/HZ2rYAOfwCug2tuaI1d0vnxzsuep+9k3H+iL6R2vq8bYnoICsNDpP7ebrlZiQAYYLgASFNRrq6p3JSWFMmV5wmRiUuLcnY0VBkVeS/MS6NrYZcvDnAx2NYytWFREBeoaIj6/GWqwQgjEn2J4qvM1Bg2wnz5kqqZN22vwSepaiLAJMPwnUsOl3o63za7VHdAK/XBVRKnx4e3nph4ZhUMwwSOeT0kCxNrXR1nqOgxRWI8nq3Sy/VMQoymuPh1CyTu1nqF5WYmUtVH+CMlIRwCCZm367UTdVQjEfBYjSoWNRL1ZPeNtju3b+FECZORnmMBrlTizCvi26tjxURmg1MVBogrL0qECoRMILGhJt377FaS7zpXE3qkVF6T7gOIKCGYn6p0L4zxOFwc2D1bv76tONBXzLr2QYyhR5g8deIIvebU/G1igkXsFJzDSdJoBNBgRpigvxbwoRjwGF25pgof1n7/BFlNmpUxe3F15rr3zKU1xPgLMNuduFcAYE9JDH/uH2YDsM1WlPa8g2Fvp43t1ylpEmusN1hB+OSkpuuvNd1/PfXAhDHFcKnm7J+w5/a4yWL2YKVuL0Biv4umLsgwJJ558O1RIxHglpBJdsF4JORkvbjdnZgI24RL3NL8PK6kJRNlCJVq/gz1oqIb6SleArLKBWAsXMKbHhy0hBZITQ8BjXB9Syl3xfIGgIoqHc9Zfr13dxMrXLtzh4XHRz9KGKzNEFqeP58wJROv1yHQgbEe9Uhu3riaGKtA+pAi0FvuJw6Q8hVyL4ghqOgzTJPHjx7MTo/PTIuFzpXcCJQK8rJT9uxqvnnDyBLyy+mJu086z9zNqb9kv/ncioKTS/OOLzEM9czxCbt0enq5R5Zm7VsOcCuq4BuIPqEHhKzV1SsyuuGUVQhRtYcbqTfzk60LDP9FmfoDMQer+yT7wPLcIySNUNTLkVZHDmT73HJ/7zDSmykAyz+5RJnyI0AO3noX+PoWMCEG3yPgjbSmldkHlgECSar9NDLv29C0H/yD1opcHQDn+Pqv1wlyGANyGbs/AsDPcGgX67Kvli0oqOaFwXDHglOLAZR6S0ixIitk78C6z7FS/u7Exwx+F7GlX6bUr8pk5GFYli9rZTbr3dlMMtPn5GSq2hdvv+DU86DsyXNSiXr50gDadgGoAIzEzW5x2/VrV7bVlHNph2h4aIgoaJwaofSjNTMYUhuV5J6enuYehDuGx0UF4Usvnj9HngL0tkN7aUqEbnn65FF8MiEmhDp/cBXeaQRPC8xkvgIzOAo+L62JhTGWRttU25ZaVlqOnTA3Ur6p8BUn7qtXLnG/hRDOwhzMm0R2LzmOILHXRFBUqUdzsuViZ6G7ndTThQtlh4fve3uQIoX4UMmjnpbhboLBHva0DJ2pl0vdIK6A7ed0KB4bfUy5oDBRGtZpAzBDDBbm5wKoqWe2SD0RoM/zg1fFVC/KfUOAp+NeosZBVBNJDRjzB/s4W18Rdm/K8cVUGBpS693h1ChcInLgBhjHjx6iRs8wzxbmplJZjgh/F8SBMomTUFsSBp/RWqs9i4PVCp0oHfYrdvOCe881DwpnAbCOP9fcCiIZtrfVpsd/+3H1b0kp8aaMo2L5D5rJu2/ofWn94vl/MGn3fCdeJzIzYmkATazoqCbMm3L6eq3t4oXzHANE4tzFkNzM1gIJ7GHwFTB+Yq48mxRfUWuk8nl+HjpZTRUUPM/Lm8jLY3JiqvmJLzWHB/OYZTY+z/ZcqdpMhRgV39+lJe5TKS06hs7jstD1Tlp1ZqpByILKV7GRcgsls2jZ8ZlTx+ZKnd2Bz1W+qSA9OQrCHZj+A30FMMUCyoK/k+PDIMQZM6glgL3u3b0NQO7582cAz+AfPALkO9fXVV1RaphJkwgdUuKizvf1GEYk0zNTYy9uXRppOXwjYmvP9xZV+7CISyfrBwFi4RlCS0vftSKu/IuI3G8hFkfBbqGTo2CDs8CBZLdIgRmcjIe9RGxHDIJ9bFCPjiQH6j6BcDmnbSkEr7ocWrfVRTXv+tsqDIMvK3bTl8GxP2fu/mhhk5CodcEKHmoTngWnF6c1faxM/jFu85dqE28HG8dt/gJJrYDwAbCFpv5AyKsE3i/DnBXrlvYa7jFCsj2zOO/Yh1n7lqNfNry7v3ytl8geTobQLJnaRcK2xZ+GcIPU1yYw/Neowq+Taz+BXxN8RkwpW52Ixl06FuUcWhaXZ+vnRaRZczMTLczPABxqadqGZV0Qhhou58NxMEqGDsPUoQO7uaNN845aCLLNvwUck25PFQWvDF7CFUN407taWVeqa0+NdMfHRqk+R1KckuvNxWbyLw1wLb/M1OieOX2MbkkRF/fjmJLCN3PpUKrBksJj8+1o236uz+TQrRuNDdU0XSlytxfCu0SGjBUWmFPZfYVU2P6oUFeejaEIClas8V3WFyQFU6n6x307+/aXY4xhCSPxyuBFLtHDELTTVFiIj/OxTfLeqlk4qrtK0bE1JEyrc8jmCV1t05QCAGB6pmF6AAyAGWyjjhVjMg3uc8PVUhoe+Hvz9HikgDARWcGsreUi2gVKHA8XB16qD9+W6SPW0hHhHjC8OV+ZBfZQ8+T4LP8utGDuX64ZP2x1wu1Rrf6hLv+iedr+dkJVAISTQ5p76Zqe/0zso6Ynfstx9W9KsH6aMEdZBiCAovcXmMtnql34hLMGsMHkZkOhOpmXsQMWHRlA3aUftGnZ/655cWPuXZ62z1x1fTPCG/MgNqQkhNFxSp2TkpkYHiBx9RJshP8KGO14tFQGOOTOt+UxnlSE0OzlGucjkIoc1HKfpwDAipkMWFHRYFpymMQNXg3ydNkWGvgsP38+SEylellYEOPNxxMIFDs/PWtSbOPSxX48f1yCIrWzWnNPrfBU0XwWELTFAxFKP0OlYLx0L19O4tpqeIgUF3oNpZzMzzFJcaF4EItuvZcvAU0B6Lp18zoALaNnpdeevXjc3dmemRYr9xfp+/8wgvKBUvHxI223h25yI7AZzfTI86vnhqtaB6U1feuLO1ZYIFfIcUxu/yD/+JLMPR/Fln6pSPwxMOxXeeQvfoHrILhkiricoROHXPcN8CS8Gpr+PQToSbWfpDZ+DHtlH1qWy8jTsWv/ve9zo2HVO8T1R9tZYcA3QwRFql6XubeDDQD8oMAgZl9R6wLzrgGKNYCLEio+Iw4HRz5krOc4SoztC1Rshlr2Wt85QFbZB5Zn7V8OvwXAZvHlX4RnfxcUtdrHxxbOiqAydsHC0dvLzk+2Tha6JjhmNWyWuW85ixvNpIKZa1Lc9x58ovCs7wNC1kg87QDs0THBEg96LmcM5g6lXLJ71w7449AB1l9kaOgmDGtUAr5UnXP65FFDXcS9u82RULi69uEKX1xpgpEcnynITaHjFRVUhPENlfrgTHR+9LOVilj+QmsT3aCn21xMuW93M03iXRhgaZyACqjPh+WjN52tEEkClnvFuRspFXpK+vANUtNqAjz4tmF+wt7cTE1J6UIqKKrU47k5IZ4uQnfiKaqX2sLCOTfndRV50eP9bGHYeP/u7aVJPOd1XMFJk1Hj9BTFMAFSPlcSBhv1JyB4z80uIZhoD+oBqt5q5ZFS1ihMxGfFw8hqqdytNZ9I0hNV+mqSCuvWIreeKmV9pnRnnl9/bSgANgEDw6LDZXpAHe49qk0fFRZgCKXgp0E3EPBIvu5kWfDF+rC9hQESppgN7yiuac0CxHITl4gVEwm2/4SDmtYQMuGLq9bH7WU6XToSIf8LkgF7cvztxKmje4mceO//pen8K10W5LftS/bb8w171qW7M+7nWAfH5/m17ZrlLD7SYBqJKXQ5sWe9Fh38abum4y+0wHLxHA53gNO6/z3rKQ5n9RtrEM3TyuygIMn01NTo3j0PVYVHY8JF7nYpvqIgsYu/h2OezEsV6A3/rVD458skl1OSnublTRcUjOZks8bBjGjStEqtDvR2dlsPozN0J9d1JUHSmfmN/kXF9aGBTq5Eq8ONZ1MQKtOYMAGDMRqTUTBcwoAIWEXPBOb40YPzuzjbt1WyOavTx4zCMMoXV+WzhRD797ZYcuThYTaBxnApBxf8a4VpA+bjpl2bYpP5Yt5GqmcIkRwWz8SFKw7sa+nsPDk2plvJezE1dnv89OnbOXXn7dXtSy3Jb2BUWnBqcfquFSn1q2JKvmK4hUT9At5X6MgWcZHqFw97b4ltcMzPMcVfxW7+Mq1pJcSCROmbk9pisRZ1cH6X5nrXX78VGwCe2E1fAuKSKddIfW08Gb1BUmDmQAQ24R4WaxVflMk/Rqu+Tqj8LP/4EjQE15FgF8jJAH2f9QzBUZEfzjOq4Bs4T3nEL94SO1oCR6zMHB29RPZwhhG536Y1rySg8cwHXLawigFg8GGT6z6JUn0Nv0S0pYYfJte5GKYDywcZABLclR1/b7fmxjoASCEysZ57GIzARlkVgNYAjBliPxjFI5R+XB7j9WtXJid1qkVUv/7I4X2UUS/1dMG8RFGBTpHf6LpYsSqLOi9TyX6jraZyE0VcqGU3NvpYJnXXK0WzvFGy5ZxVx3OEVM+eclmIXHmMK4MXs7Ty/Z7MWqq3p0tVTNj4AqbFioobw4IwFQbfuN654WwocLM90ajCbBgRS+xujlN6CdzILoaFXvrpyou6dGVuZqLeq9xsJ1WrP745iKjVVyo6ZyMxgFW5kSIAQu5udh7uiIsIiycnUtSY4wt7AVQD9IXUxIoUCWwm5NunhwpC/VwQvAGg0ssSwwnQeyA1MULv9B6PPKLUWQ++PQQwgL4Ag53ZEhzu70KJjjTHuzANQtyufzMrbdX9t0RuYD7RwxPNrWDOof5Ec+knoozwFrIEk5rxo5qrjrM+F7Ii72W/IyVa365s0BaJ/ekbUqEYCud42/21OT2Zy2u0YvT/yVIx+rF9JM+rQ2ImK3ymrnho2mlezv4NfG4YI2BqsaRumMvoI6SLbYzgz7HjmpKS43GRfL7tjrCgwbTk8bxcArSQYaguIn/g39wCMJVqLD8PtufzWGkNnAAiJbyp+TmZqFRP8/NyArzgNDyZVb2re1tNQQ5UlYCeGKvc2VxPPVggSqjcUjwPE2dsA+d7sUw8MzXGPF2H6upy/T3NJyFp9cKrFwlgu3v39oWBcxf6+44fPQQXBCIMwmsCDCYkAEzs5hgY4KbaFF5WFb//xOabD7rvjfZff3S07371ydsp+67KGi+4l3d/a3meIbdtaUoDWa1XpvzoG7Be7E506shS/Qbyvj4+tmSpXkEkBwCboVcyyW51LirqfY/V5Oh8l9p6138DOTpEO0wWF9AOUbloXZFU82l41nfBMasBm/kFrGcJtI6oxknEPwDzKBJ/Sqz6LLl+FQCknMPLiA4khytL6t/aF5jVSTVIAWgl1X0SmkYgmV/gOoIeBRvIqscGZzhPidgezhx+nvAjJVixl7hFw0kGRa3GVRgu+lIEesJYVKrOoYtNMKLCDAKh8IP7d8fHx0zxpihEMdPjo0Nwraqz4/SWzYURSiMmY74S15zMBAhn66rLCvPSIKynhVs6HmNj3ZMn44j6UP9genoKhnruNtQOmFpHAnYyrCmC41Cp8cRYhZmBHXAX1ZGnhWGUkUjyY1YyEumEy2SQXmn1rf98D/3gOxqq9Mjw8KG4FwdmZGe39fnh8sG8HHb6fjWR+tsZ6d5MVgfgkF6q6uHwgwApHzBYXKgE0Re6Np9qVov4dmJ3YshpPjiB2w+tn7G3nzmhtwEVvYDLiCAcsFNhjMdAXRgVrOeSDPvrQuHJnAiRRLgROYpwGgCHAGVJPRy8RRtTle5pSneAanA0LIOHV1Gcw6jD553bt+gqql5SF+5MKk4Dx4deluQFJ9axNSQphE8V6uGHMG6tOqLJYOIl0SmYBcD+leZuAmEnWtte3NBc2ajp/TsdlfGq09uhIE7e1ozu1lz4fNbnav8zzYVPNaOtv01m2R8DDIP2qIb5av9i4X23TN2dXIe7/iWaGROW7TNTmsG17Ga9/81SI7wHxRzbhLVkCcFoe3KSMB5pwu01Z3UvXxoIDhDDaAITjCWktQytzZRM6s4K746OTagKw7zcAES58mzipYIneXlzl/mq1XkyLzeejcAdxi/7YE8XMdFOtFd4uTwnu88rIaYuGs7O8BEytvd8u4wQv6lnz4xCGqrSjpXKtIIc5gNDKxKrckoYN3h7ONJSBKMN5hUsuJL5CowKcxm23bt20JN8+mT+JaeTk5Od7aeI143YyXghH49koiILvlUd+3xr33dbz31ddm5VUc8H86uqwoKWxOrPINQjYZ8Da8kF7+Lrv14e+UtUwTcQxeafXIIiclzx+neSGO/6b1xVn61ppAzbrkXoFQbwLHv/8oSKzwF6BYb/4iO1IZJFjk6Y7IX7H34OcP8T5ZjNXwDgydq/PK15JVZhvb5aO8R78EMDBEjERas/VST9SH6YjC0ePMLZwskn1X4aGPYrZsK5KSYY8wFK6U0TI48e5mYlUhVBGNCiw2UQid4zGABhaIUQOT87OS4qyJS7oL83Lzcz8e6dIbrX8aMH05OjDMtTzffdO3c0a9VrlUESGPOvXb3M3QDOAVN5hw600hDZKPGPS1k8uG+XxrQx1y6tKj30/Jxk3AwmVhpJz6OwB9VKiPa9CbMQCxuFIjDsm9JTOdfXRUuYcFVUJNyoUvg/JnL2xa9i2ZwT4Im1CYZXeFNxLlk2dVlfVRj7pH83psIe9bZkxcn4ruuNohr9Je6xUVrhBjO7npkBfCh6s3WcPYlCkfBj9BJuaMnzO18b2l1lpOKrp4qUex0sCgyROvNcdRDLQwu6oAsZPQ8ATvCkj2ijrujOQEizlXNjUKoqMkJpwlbAsw/ydtrHKIKc3RKs9HURai3I4Ns/uL91YQK+51d0WQRMctwQTj3pmQ/sGQojNVc6KYd/1Dw9+xZgwvhh4kLW/TezANj59zTXPTTPejR/PO0Pv91Tu5v8pt2+7yTovkszEGhqVNP3/+is6CwFPat1B79fYHKzxy3k58F6jn30Wj8uXb0zb0vFofO56BlTkmlp795EqdCdSUO5uK2vVPjPMWqri66lpYiZEjLY8WJ6aiOTFoMjBImdn2Aybb7kh9IgKUriigX2u7QGHXowjFvPXV1RSlN8hH59cT7JdDgm0sEpL7Gmcg46O+Ul7mlttOQtIKyhk83FC+etOj0Y7vOykraWqbfVbOFCUKPGyl4i+/SWFUV975mKCAleOmNp8JfTtjSx6jPoSXWfKFN+gPAuMu+blIZV6S0r844vYWlaXUT7e94icu/6u/7bwmazK7jg7/yTiwFoJdeviir4Jizje3nEL57CDaycxgaCykiE57YxJOEnYiT9mlcfVFr0iIm47IPLotVf+/jYAO4CZOgvX8vqLnKyRhkp0RA+Gl2PvzJ4CWAYRTK6nJWXS1Z6XNXWktad2x89HNZLv8Awm5oYATGofgGqli4IMAmQw4A2fQS7QwRPRQjN95iIQAhb6X8B/8D4DDAGNUK4OZPTJ49yDRINyW8vnj+nnMlAX4EZLARnyBWETIoLZSaFKSwDhl6ing8tSs0QJuV+wldZehvimLylJISZSejBS9wqO0bO3ibMm384JoIQW2Bat3ZqVhfdy8wQM4eCC6jHNJl6+TKS0d3lu6xvq88bO9eKMOx+Z2O4XCR0IxHFnAVRXAorzHHcz/Ly5SRlvgDC1HDq9JCAkx8l6qsh5mCm5DeObZKnKt3FTAINEBdAI+gezB+AxNxcbIN9nHfl+2eFC5E9CPee4SVNS2ZlEuEOgTsKn7/Q30fvZ9y3Ict3oC6svzZ0S5IXzYNB72xfuBj48s+z4Modon0yNTlqXTnPk5Oa3v/CSab9DRESN5W0eI0ZsCHG4+pPZvMP/4XmTiKpVftja3/QvGu0vbxHElysxLyX2XvxNFvx1fG/WlrE9ahWB976/ru5xQNAYug51vlXFtntzTcxwg3KYTbVM7vQa1yppSuDF1km3sC5FnVOmJeb0N0OBZdkHk7DWZkmmQwq1bRKlebnAbgL8FJbTPiMuijUyw0wmI/I4Xxy/CtRINRFg6lJiMEEJCEmHbs9ZPhBhh/cp9N/fk7ykcP7dNpZvV3zhWEkzhgfH0N9YYgkzDtBA+jFd0yICbHkLSYmJiiDvMEYvNRrz549Pd/XDQgTQiijsQ5SgIoKM2urNqOlNdooRRd9VdT73kKFpHlHP8w79iG3JEbN1Yh/F7W/6/80KspoTSMme7OYXFl08VeAyoiihretp/sGd3vngJA1hWcWv0nyLZxVSf8/wq8eS7/EvI1iRownQulXvqmwq+MM1jiZb7eHbm6r2bK5JI+iDm73k7jBqAiQDEbaoVs3KOMAhs3r1wZ3NFSp8tMUgZ6UHM7tm4rzTp04AiDwyROAgY9PnmgDXLepOBfgRHJ8GATZuZmJANgAUFE3MABvMIxz9fE1DPeee/ye7nZq8EVVQAz1GGEzCmBajKl3cHN9KGmIy3nIPYNZEkkH8AzMnvOYWQC8EZZ7WuyrmIYdO3KAfsyjbXNLiwHihW+Ezhowk7rz7XIDJcfjooigsVVgTK2uUgRgpYBhXgvNA0R8O3+Jy+Uj1egYNsJoJHozrs0AYMyLCcPXmhSno6R2nJ0lRt28o5YWIiI/BfA/LdMCJAb4JztCeHZLiCkkBs8DGGst8E9RuAPiivB3jQxwDZQ4Rcvc0kIFqhgPwGltJXI/TwfEdXoikMi6pPg/JSEcn7x29TK1lgFEFyvnHS4OhPcCGLa7wN+Lydchraa+doslP0BL2w2pNnn1P60wXtJFIec0d2Jpfc3M+aWae5maF9feQqA+/Uxz6btZAAwi6it2mqcdmj/O9ruEYTOEOji/dj9PJ4c4ajYX/KCQs6VlSGxsHyeN+/+Z8ymn1XF3El7TNUKhWG43o3w1PT1NV5IqtxTjrDA6OhIU4MHj2fp76BhurjybxrAgkwkxleppXq6M8Q+BjYcy0kqDpHAEHs+mPjSQKCi+YjWwuiibUCBsvBhMGBjoaUj359Yrw2RDTV3mkWgybDBzW1L39eLFC8TAFuolwvVHgDdniQJEIcWqLIrZDDtMAEUFGbRQga4merhtDAz7dYGFLpjkwLtA/F1/1w0l4Ckqyz++JGP3R8rUH6JUX5NUc/ubw2AFpxYrkn5EKVRciJF6Oew/UvXcsmpho8zD9OSoSI674CyvC5FDiEwMU0lN5WbuuPfi+fORkUcH9+3KSImmtVjcHuDDhxEVkN7jxyPcUZFLj4SxF72hIRynO6KbM0TkXCYklQekXeYr4B6ZhKzXr9L1KdgXZRVNEnfu3qYbSz2dkbFPbaDgU98zS1M3vhj7eATnprLS/FeZkujEnZUWa0ZtX6/B96iUS2haDJdNwyVu28OCJvLzLKIpqosupiQChBML7OX+IsNcYmlRDqbCKvNjxhlGIro2b8oK5zmvZ0DjHNiVaz+Qn5PMfen6tSuUeF9RruaAzNZZLqZudkHeTkdK5WZU6ftqCBgDtIYCiafKgtHWub82dGBbWEWKhM8k7gA1GZbwcZN1iOS3bFbRE+O52sYH8ZijERrk3sIAudYlDI42P+g+RwZp0JYUxVgroQGR6r0MTef/xgal8Md1j+nxs28rvp+euD5DA+mBFZqRbWYEF97BsLfUABaf+0fN+LH57Ds1ojn//2pLv/5uDreBm366ZKglwokA4rmqMlcdTS88DJD6QthmuOQ1XSS95UCSZtlmMs3C5dZjcTbMbVgqBgN0XbQyW+aFvESAQGXBvmZg2FRBQRQjLi8RbozzETCMBXuATIOpSYT58KqiTEU9iXECMvRrK5Lrqww/DjVx5naAN69SG0bxFVaIyf2Eeg4heo26lFriiwJXm0otq/LTDPOWk5OTJ0+0cTWvDEvb4Y1gctVbX6Sl8xCBJVZ9BnHhuyj5XX/X37RPGmNitrBCHebNAIr73stoXeEbsJ6YjLlvQDUOVYX/0MNzrz65PHv29PjRQ0fbDjQ31iXFhVKIwu2AbdT56YcP7rl7Z4i7rgQYprqilMsh1PMdhl3MD61cRQqAQAkxIaYYAdyuR0qEc+C+aoYoAScP+FCnYsKoSsAwy8WiABhQEcTydvb08XlLLOooOI+GkXg5J5I0mmuiNHuaGXNxW5/sKxzAydo8dUVdlBvghVVhhqkwuGiUkdhcljp+HhmJTY96W2KJRqKtJVRMijBRHlPHano5STEwHOcx1rHjSvjYaKEqCyAldCKgDyfgapuscAd8BbCqp0ppCozBS7BNN/6hfbK/LrQ0wRMdw/y93QwTquWbCukZwm1ZV12mQ7ZudkpfYmKGzmMHi2QBXo4Ct9cjjfgq7fmgZmAVp/LqfVaHY+yAZvLWW8qzvNBc+IygwRteJnUW3sGw/5+99wBvK8vSAyd4xmPPjCd47LF3d7yz33jt2V3vuqq6Qlfu6uququmq6kqSSCqSBMCcc845gwkkwRwVSFEUs5jFKFLMOZNizjmHt+fhAhePD5lBqXC++/GDIOBl3HP+e875/xdZVzgdyScbBCR2eBp6meOdflLuDT1zs0EyngYs0AygXJ4UHKB2zC0DY/B3xO6AhIh+jFhKuSDlNJgEMQ8v1S/Cm2KpAnERnbkRA6kTlhTz1VRcXKwXhgamQ9mIDQkAlYOBxnpEuMTSBW5sqp0Z6uCCaR0BOV9T7aMY+N+Yc6HHveNgiWZ/1KIt2tVA1UvBY3jofBh1sM6MdCnn0ZFBVB1uYcJcofgJSVZcmAMAD6Z1UawIm8JF8KIVQRBPVJYXi12OBf+EW870mNeCCz4iA0FlZKwcyvH6dq/Fdr4Z8OATe79vTEyuAPrSY5GZcM9g9Yae2wdSlx2Xlxa3tjYxnIApBYLamelJmH+kLWweHs7PzfR0tYPLSEvm2onwbQBIc3e2TIyLgAGOBpHCw6Td8rSBGxUSFR5AJRznx9bmOgmx4Y+KHsJMLprhwSpSCg3Ma09WuszPUpn0QwLcpYAoqr4z4mkkeI1PHpRuMSRoJnZNUJLB2Z2lYxkZbiU4dUqtve0pbdWSxVQBRx9lZTjFDpa43srlTgQHItkreyt9eHJomx0c6DXSvaHHUgOc3FOeutJVgDgSy+6EafN6pURVmE+EUQf78DhheA8fpi4vUpeYxQqBzg0NtD/ICrQx5DdTMEm1rhQ/g6epdn2ZJ/SaEZVia4Z9U6ptC4VWEd4HPNacZmdvegtR1YezfWi6cOCpneyEYQZOgvHqIdWiPXTh6113SPr7Ao65qd51LB4NN72h7vFLEU9vVBE9vxBwg/8XktTgkB+rHB+s7W8OvLADO9p6YSBQCcNkwYsDsmIVg5y5064oHO0RHf+R3MK0rEzF/hQpuYB2N+Ug35NdQzT/qfAgF2LOjqlOUfAgthafbAltaRT9fF1NpaChyBa9g2TphYV82Q/Iji9eQgz+9vj7SExtxcY2eruTNIm8cgWUQxsiV9doS2sxp+TqiI0dCfSHzcLG0RGWFudJWWvEpXrnJVEPLhx1FAT5udKIm2iGkXBD/WN57rJoKLC/v5eRGid2sRmcHwAw6hqhFHStq3nd2uV7aVquyqEcyvHqs+3Dr9v3zmcGOmo6N0nCUrIljHHtXrnz5r5sytbqqtIn9dUQXE48Gw3wcYZpBwCSqaGGmaEmRI2AFuRR+wAgNzY6lPvgLqaJEx1J8ZFdna14Qoav9Pd1c8L8RSkWIbT1dLUufZTf19OJ/SCVZ4JWeSiJIRbAAJVCEBMt8udnqTExKq7D3CSoth8OBudqqCMlMVp6WgyfBQY/imaxqAboi98y19N5FqdWXJgD3gpzsehpXVVnXLbVU+c7elFfz42NtTZGi61is3koL6TLVDM3VB+tu0c2hpGpsAJPewOWBvkteNik1SE9G6M+DBGhvlSEhnsUwRGLWV2dnSUKCom09HZfT3XNy6hwBiAQU1PV0VQ92l03i22ClME6xPWMIeFmgE899xzjvPQwnYYoDyTcaFzYiQdgNkOda4k++t285BsMQHQOpuoswXYAuUFwcl7RyBnCyn1iTFMoqNv3wSvB/K6EYS8DCls+bvtbCovL/0IcnILplTcPbjUS8xyyRlGmrRUJH9bFRLn2MKYlPMhnJmddFzhS+Be7vLQoSm+FFQZFcV16Siy1xm9leQmtUEKsz6cw7uis9XRBOSiYfMvcHIk4ieXjxzExdV6udvoaBlpXbfTVqz2c+YtqFGGx4+ioo9NpiPG2X+rmoMNSxe3XouUN6+trdzMS/bwcXRzMwOtTWZLPbpjZX7ovwewgGNzKbzBN93R33M1IEg1NfD3tm5vqIXaRuRH4Om4Ms3b9nv3oA07DO6hmSRmzKodyvE7daPCjDnz4sbnNT3whZl4SzML+x5ynNsfHRwr5mo31NbGcq/DmQH+P9LUnbOA4RoYH83LuBfg4iS0adLI1fpCVAQAMd23Nz83AnOkmofEMfFNFaeHKyvLi4jwtE2VpwqqvrQIvIFa+DFxhZXkRHDYcEjiChw/uUlkZAbyJNhhTUSU1WRcZ6ie6ykYbJUW5MmHY0dERYnEg2eqXTslWv7e7iyrkpZ+C/DYyPICpUEjowlLVYakB3BoK8gefSzrxWB4ki4sD3+3II+6COyu2EIMTTpLjA+KyMWXMPH2w1JG31l3YXZ5qrHsDsBmgZekcV3CVqOQc4OIxFzz4Pvw+IGT6N1tb8YLvQXR0jpMNg3EFrQUjjMTQUIUDSA8wfBxvVZtIqjyjosFuHqcivADU9CTZppBj7mPLMNK5hug0bCx0RYtuno2P0mC/FkPVVO/6o2iL3nuOiAKkOc3O3VJTR5AHszBhSmnRf362VkKWdwmZvTmKsSkq7WedDYOfZ/9XJ1gsSbnk+Qvf7fIdUi0OCZ3Jo5+wN05qe6MjnDB7/pdpZ2dbrDgmCuLp/nJrCy/q3L+XSsUP4CbxFg+Tk/1MtbWYKlpMVQ9jJtnIKwVExcbuREbOsIN34WNI3zk6Zj0ijKfyzF0MZXsas7xNtFbDw2SrkImriFgPDzPVuYH4i6R7MjmDBoVsbHQIkfuD/5uW7E4gqkAtEAClxCYhJXxrFnCjaBUiuO3szPTRkUH5YTnm0kWKYeS1Mr9sZnXJJ/3XrzkSU/LjK8fPZsR2vBla8r6V4w8G2leRGhgAMJ1bNzzif3un+4fdw9XTFCttrGPlWdoAzxIS4A6eAtyEnDxvEHND9Bwa7BUa5GluxBDtawVIxuUEZ91NqXlcPj42Alu+nRYvlsLe0dYIvBJMp1l3U+/dTsrJvt3R3ozn/76eTtGEmJmhJjvQAzCknZU+bXUS/imdtwmLQKKReSdZGBQsL0awfT1drAA3UjmTYHcyE1wAKgx1rp+RJhEzRmI56bMbINUn9dW41g6R2sNzBc46w94i38Uu0cb0qY9HlYezNq/jINjfTex2errajfVvmerfzE0KWOksIOXCOvIjfKyZ6iQ5R3REoIRVgCO07AuukybbDZerpDgXNovXQAEa0XlBqqponeowOv29Q8z1UKjAPykWSWAIJ2Wuf8PNUiPIUSvN3xCGry2T46brZKYOUEpP64Rqc4CPk+hqRXFBDg2DuVpoPI7n04EgGsYwF22mhipuYxsc6H3BEfTRDvHMWBhCD3xFEncrTQnDFEwTrJNAaOC3QiTW/yuykPSijUzgImKPfyKbGmXahCVJ/Tn4DbE79EKuE0wc6McPsTjMF5iZ18fDjjbvg49EKReMFsC9icn21NY2eLmiOgQmU6XLz0sG5QZMggiAcblbkZFx1saG2teeersT8QmN3u63NC5rMK50+3mTC2wKS0ZyF0JDbPXVdXhzK1lwsjj/nC8v7s0FyCQ1H8VfnXV1NJeev1pZXpqbna4oLRStcyDXAnu7TlHGgJYkhYNFgjEd9esGumqhZe9fkJjsy4DBOPXvRNa+q0RiyvFzyIMFF31oYnpF59Z1FDWS4ukWlz2TPo9te3tyveEss1x5aaGk5Txcgzc81D8/Nyv/NgEyNTbUAO5COERsbWFpcR5sFjCVt7utl5uNaIE9TImAPRrqHoviQDwzyzNkMiXs7+8725vilh7RVAZE5HAM4EOpm42NZkuCGegFFoMukEqUL91QcaZYBr+zLjsvLcbyNM0EuIVUiAE8xuARKiKKLD1BoYekSpPGsqyZ5hzAYKgrrC4nGpUjwn2XRIgC1wfVjgIUlHnv6GIw8/Pig5D4hIPoKE9jFupUp4IxwGZIIoylqSp8wZNs1ju5L1rjA4JhXE4wFYPZm9xqSLZFGAz+1ifZBDmwkGgYamI/BZ3mOdtSKtH1z5SmnjDFkmD7M6+iZpcShl0cpt8mnhkJawVHb5yOrkORCoBRIfcGIDF55BReiOQCwqoHBy4OZlQFYTxlmOir06aD1ZVltO4IEzrK7SBZSfA6WGGTtLm5hbBQM14OCubibEdruWhto2MOY7iptma3NC/f0LhE0tYnJKL6Rhi1nq7ybYSeDcM6zoha8DzFN+SzkeEBFEY42RlLAUhwqa0EEqL5vN5usQuQleVFcAuo67iwcYhUEuMim5vqZ6ZP2ava39eNOJHJVBiLUr+uft0LorTXlDURAtOQ4g+CCz56bXGmcigHhRTRxu07JBKNMVhk3buxnW88GrakTQgHB/sQso+ODK6vy+su9/f3ABGFBXtLCYhN9G9FsH3TU2KbntSur63Kk95ZXV15Nj5aUVbECfcHoAVBqigqgwnQ2ky7orQQ8Nid9ASAZ7AjWpG2o61Regq3quLR4uI88gLLy4u0RIqUIalC7Jhn6IphRkSxXBT483Cc+NgkwTBsmFe9q7P1dHP7xLNRRL8kKR9FnKqrnGpwFlQKCqruMxWlpCZFU7+1MD9bX1uVkhSTwHaZbc5Z6shfbM9b6sgLdDNDMAy2SeO6ELfKGYUikPv3UkUZXKhaBcLvFBaKLtfC3+nQkCRbU2o2DF4zBWWKcg4kCE6LmmwsdLE4GNtJqyXNDtEt9txzzIswM9e/waLINEvvX7j4MrJdYjZQCMB63yEWYhUpr+olhn4kWv+arAtTmhKGnVy0uSN8sPo+uXCp751uouN/F+zu44v91QjcwKm/jkurkcD8/cw0SeS84Fown15vTwchkJWESZBewF1YFG6hz2SQdYnuxszDqCjZ5IexcUUu9ho89lgexWIYERdf6e7EYJIwrNnHQ+FsWAx3JSwUVyQmJ3BO0Tt3LjcIU8y3tzZJ+SRe2AO3QWNBHOjvAR+GhUExWSXENIMDvYoyIEsKdx5k3XZwZCGyRLQKaKCjxn70QUzb64tSmpQBunL8LFJh4VXvGRmo6rH4AADwmFfy53FdbyS2fba+S09TYJZXWws9QE2Ik2NWjkX6vd3d/r5u+Hx0RKCPh51owyqV5BBgFTvQo6e7Q/65FLYPiOhh9p2E2HDRqkUrUy0PFysLE2ZUeICkUklTA3WYYB9XlqBFxo62ZupBwmsTfXXYMjW5B8cpz/pdaLCXaEWimJl2ZRmXnIDzleK+YafId1iasEQ50OXFSIKlUhoRvzBa2dlOjueAf5yW2oUlba376Ah8UEP94xhOkCTGL4SW8WUEMIypMtVv/JjAdlrrLgIk9qwhy0z/lg4vjQZYGrExSzLwp/gyErwWLHpZh0AnjZLnWaK3SHBj1yPCwyz09QSMzZiUP8Rcr5DD9vF2BFwNtwCeK1r1o6eLFSBJS8H6KXyMJjqHljjRAwYYzNVCA9AXyfnB4+RI8zdEpCDUzaJ2D2lFXovJxKQNMeNPHMydb8qCWLlP9LzFD1yb/zWPNE7u2HJvjBhVF3LOzXMIpSlhGN3GtSlkGAYXnjPdbiea/4S/u2m3C0xnHR6eEV3gsgcbcx2I/qsqHglhWNcJGLaysmxmqIk8GXLJMO2Kh2FdXWVujoCp9HgQaCokSJa0CHchjG2mc0OHl0DLsDcHDLYfFeVqyIB/uhoxt6U3mEnAdSWuDkwBYf150dCfwnD1i7+3E5/IhGLgX+fnZghekzcmR8aMxkODfVERATS/ArFRfm7WwvysokcC++rv7Xr44C6SDhP9QNd0dlDux8ZGKoDESBj2ehclKody/GyoEX3Sfo36wVBLmIXdj5z6d2Ja3uqeF5N7L8i7j5PzVDZCiOnHRoflX/iDWRcwCXwLc9aJJSfMSI0TVfWVaTABQoybmhSNCzqo20RNuVIGRM9wmhVlRfAXgmnYVG9Px8SzUdgsHAzgCkBoKYlRcTGh0sEANryC6evpIMUpw15wQk9Khorg0UehnuEAH+dTLgjv8KnSHWwMJWWWsHAluJWFhbNG9hAYPCp6COgap4Awvi3Kz8bAj9rSrKWp4mylM9/6cKkjb6k9L8LXGoCZDkO1uPChFJnp7a0t2AvaQllJPgbqLU8bfD3t8cbB557ITPb301rTR4MCbPXVGQwVtFyrx0uCIRjmbaJFVFXBNsFrb6yvra2tjAwPdHa0dLQ3g/cE1Afx0ujIICYoFkVQ+/v7mEqapaka66mHSD7aM+yDHFlMTVVq8g3rGUhsU1/JEsIk+Xng5LGDRWLCQrhlUm63Wu6V5kNixpukweNr5/45sXzvlIexXkamSfo+IFbzlDDsdbSjDWLSimj7O/6zMsYg37lQm7QWPtYXicTOaIuL8whcmejf2tzcoKrL05qSwRshr4DZOzAMoyvGLC6uRIbb8ZqyYILLcbKRUVLI5Y4HByC1MTPdGwuhbIBhBS52TF4q7I69pcIViVzucliohe5N1BUGrvSMdRdnMQhc8CUFv4vc4eHBwZP66tBgL3BXWNIE0Bd2WuDUAYBRV2rtrQ0goHmYfUe6aKmYOfZgHzxHAjcckDbemthmg/2jzZyRqzbu35ERG5kNuxpa8j5XCcOUQzle7hHV+LaU1C4JwzI+wzAM0XIIyhHFT4yzM1Ox0WxRDQyYkbzd7QrzsxUK2dfXVmsel8MGI9i+YukQLYxPTw0HYWt7a1N6CtfLzeYUQmHsIM/qqlKAYfJ31SJvAn+pXwEsR+0lkwTeqMSJcMBS9tLZ3oLm/+jIoNNdmf7eLpnkHFl3U4QUl47mijoXSYazkYDnW5qfYMUUQIaRoX785QCmGkvjCkP9sq+z8UxzzhKPn2Piyf3SO2FDLeXSt48VpeHJoeWgigoe4DNC6nNCa22l9oNtcCJ9TbQ1GVcMeLiLyVDRZqqa8p5PbZaqo4Hm0cOHUo6BSo8JoRF9X7waExQy6bGuGulcq06wHrzvXBBp7myugZvBAP1CHIVXDTjh/mIwPECd2SBhPInGpN25FYt1/pNwsyPX5e3cgaOajyJ63hAk0P6Y/O6m4l2mx3vEUgYx+K0wdTFyTQnDXl8DkI3v9ODXZIb3Qo0qOj4f8dzOcmtzQ8oykqgPQwtmAMNWV5ZJclVByQStXGRleQkBNlQDQJ1qUY3iySxbYYSlAapLdDOSVZfI5Y4G+etpqcEkaKuvDh+eDAky1L6GSJZmQoJlJNNER1xcnrOthuYV5AZmX3TPa1lJPqbeQkt3qUnCBnGsy4kLGEQ4x8wa6h7LQz1Ptd3dHdggeFlcFSmgxbeToh6zvD3szkZyrtcM9VTDKn+pzIYph3K87GQzDW9znrwtiWwGYJhbzBeYHdHC7qeoJ28ntf9q50BGqRtAo/LSwuiIQNHGG3MjRmJcRD+1K1g+m3g2Ctu8k55AQ01pydwzLpYBKAL/9biyBKAObdKTZ9hbG+Tcvy2JSUL02EgYdnCAC+02NtZRhy0mx8/OSh8e6sf1hPDJnOzb1BmeGxUixYkjqnqyhUzyx6Qb1j0TW/uAbHpqAvcaoNLBjvZmha/8Adg+jjqmJp9hAE/DkJhHXoeham6oHuxh7mar96whCwk3w1jqyNvoLzs+kFFpj7sQsTYAvkEQqMClg1ilquKRCLTtPNkHYXdL8zJqAwsy033gZN3u593h6wVBC8AwCEX2cqWJCsxMT+LEplgSlz4BDNZmqrlYaDQk23DcdAx4RB3ofTsr/YWFObh68LTwg66udnFpzZ4TAAxQ07QbcbB01rhkf4pMZFGTYGtF8mU1tshP9v5S+N2B38r7XaptNhKzAUT/53SEOWWvhGGvta2XCfOnA19e7L7guWz7j/x9tf4VsfE8BNFLi/NszHVSEqPlvR7ra2gKQPxOOzs7GDA8baw7EaALesP8vfmsrOFsH/RJUXXgw8EBS72bSJrZWOf6QmiINCjFje3z90Ef1mGpDQT4RlsZIralUHO9IyTirEg54k5kJMyhCMXROoNflOFFRwsTJkzfiE7X0dYINeABjoW4RLS6BhBye9vTzU0FMreAq+tqKm+nxWPmLkGhzg3YaUdb897urvQt+Pvak2SJAMN01cLKlTBMOZTjVW4Ma3krouZdIyMVpEWho3Hd986vYjvfqJ9QILgHLAGIouZxGSfMn1rvF+jrchbUlP8wE09T5kaaojXbpy9/OTocHxvubG8BDEmliZc5YH6O54a1Nj/Z39+TYy9HgJf29vYQBgB4KQbdWemnp3ABEWFSYpwmAsAgcQm3p/OMFxmOH0tjAzqV8kksPIMTnlxO8JP66sGBXiTRJtNl4D0uLswXF+RgAEnTvIb/RbcbYA/c7raS5N2BUh4zR/5Yfeb00wfwGpDY9pwM1miALmhFGEZbSxOGYbOz02ixEjym+Gzts2c4FXYQFeVqxED9YBAqJNmaHsP7CUnJtmZkZoypAv97VFMjT7JREnxqaqzFMCzQgeVprampoYLZ7cEdowxwT3cHvvJiaRKPtweI5n/FjyTHdc+nK2w2iAxKMfIZvkLIKVmxP0lGzviLzX9CzHgp/vvcIbFW61+eQF+tf0OM3iJmfEiiOyUMe81tp1uIxMb1LnZfG1UUFem/J3YHLnRvVRWPUMkH/J7lLBqBqB2zdNTXVsJcFhXOb0YCYHDiVACw8RoGsFY9luYQQz5xcDDAjQYkBrBKV+vqWFCANNp6bmyXnzeLNyHqsdQAQRlqX4MvWuvdmpWO38RhsKEgf18Tbdxu6+FidS4kFmc0uB14gRC8I3g4mHDBDVeWF4GTtjzZiQHAODme09XZuiU3AIOAoLGh+v69VD8vR1ocEB0Z1NhQMz01Iedic7hgoVGPeS04/yOliLNyvLSab4AxlJdCJj9HWNn7BjpqOP5jF3+YO6hxcLR9uqlsZnry3p1kQBemhhp3M87aoLK7uwMh7KOihwCZaBPU8vJiYlxEVERASmJ01t2UovxsRSsC+Jmlrc2O9uasu6liCf0kDQAMgDnTkrkA52ALNFRGJccCH4per6wsUwu/pQzwBeBtpRwzTNeYg0SSfJZ0A9iMggEvNxuZ+BbckGg3IEQR6BjAp+zsyPW0PG2sO8FS6GJFdWFFgkZ0LU2Vwnvcnoq07HhfbpB9oJtZmLfVsydZSx1560PVhAQZcTiL8bGRyvLigtwstJ0Iti+1k2p7awueGbhfEo8PIoG0ND6DV3iomS6fwQtgmJOB5mCg3217C6RGDe+3+XoS09LqaGDvGCeLrWjFfXdka6XONT3K6ipcKJxHzc5Mx+xckhvDssnM1XLmOcQi251k9SCOSwe/IdYeyfdDaiblbTH/XNd/J5NyG7UKrr6sEdMeRNd/OwHA+j4mYeEFh8dKGPaS2Wq+ME81YXGBOwLQj/nrz6+cd3VlubWlkVbGDWE9TpG7O1vKzzWMCA9RWQjB4+XDleLUMuW93V20uhbk70rwWRb5EX+eWI71tjYvYxaLqaLDUh0I9JWGpmJi1ng6y1osEjvxkJvaDY1L+c62RFy8Ii1hsS0+HgDhGAwVqpzxKaS0LsKoXQH5DzPBDdPEl8HtgeNvbX4if8M63IWpyWc1j8txKzB28+xAj5Ki3LFRhfXoEmLDkXoYjODCD5UwTDleHugV0/wW4Apu+5vwAt7h1L8Dz6cSjElXbfZM+I0OqkhkXDe1vBTX/MnKzllVUiAuX15avFD6WeyJhNDIziTIzzUkwB3RGikcf25v3UlPkMIXInbAtGxvbQDT6cjwoMxdLC7OJ8ZFAvKhVvqJDjH1ciIWGuRJY2xSyDDmoVFtiV8u3linprDotJZm2vLAMHBGAHdxNQ2AQFrNKqabgkvq72mrx1Jjql+GcfPaD48y2LuDpZu9xUuTEi9yXU0F3L77mWmItBCiHbHsjoDnwSHu7OyI30p7O+qDmAoJMiA5OfgkGQa8HB1EDgCWNBiXC1zsiHv3CAnNHWOjw1RRZkCGYj8GARWVvh9Lg1LptSCs8nCxonGNXKDNhRItfyZIZP0pMR8ud0K8VEiESIo/3SQOFFdhXYgVojhy/BExrEKsPiSU9nOEYWTeukGYk73QUtSNGmFDWtu/J0kUz2zo521nqfekXkhoA8AM/+ZbnirQKInkHWFkZ6Yj/4pq3EmeXAqWAx+GqPzAEfJXg0L5q0Hi10TX1+MdLJDwV5mb45F0qsMYbpm7IwAwFlMVkBuToRJtZbQZGaEAQWJc/ERwIGAwmvyir6f9C+TnoKUT8aWWNKSt5InY5MR4AjecJqQDEcP9e6mn1hDjrYwWCxjVrvvf/xRCXmUsqxzPLcGFclwxCHG1vMXlgS7AEoC+oprIJqjwyl8CrrCw/9HK6QcT0ytu0V9E1pL6V0owJh6Gdb7pEPC1jjoPhmlcdwr5Xdtc/POc92D67e/rbm6q7+5sm5+blb/y8OjoCJdmiPZxJcZFPHxw9xTHA4FvT3dHTvZtHw87jBnkHOD7wM9CGC0df8IpAx7z93aigTE47LiY0Ht3kiUmPU7Ow+CCT4c5AVZZ8/Jy8qxCAqgoyM0qyLtPpZo0N9KEAZjBz8uhq7NVHh9aUVZUUpSLS+s54f60D+DyGXKxlamKiJThr4neje7y1K6ylLgQx9ioEEkryOAcw4K9kxM4qL/OzdFc7KmRz1tv14OsDInXLTMTAgYk1oxlRRFSgghEg3GFa21Ehit9fZLO9FHRQ+ptpXXRQwQF74ALdrI1pok0lBTn0p7/iWejuF1QfvGG09jW0xNsBSQdonyJrP1psmSx5S/4X+z4L8Ssn8J7P5gjJixPZMAGviQ2n5xfdFVFTJiSKsEHC8Qraz8/GEYisSfCZ0shlTpFjcpyM+OpmCS5OMOpD9yfSvB0MzAJVdbdFPpPaX9P0vpQWUk+5oxC3iXAxxnNxTBHULeACrtxqTqW6ZBEvNuUngyTGsCqYDNd2bWFsXEtPh4OBhreJlqkUBjMgzHyliMex8S0+Hja6aujfBri30ezG/ghmSqQz9MgdBD17uDwkuIjy0oKNuTLYa6vrUIUQgVgFibM4oKc8bGRs/dXALpDW9bRuO6d+uvXVb5ZOV5wXgvwVdubALHQIIFWC4m74L8i696JanwbEFdE9bvskg/8sz51i/rS2vV7c5ufTM0vG+qp6WpeB0QBf/UY13Ru3bCw+zEw52NO/TsIrSkvL0W1+a2gvI8M9VX5EhTaV1PqVQ+Odp7njNdHaaEB2GNnqRcdGSQnLyJMdHU1FRbGTDcnC9zpRB1cTnB7a9PpihUhXodIvb62KjzEW4rEmTgdanVfT4fsrPSFhTkpbFi7uzuLC/PgXkuL86qrSgf6e6RMznAwNLlI3krm2unWEPGabL44RlyqjQwP3rudBIiUWk+RkRq3srIMQ37XOT01ASdYW12ONzI40Ev7DKYMoQ5dppqNKSPE04JMRqlfhr+ShNfQSWGvV/ooX3rms+ZxOQCbEzcIXk9OEnl5KCHWG+Bro3cLkJielpoWUxVemOverPdyPYqLk4LByBPJuYePH17TbtzS4gJEYlSED09X1t1UuJ7iwDafm9rB2vDU6nAy7HCNrMMSJgP+jsRRR3L8ZI73SGDT/vcCMvq/IOsJFaQGOd4dPh74Wlh9hogQARMS57E4fnxALCYJyUJa/uBgLkUJw141owL0RbnXCPdnjvu/IAleljLkW9bbJOto+WsJ/0ictigfG85iuziYUudxLDwFTquuprKzvWVzc2N4qB/mR5jawtk+Ynttm57wG0ltLHSRn4iNZlPzY/zf8sEBErV0sDGEeQcmaNxdLamHuKO0mFeUSLZ7bcmT2uLG7nM4xzxBD1SsSFLVy/xWXHybryfModqCPFhIgPvI8ADqxYK/p6tguQiDi8bl6WlSByfMX37pzI72Zog8sPYlSvfBVH4K4R1Jtry0iLrwddSvu4R/Fd/zP3GszB9tbykzD8pxasaI2M43AWuFlr0PCME/81O/zE8Dcz7xSf/MxuM7G/fvrJy/NzG9YmZ1CRCXkaGKgTYJuuBRJHEX4xrgLqxBLFzGBjCmcd3K+QcAY2Hl75PPZ7Py+eSnwlw5X2rfvIFWVbhpNqt7Y8950hsbHbYW6ZgCVwX4anFRrrommMD39/e3NjfAoyXEhoumsNydLSFGB8xz6oMEF4mJ1MWrD9sZi0I1mCe93GwAXdBUHCH0x4kaBAOam+qlc36Q+lRbW/j1Ga85+BR5BDNhR4CH42JCAXchavVTVNMgW11dgXvk7W6LtgDBhuhZzM1Oi1JuooQYS4OvMgrjnjgYBlsbGuxLjIvgE8QbaszPyVbOHB0ZXFlZ4h/J3Bxx/z5NN2w5PCze2sRA65qbETPF1mwxLJTIuE1MSHPHK8tLOMnpbGcimtiEx5Wq9GBpwoJHXdLW7qQnCJbOUy/k50fWAf6jMMrteZvYlo/ddL2U6H1X+MWh70liQ0VtJfvE3rv/OzEXBMjsfE5tvYLOsjhyndibJF5Z+7nCMOKImI8kWv+dQuLfRxttRDOidvl3ZJuZbMi+fzz0k+Bp/vGs+P/4OCk+Ev/Iy0oKACCNj43kP8wUneNo/g+AU3RkUM3j8tWVZVxWgYmSwL0hYo+7GUm4BuPE5M5Lfxnp3UTAxt+bT/2EusVEraH+Mdak7/Lz4oMr6YMEXTHoxVIYO8vRajGULRGJ8XDaYKCftd4tbUEezNXRfH1tFa4Sro4QwyDy3G12djolMdrN0Zx6O0KDvaoqHuFWXZmBwv3MNGoo4OflUFleLOfX5TdAemjFUY95DeJg37u/8r//qe/tz/yzPvW7+6uA7E/YxR+QZWAdb+IWHeV4KSr6XnoR4djONyKq3/WI/621y/ckaQTzGk5qoRf8fzJ5cAsGL4dD8nby3gEYpqt5jf8xDRKbwfvCZXXeVwz1Vb1SPo9pJnenXCyAi4ArEuFadXe3vpDZD2L09ran9++l0hqQbC30pJP4SUBlswW5WQ7WhqI1BVHhAV2draJpJXkTdz2ddTWVqUkx4SHemCEDs02gudfb3Q52JIrHYqNCCvLu01JAiMkDABhg0Y2NdYBke7u7MuVkzgjDNjc3EOcTQEeZPV3g8eG+AALEvgkwBg1VyhOTUNdz4WpIynaODA8C8ADUh0tpaAMur6TvwmX08bATXSCW59i2uzqJpCRxNTixc+yQp97uZBViaSnR3U1sy7hihYKmOxiPK0tE7xdcPSoMKxToVos9I0yQ1tRYe86/uo1aYtJWSLFI6uUySa55mbbTR4xpUvgzPiBWcxXb9f4MscAlhi+dxEhqR/vnlO7bbCBGNU5svPeXxPJd4hW3ny0MQ+v/94QtgyvZciRCl4leLGf+h8RasRzP5dTxKON4TO8c1B541e2ulIAefJtCNRWILdfDxQpcI8qho/VF2EhnRwu809bShD6WnHACl+LycSQUBu4K/RMmR7GuZWVlGRHLMhgq1R7Oiqkwx8bddbD84cZ3ec624r8Yw92LjspztkPai7gtGItipSRGYaT6Ah8uOJ783CwsDIJZjOXp0sauCyAcdQ3Y1EA9Oyv93AEYPmD8OOmxeFGvID7m/SX/y8TkilPI7/wzP41qfBtiayWNxwtMLqHSvuimX5AvXsIbwUNE3La3Au5/4hz2L8bGKjq3bpBYi0WZkXgCCeRg8FEWIAd+BoynJA7gytT8srnNTxZ2P1o5/mDv+62D/zcwTMyuiIIxA101C/sf/e9/ymnglSk2/2wrEt8Mq/iloa4ayh8625vKyTx+oeUAA/09qUnROE5tbKg55VL4+lpRwQNUoEHPXNkal5XkLy+dqUBgfm4G0IJo5g0m3vyHmeBoxBZJokQQRN79fd3U5Bj6CwaAB+ZteHEssHO/yONjI2gCFytmRbsdD7Pv3M1IBGeEjz+Go7BaNJJQwxiJxrEsycuIpjTraiok5Qzh0cVNy3BTFLu5vb3iV3Lhzbj40SB/UlAHYoxW2YsUAPIxCAfEiNay4djgSuJbCZcCk2+Bp14VV4uIbHpqArta6gNzDraYeAKltP+9vCyLk1ZCHvmWf0My2ClavbVRRTaeUfc+xiLWK8/nvLbbiRFVMlDHG2/5s9cAgClhGJq69IT1r1tyJE8O1wDcC2pt//Z5Ch2gX3t9baUkiKXLUtPSVGVpqmozVLWZvL88uUAdppquIO+P0+UtTxvW11axdBj4SN7Gq3DBGxVfYR2M4sIc+GdO9m0+GavuDbEsUnCoqOJck3El08FKARgWE7MeHm6rr35L8zKAMfoXeUmwXQ6Hba6noXmFdlKAFdHe01O4iq6cna+NjgziY8DDzcmi5nGZPEz0QwN9leVFSfGRNIwdGeo3d5GC1O2tTTTaD/rgZSdQoGzp8KNH/G8Dsj+J7XiDbMuhhLxRTeeAMS4ihobjJEfzWy9tBok88da30F8ErlDfFIAZlISEv+SLtjcjqt8NLvjIJfIrgCh2Xr/3z/qUUjv6Jmq1eoEwADUWBud/ZOfzLaotFEImFl8UAb0J2MlQT9XE7LKl4w923t86Bn3tEPANvPDN+Cwo76Pwqvc4DW9HNb1NXhyE63gXIaKWzK0Z6avq3LyBt0yuHTCuoYcz8MEnUTyA+nMDY3BxsgfUolJNkWrzy6OgiGxqagLwzIOsDImMdnJGZdtbZSUFNEku3C4bGuSpUEX6yvISLX2Esje0JJ6poYaR3k1wNF5uNjDgtejeYQotys8GtIBVH0msIqhUhKh9b28PIIc8MAw+IzOBRrUn9dX87qniPOmf7OxoQe4SvNJZKhIJSjcaXHZ5OpypHYOoMl/SGgECqwBfsTo2lxOswJHBZhMTxXZAHEVF9QX6JdiYeBlrzYYGEwmJxMqK9IcNPwkm+urUgk84QrinSM4bnDteJY8KD5CyQRxQOdkaU/AnecNP/5PY6SamnE+wGg5fIvbk6HrYaj1BZN/9/ylGoXG8R6yXE0tpwuIyxOcxF3Zus8Za8YmNk9zjVsR2G/G62M8ehoHNhfATuO1/TyY9ZdsRMa4tSIm+fxr6zjPAMPBeoml9poaKxq0rRjrXve20/Ry0HUw17E01nC0YrpYMlqaKrbG6ie51gGcn1qG1rvIKMDQRDAOEgHwP6g6Cd3ByCQw34KL018z0JFZRlERaBTMm/C+LqRpsrqsA7WEMdymMbaJ9XV3zcpbjSfwWG7cfFZXrbOtsyGCdJEXEhZTIlYJ7phLxP7/c6tJidVUpppHETbrRkUF1NZUyq0T29/cBYwN+w51+eKa+fy+1tfnJBRE/wt2EgCOc7YOiimB/N3agh4uDKXhocCpwVeGm0w6Jx6ZIciToa1/1TPw8oubd6KZfxLS9ddbiumbUQfQGp56kakAxN589T1aZGQYeQiSD8kVtPJZzEtW8FVr+PrvkA07DOyhR83KltuBkO96MrHsntPyXEY/fC6v4ZVD+RwBjwh+/hxCXU8jvHAO/dov+0jXyK3vfb40M4edMQg7yRqhfN9C+auP2HWAel4ivggs+jKwl7wi/fPQ5QjJ0IoAJHfy/sXL6ARUfYtwFr0ncpa0GB2xkoOoW/QWcYFj5+3CycMdxfg/dL4Q2eXf/LUyleLLT7I3Q0vc94n5rZKSCIQem+tRjXDOzuhSU9zH5ZP48ahThut3vV+2dLOkdaLIxFVbu1ddWva6uG5wRhPXgg3BChlr3kcANz7qbKpO3fXJiHOY3mGbBscKmZilLXUdHhyPDA4BqUhKjqXUNEJTfzUgaHOhtbKgGSEllAsSAzd7aICM1rqO9WRRwInwFG5c+pSuaNEuO5yA4tCI5D4NdFSrgBDCJlbhOcf0Be+CusJrH5fLdssOOtma41EH+rl2drXKsO1fhVNjEM0X6G/v6RAAYF0KRaXZIuIU+g3FFm0WSMxvrXG/38yLKK+TBt6KFQtSs4PjYCIZq0mtusWIY4lpTCGxLQIqdQto5IQ24rIcHYtdZf6L1rwX9Y78g5iOIw3VFsO4ISWFP3W/zHxNTjnLBP3kfsnWi/T9TUOL/Syylv2bzmBKG8WyMJVSm23smV06s7d8LKBC9n7vvOcRIDJAVYLAId4PkQLORfH+iJYZojtmui9iqjdhtiNxriOy877VcFTqU5+9iwdBmqAIq0xOX6MjJJiVKKsuL8Qpfd6fQe01NPjPS5ctDo6UgxKmIkjxiDxIhN12WmpH2tRl2sLxazCQMCzXVuaGheaXEzYEPwwDFcbmtvp7exlowe1KJ6cFr3s1IxP8En0ddbwM48XzuyOrKcnZWuo2FLo2kGA4PpmZ5ttDS/ATzr+AB71RVPFpdXbmgw97b22tvbcKtdMjVIVU6wITgVzBf1vb2Vmd7y/3MNPCd1IwZGVtrkm055jY/BT78GCJjFD0jMg8cQ/OzWwIucn62R0D7QX6lg9/PE171njv3CwigAWk4BZPVj4BJAJIBFOF2CLM9/BcdguwQABgAHs0k8ICAPqySDOsBHAYXfsh+9AHJtufyva3n7+E4ySI3i8s27t95p3xOS+K9KI1ddMyBDz4GfGViesVAR83IUMVQTxU1RwFcgQFv6qjfQHlIVLNHpoBY1Gb3q6iQD8EeY2MVc9ufXDlfeib8Bq7DCUR6MagMnQjcKYeAr3k0hvxeL34qjNe7ZWH3o1fS53BH4BaHlf+SjxJb+bBZ0lEltX2eP6BX+8y/ctQ1f0A/u/dmRud3iW2fxbV8AF+M63oDNggPDCqapWZuYdcA+eB2Ay6N7TwTlaIwj/pSNgfCZUzr/bxz4sHm1mpxQa61sT71UqBK8tfbAFeA84oI9aU25+D01P17qaL0fdiKC3NoJCKtLY2bImULi4vzRQUPYilMS1RKCdh+XEyo2PyYk50xeIenjXVNT2rX19eoZOsIaaBKxTNegYX5WbQ26uflIL/bQl3l5kaM0ymdwKMlaFd2PN2ysnQrKcrFLNAAMhXbQUEBLQk2F8oGAGaodQ3QF747mowrAaY6ZN5Mcm8Y4HmMrkdHJOqb4WZ7N0dz6YeGN5jLW8U+hwXW2QBKIeL/SqqEyU4B55wQUx6+ROa1FAhDd0i4hcWfkBzZGJPYeHze+Yc9PiFH698Qc8GvNDG9EoZJXxVYIAZ+I1gSeIPYl2NKIsnoeYWqnf8kV/vjGWxqaiL/YebY6DAi54EfLUJBEHvpMNUecmxI9NURR7RxieZocsA/eXiMaOWS77fGEG2xO3URvTk+gU66WoJiReoA70KcJJallSyHBnvhhBj8ky2QmIRZEryL6DG3Nj9BH2AwVJ56u8vF0sFrnO3w8wKsBaPUzZEUcY6NPeZyi13tGTwGfGqTN2qympudxsV7DfXkFLC8zGf883SxumgF5/W11YfZd3DVBJ+C1saw9FG+PDraED2UleTTFlMhbgCYDcDs1B0d8pz17u4OVVfawpgZwfZFOFa6AcCmnS9KdBjoqtn5fAtxP0S9IcUfhj9+D6BR1BOynIx80Ui+4DS8HVn3DqCO0NL3/TI/dYn4ytbr9+6xXzgG/87KmeQlB8hBwgweJQMK3wGTmFlfMjW77JPxWVDuR4EPPgnM+dgx6Gvf25/BO24xX8BwCvmdidkV2AIgN4AfcCQmJleMTa4goILxCcn3gCgfNK7r3LoBhxr7QnNigEPYJe/b+31jan6ZzzxB8gHyeClY1/QxR8VJxCUs7WMJr78+i147ii4gbBaug43bd4CO7Lx+H8DrmwI4gUr7EDA7CypD6S+AKICfncP+xdzqJx0NsvuLZNQAQKitZmn/I9xcHpPhLzEalM4vH9fyIQCt4iHz9tmUmY3WnQP60v7h0d7Owera7uTcZteztdrBlbym4bSEeLa+CJWiPg+dGhvDs/GD753PYjv5STZFzzG88pfskg/gYRYedjM/Actn2xeXm4IzvVCcj9Yj4ls/vt9g2dJdkhgX6WJnJapBTFUfeRWtuCAnNMgTAtb0lFjAM+Njw1IKGqcmn0VFBKBFQ9p1yEiNq6upFKWW3dnZLispsOKRWwgdYjRbkkTY2OhQdmY6TJiirVCTE+NwhNRKP9EcHTvQA7UAKIpJZIAOQf+2zFJ8nGTr7GjBQPEUCRkAfpgu5VHR+Uvxkowjggwk4FvAsQq5QCIjg7q8uxUZ6WHEvKVJMuPraV1lMkgmZ9iyNkvV2VDzgMMhZmclVYsg2WgY9zPTpCRmse42J4xUTpudmdre2tra3NjYWEegt6uzFR6q+blZvEHU5XEegUgFn5i+5y1iT5YmxO4AmSvD8Knzv5IVYcfyR0pHZMcXlU0RyZGdVxuYqG01kxmw/anXFX8oYRienA6FLPbd/4PMd8m0rn/mJ9CONi7uuIaH+hHtIQAeFwfT+/dS83Oz+As56lcS/EyIzngSdD2Npo+22L0GzmFjFAnM4J+Aytpjj5pj2rO8rI1usTRVaZ1LsK+ax0L1D1p9Oa70MzdiVJYXUemqvNxsRA8bJiCU3gH4lGRjSnBj5cyGzYexXQ0Z1zUu3ba3WOdwCl3s3I1YMGNSO8ECfJxwqy5Mf5hnHxVkg49BvL2AZ84iZyzdujvbwkO8qZUqcGXgSEaGB2TWH5Jf72qDwAJ8Oa1ZuaK0cGrqlAl9+CLcmrBgb9gOAHXpTfDPxkcxqWZ6Cleh1mfwLYCBaa1rZGcOwhIACbSvGumrGhupmFpehkAckICZ1SULux9NLS7Da4BMBjo8DSh1CisDyQhyjUq6gDEGos7j4yjea8zigBNEiONBgGSuCdELS3w8RAI8A1WUKXox7O1dbwTmfmyorwqAEA5GiKAQeaDGiRMUvqPJh5G4Tw9ew0bQ9eQnyhjX6CAZkazwLo6J2WULgEZBX/uk/Tqk6MOA7E9QFhFXb1KrOsVeHCqaAlDnlURKKvPxsyaP0lD9urnVJUC5oWXvI7SJtiwJk8S3fJzVc7VqzG1gKX9us3N9dwqA1ime/yd11fADDPZ3E32EUN2mo/el9GKbyp6QnAGN5K7P5GN+f8M79ddweQHjWTn+IGQKbXkrsu6d6KZfRFS/Cy/4WVnUvNf5Jrf1rYAHnwQXfoj6+s4XjJEPTwe5i+D8j5wCv3F0Vbc11zdgaIg+6vALrSgreqXdMhy/6Ek52hrFc8NGhgclTe/wPqnwERWCcylUIBQS4N7a/IQGPJYWF/p7u3BPLwA56Z1OAEIkrXbx6tOGS4pywauK5dACxxTo65LADQcnu7g4T6VcQr1GiqIy+DymL6Z2E0hJQ8FfwKvUWhhFLVeghOntbru3t3futx5X7sG4m5GoaKUHkZyMW8r3o6J8TLSZvGVcLaYqhBMJNqauRgwdliq8GWCmcxwTI4mtHmeubCx0pSyttrc9xUcLQVpaMtdY75a9tQF8y9pM28/LEbAuqnDBej8waqvLz+16bbeTzPLSZcE2G8g2sJY/5we6rX9FUnEcKlJus5J1AoC1/AUxzjxef3yhMfBrb0oYdtIGvhKkaC8Tx7IUDKccieGfiJ3eCz0iTP5OGzpMNUuDm4sVbDLlJYrB2mObbrtbGd0qiXMgOmKF7/PA2HJ1mLetFlNDmFwy0rs5Ozvd1dmKXR3ACXwMzU31ogow1KILsbMwSqCxmCp+ptoKtIfFxs6HhjgYaFrr3YLBYKiwmKp6lKo5mJ1prErzc7OoINDcSBOV8N3PTEOflyLccWrraGuOiwml1ubZmOvAzLuyLBcZ5uLC/O20eNo1dHEwgxt9FgrEzvYWWgcXHCFWpBELpdJTYgGvIq2CU1hPVzuuTRVN1AhTTwIicgQh+FCKdVUhhk/69hX8CsoOkfiE8l1AJj7pv37+CTEI1qMa33aJ/JLETgLIhMASXBYzy0u2nr+HYef9rY37d07s3zkEfGPvR7JW2Hr9HjCtjdt3vrd/BRAOhivnS3fuF+GP32M/IsWOvZI/h8+YmF7h59Mk4E8M2FAbFezL3v8b+Ot391ckJUb9O6hZK6L6vciad0k+TF5iB+E0uFzwmeACstrTI/a3AKoRxsP1h4C3fdI/i0Kdaa1SSg1//bCf9WQybGSlbGPvnPX9IECE36OvpwNVZw9lxsz1dTJSEnZ3t/YONhe2u3sXs2sn/HIHtFPav4x5+g6f776Z344I5x5S+KGBrhr58PAQMlxbuB0OgV/DXYDXFrY/GRupwC2DS+GV9DmAW5fwr1w5X8HNQnfW2uV7t6gvUdrqrDWNzWT6K67rDbgpLpFf2bh+j5sDxf4iIPh7Ul/9qjvk7q42Kb9rUwN1X097wFSSvj402Bfk74qKI2jDx8MuOjIIZl2a7Fh/X3dleVFrS+PZD35/f//Z+Cg8imJVs/ApeLvbPcjKkFNLTayBB0TUjuZGjFVZjWEYRqKcIakauqVwOQ9gVEtB/rCn+zyrXhFKhLPAIl2Aujc3FYzyqdmw2NgSNwcNxhV9XhLMxZDRH+hPcLkexiyAZBqal4td7cn2B3HZMDgY3CwAeEzKDuEu8/nPTLWeNtbJ45XAQc9eJOfWyQzWDjHlJJRyhjGuQ+z0SPnGwd7KCcqQgwVi8OsTGbC+D+SitVOaEobJ+dvntzNuNZMtgAK5g4uuNpRrjWN7i6pzjwdLUyXISfeATHaJpMJauYN5fgBd1G9dtjVR36iNoKfLOuO6sr0NtK/qUVJM4HhanjZgF07l9EtL5kqZTQChiSUAzEiNI+EiT8R5MyJcLiTG5cKEuBUZWenhpMcrGKDuCLAihP5ir1JyAodP98RzybinVqwi5Klt4tlYalL0iUYCK314Rx4ABhP6zPRkWUk+FdACbgQfDFf+1Io3BE+cJ+f+7RNMmKZawf5usK8LorYXLq5tboQEuLs6msOAF4BOOeH+nq7WznYmthZ6NJkdeAccqpOtMfwF2AnxE5y7l5uNi4Mp/IXX8EV7awPwvvDPAB8nN0dzb3fbqPCA1KSYuxlJABrDgr2jIwLhuUpJjGYHesDH4DRhZN5Jrq+tys5Kh3sRFRHA5QRHsH0hCvfzcoBgy97K0NPT2NNfx87xhqGuGhWGAc7htr/1nKk4AMZYu32nLaD4QwDGyEAVYvrAnI+jmt7GmCex4+MHg1crnjnUTHpWT3jUTHpVj/lWj/rVTfnWT/rXT/k3TAc2TAc1TAU9mWI3zoQ2zrCbZiIan3GKKuNz85NSE7kAs+EioIVYSYAW5dxQGg2RFiKaeDgkU4vLXom/CXz4cUjxBzD8Mz8FUGFkqIK+iKgOsQiYhd2P3im/RgCMxk8Y0/xOSvtXWT3XiofMW2fiZzZaREsNzz/wODqCybO5qT45noOrgNBZ+7q5rK6c+M3uHW4s7QwMLhSXt0UU9ps+6GGmtfzomfQbuBTU7KIe45qwPQ+tLPCWFXTUT+Rm+ZQhgoY957B/CSn6kNPwzul0BVDfHTw8gAkBeAPqg4cHc5/gQmi40bFRISmJURAWj40OyUNb90pYX29XdVVpONsHZgyYWETZXA20r8Hk09neImm6W1iYAzyWn5tFYz7Ek/DttHjpesenePao/1xeXhwfG4GJC+ZJK1MtsSky+JEW5GaBjziFL8CpGJlcgjjP9qjoIfpKPFcBRjv8dUwiHxvNPt/bjZZ0MSsYjJLi3NNsqKgItYT1BfjqkIWIahBOBJvr7URFwUiyMQUMBsPBQGMrMoJMnYmrdAV8i9nI4DkZGuiTtKaJHy14QfAkWAHlAjbGXxcd4CsviHbrJCJdJlYenNA77vpv8ugzUZ7mDWIpgxj6jkKE+L8RY1qol+z4+PD4+JBQmhKGnRMSQ0/tGtH/a/7TNqr+MhwZTEwAhACM4ZJ3lqZqgKPOZn3k0ckqRF6HWCzAsIGHvoinHsZYYQD5Jg2qdcRFexpRE2LgxspLC/ESHcyzfBy4tUVNs3i6WNEELi2MmWKT9bBB5G/kEnHm8XBMsYPK3Z2s9W6hAm7qXiJCfcEfS7pE4ORQ/QnS9OC1h93CVdpnNwhrwNNT3SdggLKSAnkI6Ccnxu+kJ8CBUTsWIGyqKC1cXlo8y0S8uDAfHRlE7WpgB3lC3ClnXu68DDH2UkOQ/f098EwrK8sD/T0d7c3wd3xsGB6kvd1deJiR3AohFDndRy9Q3AzXBAUxsE2ZTGJyLWRsbe3v7x4c7WztL/T2P83OSjHQ4eMfUzOV6MZ3z0LeIH8qI7bzjcjad+39vjUksyv8MJ0sltNV80r6DeI25BGsvxXX8kHxkFn/4sPzShNBYAdRJvx88nLuwWOM6dHE5MpYlIylAGBgHMJv2KNUe+rxpL1sPX4PCA0VN2IABmeR1XO1cswVTmRpe3DvcPMFTqHwUFG5fMgfoLXx+KhE+pyu7iZ2iKsuQ0yV7CkGwmymlpeCCz6K636DX+op0HXgNr8X1/JhQuunyW2/Tev4+k7XT3DdHvRpFAzrPBozqRxzrh4Mjc3XsHbhp7+osNDDxSo7Mx2iQ7jFZ0mnvCoGEwtM9RDj+no60CQZecXqztK7W2FKGR0ZBDjkZGsskpq4kXP/9nllJyRNTTDvbWysTzwbLcrPFuVXRMWQ4GFhDlfIL2BQJNp1JmlCwLBBZhGjqO3s7OALODX57Nzv8tTUBOa7kqRNKtuGhyGoOIyO9jXVZjJUUGFOpqNVX6Cfvb6GJuMKzF2krqmnC5kKeyyeWKK/r5t6d7i8FnoxHxOwgkGEgDoG4fbBlZmfm4FQhHSCbc1VFY/iYkLx6iQAcnmaF85q66VExz8K4VPb3xHz4cSx3D3nRzvEar4wM0HWMf4lMe1KHK4SSlPCsAuOK+eItv8gSN3qvSQH1d7ahImYGOpX0oLMic44agasPs010c+0McON5Opoi+V4GKqq/QgfXqoUV7jYEVeR6MSgwLAHWRljo8P4n1hlGKJkdqAHfn9keJBK7UDKg9qZiF3Aw3UFMP0l25oSMVyJGTAud4/DSbMzN9S+xmSq0JJgfl4O8tQW4gI58CuAxNA8bmOuo3A9w0nr6e6glSDC+dbXVsrpJiH8PbESz2tOKMzPRm27Z0vNjVI79MAvVpQVXXT66/Uw4aI48+aTtryhlYKKcfv4lo8vCoDxWol8Mj4zs7hEdoLxYAzE0/DXMfDrsPL3Ua4jpuWd3H6d7vm7a7vPLvT0Iazp7my7dzspPYULuN3b3U40KpXCCEIte0tJ4LYPF3QtpTbOhFWMuRQMGMIpPJkMHVkuW9udeNnuO6Yyw82ctdXlME1RKR8GB3qDfNz0mddFAZidpR6EzuFsH293W5gE7K30RXn5pIMxQz3VvGr2yFLV2FrF1EbDzGbrwlbvys7I+t7U9sHS3uH6wdH20fEBlWl6c2vt3p0kY20mNf0FjiDQ1+VJfbUk0dufgwHsxMXn1EIvedAU3PGiggeiGs1wYXOyb4+PDT+fUwDvkPvgLjxOog8S+FxwwZJADs37wDOJeNJFCUikwzZ7a4NTZE1xV5j0Or1TW17OPXwd6mpOS/wAP+rExI2IcHPdGzqCqh9yeZdFpsV0tdRuaV7OdrQmu9bj4iTphsF1xp3nCCTDDEDt10D2MPsOJUAakHJQW1ubgM2KC3PON/sqPogFvNTyZ0IENWFJ7A7JvZBweLycTfSd5OHo+1BZhaiEYc9z+fSOEImN3noZqhMT4yLwT50Pw9oFOa427nRJsJ7W1evXL1ka3lyrDgeU1ZHlleBn8iTDjWTpEHSFkWmxZmHzGDUbdjst/tn4KE74UPkPR0cGcSan5nE5tRuVE+YvqU8ap9Fg1nM00NzlRNLrErmxxzExDV6umQ5WMVZG6jwWI9HwJSUxSp7rg8sYILI8ONj39XRAa5yKiY1QbHJiPPNOMvVIfD3tszPT5UFQK8tL4D9gBqcW5kH0DyGCnJ5Suo2PjeD1QgjIAKK/NjVIz8GE69CsqwnRkfxbtjvaMB2c3X/j3AgVUHao9a3AnI+tXb9HBBsop4QAGLv4A27bW7d7vi0Ztm6fTV3aHnpRF2Rvd7e/twueWPgRQWgL0RVgM7EVXCjai40KAQzwnJOu52LNTfXwK6ap/bo6mkPw3VD3GAatklZAQmAHX8RdN4Bj9/b2IJSHcL+upgKCsOysdJimUhKjC/LuNzZUl5cWJsZFRkUEYEZZ3AHoZm/b0d4o5/yTk3nP0liXCoB9POwgOocp93mUM70K9rSxDjxXaLAXlY0D0BTAmILcrIH+HikFC3CF4et30hNsTnY+wxNelJ8tp9bIGW1ooK8wP5v2TOIB8Aw8TmvzEymVitNTE6YG6gp1eSXFR6Ltw65PAR3R6qqTnfHKynmWFqNHGk4B0zIDvDwT1/Hg4AE3xtlQU5sicqPHK8/RZqndcbAkmTlgtEqTLxsc6BWZAG8lxIZDILS+toqWA3Dvn5ebDVJ8OY/LsUfMRxET5sRC1PGBIkvJxwfEApfo+AchfBr4klgrlPvr+8RiwnHPOycAWOd/5aXR9pQTjhKGPV9be0S0/BuBOJjXeS3z5ErQO5Zp1FUZgE9cbyMy6yUg5OjJ9lG/eVmLoWqoc43P24HY6hFUa445fBpdEueQGmS+18AhIVkbWbjIKytSw/hqaLAPz/7goqirbkH+rjiZg4lZzQw1pUf/eD5lMVU6fAV1iTFk+us4OnqKHRxlZcijp1ehKTLDdIajQHn0KJFh6qex0SFcgySTwFfU5udm4VvUGkJnO5O2liZ5Qh9wGxCNUdmQ8MLhORYhgOt1cTAL8HH+OYgCna/BTaRKbBtoX4PbfQLirtYUjGjzlaMV4VHkCw0LpJzgu353f2Xj+h2fvR11Ft26YWSoAu9n9P2mfNzu2VrtwdH2y3mhDg72mxprI9i+mCgM/RDgx/VKPwAQ0aLsgcwB0e2DrIzOjpZTcBjgh62zvSWGE0QDY5kZaZJXcJYH+/vKS4pN9NRpbaj1tVXKdLckg8cSowta63KgrwuNtR+eAepkvrGxDiialhyDf8LdP0vXrkwDFCRnKtXJ1jguJrS5qV6UDausJB8TCMtZv+ftzle7VijvB1cMroajrZHoQu05Wn1tJU5sngMSHhvj2JtBgIF6HLRZqpqMK7b66q2+nmQtImCwJ0/kWAGPRECXNuBSgK+ntmw8PG10d9KOSFGvnjcEelx/cLjeIO9X9yfpTIaD/yIvjeHuALGYKGzJ4e/9T8jo92BeOcMoYZgittNHrJ0TS++0h+Bp/rMzyjTD/IinS0A7p9hCdKTQl2szVB3MNHbqI/ncG80xO0840V5GLE3V7AhrITyjtI3dDrG4cf0SfHelKpRoIUEaoDVj3Ws4AQVTHhY+9nSxokEO3NFLA0vSkQnmnIV5sMnbnUhIBAC2EMbu8ffxMdFCJYiiZSEAVmGzHe3N+E05p+P2tqcom5ecwAF8gl6Db1bI0+Q9zKSuiAP+rCwvRuteMm1mehIRk5zsWHBqqHt87o85eERJyjZKk26NDdXUGxQe4l1dVUaNb+DCzi73FfXZcure47OQ8/SmYjuErzGlOxrctrd4kly/IKWreXzl/AyYgFweXpiYXPFLuFTQYzmy8mj34NXIXsKlaGtpGujv2Vhf6+5qez2Srvv7+5jUR9Jwd7Y8Lwo4uIai0wIEcLTVpeWlRS4n2EYcJ62vp8OpGU1/VoYJ60QzGBGhvnDNMayieS5w0PNzMw31j2miiHDlwSMszM9exNEW5WeLFkYC3gZv62xnIvZEnO1NH1eWUDeCM66iOmZirba6HNVAAn7Y3lZsfQFwIHaLF6HJCTcFJwaz7qacyza3no072xqTWmFMFWu9W/kudpuRkeSK8O3bxIi8MA8eAMQTIyqBQB3n0Cl3tHmCTgON7VZ5v74YLxBx/k/EwBfSuRBPAL/5cKLtb0/stPUviXF9YvOJclZRwjAFDZ5C9DA9M5TNOC/P00nKk/+RICfmKTJryMsSg+WMYfh7O51CmRcvS5PFh9d+CnfTP6Z2fLXEHDdFzZUF8+sPT5LXz5WFGOpcY2ioeFizDhG5Yntsy10PHSafCcOVp/teX1Mp4E83pXUdbGys0xpIHGwMZaZiAHAiLKTDUvMz1c5wsAy30DfXvYnyY2JLED1crNB3wSniJmwplMQ0V4pyaOBmRoYHkCcL8nOV57vgkMAHh1KKiOyt9Esf5UtaX8TXBzzH1uZGU2NtoK8LrcQ/NioEXOZFaKoo7SzGI3S5LqoZABGAp6uNt6u9q511XHR4dJSfmbmqve+3TiG/g2Hr9Xsn9u98kr/wTPht5KOvkp58m/jkm6Smr5Oavoqqe98t5gtAWQC9bDy+s3L8QZ/Fz4CRCTcdNX9/i9KG+OHFsq2DBeX1f0msrCRfLJW5qaFGVcWjc//Z1tdW0mQ/LE21YNKI54ZBAM0J87c8qSCM+dkqSgufRzf/a/TrbmyoBo8mNlAOD/EuLsiZlqzNuLS4UFdTAR6Qdqd4KC5KHg6M+bnZyFC/YH83cPQ52bfBkUn/cOadZKqAJAAzAH4ry0sd7c1wqEgDU5SMhBsVAg6r5WkD7iOQk48K1wJkZylWJ0Ilqe/v676Ie4e7N430bk5PnVtz6fbmxnR/77PGhp2mRqKllejqIsbGCMVjMIJXyArzhmgRaQI3XE5+FGm2mkfqIFNzWX0fk3WJ8lcD7k8R85Fk/kDONrCDRTLE7T1Zgtj1z8S0m9wQTmlKGHYCFR2SystCYQTd89nsApdo/mP+NpczT+zw6ACGzA0cHhxwwk9MpqcoTUSqzXosNRPd6/dCLadLgunEGwCu4J1myj+bybax7YZIHzstLU0V9VuX04PM+TJiHXGZYZYat66g43F1sthemL+dFINqFONiQkXTXJjOHiIDmI67O9tk/MAP9rmcYHzKWkxVBuMKk6miI8KCCI4kNpqNloEBeiHtL4LSDlckdwn7PUE31/3MNNQCZGaoKX0h+fDwsL3tKW31MS2ZK53bA12fqakJ8Ig0wS4j3RvsIE9J3PrSUOtA39amUgbxwg2eTElRmqhyNFVDGTOSm+ipW1tqOjnoebibePuYmZnyZKMZAqFkTSGbgrOtWVdvwwkBFqW9NNbf20WtMgBQ1NfTeXFiPrMzU1RaHZkDJqWmxlrlbTqN016YGxsdKszPxj20tJqL1KRoKW08MA8/KnpIm9hRiaPMCvmykgIas0tJUa708j8AXRDcB/m5YqBOpQUeGx0GoE5DhmKxmczLAiATp/tkenCaYSIKdqDHBRViYDIwN0fzl7n7EWIGcNblpYUAmMm20vrHOzs7UuIEuWx3hGj/e0H4+kekLO1O9wWew9E2MRtEtP/nEwCs4x+IxQSlFrMShp3pySJFwYVVrf+KWCuRtU7SRYzeIBaTiH2p3NDj2oL1iX9LLN9T9LCwjBV1FBU8UCxi4HGn6jJJGLb2OOwETSJt8Fq/yKxXS8xydVigky5DQ0WHqWakc22kwJ9PXt8Zf4dtoalOwjBNxpUIC4MRToSzlaEuSw1AUc1j8eLugMRggM/ASEmKdbQ1y4wzYNrlMTSSKze4bgeTCGECfXCZ8iIZQXsb4ueQrlUPUyTsi7ayhfiCZeD942Nw8/kPM2kL2IAkwV8qWpmwsbEOh5EYRzY2JMSGK5vvn4ONDA+gBgljvVt2VvoQ70K0hLOvYoV9aMyBfH1qwRBLZQG/cUXLfpT2/K2jvbm6qrS3p2Nr68JZ9cfHRiTxMWCEADFodESgnHoYSpMxu66v1dVUitXedLAxBCclDcvxStFiOEFU8DwlK0uDGndFxc2antTu78uo0IGNo7IXQGX0UGVrq7ggJ9jfDesa0waXEyyziRGgF671oOlWyzwpVKsPsHZ5afEi7hQcD8bMAG9ek5D06Eguh74/SXT+kwAL/aejlYKLPKYtMuKl0tDD6PkFmW/Yn1LOGEoYdmY73ic5YVr/RpjV3ZHciLU/S3T8F2Ep7azknP7RJjHGFC5UzCvG00pl16CO5HiO/C3XOzvbaB2LqaES52ssRgpMgMH2nnDSgsydLDR97bXtTTUQHaI2Q7XlrgdJ2kFWMHLXqsNdLBnaDJK81cWQ8dTH3VrvFuJ1dXe2PJdGcCmiz35eDvduJ9EKG5oaa/n5/Vj+5cUkRTJVKalOV5ThLVOciDNsPDzEm1rq7eVm09bSJLNetL+3ix3kSStnio4MAii1rkjnDATojQ01cAy0NVfl+vfzMbj+E89G5+dm4Je1v78Hf9fWVsbHhuE+Tk6MPxsfhRcQtRQX5jx8cDc1KRoiOdFSRrEDbig8SDTmD6Upje929vdKH+XToD470CMv5x4AwrnZaaSqp7TzjAuOj3u62kODvUSTYz4ediEB7tJToDA5VFU8crY3rXlcJk9PFMChnu6OuxlJ1DJ1uOOwhYhQX+mFEgDVujpbpTwDAA4fV5ZERQTQBDxhdrKz0vf3dkqKjxwZHhC7BQg50IfB9ch/9fb29sBlo1PobG+5oHuEFiKRksS58AlT1z4GB/pe6gd06ynR+ldkbDkbQMalFwXANohJWyHeQ2P4CrHZoICYmNKUMEwu2x3g568Aj21KZgfeHTzxOLb8WzIvLC0A/1yYZ1vKkP9waBWJdCQmd6srqp/RI+WbVdozPYmueDEwrINs+rp+/ZIWQ5WlqarNVNVjqd26ednbVovk80BtYx1xU4+CUGOYJuNKkYv9E283dc3LuB7gXOIA3M5L43y/nRYv9vMQE+OFOrRqCIEskkoLDfKUf78z05O0Hq0AHyfqitTy0mLW3VRaGWFBbpZMUqzZmSmaZhqMsGDvlmbF2lhXVpZLi/MkNWFfUNm90s5ugM0eFT0ESAY4XEp1mUIhjtJ+ts9SRVlRcgKn5nH54ECvMgf+fGx+bgZQiujc6+JgCqhJeh/XKTq6hwb7PF2tRacITpj/2XufADreu5MsaSJyd7Z8mH2HWj24vb2FEv6AcxTiVa8TNI2zAz0u6L7ATwDLoipaKCQJzgJ83N3YSEuPR0se1VWlL/WjuTdKbFRf1MYPFohJ6xNSzk//kBi9eYF7vDAoSeyNK2HYq2OruTJg1dEm0fvLE0hswlTGWsKwCqU68S4hHz8HFZAE+7slcMOpdBfwjpzNneNjI6gwQEtT1dmCMVfOJjpFkFgr91lxkLnBDU31K0wNFc1bV4x0rvk76Azl+/MZFNtjN+sjY32MtRmqiD5oONB/MjgQt2yFh3ifS0wAWC4y1M/GXMfX0yE02AscBlwHKVsGJ+HhYoVW9VBd4sb6GjpfR1sjhYiDqT1pAvWwUdg1eL7SR/nURUQ/L8esuylStMXgLCYnxltbGrlRIdRGalsLPdhLe9tT+a/V/v7e+NhwSVEutZTRwoQJvi0n+3ZbS9PTxrrO9hY5SYeV9mINHte+ns7K8iJ4yAN9XeBXDAOe87yHmT9nUV2l4XkDQnCYNwBrKSHWy2abmxv3M9PCgr3dTvaIWpvr1NVU9PV2nWM2Zm9vDyaKvJx7UREBMNtT5RAA+HV2tJyRhhEmInAcyfEc8LOiLC/wZkP9Y3gUtzY3MGOWt7ut/NsHwIYKTMBvdnW2XtAdgaiDT81vZ3ymosfVVaK5mcjJIVJTR6IiQh3MtViquEo8Oyv91OITYg0ClZAAdwB4L6/X3u4iM2xd/ywkoO95mxi+RKxXnHXLxwfEdhuxO/z8zuVwlewhavlzYinldZqOXikYtlZCHKFY/Py82uE6sRBzokxWJjH9M0Phh0euywefhql9tOADVleW/b2dqNPlw+w78lQxYeJ4lqaKpeHN8kQnUhmshU6N2Jvj42OnHeNllB5szusH42IMtlgV6munhcg5tJiq3iZa25zIo+hoL2OWFk+8CyDiOU5VAKVoUcghmISiR4xXYWo75hlOJCqUI8IC06iKDLxIf2+Xt7sdtQQRrn93V5tU1LQP/tjTxYpWPuTn5VCQd39dQebuwYFe3H+MXT6E7OdbfaE0pSntJUHpsVEh8DM/B/40pV2YwSTf2d5CU9kmV8eMmQ+yMk4hxQu+PutuKsz2Yv93cWEekACtc8zShAVufWZ68uyns7622vK0QVIjnJ2VPiagkv8xBiB30dXyAGbw2mhBbtapVz6Imhq+NmlcXKefF5OhonVSj5SkL45mn9dh7+7uoEsaEepLC2leipWXnR5i9BZZuoXj1cF/kVYdphAA26zjCZT9ITH47XkG5FIMgv+etwT0e3pKGPbcbW+C6P+ULJwdYxzvX4CQ3EYNhdjjT2WsExxtEkPfUxJolrJ/DjtC0UMY8dwwgkeOhOWG+XG5mXbpo3yZv3xMqq7NUGVqqJQlOB7y8BVfrBn1jAHoIokTY0hexFYuIu0gOuMGcv1sjdXhWwY8DMZkqowFBRDcWCI2LtfZVoNBYjNrU63dnZ0Xcp9hLkPCx4CXULF+fW0Vv2c3RwFmlIWFOVsLPRgTz0ZjOEEAeKhqMKYGJHeC9P63nu4OUQDm4WLV2FCt6AwLmDYhNpwmCwuOcGlRyV2uNKW9tnZwsH9eymNKO1+DOXxxcZ4qA1DzuCwlMYoWstuY66QmRcs/UR8dHXm726IeKnDTjQ01Yp0FPBidHS3I01GUCRiKMsgjGxsdpq1Rwnnl3L/NDvTAZX60IafCGBhgUfSVyFC/C7oX4KzdnCzQXoz1bp1+2aKqigRgMGJjV8LD7PU1IMLRZqliimYsvHZeGugQRcjUNX0xeGy9jBj+iUx84TC1+/8hltLPZ+NrRaS0NEZ3nf/H8+grOz4kRczQHtv+9jWrS3xFYBguBSSfp/+LTE2eN9Ampl0pTDX/IOM2H20Q/b8SHtLoDWJLWrJ+eWmRWsBtoq+OSG/hJwozL61OHUAaxPo9Xe2SluL29/fQXI/461maqm5WzMIY+6a7Hh1ZXgMPfcnkGB+ScZFEGACziUdBt0MsDbWvaWmqIjl5O331Cg9nUkgeZi4udySIzDvpggvRulZRlHsRnk+uqS0/m884fy+V4BE0ISwkpygKts3Nje7OtrKSfGr9J2wqgRsunTv4aWNdRKgv1YGBM+ZygqurShUiLoPz7evtIlm2bAxpfn1yYvwUV6+9tQk8q6+nA7hwZSClNKUpTWmndkaAVUS7spub6iPYvtTic8S4kxzPqa+tqqupkC5fDvjK3tqA+t1AX5e7GUmY+5dqW1ubbS1NYcHeNFHvkeHBleUlOU8EXAziuAcMCVujEY3093YlxkWKinNERQQA2pEpRgf4E/WSwV+FaBUVsuLCHGqJyim30t3Nx2Bc7lxYqL+pDuAuAGDOhpoN3u6P3ByTbE0RA5mlmfbGxvrZD3twoBelwlwczHZe0LK1GKyylEKMsU402nT/D7IHbP/MIhxHW0fLuaR2bstfCDIWf0yMqBGrBRd/XvvEmKYAg/0HYjHxNZuOXhEYNq574sFavnMhe9kbExIn9n9KZr2kPZSbvATdHwifyClnQc0k3WiEEFamWgCxZmemEDJZXVkW28ULs39lefHU1MTQYB9tEoTplVoFrsVLi6EBqOxeqGVLpudQnt90SdBBY1Rvrm+Eu4Ee66qm+hWkmAx/AYb1B/gS8Qn8yYtcQ4q7bW/BYKjoaV011VdfOI8aCarbk3MJCvwcgkCGOtcBg4GnREtlpoYa8rDkY3eYl3OPSuxrpHsjLZk7Mjwg6SsAbsEH05rKbC30bqfFyxSKEbW52elwtg/tptfXVipayoisqbGWSnKdFB+pDKSUpjSlKe0iDLxtcUEOtYACD2932/bWpvU1iQvBWGRFVNTrSb14OoT2tqd+Xo40ze6iggcy26HBq9LED9ydLUUbCsAbgssQPaRgfzc4F0nLo3u7u6HBXuiTOfdvX9ClhiuJgauJvvrQ6fgM19aIuDh+GBPDDTbTVde8zFto1lgJDyPiEyHOueNgweJplga62x0dnoPoGS5lkiSHcxo7XCdW7pOKXora/ixZwzX47Yk4ue1vyaD0XFJVe6PE0O9PbLz3vdPXNyqqhwa4C+93vfL1m3BeERh2tEH0fSS8E32fXFQ16myQcC9jWmQJrBTbHSI6/08KEvuD/aVisR/Ewse47hx+wwA2gvxdITQneF28eQ8zaRzoVPErCOIjQ/1yH9wtfZQPaKGzo6W7s01UnISXHyPbxgCPwQsT3euulgwdphq8g3SZUT8YzEePPZwBdwkxGC+V3+TtjvgSNRlXOjLvvJBbDT4DLgs61KeNdfBOpkCRua5Grl9gV2crkoSiLkmKXY/EzgxukI+HHa1EpLK8aFNx3Z6F+dmUxGjqeqqzncmDrIxT8GKBIwS8ff9eKu0WP8y+QyhNaUpTmtIuzObnZkqL87zcbESdrIUJ8056AkzpYoXmurvaAOGIfsvUQL2jrVnsciR4vaqKR1QCD0RWAfhHSkkkfEtU0xkOWGwpbEd7c0JsuJHuDdrng/xcYS/gtmi5QThBfBibF6ZfBzgQH0liXMQpt9LcjFNhs+xgA8FCc5efN5GYNM0ODjbX02Kq6GqpMRkqzVFhxJnpNFZWllGbiZOt8Zlza8dkedd2BwmZuv8nGUlO2im2ge0uouv/Pkn6/WckL+LexDncoeN9YspJKAFFKoy9SWrnHp2WPmDWj2j9a2IuVIEYfuQqf9ezga/lVPPqUHTszxCDv6NoHVwin4+LsNU8komFz8BxVcaHt5qoHZDHy9ni1zgODsTmu/gyWdxwVOoA03pxQQ6VRkLKsDRh0WjZRfEYORkxVOEFegf+CfgqzEI/wtKg0t1pmxPJr0jkLyPFbEZGBJvpMhhXdFhqzkbM48UXwx4xNjqMLkIE25cgiezH0D+T4znS7tvKckVpYXREIFXcCf4JqFVSIg45vwCfE1wpEaG+DfWPpcvIiHkQtjbLSvLD2T42vBIRNEKDvQDLrSqeTCN4NQ+0Tms3Jwt4PEqKcjdOlVK7UEN8KsrQ7TlcZ+VFUJrSnpsdHR3293WXFOeCM6K1WoFX8vGwE1uQtru70/K0QSybvJujeVJ8pNiyiKmpifyHmTTgZ27EYAd63M9ME8tSONDf4+liBQdGa2MGSCN24Q+caXZmujXFSeFMFJwL5qxqqHuMfC7ANkBKF3Rt4dLh9go4pMXFU9U9wpSYnY0revJdbDV5nWBRloYHUVF3HCzNdG8A+oLg56bG5SxHKyIhgWRTPJthpjSILs7wbG2QMspD35HtVQCccHA7JzeJyGYDqaLU/K+F3x34gtzmznlo3hwskrCHyiXe8hfEtMdpknXI1kqIUXWS1QORhRByg+H/n73vAGtry869k0kmmUzJTCYvkzIpk0neJHmTzPW9brf33ovbtXHBxqb33ovpvRdRDcaAKTY2Ni5gerVE7733jhASQuetrSOOjoQQkpAw4p7/258vF04/++y9/r3W+teYB1J3nEmRUZmcomEqHRFZWNenwj7R/bWqfGIL+UJyNbndCg30D6Ks8+qWMW+3s9OkUCZHWyOiKuJAfw8Y9N7u9lvl18rSdDf95orWSTMdjVuOVvXe7uY6Z09f+Oahi724Q4wWOx4SZKt//go/tzU9IojNfgZF/WB0xotsAtVcWlpEuh389DmYkCRq+8IGNVWlYmwKxncpOVQwuZaVFIhxY34UaL7cg9UaR2y6hRnRzcmiTs5iYuTF1NBAT3JEpb21QcGje/LqGlPC9xQoUKCgLAwPDWTcSBKblxNoYVLKbDKZy8DHiOg+4YxvY5RxI3FwoE8ihasoK4oM8xVzjqGFyITIzWEdK0zm2OhwX29XdEQAmYwZ6527lXVDoq9mfm62qPDB5quyNLkMsw/cJhGQmZWRorrnSeh/QIuLCVHwKBwOlpzMN2BiOFFRVw21cGUOf1PtIDPd85rHtLVOwW90L5/KdrBai45CS8/DO0q4AHsDV48ERq34uhhnXCgiLwwj/FtsyFSm3eduY+0vI5tT6Jk4hi3cV86LWe1GBKz5/4pcW/srSHpR0fVDVGi34R+ER5uTtzTcfjZm1K1u2GKBiP7mosrq8Y26Cs8y6iytE8ze5HZ+gxYhZq5L+3S53KqKEjD0pXAnsN0f5ucCH+vsaB0a7IcBNyUpJjY6GKxwK9Mr0KQ7yoB6Xbx4HEYcPAFMV5SD+ZhcqfJwvWlvAWTsitaps5rHit0cxWkYfz2pzN0ZjqPLD1/09XTEKynvJtirq4SsRXtrE/wmyM+NHKZIxsT4qFhJZZhCHj+4KyUVraO9RSwEESZFYGUKeJngNQX4uIjlWA/093IVij6HKfNafCTZm4fHpcibnDYzPZWcGAUkE56b9FptFChQoEBBdsC8TIsMJM/F+CQlHQWP7okpf+ALdnCorbQBZ2enwR4gQvSJyMaCR3lbaUI0NtBjIgNgG+HaoqVeUWH+VpN4PaN2c5wO+daA3anKlFuYJ/NMD1drBdcN2Wzs2jU8nGcpPMxS9xxYO/p8m0eLz8cuXjxhoXO20sMFi4sXhP8M7Shar7+vW17BSQlYm8LqfkYiYH+DRNg5IzLwkSVsIlTEe1b/K7njGLfp4idECFjjb7C5LAWjzzgjvIlQXstBkQN2f7GVjMJOwONxeerpLlPD8s0rTVjnh0JJQ+ZTOUk1C2N1YGsyKBGNBwj7+lyWsoaehrrattZGsNRh7E5JisaViCTGkbs4mAFti44IuJ2TVllePNDfA2NiTVXpzbQkf2/nq86W+fdu3bmVERzgHhLokXKNVvbkUXGAl6fx5VuO1m1+PsC7iKIZQMwc9DVNtTU0+QGHMDBdNdSaCw8VCUok+cQeuNhp8sXryWPN8NCAcksfbv058eDJ4GcP9r8Kv7mREkdIPBGbjY0OR4b6kqcceysD4GlS9AxhAvPzciTHx4cHe3e0NW+rGSWGkeHB+3k5Ph4iHsvUZJqUDDRpPXqFeTP9WqCvK7EGCYdNTY6F6RkYqbxHq60pF2P7OywPSoECBQoUyBga7CNEDqXolYvQqplpmMRhPhVbToX/hXnt8cO8rZQ/ens6k+IjjPWEOQh2lnpBfm4tTZLrXsJMHU8LJZ/C66ptbXU5my2hrPzqKquxni62PaFrpbqqKouLC2Tjx9XBTGFzAcvKwmkYMyLCw+jyec1juPsL7BxHA80KDxegZ6gqz0b+GBDcnVw5oVNfVlKwo0ewcB8btsFm01Cq1dqEDHfKRTGBZD2Chl9jUzSUsKNcDGiTZMDPyUQOxc3sFVSPt/M9rO6vRAgYmO4Lj3ZD4J6iYSrHOlOoCVP3SzmEE5drUBIk/c8Rf5sI2n77idANvvcv0iXpFcbc3CxRP15K079y2spM29PNxtHGCH4wNdD08XAAIgcWvMjC0ODgw6uOUZYG7kZaRlfOkB1iwL5Qqhhfn8PL+MoiGphoEjgYfzhbj4mJtTS8uMHEgA/AIA7XAPRPSgCGEjE40IcLlsBJYVLp7ekiQtjptZU1VWV+Xk7k1Dhz40vASKW4s2AvsVQrIHhIg565LO+1PSnIJ3M/nKk21D9VpCOvr8OYTk6zhiOnJMUooOdBrIaSxfHhwlqa65VVJmXfgHIPUqBAYefDSFVFyYP7tzkctlw7tjY3JMaFS85N2DqQHsiYWOC9gfaZkECPrZxp3V3t0eH+ImnPwd5Scp6bGhhinjc4fkJsmOocYuSq2TDxyeJUlIyyMsJ0AcPmpoOlq+ElMHiu2ZgwIyNQyA95uTk5GdvBhAhTNnHZHW3Nu9fbgAt1f0UShPtTbNBIJvKmANgDyOKFNhWnyO7AKlsPisdbtj6PktYo7B8aBuDOY/2XhImDskUnrvdeEOkZ21Y84HFRdWY8DLL+V6h7qeL74rCBDyDHDikdSMYGlOxm+rXCx/dvZ6fduB6fQAs10T176dKJK1onyRwMqBdwKs2LJxz0NRt9PDlRUVjMFhxsY8WIzQ+zxn1idpZ6QPzgBx8Pe6bKRJPEQLCm69doMPbBjCKWhbyRA2YL9z6x9ewCPASIDXkXdxcreOAK2OIwEdKigsgqKSlJ0TCnKnB3U1MTxU8e+no6kuPys2+mjAwP7uShRYUhFV0LOFTm9dnZaWqA2+qLo5gYhX1PEiR6PyjsEbS2NMLUJhYeD8znZlqS2OoqAfg9zBpAjcSWaGOjg8tKCkZGhjZHT5SVFMJfCf8bzOMwhQ0N9m/FMSrKnogVMoV9067HqyKkAmZhscw0+I0inXZqCouNJZgY8K61yMiRoABcv17ctnn0aCfXDDYAbocY653bpTATVhs2qI/qZREaBMDHlopVe1IgeFz5fYbzecgDJkbA2o4iFX6MmnD3Hw3DIWRif4HNZmy/fZ8oDWv4e4wrQymqyaiNFYgfoZBIlWF6anKgv6enuwNs6KT4COAhYOhvztOV0nS0TuluShjTv/ydn4n2Q1f7Jh8P5AQTWxzamonBQGavf0H3skDpHliixJRiFQHmFXKZrIb6p2Jiu/BwigofSMnCmpwYy8lKFZvk4NnKG4KI8bOtxBKg01MTFFN2gvcbFxOC01qBXy7AnUGvhqvd+UNra22EKbOjvYUa2ihQ+J7TsN2JXKCww9fU2dGanBhFnqe2cnARaGmqhwkILx9M5ksONob5eRLkmjvamhNoYeSSOTVVZVvlYjGXl+DgjrZG5INbm2nn5WYODw0o9/ZvblSjIVpJkUI0qbZWLKJHcrBPYiK2uCN9+dbmBmL9V/VreevIDcD4S6HJ2vxfSMp7D4Ldh/V8I07Aek8iBwmPCsbZ3zQMAN1U8NZ/gM3f2WZj5lNUU1zEIXZHprN0fy10rbJ3j4osLi7Mzk63tzVHhvmGBnpev0aD8drMUNPWQtfM8KIsfEzr0gl/E200JAH7io2ViYCR5DqAiRlcOY1rfsAZd+irkQtM5nJqcuyjB3cmxkcf3L8txsGC/NykTwkP83PF6rBZmWlvO71tBofDgbOTFX59PBwYCqkgAm1LiA0juBzMhfA2Cx/fp4YhChQoUNjH6Ovt6upsgwloblZyXnp7a5Ofl5OB9pmUpBgZFwrn52YLHt3DVfvILS4mZH5ewvpybXU5eVVXuhI9i8W6dzfbwuSyCBkz16HXVkqpXi23hSMq1IFP022tjfITlnXs9u1tV5ax3t4dXvDjh3n4dWbfTFFtj5nPRSWShWLxP8aGTBTxUKl8IYGLTUYizX1hwOSPsD4NOXKFKBq2H25iOn4jT+wX2+tgcudRwCshndn8XzIlIHLGsAG9jV1+L/fHwJ1fH3bm9FkoLO1IXrgaHRlaWlqcnBiDYRQYQnS4PwxkwJEcbYwiQnyyMlIyb15vKi+5aqajzS9i6GxwcS0qSj4Cxh+zpkJDAkzRQcilq8ZGh3fz3ZaVFBIyiXgkQHpqAsxnUnapKHtCrp4Ju9AiA2tryhW4crHiXW5OFiVFj9bX5VbjaWpgXIuPtNtYvIS5Df5X6SuLFChQoEBhT2FlhRkdEUDWoujuapfoSFldZSkwKYBtUFNVFh7iTSYzzvamVZUlmwUSJyfG0ZomX1lqK4UPMmampx7cu20n6nbDUyGUVdMZnkZkqK8Yk7x+jSZ30MrSElZcvKU9c+PGDgUScRCJYTvV55B2I8VYz3FRacHPUX3nPQh2H9b5LulS/wTr+gRbKqe++u8fDcP49d2EFQmyZViEeYw4G779oKGsZxkyFezS8aYcgptA4dpf5S8SPIf8y3B5ylbVXFiYm5ubFROxjQ7zwx1ily6dqHB3EQZPy8bBsBiat/GVC5rHxMZHxfNo5UR/Xw8hRUVo2kp3Z7HZbGKxCm/AxxRTL5yenszOvE5IccAPpcWPYZqUb52Ix4O7gBmLHM2YnBglPZpxemqyo62Z0tWgQIECBXXH1OT45nCVqHA/5S5owlwDtCqEJHqBZ0HXVJVu3nigv6exni57TN383GxqcuzmOtSFj+8rJTBvfZ1LjpkklIcVqfsyOordu4dqNBPRienpGIOBsZSgkA5WlpmhJp6PNzTYp/y+sjaF/EjkmkxtR1HZ4j0I3io25omkGoWXehhbrqS+9+8xDcP4PrH6v+F7b3+CrcigmgA9pvUg1noAm82UueexsfbXBH1uQFfWpMPZm+Ihs7uyWkCvrcSt/8uXTjoZaHIiI2V1iNFoC+FhidYmuN497gIiap7czk5T9ZWjFbj7t8khhbYWupXlRdIrmE1OjDvbmxK7ONkaExWx5Z3PHubnknO3nO1NFAiTAK4FJJBMwJzsTDbXPRObsO/cysDl5mlRQdTwRIECBQpqDZhQYPoI8nMT06k30T/f3dmu9NMx6NV2VvpiAfwwFcq7hrgZzU11MCuRdfPxitIK1NvcjJUV5lVnSzEm5uPhkJ+Xw1KAQS0vYxMT2MgINjODcZW26l1ZXkxoWiq5niqPi5xgHe+IeJYG9bH15b3Yp9dXsL7zIjZtx5sy6Syo8DPjYOxBjN1P0bBnjWFbYTHyqRiVnII7jxzEgrLir8vE99ZmRGJ88R15u5E8nRgXobfhEKvxdJPJIUaLXYmMcDO8dJ7vB7Mw1urt6cT41SeJCiQ7mZCkL57VVpcHB7iTFSOBxhQ/ebhtMHpnR6uHqzVRi7meUatYiTOYLz3dbMmFU+CSttKt2gpM5jKQRiBdZE64VdkW4snk5qQTNw4U9AmVM0aBAgUK+4WMjY4MtbY0xsWEkGsrS1+YUwwLC3Obq6rYWxncSImTIiksI8ZGh29np+FOISJKpaKsiLtjtrO8vARzPbkUJ96AnklPY9s1pF2PJ7LvlHnctSms/WURE7HnOKqutDcx7oM1/U54qS1/xBYLkJrIswJ7COvXxJr/Gzlg6n+FcYbVbnDYXzSMu4CNuiCtDrx/TMer5ixzqOcJCNVrMkUYzmWh+mbkz2wicBeeB4yY1nxtiUsXTwSY6mzvDYuNmw8Pi7DQx3XqgQIR4eNALeytDXBmorA7fp0PiX+C+UksoALOHuTnJouoRmM9nZhyIkJ8JicUkZEdGR7MzrxOdsFFhfkpIDkIEwZMeOQYfekEjJikCRoJz0EpqokUKFCgQGGv8bGH+bnEHAHT3FaiHTuf/ZMTo8VYja2FLlAdBdKbxdDd2U5er8RnXqXcCMy5XldtNxenCfB1USy/QIkg5migncpjEX1I+I0sxTHqqjx5dx7ieMo50ho2cwNrfZEkxfHnWOsLGJP+jD6kNVT5uuszEdO67ZA61obeXzRMwHmyUf9Aih0/w5ZKVXIKzggS6hCsWxxD9G9bLBVjLf8r7C6Nv+EvIagcrc0NQC20tU5Z6p6bCwvZsmQzLRZLvTF2Kzvqqt0FTUHV5qrKEvKhiOjwGylxyro8LpcLI1pCbJiB9hmyNm7a9fiB/h5ZjjAzPeXqaC4oTxnivblwyrbo7+vOuJFELO+ZGmgG+rrWM+RefltaXMjKSCaO4+JglpuTLrsn7UlBvquDWV5u5vzcLEaBAgUKFPYpFhfm42JCjHTPuTlZLC0tqu5Ew0MD8bRQIqcAb/7ezgqIBosbQRx2VkYKmS9ZmlxOT03YeTUtmMTxNV+xZmZ4Me9O1i6LhBGYnZ3GRR3hrY2PjSjnoEBsGv9JJVIcwEbA+u3+CpmarLYdHQqI3EwKyt8RKcd8gF+77FlUA4NHNKgv9IXgrel3yO/ybAMjKRomahGXYfV/LeDrY54q6SsrTcKCetAj12RYB+IuYt1finQd2dPSdgA8Y+rSxRMRFvrsKH7SKnKLxQgJ2M1MZlNTf0uTi5OF9saQV/BIvLZ1dWXpxmioqZR1r4b6pz4eDuRx1tfTsbT48aI8seZFhflERu9WrratMDE+CsSJfAGZ6demphSpTA+Py8Zcl9BmvJ2TJi8hhIun6vxQoECBwvcEYM0rUMRSMQrBeFolNtvm3srY+ZE72luc7U3IhzU10LyREgckTeFjjowMAdUhjiZGxgx1NEICPcBO2MkpFEBtdTnhUWQpQfCDhwToidAtaGMeSiJgfLdV83+TxAjKFD/afC7W+C+i5ZiPPJtqYHBGXEOS/kPR6zmEykavs9R3HFATGsaswxYeyJdPBR1R2L/dVXJVnGGkzimQWzSQzdZeEYp8IE/aN4r3SBk35PEybiShOs5ap3S0Tk0GB2HxCWtRkbzoKCwuHsvMmq6pYs7NlTx5aGioqUtKx8J3X11lNdbT8akCRj1CurC8tFBxAstkNjUwxAR2Pd1sYJ5QIFgCzwqD65FrX2B6wN8sjLWIC4inhba2KLIQ1dJcHx3uTxzHz8tRJRpKFChQoECBgqLgrq0BewkJcMddWI62Rko57NzsDPAusejHqDC/mWkFw+GIms7+3s7DQwO0yECxPLcNAQ97ieXRVPToiEx7BxtDJegYs1pEuMSwtXIulNUqFC9APqJ/x+bvKXio9SWkIi7mcZqMejZRfwv5iG6Jad01/19s5rrShccpGibRbG9CuXfw0Ls+k++JT8djdT8X+MSGzGRyWMmLtUms6d9IH5IMbjfOqLA/DWgrctKJQF7zH7H+yxhTpsC5tTVOSKAHDB9XtE7GWBqWebi4GF70NdF+FBpQ+TAvPjbM2sZQZ6M4GIx3FWVP8B27OtsiQnzgl8C+cJIDBANfnbK10GXKXzZkfHy0tqbcydaYPJjGRgdXlhfvZFyTSzN3dnb60YM75EIoMC0pEIKI8bUZyc40d2dLoHaUR4sCBQq7BqUohlNQd0xPTXq720eH+1eUFW0bMnfnVgbMekWFD+Q6xeLCfFVFyVb6w50drWAnkGMUrcy0szJS4MLkOgubzcYT5+BQxMJo3p2szdlieOr14wd3d8Et1tHWTOTyKUcseipOSCf6zitB4mKlHhu9KlSQb/hHbNQZ4ygUIMpjo7QrchJN3S+QMsfa5LPp3FM0pBtJJmC9Z1DNaO7C/vh41YGGAb0hnr7sNb4EI0cB1vB3G6GDB7F1pvIvj1mHSDmRJyaLb5Tdz+vTWu/XUeQjgZ5X/6sNjZr/kXFlYmiwHx9ELl86eeniCfgXmubF41qXTuiKDmpE+UhgXFamV4jqW8RkT6wJlRbLUYqaLx5YjEuxkwuPNDUydq0fra6yUpNjLU0uky+gulKR7MHp6clbWTfIalG3c9J2OUaCAgUKFCjse6yvrw/090iXIgSORGjzmhleVMXEevf2Tfz4KUkxIyOSSyE/ralwc7IQi1HcnOAgBQx6NaFWj/9mbY1DLkVjqKMhRsbAPunr7VLpKyCvtyonOW25Gut4G1WU3XlZMCYdLegz/mKDMv0cG7ZE5WoVYmDI3iYHNOIOBtazFEfh9etuSCr8KzZojM3f2WcfuFrQMCuRPjGTKt/u7D5UVw7ft19TJcKacIrGfxacYtRZtT1yuUbkacgWb8lms/28HDcvJpFbPC0UDz6cGB9NoIXBUE74x0ZJY25zUx0hQSFjHGBbayOhokGIcACLU0aAtUyAE5WXFgb4uBAXYKCtcT8vB8ih3K96dfVmWpK1uQ4pnNJWFeVfKFCgQIHC9xw8Hg9f+gzwdZGu2z440EcsMoYEKD8Ro7+vhwgOhBn88cM8iSuPy8tLt7PTxLK5cnPSZcyCwwX9jfXOE8yqsryILC4CDDAsyFPMetG/cvpaQmRPd4cqcu3gNgmKa6x3TuFgS+WDO4v1nUXiioRB2PBrxaULB/SQ8qFYaaXJyGd+l5zlLmxAE2v6rUwFoigaphKwh1ARsMbfbHD9X2IrzXKa4W1Yyx8Eu3d9hg6odAA7IqITB/V3WhaM1YKNOGEjjhh7QPy7Y41hjaTK5fV/jYrWyYDZmWmykDo5cuDxg7s4kWhtafR2tyeyY6GZG10iYhSJWcHTzQb/K712m6LpuAw9OWocjl9TVaoiiV6Jd30z/ZqjjRFxAY62RkAyFZC+rSwvvpV1w9vdjjiUnaUeTEW7k2NNgQIFChS+hzSMkNaAqTnterwUJSqYx935RZBjo4JUcTEwb5KLLLs6mN3NzZydnZY49UeF+ZEjCV0dzctKCqQH7U9OjOExOLAx/hui6A7uH8NTIRYW5spKCglNLHKDuf5GSpy8kZDSAcSPuJHwEO+dy/0rB4sFIiqLDX+PjftjTIW8oNMJWPfXJCV6sJM/2B0pbzmwNytZf19oGA6gXoRG/KiL/FyuH2v+r43Evt+jVQTZAafr18QW7m/TD1Z7UdyjMNhX4XGXI6wjAZ/Z6iZv+6AhRv8z4TczoCfrYkd/b7D/VeBdUeF+hLOLWDZjsVhkyQq8drNEWdvaGoFkkLuL1VaC7PB7YHfk+AFbS72Soke7ljo1NTle+Pi+LSkHDFpyYvSy/CltGL8aGPk4fl5O9YxaBZxpFChQoECBguzo7+sJJhXVjIsJkVIHhbu2xnhapbp6x3DqlKRo8mxobnSpqDBf4sZ19Gox6Y6wIE8pa6CZG+IcKUkx+G+yb6YQASxExgSOxgb6VqE9dpZ6iXERXZ1tkxPjO0+eJKIxZVl63j1W0n1igzX9GTI4FVOlB6t10EjEA9b4D6vj6dRHR9GwLTCfu+FuMlZo/GhEUpv4ETo/lLXYNvOpUFS0871tJArZA8IUyekkBW9zuUrUL/wqkqwR42lwL83/KQwFlqdw+NLiAgxM3u72xOiGK/txOOxAX1eyl9/eymCrVSXgIfhmTwrEx182m11WUkgOEHe0NXqUf0fGclgwRgPD2Sr0XEa0NNWTc8Dw0V+BSinwTO7n5QT5ucFT2ohJOJ+TlSo9TJ8CBQoUKFBQIqorSwn/j1yJ2apAbXW5O8ktBtZCbHSwxGi9gf7e69doZDJmqKORnBi1uSwNTPp4ujX8OzKMYnxgyiZWclOTYzcfvODRvZAAd7Gcc3Iz1jsHtgqwQbmq4JAxPT1JRNNYmFyG/90rHWKxGOt4E+s9JaNU2ya+PotE7Jr+Q0SHY+AKttq5KyRyGVvtRt4RCupXN2wmFQUochRNkeTOYR1vCZVk5u9uvwu7T6RK94DuNtsvPBLUE6P/GZJnVKCawfoKErIXkYU5LYlOlQn5YesL2Jp81a6AFBHRBUBRBPe6utrd1d7SXF9TVYYPf9Zm2hLZC5AlvOCynaXewoJQNLa9rZmchGZqoHnnVsbiwryMV/W0pgI/LIzahGi+XOhoaw4L9iJCCOCHuJiQhvqn8h4HHsWDe7fJKW0w1t+7kzUxPkqNGhQoUKBAYZexvLzU1MC4mX5toL9nd87IYq2APSAxhoXDYcNlAKEih8/k3sqQWI16eGggOiKATJDsrQ3u5mYSrIa5vERYDg/u38b4OvjEWipsLCUjC84I876/t7OU1Hdrcx24NnmLeYKdQw59vBYfuR+6EXcRm0lG6vNi2oOqyNaRcnbGj7Guj6iPGtu35ZulrwEQUbCMn8oUStt7UqS/TmwXdc1qQdmEAtp2RZGL5LGRPCiZ/k3FSNhsMkq4Qf9FeU/CeFpF0JXHD0QY6eTEGDH0RIf7S9w92P8qvgEtMhAtZQ0PRob6kge+ID83fE1LFjCZyzBK4lXqN8ISouWaLWCgj40KIl9AgI9Lc1Od9B1hgoEd4TrFAu4JgVrcYZiaHLtr+WzqBR6Pp4QiKhQoUKBAQQXj8052xyuF+nk5SQmDbKh/am50iRwQWFYiOa2ot6fTx8NeTEcxJNBjfn4OCA/xy8YGOlw2QdvMDDXBINn2UuEgHe0t+fduOdgYbkXGrjpbPn6YV1NVCvO7RMFD4GmTE+OlxY+TE6ODA9zFikfXVperd29Ym0HmK9kDhifpAC/aBQDNmwgVObuKBe0oGraXmdgC1v3FhsTF/0HpidLrfa1NIjkaudQamXTk4RXIfdoqKJS/8Air+6sNf/HPJDPGEUdhfPB8rrw6kE8K8nGvl7HeORgiyawmPNibSAuuo1dv3re9rRn3XJkYXLibm0lWb/d0s4GBWPbIPRhzcX0k8rqajLW8YN/eni6vq7ZE3CBcMBAwIJmy7A7cD7go7Cs2KHM4bDwc397K4JlHgOzvmZ4CBQoUKOw1gHlAFoWvrSnfSqt9aLAvOtyfnAoObEcslQsH0lHMSbOz0idP997udrSNJdTEuAjY7N7dbEKoeavEs62wtLRYVPgArkeiJhk5pS3A18Xf2/nOrYzsmylZGckVZU/gSsAWkrg92Anj6hsLs85CFIgQ9BZEUT2PSo2pvvrW2soQCj0jnBMCtbyPVFLLl6Jh6oSJQNGov+1Myf6LovqEA9tsj9K3fi9MKlNM5mW1SyiGA7yOKSm4rucYSb/eS94zEGqw1uY6ZAV5sK09XK3xP1maXJ6dkaCGFBnmKzZU2Vrq1VSVcThy6HAM9Pfg1aWJpa/EuHAZx7unNRUwPZDlmIA7yVhCZHp6EjbGmSSwPnJopYBKM5fbWhsVk/SgCBgFChQoUNg55udmZcysVi4SaGFi87uh7tkgP7eYyACJ2/d0dxA2A74xsCmJsRLAlKLC/TZTHQuTyysrTKBDxnoCZfybaUkKX//qKquspMDTzUZMJkSx5mBjKMUluHfBasHm87DWF0UVB15Gv1RF9SZxM7gJG7yMNfxC5OyMn2ITIdR3TdEwnIkFiTAx7qLU5QSmCJuXRZ+QPSisRN57Ugl0sfNDkgW88QlxhoVUjfEX2Nwtec9AyGnAsEsWnyXX64gI8RGzudmrq7gridjGyc5ELmkNNpudmX6NvPgUFuw1KtsR6LWVAb4uIsr7ple2ioWQ8DLX14mgSmjbxi6qC5YWFzbzSRUBnuGu6V5SoECBwvcTXC73mSx49fV22Vromhhc2MxJbuekwfS9eRcOh11bU+5sb0LOTZiaHN+8JdxRUwMD/iqSMGZlADM7kY4Ff5U9sVwKicVF8OVtpgaaV50tCfskN0ft9AN52BQNo/9IlIC9hpQ5MNV3p5UGJB8idvam/0CBiKu9Cn0GsxirVfwGsf2wEPzc932Em4oTBv61HdlG6GKpRKgUD5SMJ0PQHbMWVf4m0rfkFNLg9zQuNupGon/aSMMDjWJcYRdEnreNwuct/yOvck57WzNRK8zCWIvQk4WBsrqylJC2T09NIHYZHhrAo8aJhaL8vBzZxYhgbK2pKiPiHqHByFtZXixLRY7JibFrCZHEjigE0dfl3t1s2ekHjMuJceFEmZGqipJ90515fFCGy7ZPiXoIFCjsHcirnUBhd7C8vDQ7Mw3UKDkx2sfDnrzq6ulmU15aKJGMzc3NJidGEQTGwuQybCkxSQHXIna0NdrMgnw9HSXyN7mwusoiYm08XK0LHuWJrd5K9HrdSIljPK2anpoEgwTlnEcH385OUzNX2Gon0lEkVNxQDth/YuN+Oy1pK1OnqUZZPGQCxvgLrPtLZGwrIFknMNRjEYWr/2ts8cn++8r2Iw1D8abyOFuXyoXRg91fbNNNh6348YE/We/Xk4mG4d9Dw99vLEW8so3k/VYYDxB+UdDFJd4FudNPRsnV4588vk8O1CaXJH5aU0H8KSsjeaC/J//eLXKFMRhw5SphPDY6TJazN9Q9m5IUIzGOXAwT46NxMSFENjBMCYlxEeSUNlnAZC67u1gRHKy7q52aaylQoEBhl8FisRYXF8BAf/TgjrO9aZCfW2py7KP8OxItewq7AO7amnTvU2MD3U60FCeuaigRHe0tZFH7kAD3rSbrFSZTTEcRWmV58c7viKz8QeR49/V2dXW2DfT3FhXmp6cmkAMpvd3tlxYX1PstrjORxwkYi7Cy899hY+5IJ1y1vWcZ+QPGvEQIWN3PsUEDBYtKCyy2OqzjHeEB+zQoGrbnAf2g6bdY21HkD5UeZCjGYep+tsHEPt9GuHO5BqnYy3dVTUgff4eVncllzsc8xf8KnHDEQcT/i9xicsTalZUUkDUGhwaFJR0a6p8SlezF1o0YktQ7tgKHw4FR24p0KFdHc1m0d3HnG1m2KCTQo621UYGnODc3i7v+IkN9qbwvChQoUNhlLC0t5t3JAoNeYsWn2OhgSnz1GVjv69zwEG9zo0vw/GFa53Akk+HJibHsmykEvzI3vjS3dd4aMG2yor2hjsa9u9kSU8eBiV2/RiN726zNde7mZq6uiqwmyxXIABsTKvaebrZbpawTNMze2qC1pVGNXyFvFWketr8iNALpP8SGTJHInKrBGUZlk+B0ZBO0XxNVwVX8mKNY/yWUSCYUZfg/ChZJo2iYwsMCNmKPPD9jHjL3Qg5KPSTe2XSiPEypWVhSrPGfMZaynSQL+cLKzt1fYVz5I55Xe7GGf9gQmflY8jYDOiKfwaCRXGcoKykkJOOBLJEFEpsaGWKTZVxMiOxxgGtrHBjZva7akqPAiwofbOvoh5G04NE9QoYRxvHUZFprc8NOXkVHW/PjB3clljdRazCZyzB3wkuBeRHsmNnZ6Y72lqnJcSqDiwIFCs8cYOOmJEVHRwRIqbeLN8WKRlLYCWDKCPBxIS/FSskyYLFWcPZion9+25m0rbWRrIQMjOhhfq5EUgRTs5iIIi4xL5foF4HOjlac14FVM9AvOR8p7Xo8EZVDpGOoH9ZmsHF/rPWAiAwGELDlCtX3mzlszBtr+QPJ+fYP2LANtlioOJmcikFer8bfiFizw5aImO3oKU3uzbe3h2kYWT9jMkymXdiDIrGwTb+VLxeLuyhkcU3/gS0VK/mOmAys/m83fGLnFDnCuJ9Qnl6ip4vHFpHE6b+swMjlZCfIr40K9xNc+PJSxo0kodKr8SUYGeVguCtM8viOp5kxZfBEzc/PebvbkSVAWprqqflSSMxXWUOD/XWMmsryomsJkbaWes72JhbGWuZGl1wdzHDuamZ4MSrMb6ulTQoUKFBQNcbHRiJCfIiaItu2kEAP6qHtPubnZsnFP2E2mZ7a0nKFeSeeFtrSXC+jh+p+Xg75FQMZqywv3rzv1OT4zbQkQikRbz4e9ltp5UtBCL/kDL5kLNEsycpIJjLM6bWV6vraWG1Yy/8T1eF4BVuUv8oOjyt/j8kVqhIIfBj/hK5HYcymiZBJvFZT70m+rMhObO+nqD418DrO8B58gXuYhqEX/J8bb+KvkHS7LDSaLCuPAvP+n3xMjN0nrC7H+Cm28EDJN7WQjz39E8HxR5zk/044WPvr/Pv6A+KcErFUijX9m2Cb1R4FrnF4aCA82NvV0Tw5ERVQ7upsI3uxgAtttbAkEShAfCMXC5q/t7OMfn8ghESpR/0rpxNiw3Y/aBvYi1hZ510D+bws1srE+Cj8C5PiQH8PzF4pSTGxUUGONkbkKA4pzcPVel86AClQ+F5heXlJvQL2YMzJyUoVs6qh6Wp9p3vxtJWxQYif1+2s9GtxUeShDKYM2ctOUlDqvMOFeZ/I/YYXMTU1oayDtzY3pCRFW5vrkBUXyWVyyLM/mArkNAR7KwO5BI1HRobwXmeif17ijlUVJUSNnKLCB8/gWbP7scWiHZXtWu1GSV91vxRhQb2ncRU3mV/5MtIvGHVFFiPsy5NtxRbsTNwWFUqA/BcS4VDM48Rqx2au8zVFyIzuX1B4F9yjwuDOohK+A3pILkEQIpe0Bz+6vZ0bts5EnUwg+H5GNqKyhrW/KsrE/ihfz+CMYz3fbjCxv8TmspV8UzPJwhTGURe5d1+bwiYjt7kj6HyTYZzFnUY5j40OFz95SA7sLnx8Xy41Dhh2iRVQmGVlDDUpLX5MhHRDAxLY0db8TDogMMbJiTGl8ytgd+NjI3l3suJiQrIykvNyM2lRQempCfBzUnxEVLhfkJ+bp5tNZJjvg/u3E+PCne1NDXXPIu1g/fOSiRbYNFqndS+dJv5X0EQ3c3e2BAonZYGTAgUKexkw/KoFP2EuL3V3tudkpoopOuhonjHQumBjapR1M7m3r42zJrD5xqd79LWEvjJ6bSUlZ/oMMTM9FRXmRzAl5R58cWE+M/0aIc7saGN0Py9H4pYwVZFTy/huMQcZI2LgsvFdwoO9JZhIa2uEjVFWUrjbz3elCdEeXEVjKlaRI4AdOOqMMX5CMnT/F5sMl0MQQfCh1mJN/47R/1xY0HlbKUXmU6zrU5G4M8ZPsXFfbF2hTPvlSqzzA3Fd+5b/QSGIOynuzJ3DRuwQkSMfFs4ig1Y+b53LW9/VpS51kOjAadVEkMwvYBEbdRfpoH0X5DsjdERhqOsPsMkIWVcIZF2ocRRe23zu3nzqba2NYP2TnWBPa+SIMwamUVleRGSauTlZwKy87V6wDTH648SvrKRAsbhwpWBqamJRSS644aGBR/l3woK93F2s4GHutKAk8K5Lp3UunNE+f0bnAjJfDHRPGuid1L2Ifon42BW0Af+vZ8T4mIuDmcQFSAoUKFDYCbo628BKzruTZSvKvvgE7LTO+bPJ6d5N3QX09rs9U8WN46mlw273ei9ndn8Zlvep7kUBDTPWO7drxQ8pbAV4BTAXZ99MmRgfVcXxO9qaPd2EUTa3s7cke3X0arIpYqCtsZm2iQWtMJnLro7m/I3PbFZcBA4WEykQZowO999VA4MzhlwLhHMG2lyO3AeZzRCWikXs4s+xnmPIaSEXVrtQ/hXjL4XH6TmB3GLSFoFaEDuq+7moAIE+j6VI1BWK1RrQFVF0RDzwRUSfeDtgQdwFJCdBfj5P/wSRiNl0bKlsewbGYfJ43DX2wjp39/I41IGGwbufuy1/T83E6n8lfBNDZvIVemPWYa0HSf7W/0b+VrgSzihKVFvfWQUJoHndX25ov/wttvBoTz3vpcWFosIH5KKN1+Ij5TLcy0oKwdYX6B2ZaefeytjWhwbD6L272eRSzjAHtDSrcSYYm83uaG+h11Y+zM8NCfSQMXpwW/ZlcPlcWIBvcmJkbKzv9RxP/9hz/ilfB957NazoaFjREf/c17zT3/S/9Xrwo5f977zuHPahlcuXiJJdPE0mY2FBng11tRQZo0CBwg7B4/GGhwbaWhuT4iMkrhahtSHNM2bW37rHf3Cj6bOUtjfjGl+MqXshmv5i9FPUaA0HfLPfAJ5GFKQS08ejsC/B5XLv5mbilgYQLSn+T7AfUpJiyF0r7Xo8OeEZ9oX/xdX2wZZITaYRLGvz0W5l3SBSEHePg7H7kLeKbJS2HUXcQK7qSsynSH6Q7IkC3rIsZ1bb2gSiguQr6XwPJWVJ+8jXUM0ksm8Dfu75FptOkP+tzyN7fshcKE5OeMDGfXYwDHERhwQC1vRbUljjPyOmJ7O44hp7kccvXcuD/6yxeOu7FHewr8s3s9pEnJKD+vKVrlubQS5ackdp+DtUWGw2QxmLIsPCss70P0X+sb1RDhyGs7BgL/J4d+eWrPfLXl2triz183Ii9k2Mi5BlXbOOXk2OQjQ10ITRWU07HXN5Ca0H52aSF/BkbwbaZySyL1d7q9jIsKInDyanRuAsY/NNJb1eN1o/jmt6MbbxAK3+AJg10MCgEbT6F+CX6E8NB7zT3jI2Oa7HT8YQSxhbYTIxChQoUFBggmWtNDbQwdIVX2PiDzXa58/oan2nd+U7B/9Pgu6/Ell5KLbpeYJ6kRsMU1fj3oPtlVgwioK6oK+3y93ZMjY6eNstyXKLuJbjPEkrf22NMzM9BT/UM2qJbTaXw6ksLyKSzXapYs3aFNKsJvudGD+WT8cb45ef7fpMhIC1v47N58ldjnkuW8Qqpv8IxRNKMQjXmbxBYxFLuOHXqBSYLGINmzERLBRfEHrhjiGXoMK+DWCVcAvA4shPGHe9rE3Laf2ui947RcOUgpVmkbc+ESrn7k2oUoHw1f4AFQ1TFpaKRXzT4wHP/GkNDfZHhvmS86TbW5tk372i7Am5pFhp8WNZ9C1gM/LYCpO6LJXE9hqWFhdaWxojQnzI+cdizcLkMjCfzPRrjKdVzU11JUWP4PF2drTCvzAVgfEBPw8PDTCeVkeHB0QE+2alp2beuB7g5VFRWiQgumtLbZO3bnV/h0gX44XNBo2Exrd7oqoP+ma+YWR0QvucBtktFuDlPj03QpkCFCjscXC53L2TLgVD1qMHd8jSTUh1gx8FjdjXpdMmZsecQj4KuIu89Li/a6vxCn4fWXEYtscXiQy0zzyrTGAKz6xvr61tW7oG42c6FDy652hjRDYzqipKRC1pHtEtaZGBYhLBK0ymjbkunqkObE31CxUd6+PhWOv/itCD7s9ReVvZwRnn9Z4RD95DTgX5o+Zm04Q2J/2HWO932xi0QPO63hc5b9shWdKrJNHIbqzrI1EC9gN0AdsGCm5Lcclha4SbcblKEeHHZ4Tn9v8nvjaJKiYTkhvysp3lCqHKfN0vkfqFMheCLpD8wu8/w4fEYrGyb6YQMYHwQ0pStLzCevl8RVoYJSvLi2TRox8ZGSJ73gx1z6anJqhX5wJbhBYVFODjYm9tIJF62VnqZdxIKi8tBJK2sDCnmCG1xJpsnEh+1G+a2Ph6DP1FmdiXKBMDWye26XkwiWw9PgdzhxDzgB9sbS/euOdUNuDXOp0xs9K1zqOKjFGgsOewuspaX+c+82soKnwAo7Shjoj0PJ6kamR0wjnsI8+Ud/xuvR5ZfZDw0kdLHbJgaAovO2ygfxIflCxNLi/uuiLutuDxQXXCvYDp6UmxAMXIMN++XoFzZqC/B/fNXnW2FHtlS4sL0eH++C6Fj+/vQq8R1xIES3L0qjy26zRS+SMTsKbfrQ9ZK1gNDGgbYcq2HkAijVvS4jlszBPreFtct3AiEP1JXiwWIn9X3S9IfrzX1geMkdai4o+Wi9KOBo3Ea4v1fIuoo1xCkRQN2z30aQhfFfQwuTDqvCH1cVbaZgq8+5UmoX79s6NhI8ODESE+pGohpor5o4C21VSVyigrPzjQRy7i6eFq3dHWrEYixVNTE0TlR7EG04C7i9W1hKg6es1ORPa5PPbgYsmTQZuExqPRCrCvzRYPP2rRK/Utfe3vyEwMrCgT82MuUR/QGAcz2r4q6LVpn7o9s9LJXaeqjVGgQAGBzWaHBXmKic7zCZiGtdsXTqEfhRUfiW06IGBf9BdkH5TCio4geSH+iGSsdw7sbOppU5COttZGC5PLRFc0M9ScmZ7icDjB/lfx35QUPert6YROS+xCcLDsmym7cYlAM0RiCF9D4gKy04zJCGHeCq4zMWK/MzfAOjZshXjLgI40VUMmA4kuivmXur/eskKSdAI2cEXkIdT9DBvQ3qni3XI1ephiV9h7RvGC0RQNw6YSsLks+fIUFeuCKLJ2453J9cLYfajrjPshLXvJHbcW6/mGrwIiL6fnYB1vCfyzU7Rn8vhrqsoI6VicDimxTojkN7HOfZifS4goQrt3N1t6fWEWa2Vudga2WWEy19Y4z5CtsVisOnp1XEyIudGlzRlclsY6IX7ePV2dazur7bOwOtgwnnyz5fjOqddmz1hcy/MuER8i4TLNM7papwkdM+2zGs7hH4ItFVVzEGWX1R1Mb/viyYDD07GIlqn0vrmisaW6OVY/a22OzV0GiigWSE2BAoV9jLzcTPJYBwRMX/uUlcuXHtfegVEFUS/GC4qsDTFeiCg/bGR0glgYUuNCuhR2EU2NDDITCw/2TogNE/jHQn3BTlhaXABTARfhGBrsw124Lg5m8KfduL6VRlLKia8cMXKsNqThTuYYTb9VGsfgzm+zAdlOpv8p1vCPSJ1ufVlOI28ZJb+JVWEeNEK2tOKG4wqqLdb9JUb/M5KD8f+gWMeZVLXuyc+ahhHhgjKWBduR+b8irELG+Cmq26AUzN8VhNvSf6RQHbBpdAQlppzJDCA2xLCFVzlsrKdLp0NKeFrzc+T0MzPDi8BqtlxMWZh/lH/nVtYNR1sjoG3wr7W5jrO9qZuTxY2UOAa9enxsV/OantZUeLvbidch5WvEezo7trc2MZlyp/yu84BWMoHbLLHHJpdbakeislrPxDFeVj4BI+we+ouR1QdDCl5yo72PV/Ihi0rr65wyMTvmFvt+eOmRoAcvhxYe9c16AxqtHkmAxDUeTm5543rL+xkdX9xsOZHVcj6v+0rBoGXliF/9RELHzJ3hxeopZtsSexSo2roqg7N5PN7U5HhDXe1Afy97dXVkZEhZh6XsGwoUNg995BFPT0vjavRnoYUvAfuiNR7Yka+ejpqF/dfEQHQtPpJ64BRktGES48T1OYP9rxLDOIvFGh4amJubBeMBd5opa6aQYS7hYHO31gdMeLMy155deIS1vyyiCN/83yiha20X/cOD+htRY+5IW0FOlQuMPYQcEk2/IwdSoqLJ7P4dWI33kOI8+ZjosP+GnHtbuUYoGiYrFh8jtk2k601F78ZJJ6OEteqGLZVwQCCQIvIsFmrx4ufnZsXECctLd8Olm5uTTpzUyvQKDJGSyck6t7K8mFC936rZmOs+fnCXyZS2VDMyPAiH2skK69jo8J1bGWQFyA32pWFloncjLbKt86lw7MXW19ZXVrkLy5zxWVbPJLMZmEnfXGHHdG7DeMrTkeiKQf8nfU4Pus3udFzJbtNIb/42peGjxLq34hivqo56bWZiQKvimp73yXjTxPSY9nkNYYyi1nc6SGD6tIHuSb0r3wEr0+PLnVk5f2nl8oVz+IdA3mzcP3dPfNfC4Stj02N23p9ejXvPL+f14Icvh5cexpe3E5pfvt76XmbHt3e7LxUMWFUO+wJJ65y5M7hQzidpY2zukozONKDiszPTMK0O9PcwnlZBL21uqsu7k+Xhag39B49iwstbpybH3rubnXsro7+vWxZtGMmrImsciolRoCBm7JoZXiRimM3NTwfefo/W+AKMIUoZjmgNB2y9PkMVDvmncHUwk54Fx2Kt9Pf1wFAAY3JsVFBTAyMnKxWfSmCsVvVKIoU9BQ6Hk0ALE0kVC/UlxnD8B5gv8D9lpl+Tvgb3zNIveatY91eiiWS/Qukz3F3Pk1xfQjlgk+Fy78gewKaTRTXtnsN6TyJVcIWx8BDr/FC0BPMPEVMFaqpuCWB7lYb1nRPvdisNu3FeeIVEuOpUzE6LgA3oitwF48c71X5RPdpbm6zNtIlhy9fTcXCgb3dOfZ8v44FnoDU1MiRus8JkiknTEpEwfJ4gXoDLx8N+fl5y5mhfbxcR/ZgYFx4d7h8W7HX9Gi0pPuJmWlJVZUlFWVFPdwe07q52mM7hyQz098LT6O3pgj/lZKb6eDiQSy0LaiJrnjE2Pe4Xf/o+42r1YETliG/hgNW9Hp2c9vM3W45fb/wkqf6deMZrNPqRXWNWChtAkVWHgFwZ6p/EJc7Id4qEp7X4j10LOc3ATtooGM1vmqdxT6D2OQ1d/qsxMjxhZvONpeNXTqEfAcHzTnvLKQT9AAwt9MnR0MKjkZWHIioORZe9GV/1UXrDyZwG7ftNthXDfozR+LbxOwMzVSOzDcPjnf0DHY0N9Lu5mWFBnk62xuZGl4B0yV7wWv/K6RA/r/YuxhyrZ5bVNcvqnllBjcP/0jdn26sj7wKeSdFFCruDhvqnxMcFn//V+PfjW/+oxFEotvGAa/QHV84KZD/srPS3Uoeanp4sKylwsjMx0NbYvCRHSEOBMa3wQgwFdURWeoqJ/gVi/A/0dYUJnfgrTOUWJpdTkqKBokuJSHxGQizrqPBX6wskMYx/woZtduQ+2mVwF7GlcpFbQCogzyOfh4IHnEc8sOe4KAH7U0TqmIz913ufKQ1j96GwTnKgZ90vsBkZsifl9ZNuxkQIkj0UqFse2ZFnk8dFYbtN/y5SimEqbq+9aXx8mZ6ajI0KIsq8GOudy8vN5O4skUm+d766mpl+Le16/FYlO2qqSh1sDMV0kMHWN9PXTk6M9HZzTIgL8XSx0zl/lij3iQsSVleV4hMwfqfcdU5zYx1Z3HaHDVXCOadhoHvS3u9T38w3I6sO4lW5FMuI2DsNiSg2HggvPQKUyUAPkTG87A9ZUFG8KBBelRUaQYm1BKVa8ZQznKmifDP+D3pXvjPQPaWvc8rY9JixyXE4CzQT82PGxuhnC/uvTS2/NTQ8Ac9WX/sUbIyaMl6ZtfO3dl5fBea+Gd8EL+vF8hF36BjlJcVidavHx0Z2L1JFWd8Rm/3MdfMofE+QcSORGANNzI5FVh2KYSh5CAorPgKjgc5GYcOocL/NBnF1ZamFsZb0T97dxerxw7zJiXHqrX3f8LSmwlRfmLBtaXJ5bFTohyHql/I2sCcuei4ba3tJqNMGxvCo2zPwgO0EnGEUOSmSBvYLuctSk2174FpARMUUOFoPYCv1+7Xr7gGJDni4zf8p4k1itUvdvgklLHZ9jIJQdwJmnVC+s+PNnSrRz99BHYWcUsnbc6ERvT2dZFoS5Oe21yp09fd1i1UCBQJ2PTkqLcf/SW3S4Ext8+C99on88tZ4v/jT5rZf45xBUPhY50xYmMtVJyv/IPPwRBOfID1drc38ga9LwScYuLdHT2vrSX2jCCneTC2+dQj8OCj/5dgmPvuiqzf72qxXFtv0fOiTo3ben+ppnYabRdSI7yLDm8ADduEMHqkIlAmxJp1TOqRHhLvIdPDHu+FGI568Ls7uLm20jZ+Fb0Rr443wtyT8b7gLTpaGNiaVqMYJoYHOKWu3LzyT30nufGlmcaiiRFwnd22Ns7rKouwYChQkzJPLS0QtRPianEI+im08oPTxB47pl/0GP+tM8PEC6SKuYXpqMsDHRfb1F1tLvevXaMEB7umpCWCLV5YXN9Q/hRtpb22am52B38D/1jFq8Gq/FNQOXC4X95dyOGwmc5lYkFpYmKVFhBLrsz4eDrMz0xKXpJ89DQMCNqAtoiLY8r+85Wo1exNAt8iVnet+hmLcVhSt+7fahbX8UVwlH1jZfJ46+QbVkoahp98jEhfb8ba0cNJhmw0v1mFpspsyMcBGoTRny//slG0DkSOXiZhJ3kPD1tpaeWmh5YaskK2Fbu6tjD0YQx8e4i1SdMvciNFQ2jiUfb3t7Vh+KgLiP3zV9bjm56NqDjoGfUw2u3HehUfKkX1luD8NUTXdUxYOX5ny/TCGhidwxw4y98/zQ+z43GCDTpxBbMTyWzufT6G5xb6P3F9NzysrHWKPxijWo6rQgXmvws3C4w0peMk1+gPXqA+cQj/ySH7HM+Vt98R3g/JfCS08GlrEb0+O+tx80zPlHaeQj+19PwW2Y2bzjZn1N0aGJ5BfC8804zvEBLxra96LvyNy+VdLp69sPT/jP//PXKI+uBr7PrwFYMIukR+4RHwIF+ae8J5H0rvuCe86h394NRZ+fscx+GN4ZfhxSNlu6J3qap72uPZOaZ8vZcRQoCA7rifRyMtSMDjQ6g+oQsEVDmtz9QsiQ8zc+NKjB3ee1lTczklzJScJ873uZgZaMeEhmTdSs9JSg3w9BWtqkgYWPJjZSPccXh/F1ECTKHpmZngxOTFaXYQZ91ntMqDWjQ30spICBr26tqYciHFFWRH8Lxgqd2/frOP/8klB/q2sG8ClU5JiosP9I0J8wG4JDfT0cLW2MdcNCXB3dTS3s9SD/0256VNAj+idKllkThYW5Zjp6eDGgLWZdvGTh2LhD2JPdbfvnD2ANAPJZKPh77ExLxTapy5gD6KAw/ZXRG5h0HAbD8q2WK4RqRM9elWdnona0zCEdX6FAUIf5vcYq0PyhuRS3H0aOz3t2oyQATb8Glt8stPeyfhLwdEmQvbIk11aXICBTLhSaKE7MT66zX2w2RKXkVQ7Lk9PGuudF16nmdHKylLtaCSQrq1CWWiNB8D6J2ZuyZb9WQ1gXIg/PHg5+MHLsBdwjIiKQ+Glh/1vvwakwjn0I3u/T6xdvjQ0OGFschxMfO+0t3yz3gi6/0pk1SE88nAfBB/KZQ/F0F+IofNdZI0HxBoumUg0eDLEI6LVoUcUVXswvOxw8MOXA++/Akabg98nlo5foShE02OImyHeK2C88HYE/146bWhwElUfCkF8j88DD+EHx5vw7A0HNl8SuUVWHnIK/hj4mLHZsStnhdIjcF7tcxoBWe8vKFD/hAKF7w2Wl5bW17kTk8MVFYXJ1yKImAL4lIyMTsCwqaKRED5ez5S3iQwxCeO5FnLHpSTRurtbxTRp7+VlxNOC9TTPbzUXSGkG2mcSaGF7v1iZGtEw1soKl8uBNjY6AnSro625u6sNSDXwq7KSwuTEKG93e+DDysoXEBRcQQXEj4ff/6R6KLyglmase54Y/IGn9ff1SHyYu/pIeWuIgJELGSOhOGtsbUKdBoiJUJFboP8p0qVTimIhbxWbCEJSjTM3VF/CiqJhW6H3OxIbPopk5ySwinIkN09sBu9spz2Ah3V/vuFU/bm04uKyYFAHa/g5im1l74lsk+GhAXcXK3Lo/EB/r/RdaqpKXRzMbMx1t9K9UBGI6or8WMSzT5+Wdk09lL7yCnZ/WPERGHyRCwt3gGgJQtGgAa2ydEByESEFL+GOLJwnII7BQA2pLdcj8oDOUvsiWBjAHwhzX64ipFQjy0+jZ1vH917WH8BZdFjRkajqg0CDA++9AhTXP/c13+w3gHH53Xrd5+YbAbmvwXuEjTeYnuJPHnZEztKW54FgA7s23CgLiwe4Wjl/SR+PwihQ+F5icmJ8ZHhwhcnkcrlM5jKYyGCe1jNq793Nvn6NFhbkGeDjYmWq7XHV3MLsgph/SfuchkfSu6qISNwYOl6AIcLG/XM4kcTUXGtz7eKKbBZ7YWl1bHypqW/+ccv0jdqx0JJBl7I+v8ahrCf0ONo1R32tc+SKiILgiAukWHRJdnx4iLcUn8n3GLx1HgfX/mVyphZWh2ZWOieWm0YWawfmS7pn89tmspqmkulj0eV9wYWtPvmNzsm3nOwtjb18jD28DA11NMhLq1uxaxSyfkm84fHt0uLPyZJRmhu7nD8TmPdqYseBwNvv6Gsj3k7odlSWi5h2uyviso6q47YdESFgLX/AZjPUqS/M5yHheDEdjh26LijsORq2voxqhxMsq/eU5GxF6NCE6DyusbFD0Q7urFC2kfGTHYrO89hj2NqzDzqHKTY1mWZicIEY77IyUhYXtizeB/Nx7q2MsGAvYvtd0y1gr66SNWd1L2owGoroA8mxDTJpr4MF73PzTVxhQu/Kd5aOX7nFvO+e9G5Y0VEijlEW/oBzM4pKqYKbIdcZHedmB8g+LsHP9cp3NsK5gIwF3HlNqDWiddpA51RyyVkuj5K0prD/UVZSWFr8+HZOGnCMm2lJAb4uYBMbaJ+xtzZwc7KwtdDd0kTeFDysfeGMsfHxiIpDKh0hYbgOLz1ibvvNhsYPLvaDbGs4e0zhp1mdx5KaX4+tE/fIoaG77oX45oMpze/6Z78DR0BpqxfOXDmrcdVDv7A8vbL2fuGT3IKCOyXFD/w8XF1srN2d7MSShyPD9knE8sT46J1bGSzWytTkuALckoetDy9WVwz553XqZ7aeSm/+Gtf+TWx4Lb7hCI1xEF4TDN0w8wJt9k57yyHgE2u3LwwNYPLlBzvw0wS2YrxE8Dkht6uvfcpA95ShwQlD/ZOGhvx/9U8aGR03t/nGzOobmNbh+M5hH7pEfeBGe985/ENo7gnvevKD5D2uveOR9I6936dm1t/AXga6J+GNWzp8hUdShDx+ydb9c+Qg1RKEp6YkxWylDaZCLFfzdThE2cv0NXWKuFtpwgZ0hFIi0NpfR9J0vNU9fdmzmdjsTYy317WsntuLFzVFQ1yI8IlJdCst12BDpkSf4DFbdrzow0ZlxIh+Nh6g1gPx8NCAh6s1MfZZGGvBfCxle3ptpY25LrE9kKLGevoOl4u4a2syevw7O1rJsgqpqVHVA9E0mZddca0/GHZDnxwNfvQSGP24U0Um9kW1fd2gD1yNe0//yne4TQDTP0zSPbOPKBudwn7F7Ow0WOHBAe6yxnQRMqeicjjkGhVGRif8cl7fKj5cuR9sVO2LYHaDCW5u9zVY3rjnHMZ2mc5OfxHfLLTwqEfyO46BH0eVvpHV+e3DPtOnw7Se6cL5laGV1QXuOhIHfnj/tu5FEc/b05oK9c2/ampkAN9Oio+wtdSDe3GyMzE3vuTuYlX85OHkxJgsR5hj9T8djc5qPSUIGCH+5bMaIOGBea+Glx0OevCya8z7lo5fmZgd09nIpiYkr3RJ3Um8uonmaSPj4zbun9v7fup5/W3vG2/5ZLwJZAlebljJkbDiI/i/4SVHUF0Tfny7WFC6xIaHzIQVHQ0rOuKbiYIsYug4q0d/cgr7UJsUnX7V2TLvThaHI5StV+EbX5tGPoOGvyOJWPwSZVXtsEjSboIzjnJ/GD8W3kLnW+sTETvVZVD50n4f1ndhow61F0XDFALw7IZ/EObqbbVsMB2P+kfT75RGyodMhL2tXxPjjKrdWAxjyo2UOHKRpZAA99Gt/VqrqyyyMIb+ldM305LIg9ROrkTGAS45MZoIIImO8GkZzpPFDyZxCkfUi07RD6oJW1zz886hHxFromAZ0DKMJQc8U6CwJ4d0GQfSFSaz8PF9olLi9hU4Ngxo5JHQO2nr+Zmt12cO/p/g/gcLh6+sXL608fgcLOaI8kMqUebYYllNsILGX1ATpObKtabGd7+L5fQSseipre/d6TlXMeLVOnY3LSuAnE5mba6zssJUx04yNTVBrgUq1oz1zocHe6ddj6+pKh0eGpiZnoKuIlgt5XKXWbPtk7n3Oo3jGK8g8lN9EFpk5aHIKvzfQ8CLgAmbmH+Lknj1UcgJztJ1N/SxEOk6jytdndG/8h3enaycv3QO/9DrxlueKW97pb7tl/26b/brEeWHBQERG8H/gjRjhnjD49vlC7jAXzqpo+KHgg4Ml61LUvPKSL22zlN9nZ6ON0RL4/41tvhYfcadNWSHN/+eJGP+l9igAcbj7OnLXl/GJoKRZiNZPgR+SdEwhVj4MNZ6cEP//T+wESfJm7FaZJVnmb6G9XyD9Z7BVju37nlcRO0afk1IiGJqldDf2dFKFvYFTpWflyPFqQWDMjlzzMPVuqe7YzcvGCaDqDA/A+0z+KKsudHltGKThOZDFHmgmjJFRxoOuER9ILC3+KkI1Yz7lH1PQS0AhvK262JPHt8PC/LE3SASuZZ4Os1FJAPrEvmBd9pbgXmv4h4JMVEcgQxSPbKYn0G0tlxWuGKiIM3Pxz1BwqpEdCLMmN2d7erYSbzd7WWvbm9qoGlvZeDj4eDn5YTCU201jYxOmFl9a277tbntN6haicUxY9PjqLSjyXFj02OIw/BFlXA3F+71wp2lyHF6TsPI8ISd76d23p9Bd4K+FPLo5bCSI3iUijDX+hkpXcXwyXxIwUvGxie0z2sQvDEgUr+oOWSS2ayqV7L4RESPvv3lLWXnlIuZlPX2N3htR7GFHQR9zN3CWl8kMZl/xMZ9d+n6FR8o57ERB6zxX8VrjvWe3OPU8bk9/VjZfSIS8L3f7aiwXc8xwXG6Pt2OHDQIc88a/xnp2qsDHty/TR5tYZDt7emUsn0do4a8fVxMCHt1tyN9yY44GNCdg76MbXqeYg5UU3LOScOBkEcv6VwUVpkz0tEcGx2hTHwK6o6x0eFr8ZHi6Td45XR+SpVr9AdeN95y8P/katx78ION++duse8F3HkNycA2PS9QxKl7QYK7ib7/F2hi6w7dehTk6+5CjAzuLlbM5SX16gNAwsW4t72FiZOVpaHWRaL+iqBqiJZkSWHUZ/hBg3j0oLCRCjwK5G35h0KZDg5f2ft+6pH0jkfyO6GFR8l6ubh3a0/1H7ik8NLDdj6fap8T1DLBaaRz6Gf9809U8lYmQki262mMo+LphsdGxbXIeuNN/46tr8h9nKVi5K4QesB+gk1GIkVxmfateDYJb2tT2IAustVFCNifYJ0f8Pp1Zb14ioZtCeDfZCbW8gdsqUzBQwlzyX6ALVdss/FsJlb/N8ISckDM9vJDYrES48LJCrzAqeZmt+l88bRQYnnM292ezWbv7jWv3L19kySXhEZ5/9u7kX5Ate+jfmPti/Z+n5KryaWnJlBGPAW1Rm1NuZXpFTGT2sjwhLnt1w4Bn4CJHFF+OLZpwx2xEQMmCAajZGD5I8ODfoP5xUl7C2PiGRYV5qtLBwDGeDc3U8TZpXU2PT0a7mh2YbSlp/heSURAlLalw1eWjl8ZmxzX1z6luyE/iBe7x2MLBZ4uQclNUtvwoxrqnwRKb2r5rbntNy6RH4QUvAQjKpG1pRZp2HhVTKdgJOUljFE/p+Ede2xgvkQF3GAGG/dB2m/TiSonYON+KHqQHImHC4HIJfvO7sP6tUgE7KdY18cYs05WT9Soq8BEV4p4vax2ZCvW/SUKOxTzgPVpqJGE43PqcZlzOcIHjdh5mCLva+IWxiAqRL+JbauWttotLO5c91co3nSvAkgXWY2jqZEhy145WamwvYn++bKSwl0u5QyTx1VnS5Ec8Svf+eW8TgkVUk1lCScoNMXe91NtUirIzbSk3ZUtpkBBOaDXVj7MzxXjYGBTOgR+HFl9CExkAdeiRlQZ/CQ1Q1EZGTFE7pCZoebul81UACtMpqebrUj5rAtnrB1O3mnVz+z8Mrn1jWutryY2vxzXcAhPkYqoOBRa+JLfrdddoz4ANmJ99XMUiGj3tbHpMeBXwNCMTY/DCIlzNgv7ry0cvrL1+Nwx5GOP5HeQbEbloaiagxuFXtQyBzuGH6AeUXbYwf8TAflED+20uc031T3quSqHogcPiDAQ+p9h3V9gy5UY86nMjHGCX9Ds58KD9Hwrh+9hioY1/ka470yq6m+bh3KLxryEYn4E84SLWa5Sr3f4nNpc6VK5COWFTrMunxIrjzOJdb4pPELXR9hq73Z0oRar/5Vwl+mkPfdUlhYDfIXJYOHB3mOjw7J+v3Ozj/LvjI48g/pmOZmpItbD+TOOQR/HNj9PqWtQTZWroQdCCo/qbagm4q2irAj7HmNlham+0nDfT0xOjCfEhm1WO7xyVsPK5cuoWlSqgRpI5WqJzUfvFkQZX9ZSr2GhtDxPhyQ7oX1Ww8bjc8SUtvBN4donQhcWA4lSQleJBH5V+2Lww5fDSw+jaBS8dgudX2KkXrgx/5f7IVQVfwi+WW/gFUfxPDcD3ZNPiu6o26ocDxEPMg9pfx2Jy8sOsKLHPETyqVr+gC3InDi9kI/1HBe5gOb/Vnn4JcbXb6z7pbgHrOPNXXXEfR9pGHJPdWF9Z4Wa8uihD8t3hHUm1neeJFzzN8iRKr2EOZMhlLth/FiODqp6dLQ1+3g44EOwoe7ZB/dvq8Ugwl5dtbXQJQeyGxsfDy87TOnLU20XmJhr9Af41It3P293u9XV72/Z1vm52fV1LkZhz2NsdPjxw7x4Wqix3rnNhZjAjnSJ+DCy6hA1iipmlxd2eKanCx1iLg5mu/9djI4MtbU2DvT3PMzPTYwLz76Zkp6aUPzkYWtLI8z1/X09zOWl8bHR8cnBnrHKJ/URtvbnybXpzay+DS85QmuQuQPQBQ3P46LVbZTNpJPaPp4LGg/g2WLILaaFGkwK/t7Osi9kK4lJ7SQQiYe1/FFoD4M1y52XdVfuAjaTIqLl2PoCkrjgyFDbgMdBfpEBXREWVPczVNx5qXRXHtoa1vG2iGzERKj6qh8/p36XDESo4R+F+i3bZnlJWE6MwBh/Iaw5Bp1p2wWD5v/acPj+EJu5vhceQ8GjPANtgewPsJqmBoa6vMCMG4kicRQXT7snvhsrT5UwvCgzHuotXkKEisOh2naqiW6098lJYkWFDygrn8LemuUW5hrqajs7Wq9fo93Py0lNoZkYXNikw3FG+7yGocEJC/uvPa+/HdtIDX07UE1sep7RfcvV3mJD4f3c0GDf7rzr1uYGoFvBAe6GOhp4qvZWIofWZtqGumeN9PkOnAtnCA4GRMLE/Fh42WEqs1pe+g3v/WrCu+hhbjBwsKZ2Ry2Tx6Tzes9gzf+JRAgVX0i7i9S/5SqNtVKPjdiJeMDq/4af6SPbIv5UrLgLDtqQhTQFcoWxXIno1rC1BOUPziiKvew9hc0kq/to/5xaXvViAarAIFCD+R2KjpW7795D3lsgY/Tn1vv1t9+eWYtE8wkBFugWzxSNDXR8yMaTu2C2VpdXV1ZSIGZMBAU4Vw9GZXd/Q2tAIRCCmiGb4rkRy2p6HukdlR32TnvLOfxDe79PLR2/AgrnFveea8z7jsEfg3ntl/N6WPERvG4jNc1QbavIHLBckU8MT6c0uby0tIhRUBOw2ezhoQF1v4vpqcmhwX56bWVMZABfOtwxPy/nVtYNsMhpkYFWEmtAoeip03jJJkP9k+Z2X9t5fRby+CW0LEUNdztuN9u/SUhxJ8qIBfi6qO7tc9fWoDGZywWP8sTcm+TXTQ6fFiu3Tf7NFQ0NmBDjmlUlMhxVczCi7PD+dI7xrQvvjDcNDU4QMwJ6+z4urS0qk8heZyHhbkKOu+V/dmvoHMJGnbG6X5ASyf4cWbPsftn4WzPW9Yk4AWs7jLQbxLA2jXK0uj5YX9xBmtZylbB2VMcbe71g9PeOhmH8AEUiJpX+I2wiSJGDrDRhy9WoVpgs4Ayjyg9E5+v+SiXsXwY8fnAXqBc+WNiY67a3NqnLS1tb4zjZGpPnjxspccIFYOZY51hReU9Icu1XCc2HidqOUdUHgVx5Xn8bWJat12cGeidhEhIrg0M0MFNQHVKPzwPuvkqtDVNtq0VQ4OqmFseIFdCQQI9tlUXVCPs7zpDL5U5NTajdZU9Njo+ODA0O9FVVlCQnRpsbXZLi9xCXFN/wfljYIQlEn5tvhJceQRZk/QEqClFpDrGGA8HZn+teFNjiRrrnujrblD4DPq2pSIyLcHU0d3Uws7cykPzGL511tbP283S66mZGTHO6hKrhJaGOPO4WMzE7FlZ0lOoJO/CFHggtOmrl8iXS69+guBEhPqqxXXux7s9JROiH8vmyFJkPVrCJQKS6Qah/41lkUzGyJpKN+6E6T2Q9DOBvQ6bYfJ54UOV8LgpNrP9bQazZoJkiF8ykY30XkDCeMOXs9xQN26sgZwf2nFCkSIJcACbW+E+kWNhfIl373fQCLszfTL9GDNZ+Xo4jw+pUXRrsJ2d7U/J8A7NRajKtrKiwpLDwyeMH9+7k3L+XRaN5PayIDrxxDAwOS6cvTS2/RfVMNpRzBZOQlmBREC+Sg0ZPIkLj0mntcxoGuifDio9STIxqEltcy/NX4967oqFBdMVAX9d9M6z39/VQ/r29Ax6PV1NVZmJwgQgjF/N44OU6BLb1xdNkU5uf93Va78oph8BPgu6/gkxGIvSa0uFQumOk/qC92xnCIebr6agsAZvZmek6erW/t7MUsm2qf8nR0owWETI1PcrlcjrHCtLp57zT3vJIetfO+zNTi2+tnL9E9ZSNjxvqnzQ1P2ZocELvynf6OqcC7rwqe1Q/1baSs6fVvQCTAhHtaWqgeT8vR9kKRjyRssi936m2LC2Pg6INhZFceATZb7ERR1lz0nhcbMxzkwfsiOQcMFYHOd8Ha3tJbl8F2PBjHiLCeAJ5vMR9PEE8p/Z3MOKEvGFEtWxZ8gt3AlY71naUtCTwI5SquDscbHEhPFhY7Dg00JPFUj9pATKN3GLdl29/XDinc0FD5/wZctA2XnIRyJi+9imYe3AaBhOSmfU30IyMTpBDNWBL3+w39lWsPP1FyvBSpkOs5Ii5zTdEkpi1mfbS4sL+GNZ7e7r24ALN+vr6LhfGeObur4JHeXExIZ5uNpsdX4hraZ4z1D0LVrWB7kkDvZNGhieMTY+bmB8zMTtmBL/UO4mXaXKNeT+k4CVBoDU1Aqi4zrtfxvu6FzQ2KnBq9PbsNOaFxVrp7+txtjfZctbT+s5M/4q3q9P4xDAP4y0ypzqmb9/uOoOH6BNlkSPKD8MVRlQcghZeciSy+mDok6PBD18OLaJWG5VW1ITW8IJX6tv4Cgj+dh7cvz07q9TqBW2HcVl5XudnGLNeVaMPsw5FG7b8QUTIvuUPvOnrGEe2UIKlEmzMG+t8V4QONf4G5YbxViWtss8Kbo1QXFztkcfJMYakyDveEidgrc8j+Q3gkxQN29OYy0E9TNBL/hUFsKrWp7OIPKTC6MQvdkGhpaO9xdZSjxzLx+WqZdwR2GHAxMD4kDEah9wcrU3jU518brwX8vglaIF5rwbcfRWvIwkNJicgY8RCJoykvplvyCEbteczmuCWYd6lZlylBaI0Hgi89wo50SImMkBNP6vNbuddrsYu01WtrX1PRCnr6NUwRFsYa0mILdQ8o31Ow0RPy8VNO/S6ZnThR5GVh0ILj8I4hkozVRyKqj4IDWzusKKjgjJNdUiUiPpgd8khVveCnccXxDySdj1e4W4wMT6aQAuDiZtI5BZrXm6Oof5+9+5kcTjIrh1ZqH3cb5HY/KpEso37PwXC8bigfB3qG1QsonI7AMwLvllvGOqfJPqAtbkOvEqljQ5Lxby+i9hSmaqs07ksVI2J8WNSIeafICkLVisiMzPXebNZ2x9nPlckhQxPwwFixt16pXLcj+TlO4mtTcpx2UDYxPge46e87uO82eztrev1JYw9KF+haoqGqQQzqSSX67+jcuCq9Uw9ERYxaz2oahpW/OShmeFFYuxOSYp+5s97YWGu4NG9yvKi2prynKxUem3l3Nys7LsP9Peg+HgHMwNtDTNDTTxHGawW+BkmLW93u5AA93haaFZGcnbm9VtZN3Jz0nu6O1aYTPSqV9vLhjzjG17Gl4dj8CI5/NHTO+0tQQYzXoss8OP9FKqB7LOag9RMqczF77oXrF2FJhf0Q7m6MQUKYpicGIORTWJml5medkjg1bhkt6wHvnH5FxIZSB5WIErEN6aJ0kzIwub/knKAPxOHWPCDl/ki5ujdWZpcflojtxrz9PTk7ew0a4k6K5e/8/VwbqyrI2Rm1ris7rm8e72X4b1Tz3+PrNCFlRyxcvmSiFp3tDVqaarf66PPVLSI+wuvsTTihK12C7cZttqGAbJaUBqYSC2yV7HFx9uffcResP1kpBzXDGxt1E2c8jX/HpFGWfYdcUQy5oyfIpaoth6z5/bPBDh/D8W8CqjRi0jbUKVYm0bdbshUpc43Doedmkwjj+AJtLDNC/ZTk+NAWqIjAqanJlV60yPDg/fzcmKjg63NdcSmFiBRfl5OmenXqitLqypLBge2Z8Krq6zRkSEwfCcnxhn0avhhfm6WxZIpwW+O1U8fjbndfpEkqPhCdO1BE3OB7oKO5hlLx6+iayk7hmrSpls32vuEQJaB9hn1SraksEfA4/HaW5uKnzwkxyzgkYc6589GhHmnpPs9KIthDFy/3XkhofkoSu6ixqW96g8BDmzn8yl5dQbYteydoaqiRCIBCw3wykxLbWlqEJrEi1WMiaj0ts+px74Hp4bw0sPWV4WLdIY6Ghk3kvai+hFnBJuKQx4wsTrO/VpIgk4M7AHJjiMeF9ViHjITipCjilC/xiajZDaJp1AlMSR8L+OgyUUi52K8sfNDVFCKPbT97ss1wjpSqP1AVr1HiobJ1quGect1CjqpiNLadX+FKiqoM4YG+8NDhMlgMME3NTI2J4zWM2pxX5mx3nnYRYmGBbkY9Mz0VHJiFKHQKEwuv8iXx7h4WkxC19pMJz/vVm3TXfpYTPmIB3NNVfxwaKHqUY8ljX4YaFhUzYsEDYNrM9Q/GV5yhIrio5qUlW/frDeAsRP9tr+vmyIVFORFRVmRuAfs0mkzPe3EpIDKhqynfcm3u86hghxU/JiaJI5GlB82NDxBJAj5ezvDDChLT4A5enPIfYC3a+mTQqG/YW2+bebmra4z1BLhHu8GMEHY+36qfU4YUwpW0B4ad7iL2LCNCHFCBOwVbO62fMdh92H9miIHafhHFGLGGVfVZc9liwiPozP+PdJdlIkgjGPDlsgDJlI8+ufY2qSaTh97j4YtFiIqRf8heklr8ktIszpESi0vFqnpi+nsaLUyvUJ8/LSoIIkLcvTaSiL3t+DRPRV55PLzcsjuL12t03wHwpn/z957QLX1bWfiv5dkkpeXZCblJZPMTDL5J5l/ypokYIONce+9G9MxYGMESKL33nvv3Q1sAzZu2Nhg000zvffeDKZ3JO6coyOuijFGIPrZay8WlqUrcXXvPvs7e+/v09WRM7GQNzC/pmd4g+TPAE6uXpBh2eKa88PjyVUWa3q6+iYqMrosglPOUimyWmryCCWC6On86BimkMK+BAxziz9C7ncCz8pIJbBhW7bV1VbBYdeF4R8QG8HlRL2j7ORg8DLNO63J7mH1YYy+tiKDuX3ESXUlTv7t42H/k9ySwSgsyOEbAzPRoX5K+cDJbmY6inpDHpafwmd4q4xkhxbuNraB3YkkFfOzuEfL3sxes4GluT6oAMZNUvDlV1DpeFTw9etrEA81fOlfsLSY12w7cqKIqN7Fz8PRcH5Z7zj8BjZMLrDhcxGH/C0k39+ytrlg2PxoJlEjzgXrD8I6qaA2lsEhzaz4+/mp5i33rSS9SiAlHQ1172ampyz6tJeJT0iFk8/ZQgac8wRzem4kPT3J0daUe4uXcgsSFdqFnvZ5eygwa19Y4b7Qot3BueK+HyQ9nh+2Cztl6XVeR+8m5JFnEb8CbAbzEoqsp6vNh+RXa6q7GhMTyKGtvy1vE3AGc1pgX3qVDcjYS6fJkJdNYkIshhbYfmpMJqOs9Mu9yEDunBsEOh2aoo2HlO+zq/cLL0VW7MbBZ6tGBtZUnoX7BY2FjmW6plJ0RMDE+OLiRcVF+W5OlryEUrqZaR+nZ2CP/eTsYPW3p8mN9Kjig/jcbrn54cDsvfbhp2iasuT+cnCAe0lR/s87iphzQo47M+3MvmCi04BHOQnCmHOwSU+wDI9B9LrxjIGV/Fei6TqcDVsT9FVCtGmxiBB/h4d3sY1CDL/+2UedhaW5phsQavLx5vd6Qoq+2c4tvZpsMhg2/IEo+oUf6fZ5C3iUWaLsrxcmFH/DIlHZSvb29TOOkAhNtbysaLEkgJmwwPxurE9Zvcrk8HRb50h+Tf+Lop6wjA6bpAaN+x9V7NyU+OYcdHRvGllfdXl8NLLqP9FwORoih3RepbCCH1EOHaAy76QDlp7nqeqyUHFSDQrjkOM3D6KDa2sqJybGhXvexsfHTfQ10btQlBWMba5Anl/MMIZ9qWQLioOTjazAU99v7U5mbOtgLc0NPh72fC2I6kqKJjZXA9MORJTDwRIMwLZDT1qxuKWTDDk7iroTZ6Z52LrBP9M/JfNprmirqXV3t7HKX5253e4Pyo/j87mld+siK0V83u2nacpwTQwqf+3rXceoMw9rQeV/z58eV/wDMZIi4B7SJDEQTdQd4WXF+DdYp1oTAFbMYm78NX/TYzsVKln/1AAs5NYHJqtno6kEc2J7LCibrylx4B6HaYMNpX4twNgfQvnt2rDMWvqXxMCDLfRlzM/PJ3DJatlbGy5KdDE1NRn/5D75tNrqitW86QxjNLvdNbJEEoEon+T9ll7nkZw8O+KwaL7AUmRqfzkkXxxim5/12EBUxpI68U+XMLG/rKUux54f41qrwF8nxAoeY24u7vE9chWkUmS93hzYVqJh2NdmfQ3OE9PWlkbVMBpFsa21icCG7Tv7NtD/KvFpYkIsAGB8ImCQgN5AyvXJ0aDP4jjmbDOeBr/X50z0tLj7lgESq6oobWyoDQv2Cg5wt7XU58FgqorBgW6dvXVdwyUpbbqRxZL4NG6bPlWP54fpNBmyQOrmZLl+SGzuKw8HPdIBqz8lcPfgWDaPfjRkor8Cm86Y48JOZxnws7WqctSkilheLUL0hy9r4Gi6gWiS5rycrIB9224dK5uSooMxSrTTeVlQfiFalAU7yGzP1prY6+nu9PV04GZERPzsfNbR3mpjoYeeY22uu4zK+BIAbKywx/9JzVkkPAJwi7nbBZqmLEVZkU1aoCbH4t6QB8DMK+lAYOZeQYtL8PmFuwMyJJwfHtPRkebmQoBtG6bawuId+pydxsmKlBVsg85EVojstHUiOE8cL5YCXp8skYM7bIIZAMMEokTDtu2ttaUpwNfF3dnqR+TjIEIamF73ebc/skoEV8C2n0eVSSTk0y3sFLiR2I8cLHB27kofqm0SG6SxnNe2nCUOytlj6XkeknawlgwDnTsFeVnrskk/TVT9Jxd2usTWAVu+jabBnkNuLFezDyKitbDBJ1DJifu9in6XUXuW2Re0jBIWE1KG9HrxzICV/hbOKA0mQHS37WwTE9bPzxC97jx0KO3U7brYN9TXGOreJaN5cID7oj3oIyND9taG6Dl+3k5Dg0vtKID/nZ2dqa+rfnQ/dJhXDYkxN9fWV/K4QNHx3gnbkNMm9pf1Ta7DuS9lBfaQDEtmVPOOHJ0q455wOKIClrZWmGQUsRmHQvLEzd0vkEIcqCAmLBj2+FEkB4apKLg+ProTmTkK8UopMAzj1g3To6muteQDts1vVZWlic9iszJSg/zc+ApfcGdKjcy55UE0M7K+CjV/S3ERbDuXQYI+7wFLJN82Io8sgYrCXQUlY5srrP58fDFs5yUjtFDM3PUiXDXU2ET2IMtaj8A0/BqObxX9AdHjIkg/3jwxGE+03OIZrKr4R4FbGZf5ZuPFRON5/h7CxitQD3pZr5+DCLP4NzwvrzuydcnotzgMQzZRDFE1+X1028Ba2fay4qJ8CxMahxQx2HtRDNbS3IBIEcFtD5DV7OwiGyEAbvX2dGWkfQgJ8DDQuWNtpkOjwJXD1EDT09UmItTXw8UK/GJnbUDVkKWqs7sNwU8NFuM8pPlSRSyIcgZm17xeHwzK3iOs8SoQv4Jzxc2cL2mAd1RWoGsogc8plBM4MzPjaGNETsmDt2APreFlA/vPmhJB5kRmV29exmMQspMNBNXkpEQaRXGRQgcIkiqKWupyIHLCFus78iBCmjlf9P8kgdPu5XhI/haWno+sFHGOOcYmAVZjTwOi1RONTIMrwen+cZ/3kvhi2AmrRniZqG3wabhnzRp619G6lZmewjc0uCY23cScEGQIZbKSaJbhHcr6a6JdZyUk5EsbOODwW6JJioeBo+xvIAAber7cgwxEQ7Z9nhZEccjAsR0rYFsKhiF83GHI+WKqd29dfYDvLS31Hfdi/zQ2etGnfe3rQcKgAINVlBd//4Sa6nKQRBrrU74XLVnckeqXmjzayUNgjKYpY2B63dz1os+7/agEL9w2m7CSXeCAHomHYl84CXEDafDbAEksCf4Kt7gjmKQe+3KuRv80CZBVk/wxTY11GIrsZEt9/2bRTjNrE4MPGQ+DEuVAng1wl9vTI66xR2ERrGwLUnHgmvlKw4Xrk6Pa2tIAilNUFOhUGQC97EJPgweDc8XDwdJWsgu3he8cIs2IChG7sFNwz3qhPB7o57qJxJ0BLoLiWlxtgeX/m+i2Zck3C9WY40SPI4cVj2SQ73EiGIMCJPmtt3nGwCr+ERbxiPmdsO78smU+aa8bh2ul4eL24Ej5nJ3G3fTy9s1zbsVk0hhzc66O5lCExEDje+LEiYnxT6nv+JtnFu2dUGMjLra61x1IZaFneMPE7rK1/1nnR8f80yVQbgErYGtQTUI7hSB+vWlVEuJpbG9rITewwR8I/hA8KI99OVejbdAZykJHorE+ZdFpTGw7x7zcbPmazbycnSqrClt6Cl82KILABQIjarFeowiJfZOPBgVm7fFNkXSJPer9bj+iBYZQvGgXxrc7kGUXrCA2gWe4iTQfRAWvKgCNZULswZxcbSAbfsMvKdahS8x2CRvpfSU6TYiKf+JXYR6IIhgjAhxn6Dk/bSP4tNuu621bwDBY9UggSv50oVv0sBD0DcayiS4LoteDmKpZ5z+ltaWRW3PGwoS2KDE9+yod/EbXUARIrKebXx7hY8pba3Ndfrh1Rx52/d1V1VBRRB3MGrflKbcUaZqyxjZXrLzP2QSccYg66fnqoP8nieA8cRBN0IoCp4qL1i7r3eWVdMDQ4hpAfa5Pj+Z0uAqtUD81ZbwwQA9iooXHeVwNw/7zkcWi3fqmnHkPHw/7+fl5AtuONMbcXG5Ouj5dlYyi8U8eNTZWTU6PVvUlPKo+hm8Z7OyGNBKH4xOCL4aSXeZuFyBjx0LcSH77YiUBaLwAKmih8a2hxJU2jjGIwTiiWZ5LCuxPiXYaMboMSur5OQFKT2MZRJ8vr4zyr1gcho+grvTybaqaaFXjZaK/CBPyHWa/bLHPO1lGVP3HAnfKnxOTpas52HztAgSvOwqJaNbFxsfHKsqKzQw1yfvWxkKvt7d7qc85P9/cVD8zM8ODSQcHIsP8+PoMAejS19J48ji8rqmotjUrMpFq5nKRNcZw3cr3nEfiIQS3ONt4JbvYO3lrX8QPyRfTM5SiKCmiipyR1dXaLuEMiTZ/zdHVZSvwgqzawOwayrPxOoH9xyvo7pACjmIYVV3+WdxDjEZ2rPX1dnPH0rAAf/DgyHRnQv3VMBxJsGPH/gMkBjINM+eL6kocJPb+3UvBos9ULVRXIqHIlOC98bOd819DiPrTXMyEfwDZxZddYGBM1BHMqWV91A4DPgaO+dpDxFiWYA2EjEGiQ48o/iMe/v2B6J25+vyy9T4yY5SoO8bRgBNUO5wL3RDVojxcLmtfBh3o/8oNwBBb4MqkJ7IzP/Fo1ygruDla5H5519JdVNmV+LHN8GHVkYgKCLS83+4PzhOLqBDZQArd8JJdvh8kEVYkJy7MjDX6Vs0PPs0YflR6kU5lwzB0ZNenR3BfIvalOxLdnx3WvMPRSf820I/RyI41EJl1tJRJ6afKupzGwXf3y4/gOwU7duxLDw2CnxYe57nnxGIfRgwNLXsyqsdlIRH9Hah5KxAjxfwM5MaokeDvDAQPCpoP/yTTaoR0iwDd8Yk+90cJHG2HnvOLA3earZV4NIZha2WzPUSzLKcm9nWl/biDcTwkno1XCebYmn7w2Ifh3Njpzcv4lY2jMJlMjsiYmhyNIu8arPyuzPpDi97D6sM8cItFprw+Ja+lQ5Xfx31a6hy6Z4iXbsuHBLmtCpIzZ5JbqXZhp7i1oTVUFCBxcAke3sD+YxLqchEL9wtkW7+ZkdbkJB4M27lWXVlGBhA6RTkunxKOG5uxY8e+vJpYWOkux+gTkPNsYTvYylS7q6tjqSyOMTOPENfoJ1gXqvxXwdoR575Cyg3uWgKkFpQguqyh7pYQbaIQvhGAdtwtiC0K86NZgs2AAesPgwUPbrgIIOhY9g5ffX7Zwp+924ZzTQBAtUIk9oSHTKZNfY0+7MTE+NPYaG4Mdi8ycMVHGxkZ4nAD3lYwML0RWb57Leo/ML4ICcyEl+yCie9CFzXAYzQNWa+XR3rHS1Z8HuoGXzo/OsaifOQZrNfRvRmcL7bh4BP7ZsVgokGfxbV1bpJLZkSoL4YiO9nycjPJwVptunRg9h6sxYwdO/blDxtHVIg43jvBElyVJ7kTlyo/zTMXalDzUBdLIGaOyUqi4v/wimsdZakbzwk1bS0hGs7xi4C1KBHjeQIfaqoGCv9yH6f+5PqTMmAYtgbWocc123d+hQwzox+J8r/jFIUBMFsDC/Z358Zgr17ELUqKuNxOvOkpHw/7hWqYvJa6rMeLQ0KEYWGIL7EMKn2FFIhBcjBYUlstRYd/mgSdLq2pKo/SHQSWEmqlZ1fEeznNGI76KI0OxT0gp66oaOp4aQM7MLFvaqLhclFwHVp6nidVm4GXFOXjxWAnG9njDeOSzs2gz+sCw4pY+1wsGSIcr7Bj3/JIrFzU+eFxqjo7J6Gqy6elvhNyqGIMw6pD2X/n4uH4M0jvMT8r1HcZhHWq4j/mmt36fTgN1OMseO/WBNHtwNPNWPnPLDJ6bNsDhhHzxHASUf6/FroKL6+wq3CyAurEceC+shA/IoBb3L2Itpb6pcWFqz9sZ0cbkmZGqQNNUwYkl6tkCGRTJpaJhuSLeb89YO1/VlvnpoHpdWPbK1be50NWXV8C2YZP8n6o1MQKUhRlBeuAM+Adczt8BD6r83MJeXSalgx3OyIUQ7stbxNwJvizeFgxXhiw89R10d3hkXgIXDZkOyK4FCNDgjBH4o6HYR/JTml9k+uQfHwtW5oR6z0Ip8G54oGZe50fHfNL2Qeuz5BCzHuOHfvWRmIgd6KxRN5RSAkOcG9taRJOnJpp4yeI77IgZjuFGQrH84h2OiepBl78a8hnuLLZrdFUOD/G6UL870SnMTE3gFecTQ/DlkPYwnPdfOZA7dpDsLy7EvQ/QjQrcAkX6AtFuntycoJTtrorZ2epPzQoNP1ybnQH4IeugZRv6r6IChHBEoii3SQDr+fLgyAhsAk4q2d4A7HeQ3FnVag2pq6g6Bh9IrJKZJXZCfh49hEnEUu4JquO5/teMqpSfGS6XaC//UtXuKHFNe5kGuRP6opK5m4XIspF8NYydp7NhQqRoFxxm8Az8MLmat8HF7aOrvTAYOd2je9vXz8ryMtmMFYbygYHt/nCSbLOwqZEnZtBOWtVDUOdBf5pEk73T4AIpmcoRafKgOtQW1va6/VBqKyIYRh27Ft6xSkT9Xp9AN7XC2oohrp3V8bExo/BavZxSST/HfEtRqhFjVmi1x1WvTh1tv9KNF4jxlfWKsIkvobASh2nvKG4wuQcw7D1trEs2PMKENFkuQCvGnnHaSws+xti4ssK371dm3PRdNutUkevp7szwNeFuw7W2SFMCfOxsVFzYyqHmkJVAdz5DpEnWSv9rp/qXYBgAesDhbt9U/Z5vDhk4XEepCBI2RlALzhwBWeu5MlRLvDTMfrkKnVLw8tFuSkTwRuBzDirNmhGEJrKsZkeCycpTlOZmjydJq1ndMPY9kpAxl6MwbBztSCKuDw+auV9TtdAinJLEWB1DlfnbXi/RKUqTDOGt01An2VMsEYOoFWWsWknnOxM3r55/nXZrKTdXR2Z6Sn5uVmlxYXpn95/THlroHMnMSG2pblhuy6EWRmpVHV5jmozQETCHrUFkTOiQsQ/XcLK7yyVIgvCFxwjuc3eFKDcUjC0vIr4r/Gd+xPHNUPsm3z8uELEJ3k/TZODxEAqiMh451nGO5OyjF6MuW88ZaV2KuwbFCIAG/1EVIvwzoAprnB2izFCjLwn6o5zDgWO3B8m5LZJDMPW0HpcOboHw0kCvHCun6g9yAHx31aqAtRlvVCK/c1qsHtBXpYuVZnESI8fRa6MFHFp6+3pinnAUxMDq7uR1VWw2EdUiC7ec1gOJ758UyQ9nh+yCThrYHodvRChLwTDYJcjVQZgG5qmLMJjaJ8Y/JfTfVgTI9OFyGJJgVg6wKss3C9wj+WAt25uEiy9a+7N1VzIp8GnAjkN+HPQTjMerMdOZr3gYnB6cFxTDeS4imTzKrqM78ormbtfeFujPzn3bdtE877xytiKi/FV0mlt5jk10Xpad7nnUfXpqgE+zkmvnw0PDy0eQefmwJ0Y+zDcQPsOnyI8F7FQUNrH5KKCvKmpbUUs2dRYxy3+YRt0RrgS8CD2+n6QtAk8gxpiYURVg3tbmlz7AuBxx3snMEMjduzbYQEqF/V5L6mtLU12J9pZ6vOpvwpSCmslSn/LToy/Bgkz9o28hTCp6Hc5jHeNlyBjwgpsrg/iw/K/5aqn/cl8hzljEhfBthYMG0nmmuT7F2IsU7CLoJZLP6FdZ6XpjC9R+mdzTaor/iNqayrJvVXgkWF+a3rO0lLfkcSJaGNVV1/K682BiAoRbqVmjxeH7EJPO0SdBP+rpS6HQJeGKnSQp4Lf9QxvAIxkH37KO+lAQMbegMy94KfL46MA55BIDLzQ4/HFT412Zb0Pu8bzR2c66769eFp5ZelNXJAQR1aKgKMZWFwjoxLywvwcQSdzPmTeI7NqDWUFK59z8A/EAAw798WWudfE7jJMeVFFlyVurq6kqK19E1ww1v5nU2vtyMLR9rD8zgD2XmyliFvcEe7NDm63MtX+mJLER9Bf31ziYGXKJwfPLfTH7SAOODhox39wSHjj+yE9prT5RVN/eu946dBU88Rs/xxzcp7YYie2s6ONBJ/gvJnYXYkoFxEWAANXo/uzw6zIrEiqzIPLEsRVHd2bJMkQiGnadOmgHHEcyrBj3wYObnyQdMENl4U2+OiIgFU0JbYyv0YIk11wNI1Rf5WnAlb654KR5nPb0AvIvcF9tDZNYhoOxTFmBuaZuBS2hWAYX2dg8W+Ib48FacrpITq56BObFYjpFTXSgOMwVsL2MTQ0mJWRqk+/je46GkXhaWw0Y25urc9Zc1ODp6sNV4MiWOPlAOhyjD7h/PC4hccFA/NrAEEhuIU6YSAAU4GCg3pGN6x8zzk/OhacJ86GbaWiiMIrslQiru5SYr6xhYkmSmchk5iGyof3iZNT49ytUPXf3qS26UeVScAXlkCa+/AS9i8ADUI49+Sovul1krAeuIuDWUry6xVU0EMf6aMUE/wJUCKseDdOXLCTszdBOXsc753Q1rnJGR1kXfCmDpcB+vJL3RdZtSuv24O57Xokqr8+Q7uw4HYDNzV7q0JtcShlYaT94JFXypeA+FSbey8NjE0XeJbV4G2FKjawh1Od/U+4ZaOigKIH6lIG/2T31KnLAURhH34qsmxPdMmhmPLzz2plXterpTaZZre7fp2o2hJnLyPtAwky6VQZ77cHVlyYCmOFPkQ265O838r7HCRPuy1PNiyYOl7yfrsfXIoh+WIeiYeg+rwagmfyPs8vRlbiaIYd+7boTiwXdYk5RrYUAffxsB8eGtzgYDfTTjRJsWprXBJeLcrEWIbAh2JOEQPRkC6fw8PxP2FD4/Crn7yOMb3NtkG3FwwD1mFIFP0e15iW7XL6SmcmF4Yfemy48P1vVz4qJqBNT09Zm+lw5zo5WZ/W87S9f/eSM+GgJsdOnlj5E2v/VY6NvlglAiOrq65PjoIsAaQCqG7GIkIUiym/8Laelt/p1zL0aXiqjTEPy+jlZV90tFS4t8OtTQ2aGur5t1cmewraw8Kzz4Bj+n6Q9Pu4L6RADLyLrr6UuhKnMQyOmjyLHR9fCdAdm+k2tWG3XIMDer48uLL2IfAJgeN1YluteZUigVl7wMVGdiHCFsRbCjSqjHPMMbTFEF12oHUoY1tG8/aRnNCi3bZBZ8Dfy8MgyqrwsJ2XWRQhK3abHOtp4KexzRWnh8fdEw67xR8Bd7HL46MOUScdo0+4xR0BiEJHT4rsXmY7a3OHoqzo9OA4i+xH1P+TREDmXlQhr+5/tiXOHsiNuMQY5akast5JByIrRWBULOLhNPoB8QaLer50F5KqBxDL89VBu7BTWqw4DFEW6xc6Tdr54TE2UyJsot4dWSVi7sZWEgffmqGJXFAOHnDFjn37IDGo7HybB4nNzvIktGhgbD3C3OhHovYAUfpXXKjpfxCtt1ei+Dw3QAxEQcFo7gpY4zVYw1jObvo8Y14YNHhcR9yq+6qbmLB+4B6PsHLdEWK2a+lXTA2VTA4WEghhDyVySDvK/36FbJuCGAAVUeH+ZIqjR1PNTE9Z/9P28F7ook1EqP+QTpMxsb8M8iqAvlCAiCqTeFR+8mWtalabc8O3t/0TtXM/ICYpKynkZgQBrq1560lMVHlpUWZ6anZGWnNTY31ddUb6OxtLur7xDSpFlqYpo2d4A02ic7+wrnblG+SN/R9hlsnaXtKmS7O46XetDIbhOe/tVAQLL9kFkIO+6XWEJdA1Dy5Ca/+zgVl7UWUjuuRQ89BHYpva14mq0EJxHZ2bZDsiAEi2wacRlHJ9chTc+6hKBgtZvCUygBPAfWpget3rzQFwJlEnMyJQRVw+ZHtzcJ44OM8AcTndO2EXctom8IxtyGkD82vgvcDNHlIgBh6EwUFb2v/TPvC9NH57vyXO3vT0lI2FHjdw1TW44ZV0AO7XsLQTEckhFPtaqHShM4OYD0FEDczeA061uetFmqYsTUuGbDdA+EpLXc7E4XJA+l6SzPZ+6fH3jXoN397HPQ8gW7XBSwwtr0J1EFzhx459uyAxh8iTSFAH3eZxj+8zmQwSgzHm5tYchgHMA6nsuAa3wO9dVstETfzFtG4bHiwHeTjEiOE3Qvy4BHN6GbW4SQgsO02J+hPzlf8G8eT89JZbuDe3bth4LsTW3JoDX3/SWTs33c9B2ADfV/37QnPjHxI9jmv3SQEUsTSlk0u4tblud1fHhpyz4i95bC54kngDsiHftPQ67/H8MEgUHpQfe92okt3hVDfwune8bHiqbfkshUNDg9ytj4s4q+AG/bY8LL6pyfMBMLqm0peCz6v5A5OLndCAO8hyQL4CExrMLbbjKekDMvaaOlzWgPoKCiiXBamwfdhpJKaHaguv69SHplqI7Wvjs73uDy9R1VmS7qyTYOp4KaRQjI0WkEZwoZjni0PgcTQdulANgzNL9mGnUD3np3Oe3MAMeoUIACperw+Csw0OzmKkVADH9/sIYVjLUPpWOYFVlaUwcN3mUCaCv0JXXwpgeyPrq8a2V8xcLgLAaWJ32cLjPJww9D1n7XcWPEinQR158JNKYfVqcvUgaUIhDUUj6ysAgIGzca/8YFK9VkVfbP9EzdQcuzdpZGTY2d6MGwEa21wJLdy9Sp1G7NixbwovYommvtsPWewXwouvl+PoCCTpnZ2d/RFtktBsMIGoFuXSYv4vRH84pDQUFMjNfSM6zSAHHjcAq/j/YNlDUKGpxd9iZn4kbb7LjqjeNV/5r8RU3Q8S/a/EwH2i4RxR8Y88n6T4N8Rs75ZbuLeCfDO4gMDJJU80wLvMcQHKpk3XOa/tD1uTTei+HhsLXXIRDfRzHej/ulFna3JyIvZhuL7uLXC36+jetPA87/fqXFypQnq7RfVAXO946Sqp4UZGhtydrZZCYj/2YH/3+rrq1bw7c34uPFmBPRgGskyHy3B/Gkf5HczGATCAXdgpKpTLVEBzUFAFwegGQAWwo2yhuetl7Z05oawTm7meMzNuYaGG6irg9gf4ARZV+NrbWAKj4Bf/TxLuzw47xxyz9DzveO+E7wdJ2FC30rwfzoKWikZVizhEnkQtjjp6UsF54uCA7SM5W+gcZmQm6cKuSwVuRXiEV9nOwa4KFBX2yByb+RAqfHAaEFDVERzNNvh0RP7RDy06Vf0JYzOL7z17udtyUzUCnByGN5iwY99eNTGwKqHgjO70iBAfxFy/hqWwsUwIVzgA7Hchd91k6UoO1SxPlP01D+yp/Geiz4eY7RbG6tUMlZ3Jwgny72fVJr4QHXqwkZL7acibbhLjBbgpcc1s+DU36p1vuEzML5v0gjlJNJznXIIA1AnVAK4wN9IiV9DEZ7HrQMjx8/ulL/fxG9vClgcdYznjs33LEqYQpHsn/ukDMyMtvh5F4LpUZX26qoEOD+E1jaJoY6GX9jF59W/9bbIhOHsfnS6NhtwMLa7xT25g30mrGoAZNoFnUMqLKFuAO0afYFd1Fp6Z2mQqkDDdFjXmPMPTX5s9NgmB0M3ArD1hxYvvzn7XWSeEwgtAYhae5yks6g6AhGEvX9Hu7rGiLXQOZxkTryqprr7qWmoKJLkLB3ctzMKhaVsNVukMkc4jRhPKLaS7COWYDY3k/B/d+dBgWjf4cmzmh5nK7OxsWuo7cqYXvFxH52Zg9p7NNh4Gvk0cc7BjXy1jR+wxLXUOd+LT2GiyO1HINlVLdNvDwhenC/F/E70eKzkUAD9tWrwVsH+Ac0OrU9Zl22ga5NIr/Uve4/8j0efPn7jOM4jKf+Utf/0hUb0LkouMpm/dhfuXrZNiTBKDcexxr04jQiBCZOYEfAmHPlGWGHpGrFovaG5uNun1M7qmErqddLSUwT+JHWOTkxNTU5MlxQXlpUWN9bV1NZU11eUDA1+HhgZHR0fA418KPqe8f12Yn9PZ0TY1JZxCRFnfIxDL9I2va96GqZ6unlRwLuZ33qGNiO7PD4NcH3JvqrHzVwDLPV8e4lMvqOiL3Tl35dM0Q5KMFPxi7X82olJkPb8UM5eLFBZEMbK6ytoiEeufqN1yp3GOOd3R3vwpJfnth0eBITbuwWp2IWdsAqDbBp+2Cz1l4nAZ/IH6xjfAFahneAPOwtGl9U2u2wSdcXt0KS5bt6o3cXiq/adsnI0NtW5Oltx1MJYGvQSm6MCOfbuy2Ls9PcLu3VgQlRX2ZlIn0aoGFcbYWOWPIVAZSSYE3YtkjBADD4jGq8SX3+HCcv+L6PNerXg0AFR9PlBkrEmKKP4jHmRV9j/hmy7e8sYkqv4DPqfq34lWVXiEqdptsGr/stWWx36ix4VgroRhj+i24/myGy6s5oPU1lQ62Zlw2J+NaQCKENjW2LI67L3f7adSZNmzYRbX8GzYzsRgvimSVHU5kotCXUnRyvsc5Pzkos0MKxKv/Pp0TS/IdaK3WrYlN9ItPc+zSUpuy+saSAXl7IlYL0VgyAl27wQitdfVlwrOF4ss3jc81bbVw84cc+rbVH3jUFJOl9PLRsWH1YeRUnxIgRhyKNKYc+ptvXbdYOLY8kbex0ZHEp4+oFEUyREyirKimfOlkEI4RoLvcezYt3FNzC70FLd4T15upjADVuPlBQD2a6LxCjFdv6IiVSpR8U/8M2CdZsIZvgJAi7+r8FdEp/H8t3hiZklWhclSYjiJmGdsp7T2F2JHWYc+l7z3n0EKkBVZc1O9sZ46eQs52hj19nQt54Vf+3rLy4qqKkqzMz/lZH3Kz80qLspvaW6YmpoksC3D3jRQbALPsGkY0I57MW5K3HHjziAJtvQ6T8qCgSvB0vs8hGdcNYSY8gvtw2s+lYQ6+zePlfbdC/q8h06TZndpqsB7xCd5//pk9iC9sA8/Bb8XNTktdTnv5P3R5QcmZvu3WyPA3LfOkfzS3vtZ7S4fmgyy2pzqB19PzA4IsFs9O+vjYc8l8whHyJwfHkfoDt/j2LFv75HmkHwxxC6LIgBVXf7VizihbOoxx0uJqn8hGi/BWZ7pxhX2DU58gVJPPHWLc7DIJixruMg1YPavUKFqLHvHprW/7Li/uP4E14zZpZXVwQy0ObNPYDUdG12ccKarq+PNq4TiL3lZGakPooJdHS0MddQW5a6wMKE9fhSZ9zlzbg5rjf/QphnD3vGXyciloSpvaI6rYTuRmz4gYy9VQ5aUujK2vcKXvz6tvDE01bwzb5Pm4VSfuOskyQQARTQtGZ93+8PXviYGwJ5LzDFUiwPv6xB1Iqb6xAxjDMcuPnuZ+IQLg8nTNGW93+2PrBLBoQz7Os/7BWPxzA1axQASM3e9SG4mAi/IyxICDJvpF5gCkc9mWnnI6Ev+jPj2SMgFqGYFtmRZh64w0R2GYVvDJkqIot9nX1593oK+urOjzUj3Ljf13xKFrHuRgYJyCfp6OYK3wGnK4hso7bmGlldJgZ27CkrWvufWreEK+6bp6BCxDz9JioMBjOGfJsHNMPG6jiJQaWL72fB0q4s3hRQHA5CMpikDxYirRNa02BJRIeL15gCbqlFFwcz54tOai3PMaRy7uC0j7QNHrk0Vdm/6pkhixlfsG+CFu4PzxLF+5kYhMRCNjW2vqCspkvnk5ij3V/JQ26+MVnFpm/s2P/Qa8s5j24kwDCYpSXDKsFoMTpoJYr09XVam2uQK+uh+2OzszI+enJP1ia6huIiS8m15Q6qGkbYmVU0J8W7xKWsZaN9JepUwMjKEr05OWJiceB4fa2ZAQ2klVOa9BecoAjP34h6endbOEfRZXM/oBpnrO0af4IbiGa12OO8H9m24JyLUV4cuo7WAxLTp0rYhp4Ny9giFFPF7wnpEXAneAn01UF1dW/ppifw8wcRfB2mpH5K41wJwJQdm7cHDYNix78gh513g9je2uUJh7SpS1eXLyzYDr+w88TWIVQf7E6LHFQdtDMPWxmZaoRC4QEhgYsLOUp9cQV8mPlnq8DMz9taGfLrGdhbGz5487uhommPMNvfkR6TIOt4/YeFxQc/wxvdgLNAPX/2Q/6Cnu7OkKN/DxZpU4wHnikqRNXe7EFq4ey0SSuybfLjZ9clRdmMqC2D4pe4jL4Pinkh813Bbek4CR4z4DtQRNjC/FpCxV4jNb1C6jQWDnR8e19G9SRarWVSNCvdf6+FvgTQGg2Flps3Fi6joEnssokIE39fYse9YuqmQAjED0+uov0OXprJKbVWh2dcQYiwHB20MwzbN8jk3FxLoQS6f0REBS9TBgOXmpHMDMAcr03dvXrB3qSebsjsdH1QdQXI9wINzxQGo0IGaofIkhyldQ7GpsW6z8bCts33OTtPWvMU9yA4AmKXXef+PIPMWxXWwnbl9aOp4idTvNrS8hkg7Qr+I1fa/xJHq+7z//dtXpCaVJkve2v354dXfO1CvuUwU3IYgfDnHHNNnpRF8e0kA+Pn5W+FvAdnU1NSXgs/cpTDw0/PVQdyOiB37jl7UykR9U/bRNGRRTPBys52Zmdkye+XMCWJuEId3DMPW1qanpwJ8Xcjl8/3bn2R7H1PekuAB3FfP45Bs0XxN/7NXTUrhxWLfbydDlZhCMZuAM5qqnHlNOyuDzaAEvSE2MjKUmBCrT7/Np6gDp4DKRHERbKd2JO4GSb+O7k20XIGfHi+gSlhM+YXOkXwcqZaISBx0xJqm83p9ILJaBNxHcD6haBcKQUjzmq3mDO4ylqYzemRB4lkUoq9S0YhKkeA8cUvP8yZ2l2EFjCUUtsiwq5qcsTZ9WkiagVvR+nq70z8lxz2+5+ftZG6kRaMoLABUOSpF1j3hMG5HxI4dO4iuIBpoqbFbPOIf31+iRWgz7fONENW7oSo0lFrGhmHYmtmbVwlkYgFW06Ulz9tam0hBZ5Amhga5MZizdYOJ8TVXl24EgjlQmaiJw2VSCkmXqtzc1LAzi2AgZeHZVldVdA67gVJAHLJ3cv+Gd9IBSJCoBkthAAOEl+x6Vi03Ot2Fw9TSNbFncY84VWUVBT3DG27xh32S9/t/kgjOFwvI3OufJhGQsdc3RdLx3gmAr+xCT7s+OerzXtL3g6Tni0Pgd/C41+uDIfli3m/324edMjCD5S9YAbsjDxz8QlGGYAzNu8L/YjGAPX5jtzNL+tPTU7EPw7mL+QiXaoJL97b8XXklh6iTkBoR39fYsWMHSKxSxMr7HOWWoqaqfFC4+Y8CyyaSSBl+C1nsSTKPsYzVHpA5SXwNJGoliU5ToX3ImRZiNA1Ou2EYtnUtNyedhFVGuncHB5diYOvt6TJbwA9ww15NMT7d7FnjVYF2RNiqO6yDmBhoDPTvLDKZwvwcvj11mpryx7RnCTXSYSWrKoKFQFYoTM67tbcMkTowqo7a+J+NrpIYnGrCYWo55mBjxN0Rh4a4aFoyuvpS4CdNUxb81GIpYkMcxYJS4J/wEVU2ykKUElSKLMgVSB4ORIhvbHvFwv2CkfVVK99z1n5nzZwvAg/9eLFq4PHOOcMT42Ozs7OjI8P5uVmOXGd7QZ0ZQlZw9vRNrtsEnA3K2YPbqrFjx05uMvok76dqyIJgCyJqWp0LY35ztiYyiYkiokmKX3955N0qdq3qiW8Piar/WKBn/NXc+GIDcgKdEPDk1jtEyZ/CA65MwBrDsM1g5aVF5DpqaqDZ3PTD75IxN5eVkUrKgoFFF9xIHokC9pwUwYkL26AzSBAJ+YtnG5zHrPNmNjiN3OmLLk2loa5mYKQ1shynLHihEjVdKBeDpNb50fHMDmscppZpdTWV3F2+7OLMHXkQrOBPNXlU12KTkaqxSzfklhAZ2dATUEESJA02gWcCMvai3kVU0o+tOZnaYFHXlTa9SvmarWNMJiM786OZkZaNhR63ogmJeAHWNXO56JF4yD9NIiRfLKJcNKwIBzTs2LHz7DN6Jx3QUpdTV1RyizvytlmTOb+GYymMsQJi8Ilgr5mqIuqOEl9+h4vR/veJFkVi4B6xso8620u0qhJFf8AP6kY/8T9z6Pk8wGnjBcvDdY08CtGbm2sEw7Af2uTEhI2FLlpKQfqyBH3N+PhYeLA39xQTyGY8Xx5cAQUWFPUr2K1NlyZnLTxdbXYUDGttaeROYp49gZN1DUNJOEZjlZXAzL10mjRCDnqGN0A62zteiiPV8q26qpwcT0LCwaiHkKOcoS5HVZcFjwN8hUhxUOchDxhTk0P9h5Ze5yEAA+iLVaa+X3oipdm44dvbacbwTjuxRYW5fDV8DVb5C5w6Hd2bzjHHQgrE0IgdGsbDtzN27Nj5Jp8DMvd6vjzkdP8EWN380yRArMjv9FurmNVlyUZTjddg595PbW6AaJYlin/DA5bK/prV8rdSG3oOFZx5ANiviMp/I9q1+RWoB+6zn1D17wRz7Of1upo9nGNW/ssmFyjDMGxxa2tt8vV0INfUwvzFwfTExHhG2gfOIJOanKYqZJLweL7y2Wtw77nFHaFpymqqsWtiYI3fsQlNXQ1Ev0V9gThMYzopK59zZEeihceFN023sSaVoFaQl/Us7mF0REBooKd3kK6F53mbwDO2wacdok56JB7yebff94OkW/wRv9R93m/3g0DkEnvM1OESaj5ktybekTe2uQIeh7wdJWKJNUqZbY41/S8mZ7/tzFM6MzPDzeEEy1/KSvqm1639zro/OxycKw7RF4Ze2LFj/5k7Rp8wdbzk9PA43K9hUZHldfgKsyY2PwsrYABQcVe0RpKXrJqNEv0RROX/z4OXqsWILgtieqUTASPviQ49/gpYyy1iPB9+Qh741080Xecpl038TFptngGxHHpy/RlitnOTryAYhi1e/+FW/fqY8naRUursTNLrZ9ZmOnzd/yChgdnJ6vivIqtEzN0ukFwd5sa0pWfSthkA5nQkat0eHoK53esGNRygN4+HFKz3iB3IYoPzxHV02ByJFBUF55hjrSNpOFitqimFOZPdbXuvUhJ5dIVkdPn+6LL94Jco8LMc/gIeB7/7JZ10vX/R48k5t5jzvq9Pgeck1suW9EQNTNThxSLY351rFVB099b2fX4lKGdPZKUIatTEEQP7tvVCfBKETNRh6XVeTe6WmfPFwKw9SJIxs1NIsh+MYaLhPD/46XFeMsDNEfUneJ5f/EdEpxnBXCn/LXMcdjB++RXPMWsPQnIO/vpbP9QuIwEV8NLfEp1G/DhtURuIIir+gejfGjqiGIbx2+jIcPyT++Sy+jwhhlPF7Wx/8yqhpCg/Ktzfy82WWx6HcktBmybtdP+4UPSswstF3eKOUJQVSYDn528xP78jNv6Hh4cMF4YrdLRUBgb6ZhkT98oP4wC9iWBY/nrDMDi+/F4SMkaowdtBW/tmTPGVOeY0jlert7GZ7mX57ILDf/bskHD086SCyST37MBCoE2TDcs+FlklgtEXduzYBW9NhBuOBqbX79xUBsuc1+sDEeUivu8lA4NtHt0LX1qu9ufWLMePwdq1f4JqGINE8a+5ClZKxNQqtt4A5Kv6T54PUPLfYFnse5uqIWoP8UI1SWKmVZDQPLFVFhEMw3hscHCAnAcDHujrSk5Gfc5O55u9RuuuhoqCNl3aLuR0YNZeAJ+WZqVf/q0YlL1HR+cmoiNDUxwJn6ynGDtCJs/NyYJs8nz14kn7twLMU7959j6Dc8WFcpELvDERf4RNzqGi4BB5MqvTAcerbVNQ2tIwjKSgpCgr2IWeXsFIMHbs2LEv7DnuAskkiCRUdTmapqyx7RXwC2oDCQnwWFW06jReKCv9FdGqRgy/WkZ0niNabxNFv0dU/jMc0FqZTXwhOnSJuiM8sKrqP4heDwi3vn/PyQqi7C+4imB/SfR6LmMkbKsahmE89jyeo65jZ6k/PARhz+zszKvEp/wA7LY85ZYinSrj9PB40GdYOxbu9ifIO10eH0UkZmjgW9dAKuaz8thM97b/FoID3EmUGx8fkdvmh7eWd3JHIhoMs/C4gFSqtLWlg3L2dI7m4XiFYdiGW2NDLVUdcffLgeUAcpaU4GCFHTv2VW3ER1aJeL0+gPJMboak5LcvVh6t5r5CSAMw1VStAPGZOTM/UU6suAYwkkKU/jkPACv6hWinL9HWyPz2gij6FbsBstsG0jNua8MwjGMV5cU6WsroWnewMeru6gAPlhYXujqaf18E09WXsvQ6758mEVGxVv0nANrZh59CuyBwq/WWopH11Wd1N6fmtjkR2bukxIVqmLyrk9mzIq0wnNnsbI5EcKPRtGQQR6K+yfX4qptryuSLDdsyLSX59ULDgoKhxTW4FhThexY7duxC2Hy0DTwDBd9V5bnzz+Ivq9iCHEme73Ffp+A43UA0yxNF/4WrrvVXROttRv/PWfLn+yPn23SIycqdsIhgGMY2ALfInkNLU/rIyBB4sKz0C42iyM3UDCmelRT1jG4E54rDCtiawoMiiMSsfNnscHBHRF3O69XBuNorTYOpm1XaTwhWVVFKnnMdjduRqTLhZbgpcUerqThGnSQ5EsEdUT/4CocsbBtuw8ND5sZUNgxTUXB+eDwCRyrs2LELr0HRJ3m/rr4UOwlkuZmR1reB/s0dGZNgC2LxH3IRe/ya+BoMWTewYRi2qLW2NKHGEuCGOmod7XAQcHZ2xs7KgLsCBn4amF43c7no93Ef4hJdh+0QjxeHyKq0hqqCvsl1AM/AzZlQc7OiP2Zstof8K2ZmZsDNOTg4AP6cpsa6sbHRLfp1zM3NOtoak+MWTpHn8LjFDodh1qzNCE01OSpFNiLj0uzWmb7Fto2ttrqCQ85BvxmYtQe3T2PHjl2Yy1+FiM97SR1dKbQComjz7l3cJo2J0/X8ZIwlfwIbC3/KMo9h2E62melpUvVFR0v5c3YaehyAMRKbaajKG5hd9357AO1PrF+PHKu/xSbgLASBauw9V5vAM4HZe/xS9gGEFvzpVEqxz/v3z/28na3MtHVpKno0VTbNvZFWWLBXgI/zo/uhpcWFFWXFnR1tY6MjW2IS40lMFDmDp2sgFZi5dzPmN0W7g/PEgzaCsmKnNcqb2F3WUIWDYXSqzOeWABy1sG0Gi3t8n9wtsg8/hdilsWPHjl2Y2/ElkD7RyucczIjUIC2cvZd85/hmkpNlThBjGUSbJlHypzzUGi2K236yC8OwVV88TCZAKWTJK/3Te/K/6uuqdbRukQ0nZs4XI6tENiDhLoJSEgB6kVVpjdvyNC0ZqoYsGhsjh8c4EtIs55tnAw5QpaGOmqujefqn5InxTU0709LcQGJgirKic8wxnOLs2P54z5cHFy51BVNzhbGZXhy4sW2GHgpdmgqKwHoGN0CehEth2LFjXyvSjkoRx3snWDUxyNxm63+hbvDlpgiF3TZExT/xVsD+jOhxgowg2DAM+6nFPgwnUYqvp8PcHI+EgqerDQnDTOwvw9a4wo25A0MLxfSMbpCCzpCrAJWn1eThuJqyAnANVeiocYuupWBAV9dUVULkct9DMktT+tvXz2qqy2emN6P40uTkhKmBJnnyXR8fDccwbEc6WHusfNjjkepKioH36ThqY9sMFhnmR5bCrP3O4sZp7Nixr+mOfHjpLmv/s5RbCiApAm7ufLWzf0NJLIaTiIaL/FpkrWqCCXxhGLaT//jmpgYSlrg5WSJaDm5LXqDsQ9jGLe7IRnFFgPf1enMAfAYSU0HNaGUIsfRNrlt6nbePOOWWcNjzxUHvpAN+qfsC0w4l5hu9ynJ79T4kPMxDV0MdIDStxUpkgX6um/CrmZgYt7XU5zT8hOGGn53ZFi/qnnB4gR1HQddAqrjjPo7a2Dbc5uZmSYVJcGW6Pz+MaYSwY8e+1kgsrGSXS+wxPcMbFGVFDVV5cxONigqhjl3NDRDjnwnGknTcjCGiP5xfB6zoD4hmGeLbQ7w6YBgmQL3Fw8WazTxjqDk0+O3753wb6Cf7EtF0FoA6G7XcRlSIuDw+qqGiQLmlqK6kSNOSsfQ875V0ILQQchgABx8MeinkbwQOfo8s3x1fey2/NbS2NScz51Wwn7uGiuL3xbHU92/W55zPzMxMTU0u88klxQUkDLPyPodh2A6sg/l/ktCmS0O6XjV4Awa+Oz/NGMZRG9uGW1dXB42iwB4bNr0OxfTwgCh27NjXJRUMytlj6nAZ9jrdltdSl3vzMn61EY05Pt/tMF9/gSj/W4ipKv7PIjNd8zOMwWRmhzlR8be8AOx3iVpJYrIUrwsYhq2wpQR4QV72j572/t1LckhJkzWU5fNeEkCdjekPLtnl+eqgfcRJK59zAIBFVIrAT/Kz5T+seNf9KsnUFqOWr7l1TYWxj0IN6RrcYIxGUQRngMFgrPU5n2fZMp/c2FDLTnRUFMycLmLpsJ2151e8y/PlQX2T66gRF/y0Cz2V3emIQza2zWC5ORm8HYl4kwg7duzrlwqyymJHaRqyqGPf19OhtLhwheGMMUzU7ufvLRzL4X3OCFF/Biovcz+n+A+JZllisoyYn10lCiSmm4iZTgzDdpCRRHzAExNil35ysL87+WSQDhpaXvX7KLFRqADgLlT7WsEHAK+Nq72SUmuVW/3I1oZGv3Mb7qYsdCo62Zk0N9Vvnu+o/2uvtuYt1PNjYHaN5I3Evv1pOcpFHVhCYYiZg6XTcCOyRHJ4ug0DAGwbbiMjQyb6FHJFcIk9hjsSsWPHvt7kbeWifqn7zN0vwJETSPR961PqO8HDWQpR9X95wFXZ/yC6rAk+Zdo+H36c1niNmG5cbTCdG5hvUSWq/oMo/mOiWmTVcA7DsC1i1ZVlqMwCPCLU96f1mYy0DyQLPNz+VFIESCwcoKCirbqPElEhGvTxeGyySWrOA2d7M9ipyAJjulTl9+9ebpKvqae7k66huKDJIx2QtRcTke2IjotyUf80CUgEyirYwhlIJUX78FMprXoYAGDbDPb6RRxJSwt+gkwoHNfqsWPHvhH78uFlohbuF6C67F05uqZSdVW5QNFspkGFU+Mq/QuiP4JgDC7yvPqTHABWLUYMv1ptGGVOER167B5I5C0qsCyGYdi2t77ebn36bbSIGutTBgcHlvOqkqJCM31tkhoepIauT49u6R1QCMbKRR7XnHlRrOvsq0pVl0cbKsDjHt/bDBrtQ0ODCP0itaiATAzDdkSvRWDWXmPbK6gXEd5xanKO906Ar751OAMDAGwbbs1N9SYGGhwhE6dLMC7hQj127Ng3aNEMyRczsb+MmptM9CmVFSXLn/6YH3oDq2FlfwN7Dicrfvi8sSxYsyr9LSyUMUZWHkBnu4jJcmLoBRwn4ya4rz9NzE/vzDVlZ8GwifGx8BCfRVXCfmpdvY1G5tLsLikVBSOrq9sDFYSX7ooo2+399oC+8Q2NBWkyfbrqk5iojf2yOjvadLSUESk/TVMmIE0Cj4dt5129kl2RVSJ+H/cByI12BOAopqaMR+KhiArRe6VHp+dGhHuBbQkdc2ybyooKc7lHhbXp0kE5e/D2EHbs2DcSiRXDUTG7sFNad+VRib68VBD6xPk5SH7486dNE3N9K4+eM61E612i9M8hpQd3c2OnETHbC46+Y5eVnQXDMtNTOCphXo4C5mHz0Z9YlziLtI2mxarPbBdgEF4uGvRZ3NDyKkVJkSyLPYgO7u3t3qgvKzvz44JaAKsaloGrYdtz/QgvE42oEPFN2ecQdRLuBSxwctBpMr4fJJFe3KdmS6FfYBiGYRNsF29i3N7akNOdfkvR3O3CRtE1Yce+IR6SL4ZPwmZcSYsgt7B3jBTllhKIToa6dz+mvN0soRMAsDZNouRP+afLmqSIDl3+ITQMw7axzczMuDlZkOLFy2xH5LaPjZa6BlKoIAaQmNfrg+Fl2wcYwNJ2gZhDxEkDs2uk6LM+XTX+yX2Qf6z/99VQX0PXVOJUwzL24mrYNmulQMS77s8P24eepmnKUpQUEf0uuPx09W/6JO9Hfb/hRXv6xssJbNg2zr4N9JMCJ4gg0cTucmghzH7wvYx9B3khP1FEcJ548Gdx3Je7GfY0H5SciYqxJ9uaKsqKNzJoMkZg82GTND8AK/qF0SDN7AvBy8qOg2GP7oeiS5OqLl9XsxLp8aKuKF19KbIv0S3+yDYTswqD3DsioUW7HO+doFFlYF2CVeC2Ntd9Hv+ou6tjPb+v3p4udv+PGpTPdos7ArJ2HGq3OrkTbIJladz5p0nYBJ7RNZACGa0GS4gcOJTFU1LU0b3pmyJJft1J9Zo4UmPbQOtob7Uy0+bw5arKg0s0MGsPuJjxTY0dO/bNw9jxukI3KtILbaPraCnX1VZtQMRkThBfA4nyv+Mvf5X8V6LbjhhLx2vKToRhVRWl5CIaHRGwgiOMznRF51yiacpoqbFhmLX/2W1JkMUCY6L+6RLGNldAwgF3VlhgTEfrVkSo79joyPp8ZePjYyQrNIgpVIqsx8tD4VjEeQtTwsDvzue9pFv8ETOXi3SqDOWWIru2fFse/A5SWzPnixae5/1S90Vw8d80DX7AkRrbRlljQ62dlQG5fIBr1c5az/vtwaUpmsKKMG8HduzY19sjK3ZVtCTbW5qheBXg67Ku4XKqlmhVherP3Oir9K8gIUePMzHTjheUHQrDOtpbzQw10UVpZ6k/KjiQmJjtf14v4/H8EDk3BbWMjG+gDf7tmjcDPOb58qCl13kIxlTYf7iVqXbc43tlJYVM5ppTi755lcC9Aw3n8dIkIjAS24LTX34f97knHDa2vgIgPbiW2F2vavA+AnmtnoGUXfDpgMy9qFDG3X0aX3VzjjmNIzW29bf5+fnkpESSkwPtB2VmpNR1f1y6HR1c8/6fJCB7B0Zi2LFjX9+Wk6fVVxLe+CFZZ12qcuKz2MmJiTUPlyPJROMlouS/cTUf/gHReIUYiCJme/BqstNhGKm/rK15q7G+dgVHSG+xDS/d5ZsiSaXAUSU0G2AbdCZie6t2Fu0GOTFwt/gjRpZXKcrs1BmdTC932y8Fnwf6v67dFwdih4ONEfc8hpnzRZjc4CGxTX/lgO8ISpqUioYW7naIPEnVkIXNh6rsyiq4ieDlpCZvYHodXF3gOT9SJC/uicRhGtuG2Itnj8ngAzV5NBSLCvLB45md1j/devD9IIlZhbBjx74BrYllolm1QZ4udihZBV5UmLuGgXIwDkp+lfwJTwWs6v8SE0V4EdkJMOznXGdpH5PJdTQxIXYlxbSRz+FFe8PLRb3f7tdSl0NKxyCJdE84HF62IyozMEUu2mUXeoqmKQtubHJmDO21xD2+V1VZ2tnRNjsrfAV0cFgk4kwWIek0aZ/3kngqY9OVvFi4C5WzwCP+n/b5vNtvG3RGV09K47Y8alUHXx8EYyoKAJVZeFzwSjoQxoL6SxQNuka/4DCNbf0tI+0DNwYzMdCoLCsDjzPnZ59UXloJmQF27Nixr4s/rjnT0FZga2aCwpebk8XoWoyTTNUQ7dq8/Ye/Jap3E82yxNw3vIhgGAatoqyYRmEn8c72pitg/OsbL48ugVQcTg+P07Rk2NkkVDSS9d9JSlYAhoEkOzBzr/e7/eauF8EZoChzwBhwGkXBzlK/uale6N9xcWG+Hu0Od03M0PxaSIEYnL7AAXcjKl1k2yrkmi+HHpi9x/eDpPfb/U4Pjlt4nNc3uU7TlIGIndVzqKkmxyqFyRtZXbX2P+sWf8T/kwQqtH7f0xteJBFdcvhh2Zm4SqlPzZbMeQYO09jW2crLisiAY6ij9v7tS1LR/utEZegXzNmNncdD8sVCCvB5wL6JlukPLTqDIz1G2ux5HBsLvZ7uTuHER8YwMRBN1Owhiv+QqwXx9+BU2EwbXj5wUyLHmEymo60xuZp2dgh8fUwzRp5UXIVZZuZebW1pNB+FkJjL46M7pBT2/ZwPwGPOMceMba7A9PqWItlphmg8gv08p6aE3Igc/tgYspnfYUsFAAwcmIVVU9e7yRD1GaJWVfC7f7qEQ+RJAKssvc7r6NxEnJaQ6pCFuGA7BLoq1OSoFFlLz/Nerw9HFR+MqT4eV3s5sU4hqV4zpckkq83lS1doeV9Mw7d3HSO5fePlQ1OtE7MDM4xxDMCwbZTdiwwiFw5zY2ptDZttbHy2920DDQcE7Nixb/7x/oaBd21tjTT1W+SOUntby6ox2CBR+a/8FIhdJszxMrxwYBjGbwV5WQvYQPl5/KMVHCGv0wdgML/Uffqm10FmiXJKdSVFK+9zERU7miiCBcZ2eb0+ALJwA/NriDoSMnmoKtyVV0opChicbpyfFwKHx/B0a0aneWDafkOLa9o6N2GWf0vRxO4KYnTEoXbt8DaE3CxyecSJ4psi6f9JwvPVQQC/Ae4ysbtM05RhfR0KnKFBNZ5BGlMDTTcny8cPIysb03snSganGsZmemcYY4wdL9eITeg2MTHe29PV19s9ODjQ1dne092JptI72lsK8rLJp33t6/3plvDk5IS5kRanI1FNLtDbY44x0zKSGltxEQcH7Nixbwl/WnMBBLSP799pLQyJPYgOHh8fW0VxY4JovMrPQd/jhBcgDMMWsScxUeQ6GhzgvoIj1H5LDMoVt/I5R9OURQSJmixiN4DBtjFBomBgrHQhR/8g6RB1Ut/4hpE1zNEDM/dGlIg/q5avG3gzw1i59HNt/8vI4gMkKgjM3uP+7DDw4FxxjMGEg7VKdqFKF4JbaLIrvGRXSO7e0Pz9vu8OO90/7hh9wsDsGpUiS9OQhdJeC/NdqDhJuo6Wsj79tp2lvqerTWZ6Ckh2x8ZGcYRdVtV9egqfhBXY1NQUwF0AZYFLzsxQk66pBFyPpkqjKNA1FC2Maa6OFvp0VXBx+no6RIX7+3jY62jdAv8V+zDi/buXRYW5M9OLkHB+G+gHFzNnHlVFMfihXmK9LI4Y2LFjX+ZcaEjBpmhdLmapJL9PekMGNBAGVx5z+3x5MFibOjHdgFciDMMWK6EMD6HVly0lXi6wlHjT0IfgtGN6RjegtBFrpx9u+d+Wd354PLxcFLfD8RMqlu4CpyWkEGb2EbznJ7b8YlF32MSsYISKU3NDaS3W/H1xqCUSMjrg8/+TQT4IU4vY8BXBLe5RLgS3gj6L+3+SAAsGRNGRJx3vnXB6cNzS65yZy0V9IxkT01s0DQVELs9qMpTj9BlyoNctb3e7Ny/jGxtq+7/2jowMzc3N4qgqqM3Pz+OTIChwLS8tAuhLW/MW9wUpqFub6YCk5M2rhKqKUnBM8osI8nNjM9SryusZSgXnYnZW7NhXOEWMfQM9skRyeLoVxLTPmZlk3IsK95+ZWVFPCgnDin9DdNvglQjDsB/ag6hg8oJ7EhMl2GU2Xp5W52LmfIlVBGNry1IpsmZOlzxeHEKzMfje/mHY/cHJuV96LL3VtmkwdYbx8wpJ+0hOfNUNfD4FYk+BSLiMja+C88SC88UA+grOFQ/M2hOYtdcvRdI/TcIj8ZDL46MAawHEZeN/VlfvJrjIIZ2GlgzlFhtxsStdiNtQjT9t1aWpgNz34b3Q5wkxn7PTv/b14hiK0ZRQTl1dTSVAVgDMF+RlLd032N/f5+vluBr0tahbGNOc7U1fvYgrKconh4opKgoOESd3eAs6duwCzxIX7yKniMMWSHR5ei5Ycw34XK2DJzVSGEy4PZr08jkZ7h5EB68kUo/lEO3azA5jYqoKL1sYhv3QykoKyUvNzspgmSzqDAbjS8Hn4FBbfWMpqgbsQkQ9VyArpVNlAACLqBDZgZwcK2x1+3G1Kqb8XF6H7+hM1+Kb3LOjWd3WIQViAEigGg4K1uzyWhHeYOPlSoG4SwRgLdenR3yS93s8PwTwlZnLRT2jG/rG1w0trunqS9Fp0uAChi2FmqyWwgWshaq7kMxwgU3+e6drKBrqqFmY0Hw87D8kvyor/bKmMnFb2ubmZteEEXhbG5PJ7GhvzUxPcXEwI+ur4KceTdXf2/nFs8cAm/G9BGAkGws9/mtVDW6WmejSbM2MtW7fot5R1tOk6FLUEVsMCOYL+wuK6Gonb4Hve2u5HbIBackEZWM2IOzYBSOHsPY7a2B2zeXJUf/0vf6f9nknHXCJPWYTeMYm4Iyl13mHqJMeiYe83+7H+9rr45X9MSh+3osIJOPb2oqJYdvJMIxb8PdzdvpyXtLT0+lgY4zWXVQH0GQRsoP129TpUkCGBAZgy4+/wZ/FQ/LFFuUiJz265Ehaq3X3OFsSamjwW35uFoDBQTFaNgFnAYTQNbhhaHnV2Payy+OjvimSkBexZBd7HqxoR0MvtJUITojni4POj45Z+ZwDoAtcrqQqF8JXIN1kk8XfYfliLYXcTlWXN9K96+po7uFi7eVm+zQ2OifrU1dXx9jY6OTkBA6UuBomXGtsqH3/7qWzvSm48JaoUAFU9jLxCXli62qruP8XQiwVRUsjvYSnD7q7W9HTvg50Dgx1jo739w+1ldWlOLlTHN1vu/tpxLyxCo7R0je6QadLW/tccoyEDJ/GNlcA0EJtt9/fHZRbila+5/CePXbsAtF3eSUdADcUWolomrJUDVnuvQ+UWcHlSU3OPuIk3uNYB4+vuoko09pbW/Xpt1F806erfkp9hxcjDMOEbNyamyb6lOWQBPRPVoU+MEFk9IhxG7iuvpSV71n354fDWSV1fBsvj7Fjl+P9E3SatJ7hDZfYYwA1wTmxkl3ccZZkgwgrgoznMckG4SE+lqbaC9PwrBh9G8JgtIeNyFG0taWNrK8AYGbtf3YHfR1w6A41b4gi0Tb3Z4dtg86YOV/S0b1J7vFzsNYd9nnjrHbsyS4FhNNoFAULE9h59SzuUUry66RXCbk5GdWVZS3NDZ0dbZu5mMNkMoDheL2lbXJioquzHVx13u52ArULxjwIBwAMvNBYn8Jd/jI0v+F+X+pNgW1WfUB2m2tau0Vqm/6HFr23zeqJjbJxdZce15yJKj4cVSoZWbY3sgIkiLtCCsRgdQtEoQpR1DEVkLHXIeqEvvF1TdYxufUJ9U2uB+eL4zQRO3ZBOuR3gRVKQ1mBXVJWg5uAnD0Ort8R4ZPXmwN4m3sdvLA7EMXh5sYGS1M6+gr0aKoTqyFOxIZhGJ+B64kE+jpatwAk+0kj4vxMbqdPRPE+Q4trsDtFDSayJg6XXWKOheSLobIDvnsFGAwr3K1rcIPc6zK2u+yReMjv4z7/j/sQhwdk//+4zybwjJX3ORP7y1SKLMx71MiILM+NH0gOdBSs1ZUU78orAYwHvpptT9EBmU5Y6Mst4bCV7zlztwumzpfA386GqQipItDFOldUdVmalgydLq1ndMPI6qql9zlrv7N2YaecHx53fXrkYdqdrKLHNTXlXZ0d3DwEW8jGxkaHh4dwvN5yNjU1VZifA3CUm5MFJDPUUOTrJCTHEbmdDAs/6hUEL7ENPg1vE1aveDiL8HNZPAFoaoW3gM8q3e/yfHVQR+cm0icEx9czvAGL8LgUhh27IKUwsPqgzVPuG5x9j6vCpQrVscnNDiufc+Auxqdu7RGy+NcJdo93R3srav8GHhHig9cpDMOEZp+z08ilOjvz408aEcdKXtSqoMDh9OA4jBS3FG0CzkRUsGs1+L4VFIaFABimL6WhIr/QMqSA2E0APAC4yy3+sMvjo3SqDDjPKCgjlIUSL7qWjIHZNXD+kSKwhfsFbW1pdBwAMLR1bhqYXQdJEoBw25uqHnUeBueKu8UfMba5gig6ydwUrWqockvTlAGZopnzRdugM76pkgEZewOz9oYUQIwKEG9UxZ74uqtFvUH9k9XzBBNHOmzrbD3dnfFPHywyx4X2XFQUqBQ5PQMpU6dL4GZ3fnTcOeaYS+wxcNk7RJ0EYQROLcK2Jf6WRXA7gJDieO8EZ2RUSOELhH1wE9kGn9GmSxvbXgnK3gMHV3Bgx459maR8lSIezw+jPRT0U1dPysLtAli1wbLu/OiYW8Jh3w+Svu8ljWyusCvPrBzA/fkhvOW9Dv6y9jZzfg7F58z0FBqFXbH8mJKEFywMw4RjANajqyo6ImCJLf+hqeakeq3QL2Lc+MHrzQHPl4dCsSjw6iAESKRIcn/2oB2rxsj9CHc2BtzM6RLIwALS96JMiEOnnrPHJ3k/cJAbgd/B8YPzxJba9t4G1CYlUBvN2u+sDkuoGiFVmI+qsCWSdfRuAtzlEnMMrGTgjAWzZvBIFhPgD8qOfWgybBx8PzTVilWSsW2U9fV2m3ErIC+UvzRY+wh0qrR9xCn/TxLBueLcDJ8LrDOiwXninq8OGlpeo2nIsCk0WK+lqCiAu8M/XWLp0dPVjLZGVMDBS/TBcEjHjn2ZuxgAg4F7lqYFb1hNVXl94xvebw/AJZtXJQWs4JFV/+mReIg9BsLaWDGxu4xh2Pp4bf9LMkoDJEYG55LiArxsYRi2WmMyGY4L5BwtzYsrys0xp4u6I6JLDv1oCAeT9qx6PEzU++1+u5DTunpSICLD4piq/AJfGQ8jH/gnVUPWLf4ICtNh3+ErWBcq3UUCDNhNtE1LlGiIDvxi4XGBTkWEARzueJqmjLHtFbuwU+DEgvQUMf/C01LCYaSMKjmYVE+t6U+cmB3AQQ3bxlpxUb6tpf735S8tdbg7bu1/NihnD5vJ+gflLNa9Dy9yt7gj7D0INTkd3ZvgLkBzkmu+IYLnwbBjF+R+cYk5RqXIslhzFLTp0gGZeyPKRRa9jwAS80vdB7sT0TyCmhx4oe97yYgKkbDtu826Sfxp5Q2QBqNAPT8/7+FihUK0l5stXrkwDFutDQ1+06OxJZvb21qW6ELEvtZIDOCEoOw9fin7QvLF3J8fMne7oG9yXc/oBupURD+pFDmX2GORlSI7OewiABZSIObx4pCR1VVU8mJBL1ljmys2gWcATPVPk0C8JuDE8gHRmPLzb+o0ynof/kgAABu29bSG+pqQQA8+MkMdyh1duqKZ4xWfZElwqcPLeHkgB17tRbs9nh9yfXKUtQchhnfKsGPfbNzI4Ha2DTqtfksRdbtoa0u7xR1BG4tLjJDZhZ1SV2RPirJfxVrsQgrF2P0dmBptbbygK5CM2DXV5SRV8seUJMbcHF7FMAxbuc3Ozrg6mrPIOZT5pvkHJ5te11HWpzSPt3O4W+zgnFLZAtNJ4W7PVwcNzK5TVBR09G76JO9fOlLvhFMEslL3Z4cNTK+TfIYAiRnbXvFLhaQmqIvj+9XoccWl7DbXjpG8WSamkse2SZoRmKkfkkiGJJRahQX45ealpBb7heccQxKuK2j5ZrcsluJ5XezYN98SVihm6nQJdRiixQvK1fysoRe8MDhfzMzpEmVBuA9BOCpFVt/kOhwhey/p/0kiHCOxNXGxztF8MnSHBnmRQbujvRWvZRiGrcr6+/uexz0q+fKFkxzMz1V+fXy/9Ng6UQV+2e33cR9CIPhu5z85RTCjCs4TA2AsIGPvDu8FRwuYkfUV1LGJBuq06dIuMcdQR9b3L3lYdiq9xbZl6NMsA6MvbJvIRkeG/b2dOQBMTY6qqlJWVsBgzhZ2BkVXSuB4iB37NlzTv+w2sYNLGBKboWnK+KctV2QVpUmO0SdoGrKUW2xJFRA64HYkAGbqclQNWdenR6Jq/hPvawvdE6plmfNsAZhvA/0mC0IgIIzjghiGYcK0pqEPz2vl1xNmWPmcA+HD0OKaT/J+vJHzo0aj5Xclbe9eDivvcxRlRTYp/215c5eLgVl7Iyp42q7CisRf1KiU9ES3DmVMzn3brrcqFj7eutbW2ozaEMgi2LO4R929jQ19qfH1l39Y/irEwRA79i3sEWWiACZpLGAwPaMbPsmSghHbsHZm/T9JOERAclTKLUVu+kR4TAMoQPq9yAT21XvDt7dkDC8r/UJKiWCuDgzDhGODU43vG/XXecgnJF9MW1sahBI12Vuuj4+Gl2PmH+w/7LNyiz/CpuyHvRyK9uGn2APKC8+Jq5Iq6g77NtmAgxS2zWkMBuNzdjqHEVFNjnpHJSM9eWp2JKPDcukKGIiWOA5gx76FYVi5qNP9E3cVlNSVFHUNbgRk7l1ZzsOiJxUJytmDmL24ebxgl76SopX3ObY2ID7twvOY8nPTjBEymEeHB6BzHhLggZc2DMNWZcz52bqB1/dKjm5Im5mF+wWqOtS0Cc4Tx5V07EvAMDOXi7CXQ00OrGHgd0Q2FV6090nF1c8dnn3jlWTPADZsm816ujv9vZ0dbY25uRDdHW3aOmq7h8tjq0/ie3yjeZJ2obFS+BNXErCvTWNLcK44WLwsPc8HZq1WZA9cpZFVIr4fJJHCDZ/KhZnzxYAMCcTbsei8NPYVeE67B1dI7yIne6sqSvEah2HYCq1u4E1cpdQGst6FFIjBqadSzHeM/SftHA5RJ9UVlMAaA6B7aOHuiJI9JT3RI9PtDCYW+8K22e3Vizg+Pvq4p1GdA+Ufmg3CSwW7F0DMxAFB6B3yvimSLrHHjK2v2AScCcrBe4LY1yrnYQN+oSQ8LA4eE4fLVIocYqvSVGNHGHUlRX2T6yH5Yn4f91n7nfV6dRBLqwuFqwMgsTnmJIrqeTk51LsQADvbm05NTeFlDsOwZda+GDnt7lNzQ+MzfWktVpth/cP7NNiXc50E54s53j/h/W7/gnqS2Ou6u5j5ENuWsIK8bG4YFnMvCjz4rOnqCtJ9DMOEG1giKkQCs/aw5QdVFO4qKDneOxFZJYJ3BrFviQob2st2un/czOUiFbJ3KCIKK43b8kbWV2haMuqKSnoGUkGfxfElLRSPq5JqGGTPiRXm5VLvKoGQ/vbNczywjWHYsmxwsglcRvFVMo/KzuLbCfvWSpjCWSrM3A/GV0n3jpXhqIRtk1tjQy2JwWzM9JlMxgxj9F7ZQXxfb+wOIHAoCmJynU11gAb2KLJ2oadD8sWgQAgui2HfClcylKmoEPF6fdAm8Iy+yXUoDK2iADk81OCFDX5xjjkWgcfvhefvGmlfJypAbE9MiAXRw9RAc3Z2Fq90GIb93Aq7gvD9g31bbQd+ES/tuY8DE7ZN3ZSY+JSEYYHecMDg60Rl6Bdc19oosgQR8NPM5aKR1VWo/64qz9MyqiYP0lZD82tebw7AAVRWCxmuJGDfCiOOoghr+aXugwKb5tfUlRQ1VBXAT9vg0xFlGIYJ2d80qw6MttTWVHDLPmHDMGwpe1Grgu8c7NvPv3SF4tiEbfPCMK7ZMEdrc/BI81Aqvm3Xk4EDpKfQK0QArHJPOGxsewV1IaL2LaRGyJZgYn1N4HfwXwZm11iECnuD88Qh+xxOZLFvkfoYYlN0iDypb3zdzOUibGbGpd018NdNKniBwzBsuTbDGHtQdhLtl+B5TezbqiZWsiulVX9spgdHKGyb0Lq6Oqjq7PxeT+vu7NxMaV80vm3XIzL8v/bOw62ppG3jf9K3uoK9revuu66uvQJ2pEgoIkUFBRERRQQBQYo0BcEKCIrYEJAaEhIChB4IJZRAgPTiNyeHjSyioiaUcP+u++KiBBLmZJ6Z+8wzz1Qvjq/95UbuqosxfxJdTv7dJ2Sz4zEryoDRZeWOWrt777wY++fVB+vOXd3McNpHGzDDT8kjXV33uLrtuRDx1/UsqtTBf/YzY2oLzeU3P3cRMWBEC205l+qkM9U3izsDP37ExjDYsGnQISkhXTE4gxpsLqdsQJYFZDaZGHQORgZv+6CsEUEKzDXkcpmPF2N8meWIdWdX0/uOi+i5plsNoLaSsqnFq1sfLAKTfmc47rc/aGN/2Jpa9fr3+EE6HfHclS1UmiL3F/3dycVRxZZnr2xx0D/MUHeOPHK8Et0xq/NhmyLfLyUP1le9WzS+8qD/cvJrwIWA5kQZjwX3boyp/PX60zURb5fN2Cy3ujsBwxxs2Lcp7gyKLFzqZL//+N7Dp8/tQIlCaOoZjD6KzaN0o7AXK50Z++wP23hf2hpfsrFd8g5xCsw1rgf7GWxYYzMrr9kB0ca4N7/ptEMqb5BJrQOQsOB7Y5OL817KUB3/z+4vareM9cFzV7fcfLU8usxi4lBIH3ob8njt+fBNTnb61bBj/zmUyf6QjYvrntCc1eRZLsb8efrMTi//bZeTN1CnaNBermYRfQQLbnRC0OzYsIolxIPNZFHZOKblkLwdwxxs2NczEsfu1f5F2TC7/c7Oe8MLVuCQdehzDxZTbhFVZBldNm+KB8RzF5HJkJ2NDX1YCnV/oWJlTV/SvOuhfaKeosJXudkP01LjE2Jvkll7yJUL8bfDyZc5TzOJhJ2Cn3wKrVaDorqzRcqdaIMNKy7NyazbjoBjxHsxZES7lLAxOGO9X9T/PHx3eF7YRhWIOzzZgFFLW8esiHG6kr7+VomlfhVr8edhMI5DZR6SmdylOxvd3HdP+iNUWUW7A+5eO+ntZFRJusPWHr7b/W9ReY/nb2zyvrTVw2dHaPbqBN4vuDoQNCsJmTP8jE/rjyg0EkPA7+3t7uyAMYMNm4BguJgeYG7krSSjC/aGQVOo8teoYsuYyvm0lzeuZtH1rDWG+Ra108Nz143nK193nlaox+ZL96zlsk67Hp80X5wkF8dDodf8yz68Vyp/8MRqjUbzpR+p1SqZFIewmZC7ybH0dbQ/bPOuIi2FtwYBxzg3YmoWBd9fP35ikt4RUdZIX2xj/Mt/V8OIfbI7SJVApLMQvzlRo1fYbn2wcHXbY3/QZuJfGy/mYfufFTY66ZF+DPnozNgblIrkfwhaKHrd7klCfWNDXUxkiLvzUdeTh7Me39dqNRj+YMMo3gsCDRtpkI4ITanoMgsy55hneeRMyj2eD9tE7d8Yd2LUx3NXN8c9dE5NvM2urpzLHXNsbPRRZqrryUNf92ATRR4/zT+u0zPpmyo9nR1tBfk5Uuknpzo8PIT4bjrS7yaMz9cPH8wrikioRRA22o0Y0tmJv5qUdkg+egdsDYj/wzf0b8bJfcSknfLcdfXBuqgiy++yRmS4DHux8kr6+supG4i8L21lOO7zPL/9etZqhuN+utwi7cocbScvu5EfXc9ePem0QwiCzDU1uqr9bqC/18RQ0CfqxfAHG/ZRoZGk1vxhsGHjCfToNtBnulViOf+28zKpO+I3nq2iJlsHbei71PTciARBJzur6fuWGWNEMlxfx7kTF3HW8+Qkl+XufMzb3T448NztW6HRkdf8fd0nPYBhbx0fE8ZhM7/rNpt0bDT7aeaVS2cvnHMjcnY8SBVP9/MMCwm4ce1iQuzNNwXPEd9NhFwu9/NxG5+dHz18J8s5not0NeOkHkW9t2Q47ZtQaJ4qp+HutTPkyRr6TFvyMeLNsmsP11JZiNxFP7A8FffvxrN4fbJi5Ltl1OHO3F8uJ//uemoPMXueF7a5uO5xst9vWJanPdgJq4MBcX/g2FwIWiDbOmIrl3qesbKnC7HqJWhvxQgIG/axbeidwaxfz1p9PmxTYOLvVBkGVHOCJqqCsmF0fdv5l5vEWUQmXhci/nJm7KUNmEHEiT1+cHdoSDx1qp5aPTPdkLimgf4+4r7evck/f8bl82UuT7cTb1+/GB4eIpbJsIqlVquKCl9d8vP8/PEdgrYpn2XSghj5m3eTYwP9vb6+yEaVjmhhjSiFuo9aBHrjQq7paZfxpFMXuyOJrw7jRpixbFh0qcUpr112NvqkwUM2pzx2XXuwlq6UOPEuNX0Ks7HuedN1EcknxI9RHyt/jS5bEvJ0jZf/NsZJquS9m/vus0FbfEL+JhEVeYkQZM6b0CZMpGPZi29kbwi55sOwPUIHfDLiYwSEDfs4phTlNp6MYy8mQwLDcZ+d9UHHY1aR75YhOxGaVF+Izkskc4v5aNGpEtWcRVHvl/pc20ynJk7UmdOOqUkxr/KfvS7I5dWyu4QddTxOYnzkxfOn8/Oy+vt+JHNALpeJxQMKhZx4PGKKyGy7v19E1NvT1drSyK6ubG5sIFGYPFENuyoyLOgU4+jn/ues58nLF73vJt+WSL6YFqhUKMgr9/FmTPzF0Gv+5NknGDAtnXNIXoawU1BV8YH4N0OBvv/I9oAhh9Mg+8PWYQ+3F7SfwhEoRkcqHTvr4UgtkthanfM+mVZqE4tENeMlAkW8XXY+fJOHz/aAuI0x5RbU6tNMha/xOvX62oxkhCVmLzx/xfWna4gro8IRe6oSIBAEmUfwYS6OeLMsqthyYjdP4Pz6piFQ2N3iwXBg2Nv0dAsxAsKGjd8Wr+qJuRj9N13ZyctvG3UbDyME9JkTm49LYZPMWEzlEjf33XYHbagUbdupF38Y9taGE3WJ3JwO52Y/HBzo/7znqFRKwsiIpL2tmaihvrairPhR5t2wkAAfLwZxVr5nnL1O2ZG/5u58zPXkYXfno84ONtPZ5UV+Pf95lkwqnWYBw96e7jtxERP/gofbicz0pBe5T3hcNnFccTFh4aGB5MWQ/24q92XtfYrhYnf8dlRofFzo5ctuDIfxhxF74OS4P7xgRVVPNKK80SG2nH6zEevr7nYwtmQ9VkiM68ToxS4jLnn97IvB2ApBCyALMeTpmujSyTvqSQTg9KYJu1rqamsx/MGGfaKjs8nN6aj9YetTHrsoD4Z5AGSuu/bZi0Ier/Hw3e5kv59e5/lUxIwqpGb1JW920dddoZBP7DXEdwX6eZ4/6+Ltbj/9KhpfETFpgf5e2U8yWlua5HLZD3Tk4IsXvvT6/1MhwNaKLhZHfNfVS/65WY9Fom6NRj043C2RdXN7M+KfHSPGzLAUdi54Cxk8WsSvEOWNDrHxVy6dpdchT9paheaumn+1avULPlQ+HmsxvfJj2C5FL/vQ52XFkS9Z46dmxekfPCmBhz7y2FTVehD9IAia4SJhU0WeZO7qUQWKc8CGTaC5ke/OOEGmZWe8j4fnrsPOBMjsndjtyl9vvl5+PXt1YNLvAXFUwTSic9c2e/ruOn/Gxf6QDbWN/qiVt6vTZT9fpxPU+pUb48iQePBTLtnYaFDA2a+7ndMux08xjvp4M4IDfYhb8/M5RUTs3NXLPpHhV8JCAm7dvBp/O/xBRkpR4asadtXQkPgrheOnN6eXX7/i95+Uwn+TDKmDjI5YM2wPn3a2Cw7yTk+PqWS9kivGyyGKRuvq+h8VtLsn8pZGl1m4e+90pFM39b9OGiqBs0Qsa0GUNwWx0aGGGhLXHq2dTxGY2mdFuanIwqURb5aRPhX1filxkkF3fwtK3eAX8ZdPyN8+IZsvRPx1KWHj1cx14QUrbpVYRpdahL9cQR5P17egTFrNoqiipTdyV8UyF09zsYh6ZPW48aPEXEyfjIz4ZswMiMpfcdg0BBldjxsOyNQDGPtgwyg6O9q99Pfyne2OdHW38gez0EOgBVG2iLWYLg1qmAhSxdOqLQsbwl+XJsXduVxYlE3XtBAK29JS7hQ8z53YcUYkkvPeboalJ2fHgwEXPOJiwl7kPeXVsquZ5fz6WolkSCweoNe1lEolXQ7eKGU/VFrpkLy9behtdU9CkSDofXvge0FgkfBSaU9wasFJR9sD9oes6RxjKsfy5D4i4qzOBm25nbsvo4Txst6/pO1GWVfYh66rxcJLT5sO0DMtel57JvAf+8PjS2F2NjbEoJImusf5W61VIMqbgoTYm4ZSKD7XNs9pGza+04naaUmn+YXmrD59dgd5gzk57CdydqI+MRyQ5WD4RF+93cl+v6vrHrdTu8kn5GEXb/9BbGfQvd+IK/M8v93+sM2lOxu/XcNdX2OD2qpabBnxehlxgOQT8mXY85X/HruMEGecFEpyXUjQiHy3FE4MgoyrZ3wHnQ4lr2DDPn5MjI+kZwCFbwvo71R0RaCHQAvWnpFZYCpvbVrNtmeNDu87L3L7U7skVRJpj1oz2YQoFLL2tubmJn5dLaenu+snF7K+glanGlX2do8wa0UZxYLgbL7tXc5fccxlU/4LZBp6NWOdd8BWL79twffXh71YQeapRLf/La5NzZ5ZU9zhppPEfEI2G4pJknmzp982up4beVKEeBORkZY4nix63MrFee/3nl5l6mJfn+5TcBbR5QdDn60KiP/jbNA/Hr7bT9odoI8tpjJd9cmu5KPh3zF8/p+12WPUOVrkR+Mna+ntGb1mS35EfN1kJ6a/aUKLTnS8nLKBeDnSVgxH6haDi8tet9O7yR93c999OXUDvQED0cwY8XDx1cx1N56tQskuCDK6WsQvMfwtdBv27k0+fUZQXEyYWq0yfJ8/kJNQvQqdBFrAZY4+ZXWTqWcCxyK9flMW/+jLZo8iweWKrqgmcU7/KF+plhrdcY0ouoSS8tq+jNLO0DdtvnmNjPu12+9Ur/6urEs6QYvek0PPX7++PYY8MrrMwsN3h8ORT1vCTnnuiqGqulEzsA8d1xHiTReHDb7F1W3PrQ9zxYZRJW3KlxBj43P973NXt/iG/k28PXmFxPCML3AdtaaM1pTbEe0OnPM5HnDRxe3k4WlsVjTU5LQJSv3tTsP/jWcb6n0XeetGvFsW+Y5a+AqI/cPrwjY6Z3jc+/3r3yiPd8zK7pDN2eAtN/JWYgHHWO8BtCQEmULJ7A29ozUYAReuDWNVV9Aj3/VgP5VKOemnDQPIToSgbywUJHPXPG7cV9DuXtET0Tz0ok9aK1MPflc3lKuHRGNc/sCzcuHNvCZGRu2OhOoVM1yWgDrotmjp2aAthlxE8onrqd033yw3rEvwB3IQ4k1ENbN83I0cs3Lz2BVTsWT20+qYvybU/hL+asXpMzvtD9p8SjIkvuvYZN/l5LCfmC76cy93+5jIkPzn2fxGjkwxolYrRb091VVlmelJVy/7+J5xJh8Nx1VP4ceOW10Ktn1bE5XNPh1fvu7mq+VXM9d5X/qHTnckol/J+Aug19Y+O2JB7w+tgjPW/cDhyKT3JbN/Q3CDoB8QVRgQ7fA9usfZLFN9cc6g04Mh0jxtmFKh8Pd1JyOWt7t9l7BjyscQJxbPXI5+AkHTVxJ7/aM6q5fNHpVdMS3il31jPGK01FqF7qNWq1MpNaNieUvb0BtWT+Kr1jPkkbM/52P+GlO+xN1rp/0hG4MZ8PTdHvV+6cTMLtEYDpo0FcXvXxv8g3fA1kmHfs7CAoi+qHpA3EYX1z0Oh62n9EtuTocDLngkxt36UJUj6OHU8dg5TzObGuul0rGvjzv6bF558fs3F33dJx4L8cnUnTgYcvliZHiQh8dhJ/v9hjW3iV5rfN+j3QF6K5qr2x4Xl73/cWLHrFwYe6OKPr2HY6ssyHB2p3p1Emt9GmfLozrrZ3yHN62+pZ03SGfkDzzrGC4hvVWiECrUkq6RqoIWrzus1QhoEASZVGS28BUbRm9QB2Zow8goSA9X3JrqrzyMzL3IoIV+AkE/Y8zSuf885O3PqN2ZUrMxjrl0rm3Ej3i3jMxlx2ex+nWG8IIV8dxFE3MnyNwUId5EpN9NMNiwH1vDMW4FjpuvVpwL3kLXC51okJwdD8bfDq8oK2pvaxno7/vJYjPEjHV2tLOY5Y8f3PVwOzH1MeK2n5s067jbIc/LQ1NeOkW/+jOqyDLizbLoD0vjK9b6Xtll2NZIN2bEbc+WwdfdoxWiMe6grHFYLhhTivS3ReTTeYWjyu7avoxHdQcQxCAIMp14fQ8xDi4sGzY40O/jxSADVfaTjG8+uFNSmshag34CQWa692PxzYLlVFKZ7XhWGMNhf9T7/9RGe1J/EPHddIRe9R8/Jtthf8TbZbNYDiGOtdgv6n/0mXITzU+2fqXrS3kTP09rS+NZD8ev7x8LuXIhLTW+lss2/JZU1d89WiUcKRuQ8aUqUf+QICYibOKv+J/xNsZ2TU2zOP9xnQ1iBQRBplB89XLRGE5zXjA2jHiwgAseZIhKSoiaZtZp9ygzjbsFXQWCzNCG1SwKTPrdMO12PGbl7rUr+r9VIvKbTyO+mwiZVHrOy2l8Y5j77uiyJdM8OMsUHiy6zMLFZa/D0U+LYBFhgbxa9gy0Q3dXJxmS/H3dPVxtT7seJ08d6OeZlHAr/3lWeWlRc2PDdP6IWq1OTbodHHjuyqWzfj5uFWXFxnp5uo9awXBxXhMjkbUOQQOCIOMqo3aHVNWHAXFB2LC01DgywhEn9l0pJSPKrnucv9FVIMispC8F7nFuB53N5UCcwKndtz5YTKqNViQIQnw3EXU8jqEsyrngWTs0jM6E9A7YOjERMTryGr2ba8Ygo9LwkJiosqLkJ5/687pTRmFMKarpTblb8xeiBwRBRtSLJjcMiAvChoWFBJDx9cnDNKl0jFVdkf00U9gpmM4vDslb07nb0FUgyIwyEheFPFlDpSPajpcLv3Tn93jOL5MexuyORXw3EczKUoMNOx+2aRY2hhErzlx8I2/lmcufju12cTyUn5eFq/PFNUz1IKf3XmbtbsSQOa7YKouE6lXJ7A3p3K2P6qxy+PYFLZ5FgsuVXdFcURqv7+EzvgNaCZoj4g9kI7qavw0bEg9WM8v5DbxznifpEdfHizHN+45jSlFeEwNdBYLMJCWds8gv6i+6RqLDUWt3713R5Us+Pyaobegd4ruJeJH31GDDLkb/b4ZtGHU6c+0vF2P/cKALwetfyWmX46zqClyab+dAauUt4peF7RexfXoGtYQ+VCCNs+Uhb38O/0R+s3the0CFMKq6J4Ejulff/7hxILd16HWHpKR7lDkoaxxVdivUEo32i6ujojEucWX3OJvRvNDsKqF6lWC4GKHVzG0YTeHbl4biV2mpcdNPUFRoJE/rj/z4+6wSPQ2C5pANOx++ye6QNV0mMSx/heGgsE8rZsylw3IB4ruJeJiRYqjsdzZwa9wM1uegzosrtvS5tpkuzUK/jEt+ntPMjwATUkXaigSXk9ioKvxzi/PMZYmstak1/8vk7c5qOJrXxHjbdqG0M5T4K2KuWsWvu0aqBqR8iUIoVw/ry10a80glpWa0vv9JRu0OXAhoFkXCyIiyC0HV/G1YdMQ1etB9kJHyvb9LnFhuo+OPvcOiy5agm0HQ3KnPEXx/veMxK/tD1pdTNky5FHOPs0mtlSG+m4jM9GTDycUuznujSiw/X400US5iHHuRl/82Oxsbw3HMQQFnRyTDuCg/xphSVCYMT635A4FlYk4gMVcJ1auIv0phb7xfu/1xvQ2ZP7xqPVMsCK7qus0V3W8afN4pKRONcYfk7TLVoEoj1epm7awk8uwNA1lP6g/h2kGzpcL2iwin5m/DMtIS6XF3mhWoJqHVqd62+aG3QJAZOLGrmesuJ2+gikNMdWpwbqMTgrvpSE2K+XTU1VFrciFmJi+RXO7QnNWGk+IY9tZJCVESyRCuyE8iVw8TU0F6jblvuFqZzP594oar94LAyq5bnN57jYO5dEJg3xhvUNZIzNWosof4K6VmRKNTzouLqPuoFY3VlnaGkf8RYwQ086ruTkAsNXMb1tnRHhYSkBgfOc2a9VM6sTetvugtEGQGhTq+UqCvtDMUwd10cNhMgw2zP2LteX47tRrGNHk6YnSZxSnPT+cdZz/NxLUwLj2j7IIW7/mXqFy9PIm1/h5n0wPevqyG48+bXN+1+5cJw1k9iQ39WW1D7z5tuNJ8bcOVeTCq7NWvcP4PwwQ0w3sgOyQliKLmbMOMdc+ouCMYHQaCULsJ/DBxMWETF8Su3PstnjvZFccy9apeTIm5+CfPFovn/nIpYSNdmkV/MvJ5uVyOC2EKRGOcwvaLxNXMmq1iLqcTAu9xNt+v3f6At/dxvQ3xV7mNTm9afUs6Qpjd8by+By3il10jFX1SnkTRIVcPqbRS3UctLt+/K5xiZnfcA96e2CoLjAjQzIiYf5l6EL0PNuzbVAij0GEgyFyzjwbk9QjuJqW3p+vMaUfDDjEnuwPXs1Yn8H6J41CrlPGcRbGsxTHlS4ii3i+99cEiusyCKmjJWkx+ShT7nUtncexF4QUrGI776bIcmelJGo0GV8GkKDWjjQO5WQ1HjVrQwvJO9eqUGsOGq5PjG666b3NF6U2DL+gNV8NyAZ0QqNbKNDqlTgdz9aN3nXVaYlNfNnuQlsfQAM2A8ptPo9/Bhk0LZnccOgwEmZ9SuL8pNdLBgb7UpNhnWQ+wZmIicnMeGRbEHI9ZOTvt9Qn5O+jubyFP1lx9sO7c1c1u7rtPnd7lzNhHPnE7Tcnr4razV7ZcSvw9pvLX6R/6TK+nefjsoMvTBwWc+eG8dPADtIhfZvNtv3mZPtVkrzPUZL9ULoys6U3hDzwTDL/vGWWLZS1jShExePNlw5XZ0C9teN8eGMdchgECMrXYvcnocbBh04Iruo8OA0FmpuwWG9K705KSaIcQExmiUqkQ642/WqJU3r4V+smJHbey1x/k5XBU//GINfFmlI5bjX9yzIp8k3rMUWtizK6k6fMYv7ksRlVHXOwf/adhS1jh25do/Jmna6TiXbv/61YfYq4+dFwn/orVc6eu/1Gr+HX3CJPM8iUKoUItMXpNdmBEBmR8cuEe8vZjmIBMuHObubRt6C26G2zYtKjvf4rMaQgyHzF/LRXeEA/1uTocNziEhjouYr0pIP42/na4oZ2nkO2BcSdGZHtg4nayk7ZWwRnrqBKLzK9dzfEtYYet6eqIqUkxaHYAfgatTtUifvWM74DxAjKRktkbFBoJ+hps2LRoHsyf3yv1OF0aggz34TiLqlsfxESETjQD/PpaBDoTodPprgf7fW7AiOlyOGLtZLffzX23i/Nexsl9Tg77qYWyY1aG1TOHo1Z0sfspt4rFsRfFc6mTCYhho7eE+Z5xFvV2o80BMAptQ+9++DxVCPq63rT6oovBhk0XwfD7O6zV6DYQNN+XwuKqLUKjnOlNRPTRUtlPMpCUaFLkcnlDHbfw7ctX+c/y87JeF+SWfyiq41dUNWU8ZZ6KrVgZVWwZ+W7pzdfLrz1c63Fuh/3B8cOXKat21Nr/1v/i2IuJ9AUV9TvBWOTLRaHPVp0J/IdaT9N7ME+3E73wYAAYG9EYt1wYmcbdghEEMq4qhFHoX7Bh06VrpCqZ/Ru6DQTNaxsWU7nE3XunYR8RsWFFha8Q32YRsay5qjvmUcN+Yq4SeL/cfLXcy28bZcNsx7MWiWcOiNsYWbg0utQipmLJrRLLm2+We1/6hzqU7N9cRCJi8NCYAJgIpWasti8jnbt1xsL1HdbqjNpd8czlGLnMWC3iAnQu2LDp3xOqTWFvRLeBoHmclFhDpbFRq2H/Tt9dTx5+8jBNqVAgxM0iuo/aTskH6jSq2r/iub/43thkWLHUbxWzcnLY73pqzynPXa6ue5yd9k28gkTPsh+iDQEwNSqNlD/w7FHdAVNG6SVp3C3V3QkShVCrUw/LBa3i16WdoU/rjySzf0dhfTNTRu1OlVaKngUbNl0GpPxv3g363hNvIAiaYScWEPeH/UEb+8NUsT56Hn/l0tk+US9C3Fy46S6Sctldd4OvnaazDQ17yT5VVpzwfaKKsmK0GwAzhlan6hvjlQnDE1lrjRiZ07n/EPdFZllqrWzK55Wrh8XyFuFIeU1vyosmN+LWUEHNDJTb6ET8NroVbNh0GVF0ZfJ2f+UtFVVkiaoYEDRnFVu9OLp8SfD99ZeTN7i57zYktp3zcsp6ch/LYnOE4eGB2Fthzo4Hv1Jo8XqwH7Oy1FQLdDh87Ien6VqtRDKEdjB7JIrOcmFEas3/fiogV1nkNTLaht5+yX19CbVWPiDj1/U9Kmy/+IC3J74a6YvzVWWd4ehNsGHfwZhSlFn7RScW+X4pbBgEzfVaHZxF8dxfbpVYng36hz6xiirNd8RaLB4gfVyj0SDQzQUqK0ounHX183HzcLUlvsvJzooYs7Mejlcv+5SWFGq1JrxMsGE/Y8PUapS9WSjI1eKKrqgHvL3ftTYVx7Qk8yji4gakfGP0Vq1E0dE69LqkIySHfyKjdlcSaz1GunmkDkkJuhJs2HcgUw/m8O2QlAhB83tljEWV3bvxfKVPyN/OTvv8IjY9LLgUctkv4IJHyp3o0dERxLpZR6VSqlSqkRFJQx23s6Ott6dLKh1Ds8xl4GAX5EXX9oyy85tPxTGXfj3qptb8WSGMGpQ1aXWmuo2i1allqsGe0eqa3hTykvR7SZZgvJvLSmKvH5YL0I9gw75ncqCV5vDt0XkgyAx2i8VWL44qsrxT/39X0n6zP2RD57w9fXgfgQ4AAKbPgIz/ps13ytNWc/gn+AM5crV4hl+SWqsgr6q+/3Fh+6VM3u6E6hUY9ebEyMu0TGStucfZ9IC392nDEV7fA3Qf2LDvQ6rqz208ib4EQWaQpkicGH0aFVUEQp+meDPKV6WWI9ABYJQlMqySLRwGZU36TMU9ZKr9pP5QmTC8Z7R6LrwwrU4jUXS2Db0pF0YQu5jdYJvM3oAR0Li1LhOqV6XW/JFRu4uYq7xGxssWj7dtF4oEV8o6w5nd8XV9j0j794yyxbIWMotWaaU6nRZdBjbsR4eWj9rC9gB0PAgymzTF4Iz1zoy9Ls57bzxf+bhlh2C4UKNVItYB8FPTX60WNmyhQSLnoKx5jr9IuVrcM8pi9STlN59O527DIDhtl7Xzaf2RF81uZA5cIYziiu43i/O7RqoGpPxRZY9SM4b6h7BhM0dBizc6JwSZSaYEe1FUsWVUkSWxZOTLRNZaMrQgygEAgNn7xn5pfX3/U7r64oJMX6RcVkrNxs9cVnrzIHFZFXqX1a3UjMJlwYbNIVRa6YfO65i/QpDZFLUnSmb/Vi6MHFZg3zAAACwsdDrtiKJLMFzE63tYLozI5tsms383jxOT8xoZb9vOl3aGVfckcEVp9f1PiMVqHy7sGqmc4LJQ7BQ2bL7B7k3G/BW6jVKZZqF3bf5kNEJYAwAA8FF/fnTPKJvVk/SyxUOfvjjvqi8uqeyKVmtxNiZsmPlS3BGM+esC92DRpRYxFaiNO4/1tP6IYLgI0QwAAMCUaLTKASm/YSDrXbv/A96+RNaauV8Rnj+QgwsHG2b+NPRnTVmqFVoYyWy/RrxdFl6wPLZ6MVpj3imr4WjHcInuI6o2AQAAmBZkyJCq+vulDe1DhZVdt7L5tik1G+fU0FYkCBpV9uJKwYYtFJrF+ZjRLugFsTKshs0zpdb8wR94BgMGAADgJ1FoJN2jTHZvckGLdxp3S2yVxeydzbWU1XMHVwQ2bMHB63uYyFqH2S22h0EzrJjKJd9lg9O5W8uFkaPKHkQtAAAAxkWtlQ/I+PyBnA8d1wtavB7VWSVUr5yZ0fAZ34G4QVwC2LAFimiMM9cWpiFogTix6TwsoXpFScc1uXoIwQoAAMDMMKKkCjCWCyNy+CdSa/4w0ThIXB/yO2DDFjqdkrI45lJMiyForim30WlQ1ogYBQAAYLag0xdreu++ajlDpS8yjZC+mFrzZ9vQG7QtbBigIB0so3YXZr0QNEf0qvVM+3Ahta0azAY6PWgHAACYiForH5Q1EQfFEd0rElzRpy+u+t4B7nXrWZx1CRsG/sOYUnSfux3TXwiaXWXydjeL8xGRAAAAzH1Gld2C4eJyYcQzvsM30xcTqlc29Geh0WDDwBT0S+vucTZhHgxBs6JE1lp2T7JaK5uZ/q5Rq7HgAwAAwFjI1cPdo0yO6F5Bi2c6959JC2Xp3K1CSTlaCTYMfJExpSivkYEJMQTN8JmVXFEaCiECYE7gNgdYyKi1ilFlt2iM2ziQWyQIYvckKzQSNAtsGPhmz5E/qrPCzBiCZkylnWGIPADAhgEAAGzYQmdMKcrm22JyDEEzI/5ANsIOAAAAAGDDwEetTlMkCML8GIJMVIm+Rfyqa6Syd6ymX1qn0kgRcwAAAAAAGwZoJ6Z+3uSCGTMEGVF3OX/x+jIRXgAAAAAAGwa+iEarxJoYBBlF9zibmd2xcrUYgWWegq0+AACEEQBgw2YUZncc5tAQ9BOV6NdUdsXI1cMIJgAAABsGjN2oGrQCbJg5v8VLOq5hMg1BP6C8RsagrBFBBAAAADCND9OiEWDDzJy6vkfxzOWYVUPQNFXQ4ikcwVGVAAAAAIANAz8Hr+8B5tYQ9E09rrcRDL+fk7cMkQ8DAAAAANiweenEMuOYSzHPhqAplcLeWNV1W6NVIlYAAAAAwERo1bKPH+fErVXYsBmlU1KaWvNHEnt9Ss3Ge5xNadwtGbU7HvD2PKo78KT+YFbD0Rz+idxGpxdNbgUtnq9bfd61+RcJgt4LAsl3Mmt3J7LWYrIOmZ+S2b9xRfdlqkGECAAAAACYmLmS3gIbNtMo1BK5elipGVVppWqtXKtT6fdHfvsNodVppKqBvjEef+BZZdetly0eD3n7yfwVk3gjiPlrVLFldPkSNMUMK5ZpUSS4MqrsRmQAAAAAwIICNmxee3mtXD1EjFmzOL+q+3ZBi2dWw9FM3u7Umv8lVK/CFB+ay4pjLsvh2wklqMMBAAAAANgwMP/R6jQqjXRU2dM1UlXX96i080ZeIyOjdmciaw2m/tAcUQ7/xICUj94KAAAAANgwMKPMcNU1rU4lVfX1jLL4A9llwvDnTa4PePsSWevgB6AZ1v3a7fyBHN1HHFQCAAAAANgwYO42bIoX8FErVfWLxjhNgy8qu6KLBEEFLd7ZDbZp3C3x1TjiDDK+Mmp3lgsjFRoJuv/cjAnAjN5LuM0BAACwYWC+zb3UWoVE0SmUlPP6HhYLgnMbHe/Xbk+oXgkXAf2wEqpXMLtj1VoZ+jtsGJiJ95JWrdXI504pMAAAALBhmHv9CBqdckTZ1TNaXd//5EPn9edNrhm1O++wVsNd/LAe8PaVdYZzRPfetfs/qT+UxF5vxv9sQYv3oKwRPR2AmUSrUajkgzqdGk0BAACwYcCMBnidakwlEo1xWode1/c/ZvUkEjuR3WB7j7M5nomExqmVzv2npONqff/TvjHepJ1RMvUg1Zji11zR/XLhzbwmBmnJOOay+f4vv2o90ykpQ38BYNacmKxHoxpDUwAAAGwYMHPUWtmwXCCUlHNF6UWCoGeNDmmcLQs8ofF+7fayzvCukarvSslTa+VD8nZizCq7buU3u6dzt86vDXsPefu7R5joEQDMLjqtCjYMAABgw8DCNGbyEUVX10hFrSjjQ8f13EbHdO62RNbaOOZS894KlcnbXS6MFI1xNDrlzzejRquUKDoFw++Z3XEFLd6ZtbsTWWvimJZz8H+/y/mrpvcutoEBMFeCsHJEp9OgHQAAADYMLHSIo5CpBofkbcSitA8VsnruvGvzf9pwJLXmz3mdhhdTseQ+Z2dRxxXyT0kUQq0p5z1anVqq6hfLW7pHmHV9j4oEV57UH06p2TjrBowjuidXD+NNDsDcQauRwYYBAABsGABfRKkZG5K3C4aLanpT3wsCsxqO3+P8PS8y8TJ5u0s7QzuHy4nDnNUGHOmX1jX0Z33ouJ7DP5Fa879YpsWMNUJJxzW5Woy3MQAAAAAAbBiY36i1Momis1PygSu6X9YZ/rbtwosmt0d1VsnsDbFVFrNrvdK4W162eDC7Y3tGWdo5WYuM2NpBWVPjYG6FMKqwPeB5kzOxi0bfpxfHXPa27Xz3KLaBzTIdgraebiHaAQAwT8BpCgDAhoF5iEItGZDxW8QF1T0Jb1p9H9fbpNb8OTMrP/drtxcJgrpGKufj3ietTjOi7OqQlNT0pr5q837E35tU81OnDuQ2OvWO1eANOReoYVW2NPPRDgCAeeLBYMMAgA0DZgG98tM29K6mN+Vt2/mnDUfucTYZsQpIJm93SUdIzyjLKPU25sowqNOOKfuFI1RZy3dt/o/qrKZ/fBlpXmKD587/olHjcCQAAAAAwIYBMNuotFKxvKVTUtY0+ILYjIquqJfNHg95+5PZv03/mK8iQVB9/+M+KU+7MDa4y9Xi3tGaFvFLQ1nLNO6WScuMj+oOkDaZO9vA8nIeXb7o3SfqwXt+bpl8zbBO1Yt2AAAAAGDDAKAmh3L1cL+UR5yGvvK71+N6mxT2xttVSz6lHXK3l3aGCiXlKLn+UX/ewKCsqVX8mvjYvEYG+ajSSOfUK3yV/yw/L0unQ37LXOpmWoWm4+xHaQWaAgAAAIANA2BqFGpJv7Shdeg1p/dez2j17FY7BMAsbJhSpx5EOwAAAACwYQAAAAAAAAAAGwbAjKPT6eRyuUaDk0wBAAAAAABsGAAzgkwmHR7CEcMAAAAAAAA2DJgpOj1z7SXhugAAAAAAANgwYM42DI0AAAAAAAAAbBgAAAAAAAAAwIYBAAAAAAAAAGwYAAAAM0Mul/eJetAOAAAAAGwYAACAGUKjVkulY2gHAAAAADYMAAAAAAAAAGDDAAAAgHmCTqdFIwAAAIANAwAAAGYOrVaFRgAAAAAbBgAAAAAAAAAANgyAn0MsHujpFqIdAAAAAAAAbBgAM8TrgryQK+fRDgAAAAAAADYMAAAAAAAAAGDDAAAAAAAAAADAhgEAAAAAAAAAbBgAAAAAAAAAwIYBAAAAAAAAAJgp/h8YSPos+EwMFAAAAABJRU5ErkJggg==" />
+
+ <p class="version">
+ <strong>Rails version:</strong> <%= Rails.version %><br />
+ <strong>Ruby version:</strong> <%= RUBY_VERSION %> (<%= RUBY_PLATFORM %>)
+ </p>
+ </section>
+ </div>
+</body>
</html>
diff --git a/railties/lib/rails/test_help.rb b/railties/lib/rails/test_help.rb
index 828039dc43..4e3ec184be 100644
--- a/railties/lib/rails/test_help.rb
+++ b/railties/lib/rails/test_help.rb
@@ -1,41 +1,49 @@
+# frozen_string_literal: true
+
# Make double-sure the RAILS_ENV is not set to production,
# so fixtures aren't loaded into that environment
abort("Abort testing: Your Rails environment is running in production mode!") if Rails.env.production?
-require "rails/test_unit/minitest_plugin"
-require 'active_support/test_case'
-require 'action_controller'
-require 'action_controller/test_case'
-require 'action_dispatch/testing/integration'
-require 'rails/generators/test_case'
+require "active_support/test_case"
+require "action_controller"
+require "action_controller/test_case"
+require "action_dispatch/testing/integration"
+require "rails/generators/test_case"
-require 'active_support/testing/autorun'
+require "active_support/testing/autorun"
if defined?(ActiveRecord::Base)
- ActiveRecord::Migration.maintain_test_schema!
+ begin
+ ActiveRecord::Migration.maintain_test_schema!
+ rescue ActiveRecord::PendingMigrationError => e
+ puts e.to_s.strip
+ exit 1
+ end
- class ActiveSupport::TestCase
+ ActiveSupport.on_load(:active_support_test_case) do
+ include ActiveRecord::TestDatabases
include ActiveRecord::TestFixtures
+
self.fixture_path = "#{Rails.root}/test/fixtures/"
- self.file_fixture_path = self.fixture_path + "files"
+ self.file_fixture_path = fixture_path + "files"
end
- ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path
-
- def create_fixtures(*fixture_set_names, &block)
- FixtureSet.create_fixtures(ActiveSupport::TestCase.fixture_path, fixture_set_names, {}, &block)
+ ActiveSupport.on_load(:action_dispatch_integration_test) do
+ self.fixture_path = ActiveSupport::TestCase.fixture_path
end
end
-class ActionController::TestCase
- def before_setup
+# :enddoc:
+
+ActiveSupport.on_load(:action_controller_test_case) do
+ def before_setup # :nodoc:
@routes = Rails.application.routes
super
end
end
-class ActionDispatch::IntegrationTest
- def before_setup
+ActiveSupport.on_load(:action_dispatch_integration_test) do
+ def before_setup # :nodoc:
@routes = Rails.application.routes
super
end
diff --git a/railties/lib/rails/test_unit/line_filtering.rb b/railties/lib/rails/test_unit/line_filtering.rb
new file mode 100644
index 0000000000..f8ca77fe4a
--- /dev/null
+++ b/railties/lib/rails/test_unit/line_filtering.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require "rails/test_unit/runner"
+
+module Rails
+ module LineFiltering # :nodoc:
+ def run(reporter, options = {})
+ options[:filter] = Rails::TestUnit::Runner.compose_filter(self, options[:filter])
+
+ super
+ end
+ end
+end
diff --git a/railties/lib/rails/test_unit/minitest_plugin.rb b/railties/lib/rails/test_unit/minitest_plugin.rb
deleted file mode 100644
index ab71298509..0000000000
--- a/railties/lib/rails/test_unit/minitest_plugin.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-require "active_support/core_ext/module/attribute_accessors"
-require "rails/test_unit/reporter"
-require "rails/test_unit/test_requirer"
-
-module Minitest
- def self.plugin_rails_options(opts, options)
- opts.separator ""
- opts.separator "Usage: bin/rails test [options] [files or directories]"
- opts.separator "You can run a single test by appending a line number to a filename:"
- opts.separator ""
- opts.separator " bin/rails test test/models/user_test.rb:27"
- opts.separator ""
- opts.separator "You can run multiple files and directories at the same time:"
- opts.separator ""
- opts.separator " bin/rails test test/controllers test/integration/login_test.rb"
- opts.separator ""
-
- opts.separator "Rails options:"
- opts.on("-e", "--environment [ENV]",
- "Run tests in the ENV environment") do |env|
- options[:environment] = env.strip
- end
-
- opts.on("-b", "--backtrace",
- "Show the complete backtrace") do
- options[:full_backtrace] = true
- end
-
- options[:patterns] = opts.order!
- end
-
- def self.plugin_rails_init(options)
- self.run_with_rails_extension = true
-
- ENV["RAILS_ENV"] = options[:environment] || "test"
-
- ::Rails::TestRequirer.require_files options[:patterns] unless run_with_autorun
-
- unless options[:full_backtrace] || ENV["BACKTRACE"]
- # Plugin can run without Rails loaded, check before filtering.
- Minitest.backtrace_filter = ::Rails.backtrace_cleaner if ::Rails.respond_to?(:backtrace_cleaner)
- end
-
- self.reporter << ::Rails::TestUnitReporter.new(options[:io], options)
- end
-
- mattr_accessor(:run_with_autorun) { false }
- mattr_accessor(:run_with_rails_extension) { false }
-end
-
-Minitest.extensions << 'rails'
diff --git a/railties/lib/rails/test_unit/railtie.rb b/railties/lib/rails/test_unit/railtie.rb
index 75180ff978..42b6daa3d1 100644
--- a/railties/lib/rails/test_unit/railtie.rb
+++ b/railties/lib/rails/test_unit/railtie.rb
@@ -1,5 +1,9 @@
+# frozen_string_literal: true
+
+require "rails/test_unit/line_filtering"
+
if defined?(Rake.application) && Rake.application.top_level_tasks.grep(/^(default$|test(:|$))/).any?
- ENV['RAILS_ENV'] ||= 'test'
+ ENV["RAILS_ENV"] ||= Rake.application.options.show_tasks ? "development" : "test"
end
module Rails
@@ -9,6 +13,13 @@ module Rails
fixture_replacement: nil
c.integration_tool :test_unit
+ c.system_tests :test_unit
+ end
+
+ initializer "test_unit.line_filtering" do
+ ActiveSupport.on_load(:active_support_test_case) {
+ ActiveSupport::TestCase.extend Rails::LineFiltering
+ }
end
rake_tasks do
diff --git a/railties/lib/rails/test_unit/reporter.rb b/railties/lib/rails/test_unit/reporter.rb
index 09b8675cf8..417933836b 100644
--- a/railties/lib/rails/test_unit/reporter.rb
+++ b/railties/lib/rails/test_unit/reporter.rb
@@ -1,13 +1,37 @@
+# frozen_string_literal: true
+
require "active_support/core_ext/class/attribute"
require "minitest"
module Rails
class TestUnitReporter < Minitest::StatisticsReporter
- class_attribute :executable
- self.executable = "bin/rails test"
+ class_attribute :executable, default: "rails test"
+
+ def record(result)
+ super
+
+ if options[:verbose]
+ io.puts color_output(format_line(result), by: result)
+ else
+ io.print color_output(result.result_code, by: result)
+ end
+
+ if output_inline? && result.failure && (!result.skipped? || options[:verbose])
+ io.puts
+ io.puts
+ io.puts color_output(result, by: result)
+ io.puts
+ io.puts format_rerun_snippet(result)
+ io.puts
+ end
+
+ if fail_fast? && result.failure && !result.skipped?
+ raise Interrupt
+ end
+ end
def report
- return if filtered_results.empty?
+ return if output_inline? || filtered_results.empty?
io.puts
io.puts "Failed tests:"
io.puts
@@ -15,10 +39,7 @@ module Rails
end
def aggregated_results # :nodoc:
- filtered_results.map do |result|
- location, line = result.method(result.name).source_location
- "#{self.executable} #{relative_path_for(location)}:#{line}"
- end.join "\n"
+ filtered_results.map { |result| format_rerun_snippet(result) }.join "\n"
end
def filtered_results
@@ -30,7 +51,60 @@ module Rails
end
def relative_path_for(file)
- file.sub(/^#{Rails.root}\/?/, '')
+ file.sub(/^#{app_root}\/?/, "")
end
+
+ private
+ def output_inline?
+ options[:output_inline]
+ end
+
+ def fail_fast?
+ options[:fail_fast]
+ end
+
+ def format_line(result)
+ klass = result.respond_to?(:klass) ? result.klass : result.class
+ "%s#%s = %.2f s = %s" % [klass, result.name, result.time, result.result_code]
+ end
+
+ def format_rerun_snippet(result)
+ location, line = if result.respond_to?(:source_location)
+ result.source_location
+ else
+ result.method(result.name).source_location
+ end
+
+ "#{executable} #{relative_path_for(location)}:#{line}"
+ end
+
+ def app_root
+ @app_root ||=
+ if defined?(ENGINE_ROOT)
+ ENGINE_ROOT
+ elsif Rails.respond_to?(:root)
+ Rails.root
+ end
+ end
+
+ def colored_output?
+ options[:color] && io.respond_to?(:tty?) && io.tty?
+ end
+
+ codes = { red: 31, green: 32, yellow: 33 }
+ COLOR_BY_RESULT_CODE = {
+ "." => codes[:green],
+ "E" => codes[:red],
+ "F" => codes[:red],
+ "S" => codes[:yellow]
+ }
+
+ def color_output(string, by:)
+ if colored_output?
+ "\e[#{COLOR_BY_RESULT_CODE[by.result_code]}m#{string}\e[0m"
+ else
+ string
+ end
+ end
end
end
diff --git a/railties/lib/rails/test_unit/runner.rb b/railties/lib/rails/test_unit/runner.rb
new file mode 100644
index 0000000000..d38952bb30
--- /dev/null
+++ b/railties/lib/rails/test_unit/runner.rb
@@ -0,0 +1,143 @@
+# frozen_string_literal: true
+
+require "shellwords"
+require "method_source"
+require "rake/file_list"
+require "active_support/core_ext/module/attribute_accessors"
+
+module Rails
+ module TestUnit
+ class Runner
+ mattr_reader :filters, default: []
+
+ class << self
+ def attach_before_load_options(opts)
+ opts.on("--warnings", "-w", "Run with Ruby warnings enabled") { }
+ opts.on("-e", "--environment ENV", "Run tests in the ENV environment") { }
+ end
+
+ def parse_options(argv)
+ # Perform manual parsing and cleanup since option parser raises on unknown options.
+ env_index = argv.index("--environment") || argv.index("-e")
+ if env_index
+ argv.delete_at(env_index)
+ environment = argv.delete_at(env_index).strip
+ end
+ ENV["RAILS_ENV"] = environment || "test"
+
+ w_index = argv.index("--warnings") || argv.index("-w")
+ $VERBOSE = argv.delete_at(w_index) if w_index
+ end
+
+ def rake_run(argv = [])
+ ARGV.replace Shellwords.split(ENV["TESTOPTS"] || "")
+
+ run(argv)
+ end
+
+ def run(argv = [])
+ load_tests(argv)
+
+ require "active_support/testing/autorun"
+ end
+
+ def load_tests(argv)
+ patterns = extract_filters(argv)
+
+ tests = Rake::FileList[patterns.any? ? patterns : "test/**/*_test.rb"]
+ tests.exclude("test/system/**/*") if patterns.empty?
+
+ tests.to_a.each { |path| require File.expand_path(path) }
+ end
+
+ def compose_filter(runnable, filter)
+ if filters.any? { |_, lines| lines.any? }
+ CompositeFilter.new(runnable, filter, filters)
+ else
+ filter
+ end
+ end
+
+ private
+ def extract_filters(argv)
+ # Extract absolute and relative paths but skip -n /.*/ regexp filters.
+ argv.select { |arg| arg =~ %r%^/?\w+/% && !arg.end_with?("/") }.map do |path|
+ case
+ when /(:\d+)+$/.match?(path)
+ file, *lines = path.split(":")
+ filters << [ file, lines ]
+ file
+ when Dir.exist?(path)
+ "#{path}/**/*_test.rb"
+ else
+ filters << [ path, [] ]
+ path
+ end
+ end
+ end
+ end
+ end
+
+ class CompositeFilter # :nodoc:
+ attr_reader :named_filter
+
+ def initialize(runnable, filter, patterns)
+ @runnable = runnable
+ @named_filter = derive_named_filter(filter)
+ @filters = [ @named_filter, *derive_line_filters(patterns) ].compact
+ end
+
+ # minitest uses === to find matching filters.
+ def ===(method)
+ @filters.any? { |filter| filter === method }
+ end
+
+ private
+ def derive_named_filter(filter)
+ if filter.respond_to?(:named_filter)
+ filter.named_filter
+ elsif filter =~ %r%/(.*)/% # Regexp filtering copied from minitest.
+ Regexp.new $1
+ elsif filter.is_a?(String)
+ filter
+ end
+ end
+
+ def derive_line_filters(patterns)
+ patterns.flat_map do |file, lines|
+ if lines.empty?
+ Filter.new(@runnable, file, nil) if file
+ else
+ lines.map { |line| Filter.new(@runnable, file, line) }
+ end
+ end
+ end
+ end
+
+ class Filter # :nodoc:
+ def initialize(runnable, file, line)
+ @runnable, @file = runnable, File.expand_path(file)
+ @line = line.to_i if line
+ end
+
+ def ===(method)
+ return unless @runnable.method_defined?(method)
+
+ if @line
+ test_file, test_range = definition_for(@runnable.instance_method(method))
+ test_file == @file && test_range.include?(@line)
+ else
+ @runnable.instance_method(method).source_location.first == @file
+ end
+ end
+
+ private
+ def definition_for(method)
+ file, start_line = method.source_location
+ end_line = method.source.count("\n") + start_line - 1
+
+ return file, start_line..end_line
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/test_unit/test_requirer.rb b/railties/lib/rails/test_unit/test_requirer.rb
deleted file mode 100644
index 84c2256729..0000000000
--- a/railties/lib/rails/test_unit/test_requirer.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-require 'active_support/core_ext/object/blank'
-require 'rake/file_list'
-
-module Rails
- class TestRequirer # :nodoc:
- class << self
- def require_files(patterns)
- patterns = expand_patterns(patterns)
-
- Rake::FileList[patterns.compact.presence || 'test/**/*_test.rb'].to_a.each do |file|
- require File.expand_path(file)
- end
- end
-
- private
- def expand_patterns(patterns)
- patterns.map do |arg|
- arg = arg.gsub(/:(\d+)?$/, '')
- if Dir.exist?(arg)
- "#{arg}/**/*_test.rb"
- elsif File.file?(arg)
- arg
- end
- end
- end
- end
- end
-end
diff --git a/railties/lib/rails/test_unit/testing.rake b/railties/lib/rails/test_unit/testing.rake
index dda492f974..32ac27a135 100644
--- a/railties/lib/rails/test_unit/testing.rake
+++ b/railties/lib/rails/test_unit/testing.rake
@@ -1,13 +1,20 @@
-gem 'minitest'
-require 'minitest'
-require 'rails/test_unit/minitest_plugin'
+# frozen_string_literal: true
+
+gem "minitest"
+require "minitest"
+require "rails/test_unit/runner"
task default: :test
-desc "Runs all tests in test folder"
+desc "Runs all tests in test folder except system ones"
task :test do
$: << "test"
- Minitest.run(['test'])
+
+ if ENV.key?("TEST")
+ Rails::TestUnit::Runner.rake_run([ENV["TEST"]])
+ else
+ Rails::TestUnit::Runner.rake_run
+ end
end
namespace :test do
@@ -16,30 +23,36 @@ namespace :test do
# If used with Active Record, this task runs before the database schema is synchronized.
end
- task :run => %w[test]
+ task run: %w[test]
desc "Run tests quickly, but also reset db"
- task :db => %w[db:test:prepare test]
+ task db: %w[db:test:prepare test]
["models", "helpers", "controllers", "mailers", "integration", "jobs"].each do |name|
task name => "test:prepare" do
$: << "test"
- Minitest.run(["test/#{name}"])
+ Rails::TestUnit::Runner.rake_run(["test/#{name}"])
end
end
- task :generators => "test:prepare" do
+ task generators: "test:prepare" do
+ $: << "test"
+ Rails::TestUnit::Runner.rake_run(["test/lib/generators"])
+ end
+
+ task units: "test:prepare" do
$: << "test"
- Minitest.run(["test/lib/generators"])
+ Rails::TestUnit::Runner.rake_run(["test/models", "test/helpers", "test/unit"])
end
- task :units => "test:prepare" do
+ task functionals: "test:prepare" do
$: << "test"
- Minitest.run(["test/models", "test/helpers", "test/unit"])
+ Rails::TestUnit::Runner.rake_run(["test/controllers", "test/mailers", "test/functional"])
end
- task :functionals => "test:prepare" do
+ desc "Run system tests only"
+ task system: "test:prepare" do
$: << "test"
- Minitest.run(["test/controllers", "test/mailers", "test/functional"])
+ Rails::TestUnit::Runner.rake_run(["test/system"])
end
end
diff --git a/railties/lib/rails/version.rb b/railties/lib/rails/version.rb
index df351c4238..ba6763a572 100644
--- a/railties/lib/rails/version.rb
+++ b/railties/lib/rails/version.rb
@@ -1,4 +1,6 @@
-require_relative 'gem_version'
+# frozen_string_literal: true
+
+require_relative "gem_version"
module Rails
# Returns the version of the currently loaded Rails as a string.
diff --git a/railties/lib/rails/welcome_controller.rb b/railties/lib/rails/welcome_controller.rb
index de9cd18b01..5b84b57679 100644
--- a/railties/lib/rails/welcome_controller.rb
+++ b/railties/lib/rails/welcome_controller.rb
@@ -1,4 +1,6 @@
-require 'rails/application_controller'
+# frozen_string_literal: true
+
+require "rails/application_controller"
class Rails::WelcomeController < Rails::ApplicationController # :nodoc:
layout false
diff --git a/railties/railties.gemspec b/railties/railties.gemspec
index afe1603448..4e4a504c97 100644
--- a/railties/railties.gemspec
+++ b/railties/railties.gemspec
@@ -1,34 +1,44 @@
-version = File.read(File.expand_path('../../RAILS_VERSION', __FILE__)).strip
+# frozen_string_literal: true
+
+version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip
Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
- s.name = 'railties'
+ s.name = "railties"
s.version = version
- s.summary = 'Tools for creating, working with, and running Rails applications.'
- s.description = 'Rails internals: application bootup, plugins, generators, and rake tasks.'
+ s.summary = "Tools for creating, working with, and running Rails applications."
+ s.description = "Rails internals: application bootup, plugins, generators, and rake tasks."
+
+ s.required_ruby_version = ">= 2.4.1"
+
+ s.license = "MIT"
- s.required_ruby_version = '>= 2.2.2'
+ s.author = "David Heinemeier Hansson"
+ s.email = "david@loudthinking.com"
+ s.homepage = "http://rubyonrails.org"
- s.license = 'MIT'
+ s.files = Dir["CHANGELOG.md", "README.rdoc", "MIT-LICENSE", "RDOC_MAIN.rdoc", "exe/**/*", "lib/**/{*,.[a-z]*}"]
+ s.require_path = "lib"
- s.author = 'David Heinemeier Hansson'
- s.email = 'david@loudthinking.com'
- s.homepage = 'http://www.rubyonrails.org'
+ s.bindir = "exe"
+ s.executables = ["rails"]
- s.files = Dir['CHANGELOG.md', 'README.rdoc', 'RDOC_MAIN.rdoc', 'exe/**/*', 'lib/**/{*,.[a-z]*}']
- s.require_path = 'lib'
+ s.rdoc_options << "--exclude" << "."
- s.bindir = 'exe'
- s.executables = ['rails']
+ s.metadata = {
+ "source_code_uri" => "https://github.com/rails/rails/tree/v#{version}/railties",
+ "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/railties/CHANGELOG.md"
+ }
- s.rdoc_options << '--exclude' << '.'
+ # NOTE: Please read our dependency guidelines before updating versions:
+ # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves
- s.add_dependency 'activesupport', version
- s.add_dependency 'actionpack', version
+ s.add_dependency "activesupport", version
+ s.add_dependency "actionpack", version
- s.add_dependency 'rake', '>= 0.8.7'
- s.add_dependency 'thor', '>= 0.18.1', '< 2.0'
- s.add_dependency 'method_source'
+ s.add_dependency "rake", ">= 0.8.7"
+ s.add_dependency "thor", ">= 0.19.0", "< 2.0"
+ s.add_dependency "method_source"
- s.add_development_dependency 'actionview', version
+ s.add_development_dependency "actionview", version
end
diff --git a/railties/test/abstract_unit.rb b/railties/test/abstract_unit.rb
index 794d180e5d..9c866263f0 100644
--- a/railties/test/abstract_unit.rb
+++ b/railties/test/abstract_unit.rb
@@ -1,33 +1,34 @@
-ENV["RAILS_ENV"] ||= "test"
+# frozen_string_literal: true
-require File.expand_path("../../../load_paths", __FILE__)
+ENV["RAILS_ENV"] ||= "test"
-require 'stringio'
-require 'active_support/testing/autorun'
-require 'active_support/testing/stream'
-require 'fileutils'
+require "stringio"
+require "active_support/testing/autorun"
+require "active_support/testing/stream"
+require "fileutils"
-require 'active_support'
-require 'action_controller'
-require 'action_view'
-require 'rails/all'
+require "active_support"
+require "action_controller"
+require "action_view"
+require "rails/all"
module TestApp
class Application < Rails::Application
- config.root = File.dirname(__FILE__)
- secrets.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33'
+ config.root = __dir__
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
+
+ private
+ # 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
end
diff --git a/railties/test/app_loader_test.rb b/railties/test/app_loader_test.rb
index 5946c8fd4c..0deb1a76df 100644
--- a/railties/test/app_loader_test.rb
+++ b/railties/test/app_loader_test.rb
@@ -1,23 +1,25 @@
-require 'tmpdir'
-require 'abstract_unit'
-require 'rails/app_loader'
+# frozen_string_literal: true
+
+require "tmpdir"
+require "abstract_unit"
+require "rails/app_loader"
class AppLoaderTest < ActiveSupport::TestCase
def loader
@loader ||= Class.new do
extend Rails::AppLoader
- def self.exec_arguments
- @exec_arguments
- end
+ class << self
+ attr_accessor :exec_arguments
- def self.exec(*args)
- @exec_arguments = args
+ def exec(*args)
+ self.exec_arguments = args
+ end
end
end
end
- def write(filename, contents=nil)
+ def write(filename, contents = nil)
FileUtils.mkdir_p(File.dirname(filename))
File.write(filename, contents)
end
@@ -27,27 +29,27 @@ class AppLoaderTest < ActiveSupport::TestCase
end
setup do
- @tmp = Dir.mktmpdir('railties-rails-loader-test-suite')
+ @tmp = Dir.mktmpdir("railties-rails-loader-test-suite")
@cwd = Dir.pwd
Dir.chdir(@tmp)
end
- ['bin', 'script'].each do |script_dir|
+ ["bin", "script"].each do |script_dir|
exe = "#{script_dir}/rails"
test "is not in a Rails application if #{exe} is not found in the current or parent directories" do
def loader.find_executables; end
- assert !loader.exec_app
+ assert_not loader.exec_app
end
test "is not in a Rails application if #{exe} exists but is a folder" do
FileUtils.mkdir_p(exe)
- assert !loader.exec_app
+ assert_not loader.exec_app
end
- ['APP_PATH', 'ENGINE_PATH'].each do |keyword|
+ ["APP_PATH", "ENGINE_PATH"].each do |keyword|
test "is in a Rails application if #{exe} exists and contains #{keyword}" do
write exe, keyword
@@ -59,14 +61,14 @@ class AppLoaderTest < ActiveSupport::TestCase
test "is not in a Rails application if #{exe} exists but doesn't contain #{keyword}" do
write exe
- assert !loader.exec_app
+ assert_not loader.exec_app
end
test "is in a Rails application if parent directory has #{exe} containing #{keyword} and chdirs to the root directory" do
write "foo/bar/#{exe}"
write "foo/#{exe}", keyword
- Dir.chdir('foo/bar')
+ Dir.chdir("foo/bar")
loader.exec_app
@@ -74,7 +76,7 @@ class AppLoaderTest < ActiveSupport::TestCase
# Compare the realpath in case either of them has symlinks.
#
- # This happens in particular in Mac OS X, where @tmp starts
+ # This happens in particular in macOS, where @tmp starts
# with "/var", and Dir.pwd with "/private/var", due to a
# default system symlink var -> private/var.
assert_equal File.realpath("#@tmp/foo"), File.realpath(Dir.pwd)
diff --git a/railties/test/application/asset_debugging_test.rb b/railties/test/application/asset_debugging_test.rb
index 36ab8109a7..3e0f31860b 100644
--- a/railties/test/application/asset_debugging_test.rb
+++ b/railties/test/application/asset_debugging_test.rb
@@ -1,5 +1,7 @@
-require 'isolation/abstract_unit'
-require 'rack/test'
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "rack/test"
module ApplicationTests
class AssetDebuggingTest < ActiveSupport::TestCase
@@ -25,8 +27,6 @@ module ApplicationTests
RUBY
ENV["RAILS_ENV"] = "production"
-
- boot_rails
end
def teardown
@@ -36,14 +36,15 @@ module ApplicationTests
test "assets are concatenated when debug is off and compile is off either if debug_assets param is provided" do
# config.assets.debug and config.assets.compile are false for production environment
ENV["RAILS_ENV"] = "production"
- output = Dir.chdir(app_path){ `bin/rake assets:precompile --trace 2>&1` }
- assert $?.success?, output
- require "#{app_path}/config/environment"
+ rails "assets:precompile", "--trace"
+
+ # Load app env
+ app "production"
class ::PostsController < ActionController::Base ; end
# the debug_assets params isn't used if compile is off
- get '/posts?debug_assets=true'
+ get "/posts?debug_assets=true"
assert_match(/<script src="\/assets\/application-([0-z]+)\.js"><\/script>/, last_response.body)
assert_no_match(/<script src="\/assets\/xmlhr-([0-z]+)\.js"><\/script>/, last_response.body)
end
@@ -51,14 +52,112 @@ module ApplicationTests
test "assets aren't concatenated when compile is true is on and debug_assets params is true" do
add_to_env_config "production", "config.assets.compile = true"
- ENV["RAILS_ENV"] = "production"
- require "#{app_path}/config/environment"
+ # Load app env
+ app "production"
class ::PostsController < ActionController::Base ; end
- get '/posts?debug_assets=true'
+ get "/posts?debug_assets=true"
assert_match(/<script src="\/assets\/application(\.self)?-([0-z]+)\.js\?body=1"><\/script>/, last_response.body)
assert_match(/<script src="\/assets\/xmlhr(\.self)?-([0-z]+)\.js\?body=1"><\/script>/, last_response.body)
end
+
+ test "public path and tag methods are not over-written by the asset pipeline" do
+ contents = "doesnotexist"
+ cases = {
+ asset_path: %r{/#{contents}},
+ image_path: %r{/images/#{contents}},
+ video_path: %r{/videos/#{contents}},
+ audio_path: %r{/audios/#{contents}},
+ font_path: %r{/fonts/#{contents}},
+ javascript_path: %r{/javascripts/#{contents}},
+ stylesheet_path: %r{/stylesheets/#{contents}},
+ image_tag: %r{<img src="/images/#{contents}"},
+ favicon_link_tag: %r{<link rel="shortcut icon" type="image/x-icon" href="/images/#{contents}" />},
+ stylesheet_link_tag: %r{<link rel="stylesheet" media="screen" href="/stylesheets/#{contents}.css" />},
+ javascript_include_tag: %r{<script src="/javascripts/#{contents}.js">},
+ audio_tag: %r{<audio src="/audios/#{contents}"></audio>},
+ video_tag: %r{<video src="/videos/#{contents}"></video>},
+ image_submit_tag: %r{<input type="image" src="/images/#{contents}" />}
+ }
+
+ cases.each do |(view_method, tag_match)|
+ app_file "app/views/posts/index.html.erb", "<%= #{view_method} '#{contents}', skip_pipeline: true %>"
+
+ app "development"
+
+ class ::PostsController < ActionController::Base ; end
+
+ get "/posts?debug_assets=true"
+
+ body = last_response.body
+ assert_match(tag_match, body, "Expected `#{view_method}` to produce a match to #{tag_match}, but did not: #{body}")
+ end
+ end
+
+ test "public url methods are not over-written by the asset pipeline" do
+ contents = "doesnotexist"
+ cases = {
+ asset_url: %r{http://example.org/#{contents}},
+ image_url: %r{http://example.org/images/#{contents}},
+ video_url: %r{http://example.org/videos/#{contents}},
+ audio_url: %r{http://example.org/audios/#{contents}},
+ font_url: %r{http://example.org/fonts/#{contents}},
+ javascript_url: %r{http://example.org/javascripts/#{contents}},
+ stylesheet_url: %r{http://example.org/stylesheets/#{contents}},
+ }
+
+ cases.each do |(view_method, tag_match)|
+ app_file "app/views/posts/index.html.erb", "<%= #{view_method} '#{contents}', skip_pipeline: true %>"
+
+ app "development"
+
+ class ::PostsController < ActionController::Base ; end
+
+ get "/posts?debug_assets=true"
+
+ body = last_response.body
+ assert_match(tag_match, body, "Expected `#{view_method}` to produce a match to #{tag_match}, but did not: #{body}")
+ end
+ end
+
+ test "{ skip_pipeline: true } does not use the asset pipeline" do
+ cases = {
+ /\/assets\/application-.*.\.js/ => {},
+ /application.js/ => { skip_pipeline: true },
+ }
+ cases.each do |(tag_match, options_hash)|
+ app_file "app/views/posts/index.html.erb", "<%= asset_path('application.js', #{options_hash}) %>"
+
+ app "development"
+
+ class ::PostsController < ActionController::Base ; end
+
+ get "/posts?debug_assets=true"
+
+ body = last_response.body.strip
+ assert_match(tag_match, body, "Expected `asset_path` with `#{options_hash}` to produce a match to #{tag_match}, but did not: #{body}")
+ end
+ end
+
+ test "public_compute_asset_path does not use the asset pipeline" do
+ cases = {
+ compute_asset_path: /\/assets\/application-.*.\.js/,
+ public_compute_asset_path: /application.js/,
+ }
+
+ cases.each do |(view_method, tag_match)|
+ app_file "app/views/posts/index.html.erb", "<%= #{ view_method } 'application.js' %>"
+
+ app "development"
+
+ class ::PostsController < ActionController::Base ; end
+
+ get "/posts?debug_assets=true"
+
+ body = last_response.body.strip
+ assert_match(tag_match, body, "Expected `#{view_method}` to produce a match to #{ tag_match }, but did not: #{ body }")
+ end
+ end
end
end
diff --git a/railties/test/application/assets_test.rb b/railties/test/application/assets_test.rb
index 6952472ff5..46ee0d670e 100644
--- a/railties/test/application/assets_test.rb
+++ b/railties/test/application/assets_test.rb
@@ -1,6 +1,8 @@
-require 'isolation/abstract_unit'
-require 'rack/test'
-require 'active_support/json'
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "rack/test"
+require "active_support/json"
module ApplicationTests
class AssetsTest < ActiveSupport::TestCase
@@ -9,7 +11,6 @@ module ApplicationTests
def setup
build_app(initializers: true)
- boot_rails
end
def teardown
@@ -17,33 +18,43 @@ module ApplicationTests
end
def precompile!(env = nil)
- quietly do
- precompile_task = "bin/rake assets:precompile #{env} --trace 2>&1"
- output = Dir.chdir(app_path) { %x[ #{precompile_task} ] }
- assert $?.success?, output
- output
+ with_env env.to_h do
+ quietly do
+ precompile_task = "bin/rails assets:precompile --trace 2>&1"
+ output = Dir.chdir(app_path) { %x[ #{precompile_task} ] }
+ assert $?.success?, output
+ output
+ end
end
end
+ def with_env(env)
+ env.each { |k, v| ENV[k.to_s] = v }
+ yield
+ ensure
+ env.each_key { |k| ENV.delete k.to_s }
+ end
+
def clean_assets!
quietly do
- assert Dir.chdir(app_path) { system('bin/rake assets:clobber') }
+ assert Dir.chdir(app_path) { system("bin/rails assets:clobber") }
end
end
def assert_file_exists(filename)
- assert Dir[filename].first, "missing #{filename}"
+ globbed = Dir[filename]
+ assert globbed.one?, "Found #{globbed.size} files matching #{filename}. All files in the directory: #{Dir.entries(File.dirname(filename)).inspect}"
end
def assert_no_file_exists(filename)
- assert !File.exist?(filename), "#{filename} does exist"
+ assert_not File.exist?(filename), "#{filename} does exist"
end
test "assets routes have higher priority" do
app_file "app/assets/images/rails.png", "notactuallyapng"
app_file "app/assets/javascripts/demo.js.erb", "a = <%= image_path('rails.png').inspect %>;"
- app_file 'config/routes.rb', <<-RUBY
+ app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get '*path', to: lambda { |env| [200, { "Content-Type" => "text/html" }, ["Not an asset"]] }
end
@@ -57,24 +68,11 @@ module ApplicationTests
assert_equal 'a = "/assets/rails.png";', last_response.body.strip
end
- test "assets do not require compressors until it is used" do
- app_file "app/assets/javascripts/demo.js.erb", "<%= :alert %>();"
- add_to_env_config "production", "config.assets.compile = true"
-
- ENV["RAILS_ENV"] = "production"
- require "#{app_path}/config/environment"
-
- assert !defined?(Uglifier)
- get "/assets/demo.js"
- assert_match "alert()", last_response.body
- assert defined?(Uglifier)
- end
-
test "precompile creates the file, gives it the original asset's content and run in production as default" do
+ app_file "app/assets/config/manifest.js", "//= link_tree ../javascripts"
app_file "app/assets/javascripts/application.js", "alert();"
app_file "app/assets/javascripts/foo/application.js", "alert();"
- ENV["RAILS_ENV"] = nil
precompile!
files = Dir["#{app_path}/public/assets/application-*.js"]
@@ -86,6 +84,7 @@ module ApplicationTests
end
def test_precompile_does_not_hit_the_database
+ app_file "app/assets/config/manifest.js", "//= link_tree ../javascripts"
app_file "app/assets/javascripts/application.js", "alert();"
app_file "app/assets/javascripts/foo/application.js", "alert();"
app_file "app/controllers/users_controller.rb", <<-eoruby
@@ -95,10 +94,9 @@ module ApplicationTests
class User < ActiveRecord::Base; raise 'should not be reached'; end
eoruby
- ENV['RAILS_ENV'] = 'production'
- ENV['DATABASE_URL'] = 'postgresql://baduser:badpass@127.0.0.1/dbname'
-
- precompile!
+ precompile! \
+ RAILS_ENV: "production",
+ DATABASE_URL: "postgresql://baduser:badpass@127.0.0.1/dbname"
files = Dir["#{app_path}/public/assets/application-*.js"]
files << Dir["#{app_path}/public/assets/foo/application-*.js"].first
@@ -106,9 +104,6 @@ module ApplicationTests
assert_not_nil file, "Expected application.js asset to be generated, but none found"
assert_equal "alert();".strip, File.read(file).strip
end
- ensure
- ENV.delete 'RAILS_ENV'
- ENV.delete 'DATABASE_URL'
end
test "precompile application.js and application.css and all other non JS/CSS files" do
@@ -163,40 +158,64 @@ module ApplicationTests
precompile!
+ assert_file_exists("#{app_path}/public/assets/something/index-*.js")
+ end
+
+ test "precompile use assets defined in app env config" do
+ add_to_env_config "production", 'config.assets.precompile = [ "something.js" ]'
+ app_file "app/assets/javascripts/something.js.erb", "alert();"
+
+ precompile! RAILS_ENV: "production"
+
assert_file_exists("#{app_path}/public/assets/something-*.js")
end
- test 'precompile use assets defined in app env config' do
- add_to_env_config 'production', 'config.assets.precompile = [ "something.js" ]'
+ test "sprockets cache is not shared between environments" do
+ app_file "app/assets/images/rails.png", "notactuallyapng"
+ app_file "app/assets/stylesheets/application.css.erb", "body { background: '<%= asset_path('rails.png') %>'; }"
+ add_to_env_config "production", 'config.assets.prefix = "production_assets"'
- app_file 'app/assets/javascripts/something.js.erb', 'alert();'
+ precompile!
- precompile! 'RAILS_ENV=production'
+ assert_file_exists("#{app_path}/public/assets/application-*.css")
- assert_file_exists("#{app_path}/public/assets/something-*.js")
+ file = Dir["#{app_path}/public/assets/application-*.css"].first
+ assert_match(/assets\/rails-([0-z]+)\.png/, File.read(file))
+
+ precompile! RAILS_ENV: "production"
+
+ assert_file_exists("#{app_path}/public/production_assets/application-*.css")
+
+ file = Dir["#{app_path}/public/production_assets/application-*.css"].first
+ assert_match(/production_assets\/rails-([0-z]+)\.png/, File.read(file))
end
- test 'precompile use assets defined in app config and reassigned in app env config' do
- add_to_config 'config.assets.precompile = [ "something.js" ]'
- add_to_env_config 'production', 'config.assets.precompile += [ "another.js" ]'
+ test "precompile use assets defined in app config and reassigned in app env config" do
+ add_to_config 'config.assets.precompile = [ "something_manifest.js" ]'
+ add_to_env_config "production", 'config.assets.precompile += [ "another_manifest.js" ]'
+
+ app_file "app/assets/config/something_manifest.js", "//= link something.js"
+ app_file "app/assets/config/another_manifest.js", "//= link another.js"
- app_file 'app/assets/javascripts/something.js.erb', 'alert();'
- app_file 'app/assets/javascripts/another.js.erb', 'alert();'
+ app_file "app/assets/javascripts/something.js.erb", "alert();"
+ app_file "app/assets/javascripts/another.js.erb", "alert();"
- precompile! 'RAILS_ENV=production'
+ precompile! RAILS_ENV: "production"
+ assert_file_exists("#{app_path}/public/assets/something_manifest-*.js")
assert_file_exists("#{app_path}/public/assets/something-*.js")
+ assert_file_exists("#{app_path}/public/assets/another_manifest-*.js")
assert_file_exists("#{app_path}/public/assets/another-*.js")
end
- test "asset pipeline should use a Sprockets::Index when config.assets.digest is true" do
+ test "asset pipeline should use a Sprockets::CachedEnvironment when config.assets.digest is true" do
add_to_config "config.action_controller.perform_caching = false"
add_to_env_config "production", "config.assets.compile = true"
- ENV["RAILS_ENV"] = "production"
- require "#{app_path}/config/environment"
+ # Load app env
+ app "production"
- assert_equal Sprockets::Index, Rails.application.assets.class
+ assert_equal Sprockets::CachedEnvironment, Rails.application.assets.class
end
test "precompile creates a manifest file with all the assets listed" do
@@ -205,8 +224,8 @@ module ApplicationTests
app_file "app/assets/javascripts/application.js", "alert();"
precompile!
- manifest = Dir["#{app_path}/public/assets/.sprockets-manifest-*.json"].first
+ manifest = Dir["#{app_path}/public/assets/.sprockets-manifest-*.json"].first
assets = ActiveSupport::JSON.decode(File.read(manifest))
assert_match(/application-([0-z]+)\.js/, assets["assets"]["application.js"])
assert_match(/application-([0-z]+)\.css/, assets["assets"]["application.css"])
@@ -225,30 +244,29 @@ module ApplicationTests
test "assets do not require any assets group gem when manifest file is present" do
app_file "app/assets/javascripts/application.js", "alert();"
- add_to_env_config "production", "config.serve_static_files = true"
+ add_to_env_config "production", "config.public_file_server.enabled = true"
- ENV["RAILS_ENV"] = "production"
- precompile!
+ precompile! RAILS_ENV: "production"
manifest = Dir["#{app_path}/public/assets/.sprockets-manifest-*.json"].first
assets = ActiveSupport::JSON.decode(File.read(manifest))
asset_path = assets["assets"]["application.js"]
- require "#{app_path}/config/environment"
+ # Load app env
+ app "production"
# Checking if Uglifier is defined we can know if Sprockets was reached or not
- assert !defined?(Uglifier)
+ assert_not defined?(Uglifier)
get "/assets/#{asset_path}"
assert_match "alert()", last_response.body
- assert !defined?(Uglifier)
+ assert_not defined?(Uglifier)
end
- test "precompile properly refers files referenced with asset_path and runs in the provided RAILS_ENV" do
+ test "precompile properly refers files referenced with asset_path" do
app_file "app/assets/images/rails.png", "notactuallyapng"
- app_file "app/assets/stylesheets/application.css.erb", "<%= asset_path('rails.png') %>"
- add_to_env_config "test", "config.assets.digest = true"
+ app_file "app/assets/stylesheets/application.css.erb", "p { background-image: url(<%= asset_path('rails.png') %>) }"
- precompile!('RAILS_ENV=test')
+ precompile!
file = Dir["#{app_path}/public/assets/application-*.css"].first
assert_match(/\/assets\/rails-([0-z]+)\.png/, File.read(file))
@@ -257,10 +275,9 @@ module ApplicationTests
test "precompile shouldn't use the digests present in manifest.json" do
app_file "app/assets/images/rails.png", "notactuallyapng"
- app_file "app/assets/stylesheets/application.css.erb", "p { url: <%= asset_path('rails.png') %> }"
+ app_file "app/assets/stylesheets/application.css.erb", "p { background-image: url(<%= asset_path('rails.png') %>) }"
- ENV["RAILS_ENV"] = "production"
- precompile!
+ precompile! RAILS_ENV: "production"
manifest = Dir["#{app_path}/public/assets/.sprockets-manifest-*.json"].first
assets = ActiveSupport::JSON.decode(File.read(manifest))
@@ -269,17 +286,16 @@ module ApplicationTests
app_file "app/assets/images/rails.png", "p { url: change }"
precompile!
- assets = ActiveSupport::JSON.decode(File.read(manifest))
+ assets = ActiveSupport::JSON.decode(File.read(manifest))
assert_not_equal asset_path, assets["assets"]["application.css"]
end
- test "precompile appends the md5 hash to files referenced with asset_path and run in production with digest true" do
+ test "precompile appends the MD5 hash to files referenced with asset_path and run in production with digest true" do
app_file "app/assets/images/rails.png", "notactuallyapng"
- app_file "app/assets/stylesheets/application.css.erb", "<%= asset_path('rails.png') %>"
+ app_file "app/assets/stylesheets/application.css.erb", "p { background-image: url(<%= asset_path('rails.png') %>) }"
- ENV["RAILS_ENV"] = "production"
- precompile!
+ precompile! RAILS_ENV: "production"
file = Dir["#{app_path}/public/assets/application-*.css"].first
assert_match(/\/assets\/rails-([0-z]+)\.png/, File.read(file))
@@ -288,7 +304,8 @@ module ApplicationTests
test "precompile should handle utf8 filenames" do
filename = "レイルズ.png"
app_file "app/assets/images/#{filename}", "not an image really"
- add_to_config "config.assets.precompile = [ /\.png$/, /application.(css|js)$/ ]"
+ app_file "app/assets/config/manifest.js", "//= link_tree ../images"
+ add_to_config "config.assets.precompile = %w(manifest.js)"
precompile!
@@ -296,7 +313,8 @@ module ApplicationTests
assets = ActiveSupport::JSON.decode(File.read(manifest))
assert asset_path = assets["assets"].find { |(k, _)| k && k =~ /.png/ }[1]
- require "#{app_path}/config/environment"
+ # Load app env
+ app "development"
get "/assets/#{URI.parser.escape(asset_path)}"
assert_match "not an image really", last_response.body
@@ -310,8 +328,7 @@ module ApplicationTests
clean_assets!
- files = Dir["#{app_path}/public/assets/**/*", "#{app_path}/tmp/cache/assets/development/*",
- "#{app_path}/tmp/cache/assets/test/*", "#{app_path}/tmp/cache/assets/production/*"]
+ files = Dir["#{app_path}/public/assets/**/*"]
assert_equal 0, files.length, "Expected no assets, but found #{files.join(', ')}"
end
@@ -319,8 +336,8 @@ module ApplicationTests
app_file "app/assets/javascripts/demo.js.erb", "<%= :alert %>();"
add_to_config "config.assets.compile = false"
- ENV["RAILS_ENV"] = "production"
- require "#{app_path}/config/environment"
+ # Load app env
+ app "production"
get "/assets/demo.js"
assert_equal 404, last_response.status
@@ -337,21 +354,22 @@ module ApplicationTests
add_to_env_config "development", "config.assets.digest = false"
- require "#{app_path}/config/environment"
+ # Load app env
+ app "development"
class ::OmgController < ActionController::Base
def index
flash[:cool_story] = true
- render text: "ok"
+ render plain: "ok"
end
end
get "/omg"
- assert_equal 'ok', last_response.body
+ assert_equal "ok", last_response.body
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
@@ -363,7 +381,8 @@ module ApplicationTests
add_to_env_config "development", "config.assets.digest = false"
- require "#{app_path}/config/environment"
+ # Load app env
+ app "development"
get "/assets/demo.js"
assert_match "alert();", last_response.body
@@ -374,15 +393,15 @@ module ApplicationTests
app_with_assets_in_view
# config.assets.debug and config.assets.compile are false for production environment
- ENV["RAILS_ENV"] = "production"
- precompile!
+ precompile! RAILS_ENV: "production"
- require "#{app_path}/config/environment"
+ # Load app env
+ app "production"
class ::PostsController < ActionController::Base ; end
# the debug_assets params isn't used if compile is off
- get '/posts?debug_assets=true'
+ get "/posts?debug_assets=true"
assert_match(/<script src="\/assets\/application-([0-z]+)\.js"><\/script>/, last_response.body)
assert_no_match(/<script src="\/assets\/xmlhr-([0-z]+)\.js"><\/script>/, last_response.body)
end
@@ -393,7 +412,8 @@ module ApplicationTests
app_file "app/assets/javascripts/xmlhr.js.erb", "<%= Post.name %>"
precompile!
- assert_equal "Post;\n", File.read(Dir["#{app_path}/public/assets/application-*.js"].first)
+
+ assert_match(/Post;/, File.read(Dir["#{app_path}/public/assets/application-*.js"].first))
end
test "initialization on the assets group should set assets_dir" do
@@ -409,13 +429,13 @@ module ApplicationTests
end
test "digested assets are not mistakenly removed" do
- app_file "app/assets/application.js", "alert();"
+ app_file "app/assets/application.css", "div { font-weight: bold }"
add_to_config "config.assets.compile = true"
precompile!
- files = Dir["#{app_path}/public/assets/application-*.js"]
- assert_equal 1, files.length, "Expected digested application.js asset to be generated, but none found"
+ files = Dir["#{app_path}/public/assets/application-*.css"]
+ assert_equal 1, files.length, "Expected digested application.css asset to be generated, but none found"
end
test "digested assets are removed from configured path" do
@@ -434,12 +454,15 @@ module ApplicationTests
app_with_assets_in_view
add_to_config "config.asset_host = 'example.com'"
add_to_env_config "development", "config.assets.digest = false"
- require "#{app_path}/config/environment"
+
+ # Load app env
+ app "development"
+
class ::PostsController < ActionController::Base; end
- get '/posts', {}, {'HTTPS'=>'off'}
+ get "/posts", {}, { "HTTPS" => "off" }
assert_match('src="http://example.com/assets/application.self.js', last_response.body)
- get '/posts', {}, {'HTTPS'=>'on'}
+ get "/posts", {}, { "HTTPS" => "on" }
assert_match('src="https://example.com/assets/application.self.js', last_response.body)
end
@@ -449,6 +472,7 @@ module ApplicationTests
add_to_config "config.assets.precompile = %w{rails.png image_loader.js}"
add_to_config "config.asset_host = 'example.com'"
add_to_env_config "development", "config.assets.digest = false"
+
precompile!
assert_match "src='//example.com/assets/rails.png'", File.read(Dir["#{app_path}/public/assets/image_loader-*.js"].first)
@@ -460,6 +484,7 @@ module ApplicationTests
app_file "app/assets/javascripts/app.js.erb", "var src='<%= image_path('rails.png') %>';"
add_to_config "config.assets.precompile = %w{rails.png app.js}"
add_to_env_config "development", "config.assets.digest = false"
+
precompile!
assert_match "src='/sub/uri/assets/rails.png'", File.read(Dir["#{app_path}/public/assets/app-*.js"].first)
@@ -467,16 +492,16 @@ module ApplicationTests
private
- def app_with_assets_in_view
- app_file "app/assets/javascripts/application.js", "//= require_tree ."
- app_file "app/assets/javascripts/xmlhr.js", "function f1() { alert(); }"
- app_file "app/views/posts/index.html.erb", "<%= javascript_include_tag 'application' %>"
+ def app_with_assets_in_view
+ app_file "app/assets/javascripts/application.js", "//= require_tree ."
+ app_file "app/assets/javascripts/xmlhr.js", "function f1() { alert(); }"
+ app_file "app/views/posts/index.html.erb", "<%= javascript_include_tag 'application' %>"
- app_file "config/routes.rb", <<-RUBY
+ app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get '/posts', :to => "posts#index"
end
RUBY
- end
+ end
end
end
diff --git a/railties/test/application/bin_setup_test.rb b/railties/test/application/bin_setup_test.rb
index 1bdced02e9..a952d2466b 100644
--- a/railties/test/application/bin_setup_test.rb
+++ b/railties/test/application/bin_setup_test.rb
@@ -1,4 +1,6 @@
-require 'isolation/abstract_unit'
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
module ApplicationTests
class BinSetupTest < ActiveSupport::TestCase
@@ -14,39 +16,49 @@ module ApplicationTests
def test_bin_setup
Dir.chdir(app_path) do
- app_file 'db/schema.rb', <<-RUBY
+ app_file "db/schema.rb", <<-RUBY
ActiveRecord::Schema.define(version: 20140423102712) do
create_table(:articles) {}
end
RUBY
- list_tables = lambda { `bin/rails runner 'p ActiveRecord::Base.connection.tables'`.strip }
- File.write("log/my.log", "zomg!")
+ list_tables = lambda { rails("runner", "p ActiveRecord::Base.connection.tables").strip }
+ File.write("log/test.log", "zomg!")
- assert_equal '[]', list_tables.call
- assert_equal 5, File.size("log/my.log")
+ assert_equal "[]", list_tables.call
+ assert_equal 5, File.size("log/test.log")
assert_not File.exist?("tmp/restart.txt")
`bin/setup 2>&1`
- assert_equal 0, File.size("log/my.log")
- assert_equal '["articles", "schema_migrations"]', list_tables.call
+ assert_equal 0, File.size("log/test.log")
+ assert_equal '["articles", "schema_migrations", "ar_internal_metadata"]', list_tables.call
assert File.exist?("tmp/restart.txt")
end
end
def test_bin_setup_output
Dir.chdir(app_path) do
- app_file 'db/schema.rb', ""
+ app_file "db/schema.rb", ""
output = `bin/setup 2>&1`
- assert_equal(<<-OUTPUT, output)
-== Installing dependencies ==
-The Gemfile's dependencies are satisfied
-== Preparing database ==
+ # Ignore line that's only output by Bundler < 1.14
+ output.sub!(/^Resolving dependencies\.\.\.\n/, "")
+ # Suppress Bundler platform warnings from output
+ output.gsub!(/^The dependency .* will be unused .*\.\n/, "")
+ # Ignore warnings such as `Psych.safe_load is deprecated`
+ output.gsub!(/^warning:\s.*\n/, "")
+
+ assert_equal(<<~OUTPUT, output)
+ == Installing dependencies ==
+ The Gemfile's dependencies are satisfied
+
+ == Preparing database ==
+ Created database 'db/development.sqlite3'
+ Created database 'db/test.sqlite3'
-== Removing old logs and tempfiles ==
+ == Removing old logs and tempfiles ==
-== Restarting application server ==
+ == Restarting application server ==
OUTPUT
end
end
diff --git a/railties/test/application/configuration/custom_test.rb b/railties/test/application/configuration/custom_test.rb
index 28b3b2f2d6..608bc2fbe3 100644
--- a/railties/test/application/configuration/custom_test.rb
+++ b/railties/test/application/configuration/custom_test.rb
@@ -1,20 +1,20 @@
-require 'isolation/abstract_unit'
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
module ApplicationTests
module ConfigurationTests
class CustomTest < ActiveSupport::TestCase
def setup
build_app
- boot_rails
FileUtils.rm_rf("#{app_path}/config/environments")
end
def teardown
teardown_app
- FileUtils.rm_rf(new_app) if File.directory?(new_app)
end
- test 'access custom configuration point' do
+ test "access custom configuration point" do
add_to_config <<-RUBY
config.x.payment_processing.schedule = :daily
config.x.payment_processing.retries = 3
@@ -29,23 +29,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
+
+ # test that custom configuration responds to all messages
+ assert_respond_to x, :i_do_not_exist
+ assert_kind_of Method, x.method(:i_do_not_exist)
+ assert_kind_of ActiveSupport::OrderedOptions, x.i_do_not_exist
end
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 f677a7c42a..b8a0434432 100644
--- a/railties/test/application/configuration_test.rb
+++ b/railties/test/application/configuration_test.rb
@@ -1,6 +1,9 @@
+# frozen_string_literal: true
+
require "isolation/abstract_unit"
-require 'rack/test'
-require 'env_helpers'
+require "rack/test"
+require "env_helpers"
+require "set"
class ::MyMailInterceptor
def self.delivering_email(email); email; end
@@ -34,14 +37,21 @@ module ApplicationTests
FileUtils.cp_r(app_path, new_app)
end
- def app
- @app ||= Rails.application
+ def app(env = "development")
+ @app ||= begin
+ ENV["RAILS_ENV"] = env
+
+ require "#{app_path}/config/environment"
+
+ Rails.application
+ ensure
+ ENV.delete "RAILS_ENV"
+ end
end
def setup
build_app
- boot_rails
- supress_default_config
+ suppress_default_config
end
def teardown
@@ -49,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
@@ -64,12 +74,42 @@ module ApplicationTests
switch_env "RAILS_ENV", nil do
Rails.env = "development"
assert_equal "development", Rails.env
- assert_nil ENV['RAILS_ENV']
+ assert_nil ENV["RAILS_ENV"]
+ 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
+
+ with_rails_env "development" do
+ app "development"
+ assert_predicate Rails.application.config.log_tags, :blank?
+ end
+ end
+
+ test "By default logs are tagged with :request_id in production" do
+ restore_default_config
+
+ with_rails_env "production" do
+ app "production"
+ assert_equal [:request_id], Rails.application.config.log_tags
end
end
test "lib dir is on LOAD_PATH during config" do
- app_file 'lib/my_logger.rb', <<-RUBY
+ app_file "lib/my_logger.rb", <<-RUBY
require "logger"
class MyLogger < ::Logger
end
@@ -78,8 +118,22 @@ module ApplicationTests
require 'my_logger'
config.logger = MyLogger.new STDOUT
RUBY
- require "#{app_path}/config/environment"
- assert_equal 'MyLogger', Rails.application.config.logger.class.name
+
+ app "development"
+
+ assert_equal "MyLogger", Rails.application.config.logger.class.name
+ end
+
+ test "raises an error if cache does not support recyclable cache keys" do
+ build_app(initializers: true)
+ add_to_env_config "production", "config.cache_store = Class.new {}.new"
+ add_to_env_config "production", "config.active_record.cache_versioning = true"
+
+ error = assert_raise(RuntimeError) do
+ app "production"
+ end
+
+ assert_match(/You're using a cache/, error.message)
end
test "a renders exception on pending migration" do
@@ -89,15 +143,15 @@ module ApplicationTests
config.action_dispatch.show_exceptions = true
RUBY
- app_file 'db/migrate/20140708012246_create_user.rb', <<-RUBY
- class CreateUser < ActiveRecord::Migration
+ app_file "db/migrate/20140708012246_create_user.rb", <<-RUBY
+ class CreateUser < ActiveRecord::Migration::Current
def change
create_table :users
end
end
RUBY
- require "#{app_path}/config/environment"
+ app "development"
ActiveRecord::Migrator.migrations_paths = ["#{app_path}/db/migrate"]
@@ -126,31 +180,29 @@ module ApplicationTests
end
test "Rails.application is nil until app is initialized" do
- require 'rails'
+ require "rails"
assert_nil Rails.application
- require "#{app_path}/config/environment"
+ app "development"
assert_equal AppTemplate::Application.instance, Rails.application
end
test "Rails.application responds to all instance methods" do
- require "#{app_path}/config/environment"
- assert_respond_to Rails.application, :routes_reloader
+ app "development"
assert_equal Rails.application.routes_reloader, AppTemplate::Application.routes_reloader
end
test "Rails::Application responds to paths" do
- require "#{app_path}/config/environment"
- assert_respond_to AppTemplate::Application, :paths
+ app "development"
assert_equal ["#{app_path}/app/views"], AppTemplate::Application.paths["app/views"].expanded
end
test "the application root is set correctly" do
- require "#{app_path}/config/environment"
+ app "development"
assert_equal Pathname.new(app_path), Rails.application.root
end
test "the application root can be seen from the application singleton" do
- require "#{app_path}/config/environment"
+ app "development"
assert_equal Pathname.new(app_path), AppTemplate::Application.root
end
@@ -162,7 +214,8 @@ module ApplicationTests
use_frameworks []
- require "#{app_path}/config/environment"
+ app "development"
+
assert_equal Pathname.new(new_app), Rails.application.root
end
@@ -172,7 +225,7 @@ module ApplicationTests
use_frameworks []
Dir.chdir("#{app_path}") do
- require "#{app_path}/config/environment"
+ app "development"
assert_equal Pathname.new("#{app_path}"), Rails.application.root
end
end
@@ -181,7 +234,9 @@ module ApplicationTests
add_to_config <<-RUBY
config.root = "#{app_path}"
RUBY
- require "#{app_path}/config/environment"
+
+ app "development"
+
assert_instance_of Pathname, Rails.root
end
@@ -189,26 +244,139 @@ module ApplicationTests
add_to_config <<-RUBY
config.paths["public"] = "somewhere"
RUBY
- require "#{app_path}/config/environment"
+
+ app "development"
+
assert_instance_of Pathname, Rails.public_path
end
+ test "does not eager load controller actions in development" do
+ app_file "app/controllers/posts_controller.rb", <<-RUBY
+ class PostsController < ActionController::Base
+ def index;end
+ def show;end
+ end
+ RUBY
+
+ app "development"
+
+ assert_nil PostsController.instance_variable_get(:@action_methods)
+ end
+
+ test "eager loads controller actions in production" do
+ app_file "app/controllers/posts_controller.rb", <<-RUBY
+ class PostsController < ActionController::Base
+ def index;end
+ def show;end
+ end
+ RUBY
+
+ add_to_config <<-RUBY
+ config.eager_load = true
+ config.cache_classes = true
+ RUBY
+
+ app "production"
+
+ assert_equal %w(index show).to_set, PostsController.instance_variable_get(:@action_methods)
+ end
+
+ test "does not eager load mailer actions in development" do
+ app_file "app/mailers/posts_mailer.rb", <<-RUBY
+ class PostsMailer < ActionMailer::Base
+ def noop_email;end
+ end
+ RUBY
+
+ app "development"
+
+ assert_nil PostsMailer.instance_variable_get(:@action_methods)
+ end
+
+ test "eager loads mailer actions in production" do
+ app_file "app/mailers/posts_mailer.rb", <<-RUBY
+ class PostsMailer < ActionMailer::Base
+ def noop_email;end
+ end
+ RUBY
+
+ add_to_config <<-RUBY
+ config.eager_load = true
+ config.cache_classes = true
+ RUBY
+
+ app "production"
+
+ assert_equal %w(noop_email).to_set, PostsMailer.instance_variable_get(:@action_methods)
+ end
+
+ test "does not eager load attribute methods in development" do
+ app_file "app/models/post.rb", <<-RUBY
+ class Post < ActiveRecord::Base
+ end
+ RUBY
+
+ app_file "config/initializers/active_record.rb", <<-RUBY
+ ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
+ ActiveRecord::Migration.verbose = false
+ ActiveRecord::Schema.define(version: 1) do
+ create_table :posts do |t|
+ t.string :title
+ end
+ end
+ RUBY
+
+ app "development"
+
+ assert_not_includes Post.instance_methods, :title
+ end
+
+ test "eager loads attribute methods in production" do
+ app_file "app/models/post.rb", <<-RUBY
+ class Post < ActiveRecord::Base
+ end
+ RUBY
+
+ app_file "config/initializers/active_record.rb", <<-RUBY
+ ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
+ ActiveRecord::Migration.verbose = false
+ ActiveRecord::Schema.define(version: 1) do
+ create_table :posts do |t|
+ t.string :title
+ end
+ end
+ RUBY
+
+ add_to_config <<-RUBY
+ config.eager_load = true
+ config.cache_classes = true
+ RUBY
+
+ app "production"
+
+ assert_includes Post.instance_methods, :title
+ end
+
test "initialize an eager loaded, cache classes app" do
add_to_config <<-RUBY
config.eager_load = true
config.cache_classes = true
RUBY
- require "#{app_path}/config/application"
- assert Rails.application.initialize!
+ app "development"
+
+ assert_equal :require, ActiveSupport::Dependencies.mechanism
end
test "application is always added to eager_load namespaces" do
- require "#{app_path}/config/application"
+ app "development"
assert_includes Rails.application.config.eager_load_namespaces, AppTemplate::Application
end
test "the application can be eager loaded even when there are no frameworks" do
+ FileUtils.rm_rf("#{app_path}/app/jobs/application_job.rb")
+ FileUtils.rm_rf("#{app_path}/app/models/application_record.rb")
+ FileUtils.rm_rf("#{app_path}/app/mailers/application_mailer.rb")
FileUtils.rm_rf("#{app_path}/config/environments")
add_to_config <<-RUBY
config.eager_load = true
@@ -218,7 +386,7 @@ module ApplicationTests
use_frameworks []
assert_nothing_raised do
- require "#{app_path}/config/application"
+ app "development"
end
end
@@ -230,18 +398,18 @@ module ApplicationTests
RUBY
assert_nothing_raised do
- require "#{app_path}/config/application"
+ app "development"
end
end
test "filter_parameters should be able to set via config.filter_parameters in an initializer" do
- app_file 'config/initializers/filter_parameters_logging.rb', <<-RUBY
+ app_file "config/initializers/filter_parameters_logging.rb", <<-RUBY
Rails.application.config.filter_parameters += [ :password, :foo, 'bar' ]
RUBY
- require "#{app_path}/config/environment"
+ app "development"
- assert_equal [:password, :foo, 'bar'], Rails.application.env_config['action_dispatch.parameter_filter']
+ assert_equal [:password, :foo, "bar"], Rails.application.env_config["action_dispatch.parameter_filter"]
end
test "config.to_prepare is forwarded to ActionDispatch" do
@@ -253,9 +421,9 @@ module ApplicationTests
end
RUBY
- assert !$prepared
+ assert_not $prepared
- require "#{app_path}/config/environment"
+ app "development"
get "/"
assert $prepared
@@ -267,7 +435,7 @@ module ApplicationTests
end
test "skipping config.encoding still results in 'utf-8' as the default" do
- require "#{app_path}/config/application"
+ app "development"
assert_utf8
end
@@ -276,7 +444,7 @@ module ApplicationTests
config.encoding = "utf-8"
RUBY
- require "#{app_path}/config/application"
+ app "development"
assert_utf8
end
@@ -285,133 +453,173 @@ module ApplicationTests
config.paths["public"] = "somewhere"
RUBY
- require "#{app_path}/config/application"
+ app "development"
assert_equal Pathname.new(app_path).join("somewhere"), Rails.public_path
end
- test "In production mode, config.serve_static_files is off by default" do
+ test "In production mode, config.public_file_server.enabled is off by default" do
restore_default_config
with_rails_env "production" do
- require "#{app_path}/config/environment"
- assert_not app.config.serve_static_files
+ app "production"
+ assert_not app.config.public_file_server.enabled
end
end
- test "In production mode, config.serve_static_files is enabled when RAILS_SERVE_STATIC_FILES is set" do
+ test "In production mode, config.public_file_server.enabled is enabled when RAILS_SERVE_STATIC_FILES is set" do
restore_default_config
with_rails_env "production" do
switch_env "RAILS_SERVE_STATIC_FILES", "1" do
- require "#{app_path}/config/environment"
- assert app.config.serve_static_files
+ app "production"
+ assert app.config.public_file_server.enabled
+ end
+ end
+ end
+
+ test "In production mode, STDOUT logging is enabled when RAILS_LOG_TO_STDOUT is set" do
+ restore_default_config
+
+ with_rails_env "production" do
+ switch_env "RAILS_LOG_TO_STDOUT", "1" do
+ app "production"
+ assert ActiveSupport::Logger.logger_outputs_to?(app.config.logger, STDOUT)
end
end
end
- test "In production mode, config.serve_static_files is disabled when RAILS_SERVE_STATIC_FILES is blank" do
+ test "In production mode, config.public_file_server.enabled is disabled when RAILS_SERVE_STATIC_FILES is blank" do
restore_default_config
with_rails_env "production" do
switch_env "RAILS_SERVE_STATIC_FILES", " " do
- require "#{app_path}/config/environment"
- assert_not app.config.serve_static_files
+ app "production"
+ assert_not app.config.public_file_server.enabled
end
end
end
test "Use key_generator when secret_key_base is set" do
make_basic_app do |application|
- application.secrets.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33'
+ application.secrets.secret_key_base = "b3c631c314c0bbca50c1b2843150fe33"
application.config.session_store :disabled
end
class ::OmgController < ActionController::Base
def index
cookies.signed[:some_key] = "some_value"
- render text: cookies[:some_key]
+ render plain: cookies[:some_key]
end
end
get "/"
- secret = app.key_generator.generate_key('signed cookie')
+ secret = app.key_generator.generate_key("signed cookie")
verifier = ActiveSupport::MessageVerifier.new(secret)
- assert_equal 'some_value', verifier.verify(last_response.body)
+ assert_equal "some_value", verifier.verify(last_response.body)
end
test "application verifier can be used in the entire application" do
make_basic_app do |application|
- application.secrets.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33'
+ application.secrets.secret_key_base = "b3c631c314c0bbca50c1b2843150fe33"
application.config.session_store :disabled
end
message = app.message_verifier(:sensitive_value).generate("some_value")
- assert_equal 'some_value', Rails.application.message_verifier(:sensitive_value).verify(message)
+ assert_equal "some_value", Rails.application.message_verifier(:sensitive_value).verify(message)
- secret = app.key_generator.generate_key('sensitive_value')
+ secret = app.key_generator.generate_key("sensitive_value")
verifier = ActiveSupport::MessageVerifier.new(secret)
- assert_equal 'some_value', verifier.verify(message)
+ assert_equal "some_value", verifier.verify(message)
end
test "application message verifier can be used when the key_generator is ActiveSupport::LegacyKeyGenerator" do
- app_file 'config/initializers/secret_token.rb', <<-RUBY
+ app_file "config/initializers/secret_token.rb", <<-RUBY
+ Rails.application.credentials.secret_key_base = nil
Rails.application.config.secret_token = "b3c631c314c0bbca50c1b2843150fe33"
RUBY
- app_file 'config/secrets.yml', <<-YAML
- development:
- secret_key_base:
- YAML
- require "#{app_path}/config/environment"
+ app "production"
- assert_equal app.env_config['action_dispatch.key_generator'], Rails.application.key_generator
- assert_equal app.env_config['action_dispatch.key_generator'].class, ActiveSupport::LegacyKeyGenerator
+ assert_kind_of ActiveSupport::LegacyKeyGenerator, Rails.application.key_generator
message = app.message_verifier(:sensitive_value).generate("some_value")
- assert_equal 'some_value', Rails.application.message_verifier(:sensitive_value).verify(message)
+ assert_equal "some_value", Rails.application.message_verifier(:sensitive_value).verify(message)
end
- test "warns when secrets.secret_key_base is blank and config.secret_token is set" do
- app_file 'config/initializers/secret_token.rb', <<-RUBY
+ test "config.secret_token is deprecated" do
+ app_file "config/initializers/secret_token.rb", <<-RUBY
Rails.application.config.secret_token = "b3c631c314c0bbca50c1b2843150fe33"
RUBY
- app_file 'config/secrets.yml', <<-YAML
- development:
- secret_key_base:
+
+ app "production"
+
+ assert_deprecated(/secret_token/) do
+ app.secrets
+ end
+ end
+
+ test "secrets.secret_token is deprecated" do
+ app_file "config/secrets.yml", <<-YAML
+ production:
+ secret_token: "b3c631c314c0bbca50c1b2843150fe33"
YAML
- require "#{app_path}/config/environment"
- assert_deprecated(/You didn't set `secret_key_base`./) do
- app.env_config
+ app "production"
+
+ assert_deprecated(/secret_token/) do
+ app.secrets
+ end
+ end
+
+
+ test "raises when secret_key_base is blank" do
+ app_file "config/initializers/secret_token.rb", <<-RUBY
+ Rails.application.credentials.secret_key_base = nil
+ RUBY
+
+ error = assert_raise(ArgumentError) do
+ app "production"
+ end
+ assert_match(/Missing `secret_key_base`./, error.message)
+ end
+
+ test "raise when secret_key_base is not a type of string" do
+ add_to_config <<-RUBY
+ Rails.application.credentials.secret_key_base = 123
+ RUBY
+
+ assert_raise(ArgumentError) do
+ app "production"
end
end
test "prefer secrets.secret_token over config.secret_token" do
- app_file 'config/initializers/secret_token.rb', <<-RUBY
+ app_file "config/initializers/secret_token.rb", <<-RUBY
Rails.application.config.secret_token = ""
RUBY
- app_file 'config/secrets.yml', <<-YAML
+ app_file "config/secrets.yml", <<-YAML
development:
secret_token: 3b7cd727ee24e8444053437c36cc66c3
YAML
- require "#{app_path}/config/environment"
- assert_equal '3b7cd727ee24e8444053437c36cc66c3', app.secrets.secret_token
+ app "development"
+
+ assert_equal "3b7cd727ee24e8444053437c36cc66c3", app.secrets.secret_token
end
test "application verifier can build different verifiers" do
make_basic_app do |application|
- application.secrets.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33'
+ application.credentials.secret_key_base = "b3c631c314c0bbca50c1b2843150fe33"
application.config.session_store :disabled
end
default_verifier = app.message_verifier(:sensitive_value)
text_verifier = app.message_verifier(:text)
- message = text_verifier.generate('some_value')
+ message = text_verifier.generate("some_value")
- assert_equal 'some_value', text_verifier.verify(message)
+ assert_equal "some_value", text_verifier.verify(message)
assert_raises ActiveSupport::MessageVerifier::InvalidSignature do
default_verifier.verify(message)
end
@@ -421,104 +629,151 @@ module ApplicationTests
end
test "secrets.secret_key_base is used when config/secrets.yml is present" do
- app_file 'config/secrets.yml', <<-YAML
+ app_file "config/secrets.yml", <<-YAML
development:
secret_key_base: 3b7cd727ee24e8444053437c36cc66c3
YAML
- require "#{app_path}/config/environment"
- assert_equal '3b7cd727ee24e8444053437c36cc66c3', app.secrets.secret_key_base
+ app "development"
+ assert_equal "3b7cd727ee24e8444053437c36cc66c3", app.secrets.secret_key_base
+ assert_equal "3b7cd727ee24e8444053437c36cc66c3", app.secret_key_base
end
test "secret_key_base is copied from config to secrets when not set" do
remove_file "config/secrets.yml"
- app_file 'config/initializers/secret_token.rb', <<-RUBY
+ app_file "config/initializers/secret_token.rb", <<-RUBY
Rails.application.config.secret_key_base = "3b7cd727ee24e8444053437c36cc66c3"
RUBY
- require "#{app_path}/config/environment"
- assert_equal '3b7cd727ee24e8444053437c36cc66c3', app.secrets.secret_key_base
+ app "development"
+ assert_equal "3b7cd727ee24e8444053437c36cc66c3", app.secrets.secret_key_base
end
test "config.secret_token over-writes a blank secrets.secret_token" do
- app_file 'config/initializers/secret_token.rb', <<-RUBY
+ app_file "config/initializers/secret_token.rb", <<-RUBY
Rails.application.config.secret_token = "b3c631c314c0bbca50c1b2843150fe33"
RUBY
- app_file 'config/secrets.yml', <<-YAML
+ app_file "config/secrets.yml", <<-YAML
development:
secret_key_base:
secret_token:
YAML
- require "#{app_path}/config/environment"
- assert_equal 'b3c631c314c0bbca50c1b2843150fe33', app.secrets.secret_token
- assert_equal 'b3c631c314c0bbca50c1b2843150fe33', app.config.secret_token
+ app "development"
+
+ assert_equal "b3c631c314c0bbca50c1b2843150fe33", app.secrets.secret_token
+ assert_equal "b3c631c314c0bbca50c1b2843150fe33", app.config.secret_token
end
test "custom secrets saved in config/secrets.yml are loaded in app secrets" do
- app_file 'config/secrets.yml', <<-YAML
+ app_file "config/secrets.yml", <<-YAML
development:
secret_key_base: 3b7cd727ee24e8444053437c36cc66c3
aws_access_key_id: myamazonaccesskeyid
aws_secret_access_key: myamazonsecretaccesskey
YAML
- require "#{app_path}/config/environment"
- assert_equal 'myamazonaccesskeyid', app.secrets.aws_access_key_id
- assert_equal 'myamazonsecretaccesskey', app.secrets.aws_secret_access_key
+ app "development"
+
+ assert_equal "myamazonaccesskeyid", app.secrets.aws_access_key_id
+ assert_equal "myamazonsecretaccesskey", app.secrets.aws_secret_access_key
+ end
+
+ test "shared secrets saved in config/secrets.yml are loaded in app secrets" do
+ app_file "config/secrets.yml", <<-YAML
+ shared:
+ api_key: 3b7cd727
+ YAML
+
+ app "development"
+
+ assert_equal "3b7cd727", app.secrets.api_key
+ end
+
+ test "shared secrets will yield to environment specific secrets" do
+ app_file "config/secrets.yml", <<-YAML
+ shared:
+ api_key: 3b7cd727
+
+ development:
+ api_key: abc12345
+ YAML
+
+ app "development"
+
+ assert_equal "abc12345", app.secrets.api_key
end
test "blank config/secrets.yml does not crash the loading process" do
- app_file 'config/secrets.yml', <<-YAML
+ app_file "config/secrets.yml", <<-YAML
YAML
- require "#{app_path}/config/environment"
+
+ app "development"
assert_nil app.secrets.not_defined
end
test "config.secret_key_base over-writes a blank secrets.secret_key_base" do
- app_file 'config/initializers/secret_token.rb', <<-RUBY
+ app_file "config/initializers/secret_token.rb", <<-RUBY
Rails.application.config.secret_key_base = "iaminallyoursecretkeybase"
RUBY
- app_file 'config/secrets.yml', <<-YAML
+ app_file "config/secrets.yml", <<-YAML
development:
secret_key_base:
YAML
- require "#{app_path}/config/environment"
+
+ app "development"
assert_equal "iaminallyoursecretkeybase", app.secrets.secret_key_base
end
test "uses ActiveSupport::LegacyKeyGenerator as app.key_generator when secrets.secret_key_base is blank" do
- app_file 'config/initializers/secret_token.rb', <<-RUBY
+ app_file "config/initializers/secret_token.rb", <<-RUBY
+ Rails.application.credentials.secret_key_base = nil
Rails.application.config.secret_token = "b3c631c314c0bbca50c1b2843150fe33"
RUBY
- app_file 'config/secrets.yml', <<-YAML
- development:
- secret_key_base:
- YAML
- require "#{app_path}/config/environment"
- assert_equal 'b3c631c314c0bbca50c1b2843150fe33', app.config.secret_token
- assert_equal nil, app.secrets.secret_key_base
- assert_equal app.key_generator.class, ActiveSupport::LegacyKeyGenerator
+ app "production"
+
+ assert_equal "b3c631c314c0bbca50c1b2843150fe33", app.config.secret_token
+ assert_nil app.credentials.secret_key_base
+ assert_kind_of ActiveSupport::LegacyKeyGenerator, app.key_generator
end
- test "uses ActiveSupport::LegacyKeyGenerator with config.secret_token as app.key_generator when secrets.secret_key_base is blank" do
- app_file 'config/initializers/secret_token.rb', <<-RUBY
- Rails.application.config.secret_token = ""
- RUBY
- app_file 'config/secrets.yml', <<-YAML
+ 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:
- secret_key_base:
+ smtp_settings:
+ address: "smtp.example.com"
+ user_name: "postmaster@example.com"
+ password: "697361616320736c6f616e2028656c6f7265737429"
YAML
- require "#{app_path}/config/environment"
- assert_equal '', app.config.secret_token
- assert_equal nil, app.secrets.secret_key_base
- assert_raise ArgumentError, /\AA secret is required/ do
- app.key_generator
+ app "development"
+
+ assert_equal "697361616320736c6f616e2028656c6f7265737429", app.secrets.smtp_settings[:password]
+ end
+
+ test "require_master_key aborts app boot when missing key" do
+ skip "can't run without fork" unless Process.respond_to?(:fork)
+
+ remove_file "config/master.key"
+ add_to_config "config.require_master_key = true"
+
+ error = capture(:stderr) do
+ Process.wait(Process.fork { app "development" })
end
+
+ assert_equal 1, $?.exitstatus
+ assert_match(/Missing.*RAILS_MASTER_KEY/, error)
+ end
+
+ test "credentials does not raise error when require_master_key is false and master key does not exist" do
+ remove_file "config/master.key"
+ add_to_config "config.require_master_key = false"
+ app "development"
+
+ assert_not app.credentials.secret_key_base
end
test "protect from forgery is the default in a new app" do
@@ -535,8 +790,7 @@ module ApplicationTests
end
test "default form builder specified as a string" do
-
- app_file 'config/initializers/form_builder.rb', <<-RUBY
+ app_file "config/initializers/form_builder.rb", <<-RUBY
class CustomFormBuilder < ActionView::Helpers::FormBuilder
def text_field(attribute, *args)
label(attribute) + super(attribute, *args)
@@ -545,15 +799,14 @@ module ApplicationTests
Rails.configuration.action_view.default_form_builder = "CustomFormBuilder"
RUBY
- app_file 'app/models/post.rb', <<-RUBY
+ app_file "app/models/post.rb", <<-RUBY
class Post
include ActiveModel::Model
attr_accessor :name
end
RUBY
-
- app_file 'app/controllers/posts_controller.rb', <<-RUBY
+ app_file "app/controllers/posts_controller.rb", <<-RUBY
class PostsController < ApplicationController
def index
render inline: "<%= begin; form_for(Post.new) {|f| f.text_field(:name)}; rescue => e; e.to_s; end %>"
@@ -567,14 +820,136 @@ module ApplicationTests
end
RUBY
- require "#{app_path}/config/environment"
+ app "development"
get "/posts"
assert_match(/label/, last_response.body)
end
+ test "form_with can be configured with form_with_generates_ids" do
+ app_file "config/initializers/form_builder.rb", <<-RUBY
+ Rails.configuration.action_view.form_with_generates_ids = false
+ RUBY
+
+ app_file "app/models/post.rb", <<-RUBY
+ class Post
+ include ActiveModel::Model
+ attr_accessor :name
+ end
+ RUBY
+
+ app_file "app/controllers/posts_controller.rb", <<-RUBY
+ class PostsController < ApplicationController
+ def index
+ render inline: "<%= begin; form_with(model: Post.new) {|f| f.text_field(:name)}; rescue => e; e.to_s; end %>"
+ end
+ end
+ RUBY
+
+ add_to_config <<-RUBY
+ routes.prepend do
+ resources :posts
+ end
+ RUBY
+
+ app "development"
+
+ get "/posts"
+
+ assert_no_match(/id=('|")post_name('|")/, last_response.body)
+ end
+
+ test "form_with outputs ids by default" do
+ app_file "app/models/post.rb", <<-RUBY
+ class Post
+ include ActiveModel::Model
+ attr_accessor :name
+ end
+ RUBY
+
+ app_file "app/controllers/posts_controller.rb", <<-RUBY
+ class PostsController < ApplicationController
+ def index
+ render inline: "<%= begin; form_with(model: Post.new) {|f| f.text_field(:name)}; rescue => e; e.to_s; end %>"
+ end
+ end
+ RUBY
+
+ add_to_config <<-RUBY
+ routes.prepend do
+ resources :posts
+ end
+ RUBY
+
+ app "development"
+
+ get "/posts"
+
+ assert_match(/id=('|")post_name('|")/, last_response.body)
+ end
+
+ test "form_with can be configured with form_with_generates_remote_forms" do
+ app_file "config/initializers/form_builder.rb", <<-RUBY
+ Rails.configuration.action_view.form_with_generates_remote_forms = false
+ RUBY
+
+ app_file "app/models/post.rb", <<-RUBY
+ class Post
+ include ActiveModel::Model
+ attr_accessor :name
+ end
+ RUBY
+
+ app_file "app/controllers/posts_controller.rb", <<-RUBY
+ class PostsController < ApplicationController
+ def index
+ render inline: "<%= begin; form_with(model: Post.new) {|f| f.text_field(:name)}; rescue => e; e.to_s; end %>"
+ end
+ end
+ RUBY
+
+ add_to_config <<-RUBY
+ routes.prepend do
+ resources :posts
+ end
+ RUBY
+
+ app "development"
+
+ get "/posts"
+ assert_no_match(/data-remote/, last_response.body)
+ end
+
+ test "form_with generates remote forms by default" do
+ app_file "app/models/post.rb", <<-RUBY
+ class Post
+ include ActiveModel::Model
+ attr_accessor :name
+ end
+ RUBY
+
+ app_file "app/controllers/posts_controller.rb", <<-RUBY
+ class PostsController < ApplicationController
+ def index
+ render inline: "<%= begin; form_with(model: Post.new) {|f| f.text_field(:name)}; rescue => e; e.to_s; end %>"
+ end
+ end
+ RUBY
+
+ add_to_config <<-RUBY
+ routes.prepend do
+ resources :posts
+ end
+ RUBY
+
+ app "development"
+
+ get "/posts"
+ assert_match(/data-remote/, last_response.body)
+ end
+
test "default method for update can be changed" do
- app_file 'app/models/post.rb', <<-RUBY
+ app_file "app/models/post.rb", <<-RUBY
class Post
include ActiveModel::Model
def to_key; [1]; end
@@ -584,19 +959,19 @@ module ApplicationTests
token = "cf50faa3fe97702ca1ae"
- app_file 'app/controllers/posts_controller.rb', <<-RUBY
+ app_file "app/controllers/posts_controller.rb", <<-RUBY
class PostsController < ApplicationController
def show
render inline: "<%= begin; form_for(Post.new) {}; rescue => e; e.to_s; end %>"
end
def update
- render text: "update"
+ render plain: "update"
end
private
- def form_authenticity_token; token; end # stub the authenticy token
+ def form_authenticity_token(*args); token; end # stub the authenticity token
end
RUBY
@@ -606,9 +981,9 @@ module ApplicationTests
end
RUBY
- require "#{app_path}/config/environment"
+ app "development"
- params = {authenticity_token: token}
+ params = { authenticity_token: token }
get "/posts/1"
assert_match(/patch/, last_response.body)
@@ -628,7 +1003,7 @@ module ApplicationTests
test "request forgery token param can be changed" do
make_basic_app do |application|
- application.config.action_controller.request_forgery_protection_token = '_xsrf_token_here'
+ application.config.action_controller.request_forgery_protection_token = "_xsrf_token_here"
end
class ::OmgController < ActionController::Base
@@ -659,12 +1034,12 @@ module ApplicationTests
config.action_mailer.interceptors = MyMailInterceptor
RUBY
- require "#{app_path}/config/environment"
- require "mail"
+ app "development"
+ require "mail"
_ = ActionMailer::Base
- assert_equal [::MyMailInterceptor], ::Mail.send(:class_variable_get, "@@delivery_interceptors")
+ assert_equal [::MyMailInterceptor], ::Mail.class_variable_get(:@@delivery_interceptors)
end
test "registers multiple interceptors with ActionMailer" do
@@ -672,12 +1047,12 @@ module ApplicationTests
config.action_mailer.interceptors = [MyMailInterceptor, "MyOtherMailInterceptor"]
RUBY
- require "#{app_path}/config/environment"
- require "mail"
+ app "development"
+ require "mail"
_ = ActionMailer::Base
- assert_equal [::MyMailInterceptor, ::MyOtherMailInterceptor], ::Mail.send(:class_variable_get, "@@delivery_interceptors")
+ assert_equal [::MyMailInterceptor, ::MyOtherMailInterceptor], ::Mail.class_variable_get(:@@delivery_interceptors)
end
test "registers preview interceptors with ActionMailer" do
@@ -685,9 +1060,9 @@ module ApplicationTests
config.action_mailer.preview_interceptors = MyPreviewMailInterceptor
RUBY
- require "#{app_path}/config/environment"
- require "mail"
+ app "development"
+ require "mail"
_ = ActionMailer::Base
assert_equal [ActionMailer::InlinePreviewInterceptor, ::MyPreviewMailInterceptor], ActionMailer::Base.preview_interceptors
@@ -698,22 +1073,22 @@ module ApplicationTests
config.action_mailer.preview_interceptors = [MyPreviewMailInterceptor, "MyOtherPreviewMailInterceptor"]
RUBY
- require "#{app_path}/config/environment"
- require "mail"
+ app "development"
+ require "mail"
_ = ActionMailer::Base
assert_equal [ActionMailer::InlinePreviewInterceptor, MyPreviewMailInterceptor, MyOtherPreviewMailInterceptor], ActionMailer::Base.preview_interceptors
end
test "default preview interceptor can be removed" do
- app_file 'config/initializers/preview_interceptors.rb', <<-RUBY
+ app_file "config/initializers/preview_interceptors.rb", <<-RUBY
ActionMailer::Base.preview_interceptors.delete(ActionMailer::InlinePreviewInterceptor)
RUBY
- require "#{app_path}/config/environment"
- require "mail"
+ app "development"
+ require "mail"
_ = ActionMailer::Base
assert_equal [], ActionMailer::Base.preview_interceptors
@@ -724,12 +1099,12 @@ module ApplicationTests
config.action_mailer.observers = MyMailObserver
RUBY
- require "#{app_path}/config/environment"
- require "mail"
+ app "development"
+ require "mail"
_ = ActionMailer::Base
- assert_equal [::MyMailObserver], ::Mail.send(:class_variable_get, "@@delivery_notification_observers")
+ assert_equal [::MyMailObserver], ::Mail.class_variable_get(:@@delivery_notification_observers)
end
test "registers multiple observers with ActionMailer" do
@@ -737,12 +1112,12 @@ module ApplicationTests
config.action_mailer.observers = [MyMailObserver, "MyOtherMailObserver"]
RUBY
- require "#{app_path}/config/environment"
- require "mail"
+ app "development"
+ require "mail"
_ = ActionMailer::Base
- assert_equal [::MyMailObserver, ::MyOtherMailObserver], ::Mail.send(:class_variable_get, "@@delivery_notification_observers")
+ assert_equal [::MyMailObserver, ::MyOtherMailObserver], ::Mail.class_variable_get(:@@delivery_notification_observers)
end
test "allows setting the queue name for the ActionMailer::DeliveryJob" do
@@ -750,12 +1125,12 @@ module ApplicationTests
config.action_mailer.deliver_later_queue_name = 'test_default'
RUBY
- require "#{app_path}/config/environment"
- require "mail"
+ app "development"
+ require "mail"
_ = ActionMailer::Base
- assert_equal 'test_default', ActionMailer::Base.send(:class_variable_get, "@@deliver_later_queue_name")
+ assert_equal "test_default", ActionMailer::Base.class_variable_get(:@@deliver_later_queue_name)
end
test "valid timezone is setup correctly" do
@@ -764,7 +1139,7 @@ module ApplicationTests
config.time_zone = "Wellington"
RUBY
- require "#{app_path}/config/environment"
+ app "development"
assert_equal "Wellington", Rails.application.config.time_zone
end
@@ -776,7 +1151,7 @@ module ApplicationTests
RUBY
assert_raise(ArgumentError) do
- require "#{app_path}/config/environment"
+ app "development"
end
end
@@ -786,7 +1161,7 @@ module ApplicationTests
config.beginning_of_week = :wednesday
RUBY
- require "#{app_path}/config/environment"
+ app "development"
assert_equal :wednesday, Rails.application.config.beginning_of_week
end
@@ -798,22 +1173,24 @@ module ApplicationTests
RUBY
assert_raise(ArgumentError) do
- require "#{app_path}/config/environment"
+ app "development"
end
end
test "config.action_view.cache_template_loading with cache_classes default" do
add_to_config "config.cache_classes = true"
- require "#{app_path}/config/environment"
- require 'action_view/base'
+
+ app "development"
+ require "action_view/base"
assert_equal true, ActionView::Resolver.caching?
end
test "config.action_view.cache_template_loading without cache_classes default" do
add_to_config "config.cache_classes = false"
- require "#{app_path}/config/environment"
- require 'action_view/base'
+
+ app "development"
+ require "action_view/base"
assert_equal false, ActionView::Resolver.caching?
end
@@ -823,8 +1200,9 @@ module ApplicationTests
config.cache_classes = true
config.action_view.cache_template_loading = false
RUBY
- require "#{app_path}/config/environment"
- require 'action_view/base'
+
+ app "development"
+ require "action_view/base"
assert_equal false, ActionView::Resolver.caching?
end
@@ -834,8 +1212,9 @@ module ApplicationTests
config.cache_classes = false
config.action_view.cache_template_loading = true
RUBY
- require "#{app_path}/config/environment"
- require 'action_view/base'
+
+ app "development"
+ require "action_view/base"
assert_equal true, ActionView::Resolver.caching?
end
@@ -845,11 +1224,11 @@ module ApplicationTests
add_to_env_config "development", "config.cache_classes = false"
# These requires are to emulate an engine loading Action View before the application
- require 'action_view'
- require 'action_view/railtie'
- require 'action_view/base'
+ require "action_view"
+ require "action_view/railtie"
+ require "action_view/base"
- require "#{app_path}/config/environment"
+ app "development"
assert_equal false, ActionView::Resolver.caching?
end
@@ -861,20 +1240,20 @@ module ApplicationTests
class ::OmgController < ActionController::Base
def index
- render text: env["action_dispatch.show_exceptions"]
+ render plain: request.env["action_dispatch.show_exceptions"]
end
end
get "/"
- assert_equal 'true', last_response.body
+ assert_equal "true", last_response.body
end
test "config.action_controller.wrap_parameters is set in ActionController::Base" do
- app_file 'config/initializers/wrap_parameters.rb', <<-RUBY
+ app_file "config/initializers/wrap_parameters.rb", <<-RUBY
ActionController::Base.wrap_parameters format: [:json]
RUBY
- app_file 'app/models/post.rb', <<-RUBY
+ app_file "app/models/post.rb", <<-RUBY
class Post
def self.attribute_names
%w(title)
@@ -882,16 +1261,16 @@ module ApplicationTests
end
RUBY
- app_file 'app/controllers/application_controller.rb', <<-RUBY
+ app_file "app/controllers/application_controller.rb", <<-RUBY
class ApplicationController < ActionController::Base
protect_from_forgery with: :reset_session # as we are testing API here
end
RUBY
- app_file 'app/controllers/posts_controller.rb', <<-RUBY
+ app_file "app/controllers/posts_controller.rb", <<-RUBY
class PostsController < ApplicationController
def create
- render text: params[:post].inspect
+ render plain: params[:post].inspect
end
end
RUBY
@@ -902,17 +1281,17 @@ module ApplicationTests
end
RUBY
- require "#{app_path}/config/environment"
+ app "development"
post "/posts.json", '{ "title": "foo", "name": "bar" }', "CONTENT_TYPE" => "application/json"
- assert_equal '{"title"=>"foo"}', last_response.body
+ assert_equal '<ActionController::Parameters {"title"=>"foo"} permitted: false>', last_response.body
end
test "config.action_controller.permit_all_parameters = true" do
- app_file 'app/controllers/posts_controller.rb', <<-RUBY
+ app_file "app/controllers/posts_controller.rb", <<-RUBY
class PostsController < ActionController::Base
def create
- render text: params[:post].permitted? ? "permitted" : "forbidden"
+ render plain: params[:post].permitted? ? "permitted" : "forbidden"
end
end
RUBY
@@ -924,17 +1303,17 @@ module ApplicationTests
config.action_controller.permit_all_parameters = true
RUBY
- require "#{app_path}/config/environment"
+ app "development"
- post "/posts", {post: {"title" =>"zomg"}}
- assert_equal 'permitted', last_response.body
+ post "/posts", post: { "title" => "zomg" }
+ assert_equal "permitted", last_response.body
end
test "config.action_controller.action_on_unpermitted_parameters = :raise" do
- app_file 'app/controllers/posts_controller.rb', <<-RUBY
+ app_file "app/controllers/posts_controller.rb", <<-RUBY
class PostsController < ActionController::Base
def create
- render text: params.require(:post).permit(:name)
+ render plain: params.require(:post).permit(:name)
end
end
RUBY
@@ -946,16 +1325,23 @@ module ApplicationTests
config.action_controller.action_on_unpermitted_parameters = :raise
RUBY
- require "#{app_path}/config/environment"
+ app "development"
+
+ force_lazy_load_hooks { ActionController::Base }
+ force_lazy_load_hooks { ActionController::API }
assert_equal :raise, ActionController::Parameters.action_on_unpermitted_parameters
- post "/posts", {post: {"title" =>"zomg"}}
+ post "/posts", post: { "title" => "zomg" }
assert_match "We're sorry, but something went wrong", last_response.body
end
test "config.action_controller.always_permitted_parameters are: controller, action by default" do
- require "#{app_path}/config/environment"
+ app "development"
+
+ force_lazy_load_hooks { ActionController::Base }
+ force_lazy_load_hooks { ActionController::API }
+
assert_equal %w(controller action), ActionController::Parameters.always_permitted_parameters
end
@@ -963,15 +1349,20 @@ module ApplicationTests
add_to_config <<-RUBY
config.action_controller.always_permitted_parameters = %w( controller action format )
RUBY
- require "#{app_path}/config/environment"
+
+ app "development"
+
+ force_lazy_load_hooks { ActionController::Base }
+ force_lazy_load_hooks { ActionController::API }
+
assert_equal %w( controller action format ), ActionController::Parameters.always_permitted_parameters
end
- test "config.action_controller.always_permitted_parameters = ['controller','action','format'] does not raise exeception" do
- app_file 'app/controllers/posts_controller.rb', <<-RUBY
+ test "config.action_controller.always_permitted_parameters = ['controller','action','format'] does not raise exception" do
+ app_file "app/controllers/posts_controller.rb", <<-RUBY
class PostsController < ActionController::Base
def create
- render text: params.permit(post: [:title])
+ render plain: params.permit(post: [:title])
end
end
RUBY
@@ -984,38 +1375,87 @@ module ApplicationTests
config.action_controller.action_on_unpermitted_parameters = :raise
RUBY
- require "#{app_path}/config/environment"
+ app "development"
+
+ force_lazy_load_hooks { ActionController::Base }
+ force_lazy_load_hooks { ActionController::API }
assert_equal :raise, ActionController::Parameters.action_on_unpermitted_parameters
- post "/posts", {post: {"title" =>"zomg"}, format: "json"}
+ post "/posts", post: { "title" => "zomg" }, format: "json"
assert_equal 200, last_response.status
end
- test "config.action_controller.action_on_unpermitted_parameters is :log by default on development" do
- ENV["RAILS_ENV"] = "development"
+ test "config.action_controller.action_on_unpermitted_parameters is :log by default in development" do
+ app "development"
- require "#{app_path}/config/environment"
+ force_lazy_load_hooks { ActionController::Base }
+ force_lazy_load_hooks { ActionController::API }
assert_equal :log, ActionController::Parameters.action_on_unpermitted_parameters
end
- test "config.action_controller.action_on_unpermitted_parameters is :log by default on test" do
- ENV["RAILS_ENV"] = "test"
+ test "config.action_controller.action_on_unpermitted_parameters is :log by default in test" do
+ app "test"
- require "#{app_path}/config/environment"
+ force_lazy_load_hooks { ActionController::Base }
+ force_lazy_load_hooks { ActionController::API }
assert_equal :log, ActionController::Parameters.action_on_unpermitted_parameters
end
- test "config.action_controller.action_on_unpermitted_parameters is false by default on production" do
- ENV["RAILS_ENV"] = "production"
+ test "config.action_controller.action_on_unpermitted_parameters is false by default in production" do
+ app "production"
- require "#{app_path}/config/environment"
+ force_lazy_load_hooks { ActionController::Base }
+ force_lazy_load_hooks { ActionController::API }
assert_equal false, ActionController::Parameters.action_on_unpermitted_parameters
end
+ test "config.action_controller.default_protect_from_forgery is true by default" do
+ app "development"
+
+ assert_equal true, ActionController::Base.default_protect_from_forgery
+ assert_includes ActionController::Base.__callbacks[:process_action].map(&:filter), :verify_authenticity_token
+ end
+
+ test "config.action_controller.permit_all_parameters can be configured in an initializer" do
+ app_file "config/initializers/permit_all_parameters.rb", <<-RUBY
+ Rails.application.config.action_controller.permit_all_parameters = true
+ RUBY
+
+ app "development"
+
+ force_lazy_load_hooks { ActionController::Base }
+ force_lazy_load_hooks { ActionController::API }
+ assert_equal true, ActionController::Parameters.permit_all_parameters
+ end
+
+ test "config.action_controller.always_permitted_parameters can be configured in an initializer" do
+ app_file "config/initializers/always_permitted_parameters.rb", <<-RUBY
+ Rails.application.config.action_controller.always_permitted_parameters = []
+ RUBY
+
+ app "development"
+
+ force_lazy_load_hooks { ActionController::Base }
+ force_lazy_load_hooks { ActionController::API }
+ assert_equal [], ActionController::Parameters.always_permitted_parameters
+ end
+
+ test "config.action_controller.action_on_unpermitted_parameters can be configured in an initializer" do
+ app_file "config/initializers/action_on_unpermitted_parameters.rb", <<-RUBY
+ Rails.application.config.action_controller.action_on_unpermitted_parameters = :raise
+ RUBY
+
+ app "development"
+
+ force_lazy_load_hooks { ActionController::Base }
+ force_lazy_load_hooks { ActionController::API }
+ assert_equal :raise, ActionController::Parameters.action_on_unpermitted_parameters
+ end
+
test "config.action_dispatch.ignore_accept_header" do
make_basic_app do |application|
application.config.action_dispatch.ignore_accept_header = true
@@ -1024,28 +1464,27 @@ module ApplicationTests
class ::OmgController < ActionController::Base
def index
respond_to do |format|
- format.html { render text: "HTML" }
- format.xml { render text: "XML" }
+ format.html { render plain: "HTML" }
+ format.xml { render plain: "XML" }
end
end
end
- get "/", {}, "HTTP_ACCEPT" => "application/xml"
- assert_equal 'HTML', last_response.body
+ get "/", {}, { "HTTP_ACCEPT" => "application/xml" }
+ assert_equal "HTML", last_response.body
- get "/", { format: :xml }, "HTTP_ACCEPT" => "application/xml"
- assert_equal 'XML', last_response.body
+ get "/", { format: :xml }, { "HTTP_ACCEPT" => "application/xml" }
+ assert_equal "XML", last_response.body
end
- test "Rails.application#env_config exists and include some existing parameters" do
+ test "Rails.application#env_config exists and includes some existing parameters" do
make_basic_app
- assert_respond_to app, :env_config
- assert_equal app.env_config['action_dispatch.parameter_filter'], app.config.filter_parameters
- assert_equal app.env_config['action_dispatch.show_exceptions'], app.config.action_dispatch.show_exceptions
- assert_equal app.env_config['action_dispatch.logger'], Rails.logger
- assert_equal app.env_config['action_dispatch.backtrace_cleaner'], Rails.backtrace_cleaner
- assert_equal app.env_config['action_dispatch.key_generator'], Rails.application.key_generator
+ assert_equal app.env_config["action_dispatch.parameter_filter"], app.config.filter_parameters
+ assert_equal app.env_config["action_dispatch.show_exceptions"], app.config.action_dispatch.show_exceptions
+ assert_equal app.env_config["action_dispatch.logger"], Rails.logger
+ assert_equal app.env_config["action_dispatch.backtrace_cleaner"], Rails.backtrace_cleaner
+ assert_equal app.env_config["action_dispatch.key_generator"], Rails.application.key_generator
end
test "config.colorize_logging default is true" do
@@ -1065,11 +1504,28 @@ 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
+ make_basic_app do |application|
+ application.config.session_store :disabled
+ end
+
+ assert_nil app.config.session_store
+ end
+
+ test "default session store initializer sets session store to cookie store" do
+ session_options = { key: "_myapp_session", cookie_only: true }
+ make_basic_app
+
+ assert_equal ActionDispatch::Session::CookieStore, app.config.session_store
+ assert_equal session_options, app.config.session_options
end
test "config.log_level with custom logger" do
@@ -1083,27 +1539,30 @@ module ApplicationTests
test "respond_to? accepts include_private" do
make_basic_app
- assert_not Rails.configuration.respond_to?(:method_missing)
+ assert_not_respond_to Rails.configuration, :method_missing
assert Rails.configuration.respond_to?(:method_missing, true)
end
test "config.active_record.dump_schema_after_migration is false on production" do
build_app
- ENV["RAILS_ENV"] = "production"
- require "#{app_path}/config/environment"
+ app "production"
assert_not ActiveRecord::Base.dump_schema_after_migration
end
- test "config.active_record.dump_schema_after_migration is true by default on development" do
- ENV["RAILS_ENV"] = "development"
-
- require "#{app_path}/config/environment"
+ test "config.active_record.dump_schema_after_migration is true by default in development" do
+ app "development"
assert ActiveRecord::Base.dump_schema_after_migration
end
+ test "config.active_record.verbose_query_logs is false by default in development" do
+ app "development"
+
+ assert_not ActiveRecord::Base.verbose_query_logs
+ end
+
test "config.annotations wrapping SourceAnnotationExtractor::Annotation class" do
make_basic_app do |application|
application.config.annotations.register_extensions("coffee") do |tag|
@@ -1111,7 +1570,7 @@ module ApplicationTests
end
end
- assert_not_nil SourceAnnotationExtractor::Annotation.extensions[/\.(coffee)$/]
+ assert_not_nil Rails::SourceAnnotationExtractor::Annotation.extensions[/\.(coffee)$/]
end
test "rake_tasks block works at instance level" do
@@ -1125,12 +1584,12 @@ module ApplicationTests
end
RUBY
- require "#{app_path}/config/environment"
+ app "development"
assert_not Rails.configuration.ran_block
- require 'rake'
- require 'rake/testtask'
- require 'rdoc/task'
+ require "rake"
+ require "rake/testtask"
+ require "rdoc/task"
Rails.application.load_tasks
assert Rails.configuration.ran_block
@@ -1147,7 +1606,7 @@ module ApplicationTests
end
RUBY
- require "#{app_path}/config/environment"
+ app "development"
assert_not Rails.configuration.ran_block
Rails.application.load_generators
@@ -1165,7 +1624,7 @@ module ApplicationTests
end
RUBY
- require "#{app_path}/config/environment"
+ app "development"
assert_not Rails.configuration.ran_block
Rails.application.load_console
@@ -1183,7 +1642,7 @@ module ApplicationTests
end
RUBY
- require "#{app_path}/config/environment"
+ app "development"
assert_not Rails.configuration.ran_block
Rails.application.load_runner
@@ -1191,7 +1650,7 @@ module ApplicationTests
end
test "loading the first existing database configuration available" do
- app_file 'config/environments/development.rb', <<-RUBY
+ app_file "config/environments/development.rb", <<-RUBY
Rails.application.configure do
config.paths.add 'config/database', with: 'config/nonexistent.yml'
@@ -1199,57 +1658,159 @@ module ApplicationTests
end
RUBY
- require "#{app_path}/config/environment"
+ app "development"
assert_kind_of Hash, Rails.application.config.database_configuration
end
- test 'raises with proper error message if no database configuration found' do
+ test "autoload paths do not include asset paths" do
+ app "development"
+ ActiveSupport::Dependencies.autoload_paths.each do |path|
+ assert_not_operator path, :ends_with?, "app/assets"
+ assert_not_operator path, :ends_with?, "app/javascript"
+ end
+ end
+
+ test "raises with proper error message if no database configuration found" do
FileUtils.rm("#{app_path}/config/database.yml")
- require "#{app_path}/config/environment"
err = assert_raises RuntimeError do
+ app "development"
Rails.application.config.database_configuration
end
- assert_match 'config/database', err.message
+ assert_match "config/database", err.message
end
- test 'config.action_mailer.show_previews defaults to true in development' do
- Rails.env = "development"
- require "#{app_path}/config/environment"
+ test "loads database.yml using shared keys" do
+ app_file "config/database.yml", <<-YAML
+ shared:
+ username: bobby
+ adapter: sqlite3
+
+ development:
+ database: 'dev_db'
+ YAML
+
+ app "development"
+
+ ar_config = Rails.application.config.database_configuration
+ assert_equal "sqlite3", ar_config["development"]["adapter"]
+ assert_equal "bobby", ar_config["development"]["username"]
+ assert_equal "dev_db", ar_config["development"]["database"]
+ end
+
+ test "loads database.yml using shared keys for undefined environments" do
+ app_file "config/database.yml", <<-YAML
+ shared:
+ username: bobby
+ adapter: sqlite3
+ database: 'dev_db'
+ YAML
+
+ app "development"
+
+ ar_config = Rails.application.config.database_configuration
+ assert_equal "sqlite3", ar_config["development"]["adapter"]
+ assert_equal "bobby", ar_config["development"]["username"]
+ assert_equal "dev_db", ar_config["development"]["database"]
+ end
+
+ test "config.action_mailer.show_previews defaults to true in development" do
+ app "development"
assert Rails.application.config.action_mailer.show_previews
end
- test 'config.action_mailer.show_previews defaults to false in production' do
- Rails.env = "production"
- require "#{app_path}/config/environment"
+ test "config.action_mailer.show_previews defaults to false in production" do
+ app "production"
assert_equal false, Rails.application.config.action_mailer.show_previews
end
- test 'config.action_mailer.show_previews can be set in the configuration file' do
- Rails.env = "production"
+ test "config.action_mailer.show_previews can be set in the configuration file" do
add_to_config <<-RUBY
config.action_mailer.show_previews = true
RUBY
- require "#{app_path}/config/environment"
+
+ app "production"
assert_equal true, Rails.application.config.action_mailer.show_previews
end
test "config_for loads custom configuration from yaml files" do
- app_file 'config/custom.yml', <<-RUBY
+ app_file "config/custom.yml", <<-RUBY
development:
- key: 'custom key'
+ foo: 'bar'
RUBY
add_to_config <<-RUBY
config.my_custom_config = config_for('custom')
RUBY
- require "#{app_path}/config/environment"
+ app "development"
+
+ assert_equal "bar", Rails.application.config.my_custom_config["foo"]
+ end
+
+ test "config_for loads custom configuration from yaml accessible as symbol" do
+ app_file "config/custom.yml", <<-RUBY
+ development:
+ foo: 'bar'
+ RUBY
+
+ add_to_config <<-RUBY
+ config.my_custom_config = config_for('custom')
+ RUBY
+
+ app "development"
+
+ assert_equal "bar", Rails.application.config.my_custom_config[:foo]
+ end
+
+ test "config_for loads custom configuration from yaml accessible as method" do
+ app_file "config/custom.yml", <<-RUBY
+ development:
+ foo: 'bar'
+ RUBY
+
+ add_to_config <<-RUBY
+ config.my_custom_config = config_for('custom')
+ RUBY
+
+ app "development"
+
+ assert_equal "bar", Rails.application.config.my_custom_config.foo
+ end
+
+ test "config_for loads nested custom configuration from yaml as symbol keys" do
+ app_file "config/custom.yml", <<-RUBY
+ development:
+ foo:
+ bar:
+ baz: 1
+ RUBY
+
+ add_to_config <<-RUBY
+ config.my_custom_config = config_for('custom')
+ RUBY
+
+ app "development"
+
+ assert_equal 1, Rails.application.config.my_custom_config.foo[:bar][:baz]
+ end
+
+ test "config_for uses the Pathname object if it is provided" do
+ app_file "config/custom.yml", <<-RUBY
+ development:
+ key: 'custom key'
+ RUBY
+
+ add_to_config <<-RUBY
+ config.my_custom_config = config_for(Pathname.new(Rails.root.join("config/custom.yml")))
+ RUBY
+
+ app "development"
- assert_equal 'custom key', Rails.application.config.my_custom_config['key']
+ assert_equal "custom key", Rails.application.config.my_custom_config["key"]
end
test "config_for raises an exception if the file does not exist" do
@@ -1258,14 +1819,14 @@ module ApplicationTests
RUBY
exception = assert_raises(RuntimeError) do
- require "#{app_path}/config/environment"
+ app "development"
end
assert_equal "Could not load configuration. No such file - #{app_path}/config/custom.yml", exception.message
end
test "config_for without the environment configured returns an empty hash" do
- app_file 'config/custom.yml', <<-RUBY
+ app_file "config/custom.yml", <<-RUBY
test:
key: 'custom key'
RUBY
@@ -1273,25 +1834,71 @@ module ApplicationTests
add_to_config <<-RUBY
config.my_custom_config = config_for('custom')
RUBY
- require "#{app_path}/config/environment"
+
+ app "development"
assert_equal({}, Rails.application.config.my_custom_config)
end
test "config_for with empty file returns an empty hash" do
- app_file 'config/custom.yml', <<-RUBY
+ app_file "config/custom.yml", <<-RUBY
RUBY
add_to_config <<-RUBY
config.my_custom_config = config_for('custom')
RUBY
- require "#{app_path}/config/environment"
+
+ app "development"
assert_equal({}, Rails.application.config.my_custom_config)
end
+ test "default SQLite3Adapter.represent_boolean_as_integer for 5.1 is false" do
+ remove_from_config '.*config\.load_defaults.*\n'
+
+ app_file "app/models/post.rb", <<-RUBY
+ class Post < ActiveRecord::Base
+ end
+ RUBY
+
+ app "development"
+ force_lazy_load_hooks { Post }
+
+ assert_not ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer
+ end
+
+ test "default SQLite3Adapter.represent_boolean_as_integer for new installs is true" do
+ app_file "app/models/post.rb", <<-RUBY
+ class Post < ActiveRecord::Base
+ end
+ RUBY
+
+ app "development"
+ force_lazy_load_hooks { Post }
+
+ assert ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer
+ end
+
+ test "represent_boolean_as_integer should be able to set via config.active_record.sqlite3.represent_boolean_as_integer" do
+ remove_from_config '.*config\.load_defaults.*\n'
+
+ app_file "config/initializers/new_framework_defaults_6_0.rb", <<-RUBY
+ Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true
+ RUBY
+
+ app_file "app/models/post.rb", <<-RUBY
+ class Post < ActiveRecord::Base
+ end
+ RUBY
+
+ app "development"
+ force_lazy_load_hooks { Post }
+
+ assert ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer
+ end
+
test "config_for containing ERB tags should evaluate" do
- app_file 'config/custom.yml', <<-RUBY
+ app_file "config/custom.yml", <<-RUBY
development:
key: <%= 'custom key' %>
RUBY
@@ -1299,13 +1906,14 @@ module ApplicationTests
add_to_config <<-RUBY
config.my_custom_config = config_for('custom')
RUBY
- require "#{app_path}/config/environment"
- assert_equal 'custom key', Rails.application.config.my_custom_config['key']
+ app "development"
+
+ assert_equal "custom key", Rails.application.config.my_custom_config["key"]
end
- test "config_for with syntax error show a more descritive exception" do
- app_file 'config/custom.yml', <<-RUBY
+ test "config_for with syntax error show a more descriptive exception" do
+ app_file "config/custom.yml", <<-RUBY
development:
key: foo:
RUBY
@@ -1315,10 +1923,224 @@ module ApplicationTests
RUBY
exception = assert_raises(RuntimeError) do
- require "#{app_path}/config/environment"
+ app "development"
+ end
+
+ assert_match "YAML syntax error occurred while parsing", exception.message
+ end
+
+ test "config_for allows overriding the environment" do
+ app_file "config/custom.yml", <<-RUBY
+ test:
+ key: 'walrus'
+ production:
+ key: 'unicorn'
+ RUBY
+
+ add_to_config <<-RUBY
+ config.my_custom_config = config_for('custom', env: 'production')
+ RUBY
+ require "#{app_path}/config/environment"
+
+ assert_equal "unicorn", Rails.application.config.my_custom_config["key"]
+ end
+
+ test "api_only is false by default" do
+ app "development"
+ assert_not Rails.application.config.api_only
+ end
+
+ test "api_only generator config is set when api_only is set" do
+ add_to_config <<-RUBY
+ config.api_only = true
+ RUBY
+ app "development"
+
+ Rails.application.load_generators
+ assert Rails.configuration.api_only
+ end
+
+ test "debug_exception_response_format is :api by default if api_only is enabled" do
+ add_to_config <<-RUBY
+ config.api_only = true
+ RUBY
+ app "development"
+
+ assert_equal :api, Rails.configuration.debug_exception_response_format
+ end
+
+ test "debug_exception_response_format can be overridden" do
+ add_to_config <<-RUBY
+ config.api_only = true
+ RUBY
+
+ app_file "config/environments/development.rb", <<-RUBY
+ Rails.application.configure do
+ config.debug_exception_response_format = :default
+ end
+ RUBY
+
+ app "development"
+
+ 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
- assert_match 'YAML syntax error occurred while parsing', exception.message
+ 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
+
+ test "ActiveSupport::MessageEncryptor.use_authenticated_message_encryption is true by default for new apps" do
+ app "development"
+
+ assert_equal true, ActiveSupport::MessageEncryptor.use_authenticated_message_encryption
+ end
+
+ test "ActiveSupport::MessageEncryptor.use_authenticated_message_encryption is false by default for upgraded apps" do
+ remove_from_config '.*config\.load_defaults.*\n'
+
+ app "development"
+
+ assert_equal false, ActiveSupport::MessageEncryptor.use_authenticated_message_encryption
+ end
+
+ test "ActiveSupport::MessageEncryptor.use_authenticated_message_encryption can be configured via config.active_support.use_authenticated_message_encryption" do
+ remove_from_config '.*config\.load_defaults.*\n'
+
+ app_file "config/initializers/new_framework_defaults_6_0.rb", <<-RUBY
+ Rails.application.config.active_support.use_authenticated_message_encryption = true
+ RUBY
+
+ app "development"
+
+ assert_equal true, ActiveSupport::MessageEncryptor.use_authenticated_message_encryption
+ end
+
+ test "ActiveSupport::Digest.hash_digest_class is Digest::SHA1 by default for new apps" do
+ app "development"
+
+ assert_equal Digest::SHA1, ActiveSupport::Digest.hash_digest_class
+ end
+
+ test "ActiveSupport::Digest.hash_digest_class is Digest::MD5 by default for upgraded apps" do
+ remove_from_config '.*config\.load_defaults.*\n'
+
+ app "development"
+
+ assert_equal Digest::MD5, ActiveSupport::Digest.hash_digest_class
+ end
+
+ test "ActiveSupport::Digest.hash_digest_class can be configured via config.active_support.use_sha1_digests" do
+ remove_from_config '.*config\.load_defaults.*\n'
+
+ app_file "config/initializers/new_framework_defaults_6_0.rb", <<-RUBY
+ Rails.application.config.active_support.use_sha1_digests = true
+ RUBY
+
+ app "development"
+
+ assert_equal Digest::SHA1, ActiveSupport::Digest.hash_digest_class
+ end
+
+ test "custom serializers should be able to set via config.active_job.custom_serializers in an initializer" do
+ class ::DummySerializer < ActiveJob::Serializers::ObjectSerializer; end
+
+ app_file "config/initializers/custom_serializers.rb", <<-RUBY
+ Rails.application.config.active_job.custom_serializers << DummySerializer
+ RUBY
+
+ app "development"
+
+ assert_includes ActiveJob::Serializers.serializers, DummySerializer
+ end
+
+ test "ActionView::Helpers::FormTagHelper.default_enforce_utf8 is false by default" do
+ app "development"
+ assert_equal false, ActionView::Helpers::FormTagHelper.default_enforce_utf8
+ end
+
+ test "ActionView::Helpers::FormTagHelper.default_enforce_utf8 is true in an upgraded app" do
+ remove_from_config '.*config\.load_defaults.*\n'
+ add_to_config 'config.load_defaults "5.2"'
+
+ app "development"
+
+ assert_equal true, ActionView::Helpers::FormTagHelper.default_enforce_utf8
+ end
+
+ test "ActionView::Helpers::FormTagHelper.default_enforce_utf8 can be configured via config.action_view.default_enforce_utf8" do
+ remove_from_config '.*config\.load_defaults.*\n'
+
+ app_file "config/initializers/new_framework_defaults_6_0.rb", <<-RUBY
+ Rails.application.config.action_view.default_enforce_utf8 = true
+ RUBY
+
+ app "development"
+
+ assert_equal true, ActionView::Helpers::FormTagHelper.default_enforce_utf8
+ end
+
+ test "ActionView::Template.finalize_compiled_template_methods is true by default" do
+ app "test"
+ assert_equal true, ActionView::Template.finalize_compiled_template_methods
+ end
+
+ test "ActionView::Template.finalize_compiled_template_methods can be configured via config.action_view.finalize_compiled_template_methods" do
+ app_file "config/environments/test.rb", <<-RUBY
+ Rails.application.configure do
+ config.action_view.finalize_compiled_template_methods = false
+ end
+ RUBY
+
+ app "test"
+
+ assert_equal false, ActionView::Template.finalize_compiled_template_methods
+ end
+
+ test "ActiveRecord::Base.filter_attributes should equal to filter_parameters" do
+ app_file "config/initializers/filter_parameters_logging.rb", <<-RUBY
+ Rails.application.config.filter_parameters += [ :password, :credit_card_number ]
+ RUBY
+ app "development"
+ assert_equal [ :password, :credit_card_number ], Rails.application.config.filter_parameters
+ assert_equal [ :password, :credit_card_number ], ActiveRecord::Base.filter_attributes
+ end
+
+ test "ActiveStorage.routes_prefix can be configured via config.active_storage.routes_prefix" do
+ app_file "config/environments/development.rb", <<-RUBY
+ Rails.application.configure do
+ config.active_storage.routes_prefix = '/files'
+ end
+ RUBY
+
+ output = rails("routes", "-g", "active_storage")
+ assert_equal <<~MESSAGE, output
+ Prefix Verb URI Pattern Controller#Action
+ rails_service_blob GET /files/blobs/:signed_id/*filename(.:format) active_storage/blobs#show
+ rails_blob_representation GET /files/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations#show
+ rails_disk_service GET /files/disk/:encoded_key/*filename(.:format) active_storage/disk#show
+ update_rails_disk_service PUT /files/disk/:encoded_token(.:format) active_storage/disk#update
+ rails_direct_uploads POST /files/direct_uploads(.:format) active_storage/direct_uploads#create
+ MESSAGE
+ end
+
+ private
+ def force_lazy_load_hooks
+ yield # Tasty clarifying sugar, homie! We only need to reference a constant to load it.
+ end
end
end
diff --git a/railties/test/application/console_test.rb b/railties/test/application/console_test.rb
index 7bf123d12b..29f8b1e3d9 100644
--- a/railties/test/application/console_test.rb
+++ b/railties/test/application/console_test.rb
@@ -1,11 +1,13 @@
-require 'isolation/abstract_unit'
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "console_helpers"
class ConsoleTest < ActiveSupport::TestCase
include ActiveSupport::Testing::Isolation
def setup
build_app
- boot_rails
end
def teardown
@@ -30,7 +32,7 @@ class ConsoleTest < ActiveSupport::TestCase
end
def test_app_can_access_path_helper_method
- app_file 'config/routes.rb', <<-RUBY
+ app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get 'foo', to: 'foo#index'
end
@@ -38,7 +40,7 @@ class ConsoleTest < ActiveSupport::TestCase
load_environment
console_session = irb_context.app
- assert_equal '/foo', console_session.foo_path
+ assert_equal "/foo", console_session.foo_path
end
def test_new_session_should_return_integration_session
@@ -52,12 +54,11 @@ class ConsoleTest < ActiveSupport::TestCase
a = b = c = nil
# TODO: These should be defined on the initializer
- ActionDispatch::Reloader.to_cleanup { a = b = c = 1 }
- ActionDispatch::Reloader.to_cleanup { b = c = 2 }
- ActionDispatch::Reloader.to_prepare { c = 3 }
+ ActiveSupport::Reloader.to_complete { a = b = c = 1 }
+ ActiveSupport::Reloader.to_complete { b = c = 2 }
+ ActiveSupport::Reloader.to_prepare { c = 3 }
- # Hide Reloading... output
- silence_stream(STDOUT) { irb_context.reload! }
+ irb_context.reload!(false)
assert_equal 1, a
assert_equal 2, b
@@ -72,7 +73,7 @@ class ConsoleTest < ActiveSupport::TestCase
MODEL
load_environment
- assert User.new.respond_to?(:name)
+ assert_respond_to User.new, :name
app_file "app/models/user.rb", <<-MODEL
class User
@@ -80,9 +81,9 @@ class ConsoleTest < ActiveSupport::TestCase
end
MODEL
- assert !User.new.respond_to?(:age)
- silence_stream(STDOUT) { irb_context.reload! }
- assert User.new.respond_to?(:age)
+ assert_not_respond_to User.new, :age
+ irb_context.reload!(false)
+ assert_respond_to User.new, :age
end
def test_access_to_helpers
@@ -90,76 +91,68 @@ class ConsoleTest < ActiveSupport::TestCase
helper = irb_context.helper
assert_not_nil helper
assert_instance_of ActionView::Base, helper
- assert_equal 'Once upon a time in a world...',
- helper.truncate('Once upon a time in a world far far away')
+ assert_equal "Once upon a time in a world...",
+ helper.truncate("Once upon a time in a world far far away")
end
end
-begin
- require "pty"
-rescue LoadError
-end
-
class FullStackConsoleTest < ActiveSupport::TestCase
+ include ConsoleHelpers
+
def setup
- skip "PTY unavailable" unless defined?(PTY) && PTY.respond_to?(:open)
+ skip "PTY unavailable" unless available_pty?
build_app
- app_file 'app/models/post.rb', <<-CODE
+ app_file "app/models/post.rb", <<-CODE
class Post < ActiveRecord::Base
end
CODE
system "#{app_path}/bin/rails runner 'Post.connection.create_table :posts'"
- @master, @slave = PTY.open
+ @primary, @replica = PTY.open
end
def teardown
teardown_app
end
- def assert_output(expected, timeout = 1)
- timeout = Time.now + timeout
-
- output = ""
- until output.include?(expected) || Time.now > timeout
- if IO.select([@master], [], [], 0.1)
- output << @master.read(1)
- end
- end
-
- assert output.include?(expected), "#{expected.inspect} expected, but got:\n\n#{output}"
- end
-
def write_prompt(command, expected_output = nil)
- @master.puts command
- assert_output command
- assert_output expected_output if expected_output
- assert_output "> "
+ @primary.puts command
+ assert_output command, @primary
+ assert_output expected_output, @primary if expected_output
+ assert_output "> ", @primary
end
- def spawn_console
+ def spawn_console(options)
Process.spawn(
- "#{app_path}/bin/rails console --sandbox",
- in: @slave, out: @slave, err: @slave
+ "#{app_path}/bin/rails console #{options}",
+ in: @replica, out: @replica, err: @replica
)
- assert_output "> ", 30
+ assert_output "> ", @primary, 30
end
def test_sandbox
- spawn_console
+ spawn_console("--sandbox")
write_prompt "Post.count", "=> 0"
write_prompt "Post.create"
write_prompt "Post.count", "=> 1"
- @master.puts "quit"
+ @primary.puts "quit"
- spawn_console
+ spawn_console("--sandbox")
write_prompt "Post.count", "=> 0"
write_prompt "Post.transaction { Post.create; raise }"
write_prompt "Post.count", "=> 0"
- @master.puts "quit"
+ @primary.puts "quit"
+ end
+
+ def test_environment_option_and_irb_option
+ spawn_console("test -- --verbose")
+
+ write_prompt "a = 1", "a = 1"
+ write_prompt "puts Rails.env", "puts Rails.env\r\ntest"
+ @primary.puts "quit"
end
end
diff --git a/railties/test/application/content_security_policy_test.rb b/railties/test/application/content_security_policy_test.rb
new file mode 100644
index 0000000000..0d28df16f8
--- /dev/null
+++ b/railties/test/application/content_security_policy_test.rb
@@ -0,0 +1,223 @@
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "rack/test"
+
+module ApplicationTests
+ class ContentSecurityPolicyTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation
+ include Rack::Test::Methods
+
+ def setup
+ build_app
+ end
+
+ def teardown
+ teardown_app
+ end
+
+ test "default content security policy is nil" do
+ controller :pages, <<-RUBY
+ class PagesController < ApplicationController
+ def index
+ render html: "<h1>Welcome to Rails!</h1>"
+ end
+ end
+ RUBY
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ root to: "pages#index"
+ end
+ RUBY
+
+ app("development")
+
+ get "/"
+ assert_nil last_response.headers["Content-Security-Policy"]
+ end
+
+ test "empty content security policy is generated" do
+ controller :pages, <<-RUBY
+ class PagesController < ApplicationController
+ def index
+ render html: "<h1>Welcome to Rails!</h1>"
+ end
+ end
+ RUBY
+
+ app_file "config/initializers/content_security_policy.rb", <<-RUBY
+ Rails.application.config.content_security_policy do |p|
+ end
+ RUBY
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ root to: "pages#index"
+ end
+ RUBY
+
+ app("development")
+
+ get "/"
+ assert_policy ""
+ end
+
+ test "global content security policy in an initializer" do
+ controller :pages, <<-RUBY
+ class PagesController < ApplicationController
+ def index
+ render html: "<h1>Welcome to Rails!</h1>"
+ end
+ end
+ RUBY
+
+ app_file "config/initializers/content_security_policy.rb", <<-RUBY
+ Rails.application.config.content_security_policy do |p|
+ p.default_src :self, :https
+ end
+ RUBY
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ root to: "pages#index"
+ end
+ RUBY
+
+ app("development")
+
+ get "/"
+ assert_policy "default-src 'self' https:"
+ end
+
+ test "global report only content security policy in an initializer" do
+ controller :pages, <<-RUBY
+ class PagesController < ApplicationController
+ def index
+ render html: "<h1>Welcome to Rails!</h1>"
+ end
+ end
+ RUBY
+
+ app_file "config/initializers/content_security_policy.rb", <<-RUBY
+ Rails.application.config.content_security_policy do |p|
+ p.default_src :self, :https
+ end
+
+ Rails.application.config.content_security_policy_report_only = true
+ RUBY
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ root to: "pages#index"
+ end
+ RUBY
+
+ app("development")
+
+ get "/"
+ assert_policy "default-src 'self' https:", report_only: true
+ end
+
+ test "override content security policy in a controller" do
+ controller :pages, <<-RUBY
+ class PagesController < ApplicationController
+ content_security_policy do |p|
+ p.default_src "https://example.com"
+ end
+
+ def index
+ render html: "<h1>Welcome to Rails!</h1>"
+ end
+ end
+ RUBY
+
+ app_file "config/initializers/content_security_policy.rb", <<-RUBY
+ Rails.application.config.content_security_policy do |p|
+ p.default_src :self, :https
+ end
+ RUBY
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ root to: "pages#index"
+ end
+ RUBY
+
+ app("development")
+
+ get "/"
+ assert_policy "default-src https://example.com"
+ end
+
+ test "override content security policy to report only in a controller" do
+ controller :pages, <<-RUBY
+ class PagesController < ApplicationController
+ content_security_policy_report_only
+
+ def index
+ render html: "<h1>Welcome to Rails!</h1>"
+ end
+ end
+ RUBY
+
+ app_file "config/initializers/content_security_policy.rb", <<-RUBY
+ Rails.application.config.content_security_policy do |p|
+ p.default_src :self, :https
+ end
+ RUBY
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ root to: "pages#index"
+ end
+ RUBY
+
+ app("development")
+
+ get "/"
+ assert_policy "default-src 'self' https:", report_only: true
+ end
+
+ test "global content security policy added to rack app" do
+ app_file "config/initializers/content_security_policy.rb", <<-RUBY
+ Rails.application.config.content_security_policy do |p|
+ p.default_src :self, :https
+ end
+ RUBY
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+
+ app = ->(env) {
+ [200, { "Content-Type" => "text/html" }, ["<p>Hello, World!</p>"]]
+ }
+
+ root to: app
+ end
+ RUBY
+
+ app("development")
+
+ get "/"
+ assert_policy "default-src 'self' https:"
+ end
+
+ private
+
+ def assert_policy(expected, report_only: false)
+ assert_equal 200, last_response.status
+
+ if report_only
+ expected_header = "Content-Security-Policy-Report-Only"
+ unexpected_header = "Content-Security-Policy"
+ else
+ expected_header = "Content-Security-Policy"
+ unexpected_header = "Content-Security-Policy-Report-Only"
+ end
+
+ assert_nil last_response.headers[unexpected_header]
+ assert_equal expected, last_response.headers[expected_header]
+ end
+ end
+end
diff --git a/railties/test/application/current_attributes_integration_test.rb b/railties/test/application/current_attributes_integration_test.rb
new file mode 100644
index 0000000000..146e96facc
--- /dev/null
+++ b/railties/test/application/current_attributes_integration_test.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "rack/test"
+
+class CurrentAttributesIntegrationTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation
+ include Rack::Test::Methods
+
+ setup do
+ build_app
+
+ app_file "app/models/current.rb", <<-RUBY
+ class Current < ActiveSupport::CurrentAttributes
+ attribute :customer
+
+ resets { Time.zone = "UTC" }
+
+ def customer=(customer)
+ super
+ Time.zone = customer.try(:time_zone)
+ end
+ end
+ RUBY
+
+ app_file "app/models/customer.rb", <<-RUBY
+ class Customer < Struct.new(:name)
+ def time_zone
+ "Copenhagen"
+ end
+ end
+ RUBY
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ get "/customers/:action", controller: :customers
+ end
+ RUBY
+
+ app_file "app/controllers/customers_controller.rb", <<-RUBY
+ class CustomersController < ApplicationController
+ layout false
+
+ def set_current_customer
+ Current.customer = Customer.new("david")
+ render :index
+ end
+
+ def set_no_customer
+ render :index
+ end
+ end
+ RUBY
+
+ app_file "app/views/customers/index.html.erb", <<-RUBY
+ <%= Current.customer.try(:name) || 'noone' %>,<%= Time.zone.name %>
+ RUBY
+
+ require "#{app_path}/config/environment"
+ end
+
+ teardown :teardown_app
+
+ test "current customer is assigned and cleared" do
+ get "/customers/set_current_customer"
+ assert_equal 200, last_response.status
+ assert_match(/david,Copenhagen/, last_response.body)
+
+ get "/customers/set_no_customer"
+ assert_equal 200, last_response.status
+ assert_match(/noone,UTC/, last_response.body)
+ end
+
+ test "resets after execution" do
+ assert_nil Current.customer
+ assert_equal "UTC", Time.zone.name
+
+ Rails.application.executor.wrap do
+ Current.customer = Customer.new("david")
+
+ assert_equal "david", Current.customer.name
+ assert_equal "Copenhagen", Time.zone.name
+ end
+
+ assert_nil Current.customer
+ assert_equal "UTC", Time.zone.name
+ end
+end
diff --git a/railties/test/application/dbconsole_test.rb b/railties/test/application/dbconsole_test.rb
new file mode 100644
index 0000000000..8c03fe4ac6
--- /dev/null
+++ b/railties/test/application/dbconsole_test.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "console_helpers"
+
+module ApplicationTests
+ class DBConsoleTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation
+ include ConsoleHelpers
+
+ def setup
+ skip "PTY unavailable" unless available_pty?
+
+ build_app
+ end
+
+ def teardown
+ teardown_app
+ end
+
+ def test_use_value_defined_in_environment_file_in_database_yml
+ app_file "config/database.yml", <<-YAML
+ development:
+ database: <%= Rails.application.config.database %>
+ adapter: sqlite3
+ pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
+ timeout: 5000
+ YAML
+
+ app_file "config/environments/development.rb", <<-RUBY
+ Rails.application.configure do
+ config.database = "db/development.sqlite3"
+ end
+ RUBY
+
+ primary, replica = PTY.open
+ spawn_dbconsole(replica)
+ assert_output("sqlite>", primary)
+ ensure
+ primary.puts ".exit"
+ end
+
+ def test_respect_environment_option
+ app_file "config/database.yml", <<-YAML
+ default: &default
+ adapter: sqlite3
+ pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
+ timeout: 5000
+
+ development:
+ <<: *default
+ database: db/development.sqlite3
+
+ production:
+ <<: *default
+ database: db/production.sqlite3
+ YAML
+
+ primary, replica = PTY.open
+ spawn_dbconsole(replica, "-e production")
+ assert_output("sqlite>", primary)
+
+ primary.puts "pragma database_list;"
+ assert_output("production.sqlite3", primary)
+ ensure
+ primary.puts ".exit"
+ end
+
+ private
+ def spawn_dbconsole(fd, options = nil)
+ Process.spawn("#{app_path}/bin/rails dbconsole #{options}", in: fd, out: fd, err: fd)
+ end
+ end
+end
diff --git a/railties/test/application/generators_test.rb b/railties/test/application/generators_test.rb
index 84cc6e120b..e5e557d204 100644
--- a/railties/test/application/generators_test.rb
+++ b/railties/test/application/generators_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "isolation/abstract_unit"
module ApplicationTests
@@ -6,7 +8,6 @@ module ApplicationTests
def setup
build_app
- boot_rails
end
def teardown
@@ -30,7 +31,7 @@ module ApplicationTests
end
test "allow running plugin new generator inside Rails app directory" do
- FileUtils.cd(rails_root){ `ruby bin/rails plugin new vendor/plugins/bukkits` }
+ rails "plugin", "new", "vendor/plugins/bukkits"
assert File.exist?(File.join(rails_root, "vendor/plugins/bukkits/test/dummy/config/application.rb"))
end
@@ -114,7 +115,7 @@ module ApplicationTests
test "generators with string and hash for options should generate symbol keys" do
with_bare_config do |c|
c.generators do |g|
- g.orm 'data_mapper', migration: false
+ g.orm "data_mapper", migration: false
end
expected = {
@@ -135,10 +136,10 @@ module ApplicationTests
require "#{app_path}/config/environment"
Rails.application.load_generators
- assert Rails::Generators.hidden_namespaces.include?("assets")
- assert Rails::Generators.hidden_namespaces.include?("helper")
- assert Rails::Generators.hidden_namespaces.include?("js")
- assert Rails::Generators.hidden_namespaces.include?("css")
+ assert_includes Rails::Generators.hidden_namespaces, "assets"
+ assert_includes Rails::Generators.hidden_namespaces, "helper"
+ assert_includes Rails::Generators.hidden_namespaces, "js"
+ assert_includes Rails::Generators.hidden_namespaces, "css"
assert Rails::Generators.options[:rails][:api]
assert_equal false, Rails::Generators.options[:rails][:assets]
assert_equal false, Rails::Generators.options[:rails][:helper]
@@ -160,5 +161,42 @@ module ApplicationTests
assert Rails::Generators.options[:rails][:helper]
assert_equal :my_template, Rails::Generators.options[:rails][:template_engine]
end
+
+ test "api only generator generate mailer views" do
+ add_to_config <<-RUBY
+ config.api_only = true
+ RUBY
+
+ rails "generate", "mailer", "notifier", "foo"
+ assert File.exist?(File.join(rails_root, "app/views/notifier_mailer/foo.text.erb"))
+ assert File.exist?(File.join(rails_root, "app/views/notifier_mailer/foo.html.erb"))
+ end
+
+ test "ARGV is mutated as expected" do
+ require "#{app_path}/config/environment"
+ require "rails/command"
+ 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 and hidden commands" do
+ FileUtils.cd(rails_root) do
+ output = rails("generate", "--help")
+ assert_no_match "active_record:migration", output
+ assert_no_match "credentials", output
+
+ output = rails("destroy", "--help")
+ assert_no_match "active_record:migration", output
+ 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..f728fc3b85
--- /dev/null
+++ b/railties/test/application/help_test.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+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 = rails("help")
+ assert_match "The most common rails commands are", output
+ end
+
+ test "short-cut alias works" do
+ output = 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 7bba910b9e..1530ea82d6 100644
--- a/railties/test/application/initializers/frameworks_test.rb
+++ b/railties/test/application/initializers/frameworks_test.rb
@@ -1,5 +1,6 @@
+# frozen_string_literal: true
+
require "isolation/abstract_unit"
-require 'set'
module ApplicationTests
class FrameworksTest < ActiveSupport::TestCase
@@ -7,7 +8,6 @@ module ApplicationTests
def setup
build_app
- boot_rails
FileUtils.rm_rf "#{app_path}/config/environments"
end
@@ -17,7 +17,7 @@ module ApplicationTests
# AC & AM
test "set load paths set only if action controller or action mailer are in use" do
- assert_nothing_raised NameError do
+ assert_nothing_raised do
add_to_config <<-RUBY
config.root = "#{app_path}"
RUBY
@@ -50,6 +50,17 @@ module ApplicationTests
assert_equal "test.rails", ActionMailer::Base.default_url_options[:host]
end
+ test "Default to HTTPS for ActionMailer URLs when force_ssl is on" do
+ app_file "config/environments/development.rb", <<-RUBY
+ Rails.application.configure do
+ config.force_ssl = true
+ end
+ RUBY
+
+ require "#{app_path}/config/environment"
+ assert_equal "https", ActionMailer::Base.default_url_options[:protocol]
+ end
+
test "includes url helpers as action methods" do
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
@@ -119,7 +130,7 @@ module ApplicationTests
end
RUBY
- require 'rack/test'
+ require "rack/test"
extend Rack::Test::Methods
get "/foo/included_helpers"
@@ -151,10 +162,10 @@ module ApplicationTests
end
RUBY
- require 'rack/test'
+ require "rack/test"
extend Rack::Test::Methods
- get 'omg/show'
+ get "omg/show"
assert_equal '{"omg":"omg"}', last_response.body
end
@@ -166,7 +177,7 @@ module ApplicationTests
end
test "assignment config.encoding to default_charset" do
- charset = 'Shift_JIS'
+ charset = "Shift_JIS"
add_to_config "config.encoding = '#{charset}'"
require "#{app_path}/config/environment"
assert_equal charset, ActionDispatch::Response.default_charset
@@ -176,7 +187,7 @@ module ApplicationTests
test "if there's no config.active_support.bare, all of ActiveSupport is required" do
use_frameworks []
require "#{app_path}/config/environment"
- assert_nothing_raised { [1,2,3].sample }
+ assert_nothing_raised { [1, 2, 3].sample }
end
test "config.active_support.bare does not require all of ActiveSupport" do
@@ -194,41 +205,35 @@ module ApplicationTests
test "active_record extensions are applied to ActiveRecord" do
add_to_config "config.active_record.table_name_prefix = 'tbl_'"
require "#{app_path}/config/environment"
- assert_equal 'tbl_', ActiveRecord::Base.table_name_prefix
+ assert_equal "tbl_", ActiveRecord::Base.table_name_prefix
end
test "database middleware doesn't initialize when activerecord is not in frameworks" do
use_frameworks []
require "#{app_path}/config/environment"
- assert_nil defined?(ActiveRecord::Base)
+ assert !defined?(ActiveRecord::Base) || ActiveRecord.autoload?(:Base)
end
test "use schema cache dump" do
- Dir.chdir(app_path) do
- `rails generate model post title:string;
- bin/rake db:migrate db:schema:cache:dump`
- end
+ rails %w(generate model post title:string)
+ rails %w(db:migrate db:schema:cache:dump)
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
- Dir.chdir(app_path) do
- `rails generate model post title:string;
- bin/rake db:migrate db:schema:cache:dump db:rollback`
- end
- silence_warnings {
- require "#{app_path}/config/environment"
- assert !ActiveRecord::Base.connection.schema_cache.tables("posts")
- }
+ rails %w(generate model post title:string)
+ rails %w(db:migrate db:schema:cache:dump db:rollback)
+ require "#{app_path}/config/environment"
+ assert_not ActiveRecord::Base.connection.schema_cache.data_sources("posts")
end
test "active record establish_connection uses Rails.env if DATABASE_URL is not set" do
begin
require "#{app_path}/config/environment"
orig_database_url = ENV.delete("DATABASE_URL")
- orig_rails_env, Rails.env = Rails.env, 'development'
+ orig_rails_env, Rails.env = Rails.env, "development"
ActiveRecord::Base.establish_connection
assert ActiveRecord::Base.connection
assert_match(/#{ActiveRecord::Base.configurations[Rails.env]['database']}/, ActiveRecord::Base.connection_config[:database])
@@ -243,7 +248,7 @@ module ApplicationTests
begin
require "#{app_path}/config/environment"
orig_database_url = ENV.delete("DATABASE_URL")
- orig_rails_env, Rails.env = Rails.env, 'development'
+ orig_rails_env, Rails.env = Rails.env, "development"
database_url_db_name = "db/database_url_db.sqlite3"
ENV["DATABASE_URL"] = "sqlite3:#{database_url_db_name}"
ActiveRecord::Base.establish_connection
@@ -255,5 +260,13 @@ module ApplicationTests
Rails.env = orig_rails_env if orig_rails_env
end
end
+
+ test "connections checked out during initialization are returned to the pool" do
+ app_file "config/initializers/active_record.rb", <<-RUBY
+ ActiveRecord::Base.connection
+ RUBY
+ require "#{app_path}/config/environment"
+ assert_not_predicate ActiveRecord::Base.connection_pool, :active_connection?
+ end
end
end
diff --git a/railties/test/application/initializers/hooks_test.rb b/railties/test/application/initializers/hooks_test.rb
index b2cea0a8e1..1e130c2f9e 100644
--- a/railties/test/application/initializers/hooks_test.rb
+++ b/railties/test/application/initializers/hooks_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "isolation/abstract_unit"
module ApplicationTests
@@ -6,7 +8,6 @@ module ApplicationTests
def setup
build_app
- boot_rails
FileUtils.rm_rf "#{app_path}/config/environments"
end
@@ -32,7 +33,7 @@ module ApplicationTests
RUBY
require "#{app_path}/config/environment"
- assert_equal [1,2,3], $initialization_callbacks
+ assert_equal [1, 2, 3], $initialization_callbacks
end
test "hooks block works correctly with eager_load" do
@@ -47,7 +48,7 @@ module ApplicationTests
RUBY
require "#{app_path}/config/environment"
- assert_equal [1,2,3,4], $initialization_callbacks
+ assert_equal [1, 2, 3, 4], $initialization_callbacks
end
test "after_initialize runs after frameworks have been initialized" do
@@ -58,7 +59,7 @@ module ApplicationTests
require "#{app_path}/config/environment"
assert $activerecord_configurations
- assert $activerecord_configurations['development']
+ assert $activerecord_configurations["development"]
end
test "after_initialize happens after to_prepare in development" do
diff --git a/railties/test/application/initializers/i18n_test.rb b/railties/test/application/initializers/i18n_test.rb
index 9ee54796a4..8058052771 100644
--- a/railties/test/application/initializers/i18n_test.rb
+++ b/railties/test/application/initializers/i18n_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "isolation/abstract_unit"
module ApplicationTests
@@ -6,7 +8,6 @@ module ApplicationTests
def setup
build_app
- boot_rails
FileUtils.rm_rf "#{app_path}/config/environments"
require "rails/all"
end
@@ -31,7 +32,7 @@ module ApplicationTests
end
def assert_no_fallbacks
- assert !I18n.backend.class.included_modules.include?(I18n::Backend::Fallbacks)
+ assert_not_includes I18n.backend.class.included_modules, I18n::Backend::Fallbacks
end
# Locales
@@ -63,8 +64,8 @@ module ApplicationTests
"#{app_path}/config/locales/en.yml", "#{app_path}/config/another_locale.yml"
], Rails.application.config.i18n.load_path
- assert I18n.load_path.include?("#{app_path}/config/locales/en.yml")
- assert I18n.load_path.include?("#{app_path}/config/another_locale.yml")
+ assert_includes I18n.load_path, "#{app_path}/config/locales/en.yml"
+ assert_includes I18n.load_path, "#{app_path}/config/another_locale.yml"
end
test "load_path is populated before eager loaded models" do
@@ -77,19 +78,19 @@ en:
foo: "1"
YAML
- app_file 'app/models/foo.rb', <<-RUBY
+ app_file "app/models/foo.rb", <<-RUBY
class Foo < ActiveRecord::Base
@foo = I18n.t(:foo)
end
RUBY
- app_file 'config/routes.rb', <<-RUBY
+ app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get '/i18n', :to => lambda { |env| [200, {}, [Foo.instance_variable_get('@foo')]] }
end
RUBY
- require 'rack/test'
+ require "rack/test"
extend Rack::Test::Methods
load_app
@@ -107,13 +108,13 @@ en:
foo: "1"
YAML
- app_file 'config/routes.rb', <<-RUBY
+ app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get '/i18n', :to => lambda { |env| [200, {}, [I18n.t(:foo)]] }
end
RUBY
- require 'rack/test'
+ require "rack/test"
extend Rack::Test::Methods
load_app
@@ -132,6 +133,79 @@ en:
assert_equal "2", last_response.body
end
+ test "new locale files are loaded" do
+ add_to_config <<-RUBY
+ config.cache_classes = false
+ RUBY
+
+ app_file "config/locales/en.yml", <<-YAML
+en:
+ foo: "1"
+ YAML
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ get '/i18n', :to => lambda { |env| [200, {}, [I18n.t(:foo)]] }
+ end
+ RUBY
+
+ require "rack/test"
+ extend Rack::Test::Methods
+ load_app
+
+ get "/i18n"
+ assert_equal "1", last_response.body
+
+ # Wait a full second so we have time for changes to propagate
+ sleep(1)
+
+ remove_file "config/locales/en.yml"
+ app_file "config/locales/custom.en.yml", <<-YAML
+en:
+ foo: "2"
+ YAML
+
+ get "/i18n"
+ assert_equal "2", last_response.body
+ end
+
+ test "I18n.load_path is reloaded" do
+ add_to_config <<-RUBY
+ config.cache_classes = false
+ RUBY
+
+ app_file "config/locales/en.yml", <<-YAML
+en:
+ foo: "1"
+ YAML
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ get '/i18n', :to => lambda { |env| [200, {}, [I18n.load_path.inspect]] }
+ end
+ RUBY
+
+ require "rack/test"
+ extend Rack::Test::Methods
+ load_app
+
+ get "/i18n"
+
+ assert_match "en.yml", last_response.body
+
+ # Wait a full second so we have time for changes to propagate
+ sleep(1)
+
+ app_file "config/locales/fr.yml", <<-YAML
+fr:
+ foo: "2"
+ YAML
+
+ get "/i18n"
+ assert_match "fr.yml", last_response.body
+ assert_match "en.yml", last_response.body
+ end
+
# Fallbacks
test "not using config.i18n.fallbacks does not initialize I18n.fallbacks" do
I18n.backend = Class.new(I18n::Backend::Simple).new
@@ -142,7 +216,7 @@ en:
test "config.i18n.fallbacks = true initializes I18n.fallbacks with default settings" do
I18n::Railtie.config.i18n.fallbacks = true
load_app
- assert I18n.backend.class.included_modules.include?(I18n::Backend::Fallbacks)
+ assert_includes I18n.backend.class.included_modules, I18n::Backend::Fallbacks
assert_fallbacks de: [:de, :en]
end
@@ -150,7 +224,7 @@ en:
I18n::Railtie.config.i18n.fallbacks = true
I18n::Railtie.config.i18n.backend = Class.new(I18n::Backend::Simple).new
load_app
- assert I18n.backend.class.included_modules.include?(I18n::Backend::Fallbacks)
+ assert_includes I18n.backend.class.included_modules, I18n::Backend::Fallbacks
assert_fallbacks de: [:de, :en]
end
@@ -161,7 +235,7 @@ en:
end
test "config.i18n.fallbacks.map = { :ca => :'es-ES' } initializes fallbacks with a mapping ca => es-ES" do
- I18n::Railtie.config.i18n.fallbacks.map = { :ca => :'es-ES' }
+ I18n::Railtie.config.i18n.fallbacks.map = { ca: :'es-ES' }
load_app
assert_fallbacks ca: [:ca, :"es-ES", :es, :en]
end
@@ -172,18 +246,24 @@ en:
assert_fallbacks de: [:de, :'en-US', :en]
end
- test "[shortcut] config.i18n.fallbacks = [{ :ca => :'es-ES' }] initializes fallbacks with a mapping de-AT => de-DE" do
- I18n::Railtie.config.i18n.fallbacks.map = { :ca => :'es-ES' }
+ test "[shortcut] config.i18n.fallbacks = [{ :ca => :'es-ES' }] initializes fallbacks with a mapping ca => es-ES" do
+ I18n::Railtie.config.i18n.fallbacks = [{ ca: :'es-ES' }]
load_app
assert_fallbacks ca: [:ca, :"es-ES", :es, :en]
end
test "[shortcut] config.i18n.fallbacks = [:'en-US', { :ca => :'es-ES' }] initializes fallbacks with the given arguments" do
- I18n::Railtie.config.i18n.fallbacks = [:'en-US', { :ca => :'es-ES' }]
+ I18n::Railtie.config.i18n.fallbacks = [:'en-US', { ca: :'es-ES' }]
load_app
assert_fallbacks ca: [:ca, :"es-ES", :es, :'en-US', :en]
end
+ test "[shortcut] config.i18n.fallbacks = { ca: :en } initializes fallbacks with a mapping ca => :en" do
+ I18n::Railtie.config.i18n.fallbacks = { ca: :en }
+ load_app
+ assert_fallbacks ca: [:ca, :en]
+ end
+
test "disable config.i18n.enforce_available_locales" do
add_to_config <<-RUBY
config.i18n.enforce_available_locales = false
diff --git a/railties/test/application/initializers/load_path_test.rb b/railties/test/application/initializers/load_path_test.rb
index cd05956356..78cd4776d6 100644
--- a/railties/test/application/initializers/load_path_test.rb
+++ b/railties/test/application/initializers/load_path_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "isolation/abstract_unit"
module ApplicationTests
@@ -6,7 +8,6 @@ module ApplicationTests
def setup
build_app
- boot_rails
FileUtils.rm_rf "#{app_path}/config/environments"
end
@@ -20,7 +21,7 @@ module ApplicationTests
RUBY
require "#{app_path}/config/environment"
- assert $:.include?("#{app_path}/app/models")
+ assert_includes $:, "#{app_path}/app/models"
end
test "initializing an application allows to load code on lib path inside application class definition" do
@@ -37,7 +38,7 @@ module ApplicationTests
require "#{app_path}/config/environment"
end
- assert $:.include?("#{app_path}/lib")
+ assert_includes $:, "#{app_path}/lib"
end
test "initializing an application eager load any path under app" do
diff --git a/railties/test/application/initializers/notifications_test.rb b/railties/test/application/initializers/notifications_test.rb
index 95655b74cf..c65c955734 100644
--- a/railties/test/application/initializers/notifications_test.rb
+++ b/railties/test/application/initializers/notifications_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "isolation/abstract_unit"
module ApplicationTests
@@ -6,7 +8,6 @@ module ApplicationTests
def setup
build_app
- boot_rails
end
def teardown
@@ -31,6 +32,7 @@ module ApplicationTests
logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
ActiveRecord::Base.logger = logger
+ ActiveRecord::Base.verbose_query_logs = false
# Mimic Active Record notifications
instrument "sql.active_record", name: "SQL", sql: "SHOW tables"
@@ -40,17 +42,17 @@ module ApplicationTests
assert_match(/SHOW tables/, logger.logged(:debug).last)
end
- test 'rails load_config_initializer event is instrumented' do
- app_file 'config/initializers/foo.rb', ''
+ test "rails load_config_initializer event is instrumented" do
+ app_file "config/initializers/foo.rb", ""
events = []
callback = ->(*_) { events << _ }
- ActiveSupport::Notifications.subscribed(callback, 'load_config_initializer.railties') do
+ ActiveSupport::Notifications.subscribed(callback, "load_config_initializer.railties") do
app
end
assert_equal %w[load_config_initializer.railties], events.map(&:first)
- assert_includes events.first.last[:initializer], 'config/initializers/foo.rb'
+ assert_includes events.first.last[:initializer], "config/initializers/foo.rb"
end
end
end
diff --git a/railties/test/application/integration_test_case_test.rb b/railties/test/application/integration_test_case_test.rb
new file mode 100644
index 0000000000..c08761092b
--- /dev/null
+++ b/railties/test/application/integration_test_case_test.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "env_helpers"
+
+module ApplicationTests
+ class IntegrationTestCaseTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation, EnvHelpers
+
+ setup do
+ build_app
+ end
+
+ teardown do
+ teardown_app
+ end
+
+ test "resets Action Mailer test deliveries" do
+ rails "generate", "mailer", "BaseMailer", "welcome"
+
+ app_file "test/integration/mailer_integration_test.rb", <<-RUBY
+ require 'test_helper'
+
+ class MailerIntegrationTest < ActionDispatch::IntegrationTest
+ setup do
+ @old_delivery_method = ActionMailer::Base.delivery_method
+ ActionMailer::Base.delivery_method = :test
+ end
+
+ teardown do
+ ActionMailer::Base.delivery_method = @old_delivery_method
+ end
+
+ 2.times do |i|
+ define_method "test_resets_deliveries_\#{i}" do
+ BaseMailer.welcome.deliver_now
+ assert_equal 1, ActionMailer::Base.deliveries.count
+ end
+ end
+ end
+ RUBY
+
+ with_rails_env("test") { rails("db:migrate") }
+ output = rails("test")
+ assert_match(/0 failures, 0 errors/, output)
+ end
+ end
+
+ class IntegrationTestDefaultApp < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation, EnvHelpers
+
+ setup do
+ build_app
+ end
+
+ teardown do
+ teardown_app
+ end
+
+ test "app method of integration tests returns test_app by default" do
+ app_file "test/integration/default_app_test.rb", <<-RUBY
+ require 'test_helper'
+
+ class DefaultAppIntegrationTest < ActionDispatch::IntegrationTest
+ def test_app_returns_action_dispatch_test_app_by_default
+ assert_equal ActionDispatch.test_app, app
+ end
+ end
+ RUBY
+
+ with_rails_env("test") { rails("db:migrate") }
+ output = rails("test")
+ assert_match(/0 failures, 0 errors/, output)
+ end
+ end
+end
diff --git a/railties/test/application/loading_test.rb b/railties/test/application/loading_test.rb
index 1027bca2c1..bfa66770bd 100644
--- a/railties/test/application/loading_test.rb
+++ b/railties/test/application/loading_test.rb
@@ -1,11 +1,12 @@
-require 'isolation/abstract_unit'
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
class LoadingTest < ActiveSupport::TestCase
include ActiveSupport::Testing::Isolation
def setup
build_app
- boot_rails
end
def teardown
@@ -26,11 +27,11 @@ class LoadingTest < ActiveSupport::TestCase
require "#{rails_root}/config/environment"
setup_ar!
- p = Post.create(title: 'omg')
+ p = Post.create(title: "omg")
assert_equal 1, Post.count
- assert_equal 'omg', p.title
+ assert_equal "omg", p.title
p = Post.first
- assert_equal 'omg', p.title
+ assert_equal "omg", p.title
end
test "concerns in app are autoloaded" do
@@ -56,10 +57,10 @@ class LoadingTest < ActiveSupport::TestCase
require "#{rails_root}/config/environment"
- assert_nothing_raised(NameError) { Trackable }
- assert_nothing_raised(NameError) { EmailLoggable }
- assert_nothing_raised(NameError) { Orderable }
- assert_nothing_raised(NameError) { Matchable }
+ assert_nothing_raised { Trackable }
+ assert_nothing_raised { EmailLoggable }
+ assert_nothing_raised { Orderable }
+ assert_nothing_raised { Matchable }
end
test "models without table do not panic on scope definitions when loaded" do
@@ -103,24 +104,24 @@ class LoadingTest < ActiveSupport::TestCase
end
MODEL
- app_file 'config/routes.rb', <<-RUBY
+ app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get '/load', to: lambda { |env| [200, {}, Post.all] }
get '/unload', to: lambda { |env| [200, {}, []] }
end
RUBY
- require 'rack/test'
+ require "rack/test"
extend Rack::Test::Methods
require "#{rails_root}/config/environment"
setup_ar!
- assert_equal [ActiveRecord::SchemaMigration], ActiveRecord::Base.descendants
+ assert_equal [ActiveStorage::Blob, ActiveStorage::Attachment, ActiveRecord::SchemaMigration, ActiveRecord::InternalMetadata].collect(&:to_s).sort, ActiveRecord::Base.descendants.collect(&:to_s).sort
get "/load"
- assert_equal [ActiveRecord::SchemaMigration, Post], ActiveRecord::Base.descendants
+ assert_equal [ActiveStorage::Blob, ActiveStorage::Attachment, ActiveRecord::SchemaMigration, ActiveRecord::InternalMetadata, Post].collect(&:to_s).sort, ActiveRecord::Base.descendants.collect(&:to_s).sort
get "/unload"
- assert_equal [ActiveRecord::SchemaMigration], ActiveRecord::Base.descendants
+ assert_equal [ActiveStorage::Blob, ActiveStorage::Attachment, ActiveRecord::SchemaMigration, ActiveRecord::InternalMetadata].collect(&:to_s).sort, ActiveRecord::Base.descendants.collect(&:to_s).sort
end
test "initialize cant be called twice" do
@@ -133,7 +134,7 @@ class LoadingTest < ActiveSupport::TestCase
config.cache_classes = false
RUBY
- app_file 'config/routes.rb', <<-RUBY
+ app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get '/c', to: lambda { |env| [200, {"Content-Type" => "text/plain"}, [User.counter.to_s]] }
end
@@ -145,7 +146,7 @@ class LoadingTest < ActiveSupport::TestCase
end
MODEL
- require 'rack/test'
+ require "rack/test"
extend Rack::Test::Methods
require "#{rails_root}/config/environment"
@@ -169,10 +170,12 @@ class LoadingTest < ActiveSupport::TestCase
config.file_watcher = Class.new do
def initialize(*); end
def updated?; false; end
+ def execute; end
+ def execute_if_updated; false; end
end
RUBY
- app_file 'config/routes.rb', <<-RUBY
+ app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get '/c', to: lambda { |env| [200, {"Content-Type" => "text/plain"}, [User.counter.to_s]] }
end
@@ -184,7 +187,7 @@ class LoadingTest < ActiveSupport::TestCase
end
MODEL
- require 'rack/test'
+ require "rack/test"
extend Rack::Test::Methods
require "#{rails_root}/config/environment"
@@ -207,7 +210,7 @@ class LoadingTest < ActiveSupport::TestCase
config.cache_classes = false
RUBY
- app_file 'config/routes.rb', <<-RUBY
+ app_file "config/routes.rb", <<-RUBY
$counter ||= 0
Rails.application.routes.draw do
get '/c', to: lambda { |env| User.name; [200, {"Content-Type" => "text/plain"}, [$counter.to_s]] }
@@ -220,7 +223,7 @@ class LoadingTest < ActiveSupport::TestCase
end
MODEL
- require 'rack/test'
+ require "rack/test"
extend Rack::Test::Methods
require "#{rails_root}/config/environment"
@@ -239,7 +242,7 @@ class LoadingTest < ActiveSupport::TestCase
config.cache_classes = false
RUBY
- app_file 'config/routes.rb', <<-RUBY
+ app_file "config/routes.rb", <<-RUBY
$counter ||= 1
$counter *= 2
Rails.application.routes.draw do
@@ -253,7 +256,7 @@ class LoadingTest < ActiveSupport::TestCase
end
MODEL
- require 'rack/test'
+ require "rack/test"
extend Rack::Test::Methods
require "#{rails_root}/config/environment"
@@ -272,7 +275,7 @@ class LoadingTest < ActiveSupport::TestCase
config.cache_classes = false
RUBY
- app_file 'config/routes.rb', <<-RUBY
+ app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get '/title', to: lambda { |env| [200, {"Content-Type" => "text/plain"}, [Post.new.title]] }
get '/body', to: lambda { |env| [200, {"Content-Type" => "text/plain"}, [Post.new.body]] }
@@ -284,11 +287,11 @@ class LoadingTest < ActiveSupport::TestCase
end
MODEL
- require 'rack/test'
+ require "rack/test"
extend Rack::Test::Methods
app_file "db/migrate/1_create_posts.rb", <<-MIGRATION
- class CreatePosts < ActiveRecord::Migration
+ class CreatePosts < ActiveRecord::Migration::Current
def change
create_table :posts do |t|
t.string :title, default: "TITLE"
@@ -297,21 +300,21 @@ class LoadingTest < ActiveSupport::TestCase
end
MIGRATION
- Dir.chdir(app_path) { `rake db:migrate`}
+ rails("db:migrate")
require "#{rails_root}/config/environment"
get "/title"
assert_equal "TITLE", last_response.body
app_file "db/migrate/2_add_body_to_posts.rb", <<-MIGRATION
- class AddBodyToPosts < ActiveRecord::Migration
+ class AddBodyToPosts < ActiveRecord::Migration::Current
def change
add_column :posts, :body, :text, default: "BODY"
end
end
MIGRATION
- Dir.chdir(app_path) { `rake db:migrate` }
+ rails("db:migrate")
get "/body"
assert_equal "BODY", last_response.body
@@ -339,32 +342,118 @@ class LoadingTest < ActiveSupport::TestCase
require "#{rails_root}/config/environment"
- require 'rack/test'
+ require "rack/test"
extend Rack::Test::Methods
- get '/omg/show'
- assert_equal 'OK', last_response.body
+ get "/omg/show"
+ assert_equal "OK", last_response.body
end
def test_initialize_can_be_called_at_any_time
require "#{app_path}/config/application"
- assert !Rails.initialized?
- assert !Rails.application.initialized?
+ assert_not_predicate Rails, :initialized?
+ assert_not_predicate Rails.application, :initialized?
Rails.initialize!
- assert Rails.initialized?
- assert Rails.application.initialized?
+ assert_predicate Rails, :initialized?
+ assert_predicate Rails.application, :initialized?
end
- protected
-
- def setup_ar!
- ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
- ActiveRecord::Migration.verbose = false
- ActiveRecord::Schema.define(version: 1) do
- create_table :posts do |t|
- t.string :title
+ test "frameworks aren't loaded during initialization" do
+ app_file "config/initializers/raise_when_frameworks_load.rb", <<-RUBY
+ %i(action_controller action_mailer active_job active_record).each do |framework|
+ ActiveSupport.on_load(framework) { raise "\#{framework} loaded!" }
end
+ RUBY
+
+ assert_nothing_raised do
+ require "#{app_path}/config/environment"
end
end
+
+ test "active record query cache hooks are installed before first request in production" do
+ app_file "app/controllers/omg_controller.rb", <<-RUBY
+ begin
+ class OmgController < ActionController::Metal
+ ActiveSupport.run_load_hooks(:action_controller, self)
+ def show
+ if ActiveRecord::Base.connection.query_cache_enabled
+ self.response_body = ["Query cache is enabled."]
+ else
+ self.response_body = ["Expected ActiveRecord::Base.connection.query_cache_enabled to be true"]
+ end
+ end
+ end
+ rescue => e
+ puts "Error loading metal: \#{e.class} \#{e.message}"
+ end
+ RUBY
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ get "/:controller(/:action)"
+ end
+ RUBY
+
+ boot_app "production"
+
+ require "rack/test"
+ extend Rack::Test::Methods
+
+ get "/omg/show"
+ assert_equal "Query cache is enabled.", last_response.body
+ end
+
+ test "active record query cache hooks are installed before first request in development" do
+ app_file "app/controllers/omg_controller.rb", <<-RUBY
+ begin
+ class OmgController < ActionController::Metal
+ ActiveSupport.run_load_hooks(:action_controller, self)
+ def show
+ if ActiveRecord::Base.connection.query_cache_enabled
+ self.response_body = ["Query cache is enabled."]
+ else
+ self.response_body = ["Expected ActiveRecord::Base.connection.query_cache_enabled to be true"]
+ end
+ end
+ end
+ rescue => e
+ puts "Error loading metal: \#{e.class} \#{e.message}"
+ end
+ RUBY
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ get "/:controller(/:action)"
+ end
+ RUBY
+
+ boot_app "development"
+
+ require "rack/test"
+ extend Rack::Test::Methods
+
+ get "/omg/show"
+ assert_equal "Query cache is enabled.", last_response.body
+ end
+
+ private
+
+ def setup_ar!
+ ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
+ ActiveRecord::Migration.verbose = false
+ ActiveRecord::Schema.define(version: 1) do
+ create_table :posts do |t|
+ t.string :title
+ end
+ end
+ end
+
+ def boot_app(env = "development")
+ ENV["RAILS_ENV"] = env
+
+ require "#{app_path}/config/environment"
+ ensure
+ ENV.delete "RAILS_ENV"
+ end
end
diff --git a/railties/test/application/mailer_previews_test.rb b/railties/test/application/mailer_previews_test.rb
index e462d2c15e..ba186bda44 100644
--- a/railties/test/application/mailer_previews_test.rb
+++ b/railties/test/application/mailer_previews_test.rb
@@ -1,6 +1,8 @@
-require 'isolation/abstract_unit'
-require 'rack/test'
-require 'base64'
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "rack/test"
+require "base64"
module ApplicationTests
class MailerPreviewsTest < ActiveSupport::TestCase
@@ -9,7 +11,6 @@ module ApplicationTests
def setup
build_app
- boot_rails
end
def teardown
@@ -28,10 +29,10 @@ module ApplicationTests
assert_equal 404, last_response.status
end
- test "/rails/mailers is accessible with correct configuraiton" do
+ test "/rails/mailers is accessible with correct configuration" do
add_to_config "config.action_mailer.show_previews = true"
app("production")
- get "/rails/mailers"
+ get "/rails/mailers", {}, { "REMOTE_ADDR" => "4.2.42.42" }
assert_equal 200, last_response.status
end
@@ -54,7 +55,7 @@ module ApplicationTests
end
test "mailer previews are loaded from the default preview_path" do
- mailer 'notifier', <<-RUBY
+ mailer "notifier", <<-RUBY
class Notifier < ActionMailer::Base
default from: "from@example.com"
@@ -64,11 +65,11 @@ module ApplicationTests
end
RUBY
- text_template 'notifier/foo', <<-RUBY
+ text_template "notifier/foo", <<-RUBY
Hello, World!
RUBY
- mailer_preview 'notifier', <<-RUBY
+ mailer_preview "notifier", <<-RUBY
class NotifierPreview < ActionMailer::Preview
def foo
Notifier.foo
@@ -76,7 +77,7 @@ module ApplicationTests
end
RUBY
- app('development')
+ app("development")
get "/rails/mailers"
assert_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body
@@ -86,7 +87,7 @@ module ApplicationTests
test "mailer previews are loaded from a custom preview_path" do
add_to_config "config.action_mailer.preview_path = '#{app_path}/lib/mailer_previews'"
- mailer 'notifier', <<-RUBY
+ mailer "notifier", <<-RUBY
class Notifier < ActionMailer::Base
default from: "from@example.com"
@@ -96,11 +97,11 @@ module ApplicationTests
end
RUBY
- text_template 'notifier/foo', <<-RUBY
+ text_template "notifier/foo", <<-RUBY
Hello, World!
RUBY
- app_file 'lib/mailer_previews/notifier_preview.rb', <<-RUBY
+ app_file "lib/mailer_previews/notifier_preview.rb", <<-RUBY
class NotifierPreview < ActionMailer::Preview
def foo
Notifier.foo
@@ -108,7 +109,7 @@ module ApplicationTests
end
RUBY
- app('development')
+ app("development")
get "/rails/mailers"
assert_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body
@@ -116,12 +117,12 @@ module ApplicationTests
end
test "mailer previews are reloaded across requests" do
- app('development')
+ app("development")
get "/rails/mailers"
assert_no_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body
- mailer 'notifier', <<-RUBY
+ mailer "notifier", <<-RUBY
class Notifier < ActionMailer::Base
default from: "from@example.com"
@@ -131,11 +132,11 @@ module ApplicationTests
end
RUBY
- text_template 'notifier/foo', <<-RUBY
+ text_template "notifier/foo", <<-RUBY
Hello, World!
RUBY
- mailer_preview 'notifier', <<-RUBY
+ mailer_preview "notifier", <<-RUBY
class NotifierPreview < ActionMailer::Preview
def foo
Notifier.foo
@@ -146,7 +147,7 @@ module ApplicationTests
get "/rails/mailers"
assert_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body
- remove_file 'test/mailers/previews/notifier_preview.rb'
+ remove_file "test/mailers/previews/notifier_preview.rb"
sleep(1)
get "/rails/mailers"
@@ -154,7 +155,7 @@ module ApplicationTests
end
test "mailer preview actions are added and removed" do
- mailer 'notifier', <<-RUBY
+ mailer "notifier", <<-RUBY
class Notifier < ActionMailer::Base
default from: "from@example.com"
@@ -164,11 +165,11 @@ module ApplicationTests
end
RUBY
- text_template 'notifier/foo', <<-RUBY
+ text_template "notifier/foo", <<-RUBY
Hello, World!
RUBY
- mailer_preview 'notifier', <<-RUBY
+ mailer_preview "notifier", <<-RUBY
class NotifierPreview < ActionMailer::Preview
def foo
Notifier.foo
@@ -176,14 +177,14 @@ module ApplicationTests
end
RUBY
- app('development')
+ app("development")
get "/rails/mailers"
assert_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body
assert_match '<li><a href="/rails/mailers/notifier/foo">foo</a></li>', last_response.body
assert_no_match '<li><a href="/rails/mailers/notifier/bar">bar</a></li>', last_response.body
- mailer 'notifier', <<-RUBY
+ mailer "notifier", <<-RUBY
class Notifier < ActionMailer::Base
default from: "from@example.com"
@@ -197,15 +198,15 @@ module ApplicationTests
end
RUBY
- text_template 'notifier/foo', <<-RUBY
+ text_template "notifier/foo", <<-RUBY
Hello, World!
RUBY
- text_template 'notifier/bar', <<-RUBY
+ text_template "notifier/bar", <<-RUBY
Goodbye, World!
RUBY
- mailer_preview 'notifier', <<-RUBY
+ mailer_preview "notifier", <<-RUBY
class NotifierPreview < ActionMailer::Preview
def foo
Notifier.foo
@@ -224,7 +225,7 @@ module ApplicationTests
assert_match '<li><a href="/rails/mailers/notifier/foo">foo</a></li>', last_response.body
assert_match '<li><a href="/rails/mailers/notifier/bar">bar</a></li>', last_response.body
- mailer 'notifier', <<-RUBY
+ mailer "notifier", <<-RUBY
class Notifier < ActionMailer::Base
default from: "from@example.com"
@@ -234,9 +235,9 @@ module ApplicationTests
end
RUBY
- remove_file 'app/views/notifier/bar.text.erb'
+ remove_file "app/views/notifier/bar.text.erb"
- mailer_preview 'notifier', <<-RUBY
+ mailer_preview "notifier", <<-RUBY
class NotifierPreview < ActionMailer::Preview
def foo
Notifier.foo
@@ -255,12 +256,12 @@ module ApplicationTests
test "mailer previews are reloaded from a custom preview_path" do
add_to_config "config.action_mailer.preview_path = '#{app_path}/lib/mailer_previews'"
- app('development')
+ app("development")
get "/rails/mailers"
assert_no_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body
- mailer 'notifier', <<-RUBY
+ mailer "notifier", <<-RUBY
class Notifier < ActionMailer::Base
default from: "from@example.com"
@@ -270,11 +271,11 @@ module ApplicationTests
end
RUBY
- text_template 'notifier/foo', <<-RUBY
+ text_template "notifier/foo", <<-RUBY
Hello, World!
RUBY
- app_file 'lib/mailer_previews/notifier_preview.rb', <<-RUBY
+ app_file "lib/mailer_previews/notifier_preview.rb", <<-RUBY
class NotifierPreview < ActionMailer::Preview
def foo
Notifier.foo
@@ -285,7 +286,7 @@ module ApplicationTests
get "/rails/mailers"
assert_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body
- remove_file 'lib/mailer_previews/notifier_preview.rb'
+ remove_file "lib/mailer_previews/notifier_preview.rb"
sleep(1)
get "/rails/mailers"
@@ -293,14 +294,14 @@ module ApplicationTests
end
test "mailer preview not found" do
- app('development')
+ app("development")
get "/rails/mailers/notifier"
- assert last_response.not_found?
+ assert_predicate last_response, :not_found?
assert_match "Mailer preview &#39;notifier&#39; not found", last_response.body
end
test "mailer preview email not found" do
- mailer 'notifier', <<-RUBY
+ mailer "notifier", <<-RUBY
class Notifier < ActionMailer::Base
default from: "from@example.com"
@@ -310,11 +311,11 @@ module ApplicationTests
end
RUBY
- text_template 'notifier/foo', <<-RUBY
+ text_template "notifier/foo", <<-RUBY
Hello, World!
RUBY
- mailer_preview 'notifier', <<-RUBY
+ mailer_preview "notifier", <<-RUBY
class NotifierPreview < ActionMailer::Preview
def foo
Notifier.foo
@@ -322,15 +323,15 @@ module ApplicationTests
end
RUBY
- app('development')
+ app("development")
get "/rails/mailers/notifier/bar"
- assert last_response.not_found?
+ assert_predicate last_response, :not_found?
assert_match "Email &#39;bar&#39; not found in NotifierPreview", last_response.body
end
test "mailer preview NullMail" do
- mailer 'notifier', <<-RUBY
+ mailer "notifier", <<-RUBY
class Notifier < ActionMailer::Base
default from: "from@example.com"
@@ -340,7 +341,7 @@ module ApplicationTests
end
RUBY
- mailer_preview 'notifier', <<-RUBY
+ mailer_preview "notifier", <<-RUBY
class NotifierPreview < ActionMailer::Preview
def foo
Notifier.foo
@@ -348,7 +349,7 @@ module ApplicationTests
end
RUBY
- app('development')
+ app("development")
get "/rails/mailers/notifier/foo"
assert_match "You are trying to preview an email that does not have any content.", last_response.body
@@ -356,7 +357,7 @@ module ApplicationTests
end
test "mailer preview email part not found" do
- mailer 'notifier', <<-RUBY
+ mailer "notifier", <<-RUBY
class Notifier < ActionMailer::Base
default from: "from@example.com"
@@ -366,11 +367,11 @@ module ApplicationTests
end
RUBY
- text_template 'notifier/foo', <<-RUBY
+ text_template "notifier/foo", <<-RUBY
Hello, World!
RUBY
- mailer_preview 'notifier', <<-RUBY
+ mailer_preview "notifier", <<-RUBY
class NotifierPreview < ActionMailer::Preview
def foo
Notifier.foo
@@ -378,15 +379,15 @@ module ApplicationTests
end
RUBY
- app('development')
+ app("development")
get "/rails/mailers/notifier/foo?part=text%2Fhtml"
- assert last_response.not_found?
+ assert_predicate last_response, :not_found?
assert_match "Email part &#39;text/html&#39; not found in NotifierPreview#foo", last_response.body
end
test "message header uses full display names" do
- mailer 'notifier', <<-RUBY
+ mailer "notifier", <<-RUBY
class Notifier < ActionMailer::Base
default from: "Ruby on Rails <core@rubyonrails.org>"
@@ -397,11 +398,11 @@ module ApplicationTests
end
RUBY
- text_template 'notifier/foo', <<-RUBY
+ text_template "notifier/foo", <<-RUBY
Hello, World!
RUBY
- mailer_preview 'notifier', <<-RUBY
+ mailer_preview "notifier", <<-RUBY
class NotifierPreview < ActionMailer::Preview
def foo
Notifier.foo
@@ -409,7 +410,7 @@ module ApplicationTests
end
RUBY
- app('development')
+ app("development")
get "/rails/mailers/notifier/foo"
assert_equal 200, last_response.status
@@ -419,7 +420,51 @@ module ApplicationTests
end
test "part menu selects correct option" do
- mailer 'notifier', <<-RUBY
+ mailer "notifier", <<-RUBY
+ class Notifier < ActionMailer::Base
+ default from: "from@example.com"
+
+ def foo
+ mail to: "to@example.org"
+ end
+ end
+ RUBY
+
+ html_template "notifier/foo", <<-RUBY
+ <p>Hello, World!</p>
+ RUBY
+
+ text_template "notifier/foo", <<-RUBY
+ Hello, World!
+ RUBY
+
+ mailer_preview "notifier", <<-RUBY
+ class NotifierPreview < ActionMailer::Preview
+ def foo
+ Notifier.foo
+ end
+ end
+ RUBY
+
+ app("development")
+
+ get "/rails/mailers/notifier/foo.html"
+ assert_equal 200, last_response.status
+ assert_match '<option selected value="part=text%2Fhtml">View as HTML email</option>', last_response.body
+
+ get "/rails/mailers/notifier/foo.txt"
+ assert_equal 200, last_response.status
+ assert_match '<option selected value="part=text%2Fplain">View as plain-text email</option>', last_response.body
+ end
+
+ test "locale menu selects correct option" do
+ app_file "config/initializers/available_locales.rb", <<-RUBY
+ Rails.application.configure do
+ config.i18n.available_locales = %i[en ja]
+ end
+ RUBY
+
+ mailer "notifier", <<-RUBY
class Notifier < ActionMailer::Base
default from: "from@example.com"
@@ -429,15 +474,15 @@ module ApplicationTests
end
RUBY
- html_template 'notifier/foo', <<-RUBY
+ html_template "notifier/foo", <<-RUBY
<p>Hello, World!</p>
RUBY
- text_template 'notifier/foo', <<-RUBY
+ text_template "notifier/foo", <<-RUBY
Hello, World!
RUBY
- mailer_preview 'notifier', <<-RUBY
+ mailer_preview "notifier", <<-RUBY
class NotifierPreview < ActionMailer::Preview
def foo
Notifier.foo
@@ -445,19 +490,31 @@ module ApplicationTests
end
RUBY
- app('development')
+ app("development")
get "/rails/mailers/notifier/foo.html"
assert_equal 200, last_response.status
- assert_match '<option selected value="?part=text%2Fhtml">View as HTML email</option>', last_response.body
+ assert_match '<option selected value="locale=en">en', last_response.body
+ assert_match '<option value="locale=ja">ja', last_response.body
+
+ get "/rails/mailers/notifier/foo.html?locale=ja"
+ assert_equal 200, last_response.status
+ assert_match '<option value="locale=en">en', last_response.body
+ assert_match '<option selected value="locale=ja">ja', last_response.body
get "/rails/mailers/notifier/foo.txt"
assert_equal 200, last_response.status
- assert_match '<option selected value="?part=text%2Fplain">View as plain-text email</option>', last_response.body
+ assert_match '<option selected value="locale=en">en', last_response.body
+ assert_match '<option value="locale=ja">ja', last_response.body
+
+ get "/rails/mailers/notifier/foo.txt?locale=ja"
+ assert_equal 200, last_response.status
+ assert_match '<option value="locale=en">en', last_response.body
+ assert_match '<option selected value="locale=ja">ja', last_response.body
end
test "mailer previews create correct links when loaded on a subdirectory" do
- mailer 'notifier', <<-RUBY
+ mailer "notifier", <<-RUBY
class Notifier < ActionMailer::Base
default from: "from@example.com"
@@ -467,11 +524,11 @@ module ApplicationTests
end
RUBY
- text_template 'notifier/foo', <<-RUBY
+ text_template "notifier/foo", <<-RUBY
Hello, World!
RUBY
- mailer_preview 'notifier', <<-RUBY
+ mailer_preview "notifier", <<-RUBY
class NotifierPreview < ActionMailer::Preview
def foo
Notifier.foo
@@ -479,17 +536,68 @@ module ApplicationTests
end
RUBY
- app('development')
+ app("development")
- get "/rails/mailers", {}, 'SCRIPT_NAME' => '/my_app'
+ get "/rails/mailers", {}, { "SCRIPT_NAME" => "/my_app" }
assert_match '<h3><a href="/my_app/rails/mailers/notifier">Notifier</a></h3>', last_response.body
assert_match '<li><a href="/my_app/rails/mailers/notifier/foo">foo</a></li>', last_response.body
end
+ test "mailer preview receives query params" do
+ mailer "notifier", <<-RUBY
+ class Notifier < ActionMailer::Base
+ default from: "from@example.com"
+
+ def foo(name)
+ @name = name
+ mail to: "to@example.org"
+ end
+ end
+ RUBY
+
+ html_template "notifier/foo", <<-RUBY
+ <p>Hello, <%= @name %>!</p>
+ RUBY
+
+ text_template "notifier/foo", <<-RUBY
+ Hello, <%= @name %>!
+ RUBY
+
+ mailer_preview "notifier", <<-RUBY
+ class NotifierPreview < ActionMailer::Preview
+ def foo
+ Notifier.foo(params[:name] || "World")
+ end
+ end
+ RUBY
+
+ app("development")
+
+ get "/rails/mailers/notifier/foo.txt"
+ assert_equal 200, last_response.status
+ assert_match '<iframe seamless name="messageBody" src="?part=text%2Fplain">', last_response.body
+ assert_match '<option selected value="part=text%2Fplain">', last_response.body
+ assert_match '<option value="part=text%2Fhtml">', last_response.body
+
+ get "/rails/mailers/notifier/foo?part=text%2Fplain"
+ assert_equal 200, last_response.status
+ assert_match %r[Hello, World!], last_response.body
+
+ get "/rails/mailers/notifier/foo.html?name=Ruby"
+ assert_equal 200, last_response.status
+ assert_match '<iframe seamless name="messageBody" src="?name=Ruby&amp;part=text%2Fhtml">', last_response.body
+ assert_match '<option selected value="name=Ruby&amp;part=text%2Fhtml">', last_response.body
+ assert_match '<option value="name=Ruby&amp;part=text%2Fplain">', last_response.body
+
+ get "/rails/mailers/notifier/foo?name=Ruby&part=text%2Fhtml"
+ assert_equal 200, last_response.status
+ assert_match %r[<p>Hello, Ruby!</p>], last_response.body
+ end
+
test "plain text mailer preview with attachment" do
image_file "pixel.png", "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEWzIioca/JlAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJgggo="
- mailer 'notifier', <<-RUBY
+ mailer "notifier", <<-RUBY
class Notifier < ActionMailer::Base
default from: "from@example.com"
@@ -500,11 +608,11 @@ module ApplicationTests
end
RUBY
- text_template 'notifier/foo', <<-RUBY
+ text_template "notifier/foo", <<-RUBY
Hello, World!
RUBY
- mailer_preview 'notifier', <<-RUBY
+ mailer_preview "notifier", <<-RUBY
class NotifierPreview < ActionMailer::Preview
def foo
Notifier.foo
@@ -512,7 +620,7 @@ module ApplicationTests
end
RUBY
- app('development')
+ app("development")
get "/rails/mailers/notifier/foo"
assert_equal 200, last_response.status
@@ -526,7 +634,7 @@ module ApplicationTests
test "multipart mailer preview with attachment" do
image_file "pixel.png", "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEWzIioca/JlAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJgggo="
- mailer 'notifier', <<-RUBY
+ mailer "notifier", <<-RUBY
class Notifier < ActionMailer::Base
default from: "from@example.com"
@@ -537,15 +645,15 @@ module ApplicationTests
end
RUBY
- text_template 'notifier/foo', <<-RUBY
+ text_template "notifier/foo", <<-RUBY
Hello, World!
RUBY
- html_template 'notifier/foo', <<-RUBY
+ html_template "notifier/foo", <<-RUBY
<p>Hello, World!</p>
RUBY
- mailer_preview 'notifier', <<-RUBY
+ mailer_preview "notifier", <<-RUBY
class NotifierPreview < ActionMailer::Preview
def foo
Notifier.foo
@@ -553,7 +661,7 @@ module ApplicationTests
end
RUBY
- app('development')
+ app("development")
get "/rails/mailers/notifier/foo"
assert_equal 200, last_response.status
@@ -571,7 +679,7 @@ module ApplicationTests
test "multipart mailer preview with inline attachment" do
image_file "pixel.png", "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEWzIioca/JlAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJgggo="
- mailer 'notifier', <<-RUBY
+ mailer "notifier", <<-RUBY
class Notifier < ActionMailer::Base
default from: "from@example.com"
@@ -582,16 +690,16 @@ module ApplicationTests
end
RUBY
- text_template 'notifier/foo', <<-RUBY
+ text_template "notifier/foo", <<-RUBY
Hello, World!
RUBY
- html_template 'notifier/foo', <<-RUBY
+ html_template "notifier/foo", <<-RUBY
<p>Hello, World!</p>
<%= image_tag attachments['pixel.png'].url %>
RUBY
- mailer_preview 'notifier', <<-RUBY
+ mailer_preview "notifier", <<-RUBY
class NotifierPreview < ActionMailer::Preview
def foo
Notifier.foo
@@ -599,7 +707,7 @@ module ApplicationTests
end
RUBY
- app('development')
+ app("development")
get "/rails/mailers/notifier/foo"
assert_equal 200, last_response.status
@@ -616,7 +724,7 @@ module ApplicationTests
end
test "multipart mailer preview with attached email" do
- mailer 'notifier', <<-RUBY
+ mailer "notifier", <<-RUBY
class Notifier < ActionMailer::Base
default from: "from@example.com"
@@ -641,15 +749,15 @@ module ApplicationTests
end
RUBY
- text_template 'notifier/foo', <<-RUBY
+ text_template "notifier/foo", <<-RUBY
Hello, World!
RUBY
- html_template 'notifier/foo', <<-RUBY
+ html_template "notifier/foo", <<-RUBY
<p>Hello, World!</p>
RUBY
- mailer_preview 'notifier', <<-RUBY
+ mailer_preview "notifier", <<-RUBY
class NotifierPreview < ActionMailer::Preview
def foo
Notifier.foo
@@ -657,7 +765,7 @@ module ApplicationTests
end
RUBY
- app('development')
+ app("development")
get "/rails/mailers/notifier/foo"
assert_equal 200, last_response.status
@@ -672,6 +780,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
@@ -695,7 +837,7 @@ module ApplicationTests
end
def image_file(name, contents)
- app_file("public/images/#{name}", Base64.strict_decode64(contents), 'wb')
+ app_file("public/images/#{name}", Base64.strict_decode64(contents), "wb")
end
end
end
diff --git a/railties/test/application/middleware/cache_test.rb b/railties/test/application/middleware/cache_test.rb
index c951dabd6c..3768d8ce2d 100644
--- a/railties/test/application/middleware/cache_test.rb
+++ b/railties/test/application/middleware/cache_test.rb
@@ -1,4 +1,6 @@
-require 'isolation/abstract_unit'
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
module ApplicationTests
class CacheTest < ActiveSupport::TestCase
@@ -6,8 +8,7 @@ module ApplicationTests
def setup
build_app
- boot_rails
- require 'rack/test'
+ require "rack/test"
extend Rack::Test::Methods
end
@@ -20,7 +21,7 @@ module ApplicationTests
class ExpiresController < ApplicationController
def expires_header
expires_in 10, public: !params[:private]
- render text: SecureRandom.hex(16)
+ render plain: SecureRandom.hex(16)
end
def expires_etag
@@ -33,18 +34,18 @@ module ApplicationTests
end
def keeps_if_modified_since
- render :text => request.headers['If-Modified-Since']
+ render plain: request.headers['If-Modified-Since']
end
private
def render_conditionally(headers)
if stale?(headers.merge(public: !params[:private]))
- render text: SecureRandom.hex(16)
+ render plain: SecureRandom.hex(16)
end
end
end
RUBY
- app_file 'config/routes.rb', <<-RUBY
+ app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get ':controller(/:action)'
end
@@ -55,7 +56,7 @@ module ApplicationTests
simple_controller
expected = "Wed, 30 May 1984 19:43:31 GMT"
- get "/expires/keeps_if_modified_since", {}, "HTTP_IF_MODIFIED_SINCE" => expected
+ get "/expires/keeps_if_modified_since", {}, { "HTTP_IF_MODIFIED_SINCE" => expected }
assert_equal 200, last_response.status
assert_equal expected, last_response.body, "cache should have kept If-Modified-Since"
@@ -66,12 +67,12 @@ module ApplicationTests
app("development")
get "/expires/expires_header"
- assert_nil last_response.headers['X-Rack-Cache']
+ assert_nil last_response.headers["X-Rack-Cache"]
body = last_response.body
get "/expires/expires_header"
- assert_nil last_response.headers['X-Rack-Cache']
+ assert_nil last_response.headers["X-Rack-Cache"]
assert_not_equal body, last_response.body
end
@@ -118,12 +119,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
@@ -138,8 +139,8 @@ module ApplicationTests
body = last_response.body
etag = last_response.headers["ETag"]
- get "/expires/expires_etag", {private: true}, "If-None-Match" => etag
- assert_equal "miss", last_response.headers["X-Rack-Cache"]
+ 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
@@ -152,12 +153,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
@@ -172,8 +173,8 @@ module ApplicationTests
body = last_response.body
last = last_response.headers["Last-Modified"]
- get "/expires/expires_last_modified", {private: true}, "If-Modified-Since" => last
- assert_equal "miss", last_response.headers["X-Rack-Cache"]
+ 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
end
diff --git a/railties/test/application/middleware/cookies_test.rb b/railties/test/application/middleware/cookies_test.rb
index bbb7627be9..fe48ef3f03 100644
--- a/railties/test/application/middleware/cookies_test.rb
+++ b/railties/test/application/middleware/cookies_test.rb
@@ -1,8 +1,12 @@
-require 'isolation/abstract_unit'
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "rack/test"
module ApplicationTests
class CookiesTest < ActiveSupport::TestCase
include ActiveSupport::Testing::Isolation
+ include Rack::Test::Methods
def new_app
File.expand_path("#{app_path}/../new_app")
@@ -10,38 +14,180 @@ module ApplicationTests
def setup
build_app
- boot_rails
FileUtils.rm_rf("#{app_path}/config/environments")
end
+ def app
+ Rails.application
+ end
+
def teardown
teardown_app
FileUtils.rm_rf(new_app) if File.directory?(new_app)
end
- test 'always_write_cookie is true by default in development' do
- require 'rails'
- Rails.env = 'development'
+ test "always_write_cookie is true by default in development" do
+ require "rails"
+ Rails.env = "development"
require "#{app_path}/config/environment"
assert_equal true, ActionDispatch::Cookies::CookieJar.always_write_cookie
end
- test 'always_write_cookie is false by default in production' do
- require 'rails'
- Rails.env = 'production'
+ test "always_write_cookie is false by default in production" do
+ require "rails"
+ Rails.env = "production"
require "#{app_path}/config/environment"
assert_equal false, ActionDispatch::Cookies::CookieJar.always_write_cookie
end
- test 'always_write_cookie can be overridden' do
+ test "always_write_cookie can be overridden" do
add_to_config <<-RUBY
config.action_dispatch.always_write_cookie = false
RUBY
- require 'rails'
- Rails.env = 'development'
+ require "rails"
+ Rails.env = "development"
require "#{app_path}/config/environment"
assert_equal false, ActionDispatch::Cookies::CookieJar.always_write_cookie
end
+
+ test "signed cookies with SHA512 digest and rotated out SHA256 and SHA1 digests" do
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ get ':controller(/:action)'
+ post ':controller(/:action)'
+ end
+ RUBY
+
+ controller :foo, <<-RUBY
+ class FooController < ActionController::Base
+ protect_from_forgery with: :null_session
+
+ def write_raw_cookie_sha1
+ cookies[:signed_cookie] = TestVerifiers.sha1.generate("signed cookie")
+ head :ok
+ end
+
+ def write_raw_cookie_sha256
+ cookies[:signed_cookie] = TestVerifiers.sha256.generate("signed cookie")
+ head :ok
+ end
+
+ def read_signed
+ render plain: cookies.signed[:signed_cookie].inspect
+ end
+
+ def read_raw_cookie
+ render plain: cookies[:signed_cookie]
+ end
+ end
+ RUBY
+
+ add_to_config <<-RUBY
+ sha1_secret = Rails.application.key_generator.generate_key("sha1")
+ sha256_secret = Rails.application.key_generator.generate_key("sha256")
+
+ ::TestVerifiers = Class.new do
+ class_attribute :sha1, default: ActiveSupport::MessageVerifier.new(sha1_secret, digest: "SHA1")
+ class_attribute :sha256, default: ActiveSupport::MessageVerifier.new(sha256_secret, digest: "SHA256")
+ end
+
+ config.action_dispatch.signed_cookie_digest = "SHA512"
+ config.action_dispatch.signed_cookie_salt = "sha512 salt"
+
+ config.action_dispatch.cookies_rotations.tap do |cookies|
+ cookies.rotate :signed, sha1_secret, digest: "SHA1"
+ cookies.rotate :signed, sha256_secret, digest: "SHA256"
+ end
+ RUBY
+
+ require "#{app_path}/config/environment"
+
+ verifier_sha512 = ActiveSupport::MessageVerifier.new(app.key_generator.generate_key("sha512 salt"), digest: :SHA512)
+
+ get "/foo/write_raw_cookie_sha1"
+ get "/foo/read_signed"
+ assert_equal "signed cookie".inspect, last_response.body
+
+ get "/foo/read_raw_cookie"
+ assert_equal "signed cookie", verifier_sha512.verify(last_response.body, purpose: "cookie.signed_cookie")
+
+ get "/foo/write_raw_cookie_sha256"
+ get "/foo/read_signed"
+ assert_equal "signed cookie".inspect, last_response.body
+
+ get "/foo/read_raw_cookie"
+ assert_equal "signed cookie", verifier_sha512.verify(last_response.body, purpose: "cookie.signed_cookie")
+ end
+
+ test "encrypted cookies rotating multiple encryption keys" do
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ get ':controller(/:action)'
+ post ':controller(/:action)'
+ end
+ RUBY
+
+ controller :foo, <<-RUBY
+ class FooController < ActionController::Base
+ protect_from_forgery with: :null_session
+
+ def write_raw_cookie_one
+ cookies[:encrypted_cookie] = TestEncryptors.first_gcm.encrypt_and_sign("encrypted cookie")
+ head :ok
+ end
+
+ def write_raw_cookie_two
+ cookies[:encrypted_cookie] = TestEncryptors.second_gcm.encrypt_and_sign("encrypted cookie")
+ head :ok
+ end
+
+ def read_encrypted
+ render plain: cookies.encrypted[:encrypted_cookie].inspect
+ end
+
+ def read_raw_cookie
+ render plain: cookies[:encrypted_cookie]
+ end
+ end
+ RUBY
+
+ add_to_config <<-RUBY
+ first_secret = Rails.application.key_generator.generate_key("first", 32)
+ second_secret = Rails.application.key_generator.generate_key("second", 32)
+
+ ::TestEncryptors = Class.new do
+ class_attribute :first_gcm, default: ActiveSupport::MessageEncryptor.new(first_secret, cipher: "aes-256-gcm")
+ class_attribute :second_gcm, default: ActiveSupport::MessageEncryptor.new(second_secret, cipher: "aes-256-gcm")
+ end
+
+ config.action_dispatch.use_authenticated_cookie_encryption = true
+ config.action_dispatch.encrypted_cookie_cipher = "aes-256-gcm"
+ config.action_dispatch.authenticated_encrypted_cookie_salt = "salt"
+
+ config.action_dispatch.cookies_rotations.tap do |cookies|
+ cookies.rotate :encrypted, first_secret
+ cookies.rotate :encrypted, second_secret
+ end
+ RUBY
+
+ require "#{app_path}/config/environment"
+
+ encryptor = ActiveSupport::MessageEncryptor.new(app.key_generator.generate_key("salt", 32), cipher: "aes-256-gcm")
+
+ get "/foo/write_raw_cookie_one"
+ get "/foo/read_encrypted"
+ assert_equal "encrypted cookie".inspect, last_response.body
+
+ get "/foo/read_raw_cookie"
+ assert_equal "encrypted cookie", encryptor.decrypt_and_verify(last_response.body, purpose: "cookie.encrypted_cookie")
+
+ get "/foo/write_raw_cookie_two"
+ get "/foo/read_encrypted"
+ assert_equal "encrypted cookie".inspect, last_response.body
+
+ get "/foo/read_raw_cookie"
+ assert_equal "encrypted cookie", encryptor.decrypt_and_verify(last_response.body, purpose: "cookie.encrypted_cookie")
+ end
end
end
diff --git a/railties/test/application/middleware/exceptions_test.rb b/railties/test/application/middleware/exceptions_test.rb
index 4906f9a1e8..2d659ade8d 100644
--- a/railties/test/application/middleware/exceptions_test.rb
+++ b/railties/test/application/middleware/exceptions_test.rb
@@ -1,5 +1,7 @@
-require 'isolation/abstract_unit'
-require 'rack/test'
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "rack/test"
module ApplicationTests
class MiddlewareExceptionsTest < ActiveSupport::TestCase
@@ -8,7 +10,6 @@ module ApplicationTests
def setup
build_app
- boot_rails
end
def teardown
@@ -48,7 +49,7 @@ module ApplicationTests
test "uses custom exceptions app" do
add_to_config <<-RUBY
config.exceptions_app = lambda do |env|
- [404, { "Content-Type" => "text/plain" }, ["YOU FAILED BRO"]]
+ [404, { "Content-Type" => "text/plain" }, ["YOU FAILED"]]
end
RUBY
@@ -56,7 +57,7 @@ module ApplicationTests
get "/foo"
assert_equal 404, last_response.status
- assert_equal "YOU FAILED BRO", last_response.body
+ assert_equal "YOU FAILED", last_response.body
end
test "url generation error when action_dispatch.show_exceptions is set raises an exception" do
@@ -67,10 +68,10 @@ module ApplicationTests
end
end
RUBY
-
+
app.config.action_dispatch.show_exceptions = true
- get '/foo'
+ get "/foo"
assert_equal 500, last_response.status
end
@@ -78,15 +79,15 @@ module ApplicationTests
app.config.action_dispatch.show_exceptions = false
assert_raise(ActionController::RoutingError) do
- get '/foo'
+ get "/foo"
end
end
test "unspecified route when action_dispatch.show_exceptions is set shows 404" do
app.config.action_dispatch.show_exceptions = true
- assert_nothing_raised(ActionController::RoutingError) do
- get '/foo'
+ assert_nothing_raised do
+ get "/foo"
assert_match "The page you were looking for doesn't exist.", last_response.body
end
end
@@ -95,12 +96,26 @@ module ApplicationTests
app.config.action_dispatch.show_exceptions = true
app.config.consider_all_requests_local = true
- assert_nothing_raised(ActionController::RoutingError) do
- get '/foo'
+ assert_nothing_raised do
+ get "/foo"
assert_match "No route matches", last_response.body
end
end
+ test "routing to a nonexistent controller when action_dispatch.show_exceptions and consider_all_requests_local are set shows diagnostics" do
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ resources :articles
+ end
+ RUBY
+
+ app.config.action_dispatch.show_exceptions = true
+ app.config.consider_all_requests_local = true
+
+ get "/articles"
+ assert_match "<title>Action Controller: Exception caught</title>", last_response.body
+ end
+
test "displays diagnostics message when exception raised in template that contains UTF-8" do
controller :foo, <<-RUBY
class FooController < ActionController::Base
@@ -112,12 +127,12 @@ module ApplicationTests
app.config.action_dispatch.show_exceptions = true
app.config.consider_all_requests_local = true
- app_file 'app/views/foo/index.html.erb', <<-ERB
+ app_file "app/views/foo/index.html.erb", <<-ERB
<% raise 'boooom' %>
✓測試テスト시험
ERB
- get '/foo', :utf8 => '✓'
+ get "/foo", utf8: "✓"
assert_match(/boooom/, last_response.body)
assert_match(/測試テスト시험/, last_response.body)
end
diff --git a/railties/test/application/middleware/remote_ip_test.rb b/railties/test/application/middleware/remote_ip_test.rb
index 97d5b5c698..83cf8a27f7 100644
--- a/railties/test/application/middleware/remote_ip_test.rb
+++ b/railties/test/application/middleware/remote_ip_test.rb
@@ -1,6 +1,8 @@
-require 'ipaddr'
-require 'isolation/abstract_unit'
-require 'active_support/key_generator'
+# frozen_string_literal: true
+
+require "ipaddr"
+require "isolation/abstract_unit"
+require "active_support/key_generator"
module ApplicationTests
class RemoteIpTest < ActiveSupport::TestCase
@@ -9,8 +11,8 @@ module ApplicationTests
def remote_ip(env = {})
remote_ip = nil
env = Rack::MockRequest.env_for("/").merge(env).merge!(
- 'action_dispatch.show_exceptions' => false,
- 'action_dispatch.key_generator' => ActiveSupport::LegacyKeyGenerator.new('b3c631c314c0bbca50c1b2843150fe33')
+ "action_dispatch.show_exceptions" => false,
+ "action_dispatch.key_generator" => ActiveSupport::LegacyKeyGenerator.new("b3c631c314c0bbca50c1b2843150fe33")
)
endpoint = Proc.new do |e|
@@ -36,10 +38,10 @@ module ApplicationTests
test "works with both headers individually" do
make_basic_app
- assert_nothing_raised(ActionDispatch::RemoteIp::IpSpoofAttackError) do
+ assert_nothing_raised do
assert_equal "1.1.1.1", remote_ip("HTTP_X_FORWARDED_FOR" => "1.1.1.1")
end
- assert_nothing_raised(ActionDispatch::RemoteIp::IpSpoofAttackError) do
+ assert_nothing_raised do
assert_equal "1.1.1.2", remote_ip("HTTP_CLIENT_IP" => "1.1.1.2")
end
end
@@ -49,7 +51,7 @@ module ApplicationTests
app.config.action_dispatch.ip_spoofing_check = false
end
- assert_nothing_raised(ActionDispatch::RemoteIp::IpSpoofAttackError) do
+ assert_nothing_raised do
assert_equal "1.1.1.1", remote_ip("HTTP_X_FORWARDED_FOR" => "1.1.1.1", "HTTP_CLIENT_IP" => "1.1.1.2")
end
end
@@ -69,7 +71,7 @@ module ApplicationTests
test "the user can set trusted proxies with an IPAddr argument" do
make_basic_app do |app|
- app.config.action_dispatch.trusted_proxies = IPAddr.new('4.2.42.0/24')
+ app.config.action_dispatch.trusted_proxies = IPAddr.new("4.2.42.0/24")
end
assert_equal "1.1.1.1", remote_ip("REMOTE_ADDR" => "1.1.1.1", "HTTP_X_FORWARDED_FOR" => "10.0.0.0,4.2.42.42")
diff --git a/railties/test/application/middleware/sendfile_test.rb b/railties/test/application/middleware/sendfile_test.rb
index dc96480d6d..818ad61c64 100644
--- a/railties/test/application/middleware/sendfile_test.rb
+++ b/railties/test/application/middleware/sendfile_test.rb
@@ -1,4 +1,6 @@
-require 'isolation/abstract_unit'
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
module ApplicationTests
class SendfileTest < ActiveSupport::TestCase
@@ -6,7 +8,6 @@ module ApplicationTests
def setup
build_app
- boot_rails
FileUtils.rm_rf "#{app_path}/config/environments"
end
@@ -14,10 +15,6 @@ module ApplicationTests
teardown_app
end
- def app
- @app ||= Rails.application
- end
-
define_method :simple_controller do
class ::OmgController < ActionController::Base
def index
@@ -32,7 +29,7 @@ module ApplicationTests
simple_controller
get "/"
- assert !last_response.headers["X-Sendfile"]
+ assert_not last_response.headers["X-Sendfile"]
assert_equal File.read(__FILE__), last_response.body
end
@@ -49,7 +46,7 @@ module ApplicationTests
test "config.action_dispatch.x_sendfile_header is sent to Rack::Sendfile" do
make_basic_app do |app|
- app.config.action_dispatch.x_sendfile_header = 'X-Lighttpd-Send-File'
+ app.config.action_dispatch.x_sendfile_header = "X-Lighttpd-Send-File"
end
simple_controller
@@ -60,8 +57,8 @@ module ApplicationTests
test "files handled by ActionDispatch::Static are handled by Rack::Sendfile" do
make_basic_app do |app|
- app.config.action_dispatch.x_sendfile_header = 'X-Sendfile'
- app.config.serve_static_files = true
+ app.config.action_dispatch.x_sendfile_header = "X-Sendfile"
+ app.config.public_file_server.enabled = true
app.paths["public"] = File.join(rails_root, "public")
end
diff --git a/railties/test/application/middleware/session_test.rb b/railties/test/application/middleware/session_test.rb
index 25eadfc387..b25e56b625 100644
--- a/railties/test/application/middleware/session_test.rb
+++ b/railties/test/application/middleware/session_test.rb
@@ -1,5 +1,7 @@
-require 'isolation/abstract_unit'
-require 'rack/test'
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "rack/test"
module ApplicationTests
class MiddlewareSessionTest < ActiveSupport::TestCase
@@ -8,7 +10,6 @@ module ApplicationTests
def setup
build_app
- boot_rails
FileUtils.rm_rf "#{app_path}/config/environments"
end
@@ -20,12 +21,19 @@ module ApplicationTests
@app ||= Rails.application
end
- test "config.force_ssl sets cookie to secure only" do
+ test "config.force_ssl sets cookie to secure only by default" do
add_to_config "config.force_ssl = true"
require "#{app_path}/config/environment"
assert app.config.session_options[:secure], "Expected session to be marked as secure"
end
+ test "config.force_ssl doesn't set cookie to secure only when changed from default" do
+ add_to_config "config.force_ssl = true"
+ add_to_config "config.ssl_options = { secure_cookies: false }"
+ require "#{app_path}/config/environment"
+ assert_not app.config.session_options[:secure]
+ end
+
test "session is not loaded if it's not used" do
make_basic_app
@@ -43,11 +51,11 @@ module ApplicationTests
get "/"
assert last_request.env["HTTP_COOKIE"]
- assert !last_response.headers["Set-Cookie"]
+ assert_not last_response.headers["Set-Cookie"]
end
test "session is empty and isn't saved on unverified request when using :null_session protect method" do
- app_file 'config/routes.rb', <<-RUBY
+ app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get ':controller(/:action)'
post ':controller(/:action)'
@@ -64,7 +72,7 @@ module ApplicationTests
end
def read_session
- render text: session[:foo].inspect
+ render plain: session[:foo].inspect
end
end
RUBY
@@ -75,20 +83,20 @@ module ApplicationTests
require "#{app_path}/config/environment"
- get '/foo/write_session'
- get '/foo/read_session'
- assert_equal '1', last_response.body
+ get "/foo/write_session"
+ get "/foo/read_session"
+ assert_equal "1", last_response.body
- post '/foo/read_session' # Read session using POST request without CSRF token
- assert_equal 'nil', last_response.body # Stored value shouldn't be accessible
+ post "/foo/read_session" # Read session using POST request without CSRF token
+ assert_equal "nil", last_response.body # Stored value shouldn't be accessible
- post '/foo/write_session' # Write session using POST request without CSRF token
- get '/foo/read_session' # Session shouldn't be changed
- assert_equal '1', last_response.body
+ post "/foo/write_session" # Write session using POST request without CSRF token
+ get "/foo/read_session" # Session shouldn't be changed
+ assert_equal "1", last_response.body
end
test "cookie jar is empty and isn't saved on unverified request when using :null_session protect method" do
- app_file 'config/routes.rb', <<-RUBY
+ app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get ':controller(/:action)'
post ':controller(/:action)'
@@ -105,7 +113,7 @@ module ApplicationTests
end
def read_cookie
- render text: cookies[:foo].inspect
+ render plain: cookies[:foo].inspect
end
end
RUBY
@@ -116,20 +124,20 @@ module ApplicationTests
require "#{app_path}/config/environment"
- get '/foo/write_cookie'
- get '/foo/read_cookie'
+ get "/foo/write_cookie"
+ get "/foo/read_cookie"
assert_equal '"1"', last_response.body
- post '/foo/read_cookie' # Read cookie using POST request without CSRF token
- assert_equal 'nil', last_response.body # Stored value shouldn't be accessible
+ post "/foo/read_cookie" # Read cookie using POST request without CSRF token
+ assert_equal "nil", last_response.body # Stored value shouldn't be accessible
- post '/foo/write_cookie' # Write cookie using POST request without CSRF token
- get '/foo/read_cookie' # Cookie shouldn't be changed
+ post "/foo/write_cookie" # Write cookie using POST request without CSRF token
+ get "/foo/read_cookie" # Cookie shouldn't be changed
assert_equal '"1"', last_response.body
end
test "session using encrypted cookie store" do
- app_file 'config/routes.rb', <<-RUBY
+ app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get ':controller(/:action)'
end
@@ -143,38 +151,43 @@ module ApplicationTests
end
def read_session
- render text: session[:foo]
+ render plain: session[:foo]
end
def read_encrypted_cookie
- render text: cookies.encrypted[:_myapp_session]['foo']
+ render plain: cookies.encrypted[:_myapp_session]['foo']
end
def read_raw_cookie
- render text: cookies[:_myapp_session]
+ render plain: cookies[:_myapp_session]
end
end
RUBY
+ add_to_config <<-RUBY
+ # Enable AEAD cookies
+ config.action_dispatch.use_authenticated_cookie_encryption = true
+ RUBY
+
require "#{app_path}/config/environment"
- get '/foo/write_session'
- get '/foo/read_session'
- assert_equal '1', last_response.body
+ get "/foo/write_session"
+ get "/foo/read_session"
+ assert_equal "1", last_response.body
- get '/foo/read_encrypted_cookie'
- assert_equal '1', last_response.body
+ get "/foo/read_encrypted_cookie"
+ assert_equal "1", last_response.body
- 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)
+ cipher = "aes-256-gcm"
+ secret = app.key_generator.generate_key("authenticated encrypted cookie")
+ encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher)
- get '/foo/read_raw_cookie'
- assert_equal 1, encryptor.decrypt_and_verify(last_response.body)['foo']
+ get "/foo/read_raw_cookie"
+ assert_equal 1, encryptor.decrypt_and_verify(last_response.body, purpose: "cookie._myapp_session")["foo"]
end
test "session upgrading signature to encryption cookie store works the same way as encrypted cookie store" do
- app_file 'config/routes.rb', <<-RUBY
+ app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get ':controller(/:action)'
end
@@ -188,42 +201,45 @@ module ApplicationTests
end
def read_session
- render text: session[:foo]
+ render plain: session[:foo]
end
def read_encrypted_cookie
- render text: cookies.encrypted[:_myapp_session]['foo']
+ render plain: cookies.encrypted[:_myapp_session]['foo']
end
def read_raw_cookie
- render text: cookies[:_myapp_session]
+ render plain: cookies[:_myapp_session]
end
end
RUBY
add_to_config <<-RUBY
secrets.secret_token = "3b7cd727ee24e8444053437c36cc66c4"
+
+ # Enable AEAD cookies
+ config.action_dispatch.use_authenticated_cookie_encryption = true
RUBY
require "#{app_path}/config/environment"
- get '/foo/write_session'
- get '/foo/read_session'
- assert_equal '1', last_response.body
+ get "/foo/write_session"
+ get "/foo/read_session"
+ assert_equal "1", last_response.body
- get '/foo/read_encrypted_cookie'
- assert_equal '1', last_response.body
+ get "/foo/read_encrypted_cookie"
+ assert_equal "1", last_response.body
- 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)
+ cipher = "aes-256-gcm"
+ secret = app.key_generator.generate_key("authenticated encrypted cookie")
+ encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher)
- get '/foo/read_raw_cookie'
- assert_equal 1, encryptor.decrypt_and_verify(last_response.body)['foo']
+ get "/foo/read_raw_cookie"
+ assert_equal 1, encryptor.decrypt_and_verify(last_response.body, purpose: "cookie._myapp_session")["foo"]
end
test "session upgrading signature to encryption cookie store upgrades session to encrypted mode" do
- app_file 'config/routes.rb', <<-RUBY
+ app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get ':controller(/:action)'
end
@@ -243,46 +259,119 @@ module ApplicationTests
end
def read_session
- render text: session[:foo]
+ render plain: session[:foo]
end
def read_encrypted_cookie
- render text: cookies.encrypted[:_myapp_session]['foo']
+ render plain: cookies.encrypted[:_myapp_session]['foo']
end
def read_raw_cookie
- render text: cookies[:_myapp_session]
+ render plain: cookies[:_myapp_session]
end
end
RUBY
add_to_config <<-RUBY
secrets.secret_token = "3b7cd727ee24e8444053437c36cc66c4"
+
+ # Enable AEAD cookies
+ config.action_dispatch.use_authenticated_cookie_encryption = true
RUBY
require "#{app_path}/config/environment"
- get '/foo/write_raw_session'
- get '/foo/read_session'
- assert_equal '1', last_response.body
+ get "/foo/write_raw_session"
+ get "/foo/read_session"
+ assert_equal "1", last_response.body
+
+ get "/foo/write_session"
+ get "/foo/read_session"
+ assert_equal "2", last_response.body
+
+ get "/foo/read_encrypted_cookie"
+ assert_equal "2", last_response.body
+
+ cipher = "aes-256-gcm"
+ secret = app.key_generator.generate_key("authenticated encrypted cookie")
+ encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher)
+
+ get "/foo/read_raw_cookie"
+ assert_equal 2, encryptor.decrypt_and_verify(last_response.body, purpose: "cookie._myapp_session")["foo"]
+ end
+
+ test "session upgrading from AES-CBC-HMAC encryption to AES-GCM encryption" do
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ get ':controller(/:action)'
+ end
+ RUBY
+
+ controller :foo, <<-RUBY
+ class FooController < ActionController::Base
+ def write_raw_session
+ # AES-256-CBC with SHA1 HMAC
+ # {"session_id"=>"1965d95720fffc123941bdfb7d2e6870", "foo"=>1}
+ cookies[:_myapp_session] = "TlgrdS85aUpDd1R2cDlPWlR6K0FJeGExckwySjZ2Z0pkR3d2QnRObGxZT25aalJWYWVvbFVLcHF4d0VQVDdSaFF2QjFPbG9MVjJzeWp3YjcyRUlKUUU2ZlR4bXlSNG9ZUkJPRUtld0E3dVU9LS0xNDZXbGpRZ3NjdW43N2haUEZJSUNRPT0=--3639b5ce54c09495cfeaae928cd5634e0c4b2e96"
+ head :ok
+ end
+
+ def write_session
+ session[:foo] = session[:foo] + 1
+ head :ok
+ end
+
+ def read_session
+ render plain: session[:foo]
+ end
+
+ def read_encrypted_cookie
+ render plain: cookies.encrypted[:_myapp_session]['foo']
+ end
+
+ def read_raw_cookie
+ render plain: cookies[:_myapp_session]
+ end
+ end
+ RUBY
+
+ add_to_config <<-RUBY
+ # Use a static key
+ Rails.application.credentials.secret_key_base = "known key base"
- get '/foo/write_session'
- get '/foo/read_session'
- assert_equal '2', last_response.body
+ # Enable AEAD cookies
+ config.action_dispatch.use_authenticated_cookie_encryption = true
+ RUBY
+
+ begin
+ old_rails_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "production"
+
+ require "#{app_path}/config/environment"
+
+ get "/foo/write_raw_session"
+ get "/foo/read_session"
+ assert_equal "1", last_response.body
+
+ get "/foo/write_session"
+ get "/foo/read_session"
+ assert_equal "2", last_response.body
- get '/foo/read_encrypted_cookie'
- assert_equal '2', last_response.body
+ get "/foo/read_encrypted_cookie"
+ assert_equal "2", last_response.body
- 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)
+ cipher = "aes-256-gcm"
+ secret = app.key_generator.generate_key("authenticated encrypted cookie")
+ encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher)
- get '/foo/read_raw_cookie'
- assert_equal 2, encryptor.decrypt_and_verify(last_response.body)['foo']
+ get "/foo/read_raw_cookie"
+ assert_equal 2, encryptor.decrypt_and_verify(last_response.body, purpose: "cookie._myapp_session")["foo"]
+ ensure
+ ENV["RAILS_ENV"] = old_rails_env
+ end
end
test "session upgrading legacy signed cookies to new signed cookies" do
- app_file 'config/routes.rb', <<-RUBY
+ app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get ':controller(/:action)'
end
@@ -302,41 +391,81 @@ module ApplicationTests
end
def read_session
- render text: session[:foo]
+ render plain: session[:foo]
end
def read_signed_cookie
- render text: cookies.signed[:_myapp_session]['foo']
+ render plain: cookies.signed[:_myapp_session]['foo']
end
def read_raw_cookie
- render text: cookies[:_myapp_session]
+ render plain: cookies[:_myapp_session]
end
end
RUBY
add_to_config <<-RUBY
secrets.secret_token = "3b7cd727ee24e8444053437c36cc66c4"
- secrets.secret_key_base = nil
+ Rails.application.credentials.secret_key_base = nil
RUBY
- require "#{app_path}/config/environment"
+ begin
+ old_rails_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "production"
+
+ require "#{app_path}/config/environment"
+
+ get "/foo/write_raw_session"
+ get "/foo/read_session"
+ assert_equal "1", last_response.body
- get '/foo/write_raw_session'
- get '/foo/read_session'
- assert_equal '1', last_response.body
+ get "/foo/write_session"
+ get "/foo/read_session"
+ assert_equal "2", last_response.body
- get '/foo/write_session'
- get '/foo/read_session'
- assert_equal '2', last_response.body
+ get "/foo/read_signed_cookie"
+ assert_equal "2", last_response.body
- get '/foo/read_signed_cookie'
- assert_equal '2', last_response.body
+ verifier = ActiveSupport::MessageVerifier.new(app.secrets.secret_token)
- verifier = ActiveSupport::MessageVerifier.new(app.secrets.secret_token)
+ get "/foo/read_raw_cookie"
+ assert_equal 2, verifier.verify(last_response.body, purpose: "cookie._myapp_session")["foo"]
+ ensure
+ ENV["RAILS_ENV"] = old_rails_env
+ end
+ end
+
+ test "calling reset_session on request does not trigger an error for API apps" do
+ add_to_config "config.api_only = true"
+
+ controller :test, <<-RUBY
+ class TestController < ApplicationController
+ def dump_flash
+ request.reset_session
+ render plain: 'It worked!'
+ end
+ end
+ RUBY
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ get '/dump_flash' => "test#dump_flash"
+ end
+ RUBY
+
+ require "#{app_path}/config/environment"
+
+ get "/dump_flash"
- get '/foo/read_raw_cookie'
- assert_equal 2, verifier.verify(last_response.body)['foo']
+ assert_equal 200, last_response.status
+ assert_equal "It worked!", last_response.body
+
+ assert_not_includes Rails.application.middleware, ActionDispatch::Flash
+ end
+
+ test "cookie_only is set to true even if user tries to overwrite it" do
+ add_to_config "config.session_store :cookie_store, key: '_myapp_session', cookie_only: false"
+ require "#{app_path}/config/environment"
+ assert app.config.session_options[:cookie_only], "Expected cookie_only to be set to true"
end
end
end
diff --git a/railties/test/application/middleware/static_test.rb b/railties/test/application/middleware/static_test.rb
index 1a46cd3568..0977042cfe 100644
--- a/railties/test/application/middleware/static_test.rb
+++ b/railties/test/application/middleware/static_test.rb
@@ -1,5 +1,7 @@
-require 'isolation/abstract_unit'
-require 'rack/test'
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "rack/test"
module ApplicationTests
class MiddlewareStaticTest < ActiveSupport::TestCase
@@ -18,32 +20,49 @@ module ApplicationTests
# Regression test to #8907
# See https://github.com/rails/rails/commit/9cc82b77196d21a5c7021f6dca59ab9b2b158a45#commitcomment-2416514
test "doesn't set Cache-Control header when it is nil" do
- app_file "public/foo.html", 'static'
+ app_file "public/foo.html", "static"
+
+ require "#{app_path}/config/environment"
+
+ get "foo"
+
+ assert_not last_response.headers.has_key?("Cache-Control"), "Cache-Control should not be set"
+ end
+
+ test "headers for static files are configurable" do
+ app_file "public/about.html", "static"
+ add_to_config <<-CONFIG
+ config.public_file_server.headers = {
+ "Access-Control-Allow-Origin" => "http://rubyonrails.org",
+ "Cache-Control" => "public, max-age=60"
+ }
+ CONFIG
require "#{app_path}/config/environment"
- get 'foo'
+ get "/about.html"
- assert_not last_response.headers.has_key?('Cache-Control'), "Cache-Control should not be set"
+ assert_equal "http://rubyonrails.org", last_response.headers["Access-Control-Allow-Origin"]
+ assert_equal "public, max-age=60", last_response.headers["Cache-Control"]
end
- test "static_index defaults to 'index'" do
+ test "public_file_server.index_name defaults to 'index'" do
app_file "public/index.html", "/index.html"
-
+
require "#{app_path}/config/environment"
- get '/'
-
+ get "/"
+
assert_equal "/index.html\n", last_response.body
end
- test "static_index configurable" do
+ test "public_file_server.index_name configurable" do
app_file "public/other-index.html", "/other-index.html"
- add_to_config "config.static_index = 'other-index'"
-
+ add_to_config "config.public_file_server.index_name = 'other-index'"
+
require "#{app_path}/config/environment"
- get '/'
+ get "/"
assert_equal "/other-index.html\n", last_response.body
end
diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb
index d298e8d632..631f5bac7f 100644
--- a/railties/test/application/middleware_test.rb
+++ b/railties/test/application/middleware_test.rb
@@ -1,4 +1,6 @@
-require 'isolation/abstract_unit'
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
module ApplicationTests
class MiddlewareTest < ActiveSupport::TestCase
@@ -6,7 +8,6 @@ module ApplicationTests
def setup
build_app
- boot_rails
FileUtils.rm_rf "#{app_path}/config/environments"
end
@@ -24,29 +25,29 @@ module ApplicationTests
boot!
assert_equal [
+ "Webpacker::DevServerProxy",
"Rack::Sendfile",
"ActionDispatch::Static",
- "ActionDispatch::LoadInterlock",
+ "ActionDispatch::Executor",
"ActiveSupport::Cache::Strategy::LocalCache",
"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",
- "ActiveRecord::ConnectionAdapters::ConnectionManagement",
- "ActiveRecord::QueryCache",
"ActionDispatch::Cookies",
"ActionDispatch::Session::CookieStore",
"ActionDispatch::Flash",
- "ActionDispatch::ParamsParser",
+ "ActionDispatch::ContentSecurityPolicy::Middleware",
"Rack::Head",
"Rack::ConditionalGet",
- "Rack::ETag"
+ "Rack::ETag",
+ "Rack::TempfileReaper"
], middleware
end
@@ -56,31 +57,60 @@ module ApplicationTests
boot!
assert_equal [
+ "Webpacker::DevServerProxy",
"Rack::Sendfile",
"ActionDispatch::Static",
- "ActionDispatch::LoadInterlock",
+ "ActionDispatch::Executor",
"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",
- "ActiveRecord::ConnectionAdapters::ConnectionManagement",
- "ActiveRecord::QueryCache",
- "ActionDispatch::ParamsParser",
"Rack::Head",
"Rack::ConditionalGet",
"Rack::ETag"
], 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!
- assert !middleware.include?("Rack::Cache"), "Rack::Cache is not included in the default stack unless you set config.action_dispatch.rack_cache"
+ assert_not_includes middleware, "Rack::Cache", "Rack::Cache is not included in the default stack unless you set config.action_dispatch.rack_cache"
end
test "Rack::Cache is present when action_dispatch.rack_cache is set" do
@@ -88,7 +118,7 @@ module ApplicationTests
boot!
- assert middleware.include?("Rack::Cache")
+ assert_includes middleware, "Rack::Cache"
end
test "ActiveRecord::Migration::CheckPending is present when active_record.migration_error is set to :page_load" do
@@ -96,43 +126,32 @@ module ApplicationTests
boot!
- assert middleware.include?("ActiveRecord::Migration::CheckPending")
+ assert_includes middleware, "ActiveRecord::Migration::CheckPending"
end
test "ActionDispatch::SSL is present when force_ssl is set" do
add_to_config "config.force_ssl = true"
boot!
- assert middleware.include?("ActionDispatch::SSL")
+ assert_includes middleware, "ActionDispatch::SSL"
end
test "ActionDispatch::SSL is configured with options when given" do
add_to_config "config.force_ssl = true"
- add_to_config "config.ssl_options = { host: 'example.com' }"
+ add_to_config "config.ssl_options = { redirect: { host: 'example.com' } }"
boot!
- assert_equal [{host: 'example.com'}], Rails.application.middleware.first.args
+ assert_equal [{ redirect: { host: "example.com" } }], Rails.application.middleware[1].args
end
test "removing Active Record omits its middleware" do
use_frameworks []
boot!
- assert !middleware.include?("ActiveRecord::ConnectionAdapters::ConnectionManagement")
- assert !middleware.include?("ActiveRecord::QueryCache")
- assert !middleware.include?("ActiveRecord::Migration::CheckPending")
+ assert_not_includes middleware, "ActiveRecord::Migration::CheckPending"
end
- test "includes interlock if cache_classes is set but eager_load is not" do
- add_to_config "config.cache_classes = true"
+ test "includes executor" do
boot!
- assert_not_includes middleware, "Rack::Lock"
- assert_includes middleware, "ActionDispatch::LoadInterlock"
- end
-
- test "includes interlock if cache_classes is off" do
- add_to_config "config.cache_classes = false"
- boot!
- assert_not_includes middleware, "Rack::Lock"
- assert_includes middleware, "ActionDispatch::LoadInterlock"
+ assert_includes middleware, "ActionDispatch::Executor"
end
test "does not include lock if cache_classes is set and so is eager_load" do
@@ -140,40 +159,37 @@ module ApplicationTests
add_to_config "config.eager_load = true"
boot!
assert_not_includes middleware, "Rack::Lock"
- assert_not_includes middleware, "ActionDispatch::LoadInterlock"
end
test "does not include lock if allow_concurrency is set to :unsafe" do
add_to_config "config.allow_concurrency = :unsafe"
boot!
assert_not_includes middleware, "Rack::Lock"
- assert_not_includes middleware, "ActionDispatch::LoadInterlock"
end
test "includes lock if allow_concurrency is disabled" do
add_to_config "config.allow_concurrency = false"
boot!
assert_includes middleware, "Rack::Lock"
- assert_not_includes middleware, "ActionDispatch::LoadInterlock"
end
- test "removes static asset server if serve_static_files is disabled" do
- add_to_config "config.serve_static_files = false"
+ test "removes static asset server if public_file_server.enabled is disabled" do
+ add_to_config "config.public_file_server.enabled = false"
boot!
- assert !middleware.include?("ActionDispatch::Static")
+ assert_not_includes middleware, "ActionDispatch::Static"
end
test "can delete a middleware from the stack" do
add_to_config "config.middleware.delete ActionDispatch::Static"
boot!
- assert !middleware.include?("ActionDispatch::Static")
+ assert_not_includes middleware, "ActionDispatch::Static"
end
test "can delete a middleware from the stack even if insert_before is added after delete" do
add_to_config "config.middleware.delete Rack::Runtime"
add_to_config "config.middleware.insert_before(Rack::Runtime, Rack::Config)"
boot!
- assert middleware.include?("Rack::Config")
+ assert_includes middleware, "Rack::Config"
assert_not middleware.include?("Rack::Runtime")
end
@@ -181,21 +197,21 @@ module ApplicationTests
add_to_config "config.middleware.delete Rack::Runtime"
add_to_config "config.middleware.insert_after(Rack::Runtime, Rack::Config)"
boot!
- assert middleware.include?("Rack::Config")
+ assert_includes middleware, "Rack::Config"
assert_not middleware.include?("Rack::Runtime")
end
test "includes exceptions middlewares even if action_dispatch.show_exceptions is disabled" do
add_to_config "config.action_dispatch.show_exceptions = false"
boot!
- assert middleware.include?("ActionDispatch::ShowExceptions")
- assert middleware.include?("ActionDispatch::DebugExceptions")
+ assert_includes middleware, "ActionDispatch::ShowExceptions"
+ assert_includes middleware, "ActionDispatch::DebugExceptions"
end
test "removes ActionDispatch::Reloader if cache_classes is true" do
add_to_config "config.cache_classes = true"
boot!
- assert !middleware.include?("ActionDispatch::Reloader")
+ assert_not_includes middleware, "ActionDispatch::Reloader"
end
test "use middleware" do
@@ -208,35 +224,36 @@ module ApplicationTests
test "insert middleware after" do
add_to_config "config.middleware.insert_after Rack::Sendfile, Rack::Config"
boot!
- assert_equal "Rack::Config", middleware.second
+ assert_equal "Rack::Config", middleware.third
end
- test 'unshift middleware' do
- add_to_config 'config.middleware.unshift Rack::Config'
+ test "unshift middleware" do
+ add_to_config "config.middleware.unshift Rack::Config"
boot!
- assert_equal 'Rack::Config', middleware.first
+ assert_equal "Rack::Config", middleware.second
end
test "Rails.cache does not respond to middleware" do
add_to_config "config.cache_store = :memory_store"
boot!
- assert_equal "Rack::Runtime", middleware.fourth
+ assert_equal "Rack::Runtime", middleware.fifth
end
test "Rails.cache does respond to middleware" do
boot!
- assert_equal "Rack::Runtime", middleware.fifth
+ assert_equal "ActiveSupport::Cache::Strategy::LocalCache", middleware.fifth
+ assert_equal "Rack::Runtime", middleware[5]
end
test "insert middleware before" do
add_to_config "config.middleware.insert_before Rack::Sendfile, Rack::Config"
boot!
- assert_equal "Rack::Config", middleware.first
+ assert_equal "Rack::Config", middleware.second
end
test "can't change middleware after it's built" do
boot!
- assert_raise RuntimeError do
+ assert_raise frozen_error_class do
app.config.middleware.use Rack::Config
end
end
@@ -248,35 +265,35 @@ module ApplicationTests
class ::OmgController < ActionController::Base
def index
if params[:nothing]
- render text: ""
+ render plain: ""
else
- render text: "OMG"
+ render plain: "OMG"
end
end
end
- etag = "W/" + "5af83e3196bf99f440f31f2e1a6c9afe".inspect
+ etag = "W/" + "c00862d1c6c1cf7c1b49388306e7b3c1".inspect
get "/"
assert_equal 200, last_response.status
assert_equal "OMG", last_response.body
- assert_equal "text/html; charset=utf-8", last_response.headers["Content-Type"]
+ assert_equal "text/plain; charset=utf-8", 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"]
- get "/", {}, "HTTP_IF_NONE_MATCH" => etag
+ 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"]
get "/?nothing=true"
assert_equal 200, last_response.status
assert_equal "", last_response.body
- assert_equal "text/html; charset=utf-8", last_response.headers["Content-Type"]
+ 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/multiple_applications_test.rb b/railties/test/application/multiple_applications_test.rb
index f2770a9cb4..d6c81c1fe2 100644
--- a/railties/test/application/multiple_applications_test.rb
+++ b/railties/test/application/multiple_applications_test.rb
@@ -1,4 +1,6 @@
-require 'isolation/abstract_unit'
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
module ApplicationTests
class MultipleApplicationsTest < ActiveSupport::TestCase
@@ -6,9 +8,8 @@ module ApplicationTests
def setup
build_app(initializers: true)
- boot_rails
require "#{rails_root}/config/environment"
- Rails.application.config.some_setting = 'something_or_other'
+ Rails.application.config.some_setting = "something_or_other"
end
def teardown
@@ -88,9 +89,9 @@ module ApplicationTests
require "#{app_path}/config/environment"
assert_equal 0, run_count, "The count should stay at zero without any calls to the rake tasks"
- require 'rake'
- require 'rake/testtask'
- require 'rdoc/task'
+ require "rake"
+ require "rake/testtask"
+ require "rdoc/task"
Rails.application.load_tasks
assert_equal 2, run_count, "Calling a rake task should result in two increments to the count"
end
diff --git a/railties/test/application/paths_test.rb b/railties/test/application/paths_test.rb
index 4029984ce9..28a9206daa 100644
--- a/railties/test/application/paths_test.rb
+++ b/railties/test/application/paths_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "isolation/abstract_unit"
module ApplicationTests
@@ -6,7 +8,6 @@ module ApplicationTests
def setup
build_app
- boot_rails
FileUtils.rm_rf("#{app_path}/config/environments")
app_file "config/environments/development.rb", ""
add_to_config <<-RUBY
@@ -36,7 +37,7 @@ module ApplicationTests
end
def assert_not_in_load_path(*path)
- assert !$:.any? { |p| File.expand_path(p) == root(*path) }, "Load path includes '#{root(*path)}'. They are:\n-----\n #{$:.join("\n")}\n-----"
+ assert_not $:.any? { |p| File.expand_path(p) == root(*path) }, "Load path includes '#{root(*path)}'. They are:\n-----\n #{$:.join("\n")}\n-----"
end
test "booting up Rails yields a valid paths object" do
@@ -56,9 +57,9 @@ module ApplicationTests
test "booting up Rails yields a list of paths that are eager" do
eager_load = @paths.eager_load
- assert eager_load.include?(root("app/controllers"))
- assert eager_load.include?(root("app/helpers"))
- assert eager_load.include?(root("app/models"))
+ assert_includes eager_load, root("app/controllers")
+ assert_includes eager_load, root("app/helpers")
+ assert_includes eager_load, root("app/models")
end
test "environments has a glob equal to the current environment" do
diff --git a/railties/test/application/per_request_digest_cache_test.rb b/railties/test/application/per_request_digest_cache_test.rb
new file mode 100644
index 0000000000..ab055c7648
--- /dev/null
+++ b/railties/test/application/per_request_digest_cache_test.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "rack/test"
+require "minitest/mock"
+
+require "action_view"
+
+class PerRequestDigestCacheTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation
+ include Rack::Test::Methods
+
+ setup do
+ build_app
+ add_to_config "config.consider_all_requests_local = true"
+
+ app_file "app/models/customer.rb", <<-RUBY
+ class Customer < Struct.new(:name, :id)
+ extend ActiveModel::Naming
+ include ActiveModel::Conversion
+
+ def cache_key
+ [ name, id ].join("/")
+ end
+ end
+ RUBY
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ resources :customers, only: :index
+ end
+ RUBY
+
+ app_file "app/controllers/customers_controller.rb", <<-RUBY
+ class CustomersController < ApplicationController
+ self.perform_caching = true
+
+ def index
+ render [ Customer.new('david', 1), Customer.new('dingus', 2) ]
+ end
+ end
+ RUBY
+
+ app_file "app/views/customers/_customer.html.erb", <<-RUBY
+ <% cache customer do %>
+ <%= customer.name %>
+ <% end %>
+ RUBY
+
+ require "#{app_path}/config/environment"
+ end
+
+ teardown :teardown_app
+
+ test "digests are reused when rendering the same template twice" do
+ get "/customers"
+ assert_equal 200, last_response.status
+
+ values = ActionView::LookupContext::DetailsKey.digest_caches.first.values
+ assert_equal [ "effc8928d0b33535c8a21d24ec617161" ], values
+ assert_equal %w(david dingus), last_response.body.split.map(&:strip)
+ end
+
+ test "template digests are cleared before a request" do
+ assert_called(ActionView::LookupContext::DetailsKey, :clear) do
+ get "/customers"
+ assert_equal 200, last_response.status
+ end
+ end
+end
diff --git a/railties/test/application/rack/logger_test.rb b/railties/test/application/rack/logger_test.rb
index 0082ec9cd2..ea425d5fa5 100644
--- a/railties/test/application/rack/logger_test.rb
+++ b/railties/test/application/rack/logger_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "isolation/abstract_unit"
require "active_support/log_subscriber/test_helper"
require "rack/test"
@@ -41,16 +43,22 @@ module ApplicationTests
end
test "logger logs HTTP verb override" do
- post "/", _method: 'put'
+ post "/", _method: "put"
wait
assert_match 'Started PUT "/"', logs
end
test "logger logs HEAD requests" do
- post "/", _method: 'head'
+ post "/", _method: "head"
wait
assert_match 'Started HEAD "/"', logs
end
+
+ test "logger logs correct remote IP address" do
+ get "/", {}, { "REMOTE_ADDR" => "127.0.0.1", "HTTP_X_FORWARDED_FOR" => "1.2.3.4" }
+ wait
+ assert_match 'Started GET "/" for 1.2.3.4', logs
+ end
end
end
end
diff --git a/railties/test/application/rackup_test.rb b/railties/test/application/rackup_test.rb
index 49ac9fc66c..383f18a7da 100644
--- a/railties/test/application/rackup_test.rb
+++ b/railties/test/application/rackup_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "isolation/abstract_unit"
module ApplicationTests
@@ -12,14 +14,13 @@ module ApplicationTests
def setup
build_app
- boot_rails
end
def teardown
teardown_app
end
- test "rails app is present" do
+ test "Rails app is present" do
assert File.exist?(app_path("config"))
end
@@ -37,7 +38,7 @@ module ApplicationTests
test "the config object is available on the application object" do
rackup
- assert_equal 'UTC', Rails.application.config.time_zone
+ assert_equal "UTC", Rails.application.config.time_zone
end
end
end
diff --git a/railties/test/application/rake/dbs_test.rb b/railties/test/application/rake/dbs_test.rb
index e7beab8b5e..039987ac8c 100644
--- a/railties/test/application/rake/dbs_test.rb
+++ b/railties/test/application/rake/dbs_test.rb
@@ -1,5 +1,6 @@
+# frozen_string_literal: true
+
require "isolation/abstract_unit"
-require "active_support/core_ext/string/strip"
module ApplicationTests
module RakeTests
@@ -8,7 +9,6 @@ module ApplicationTests
def setup
build_app
- boot_rails
FileUtils.rm_rf("#{app_path}/config/environments")
end
@@ -21,50 +21,136 @@ module ApplicationTests
end
def set_database_url
- ENV['DATABASE_URL'] = "sqlite3:#{database_url_db_name}"
+ ENV["DATABASE_URL"] = "sqlite3:#{database_url_db_name}"
# ensure it's using the DATABASE_URL
FileUtils.rm_rf("#{app_path}/config/database.yml")
end
- def db_create_and_drop(expected_database)
+ def db_create_and_drop(expected_database, environment_loaded: true)
Dir.chdir(app_path) do
- output = `bin/rake db:create`
- assert_empty output
+ output = rails("db:create")
+ assert_match(/Created database/, output)
assert File.exist?(expected_database)
- assert_equal expected_database, ActiveRecord::Base.connection_config[:database]
- output = `bin/rake db:drop`
- assert_empty output
- assert !File.exist?(expected_database)
+ yield if block_given?
+ assert_equal expected_database, ActiveRecord::Base.connection_config[:database] if environment_loaded
+ output = rails("db:drop")
+ assert_match(/Dropped database/, output)
+ assert_not File.exist?(expected_database)
end
end
- test 'db:create and db:drop without database url' do
+ test "db:create and db:drop without database url" do
require "#{app_path}/config/environment"
- db_create_and_drop ActiveRecord::Base.configurations[Rails.env]['database']
+ db_create_and_drop ActiveRecord::Base.configurations[Rails.env]["database"]
end
- test 'db:create and db:drop with database url' do
+ test "db:create and db:drop with database url" do
require "#{app_path}/config/environment"
set_database_url
db_create_and_drop database_url_db_name
end
- def db_migrate_and_status(expected_database)
+ test "db:create and db:drop respect environment setting" do
+ app_file "config/database.yml", <<-YAML
+ development:
+ database: db/development.sqlite3
+ adapter: sqlite3
+ YAML
+
+ app_file "config/environments/development.rb", <<-RUBY
+ Rails.application.configure do
+ config.read_encrypted_secrets = true
+ end
+ RUBY
+
+ app_file "lib/tasks/check_env.rake", <<-RUBY
+ Rake::Task["db:create"].enhance do
+ File.write("tmp/config_value", Rails.application.config.read_encrypted_secrets)
+ end
+ RUBY
+
+ db_create_and_drop("db/development.sqlite3", environment_loaded: false) do
+ assert File.exist?("tmp/config_value")
+ assert_equal "true", File.read("tmp/config_value")
+ end
+ end
+
+ def with_database_existing
Dir.chdir(app_path) do
- `bin/rails generate model book title:string;
- bin/rake db:migrate`
- output = `bin/rake db:migrate:status`
- assert_match(%r{database:\s+\S*#{Regexp.escape(expected_database)}}, output)
- assert_match(/up\s+\d{14}\s+Create books/, output)
+ set_database_url
+ rails "db:create"
+ yield
+ rails "db:drop"
+ end
+ end
+
+ test "db:create failure because database exists" do
+ with_database_existing do
+ output = rails("db:create")
+ assert_match(/already exists/, output)
+ end
+ end
+
+ def with_bad_permissions
+ Dir.chdir(app_path) do
+ set_database_url
+ FileUtils.chmod("-w", "db")
+ yield
+ FileUtils.chmod("+w", "db")
+ end
+ end
+
+ test "db:create failure because bad permissions" do
+ with_bad_permissions do
+ output = rails("db:create", allow_failure: true)
+ assert_match("Couldn't create '#{database_url_db_name}' database. Please check your configuration.", output)
+ assert_equal 1, $?.exitstatus
end
end
- test 'db:migrate and db:migrate:status without database_url' do
+ test "db:create works when schema cache exists and database does not exist" do
+ use_postgresql
+
+ begin
+ rails %w(db:create db:migrate db:schema:cache:dump)
+
+ rails "db:drop"
+ rails "db:create"
+ assert_equal 0, $?.exitstatus
+ ensure
+ rails "db:drop" rescue nil
+ end
+ end
+
+ test "db:drop failure because database does not exist" do
+ output = rails("db:drop:_unsafe", "--trace")
+ assert_match(/does not exist/, output)
+ end
+
+ test "db:drop failure because bad permissions" do
+ with_database_existing do
+ with_bad_permissions do
+ output = rails("db:drop", allow_failure: true)
+ assert_match(/Couldn't drop/, output)
+ assert_equal 1, $?.exitstatus
+ end
+ end
+ end
+
+ def db_migrate_and_status(expected_database)
+ rails "generate", "model", "book", "title:string"
+ rails "db:migrate"
+ output = rails("db:migrate:status")
+ assert_match(%r{database:\s+\S*#{Regexp.escape(expected_database)}}, output)
+ assert_match(/up\s+\d{14}\s+Create books/, output)
+ end
+
+ test "db:migrate and db:migrate:status without database_url" do
require "#{app_path}/config/environment"
- db_migrate_and_status ActiveRecord::Base.configurations[Rails.env]['database']
+ db_migrate_and_status ActiveRecord::Base.configurations[Rails.env]["database"]
end
- test 'db:migrate and db:migrate:status with database_url' do
+ test "db:migrate and db:migrate:status with database_url" do
require "#{app_path}/config/environment"
set_database_url
db_migrate_and_status database_url_db_name
@@ -72,170 +158,176 @@ module ApplicationTests
def db_schema_dump
Dir.chdir(app_path) do
- `bin/rails generate model book title:string;
- bin/rake db:migrate db:schema:dump`
+ rails "generate", "model", "book", "title:string"
+ rails "db:migrate", "db:schema:dump"
schema_dump = File.read("db/schema.rb")
assert_match(/create_table \"books\"/, schema_dump)
end
end
- test 'db:schema:dump without database_url' do
+ test "db:schema:dump without database_url" do
db_schema_dump
end
- test 'db:schema:dump with database_url' do
+ test "db:schema:dump with database_url" do
set_database_url
db_schema_dump
end
def db_fixtures_load(expected_database)
Dir.chdir(app_path) do
- `bin/rails generate model book title:string;
- bin/rake db:migrate db:fixtures:load`
+ rails "generate", "model", "book", "title:string"
+ rails "db:migrate", "db:fixtures:load"
assert_match expected_database, ActiveRecord::Base.connection_config[:database]
require "#{app_path}/app/models/book"
assert_equal 2, Book.count
end
end
- test 'db:fixtures:load without database_url' do
+ test "db:fixtures:load without database_url" do
require "#{app_path}/config/environment"
- db_fixtures_load ActiveRecord::Base.configurations[Rails.env]['database']
+ db_fixtures_load ActiveRecord::Base.configurations[Rails.env]["database"]
end
- test 'db:fixtures:load with database_url' do
+ test "db:fixtures:load with database_url" do
require "#{app_path}/config/environment"
set_database_url
db_fixtures_load database_url_db_name
end
- test 'db:fixtures:load with namespaced fixture' do
+ test "db:fixtures:load with namespaced fixture" do
require "#{app_path}/config/environment"
- Dir.chdir(app_path) do
- `bin/rails generate model admin::book title:string;
- bin/rake db:migrate db:fixtures:load`
- require "#{app_path}/app/models/admin/book"
- assert_equal 2, Admin::Book.count
- end
+
+ rails "generate", "model", "admin::book", "title:string"
+ rails "db:migrate", "db:fixtures:load"
+ require "#{app_path}/app/models/admin/book"
+ assert_equal 2, Admin::Book.count
end
def db_structure_dump_and_load(expected_database)
Dir.chdir(app_path) do
- `bin/rails generate model book title:string;
- bin/rake db:migrate db:structure:dump`
+ rails "generate", "model", "book", "title:string"
+ rails "db:migrate", "db:structure:dump"
structure_dump = File.read("db/structure.sql")
- assert_match(/CREATE TABLE \"books\"/, structure_dump)
- `bin/rake environment db:drop db:structure:load`
+ assert_match(/CREATE TABLE (?:IF NOT EXISTS )?\"books\"/, structure_dump)
+ rails "environment", "db:drop", "db:structure:load"
assert_match expected_database, ActiveRecord::Base.connection_config[:database]
require "#{app_path}/app/models/book"
- #if structure is not loaded correctly, exception would be raised
+ # if structure is not loaded correctly, exception would be raised
assert_equal 0, Book.count
end
end
- test 'db:structure:dump and db:structure:load without database_url' do
+ test "db:structure:dump and db:structure:load without database_url" do
require "#{app_path}/config/environment"
- db_structure_dump_and_load ActiveRecord::Base.configurations[Rails.env]['database']
+ db_structure_dump_and_load ActiveRecord::Base.configurations[Rails.env]["database"]
end
- test 'db:structure:dump and db:structure:load with database_url' do
+ test "db:structure:dump and db:structure:load with database_url" do
require "#{app_path}/config/environment"
set_database_url
db_structure_dump_and_load database_url_db_name
end
- test 'db:structure:dump does not dump schema information when no migrations are used' do
- Dir.chdir(app_path) do
- # create table without migrations
- `bin/rails runner 'ActiveRecord::Base.connection.create_table(:posts) {|t| t.string :title }'`
+ test "db:structure:dump and db:structure:load set ar_internal_metadata" do
+ require "#{app_path}/config/environment"
+ db_structure_dump_and_load ActiveRecord::Base.configurations[Rails.env]["database"]
- stderr_output = capture(:stderr) { `bin/rake db:structure:dump` }
- assert_empty stderr_output
- structure_dump = File.read("db/structure.sql")
- assert_match(/CREATE TABLE \"posts\"/, structure_dump)
- end
+ assert_equal "test", rails("runner", "-e", "test", "puts ActiveRecord::InternalMetadata[:environment]").strip
+ assert_equal "development", rails("runner", "puts ActiveRecord::InternalMetadata[:environment]").strip
end
- test 'db:schema:load and db:structure:load do not purge the existing database' do
- Dir.chdir(app_path) do
- `bin/rails runner 'ActiveRecord::Base.connection.create_table(:posts) {|t| t.string :title }'`
+ test "db:structure:dump does not dump schema information when no migrations are used" do
+ # create table without migrations
+ rails "runner", "ActiveRecord::Base.connection.create_table(:posts) {|t| t.string :title }"
- app_file 'db/schema.rb', <<-RUBY
- ActiveRecord::Schema.define(version: 20140423102712) do
- create_table(:comments) {}
- end
- RUBY
+ stderr_output = capture(:stderr) { rails("db:structure:dump", stderr: true, allow_failure: true) }
+ assert_empty stderr_output
+ structure_dump = File.read("#{app_path}/db/structure.sql")
+ assert_match(/CREATE TABLE (?:IF NOT EXISTS )?\"posts\"/, structure_dump)
+ end
- list_tables = lambda { `bin/rails runner 'p ActiveRecord::Base.connection.tables'`.strip }
+ test "db:schema:load and db:structure:load do not purge the existing database" do
+ rails "runner", "ActiveRecord::Base.connection.create_table(:posts) {|t| t.string :title }"
- assert_equal '["posts"]', list_tables[]
- `bin/rake db:schema:load`
- assert_equal '["posts", "comments", "schema_migrations"]', list_tables[]
+ app_file "db/schema.rb", <<-RUBY
+ ActiveRecord::Schema.define(version: 20140423102712) do
+ create_table(:comments) {}
+ end
+ RUBY
- app_file 'db/structure.sql', <<-SQL
- CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(255));
- SQL
+ list_tables = lambda { rails("runner", "p ActiveRecord::Base.connection.tables").strip }
- `bin/rake db:structure:load`
- assert_equal '["posts", "comments", "schema_migrations", "users"]', list_tables[]
- end
+ assert_equal '["posts"]', list_tables[]
+ rails "db:schema:load"
+ assert_equal '["posts", "comments", "schema_migrations", "ar_internal_metadata"]', list_tables[]
+
+ app_file "db/structure.sql", <<-SQL
+ CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(255));
+ SQL
+
+ rails "db:structure:load"
+ assert_equal '["posts", "comments", "schema_migrations", "ar_internal_metadata", "users"]', list_tables[]
end
test "db:schema:load with inflections" do
- Dir.chdir(app_path) do
- app_file 'config/initializers/inflection.rb', <<-RUBY
- ActiveSupport::Inflector.inflections do |inflect|
- inflect.irregular 'goose', 'geese'
- end
- RUBY
- app_file 'config/initializers/primary_key_table_name.rb', <<-RUBY
- ActiveRecord::Base.primary_key_prefix_type = :table_name
- RUBY
- app_file 'db/schema.rb', <<-RUBY
- ActiveRecord::Schema.define(version: 20140423102712) do
- create_table("goose".pluralize) do |t|
- t.string :name
- end
+ app_file "config/initializers/inflection.rb", <<-RUBY
+ ActiveSupport::Inflector.inflections do |inflect|
+ inflect.irregular 'goose', 'geese'
+ end
+ RUBY
+ app_file "config/initializers/primary_key_table_name.rb", <<-RUBY
+ ActiveRecord::Base.primary_key_prefix_type = :table_name
+ RUBY
+ app_file "db/schema.rb", <<-RUBY
+ ActiveRecord::Schema.define(version: 20140423102712) do
+ create_table("goose".pluralize) do |t|
+ t.string :name
end
- RUBY
+ end
+ RUBY
- `bin/rake db:schema:load`
+ rails "db:schema:load"
- tables = `bin/rails runner 'p ActiveRecord::Base.connection.tables'`.strip
- assert_match(/"geese"/, tables)
+ tables = rails("runner", "p ActiveRecord::Base.connection.tables").strip
+ assert_match(/"geese"/, tables)
- columns = `bin/rails runner 'p ActiveRecord::Base.connection.columns("geese").map(&:name)'`.strip
- assert_equal columns, '["gooseid", "name"]'
- end
+ columns = rails("runner", "p ActiveRecord::Base.connection.columns('geese').map(&:name)").strip
+ assert_equal columns, '["gooseid", "name"]'
+ end
+
+ test "db:schema:load fails if schema.rb doesn't exist yet" do
+ stderr_output = capture(:stderr) { rails("db:schema:load", stderr: true, allow_failure: true) }
+ assert_match(/Run `rails db:migrate` to create it/, stderr_output)
end
def db_test_load_structure
Dir.chdir(app_path) do
- `bin/rails generate model book title:string;
- bin/rake db:migrate db:structure:dump db:test:load_structure`
+ rails "generate", "model", "book", "title:string"
+ rails "db:migrate", "db:structure:dump", "db:test:load_structure"
ActiveRecord::Base.configurations = Rails.application.config.database_configuration
ActiveRecord::Base.establish_connection :test
require "#{app_path}/app/models/book"
- #if structure is not loaded correctly, exception would be raised
+ # if structure is not loaded correctly, exception would be raised
assert_equal 0, Book.count
- assert_match ActiveRecord::Base.configurations['test']['database'],
+ assert_match ActiveRecord::Base.configurations["test"]["database"],
ActiveRecord::Base.connection_config[:database]
end
end
- test 'db:test:load_structure without database_url' do
+ test "db:test:load_structure without database_url" do
require "#{app_path}/config/environment"
db_test_load_structure
end
- test 'db:setup loads schema and seeds database' do
+ test "db:setup loads schema and seeds database" do
begin
@old_rails_env = ENV["RAILS_ENV"]
@old_rack_env = ENV["RACK_ENV"]
ENV.delete "RAILS_ENV"
ENV.delete "RACK_ENV"
- app_file 'db/schema.rb', <<-RUBY
+ app_file "db/schema.rb", <<-RUBY
ActiveRecord::Schema.define(version: "1") do
create_table :users do |t|
t.string :name
@@ -243,19 +335,56 @@ module ApplicationTests
end
RUBY
- app_file 'db/seeds.rb', <<-RUBY
+ app_file "db/seeds.rb", <<-RUBY
puts ActiveRecord::Base.connection_config[:database]
RUBY
- Dir.chdir(app_path) do
- database_path = `bin/rake db:setup`
- assert_equal "development.sqlite3", File.basename(database_path.strip)
- end
+ database_path = rails("db:setup")
+ assert_equal "development.sqlite3", File.basename(database_path.strip)
ensure
ENV["RAILS_ENV"] = @old_rails_env
ENV["RACK_ENV"] = @old_rack_env
end
end
+
+ test "db:setup sets ar_internal_metadata" do
+ app_file "db/schema.rb", ""
+ rails "db:setup"
+
+ test_environment = lambda { rails("runner", "-e", "test", "puts ActiveRecord::InternalMetadata[:environment]").strip }
+ development_environment = lambda { rails("runner", "puts ActiveRecord::InternalMetadata[:environment]").strip }
+
+ assert_equal "test", test_environment.call
+ assert_equal "development", development_environment.call
+
+ app_file "db/structure.sql", ""
+ app_file "config/initializers/enable_sql_schema_format.rb", <<-RUBY
+ Rails.application.config.active_record.schema_format = :sql
+ RUBY
+
+ rails "db:setup"
+
+ assert_equal "test", test_environment.call
+ assert_equal "development", development_environment.call
+ end
+
+ test "db:test:prepare sets test ar_internal_metadata" do
+ app_file "db/schema.rb", ""
+ rails "db:test:prepare"
+
+ test_environment = lambda { rails("runner", "-e", "test", "puts ActiveRecord::InternalMetadata[:environment]").strip }
+
+ assert_equal "test", test_environment.call
+
+ app_file "db/structure.sql", ""
+ app_file "config/initializers/enable_sql_schema_format.rb", <<-RUBY
+ Rails.application.config.active_record.schema_format = :sql
+ RUBY
+
+ rails "db:test:prepare"
+
+ assert_equal "test", test_environment.call
+ end
end
end
end
diff --git a/railties/test/application/rake/dev_test.rb b/railties/test/application/rake/dev_test.rb
new file mode 100644
index 0000000000..a87f453075
--- /dev/null
+++ b/railties/test/application/rake/dev_test.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+
+module ApplicationTests
+ module RakeTests
+ class RakeDevTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation
+
+ def setup
+ build_app
+ add_to_env_config("development", "config.active_support.deprecation = :stderr")
+ end
+
+ def teardown
+ teardown_app
+ end
+
+ test "dev:cache creates file and outputs message" do
+ Dir.chdir(app_path) do
+ stderr = capture(:stderr) do
+ output = run_rake_dev_cache
+ assert File.exist?("tmp/caching-dev.txt")
+ assert_match(/Development mode is now being cached/, output)
+ end
+ assert_match(/DEPRECATION WARNING: Using `bin\/rake dev:cache` is deprecated and will be removed in Rails 6.1/, stderr)
+ end
+ end
+
+ test "dev:cache deletes file and outputs message" do
+ Dir.chdir(app_path) do
+ stderr = capture(:stderr) do
+ run_rake_dev_cache # Create caching file.
+ output = run_rake_dev_cache # Delete caching file.
+ assert_not File.exist?("tmp/caching-dev.txt")
+ assert_match(/Development mode is no longer being cached/, output)
+ end
+ assert_match(/DEPRECATION WARNING: Using `bin\/rake dev:cache` is deprecated and will be removed in Rails 6.1/, stderr)
+ end
+ end
+
+ test "dev:cache touches tmp/restart.txt" do
+ Dir.chdir(app_path) do
+ stderr = capture(:stderr) do
+ run_rake_dev_cache
+ assert File.exist?("tmp/restart.txt")
+
+ prev_mtime = File.mtime("tmp/restart.txt")
+ run_rake_dev_cache
+ curr_mtime = File.mtime("tmp/restart.txt")
+ assert_not_equal prev_mtime, curr_mtime
+ end
+ assert_match(/DEPRECATION WARNING: Using `bin\/rake dev:cache` is deprecated and will be removed in Rails 6.1/, stderr)
+ end
+ end
+
+ private
+ def run_rake_dev_cache
+ `bin/rake dev:cache`
+ end
+ end
+ end
+end
diff --git a/railties/test/application/rake/framework_test.rb b/railties/test/application/rake/framework_test.rb
index ec57af79f6..644b1924b5 100644
--- a/railties/test/application/rake/framework_test.rb
+++ b/railties/test/application/rake/framework_test.rb
@@ -1,5 +1,6 @@
+# frozen_string_literal: true
+
require "isolation/abstract_unit"
-require "active_support/core_ext/string/strip"
module ApplicationTests
module RakeTests
@@ -8,7 +9,6 @@ module ApplicationTests
def setup
build_app
- boot_rails
FileUtils.rm_rf("#{app_path}/config/environments")
end
@@ -17,14 +17,14 @@ module ApplicationTests
end
def load_tasks
- require 'rake'
- require 'rdoc/task'
- require 'rake/testtask'
+ require "rake"
+ require "rdoc/task"
+ require "rake/testtask"
Rails.application.load_tasks
end
- test 'requiring the rake task should not define method .app_generator on Object' do
+ test "requiring the rake task should not define method .app_generator on Object" do
require "#{app_path}/config/environment"
load_tasks
@@ -34,7 +34,7 @@ module ApplicationTests
end
end
- test 'requiring the rake task should not define method .invoke_from_app_generator on Object' do
+ test "requiring the rake task should not define method .invoke_from_app_generator on Object" do
require "#{app_path}/config/environment"
load_tasks
diff --git a/railties/test/application/rake/initializers_test.rb b/railties/test/application/rake/initializers_test.rb
new file mode 100644
index 0000000000..8de4967021
--- /dev/null
+++ b/railties/test/application/rake/initializers_test.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+
+module ApplicationTests
+ module RakeTests
+ class RakeInitializersTest < ActiveSupport::TestCase
+ setup :build_app
+ teardown :teardown_app
+
+ test "`rake initializers` prints out defined initializers invoked by Rails" do
+ capture(:stderr) do
+ initial_output = run_rake_initializers
+ initial_output_length = initial_output.split("\n").length
+
+ assert_operator initial_output_length, :>, 0
+ assert_not initial_output.include?("set_added_test_module")
+
+ add_to_config <<-RUBY
+ initializer(:set_added_test_module) { }
+ RUBY
+
+ final_output = run_rake_initializers
+ final_output_length = final_output.split("\n").length
+
+ assert_equal 1, (final_output_length - initial_output_length)
+ assert final_output.include?("set_added_test_module")
+ end
+ end
+
+ test "`rake initializers` outputs a deprecation warning" do
+ add_to_env_config("development", "config.active_support.deprecation = :stderr")
+
+ stderr = capture(:stderr) { run_rake_initializers }
+ assert_match(/DEPRECATION WARNING: Using `bin\/rake initializers` is deprecated and will be removed in Rails 6.1/, stderr)
+ end
+
+ private
+ def run_rake_initializers
+ Dir.chdir(app_path) { `bin/rake initializers` }
+ end
+ end
+ end
+end
diff --git a/railties/test/application/rake/log_test.rb b/railties/test/application/rake/log_test.rb
new file mode 100644
index 0000000000..678f26db26
--- /dev/null
+++ b/railties/test/application/rake/log_test.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+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 2d8bd7c571..47c5ac105a 100644
--- a/railties/test/application/rake/migrations_test.rb
+++ b/railties/test/application/rake/migrations_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "isolation/abstract_unit"
module ApplicationTests
@@ -5,7 +7,6 @@ module ApplicationTests
class RakeMigrationsTest < ActiveSupport::TestCase
def setup
build_app
- boot_rails
FileUtils.rm_rf("#{app_path}/config/environments")
end
@@ -13,189 +14,441 @@ module ApplicationTests
teardown_app
end
- test 'running migrations with given scope' do
- Dir.chdir(app_path) do
- `bin/rails generate model user username:string password:string`
+ test "running migrations with given scope" do
+ rails "generate", "model", "user", "username:string", "password:string"
- app_file "db/migrate/01_a_migration.bukkits.rb", <<-MIGRATION
- class AMigration < ActiveRecord::Migration
- end
- MIGRATION
+ app_file "db/migrate/01_a_migration.bukkits.rb", <<-MIGRATION
+ class AMigration < ActiveRecord::Migration::Current
+ end
+ MIGRATION
- output = `bin/rake db:migrate SCOPE=bukkits`
- assert_no_match(/create_table\(:users\)/, output)
- assert_no_match(/CreateUsers/, output)
- assert_no_match(/add_column\(:users, :email, :string\)/, output)
+ output = rails("db:migrate", "SCOPE=bukkits")
+ assert_no_match(/create_table\(:users\)/, output)
+ assert_no_match(/CreateUsers/, output)
+ assert_no_match(/add_column\(:users, :email, :string\)/, output)
- assert_match(/AMigration: migrated/, output)
+ assert_match(/AMigration: migrated/, output)
- output = `bin/rake db:migrate SCOPE=bukkits VERSION=0`
- assert_no_match(/drop_table\(:users\)/, output)
- assert_no_match(/CreateUsers/, output)
- assert_no_match(/remove_column\(:users, :email\)/, output)
+ # run all the migrations to test scope for down
+ output = rails("db:migrate")
+ assert_match(/CreateUsers: migrated/, output)
- assert_match(/AMigration: reverted/, output)
- end
+ output = rails("db:migrate", "SCOPE=bukkits", "VERSION=0")
+ assert_no_match(/drop_table\(:users\)/, output)
+ assert_no_match(/CreateUsers/, output)
+ assert_no_match(/remove_column\(:users, :email\)/, output)
+
+ assert_match(/AMigration: reverted/, output)
+
+ output = rails("db:migrate", "VERSION=0")
+
+ assert_match(/CreateUsers: reverted/, output)
end
- test 'model and migration generator with change syntax' do
- Dir.chdir(app_path) do
- `bin/rails generate model user username:string password:string;
- bin/rails generate migration add_email_to_users email:string`
-
- output = `bin/rake db:migrate`
- assert_match(/create_table\(:users\)/, output)
- assert_match(/CreateUsers: migrated/, output)
- assert_match(/add_column\(:users, :email, :string\)/, output)
- assert_match(/AddEmailToUsers: migrated/, output)
-
- output = `bin/rake db:rollback STEP=2`
- assert_match(/drop_table\(:users\)/, output)
- assert_match(/CreateUsers: reverted/, output)
- assert_match(/remove_column\(:users, :email, :string\)/, output)
- assert_match(/AddEmailToUsers: reverted/, output)
- end
+ test "version outputs current version" do
+ app_file "db/migrate/01_one_migration.rb", <<-MIGRATION
+ class OneMigration < ActiveRecord::Migration::Current
+ end
+ MIGRATION
+
+ rails "db:migrate"
+
+ output = rails("db:version")
+ assert_match(/Current version: 1/, output)
end
- test 'migration status when schema migrations table is not present' do
- output = Dir.chdir(app_path){ `bin/rake db:migrate:status 2>&1` }
+ test "migrate with specified VERSION in different formats" do
+ app_file "db/migrate/01_one_migration.rb", <<-MIGRATION
+ class OneMigration < ActiveRecord::Migration::Current
+ end
+ MIGRATION
+
+ app_file "db/migrate/02_two_migration.rb", <<-MIGRATION
+ class TwoMigration < ActiveRecord::Migration::Current
+ end
+ MIGRATION
+
+ app_file "db/migrate/03_three_migration.rb", <<-MIGRATION
+ class ThreeMigration < ActiveRecord::Migration::Current
+ end
+ MIGRATION
+
+ rails "db:migrate"
+
+ output = rails("db:migrate:status")
+ assert_match(/up\s+001\s+One migration/, output)
+ assert_match(/up\s+002\s+Two migration/, output)
+ assert_match(/up\s+003\s+Three migration/, output)
+
+ rails "db:migrate", "VERSION=01_one_migration.rb"
+ output = rails("db:migrate:status")
+ assert_match(/up\s+001\s+One migration/, output)
+ assert_match(/down\s+002\s+Two migration/, output)
+ assert_match(/down\s+003\s+Three migration/, output)
+
+ rails "db:migrate", "VERSION=3"
+ output = rails("db:migrate:status")
+ assert_match(/up\s+001\s+One migration/, output)
+ assert_match(/up\s+002\s+Two migration/, output)
+ assert_match(/up\s+003\s+Three migration/, output)
+
+ rails "db:migrate", "VERSION=001"
+ output = rails("db:migrate:status")
+ assert_match(/up\s+001\s+One migration/, output)
+ assert_match(/down\s+002\s+Two migration/, output)
+ assert_match(/down\s+003\s+Three migration/, output)
+ end
+
+ test "migration with empty version" do
+ app_file "db/migrate/01_one_migration.rb", <<-MIGRATION
+ class OneMigration < ActiveRecord::Migration::Current
+ end
+ MIGRATION
+
+ app_file "db/migrate/02_two_migration.rb", <<-MIGRATION
+ class TwoMigration < ActiveRecord::Migration::Current
+ end
+ MIGRATION
+
+ rails("db:migrate", "VERSION=")
+
+ output = rails("db:migrate:status")
+ assert_match(/up\s+001\s+One migration/, output)
+ assert_match(/up\s+002\s+Two migration/, output)
+
+ output = rails("db:migrate:redo", "VERSION=", allow_failure: true)
+ assert_match(/Empty VERSION provided/, output)
+
+ output = rails("db:migrate:up", "VERSION=", allow_failure: true)
+ assert_match(/VERSION is required/, output)
+
+ output = rails("db:migrate:up", allow_failure: true)
+ assert_match(/VERSION is required/, output)
+
+ output = rails("db:migrate:down", "VERSION=", allow_failure: true)
+ assert_match(/VERSION is required - To go down one migration, use db:rollback/, output)
+
+ output = rails("db:migrate:down", allow_failure: true)
+ assert_match(/VERSION is required - To go down one migration, use db:rollback/, output)
+
+ output = rails("db:migrate:status")
+ assert_match(/up\s+001\s+One migration/, output)
+ assert_match(/up\s+002\s+Two migration/, output)
+ end
+
+ test "migration with 0 version" do
+ app_file "db/migrate/01_one_migration.rb", <<-MIGRATION
+ class OneMigration < ActiveRecord::Migration::Current
+ end
+ MIGRATION
+
+ app_file "db/migrate/02_two_migration.rb", <<-MIGRATION
+ class TwoMigration < ActiveRecord::Migration::Current
+ end
+ MIGRATION
+
+ rails "db:migrate"
+
+ output = rails("db:migrate:status")
+ assert_match(/up\s+001\s+One migration/, output)
+ assert_match(/up\s+002\s+Two migration/, output)
+
+ rails "db:migrate", "VERSION=0"
+
+ output = rails("db:migrate:status")
+ assert_match(/down\s+001\s+One migration/, output)
+ assert_match(/down\s+002\s+Two migration/, output)
+ end
+
+ test "model and migration generator with change syntax" do
+ rails "generate", "model", "user", "username:string", "password:string"
+ rails "generate", "migration", "add_email_to_users", "email:string"
+
+ output = rails("db:migrate")
+ assert_match(/create_table\(:users\)/, output)
+ assert_match(/CreateUsers: migrated/, output)
+ assert_match(/add_column\(:users, :email, :string\)/, output)
+ assert_match(/AddEmailToUsers: migrated/, output)
+
+ output = rails("db:rollback", "STEP=2")
+ assert_match(/drop_table\(:users\)/, output)
+ assert_match(/CreateUsers: reverted/, output)
+ assert_match(/remove_column\(:users, :email, :string\)/, output)
+ assert_match(/AddEmailToUsers: reverted/, output)
+ end
+
+ test "migration status when schema migrations table is not present" do
+ output = rails("db:migrate:status", allow_failure: true)
assert_equal "Schema migrations table does not exist yet.\n", output
end
- test 'test migration status' do
+ test "migration status" do
+ rails "generate", "model", "user", "username:string", "password:string"
+ rails "generate", "migration", "add_email_to_users", "email:string"
+ rails "db:migrate"
+
+ output = rails("db:migrate:status")
+
+ assert_match(/up\s+\d{14}\s+Create users/, output)
+ assert_match(/up\s+\d{14}\s+Add email to users/, output)
+
+ rails "db:rollback", "STEP=1"
+ output = rails("db:migrate:status")
+
+ assert_match(/up\s+\d{14}\s+Create users/, output)
+ assert_match(/down\s+\d{14}\s+Add email to users/, output)
+ end
+
+ test "migration status without timestamps" do
+ add_to_config("config.active_record.timestamped_migrations = false")
+
+ rails "generate", "model", "user", "username:string", "password:string"
+ rails "generate", "migration", "add_email_to_users", "email:string"
+ rails "db:migrate"
+
+ output = rails("db:migrate:status")
+
+ assert_match(/up\s+\d{3,}\s+Create users/, output)
+ assert_match(/up\s+\d{3,}\s+Add email to users/, output)
+
+ rails "db:rollback", "STEP=1"
+ output = rails("db:migrate:status")
+
+ assert_match(/up\s+\d{3,}\s+Create users/, output)
+ assert_match(/down\s+\d{3,}\s+Add email to users/, output)
+ end
+
+ test "migration status after rollback and redo" do
+ rails "generate", "model", "user", "username:string", "password:string"
+ rails "generate", "migration", "add_email_to_users", "email:string"
+ rails "db:migrate"
+
+ output = rails("db:migrate:status")
+
+ assert_match(/up\s+\d{14}\s+Create users/, output)
+ assert_match(/up\s+\d{14}\s+Add email to users/, output)
+
+ rails "db:rollback", "STEP=2"
+ output = rails("db:migrate:status")
+
+ assert_match(/down\s+\d{14}\s+Create users/, output)
+ assert_match(/down\s+\d{14}\s+Add email to users/, output)
+
+ rails "db:migrate:redo"
+ output = rails("db:migrate:status")
+
+ assert_match(/up\s+\d{14}\s+Create users/, output)
+ assert_match(/up\s+\d{14}\s+Add email to users/, output)
+ end
+
+ test "migration status after rollback and forward" do
+ rails "generate", "model", "user", "username:string", "password:string"
+ rails "generate", "migration", "add_email_to_users", "email:string"
+ rails "db:migrate"
+
+ output = rails("db:migrate:status")
+
+ assert_match(/up\s+\d{14}\s+Create users/, output)
+ assert_match(/up\s+\d{14}\s+Add email to users/, output)
+
+ rails "db:rollback", "STEP=2"
+ output = rails("db:migrate:status")
+
+ assert_match(/down\s+\d{14}\s+Create users/, output)
+ assert_match(/down\s+\d{14}\s+Add email to users/, output)
+
+ rails "db:forward", "STEP=2"
+ output = rails("db:migrate:status")
+
+ assert_match(/up\s+\d{14}\s+Create users/, output)
+ assert_match(/up\s+\d{14}\s+Add email to users/, output)
+ end
+
+ test "raise error on any move when current migration does not exist" do
Dir.chdir(app_path) do
- `bin/rails generate model user username:string password:string;
- bin/rails generate migration add_email_to_users email:string;
- bin/rake db:migrate`
+ rails "generate", "model", "user", "username:string", "password:string"
+ rails "generate", "migration", "add_email_to_users", "email:string"
+ rails "db:migrate"
+ `rm db/migrate/*email*.rb`
- output = `bin/rake db:migrate:status`
+ output = rails("db:migrate:status")
+ assert_match(/up\s+\d{14}\s+Create users/, output)
+ assert_match(/up\s+\d{14}\s+\** NO FILE \**/, output)
+
+ output = rails("db:rollback", allow_failure: true)
+ assert_match(/rails aborted!/, output)
+ assert_match(/ActiveRecord::UnknownMigrationVersionError:/, output)
+ assert_match(/No migration with version number\s\d{14}\./, output)
+ output = rails("db:migrate:status")
assert_match(/up\s+\d{14}\s+Create users/, output)
- assert_match(/up\s+\d{14}\s+Add email to users/, output)
+ assert_match(/up\s+\d{14}\s+\** NO FILE \**/, output)
- `bin/rake db:rollback STEP=1`
- output = `bin/rake db:migrate:status`
+ output = rails("db:forward", allow_failure: true)
+ assert_match(/rails aborted!/, output)
+ assert_match(/ActiveRecord::UnknownMigrationVersionError:/, output)
+ assert_match(/No migration with version number\s\d{14}\./, output)
+ output = rails("db:migrate:status")
assert_match(/up\s+\d{14}\s+Create users/, output)
- assert_match(/down\s+\d{14}\s+Add email to users/, output)
+ assert_match(/up\s+\d{14}\s+\** NO FILE \**/, output)
end
end
- test 'migration status without timestamps' do
- add_to_config('config.active_record.timestamped_migrations = false')
+ test "raise error on any move when target migration does not exist" do
+ app_file "db/migrate/01_one_migration.rb", <<-MIGRATION
+ class OneMigration < ActiveRecord::Migration::Current
+ end
+ MIGRATION
- Dir.chdir(app_path) do
- `bin/rails generate model user username:string password:string;
- bin/rails generate migration add_email_to_users email:string;
- bin/rake db:migrate`
+ app_file "db/migrate/02_two_migration.rb", <<-MIGRATION
+ class TwoMigration < ActiveRecord::Migration::Current
+ end
+ MIGRATION
- output = `bin/rake db:migrate:status`
+ rails "db:migrate"
- assert_match(/up\s+\d{3,}\s+Create users/, output)
- assert_match(/up\s+\d{3,}\s+Add email to users/, output)
+ output = rails("db:migrate:status")
+ assert_match(/up\s+001\s+One migration/, output)
+ assert_match(/up\s+002\s+Two migration/, output)
- `bin/rake db:rollback STEP=1`
- output = `bin/rake db:migrate:status`
+ output = rails("db:migrate", "VERSION=3", allow_failure: true)
+ assert_match(/rails aborted!/, output)
+ assert_match(/ActiveRecord::UnknownMigrationVersionError:/, output)
+ assert_match(/No migration with version number 3/, output)
- assert_match(/up\s+\d{3,}\s+Create users/, output)
- assert_match(/down\s+\d{3,}\s+Add email to users/, output)
- end
+ output = rails("db:migrate:status")
+ assert_match(/up\s+001\s+One migration/, output)
+ assert_match(/up\s+002\s+Two migration/, output)
end
- test 'test migration status after rollback and redo' do
- Dir.chdir(app_path) do
- `bin/rails generate model user username:string password:string;
- bin/rails generate migration add_email_to_users email:string;
- bin/rake db:migrate`
+ test "raise error on any move when VERSION has invalid format" do
+ output = rails("db:migrate", "VERSION=unknown", allow_failure: true)
+ assert_match(/rails aborted!/, output)
+ assert_match(/Invalid format of target version/, output)
- output = `bin/rake db:migrate:status`
+ output = rails("db:migrate", "VERSION=0.1.11", allow_failure: true)
+ assert_match(/rails aborted!/, output)
+ assert_match(/Invalid format of target version/, output)
- assert_match(/up\s+\d{14}\s+Create users/, output)
- assert_match(/up\s+\d{14}\s+Add email to users/, output)
+ output = rails("db:migrate", "VERSION=1.1.11", allow_failure: true)
+ assert_match(/rails aborted!/, output)
+ assert_match(/Invalid format of target version/, output)
- `bin/rake db:rollback STEP=2`
- output = `bin/rake db:migrate:status`
+ output = rails("db:migrate", "VERSION='0 '", allow_failure: true)
+ assert_match(/rails aborted!/, output)
+ assert_match(/Invalid format of target version/, output)
- assert_match(/down\s+\d{14}\s+Create users/, output)
- assert_match(/down\s+\d{14}\s+Add email to users/, output)
+ output = rails("db:migrate", "VERSION=1.", allow_failure: true)
+ assert_match(/rails aborted!/, output)
+ assert_match(/Invalid format of target version/, output)
- `bin/rake db:migrate:redo`
- output = `bin/rake db:migrate:status`
+ output = rails("db:migrate", "VERSION=1_", allow_failure: true)
+ assert_match(/rails aborted!/, output)
+ assert_match(/Invalid format of target version/, output)
- assert_match(/up\s+\d{14}\s+Create users/, output)
- assert_match(/up\s+\d{14}\s+Add email to users/, output)
- end
+ output = rails("db:migrate", "VERSION=1_name", allow_failure: true)
+ assert_match(/rails aborted!/, output)
+ assert_match(/Invalid format of target version/, output)
+
+ output = rails("db:migrate:redo", "VERSION=unknown", allow_failure: true)
+ assert_match(/rails aborted!/, output)
+ assert_match(/Invalid format of target version/, output)
+
+ output = rails("db:migrate:up", "VERSION=unknown", allow_failure: true)
+ assert_match(/rails aborted!/, output)
+ assert_match(/Invalid format of target version/, output)
+
+ output = rails("db:migrate:down", "VERSION=unknown", allow_failure: true)
+ assert_match(/rails aborted!/, output)
+ assert_match(/Invalid format of target version/, output)
end
- test 'migration status after rollback and redo without timestamps' do
- add_to_config('config.active_record.timestamped_migrations = false')
+ test "migration status after rollback and redo without timestamps" do
+ add_to_config("config.active_record.timestamped_migrations = false")
- Dir.chdir(app_path) do
- `bin/rails generate model user username:string password:string;
- bin/rails generate migration add_email_to_users email:string;
- bin/rake db:migrate`
+ rails "generate", "model", "user", "username:string", "password:string"
+ rails "generate", "migration", "add_email_to_users", "email:string"
+ rails "db:migrate"
- output = `bin/rake db:migrate:status`
+ output = rails("db:migrate:status")
- assert_match(/up\s+\d{3,}\s+Create users/, output)
- assert_match(/up\s+\d{3,}\s+Add email to users/, output)
+ assert_match(/up\s+\d{3,}\s+Create users/, output)
+ assert_match(/up\s+\d{3,}\s+Add email to users/, output)
- `bin/rake db:rollback STEP=2`
- output = `bin/rake db:migrate:status`
+ rails "db:rollback", "STEP=2"
+ output = rails("db:migrate:status")
- assert_match(/down\s+\d{3,}\s+Create users/, output)
- assert_match(/down\s+\d{3,}\s+Add email to users/, output)
+ assert_match(/down\s+\d{3,}\s+Create users/, output)
+ assert_match(/down\s+\d{3,}\s+Add email to users/, output)
- `bin/rake db:migrate:redo`
- output = `bin/rake db:migrate:status`
+ rails "db:migrate:redo"
+ output = rails("db:migrate:status")
- assert_match(/up\s+\d{3,}\s+Create users/, output)
- assert_match(/up\s+\d{3,}\s+Add email to users/, output)
- end
+ assert_match(/up\s+\d{3,}\s+Create users/, output)
+ assert_match(/up\s+\d{3,}\s+Add email to users/, output)
+ end
+
+ test "running migrations with not timestamp head migration files" do
+ app_file "db/migrate/1_one_migration.rb", <<-MIGRATION
+ class OneMigration < ActiveRecord::Migration::Current
+ end
+ MIGRATION
+
+ app_file "db/migrate/02_two_migration.rb", <<-MIGRATION
+ class TwoMigration < ActiveRecord::Migration::Current
+ end
+ MIGRATION
+
+ rails "db:migrate"
+
+ output = rails("db:migrate:status")
+
+ assert_match(/up\s+001\s+One migration/, output)
+ assert_match(/up\s+002\s+Two migration/, output)
end
- test 'schema generation when dump_schema_after_migration is set' do
- add_to_config('config.active_record.dump_schema_after_migration = false')
+ test "schema generation when dump_schema_after_migration is set" do
+ add_to_config("config.active_record.dump_schema_after_migration = false")
Dir.chdir(app_path) do
- `bin/rails generate model book title:string`
- output = `bin/rails generate model author name:string`
+ rails "generate", "model", "book", "title:string"
+ output = rails("generate", "model", "author", "name:string")
version = output =~ %r{[^/]+db/migrate/(\d+)_create_authors\.rb} && $1
- `bin/rake db:migrate db:rollback db:forward db:migrate:up db:migrate:down VERSION=#{version}`
- assert !File.exist?("db/schema.rb"), "should not dump schema when configured not to"
+ rails "db:migrate", "db:rollback", "db:forward", "db:migrate:up", "db:migrate:down", "VERSION=#{version}"
+ assert_not File.exist?("db/schema.rb"), "should not dump schema when configured not to"
end
- add_to_config('config.active_record.dump_schema_after_migration = true')
+ add_to_config("config.active_record.dump_schema_after_migration = true")
Dir.chdir(app_path) do
- `bin/rails generate model reviews book_id:integer`
- `bin/rake db:migrate`
+ rails "generate", "model", "reviews", "book_id:integer"
+ rails "db:migrate"
structure_dump = File.read("db/schema.rb")
assert_match(/create_table "reviews"/, structure_dump)
end
end
- test 'default schema generation after migration' do
+ test "default schema generation after migration" do
Dir.chdir(app_path) do
- `bin/rails generate model book title:string;
- bin/rake db:migrate`
+ rails "generate", "model", "book", "title:string"
+ rails "db:migrate"
structure_dump = File.read("db/schema.rb")
assert_match(/create_table "books"/, structure_dump)
end
end
- test 'test migration status migrated file is deleted' do
+ test "migration status migrated file is deleted" do
Dir.chdir(app_path) do
- `bin/rails generate model user username:string password:string;
- bin/rails generate migration add_email_to_users email:string;
- bin/rake db:migrate
- rm db/migrate/*email*.rb`
+ rails "generate", "model", "user", "username:string", "password:string"
+ rails "generate", "migration", "add_email_to_users", "email:string"
+ rails "db:migrate"
+ `rm db/migrate/*email*.rb`
- output = `bin/rake db:migrate:status`
- File.write('test.txt', output)
+ output = rails("db:migrate:status")
assert_match(/up\s+\d{14}\s+Create users/, output)
assert_match(/up\s+\d{14}\s+\** NO FILE \**/, output)
diff --git a/railties/test/application/rake/multi_dbs_test.rb b/railties/test/application/rake/multi_dbs_test.rb
new file mode 100644
index 0000000000..ef99365e75
--- /dev/null
+++ b/railties/test/application/rake/multi_dbs_test.rb
@@ -0,0 +1,230 @@
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+
+module ApplicationTests
+ module RakeTests
+ class RakeMultiDbsTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation
+
+ def setup
+ build_app(multi_db: true)
+ FileUtils.rm_rf("#{app_path}/config/environments")
+ end
+
+ def teardown
+ teardown_app
+ end
+
+ def db_create_and_drop(namespace, expected_database)
+ Dir.chdir(app_path) do
+ output = rails("db:create")
+ assert_match(/Created database/, output)
+ assert_match_namespace(namespace, output)
+ assert_no_match(/already exists/, output)
+ assert File.exist?(expected_database)
+
+
+ output = rails("db:drop")
+ assert_match(/Dropped database/, output)
+ assert_match_namespace(namespace, output)
+ assert_no_match(/does not exist/, output)
+ assert_not File.exist?(expected_database)
+ end
+ end
+
+ def db_create_and_drop_namespace(namespace, expected_database)
+ Dir.chdir(app_path) do
+ output = rails("db:create:#{namespace}")
+ assert_match(/Created database/, output)
+ assert_match_namespace(namespace, output)
+ assert File.exist?(expected_database)
+
+ output = rails("db:drop:#{namespace}")
+ assert_match(/Dropped database/, output)
+ assert_match_namespace(namespace, output)
+ assert_not File.exist?(expected_database)
+ end
+ end
+
+ def assert_match_namespace(namespace, output)
+ if namespace == "primary"
+ assert_match(/#{Rails.env}.sqlite3/, output)
+ else
+ assert_match(/#{Rails.env}_#{namespace}.sqlite3/, output)
+ end
+ end
+
+ def db_migrate_and_migrate_status
+ Dir.chdir(app_path) do
+ generate_models_for_animals
+ rails "db:migrate"
+ output = rails "db:migrate:status"
+ assert_match(/up \d+ Create books/, output)
+ assert_match(/up \d+ Create dogs/, output)
+ end
+ end
+
+ def db_migrate_and_schema_cache_dump
+ Dir.chdir(app_path) do
+ generate_models_for_animals
+ rails "db:migrate"
+ rails "db:schema:cache:dump"
+ assert File.exist?("db/schema_cache.yml")
+ assert File.exist?("db/animals_schema_cache.yml")
+ end
+ end
+
+ def db_migrate_and_schema_cache_dump_and_schema_cache_clear
+ Dir.chdir(app_path) do
+ generate_models_for_animals
+ rails "db:migrate"
+ rails "db:schema:cache:dump"
+ rails "db:schema:cache:clear"
+ assert_not File.exist?("db/schema_cache.yml")
+ assert_not File.exist?("db/animals_schema_cache.yml")
+ end
+ end
+
+ def db_migrate_and_schema_dump_and_load(format)
+ Dir.chdir(app_path) do
+ generate_models_for_animals
+ rails "db:migrate", "db:#{format}:dump"
+
+ if format == "schema"
+ schema_dump = File.read("db/#{format}.rb")
+ schema_dump_animals = File.read("db/animals_#{format}.rb")
+ assert_match(/create_table \"books\"/, schema_dump)
+ assert_match(/create_table \"dogs\"/, schema_dump_animals)
+ else
+ schema_dump = File.read("db/#{format}.sql")
+ schema_dump_animals = File.read("db/animals_#{format}.sql")
+ assert_match(/CREATE TABLE (?:IF NOT EXISTS )?\"books\"/, schema_dump)
+ assert_match(/CREATE TABLE (?:IF NOT EXISTS )?\"dogs\"/, schema_dump_animals)
+ end
+
+ rails "db:#{format}:load"
+
+ ar_tables = lambda { rails("runner", "p ActiveRecord::Base.connection.tables").strip }
+ animals_tables = lambda { rails("runner", "p AnimalsBase.connection.tables").strip }
+
+ assert_equal '["schema_migrations", "ar_internal_metadata", "books"]', ar_tables[]
+ assert_equal '["schema_migrations", "ar_internal_metadata", "dogs"]', animals_tables[]
+ end
+ end
+
+ def db_migrate_namespaced(namespace)
+ Dir.chdir(app_path) do
+ generate_models_for_animals
+ output = rails("db:migrate:#{namespace}")
+ if namespace == "primary"
+ assert_match(/CreateBooks: migrated/, output)
+ else
+ assert_match(/CreateDogs: migrated/, output)
+ end
+ end
+ end
+
+ def db_migrate_status_namespaced(namespace)
+ Dir.chdir(app_path) do
+ generate_models_for_animals
+ output = rails("db:migrate:status:#{namespace}")
+ if namespace == "primary"
+ assert_match(/up \d+ Create books/, output)
+ else
+ assert_match(/up \d+ Create dogs/, output)
+ end
+ end
+ end
+
+ def write_models_for_animals
+ # make a directory for the animals migration
+ FileUtils.mkdir_p("#{app_path}/db/animals_migrate")
+ # move the dogs migration if it unless it already lives there
+ FileUtils.mv(Dir.glob("#{app_path}/db/migrate/**/*dogs.rb").first, "db/animals_migrate/") unless Dir.glob("#{app_path}/db/animals_migrate/**/*dogs.rb").first
+ # delete the dogs migration if it's still present in the
+ # migrate folder. This is necessary because sometimes
+ # the code isn't fast enough and an extra migration gets made
+ FileUtils.rm(Dir.glob("#{app_path}/db/migrate/**/*dogs.rb").first) if Dir.glob("#{app_path}/db/migrate/**/*dogs.rb").first
+
+ # change the base of the dog model
+ app_path("/app/models/dog.rb") do |file_name|
+ file = File.read("#{app_path}/app/models/dog.rb")
+ file.sub!(/ApplicationRecord/, "AnimalsBase")
+ File.write(file_name, file)
+ end
+
+ # create the base model for dog to inherit from
+ File.open("#{app_path}/app/models/animals_base.rb", "w") do |file|
+ file.write(<<~EOS)
+ class AnimalsBase < ActiveRecord::Base
+ self.abstract_class = true
+
+ establish_connection :animals
+ end
+ EOS
+ end
+ end
+
+ def generate_models_for_animals
+ rails "generate", "model", "book", "title:string"
+ rails "generate", "model", "dog", "name:string"
+ write_models_for_animals
+ end
+
+ test "db:create and db:drop works on all databases for env" do
+ require "#{app_path}/config/environment"
+ ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
+ db_create_and_drop db_config.spec_name, db_config.config["database"]
+ end
+ end
+
+ test "db:create:namespace and db:drop:namespace works on specified databases" do
+ require "#{app_path}/config/environment"
+ ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
+ db_create_and_drop_namespace db_config.spec_name, db_config.config["database"]
+ end
+ end
+
+ test "db:migrate and db:schema:dump and db:schema:load works on all databases" do
+ require "#{app_path}/config/environment"
+ db_migrate_and_schema_dump_and_load "schema"
+ end
+
+ test "db:migrate and db:structure:dump and db:structure:load works on all databases" do
+ require "#{app_path}/config/environment"
+ db_migrate_and_schema_dump_and_load "structure"
+ end
+
+ test "db:migrate:namespace works" do
+ require "#{app_path}/config/environment"
+ ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
+ db_migrate_namespaced db_config.spec_name
+ end
+ end
+
+ test "db:migrate:status works on all databases" do
+ require "#{app_path}/config/environment"
+ db_migrate_and_migrate_status
+ end
+
+ test "db:migrate:status:namespace works" do
+ require "#{app_path}/config/environment"
+ ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
+ db_migrate_namespaced db_config.spec_name
+ db_migrate_status_namespaced db_config.spec_name
+ end
+ end
+
+ test "db:schema:cache:dump works on all databases" do
+ require "#{app_path}/config/environment"
+ db_migrate_and_schema_cache_dump
+ end
+
+ test "db:schema:cache:clear works on all databases" do
+ require "#{app_path}/config/environment"
+ db_migrate_and_schema_cache_dump_and_schema_cache_clear
+ end
+ end
+ end
+end
diff --git a/railties/test/application/rake/notes_test.rb b/railties/test/application/rake/notes_test.rb
index d6f5fee5c3..60802ef7c4 100644
--- a/railties/test/application/rake/notes_test.rb
+++ b/railties/test/application/rake/notes_test.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
require "isolation/abstract_unit"
-require 'rails/source_annotation_extractor'
+require "rails/source_annotation_extractor"
module ApplicationTests
module RakeTests
@@ -8,6 +10,7 @@ module ApplicationTests
def setup
build_app
+ add_to_env_config("development", "config.active_support.deprecation = :stderr")
require "rails/all"
super
end
@@ -17,33 +20,36 @@ module ApplicationTests
teardown_app
end
- test 'notes finds notes for certain file_types' do
+ test "notes finds notes for certain file_types" do
app_file "app/views/home/index.html.erb", "<% # TODO: note in erb %>"
app_file "app/assets/javascripts/application.js", "// TODO: note in js"
app_file "app/assets/stylesheets/application.css", "// TODO: note in css"
app_file "app/controllers/application_controller.rb", 1000.times.map { "" }.join("\n") << "# TODO: note in ruby"
app_file "lib/tasks/task.rake", "# TODO: note in rake"
- app_file 'app/views/home/index.html.builder', '# TODO: note in builder'
- app_file 'config/locales/en.yml', '# TODO: note in yml'
- app_file 'config/locales/en.yaml', '# TODO: note in yaml'
+ app_file "app/views/home/index.html.builder", "# TODO: note in builder"
+ app_file "config/locales/en.yml", "# TODO: note in yml"
+ app_file "config/locales/en.yaml", "# TODO: note in yaml"
app_file "app/views/home/index.ruby", "# TODO: note in ruby"
- run_rake_notes do |output, lines|
- assert_match(/note in erb/, output)
- assert_match(/note in js/, output)
- assert_match(/note in css/, output)
- assert_match(/note in rake/, output)
- assert_match(/note in builder/, output)
- assert_match(/note in yml/, output)
- assert_match(/note in yaml/, output)
- assert_match(/note in ruby/, output)
-
- assert_equal 9, lines.size
- assert_equal [4], lines.map(&:size).uniq
+ stderr = capture(:stderr) do
+ run_rake_notes do |output, lines|
+ assert_match(/note in erb/, output)
+ assert_match(/note in js/, output)
+ assert_match(/note in css/, output)
+ assert_match(/note in rake/, output)
+ assert_match(/note in builder/, output)
+ assert_match(/note in yml/, output)
+ assert_match(/note in yaml/, output)
+ assert_match(/note in ruby/, output)
+
+ assert_equal 9, lines.size
+ assert_equal [4], lines.map(&:size).uniq
+ end
end
+ assert_match(/DEPRECATION WARNING: This rake task is deprecated and will be removed in Rails 6.1/, stderr)
end
- test 'notes finds notes in default directories' do
+ test "notes finds notes in default directories" do
app_file "app/controllers/some_controller.rb", "# TODO: note in app directory"
app_file "config/initializers/some_initializer.rb", "# TODO: note in config directory"
app_file "db/some_seeds.rb", "# TODO: note in db directory"
@@ -52,20 +58,23 @@ module ApplicationTests
app_file "some_other_dir/blah.rb", "# TODO: note in some_other directory"
- run_rake_notes do |output, lines|
- assert_match(/note in app directory/, output)
- assert_match(/note in config directory/, output)
- assert_match(/note in db directory/, output)
- assert_match(/note in lib directory/, output)
- assert_match(/note in test directory/, output)
- assert_no_match(/note in some_other directory/, output)
-
- assert_equal 5, lines.size
- assert_equal [4], lines.map(&:size).uniq
+ stderr = capture(:stderr) do
+ run_rake_notes do |output, lines|
+ assert_match(/note in app directory/, output)
+ assert_match(/note in config directory/, output)
+ assert_match(/note in db directory/, output)
+ assert_match(/note in lib directory/, output)
+ assert_match(/note in test directory/, output)
+ assert_no_match(/note in some_other directory/, output)
+
+ assert_equal 5, lines.size
+ assert_equal [4], lines.map(&:size).uniq
+ end
end
+ assert_match(/DEPRECATION WARNING: This rake task is deprecated and will be removed in Rails 6.1/, stderr)
end
- test 'notes finds notes in custom directories' do
+ test "notes finds notes in custom directories" do
app_file "app/controllers/some_controller.rb", "# TODO: note in app directory"
app_file "config/initializers/some_initializer.rb", "# TODO: note in config directory"
app_file "db/some_seeds.rb", "# TODO: note in db directory"
@@ -74,23 +83,28 @@ module ApplicationTests
app_file "some_other_dir/blah.rb", "# TODO: note in some_other directory"
- run_rake_notes "SOURCE_ANNOTATION_DIRECTORIES='some_other_dir' bin/rake notes" do |output, lines|
- assert_match(/note in app directory/, output)
- assert_match(/note in config directory/, output)
- assert_match(/note in db directory/, output)
- assert_match(/note in lib directory/, output)
- assert_match(/note in test directory/, output)
+ stderr = capture(:stderr) do
+ run_rake_notes "SOURCE_ANNOTATION_DIRECTORIES='some_other_dir' bin/rake notes" do |output, lines|
+ assert_match(/note in app directory/, output)
+ assert_match(/note in config directory/, output)
+ assert_match(/note in db directory/, output)
+ assert_match(/note in lib directory/, output)
+ assert_match(/note in test directory/, output)
- assert_match(/note in some_other directory/, output)
+ assert_match(/note in some_other directory/, output)
- assert_equal 6, lines.size
- assert_equal [4], lines.map(&:size).uniq
+ assert_equal 6, lines.size
+ assert_equal [4], lines.map(&:size).uniq
+ end
end
+ assert_match(/DEPRECATION WARNING: This rake task is deprecated and will be removed in Rails 6.1/, stderr)
+ assert_match(/DEPRECATION WARNING: `SOURCE_ANNOTATION_DIRECTORIES` is deprecated and will be removed in Rails 6.1/, stderr)
end
- test 'custom rake task finds specific notes in specific directories' do
+ test "custom rake task finds specific notes in specific directories" do
app_file "app/controllers/some_controller.rb", "# TODO: note in app directory"
- app_file "lib/some_file.rb", "# OPTIMIZE: note in lib directory\n" << "# FIXME: note in lib directory"
+ app_file "lib/some_file.rb", "# OPTIMIZE: note in lib directory\n" \
+ "# FIXME: note in lib directory"
app_file "test/some_test.rb", 1000.times.map { "" }.join("\n") << "# TODO: note in test directory"
app_file "lib/tasks/notes_custom.rake", <<-EOS
@@ -98,11 +112,11 @@ module ApplicationTests
task :notes_custom do
tags = 'TODO|FIXME'
opts = { dirs: %w(lib test), tag: true }
- SourceAnnotationExtractor.enumerate(tags, opts)
+ Rails::SourceAnnotationExtractor.enumerate(tags, opts)
end
EOS
- run_rake_notes "bin/rake notes_custom" do |output, lines|
+ run_rake_notes "bin/rails notes_custom" do |output, lines|
assert_match(/\[FIXME\] note in lib directory/, output)
assert_match(/\[TODO\] note in test directory/, output)
assert_no_match(/OPTIMIZE/, output)
@@ -113,44 +127,48 @@ module ApplicationTests
end
end
- test 'register a new extension' do
+ test "register a new extension" do
+ add_to_config "config.assets.precompile = []"
add_to_config %q{ config.annotations.register_extensions("scss", "sass") { |annotation| /\/\/\s*(#{annotation}):?\s*(.*)$/ } }
app_file "app/assets/stylesheets/application.css.scss", "// TODO: note in scss"
app_file "app/assets/stylesheets/application.css.sass", "// TODO: note in sass"
- run_rake_notes do |output, lines|
- assert_match(/note in scss/, output)
- assert_match(/note in sass/, output)
- assert_equal 2, lines.size
+ stderr = capture(:stderr) do
+ run_rake_notes do |output, lines|
+ assert_match(/note in scss/, output)
+ assert_match(/note in sass/, output)
+ assert_equal 2, lines.size
+ end
end
+ assert_match(/DEPRECATION WARNING: This rake task is deprecated and will be removed in Rails 6.1/, stderr)
end
- private
+ test "register additional directories" do
+ app_file "spec/spec_helper.rb", "# TODO: note in spec"
+ app_file "spec/models/user_spec.rb", "# TODO: note in model spec"
+ add_to_config ' config.annotations.register_directories("spec") '
- def run_rake_notes(command = 'bin/rake notes')
- boot_rails
- load_tasks
-
- Dir.chdir(app_path) do
- output = `#{command}`
- lines = output.scan(/\[([0-9\s]+)\]\s/).flatten
-
- yield output, lines
+ stderr = capture(:stderr) do
+ run_rake_notes do |output, lines|
+ assert_match(/note in spec/, output)
+ assert_match(/note in model spec/, output)
+ assert_equal 2, lines.size
+ end
end
+ assert_match(/DEPRECATION WARNING: This rake task is deprecated and will be removed in Rails 6.1/, stderr)
end
- def load_tasks
- require 'rake'
- require 'rdoc/task'
- require 'rake/testtask'
+ private
- Rails.application.load_tasks
- end
+ def run_rake_notes(command = "bin/rake notes")
+ Dir.chdir(app_path) do
+ output = `#{command}`
- def boot_rails
- super
- require "#{app_path}/config/environment"
- end
+ lines = output.scan(/\[([0-9\s]+)\]\s/).flatten
+
+ yield output, lines
+ end
+ end
end
end
end
diff --git a/railties/test/application/rake/restart_test.rb b/railties/test/application/rake/restart_test.rb
index 4cae199e6b..8614560bf2 100644
--- a/railties/test/application/rake/restart_test.rb
+++ b/railties/test/application/rake/restart_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "isolation/abstract_unit"
module ApplicationTests
@@ -7,31 +9,30 @@ module ApplicationTests
def setup
build_app
- boot_rails
end
def teardown
teardown_app
end
- test 'rake restart touches tmp/restart.txt' do
+ test "rails restart touches tmp/restart.txt" do
Dir.chdir(app_path) do
- `rake restart`
+ rails "restart"
assert File.exist?("tmp/restart.txt")
prev_mtime = File.mtime("tmp/restart.txt")
sleep(1)
- `rake restart`
+ rails "restart"
curr_mtime = File.mtime("tmp/restart.txt")
assert_not_equal prev_mtime, curr_mtime
end
end
- test 'rake restart should work even if tmp folder does not exist' do
+ test "rails restart should work even if tmp folder does not exist" do
Dir.chdir(app_path) do
- FileUtils.remove_dir('tmp')
- `rake restart`
- assert File.exist?('tmp/restart.txt')
+ FileUtils.remove_dir("tmp")
+ rails "restart"
+ assert File.exist?("tmp/restart.txt")
end
end
end
diff --git a/railties/test/application/rake/routes_test.rb b/railties/test/application/rake/routes_test.rb
new file mode 100644
index 0000000000..2c23ff4679
--- /dev/null
+++ b/railties/test/application/rake/routes_test.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+
+module ApplicationTests
+ module RakeTests
+ class RakeRoutesTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation
+ setup :build_app
+ teardown :teardown_app
+
+ test "`rake routes` outputs routes" do
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ get '/cart', to: 'cart#show'
+ end
+ RUBY
+
+ assert_equal <<~MESSAGE, run_rake_routes
+ Prefix Verb URI Pattern Controller#Action
+ cart GET /cart(.:format) cart#show
+ rails_service_blob GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs#show
+rails_blob_representation GET /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations#show
+ rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show
+update_rails_disk_service PUT /rails/active_storage/disk/:encoded_token(.:format) active_storage/disk#update
+ rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create
+ MESSAGE
+ end
+
+ test "`rake routes` outputs a deprecation warning" do
+ add_to_env_config("development", "config.active_support.deprecation = :stderr")
+
+ stderr = capture(:stderr) { run_rake_routes }
+ assert_match(/DEPRECATION WARNING: Using `bin\/rake routes` is deprecated and will be removed in Rails 6.1/, stderr)
+ end
+
+ private
+ def run_rake_routes
+ Dir.chdir(app_path) { `bin/rake routes` }
+ end
+ end
+ end
+end
diff --git a/railties/test/application/rake/tmp_test.rb b/railties/test/application/rake/tmp_test.rb
new file mode 100644
index 0000000000..048fd7adcc
--- /dev/null
+++ b/railties/test/application/rake/tmp_test.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+
+module ApplicationTests
+ module RakeTests
+ class TmpTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation
+
+ def setup
+ build_app
+ end
+
+ def teardown
+ teardown_app
+ end
+
+ test "tmp:clear clear cache, socket and screenshot files" do
+ Dir.chdir(app_path) do
+ FileUtils.mkdir_p("tmp/cache")
+ FileUtils.touch("tmp/cache/cache_file")
+
+ FileUtils.mkdir_p("tmp/sockets")
+ FileUtils.touch("tmp/sockets/socket_file")
+
+ FileUtils.mkdir_p("tmp/screenshots")
+ FileUtils.touch("tmp/screenshots/fail.png")
+
+ rails "tmp:clear"
+
+ assert_not File.exist?("tmp/cache/cache_file")
+ assert_not File.exist?("tmp/sockets/socket_file")
+ assert_not File.exist?("tmp/screenshots/fail.png")
+ end
+ end
+
+ test "tmp:clear should work if folder missing" do
+ FileUtils.remove_dir("#{app_path}/tmp")
+ rails "tmp:clear"
+ end
+ end
+ end
+end
diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb
index a040dd4cf6..c38b91ef03 100644
--- a/railties/test/application/rake_test.rb
+++ b/railties/test/application/rake_test.rb
@@ -1,14 +1,14 @@
-# coding:utf-8
+# frozen_string_literal: true
+
require "isolation/abstract_unit"
-require "active_support/core_ext/string/strip"
+require "env_helpers"
module ApplicationTests
class RakeTest < ActiveSupport::TestCase
- include ActiveSupport::Testing::Isolation
+ include ActiveSupport::Testing::Isolation, EnvHelpers
def setup
build_app
- boot_rails
end
def teardown
@@ -25,6 +25,26 @@ module ApplicationTests
assert $task_loaded
end
+ test "task is protected when previous migration was production" do
+ with_rails_env "production" do
+ rails "generate", "model", "product", "name:string"
+ rails "db:create", "db:migrate"
+ output = rails("db:test:prepare", allow_failure: true)
+
+ assert_match(/ActiveRecord::ProtectedEnvironmentError/, output)
+ end
+ end
+
+ def test_not_protected_when_previous_migration_was_not_production
+ with_rails_env "test" do
+ rails "generate", "model", "product", "name:string"
+ rails "db:create", "db:migrate"
+ output = rails("db:test:prepare", "test")
+
+ assert_no_match(/ActiveRecord::ProtectedEnvironmentError/, output)
+ end
+ end
+
def test_environment_is_required_in_rake_tasks
app_file "config/environment.rb", <<-RUBY
SuperMiddleware = Struct.new(:app)
@@ -36,7 +56,7 @@ module ApplicationTests
Rails.application.initialize!
RUBY
- assert_match("SuperMiddleware", Dir.chdir(app_path){ `bin/rake middleware` })
+ assert_match("SuperMiddleware", rails("middleware"))
end
def test_initializers_are_executed_in_rake_tasks
@@ -51,7 +71,7 @@ module ApplicationTests
end
RUBY
- output = Dir.chdir(app_path){ `bin/rake do_nothing` }
+ output = rails("do_nothing")
assert_match "Doing something...", output
end
@@ -64,7 +84,7 @@ module ApplicationTests
end
RUBY
- app_file 'app/models/hello.rb', <<-RUBY
+ app_file "app/models/hello.rb", <<-RUBY
class Hello
def world
puts 'Hello world'
@@ -72,74 +92,34 @@ module ApplicationTests
end
RUBY
- output = Dir.chdir(app_path) { `bin/rake do_nothing` }
- assert_match 'Hello world', output
+ output = rails("do_nothing")
+ assert_match "Hello world", output
end
def test_should_not_eager_load_model_for_rake
add_to_config <<-RUBY
rake_tasks do
task do_nothing: :environment do
+ puts 'There is nothing'
end
end
RUBY
- add_to_env_config 'production', <<-RUBY
+ add_to_env_config "production", <<-RUBY
config.eager_load = true
RUBY
- app_file 'app/models/hello.rb', <<-RUBY
+ app_file "app/models/hello.rb", <<-RUBY
raise 'should not be pre-required for rake even eager_load=true'
RUBY
- Dir.chdir(app_path) do
- assert system('bin/rake do_nothing RAILS_ENV=production'),
- 'should not be pre-required for rake even eager_load=true'
- end
+ output = rails("do_nothing", "RAILS_ENV=production")
+ assert_match "There is nothing", output
end
def test_code_statistics_sanity
- assert_match "Code LOC: 7 Test LOC: 0 Code to Test Ratio: 1:0.0",
- Dir.chdir(app_path){ `bin/rake stats` }
- end
-
- def test_rake_routes_calls_the_route_inspector
- app_file "config/routes.rb", <<-RUBY
- Rails.application.routes.draw do
- get '/cart', to: 'cart#show'
- end
- RUBY
-
- output = Dir.chdir(app_path){ `bin/rake routes` }
- assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output
- end
-
- def test_rake_routes_with_controller_environment
- app_file "config/routes.rb", <<-RUBY
- Rails.application.routes.draw do
- get '/cart', to: 'cart#show'
- get '/basketball', to: 'basketball#index'
- end
- RUBY
-
- ENV['CONTROLLER'] = 'cart'
- output = Dir.chdir(app_path){ `bin/rake routes` }
- assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output
- end
-
- def test_rake_routes_displays_message_when_no_routes_are_defined
- app_file "config/routes.rb", <<-RUBY
- Rails.application.routes.draw do
- end
- RUBY
-
- assert_equal <<-MESSAGE.strip_heredoc, Dir.chdir(app_path){ `bin/rake routes` }
- You don't have any routes defined!
-
- Please add some routes in config/routes.rb.
-
- For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html.
- MESSAGE
+ assert_match "Code LOC: 32 Test LOC: 0 Code to Test Ratio: 1:0.0",
+ rails("stats")
end
def test_logger_is_flushed_when_exiting_production_rake_tasks
@@ -151,46 +131,39 @@ module ApplicationTests
end
RUBY
- output = Dir.chdir(app_path){ `bin/rake log_something RAILS_ENV=production && cat log/production.log` }
- assert_match "Sample log message", output
+ rails "log_something", "RAILS_ENV=production"
+ assert_match "Sample log message", File.read("#{app_path}/log/production.log")
end
def test_loading_specific_fixtures
- Dir.chdir(app_path) do
- `bin/rails generate model user username:string password:string;
- bin/rails generate model product name:string;
- bin/rake db:migrate`
- end
+ rails "generate", "model", "user", "username:string", "password:string"
+ rails "generate", "model", "product", "name:string"
+ rails "db:migrate"
require "#{rails_root}/config/environment"
# loading a specific fixture
- errormsg = Dir.chdir(app_path) { `bin/rake db:fixtures:load FIXTURES=products` }
- assert $?.success?, errormsg
+ rails "db:fixtures:load", "FIXTURES=products"
assert_equal 2, ::AppTemplate::Application::Product.count
assert_equal 0, ::AppTemplate::Application::User.count
end
def test_loading_only_yml_fixtures
- Dir.chdir(app_path) do
- `bin/rake db:migrate`
- end
+ rails "db:migrate"
app_file "test/fixtures/products.csv", ""
require "#{rails_root}/config/environment"
- errormsg = Dir.chdir(app_path) { `bin/rake db:fixtures:load` }
- assert $?.success?, errormsg
+ rails "db:fixtures:load"
end
def test_scaffold_tests_pass_by_default
- output = Dir.chdir(app_path) do
- `bin/rails generate scaffold user username:string password:string;
- bin/rake db:migrate test`
- end
+ rails "generate", "scaffold", "user", "username:string", "password:string"
+ with_rails_env("test") { rails("db:migrate") }
+ output = rails("test")
- assert_match(/7 runs, 12 assertions, 0 failures, 0 errors/, output)
+ assert_match(/7 runs, 9 assertions, 0 failures, 0 errors/, output)
assert_no_match(/Errors running/, output)
end
@@ -204,93 +177,71 @@ module ApplicationTests
end
RUBY
- output = Dir.chdir(app_path) do
- `bin/rails generate scaffold user username:string password:string;
- bin/rake db:migrate test`
- end
+ rails "generate", "scaffold", "user", "username:string", "password:string"
+ with_rails_env("test") { rails("db:migrate") }
+ output = rails("test")
assert_match(/5 runs, 7 assertions, 0 failures, 0 errors/, output)
assert_no_match(/Errors running/, output)
end
- def test_scaffold_with_references_columns_tests_pass_when_belongs_to_is_optional
- app_file "config/initializers/active_record_belongs_to_required_by_default.rb",
- "Rails.application.config.active_record.belongs_to_required_by_default = false"
-
- output = Dir.chdir(app_path) do
- `bin/rails generate scaffold LineItems product:references cart:belongs_to;
- bin/rake db:migrate test`
+ def test_scaffold_with_references_columns_tests_pass_by_default
+ rails "generate", "model", "Product"
+ rails "generate", "model", "Cart"
+ rails "generate", "scaffold", "LineItems", "product:references", "cart:belongs_to"
+ with_rails_env("test") do
+ rails("db:migrate")
+ rails("webpacker:compile")
end
+ output = rails("test")
- assert_match(/7 runs, 12 assertions, 0 failures, 0 errors/, output)
+ assert_match(/7 runs, 9 assertions, 0 failures, 0 errors/, output)
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/rake db:migrate;
- bin/rake 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
- `bin/rails generate scaffold user username:string;
- bin/rake db:migrate;
- bin/rake db:test:prepare 2>&1 --trace`
- end
+ rails "generate", "scaffold", "user", "username:string"
+ rails "db:migrate"
+ output = rails("db:test:prepare", "--trace")
assert_match(/Execute db:test:load_structure/, output)
end
def test_rake_dump_structure_should_respect_db_structure_env_variable
- Dir.chdir(app_path) do
- # ensure we have a schema_migrations table to dump
- `bin/rake db:migrate db:structure:dump SCHEMA=db/my_structure.sql`
- end
- assert File.exist?(File.join(app_path, 'db', 'my_structure.sql'))
+ # ensure we have a schema_migrations table to dump
+ rails "db:migrate", "db:structure:dump", "SCHEMA=db/my_structure.sql"
+ assert File.exist?(File.join(app_path, "db", "my_structure.sql"))
end
def test_rake_dump_structure_should_be_called_twice_when_migrate_redo
add_to_config "config.active_record.schema_format = :sql"
- output = Dir.chdir(app_path) do
- `bin/rails g model post title:string;
- bin/rake db:migrate:redo 2>&1 --trace;`
- end
+ rails "g", "model", "post", "title:string"
+ output = rails("db:migrate:redo", "--trace")
# expect only Invoke db:structure:dump (first_time)
assert_no_match(/^\*\* Invoke db:structure:dump\s+$/, output)
end
def test_rake_dump_schema_cache
- Dir.chdir(app_path) do
- `bin/rails generate model post title:string;
- bin/rails generate model product name:string;
- bin/rake db:migrate db:schema:cache:dump`
- end
- assert File.exist?(File.join(app_path, 'db', 'schema_cache.dump'))
+ rails "generate", "model", "post", "title:string"
+ rails "generate", "model", "product", "name:string"
+ rails "db:migrate", "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/rake db:schema:cache:dump db:schema:cache:clear`
- end
- assert !File.exist?(File.join(app_path, 'db', 'schema_cache.dump'))
+ rails "db:schema:cache:dump", "db:schema:cache:clear"
+ assert_not File.exist?(File.join(app_path, "db", "schema_cache.yml"))
end
def test_copy_templates
- Dir.chdir(app_path) do
- `bin/rake rails:templates:copy`
- %w(controller mailer scaffold).each do |dir|
- assert File.exist?(File.join(app_path, 'lib', 'templates', 'erb', dir))
- end
- %w(controller helper scaffold_controller assets).each do |dir|
- assert File.exist?(File.join(app_path, 'lib', 'templates', 'rails', dir))
- end
+ rails "app:templates:copy"
+ %w(controller mailer scaffold).each do |dir|
+ assert File.exist?(File.join(app_path, "lib", "templates", "erb", dir))
+ end
+ %w(controller helper scaffold_controller assets).each do |dir|
+ assert File.exist?(File.join(app_path, "lib", "templates", "rails", dir))
end
end
@@ -298,18 +249,8 @@ module ApplicationTests
app_file "config/initializers/dummy.rb", "puts 'Hello, World!'"
app_file "template.rb", ""
- output = Dir.chdir(app_path) do
- `bin/rake rails:template LOCATION=template.rb`
- end
-
+ output = rails("app:template", "LOCATION=template.rb")
assert_match(/Hello, World!/, output)
end
-
- def test_tmp_clear_should_work_if_folder_missing
- FileUtils.remove_dir("#{app_path}/tmp")
- errormsg = Dir.chdir(app_path) { `bin/rake tmp:clear` }
- assert_predicate $?, :success?
- assert_empty errormsg
- end
end
end
diff --git a/railties/test/application/rendering_test.rb b/railties/test/application/rendering_test.rb
index b01febd768..ab1591f388 100644
--- a/railties/test/application/rendering_test.rb
+++ b/railties/test/application/rendering_test.rb
@@ -1,14 +1,15 @@
-require 'isolation/abstract_unit'
-require 'rack/test'
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "rack/test"
module ApplicationTests
- class RoutingTest < ActiveSupport::TestCase
+ class RenderingTest < ActiveSupport::TestCase
include ActiveSupport::Testing::Isolation
include Rack::Test::Methods
def setup
build_app
- boot_rails
end
def teardown
@@ -16,13 +17,13 @@ module ApplicationTests
end
test "Unknown format falls back to HTML template" do
- app_file 'config/routes.rb', <<-RUBY
+ app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get 'pages/:id', to: 'pages#show'
end
RUBY
- app_file 'app/controllers/pages_controller.rb', <<-RUBY
+ app_file "app/controllers/pages_controller.rb", <<-RUBY
class PagesController < ApplicationController
layout false
@@ -31,14 +32,14 @@ module ApplicationTests
end
RUBY
- app_file 'app/views/pages/show.html.erb', <<-RUBY
+ app_file "app/views/pages/show.html.erb", <<-RUBY
<%= params[:id] %>
RUBY
- get '/pages/foo'
+ get "/pages/foo"
assert_equal 200, last_response.status
- get '/pages/foo.bar'
+ get "/pages/foo.bar"
assert_equal 200, last_response.status
end
end
diff --git a/railties/test/application/routing_test.rb b/railties/test/application/routing_test.rb
index cbada6be97..bec038fb51 100644
--- a/railties/test/application/routing_test.rb
+++ b/railties/test/application/routing_test.rb
@@ -1,5 +1,7 @@
-require 'isolation/abstract_unit'
-require 'rack/test'
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "rack/test"
module ApplicationTests
class RoutingTest < ActiveSupport::TestCase
@@ -8,7 +10,6 @@ module ApplicationTests
def setup
build_app
- boot_rails
end
def teardown
@@ -21,6 +22,12 @@ module ApplicationTests
assert_equal 200, last_response.status
end
+ test "rails/info in development" do
+ app("development")
+ get "/rails/info"
+ assert_equal 302, last_response.status
+ end
+
test "rails/info/routes in development" do
app("development")
get "/rails/info/routes"
@@ -33,28 +40,46 @@ module ApplicationTests
assert_equal 200, last_response.status
end
+ test "/rails/info routes are accessible with globbing route present" do
+ app("development")
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ get '*foo', to: 'foo#index'
+ end
+ RUBY
+
+ get "/rails/info"
+ assert_equal 302, last_response.status
+
+ get "rails/info/routes"
+ assert_equal 200, last_response.status
+
+ get "rails/info/properties"
+ assert_equal 200, last_response.status
+ end
+
test "root takes precedence over internal welcome controller" do
app("development")
- get '/'
- assert_match %r{<h1>Getting started</h1>} , last_response.body
+ assert_welcome get("/")
controller :foo, <<-RUBY
class FooController < ApplicationController
def index
- render text: "foo"
+ render plain: "foo"
end
end
RUBY
- app_file 'config/routes.rb', <<-RUBY
+ app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
root to: "foo#index"
end
RUBY
- get '/'
- assert_equal 'foo', last_response.body
+ get "/"
+ assert_equal "foo", last_response.body
end
test "rails/welcome in production" do
@@ -63,6 +88,12 @@ module ApplicationTests
assert_equal 404, last_response.status
end
+ test "rails/info in production" do
+ app("production")
+ get "/rails/info"
+ assert_equal 404, last_response.status
+ end
+
test "rails/info/routes in production" do
app("production")
get "/rails/info/routes"
@@ -78,8 +109,8 @@ module ApplicationTests
test "simple controller" do
simple_controller
- get '/foo'
- assert_equal 'foo', last_response.body
+ get "/foo"
+ assert_equal "foo", last_response.body
end
test "simple controller with helper" do
@@ -91,7 +122,7 @@ module ApplicationTests
end
RUBY
- app_file 'app/helpers/bar_helper.rb', <<-RUBY
+ app_file "app/helpers/bar_helper.rb", <<-RUBY
module BarHelper
def foo_or_bar?
"bar"
@@ -99,18 +130,18 @@ module ApplicationTests
end
RUBY
- app_file 'config/routes.rb', <<-RUBY
+ app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get ':controller(/:action)'
end
RUBY
- get '/foo'
- assert_equal 'bar', last_response.body
+ get "/foo"
+ assert_equal "bar", last_response.body
end
test "mount rack app" do
- app_file 'config/routes.rb', <<-RUBY
+ app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
mount lambda { |env| [200, {}, [env["PATH_INFO"]]] }, at: "/blog"
# The line below is required because mount sometimes
@@ -119,35 +150,35 @@ module ApplicationTests
end
RUBY
- get '/blog/archives'
- assert_equal '/archives', last_response.body
+ get "/blog/archives"
+ assert_equal "/archives", last_response.body
end
test "mount named rack app" do
controller :foo, <<-RUBY
class FooController < ApplicationController
def index
- render text: my_blog_path
+ render plain: my_blog_path
end
end
RUBY
- app_file 'config/routes.rb', <<-RUBY
+ app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
mount lambda { |env| [200, {}, [env["PATH_INFO"]]] }, at: "/blog", as: "my_blog"
get '/foo' => 'foo#index'
end
RUBY
- get '/foo'
- assert_equal '/blog', last_response.body
+ get "/foo"
+ assert_equal "/blog", last_response.body
end
test "multiple controllers" do
controller :foo, <<-RUBY
class FooController < ApplicationController
def index
- render text: "foo"
+ render plain: "foo"
end
end
RUBY
@@ -155,59 +186,59 @@ module ApplicationTests
controller :bar, <<-RUBY
class BarController < ActionController::Base
def index
- render text: "bar"
+ render plain: "bar"
end
end
RUBY
- app_file 'config/routes.rb', <<-RUBY
+ app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get ':controller(/:action)'
end
RUBY
- get '/foo'
- assert_equal 'foo', last_response.body
+ get "/foo"
+ assert_equal "foo", last_response.body
- get '/bar'
- assert_equal 'bar', last_response.body
+ get "/bar"
+ assert_equal "bar", last_response.body
end
test "nested controller" do
- controller 'foo', <<-RUBY
+ controller "foo", <<-RUBY
class FooController < ApplicationController
def index
- render text: "foo"
+ render plain: "foo"
end
end
RUBY
- controller 'admin/foo', <<-RUBY
+ controller "admin/foo", <<-RUBY
module Admin
class FooController < ApplicationController
def index
- render text: "admin::foo"
+ render plain: "admin::foo"
end
end
end
RUBY
- app_file 'config/routes.rb', <<-RUBY
+ app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get 'admin/foo', to: 'admin/foo#index'
get 'foo', to: 'foo#index'
end
RUBY
- get '/foo'
- assert_equal 'foo', last_response.body
+ get "/foo"
+ assert_equal "foo", last_response.body
- get '/admin/foo'
- assert_equal 'admin::foo', last_response.body
+ get "/admin/foo"
+ assert_equal "admin::foo", last_response.body
end
test "routes appending blocks" do
- app_file 'config/routes.rb', <<-RUBY
+ app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get ':controller/:action'
end
@@ -219,60 +250,108 @@ module ApplicationTests
end
R
- app 'development'
+ app "development"
- get '/win'
- assert_equal 'WIN', last_response.body
+ get "/win"
+ assert_equal "WIN", last_response.body
- app_file 'config/routes.rb', <<-R
+ app_file "config/routes.rb", <<-R
Rails.application.routes.draw do
get 'lol' => 'hello#index'
end
R
- get '/win'
- assert_equal 'WIN', last_response.body
+ get "/win"
+ 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
def bar
- render text: "bar"
+ render plain: "bar"
end
def baz
- render text: "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 self.model_name
+ @_model_name ||= ActiveModel::Name.new(self.class, nil, "User")
+ end
+
+ def persisted?
+ false
end
end
RUBY
- app_file 'config/routes.rb', <<-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
app(mode)
- get '/foo'
- assert_equal 'bar', last_response.body
+ get "/foo"
+ assert_equal "bar", last_response.body
+
+ get "/custom"
+ assert_equal "http://www.microsoft.com", last_response.body
- app_file 'config/routes.rb', <<-RUBY
+ 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
+ get "/foo"
+ 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
- test 'routes are loaded just after initialization' do
+ test "routes are loaded just after initialization" do
require "#{app_path}/config/application"
# Create the rack app just inside after initialize callback
@@ -280,17 +359,17 @@ module ApplicationTests
::InitializeRackApp = lambda { |env| [200, {}, ["InitializeRackApp"]] }
end
- app_file 'config/routes.rb', <<-RUBY
+ app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get 'foo', to: ::InitializeRackApp
end
RUBY
- get '/foo'
+ get "/foo"
assert_equal "InitializeRackApp", last_response.body
end
- test 'reload_routes! is part of Rails.application API' do
+ test "reload_routes! is part of Rails.application API" do
app("development")
assert_nothing_raised do
Rails.application.reload_routes!
@@ -298,36 +377,44 @@ module ApplicationTests
end
def test_root_path
- app('development')
+ app("development")
controller :foo, <<-RUBY
class FooController < ApplicationController
def index
- render :text => "foo"
+ render plain: "foo"
end
end
RUBY
- app_file 'config/routes.rb', <<-RUBY
+ app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get 'foo', :to => 'foo#index'
root :to => 'foo#index'
end
RUBY
- remove_file 'public/index.html'
+ remove_file "public/index.html"
- get '/'
- assert_equal 'foo', last_response.body
+ get "/"
+ assert_equal "foo", last_response.body
end
- test 'routes are added and removed when reloading' do
- app('development')
+ test "routes are added and removed when reloading" do
+ app("development")
controller :foo, <<-RUBY
class FooController < ApplicationController
def index
- render text: "foo"
+ render plain: "foo"
+ end
+
+ def custom
+ render plain: custom_url
+ end
+
+ def mapping
+ render plain: url_for(User.new)
end
end
RUBY
@@ -335,45 +422,74 @@ module ApplicationTests
controller :bar, <<-RUBY
class BarController < ApplicationController
def index
- render text: "bar"
+ render plain: "bar"
end
end
RUBY
- app_file 'config/routes.rb', <<-RUBY
+ app_file "app/models/user.rb", <<-RUBY
+ class User
+ extend ActiveModel::Naming
+ include ActiveModel::Conversion
+
+ def self.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'
end
RUBY
- get '/foo'
- assert_equal 'foo', last_response.body
- assert_equal '/foo', Rails.application.routes.url_helpers.foo_path
+ get "/foo"
+ assert_equal "foo", last_response.body
+ assert_equal "/foo", Rails.application.routes.url_helpers.foo_path
- get '/bar'
+ get "/bar"
assert_equal 404, last_response.status
assert_raises NoMethodError do
- assert_equal '/bar', Rails.application.routes.url_helpers.bar_path
+ assert_equal "/bar", Rails.application.routes.url_helpers.bar_path
end
- app_file 'config/routes.rb', <<-RUBY
+ app_file "config/routes.rb", <<-RUBY
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
Rails.application.reload_routes!
- get '/foo'
- assert_equal 'foo', last_response.body
- assert_equal '/foo', Rails.application.routes.url_helpers.foo_path
+ get "/foo"
+ assert_equal "foo", last_response.body
+ assert_equal "/foo", Rails.application.routes.url_helpers.foo_path
- get '/bar'
- assert_equal 'bar', last_response.body
- assert_equal '/bar', Rails.application.routes.url_helpers.bar_path
+ get "/bar"
+ assert_equal "bar", last_response.body
+ assert_equal "/bar", Rails.application.routes.url_helpers.bar_path
- app_file 'config/routes.rb', <<-RUBY
+ 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'
end
@@ -381,24 +497,36 @@ module ApplicationTests
Rails.application.reload_routes!
- get '/foo'
- assert_equal 'foo', last_response.body
- assert_equal '/foo', Rails.application.routes.url_helpers.foo_path
+ get "/foo"
+ assert_equal "foo", last_response.body
+ assert_equal "/foo", Rails.application.routes.url_helpers.foo_path
+
+ get "/bar"
+ assert_equal 404, last_response.status
+ 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 '/bar'
+ get "/mapping"
assert_equal 404, last_response.status
assert_raises NoMethodError do
- assert_equal '/bar', Rails.application.routes.url_helpers.bar_path
+ assert_equal "/profile", Rails.application.routes.url_helpers.polymorphic_path(User.new)
end
end
- test 'named routes are cleared when reloading' do
- app('development')
+ test "named routes are cleared when reloading" do
+ app("development")
controller :foo, <<-RUBY
class FooController < ApplicationController
def index
- render text: "foo"
+ render plain: "foo"
end
end
RUBY
@@ -406,63 +534,149 @@ module ApplicationTests
controller :bar, <<-RUBY
class BarController < ApplicationController
def index
- render text: "bar"
+ render plain: "bar"
end
end
RUBY
- app_file 'config/routes.rb', <<-RUBY
+ app_file "app/models/user.rb", <<-RUBY
+ class User
+ extend ActiveModel::Naming
+ include ActiveModel::Conversion
+
+ def self.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')
+ 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
+ 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
Rails.application.reload_routes!
- get '/en/foo'
+ get "/en/foo"
assert_equal 404, last_response.status
- get '/en/bar'
- assert_equal 'bar', last_response.body
- assert_equal '/en/bar', Rails.application.routes.url_helpers.foo_path(:locale => 'en')
+ 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
- app_file 'config/initializers/inflection.rb', <<-RUBY
+ test "resource routing with irregular inflection" do
+ app_file "config/initializers/inflection.rb", <<-RUBY
ActiveSupport::Inflector.inflections do |inflect|
inflect.irregular 'yazi', 'yazilar'
end
RUBY
- app_file 'config/routes.rb', <<-RUBY
+ app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
resources :yazilar
end
RUBY
- controller 'yazilar', <<-RUBY
+ controller "yazilar", <<-RUBY
class YazilarController < ApplicationController
def index
- render text: 'yazilar#index'
+ render plain: 'yazilar#index'
end
end
RUBY
- get '/yazilars'
+ get "/yazilars"
assert_equal 404, last_response.status
- get '/yazilar'
+ 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 0c180339b4..8f5f48c281 100644
--- a/railties/test/application/runner_test.rb
+++ b/railties/test/application/runner_test.rb
@@ -1,5 +1,7 @@
-require 'isolation/abstract_unit'
-require 'env_helpers'
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "env_helpers"
module ApplicationTests
class RunnerTest < ActiveSupport::TestCase
@@ -8,7 +10,6 @@ module ApplicationTests
def setup
build_app
- boot_rails
# Lets create a model so we have something to play with
app_file "app/models/user.rb", <<-MODEL
@@ -25,15 +26,20 @@ module ApplicationTests
end
def test_should_include_runner_in_shebang_line_in_help_without_option
- assert_match "/rails runner", Dir.chdir(app_path) { `bin/rails runner` }
+ assert_match "/rails runner", rails("runner", allow_failure: true)
end
def test_should_include_runner_in_shebang_line_in_help
- assert_match "/rails runner", Dir.chdir(app_path) { `bin/rails runner --help` }
+ assert_match "/rails runner", rails("runner", "--help")
end
def test_should_run_ruby_statement
- assert_match "42", Dir.chdir(app_path) { `bin/rails runner "puts User.count"` }
+ assert_match "42", rails("runner", "puts User.count")
+ end
+
+ def test_should_set_argv_when_running_code
+ output = 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
@@ -41,7 +47,16 @@ module ApplicationTests
puts User.count
SCRIPT
- assert_match "42", Dir.chdir(app_path) { `bin/rails runner "bin/count_users.rb"` }
+ assert_match "42", rails("runner", "bin/count_users.rb")
+ end
+
+ def test_no_minitest_loaded_in_production_mode
+ app_file "bin/print_features.rb", <<-SCRIPT
+ p $LOADED_FEATURES.grep(/minitest/)
+ SCRIPT
+ assert_match "[]", Dir.chdir(app_path) {
+ `RAILS_ENV=production bin/rails runner "bin/print_features.rb"`
+ }
end
def test_should_set_dollar_0_to_file
@@ -49,7 +64,7 @@ module ApplicationTests
puts $0
SCRIPT
- assert_match "bin/dollar0.rb", Dir.chdir(app_path) { `bin/rails runner "bin/dollar0.rb"` }
+ assert_match "bin/dollar0.rb", rails("runner", "bin/dollar0.rb")
end
def test_should_set_dollar_program_name_to_file
@@ -57,7 +72,23 @@ module ApplicationTests
puts $PROGRAM_NAME
SCRIPT
- assert_match "bin/program_name.rb", Dir.chdir(app_path) { `bin/rails runner "bin/program_name.rb"` }
+ assert_match "bin/program_name.rb", rails("runner", "bin/program_name.rb")
+ end
+
+ def test_passes_extra_args_to_file
+ app_file "bin/program_name.rb", <<-SCRIPT
+ p ARGV
+ SCRIPT
+
+ assert_match %w( a b ).to_s, rails("runner", "bin/program_name.rb", "a", "b")
+ end
+
+ def test_should_run_stdin
+ app_file "bin/count_users.rb", <<-SCRIPT
+ puts User.count
+ SCRIPT
+
+ assert_match "42", Dir.chdir(app_path) { `cat bin/count_users.rb | bin/rails runner -` }
end
def test_with_hook
@@ -67,23 +98,47 @@ module ApplicationTests
end
RUBY
- assert_match "true", Dir.chdir(app_path) { `bin/rails runner "puts Rails.application.config.ran"` }
+ assert_match "true", rails("runner", "puts Rails.application.config.ran")
end
def test_default_environment
- assert_match "development", Dir.chdir(app_path) { `bin/rails runner "puts Rails.env"` }
+ assert_match "development", rails("runner", "puts Rails.env")
+ end
+
+ def test_runner_detects_syntax_errors
+ output = rails("runner", "puts 'hello world", allow_failure: true)
+ assert_not_predicate $?, :success?
+ assert_match "unterminated string meets end of file", output
+ end
+
+ def test_runner_detects_bad_script_name
+ output = rails("runner", "iuiqwiourowe", allow_failure: true)
+ assert_not_predicate $?, :success?
+ assert_match "undefined local variable or method `iuiqwiourowe' for", output
end
def test_environment_with_rails_env
with_rails_env "production" do
- assert_match "production", Dir.chdir(app_path) { `bin/rails runner "puts Rails.env"` }
+ assert_match "production", rails("runner", "puts Rails.env")
end
end
def test_environment_with_rack_env
with_rack_env "production" do
- assert_match "production", Dir.chdir(app_path) { `bin/rails runner "puts Rails.env"` }
+ assert_match "production", rails("runner", "puts Rails.env")
+ end
+ end
+
+ def test_can_call_same_name_class_as_defined_in_thor
+ app_file "app/models/task.rb", <<-MODEL
+ class Task
+ def self.count
+ 42
+ end
end
+ MODEL
+
+ assert_match "42", rails("runner", "puts Task.count")
end
end
end
diff --git a/railties/test/application/server_test.rb b/railties/test/application/server_test.rb
new file mode 100644
index 0000000000..ab9e910aed
--- /dev/null
+++ b/railties/test/application/server_test.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "console_helpers"
+require "rails/command"
+require "rails/commands/server/server_command"
+
+module ApplicationTests
+ class ServerTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation
+ include ConsoleHelpers
+
+ def setup
+ build_app
+ end
+
+ def teardown
+ teardown_app
+ end
+
+ test "deprecate support of older `config.ru`" do
+ remove_file "config.ru"
+ app_file "config.ru", <<-RUBY
+ require_relative 'config/environment'
+ run AppTemplate::Application
+ RUBY
+
+ server = Rails::Server.new(config: "#{app_path}/config.ru")
+ server.app
+
+ log = File.read(Rails.application.config.paths["log"].first)
+ assert_match(/DEPRECATION WARNING: Using `Rails::Application` subclass to start the server is deprecated/, log)
+ end
+
+ test "restart rails server with custom pid file path" do
+ skip "PTY unavailable" unless available_pty?
+
+ File.open("#{app_path}/config/boot.rb", "w") do |f|
+ f.puts "ENV['BUNDLE_GEMFILE'] = '#{Bundler.default_gemfile}'"
+ f.puts "require 'bundler/setup'"
+ end
+
+ primary, replica = PTY.open
+ pid = nil
+
+ begin
+ pid = Process.spawn("#{app_path}/bin/rails server -P tmp/dummy.pid", in: replica, out: replica, err: replica)
+ assert_output("Listening", primary)
+
+ rails("restart")
+
+ assert_output("Restarting", primary)
+ assert_output("Inherited", primary)
+ ensure
+ kill(pid) if pid
+ end
+ end
+
+ private
+ def kill(pid)
+ Process.kill("TERM", pid)
+ Process.wait(pid)
+ rescue Errno::ESRCH
+ end
+ end
+end
diff --git a/railties/test/application/test_runner_test.rb b/railties/test/application/test_runner_test.rb
index 494e6dd7bd..140703e118 100644
--- a/railties/test/application/test_runner_test.rb
+++ b/railties/test/application/test_runner_test.rb
@@ -1,6 +1,7 @@
-require 'isolation/abstract_unit'
-require 'active_support/core_ext/string/strip'
-require 'env_helpers'
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "env_helpers"
module ApplicationTests
class TestRunnerTest < ActiveSupport::TestCase
@@ -15,32 +16,56 @@ module ApplicationTests
teardown_app
end
+ def test_run_via_backwardscompatibility
+ require "minitest/rails_plugin"
+
+ assert_nothing_raised do
+ Minitest.run_via[:ruby] = true
+ end
+
+ assert Minitest.run_via[:ruby]
+ end
+
def test_run_single_file
- create_test_file :models, 'foo'
- create_test_file :models, 'bar'
+ create_test_file :models, "foo"
+ create_test_file :models, "bar"
assert_match "1 runs, 1 assertions, 0 failures", run_test_command("test/models/foo_test.rb")
end
+ def test_run_single_file_with_absolute_path
+ create_test_file :models, "foo"
+ create_test_file :models, "bar"
+ assert_match "1 runs, 1 assertions, 0 failures", run_test_command("#{app_path}/test/models/foo_test.rb")
+ end
+
def test_run_multiple_files
- create_test_file :models, 'foo'
- create_test_file :models, 'bar'
+ create_test_file :models, "foo"
+ create_test_file :models, "bar"
assert_match "2 runs, 2 assertions, 0 failures", run_test_command("test/models/foo_test.rb test/models/bar_test.rb")
end
+ def test_run_multiple_files_with_absolute_paths
+ create_test_file :models, "foo"
+ create_test_file :controllers, "foobar_controller"
+ create_test_file :models, "bar"
+
+ assert_match "2 runs, 2 assertions, 0 failures", run_test_command("#{app_path}/test/models/foo_test.rb #{app_path}/test/controllers/foobar_controller_test.rb")
+ end
+
def test_run_file_with_syntax_error
- app_file 'test/models/error_test.rb', <<-RUBY
+ app_file "test/models/error_test.rb", <<-RUBY
require 'test_helper'
def; end
RUBY
- error = capture(:stderr) { run_test_command('test/models/error_test.rb') }
+ error = capture(:stderr) { run_test_command("test/models/error_test.rb", stderr: true) }
assert_match "syntax error", error
end
def test_run_models
- create_test_file :models, 'foo'
- create_test_file :models, 'bar'
- create_test_file :controllers, 'foobar_controller'
+ create_test_file :models, "foo"
+ create_test_file :models, "bar"
+ create_test_file :controllers, "foobar_controller"
run_test_command("test/models").tap do |output|
assert_match "FooTest", output
assert_match "BarTest", output
@@ -49,9 +74,9 @@ module ApplicationTests
end
def test_run_helpers
- create_test_file :helpers, 'foo_helper'
- create_test_file :helpers, 'bar_helper'
- create_test_file :controllers, 'foobar_controller'
+ create_test_file :helpers, "foo_helper"
+ create_test_file :helpers, "bar_helper"
+ create_test_file :controllers, "foobar_controller"
run_test_command("test/helpers").tap do |output|
assert_match "FooHelperTest", output
assert_match "BarHelperTest", output
@@ -60,12 +85,12 @@ 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|
+ create_test_file :models, "foo"
+ create_test_file :helpers, "bar_helper"
+ create_test_file :unit, "baz_unit"
+ create_test_file :controllers, "foobar_controller"
+
+ rails("test:units").tap do |output|
assert_match "FooTest", output
assert_match "BarHelperTest", output
assert_match "BazUnitTest", output
@@ -74,9 +99,9 @@ module ApplicationTests
end
def test_run_controllers
- create_test_file :controllers, 'foo_controller'
- create_test_file :controllers, 'bar_controller'
- create_test_file :models, 'foo'
+ create_test_file :controllers, "foo_controller"
+ create_test_file :controllers, "bar_controller"
+ create_test_file :models, "foo"
run_test_command("test/controllers").tap do |output|
assert_match "FooControllerTest", output
assert_match "BarControllerTest", output
@@ -85,9 +110,9 @@ module ApplicationTests
end
def test_run_mailers
- create_test_file :mailers, 'foo_mailer'
- create_test_file :mailers, 'bar_mailer'
- create_test_file :models, 'foo'
+ create_test_file :mailers, "foo_mailer"
+ create_test_file :mailers, "bar_mailer"
+ create_test_file :models, "foo"
run_test_command("test/mailers").tap do |output|
assert_match "FooMailerTest", output
assert_match "BarMailerTest", output
@@ -96,9 +121,9 @@ module ApplicationTests
end
def test_run_jobs
- create_test_file :jobs, 'foo_job'
- create_test_file :jobs, 'bar_job'
- create_test_file :models, 'foo'
+ create_test_file :jobs, "foo_job"
+ create_test_file :jobs, "bar_job"
+ create_test_file :models, "foo"
run_test_command("test/jobs").tap do |output|
assert_match "FooJobTest", output
assert_match "BarJobTest", output
@@ -107,12 +132,12 @@ 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|
+ create_test_file :mailers, "foo_mailer"
+ create_test_file :controllers, "bar_controller"
+ create_test_file :functional, "baz_functional"
+ create_test_file :models, "foo"
+
+ rails("test:functionals").tap do |output|
assert_match "FooMailerTest", output
assert_match "BarControllerTest", output
assert_match "BazFunctionalTest", output
@@ -121,8 +146,8 @@ module ApplicationTests
end
def test_run_integration
- create_test_file :integration, 'foo_integration'
- create_test_file :models, 'foo'
+ create_test_file :integration, "foo_integration"
+ create_test_file :models, "foo"
run_test_command("test/integration").tap do |output|
assert_match "FooIntegration", output
assert_match "1 runs, 1 assertions, 0 failures", output
@@ -132,14 +157,14 @@ module ApplicationTests
def test_run_all_suites
suites = [:models, :helpers, :unit, :controllers, :mailers, :functional, :integration, :jobs]
suites.each { |suite| create_test_file suite, "foo_#{suite}" }
- run_test_command('') .tap do |output|
+ run_test_command("") .tap do |output|
suites.each { |suite| assert_match "Foo#{suite.to_s.camelize}Test", output }
assert_match "8 runs, 8 assertions, 0 failures", output
end
end
def test_run_named_test
- app_file 'test/unit/chu_2_koi_test.rb', <<-RUBY
+ app_file "test/unit/chu_2_koi_test.rb", <<-RUBY
require 'test_helper'
class Chu2KoiTest < ActiveSupport::TestCase
@@ -153,14 +178,14 @@ module ApplicationTests
end
RUBY
- run_test_command('-n test_rikka test/unit/chu_2_koi_test.rb').tap do |output|
+ run_test_command("-n test_rikka test/unit/chu_2_koi_test.rb").tap do |output|
assert_match "Rikka", output
assert_no_match "Sanae", output
end
end
def test_run_matched_test
- app_file 'test/unit/chu_2_koi_test.rb', <<-RUBY
+ app_file "test/unit/chu_2_koi_test.rb", <<-RUBY
require 'test_helper'
class Chu2KoiTest < ActiveSupport::TestCase
@@ -174,7 +199,7 @@ module ApplicationTests
end
RUBY
- run_test_command('-n /rikka/ test/unit/chu_2_koi_test.rb').tap do |output|
+ run_test_command("-n /rikka/ test/unit/chu_2_koi_test.rb").tap do |output|
assert_match "Rikka", output
assert_no_match "Sanae", output
end
@@ -195,14 +220,14 @@ module ApplicationTests
def test_run_with_model
skip "These feel a bit odd. Not sure we should keep supporting them."
create_model_with_fixture
- create_fixture_test 'models', 'user'
+ create_fixture_test "models", "user"
assert_match "3 users", run_task(["test models/user"])
assert_match "3 users", run_task(["test app/models/user.rb"])
end
def test_run_different_environment_using_env_var
skip "no longer possible. Running tests in a different environment should be explicit"
- app_file 'test/unit/env_test.rb', <<-RUBY
+ app_file "test/unit/env_test.rb", <<-RUBY
require 'test_helper'
class EnvTest < ActiveSupport::TestCase
@@ -212,14 +237,14 @@ module ApplicationTests
end
RUBY
- ENV['RAILS_ENV'] = 'development'
- assert_match "development", run_test_command('test/unit/env_test.rb')
+ ENV["RAILS_ENV"] = "development"
+ assert_match "development", run_test_command("test/unit/env_test.rb")
end
def test_run_in_test_environment_by_default
create_env_test
- assert_match "Current Environment: test", run_test_command('test/unit/env_test.rb')
+ assert_match "Current Environment: test", run_test_command("test/unit/env_test.rb")
end
def test_run_different_environment
@@ -231,22 +256,39 @@ module ApplicationTests
def test_generated_scaffold_works_with_rails_test
create_scaffold
- assert_match "0 failures, 0 errors, 0 skips", run_test_command('')
+ assert_match "0 failures, 0 errors, 0 skips", run_test_command("")
+ end
+
+ def test_generated_controller_works_with_rails_test
+ create_controller
+ assert_match "0 failures, 0 errors, 0 skips", run_test_command("")
end
def test_run_multiple_folders
- create_test_file :models, 'account'
- create_test_file :controllers, 'accounts_controller'
+ create_test_file :models, "account"
+ create_test_file :controllers, "accounts_controller"
+
+ run_test_command("test/models test/controllers").tap do |output|
+ assert_match "AccountTest", output
+ assert_match "AccountsControllerTest", output
+ assert_match "2 runs, 2 assertions, 0 failures, 0 errors, 0 skips", output
+ end
+ end
- run_test_command('test/models test/controllers').tap do |output|
- assert_match 'AccountTest', output
- assert_match 'AccountsControllerTest', output
- assert_match '2 runs, 2 assertions, 0 failures, 0 errors, 0 skips', output
+ def test_run_multiple_folders_with_absolute_paths
+ create_test_file :models, "account"
+ create_test_file :controllers, "accounts_controller"
+ create_test_file :helpers, "foo_helper"
+
+ run_test_command("#{app_path}/test/models #{app_path}/test/controllers").tap do |output|
+ assert_match "AccountTest", output
+ assert_match "AccountsControllerTest", output
+ assert_match "2 runs, 2 assertions, 0 failures, 0 errors, 0 skips", output
end
end
def test_run_with_ruby_command
- app_file 'test/models/post_test.rb', <<-RUBY
+ app_file "test/models/post_test.rb", <<-RUBY
require 'test_helper'
class PostTest < ActiveSupport::TestCase
@@ -259,15 +301,15 @@ module ApplicationTests
Dir.chdir(app_path) do
`ruby -Itest test/models/post_test.rb`.tap do |output|
- assert_match 'PostTest', output
- assert_no_match 'is already defined in', output
+ assert_match "PostTest", output
+ assert_no_match "is already defined in", output
end
end
end
def test_mix_files_and_line_filters
- create_test_file :models, 'account'
- app_file 'test/models/post_test.rb', <<-RUBY
+ create_test_file :models, "account"
+ app_file "test/models/post_test.rb", <<-RUBY
require 'test_helper'
class PostTest < ActiveSupport::TestCase
@@ -282,73 +324,503 @@ module ApplicationTests
end
RUBY
- run_test_command('test/models/account_test.rb test/models/post_test.rb:4').tap do |output|
- assert_match 'AccountTest', output
- assert_match 'PostTest', output
- assert_match '2 runs, 2 assertions', output
+ run_test_command("test/models/account_test.rb test/models/post_test.rb:4").tap do |output|
+ assert_match "AccountTest", output
+ assert_match "PostTest", output
+ assert_match "2 runs, 2 assertions", output
+ end
+ end
+
+ def test_more_than_one_line_filter
+ app_file "test/models/post_test.rb", <<-RUBY
+ require 'test_helper'
+
+ class PostTest < ActiveSupport::TestCase
+ test "first filter" do
+ puts 'PostTest:FirstFilter'
+ assert true
+ end
+
+ test "second filter" do
+ puts 'PostTest:SecondFilter'
+ assert true
+ end
+
+ test "line filter does not run this" do
+ assert true
+ end
+ end
+ RUBY
+
+ run_test_command("test/models/post_test.rb:4:9").tap do |output|
+ assert_match "PostTest:FirstFilter", output
+ assert_match "PostTest:SecondFilter", output
+ assert_match "2 runs, 2 assertions", output
+ end
+ end
+
+ def test_more_than_one_line_filter_with_multiple_files
+ app_file "test/models/account_test.rb", <<-RUBY
+ require 'test_helper'
+
+ class AccountTest < ActiveSupport::TestCase
+ test "first filter" do
+ puts 'AccountTest:FirstFilter'
+ assert true
+ end
+
+ test "second filter" do
+ puts 'AccountTest:SecondFilter'
+ assert true
+ end
+
+ test "line filter does not run this" do
+ assert true
+ end
+ end
+ RUBY
+
+ app_file "test/models/post_test.rb", <<-RUBY
+ require 'test_helper'
+
+ class PostTest < ActiveSupport::TestCase
+ test "first filter" do
+ puts 'PostTest:FirstFilter'
+ assert true
+ end
+
+ test "second filter" do
+ puts 'PostTest:SecondFilter'
+ assert true
+ end
+
+ test "line filter does not run this" do
+ assert true
+ end
+ end
+ RUBY
+
+ run_test_command("test/models/account_test.rb:4:9 test/models/post_test.rb:4:9").tap do |output|
+ assert_match "AccountTest:FirstFilter", output
+ assert_match "AccountTest:SecondFilter", output
+ assert_match "PostTest:FirstFilter", output
+ assert_match "PostTest:SecondFilter", output
+ assert_match "4 runs, 4 assertions", output
end
end
def test_multiple_line_filters
- create_test_file :models, 'account'
- create_test_file :models, 'post'
+ create_test_file :models, "account"
+ create_test_file :models, "post"
- run_test_command('test/models/account_test.rb:4 test/models/post_test.rb:4').tap do |output|
- assert_match 'AccountTest', output
- assert_match 'PostTest', output
+ run_test_command("test/models/account_test.rb:4 test/models/post_test.rb:4").tap do |output|
+ assert_match "AccountTest", output
+ assert_match "PostTest", output
end
end
- def test_line_filter_without_line_runs_all_tests
- create_test_file :models, 'account'
+ def test_line_filters_trigger_only_one_runnable
+ app_file "test/models/post_test.rb", <<-RUBY
+ require 'test_helper'
+
+ class PostTest < ActiveSupport::TestCase
+ test 'truth' do
+ assert true
+ end
+ end
+
+ class SecondPostTest < ActiveSupport::TestCase
+ test 'truth' do
+ assert false, 'ran second runnable'
+ end
+ end
+ RUBY
- run_test_command('test/models/account_test.rb:').tap do |output|
- assert_match 'AccountTest', output
+ # Pass seed guaranteeing failure.
+ run_test_command("test/models/post_test.rb:4 --seed 30410").tap do |output|
+ assert_no_match "ran second runnable", output
+ assert_match "1 runs, 1 assertions", output
+ end
+ end
+
+ def test_line_filter_with_minitest_string_filter
+ app_file "test/models/post_test.rb", <<-RUBY
+ require 'test_helper'
+
+ class PostTest < ActiveSupport::TestCase
+ test 'by line' do
+ puts 'by line'
+ assert true
+ end
+
+ test 'by name' do
+ puts 'by name'
+ assert true
+ end
+ end
+ RUBY
+
+ run_test_command("test/models/post_test.rb:4 -n test_by_name").tap do |output|
+ assert_match "by line", output
+ assert_match "by name", output
+ assert_match "2 runs, 2 assertions", output
end
end
def test_shows_filtered_backtrace_by_default
create_backtrace_test
- assert_match 'Rails::BacktraceCleaner', run_test_command('test/unit/backtrace_test.rb')
+ assert_match "Rails::BacktraceCleaner", run_test_command("test/unit/backtrace_test.rb")
end
def test_backtrace_option
create_backtrace_test
- assert_match 'Minitest::BacktraceFilter', run_test_command('test/unit/backtrace_test.rb -b')
- assert_match 'Minitest::BacktraceFilter',
- run_test_command('test/unit/backtrace_test.rb --backtrace')
+ assert_match "Minitest::BacktraceFilter", run_test_command("test/unit/backtrace_test.rb -b")
+ assert_match "Minitest::BacktraceFilter",
+ run_test_command("test/unit/backtrace_test.rb --backtrace")
end
def test_show_full_backtrace_using_backtrace_environment_variable
create_backtrace_test
- switch_env 'BACKTRACE', 'true' do
- assert_match 'Minitest::BacktraceFilter', run_test_command('test/unit/backtrace_test.rb')
+ switch_env "BACKTRACE", "true" do
+ assert_match "Minitest::BacktraceFilter", run_test_command("test/unit/backtrace_test.rb")
end
end
def test_run_app_without_rails_loaded
# Simulate a real Rails app boot.
- app_file 'config/boot.rb', <<-RUBY
- ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
+ app_file "config/boot.rb", <<-RUBY
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
require 'bundler/setup' # Set up gems listed in the Gemfile.
RUBY
- assert_match '0 runs, 0 assertions', run_test_command('')
+ assert_match "0 runs, 0 assertions", run_test_command("")
+ end
+
+ def test_output_inline_by_default
+ create_test_file :models, "post", pass: false, print: false
+
+ output = run_test_command("test/models/post_test.rb")
+ expect = %r{Running:\n\nF\n\nFailure:\nPostTest#test_truth \[[^\]]+test/models/post_test.rb:6\]:\nwups!\n\nrails test test/models/post_test.rb:4\n\n\n\n}
+ assert_match expect, output
+ end
+
+ def test_only_inline_failure_output
+ create_test_file :models, "post", pass: false
+
+ output = run_test_command("test/models/post_test.rb")
+ assert_match %r{Finished in.*\n1 runs, 1 assertions}, output
+ end
+
+ def test_fail_fast
+ create_test_file :models, "post", pass: false
+
+ assert_match(/Interrupt/,
+ capture(:stderr) { run_test_command("test/models/post_test.rb --fail-fast", stderr: true) })
+ end
+
+ def test_run_in_parallel_with_processes
+ file_name = create_parallel_processes_test_file
+
+ app_file "db/schema.rb", <<-RUBY
+ ActiveRecord::Schema.define(version: 1) do
+ create_table :users do |t|
+ t.string :name
+ end
+ end
+ RUBY
+
+ output = run_test_command(file_name)
+
+ assert_match %r{Finished in.*\n2 runs, 2 assertions}, output
+ assert_no_match "create_table(:users)", output
+ end
+
+ def test_run_in_parallel_with_threads
+ app_path("/test/test_helper.rb") do |file_name|
+ file = File.read(file_name)
+ file.sub!(/parallelize\(([^\)]*)\)/, "parallelize(\\1, with: :threads)")
+ File.write(file_name, file)
+ end
+
+ file_name = create_parallel_threads_test_file
+
+ app_file "db/schema.rb", <<-RUBY
+ ActiveRecord::Schema.define(version: 1) do
+ create_table :users do |t|
+ t.string :name
+ end
+ end
+ RUBY
+
+ output = run_test_command(file_name)
+
+ assert_match %r{Finished in.*\n2 runs, 2 assertions}, output
+ assert_no_match "create_table(:users)", output
+ end
+
+ def test_run_in_parallel_with_unmarshable_exception
+ file = app_file "test/fail_test.rb", <<-RUBY
+ require "test_helper"
+ class FailTest < ActiveSupport::TestCase
+ class BadError < StandardError
+ def initialize
+ super
+ @proc = ->{ }
+ end
+ end
+
+ test "fail" do
+ raise BadError
+ assert true
+ end
+ end
+ RUBY
+
+ output = run_test_command(file)
+
+ assert_match "DRb::DRbRemoteError: FailTest::BadError", output
+ assert_match "1 runs, 0 assertions, 0 failures, 1 errors", output
+ end
+
+ def test_run_in_parallel_with_unknown_object
+ create_scaffold
+ app_file "config/environments/test.rb", <<-RUBY
+ Rails.application.configure do
+ config.action_controller.allow_forgery_protection = true
+ config.action_dispatch.show_exceptions = false
+ end
+ RUBY
+
+ output = run_test_command("-n test_should_create_user")
+
+ assert_match "ActionController::InvalidAuthenticityToken", output
+ end
+
+ def test_raise_error_when_specified_file_does_not_exist
+ error = capture(:stderr) { run_test_command("test/not_exists.rb", stderr: true) }
+ assert_match(%r{cannot load such file.+test/not_exists\.rb}, error)
+ end
+
+ def test_pass_TEST_env_on_rake_test
+ create_test_file :models, "account"
+ create_test_file :models, "post", pass: false
+ # This specifically verifies TEST for backwards compatibility with rake test
+ # as `rails test` already supports running tests from a single file more cleanly.
+ output = Dir.chdir(app_path) { `bin/rake test TEST=test/models/post_test.rb` }
+
+ assert_match "PostTest", output, "passing TEST= should run selected test"
+ assert_no_match "AccountTest", output, "passing TEST= should only run selected test"
+ assert_match "1 runs, 1 assertions", output
+ end
+
+ def test_pass_rake_options
+ create_test_file :models, "account"
+ output = Dir.chdir(app_path) { `bin/rake --rakefile Rakefile --trace=stdout test` }
+
+ assert_match "1 runs, 1 assertions", output
+ assert_match "Execute test", output
+ end
+
+ def test_rails_db_create_all_restores_db_connection
+ create_test_file :models, "account"
+ rails "db:create:all", "db:migrate"
+ output = Dir.chdir(app_path) { `echo ".tables" | rails dbconsole` }
+ assert_match "ar_internal_metadata", output, "tables should be dumped"
+ end
+
+ def test_rails_db_create_all_restores_db_connection_after_drop
+ create_test_file :models, "account"
+ rails "db:create:all" # create all to avoid warnings
+ rails "db:drop:all", "db:create:all", "db:migrate"
+ output = Dir.chdir(app_path) { `echo ".tables" | rails dbconsole` }
+ assert_match "ar_internal_metadata", output, "tables should be dumped"
+ end
+
+ def test_rake_passes_TESTOPTS_to_minitest
+ create_test_file :models, "account"
+ output = Dir.chdir(app_path) { `bin/rake test TESTOPTS=-v` }
+ assert_match "AccountTest#test_truth", output, "passing TEST= should run selected test"
+ end
+
+ def test_running_with_ruby_gets_test_env_by_default
+ # Subshells inherit `ENV`, so we need to ensure `RAILS_ENV` is set to
+ # nil before we run the tests in the test app.
+ re, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], nil
+
+ file = create_test_for_env("test")
+ results = Dir.chdir(app_path) {
+ `ruby -Ilib:test #{file}`.each_line.map { |line| JSON.parse line }
+ }
+ assert_equal 1, results.length
+ failures = results.first["failures"]
+ flunk(failures.first) unless failures.empty?
+
+ ensure
+ ENV["RAILS_ENV"] = re
+ end
+
+ def test_running_with_ruby_can_set_env_via_cmdline
+ # Subshells inherit `ENV`, so we need to ensure `RAILS_ENV` is set to
+ # nil before we run the tests in the test app.
+ re, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], nil
+
+ file = create_test_for_env("development")
+ results = Dir.chdir(app_path) {
+ `RAILS_ENV=development ruby -Ilib:test #{file}`.each_line.map { |line| JSON.parse line }
+ }
+ assert_equal 1, results.length
+ failures = results.first["failures"]
+ flunk(failures.first) unless failures.empty?
+
+ ensure
+ ENV["RAILS_ENV"] = re
+ end
+
+ def test_rake_passes_multiple_TESTOPTS_to_minitest
+ create_test_file :models, "account"
+ output = Dir.chdir(app_path) { `bin/rake test TESTOPTS='-v --seed=1234'` }
+ assert_match "AccountTest#test_truth", output, "passing TEST= should run selected test"
+ 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", stderr: true) })
+ 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_reset_sessions_on_failed_system_test_screenshot
+ app_file "test/system/reset_sessions_on_failed_system_test_screenshot_test.rb", <<~RUBY
+ require "application_system_test_case"
+
+ class ResetSessionsOnFailedSystemTestScreenshotTest < ApplicationSystemTestCase
+ ActionDispatch::SystemTestCase.class_eval do
+ def take_failed_screenshot
+ raise Capybara::CapybaraError
+ end
+ end
+
+ Capybara.instance_eval do
+ def reset_sessions!
+ puts "Capybara.reset_sessions! called"
+ end
+ end
+
+ test "dummy" do
+ end
+ end
+ RUBY
+ output = run_test_command("test/system/reset_sessions_on_failed_system_test_screenshot_test.rb")
+ assert_match "Capybara.reset_sessions! called", output
+ 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}` }
+ def run_test_command(arguments = "test/unit/test_test.rb", **opts)
+ rails "t", *Shellwords.split(arguments), allow_failure: true, **opts
end
def create_model_with_fixture
- script 'generate model user name:string'
+ rails "generate", "model", "user", "name:string"
- app_file 'test/fixtures/users.yml', <<-YAML.strip_heredoc
+ app_file "test/fixtures/users.yml", <<~YAML
vampire:
id: 1
name: Koyomi Araragi
@@ -363,7 +835,7 @@ module ApplicationTests
run_migration
end
- def create_fixture_test(path = :unit, name = 'test')
+ def create_fixture_test(path = :unit, name = "test")
app_file "test/#{path}/#{name}_test.rb", <<-RUBY
require 'test_helper'
@@ -376,7 +848,7 @@ module ApplicationTests
end
def create_backtrace_test
- app_file 'test/unit/backtrace_test.rb', <<-RUBY
+ app_file "test/unit/backtrace_test.rb", <<-RUBY
require 'test_helper'
class BacktraceTest < ActiveSupport::TestCase
@@ -388,24 +860,114 @@ module ApplicationTests
end
def create_schema
- app_file 'db/schema.rb', ''
+ app_file "db/schema.rb", ""
+ end
+
+ def create_test_for_env(env)
+ app_file "test/models/environment_test.rb", <<-RUBY
+ require 'test_helper'
+ class JSONReporter < Minitest::AbstractReporter
+ def record(result)
+ puts JSON.dump(klass: result.class.name,
+ name: result.name,
+ failures: result.failures,
+ assertions: result.assertions,
+ time: result.time)
+ end
+ end
+
+ def Minitest.plugin_json_reporter_init(opts)
+ Minitest.reporter.reporters.clear
+ Minitest.reporter.reporters << JSONReporter.new
+ end
+
+ Minitest.extensions << "rails"
+ Minitest.extensions << "json_reporter"
+
+ # Minitest uses RubyGems to find plugins, and since RubyGems
+ # doesn't know about the Rails installation we're pointing at,
+ # Minitest won't require the Rails minitest plugin when we run
+ # these integration tests. So we have to manually require the
+ # Minitest plugin here.
+ require 'minitest/rails_plugin'
+
+ class EnvironmentTest < ActiveSupport::TestCase
+ def test_environment
+ test_db = ActiveRecord::Base.configurations[#{env.dump}]["database"]
+ db_file = ActiveRecord::Base.connection_config[:database]
+ assert_match(test_db, db_file)
+ assert_equal #{env.dump}, ENV["RAILS_ENV"]
+ end
+ end
+ RUBY
end
- def create_test_file(path = :unit, name = 'test')
+ def create_test_file(path = :unit, name = "test", pass: true, print: true)
app_file "test/#{path}/#{name}_test.rb", <<-RUBY
require 'test_helper'
class #{name.camelize}Test < ActiveSupport::TestCase
def test_truth
- puts "#{name.camelize}Test"
- assert true
+ puts "#{name.camelize}Test" if #{print}
+ assert #{pass}, 'wups!'
+ end
+ end
+ RUBY
+ end
+
+ def create_parallel_processes_test_file
+ app_file "test/models/parallel_test.rb", <<-RUBY
+ require 'test_helper'
+
+ class ParallelTest < ActiveSupport::TestCase
+ RD1, WR1 = IO.pipe
+ RD2, WR2 = IO.pipe
+
+ test "one" do
+ WR1.close
+ assert_equal "x", RD1.read(1) # blocks until two runs
+
+ RD2.close
+ WR2.write "y" # Allow two to run
+ WR2.close
+ end
+
+ test "two" do
+ RD1.close
+ WR1.write "x" # Allow one to run
+ WR1.close
+
+ WR2.close
+ assert_equal "y", RD2.read(1) # blocks until one runs
+ end
+ end
+ RUBY
+ end
+
+ def create_parallel_threads_test_file
+ app_file "test/models/parallel_test.rb", <<-RUBY
+ require 'test_helper'
+
+ class ParallelTest < ActiveSupport::TestCase
+ Q1 = Queue.new
+ Q2 = Queue.new
+ test "one" do
+ assert_equal "x", Q1.pop # blocks until two runs
+
+ Q2 << "y"
+ end
+
+ test "two" do
+ Q1 << "x"
+
+ assert_equal "y", Q2.pop # blocks until one runs
end
end
RUBY
end
def create_env_test
- app_file 'test/unit/env_test.rb', <<-RUBY
+ app_file "test/unit/env_test.rb", <<-RUBY
require 'test_helper'
class EnvTest < ActiveSupport::TestCase
@@ -417,13 +979,17 @@ module ApplicationTests
end
def create_scaffold
- script 'generate scaffold user name:string'
- Dir.chdir(app_path) { File.exist?('app/models/user.rb') }
+ rails "generate", "scaffold", "user", "name:string"
+ assert File.exist?("#{app_path}/app/models/user.rb")
run_migration
end
+ def create_controller
+ rails "generate", "controller", "admin/dashboard", "index"
+ end
+
def run_migration
- Dir.chdir(app_path) { `bin/rake db:migrate` }
+ rails "db:migrate"
end
end
end
diff --git a/railties/test/application/test_test.rb b/railties/test/application/test_test.rb
index 0e997f4ba7..fb43bebfbe 100644
--- a/railties/test/application/test_test.rb
+++ b/railties/test/application/test_test.rb
@@ -1,20 +1,26 @@
-require 'isolation/abstract_unit'
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
module ApplicationTests
class TestTest < ActiveSupport::TestCase
include ActiveSupport::Testing::Isolation
def setup
+ @old = ENV["PARALLEL_WORKERS"]
+ ENV["PARALLEL_WORKERS"] = "0"
+
build_app
- boot_rails
end
def teardown
+ ENV["PARALLEL_WORKERS"] = @old
+
teardown_app
end
- test "truth" do
- app_file 'test/unit/foo_test.rb', <<-RUBY
+ test "simple successful test" do
+ app_file "test/unit/foo_test.rb", <<-RUBY
require 'test_helper'
class FooTest < ActiveSupport::TestCase
@@ -24,20 +30,52 @@ module ApplicationTests
end
RUBY
- assert_successful_test_run 'unit/foo_test.rb'
+ assert_successful_test_run "unit/foo_test.rb"
+ end
+
+ test "after_run" do
+ app_file "test/unit/foo_test.rb", <<-RUBY
+ require 'test_helper'
+
+ Minitest.after_run { puts "WORLD" }
+ Minitest.after_run { puts "HELLO" }
+
+ class FooTest < ActiveSupport::TestCase
+ def test_truth
+ assert true
+ end
+ end
+ RUBY
+
+ result = assert_successful_test_run "unit/foo_test.rb"
+ assert_equal ["HELLO", "WORLD"], result.scan(/HELLO|WORLD/) # only once and in correct order
+ end
+
+ test "simple failed test" do
+ app_file "test/unit/foo_test.rb", <<-RUBY
+ require 'test_helper'
+
+ class FooTest < ActiveSupport::TestCase
+ def test_truth
+ assert false
+ end
+ end
+ RUBY
+
+ assert_unsuccessful_run "unit/foo_test.rb", "Failure:\nFooTest#test_truth"
end
test "integration test" do
- controller 'posts', <<-RUBY
+ controller "posts", <<-RUBY
class PostsController < ActionController::Base
end
RUBY
- app_file 'app/views/posts/index.html.erb', <<-HTML
+ app_file "app/views/posts/index.html.erb", <<-HTML
Posts#index
HTML
- app_file 'test/integration/posts_test.rb', <<-RUBY
+ app_file "test/integration/posts_test.rb", <<-RUBY
require 'test_helper'
class PostsTest < ActionDispatch::IntegrationTest
@@ -49,11 +87,11 @@ module ApplicationTests
end
RUBY
- assert_successful_test_run 'integration/posts_test.rb'
+ assert_successful_test_run "integration/posts_test.rb"
end
test "enable full backtraces on test failures" do
- app_file 'test/unit/failing_test.rb', <<-RUBY
+ app_file "test/unit/failing_test.rb", <<-RUBY
require 'test_helper'
class FailingTest < ActiveSupport::TestCase
@@ -63,16 +101,16 @@ module ApplicationTests
end
RUBY
- output = run_test_file('unit/failing_test.rb', env: { "BACKTRACE" => "1" })
+ output = run_test_file("unit/failing_test.rb", env: { "BACKTRACE" => "1" })
assert_match %r{test/unit/failing_test\.rb}, output
assert_match %r{test/unit/failing_test\.rb:4}, output
end
test "ruby schema migrations" do
- output = script('generate model user name:string')
+ output = rails("generate", "model", "user", "name:string")
version = output.match(/(\d+)_create_users\.rb/)[1]
- app_file 'test/models/user_test.rb', <<-RUBY
+ app_file "test/models/user_test.rb", <<-RUBY
require 'test_helper'
class UserTest < ActiveSupport::TestCase
@@ -81,11 +119,11 @@ module ApplicationTests
end
end
RUBY
- app_file 'db/schema.rb', ''
+ app_file "db/schema.rb", ""
assert_unsuccessful_run "models/user_test.rb", "Migrations are pending"
- app_file 'db/schema.rb', <<-RUBY
+ app_file "db/schema.rb", <<-RUBY
ActiveRecord::Schema.define(version: #{version}) do
create_table :users do |t|
t.string :name
@@ -93,7 +131,7 @@ module ApplicationTests
end
RUBY
- app_file 'config/initializers/disable_maintain_test_schema.rb', <<-RUBY
+ app_file "config/initializers/disable_maintain_test_schema.rb", <<-RUBY
Rails.application.config.active_record.maintain_test_schema = false
RUBY
@@ -101,15 +139,15 @@ module ApplicationTests
File.delete "#{app_path}/config/initializers/disable_maintain_test_schema.rb"
- result = assert_successful_test_run('models/user_test.rb')
- assert !result.include?("create_table(:users)")
+ result = assert_successful_test_run("models/user_test.rb")
+ assert_not_includes result, "create_table(:users)"
end
test "sql structure migrations" do
- output = script('generate model user name:string')
+ output = rails("generate", "model", "user", "name:string")
version = output.match(/(\d+)_create_users\.rb/)[1]
- app_file 'test/models/user_test.rb', <<-RUBY
+ app_file "test/models/user_test.rb", <<-RUBY
require 'test_helper'
class UserTest < ActiveSupport::TestCase
@@ -119,21 +157,21 @@ module ApplicationTests
end
RUBY
- app_file 'db/structure.sql', ''
- app_file 'config/initializers/enable_sql_schema_format.rb', <<-RUBY
+ app_file "db/structure.sql", ""
+ app_file "config/initializers/enable_sql_schema_format.rb", <<-RUBY
Rails.application.config.active_record.schema_format = :sql
RUBY
assert_unsuccessful_run "models/user_test.rb", "Migrations are pending"
- app_file 'db/structure.sql', <<-SQL
+ app_file "db/structure.sql", <<-SQL
CREATE TABLE "schema_migrations" ("version" varchar(255) NOT NULL);
CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version");
CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(255));
INSERT INTO schema_migrations (version) VALUES ('#{version}');
SQL
- app_file 'config/initializers/disable_maintain_test_schema.rb', <<-RUBY
+ app_file "config/initializers/disable_maintain_test_schema.rb", <<-RUBY
Rails.application.config.active_record.maintain_test_schema = false
RUBY
@@ -141,14 +179,14 @@ module ApplicationTests
File.delete "#{app_path}/config/initializers/disable_maintain_test_schema.rb"
- assert_successful_test_run('models/user_test.rb')
+ assert_successful_test_run("models/user_test.rb")
end
test "sql structure migrations when adding column to existing table" do
- output_1 = script('generate model user name:string')
+ output_1 = rails("generate", "model", "user", "name:string")
version_1 = output_1.match(/(\d+)_create_users\.rb/)[1]
- app_file 'test/models/user_test.rb', <<-RUBY
+ app_file "test/models/user_test.rb", <<-RUBY
require 'test_helper'
class UserTest < ActiveSupport::TestCase
test "user" do
@@ -157,23 +195,23 @@ module ApplicationTests
end
RUBY
- app_file 'config/initializers/enable_sql_schema_format.rb', <<-RUBY
+ app_file "config/initializers/enable_sql_schema_format.rb", <<-RUBY
Rails.application.config.active_record.schema_format = :sql
RUBY
- app_file 'db/structure.sql', <<-SQL
+ app_file "db/structure.sql", <<-SQL
CREATE TABLE "schema_migrations" ("version" varchar(255) NOT NULL);
CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version");
CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(255));
INSERT INTO schema_migrations (version) VALUES ('#{version_1}');
SQL
- assert_successful_test_run('models/user_test.rb')
+ assert_successful_test_run("models/user_test.rb")
- output_2 = script('generate migration add_email_to_users')
+ output_2 = rails("generate", "migration", "add_email_to_users")
version_2 = output_2.match(/(\d+)_add_email_to_users\.rb/)[1]
- app_file 'test/models/user_test.rb', <<-RUBY
+ app_file "test/models/user_test.rb", <<-RUBY
require 'test_helper'
class UserTest < ActiveSupport::TestCase
@@ -183,7 +221,7 @@ module ApplicationTests
end
RUBY
- app_file 'db/structure.sql', <<-SQL
+ app_file "db/structure.sql", <<-SQL
CREATE TABLE "schema_migrations" ("version" varchar(255) NOT NULL);
CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version");
CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(255), "email" varchar(255));
@@ -191,17 +229,17 @@ module ApplicationTests
INSERT INTO schema_migrations (version) VALUES ('#{version_2}');
SQL
- assert_successful_test_run('models/user_test.rb')
+ assert_successful_test_run("models/user_test.rb")
end
# TODO: would be nice if we could detect the schema change automatically.
# For now, the user has to synchronize the schema manually.
# This test-case serves as a reminder for this use-case.
test "manually synchronize test schema after rollback" do
- output = script('generate model user name:string')
+ output = rails("generate", "model", "user", "name:string")
version = output.match(/(\d+)_create_users\.rb/)[1]
- app_file 'test/models/user_test.rb', <<-RUBY
+ app_file "test/models/user_test.rb", <<-RUBY
require 'test_helper'
class UserTest < ActiveSupport::TestCase
@@ -210,7 +248,7 @@ module ApplicationTests
end
end
RUBY
- app_file 'db/schema.rb', <<-RUBY
+ app_file "db/schema.rb", <<-RUBY
ActiveRecord::Schema.define(version: #{version}) do
create_table :users do |t|
t.string :name
@@ -221,7 +259,7 @@ module ApplicationTests
assert_successful_test_run "models/user_test.rb"
# Simulate `db:rollback` + edit of the migration file + `db:migrate`
- app_file 'db/schema.rb', <<-RUBY
+ app_file "db/schema.rb", <<-RUBY
ActiveRecord::Schema.define(version: #{version}) do
create_table :users do |t|
t.string :name
@@ -232,7 +270,7 @@ module ApplicationTests
assert_successful_test_run "models/user_test.rb"
- Dir.chdir(app_path) { `bin/rake db:test:prepare` }
+ rails "db:test:prepare"
assert_unsuccessful_run "models/user_test.rb", <<-ASSERTION
Expected: ["id", "name"]
@@ -241,10 +279,10 @@ Expected: ["id", "name"]
end
test "hooks for plugins" do
- output = script('generate model user name:string')
+ output = rails("generate", "model", "user", "name:string")
version = output.match(/(\d+)_create_users\.rb/)[1]
- app_file 'lib/tasks/hooks.rake', <<-RUBY
+ app_file "lib/tasks/hooks.rake", <<-RUBY
task :before_hook do
has_user_table = ActiveRecord::Base.connection.table_exists?('users')
puts "before: " + has_user_table.to_s
@@ -259,7 +297,7 @@ Expected: ["id", "name"]
Rake::Task[:after_hook].invoke
end
RUBY
- app_file 'test/models/user_test.rb', <<-RUBY
+ app_file "test/models/user_test.rb", <<-RUBY
require 'test_helper'
class UserTest < ActiveSupport::TestCase
test "user" do
@@ -269,7 +307,7 @@ Expected: ["id", "name"]
RUBY
# Simulate `db:migrate`
- app_file 'db/schema.rb', <<-RUBY
+ app_file "db/schema.rb", <<-RUBY
ActiveRecord::Schema.define(version: #{version}) do
create_table :users do |t|
t.string :name
@@ -290,7 +328,7 @@ Expected: ["id", "name"]
def assert_unsuccessful_run(name, message)
result = run_test_file(name)
assert_not_equal 0, $?.to_i
- assert result.include?(message)
+ assert_includes result, message
result
end
@@ -301,7 +339,7 @@ Expected: ["id", "name"]
end
def run_test_file(name, options = {})
- Dir.chdir(app_path) { `bin/rails test "#{app_path}/test/#{name}" 2>&1` }
+ rails "test", "#{app_path}/test/#{name}", allow_failure: true
end
end
end
diff --git a/railties/test/application/url_generation_test.rb b/railties/test/application/url_generation_test.rb
index 894e18cb39..f22b9fda3d 100644
--- a/railties/test/application/url_generation_test.rb
+++ b/railties/test/application/url_generation_test.rb
@@ -1,4 +1,6 @@
-require 'isolation/abstract_unit'
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
module ApplicationTests
class UrlGenerationTest < ActiveSupport::TestCase
@@ -9,13 +11,11 @@ module ApplicationTests
end
test "it works" do
- boot_rails
require "rails"
require "action_controller/railtie"
require "action_view/railtie"
class MyApp < Rails::Application
- secrets.secret_key_base = "3b7cd727ee24e8444053437c36cc66c4"
config.session_store :cookie_store, key: "_myapp_session"
config.active_support.deprecation = :log
config.eager_load = false
@@ -28,7 +28,7 @@ module ApplicationTests
class ::OmgController < ::ApplicationController
def index
- render text: omg_path
+ render plain: omg_path
end
end
@@ -36,7 +36,7 @@ module ApplicationTests
get "/" => "omg#index", as: :omg
end
- require 'rack/test'
+ require "rack/test"
extend Rack::Test::Methods
get "/"
@@ -44,12 +44,11 @@ module ApplicationTests
end
def test_routes_know_the_relative_root
- boot_rails
require "rails"
require "action_controller/railtie"
require "action_view/railtie"
- relative_url = '/hello'
+ relative_url = "/hello"
ENV["RAILS_RELATIVE_URL_ROOT"] = relative_url
app = Class.new(Rails::Application)
assert_equal relative_url, app.routes.relative_url_root
diff --git a/railties/test/application/version_test.rb b/railties/test/application/version_test.rb
new file mode 100644
index 0000000000..ae85cf8f05
--- /dev/null
+++ b/railties/test/application/version_test.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+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 = rails("version")
+ assert_equal "Rails #{Rails.gem_version}\n", output
+ end
+
+ test "short-cut alias works" do
+ output = 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 2dd74f8fd1..90e084ddca 100644
--- a/railties/test/backtrace_cleaner_test.rb
+++ b/railties/test/backtrace_cleaner_test.rb
@@ -1,24 +1,26 @@
-require 'abstract_unit'
-require 'rails/backtrace_cleaner'
+# frozen_string_literal: true
-class BacktraceCleanerVendorGemTest < ActiveSupport::TestCase
+require "abstract_unit"
+require "rails/backtrace_cleaner"
+
+class BacktraceCleanerTest < ActiveSupport::TestCase
def setup
@cleaner = Rails::BacktraceCleaner.new
end
- test "should format installed gems correctly" do
- @backtrace = [ "#{Gem.path[0]}/gems/nosuchgem-1.2.3/lib/foo.rb" ]
- @result = @cleaner.clean(@backtrace, :all)
- assert_equal "nosuchgem (1.2.3) lib/foo.rb", @result[0]
+ test "should consider traces from irb lines as User code" do
+ backtrace = [ "(irb):1",
+ "/Path/to/rails/railties/lib/rails/commands/console.rb:77:in `start'",
+ "bin/rails:4:in `<main>'" ]
+ result = @cleaner.clean(backtrace)
+ assert_equal "(irb):1", result[0]
+ assert_equal 1, result.length
end
- 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
- @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]
- end
+ test "should omit ActionView template methods names" do
+ method_name = ActionView::Template.new(nil, "app/views/application/index.html.erb", nil, {}).send :method_name
+ backtrace = [ "app/views/application/index.html.erb:4:in `block in #{method_name}'"]
+ result = @cleaner.clean(backtrace, :all)
+ assert_equal "app/views/application/index.html.erb:4", result[0]
end
end
diff --git a/railties/test/code_statistics_calculator_test.rb b/railties/test/code_statistics_calculator_test.rb
index 46445a001a..e763cfb376 100644
--- a/railties/test/code_statistics_calculator_test.rb
+++ b/railties/test/code_statistics_calculator_test.rb
@@ -1,12 +1,14 @@
-require 'abstract_unit'
-require 'rails/code_statistics_calculator'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "rails/code_statistics_calculator"
class CodeStatisticsCalculatorTest < ActiveSupport::TestCase
def setup
@code_statistics_calculator = CodeStatisticsCalculator.new
end
- test 'calculate statistics using #add_by_file_path' do
+ test "calculate statistics using #add_by_file_path" do
code = <<-RUBY
def foo
puts 'foo'
@@ -14,7 +16,7 @@ class CodeStatisticsCalculatorTest < ActiveSupport::TestCase
end
RUBY
- temp_file 'stats.rb', code do |path|
+ temp_file "stats.rb", code do |path|
@code_statistics_calculator.add_by_file_path path
assert_equal 4, @code_statistics_calculator.lines
@@ -24,7 +26,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
@@ -37,13 +39,13 @@ class CodeStatisticsCalculatorTest < ActiveSupport::TestCase
end
RUBY
- temp_file 'foo_test.rb', code do |path|
+ temp_file "foo_test.rb", code do |path|
@code_statistics_calculator.add_by_file_path path
assert_equal 2, @code_statistics_calculator.methods
end
end
- test 'add statistics to another using #add' do
+ test "add statistics to another using #add" do
code_statistics_calculator_1 = CodeStatisticsCalculator.new(1, 2, 3, 4)
@code_statistics_calculator.add(code_statistics_calculator_1)
@@ -52,7 +54,7 @@ class CodeStatisticsCalculatorTest < ActiveSupport::TestCase
assert_equal 3, @code_statistics_calculator.classes
assert_equal 4, @code_statistics_calculator.methods
- code_statistics_calculator_2 = CodeStatisticsCalculator.new(2, 3, 4, 5)
+ code_statistics_calculator_2 = CodeStatisticsCalculator.new(2, 3, 4, 5)
@code_statistics_calculator.add(code_statistics_calculator_2)
assert_equal 3, @code_statistics_calculator.lines
@@ -61,7 +63,7 @@ class CodeStatisticsCalculatorTest < ActiveSupport::TestCase
assert_equal 9, @code_statistics_calculator.methods
end
- test 'accumulate statistics using #add_by_io' do
+ test "accumulate statistics using #add_by_io" do
code_statistics_calculator_1 = CodeStatisticsCalculator.new(1, 2, 3, 4)
@code_statistics_calculator.add(code_statistics_calculator_1)
@@ -82,7 +84,7 @@ class CodeStatisticsCalculatorTest < ActiveSupport::TestCase
assert_equal 6, @code_statistics_calculator.methods
end
- test 'calculate number of Ruby methods' do
+ test "calculate number of Ruby methods" do
code = <<-'CODE'
def foo
puts 'foo'
@@ -101,7 +103,7 @@ class CodeStatisticsCalculatorTest < ActiveSupport::TestCase
assert_equal 3, @code_statistics_calculator.methods
end
- test 'calculate Ruby LOCs' do
+ test "calculate Ruby LOCs" do
code = <<-'CODE'
def foo
puts 'foo'
@@ -119,7 +121,7 @@ class CodeStatisticsCalculatorTest < ActiveSupport::TestCase
assert_equal 5, @code_statistics_calculator.code_lines
end
- test 'calculate number of Ruby classes' do
+ test "calculate number of Ruby classes" do
code = <<-'CODE'
class Foo < Bar
def foo
@@ -138,7 +140,7 @@ class CodeStatisticsCalculatorTest < ActiveSupport::TestCase
assert_equal 2, @code_statistics_calculator.classes
end
- test 'skip Ruby comments' do
+ test "skip Ruby comments" do
code = <<-'CODE'
=begin
class Foo
@@ -160,7 +162,7 @@ class CodeStatisticsCalculatorTest < ActiveSupport::TestCase
assert_equal 0, @code_statistics_calculator.methods
end
- test 'calculate number of JS methods' do
+ test "calculate number of JS methods" do
code = <<-'CODE'
function foo(x, y, z) {
doX();
@@ -179,7 +181,7 @@ class CodeStatisticsCalculatorTest < ActiveSupport::TestCase
assert_equal 3, @code_statistics_calculator.methods
end
- test 'calculate JS LOCs' do
+ test "calculate JS LOCs" do
code = <<-'CODE'
function foo()
alert('foo');
@@ -196,7 +198,7 @@ class CodeStatisticsCalculatorTest < ActiveSupport::TestCase
assert_equal 4, @code_statistics_calculator.code_lines
end
- test 'skip JS comments' do
+ test "skip JS comments" do
code = <<-'CODE'
/*
* var f = function () {
@@ -216,7 +218,7 @@ class CodeStatisticsCalculatorTest < ActiveSupport::TestCase
assert_equal 0, @code_statistics_calculator.methods
end
- test 'calculate number of CoffeeScript methods' do
+ test "calculate number of CoffeeScript methods" do
code = <<-'CODE'
square = (x) -> x * x
@@ -235,7 +237,7 @@ class CodeStatisticsCalculatorTest < ActiveSupport::TestCase
assert_equal 4, @code_statistics_calculator.methods
end
- test 'calculate CoffeeScript LOCs' do
+ test "calculate CoffeeScript LOCs" do
code = <<-'CODE'
# Assignment:
number = 42
@@ -256,7 +258,7 @@ class CodeStatisticsCalculatorTest < ActiveSupport::TestCase
assert_equal 3, @code_statistics_calculator.code_lines
end
- test 'calculate number of CoffeeScript classes' do
+ test "calculate number of CoffeeScript classes" do
code = <<-'CODE'
class Animal
constructor: (@name) ->
@@ -277,7 +279,7 @@ class CodeStatisticsCalculatorTest < ActiveSupport::TestCase
assert_equal 2, @code_statistics_calculator.classes
end
- test 'skip CoffeeScript comments' do
+ test "skip CoffeeScript comments" do
code = <<-'CODE'
###
class Animal
@@ -299,9 +301,25 @@ class Animal
assert_equal 0, @code_statistics_calculator.methods
end
+ test "count rake tasks" do
+ code = <<-'CODE'
+ task :test_task do
+ puts 'foo'
+ end
+
+ CODE
+
+ @code_statistics_calculator.add_by_io(StringIO.new(code), :rake)
+
+ assert_equal 4, @code_statistics_calculator.lines
+ assert_equal 3, @code_statistics_calculator.code_lines
+ assert_equal 0, @code_statistics_calculator.classes
+ assert_equal 0, @code_statistics_calculator.methods
+ end
+
private
def temp_file(name, content)
- dir = File.expand_path '../fixtures/tmp', __FILE__
+ dir = File.expand_path "fixtures/tmp", __dir__
path = "#{dir}/#{name}"
FileUtils.mkdir_p dir
diff --git a/railties/test/code_statistics_test.rb b/railties/test/code_statistics_test.rb
index 1b1ff80bc1..7ad1ac3094 100644
--- a/railties/test/code_statistics_test.rb
+++ b/railties/test/code_statistics_test.rb
@@ -1,10 +1,12 @@
-require 'abstract_unit'
-require 'rails/code_statistics'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "rails/code_statistics"
class CodeStatisticsTest < ActiveSupport::TestCase
def setup
- @tmp_path = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'tmp'))
- @dir_js = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'tmp', 'lib.js'))
+ @tmp_path = File.expand_path("fixtures/tmp", __dir__)
+ @dir_js = File.join(@tmp_path, "lib.js")
FileUtils.mkdir_p(@dir_js)
end
@@ -12,9 +14,21 @@ class CodeStatisticsTest < ActiveSupport::TestCase
FileUtils.rm_rf(@tmp_path)
end
- test 'ignores directories that happen to have source files extensions' do
+ test "ignores directories that happen to have source files extensions" do
+ assert_nothing_raised do
+ @code_statistics = CodeStatistics.new(["tmp dir", @tmp_path])
+ end
+ end
+
+ test "ignores hidden files" do
+ File.write File.join(@tmp_path, ".example.rb"), <<-CODE
+ def foo
+ puts 'foo'
+ end
+ CODE
+
assert_nothing_raised do
- @code_statistics = CodeStatistics.new(['tmp dir', @tmp_path])
+ CodeStatistics.new(["hidden file", @tmp_path])
end
end
end
diff --git a/railties/test/command/base_test.rb b/railties/test/command/base_test.rb
new file mode 100644
index 0000000000..a49ae8aae7
--- /dev/null
+++ b/railties/test/command/base_test.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+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 secrets:show), Rails::Command::SecretsCommand.printing_commands
+ end
+end
diff --git a/railties/test/command/spellchecker_test.rb b/railties/test/command/spellchecker_test.rb
new file mode 100644
index 0000000000..e6a7a3957c
--- /dev/null
+++ b/railties/test/command/spellchecker_test.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "rails/command/spellchecker"
+
+class Rails::Command::SpellcheckerTest < ActiveSupport::TestCase
+ test "suggests a word correction from dictionary" do
+ assert_equal "thin", Rails::Command::Spellchecker.suggest("tin", from: %w(puma thin cgi))
+ end
+end
diff --git a/railties/test/commands/console_test.rb b/railties/test/commands/console_test.rb
index de0cf0ba9e..0b2fe204f8 100644
--- a/railties/test/commands/console_test.rb
+++ b/railties/test/commands/console_test.rb
@@ -1,6 +1,9 @@
-require 'abstract_unit'
-require 'env_helpers'
-require 'rails/commands/console'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "env_helpers"
+require "rails/command"
+require "rails/commands/console/console_command"
class Rails::ConsoleTest < ActiveSupport::TestCase
include EnvHelpers
@@ -17,37 +20,36 @@ class Rails::ConsoleTest < ActiveSupport::TestCase
def test_sandbox_option
console = Rails::Console.new(app, parse_arguments(["--sandbox"]))
- assert console.sandbox?
+ assert_predicate console, :sandbox?
end
def test_short_version_of_sandbox_option
console = Rails::Console.new(app, parse_arguments(["-s"]))
- assert console.sandbox?
+ assert_predicate console, :sandbox?
end
def test_no_options
console = Rails::Console.new(app, parse_arguments([]))
- assert !console.sandbox?
+ assert_not_predicate console, :sandbox?
end
def test_start
start
- assert app.console.started?
+ assert_predicate app.console, :started?
assert_match(/Loading \w+ environment \(Rails/, output)
end
def test_start_with_sandbox
start ["--sandbox"]
-
- assert app.console.started?
+ assert_predicate app.console, :started?
assert app.sandbox
assert_match(/Loading \w+ environment in sandbox \(Rails/, output)
end
def test_console_with_environment
- start ["-e production"]
+ start ["-e", "production"]
assert_match(/\sproduction\s/, output)
end
@@ -64,52 +66,73 @@ class Rails::ConsoleTest < ActiveSupport::TestCase
end
def test_default_environment_with_rails_env
- with_rails_env 'special-production' do
+ with_rails_env "special-production" do
start
assert_match(/\sspecial-production\s/, output)
end
end
def test_default_environment_with_rack_env
- with_rack_env 'production' do
+ with_rack_env "production" do
start
assert_match(/\sproduction\s/, output)
end
end
def test_e_option
- start ['-e', 'special-production']
+ start ["-e", "special-production"]
assert_match(/\sspecial-production\s/, output)
end
+ def test_e_option_is_properly_expanded
+ start ["-e", "prod"]
+ assert_match(/\sproduction\s/, output)
+ end
+
def test_environment_option
- start ['--environment=special-production']
+ start ["--environment=special-production"]
assert_match(/\sspecial-production\s/, output)
end
def test_rails_env_is_production_when_first_argument_is_p
- start ['p']
- assert_match(/\sproduction\s/, output)
+ assert_deprecated do
+ start ["p"]
+ assert_match(/\sproduction\s/, output)
+ end
end
def test_rails_env_is_test_when_first_argument_is_t
- start ['t']
- assert_match(/\stest\s/, output)
+ assert_deprecated do
+ start ["t"]
+ assert_match(/\stest\s/, output)
+ end
end
def test_rails_env_is_development_when_argument_is_d
- start ['d']
- assert_match(/\sdevelopment\s/, output)
+ assert_deprecated do
+ start ["d"]
+ assert_match(/\sdevelopment\s/, output)
+ end
end
def test_rails_env_is_dev_when_argument_is_dev_and_dev_env_is_present
- stubbed_console = Class.new(Rails::Console) do
- def available_environments
- ['dev']
+ Rails::Command::ConsoleCommand.class_eval do
+ alias_method :old_environments, :available_environments
+
+ define_method :available_environments do
+ ["dev"]
end
end
- options = stubbed_console.parse_arguments(['dev'])
- assert_match('dev', options[:environment])
+
+ assert_deprecated do
+ assert_match("dev", parse_arguments(["dev"])[:environment])
+ end
+ ensure
+ Rails::Command::ConsoleCommand.class_eval do
+ undef_method :available_environments
+ alias_method :available_environments, :old_environments
+ undef_method :old_environments
+ end
end
attr_reader :output
@@ -117,38 +140,37 @@ class Rails::ConsoleTest < ActiveSupport::TestCase
private
- def start(argv = [])
- rails_console = Rails::Console.new(app, parse_arguments(argv))
- @output = capture(:stdout) { rails_console.start }
- end
+ def start(argv = [])
+ rails_console = Rails::Console.new(app, parse_arguments(argv))
+ @output = capture(:stdout) { rails_console.start }
+ end
- def app
- @app ||= build_app(FakeConsole)
- end
+ def app
+ @app ||= build_app(FakeConsole)
+ end
- def build_app(console)
- mocked_console = Class.new do
- attr_reader :sandbox, :console
+ def build_app(console)
+ mocked_console = Class.new do
+ attr_accessor :sandbox
+ attr_reader :console
- def initialize(console)
- @console = console
- end
+ def initialize(console)
+ @console = console
+ end
- def config
- self
- end
-
- def sandbox=(arg)
- @sandbox = arg
- end
+ def config
+ self
+ end
- def load_console
+ def load_console
+ end
end
+ mocked_console.new(console)
end
- mocked_console.new(console)
- end
- def parse_arguments(args)
- Rails::Console.parse_arguments(args)
- end
+ def parse_arguments(args)
+ command = Rails::Command::ConsoleCommand.new([], args)
+ command.send(:extract_environment_option_from_argument)
+ command.options
+ end
end
diff --git a/railties/test/commands/credentials_test.rb b/railties/test/commands/credentials_test.rb
new file mode 100644
index 0000000000..7842b0db61
--- /dev/null
+++ b/railties/test/commands/credentials_test.rb
@@ -0,0 +1,102 @@
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "env_helpers"
+require "rails/command"
+require "rails/commands/credentials/credentials_command"
+
+class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation, EnvHelpers
+
+ setup { build_app }
+
+ teardown { teardown_app }
+
+ test "edit without editor gives hint" do
+ run_edit_command(editor: "").tap do |output|
+ assert_match "No $EDITOR to open file in", output
+ assert_match "rails credentials:edit", output
+ end
+ end
+
+ test "edit credentials" do
+ # Run twice to ensure credentials can be reread after first edit pass.
+ 2.times do
+ assert_match(/access_key_id: 123/, run_edit_command)
+ end
+ end
+
+ test "edit command does not add master key to gitignore when already exist" do
+ run_edit_command
+
+ Dir.chdir(app_path) do
+ gitignore = File.read(".gitignore")
+ assert_equal 1, gitignore.scan(%r|config/master\.key|).length
+ end
+ end
+
+ test "edit command does not overwrite by default if credentials already exists" do
+ run_edit_command(editor: "eval echo api_key: abc >")
+ assert_match(/api_key: abc/, run_show_command)
+
+ run_edit_command
+ assert_match(/api_key: abc/, run_show_command)
+ end
+
+ test "edit command does not add master key when `RAILS_MASTER_KEY` env specified" do
+ Dir.chdir(app_path) do
+ key = IO.binread("config/master.key").strip
+ FileUtils.rm("config/master.key")
+
+ switch_env("RAILS_MASTER_KEY", key) do
+ assert_match(/access_key_id: 123/, run_edit_command)
+ assert_not File.exist?("config/master.key")
+ end
+ end
+ end
+
+ test "edit command modifies file specified by environment option" do
+ assert_match(/access_key_id: 123/, run_edit_command(environment: "production"))
+ Dir.chdir(app_path) do
+ assert File.exist?("config/credentials/production.key")
+ assert File.exist?("config/credentials/production.yml.enc")
+ end
+ end
+
+ test "show credentials" do
+ assert_match(/access_key_id: 123/, run_show_command)
+ end
+
+ test "show command raise error when require_master_key is specified and key does not exist" do
+ remove_file "config/master.key"
+ add_to_config "config.require_master_key = true"
+
+ assert_match(/Missing encryption key to decrypt file with/, run_show_command(allow_failure: true))
+ end
+
+ test "show command does not raise error when require_master_key is false and master key does not exist" do
+ remove_file "config/master.key"
+ add_to_config "config.require_master_key = false"
+
+ assert_match(/Missing 'config\/master\.key' to decrypt credentials/, run_show_command)
+ end
+
+ test "show command displays content specified by environment option" do
+ run_edit_command(environment: "production")
+
+ assert_match(/access_key_id: 123/, run_show_command(environment: "production"))
+ end
+
+ private
+ def run_edit_command(editor: "cat", environment: nil, **options)
+ switch_env("EDITOR", editor) do
+ args = environment ? ["--environment", environment] : []
+ rails "credentials:edit", args, **options
+ end
+ end
+
+ def run_show_command(environment: nil, **options)
+ args = environment ? ["--environment", environment] : []
+ rails "credentials:show", args, **options
+ end
+end
diff --git a/railties/test/commands/dbconsole_test.rb b/railties/test/commands/dbconsole_test.rb
index 7950ed6aa7..5e3eca6585 100644
--- a/railties/test/commands/dbconsole_test.rb
+++ b/railties/test/commands/dbconsole_test.rb
@@ -1,30 +1,31 @@
-require 'abstract_unit'
-require 'minitest/mock'
-require 'rails/commands/dbconsole'
-
-class Rails::DBConsoleTest < ActiveSupport::TestCase
+# frozen_string_literal: true
+require "abstract_unit"
+require "minitest/mock"
+require "rails/command"
+require "rails/commands/dbconsole/dbconsole_command"
+class Rails::DBConsoleTest < ActiveSupport::TestCase
def setup
- Rails::DBConsole.const_set('APP_PATH', 'rails/all')
+ Rails::DBConsole.const_set("APP_PATH", "rails/all")
end
def teardown
- Rails::DBConsole.send(:remove_const, 'APP_PATH')
- %w[PGUSER PGHOST PGPORT PGPASSWORD DATABASE_URL].each{|key| ENV.delete(key)}
+ Rails::DBConsole.send(:remove_const, "APP_PATH")
+ %w[PGUSER PGHOST PGPORT PGPASSWORD DATABASE_URL].each { |key| ENV.delete(key) }
end
def test_config_with_db_config_only
config_sample = {
- "test"=> {
- "adapter"=> "sqlite3",
- "host"=> "localhost",
- "port"=> "9000",
- "database"=> "foo_test",
- "user"=> "foo",
- "password"=> "bar",
- "pool"=> "5",
- "timeout"=> "3000"
+ "test" => {
+ "adapter" => "sqlite3",
+ "host" => "localhost",
+ "port" => "9000",
+ "database" => "foo_test",
+ "user" => "foo",
+ "password" => "bar",
+ "pool" => "5",
+ "timeout" => "3000"
}
}
app_db_config(config_sample) do
@@ -41,7 +42,7 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase
end
def test_config_with_database_url_only
- ENV['DATABASE_URL'] = 'postgresql://foo:bar@localhost:9000/foo_test?pool=5&timeout=3000'
+ ENV["DATABASE_URL"] = "postgresql://foo:bar@localhost:9000/foo_test?pool=5&timeout=3000"
expected = {
"adapter" => "postgresql",
"host" => "localhost",
@@ -60,7 +61,7 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase
def test_config_choose_database_url_if_exists
host = "database-url-host.com"
- ENV['DATABASE_URL'] = "postgresql://foo:bar@#{host}:9000/foo_test?pool=5&timeout=3000"
+ ENV["DATABASE_URL"] = "postgresql://foo:bar@#{host}:9000/foo_test?pool=5&timeout=3000"
sample_config = {
"test" => {
"adapter" => "postgresql",
@@ -81,144 +82,197 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase
def test_env
assert_equal "test", Rails::DBConsole.new.environment
- ENV['RAILS_ENV'] = nil
- ENV['RACK_ENV'] = nil
+ ENV["RAILS_ENV"] = nil
+ ENV["RACK_ENV"] = nil
Rails.stub(:respond_to?, false) do
assert_equal "development", Rails::DBConsole.new.environment
- ENV['RACK_ENV'] = "rack_env"
+ ENV["RACK_ENV"] = "rack_env"
assert_equal "rack_env", Rails::DBConsole.new.environment
- ENV['RAILS_ENV'] = "rails_env"
+ ENV["RAILS_ENV"] = "rails_env"
assert_equal "rails_env", Rails::DBConsole.new.environment
end
ensure
- ENV['RAILS_ENV'] = "test"
- ENV['RACK_ENV'] = nil
+ ENV["RAILS_ENV"] = "test"
+ ENV["RACK_ENV"] = nil
end
def test_rails_env_is_development_when_argument_is_dev
- Rails::DBConsole.stub(:available_environments, ['development', 'test']) do
- options = Rails::DBConsole.send(:parse_arguments, ['dev'])
- assert_match('development', options[:environment])
+ assert_deprecated do
+ stub_available_environments([ "development", "test" ]) do
+ assert_match("development", parse_arguments([ "dev" ])[:environment])
+ end
+ end
+ end
+
+ def test_rails_env_is_development_when_environment_option_is_dev
+ stub_available_environments([ "development", "test" ]) do
+ assert_match("development", parse_arguments([ "-e", "dev" ])[:environment])
end
end
def test_rails_env_is_dev_when_argument_is_dev_and_dev_env_is_present
- Rails::DBConsole.stub(:available_environments, ['dev']) do
- options = Rails::DBConsole.send(:parse_arguments, ['dev'])
- assert_match('dev', options[:environment])
+ assert_deprecated do
+ stub_available_environments([ "dev" ]) do
+ assert_match("dev", parse_arguments([ "dev" ])[:environment])
+ end
end
end
def test_mysql
- start(adapter: 'mysql', database: 'db')
- assert !aborted
- assert_equal [%w[mysql mysql5], 'db'], dbconsole.find_cmd_and_exec_args
+ start(adapter: "mysql2", database: "db")
+ assert_not aborted
+ assert_equal [%w[mysql mysql5], "db"], dbconsole.find_cmd_and_exec_args
end
def test_mysql_full
- start(adapter: 'mysql', database: 'db', host: 'locahost', port: 1234, socket: 'socket', username: 'user', password: 'qwerty', encoding: 'UTF-8')
- assert !aborted
- assert_equal [%w[mysql mysql5], '--host=locahost', '--port=1234', '--socket=socket', '--user=user', '--default-character-set=UTF-8', '-p', 'db'], dbconsole.find_cmd_and_exec_args
+ start(adapter: "mysql2", database: "db", host: "locahost", port: 1234, socket: "socket", username: "user", password: "qwerty", encoding: "UTF-8")
+ assert_not aborted
+ assert_equal [%w[mysql mysql5], "--host=locahost", "--port=1234", "--socket=socket", "--user=user", "--default-character-set=UTF-8", "-p", "db"], dbconsole.find_cmd_and_exec_args
end
def test_mysql_include_password
- start({adapter: 'mysql', database: 'db', username: 'user', password: 'qwerty'}, ['-p'])
- assert !aborted
- assert_equal [%w[mysql mysql5], '--user=user', '--password=qwerty', 'db'], dbconsole.find_cmd_and_exec_args
+ start({ adapter: "mysql2", database: "db", username: "user", password: "qwerty" }, ["-p"])
+ assert_not aborted
+ assert_equal [%w[mysql mysql5], "--user=user", "--password=qwerty", "db"], dbconsole.find_cmd_and_exec_args
end
def test_postgresql
- start(adapter: 'postgresql', database: 'db')
- assert !aborted
- assert_equal ['psql', 'db'], dbconsole.find_cmd_and_exec_args
+ start(adapter: "postgresql", database: "db")
+ assert_not aborted
+ assert_equal ["psql", "db"], dbconsole.find_cmd_and_exec_args
end
def test_postgresql_full
- start(adapter: 'postgresql', database: 'db', username: 'user', password: 'q1w2e3', host: 'host', port: 5432)
- assert !aborted
- assert_equal ['psql', 'db'], dbconsole.find_cmd_and_exec_args
- assert_equal 'user', ENV['PGUSER']
- assert_equal 'host', ENV['PGHOST']
- assert_equal '5432', ENV['PGPORT']
- assert_not_equal 'q1w2e3', ENV['PGPASSWORD']
+ start(adapter: "postgresql", database: "db", username: "user", password: "q1w2e3", host: "host", port: 5432)
+ assert_not aborted
+ assert_equal ["psql", "db"], dbconsole.find_cmd_and_exec_args
+ assert_equal "user", ENV["PGUSER"]
+ assert_equal "host", ENV["PGHOST"]
+ assert_equal "5432", ENV["PGPORT"]
+ assert_not_equal "q1w2e3", ENV["PGPASSWORD"]
end
def test_postgresql_include_password
- start({adapter: 'postgresql', database: 'db', username: 'user', password: 'q1w2e3'}, ['-p'])
- assert !aborted
- assert_equal ['psql', 'db'], dbconsole.find_cmd_and_exec_args
- assert_equal 'user', ENV['PGUSER']
- assert_equal 'q1w2e3', ENV['PGPASSWORD']
+ start({ adapter: "postgresql", database: "db", username: "user", password: "q1w2e3" }, ["-p"])
+ assert_not aborted
+ assert_equal ["psql", "db"], dbconsole.find_cmd_and_exec_args
+ assert_equal "user", ENV["PGUSER"]
+ assert_equal "q1w2e3", ENV["PGPASSWORD"]
end
def test_sqlite3
- start(adapter: 'sqlite3', database: 'db.sqlite3')
- assert !aborted
- assert_equal ['sqlite3', Rails.root.join('db.sqlite3').to_s], dbconsole.find_cmd_and_exec_args
+ start(adapter: "sqlite3", database: "db.sqlite3")
+ assert_not aborted
+ assert_equal ["sqlite3", Rails.root.join("db.sqlite3").to_s], dbconsole.find_cmd_and_exec_args
end
def test_sqlite3_mode
- start({adapter: 'sqlite3', database: 'db.sqlite3'}, ['--mode', 'html'])
- assert !aborted
- assert_equal ['sqlite3', '-html', Rails.root.join('db.sqlite3').to_s], dbconsole.find_cmd_and_exec_args
+ start({ adapter: "sqlite3", database: "db.sqlite3" }, ["--mode", "html"])
+ assert_not aborted
+ assert_equal ["sqlite3", "-html", Rails.root.join("db.sqlite3").to_s], dbconsole.find_cmd_and_exec_args
end
def test_sqlite3_header
- start({adapter: 'sqlite3', database: 'db.sqlite3'}, ['--header'])
- assert_equal ['sqlite3', '-header', Rails.root.join('db.sqlite3').to_s], dbconsole.find_cmd_and_exec_args
+ start({ adapter: "sqlite3", database: "db.sqlite3" }, ["--header"])
+ assert_equal ["sqlite3", "-header", Rails.root.join("db.sqlite3").to_s], dbconsole.find_cmd_and_exec_args
end
def test_sqlite3_db_absolute_path
- start(adapter: 'sqlite3', database: '/tmp/db.sqlite3')
- assert !aborted
- assert_equal ['sqlite3', '/tmp/db.sqlite3'], dbconsole.find_cmd_and_exec_args
+ start(adapter: "sqlite3", database: "/tmp/db.sqlite3")
+ assert_not aborted
+ assert_equal ["sqlite3", "/tmp/db.sqlite3"], dbconsole.find_cmd_and_exec_args
end
def test_sqlite3_db_without_defined_rails_root
Rails.stub(:respond_to?, false) do
- start(adapter: 'sqlite3', database: 'config/db.sqlite3')
- assert !aborted
- assert_equal ['sqlite3', Rails.root.join('../config/db.sqlite3').to_s], dbconsole.find_cmd_and_exec_args
+ start(adapter: "sqlite3", database: "config/db.sqlite3")
+ assert_not aborted
+ assert_equal ["sqlite3", Rails.root.join("../config/db.sqlite3").to_s], dbconsole.find_cmd_and_exec_args
end
end
def test_oracle
- start(adapter: 'oracle', database: 'db', username: 'user', password: 'secret')
- assert !aborted
- assert_equal ['sqlplus', 'user@db'], dbconsole.find_cmd_and_exec_args
+ start(adapter: "oracle", database: "db", username: "user", password: "secret")
+ assert_not aborted
+ assert_equal ["sqlplus", "user@db"], dbconsole.find_cmd_and_exec_args
end
def test_oracle_include_password
- start({adapter: 'oracle', database: 'db', username: 'user', password: 'secret'}, ['-p'])
- assert !aborted
- assert_equal ['sqlplus', 'user/secret@db'], dbconsole.find_cmd_and_exec_args
+ start({ adapter: "oracle", database: "db", username: "user", password: "secret" }, ["-p"])
+ assert_not aborted
+ assert_equal ["sqlplus", "user/secret@db"], dbconsole.find_cmd_and_exec_args
+ end
+
+ def test_sqlserver
+ start(adapter: "sqlserver", database: "db", username: "user", password: "secret", host: "localhost", port: 1433)
+ assert_not aborted
+ assert_equal ["sqsh", "-D", "db", "-U", "user", "-P", "secret", "-S", "localhost:1433"], dbconsole.find_cmd_and_exec_args
end
def test_unknown_command_line_client
- start(adapter: 'unknown', database: 'db')
+ start(adapter: "unknown", database: "db")
assert aborted
assert_match(/Unknown command-line client for db/, output)
end
+ def test_primary_is_automatically_picked_with_3_level_configuration
+ sample_config = {
+ "test" => {
+ "primary" => {
+ "adapter" => "postgresql"
+ }
+ }
+ }
+
+ app_db_config(sample_config) do
+ assert_equal "postgresql", Rails::DBConsole.new.config["adapter"]
+ end
+ end
+
+ def test_specifying_a_custom_connection_and_environment
+ stub_available_environments(["development"]) do
+ dbconsole = parse_arguments(["-c", "custom", "-e", "development"])
+
+ assert_equal "development", dbconsole[:environment]
+ assert_equal "custom", dbconsole.connection
+ end
+ end
+
+ def test_specifying_a_missing_connection
+ app_db_config({}) do
+ e = assert_raises(ActiveRecord::AdapterNotSpecified) do
+ Rails::Command.invoke(:dbconsole, ["-c", "i_do_not_exist"])
+ end
+
+ assert_includes e.message, "'i_do_not_exist' connection is not configured."
+ end
+ end
+
+ def test_specifying_a_missing_environment
+ app_db_config({}) do
+ e = assert_raises(ActiveRecord::AdapterNotSpecified) do
+ Rails::Command.invoke(:dbconsole)
+ end
+
+ assert_includes e.message, "'test' database is not configured."
+ end
+ end
+
def test_print_help_short
stdout = capture(:stdout) do
- start({}, ['-h'])
+ Rails::Command.invoke(:dbconsole, ["-h"])
end
- assert aborted
- assert_equal '', output
- assert_match(/Usage:.*dbconsole/, stdout)
+ assert_match(/rails dbconsole \[environment\]/, stdout)
end
def test_print_help_long
stdout = capture(:stdout) do
- start({}, ['--help'])
+ Rails::Command.invoke(:dbconsole, ["--help"])
end
- assert aborted
- assert_equal '', output
- assert_match(/Usage:.*dbconsole/, stdout)
+ assert_match(/rails dbconsole \[environment\]/, stdout)
end
attr_reader :aborted, :output
@@ -226,39 +280,76 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase
private
- def app_db_config(results)
- Rails.application.config.stub(:database_configuration, results || {}) do
- yield
+ def app_db_config(results)
+ Rails.application.config.stub(:database_configuration, results || {}) do
+ yield
+ end
end
- end
- def dbconsole
- @dbconsole ||= Class.new(Rails::DBConsole) do
- attr_reader :find_cmd_and_exec_args
+ def make_dbconsole
+ Class.new(Rails::DBConsole) do
+ attr_reader :find_cmd_and_exec_args
- def find_cmd_and_exec(*args)
- @find_cmd_and_exec_args = args
+ def find_cmd_and_exec(*args)
+ @find_cmd_and_exec_args = args
+ end
end
- end.new(nil)
- end
+ end
+
+ attr_reader :dbconsole
- def start(config = {}, argv = [])
- dbconsole.stub(:config, config.stringify_keys) do
- dbconsole.stub(:arguments, argv) do
- capture_abort { dbconsole.start }
+ def start(config = {}, argv = [])
+ @dbconsole = make_dbconsole.new(parse_arguments(argv))
+ @dbconsole.stub(:config, config.stringify_keys) do
+ capture_abort { @dbconsole.start }
end
end
- end
- def capture_abort
- @aborted = false
- @output = capture(:stderr) do
- begin
- yield
- rescue SystemExit
- @aborted = true
+ def capture_abort
+ @aborted = false
+ @output = capture(:stderr) do
+ begin
+ yield
+ rescue SystemExit
+ @aborted = true
+ end
end
end
- end
+ def stub_available_environments(environments)
+ Rails::Command::DbconsoleCommand.class_eval do
+ alias_method :old_environments, :available_environments
+
+ define_method :available_environments do
+ environments
+ end
+ end
+
+ yield
+ ensure
+ Rails::Command::DbconsoleCommand.class_eval do
+ undef_method :available_environments
+ alias_method :available_environments, :old_environments
+ undef_method :old_environments
+ end
+ end
+
+ def parse_arguments(args)
+ Rails::Command::DbconsoleCommand.class_eval do
+ alias_method :old_perform, :perform
+ define_method(:perform) do
+ extract_environment_option_from_argument
+
+ options
+ end
+ end
+
+ Rails::Command.invoke(:dbconsole, args)
+ ensure
+ Rails::Command::DbconsoleCommand.class_eval do
+ undef_method :perform
+ alias_method :perform, :old_perform
+ undef_method :old_perform
+ end
+ end
end
diff --git a/railties/test/commands/dev_test.rb b/railties/test/commands/dev_test.rb
new file mode 100644
index 0000000000..ae8516fe9a
--- /dev/null
+++ b/railties/test/commands/dev_test.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "rails/command"
+
+class Rails::Command::DevTest < ActiveSupport::TestCase
+ setup :build_app
+ teardown :teardown_app
+
+ test "`rails dev:cache` creates both caching and restart file when restart file doesn't exist and dev caching is currently off" do
+ Dir.chdir(app_path) do
+ assert_not File.exist?("tmp/caching-dev.txt")
+ assert_not File.exist?("tmp/restart.txt")
+
+ assert_equal <<~OUTPUT, run_dev_cache_command
+ Development mode is now being cached.
+ OUTPUT
+
+ assert File.exist?("tmp/caching-dev.txt")
+ assert File.exist?("tmp/restart.txt")
+ end
+ end
+
+ test "`rails dev:cache` creates caching file and touches restart file when dev caching is currently off" do
+ Dir.chdir(app_path) do
+ app_file("tmp/restart.txt", "")
+
+ assert_not File.exist?("tmp/caching-dev.txt")
+ assert File.exist?("tmp/restart.txt")
+ restart_file_time_before = File.mtime("tmp/restart.txt")
+
+ assert_equal <<~OUTPUT, run_dev_cache_command
+ Development mode is now being cached.
+ OUTPUT
+
+ assert File.exist?("tmp/caching-dev.txt")
+ restart_file_time_after = File.mtime("tmp/restart.txt")
+ assert_operator restart_file_time_before, :<, restart_file_time_after
+ end
+ end
+
+ test "`rails dev:cache` removes caching file and touches restart file when dev caching is currently on" do
+ Dir.chdir(app_path) do
+ app_file("tmp/caching-dev.txt", "")
+ app_file("tmp/restart.txt", "")
+
+ assert File.exist?("tmp/caching-dev.txt")
+ assert File.exist?("tmp/restart.txt")
+ restart_file_time_before = File.mtime("tmp/restart.txt")
+
+ assert_equal <<~OUTPUT, run_dev_cache_command
+ Development mode is no longer being cached.
+ OUTPUT
+
+ assert_not File.exist?("tmp/caching-dev.txt")
+ restart_file_time_after = File.mtime("tmp/restart.txt")
+ assert_operator restart_file_time_before, :<, restart_file_time_after
+ end
+ end
+
+ private
+ def run_dev_cache_command
+ rails "dev:cache"
+ end
+end
diff --git a/railties/test/commands/encrypted_test.rb b/railties/test/commands/encrypted_test.rb
new file mode 100644
index 0000000000..8b608fe8c0
--- /dev/null
+++ b/railties/test/commands/encrypted_test.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "env_helpers"
+require "rails/command"
+require "rails/commands/encrypted/encrypted_command"
+
+class Rails::Command::EncryptedCommandTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation, EnvHelpers
+
+ setup :build_app
+ teardown :teardown_app
+
+ test "edit without editor gives hint" do
+ run_edit_command("config/tokens.yml.enc", editor: "").tap do |output|
+ assert_match "No $EDITOR to open file in", output
+ assert_match "rails encrypted:edit", output
+ end
+ end
+
+ test "edit encrypted file" do
+ # Run twice to ensure file can be reread after first edit pass.
+ 2.times do
+ assert_match(/access_key_id: 123/, run_edit_command("config/tokens.yml.enc"))
+ end
+ end
+
+ test "edit command does not add master key to gitignore when already exist" do
+ run_edit_command("config/tokens.yml.enc")
+
+ Dir.chdir(app_path) do
+ assert_match "/config/master.key", File.read(".gitignore")
+ end
+ end
+
+ test "edit command does not add master key when `RAILS_MASTER_KEY` env specified" do
+ Dir.chdir(app_path) do
+ key = IO.binread("config/master.key").strip
+ FileUtils.rm("config/master.key")
+
+ switch_env("RAILS_MASTER_KEY", key) do
+ run_edit_command("config/tokens.yml.enc")
+ assert_not File.exist?("config/master.key")
+ end
+ end
+ end
+
+ test "edit encrypts file with custom key" do
+ run_edit_command("config/tokens.yml.enc", key: "config/tokens.key")
+
+ Dir.chdir(app_path) do
+ assert File.exist?("config/tokens.yml.enc")
+ assert File.exist?("config/tokens.key")
+
+ assert_match "/config/tokens.key", File.read(".gitignore")
+ end
+
+ assert_match(/access_key_id: 123/, run_edit_command("config/tokens.yml.enc", key: "config/tokens.key"))
+ end
+
+ test "show encrypted file with custom key" do
+ run_edit_command("config/tokens.yml.enc", key: "config/tokens.key")
+
+ assert_match(/access_key_id: 123/, run_show_command("config/tokens.yml.enc", key: "config/tokens.key"))
+ end
+
+ test "show command raise error when require_master_key is specified and key does not exist" do
+ add_to_config "config.require_master_key = true"
+
+ assert_match(/Missing encryption key to decrypt file with/,
+ run_show_command("config/tokens.yml.enc", key: "unexist.key", allow_failure: true))
+ end
+
+ test "show command does not raise error when require_master_key is false and master key does not exist" do
+ remove_file "config/master.key"
+ add_to_config "config.require_master_key = false"
+
+ assert_match(/Missing 'config\/master\.key' to decrypt data/, run_show_command("config/tokens.yml.enc"))
+ end
+
+ test "won't corrupt encrypted file when passed wrong key" do
+ run_edit_command("config/tokens.yml.enc", key: "config/tokens.key")
+
+ assert_match "passed the wrong key",
+ run_edit_command("config/tokens.yml.enc", allow_failure: true)
+
+ assert_match(/access_key_id: 123/, run_show_command("config/tokens.yml.enc", key: "config/tokens.key"))
+ end
+
+ private
+ def run_edit_command(file, key: nil, editor: "cat", **options)
+ switch_env("EDITOR", editor) do
+ rails "encrypted:edit", prepare_args(file, key), **options
+ end
+ end
+
+ def run_show_command(file, key: nil, **options)
+ rails "encrypted:show", prepare_args(file, key), **options
+ end
+
+ def prepare_args(file, key)
+ args = [ file ]
+ args.push("--key", key) if key
+ args
+ end
+end
diff --git a/railties/test/commands/initializers_test.rb b/railties/test/commands/initializers_test.rb
new file mode 100644
index 0000000000..bdfbb3021c
--- /dev/null
+++ b/railties/test/commands/initializers_test.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "rails/command"
+
+class Rails::Command::InitializersTest < ActiveSupport::TestCase
+ setup :build_app
+ teardown :teardown_app
+
+ test "`rails initializers` prints out defined initializers invoked by Rails" do
+ initial_output = run_initializers_command
+ initial_output_length = initial_output.split("\n").length
+
+ assert_operator initial_output_length, :>, 0
+ assert_not initial_output.include?("set_added_test_module")
+
+ add_to_config <<-RUBY
+ initializer(:set_added_test_module) { }
+ RUBY
+
+ final_output = run_initializers_command
+ final_output_length = final_output.split("\n").length
+
+ assert_equal 1, (final_output_length - initial_output_length)
+ assert final_output.include?("set_added_test_module")
+ end
+
+ private
+ def run_initializers_command
+ rails "initializers"
+ end
+end
diff --git a/railties/test/commands/notes_test.rb b/railties/test/commands/notes_test.rb
new file mode 100644
index 0000000000..147019e299
--- /dev/null
+++ b/railties/test/commands/notes_test.rb
@@ -0,0 +1,128 @@
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "rails/command"
+require "rails/commands/notes/notes_command"
+
+class Rails::Command::NotesTest < ActiveSupport::TestCase
+ setup :build_app
+ teardown :teardown_app
+
+ test "`rails notes` displays results for default directories and default annotations with aligned line number and annotation tag" do
+ app_file "app/controllers/some_controller.rb", "# OPTIMIZE: note in app directory"
+ app_file "config/initializers/some_initializer.rb", "# TODO: note in config directory"
+ app_file "db/some_seeds.rb", "# FIXME: note in db directory"
+ app_file "lib/some_file.rb", "# TODO: note in lib directory"
+ app_file "test/some_test.rb", "\n" * 100 + "# FIXME: note in test directory"
+
+ app_file "some_other_dir/blah.rb", "# TODO: note in some_other directory"
+
+ assert_equal <<~OUTPUT, run_notes_command
+ app/controllers/some_controller.rb:
+ * [ 1] [OPTIMIZE] note in app directory
+
+ config/initializers/some_initializer.rb:
+ * [ 1] [TODO] note in config directory
+
+ db/some_seeds.rb:
+ * [ 1] [FIXME] note in db directory
+
+ lib/some_file.rb:
+ * [ 1] [TODO] note in lib directory
+
+ test/some_test.rb:
+ * [101] [FIXME] note in test directory
+
+ OUTPUT
+ end
+
+ test "`rails notes` displays an empty string when no results were found" do
+ assert_equal "", run_notes_command
+ end
+
+ test "`rails notes --annotations` displays results for a single annotation without being prefixed by a tag" do
+ app_file "db/some_seeds.rb", "# FIXME: note in db directory"
+ app_file "test/some_test.rb", "# FIXME: note in test directory"
+
+ app_file "app/controllers/some_controller.rb", "# OPTIMIZE: note in app directory"
+ app_file "config/initializers/some_initializer.rb", "# TODO: note in config directory"
+
+ assert_equal <<~OUTPUT, run_notes_command(["--annotations", "FIXME"])
+ db/some_seeds.rb:
+ * [1] note in db directory
+
+ test/some_test.rb:
+ * [1] note in test directory
+
+ OUTPUT
+ end
+
+ test "`rails notes --annotations` displays results for multiple annotations being prefixed by a tag" do
+ app_file "app/controllers/some_controller.rb", "# FOOBAR: note in app directory"
+ app_file "config/initializers/some_initializer.rb", "# TODO: note in config directory"
+ app_file "lib/some_file.rb", "# TODO: note in lib directory"
+
+ app_file "test/some_test.rb", "# FIXME: note in test directory"
+
+ assert_equal <<~OUTPUT, run_notes_command(["--annotations", "FOOBAR", "TODO"])
+ app/controllers/some_controller.rb:
+ * [1] [FOOBAR] note in app directory
+
+ config/initializers/some_initializer.rb:
+ * [1] [TODO] note in config directory
+
+ lib/some_file.rb:
+ * [1] [TODO] note in lib directory
+
+ OUTPUT
+ end
+
+ test "displays results from additional directories added to the default directories from a config file" do
+ app_file "db/some_seeds.rb", "# FIXME: note in db directory"
+ app_file "lib/some_file.rb", "# TODO: note in lib directory"
+ app_file "spec/spec_helper.rb", "# TODO: note in spec"
+ app_file "spec/models/user_spec.rb", "# TODO: note in model spec"
+
+ add_to_config "config.annotations.register_directories \"spec\""
+
+ assert_equal <<~OUTPUT, run_notes_command
+ db/some_seeds.rb:
+ * [1] [FIXME] note in db directory
+
+ lib/some_file.rb:
+ * [1] [TODO] note in lib directory
+
+ spec/models/user_spec.rb:
+ * [1] [TODO] note in model spec
+
+ spec/spec_helper.rb:
+ * [1] [TODO] note in spec
+
+ OUTPUT
+ end
+
+ test "displays results from additional file extensions added to the default extensions from a config file" do
+ add_to_config "config.assets.precompile = []"
+ add_to_config %q{ config.annotations.register_extensions("scss", "sass") { |annotation| /\/\/\s*(#{annotation}):?\s*(.*)$/ } }
+ app_file "db/some_seeds.rb", "# FIXME: note in db directory"
+ app_file "app/assets/stylesheets/application.css.scss", "// TODO: note in scss"
+ app_file "app/assets/stylesheets/application.css.sass", "// TODO: note in sass"
+
+ assert_equal <<~OUTPUT, run_notes_command
+ app/assets/stylesheets/application.css.sass:
+ * [1] [TODO] note in sass
+
+ app/assets/stylesheets/application.css.scss:
+ * [1] [TODO] note in scss
+
+ db/some_seeds.rb:
+ * [1] [FIXME] note in db directory
+
+ OUTPUT
+ end
+
+ private
+ def run_notes_command(args = [])
+ rails "notes", args
+ end
+end
diff --git a/railties/test/commands/routes_test.rb b/railties/test/commands/routes_test.rb
new file mode 100644
index 0000000000..693e532c5b
--- /dev/null
+++ b/railties/test/commands/routes_test.rb
@@ -0,0 +1,217 @@
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "rails/command"
+require "rails/commands/routes/routes_command"
+require "io/console/size"
+
+class Rails::Command::RoutesTest < ActiveSupport::TestCase
+ setup :build_app
+ teardown :teardown_app
+
+ test "singular resource output in rails routes" do
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ resource :post
+ resource :user_permission
+ end
+ RUBY
+
+ expected_post_output = [" Prefix Verb URI Pattern Controller#Action",
+ " new_post GET /post/new(.:format) posts#new",
+ "edit_post GET /post/edit(.:format) posts#edit",
+ " post GET /post(.:format) posts#show",
+ " PATCH /post(.:format) posts#update",
+ " PUT /post(.:format) posts#update",
+ " DELETE /post(.:format) posts#destroy",
+ " POST /post(.:format) posts#create\n"].join("\n")
+
+ output = run_routes_command(["-c", "PostController"])
+ assert_equal expected_post_output, output
+
+ expected_perm_output = [" Prefix Verb URI Pattern Controller#Action",
+ " new_user_permission GET /user_permission/new(.:format) user_permissions#new",
+ "edit_user_permission GET /user_permission/edit(.:format) user_permissions#edit",
+ " user_permission GET /user_permission(.:format) user_permissions#show",
+ " PATCH /user_permission(.:format) user_permissions#update",
+ " PUT /user_permission(.:format) user_permissions#update",
+ " DELETE /user_permission(.:format) user_permissions#destroy",
+ " POST /user_permission(.:format) user_permissions#create\n"].join("\n")
+
+ output = run_routes_command(["-c", "UserPermissionController"])
+ assert_equal expected_perm_output, output
+ end
+
+ test "rails routes with global search key" do
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ get '/cart', to: 'cart#show'
+ post '/cart', to: 'cart#create'
+ get '/basketballs', to: 'basketball#index'
+ end
+ RUBY
+
+ output = run_routes_command(["-g", "show"])
+ assert_equal <<~MESSAGE, output
+ Prefix Verb URI Pattern Controller#Action
+ cart GET /cart(.:format) cart#show
+ rails_service_blob GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs#show
+ rails_blob_representation GET /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations#show
+ rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show
+ MESSAGE
+
+ output = run_routes_command(["-g", "POST"])
+ assert_equal <<~MESSAGE, output
+ Prefix Verb URI Pattern Controller#Action
+ POST /cart(.:format) cart#create
+ rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create
+ MESSAGE
+
+ output = run_routes_command(["-g", "basketballs"])
+ assert_equal " Prefix Verb URI Pattern Controller#Action\n" \
+ "basketballs GET /basketballs(.:format) basketball#index\n", output
+ end
+
+ test "rails routes with controller search key" do
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ get '/cart', to: 'cart#show'
+ get '/basketball', to: 'basketball#index'
+ get '/user_permission', to: 'user_permission#index'
+ end
+ RUBY
+
+ expected_cart_output = "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n"
+ output = run_routes_command(["-c", "cart"])
+ assert_equal expected_cart_output, output
+
+ output = run_routes_command(["-c", "Cart"])
+ assert_equal expected_cart_output, output
+
+ output = run_routes_command(["-c", "CartController"])
+ assert_equal expected_cart_output, output
+
+ expected_perm_output = [" Prefix Verb URI Pattern Controller#Action",
+ "user_permission GET /user_permission(.:format) user_permission#index\n"].join("\n")
+ output = run_routes_command(["-c", "user_permission"])
+ assert_equal expected_perm_output, output
+
+ output = run_routes_command(["-c", "UserPermission"])
+ assert_equal expected_perm_output, output
+
+ output = run_routes_command(["-c", "UserPermissionController"])
+ assert_equal expected_perm_output, output
+ end
+
+ test "rails routes with namespaced controller search key" do
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ namespace :admin do
+ resource :post
+ resource :user_permission
+ end
+ end
+ RUBY
+
+ expected_post_output = [" Prefix Verb URI Pattern Controller#Action",
+ " new_admin_post GET /admin/post/new(.:format) admin/posts#new",
+ "edit_admin_post GET /admin/post/edit(.:format) admin/posts#edit",
+ " admin_post GET /admin/post(.:format) admin/posts#show",
+ " PATCH /admin/post(.:format) admin/posts#update",
+ " PUT /admin/post(.:format) admin/posts#update",
+ " DELETE /admin/post(.:format) admin/posts#destroy",
+ " POST /admin/post(.:format) admin/posts#create\n"].join("\n")
+
+ output = run_routes_command(["-c", "Admin::PostController"])
+ assert_equal expected_post_output, output
+
+ output = run_routes_command(["-c", "PostController"])
+ assert_equal expected_post_output, output
+
+ expected_perm_output = [" Prefix Verb URI Pattern Controller#Action",
+ " new_admin_user_permission GET /admin/user_permission/new(.:format) admin/user_permissions#new",
+ "edit_admin_user_permission GET /admin/user_permission/edit(.:format) admin/user_permissions#edit",
+ " admin_user_permission GET /admin/user_permission(.:format) admin/user_permissions#show",
+ " PATCH /admin/user_permission(.:format) admin/user_permissions#update",
+ " PUT /admin/user_permission(.:format) admin/user_permissions#update",
+ " DELETE /admin/user_permission(.:format) admin/user_permissions#destroy",
+ " POST /admin/user_permission(.:format) admin/user_permissions#create\n"].join("\n")
+
+ output = run_routes_command(["-c", "Admin::UserPermissionController"])
+ assert_equal expected_perm_output, output
+
+ output = run_routes_command(["-c", "UserPermissionController"])
+ assert_equal expected_perm_output, output
+ end
+
+ test "rails routes displays message when no routes are defined" do
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ end
+ RUBY
+
+ assert_equal <<~MESSAGE, run_routes_command
+ Prefix Verb URI Pattern Controller#Action
+ rails_service_blob GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs#show
+ rails_blob_representation GET /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations#show
+ rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show
+ update_rails_disk_service PUT /rails/active_storage/disk/:encoded_token(.:format) active_storage/disk#update
+ rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create
+ MESSAGE
+ end
+
+ test "rails routes with expanded option" do
+ begin
+ previous_console_winsize = IO.console.winsize
+ IO.console.winsize = [0, 27]
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ get '/cart', to: 'cart#show'
+ end
+ RUBY
+
+ output = run_routes_command(["--expanded"])
+
+ assert_equal <<~MESSAGE, output
+ --[ Route 1 ]--------------
+ Prefix | cart
+ Verb | GET
+ URI | /cart(.:format)
+ Controller#Action | cart#show
+ --[ Route 2 ]--------------
+ Prefix | rails_service_blob
+ Verb | GET
+ URI | /rails/active_storage/blobs/:signed_id/*filename(.:format)
+ Controller#Action | active_storage/blobs#show
+ --[ Route 3 ]--------------
+ Prefix | rails_blob_representation
+ Verb | GET
+ URI | /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format)
+ Controller#Action | active_storage/representations#show
+ --[ Route 4 ]--------------
+ Prefix | rails_disk_service
+ Verb | GET
+ URI | /rails/active_storage/disk/:encoded_key/*filename(.:format)
+ Controller#Action | active_storage/disk#show
+ --[ Route 5 ]--------------
+ Prefix | update_rails_disk_service
+ Verb | PUT
+ URI | /rails/active_storage/disk/:encoded_token(.:format)
+ Controller#Action | active_storage/disk#update
+ --[ Route 6 ]--------------
+ Prefix | rails_direct_uploads
+ Verb | POST
+ URI | /rails/active_storage/direct_uploads(.:format)
+ Controller#Action | active_storage/direct_uploads#create
+ MESSAGE
+ ensure
+ IO.console.winsize = previous_console_winsize
+ end
+ end
+
+ private
+ def run_routes_command(args = [])
+ rails "routes", args
+ end
+end
diff --git a/railties/test/commands/secrets_test.rb b/railties/test/commands/secrets_test.rb
new file mode 100644
index 0000000000..6b9f284a0c
--- /dev/null
+++ b/railties/test/commands/secrets_test.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "env_helpers"
+require "rails/command"
+require "rails/commands/secrets/secrets_command"
+
+class Rails::Command::SecretsCommandTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation, EnvHelpers
+
+ setup :build_app
+ teardown :teardown_app
+
+ test "edit without editor gives hint" do
+ assert_match "No $EDITOR to open decrypted secrets in", run_edit_command(editor: "")
+ end
+
+ test "encrypted secrets are deprecated when using credentials" do
+ assert_match "Encrypted secrets is deprecated", run_setup_command
+ assert_equal 1, $?.exitstatus
+ assert_not File.exist?("config/secrets.yml.enc")
+ end
+
+ test "encrypted secrets are deprecated when running edit without setup" do
+ assert_match "Encrypted secrets is deprecated", run_setup_command
+ assert_equal 1, $?.exitstatus
+ assert_not File.exist?("config/secrets.yml.enc")
+ end
+
+ test "encrypted secrets are deprecated for 5.1 config/secrets.yml apps" do
+ Dir.chdir(app_path) do
+ FileUtils.rm("config/credentials.yml.enc")
+ FileUtils.touch("config/secrets.yml")
+
+ assert_match "Encrypted secrets is deprecated", run_setup_command
+ assert_equal 1, $?.exitstatus
+ assert_not File.exist?("config/secrets.yml.enc")
+ end
+ end
+
+ test "edit secrets" do
+ prevent_deprecation
+
+ # 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
+
+ test "show secrets" do
+ prevent_deprecation
+
+ assert_match(/external_api_key: 1466aac22e6a869134be3d09b9e89232fc2c2289/, run_show_command)
+ end
+
+ private
+ def prevent_deprecation
+ Dir.chdir(app_path) do
+ File.write("config/secrets.yml.key", "f731758c639da2604dfb6bf3d1025de8")
+ File.write("config/secrets.yml.enc", "sEB0mHxDbeP1/KdnMk00wyzPFACl9K6t0cZWn5/Mfx/YbTHvnI07vrneqHg9kaH3wOS7L6pIQteu1P077OtE4BSx/ZRc/sgQPHyWu/tXsrfHqnPNpayOF/XZqizE91JacSFItNMWpuPsp9ynbzz+7cGhoB1S4aPNIU6u0doMrzdngDbijsaAFJmsHIQh6t/QHoJx--8aMoE0PvUWmw1Iqz--ldFqnM/K0g9k17M8PKoN/Q==")
+ end
+ end
+
+ def run_edit_command(editor: "cat")
+ switch_env("EDITOR", editor) do
+ rails "secrets:edit", allow_failure: true
+ end
+ end
+
+ def run_show_command
+ rails "secrets:show", allow_failure: true
+ end
+
+ def run_setup_command
+ rails "secrets:setup", allow_failure: true
+ end
+end
diff --git a/railties/test/commands/server_test.rb b/railties/test/commands/server_test.rb
index ba688f1e9e..e5b1da6ea4 100644
--- a/railties/test/commands/server_test.rb
+++ b/railties/test/commands/server_test.rb
@@ -1,88 +1,284 @@
-require 'abstract_unit'
-require 'env_helpers'
-require 'rails/commands/server'
+# frozen_string_literal: true
-class Rails::ServerTest < ActiveSupport::TestCase
+require "isolation/abstract_unit"
+require "env_helpers"
+require "rails/command"
+require "rails/commands/server/server_command"
+
+class Rails::Command::ServerCommandTest < ActiveSupport::TestCase
include EnvHelpers
def test_environment_with_server_option
- args = ["thin", "-e", "production"]
- options = Rails::Server::Options.new.parse!(args)
- assert_equal 'production', options[:environment]
- assert_equal 'thin', options[:server]
+ args = ["-u", "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)
- assert_equal 'production', options[:environment]
+ args = ["-e", "production"]
+ options = parse_arguments(args)
+ assert_equal "production", options[:environment]
assert_nil options[:server]
end
+ def test_explicit_using_option
+ args = ["-u", "thin"]
+ options = parse_arguments(args)
+ assert_equal "thin", options[:server]
+ end
+
+ def test_using_server_mistype
+ assert_match(/Could not find server "tin". Maybe you meant "thin"?/, run_command("--using", "tin"))
+ end
+
+ def test_using_positional_argument_deprecation
+ assert_match(/DEPRECATION WARNING/, run_command("tin"))
+ end
+
+ def test_using_known_server_that_isnt_in_the_gemfile
+ assert_match(/Could not load server "unicorn". Maybe you need to the add it to the Gemfile/, run_command("-u", "unicorn"))
+ end
+
+ def test_daemon_with_option
+ args = ["-d"]
+ options = parse_arguments(args)
+ assert_equal true, options[:daemonize]
+ end
+
+ def test_daemon_without_option
+ args = []
+ options = parse_arguments(args)
+ assert_equal false, options[:daemonize]
+ end
+
def test_server_option_without_environment
- args = ["thin"]
- options = Rails::Server::Options.new.parse!(args)
- assert_nil options[:environment]
- assert_equal 'thin', options[:server]
+ args = ["-u", "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]
+ with_rails_env "production" do
+ options = parse_arguments
+ assert_equal "production", options[:environment]
end
end
end
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]
+ with_rack_env "production" do
+ options = parse_arguments
+ assert_equal "production", options[:environment]
end
end
end
+ def test_environment_with_port
+ switch_env "PORT", "1234" do
+ options = parse_arguments
+ assert_equal 1234, options[:Port]
+ end
+ end
+
+ def test_environment_with_host
+ switch_env "HOST", "1.2.3.4" do
+ assert_deprecated do
+ options = parse_arguments
+ assert_equal "1.2.3.4", options[:Host]
+ end
+ end
+ end
+
+ def test_environment_with_binding
+ switch_env "BINDING", "1.2.3.4" do
+ options = parse_arguments
+ assert_equal "1.2.3.4", options[:Host]
+ end
+ end
+
+ def test_caching_without_option
+ args = []
+ options = parse_arguments(args)
+ assert_nil options[:caching]
+ end
+
+ def test_caching_with_option
+ args = ["--dev-caching"]
+ options = parse_arguments(args)
+ assert_equal true, options[:caching]
+
+ args = ["--no-dev-caching"]
+ options = parse_arguments(args)
+ assert_equal false, options[:caching]
+ end
+
+ def test_early_hints_with_option
+ args = ["--early-hints"]
+ options = parse_arguments(args)
+ assert_equal true, options[:early_hints]
+ end
+
+ def test_early_hints_is_nil_by_default
+ args = []
+ options = parse_arguments(args)
+ assert_nil options[:early_hints]
+ end
+
def test_log_stdout
with_rack_env nil do
with_rails_env nil do
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", "development", "-d"]
+ options = parse_arguments(args)
+ assert_equal false, 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 = ["-e", "development", "--no-log-to-stdout"]
+ options = parse_arguments(args)
+ assert_equal false, options[:log_stdout]
+
+ args = ["-e", "production", "--log-to-stdout"]
+ options = parse_arguments(args)
+ assert_equal true, 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
+ 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
+ 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
+ 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 "PORT", "1234" do
+ args = ["-p", "5678"]
+ options = parse_arguments(args)
+ assert_equal 5678, options[:Port]
+ end
+
+ switch_env "PORT", "1234" do
+ args = ["-p", "3000"]
+ options = parse_arguments(args)
+ assert_equal 3000, options[:Port]
+ end
+
+ switch_env "BINDING", "1.2.3.4" do
+ args = ["-b", "127.0.0.1"]
+ options = parse_arguments(args)
+ assert_equal "127.0.0.1", options[:Host]
+ 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]
+
+ server_options = parse_arguments(["-p3001", "-C", "--binding", "127.0.0.1"])
+ assert_equal [:Port, :Host, :caching], server_options[:user_supplied_options]
+
+ server_options = parse_arguments(["--port=3001"])
+ assert_equal [:Port], server_options[:user_supplied_options]
+
+ switch_env "BINDING", "1.2.3.4" do
+ server_options = parse_arguments
+ assert_equal [:Host], server_options[:user_supplied_options]
+ end
+ end
+
+ def test_default_options
+ server = Rails::Server.new
+ old_default_options = server.default_options
+
+ Dir.chdir("..") do
+ assert_equal old_default_options, server.default_options
+ end
+ end
+
+ def test_restart_command_contains_customized_options
+ original_args = ARGV.dup
+ args = %w(-p 4567 -b 127.0.0.1 -c dummy_config.ru -d -e test -P tmp/server.pid -C)
+ ARGV.replace args
+
+ expected = "bin/rails server -p 4567 -b 127.0.0.1 -c dummy_config.ru -d -e test -P tmp/server.pid -C --restart"
+
+ assert_equal expected, parse_arguments(args)[:restart_cmd]
+ ensure
+ ARGV.replace original_args
+ end
+
+ def test_served_url
+ args = %w(-u webrick -b 127.0.0.1 -p 4567)
+ server = Rails::Server.new(parse_arguments(args))
+ assert_equal "http://127.0.0.1:4567", server.served_url
+ end
+
+ private
+ def run_command(*args)
+ build_app
+ rails "server", *args
+ ensure
+ teardown_app
+ end
+
+ def parse_arguments(args = [])
+ Rails::Command::ServerCommand.new([], args).server_options
+ end
end
diff --git a/railties/test/configuration/middleware_stack_proxy_test.rb b/railties/test/configuration/middleware_stack_proxy_test.rb
index d5072614cf..bc72b7f0c9 100644
--- a/railties/test/configuration/middleware_stack_proxy_test.rb
+++ b/railties/test/configuration/middleware_stack_proxy_test.rb
@@ -1,8 +1,10 @@
-require 'active_support'
-require 'active_support/testing/autorun'
-require 'rails/configuration'
-require 'active_support/test_case'
-require 'minitest/mock'
+# frozen_string_literal: true
+
+require "active_support"
+require "active_support/testing/autorun"
+require "rails/configuration"
+require "active_support/test_case"
+require "minitest/mock"
module Rails
module Configuration
@@ -50,12 +52,12 @@ module Rails
private
- def assert_playback(msg_name, args)
- mock = Minitest::Mock.new
- mock.expect :send, nil, [msg_name, args]
- @stack.merge_into(mock)
- mock.verify
- end
+ def assert_playback(msg_name, args)
+ mock = Minitest::Mock.new
+ mock.expect :send, nil, [msg_name, args]
+ @stack.merge_into(mock)
+ mock.verify
+ end
end
end
end
diff --git a/railties/test/console_helpers.rb b/railties/test/console_helpers.rb
new file mode 100644
index 0000000000..67f55fdc45
--- /dev/null
+++ b/railties/test/console_helpers.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+begin
+ require "pty"
+rescue LoadError
+end
+
+module ConsoleHelpers
+ 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 available_pty?
+ defined?(PTY) && PTY.respond_to?(:open)
+ end
+end
diff --git a/railties/test/credentials_test.rb b/railties/test/credentials_test.rb
new file mode 100644
index 0000000000..03370e0fc7
--- /dev/null
+++ b/railties/test/credentials_test.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+
+class Rails::CredentialsTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation
+
+ setup :build_app
+ teardown :teardown_app
+
+ test "reads credentials from environment specific path" do
+ with_credentials do |content, key|
+ Dir.chdir(app_path) do
+ Dir.mkdir("config/credentials")
+ File.write("config/credentials/production.yml.enc", content)
+ File.write("config/credentials/production.key", key)
+ end
+
+ app("production")
+
+ assert_equal "revealed", Rails.application.credentials.mystery
+ end
+ end
+
+ test "reads credentials from customized path and key" do
+ with_credentials do |content, key|
+ Dir.chdir(app_path) do
+ Dir.mkdir("config/credentials")
+ File.write("config/credentials/staging.yml.enc", content)
+ File.write("config/credentials/staging.key", key)
+ end
+
+ add_to_env_config("production", "config.credentials.content_path = config.root.join('config/credentials/staging.yml.enc')")
+ add_to_env_config("production", "config.credentials.key_path = config.root.join('config/credentials/staging.key')")
+ app("production")
+
+ assert_equal "revealed", Rails.application.credentials.mystery
+ end
+ end
+
+ private
+ def with_credentials
+ key = "2117e775dc2024d4f49ddf3aeb585919"
+ # secret_key_base: secret
+ # mystery: revealed
+ content = "vgvKu4MBepIgZ5VHQMMPwnQNsLlWD9LKmJHu3UA/8yj6x+3fNhz3DwL9brX7UA==--qLdxHP6e34xeTAiI--nrcAsleXuo9NqiEuhntAhw=="
+ yield(content, key)
+ end
+end
diff --git a/railties/test/engine/commands_test.rb b/railties/test/engine/commands_test.rb
new file mode 100644
index 0000000000..0e5167578d
--- /dev/null
+++ b/railties/test/engine/commands_test.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "console_helpers"
+
+class Rails::Engine::CommandsTest < ActiveSupport::TestCase
+ include ConsoleHelpers
+
+ 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({ "SKIP_REQUIRE_WEBPACKER" => "true" }, "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?
+
+ primary, replica = PTY.open
+ spawn_command("console", replica)
+ assert_output(">", primary)
+ ensure
+ primary.puts "quit"
+ end
+
+ def test_dbconsole_command_work_inside_engine
+ skip "PTY unavailable" unless available_pty?
+
+ primary, replica = PTY.open
+ spawn_command("dbconsole", replica)
+ assert_output("sqlite>", primary)
+ ensure
+ primary.puts ".exit"
+ end
+
+ def test_server_command_work_inside_engine
+ skip "PTY unavailable" unless available_pty?
+
+ primary, replica = PTY.open
+ pid = spawn_command("server", replica)
+ assert_output("Listening on", primary)
+ ensure
+ kill(pid)
+ end
+
+ private
+ def plugin_path
+ "#{@destination_root}/bukkits"
+ end
+
+ def spawn_command(command, fd)
+ Process.spawn(
+ { "SKIP_REQUIRE_WEBPACKER" => "true" },
+ "#{plugin_path}/bin/rails #{command}",
+ in: fd, out: fd, err: fd
+ )
+ end
+
+ def kill(pid)
+ Process.kill("TERM", pid)
+ Process.wait(pid)
+ rescue Errno::ESRCH
+ end
+end
diff --git a/railties/test/engine/test_test.rb b/railties/test/engine/test_test.rb
new file mode 100644
index 0000000000..18af85a0aa
--- /dev/null
+++ b/railties/test/engine/test_test.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class Rails::Engine::TestTest < ActiveSupport::TestCase
+ setup do
+ @destination_root = Dir.mktmpdir("bukkits")
+ Dir.chdir(@destination_root) { `bundle exec rails plugin new bukkits --mountable` }
+ end
+
+ teardown do
+ FileUtils.rm_rf(@destination_root)
+ end
+
+ test "automatically synchronize test schema" do
+ Dir.chdir(plugin_path) do
+ # In order to confirm that migration files are loaded, generate multiple migration files.
+ `bin/rails generate model user name:string;
+ bin/rails generate model todo name:string;
+ RAILS_ENV=development bin/rails db:migrate`
+
+ output = `bin/rails test test/models/bukkits/user_test.rb`
+ assert_includes(output, "0 runs, 0 assertions, 0 failures, 0 errors, 0 skips")
+ end
+ end
+
+ private
+ def plugin_path
+ "#{@destination_root}/bukkits"
+ end
+end
diff --git a/railties/test/engine_test.rb b/railties/test/engine_test.rb
index f46fb748f5..19379e200c 100644
--- a/railties/test/engine_test.rb
+++ b/railties/test/engine_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class EngineTest < ActiveSupport::TestCase
test "reports routes as available only if they're actually present" do
@@ -9,7 +11,7 @@ class EngineTest < ActiveSupport::TestCase
end
end
- assert !engine.routes?
+ assert_not_predicate engine, :routes?
end
def test_application_can_be_subclassed
diff --git a/railties/test/env_helpers.rb b/railties/test/env_helpers.rb
index 330fe150ca..336832b867 100644
--- a/railties/test/env_helpers.rb
+++ b/railties/test/env_helpers.rb
@@ -1,30 +1,32 @@
-require 'rails'
+# frozen_string_literal: true
+
+require "rails"
module EnvHelpers
private
- def with_rails_env(env)
- Rails.instance_variable_set :@_env, nil
- switch_env 'RAILS_ENV', env do
- switch_env 'RACK_ENV', nil do
- yield
+ def with_rails_env(env)
+ Rails.instance_variable_set :@_env, nil
+ switch_env "RAILS_ENV", env do
+ switch_env "RACK_ENV", nil do
+ yield
+ end
end
end
- end
- def with_rack_env(env)
- Rails.instance_variable_set :@_env, nil
- switch_env 'RACK_ENV', env do
- switch_env 'RAILS_ENV', nil do
- yield
+ def with_rack_env(env)
+ Rails.instance_variable_set :@_env, nil
+ switch_env "RACK_ENV", env do
+ switch_env "RAILS_ENV", nil do
+ yield
+ end
end
end
- end
- def switch_env(key, value)
- old, ENV[key] = ENV[key], value
- yield
- ensure
- ENV[key] = old
- end
+ def switch_env(key, value)
+ old, ENV[key] = ENV[key], value
+ yield
+ ensure
+ ENV[key] = old
+ end
end
diff --git a/railties/test/fixtures/about_yml_plugins/bad_about_yml/about.yml b/railties/test/fixtures/about_yml_plugins/bad_about_yml/about.yml
deleted file mode 100644
index fe80872a16..0000000000
--- a/railties/test/fixtures/about_yml_plugins/bad_about_yml/about.yml
+++ /dev/null
@@ -1 +0,0 @@
-# an empty YAML file - any content in here seems to get parsed as a string \ No newline at end of file
diff --git a/railties/test/fixtures/about_yml_plugins/bad_about_yml/init.rb b/railties/test/fixtures/about_yml_plugins/bad_about_yml/init.rb
deleted file mode 100644
index d4262f8971..0000000000
--- a/railties/test/fixtures/about_yml_plugins/bad_about_yml/init.rb
+++ /dev/null
@@ -1 +0,0 @@
-# intentionally empty \ No newline at end of file
diff --git a/railties/test/fixtures/about_yml_plugins/plugin_without_about_yml/init.rb b/railties/test/fixtures/about_yml_plugins/plugin_without_about_yml/init.rb
deleted file mode 100644
index d4262f8971..0000000000
--- a/railties/test/fixtures/about_yml_plugins/plugin_without_about_yml/init.rb
+++ /dev/null
@@ -1 +0,0 @@
-# intentionally empty \ No newline at end of file
diff --git a/railties/test/fixtures/lib/create_test_dummy_template.rb b/railties/test/fixtures/lib/create_test_dummy_template.rb
index e4378bbd1a..b9eb6a912d 100644
--- a/railties/test/fixtures/lib/create_test_dummy_template.rb
+++ b/railties/test/fixtures/lib/create_test_dummy_template.rb
@@ -1 +1,3 @@
+# frozen_string_literal: true
+
create_dummy_app("spec/dummy")
diff --git a/railties/test/fixtures/lib/generators/active_record/fixjour_generator.rb b/railties/test/fixtures/lib/generators/active_record/fixjour_generator.rb
index a7d079a1bc..f196971f20 100644
--- a/railties/test/fixtures/lib/generators/active_record/fixjour_generator.rb
+++ b/railties/test/fixtures/lib/generators/active_record/fixjour_generator.rb
@@ -1,4 +1,6 @@
-require 'rails/generators/active_record'
+# frozen_string_literal: true
+
+require "rails/generators/active_record"
module ActiveRecord
module Generators
diff --git a/railties/test/fixtures/lib/generators/fixjour_generator.rb b/railties/test/fixtures/lib/generators/fixjour_generator.rb
index ef3e9edbed..22197835a8 100644
--- a/railties/test/fixtures/lib/generators/fixjour_generator.rb
+++ b/railties/test/fixtures/lib/generators/fixjour_generator.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
class FixjourGenerator < Rails::Generators::NamedBase
end
diff --git a/railties/test/fixtures/lib/generators/model_generator.rb b/railties/test/fixtures/lib/generators/model_generator.rb
index 9098a8a354..3009472c3d 100644
--- a/railties/test/fixtures/lib/generators/model_generator.rb
+++ b/railties/test/fixtures/lib/generators/model_generator.rb
@@ -1 +1,3 @@
-raise "I should never be loaded" \ No newline at end of file
+# frozen_string_literal: true
+
+raise "I should never be loaded"
diff --git a/railties/test/fixtures/lib/generators/usage_template/usage_template_generator.rb b/railties/test/fixtures/lib/generators/usage_template/usage_template_generator.rb
index 078b0f9412..5a847a8bd2 100644
--- a/railties/test/fixtures/lib/generators/usage_template/usage_template_generator.rb
+++ b/railties/test/fixtures/lib/generators/usage_template/usage_template_generator.rb
@@ -1,5 +1,7 @@
-require 'rails/generators'
+# frozen_string_literal: true
+
+require "rails/generators"
class UsageTemplateGenerator < Rails::Generators::Base
- source_root File.expand_path("templates", File.dirname(__FILE__))
+ source_root File.expand_path("templates", __dir__)
end
diff --git a/railties/test/fixtures/lib/rails/generators/foobar/foobar_generator.rb b/railties/test/fixtures/lib/rails/generators/foobar/foobar_generator.rb
index d1de8c56fa..159843866c 100644
--- a/railties/test/fixtures/lib/rails/generators/foobar/foobar_generator.rb
+++ b/railties/test/fixtures/lib/rails/generators/foobar/foobar_generator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Foobar
class FoobarGenerator < Rails::Generators::Base
end
diff --git a/railties/test/fixtures/lib/template.rb b/railties/test/fixtures/lib/template.rb
index c14a1a8784..44083c25e8 100644
--- a/railties/test/fixtures/lib/template.rb
+++ b/railties/test/fixtures/lib/template.rb
@@ -1 +1,3 @@
+# frozen_string_literal: true
+
say "It works from file!"
diff --git a/railties/test/generators/actions_test.rb b/railties/test/generators/actions_test.rb
index 2857dae07e..a2b35124c5 100644
--- a/railties/test/generators/actions_test.rb
+++ b/railties/test/generators/actions_test.rb
@@ -1,7 +1,8 @@
-require 'generators/generators_test_helper'
-require 'rails/generators/rails/app/app_generator'
-require 'env_helpers'
-require 'minitest/mock'
+# frozen_string_literal: true
+
+require "generators/generators_test_helper"
+require "rails/generators/rails/app/app_generator"
+require "env_helpers"
class ActionsTest < Rails::Generators::TestCase
include GeneratorsTestHelper
@@ -12,240 +13,422 @@ class ActionsTest < Rails::Generators::TestCase
def setup
Rails.application = TestApp::Application
- @mock_generator = Minitest::Mock.new
super
end
def teardown
Rails.application = TestApp::Application.instance
- @mock_generator.verify
end
def test_invoke_other_generator_with_shortcut
- action :invoke, 'model', ['my_model']
- assert_file 'app/models/my_model.rb', /MyModel/
+ action :invoke, "model", ["my_model"]
+ assert_file "app/models/my_model.rb", /MyModel/
end
def test_invoke_other_generator_with_full_namespace
- action :invoke, 'rails:model', ['my_model']
- assert_file 'app/models/my_model.rb', /MyModel/
+ action :invoke, "rails:model", ["my_model"]
+ assert_file "app/models/my_model.rb", /MyModel/
end
def test_create_file_should_write_data_to_file_path
- action :create_file, 'lib/test_file.rb', 'heres test data'
- assert_file 'lib/test_file.rb', 'heres test data'
+ action :create_file, "lib/test_file.rb", "heres test data"
+ assert_file "lib/test_file.rb", "heres test data"
end
def test_create_file_should_write_block_contents_to_file_path
- action(:create_file, 'lib/test_file.rb'){ 'heres block data' }
- assert_file 'lib/test_file.rb', 'heres block data'
+ action(:create_file, "lib/test_file.rb") { "heres block data" }
+ assert_file "lib/test_file.rb", "heres block data"
end
def test_add_source_adds_source_to_gemfile
run_generator
- action :add_source, 'http://gems.github.com'
- assert_file 'Gemfile', /source 'http:\/\/gems\.github\.com'/
+ action :add_source, "http://gems.github.com"
+ assert_file "Gemfile", /source 'http:\/\/gems\.github\.com'/
end
def test_add_source_with_block_adds_source_to_gemfile_with_gem
run_generator
- action :add_source, 'http://gems.github.com' do
- gem 'rspec-rails'
+ action :add_source, "http://gems.github.com" do
+ gem "rspec-rails"
end
- assert_file 'Gemfile', /source 'http:\/\/gems\.github\.com' do\n gem 'rspec-rails'\nend/
+ assert_file "Gemfile", /source 'http:\/\/gems\.github\.com' do\n gem 'rspec-rails'\nend/
+ end
+
+ def test_add_source_with_block_adds_source_to_gemfile_after_gem
+ run_generator
+ action :gem, "will-paginate"
+ action :add_source, "http://gems.github.com" do
+ gem "rspec-rails"
+ end
+ assert_file "Gemfile", /gem 'will-paginate'\nsource 'http:\/\/gems\.github\.com' do\n gem 'rspec-rails'\nend/
end
def test_gem_should_put_gem_dependency_in_gemfile
run_generator
- action :gem, 'will-paginate'
- assert_file 'Gemfile', /gem 'will\-paginate'/
+ action :gem, "will-paginate"
+ assert_file "Gemfile", /gem 'will\-paginate'/
end
def test_gem_with_version_should_include_version_in_gemfile
run_generator
-
- action :gem, 'rspec', '>=2.0.0.a5'
-
- assert_file 'Gemfile', /gem 'rspec', '>=2.0.0.a5'/
+ action :gem, "rspec", ">= 2.0.0.a5"
+ action :gem, "RedCloth", ">= 4.1.0", "< 4.2.0"
+ action :gem, "nokogiri", version: ">= 1.4.2"
+ action :gem, "faker", version: [">= 0.1.0", "< 0.3.0"]
+
+ assert_file "Gemfile" do |content|
+ assert_match(/gem 'rspec', '>= 2\.0\.0\.a5'/, content)
+ assert_match(/gem 'RedCloth', '>= 4\.1\.0', '< 4\.2\.0'/, content)
+ assert_match(/gem 'nokogiri', '>= 1\.4\.2'/, content)
+ assert_match(/gem 'faker', '>= 0\.1\.0', '< 0\.3\.0'/, content)
+ end
end
def test_gem_should_insert_on_separate_lines
run_generator
- File.open('Gemfile', 'a') {|f| f.write('# Some content...') }
+ File.open("Gemfile", "a") { |f| f.write("# Some content...") }
- action :gem, 'rspec'
- action :gem, 'rspec-rails'
+ action :gem, "rspec"
+ action :gem, "rspec-rails"
- assert_file 'Gemfile', /^gem 'rspec'$/
- assert_file 'Gemfile', /^gem 'rspec-rails'$/
+ assert_file "Gemfile", /^gem 'rspec'$/
+ assert_file "Gemfile", /^gem 'rspec-rails'$/
end
def test_gem_should_include_options
run_generator
- action :gem, 'rspec', github: 'dchelimsky/rspec', tag: '1.2.9.rc1'
+ action :gem, "rspec", github: "dchelimsky/rspec", tag: "1.2.9.rc1"
- assert_file 'Gemfile', /gem 'rspec', github: 'dchelimsky\/rspec', tag: '1\.2\.9\.rc1'/
+ assert_file "Gemfile", /gem 'rspec', github: 'dchelimsky\/rspec', tag: '1\.2\.9\.rc1'/
end
def test_gem_with_non_string_options
run_generator
- action :gem, 'rspec', require: false
- action :gem, 'rspec-rails', group: [:development, :test]
+ action :gem, "rspec", require: false
+ action :gem, "rspec-rails", group: [:development, :test]
- assert_file 'Gemfile', /^gem 'rspec', require: false$/
- assert_file 'Gemfile', /^gem 'rspec-rails', group: \[:development, :test\]$/
+ assert_file "Gemfile", /^gem 'rspec', require: false$/
+ assert_file "Gemfile", /^gem 'rspec-rails', group: \[:development, :test\]$/
end
def test_gem_falls_back_to_inspect_if_string_contains_single_quote
run_generator
- action :gem, 'rspec', ">=2.0'0"
+ action :gem, "rspec", ">=2.0'0"
+
+ assert_file "Gemfile", /^gem 'rspec', ">=2\.0'0"$/
+ end
+
+ def test_gem_works_even_if_frozen_string_is_passed_as_argument
+ run_generator
+
+ action :gem, -"frozen_gem", -"1.0.0"
- assert_file 'Gemfile', /^gem 'rspec', ">=2\.0'0"$/
+ assert_file "Gemfile", /^gem 'frozen_gem', '1.0.0'$/
end
def test_gem_group_should_wrap_gems_in_a_group
run_generator
action :gem_group, :development, :test do
- gem 'rspec-rails'
+ gem "rspec-rails"
end
action :gem_group, :test do
- gem 'fakeweb'
+ gem "fakeweb"
end
- assert_file 'Gemfile', /\ngroup :development, :test do\n gem 'rspec-rails'\nend\n\ngroup :test do\n gem 'fakeweb'\nend/
+ assert_file "Gemfile", /\ngroup :development, :test do\n gem 'rspec-rails'\nend\n\ngroup :test do\n gem 'fakeweb'\nend/
+ end
+
+ def test_github_should_create_an_indented_block
+ run_generator
+
+ action :github, "user/repo" do
+ gem "foo"
+ gem "bar"
+ gem "baz"
+ end
+
+ assert_file "Gemfile", /\ngithub 'user\/repo' do\n gem 'foo'\n gem 'bar'\n gem 'baz'\nend/
+ end
+
+ def test_github_should_create_an_indented_block_with_options
+ run_generator
+
+ action :github, "user/repo", a: "correct", other: true do
+ gem "foo"
+ gem "bar"
+ gem "baz"
+ end
+
+ assert_file "Gemfile", /\ngithub 'user\/repo', a: 'correct', other: true do\n gem 'foo'\n gem 'bar'\n gem 'baz'\nend/
+ end
+
+ def test_github_should_create_an_indented_block_within_a_group
+ run_generator
+
+ action :gem_group, :magic do
+ github "user/repo", a: "correct", other: true do
+ gem "foo"
+ gem "bar"
+ gem "baz"
+ end
+ end
+
+ assert_file "Gemfile", /\ngroup :magic do\n github 'user\/repo', a: 'correct', other: true do\n gem 'foo'\n gem 'bar'\n gem 'baz'\n end\nend\n/
end
def test_environment_should_include_data_in_environment_initializer_block
run_generator
autoload_paths = 'config.autoload_paths += %w["#{Rails.root}/app/extras"]'
action :environment, autoload_paths
- assert_file 'config/application.rb', / class Application < Rails::Application\n #{Regexp.escape(autoload_paths)}/
+ assert_file "config/application.rb", / class Application < Rails::Application\n #{Regexp.escape(autoload_paths)}\n/
end
def test_environment_should_include_data_in_environment_initializer_block_with_env_option
run_generator
autoload_paths = 'config.autoload_paths += %w["#{Rails.root}/app/extras"]'
- action :environment, autoload_paths, env: 'development'
- assert_file "config/environments/development.rb", /Rails\.application\.configure do\n #{Regexp.escape(autoload_paths)}/
+ action :environment, autoload_paths, env: "development"
+ assert_file "config/environments/development.rb", /Rails\.application\.configure do\n #{Regexp.escape(autoload_paths)}\n/
end
def test_environment_with_block_should_include_block_contents_in_environment_initializer_block
run_generator
action :environment do
- _ = '# This wont be added'# assignment to silence parse-time warning "unused literal ignored"
- '# This will be added'
+ _ = "# This wont be added"# assignment to silence parse-time warning "unused literal ignored"
+ "# This will be added"
end
- assert_file 'config/application.rb' do |content|
+ assert_file "config/application.rb" do |content|
assert_match(/# This will be added/, content)
assert_no_match(/# This wont be added/, content)
end
end
+ def test_environment_with_block_should_include_block_contents_with_multiline_data_in_environment_initializer_block
+ run_generator
+ data = <<-RUBY
+ config.encoding = "utf-8"
+ config.time_zone = "UTC"
+ RUBY
+ action(:environment) { data }
+ assert_file "config/application.rb", / class Application < Rails::Application\n#{Regexp.escape(data.strip_heredoc.indent(4))}/
+ end
+
+ def test_environment_should_include_block_contents_with_multiline_data_in_environment_initializer_block_with_env_option
+ run_generator
+ data = <<-RUBY
+ config.encoding = "utf-8"
+ config.time_zone = "UTC"
+ RUBY
+ action(:environment, nil, env: "development") { data }
+ assert_file "config/environments/development.rb", /Rails\.application\.configure do\n#{Regexp.escape(data.strip_heredoc.indent(2))}/
+ end
+
def test_git_with_symbol_should_run_command_using_git_scm
- @mock_generator.expect(:call, nil, ['git init'])
- generator.stub(:run, @mock_generator) do
+ assert_called_with(generator, :run, ["git init"]) do
action :git, :init
end
end
def test_git_with_hash_should_run_each_command_using_git_scm
- @mock_generator.expect(:call, nil, ["git rm README"])
- @mock_generator.expect(:call, nil, ["git add ."])
- generator.stub(:run, @mock_generator) do
- action :git, rm: 'README', add: '.'
+ assert_called_with(generator, :run, [ ["git rm README"], ["git add ."] ]) do
+ action :git, rm: "README", add: "."
end
end
def test_vendor_should_write_data_to_file_in_vendor
- action :vendor, 'vendor_file.rb', '# vendor data'
- assert_file 'vendor/vendor_file.rb', '# vendor data'
+ action :vendor, "vendor_file.rb", "# vendor data"
+ assert_file "vendor/vendor_file.rb", "# vendor data\n"
+ end
+
+ def test_vendor_should_write_data_to_file_with_block_in_vendor
+ code = <<-RUBY
+ puts "one"
+ puts "two"
+ puts "three"
+ RUBY
+ action(:vendor, "vendor_file.rb") { code }
+ assert_file "vendor/vendor_file.rb", code.strip_heredoc
end
def test_lib_should_write_data_to_file_in_lib
- action :lib, 'my_library.rb', 'class MyLibrary'
- assert_file 'lib/my_library.rb', 'class MyLibrary'
+ action :lib, "my_library.rb", "class MyLibrary"
+ assert_file "lib/my_library.rb", "class MyLibrary\n"
+ end
+
+ def test_lib_should_write_data_to_file_with_block_in_lib
+ code = <<-RUBY
+ class MyLib
+ MY_CONSTANT = 123
+ end
+ RUBY
+ action(:lib, "my_library.rb") { code }
+ assert_file "lib/my_library.rb", code.strip_heredoc
end
def test_rakefile_should_write_date_to_file_in_lib_tasks
- action :rakefile, 'myapp.rake', 'task run: [:environment]'
- assert_file 'lib/tasks/myapp.rake', 'task run: [:environment]'
+ action :rakefile, "myapp.rake", "task run: [:environment]"
+ assert_file "lib/tasks/myapp.rake", "task run: [:environment]\n"
+ end
+
+ def test_rakefile_should_write_date_to_file_with_block_in_lib_tasks
+ code = <<-RUBY
+ task rock: :environment do
+ puts "Rockin'"
+ end
+ RUBY
+ action(:rakefile, "myapp.rake") { code }
+ assert_file "lib/tasks/myapp.rake", code.strip_heredoc
end
def test_initializer_should_write_date_to_file_in_config_initializers
- action :initializer, 'constants.rb', 'MY_CONSTANT = 42'
- assert_file 'config/initializers/constants.rb', 'MY_CONSTANT = 42'
+ action :initializer, "constants.rb", "MY_CONSTANT = 42"
+ assert_file "config/initializers/constants.rb", "MY_CONSTANT = 42\n"
+ end
+
+ def test_initializer_should_write_date_to_file_with_block_in_config_initializers
+ code = <<-RUBY
+ MyLib.configure do |config|
+ config.value = 123
+ end
+ RUBY
+ action(:initializer, "constants.rb") { code }
+ assert_file "config/initializers/constants.rb", code.strip_heredoc
end
def test_generate_should_run_script_generate_with_argument_and_options
- @mock_generator.expect(:call, nil, ['bin/rails generate model MyModel', verbose: false])
- generator.stub(:run_ruby_script, @mock_generator) do
- action :generate, 'model', 'MyModel'
+ assert_called_with(generator, :run_ruby_script, ["bin/rails generate model MyModel", verbose: false]) do
+ action :generate, "model", "MyModel"
end
end
def test_rake_should_run_rake_command_with_default_env
- @mock_generator.expect(:call, nil, ["rake log:clear RAILS_ENV=development", verbose: false])
- generator.stub(:run, @mock_generator) do
+ assert_called_with(generator, :run, ["rake log:clear RAILS_ENV=development", verbose: false]) do
with_rails_env nil do
- action :rake, 'log:clear'
+ action :rake, "log:clear"
end
end
end
def test_rake_with_env_option_should_run_rake_command_in_env
- @mock_generator.expect(:call, nil, ['rake log:clear RAILS_ENV=production', verbose: false])
- generator.stub(:run, @mock_generator) do
- action :rake, 'log:clear', env: 'production'
+ assert_called_with(generator, :run, ["rake log:clear RAILS_ENV=production", verbose: false]) do
+ action :rake, "log:clear", env: "production"
end
end
- def test_rake_with_rails_env_variable_should_run_rake_command_in_env
- @mock_generator.expect(:call, nil, ['rake log:clear RAILS_ENV=production', verbose: false])
- generator.stub(:run, @mock_generator) do
+ test "rake with RAILS_ENV variable should run rake command in env" do
+ assert_called_with(generator, :run, ["rake log:clear RAILS_ENV=production", verbose: false]) do
with_rails_env "production" do
- action :rake, 'log:clear'
+ action :rake, "log:clear"
end
end
end
- def test_env_option_should_win_over_rails_env_variable_when_running_rake
- @mock_generator.expect(:call, nil, ['rake log:clear RAILS_ENV=production', verbose: false])
- generator.stub(:run, @mock_generator) do
+ test "env option should win over RAILS_ENV variable when running rake" do
+ assert_called_with(generator, :run, ["rake log:clear RAILS_ENV=production", verbose: false]) do
with_rails_env "staging" do
- action :rake, 'log:clear', env: 'production'
+ action :rake, "log:clear", env: "production"
end
end
end
- def test_rake_with_sudo_option_should_run_rake_command_with_sudo
- @mock_generator.expect(:call, nil, ["sudo rake log:clear RAILS_ENV=development", verbose: false])
- generator.stub(:run, @mock_generator) do
+ test "rake with sudo option should run rake command with sudo" do
+ assert_called_with(generator, :run, ["sudo rake log:clear RAILS_ENV=development", verbose: false]) do
with_rails_env nil do
- action :rake, 'log:clear', sudo: true
+ action :rake, "log:clear", sudo: true
+ end
+ end
+ end
+
+ test "rake command with capture option should run rake command with capture" do
+ assert_called_with(generator, :run, ["rake log:clear RAILS_ENV=development", verbose: false, capture: true]) do
+ with_rails_env nil do
+ action :rake, "log:clear", capture: true
+ end
+ end
+ end
+
+ test "rails command should run rails_command with default env" do
+ assert_called_with(generator, :run, ["rails log:clear RAILS_ENV=development", verbose: false]) do
+ with_rails_env nil do
+ action :rails_command, "log:clear"
+ end
+ end
+ end
+
+ test "rails command with env option should run rails_command with same env" do
+ assert_called_with(generator, :run, ["rails log:clear RAILS_ENV=production", verbose: false]) do
+ action :rails_command, "log:clear", env: "production"
+ end
+ end
+
+ test "rails command with RAILS_ENV variable should run rails_command in env" do
+ assert_called_with(generator, :run, ["rails log:clear RAILS_ENV=production", verbose: false]) do
+ with_rails_env "production" do
+ action :rails_command, "log:clear"
+ end
+ end
+ end
+
+ def test_env_option_should_win_over_rails_env_variable_when_running_rails
+ assert_called_with(generator, :run, ["rails log:clear RAILS_ENV=production", verbose: false]) do
+ with_rails_env "staging" do
+ action :rails_command, "log:clear", env: "production"
+ end
+ end
+ end
+
+ test "rails command with sudo option should run rails_command with sudo" do
+ assert_called_with(generator, :run, ["sudo rails log:clear RAILS_ENV=development", verbose: false]) do
+ with_rails_env nil do
+ action :rails_command, "log:clear", sudo: true
+ end
+ end
+ end
+
+ test "rails command with capture option should run rails_command with capture" do
+ assert_called_with(generator, :run, ["rails log:clear RAILS_ENV=development", verbose: false, capture: true]) do
+ with_rails_env nil do
+ action :rails_command, "log:clear", capture: true
end
end
end
def test_capify_should_run_the_capify_command
- @mock_generator.expect(:call, nil, ['capify .', verbose: false])
- generator.stub(:run, @mock_generator) do
- action :capify!
+ content = capture(:stderr) do
+ assert_called_with(generator, :run, ["capify .", verbose: false]) do
+ action :capify!
+ end
end
+ assert_match(/DEPRECATION WARNING: `capify!` is deprecated/, content)
end
def test_route_should_add_data_to_the_routes_block_in_config_routes
run_generator
route_command = "route '/login', controller: 'sessions', action: 'new'"
action :route, route_command
- assert_file 'config/routes.rb', /#{Regexp.escape(route_command)}/
+ assert_file "config/routes.rb", /#{Regexp.escape(route_command)}/
+ end
+
+ def test_route_should_be_idempotent
+ run_generator
+ route_path = File.expand_path("config/routes.rb", destination_root)
+
+ # runs first time, not asserting
+ action :route, "root 'welcome#index'"
+ content_1 = File.read(route_path)
+
+ # runs second time
+ action :route, "root 'welcome#index'"
+ content_2 = File.read(route_path)
+
+ assert_equal content_1, content_2
end
def test_route_should_add_data_with_an_new_line
@@ -255,11 +438,18 @@ class ActionsTest < Rails::Generators::TestCase
content = File.read(route_path)
# Remove all of the comments and blank lines from the routes file
- content.gsub!(/^ \#.*\n/, '')
- content.gsub!(/^\n/, '')
+ content.gsub!(/^ \#.*\n/, "")
+ content.gsub!(/^\n/, "")
+
+ File.write(route_path, content)
+
+ routes = <<-F
+Rails.application.routes.draw do
+ root 'welcome#index'
+end
+F
- File.open(route_path, "wb") { |file| file.write(content) }
- assert_file "config/routes.rb", /\.routes\.draw do\n root 'welcome#index'\nend\n\z/
+ assert_file "config/routes.rb", routes
action :route, "resources :product_lines"
@@ -274,8 +464,7 @@ F
def test_readme
run_generator
- 2.times { @mock_generator.expect(:call, destination_root,[]) }
- Rails::Generators::AppGenerator.stub(:source_root, @mock_generator) do
+ assert_called(Rails::Generators::AppGenerator, :source_root, times: 2, returns: destination_root) do
assert_match "application up and running", action(:readme, "README.md")
end
end
@@ -283,8 +472,7 @@ F
def test_readme_with_quiet
generator(default_arguments, quiet: true)
run_generator
- 2.times { @mock_generator.expect(:call, destination_root,[]) }
- Rails::Generators::AppGenerator.stub(:source_root, @mock_generator) do
+ assert_called(Rails::Generators::AppGenerator, :source_root, times: 2, returns: destination_root) do
assert_no_match "application up and running", action(:readme, "README.md")
end
end
@@ -307,10 +495,9 @@ F
assert_equal("", action(:log, :yes, "YES"))
end
- protected
+ private
def action(*args, &block)
- capture(:stdout){ generator.send(*args, &block) }
+ capture(:stdout) { generator.send(*args, &block) }
end
-
end
diff --git a/railties/test/generators/api_app_generator_test.rb b/railties/test/generators/api_app_generator_test.rb
index 9978ad0da1..4f2894e71e 100644
--- a/railties/test/generators/api_app_generator_test.rb
+++ b/railties/test/generators/api_app_generator_test.rb
@@ -1,17 +1,19 @@
-require 'generators/generators_test_helper'
-require 'rails/generators/rails/app/app_generator'
+# frozen_string_literal: true
+
+require "generators/generators_test_helper"
+require "rails/generators/rails/app/app_generator"
class ApiAppGeneratorTest < Rails::Generators::TestCase
include GeneratorsTestHelper
tests Rails::Generators::AppGenerator
- arguments [destination_root, '--api']
+ arguments [destination_root, "--api"]
def setup
Rails.application = TestApp::Application
super
- Kernel::silence_warnings do
+ Kernel.silence_warnings do
Thor::Base.shell.send(:attr_accessor, :always_force)
@shell = Thor::Base.shell.new
@shell.send(:always_force=, true)
@@ -33,64 +35,147 @@ class ApiAppGeneratorTest < Rails::Generators::TestCase
def test_api_modified_files
run_generator
+ assert_file ".gitignore" do |content|
+ assert_no_match(/\/public\/assets/, content)
+ end
+
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 'jbuilder'/, content)
- assert_match(/gem 'active_model_serializers'/, content)
+ assert_no_match(/gem 'web-console'/, content)
+ assert_no_match(/gem 'capybara'/, content)
+ assert_no_match(/gem 'selenium-webdriver'/, content)
+ assert_match(/# gem 'jbuilder'/, content)
+ assert_match(/# gem 'rack-cors'/, content)
+ end
+
+ assert_file "config/application.rb", /config\.api_only = true/
+ assert_file "app/controllers/application_controller.rb", /ActionController::API/
+ end
+
+ def test_generator_if_skip_action_cable_is_given
+ run_generator [destination_root, "--api", "--skip-action-cable"]
+ assert_file "config/application.rb", /#\s+require\s+["']action_cable\/engine["']/
+ assert_no_file "config/cable.yml"
+ assert_no_file "app/channels"
+ assert_file "Gemfile" do |content|
+ assert_no_match(/redis/, content)
end
+ end
- assert_file "config/application.rb" do |content|
- assert_match(/config.api_only = true/, content)
+ def test_generator_if_skip_action_mailer_is_given
+ run_generator [destination_root, "--api", "--skip-action-mailer"]
+ assert_file "config/application.rb", /#\s+require\s+["']action_mailer\/railtie["']/
+ assert_file "config/environments/development.rb" do |content|
+ assert_no_match(/config\.action_mailer/, content)
+ end
+ assert_file "config/environments/test.rb" do |content|
+ assert_no_match(/config\.action_mailer/, content)
+ end
+ assert_file "config/environments/production.rb" do |content|
+ assert_no_match(/config\.action_mailer/, content)
end
+ assert_no_directory "app/mailers"
+ assert_no_directory "test/mailers"
+ assert_no_directory "app/views"
+ end
- assert_file "config/initializers/cors.rb"
+ def test_app_update_does_not_generate_unnecessary_config_files
+ run_generator
- assert_file "config/initializers/wrap_parameters.rb"
+ generator = Rails::Generators::AppGenerator.new ["rails"],
+ { api: true, update: true }, { destination_root: destination_root, shell: @shell }
+ quietly { generator.send(:update_config_files) }
- assert_file "app/controllers/application_controller.rb", /ActionController::API/
+ assert_no_file "config/initializers/cookies_serializer.rb"
+ assert_no_file "config/initializers/assets.rb"
+ assert_no_file "config/initializers/content_security_policy.rb"
end
- private
+ def test_app_update_does_not_generate_unnecessary_bin_files
+ run_generator
- def default_files
- files = %W(
- .gitignore
- Gemfile
- Rakefile
- config.ru
- app/controllers
- app/mailers
- app/models
- config/environments
- config/initializers
- config/locales
- db
- lib
- lib/tasks
- log
- test/fixtures
- test/controllers
- test/integration
- test/models
- tmp
- vendor
- )
- files.concat %w(bin/bundle bin/rails bin/rake)
- files
- end
+ generator = Rails::Generators::AppGenerator.new ["rails"],
+ { api: true, update: true }, { destination_root: destination_root, shell: @shell }
+ quietly { generator.send(:update_bin_files) }
- def skipped_files
- %w(app/assets
- app/helpers
- app/views
- config/initializers/assets.rb
- config/initializers/cookies_serializer.rb
- config/initializers/session_store.rb
- lib/assets
- vendor/assets
- test/helpers
- tmp/cache/assets)
+ assert_no_file "bin/yarn"
end
+
+ private
+
+ def default_files
+ %w(.gitignore
+ .ruby-version
+ README.md
+ Gemfile
+ Rakefile
+ config.ru
+ app/channels
+ app/controllers
+ app/mailers
+ app/models
+ app/views/layouts
+ app/views/layouts/mailer.html.erb
+ app/views/layouts/mailer.text.erb
+ bin/rails
+ bin/rake
+ bin/setup
+ bin/update
+ config/application.rb
+ config/boot.rb
+ config/cable.yml
+ config/environment.rb
+ config/environments
+ config/environments/development.rb
+ config/environments/production.rb
+ config/environments/test.rb
+ config/initializers
+ config/initializers/application_controller_renderer.rb
+ config/initializers/backtrace_silencers.rb
+ config/initializers/cors.rb
+ config/initializers/filter_parameter_logging.rb
+ config/initializers/inflections.rb
+ config/initializers/mime_types.rb
+ config/initializers/wrap_parameters.rb
+ config/locales
+ config/locales/en.yml
+ config/puma.rb
+ config/routes.rb
+ config/credentials.yml.enc
+ config/spring.rb
+ config/storage.yml
+ db
+ db/seeds.rb
+ lib
+ lib/tasks
+ log
+ test/fixtures
+ test/controllers
+ test/integration
+ test/models
+ tmp
+ vendor
+ )
+ end
+
+ def skipped_files
+ %w(app/assets
+ app/helpers
+ app/views/layouts/application.html.erb
+ bin/yarn
+ config/initializers/assets.rb
+ config/initializers/cookies_serializer.rb
+ config/initializers/content_security_policy.rb
+ lib/assets
+ test/helpers
+ tmp/cache/assets
+ public/404.html
+ public/422.html
+ public/500.html
+ public/apple-touch-icon-precomposed.png
+ public/apple-touch-icon.png
+ public/favicon.ico
+ package.json
+ )
+ end
end
diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb
index daf362357c..32d00f3157 100644
--- a/railties/test/generators/app_generator_test.rb
+++ b/railties/test/generators/app_generator_test.rb
@@ -1,37 +1,83 @@
-require 'generators/generators_test_helper'
-require 'rails/generators/rails/app/app_generator'
-require 'generators/shared_generator_tests'
-require 'mocha/setup' # FIXME: stop using mocha
+# frozen_string_literal: true
+
+require "generators/generators_test_helper"
+require "rails/generators/rails/app/app_generator"
+require "generators/shared_generator_tests"
DEFAULT_APP_FILES = %w(
.gitignore
+ .ruby-version
README.md
Gemfile
Rakefile
config.ru
- app/assets/javascripts
- app/assets/stylesheets
+ app/assets/config/manifest.js
app/assets/images
+ app/javascript
+ app/javascript/channels
+ app/javascript/channels/consumer.js
+ app/javascript/channels/index.js
+ app/javascript/packs/application.js
+ app/assets/stylesheets
+ app/assets/stylesheets/application.css
+ app/channels/application_cable/channel.rb
+ app/channels/application_cable/connection.rb
app/controllers
+ app/controllers/application_controller.rb
app/controllers/concerns
app/helpers
+ app/helpers/application_helper.rb
app/mailers
+ app/mailers/application_mailer.rb
app/models
+ app/models/application_record.rb
app/models/concerns
app/jobs
+ app/jobs/application_job.rb
app/views/layouts
- bin/bundle
+ app/views/layouts/application.html.erb
+ app/views/layouts/mailer.html.erb
+ app/views/layouts/mailer.text.erb
bin/rails
bin/rake
bin/setup
+ bin/update
+ bin/yarn
+ config/application.rb
+ config/boot.rb
+ config/cable.yml
+ config/environment.rb
config/environments
+ config/environments/development.rb
+ config/environments/production.rb
+ config/environments/test.rb
config/initializers
+ config/initializers/application_controller_renderer.rb
+ config/initializers/assets.rb
+ config/initializers/backtrace_silencers.rb
+ config/initializers/cookies_serializer.rb
+ config/initializers/content_security_policy.rb
+ config/initializers/filter_parameter_logging.rb
+ config/initializers/inflections.rb
+ config/initializers/mime_types.rb
+ config/initializers/wrap_parameters.rb
config/locales
+ config/locales/en.yml
+ config/puma.rb
+ config/routes.rb
+ config/credentials.yml.enc
+ config/spring.rb
+ config/storage.yml
db
+ db/seeds.rb
lib
lib/tasks
lib/assets
log
+ package.json
+ public
+ storage
+ test/application_system_test_case.rb
test/test_helper.rb
test/fixtures
test/fixtures/files
@@ -40,13 +86,12 @@ 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
+ tmp/storage
)
class AppGeneratorTest < Rails::Generators::TestCase
@@ -60,13 +105,22 @@ class AppGeneratorTest < Rails::Generators::TestCase
::DEFAULT_APP_FILES
end
+ def test_skip_bundle
+ assert_not_called(generator([destination_root], skip_bundle: true, skip_webpack_install: true), :bundle_command) do
+ quietly { generator.invoke_all }
+ # skip_bundle is only about running bundle install, ensure the Gemfile is still
+ # generated.
+ assert_file "Gemfile"
+ end
+ end
+
def test_assets
run_generator
- assert_file("app/views/layouts/application.html.erb", /stylesheet_link_tag\s+'application', media: 'all', 'data-turbolinks-track' => true/)
- assert_file("app/views/layouts/application.html.erb", /javascript_include_tag\s+'application', 'data-turbolinks-track' => true/)
+ assert_file("app/views/layouts/application.html.erb", /stylesheet_link_tag\s+'application', media: 'all', 'data-turbolinks-track': 'reload'/)
+ assert_file("app/views/layouts/application.html.erb", /javascript_pack_tag\s+'application', 'data-turbolinks-track': 'reload'/)
assert_file("app/assets/stylesheets/application.css")
- assert_file("app/assets/javascripts/application.js")
+ assert_file("app/javascript/packs/application.js")
end
def test_application_job_file_present
@@ -75,7 +129,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
def test_invalid_application_name_raises_an_error
- content = capture(:stderr){ run_generator [File.join(destination_root, "43-things")] }
+ content = capture(:stderr) { run_generator [File.join(destination_root, "43-things")] }
assert_equal "Invalid application name 43-things. Please give a name which does not start with numbers.\n", content
end
@@ -86,12 +140,12 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
def test_application_new_exits_with_non_zero_code_on_invalid_application_name
- quietly { system 'rails new test --no-rc' }
+ quietly { system "rails new test --no-rc" }
assert_equal false, $?.success?
end
def test_application_new_exits_with_message_and_non_zero_code_when_generating_inside_existing_rails_directory
- app_root = File.join(destination_root, 'myfirstapp')
+ app_root = File.join(destination_root, "myfirstapp")
run_generator [app_root]
output = nil
Dir.chdir(app_root) do
@@ -102,7 +156,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
def test_application_new_show_help_message_inside_existing_rails_directory
- app_root = File.join(destination_root, 'myfirstapp')
+ app_root = File.join(destination_root, "myfirstapp")
run_generator [app_root]
output = Dir.chdir(app_root) do
`rails new --help`
@@ -117,35 +171,31 @@ class AppGeneratorTest < Rails::Generators::TestCase
run_generator [app_root]
- Rails.application.config.root = app_moved_root
- Rails.application.class.stubs(:name).returns("Myapp")
- Rails.application.stubs(:is_a?).returns(Rails::Application)
-
- FileUtils.mv(app_root, app_moved_root)
+ stub_rails_application(app_moved_root) do
+ Rails.application.stub(:is_a?, -> *args { Rails::Application }) do
+ FileUtils.mv(app_root, app_moved_root)
- # make sure we are in correct dir
- FileUtils.cd(app_moved_root)
+ # make sure we are in correct dir
+ FileUtils.cd(app_moved_root)
- generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true },
- destination_root: app_moved_root, shell: @shell
- generator.send(:app_const)
- quietly { generator.send(:update_config_files) }
- assert_file "myapp_moved/config/environment.rb", /Rails\.application\.initialize!/
- assert_file "myapp_moved/config/initializers/session_store.rb", /_myapp_session/
+ generator = Rails::Generators::AppGenerator.new ["rails"], [],
+ destination_root: app_moved_root, shell: @shell
+ generator.send(:app_const)
+ quietly { generator.send(:update_config_files) }
+ assert_file "myapp_moved/config/environment.rb", /Rails\.application\.initialize!/
+ end
+ end
end
- def test_rails_update_generates_correct_session_key
- app_root = File.join(destination_root, 'myapp')
+ def test_app_update_generates_correct_session_key
+ app_root = File.join(destination_root, "myapp")
run_generator [app_root]
- Rails.application.config.root = app_root
- Rails.application.class.stubs(:name).returns("Myapp")
- Rails.application.stubs(:is_a?).returns(Rails::Application)
-
- generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell
- generator.send(:app_const)
- quietly { generator.send(:update_config_files) }
- assert_file "myapp/config/initializers/session_store.rb", /_myapp_session/
+ 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) }
+ end
end
def test_new_application_use_json_serialzier
@@ -154,98 +204,272 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_file("config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :json/)
end
- def test_rails_update_keep_the_cookie_serializer_if_it_is_already_configured
- app_root = File.join(destination_root, 'myapp')
- run_generator [app_root]
+ def test_new_application_not_include_api_initializers
+ run_generator
- Rails.application.config.root = app_root
- Rails.application.class.stubs(:name).returns("Myapp")
- Rails.application.stubs(:is_a?).returns(Rails::Application)
+ assert_no_file "config/initializers/cors.rb"
+ end
- generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, 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/)
+ def test_new_application_doesnt_need_defaults
+ run_generator
+ assert_no_file "config/initializers/new_framework_defaults_6_0.rb"
end
- def test_rails_update_does_not_create_callback_terminator_initializer
- app_root = File.join(destination_root, 'myapp')
+ def test_new_application_load_defaults
+ app_root = File.join(destination_root, "myfirstapp")
run_generator [app_root]
- FileUtils.rm("#{app_root}/config/initializers/callback_terminator.rb")
+ output = nil
- Rails.application.config.root = app_root
- Rails.application.class.stubs(:name).returns("Myapp")
- Rails.application.stubs(:is_a?).returns(Rails::Application)
+ assert_file "#{app_root}/config/application.rb", /\s+config\.load_defaults #{Rails::VERSION::STRING.to_f}/
- generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell
- generator.send(:app_const)
- quietly { generator.send(:update_config_files) }
- assert_no_file "#{app_root}/config/initializers/callback_terminator.rb"
+ Dir.chdir(app_root) do
+ output = `SKIP_REQUIRE_WEBPACKER=true ./bin/rails r "puts Rails.application.config.assets.unknown_asset_fallback"`
+ end
+
+ assert_equal "false\n", output
end
- def test_rails_update_does_not_remove_callback_terminator_initializer_if_already_present
- app_root = File.join(destination_root, 'myapp')
- run_generator [app_root]
+ def test_csp_initializer_include_connect_src_example
+ run_generator
- FileUtils.touch("#{app_root}/config/initializers/callback_terminator.rb")
+ assert_file "config/initializers/content_security_policy.rb" do |content|
+ assert_match(/# policy\.connect_src/, content)
+ end
+ end
- Rails.application.config.root = app_root
- Rails.application.class.stubs(:name).returns("Myapp")
- Rails.application.stubs(:is_a?).returns(Rails::Application)
+ def test_app_update_keep_the_cookie_serializer_if_it_is_already_configured
+ app_root = File.join(destination_root, "myapp")
+ run_generator [app_root]
- generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell
- generator.send(:app_const)
- quietly { generator.send(:update_config_files) }
- assert_file "#{app_root}/config/initializers/callback_terminator.rb"
+ 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/)
+ end
end
- def test_rails_update_set_the_cookie_serializer_to_marchal_if_it_is_not_already_configured
- app_root = File.join(destination_root, 'myapp')
+ 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")
- Rails.application.config.root = app_root
- Rails.application.class.stubs(:name).returns("Myapp")
- Rails.application.stubs(:is_a?).returns(Rails::Application)
-
- generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell
- generator.send(:app_const)
- quietly { generator.send(:update_config_files) }
- assert_file("#{app_root}/config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :marshal/)
+ 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/)
+ end
end
- def test_rails_update_does_not_create_active_record_belongs_to_required_by_default
- app_root = File.join(destination_root, 'myapp')
+ 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/active_record_belongs_to_required_by_default.rb")
+ assert_no_file "#{app_root}/config/initializers/new_framework_defaults_6_0.rb"
- Rails.application.config.root = app_root
- Rails.application.class.stubs(:name).returns("Myapp")
- Rails.application.stubs(:is_a?).returns(Rails::Application)
+ 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) }
- generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell
- generator.send(:app_const)
- quietly { generator.send(:update_config_files) }
- assert_no_file "#{app_root}/config/initializers/active_record_belongs_to_required_by_default.rb"
+ assert_file "#{app_root}/config/initializers/new_framework_defaults_6_0.rb"
+ end
+ end
+
+ def test_app_update_does_not_create_rack_cors
+ app_root = File.join(destination_root, "myapp")
+ run_generator [app_root]
+
+ stub_rails_application(app_root) do
+ generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell
+ generator.send(:app_const)
+ quietly { generator.send(:update_config_files) }
+ assert_no_file "#{app_root}/config/initializers/cors.rb"
+ end
end
- def test_rails_update_does_not_remove_active_record_belongs_to_required_by_default_if_already_present
- app_root = File.join(destination_root, 'myapp')
+ def test_app_update_does_not_remove_rack_cors_if_already_present
+ app_root = File.join(destination_root, "myapp")
run_generator [app_root]
- FileUtils.touch("#{app_root}/config/initializers/active_record_belongs_to_required_by_default.rb")
+ FileUtils.touch("#{app_root}/config/initializers/cors.rb")
+
+ stub_rails_application(app_root) do
+ generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell
+ generator.send(:app_const)
+ quietly { generator.send(:update_config_files) }
+ assert_file "#{app_root}/config/initializers/cors.rb"
+ end
+ end
+
+ def test_app_update_does_not_generate_yarn_contents_when_bin_yarn_is_not_used
+ app_root = File.join(destination_root, "myapp")
+ run_generator [app_root, "--skip-javascript"]
+
+ stub_rails_application(app_root) do
+ generator = Rails::Generators::AppGenerator.new ["rails"], { update: true, skip_javascript: true }, { destination_root: app_root, shell: @shell }
+ generator.send(:app_const)
+ quietly { generator.send(:update_bin_files) }
+
+ assert_no_file "#{app_root}/bin/yarn"
+
+ assert_file "#{app_root}/bin/setup" do |content|
+ assert_no_match(/system\('bin\/yarn'\)/, content)
+ end
+
+ assert_file "#{app_root}/bin/update" do |content|
+ assert_no_match(/system\('bin\/yarn'\)/, content)
+ end
+ end
+ end
+
+ def test_app_update_does_not_generate_assets_initializer_when_skip_sprockets_is_given
+ app_root = File.join(destination_root, "myapp")
+ run_generator [app_root, "--skip-sprockets"]
+
+ stub_rails_application(app_root) do
+ generator = Rails::Generators::AppGenerator.new ["rails"], { update: true, skip_sprockets: true }, { destination_root: app_root, shell: @shell }
+ generator.send(:app_const)
+ quietly { generator.send(:update_config_files) }
+
+ assert_no_file "#{app_root}/config/initializers/assets.rb"
+ end
+ end
+
+ def test_app_update_does_not_generate_spring_contents_when_skip_spring_is_given
+ app_root = File.join(destination_root, "myapp")
+ run_generator [app_root, "--skip-spring"]
+
+ FileUtils.cd(app_root) do
+ quietly { system("bin/rails app:update") }
+ end
+
+ assert_no_file "#{app_root}/config/spring.rb"
+ end
+
+ def test_app_update_does_not_generate_action_cable_contents_when_skip_action_cable_is_given
+ app_root = File.join(destination_root, "myapp")
+ run_generator [app_root, "--skip-action-cable"]
+
+ FileUtils.cd(app_root) do
+ quietly { system("bin/rails app:update") }
+ end
+
+ assert_no_file "#{app_root}/config/cable.yml"
+ assert_file "#{app_root}/config/environments/production.rb" do |content|
+ assert_no_match(/config\.action_cable/, content)
+ end
+ end
+
+ def test_app_update_does_not_generate_bootsnap_contents_when_skip_bootsnap_is_given
+ app_root = File.join(destination_root, "myapp")
+ run_generator [app_root, "--skip-bootsnap"]
+
+ FileUtils.cd(app_root) do
+ quietly { system("bin/rails app:update") }
+ end
+
+ assert_file "#{app_root}/config/boot.rb" do |content|
+ assert_no_match(/require 'bootsnap\/setup'/, content)
+ end
+ end
+
+ def test_gem_for_active_storage
+ run_generator
+ assert_file "Gemfile", /^# gem 'image_processing'/
+ end
+
+ def test_gem_for_active_storage_when_skip_active_storage_is_given
+ run_generator [destination_root, "--skip-active-storage"]
+
+ assert_no_gem "image_processing"
+ end
+
+ def test_app_update_does_not_generate_active_storage_contents_when_skip_active_storage_is_given
+ app_root = File.join(destination_root, "myapp")
+ run_generator [app_root, "--skip-active-storage"]
+
+ FileUtils.cd(app_root) do
+ quietly { system("bin/rails app:update") }
+ end
+
+ assert_file "#{app_root}/config/environments/development.rb" do |content|
+ assert_no_match(/config\.active_storage/, content)
+ end
+
+ assert_file "#{app_root}/config/environments/production.rb" do |content|
+ assert_no_match(/config\.active_storage/, content)
+ end
+
+ assert_file "#{app_root}/config/environments/test.rb" do |content|
+ assert_no_match(/config\.active_storage/, content)
+ end
+
+ assert_no_file "#{app_root}/config/storage.yml"
+ end
+
+ def test_app_update_does_not_generate_active_storage_contents_when_skip_active_record_is_given
+ app_root = File.join(destination_root, "myapp")
+ run_generator [app_root, "--skip-active-record"]
+
+ FileUtils.cd(app_root) do
+ quietly { system("bin/rails app:update") }
+ end
+
+ assert_file "#{app_root}/config/environments/development.rb" do |content|
+ assert_no_match(/config\.active_storage/, content)
+ end
+
+ assert_file "#{app_root}/config/environments/production.rb" do |content|
+ assert_no_match(/config\.active_storage/, content)
+ end
+
+ assert_file "#{app_root}/config/environments/test.rb" do |content|
+ assert_no_match(/config\.active_storage/, content)
+ end
+
+ assert_no_file "#{app_root}/config/storage.yml"
+ end
+
+ def test_app_update_does_not_change_config_target_version
+ run_generator
+
+ FileUtils.cd(destination_root) do
+ config = "config/application.rb"
+ content = File.read(config)
+ File.write(config, content.gsub(/config\.load_defaults #{Rails::VERSION::STRING.to_f}/, "config.load_defaults 5.1"))
+ quietly { system("bin/rails app:update") }
+ end
+
+ assert_file "config/application.rb", /\s+config\.load_defaults 5\.1/
+ end
+
+ def test_app_update_does_not_change_app_name_when_app_name_is_hyphenated_name
+ app_root = File.join(destination_root, "hyphenated-app")
+ run_generator [app_root, "-d", "postgresql"]
+
+ assert_file "#{app_root}/config/database.yml" do |content|
+ assert_match(/hyphenated_app_development/, content)
+ assert_no_match(/hyphenated-app_development/, content)
+ end
+
+ assert_file "#{app_root}/config/cable.yml" do |content|
+ assert_match(/hyphenated_app/, content)
+ assert_no_match(/hyphenated-app/, content)
+ end
- Rails.application.config.root = app_root
- Rails.application.class.stubs(:name).returns("Myapp")
- Rails.application.stubs(:is_a?).returns(Rails::Application)
+ FileUtils.cd(app_root) do
+ quietly { system("bin/rails app:update") }
+ end
- generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell
- generator.send(:app_const)
- quietly { generator.send(:update_config_files) }
- assert_file "#{app_root}/config/initializers/active_record_belongs_to_required_by_default.rb"
+ assert_file "#{app_root}/config/cable.yml" do |content|
+ assert_match(/hyphenated_app/, content)
+ assert_no_match(/hyphenated-app/, content)
+ end
end
def test_application_names_are_not_singularized
@@ -256,7 +480,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
def test_gemfile_has_no_whitespace_errors
run_generator
absolute = File.expand_path("Gemfile", destination_root)
- File.open(absolute, 'r') do |f|
+ File.open(absolute, "r") do |f|
f.each_line do |line|
assert_no_match %r{/^[ \t]+$/}, line
end
@@ -273,13 +497,13 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
end
- def test_config_another_database
+ def test_config_mysql_database
run_generator([destination_root, "-d", "mysql"])
assert_file "config/database.yml", /mysql/
if defined?(JRUBY_VERSION)
assert_gem "activerecord-jdbcmysql-adapter"
else
- assert_gem "mysql2"
+ assert_gem "mysql2", "'>= 0.4.4'"
end
end
@@ -294,7 +518,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
if defined?(JRUBY_VERSION)
assert_gem "activerecord-jdbcpostgresql-adapter"
else
- assert_gem "pg"
+ assert_gem "pg", "'>= 0.18', '< 2.0'"
end
end
@@ -331,122 +555,109 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
end
- def test_generator_without_skips
- run_generator
- assert_file "config/application.rb", /\s+require\s+["']rails\/all["']/
- assert_file "config/environments/development.rb" do |content|
- assert_match(/config\.action_mailer\.raise_delivery_errors = false/, content)
- end
- assert_file "config/environments/test.rb" do |content|
- assert_match(/config\.action_mailer\.delivery_method = :test/, content)
- end
- assert_file "config/environments/production.rb" do |content|
- assert_match(/# config\.action_mailer\.raise_delivery_errors = false/, content)
- end
+ def test_generator_defaults_to_puma_version
+ run_generator [destination_root]
+ assert_gem "puma", "'~> 3.11'"
end
- def test_generator_if_skip_active_record_is_given
- run_generator [destination_root, "--skip-active-record"]
- assert_no_file "config/database.yml"
- assert_no_file "config/initializers/active_record_belongs_to_required_by_default.rb"
- assert_file "config/application.rb", /#\s+require\s+["']active_record\/railtie["']/
- assert_file "test/test_helper.rb" do |helper_content|
- assert_no_match(/fixtures :all/, helper_content)
- end
+ def test_generator_if_skip_puma_is_given
+ run_generator [destination_root, "--skip-puma"]
+ assert_no_file "config/puma.rb"
+ assert_no_gem "puma"
end
- def test_generator_if_skip_action_mailer_is_given
- run_generator [destination_root, "--skip-action-mailer"]
- assert_file "config/application.rb", /#\s+require\s+["']action_mailer\/railtie["']/
- assert_file "config/environments/development.rb" do |content|
- assert_no_match(/config\.action_mailer/, content)
- end
- assert_file "config/environments/test.rb" do |content|
- assert_no_match(/config\.action_mailer/, content)
- end
- assert_file "config/environments/production.rb" do |content|
- assert_no_match(/config\.action_mailer/, content)
- end
- end
+ def test_generator_has_assets_gems
+ run_generator
- def test_generator_if_skip_sprockets_is_given
- run_generator [destination_root, "--skip-sprockets"]
- assert_no_file "config/initializers/assets.rb"
- assert_file "config/application.rb" do |content|
- assert_match(/#\s+require\s+["']sprockets\/railtie["']/, content)
- end
- assert_file "Gemfile" do |content|
- assert_no_match(/sass-rails/, content)
- assert_no_match(/uglifier/, content)
- assert_match(/coffee-rails/, content)
- end
- assert_file "config/environments/development.rb" do |content|
- assert_no_match(/config\.assets\.debug = true/, content)
- end
- assert_file "config/environments/production.rb" do |content|
- assert_no_match(/config\.assets\.digest = true/, content)
- assert_no_match(/config\.assets\.js_compressor = :uglifier/, content)
- assert_no_match(/config\.assets\.css_compressor = :sass/, content)
- end
+ assert_gem "sass-rails"
end
- def test_inclusion_of_javascript_runtime
+ def test_action_cable_redis_gems
run_generator
- if defined?(JRUBY_VERSION)
- assert_gem "therubyrhino"
- else
- assert_file "Gemfile", /# gem 'therubyracer', platforms: :ruby/
- end
+ assert_file "Gemfile", /^# gem 'redis'/
end
- def test_jquery_is_the_default_javascript_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
- end
- assert_gem "jquery-rails"
+ def test_generator_if_skip_test_is_given
+ run_generator [destination_root, "--skip-test"]
+
+ assert_file "config/application.rb", /#\s+require\s+["']rails\/test_unit\/railtie["']/
+
+ assert_no_gem "capybara"
+ assert_no_gem "selenium-webdriver"
+ assert_no_gem "chromedriver-helper"
+
+ assert_no_directory("test")
+ end
+
+ def test_generator_if_skip_system_test_is_given
+ run_generator [destination_root, "--skip-system-test"]
+ assert_no_gem "capybara"
+ assert_no_gem "selenium-webdriver"
+ assert_no_gem "chromedriver-helper"
+
+ assert_directory("test")
+
+ assert_no_directory("test/system")
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
+ def test_does_not_generate_system_test_files_if_skip_system_test_is_given
+ run_generator [destination_root, "--skip-system-test"]
+
+ Dir.chdir(destination_root) do
+ quietly { `./bin/rails g scaffold User` }
+
+ assert_no_file("test/application_system_test_case.rb")
+ assert_no_file("test/system/users_test.rb")
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_no_file "app/javascript"
assert_file "app/views/layouts/application.html.erb" do |contents|
assert_match(/stylesheet_link_tag\s+'application', media: 'all' %>/, contents)
- assert_no_match(/javascript_include_tag\s+'application' \%>/, contents)
- end
-
- assert_file "Gemfile" do |content|
- assert_no_match(/coffee-rails/, content)
- assert_no_match(/jquery-rails/, content)
+ assert_no_match(/javascript_pack_tag\s+'application'/, contents)
end
end
def test_inclusion_of_jbuilder
run_generator
- assert_gem 'jbuilder'
+ assert_gem "jbuilder"
end
def test_inclusion_of_a_debugger
run_generator
if defined?(JRUBY_VERSION) || RUBY_ENGINE == "rbx"
- assert_file "Gemfile" do |content|
- assert_no_match(/byebug/, content)
- end
+ assert_no_gem "byebug"
+ else
+ assert_gem "byebug"
+ end
+ end
+
+ def test_inclusion_of_listen_related_configuration_by_default
+ run_generator
+ if RbConfig::CONFIG["host_os"] =~ /darwin|linux/
+ assert_listen_related_configuration
else
- assert_gem 'byebug'
+ assert_no_listen_related_configuration
+ end
+ end
+
+ def test_non_inclusion_of_listen_related_configuration_if_skip_listen
+ run_generator [destination_root, "--skip-listen"]
+ assert_no_listen_related_configuration
+ end
+
+ def test_evented_file_update_checker_config
+ run_generator
+ assert_file "config/environments/development.rb" do |content|
+ if RbConfig::CONFIG["host_os"] =~ /darwin|linux/
+ assert_match(/^\s*config\.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content)
+ else
+ assert_match(/^\s*# config\.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content)
+ end
end
end
@@ -456,13 +667,15 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
def test_usage_read_from_file
- File.expects(:read).returns("USAGE FROM FILE")
- assert_equal "USAGE FROM FILE", Rails::Generators::AppGenerator.desc
+ assert_called(File, :read, returns: "USAGE FROM FILE") do
+ assert_equal "USAGE FROM FILE", Rails::Generators::AppGenerator.desc
+ end
end
def test_default_usage
- Rails::Generators::AppGenerator.expects(:usage_path).returns(nil)
- assert_match(/Create rails files for app generator/, Rails::Generators::AppGenerator.desc)
+ assert_called(Rails::Generators::AppGenerator, :usage_path, returns: nil) do
+ assert_match(/Create rails files for app generator/, Rails::Generators::AppGenerator.desc)
+ end
end
def test_default_namespace
@@ -470,55 +683,48 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
def test_file_is_added_for_backwards_compatibility
- action :file, 'lib/test_file.rb', 'heres test data'
- assert_file 'lib/test_file.rb', 'heres test data'
- end
-
- def test_tests_are_removed_from_frameworks_if_skip_test_is_given
- run_generator [destination_root, "--skip-test"]
- assert_file "config/application.rb", /#\s+require\s+["']rails\/test_unit\/railtie["']/
+ action :file, "lib/test_file.rb", "heres test data"
+ assert_file "lib/test_file.rb", "heres test data"
end
- def test_no_active_record_or_tests_if_skips_given
- run_generator [destination_root, "--skip-test", "--skip-active-record"]
- assert_file "config/application.rb", /#\s+require\s+["']rails\/test_unit\/railtie["']/
- assert_file "config/application.rb", /#\s+require\s+["']active_record\/railtie["']/
- assert_file "config/application.rb", /\s+require\s+["']active_job\/railtie["']/
+ 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_new_hash_style
- run_generator
- assert_file "config/initializers/session_store.rb" do |file|
- assert_match(/config.session_store :cookie_store, key: '_.+_session'/, file)
- end
+ def test_quiet_option
+ output = run_generator [File.join(destination_root, "myapp"), "--quiet"]
+ assert_empty output
end
- def test_pretend_option
- output = run_generator [File.join(destination_root, "myapp"), "--pretend"]
- assert_no_match(/run bundle install/, output)
+ def test_force_option_overwrites_every_file_except_master_key
+ run_generator [File.join(destination_root, "myapp")]
+ output = run_generator [File.join(destination_root, "myapp"), "--force"]
+ assert_match(/force/, output)
+ assert_no_match("force config/master.key", output)
end
def test_application_name_with_spaces
path = File.join(destination_root, "foo bar")
# This also applies to MySQL apps but not with SQLite
- run_generator [path, "-d", 'postgresql']
+ run_generator [path, "-d", "postgresql"]
assert_file "foo bar/config/database.yml", /database: foo_bar_development/
- assert_file "foo bar/config/initializers/session_store.rb", /key: '_foo_bar/
end
def test_web_console
run_generator
- assert_gem 'web-console'
+ assert_gem "web-console"
end
def test_web_console_with_dev_option
- run_generator [destination_root, "--dev"]
+ run_generator [destination_root, "--dev", "--skip-bundle"]
assert_file "Gemfile" do |content|
assert_match(/gem 'web-console',\s+github: 'rails\/web-console'/, content)
- assert_no_match(/gem 'web-console', '~> 2.0'/, content)
+ assert_no_match(/\Agem 'web-console', '>= 3\.3\.0'\z/, content)
end
end
@@ -527,92 +733,202 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_file "Gemfile" do |content|
assert_match(/gem 'web-console',\s+github: 'rails\/web-console'/, content)
- assert_no_match(/gem 'web-console', '~> 2.0'/, content)
+ assert_no_match(/\Agem 'web-console', '>= 3\.3\.0'\z/, content)
end
end
+ def test_generation_runs_bundle_install
+ generator([destination_root], skip_webpack_install: true)
+
+ assert_bundler_command_called("install")
+ end
+
+ def test_dev_option
+ generator([destination_root], dev: true, skip_webpack_install: true)
+
+ assert_bundler_command_called("install")
+ rails_path = File.expand_path("../../..", Rails.root)
+ assert_file "Gemfile", /^gem\s+["']rails["'],\s+path:\s+["']#{Regexp.escape(rails_path)}["']$/
+ end
+
+ def test_edge_option
+ generator([destination_root], edge: true, skip_webpack_install: true)
+
+ assert_bundler_command_called("install")
+ assert_file "Gemfile", %r{^gem\s+["']rails["'],\s+github:\s+["']#{Regexp.escape("rails/rails")}["']$}
+ end
+
def test_spring
run_generator
- assert_gem 'spring'
+ assert_gem "spring"
+ end
+
+ def test_bundler_binstub
+ generator([destination_root], skip_webpack_install: true)
+
+ assert_bundler_command_called("binstubs bundler")
end
def test_spring_binstubs
jruby_skip "spring doesn't run on JRuby"
- generator.stubs(:bundle_command).with('install')
- generator.expects(:bundle_command).with('exec spring binstub --all').once
- quietly { generator.invoke_all }
+
+ generator([destination_root], skip_webpack_install: true)
+
+ assert_bundler_command_called("exec spring binstub --all")
end
def test_spring_no_fork
jruby_skip "spring doesn't run on JRuby"
- Process.stubs(:respond_to?).with(:fork).returns(false)
- run_generator
+ assert_called_with(Process, :respond_to?, [[:fork], [:fork]], returns: false) do
+ run_generator
- assert_file "Gemfile" do |content|
- assert_no_match(/spring/, content)
+ assert_no_gem "spring"
end
end
def test_skip_spring
run_generator [destination_root, "--skip-spring"]
- assert_file "Gemfile" do |content|
- assert_no_match(/spring/, content)
- end
+ assert_no_file "config/spring.rb"
+ assert_no_gem "spring"
end
def test_spring_with_dev_option
- run_generator [destination_root, "--dev"]
+ run_generator [destination_root, "--dev", "--skip-bundle"]
- assert_file "Gemfile" do |content|
- assert_no_match(/spring/, content)
+ assert_no_gem "spring"
+ end
+
+ def test_skip_javascript_option
+ command_check = -> command, *_ do
+ @called ||= 0
+ if command == "webpacker:install"
+ @called += 1
+ assert_equal 0, @called, "webpacker:install expected not to be called, but was called #{@called} times."
+ end
+ end
+
+ generator([destination_root], skip_javascript: true).stub(:rails_command, command_check) do
+ generator.stub :bundle_command, nil do
+ quietly { generator.invoke_all }
+ end
+ end
+
+ assert_no_gem "webpacker"
+ assert_file "config/initializers/content_security_policy.rb" do |content|
+ assert_no_match(/policy\.connect_src/, content)
end
end
+ def test_webpack_option_with_js_framework
+ command_check = -> command, *_ do
+ case command
+ when "webpacker:install"
+ @webpacker ||= 0
+ @webpacker += 1
+ assert_equal 1, @webpacker, "webpacker:install expected to be called once, but was called #{@webpacker} times."
+ when "webpacker:install:react"
+ @react ||= 0
+ @react += 1
+ assert_equal 1, @react, "webpacker:install:react expected to be called once, but was called #{@react} times."
+ end
+ end
+
+ generator([destination_root], webpack: "react").stub(:rails_command, command_check) do
+ generator.stub :bundle_command, nil do
+ quietly { generator.invoke_all }
+ end
+ end
+
+ assert_gem "webpacker"
+ end
+
+ def test_skip_webpack_install
+ command_check = -> command do
+ if command == "webpacker:install"
+ assert false, "webpacker:install expected not to be called."
+ end
+ end
+
+ generator([destination_root], skip_webpack_install: true).stub(:rails_command, command_check) do
+ quietly { generator.invoke_all }
+ end
+
+ assert_gem "webpacker"
+ end
+
def test_generator_if_skip_turbolinks_is_given
run_generator [destination_root, "--skip-turbolinks"]
- assert_file "Gemfile" do |content|
- assert_no_match(/turbolinks/, content)
- end
+ assert_no_gem "turbolinks"
assert_file "app/views/layouts/application.html.erb" do |content|
assert_no_match(/data-turbolinks-track/, content)
end
- assert_file "app/assets/javascripts/application.js" do |content|
+ assert_file "app/javascript/packs/application.js" do |content|
assert_no_match(/turbolinks/, content)
end
end
- def test_gitignore_when_sqlite3
- run_generator
+ def test_bootsnap
+ run_generator [destination_root, "--no-skip-bootsnap"]
- assert_file '.gitignore' do |content|
- assert_match(/sqlite3/, content)
+ unless defined?(JRUBY_VERSION)
+ assert_gem "bootsnap"
+ assert_file "config/boot.rb" do |content|
+ assert_match(/require 'bootsnap\/setup'/, content)
+ end
+ else
+ assert_no_gem "bootsnap"
+ assert_file "config/boot.rb" do |content|
+ assert_no_match(/require 'bootsnap\/setup'/, content)
+ end
end
end
- def test_gitignore_when_no_active_record
- run_generator [destination_root, '--skip-active-record']
+ def test_skip_bootsnap
+ run_generator [destination_root, "--skip-bootsnap"]
- assert_file '.gitignore' do |content|
- assert_no_match(/sqlite/i, content)
+ assert_no_gem "bootsnap"
+ assert_file "config/boot.rb" do |content|
+ assert_no_match(/require 'bootsnap\/setup'/, content)
end
end
- def test_gitignore_when_non_sqlite3_db
- run_generator([destination_root, "-d", "mysql"])
+ def test_bootsnap_with_dev_option
+ run_generator [destination_root, "--dev", "--skip-bundle"]
- assert_file '.gitignore' do |content|
- assert_no_match(/sqlite/i, content)
+ assert_no_gem "bootsnap"
+ assert_file "config/boot.rb" do |content|
+ assert_no_match(/require 'bootsnap\/setup'/, content)
end
end
+ def test_inclusion_of_ruby_version
+ run_generator
+
+ assert_file "Gemfile" do |content|
+ assert_match(/ruby '#{RUBY_VERSION}'/, content)
+ end
+ assert_file ".ruby-version" do |content|
+ if ENV["RBENV_VERSION"]
+ assert_match(/#{ENV["RBENV_VERSION"]}/, content)
+ elsif ENV["rvm_ruby_string"]
+ assert_match(/#{ENV["rvm_ruby_string"]}/, content)
+ else
+ assert_match(/#{RUBY_ENGINE}-#{RUBY_ENGINE_VERSION}/, content)
+ end
+ 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(
app/assets/images
- app/mailers
- app/models
app/controllers/concerns
app/models/concerns
lib/tasks
@@ -626,7 +942,6 @@ class AppGeneratorTest < Rails::Generators::TestCase
test/helpers
test/integration
tmp
- vendor/assets/stylesheets
)
folders_with_keep.each do |folder|
assert_file("#{folder}/.keep")
@@ -635,7 +950,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
def test_psych_gem
run_generator
- gem_regex = /gem 'psych',\s+'~> 2.0',\s+platforms: :rbx/
+ gem_regex = /gem 'psych',\s+'~> 2\.0',\s+platforms: :rbx/
assert_file "Gemfile" do |content|
if defined?(Rubinius)
@@ -647,27 +962,115 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
def test_after_bundle_callback
- path = 'http://example.org/rails_template'
- template = %{ after_bundle { run 'echo ran after_bundle' } }
+ path = "http://example.org/rails_template"
+ template = +%{ after_bundle { run 'echo ran after_bundle' } }
template.instance_eval "def read; self; end" # Make the string respond to read
- generator([destination_root], template: path).expects(:open).with(path, 'Accept' => 'application/x-thor-template').returns(template)
+ check_open = -> *args do
+ assert_equal [ path, "Accept" => "application/x-thor-template" ], args
+ template
+ end
+
+ sequence = ["git init", "install", "binstubs bundler", "exec spring binstub --all", "webpacker:install", "echo ran after_bundle"]
+ @sequence_step ||= 0
+ ensure_bundler_first = -> command, options = nil do
+ assert_equal sequence[@sequence_step], command, "commands should be called in sequence #{sequence}"
+ @sequence_step += 1
+ end
- bundler_first = sequence('bundle, binstubs, after_bundle')
- generator.expects(:bundle_command).with('install').once.in_sequence(bundler_first)
- generator.expects(:bundle_command).with('exec spring binstub --all').in_sequence(bundler_first)
- generator.expects(:run).with('echo ran after_bundle').in_sequence(bundler_first)
+ generator([destination_root], template: path).stub(:open, check_open, template) do
+ generator.stub(:bundle_command, ensure_bundler_first) do
+ generator.stub(:run, ensure_bundler_first) do
+ generator.stub(:rails_command, ensure_bundler_first) do
+ quietly { generator.invoke_all }
+ end
+ end
+ end
+ end
- quietly { generator.invoke_all }
+ assert_equal 6, @sequence_step
end
- protected
+ def test_gitignore
+ run_generator
- def action(*args, &block)
- capture(:stdout) { generator.send(*args, &block) }
+ assert_file ".gitignore" do |content|
+ assert_match(/config\/master\.key/, content)
+ end
end
- def assert_gem(gem)
- assert_file "Gemfile", /^\s*gem\s+["']#{gem}["']$*/
+ def test_system_tests_directory_generated
+ run_generator
+
+ assert_directory("test/system")
+ assert_file("test/system/.keep")
end
+
+ unless Gem.win_platform?
+ def test_master_key_is_only_readable_by_the_owner
+ run_generator
+
+ stat = File.stat("config/master.key")
+ assert_equal "100600", sprintf("%o", stat.mode)
+ end
+ end
+
+ private
+ def stub_rails_application(root)
+ Rails.application.config.root = root
+ Rails.application.class.stub(:name, "Myapp") do
+ yield
+ end
+ end
+
+ def action(*args, &block)
+ capture(:stdout) { generator.send(*args, &block) }
+ end
+
+ def assert_gem(gem, constraint = nil)
+ if constraint
+ assert_file "Gemfile", /^\s*gem\s+["']#{gem}["'], #{constraint}$*/
+ else
+ assert_file "Gemfile", /^\s*gem\s+["']#{gem}["']$*/
+ end
+ end
+
+ def assert_no_gem(gem)
+ assert_file "Gemfile" do |content|
+ assert_no_match(gem, content)
+ end
+ end
+
+ def assert_listen_related_configuration
+ assert_gem "listen"
+ assert_gem "spring-watcher-listen"
+
+ assert_file "config/environments/development.rb" do |content|
+ assert_match(/^\s*config\.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content)
+ end
+ end
+
+ def assert_no_listen_related_configuration
+ assert_no_gem "listen"
+
+ assert_file "config/environments/development.rb" do |content|
+ assert_match(/^\s*# config\.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content)
+ end
+ end
+
+ def assert_bundler_command_called(target_command)
+ command_check = -> command do
+ @command_called ||= 0
+
+ case command
+ when target_command
+ @command_called += 1
+ assert_equal 1, @command_called, "#{command} expected to be called once, but was called #{@command_called} times."
+ end
+ end
+
+ generator.stub :bundle_command, command_check do
+ quietly { generator.invoke_all }
+ end
+ end
end
diff --git a/railties/test/generators/application_record_generator_test.rb b/railties/test/generators/application_record_generator_test.rb
new file mode 100644
index 0000000000..2c0aa7211b
--- /dev/null
+++ b/railties/test/generators/application_record_generator_test.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require "generators/generators_test_helper"
+require "rails/generators/rails/application_record/application_record_generator"
+
+class ApplicationRecordGeneratorTest < Rails::Generators::TestCase
+ include GeneratorsTestHelper
+
+ def test_application_record_skeleton_is_created
+ run_generator
+ assert_file "app/models/application_record.rb" do |record|
+ assert_match(/class ApplicationRecord < ActiveRecord::Base/, record)
+ assert_match(/self\.abstract_class = true/, record)
+ end
+ end
+end
diff --git a/railties/test/generators/argv_scrubber_test.rb b/railties/test/generators/argv_scrubber_test.rb
index 31e07bc8da..9ef61dc978 100644
--- a/railties/test/generators/argv_scrubber_test.rb
+++ b/railties/test/generators/argv_scrubber_test.rb
@@ -1,7 +1,9 @@
-require 'active_support/test_case'
-require 'active_support/testing/autorun'
-require 'rails/generators/rails/app/app_generator'
-require 'tempfile'
+# frozen_string_literal: true
+
+require "active_support/test_case"
+require "active_support/testing/autorun"
+require "rails/generators/rails/app/app_generator"
+require "tempfile"
module Rails
module Generators
@@ -11,7 +13,7 @@ module Rails
# *must* act this way, I just want to prevent regressions.
def test_version
- ['-v', '--version'].each do |str|
+ ["-v", "--version"].each do |str|
scrubber = ARGVScrubber.new [str]
output = nil
exit_code = nil
@@ -26,37 +28,37 @@ module Rails
end
def test_default_help
- argv = ['zomg', 'how', 'are', 'you']
+ argv = ["zomg", "how", "are", "you"]
scrubber = ARGVScrubber.new argv
args = scrubber.prepare!
- assert_equal ['--help'] + argv.drop(1), args
+ assert_equal ["--help"] + argv.drop(1), args
end
def test_prepare_returns_args
- scrubber = ARGVScrubber.new ['hi mom']
+ scrubber = ARGVScrubber.new ["hi mom"]
args = scrubber.prepare!
- assert_equal '--help', args.first
+ assert_equal "--help", args.first
end
def test_no_mutations
- scrubber = ARGVScrubber.new ['hi mom'].freeze
+ scrubber = ARGVScrubber.new ["hi mom"].freeze
args = scrubber.prepare!
- assert_equal '--help', args.first
+ assert_equal "--help", args.first
end
def test_new_command_no_rc
scrubber = Class.new(ARGVScrubber) {
def self.default_rc_file
- File.join(Dir.tmpdir, 'whatever')
+ File.join(Dir.tmpdir, "whatever")
end
- }.new ['new']
+ }.new ["new"]
args = scrubber.prepare!
assert_equal [], args
end
def test_new_homedir_rc
- file = Tempfile.new 'myrcfile'
- file.puts '--hello-world'
+ file = Tempfile.new "myrcfile"
+ file.puts "--hello-world"
file.flush
message = nil
@@ -65,10 +67,10 @@ module Rails
file.path
end
define_method(:puts) { |msg| message = msg }
- }.new ['new']
+ }.new ["new"]
args = scrubber.prepare!
- assert_equal ['--hello-world'], args
- assert_match 'hello-world', message
+ assert_equal ["--hello-world"], args
+ assert_match "hello-world", message
assert_match file.path, message
ensure
file.close
@@ -76,33 +78,32 @@ module Rails
end
def test_rc_whitespace_separated
- file = Tempfile.new 'myrcfile'
- file.puts '--hello --world'
+ file = Tempfile.new "myrcfile"
+ file.puts "--hello --world"
file.flush
- message = nil
scrubber = Class.new(ARGVScrubber) {
- define_method(:puts) { |msg| message = msg }
- }.new ['new', "--rc=#{file.path}"]
+ define_method(:puts) { |msg| }
+ }.new ["new", "--rc=#{file.path}"]
args = scrubber.prepare!
- assert_equal ['--hello', '--world'], args
+ assert_equal ["--hello", "--world"], args
ensure
file.close
file.unlink
end
def test_new_rc_option
- file = Tempfile.new 'myrcfile'
- file.puts '--hello-world'
+ file = Tempfile.new "myrcfile"
+ file.puts "--hello-world"
file.flush
message = nil
scrubber = Class.new(ARGVScrubber) {
define_method(:puts) { |msg| message = msg }
- }.new ['new', "--rc=#{file.path}"]
+ }.new ["new", "--rc=#{file.path}"]
args = scrubber.prepare!
- assert_equal ['--hello-world'], args
- assert_match 'hello-world', message
+ assert_equal ["--hello-world"], args
+ assert_match "hello-world", message
assert_match file.path, message
ensure
file.close
@@ -110,14 +111,14 @@ module Rails
end
def test_new_rc_option_and_custom_options
- file = Tempfile.new 'myrcfile'
- file.puts '--hello'
- file.puts '--world'
+ file = Tempfile.new "myrcfile"
+ file.puts "--hello"
+ file.puts "--world"
file.flush
scrubber = Class.new(ARGVScrubber) {
define_method(:puts) { |msg| }
- }.new ['new', 'tenderapp', '--love', "--rc=#{file.path}"]
+ }.new ["new", "tenderapp", "--love", "--rc=#{file.path}"]
args = scrubber.prepare!
assert_equal ["tenderapp", "--hello", "--world", "--love"], args
@@ -127,7 +128,7 @@ module Rails
end
def test_no_rc
- scrubber = ARGVScrubber.new ['new', '--no-rc']
+ scrubber = ARGVScrubber.new ["new", "--no-rc"]
args = scrubber.prepare!
assert_equal [], args
end
diff --git a/railties/test/generators/assets_generator_test.rb b/railties/test/generators/assets_generator_test.rb
index a2b94f2e50..83d2429acf 100644
--- a/railties/test/generators/assets_generator_test.rb
+++ b/railties/test/generators/assets_generator_test.rb
@@ -1,5 +1,7 @@
-require 'generators/generators_test_helper'
-require 'rails/generators/rails/assets/assets_generator'
+# frozen_string_literal: true
+
+require "generators/generators_test_helper"
+require "rails/generators/rails/assets/assets_generator"
class AssetsGeneratorTest < Rails::Generators::TestCase
include GeneratorsTestHelper
@@ -7,13 +9,11 @@ class AssetsGeneratorTest < Rails::Generators::TestCase
def test_assets
run_generator
- assert_file "app/assets/javascripts/posts.js"
assert_file "app/assets/stylesheets/posts.css"
end
def test_skipping_assets
- run_generator ["posts", "--no-stylesheets", "--no-javascripts"]
- assert_no_file "app/assets/javascripts/posts.js"
+ run_generator ["posts", "--no-stylesheets"]
assert_no_file "app/assets/stylesheets/posts.css"
end
end
diff --git a/railties/test/generators/channel_generator_test.rb b/railties/test/generators/channel_generator_test.rb
new file mode 100644
index 0000000000..1cb8465539
--- /dev/null
+++ b/railties/test/generators/channel_generator_test.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require "generators/generators_test_helper"
+require "rails/generators/channel/channel_generator"
+
+class ChannelGeneratorTest < Rails::Generators::TestCase
+ include GeneratorsTestHelper
+ tests Rails::Generators::ChannelGenerator
+
+ def test_application_cable_skeleton_is_created
+ run_generator ["books"]
+
+ assert_file "app/channels/application_cable/channel.rb" do |cable|
+ assert_match(/module ApplicationCable\n class Channel < ActionCable::Channel::Base\n/, cable)
+ end
+
+ assert_file "app/channels/application_cable/connection.rb" do |cable|
+ assert_match(/module ApplicationCable\n class Connection < ActionCable::Connection::Base\n/, cable)
+ end
+ end
+
+ def test_channel_is_created
+ run_generator ["chat"]
+
+ assert_file "app/channels/chat_channel.rb" do |channel|
+ assert_match(/class ChatChannel < ApplicationCable::Channel/, channel)
+ end
+
+ assert_file "app/javascript/channels/chat_channel.js" do |channel|
+ assert_match(/import consumer from "\.\/consumer"\s+consumer\.subscriptions\.create\("ChatChannel/, channel)
+ end
+ end
+
+ def test_channel_with_multiple_actions_is_created
+ run_generator ["chat", "speak", "mute"]
+
+ assert_file "app/channels/chat_channel.rb" do |channel|
+ assert_match(/class ChatChannel < ApplicationCable::Channel/, channel)
+ assert_match(/def speak/, channel)
+ assert_match(/def mute/, channel)
+ end
+
+ assert_file "app/javascript/channels/chat_channel.js" do |channel|
+ assert_match(/import consumer from "\.\/consumer"\s+consumer\.subscriptions\.create\("ChatChannel/, channel)
+ assert_match(/,\n\n speak/, channel)
+ assert_match(/,\n\n mute: function\(\) \{\n return this\.perform\('mute'\);\n \}\n\}\);/, channel)
+ end
+ end
+
+ def test_channel_asset_is_not_created_when_skip_assets_is_passed
+ run_generator ["chat", "--skip-assets"]
+
+ assert_file "app/channels/chat_channel.rb" do |channel|
+ assert_match(/class ChatChannel < ApplicationCable::Channel/, channel)
+ end
+
+ assert_no_file "app/javascript/channels/chat_channel.js"
+ end
+
+ def test_consumer_js_is_created_if_not_present_already
+ run_generator ["chat"]
+ FileUtils.rm("#{destination_root}/app/javascript/channels/index.js")
+ FileUtils.rm("#{destination_root}/app/javascript/channels/consumer.js")
+ run_generator ["camp"]
+
+ assert_file "app/javascript/channels/index.js"
+ assert_file "app/javascript/channels/consumer.js"
+ end
+
+ def test_channel_on_revoke
+ run_generator ["chat"]
+ run_generator ["chat"], behavior: :revoke
+
+ assert_no_file "app/channels/chat_channel.rb"
+ assert_no_file "app/javascript/channels/chat_channel.js"
+
+ assert_file "app/channels/application_cable/channel.rb"
+ assert_file "app/channels/application_cable/connection.rb"
+ assert_file "app/javascript/channels/index.js"
+ assert_file "app/javascript/channels/consumer.js"
+ end
+
+ def test_channel_suffix_is_not_duplicated
+ run_generator ["chat_channel"]
+
+ assert_no_file "app/channels/chat_channel_channel.rb"
+ assert_file "app/channels/chat_channel.rb"
+
+ assert_no_file "app/javascript/channels/chat_channel_channel.js"
+ assert_file "app/javascript/channels/chat_channel.js"
+ end
+end
diff --git a/railties/test/generators/controller_generator_test.rb b/railties/test/generators/controller_generator_test.rb
index 1351151afb..8786756c68 100644
--- a/railties/test/generators/controller_generator_test.rb
+++ b/railties/test/generators/controller_generator_test.rb
@@ -1,5 +1,7 @@
-require 'generators/generators_test_helper'
-require 'rails/generators/rails/controller/controller_generator'
+# frozen_string_literal: true
+
+require "generators/generators_test_helper"
+require "rails/generators/rails/controller/controller_generator"
class ControllerGeneratorTest < Rails::Generators::TestCase
include GeneratorsTestHelper
@@ -19,7 +21,7 @@ class ControllerGeneratorTest < Rails::Generators::TestCase
def test_check_class_collision
Object.send :const_set, :ObjectController, Class.new
- content = capture(:stderr){ run_generator ["object"] }
+ content = capture(:stderr) { run_generator ["object"] }
assert_match(/The name 'ObjectController' is either already used in your application or reserved/, content)
ensure
Object.send :remove_const, :ObjectController
@@ -37,13 +39,11 @@ class ControllerGeneratorTest < Rails::Generators::TestCase
def test_invokes_assets
run_generator
- assert_file "app/assets/javascripts/account.js"
assert_file "app/assets/stylesheets/account.css"
end
def test_does_not_invoke_assets_if_required
run_generator ["account", "--skip-assets"]
- assert_no_file "app/assets/javascripts/account.js"
assert_no_file "app/assets/stylesheets/account.css"
end
@@ -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
@@ -100,4 +100,37 @@ class ControllerGeneratorTest < Rails::Generators::TestCase
assert_match(/^ namespace :admin do\n get 'dashboard\/index'\n end$/, route)
end
end
+
+ def test_namespaced_routes_with_multiple_actions_are_created_in_routes
+ run_generator ["admin/dashboard", "index", "show"]
+ assert_file "config/routes.rb" do |route|
+ assert_match(/^ namespace :admin do\n get 'dashboard\/index'\n get 'dashboard\/show'\n end$/, route)
+ end
+ end
+
+ def test_does_not_add_routes_when_action_is_not_specified
+ run_generator ["admin/dashboard"]
+ assert_file "config/routes.rb" do |routes|
+ assert_no_match(/namespace :admin/, routes)
+ end
+ end
+
+ def test_controller_suffix_is_not_duplicated
+ run_generator ["account_controller"]
+
+ assert_no_file "app/controllers/account_controller_controller.rb"
+ assert_file "app/controllers/account_controller.rb"
+
+ assert_no_file "app/views/account_controller/"
+ assert_file "app/views/account/"
+
+ assert_no_file "test/controllers/account_controller_controller_test.rb"
+ assert_file "test/controllers/account_controller_test.rb"
+
+ assert_no_file "app/helpers/account_controller_helper.rb"
+ assert_file "app/helpers/account_helper.rb"
+
+ assert_no_file "app/assets/stylesheets/account_controller.css"
+ assert_file "app/assets/stylesheets/account.css"
+ end
end
diff --git a/railties/test/generators/create_migration_test.rb b/railties/test/generators/create_migration_test.rb
index e16a77479a..2ae38045c5 100644
--- a/railties/test/generators/create_migration_test.rb
+++ b/railties/test/generators/create_migration_test.rb
@@ -1,5 +1,7 @@
-require 'generators/generators_test_helper'
-require 'rails/generators/rails/migration/migration_generator'
+# frozen_string_literal: true
+
+require "generators/generators_test_helper"
+require "rails/generators/rails/migration/migration_generator"
class CreateMigrationTest < Rails::Generators::TestCase
include GeneratorsTestHelper
@@ -19,12 +21,12 @@ class CreateMigrationTest < Rails::Generators::TestCase
end
def create_migration(destination_path = default_destination_path, config = {}, generator_options = {}, &block)
- migration_name = File.basename(destination_path, '.rb')
+ migration_name = File.basename(destination_path, ".rb")
generator([migration_name], generator_options)
generator.set_migration_assigns!(destination_path)
dir, base = File.split(destination_path)
- timestamped_destination_path = File.join(dir, ["%migration_number%", base].join('_'))
+ timestamped_destination_path = File.join(dir, ["%migration_number%", base].join("_"))
@migration = Rails::Generators::Actions::CreateMigration.new(generator, timestamped_destination_path, block || "contents", config)
end
@@ -46,7 +48,7 @@ class CreateMigrationTest < Rails::Generators::TestCase
def test_invoke
create_migration
- assert_match(/create db\/migrate\/1_create_articles.rb\n/, invoke!)
+ assert_match(/create db\/migrate\/1_create_articles\.rb\n/, invoke!)
assert_file @migration.destination
end
@@ -67,8 +69,8 @@ class CreateMigrationTest < Rails::Generators::TestCase
migration_exists!
create_migration
- assert_match(/identical db\/migrate\/1_create_articles.rb\n/, invoke!)
- assert @migration.identical?
+ assert_match(/identical db\/migrate\/1_create_articles\.rb\n/, invoke!)
+ assert_predicate @migration, :identical?
end
def test_invoke_when_exists_not_identical
@@ -84,8 +86,8 @@ class CreateMigrationTest < Rails::Generators::TestCase
create_migration(dest, force: true) { "different content" }
stdout = invoke!
- assert_match(/remove db\/migrate\/1_migration.rb\n/, stdout)
- assert_match(/create db\/migrate\/2_migration.rb\n/, stdout)
+ assert_match(/remove db\/migrate\/1_migration\.rb\n/, stdout)
+ assert_match(/create db\/migrate\/2_migration\.rb\n/, stdout)
assert_file @migration.destination
assert_no_file @existing_migration.destination
end
@@ -97,8 +99,8 @@ class CreateMigrationTest < Rails::Generators::TestCase
end
stdout = invoke!
- assert_match(/remove db\/migrate\/1_create_articles.rb\n/, stdout)
- assert_match(/create db\/migrate\/2_create_articles.rb\n/, stdout)
+ assert_match(/remove db\/migrate\/1_create_articles\.rb\n/, stdout)
+ assert_match(/create db\/migrate\/2_create_articles\.rb\n/, stdout)
assert_no_file @migration.destination
end
@@ -106,7 +108,7 @@ class CreateMigrationTest < Rails::Generators::TestCase
migration_exists!
create_migration(default_destination_path, {}, { skip: true }) { "different content" }
- assert_match(/skip db\/migrate\/2_create_articles.rb\n/, invoke!)
+ assert_match(/skip db\/migrate\/2_create_articles\.rb\n/, invoke!)
assert_no_file @migration.destination
end
@@ -114,7 +116,7 @@ class CreateMigrationTest < Rails::Generators::TestCase
migration_exists!
create_migration
- assert_match(/remove db\/migrate\/1_create_articles.rb\n/, revoke!)
+ assert_match(/remove db\/migrate\/1_create_articles\.rb\n/, revoke!)
assert_no_file @existing_migration.destination
end
@@ -122,13 +124,13 @@ class CreateMigrationTest < Rails::Generators::TestCase
migration_exists!
create_migration(default_destination_path, {}, { pretend: true })
- assert_match(/remove db\/migrate\/1_create_articles.rb\n/, revoke!)
+ assert_match(/remove db\/migrate\/1_create_articles\.rb\n/, revoke!)
assert_file @existing_migration.destination
end
def test_revoke_when_no_exists
create_migration
- assert_match(/remove db\/migrate\/1_create_articles.rb\n/, revoke!)
+ assert_match(/remove db\/migrate\/1_create_articles\.rb\n/, revoke!)
end
end
diff --git a/railties/test/generators/generated_attribute_test.rb b/railties/test/generators/generated_attribute_test.rb
index ee7c009305..772b4f6f0d 100644
--- a/railties/test/generators/generated_attribute_test.rb
+++ b/railties/test/generators/generated_attribute_test.rb
@@ -1,5 +1,7 @@
-require 'generators/generators_test_helper'
-require 'rails/generators/generated_attribute'
+# frozen_string_literal: true
+
+require "generators/generators_test_helper"
+require "rails/generators/generated_attribute"
class GeneratedAttributeTest < Rails::Generators::TestCase
include GeneratorsTestHelper
@@ -51,7 +53,7 @@ class GeneratedAttributeTest < Rails::Generators::TestCase
end
def test_default_value_is_decimal
- assert_field_default_value :decimal, '9.99'
+ assert_field_default_value :decimal, "9.99"
end
def test_default_value_is_datetime
@@ -65,7 +67,7 @@ class GeneratedAttributeTest < Rails::Generators::TestCase
end
def test_default_value_is_string
- assert_field_default_value :string, 'MyString'
+ assert_field_default_value :string, "MyString"
end
def test_default_value_for_type
@@ -74,7 +76,7 @@ class GeneratedAttributeTest < Rails::Generators::TestCase
end
def test_default_value_is_text
- assert_field_default_value :text, 'MyText'
+ assert_field_default_value :text, "MyText"
end
def test_default_value_is_boolean
@@ -89,64 +91,64 @@ class GeneratedAttributeTest < Rails::Generators::TestCase
def test_default_value_is_empty_string
%w(foo bar baz).each do |attribute_type|
- assert_field_default_value attribute_type, ''
+ assert_field_default_value attribute_type, ""
end
end
def test_human_name
assert_equal(
- 'Full name',
- create_generated_attribute(:string, 'full_name').human_name
+ "Full name",
+ create_generated_attribute(:string, "full_name").human_name
)
end
def test_reference_is_true
%w(references belongs_to).each do |attribute_type|
- assert create_generated_attribute(attribute_type).reference?
+ assert_predicate create_generated_attribute(attribute_type), :reference?
end
end
def test_reference_is_false
%w(foo bar baz).each do |attribute_type|
- assert !create_generated_attribute(attribute_type).reference?
+ assert_not_predicate create_generated_attribute(attribute_type), :reference?
end
end
def test_polymorphic_reference_is_true
%w(references belongs_to).each do |attribute_type|
- assert create_generated_attribute("#{attribute_type}{polymorphic}").polymorphic?
+ assert_predicate create_generated_attribute("#{attribute_type}{polymorphic}"), :polymorphic?
end
end
def test_polymorphic_reference_is_false
%w(foo bar baz).each do |attribute_type|
- assert !create_generated_attribute("#{attribute_type}{polymorphic}").polymorphic?
+ assert_not_predicate create_generated_attribute("#{attribute_type}{polymorphic}"), :polymorphic?
end
end
def test_blank_type_defaults_to_string_raises_exception
- assert_equal :string, create_generated_attribute(nil, 'title').type
- assert_equal :string, create_generated_attribute("", 'title').type
+ assert_equal :string, create_generated_attribute(nil, "title").type
+ assert_equal :string, create_generated_attribute("", "title").type
end
def test_handles_index_names_for_references
- assert_equal "post", create_generated_attribute('string', 'post').index_name
- assert_equal "post_id", create_generated_attribute('references', 'post').index_name
- assert_equal "post_id", create_generated_attribute('belongs_to', 'post').index_name
- assert_equal ["post_id", "post_type"], create_generated_attribute('references{polymorphic}', 'post').index_name
+ assert_equal "post", create_generated_attribute("string", "post").index_name
+ assert_equal "post_id", create_generated_attribute("references", "post").index_name
+ assert_equal "post_id", create_generated_attribute("belongs_to", "post").index_name
+ assert_equal ["post_id", "post_type"], create_generated_attribute("references{polymorphic}", "post").index_name
end
def test_handles_column_names_for_references
- assert_equal "post", create_generated_attribute('string', 'post').column_name
- assert_equal "post_id", create_generated_attribute('references', 'post').column_name
- assert_equal "post_id", create_generated_attribute('belongs_to', 'post').column_name
+ assert_equal "post", create_generated_attribute("string", "post").column_name
+ assert_equal "post_id", create_generated_attribute("references", "post").column_name
+ assert_equal "post_id", create_generated_attribute("belongs_to", "post").column_name
end
def test_parse_required_attribute_with_index
att = Rails::Generators::GeneratedAttribute.parse("supplier:references{required}:index")
assert_equal "supplier", att.name
assert_equal :references, att.type
- assert att.has_index?
- assert att.required?
+ assert_predicate att, :has_index?
+ assert_predicate att, :required?
end
end
diff --git a/railties/test/generators/generator_generator_test.rb b/railties/test/generators/generator_generator_test.rb
index dcfeaaa8e0..eaa964cabc 100644
--- a/railties/test/generators/generator_generator_test.rb
+++ b/railties/test/generators/generator_generator_test.rb
@@ -1,5 +1,7 @@
-require 'generators/generators_test_helper'
-require 'rails/generators/rails/generator/generator_generator'
+# frozen_string_literal: true
+
+require "generators/generators_test_helper"
+require "rails/generators/rails/generator/generator_generator"
class GeneratorGeneratorTest < Rails::Generators::TestCase
include GeneratorsTestHelper
@@ -12,7 +14,7 @@ class GeneratorGeneratorTest < Rails::Generators::TestCase
lib/generators/awesome
lib/generators/awesome/USAGE
lib/generators/awesome/templates
- ).each{ |path| assert_file path }
+ ).each { |path| assert_file path }
assert_file "lib/generators/awesome/awesome_generator.rb",
/class AwesomeGenerator < Rails::Generators::NamedBase/
@@ -28,7 +30,7 @@ class GeneratorGeneratorTest < Rails::Generators::TestCase
lib/generators/rails/awesome
lib/generators/rails/awesome/USAGE
lib/generators/rails/awesome/templates
- ).each{ |path| assert_file path }
+ ).each { |path| assert_file path }
assert_file "lib/generators/rails/awesome/awesome_generator.rb",
/class Rails::AwesomeGenerator < Rails::Generators::NamedBase/
@@ -44,7 +46,7 @@ class GeneratorGeneratorTest < Rails::Generators::TestCase
lib/generators/
lib/generators/USAGE
lib/generators/templates
- ).each{ |path| assert_file path }
+ ).each { |path| assert_file path }
assert_file "lib/generators/awesome_generator.rb",
/class AwesomeGenerator < Rails::Generators::NamedBase/
@@ -60,7 +62,7 @@ class GeneratorGeneratorTest < Rails::Generators::TestCase
lib/generators/rails
lib/generators/rails/USAGE
lib/generators/rails/templates
- ).each{ |path| assert_file path }
+ ).each { |path| assert_file path }
assert_file "lib/generators/rails/awesome_generator.rb",
/class Rails::AwesomeGenerator < Rails::Generators::NamedBase/
diff --git a/railties/test/generators/generator_test.rb b/railties/test/generators/generator_test.rb
index 7871399dd7..5f7daf5ac3 100644
--- a/railties/test/generators/generator_test.rb
+++ b/railties/test/generators/generator_test.rb
@@ -1,6 +1,8 @@
-require 'active_support/test_case'
-require 'active_support/testing/autorun'
-require 'rails/generators/app_base'
+# frozen_string_literal: true
+
+require "active_support/test_case"
+require "active_support/testing/autorun"
+require "rails/generators/app_base"
module Rails
module Generators
@@ -20,47 +22,47 @@ module Rails
end
def test_construction
- klass = make_builder_class
- assert klass.start(['new', 'blah'])
+ klass = make_builder_class
+ assert klass.start(["new", "blah"])
end
def test_add_gem
klass = make_builder_class
- generator = klass.start(['new', 'blah'])
- generator.gemfile_entry 'tenderlove'
- assert_includes generator.gemfile_entries.map(&:name), 'tenderlove'
+ generator = klass.start(["new", "blah"])
+ generator.gemfile_entry "tenderlove"
+ assert_includes generator.gemfile_entries.map(&:name), "tenderlove"
end
def test_add_gem_with_version
klass = make_builder_class
- generator = klass.start(['new', 'blah'])
- generator.gemfile_entry 'tenderlove', '2.0.0'
+ generator = klass.start(["new", "blah"])
+ generator.gemfile_entry "tenderlove", "2.0.0"
assert generator.gemfile_entries.find { |gfe|
- gfe.name == 'tenderlove' && gfe.version == '2.0.0'
+ gfe.name == "tenderlove" && gfe.version == "2.0.0"
}
end
def test_add_github_gem
klass = make_builder_class
- generator = klass.start(['new', 'blah'])
- generator.gemfile_entry 'tenderlove', github: 'hello world'
+ generator = klass.start(["new", "blah"])
+ generator.gemfile_entry "tenderlove", github: "hello world"
assert generator.gemfile_entries.find { |gfe|
- gfe.name == 'tenderlove' && gfe.options[:github] == 'hello world'
+ gfe.name == "tenderlove" && gfe.options[:github] == "hello world"
}
end
def test_add_path_gem
klass = make_builder_class
- generator = klass.start(['new', 'blah'])
- generator.gemfile_entry 'tenderlove', path: 'hello world'
+ generator = klass.start(["new", "blah"])
+ generator.gemfile_entry "tenderlove", path: "hello world"
assert generator.gemfile_entries.find { |gfe|
- gfe.name == 'tenderlove' && gfe.options[:path] == 'hello world'
+ gfe.name == "tenderlove" && gfe.options[:path] == "hello world"
}
end
def test_filter
klass = make_builder_class
- generator = klass.start(['new', 'blah'])
+ generator = klass.start(["new", "blah"])
gems = generator.gemfile_entries
generator.add_gem_entry_filter { |gem|
gem.name != gems.first.name
@@ -70,7 +72,7 @@ module Rails
def test_two_filters
klass = make_builder_class
- generator = klass.start(['new', 'blah'])
+ generator = klass.start(["new", "blah"])
gems = generator.gemfile_entries
generator.add_gem_entry_filter { |gem|
gem.name != gems.first.name
@@ -80,6 +82,21 @@ module Rails
}
assert_equal gems.drop(2), generator.gemfile_entries
end
+
+ def test_recommended_rails_versions
+ klass = make_builder_class
+ generator = klass.start(["new", "blah"])
+
+ 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", 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", ">= 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
end
diff --git a/railties/test/generators/generators_test_helper.rb b/railties/test/generators/generators_test_helper.rb
index 62ca0ecb4b..25d5dba1d8 100644
--- a/railties/test/generators/generators_test_helper.rb
+++ b/railties/test/generators/generators_test_helper.rb
@@ -1,14 +1,17 @@
-require 'abstract_unit'
-require 'active_support/core_ext/module/remove_method'
-require 'active_support/testing/stream'
-require 'rails/generators'
-require 'rails/generators/test_case'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/module/remove_method"
+require "active_support/testing/stream"
+require "active_support/testing/method_call_assertions"
+require "rails/generators"
+require "rails/generators/test_case"
module Rails
class << self
remove_possible_method :root
def root
- @root ||= Pathname.new(File.expand_path('../../fixtures', __FILE__))
+ @root ||= Pathname.new(File.expand_path("../fixtures", __dir__))
end
end
end
@@ -19,12 +22,13 @@ Rails.application.config.generators.templates = [File.join(Rails.root, "lib", "t
# Rails.application.config.generators to Rails::Generators
Rails.application.load_generators
-require 'active_record'
-require 'action_dispatch'
-require 'action_view'
+require "active_record"
+require "action_dispatch"
+require "action_view"
module GeneratorsTestHelper
include ActiveSupport::Testing::Stream
+ include ActiveSupport::Testing::MethodCallAssertions
def self.included(base)
base.class_eval do
@@ -32,17 +36,31 @@ module GeneratorsTestHelper
setup :prepare_destination
begin
- base.tests Rails::Generators.const_get(base.name.sub(/Test$/, ''))
+ base.tests Rails::Generators.const_get(base.name.sub(/Test$/, ""))
rescue
end
end
end
+ def with_secondary_database_configuration
+ original_configurations = ActiveRecord::Base.configurations
+ ActiveRecord::Base.configurations = {
+ test: {
+ secondary: {
+ database: "db/secondary.sqlite3",
+ migrations_paths: "db/secondary_migrate",
+ },
+ },
+ }
+ yield
+ ensure
+ ActiveRecord::Base.configurations = original_configurations
+ end
+
def copy_routes
- routes = File.expand_path("../../../lib/rails/generators/rails/app/templates/config/routes.rb", __FILE__)
+ routes = File.expand_path("../../lib/rails/generators/rails/app/templates/config/routes.rb.tt", __dir__)
destination = File.join(destination_root, "config")
FileUtils.mkdir_p(destination)
- FileUtils.cp routes, destination
+ FileUtils.cp routes, File.join(destination, "routes.rb")
end
-
end
diff --git a/railties/test/generators/helper_generator_test.rb b/railties/test/generators/helper_generator_test.rb
index add04f21a4..4cdb6adf82 100644
--- a/railties/test/generators/helper_generator_test.rb
+++ b/railties/test/generators/helper_generator_test.rb
@@ -1,5 +1,7 @@
-require 'generators/generators_test_helper'
-require 'rails/generators/rails/helper/helper_generator'
+# frozen_string_literal: true
+
+require "generators/generators_test_helper"
+require "rails/generators/rails/helper/helper_generator"
ObjectHelper = Class.new
AnotherObjectHelperTest = Class.new
@@ -14,7 +16,7 @@ class HelperGeneratorTest < Rails::Generators::TestCase
end
def test_check_class_collision
- content = capture(:stderr){ run_generator ["object"] }
+ content = capture(:stderr) { run_generator ["object"] }
assert_match(/The name 'ObjectHelper' is either already used in your application or reserved/, content)
end
diff --git a/railties/test/generators/integration_test_generator_test.rb b/railties/test/generators/integration_test_generator_test.rb
index d05ed76d24..82791f1a27 100644
--- a/railties/test/generators/integration_test_generator_test.rb
+++ b/railties/test/generators/integration_test_generator_test.rb
@@ -1,12 +1,18 @@
-require 'generators/generators_test_helper'
-require 'rails/generators/rails/integration_test/integration_test_generator'
+# frozen_string_literal: true
+
+require "generators/generators_test_helper"
+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/job_generator_test.rb b/railties/test/generators/job_generator_test.rb
index 7fd8f2062f..234ba6dad7 100644
--- a/railties/test/generators/job_generator_test.rb
+++ b/railties/test/generators/job_generator_test.rb
@@ -1,5 +1,7 @@
-require 'generators/generators_test_helper'
-require 'rails/generators/job/job_generator'
+# frozen_string_literal: true
+
+require "generators/generators_test_helper"
+require "rails/generators/job/job_generator"
class JobGeneratorTest < Rails::Generators::TestCase
include GeneratorsTestHelper
@@ -26,4 +28,21 @@ class JobGeneratorTest < Rails::Generators::TestCase
assert_match(/queue_as :admin/, job)
end
end
+
+ def test_application_job_skeleton_is_created
+ run_generator ["refresh_counters"]
+ assert_file "app/jobs/application_job.rb" do |job|
+ assert_match(/class ApplicationJob < ActiveJob::Base/, job)
+ end
+ end
+
+ def test_job_suffix_is_not_duplicated
+ run_generator ["notifier_job"]
+
+ assert_no_file "app/jobs/notifier_job_job.rb"
+ assert_file "app/jobs/notifier_job.rb"
+
+ assert_no_file "test/jobs/notifier_job_job_test.rb"
+ assert_file "test/jobs/notifier_job_test.rb"
+ end
end
diff --git a/railties/test/generators/mailer_generator_test.rb b/railties/test/generators/mailer_generator_test.rb
index f01e8cd2d9..ddac6e1a1e 100644
--- a/railties/test/generators/mailer_generator_test.rb
+++ b/railties/test/generators/mailer_generator_test.rb
@@ -1,5 +1,7 @@
-require 'generators/generators_test_helper'
-require 'rails/generators/mailer/mailer_generator'
+# frozen_string_literal: true
+
+require "generators/generators_test_helper"
+require "rails/generators/mailer/mailer_generator"
class MailerGeneratorTest < Rails::Generators::TestCase
include GeneratorsTestHelper
@@ -9,16 +11,13 @@ class MailerGeneratorTest < Rails::Generators::TestCase
run_generator
assert_file "app/mailers/notifier_mailer.rb" do |mailer|
assert_match(/class NotifierMailer < ApplicationMailer/, mailer)
- assert_no_match(/default from: "from@example.com"/, mailer)
+ assert_no_match(/default from: "from@example\.com"/, mailer)
assert_no_match(/layout :mailer_notifier/, mailer)
end
- end
- def test_application_mailer_skeleton_is_created
- run_generator
assert_file "app/mailers/application_mailer.rb" do |mailer|
assert_match(/class ApplicationMailer < ActionMailer::Base/, mailer)
- assert_match(/default from: "from@example.com"/, mailer)
+ assert_match(/default from: 'from@example\.com'/, mailer)
assert_match(/layout 'mailer'/, mailer)
end
end
@@ -33,7 +32,7 @@ class MailerGeneratorTest < Rails::Generators::TestCase
def test_check_class_collision
Object.send :const_set, :NotifierMailer, Class.new
- content = capture(:stderr){ run_generator }
+ content = capture(:stderr) { run_generator }
assert_match(/The name 'NotifierMailer' is either already used in your application or reserved/, content)
ensure
Object.send :remove_const, :NotifierMailer
@@ -51,18 +50,18 @@ class MailerGeneratorTest < Rails::Generators::TestCase
assert_match(/class NotifierMailerPreview < ActionMailer::Preview/, preview)
assert_match(/\# Preview this email at http:\/\/localhost\:3000\/rails\/mailers\/notifier_mailer\/foo/, preview)
assert_instance_method :foo, preview do |foo|
- assert_match(/NotifierMailer.foo/, foo)
+ assert_match(/NotifierMailer\.foo/, foo)
end
assert_match(/\# Preview this email at http:\/\/localhost\:3000\/rails\/mailers\/notifier_mailer\/bar/, preview)
assert_instance_method :bar, preview do |bar|
- assert_match(/NotifierMailer.bar/, bar)
+ assert_match(/NotifierMailer\.bar/, bar)
end
end
end
def test_check_test_class_collision
Object.send :const_set, :NotifierMailerTest, Class.new
- content = capture(:stderr){ run_generator }
+ content = capture(:stderr) { run_generator }
assert_match(/The name 'NotifierMailerTest' is either already used in your application or reserved/, content)
ensure
Object.send :remove_const, :NotifierMailerTest
@@ -70,7 +69,7 @@ class MailerGeneratorTest < Rails::Generators::TestCase
def test_check_preview_class_collision
Object.send :const_set, :NotifierMailerPreview, Class.new
- content = capture(:stderr){ run_generator }
+ content = capture(:stderr) { run_generator }
assert_match(/The name 'NotifierMailerPreview' is either already used in your application or reserved/, content)
ensure
Object.send :remove_const, :NotifierMailerPreview
@@ -106,15 +105,13 @@ class MailerGeneratorTest < Rails::Generators::TestCase
end
assert_file "app/views/layouts/mailer.html.erb" do |view|
- assert_match(%r{<html>\n <body>\n <%= yield %>\n </body>\n</html>}, view)
+ assert_match(%r{<body>\n <%= yield %>\n </body>}, view)
end
end
def test_invokes_default_template_engine_even_with_no_action
run_generator ["notifier"]
assert_file "app/views/notifier_mailer"
- assert_file "app/views/layouts/mailer.text.erb"
- assert_file "app/views/layouts/mailer.html.erb"
end
def test_logs_if_the_template_engine_cannot_be_found
@@ -142,12 +139,12 @@ class MailerGeneratorTest < Rails::Generators::TestCase
assert_file "app/mailers/notifier_mailer.rb" do |mailer|
assert_instance_method :foo, mailer do |foo|
- assert_match(/mail to: "to@example.org"/, foo)
+ assert_match(/mail to: "to@example\.org"/, foo)
assert_match(/@greeting = "Hi"/, foo)
end
assert_instance_method :bar, mailer do |bar|
- assert_match(/mail to: "to@example.org"/, bar)
+ assert_match(/mail to: "to@example\.org"/, bar)
assert_match(/@greeting = "Hi"/, bar)
end
end
@@ -162,10 +159,6 @@ class MailerGeneratorTest < Rails::Generators::TestCase
assert_no_file "app/views/notifier/bar.text.erb"
assert_no_file "app/views/notifier/foo.html.erb"
assert_no_file "app/views/notifier/bar.html.erb"
-
- assert_file "app/mailers/application_mailer.rb"
- assert_file "app/views/layouts/mailer.text.erb"
- assert_file "app/views/layouts/mailer.html.erb"
end
def test_mailer_suffix_is_not_duplicated
diff --git a/railties/test/generators/migration_generator_test.rb b/railties/test/generators/migration_generator_test.rb
index 57bc220558..5812cbdfc9 100644
--- a/railties/test/generators/migration_generator_test.rb
+++ b/railties/test/generators/migration_generator_test.rb
@@ -1,5 +1,7 @@
-require 'generators/generators_test_helper'
-require 'rails/generators/rails/migration/migration_generator'
+# frozen_string_literal: true
+
+require "generators/generators_test_helper"
+require "rails/generators/rails/migration/migration_generator"
class MigrationGeneratorTest < Rails::Generators::TestCase
include GeneratorsTestHelper
@@ -7,7 +9,7 @@ class MigrationGeneratorTest < Rails::Generators::TestCase
def test_migration
migration = "change_title_body_from_posts"
run_generator [migration]
- assert_migration "db/migrate/#{migration}.rb", /class ChangeTitleBodyFromPosts < ActiveRecord::Migration/
+ assert_migration "db/migrate/#{migration}.rb", /class ChangeTitleBodyFromPosts < ActiveRecord::Migration\[[0-9.]+\]/
end
def test_migrations_generated_simultaneously
@@ -17,7 +19,7 @@ class MigrationGeneratorTest < Rails::Generators::TestCase
run_generator [migration]
file_name = migration_file_name "db/migrate/#{migration}.rb"
- File.basename(file_name).split('_').first
+ File.basename(file_name).split("_").first
end
assert_not_equal first_migration_number, second_migration_number
@@ -26,7 +28,7 @@ class MigrationGeneratorTest < Rails::Generators::TestCase
def test_migration_with_class_name
migration = "ChangeTitleBodyFromPosts"
run_generator [migration]
- assert_migration "db/migrate/change_title_body_from_posts.rb", /class #{migration} < ActiveRecord::Migration/
+ assert_migration "db/migrate/change_title_body_from_posts.rb", /class #{migration} < ActiveRecord::Migration\[[0-9.]+\]/
end
def test_migration_with_invalid_file_name
@@ -48,6 +50,17 @@ class MigrationGeneratorTest < Rails::Generators::TestCase
end
end
+ def test_add_migration_with_table_having_from_in_title
+ migration = "add_email_address_to_excluded_from_campaign"
+ run_generator [migration, "email_address:string"]
+
+ assert_migration "db/migrate/#{migration}.rb" do |content|
+ assert_method :change, content do |change|
+ assert_match(/add_column :excluded_from_campaigns, :email_address, :string/, change)
+ end
+ end
+ end
+
def test_remove_migration_with_indexed_attribute
migration = "remove_title_body_from_posts"
run_generator [migration, "title:string:index", "body:text"]
@@ -73,14 +86,25 @@ class MigrationGeneratorTest < Rails::Generators::TestCase
end
end
+ def test_remove_migration_with_table_having_to_in_title
+ migration = "remove_email_address_from_sent_to_user"
+ run_generator [migration, "email_address:string"]
+
+ assert_migration "db/migrate/#{migration}.rb" do |content|
+ assert_method :change, content do |change|
+ assert_match(/remove_column :sent_to_users, :email_address, :string/, change)
+ end
+ end
+ end
+
def test_remove_migration_with_references_options
migration = "remove_references_from_books"
run_generator [migration, "author:belongs_to", "distributor:references{polymorphic}"]
assert_migration "db/migrate/#{migration}.rb" do |content|
assert_method :change, content do |change|
- assert_match(/remove_reference :books, :author, index: true/, change)
- assert_match(/remove_reference :books, :distributor, polymorphic: true, index: true/, change)
+ assert_match(/remove_reference :books, :author/, change)
+ assert_match(/remove_reference :books, :distributor, polymorphic: true/, change)
end
end
end
@@ -166,8 +190,8 @@ class MigrationGeneratorTest < Rails::Generators::TestCase
assert_migration "db/migrate/#{migration}.rb" do |content|
assert_method :change, content do |change|
- assert_match(/add_reference :books, :author, index: true/, change)
- assert_match(/add_reference :books, :distributor, polymorphic: true, index: true/, change)
+ assert_match(/add_reference :books, :author/, change)
+ assert_match(/add_reference :books, :distributor, polymorphic: true/, change)
end
end
end
@@ -178,8 +202,8 @@ class MigrationGeneratorTest < Rails::Generators::TestCase
assert_migration "db/migrate/#{migration}.rb" do |content|
assert_method :change, content do |change|
- assert_match(/add_reference :books, :author, index: true, null: false/, change)
- assert_match(/add_reference :books, :distributor, polymorphic: true, index: true, null: false/, change)
+ assert_match(/add_reference :books, :author, null: false/, change)
+ assert_match(/add_reference :books, :distributor, polymorphic: true, null: false/, change)
end
end
end
@@ -204,8 +228,8 @@ class MigrationGeneratorTest < Rails::Generators::TestCase
assert_migration "db/migrate/#{migration}.rb" do |content|
assert_method :change, content do |change|
assert_match(/create_join_table :artists, :musics/, change)
- assert_match(/# t.index \[:artist_id, :music_id\]/, change)
- assert_match(/ t.index \[:music_id, :artist_id\], unique: true/, change)
+ assert_match(/# t\.index \[:artist_id, :music_id\]/, change)
+ assert_match(/ t\.index \[:music_id, :artist_id\], unique: true/, change)
end
end
end
@@ -221,6 +245,26 @@ class MigrationGeneratorTest < Rails::Generators::TestCase
end
end
+ def test_add_uuid_to_create_table_migration
+ run_generator ["create_books", "--primary_key_type=uuid"]
+ assert_migration "db/migrate/create_books.rb" do |content|
+ assert_method :change, content do |change|
+ assert_match(/create_table :books, id: :uuid/, change)
+ end
+ end
+ end
+
+ def test_database_puts_migrations_in_configured_folder
+ with_secondary_database_configuration do
+ run_generator ["create_books", "--database=secondary"]
+ assert_migration "db/secondary_migrate/create_books.rb" do |content|
+ assert_method :change, content do |change|
+ assert_match(/create_table :books/, change)
+ end
+ end
+ end
+ end
+
def test_should_create_empty_migrations_if_name_not_start_with_add_or_remove_or_create
migration = "delete_books"
run_generator [migration, "title:string", "content:text"]
@@ -239,7 +283,7 @@ class MigrationGeneratorTest < Rails::Generators::TestCase
def test_migration_with_singular_table_name
with_singular_table_name do
migration = "add_title_body_to_post"
- run_generator [migration, 'title:string']
+ run_generator [migration, "title:string"]
assert_migration "db/migrate/#{migration}.rb" do |content|
assert_method :change, content do |change|
assert_match(/add_column :post, :title, :string/, change)
@@ -256,8 +300,8 @@ class MigrationGeneratorTest < Rails::Generators::TestCase
assert_migration "db/migrate/#{migration}.rb" do |content|
assert_method :change, content do |change|
assert_match(/create_join_table :artist, :music/, change)
- assert_match(/# t.index \[:artist_id, :music_id\]/, change)
- assert_match(/ t.index \[:music_id, :artist_id\], unique: true/, change)
+ assert_match(/# t\.index \[:artist_id, :music_id\]/, change)
+ assert_match(/ t\.index \[:music_id, :artist_id\], unique: true/, change)
end
end
end
@@ -300,6 +344,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 abd3ff50a4..b06db6dd8a 100644
--- a/railties/test/generators/model_generator_test.rb
+++ b/railties/test/generators/model_generator_test.rb
@@ -1,11 +1,17 @@
-require 'generators/generators_test_helper'
-require 'rails/generators/rails/model/model_generator'
-require 'active_support/core_ext/string/strip'
+# frozen_string_literal: true
+
+require "generators/generators_test_helper"
+require "rails/generators/rails/model/model_generator"
class ModelGeneratorTest < Rails::Generators::TestCase
include GeneratorsTestHelper
arguments %w(Account name:string age:integer)
+ def setup
+ super
+ Rails::Generators::ModelHelpers.skip_warn = false
+ end
+
def test_help_shows_invoked_generators_options
content = run_generator ["--help"]
assert_match(/ActiveRecord options:/, content)
@@ -26,7 +32,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase
def test_invokes_default_orm
run_generator
- assert_file "app/models/account.rb", /class Account < ActiveRecord::Base/
+ assert_file "app/models/account.rb", /class Account < ApplicationRecord/
end
def test_model_with_parent_option
@@ -36,12 +42,24 @@ class ModelGeneratorTest < Rails::Generators::TestCase
end
def test_plural_names_are_singularized
- content = run_generator ["accounts".freeze]
- assert_file "app/models/account.rb", /class Account < ActiveRecord::Base/
+ content = run_generator ["accounts"]
+ assert_file "app/models/account.rb", /class Account < ApplicationRecord/
assert_file "test/models/account_test.rb", /class AccountTest/
assert_match(/\[WARNING\] The model name 'accounts' was recognized as a plural, using the singular 'account' instead\. Override with --force-plural or setup custom inflection rules for this noun before running the generator\./, content)
end
+ def test_unknown_inflection_rule_are_warned
+ content = run_generator ["porsche"]
+ assert_match("[WARNING] Rails cannot recover singular form from its plural form 'porsches'.\nPlease setup custom inflection rules for this noun before running the generator in config/initializers/inflections.rb.", content)
+ assert_file "app/models/porsche.rb", /class Porsche < ApplicationRecord/
+
+ uncountable_content = run_generator ["sheep"]
+ assert_no_match("[WARNING] Rails cannot recover singular form from its plural form", uncountable_content)
+
+ regular_content = run_generator ["account"]
+ assert_no_match("[WARNING] Rails cannot recover singular form from its plural form", regular_content)
+ end
+
def test_model_with_underscored_parent_option
run_generator ["account", "--parent", "admin/account"]
assert_file "app/models/account.rb", /class Account < Admin::Account/
@@ -52,17 +70,17 @@ class ModelGeneratorTest < Rails::Generators::TestCase
assert_file "app/models/admin.rb", /module Admin/
assert_file "app/models/admin.rb", /def self\.table_name_prefix/
assert_file "app/models/admin.rb", /'admin_'/
- assert_file "app/models/admin/account.rb", /class Admin::Account < ActiveRecord::Base/
+ assert_file "app/models/admin/account.rb", /class Admin::Account < ApplicationRecord/
end
def test_migration
run_generator
- assert_migration "db/migrate/create_accounts.rb", /class CreateAccounts < ActiveRecord::Migration/
+ assert_migration "db/migrate/create_accounts.rb", /class CreateAccounts < ActiveRecord::Migration\[[0-9.]+\]/
end
def test_migration_with_namespace
run_generator ["Gallery::Image"]
- assert_migration "db/migrate/create_gallery_images", /class CreateGalleryImages < ActiveRecord::Migration/
+ assert_migration "db/migrate/create_gallery_images", /class CreateGalleryImages < ActiveRecord::Migration\[[0-9.]+\]/
assert_no_migration "db/migrate/create_images"
end
@@ -70,7 +88,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase
run_generator ["Admin::Gallery::Image"]
assert_no_migration "db/migrate/create_images"
assert_no_migration "db/migrate/create_gallery_images"
- assert_migration "db/migrate/create_admin_gallery_images", /class CreateAdminGalleryImages < ActiveRecord::Migration/
+ assert_migration "db/migrate/create_admin_gallery_images", /class CreateAdminGalleryImages < ActiveRecord::Migration\[[0-9.]+\]/
assert_migration "db/migrate/create_admin_gallery_images", /create_table :admin_gallery_images/
end
@@ -80,7 +98,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase
assert_no_migration "db/migrate/create_images"
assert_no_migration "db/migrate/create_gallery_images"
assert_no_migration "db/migrate/create_admin_gallery_images"
- assert_migration "db/migrate/create_admin_gallery_image", /class CreateAdminGalleryImage < ActiveRecord::Migration/
+ assert_migration "db/migrate/create_admin_gallery_image", /class CreateAdminGalleryImage < ActiveRecord::Migration\[[0-9.]+\]/
assert_migration "db/migrate/create_admin_gallery_image", /create_table :admin_gallery_image/
ensure
ActiveRecord::Base.pluralize_table_names = true
@@ -89,7 +107,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase
def test_migration_with_namespaces_in_model_name_without_plurization
ActiveRecord::Base.pluralize_table_names = false
run_generator ["Gallery::Image"]
- assert_migration "db/migrate/create_gallery_image", /class CreateGalleryImage < ActiveRecord::Migration/
+ assert_migration "db/migrate/create_gallery_image", /class CreateGalleryImage < ActiveRecord::Migration\[[0-9.]+\]/
assert_no_migration "db/migrate/create_gallery_images"
ensure
ActiveRecord::Base.pluralize_table_names = true
@@ -98,7 +116,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase
def test_migration_without_pluralization
ActiveRecord::Base.pluralize_table_names = false
run_generator
- assert_migration "db/migrate/create_account", /class CreateAccount < ActiveRecord::Migration/
+ assert_migration "db/migrate/create_account", /class CreateAccount < ActiveRecord::Migration\[[0-9.]+\]/
assert_no_migration "db/migrate/create_accounts"
ensure
ActiveRecord::Base.pluralize_table_names = true
@@ -193,14 +211,25 @@ class ModelGeneratorTest < Rails::Generators::TestCase
def test_migration_without_timestamps
ActiveRecord::Base.timestamped_migrations = false
run_generator ["account"]
- assert_file "db/migrate/001_create_accounts.rb", /class CreateAccounts < ActiveRecord::Migration/
+ assert_file "db/migrate/001_create_accounts.rb", /class CreateAccounts < ActiveRecord::Migration\[[0-9.]+\]/
run_generator ["project"]
- assert_file "db/migrate/002_create_projects.rb", /class CreateProjects < ActiveRecord::Migration/
+ assert_file "db/migrate/002_create_projects.rb", /class CreateProjects < ActiveRecord::Migration\[[0-9.]+\]/
ensure
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/
@@ -223,7 +252,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase
def test_migration_with_timestamps
run_generator
- assert_migration "db/migrate/create_accounts.rb", /t.timestamps/
+ assert_migration "db/migrate/create_accounts.rb", /t\.timestamps/
end
def test_migration_timestamps_are_skipped
@@ -231,7 +260,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase
assert_migration "db/migrate/create_accounts.rb" do |m|
assert_method :change, m do |up|
- assert_no_match(/t.timestamps/, up)
+ assert_no_match(/t\.timestamps/, up)
end
end
end
@@ -239,24 +268,24 @@ class ModelGeneratorTest < Rails::Generators::TestCase
def test_migration_is_skipped_with_skip_option
run_generator
output = run_generator ["Account", "--skip"]
- assert_match %r{skip\s+db/migrate/\d+_create_accounts.rb}, output
+ assert_match %r{skip\s+db/migrate/\d+_create_accounts\.rb}, output
end
def test_migration_is_ignored_as_identical_with_skip_option
run_generator ["Account"]
output = run_generator ["Account", "--skip"]
- assert_match %r{identical\s+db/migrate/\d+_create_accounts.rb}, output
+ assert_match %r{identical\s+db/migrate/\d+_create_accounts\.rb}, output
end
def test_migration_is_skipped_on_skip_behavior
run_generator
output = run_generator ["Account"], behavior: :skip
- assert_match %r{skip\s+db/migrate/\d+_create_accounts.rb}, output
+ assert_match %r{skip\s+db/migrate/\d+_create_accounts\.rb}, output
end
def test_migration_error_is_not_shown_on_revoke
run_generator
- error = capture(:stderr){ run_generator ["Account"], behavior: :revoke }
+ error = capture(:stderr) { run_generator ["Account"], behavior: :revoke }
assert_no_match(/Another migration is already named create_accounts/, error)
end
@@ -281,31 +310,31 @@ class ModelGeneratorTest < Rails::Generators::TestCase
assert_file "test/fixtures/accounts.yml", /name: MyString/, /age: 1/
assert_generated_fixture("test/fixtures/accounts.yml",
- {"one"=>{"name"=>"MyString", "age"=>1}, "two"=>{"name"=>"MyString", "age"=>1}})
+ "one" => { "name" => "MyString", "age" => 1 }, "two" => { "name" => "MyString", "age" => 1 })
end
def test_fixtures_use_the_references_ids
run_generator ["LineItem", "product:references", "cart:belongs_to"]
- assert_file "test/fixtures/line_items.yml", /product: \n cart: /
+ assert_file "test/fixtures/line_items.yml", /product: one\n cart: one/
assert_generated_fixture("test/fixtures/line_items.yml",
- {"one"=>{"product"=>nil, "cart"=>nil}, "two"=>{"product"=>nil, "cart"=>nil}})
+ "one" => { "product" => "one", "cart" => "one" }, "two" => { "product" => "two", "cart" => "two" })
end
def test_fixtures_use_the_references_ids_and_type
run_generator ["LineItem", "product:references{polymorphic}", "cart:belongs_to"]
- assert_file "test/fixtures/line_items.yml", /product: \n product_type: Product\n cart: /
+ assert_file "test/fixtures/line_items.yml", /product: one\n product_type: Product\n cart: one/
assert_generated_fixture("test/fixtures/line_items.yml",
- {"one"=>{"product"=>nil, "product_type"=>"Product", "cart"=>nil},
- "two"=>{"product"=>nil, "product_type"=>"Product", "cart"=>nil}})
+ "one" => { "product" => "one", "product_type" => "Product", "cart" => "one" },
+ "two" => { "product" => "two", "product_type" => "Product", "cart" => "two" })
end
def test_fixtures_respect_reserved_yml_keywords
run_generator ["LineItem", "no:integer", "Off:boolean", "ON:boolean"]
assert_generated_fixture("test/fixtures/line_items.yml",
- {"one"=>{"no"=>1, "Off"=>false, "ON"=>false}, "two"=>{"no"=>1, "Off"=>false, "ON"=>false}})
+ "one" => { "no" => 1, "Off" => false, "ON" => false }, "two" => { "no" => 1, "Off" => false, "ON" => false })
end
def test_fixture_is_skipped
@@ -324,52 +353,52 @@ class ModelGeneratorTest < Rails::Generators::TestCase
ActiveRecord::Base.pluralize_table_names = false
run_generator
assert_generated_fixture("test/fixtures/account.yml",
- {"one"=>{"name"=>"MyString", "age"=>1}, "two"=>{"name"=>"MyString", "age"=>1}})
+ "one" => { "name" => "MyString", "age" => 1 }, "two" => { "name" => "MyString", "age" => 1 })
ensure
ActiveRecord::Base.pluralize_table_names = original_pluralize_table_name
end
def test_check_class_collision
- content = capture(:stderr){ run_generator ["object"] }
+ content = capture(:stderr) { run_generator ["object"] }
assert_match(/The name 'Object' is either already used in your application or reserved/, content)
end
- def test_index_is_added_for_belongs_to_association
- run_generator ["account", "supplier:belongs_to"]
+ def test_index_is_skipped_for_belongs_to_association
+ run_generator ["account", "supplier:belongs_to", "--no-indexes"]
assert_migration "db/migrate/create_accounts.rb" do |m|
assert_method :change, m do |up|
- assert_match(/index: true/, up)
+ assert_no_match(/index: true/, up)
end
end
end
- def test_index_is_added_for_references_association
- run_generator ["account", "supplier:references"]
+ def test_index_is_skipped_for_references_association
+ run_generator ["account", "supplier:references", "--no-indexes"]
assert_migration "db/migrate/create_accounts.rb" do |m|
assert_method :change, m do |up|
- assert_match(/index: true/, up)
+ assert_no_match(/index: true/, up)
end
end
end
- def test_index_is_skipped_for_belongs_to_association
- run_generator ["account", "supplier:belongs_to", "--no-indexes"]
-
- assert_migration "db/migrate/create_accounts.rb" do |m|
- assert_method :change, m do |up|
- assert_no_match(/index: true/, up)
+ def test_add_uuid_to_create_table_migration
+ run_generator ["account", "--primary_key_type=uuid"]
+ assert_migration "db/migrate/create_accounts.rb" do |content|
+ assert_method :change, content do |change|
+ assert_match(/create_table :accounts, id: :uuid/, change)
end
end
end
- def test_index_is_skipped_for_references_association
- run_generator ["account", "supplier:references", "--no-indexes"]
-
- assert_migration "db/migrate/create_accounts.rb" do |m|
- assert_method :change, m do |up|
- assert_no_match(/index: true/, up)
+ def test_database_puts_migrations_in_configured_folder
+ with_secondary_database_configuration do
+ run_generator ["account", "--database=secondary"]
+ assert_migration "db/secondary_migrate/create_accounts.rb" do |content|
+ assert_method :change, content do |change|
+ assert_match(/create_table :accounts/, change)
+ end
end
end
end
@@ -377,10 +406,10 @@ class ModelGeneratorTest < Rails::Generators::TestCase
def test_required_belongs_to_adds_required_association
run_generator ["account", "supplier:references{required}"]
- expected_file = <<-FILE.strip_heredoc
- class Account < ActiveRecord::Base
- belongs_to :supplier, required: true
- end
+ expected_file = <<~FILE
+ class Account < ApplicationRecord
+ belongs_to :supplier, required: true
+ end
FILE
assert_file "app/models/account.rb", expected_file
end
@@ -388,10 +417,10 @@ class ModelGeneratorTest < Rails::Generators::TestCase
def test_required_polymorphic_belongs_to_generages_correct_model
run_generator ["account", "supplier:references{required,polymorphic}"]
- expected_file = <<-FILE.strip_heredoc
- class Account < ActiveRecord::Base
- belongs_to :supplier, polymorphic: true, required: true
- end
+ expected_file = <<~FILE
+ class Account < ApplicationRecord
+ belongs_to :supplier, polymorphic: true, required: true
+ end
FILE
assert_file "app/models/account.rb", expected_file
end
@@ -399,10 +428,10 @@ class ModelGeneratorTest < Rails::Generators::TestCase
def test_required_and_polymorphic_are_order_independent
run_generator ["account", "supplier:references{polymorphic.required}"]
- expected_file = <<-FILE.strip_heredoc
- class Account < ActiveRecord::Base
- belongs_to :supplier, polymorphic: true, required: true
- end
+ expected_file = <<~FILE
+ class Account < ApplicationRecord
+ belongs_to :supplier, polymorphic: true, required: true
+ end
FILE
assert_file "app/models/account.rb", expected_file
end
@@ -450,11 +479,11 @@ class ModelGeneratorTest < Rails::Generators::TestCase
def test_token_option_adds_has_secure_token
run_generator ["user", "token:token", "auth_token:token"]
- expected_file = <<-FILE.strip_heredoc
- class User < ActiveRecord::Base
- has_secure_token
- has_secure_token :auth_token
- end
+ expected_file = <<~FILE
+ class User < ApplicationRecord
+ has_secure_token
+ has_secure_token :auth_token
+ end
FILE
assert_file "app/models/user.rb", expected_file
end
diff --git a/railties/test/generators/named_base_test.rb b/railties/test/generators/named_base_test.rb
index 1c32fc1bfd..4e61b660d7 100644
--- a/railties/test/generators/named_base_test.rb
+++ b/railties/test/generators/named_base_test.rb
@@ -1,85 +1,108 @@
-require 'generators/generators_test_helper'
-require 'rails/generators/rails/scaffold_controller/scaffold_controller_generator'
-require 'minitest/mock'
+# frozen_string_literal: true
+
+require "generators/generators_test_helper"
+require "rails/generators/rails/scaffold_controller/scaffold_controller_generator"
class NamedBaseTest < Rails::Generators::TestCase
include GeneratorsTestHelper
tests Rails::Generators::ScaffoldControllerGenerator
def test_named_generator_with_underscore
- g = generator ['line_item']
- assert_name g, 'line_item', :name
+ g = generator ["line_item"]
+ assert_name g, "line_item", :name
assert_name g, %w(), :class_path
- assert_name g, 'LineItem', :class_name
- assert_name g, 'line_item', :file_path
- assert_name g, 'line_item', :file_name
- assert_name g, 'Line item', :human_name
- assert_name g, 'line_item', :singular_name
- assert_name g, 'line_items', :plural_name
- assert_name g, 'line_item', :i18n_scope
- assert_name g, 'line_items', :table_name
+ assert_name g, "LineItem", :class_name
+ assert_name g, "line_item", :file_path
+ assert_name g, "line_item", :file_name
+ assert_name g, "Line item", :human_name
+ assert_name g, "line_item", :singular_name
+ assert_name g, "line_items", :plural_name
+ assert_name g, "line_item", :i18n_scope
+ assert_name g, "line_items", :table_name
end
def test_named_generator_attributes
- g = generator ['admin/foo']
- assert_name g, 'admin/foo', :name
+ g = generator ["admin/foo"]
+ assert_name g, "admin/foo", :name
assert_name g, %w(admin), :class_path
- assert_name g, 'Admin::Foo', :class_name
- assert_name g, 'admin/foo', :file_path
- assert_name g, 'foo', :file_name
- assert_name g, 'Foo', :human_name
- assert_name g, 'foo', :singular_name
- assert_name g, 'foos', :plural_name
- assert_name g, 'admin.foo', :i18n_scope
- assert_name g, 'admin_foos', :table_name
+ assert_name g, "Admin::Foo", :class_name
+ assert_name g, "admin/foo", :file_path
+ assert_name g, "foo", :file_name
+ assert_name g, "Foo", :human_name
+ assert_name g, "foo", :singular_name
+ assert_name g, "foos", :plural_name
+ assert_name g, "admin.foo", :i18n_scope
+ assert_name g, "admin_foos", :table_name
+ assert_name g, "admin/foos", :controller_name
+ assert_name g, %w(admin), :controller_class_path
+ assert_name g, "Admin::Foos", :controller_class_name
+ assert_name g, "admin/foos", :controller_file_path
+ assert_name g, "foos", :controller_file_name
+ assert_name g, "admin.foos", :controller_i18n_scope
+ assert_name g, "admin_foo", :singular_route_name
+ assert_name g, "admin_foos", :plural_route_name
+ assert_name g, "@admin_foo", :redirect_resource_name
+ assert_name g, "admin_foo", :model_resource_name
+ assert_name g, "admin_foos", :index_helper
end
def test_named_generator_attributes_as_ruby
- g = generator ['Admin::Foo']
- assert_name g, 'Admin::Foo', :name
+ g = generator ["Admin::Foo"]
+ assert_name g, "Admin::Foo", :name
assert_name g, %w(admin), :class_path
- assert_name g, 'Admin::Foo', :class_name
- assert_name g, 'admin/foo', :file_path
- assert_name g, 'foo', :file_name
- assert_name g, 'foo', :singular_name
- assert_name g, 'Foo', :human_name
- assert_name g, 'foos', :plural_name
- assert_name g, 'admin.foo', :i18n_scope
- assert_name g, 'admin_foos', :table_name
+ assert_name g, "Admin::Foo", :class_name
+ assert_name g, "admin/foo", :file_path
+ assert_name g, "foo", :file_name
+ assert_name g, "foo", :singular_name
+ assert_name g, "Foo", :human_name
+ assert_name g, "foos", :plural_name
+ assert_name g, "admin.foo", :i18n_scope
+ assert_name g, "admin_foos", :table_name
+ assert_name g, "Admin::Foos", :controller_name
+ assert_name g, %w(admin), :controller_class_path
+ assert_name g, "Admin::Foos", :controller_class_name
+ assert_name g, "admin/foos", :controller_file_path
+ assert_name g, "foos", :controller_file_name
+ assert_name g, "admin.foos", :controller_i18n_scope
+ assert_name g, "admin_foo", :singular_route_name
+ assert_name g, "admin_foos", :plural_route_name
+ assert_name g, "@admin_foo", :redirect_resource_name
+ assert_name g, "admin_foo", :model_resource_name
+ assert_name g, "admin_foos", :index_helper
end
def test_named_generator_attributes_without_pluralized
original_pluralize_table_names = ActiveRecord::Base.pluralize_table_names
ActiveRecord::Base.pluralize_table_names = false
- g = generator ['admin/foo']
- assert_name g, 'admin_foo', :table_name
+ g = generator ["admin/foo"]
+ assert_name g, "admin_foo", :table_name
ensure
ActiveRecord::Base.pluralize_table_names = original_pluralize_table_names
end
- def test_scaffold_plural_names
- g = generator ['admin/foo']
- assert_name g, 'admin/foos', :controller_name
+ def test_namespaced_scaffold_plural_names
+ g = generator ["admin/foo"]
+ assert_name g, "admin/foos", :controller_name
assert_name g, %w(admin), :controller_class_path
- assert_name g, 'Admin::Foos', :controller_class_name
- assert_name g, 'admin/foos', :controller_file_path
- assert_name g, 'foos', :controller_file_name
- assert_name g, 'admin.foos', :controller_i18n_scope
+ assert_name g, "Admin::Foos", :controller_class_name
+ assert_name g, "admin/foos", :controller_file_path
+ assert_name g, "foos", :controller_file_name
+ assert_name g, "admin.foos", :controller_i18n_scope
end
- def test_scaffold_plural_names_as_ruby
- g = generator ['Admin::Foo']
- assert_name g, 'Admin::Foos', :controller_name
+ def test_namespaced_scaffold_plural_names_as_ruby
+ g = generator ["Admin::Foo"]
+ assert_name g, "Admin::Foos", :controller_name
assert_name g, %w(admin), :controller_class_path
- assert_name g, 'Admin::Foos', :controller_class_name
- assert_name g, 'admin/foos', :controller_file_path
- assert_name g, 'foos', :controller_file_name
- assert_name g, 'admin.foos', :controller_i18n_scope
+ assert_name g, "Admin::Foos", :controller_class_name
+ assert_name g, "admin/foos", :controller_file_path
+ assert_name g, "foos", :controller_file_name
+ assert_name g, "admin.foos", :controller_i18n_scope
end
def test_application_name
- g = generator ['Admin::Foo']
+ g = generator ["Admin::Foo"]
Rails.stub(:application, Object.new) do
assert_name g, "object", :application_name
end
@@ -90,49 +113,62 @@ class NamedBaseTest < Rails::Generators::TestCase
end
def test_index_helper
- g = generator ['Post']
- assert_name g, 'posts', :index_helper
+ g = generator ["Post"]
+ assert_name g, "posts", :index_helper
end
def test_index_helper_to_pluralize_once
- g = generator ['Stadium']
- assert_name g, 'stadia', :index_helper
+ g = generator ["Stadium"]
+ assert_name g, "stadia", :index_helper
end
def test_index_helper_with_uncountable
- g = generator ['Sheep']
- assert_name g, 'sheep_index', :index_helper
+ g = generator ["Sheep"]
+ assert_name g, "sheep_index", :index_helper
end
def test_hide_namespace
- g = generator ['Hidden']
- g.class.stub(:namespace, 'hidden') do
- assert !Rails::Generators.hidden_namespaces.include?('hidden')
+ g = generator ["Hidden"]
+ g.class.stub(:namespace, "hidden") do
+ assert_not_includes Rails::Generators.hidden_namespaces, "hidden"
g.class.hide!
- assert Rails::Generators.hidden_namespaces.include?('hidden')
+ assert_includes Rails::Generators.hidden_namespaces, "hidden"
end
end
def test_scaffold_plural_names_with_model_name_option
- g = generator ['Admin::Foo'], model_name: 'User'
- assert_name g, 'user', :singular_name
- assert_name g, 'User', :name
- assert_name g, 'user', :file_path
- assert_name g, 'User', :class_name
- assert_name g, 'user', :file_name
- assert_name g, 'User', :human_name
- assert_name g, 'users', :plural_name
- assert_name g, 'user', :i18n_scope
- assert_name g, 'users', :table_name
- assert_name g, 'Admin::Foos', :controller_name
+ g = generator ["Admin::Foo"], model_name: "User"
+ assert_name g, "user", :singular_name
+ assert_name g, "User", :name
+ assert_name g, "user", :file_path
+ assert_name g, "User", :class_name
+ assert_name g, "user", :file_name
+ assert_name g, "User", :human_name
+ assert_name g, "users", :plural_name
+ assert_name g, "user", :i18n_scope
+ assert_name g, "users", :table_name
+ assert_name g, "Admin::Foos", :controller_name
assert_name g, %w(admin), :controller_class_path
- assert_name g, 'Admin::Foos', :controller_class_name
- assert_name g, 'admin/foos', :controller_file_path
- assert_name g, 'foos', :controller_file_name
- assert_name g, 'admin.foos', :controller_i18n_scope
+ assert_name g, "Admin::Foos", :controller_class_name
+ assert_name g, "admin/foos", :controller_file_path
+ assert_name g, "foos", :controller_file_name
+ assert_name g, "admin.foos", :controller_i18n_scope
+ assert_name g, "admin_user", :singular_route_name
+ assert_name g, "admin_users", :plural_route_name
+ assert_name g, "[:admin, @user]", :redirect_resource_name
+ assert_name g, "[:admin, user]", :model_resource_name
+ assert_name g, "admin_users", :index_helper
+ end
+
+ def test_scaffold_plural_names
+ g = generator ["User"]
+ assert_name g, "@user", :redirect_resource_name
+ assert_name g, "user", :model_resource_name
+ assert_name g, "user", :singular_route_name
+ assert_name g, "users", :plural_route_name
end
- protected
+ private
def assert_name(generator, value, method)
assert_equal value, generator.send(method)
diff --git a/railties/test/generators/namespaced_generators_test.rb b/railties/test/generators/namespaced_generators_test.rb
index e839b67960..4b75a31f17 100644
--- a/railties/test/generators/namespaced_generators_test.rb
+++ b/railties/test/generators/namespaced_generators_test.rb
@@ -1,8 +1,11 @@
-require 'generators/generators_test_helper'
-require 'rails/generators/rails/controller/controller_generator'
-require 'rails/generators/rails/model/model_generator'
-require 'rails/generators/mailer/mailer_generator'
-require 'rails/generators/rails/scaffold/scaffold_generator'
+# frozen_string_literal: true
+
+require "generators/generators_test_helper"
+require "rails/generators/rails/controller/controller_generator"
+require "rails/generators/rails/model/model_generator"
+require "rails/generators/mailer/mailer_generator"
+require "rails/generators/rails/scaffold/scaffold_generator"
+require "rails/generators/rails/application_record/application_record_generator"
class NamespacedGeneratorTestCase < Rails::Generators::TestCase
include GeneratorsTestHelper
@@ -91,7 +94,7 @@ class NamespacedModelGeneratorTest < NamespacedGeneratorTestCase
def test_adds_namespace_to_model
run_generator
- assert_file "app/models/test_app/account.rb", /module TestApp/, / class Account < ActiveRecord::Base/
+ assert_file "app/models/test_app/account.rb", /module TestApp/, / class Account < ApplicationRecord/
end
def test_model_with_namespace
@@ -99,17 +102,17 @@ class NamespacedModelGeneratorTest < NamespacedGeneratorTestCase
assert_file "app/models/test_app/admin.rb", /module TestApp/, /module Admin/
assert_file "app/models/test_app/admin.rb", /def self\.table_name_prefix/
assert_file "app/models/test_app/admin.rb", /'test_app_admin_'/
- assert_file "app/models/test_app/admin/account.rb", /module TestApp/, /class Admin::Account < ActiveRecord::Base/
+ assert_file "app/models/test_app/admin/account.rb", /module TestApp/, /class Admin::Account < ApplicationRecord/
end
def test_migration
run_generator
- assert_migration "db/migrate/create_test_app_accounts.rb", /create_table :test_app_accounts/, /class CreateTestAppAccounts < ActiveRecord::Migration/
+ assert_migration "db/migrate/create_test_app_accounts.rb", /create_table :test_app_accounts/, /class CreateTestAppAccounts < ActiveRecord::Migration\[[0-9.]+\]/
end
def test_migration_with_namespace
run_generator ["Gallery::Image"]
- assert_migration "db/migrate/create_test_app_gallery_images", /class CreateTestAppGalleryImages < ActiveRecord::Migration/
+ assert_migration "db/migrate/create_test_app_gallery_images", /class CreateTestAppGalleryImages < ActiveRecord::Migration\[[0-9.]+\]/
assert_no_migration "db/migrate/create_test_app_images"
end
@@ -117,7 +120,7 @@ class NamespacedModelGeneratorTest < NamespacedGeneratorTestCase
run_generator ["Admin::Gallery::Image"]
assert_no_migration "db/migrate/create_images"
assert_no_migration "db/migrate/create_gallery_images"
- assert_migration "db/migrate/create_test_app_admin_gallery_images", /class CreateTestAppAdminGalleryImages < ActiveRecord::Migration/
+ assert_migration "db/migrate/create_test_app_admin_gallery_images", /class CreateTestAppAdminGalleryImages < ActiveRecord::Migration\[[0-9.]+\]/
assert_migration "db/migrate/create_test_app_admin_gallery_images", /create_table :test_app_admin_gallery_images/
end
@@ -127,7 +130,7 @@ class NamespacedModelGeneratorTest < NamespacedGeneratorTestCase
assert_no_migration "db/migrate/create_images"
assert_no_migration "db/migrate/create_gallery_images"
assert_no_migration "db/migrate/create_test_app_admin_gallery_images"
- assert_migration "db/migrate/create_test_app_admin_gallery_image", /class CreateTestAppAdminGalleryImage < ActiveRecord::Migration/
+ assert_migration "db/migrate/create_test_app_admin_gallery_image", /class CreateTestAppAdminGalleryImage < ActiveRecord::Migration\[[0-9.]+\]/
assert_migration "db/migrate/create_test_app_admin_gallery_image", /create_table :test_app_admin_gallery_image/
ensure
ActiveRecord::Base.pluralize_table_names = true
@@ -149,7 +152,7 @@ class NamespacedMailerGeneratorTest < NamespacedGeneratorTestCase
assert_file "app/mailers/test_app/notifier_mailer.rb" do |mailer|
assert_match(/module TestApp/, mailer)
assert_match(/class NotifierMailer < ApplicationMailer/, mailer)
- assert_no_match(/default from: "from@example.com"/, mailer)
+ assert_no_match(/default from: "from@example\.com"/, mailer)
end
end
@@ -201,7 +204,7 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase
run_generator
# Model
- assert_file "app/models/test_app/product_line.rb", /module TestApp\n class ProductLine < ActiveRecord::Base/
+ assert_file "app/models/test_app/product_line.rb", /module TestApp\n class ProductLine < ApplicationRecord/
assert_file "test/models/test_app/product_line_test.rb", /module TestApp\n class ProductLineTest < ActiveSupport::TestCase/
assert_file "test/fixtures/test_app/product_lines.yml"
assert_migration "db/migrate/create_test_app_product_lines.rb"
@@ -218,7 +221,7 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase
/class ProductLinesController < ApplicationController/
assert_file "test/controllers/test_app/product_lines_controller_test.rb",
- /module TestApp\n class ProductLinesControllerTest < ActionController::TestCase/
+ /module TestApp\n class ProductLinesControllerTest < ActionDispatch::IntegrationTest/
# Views
%w(index edit new show _form).each do |view|
@@ -268,7 +271,7 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase
# Model
assert_file "app/models/test_app/admin.rb", /module TestApp\n module Admin/
- assert_file "app/models/test_app/admin/role.rb", /module TestApp\n class Admin::Role < ActiveRecord::Base/
+ assert_file "app/models/test_app/admin/role.rb", /module TestApp\n class Admin::Role < ApplicationRecord/
assert_file "test/models/test_app/admin/role_test.rb", /module TestApp\n class Admin::RoleTest < ActiveSupport::TestCase/
assert_file "test/fixtures/test_app/admin/roles.yml"
assert_migration "db/migrate/create_test_app_admin_roles.rb"
@@ -281,10 +284,11 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase
# Controller
assert_file "app/controllers/test_app/admin/roles_controller.rb" do |content|
assert_match(/module TestApp\n class Admin::RolesController < ApplicationController/, content)
+ assert_match(%r(require_dependency "test_app/application_controller"), content)
end
assert_file "test/controllers/test_app/admin/roles_controller_test.rb",
- /module TestApp\n class Admin::RolesControllerTest < ActionController::TestCase/
+ /module TestApp\n class Admin::RolesControllerTest < ActionDispatch::IntegrationTest/
# Views
%w(index edit new show _form).each do |view|
@@ -335,7 +339,7 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase
# Model
assert_file "app/models/test_app/admin/user/special.rb", /module TestApp\n module Admin/
- assert_file "app/models/test_app/admin/user/special/role.rb", /module TestApp\n class Admin::User::Special::Role < ActiveRecord::Base/
+ assert_file "app/models/test_app/admin/user/special/role.rb", /module TestApp\n class Admin::User::Special::Role < ApplicationRecord/
assert_file "test/models/test_app/admin/user/special/role_test.rb", /module TestApp\n class Admin::User::Special::RoleTest < ActiveSupport::TestCase/
assert_file "test/fixtures/test_app/admin/user/special/roles.yml"
assert_migration "db/migrate/create_test_app_admin_user_special_roles.rb"
@@ -351,7 +355,7 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase
end
assert_file "test/controllers/test_app/admin/user/special/roles_controller_test.rb",
- /module TestApp\n class Admin::User::Special::RolesControllerTest < ActionController::TestCase/
+ /module TestApp\n class Admin::User::Special::RolesControllerTest < ActionDispatch::IntegrationTest/
# Views
%w(index edit new show _form).each do |view|
@@ -395,4 +399,38 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase
# Stylesheets (should not be removed)
assert_file "app/assets/stylesheets/scaffold.css"
end
+
+ def test_api_scaffold_with_namespace_on_invoke
+ run_generator [ "admin/role", "name:string", "description:string", "--api" ]
+
+ # Model
+ assert_file "app/models/test_app/admin.rb", /module TestApp\n module Admin/
+ assert_file "app/models/test_app/admin/role.rb", /module TestApp\n class Admin::Role < ApplicationRecord/
+ assert_file "test/models/test_app/admin/role_test.rb", /module TestApp\n class Admin::RoleTest < ActiveSupport::TestCase/
+ assert_file "test/fixtures/test_app/admin/roles.yml"
+ assert_migration "db/migrate/create_test_app_admin_roles.rb"
+
+ # Route
+ assert_file "config/routes.rb" do |route|
+ assert_match(/^ namespace :admin do\n resources :roles\n end$/, route)
+ end
+
+ # Controller
+ assert_file "app/controllers/test_app/admin/roles_controller.rb" do |content|
+ assert_match(/module TestApp\n class Admin::RolesController < ApplicationController/, content)
+ assert_match(%r(require_dependency "test_app/application_controller"), content)
+ end
+ assert_file "test/controllers/test_app/admin/roles_controller_test.rb",
+ /module TestApp\n class Admin::RolesControllerTest < ActionDispatch::IntegrationTest/
+ end
+end
+
+class NamespacedApplicationRecordGeneratorTest < NamespacedGeneratorTestCase
+ include GeneratorsTestHelper
+ tests Rails::Generators::ApplicationRecordGenerator
+
+ def test_adds_namespace_to_application_record
+ run_generator
+ assert_file "app/models/test_app/application_record.rb", /module TestApp/, / class ApplicationRecord < ActiveRecord::Base/
+ end
end
diff --git a/railties/test/generators/orm_test.rb b/railties/test/generators/orm_test.rb
index 88ae930554..6eaf2fbfd3 100644
--- a/railties/test/generators/orm_test.rb
+++ b/railties/test/generators/orm_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "generators/generators_test_helper"
require "rails/generators/rails/scaffold_controller/scaffold_controller_generator"
diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb
index 73e68863f4..66286fc554 100644
--- a/railties/test/generators/plugin_generator_test.rb
+++ b/railties/test/generators/plugin_generator_test.rb
@@ -1,13 +1,15 @@
-require 'generators/generators_test_helper'
-require 'rails/generators/rails/plugin/plugin_generator'
-require 'generators/shared_generator_tests'
-require 'mocha/setup' # FIXME: stop using mocha
+# frozen_string_literal: true
+
+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
Gemfile
Rakefile
- README.rdoc
+ README.md
bukkits.gemspec
MIT-LICENSE
lib
@@ -24,23 +26,27 @@ class PluginGeneratorTest < Rails::Generators::TestCase
destination File.join(Rails.root, "tmp/bukkits")
arguments [destination_root]
+ def application_path
+ "#{destination_root}/test/dummy"
+ end
+
# brings setup, teardown, and some tests
include SharedGeneratorTests
def test_invalid_plugin_name_raises_an_error
- content = capture(:stderr){ run_generator [File.join(destination_root, "my_plugin-31fr-extension")] }
+ content = capture(:stderr) { run_generator [File.join(destination_root, "my_plugin-31fr-extension")] }
assert_equal "Invalid plugin name my_plugin-31fr-extension. Please give a name which does not contain a namespace starting with numeric characters.\n", content
- content = capture(:stderr){ run_generator [File.join(destination_root, "things4.3")] }
+ content = capture(:stderr) { run_generator [File.join(destination_root, "things4.3")] }
assert_equal "Invalid plugin name things4.3. Please give a name which uses only alphabetic, numeric, \"_\" or \"-\" characters.\n", content
- content = capture(:stderr){ run_generator [File.join(destination_root, "43things")] }
+ content = capture(:stderr) { run_generator [File.join(destination_root, "43things")] }
assert_equal "Invalid plugin name 43things. Please give a name which does not start with numbers.\n", content
- content = capture(:stderr){ run_generator [File.join(destination_root, "plugin")] }
+ content = capture(:stderr) { run_generator [File.join(destination_root, "plugin")] }
assert_equal "Invalid plugin name plugin. Please give a name which does not match one of the reserved rails words: application, destroy, plugin, runner, test\n", content
- content = capture(:stderr){ run_generator [File.join(destination_root, "Digest")] }
+ content = capture(:stderr) { run_generator [File.join(destination_root, "Digest")] }
assert_equal "Invalid plugin name Digest, constant Digest is already in use. Please choose another plugin name.\n", content
end
@@ -48,7 +54,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase
run_generator [File.join(destination_root, "hyphenated-name")]
assert_no_file "hyphenated-name/lib/hyphenated-name.rb"
assert_no_file "hyphenated-name/lib/hyphenated_name.rb"
- assert_file "hyphenated-name/lib/hyphenated/name.rb", /module Hyphenated\n module Name\n # Your code goes here...\n end\nend/
+ assert_file "hyphenated-name/lib/hyphenated/name.rb", /module Hyphenated\n module Name\n # Your code goes here\.\.\.\n end\nend/
end
def test_correct_file_in_lib_folder_of_camelcase_plugin_name
@@ -59,14 +65,33 @@ class PluginGeneratorTest < Rails::Generators::TestCase
def test_generating_without_options
run_generator
- assert_file "README.rdoc", /Bukkits/
+ assert_file "README.md", /Bukkits/
assert_no_file "config/routes.rb"
+ assert_no_file "app/assets/config/bukkits_manifest.js"
assert_file "test/test_helper.rb" do |content|
- assert_match(/require.+test\/dummy\/config\/environment/, content)
+ assert_match(/require_relative.+test\/dummy\/config\/environment/, content)
assert_match(/ActiveRecord::Migrator\.migrations_paths.+test\/dummy\/db\/migrate/, content)
assert_match(/Minitest\.backtrace_filter = Minitest::BacktraceFilter\.new/, content)
+ assert_match(/Rails::TestUnitReporter\.executable = 'bin\/test'/, content)
end
+ assert_file "lib/bukkits/railtie.rb", /module Bukkits\n class Railtie < ::Rails::Railtie\n end\nend/
+ assert_file "lib/bukkits.rb", /require "bukkits\/railtie"/
assert_file "test/bukkits_test.rb", /assert_kind_of Module, Bukkits/
+ assert_file "bin/test"
+ assert_no_file "bin/rails"
+ end
+
+ def test_generating_in_full_mode_with_almost_of_all_skip_options
+ run_generator [destination_root, "--full", "-M", "-O", "-C", "-S", "-T", "--skip-active-storage"]
+ assert_file "bin/rails" do |content|
+ assert_no_match(/\s+require\s+["']rails\/all["']/, content)
+ end
+ assert_file "bin/rails", /#\s+require\s+["']active_record\/railtie["']/
+ assert_file "bin/rails", /#\s+require\s+["']active_storage\/engine["']/
+ assert_file "bin/rails", /#\s+require\s+["']action_mailer\/railtie["']/
+ assert_file "bin/rails", /#\s+require\s+["']action_cable\/engine["']/
+ assert_file "bin/rails", /#\s+require\s+["']sprockets\/railtie["']/
+ assert_file "bin/rails", /#\s+require\s+["']rails\/test_unit\/railtie["']/
end
def test_generating_test_files_in_full_mode
@@ -76,8 +101,13 @@ class PluginGeneratorTest < Rails::Generators::TestCase
assert_file "test/integration/navigation_test.rb", /ActionDispatch::IntegrationTest/
end
+ def test_inclusion_of_git_source
+ run_generator [destination_root]
+ assert_file "Gemfile", /git_source/
+ end
+
def test_inclusion_of_a_debugger
- run_generator [destination_root, '--full']
+ run_generator [destination_root, "--full"]
if defined?(JRUBY_VERSION) || RUBY_ENGINE == "rbx"
assert_file "Gemfile" do |content|
assert_no_match(/byebug/, content)
@@ -91,25 +121,25 @@ class PluginGeneratorTest < Rails::Generators::TestCase
run_generator [destination_root, "-T", "--full"]
assert_no_directory "test/integration/"
- assert_no_file "test"
+ assert_no_directory "test"
assert_file "Rakefile" do |contents|
assert_no_match(/APP_RAKEFILE/, contents)
end
+ assert_file "bin/rails" do |contents|
+ assert_no_match(/APP_PATH/, contents)
+ end
end
def test_generating_adds_dummy_app_rake_tasks_without_unit_test_files
- run_generator [destination_root, "-T", "--mountable", '--dummy-path', 'my_dummy_app']
+ run_generator [destination_root, "-T", "--mountable", "--dummy-path", "my_dummy_app"]
assert_file "Rakefile", /APP_RAKEFILE/
+ assert_file "bin/rails", /APP_PATH/
end
def test_generating_adds_dummy_app_without_javascript_and_assets_deps
run_generator
assert_file "test/dummy/app/assets/stylesheets/application.css"
-
- assert_file "test/dummy/app/assets/javascripts/application.js" do |contents|
- assert_no_match(/jquery/, contents)
- end
end
def test_ensure_that_plugin_options_are_not_passed_to_app_generator
@@ -120,8 +150,8 @@ class PluginGeneratorTest < Rails::Generators::TestCase
def test_ensure_that_test_dummy_can_be_generated_from_a_template
FileUtils.cd(Rails.root)
run_generator([destination_root, "-m", "lib/create_test_dummy_template.rb", "--skip-test"])
- assert_file "spec/dummy"
- assert_no_file "test"
+ assert_directory "spec/dummy"
+ assert_no_directory "test"
end
def test_database_entry_is_generated_for_sqlite3_by_default_in_full_mode
@@ -140,78 +170,47 @@ class PluginGeneratorTest < Rails::Generators::TestCase
run_generator [destination_root, "--skip-active-record"]
assert_file "bukkits.gemspec" do |contents|
- assert_no_match(/s.add_development_dependency "sqlite3"/, contents)
+ assert_no_match(/s\.add_development_dependency "sqlite3"/, contents)
end
end
- def test_app_generator_without_skips
- run_generator
- assert_file "test/dummy/config/application.rb", /\s+require\s+["']rails\/all["']/
- assert_file "test/dummy/config/environments/development.rb" do |content|
- assert_match(/config\.action_mailer\.raise_delivery_errors = false/, content)
- end
- assert_file "test/dummy/config/environments/test.rb" do |content|
- assert_match(/config\.action_mailer\.delivery_method = :test/, content)
- end
- assert_file "test/dummy/config/environments/production.rb" do |content|
- assert_match(/# config\.action_mailer\.raise_delivery_errors = false/, content)
- end
- end
-
- def test_active_record_is_removed_from_frameworks_if_skip_active_record_is_given
- run_generator [destination_root, "--skip-active-record"]
- assert_file "test/dummy/config/application.rb", /#\s+require\s+["']active_record\/railtie["']/
- end
-
def test_ensure_that_skip_active_record_option_is_passed_to_app_generator
run_generator [destination_root, "--skip_active_record"]
- assert_no_file "test/dummy/config/database.yml"
assert_file "test/test_helper.rb" do |contents|
assert_no_match(/ActiveRecord/, contents)
end
end
- def test_action_mailer_is_removed_from_frameworks_if_skip_action_mailer_is_given
- run_generator [destination_root, "--skip-action-mailer"]
- assert_file "test/dummy/config/application.rb", /#\s+require\s+["']action_mailer\/railtie["']/
- assert_file "test/dummy/config/environments/development.rb" do |content|
- assert_no_match(/config\.action_mailer/, content)
- end
- assert_file "test/dummy/config/environments/test.rb" do |content|
- assert_no_match(/config\.action_mailer/, content)
- end
- assert_file "test/dummy/config/environments/production.rb" do |content|
- assert_no_match(/config\.action_mailer/, content)
- end
- end
-
def test_ensure_that_database_option_is_passed_to_app_generator
run_generator [destination_root, "--database", "postgresql"]
assert_file "test/dummy/config/database.yml", /postgres/
end
- def test_generation_runs_bundle_install_with_full_and_mountable
- result = run_generator [destination_root, "--mountable", "--full", "--dev"]
- assert_match(/run bundle install/, result)
- assert $?.success?, "Command failed: #{result}"
- assert_file "#{destination_root}/Gemfile.lock" do |contents|
- assert_match(/bukkits/, contents)
- end
+ def test_generation_runs_bundle_install
+ assert_generates_without_bundler
end
- def test_skipping_javascripts_without_mountable_option
- run_generator
- assert_no_file "app/assets/javascripts/bukkits/application.js"
+ def test_dev_option
+ assert_generates_without_bundler(dev: true)
+ rails_path = File.expand_path("../../..", Rails.root)
+ assert_file "Gemfile", /^gem\s+["']rails["'],\s+path:\s+["']#{Regexp.escape(rails_path)}["']$/
end
- def test_javascripts_generation
- run_generator [destination_root, "--mountable"]
- assert_file "app/assets/javascripts/bukkits/application.js"
+ def test_edge_option
+ assert_generates_without_bundler(edge: true)
+ assert_file "Gemfile", %r{^gem\s+["']rails["'],\s+github:\s+["']#{Regexp.escape("rails/rails")}["']$}
+ end
+
+ def test_generation_does_not_run_bundle_install_with_full_and_mountable
+ assert_generates_without_bundler(mountable: true, full: true, dev: true)
+ assert_no_file "#{destination_root}/Gemfile.lock"
end
- def test_skip_javascripts
+ def test_skip_javascript
run_generator [destination_root, "--skip-javascript", "--mountable"]
- assert_no_file "app/assets/javascripts/bukkits/application.js"
+ assert_file "app/views/layouts/bukkits/application.html.erb" do |content|
+ assert_no_match "javascript_pack_tag", content
+ end
end
def test_template_from_dir_pwd
@@ -222,28 +221,27 @@ class PluginGeneratorTest < Rails::Generators::TestCase
def test_ensure_that_tests_work
run_generator
FileUtils.cd destination_root
- quietly { system 'bundle install' }
- assert_match(/1 runs, 1 assertions, 0 failures, 0 errors/, `bundle exec rake test 2>&1`)
+ quietly { system "bundle install" }
+ assert_match(/1 runs, 1 assertions, 0 failures, 0 errors/, `bin/test 2>&1`)
end
def test_ensure_that_tests_works_in_full_mode
run_generator [destination_root, "--full", "--skip_active_record"]
FileUtils.cd destination_root
- quietly { system 'bundle install' }
+ quietly { system "bundle install" }
assert_match(/1 runs, 1 assertions, 0 failures, 0 errors/, `bundle exec rake test 2>&1`)
end
def test_ensure_that_migration_tasks_work_with_mountable_option
run_generator [destination_root, "--mountable"]
FileUtils.cd destination_root
- quietly { system 'bundle install' }
- output = `bundle exec rake db:migrate 2>&1`
+ quietly { system "bundle install" }
+ output = `bin/rails db:migrate 2>&1`
assert $?.success?, "Command failed: #{output}"
end
def test_creating_engine_in_full_mode
run_generator [destination_root, "--full"]
- assert_file "app/assets/javascripts/bukkits"
assert_file "app/assets/stylesheets/bukkits"
assert_file "app/assets/images/bukkits"
assert_file "app/models"
@@ -251,7 +249,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase
assert_file "app/views"
assert_file "app/helpers"
assert_file "app/mailers"
- assert_file "bin/rails"
+ assert_file "bin/rails", /\s+require\s+["']rails\/all["']/
assert_file "config/routes.rb", /Rails.application.routes.draw do/
assert_file "lib/bukkits/engine.rb", /module Bukkits\n class Engine < ::Rails::Engine\n end\nend/
assert_file "lib/bukkits.rb", /require "bukkits\/engine"/
@@ -259,7 +257,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase
def test_creating_engine_with_hyphenated_name_in_full_mode
run_generator [File.join(destination_root, "hyphenated-name"), "--full"]
- assert_file "hyphenated-name/app/assets/javascripts/hyphenated/name"
+ assert_no_file "hyphenated-name/app/assets/javascripts/hyphenated/name"
assert_file "hyphenated-name/app/assets/stylesheets/hyphenated/name"
assert_file "hyphenated-name/app/assets/images/hyphenated/name"
assert_file "hyphenated-name/app/models"
@@ -271,12 +269,12 @@ class PluginGeneratorTest < Rails::Generators::TestCase
assert_file "hyphenated-name/config/routes.rb", /Rails.application.routes.draw do/
assert_file "hyphenated-name/lib/hyphenated/name/engine.rb", /module Hyphenated\n module Name\n class Engine < ::Rails::Engine\n end\n end\nend/
assert_file "hyphenated-name/lib/hyphenated/name.rb", /require "hyphenated\/name\/engine"/
- assert_file "hyphenated-name/bin/rails", /\.\.\/\.\.\/lib\/hyphenated\/name\/engine/
+ assert_file "hyphenated-name/bin/rails", /\.\.\/lib\/hyphenated\/name\/engine/
end
def test_creating_engine_with_hyphenated_and_underscored_name_in_full_mode
run_generator [File.join(destination_root, "my_hyphenated-name"), "--full"]
- assert_file "my_hyphenated-name/app/assets/javascripts/my_hyphenated/name"
+ assert_no_file "my_hyphenated-name/app/assets/javascripts/my_hyphenated/name"
assert_file "my_hyphenated-name/app/assets/stylesheets/my_hyphenated/name"
assert_file "my_hyphenated-name/app/assets/images/my_hyphenated/name"
assert_file "my_hyphenated-name/app/models"
@@ -285,106 +283,124 @@ class PluginGeneratorTest < Rails::Generators::TestCase
assert_file "my_hyphenated-name/app/helpers"
assert_file "my_hyphenated-name/app/mailers"
assert_file "my_hyphenated-name/bin/rails"
- assert_file "my_hyphenated-name/config/routes.rb", /Rails.application.routes.draw do/
+ assert_file "my_hyphenated-name/config/routes.rb", /Rails\.application\.routes\.draw do/
assert_file "my_hyphenated-name/lib/my_hyphenated/name/engine.rb", /module MyHyphenated\n module Name\n class Engine < ::Rails::Engine\n end\n end\nend/
assert_file "my_hyphenated-name/lib/my_hyphenated/name.rb", /require "my_hyphenated\/name\/engine"/
- assert_file "my_hyphenated-name/bin/rails", /\.\.\/\.\.\/lib\/my_hyphenated\/name\/engine/
+ assert_file "my_hyphenated-name/bin/rails", /\.\.\/lib\/my_hyphenated\/name\/engine/
end
def test_being_quiet_while_creating_dummy_application
- assert_no_match(/create\s+config\/application.rb/, run_generator)
+ assert_no_match(/create\s+config\/application\.rb/, run_generator)
end
def test_create_mountable_application_with_mountable_option
run_generator [destination_root, "--mountable"]
- assert_file "app/assets/javascripts/bukkits"
+ assert_no_file "app/assets/javascripts/bukkits"
assert_file "app/assets/stylesheets/bukkits"
assert_file "app/assets/images/bukkits"
- assert_file "config/routes.rb", /Bukkits::Engine.routes.draw do/
+ assert_file "config/routes.rb", /Bukkits::Engine\.routes\.draw do/
assert_file "lib/bukkits/engine.rb", /isolate_namespace Bukkits/
assert_file "test/dummy/config/routes.rb", /mount Bukkits::Engine => "\/bukkits"/
assert_file "app/controllers/bukkits/application_controller.rb", /module Bukkits\n class ApplicationController < ActionController::Base/
+ assert_file "app/models/bukkits/application_record.rb", /module Bukkits\n class ApplicationRecord < ActiveRecord::Base/
+ assert_file "app/jobs/bukkits/application_job.rb", /module Bukkits\n class ApplicationJob < ActiveJob::Base/
+ assert_file "app/mailers/bukkits/application_mailer.rb", /module Bukkits\n class ApplicationMailer < ActionMailer::Base\n default from: 'from@example\.com'\n layout 'mailer'\n/
assert_file "app/helpers/bukkits/application_helper.rb", /module Bukkits\n module ApplicationHelper/
assert_file "app/views/layouts/bukkits/application.html.erb" do |contents|
assert_match "<title>Bukkits</title>", contents
+ assert_match "<%= csrf_meta_tags %>", contents
+ assert_match "<%= csp_meta_tag %>", contents
assert_match(/stylesheet_link_tag\s+['"]bukkits\/application['"]/, contents)
- assert_match(/javascript_include_tag\s+['"]bukkits\/application['"]/, contents)
+ assert_no_match(/javascript_include_tag\s+['"]bukkits\/application['"]/, contents)
+ assert_match "<%= yield %>", contents
end
assert_file "test/test_helper.rb" do |content|
assert_match(/ActiveRecord::Migrator\.migrations_paths.+\.\.\/test\/dummy\/db\/migrate/, content)
assert_match(/ActiveRecord::Migrator\.migrations_paths.+<<.+\.\.\/db\/migrate/, content)
+ assert_match(/ActionDispatch::IntegrationTest\.fixture_path = ActiveSupport::TestCase\.fixture_pat/, content)
+ assert_no_match(/Rails::TestUnitReporter\.executable = 'bin\/test'/, content)
end
+ assert_no_file "bin/test"
end
def test_create_mountable_application_with_mountable_option_and_hypenated_name
run_generator [File.join(destination_root, "hyphenated-name"), "--mountable"]
- assert_file "hyphenated-name/app/assets/javascripts/hyphenated/name"
+ assert_no_file "hyphenated-name/app/assets/javascripts/hyphenated/name"
assert_file "hyphenated-name/app/assets/stylesheets/hyphenated/name"
assert_file "hyphenated-name/app/assets/images/hyphenated/name"
- assert_file "hyphenated-name/config/routes.rb", /Hyphenated::Name::Engine.routes.draw do/
- assert_file "hyphenated-name/lib/hyphenated/name/version.rb", /module Hyphenated\n module Name\n VERSION = "0.0.1"\n end\nend/
+ assert_file "hyphenated-name/config/routes.rb", /Hyphenated::Name::Engine\.routes\.draw do/
+ assert_file "hyphenated-name/lib/hyphenated/name/version.rb", /module Hyphenated\n module Name\n VERSION = '0\.1\.0'\n end\nend/
assert_file "hyphenated-name/lib/hyphenated/name/engine.rb", /module Hyphenated\n module Name\n class Engine < ::Rails::Engine\n isolate_namespace Hyphenated::Name\n end\n end\nend/
assert_file "hyphenated-name/lib/hyphenated/name.rb", /require "hyphenated\/name\/engine"/
assert_file "hyphenated-name/test/dummy/config/routes.rb", /mount Hyphenated::Name::Engine => "\/hyphenated-name"/
- assert_file "hyphenated-name/app/controllers/hyphenated/name/application_controller.rb", /module Hyphenated\n module Name\n class ApplicationController < ActionController::Base\n end\n end\nend/
+ assert_file "hyphenated-name/app/controllers/hyphenated/name/application_controller.rb", /module Hyphenated\n module Name\n class ApplicationController < ActionController::Base\n protect_from_forgery with: :exception\n end\n end\nend\n/
+ assert_file "hyphenated-name/app/models/hyphenated/name/application_record.rb", /module Hyphenated\n module Name\n class ApplicationRecord < ActiveRecord::Base\n self\.abstract_class = true\n end\n end\nend/
+ assert_file "hyphenated-name/app/jobs/hyphenated/name/application_job.rb", /module Hyphenated\n module Name\n class ApplicationJob < ActiveJob::Base/
+ assert_file "hyphenated-name/app/mailers/hyphenated/name/application_mailer.rb", /module Hyphenated\n module Name\n class ApplicationMailer < ActionMailer::Base\n default from: 'from@example\.com'\n layout 'mailer'\n end\n end\nend/
assert_file "hyphenated-name/app/helpers/hyphenated/name/application_helper.rb", /module Hyphenated\n module Name\n module ApplicationHelper\n end\n end\nend/
assert_file "hyphenated-name/app/views/layouts/hyphenated/name/application.html.erb" do |contents|
assert_match "<title>Hyphenated name</title>", contents
assert_match(/stylesheet_link_tag\s+['"]hyphenated\/name\/application['"]/, contents)
- assert_match(/javascript_include_tag\s+['"]hyphenated\/name\/application['"]/, contents)
+ assert_no_match(/javascript_include_tag\s+['"]hyphenated\/name\/application['"]/, contents)
end
end
def test_create_mountable_application_with_mountable_option_and_hypenated_and_underscored_name
run_generator [File.join(destination_root, "my_hyphenated-name"), "--mountable"]
- assert_file "my_hyphenated-name/app/assets/javascripts/my_hyphenated/name"
+ assert_no_file "my_hyphenated-name/app/assets/javascripts/my_hyphenated/name"
assert_file "my_hyphenated-name/app/assets/stylesheets/my_hyphenated/name"
assert_file "my_hyphenated-name/app/assets/images/my_hyphenated/name"
- assert_file "my_hyphenated-name/config/routes.rb", /MyHyphenated::Name::Engine.routes.draw do/
- assert_file "my_hyphenated-name/lib/my_hyphenated/name/version.rb", /module MyHyphenated\n module Name\n VERSION = "0.0.1"\n end\nend/
+ assert_file "my_hyphenated-name/config/routes.rb", /MyHyphenated::Name::Engine\.routes\.draw do/
+ assert_file "my_hyphenated-name/lib/my_hyphenated/name/version.rb", /module MyHyphenated\n module Name\n VERSION = '0\.1\.0'\n end\nend/
assert_file "my_hyphenated-name/lib/my_hyphenated/name/engine.rb", /module MyHyphenated\n module Name\n class Engine < ::Rails::Engine\n isolate_namespace MyHyphenated::Name\n end\n end\nend/
assert_file "my_hyphenated-name/lib/my_hyphenated/name.rb", /require "my_hyphenated\/name\/engine"/
assert_file "my_hyphenated-name/test/dummy/config/routes.rb", /mount MyHyphenated::Name::Engine => "\/my_hyphenated-name"/
- assert_file "my_hyphenated-name/app/controllers/my_hyphenated/name/application_controller.rb", /module MyHyphenated\n module Name\n class ApplicationController < ActionController::Base\n end\n end\nend/
+ assert_file "my_hyphenated-name/app/controllers/my_hyphenated/name/application_controller.rb", /module MyHyphenated\n module Name\n class ApplicationController < ActionController::Base\n protect_from_forgery with: :exception\n end\n end\nend\n/
+ assert_file "my_hyphenated-name/app/models/my_hyphenated/name/application_record.rb", /module MyHyphenated\n module Name\n class ApplicationRecord < ActiveRecord::Base\n self\.abstract_class = true\n end\n end\nend/
+ assert_file "my_hyphenated-name/app/jobs/my_hyphenated/name/application_job.rb", /module MyHyphenated\n module Name\n class ApplicationJob < ActiveJob::Base/
+ assert_file "my_hyphenated-name/app/mailers/my_hyphenated/name/application_mailer.rb", /module MyHyphenated\n module Name\n class ApplicationMailer < ActionMailer::Base\n default from: 'from@example\.com'\n layout 'mailer'\n end\n end\nend/
assert_file "my_hyphenated-name/app/helpers/my_hyphenated/name/application_helper.rb", /module MyHyphenated\n module Name\n module ApplicationHelper\n end\n end\nend/
assert_file "my_hyphenated-name/app/views/layouts/my_hyphenated/name/application.html.erb" do |contents|
assert_match "<title>My hyphenated name</title>", contents
assert_match(/stylesheet_link_tag\s+['"]my_hyphenated\/name\/application['"]/, contents)
- assert_match(/javascript_include_tag\s+['"]my_hyphenated\/name\/application['"]/, contents)
+ assert_no_match(/javascript_include_tag\s+['"]my_hyphenated\/name\/application['"]/, contents)
end
end
def test_create_mountable_application_with_mountable_option_and_multiple_hypenates_in_name
run_generator [File.join(destination_root, "deep-hyphenated-name"), "--mountable"]
- assert_file "deep-hyphenated-name/app/assets/javascripts/deep/hyphenated/name"
+ assert_no_file "deep-hyphenated-name/app/assets/javascripts/deep/hyphenated/name"
assert_file "deep-hyphenated-name/app/assets/stylesheets/deep/hyphenated/name"
assert_file "deep-hyphenated-name/app/assets/images/deep/hyphenated/name"
- assert_file "deep-hyphenated-name/config/routes.rb", /Deep::Hyphenated::Name::Engine.routes.draw do/
- assert_file "deep-hyphenated-name/lib/deep/hyphenated/name/version.rb", /module Deep\n module Hyphenated\n module Name\n VERSION = "0.0.1"\n end\n end\nend/
+ assert_file "deep-hyphenated-name/config/routes.rb", /Deep::Hyphenated::Name::Engine\.routes\.draw do/
+ assert_file "deep-hyphenated-name/lib/deep/hyphenated/name/version.rb", /module Deep\n module Hyphenated\n module Name\n VERSION = '0\.1\.0'\n end\n end\nend/
assert_file "deep-hyphenated-name/lib/deep/hyphenated/name/engine.rb", /module Deep\n module Hyphenated\n module Name\n class Engine < ::Rails::Engine\n isolate_namespace Deep::Hyphenated::Name\n end\n end\n end\nend/
assert_file "deep-hyphenated-name/lib/deep/hyphenated/name.rb", /require "deep\/hyphenated\/name\/engine"/
assert_file "deep-hyphenated-name/test/dummy/config/routes.rb", /mount Deep::Hyphenated::Name::Engine => "\/deep-hyphenated-name"/
- assert_file "deep-hyphenated-name/app/controllers/deep/hyphenated/name/application_controller.rb", /module Deep\n module Hyphenated\n module Name\n class ApplicationController < ActionController::Base\n end\n end\n end\nend/
+ assert_file "deep-hyphenated-name/app/controllers/deep/hyphenated/name/application_controller.rb", /module Deep\n module Hyphenated\n module Name\n class ApplicationController < ActionController::Base\n protect_from_forgery with: :exception\n end\n end\n end\nend\n/
+ assert_file "deep-hyphenated-name/app/models/deep/hyphenated/name/application_record.rb", /module Deep\n module Hyphenated\n module Name\n class ApplicationRecord < ActiveRecord::Base\n self\.abstract_class = true\n end\n end\n end\nend/
+ assert_file "deep-hyphenated-name/app/jobs/deep/hyphenated/name/application_job.rb", /module Deep\n module Hyphenated\n module Name\n class ApplicationJob < ActiveJob::Base/
+ assert_file "deep-hyphenated-name/app/mailers/deep/hyphenated/name/application_mailer.rb", /module Deep\n module Hyphenated\n module Name\n class ApplicationMailer < ActionMailer::Base\n default from: 'from@example\.com'\n layout 'mailer'\n end\n end\n end\nend/
assert_file "deep-hyphenated-name/app/helpers/deep/hyphenated/name/application_helper.rb", /module Deep\n module Hyphenated\n module Name\n module ApplicationHelper\n end\n end\n end\nend/
assert_file "deep-hyphenated-name/app/views/layouts/deep/hyphenated/name/application.html.erb" do |contents|
assert_match "<title>Deep hyphenated name</title>", contents
assert_match(/stylesheet_link_tag\s+['"]deep\/hyphenated\/name\/application['"]/, contents)
- assert_match(/javascript_include_tag\s+['"]deep\/hyphenated\/name\/application['"]/, contents)
+ assert_no_match(/javascript_include_tag\s+['"]deep\/hyphenated\/name\/application['"]/, contents)
end
end
def test_creating_gemspec
run_generator
- assert_file "bukkits.gemspec", /s.name\s+= "bukkits"/
- assert_file "bukkits.gemspec", /s.files = Dir\["\{app,config,db,lib\}\/\*\*\/\*", "MIT-LICENSE", "Rakefile", "README\.rdoc"\]/
- assert_file "bukkits.gemspec", /s.test_files = Dir\["test\/\*\*\/\*"\]/
- assert_file "bukkits.gemspec", /s.version\s+ = Bukkits::VERSION/
+ assert_file "bukkits.gemspec", /spec\.name\s+= "bukkits"/
+ assert_file "bukkits.gemspec", /spec\.files = Dir\["\{app,config,db,lib\}\/\*\*\/\*", "MIT-LICENSE", "Rakefile", "README\.md"\]/
+ assert_file "bukkits.gemspec", /spec\.version\s+ = Bukkits::VERSION/
end
def test_usage_of_engine_commands
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", /ENGINE_PATH = File\.expand_path\('\.\.\/lib\/bukkits\/engine', __dir__\)/
+ assert_file "bin/rails", /ENGINE_ROOT = File\.expand_path\('\.\.', __dir__\)/
+ assert_file "bin/rails", %r|APP_PATH = File\.expand_path\('\.\./test/dummy/config/application', __dir__\)|
assert_file "bin/rails", /require 'rails\/all'/
assert_file "bin/rails", /require 'rails\/engine\/commands'/
end
@@ -400,7 +416,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase
assert_file "spec/dummy/config/application.rb"
assert_no_file "test/dummy"
assert_file "test/test_helper.rb" do |content|
- assert_match(/require.+spec\/dummy\/config\/environment/, content)
+ assert_match(/require_relative.+spec\/dummy\/config\/environment/, content)
assert_match(/ActiveRecord::Migrator\.migrations_paths.+spec\/dummy\/db\/migrate/, content)
end
end
@@ -411,22 +427,29 @@ class PluginGeneratorTest < Rails::Generators::TestCase
assert_file "spec/fake/config/application.rb"
assert_no_file "test/dummy"
assert_file "test/test_helper.rb" do |content|
- assert_match(/require.+spec\/fake\/config\/environment/, content)
+ assert_match(/require_relative.+spec\/fake\/config\/environment/, content)
assert_match(/ActiveRecord::Migrator\.migrations_paths.+spec\/fake\/db\/migrate/, content)
end
end
def test_creating_dummy_without_tests_but_with_dummy_path
run_generator [destination_root, "--dummy_path", "spec/dummy", "--skip-test"]
- assert_file "spec/dummy"
- assert_file "spec/dummy/config/application.rb"
- assert_no_file "test"
- assert_no_file "test/test_helper.rb"
- assert_file '.gitignore' do |contents|
+ assert_directory "spec/dummy"
+ assert_file "spec/dummy/config/application.rb", /#\s+require\s+["']rails\/test_unit\/railtie["']/
+ assert_no_directory "test"
+ assert_file ".gitignore" do |contents|
assert_match(/spec\/dummy/, contents)
end
end
+ def test_dummy_appplication_skip_listen_by_default
+ run_generator
+
+ assert_file "test/dummy/config/environments/development.rb" do |contents|
+ assert_match(/^\s*# config\.file_watcher = ActiveSupport::EventedFileUpdateChecker/, contents)
+ end
+ end
+
def test_ensure_that_gitignore_can_be_generated_from_a_template_for_dummy_path
FileUtils.cd(Rails.root)
run_generator([destination_root, "--dummy_path", "spec/dummy", "--skip-test"])
@@ -435,14 +458,26 @@ class PluginGeneratorTest < Rails::Generators::TestCase
end
end
+ def test_unnecessary_files_are_not_generated_in_dummy_application
+ run_generator
+ assert_no_file "test/dummy/.gitignore"
+ assert_no_file "test/dummy/db/seeds.rb"
+ assert_no_file "test/dummy/Gemfile"
+ assert_no_file "test/dummy/public/robots.txt"
+ assert_no_file "test/dummy/README.md"
+ assert_no_file "test/dummy/config/master.key"
+ assert_no_file "test/dummy/config/credentials.yml.enc"
+ assert_no_directory "test/dummy/lib/tasks"
+ assert_no_directory "test/dummy/test"
+ assert_no_directory "test/dummy/vendor"
+ assert_no_directory "test/dummy/.git"
+ end
+
def test_skipping_test_files
run_generator [destination_root, "--skip-test"]
- assert_no_file "test"
- assert_file "bukkits.gemspec" do |contents|
- assert_no_match(/s.test_files = Dir\["test\/\*\*\/\*"\]/, contents)
- end
- assert_file '.gitignore' do |contents|
- assert_no_match(/test\dummy/, contents)
+ assert_no_directory "test"
+ assert_file ".gitignore" do |contents|
+ assert_no_match(/test\/dummy/, contents)
end
end
@@ -450,10 +485,9 @@ class PluginGeneratorTest < Rails::Generators::TestCase
run_generator [destination_root, "--skip-gemspec"]
assert_no_file "bukkits.gemspec"
assert_file "Gemfile" do |contents|
- assert_no_match('gemspec', contents)
- assert_match(/gem 'rails', '~> #{Rails.version}'/, contents)
+ assert_no_match("gemspec", contents)
+ assert_match(/gem 'rails'/, contents)
assert_match_sqlite3(contents)
- assert_no_match(/# gem "jquery-rails"/, contents)
end
end
@@ -461,8 +495,8 @@ class PluginGeneratorTest < Rails::Generators::TestCase
run_generator [destination_root, "--skip-gemspec", "--full"]
assert_no_file "bukkits.gemspec"
assert_file "Gemfile" do |contents|
- assert_no_match('gemspec', contents)
- assert_match(/gem 'rails', '~> #{Rails.version}'/, contents)
+ assert_no_match("gemspec", contents)
+ assert_match(/gem 'rails'/, contents)
assert_match_sqlite3(contents)
end
end
@@ -470,21 +504,37 @@ class PluginGeneratorTest < Rails::Generators::TestCase
def test_creating_plugin_in_app_directory_adds_gemfile_entry
# simulate application existence
gemfile_path = "#{Rails.root}/Gemfile"
- Object.const_set('APP_PATH', Rails.root)
+ Object.const_set("APP_PATH", Rails.root)
FileUtils.touch gemfile_path
+ File.write(gemfile_path, "#foo")
run_generator
- assert_file gemfile_path, /gem 'bukkits', path: 'tmp\/bukkits'/
+ assert_file gemfile_path, /^gem 'bukkits', path: 'tmp\/bukkits'/
+ ensure
+ Object.send(:remove_const, "APP_PATH")
+ FileUtils.rm gemfile_path
+ 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')
+ Object.send(:remove_const, "APP_PATH")
FileUtils.rm gemfile_path
end
def test_skipping_gemfile_entry
# simulate application existence
gemfile_path = "#{Rails.root}/Gemfile"
- Object.const_set('APP_PATH', Rails.root)
+ Object.const_set("APP_PATH", Rails.root)
FileUtils.touch gemfile_path
run_generator [destination_root, "--skip-gemfile-entry"]
@@ -493,7 +543,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase
assert_no_match(/gem 'bukkits', path: 'tmp\/bukkits'/, contents)
end
ensure
- Object.send(:remove_const, 'APP_PATH')
+ Object.send(:remove_const, "APP_PATH")
FileUtils.rm gemfile_path
end
@@ -534,7 +584,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase
name = "TODO: Write your name"
email = "TODO: Write your email address"
- run_generator [destination_root, '--skip-git']
+ run_generator [destination_root, "--skip-git"]
assert_file "MIT-LICENSE" do |contents|
assert_match name, contents
end
@@ -544,20 +594,188 @@ class PluginGeneratorTest < Rails::Generators::TestCase
end
end
-protected
- def action(*args, &block)
- silence(:stdout){ generator.send(*args, &block) }
+ def test_skipping_useless_folders_generation_for_api_engines
+ ["--full", "--mountable"].each do |option|
+ run_generator [destination_root, option, "--api"]
+
+ assert_no_directory "app/assets"
+ assert_no_directory "app/helpers"
+ assert_no_directory "app/views"
+
+ FileUtils.rm_rf destination_root
+ end
end
- def default_files
- ::DEFAULT_PLUGIN_FILES
+ def test_application_controller_parent_for_mountable_api_plugins
+ run_generator [destination_root, "--mountable", "--api"]
+
+ assert_file "app/controllers/bukkits/application_controller.rb" do |content|
+ assert_match "ApplicationController < ActionController::API", content
+ end
end
- def assert_match_sqlite3(contents)
- if defined?(JRUBY_VERSION)
- assert_match(/group :development do\n gem 'activerecord-jdbcsqlite3-adapter'\nend/, contents)
- else
- assert_match(/group :development do\n gem 'sqlite3'\nend/, contents)
+ def test_dummy_api_application_for_api_plugins
+ run_generator [destination_root, "--api"]
+
+ assert_file "test/dummy/config/application.rb" do |content|
+ assert_match "config.api_only = true", content
+ end
+ end
+
+ def test_api_generators_configuration_for_api_engines
+ run_generator [destination_root, "--full", "--api"]
+
+ assert_file "lib/bukkits/engine.rb" do |content|
+ assert_match "config.generators.api_only = true", content
end
end
+
+ def test_scaffold_generator_for_mountable_api_plugins
+ run_generator [destination_root, "--mountable", "--api"]
+
+ capture(:stdout) do
+ `#{destination_root}/bin/rails g scaffold article`
+ end
+
+ assert_file "app/models/bukkits/article.rb"
+ assert_file "app/controllers/bukkits/articles_controller.rb" do |content|
+ assert_match "only: [:show, :update, :destroy]", content
+ end
+
+ assert_no_directory "app/assets"
+ assert_no_directory "app/helpers"
+ assert_no_directory "app/views"
+ end
+
+ def test_model_with_existent_application_record_in_mountable_engine
+ run_generator [destination_root, "--mountable"]
+ capture(:stdout) do
+ `#{destination_root}/bin/rails g model article`
+ end
+
+ assert_file "app/models/bukkits/article.rb", /class Article < ApplicationRecord/
+ end
+
+ def test_generate_application_mailer_when_does_not_exist_in_mountable_engine
+ run_generator [destination_root, "--mountable"]
+ FileUtils.rm "#{destination_root}/app/mailers/bukkits/application_mailer.rb"
+ capture(:stdout) do
+ `#{destination_root}/bin/rails g mailer User`
+ end
+
+ assert_file "#{destination_root}/app/mailers/bukkits/application_mailer.rb" do |mailer|
+ assert_match(/module Bukkits/, mailer)
+ assert_match(/class ApplicationMailer < ActionMailer::Base/, mailer)
+ end
+ end
+
+ def test_generate_mailer_layouts_when_does_not_exist_in_mountable_engine
+ run_generator [destination_root, "--mountable"]
+ capture(:stdout) do
+ `#{destination_root}/bin/rails g mailer User`
+ end
+
+ assert_file "#{destination_root}/app/views/layouts/bukkits/mailer.text.erb" do |view|
+ assert_match(/<%= yield %>/, view)
+ end
+
+ assert_file "#{destination_root}/app/views/layouts/bukkits/mailer.html.erb" do |view|
+ assert_match(%r{<body>\n <%= yield %>\n </body>}, view)
+ end
+ end
+
+ def test_generate_application_job_when_does_not_exist_in_mountable_engine
+ run_generator [destination_root, "--mountable"]
+ FileUtils.rm "#{destination_root}/app/jobs/bukkits/application_job.rb"
+ capture(:stdout) do
+ `#{destination_root}/bin/rails g job refresh_counters`
+ end
+
+ assert_file "#{destination_root}/app/jobs/bukkits/application_job.rb" do |record|
+ assert_match(/module Bukkits/, record)
+ assert_match(/class ApplicationJob < ActiveJob::Base/, record)
+ end
+ end
+
+ def test_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', __dir__\)|, content)
+ end
+ ensure
+ Object.send(:remove_const, "ENGINE_ROOT")
+ end
+
+ def test_after_bundle_callback
+ path = "http://example.org/rails_template"
+ template = +%{ after_bundle { run "echo ran after_bundle" } }
+ template.instance_eval "def read; self; end" # Make the string respond to read
+
+ check_open = -> *args do
+ assert_equal [ path, "Accept" => "application/x-thor-template" ], args
+ template
+ end
+
+ sequence = ["echo ran after_bundle"]
+ @sequence_step ||= 0
+ ensure_bundler_first = -> command do
+ assert_equal sequence[@sequence_step], command, "commands should be called in sequence #{sequence}"
+ @sequence_step += 1
+ end
+
+ content = nil
+ generator([destination_root], template: path).stub(:open, check_open, template) do
+ generator.stub(:bundle_command, ensure_bundler_first) do
+ generator.stub(:run, ensure_bundler_first) do
+ silence_stream($stdout) do
+ content = capture(:stderr) { generator.invoke_all }
+ end
+ end
+ end
+ end
+
+ assert_equal 1, @sequence_step
+ assert_match(/DEPRECATION WARNING: `after_bundle` is deprecated/, content)
+ end
+
+ private
+
+ def action(*args, &block)
+ silence(:stdout) { generator.send(*args, &block) }
+ end
+
+ def default_files
+ ::DEFAULT_PLUGIN_FILES
+ end
+
+ def assert_match_sqlite3(contents)
+ if defined?(JRUBY_VERSION)
+ assert_match(/group :development do\n gem 'activerecord-jdbcsqlite3-adapter'\nend/, contents)
+ else
+ assert_match(/group :development do\n gem 'sqlite3'\nend/, contents)
+ end
+ end
+
+ def assert_generates_without_bundler(options = {})
+ generator([destination_root], options)
+
+ command_check = -> command do
+ case command
+ when "install"
+ flunk "install expected to not be called"
+ when "exec spring binstub --all"
+ # Called when running tests with spring, let through unscathed.
+ end
+ end
+
+ generator.stub :bundle_command, command_check do
+ quietly { generator.invoke_all }
+ end
+ end
end
diff --git a/railties/test/generators/plugin_test_helper.rb b/railties/test/generators/plugin_test_helper.rb
new file mode 100644
index 0000000000..528f8d88f9
--- /dev/null
+++ b/railties/test/generators/plugin_test_helper.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "tmpdir"
+
+module PluginTestHelper
+ def create_test_file(name, pass: true)
+ plugin_file "test/#{name}_test.rb", <<-RUBY
+ require 'test_helper'
+
+ class #{name.camelize}Test < ActiveSupport::TestCase
+ def test_truth
+ puts "#{name.camelize}Test"
+ assert #{pass}, 'wups!'
+ end
+ end
+ RUBY
+ end
+
+ def plugin_file(path, contents, mode: "w")
+ FileUtils.mkdir_p File.dirname("#{plugin_path}/#{path}")
+ File.open("#{plugin_path}/#{path}", mode) do |f|
+ f.puts contents
+ end
+ end
+end
diff --git a/railties/test/generators/plugin_test_runner_test.rb b/railties/test/generators/plugin_test_runner_test.rb
new file mode 100644
index 0000000000..89c3f1e496
--- /dev/null
+++ b/railties/test/generators/plugin_test_runner_test.rb
@@ -0,0 +1,122 @@
+# frozen_string_literal: true
+
+require "generators/plugin_test_helper"
+
+class PluginTestRunnerTest < ActiveSupport::TestCase
+ include PluginTestHelper
+
+ def setup
+ @destination_root = Dir.mktmpdir("bukkits")
+ Dir.chdir(@destination_root) { `bundle exec rails plugin new bukkits --skip-bundle` }
+ plugin_file "test/dummy/db/schema.rb", ""
+ end
+
+ def teardown
+ FileUtils.rm_rf(@destination_root)
+ end
+
+ def test_run_single_file
+ create_test_file "foo"
+ create_test_file "bar"
+ assert_match "1 runs, 1 assertions, 0 failures", run_test_command("test/foo_test.rb")
+ end
+
+ def test_run_multiple_files
+ create_test_file "foo"
+ create_test_file "bar"
+ assert_match "2 runs, 2 assertions, 0 failures", run_test_command("test/foo_test.rb test/bar_test.rb")
+ end
+
+ def test_mix_files_and_line_filters
+ create_test_file "account"
+ plugin_file "test/post_test.rb", <<-RUBY
+ require 'test_helper'
+
+ class PostTest < ActiveSupport::TestCase
+ def test_post
+ puts 'PostTest'
+ assert true
+ end
+
+ def test_line_filter_does_not_run_this
+ assert true
+ end
+ end
+ RUBY
+
+ run_test_command("test/account_test.rb test/post_test.rb:4").tap do |output|
+ assert_match "AccountTest", output
+ assert_match "PostTest", output
+ assert_match "2 runs, 2 assertions", output
+ end
+ end
+
+ def test_multiple_line_filters
+ create_test_file "account"
+ create_test_file "post"
+
+ run_test_command("test/account_test.rb:4 test/post_test.rb:4").tap do |output|
+ assert_match "AccountTest", output
+ assert_match "PostTest", output
+ end
+ end
+
+ def test_output_inline_by_default
+ create_test_file "post", pass: false
+
+ output = run_test_command("test/post_test.rb")
+ expect = %r{Running:\n\nPostTest\nF\n\nFailure:\nPostTest#test_truth \[[^\]]+test/post_test.rb:6\]:\nwups!\n\nbin/test (/private)?#{plugin_path}/test/post_test.rb:4}
+ assert_match expect, output
+ end
+
+ def test_only_inline_failure_output
+ create_test_file "post", pass: false
+
+ output = run_test_command("test/post_test.rb")
+ assert_match %r{Finished in.*\n1 runs, 1 assertions}, output
+ end
+
+ def test_fail_fast
+ create_test_file "post", pass: false
+
+ assert_match(/Interrupt/,
+ capture(:stderr) { run_test_command("test/post_test.rb --fail-fast") })
+ end
+
+ def test_raise_error_when_specified_file_does_not_exist
+ error = capture(:stderr) { run_test_command("test/not_exists.rb") }
+ assert_match(%r{cannot load such file.+test/not_exists\.rb}, error)
+ end
+
+ def test_executed_only_once
+ create_test_file "foo"
+ result = run_test_command("test/foo_test.rb")
+ 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
+
+ def test_run_rake_test
+ create_test_file "foo"
+ result = Dir.chdir(plugin_path) { `rake test TEST=test/foo_test.rb` }
+ assert_match "1 runs, 1 assertions, 0 failures", result
+ end
+
+ private
+ def plugin_path
+ "#{@destination_root}/bukkits"
+ end
+
+ def run_test_command(arguments)
+ Dir.chdir(plugin_path) { `bin/test #{arguments}` }
+ end
+end
diff --git a/railties/test/generators/resource_generator_test.rb b/railties/test/generators/resource_generator_test.rb
index 581d80d60e..b99b4baf6b 100644
--- a/railties/test/generators/resource_generator_test.rb
+++ b/railties/test/generators/resource_generator_test.rb
@@ -1,11 +1,17 @@
-require 'generators/generators_test_helper'
-require 'rails/generators/rails/resource/resource_generator'
+# frozen_string_literal: true
+
+require "generators/generators_test_helper"
+require "rails/generators/rails/resource/resource_generator"
class ResourceGeneratorTest < Rails::Generators::TestCase
include GeneratorsTestHelper
arguments %w(account)
- setup :copy_routes
+ def setup
+ super
+ copy_routes
+ Rails::Generators::ModelHelpers.skip_warn = false
+ end
def test_help_with_inherited_options
content = run_generator ["--help"]
@@ -33,7 +39,7 @@ class ResourceGeneratorTest < Rails::Generators::TestCase
def test_resource_controller_with_pluralized_class_name
run_generator
assert_file "app/controllers/accounts_controller.rb", /class AccountsController < ApplicationController/
- assert_file "test/controllers/accounts_controller_test.rb", /class AccountsControllerTest < ActionController::TestCase/
+ assert_file "test/controllers/accounts_controller_test.rb", /class AccountsControllerTest < ActionDispatch::IntegrationTest/
assert_file "app/helpers/accounts_helper.rb", /module AccountsHelper/
end
@@ -59,21 +65,21 @@ class ResourceGeneratorTest < Rails::Generators::TestCase
end
def test_plural_names_are_singularized
- content = run_generator ["accounts".freeze]
- assert_file "app/models/account.rb", /class Account < ActiveRecord::Base/
+ content = run_generator ["accounts"]
+ assert_file "app/models/account.rb", /class Account < ApplicationRecord/
assert_file "test/models/account_test.rb", /class AccountTest/
assert_match(/\[WARNING\] The model name 'accounts' was recognized as a plural, using the singular 'account' instead\. Override with --force-plural or setup custom inflection rules for this noun before running the generator\./, content)
end
def test_plural_names_can_be_forced
content = run_generator ["accounts", "--force-plural"]
- assert_file "app/models/accounts.rb", /class Accounts < ActiveRecord::Base/
+ assert_file "app/models/accounts.rb", /class Accounts < ApplicationRecord/
assert_file "test/models/accounts_test.rb", /class AccountsTest/
assert_no_match(/\[WARNING\]/, content)
end
def test_mass_nouns_do_not_throw_warnings
- content = run_generator ["sheep".freeze]
+ content = run_generator ["sheep"]
assert_no_match(/\[WARNING\]/, content)
end
diff --git a/railties/test/generators/scaffold_controller_generator_test.rb b/railties/test/generators/scaffold_controller_generator_test.rb
index 648b8d9325..fd5aa817b4 100644
--- a/railties/test/generators/scaffold_controller_generator_test.rb
+++ b/railties/test/generators/scaffold_controller_generator_test.rb
@@ -1,5 +1,7 @@
-require 'generators/generators_test_helper'
-require 'rails/generators/rails/scaffold_controller/scaffold_controller_generator'
+# frozen_string_literal: true
+
+require "generators/generators_test_helper"
+require "rails/generators/rails/scaffold_controller/scaffold_controller_generator"
module Unknown
module Generators
@@ -56,7 +58,7 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase
assert_file "app/controllers/users_controller.rb" do |content|
assert_match(/def user_params/, content)
- assert_match(/params\[:user\]/, content)
+ assert_match(/params\.fetch\(:user, \{\}\)/, content)
end
end
@@ -104,10 +106,10 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase
run_generator ["User", "name:string", "age:integer", "organization:references{polymorphic}"]
assert_file "test/controllers/users_controller_test.rb" do |content|
- assert_match(/class UsersControllerTest < ActionController::TestCase/, content)
+ assert_match(/class UsersControllerTest < ActionDispatch::IntegrationTest/, content)
assert_match(/test "should get index"/, content)
- assert_match(/post :create, params: \{ user: \{ age: @user\.age, name: @user\.name, organization_id: @user\.organization_id, organization_type: @user\.organization_type \} \}/, content)
- assert_match(/patch :update, params: \{ id: @user, user: \{ age: @user\.age, name: @user\.name, organization_id: @user\.organization_id, organization_type: @user\.organization_type \} \}/, content)
+ assert_match(/post users_url, params: \{ user: \{ age: @user\.age, name: @user\.name, organization_id: @user\.organization_id, organization_type: @user\.organization_type \} \}/, content)
+ assert_match(/patch user_url\(@user\), params: \{ user: \{ age: @user\.age, name: @user\.name, organization_id: @user\.organization_id, organization_type: @user\.organization_type \} \}/, content)
end
end
@@ -115,10 +117,10 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase
run_generator ["User"]
assert_file "test/controllers/users_controller_test.rb" do |content|
- assert_match(/class UsersControllerTest < ActionController::TestCase/, content)
+ assert_match(/class UsersControllerTest < ActionDispatch::IntegrationTest/, content)
assert_match(/test "should get index"/, content)
- assert_match(/post :create, params: \{ user: \{ \} \}/, content)
- assert_match(/patch :update, params: \{ id: @user, user: \{ \} \}/, content)
+ assert_match(/post users_url, params: \{ user: \{ \} \}/, content)
+ assert_match(/patch user_url\(@user\), params: \{ user: \{ \} \}/, content)
end
end
@@ -172,6 +174,29 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase
assert_instance_method :index, content do |m|
assert_match("@users = User.all", m)
end
+
+ assert_instance_method :create, content do |m|
+ assert_match("redirect_to [:admin, @user]", m)
+ end
+
+ assert_instance_method :update, content do |m|
+ assert_match("redirect_to [:admin, @user]", m)
+ end
+ end
+
+ assert_file "app/views/admin/users/index.html.erb" do |content|
+ assert_match("'Show', [:admin, user]", content)
+ assert_match("'Edit', edit_admin_user_path(user)", content)
+ assert_match("'Destroy', [:admin, user]", content)
+ assert_match("'New User', new_admin_user_path", content)
+ end
+
+ assert_file "app/views/admin/users/new.html.erb" do |content|
+ assert_match("'Back', admin_users_path", content)
+ end
+
+ assert_file "app/views/admin/users/_form.html.erb" do |content|
+ assert_match("model: [:admin, user]", content)
end
end
@@ -182,7 +207,8 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase
Dir.chdir(engine_path) do
quietly { `bin/rails g controller dashboard foo` }
- assert_match(/2 runs, 2 assertions, 0 failures, 0 errors/, `bundle exec rake test 2>&1`)
+ quietly { `bin/rails db:migrate RAILS_ENV=test` }
+ assert_match(/2 runs, 2 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`)
end
end
@@ -193,7 +219,8 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase
Dir.chdir(engine_path) do
quietly { `bin/rails g controller dashboard foo` }
- assert_match(/2 runs, 2 assertions, 0 failures, 0 errors/, `bundle exec rake test 2>&1`)
+ quietly { `bin/rails db:migrate RAILS_ENV=test` }
+ assert_match(/2 runs, 2 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`)
end
end
@@ -230,16 +257,22 @@ 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
run_generator ["User", "name:string", "age:integer", "organization:references{polymorphic}", "--api"]
assert_file "test/controllers/users_controller_test.rb" do |content|
- assert_match(/class UsersControllerTest < ActionController::TestCase/, content)
+ assert_match(/class UsersControllerTest < ActionDispatch::IntegrationTest/, content)
assert_match(/test "should get index"/, content)
- assert_match(/post :create, params: \{ user: \{ age: @user\.age, name: @user\.name, organization_id: @user\.organization_id, organization_type: @user\.organization_type \} \}/, content)
- assert_match(/patch :update, params: \{ id: @user, user: \{ age: @user\.age, name: @user\.name, organization_id: @user\.organization_id, organization_type: @user\.organization_type \} \}/, content)
+ assert_match(/post users_url, params: \{ user: \{ age: @user\.age, name: @user\.name, organization_id: @user\.organization_id, organization_type: @user\.organization_type \} \}, as: :json/, content)
+ assert_match(/patch user_url\(@user\), params: \{ user: \{ age: @user\.age, name: @user\.name, organization_id: @user\.organization_id, organization_type: @user\.organization_type \} \}, as: :json/, content)
assert_no_match(/assert_redirected_to/, content)
end
end
diff --git a/railties/test/generators/scaffold_generator_test.rb b/railties/test/generators/scaffold_generator_test.rb
index debf1140ab..3cffdddd38 100644
--- a/railties/test/generators/scaffold_generator_test.rb
+++ b/railties/test/generators/scaffold_generator_test.rb
@@ -1,5 +1,7 @@
-require 'generators/generators_test_helper'
-require 'rails/generators/rails/scaffold/scaffold_generator'
+# frozen_string_literal: true
+
+require "generators/generators_test_helper"
+require "rails/generators/rails/scaffold/scaffold_generator"
class ScaffoldGeneratorTest < Rails::Generators::TestCase
include GeneratorsTestHelper
@@ -11,11 +13,11 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
run_generator
# Model
- assert_file "app/models/product_line.rb", /class ProductLine < ActiveRecord::Base/
+ assert_file "app/models/product_line.rb", /class ProductLine < ApplicationRecord/
assert_file "test/models/product_line_test.rb", /class ProductLineTest < ActiveSupport::TestCase/
assert_file "test/fixtures/product_lines.yml"
- assert_migration "db/migrate/create_product_lines.rb", /belongs_to :product, index: true/
- assert_migration "db/migrate/create_product_lines.rb", /references :user, index: true/
+ assert_migration "db/migrate/create_product_lines.rb", /belongs_to :product/
+ assert_migration "db/migrate/create_product_lines.rb", /references :user/
# Route
assert_file "config/routes.rb" do |route|
@@ -57,9 +59,17 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
end
assert_file "test/controllers/product_lines_controller_test.rb" do |test|
- assert_match(/class ProductLinesControllerTest < ActionController::TestCase/, test)
- assert_match(/post :create, params: \{ product_line: \{ product_id: @product_line\.product_id, title: @product_line\.title, user_id: @product_line\.user_id \} \}/, test)
- assert_match(/patch :update, params: \{ id: @product_line, product_line: \{ product_id: @product_line\.product_id, title: @product_line\.title, user_id: @product_line\.user_id \} \}/, test)
+ assert_match(/class ProductLinesControllerTest < ActionDispatch::IntegrationTest/, test)
+ assert_match(/post product_lines_url, params: \{ product_line: \{ product_id: @product_line\.product_id, title: @product_line\.title, user_id: @product_line\.user_id \} \}/, test)
+ 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)
+ assert_match(/visit product_lines_url/, test)
+ assert_match(/fill_in "Title", with: @product_line\.title/, test)
+ assert_match(/assert_text "Product line was successfully updated"/, test)
end
# Views
@@ -74,8 +84,8 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
end
assert_file "app/views/product_lines/_form.html.erb" do |test|
- assert_match 'product_line', test
- assert_no_match '@product_line', test
+ assert_match "product_line", test
+ assert_no_match "@product_line", test
end
# Helpers
@@ -83,7 +93,6 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
# Assets
assert_file "app/assets/stylesheets/scaffold.css"
- assert_file "app/assets/javascripts/product_lines.js"
assert_file "app/assets/stylesheets/product_lines.css"
end
@@ -91,11 +100,11 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
run_generator %w(product_line title:string product:belongs_to user:references --api --no-template-engine --no-helper --no-assets)
# Model
- assert_file "app/models/product_line.rb", /class ProductLine < ActiveRecord::Base/
+ assert_file "app/models/product_line.rb", /class ProductLine < ApplicationRecord/
assert_file "test/models/product_line_test.rb", /class ProductLineTest < ActiveSupport::TestCase/
assert_file "test/fixtures/product_lines.yml"
- assert_migration "db/migrate/create_product_lines.rb", /belongs_to :product, index: true/
- assert_migration "db/migrate/create_product_lines.rb", /references :user, index: true/
+ assert_migration "db/migrate/create_product_lines.rb", /belongs_to :product/
+ assert_migration "db/migrate/create_product_lines.rb", /references :user/
# Route
assert_file "config/routes.rb" do |route|
@@ -135,12 +144,15 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
end
assert_file "test/controllers/product_lines_controller_test.rb" do |test|
- assert_match(/class ProductLinesControllerTest < ActionController::TestCase/, test)
- assert_match(/post :create, params: \{ product_line: \{ product_id: @product_line\.product_id, title: @product_line\.title, user_id: @product_line\.user_id \} \}/, test)
- assert_match(/patch :update, params: \{ id: @product_line, product_line: \{ product_id: @product_line\.product_id, title: @product_line\.title, user_id: @product_line\.user_id \} \}/, test)
+ assert_match(/class ProductLinesControllerTest < ActionDispatch::IntegrationTest/, test)
+ assert_match(/post product_lines_url, params: \{ product_line: \{ product_id: @product_line\.product_id, title: @product_line\.title, user_id: @product_line\.user_id \} \}/, test)
+ 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)
assert_no_match(/assert_redirected_to/, test)
end
+ # System tests
+ assert_no_file "test/system/product_lines_test.rb"
+
# Views
assert_no_file "app/views/layouts/product_lines.html.erb"
@@ -153,7 +165,6 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
# Assets
assert_no_file "app/assets/stylesheets/scaffold.css"
- assert_no_file "app/assets/javascripts/product_lines.js"
assert_no_file "app/assets/stylesheets/product_lines.css"
end
@@ -161,10 +172,20 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
run_generator ["product_line"]
assert_file "test/controllers/product_lines_controller_test.rb" do |content|
- assert_match(/class ProductLinesControllerTest < ActionController::TestCase/, content)
+ assert_match(/class ProductLinesControllerTest < ActionDispatch::IntegrationTest/, content)
assert_match(/test "should get index"/, content)
- assert_match(/post :create, params: \{ product_line: \{ \} \}/, content)
- assert_match(/patch :update, params: \{ id: @product_line, product_line: \{ \} \}/, content)
+ assert_match(/post product_lines_url, params: \{ product_line: \{ \} \}/, content)
+ assert_match(/patch product_line_url\(@product_line\), params: \{ product_line: \{ \} \}/, content)
+ end
+ end
+
+ def test_system_tests_without_attributes
+ run_generator ["product_line"]
+
+ assert_file "test/system/product_lines_test.rb" do |content|
+ assert_match(/class ProductLinesTest < ApplicationSystemTestCase/, content)
+ assert_match(/test "visiting the index"/, content)
+ assert_no_match(/fill_in/, content)
end
end
@@ -187,6 +208,9 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
assert_no_file "app/controllers/product_lines_controller.rb"
assert_no_file "test/controllers/product_lines_controller_test.rb"
+ # System tests
+ assert_no_file "test/system/product_lines_test.rb"
+
# Views
assert_no_file "app/views/product_lines"
assert_no_file "app/views/layouts/product_lines.html.erb"
@@ -196,7 +220,6 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
# Assets
assert_file "app/assets/stylesheets/scaffold.css", /:visited/
- assert_no_file "app/assets/javascripts/product_lines.js"
assert_no_file "app/assets/stylesheets/product_lines.css"
end
@@ -205,7 +228,7 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
# Model
assert_file "app/models/admin.rb", /module Admin/
- assert_file "app/models/admin/role.rb", /class Admin::Role < ActiveRecord::Base/
+ assert_file "app/models/admin/role.rb", /class Admin::Role < ApplicationRecord/
assert_file "test/models/admin/role_test.rb", /class Admin::RoleTest < ActiveSupport::TestCase/
assert_file "test/fixtures/admin/roles.yml"
assert_migration "db/migrate/create_admin_roles.rb"
@@ -250,10 +273,20 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
end
assert_file "test/controllers/admin/roles_controller_test.rb",
- /class Admin::RolesControllerTest < ActionController::TestCase/
+ /class Admin::RolesControllerTest < ActionDispatch::IntegrationTest/
+
+ assert_file "test/system/admin/roles_test.rb",
+ /class Admin::RolesTest < ApplicationSystemTestCase/
# Views
- %w(index edit new show _form).each do |view|
+ assert_file "app/views/admin/roles/index.html.erb" do |content|
+ assert_match("'Show', admin_role", content)
+ assert_match("'Edit', edit_admin_role_path(admin_role)", content)
+ assert_match("'Destroy', admin_role", content)
+ assert_match("'New Admin Role', new_admin_role_path", content)
+ end
+
+ %w(edit new show _form).each do |view|
assert_file "app/views/admin/roles/#{view}.html.erb"
end
assert_no_file "app/views/layouts/admin/roles.html.erb"
@@ -263,13 +296,12 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
# Assets
assert_file "app/assets/stylesheets/scaffold.css", /:visited/
- assert_file "app/assets/javascripts/admin/roles.js"
assert_file "app/assets/stylesheets/admin/roles.css"
end
def test_scaffold_with_namespace_on_revoke
run_generator [ "admin/role", "name:string", "description:string" ]
- run_generator [ "admin/role" ], :behavior => :revoke
+ run_generator [ "admin/role" ], behavior: :revoke
# Model
assert_file "app/models/admin.rb" # ( should not be remove )
@@ -287,6 +319,9 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
assert_no_file "app/controllers/admin/roles_controller.rb"
assert_no_file "test/controllers/admin/roles_controller_test.rb"
+ # System tests
+ assert_no_file "test/system/admin/roles_test.rb"
+
# Views
assert_no_file "app/views/admin/roles"
assert_no_file "app/views/layouts/admin/roles.html.erb"
@@ -296,7 +331,6 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
# Assets
assert_file "app/assets/stylesheets/scaffold.css"
- assert_no_file "app/assets/javascripts/admin/roles.js"
assert_no_file "app/assets/stylesheets/admin/roles.css"
end
@@ -308,9 +342,9 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
content = File.read(route_path).gsub(/\.routes\.draw do/) do |match|
"#{match} |map|"
end
- File.open(route_path, "wb") { |file| file.write(content) }
+ File.write(route_path, content)
- run_generator ["product_line"], :behavior => :revoke
+ run_generator ["product_line"], behavior: :revoke
assert_file "config/routes.rb", /\.routes\.draw do\s*\|map\|\s*$/
end
@@ -322,13 +356,13 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
content = File.read(route_path)
# Remove all of the comments and blank lines from the routes file
- content.gsub!(/^ \#.*\n/, '')
- content.gsub!(/^\n/, '')
+ content.gsub!(/^ \#.*\n/, "")
+ content.gsub!(/^\n/, "")
- File.open(route_path, "wb") { |file| file.write(content) }
+ File.write(route_path, content)
assert_file "config/routes.rb", /\.routes\.draw do\n resources :product_lines\nend\n\z/
- run_generator ["product_line"], :behavior => :revoke
+ run_generator ["product_line"], behavior: :revoke
assert_file "config/routes.rb", /\.routes\.draw do\nend\n\z/
end
@@ -341,28 +375,24 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
def test_scaffold_generator_no_assets_with_switch_no_assets
run_generator [ "posts", "--no-assets" ]
assert_no_file "app/assets/stylesheets/scaffold.css"
- assert_no_file "app/assets/javascripts/posts.js"
assert_no_file "app/assets/stylesheets/posts.css"
end
def test_scaffold_generator_no_assets_with_switch_assets_false
run_generator [ "posts", "--assets=false" ]
assert_no_file "app/assets/stylesheets/scaffold.css"
- assert_no_file "app/assets/javascripts/posts.js"
assert_no_file "app/assets/stylesheets/posts.css"
end
def test_scaffold_generator_no_scaffold_stylesheet_with_switch_no_scaffold_stylesheet
run_generator [ "posts", "--no-scaffold-stylesheet" ]
assert_no_file "app/assets/stylesheets/scaffold.css"
- assert_file "app/assets/javascripts/posts.js"
assert_file "app/assets/stylesheets/posts.css"
end
def test_scaffold_generator_no_scaffold_stylesheet_with_switch_scaffold_stylesheet_false
run_generator [ "posts", "--scaffold-stylesheet=false" ]
assert_no_file "app/assets/stylesheets/scaffold.css"
- assert_file "app/assets/javascripts/posts.js"
assert_file "app/assets/stylesheets/posts.css"
end
@@ -390,17 +420,9 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
def test_scaffold_generator_no_stylesheets
run_generator [ "posts", "--no-stylesheets" ]
assert_no_file "app/assets/stylesheets/scaffold.css"
- assert_file "app/assets/javascripts/posts.js"
assert_no_file "app/assets/stylesheets/posts.css"
end
- def test_scaffold_generator_no_javascripts
- run_generator [ "posts", "--no-javascripts" ]
- assert_file "app/assets/stylesheets/scaffold.css"
- assert_no_file "app/assets/javascripts/posts.js"
- assert_file "app/assets/stylesheets/posts.css"
- end
-
def test_scaffold_generator_outputs_error_message_on_missing_attribute_type
run_generator ["post", "title", "body:text", "author"]
@@ -413,8 +435,8 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
end
end
- def test_scaffold_generator_belongs_to
- run_generator ["account", "name", "currency:belongs_to"]
+ def test_scaffold_generator_belongs_to_and_references
+ run_generator ["account", "name", "currency:belongs_to", "user:references"]
assert_file "app/models/account.rb", /belongs_to :currency/
@@ -427,13 +449,31 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
assert_file "app/controllers/accounts_controller.rb" do |content|
assert_instance_method :account_params, content do |m|
- assert_match(/permit\(:name, :currency_id\)/, m)
+ assert_match(/permit\(:name, :currency_id, :user_id\)/, m)
end
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 %>/, content)
+ assert_match(/^\W{4}<%= form\.text_field :currency_id %>/, content)
+ end
+
+ assert_file "app/views/accounts/index.html.erb" do |content|
+ assert_match(/^\W{8}<td><%= account\.name %><\/td>/, content)
+ assert_match(/^\W{8}<td><%= account\.user_id %><\/td>/, content)
+ end
+
+ assert_file "app/views/accounts/show.html.erb" do |content|
+ assert_match(/^\W{2}<%= @account\.name %>/, content)
+ assert_match(/^\W{2}<%= @account\.user_id %>/, content)
+ end
+ end
+
+ def test_scaffold_generator_database
+ with_secondary_database_configuration do
+ run_generator ["posts", "--database=secondary"]
+
+ assert_migration "db/secondary_migrate/create_posts.rb"
end
end
@@ -456,8 +496,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 %>/, content)
+ assert_match(/<%= form\.password_field :password_confirmation %>/, content)
end
assert_file "app/views/users/index.html.erb" do |content|
@@ -473,6 +513,11 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
assert_match(/password_confirmation: 'secret'/, content)
end
+ assert_file "test/system/users_test.rb" do |content|
+ assert_match(/fill_in "Password", with: 'secret'/, content)
+ assert_match(/fill_in "Password confirmation", with: 'secret'/, content)
+ end
+
assert_file "test/fixtures/users.yml" do |content|
assert_match(/password_digest: <%= BCrypt::Password.create\('secret'\) %>/, content)
end
@@ -486,9 +531,29 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
Dir.chdir(engine_path) do
quietly do
`bin/rails g scaffold User name:string age:integer;
- bundle exec rake db:migrate`
+ bin/rails db:migrate`
+ 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_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_match(/8 runs, 13 assertions, 0 failures, 0 errors/, `bundle exec rake test 2>&1`)
+
+ 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
@@ -500,9 +565,94 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
Dir.chdir(engine_path) do
quietly do
`bin/rails g scaffold User name:string age:integer;
- bundle exec rake db:migrate`
+ bin/rails db:migrate`
+ 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_api_mountable_engine
+ Dir.chdir(destination_root) { `bundle exec rails plugin new bukkits --mountable --api` }
+
+ engine_path = File.join(destination_root, "bukkits")
+
+ Dir.chdir(engine_path) do
+ quietly do
+ `bin/rails g scaffold User name:string age:integer;
+ bin/rails db:migrate`
+ end
+ assert_match(/6 runs, 8 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`)
+ end
+ end
+
+ def test_scaffold_tests_pass_by_default_inside_api_full_engine
+ Dir.chdir(destination_root) { `bundle exec rails plugin new bukkits --full --api` }
+
+ engine_path = File.join(destination_root, "bukkits")
+
+ Dir.chdir(engine_path) do
+ quietly do
+ `bin/rails g scaffold User name:string age:integer;
+ bin/rails db:migrate`
end
- assert_match(/8 runs, 13 assertions, 0 failures, 0 errors/, `bundle exec rake test 2>&1`)
+ 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?("test/system/bukkits/users_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/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?("test/system/bukkits/users_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/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 77372fb514..2dda856f25 100644
--- a/railties/test/generators/shared_generator_tests.rb
+++ b/railties/test/generators/shared_generator_tests.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
#
# Tests, setup, and teardown common to the application and plugin generator suites.
#
@@ -5,9 +7,9 @@ module SharedGeneratorTests
def setup
Rails.application = TestApp::Application
super
- Rails::Generators::AppGenerator.instance_variable_set('@desc', nil)
+ Rails::Generators::AppGenerator.instance_variable_set("@desc", nil)
- Kernel::silence_warnings do
+ Kernel.silence_warnings do
Thor::Base.shell.send(:attr_accessor, :always_force)
@shell = Thor::Base.shell.new
@shell.send(:always_force=, true)
@@ -16,34 +18,27 @@ module SharedGeneratorTests
def teardown
super
- Rails::Generators::AppGenerator.instance_variable_set('@desc', nil)
+ Rails::Generators::AppGenerator.instance_variable_set("@desc", nil)
Rails.application = TestApp::Application.instance
end
+ def application_path
+ destination_root
+ end
+
def test_skeleton_is_created
run_generator
default_files.each { |path| assert_file path }
end
- def assert_generates_with_bundler(options = {})
- generator([destination_root], options)
- generator.expects(:bundle_command).with('install').once
- generator.stubs(:bundle_command).with('exec spring binstub --all')
- quietly { generator.invoke_all }
- end
-
- def test_generation_runs_bundle_install
- assert_generates_with_bundler
- end
-
def test_plugin_new_generate_pretend
run_generator ["testapp", "--pretend"]
- default_files.each{ |path| assert_no_file File.join("testapp",path) }
+ default_files.each { |path| assert_no_file File.join("testapp", path) }
end
def test_invalid_database_option_raises_an_error
- content = capture(:stderr){ run_generator([destination_root, "-d", "unknown"]) }
+ content = capture(:stderr) { run_generator([destination_root, "-d", "unknown"]) }
assert_match(/Invalid value for \-\-database option/, content)
end
@@ -55,15 +50,15 @@ module SharedGeneratorTests
def test_name_collision_raises_an_error
reserved_words = %w[application destroy plugin runner test]
reserved_words.each do |reserved|
- content = capture(:stderr){ run_generator [File.join(destination_root, reserved)] }
- assert_match(/Invalid \w+ name #{reserved}. Please give a name which does not match one of the reserved rails words: application, destroy, plugin, runner, test\n/, content)
+ content = capture(:stderr) { run_generator [File.join(destination_root, reserved)] }
+ assert_match(/Invalid \w+ name #{reserved}\. Please give a name which does not match one of the reserved rails words: application, destroy, plugin, runner, test\n/, content)
end
end
def test_name_raises_an_error_if_name_already_used_constant
%w{ String Hash Class Module Set Symbol }.each do |ruby_class|
- content = capture(:stderr){ run_generator [File.join(destination_root, ruby_class)] }
- assert_match(/Invalid \w+ name #{ruby_class}, constant #{ruby_class} is already in use. Please choose another \w+ name.\n/, content)
+ content = capture(:stderr) { run_generator [File.join(destination_root, ruby_class)] }
+ assert_match(/Invalid \w+ name #{ruby_class}, constant #{ruby_class} is already in use\. Please choose another \w+ name\.\n/, content)
end
end
@@ -79,7 +74,7 @@ module SharedGeneratorTests
def test_template_raises_an_error_with_invalid_path
quietly do
- content = capture(:stderr){ run_generator([destination_root, "-m", "non/existent/path"]) }
+ content = capture(:stderr) { run_generator([destination_root, "-m", "non/existent/path"]) }
assert_match(/The template \[.*\] could not be loaded/, content)
assert_match(/non\/existent\/path/, content)
@@ -88,52 +83,268 @@ module SharedGeneratorTests
def test_template_is_executed_when_supplied_an_https_path
path = "https://gist.github.com/josevalim/103208/raw/"
- template = %{ say "It works!" }
+ template = +%{ say "It works!" }
template.instance_eval "def read; self; end" # Make the string respond to read
- generator([destination_root], template: path).expects(:open).with(path, 'Accept' => 'application/x-thor-template').returns(template)
- quietly { assert_match(/It works!/, capture(:stdout) { generator.invoke_all }) }
+ check_open = -> *args do
+ assert_equal [ path, "Accept" => "application/x-thor-template" ], args
+ template
+ end
+
+ generator([destination_root], template: path, skip_webpack_install: true).stub(:open, check_open, template) do
+ generator.stub :bundle_command, nil do
+ quietly { assert_match(/It works!/, capture(:stdout) { generator.invoke_all }) }
+ end
+ end
end
- def test_dev_option
- assert_generates_with_bundler dev: true
- rails_path = File.expand_path('../../..', Rails.root)
- assert_file 'Gemfile', /^gem\s+["']rails["'],\s+path:\s+["']#{Regexp.escape(rails_path)}["']$/
+ def test_skip_gemfile
+ assert_not_called(generator([destination_root], skip_gemfile: true, skip_webpack_install: true), :bundle_command) do
+ quietly { generator.invoke_all }
+ assert_no_file "Gemfile"
+ end
end
- def test_edge_option
- assert_generates_with_bundler edge: true
- assert_file 'Gemfile', %r{^gem\s+["']rails["'],\s+github:\s+["']#{Regexp.escape("rails/rails")}["']$}
+ def test_skip_git
+ run_generator [destination_root, "--skip-git", "--full"]
+ assert_no_file(".gitignore")
+ assert_no_directory(".git")
end
- def test_skip_gemfile
- generator([destination_root], skip_gemfile: true).expects(:bundle_command).never
- quietly { generator.invoke_all }
- assert_no_file 'Gemfile'
+ def test_skip_keeps
+ run_generator [destination_root, "--skip-keeps", "--full"]
+
+ assert_file ".gitignore" do |content|
+ assert_no_match(/\.keep/, content)
+ end
+
+ assert_no_file("app/models/concerns/.keep")
end
- def test_skip_bundle
- generator([destination_root], skip_bundle: true).expects(:bundle_command).never
- quietly { generator.invoke_all }
+ def test_default_frameworks_are_required_when_others_are_removed
+ run_generator [
+ destination_root,
+ "--skip-active-record",
+ "--skip-active-storage",
+ "--skip-action-mailer",
+ "--skip-action-cable",
+ "--skip-sprockets"
+ ]
- # skip_bundle is only about running bundle install, ensure the Gemfile is still
- # generated.
- assert_file 'Gemfile'
+ assert_file "#{application_path}/config/application.rb", /^require\s+["']rails["']/
+ assert_file "#{application_path}/config/application.rb", /^require\s+["']active_model\/railtie["']/
+ assert_file "#{application_path}/config/application.rb", /^require\s+["']active_job\/railtie["']/
+ assert_file "#{application_path}/config/application.rb", /^# require\s+["']active_record\/railtie["']/
+ assert_file "#{application_path}/config/application.rb", /^# require\s+["']active_storage\/engine["']/
+ assert_file "#{application_path}/config/application.rb", /^require\s+["']action_controller\/railtie["']/
+ assert_file "#{application_path}/config/application.rb", /^# require\s+["']action_mailer\/railtie["']/
+ assert_file "#{application_path}/config/application.rb", /^require\s+["']action_view\/railtie["']/
+ assert_file "#{application_path}/config/application.rb", /^# require\s+["']action_cable\/engine["']/
+ assert_file "#{application_path}/config/application.rb", /^# require\s+["']sprockets\/railtie["']/
+ assert_file "#{application_path}/config/application.rb", /^require\s+["']rails\/test_unit\/railtie["']/
end
- def test_skip_git
- run_generator [destination_root, '--skip-git', '--full']
- assert_no_file('.gitignore')
- assert_file('app/mailers/.keep')
+ def test_generator_without_skips
+ run_generator
+ assert_file "#{application_path}/config/application.rb", /\s+require\s+["']rails\/all["']/
+ assert_file "#{application_path}/config/environments/development.rb" do |content|
+ assert_match(/config\.action_mailer\.raise_delivery_errors = false/, content)
+ end
+ assert_file "#{application_path}/config/environments/test.rb" do |content|
+ assert_match(/config\.action_mailer\.delivery_method = :test/, content)
+ end
+ assert_file "#{application_path}/config/environments/production.rb" do |content|
+ assert_match(/# config\.action_mailer\.raise_delivery_errors = false/, content)
+ assert_match(/^ # config\.require_master_key = true/, content)
+ end
end
- def test_skip_keeps
- run_generator [destination_root, '--skip-keeps', '--full']
+ def test_gitignore_when_sqlite3
+ run_generator
- assert_file '.gitignore' do |content|
- assert_no_match(/\.keep/, content)
+ assert_file ".gitignore" do |content|
+ assert_match(/sqlite3/, content)
+ end
+ end
+
+ def test_gitignore_when_non_sqlite3_db
+ run_generator([destination_root, "-d", "mysql"])
+
+ assert_file ".gitignore" do |content|
+ assert_no_match(/sqlite/i, content)
+ end
+ end
+
+ def test_generator_if_skip_active_record_is_given
+ run_generator [destination_root, "--skip-active-record"]
+ assert_no_directory "#{application_path}/db/"
+ assert_no_file "#{application_path}/config/database.yml"
+ assert_no_file "#{application_path}/app/models/application_record.rb"
+ assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']active_record\/railtie["']/
+ assert_file "test/test_helper.rb" do |helper_content|
+ assert_no_match(/fixtures :all/, helper_content)
+ end
+ assert_file "#{application_path}/bin/setup" do |setup_content|
+ assert_no_match(/db:setup/, setup_content)
+ end
+ assert_file "#{application_path}/bin/update" do |update_content|
+ assert_no_match(/db:migrate/, update_content)
+ end
+ assert_file ".gitignore" do |content|
+ assert_no_match(/sqlite/i, content)
+ end
+ end
+
+ def test_generator_for_active_storage
+ run_generator
+
+ unless generator_class.name == "Rails::Generators::PluginGenerator"
+ assert_file "#{application_path}/app/javascript/packs/application.js" do |content|
+ assert_match(/^import \* as ActiveStorage from "activestorage"\nActiveStorage.start\(\)/, content)
+ end
+ end
+
+ assert_file "#{application_path}/config/environments/development.rb" do |content|
+ assert_match(/config\.active_storage/, content)
+ end
+
+ assert_file "#{application_path}/config/environments/production.rb" do |content|
+ assert_match(/config\.active_storage/, content)
+ end
+
+ assert_file "#{application_path}/config/environments/test.rb" do |content|
+ assert_match(/config\.active_storage/, content)
+ end
+
+ assert_file "#{application_path}/config/storage.yml"
+ assert_directory "#{application_path}/storage"
+ assert_directory "#{application_path}/tmp/storage"
+
+ assert_file ".gitignore" do |content|
+ assert_match(/\/storage\//, content)
end
+ end
+
+ def test_generator_if_skip_active_storage_is_given
+ run_generator [destination_root, "--skip-active-storage"]
+
+ assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']active_storage\/engine["']/
+
+ assert_file "#{application_path}/app/javascript/packs/application.js" do |content|
+ assert_no_match(/activestorage/, content)
+ end
+
+ assert_file "#{application_path}/config/environments/development.rb" do |content|
+ assert_no_match(/config\.active_storage/, content)
+ end
+
+ assert_file "#{application_path}/config/environments/production.rb" do |content|
+ assert_no_match(/config\.active_storage/, content)
+ end
+
+ assert_file "#{application_path}/config/environments/test.rb" do |content|
+ assert_no_match(/config\.active_storage/, content)
+ end
+
+ assert_no_file "#{application_path}/config/storage.yml"
+ assert_no_directory "#{application_path}/storage"
+ assert_no_directory "#{application_path}/tmp/storage"
+
+ assert_file ".gitignore" do |content|
+ assert_no_match(/\/storage\//, content)
+ end
+ end
+
+ def test_generator_does_not_generate_active_storage_contents_if_skip_active_record_is_given
+ run_generator [destination_root, "--skip-active-record"]
+
+ assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']active_storage\/engine["']/
+
+ assert_file "#{application_path}/app/javascript/packs/application.js" do |content|
+ assert_no_match(/^import * as ActiveStorage from "activestorage"\nActiveStorage.start\(\)/, content)
+ end
+
+ assert_file "#{application_path}/config/environments/development.rb" do |content|
+ assert_no_match(/config\.active_storage/, content)
+ end
+
+ assert_file "#{application_path}/config/environments/production.rb" do |content|
+ assert_no_match(/config\.active_storage/, content)
+ end
+
+ assert_file "#{application_path}/config/environments/test.rb" do |content|
+ assert_no_match(/config\.active_storage/, content)
+ end
+
+ assert_no_file "#{application_path}/config/storage.yml"
+ assert_no_directory "#{application_path}/storage"
+ assert_no_directory "#{application_path}/tmp/storage"
+
+ assert_file ".gitignore" do |content|
+ assert_no_match(/\/storage\//, content)
+ end
+ end
+
+ def test_generator_if_skip_action_mailer_is_given
+ run_generator [destination_root, "--skip-action-mailer"]
+ assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']action_mailer\/railtie["']/
+ assert_file "#{application_path}/config/environments/development.rb" do |content|
+ assert_no_match(/config\.action_mailer/, content)
+ end
+ assert_file "#{application_path}/config/environments/test.rb" do |content|
+ assert_no_match(/config\.action_mailer/, content)
+ end
+ assert_file "#{application_path}/config/environments/production.rb" do |content|
+ assert_no_match(/config\.action_mailer/, content)
+ end
+ assert_no_directory "#{application_path}/app/mailers"
+ assert_no_directory "#{application_path}/test/mailers"
+ end
+
+ def test_generator_if_skip_action_cable_is_given
+ run_generator [destination_root, "--skip-action-cable"]
+ assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']action_cable\/engine["']/
+ assert_no_file "#{application_path}/config/cable.yml"
+ assert_no_file "#{application_path}/app/javascript/consumer.js"
+ assert_no_directory "#{application_path}/app/javascript/channels"
+ assert_no_directory "#{application_path}/app/channels"
+ assert_file "Gemfile" do |content|
+ assert_no_match(/redis/, content)
+ end
+ end
+
+ def test_generator_if_skip_sprockets_is_given
+ run_generator [destination_root, "--skip-sprockets"]
+
+ assert_no_file "#{application_path}/config/initializers/assets.rb"
+
+ assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']sprockets\/railtie["']/
+
+ assert_file "Gemfile" do |content|
+ assert_no_match(/sass-rails/, content)
+ end
+
+ assert_file "#{application_path}/config/environments/development.rb" do |content|
+ assert_no_match(/config\.assets\.debug/, content)
+ end
+
+ assert_file "#{application_path}/config/environments/production.rb" do |content|
+ assert_no_match(/config\.assets\.digest/, content)
+ assert_no_match(/config\.assets\.css_compressor/, content)
+ assert_no_match(/config\.assets\.compile/, content)
+ end
+ end
+
+ def test_generator_for_yarn
+ skip "#34009 disabled JS by default for plugins" if generator_class.name == "Rails::Generators::PluginGenerator"
+ run_generator
+ assert_file "#{application_path}/package.json", /dependencies/
+ assert_file "#{application_path}/bin/yarn"
+ end
- assert_no_file('app/mailers/.keep')
+ def test_generator_for_yarn_skipped
+ run_generator([destination_root, "--skip-javascript"])
+ assert_no_file "#{application_path}/package.json"
+ assert_no_file "#{application_path}/bin/yarn"
end
end
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..efa70a050b
--- /dev/null
+++ b/railties/test/generators/system_test_generator_test.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+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
+
+ def test_namespaced_system_test_skeleton_is_created
+ run_generator %w(admin/user)
+ assert_file "test/system/admin/users_test.rb", /class Admin::UsersTest < ApplicationSystemTestCase/
+ end
+end
diff --git a/railties/test/generators/task_generator_test.rb b/railties/test/generators/task_generator_test.rb
index d5bd44b9db..5f162919d8 100644
--- a/railties/test/generators/task_generator_test.rb
+++ b/railties/test/generators/task_generator_test.rb
@@ -1,5 +1,7 @@
-require 'generators/generators_test_helper'
-require 'rails/generators/rails/task/task_generator'
+# frozen_string_literal: true
+
+require "generators/generators_test_helper"
+require "rails/generators/rails/task/task_generator"
class TaskGeneratorTest < Rails::Generators::TestCase
include GeneratorsTestHelper
@@ -15,10 +17,10 @@ class TaskGeneratorTest < Rails::Generators::TestCase
end
def test_task_on_revoke
- task_path = 'lib/tasks/feeds.rake'
+ task_path = "lib/tasks/feeds.rake"
run_generator
assert_file task_path
- run_generator ['feeds'], behavior: :revoke
+ run_generator ["feeds"], behavior: :revoke
assert_no_file task_path
end
end
diff --git a/railties/test/generators/test_runner_in_engine_test.rb b/railties/test/generators/test_runner_in_engine_test.rb
new file mode 100644
index 0000000000..bd102a32b5
--- /dev/null
+++ b/railties/test/generators/test_runner_in_engine_test.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require "generators/plugin_test_helper"
+
+class TestRunnerInEngineTest < ActiveSupport::TestCase
+ include PluginTestHelper
+
+ def setup
+ @destination_root = Dir.mktmpdir("bukkits")
+ Dir.chdir(@destination_root) { `bundle exec rails plugin new bukkits --full --skip-bundle` }
+ plugin_file "test/dummy/db/schema.rb", ""
+ end
+
+ def teardown
+ FileUtils.rm_rf(@destination_root)
+ end
+
+ def test_rerun_snippet_is_relative_path
+ create_test_file "post", pass: false
+
+ output = run_test_command("test/post_test.rb")
+ expect = %r{Running:\n\nPostTest\nF\n\nFailure:\nPostTest#test_truth \[[^\]]+test/post_test\.rb:6\]:\nwups!\n\nrails test test/post_test\.rb:4}
+ assert_match expect, output
+ end
+
+ private
+ def plugin_path
+ "#{@destination_root}/bukkits"
+ end
+
+ def run_test_command(arguments)
+ Dir.chdir(plugin_path) { `bin/rails test #{arguments}` }
+ end
+end
diff --git a/railties/test/generators_test.rb b/railties/test/generators_test.rb
index 31a575749a..f98c1f78f7 100644
--- a/railties/test/generators_test.rb
+++ b/railties/test/generators_test.rb
@@ -1,7 +1,8 @@
-require 'generators/generators_test_helper'
-require 'rails/generators/rails/model/model_generator'
-require 'rails/generators/test_unit/model/model_generator'
-require 'minitest/mock'
+# frozen_string_literal: true
+
+require "generators/generators_test_helper"
+require "rails/generators/rails/model/model_generator"
+require "rails/generators/test_unit/model/model_generator"
class GeneratorsTest < Rails::Generators::TestCase
include GeneratorsTestHelper
@@ -9,65 +10,66 @@ class GeneratorsTest < Rails::Generators::TestCase
def setup
@path = File.expand_path("lib", Rails.root)
$LOAD_PATH.unshift(@path)
- @mock_generator = MiniTest::Mock.new
end
def teardown
$LOAD_PATH.delete(@path)
- @mock_generator.verify
end
def test_simple_invoke
- assert File.exist?(File.join(@path, 'generators', 'model_generator.rb'))
- @mock_generator.expect(:call, nil, [["Account"],{}])
- TestUnit::Generators::ModelGenerator.stub(:start, @mock_generator) do
+ assert File.exist?(File.join(@path, "generators", "model_generator.rb"))
+ assert_called_with(TestUnit::Generators::ModelGenerator, :start, [["Account"], {}]) do
Rails::Generators.invoke("test_unit:model", ["Account"])
end
end
def test_invoke_when_generator_is_not_found
name = :unknown
- output = capture(:stdout){ Rails::Generators.invoke name }
+ output = capture(:stdout) { Rails::Generators.invoke name }
assert_match "Could not find generator '#{name}'", output
assert_match "`rails generate --help`", output
end
def test_generator_suggestions
name = :migrationz
- output = capture(:stdout){ Rails::Generators.invoke name }
- assert_match "Maybe you meant 'migration'", output
+ output = capture(:stdout) { Rails::Generators.invoke name }
+ assert_match 'Maybe you meant "migration"?', output
end
- def test_generator_multiple_suggestions
+ def test_generator_suggestions_except_en_locale
+ orig_available_locales = I18n.available_locales
+ orig_default_locale = I18n.default_locale
+ I18n.available_locales = :ja
+ I18n.default_locale = :ja
name = :tas
- output = capture(:stdout){ Rails::Generators.invoke name }
- assert_match "Maybe you meant 'task', 'job' or", output
+ output = capture(:stdout) { Rails::Generators.invoke name }
+ assert_match 'Maybe you meant "task"?', output
+ ensure
+ I18n.available_locales = orig_available_locales
+ I18n.default_locale = orig_default_locale
end
def test_help_when_a_generator_with_required_arguments_is_invoked_without_arguments
- output = capture(:stdout){ Rails::Generators.invoke :model, [] }
+ output = capture(:stdout) { Rails::Generators.invoke :model, [] }
assert_match(/Description:/, output)
end
def test_should_give_higher_preference_to_rails_generators
- assert File.exist?(File.join(@path, 'generators', 'model_generator.rb'))
- @mock_generator.expect(:call, nil, [["Account"],{}])
- Rails::Generators::ModelGenerator.stub(:start, @mock_generator) do
- warnings = capture(:stderr){ Rails::Generators.invoke :model, ["Account"] }
- assert warnings.empty?
+ assert File.exist?(File.join(@path, "generators", "model_generator.rb"))
+ assert_called_with(Rails::Generators::ModelGenerator, :start, [["Account"], {}]) do
+ warnings = capture(:stderr) { Rails::Generators.invoke :model, ["Account"] }
+ assert_empty warnings
end
end
def test_invoke_with_default_values
- @mock_generator.expect(:call, nil, [["Account"],{}])
- Rails::Generators::ModelGenerator.stub(:start, @mock_generator) do
+ assert_called_with(Rails::Generators::ModelGenerator, :start, [["Account"], {}]) do
Rails::Generators.invoke :model, ["Account"]
end
end
def test_invoke_with_config_values
- @mock_generator.expect(:call, nil, [["Account"],{behavior: :skip}])
- Rails::Generators::ModelGenerator.stub(:start, @mock_generator) do
+ assert_called_with(Rails::Generators::ModelGenerator, :start, [["Account"], behavior: :skip]) do
Rails::Generators.invoke :model, ["Account"], behavior: :skip
end
end
@@ -115,15 +117,14 @@ class GeneratorsTest < Rails::Generators::TestCase
def test_invoke_with_nested_namespaces
model_generator = Minitest::Mock.new
model_generator.expect(:start, nil, [["Account"], {}])
- @mock_generator.expect(:call, model_generator, ['namespace', 'my:awesome'])
- Rails::Generators.stub(:find_by_namespace, @mock_generator) do
- Rails::Generators.invoke 'my:awesome:namespace', ["Account"]
+ assert_called_with(Rails::Generators, :find_by_namespace, ["namespace", "my:awesome"], returns: model_generator) do
+ Rails::Generators.invoke "my:awesome:namespace", ["Account"]
end
model_generator.verify
end
def test_rails_generators_help_with_builtin_information
- output = capture(:stdout){ Rails::Generators.help }
+ output = capture(:stdout) { Rails::Generators.help }
assert_match(/Rails:/, output)
assert_match(/^ model$/, output)
assert_match(/^ scaffold_controller$/, output)
@@ -131,19 +132,19 @@ class GeneratorsTest < Rails::Generators::TestCase
end
def test_rails_generators_help_does_not_include_app_nor_plugin_new
- output = capture(:stdout){ Rails::Generators.help }
- assert_no_match(/app/, output)
+ output = capture(:stdout) { Rails::Generators.help }
+ assert_no_match(/app\W/, output)
assert_no_match(/[^:]plugin/, output)
end
def test_rails_generators_with_others_information
- output = capture(:stdout){ Rails::Generators.help }
+ output = capture(:stdout) { Rails::Generators.help }
assert_match(/Fixjour:/, output)
assert_match(/^ fixjour$/, output)
end
def test_rails_generators_does_not_show_active_record_hooks
- output = capture(:stdout){ Rails::Generators.help }
+ output = capture(:stdout) { Rails::Generators.help }
assert_match(/ActiveRecord:/, output)
assert_match(/^ active_record:fixjour$/, output)
end
@@ -185,8 +186,7 @@ class GeneratorsTest < Rails::Generators::TestCase
def test_fallbacks_for_generators_on_invoke
Rails::Generators.fallbacks[:shoulda] = :test_unit
- @mock_generator.expect(:call, nil, [["Account"],{}])
- TestUnit::Generators::ModelGenerator.stub(:start, @mock_generator) do
+ assert_called_with(TestUnit::Generators::ModelGenerator, :start, [["Account"], {}]) do
Rails::Generators.invoke "shoulda:model", ["Account"]
end
ensure
@@ -196,8 +196,7 @@ class GeneratorsTest < Rails::Generators::TestCase
def test_nested_fallbacks_for_generators
Rails::Generators.fallbacks[:shoulda] = :test_unit
Rails::Generators.fallbacks[:super_shoulda] = :shoulda
- @mock_generator.expect(:call, nil, [["Account"],{}])
- TestUnit::Generators::ModelGenerator.stub(:start, @mock_generator) do
+ assert_called_with(TestUnit::Generators::ModelGenerator, :start, [["Account"], {}]) do
Rails::Generators.invoke "super_shoulda:model", ["Account"]
end
ensure
@@ -210,7 +209,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
@@ -224,7 +223,7 @@ class GeneratorsTest < Rails::Generators::TestCase
# Create template
mkdir_p(File.dirname(template))
- File.open(template, 'w'){ |f| f.write "empty" }
+ File.open(template, "w") { |f| f.write "empty" }
capture(:stdout) do
Rails::Generators.invoke :model, ["user"], destination_root: destination_root
@@ -239,18 +238,18 @@ class GeneratorsTest < Rails::Generators::TestCase
def test_source_paths_for_not_namespaced_generators
mspec = Rails::Generators.find_by_namespace :fixjour
- assert mspec.source_paths.include?(File.join(Rails.root, "lib", "templates", "fixjour"))
+ assert_includes mspec.source_paths, File.join(Rails.root, "lib", "templates", "fixjour")
end
def test_usage_with_embedded_ruby
- require File.expand_path("fixtures/lib/generators/usage_template/usage_template_generator", File.dirname(__FILE__))
- output = capture(:stdout) { Rails::Generators.invoke :usage_template, ['--help'] }
+ require_relative "fixtures/lib/generators/usage_template/usage_template_generator"
+ output = capture(:stdout) { Rails::Generators.invoke :usage_template, ["--help"] }
assert_match(/:: 2 ::/, output)
end
def test_hide_namespace
- assert !Rails::Generators.hidden_namespaces.include?("special:namespace")
+ assert_not_includes Rails::Generators.hidden_namespaces, "special:namespace"
Rails::Generators.hide_namespace("special:namespace")
- assert Rails::Generators.hidden_namespaces.include?("special:namespace")
+ assert_includes Rails::Generators.hidden_namespaces, "special:namespace"
end
end
diff --git a/railties/test/initializable_test.rb b/railties/test/initializable_test.rb
index ed9573453b..59fee245f9 100644
--- a/railties/test/initializable_test.rb
+++ b/railties/test/initializable_test.rb
@@ -1,8 +1,9 @@
-require 'abstract_unit'
-require 'rails/initializable'
+# frozen_string_literal: true
-module InitializableTests
+require "abstract_unit"
+require "rails/initializable"
+module InitializableTests
class Foo
include Rails::Initializable
attr_accessor :foo, :bar
@@ -174,6 +175,11 @@ module InitializableTests
end
end
end
+
+ test "Initializer provides context's class name" do
+ foo = Foo.new
+ assert_equal foo.class, foo.initializers.first.context_class
+ end
end
class BeforeAfter < ActiveSupport::TestCase
@@ -215,8 +221,8 @@ module InitializableTests
class WithArgsTest < ActiveSupport::TestCase
test "running initializers with args" do
$with_arg = nil
- WithArgs.new.run_initializers(:default, 'foo')
- assert_equal 'foo', $with_arg
+ WithArgs.new.run_initializers(:default, "foo")
+ assert_equal "foo", $with_arg
end
end
diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb
index 65d8a55421..d4eed69a87 100644
--- a/railties/test/isolation/abstract_unit.rb
+++ b/railties/test/isolation/abstract_unit.rb
@@ -1,40 +1,53 @@
+# frozen_string_literal: true
+
# Note:
# It is important to keep this file as light as possible
# the goal for tests that require this is to test booting up
-# rails from an empty state, so anything added here could
+# Rails from an empty state, so anything added here could
# hide potential failures
#
# It is also good to know what is the bare minimum to get
# Rails booted up.
-require 'fileutils'
+require "fileutils"
+
+require "bundler/setup" unless defined?(Bundler)
+require "active_support"
+require "active_support/testing/autorun"
+require "active_support/testing/stream"
+require "active_support/testing/method_call_assertions"
+require "active_support/test_case"
+require "minitest/retry"
-require 'bundler/setup' unless defined?(Bundler)
-require 'active_support'
-require 'active_support/testing/autorun'
-require 'active_support/testing/stream'
-require 'active_support/test_case'
+Minitest::Retry.use!(verbose: false, retry_count: 1)
-RAILS_FRAMEWORK_ROOT = File.expand_path("#{File.dirname(__FILE__)}/../../..")
+RAILS_FRAMEWORK_ROOT = File.expand_path("../../..", __dir__)
# These files do not require any others and are needed
# to run the tests
+require "active_support/core_ext/object/blank"
require "active_support/testing/isolation"
require "active_support/core_ext/kernel/reporting"
-require 'tmpdir'
+require "tmpdir"
+require "rails/secrets"
module TestHelpers
module Paths
def app_template_path
- File.join Dir.tmpdir, 'app_template'
+ File.join RAILS_FRAMEWORK_ROOT, "tmp/templates/app_template"
end
def tmp_path(*args)
- @tmp_path ||= File.realpath(Dir.mktmpdir)
+ @tmp_path ||= File.realpath(Dir.mktmpdir(nil, File.join(RAILS_FRAMEWORK_ROOT, "tmp")))
File.join(@tmp_path, *args)
end
def app_path(*args)
- tmp_path(*%w[app] + args)
+ path = tmp_path(*%w[app] + args)
+ if block_given?
+ yield path
+ else
+ path
+ end
end
def framework_path
@@ -51,7 +64,9 @@ module TestHelpers
old_env = ENV["RAILS_ENV"]
@app ||= begin
ENV["RAILS_ENV"] = env
+
require "#{app_path}/config/environment"
+
Rails.application
end
ensure
@@ -59,8 +74,8 @@ module TestHelpers
end
def extract_body(response)
- "".tap do |body|
- response[2].each {|chunk| body << chunk }
+ (+"").tap do |body|
+ response[2].each { |chunk| body << chunk }
end
end
@@ -69,88 +84,130 @@ module TestHelpers
end
def assert_welcome(resp)
- assert_equal 200, resp[0]
- assert_match 'text/html', resp[1]["Content-Type"]
- assert_match 'charset=utf-8', resp[1]["Content-Type"]
- assert extract_body(resp).match(/Welcome aboard/)
- end
-
- def assert_success(resp)
- assert_equal 202, resp[0]
- end
-
- def assert_missing(resp)
- assert_equal 404, resp[0]
- end
-
- def assert_header(key, value, resp)
- assert_equal value, resp[1][key.to_s]
- end
+ resp = Array(resp)
- def assert_body(expected, resp)
- assert_equal expected, extract_body(resp)
+ assert_equal 200, resp[0]
+ assert_match "text/html", resp[1]["Content-Type"]
+ assert_match "charset=utf-8", resp[1]["Content-Type"]
+ assert extract_body(resp).match(/Yay! You.*re on Rails!/)
end
end
module Generation
# Build an application by invoking the generator and going through the whole stack.
def build_app(options = {})
- @prev_rails_env = ENV['RAILS_ENV']
- ENV['RAILS_ENV'] = "development"
- ENV['SECRET_KEY_BASE'] ||= SecureRandom.hex(16)
+ @prev_rails_env = ENV["RAILS_ENV"]
+ ENV["RAILS_ENV"] = "development"
FileUtils.rm_rf(app_path)
FileUtils.cp_r(app_template_path, app_path)
# Delete the initializers unless requested
unless options[:initializers]
- Dir["#{app_path}/config/initializers/*.rb"].each do |initializer|
+ Dir["#{app_path}/config/initializers/**/*.rb"].each do |initializer|
File.delete(initializer)
end
end
- gemfile_path = "#{app_path}/Gemfile"
- if options[:gemfile].blank? && File.exist?(gemfile_path)
- File.delete gemfile_path
- end
-
routes = File.read("#{app_path}/config/routes.rb")
- if routes =~ /(\n\s*end\s*)\Z/
- File.open("#{app_path}/config/routes.rb", 'w') do |f|
- f.puts $` + "\nmatch ':controller(/:action(/:id))(.:format)', via: :all\n" + $1
+ if routes =~ /(\n\s*end\s*)\z/
+ File.open("#{app_path}/config/routes.rb", "w") do |f|
+ f.puts $` + "\nActiveSupport::Deprecation.silence { match ':controller(/:action(/:id))(.:format)', via: :all }\n" + $1
end
end
- File.open("#{app_path}/config/database.yml", "w") do |f|
- f.puts <<-YAML
- default: &default
- adapter: sqlite3
- pool: 5
- timeout: 5000
- development:
- <<: *default
- database: db/development.sqlite3
- test:
- <<: *default
- database: db/test.sqlite3
- production:
- <<: *default
- database: db/production.sqlite3
- YAML
+ if options[:multi_db]
+ File.open("#{app_path}/config/database.yml", "w") do |f|
+ f.puts <<-YAML
+ default: &default
+ adapter: sqlite3
+ pool: 5
+ timeout: 5000
+ development:
+ primary:
+ <<: *default
+ database: db/development.sqlite3
+ primary_readonly:
+ <<: *default
+ database: db/development.sqlite3
+ replica: true
+ animals:
+ <<: *default
+ database: db/development_animals.sqlite3
+ migrations_paths: db/animals_migrate
+ animals_readonly:
+ <<: *default
+ database: db/development_animals.sqlite3
+ migrations_paths: db/animals_migrate
+ replica: true
+ test:
+ primary:
+ <<: *default
+ database: db/test.sqlite3
+ primary_readonly:
+ <<: *default
+ database: db/test.sqlite3
+ replica: true
+ animals:
+ <<: *default
+ database: db/test_animals.sqlite3
+ migrations_paths: db/animals_migrate
+ animals_readonly:
+ <<: *default
+ database: db/test_animals.sqlite3
+ migrations_paths: db/animals_migrate
+ replica: true
+ production:
+ primary:
+ <<: *default
+ database: db/production.sqlite3
+ primary_readonly:
+ <<: *default
+ database: db/production.sqlite3
+ replica: true
+ animals:
+ <<: *default
+ database: db/production_animals.sqlite3
+ migrations_paths: db/animals_migrate
+ animals_readonly:
+ <<: *default
+ database: db/production_animals.sqlite3
+ migrations_paths: db/animals_migrate
+ readonly: true
+ YAML
+ end
+ else
+ File.open("#{app_path}/config/database.yml", "w") do |f|
+ f.puts <<-YAML
+ default: &default
+ adapter: sqlite3
+ pool: 5
+ timeout: 5000
+ development:
+ <<: *default
+ database: db/development.sqlite3
+ test:
+ <<: *default
+ database: db/test.sqlite3
+ production:
+ <<: *default
+ database: db/production.sqlite3
+ YAML
+ end
end
add_to_config <<-RUBY
config.eager_load = false
config.session_store :cookie_store, key: "_myapp_session"
config.active_support.deprecation = :log
- config.active_support.test_order = :random
config.action_controller.allow_forgery_protection = false
config.log_level = :info
RUBY
end
def teardown_app
- ENV['RAILS_ENV'] = @prev_rails_env if @prev_rails_env
+ ENV["RAILS_ENV"] = @prev_rails_env if @prev_rails_env
+ FileUtils.rm_rf(tmp_path)
end
# Make a very basic app, without creating the whole directory structure.
@@ -160,22 +217,22 @@ module TestHelpers
require "action_controller/railtie"
require "action_view/railtie"
- app = Class.new(Rails::Application)
- app.config.eager_load = false
- app.secrets.secret_key_base = "3b7cd727ee24e8444053437c36cc66c4"
- app.config.session_store :cookie_store, key: "_myapp_session"
- app.config.active_support.deprecation = :log
- app.config.active_support.test_order = :random
- app.config.log_level = :info
+ @app = Class.new(Rails::Application) do
+ def self.name; "RailtiesTestApp"; end
+ end
+ @app.config.eager_load = false
+ @app.config.session_store :cookie_store, key: "_myapp_session"
+ @app.config.active_support.deprecation = :log
+ @app.config.log_level = :info
- yield app if block_given?
- app.initialize!
+ yield @app if block_given?
+ @app.initialize!
- app.routes.draw do
+ @app.routes.draw do
get "/" => "omg#index"
end
- require 'rack/test'
+ require "rack/test"
extend ::Rack::Test::Methods
end
@@ -183,12 +240,12 @@ module TestHelpers
controller :foo, <<-RUBY
class FooController < ApplicationController
def index
- render text: "foo"
+ render plain: "foo"
end
end
RUBY
- app_file 'config/routes.rb', <<-RUBY
+ app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get ':controller(/:action)'
end
@@ -205,7 +262,7 @@ module TestHelpers
def write(file, string)
path = "#{@path}/#{file}"
FileUtils.mkdir_p(File.dirname(path))
- File.open(path, "w") {|f| f.puts string }
+ File.open(path, "w") { |f| f.puts string }
end
def delete(file)
@@ -218,10 +275,10 @@ module TestHelpers
FileUtils.mkdir_p(dir)
app = File.readlines("#{app_path}/config/application.rb")
- app.insert(2, "$:.unshift(\"#{dir}/lib\")")
- app.insert(3, "require #{name.inspect}")
+ app.insert(4, "$:.unshift(\"#{dir}/lib\")")
+ app.insert(5, "require #{name.inspect}")
- File.open("#{app_path}/config/application.rb", 'r+') do |f|
+ File.open("#{app_path}/config/application.rb", "r+") do |f|
f.puts app
end
@@ -230,16 +287,92 @@ module TestHelpers
end
end
- def script(script)
- Dir.chdir(app_path) do
- `#{Gem.ruby} #{app_path}/bin/rails #{script}`
+ # Invoke a bin/rails command inside the app
+ #
+ # allow_failure:: true to return normally if the command exits with
+ # a non-zero status. By default, this method will raise.
+ # stderr:: true to pass STDERR output straight to the "real" STDERR.
+ # By default, the STDERR and STDOUT of the process will be
+ # combined in the returned string.
+ def rails(*args, allow_failure: false, stderr: false)
+ args = args.flatten
+ fork = true
+
+ command = "bin/rails #{Shellwords.join args}#{' 2>&1' unless stderr}"
+
+ # Don't fork if the environment has disabled it
+ fork = false if ENV["NO_FORK"]
+
+ # Don't fork if the runtime isn't able to
+ fork = false if !Process.respond_to?(:fork)
+
+ # Don't fork if we're re-invoking minitest
+ fork = false if args.first == "t" || args.grep(/\Atest(:|\z)/).any?
+
+ if fork
+ out_read, out_write = IO.pipe
+ if stderr
+ err_read, err_write = IO.pipe
+ else
+ err_write = out_write
+ end
+
+ pid = fork do
+ out_read.close
+ err_read.close if err_read
+
+ $stdin.reopen(File::NULL, "r")
+ $stdout.reopen(out_write)
+ $stderr.reopen(err_write)
+
+ at_exit do
+ case $!
+ when SystemExit
+ exit! $!.status
+ when nil
+ exit! 0
+ else
+ err_write.puts "#{$!.class}: #{$!}"
+ exit! 1
+ end
+ end
+
+ Rails.instance_variable_set :@_env, nil
+
+ $-v = $-w = false
+ Dir.chdir app_path unless Dir.pwd == app_path
+
+ ARGV.replace(args)
+ load "./bin/rails"
+
+ exit! 0
+ end
+
+ out_write.close
+
+ if err_read
+ err_write.close
+
+ $stderr.write err_read.read
+ end
+
+ output = out_read.read
+
+ Process.waitpid pid
+
+ else
+ output = `cd #{app_path}; #{command}`
end
+
+ raise "rails command failed (#{$?.exitstatus}): #{command}\n#{output}" unless allow_failure || $?.success?
+
+ output
end
def add_to_top_of_config(str)
environment = File.read("#{app_path}/config/application.rb")
if environment =~ /(Rails::Application\s*)/
- File.open("#{app_path}/config/application.rb", 'w') do |f|
+ File.open("#{app_path}/config/application.rb", "w") do |f|
f.puts $` + $1 + "\n#{str}\n" + $'
end
end
@@ -247,8 +380,8 @@ module TestHelpers
def add_to_config(str)
environment = File.read("#{app_path}/config/application.rb")
- if environment =~ /(\n\s*end\s*end\s*)\Z/
- File.open("#{app_path}/config/application.rb", 'w') do |f|
+ if environment =~ /(\n\s*end\s*end\s*)\z/
+ File.open("#{app_path}/config/application.rb", "w") do |f|
f.puts $` + "\n#{str}\n" + $1
end
end
@@ -256,25 +389,34 @@ module TestHelpers
def add_to_env_config(env, str)
environment = File.read("#{app_path}/config/environments/#{env}.rb")
- if environment =~ /(\n\s*end\s*)\Z/
- File.open("#{app_path}/config/environments/#{env}.rb", 'w') do |f|
+ if environment =~ /(\n\s*end\s*)\z/
+ File.open("#{app_path}/config/environments/#{env}.rb", "w") do |f|
f.puts $` + "\n#{str}\n" + $1
end
end
end
def remove_from_config(str)
- file = "#{app_path}/config/application.rb"
+ remove_from_file("#{app_path}/config/application.rb", str)
+ end
+
+ def remove_from_env_config(env, str)
+ remove_from_file("#{app_path}/config/environments/#{env}.rb", str)
+ end
+
+ def remove_from_file(file, str)
contents = File.read(file)
contents.sub!(/#{str}/, "")
- File.open(file, "w+") { |f| f.puts contents }
+ File.write(file, contents)
end
- def app_file(path, contents, mode = 'w')
- FileUtils.mkdir_p File.dirname("#{app_path}/#{path}")
- File.open("#{app_path}/#{path}", mode) do |f|
+ def app_file(path, contents, mode = "w")
+ file_name = "#{app_path}/#{path}"
+ FileUtils.mkdir_p File.dirname(file_name)
+ File.open(file_name, mode) do |f|
f.puts contents
end
+ file_name
end
def remove_file(path)
@@ -286,17 +428,28 @@ module TestHelpers
end
def use_frameworks(arr)
- to_remove = [:actionmailer, :activerecord] - arr
+ to_remove = [:actionmailer, :activerecord, :activestorage, :activejob] - arr
if to_remove.include?(:activerecord)
- remove_from_config 'config.active_record.*'
+ remove_from_config "config.active_record.*"
end
- $:.reject! {|path| path =~ %r'/(#{to_remove.join('|')})/' }
+ $:.reject! { |path| path =~ %r'/(#{to_remove.join('|')})/' }
end
- def boot_rails
- require File.expand_path('../../../../load_paths', __FILE__)
+ def use_postgresql
+ File.open("#{app_path}/config/database.yml", "w") do |f|
+ f.puts <<-YAML
+ default: &default
+ adapter: postgresql
+ pool: 5
+ database: railties_test
+ development:
+ <<: *default
+ test:
+ <<: *default
+ YAML
+ end
end
end
end
@@ -306,9 +459,11 @@ class ActiveSupport::TestCase
include TestHelpers::Rack
include TestHelpers::Generation
include ActiveSupport::Testing::Stream
+ include ActiveSupport::Testing::MethodCallAssertions
- self.test_order = :sorted
-
+ def frozen_error_class
+ Object.const_defined?(:FrozenError) ? FrozenError : RuntimeError
+ end
end
# Create a scope and build a fixture rails app
@@ -317,14 +472,42 @@ Module.new do
# Build a rails app
FileUtils.rm_rf(app_template_path)
- FileUtils.mkdir(app_template_path)
+ FileUtils.mkdir_p(app_template_path)
- environment = File.expand_path('../../../../load_paths', __FILE__)
- require_environment = "-r #{environment}"
+ Dir.chdir "#{RAILS_FRAMEWORK_ROOT}/actionview" do
+ `yarn build`
+ end
- `#{Gem.ruby} #{require_environment} #{RAILS_FRAMEWORK_ROOT}/railties/exe/rails new #{app_template_path} --skip-gemfile --no-rc`
- File.open("#{app_template_path}/config/boot.rb", 'w') do |f|
- f.puts "require '#{environment}'"
+ `#{Gem.ruby} #{RAILS_FRAMEWORK_ROOT}/railties/exe/rails new #{app_template_path} --skip-gemfile --skip-listen --no-rc`
+ File.open("#{app_template_path}/config/boot.rb", "w") do |f|
f.puts "require 'rails/all'"
end
+
+ Dir.chdir(app_template_path) { `yarn add https://github.com/rails/webpacker.git` } # Use the latest version.
+
+ # Manually install `webpack` as bin symlinks are not created for subdependencies
+ # in workspaces. See https://github.com/yarnpkg/yarn/issues/4964
+ Dir.chdir(app_template_path) { `yarn add webpack@4.17.1 --tilde` }
+ Dir.chdir(app_template_path) { `yarn add webpack-cli` }
+
+ # Fake 'Bundler.require' -- we run using the repo's Gemfile, not an
+ # app-specific one: we don't want to require every gem that lists.
+ contents = File.read("#{app_template_path}/config/application.rb")
+ contents.sub!(/^Bundler\.require.*/, "%w(turbolinks webpacker).each { |r| require r }")
+ File.write("#{app_template_path}/config/application.rb", contents)
+
+ require "rails"
+
+ require "active_model"
+ require "active_job"
+ require "active_record"
+ require "action_controller"
+ require "action_mailer"
+ require "action_view"
+ require "active_storage"
+ require "action_cable"
+ require "sprockets"
+
+ require "action_view/helpers"
+ require "action_dispatch/routing/route_set"
end unless defined?(RAILS_ISOLATED_ENGINE)
diff --git a/railties/test/json_params_parsing_test.rb b/railties/test/json_params_parsing_test.rb
new file mode 100644
index 0000000000..65ad9673ff
--- /dev/null
+++ b/railties/test/json_params_parsing_test.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "action_dispatch"
+require "active_record"
+
+class JsonParamsParsingTest < ActionDispatch::IntegrationTest
+ def test_prevent_null_query
+ # Make sure we have data to find
+ klass = Class.new(ActiveRecord::Base) do
+ def self.name; "Foo"; end
+ establish_connection adapter: "sqlite3", database: ":memory:"
+ connection.create_table "foos" do |t|
+ t.string :title
+ t.timestamps null: false
+ end
+ end
+ klass.create
+ assert klass.first
+
+ app = ->(env) {
+ request = ActionDispatch::Request.new env
+ params = ActionController::Parameters.new request.parameters
+ if params[:t]
+ klass.find_by_title(params[:t])
+ else
+ nil
+ end
+ }
+
+ assert_nil app.call(make_env("t" => nil))
+ assert_nil app.call(make_env("t" => [nil]))
+
+ [[[nil]], [[[nil]]]].each do |data|
+ assert_nil app.call(make_env("t" => data))
+ end
+ ensure
+ klass.connection.drop_table("foos")
+ end
+
+ private
+ def make_env(json)
+ data = JSON.dump json
+ content_length = data.length
+ {
+ "CONTENT_LENGTH" => content_length,
+ "CONTENT_TYPE" => "application/json",
+ "rack.input" => StringIO.new(data)
+ }
+ end
+end
diff --git a/railties/test/minitest/rails_plugin_test.rb b/railties/test/minitest/rails_plugin_test.rb
new file mode 100644
index 0000000000..7c3a2022a9
--- /dev/null
+++ b/railties/test/minitest/rails_plugin_test.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class Minitest::RailsPluginTest < ActiveSupport::TestCase
+ setup do
+ @options = Minitest.process_args []
+ @output = StringIO.new("".encode("UTF-8"))
+ end
+
+ test "default reporters are replaced" do
+ with_reporter Minitest::CompositeReporter.new do |reporter|
+ reporter << Minitest::SummaryReporter.new(@output, @options)
+ reporter << Minitest::ProgressReporter.new(@output, @options)
+ reporter << Minitest::Reporter.new(@output, @options)
+
+ Minitest.plugin_rails_init({})
+
+ assert_equal 3, reporter.reporters.count
+ assert reporter.reporters.any? { |candidate| candidate.kind_of?(Minitest::SuppressedSummaryReporter) }
+ assert reporter.reporters.any? { |candidate| candidate.kind_of?(::Rails::TestUnitReporter) }
+ assert reporter.reporters.any? { |candidate| candidate.kind_of?(Minitest::Reporter) }
+ end
+ end
+
+ test "no custom reporters are added if nothing to replace" do
+ with_reporter Minitest::CompositeReporter.new do |reporter|
+ Minitest.plugin_rails_init({})
+
+ assert_empty reporter.reporters
+ end
+ end
+
+ private
+ def with_reporter(reporter)
+ old_reporter, Minitest.reporter = Minitest.reporter, reporter
+
+ yield reporter
+ ensure
+ Minitest.reporter = old_reporter
+ end
+end
diff --git a/railties/test/path_generation_test.rb b/railties/test/path_generation_test.rb
index 27e64b97b7..849b183b37 100644
--- a/railties/test/path_generation_test.rb
+++ b/railties/test/path_generation_test.rb
@@ -1,6 +1,8 @@
-require 'abstract_unit'
-require 'active_support/core_ext/object/with_options'
-require 'active_support/core_ext/object/json'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/object/with_options"
+require "active_support/core_ext/object/json"
class PathGenerationTest < ActiveSupport::TestCase
attr_reader :app
@@ -11,26 +13,26 @@ class PathGenerationTest < ActiveSupport::TestCase
super()
end
- class Dispatcher < ActionDispatch::Routing::RouteSet::Dispatcher
- def initialize(defaults, set, block)
- super(defaults)
+ class Request < DelegateClass(ActionDispatch::Request)
+ def initialize(target, url_helpers, block)
+ super(target)
+ @url_helpers = url_helpers
@block = block
- @set = set
end
- def controller_reference(controller_param)
+ def controller_class
+ url_helpers = @url_helpers
block = @block
- set = @set
Class.new(ActionController::Base) {
- include set.url_helpers
+ include url_helpers
define_method(:process) { |name| block.call(self) }
def to_a; [200, {}, []]; end
}
end
end
- def dispatcher defaults
- TestSet::Dispatcher.new defaults, self, @block
+ def make_request(env)
+ Request.new(super, url_helpers, @block)
end
end
@@ -38,11 +40,11 @@ class PathGenerationTest < ActiveSupport::TestCase
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 }
- params['SCRIPT_NAME'] = script_name if script_name
+ params["SCRIPT_NAME"] = script_name if script_name
status, headers, body = app.call(params)
new_body = []
@@ -56,12 +58,14 @@ class PathGenerationTest < ActiveSupport::TestCase
Rails.logger = Logger.new nil
app = Class.new(Rails::Application) {
+ def self.name; "ScriptNameTestApp"; end
+
attr_accessor :controller
+
def initialize
super
app = self
@routes = TestSet.new ->(c) { app.controller = c }
- secrets.secret_key_base = "foo"
secrets.secret_token = "foo"
end
def app; routes; end
@@ -72,11 +76,11 @@ class PathGenerationTest < ActiveSupport::TestCase
url = URI("http://example.org/blogs")
- send_request(url, 'GET', nil, '/FOO')
- assert_equal '/FOO/blogs', app.instance.controller.blogs_path
+ send_request(url, "GET", nil, "/FOO")
+ assert_equal "/FOO/blogs", app.instance.controller.blogs_path
- send_request(url, 'GET', nil)
- assert_equal '/blogs', app.instance.controller.blogs_path
+ send_request(url, "GET", nil)
+ assert_equal "/blogs", app.instance.controller.blogs_path
ensure
Rails.logger = original_logger
end
diff --git a/railties/test/paths_test.rb b/railties/test/paths_test.rb
index 12630e4d01..9f5bb37c20 100644
--- a/railties/test/paths_test.rb
+++ b/railties/test/paths_test.rb
@@ -1,6 +1,8 @@
-require 'abstract_unit'
-require 'rails/paths'
-require 'minitest/mock'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "rails/paths"
+require "minitest/mock"
class PathsTest < ActiveSupport::TestCase
def setup
@@ -62,6 +64,13 @@ class PathsTest < ActiveSupport::TestCase
assert_equal ["/foo/bar/baz"], @root["app/models"].to_a
end
+ test "absolute current path" do
+ @root.add "config"
+ @root.add "config/locales"
+
+ assert_equal "/foo/bar/config/locales", @root["config/locales"].absolute_current
+ end
+
test "adding multiple physical paths as an array" do
@root.add "app", with: ["/app", "/app2"]
assert_equal ["/app", "/app2"], @root["app"].to_a
@@ -95,35 +104,35 @@ class PathsTest < ActiveSupport::TestCase
File.stub(:exist?, true) do
@root.add "app", with: "/app"
@root["app"].autoload_once!
- assert @root["app"].autoload_once?
- assert @root.autoload_once.include?(@root["app"].expanded.first)
+ assert_predicate @root["app"], :autoload_once?
+ assert_includes @root.autoload_once, @root["app"].expanded.first
end
end
test "it is possible to remove a path that should be autoloaded only once" do
@root["app"] = "/app"
@root["app"].autoload_once!
- assert @root["app"].autoload_once?
+ assert_predicate @root["app"], :autoload_once?
@root["app"].skip_autoload_once!
- assert !@root["app"].autoload_once?
- assert !@root.autoload_once.include?(@root["app"].expanded.first)
+ assert_not_predicate @root["app"], :autoload_once?
+ assert_not_includes @root.autoload_once, @root["app"].expanded.first
end
test "it is possible to add a path without assignment and specify it should be loaded only once" do
File.stub(:exist?, true) do
@root.add "app", with: "/app", autoload_once: true
- assert @root["app"].autoload_once?
- assert @root.autoload_once.include?("/app")
+ assert_predicate @root["app"], :autoload_once?
+ assert_includes @root.autoload_once, "/app"
end
end
test "it is possible to add multiple paths without assignment and specify it should be loaded only once" do
File.stub(:exist?, true) do
@root.add "app", with: ["/app", "/app2"], autoload_once: true
- assert @root["app"].autoload_once?
- assert @root.autoload_once.include?("/app")
- assert @root.autoload_once.include?("/app2")
+ assert_predicate @root["app"], :autoload_once?
+ assert_includes @root.autoload_once, "/app"
+ assert_includes @root.autoload_once, "/app2"
end
end
@@ -132,7 +141,7 @@ class PathsTest < ActiveSupport::TestCase
@root["app"] = "/app"
@root["app"].autoload_once!
@root["app"].autoload_once!
- assert_equal 1, @root.autoload_once.select {|p| p == @root["app"].expanded.first }.size
+ assert_equal 1, @root.autoload_once.select { |p| p == @root["app"].expanded.first }.size
end
end
@@ -149,45 +158,45 @@ class PathsTest < ActiveSupport::TestCase
File.stub(:exist?, true) do
@root["app"] = "/app"
@root["app"].eager_load!
- assert @root["app"].eager_load?
- assert @root.eager_load.include?(@root["app"].to_a.first)
+ assert_predicate @root["app"], :eager_load?
+ assert_includes @root.eager_load, @root["app"].to_a.first
end
end
test "it is possible to skip a path from eager loading" do
@root["app"] = "/app"
@root["app"].eager_load!
- assert @root["app"].eager_load?
+ assert_predicate @root["app"], :eager_load?
@root["app"].skip_eager_load!
- assert !@root["app"].eager_load?
- assert !@root.eager_load.include?(@root["app"].to_a.first)
+ assert_not_predicate @root["app"], :eager_load?
+ assert_not_includes @root.eager_load, @root["app"].to_a.first
end
test "it is possible to add a path without assignment and mark it as eager" do
File.stub(:exist?, true) do
@root.add "app", with: "/app", eager_load: true
- assert @root["app"].eager_load?
- assert @root.eager_load.include?("/app")
+ assert_predicate @root["app"], :eager_load?
+ assert_includes @root.eager_load, "/app"
end
end
test "it is possible to add multiple paths without assignment and mark them as eager" do
File.stub(:exist?, true) do
@root.add "app", with: ["/app", "/app2"], eager_load: true
- assert @root["app"].eager_load?
- assert @root.eager_load.include?("/app")
- assert @root.eager_load.include?("/app2")
+ assert_predicate @root["app"], :eager_load?
+ assert_includes @root.eager_load, "/app"
+ assert_includes @root.eager_load, "/app2"
end
end
test "it is possible to create a path without assignment and mark it both as eager and load once" do
File.stub(:exist?, true) do
@root.add "app", with: "/app", eager_load: true, autoload_once: true
- assert @root["app"].eager_load?
- assert @root["app"].autoload_once?
- assert @root.eager_load.include?("/app")
- assert @root.autoload_once.include?("/app")
+ assert_predicate @root["app"], :eager_load?
+ assert_predicate @root["app"], :autoload_once?
+ assert_includes @root.eager_load, "/app"
+ assert_includes @root.autoload_once, "/app"
end
end
@@ -196,7 +205,7 @@ class PathsTest < ActiveSupport::TestCase
@root["app"] = "/app"
@root["app"].eager_load!
@root["app"].eager_load!
- assert_equal 1, @root.eager_load.select {|p| p == @root["app"].expanded.first }.size
+ assert_equal 1, @root.eager_load.select { |p| p == @root["app"].expanded.first }.size
end
end
@@ -215,6 +224,12 @@ class PathsTest < ActiveSupport::TestCase
assert_equal "*.rb", @root["app"].glob
end
+ test "it should be possible to get extensions by glob" do
+ @root["app"] = "/app"
+ @root["app"].glob = "*.{rb,yml}"
+ assert_equal ["rb", "yml"], @root["app"].extensions
+ end
+
test "it should be possible to override a path's default glob without assignment" do
@root.add "app", with: "/app", glob: "*.rb"
assert_equal "*.rb", @root["app"].glob
@@ -239,7 +254,7 @@ class PathsTest < ActiveSupport::TestCase
test "a path can be added to the load path on creation" do
File.stub(:exist?, true) do
@root.add "app", with: "/app", load_path: true
- assert @root["app"].load_path?
+ assert_predicate @root["app"], :load_path?
assert_equal ["/app"], @root.load_paths
end
end
@@ -256,8 +271,28 @@ class PathsTest < ActiveSupport::TestCase
test "a path can be marked as autoload on creation" do
File.stub(:exist?, true) do
@root.add "app", with: "/app", autoload: true
- assert @root["app"].autoload?
+ assert_predicate @root["app"], :autoload?
assert_equal ["/app"], @root.autoload_paths
end
end
end
+
+class PathsIntegrationTest < ActiveSupport::TestCase
+ test "A failed symlink is still a valid file" do
+ Dir.mktmpdir do |dir|
+ Dir.chdir(dir) do
+ FileUtils.mkdir_p("foo")
+ File.symlink("foo/doesnotexist.rb", "foo/bar.rb")
+ assert_equal true, File.symlink?("foo/bar.rb")
+
+ root = Rails::Paths::Root.new("foo")
+ root.add "bar.rb"
+
+ exception = assert_raises(RuntimeError) do
+ root["bar.rb"].existent
+ end
+ assert_match File.expand_path("foo/bar.rb"), exception.message
+ end
+ end
+ end
+end
diff --git a/railties/test/rack_logger_test.rb b/railties/test/rack_logger_test.rb
index fcc79b57fb..ac37062e6d 100644
--- a/railties/test/rack_logger_test.rb
+++ b/railties/test/rack_logger_test.rb
@@ -1,8 +1,10 @@
-require 'abstract_unit'
-require 'active_support/testing/autorun'
-require 'active_support/test_case'
-require 'rails/rack/logger'
-require 'logger'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/testing/autorun"
+require "active_support/test_case"
+require "rails/rack/logger"
+require "logger"
module Rails
module Rack
@@ -12,15 +14,22 @@ module Rails
attr_reader :logger
- def initialize(logger = NULL, taggers = nil, &block)
- super(->(_) { block.call; [200, {}, []] }, taggers)
+ def initialize(logger = NULL, app: nil, taggers: nil, &block)
+ app ||= ->(_) { block.call; [200, {}, []] }
+ super(app, taggers)
@logger = logger
end
def development?; false; end
end
- class Subscriber < Struct.new(:starts, :finishes)
+ class TestApp < Struct.new(:response)
+ def call(_env)
+ response
+ end
+ end
+
+ Subscriber = Struct.new(:starts, :finishes) do
def initialize(starts = [], finishes = [])
super
end
@@ -39,7 +48,7 @@ module Rails
def setup
@subscriber = Subscriber.new
@notifier = ActiveSupport::Notifications.notifier
- @subscription = notifier.subscribe 'request.action_dispatch', subscriber
+ @subscription = notifier.subscribe "request.action_dispatch", subscriber
end
def teardown
@@ -49,9 +58,9 @@ module Rails
def test_notification
logger = TestLogger.new { }
- assert_difference('subscriber.starts.length') do
- assert_difference('subscriber.finishes.length') do
- logger.call('REQUEST_METHOD' => 'GET').last.close
+ assert_difference("subscriber.starts.length") do
+ assert_difference("subscriber.finishes.length") do
+ logger.call("REQUEST_METHOD" => "GET").last.close
end
end
end
@@ -62,14 +71,25 @@ module Rails
raise NotImplementedError
end
- assert_difference('subscriber.starts.length') do
- assert_difference('subscriber.finishes.length') do
+ assert_difference("subscriber.starts.length") do
+ assert_difference("subscriber.finishes.length") do
assert_raises(NotImplementedError) do
- logger.call 'REQUEST_METHOD' => 'GET'
+ logger.call "REQUEST_METHOD" => "GET"
end
end
end
end
+
+ def test_logger_does_not_mutate_app_return
+ response = [].freeze
+ app = TestApp.new(response)
+ logger = TestLogger.new(app: app)
+ assert_no_changes("response") do
+ assert_nothing_raised do
+ logger.call("REQUEST_METHOD" => "GET")
+ end
+ end
+ end
end
end
end
diff --git a/railties/test/rails_info_controller_test.rb b/railties/test/rails_info_controller_test.rb
index c51503c2b7..6ab68f8333 100644
--- a/railties/test/rails_info_controller_test.rb
+++ b/railties/test/rails_info_controller_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module ActionController
class Base
@@ -11,8 +13,8 @@ class InfoControllerTest < ActionController::TestCase
def setup
Rails.application.routes.draw do
- get '/rails/info/properties' => "rails/info#properties"
- get '/rails/info/routes' => "rails/info#routes"
+ get "/rails/info/properties" => "rails/info#properties"
+ get "/rails/info/routes" => "rails/info#routes"
end
@routes = Rails.application.routes
@@ -30,7 +32,7 @@ class InfoControllerTest < ActionController::TestCase
test "info controller renders an error message when request was forbidden" do
@request.env["REMOTE_ADDR"] = "example.org"
get :properties
- assert_select 'p'
+ assert_select "p"
end
test "info controller allows requests when all requests are considered local" do
@@ -45,7 +47,12 @@ class InfoControllerTest < ActionController::TestCase
test "info controller renders a table with properties" do
get :properties
- assert_select 'table'
+ assert_select "table"
+ end
+
+ test "info controller renders json with properties" do
+ get :properties, format: :json
+ assert_equal Rails::Info.to_json, response.body
end
test "info controller renders with routes" do
@@ -54,28 +61,34 @@ class InfoControllerTest < ActionController::TestCase
end
test "info controller returns exact matches" do
- exact_count = -> { JSON(response.body)['exact'].size }
+ exact_count = -> { JSON(response.body)["exact"].size }
- get :routes, params: { path: 'rails/info/route' }
- assert exact_count.call == 0, 'should not match incomplete routes'
+ get :routes, params: { path: "rails/info/route" }
+ assert exact_count.call == 0, "should not match incomplete routes"
- get :routes, params: { path: 'rails/info/routes' }
- assert exact_count.call == 1, 'should match complete routes'
+ get :routes, params: { path: "rails/info/routes" }
+ assert exact_count.call == 1, "should match complete routes"
- get :routes, params: { path: 'rails/info/routes.html' }
- assert exact_count.call == 1, 'should match complete routes with optional parts'
+ get :routes, params: { path: "rails/info/routes.html" }
+ assert exact_count.call == 1, "should match complete routes with optional parts"
end
test "info controller returns fuzzy matches" do
- fuzzy_count = -> { JSON(response.body)['fuzzy'].size }
+ fuzzy_count = -> { JSON(response.body)["fuzzy"].size }
- get :routes, params: { path: 'rails/info' }
- assert fuzzy_count.call == 2, 'should match incomplete routes'
+ get :routes, params: { path: "rails/info" }
+ assert fuzzy_count.call == 2, "should match incomplete routes"
- get :routes, params: { path: 'rails/info/routes' }
- assert fuzzy_count.call == 1, 'should match complete routes'
+ get :routes, params: { path: "rails/info/routes" }
+ assert fuzzy_count.call == 1, "should match complete routes"
- get :routes, params: { path: 'rails/info/routes.html' }
- assert fuzzy_count.call == 0, 'should match optional parts of route literally'
+ get :routes, params: { path: "rails/info/routes.html" }
+ assert fuzzy_count.call == 0, "should match optional parts of route literally"
+ end
+
+ test "internal routes do not have a default params[:internal] value" do
+ get :properties
+ assert_response :success
+ assert_nil @controller.params[:internal]
end
end
diff --git a/railties/test/rails_info_test.rb b/railties/test/rails_info_test.rb
index 92e4af25b5..d167a86e56 100644
--- a/railties/test/rails_info_test.rb
+++ b/railties/test/rails_info_test.rb
@@ -1,60 +1,61 @@
-require 'abstract_unit'
+# frozen_string_literal: true
-unless defined?(Rails) && defined?(Rails::Info)
- module Rails
- class Info; end
- end
-end
-
-require "active_support/core_ext/kernel/reporting"
+require "abstract_unit"
class InfoTest < ActiveSupport::TestCase
- def setup
- Rails.send :remove_const, :Info
- silence_warnings { load 'rails/info.rb' }
- end
-
def test_property_with_block_swallows_exceptions_and_ignores_property
assert_nothing_raised do
Rails::Info.module_eval do
- property('Bogus') {raise}
+ property("Bogus") { raise }
end
end
- assert !property_defined?('Bogus')
+ assert_not property_defined?("Bogus")
end
def test_property_with_string
Rails::Info.module_eval do
- property 'Hello', 'World'
+ property "Hello", "World"
end
- assert_property 'Hello', 'World'
+ assert_property "Hello", "World"
end
def test_property_with_block
Rails::Info.module_eval do
- property('Goodbye') {'World'}
+ property("Goodbye") { "World" }
end
- assert_property 'Goodbye', 'World'
+ assert_property "Goodbye", "World"
end
def test_rails_version
- assert_property 'Rails version',
- File.read(File.realpath('../../../RAILS_VERSION', __FILE__)).chomp
+ assert_property "Rails version",
+ File.read(File.realpath("../../RAILS_VERSION", __dir__)).chomp
end
def test_html_includes_middleware
Rails::Info.module_eval do
- property 'Middleware', ['Rack::Lock', 'Rack::Static']
+ property "Middleware", ["Rack::Lock", "Rack::Static"]
end
html = Rails::Info.to_html
- assert html.include?('<tr><td class="name">Middleware</td>')
- properties.value_for('Middleware').each do |value|
- assert html.include?("<li>#{CGI.escapeHTML(value)}</li>")
+ assert_includes html, '<tr><td class="name">Middleware</td>'
+ properties.value_for("Middleware").each do |value|
+ assert_includes html, "<li>#{CGI.escapeHTML(value)}</li>"
+ end
+ end
+
+ def test_json_includes_middleware
+ Rails::Info.module_eval do
+ property "Middleware", ["Rack::Lock", "Rack::Static"]
+ end
+
+ hash = JSON.parse(Rails::Info.to_json)
+ assert_includes hash.keys, "Middleware"
+ properties.value_for("Middleware").each do |value|
+ assert_includes hash["Middleware"], value
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 79bd7a8241..4ac8f8d741 100644
--- a/railties/test/railties/engine_test.rb
+++ b/railties/test/railties/engine_test.rb
@@ -1,10 +1,11 @@
+# frozen_string_literal: true
+
require "isolation/abstract_unit"
require "stringio"
require "rack/test"
module RailtiesTest
class EngineTest < ActiveSupport::TestCase
-
include ActiveSupport::Testing::Isolation
include Rack::Test::Methods
@@ -28,17 +29,19 @@ module RailtiesTest
end
def boot_rails
- super
require "#{app_path}/config/environment"
end
+ def migrations
+ migration_root = File.expand_path(ActiveRecord::Migrator.migrations_paths.first, app_path)
+ ActiveRecord::MigrationContext.new(migration_root).migrations
+ end
+
test "serving sprocket's assets" do
@plugin.write "app/assets/javascripts/engine.js.erb", "<%= :alert %>();"
add_to_env_config "development", "config.assets.digest = false"
boot_rails
- require 'rack/test'
- extend Rack::Test::Methods
get "/assets/engine.js"
assert_match "alert()", last_response.body
@@ -63,56 +66,57 @@ module RailtiesTest
test "copying migrations" do
@plugin.write "db/migrate/1_create_users.rb", <<-RUBY
- class CreateUsers < ActiveRecord::Migration
+ class CreateUsers < ActiveRecord::Migration::Current
end
RUBY
@plugin.write "db/migrate/2_add_last_name_to_users.rb", <<-RUBY
- class AddLastNameToUsers < ActiveRecord::Migration
+ class AddLastNameToUsers < ActiveRecord::Migration::Current
end
RUBY
@plugin.write "db/migrate/3_create_sessions.rb", <<-RUBY
- class CreateSessions < ActiveRecord::Migration
+ class CreateSessions < ActiveRecord::Migration::Current
end
RUBY
app_file "db/migrate/1_create_sessions.rb", <<-RUBY
- class CreateSessions < ActiveRecord::Migration
+ class CreateSessions < ActiveRecord::Migration::Current
def up
end
end
RUBY
- add_to_config "ActiveRecord::Base.timestamped_migrations = false"
-
boot_rails
Dir.chdir(app_path) do
+ # Install Active Storage migration file first so as not to affect test.
+ `bundle exec rake active_storage:install`
output = `bundle exec rake bukkits:install:migrations`
- assert File.exist?("#{app_path}/db/migrate/2_create_users.bukkits.rb")
- assert File.exist?("#{app_path}/db/migrate/3_add_last_name_to_users.bukkits.rb")
- assert_match(/Copied migration 2_create_users.bukkits.rb from bukkits/, output)
- assert_match(/Copied migration 3_add_last_name_to_users.bukkits.rb from bukkits/, output)
- assert_match(/NOTE: Migration 3_create_sessions.rb from bukkits has been skipped/, output)
- assert_equal 3, Dir["#{app_path}/db/migrate/*.rb"].length
+ ["CreateUsers", "AddLastNameToUsers", "CreateSessions"].each do |migration_name|
+ assert migrations.detect { |migration| migration.name == migration_name }
+ end
+ assert_match(/Copied migration \d+_create_users\.bukkits\.rb from bukkits/, output)
+ assert_match(/Copied migration \d+_add_last_name_to_users\.bukkits\.rb from bukkits/, output)
+ assert_match(/NOTE: Migration \d+_create_sessions\.rb from bukkits has been skipped/, output)
- output = `bundle exec rake railties:install:migrations`.split("\n")
+ migrations_count = Dir["#{app_path}/db/migrate/*.rb"].length
- assert_no_match(/2_create_users/, output.join("\n"))
+ assert_equal migrations.length, migrations_count
- bukkits_migration_order = output.index(output.detect{|o| /NOTE: Migration 3_create_sessions.rb from bukkits has been skipped/ =~ o })
- assert_not_nil bukkits_migration_order, "Expected migration to be skipped"
-
- migrations_count = Dir["#{app_path}/db/migrate/*.rb"].length
- `bundle exec rake railties:install:migrations`
+ output = `bundle exec rake railties:install:migrations`.split("\n")
assert_equal migrations_count, Dir["#{app_path}/db/migrate/*.rb"].length
+
+ assert_no_match(/\d+_create_users/, output.join("\n"))
+
+ bukkits_migration_order = output.index(output.detect { |o| /NOTE: Migration \d+_create_sessions\.rb from bukkits has been skipped/ =~ o })
+ assert_not_nil bukkits_migration_order, "Expected migration to be skipped"
end
end
- test 'respects the order of railties when installing migrations' do
+ test "respects the order of railties when installing migrations" do
@blog = engine "blog" do |plugin|
plugin.write "lib/blog.rb", <<-RUBY
module Blog
@@ -123,12 +127,12 @@ module RailtiesTest
end
@plugin.write "db/migrate/1_create_users.rb", <<-RUBY
- class CreateUsers < ActiveRecord::Migration
+ class CreateUsers < ActiveRecord::Migration::Current
end
RUBY
@blog.write "db/migrate/2_create_blogs.rb", <<-RUBY
- class CreateBlogs < ActiveRecord::Migration
+ class CreateBlogs < ActiveRecord::Migration::Current
end
RUBY
@@ -137,10 +141,10 @@ module RailtiesTest
boot_rails
Dir.chdir(app_path) do
- output = `bundle exec rake railties:install:migrations`.split("\n")
+ output = `bundle exec rake railties:install:migrations`.split("\n")
- assert_match(/Copied migration \d+_create_users.bukkits.rb from bukkits/, output.first)
- assert_match(/Copied migration \d+_create_blogs.blog_engine.rb from blog_engine/, output.last)
+ assert_match(/Copied migration \d+_create_users\.bukkits\.rb from bukkits/, output.first)
+ assert_match(/Copied migration \d+_create_blogs\.blog_engine\.rb from blog_engine/, output.second)
end
end
@@ -163,20 +167,22 @@ module RailtiesTest
end
@core.write "db/migrate/1_create_users.rb", <<-RUBY
- class CreateUsers < ActiveRecord::Migration; end
+ class CreateUsers < ActiveRecord::Migration::Current; end
RUBY
@api.write "db/migrate/2_create_keys.rb", <<-RUBY
- class CreateKeys < ActiveRecord::Migration; end
+ class CreateKeys < ActiveRecord::Migration::Current; end
RUBY
boot_rails
Dir.chdir(app_path) do
- output = `bundle exec rake railties:install:migrations`.split("\n")
+ # Install Active Storage migration file first so as not to affect test.
+ `bundle exec rake active_storage:install`
+ output = `bundle exec rake railties:install:migrations`.split("\n")
- assert_match(/Copied migration \d+_create_users.core_engine.rb from core_engine/, output.first)
- assert_match(/Copied migration \d+_create_keys.api_engine.rb from api_engine/, output.last)
+ assert_match(/Copied migration \d+_create_users\.core_engine\.rb from core_engine/, output.first)
+ assert_match(/Copied migration \d+_create_keys\.api_engine\.rb from api_engine/, output.second)
end
end
@@ -190,7 +196,7 @@ module RailtiesTest
RUBY
@plugin.write "db/migrate/0_add_first_name_to_users.rb", <<-RUBY
- class AddFirstNameToUsers < ActiveRecord::Migration
+ class AddFirstNameToUsers < ActiveRecord::Migration::Current
end
RUBY
@@ -205,19 +211,22 @@ module RailtiesTest
Dir.chdir(@plugin.path) do
output = `bundle exec rake app:bukkits:install:migrations`
- assert File.exist?("#{app_path}/db/migrate/0_add_first_name_to_users.bukkits.rb")
- assert_match(/Copied migration 0_add_first_name_to_users.bukkits.rb from bukkits/, output)
- assert_equal 1, Dir["#{app_path}/db/migrate/*.rb"].length
+
+ migration_with_engine_path = migrations.detect { |migration| migration.name == "AddFirstNameToUsers" }
+ assert migration_with_engine_path
+ assert_match(/\/db\/migrate\/\d+_add_first_name_to_users\.bukkits\.rb/, migration_with_engine_path.filename)
+ assert_match(/Copied migration \d+_add_first_name_to_users\.bukkits\.rb from bukkits/, output)
+ assert_equal migrations.length, Dir["#{app_path}/db/migrate/*.rb"].length
end
end
test "no rake task without migrations" do
boot_rails
- require 'rake'
- require 'rdoc/task'
- require 'rake/testtask'
+ require "rake"
+ require "rdoc/task"
+ require "rake/testtask"
Rails.application.load_tasks
- assert !Rake::Task.task_defined?('bukkits:install:migrations')
+ assert_not Rake::Task.task_defined?("bukkits:install:migrations")
end
test "puts its lib directory on load path" do
@@ -322,8 +331,6 @@ module RailtiesTest
RUBY
boot_rails
- require 'rack/test'
- extend Rack::Test::Methods
get "/sprokkit"
assert_equal "I am a Sprokkit", last_response.body
@@ -333,7 +340,7 @@ module RailtiesTest
controller "foo", <<-RUBY
class FooController < ActionController::Base
def index
- render :text => "foo"
+ render plain: "foo"
end
end
RUBY
@@ -347,7 +354,7 @@ module RailtiesTest
@plugin.write "app/controllers/bar_controller.rb", <<-RUBY
class BarController < ActionController::Base
def index
- render :text => "bar"
+ render plain: "bar"
end
end
RUBY
@@ -360,14 +367,12 @@ module RailtiesTest
RUBY
boot_rails
- require 'rack/test'
- extend Rack::Test::Methods
- get '/foo'
- assert_equal 'foo', last_response.body
+ get "/foo"
+ assert_equal "foo", last_response.body
- get '/bar'
- assert_equal 'bar', last_response.body
+ get "/bar"
+ assert_equal "bar", last_response.body
end
test "rake tasks lib tasks are loaded" do
@@ -379,9 +384,9 @@ module RailtiesTest
RUBY
boot_rails
- require 'rake'
- require 'rdoc/task'
- require 'rake/testtask'
+ require "rake"
+ require "rdoc/task"
+ require "rake/testtask"
Rails.application.load_tasks
Rake::Task[:foo].invoke
assert $executed
@@ -392,18 +397,18 @@ module RailtiesTest
config.i18n.load_path << "#{app_path}/app/locales/en.yml"
RUBY
- app_file 'app/locales/en.yml', <<-YAML
+ app_file "app/locales/en.yml", <<-YAML
en:
bar: "1"
YAML
- app_file 'config/locales/en.yml', <<-YAML
+ app_file "config/locales/en.yml", <<-YAML
en:
foo: "2"
bar: "2"
YAML
- @plugin.write 'config/locales/en.yml', <<-YAML
+ @plugin.write "config/locales/en.yml", <<-YAML
en:
foo: "3"
YAML
@@ -444,14 +449,12 @@ YAML
@plugin.write "app/controllers/admin/foo/bar_controller.rb", <<-RUBY
class Admin::Foo::BarController < ApplicationController
def index
- render text: "Rendered from namespace"
+ render plain: "Rendered from namespace"
end
end
RUBY
boot_rails
- require 'rack/test'
- extend Rack::Test::Methods
get "/admin/foo/bar"
assert_equal 200, last_response.status
@@ -481,7 +484,7 @@ YAML
end
RUBY
- add_to_config "config.middleware.use \"Bukkits\""
+ add_to_config "config.middleware.use Bukkits"
boot_rails
end
@@ -513,7 +516,7 @@ YAML
def call(env)
response = @app.call(env)
- response[2].each(&:upcase!)
+ response[2] = response[2].collect(&:upcase)
response
end
end
@@ -546,7 +549,7 @@ YAML
controller "foo", <<-RUBY
class FooController < ActionController::Base
def index
- render text: params[:username]
+ render plain: params[:username]
end
end
RUBY
@@ -567,7 +570,6 @@ YAML
get("/arunagw")
assert_equal "arunagw", last_response.body
-
end
test "it provides routes as default endpoint" do
@@ -637,11 +639,11 @@ YAML
env = Rack::MockRequest.env_for("/")
Bukkits::Engine.call(env)
- assert_equal Bukkits::Engine.routes, env['action_dispatch.routes']
+ assert_equal Bukkits::Engine.routes, env["action_dispatch.routes"]
env = Rack::MockRequest.env_for("/")
Rails.application.call(env)
- assert_equal Rails.application.routes, env['action_dispatch.routes']
+ assert_equal Rails.application.routes, env["action_dispatch.routes"]
end
test "isolated engine should include only its own routes and helpers" do
@@ -710,7 +712,7 @@ YAML
end
def show
- render text: foo_path
+ render plain: foo_path
end
def from_app
@@ -722,7 +724,7 @@ YAML
end
def polymorphic_path_without_namespace
- render text: polymorphic_path(Post.new)
+ render plain: polymorphic_path(Post.new)
end
end
RUBY
@@ -742,7 +744,7 @@ YAML
assert_equal "bukkits", Bukkits::Engine.engine_name
assert_equal Bukkits.railtie_namespace, Bukkits::Engine
assert ::Bukkits::MyMailer.method_defined?(:foo_url)
- assert !::Bukkits::MyMailer.method_defined?(:bar_url)
+ assert_not ::Bukkits::MyMailer.method_defined?(:bar_url)
get("/bukkits/from_app")
assert_equal "false", last_response.body
@@ -845,7 +847,7 @@ YAML
@plugin.write "app/controllers/bukkits/awesome/foo_controller.rb", <<-RUBY
class Bukkits::Awesome::FooController < ActionController::Base
def index
- render :text => "ok"
+ render plain: "ok"
end
end
RUBY
@@ -892,7 +894,17 @@ YAML
end
RUBY
- add_to_config "isolate_namespace AppTemplate"
+ engine "loaded_first" do |plugin|
+ plugin.write "lib/loaded_first.rb", <<-RUBY
+ module AppTemplate
+ module LoadedFirst
+ class Engine < ::Rails::Engine
+ isolate_namespace(AppTemplate)
+ end
+ end
+ end
+ RUBY
+ end
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do end
@@ -900,7 +912,7 @@ YAML
boot_rails
- assert_equal AppTemplate.railtie_namespace, AppTemplate::Engine
+ assert_equal AppTemplate::LoadedFirst::Engine, AppTemplate.railtie_namespace
end
test "properly reload routes" do
@@ -967,14 +979,14 @@ YAML
boot_rails
app_generators = Rails.application.config.generators.options[:rails]
- assert_equal :mongoid , app_generators[:orm]
- assert_equal :liquid , app_generators[:template_engine]
+ assert_equal :mongoid, app_generators[:orm]
+ assert_equal :liquid, app_generators[:template_engine]
assert_equal :test_unit, app_generators[:test_framework]
generators = Bukkits::Engine.config.generators.options[:rails]
assert_equal :data_mapper, generators[:orm]
- assert_equal :haml , generators[:template_engine]
- assert_equal :rspec , generators[:test_framework]
+ assert_equal :haml, generators[:template_engine]
+ assert_equal :rspec, generators[:test_framework]
end
test "engine should get default generators with ability to overwrite them" do
@@ -990,10 +1002,10 @@ YAML
generators = Bukkits::Engine.config.generators.options[:rails]
assert_equal :active_record, generators[:orm]
- assert_equal :rspec , generators[:test_framework]
+ assert_equal :rspec, generators[:test_framework]
app_generators = Rails.application.config.generators.options[:rails]
- assert_equal :test_unit , app_generators[:test_framework]
+ assert_equal :test_unit, app_generators[:test_framework]
end
test "do not create table_name_prefix method if it already exists" do
@@ -1028,7 +1040,7 @@ YAML
# check expanding paths
engine_dir = @plugin.path.chomp("/").split("/").last
- engine_path = File.join(@plugin.path, '..', engine_dir)
+ engine_path = File.join(@plugin.path, "..", engine_dir)
assert_equal Bukkits::Engine.instance, Rails::Engine.find(engine_path)
end
@@ -1155,10 +1167,10 @@ YAML
assert_equal "App's bar partial", last_response.body.strip
get("/assets/foo.js")
- assert_equal "// Bukkit's foo js", last_response.body.strip
+ assert_match "// Bukkit's foo js", last_response.body.strip
get("/assets/bar.js")
- assert_equal "// App's bar js", last_response.body.strip
+ assert_match "// App's bar js", last_response.body.strip
# ensure that railties are not added twice
railties = Rails.application.send(:ordered_railties).map(&:class)
@@ -1205,7 +1217,7 @@ YAML
test "engine can be properly mounted at root" do
add_to_config("config.action_dispatch.show_exceptions = false")
- add_to_config("config.serve_static_files = false")
+ add_to_config("config.public_file_server.enabled = false")
@plugin.write "lib/bukkits.rb", <<-RUBY
module Bukkits
@@ -1230,13 +1242,12 @@ YAML
fullpath: \#{request.fullpath}
path: \#{request.path}
TEXT
- render text: text
+ render plain: text
end
end
end
RUBY
-
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
mount Bukkits::Engine => "/"
@@ -1268,7 +1279,7 @@ YAML
app_file "app/controllers/bar_controller.rb", <<-RUBY
class BarController < ApplicationController
def index
- render text: bukkits.bukkit_path
+ render plain: bukkits.bukkit_path
end
end
RUBY
@@ -1286,22 +1297,200 @@ YAML
end
RUBY
+ @plugin.write "app/controllers/bukkits/bukkit_controller.rb", <<-RUBY
+ class Bukkits::BukkitController < ActionController::Base
+ def index
+ render plain: main_app.bar_path
+ end
+ end
+ RUBY
+
+ boot_rails
+
+ get("/bukkits/bukkit", {}, { "SCRIPT_NAME" => "/foo" })
+ assert_equal "/foo/bar", last_response.body
+
+ get("/bar", {}, { "SCRIPT_NAME" => "/foo" })
+ assert_equal "/foo/bukkits/bukkit", last_response.body
+ end
+
+ test "paths are properly generated when application is mounted at sub-path and relative_url_root is set" do
+ add_to_config "config.relative_url_root = '/foo'"
+
+ @plugin.write "lib/bukkits.rb", <<-RUBY
+ module Bukkits
+ class Engine < ::Rails::Engine
+ isolate_namespace Bukkits
+ end
+ end
+ RUBY
+
+ app_file "app/controllers/bar_controller.rb", <<-RUBY
+ class BarController < ApplicationController
+ def index
+ render plain: bukkits.bukkit_path
+ end
+ end
+ RUBY
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ get '/bar' => 'bar#index', :as => 'bar'
+ mount Bukkits::Engine => "/bukkits", :as => "bukkits"
+ end
+ RUBY
+
+ @plugin.write "config/routes.rb", <<-RUBY
+ Bukkits::Engine.routes.draw do
+ get '/bukkit' => 'bukkit#index'
+ end
+ RUBY
@plugin.write "app/controllers/bukkits/bukkit_controller.rb", <<-RUBY
class Bukkits::BukkitController < ActionController::Base
def index
- render text: main_app.bar_path
+ render plain: main_app.bar_path
+ end
+ end
+ RUBY
+
+ boot_rails
+
+ get("/bukkits/bukkit", {}, { "SCRIPT_NAME" => "/foo" })
+ assert_equal "/foo/bar", last_response.body
+
+ get("/bar", {}, { "SCRIPT_NAME" => "/foo" })
+ assert_equal "/foo/bukkits/bukkit", last_response.body
+ end
+
+ test "isolated engine can be mounted under multiple static locations" do
+ app_file "app/controllers/foos_controller.rb", <<-RUBY
+ class FoosController < ApplicationController
+ def through_fruits
+ render plain: fruit_bukkits.posts_path
+ end
+
+ def through_vegetables
+ render plain: vegetable_bukkits.posts_path
+ end
+ end
+ RUBY
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ scope "/fruits" do
+ mount Bukkits::Engine => "/bukkits", as: :fruit_bukkits
+ end
+
+ scope "/vegetables" do
+ mount Bukkits::Engine => "/bukkits", as: :vegetable_bukkits
+ end
+
+ get "/through_fruits" => "foos#through_fruits"
+ get "/through_vegetables" => "foos#through_vegetables"
+ end
+ RUBY
+
+ @plugin.write "config/routes.rb", <<-RUBY
+ Bukkits::Engine.routes.draw do
+ resources :posts, only: :index
+ end
+ RUBY
+
+ boot_rails
+
+ get("/through_fruits")
+ assert_equal "/fruits/bukkits/posts", last_response.body
+
+ get("/through_vegetables")
+ assert_equal "/vegetables/bukkits/posts", last_response.body
+ end
+
+ test "isolated engine can be mounted under multiple dynamic locations" do
+ app_file "app/controllers/foos_controller.rb", <<-RUBY
+ class FoosController < ApplicationController
+ def through_fruits
+ render plain: fruit_bukkits.posts_path(fruit_id: 1)
+ end
+
+ def through_vegetables
+ render plain: vegetable_bukkits.posts_path(vegetable_id: 1)
+ end
+ end
+ RUBY
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ resources :fruits do
+ mount Bukkits::Engine => "/bukkits"
+ end
+
+ resources :vegetables do
+ mount Bukkits::Engine => "/bukkits"
+ end
+
+ get "/through_fruits" => "foos#through_fruits"
+ get "/through_vegetables" => "foos#through_vegetables"
+ end
+ RUBY
+
+ @plugin.write "config/routes.rb", <<-RUBY
+ Bukkits::Engine.routes.draw do
+ resources :posts, only: :index
+ end
+ RUBY
+
+ boot_rails
+
+ get("/through_fruits")
+ assert_equal "/fruits/1/bukkits/posts", last_response.body
+
+ get("/through_vegetables")
+ assert_equal "/vegetables/1/bukkits/posts", last_response.body
+ end
+
+ test "route helpers resolve script name correctly when called with different script name from current one" do
+ @plugin.write "app/controllers/posts_controller.rb", <<-RUBY
+ class PostsController < ActionController::Base
+ def index
+ render plain: fruit_bukkits.posts_path(fruit_id: 2)
end
end
RUBY
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ resources :fruits do
+ mount Bukkits::Engine => "/bukkits"
+ end
+ end
+ RUBY
+
+ @plugin.write "config/routes.rb", <<-RUBY
+ Bukkits::Engine.routes.draw do
+ resources :posts, only: :index
+ end
+ RUBY
+
boot_rails
- get("/bukkits/bukkit", {}, {'SCRIPT_NAME' => '/foo'})
- assert_equal '/foo/bar', last_response.body
+ get("/fruits/1/bukkits/posts")
+ assert_equal "/fruits/2/bukkits/posts", last_response.body
+ end
- get("/bar", {}, {'SCRIPT_NAME' => '/foo'})
- assert_equal '/foo/bukkits/bukkit', last_response.body
+ test "active_storage:install task works within engine" do
+ @plugin.write "Rakefile", <<-RUBY
+ APP_RAKEFILE = '#{app_path}/Rakefile'
+ load 'rails/tasks/engine.rake'
+ RUBY
+
+ Dir.chdir(@plugin.path) do
+ output = `bundle exec rake app:active_storage:install`
+ assert $?.success?, output
+
+ active_storage_migration = migrations.detect { |migration| migration.name == "CreateActiveStorageTables" }
+ assert active_storage_migration
+ end
end
private
diff --git a/railties/test/railties/generators_test.rb b/railties/test/railties/generators_test.rb
index 5f4171d44b..8383cb3050 100644
--- a/railties/test/railties/generators_test.rb
+++ b/railties/test/railties/generators_test.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
RAILS_ISOLATED_ENGINE = true
require "isolation/abstract_unit"
-require 'generators/generators_test_helper'
+require "generators/generators_test_helper"
require "rails/generators/test_case"
module RailtiesTests
@@ -9,7 +11,7 @@ module RailtiesTests
include ActiveSupport::Testing::Isolation
def destination_root
- tmp_path 'foo_bar'
+ tmp_path "foo_bar"
end
def tmp_path(*args)
@@ -18,7 +20,7 @@ module RailtiesTests
end
def engine_path
- tmp_path('foo_bar')
+ tmp_path("foo_bar")
end
def bundled_rails(cmd)
@@ -26,14 +28,10 @@ module RailtiesTests
end
def rails(cmd)
- environment = File.expand_path('../../../../load_paths', __FILE__)
- if File.exist?("#{environment}.rb")
- require_environment = "-r #{environment}"
- end
- `#{Gem.ruby} #{require_environment} #{RAILS_FRAMEWORK_ROOT}/railties/exe/rails #{cmd}`
+ `#{Gem.ruby} #{RAILS_FRAMEWORK_ROOT}/railties/exe/rails #{cmd}`
end
- def build_engine(is_mountable=false)
+ def build_engine(is_mountable = false)
FileUtils.rm_rf(engine_path)
FileUtils.mkdir_p(engine_path)
@@ -43,7 +41,7 @@ module RailtiesTests
Dir.chdir(engine_path) do
File.open("Gemfile", "w") do |f|
- f.write <<-GEMFILE.gsub(/^ {12}/, '')
+ f.write <<-GEMFILE.gsub(/^ {12}/, "")
source "https://rubygems.org"
gem 'rails', path: '#{RAILS_FRAMEWORK_ROOT}'
diff --git a/railties/test/railties/mounted_engine_test.rb b/railties/test/railties/mounted_engine_test.rb
index fb2071c7c3..48f0fbc80f 100644
--- a/railties/test/railties/mounted_engine_test.rb
+++ b/railties/test/railties/mounted_engine_test.rb
@@ -1,10 +1,12 @@
-require 'isolation/abstract_unit'
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
module ApplicationTests
class ApplicationRoutingTest < ActiveSupport::TestCase
- require 'rack/test'
- include Rack::Test::Methods
- include ActiveSupport::Testing::Isolation
+ require "rack/test"
+ include Rack::Test::Methods
+ include ActiveSupport::Testing::Isolation
def setup
build_app
@@ -15,7 +17,7 @@ module ApplicationTests
@plugin = engine "blog"
@metrics_plugin = engine "metrics"
- app_file 'config/routes.rb', <<-RUBY
+ app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
mount Weblog::Engine, :at => '/', :as => 'weblog'
resources :posts
@@ -34,7 +36,6 @@ module ApplicationTests
end
RUBY
-
@simple_plugin.write "lib/weblog.rb", <<-RUBY
module Weblog
class Engine < ::Rails::Engine
@@ -51,7 +52,7 @@ module ApplicationTests
@simple_plugin.write "app/controllers/weblogs_controller.rb", <<-RUBY
class WeblogsController < ActionController::Base
def index
- render text: request.url
+ render plain: request.url
end
end
RUBY
@@ -75,7 +76,7 @@ module ApplicationTests
module Metrics
class GeneratingController < ActionController::Base
def generate_blog_route
- render text: blog.post_path(1)
+ render plain: blog.post_path(1)
end
def generate_blog_route_in_view
@@ -112,6 +113,7 @@ module ApplicationTests
@plugin.write "config/routes.rb", <<-RUBY
Blog::Engine.routes.draw do
resources :posts
+ get '/different_context', to: 'posts#different_context'
get '/generate_application_route', to: 'posts#generate_application_route'
get '/application_route_in_view', to: 'posts#application_route_in_view'
get '/engine_polymorphic_path', to: 'posts#engine_polymorphic_path'
@@ -123,14 +125,18 @@ module ApplicationTests
module Blog
class PostsController < ActionController::Base
def index
- render text: blog.post_path(1)
+ render plain: blog.post_path(1)
+ end
+
+ def different_context
+ render plain: blog.post_path(1, user: "ada")
end
def generate_application_route
path = main_app.url_for(controller: "/main",
action: "index",
only_path: true)
- render text: path
+ render plain: path
end
def application_route_in_view
@@ -138,11 +144,11 @@ module ApplicationTests
end
def engine_polymorphic_path
- render text: polymorphic_path(Post.new)
+ render plain: polymorphic_path(Post.new)
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
@@ -151,7 +157,7 @@ module ApplicationTests
app_file "app/controllers/application_generating_controller.rb", <<-RUBY
class ApplicationGeneratingController < ActionController::Base
def engine_route
- render text: blog.posts_path
+ render plain: blog.posts_path
end
def engine_route_in_view
@@ -159,7 +165,7 @@ module ApplicationTests
end
def weblog_engine_route
- render text: weblog.weblogs_path
+ render plain: weblog.weblogs_path
end
def weblog_engine_route_in_view
@@ -167,20 +173,18 @@ module ApplicationTests
end
def url_for_engine_route
- render text: blog.url_for(controller: "blog/posts", action: "index", user: "john", only_path: true)
+ render plain: blog.url_for(controller: "blog/posts", action: "index", user: "john", only_path: true)
end
def polymorphic_route
- render text: polymorphic_url([blog, Blog::Post.new])
+ render plain: polymorphic_url([blog, Blog::Post.new])
end
def application_polymorphic_path
- render text: polymorphic_path(Blog::Post.new)
+ render plain: polymorphic_path(Blog::Post.new)
end
end
RUBY
-
- boot_rails
end
def teardown
@@ -199,8 +203,12 @@ module ApplicationTests
get "/john/blog/posts"
assert_equal "/john/blog/posts/1", last_response.body
+ # test generating engine route from engine with a different context
+ get "/john/blog/different_context"
+ assert_equal "/ada/blog/posts/1", last_response.body
+
# test generating engine's route from engine with default_url_options
- get "/john/blog/posts", {}, 'SCRIPT_NAME' => "/foo"
+ get "/john/blog/posts", {}, { "SCRIPT_NAME" => "/foo" }
assert_equal "/foo/john/blog/posts/1", last_response.body
# test generating engine's route from application
@@ -214,10 +222,10 @@ module ApplicationTests
assert_equal "/john/blog/posts", last_response.body
# test generating engine's route from application with default_url_options
- get "/engine_route", {}, 'SCRIPT_NAME' => "/foo"
+ get "/engine_route", {}, { "SCRIPT_NAME" => "/foo" }
assert_equal "/foo/anonymous/blog/posts", last_response.body
- get "/url_for_engine_route", {}, 'SCRIPT_NAME' => "/foo"
+ get "/url_for_engine_route", {}, { "SCRIPT_NAME" => "/foo" }
assert_equal "/foo/john/blog/posts", last_response.body
# test generating application's route from engine
@@ -229,21 +237,20 @@ module ApplicationTests
# test generating engine's route from other engine
get "/metrics/generate_blog_route"
- assert_equal '/anonymous/blog/posts/1', last_response.body
+ assert_equal "/anonymous/blog/posts/1", last_response.body
get "/metrics/generate_blog_route_in_view"
- assert_equal '/anonymous/blog/posts/1', last_response.body
+ assert_equal "/anonymous/blog/posts/1", last_response.body
# test generating engine's route from other engine with default_url_options
- get "/metrics/generate_blog_route", {}, 'SCRIPT_NAME' => '/foo'
- assert_equal '/foo/anonymous/blog/posts/1', last_response.body
-
- get "/metrics/generate_blog_route_in_view", {}, 'SCRIPT_NAME' => '/foo'
- assert_equal '/foo/anonymous/blog/posts/1', last_response.body
+ get "/metrics/generate_blog_route", {}, { "SCRIPT_NAME" => "/foo" }
+ assert_equal "/foo/anonymous/blog/posts/1", last_response.body
+ get "/metrics/generate_blog_route_in_view", {}, { "SCRIPT_NAME" => "/foo" }
+ assert_equal "/foo/anonymous/blog/posts/1", last_response.body
# test generating application's route from engine with default_url_options
- get "/someone/blog/generate_application_route", {}, 'SCRIPT_NAME' => '/foo'
+ get "/someone/blog/generate_application_route", {}, { "SCRIPT_NAME" => "/foo" }
assert_equal "/foo/", last_response.body
# test polymorphic routes
diff --git a/railties/test/railties/railtie_test.rb b/railties/test/railties/railtie_test.rb
index 5042d628cf..7c3d1e3759 100644
--- a/railties/test/railties/railtie_test.rb
+++ b/railties/test/railties/railtie_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "isolation/abstract_unit"
module RailtiesTest
@@ -6,7 +8,6 @@ module RailtiesTest
def setup
build_app
- boot_rails
FileUtils.rm_rf("#{app_path}/config/environments")
require "rails/all"
end
@@ -64,7 +65,7 @@ module RailtiesTest
test "railtie can add to_prepare callbacks" do
$to_prepare = false
class Foo < Rails::Railtie ; config.to_prepare { $to_prepare = true } ; end
- assert !$to_prepare
+ assert_not $to_prepare
require "#{app_path}/config/environment"
require "rack/test"
extend Rack::Test::Methods
@@ -80,10 +81,17 @@ module RailtiesTest
assert_equal app_path, $before_configuration
end
+ test "before_configuration callbacks run as soon as the application constant inherits from Rails::Application" do
+ $before_configuration = false
+ class Foo < Rails::Railtie ; config.before_configuration { $before_configuration = true } ; end
+ class Application < Rails::Application ; end
+ assert $before_configuration
+ end
+
test "railtie can add after_initialize callbacks" do
$after_initialize = false
class Foo < Rails::Railtie ; config.after_initialize { $after_initialize = true } ; end
- assert !$after_initialize
+ assert_not $after_initialize
require "#{app_path}/config/environment"
assert $after_initialize
end
@@ -99,10 +107,10 @@ module RailtiesTest
require "#{app_path}/config/environment"
- assert !$ran_block
- require 'rake'
- require 'rake/testtask'
- require 'rdoc/task'
+ assert_not $ran_block
+ require "rake"
+ require "rake/testtask"
+ require "rdoc/task"
Rails.application.load_tasks
assert $ran_block
@@ -124,12 +132,12 @@ module RailtiesTest
require "#{app_path}/config/environment"
assert_equal [], $ran_block
- require 'rake'
- require 'rake/testtask'
- require 'rdoc/task'
+ require "rake"
+ require "rake/testtask"
+ require "rdoc/task"
Rails.application.load_tasks
- assert $ran_block.include?("my_tie")
+ assert_includes $ran_block, "my_tie"
end
test "generators block is executed when MyApp.load_generators is called" do
@@ -143,7 +151,7 @@ module RailtiesTest
require "#{app_path}/config/environment"
- assert !$ran_block
+ assert_not $ran_block
Rails.application.load_generators
assert $ran_block
end
@@ -159,7 +167,7 @@ module RailtiesTest
require "#{app_path}/config/environment"
- assert !$ran_block
+ assert_not $ran_block
Rails.application.load_console
assert $ran_block
end
@@ -175,7 +183,7 @@ module RailtiesTest
require "#{app_path}/config/environment"
- assert !$ran_block
+ assert_not $ran_block
Rails.application.load_runner
assert $ran_block
end
@@ -189,7 +197,7 @@ module RailtiesTest
end
end
- assert !$ran_block
+ assert_not $ran_block
require "#{app_path}/config/environment"
assert $ran_block
end
@@ -197,8 +205,8 @@ module RailtiesTest
test "we can change our environment if we want to" do
begin
original_env = Rails.env
- Rails.env = 'foo'
- assert_equal('foo', Rails.env)
+ Rails.env = "foo"
+ assert_equal("foo", Rails.env)
ensure
Rails.env = original_env
assert_equal(original_env, Rails.env)
diff --git a/railties/test/secrets_test.rb b/railties/test/secrets_test.rb
new file mode 100644
index 0000000000..06877bc76a
--- /dev/null
+++ b/railties/test/secrets_test.rb
@@ -0,0 +1,178 @@
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "rails/secrets"
+
+class Rails::SecretsTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation
+
+ setup :build_app
+ teardown :teardown_app
+
+ test "setting read to false skips parsing" do
+ run_secrets_generator do
+ Rails::Secrets.write(<<-end_of_secrets)
+ production:
+ yeah_yeah: lets-walk-in-the-cool-evening-light
+ end_of_secrets
+
+ add_to_env_config("production", "config.read_encrypted_secrets = false")
+ app("production")
+
+ assert_not Rails.application.secrets.yeah_yeah
+ 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)
+ production:
+ yeah_yeah: lets-go-walking-down-this-empty-street
+ end_of_secrets
+
+ Rails::Secrets.write(<<-end_of_secrets)
+ production:
+ yeah_yeah: lets-walk-in-the-cool-evening-light
+ end_of_secrets
+
+ add_to_env_config("production", "config.read_encrypted_secrets = true")
+ app("production")
+
+ assert_equal "lets-walk-in-the-cool-evening-light", Rails.application.secrets.yeah_yeah
+ end
+ end
+
+ test "refer secrets inside env config" do
+ run_secrets_generator do
+ Rails::Secrets.write(<<-end_of_yaml)
+ production:
+ some_secret: yeah yeah
+ end_of_yaml
+
+ add_to_env_config "production", <<-end_of_config
+ config.dereferenced_secret = Rails.application.secrets.some_secret
+ end_of_config
+
+ app("production")
+
+ assert_equal "yeah yeah", Rails.application.config.dereferenced_secret
+ end
+ end
+
+ test "do not update secrets.yml.enc when secretes do not change" do
+ run_secrets_generator do
+ Rails::Secrets.read_for_editing do |tmp_path|
+ File.write(tmp_path, "Empty streets, empty nights. The Downtown Lights.")
+ end
+
+ FileUtils.cp("config/secrets.yml.enc", "config/secrets.yml.enc.bk")
+
+ Rails::Secrets.read_for_editing do |tmp_path|
+ File.write(tmp_path, "Empty streets, empty nights. The Downtown Lights.")
+ end
+
+ assert_equal File.read("config/secrets.yml.enc.bk"), File.read("config/secrets.yml.enc")
+ end
+ end
+
+ test "can read secrets written in binary" do
+ run_secrets_generator do
+ secrets = <<-end_of_secrets
+ production:
+ api_key: 00112233445566778899aabbccddeeff…
+ end_of_secrets
+
+ Rails::Secrets.write(secrets.dup.force_encoding(Encoding::ASCII_8BIT))
+
+ Rails::Secrets.read_for_editing do |tmp_path|
+ assert_match(/production:\n\s*api_key: 00112233445566778899aabbccddeeff…\n/, File.read(tmp_path))
+ end
+
+ app("production")
+
+ assert_equal "00112233445566778899aabbccddeeff…", Rails.application.secrets.api_key
+ end
+ end
+
+ test "can read secrets written in non-binary" do
+ run_secrets_generator do
+ secrets = <<-end_of_secrets
+ production:
+ api_key: 00112233445566778899aabbccddeeff…
+ end_of_secrets
+
+ Rails::Secrets.write(secrets)
+
+ Rails::Secrets.read_for_editing do |tmp_path|
+ assert_equal(secrets.dup.force_encoding(Encoding::ASCII_8BIT), IO.binread(tmp_path))
+ end
+
+ app("production")
+
+ assert_equal "00112233445566778899aabbccddeeff…", Rails.application.secrets.api_key
+ end
+ end
+
+ private
+ def run_secrets_generator
+ Dir.chdir(app_path) do
+ File.write("config/secrets.yml.key", "f731758c639da2604dfb6bf3d1025de8")
+ File.write("config/secrets.yml.enc", "sEB0mHxDbeP1/KdnMk00wyzPFACl9K6t0cZWn5/Mfx/YbTHvnI07vrneqHg9kaH3wOS7L6pIQteu1P077OtE4BSx/ZRc/sgQPHyWu/tXsrfHqnPNpayOF/XZqizE91JacSFItNMWpuPsp9ynbzz+7cGhoB1S4aPNIU6u0doMrzdngDbijsaAFJmsHIQh6t/QHoJx--8aMoE0PvUWmw1Iqz--ldFqnM/K0g9k17M8PKoN/Q==")
+
+ add_to_config <<-RUBY
+ config.read_encrypted_secrets = true
+ RUBY
+
+ yield
+ end
+ end
+end
diff --git a/railties/test/test_unit/reporter_test.rb b/railties/test/test_unit/reporter_test.rb
index 3066ba82d6..81b7ab19a1 100644
--- a/railties/test/test_unit/reporter_test.rb
+++ b/railties/test/test_unit/reporter_test.rb
@@ -1,5 +1,8 @@
-require 'abstract_unit'
-require 'rails/test_unit/reporter'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "rails/test_unit/reporter"
+require "minitest/mock"
class TestUnitReporterTest < ActiveSupport::TestCase
class ExampleTest < Minitest::Test
@@ -8,14 +11,14 @@ class TestUnitReporterTest < ActiveSupport::TestCase
setup do
@output = StringIO.new
- @reporter = Rails::TestUnitReporter.new @output
+ @reporter = Rails::TestUnitReporter.new @output, output_inline: true
end
test "prints rerun snippet to run a single failed test" do
@reporter.record(failed_test)
@reporter.report
- assert_match %r{^bin/rails test .*test/test_unit/reporter_test.rb:6$}, @output.string
+ assert_match %r{^rails test .*test/test_unit/reporter_test\.rb:\d+$}, @output.string
assert_rerun_snippet_count 1
end
@@ -32,7 +35,7 @@ class TestUnitReporterTest < ActiveSupport::TestCase
@reporter.record(passing_test)
@reporter.record(skipped_test)
@reporter.report
- assert_no_match 'Failed tests:', @output.string
+ assert_no_match "Failed tests:", @output.string
assert_rerun_snippet_count 0
end
@@ -51,38 +54,145 @@ class TestUnitReporterTest < ActiveSupport::TestCase
@reporter.record(failed_test)
@reporter.report
- assert_match %r{^bin/test .*test/test_unit/reporter_test.rb:6$}, @output.string
+ assert_match %r{^bin/test .*test/test_unit/reporter_test\.rb:\d+$}, @output.string
ensure
Rails::TestUnitReporter.executable = original_executable
end
end
- private
- def assert_rerun_snippet_count(snippet_count)
- assert_equal snippet_count, @output.string.scan(%r{^bin/rails test }).size
+ test "outputs failures inline" do
+ @reporter.record(failed_test)
+ @reporter.report
+
+ expect = %r{\AF\n\nFailure:\nTestUnitReporterTest::ExampleTest#woot \[[^\]]+\]:\nboo\n\nrails test test/test_unit/reporter_test\.rb:\d+\n\n\z}
+ assert_match expect, @output.string
+ end
+
+ test "outputs errors inline" do
+ @reporter.record(errored_test)
+ @reporter.report
+
+ expect = %r{\AE\n\nError:\nTestUnitReporterTest::ExampleTest#woot:\nArgumentError: wups\n \n\nrails test .*test/test_unit/reporter_test\.rb:\d+\n\n\z}
+ assert_match expect, @output.string
+ end
+
+ test "outputs skipped tests inline if verbose" do
+ verbose = Rails::TestUnitReporter.new @output, verbose: true, output_inline: true
+ verbose.record(skipped_test)
+ verbose.report
+
+ expect = %r{\ATestUnitReporterTest::ExampleTest#woot = 10\.00 s = S\n\n\nSkipped:\nTestUnitReporterTest::ExampleTest#woot \[[^\]]+\]:\nskipchurches, misstemples\n\nrails test test/test_unit/reporter_test\.rb:\d+\n\n\z}
+ assert_match expect, @output.string
+ end
+
+ test "does not output rerun snippets after run" do
+ @reporter.record(failed_test)
+ @reporter.report
+
+ assert_no_match "Failed tests:", @output.string
+ end
+
+ test "fail fast interrupts run on failure" do
+ fail_fast = Rails::TestUnitReporter.new @output, fail_fast: true
+ interrupt_raised = false
+
+ # Minitest passes through Interrupt, catch it manually.
+ begin
+ fail_fast.record(failed_test)
+ rescue Interrupt
+ interrupt_raised = true
+ ensure
+ assert interrupt_raised, "Expected Interrupt to be raised."
+ end
+ end
+
+ test "fail fast interrupts run on error" do
+ fail_fast = Rails::TestUnitReporter.new @output, fail_fast: true
+ interrupt_raised = false
+
+ # Minitest passes through Interrupt, catch it manually.
+ begin
+ fail_fast.record(errored_test)
+ rescue Interrupt
+ interrupt_raised = true
+ ensure
+ assert interrupt_raised, "Expected Interrupt to be raised."
+ end
end
- def failed_test
- ft = ExampleTest.new(:woot)
- ft.failures << begin
- raise Minitest::Assertion, "boo"
- rescue Minitest::Assertion => e
- e
- end
- ft
+ test "fail fast does not interrupt run skips" do
+ fail_fast = Rails::TestUnitReporter.new @output, fail_fast: true
+
+ fail_fast.record(skipped_test)
+ assert_no_match "Failed tests:", @output.string
end
- def passing_test
- ExampleTest.new(:woot)
+ test "outputs colored passing results" do
+ @output.stub(:tty?, true) do
+ colored = Rails::TestUnitReporter.new @output, color: true, output_inline: true
+ colored.record(passing_test)
+
+ expect = %r{\e\[32m\.\e\[0m}
+ assert_match expect, @output.string
+ end
+ end
+
+ test "outputs colored skipped results" do
+ @output.stub(:tty?, true) do
+ colored = Rails::TestUnitReporter.new @output, color: true, output_inline: true
+ colored.record(skipped_test)
+
+ expect = %r{\e\[33mS\e\[0m}
+ assert_match expect, @output.string
+ end
end
- def skipped_test
- st = ExampleTest.new(:woot)
- st.failures << begin
- raise Minitest::Skip
- rescue Minitest::Assertion => e
- e
- end
- st
+ test "outputs colored failed results" do
+ @output.stub(:tty?, true) do
+ colored = Rails::TestUnitReporter.new @output, color: true, output_inline: true
+ colored.record(errored_test)
+
+ expected = %r{\e\[31mE\e\[0m\n\n\e\[31mError:\nTestUnitReporterTest::ExampleTest#woot:\nArgumentError: wups\n \n\e\[0m}
+ assert_match expected, @output.string
+ end
end
+
+ private
+ def assert_rerun_snippet_count(snippet_count)
+ assert_equal snippet_count, @output.string.scan(%r{^rails test }).size
+ end
+
+ def failed_test
+ ft = Minitest::Result.from(ExampleTest.new(:woot))
+ ft.failures << begin
+ raise Minitest::Assertion, "boo"
+ rescue Minitest::Assertion => e
+ e
+ end
+ ft
+ end
+
+ def errored_test
+ error = ArgumentError.new("wups")
+ error.set_backtrace([ "some_test.rb:4" ])
+
+ et = Minitest::Result.from(ExampleTest.new(:woot))
+ et.failures << Minitest::UnexpectedError.new(error)
+ et
+ end
+
+ def passing_test
+ Minitest::Result.from(ExampleTest.new(:woot))
+ end
+
+ def skipped_test
+ st = Minitest::Result.from(ExampleTest.new(:woot))
+ st.failures << begin
+ raise Minitest::Skip, "skipchurches, misstemples"
+ rescue Minitest::Assertion => e
+ e
+ end
+ st.time = 10
+ st
+ end
end
diff --git a/railties/test/version_test.rb b/railties/test/version_test.rb
index f270d8f0c9..17a024fe7f 100644
--- a/railties/test/version_test.rb
+++ b/railties/test/version_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class VersionTest < ActiveSupport::TestCase
def test_rails_version_returns_a_string
diff --git a/tasks/release.rb b/tasks/release.rb
index d8c1390eef..1e83814bae 100644
--- a/tasks/release.rb
+++ b/tasks/release.rb
@@ -1,12 +1,27 @@
-FRAMEWORKS = %w( activesupport activemodel activerecord actionview actionpack activejob actionmailer railties )
+# frozen_string_literal: true
-root = File.expand_path('../../', __FILE__)
+FRAMEWORKS = %w( activesupport activemodel activerecord actionview actionpack activejob actionmailer actioncable activestorage railties )
+FRAMEWORK_NAMES = Hash.new { |h, k| k.split(/(?<=active|action)/).map(&:capitalize).join(" ") }
+
+root = File.expand_path("..", __dir__)
version = File.read("#{root}/RAILS_VERSION").strip
tag = "v#{version}"
directory "pkg"
-(FRAMEWORKS + ['rails']).each do |framework|
+# 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"
gemspec = "#{framework}.gemspec"
@@ -27,7 +42,7 @@ directory "pkg"
file = Dir[glob].first
ruby = File.read(file)
- major, minor, tiny, pre = version.split('.')
+ major, minor, tiny, pre = version.split(".", 4)
pre = pre ? pre.inspect : "nil"
ruby.gsub!(/^(\s*)MAJOR(\s*)= .*?$/, "\\1MAJOR = #{major}")
@@ -42,45 +57,75 @@ directory "pkg"
ruby.gsub!(/^(\s*)PRE(\s*)= .*?$/, "\\1PRE = #{pre}")
raise "Could not insert PRE in #{file}" unless $1
- File.open(file, 'w') { |f| f.write ruby }
+ 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
cmd = ""
- cmd << "cd #{framework} && " unless framework == "rails"
- cmd << "gem build #{gemspec} && mv #{framework}-#{version}.gem #{root}/pkg/"
+ cmd += "cd #{framework} && " unless framework == "rails"
+ cmd += "bundle exec rake package && " unless framework == "rails"
+ cmd += "gem build #{gemspec} && mv #{framework}-#{version}.gem #{root}/pkg/"
sh cmd
end
- task :build => [:clean, gem]
- task :install => :build do
- sh "gem install #{gem}"
+ task build: [:clean, gem]
+ task install: :build do
+ sh "gem install --pre #{gem}"
end
- task :prep_release => [:ensure_clean_state, :build]
-
- task :push => :build do
+ task push: :build do
sh "gem push #{gem}"
+
+ if File.exist?("#{framework}/package.json")
+ Dir.chdir("#{framework}") do
+ npm_tag = /[a-z]/.match?(version) ? "pre" : "latest"
+ sh "npm publish --tag #{npm_tag}"
+ end
+ end
end
end
end
namespace :changelog do
+ task :header 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"
+ header += "* No changes.\n\n\n" if current_contents.start_with?("##")
+ contents = header + current_contents
+ File.write(fname, contents)
+ end
+ end
+
task :release_date do
- (FRAMEWORKS + ['guides']).each do |fw|
- require 'date'
- replace = '\1(' + Date.today.strftime('%B %d, %Y') + ')'
- fname = File.join fw, 'CHANGELOG.md'
+ (FRAMEWORKS + ["guides"]).each do |fw|
+ require "date"
+ replace = "## Rails #{version} (#{Date.today.strftime('%B %d, %Y')}) ##\n"
+ fname = File.join fw, "CHANGELOG.md"
- contents = File.read(fname).sub(/^([^(]*)\(unreleased\)/, replace)
- File.open(fname, 'wb') { |f| f.write contents }
+ contents = File.read(fname).sub(/^(## Rails .*)\n/, replace)
+ File.write(fname, contents)
end
end
task :release_summary do
- (FRAMEWORKS + ['guides']).each do |fw|
+ (FRAMEWORKS + ["guides"]).each do |fw|
puts "## #{fw}"
- fname = File.join fw, 'CHANGELOG.md'
+ fname = File.join fw, "CHANGELOG.md"
contents = File.readlines fname
contents.shift
changes = []
@@ -92,37 +137,122 @@ namespace :changelog do
end
namespace :all do
- task :build => FRAMEWORKS.map { |f| "#{f}:build" } + ['rails:build']
- task :update_versions => FRAMEWORKS.map { |f| "#{f}:update_versions" } + ['rails:update_versions']
- task :install => FRAMEWORKS.map { |f| "#{f}:install" } + ['rails:install']
- task :push => FRAMEWORKS.map { |f| "#{f}:push" } + ['rails:push']
+ task build: FRAMEWORKS.map { |f| "#{f}:build" } + ["rails:build"]
+ task update_versions: FRAMEWORKS.map { |f| "#{f}:update_versions" } + ["rails:update_versions"]
+ task install: FRAMEWORKS.map { |f| "#{f}:install" } + ["rails:install"]
+ task push: FRAMEWORKS.map { |f| "#{f}:push" } + ["rails:push"]
task :ensure_clean_state do
- unless `git status -s | grep -v RAILS_VERSION`.strip.empty?
+ unless `git status -s | grep -v 'RAILS_VERSION\\|CHANGELOG\\|Gemfile.lock\\|package.json\\|version.rb\\|tasks/release.rb'`.strip.empty?
abort "[ABORTING] `git status` reports a dirty tree. Make sure all changes are committed"
end
- unless ENV['SKIP_TAG'] || `git tag | grep '^#{tag}$'`.strip.empty?
+ unless ENV["SKIP_TAG"] || `git tag | grep '^#{tag}$'`.strip.empty?
abort "[ABORTING] `git tag` shows that #{tag} already exists. Has this version already\n"\
" been released? Git tagging can be skipped by setting SKIP_TAG=1"
end
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"
+ task verify: :install do
+ app_name = "pkg/verify-#{version}-#{Time.now.to_i}"
+ sh "rails _#{version}_ new #{app_name} --skip-bundle" # Generate with the right version.
+ cd app_name
+
+ # Replace the generated gemfile entry with the exact version.
+ File.write("Gemfile", File.read("Gemfile").sub(/^gem 'rails.*/, "gem 'rails', '#{version}'"))
+ sh "bundle"
+
+ sh "rails generate scaffold user name admin:boolean && rails db:migrate"
+
+ puts "Booting a Rails server. Verify the release by:"
+ puts
+ puts "- Seeing the correct release number on the root page"
+ puts "- Viewing /users"
+ puts "- Creating a user"
+ puts "- Updating a user (e.g. disable the admin flag)"
+ puts "- Deleting a user on /users"
+ puts "- Whatever else you want."
+ begin
+ sh "rails server"
+ rescue Interrupt
+ # Server passes along interrupt. Prevent halting verify task.
end
+ end
- sh "git add . && git commit --verbose --template=pkg/commit_message.txt"
- rm_f "pkg/commit_message.txt"
+ task :bundle do
+ sh "bundle check"
+ end
+
+ task :commit do
+ 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"
+ end
end
task :tag do
- sh "git tag -m '#{tag} release' #{tag}"
+ sh "git tag -s -m '#{tag} release' #{tag}"
sh "git push --tags"
end
- task :release => %w(ensure_clean_state build commit tag push)
+ task prep_release: %w(ensure_clean_state build bundle commit)
+
+ task release: %w(prep_release tag push)
+end
+
+module Announcement
+ class Version
+ def initialize(version)
+ @version, @gem_version = version, Gem::Version.new(version)
+ end
+
+ def to_s
+ @version
+ end
+
+ def previous
+ @gem_version.segments[0, 3].tap { |v| v[2] -= 1 }.join(".")
+ end
+
+ def major_or_security?
+ @gem_version.segments[2].zero? || @gem_version.segments[3].is_a?(Integer)
+ end
+
+ def rc?
+ @version =~ /rc/
+ end
+ end
+end
+
+task :announce do
+ Dir.chdir("pkg/") do
+ versions = ENV["VERSIONS"] ? ENV["VERSIONS"].split(",") : [ version ]
+ versions = versions.sort.map { |v| Announcement::Version.new(v) }
+
+ raise "Only valid for patch releases" if versions.any?(&:major_or_security?)
+
+ if versions.any?(&: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
+ end
+
+ require "erb"
+ template = File.read("../tasks/release_announcement_draft.erb")
+
+ match = ERB.version.match(/\Aerb\.rb \[(?<version>[^ ]+) /)
+ if match && match[:version] >= "2.2.0" # Ruby 2.6+
+ puts ERB.new(template, trim_mode: "<>").result(binding)
+ else
+ puts ERB.new(template, nil, "<>").result(binding)
+ end
+ end
end
diff --git a/tasks/release_announcement_draft.erb b/tasks/release_announcement_draft.erb
new file mode 100644
index 0000000000..3dbb8c053f
--- /dev/null
+++ b/tasks/release_announcement_draft.erb
@@ -0,0 +1,38 @@
+Hi everyone,
+
+I am happy to announce that Rails <%= versions.join(" and ") %> <%= versions.size > 1 ? "have" : "has" %> been released.
+
+<% if future_date %>
+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.
+<% end %>
+<% versions.each do |version| %>
+
+## CHANGES since <%= version.previous %>
+
+To view the changes for each gem, please read the changelogs on GitHub:
+ <% FRAMEWORKS.sort.each do |framework| %>
+<%= "* [#{FRAMEWORK_NAMES[framework]} CHANGELOG](https://github.com/rails/rails/blob/v#{version}/#{framework}/CHANGELOG.md)" %>
+ <% end %>
+
+*Full listing*
+
+To see the full list of changes, [check out all the commits on
+GitHub](https://github.com/rails/rails/compare/v<%= "#{version.previous}...v#{version}" %>).
+ <% end %>
+## 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.
+
+<% versions.each do |version| %>
+Here are the checksums for <%= version %>:
+
+```
+$ shasum -a 256 *-<%= version %>.gem
+<%= `shasum -a 256 *-#{version}.gem` %>
+```
+
+<% end %>
+As always, huge thanks to the many contributors who helped with this release.
diff --git a/tools/README.md b/tools/README.md
index 25ab798bd5..f133b27128 100644
--- a/tools/README.md
+++ b/tools/README.md
@@ -1,7 +1,10 @@
-## Rails dev tools
+# Rails dev tools
This is a collection of utilities used for Rails internal development.
They aren't used by Rails apps directly.
* `console` drops you in irb and loads local Rails repos
* `profile` profiles `Kernel#require` to help reduce startup time
+ * `line_statistics` provides CodeTools module and LineStatistics class to count lines
+ * `test` is loaded by every major component of Rails to simplify testing, for example:
+ `cd ./actioncable; bin/test ./path/to/actioncable_test_with_line_number.rb:5`
diff --git a/tools/console b/tools/console
index ea995a1a54..ee08e22502 100755
--- a/tools/console
+++ b/tools/console
@@ -1,7 +1,11 @@
#!/usr/bin/env ruby
-require File.expand_path('../../load_paths', __FILE__)
-require 'rails/all'
-require 'active_support/all'
-require 'irb'
-require 'irb/completion'
+# frozen_string_literal: true
+
+require "bundler"
+Bundler.setup
+
+require "rails/all"
+require "active_support/all"
+require "irb"
+require "irb/completion"
IRB.start
diff --git a/tools/profile b/tools/profile
index 191e73b3dd..6fb571f43b 100755
--- a/tools/profile
+++ b/tools/profile
@@ -1,21 +1,23 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
+
# Profile require calls giving information about the time and the files that are called
# when loading the provided file.
#
# Example:
# tools/profile activesupport/lib/active_support.rb [ruby-prof mode] [ruby-prof printer]
-ENV['NO_RELOAD'] ||= '1'
-ENV['RAILS_ENV'] ||= 'development'
+ENV["NO_RELOAD"] ||= "1"
+ENV["RAILS_ENV"] ||= "development"
module CodeTools
class Profiler
Error = Class.new(StandardError)
attr_reader :path, :mode
- def initialize(path, mode=nil)
+ def initialize(path, mode = nil)
assert_ruby_file_exists(path)
@path, @mode = path, mode
- require 'benchmark'
+ require "benchmark"
end
def profile_requires
@@ -23,7 +25,7 @@ module CodeTools
before_rss = `ps -o rss= -p #{Process.pid}`.to_i
if mode
- require 'ruby-prof'
+ require "ruby-prof"
RubyProf.measure_mode = RubyProf.const_get(mode.upcase)
RubyProf.start
else
@@ -43,13 +45,13 @@ module CodeTools
elsif RubyProf.const_defined?(:CallStackPrinter)
filename = "#{File.basename(path, '.rb')}.#{mode}.html"
puts "RubyProf outputting to #{filename}"
- File.open(filename, 'w') do |out|
+ File.open(filename, "w") do |out|
RubyProf::CallStackPrinter.new(results).print(out)
end
else
filename = "#{File.basename(path, '.rb')}.#{mode}.callgrind"
puts "RubyProf outputting to #{filename}"
- File.open(filename, 'w') do |out|
+ File.open(filename, "w") do |out|
RubyProf::CallTreePrinter.new(results).print(out)
end
end
@@ -57,7 +59,7 @@ module CodeTools
RequireProfiler.stats.each do |file, depth, sec|
if sec
- puts "%8.1f ms %s%s" % [sec * 1000, ' ' * depth, file]
+ puts "%8.1f ms %s%s" % [sec * 1000, " " * depth, file]
else
puts "#{' ' * (13 + depth)}#{file}"
end
@@ -67,51 +69,51 @@ module CodeTools
private
- def assert_ruby_file_exists(path)
- fail Error.new("No such file") unless File.exist?(path)
- fail Error.new("#{path} is a directory") if File.directory?(path)
- ruby_extension = File.extname(path) == '.rb'
- ruby_executable = File.open(path, 'rb') {|f| f.readline } =~ [/\A#!.*ruby/]
- fail Error.new("Not a ruby file") unless ruby_extension or ruby_executable
- end
+ def assert_ruby_file_exists(path)
+ fail Error.new("No such file") unless File.exist?(path)
+ fail Error.new("#{path} is a directory") if File.directory?(path)
+ ruby_extension = File.extname(path) == ".rb"
+ ruby_executable = File.open(path, "rb") { |f| f.readline } =~ [/\A#!.*ruby/]
+ fail Error.new("Not a ruby file") unless ruby_extension || ruby_executable
+ end
- module RequireProfiler
- private
- def require(file, *args) RequireProfiler.profile(file) { super } end
- def load(file, *args) RequireProfiler.profile(file) { super } end
+ module RequireProfiler
+ private
+ def require(file, *args) RequireProfiler.profile(file) { super } end
+ def load(file, *args) RequireProfiler.profile(file) { super } end
- @depth, @stats = 0, []
- class << self
- attr_accessor :depth
- attr_accessor :stats
+ @depth, @stats = 0, []
+ class << self
+ attr_accessor :depth
+ attr_accessor :stats
- def profile(file)
- stats << [file, depth]
- self.depth += 1
- result = nil
- elapsed = Benchmark.realtime { result = yield }
- self.depth -= 1
- stats.pop if stats.last.first == file
- stats << [file, depth, elapsed] if result
- result
- end
+ def profile(file)
+ stats << [file, depth]
+ self.depth += 1
+ result = nil
+ elapsed = Benchmark.realtime { result = yield }
+ self.depth -= 1
+ stats.pop if stats.last.first == file
+ stats << [file, depth, elapsed] if result
+ result
+ end
+ end
end
- end
end
end
# ruby-prof printer name causes the third arg to be sent :classify
# which is probably overkill if you already know the name of the ruby-prof
# printer you want to use, e.g. Graph
begin
- require 'active_support/inflector'
- require 'active_support/core_ext/string/inflections'
+ require "active_support/inflector"
+ require "active_support/core_ext/string/inflections"
rescue LoadError
STDERR.puts $!.message
class String
# File activesupport/lib/active_support/inflector/methods.rb, line 150
def classify
# strip out any leading schema name
- camelize(self.sub(/.*\./, ''))
+ camelize(sub(/.*\./, ""))
end
# File activesupport/lib/active_support/inflector/methods.rb, line 68
def camelize(uppercase_first_letter = true)
@@ -121,7 +123,7 @@ rescue LoadError
else
string = string.sub(/^(?:(?=\b|[A-Z_])|\w)/) { |match| match.downcase }
end
- string.gsub(/(?:_|(\/))([a-z\d]*)/) { "#{$1}#{$2.capitalize}" }.gsub('/', '::')
+ string.gsub(/(?:_|(\/))([a-z\d]*)/) { "#{$1}#{$2.capitalize}" }.gsub("/", "::")
end
end
end
diff --git a/tools/test.rb b/tools/test.rb
index 70f295b554..1fd3ee30eb 100644
--- a/tools/test.rb
+++ b/tools/test.rb
@@ -1,12 +1,26 @@
+# frozen_string_literal: true
+
$: << File.expand_path("test", COMPONENT_ROOT)
-require File.expand_path("../../load_paths", __FILE__)
-require "rails/test_unit/minitest_plugin"
-module Rails
- # Necessary to get rerun-snippts working.
- def self.root
+require "bundler"
+Bundler.setup
+
+require "rails/test_unit/runner"
+require "rails/test_unit/reporter"
+require "rails/test_unit/line_filtering"
+require "active_support"
+require "active_support/test_case"
+
+class << Rails
+ # Necessary to get rerun-snippets working.
+ def root
@root ||= Pathname.new(COMPONENT_ROOT)
end
+ alias __root root
end
+ActiveSupport::TestCase.extend Rails::LineFiltering
Rails::TestUnitReporter.executable = "bin/test"
+
+Rails::TestUnit::Runner.parse_options(ARGV)
+Rails::TestUnit::Runner.run(ARGV)
diff --git a/version.rb b/version.rb
index 7d74b1bfe5..54bfbdd516 100644
--- a/version.rb
+++ b/version.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Rails
# Returns the version of the currently loaded Rails as a <tt>Gem::Version</tt>
def self.gem_version
@@ -5,7 +7,7 @@ module Rails
end
module VERSION
- MAJOR = 5
+ MAJOR = 6
MINOR = 0
TINY = 0
PRE = "alpha"